edwh-editorjs 2.5.0a4__tar.gz → 2.6.1__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.5.0a4 → edwh_editorjs-2.6.1}/CHANGELOG.md +18 -0
- {edwh_editorjs-2.5.0a4 → edwh_editorjs-2.6.1}/PKG-INFO +3 -2
- edwh_editorjs-2.6.1/blocks-diff.txt +42 -0
- edwh_editorjs-2.6.1/blog.md +86 -0
- edwh_editorjs-2.6.1/debug.py +22 -0
- edwh_editorjs-2.6.1/editorjs/__about__.py +1 -0
- {edwh_editorjs-2.5.0a4 → edwh_editorjs-2.6.1}/editorjs/blocks.py +56 -41
- {edwh_editorjs-2.5.0a4 → edwh_editorjs-2.6.1}/editorjs/core.py +0 -2
- {edwh_editorjs-2.5.0a4 → edwh_editorjs-2.6.1}/pyproject.toml +1 -0
- {edwh_editorjs-2.5.0a4 → edwh_editorjs-2.6.1}/tests/test_core.py +84 -2
- edwh_editorjs-2.5.0a4/editorjs/__about__.py +0 -1
- {edwh_editorjs-2.5.0a4 → edwh_editorjs-2.6.1}/.gitignore +0 -0
- {edwh_editorjs-2.5.0a4 → edwh_editorjs-2.6.1}/README.md +0 -0
- {edwh_editorjs-2.5.0a4 → edwh_editorjs-2.6.1}/editorjs/__init__.py +0 -0
- {edwh_editorjs-2.5.0a4 → edwh_editorjs-2.6.1}/editorjs/exceptions.py +0 -0
- {edwh_editorjs-2.5.0a4 → edwh_editorjs-2.6.1}/editorjs/helpers.py +0 -0
- {edwh_editorjs-2.5.0a4 → edwh_editorjs-2.6.1}/editorjs/types.py +0 -0
- {edwh_editorjs-2.5.0a4 → edwh_editorjs-2.6.1}/tests/__init__.py +0 -0
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
<!--next-version-placeholder-->
|
|
4
4
|
|
|
5
|
+
## v2.6.1 (2026-02-05)
|
|
6
|
+
|
|
7
|
+
### Fix
|
|
8
|
+
|
|
9
|
+
* **paragraph:** Handle nested HTML in alignment tag closing detection ([`7f910e1`](https://github.com/educationwarehouse/edwh-editorjs/commit/7f910e15103fe25ae0e4d1158deecc15d267d926))
|
|
10
|
+
|
|
11
|
+
## v2.6.0 (2026-02-05)
|
|
12
|
+
|
|
13
|
+
### Fix
|
|
14
|
+
|
|
15
|
+
* **custom:** Replace recursive html parser with lxml to only parse outer layer and keep inner HTML intact ([`8a8908c`](https://github.com/educationwarehouse/edwh-editorjs/commit/8a8908c30708573df11f2011b24fda8c4984de21))
|
|
16
|
+
|
|
17
|
+
## v2.5.0 (2025-03-20)
|
|
18
|
+
|
|
19
|
+
### Feature
|
|
20
|
+
|
|
21
|
+
* Improve markdown conversion and image handling in EditorJS blocks ([`80d0fc0`](https://github.com/educationwarehouse/edwh-editorjs/commit/80d0fc00a4e39b279de35998d1a85a37282c58a7))
|
|
22
|
+
|
|
5
23
|
## v2.4.0 (2024-12-02)
|
|
6
24
|
|
|
7
25
|
### Feature
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: edwh-editorjs
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.1
|
|
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>
|
|
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
15
15
|
Requires-Python: >=3.10
|
|
16
16
|
Requires-Dist: html2markdown
|
|
17
17
|
Requires-Dist: humanize
|
|
18
|
+
Requires-Dist: lxml
|
|
18
19
|
Requires-Dist: markdown2
|
|
19
20
|
Requires-Dist: mdast
|
|
20
21
|
Provides-Extra: dev
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
diff --git a/editorjs/blocks.py b/editorjs/blocks.py
|
|
2
|
+
index 9627a82..bc93cee 100644
|
|
3
|
+
--- a/editorjs/blocks.py
|
|
4
|
+
+++ b/editorjs/blocks.py
|
|
5
|
+
@@ -210,14 +210,30 @@ class ParagraphBlock(EditorJSBlock):
|
|
6
|
+
|
|
7
|
+
if child.get("value", "").endswith("/>"):
|
|
8
|
+
# self-closing
|
|
9
|
+
- result.append(EditorJSCustom.to_json(node))
|
|
10
|
+
+ result.append(EditorJSCustom.to_json({"children": [child]}))
|
|
11
|
+
else:
|
|
12
|
+
- # <editorjs>something</editorjs> = 3 children
|
|
13
|
+
- result.extend(
|
|
14
|
+
- EditorJSCustom.to_json({"children": nodes[idx : idx + 2]})
|
|
15
|
+
- )
|
|
16
|
+
-
|
|
17
|
+
- skip = 2
|
|
18
|
+
+ # <editorjs>...</editorjs> may include nested HTML; find the closing tag
|
|
19
|
+
+ end_idx = None
|
|
20
|
+
+ for j, next_child in enumerate(nodes[idx + 1 :], start=idx + 1):
|
|
21
|
+
+ if next_child.get("type") == "html" and next_child.get(
|
|
22
|
+
+ "value", ""
|
|
23
|
+
+ ).strip() == "</editorjs>":
|
|
24
|
+
+ end_idx = j
|
|
25
|
+
+ break
|
|
26
|
+
+
|
|
27
|
+
+ if end_idx is None:
|
|
28
|
+
+ # fallback to previous behavior if tag is malformed
|
|
29
|
+
+ result.extend(
|
|
30
|
+
+ EditorJSCustom.to_json({"children": nodes[idx : idx + 2]})
|
|
31
|
+
+ )
|
|
32
|
+
+ skip = 2
|
|
33
|
+
+ else:
|
|
34
|
+
+ result.extend(
|
|
35
|
+
+ EditorJSCustom.to_json(
|
|
36
|
+
+ {"children": nodes[idx : end_idx + 1]}
|
|
37
|
+
+ )
|
|
38
|
+
+ )
|
|
39
|
+
+ skip = end_idx - idx
|
|
40
|
+
|
|
41
|
+
continue
|
|
42
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Via de pilot **‘Ambtenaar in de Praktijk’** doen Leidse ambtenaren gerichte praktijkervaring op in de stad. Zo loopt Lies Timmering, projectleider van de **Leidse Gelijke Kansen Aanpak**, elke woensdagmorgen mee bij **BuZz**, waar zij te vinden is bij de buurtsoep en meewerkt met bewoners en vrijwilligers. In deze blog deelt Lies haar persoonlijke reflectie op wat deze ervaringen voor haar betekenen en hoe ze helpen om beleid beter te laten aansluiten op wat er écht speelt in de stad.
|
|
2
|
+
|
|
3
|
+
<editorjs type="image" caption="Foto: BuZz Leiden " border="" background="" stretched="1" url="https://py4web.onderwijsindeleidseregio.nl/thumb/upload/af1ac9a1-fd7a-416e-a5dc-6ee97266397d.jpg?hash=075d9622bd25e284eb23ea34a22537dd707d8437" />
|
|
4
|
+
|
|
5
|
+
***
|
|
6
|
+
|
|
7
|
+
Vandaag pakte ik mijn eed en belofte er nog eens bij. Ik beloofde als ambtenaar bij de start van mijn functie ‘plechtig’ het volgende:
|
|
8
|
+
|
|
9
|
+
1. Ik zal de gerechtigheid dienen.
|
|
10
|
+
2. Ik zal trouw zijn aan de grondwet en de overige wetten van het rijk.
|
|
11
|
+
3. Ik zal mij inzetten voor het welzijn en de rechten van alle burgers van Leiden.
|
|
12
|
+
4. Ik zal onpartijdig handelen en de democratische beginselen en procedures respecteren.
|
|
13
|
+
5. Ik ben loyaal ten opzichte van de bestuursorganen van de stad en het door hen vastgestelde beleid.
|
|
14
|
+
6. Ik zal van de overheidsmacht die mij is toevertrouwd geen misbruik maken.
|
|
15
|
+
7. Ik zal zorgvuldig omgaan met informatie.
|
|
16
|
+
8. Ik zal de geloofwaardigheid van het ambt niet schaden.
|
|
17
|
+
9. Ik zal het vertrouwen, dat de burger in mij mag stellen, niet beschamen.
|
|
18
|
+
10. Ik zal mij een zelfstandig oordeel vormen over de morele juistheid van mijn handelen.
|
|
19
|
+
|
|
20
|
+
Tevens verklaar ik dat ik bekend ben met de gedragscode voor ambtenaren van de gemeente Leiden en deze als leidraad zal hanteren.
|
|
21
|
+
|
|
22
|
+
**Dat verklaar en beloof ik!**
|
|
23
|
+
|
|
24
|
+
Toen ik mocht starten bij de gemeente had ik weinig beeld bij wat een ambtenaar nou precies deed. Wat het betekende om te werken voor een gemeente. Mensen zeiden me: “Bij de gemeente, jij? Dat gaat jou toch allemaal veel te traag, dat is niks voor jou?!” Maar de inhoud van de functie sprak me ontzettend aan, het team waarin ik werd ontvangen was ongelofelijk warm en de rest zou ik nog ontdekken.
|
|
25
|
+
|
|
26
|
+
<editorjs type='alignment' tag='p' alignment='center'> <b>Werk dat ertoe doet </b> </editorjs>
|
|
27
|
+
|
|
28
|
+
Toen ik mocht starten bij de gemeente had ik weinig beeld bij wat een ambtenaar nou precies deed. Wat het betekende om te werken voor een gemeente. Mensen zeiden me: “Bij de gemeente, jij? Dat gaat jou toch allemaal veel te traag, dat is niks voor jou?!” Maar de inhoud van de functie sprak me ontzettend aan, het team waarin ik werd ontvangen was ongelofelijk warm en de rest zou ik nog ontdekken.
|
|
29
|
+
|
|
30
|
+
Later kon ik al deze mensen vertellen dat ik een geweldige baan had gevonden. Een baan waarvan mijn hart sneller ging kloppen. Ik mocht werk doen dat ertoe deed. Niks ging langzaam, eerder te snel. Nog nooit werkte ik in zo’n dynamische omgeving als een gemeente, geen dag is hetzelfde!
|
|
31
|
+
|
|
32
|
+
Iedereen die mij spreekt, die weet: ik geniet van mijn werk. Ik mag met ontzettend fijne mensen werken en ik mag werk doen dat ertoe doet. Ik mag mij inzetten voor het welzijn en de rechten van burgers in Leiden. Net zoals ik heb beloofd.
|
|
33
|
+
|
|
34
|
+
Wat ik wel merkte toen ik vanuit mijn functie met zoveel mogelijk partners en burgers in de stad ging praten, was dat er een flinke kloof was tussen de beleidswereld (gemeente, overheid) en de leefwereld van mensen, de wijken, de scholen, de plekken waarvoor het beleid werd gemaakt. Daar ervaarden mensen soms afstand, onbegrip en frustratie doordat de realiteit weerbarstig is en het beleid zwart-wit. Dit is natuurlijk niet iets Leids. Dit is landelijk zo, dit is internationaal zo. De afstand tussen overheid en ‘burgers’ lijkt groot, en bovendien ook steeds groter te worden. Regels, procedures, systemen. Maar ook simpelweg taal. Alsof het verschillende talen zijn, maar het zijn zeker verschillende belevingen...
|
|
35
|
+
|
|
36
|
+
Wat in mijn eed en belofte niet echt scherp wordt, is het waartoe van mijn werk. Waartoe beloof ik te doen wat ik als ambtenaar ga doen? Zeker, trouw aan de wet, zorgvuldigheid, inzetten voor het welzijn, loyaal aan het stadsbestuur. Ja. Maar waartoe doe ik dit dan eigenlijk?
|
|
37
|
+
|
|
38
|
+
Volgens AI bestaan “\[...] ambtenaren om het functioneren van de samenleving te waarborgen door het uitvoeren van overheidstaken, het ontwikkelen en uitvoeren van beleid, en het leveren van publieke diensten. Zij zorgen voor de handhaving van wetten, innen belastingen, verlenen vergunningen en ondersteunen politieke bestuurders.”
|
|
39
|
+
|
|
40
|
+
Herkenbaar en zeker, dat doen we. Maar nogmaals, waartoe doen we dit? Anno 2026 is een ambtenaar toch zeker iemand die zich bewust is van het feit dat de functie bestaat bij de gratie van deze vorm van democratie? Dat het doel is om de inwoners te dienen, en dus ook nabij te zijn om zelf te ervaren dat je werk goed uitpakt, en dus dienend is aan het welzijn en de rechten van de burgers? Soms raak ik zelf zo overweldigd door de systemen waarin we allen ons werk proberen uit te voeren dat het eerder voelt alsof je trouw hebt beloofd te zijn aan Excel-sheets en processen van besluitvorming, dan aan je inzetten voor het welzijn van de inwoners. De praktijk van een ambtenaar is ook best weerbarstig…
|
|
41
|
+
|
|
42
|
+
Tim ‘s Jongers <a href="https://decorrespondent.nl/15957/beleid-wordt-beter-als-ambtenaren-achter-hun-laptop-vandaan-komen-daarom-een-pleidooi-voor-een-maatschappelijke-diensttijd/91ead8ad-ffb7-0a2a-15bf-316fe8e4578a" target="_blank">schreef eens dat beleid beter wordt als ambtenaren achter hun laptops vandaan komen </a> en onderdeel worden van de leefwereld waarvoor zij beleid maken. In Leiden kennen wij gelukkig verschillende werkprincipes die ons hierop wijzen, zo ook: wij werken van buiten (wat leeft in de stad) naar binnen (we maken beleid dat aansluit op wat leeft in de stad).
|
|
43
|
+
|
|
44
|
+
<editorjs type='alignment' tag='h2' alignment='center'>Leidse Ambtenaren in de Praktijk </editorjs>
|
|
45
|
+
|
|
46
|
+
Eén van de projecten die vanuit de Leidse Gelijke Kansen Aanpak is opgestart is de pilot ‘Ambtenaar in de Praktijk’. Via Ambtenaar in de Praktijk, doen ambtenaren van gemeente Leiden gerichte praktijkervaring op. Op die manier ervaren zij direct hoe beleid uitpakt in de praktijk en die kennis en ervaring brengen zij mee terug naar hun beleidsafdelingen.
|
|
47
|
+
|
|
48
|
+
“Ik krijg door het meewerken in de praktijk van een brugfunctionaris meer inzicht in de problematieken die scholen binnenkomen. Ik realiseerde me dat problemen zichtbaar worden via het kind maar dat de problemen altijd verder reiken.” — Ambtenaar in de praktijk
|
|
49
|
+
|
|
50
|
+
<editorjs type='alignment' tag='p' alignment='center'> <b>Gerichte praktijkervaring via partners in de stad </b> </editorjs>
|
|
51
|
+
|
|
52
|
+
In aanloop naar deze pilot hebben we de standpunten van onze samenwerkingspartners geïnventariseerd en hen gevraagd: ‘stel dat we dit gaan doen, wat zou je dan waardevolle praktijkervaring vinden voor een ambtenaar en hoe kun je dat faciliteren?’
|
|
53
|
+
|
|
54
|
+
Op die manier zijn we gekomen tot een soort menukaart met daarop min of meer kant-en-klare ideeën voor het opdoen van praktijkervaringen. Denk aan het meewerken in een buurthuis of buurtontmoetingsplek tijdens een inloop voor inwoners. Of denk aan het meewerken bij een speelgroep van JES Rijnland waar ouders met zeer jonge kinderen komen. Of misschien het meewerken bij een spreekuur rondom schulden. Of meewerken op een Leidse basisschool om zo meer mee te krijgen van wat er zoals speelt in en rond het onderwijs.
|
|
55
|
+
|
|
56
|
+
“Bij de speelgroep merk ik dat er vaak sprake is van een taalbarrière. Ouders gebruiken de ontmoetingen bij een speelgroep dus ook om onderling informatie uit te wisselen. Het is een soort vindplaats. Ouders en de professional(s) van de speelgroep delen kennis en helpen elkaar dus via deze plek.” — Ambtenaar in de praktijk
|
|
57
|
+
|
|
58
|
+
<editorjs type='alignment' tag='p' alignment='center'> <b>De gemeente organiseert en faciliteert </b> </editorjs>
|
|
59
|
+
|
|
60
|
+
Het is geweldig fijn dat gemeente Leiden haar ambtenaren hiervoor 32u op jaarbasis ruimte biedt. Op die manier geven we echt uitvoering aan ‘werken van buiten naar binnen’. De coördinatie van de pilot gebeurt vanuit de Leidse Gelijke Kansen Aanpak. Het is natuurlijk niet de bedoeling dat dit een verzwaring oplevert voor de partners in de stad. Het moet elkaar gaan versterken.
|
|
61
|
+
|
|
62
|
+
“Het is alleen al prachtig dat we nu zo nabij kunnen zijn. De partners voelen zich gezien en gehoord in hun werk. En andersom kun je ook een inkijkje geven in hoe het dan binnen de gemeente loopt, waarom dingen soms wat tijd vragen. Of gewoon door te zeggen, “Ja, wij balen er ook enorm van dat dit nog wat rommelig loopt. We werken er hard aan om dit beter te maken.” Het vertrouwen waarmee de partners deze pilot in de stad hebben ontvangen is geweldig. En nu op naar verdere uitbreiding!” — Ambtenaar in de praktijk
|
|
63
|
+
|
|
64
|
+
<editorjs type='alignment' tag='p' alignment='center'> <b>Ongelofelijk leerzame weken </b> </editorjs>
|
|
65
|
+
|
|
66
|
+
De reacties in de stad zijn dan ook positief; de partnerorganisaties benadrukken het belang van praktijkervaring voor ambtenaren. Het is zeer leerzaam om in de praktijk te ervaren dat jouw beeld van hoe iets uitwerkt, misschien moet worden bijgesteld omdat de praktijk nu eenmaal weerbarstig is.
|
|
67
|
+
|
|
68
|
+
“Op papier staat het mooi, maar in de praktijk valt het me op hoeveel je vraagt van partijen op het gebied van samenwerking en uitwisseling. Als je je handen dagelijks vol hebt aan het ondersteunen van inwoners, dan is het niet realistisch om te verwachten dat men ook nog zelf op zoek gaat naar allerlei samenwerkingen. Zonde, want je ziet wel hoeveel men onderling van die kennis zou kunnen profiteren. Zowel de professionals als inwoners.” — Ambtenaar in de praktijk
|
|
69
|
+
|
|
70
|
+
<editorjs type='alignment' tag='p' alignment='center'> <b>De volgende ronde gaat in maart van start </b> </editorjs>
|
|
71
|
+
|
|
72
|
+
De eerste ronde van deze mooie pilot is bijna afgerond. De betrokken ambtenaren ronden de pilot met elkaar af door de geleerde en ervaren lessen met elkaar om te zetten naar advies terug naar de beleidswereld. Onderling worden deze natuurlijk ook vast gedeeld met de volgende groep ambtenaren en met de betrokken partijen. Het is een prachtig leerproces. Wat leren wij van hen? Wat leren zij van ons? Wat betekent dit en hoe kunnen we dit leren omzetten in het daadwerkelijk verkleinen van die kloof tussen praktijk en beleid?
|
|
73
|
+
|
|
74
|
+
Zo geeft een collega aan dat ze ziet hoe brieven in duidelijke taal vanuit de gemeente, toch vaak niet duidelijk genoeg lijken te zijn voor inwoners. Dat kun je niet weten, tenzij je spreekt met de inwoners zelf én met de professionals die hen ondersteunen.
|
|
75
|
+
|
|
76
|
+
“Bij het Goede Buur spreekuur werk ik met gepassioneerde hulpverleners die de inwoner zo goed mogelijk willen helpen.” — Ambtenaar in de praktijk
|
|
77
|
+
|
|
78
|
+
Eén ding is zeker: dit zou onderdeel moeten kunnen zijn van je werk. Gemeente Leiden werkt graag kennisgericht, en via deze praktijkervaring maken we daar ook praktijkervaringskennisgericht werken van. Minimaal ieder jaar een week meewerken in de praktijk. De mouwen opstropen, doen, ondergaan en ervaren. Dit zou toch voor eenieder moeten gelden in elke positie waarbij jouw beleid invloed heeft op de ander?
|
|
79
|
+
|
|
80
|
+
<editorjs type='alignment' tag='p' alignment='center'> <b>College in de Praktijk? </b> </editorjs>
|
|
81
|
+
|
|
82
|
+
Pasgeleden opperde een collega dat we misschien ook moeten denken aan een uitbreiding van de pilot; ‘College in de Praktijk’, zodat na de verkiezingen het college van wethouders en burgemeester ook de Leidse praktijk echt kan ervaren. Een werkbezoek is namelijk echt iets heel anders als gewoon even ergens zelf de koffie zetten, stoelen uitklappen voor de vele inwoners die binnenkomen, samen soep koken, luisteren naar de verhalen van zij die even steun nodig hebben of vragen komen stellen. Zij die even warmte opzoeken omdat het thuis koud is. Of even stoom komen afblazen omdat ze niet als volwaardig worden gezien...
|
|
83
|
+
|
|
84
|
+
“Heel eerlijk? Ik had geen idee. Ik wist het wel, maar ik realiseerde me het niet. Zelf voelen hoe het moet zijn om geen netwerk te hebben, om geen idee te hebben van de wereld om je heen. Zoveel te bieden, zo slim, getalenteerd. Zo betrokken en sociaal. Maar toch vast blijven zitten omdat je nu eenmaal de weg niet kent. Je denkt, dat hebben we allemaal goed ingeregeld! Maar in de praktijk zie je dat het niet zo makkelijk is...” — Ambtenaar in de praktijk
|
|
85
|
+
|
|
86
|
+
***
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from editorjs import EditorJS
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
# blog = Path("blog.md").read_text()
|
|
8
|
+
|
|
9
|
+
ejs = EditorJS.from_json(
|
|
10
|
+
'{"time":1770292823347,"blocks":[{"id":"QKelxSHz2Y","type":"paragraph","data":{"text":"rechts basis"},"tunes":{"alignmentTune":{"alignment":"right"}}},{"id":"IUX70yigzz","type":"paragraph","data":{"text":"links"},"tunes":{"alignmentTune":{"alignment":"left"}}},{"id":"SaWthD_Vlr","type":"paragraph","data":{"text":"<b>rechts duur</b>"},"tunes":{"alignmentTune":{"alignment":"right"}}}],"version":"2.30.7"}'
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# if html = <editorjs type='alignment' tag='p' alignment='center'> <b>Werk dat ertoe doet </b> </editorjs>
|
|
14
|
+
# assert parse_html(html).type == 'alignment'
|
|
15
|
+
|
|
16
|
+
print(ejs.to_html()) # good
|
|
17
|
+
print(ejs.to_json()) # bad, fixme
|
|
18
|
+
# {"time": 1770292880600, "blocks": [{"type": "paragraph", "data": {"text": ""}, "tunes": {"alignmentTune": {"alignment": "right"}}}, {"type": "paragraph", "data": {"text": "links"}}, {"type": "paragraph", "data": {"text": "<b></b>"}, "tunes": {"alignmentTune": {"alignment": "right"}}}, {"type": "raw", "data": {"html": "</b></editorjs>"}}], "version": "2.30.6"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.6.1"
|
|
@@ -5,15 +5,15 @@ mdast to editorjs
|
|
|
5
5
|
import abc
|
|
6
6
|
import re
|
|
7
7
|
import typing as t
|
|
8
|
-
from html.parser import HTMLParser
|
|
9
8
|
from urllib.parse import urlparse
|
|
10
9
|
|
|
10
|
+
import html2markdown
|
|
11
11
|
import humanize
|
|
12
|
+
import lxml.html
|
|
12
13
|
import markdown2
|
|
13
14
|
|
|
14
15
|
from .exceptions import TODO, Unreachable
|
|
15
16
|
from .types import EditorChildData, MDChildNode
|
|
16
|
-
import html2markdown
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class EditorJSBlock(abc.ABC):
|
|
@@ -55,8 +55,6 @@ def process_styled_content(item: MDChildNode, strict: bool = True) -> str:
|
|
|
55
55
|
"""
|
|
56
56
|
_type = item.get("type")
|
|
57
57
|
|
|
58
|
-
print('process_styled_content', _type)
|
|
59
|
-
|
|
60
58
|
html_wrappers = {
|
|
61
59
|
"text": "{value}",
|
|
62
60
|
"html": "{value}",
|
|
@@ -110,9 +108,9 @@ class HeadingBlock(EditorJSBlock):
|
|
|
110
108
|
raise ValueError("Header level must be between 1 and 6.")
|
|
111
109
|
|
|
112
110
|
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
tunes.get("alignmentTune")
|
|
112
|
+
and (alignment := tunes["alignmentTune"].get("alignment"))
|
|
113
|
+
and (alignment != "left")
|
|
116
114
|
):
|
|
117
115
|
# can't just return regular HTML because then it will turn into a raw block
|
|
118
116
|
return AlignmentBlock.to_markdown(
|
|
@@ -172,9 +170,9 @@ class ParagraphBlock(EditorJSBlock):
|
|
|
172
170
|
tunes = data.get("tunes", {})
|
|
173
171
|
|
|
174
172
|
if (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
173
|
+
tunes.get("alignmentTune")
|
|
174
|
+
and (alignment := tunes["alignmentTune"].get("alignment"))
|
|
175
|
+
and (alignment != "left")
|
|
178
176
|
):
|
|
179
177
|
return AlignmentBlock.to_markdown(
|
|
180
178
|
{
|
|
@@ -189,6 +187,18 @@ class ParagraphBlock(EditorJSBlock):
|
|
|
189
187
|
|
|
190
188
|
return f"{text}\n\n"
|
|
191
189
|
|
|
190
|
+
@staticmethod
|
|
191
|
+
def _find_closing_editorjs_tag(nodes: list, start_idx: int) -> int | None:
|
|
192
|
+
"""Find index of closing </editorjs> tag."""
|
|
193
|
+
for idx, node in enumerate(nodes[start_idx:], start=start_idx):
|
|
194
|
+
if (
|
|
195
|
+
node.get("type") == "html"
|
|
196
|
+
and node.get("value", "").strip() == "</editorjs>"
|
|
197
|
+
):
|
|
198
|
+
return idx
|
|
199
|
+
|
|
200
|
+
return None
|
|
201
|
+
|
|
192
202
|
@classmethod
|
|
193
203
|
def to_json(cls, node: MDChildNode) -> list[dict]:
|
|
194
204
|
result = []
|
|
@@ -212,14 +222,17 @@ class ParagraphBlock(EditorJSBlock):
|
|
|
212
222
|
|
|
213
223
|
if child.get("value", "").endswith("/>"):
|
|
214
224
|
# self-closing
|
|
215
|
-
result.append(EditorJSCustom.to_json(
|
|
225
|
+
result.append(EditorJSCustom.to_json({"children": [child]}))
|
|
216
226
|
else:
|
|
217
|
-
# <editorjs
|
|
218
|
-
|
|
219
|
-
|
|
227
|
+
# <editorjs>...</editorjs> may include nested HTML; find the closing tag
|
|
228
|
+
end_idx = cls._find_closing_editorjs_tag(nodes, idx + 1)
|
|
229
|
+
children_slice = (
|
|
230
|
+
nodes[idx : idx + 2]
|
|
231
|
+
if end_idx is None
|
|
232
|
+
else nodes[idx : end_idx + 1]
|
|
220
233
|
)
|
|
221
|
-
|
|
222
|
-
|
|
234
|
+
skip = 2 if end_idx is None else end_idx - idx
|
|
235
|
+
result.extend(EditorJSCustom.to_json({"children": children_slice}))
|
|
223
236
|
|
|
224
237
|
continue
|
|
225
238
|
|
|
@@ -398,7 +411,7 @@ class CodeBlock(EditorJSBlock):
|
|
|
398
411
|
@classmethod
|
|
399
412
|
def to_markdown(cls, data: EditorChildData) -> str:
|
|
400
413
|
code = data.get("code", "")
|
|
401
|
-
return f"```\n
|
|
414
|
+
return f"```\n{code}\n```\n"
|
|
402
415
|
|
|
403
416
|
@classmethod
|
|
404
417
|
def to_json(cls, node: MDChildNode) -> list[dict]:
|
|
@@ -457,9 +470,9 @@ class ImageBlock(EditorJSBlock):
|
|
|
457
470
|
border = node.get("border") or ""
|
|
458
471
|
|
|
459
472
|
return f"""
|
|
460
|
-
<div class="ce-block {stretched and
|
|
473
|
+
<div class="ce-block {stretched and "ce-block--stretched"}">
|
|
461
474
|
<div class="ce-block__content">
|
|
462
|
-
<div class="cdx-block image-tool image-tool--filled {background and
|
|
475
|
+
<div class="cdx-block image-tool image-tool--filled {background and "image-tool--withBackground"} {stretched and "image-tool--stretched"} {border and "image-tool--withBorder"}">
|
|
463
476
|
<div class="image-tool__image">
|
|
464
477
|
<figure>
|
|
465
478
|
<img class="image-tool__image-picture" src="{url}" title="{caption}" alt="{caption}">
|
|
@@ -514,7 +527,6 @@ class QuoteBlock(EditorJSBlock):
|
|
|
514
527
|
|
|
515
528
|
@block("raw", "html")
|
|
516
529
|
class RawBlock(EditorJSBlock):
|
|
517
|
-
|
|
518
530
|
@classmethod
|
|
519
531
|
def to_markdown(cls, data: EditorChildData) -> str:
|
|
520
532
|
text = data.get("html", "")
|
|
@@ -537,7 +549,6 @@ class RawBlock(EditorJSBlock):
|
|
|
537
549
|
|
|
538
550
|
@block("table")
|
|
539
551
|
class TableBlock(EditorJSBlock):
|
|
540
|
-
|
|
541
552
|
@classmethod
|
|
542
553
|
def to_markdown(cls, data: EditorChildData) -> str:
|
|
543
554
|
"""
|
|
@@ -656,7 +667,6 @@ class LinkBlock(EditorJSBlock):
|
|
|
656
667
|
|
|
657
668
|
@block("attaches")
|
|
658
669
|
class AttachmentBlock(EditorJSBlock):
|
|
659
|
-
|
|
660
670
|
@classmethod
|
|
661
671
|
def to_markdown(cls, data: EditorChildData) -> str:
|
|
662
672
|
title = data.get("title", "")
|
|
@@ -721,7 +731,7 @@ class AttachmentBlock(EditorJSBlock):
|
|
|
721
731
|
</div>
|
|
722
732
|
{file_size}
|
|
723
733
|
</div>
|
|
724
|
-
<a class="cdx-attaches__download-button" href="{node.get(
|
|
734
|
+
<a class="cdx-attaches__download-button" href="{node.get("file", "")}" target="_blank" rel="nofollow noindex noreferrer" title="{node.get("name", "")}">
|
|
725
735
|
<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>
|
|
726
736
|
</a>
|
|
727
737
|
</div>
|
|
@@ -773,7 +783,6 @@ class AlignmentBlock(EditorJSBlock):
|
|
|
773
783
|
|
|
774
784
|
@block("embed")
|
|
775
785
|
class EmbedBlock(EditorJSBlock):
|
|
776
|
-
|
|
777
786
|
@classmethod
|
|
778
787
|
def to_markdown(cls, data: EditorChildData) -> str:
|
|
779
788
|
service = data.get("service", "")
|
|
@@ -802,20 +811,6 @@ class EmbedBlock(EditorJSBlock):
|
|
|
802
811
|
### end blocks
|
|
803
812
|
|
|
804
813
|
|
|
805
|
-
class AttributeParser(HTMLParser):
|
|
806
|
-
def __init__(self):
|
|
807
|
-
super().__init__()
|
|
808
|
-
self.attributes = {}
|
|
809
|
-
self.data = None
|
|
810
|
-
|
|
811
|
-
def handle_starttag(self, tag, attrs):
|
|
812
|
-
# Collect attributes when the tag is encountered
|
|
813
|
-
self.attributes = dict(attrs)
|
|
814
|
-
|
|
815
|
-
def handle_data(self, data):
|
|
816
|
-
self.data = data
|
|
817
|
-
|
|
818
|
-
|
|
819
814
|
class EditorJSCustom(EditorJSBlock, markdown2.Extra):
|
|
820
815
|
"""
|
|
821
816
|
Special type of block to deal with custom attributes.
|
|
@@ -828,10 +823,29 @@ class EditorJSCustom(EditorJSBlock, markdown2.Extra):
|
|
|
828
823
|
|
|
829
824
|
@classmethod
|
|
830
825
|
def parse_html(cls, html: str):
|
|
831
|
-
|
|
832
|
-
|
|
826
|
+
"""
|
|
827
|
+
Extract attributes from the outermost HTML element and return its inner HTML.
|
|
828
|
+
|
|
829
|
+
This function parses the provided markup, identifies the root element,
|
|
830
|
+
returns its attributes as a dictionary, and serializes all direct child
|
|
831
|
+
nodes back into an HTML string.
|
|
832
|
+
|
|
833
|
+
Args:
|
|
834
|
+
html: A string containing a single root HTML element.
|
|
835
|
+
|
|
836
|
+
Returns:
|
|
837
|
+
A tuple of:
|
|
838
|
+
- dict[str, str]: Attributes of the root element
|
|
839
|
+
- str: Inner HTML of the root element
|
|
840
|
+
"""
|
|
841
|
+
root = lxml.html.fromstring(html)
|
|
842
|
+
|
|
843
|
+
attributes = dict(root.attrib)
|
|
844
|
+
inner_html = "".join(
|
|
845
|
+
lxml.html.tostring(child, encoding="unicode") for child in root
|
|
846
|
+
)
|
|
833
847
|
|
|
834
|
-
return
|
|
848
|
+
return attributes, inner_html
|
|
835
849
|
|
|
836
850
|
@classmethod
|
|
837
851
|
def to_markdown(cls, data: EditorChildData) -> str:
|
|
@@ -846,6 +860,7 @@ class EditorJSCustom(EditorJSBlock, markdown2.Extra):
|
|
|
846
860
|
handler = BLOCKS.get(_type)
|
|
847
861
|
|
|
848
862
|
if not handler:
|
|
863
|
+
raise ValueError(f"debug: {attrs = } {body = }") # fixme
|
|
849
864
|
raise ValueError(f"Unknown custom type {_type}")
|
|
850
865
|
|
|
851
866
|
return handler, attrs
|
|
@@ -100,13 +100,11 @@ class EditorJS:
|
|
|
100
100
|
try:
|
|
101
101
|
blocks.extend(block.to_json(child))
|
|
102
102
|
except Exception as e:
|
|
103
|
-
|
|
104
103
|
warnings.warn(
|
|
105
104
|
"to_json: Oh oh, unexpected block failure!",
|
|
106
105
|
category=RuntimeWarning,
|
|
107
106
|
source=e,
|
|
108
107
|
)
|
|
109
|
-
raise e
|
|
110
108
|
# if isinstance(e, TODO):
|
|
111
109
|
# raise e
|
|
112
110
|
|
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
import textwrap
|
|
3
3
|
|
|
4
4
|
from editorjs import EditorJS
|
|
5
|
+
from editorjs.blocks import EditorJSCustom
|
|
5
6
|
|
|
6
7
|
EXAMPLE_MD = textwrap.dedent("""
|
|
7
8
|
# Heading
|
|
@@ -185,7 +186,8 @@ asdfsdajgdsjaklgkjds
|
|
|
185
186
|
|
|
186
187
|
|
|
187
188
|
def test_code():
|
|
188
|
-
e = EditorJS.from_markdown(
|
|
189
|
+
e = EditorJS.from_markdown(
|
|
190
|
+
textwrap.dedent("""
|
|
189
191
|
Read code:
|
|
190
192
|
|
|
191
193
|
```
|
|
@@ -193,7 +195,8 @@ def test_code():
|
|
|
193
195
|
```
|
|
194
196
|
|
|
195
197
|
End of code
|
|
196
|
-
""")
|
|
198
|
+
""")
|
|
199
|
+
)
|
|
197
200
|
|
|
198
201
|
blocks = json.loads(e.to_json())
|
|
199
202
|
|
|
@@ -266,6 +269,7 @@ def test_figcaption():
|
|
|
266
269
|
|
|
267
270
|
assert "figcaption" in html
|
|
268
271
|
|
|
272
|
+
|
|
269
273
|
def test_bold():
|
|
270
274
|
js = """{"time":1742475802066,"blocks":[{"id":"v_Kc51dnJH","type":"paragraph","data":{"text":"Deze tekst is <b>half bold</b> en half niet"},"tunes":{"alignmentTune":{"alignment":"left"}}},{"id":"q_fkuEFcY5","type":"paragraph","data":{"text":"<b>Deze tekst is heel bold</b>"},"tunes":{"alignmentTune":{"alignment":"left"}}}],"version":"2.30.7"}"""
|
|
271
275
|
|
|
@@ -291,3 +295,81 @@ def test_quotes():
|
|
|
291
295
|
e.to_markdown(),
|
|
292
296
|
)
|
|
293
297
|
|
|
298
|
+
|
|
299
|
+
# test case based on issue HETNIEUWELEIDEN-019c28cd-56b8-7778-be0c-60fb9e930638
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def test_nonrecusrive_attr_parser():
|
|
303
|
+
html = "<div type='outer'><div type='inner'>contents</div></div>"
|
|
304
|
+
|
|
305
|
+
attributes, data = EditorJSCustom.parse_html(html)
|
|
306
|
+
|
|
307
|
+
assert attributes == {"type": "outer"}
|
|
308
|
+
assert data == '<div type="inner">contents</div>'
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_editorjs_alignment_tag():
|
|
312
|
+
"""Test that editorjs alignment tags with nested HTML are parsed correctly."""
|
|
313
|
+
md_input = "<editorjs type='alignment' tag='p' alignment='center'> <b>Werk dat ertoe doet </b> </editorjs>"
|
|
314
|
+
|
|
315
|
+
e = EditorJS.from_markdown(md_input)
|
|
316
|
+
|
|
317
|
+
print("MDAST:", e.to_mdast())
|
|
318
|
+
print("Markdown:", e.to_markdown())
|
|
319
|
+
print("JSON:", e.to_json())
|
|
320
|
+
|
|
321
|
+
html = e.to_html()
|
|
322
|
+
print("HTML:", html)
|
|
323
|
+
|
|
324
|
+
# The type should be detected as 'alignment' (or mapped to 'paragraph' with alignment tune)
|
|
325
|
+
blocks = json.loads(e.to_json())
|
|
326
|
+
|
|
327
|
+
assert blocks
|
|
328
|
+
|
|
329
|
+
assert "<b>Werk dat ertoe doet" in html
|
|
330
|
+
assert "<editorjs" not in html
|
|
331
|
+
assert "<code" not in html
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_alignment_with_nested_bold_roundtrip():
|
|
335
|
+
"""
|
|
336
|
+
Test that alignment tags preserve nested bold HTML through JSON roundtrip.
|
|
337
|
+
|
|
338
|
+
Regression test for issue where nested HTML in alignment tags causes:
|
|
339
|
+
- Text content to disappear
|
|
340
|
+
- Malformed closing tags to appear as separate raw blocks
|
|
341
|
+
"""
|
|
342
|
+
# Input: three paragraphs with different alignments, one with nested bold
|
|
343
|
+
input_json = r"""{"time":1770292823347,"blocks":[{"id":"QKelxSHz2Y","type":"paragraph","data":{"text":"rechts basis"},"tunes":{"alignmentTune":{"alignment":"right"}}},{"id":"IUX70yigzz","type":"paragraph","data":{"text":"links"},"tunes":{"alignmentTune":{"alignment":"left"}}},{"id":"SaWthD_Vlr","type":"paragraph","data":{"text":"<b>rechts duur</b>"},"tunes":{"alignmentTune":{"alignment":"right"}}}],"version":"2.30.7"}"""
|
|
344
|
+
# faulty output: {"time": 1770292880600, "blocks": [{"type": "paragraph", "data": {"text": ""}, "tunes": {"alignmentTune": {"alignment": "right"}}}, {"type": "paragraph", "data": {"text": "links"}}, {"type": "paragraph", "data": {"text": "<b></b>"}, "tunes": {"alignmentTune": {"alignment": "right"}}}, {"type": "raw", "data": {"html": "</b></editorjs>"}}], "version": "2.30.6"}
|
|
345
|
+
|
|
346
|
+
e = EditorJS.from_json(input_json)
|
|
347
|
+
|
|
348
|
+
# Convert through markdown and back to JSON
|
|
349
|
+
md = e.to_markdown()
|
|
350
|
+
print("Markdown output:", md)
|
|
351
|
+
|
|
352
|
+
e2 = EditorJS.from_markdown(md)
|
|
353
|
+
output_json = json.loads(e2.to_json())
|
|
354
|
+
|
|
355
|
+
output_str = json.dumps(output_json, indent=2)
|
|
356
|
+
print("Output JSON:", output_str)
|
|
357
|
+
|
|
358
|
+
# Should have exactly 3 blocks (not 4 with a malformed raw block)
|
|
359
|
+
assert len(output_json["blocks"]) == 3
|
|
360
|
+
|
|
361
|
+
# All should be paragraphs (not raw blocks)
|
|
362
|
+
for block in output_json["blocks"]:
|
|
363
|
+
assert block["type"] == "paragraph"
|
|
364
|
+
|
|
365
|
+
# Third block should preserve the bold content
|
|
366
|
+
assert (
|
|
367
|
+
"<b>rechts duur</b>" in output_json["blocks"][2]["data"]["text"]
|
|
368
|
+
or "<b>rechts duur </b>" in output_json["blocks"][2]["data"]["text"]
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Third block should preserve alignment
|
|
372
|
+
assert output_json["blocks"][2]["tunes"]["alignmentTune"]["alignment"] == "right"
|
|
373
|
+
|
|
374
|
+
assert "raw" not in output_str
|
|
375
|
+
assert "html" not in output_str
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.5.0a4"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|