nitcatalog: add option in inject piwik tracker
[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 if not contributors.is_empty then
495 res.add "<h3>Contributors</h3>\n<ul class=\"box\">"
496 for c in contributors do
497 add_contrib(c, mpackage, res)
498 end
499 res.add "</ul>"
500 end
501 score += contributors.length.to_f
502
503 var mmodules = 0
504 var mclasses = 0
505 var mmethods = 0
506 var loc = 0
507 for g in mpackage.mgroups do
508 mmodules += g.mmodules.length
509 for m in g.mmodules do
510 var am = modelbuilder.mmodule2node(m)
511 if am != null then
512 var file = am.location.file
513 if file != null then
514 loc += file.line_starts.length - 1
515 end
516 end
517 for cd in m.mclassdefs do
518 mclasses += 1
519 for pd in cd.mpropdefs do
520 if not pd isa MMethodDef then continue
521 mmethods += 1
522 end
523 end
524 end
525 end
526 self.mmodules[mpackage] = mmodules
527 self.mclasses[mpackage] = mclasses
528 self.mmethods[mpackage] = mmethods
529 self.loc[mpackage] = loc
530
531 #score += mmodules.score
532 score += mclasses.score
533 score += mmethods.score
534 score += loc.score
535
536 res.add """
537 <h3>Stats</h3>
538 <ul class="box">
539 <li>{{{mmodules}}} modules</li>
540 <li>{{{mclasses}}} classes</li>
541 <li>{{{mmethods}}} methods</li>
542 <li>{{{loc}}} lines of code</li>
543 </ul>
544 """
545
546 res.add """
547 </div>
548 """
549 self.score[mpackage] = score.to_i
550
551 return res
552 end
553
554 # Return a short HTML sequence for a package
555 #
556 # Intended to use in lists.
557 fun li_package(p: MPackage): String
558 do
559 var res = ""
560 var f = "p/{p.name}.html"
561 res += "<a href=\"{f}\">{p}</a>"
562 var d = p.mdoc_or_fallback
563 if d != null then res += " - {d.html_synopsis.write_to_string}"
564 return res
565 end
566
567 # List packages by group.
568 #
569 # For each key of the `map` a `<h3>` is generated.
570 # Each package is then listed.
571 #
572 # The list of keys is generated first to allow fast access to the correct `<h3>`.
573 # `id_prefix` is used to give an id to the `<h3>` element.
574 fun list_by(map: MultiHashMap[String, MPackage], id_prefix: String): Template
575 do
576 var res = new Template
577 var keys = map.keys.to_a
578 alpha_comparator.sort(keys)
579 var list = [for x in keys do "<a href=\"#{id_prefix}{x.html_escape}\">{x.html_escape}</a>"]
580 res.add_list(list, ", ", " and ")
581
582 for k in keys do
583 var projs = map[k].to_a
584 alpha_comparator.sort(projs)
585 var e = k.html_escape
586 res.add "<h3 id=\"{id_prefix}{e}\">{e} ({projs.length})</h3>\n<ul>\n"
587 for p in projs do
588 res.add "<li>"
589 res.add li_package(p)
590 res.add "</li>"
591 end
592 res.add "</ul>"
593 end
594 return res
595 end
596
597 # List the 10 best packages from `cpt`
598 fun list_best(cpt: Counter[MPackage]): Template
599 do
600 var res = new Template
601 res.add "<ul>"
602 var best = cpt.sort
603 for i in [1..10] do
604 if i > best.length then break
605 var p = best[best.length-i]
606 res.add "<li>"
607 res.add li_package(p)
608 # res.add " ({cpt[p]})"
609 res.add "</li>"
610 end
611 res.add "</ul>"
612 return res
613 end
614
615 # Collect more information on a package using the `git` tool.
616 fun git_info(mpackage: MPackage)
617 do
618 var ini = mpackage.ini
619 if ini == null then return
620
621 # TODO use real git info
622 #var repo = ini.get_or_null("upstream.git")
623 #var branch = ini.get_or_null("upstream.git.branch")
624 #var directory = ini.get_or_null("upstream.git.directory")
625
626 var dirpath = mpackage.root.filepath
627 if dirpath == null then return
628
629 # Collect commits info
630 var res = git_run("log", "--no-merges", "--follow", "--pretty=tformat:%ad;%aN <%aE>", "--", dirpath)
631 var contributors = new Counter[String]
632 var commits = res.split("\n")
633 if commits.not_empty and commits.last == "" then commits.pop
634 self.commits[mpackage] = commits.length
635 for l in commits do
636 var s = l.split_once_on(';')
637 if s.length != 2 or s.last == "" then continue
638
639 # Collect date of last and first commit
640 if mpackage.last_date == null then mpackage.last_date = s.first
641 mpackage.first_date = s.first
642
643 # Count contributors
644 contributors.inc(s.last)
645 end
646 for c in contributors.sort.reverse_iterator do
647 mpackage.contributors.add c
648 end
649
650 end
651
652 # Produce a HTML table containig information on the packages
653 #
654 # `package_page` must have been called before so that information is computed.
655 fun table_packages(mpackages: Array[MPackage]): Template
656 do
657 alpha_comparator.sort(mpackages)
658 var res = new Template
659 res.add "<table data-toggle=\"table\" data-sort-name=\"name\" data-sort-order=\"desc\" width=\"100%\">\n"
660 res.add "<thead><tr>\n"
661 res.add "<th data-field=\"name\" data-sortable=\"true\">name</th>\n"
662 res.add "<th data-field=\"maint\" data-sortable=\"true\">maint</th>\n"
663 res.add "<th data-field=\"contrib\" data-sortable=\"true\">contrib</th>\n"
664 if deps.not_empty then
665 res.add "<th data-field=\"reqs\" data-sortable=\"true\">reqs</th>\n"
666 res.add "<th data-field=\"dreqs\" data-sortable=\"true\">direct<br>reqs</th>\n"
667 res.add "<th data-field=\"cli\" data-sortable=\"true\">clients</th>\n"
668 res.add "<th data-field=\"dcli\" data-sortable=\"true\">direct<br>clients</th>\n"
669 end
670 res.add "<th data-field=\"mod\" data-sortable=\"true\">modules</th>\n"
671 res.add "<th data-field=\"cla\" data-sortable=\"true\">classes</th>\n"
672 res.add "<th data-field=\"met\" data-sortable=\"true\">methods</th>\n"
673 res.add "<th data-field=\"loc\" data-sortable=\"true\">lines</th>\n"
674 res.add "<th data-field=\"score\" data-sortable=\"true\">score</th>\n"
675 res.add "</tr></thead>"
676 for p in mpackages do
677 res.add "<tr>"
678 res.add "<td><a href=\"p/{p.name}.html\">{p.name}</a></td>"
679 var maint = "?"
680 if p.maintainers.not_empty then maint = p.maintainers.first
681 res.add "<td>{maint}</td>"
682 res.add "<td>{p.contributors.length}</td>"
683 if deps.not_empty then
684 res.add "<td>{deps[p].greaters.length-1}</td>"
685 res.add "<td>{deps[p].direct_greaters.length}</td>"
686 res.add "<td>{deps[p].smallers.length-1}</td>"
687 res.add "<td>{deps[p].direct_smallers.length}</td>"
688 end
689 res.add "<td>{mmodules[p]}</td>"
690 res.add "<td>{mclasses[p]}</td>"
691 res.add "<td>{mmethods[p]}</td>"
692 res.add "<td>{loc[p]}</td>"
693 res.add "<td>{score[p]}</td>"
694 res.add "</tr>\n"
695 end
696 res.add "</table>\n"
697 return res
698 end
699
700 # Piwik tracker URL, if any
701 var piwik_tracker: nullable String = null
702
703 # Piwik site ID
704 # Used when `piwik_tracker` is set
705 var piwik_site_id: Int = 1
706 end
707
708 # Execute a git command and return the result
709 fun git_run(command: String...): String
710 do
711 # print "git {command.join(" ")}"
712 var p = new ProcessReader("git", command...)
713 var res = p.read_all
714 p.close
715 p.wait
716 return res
717 end
718
719 var model = new Model
720 var tc = new ToolContext
721
722 var opt_dir = new OptionString("Directory where the HTML files are generated", "-d", "--dir")
723 var opt_no_git = new OptionBool("Do not gather git information from the working directory", "--no-git")
724 var opt_no_parse = new OptionBool("Do not parse nit files (no importation information)", "--no-parse")
725 var opt_no_model = new OptionBool("Do not analyse nit files (no class/method information)", "--no-model")
726
727 # Piwik tracker URL.
728 # If you want to monitor your visitors.
729 var opt_piwik_tracker = new OptionString("Piwik tracker URL (ex: `nitlanguage.org/piwik/`)", "--piwik-tracker")
730 # Piwik tracker site id.
731 var opt_piwik_site_id = new OptionString("Piwik site ID", "--piwik-site-id")
732
733 tc.option_context.add_option(opt_dir, opt_no_git, opt_no_parse, opt_no_model, opt_piwik_tracker, opt_piwik_site_id)
734
735 tc.process_options(sys.args)
736 tc.keep_going = true
737
738 var modelbuilder = new ModelBuilder(model, tc)
739 var catalog = new Catalog(modelbuilder)
740
741 catalog.piwik_tracker = opt_piwik_tracker.value
742 var piwik_site_id = opt_piwik_site_id.value
743 if piwik_site_id != null then
744 if catalog.piwik_tracker == null then
745 print_error "Warning: ignored `{opt_piwik_site_id}` because `{opt_piwik_tracker}` is not set."
746 else if piwik_site_id.is_int then
747 print_error "Warning: ignored `{opt_piwik_site_id}`, an integer is required."
748 else
749 catalog.piwik_site_id = piwik_site_id.to_i
750 end
751 end
752
753
754 # Get files or groups
755 var args = tc.option_context.rest
756 if opt_no_parse.value then
757 modelbuilder.scan_full(args)
758 else
759 modelbuilder.parse_full(args)
760 end
761
762 # Scan packages and compute information
763 for p in model.mpackages do
764 var g = p.root
765 assert g != null
766 modelbuilder.scan_group(g)
767
768 # Load the module to process importation information
769 if opt_no_parse.value then continue
770
771 catalog.deps.add_node(p)
772 for gg in p.mgroups do for m in gg.mmodules do
773 for im in m.in_importation.direct_greaters do
774 var ip = im.mpackage
775 if ip == null or ip == p then continue
776 catalog.deps.add_edge(p, ip)
777 end
778 end
779 end
780
781 if not opt_no_git.value then for p in model.mpackages do
782 catalog.git_info(p)
783 end
784
785 # Run phases to modelize classes and properties (so we can count them)
786 if not opt_no_model.value then
787 modelbuilder.run_phases
788 end
789
790 var out = opt_dir.value or else "catalog.out"
791 (out/"p").mkdir
792
793 # Generate the css (hard coded)
794 var css = """
795 body {
796 margin-top: 15px;
797 background-color: #f8f8f8;
798 }
799
800 a {
801 color: #0D8921;
802 text-decoration: none;
803 }
804
805 a:hover {
806 color: #333;
807 text-decoration: none;
808 }
809
810 h1 {
811 font-weight: bold;
812 color: #0D8921;
813 font-size: 22px;
814 }
815
816 h2 {
817 color: #6C6C6C;
818 font-size: 18px;
819 border-bottom: solid 3px #CCC;
820 }
821
822 h3 {
823 color: #6C6C6C;
824 font-size: 15px;
825 border-bottom: solid 1px #CCC;
826 }
827
828 ul {
829 list-style-type: square;
830 }
831
832 dd {
833 color: #6C6C6C;
834 margin-top: 1em;
835 margin-bottom: 1em;
836 }
837
838 pre {
839 border: 1px solid #CCC;
840 font-family: Monospace;
841 color: #2d5003;
842 background-color: rgb(250, 250, 250);
843 }
844
845 code {
846 font-family: Monospace;
847 color: #2d5003;
848 }
849
850 footer {
851 margin-top: 20px;
852 }
853
854 .container {
855 margin: 0 auto;
856 padding: 0 20px;
857 }
858
859 .content {
860 float: left;
861 margin-top: 40px;
862 width: 65%;
863 }
864
865 .sidebar {
866 float: right;
867 margin-top: 40px;
868 width: 30%
869 }
870
871 .sidebar h3 {
872 color: #0D8921;
873 font-size: 18px;
874 border-bottom: 0px;
875 }
876
877 .box {
878 margin: 0;
879 padding: 0;
880 }
881
882 .box li {
883 line-height: 2.5;
884 white-space: nowrap;
885 overflow: hidden;
886 text-overflow: ellipsis;
887 padding-right: 10px;
888 border-bottom: 1px solid rgba(0,0,0,0.2);
889 }
890 """
891 css.write_to_file(out/"style.css")
892
893 # PAGES
894
895 for p in model.mpackages do
896 # print p
897 var f = "p/{p.name}.html"
898 catalog.package_page(p).write_to_file(out/f)
899 end
900
901 # INDEX
902
903 var index = catalog.new_page("")
904 index.more_head.add "<title>Packages in Nit</title>"
905
906 index.add """
907 <div class="content">
908 <h1>Packages in Nit</h1>
909 """
910
911 index.add "<h2>Highlighted Packages</h2>\n"
912 index.add catalog.list_best(catalog.score)
913
914 if catalog.deps.not_empty then
915 index.add "<h2>Most Required</h2>\n"
916 var reqs = new Counter[MPackage]
917 for p in model.mpackages do
918 reqs[p] = catalog.deps[p].smallers.length - 1
919 end
920 index.add catalog.list_best(reqs)
921 end
922
923 index.add "<h2>By First Tag</h2>\n"
924 index.add catalog.list_by(catalog.cat2proj, "cat_")
925
926 index.add "<h2>By Any Tag</h2>\n"
927 index.add catalog.list_by(catalog.tag2proj, "tag_")
928
929 index.add """
930 </div>
931 <div class="sidebar">
932 <h3>Stats</h3>
933 <ul class="box">
934 <li>{{{model.mpackages.length}}} packages</li>
935 <li>{{{catalog.maint2proj.length}}} maintainers</li>
936 <li>{{{catalog.contrib2proj.length}}} contributors</li>
937 <li>{{{catalog.tag2proj.length}}} tags</li>
938 <li>{{{catalog.mmodules.sum}}} modules</li>
939 <li>{{{catalog.mclasses.sum}}} classes</li>
940 <li>{{{catalog.mmethods.sum}}} methods</li>
941 <li>{{{catalog.loc.sum}}} lines of code</li>
942 </ul>
943 </div>
944 """
945
946 index.write_to_file(out/"index.html")
947
948 # PEOPLE
949
950 var page = catalog.new_page("")
951 page.more_head.add "<title>People of Nit</title>"
952 page.add """<div class="content">\n<h1>People of Nit</h1>\n"""
953 page.add "<h2>By Maintainer</h2>\n"
954 page.add catalog.list_by(catalog.maint2proj, "maint_")
955 page.add "<h2>By Contributor</h2>\n"
956 page.add catalog.list_by(catalog.contrib2proj, "contrib_")
957 page.add "</div>\n"
958 page.write_to_file(out/"people.html")
959
960 # TABLE
961
962 page = catalog.new_page("")
963 page.more_head.add "<title>Projets of Nit</title>"
964 page.add """<div class="content">\n<h1>People of Nit</h1>\n"""
965 page.add "<h2>Table of Projets</h2>\n"
966 page.add catalog.table_packages(model.mpackages)
967 page.add "</div>\n"
968 page.write_to_file(out/"table.html")