marklas 0.1.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.
marklas-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: marklas
3
+ Version: 0.1.0
4
+ Summary: Bidirectional converter between GitHub Flavored Markdown and Atlassian Document Format
5
+ Author: byExist
6
+ Author-email: byExist <jongbeom.kwon@gmail.com>
7
+ License-Expression: MIT
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Typing :: Typed
12
+ Requires-Dist: mistune>=3.2
13
+ Requires-Dist: black>=26.1.0 ; extra == 'dev'
14
+ Requires-Dist: pytest>=8.0.0 ; extra == 'dev'
15
+ Requires-Dist: pytest-cov>=4.1.0 ; extra == 'dev'
16
+ Requires-Python: >=3.13
17
+ Provides-Extra: dev
18
+ Description-Content-Type: text/markdown
19
+
20
+ # marklas
21
+
22
+ Bidirectional conversion library between GFM (GitHub Flavored Markdown) and ADF (Atlassian Document Format).
23
+
24
+ Converts between Markdown and ADF via an intermediate AST representation.
25
+
26
+ ```
27
+ ADF ──parser──▶ AST ──renderer──▶ Markdown
28
+ Markdown ──parser──▶ AST ──renderer──▶ ADF
29
+ ```
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install marklas
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### Markdown → ADF
40
+
41
+ ```python
42
+ from marklas.parser.md import parse
43
+ from marklas.renderer.adf import render
44
+
45
+ doc = parse("**Hello** world")
46
+ adf = render(doc)
47
+ # {
48
+ # "type": "doc",
49
+ # "version": 1,
50
+ # "content": [
51
+ # {
52
+ # "type": "paragraph",
53
+ # "content": [
54
+ # {"type": "text", "text": "Hello", "marks": [{"type": "strong"}]},
55
+ # {"type": "text", "text": " world"}
56
+ # ]
57
+ # }
58
+ # ]
59
+ # }
60
+ ```
61
+
62
+ ### ADF → Markdown
63
+
64
+ ```python
65
+ from typing import Any
66
+
67
+ from marklas.parser.adf import parse
68
+ from marklas.renderer.md import render
69
+
70
+ adf: dict[str, Any] = {
71
+ "type": "doc",
72
+ "version": 1,
73
+ "content": [
74
+ {
75
+ "type": "paragraph",
76
+ "content": [
77
+ {"type": "text", "text": "Hello", "marks": [{"type": "strong"}]},
78
+ {"type": "text", "text": " world"},
79
+ ],
80
+ }
81
+ ],
82
+ }
83
+ doc = parse(adf)
84
+ md = render(doc)
85
+ # "**Hello** world\n"
86
+ ```
87
+
88
+ ## Conversion Rules
89
+
90
+ ### Block
91
+
92
+ | ADF | AST | Markdown |
93
+ | ------------------------------------------ | ------------------------------------------------ | ----------------------------- |
94
+ | `paragraph` | `Paragraph(children)` | inline content |
95
+ | `heading` (level 1-6) | `Heading(level, children)` | `# ~ ######` |
96
+ | `codeBlock` (language?) | `CodeBlock(code, language?)` | ` ```lang\ncode\n``` ` |
97
+ | `blockquote` | `BlockQuote(children)` | `> text` |
98
+ | `bulletList > listItem` | `BulletList(items) > ListItem(children)` | `- item` |
99
+ | `orderedList > listItem` | `OrderedList(items, start) > ListItem(children)` | `1. item` |
100
+ | `taskList > taskItem` | `BulletList > ListItem(checked=bool)` | `- [x]` / `- [ ]` |
101
+ | `decisionList > decisionItem` | `BulletList > ListItem(checked=bool)` | `- [x]` / `- [ ]` |
102
+ | `rule` | `ThematicBreak` | `---` |
103
+ | `table > tableRow > tableHeader/tableCell` | `Table(head, body, alignments)` | GFM table |
104
+ | `mediaSingle > media` (external) | `Paragraph > Image(url, alt)` | `![alt](url)` |
105
+ | `mediaSingle > media` (non-external) | `Paragraph > Text("[Image: id]")` | `[Image: id]` |
106
+ | `mediaGroup > media` | `Paragraph > Image/Text` | `![alt](url)` / `[Image: id]` |
107
+ | `panel` | `BlockQuote(children)` | `> text` |
108
+ | `expand` / `nestedExpand` (title?) | `BlockQuote(children)` (title prepended) | `> title\n> text` |
109
+ | `layoutSection > layoutColumn` | flattened blocks | columns flattened |
110
+ | `blockCard` (url) | `Paragraph > Link(url)` | `[url](url)` |
111
+ | `embedCard` (url) | `Paragraph > Link(url)` | `[url](url)` |
112
+
113
+ ### Inline
114
+
115
+ | ADF | AST | Markdown |
116
+ | ---------------------- | ----------------------------- | ------------------- |
117
+ | `text` | `Text(text)` | plain text |
118
+ | `text` + `strong` mark | `Strong(children)` | `**text**` |
119
+ | `text` + `em` mark | `Emphasis(children)` | `*text*` |
120
+ | `text` + `strike` mark | `Strikethrough(children)` | `~~text~~` |
121
+ | `text` + `code` mark | `CodeSpan(code)` | `` `code` `` |
122
+ | `text` + `link` mark | `Link(url, children, title?)` | `[text](url)` |
123
+ | `hardBreak` | `HardBreak` | `\` + newline |
124
+ | — | `SoftBreak` | newline |
125
+ | `mention` | `CodeSpan(code)` | `` `@user` `` |
126
+ | `emoji` | `Text(text)` | `:shortName:` |
127
+ | `date` | `CodeSpan(code)` | `` `2024-01-01` `` |
128
+ | `status` | `CodeSpan(code)` | `` `status text` `` |
129
+ | `inlineCard` (url) | `Link(url)` | `[url](url)` |
130
+ | — | `Image(url, alt, title?)` | `![alt](url)` |
131
+
132
+ ### Not Supported
133
+
134
+ | Element | Behavior |
135
+ | ---------------------------------------------------------------------- | -------------------- |
136
+ | ADF marks: `underline`, `textColor`, `backgroundColor`, `subsup` | silently ignored |
137
+ | ADF blocks: `extension`, `bodiedExtension`, `syncBlock`, `bodiedSyncBlock` | `[type]` placeholder |
138
+ | ADF inlines: `placeholder`, `inlineExtension`, `mediaInline` | `[type]` placeholder |
139
+ | ADF table: `colspan`, `rowspan` | expanded with empty cells |
140
+ | ADF table: `background`, `colwidth` | attributes ignored |
141
+ | ADF table: non-paragraph cell content | `[type]` placeholder |
142
+ | Markdown: raw HTML (block, inline) | silently ignored |
143
+
144
+ ## Development
145
+
146
+ ```bash
147
+ uv sync --extra dev
148
+ uv run pytest -v
149
+ uv run black src/ tests/
150
+ ```
@@ -0,0 +1,131 @@
1
+ # marklas
2
+
3
+ Bidirectional conversion library between GFM (GitHub Flavored Markdown) and ADF (Atlassian Document Format).
4
+
5
+ Converts between Markdown and ADF via an intermediate AST representation.
6
+
7
+ ```
8
+ ADF ──parser──▶ AST ──renderer──▶ Markdown
9
+ Markdown ──parser──▶ AST ──renderer──▶ ADF
10
+ ```
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install marklas
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ### Markdown → ADF
21
+
22
+ ```python
23
+ from marklas.parser.md import parse
24
+ from marklas.renderer.adf import render
25
+
26
+ doc = parse("**Hello** world")
27
+ adf = render(doc)
28
+ # {
29
+ # "type": "doc",
30
+ # "version": 1,
31
+ # "content": [
32
+ # {
33
+ # "type": "paragraph",
34
+ # "content": [
35
+ # {"type": "text", "text": "Hello", "marks": [{"type": "strong"}]},
36
+ # {"type": "text", "text": " world"}
37
+ # ]
38
+ # }
39
+ # ]
40
+ # }
41
+ ```
42
+
43
+ ### ADF → Markdown
44
+
45
+ ```python
46
+ from typing import Any
47
+
48
+ from marklas.parser.adf import parse
49
+ from marklas.renderer.md import render
50
+
51
+ adf: dict[str, Any] = {
52
+ "type": "doc",
53
+ "version": 1,
54
+ "content": [
55
+ {
56
+ "type": "paragraph",
57
+ "content": [
58
+ {"type": "text", "text": "Hello", "marks": [{"type": "strong"}]},
59
+ {"type": "text", "text": " world"},
60
+ ],
61
+ }
62
+ ],
63
+ }
64
+ doc = parse(adf)
65
+ md = render(doc)
66
+ # "**Hello** world\n"
67
+ ```
68
+
69
+ ## Conversion Rules
70
+
71
+ ### Block
72
+
73
+ | ADF | AST | Markdown |
74
+ | ------------------------------------------ | ------------------------------------------------ | ----------------------------- |
75
+ | `paragraph` | `Paragraph(children)` | inline content |
76
+ | `heading` (level 1-6) | `Heading(level, children)` | `# ~ ######` |
77
+ | `codeBlock` (language?) | `CodeBlock(code, language?)` | ` ```lang\ncode\n``` ` |
78
+ | `blockquote` | `BlockQuote(children)` | `> text` |
79
+ | `bulletList > listItem` | `BulletList(items) > ListItem(children)` | `- item` |
80
+ | `orderedList > listItem` | `OrderedList(items, start) > ListItem(children)` | `1. item` |
81
+ | `taskList > taskItem` | `BulletList > ListItem(checked=bool)` | `- [x]` / `- [ ]` |
82
+ | `decisionList > decisionItem` | `BulletList > ListItem(checked=bool)` | `- [x]` / `- [ ]` |
83
+ | `rule` | `ThematicBreak` | `---` |
84
+ | `table > tableRow > tableHeader/tableCell` | `Table(head, body, alignments)` | GFM table |
85
+ | `mediaSingle > media` (external) | `Paragraph > Image(url, alt)` | `![alt](url)` |
86
+ | `mediaSingle > media` (non-external) | `Paragraph > Text("[Image: id]")` | `[Image: id]` |
87
+ | `mediaGroup > media` | `Paragraph > Image/Text` | `![alt](url)` / `[Image: id]` |
88
+ | `panel` | `BlockQuote(children)` | `> text` |
89
+ | `expand` / `nestedExpand` (title?) | `BlockQuote(children)` (title prepended) | `> title\n> text` |
90
+ | `layoutSection > layoutColumn` | flattened blocks | columns flattened |
91
+ | `blockCard` (url) | `Paragraph > Link(url)` | `[url](url)` |
92
+ | `embedCard` (url) | `Paragraph > Link(url)` | `[url](url)` |
93
+
94
+ ### Inline
95
+
96
+ | ADF | AST | Markdown |
97
+ | ---------------------- | ----------------------------- | ------------------- |
98
+ | `text` | `Text(text)` | plain text |
99
+ | `text` + `strong` mark | `Strong(children)` | `**text**` |
100
+ | `text` + `em` mark | `Emphasis(children)` | `*text*` |
101
+ | `text` + `strike` mark | `Strikethrough(children)` | `~~text~~` |
102
+ | `text` + `code` mark | `CodeSpan(code)` | `` `code` `` |
103
+ | `text` + `link` mark | `Link(url, children, title?)` | `[text](url)` |
104
+ | `hardBreak` | `HardBreak` | `\` + newline |
105
+ | — | `SoftBreak` | newline |
106
+ | `mention` | `CodeSpan(code)` | `` `@user` `` |
107
+ | `emoji` | `Text(text)` | `:shortName:` |
108
+ | `date` | `CodeSpan(code)` | `` `2024-01-01` `` |
109
+ | `status` | `CodeSpan(code)` | `` `status text` `` |
110
+ | `inlineCard` (url) | `Link(url)` | `[url](url)` |
111
+ | — | `Image(url, alt, title?)` | `![alt](url)` |
112
+
113
+ ### Not Supported
114
+
115
+ | Element | Behavior |
116
+ | ---------------------------------------------------------------------- | -------------------- |
117
+ | ADF marks: `underline`, `textColor`, `backgroundColor`, `subsup` | silently ignored |
118
+ | ADF blocks: `extension`, `bodiedExtension`, `syncBlock`, `bodiedSyncBlock` | `[type]` placeholder |
119
+ | ADF inlines: `placeholder`, `inlineExtension`, `mediaInline` | `[type]` placeholder |
120
+ | ADF table: `colspan`, `rowspan` | expanded with empty cells |
121
+ | ADF table: `background`, `colwidth` | attributes ignored |
122
+ | ADF table: non-paragraph cell content | `[type]` placeholder |
123
+ | Markdown: raw HTML (block, inline) | silently ignored |
124
+
125
+ ## Development
126
+
127
+ ```bash
128
+ uv sync --extra dev
129
+ uv run pytest -v
130
+ uv run black src/ tests/
131
+ ```
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "marklas"
3
+ version = "0.1.0"
4
+ description = "Bidirectional converter between GitHub Flavored Markdown and Atlassian Document Format"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "byExist", email = "jongbeom.kwon@gmail.com" }
9
+ ]
10
+ requires-python = ">=3.13"
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3.13",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Typing :: Typed",
16
+ ]
17
+ dependencies = ["mistune>=3.2"]
18
+
19
+ [project.optional-dependencies]
20
+ dev = [
21
+ "black>=26.1.0",
22
+ "pytest>=8.0.0",
23
+ "pytest-cov>=4.1.0",
24
+ ]
25
+
26
+ [build-system]
27
+ requires = ["uv_build>=0.9.21,<0.10.0"]
28
+ build-backend = "uv_build"
@@ -0,0 +1,3 @@
1
+ from marklas.ast.blocks import Document
2
+
3
+ __all__ = ["Document"]
File without changes
@@ -0,0 +1,6 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Node:
6
+ pass
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Literal
5
+
6
+ from marklas.ast.base import Node
7
+ from marklas.ast.inlines import Inline
8
+
9
+
10
+ @dataclass
11
+ class Block(Node):
12
+ pass
13
+
14
+
15
+ @dataclass
16
+ class Document(Node):
17
+ children: list[Block]
18
+
19
+
20
+ @dataclass
21
+ class Paragraph(Block):
22
+ children: list[Inline]
23
+
24
+
25
+ @dataclass
26
+ class Heading(Block):
27
+ level: Literal[1, 2, 3, 4, 5, 6]
28
+ children: list[Inline]
29
+
30
+
31
+ @dataclass
32
+ class CodeBlock(Block):
33
+ code: str
34
+ language: str | None = None
35
+
36
+
37
+ @dataclass
38
+ class BlockQuote(Block):
39
+ children: list[Block]
40
+
41
+
42
+ @dataclass
43
+ class ThematicBreak(Block):
44
+ pass
45
+
46
+
47
+ @dataclass
48
+ class ListItem(Block):
49
+ children: list[Block]
50
+ checked: bool | None = None
51
+
52
+
53
+ @dataclass
54
+ class BulletList(Block):
55
+ items: list[ListItem]
56
+ tight: bool = True
57
+
58
+
59
+ @dataclass
60
+ class OrderedList(Block):
61
+ items: list[ListItem]
62
+ start: int = 1
63
+ tight: bool = True
64
+
65
+
66
+ @dataclass
67
+ class TableCell(Node):
68
+ children: list[Inline]
69
+
70
+
71
+ @dataclass
72
+ class Table(Block):
73
+ head: list[TableCell]
74
+ body: list[list[TableCell]]
75
+ alignments: list[Literal["left", "center", "right"] | None] = field(
76
+ default_factory=list[Literal["left", "center", "right"] | None]
77
+ )
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from marklas.ast.base import Node
6
+
7
+
8
+ @dataclass
9
+ class Inline(Node):
10
+ pass
11
+
12
+
13
+ @dataclass
14
+ class Text(Inline):
15
+ text: str
16
+
17
+
18
+ @dataclass
19
+ class Strong(Inline):
20
+ children: list[Inline]
21
+
22
+
23
+ @dataclass
24
+ class Emphasis(Inline):
25
+ children: list[Inline]
26
+
27
+
28
+ @dataclass
29
+ class Strikethrough(Inline):
30
+ children: list[Inline]
31
+
32
+
33
+ @dataclass
34
+ class Link(Inline):
35
+ url: str
36
+ children: list[Inline]
37
+ title: str | None = None
38
+
39
+
40
+ @dataclass
41
+ class Image(Inline):
42
+ url: str
43
+ alt: str = ""
44
+ title: str | None = None
45
+
46
+
47
+ @dataclass
48
+ class CodeSpan(Inline):
49
+ code: str
50
+
51
+
52
+ @dataclass
53
+ class HardBreak(Inline):
54
+ pass
55
+
56
+
57
+ @dataclass
58
+ class SoftBreak(Inline):
59
+ pass
File without changes