"""
Atlassian Document Format (ADF) utilities.
This module provides utilities for parsing ADF content from Jira Cloud.
"""
from datetime import datetime, timezone
def adf_to_text(adf_content: dict | list | str | None) -> str | None:
"""
Convert Atlassian Document Format (ADF) content to plain text.
ADF is Jira Cloud's rich text format returned for fields like description.
This function recursively extracts text content from the ADF structure.
Args:
adf_content: ADF document (dict), content list, string, or None
Returns:
Plain text string or None if no content
"""
if adf_content is None:
return None
if isinstance(adf_content, str):
return adf_content
if isinstance(adf_content, list):
texts = []
for item in adf_content:
text = adf_to_text(item)
if text:
texts.append(text)
return "\n".join(texts) if texts else None
if isinstance(adf_content, dict):
# Check if this is a text node
if adf_content.get("type") == "text":
return adf_content.get("text", "")
# Check if this is a hardBreak node
if adf_content.get("type") == "hardBreak":
return "\n"
# Check if this is a mention node
if adf_content.get("type") == "mention":
attrs = adf_content.get("attrs", {})
return attrs.get("text") or f"@{attrs.get('id', 'unknown')}"
# Check if this is an emoji node
if adf_content.get("type") == "emoji":
attrs = adf_content.get("attrs", {})
return attrs.get("text") or attrs.get("shortName", "")
# Check if this is a date node
if adf_content.get("type") == "date":
attrs = adf_content.get("attrs", {})
timestamp = attrs.get("timestamp")
if timestamp:
try:
dt = datetime.fromtimestamp(int(timestamp) / 1000, tz=timezone.utc)
return dt.strftime("%Y-%m-%d")
except (ValueError, OSError, TypeError):
return str(timestamp)
return ""
# Check if this is a status node
if adf_content.get("type") == "status":
attrs = adf_content.get("attrs", {})
return f"[{attrs.get('text', '')}]"
# Check if this is an inlineCard node
if adf_content.get("type") == "inlineCard":
attrs = adf_content.get("attrs", {})
url = attrs.get("url")
if url:
return url
data = attrs.get("data", {})
return data.get("url") or data.get("name", "")
# Check if this is a codeBlock node
if adf_content.get("type") == "codeBlock":
content = adf_content.get("content", [])
code_text = adf_to_text(content) or ""
return f"```\n{code_text}\n```"
# Recursively process content
content = adf_content.get("content")
if content:
return adf_to_text(content)
return None
return None