nixos-render-docs: parse GFM-alerts as admonitions
Mostly useful for out-of-tree users of nixos-render-docs, but could also be useful if NixOS/Nixpkgs decides to transition to GFM's flavour of admonition syntax in the future. Both GFM-style and pandoc-fenced-div will now be tokenised as "admonitions".
This commit is contained in:
@@ -579,6 +579,58 @@ def _block_titles(block: str) -> Callable[[markdown_it.MarkdownIt], None]:
|
||||
|
||||
return do_add
|
||||
|
||||
|
||||
def _gfm_alerts(md: markdown_it.MarkdownIt) -> None:
|
||||
|
||||
_ALERT_PATTERN = re.compile(r"^\[\!(TIP|NOTE|IMPORTANT|WARNING|CAUTION)\][ \t]*(?:\n|$)", re.IGNORECASE)
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Entry:
|
||||
open: Token
|
||||
content: Token | None = None
|
||||
|
||||
"""
|
||||
Find blockquote tokens and convert GFM-alert-style blockquotes to admonition tokens.
|
||||
"""
|
||||
def gfm_alert(state: markdown_it.rules_core.StateCore) -> None:
|
||||
stack: list[Entry] = []
|
||||
size = len(state.tokens)
|
||||
|
||||
for i, token in enumerate(state.tokens):
|
||||
match token.type:
|
||||
case "blockquote_open":
|
||||
entry = Entry(token)
|
||||
# Get the first inline token of the blockquote's first paragraph
|
||||
if i + 2 < size:
|
||||
para = state.tokens[i + 1]
|
||||
inline = state.tokens[i + 2]
|
||||
if para and para.type == "paragraph_open" and inline and inline.type == "inline":
|
||||
entry.content = inline
|
||||
stack.append(entry)
|
||||
|
||||
case "blockquote_close":
|
||||
entry = stack.pop()
|
||||
|
||||
if entry.content is None:
|
||||
continue
|
||||
|
||||
m = _ALERT_PATTERN.match(entry.content.content)
|
||||
if m is None:
|
||||
continue
|
||||
|
||||
# Remove the alert marker from the rendered text.
|
||||
entry.content.content = entry.content.content[m.end() :]
|
||||
|
||||
# Rewrite the enclosing blockquote as an admonition.
|
||||
entry.open.type = "admonition_open"
|
||||
entry.open.tag = "div"
|
||||
entry.open.meta["kind"] = m.group(1).lower()
|
||||
token.type = "admonition_close"
|
||||
token.tag = "div"
|
||||
|
||||
md.core.ruler.after("block", "github-alerts", gfm_alert)
|
||||
|
||||
|
||||
TR = TypeVar('TR', bound='Renderer')
|
||||
|
||||
class Converter(ABC, Generic[TR]):
|
||||
@@ -626,6 +678,7 @@ class Converter(ABC, Generic[TR]):
|
||||
self._md.use(_block_attr)
|
||||
self._md.use(_block_titles("example"))
|
||||
self._md.use(_block_titles("figure"))
|
||||
self._md.use(_gfm_alerts)
|
||||
self._md.enable(["smartquotes", "replacements"])
|
||||
|
||||
def _parse(self, src: str) -> list[Token]:
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
sample1 = """\
|
||||
> [!NOTE]
|
||||
> This is a *GFM* note.
|
||||
>
|
||||
> > [!caution]
|
||||
> > This is a **nested** GFM alert.
|
||||
|
||||
:::: {.warning}
|
||||
foo
|
||||
::: {.note}
|
||||
|
||||
@@ -45,6 +45,18 @@ f
|
||||
def test_full() -> None:
|
||||
c = Converter({ 'man(1)': 'http://example.org' })
|
||||
assert c._render(sample1) == """\
|
||||
[NOTE]
|
||||
====
|
||||
This is a __GFM__ note{zwsp}.
|
||||
|
||||
[CAUTION]
|
||||
=====
|
||||
This is a **nested** GFM alert{zwsp}.
|
||||
=====
|
||||
|
||||
====
|
||||
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
foo
|
||||
|
||||
@@ -27,6 +27,10 @@ def test_indented_fence() -> None:
|
||||
def test_full() -> None:
|
||||
c = Converter({ 'man(1)': 'http://example.org' })
|
||||
assert c._render(sample1) == """\
|
||||
**Note:** This is a *GFM* note\\.
|
||||
|
||||
**Caution:** This is a **nested** GFM alert\\.
|
||||
|
||||
**Warning:** foo
|
||||
|
||||
**Note:** nested
|
||||
|
||||
@@ -149,6 +149,14 @@ def test_footnotes() -> None:
|
||||
def test_full() -> None:
|
||||
c = Converter({ 'man(1)': 'http://example.org' }, {})
|
||||
assert c._render(sample1) == unpretty("""
|
||||
<div class="note">
|
||||
<h3 class="title">Note</h3>
|
||||
<p>This is a <span class="emphasis"><em>GFM</em></span> note.</p>
|
||||
<div class="caution">
|
||||
<h3 class="title">Caution</h3>
|
||||
<p>This is a <span class="strong"><strong>nested</strong></span> GFM alert.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="warning">
|
||||
<h3 class="title">Warning</h3>
|
||||
<p>foo</p>
|
||||
|
||||
@@ -41,6 +41,18 @@ def test_full() -> None:
|
||||
assert c._render(sample1) == """\
|
||||
.sp
|
||||
.RS 4
|
||||
\\fBNote\\fP
|
||||
.br
|
||||
This is a \\fIGFM\\fR note\\&.
|
||||
.sp
|
||||
.RS 4
|
||||
\\fBCaution\\fP
|
||||
.br
|
||||
This is a \\fBnested\\fR GFM alert\\&.
|
||||
.RE
|
||||
.RE
|
||||
.sp
|
||||
.RS 4
|
||||
\\fBWarning\\fP
|
||||
.br
|
||||
foo
|
||||
|
||||
@@ -429,6 +429,90 @@ def test_admonitions() -> None:
|
||||
children=None, content='', markup=':::', info='', meta={}, block=True, hidden=False)
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("alert", "kind"),
|
||||
[
|
||||
("NOTE", "note"),
|
||||
("TIP", "tip"),
|
||||
("IMPORTANT", "important"),
|
||||
("WARNING", "warning"),
|
||||
("CAUTION", "caution"),
|
||||
],
|
||||
)
|
||||
def test_gfm_alert_kinds(alert: str, kind: str) -> None:
|
||||
c = Converter({})
|
||||
assert c._parse(f"> [!{alert}]\n> body") == [
|
||||
Token(type='admonition_open', tag='div', markup='>', meta={'kind': kind}, nesting=1, map=[0, 2], block=True),
|
||||
Token(type='paragraph_open', tag='p', nesting=1, map=[0, 2], level=1, block=True),
|
||||
Token(type='inline', tag='', nesting=0, map=[0, 2], level=2, block=True, content='body', children=[
|
||||
Token(type='text', tag='', nesting=0, content='body'),
|
||||
]),
|
||||
Token(type='paragraph_close', tag='p', nesting=-1, level=1, block=True),
|
||||
Token(type='admonition_close', tag='div', nesting=-1, markup='>', block=True)
|
||||
]
|
||||
|
||||
def test_gfm_alert_basic_structure_preserved() -> None:
|
||||
c = Converter({})
|
||||
tokens = c._parse("> [!NOTE]\n> body text")
|
||||
assert any(t.type == "inline" for t in tokens)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"md",
|
||||
[
|
||||
"> # [!NOTE]",
|
||||
"> - [!NOTE]",
|
||||
"> - [!WARNING] body",
|
||||
"> ### [!TIP]",
|
||||
"> ```\n> [!NOTE]\n> ```",
|
||||
"> This is not [!NOTE] an alert",
|
||||
"> [!NOTE] with trailing text is ignored",
|
||||
],
|
||||
)
|
||||
def test_gfm_alert_rejected(md: str) -> None:
|
||||
c = Converter({})
|
||||
tokens = c._parse(md)
|
||||
assert all(t.type != "admonition_open" for t in tokens)
|
||||
assert tokens[0].type == "blockquote_open"
|
||||
assert tokens[-1].type == "blockquote_close"
|
||||
|
||||
def test_gfm_alert_multi_paragraph() -> None:
|
||||
c = Converter({})
|
||||
assert c._parse(
|
||||
"> [!NOTE]\n"
|
||||
"> line 1\n"
|
||||
">\n"
|
||||
"> line 2"
|
||||
) == [
|
||||
Token(type='admonition_open', tag='div', markup='>', meta={'kind': 'note'}, nesting=1, map=[0, 4], block=True),
|
||||
Token(type='paragraph_open', tag='p', nesting=1, map=[0, 2], level=1, block=True),
|
||||
Token(type='inline', tag='', nesting=0, map=[0, 2], level=2, block=True, content='line 1', children=[
|
||||
Token(type='text', tag='', nesting=0, content='line 1'),
|
||||
]),
|
||||
Token(type='paragraph_close', tag='p', nesting=-1, level=1, block=True),
|
||||
Token(type='paragraph_open', tag='p', nesting=1, map=[3, 4], level=1, block=True),
|
||||
Token(type='inline', tag='', nesting=0, map=[3, 4], level=2, block=True, content='line 2', children=[
|
||||
Token(type='text', tag='', nesting=0, content='line 2')
|
||||
]),
|
||||
Token(type='paragraph_close', tag='p', nesting=-1, level=1, block=True),
|
||||
Token(type='admonition_close', tag='div', markup='>', block=True, nesting=-1),
|
||||
]
|
||||
|
||||
def test_gfm_alert_whitespace_tolerant() -> None:
|
||||
c = Converter({})
|
||||
tokens = c._parse("> [!NOTE] \n> body")
|
||||
assert tokens[0].type == "admonition_open"
|
||||
assert tokens[0].meta["kind"] == "note"
|
||||
|
||||
def test_gfm_alert_empty_body_allowed() -> None:
|
||||
c = Converter({})
|
||||
assert c._parse("> [!NOTE]\n>") == [
|
||||
Token(type='admonition_open', tag='div', markup='>', meta={'kind': 'note'}, nesting=1, map=[0, 2], block=True),
|
||||
Token(type='paragraph_open', tag='p', nesting=1, map=[0, 1], level=1, block=True),
|
||||
Token(type='inline', tag='', nesting=0, map=[0, 1], level=2, block=True, children=[]),
|
||||
Token(type='paragraph_close', tag='p', nesting=-1, level=1, block=True),
|
||||
Token(type='admonition_close', tag='div', markup='>', nesting=-1, block=True),
|
||||
]
|
||||
|
||||
def test_example() -> None:
|
||||
c = Converter({})
|
||||
assert c._parse("::: {.example}\n# foo") == [
|
||||
|
||||
Reference in New Issue
Block a user