rngdoc.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. #!/usr/bin/python2 -O
  2. # vim: fileencoding=utf-8
  3. #
  4. # The Qubes OS Project, https://www.qubes-os.org/
  5. #
  6. # Copyright (C) 2014-2015 Joanna Rutkowska <joanna@invisiblethingslab.com>
  7. # Copyright (C) 2014-2015 Wojtek Porczyk <woju@invisiblethingslab.com>
  8. #
  9. # This program is free software; you can redistribute it and/or modify
  10. # it under the terms of the GNU General Public License as published by
  11. # the Free Software Foundation; either version 2 of the License, or
  12. # (at your option) any later version.
  13. #
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU General Public License along
  20. # with this program; if not, write to the Free Software Foundation, Inc.,
  21. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  22. #
  23. from __future__ import print_function
  24. import sys
  25. import textwrap
  26. import lxml.etree
  27. class Element(object):
  28. def __init__(self, schema, xml):
  29. self.schema = schema
  30. self.xml = xml
  31. @property
  32. def nsmap(self):
  33. return self.schema.nsmap
  34. @property
  35. def name(self):
  36. return self.xml.get('name')
  37. def get_description(self, xml=None, wrap=True):
  38. if xml is None:
  39. xml = self.xml
  40. xml = xml.xpath('./doc:description', namespaces=self.nsmap)
  41. if not xml:
  42. return ''
  43. xml = xml[0]
  44. if wrap:
  45. return ''.join(self.schema.wrapper.fill(p) + '\n\n'
  46. for p in textwrap.dedent(xml.text.strip('\n')).split('\n\n'))
  47. else:
  48. return ' '.join(xml.text.strip().split())
  49. def get_data_type(self, xml=None):
  50. if xml is None:
  51. xml = self.xml
  52. value = xml.xpath('./rng:value', namespaces=self.nsmap)
  53. if value:
  54. value = '``{}``'.format(value[0].text.strip())
  55. else:
  56. metavar = xml.xpath('./doc:metavar', namespaces=self.nsmap)
  57. if metavar:
  58. value = '``{}``'.format(metavar[0].text.strip())
  59. else:
  60. value = ''
  61. xml = xml.xpath('./rng:data', namespaces=self.nsmap)
  62. if not xml:
  63. return ('', value)
  64. xml = xml[0]
  65. type_ = xml.get('type', '')
  66. if not value:
  67. pattern = xml.xpath('./rng:param[@name="pattern"]',
  68. namespaces=self.nsmap)
  69. if pattern:
  70. value = '``{}``'.format(pattern[0].text.strip())
  71. return type_, value
  72. def get_attributes(self):
  73. for xml in self.xml.xpath('''./rng:attribute |
  74. ./rng:optional/rng:attribute |
  75. ./rng:choice/rng:attribute''', namespaces=self.nsmap):
  76. required = xml.getparent() == self.xml and 'yes' or 'no'
  77. yield (xml, required)
  78. def resolve_ref(self, ref):
  79. refs = self.xml.xpath(
  80. '//rng:define[name="{}"]/rng:element'.format(ref['name']))
  81. return refs[0] if refs else None
  82. def get_child_elements(self):
  83. for xml in self.xml.xpath('''./rng:element | ./rng:ref |
  84. ./rng:optional/rng:element | ./rng:optional/rng:ref |
  85. ./rng:zeroOrMore/rng:element | ./rng:zeroOrMore/rng:ref |
  86. ./rng:oneOrMore/rng:element | ./rng:oneOrMore/rng:ref''',
  87. namespaces=self.nsmap):
  88. parent = xml.getparent()
  89. qname = lxml.etree.QName(parent)
  90. if parent == self.xml:
  91. number = '1'
  92. elif qname.localname == 'optional':
  93. number = '?'
  94. elif qname.localname == 'zeroOrMore':
  95. number = '\\*'
  96. elif qname.localname == 'oneOrMore':
  97. number = '\\+'
  98. else:
  99. print(parent.tag)
  100. if xml.tag == 'ref':
  101. xml = self.resolve_ref(xml)
  102. if xml is None:
  103. continue
  104. yield (self.schema.elements[xml.get('name')], number)
  105. def write_rst(self, stream):
  106. stream.write('.. _qubesxml-element-{}:\n\n'.format(self.name))
  107. stream.write(make_rst_section('Element: **{}**'.format(self.name), '-'))
  108. stream.write(self.get_description())
  109. attrtable = []
  110. for attr, required in self.get_attributes():
  111. type_, value = self.get_data_type(attr)
  112. attrtable.append((
  113. attr.get('name'),
  114. required,
  115. type_,
  116. value,
  117. self.get_description(attr, wrap=False)))
  118. if attrtable:
  119. stream.write(make_rst_section('Attributes', '^'))
  120. write_rst_table(stream, attrtable,
  121. ('attribute', 'req.', 'type', 'value', 'description'))
  122. childtable = [(':ref:`{0} <qubesxml-element-{0}>`'.format(
  123. child.xml.get('name')), n)
  124. for child, n in self.get_child_elements()]
  125. if childtable:
  126. stream.write(make_rst_section('Child elements', '^'))
  127. write_rst_table(stream, childtable, ('element', 'number'))
  128. class Schema(object):
  129. # pylint: disable=too-few-public-methods
  130. nsmap = {
  131. 'rng': 'http://relaxng.org/ns/structure/1.0',
  132. 'q': 'http://qubes-os.org/qubes/3',
  133. 'doc': 'http://qubes-os.org/qubes-doc/1'}
  134. def __init__(self, xml):
  135. self.xml = xml
  136. self.wrapper = textwrap.TextWrapper(width=80,
  137. break_long_words=False, break_on_hyphens=False)
  138. self.elements = {}
  139. for node in self.xml.xpath('//rng:element', namespaces=self.nsmap):
  140. element = Element(self, node)
  141. self.elements[element.name] = element
  142. def make_rst_section(heading, char):
  143. return '{}\n{}\n\n'.format(heading, char[0] * len(heading))
  144. def write_rst_table(stream, itr, heads):
  145. stream.write('.. csv-table::\n')
  146. stream.write(' :header: {}\n'.format(', '.join('"{}"'.format(c)
  147. for c in heads)))
  148. stream.write(' :widths: {}\n\n'.format(', '.join('1'
  149. for c in heads)))
  150. for row in itr:
  151. stream.write(' {}\n'.format(', '.join('"{}"'.format(i) for i in row)))
  152. stream.write('\n')
  153. def main(filename, example):
  154. schema = Schema(lxml.etree.parse(open(filename, 'rb')))
  155. sys.stdout.write(make_rst_section('Qubes XML specification', '='))
  156. sys.stdout.write('''
  157. This is the documentation of qubes.xml autogenerated from RelaxNG source.
  158. Quick example, worth thousands lines of specification:
  159. .. literalinclude:: {}
  160. :language: xml
  161. '''[1:].format(example))
  162. for name in sorted(schema.elements):
  163. schema.elements[name].write_rst(sys.stdout)
  164. if __name__ == '__main__':
  165. main(*sys.argv[1:])
  166. # vim: ts=4 sw=4 et