···
5
+
from typing import Any, Dict, List
6
+
from collections.abc import MutableMapping, Sequence
11
+
import markdown_it.renderer
12
+
from markdown_it.token import Token
13
+
from markdown_it.utils import OptionsDict
14
+
from mdit_py_plugins.container import container_plugin
15
+
from mdit_py_plugins.deflist import deflist_plugin
16
+
from mdit_py_plugins.myst_role import myst_role_plugin
17
+
from xml.sax.saxutils import escape, quoteattr
19
+
manpage_urls = json.load(open(os.getenv('MANPAGE_URLS')))
21
+
class Renderer(markdown_it.renderer.RendererProtocol):
22
+
__output__ = "docbook"
23
+
def __init__(self, parser=None):
26
+
for k, v in inspect.getmembers(self, predicate=inspect.ismethod)
27
+
if not (k.startswith("render") or k.startswith("_"))
29
+
"container_{.note}_open": self._note_open,
30
+
"container_{.note}_close": self._note_close,
31
+
"container_{.important}_open": self._important_open,
32
+
"container_{.important}_close": self._important_close,
33
+
"container_{.warning}_open": self._warning_open,
34
+
"container_{.warning}_close": self._warning_close,
36
+
def render(self, tokens: Sequence[Token], options: OptionsDict, env: MutableMapping) -> str:
37
+
assert '-link-tag-stack' not in env
38
+
env['-link-tag-stack'] = []
39
+
assert '-deflist-stack' not in env
40
+
env['-deflist-stack'] = []
41
+
def do_one(i, token):
42
+
if token.type == "inline":
43
+
assert token.children is not None
44
+
return self.renderInline(token.children, options, env)
45
+
elif token.type in self.rules:
46
+
return self.rules[token.type](tokens[i], tokens, i, options, env)
48
+
raise NotImplementedError("md token not supported yet", token)
49
+
return "".join(map(lambda arg: do_one(*arg), enumerate(tokens)))
50
+
def renderInline(self, tokens: Sequence[Token], options: OptionsDict, env: MutableMapping) -> str:
51
+
# HACK to support docbook links and xrefs. link handling is only necessary because the docbook
52
+
# manpage stylesheet converts - in urls to a mathematical minus, which may be somewhat incorrect.
53
+
for i, token in enumerate(tokens):
54
+
if token.type != 'link_open':
57
+
# turn [](#foo) into xrefs
58
+
if token.attrs['href'][0:1] == '#' and tokens[i + 1].type == 'link_close':
60
+
# turn <x> into links without contents
61
+
if tokens[i + 1].type == 'text' and tokens[i + 1].content == token.attrs['href']:
62
+
tokens[i + 1].content = ''
64
+
def do_one(i, token):
65
+
if token.type in self.rules:
66
+
return self.rules[token.type](tokens[i], tokens, i, options, env)
68
+
raise NotImplementedError("md node not supported yet", token)
69
+
return "".join(map(lambda arg: do_one(*arg), enumerate(tokens)))
71
+
def text(self, token, tokens, i, options, env):
72
+
return escape(token.content)
73
+
def paragraph_open(self, token, tokens, i, options, env):
75
+
def paragraph_close(self, token, tokens, i, options, env):
77
+
def hardbreak(self, token, tokens, i, options, env):
78
+
return "<literallayout>\n</literallayout>"
79
+
def softbreak(self, token, tokens, i, options, env):
80
+
# should check options.breaks() and emit hard break if so
82
+
def code_inline(self, token, tokens, i, options, env):
83
+
return f"<literal>{escape(token.content)}</literal>"
84
+
def code_block(self, token, tokens, i, options, env):
85
+
return f"<programlisting>{escape(token.content)}</programlisting>"
86
+
def link_open(self, token, tokens, i, options, env):
87
+
env['-link-tag-stack'].append(token.tag)
88
+
(attr, start) = ('linkend', 1) if token.attrs['href'][0] == '#' else ('xlink:href', 0)
89
+
return f"<{token.tag} {attr}={quoteattr(token.attrs['href'][start:])}>"
90
+
def link_close(self, token, tokens, i, options, env):
91
+
return f"</{env['-link-tag-stack'].pop()}>"
92
+
def list_item_open(self, token, tokens, i, options, env):
94
+
def list_item_close(self, token, tokens, i, options, env):
95
+
return "</listitem>\n"
96
+
# HACK open and close para for docbook change size. remove soon.
97
+
def bullet_list_open(self, token, tokens, i, options, env):
98
+
return "<para><itemizedlist>\n"
99
+
def bullet_list_close(self, token, tokens, i, options, env):
100
+
return "\n</itemizedlist></para>"
101
+
def em_open(self, token, tokens, i, options, env):
102
+
return "<emphasis>"
103
+
def em_close(self, token, tokens, i, options, env):
104
+
return "</emphasis>"
105
+
def strong_open(self, token, tokens, i, options, env):
106
+
return "<emphasis role=\"strong\">"
107
+
def strong_close(self, token, tokens, i, options, env):
108
+
return "</emphasis>"
109
+
def fence(self, token, tokens, i, options, env):
110
+
info = f" language={quoteattr(token.info)}" if token.info != "" else ""
111
+
return f"<programlisting{info}>{escape(token.content)}</programlisting>"
112
+
def blockquote_open(self, token, tokens, i, options, env):
113
+
return "<para><blockquote>"
114
+
def blockquote_close(self, token, tokens, i, options, env):
115
+
return "</blockquote></para>"
116
+
def _note_open(self, token, tokens, i, options, env):
117
+
return "<para><note>"
118
+
def _note_close(self, token, tokens, i, options, env):
119
+
return "</note></para>"
120
+
def _important_open(self, token, tokens, i, options, env):
121
+
return "<para><important>"
122
+
def _important_close(self, token, tokens, i, options, env):
123
+
return "</important></para>"
124
+
def _warning_open(self, token, tokens, i, options, env):
125
+
return "<para><warning>"
126
+
def _warning_close(self, token, tokens, i, options, env):
127
+
return "</warning></para>"
128
+
# markdown-it emits tokens based on the html syntax tree, but docbook is
129
+
# slightly different. html has <dl>{<dt/>{<dd/>}}</dl>,
130
+
# docbook has <variablelist>{<varlistentry><term/><listitem/></varlistentry>}<variablelist>
131
+
# we have to reject multiple definitions for the same term for time being.
132
+
def dl_open(self, token, tokens, i, options, env):
133
+
env['-deflist-stack'].append({})
134
+
return "<para><variablelist>"
135
+
def dl_close(self, token, tokens, i, options, env):
136
+
env['-deflist-stack'].pop()
137
+
return "</variablelist></para>"
138
+
def dt_open(self, token, tokens, i, options, env):
139
+
env['-deflist-stack'][-1]['has-dd'] = False
140
+
return "<varlistentry><term>"
141
+
def dt_close(self, token, tokens, i, options, env):
143
+
def dd_open(self, token, tokens, i, options, env):
144
+
if env['-deflist-stack'][-1]['has-dd']:
145
+
raise Exception("multiple definitions per term not supported")
146
+
env['-deflist-stack'][-1]['has-dd'] = True
147
+
return "<listitem>"
148
+
def dd_close(self, token, tokens, i, options, env):
149
+
return "</listitem></varlistentry>"
150
+
def myst_role(self, token, tokens, i, options, env):
151
+
if token.meta['name'] == 'command':
152
+
return f"<command>{escape(token.content)}</command>"
153
+
if token.meta['name'] == 'file':
154
+
return f"<filename>{escape(token.content)}</filename>"
155
+
if token.meta['name'] == 'var':
156
+
return f"<varname>{escape(token.content)}</varname>"
157
+
if token.meta['name'] == 'env':
158
+
return f"<envar>{escape(token.content)}</envar>"
159
+
if token.meta['name'] == 'option':
160
+
return f"<option>{escape(token.content)}</option>"
161
+
if token.meta['name'] == 'manpage':
162
+
[page, section] = [ s.strip() for s in token.content.rsplit('(', 1) ]
163
+
section = section[:-1]
164
+
man = f"{page}({section})"
165
+
title = f"<refentrytitle>{escape(page)}</refentrytitle>"
166
+
vol = f"<manvolnum>{escape(section)}</manvolnum>"
167
+
ref = f"<citerefentry>{title}{vol}</citerefentry>"
168
+
if man in manpage_urls:
169
+
return f"<link xlink:href={quoteattr(manpage_urls[man])}>{ref}</link>"
172
+
raise NotImplementedError("md node not supported yet", token)
175
+
markdown_it.MarkdownIt(renderer_cls=Renderer)
176
+
# TODO maybe fork the plugin and have only a single rule for all?
177
+
.use(container_plugin, name="{.note}")
178
+
.use(container_plugin, name="{.important}")
179
+
.use(container_plugin, name="{.warning}")
180
+
.use(deflist_plugin)
181
+
.use(myst_role_plugin)
184
+
# converts in-place!
185
+
def convertMD(options: Dict[str, Any]) -> str:
186
+
def optionIs(option: Dict[str, Any], key: str, typ: str) -> bool:
187
+
if key not in option: return False
188
+
if type(option[key]) != dict: return False
189
+
if '_type' not in option[key]: return False
190
+
return option[key]['_type'] == typ
192
+
def convertCode(name: str, option: Dict[str, Any], key: str):
193
+
if optionIs(option, key, 'literalMD'):
194
+
option[key] = md.render(f"*{key.capitalize()}:*\n{option[key]['text']}")
195
+
elif optionIs(option, key, 'literalExpression'):
196
+
code = option[key]['text']
197
+
# for multi-line code blocks we only have to count ` runs at the beginning
198
+
# of a line, but this is much easier.
199
+
multiline = '\n' in code
200
+
longest, current = (0, 0)
202
+
current = current + 1 if c == '`' else 0
203
+
longest = max(current, longest)
204
+
# inline literals need a space to separate ticks from content, code blocks
205
+
# need newlines. inline literals need one extra tick, code blocks need three.
206
+
ticks, sep = ('`' * (longest + (3 if multiline else 1)), '\n' if multiline else ' ')
207
+
code = f"{ticks}{sep}{code}{sep}{ticks}"
208
+
option[key] = md.render(f"*{key.capitalize()}:*\n{code}")
209
+
elif optionIs(option, key, 'literalDocBook'):
210
+
option[key] = f"<para><emphasis>{key.capitalize()}:</emphasis> {option[key]['text']}</para>"
211
+
elif key in option:
212
+
raise Exception(f"{name} {key} has unrecognized type", option[key])
214
+
for (name, option) in options.items():
216
+
if optionIs(option, 'description', 'mdDoc'):
217
+
option['description'] = md.render(option['description']['text'])
218
+
elif markdownByDefault:
219
+
option['description'] = md.render(option['description'])
221
+
option['description'] = ("<nixos:option-description><para>" +
222
+
option['description'] +
223
+
"</para></nixos:option-description>")
225
+
convertCode(name, option, 'example')
226
+
convertCode(name, option, 'default')
228
+
if 'relatedPackages' in option:
229
+
option['relatedPackages'] = md.render(option['relatedPackages'])
230
+
except Exception as e:
231
+
raise Exception(f"Failed to render option {name}") from e
235
+
id_translate_table = {
236
+
ord('*'): ord('_'),
237
+
ord('<'): ord('_'),
238
+
ord(' '): ord('_'),
239
+
ord('>'): ord('_'),
240
+
ord('['): ord('_'),
241
+
ord(']'): ord('_'),
242
+
ord(':'): ord('_'),
243
+
ord('"'): ord('_'),
247
+
if n not in os.environ:
248
+
raise RuntimeError("required environment variable not set", n)
249
+
return os.environ[n]
251
+
OTD_REVISION = need_env('OTD_REVISION')
252
+
OTD_DOCUMENT_TYPE = need_env('OTD_DOCUMENT_TYPE')
253
+
OTD_VARIABLE_LIST_ID = need_env('OTD_VARIABLE_LIST_ID')
254
+
OTD_OPTION_ID_PREFIX = need_env('OTD_OPTION_ID_PREFIX')
256
+
def print_decl_def(header, locs):
257
+
print(f"""<para><emphasis>{header}:</emphasis></para>""")
258
+
print(f"""<simplelist>""")
260
+
# locations can be either plain strings (specific to nixpkgs), or attrsets
261
+
# { name = "foo/bar.nix"; url = "https://github.com/....."; }
262
+
if isinstance(loc, str):
263
+
# Hyperlink the filename either to the NixOS github
264
+
# repository (if it’s a module and we have a revision number),
265
+
# or to the local filesystem.
266
+
if not loc.startswith('/'):
267
+
if OTD_REVISION == 'local':
268
+
href = f"https://github.com/NixOS/nixpkgs/blob/master/{loc}"
270
+
href = f"https://github.com/NixOS/nixpkgs/blob/{OTD_REVISION}/{loc}"
272
+
href = f"file://{loc}"
273
+
# Print the filename and make it user-friendly by replacing the
274
+
# /nix/store/<hash> prefix by the default location of nixos
276
+
if not loc.startswith('/'):
277
+
name = f"<nixpkgs/{loc}>"
278
+
elif loc.contains('nixops') and loc.contains('/nix/'):
279
+
name = f"<nixops/{loc[loc.find('/nix/') + 5:]}>"
282
+
print(f"""<member><filename xlink:href={quoteattr(href)}>""")
283
+
print(escape(name))
284
+
print(f"""</filename></member>""")
286
+
href = f" xlink:href={quoteattr(loc['url'])}" if 'url' in loc else ""
287
+
print(f"""<member><filename{href}>{escape(loc['name'])}</filename></member>""")
288
+
print(f"""</simplelist>""")
290
+
markdownByDefault = False
292
+
for arg in sys.argv[1:]:
293
+
if arg == "--markdown-by-default":
295
+
markdownByDefault = True
297
+
options = convertMD(json.load(open(sys.argv[1 + optOffset], 'r')))
299
+
keys = list(options.keys())
300
+
keys.sort(key=lambda opt: [ (0 if p.startswith("enable") else 1 if p.startswith("package") else 2, p)
301
+
for p in options[opt]['loc'] ])
303
+
print(f"""<?xml version="1.0" encoding="UTF-8"?>""")
304
+
if OTD_DOCUMENT_TYPE == 'appendix':
305
+
print("""<appendix xmlns="http://docbook.org/ns/docbook" xml:id="appendix-configuration-options">""")
306
+
print(""" <title>Configuration Options</title>""")
307
+
print(f"""<variablelist xmlns:xlink="http://www.w3.org/1999/xlink"
308
+
xmlns:nixos="tag:nixos.org"
309
+
xml:id="{OTD_VARIABLE_LIST_ID}">""")
312
+
opt = options[name]
313
+
id = OTD_OPTION_ID_PREFIX + name.translate(id_translate_table)
314
+
print(f"""<varlistentry>""")
315
+
# NOTE adding extra spaces here introduces spaces into xref link expansions
316
+
print(f"""<term xlink:href={quoteattr("#" + id)} xml:id={quoteattr(id)}>""", end='')
317
+
print(f"""<option>{escape(name)}</option>""", end='')
318
+
print(f"""</term>""")
319
+
print(f"""<listitem>""")
320
+
print(opt['description'])
321
+
if typ := opt.get('type'):
322
+
ro = " <emphasis>(read only)</emphasis>" if opt.get('readOnly', False) else ""
323
+
print(f"""<para><emphasis>Type:</emphasis> {escape(typ)}{ro}</para>""")
324
+
if default := opt.get('default'):
326
+
if example := opt.get('example'):
328
+
if related := opt.get('relatedPackages'):
329
+
print(f"""<para>""")
330
+
print(f""" <emphasis>Related packages:</emphasis>""")
331
+
print(f"""</para>""")
333
+
if decl := opt.get('declarations'):
334
+
print_decl_def("Declared by", decl)
335
+
if defs := opt.get('definitions'):
336
+
print_decl_def("Defined by", defs)
337
+
print(f"""</listitem>""")
338
+
print(f"""</varlistentry>""")
340
+
print("""</variablelist>""")
341
+
if OTD_DOCUMENT_TYPE == 'appendix':
342
+
print("""</appendix>""")