rngdoc.py 5.7 KB

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