From 7ffb20780506987d67303ec31d7a09a0fffa7812 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 16 Aug 2026 00:11:27 +0200 Subject: [PATCH 1/2] feat: add support for github alert-syntax --- compendium/compendium/page/docs/docs.css | 35 +++++++++ compendium/docs.py | 71 ++++++++++++++++++- .../docs/de/compendium/docs/authoring.md | 28 ++++++++ .../docs/en/compendium/docs/authoring.md | 27 +++++++ compendium/tests/test_docs.py | 47 ++++++++++++ 5 files changed, 207 insertions(+), 1 deletion(-) diff --git a/compendium/compendium/page/docs/docs.css b/compendium/compendium/page/docs/docs.css index e6cdc7a..1283a9d 100644 --- a/compendium/compendium/page/docs/docs.css +++ b/compendium/compendium/page/docs/docs.css @@ -326,6 +326,41 @@ margin-bottom: 0; } +.docs-reading-pane .docs-alert-title { + display: flex; + align-items: center; + gap: 0.4em; + margin: 0 0 0.4em; + font-weight: 600; + line-height: 1.2; + color: var(--docs-alert-color, var(--text-color)); +} + +.docs-reading-pane blockquote.docs-alert-note { + --docs-alert-color: var(--alert-text-info); + border-left-color: var(--docs-alert-color); +} + +.docs-reading-pane blockquote.docs-alert-tip { + --docs-alert-color: var(--alert-text-success); + border-left-color: var(--docs-alert-color); +} + +.docs-reading-pane blockquote.docs-alert-important { + --docs-alert-color: var(--purple-500); + border-left-color: var(--docs-alert-color); +} + +.docs-reading-pane blockquote.docs-alert-warning { + --docs-alert-color: var(--alert-text-warning); + border-left-color: var(--docs-alert-color); +} + +.docs-reading-pane blockquote.docs-alert-caution { + --docs-alert-color: var(--alert-text-danger); + border-left-color: var(--docs-alert-color); +} + .docs-state { display: flex; align-items: center; diff --git a/compendium/docs.py b/compendium/docs.py index 5ac69db..a675b79 100644 --- a/compendium/docs.py +++ b/compendium/docs.py @@ -23,6 +23,14 @@ DEFAULT_ROLE = "Desk User" ALLOWED_FRONTMATTER_KEYS = ("title", "order", "roles") IMAGE_SRC_PATTERN = re.compile(r'(]+src=["\'])([^"\']+)(["\'])', re.IGNORECASE) +GITHUB_ALERT_MARKER = re.compile(r"^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][ \t]*", re.IGNORECASE) +GITHUB_ALERTS = { + "NOTE": ("Note", "info"), + "TIP": ("Tip", "light-bulb"), + "IMPORTANT": ("Important", "megaphone"), + "WARNING": ("Warning", "alert"), + "CAUTION": ("Caution", "stop"), +} @frappe.whitelist() @@ -796,10 +804,71 @@ def sort_tree_nodes(nodes): def render_page_content(body, page_path="", locale=None): html = frappe.utils.md_to_html(body or "") - content = sanitize_html(str(html), linkify=True) + content = apply_github_alerts(str(html)) + content = sanitize_html(content, linkify=True) return rewrite_asset_urls(content, page_path, locale) +def apply_github_alerts(html): + """Turn GitHub alert markers into a titled, typed blockquote.""" + from bs4 import BeautifulSoup, NavigableString + + if not html or "[!" not in html: + return html + + soup = BeautifulSoup(html, "html.parser") + for blockquote in soup.find_all("blockquote"): + paragraph = next((child for child in blockquote.children if child.name), None) + if not paragraph or paragraph.name != "p": + continue + + text_node = next( + ( + child + for child in paragraph.children + if isinstance(child, NavigableString) and str(child).strip() + ), + None, + ) + if text_node is None: + continue + + leading = str(text_node).lstrip() + match = GITHUB_ALERT_MARKER.match(leading) + if not match: + continue + + alert_type = match.group(1).upper() + remainder = leading[match.end() :].lstrip() + blockquote["class"] = [ + *(blockquote.get("class") or []), + "docs-alert", + f"docs-alert-{alert_type.lower()}", + ] + title = build_alert_title(soup, alert_type) + + if remainder: + text_node.replace_with(remainder) + paragraph.insert_before(title) + else: + text_node.extract() + if paragraph.get_text(strip=True) or paragraph.find(True): + paragraph.insert_before(title) + else: + paragraph.replace_with(title) + + return str(soup) + + +def build_alert_title(soup, alert_type): + label, icon = GITHUB_ALERTS[alert_type] + title = soup.new_tag("p", attrs={"class": "docs-alert-title"}) + icon_tag = soup.new_tag("i", attrs={"class": f"octicon octicon-{icon}"}) + title.append(icon_tag) + title.append(label) + return title + + def rewrite_asset_urls(html, page_path, locale=None): def replace(match): prefix, src, suffix = match.groups() diff --git a/compendium/docs/de/compendium/docs/authoring.md b/compendium/docs/de/compendium/docs/authoring.md index 0f17c63..03f7b3d 100644 --- a/compendium/docs/de/compendium/docs/authoring.md +++ b/compendium/docs/de/compendium/docs/authoring.md @@ -110,6 +110,34 @@ def hello(): print("Hello") ``` +## Alerts + +Die GitHub-Alert-Syntax macht aus einem Blockzitat einen Hinweis. Die Zeile +`[!TYPE]` wird durch ein Symbol und eine fette Überschrift ersetzt. Fünf Typen +stehen zur Verfügung: + +> [!NOTE] +> Zusatzinformation, die auch beim Überfliegen nützlich ist. + +> [!TIP] +> Ein Rat, der eine Aufgabe leichter macht. + +> [!IMPORTANT] +> Information, die der Leser braucht, um sein Ziel zu erreichen. + +> [!WARNING] +> Dringende Information. Handeln Sie, um ein Problem zu vermeiden. + +> [!CAUTION] +> Ein Risiko oder eine negative Folge einer Handlung. + +Schreiben Sie sie als Blockzitat, dessen erste Zeile der Typmarker ist: + +```md +> [!NOTE] +> Zusatzinformation, die auch beim Überfliegen nützlich ist. +``` + ## Beispielseite ```md diff --git a/compendium/docs/en/compendium/docs/authoring.md b/compendium/docs/en/compendium/docs/authoring.md index 8a799ea..57d605f 100644 --- a/compendium/docs/en/compendium/docs/authoring.md +++ b/compendium/docs/en/compendium/docs/authoring.md @@ -106,6 +106,33 @@ def hello(): print("Hello") ``` +## Alerts + +GitHub alert syntax turns a blockquote into a callout. The `[!TYPE]` line +becomes an icon and a bold heading. Five types are available: + +> [!NOTE] +> Extra information that is useful even when skimming. + +> [!TIP] +> Advice that makes a task easier. + +> [!IMPORTANT] +> Information the reader needs to reach their goal. + +> [!WARNING] +> Urgent information. Act on it to avoid a problem. + +> [!CAUTION] +> A risk or a negative outcome of an action. + +Write them as a blockquote whose first line is the type marker: + +```md +> [!NOTE] +> Extra information that is useful even when skimming. +``` + ## Example page ```md diff --git a/compendium/tests/test_docs.py b/compendium/tests/test_docs.py index 9e6e668..65e5ae4 100644 --- a/compendium/tests/test_docs.py +++ b/compendium/tests/test_docs.py @@ -156,6 +156,53 @@ def test_mermaid_fence_keeps_language_class(self): self.assertIn("language-mermaid", html) self.assertIn("flowchart LR", html) + def test_github_alert_replaces_marker_with_title(self): + html = render_page_content("> [!NOTE]\n> Useful information") + self.assertIn("docs-alert-note", html) + self.assertIn("octicon-info", html) + self.assertIn("Note", html) + self.assertIn("Useful information", html) + self.assertNotIn("[!NOTE]", html) + + def test_github_alert_types(self): + cases = { + "TIP": ("docs-alert-tip", "octicon-light-bulb", "Tip"), + "IMPORTANT": ("docs-alert-important", "octicon-megaphone", "Important"), + "WARNING": ("docs-alert-warning", "octicon-alert", "Warning"), + "CAUTION": ("docs-alert-caution", "octicon-stop", "Caution"), + } + for marker, (css_class, icon, heading) in cases.items(): + html = render_page_content(f"> [!{marker}]\n> Body") + self.assertIn(css_class, html) + self.assertIn(icon, html) + self.assertIn(heading, html) + self.assertIn("Body", html) + self.assertNotIn(f"[!{marker}]", html) + + def test_github_alert_with_following_list(self): + html = render_page_content("> [!NOTE]\n>\n> - first\n> - second") + self.assertIn("docs-alert-note", html) + self.assertIn("octicon-info", html) + self.assertNotIn("[!NOTE]", html) + self.assertIn("
  • ", html) + + def test_unknown_alert_marker_stays_blockquote(self): + html = render_page_content("> [!UNKNOWN]\n> Body") + self.assertNotIn("docs-alert", html) + self.assertIn("[!UNKNOWN]", html) + self.assertIn("Body", html) + + def test_plain_blockquote_is_unchanged(self): + html = render_page_content("> just a quote") + self.assertNotIn("docs-alert", html) + self.assertNotIn("octicon", html) + self.assertIn("just a quote", html) + + def test_github_alert_type_is_case_insensitive(self): + html = render_page_content("> [!note]\n> Useful") + self.assertIn("docs-alert-note", html) + self.assertNotIn("[!note]", html) + def test_protected_assets_require_page_access(self): with self.docs_environment( { From 06b6c7b0490f7fda15cd2ede7c3564539a1dfae0 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 16 Aug 2026 01:14:21 +0200 Subject: [PATCH 2/2] fix: require GitHub alert markers at the start of a blockquote --- compendium/docs.py | 4 ++-- compendium/tests/test_docs.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/compendium/docs.py b/compendium/docs.py index a675b79..42574ec 100644 --- a/compendium/docs.py +++ b/compendium/docs.py @@ -826,11 +826,11 @@ def apply_github_alerts(html): ( child for child in paragraph.children - if isinstance(child, NavigableString) and str(child).strip() + if not isinstance(child, NavigableString) or str(child).strip() ), None, ) - if text_node is None: + if not isinstance(text_node, NavigableString): continue leading = str(text_node).lstrip() diff --git a/compendium/tests/test_docs.py b/compendium/tests/test_docs.py index 65e5ae4..08ab8f5 100644 --- a/compendium/tests/test_docs.py +++ b/compendium/tests/test_docs.py @@ -203,6 +203,12 @@ def test_github_alert_type_is_case_insensitive(self): self.assertIn("docs-alert-note", html) self.assertNotIn("[!note]", html) + def test_github_alert_ignores_marker_after_inline_markup(self): + html = render_page_content("> **See** [!NOTE]\n> not an alert") + self.assertNotIn("docs-alert", html) + self.assertIn("[!NOTE]", html) + self.assertIn("See", html) + def test_protected_assets_require_page_access(self): with self.docs_environment( {