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