feat(notion-writer): dialect->Notion enhanced markdown translator
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
121
custom-skills/32-notion-writer/code/scripts/md_translate.py
Normal file
121
custom-skills/32-notion-writer/code/scripts/md_translate.py
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Translate the notion-writer markdown dialect into Notion enhanced
|
||||
markdown for the markdown write engine. Pure functions, no I/O."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import List
|
||||
|
||||
# NOTE: `notion_writer` is imported lazily inside _translate_mentions to avoid
|
||||
# a circular import (notion_writer imports this module at its top level).
|
||||
|
||||
# Skill alert type -> (emoji, Notion enhanced-markdown background color)
|
||||
CALLOUT_MAP = {
|
||||
"NOTE": ("ℹ️", "blue_bg"),
|
||||
"TIP": ("💡", "green_bg"),
|
||||
"IMPORTANT": ("☝️", "purple_bg"),
|
||||
"WARNING": ("⚠️", "yellow_bg"),
|
||||
"CAUTION": ("🚨", "red_bg"),
|
||||
}
|
||||
_ALERT_RE = re.compile(r'^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*$')
|
||||
_MENTION_RE = re.compile(r'@\[([^\]]+)\]\(([^)\s]+)\)')
|
||||
|
||||
|
||||
def _translate_mentions(text: str) -> str:
|
||||
from notion_writer import extract_notion_id, format_id_with_dashes # lazy: breaks import cycle
|
||||
|
||||
def repl(m: "re.Match") -> str:
|
||||
title, target = m.group(1), m.group(2)
|
||||
page_id = extract_notion_id(target)
|
||||
if page_id:
|
||||
return (f'<mention-page url="{format_id_with_dashes(page_id)}">'
|
||||
f'{title}</mention-page>')
|
||||
return f"@{title}"
|
||||
return _MENTION_RE.sub(repl, text)
|
||||
|
||||
|
||||
def _indent(lines: List[str]) -> List[str]:
|
||||
return ["\t" + ln if ln.strip() else ln for ln in lines]
|
||||
|
||||
|
||||
def translate(content: str) -> str:
|
||||
lines = content.split("\n")
|
||||
out: List[str] = []
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
# Callout: > [!TYPE] then contiguous > body lines
|
||||
if line.lstrip().startswith(">"):
|
||||
body_first = line.lstrip()[1:].strip()
|
||||
alert = _ALERT_RE.match(body_first)
|
||||
if alert:
|
||||
emoji, color = CALLOUT_MAP[alert.group(1)]
|
||||
i += 1
|
||||
body: List[str] = []
|
||||
while i < len(lines) and lines[i].lstrip().startswith(">"):
|
||||
body.append(lines[i].lstrip()[1:].lstrip())
|
||||
i += 1
|
||||
out.append(f'<callout icon="{emoji}" color="{color}">')
|
||||
out.extend(_indent([_translate_mentions(b) for b in body]))
|
||||
out.append("</callout>")
|
||||
continue
|
||||
|
||||
# Columns: ::: columns / ::: column / :::
|
||||
# State machine: "::: column" opens a column; ":::" closes a column
|
||||
# when one is open, otherwise closes the wrapper. The Pandoc layout
|
||||
# emits N "::: column" opens, N ":::" column-closes, then one final
|
||||
# ":::" wrapper-close.
|
||||
if line.strip() == "::: columns":
|
||||
i += 1
|
||||
cols: List[List[str]] = []
|
||||
cur: List[str] = None
|
||||
while i < len(lines):
|
||||
s = lines[i].strip()
|
||||
if s == "::: column":
|
||||
if cur is not None:
|
||||
cols.append(cur)
|
||||
cur = []
|
||||
i += 1
|
||||
elif s == ":::":
|
||||
if cur is not None: # close the open column
|
||||
cols.append(cur)
|
||||
cur = None
|
||||
i += 1
|
||||
else: # close the wrapper
|
||||
i += 1
|
||||
break
|
||||
else:
|
||||
if cur is not None:
|
||||
cur.append(lines[i])
|
||||
i += 1
|
||||
out.append("<columns>")
|
||||
for col in cols:
|
||||
out.append("\t<column>")
|
||||
out.extend(_indent(_indent(
|
||||
[_translate_mentions(c) for c in col])))
|
||||
out.append("\t</column>")
|
||||
out.append("</columns>")
|
||||
continue
|
||||
|
||||
# Toggle: <details><summary>..</summary> body </details>
|
||||
if line.strip() == "<details>":
|
||||
out.append("<details>")
|
||||
i += 1
|
||||
if i < len(lines) and lines[i].lstrip().startswith("<summary>"):
|
||||
out.append(lines[i].strip())
|
||||
i += 1
|
||||
body = []
|
||||
while i < len(lines) and lines[i].strip() != "</details>":
|
||||
body.append(_translate_mentions(lines[i]))
|
||||
i += 1
|
||||
out.extend(_indent(body))
|
||||
out.append("</details>")
|
||||
if i < len(lines): # skip closing </details>
|
||||
i += 1
|
||||
continue
|
||||
|
||||
out.append(_translate_mentions(line))
|
||||
i += 1
|
||||
return "\n".join(out)
|
||||
Reference in New Issue
Block a user