#
# The tool scans packages and generates the HTML files of a catalog.
#
-# ## Features
-#
-# * [X] scan packages and their `.ini`
-# * [X] generate lists of packages
-# * [X] generate a page per package with the readme and most metadata
-# * [ ] link/include/be included in the documentation
-# * [ ] propose `related packages`
-# * [X] show directory content (a la nitls)
-# * [X] gather git information from the working directory
-# * [ ] gather git information from the repository
-# * [ ] gather package information from github
-# * [ ] gather people information from github
-# * [ ] reify people
-# * [ ] separate information gathering from rendering
-# * [ ] move up information gathering in (existing or new) service modules
-# * [X] add command line options
-# * [ ] harden HTML (escaping, path injection, etc)
-# * [ ] nitcorn server with RESTful API
-#
-# ## Issues and limitations
-#
-# The tool works likee the other tools and expects to find valid Nit source code in the directories
-#
-# * cruft and temporary files will be collected
-# * missing source file (e.g. not yet generated by nitcc) will make information
-# incomplete (e.g. invalid module thus partial dependency and metrics)
-#
-# How to use the tool as the basis of a Nit code archive on the web usable with a package manager is not clear.
+# See `catalog` for details
module nitcatalog
import loader # Scan&load packages, groups and modules
import doc::doc_down # Display mdoc
-import md5 # To get gravatar images
-import counter # For statistics
-import modelize # To process and count classes and methods
-
-redef class MPackage
- # Return the associated metadata from the `ini`, if any
- fun metadata(key: String): nullable String
- do
- var ini = self.ini
- if ini == null then return null
- return ini[key]
- end
-
- # The list of maintainers
- var maintainers = new Array[String]
-
- # The list of contributors
- var contributors = new Array[String]
-
- # The date of the most recent commit
- var last_date: nullable String = null
-
- # The date of the oldest commit
- var first_date: nullable String = null
-end
+import catalog
# A HTML page in a catalog
#
class CatalogPage
super Template
+ # The associated catalog, used to groups options and other global data
+ var catalog: Catalog
+
# Placeholder to include additional things before the `</head>`.
var more_head = new Template
"""
end
+ # Inject piwik HTML code if required
+ private fun add_piwik
+ do
+ var tracker_url = catalog.piwik_tracker
+ if tracker_url == null then return
+
+ var site_id = catalog.piwik_site_id
+
+ tracker_url = tracker_url.trim
+ if tracker_url.chars.last != '/' then tracker_url += "/"
+ add """
+<!-- Piwik -->
+<script type="text/javascript">
+var _paq = _paq || [];
+_paq.push(['trackPageView']);
+_paq.push(['enableLinkTracking']);
+(function() {
+var u=(("https:" == document.location.protocol) ? "https" : "http") + "://{{{tracker_url.escape_to_c}}}";
+_paq.push(['setTrackerUrl', u+'piwik.php']);
+_paq.push(['setSiteId', {{{site_id}}}]);
+var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; g.type='text/javascript';
+g.defer=true; g.async=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
+})();
+
+</script>
+<noscript><p><img src="http://{{{tracker_url.html_escape}}}piwik.php?idsite={{{site_id}}}" style="border:0;" alt="" /></p></noscript>
+<!-- End Piwik Code -->
+"""
+
+ end
+
redef fun rendering
do
add """
<script src='https://code.jquery.com/jquery-latest.min.js'></script>
<script src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.8.1/bootstrap-table-all.min.js'></script>
+"""
+ add_piwik
+ add """
+
</body>
</html>
"""
end
end
-redef class Int
- # Returns `log(self+1)`. Used to compute score of packages
- fun score: Float do return (self+1).to_f.log
-end
-
-# The main class of the calatog generator that has the knowledge
-class Catalog
-
- # The modelbuilder
- # used to access the files and count source lines of code
- var modelbuilder: ModelBuilder
-
- # Packages by tag
- var tag2proj = new MultiHashMap[String, MPackage]
-
- # Packages by category
- var cat2proj = new MultiHashMap[String, MPackage]
-
- # Packages by maintainer
- var maint2proj = new MultiHashMap[String, MPackage]
-
- # Packages by contributors
- var contrib2proj = new MultiHashMap[String, MPackage]
-
- # Dependency between packages
- var deps = new POSet[MPackage]
-
- # Number of modules by package
- var mmodules = new Counter[MPackage]
-
- # Number of classes by package
- var mclasses = new Counter[MPackage]
-
- # Number of methods by package
- var mmethods = new Counter[MPackage]
-
- # Number of line of code by package
- var loc = new Counter[MPackage]
-
- # Number of commits by package
- var commits = new Counter[MPackage]
-
- # Score by package
- #
- # The score is loosely computed using other metrics
- var score = new Counter[MPackage]
+redef class Catalog
+ # Return a empty `CatalogPage`.
+ fun new_page(rootpath: String): CatalogPage
+ do
+ return new CatalogPage(self, rootpath)
+ end
- # Scan, register and add a contributor to a package
+ # Add a contributor to a package
fun add_contrib(person: String, mpackage: MPackage, res: Template)
do
- var projs = contrib2proj[person]
- if not projs.has(mpackage) then projs.add mpackage
var name = person
var email = null
var page = null
end
# Recursively generate a level in the file tree of the *content* section
- private fun gen_content_level(ot: OrderedTree[Object], os: Array[Object], res: Template)
+ private fun gen_content_level(ot: OrderedTree[MConcern], os: Array[Object], res: Template)
do
res.add "<ul>\n"
for o in os do
var mdoc = o.mdoc
if mdoc != null then d = ": {mdoc.html_synopsis.write_to_string}"
res.add "<strong>{o.name}</strong>{d} ({o.filepath.to_s})"
- else if o isa ModulePath then
+ else if o isa MModule then
var d = ""
- var m = o.mmodule
- if m != null then
- var mdoc = m.mdoc
- if mdoc != null then d = ": {mdoc.html_synopsis.write_to_string}"
- end
+ var mdoc = o.mdoc
+ if mdoc != null then d = ": {mdoc.html_synopsis.write_to_string}"
res.add "<strong>{o.name}</strong>{d} ({o.filepath.to_s})"
else
abort
res.add "</ul>\n"
end
- # Compute information and generate a full HTML page for a package
- fun package_page(mpackage: MPackage): Writable
+ # Generate a full HTML page for a package
+ fun generate_page(mpackage: MPackage): Writable
do
- var res = new CatalogPage("..")
- var score = score[mpackage].to_f
+ var res = new_page("..")
var name = mpackage.name.html_escape
res.more_head.add """<title>{{{name}}}</title>"""
<h1 class="package-name">{{{name}}}</h1>
"""
var mdoc = mpackage.mdoc_or_fallback
- if mdoc != null then
- score += 100.0
- res.add mdoc.html_documentation
- score += mdoc.content.length.score
- end
+ if mdoc != null then res.add mdoc.html_documentation
res.add "<h2>Content</h2>"
- var ot = new OrderedTree[Object]
+ var ot = new OrderedTree[MConcern]
for g in mpackage.mgroups do
var pa = g.parent
if g.is_interesting then
ot.add(pa, g)
pa = g
end
- for mp in g.module_paths do
+ for mp in g.mmodules do
ot.add(pa, mp)
end
end
<div class="sidebar">
<ul class="box">
"""
+ var tryit = mpackage.metadata("upstream.tryit")
+ if tryit != null then
+ var e = tryit.html_escape
+ res.add "<li><a href=\"{e}\">Try<span style=\"color:white\">n</span>it!</a></li>\n"
+ end
+ var apk = mpackage.metadata("upstream.apk")
+ if apk != null then
+ var e = apk.html_escape
+ res.add "<li><a href=\"{e}\">Android apk</a></li>\n"
+ end
+
+ res.add """</ul>\n<ul class="box">\n"""
+
var homepage = mpackage.metadata("upstream.homepage")
if homepage != null then
- score += 5.0
var e = homepage.html_escape
res.add "<li><a href=\"{e}\">{e}</a></li>\n"
end
var maintainer = mpackage.metadata("package.maintainer")
if maintainer != null then
- score += 5.0
add_contrib(maintainer, mpackage, res)
- mpackage.maintainers.add maintainer
- var projs = maint2proj[maintainer]
- if not projs.has(mpackage) then projs.add mpackage
end
var license = mpackage.metadata("package.license")
if license != null then
- score += 5.0
var e = license.html_escape
res.add "<li><a href=\"http://opensource.org/licenses/{e}\">{e}</a> license</li>\n"
end
res.add "<h3>Source Code</h3>\n<ul class=\"box\">\n"
var browse = mpackage.metadata("upstream.browse")
if browse != null then
- score += 5.0
var e = browse.html_escape
res.add "<li><a href=\"{e}\">{e}</a></li>\n"
end
res.add "</ul>\n"
res.add "<h3>Tags</h3>\n"
- var tags = mpackage.metadata("package.tags")
- var ts = new Array[String]
- if tags != null then
- for t in tags.split(",") do
- t = t.trim
- if t == "" then continue
- ts.add t
- end
- end
- if ts.is_empty then ts.add "none"
var ts2 = new Array[String]
- for t in ts do
- tag2proj[t].add mpackage
+ for t in mpackage.tags do
t = t.html_escape
ts2.add "<a href=\"../index.html#tag_{t}\">{t}</a>"
end
res.add_list(ts2, ", ", ", ")
- var cat = ts.first
- cat2proj[cat].add mpackage
- score += ts.length.score
if deps.has(mpackage) then
var reqs = deps[mpackage].greaters.to_a
end
res.add_list(list, ", ", " and ")
end
-
- score += deps[mpackage].greaters.length.score
- score += deps[mpackage].direct_greaters.length.score
- score += deps[mpackage].smallers.length.score
- score += deps[mpackage].direct_smallers.length.score
end
var contributors = mpackage.contributors
end
res.add "</ul>"
end
- score += contributors.length.to_f
-
- var mmodules = 0
- var mclasses = 0
- var mmethods = 0
- var loc = 0
- for g in mpackage.mgroups do
- mmodules += g.module_paths.length
- for m in g.mmodules do
- var am = modelbuilder.mmodule2node(m)
- if am != null then
- var file = am.location.file
- if file != null then
- loc += file.line_starts.length - 1
- end
- end
- for cd in m.mclassdefs do
- mclasses += 1
- for pd in cd.mpropdefs do
- if not pd isa MMethodDef then continue
- mmethods += 1
- end
- end
- end
- end
- self.mmodules[mpackage] = mmodules
- self.mclasses[mpackage] = mclasses
- self.mmethods[mpackage] = mmethods
- self.loc[mpackage] = loc
-
- #score += mmodules.score
- score += mclasses.score
- score += mmethods.score
- score += loc.score
res.add """
<h3>Stats</h3>
<ul class="box">
-<li>{{{mmodules}}} modules</li>
-<li>{{{mclasses}}} classes</li>
-<li>{{{mmethods}}} methods</li>
-<li>{{{loc}}} lines of code</li>
+<li>{{{mmodules[mpackage]}}} modules</li>
+<li>{{{mclasses[mpackage]}}} classes</li>
+<li>{{{mmethods[mpackage]}}} methods</li>
+<li>{{{loc[mpackage]}}} lines of code</li>
</ul>
"""
res.add """
</div>
"""
- self.score[mpackage] = score.to_i
-
return res
end
return res
end
- # Collect more information on a package using the `git` tool.
- fun git_info(mpackage: MPackage)
- do
- var ini = mpackage.ini
- if ini == null then return
-
- # TODO use real git info
- #var repo = ini.get_or_null("upstream.git")
- #var branch = ini.get_or_null("upstream.git.branch")
- #var directory = ini.get_or_null("upstream.git.directory")
-
- var dirpath = mpackage.root.filepath
- if dirpath == null then return
-
- # Collect commits info
- var res = git_run("log", "--no-merges", "--follow", "--pretty=tformat:%ad;%aN <%aE>", "--", dirpath)
- var contributors = new Counter[String]
- var commits = res.split("\n")
- if commits.not_empty and commits.last == "" then commits.pop
- self.commits[mpackage] = commits.length
- for l in commits do
- var s = l.split_once_on(';')
- if s.length != 2 or s.last == "" then continue
-
- # Collect date of last and first commit
- if mpackage.last_date == null then mpackage.last_date = s.first
- mpackage.first_date = s.first
-
- # Count contributors
- contributors.inc(s.last)
- end
- for c in contributors.sort.reverse_iterator do
- mpackage.contributors.add c
- end
-
- end
-
# Produce a HTML table containig information on the packages
#
# `package_page` must have been called before so that information is computed.
res.add "</table>\n"
return res
end
-end
-# Execute a git command and return the result
-fun git_run(command: String...): String
-do
- # print "git {command.join(" ")}"
- var p = new ProcessReader("git", command...)
- var res = p.read_all
- p.close
- p.wait
- return res
+ # Piwik tracker URL, if any
+ var piwik_tracker: nullable String = null
+
+ # Piwik site ID
+ # Used when `piwik_tracker` is set
+ var piwik_site_id: Int = 1
end
var model = new Model
var opt_no_parse = new OptionBool("Do not parse nit files (no importation information)", "--no-parse")
var opt_no_model = new OptionBool("Do not analyse nit files (no class/method information)", "--no-model")
-tc.option_context.add_option(opt_dir, opt_no_git, opt_no_parse, opt_no_model)
+# Piwik tracker URL.
+# If you want to monitor your visitors.
+var opt_piwik_tracker = new OptionString("Piwik tracker URL (ex: `nitlanguage.org/piwik/`)", "--piwik-tracker")
+# Piwik tracker site id.
+var opt_piwik_site_id = new OptionString("Piwik site ID", "--piwik-site-id")
+
+tc.option_context.add_option(opt_dir, opt_no_git, opt_no_parse, opt_no_model, opt_piwik_tracker, opt_piwik_site_id)
tc.process_options(sys.args)
tc.keep_going = true
var modelbuilder = new ModelBuilder(model, tc)
var catalog = new Catalog(modelbuilder)
+catalog.piwik_tracker = opt_piwik_tracker.value
+var piwik_site_id = opt_piwik_site_id.value
+if piwik_site_id != null then
+ if catalog.piwik_tracker == null then
+ print_error "Warning: ignored `{opt_piwik_site_id}` because `{opt_piwik_tracker}` is not set."
+ else if piwik_site_id.is_int then
+ print_error "Warning: ignored `{opt_piwik_site_id}`, an integer is required."
+ else
+ catalog.piwik_site_id = piwik_site_id.to_i
+ end
+end
+
+
# Get files or groups
-for a in tc.option_context.rest do
- modelbuilder.get_mgroup(a)
- modelbuilder.identify_file(a)
+var args = tc.option_context.rest
+if opt_no_parse.value then
+ modelbuilder.scan_full(args)
+else
+ modelbuilder.parse_full(args)
end
# Scan packages and compute information
# Load the module to process importation information
if opt_no_parse.value then continue
- modelbuilder.parse_group(g)
catalog.deps.add_node(p)
for gg in p.mgroups do for m in gg.mmodules do
for p in model.mpackages do
# print p
var f = "p/{p.name}.html"
- catalog.package_page(p).write_to_file(out/f)
+ catalog.package_page(p)
+ catalog.generate_page(p).write_to_file(out/f)
end
# INDEX
-var index = new CatalogPage("")
+var index = catalog.new_page("")
index.more_head.add "<title>Packages in Nit</title>"
index.add """
# PEOPLE
-var page = new CatalogPage("")
+var page = catalog.new_page("")
page.more_head.add "<title>People of Nit</title>"
page.add """<div class="content">\n<h1>People of Nit</h1>\n"""
page.add "<h2>By Maintainer</h2>\n"
# TABLE
-page = new CatalogPage("")
+page = catalog.new_page("")
page.more_head.add "<title>Projets of Nit</title>"
page.add """<div class="content">\n<h1>People of Nit</h1>\n"""
page.add "<h2>Table of Projets</h2>\n"