import json import re import sys INLINE_CODE = re.compile(r"`([^`]+)`") BOLD = re.compile(r"\*\*([^*]+)\*\*") def text_node(text, marks=None): node = {"type": "text", "text": text} if marks: node["marks"] = [{"type": m} for m in marks] return node def inline_nodes(text): nodes = [] pos = 0 pattern = re.compile(r"`([^`]+)`|\*\*([^*]+)\*\*") for m in pattern.finditer(text): if m.start() > pos: nodes.append(text_node(text[pos:m.start()])) if m.group(1) is not None: nodes.append(text_node(m.group(1), ["code"])) else: nodes.append(text_node(m.group(2), ["strong"])) pos = m.end() if pos < len(text): nodes.append(text_node(text[pos:])) return nodes or [text_node("")] def paragraph(text): return {"type": "paragraph", "content": inline_nodes(text)} def heading(level, text): return {"type": "heading", "attrs": {"level": level}, "content": inline_nodes(text)} def code_block(lang, lines): attrs = {"language": lang} if lang else {} return {"type": "codeBlock", "attrs": attrs, "content": [text_node("\n".join(lines))]} def list_node(items, ordered): return { "type": "orderedList" if ordered else "bulletList", "content": [{"type": "listItem", "content": [paragraph(i)]} for i in items], } def cell(kind, text): return {"type": kind, "attrs": {}, "content": [paragraph(text)]} def table(rows): out = {"type": "table", "attrs": {"layout": "default"}, "content": []} for i, row in enumerate(rows): kind = "tableHeader" if i == 0 else "tableCell" out["content"].append({"type": "tableRow", "content": [cell(kind, c) for c in row]}) return out def split_row(line): return [c.strip() for c in line.strip().strip("|").split("|")] def build_adf(md): lines = md.splitlines() content = [] i = 0 while i < len(lines): line = lines[i] if not line.strip(): i += 1 continue if line.startswith("```"): lang = line[3:].strip() body = [] i += 1 while i < len(lines) and not lines[i].startswith("```"): body.append(lines[i]) i += 1 i += 1 content.append(code_block(lang, body)) continue m = re.match(r"^(#{1,6})\s+(.*)", line) if m: content.append(heading(len(m.group(1)), m.group(2).strip())) i += 1 continue if line.lstrip().startswith("|"): rows = [] while i < len(lines) and lines[i].lstrip().startswith("|"): if not re.match(r"^\s*\|[\s:|-]+\|\s*$", lines[i]): rows.append(split_row(lines[i])) i += 1 content.append(table(rows)) continue if re.match(r"^\s*- ", line): items = [] while i < len(lines) and re.match(r"^\s*- ", lines[i]): items.append(re.sub(r"^\s*- ", "", lines[i]).strip()) i += 1 content.append(list_node(items, ordered=False)) continue if re.match(r"^\s*\d+\.\s", line): items = [] while i < len(lines) and re.match(r"^\s*\d+\.\s", lines[i]): items.append(re.sub(r"^\s*\d+\.\s", "", lines[i]).strip()) i += 1 content.append(list_node(items, ordered=True)) continue para = [line.strip()] i += 1 while i < len(lines) and lines[i].strip() and not re.match(r"^(#|```|\s*-\s|\s*\d+\.\s|\s*\|)", lines[i]): para.append(lines[i].strip()) i += 1 content.append(paragraph(" ".join(para))) return {"type": "doc", "version": 1, "content": content} def append_table_rows(field_json_path, rows_md_path): field = json.load(open(field_json_path)) rows = [] for line in open(rows_md_path).read().splitlines(): if line.lstrip().startswith("|") and not re.match(r"^\s*\|[\s:|-]+\|\s*$", line): rows.append(split_row(line)) tables = [n for n in field.get("content", []) if n.get("type") == "table"] if not tables: raise SystemExit("no table found in the existing field, refusing to guess") target = tables[-1] header_cells = len(target["content"][0]["content"]) body_rows = [r for r in target["content"][1:] if any( t.strip() for c in r["content"] for t in _cell_texts(c))] target["content"] = [target["content"][0]] + body_rows for row in rows: if len(row) != header_cells: raise SystemExit(f"row has {len(row)} cells, table header has {header_cells}: {row}") target["content"].append({"type": "tableRow", "content": [cell("tableCell", c) for c in row]}) return field def _cell_texts(cell_node): out = [] def walk(n): if isinstance(n, dict): if n.get("type") == "text": out.append(n.get("text", "")) for c in n.get("content", []): walk(c) walk(cell_node) return out if __name__ == "__main__": if len(sys.argv) >= 3 and sys.argv[1] == "render": json.dump(build_adf(open(sys.argv[2]).read()), sys.stdout) elif len(sys.argv) >= 4 and sys.argv[1] == "tables": json.dump(append_table_rows(sys.argv[2], sys.argv[3]), sys.stdout) else: sys.exit("usage: review2adf.py render | tables ")