diff options
Diffstat (limited to 'lv2specgen')
| -rw-r--r-- | lv2specgen/.pylintrc.toml | 5 | ||||
| -rw-r--r-- | lv2specgen/DTD/xhtml-rdfa-1.dtd | 43 | ||||
| -rw-r--r-- | lv2specgen/DTD/xhtml-ruby-1.mod | 242 | ||||
| -rwxr-xr-x | lv2specgen/lv2docgen.py | 119 | ||||
| -rwxr-xr-x | lv2specgen/lv2specgen.py | 735 | ||||
| -rw-r--r-- | lv2specgen/meson.build | 46 | ||||
| l--------- | lv2specgen/style.css | 1 | ||||
| -rw-r--r-- | lv2specgen/template.html | 21 |
8 files changed, 661 insertions, 551 deletions
diff --git a/lv2specgen/.pylintrc.toml b/lv2specgen/.pylintrc.toml new file mode 100644 index 0000000..16a73fa --- /dev/null +++ b/lv2specgen/.pylintrc.toml @@ -0,0 +1,5 @@ +# Copyright 2026 David Robillard <d@drobilla.net> +# SPDX-License-Identifier: 0BSD OR ISC + +[tool.pylint."messages control"] +enable = ["useless-suppression"] diff --git a/lv2specgen/DTD/xhtml-rdfa-1.dtd b/lv2specgen/DTD/xhtml-rdfa-1.dtd index 26ed117..0490b09 100644 --- a/lv2specgen/DTD/xhtml-rdfa-1.dtd +++ b/lv2specgen/DTD/xhtml-rdfa-1.dtd @@ -218,7 +218,7 @@ <![%xhtml-ruby.module;[ <!ENTITY % xhtml-ruby.mod PUBLIC "-//W3C//ELEMENTS XHTML Ruby 1.0//EN" - "http://www.w3.org/TR/ruby/xhtml-ruby-1.mod" > + "xhtml-ruby-1.mod" > %xhtml-ruby.mod;]]> <!-- Presentation Module ........................................ --> @@ -437,36 +437,17 @@ <!-- end of XHTML-RDFa DTD ................................................ --> <!-- ....................................................................... --> -<!-- Add xmlns attributes to validate lv2specgen output --> -<!-- This is a pretty dirty hack, but avoids needing to write a bunch of code to - mangle DTDs to appease validation --> +<!-- Hack for lv2specgen: declare prefixes for the external vocabularies used + in LV2 (most of which are generally common for RDF), so RDFa in spec pages + can use prefixed names. This glaring separation-of-concerns violation + is... not nice, but neither is diving into the horrors of custom XML DTD + validation, so here we are. +--> -<!ATTLIST html xmlns:dc CDATA #IMPLIED> -<!ATTLIST html xmlns:dct CDATA #IMPLIED> +<!ATTLIST html xmlns:dcterms CDATA #IMPLIED> +<!ATTLIST html xmlns:doap CDATA #IMPLIED> +<!ATTLIST html xmlns:foaf CDATA #IMPLIED> +<!ATTLIST html xmlns:owl CDATA #IMPLIED> <!ATTLIST html xmlns:rdf CDATA #IMPLIED> <!ATTLIST html xmlns:rdfs CDATA #IMPLIED> - -<!ATTLIST html xmlns:atom CDATA #IMPLIED> -<!ATTLIST html xmlns:bufsz CDATA #IMPLIED> -<!ATTLIST html xmlns:da CDATA #IMPLIED> -<!ATTLIST html xmlns:dman CDATA #IMPLIED> -<!ATTLIST html xmlns:ev CDATA #IMPLIED> -<!ATTLIST html xmlns:ia CDATA #IMPLIED> -<!ATTLIST html xmlns:log CDATA #IMPLIED> -<!ATTLIST html xmlns:lv2 CDATA #IMPLIED> -<!ATTLIST html xmlns:midi CDATA #IMPLIED> -<!ATTLIST html xmlns:morph CDATA #IMPLIED> -<!ATTLIST html xmlns:opts CDATA #IMPLIED> -<!ATTLIST html xmlns:param CDATA #IMPLIED> -<!ATTLIST html xmlns:patch CDATA #IMPLIED> -<!ATTLIST html xmlns:pg CDATA #IMPLIED> -<!ATTLIST html xmlns:pprops CDATA #IMPLIED> -<!ATTLIST html xmlns:pset CDATA #IMPLIED> -<!ATTLIST html xmlns:rsz CDATA #IMPLIED> -<!ATTLIST html xmlns:state CDATA #IMPLIED> -<!ATTLIST html xmlns:time CDATA #IMPLIED> -<!ATTLIST html xmlns:ui CDATA #IMPLIED> -<!ATTLIST html xmlns:umap CDATA #IMPLIED> -<!ATTLIST html xmlns:units CDATA #IMPLIED> -<!ATTLIST html xmlns:urid CDATA #IMPLIED> -<!ATTLIST html xmlns:work CDATA #IMPLIED> +<!ATTLIST html xmlns:xsd CDATA #IMPLIED> diff --git a/lv2specgen/DTD/xhtml-ruby-1.mod b/lv2specgen/DTD/xhtml-ruby-1.mod new file mode 100644 index 0000000..978da8d --- /dev/null +++ b/lv2specgen/DTD/xhtml-ruby-1.mod @@ -0,0 +1,242 @@ +<!-- ...................................................................... --> +<!-- XHTML Ruby Module .................................................... --> +<!-- file: xhtml-ruby-1.mod + + This is XHTML, a reformulation of HTML as a modular XML application. + Copyright 1999-2001 W3C (MIT, INRIA, Keio), All Rights Reserved. + Revision: $Id: xhtml-ruby-1.mod,v 1.1 2008/06/21 19:42:10 smccarro Exp $ + + This module is based on the W3C Ruby Annotation Specification: + + http://www.w3.org/TR/ruby + + This DTD module is identified by the PUBLIC and SYSTEM identifiers: + + PUBLIC "-//W3C//ELEMENTS XHTML Ruby 1.0//EN" + SYSTEM "http://www.w3.org/TR/ruby/xhtml-ruby-1.mod" + + ...................................................................... --> + +<!-- Ruby Elements + + ruby, rbc, rtc, rb, rt, rp + + This module declares the elements and their attributes used to + support ruby annotation markup. +--> + +<!-- declare qualified element type names: +--> +<!ENTITY % ruby.qname "ruby" > +<!ENTITY % rbc.qname "rbc" > +<!ENTITY % rtc.qname "rtc" > +<!ENTITY % rb.qname "rb" > +<!ENTITY % rt.qname "rt" > +<!ENTITY % rp.qname "rp" > + +<!-- rp fallback is included by default. +--> +<!ENTITY % Ruby.fallback "INCLUDE" > +<!ENTITY % Ruby.fallback.mandatory "IGNORE" > + +<!-- Complex ruby is included by default; it may be + overridden by other modules to ignore it. +--> +<!ENTITY % Ruby.complex "INCLUDE" > + +<!-- Fragments for the content model of the ruby element --> +<![%Ruby.fallback;[ +<![%Ruby.fallback.mandatory;[ +<!ENTITY % Ruby.content.simple + "( %rb.qname;, %rp.qname;, %rt.qname;, %rp.qname; )" +> +]]> +<!ENTITY % Ruby.content.simple + "( %rb.qname;, ( %rt.qname; | ( %rp.qname;, %rt.qname;, %rp.qname; ) ) )" +> +]]> +<!ENTITY % Ruby.content.simple "( %rb.qname;, %rt.qname; )" > + +<![%Ruby.complex;[ +<!ENTITY % Ruby.content.complex + "| ( %rbc.qname;, %rtc.qname;, %rtc.qname;? )" +> +]]> +<!ENTITY % Ruby.content.complex "" > + +<!-- Content models of the rb and the rt elements are intended to + allow other inline-level elements of its parent markup language, + but it should not include ruby descendent elements. The following + parameter entity %NoRuby.content; can be used to redefine + those content models with minimum effort. It's defined as + '( #PCDATA )' by default. +--> +<!ENTITY % NoRuby.content "( #PCDATA )" > + +<!-- one or more digits (NUMBER) --> +<!ENTITY % Number.datatype "CDATA" > + +<!-- ruby element ...................................... --> + +<!ENTITY % ruby.element "INCLUDE" > +<![%ruby.element;[ +<!ENTITY % ruby.content + "( %Ruby.content.simple; %Ruby.content.complex; )" +> +<!ELEMENT %ruby.qname; %ruby.content; > +<!-- end of ruby.element -->]]> + +<![%Ruby.complex;[ +<!-- rbc (ruby base component) element ................. --> + +<!ENTITY % rbc.element "INCLUDE" > +<![%rbc.element;[ +<!ENTITY % rbc.content + "(%rb.qname;)+" +> +<!ELEMENT %rbc.qname; %rbc.content; > +<!-- end of rbc.element -->]]> + +<!-- rtc (ruby text component) element ................. --> + +<!ENTITY % rtc.element "INCLUDE" > +<![%rtc.element;[ +<!ENTITY % rtc.content + "(%rt.qname;)+" +> +<!ELEMENT %rtc.qname; %rtc.content; > +<!-- end of rtc.element -->]]> +]]> + +<!-- rb (ruby base) element ............................ --> + +<!ENTITY % rb.element "INCLUDE" > +<![%rb.element;[ +<!-- %rb.content; uses %NoRuby.content; as its content model, + which is '( #PCDATA )' by default. It may be overridden + by other modules to allow other inline-level elements + of its parent markup language, but it should not include + ruby descendent elements. +--> +<!ENTITY % rb.content "%NoRuby.content;" > +<!ELEMENT %rb.qname; %rb.content; > +<!-- end of rb.element -->]]> + +<!-- rt (ruby text) element ............................ --> + +<!ENTITY % rt.element "INCLUDE" > +<![%rt.element;[ +<!-- %rt.content; uses %NoRuby.content; as its content model, + which is '( #PCDATA )' by default. It may be overridden + by other modules to allow other inline-level elements + of its parent markup language, but it should not include + ruby descendent elements. +--> +<!ENTITY % rt.content "%NoRuby.content;" > + +<!ELEMENT %rt.qname; %rt.content; > +<!-- end of rt.element -->]]> + +<!-- rbspan attribute is used for complex ruby only ...... --> +<![%Ruby.complex;[ +<!ENTITY % rt.attlist "INCLUDE" > +<![%rt.attlist;[ +<!ATTLIST %rt.qname; + rbspan %Number.datatype; "1" +> +<!-- end of rt.attlist -->]]> +]]> + +<!-- rp (ruby parenthesis) element ..................... --> + +<![%Ruby.fallback;[ +<!ENTITY % rp.element "INCLUDE" > +<![%rp.element;[ +<!ENTITY % rp.content + "( #PCDATA )" +> +<!ELEMENT %rp.qname; %rp.content; > +<!-- end of rp.element -->]]> +]]> + +<!-- Ruby Common Attributes + + The following optional ATTLIST declarations provide an easy way + to define common attributes for ruby elements. These declarations + are ignored by default. + + Ruby elements are intended to have common attributes of its + parent markup language. For example, if a markup language defines + common attributes as a parameter entity %attrs;, you may add + those attributes by just declaring the following parameter entities + + <!ENTITY % Ruby.common.attlists "INCLUDE" > + <!ENTITY % Ruby.common.attrib "%attrs;" > + + before including the Ruby module. +--> + +<!ENTITY % Ruby.common.attlists "IGNORE" > +<![%Ruby.common.attlists;[ +<!ENTITY % Ruby.common.attrib "" > + +<!-- common attributes for ruby ........................ --> + +<!ENTITY % Ruby.common.attlist "INCLUDE" > +<![%Ruby.common.attlist;[ +<!ATTLIST %ruby.qname; + %Ruby.common.attrib; +> +<!-- end of Ruby.common.attlist -->]]> + +<![%Ruby.complex;[ +<!-- common attributes for rbc ......................... --> + +<!ENTITY % Rbc.common.attlist "INCLUDE" > +<![%Rbc.common.attlist;[ +<!ATTLIST %rbc.qname; + %Ruby.common.attrib; +> +<!-- end of Rbc.common.attlist -->]]> + +<!-- common attributes for rtc ......................... --> + +<!ENTITY % Rtc.common.attlist "INCLUDE" > +<![%Rtc.common.attlist;[ +<!ATTLIST %rtc.qname; + %Ruby.common.attrib; +> +<!-- end of Rtc.common.attlist -->]]> +]]> + +<!-- common attributes for rb .......................... --> + +<!ENTITY % Rb.common.attlist "INCLUDE" > +<![%Rb.common.attlist;[ +<!ATTLIST %rb.qname; + %Ruby.common.attrib; +> +<!-- end of Rb.common.attlist -->]]> + +<!-- common attributes for rt .......................... --> + +<!ENTITY % Rt.common.attlist "INCLUDE" > +<![%Rt.common.attlist;[ +<!ATTLIST %rt.qname; + %Ruby.common.attrib; +> +<!-- end of Rt.common.attlist -->]]> + +<![%Ruby.fallback;[ +<!-- common attributes for rp .......................... --> + +<!ENTITY % Rp.common.attlist "INCLUDE" > +<![%Rp.common.attlist;[ +<!ATTLIST %rp.qname; + %Ruby.common.attrib; +> +<!-- end of Rp.common.attlist -->]]> +]]> +]]> + +<!-- end of xhtml-ruby-1.mod --> diff --git a/lv2specgen/lv2docgen.py b/lv2specgen/lv2docgen.py index 23a239d..271e434 100755 --- a/lv2specgen/lv2docgen.py +++ b/lv2specgen/lv2docgen.py @@ -1,82 +1,87 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# lv2docgen, a documentation generator for LV2 plugins +#!/usr/bin/env python3 + # Copyright 2012 David Robillard <d@drobilla.net> -# -# Permission to use, copy, modify, and/or distribute this software for any -# purpose with or without fee is hereby granted, provided that the above -# copyright notice and this permission notice appear in all copies. -# -# THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# SPDX-License-Identifier: ISC + +"""LV2 plugin documentation generator.""" + +# pylint: disable=consider-using-f-string +# pylint: disable=missing-function-docstring +# pylint: disable=redefined-outer-name +# pylint: disable=invalid-name import errno import os import sys -__date__ = '2012-03-27' -__version__ = '0.0.0' -__authors__ = 'David Robillard' -__license__ = 'ISC License <http://www.opensource.org/licenses/isc>' -__contact__ = 'devel@lists.lv2plug.in' +__date__ = "2012-03-27" +__version__ = "0.0.0" +__authors__ = "David Robillard" +__license__ = "ISC License <http://www.opensource.org/licenses/isc>" +__contact__ = "devel@lists.lv2plug.in" try: import rdflib except ImportError: - sys.exit('Error importing rdflib') + sys.exit("Error importing rdflib") + +doap = rdflib.Namespace("http://usefulinc.com/ns/doap#") +lv2 = rdflib.Namespace("http://lv2plug.in/ns/lv2core#") +rdf = rdflib.Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#") +rdfs = rdflib.Namespace("http://www.w3.org/2000/01/rdf-schema#") -doap = rdflib.Namespace('http://usefulinc.com/ns/doap#') -lv2 = rdflib.Namespace('http://lv2plug.in/ns/lv2core#') -rdf = rdflib.Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#') -rdfs = rdflib.Namespace('http://www.w3.org/2000/01/rdf-schema#') def uri_to_path(uri): - path = uri[uri.find(':'):] + first_colon = uri.find(":") + path = uri[first_colon:] while not path[0].isalpha(): path = path[1:] return path + def get_doc(model, subject): comment = model.value(subject, rdfs.comment, None) if comment: return '<p class="content">%s</p>' % comment - return '' + return "" + def port_doc(model, port): name = model.value(port, lv2.name, None) - comment = model.value(port, rdfs.comment, None) html = '<div class="specterm"><h3>%s</h3>' % name html += get_doc(model, port) - html += '</div>' + html += "</div>" return html + def plugin_doc(model, plugin, style_uri): - uri = str(plugin) + uri = str(plugin) name = model.value(plugin, doap.name, None) - html = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd"> + dtd = "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd" + html = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" %s> <html about="%s" xmlns="http://www.w3.org/1999/xhtml" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" xmlns:lv2="http://lv2plug.in/ns/lv2core#" - xml:lang="en">''' % uri + xml:lang="en">""" % ( + uri, + dtd, + ) - html += '''<head> + html += """<head> <title>%s</title> <meta http-equiv="content-type" content="text/xhtml+xml; charset=utf-8" /> <meta name="generator" content="lv2docgen" /> <link href="%s" rel="stylesheet" type="text/css" /> </head> - <body>''' % (name, style_uri) + <body>""" % ( + name, + style_uri, + ) - html += ''' + html += """ <!-- HEADER --> <div id="header"> <h1 id="title">%s</h1> @@ -85,44 +90,51 @@ def plugin_doc(model, plugin, style_uri): <tr><th>Version</th><td>%s</td></tr> </table> </div> -''' % (name, uri, uri, '0.0.0') +""" % ( + name, + uri, + uri, + "0.0.0", + ) html += get_doc(model, plugin) - ports_html = '' - for p in model.triples([plugin, lv2.port, None]): - ports_html += port_doc(model, p[2]) + ports_html = "" + for link in model.triples([plugin, lv2.port, None]): + ports_html += port_doc(model, link[2]) - if len(ports_html): - html += ''' + if ports_html: + html += ( + """ <h2 class="sec">Ports</h2> <div class="content"> %s - </div>''' % ports_html + </div>""" + % ports_html + ) - html += ' </body></html>' + html += " </body></html>" return html -if __name__ == '__main__': - 'LV2 plugin documentation generator' +if __name__ == "__main__": if len(sys.argv) < 2: - print('Usage: %s OUTDIR FILE...' % sys.argv[0]) + print("Usage: %s OUTDIR FILE..." % sys.argv[0]) sys.exit(1) outdir = sys.argv[1] files = sys.argv[2:] model = rdflib.ConjunctiveGraph() for f in files: - model.parse(f, format='n3') + model.parse(f, format="n3") - style_uri = os.path.abspath(os.path.join(outdir, 'style.css')) + style_uri = os.path.abspath(os.path.join(outdir, "style.css")) for p in model.triples([None, rdf.type, lv2.Plugin]): plugin = p[0] html = plugin_doc(model, plugin, style_uri) path = uri_to_path(plugin) - outpath = os.path.join(outdir, path + '.html') + outpath = os.path.join(outdir, path + ".html") try: os.makedirs(os.path.dirname(outpath)) except OSError: @@ -132,7 +144,6 @@ if __name__ == '__main__': else: raise - print('Writing <%s> documentation to %s' % (plugin, outpath)) - out = open(outpath, 'w') - out.write(html) - out.close() + print("Writing <%s> documentation to %s" % (plugin, outpath)) + with open(outpath, "w", encoding="utf-8") as out: + out.write(html) diff --git a/lv2specgen/lv2specgen.py b/lv2specgen/lv2specgen.py index 14c88c3..c8112de 100755 --- a/lv2specgen/lv2specgen.py +++ b/lv2specgen/lv2specgen.py @@ -1,48 +1,48 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 # -# lv2specgen, a documentation generator for LV2 specifications. -# Copyright (c) 2009-2014 David Robillard <d@drobilla.net> +# Copyright 2009-2026 David Robillard <d@drobilla.net> +# Copyright 2003-2008 Christopher Schmidt <crschmidt@crschmidt.net> +# Copyright 2005-2008 Uldis Bojars <uldis.bojars@deri.org> +# Copyright 2007-2008 Sergio Fernández <sergio.fernandez@fundacionctic.org> +# SPDX-License-Identifier: MIT # # Based on SpecGen: # <http://forge.morfeo-project.org/wiki_en/index.php/SpecGen> -# Copyright (c) 2003-2008 Christopher Schmidt <crschmidt@crschmidt.net> -# Copyright (c) 2005-2008 Uldis Bojars <uldis.bojars@deri.org> -# Copyright (c) 2007-2008 Sergio Fernández <sergio.fernandez@fundacionctic.org> -# -# This software is licensed under the terms of the MIT License. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. + +"""Ontology specification generator tool.""" + +# pylint: disable=broad-exception-caught +# pylint: disable=c-extension-no-member +# pylint: disable=cell-var-from-loop +# pylint: disable=consider-using-f-string +# pylint: disable=global-statement +# pylint: disable=import-outside-toplevel +# pylint: disable=invalid-name +# pylint: disable=missing-class-docstring +# pylint: disable=missing-function-docstring +# pylint: disable=no-member +# pylint: disable=too-few-public-methods +# pylint: disable=too-many-arguments +# pylint: disable=too-many-branches +# pylint: disable=too-many-lines +# pylint: disable=too-many-locals +# pylint: disable=too-many-positional-arguments +# pylint: disable=too-many-statements import datetime -import markdown -import markdown.extensions import optparse import os import re import sys import time -import xml.sax.saxutils import xml.dom import xml.dom.minidom +import xml.sax.saxutils + +import markdown +import markdown.extensions -__date__ = "2011-10-26" +__date__ = "2026-02-07" __version__ = __date__.replace("-", ".") __authors__ = """ Christopher Schmidt, @@ -67,7 +67,7 @@ try: have_pygments = True except ImportError: - print("Error importing pygments, syntax highlighting disabled") + sys.stderr.write("warning: Pygments syntax highlighting unavailable\n") have_pygments = False try: @@ -86,20 +86,13 @@ spec_pre = None spec_bundle = None specgendir = None ns_list = { + "http://purl.org/dc/terms/": "dcterms", + "http://usefulinc.com/ns/doap#": "doap", + "http://xmlns.com/foaf/0.1/": "foaf", + "http://www.w3.org/2002/07/owl#": "owl", "http://www.w3.org/1999/02/22-rdf-syntax-ns#": "rdf", "http://www.w3.org/2000/01/rdf-schema#": "rdfs", - "http://www.w3.org/2002/07/owl#": "owl", "http://www.w3.org/2001/XMLSchema#": "xsd", - "http://rdfs.org/sioc/ns#": "sioc", - "http://xmlns.com/foaf/0.1/": "foaf", - "http://purl.org/dc/elements/1.1/": "dc", - "http://purl.org/dc/terms/": "dct", - "http://purl.org/rss/1.0/modules/content/": "content", - "http://www.w3.org/2003/01/geo/wgs84_pos#": "geo", - "http://www.w3.org/2004/02/skos/core#": "skos", - "http://lv2plug.in/ns/lv2core#": "lv2", - "http://usefulinc.com/ns/doap#": "doap", - "http://ontologi.es/doap-changeset#": "dcs", } rdf = rdflib.Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#") @@ -107,10 +100,27 @@ rdfs = rdflib.Namespace("http://www.w3.org/2000/01/rdf-schema#") owl = rdflib.Namespace("http://www.w3.org/2002/07/owl#") lv2 = rdflib.Namespace("http://lv2plug.in/ns/lv2core#") doap = rdflib.Namespace("http://usefulinc.com/ns/doap#") -dcs = rdflib.Namespace("http://ontologi.es/doap-changeset#") foaf = rdflib.Namespace("http://xmlns.com/foaf/0.1/") +class DTDResolver(etree.Resolver): + def resolve(self, url, _pubid, context): + path = os.path.join(specgendir, url) + return self.resolve_filename(path, context) + + +def log_error(msg): + sys.stderr.write("error: ") + sys.stderr.write(msg) + sys.stderr.write("\n") + + +def log_warning(msg): + sys.stderr.write("warning: ") + sys.stderr.write(msg) + sys.stderr.write("\n") + + def findStatements(model, s, p, o): return model.triples([s, p, o]) @@ -140,39 +150,38 @@ def getLiteralString(s): def isResource(n): - return type(n) == rdflib.URIRef + return isinstance(n, rdflib.URIRef) def isBlank(n): - return type(n) == rdflib.BNode + return isinstance(n, rdflib.BNode) def isLiteral(n): - return type(n) == rdflib.Literal + return isinstance(n, rdflib.Literal) def niceName(uri): - global spec_bundle if uri.startswith(spec_ns_str): - return uri[len(spec_ns_str) :] - elif uri == str(rdfs.seeAlso): + return uri.replace(spec_ns_str, "") + + if uri == str(rdfs.seeAlso): return "See also" regexp = re.compile("^(.*[/#])([^/#]+)$") rez = regexp.search(uri) if not rez: return uri + pref = rez.group(1) if pref in ns_list: return ns_list.get(pref, pref) + ":" + rez.group(2) - else: - print("warning: prefix %s not in ns list:" % pref) - print(ns_list) - return uri + + return uri -def termName(m, urinode): - "Trims the namespace out of a term to give a name to the term." +def termName(urinode): + """Trims the namespace out of a term to give a name to the term.""" return str(urinode).replace(spec_ns_str, "") @@ -180,17 +189,17 @@ def getLabel(m, urinode): statement = findOne(m, urinode, rdfs.label, None) if statement: return getLiteralString(getObject(statement)) - else: - return "" + + return "" def linkifyCodeIdentifiers(string): - "Add links to code documentation for identifiers like LV2_Type" + """Add links to code documentation for identifiers like LV2_Type.""" - if linkmap == {}: + if not linkmap: return string - if string in linkmap.keys(): + if string in linkmap: # Exact match for complete string return linkmap[string] @@ -207,7 +216,7 @@ def linkifyCodeIdentifiers(string): def linkifyVocabIdentifiers(m, string, classlist, proplist, instalist): - "Add links to vocabulary documentation for prefixed names like eg:Thing" + """Add links to vocabulary documentation for prefixed names.""" rgx = re.compile("([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)") namespaces = getNamespaces(m) @@ -223,15 +232,16 @@ def linkifyVocabIdentifiers(m, string, classlist, proplist, instalist): or (instalist and uri in instalist) or (proplist and uri in proplist) ): - print("warning: Link to undefined resource <%s>\n" % text) + log_warning("Link to undefined resource <%s>" % text) return '<a href="#%s">%s</a>' % (name, name) - elif prefix in namespaces: + + if prefix in namespaces: return '<a href="%s">%s</a>' % ( namespaces[match.group(1)] + match.group(2), match.group(0), ) - else: - return text + + return text return rgx.sub(translateLink, string) @@ -239,9 +249,9 @@ def linkifyVocabIdentifiers(m, string, classlist, proplist, instalist): def prettifyHtml(m, markup, subject, classlist, proplist, instalist): # Syntax highlight all C code if have_pygments: - code_rgx = re.compile('<pre class="c-code">(.*?)</pre>', re.DOTALL) + code_re = re.compile('<pre class="c-code">(.*?)</pre>', re.DOTALL) while True: - code = code_rgx.search(markup) + code = code_re.search(markup) if not code: break match_str = xml.sax.saxutils.unescape(code.group(1)) @@ -250,15 +260,13 @@ def prettifyHtml(m, markup, subject, classlist, proplist, instalist): pygments.lexers.CLexer(), pygments.formatters.HtmlFormatter(), ) - markup = code_rgx.sub(code_str, markup, 1) + markup = code_re.sub(code_str, markup, 1) # Syntax highlight all Turtle code if have_pygments: - code_rgx = re.compile( - '<pre class="turtle-code">(.*?)</pre>', re.DOTALL - ) + code_re = re.compile('<pre class="turtle-code">(.*?)</pre>', re.DOTALL) while True: - code = code_rgx.search(markup) + code = code_re.search(markup) if not code: break match_str = xml.sax.saxutils.unescape(code.group(1)) @@ -267,7 +275,7 @@ def prettifyHtml(m, markup, subject, classlist, proplist, instalist): pygments.lexers.rdf.TurtleLexer(), pygments.formatters.HtmlFormatter(), ) - markup = code_rgx.sub(code_str, markup, 1) + markup = code_re.sub(code_str, markup, 1) # Add links to code documentation for identifiers markup = linkifyCodeIdentifiers(markup) @@ -283,21 +291,24 @@ def prettifyHtml(m, markup, subject, classlist, proplist, instalist): space = match.group(1) name = match.group(2) uri = rdflib.URIRef(spec_ns + name) - if ( + known = ( (classlist and uri in classlist) or (instalist and uri in instalist) or (proplist and uri in proplist) - ): + ) + if known: return '%s<a href="#%s">%s</a>' % (space, name, name) - else: - print("warning: Link to undefined resource <%s>\n" % name) - return text + + log_warning("Link to undefined resource <%s>" % name) + return text markup = rgx.sub(translateLocalLink, markup) if not have_lxml: - print("warning: No Python lxml module found, output may be invalid") + log_warning("No Python lxml module found, output may be invalid") else: + oldcwd = os.getcwd() + try: # Parse and validate documentation as XHTML Basic 1.1 doc = ( @@ -316,15 +327,15 @@ def prettifyHtml(m, markup, subject, classlist, proplist, instalist): </html>""" ) - oldcwd = os.getcwd() os.chdir(specgendir) parser = etree.XMLParser(dtd_validation=True, no_network=True) etree.fromstring(doc.encode("utf-8"), parser) except Exception as e: - print("Invalid documentation for %s\n%s" % (subject, e)) + log_error("Invalid documentation for %s:" % subject) + sys.stderr.write(str(e) + "\n") line_num = 1 for line in doc.split("\n"): - print("%3d: %s" % (line_num, line)) + sys.stderr.write("%3d: %s\n" % (line_num, line)) line_num += 1 finally: os.chdir(oldcwd) @@ -350,11 +361,11 @@ def formatDoc(m, urinode, literal, classlist, proplist, instalist): doc = doc.replace("</%s>\n" % tag, "") return prettifyHtml(m, doc, urinode, classlist, proplist, instalist) - else: - doc = xml.sax.saxutils.escape(string) - doc = linkifyCodeIdentifiers(doc) - doc = linkifyVocabIdentifiers(m, doc, classlist, proplist, instalist) - return "<p>%s</p>" % doc + + doc = xml.sax.saxutils.escape(string) + doc = linkifyCodeIdentifiers(doc) + doc = linkifyVocabIdentifiers(m, doc, classlist, proplist, instalist) + return "<p>%s</p>" % doc def getComment(m, subject, classlist, proplist, instalist): @@ -398,7 +409,7 @@ def getFullDocumentation(m, subject, classlist, proplist, instalist): def getProperty(val, first=True): - "Return a string representing a property value in a property table" + """Return a string representing a property value in a property table.""" doc = "" if not first: doc += "<tr><th></th>" # Empty cell in header column @@ -407,16 +418,11 @@ def getProperty(val, first=True): def endProperties(first): - if first: - return "</tr>" - else: - return "" + return "</tr>" if first else "" def rdfsPropertyInfo(term, m): - """Generate HTML for properties: Domain, range""" - global classranges - global classdomains + """Generate HTML for properties: Domain, range.""" doc = "" label = getLabel(m, term) @@ -511,12 +517,13 @@ def getTermLink(uri, subject=None, predicate=None): extra, niceName(uri), ) - else: - return '<a href="%s" %s>%s</a>' % (uri, extra, niceName(uri)) + + return '<a href="%s" %s>%s</a>' % (uri, extra, niceName(uri)) def owlRestrictionInfo(term, m): - """Generate OWL restriction information for Classes""" + """Generate OWL restriction information for Classes.""" + restrictions = [] for s in findStatements(m, term, rdfs.subClassOf, None): if findOne(m, getObject(s), rdf.type, owl.Restriction): @@ -570,8 +577,6 @@ def owlRestrictionInfo(term, m): def rdfsClassInfo(term, m): """Generate rdfs-type information for Classes: ranges, and domains.""" - global classranges - global classdomains doc = "" label = getLabel(m, term) @@ -630,7 +635,8 @@ def rdfsClassInfo(term, m): def isSpecial(pred): - """Return True if `pred` shouldn't be documented generically""" + """Return True if `pred` shouldn't be documented generically.""" + return pred in [ rdf.type, rdfs.range, @@ -674,7 +680,7 @@ def blankNodeDesc(node, m): def extraInfo(term, m): - """Generate information about misc. properties of a term""" + """Generate information about misc. properties of a term.""" doc = "" properties = findStatements(m, term, None, None) first = True @@ -701,7 +707,7 @@ def extraInfo(term, m): def rdfsInstanceInfo(term, m): - """Generate rdfs-type information for instances""" + """Generate rdfs-type information for instances.""" doc = "" label = getLabel(m, term) @@ -740,8 +746,8 @@ def owlInfo(term, m): def owlTypeInfo(term, propertyType, name): if findOne(m, term, rdf.type, propertyType): return "<tr><th>Type</th><td>%s</td></tr>\n" % name - else: - return "" + + return "" res += owlTypeInfo(term, owl.DatatypeProperty, "Datatype Property") res += owlTypeInfo(term, owl.ObjectProperty, "Object Property") @@ -759,21 +765,24 @@ def isDeprecated(m, subject): return deprecated and (str(deprecated[2]).find("true") >= 0) -def docTerms(category, list, m, classlist, proplist, instalist): +def docTerms(category, termlist, m, classlist, proplist, instalist): """ A wrapper class for listing all the terms in a specific class (either Properties, or Classes. Category is 'Property' or 'Class', list is a list of term URI strings, return value is a chunk of HTML. """ doc = "" - for term in list: + for term in termlist: if not term.startswith(spec_ns_str): - sys.stderr.write("warning: Skipping external term `%s'" % term) continue - t = termName(m, term) + t = termName(term) curie = term.split(spec_ns_str[-1])[1] - doc += '<div class="specterm" id="%s" about="%s">' % (t, term) + if t: + doc += '<div class="specterm" id="%s" about="%s">' % (t, term) + else: + doc += '<div class="specterm" about="%s">' % term + doc += '<h3><a href="#%s">%s</a></h3>' % (getAnchor(term), curie) doc += '<span class="spectermtype">%s</span>' % category @@ -822,17 +831,17 @@ def docTerms(category, list, m, classlist, proplist, instalist): def getShortName(uri): uri = str(uri) if "#" in uri: - return uri.split("#")[-1] - else: - return uri.split("/")[-1] + return uri.split("#", maxsplit=1)[-1] + + return uri.split("/", maxsplit=1)[-1] def getAnchor(uri): uri = str(uri) if uri.startswith(spec_ns_str): - return uri[len(spec_ns_str) :].replace("/", "_") - else: - return getShortName(uri) + return uri.replace(spec_ns_str, "").replace("/", "_") + + return getShortName(uri) def buildIndex(m, classlist, proplist, instalist=None): @@ -842,12 +851,12 @@ def buildIndex(m, classlist, proplist, instalist=None): head = "" body = "" - def termLink(m, t): + def termLink(t): if str(t).startswith(spec_ns_str): - name = termName(m, t) + name = termName(t) return '<a href="#%s">%s</a>' % (name, name) - else: - return '<a href="%s">%s</a>' % (str(t), str(t)) + + return '<a href="%s">%s</a>' % (str(t), str(t)) if len(classlist) > 0: head += '<th><a href="#ref-classes" />Classes</th>' @@ -861,13 +870,13 @@ def buildIndex(m, classlist, proplist, instalist=None): local_subclass = False for p in findStatements(m, c, rdfs.subClassOf, None): parent = str(p[2]) - if parent[0 : len(spec_ns_str)] == spec_ns_str: + if parent.startswith(spec_ns_str): local_subclass = True if local_subclass: continue shown[c] = True - body += "<li>" + termLink(m, c) + body += "<li>" + termLink(c) def class_tree(c): tree = "" @@ -878,7 +887,7 @@ def buildIndex(m, classlist, proplist, instalist=None): subclasses += [getSubject(s)] for s in sorted(subclasses): - tree += "<li>" + termLink(m, s) + tree += "<li>" + termLink(s) tree += class_tree(s) tree += "</li>" if tree != "": @@ -893,7 +902,7 @@ def buildIndex(m, classlist, proplist, instalist=None): head += '<th><a href="#ref-properties" />Properties</th>' body += "<td><ul>" for p in sorted(proplist): - body += "<li>%s</li>" % termLink(m, p) + body += "<li>%s</li>" % termLink(p) body += "</ul></td>\n" if instalist is not None and len(instalist) > 0: @@ -925,27 +934,25 @@ def add(where, key, value): def specInformation(m, ns): + """Read through the spec model and return classlist and proplist. + + Global variables classranges and classdomains are also filled as + appropriate. """ - Read through the spec (provided as a Redland model) and return classlist - and proplist. Global variables classranges and classdomains are also filled - as appropriate. - """ - global classranges - global classdomains # Find the class information: Ranges, domains, and list of all names. classtypes = [rdfs.Class, owl.Class, rdfs.Datatype] classlist = [] for onetype in classtypes: for classStatement in findStatements(m, None, rdf.type, onetype): - for range in findStatements( + for rangelink in findStatements( m, None, rdfs.range, getSubject(classStatement) ): if not isBlank(getSubject(classStatement)): add( classranges, str(getSubject(classStatement)), - str(getSubject(range)), + str(getSubject(rangelink)), ) for domain in findStatements( m, None, rdfs.domain, getSubject(classStatement) @@ -979,14 +986,14 @@ def specInformation(m, ns): def specProperty(m, subject, predicate): - "Return a property of the spec." + """Return a property of the spec.""" for c in findStatements(m, subject, predicate, None): return getLiteralString(getObject(c)) return "" def specProperties(m, subject, predicate): - "Return a property of the spec." + """Return a property of the spec.""" properties = [] for c in findStatements(m, subject, predicate, None): properties += [getObject(c)] @@ -994,7 +1001,7 @@ def specProperties(m, subject, predicate): def specAuthors(m, subject): - "Return an HTML description of the authors of the spec." + """Return an HTML description of the authors of the spec.""" subjects = [subject] p = findOne(m, subject, lv2.project, None) @@ -1020,117 +1027,37 @@ def specAuthors(m, subject): for d in sorted(dev): if not first: devdoc += ", " - devdoc += ( - '<span class="author" property="doap:developer">%s</span>' % d - ) + + devdoc += f'<span class="author" property="doap:developer">{d}</span>' first = False if len(dev) == 1: - doc += ( - '<tr><th class="metahead">Developer</th><td>%s</td></tr>' % devdoc - ) + doc += f'<tr><th class="metahead">Developer</th><td>{devdoc}</td></tr>' elif len(dev) > 0: doc += ( - '<tr><th class="metahead">Developers</th><td>%s</td></tr>' % devdoc + f'<tr><th class="metahead">Developers</th><td>{devdoc}</td></tr>' ) maintdoc = "" first = True - for m in sorted(maint): + for name in sorted(maint): if not first: maintdoc += ", " - maintdoc += ( - '<span class="author" property="doap:maintainer">%s</span>' % m - ) - first = False - if len(maint) == 1: - doc += ( - '<tr><th class="metahead">Maintainer</th><td>%s</td></tr>' - % maintdoc - ) - elif len(maint) > 0: - doc += ( - '<tr><th class="metahead">Maintainers</th><td>%s</td></tr>' - % maintdoc - ) - - return doc - - -def releaseChangeset(m, release, prefix=""): - changeset = findOne(m, release, dcs.changeset, None) - if changeset is None: - return "" - - entry = "" - # entry = '<dd><ul>\n' - for i in sorted(findStatements(m, getObject(changeset), dcs.item, None)): - item = getObject(i) - label = findOne(m, item, rdfs.label, None) - if not label: - print("error: dcs:item has no rdfs:label") - continue - - text = getLiteralString(getObject(label)) - if prefix: - text = prefix + ": " + text - - entry += "<li>%s</li>\n" % text - - # entry += '</ul></dd>\n' - return entry - - -def specHistoryEntries(m, subject, entries): - for r in findStatements(m, subject, doap.release, None): - release = getObject(r) - revNode = findOne(m, release, doap.revision, None) - if not revNode: - print("error: doap:release has no doap:revision") - continue - - rev = getLiteralString(getObject(revNode)) - - created = findOne(m, release, doap.created, None) - - dist = findOne(m, release, doap["file-release"], None) - if dist: - entry = '<dt><a href="%s">Version %s</a>' % (getObject(dist), rev) - else: - entry = "<dt>Version %s" % rev - # print("warning: doap:release has no doap:file-release") - - if created: - entry += " (%s)</dt>\n" % getLiteralString(getObject(created)) - else: - entry += ' (<span class="warning">EXPERIMENTAL</span>)</dt>' - - entry += "<dd><ul>\n%s" % releaseChangeset(m, release) - - if dist is not None: - entries[(getObject(created), getObject(dist))] = entry - return entries - - -def specHistoryMarkup(entries): - if len(entries) > 0: - history = "<dl>\n" - for e in sorted(entries.keys(), reverse=True): - history += entries[e] + "</ul></dd>" - history += "</dl>\n" - return history - else: - return "" + maintdoc += '<span class="author" property="doap:maintainer">' + maintdoc += name + maintdoc += "</span>" + first = False + if maint: + label = "Maintainer" if len(maint) == 1 else "Maintainers" + doc += f'<tr><th class="metahead">{label}</th><td>{maintdoc}</td></tr>' -def specHistory(m, subject): - return specHistoryMarkup(specHistoryEntries(m, subject, {})) + return doc def specVersion(m, subject): - """ - Return a (minorVersion, microVersion, date) tuple - """ + """Return a (minorVersion, microVersion, date) tuple.""" + # Get the date from the latest doap release latest_doap_revision = "" latest_doap_release = None @@ -1156,10 +1083,7 @@ def specVersion(m, subject): def getInstances(model, classes, properties): - """ - Extract all resources instanced in the ontology - (aka "everything that is not a class or a property") - """ + """Extract all non-class and non-property instances in the ontology.""" instances = [] for c in classes: for i in findStatements(model, None, rdf.type, c): @@ -1183,7 +1107,7 @@ def getInstances(model, classes, properties): def load_tags(path, docdir): - "Build a (symbol => URI) map from a Doxygen tag file." + """Build a (symbol => URI) map from a Doxygen tag file.""" if not path or not docdir: return {} @@ -1206,23 +1130,22 @@ def load_tags(path, docdir): anchor, sym, ) - else: - return '<span><a href="%s/%s">%s</a></span>' % ( - docdir, - filename, - sym, - ) + + return '<span><a href="%s/%s">%s</a></span>' % ( + docdir, + filename, + sym, + ) tagdoc = xml.dom.minidom.parse(path) root = tagdoc.documentElement - linkmap = {} + result = {} for cn in root.childNodes: if ( cn.nodeType == xml.dom.Node.ELEMENT_NODE and cn.tagName == "compound" and cn.getAttribute("kind") != "page" ): - name = getChildText(cn, "name") filename = getChildText(cn, "filename") anchor = getChildText(cn, "anchor") @@ -1230,7 +1153,7 @@ def load_tags(path, docdir): filename += ".html" if cn.getAttribute("kind") != "group": - linkmap[name] = linkTo(filename, anchor, name) + result[name] = linkTo(filename, anchor, name) prefix = "" if cn.getAttribute("kind") == "struct": @@ -1241,134 +1164,20 @@ def load_tags(path, docdir): mname = prefix + getChildText(m, "name") mafile = getChildText(m, "anchorfile") manchor = getChildText(m, "anchor") - linkmap[mname] = linkTo(mafile, manchor, mname) - - return linkmap - - -def writeIndex(model, specloc, index_path, root_path, root_uri, online): - # Get extension URI - ext_node = model.value(None, rdf.type, lv2.Specification) - if not ext_node: - ext_node = model.value(None, rdf.type, owl.Ontology) - if not ext_node: - print("no extension found in %s" % bundle) - sys.exit(1) - - ext = str(ext_node) - - # Get version - minor = 0 - micro = 0 - try: - minor = int(model.value(ext_node, lv2.minorVersion, None)) - micro = int(model.value(ext_node, lv2.microVersion, None)) - except Exception: - print("warning: %s: failed to find version for %s" % (bundle, ext)) - - # Get date - date = None - for r in model.triples([ext_node, doap.release, None]): - revision = model.value(r[2], doap.revision, None) - if str(revision) == ("%d.%d" % (minor, micro)): - date = model.value(r[2], doap.created, None) - break - - # Verify that this date is the latest - if date is None: - print("warning: %s has no doap:created date" % ext_node) - else: - for r in model.triples([ext_node, doap.release, None]): - this_date = model.value(r[2], doap.created, None) - if this_date is None: - print( - "warning: %s has no doap:created date" - % (ext_node, minor, micro, date) - ) - continue - - if this_date > date: - print( - "warning: %s revision %d.%d (%s) is not the latest release" - % (ext_node, minor, micro, date) - ) - break - - # Get name and short description - name = model.value(ext_node, doap.name, None) - shortdesc = model.value(ext_node, doap.shortdesc, None) - - # Chop 'LV2' prefix from name for cleaner index - if name.startswith("LV2 "): - name = name[4:] - - # Find relative link target - if root_uri and ext_node.startswith(root_uri): - target = ext_node[len(root_uri) :] - else: - target = os.path.relpath(ext_node, root_path) - - if not online: - target += ".html" - - stem = os.path.splitext(os.path.basename(target))[0] - - # Specification (comment is to act as a sort key) - row = '<tr><!-- %s --><td><a rel="rdfs:seeAlso" href="%s">%s</a></td>' % ( - b, - target, - name, - ) - - # API - row += "<td>" - row += '<a rel="rdfs:seeAlso" href="../doc/html/group__%s.html">%s</a>' % ( - stem, - name, - ) - row += "</td>" - - # Description - if shortdesc: - row += "<td>" + str(shortdesc) + "</td>" - else: - row += "<td></td>" - - # Version - version_str = "%s.%s" % (minor, micro) - if minor == 0 or (micro % 2 != 0): - row += '<td><span style="color: red">' + version_str + "</span></td>" - else: - row += "<td>" + version_str + "</td>" + result[mname] = linkTo(mafile, manchor, mname) - # Status - deprecated = model.value(ext_node, owl.deprecated, None) - if minor == 0: - row += '<td><span class="error">Experimental</span></td>' - elif deprecated and str(deprecated[2]) != "false": - row += '<td><span class="warning">Deprecated</span></td>' - elif micro % 2 == 0: - row += '<td><span class="success">Stable</span></td>' - - row += "</tr>" - - index = open(index_path, "w") - index.write(row) - index.close() + return result def specgen( specloc, - indir, + template_path, style_uri, docdir, tags, opts, instances=False, - root_link=None, - index_path=None, - root_path=None, - root_uri=None, + validate=False, ): """The meat and potatoes: Everything starts here.""" @@ -1377,24 +1186,30 @@ def specgen( global spec_ns_str global spec_ns global spec_pre - global ns_list - global specgendir global linkmap spec_bundle = "file://%s/" % os.path.abspath(os.path.dirname(specloc)) - specgendir = os.path.abspath(indir) # Template - temploc = os.path.join(indir, "template.html") - template = None - f = open(temploc, "r") - template = f.read() - f.close() + with open(template_path, "r", encoding="utf-8") as f: + template = f.read() # Load code documentation link map from tags file linkmap = load_tags(tags, docdir) - m = rdflib.ConjunctiveGraph() + # Create a new empty dataset + rdflib_major = int(rdflib.__version__.split(".")[0]) + rdflib_minor = int(rdflib.__version__.split(".")[1]) + if rdflib_major > 7 or rdflib_major == 7 and rdflib_minor >= 5: + m = rdflib.Dataset() + else: + m = rdflib.ConjunctiveGraph() + + # RDFLib adds its own prefixes, so kludge around "time" prefix conflict + m.namespace_manager.bind( + "time", rdflib.URIRef("http://lv2plug.in/ns/ext/time#"), replace=True + ) + manifest_path = os.path.join(os.path.dirname(specloc), "manifest.ttl") if os.path.exists(manifest_path): m.parse(manifest_path, format="n3") @@ -1444,7 +1259,7 @@ def specgen( prefixes_html += "</span>" if spec_pre is None: - print("No namespace prefix for %s defined" % specloc) + log_error("No namespace prefix for %s defined" % specloc) sys.exit(1) ns_list[spec_ns_str] = spec_pre @@ -1467,6 +1282,7 @@ def specgen( proplist = docTerms( "Property", proplist, m, classlist, proplist, instalist ) + instlist = "" if instances: instlist = docTerms( "Instance", instalist, m, classlist, proplist, instalist @@ -1490,8 +1306,6 @@ def specgen( name = specProperty(m, spec, doap.name) title = name - if root_link: - name = '<a href="%s">%s</a>' % (root_link, name) template = template.replace("@TITLE@", title) template = template.replace("@NAME@", name) @@ -1500,15 +1314,9 @@ def specgen( ) template = template.replace("@URI@", spec) template = template.replace("@PREFIX@", spec_pre) - if spec_pre == "lv2": - template = template.replace("@XMLNS@", "") - else: - template = template.replace( - "@XMLNS@", ' xmlns:%s="%s"' % (spec_pre, spec_ns_str) - ) filename = os.path.basename(specloc) - basename = filename[0 : filename.rfind(".")] + basename = os.path.splitext(filename)[0] template = template.replace("@STYLE_URI@", style_uri) template = template.replace("@PREFIXES@", str(prefixes_html)) @@ -1518,7 +1326,6 @@ def specgen( template = template.replace("@REFERENCE@", termlist) template = template.replace("@FILENAME@", filename) template = template.replace("@HEADER@", basename + ".h") - template = template.replace("@HISTORY@", specHistory(m, spec)) mail_row = "" if "list_email" in opts: @@ -1560,27 +1367,28 @@ def specgen( template = template.replace("@DESCRIPTION@", docs) now = int(os.environ.get("SOURCE_DATE_EPOCH", time.time())) - build_date = datetime.datetime.utcfromtimestamp(now) + build_date = datetime.datetime.fromtimestamp(now, datetime.timezone.utc) template = template.replace("@DATE@", build_date.strftime("%F")) template = template.replace("@TIME@", build_date.strftime("%F %H:%M UTC")) - - # Write index row - if index_path is not None: - writeIndex(m, specloc, index_path, root_path, root_uri, opts["online"]) + if not validate: + return template # Validate complete output page + oldcwd = os.getcwd() try: - oldcwd = os.getcwd() os.chdir(specgendir) + parser = etree.XMLParser(dtd_validation=True, no_network=True) + parser.resolvers.add(DTDResolver()) + etree.fromstring( template.replace( '"http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd"', '"DTD/xhtml-rdfa-1.dtd"', ).encode("utf-8"), - etree.XMLParser(dtd_validation=True, no_network=True), + parser, ) except Exception as e: - sys.stderr.write("error: Validation failed for %s: %s" % (specloc, e)) + log_error("Validation failed for %s: %s" % (specloc, e)) finally: os.chdir(oldcwd) @@ -1589,17 +1397,16 @@ def specgen( def save(path, text): try: - f = open(path, "w") - f.write(text) - f.flush() - f.close() + with open(path, "w", encoding="utf-8") as f: + f.write(text) + f.flush() except Exception: e = sys.exc_info()[1] - print('Error writing to file "' + path + '": ' + str(e)) + log_error('Error writing to file "%s": %s' % (path, e)) def getNamespaces(m): - """Return a prefix:URI dictionary of all namespaces seen during parsing""" + """Return a prefix:URI dictionary of all namespaces seen during parsing.""" nspaces = {} for prefix, uri in m.namespaces(): if not re.match("default[0-9]*", prefix) and not prefix == "xml": @@ -1628,12 +1435,59 @@ def usage(): return "Usage: %s ONTOLOGY_TTL OUTPUT_HTML [OPTION]..." % script -if __name__ == "__main__": - """Ontology specification generator tool""" +def _path_from_env(variable, default): + value = os.environ.get(variable) + return value if value and os.path.isabs(value) else default + + +def _paths_from_env(variable, default): + paths = [] + value = os.environ.get(variable) + if value: + paths = [p for p in value.split(os.pathsep) if os.path.isabs(p)] + + return paths if paths else default + + +def _data_dirs(): + return _paths_from_env( + "XDG_DATA_DIRS", ["/usr/local/share/", "/usr/share/"] + ) + + +def main(): + """Main program that parses the program arguments and runs.""" + + global spec_pre + global specgendir - indir = os.path.abspath(os.path.dirname(sys.argv[0])) - if not os.path.exists(os.path.join(indir, "template.html")): - indir = os.path.join(os.path.dirname(indir), "share", "lv2specgen") + script_dir = os.path.dirname(os.path.realpath(__file__)) + if os.path.exists(os.path.join(script_dir, "template.html")): + # Run from source repository + specgendir = script_dir + lv2_source_root = os.path.dirname(script_dir) + default_template_path = os.path.join(script_dir, "template.html") + default_style_dir = os.path.join(lv2_source_root, "doc", "style") + else: + data_dir = None + for d in _data_dirs(): + path = os.path.join(d, "lv2specgen") + files_exist = all( + os.path.exists(os.path.join(path, name)) + for name in ["template.html", "style.css", "pygments.css"] + ) + if files_exist: + data_dir = path + break + + if data_dir: + # Use installed files + specgendir = data_dir + default_template_path = os.path.join(data_dir, "template.html") + default_style_dir = data_dir + else: + log_error("Unable to find lv2specgen data") + sys.exit(-2) opt = optparse.OptionParser( usage=usage(), @@ -1652,11 +1506,18 @@ if __name__ == "__main__": help="Mailing list info page address", ) opt.add_option( - "--template-dir", + "--template", + type="string", + dest="template", + default=default_template_path, + help="Template file for output page", + ) + opt.add_option( + "--style-dir", type="string", - dest="template_dir", - default=indir, - help="Template directory", + dest="style_dir", + default=default_style_dir, + help="Stylesheet directory path", ) opt.add_option( "--style-uri", @@ -1673,13 +1534,6 @@ if __name__ == "__main__": help="Doxygen output directory", ) opt.add_option( - "--index", - type="string", - dest="index_path", - default=None, - help="Index row output file", - ) - opt.add_option( "--tags", type="string", dest="tags", @@ -1687,22 +1541,6 @@ if __name__ == "__main__": help="Doxygen tags file", ) opt.add_option( - "-r", - "--root-path", - type="string", - dest="root_path", - default="", - help="Root path", - ) - opt.add_option( - "-R", - "--root-uri", - type="string", - dest="root_uri", - default="", - help="Root URI", - ) - opt.add_option( "-p", "--prefix", type="string", @@ -1723,11 +1561,10 @@ if __name__ == "__main__": help="Copy style from template directory to output directory", ) opt.add_option( - "-o", - "--online", + "--validate", action="store_true", - dest="online", - help="Generate index for online documentation", + dest="validate", + help="Validate against local XML DTDs", ) (options, args) = opt.parse_args() @@ -1738,9 +1575,7 @@ if __name__ == "__main__": sys.exit(-1) spec_pre = options.prefix - ontology = "file:" + str(args[0]) output = args[1] - index_path = options.index_path docdir = options.docdir tags = options.tags @@ -1749,31 +1584,22 @@ if __name__ == "__main__": path = os.path.dirname(spec) outdir = os.path.abspath(os.path.join(out, path)) - bundle = str(outdir) b = os.path.basename(outdir) if not os.access(os.path.abspath(spec), os.R_OK): - print("warning: extension %s has no %s.ttl file" % (b, b)) + log_error("extension %s has no %s.ttl file" % (b, b)) sys.exit(1) - # Root link - root_path = opts["root_path"] - root_uri = opts["root_uri"] - root_link = os.path.join(root_path, "index.html") - # Generate spec documentation specdoc = specgen( spec, - indir, + opts["template"], opts["style_uri"], docdir, tags, opts, instances=True, - root_link=root_link, - index_path=index_path, - root_path=root_path, - root_uri=root_uri, + validate=opts["validate"], ) # Save to HTML output file @@ -1782,7 +1608,14 @@ if __name__ == "__main__": if opts["copy_style"]: import shutil - shutil.copyfile( - os.path.join(indir, "style.css"), - os.path.join(os.path.dirname(output), "style.css"), - ) + for stylesheet in ["pygments.css", "style.css"]: + style_dir = opts["style_dir"] + output_dir = os.path.dirname(output) + shutil.copyfile( + os.path.join(style_dir, stylesheet), + os.path.join(output_dir, stylesheet), + ) + + +if __name__ == "__main__": + main() diff --git a/lv2specgen/meson.build b/lv2specgen/meson.build new file mode 100644 index 0000000..55cdecd --- /dev/null +++ b/lv2specgen/meson.build @@ -0,0 +1,46 @@ +# Copyright 2022-2026 David Robillard <d@drobilla.net> +# SPDX-License-Identifier: 0BSD OR ISC + +lv2specgen_py = files('lv2specgen.py') + +lv2_list_email = 'devel@lists.lv2plug.in' +lv2_list_page = 'http://lists.lv2plug.in/listinfo.cgi/devel-lv2plug.in' + +lv2specgen_command_prefix = [ + lv2specgen_py, + '--list-email=' + lv2_list_email, + '--list-page=' + lv2_list_page, + '--style-dir=' + lv2_source_root / 'doc' / 'style', + '--template', files('template.html'), + '--validate', +] + +if is_variable('lv2_tags') + lv2specgen_command_prefix += [ + ['--tags', lv2_tags.full_path()], # TODO: Remove full_path() in meson 0.60.0 + ] +endif + +meson.override_find_program('lv2specgen.py', lv2specgen_py) + +if not get_option('tools').disabled() + install_data( + files('lv2specgen.py'), + install_dir: get_option('bindir'), + install_mode: 'rwxr-xr-x', + ) + + install_data( + files( + '../doc/style/pygments.css', + '../doc/style/style.css', + 'template.html', + ), + install_dir: get_option('datadir') / 'lv2specgen', + ) + + install_subdir( + 'DTD', + install_dir: get_option('datadir') / 'lv2specgen', + ) +endif diff --git a/lv2specgen/style.css b/lv2specgen/style.css deleted file mode 120000 index f320096..0000000 --- a/lv2specgen/style.css +++ /dev/null @@ -1 +0,0 @@ -../doc/style.css
\ No newline at end of file diff --git a/lv2specgen/template.html b/lv2specgen/template.html index ed9d6fb..dcc2759 100644 --- a/lv2specgen/template.html +++ b/lv2specgen/template.html @@ -2,12 +2,13 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd"> <html about="@URI@" xmlns="http://www.w3.org/1999/xhtml" - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:dct="http://purl.org/dc/terms/" + xmlns:dcterms="http://purl.org/dc/terms/" + xmlns:doap="http://usefulinc.com/ns/doap#" + xmlns:foaf="http://xmlns.com/foaf/0.1/" + xmlns:owl="http://www.w3.org/2002/07/owl#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" - xmlns:lv2="http://lv2plug.in/ns/lv2core#" - @XMLNS@ + xmlns:xsd="http://www.w3.org/2001/XMLSchema#" xml:lang="en"> <head> <title>@TITLE@</title> @@ -28,7 +29,6 @@ <table id="meta"> <tr><th>ID</th><td><a href="@URI@">@URI@</a></td></tr> <tr><th>Version</th><td>@VERSION@</td></tr> - <tr><th>Date</th><td>@DATE@</td></tr> @MAIL@ @AUTHORS@ </table> @@ -42,7 +42,6 @@ <ul id="contents"> <!-- <li><a href="#sec-description">Description</a></li> --> <li><a href="#sec-index">Index</a></li> - <li><a href="#sec-history">History</a></li> @CONTENT_LINKS@ </ul> </div> @@ -61,12 +60,6 @@ @REFERENCE@ </div> - <!-- HISTORY --> - <h2 id="sec-history">History</h2> - <div class="section"> - @HISTORY@ - </div> - <!-- FOOTER --> <div id="footer"> <div> @@ -77,12 +70,12 @@ </div> <div> Valid - <a about="" rel="dct:conformsTo" resource="http://www.w3.org/TR/rdfa-syntax" + <a about="" rel="dcterms:conformsTo" resource="http://www.w3.org/TR/rdfa-syntax" href="http://validator.w3.org/check?uri=referer"> XHTML+RDFa </a> and - <a about="" rel="dct:conformsTo" resource="http://www.w3.org/TR/CSS2" + <a about="" rel="dcterms:conformsTo" resource="http://www.w3.org/TR/CSS2" href="http://jigsaw.w3.org/css-validator/check/referer"> CSS </a> |