59d76b027f4e403f6073bfceff3234daadee6927
[nit.git] / src / nitcatalog.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Basic catalog generator for Nit packages
16 #
17 # See: <http://nitlanguage.org/catalog/>
18 #
19 # The tool scans packages and generates the HTML files of a catalog.
20 #
21 # ## Features
22 #
23 # * [X] scan packages and their `.ini`
24 # * [X] generate lists of packages
25 # * [X] generate a page per package with the readme and most metadata
26 # * [ ] link/include/be included in the documentation
27 # * [ ] propose `related packages`
28 # * [X] show directory content (a la nitls)
29 # * [X] gather git information from the working directory
30 # * [ ] gather git information from the repository
31 # * [ ] gather package information from github
32 # * [ ] gather people information from github
33 # * [ ] reify people
34 # * [ ] separate information gathering from rendering
35 # * [ ] move up information gathering in (existing or new) service modules
36 # * [X] add command line options
37 # * [ ] harden HTML (escaping, path injection, etc)
38 # * [ ] nitcorn server with RESTful API
39 #
40 # ## Issues and limitations
41 #
42 # The tool works likee the other tools and expects to find valid Nit source code in the directories
43 #
44 # * cruft and temporary files will be collected
45 # * missing source file (e.g. not yet generated by nitcc) will make information
46 # incomplete (e.g. invalid module thus partial dependency and metrics)
47 #
48 # How to use the tool as the basis of a Nit code archive on the web usable with a package manager is not clear.
49 module nitcatalog
50
51 import loader # Scan&load packages, groups and modules
52 import doc::doc_down # Display mdoc
53 import md5 # To get gravatar images
54 import counter # For statistics
55 import modelize # To process and count classes and methods
56
57 redef class MPackage
58 # Return the associated metadata from the `ini`, if any
59 fun metadata(key: String): nullable String
60 do
61 var ini = self.ini
62 if ini == null then return null
63 return ini[key]
64 end
65
66 # The list of maintainers
67 var maintainers = new Array[String]
68
69 # The list of contributors
70 var contributors = new Array[String]
71
72 # The date of the most recent commit
73 var last_date: nullable String = null
74
75 # The date of the oldest commit
76 var first_date: nullable String = null
77 end
78
79 # A HTML page in a catalog
80 #
81 # This is just a template with the header pre-filled and the footer injected at rendering.
82 # Therefore, once instantiated, the content can just be added to it.
83 class CatalogPage
84 super Template
85
86 # Placeholder to include additional things before the `</head>`.
87 var more_head = new Template
88
89 # Relative path to the root directory (with the index file).
90 #
91 # Use "" for pages in the root directory
92 # Use ".." for pages in a subdirectory
93 var rootpath: String
94
95 redef init
96 do
97 add """
98 <!DOCTYPE html>
99 <html>
100 <head>
101 <meta charset="utf-8">
102 <link rel="stylesheet" media="all" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
103 <link rel="stylesheet" media="all" href="{{{rootpath / "style.css"}}}">
104 """
105 add more_head
106
107 add """
108 </head>
109 <body>
110 <div class='container-fluid'>
111 <div class='row'>
112 <nav id='topmenu' class='navbar navbar-default navbar-fixed-top' role='navigation'>
113 <div class='container-fluid'>
114 <div class='navbar-header'>
115 <button type='button' class='navbar-toggle' data-toggle='collapse' data-target='#topmenu-collapse'>
116 <span class='sr-only'>Toggle menu</span>
117 <span class='icon-bar'></span>
118 <span class='icon-bar'></span>
119 <span class='icon-bar'></span>
120 </button>
121 <span class='navbar-brand'><a href="http://nitlanguage.org/">Nitlanguage.org</a></span>
122 </div>
123 <div class='collapse navbar-collapse' id='topmenu-collapse'>
124 <ul class='nav navbar-nav'>
125 <li><a href="{{{rootpath / "index.html"}}}">Catalog</a></li>
126 </ul>
127 </div>
128 </div>
129 </nav>
130 </div>
131 """
132 end
133
134 redef fun rendering
135 do
136 add """
137 </div> <!-- container-fluid -->
138 <script src='https://code.jquery.com/jquery-latest.min.js'></script>
139 <script src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js'></script>
140 <script src='https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.8.1/bootstrap-table-all.min.js'></script>
141 </body>
142 </html>
143 """
144 end
145 end
146
147 redef class Int
148 # Returns `log(self+1)`. Used to compute score of packages
149 fun score: Float do return (self+1).to_f.log
150 end
151
152 # The main class of the calatog generator that has the knowledge
153 class Catalog
154
155 # The modelbuilder
156 # used to access the files and count source lines of code
157 var modelbuilder: ModelBuilder
158
159 # Packages by tag
160 var tag2proj = new MultiHashMap[String, MPackage]
161
162 # Packages by category
163 var cat2proj = new MultiHashMap[String, MPackage]
164
165 # Packages by maintainer
166 var maint2proj = new MultiHashMap[String, MPackage]
167
168 # Packages by contributors
169 var contrib2proj = new MultiHashMap[String, MPackage]
170
171 # Dependency between packages
172 var deps = new POSet[MPackage]
173
174 # Number of modules by package
175 var mmodules = new Counter[MPackage]
176
177 # Number of classes by package
178 var mclasses = new Counter[MPackage]
179
180 # Number of methods by package
181 var mmethods = new Counter[MPackage]
182
183 # Number of line of code by package
184 var loc = new Counter[MPackage]
185
186 # Number of commits by package
187 var commits = new Counter[MPackage]
188
189 # Score by package
190 #
191 # The score is loosely computed using other metrics
192 var score = new Counter[MPackage]
193
194 # Scan, register and add a contributor to a package
195 fun add_contrib(person: String, mpackage: MPackage, res: Template)
196 do
197 var projs = contrib2proj[person]
198 if not projs.has(mpackage) then projs.add mpackage
199 var name = person
200 var email = null
201 var page = null
202
203 # Regular expressions are broken, need to investigate.
204 # So split manually.
205 #
206 #var re = "([^<(]*?)(<([^>]*?)>)?(\\((.*)\\))?".to_re
207 #var m = (person+" ").search(re)
208 #print "{person}: `{m or else "?"}` `{m[1] or else "?"}` `{m[3] or else "?"}` `{m[5] or else "?"}`"
209 do
210 var sp1 = person.split_once_on("<")
211 if sp1.length < 2 then
212 break
213 end
214 var sp2 = sp1.last.split_once_on(">")
215 if sp2.length < 2 then
216 break
217 end
218 name = sp1.first.trim
219 email = sp2.first.trim
220 var sp3 = sp2.last.split_once_on("(")
221 if sp3.length < 2 then
222 break
223 end
224 var sp4 = sp3.last.split_once_on(")")
225 if sp4.length < 2 then
226 break
227 end
228 page = sp4.first.trim
229 end
230
231 var e = name.html_escape
232 res.add "<li>"
233 if page != null then
234 res.add "<a href=\"{page.html_escape}\">"
235 end
236 if email != null then
237 # TODO get more things from github by using the email as a key
238 # "https://api.github.com/search/users?q={email}+in:email"
239 var md5 = email.md5.to_lower
240 res.add "<img src=\"https://secure.gravatar.com/avatar/{md5}?size=20&amp;default=retro\">&nbsp;"
241 end
242 res.add "{e}"
243 if page != null then res.add "</a>"
244 res.add "</li>"
245 end
246
247 # Recursively generate a level in the file tree of the *content* section
248 private fun gen_content_level(ot: OrderedTree[Object], os: Array[Object], res: Template)
249 do
250 res.add "<ul>\n"
251 for o in os do
252 res.add "<li>"
253 if o isa MGroup then
254 var d = ""
255 var mdoc = o.mdoc
256 if mdoc != null then d = ": {mdoc.html_synopsis.write_to_string}"
257 res.add "<strong>{o.name}</strong>{d} ({o.filepath.to_s})"
258 else if o isa ModulePath then
259 var d = ""
260 var m = o.mmodule
261 if m != null then
262 var mdoc = m.mdoc
263 if mdoc != null then d = ": {mdoc.html_synopsis.write_to_string}"
264 end
265 res.add "<strong>{o.name}</strong>{d} ({o.filepath.to_s})"
266 else
267 abort
268 end
269 var subs = ot.sub.get_or_null(o)
270 if subs != null then gen_content_level(ot, subs, res)
271 res.add "</li>\n"
272 end
273 res.add "</ul>\n"
274 end
275
276 # Compute information and generate a full HTML page for a package
277 fun package_page(mpackage: MPackage): Writable
278 do
279 var res = new CatalogPage("..")
280 var score = score[mpackage].to_f
281 var name = mpackage.name.html_escape
282 res.more_head.add """<title>{{{name}}}</title>"""
283
284 res.add """
285 <div class="content">
286 <h1 class="package-name">{{{name}}}</h1>
287 """
288 var mdoc = mpackage.mdoc_or_fallback
289 if mdoc != null then
290 score += 100.0
291 res.add mdoc.html_documentation
292 score += mdoc.content.length.score
293 end
294
295 res.add "<h2>Content</h2>"
296 var ot = new OrderedTree[Object]
297 for g in mpackage.mgroups do
298 var pa = g.parent
299 if g.is_interesting then
300 ot.add(pa, g)
301 pa = g
302 end
303 for mp in g.module_paths do
304 ot.add(pa, mp)
305 end
306 end
307 ot.sort_with(alpha_comparator)
308 gen_content_level(ot, ot.roots, res)
309
310
311 res.add """
312 </div>
313 <div class="sidebar">
314 <ul class="box">
315 """
316 var tryit = mpackage.metadata("upstream.tryit")
317 if tryit != null then
318 score += 1.0
319 var e = tryit.html_escape
320 res.add "<li><a href=\"{e}\">Try<span style=\"color:white\">n</span>it!</a></li>\n"
321 end
322 var apk = mpackage.metadata("upstream.apk")
323 if apk != null then
324 score += 1.0
325 var e = apk.html_escape
326 res.add "<li><a href=\"{e}\">Android apk</a></li>\n"
327 end
328
329 res.add """</ul>\n<ul class="box">\n"""
330
331 var homepage = mpackage.metadata("upstream.homepage")
332 if homepage != null then
333 score += 5.0
334 var e = homepage.html_escape
335 res.add "<li><a href=\"{e}\">{e}</a></li>\n"
336 end
337 var maintainer = mpackage.metadata("package.maintainer")
338 if maintainer != null then
339 score += 5.0
340 add_contrib(maintainer, mpackage, res)
341 mpackage.maintainers.add maintainer
342 var projs = maint2proj[maintainer]
343 if not projs.has(mpackage) then projs.add mpackage
344 end
345 var license = mpackage.metadata("package.license")
346 if license != null then
347 score += 5.0
348 var e = license.html_escape
349 res.add "<li><a href=\"http://opensource.org/licenses/{e}\">{e}</a> license</li>\n"
350 end
351 res.add "</ul>\n"
352
353 res.add "<h3>Source Code</h3>\n<ul class=\"box\">\n"
354 var browse = mpackage.metadata("upstream.browse")
355 if browse != null then
356 score += 5.0
357 var e = browse.html_escape
358 res.add "<li><a href=\"{e}\">{e}</a></li>\n"
359 end
360 var git = mpackage.metadata("upstream.git")
361 if git != null then
362 var e = git.html_escape
363 res.add "<li><tt>{e}</tt></li>\n"
364 end
365 var last_date = mpackage.last_date
366 if last_date != null then
367 var e = last_date.html_escape
368 res.add "<li>most recent commit: {e}</li>\n"
369 end
370 var first_date = mpackage.first_date
371 if first_date != null then
372 var e = first_date.html_escape
373 res.add "<li>oldest commit: {e}</li>\n"
374 end
375 var commits = commits[mpackage]
376 if commits != 0 then
377 res.add "<li>{commits} commits</li>\n"
378 end
379 res.add "</ul>\n"
380
381 res.add "<h3>Tags</h3>\n"
382 var tags = mpackage.metadata("package.tags")
383 var ts = new Array[String]
384 if tags != null then
385 for t in tags.split(",") do
386 t = t.trim
387 if t == "" then continue
388 ts.add t
389 end
390 end
391 if ts.is_empty then ts.add "none"
392 if tryit != null then ts.add "tryit"
393 if apk != null then ts.add "apk"
394 var ts2 = new Array[String]
395 for t in ts do
396 tag2proj[t].add mpackage
397 t = t.html_escape
398 ts2.add "<a href=\"../index.html#tag_{t}\">{t}</a>"
399 end
400 res.add_list(ts2, ", ", ", ")
401 var cat = ts.first
402 cat2proj[cat].add mpackage
403 score += ts.length.score
404
405 if deps.has(mpackage) then
406 var reqs = deps[mpackage].greaters.to_a
407 reqs.remove(mpackage)
408 alpha_comparator.sort(reqs)
409 res.add "<h3>Requirements</h3>\n"
410 if reqs.is_empty then
411 res.add "none"
412 else
413 var list = new Array[String]
414 for r in reqs do
415 var direct = deps.has_direct_edge(mpackage, r)
416 var s = "<a href=\"{r}.html\">"
417 if direct then s += "<strong>"
418 s += r.to_s
419 if direct then s += "</strong>"
420 s += "</a>"
421 list.add s
422 end
423 res.add_list(list, ", ", " and ")
424 end
425
426 reqs = deps[mpackage].smallers.to_a
427 reqs.remove(mpackage)
428 alpha_comparator.sort(reqs)
429 res.add "<h3>Clients</h3>\n"
430 if reqs.is_empty then
431 res.add "none"
432 else
433 var list = new Array[String]
434 for r in reqs do
435 var direct = deps.has_direct_edge(r, mpackage)
436 var s = "<a href=\"{r}.html\">"
437 if direct then s += "<strong>"
438 s += r.to_s
439 if direct then s += "</strong>"
440 s += "</a>"
441 list.add s
442 end
443 res.add_list(list, ", ", " and ")
444 end
445
446 score += deps[mpackage].greaters.length.score
447 score += deps[mpackage].direct_greaters.length.score
448 score += deps[mpackage].smallers.length.score
449 score += deps[mpackage].direct_smallers.length.score
450 end
451
452 var contributors = mpackage.contributors
453 if not contributors.is_empty then
454 res.add "<h3>Contributors</h3>\n<ul class=\"box\">"
455 for c in contributors do
456 add_contrib(c, mpackage, res)
457 end
458 res.add "</ul>"
459 end
460 score += contributors.length.to_f
461
462 var mmodules = 0
463 var mclasses = 0
464 var mmethods = 0
465 var loc = 0
466 for g in mpackage.mgroups do
467 mmodules += g.module_paths.length
468 for m in g.mmodules do
469 var am = modelbuilder.mmodule2node(m)
470 if am != null then
471 var file = am.location.file
472 if file != null then
473 loc += file.line_starts.length - 1
474 end
475 end
476 for cd in m.mclassdefs do
477 mclasses += 1
478 for pd in cd.mpropdefs do
479 if not pd isa MMethodDef then continue
480 mmethods += 1
481 end
482 end
483 end
484 end
485 self.mmodules[mpackage] = mmodules
486 self.mclasses[mpackage] = mclasses
487 self.mmethods[mpackage] = mmethods
488 self.loc[mpackage] = loc
489
490 #score += mmodules.score
491 score += mclasses.score
492 score += mmethods.score
493 score += loc.score
494
495 res.add """
496 <h3>Stats</h3>
497 <ul class="box">
498 <li>{{{mmodules}}} modules</li>
499 <li>{{{mclasses}}} classes</li>
500 <li>{{{mmethods}}} methods</li>
501 <li>{{{loc}}} lines of code</li>
502 </ul>
503 """
504
505 res.add """
506 </div>
507 """
508 self.score[mpackage] = score.to_i
509
510 return res
511 end
512
513 # Return a short HTML sequence for a package
514 #
515 # Intended to use in lists.
516 fun li_package(p: MPackage): String
517 do
518 var res = ""
519 var f = "p/{p.name}.html"
520 res += "<a href=\"{f}\">{p}</a>"
521 var d = p.mdoc_or_fallback
522 if d != null then res += " - {d.html_synopsis.write_to_string}"
523 return res
524 end
525
526 # List packages by group.
527 #
528 # For each key of the `map` a `<h3>` is generated.
529 # Each package is then listed.
530 #
531 # The list of keys is generated first to allow fast access to the correct `<h3>`.
532 # `id_prefix` is used to give an id to the `<h3>` element.
533 fun list_by(map: MultiHashMap[String, MPackage], id_prefix: String): Template
534 do
535 var res = new Template
536 var keys = map.keys.to_a
537 alpha_comparator.sort(keys)
538 var list = [for x in keys do "<a href=\"#{id_prefix}{x.html_escape}\">{x.html_escape}</a>"]
539 res.add_list(list, ", ", " and ")
540
541 for k in keys do
542 var projs = map[k].to_a
543 alpha_comparator.sort(projs)
544 var e = k.html_escape
545 res.add "<h3 id=\"{id_prefix}{e}\">{e} ({projs.length})</h3>\n<ul>\n"
546 for p in projs do
547 res.add "<li>"
548 res.add li_package(p)
549 res.add "</li>"
550 end
551 res.add "</ul>"
552 end
553 return res
554 end
555
556 # List the 10 best packages from `cpt`
557 fun list_best(cpt: Counter[MPackage]): Template
558 do
559 var res = new Template
560 res.add "<ul>"
561 var best = cpt.sort
562 for i in [1..10] do
563 if i > best.length then break
564 var p = best[best.length-i]
565 res.add "<li>"
566 res.add li_package(p)
567 # res.add " ({cpt[p]})"
568 res.add "</li>"
569 end
570 res.add "</ul>"
571 return res
572 end
573
574 # Collect more information on a package using the `git` tool.
575 fun git_info(mpackage: MPackage)
576 do
577 var ini = mpackage.ini
578 if ini == null then return
579
580 # TODO use real git info
581 #var repo = ini.get_or_null("upstream.git")
582 #var branch = ini.get_or_null("upstream.git.branch")
583 #var directory = ini.get_or_null("upstream.git.directory")
584
585 var dirpath = mpackage.root.filepath
586 if dirpath == null then return
587
588 # Collect commits info
589 var res = git_run("log", "--no-merges", "--follow", "--pretty=tformat:%ad;%aN <%aE>", "--", dirpath)
590 var contributors = new Counter[String]
591 var commits = res.split("\n")
592 if commits.not_empty and commits.last == "" then commits.pop
593 self.commits[mpackage] = commits.length
594 for l in commits do
595 var s = l.split_once_on(';')
596 if s.length != 2 or s.last == "" then continue
597
598 # Collect date of last and first commit
599 if mpackage.last_date == null then mpackage.last_date = s.first
600 mpackage.first_date = s.first
601
602 # Count contributors
603 contributors.inc(s.last)
604 end
605 for c in contributors.sort.reverse_iterator do
606 mpackage.contributors.add c
607 end
608
609 end
610
611 # Produce a HTML table containig information on the packages
612 #
613 # `package_page` must have been called before so that information is computed.
614 fun table_packages(mpackages: Array[MPackage]): Template
615 do
616 alpha_comparator.sort(mpackages)
617 var res = new Template
618 res.add "<table data-toggle=\"table\" data-sort-name=\"name\" data-sort-order=\"desc\" width=\"100%\">\n"
619 res.add "<thead><tr>\n"
620 res.add "<th data-field=\"name\" data-sortable=\"true\">name</th>\n"
621 res.add "<th data-field=\"maint\" data-sortable=\"true\">maint</th>\n"
622 res.add "<th data-field=\"contrib\" data-sortable=\"true\">contrib</th>\n"
623 if deps.not_empty then
624 res.add "<th data-field=\"reqs\" data-sortable=\"true\">reqs</th>\n"
625 res.add "<th data-field=\"dreqs\" data-sortable=\"true\">direct<br>reqs</th>\n"
626 res.add "<th data-field=\"cli\" data-sortable=\"true\">clients</th>\n"
627 res.add "<th data-field=\"dcli\" data-sortable=\"true\">direct<br>clients</th>\n"
628 end
629 res.add "<th data-field=\"mod\" data-sortable=\"true\">modules</th>\n"
630 res.add "<th data-field=\"cla\" data-sortable=\"true\">classes</th>\n"
631 res.add "<th data-field=\"met\" data-sortable=\"true\">methods</th>\n"
632 res.add "<th data-field=\"loc\" data-sortable=\"true\">lines</th>\n"
633 res.add "<th data-field=\"score\" data-sortable=\"true\">score</th>\n"
634 res.add "</tr></thead>"
635 for p in mpackages do
636 res.add "<tr>"
637 res.add "<td><a href=\"p/{p.name}.html\">{p.name}</a></td>"
638 var maint = "?"
639 if p.maintainers.not_empty then maint = p.maintainers.first
640 res.add "<td>{maint}</td>"
641 res.add "<td>{p.contributors.length}</td>"
642 if deps.not_empty then
643 res.add "<td>{deps[p].greaters.length-1}</td>"
644 res.add "<td>{deps[p].direct_greaters.length}</td>"
645 res.add "<td>{deps[p].smallers.length-1}</td>"
646 res.add "<td>{deps[p].direct_smallers.length}</td>"
647 end
648 res.add "<td>{mmodules[p]}</td>"
649 res.add "<td>{mclasses[p]}</td>"
650 res.add "<td>{mmethods[p]}</td>"
651 res.add "<td>{loc[p]}</td>"
652 res.add "<td>{score[p]}</td>"
653 res.add "</tr>\n"
654 end
655 res.add "</table>\n"
656 return res
657 end
658 end
659
660 # Execute a git command and return the result
661 fun git_run(command: String...): String
662 do
663 # print "git {command.join(" ")}"
664 var p = new ProcessReader("git", command...)
665 var res = p.read_all
666 p.close
667 p.wait
668 return res
669 end
670
671 var model = new Model
672 var tc = new ToolContext
673
674 var opt_dir = new OptionString("Directory where the HTML files are generated", "-d", "--dir")
675 var opt_no_git = new OptionBool("Do not gather git information from the working directory", "--no-git")
676 var opt_no_parse = new OptionBool("Do not parse nit files (no importation information)", "--no-parse")
677 var opt_no_model = new OptionBool("Do not analyse nit files (no class/method information)", "--no-model")
678
679 tc.option_context.add_option(opt_dir, opt_no_git, opt_no_parse, opt_no_model)
680
681 tc.process_options(sys.args)
682 tc.keep_going = true
683
684 var modelbuilder = new ModelBuilder(model, tc)
685 var catalog = new Catalog(modelbuilder)
686
687 # Get files or groups
688 for a in tc.option_context.rest do
689 modelbuilder.get_mgroup(a)
690 modelbuilder.identify_file(a)
691 end
692
693 # Scan packages and compute information
694 for p in model.mpackages do
695 var g = p.root
696 assert g != null
697 modelbuilder.scan_group(g)
698
699 # Load the module to process importation information
700 if opt_no_parse.value then continue
701 modelbuilder.parse_group(g)
702
703 catalog.deps.add_node(p)
704 for gg in p.mgroups do for m in gg.mmodules do
705 for im in m.in_importation.direct_greaters do
706 var ip = im.mpackage
707 if ip == null or ip == p then continue
708 catalog.deps.add_edge(p, ip)
709 end
710 end
711 end
712
713 if not opt_no_git.value then for p in model.mpackages do
714 catalog.git_info(p)
715 end
716
717 # Run phases to modelize classes and properties (so we can count them)
718 if not opt_no_model.value then
719 modelbuilder.run_phases
720 end
721
722 var out = opt_dir.value or else "catalog.out"
723 (out/"p").mkdir
724
725 # Generate the css (hard coded)
726 var css = """
727 body {
728 margin-top: 15px;
729 background-color: #f8f8f8;
730 }
731
732 a {
733 color: #0D8921;
734 text-decoration: none;
735 }
736
737 a:hover {
738 color: #333;
739 text-decoration: none;
740 }
741
742 h1 {
743 font-weight: bold;
744 color: #0D8921;
745 font-size: 22px;
746 }
747
748 h2 {
749 color: #6C6C6C;
750 font-size: 18px;
751 border-bottom: solid 3px #CCC;
752 }
753
754 h3 {
755 color: #6C6C6C;
756 font-size: 15px;
757 border-bottom: solid 1px #CCC;
758 }
759
760 ul {
761 list-style-type: square;
762 }
763
764 dd {
765 color: #6C6C6C;
766 margin-top: 1em;
767 margin-bottom: 1em;
768 }
769
770 pre {
771 border: 1px solid #CCC;
772 font-family: Monospace;
773 color: #2d5003;
774 background-color: rgb(250, 250, 250);
775 }
776
777 code {
778 font-family: Monospace;
779 color: #2d5003;
780 }
781
782 footer {
783 margin-top: 20px;
784 }
785
786 .container {
787 margin: 0 auto;
788 padding: 0 20px;
789 }
790
791 .content {
792 float: left;
793 margin-top: 40px;
794 width: 65%;
795 }
796
797 .sidebar {
798 float: right;
799 margin-top: 40px;
800 width: 30%
801 }
802
803 .sidebar h3 {
804 color: #0D8921;
805 font-size: 18px;
806 border-bottom: 0px;
807 }
808
809 .box {
810 margin: 0;
811 padding: 0;
812 }
813
814 .box li {
815 line-height: 2.5;
816 white-space: nowrap;
817 overflow: hidden;
818 text-overflow: ellipsis;
819 padding-right: 10px;
820 border-bottom: 1px solid rgba(0,0,0,0.2);
821 }
822 """
823 css.write_to_file(out/"style.css")
824
825 # PAGES
826
827 for p in model.mpackages do
828 # print p
829 var f = "p/{p.name}.html"
830 catalog.package_page(p).write_to_file(out/f)
831 end
832
833 # INDEX
834
835 var index = new CatalogPage("")
836 index.more_head.add "<title>Packages in Nit</title>"
837
838 index.add """
839 <div class="content">
840 <h1>Packages in Nit</h1>
841 """
842
843 index.add "<h2>Highlighted Packages</h2>\n"
844 index.add catalog.list_best(catalog.score)
845
846 if catalog.deps.not_empty then
847 index.add "<h2>Most Required</h2>\n"
848 var reqs = new Counter[MPackage]
849 for p in model.mpackages do
850 reqs[p] = catalog.deps[p].smallers.length - 1
851 end
852 index.add catalog.list_best(reqs)
853 end
854
855 index.add "<h2>By First Tag</h2>\n"
856 index.add catalog.list_by(catalog.cat2proj, "cat_")
857
858 index.add "<h2>By Any Tag</h2>\n"
859 index.add catalog.list_by(catalog.tag2proj, "tag_")
860
861 index.add """
862 </div>
863 <div class="sidebar">
864 <h3>Stats</h3>
865 <ul class="box">
866 <li>{{{model.mpackages.length}}} packages</li>
867 <li>{{{catalog.maint2proj.length}}} maintainers</li>
868 <li>{{{catalog.contrib2proj.length}}} contributors</li>
869 <li>{{{catalog.tag2proj.length}}} tags</li>
870 <li>{{{catalog.mmodules.sum}}} modules</li>
871 <li>{{{catalog.mclasses.sum}}} classes</li>
872 <li>{{{catalog.mmethods.sum}}} methods</li>
873 <li>{{{catalog.loc.sum}}} lines of code</li>
874 </ul>
875 </div>
876 """
877
878 index.write_to_file(out/"index.html")
879
880 # PEOPLE
881
882 var page = new CatalogPage("")
883 page.more_head.add "<title>People of Nit</title>"
884 page.add """<div class="content">\n<h1>People of Nit</h1>\n"""
885 page.add "<h2>By Maintainer</h2>\n"
886 page.add catalog.list_by(catalog.maint2proj, "maint_")
887 page.add "<h2>By Contributor</h2>\n"
888 page.add catalog.list_by(catalog.contrib2proj, "contrib_")
889 page.add "</div>\n"
890 page.write_to_file(out/"people.html")
891
892 # TABLE
893
894 page = new CatalogPage("")
895 page.more_head.add "<title>Projets of Nit</title>"
896 page.add """<div class="content">\n<h1>People of Nit</h1>\n"""
897 page.add "<h2>Table of Projets</h2>\n"
898 page.add catalog.table_packages(model.mpackages)
899 page.add "</div>\n"
900 page.write_to_file(out/"table.html")