aboutsummaryrefslogtreecommitdiffstats
path: root/lv2specgen
diff options
context:
space:
mode:
Diffstat (limited to 'lv2specgen')
-rw-r--r--lv2specgen/.pylintrc.toml5
-rw-r--r--lv2specgen/DTD/xhtml-rdfa-1.dtd3
-rw-r--r--lv2specgen/DTD/xhtml-ruby-1.mod242
-rwxr-xr-xlv2specgen/lv2docgen.py34
-rwxr-xr-xlv2specgen/lv2specgen.py458
-rw-r--r--lv2specgen/meson.build49
-rw-r--r--lv2specgen/template.html9
7 files changed, 495 insertions, 305 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 bd3479e..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 ........................................ -->
@@ -444,7 +444,6 @@
validation, so here we are.
-->
-<!ATTLIST html xmlns:dcs CDATA #IMPLIED>
<!ATTLIST html xmlns:dcterms CDATA #IMPLIED>
<!ATTLIST html xmlns:doap CDATA #IMPLIED>
<!ATTLIST html xmlns:foaf 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 c5e13a7..271e434 100755
--- a/lv2specgen/lv2docgen.py
+++ b/lv2specgen/lv2docgen.py
@@ -1,20 +1,14 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-#
-# lv2docgen, a documentation generator for LV2 plugins
+
# 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
@@ -106,10 +100,10 @@ def plugin_doc(model, plugin, style_uri):
html += get_doc(model, plugin)
ports_html = ""
- for p in model.triples([plugin, lv2.port, None]):
- ports_html += port_doc(model, p[2])
+ for link in model.triples([plugin, lv2.port, None]):
+ ports_html += port_doc(model, link[2])
- if len(ports_html):
+ if ports_html:
html += (
"""
<h2 class="sec">Ports</h2>
@@ -124,8 +118,6 @@ def plugin_doc(model, plugin, style_uri):
if __name__ == "__main__":
- "LV2 plugin documentation generator"
-
if len(sys.argv) < 2:
print("Usage: %s OUTDIR FILE..." % sys.argv[0])
sys.exit(1)
@@ -153,5 +145,5 @@ if __name__ == "__main__":
raise
print("Writing <%s> documentation to %s" % (plugin, outpath))
- with open(outpath, "w") as out:
+ with open(outpath, "w", encoding="utf-8") as out:
out.write(html)
diff --git a/lv2specgen/lv2specgen.py b/lv2specgen/lv2specgen.py
index 3345b5a..c8112de 100755
--- a/lv2specgen/lv2specgen.py
+++ b/lv2specgen/lv2specgen.py
@@ -1,48 +1,48 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
#
-# 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,7 +86,6 @@ spec_pre = None
spec_bundle = None
specgendir = None
ns_list = {
- "http://ontologi.es/doap-changeset#": "dcs",
"http://purl.org/dc/terms/": "dcterms",
"http://usefulinc.com/ns/doap#": "doap",
"http://xmlns.com/foaf/0.1/": "foaf",
@@ -101,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])
@@ -134,37 +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.replace(spec_ns_str, "")
- elif uri == str(rdfs.seeAlso):
+
+ 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:
- 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, "")
@@ -172,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]
@@ -199,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)
@@ -215,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)
@@ -273,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 = (
@@ -306,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)
@@ -340,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):
@@ -388,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
@@ -397,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)
@@ -501,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):
@@ -560,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)
@@ -620,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,
@@ -664,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
@@ -691,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)
@@ -730,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")
@@ -749,18 +765,18 @@ 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):
continue
- t = termName(m, term)
+ t = termName(term)
curie = term.split(spec_ns_str[-1])[1]
if t:
doc += '<div class="specterm" id="%s" about="%s">' % (t, term)
@@ -815,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.replace(spec_ns_str, "").replace("/", "_")
- else:
- return getShortName(uri)
+
+ return getShortName(uri)
def buildIndex(m, classlist, proplist, instalist=None):
@@ -835,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>'
@@ -860,7 +876,7 @@ def buildIndex(m, classlist, proplist, instalist=None):
continue
shown[c] = True
- body += "<li>" + termLink(m, c)
+ body += "<li>" + termLink(c)
def class_tree(c):
tree = ""
@@ -871,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 != "":
@@ -886,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:
@@ -918,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)
@@ -972,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)]
@@ -987,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)
@@ -1025,98 +1039,25 @@ def specAuthors(m, subject):
maintdoc = ""
first = True
- for m in sorted(maint):
+ for name in sorted(maint):
if not first:
maintdoc += ", "
- maintdoc += (
- f'<span class="author" property="doap:maintainer">{m}</span>'
- )
+ maintdoc += '<span class="author" property="doap:maintainer">'
+ maintdoc += name
+ maintdoc += "</span>"
first = False
- if len(maint):
+
+ if maint:
label = "Maintainer" if len(maint) == 1 else "Maintainers"
doc += f'<tr><th class="metahead">{label}</th><td>{maintdoc}</td></tr>'
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
- elif int(rev.split(".")[-1]) % 2 == 0:
- print("warning: %s %s has no file-release" % (subject, rev))
-
- 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 ""
-
-
-def specHistory(m, subject):
- return specHistoryMarkup(specHistoryEntries(m, subject, {}))
-
-
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
@@ -1142,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):
@@ -1169,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 {}
@@ -1192,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")
@@ -1216,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":
@@ -1227,9 +1164,9 @@ def load_tags(path, docdir):
mname = prefix + getChildText(m, "name")
mafile = getChildText(m, "anchorfile")
manchor = getChildText(m, "anchor")
- linkmap[mname] = linkTo(mafile, manchor, mname)
+ result[mname] = linkTo(mafile, manchor, mname)
- return linkmap
+ return result
def specgen(
@@ -1240,6 +1177,7 @@ def specgen(
tags,
opts,
instances=False,
+ validate=False,
):
"""The meat and potatoes: Everything starts here."""
@@ -1248,20 +1186,24 @@ 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))
# Template
- with open(template_path, "r") as f:
+ 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(
@@ -1317,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
@@ -1340,6 +1282,7 @@ def specgen(
proplist = docTerms(
"Property", proplist, m, classlist, proplist, instalist
)
+ instlist = ""
if instances:
instlist = docTerms(
"Instance", instalist, m, classlist, proplist, instalist
@@ -1383,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:
@@ -1425,25 +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"))
+ 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\n" % (specloc, e)
- )
+ log_error("Validation failed for %s: %s" % (specloc, e))
finally:
os.chdir(oldcwd)
@@ -1452,16 +1397,16 @@ def specgen(
def save(path, text):
try:
- with open(path, "w") as f:
+ 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":
@@ -1510,36 +1455,38 @@ def _data_dirs():
)
-if __name__ == "__main__":
- """Ontology specification generator tool"""
+def main():
+ """Main program that parses the program arguments and runs."""
- data_dir = None
- for d in _data_dirs():
- path = os.path.join(d, "lv2specgen")
- if (
- os.path.exists(os.path.join(path, "template.html"))
- and os.path.exists(os.path.join(path, "style.css"))
- and os.path.exists(os.path.join(path, "pygments.css"))
- ):
- data_dir = path
- break
+ global spec_pre
+ global specgendir
- if data_dir:
- # Use installed files
- specgendir = data_dir
- default_template_path = os.path.join(data_dir, "template.html")
- default_style_dir = data_dir
+ 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:
- script_path = os.path.realpath(__file__)
- 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")
+ 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:
- sys.stderr.write("error: Unable to find lv2specgen data\n")
+ log_error("Unable to find lv2specgen data")
sys.exit(-2)
opt = optparse.OptionParser(
@@ -1613,6 +1560,12 @@ if __name__ == "__main__":
dest="copy_style",
help="Copy style from template directory to output directory",
)
+ opt.add_option(
+ "--validate",
+ action="store_true",
+ dest="validate",
+ help="Validate against local XML DTDs",
+ )
(options, args) = opt.parse_args()
opts = vars(options)
@@ -1622,7 +1575,6 @@ if __name__ == "__main__":
sys.exit(-1)
spec_pre = options.prefix
- ontology = "file:" + str(args[0])
output = args[1]
docdir = options.docdir
tags = options.tags
@@ -1632,11 +1584,10 @@ 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):
- sys.stderr.write("error: extension %s has no %s.ttl file\n" % (b, b))
+ log_error("extension %s has no %s.ttl file" % (b, b))
sys.exit(1)
# Generate spec documentation
@@ -1648,6 +1599,7 @@ if __name__ == "__main__":
tags,
opts,
instances=True,
+ validate=opts["validate"],
)
# Save to HTML output file
@@ -1663,3 +1615,7 @@ if __name__ == "__main__":
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
index 9ecb8d4..55cdecd 100644
--- a/lv2specgen/meson.build
+++ b/lv2specgen/meson.build
@@ -1,5 +1,5 @@
-# Copyright 2022 David Robillard <d@drobilla.net>
-# SPDX-License-Identifier: CC0-1.0 OR ISC
+# Copyright 2022-2026 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: 0BSD OR ISC
lv2specgen_py = files('lv2specgen.py')
@@ -10,32 +10,37 @@ 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
+ ['--tags', lv2_tags.full_path()], # TODO: Remove full_path() in meson 0.60.0
]
endif
-install_data(
- files('lv2specgen.py'),
- install_dir: get_option('bindir'),
- install_mode: 'rwxr-xr-x',
-)
-
meson.override_find_program('lv2specgen.py', lv2specgen_py)
-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',
-)
+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/template.html b/lv2specgen/template.html
index 4eb3d96..dcc2759 100644
--- a/lv2specgen/template.html
+++ b/lv2specgen/template.html
@@ -2,7 +2,6 @@
<!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:dcs="http://ontologi.es/doap-changeset#"
xmlns:dcterms="http://purl.org/dc/terms/"
xmlns:doap="http://usefulinc.com/ns/doap#"
xmlns:foaf="http://xmlns.com/foaf/0.1/"
@@ -30,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>
@@ -44,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>
@@ -63,12 +60,6 @@
@REFERENCE@
</div>
- <!-- HISTORY -->
- <h2 id="sec-history">History</h2>
- <div class="section">
- @HISTORY@
- </div>
-
<!-- FOOTER -->
<div id="footer">
<div>