1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3#
4# lv2specgen, a documentation generator for LV2 specifications.
5# Copyright (c) 2009-2014 David Robillard <d@drobilla.net>
6#
7# Based on SpecGen:
8# <http://forge.morfeo-project.org/wiki_en/index.php/SpecGen>
9# Copyright (c) 2003-2008 Christopher Schmidt <crschmidt@crschmidt.net>
10# Copyright (c) 2005-2008 Uldis Bojars <uldis.bojars@deri.org>
11# Copyright (c) 2007-2008 Sergio Fernández <sergio.fernandez@fundacionctic.org>
12#
13# This software is licensed under the terms of the MIT License.
14#
15# Permission is hereby granted, free of charge, to any person obtaining a copy
16# of this software and associated documentation files (the "Software"), to deal
17# in the Software without restriction, including without limitation the rights
18# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19# copies of the Software, and to permit persons to whom the Software is
20# furnished to do so, subject to the following conditions:
21#
22# The above copyright notice and this permission notice shall be included in
23# all copies or substantial portions of the Software.
24#
25# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
31# THE SOFTWARE.
32
33import datetime
34import markdown
35import markdown.extensions
36import optparse
37import os
38import re
39import sys
40import time
41import xml.sax.saxutils
42import xml.dom
43import xml.dom.minidom
44
45__date__ = "2011-10-26"
46__version__ = __date__.replace("-", ".")
47__authors__ = """
48Christopher Schmidt,
49Uldis Bojars,
50Sergio Fernández,
51David Robillard"""
52__license__ = "MIT License <http://www.opensource.org/licenses/mit>"
53__contact__ = "devel@lists.lv2plug.in"
54
55try:
56    from lxml import etree
57
58    have_lxml = True
59except Exception:
60    have_lxml = False
61
62try:
63    import pygments
64    import pygments.lexers
65    import pygments.lexers.rdf
66    import pygments.formatters
67
68    have_pygments = True
69except ImportError:
70    print("Error importing pygments, syntax highlighting disabled")
71    have_pygments = False
72
73try:
74    import rdflib
75except ImportError:
76    sys.exit("Error importing rdflib")
77
78# Global Variables
79classranges = {}
80classdomains = {}
81linkmap = {}
82spec_url = None
83spec_ns_str = None
84spec_ns = None
85spec_pre = None
86spec_bundle = None
87specgendir = None
88ns_list = {
89    "http://www.w3.org/1999/02/22-rdf-syntax-ns#": "rdf",
90    "http://www.w3.org/2000/01/rdf-schema#": "rdfs",
91    "http://www.w3.org/2002/07/owl#": "owl",
92    "http://www.w3.org/2001/XMLSchema#": "xsd",
93    "http://rdfs.org/sioc/ns#": "sioc",
94    "http://xmlns.com/foaf/0.1/": "foaf",
95    "http://purl.org/dc/elements/1.1/": "dc",
96    "http://purl.org/dc/terms/": "dct",
97    "http://purl.org/rss/1.0/modules/content/": "content",
98    "http://www.w3.org/2003/01/geo/wgs84_pos#": "geo",
99    "http://www.w3.org/2004/02/skos/core#": "skos",
100    "http://lv2plug.in/ns/lv2core#": "lv2",
101    "http://usefulinc.com/ns/doap#": "doap",
102    "http://ontologi.es/doap-changeset#": "dcs",
103}
104
105rdf = rdflib.Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
106rdfs = rdflib.Namespace("http://www.w3.org/2000/01/rdf-schema#")
107owl = rdflib.Namespace("http://www.w3.org/2002/07/owl#")
108lv2 = rdflib.Namespace("http://lv2plug.in/ns/lv2core#")
109doap = rdflib.Namespace("http://usefulinc.com/ns/doap#")
110dcs = rdflib.Namespace("http://ontologi.es/doap-changeset#")
111foaf = rdflib.Namespace("http://xmlns.com/foaf/0.1/")
112
113
114def findStatements(model, s, p, o):
115    return model.triples([s, p, o])
116
117
118def findOne(m, s, p, o):
119    triples = findStatements(m, s, p, o)
120    try:
121        return sorted(triples)[0]
122    except Exception:
123        return None
124
125
126def getSubject(s):
127    return s[0]
128
129
130def getPredicate(s):
131    return s[1]
132
133
134def getObject(s):
135    return s[2]
136
137
138def getLiteralString(s):
139    return s
140
141
142def isResource(n):
143    return type(n) == rdflib.URIRef
144
145
146def isBlank(n):
147    return type(n) == rdflib.BNode
148
149
150def isLiteral(n):
151    return type(n) == rdflib.Literal
152
153
154def niceName(uri):
155    global spec_bundle
156    if uri.startswith(spec_ns_str):
157        return uri[len(spec_ns_str) :]
158    elif uri == str(rdfs.seeAlso):
159        return "See also"
160
161    regexp = re.compile("^(.*[/#])([^/#]+)$")
162    rez = regexp.search(uri)
163    if not rez:
164        return uri
165    pref = rez.group(1)
166    if pref in ns_list:
167        return ns_list.get(pref, pref) + ":" + rez.group(2)
168    else:
169        print("warning: prefix %s not in ns list:" % pref)
170        print(ns_list)
171        return uri
172
173
174def termName(m, urinode):
175    "Trims the namespace out of a term to give a name to the term."
176    return str(urinode).replace(spec_ns_str, "")
177
178
179def getLabel(m, urinode):
180    statement = findOne(m, urinode, rdfs.label, None)
181    if statement:
182        return getLiteralString(getObject(statement))
183    else:
184        return ""
185
186
187def linkifyCodeIdentifiers(string):
188    "Add links to code documentation for identifiers like LV2_Type"
189
190    if linkmap == {}:
191        return string
192
193    if string in linkmap.keys():
194        # Exact match for complete string
195        return linkmap[string]
196
197    rgx = re.compile(
198        "([^a-zA-Z0-9_:])("
199        + "|".join(map(re.escape, linkmap))
200        + ")([^a-zA-Z0-9_:])"
201    )
202
203    def translateCodeLink(match):
204        return match.group(1) + linkmap[match.group(2)] + match.group(3)
205
206    return rgx.sub(translateCodeLink, string)
207
208
209def linkifyVocabIdentifiers(m, string, classlist, proplist, instalist):
210    "Add links to vocabulary documentation for prefixed names like eg:Thing"
211
212    rgx = re.compile("([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)")
213    namespaces = getNamespaces(m)
214
215    def translateLink(match):
216        text = match.group(0)
217        prefix = match.group(1)
218        name = match.group(2)
219        uri = rdflib.URIRef(spec_ns + name)
220        if prefix == spec_pre:
221            if not (
222                (classlist and uri in classlist)
223                or (instalist and uri in instalist)
224                or (proplist and uri in proplist)
225            ):
226                print("warning: Link to undefined resource <%s>\n" % text)
227            return '<a href="#%s">%s</a>' % (name, name)
228        elif prefix in namespaces:
229            return '<a href="%s">%s</a>' % (
230                namespaces[match.group(1)] + match.group(2),
231                match.group(0),
232            )
233        else:
234            return text
235
236    return rgx.sub(translateLink, string)
237
238
239def prettifyHtml(m, markup, subject, classlist, proplist, instalist):
240    # Syntax highlight all C code
241    if have_pygments:
242        code_rgx = re.compile('<pre class="c-code">(.*?)</pre>', re.DOTALL)
243        while True:
244            code = code_rgx.search(markup)
245            if not code:
246                break
247            match_str = xml.sax.saxutils.unescape(code.group(1))
248            code_str = pygments.highlight(
249                match_str,
250                pygments.lexers.CLexer(),
251                pygments.formatters.HtmlFormatter(),
252            )
253            markup = code_rgx.sub(code_str, markup, 1)
254
255    # Syntax highlight all Turtle code
256    if have_pygments:
257        code_rgx = re.compile(
258            '<pre class="turtle-code">(.*?)</pre>', re.DOTALL
259        )
260        while True:
261            code = code_rgx.search(markup)
262            if not code:
263                break
264            match_str = xml.sax.saxutils.unescape(code.group(1))
265            code_str = pygments.highlight(
266                match_str,
267                pygments.lexers.rdf.TurtleLexer(),
268                pygments.formatters.HtmlFormatter(),
269            )
270            markup = code_rgx.sub(code_str, markup, 1)
271
272    # Add links to code documentation for identifiers
273    markup = linkifyCodeIdentifiers(markup)
274
275    # Add internal links for known prefixed names
276    markup = linkifyVocabIdentifiers(m, markup, classlist, proplist, instalist)
277
278    # Transform names like #foo into links into this spec if possible
279    rgx = re.compile("([ \t\n\r\f\v^]+)#([a-zA-Z0-9_-]+)")
280
281    def translateLocalLink(match):
282        text = match.group(0)
283        space = match.group(1)
284        name = match.group(2)
285        uri = rdflib.URIRef(spec_ns + name)
286        if (
287            (classlist and uri in classlist)
288            or (instalist and uri in instalist)
289            or (proplist and uri in proplist)
290        ):
291            return '%s<a href="#%s">%s</a>' % (space, name, name)
292        else:
293            print("warning: Link to undefined resource <%s>\n" % name)
294            return text
295
296    markup = rgx.sub(translateLocalLink, markup)
297
298    if not have_lxml:
299        print("warning: No Python lxml module found, output may be invalid")
300    else:
301        try:
302            # Parse and validate documentation as XHTML Basic 1.1
303            doc = (
304                """<?xml version="1.0" encoding="UTF-8"?>
305<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN"
306                      "DTD/xhtml-basic11.dtd">
307<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
308  <head xml:lang="en" profile="profile">
309    <title>Validation Skeleton Document</title>
310  </head>
311  <body>
312"""
313                + markup
314                + """
315  </body>
316</html>"""
317            )
318
319            oldcwd = os.getcwd()
320            os.chdir(specgendir)
321            parser = etree.XMLParser(dtd_validation=True, no_network=True)
322            etree.fromstring(doc.encode("utf-8"), parser)
323        except Exception as e:
324            print("Invalid documentation for %s\n%s" % (subject, e))
325            line_num = 1
326            for line in doc.split("\n"):
327                print("%3d: %s" % (line_num, line))
328                line_num += 1
329        finally:
330            os.chdir(oldcwd)
331
332    return markup
333
334
335def formatDoc(m, urinode, literal, classlist, proplist, instalist):
336    string = getLiteralString(literal)
337
338    if literal.datatype == lv2.Markdown:
339        ext = [
340            "markdown.extensions.codehilite",
341            "markdown.extensions.tables",
342            "markdown.extensions.def_list",
343        ]
344
345        doc = markdown.markdown(string, extensions=ext)
346
347        # Hack to make tables valid XHTML Basic 1.1
348        for tag in ["thead", "tbody"]:
349            doc = doc.replace("<%s>\n" % tag, "")
350            doc = doc.replace("</%s>\n" % tag, "")
351
352        return prettifyHtml(m, doc, urinode, classlist, proplist, instalist)
353    else:
354        doc = xml.sax.saxutils.escape(string)
355        doc = linkifyCodeIdentifiers(doc)
356        doc = linkifyVocabIdentifiers(m, doc, classlist, proplist, instalist)
357        return "<p>%s</p>" % doc
358
359
360def getComment(m, subject, classlist, proplist, instalist):
361    c = findOne(m, subject, rdfs.comment, None)
362    if c:
363        comment = getObject(c)
364        return formatDoc(m, subject, comment, classlist, proplist, instalist)
365
366    return ""
367
368
369def getDetailedDocumentation(m, subject, classlist, proplist, instalist):
370    markup = ""
371
372    d = findOne(m, subject, lv2.documentation, None)
373    if d:
374        doc = getObject(d)
375        if doc.datatype == lv2.Markdown:
376            markup += formatDoc(
377                m, subject, doc, classlist, proplist, instalist
378            )
379        else:
380            html = getLiteralString(doc)
381            markup += prettifyHtml(
382                m, html, subject, classlist, proplist, instalist
383            )
384
385    return markup
386
387
388def getFullDocumentation(m, subject, classlist, proplist, instalist):
389    # Use rdfs:comment for first summary line
390    markup = getComment(m, subject, classlist, proplist, instalist)
391
392    # Use lv2:documentation for further details
393    markup += getDetailedDocumentation(
394        m, subject, classlist, proplist, instalist
395    )
396
397    return markup
398
399
400def getProperty(val, first=True):
401    "Return a string representing a property value in a property table"
402    doc = ""
403    if not first:
404        doc += "<tr><th></th>"  # Empty cell in header column
405    doc += "<td>%s</td></tr>\n" % val
406    return doc
407
408
409def endProperties(first):
410    if first:
411        return "</tr>"
412    else:
413        return ""
414
415
416def rdfsPropertyInfo(term, m):
417    """Generate HTML for properties: Domain, range"""
418    global classranges
419    global classdomains
420    doc = ""
421
422    label = getLabel(m, term)
423    if label != "":
424        doc += "<tr><th>Label</th><td>%s</td></tr>" % label
425
426    # Find subPropertyOf information
427    rlist = ""
428    first = True
429    for st in findStatements(m, term, rdfs.subPropertyOf, None):
430        k = getTermLink(getObject(st), term, rdfs.subPropertyOf)
431        rlist += getProperty(k, first)
432        first = False
433    if rlist != "":
434        doc += "<tr><th>Sub-property of</th>" + rlist
435
436    # Domain stuff
437    domains = findStatements(m, term, rdfs.domain, None)
438    domainsdoc = ""
439    first = True
440    for d in sorted(domains):
441        union = findOne(m, getObject(d), owl.unionOf, None)
442        if union:
443            uris = parseCollection(m, getObject(union))
444            for uri in uris:
445                domainsdoc += getProperty(
446                    getTermLink(uri, term, rdfs.domain), first
447                )
448                add(classdomains, uri, term)
449        else:
450            if not isBlank(getObject(d)):
451                domainsdoc += getProperty(
452                    getTermLink(getObject(d), term, rdfs.domain), first
453                )
454        first = False
455    if len(domainsdoc) > 0:
456        doc += "<tr><th>Domain</th>%s" % domainsdoc
457
458    # Range stuff
459    ranges = findStatements(m, term, rdfs.range, None)
460    rangesdoc = ""
461    first = True
462    for r in sorted(ranges):
463        union = findOne(m, getObject(r), owl.unionOf, None)
464        if union:
465            uris = parseCollection(m, getObject(union))
466            for uri in uris:
467                rangesdoc += getProperty(
468                    getTermLink(uri, term, rdfs.range), first
469                )
470                add(classranges, uri, term)
471                first = False
472        else:
473            if not isBlank(getObject(r)):
474                rangesdoc += getProperty(
475                    getTermLink(getObject(r), term, rdfs.range), first
476                )
477        first = False
478    if len(rangesdoc) > 0:
479        doc += "<tr><th>Range</th>%s" % rangesdoc
480
481    return doc
482
483
484def parseCollection(model, node):
485    uris = []
486
487    while node:
488        first = findOne(model, node, rdf.first, None)
489        rest = findOne(model, node, rdf.rest, None)
490        if not first or not rest:
491            break
492
493        uris.append(getObject(first))
494        node = getObject(rest)
495
496    return uris
497
498
499def getTermLink(uri, subject=None, predicate=None):
500    uri = str(uri)
501    extra = ""
502    if subject is not None and predicate is not None:
503        extra = 'about="%s" rel="%s" resource="%s"' % (
504            str(subject),
505            niceName(str(predicate)),
506            uri,
507        )
508    if uri.startswith(spec_ns_str):
509        return '<a href="#%s" %s>%s</a>' % (
510            uri.replace(spec_ns_str, ""),
511            extra,
512            niceName(uri),
513        )
514    else:
515        return '<a href="%s" %s>%s</a>' % (uri, extra, niceName(uri))
516
517
518def owlRestrictionInfo(term, m):
519    """Generate OWL restriction information for Classes"""
520    restrictions = []
521    for s in findStatements(m, term, rdfs.subClassOf, None):
522        if findOne(m, getObject(s), rdf.type, owl.Restriction):
523            restrictions.append(getObject(s))
524
525    if not restrictions:
526        return ""
527
528    doc = "<dl>"
529
530    for r in sorted(restrictions):
531        props = findStatements(m, r, None, None)
532        onProp = None
533        comment = None
534        for p in props:
535            if getPredicate(p) == owl.onProperty:
536                onProp = getObject(p)
537            elif getPredicate(p) == rdfs.comment:
538                comment = getObject(p)
539        if onProp is not None:
540            doc += "<dt>Restriction on %s</dt>\n" % getTermLink(onProp)
541
542            prop_str = ""
543            for p in findStatements(m, r, None, None):
544                if (
545                    getPredicate(p) == owl.onProperty
546                    or getPredicate(p) == rdfs.comment
547                    or (
548                        getPredicate(p) == rdf.type
549                        and getObject(p) == owl.Restriction
550                    )
551                    or getPredicate(p) == lv2.documentation
552                ):
553                    continue
554
555                prop_str += getTermLink(getPredicate(p))
556
557                if isResource(getObject(p)):
558                    prop_str += " " + getTermLink(getObject(p))
559                elif isLiteral(getObject(p)):
560                    prop_str += " " + getLiteralString(getObject(p))
561
562            if comment is not None:
563                prop_str += "\n<div>%s</div>\n" % getLiteralString(comment)
564
565            doc += "<dd>%s</dd>" % prop_str if prop_str else ""
566
567    doc += "</dl>"
568    return doc
569
570
571def rdfsClassInfo(term, m):
572    """Generate rdfs-type information for Classes: ranges, and domains."""
573    global classranges
574    global classdomains
575    doc = ""
576
577    label = getLabel(m, term)
578    if label != "":
579        doc += "<tr><th>Label</th><td>%s</td></tr>" % label
580
581    # Find superclasses
582    superclasses = set()
583    for st in findStatements(m, term, rdfs.subClassOf, None):
584        if not isBlank(getObject(st)):
585            uri = getObject(st)
586            superclasses |= set([uri])
587
588    if len(superclasses) > 0:
589        doc += "\n<tr><th>Subclass of</th>"
590        first = True
591        for superclass in sorted(superclasses):
592            doc += getProperty(getTermLink(superclass), first)
593            first = False
594
595    # Find subclasses
596    subclasses = set()
597    for st in findStatements(m, None, rdfs.subClassOf, term):
598        if not isBlank(getObject(st)):
599            uri = getSubject(st)
600            subclasses |= set([uri])
601
602    if len(subclasses) > 0:
603        doc += "\n<tr><th>Superclass of</th>"
604        first = True
605        for superclass in sorted(subclasses):
606            doc += getProperty(getTermLink(superclass), first)
607            first = False
608
609    # Find out about properties which have rdfs:domain of t
610    d = classdomains.get(str(term), "")
611    if d:
612        dlist = ""
613        first = True
614        for k in sorted(d):
615            dlist += getProperty(getTermLink(k), first)
616            first = False
617        doc += "<tr><th>In domain of</th>%s" % dlist
618
619    # Find out about properties which have rdfs:range of t
620    r = classranges.get(str(term), "")
621    if r:
622        rlist = ""
623        first = True
624        for k in sorted(r):
625            rlist += getProperty(getTermLink(k), first)
626            first = False
627        doc += "<tr><th>In range of</th>%s" % rlist
628
629    return doc
630
631
632def isSpecial(pred):
633    """Return True if `pred` shouldn't be documented generically"""
634    return pred in [
635        rdf.type,
636        rdfs.range,
637        rdfs.domain,
638        rdfs.label,
639        rdfs.comment,
640        rdfs.subClassOf,
641        rdfs.subPropertyOf,
642        lv2.documentation,
643        owl.withRestrictions,
644    ]
645
646
647def blankNodeDesc(node, m):
648    properties = findStatements(m, node, None, None)
649    doc = ""
650    for p in sorted(properties):
651        if isSpecial(getPredicate(p)):
652            continue
653        doc += "<tr>"
654        doc += '<td class="blankterm">%s</td>\n' % getTermLink(getPredicate(p))
655        if isResource(getObject(p)):
656            doc += '<td class="blankdef">%s</td>\n' % getTermLink(getObject(p))
657            # getTermLink(str(getObject(p)), node, getPredicate(p))
658        elif isLiteral(getObject(p)):
659            doc += '<td class="blankdef">%s</td>\n' % getLiteralString(
660                getObject(p)
661            )
662        elif isBlank(getObject(p)):
663            doc += (
664                '<td class="blankdef">'
665                + blankNodeDesc(getObject(p), m)
666                + "</td>\n"
667            )
668        else:
669            doc += '<td class="blankdef">?</td>\n'
670        doc += "</tr>"
671    if doc != "":
672        doc = '<table class="blankdesc">\n%s\n</table>\n' % doc
673    return doc
674
675
676def extraInfo(term, m):
677    """Generate information about misc. properties of a term"""
678    doc = ""
679    properties = findStatements(m, term, None, None)
680    first = True
681    for p in sorted(properties):
682        if isSpecial(getPredicate(p)):
683            continue
684        doc += "<tr><th>%s</th>\n" % getTermLink(getPredicate(p))
685        if isResource(getObject(p)):
686            doc += getProperty(
687                getTermLink(getObject(p), term, getPredicate(p)), first
688            )
689        elif isLiteral(getObject(p)):
690            doc += getProperty(
691                linkifyCodeIdentifiers(str(getObject(p))), first
692            )
693        elif isBlank(getObject(p)):
694            doc += getProperty(str(blankNodeDesc(getObject(p), m)), first)
695        else:
696            doc += getProperty("?", first)
697
698    # doc += endProperties(first)
699
700    return doc
701
702
703def rdfsInstanceInfo(term, m):
704    """Generate rdfs-type information for instances"""
705    doc = ""
706
707    label = getLabel(m, term)
708    if label != "":
709        doc += "<tr><th>Label</th><td>%s</td></tr>" % label
710
711    first = True
712    types = ""
713    for match in sorted(findStatements(m, term, rdf.type, None)):
714        types += getProperty(
715            getTermLink(getObject(match), term, rdf.type), first
716        )
717        first = False
718
719    if types != "":
720        doc += "<tr><th>Type</th>" + types
721
722    doc += endProperties(first)
723
724    return doc
725
726
727def owlInfo(term, m):
728    """Returns an extra information that is defined about a term using OWL."""
729    res = ""
730
731    # Inverse properties ( owl:inverseOf )
732    first = True
733    for st in findStatements(m, term, owl.inverseOf, None):
734        res += getProperty(getTermLink(getObject(st)), first)
735        first = False
736    if res != "":
737        res += endProperties(first)
738        res = "<tr><th>Inverse:</th>\n" + res
739
740    def owlTypeInfo(term, propertyType, name):
741        if findOne(m, term, rdf.type, propertyType):
742            return "<tr><th>Type</th><td>%s</td></tr>\n" % name
743        else:
744            return ""
745
746    res += owlTypeInfo(term, owl.DatatypeProperty, "Datatype Property")
747    res += owlTypeInfo(term, owl.ObjectProperty, "Object Property")
748    res += owlTypeInfo(term, owl.AnnotationProperty, "Annotation Property")
749    res += owlTypeInfo(
750        term, owl.InverseFunctionalProperty, "Inverse Functional Property"
751    )
752    res += owlTypeInfo(term, owl.SymmetricProperty, "Symmetric Property")
753
754    return res
755
756
757def isDeprecated(m, subject):
758    deprecated = findOne(m, subject, owl.deprecated, None)
759    return deprecated and (str(deprecated[2]).find("true") >= 0)
760
761
762def docTerms(category, list, m, classlist, proplist, instalist):
763    """
764    A wrapper class for listing all the terms in a specific class (either
765    Properties, or Classes. Category is 'Property' or 'Class', list is a
766    list of term URI strings, return value is a chunk of HTML.
767    """
768    doc = ""
769    for term in list:
770        if not term.startswith(spec_ns_str):
771            sys.stderr.write("warning: Skipping external term `%s'" % term)
772            continue
773
774        t = termName(m, term)
775        curie = term.split(spec_ns_str[-1])[1]
776        doc += '<div class="specterm" id="%s" about="%s">' % (t, term)
777        doc += '<h3><a href="#%s">%s</a></h3>' % (getAnchor(term), curie)
778        doc += '<span class="spectermtype">%s</span>' % category
779
780        comment = getFullDocumentation(m, term, classlist, proplist, instalist)
781        is_deprecated = isDeprecated(m, term)
782
783        doc += '<div class="spectermbody">'
784
785        terminfo = ""
786        extrainfo = ""
787        if category == "Property":
788            terminfo += rdfsPropertyInfo(term, m)
789            terminfo += owlInfo(term, m)
790        if category == "Class":
791            terminfo += rdfsClassInfo(term, m)
792            extrainfo += owlRestrictionInfo(term, m)
793        if category == "Instance":
794            terminfo += rdfsInstanceInfo(term, m)
795
796        terminfo += extraInfo(term, m)
797
798        if len(terminfo) > 0:  # to prevent empty list (bug #882)
799            doc += '\n<table class="terminfo">%s</table>\n' % terminfo
800
801        doc += '<div class="description">'
802
803        if is_deprecated:
804            doc += '<div class="warning">Deprecated</div>'
805
806        if comment != "":
807            doc += (
808                '<div class="comment" property="rdfs:comment">%s</div>'
809                % comment
810            )
811
812        doc += extrainfo
813
814        doc += "</div>"
815
816        doc += "</div>"
817        doc += "\n</div>\n\n"
818
819    return doc
820
821
822def getShortName(uri):
823    uri = str(uri)
824    if "#" in uri:
825        return uri.split("#")[-1]
826    else:
827        return uri.split("/")[-1]
828
829
830def getAnchor(uri):
831    uri = str(uri)
832    if uri.startswith(spec_ns_str):
833        return uri[len(spec_ns_str) :].replace("/", "_")
834    else:
835        return getShortName(uri)
836
837
838def buildIndex(m, classlist, proplist, instalist=None):
839    if not (classlist or proplist or instalist):
840        return ""
841
842    head = ""
843    body = ""
844
845    def termLink(m, t):
846        if str(t).startswith(spec_ns_str):
847            name = termName(m, t)
848            return '<a href="#%s">%s</a>' % (name, name)
849        else:
850            return '<a href="%s">%s</a>' % (str(t), str(t))
851
852    if len(classlist) > 0:
853        head += '<th><a href="#ref-classes" />Classes</th>'
854        body += "<td><ul>"
855        shown = {}
856        for c in sorted(classlist):
857            if c in shown:
858                continue
859
860            # Skip classes that are subclasses of classes defined in this spec
861            local_subclass = False
862            for p in findStatements(m, c, rdfs.subClassOf, None):
863                parent = str(p[2])
864                if parent[0 : len(spec_ns_str)] == spec_ns_str:
865                    local_subclass = True
866            if local_subclass:
867                continue
868
869            shown[c] = True
870            body += "<li>" + termLink(m, c)
871
872            def class_tree(c):
873                tree = ""
874                shown[c] = True
875
876                subclasses = []
877                for s in findStatements(m, None, rdfs.subClassOf, c):
878                    subclasses += [getSubject(s)]
879
880                for s in sorted(subclasses):
881                    tree += "<li>" + termLink(m, s)
882                    tree += class_tree(s)
883                    tree += "</li>"
884                if tree != "":
885                    tree = "<ul>" + tree + "</ul>"
886                return tree
887
888            body += class_tree(c)
889            body += "</li>"
890        body += "</ul></td>\n"
891
892    if len(proplist) > 0:
893        head += '<th><a href="#ref-properties" />Properties</th>'
894        body += "<td><ul>"
895        for p in sorted(proplist):
896            body += "<li>%s</li>" % termLink(m, p)
897        body += "</ul></td>\n"
898
899    if instalist is not None and len(instalist) > 0:
900        head += '<th><a href="#ref-instances" />Instances</th>'
901        body += "<td><ul>"
902        for i in sorted(instalist):
903            p = getShortName(i)
904            anchor = getAnchor(i)
905            body += '<li><a href="#%s">%s</a></li>' % (anchor, p)
906        body += "</ul></td>\n"
907
908    if head and body:
909        return """<table class="index">
910<thead><tr>%s</tr></thead>
911<tbody><tr>%s</tr></tbody></table>
912""" % (
913            head,
914            body,
915        )
916
917    return ""
918
919
920def add(where, key, value):
921    if key not in where:
922        where[key] = []
923    if value not in where[key]:
924        where[key].append(value)
925
926
927def specInformation(m, ns):
928    """
929    Read through the spec (provided as a Redland model) and return classlist
930    and proplist. Global variables classranges and classdomains are also filled
931    as appropriate.
932    """
933    global classranges
934    global classdomains
935
936    # Find the class information: Ranges, domains, and list of all names.
937    classtypes = [rdfs.Class, owl.Class, rdfs.Datatype]
938    classlist = []
939    for onetype in classtypes:
940        for classStatement in findStatements(m, None, rdf.type, onetype):
941            for range in findStatements(
942                m, None, rdfs.range, getSubject(classStatement)
943            ):
944                if not isBlank(getSubject(classStatement)):
945                    add(
946                        classranges,
947                        str(getSubject(classStatement)),
948                        str(getSubject(range)),
949                    )
950            for domain in findStatements(
951                m, None, rdfs.domain, getSubject(classStatement)
952            ):
953                if not isBlank(getSubject(classStatement)):
954                    add(
955                        classdomains,
956                        str(getSubject(classStatement)),
957                        str(getSubject(domain)),
958                    )
959            if not isBlank(getSubject(classStatement)):
960                klass = getSubject(classStatement)
961                if klass not in classlist and str(klass).startswith(ns):
962                    classlist.append(klass)
963
964    # Create a list of properties in the schema.
965    proptypes = [
966        rdf.Property,
967        owl.ObjectProperty,
968        owl.DatatypeProperty,
969        owl.AnnotationProperty,
970    ]
971    proplist = []
972    for onetype in proptypes:
973        for propertyStatement in findStatements(m, None, rdf.type, onetype):
974            prop = getSubject(propertyStatement)
975            if prop not in proplist and str(prop).startswith(ns):
976                proplist.append(prop)
977
978    return classlist, proplist
979
980
981def specProperty(m, subject, predicate):
982    "Return a property of the spec."
983    for c in findStatements(m, subject, predicate, None):
984        return getLiteralString(getObject(c))
985    return ""
986
987
988def specProperties(m, subject, predicate):
989    "Return a property of the spec."
990    properties = []
991    for c in findStatements(m, subject, predicate, None):
992        properties += [getObject(c)]
993    return properties
994
995
996def specAuthors(m, subject):
997    "Return an HTML description of the authors of the spec."
998
999    subjects = [subject]
1000    p = findOne(m, subject, lv2.project, None)
1001    if p:
1002        subjects += [getObject(p)]
1003
1004    dev = set()
1005    for s in subjects:
1006        for i in findStatements(m, s, doap.developer, None):
1007            for j in findStatements(m, getObject(i), foaf.name, None):
1008                dev.add(getLiteralString(getObject(j)))
1009
1010    maint = set()
1011    for s in subjects:
1012        for i in findStatements(m, s, doap.maintainer, None):
1013            for j in findStatements(m, getObject(i), foaf.name, None):
1014                maint.add(getLiteralString(getObject(j)))
1015
1016    doc = ""
1017
1018    devdoc = ""
1019    first = True
1020    for d in sorted(dev):
1021        if not first:
1022            devdoc += ", "
1023        devdoc += (
1024            '<span class="author" property="doap:developer">%s</span>' % d
1025        )
1026        first = False
1027    if len(dev) == 1:
1028        doc += (
1029            '<tr><th class="metahead">Developer</th><td>%s</td></tr>' % devdoc
1030        )
1031    elif len(dev) > 0:
1032        doc += (
1033            '<tr><th class="metahead">Developers</th><td>%s</td></tr>' % devdoc
1034        )
1035
1036    maintdoc = ""
1037    first = True
1038    for m in sorted(maint):
1039        if not first:
1040            maintdoc += ", "
1041        maintdoc += (
1042            '<span class="author" property="doap:maintainer">%s</span>' % m
1043        )
1044        first = False
1045    if len(maint) == 1:
1046        doc += (
1047            '<tr><th class="metahead">Maintainer</th><td>%s</td></tr>'
1048            % maintdoc
1049        )
1050    elif len(maint) > 0:
1051        doc += (
1052            '<tr><th class="metahead">Maintainers</th><td>%s</td></tr>'
1053            % maintdoc
1054        )
1055
1056    return doc
1057
1058
1059def releaseChangeset(m, release, prefix=""):
1060    changeset = findOne(m, release, dcs.changeset, None)
1061    if changeset is None:
1062        return ""
1063
1064    entry = ""
1065    # entry = '<dd><ul>\n'
1066    for i in sorted(findStatements(m, getObject(changeset), dcs.item, None)):
1067        item = getObject(i)
1068        label = findOne(m, item, rdfs.label, None)
1069        if not label:
1070            print("error: dcs:item has no rdfs:label")
1071            continue
1072
1073        text = getLiteralString(getObject(label))
1074        if prefix:
1075            text = prefix + ": " + text
1076
1077        entry += "<li>%s</li>\n" % text
1078
1079    # entry += '</ul></dd>\n'
1080    return entry
1081
1082
1083def specHistoryEntries(m, subject, entries):
1084    for r in findStatements(m, subject, doap.release, None):
1085        release = getObject(r)
1086        revNode = findOne(m, release, doap.revision, None)
1087        if not revNode:
1088            print("error: doap:release has no doap:revision")
1089            continue
1090
1091        rev = getLiteralString(getObject(revNode))
1092
1093        created = findOne(m, release, doap.created, None)
1094
1095        dist = findOne(m, release, doap["file-release"], None)
1096        if dist:
1097            entry = '<dt><a href="%s">Version %s</a>' % (getObject(dist), rev)
1098        else:
1099            entry = "<dt>Version %s" % rev
1100            # print("warning: doap:release has no doap:file-release")
1101
1102        if created:
1103            entry += " (%s)</dt>\n" % getLiteralString(getObject(created))
1104        else:
1105            entry += ' (<span class="warning">EXPERIMENTAL</span>)</dt>'
1106
1107        entry += "<dd><ul>\n%s" % releaseChangeset(m, release)
1108
1109        if dist is not None:
1110            entries[(getObject(created), getObject(dist))] = entry
1111
1112    return entries
1113
1114
1115def specHistoryMarkup(entries):
1116    if len(entries) > 0:
1117        history = "<dl>\n"
1118        for e in sorted(entries.keys(), reverse=True):
1119            history += entries[e] + "</ul></dd>"
1120        history += "</dl>\n"
1121        return history
1122    else:
1123        return ""
1124
1125
1126def specHistory(m, subject):
1127    return specHistoryMarkup(specHistoryEntries(m, subject, {}))
1128
1129
1130def specVersion(m, subject):
1131    """
1132    Return a (minorVersion, microVersion, date) tuple
1133    """
1134    # Get the date from the latest doap release
1135    latest_doap_revision = ""
1136    latest_doap_release = None
1137    for i in findStatements(m, subject, doap.release, None):
1138        for j in findStatements(m, getObject(i), doap.revision, None):
1139            revision = getLiteralString(getObject(j))
1140            if latest_doap_revision == "" or revision > latest_doap_revision:
1141                latest_doap_revision = revision
1142                latest_doap_release = getObject(i)
1143    date = ""
1144    if latest_doap_release is not None:
1145        for i in findStatements(m, latest_doap_release, doap.created, None):
1146            date = getLiteralString(getObject(i))
1147
1148    # Get the LV2 version
1149    minor_version = 0
1150    micro_version = 0
1151    for i in findStatements(m, None, lv2.minorVersion, None):
1152        minor_version = int(getLiteralString(getObject(i)))
1153    for i in findStatements(m, None, lv2.microVersion, None):
1154        micro_version = int(getLiteralString(getObject(i)))
1155    return (minor_version, micro_version, date)
1156
1157
1158def getInstances(model, classes, properties):
1159    """
1160    Extract all resources instanced in the ontology
1161    (aka "everything that is not a class or a property")
1162    """
1163    instances = []
1164    for c in classes:
1165        for i in findStatements(model, None, rdf.type, c):
1166            if not isResource(getSubject(i)):
1167                continue
1168            inst = getSubject(i)
1169            if inst not in instances and str(inst) != spec_url:
1170                instances.append(inst)
1171    for i in findStatements(model, None, rdf.type, None):
1172        if (
1173            (not isResource(getSubject(i)))
1174            or (getSubject(i) in classes)
1175            or (getSubject(i) in instances)
1176            or (getSubject(i) in properties)
1177        ):
1178            continue
1179        full_uri = str(getSubject(i))
1180        if full_uri.startswith(spec_ns_str):
1181            instances.append(getSubject(i))
1182    return instances
1183
1184
1185def load_tags(path, docdir):
1186    "Build a (symbol => URI) map from a Doxygen tag file."
1187
1188    if not path or not docdir:
1189        return {}
1190
1191    def getChildText(elt, tagname):
1192        "Return the content of the first child node with a certain tag name."
1193        for e in elt.childNodes:
1194            if (
1195                e.nodeType == xml.dom.Node.ELEMENT_NODE
1196                and e.tagName == tagname
1197            ):
1198                return e.firstChild.nodeValue
1199        return ""
1200
1201    def linkTo(filename, anchor, sym):
1202        if anchor:
1203            return '<span><a href="%s/%s#%s">%s</a></span>' % (
1204                docdir,
1205                filename,
1206                anchor,
1207                sym,
1208            )
1209        else:
1210            return '<span><a href="%s/%s">%s</a></span>' % (
1211                docdir,
1212                filename,
1213                sym,
1214            )
1215
1216    tagdoc = xml.dom.minidom.parse(path)
1217    root = tagdoc.documentElement
1218    linkmap = {}
1219    for cn in root.childNodes:
1220        if (
1221            cn.nodeType == xml.dom.Node.ELEMENT_NODE
1222            and cn.tagName == "compound"
1223            and cn.getAttribute("kind") != "page"
1224        ):
1225
1226            name = getChildText(cn, "name")
1227            filename = getChildText(cn, "filename")
1228            anchor = getChildText(cn, "anchor")
1229            if not filename.endswith(".html"):
1230                filename += ".html"
1231
1232            if cn.getAttribute("kind") != "group":
1233                linkmap[name] = linkTo(filename, anchor, name)
1234
1235            prefix = ""
1236            if cn.getAttribute("kind") == "struct":
1237                prefix = name + "::"
1238
1239            members = cn.getElementsByTagName("member")
1240            for m in members:
1241                mname = prefix + getChildText(m, "name")
1242                mafile = getChildText(m, "anchorfile")
1243                manchor = getChildText(m, "anchor")
1244                linkmap[mname] = linkTo(mafile, manchor, mname)
1245
1246    return linkmap
1247
1248
1249def writeIndex(model, specloc, index_path, root_path, root_uri, online):
1250    # Get extension URI
1251    ext_node = model.value(None, rdf.type, lv2.Specification)
1252    if not ext_node:
1253        ext_node = model.value(None, rdf.type, owl.Ontology)
1254    if not ext_node:
1255        print("no extension found in %s" % bundle)
1256        sys.exit(1)
1257
1258    ext = str(ext_node)
1259
1260    # Get version
1261    minor = 0
1262    micro = 0
1263    try:
1264        minor = int(model.value(ext_node, lv2.minorVersion, None))
1265        micro = int(model.value(ext_node, lv2.microVersion, None))
1266    except Exception:
1267        print("warning: %s: failed to find version for %s" % (bundle, ext))
1268
1269    # Get date
1270    date = None
1271    for r in model.triples([ext_node, doap.release, None]):
1272        revision = model.value(r[2], doap.revision, None)
1273        if str(revision) == ("%d.%d" % (minor, micro)):
1274            date = model.value(r[2], doap.created, None)
1275            break
1276
1277    # Verify that this date is the latest
1278    if date is None:
1279        print("warning: %s has no doap:created date" % ext_node)
1280    else:
1281        for r in model.triples([ext_node, doap.release, None]):
1282            this_date = model.value(r[2], doap.created, None)
1283            if this_date is None:
1284                print(
1285                    "warning: %s has no doap:created date"
1286                    % (ext_node, minor, micro, date)
1287                )
1288                continue
1289
1290            if this_date > date:
1291                print(
1292                    "warning: %s revision %d.%d (%s) is not the latest release"
1293                    % (ext_node, minor, micro, date)
1294                )
1295                break
1296
1297    # Get name and short description
1298    name = model.value(ext_node, doap.name, None)
1299    shortdesc = model.value(ext_node, doap.shortdesc, None)
1300
1301    # Chop 'LV2' prefix from name for cleaner index
1302    if name.startswith("LV2 "):
1303        name = name[4:]
1304
1305    # Find relative link target
1306    if root_uri and ext_node.startswith(root_uri):
1307        target = ext_node[len(root_uri) :]
1308    else:
1309        target = os.path.relpath(ext_node, root_path)
1310
1311    if not online:
1312        target += ".html"
1313
1314    stem = os.path.splitext(os.path.basename(target))[0]
1315
1316    # Specification (comment is to act as a sort key)
1317    row = '<tr><!-- %s --><td><a rel="rdfs:seeAlso" href="%s">%s</a></td>' % (
1318        b,
1319        target,
1320        name,
1321    )
1322
1323    # API
1324    row += "<td>"
1325    row += '<a rel="rdfs:seeAlso" href="../doc/html/group__%s.html">%s</a>' % (
1326        stem,
1327        name,
1328    )
1329    row += "</td>"
1330
1331    # Description
1332    if shortdesc:
1333        row += "<td>" + str(shortdesc) + "</td>"
1334    else:
1335        row += "<td></td>"
1336
1337    # Version
1338    version_str = "%s.%s" % (minor, micro)
1339    if minor == 0 or (micro % 2 != 0):
1340        row += '<td><span style="color: red">' + version_str + "</span></td>"
1341    else:
1342        row += "<td>" + version_str + "</td>"
1343
1344    # Status
1345    deprecated = model.value(ext_node, owl.deprecated, None)
1346    if minor == 0:
1347        row += '<td><span class="error">Experimental</span></td>'
1348    elif deprecated and str(deprecated[2]) != "false":
1349        row += '<td><span class="warning">Deprecated</span></td>'
1350    elif micro % 2 == 0:
1351        row += '<td><span class="success">Stable</span></td>'
1352
1353    row += "</tr>"
1354
1355    index = open(index_path, "w")
1356    index.write(row)
1357    index.close()
1358
1359
1360def specgen(
1361    specloc,
1362    indir,
1363    style_uri,
1364    docdir,
1365    tags,
1366    opts,
1367    instances=False,
1368    root_link=None,
1369    index_path=None,
1370    root_path=None,
1371    root_uri=None,
1372):
1373    """The meat and potatoes: Everything starts here."""
1374
1375    global spec_bundle
1376    global spec_url
1377    global spec_ns_str
1378    global spec_ns
1379    global spec_pre
1380    global ns_list
1381    global specgendir
1382    global linkmap
1383
1384    spec_bundle = "file://%s/" % os.path.abspath(os.path.dirname(specloc))
1385    specgendir = os.path.abspath(indir)
1386
1387    # Template
1388    temploc = os.path.join(indir, "template.html")
1389    template = None
1390    f = open(temploc, "r")
1391    template = f.read()
1392    f.close()
1393
1394    # Load code documentation link map from tags file
1395    linkmap = load_tags(tags, docdir)
1396
1397    m = rdflib.ConjunctiveGraph()
1398    manifest_path = os.path.join(os.path.dirname(specloc), "manifest.ttl")
1399    if os.path.exists(manifest_path):
1400        m.parse(manifest_path, format="n3")
1401    m.parse(specloc, format="n3")
1402
1403    spec_url = getOntologyNS(m)
1404    spec = rdflib.URIRef(spec_url)
1405
1406    # Load all seeAlso files recursively
1407    seeAlso = set()
1408    done = False
1409    while not done:
1410        done = True
1411        for uri in specProperties(m, spec, rdfs.seeAlso):
1412            if uri[:7] == "file://":
1413                path = uri[7:]
1414                if (
1415                    path != os.path.abspath(specloc)
1416                    and path.endswith("ttl")
1417                    and path not in seeAlso
1418                ):
1419                    seeAlso.add(path)
1420                    m.parse(path, format="n3")
1421                    done = False
1422
1423    spec_ns_str = spec_url
1424    if spec_ns_str[-1] != "/" and spec_ns_str[-1] != "#":
1425        spec_ns_str += "#"
1426
1427    spec_ns = rdflib.Namespace(spec_ns_str)
1428
1429    namespaces = getNamespaces(m)
1430    keys = sorted(namespaces.keys())
1431    prefixes_html = "<span>"
1432    for i in keys:
1433        uri = namespaces[i]
1434        if uri.startswith("file:"):
1435            continue
1436        ns_list[str(uri)] = i
1437        if (
1438            str(uri) == spec_url + "#"
1439            or str(uri) == spec_url + "/"
1440            or str(uri) == spec_url
1441        ):
1442            spec_pre = i
1443        prefixes_html += '<a href="%s">%s</a> ' % (uri, i)
1444    prefixes_html += "</span>"
1445
1446    if spec_pre is None:
1447        print("No namespace prefix for %s defined" % specloc)
1448        sys.exit(1)
1449
1450    ns_list[spec_ns_str] = spec_pre
1451
1452    classlist, proplist = specInformation(m, spec_ns_str)
1453    classlist = sorted(classlist)
1454    proplist = sorted(proplist)
1455
1456    instalist = None
1457    if instances:
1458        instalist = sorted(
1459            getInstances(m, classlist, proplist),
1460            key=lambda x: getShortName(x).lower(),
1461        )
1462
1463    azlist = buildIndex(m, classlist, proplist, instalist)
1464
1465    # Generate Term HTML
1466    classlist = docTerms("Class", classlist, m, classlist, proplist, instalist)
1467    proplist = docTerms(
1468        "Property", proplist, m, classlist, proplist, instalist
1469    )
1470    if instances:
1471        instlist = docTerms(
1472            "Instance", instalist, m, classlist, proplist, instalist
1473        )
1474
1475    termlist = ""
1476    if classlist:
1477        termlist += '<div class="section">'
1478        termlist += '<h2><a id="ref-classes" />Classes</h2>' + classlist
1479        termlist += "</div>"
1480
1481    if proplist:
1482        termlist += '<div class="section">'
1483        termlist += '<h2><a id="ref-properties" />Properties</h2>' + proplist
1484        termlist += "</div>"
1485
1486    if instlist:
1487        termlist += '<div class="section">'
1488        termlist += '<h2><a id="ref-instances" />Instances</h2>' + instlist
1489        termlist += "</div>"
1490
1491    name = specProperty(m, spec, doap.name)
1492    title = name
1493    if root_link:
1494        name = '<a href="%s">%s</a>' % (root_link, name)
1495
1496    template = template.replace("@TITLE@", title)
1497    template = template.replace("@NAME@", name)
1498    template = template.replace(
1499        "@SHORT_DESC@", specProperty(m, spec, doap.shortdesc)
1500    )
1501    template = template.replace("@URI@", spec)
1502    template = template.replace("@PREFIX@", spec_pre)
1503    if spec_pre == "lv2":
1504        template = template.replace("@XMLNS@", "")
1505    else:
1506        template = template.replace(
1507            "@XMLNS@", '      xmlns:%s="%s"' % (spec_pre, spec_ns_str)
1508        )
1509
1510    filename = os.path.basename(specloc)
1511    basename = filename[0 : filename.rfind(".")]
1512
1513    template = template.replace("@STYLE_URI@", style_uri)
1514    template = template.replace("@PREFIXES@", str(prefixes_html))
1515    template = template.replace("@BASE@", spec_ns_str)
1516    template = template.replace("@AUTHORS@", specAuthors(m, spec))
1517    template = template.replace("@INDEX@", azlist)
1518    template = template.replace("@REFERENCE@", termlist)
1519    template = template.replace("@FILENAME@", filename)
1520    template = template.replace("@HEADER@", basename + ".h")
1521    template = template.replace("@HISTORY@", specHistory(m, spec))
1522
1523    mail_row = ""
1524    if "list_email" in opts:
1525        mail_row = '<tr><th>Discuss</th><td><a href="mailto:%s">%s</a>' % (
1526            opts["list_email"],
1527            opts["list_email"],
1528        )
1529        if "list_page" in opts:
1530            mail_row += ' <a href="%s">(subscribe)</a>' % opts["list_page"]
1531        mail_row += "</td></tr>"
1532    template = template.replace("@MAIL@", mail_row)
1533
1534    version = specVersion(m, spec)  # (minor, micro, date)
1535    date_string = version[2]
1536    if date_string == "":
1537        date_string = "Undated"
1538
1539    version_string = "%s.%s" % (version[0], version[1])
1540    experimental = version[0] == 0 or version[1] % 2 == 1
1541    if experimental:
1542        version_string += ' <span class="warning">EXPERIMENTAL</span>'
1543
1544    if isDeprecated(m, rdflib.URIRef(spec_url)):
1545        version_string += ' <span class="warning">DEPRECATED</span>'
1546
1547    template = template.replace("@VERSION@", version_string)
1548
1549    content_links = ""
1550    if docdir is not None:
1551        content_links = '<li><a href="%s">API</a></li>' % os.path.join(
1552            docdir, "group__%s.html" % basename
1553        )
1554
1555    template = template.replace("@CONTENT_LINKS@", content_links)
1556
1557    docs = getDetailedDocumentation(
1558        m, rdflib.URIRef(spec_url), classlist, proplist, instalist
1559    )
1560    template = template.replace("@DESCRIPTION@", docs)
1561
1562    now = int(os.environ.get("SOURCE_DATE_EPOCH", time.time()))
1563    build_date = datetime.datetime.utcfromtimestamp(now)
1564    template = template.replace("@DATE@", build_date.strftime("%F"))
1565    template = template.replace("@TIME@", build_date.strftime("%F %H:%M UTC"))
1566
1567    # Write index row
1568    if index_path is not None:
1569        writeIndex(m, specloc, index_path, root_path, root_uri, opts["online"])
1570
1571    # Validate complete output page
1572    try:
1573        oldcwd = os.getcwd()
1574        os.chdir(specgendir)
1575        etree.fromstring(
1576            template.replace(
1577                '"http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd"',
1578                '"DTD/xhtml-rdfa-1.dtd"',
1579            ).encode("utf-8"),
1580            etree.XMLParser(dtd_validation=True, no_network=True),
1581        )
1582    except Exception as e:
1583        sys.stderr.write("error: Validation failed for %s: %s" % (specloc, e))
1584    finally:
1585        os.chdir(oldcwd)
1586
1587    return template
1588
1589
1590def save(path, text):
1591    try:
1592        f = open(path, "w")
1593        f.write(text)
1594        f.flush()
1595        f.close()
1596    except Exception:
1597        e = sys.exc_info()[1]
1598        print('Error writing to file "' + path + '": ' + str(e))
1599
1600
1601def getNamespaces(m):
1602    """Return a prefix:URI dictionary of all namespaces seen during parsing"""
1603    nspaces = {}
1604    for prefix, uri in m.namespaces():
1605        if not re.match("default[0-9]*", prefix) and not prefix == "xml":
1606            # Skip silly default namespaces added by rdflib
1607            nspaces[prefix] = uri
1608    return nspaces
1609
1610
1611def getOntologyNS(m):
1612    ns = None
1613    s = findOne(m, None, rdf.type, lv2.Specification)
1614    if not s:
1615        s = findOne(m, None, rdf.type, owl.Ontology)
1616    if s:
1617        if not isBlank(getSubject(s)):
1618            ns = str(getSubject(s))
1619
1620    if ns is None:
1621        sys.exit("Impossible to get ontology's namespace")
1622    else:
1623        return ns
1624
1625
1626def usage():
1627    script = os.path.basename(sys.argv[0])
1628    return "Usage: %s ONTOLOGY_TTL OUTPUT_HTML [OPTION]..." % script
1629
1630
1631if __name__ == "__main__":
1632    """Ontology specification generator tool"""
1633
1634    indir = os.path.abspath(os.path.dirname(sys.argv[0]))
1635    if not os.path.exists(os.path.join(indir, "template.html")):
1636        indir = os.path.join(os.path.dirname(indir), "share", "lv2specgen")
1637
1638    opt = optparse.OptionParser(
1639        usage=usage(),
1640        description="Write HTML documentation for an RDF ontology.",
1641    )
1642    opt.add_option(
1643        "--list-email",
1644        type="string",
1645        dest="list_email",
1646        help="Mailing list email address",
1647    )
1648    opt.add_option(
1649        "--list-page",
1650        type="string",
1651        dest="list_page",
1652        help="Mailing list info page address",
1653    )
1654    opt.add_option(
1655        "--template-dir",
1656        type="string",
1657        dest="template_dir",
1658        default=indir,
1659        help="Template directory",
1660    )
1661    opt.add_option(
1662        "--style-uri",
1663        type="string",
1664        dest="style_uri",
1665        default="style.css",
1666        help="Stylesheet URI",
1667    )
1668    opt.add_option(
1669        "--docdir",
1670        type="string",
1671        dest="docdir",
1672        default=None,
1673        help="Doxygen output directory",
1674    )
1675    opt.add_option(
1676        "--index",
1677        type="string",
1678        dest="index_path",
1679        default=None,
1680        help="Index row output file",
1681    )
1682    opt.add_option(
1683        "--tags",
1684        type="string",
1685        dest="tags",
1686        default=None,
1687        help="Doxygen tags file",
1688    )
1689    opt.add_option(
1690        "-r",
1691        "--root-path",
1692        type="string",
1693        dest="root_path",
1694        default="",
1695        help="Root path",
1696    )
1697    opt.add_option(
1698        "-R",
1699        "--root-uri",
1700        type="string",
1701        dest="root_uri",
1702        default="",
1703        help="Root URI",
1704    )
1705    opt.add_option(
1706        "-p",
1707        "--prefix",
1708        type="string",
1709        dest="prefix",
1710        help="Specification Turtle prefix",
1711    )
1712    opt.add_option(
1713        "-i",
1714        "--instances",
1715        action="store_true",
1716        dest="instances",
1717        help="Document instances",
1718    )
1719    opt.add_option(
1720        "--copy-style",
1721        action="store_true",
1722        dest="copy_style",
1723        help="Copy style from template directory to output directory",
1724    )
1725    opt.add_option(
1726        "-o",
1727        "--online",
1728        action="store_true",
1729        dest="online",
1730        help="Generate index for online documentation",
1731    )
1732
1733    (options, args) = opt.parse_args()
1734    opts = vars(options)
1735
1736    if len(args) < 2:
1737        opt.print_help()
1738        sys.exit(-1)
1739
1740    spec_pre = options.prefix
1741    ontology = "file:" + str(args[0])
1742    output = args[1]
1743    index_path = options.index_path
1744    docdir = options.docdir
1745    tags = options.tags
1746
1747    out = "."
1748    spec = args[0]
1749    path = os.path.dirname(spec)
1750    outdir = os.path.abspath(os.path.join(out, path))
1751
1752    bundle = str(outdir)
1753    b = os.path.basename(outdir)
1754
1755    if not os.access(os.path.abspath(spec), os.R_OK):
1756        print("warning: extension %s has no %s.ttl file" % (b, b))
1757        sys.exit(1)
1758
1759    # Root link
1760    root_path = opts["root_path"]
1761    root_uri = opts["root_uri"]
1762    root_link = os.path.join(root_path, "index.html")
1763
1764    # Generate spec documentation
1765    specdoc = specgen(
1766        spec,
1767        indir,
1768        opts["style_uri"],
1769        docdir,
1770        tags,
1771        opts,
1772        instances=True,
1773        root_link=root_link,
1774        index_path=index_path,
1775        root_path=root_path,
1776        root_uri=root_uri,
1777    )
1778
1779    # Save to HTML output file
1780    save(output, specdoc)
1781
1782    if opts["copy_style"]:
1783        import shutil
1784
1785        shutil.copyfile(
1786            os.path.join(indir, "style.css"),
1787            os.path.join(os.path.dirname(output), "style.css"),
1788        )
1789