from __future__ import annotations import re def markdown_to_adf(markdown: str, *, is_final: bool = False) -> dict: lines = markdown.split("\n") blocks: list[dict] = [] i = 0 while i < len(lines): line = lines[i] if not line.strip(): i += 1 continue if line.startswith("```"): code_lines: list[str] = [] lang = _extract_lang(line) i += 1 while i < len(lines) and not lines[i].startswith("```"): code_lines.append(lines[i]) i += 1 i += 1 blocks.append(_adf_code_block("\n".join(code_lines), lang)) continue img_match = re.match(r"!\[([^\]]*)\]\(([^)]+)\)", line.strip()) if img_match: blocks.append(_adf_media(img_match.group(2), img_match.group(1) or "image")) i += 1 continue if line.startswith("> "): quote_lines: list[str] = [] while i < len(lines) and lines[i].startswith("> "): quote_lines.append(lines[i][2:]) i += 1 blocks.append(_adf_blockquote("\n".join(quote_lines))) continue if line.startswith("#"): level = len(line) - len(line.lstrip("#")) heading_text = line.lstrip("#").strip() blocks.append(_adf_heading(heading_text, level)) i += 1 continue if line.strip().startswith("|") and line.strip().endswith("|"): table_rows: list[list[str]] = [] while i < len(lines) and lines[i].strip().startswith("|"): cells = [c.strip() for c in lines[i].strip().strip("|").split("|")] table_rows.append(cells) i += 1 if len(table_rows) >= 2: header = table_rows[0] data_start = 2 if _is_table_separator(table_rows[1]) else 1 blocks.append(_adf_table(header, table_rows[data_start:])) continue if line.startswith("- ") or line.startswith("* "): list_items: list[str] = [] while i < len(lines) and (lines[i].startswith("- ") or lines[i].startswith("* ")): list_items.append(lines[i][2:].strip()) i += 1 blocks.append(_adf_bullet_list(list_items)) continue if re.match(r"^\d+\. ", line): list_items: list[str] = [] while i < len(lines) and re.match(r"^\d+\. ", lines[i]): list_items.append(re.sub(r"^\d+\. ", "", lines[i]).strip()) i += 1 blocks.append(_adf_ordered_list(list_items)) continue if line.strip() == "---": blocks.append({"type": "rule"}) i += 1 continue blocks.append(_adf_paragraph(line)) i += 1 return {"type": "doc", "version": 1, "content": blocks} def _extract_lang(line: str) -> str: return line[3:].strip() def _is_table_separator(cells: list[str]) -> bool: return all(re.match(r"^:?-{3,}:?$", c) for c in cells) def _adf_table(header: list[str], rows: list[list[str]]) -> dict: table_rows = [ { "type": "tableRow", "content": [ { "type": "tableHeader", "content": [_adf_simple_paragraph(cell)], } for cell in header ], } ] for row in rows: table_rows.append( { "type": "tableRow", "content": [ { "type": "tableCell", "content": [_adf_simple_paragraph(cell)], } for cell in row ], } ) return { "type": "table", "attrs": {"isNumberColumnEnabled": False, "layout": "default"}, "content": table_rows, } def _adf_simple_paragraph(text: str) -> dict: return {"type": "paragraph", "content": _parse_inline(text)} def _parse_inline(text: str) -> list[dict]: result: list[dict] = [] pattern = re.compile( r"(\*\*(.+?)\*\*|" r"~~(.+?)~~|" r"`(.+?)`|" r"\*(.+?)\*|" r"\[([^\]]+)\]\(([^)]+)\)|" r"@([\w.-]+))" ) last_end = 0 for match in pattern.finditer(text): if match.start() > last_end: result.append({"type": "text", "text": text[last_end : match.start()]}) if match.group(2): result.append({"type": "text", "text": match.group(2), "marks": [{"type": "strong"}]}) elif match.group(3): result.append({"type": "text", "text": match.group(3), "marks": [{"type": "strike"}]}) elif match.group(4): result.append({"type": "text", "text": match.group(4), "marks": [{"type": "code"}]}) elif match.group(5): result.append({"type": "text", "text": match.group(5), "marks": [{"type": "em"}]}) elif match.group(6): result.append( { "type": "text", "text": match.group(6), "marks": [{"type": "link", "attrs": {"href": match.group(7)}}], } ) elif match.group(8): result.append( { "type": "mention", "attrs": { "id": match.group(8), "text": match.group(8), "accessLevel": "", }, } ) last_end = match.end() if last_end < len(text): result.append({"type": "text", "text": text[last_end:]}) if not result: result.append({"type": "text", "text": text}) return result def _adf_paragraph(text: str) -> dict: return {"type": "paragraph", "content": _parse_inline(text)} def _adf_heading(text: str, level: int) -> dict: return { "type": "heading", "attrs": {"level": min(level, 6)}, "content": _parse_inline(text), } def _adf_code_block(code: str, language: str = "") -> dict: attrs = {} if language: attrs["language"] = language return { "type": "codeBlock", "attrs": attrs, "content": [{"type": "text", "text": code}], } def _adf_blockquote(text: str) -> dict: return { "type": "blockquote", "content": [_adf_paragraph(text)], } def _adf_bullet_list(items: list[str]) -> dict: return { "type": "bulletList", "content": [ { "type": "listItem", "content": [_adf_paragraph(item)], } for item in items ], } def _adf_ordered_list(items: list[str]) -> dict: return { "type": "orderedList", "content": [ { "type": "listItem", "content": [_adf_paragraph(item)], } for item in items ], } def _adf_media(url: str, alt: str = "") -> dict: return { "type": "mediaSingle", "attrs": {"layout": "center"}, "content": [ { "type": "media", "attrs": { "type": "external", "url": url, "alt": alt, }, } ], }