+249
@@ -0,0 +1,249 @@
|
||||
"""Emit md2gost dialect Markdown from IR blocks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .blocks import (
|
||||
Block,
|
||||
CaptionBlock,
|
||||
EquationBlock,
|
||||
HeadingBlock,
|
||||
ImageBlock,
|
||||
InlineSpan,
|
||||
ListBlock,
|
||||
ListingBlock,
|
||||
PageBreakBlock,
|
||||
ParagraphBlock,
|
||||
TableBlock,
|
||||
TocPlaceholder,
|
||||
)
|
||||
|
||||
# Characters that must stay literal after DOCX→MD (Word stores bare '*', not '\*').
|
||||
# Do not escape '_': md2gost labels use it (@Рисунок:fig1_1).
|
||||
_MD_SPECIAL = str.maketrans(
|
||||
{
|
||||
"\\": "\\\\",
|
||||
"*": "\\*",
|
||||
"`": "\\`",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def escape_md_inline(text: str) -> str:
|
||||
"""Escape Markdown inline metacharacters in plain text from DOCX."""
|
||||
if not text:
|
||||
return ""
|
||||
return text.translate(_MD_SPECIAL)
|
||||
|
||||
|
||||
def _escape_md_cell(text: str) -> str:
|
||||
return escape_md_inline(text or "").replace("|", "\\|").replace("\n", " ")
|
||||
|
||||
|
||||
def _render_spans(spans: list[InlineSpan]) -> str:
|
||||
parts: list[str] = []
|
||||
for s in spans:
|
||||
if s.math:
|
||||
parts.append(f"${s.math}$")
|
||||
continue
|
||||
text = escape_md_inline(s.text or "")
|
||||
if s.href:
|
||||
parts.append(f"[{text}]({s.href})")
|
||||
continue
|
||||
if s.bold and s.italic:
|
||||
text = f"***{text}***"
|
||||
elif s.bold:
|
||||
text = f"**{text}**"
|
||||
elif s.italic:
|
||||
text = f"*{text}*"
|
||||
if s.strike:
|
||||
text = f"~~{text}~~"
|
||||
parts.append(text)
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
def _emit_caption_line(unique_id: str, text: str | None = None, *, landscape: bool = False) -> str:
|
||||
title = escape_md_inline((text or "").strip())
|
||||
flags = " +landscape" if landscape else ""
|
||||
if title:
|
||||
return f"%{unique_id} {title}{flags}".rstrip()
|
||||
return f"%{unique_id}{flags}".rstrip()
|
||||
|
||||
|
||||
def _last_nonempty(chunks: list[str]) -> str:
|
||||
for prev in reversed(chunks):
|
||||
if prev.strip():
|
||||
return prev
|
||||
return ""
|
||||
|
||||
|
||||
def _emit_table(tbl: TableBlock) -> str:
|
||||
if not tbl.rows:
|
||||
return ""
|
||||
lines: list[str] = []
|
||||
for r, row in enumerate(tbl.rows):
|
||||
cells = []
|
||||
for c, cell in enumerate(row):
|
||||
if tbl.merges and r < len(tbl.merges) and c < len(tbl.merges[r]):
|
||||
v, h = tbl.merges[r][c]
|
||||
if h == "continue":
|
||||
cells.append(">")
|
||||
continue
|
||||
if v == "continue":
|
||||
cells.append("^")
|
||||
continue
|
||||
cells.append(_escape_md_cell(cell if cell not in ("^", ">") else cell))
|
||||
lines.append("| " + " | ".join(cells) + " |")
|
||||
if r == 0:
|
||||
lines.append("| " + " | ".join("---" for _ in cells) + " |")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def emit_markdown(blocks: list[Block], *, emit_page_breaks: bool = False) -> str:
|
||||
chunks: list[str] = []
|
||||
i = 0
|
||||
while i < len(blocks):
|
||||
b = blocks[i]
|
||||
|
||||
if isinstance(b, HeadingBlock):
|
||||
hashes = "#" * max(1, min(b.level, 6))
|
||||
title = escape_md_inline(b.text or "")
|
||||
if not b.numbered:
|
||||
# Dialect flag '*' after '# ' is structural — not escaped.
|
||||
chunks.append(f"{hashes} *{title}")
|
||||
else:
|
||||
chunks.append(f"{hashes} {title}")
|
||||
chunks.append("")
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if isinstance(b, TocPlaceholder):
|
||||
chunks.append("[TOC]")
|
||||
chunks.append("")
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if isinstance(b, CaptionBlock):
|
||||
if b.is_continuation:
|
||||
i += 1
|
||||
continue
|
||||
nxt = blocks[i + 1] if i + 1 < len(blocks) else None
|
||||
if isinstance(nxt, ImageBlock) and b.kind == "figure":
|
||||
nxt.caption_id = nxt.caption_id or b.unique_id
|
||||
nxt.caption_text = nxt.caption_text or b.text
|
||||
i += 1
|
||||
continue
|
||||
if isinstance(nxt, TableBlock) and b.kind == "table":
|
||||
nxt.caption_id = nxt.caption_id or b.unique_id
|
||||
nxt.caption_text = nxt.caption_text or b.text
|
||||
if b.landscape:
|
||||
nxt.landscape = True
|
||||
chunks.append(_emit_caption_line(
|
||||
nxt.caption_id or b.unique_id,
|
||||
nxt.caption_text or b.text,
|
||||
landscape=nxt.landscape,
|
||||
))
|
||||
chunks.append("")
|
||||
i += 1
|
||||
continue
|
||||
if isinstance(nxt, ListingBlock) and b.kind == "listing":
|
||||
nxt.caption_id = nxt.caption_id or b.unique_id
|
||||
nxt.caption_text = nxt.caption_text or b.text
|
||||
chunks.append(_emit_caption_line(nxt.caption_id or b.unique_id, nxt.caption_text))
|
||||
chunks.append("")
|
||||
i += 1
|
||||
continue
|
||||
chunks.append(_emit_caption_line(b.unique_id, b.text, landscape=b.landscape))
|
||||
chunks.append("")
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if isinstance(b, ImageBlock):
|
||||
# Safety: caption still after image in IR
|
||||
nxt = blocks[i + 1] if i + 1 < len(blocks) else None
|
||||
if (
|
||||
isinstance(nxt, CaptionBlock)
|
||||
and nxt.kind == "figure"
|
||||
and not nxt.is_continuation
|
||||
):
|
||||
b.caption_id = b.caption_id or nxt.unique_id
|
||||
b.caption_text = b.caption_text or nxt.text
|
||||
# skip the caption block
|
||||
i += 1
|
||||
title = ""
|
||||
if b.caption_id:
|
||||
title_body = escape_md_inline((b.caption_text or "").strip())
|
||||
title = f" \"%{b.caption_id}" + (f" {title_body}" if title_body else "") + "\""
|
||||
alt = escape_md_inline((b.alt or "рисунок").replace('"', "'"))
|
||||
chunks.append(f"")
|
||||
chunks.append("")
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if isinstance(b, TableBlock):
|
||||
if b.caption_id:
|
||||
prev = _last_nonempty(chunks)
|
||||
if not prev.startswith(f"%{b.caption_id}"):
|
||||
chunks.append(_emit_caption_line(b.caption_id, b.caption_text, landscape=b.landscape))
|
||||
chunks.append("")
|
||||
chunks.append(_emit_table(b))
|
||||
chunks.append("")
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if isinstance(b, ListingBlock):
|
||||
if b.caption_id:
|
||||
prev = _last_nonempty(chunks)
|
||||
if not prev.startswith(f"%{b.caption_id}"):
|
||||
chunks.append(_emit_caption_line(b.caption_id, b.caption_text))
|
||||
chunks.append("")
|
||||
lang = b.language or ""
|
||||
body = "\n".join(line.rstrip("\n") for line in b.lines)
|
||||
chunks.append(f"```{lang}")
|
||||
chunks.append(body)
|
||||
chunks.append("```")
|
||||
chunks.append("")
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if isinstance(b, EquationBlock):
|
||||
if b.unique_id:
|
||||
chunks.append(f"%{b.unique_id}")
|
||||
chunks.append("$$")
|
||||
chunks.append(b.latex.strip())
|
||||
chunks.append("$$")
|
||||
chunks.append("")
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if isinstance(b, ListBlock):
|
||||
for idx, item in enumerate(b.items, start=1):
|
||||
body = escape_md_inline(item)
|
||||
if b.ordered:
|
||||
chunks.append(f"{idx}. {body}")
|
||||
else:
|
||||
chunks.append(f"- {body}")
|
||||
chunks.append("")
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if isinstance(b, ParagraphBlock):
|
||||
line = _render_spans(b.spans).strip()
|
||||
if line:
|
||||
chunks.append(line)
|
||||
chunks.append("")
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if isinstance(b, PageBreakBlock):
|
||||
if emit_page_breaks:
|
||||
chunks.append("---")
|
||||
chunks.append("")
|
||||
i += 1
|
||||
continue
|
||||
|
||||
i += 1
|
||||
|
||||
text = "\n".join(chunks)
|
||||
while "\n\n\n" in text:
|
||||
text = text.replace("\n\n\n", "\n\n")
|
||||
return text.strip() + "\n"
|
||||
Reference in New Issue
Block a user