1 # This file is part of NIT ( http://www.nitlanguage.org ).
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
15 # Basic catalog generator for Nit packages
17 # See: <http://nitlanguage.org/catalog/>
19 # The tool scans packages and generates the HTML files of a catalog.
21 # See `catalog` for details
24 import loader
# Scan&load packages, groups and modules
27 import doc
::templates
::html_model
29 # A HTML page in a catalog
31 # This is just a template with the header pre-filled and the footer injected at rendering.
32 # Therefore, once instantiated, the content can just be added to it.
36 # The associated catalog, used to groups options and other global data
39 # Placeholder to include additional things before the `</head>`.
40 var more_head
= new Template
42 # Relative path to the root directory (with the index file).
44 # Use "" for pages in the root directory
45 # Use ".." for pages in a subdirectory
54 <meta charset="utf-8">
55 <link rel="stylesheet" media="all" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
56 <link rel="stylesheet" media="all" href="{{{rootpath / "style.css"}}}">
63 <div class='container-fluid'>
65 <nav id='topmenu' class='navbar navbar-default navbar-fixed-top' role='navigation'>
66 <div class='container-fluid'>
67 <div class='navbar-header'>
68 <button type='button' class='navbar-toggle' data-toggle='collapse' data-target='#topmenu-collapse'>
69 <span class='sr-only'>Toggle menu</span>
70 <span class='icon-bar'></span>
71 <span class='icon-bar'></span>
72 <span class='icon-bar'></span>
74 <span class='navbar-brand'><a href="http://nitlanguage.org/">Nitlanguage.org</a></span>
76 <div class='collapse navbar-collapse' id='topmenu-collapse'>
77 <ul class='nav navbar-nav'>
78 <li><a href="{{{rootpath / "index.html"}}}">Catalog</a></li>
87 # Inject piwik HTML code if required
90 var tracker_url
= catalog
.piwik_tracker
91 if tracker_url
== null then return
93 var site_id
= catalog
.piwik_site_id
95 tracker_url
= tracker_url
.trim
96 if tracker_url
.chars
.last
!= '/' then tracker_url
+= "/"
99 <script type="text/javascript">
100 var _paq = _paq || [];
101 _paq.push(['trackPageView']);
102 _paq.push(['enableLinkTracking']);
104 var u=(("https:" == document.location.protocol) ? "https" : "http") + "://{{{tracker_url.escape_to_c}}}";
105 _paq.push(['setTrackerUrl', u+'piwik.php']);
106 _paq.push(['setSiteId', {{{site_id}}}]);
107 var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; g.type='text/javascript';
108 g.defer=true; g.async=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
112 <noscript><p><img src="http://{{{tracker_url.html_escape}}}piwik.php?idsite={{{site_id}}}" style="border:0;" alt="" /></p></noscript>
113 <!-- End Piwik Code -->
121 </div> <!-- container-fluid -->
122 <script src='https://code.jquery.com/jquery-latest.min.js'></script>
123 <script src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js'></script>
124 <script src='https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.8.1/bootstrap-table-all.min.js'></script>
135 redef class NitdocDecorator
136 redef fun add_image
(v
, link
, name
, comment
)
138 # Keep absolute links as is
139 if link
.has_prefix
("http://") or link
.has_prefix
("https://") then
145 # Get the directory of the doc object to deal with the relative link
146 var mdoc
= current_mdoc
147 if mdoc
== null then break
148 var source
= mdoc
.location
.file
149 if source
== null then break
150 var path
= source
.filename
151 var stat
= path
.file_stat
152 if stat
== null then break
153 if not stat
.is_dir
then path
= path
.dirname
155 # Get the full path to the local resource
156 var fulllink
= path
/ link
.to_s
157 stat
= fulllink
.file_stat
158 if stat
== null then break
160 # Get a collision-free catalog name for the resource
161 var hash
= fulllink
.md5
162 var ext
= fulllink
.file_extension
163 if ext
!= null then hash
= hash
+ "." + ext
165 # Copy the local resource in the resource directory of the catalog
166 var res
= catalog
.outdir
/ "res" / hash
167 fulllink
.file_copy_to
(res
)
169 # Hijack the link in the html.
170 link
= ".." / "res" / hash
171 super(v
, link
, name
, comment
)
176 catalog
.modelbuilder
.toolcontext
.error
(current_mdoc
.location
, "Error: cannot find local image `{link}`")
180 # The registered catalog
182 # It is used to deal with relative links in images.
183 var catalog
: Catalog is noautoinit
189 # Register `self` to the global NitdocDecorator
190 # FIXME this is ugly. But no better idea at the moment.
191 modelbuilder
.model
.nitdoc_md_processor
.decorator
.as(NitdocDecorator).catalog
= self
194 # The output directory where to generate pages
195 var outdir
: String is noautoinit
197 # Return a empty `CatalogPage`.
198 fun new_page
(rootpath
: String): CatalogPage
200 return new CatalogPage(self, rootpath
)
203 # Recursively generate a level in the file tree of the *content* section
204 private fun gen_content_level
(ot
: OrderedTree[MConcern], os
: Array[Object], res
: Template)
212 if mdoc
!= null then d
= ": {mdoc.html_synopsis.write_to_string}"
213 res
.add
"<strong>{o.name}</strong>{d} ({o.filepath.to_s})"
214 else if o
isa MModule then
217 if mdoc
!= null then d
= ": {mdoc.html_synopsis.write_to_string}"
218 res
.add
"<strong>{o.name}</strong>{d} ({o.filepath.to_s})"
222 var subs
= ot
.sub
.get_or_null
(o
)
223 if subs
!= null then gen_content_level
(ot
, subs
, res
)
229 # Generate a full HTML page for a package
230 fun generate_page
(mpackage
: MPackage): Writable
232 var res
= new_page
("..")
233 var name
= mpackage
.name
.html_escape
234 res
.more_head
.add
"""<title>{{{name}}}</title>"""
236 res
.add
"""<div class="content">"""
238 var mdoc
= mpackage
.mdoc_or_fallback
240 res
.add
"""<h1 class="package-name">{{{name}}}</h1>"""
243 <div style="float: left">
244 <h1 class="package-name">{{{name}}} - </h1>
247 res
.add mdoc
.html_documentation
250 res
.add
"<h2>Content</h2>"
251 var ot
= new OrderedTree[MConcern]
252 for g
in mpackage
.mgroups
do
254 if g
.is_interesting
then
258 for mp
in g
.mmodules
do
262 ot
.sort_with
(alpha_comparator
)
263 gen_content_level
(ot
, ot
.roots
, res
)
268 <div class="sidebar">
271 var tryit
= mpackage
.metadata
.metadata
("upstream.tryit")
272 if tryit
!= null then
273 var e
= tryit
.html_escape
274 res
.add
"<li><a href=\"{e}\
">Try<span style=\"color
:white\
">n</span>it!</a></li>\n"
276 var apk
= mpackage
.metadata
.metadata
("upstream.apk")
278 var e
= apk
.html_escape
279 res
.add
"<li><a href=\"{e}\
">Android apk</a></li>\n"
282 res
.add
"""</ul>\n<ul class="box">\n"""
284 var homepage
= mpackage
.metadata
.metadata
("upstream.homepage")
285 if homepage
!= null then
286 var e
= homepage
.html_escape
287 res
.add
"<li><a href=\"{e}\
">{e}</a></li>\n"
289 for maintainer
in mpackage
.metadata
.maintainers
do
290 res
.add
"<li>{maintainer.to_html}</li>"
292 var license
= mpackage
.metadata
.metadata
("package.license")
293 if license
!= null then
294 var e
= license
.html_escape
295 res
.add
"<li><a href=\"http
://opensource
.org
/licenses
/{e}\
">{e}</a> license</li>\n"
299 res
.add
"<h3>Source Code</h3>\n<ul class=\"box\
">\n"
300 var browse
= mpackage
.metadata
.metadata
("upstream.browse")
301 if browse
!= null then
302 var e
= browse
.html_escape
303 res
.add
"<li><a href=\"{e}\
">{e}</a></li>\n"
305 var git
= mpackage
.metadata
.metadata
("upstream.git")
307 var e
= git
.html_escape
308 res
.add
"<li><tt>{e}</tt></li>\n"
310 var last_date
= mpackage
.metadata
.last_date
311 if last_date
!= null then
312 var e
= last_date
.html_escape
313 res
.add
"<li>most recent commit: {e}</li>\n"
315 var first_date
= mpackage
.metadata
.first_date
316 if first_date
!= null then
317 var e
= first_date
.html_escape
318 res
.add
"<li>oldest commit: {e}</li>\n"
320 var commits
= commits
[mpackage
]
322 res
.add
"<li>{commits} commits</li>\n"
326 res
.add
"<h3>Quality</h3>\n<ul class=\"box\
">\n"
327 var errors
= errors
[mpackage
]
329 res
.add
"<li>{errors} errors</li>\n"
331 res
.add
"<li>{warnings[mpackage]} warnings ({warnings_per_kloc[mpackage]}/kloc)</li>\n"
332 res
.add
"<li>{documentation_score[mpackage]}% documented</li>\n"
335 res
.add
"<h3>Tags</h3>\n"
336 var ts2
= new Array[String]
337 for t
in mpackage
.metadata
.tags
do
339 ts2
.add
"<a href=\"../index
.html
#tag_{t}\">{t}</a>"
341 res
.add_list
(ts2
, ", ", ", ")
343 if deps
.has
(mpackage
) then
344 var reqs
= deps
[mpackage
].greaters
.to_a
345 reqs
.remove
(mpackage
)
346 alpha_comparator
.sort
(reqs
)
347 res
.add
"<h3>Requirements</h3>\n"
348 if reqs
.is_empty
then
351 var list
= new Array[String]
353 var direct
= deps
.has_direct_edge
(mpackage
, r
)
354 var s
= "<a href=\"{r}.html\
">"
355 if direct
then s
+= "<strong>"
357 if direct
then s
+= "</strong>"
361 res
.add_list
(list
, ", ", " and ")
364 reqs
= deps
[mpackage
].smallers
.to_a
365 reqs
.remove
(mpackage
)
366 alpha_comparator
.sort
(reqs
)
367 res
.add
"<h3>Clients</h3>\n"
368 if reqs
.is_empty
then
371 var list
= new Array[String]
373 var direct
= deps
.has_direct_edge
(r
, mpackage
)
374 var s
= "<a href=\"{r}.html\
">"
375 if direct
then s
+= "<strong>"
377 if direct
then s
+= "</strong>"
381 res
.add_list
(list
, ", ", " and ")
385 var contributors
= mpackage
.metadata
.contributors
386 if not contributors
.is_empty
then
387 res
.add
"<h3>Contributors</h3>\n<ul class=\"box\
">"
388 for c
in contributors
do
389 res
.add
"<li>{c.to_html}</li>"
397 <li>{{{mmodules[mpackage]}}} modules</li>
398 <li>{{{mclasses[mpackage]}}} classes</li>
399 <li>{{{mmethods[mpackage]}}} methods</li>
400 <li>{{{loc[mpackage]}}} lines of code</li>
410 # Return a short HTML sequence for a package
412 # Intended to use in lists.
413 fun li_package
(p
: MPackage): String
416 var f
= "p/{p.name}.html"
417 res
+= "<a href=\"{f}\
">{p}</a>"
418 var d
= p
.mdoc_or_fallback
419 if d
!= null then res
+= " - {d.html_synopsis.write_to_string}"
423 # List packages by group.
425 # For each key of the `map` a `<h3>` is generated.
426 # Each package is then listed.
428 # The list of keys is generated first to allow fast access to the correct `<h3>`.
429 # `id_prefix` is used to give an id to the `<h3>` element.
430 fun list_by
(map
: MultiHashMap[Object, MPackage], id_prefix
: String): Template
432 var res
= new Template
433 var keys
= map
.keys
.to_a
434 alpha_comparator
.sort
(keys
)
435 var list
= [for x
in keys
do "<a href=\"#{id_prefix}{x.to_s.html_escape}\">{x.to_s.html_escape}</a>"]
436 res
.add_list
(list
, ", ", " and ")
439 var projs
= map
[k
].to_a
440 alpha_comparator
.sort
(projs
)
441 var e
= k
.to_s
.html_escape
442 res
.add
"<h3 id=\"{id_prefix}{e}\
">{e} ({projs.length})</h3>\n<ul>\n"
445 res
.add li_package
(p
)
453 # List the 10 best packages from `cpt`
454 fun list_best
(cpt
: Counter[MPackage]): Template
456 var res
= new Template
460 if i
> best
.length
then break
461 var p
= best
[best
.length-i
]
463 res
.add li_package
(p
)
464 # res.add " ({cpt[p]})"
471 # Produce a HTML table containig information on the packages
473 # `package_page` must have been called before so that information is computed.
474 fun table_packages
(mpackages
: Array[MPackage]): Template
476 alpha_comparator
.sort
(mpackages
)
477 var res
= new Template
478 res
.add
"<table data-toggle=\"table\
" data-sort-name=\"name\
" data-sort-order=\"desc\
" width=\"100%\
">\n"
479 res
.add
"<thead><tr>\n"
480 res
.add
"<th data-field=\"name\
" data-sortable=\"true\
">name</th>\n"
481 res
.add
"<th data-field=\"maint\
" data-sortable=\"true\
">maint</th>\n"
482 res
.add
"<th data-field=\"contrib\
" data-sortable=\"true\
">contrib</th>\n"
483 if deps
.not_empty
then
484 res
.add
"<th data-field=\"reqs\
" data-sortable=\"true\
">reqs</th>\n"
485 res
.add
"<th data-field=\"dreqs\
" data-sortable=\"true\
">direct<br>reqs</th>\n"
486 res
.add
"<th data-field=\"cli\
" data-sortable=\"true\
">clients</th>\n"
487 res
.add
"<th data-field=\"dcli\
" data-sortable=\"true\
">direct<br>clients</th>\n"
489 res
.add
"<th data-field=\"mod\
" data-sortable=\"true\
">modules</th>\n"
490 res
.add
"<th data-field=\"cla\
" data-sortable=\"true\
">classes</th>\n"
491 res
.add
"<th data-field=\"met\
" data-sortable=\"true\
">methods</th>\n"
492 res
.add
"<th data-field=\"loc\
" data-sortable=\"true\
">lines</th>\n"
493 res
.add
"<th data-field=\"score\
" data-sortable=\"true\
">score</th>\n"
494 res
.add
"<th data-field=\"errors\
" data-sortable=\"true\
">errors</th>\n"
495 res
.add
"<th data-field=\"warnings\
" data-sortable=\"true\
">warnings</th>\n"
496 res
.add
"<th data-field=\"warnings_per_kloc\
" data-sortable=\"true\
">w/kloc</th>\n"
497 res
.add
"<th data-field=\"doc\
" data-sortable=\"true\
">doc</th>\n"
498 res
.add
"</tr></thead>"
499 for p
in mpackages
do
501 res
.add
"<td><a href=\"p
/{p.name}.html\
">{p.name}</a></td>"
503 if p
.metadata
.maintainers
.not_empty
then maint
= p
.metadata
.maintainers
.first
.name
.html_escape
504 res
.add
"<td>{maint}</td>"
505 res
.add
"<td>{p.metadata.contributors.length}</td>"
506 if deps
.not_empty
then
507 res
.add
"<td>{deps[p].greaters.length-1}</td>"
508 res
.add
"<td>{deps[p].direct_greaters.length}</td>"
509 res
.add
"<td>{deps[p].smallers.length-1}</td>"
510 res
.add
"<td>{deps[p].direct_smallers.length}</td>"
512 res
.add
"<td>{mmodules[p]}</td>"
513 res
.add
"<td>{mclasses[p]}</td>"
514 res
.add
"<td>{mmethods[p]}</td>"
515 res
.add
"<td>{loc[p]}</td>"
516 res
.add
"<td>{score[p]}</td>"
517 res
.add
"<td>{errors[p]}</td>"
518 res
.add
"<td>{warnings[p]}</td>"
519 res
.add
"<td>{warnings_per_kloc[p]}</td>"
520 res
.add
"<td>{documentation_score[p]}</td>"
527 # Piwik tracker URL, if any
528 var piwik_tracker
: nullable String = null
531 # Used when `piwik_tracker` is set
532 var piwik_site_id
: Int = 1
538 var e
= name
.html_escape
541 res
+= "<a href=\"{page.html_escape}\
">"
543 var gravatar
= self.gravatar
544 if gravatar
!= null then
545 res
+= "<img src=\"https
://secure
.gravatar
.com
/avatar
/{gravatar}?size
=20&
;default
=retro\
"> "
548 if page
!= null then res
+= "</a>"
553 var model
= new Model
554 var tc
= new ToolContext
556 var opt_dir
= new OptionString("Directory where the HTML files are generated", "-d", "--dir")
557 var opt_no_git
= new OptionBool("Do not gather git information from the working directory", "--no-git")
558 var opt_no_parse
= new OptionBool("Do not parse nit files (no importation information)", "--no-parse")
559 var opt_no_model
= new OptionBool("Do not analyse nit files (no class/method information)", "--no-model")
562 # If you want to monitor your visitors.
563 var opt_piwik_tracker
= new OptionString("Piwik tracker URL (ex: `nitlanguage.org/piwik/`)", "--piwik-tracker")
564 # Piwik tracker site id.
565 var opt_piwik_site_id
= new OptionString("Piwik site ID", "--piwik-site-id")
567 tc
.option_context
.add_option
(opt_dir
, opt_no_git
, opt_no_parse
, opt_no_model
, opt_piwik_tracker
, opt_piwik_site_id
)
569 tc
.process_options
(sys
.args
)
572 var modelbuilder
= new ModelBuilder(model
, tc
)
573 var catalog
= new Catalog(modelbuilder
)
575 catalog
.piwik_tracker
= opt_piwik_tracker
.value
576 var piwik_site_id
= opt_piwik_site_id
.value
577 if piwik_site_id
!= null then
578 if catalog
.piwik_tracker
== null then
579 print_error
"Warning: ignored `{opt_piwik_site_id}` because `{opt_piwik_tracker}` is not set."
580 else if piwik_site_id
.is_int
then
581 print_error
"Warning: ignored `{opt_piwik_site_id}`, an integer is required."
583 catalog
.piwik_site_id
= piwik_site_id
.to_i
588 # Get files or groups
589 var args
= tc
.option_context
.rest
591 if opt_no_parse
.value
then
592 mmodules
= modelbuilder
.scan_full
(args
)
594 mmodules
= modelbuilder
.parse_full
(args
)
596 var mpackages
= new Set[MPackage]
599 if p
!= null then mpackages
.add p
602 # Scan packages and compute information
603 for p
in model
.mpackages
do
606 modelbuilder
.scan_group
(g
)
608 # Load the module to process importation information
609 if opt_no_parse
.value
then continue
611 catalog
.deps
.add_node
(p
)
612 for gg
in p
.mgroups
do for m
in gg
.mmodules
do
613 for im
in m
.in_importation
.direct_greaters
do
615 if ip
== null or ip
== p
then continue
616 catalog
.deps
.add_edge
(p
, ip
)
621 if not opt_no_git
.value
then for p
in mpackages
do
625 # Run phases to modelize classes and properties (so we can count them)
626 if not opt_no_model
.value
then
627 modelbuilder
.run_phases
630 var out
= opt_dir
.value
or else "catalog.out"
636 # Generate the css (hard coded)
640 background-color: #f8f8f8;
645 text-decoration: none;
650 text-decoration: none;
662 border-bottom: solid 3px #CCC;
668 border-bottom: solid 1px #CCC;
672 list-style-type: square;
682 border: 1px solid #CCC;
683 font-family: Monospace;
685 background-color: rgb(250, 250, 250);
689 font-family: Monospace;
729 text-overflow: ellipsis;
731 border-bottom: 1px solid rgba(0,0,0,0.2);
734 css
.write_to_file
(out
/"style.css")
738 for p
in mpackages
do
740 var f
= "p/{p.name}.html"
741 catalog
.package_page
(p
)
742 catalog
.generate_page
(p
).write_to_file
(out
/f
)
745 if ini
!= null then ini
.write_to_file
(out
/"p/{p.name}.ini")
750 var index
= catalog
.new_page
("")
751 index
.more_head
.add
"<title>Packages in Nit</title>"
754 <div class="content">
755 <h1>Packages in Nit</h1>
758 index
.add
"<h2>Highlighted Packages</h2>\n"
759 index
.add catalog
.list_best
(catalog
.score
)
761 if catalog
.deps
.not_empty
then
762 index
.add
"<h2>Most Required</h2>\n"
763 var reqs
= new Counter[MPackage]
764 for p
in mpackages
do
765 reqs
[p
] = catalog
.deps
[p
].smallers
.length
- 1
767 index
.add catalog
.list_best
(reqs
)
770 index
.add
"<h2>By First Tag</h2>\n"
771 index
.add catalog
.list_by
(catalog
.cat2proj
, "cat_")
773 index
.add
"<h2>By Any Tag</h2>\n"
774 index
.add catalog
.list_by
(catalog
.tag2proj
, "tag_")
778 <div class="sidebar">
781 <li>{{{mpackages.length}}} packages</li>
782 <li>{{{catalog.maint2proj.length}}} maintainers</li>
783 <li>{{{catalog.contrib2proj.length}}} contributors</li>
784 <li>{{{catalog.tag2proj.length}}} tags</li>
785 <li>{{{catalog.mmodules.sum}}} modules</li>
786 <li>{{{catalog.mclasses.sum}}} classes</li>
787 <li>{{{catalog.mmethods.sum}}} methods</li>
788 <li>{{{catalog.loc.sum}}} lines of code</li>
793 index
.write_to_file
(out
/"index.html")
797 var page
= catalog
.new_page
("")
798 page
.more_head
.add
"<title>People of Nit</title>"
799 page
.add
"""<div class="content">\n<h1>People of Nit</h1>\n"""
800 page
.add
"<h2>By Maintainer</h2>\n"
801 page
.add catalog
.list_by
(catalog
.maint2proj
, "maint_")
802 page
.add
"<h2>By Contributor</h2>\n"
803 page
.add catalog
.list_by
(catalog
.contrib2proj
, "contrib_")
805 page
.write_to_file
(out
/"people.html")
809 page
= catalog
.new_page
("")
810 page
.more_head
.add
"<title>Projets of Nit</title>"
811 page
.add
"""<div class="content">\n<h1>People of Nit</h1>\n"""
812 page
.add
"<h2>Table of Projets</h2>\n"
813 page
.add catalog
.table_packages
(mpackages
.to_a
)
815 page
.write_to_file
(out
/"table.html")