# 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 projects # # See: # # The tool scans projects and generates the HTML files of a catalog. # # ## Features # # * [X] scan projects and their `.ini` # * [X] generate lists of projects # * [X] generate a page per project with the readme and most metadata # * [ ] link/include/be included in the documentation # * [ ] propose `related projects` # * [ ] show directory content (a la nitls) # * [X] gather git information from the working directory # * [ ] gather git information from the repository # * [ ] gather project 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 # * [ ] 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 projects, 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 MProject # 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 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 projects 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 # Projects by tag var tag2proj = new MultiHashMap[String, MProject] # Projects by category var cat2proj = new MultiHashMap[String, MProject] # Projects by maintainer var maint2proj = new MultiHashMap[String, MProject] # Projects by contributors var contrib2proj = new MultiHashMap[String, MProject] # Dependency between projects var deps = new POSet[MProject] # Number of modules by project var mmodules = new Counter[MProject] # Number of classes by project var mclasses = new Counter[MProject] # Number of methods by project var mmethods = new Counter[MProject] # Number of line of code by project var loc = new Counter[MProject] # Number of commits by project var commits = new Counter[MProject] # Score by project # # The score is loosely computed using other metrics var score = new Counter[MProject] # Scan, register and add a contributor to a project fun add_contrib(person: String, mproject: MProject, res: Template) do var projs = contrib2proj[person] if not projs.has(mproject) then projs.add mproject 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 # Compute information and generate a full HTML page for a project fun project_page(mproject: MProject): Writable do var res = new CatalogPage var score = score[mproject].to_f var name = mproject.name.html_escape res.more_head.add """{{{name}}}""" res.add """

    {{{name}}}

    """ var mdoc = mproject.mdoc_or_fallback if mdoc != null then score += 100.0 res.add mdoc.html_documentation score += mdoc.content.length.score end res.add """
    """ self.score[mproject] = score.to_i return res end # Return a short HTML sequence for a project # # Intended to use in lists. fun li_project(p: MProject): String do var res = "" var f = "{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 projects by group. # # For each key of the `map` a `

    ` is generated. # Each project 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, MProject], 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 projects from `cpt` fun list_best(cpt: Counter[MProject]): Template do var res = new Template res.add "" return res end # Collect more information on a project using the `git` tool. fun git_info(mproject: MProject) do var ini = mproject.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 = mproject.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[mproject] = 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 mproject.last_date == null then mproject.last_date = s.first mproject.first_date = s.first # Count contributors contributors.inc(s.last) end for c in contributors.sort.reverse_iterator do mproject.contributors.add c end end # Produce a HTML table containig information on the projects # # `project_page` must have been called before so that information is computed. fun table_projects(mprojects: Array[MProject]): Template do alpha_comparator.sort(mprojects) var res = new Template res.add "\n" res.add "\n" res.add "\n" res.add "\n" res.add "\n" res.add "\n" res.add "\n" res.add "\n" res.add "\n" res.add "\n" res.add "\n" res.add "\n" res.add "\n" res.add "\n" res.add "" for p in mprojects do res.add "" res.add "" var maint = "?" if p.maintainers.not_empty then maint = p.maintainers.first res.add "" res.add "" res.add "" res.add "" res.add "" res.add "" 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 tc.process_options(sys.args) tc.keep_going = true var modelbuilder = new ModelBuilder(model, tc) var catalog = new Catalog(modelbuilder) # Get files or groups for a in tc.option_context.rest do modelbuilder.get_mgroup(a) modelbuilder.identify_file(a) end # Scan projects and compute information for p in model.mprojects do var g = p.root assert g != null modelbuilder.scan_group(g) # Load the module to process importation information modelbuilder.parse_group(g) 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.mproject if ip == null or ip == p then continue catalog.deps.add_edge(p, ip) end end catalog.git_info(p) end # Run phases to modelize classes and properties (so we can count them) #modelbuilder.run_phases var out = "out" out.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.mprojects do # print p var f = "{p.name}.html" catalog.project_page(p).write_to_file(out/f) end # INDEX var index = new CatalogPage index.more_head.add "Projects in Nit" index.add """

    Projects in Nit

    """ index.add "

    Highlighted Projects

    \n" index.add catalog.list_best(catalog.score) index.add "

    Most Required

    \n" var reqs = new Counter[MProject] for p in model.mprojects do reqs[p] = catalog.deps[p].smallers.length - 1 end index.add catalog.list_best(reqs) 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_projects(model.mprojects) page.add "
    \n" page.write_to_file(out/"table.html")