rngdoc.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. #!/usr/bin/python2 -O
  2. from __future__ import print_function
  3. import sys
  4. import textwrap
  5. import lxml.etree
  6. class Element(object):
  7. def __init__(self, schema, xml):
  8. self.schema = schema
  9. self.xml = xml
  10. @property
  11. def nsmap(self):
  12. return self.schema.nsmap
  13. @property
  14. def name(self):
  15. return self.xml.get('name')
  16. def get_description(self, xml=None, wrap=True):
  17. if xml is None: xml = self.xml
  18. xml = xml.xpath('./doc:description', namespaces=self.nsmap)
  19. if not xml: return ''
  20. xml = xml[0]
  21. if wrap:
  22. return ''.join(self.schema.wrapper.fill(p) + '\n\n'
  23. for p in textwrap.dedent(xml.text.strip('\n')).split('\n\n'))
  24. else:
  25. return ' '.join(xml.text.strip().split())
  26. def get_data_type(self, xml=None):
  27. if xml is None: xml = self.xml
  28. value = xml.xpath('./rng:value', namespaces=self.nsmap)
  29. if value:
  30. value = '``{}``'.format(value[0].text.strip())
  31. else:
  32. metavar = xml.xpath('./doc:metavar', namespaces=self.nsmap)
  33. if metavar:
  34. value = '``{}``'.format(metavar[0].text.strip())
  35. else:
  36. value = ''
  37. xml = xml.xpath('./rng:data', namespaces=self.nsmap)
  38. if not xml:
  39. return ('', value)
  40. xml = xml[0]
  41. type_ = xml.get('type', '')
  42. if not value:
  43. pattern = xml.xpath('./rng:param[@name="pattern"]',
  44. namespaces=self.nsmap)
  45. if pattern:
  46. value = '``{}``'.format(pattern[0].text.strip())
  47. return type_, value
  48. def get_attributes(self):
  49. for xml in self.xml.xpath('''./rng:attribute |
  50. ./rng:optional/rng:attribute |
  51. ./rng:choice/rng:attribute''', namespaces=self.nsmap):
  52. required = xml.getparent() == self.xml and 'yes' or 'no'
  53. yield (xml, required)
  54. def resolve_ref(self, ref):
  55. refs = self.xml.xpath('//rng:define[name="{}"]/rng:element'.format(ref['name']))
  56. return refs[0] if refs else None
  57. def get_child_elements(self):
  58. for xml in self.xml.xpath('''./rng:element | ./rng:ref |
  59. ./rng:optional/rng:element | ./rng:optional/rng:ref |
  60. ./rng:zeroOrMore/rng:element | ./rng:zeroOrMore/rng:ref |
  61. ./rng:oneOrMore/rng:element | ./rng:oneOrMore/rng:ref''', namespaces=self.nsmap):
  62. parent = xml.getparent()
  63. qname = lxml.etree.QName(parent)
  64. if parent == self.xml:
  65. n = '1'
  66. elif qname.localname == 'optional':
  67. n = '?'
  68. elif qname.localname == 'zeroOrMore':
  69. n = '\\*'
  70. elif qname.localname == 'oneOrMore':
  71. n = '\\+'
  72. else:
  73. print(parent.tag)
  74. if xml.tag == 'ref':
  75. xml = self.resolve_ref(xml)
  76. if xml is None: continue
  77. yield (self.schema.elements[xml.get('name')], n)
  78. def write_rst(self, stream):
  79. stream.write('.. _qubesxml-element-{}:\n\n'.format(self.name))
  80. stream.write(make_rst_section('Element: **{}**'.format(self.name), '-'))
  81. stream.write(self.get_description())
  82. attrtable = []
  83. for attr, required in self.get_attributes():
  84. type_, value = self.get_data_type(attr)
  85. attrtable.append((
  86. attr.get('name'),
  87. required,
  88. type_,
  89. value,
  90. self.get_description(attr, wrap=False)))
  91. if attrtable:
  92. stream.write(make_rst_section('Attributes', '^'))
  93. write_rst_table(stream, attrtable,
  94. ('attribute', 'req.', 'type', 'value', 'description'))
  95. childtable = [(':ref:`{0} <qubesxml-element-{0}>`'.format(child.xml.get('name')), n)
  96. for child, n in self.get_child_elements()]
  97. if childtable:
  98. stream.write(make_rst_section('Child elements', '^'))
  99. write_rst_table(stream, childtable, ('element', 'n'))
  100. class Schema(object):
  101. nsmap = {
  102. 'rng': 'http://relaxng.org/ns/structure/1.0',
  103. 'q': 'http://qubes-os.org/qubes/3',
  104. 'doc': 'http://qubes-os.org/qubes-doc/1'}
  105. def __init__(self, xml):
  106. self.xml = xml
  107. self.wrapper = textwrap.TextWrapper(width=80,
  108. break_long_words=False, break_on_hyphens=False)
  109. self.elements = {}
  110. for x in self.xml.xpath('//rng:element', namespaces=self.nsmap):
  111. element = Element(self, x)
  112. self.elements[element.name] = element
  113. def make_rst_section(heading, c):
  114. return '{}\n{}\n\n'.format(heading, c[0] * len(heading))
  115. def write_rst_table(stream, it, heads):
  116. stream.write('.. csv-table::\n')
  117. stream.write(' :header: {}\n'.format(', '.join('"{}"'.format(c) for c in heads)))
  118. stream.write(' :widths: {}\n\n'.format(', '.join('1' for c in heads)))
  119. for row in it:
  120. stream.write(' {}\n'.format(', '.join('"{}"'.format(i) for i in row)))
  121. stream.write('\n')
  122. def main(filename, example):
  123. schema = Schema(lxml.etree.parse(open(filename, 'rb')))
  124. sys.stdout.write(make_rst_section('Qubes XML specification', '='))
  125. sys.stdout.write('This is the documentation of qubes.xml autogenerated from RelaxNG source.\n\n')
  126. sys.stdout.write('Quick example, worth thousands lines of specification:\n\n')
  127. sys.stdout.write('.. literalinclude:: {}\n :language: xml\n\n'.format(example))
  128. for name in sorted(schema.elements):
  129. schema.elements[name].write_rst(sys.stdout)
  130. if __name__ == '__main__':
  131. main(*sys.argv[1:])
  132. # vim: ts=4 sw=4 et