# This file is part of NIT ( http://www.nitlanguage.org ). # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Basic catalog generator for Nit packages # # See: # # 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. 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 # A HTML page in a catalog # # This is just a template with the header pre-filled and the footer injected at rendering. # Therefore, once instantiated, the content can just be added to it. class CatalogPage super Template # Placeholder to include additional things before the ``. var more_head = new Template # Relative path to the root directory (with the index file). # # Use "" for pages in the root directory # Use ".." for pages in a subdirectory var rootpath: String redef init do add """ """ add more_head add """
""" end redef fun rendering do add """
""" 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] # Scan, register and 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 # Regular expressions are broken, need to investigate. # So split manually. # #var re = "([^<(]*?)(<([^>]*?)>)?(\\((.*)\\))?".to_re #var m = (person+" ").search(re) #print "{person}: `{m or else "?"}` `{m[1] or else "?"}` `{m[3] or else "?"}` `{m[5] or else "?"}`" do var sp1 = person.split_once_on("<") if sp1.length < 2 then break end var sp2 = sp1.last.split_once_on(">") if sp2.length < 2 then break end name = sp1.first.trim email = sp2.first.trim var sp3 = sp2.last.split_once_on("(") if sp3.length < 2 then break end var sp4 = sp3.last.split_once_on(")") if sp4.length < 2 then break end page = sp4.first.trim end var e = name.html_escape res.add "
  • " if page != null then res.add "" end if email != null then # TODO get more things from github by using the email as a key # "https://api.github.com/search/users?q={email}+in:email" var md5 = email.md5.to_lower res.add " " end res.add "{e}" if page != null then res.add "" res.add "
  • " end # Recursively generate a level in the file tree of the *content* section private fun gen_content_level(ot: OrderedTree[MConcern], os: Array[Object], res: Template) do res.add "\n" end # Compute information and generate a full HTML page for a package fun package_page(mpackage: MPackage): Writable do var res = new CatalogPage("..") var score = score[mpackage].to_f var name = mpackage.name.html_escape res.more_head.add """{{{name}}}""" res.add """

    {{{name}}}

    """ var mdoc = mpackage.mdoc_or_fallback if mdoc != null then score += 100.0 res.add mdoc.html_documentation score += mdoc.content.length.score end res.add "

    Content

    " 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.mmodules do ot.add(pa, mp) end end ot.sort_with(alpha_comparator) gen_content_level(ot, ot.roots, res) res.add """
    """ self.score[mpackage] = score.to_i return res end # Return a short HTML sequence for a package # # Intended to use in lists. fun li_package(p: MPackage): String do var res = "" var f = "p/{p.name}.html" res += "{p}" var d = p.mdoc_or_fallback if d != null then res += " - {d.html_synopsis.write_to_string}" return res end # List packages by group. # # For each key of the `map` a `

    ` is generated. # Each package is then listed. # # The list of keys is generated first to allow fast access to the correct `

    `. # `id_prefix` is used to give an id to the `

    ` element. fun list_by(map: MultiHashMap[String, MPackage], id_prefix: String): Template do var res = new Template var keys = map.keys.to_a alpha_comparator.sort(keys) var list = [for x in keys do "{x.html_escape}"] res.add_list(list, ", ", " and ") for k in keys do var projs = map[k].to_a alpha_comparator.sort(projs) var e = k.html_escape res.add "

    {e} ({projs.length})

    \n" end return res end # List the 10 best packages from `cpt` fun list_best(cpt: Counter[MPackage]): Template do var res = new Template res.add "" 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. fun table_packages(mpackages: Array[MPackage]): Template do alpha_comparator.sort(mpackages) var res = new Template res.add "\n" res.add "\n" res.add "\n" res.add "\n" res.add "\n" if deps.not_empty then res.add "\n" res.add "\n" res.add "\n" res.add "\n" end res.add "\n" res.add "\n" res.add "\n" res.add "\n" res.add "\n" res.add "" for p in mpackages do res.add "" res.add "" var maint = "?" if p.maintainers.not_empty then maint = p.maintainers.first res.add "" res.add "" if deps.not_empty then res.add "" res.add "" res.add "" res.add "" end res.add "" res.add "" res.add "" res.add "" res.add "" res.add "\n" end res.add "
    namemaintcontribreqsdirect
    reqs
    clientsdirect
    clients
    modulesclassesmethodslinesscore
    {p.name}{maint}{p.contributors.length}{deps[p].greaters.length-1}{deps[p].direct_greaters.length}{deps[p].smallers.length-1}{deps[p].direct_smallers.length}{mmodules[p]}{mclasses[p]}{mmethods[p]}{loc[p]}{score[p]}
    \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 end var model = new Model var tc = new ToolContext var opt_dir = new OptionString("Directory where the HTML files are generated", "-d", "--dir") var opt_no_git = new OptionBool("Do not gather git information from the working directory", "--no-git") 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) tc.process_options(sys.args) tc.keep_going = true var modelbuilder = new ModelBuilder(model, tc) var catalog = new Catalog(modelbuilder) # Get files or groups 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 for p in model.mpackages do var g = p.root assert g != null modelbuilder.scan_group(g) # Load the module to process importation information if opt_no_parse.value then continue catalog.deps.add_node(p) for gg in p.mgroups do for m in gg.mmodules do for im in m.in_importation.direct_greaters do var ip = im.mpackage if ip == null or ip == p then continue catalog.deps.add_edge(p, ip) end end end if not opt_no_git.value then for p in model.mpackages do catalog.git_info(p) end # Run phases to modelize classes and properties (so we can count them) if not opt_no_model.value then modelbuilder.run_phases end var out = opt_dir.value or else "catalog.out" (out/"p").mkdir # Generate the css (hard coded) var css = """ body { margin-top: 15px; background-color: #f8f8f8; } a { color: #0D8921; text-decoration: none; } a:hover { color: #333; text-decoration: none; } h1 { font-weight: bold; color: #0D8921; font-size: 22px; } h2 { color: #6C6C6C; font-size: 18px; border-bottom: solid 3px #CCC; } h3 { color: #6C6C6C; font-size: 15px; border-bottom: solid 1px #CCC; } ul { list-style-type: square; } dd { color: #6C6C6C; margin-top: 1em; margin-bottom: 1em; } pre { border: 1px solid #CCC; font-family: Monospace; color: #2d5003; background-color: rgb(250, 250, 250); } code { font-family: Monospace; color: #2d5003; } footer { margin-top: 20px; } .container { margin: 0 auto; padding: 0 20px; } .content { float: left; margin-top: 40px; width: 65%; } .sidebar { float: right; margin-top: 40px; width: 30% } .sidebar h3 { color: #0D8921; font-size: 18px; border-bottom: 0px; } .box { margin: 0; padding: 0; } .box li { line-height: 2.5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 10px; border-bottom: 1px solid rgba(0,0,0,0.2); } """ css.write_to_file(out/"style.css") # PAGES for p in model.mpackages do # print p var f = "p/{p.name}.html" catalog.package_page(p).write_to_file(out/f) end # INDEX var index = new CatalogPage("") index.more_head.add "Packages in Nit" index.add """

    Packages in Nit

    """ index.add "

    Highlighted Packages

    \n" index.add catalog.list_best(catalog.score) if catalog.deps.not_empty then index.add "

    Most Required

    \n" var reqs = new Counter[MPackage] for p in model.mpackages do reqs[p] = catalog.deps[p].smallers.length - 1 end index.add catalog.list_best(reqs) end index.add "

    By First Tag

    \n" index.add catalog.list_by(catalog.cat2proj, "cat_") index.add "

    By Any Tag

    \n" index.add catalog.list_by(catalog.tag2proj, "tag_") index.add """
    """ index.write_to_file(out/"index.html") # PEOPLE var page = new CatalogPage("") page.more_head.add "People of Nit" page.add """
    \n

    People of Nit

    \n""" page.add "

    By Maintainer

    \n" page.add catalog.list_by(catalog.maint2proj, "maint_") page.add "

    By Contributor

    \n" page.add catalog.list_by(catalog.contrib2proj, "contrib_") page.add "
    \n" page.write_to_file(out/"people.html") # TABLE page = new CatalogPage("") page.more_head.add "Projets of Nit" page.add """
    \n

    People of Nit

    \n""" page.add "

    Table of Projets

    \n" page.add catalog.table_packages(model.mpackages) page.add "
    \n" page.write_to_file(out/"table.html")