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