122 lines
4.4 KiB
Python
122 lines
4.4 KiB
Python
#!/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)
|