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.dtd2
-rw-r--r--lv2specgen/DTD/xhtml-ruby-1.mod242
-rwxr-xr-xlv2specgen/lv2docgen.py17
-rwxr-xr-xlv2specgen/lv2specgen.py344
-rw-r--r--lv2specgen/meson.build46
6 files changed, 482 insertions, 174 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 53890bb..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 ........................................ -->
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 9b1685a..271e434 100755
--- a/lv2specgen/lv2docgen.py
+++ b/lv2specgen/lv2docgen.py
@@ -3,6 +3,13 @@
# Copyright 2012 David Robillard <d@drobilla.net>
# 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
@@ -93,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>
@@ -111,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)
@@ -140,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 2226f43..c8112de 100755
--- a/lv2specgen/lv2specgen.py
+++ b/lv2specgen/lv2specgen.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright 2009-2022 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>
@@ -9,19 +9,40 @@
# Based on SpecGen:
# <http://forge.morfeo-project.org/wiki_en/index.php/SpecGen>
+"""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
-__date__ = "2011-10-26"
+import markdown
+import markdown.extensions
+
+__date__ = "2026-02-07"
__version__ = __date__.replace("-", ".")
__authors__ = """
Christopher Schmidt,
@@ -46,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:
@@ -82,6 +103,24 @@ doap = rdflib.Namespace("http://usefulinc.com/ns/doap#")
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])
@@ -123,25 +162,26 @@ def isLiteral(n):
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, "")
@@ -149,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]
@@ -176,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)
@@ -192,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)
@@ -250,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 = (
@@ -283,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)
@@ -317,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):
@@ -365,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
@@ -374,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)
@@ -478,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):
@@ -537,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)
@@ -597,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,
@@ -641,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
@@ -668,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)
@@ -707,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")
@@ -726,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)
@@ -792,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):
@@ -812,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>'
@@ -837,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 = ""
@@ -848,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 != "":
@@ -863,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:
@@ -895,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)
@@ -949,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)]
@@ -964,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)
@@ -1002,15 +1039,16 @@ 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>'
@@ -1018,9 +1056,8 @@ def specAuthors(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
@@ -1046,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):
@@ -1073,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 {}
@@ -1096,16 +1130,16 @@ 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
@@ -1119,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":
@@ -1130,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(
@@ -1143,6 +1177,7 @@ def specgen(
tags,
opts,
instances=False,
+ validate=False,
):
"""The meat and potatoes: Everything starts here."""
@@ -1151,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(
@@ -1220,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
@@ -1243,6 +1282,7 @@ def specgen(
proplist = docTerms(
"Property", proplist, m, classlist, proplist, instalist
)
+ instlist = ""
if instances:
instlist = docTerms(
"Instance", instalist, m, classlist, proplist, instalist
@@ -1330,22 +1370,25 @@ def specgen(
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)
@@ -1354,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":
@@ -1412,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(
@@ -1515,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)
@@ -1524,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
@@ -1534,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
@@ -1550,6 +1599,7 @@ if __name__ == "__main__":
tags,
opts,
instances=True,
+ validate=opts["validate"],
)
# Save to HTML output file
@@ -1565,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 69b029b..55cdecd 100644
--- a/lv2specgen/meson.build
+++ b/lv2specgen/meson.build
@@ -1,4 +1,4 @@
-# Copyright 2022 David Robillard <d@drobilla.net>
+# Copyright 2022-2026 David Robillard <d@drobilla.net>
# SPDX-License-Identifier: 0BSD OR ISC
lv2specgen_py = files('lv2specgen.py')
@@ -11,8 +11,8 @@ lv2specgen_command_prefix = [
'--list-email=' + lv2_list_email,
'--list-page=' + lv2_list_page,
'--style-dir=' + lv2_source_root / 'doc' / 'style',
- '--template',
- files('template.html'),
+ '--template', files('template.html'),
+ '--validate',
]
if is_variable('lv2_tags')
@@ -21,24 +21,26 @@ if is_variable('lv2_tags')
]
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