adf-parse 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {adf_parse-0.1.0 → adf_parse-0.2.0}/PKG-INFO +13 -3
- {adf_parse-0.1.0 → adf_parse-0.2.0}/README.md +12 -2
- {adf_parse-0.1.0 → adf_parse-0.2.0}/pyproject.toml +4 -1
- {adf_parse-0.1.0 → adf_parse-0.2.0}/src/adf_parse/__init__.py +3 -2
- {adf_parse-0.1.0 → adf_parse-0.2.0}/src/adf_parse/enums.py +5 -1
- {adf_parse-0.1.0 → adf_parse-0.2.0}/src/adf_parse/parser.py +48 -11
- {adf_parse-0.1.0 → adf_parse-0.2.0}/src/adf_parse/models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: adf-parse
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Convert Atlassian Document Format (ADF) from Jira/Confluence into plain text or Markdown
|
|
5
5
|
Keywords: adf,atlassian,jira,confluence,markdown,parser
|
|
6
6
|
Author: Abhishek Hazra
|
|
@@ -22,7 +22,7 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|
|
|
23
23
|
Convert [Atlassian Document Format (ADF)](https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/) — the JSON format used by Jira and Confluence — into plain text / Markdown.
|
|
24
24
|
|
|
25
|
-
Zero dependencies. Supports paragraphs, headings, bullet/ordered lists (with nesting), code blocks, tables (with header detection), blockquotes, panels, expands, task lists, mentions, dates, statuses, inline cards, hard breaks, rules, and media references.
|
|
25
|
+
Zero dependencies. Supports paragraphs, headings, bullet/ordered lists (with nesting), code blocks, tables (with header detection), blockquotes, panels, expands, task lists, mentions, emoji, dates, statuses, inline cards, hard breaks, rules, and media references. Inline marks render as real Markdown: links `[text](url)`, **bold**, *italic*, ~~strikethrough~~, and `code`.
|
|
26
26
|
|
|
27
27
|
## Installation
|
|
28
28
|
|
|
@@ -51,7 +51,17 @@ adf = {
|
|
|
51
51
|
|
|
52
52
|
print(create_md_doc(adf))
|
|
53
53
|
# ## Release notes
|
|
54
|
-
#
|
|
54
|
+
#
|
|
55
|
+
# [See the docs](https://example.com)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For search indexes, RAG, and LLM/agent pipelines, use plain mode — it drops emoji and all Markdown decoration (`**`, `*`, `~~`, backticks, `[text](url)`), giving clean text while links and media are still collected as data:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from adf_parse import create_text_doc
|
|
62
|
+
|
|
63
|
+
create_text_doc(adf) # clean plain text
|
|
64
|
+
parse_doc(adf, plain=True) # same, with .links and .media_ref
|
|
55
65
|
```
|
|
56
66
|
|
|
57
67
|
To also get the extracted links and media references, use `parse_doc`:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Convert [Atlassian Document Format (ADF)](https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/) — the JSON format used by Jira and Confluence — into plain text / Markdown.
|
|
4
4
|
|
|
5
|
-
Zero dependencies. Supports paragraphs, headings, bullet/ordered lists (with nesting), code blocks, tables (with header detection), blockquotes, panels, expands, task lists, mentions, dates, statuses, inline cards, hard breaks, rules, and media references.
|
|
5
|
+
Zero dependencies. Supports paragraphs, headings, bullet/ordered lists (with nesting), code blocks, tables (with header detection), blockquotes, panels, expands, task lists, mentions, emoji, dates, statuses, inline cards, hard breaks, rules, and media references. Inline marks render as real Markdown: links `[text](url)`, **bold**, *italic*, ~~strikethrough~~, and `code`.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -31,7 +31,17 @@ adf = {
|
|
|
31
31
|
|
|
32
32
|
print(create_md_doc(adf))
|
|
33
33
|
# ## Release notes
|
|
34
|
-
#
|
|
34
|
+
#
|
|
35
|
+
# [See the docs](https://example.com)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For search indexes, RAG, and LLM/agent pipelines, use plain mode — it drops emoji and all Markdown decoration (`**`, `*`, `~~`, backticks, `[text](url)`), giving clean text while links and media are still collected as data:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from adf_parse import create_text_doc
|
|
42
|
+
|
|
43
|
+
create_text_doc(adf) # clean plain text
|
|
44
|
+
parse_doc(adf, plain=True) # same, with .links and .media_ref
|
|
35
45
|
```
|
|
36
46
|
|
|
37
47
|
To also get the extracted links and media references, use `parse_doc`:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "adf-parse"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Convert Atlassian Document Format (ADF) from Jira/Confluence into plain text or Markdown"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -25,6 +25,9 @@ dependencies = []
|
|
|
25
25
|
Homepage = "https://github.com/hazra1991/adf-parse"
|
|
26
26
|
Repository = "https://github.com/hazra1991/adf-parse"
|
|
27
27
|
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = ["pytest>=8"]
|
|
30
|
+
|
|
28
31
|
[build-system]
|
|
29
32
|
requires = ["uv_build>=0.10,<0.11"]
|
|
30
33
|
build-backend = "uv_build"
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from .enums import ADFMarkType, ADFNodeType, MediaRefType
|
|
4
4
|
from .models import MediaRef, NodeCollection
|
|
5
|
-
from .parser import create_md_doc, parse_content, parse_doc
|
|
5
|
+
from .parser import create_md_doc, create_text_doc, parse_content, parse_doc
|
|
6
6
|
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.2.0"
|
|
8
8
|
|
|
9
9
|
__all__ = [
|
|
10
10
|
"ADFMarkType",
|
|
@@ -13,6 +13,7 @@ __all__ = [
|
|
|
13
13
|
"MediaRef",
|
|
14
14
|
"NodeCollection",
|
|
15
15
|
"create_md_doc",
|
|
16
|
+
"create_text_doc",
|
|
16
17
|
"parse_content",
|
|
17
18
|
"parse_doc",
|
|
18
19
|
]
|
|
@@ -36,6 +36,7 @@ class ADFNodeType(StrEnum):
|
|
|
36
36
|
STATUS = "status" # attrs.text = "IN PROGRESS", attrs.color
|
|
37
37
|
DATE = "date" # attrs.timestamp (unix millis string)
|
|
38
38
|
INLINE_CARD = "inlineCard" # attrs.url = smart link URL
|
|
39
|
+
EMOJI = "emoji" # attrs.text = "🔥", attrs.shortName = ":fire:"
|
|
39
40
|
MEDIA_SINGLE = "mediaSingle" # container for media node
|
|
40
41
|
MEDIA = "media" # attrs.id, attrs.type, attrs.collection
|
|
41
42
|
MEDIA_INLINE = "mediaInline" # Inline Media
|
|
@@ -44,8 +45,11 @@ class ADFNodeType(StrEnum):
|
|
|
44
45
|
class ADFMarkType(StrEnum):
|
|
45
46
|
"""Mark types you actually care about during parsing."""
|
|
46
47
|
|
|
47
|
-
LINK = "link"
|
|
48
|
+
LINK = "link"
|
|
48
49
|
CODE = "code"
|
|
50
|
+
STRONG = "strong"
|
|
51
|
+
EM = "em"
|
|
52
|
+
STRIKE = "strike"
|
|
49
53
|
|
|
50
54
|
class MediaRefType(StrEnum):
|
|
51
55
|
URL = "url"
|
|
@@ -10,7 +10,13 @@ def format_list_item(prefix: str, item_text: str) -> str:
|
|
|
10
10
|
return "\n".join([prefix + lines[0]] + [pad + line for line in lines[1:]])
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def parse_content(content: list[dict]) -> NodeCollection:
|
|
13
|
+
def parse_content(content: list[dict], *, plain: bool = False) -> NodeCollection:
|
|
14
|
+
"""Parse a list of ADF nodes.
|
|
15
|
+
|
|
16
|
+
plain=True skips cosmetic output (emoji, bold/italic/strike/code
|
|
17
|
+
decoration, link syntax) for clean text aimed at search indexes
|
|
18
|
+
and LLM pipelines. Links and media refs are still collected.
|
|
19
|
+
"""
|
|
14
20
|
collection = NodeCollection()
|
|
15
21
|
|
|
16
22
|
for node in content:
|
|
@@ -19,7 +25,7 @@ def parse_content(content: list[dict]) -> NodeCollection:
|
|
|
19
25
|
|
|
20
26
|
parts: list[str] = []
|
|
21
27
|
if node.get("content"):
|
|
22
|
-
resp = parse_content(node["content"])
|
|
28
|
+
resp = parse_content(node["content"], plain=plain)
|
|
23
29
|
collection.links.extend(resp.links)
|
|
24
30
|
collection.media_ref.extend(resp.media_ref)
|
|
25
31
|
parts = resp.collected_text
|
|
@@ -27,19 +33,36 @@ def parse_content(content: list[dict]) -> NodeCollection:
|
|
|
27
33
|
# text (the only node with a "text" key)
|
|
28
34
|
if node_type == ADFNodeType.TEXT:
|
|
29
35
|
text = node["text"]
|
|
36
|
+
link_href = None
|
|
30
37
|
for mark in node.get("marks", []):
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
mark_type = mark["type"]
|
|
39
|
+
if mark_type == ADFMarkType.LINK:
|
|
40
|
+
link_href = mark["attrs"].get("href", "")
|
|
41
|
+
elif plain:
|
|
42
|
+
continue
|
|
43
|
+
elif mark_type == ADFMarkType.CODE:
|
|
36
44
|
text = f"`{text}`"
|
|
45
|
+
elif mark_type == ADFMarkType.STRONG:
|
|
46
|
+
text = f"**{text}**"
|
|
47
|
+
elif mark_type == ADFMarkType.EM:
|
|
48
|
+
text = f"*{text}*"
|
|
49
|
+
elif mark_type == ADFMarkType.STRIKE:
|
|
50
|
+
text = f"~~{text}~~"
|
|
51
|
+
if link_href:
|
|
52
|
+
collection.links.append((node["text"], link_href))
|
|
53
|
+
if not plain:
|
|
54
|
+
text = f"[{text}]({link_href})"
|
|
37
55
|
collection.collected_text.append(text)
|
|
38
56
|
|
|
39
57
|
# leaves: no content, info is in attrs
|
|
40
58
|
elif node_type == ADFNodeType.HARD_BREAK:
|
|
41
59
|
collection.collected_text.append("\n")
|
|
42
60
|
|
|
61
|
+
elif node_type == ADFNodeType.EMOJI:
|
|
62
|
+
emoji = attrs.get("text") or attrs.get("shortName", "")
|
|
63
|
+
if emoji and not plain:
|
|
64
|
+
collection.collected_text.append(emoji)
|
|
65
|
+
|
|
43
66
|
elif node_type == ADFNodeType.MENTION:
|
|
44
67
|
collection.collected_text.append(attrs.get("text") or f"@{attrs.get('id', 'unknown')}")
|
|
45
68
|
|
|
@@ -140,11 +163,25 @@ def parse_content(content: list[dict]) -> NodeCollection:
|
|
|
140
163
|
return collection
|
|
141
164
|
|
|
142
165
|
|
|
143
|
-
def parse_doc(adf: dict) -> NodeCollection:
|
|
166
|
+
def parse_doc(adf: dict, *, plain: bool = False) -> NodeCollection:
|
|
144
167
|
"""Parse a full ADF document and return the collected text, links and media refs."""
|
|
145
|
-
return parse_content(adf.get("content") or [])
|
|
168
|
+
return parse_content(adf.get("content") or [], plain=plain)
|
|
146
169
|
|
|
147
170
|
|
|
148
171
|
def create_md_doc(adf: dict) -> str:
|
|
149
|
-
"""Render a full ADF document as a Markdown string.
|
|
150
|
-
|
|
172
|
+
"""Render a full ADF document as a Markdown string.
|
|
173
|
+
|
|
174
|
+
Top-level blocks are separated by blank lines so paragraphs,
|
|
175
|
+
lists and headings stay distinct blocks in Markdown.
|
|
176
|
+
"""
|
|
177
|
+
return "\n\n".join(parse_doc(adf,plain=False).collected_text)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def create_text_doc(adf: dict) -> str:
|
|
181
|
+
"""Render a full ADF document as clean plain text.
|
|
182
|
+
|
|
183
|
+
No emoji, no bold/italic/strike/code decoration, no link syntax —
|
|
184
|
+
suited to search indexing and LLM/agent pipelines. Links and media
|
|
185
|
+
are still available via parse_doc(adf, plain=True).
|
|
186
|
+
"""
|
|
187
|
+
return "\n\n".join(parse_doc(adf, plain=True).collected_text)
|
|
File without changes
|