nixos-render-docs: add --admonition-style gfm to commonmark renderer
Support rendering markdown admonishments using GitHub-flavoured markdown "alerts" syntax. This is useful for targets like mkBook which also support the GFM-alerts extension.
This commit is contained in:
@@ -3,6 +3,7 @@ from dataclasses import dataclass
|
||||
from typing import cast, Optional
|
||||
|
||||
from .md import md_escape, md_make_code, Renderer
|
||||
from .types import AdmonitionStyle
|
||||
|
||||
from markdown_it.token import Token
|
||||
|
||||
@@ -20,12 +21,14 @@ class Par:
|
||||
class CommonMarkRenderer(Renderer):
|
||||
__output__ = "commonmark"
|
||||
|
||||
_admonition_style: AdmonitionStyle
|
||||
_parstack: list[Par]
|
||||
_link_stack: list[str]
|
||||
_list_stack: list[List]
|
||||
|
||||
def __init__(self, manpage_urls: Mapping[str, str]):
|
||||
def __init__(self, manpage_urls: Mapping[str, str], admonition_style: AdmonitionStyle = AdmonitionStyle.PLAIN):
|
||||
super().__init__(manpage_urls)
|
||||
self._admonition_style = admonition_style
|
||||
self._parstack = [ Par("") ]
|
||||
self._link_stack = []
|
||||
self._list_stack = []
|
||||
@@ -44,11 +47,23 @@ class CommonMarkRenderer(Renderer):
|
||||
return result
|
||||
|
||||
def _admonition_open(self, kind: str) -> str:
|
||||
pbreak = self._maybe_parbreak()
|
||||
self._enter_block("")
|
||||
return f"{pbreak}**{kind}:** "
|
||||
match self._admonition_style:
|
||||
case AdmonitionStyle.PLAIN:
|
||||
pbreak = self._maybe_parbreak()
|
||||
self._enter_block("")
|
||||
return f"{pbreak}**{kind}:** "
|
||||
case AdmonitionStyle.GFM:
|
||||
pbreak = self._maybe_parbreak()
|
||||
lbreak = self._break()
|
||||
self._enter_block("> ")
|
||||
return f"{pbreak}> [!{kind}]{lbreak}> "
|
||||
|
||||
def _admonition_close(self) -> str:
|
||||
self._leave_block()
|
||||
match self._admonition_style:
|
||||
case AdmonitionStyle.PLAIN:
|
||||
self._leave_block()
|
||||
case AdmonitionStyle.GFM:
|
||||
self._leave_block()
|
||||
return ""
|
||||
|
||||
def _indent_raw(self, s: str) -> str:
|
||||
|
||||
@@ -21,7 +21,7 @@ from .html import HTMLRenderer
|
||||
from .manpage import ManpageRenderer, man_escape
|
||||
from .manual_structure import make_xml_id, XrefTarget
|
||||
from .md import Converter, md_escape, md_make_code
|
||||
from .types import OptionLoc, Option, RenderedOption, AnchorStyle
|
||||
from .types import OptionLoc, Option, RenderedOption, AnchorStyle, AdmonitionStyle
|
||||
|
||||
def option_is(option: Option, key: str, typ: str) -> Optional[dict[str, str]]:
|
||||
if key not in option:
|
||||
@@ -317,18 +317,20 @@ class OptionsCommonMarkRenderer(OptionDocsRestrictions, CommonMarkRenderer):
|
||||
|
||||
class CommonMarkConverter(BaseConverter[OptionsCommonMarkRenderer]):
|
||||
__option_block_separator__ = ""
|
||||
_admonition_style: AdmonitionStyle
|
||||
_anchor_style: AnchorStyle
|
||||
_anchor_prefix: str
|
||||
|
||||
|
||||
def __init__(self, manpage_urls: Mapping[str, str], revision: str, anchor_style: AnchorStyle = AnchorStyle.NONE, anchor_prefix: str = ""):
|
||||
def __init__(self, manpage_urls: Mapping[str, str], revision: str, anchor_style: AnchorStyle = AnchorStyle.NONE, anchor_prefix: str = "", admonition_style: AdmonitionStyle = AdmonitionStyle.PLAIN):
|
||||
super().__init__(revision)
|
||||
self._renderer = OptionsCommonMarkRenderer(manpage_urls)
|
||||
self._renderer = OptionsCommonMarkRenderer(manpage_urls, admonition_style)
|
||||
self._admonition_style = admonition_style
|
||||
self._anchor_style = anchor_style
|
||||
self._anchor_prefix = anchor_prefix
|
||||
|
||||
def _parallel_render_prepare(self) -> Any:
|
||||
return (self._renderer._manpage_urls, self._revision, self._anchor_style, self._anchor_prefix)
|
||||
return (self._renderer._manpage_urls, self._revision, self._anchor_style, self._anchor_prefix, self._admonition_style)
|
||||
@classmethod
|
||||
def _parallel_render_init_worker(cls, a: Any) -> CommonMarkConverter:
|
||||
return cls(*a)
|
||||
@@ -514,6 +516,15 @@ def parse_anchor_style(value: str|AnchorStyle) -> AnchorStyle:
|
||||
except ValueError:
|
||||
raise argparse.ArgumentTypeError(f"Invalid value {value}\nExpected one of {', '.join(style.value for style in AnchorStyle)}")
|
||||
|
||||
def parse_admonition_style(value: str|AdmonitionStyle) -> AdmonitionStyle:
|
||||
if isinstance(value, AdmonitionStyle):
|
||||
# Used by `argparse.add_argument`'s `default`
|
||||
return value
|
||||
try:
|
||||
return AdmonitionStyle(value.lower())
|
||||
except ValueError:
|
||||
raise argparse.ArgumentTypeError(f"Invalid value {value}\nExpected one of {', '.join(style.value for style in AdmonitionStyle)}")
|
||||
|
||||
def _build_cli_commonmark(p: argparse.ArgumentParser) -> None:
|
||||
p.add_argument('--manpage-urls', required=True)
|
||||
p.add_argument('--revision', required=True)
|
||||
@@ -529,6 +540,13 @@ def _build_cli_commonmark(p: argparse.ArgumentParser) -> None:
|
||||
default="",
|
||||
help="(default: no prefix) String to prepend to anchor ids. Not used when anchor style is none."
|
||||
)
|
||||
p.add_argument(
|
||||
'--admonition-style',
|
||||
required=False,
|
||||
default=AdmonitionStyle.PLAIN.value,
|
||||
choices = [style.value for style in AdmonitionStyle],
|
||||
help = "(default: %(default)s) Admonition style to use for notes, warnings, etc. \nOnly plain is standard CommonMark."
|
||||
)
|
||||
p.add_argument("infile")
|
||||
p.add_argument("outfile")
|
||||
|
||||
@@ -566,7 +584,9 @@ def _run_cli_commonmark(args: argparse.Namespace) -> None:
|
||||
md = CommonMarkConverter(json.load(manpage_urls),
|
||||
revision = args.revision,
|
||||
anchor_style = parse_anchor_style(args.anchor_style),
|
||||
anchor_prefix = args.anchor_prefix)
|
||||
anchor_prefix = args.anchor_prefix,
|
||||
admonition_style = parse_admonition_style(args.admonition_style),
|
||||
)
|
||||
|
||||
with open(args.infile, 'r') as f:
|
||||
md.add_options(json.load(f))
|
||||
|
||||
@@ -17,3 +17,7 @@ RenderFn = Callable[[Token, Sequence[Token], int], str]
|
||||
class AnchorStyle(Enum):
|
||||
NONE = "none"
|
||||
LEGACY = "legacy"
|
||||
|
||||
class AdmonitionStyle(Enum):
|
||||
PLAIN = "plain"
|
||||
GFM = "gfm"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"services.frobnicator.types.<name>.enable": {
|
||||
"declarations": [
|
||||
"nixos/modules/services/frobnicator.nix"
|
||||
],
|
||||
"description": "Whether to enable the frobnication of this (`<name>`) type.\n::: {.important}\n\nAdmonition.\n\n:::",
|
||||
"loc": [
|
||||
"services",
|
||||
"frobnicator",
|
||||
"types",
|
||||
"<name>",
|
||||
"enable"
|
||||
],
|
||||
"readOnly": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
## services\.frobnicator\.types\.\<name>\.enable
|
||||
|
||||
Whether to enable the frobnication of this (` <name> `) type\.
|
||||
|
||||
> [!Important]
|
||||
> Admonition\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
boolean
|
||||
|
||||
*Declared by:*
|
||||
- [\<nixpkgs/nixos/modules/services/frobnicator\.nix>](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/frobnicator.nix)
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
## services\.frobnicator\.types\.\<name>\.enable
|
||||
|
||||
Whether to enable the frobnication of this (` <name> `) type\.
|
||||
|
||||
**Important:** Admonition\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
boolean
|
||||
|
||||
*Declared by:*
|
||||
- [\<nixpkgs/nixos/modules/services/frobnicator\.nix>](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/frobnicator.nix)
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import pytest
|
||||
|
||||
import nixos_render_docs as nrd
|
||||
|
||||
from sample_md import sample1
|
||||
@@ -6,9 +8,14 @@ from typing import Mapping
|
||||
|
||||
|
||||
class Converter(nrd.md.Converter[nrd.commonmark.CommonMarkRenderer]):
|
||||
def __init__(self, manpage_urls: Mapping[str, str]):
|
||||
def __init__(
|
||||
self,
|
||||
manpage_urls: Mapping[str, str],
|
||||
admonition_style: nrd.types.AdmonitionStyle = nrd.types.AdmonitionStyle.PLAIN,
|
||||
):
|
||||
super().__init__()
|
||||
self._renderer = nrd.commonmark.CommonMarkRenderer(manpage_urls)
|
||||
self._renderer = nrd.commonmark.CommonMarkRenderer(manpage_urls, admonition_style)
|
||||
|
||||
|
||||
# NOTE: in these tests we represent trailing spaces by ` ` and replace them with real space later,
|
||||
# since a number of editors will strip trailing whitespace on save and that would break the tests.
|
||||
@@ -24,6 +31,84 @@ def test_indented_fence() -> None:
|
||||
""".replace(' ', ' ')
|
||||
assert c._render(s) == s
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("style", "expected"),
|
||||
[
|
||||
(
|
||||
nrd.types.AdmonitionStyle.PLAIN,
|
||||
"""\
|
||||
**Warning:** foo
|
||||
|
||||
**Note:** nested
|
||||
|
||||
**Important:** nested GFM
|
||||
|
||||
:::
|
||||
|
||||
**Caution:** GFM caution
|
||||
|
||||
**Important:** nested fenced
|
||||
|
||||
**Note:** nested GFM\
|
||||
""",
|
||||
),
|
||||
(
|
||||
nrd.types.AdmonitionStyle.GFM,
|
||||
f"""\
|
||||
> [!Warning]
|
||||
> foo
|
||||
>{" "}
|
||||
> > [!Note]
|
||||
> > nested
|
||||
|
||||
> [!Important]
|
||||
> nested GFM
|
||||
|
||||
:::
|
||||
|
||||
> [!Caution]
|
||||
> GFM caution
|
||||
>{" "}
|
||||
> > [!Important]
|
||||
> > nested fenced
|
||||
>{" "}
|
||||
> > [!Note]
|
||||
> > nested GFM\
|
||||
""",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_admonition_styles(
|
||||
style: nrd.types.AdmonitionStyle,
|
||||
expected: str,
|
||||
) -> None:
|
||||
c = Converter({}, admonition_style=style)
|
||||
assert c._render("""\
|
||||
:::: {.warning}
|
||||
foo
|
||||
::: {.note}
|
||||
nested
|
||||
:::
|
||||
::::
|
||||
|
||||
> [!important]
|
||||
> nested GFM
|
||||
|
||||
:::
|
||||
|
||||
> [!caution]
|
||||
> GFM caution
|
||||
>
|
||||
> ::: {.important}
|
||||
> nested fenced
|
||||
> :::
|
||||
>
|
||||
> > [!note]
|
||||
> > nested GFM
|
||||
""") == expected
|
||||
|
||||
|
||||
def test_full() -> None:
|
||||
c = Converter({ 'man(1)': 'http://example.org' })
|
||||
assert c._render(sample1) == """\
|
||||
|
||||
@@ -39,3 +39,29 @@ def test_options_commonmark_legacy_anchors() -> None:
|
||||
c.add_options(opts)
|
||||
s = c.finalize()
|
||||
assert s == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("style", "expected_file"),
|
||||
[
|
||||
(nixos_render_docs.types.AdmonitionStyle.PLAIN,
|
||||
"tests/sample_options_admonition_plain.md"),
|
||||
(nixos_render_docs.types.AdmonitionStyle.GFM,
|
||||
"tests/sample_options_admonition_gfm.md"),
|
||||
],
|
||||
)
|
||||
def test_options_commonmark_admonition_style(style, expected_file):
|
||||
c = nixos_render_docs.options.CommonMarkConverter(
|
||||
{},
|
||||
"local",
|
||||
admonition_style=style,
|
||||
)
|
||||
|
||||
with Path("tests/sample_options_admonition.json").open() as f:
|
||||
opts = json.load(f)
|
||||
|
||||
with Path(expected_file).open() as f:
|
||||
expected = f.read()
|
||||
|
||||
c.add_options(opts)
|
||||
|
||||
assert c.finalize() == expected
|
||||
|
||||
Reference in New Issue
Block a user