98 lines
4.6 KiB
Python
98 lines
4.6 KiB
Python
#!/usr/bin/env python3
|
||
"""Convert an AISE502 module description (Markdown) into Confluence wiki markup.
|
||
|
||
python3 tools/md2confluence.py <in.md> <out.txt>
|
||
|
||
Handles the constructs these documents use: ATX headings, pipe tables, horizontal rules,
|
||
bullet and numbered lists (one nesting level), block quotes, and inline bold/italic/code.
|
||
Hard-wrapped lines are joined first, so a paragraph or list item becomes one line -- the form
|
||
Confluence expects. The "Inhalte" section's `**N. Title** *(part)*` blocks with their bullets
|
||
become a numbered list with sub-bullets, matching the earlier hand-written export.
|
||
"""
|
||
import re, sys, pathlib
|
||
|
||
def inline(t):
|
||
t = t.replace("--", "–")
|
||
t = re.sub(r"\*\*(.+?)\*\*", lambda m: "\x01" + m.group(1) + "\x01", t) # bold -> *x*
|
||
t = re.sub(r"(?<!\w)\*(.+?)\*(?!\w)", lambda m: "_" + m.group(1) + "_", t) # italic -> _x_
|
||
t = t.replace("\x01", "*")
|
||
t = re.sub(r"`([^`]+)`", lambda m: "{{" + m.group(1) + "}}", t)
|
||
return t
|
||
|
||
def unwrap(lines):
|
||
"""join hard-wrapped continuation lines into their paragraph / list item / table row"""
|
||
out = []
|
||
for raw in lines:
|
||
line = raw.rstrip("\n")
|
||
stripped = line.strip()
|
||
starts_block = (not stripped or line.startswith("#") or line.startswith("|")
|
||
or re.match(r"^\s*([-*+]|\d+\.)\s", line) or stripped.startswith(">")
|
||
or re.match(r"^-{3,}$", stripped))
|
||
if out and out[-1].strip() and not starts_block:
|
||
out[-1] = out[-1].rstrip() + " " + stripped
|
||
else:
|
||
out.append(line)
|
||
return out
|
||
|
||
def convert(md):
|
||
lines = unwrap(md.splitlines())
|
||
res, i, section, in_item = [], 0, "", False
|
||
while i < len(lines):
|
||
line = lines[i]; s = line.strip()
|
||
if not s:
|
||
res.append(""); i += 1; continue
|
||
if re.match(r"^-{3,}$", s):
|
||
res.append("----"); i += 1; continue
|
||
m = re.match(r"^(#{1,6})\s+(.*)$", s)
|
||
if m:
|
||
level, text = len(m.group(1)), inline(m.group(2))
|
||
if level == 3 and section.startswith("Inhalte"):
|
||
res.append(f"h4. {text}")
|
||
else:
|
||
res.append(f"h{min(level,4)}. {text}")
|
||
if level == 2: section = m.group(2); in_item = False
|
||
i += 1; continue
|
||
if s.startswith("|"): # table
|
||
rows = []
|
||
while i < len(lines) and lines[i].strip().startswith("|"):
|
||
rows.append([c.strip() for c in lines[i].strip().strip("|").split("|")]); i += 1
|
||
body = [r for r in rows if not all(re.fullmatch(r":?-{2,}:?", c or "-") for c in r)]
|
||
head = body[0] if body and any(body[0]) and len(body) > 1 else None
|
||
if head and rows.index(head) == 0 and any(c for c in head):
|
||
res.append("||" + "||".join(inline(c) for c in head) + "||")
|
||
body = body[1:]
|
||
for r in body:
|
||
res.append("|" + "|".join(inline(c) if c else " " for c in r) + "|")
|
||
continue
|
||
if s.startswith(">"):
|
||
res.append("{quote}"); res.append(inline(s.lstrip("> ").strip())); res.append("{quote}")
|
||
i += 1; continue
|
||
m = re.match(r"^(\s*)(\d+)\.\s+(.*)$", line) # numbered item
|
||
if m:
|
||
res.append("# " + inline(m.group(3))); in_item = True; i += 1; continue
|
||
m = re.match(r"^(\s*)[-*+]\s+(.*)$", line) # bullet
|
||
if m:
|
||
indent, text = len(m.group(1)), inline(m.group(2))
|
||
res.append(("#* " if (indent >= 2 or in_item) else "* ") + text)
|
||
i += 1; continue
|
||
m = re.match(r"^\*\*(\d+)\.\s+(.*)$", s) # "**7. Title** *(part)*"
|
||
if m and section.startswith("Inhalte"):
|
||
res.append("# " + inline("**" + m.group(2))) # the list supplies the number
|
||
in_item = True; i += 1; continue
|
||
res.append(inline(s)); in_item = False; i += 1
|
||
# Confluence restarts a numbered list after a blank line: drop blanks between list items
|
||
tight, item = [], re.compile(r"^(#\*?|\*)\s")
|
||
for j, line in enumerate(res):
|
||
if not line.strip() and tight and item.match(tight[-1]):
|
||
nxt = next((x for x in res[j + 1:] if x.strip()), "")
|
||
if item.match(nxt):
|
||
continue
|
||
tight.append(line)
|
||
text = "\n".join(tight)
|
||
return re.sub(r"\n{3,}", "\n\n", text).strip() + "\n"
|
||
|
||
if __name__ == "__main__":
|
||
src, dst = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])
|
||
dst.write_text(convert(src.read_text(encoding="utf-8")), encoding="utf-8")
|
||
print(f"wrote {dst} ({len(dst.read_text(encoding='utf-8').splitlines())} lines) from {src.name}")
|