diff --git a/marko/ext/gfm/__init__.py b/marko/ext/gfm/__init__.py index a8eb648..c3f3882 100644 --- a/marko/ext/gfm/__init__.py +++ b/marko/ext/gfm/__init__.py @@ -28,6 +28,7 @@ elements.Table, elements.TableRow, elements.TableCell, + elements.Alert, ], renderer_mixins=[renderer.GFMRendererMixin], ) diff --git a/marko/ext/gfm/elements.py b/marko/ext/gfm/elements.py index e5900f3..fa98289 100644 --- a/marko/ext/gfm/elements.py +++ b/marko/ext/gfm/elements.py @@ -216,3 +216,26 @@ def __init__(self, text: str) -> None: self.inline_body = text.strip().replace("\\|", "|") self.header = False self.align: str | None = None + + +class Alert(block.Quote): + """Alert block element: block quote with a header like WARNING, NOTE, TIP, IMPORTANT, or CAUTION.""" + + priority = block.Quote.priority + 1 + + @classmethod + def match(cls, source): + return source.expect_re(r" {,3}>\s*\[\!(WARNING|NOTE|TIP|IMPORTANT|CAUTION)\]") + + @classmethod + def parse(cls, source): + alert_type = source.match.group(1) + source.next_line(require_prefix=False) + source.consume() + state = cls(alert_type) + with source.under_state(state): + state.children = source.parser.parse_source(source) + return state + + def __init__(self, alert_type): + self.alert_type = alert_type diff --git a/marko/ext/gfm/renderer.py b/marko/ext/gfm/renderer.py index 13ced0e..a3da940 100644 --- a/marko/ext/gfm/renderer.py +++ b/marko/ext/gfm/renderer.py @@ -1,4 +1,6 @@ # mypy: disable-error-code="no-redef" +from __future__ import annotations + import re from marko.helpers import render_dispatch @@ -104,3 +106,22 @@ def render_url(self, element): @render_url.dispatch(MarkdownRenderer) def render_url(self, element): return element.dest + + @render_dispatch(HTMLRenderer) + def render_alert(self, element): + header = self.escape_html(element.alert_type) + children = self.render_children(element) + return ( + f'
\n' + f"\n" + ) + + @render_alert.dispatch(MarkdownRenderer) + def render_alert(self, element): + lines: list[str] = [] + lines.append(self._prefix + f"> [!{element.alert_type}]\n") + with self.container("> ", "> "): + for child in element.children: + lines.append(self.render(child)) + self._prefix = self._second_prefix + return "".join(lines) diff --git a/tests/test_basic.py b/tests/test_basic.py index fc353d4..58c6508 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -148,6 +148,9 @@ def test_gfm_markdown_renderer(self): | Apple | $1 | | Orange | $2 | + > [!CAUTION] + > Upcoming bugs + [^1]: go to https://example.com """ ) diff --git a/tests/test_ext.py b/tests/test_ext.py index e790d70..d20e2db 100644 --- a/tests/test_ext.py +++ b/tests/test_ext.py @@ -95,3 +95,31 @@ def test_codehilite_options(self): def test_render_code_block_with_extra(self): content = '```python filename="test.py"\nprint("hello")\n```' assert 'test.py' in self.markdown(content) + + +class TestGFMAlert: + def setup_method(self): + from marko import Markdown + from marko.ast_renderer import ASTRenderer + from marko.ext.gfm import GFM + + self.md_ast = Markdown(renderer=ASTRenderer, extensions=[GFM]) + self.md_html = Markdown(extensions=[GFM]) + + def test_alert_ast(self): + text = "> [!WARNING]\n> Foo bar\n> Bar\n" + ast = self.md_ast(text) + admon = ast["children"][0] + assert admon["element"] == "alert" + assert admon["alert_type"] == "WARNING" + inner = admon["children"][0]["children"] + assert inner[0]["children"] == "Foo bar" + assert inner[1]["element"] == "line_break" + assert inner[2]["children"] == "Bar" + + def test_alert_html(self): + text = "> [!WARNING]\n> Foo bar\n> Bar\n" + html = self.md_html(text) + assert '{header.title()}
\n{children}
' in html + assert "Warning
" in html + assert "Foo bar\nBar
" in html