nit: Added link to `CONTRIBUTING.md` from the README
[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 # See `catalog` for details
22 module nitcatalog
23
24 import loader # Scan&load packages, groups and modules
25 import doc::doc_down # Display mdoc
26 import catalog
27
28 # A HTML page in a catalog
29 #
30 # This is just a template with the header pre-filled and the footer injected at rendering.
31 # Therefore, once instantiated, the content can just be added to it.
32 class CatalogPage
33 super Template
34
35 # The associated catalog, used to groups options and other global data
36 var catalog: Catalog
37
38 # Placeholder to include additional things before the `</head>`.
39 var more_head = new Template
40
41 # Relative path to the root directory (with the index file).
42 #
43 # Use "" for pages in the root directory
44 # Use ".." for pages in a subdirectory
45 var rootpath: String
46
47 redef init
48 do
49 add """
50 <!DOCTYPE html>
51 <html>
52 <head>
53 <meta charset="utf-8">
54 <link rel="stylesheet" media="all" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
55 <link rel="stylesheet" media="all" href="{{{rootpath / "style.css"}}}">
56 """
57 add more_head
58
59 add """
60 </head>
61 <body>
62 <div class='container-fluid'>
63 <div class='row'>
64 <nav id='topmenu' class='navbar navbar-default navbar-fixed-top' role='navigation'>
65 <div class='container-fluid'>
66 <div class='navbar-header'>
67 <button type='button' class='navbar-toggle' data-toggle='collapse' data-target='#topmenu-collapse'>
68 <span class='sr-only'>Toggle menu</span>
69 <span class='icon-bar'></span>
70 <span class='icon-bar'></span>
71 <span class='icon-bar'></span>
72 </button>
73 <span class='navbar-brand'><a href="http://nitlanguage.org/">Nitlanguage.org</a></span>
74 </div>
75 <div class='collapse navbar-collapse' id='topmenu-collapse'>
76 <ul class='nav navbar-nav'>
77 <li><a href="{{{rootpath / "index.html"}}}">Catalog</a></li>
78 </ul>
79 </div>
80 </div>
81 </nav>
82 </div>
83 """
84 end
85
86 # Inject piwik HTML code if required
87 private fun add_piwik
88 do
89 var tracker_url = catalog.piwik_tracker
90 if tracker_url == null then return
91
92 var site_id = catalog.piwik_site_id
93
94 tracker_url = tracker_url.trim
95 if tracker_url.chars.last != '/' then tracker_url += "/"
96 add """
97 <!-- Piwik -->
98 <script type="text/javascript">
99 var _paq = _paq || [];
100 _paq.push(['trackPageView']);
101 _paq.push(['enableLinkTracking']);
102 (function() {
103 var u=(("https:" == document.location.protocol) ? "https" : "http") + "://{{{tracker_url.escape_to_c}}}";
104 _paq.push(['setTrackerUrl', u+'piwik.php']);
105 _paq.push(['setSiteId', {{{site_id}}}]);
106 var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; g.type='text/javascript';
107 g.defer=true; g.async=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
108 })();
109
110 </script>
111 <noscript><p><img src="http://{{{tracker_url.html_escape}}}piwik.php?idsite={{{site_id}}}" style="border:0;" alt="" /></p></noscript>
112 <!-- End Piwik Code -->
113 """
114
115 end
116
117 redef fun rendering
118 do
119 add """
120 </div> <!-- container-fluid -->
121 <script src='https://code.jquery.com/jquery-latest.min.js'></script>
122 <script src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js'></script>
123 <script src='https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.8.1/bootstrap-table-all.min.js'></script>
124 """
125 add_piwik
126 add """
127
128 </body>
129 </html>
130 """
131 end
132 end
133
134 redef class NitdocDecorator
135 redef fun add_image(v, link, name, comment)
136 do
137 # Keep absolute links as is
138 if link.has_prefix("http://") or link.has_prefix("https://") then
139 super
140 return
141 end
142
143 do
144 # Get the directory of the doc object to deal with the relative link
145 var mdoc = current_mdoc
146 if mdoc == null then break
147 var source = mdoc.location.file
148 if source == null then break
149 var path = source.filename
150 var stat = path.file_stat
151 if stat == null then break
152 if not stat.is_dir then path = path.dirname
153
154 # Get the full path to the local resource
155 var fulllink = path / link.to_s
156 stat = fulllink.file_stat
157 if stat == null then break
158
159 # Get a collision-free catalog name for the resource
160 var hash = fulllink.md5
161 var ext = fulllink.file_extension
162 if ext != null then hash = hash + "." + ext
163
164 # Copy the local resource in the resource directory of the catalog
165 var res = catalog.outdir / "res" / hash
166 fulllink.file_copy_to(res)
167
168 # Hijack the link in the html.
169 link = ".." / "res" / hash
170 super(v, link, name, comment)
171 return
172 end
173
174 # Something went bad
175 catalog.modelbuilder.toolcontext.error(current_mdoc.location, "Error: cannot find local image `{link}`")
176 super
177 end
178
179 # The registered catalog
180 #
181 # It is used to deal with relative links in images.
182 var catalog: Catalog is noautoinit
183 end
184
185 redef class Catalog
186 redef init
187 do
188 # Register `self` to the global NitdocDecorator
189 # FIXME this is ugly. But no better idea at the moment.
190 modelbuilder.model.nitdoc_md_processor.emitter.decorator.as(NitdocDecorator).catalog = self
191 end
192
193 # The output directory where to generate pages
194 var outdir: String is noautoinit
195
196 # Return a empty `CatalogPage`.
197 fun new_page(rootpath: String): CatalogPage
198 do
199 return new CatalogPage(self, rootpath)
200 end
201
202 # Recursively generate a level in the file tree of the *content* section
203 private fun gen_content_level(ot: OrderedTree[MConcern], os: Array[Object], res: Template)
204 do
205 res.add "<ul>\n"
206 for o in os do
207 res.add "<li>"
208 if o isa MGroup then
209 var d = ""
210 var mdoc = o.mdoc
211 if mdoc != null then d = ": {mdoc.html_synopsis.write_to_string}"
212 res.add "<strong>{o.name}</strong>{d} ({o.filepath.to_s})"
213 else if o isa MModule then
214 var d = ""
215 var mdoc = o.mdoc
216 if mdoc != null then d = ": {mdoc.html_synopsis.write_to_string}"
217 res.add "<strong>{o.name}</strong>{d} ({o.filepath.to_s})"
218 else
219 abort
220 end
221 var subs = ot.sub.get_or_null(o)
222 if subs != null then gen_content_level(ot, subs, res)
223 res.add "</li>\n"
224 end
225 res.add "</ul>\n"
226 end
227
228 # Generate a full HTML page for a package
229 fun generate_page(mpackage: MPackage): Writable
230 do
231 var res = new_page("..")
232 var name = mpackage.name.html_escape
233 res.more_head.add """<title>{{{name}}}</title>"""
234
235 res.add """
236 <div class="content">
237 <h1 class="package-name">{{{name}}}</h1>
238 """
239 var mdoc = mpackage.mdoc_or_fallback
240 if mdoc != null then res.add mdoc.html_documentation
241
242 res.add "<h2>Content</h2>"
243 var ot = new OrderedTree[MConcern]
244 for g in mpackage.mgroups do
245 var pa = g.parent
246 if g.is_interesting then
247 ot.add(pa, g)
248 pa = g
249 end
250 for mp in g.mmodules do
251 ot.add(pa, mp)
252 end
253 end
254 ot.sort_with(alpha_comparator)
255 gen_content_level(ot, ot.roots, res)
256
257
258 res.add """
259 </div>
260 <div class="sidebar">
261 <ul class="box">
262 """
263 var tryit = mpackage.metadata("upstream.tryit")
264 if tryit != null then
265 var e = tryit.html_escape
266 res.add "<li><a href=\"{e}\">Try<span style=\"color:white\">n</span>it!</a></li>\n"
267 end
268 var apk = mpackage.metadata("upstream.apk")
269 if apk != null then
270 var e = apk.html_escape
271 res.add "<li><a href=\"{e}\">Android apk</a></li>\n"
272 end
273
274 res.add """</ul>\n<ul class="box">\n"""
275
276 var homepage = mpackage.metadata("upstream.homepage")
277 if homepage != null then
278 var e = homepage.html_escape
279 res.add "<li><a href=\"{e}\">{e}</a></li>\n"
280 end
281 for maintainer in mpackage.maintainers do
282 res.add "<li>{maintainer.to_html}</li>"
283 end
284 var license = mpackage.metadata("package.license")
285 if license != null then
286 var e = license.html_escape
287 res.add "<li><a href=\"http://opensource.org/licenses/{e}\">{e}</a> license</li>\n"
288 end
289 res.add "</ul>\n"
290
291 res.add "<h3>Source Code</h3>\n<ul class=\"box\">\n"
292 var browse = mpackage.metadata("upstream.browse")
293 if browse != null then
294 var e = browse.html_escape
295 res.add "<li><a href=\"{e}\">{e}</a></li>\n"
296 end
297 var git = mpackage.metadata("upstream.git")
298 if git != null then
299 var e = git.html_escape
300 res.add "<li><tt>{e}</tt></li>\n"
301 end
302 var last_date = mpackage.last_date
303 if last_date != null then
304 var e = last_date.html_escape
305 res.add "<li>most recent commit: {e}</li>\n"
306 end
307 var first_date = mpackage.first_date
308 if first_date != null then
309 var e = first_date.html_escape
310 res.add "<li>oldest commit: {e}</li>\n"
311 end
312 var commits = commits[mpackage]
313 if commits != 0 then
314 res.add "<li>{commits} commits</li>\n"
315 end
316 res.add "</ul>\n"
317
318 res.add "<h3>Quality</h3>\n<ul class=\"box\">\n"
319 var errors = errors[mpackage]
320 if errors > 0 then
321 res.add "<li>{errors} errors</li>\n"
322 end
323 res.add "<li>{warnings[mpackage]} warnings ({warnings_per_kloc[mpackage]}/kloc)</li>\n"
324 res.add "<li>{documentation_score[mpackage]}% documented</li>\n"
325 res.add "</ul>\n"
326
327 res.add "<h3>Tags</h3>\n"
328 var ts2 = new Array[String]
329 for t in mpackage.tags do
330 t = t.html_escape
331 ts2.add "<a href=\"../index.html#tag_{t}\">{t}</a>"
332 end
333 res.add_list(ts2, ", ", ", ")
334
335 if deps.has(mpackage) then
336 var reqs = deps[mpackage].greaters.to_a
337 reqs.remove(mpackage)
338 alpha_comparator.sort(reqs)
339 res.add "<h3>Requirements</h3>\n"
340 if reqs.is_empty then
341 res.add "none"
342 else
343 var list = new Array[String]
344 for r in reqs do
345 var direct = deps.has_direct_edge(mpackage, r)
346 var s = "<a href=\"{r}.html\">"
347 if direct then s += "<strong>"
348 s += r.to_s
349 if direct then s += "</strong>"
350 s += "</a>"
351 list.add s
352 end
353 res.add_list(list, ", ", " and ")
354 end
355
356 reqs = deps[mpackage].smallers.to_a
357 reqs.remove(mpackage)
358 alpha_comparator.sort(reqs)
359 res.add "<h3>Clients</h3>\n"
360 if reqs.is_empty then
361 res.add "none"
362 else
363 var list = new Array[String]
364 for r in reqs do
365 var direct = deps.has_direct_edge(r, mpackage)
366 var s = "<a href=\"{r}.html\">"
367 if direct then s += "<strong>"
368 s += r.to_s
369 if direct then s += "</strong>"
370 s += "</a>"
371 list.add s
372 end
373 res.add_list(list, ", ", " and ")
374 end
375 end
376
377 var contributors = mpackage.contributors
378 if not contributors.is_empty then
379 res.add "<h3>Contributors</h3>\n<ul class=\"box\">"
380 for c in contributors do
381 res.add "<li>{c.to_html}</li>"
382 end
383 res.add "</ul>"
384 end
385
386 res.add """
387 <h3>Stats</h3>
388 <ul class="box">
389 <li>{{{mmodules[mpackage]}}} modules</li>
390 <li>{{{mclasses[mpackage]}}} classes</li>
391 <li>{{{mmethods[mpackage]}}} methods</li>
392 <li>{{{loc[mpackage]}}} lines of code</li>
393 </ul>
394 """
395
396 res.add """
397 </div>
398 """
399 return res
400 end
401
402 # Return a short HTML sequence for a package
403 #
404 # Intended to use in lists.
405 fun li_package(p: MPackage): String
406 do
407 var res = ""
408 var f = "p/{p.name}.html"
409 res += "<a href=\"{f}\">{p}</a>"
410 var d = p.mdoc_or_fallback
411 if d != null then res += " - {d.html_synopsis.write_to_string}"
412 return res
413 end
414
415 # List packages by group.
416 #
417 # For each key of the `map` a `<h3>` is generated.
418 # Each package is then listed.
419 #
420 # The list of keys is generated first to allow fast access to the correct `<h3>`.
421 # `id_prefix` is used to give an id to the `<h3>` element.
422 fun list_by(map: MultiHashMap[Object, MPackage], id_prefix: String): Template
423 do
424 var res = new Template
425 var keys = map.keys.to_a
426 alpha_comparator.sort(keys)
427 var list = [for x in keys do "<a href=\"#{id_prefix}{x.to_s.html_escape}\">{x.to_s.html_escape}</a>"]
428 res.add_list(list, ", ", " and ")
429
430 for k in keys do
431 var projs = map[k].to_a
432 alpha_comparator.sort(projs)
433 var e = k.to_s.html_escape
434 res.add "<h3 id=\"{id_prefix}{e}\">{e} ({projs.length})</h3>\n<ul>\n"
435 for p in projs do
436 res.add "<li>"
437 res.add li_package(p)
438 res.add "</li>"
439 end
440 res.add "</ul>"
441 end
442 return res
443 end
444
445 # List the 10 best packages from `cpt`
446 fun list_best(cpt: Counter[MPackage]): Template
447 do
448 var res = new Template
449 res.add "<ul>"
450 var best = cpt.sort
451 for i in [1..10] do
452 if i > best.length then break
453 var p = best[best.length-i]
454 res.add "<li>"
455 res.add li_package(p)
456 # res.add " ({cpt[p]})"
457 res.add "</li>"
458 end
459 res.add "</ul>"
460 return res
461 end
462
463 # Produce a HTML table containig information on the packages
464 #
465 # `package_page` must have been called before so that information is computed.
466 fun table_packages(mpackages: Array[MPackage]): Template
467 do
468 alpha_comparator.sort(mpackages)
469 var res = new Template
470 res.add "<table data-toggle=\"table\" data-sort-name=\"name\" data-sort-order=\"desc\" width=\"100%\">\n"
471 res.add "<thead><tr>\n"
472 res.add "<th data-field=\"name\" data-sortable=\"true\">name</th>\n"
473 res.add "<th data-field=\"maint\" data-sortable=\"true\">maint</th>\n"
474 res.add "<th data-field=\"contrib\" data-sortable=\"true\">contrib</th>\n"
475 if deps.not_empty then
476 res.add "<th data-field=\"reqs\" data-sortable=\"true\">reqs</th>\n"
477 res.add "<th data-field=\"dreqs\" data-sortable=\"true\">direct<br>reqs</th>\n"
478 res.add "<th data-field=\"cli\" data-sortable=\"true\">clients</th>\n"
479 res.add "<th data-field=\"dcli\" data-sortable=\"true\">direct<br>clients</th>\n"
480 end
481 res.add "<th data-field=\"mod\" data-sortable=\"true\">modules</th>\n"
482 res.add "<th data-field=\"cla\" data-sortable=\"true\">classes</th>\n"
483 res.add "<th data-field=\"met\" data-sortable=\"true\">methods</th>\n"
484 res.add "<th data-field=\"loc\" data-sortable=\"true\">lines</th>\n"
485 res.add "<th data-field=\"score\" data-sortable=\"true\">score</th>\n"
486 res.add "<th data-field=\"errors\" data-sortable=\"true\">errors</th>\n"
487 res.add "<th data-field=\"warnings\" data-sortable=\"true\">warnings</th>\n"
488 res.add "<th data-field=\"warnings_per_kloc\" data-sortable=\"true\">w/kloc</th>\n"
489 res.add "<th data-field=\"doc\" data-sortable=\"true\">doc</th>\n"
490 res.add "</tr></thead>"
491 for p in mpackages do
492 res.add "<tr>"
493 res.add "<td><a href=\"p/{p.name}.html\">{p.name}</a></td>"
494 var maint = "?"
495 if p.maintainers.not_empty then maint = p.maintainers.first.name.html_escape
496 res.add "<td>{maint}</td>"
497 res.add "<td>{p.contributors.length}</td>"
498 if deps.not_empty then
499 res.add "<td>{deps[p].greaters.length-1}</td>"
500 res.add "<td>{deps[p].direct_greaters.length}</td>"
501 res.add "<td>{deps[p].smallers.length-1}</td>"
502 res.add "<td>{deps[p].direct_smallers.length}</td>"
503 end
504 res.add "<td>{mmodules[p]}</td>"
505 res.add "<td>{mclasses[p]}</td>"
506 res.add "<td>{mmethods[p]}</td>"
507 res.add "<td>{loc[p]}</td>"
508 res.add "<td>{score[p]}</td>"
509 res.add "<td>{errors[p]}</td>"
510 res.add "<td>{warnings[p]}</td>"
511 res.add "<td>{warnings_per_kloc[p]}</td>"
512 res.add "<td>{documentation_score[p]}</td>"
513 res.add "</tr>\n"
514 end
515 res.add "</table>\n"
516 return res
517 end
518
519 # Piwik tracker URL, if any
520 var piwik_tracker: nullable String = null
521
522 # Piwik site ID
523 # Used when `piwik_tracker` is set
524 var piwik_site_id: Int = 1
525 end
526
527 var model = new Model
528 var tc = new ToolContext
529
530 var opt_dir = new OptionString("Directory where the HTML files are generated", "-d", "--dir")
531 var opt_no_git = new OptionBool("Do not gather git information from the working directory", "--no-git")
532 var opt_no_parse = new OptionBool("Do not parse nit files (no importation information)", "--no-parse")
533 var opt_no_model = new OptionBool("Do not analyse nit files (no class/method information)", "--no-model")
534
535 # Piwik tracker URL.
536 # If you want to monitor your visitors.
537 var opt_piwik_tracker = new OptionString("Piwik tracker URL (ex: `nitlanguage.org/piwik/`)", "--piwik-tracker")
538 # Piwik tracker site id.
539 var opt_piwik_site_id = new OptionString("Piwik site ID", "--piwik-site-id")
540
541 tc.option_context.add_option(opt_dir, opt_no_git, opt_no_parse, opt_no_model, opt_piwik_tracker, opt_piwik_site_id)
542
543 tc.process_options(sys.args)
544 tc.keep_going = true
545
546 var modelbuilder = new ModelBuilder(model, tc)
547 var catalog = new Catalog(modelbuilder)
548
549 catalog.piwik_tracker = opt_piwik_tracker.value
550 var piwik_site_id = opt_piwik_site_id.value
551 if piwik_site_id != null then
552 if catalog.piwik_tracker == null then
553 print_error "Warning: ignored `{opt_piwik_site_id}` because `{opt_piwik_tracker}` is not set."
554 else if piwik_site_id.is_int then
555 print_error "Warning: ignored `{opt_piwik_site_id}`, an integer is required."
556 else
557 catalog.piwik_site_id = piwik_site_id.to_i
558 end
559 end
560
561
562 # Get files or groups
563 var args = tc.option_context.rest
564 if opt_no_parse.value then
565 modelbuilder.scan_full(args)
566 else
567 modelbuilder.parse_full(args)
568 end
569
570 # Scan packages and compute information
571 for p in model.mpackages do
572 var g = p.root
573 assert g != null
574 modelbuilder.scan_group(g)
575
576 # Load the module to process importation information
577 if opt_no_parse.value then continue
578
579 catalog.deps.add_node(p)
580 for gg in p.mgroups do for m in gg.mmodules do
581 for im in m.in_importation.direct_greaters do
582 var ip = im.mpackage
583 if ip == null or ip == p then continue
584 catalog.deps.add_edge(p, ip)
585 end
586 end
587 end
588
589 if not opt_no_git.value then for p in model.mpackages do
590 catalog.git_info(p)
591 end
592
593 # Run phases to modelize classes and properties (so we can count them)
594 if not opt_no_model.value then
595 modelbuilder.run_phases
596 end
597
598 var out = opt_dir.value or else "catalog.out"
599 (out/"p").mkdir
600 (out/"res").mkdir
601
602 catalog.outdir = out
603
604 # Generate the css (hard coded)
605 var css = """
606 body {
607 margin-top: 15px;
608 background-color: #f8f8f8;
609 }
610
611 a {
612 color: #0D8921;
613 text-decoration: none;
614 }
615
616 a:hover {
617 color: #333;
618 text-decoration: none;
619 }
620
621 h1 {
622 font-weight: bold;
623 color: #0D8921;
624 font-size: 22px;
625 }
626
627 h2 {
628 color: #6C6C6C;
629 font-size: 18px;
630 border-bottom: solid 3px #CCC;
631 }
632
633 h3 {
634 color: #6C6C6C;
635 font-size: 15px;
636 border-bottom: solid 1px #CCC;
637 }
638
639 ul {
640 list-style-type: square;
641 }
642
643 dd {
644 color: #6C6C6C;
645 margin-top: 1em;
646 margin-bottom: 1em;
647 }
648
649 pre {
650 border: 1px solid #CCC;
651 font-family: Monospace;
652 color: #2d5003;
653 background-color: rgb(250, 250, 250);
654 }
655
656 code {
657 font-family: Monospace;
658 color: #2d5003;
659 }
660
661 footer {
662 margin-top: 20px;
663 }
664
665 .container {
666 margin: 0 auto;
667 padding: 0 20px;
668 }
669
670 .content {
671 float: left;
672 margin-top: 40px;
673 width: 65%;
674 }
675
676 .sidebar {
677 float: right;
678 margin-top: 40px;
679 width: 30%
680 }
681
682 .sidebar h3 {
683 color: #0D8921;
684 font-size: 18px;
685 border-bottom: 0px;
686 }
687
688 .box {
689 margin: 0;
690 padding: 0;
691 }
692
693 .box li {
694 line-height: 2.5;
695 white-space: nowrap;
696 overflow: hidden;
697 text-overflow: ellipsis;
698 padding-right: 10px;
699 border-bottom: 1px solid rgba(0,0,0,0.2);
700 }
701 """
702 css.write_to_file(out/"style.css")
703
704 # PAGES
705
706 for p in model.mpackages do
707 # print p
708 var f = "p/{p.name}.html"
709 catalog.package_page(p)
710 catalog.generate_page(p).write_to_file(out/f)
711 end
712
713 # INDEX
714
715 var index = catalog.new_page("")
716 index.more_head.add "<title>Packages in Nit</title>"
717
718 index.add """
719 <div class="content">
720 <h1>Packages in Nit</h1>
721 """
722
723 index.add "<h2>Highlighted Packages</h2>\n"
724 index.add catalog.list_best(catalog.score)
725
726 if catalog.deps.not_empty then
727 index.add "<h2>Most Required</h2>\n"
728 var reqs = new Counter[MPackage]
729 for p in model.mpackages do
730 reqs[p] = catalog.deps[p].smallers.length - 1
731 end
732 index.add catalog.list_best(reqs)
733 end
734
735 index.add "<h2>By First Tag</h2>\n"
736 index.add catalog.list_by(catalog.cat2proj, "cat_")
737
738 index.add "<h2>By Any Tag</h2>\n"
739 index.add catalog.list_by(catalog.tag2proj, "tag_")
740
741 index.add """
742 </div>
743 <div class="sidebar">
744 <h3>Stats</h3>
745 <ul class="box">
746 <li>{{{model.mpackages.length}}} packages</li>
747 <li>{{{catalog.maint2proj.length}}} maintainers</li>
748 <li>{{{catalog.contrib2proj.length}}} contributors</li>
749 <li>{{{catalog.tag2proj.length}}} tags</li>
750 <li>{{{catalog.mmodules.sum}}} modules</li>
751 <li>{{{catalog.mclasses.sum}}} classes</li>
752 <li>{{{catalog.mmethods.sum}}} methods</li>
753 <li>{{{catalog.loc.sum}}} lines of code</li>
754 </ul>
755 </div>
756 """
757
758 index.write_to_file(out/"index.html")
759
760 # PEOPLE
761
762 var page = catalog.new_page("")
763 page.more_head.add "<title>People of Nit</title>"
764 page.add """<div class="content">\n<h1>People of Nit</h1>\n"""
765 page.add "<h2>By Maintainer</h2>\n"
766 page.add catalog.list_by(catalog.maint2proj, "maint_")
767 page.add "<h2>By Contributor</h2>\n"
768 page.add catalog.list_by(catalog.contrib2proj, "contrib_")
769 page.add "</div>\n"
770 page.write_to_file(out/"people.html")
771
772 # TABLE
773
774 page = catalog.new_page("")
775 page.more_head.add "<title>Projets of Nit</title>"
776 page.add """<div class="content">\n<h1>People of Nit</h1>\n"""
777 page.add "<h2>Table of Projets</h2>\n"
778 page.add catalog.table_packages(model.mpackages)
779 page.add "</div>\n"
780 page.write_to_file(out/"table.html")