lol

nixos/make-options-doc: reuse markdown instance in mergeJSON

this doesn't construct a new (expensive) parser for every option, making
rendering about 30x faster.

pennae 7a091b26 3aebb4a2

+117 -117
+117 -117
nixos/lib/make-options-doc/mergeJSON.py
··· 3 3 import sys 4 4 from typing import Any, Dict, List 5 5 6 + # for MD conversion 7 + import mistune 8 + import re 9 + from xml.sax.saxutils import escape, quoteattr 10 + 6 11 JSON = Dict[str, Any] 7 12 8 13 class Key: ··· 41 46 result[opt.name] = opt.value 42 47 return result 43 48 44 - # converts in-place! 45 - def convertMD(options: Dict[str, Any]) -> str: 46 - import mistune 47 - import re 48 - from xml.sax.saxutils import escape, quoteattr 49 + admonitions = { 50 + '.warning': 'warning', 51 + '.important': 'important', 52 + '.note': 'note' 53 + } 54 + class Renderer(mistune.renderers.BaseRenderer): 55 + def _get_method(self, name): 56 + try: 57 + return super(Renderer, self)._get_method(name) 58 + except AttributeError: 59 + def not_supported(*args, **kwargs): 60 + raise NotImplementedError("md node not supported yet", name, args, **kwargs) 61 + return not_supported 49 62 50 - admonitions = { 51 - '.warning': 'warning', 52 - '.important': 'important', 53 - '.note': 'note' 54 - } 55 - class Renderer(mistune.renderers.BaseRenderer): 56 - def __init__(self, path): 57 - self.path = path 58 - def _get_method(self, name): 59 - try: 60 - return super(Renderer, self)._get_method(name) 61 - except AttributeError: 62 - def not_supported(*args, **kwargs): 63 - raise NotImplementedError("md node not supported yet", self.path, name, args, **kwargs) 64 - return not_supported 65 - 66 - def text(self, text): 67 - return escape(text) 68 - def paragraph(self, text): 69 - return text + "\n\n" 70 - def newline(self): 71 - return "<literallayout>\n</literallayout>" 72 - def codespan(self, text): 73 - return f"<literal>{escape(text)}</literal>" 74 - def block_code(self, text, info=None): 75 - info = f" language={quoteattr(info)}" if info is not None else "" 76 - return f"<programlisting{info}>\n{escape(text)}</programlisting>" 77 - def link(self, link, text=None, title=None): 78 - if link[0:1] == '#': 79 - attr = "linkend" 80 - link = quoteattr(link[1:]) 81 - else: 82 - # try to faithfully reproduce links that were of the form <link href="..."/> 83 - # in docbook format 84 - if text == link: 85 - text = "" 86 - attr = "xlink:href" 87 - link = quoteattr(link) 88 - return f"<link {attr}={link}>{text}</link>" 89 - def list(self, text, ordered, level, start=None): 90 - if ordered: 91 - raise NotImplementedError("ordered lists not supported yet") 92 - return f"<itemizedlist>\n{text}\n</itemizedlist>" 93 - def list_item(self, text, level): 94 - return f"<listitem><para>{text}</para></listitem>\n" 95 - def block_text(self, text): 96 - return text 97 - def emphasis(self, text): 98 - return f"<emphasis>{text}</emphasis>" 99 - def strong(self, text): 100 - return f"<emphasis role=\"strong\">{text}</emphasis>" 101 - def admonition(self, text, kind): 102 - if kind not in admonitions: 103 - raise NotImplementedError(f"admonition {kind} not supported yet") 104 - tag = admonitions[kind] 105 - # we don't keep whitespace here because usually we'll contain only 106 - # a single paragraph and the original docbook string is no longer 107 - # available to restore the trailer. 108 - return f"<{tag}><para>{text.rstrip()}</para></{tag}>" 109 - def block_quote(self, text): 110 - return f"<blockquote><para>{text}</para></blockquote>" 111 - def command(self, text): 112 - return f"<command>{escape(text)}</command>" 113 - def option(self, text): 114 - return f"<option>{escape(text)}</option>" 115 - def file(self, text): 116 - return f"<filename>{escape(text)}</filename>" 117 - def manpage(self, page, section): 118 - title = f"<refentrytitle>{escape(page)}</refentrytitle>" 119 - vol = f"<manvolnum>{escape(section)}</manvolnum>" 120 - return f"<citerefentry>{title}{vol}</citerefentry>" 121 - 122 - def finalize(self, data): 123 - return "".join(data) 63 + def text(self, text): 64 + return escape(text) 65 + def paragraph(self, text): 66 + return text + "\n\n" 67 + def newline(self): 68 + return "<literallayout>\n</literallayout>" 69 + def codespan(self, text): 70 + return f"<literal>{escape(text)}</literal>" 71 + def block_code(self, text, info=None): 72 + info = f" language={quoteattr(info)}" if info is not None else "" 73 + return f"<programlisting{info}>\n{escape(text)}</programlisting>" 74 + def link(self, link, text=None, title=None): 75 + if link[0:1] == '#': 76 + attr = "linkend" 77 + link = quoteattr(link[1:]) 78 + else: 79 + # try to faithfully reproduce links that were of the form <link href="..."/> 80 + # in docbook format 81 + if text == link: 82 + text = "" 83 + attr = "xlink:href" 84 + link = quoteattr(link) 85 + return f"<link {attr}={link}>{text}</link>" 86 + def list(self, text, ordered, level, start=None): 87 + if ordered: 88 + raise NotImplementedError("ordered lists not supported yet") 89 + return f"<itemizedlist>\n{text}\n</itemizedlist>" 90 + def list_item(self, text, level): 91 + return f"<listitem><para>{text}</para></listitem>\n" 92 + def block_text(self, text): 93 + return text 94 + def emphasis(self, text): 95 + return f"<emphasis>{text}</emphasis>" 96 + def strong(self, text): 97 + return f"<emphasis role=\"strong\">{text}</emphasis>" 98 + def admonition(self, text, kind): 99 + if kind not in admonitions: 100 + raise NotImplementedError(f"admonition {kind} not supported yet") 101 + tag = admonitions[kind] 102 + # we don't keep whitespace here because usually we'll contain only 103 + # a single paragraph and the original docbook string is no longer 104 + # available to restore the trailer. 105 + return f"<{tag}><para>{text.rstrip()}</para></{tag}>" 106 + def block_quote(self, text): 107 + return f"<blockquote><para>{text}</para></blockquote>" 108 + def command(self, text): 109 + return f"<command>{escape(text)}</command>" 110 + def option(self, text): 111 + return f"<option>{escape(text)}</option>" 112 + def file(self, text): 113 + return f"<filename>{escape(text)}</filename>" 114 + def manpage(self, page, section): 115 + title = f"<refentrytitle>{escape(page)}</refentrytitle>" 116 + vol = f"<manvolnum>{escape(section)}</manvolnum>" 117 + return f"<citerefentry>{title}{vol}</citerefentry>" 124 118 125 - plugins = [] 119 + def finalize(self, data): 120 + return "".join(data) 126 121 122 + def p_command(md): 127 123 COMMAND_PATTERN = r'\{command\}`(.*?)`' 128 - def command(md): 129 - def parse(self, m, state): 130 - return ('command', m.group(1)) 131 - md.inline.register_rule('command', COMMAND_PATTERN, parse) 132 - md.inline.rules.append('command') 133 - plugins.append(command) 124 + def parse(self, m, state): 125 + return ('command', m.group(1)) 126 + md.inline.register_rule('command', COMMAND_PATTERN, parse) 127 + md.inline.rules.append('command') 134 128 129 + def p_file(md): 135 130 FILE_PATTERN = r'\{file\}`(.*?)`' 136 - def file(md): 137 - def parse(self, m, state): 138 - return ('file', m.group(1)) 139 - md.inline.register_rule('file', FILE_PATTERN, parse) 140 - md.inline.rules.append('file') 141 - plugins.append(file) 131 + def parse(self, m, state): 132 + return ('file', m.group(1)) 133 + md.inline.register_rule('file', FILE_PATTERN, parse) 134 + md.inline.rules.append('file') 142 135 136 + def p_option(md): 143 137 OPTION_PATTERN = r'\{option\}`(.*?)`' 144 - def option(md): 145 - def parse(self, m, state): 146 - return ('option', m.group(1)) 147 - md.inline.register_rule('option', OPTION_PATTERN, parse) 148 - md.inline.rules.append('option') 149 - plugins.append(option) 138 + def parse(self, m, state): 139 + return ('option', m.group(1)) 140 + md.inline.register_rule('option', OPTION_PATTERN, parse) 141 + md.inline.rules.append('option') 150 142 143 + def p_manpage(md): 151 144 MANPAGE_PATTERN = r'\{manpage\}`(.*?)\((.+?)\)`' 152 - def manpage(md): 153 - def parse(self, m, state): 154 - return ('manpage', m.group(1), m.group(2)) 155 - md.inline.register_rule('manpage', MANPAGE_PATTERN, parse) 156 - md.inline.rules.append('manpage') 157 - plugins.append(manpage) 145 + def parse(self, m, state): 146 + return ('manpage', m.group(1), m.group(2)) 147 + md.inline.register_rule('manpage', MANPAGE_PATTERN, parse) 148 + md.inline.rules.append('manpage') 158 149 150 + def p_admonition(md): 159 151 ADMONITION_PATTERN = re.compile(r'^::: \{([^\n]*?)\}\n(.*?)^:::\n', flags=re.MULTILINE|re.DOTALL) 160 - def admonition(md): 161 - def parse(self, m, state): 162 - return { 163 - 'type': 'admonition', 164 - 'children': self.parse(m.group(2), state), 165 - 'params': [ m.group(1) ], 166 - } 167 - md.block.register_rule('admonition', ADMONITION_PATTERN, parse) 168 - md.block.rules.append('admonition') 169 - plugins.append(admonition) 152 + def parse(self, m, state): 153 + return { 154 + 'type': 'admonition', 155 + 'children': self.parse(m.group(2), state), 156 + 'params': [ m.group(1) ], 157 + } 158 + md.block.register_rule('admonition', ADMONITION_PATTERN, parse) 159 + md.block.rules.append('admonition') 160 + 161 + md = mistune.create_markdown(renderer=Renderer(), plugins=[ 162 + p_command, p_file, p_option, p_manpage, p_admonition 163 + ]) 170 164 165 + # converts in-place! 166 + def convertMD(options: Dict[str, Any]) -> str: 171 167 def convertString(path: str, text: str) -> str: 172 - rendered = mistune.markdown(text, renderer=Renderer(path), plugins=plugins) 173 - # keep trailing spaces so we can diff the generated XML to check for conversion bugs. 174 - return rendered.rstrip() + text[len(text.rstrip()):] 168 + try: 169 + rendered = md(text) 170 + # keep trailing spaces so we can diff the generated XML to check for conversion bugs. 171 + return rendered.rstrip() + text[len(text.rstrip()):] 172 + except: 173 + print(f"error in {path}") 174 + raise 175 175 176 176 def optionIs(option: Dict[str, Any], key: str, typ: str) -> bool: 177 177 if key not in option: return False