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