edwh-editorjs 2.0.0b3__tar.gz → 2.0.0b5__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.
Files changed (32) hide show
  1. {edwh_editorjs-2.0.0b3 → edwh_editorjs-2.0.0b5}/CHANGELOG.md +12 -0
  2. {edwh_editorjs-2.0.0b3 → edwh_editorjs-2.0.0b5}/PKG-INFO +1 -1
  3. edwh_editorjs-2.0.0b5/editorjs/__about__.py +1 -0
  4. {edwh_editorjs-2.0.0b3 → edwh_editorjs-2.0.0b5}/editorjs/blocks.py +294 -6
  5. {edwh_editorjs-2.0.0b3 → edwh_editorjs-2.0.0b5}/editorjs/core.py +7 -4
  6. edwh_editorjs-2.0.0b5/htmlcov/.gitignore +2 -0
  7. edwh_editorjs-2.0.0b5/htmlcov/class_index.html +259 -0
  8. edwh_editorjs-2.0.0b5/htmlcov/favicon_32_cb_58284776.png +0 -0
  9. edwh_editorjs-2.0.0b5/htmlcov/function_index.html +395 -0
  10. edwh_editorjs-2.0.0b5/htmlcov/index.html +132 -0
  11. edwh_editorjs-2.0.0b5/htmlcov/keybd_closed_cb_ce680311.png +0 -0
  12. edwh_editorjs-2.0.0b5/htmlcov/status.json +1 -0
  13. edwh_editorjs-2.0.0b5/htmlcov/style_cb_8e611ae1.css +337 -0
  14. edwh_editorjs-2.0.0b5/htmlcov/z_a93c8aeb4b8fa1f9___init___py.html +125 -0
  15. edwh_editorjs-2.0.0b5/htmlcov/z_a93c8aeb4b8fa1f9_blocks_py.html +406 -0
  16. edwh_editorjs-2.0.0b5/htmlcov/z_a93c8aeb4b8fa1f9_exceptions_py.html +116 -0
  17. edwh_editorjs-2.0.0b5/htmlcov/z_a93c8aeb4b8fa1f9_parser_py.html +172 -0
  18. edwh_editorjs-2.0.0b5/tests/test_core.py +112 -0
  19. edwh_editorjs-2.0.0b3/editorjs/__about__.py +0 -1
  20. edwh_editorjs-2.0.0b3/tests/test_core.py +0 -74
  21. {edwh_editorjs-2.0.0b3 → edwh_editorjs-2.0.0b5}/.github/workflows/build_documentation.yml +0 -0
  22. {edwh_editorjs-2.0.0b3 → edwh_editorjs-2.0.0b5}/.github/workflows/publish_to_pypi.yml +0 -0
  23. {edwh_editorjs-2.0.0b3 → edwh_editorjs-2.0.0b5}/.github/workflows/pytest.yml +0 -0
  24. {edwh_editorjs-2.0.0b3 → edwh_editorjs-2.0.0b5}/.gitignore +0 -0
  25. {edwh_editorjs-2.0.0b3 → edwh_editorjs-2.0.0b5}/LICENSE +0 -0
  26. {edwh_editorjs-2.0.0b3 → edwh_editorjs-2.0.0b5}/README.md +0 -0
  27. {edwh_editorjs-2.0.0b3 → edwh_editorjs-2.0.0b5}/editorjs/__init__.py +0 -0
  28. {edwh_editorjs-2.0.0b3 → edwh_editorjs-2.0.0b5}/editorjs/exceptions.py +0 -0
  29. {edwh_editorjs-2.0.0b3 → edwh_editorjs-2.0.0b5}/editorjs/helpers.py +0 -0
  30. {edwh_editorjs-2.0.0b3 → edwh_editorjs-2.0.0b5}/editorjs/types.py +0 -0
  31. {edwh_editorjs-2.0.0b3 → edwh_editorjs-2.0.0b5}/pyproject.toml +0 -0
  32. {edwh_editorjs-2.0.0b3 → edwh_editorjs-2.0.0b5}/tests/__init__.py +0 -0
