edwh-editorjs 2.0.0b1__tar.gz → 2.0.0b3__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.
- {edwh_editorjs-2.0.0b1 → edwh_editorjs-2.0.0b3}/CHANGELOG.md +12 -0
- {edwh_editorjs-2.0.0b1 → edwh_editorjs-2.0.0b3}/LICENSE +1 -0
- edwh_editorjs-2.0.0b3/PKG-INFO +28 -0
- edwh_editorjs-2.0.0b3/README.md +2 -0
- edwh_editorjs-2.0.0b3/editorjs/__about__.py +1 -0
- edwh_editorjs-2.0.0b3/editorjs/__init__.py +5 -0
- edwh_editorjs-2.0.0b3/editorjs/blocks.py +382 -0
- edwh_editorjs-2.0.0b3/editorjs/core.py +116 -0
- edwh_editorjs-2.0.0b3/editorjs/exceptions.py +3 -0
- edwh_editorjs-2.0.0b3/editorjs/helpers.py +5 -0
- edwh_editorjs-2.0.0b3/editorjs/types.py +43 -0
- {edwh_editorjs-2.0.0b1 → edwh_editorjs-2.0.0b3}/pyproject.toml +9 -7
- edwh_editorjs-2.0.0b3/tests/test_core.py +74 -0
- edwh_editorjs-2.0.0b1/PKG-INFO +0 -104
- edwh_editorjs-2.0.0b1/README.md +0 -79
- edwh_editorjs-2.0.0b1/pyeditorjs/__about__.py +0 -1
- edwh_editorjs-2.0.0b1/pyeditorjs/__init__.py +0 -30
- edwh_editorjs-2.0.0b1/pyeditorjs/blocks.py +0 -313
- edwh_editorjs-2.0.0b1/pyeditorjs/exceptions.py +0 -19
- edwh_editorjs-2.0.0b1/pyeditorjs/parser.py +0 -75
- edwh_editorjs-2.0.0b1/scripts/build_documentation.sh +0 -4
- edwh_editorjs-2.0.0b1/scripts/compile_everything.sh +0 -4
- edwh_editorjs-2.0.0b1/scripts/compile_for_pypi.sh +0 -4
- edwh_editorjs-2.0.0b1/tests/test_parser.py +0 -42
- {edwh_editorjs-2.0.0b1 → edwh_editorjs-2.0.0b3}/.github/workflows/build_documentation.yml +0 -0
- {edwh_editorjs-2.0.0b1 → edwh_editorjs-2.0.0b3}/.github/workflows/publish_to_pypi.yml +0 -0
- {edwh_editorjs-2.0.0b1 → edwh_editorjs-2.0.0b3}/.github/workflows/pytest.yml +0 -0
- {edwh_editorjs-2.0.0b1 → edwh_editorjs-2.0.0b3}/.gitignore +0 -0
- /edwh_editorjs-2.0.0b1/requirements.txt → /edwh_editorjs-2.0.0b3/tests/__init__.py +0 -0
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
<!--next-version-placeholder-->
|
|
4
4
|
|
|
5
|
+
## v2.0.0-beta.3 (2024-11-06)
|
|
6
|
+
|
|
7
|
+
### Fix
|
|
8
|
+
|
|
9
|
+
* Various fixes with nested lists ([`52a7773`](https://github.com/educationwarehouse/edwh-editorjs/commit/52a7773470dce3eee6a2d17d46b594551ed043a5))
|
|
10
|
+
|
|
11
|
+
## v2.0.0-beta.2 (2024-11-06)
|
|
12
|
+
|
|
13
|
+
### Feature
|
|
14
|
+
|
|
15
|
+
* Work in progress to do a rebuild based on mdast ([`28c12f1`](https://github.com/educationwarehouse/edwh-editorjs/commit/28c12f1f74c71a995f9f8097f1b26be45f835ad4))
|
|
16
|
+
|
|
5
17
|
## v2.0.0-beta.1 (2024-11-06)
|
|
6
18
|
|
|
7
19
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: edwh-editorjs
|
|
3
|
+
Version: 2.0.0b3
|
|
4
|
+
Summary: EditorJS.py
|
|
5
|
+
Project-URL: Homepage, https://github.com/educationwarehouse/edwh-EditorJS
|
|
6
|
+
Author-email: SKevo <skevo.cw@gmail.com>, Robin van der Noord <robin.vdn@educationwarehouse.nl>
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: bleach,clean,editor,editor.js,html,javascript,json,parser,wysiwyg
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: markdown2
|
|
19
|
+
Requires-Dist: mdast
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: edwh; extra == 'dev'
|
|
22
|
+
Requires-Dist: hatch; extra == 'dev'
|
|
23
|
+
Requires-Dist: su6[all]; extra == 'dev'
|
|
24
|
+
Requires-Dist: types-bleach; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# edwh-editorjs
|
|
28
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.0.0-beta.3"
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mdast to editorjs
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import abc
|
|
6
|
+
import re
|
|
7
|
+
import typing as t
|
|
8
|
+
|
|
9
|
+
from .exceptions import TODO
|
|
10
|
+
from .types import EditorChildData, MDChildNode
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EditorJSBlock(abc.ABC):
|
|
14
|
+
@classmethod
|
|
15
|
+
@abc.abstractmethod
|
|
16
|
+
def to_markdown(cls, data: EditorChildData) -> str: ...
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
@abc.abstractmethod
|
|
20
|
+
def to_json(cls, node: MDChildNode) -> list[dict]: ...
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
@abc.abstractmethod
|
|
24
|
+
def to_text(cls, node: MDChildNode) -> str: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
BLOCKS: dict[str, EditorJSBlock] = {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def block(*names: str):
|
|
31
|
+
def wrapper(cls):
|
|
32
|
+
for name in names:
|
|
33
|
+
BLOCKS[name] = cls
|
|
34
|
+
return cls
|
|
35
|
+
|
|
36
|
+
return wrapper
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def process_styled_content(item: MDChildNode, strict: bool = True) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Processes styled content (e.g., bold, italic) within a list item.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
item: A ChildNode dictionary representing an inline element or text.
|
|
45
|
+
strict: Raise if 'type' is not one defined in 'html_wrappers'
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
A formatted HTML string based on the item type.
|
|
49
|
+
"""
|
|
50
|
+
_type = item.get("type")
|
|
51
|
+
html_wrappers = {
|
|
52
|
+
"text": "{value}",
|
|
53
|
+
"html": "{value}",
|
|
54
|
+
"emphasis": "<i>{value}</i>",
|
|
55
|
+
"strong": "<b>{value}</b>",
|
|
56
|
+
"strongEmphasis": "<b><i>{value}</i></b>",
|
|
57
|
+
"link": '<a href="{url}">{value}</a>',
|
|
58
|
+
"inlineCode": '<code class="inline-code">{value}</code>',
|
|
59
|
+
# todo: <mark>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if _type in BLOCKS:
|
|
63
|
+
return BLOCKS[_type].to_text(item)
|
|
64
|
+
|
|
65
|
+
if strict and _type not in html_wrappers:
|
|
66
|
+
raise ValueError(f"Unsupported type {_type} in paragraph")
|
|
67
|
+
|
|
68
|
+
# Process children recursively if they exist, otherwise use the direct value
|
|
69
|
+
if children := item.get("children"):
|
|
70
|
+
value = "".join(process_styled_content(child) for child in children)
|
|
71
|
+
else:
|
|
72
|
+
value = item.get("value", "")
|
|
73
|
+
|
|
74
|
+
template = html_wrappers.get(_type, "{value}")
|
|
75
|
+
return template.format(
|
|
76
|
+
value=value, url=item.get("url", ""), caption=item.get("caption", "")
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def default_to_text(node: MDChildNode):
|
|
81
|
+
return "".join(
|
|
82
|
+
process_styled_content(child) for child in node.get("children", [])
|
|
83
|
+
) or process_styled_content(node)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@block("heading", "header")
|
|
87
|
+
class HeadingBlock(EditorJSBlock):
|
|
88
|
+
@classmethod
|
|
89
|
+
def to_markdown(cls, data: EditorChildData) -> str:
|
|
90
|
+
level = data.get("level", 1)
|
|
91
|
+
text = data.get("text", "")
|
|
92
|
+
|
|
93
|
+
if not (1 <= level <= 6):
|
|
94
|
+
raise ValueError("Header level must be between 1 and 6.")
|
|
95
|
+
|
|
96
|
+
return f"{'#' * level} {text}\n"
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def to_json(cls, node: MDChildNode) -> list[dict]:
|
|
100
|
+
"""
|
|
101
|
+
Converts a Markdown header block into structured block data.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
node: A RootNode dictionary with 'depth' and 'children'.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
A ChildNode dictionary representing the header data, or None if no children exist.
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
ValueError: If an unsupported heading depth is provided.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
depth = node.get("depth")
|
|
114
|
+
|
|
115
|
+
if depth is None or not (1 <= depth <= 6):
|
|
116
|
+
raise ValueError("Heading depth must be between 1 and 6.")
|
|
117
|
+
|
|
118
|
+
return [{"data": {"level": depth, "text": cls.to_text(node)}, "type": "header"}]
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def to_text(cls, node: MDChildNode) -> str:
|
|
122
|
+
children = node.get("children", [])
|
|
123
|
+
if children is None or not len(children) == 1:
|
|
124
|
+
raise ValueError("Header block must have exactly one child element")
|
|
125
|
+
child = children[0]
|
|
126
|
+
return child.get("value", "")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@block("paragraph")
|
|
130
|
+
class ParagraphBlock(EditorJSBlock):
|
|
131
|
+
@classmethod
|
|
132
|
+
def to_markdown(cls, data: EditorChildData) -> str:
|
|
133
|
+
text = data.get("text", "")
|
|
134
|
+
return f"{text}\n"
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def to_json(cls, node: MDChildNode) -> list[dict]:
|
|
138
|
+
result = []
|
|
139
|
+
current_text = ""
|
|
140
|
+
|
|
141
|
+
for child in node.get("children"):
|
|
142
|
+
_type = child.get("type")
|
|
143
|
+
if _type == "image":
|
|
144
|
+
if current_text:
|
|
145
|
+
result.append({"data": {"text": current_text}, "type": "paragraph"})
|
|
146
|
+
current_text = ""
|
|
147
|
+
|
|
148
|
+
result.extend(ImageBlock.to_json(child))
|
|
149
|
+
else:
|
|
150
|
+
current_text += cls.to_text(child)
|
|
151
|
+
|
|
152
|
+
# final text after image:
|
|
153
|
+
if current_text:
|
|
154
|
+
result.append({"data": {"text": current_text}, "type": "paragraph"})
|
|
155
|
+
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def to_text(cls, node: MDChildNode) -> str:
|
|
160
|
+
return default_to_text(node)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@block("list")
|
|
164
|
+
class ListBlock(EditorJSBlock):
|
|
165
|
+
@classmethod
|
|
166
|
+
def to_markdown(cls, data: EditorChildData) -> str:
|
|
167
|
+
style = data.get("style", "unordered")
|
|
168
|
+
items = data.get("items", [])
|
|
169
|
+
|
|
170
|
+
def parse_items(subitems: list[dict[str, t.Any]], depth: int = 0) -> str:
|
|
171
|
+
markdown_items = []
|
|
172
|
+
for index, item in enumerate(subitems):
|
|
173
|
+
prefix = f"{index + 1}." if style == "ordered" else "-"
|
|
174
|
+
line = f"{'\t' * depth}{prefix} {item['content']}"
|
|
175
|
+
markdown_items.append(line)
|
|
176
|
+
|
|
177
|
+
# Recurse if there are nested items
|
|
178
|
+
if item.get("items"):
|
|
179
|
+
markdown_items.append(parse_items(item["items"], depth + 1))
|
|
180
|
+
|
|
181
|
+
return "\n".join(markdown_items)
|
|
182
|
+
|
|
183
|
+
return "\n" + parse_items(items) + "\n"
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def to_json(cls, node: MDChildNode) -> list[dict]:
|
|
187
|
+
"""
|
|
188
|
+
Converts a Markdown list block with nested items and styling into structured block data.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
node: A RootNode dictionary with 'ordered' and 'children'.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
A dictionary representing the structured list data with 'items' and 'style'.
|
|
195
|
+
"""
|
|
196
|
+
items = []
|
|
197
|
+
# checklists are not supported (well) by mdast
|
|
198
|
+
# so we detect it ourselves:
|
|
199
|
+
could_be_checklist = True
|
|
200
|
+
|
|
201
|
+
def is_checklist(value: str) -> bool:
|
|
202
|
+
return value.strip().startswith(("[ ]", "[x]"))
|
|
203
|
+
|
|
204
|
+
for child in node["children"]:
|
|
205
|
+
content = ""
|
|
206
|
+
subitems = []
|
|
207
|
+
# child can have content and/or items
|
|
208
|
+
for grandchild in child["children"]:
|
|
209
|
+
_type = grandchild.get("type", "")
|
|
210
|
+
if _type == "paragraph":
|
|
211
|
+
subcontent = ParagraphBlock.to_text(grandchild)
|
|
212
|
+
could_be_checklist = could_be_checklist and is_checklist(subcontent)
|
|
213
|
+
content += "" + subcontent
|
|
214
|
+
elif _type == "list":
|
|
215
|
+
could_be_checklist = False
|
|
216
|
+
subitems.extend(ListBlock.to_json(grandchild)[0]["data"]["items"])
|
|
217
|
+
else:
|
|
218
|
+
raise ValueError(f"Unsupported type {_type} in list")
|
|
219
|
+
|
|
220
|
+
items.append(
|
|
221
|
+
{
|
|
222
|
+
"content": content,
|
|
223
|
+
"items": subitems,
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# todo: detect 'checklist':
|
|
228
|
+
"""
|
|
229
|
+
type: checklist
|
|
230
|
+
data: {items: [{text: "a", checked: false}, {text: "b", checked: false}, {text: "c", checked: true},…]}
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
if could_be_checklist:
|
|
234
|
+
return [
|
|
235
|
+
{
|
|
236
|
+
"type": "checklist",
|
|
237
|
+
"data": {
|
|
238
|
+
"items": [
|
|
239
|
+
{
|
|
240
|
+
"text": x["content"]
|
|
241
|
+
.removeprefix("[ ] ")
|
|
242
|
+
.removeprefix("[x] "),
|
|
243
|
+
"checked": x["content"].startswith("[x]"),
|
|
244
|
+
}
|
|
245
|
+
for x in items
|
|
246
|
+
]
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
else:
|
|
251
|
+
return [
|
|
252
|
+
{
|
|
253
|
+
"data": {
|
|
254
|
+
"items": items,
|
|
255
|
+
"style": "ordered" if node.get("ordered") else "unordered",
|
|
256
|
+
},
|
|
257
|
+
"type": "list",
|
|
258
|
+
}
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
@classmethod
|
|
262
|
+
def to_text(cls, node: MDChildNode) -> str:
|
|
263
|
+
return ""
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@block("checklist")
|
|
267
|
+
class ChecklistBlock(ListBlock):
|
|
268
|
+
@classmethod
|
|
269
|
+
def to_markdown(cls, data: EditorChildData) -> str:
|
|
270
|
+
markdown_items = []
|
|
271
|
+
|
|
272
|
+
for item in data.get("items", []):
|
|
273
|
+
text = item.get("text", "").strip()
|
|
274
|
+
char = "x" if item.get("checked", False) else " "
|
|
275
|
+
markdown_items.append(f"- [{char}] {text}")
|
|
276
|
+
|
|
277
|
+
return "\n" + "\n".join(markdown_items) + "\n"
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@block("thematicBreak", "delimiter")
|
|
281
|
+
class DelimiterBlock(EditorJSBlock):
|
|
282
|
+
@classmethod
|
|
283
|
+
def to_markdown(cls, data: EditorChildData) -> str:
|
|
284
|
+
return "***\n"
|
|
285
|
+
|
|
286
|
+
@classmethod
|
|
287
|
+
def to_json(cls, node: MDChildNode) -> list[dict]:
|
|
288
|
+
return [
|
|
289
|
+
{
|
|
290
|
+
"type": "delimiter",
|
|
291
|
+
"data": {},
|
|
292
|
+
}
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
@classmethod
|
|
296
|
+
def to_text(cls, node: MDChildNode) -> str:
|
|
297
|
+
return ""
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@block("code")
|
|
301
|
+
class CodeBlock(EditorJSBlock):
|
|
302
|
+
@classmethod
|
|
303
|
+
def to_markdown(cls, data: EditorChildData) -> str:
|
|
304
|
+
code = data.get("code", "")
|
|
305
|
+
return f"```\n" f"{code}" f"\n```\n"
|
|
306
|
+
|
|
307
|
+
@classmethod
|
|
308
|
+
def to_json(cls, node: MDChildNode) -> list[dict]:
|
|
309
|
+
return [
|
|
310
|
+
{
|
|
311
|
+
"data": {"code": cls.to_text(node)},
|
|
312
|
+
"type": "code",
|
|
313
|
+
}
|
|
314
|
+
]
|
|
315
|
+
|
|
316
|
+
@classmethod
|
|
317
|
+
def to_text(cls, node: MDChildNode) -> str:
|
|
318
|
+
return node.get("value", "")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@block("image")
|
|
322
|
+
class ImageBlock(EditorJSBlock):
|
|
323
|
+
@classmethod
|
|
324
|
+
def to_markdown(cls, data: EditorChildData) -> str:
|
|
325
|
+
url = data.get("url", "") or data.get("file", {}).get("url", "")
|
|
326
|
+
caption = data.get("caption", "")
|
|
327
|
+
return f"""\n"""
|
|
328
|
+
|
|
329
|
+
@classmethod
|
|
330
|
+
def to_json(cls, node: MDChildNode) -> list[dict]:
|
|
331
|
+
return [
|
|
332
|
+
{
|
|
333
|
+
"type": "image",
|
|
334
|
+
"data": {
|
|
335
|
+
"caption": cls.to_text(node),
|
|
336
|
+
"file": {"url": node.get("url")},
|
|
337
|
+
},
|
|
338
|
+
}
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
@classmethod
|
|
342
|
+
def to_text(cls, node: MDChildNode) -> str:
|
|
343
|
+
return node.get("alt") or node.get("caption") or ""
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@block("blockquote", "quote")
|
|
347
|
+
class QuoteBlock(EditorJSBlock):
|
|
348
|
+
re_cite = re.compile(r"<cite>(.+?)<\/cite>")
|
|
349
|
+
|
|
350
|
+
@classmethod
|
|
351
|
+
def to_markdown(cls, data: EditorChildData) -> str:
|
|
352
|
+
text = data.get("text", "")
|
|
353
|
+
result = f"> {text}\n"
|
|
354
|
+
if caption := data.get("caption", ""):
|
|
355
|
+
result += f"> <cite>{caption}</cite>\n"
|
|
356
|
+
return result
|
|
357
|
+
|
|
358
|
+
@classmethod
|
|
359
|
+
def to_json(cls, node: MDChildNode) -> list[dict]:
|
|
360
|
+
caption = ""
|
|
361
|
+
text = cls.to_text(node).replace("\n", "<br/>\n")
|
|
362
|
+
|
|
363
|
+
if cite := re.search(cls.re_cite, text):
|
|
364
|
+
# Capture the value of the first group
|
|
365
|
+
caption = cite.group(1)
|
|
366
|
+
# Remove the <cite>...</cite> tags from the text
|
|
367
|
+
text = re.sub(cls.re_cite, "", text)
|
|
368
|
+
|
|
369
|
+
return [
|
|
370
|
+
{
|
|
371
|
+
"data": {
|
|
372
|
+
"alignment": "left",
|
|
373
|
+
"caption": caption,
|
|
374
|
+
"text": text,
|
|
375
|
+
},
|
|
376
|
+
"type": "quote",
|
|
377
|
+
}
|
|
378
|
+
]
|
|
379
|
+
|
|
380
|
+
@classmethod
|
|
381
|
+
def to_text(cls, node: MDChildNode) -> str:
|
|
382
|
+
return default_to_text(node)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import textwrap
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
import markdown2
|
|
6
|
+
import mdast
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
|
|
9
|
+
from .blocks import BLOCKS
|
|
10
|
+
from .exceptions import TODO
|
|
11
|
+
from .helpers import unix_timestamp
|
|
12
|
+
from .types import MDRootNode
|
|
13
|
+
|
|
14
|
+
EDITORJS_VERSION = "2.30.6"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EditorJS:
|
|
18
|
+
# internal representation is mdast, because we can convert to other types
|
|
19
|
+
_mdast: MDRootNode
|
|
20
|
+
|
|
21
|
+
def __init__(self, _mdast: str | dict, extras: list = ("task_list", "fenced-code-blocks")):
|
|
22
|
+
if not isinstance(_mdast, str | dict):
|
|
23
|
+
raise TypeError("Only `str` or `dict` is supported!")
|
|
24
|
+
|
|
25
|
+
self._mdast = t.cast(
|
|
26
|
+
MDRootNode, json.loads(_mdast) if isinstance(_mdast, str) else _mdast
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
self._md = markdown2.Markdown(extras=extras) # todo: striketrough, table, ?
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_json(cls, data: str | dict) -> Self:
|
|
33
|
+
"""
|
|
34
|
+
Load from EditorJS JSON Blocks
|
|
35
|
+
"""
|
|
36
|
+
data = data if isinstance(data, dict) else json.loads(data)
|
|
37
|
+
markdown_items = []
|
|
38
|
+
for child in data["blocks"]:
|
|
39
|
+
_type = child["type"]
|
|
40
|
+
if not (block := BLOCKS.get(_type)):
|
|
41
|
+
raise TypeError(f"Unsupported block type `{_type}`")
|
|
42
|
+
|
|
43
|
+
markdown_items.append(block.to_markdown(child.get("data", {})))
|
|
44
|
+
|
|
45
|
+
markdown = "".join(markdown_items)
|
|
46
|
+
return cls.from_markdown(markdown)
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_markdown(cls, data: str) -> Self:
|
|
50
|
+
"""
|
|
51
|
+
Load from markdown string
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
return cls(mdast.md_to_json(data))
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_mdast(cls, data: str | dict) -> Self:
|
|
58
|
+
"""
|
|
59
|
+
Existing mdast representation
|
|
60
|
+
"""
|
|
61
|
+
return cls(data)
|
|
62
|
+
|
|
63
|
+
def to_json(self) -> str:
|
|
64
|
+
"""
|
|
65
|
+
Export EditorJS JSON Blocks
|
|
66
|
+
"""
|
|
67
|
+
# logic based on https://github.com/carrara88/editorjs-md-parser/blob/main/src/MarkdownImporter.js
|
|
68
|
+
blocks = []
|
|
69
|
+
for child in self._mdast["children"]:
|
|
70
|
+
_type = child["type"]
|
|
71
|
+
if not (block := BLOCKS.get(_type)):
|
|
72
|
+
raise TypeError(f"Unsupported block type `{_type}`")
|
|
73
|
+
|
|
74
|
+
blocks.extend(block.to_json(child))
|
|
75
|
+
|
|
76
|
+
data = {"time": unix_timestamp(), "blocks": blocks, "version": EDITORJS_VERSION}
|
|
77
|
+
|
|
78
|
+
return json.dumps(data)
|
|
79
|
+
|
|
80
|
+
def to_markdown(self) -> str:
|
|
81
|
+
"""
|
|
82
|
+
Export Markdown string
|
|
83
|
+
"""
|
|
84
|
+
md = mdast.json_to_md(self.to_mdast())
|
|
85
|
+
# idk why this happens:
|
|
86
|
+
md = md.replace(r"\[ ]", "[ ]")
|
|
87
|
+
md = md.replace(r"\[x]", "[x]")
|
|
88
|
+
return md
|
|
89
|
+
|
|
90
|
+
def to_mdast(self) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Export mdast representation
|
|
93
|
+
"""
|
|
94
|
+
return json.dumps(self._mdast)
|
|
95
|
+
|
|
96
|
+
def to_html(self) -> str:
|
|
97
|
+
"""
|
|
98
|
+
Export HTML string
|
|
99
|
+
"""
|
|
100
|
+
md = self.to_markdown()
|
|
101
|
+
return self._md.convert(md)
|
|
102
|
+
|
|
103
|
+
def __repr__(self):
|
|
104
|
+
md = self.to_markdown()
|
|
105
|
+
md = md.replace("\n", "\\n")
|
|
106
|
+
return f"EditorJS({md})"
|
|
107
|
+
|
|
108
|
+
def __str__(self):
|
|
109
|
+
return self.to_markdown()
|
|
110
|
+
|
|
111
|
+
# def __eq__(self, other: Self) -> bool:
|
|
112
|
+
# a = self.to_markdown()
|
|
113
|
+
# b = other.to_markdown()
|
|
114
|
+
#
|
|
115
|
+
# remove = string.punctuation + string.whitespace
|
|
116
|
+
# return a.translate(remove) == b.translate(remove)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MDPosition(t.TypedDict):
|
|
5
|
+
line: int
|
|
6
|
+
column: int
|
|
7
|
+
offset: int
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MDPositionRange(t.TypedDict):
|
|
11
|
+
start: MDPosition
|
|
12
|
+
end: MDPosition
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MDChildNode(t.TypedDict, total=False):
|
|
16
|
+
type: str # General identifier for node types
|
|
17
|
+
children: list["MDChildNode"] # Recursive children of any node type
|
|
18
|
+
position: MDPositionRange
|
|
19
|
+
value: str # Optional, for nodes like text that hold a value
|
|
20
|
+
depth: int # Optional, for nodes like headings that have a depth
|
|
21
|
+
url: t.NotRequired[str]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MDRootNode(t.TypedDict):
|
|
25
|
+
type: t.Literal["root"] # Constrains to 'root' for the root node
|
|
26
|
+
children: list[MDChildNode] # Allows any ChildNode type in children
|
|
27
|
+
position: MDPositionRange
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class EditorChildData(t.TypedDict, total=False):
|
|
31
|
+
text: str
|
|
32
|
+
items: list["EditorChildNode"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class EditorChildNode(t.TypedDict):
|
|
36
|
+
type: str
|
|
37
|
+
data: EditorChildData
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class EditorRootNode(t.TypedDict):
|
|
41
|
+
time: int
|
|
42
|
+
blocks: list[EditorChildNode]
|
|
43
|
+
version: str
|
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
name = "edwh-editorjs"
|
|
7
7
|
dynamic = ["version"]
|
|
8
8
|
|
|
9
|
-
description = "
|
|
9
|
+
description = "EditorJS.py"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
authors = [
|
|
12
12
|
{ name = "SKevo", email = "skevo.cw@gmail.com" },
|
|
@@ -26,7 +26,9 @@ classifiers = [
|
|
|
26
26
|
requires-python = ">=3.10"
|
|
27
27
|
|
|
28
28
|
dependencies = [
|
|
29
|
-
"bleach"
|
|
29
|
+
# "bleach",
|
|
30
|
+
"mdast",
|
|
31
|
+
"markdown2",
|
|
30
32
|
]
|
|
31
33
|
|
|
32
34
|
[project.optional-dependencies]
|
|
@@ -42,23 +44,23 @@ Homepage = "https://github.com/educationwarehouse/edwh-EditorJS"
|
|
|
42
44
|
|
|
43
45
|
[tool.semantic_release]
|
|
44
46
|
branch = "master"
|
|
45
|
-
version_variable = "
|
|
47
|
+
version_variable = "editorjs/__about__.py:__version__"
|
|
46
48
|
change_log = "CHANGELOG.md"
|
|
47
49
|
upload_to_repository = false
|
|
48
50
|
upload_to_release = false
|
|
49
51
|
build_command = "hatch build"
|
|
50
52
|
|
|
51
53
|
[tool.hatch.version]
|
|
52
|
-
path = "
|
|
54
|
+
path = "editorjs/__about__.py"
|
|
53
55
|
|
|
54
56
|
[tool.hatch.build.targets.wheel]
|
|
55
|
-
packages = ["
|
|
57
|
+
packages = ["editorjs"]
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
[tool.setuptools.packages.find]
|
|
59
|
-
include = ["
|
|
61
|
+
include = ["editorjs"]
|
|
60
62
|
exclude = ["tests"]
|
|
61
63
|
|
|
62
64
|
[tool.su6]
|
|
63
|
-
directory = "
|
|
65
|
+
directory = "editorjs"
|
|
64
66
|
stop-after-first-failure = true
|