···
+
from typing import Any, Dict, List
+
from collections.abc import MutableMapping, Sequence
+
import markdown_it.renderer
+
from markdown_it.token import Token
+
from markdown_it.utils import OptionsDict
+
from mdit_py_plugins.container import container_plugin
+
from mdit_py_plugins.deflist import deflist_plugin
+
from mdit_py_plugins.myst_role import myst_role_plugin
+
from xml.sax.saxutils import escape, quoteattr
+
manpage_urls = json.load(open(os.getenv('MANPAGE_URLS')))
+
class Renderer(markdown_it.renderer.RendererProtocol):
+
def __init__(self, parser=None):
+
for k, v in inspect.getmembers(self, predicate=inspect.ismethod)
+
if not (k.startswith("render") or k.startswith("_"))
+
"container_{.note}_open": self._note_open,
+
"container_{.note}_close": self._note_close,
+
"container_{.important}_open": self._important_open,
+
"container_{.important}_close": self._important_close,
+
"container_{.warning}_open": self._warning_open,
+
"container_{.warning}_close": self._warning_close,
+
def render(self, tokens: Sequence[Token], options: OptionsDict, env: MutableMapping) -> str:
+
assert '-link-tag-stack' not in env
+
env['-link-tag-stack'] = []
+
assert '-deflist-stack' not in env
+
env['-deflist-stack'] = []
+
if token.type == "inline":
+
assert token.children is not None
+
return self.renderInline(token.children, options, env)
+
elif token.type in self.rules:
+
return self.rules[token.type](tokens[i], tokens, i, options, env)
+
raise NotImplementedError("md token not supported yet", token)
+
return "".join(map(lambda arg: do_one(*arg), enumerate(tokens)))
+
def renderInline(self, tokens: Sequence[Token], options: OptionsDict, env: MutableMapping) -> str:
+
# HACK to support docbook links and xrefs. link handling is only necessary because the docbook
+
# manpage stylesheet converts - in urls to a mathematical minus, which may be somewhat incorrect.
+
for i, token in enumerate(tokens):
+
if token.type != 'link_open':
+
# turn [](#foo) into xrefs
+
if token.attrs['href'][0:1] == '#' and tokens[i + 1].type == 'link_close':
+
# turn <x> into links without contents
+
if tokens[i + 1].type == 'text' and tokens[i + 1].content == token.attrs['href']:
+
tokens[i + 1].content = ''
+
if token.type in self.rules:
+
return self.rules[token.type](tokens[i], tokens, i, options, env)
+
raise NotImplementedError("md node not supported yet", token)
+
return "".join(map(lambda arg: do_one(*arg), enumerate(tokens)))
+
def text(self, token, tokens, i, options, env):
+
return escape(token.content)
+
def paragraph_open(self, token, tokens, i, options, env):
+
def paragraph_close(self, token, tokens, i, options, env):
+
def hardbreak(self, token, tokens, i, options, env):
+
return "<literallayout>\n</literallayout>"
+
def softbreak(self, token, tokens, i, options, env):
+
# should check options.breaks() and emit hard break if so
+
def code_inline(self, token, tokens, i, options, env):
+
return f"<literal>{escape(token.content)}</literal>"
+
def code_block(self, token, tokens, i, options, env):
+
return f"<programlisting>{escape(token.content)}</programlisting>"
+
def link_open(self, token, tokens, i, options, env):
+
env['-link-tag-stack'].append(token.tag)
+
(attr, start) = ('linkend', 1) if token.attrs['href'][0] == '#' else ('xlink:href', 0)
+
return f"<{token.tag} {attr}={quoteattr(token.attrs['href'][start:])}>"
+
def link_close(self, token, tokens, i, options, env):
+
return f"</{env['-link-tag-stack'].pop()}>"
+
def list_item_open(self, token, tokens, i, options, env):
+
def list_item_close(self, token, tokens, i, options, env):
+
# HACK open and close para for docbook change size. remove soon.
+
def bullet_list_open(self, token, tokens, i, options, env):
+
return "<para><itemizedlist>\n"
+
def bullet_list_close(self, token, tokens, i, options, env):
+
return "\n</itemizedlist></para>"
+
def em_open(self, token, tokens, i, options, env):
+
def em_close(self, token, tokens, i, options, env):
+
def strong_open(self, token, tokens, i, options, env):
+
return "<emphasis role=\"strong\">"
+
def strong_close(self, token, tokens, i, options, env):
+
def fence(self, token, tokens, i, options, env):
+
info = f" language={quoteattr(token.info)}" if token.info != "" else ""
+
return f"<programlisting{info}>{escape(token.content)}</programlisting>"
+
def blockquote_open(self, token, tokens, i, options, env):
+
return "<para><blockquote>"
+
def blockquote_close(self, token, tokens, i, options, env):
+
return "</blockquote></para>"
+
def _note_open(self, token, tokens, i, options, env):
+
def _note_close(self, token, tokens, i, options, env):
+
return "</note></para>"
+
def _important_open(self, token, tokens, i, options, env):
+
return "<para><important>"
+
def _important_close(self, token, tokens, i, options, env):
+
return "</important></para>"
+
def _warning_open(self, token, tokens, i, options, env):
+
return "<para><warning>"
+
def _warning_close(self, token, tokens, i, options, env):
+
return "</warning></para>"
+
# markdown-it emits tokens based on the html syntax tree, but docbook is
+
# slightly different. html has <dl>{<dt/>{<dd/>}}</dl>,
+
# docbook has <variablelist>{<varlistentry><term/><listitem/></varlistentry>}<variablelist>
+
# we have to reject multiple definitions for the same term for time being.
+
def dl_open(self, token, tokens, i, options, env):
+
env['-deflist-stack'].append({})
+
return "<para><variablelist>"
+
def dl_close(self, token, tokens, i, options, env):
+
env['-deflist-stack'].pop()
+
return "</variablelist></para>"
+
def dt_open(self, token, tokens, i, options, env):
+
env['-deflist-stack'][-1]['has-dd'] = False
+
return "<varlistentry><term>"
+
def dt_close(self, token, tokens, i, options, env):
+
def dd_open(self, token, tokens, i, options, env):
+
if env['-deflist-stack'][-1]['has-dd']:
+
raise Exception("multiple definitions per term not supported")
+
env['-deflist-stack'][-1]['has-dd'] = True
+
def dd_close(self, token, tokens, i, options, env):
+
return "</listitem></varlistentry>"
+
def myst_role(self, token, tokens, i, options, env):
+
if token.meta['name'] == 'command':
+
return f"<command>{escape(token.content)}</command>"
+
if token.meta['name'] == 'file':
+
return f"<filename>{escape(token.content)}</filename>"
+
if token.meta['name'] == 'var':
+
return f"<varname>{escape(token.content)}</varname>"
+
if token.meta['name'] == 'env':
+
return f"<envar>{escape(token.content)}</envar>"
+
if token.meta['name'] == 'option':
+
return f"<option>{escape(token.content)}</option>"
+
if token.meta['name'] == 'manpage':
+
[page, section] = [ s.strip() for s in token.content.rsplit('(', 1) ]
+
man = f"{page}({section})"
+
title = f"<refentrytitle>{escape(page)}</refentrytitle>"
+
vol = f"<manvolnum>{escape(section)}</manvolnum>"
+
ref = f"<citerefentry>{title}{vol}</citerefentry>"
+
if man in manpage_urls:
+
return f"<link xlink:href={quoteattr(manpage_urls[man])}>{ref}</link>"
+
raise NotImplementedError("md node not supported yet", token)
+
markdown_it.MarkdownIt(renderer_cls=Renderer)
+
# TODO maybe fork the plugin and have only a single rule for all?
+
.use(container_plugin, name="{.note}")
+
.use(container_plugin, name="{.important}")
+
.use(container_plugin, name="{.warning}")
+
def convertMD(options: Dict[str, Any]) -> str:
+
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
+
def convertCode(name: str, option: Dict[str, Any], key: str):
+
if optionIs(option, key, 'literalMD'):
+
option[key] = md.render(f"*{key.capitalize()}:*\n{option[key]['text']}")
+
elif optionIs(option, key, 'literalExpression'):
+
code = option[key]['text']
+
# for multi-line code blocks we only have to count ` runs at the beginning
+
# of a line, but this is much easier.
+
multiline = '\n' in code
+
longest, current = (0, 0)
+
current = current + 1 if c == '`' else 0
+
longest = max(current, longest)
+
# inline literals need a space to separate ticks from content, code blocks
+
# need newlines. inline literals need one extra tick, code blocks need three.
+
ticks, sep = ('`' * (longest + (3 if multiline else 1)), '\n' if multiline else ' ')
+
code = f"{ticks}{sep}{code}{sep}{ticks}"
+
option[key] = md.render(f"*{key.capitalize()}:*\n{code}")
+
elif optionIs(option, key, 'literalDocBook'):
+
option[key] = f"<para><emphasis>{key.capitalize()}:</emphasis> {option[key]['text']}</para>"
+
raise Exception(f"{name} {key} has unrecognized type", option[key])
+
for (name, option) in options.items():
+
if optionIs(option, 'description', 'mdDoc'):
+
option['description'] = md.render(option['description']['text'])
+
elif markdownByDefault:
+
option['description'] = md.render(option['description'])
+
option['description'] = ("<nixos:option-description><para>" +
+
option['description'] +
+
"</para></nixos:option-description>")
+
convertCode(name, option, 'example')
+
convertCode(name, option, 'default')
+
if 'relatedPackages' in option:
+
option['relatedPackages'] = md.render(option['relatedPackages'])
+
raise Exception(f"Failed to render option {name}") from e
+
if n not in os.environ:
+
raise RuntimeError("required environment variable not set", n)
+
OTD_REVISION = need_env('OTD_REVISION')
+
OTD_DOCUMENT_TYPE = need_env('OTD_DOCUMENT_TYPE')
+
OTD_VARIABLE_LIST_ID = need_env('OTD_VARIABLE_LIST_ID')
+
OTD_OPTION_ID_PREFIX = need_env('OTD_OPTION_ID_PREFIX')
+
def print_decl_def(header, locs):
+
print(f"""<para><emphasis>{header}:</emphasis></para>""")
+
print(f"""<simplelist>""")
+
# locations can be either plain strings (specific to nixpkgs), or attrsets
+
# { name = "foo/bar.nix"; url = "https://github.com/....."; }
+
if isinstance(loc, str):
+
# Hyperlink the filename either to the NixOS github
+
# repository (if it’s a module and we have a revision number),
+
# or to the local filesystem.
+
if not loc.startswith('/'):
+
if OTD_REVISION == 'local':
+
href = f"https://github.com/NixOS/nixpkgs/blob/master/{loc}"
+
href = f"https://github.com/NixOS/nixpkgs/blob/{OTD_REVISION}/{loc}"
+
# Print the filename and make it user-friendly by replacing the
+
# /nix/store/<hash> prefix by the default location of nixos
+
if not loc.startswith('/'):
+
name = f"<nixpkgs/{loc}>"
+
elif loc.contains('nixops') and loc.contains('/nix/'):
+
name = f"<nixops/{loc[loc.find('/nix/') + 5:]}>"
+
print(f"""<member><filename xlink:href={quoteattr(href)}>""")
+
print(f"""</filename></member>""")
+
href = f" xlink:href={quoteattr(loc['url'])}" if 'url' in loc else ""
+
print(f"""<member><filename{href}>{escape(loc['name'])}</filename></member>""")
+
print(f"""</simplelist>""")
+
markdownByDefault = False
+
for arg in sys.argv[1:]:
+
if arg == "--markdown-by-default":
+
markdownByDefault = True
+
options = convertMD(json.load(open(sys.argv[1 + optOffset], 'r')))
+
keys = list(options.keys())
+
keys.sort(key=lambda opt: [ (0 if p.startswith("enable") else 1 if p.startswith("package") else 2, p)
+
for p in options[opt]['loc'] ])
+
print(f"""<?xml version="1.0" encoding="UTF-8"?>""")
+
if OTD_DOCUMENT_TYPE == 'appendix':
+
print("""<appendix xmlns="http://docbook.org/ns/docbook" xml:id="appendix-configuration-options">""")
+
print(""" <title>Configuration Options</title>""")
+
print(f"""<variablelist xmlns:xlink="http://www.w3.org/1999/xlink"
+
xmlns:nixos="tag:nixos.org"
+
xml:id="{OTD_VARIABLE_LIST_ID}">""")
+
id = OTD_OPTION_ID_PREFIX + name.translate(id_translate_table)
+
print(f"""<varlistentry>""")
+
# NOTE adding extra spaces here introduces spaces into xref link expansions
+
print(f"""<term xlink:href={quoteattr("#" + id)} xml:id={quoteattr(id)}>""", end='')
+
print(f"""<option>{escape(name)}</option>""", end='')
+
print(f"""<listitem>""")
+
print(opt['description'])
+
if typ := opt.get('type'):
+
ro = " <emphasis>(read only)</emphasis>" if opt.get('readOnly', False) else ""
+
print(f"""<para><emphasis>Type:</emphasis> {escape(typ)}{ro}</para>""")
+
if default := opt.get('default'):
+
if example := opt.get('example'):
+
if related := opt.get('relatedPackages'):
+
print(f""" <emphasis>Related packages:</emphasis>""")
+
if decl := opt.get('declarations'):
+
print_decl_def("Declared by", decl)
+
if defs := opt.get('definitions'):
+
print_decl_def("Defined by", defs)
+
print(f"""</listitem>""")
+
print(f"""</varlistentry>""")
+
print("""</variablelist>""")
+
if OTD_DOCUMENT_TYPE == 'appendix':
+
print("""</appendix>""")