mirror of
https://github.com/NixOS/nixpkgs.git
synced 2024-11-16 14:54:29 +01:00
nixos-render-docs: add manual chapter rendering support
this is not yet able to produce manual-combined.xml, but the intention is to add that support before too long. for now we'll concentrate on getting the basics working: concatenating a list of chapters into a manual-combined fragment, which will be rendered via docbook.
This commit is contained in:
parent
4e2e950ab1
commit
8b8670db10
2 changed files with 147 additions and 0 deletions
|
@ -4,6 +4,7 @@ import sys
|
|||
from typing import Any, Dict
|
||||
|
||||
from .md import Converter
|
||||
from . import manual
|
||||
from . import options
|
||||
|
||||
def main() -> None:
|
||||
|
@ -12,9 +13,12 @@ def main() -> None:
|
|||
commands = parser.add_subparsers(dest='command', required=True)
|
||||
|
||||
options.build_cli(commands.add_parser('options'))
|
||||
manual.build_cli(commands.add_parser('manual'))
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.command == 'options':
|
||||
options.run_cli(args)
|
||||
elif args.command == 'manual':
|
||||
manual.run_cli(args)
|
||||
else:
|
||||
raise RuntimeError('command not hooked up', args)
|
||||
|
|
143
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py
Normal file
143
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py
Normal file
|
@ -0,0 +1,143 @@
|
|||
import argparse
|
||||
import json
|
||||
|
||||
from abc import abstractmethod
|
||||
from collections.abc import MutableMapping, Sequence
|
||||
from typing import Any, cast, NamedTuple, Optional, Union
|
||||
from xml.sax.saxutils import escape, quoteattr
|
||||
from markdown_it.token import Token
|
||||
from markdown_it.utils import OptionsDict
|
||||
|
||||
from .docbook import DocBookRenderer
|
||||
from .md import Converter
|
||||
|
||||
class RenderedSection:
|
||||
id: Optional[str]
|
||||
chapters: list[str]
|
||||
|
||||
def __init__(self, id: Optional[str]) -> None:
|
||||
self.id = id
|
||||
self.chapters = []
|
||||
|
||||
class BaseConverter(Converter):
|
||||
_sections: list[RenderedSection]
|
||||
|
||||
def __init__(self, manpage_urls: dict[str, str]):
|
||||
super().__init__(manpage_urls)
|
||||
self._sections = []
|
||||
|
||||
def add_section(self, id: Optional[str], chapters: list[str]) -> None:
|
||||
self._sections.append(RenderedSection(id))
|
||||
for content in chapters:
|
||||
self._md.renderer._title_seen = False # type: ignore[attr-defined]
|
||||
self._sections[-1].chapters.append(self._render(content))
|
||||
|
||||
@abstractmethod
|
||||
def finalize(self) -> str: raise NotImplementedError()
|
||||
|
||||
class ManualDocBookRenderer(DocBookRenderer):
|
||||
# needed to check correctness of chapters.
|
||||
# we may want to use front matter instead of this kind of heuristic.
|
||||
_title_seen = False
|
||||
|
||||
def _heading_tag(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict,
|
||||
env: MutableMapping[str, Any]) -> tuple[str, dict[str, str]]:
|
||||
(tag, attrs) = super()._heading_tag(token, tokens, i, options, env)
|
||||
if self._title_seen:
|
||||
if token.tag == 'h1':
|
||||
raise RuntimeError("only one title heading allowed", token)
|
||||
return (tag, attrs)
|
||||
self._title_seen = True
|
||||
return ("chapter", attrs | {
|
||||
'xmlns': "http://docbook.org/ns/docbook",
|
||||
'xmlns:xlink': "http://www.w3.org/1999/xlink",
|
||||
})
|
||||
|
||||
# TODO minimize docbook diffs with existing conversions. remove soon.
|
||||
def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict,
|
||||
env: MutableMapping[str, Any]) -> str:
|
||||
return super().paragraph_open(token, tokens, i, options, env) + "\n "
|
||||
def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict,
|
||||
env: MutableMapping[str, Any]) -> str:
|
||||
return "\n" + super().paragraph_close(token, tokens, i, options, env)
|
||||
def code_block(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict,
|
||||
env: MutableMapping[str, Any]) -> str:
|
||||
return f"<programlisting>\n{escape(token.content)}</programlisting>"
|
||||
def fence(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict,
|
||||
env: MutableMapping[str, Any]) -> str:
|
||||
info = f" language={quoteattr(token.info)}" if token.info != "" else ""
|
||||
return f"<programlisting{info}>\n{escape(token.content)}</programlisting>"
|
||||
|
||||
class DocBookConverter(BaseConverter):
|
||||
__renderer__ = ManualDocBookRenderer
|
||||
|
||||
def finalize(self) -> str:
|
||||
result = []
|
||||
|
||||
for section in self._sections:
|
||||
id = "id=" + quoteattr(section.id) if section.id is not None else ""
|
||||
result.append(f'<section {id}>')
|
||||
result += section.chapters
|
||||
result.append(f'</section>')
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
|
||||
class Section:
|
||||
id: Optional[str] = None
|
||||
chapters: list[str]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.chapters = []
|
||||
|
||||
class SectionAction(argparse.Action):
|
||||
def __call__(self, parser: argparse.ArgumentParser, ns: argparse.Namespace,
|
||||
values: Union[str, Sequence[Any], None], opt_str: Optional[str] = None) -> None:
|
||||
sections = getattr(ns, self.dest)
|
||||
if sections is None: sections = []
|
||||
sections.append(Section())
|
||||
setattr(ns, self.dest, sections)
|
||||
|
||||
class SectionIDAction(argparse.Action):
|
||||
def __call__(self, parser: argparse.ArgumentParser, ns: argparse.Namespace,
|
||||
values: Union[str, Sequence[Any], None], opt_str: Optional[str] = None) -> None:
|
||||
sections = getattr(ns, self.dest)
|
||||
if sections is None: raise argparse.ArgumentError(self, "no active section")
|
||||
sections[-1].id = cast(str, values)
|
||||
|
||||
class ChaptersAction(argparse.Action):
|
||||
def __call__(self, parser: argparse.ArgumentParser, ns: argparse.Namespace,
|
||||
values: Union[str, Sequence[Any], None], opt_str: Optional[str] = None) -> None:
|
||||
sections = getattr(ns, self.dest)
|
||||
if sections is None: raise argparse.ArgumentError(self, "no active section")
|
||||
sections[-1].chapters.extend(cast(Sequence[str], values))
|
||||
|
||||
def _build_cli_db(p: argparse.ArgumentParser) -> None:
|
||||
p.add_argument('--manpage-urls', required=True)
|
||||
p.add_argument("outfile")
|
||||
p.add_argument("--section", dest="contents", action=SectionAction, nargs=0)
|
||||
p.add_argument("--section-id", dest="contents", action=SectionIDAction)
|
||||
p.add_argument("--chapters", dest="contents", action=ChaptersAction, nargs='+')
|
||||
|
||||
def _run_cli_db(args: argparse.Namespace) -> None:
|
||||
with open(args.manpage_urls, 'r') as manpage_urls:
|
||||
md = DocBookConverter(json.load(manpage_urls))
|
||||
for section in args.contents:
|
||||
chapters = []
|
||||
for p in section.chapters:
|
||||
with open(p, 'r') as f:
|
||||
chapters.append(f.read())
|
||||
md.add_section(section.id, chapters)
|
||||
with open(args.outfile, 'w') as f:
|
||||
f.write(md.finalize())
|
||||
|
||||
def build_cli(p: argparse.ArgumentParser) -> None:
|
||||
formats = p.add_subparsers(dest='format', required=True)
|
||||
_build_cli_db(formats.add_parser('docbook'))
|
||||
|
||||
def run_cli(args: argparse.Namespace) -> None:
|
||||
if args.format == 'docbook':
|
||||
_run_cli_db(args)
|
||||
else:
|
||||
raise RuntimeError('format not hooked up', args)
|
Loading…
Reference in a new issue