feat: side-by-side columns shorthand (```columns fence)

Authors write a ```columns fenced block (columns separated by a line of
===); Pandoc passes the body through verbatim as <pre class="columns">,
and a new post-Pandoc transform in lib.wiki.expand_columns expands it
into a flex <div> of <poem> columns. Runs entirely Pi-side before the
MediaWiki API write — no wiki template or PHP extension required.

The transform lives in shared lib/wiki.py (called from markdown_to_wikitext),
so it is universal across all pipelines. Stdlib only; no new deps.

Docs: SCHEMA.md author contract + songs/root decision logs.
Also ignore __pycache__/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 21:01:35 +09:00
parent 736f40f953
commit 34a278870d
5 changed files with 64 additions and 5 deletions
+29 -1
View File
@@ -1,6 +1,8 @@
"""Shared utilities for novoyuuparosk-auto-wiki pipelines."""
import html
import os
import re
import subprocess
from urllib.parse import urlparse
@@ -8,6 +10,11 @@ import mwclient
AUTO_BANNER_PREFIX = "{{Auto-generated"
# A ```columns fenced block is passed through Pandoc verbatim as
# <pre class="columns">…</pre>; columns within it are separated by a line of ===.
_COLUMNS_BLOCK_RE = re.compile(r'<pre class="columns">(.*?)</pre>', re.DOTALL)
_COLUMN_SEP_RE = re.compile(r"^\s*===\s*$", re.MULTILINE)
def strip_first_h1(text: str) -> str:
"""Remove the first '# Heading' line and any immediately following blank line."""
@@ -21,6 +28,27 @@ def strip_first_h1(text: str) -> str:
return "\n".join(lines)
def expand_columns(wikitext: str) -> str:
"""Expand ```columns fenced blocks into a flex row of <poem> columns.
Authors write a fenced code block tagged ``columns``; Pandoc passes its body
through verbatim as ``<pre class="columns">…</pre>`` (line breaks and blank
lines preserved, inline markup entity-escaped). Columns within the block are
separated by a line containing only ``===``. Each column is wrapped in
<poem> so its line breaks survive MediaWiki parsing, and content is
HTML-unescaped so inline markup written in the fence (e.g. <b>…</b>) renders
rather than appearing as literal text.
"""
def render(match: re.Match) -> str:
body = html.unescape(match.group(1))
columns = _COLUMN_SEP_RE.split(body)
poems = "".join("<poem>\n" + col.strip("\n") + "\n</poem>" for col in columns)
return '<div style="display:flex; gap:3em; align-items:flex-start">' + poems + "</div>"
return _COLUMNS_BLOCK_RE.sub(render, wikitext)
def markdown_to_wikitext(body: str) -> str:
result = subprocess.run(
["pandoc", "-f", "markdown", "-t", "mediawiki"],
@@ -30,7 +58,7 @@ def markdown_to_wikitext(body: str) -> str:
)
if result.returncode != 0:
raise RuntimeError(f"pandoc failed: {result.stderr.strip()}")
return result.stdout
return expand_columns(result.stdout)
def connect_wiki() -> mwclient.Site: