about summary refs log tree commit diff
path: root/nixos/lib/make-options-doc/mergeJSON.py
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/lib/make-options-doc/mergeJSON.py')
-rw-r--r--nixos/lib/make-options-doc/mergeJSON.py146
1 files changed, 145 insertions, 1 deletions
diff --git a/nixos/lib/make-options-doc/mergeJSON.py b/nixos/lib/make-options-doc/mergeJSON.py
index 44a188a08c999..9510b1e59a20c 100644
--- a/nixos/lib/make-options-doc/mergeJSON.py
+++ b/nixos/lib/make-options-doc/mergeJSON.py
@@ -41,6 +41,150 @@ def unpivot(options: Dict[Key, Option]) -> Dict[str, JSON]:
         result[opt.name] = opt.value
     return result
 
+# converts in-place!
+def convertMD(options: Dict[str, Any]) -> str:
+    import mistune
+    import re
+    from xml.sax.saxutils import escape, quoteattr
+
+    admonitions = {
+        '.warning': 'warning',
+        '.important': 'important',
+        '.note': 'note'
+    }
+    class Renderer(mistune.renderers.BaseRenderer):
+        def _get_method(self, name):
+            try:
+                return super(Renderer, self)._get_method(name)
+            except AttributeError:
+                def not_supported(children, **kwargs):
+                    raise NotImplementedError("md node not supported yet", name, children, **kwargs)
+                return not_supported
+
+        def text(self, text):
+            return escape(text)
+        def paragraph(self, text):
+            return text + "\n\n"
+        def codespan(self, text):
+            return f"<literal>{text}</literal>"
+        def block_code(self, text, info=None):
+            info = f" language={quoteattr(info)}" if info is not None else ""
+            return f"<programlisting{info}>\n{text}</programlisting>"
+        def link(self, link, text=None, title=None):
+            if link[0:1] == '#':
+                attr = "linkend"
+                link = quoteattr(link[1:])
+            else:
+                # try to faithfully reproduce links that were of the form <link href="..."/>
+                # in docbook format
+                if text == link:
+                    text = ""
+                attr = "xlink:href"
+                link = quoteattr(link)
+            return f"<link {attr}={link}>{text}</link>"
+        def list(self, text, ordered, level, start=None):
+            if ordered:
+                raise NotImplementedError("ordered lists not supported yet")
+            return f"<itemizedlist>\n{text}\n</itemizedlist>"
+        def list_item(self, text, level):
+            return f"<listitem><para>{text}</para></listitem>\n"
+        def block_text(self, text):
+            return text
+        def emphasis(self, text):
+            return f"<emphasis>{text}</emphasis>"
+        def strong(self, text):
+            return f"<emphasis role=\"strong\">{text}</emphasis>"
+        def admonition(self, text, kind):
+            if kind not in admonitions:
+                raise NotImplementedError(f"admonition {kind} not supported yet")
+            tag = admonitions[kind]
+            # we don't keep whitespace here because usually we'll contain only
+            # a single paragraph and the original docbook string is no longer
+            # available to restore the trailer.
+            return f"<{tag}><para>{text.rstrip()}</para></{tag}>"
+        def command(self, text):
+            return f"<command>{escape(text)}</command>"
+        def option(self, text):
+            return f"<option>{escape(text)}</option>"
+        def file(self, text):
+            return f"<filename>{escape(text)}</filename>"
+        def manpage(self, page, section):
+            title = f"<refentrytitle>{escape(page)}</refentrytitle>"
+            vol = f"<manvolnum>{escape(section)}</manvolnum>"
+            return f"<citerefentry>{title}{vol}</citerefentry>"
+
+        def finalize(self, data):
+            return "".join(data)
+
+    plugins = []
+
+    COMMAND_PATTERN = r'\{command\}`(.*?)`'
+    def command(md):
+        def parse(self, m, state):
+            return ('command', m.group(1))
+        md.inline.register_rule('command', COMMAND_PATTERN, parse)
+        md.inline.rules.append('command')
+    plugins.append(command)
+
+    FILE_PATTERN = r'\{file\}`(.*?)`'
+    def file(md):
+        def parse(self, m, state):
+            return ('file', m.group(1))
+        md.inline.register_rule('file', FILE_PATTERN, parse)
+        md.inline.rules.append('file')
+    plugins.append(file)
+
+    OPTION_PATTERN = r'\{option\}`(.*?)`'
+    def option(md):
+        def parse(self, m, state):
+            return ('option', m.group(1))
+        md.inline.register_rule('option', OPTION_PATTERN, parse)
+        md.inline.rules.append('option')
+    plugins.append(option)
+
+    MANPAGE_PATTERN = r'\{manpage\}`(.*?)\((.+?)\)`'
+    def manpage(md):
+        def parse(self, m, state):
+            return ('manpage', m.group(1), m.group(2))
+        md.inline.register_rule('manpage', MANPAGE_PATTERN, parse)
+        md.inline.rules.append('manpage')
+    plugins.append(manpage)
+
+    ADMONITION_PATTERN = re.compile(r'^::: \{([^\n]*?)\}\n(.*?)^:::\n', flags=re.MULTILINE|re.DOTALL)
+    def admonition(md):
+        def parse(self, m, state):
+            return {
+                'type': 'admonition',
+                'children': self.parse(m.group(2), state),
+                'params': [ m.group(1) ],
+            }
+        md.block.register_rule('admonition', ADMONITION_PATTERN, parse)
+        md.block.rules.append('admonition')
+    plugins.append(admonition)
+
+    def convertString(text: str) -> str:
+        rendered = mistune.markdown(text, renderer=Renderer(), plugins=plugins)
+        # keep trailing spaces so we can diff the generated XML to check for conversion bugs.
+        return rendered.rstrip() + text[len(text.rstrip()):]
+
+    def optionIs(option: Dict[str, Any], key: str, typ: str) -> bool:
+        if key not in option: return False
+        if type(option[key]) != dict: return False
+        if '_type' not in option[key]: return False
+        return option[key]['_type'] == typ
+
+    for (name, option) in options.items():
+        if optionIs(option, 'description', 'mdDoc'):
+            option['description'] = convertString(option['description']['text'])
+        if optionIs(option, 'example', 'literalMD'):
+            docbook = convertString(option['example']['text'])
+            option['example'] = { '_type': 'literalDocBook', 'text': docbook }
+        if optionIs(option, 'default', 'literalMD'):
+            docbook = convertString(option['default']['text'])
+            option['default'] = { '_type': 'literalDocBook', 'text': docbook }
+
+    return options
+
 warningsAreErrors = sys.argv[1] == "--warnings-are-errors"
 optOffset = 1 if warningsAreErrors else 0
 options = pivot(json.load(open(sys.argv[1 + optOffset], 'r')))
@@ -92,4 +236,4 @@ if hasWarnings and warningsAreErrors:
         file=sys.stderr)
     sys.exit(1)
 
-json.dump(unpivot(options), fp=sys.stdout)
+json.dump(convertMD(unpivot(options)), fp=sys.stdout)