from __future__ import annotations def adf_to_plain_text(adf: dict | None) -> str: if not adf or not isinstance(adf, dict): return "" parts: list[str] = [] _walk_nodes(adf.get("content", []), parts) return "".join(parts).strip() def adf_to_markdown(adf: dict | None) -> str: if not adf or not isinstance(adf, dict): return "" parts: list[str] = [] _walk_nodes_md(adf.get("content", []), parts) return "".join(parts).strip() def _walk_nodes(nodes: list, parts: list): for node in nodes: if not isinstance(node, dict): continue node_type = node.get("type", "") marks = node.get("marks", []) if node_type == "text": text = node.get("text", "") _process_marks(parts, text, marks) elif node_type == "hardBreak": parts.append("\n") elif node_type == "mention": parts.append(f"@{node.get('attrs', {}).get('text', 'user')}") elif node_type == "emoji": parts.append(node.get("attrs", {}).get("shortName", "")) elif node_type == "paragraph": _walk_nodes(node.get("content", []), parts) parts.append("\n") elif node_type == "heading": level = node.get("attrs", {}).get("level", 1) prefix = "#" * level + " " parts.append(prefix) _walk_nodes(node.get("content", []), parts) parts.append("\n\n") elif node_type == "codeBlock": lang = node.get("attrs", {}).get("language", "") parts.append(f"```{lang}\n") _walk_nodes(node.get("content", []), parts) parts.append("\n```\n\n") elif node_type == "blockquote": content_parts: list[str] = [] _walk_nodes(node.get("content", []), content_parts) for line in "".join(content_parts).split("\n"): if line.strip(): parts.append(f"> {line}\n") parts.append("\n") elif node_type == "bulletList": _walk_list_items(node.get("content", []), parts, "- ") elif node_type == "orderedList": _walk_list_items(node.get("content", []), parts, "1. ") elif node_type == "panel": panel_type = node.get("attrs", {}).get("panelType", "info") parts.append(f"> **[{panel_type}]** ") _walk_nodes(node.get("content", []), parts) parts.append("\n\n") elif node_type == "rule": parts.append("---\n\n") elif node_type == "table": _walk_table(node, parts) parts.append("\n") elif node_type == "media": media_attrs = node.get("attrs", {}) display_type = media_attrs.get("type", "file") filename = media_attrs.get("alt", "") or f"{display_type}_attachment" if display_type == "image": url = media_attrs.get("url", "") parts.append(f"![{filename}]({url})\n") else: parts.append(f"[📎 {filename}]\n") elif node_type == "date": timestamp = node.get("attrs", {}).get("timestamp", "") parts.append(f"[{timestamp}]") elif node_type == "status": status_text = node.get("attrs", {}).get("text", "") color = node.get("attrs", {}).get("color", "") parts.append(f"[{status_text}]({color})") elif "content" in node: _walk_nodes(node["content"], parts) def _walk_nodes_md(nodes: list, parts: list): _walk_nodes(nodes, parts) def _process_marks(parts: list, text: str, marks: list): prefix = suffix = "" links: list[str] = [] for mark in marks: mark_type = mark.get("type", "") if mark_type == "strong": prefix = "**" + prefix suffix += "**" elif mark_type == "em": prefix = "*" + prefix suffix += "*" elif mark_type == "code": prefix = "`" + prefix suffix += "`" elif mark_type == "strike": prefix = "~~" + prefix suffix += "~~" elif mark_type == "link": href = mark.get("attrs", {}).get("href", "") if href: links.append(href) if links: parts.append(f"[{prefix}{text}{suffix}]({links[-1]})") else: parts.append(f"{prefix}{text}{suffix}") def _walk_list_items(items: list, parts: list, prefix: str): for item in items: if not isinstance(item, dict): continue item_content = item.get("content", []) for node in item_content: node_type = node.get("type", "") if node_type == "paragraph": parts.append(prefix) _walk_nodes([node], parts) parts.append("\n") elif node_type in ("bulletList", "orderedList"): sub_prefix = " " + prefix _walk_list_items(node.get("content", []), parts, sub_prefix) parts.append("\n") def _walk_table(table: dict, parts: list): rows = table.get("content", []) for row_idx, row in enumerate(rows): cells = row.get("content", []) cell_texts: list[str] = [] for cell in cells: cell_parts: list[str] = [] _walk_nodes(cell.get("content", []), cell_parts) cell_texts.append("".join(cell_parts).replace("\n", " ").strip()) parts.append("| " + " | ".join(cell_texts) + " |\n") if row_idx == 0: parts.append("| " + " | ".join(["---"] * len(cells)) + " |\n")