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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adf-parse
3
- Version: 0.1.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
- # See the docs (https://example.com)
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
- # See the docs (https://example.com)
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.1.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.1.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
- if mark["type"] == ADFMarkType.LINK:
32
- ref = mark["attrs"]["href"]
33
- collection.links.append((text,ref))
34
- text = f"{text} ({ref})"
35
- elif mark["type"] == ADFMarkType.CODE:
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
- return "\n".join(parse_doc(adf).collected_text)
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)