@@ -2,6 +2,18 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v2.0.0-beta.5 (2024-11-07)
6
+
7
+ ### Feature
8
+
9
+ * Add markdown2 extra to deal with custom `<editorjs />` blocks ([`31d8647`](https://github.com/educationwarehouse/edwh-editorjs/commit/31d8647b7275e245dabf27a99c43d400217705be))
10
+
11
+ ## v2.0.0-beta.4 (2024-11-07)
12
+
13
+ ### Feature
14
+
15
+ * Support (basic) tables, custom editorjs blocks like linkTool via <editorjs> ([`a6dfadf`](https://github.com/educationwarehouse/edwh-editorjs/commit/a6dfadf21ec008fe714704a056b9ffec751d731c))
16
+
5
17
  ## v2.0.0-beta.3 (2024-11-06)
6
18
 
7
19
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: edwh-editorjs
3
- Version: 2.0.0b3
3
+ Version: 2.0.0b5
4
4
  Summary: EditorJS.py
5
5
  Project-URL: Homepage, https://github.com/educationwarehouse/edwh-EditorJS
6
6
  Author-email: SKevo <skevo.cw@gmail.com>, Robin van der Noord <robin.vdn@educationwarehouse.nl>
@@ -0,0 +1 @@
1
+ __version__ = "2.0.0-beta.5"
@@ -5,6 +5,10 @@ mdast to editorjs
5
5
  import abc
6
6
  import re
7
7
  import typing as t
8
+ from html.parser import HTMLParser
9
+ from urllib.parse import urlparse
10
+
11
+ import markdown2
8
12
 
9
13
  from .exceptions import TODO
10
14
  from .types import EditorChildData, MDChildNode
@@ -56,7 +60,7 @@ def process_styled_content(item: MDChildNode, strict: bool = True) -> str:
56
60
  "strongEmphasis": "<b><i>{value}</i></b>",
57
61
  "link": '<a href="{url}">{value}</a>',
58
62
  "inlineCode": '<code class="inline-code">{value}</code>',
59
- # todo: <mark>
63
+ # todo: <mark>, linktool
60
64
  }
61
65
 
62
66
  if _type in BLOCKS:
@@ -138,16 +142,52 @@ class ParagraphBlock(EditorJSBlock):
138
142
  result = []
139
143
  current_text = ""
140
144
 
141
- for child in node.get("children"):
145
+ skip = 0
146
+ nodes = node.get("children", [])
147
+
148
+ for idx, child in enumerate(nodes):
149
+ if skip:
150
+ skip -= 1
151
+ continue
152
+
142
153
  _type = child.get("type")
143
- if _type == "image":
154
+
155
+ # deal with custom types
156
+ if _type == "html" and child.get("value", "").startswith("<editorjs"):
157
+ # special type, e.g. <editorjs type="linkTool" href=...>...</editorjs>
158
+
159
+ if child.get("value", "").endswith("/>"):
160
+ # self-closing
161
+ result.append(EditorJSCustom.to_json(node))
162
+ continue
163
+ else:
164
+ # <editorjs>something</editorjs> = 3 children
165
+ result.extend(
166
+ EditorJSCustom.to_json({"children": nodes[idx : idx + 2]})
167
+ )
168
+
169
+ skip = 2
170
+ continue
171
+
172
+ elif _type == "image":
144
173
  if current_text:
145
174
  result.append({"data": {"text": current_text}, "type": "paragraph"})
146
175
  current_text = ""
147
176
 
148
177
  result.extend(ImageBlock.to_json(child))
149
178
  else:
150
- current_text += cls.to_text(child)
179
+ child_text = cls.to_text(child)
180
+ _child_text = child_text.strip()
181
+ if _child_text.startswith("|") and _child_text.endswith("|"):
182
+ # note: this just supports text-only tables.
183
+ # tables with more complex elements break into multiple children.
184
+ # and mdast DOES support converting into a Table/TableCell structure
185
+ # via the GFM exttension
186
+ # but their default mdast->md converter does NOT implement these functionalities.
187
+ result.extend(TableBlock.to_json(child))
188
+ continue
189
+
190
+ current_text += child_text
151
191
 
152
192
  # final text after image:
153
193
  if current_text:
@@ -180,7 +220,7 @@ class ListBlock(EditorJSBlock):
180
220
 
181
221
  return "\n".join(markdown_items)
182
222
 
183
- return "\n" + parse_items(items) + "\n"
223
+ return "\n" + parse_items(items) + "\n\n"
184
224
 
185
225
  @classmethod
186
226
  def to_json(cls, node: MDChildNode) -> list[dict]:
@@ -274,7 +314,7 @@ class ChecklistBlock(ListBlock):
274
314
  char = "x" if item.get("checked", False) else " "
275
315
  markdown_items.append(f"- [{char}] {text}")
276
316
 
277
- return "\n" + "\n".join(markdown_items) + "\n"
317
+ return "\n" + "\n".join(markdown_items) + "\n\n"
278
318
 
279
319
 
280
320
  @block("thematicBreak", "delimiter")
@@ -380,3 +420,251 @@ class QuoteBlock(EditorJSBlock):
380
420
  @classmethod
381
421
  def to_text(cls, node: MDChildNode) -> str:
382
422
  return default_to_text(node)
423
+
424
+
425
+ @block("raw")
426
+ class RawBlock(EditorJSBlock):
427
+
428
+ @classmethod
429
+ def to_markdown(cls, data: EditorChildData) -> str:
430
+ return data.get("html", "")
431
+
432
+ @classmethod
433
+ def to_json(cls, node: MDChildNode) -> list[dict]:
434
+ raise TODO(node)
435
+
436
+ @classmethod
437
+ def to_text(cls, node: MDChildNode) -> str:
438
+ raise TODO(node)
439
+
440
+
441
+ @block("table")
442
+ class TableBlock(EditorJSBlock):
443
+
444
+ @classmethod
445
+ def to_markdown(cls, data: EditorChildData) -> str:
446
+ """
447
+ | Script | Interpreter | User | System | |
448
+ |--------|-------------|------|--------|---|
449
+ | | | | | |
450
+ | | | | | |
451
+ | | | | | |
452
+ """
453
+
454
+ table = ""
455
+ rows = data.get("content", [])
456
+
457
+ # Add an empty header row if no headings are provided
458
+ if not data.get("withHeadings", False) and rows:
459
+ table += "| " + " | ".join([""] * len(rows[0])) + " |\n"
460
+ table += "|" + " - |" * len(rows[0]) + "\n"
461
+
462
+ # Populate rows
463
+ for idx, tr in enumerate(rows):
464
+ table += "| " + " | ".join(tr) + " |\n"
465
+
466
+ # Add separator if headings are enabled and it's the first row
467
+ if not idx and data.get("withHeadings", False):
468
+ table += "|" + " - |" * len(tr) + "\n"
469
+
470
+ return f"\n{table}\n"
471
+
472
+ @classmethod
473
+ def to_json(cls, node: MDChildNode) -> list[dict]:
474
+ # content":[["Yeah","Okay"],["<i>1</i>","<code class=\"inline-code\">2</code>"]]}}]
475
+ table = []
476
+ with_headings = False
477
+
478
+ # first row is headings or empty. If not empty, withHeadings is True
479
+ # second row must be ignored
480
+ for idx, row in enumerate(node.get("value", "").strip().split("\n")):
481
+ tr = [_.strip() for _ in row.split("|")[1:-1]]
482
+ if not idx:
483
+ # first
484
+ if any(tr):
485
+ with_headings = True
486
+ table.append(tr)
487
+
488
+ elif idx == 1:
489
+ continue
490
+ else:
491
+ table.append(tr)
492
+
493
+ return [
494
+ {
495
+ "type": "table",
496
+ "content": table,
497
+ "withHeadings": with_headings,
498
+ }
499
+ ]
500
+
501
+ @classmethod
502
+ def to_text(cls, node: MDChildNode) -> str:
503
+ raise TODO(node)
504
+
505
+
506
+ @block("linkTool")
507
+ class LinkBlock(EditorJSBlock):
508
+ @classmethod
509
+ def to_markdown(cls, data: EditorChildData) -> str:
510
+ link = data.get("link", "")
511
+ meta = data.get("meta", {})
512
+ title = meta.get("title", "")
513
+ description = meta.get("description", "")
514
+ image = meta.get("image", {}).get("url", "")
515
+ return f"""<editorjs type="linkTool" href="{link}" title="{title}" image="{image}">{description}</editorjs>"""
516
+
517
+ @classmethod
518
+ def to_json(cls, node: MDChildNode) -> list[dict]:
519
+ return [
520
+ {
521
+ "type": "linkTool",
522
+ "data": {
523
+ "link": node.get("href", ""),
524
+ "meta": {
525
+ "title": node.get("title", ""),
526
+ "description": node.get("body", ""),
527
+ "image": {
528
+ "url": node.get("image", ""),
529
+ },
530
+ },
531
+ },
532
+ }
533
+ ]
534
+
535
+ @classmethod
536
+ def to_text(cls, node: MDChildNode) -> str:
537
+ url = node.get("href", "")
538
+ image = node.get("image", "")
539
+ title = node.get("title", "")
540
+ body = node.get("body", "")
541
+ domain = urlparse(url).netloc
542
+
543
+ return f"""
544
+ <div class="link-tool">
545
+ <a class="link-tool__content link-tool__content--rendered" target="_blank"
546
+ rel="nofollow noindex noreferrer" href="{url}">
547
+ <div class="link-tool__image"
548
+ style="background-image: url(&quot;{image}&quot;);"></div>
549
+ <div class="link-tool__title">{title}</div>
550
+ <p class="link-tool__description">{body}</p>
551
+ <span class="link-tool__anchor">{domain}</span>
552
+ </a>
553
+ </div>
554
+ """
555
+
556
+
557
+ @block("attaches")
558
+ class AttachmentBlock(EditorJSBlock):
559
+
560
+ @classmethod
561
+ def to_markdown(cls, data: EditorChildData) -> str:
562
+ file = data.get("file", {}).get("url", "")
563
+ title = data.get("title", "")
564
+ return f"""<editorjs type="attaches" file="{file}">{title}</editorjs>"""
565
+
566
+ @classmethod
567
+ def to_json(cls, node: MDChildNode) -> list[dict]:
568
+ return [
569
+ {
570
+ "type": "attaches",
571
+ "data": {
572
+ "file": {"url": node.get("file", "")},
573
+ "title": node.get("body", ""),
574
+ },
575
+ }
576
+ ]
577
+
578
+ @classmethod
579
+ def to_text(cls, node: MDChildNode) -> str:
580
+ return f"""
581
+ <div class="cdx-attaches cdx-attaches--with-file">
582
+ <div class="cdx-attaches__file-icon">
583
+ <div class="cdx-attaches__file-icon-background">
584
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.3236 8.43554L9.49533 12.1908C9.13119 12.5505 8.93118 13.043 8.9393 13.5598C8.94741 14.0767 9.163 14.5757 9.53862 14.947C9.91424 15.3182 10.4191 15.5314 10.9422 15.5397C11.4653 15.5479 11.9637 15.3504 12.3279 14.9908L16.1562 11.2355C16.8845 10.5161 17.2845 9.53123 17.2682 8.4975C17.252 7.46376 16.8208 6.46583 16.0696 5.72324C15.3184 4.98066 14.3086 4.55425 13.2624 4.53782C12.2162 4.52138 11.2193 4.91627 10.4911 5.63562L6.66277 9.39093C5.57035 10.4699 4.97032 11.9473 4.99467 13.4979C5.01903 15.0485 5.66578 16.5454 6.79264 17.6592C7.9195 18.7731 9.43417 19.4127 11.0034 19.4374C12.5727 19.462 14.068 18.8697 15.1604 17.7907L18.9887 14.0354"></path></svg>
585
+ </div>
586
+ </div>
587
+ <div class="cdx-attaches__file-info">
588
+ <div class="cdx-attaches__title" contenteditable="true" data-placeholder="File title" data-empty="false">
589
+ {node.get("body", "")}
590
+ </div>
591
+ </div>
592
+ <a class="cdx-attaches__download-button" href="{node.get('file', '')}" target="_blank" rel="nofollow noindex noreferrer">
593
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M7 10L11.8586 14.8586C11.9367 14.9367 12.0633 14.9367 12.1414 14.8586L17 10"></path></svg>
594
+ </a>
595
+ </div>
596
+ """
597
+
598
+
599
+ class AttributeParser(HTMLParser):
600
+ def __init__(self):
601
+ super().__init__()
602
+ self.attributes = {}
603
+ self.data = None
604
+
605
+ def handle_starttag(self, tag, attrs):
606
+ # Collect attributes when the tag is encountered
607
+ self.attributes = dict(attrs)
608
+
609
+ def handle_data(self, data):
610
+ self.data = data
611
+
612
+
613
+ class EditorJSCustom(EditorJSBlock, markdown2.Extra):
614
+ """
615
+ Special type of block to deal with custom attributes
616
+ """
617
+
618
+ name = "editorjs"
619
+ order = (), (markdown2.Stage.POSTPROCESS,)
620
+
621
+ @classmethod
622
+ def parse_html(cls, html: str):
623
+ parser = AttributeParser()
624
+ parser.feed(html)
625
+
626
+ return parser.attributes, parser.data
627
+
628
+ @classmethod
629
+ def to_markdown(cls, data: EditorChildData) -> str:
630
+ raise TODO()
631
+
632
+ @classmethod
633
+ def to_json(cls, node: MDChildNode) -> list[dict]:
634
+ html = "".join(_["value"] for _ in node.get("children", []))
635
+ attrs, body = cls.parse_html(html)
636
+ _type = attrs.get("type", "")
637
+ attrs.setdefault("body", body) # only if there is no such attribute yet
638
+
639
+ if not (handler := BLOCKS.get(_type)):
640
+ raise ValueError(f"Unknown custom type {_type}")
641
+
642
+ return handler.to_json(attrs)
643
+
644
+ @classmethod
645
+ def to_text(cls, node: MDChildNode) -> str:
646
+ raise TODO()
647
+
648
+ # markdown2:
649
+ re_short = re.compile(r"<editorjs.*?/>")
650
+ re_long = re.compile(r"<editorjs.*?>.*?</editorjs>")
651
+
652
+ def run(self, text: str) -> str:
653
+ def replace_html(match):
654
+ attrs, body = self.parse_html(match.group())
655
+ _type = attrs.get("type", "")
656
+ attrs.setdefault("body", body) # only if there is no such attribute yet
657
+
658
+ if not (handler := BLOCKS.get(_type)):
659
+ raise ValueError(f"Unknown custom type {_type}")
660
+
661
+ return handler.to_text(attrs)
662
+
663
+ # Substitute using the replacement functions
664
+ text = self.re_long.sub(replace_html, text)
665
+ text = self.re_short.sub(replace_html, text)
666
+
667
+ return text
668
+
669
+
670
+ EditorJSCustom.register()
@@ -1,5 +1,4 @@
1
1
  import json
2
- import textwrap
3
2
  import typing as t
4
3
 
5
4
  import markdown2
@@ -7,7 +6,6 @@ import mdast
7
6
  from typing_extensions import Self
8
7
 
9
8
  from .blocks import BLOCKS
10
- from .exceptions import TODO
11
9
  from .helpers import unix_timestamp
12
10
  from .types import MDRootNode
13
11
 
@@ -18,7 +16,11 @@ class EditorJS:
18
16
  # internal representation is mdast, because we can convert to other types
19
17
  _mdast: MDRootNode
20
18
 
21
- def __init__(self, _mdast: str | dict, extras: list = ("task_list", "fenced-code-blocks")):
19
+ def __init__(
20
+ self,
21
+ _mdast: str | dict,
22
+ extras: list = ("task_list", "fenced-code-blocks", "tables", "editorjs"),
23
+ ):
22
24
  if not isinstance(_mdast, str | dict):
23
25
  raise TypeError("Only `str` or `dict` is supported!")
24
26
 
@@ -26,7 +28,7 @@ class EditorJS:
26
28
  MDRootNode, json.loads(_mdast) if isinstance(_mdast, str) else _mdast
27
29
  )
28
30
 
29
- self._md = markdown2.Markdown(extras=extras) # todo: striketrough, table, ?
31
+ self._md = markdown2.Markdown(extras=extras) # todo: striketrough, ?
30
32
 
31
33
  @classmethod
32
34
  def from_json(cls, data: str | dict) -> Self:
@@ -98,6 +100,7 @@ class EditorJS:
98
100
  Export HTML string
99
101
  """
100
102
  md = self.to_markdown()
103
+ # todo: deal with custom elements like linktool, attaches
101
104
  return self._md.convert(md)
102
105
 
103
106
  def __repr__(self):
@@ -0,0 +1,2 @@
1
+ # Created by coverage.py
2
+ *