@bloklabs/cli 1.3.0 → 1.4.1
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.
- package/README.md +57 -0
- package/dist/{block-builder-BhbHyKmd.mjs → block-builder-Cp5f8JCh.mjs} +174 -68
- package/dist/cli.mjs +3 -3
- package/dist/{convert-gdocs-D8zi-ZHD.mjs → convert-gdocs-BMCANuFK.mjs} +54 -54
- package/dist/{convert-gdocs-DhqPnu2C.mjs → convert-gdocs-DLaumLJ0.mjs} +54 -54
- package/dist/convert-html-BxOQfxMe.mjs +13 -0
- package/dist/convert-html-DBAHuyaK.mjs +13 -0
- package/package.json +9 -1
- package/dist/convert-html-CtLOMx8H.mjs +0 -13
- package/dist/convert-html-lcE4WDun.mjs +0 -13
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @bloklabs/cli
|
|
2
|
+
|
|
3
|
+
Command-line tools for [Blok](https://blokeditor.com) — the headless, Notion-like block editor whose content is JSON blocks, not HTML. Converts legacy HTML into Blok JSON and prints the Editor.js migration guide.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @bloklabs/cli --help
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install it locally:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install --save-dev @bloklabs/cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Usage: blok-cli [options]
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--convert-html Convert legacy HTML from stdin to Blok JSON (stdout)
|
|
24
|
+
--convert-gdocs Convert Google Docs HTML from stdin to Blok JSON (stdout)
|
|
25
|
+
--migration Output the EditorJS to Blok migration guide (LLM-friendly)
|
|
26
|
+
--output <file> Write output to a file instead of stdout
|
|
27
|
+
--help Show this help message
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Convert existing HTML to Blok JSON
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx @bloklabs/cli --convert-html < article.html --output article.json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The result is an `OutputData` document you can hand straight to `new Blok({ data })`.
|
|
37
|
+
|
|
38
|
+
### Convert a Google Docs export
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx @bloklabs/cli --convert-gdocs < gdocs-export.html --output doc.json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Migrating from Editor.js
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx @bloklabs/cli --migration --output migration-guide.md
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The guide is written to be pasted into an LLM alongside your codebase.
|
|
51
|
+
|
|
52
|
+
## Docs
|
|
53
|
+
|
|
54
|
+
- [Migration guide](https://blokeditor.com/migration)
|
|
55
|
+
- [Output data reference](https://blokeditor.com/docs/output-data)
|
|
56
|
+
|
|
57
|
+
Licensed under Apache-2.0.
|
|
@@ -248,39 +248,145 @@ function d(r, a) {
|
|
|
248
248
|
}, null)?.name ?? null;
|
|
249
249
|
}
|
|
250
250
|
//#endregion
|
|
251
|
+
//#region src/shared/inline-normalization-policy.ts
|
|
252
|
+
var f = /* @__PURE__ */ new Set([
|
|
253
|
+
"A",
|
|
254
|
+
"B",
|
|
255
|
+
"STRONG",
|
|
256
|
+
"I",
|
|
257
|
+
"EM",
|
|
258
|
+
"U",
|
|
259
|
+
"S",
|
|
260
|
+
"DEL",
|
|
261
|
+
"INS",
|
|
262
|
+
"MARK",
|
|
263
|
+
"CODE",
|
|
264
|
+
"SUB",
|
|
265
|
+
"SUP",
|
|
266
|
+
"SMALL",
|
|
267
|
+
"SPAN",
|
|
268
|
+
"FONT"
|
|
269
|
+
]), p = new Set([...f].filter((e) => e !== "A")), m = /* @__PURE__ */ new Set([
|
|
270
|
+
"IMG",
|
|
271
|
+
"IFRAME",
|
|
272
|
+
"VIDEO",
|
|
273
|
+
"AUDIO",
|
|
274
|
+
"INPUT",
|
|
275
|
+
"SVG",
|
|
276
|
+
"CANVAS",
|
|
277
|
+
"OBJECT",
|
|
278
|
+
"EMBED"
|
|
279
|
+
]), ee = /* @__PURE__ */ new Set([
|
|
280
|
+
"U",
|
|
281
|
+
"S",
|
|
282
|
+
"DEL",
|
|
283
|
+
"INS"
|
|
284
|
+
]), h = /* @__PURE__ */ new Set([
|
|
285
|
+
"background-color",
|
|
286
|
+
"background",
|
|
287
|
+
"text-decoration",
|
|
288
|
+
"text-decoration-line",
|
|
289
|
+
"border",
|
|
290
|
+
"border-bottom",
|
|
291
|
+
"box-shadow",
|
|
292
|
+
"outline"
|
|
293
|
+
]), g = /* @__PURE__ */ new Set([
|
|
294
|
+
"",
|
|
295
|
+
"none",
|
|
296
|
+
"transparent",
|
|
297
|
+
"initial",
|
|
298
|
+
"unset"
|
|
299
|
+
]), _ = (e) => {
|
|
300
|
+
let t = e.attributes.filter((e) => e.name.toLowerCase() !== "style").map((e) => `${e.name.toLowerCase()}=${e.value}`).sort(), n = e.styleDeclarations.map((e) => `${e.property.toLowerCase()}:${e.value.trim()}`).sort();
|
|
301
|
+
return `${e.tagName}|${t.join("|")}|${n.join(";")}`;
|
|
302
|
+
}, v = (e) => e.attributes.some((e) => e.name.toLowerCase() === "id"), y = (e) => ee.has(e.tagName) ? !0 : e.styleDeclarations.some((e) => h.has(e.property.toLowerCase()) && !g.has(e.value.trim().toLowerCase())), b = (e) => {
|
|
303
|
+
if (!p.has(e.tagName)) return !1;
|
|
304
|
+
let t = e.text();
|
|
305
|
+
return t.trim().length > 0 || e.hasVoidContentDescendant() ? !1 : t.length === 0 || !y(e);
|
|
306
|
+
}, te = (e, t) => !f.has(e.tagName) || v(e) || v(t) ? !1 : _(e) === _(t), ne = (e, t) => {
|
|
307
|
+
if (!f.has(e.tagName) || v(e)) return !1;
|
|
308
|
+
let n = _(e);
|
|
309
|
+
return t.some((e) => _(e) === n);
|
|
310
|
+
}, x = (e) => {
|
|
311
|
+
let t = e.style, n = Array.from({ length: t.length }, (e, n) => {
|
|
312
|
+
let r = t.item(n);
|
|
313
|
+
return {
|
|
314
|
+
property: r,
|
|
315
|
+
value: t.getPropertyValue(r)
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
return {
|
|
319
|
+
tagName: e.tagName.toUpperCase(),
|
|
320
|
+
attributes: Array.from(e.attributes).map((e) => ({
|
|
321
|
+
name: e.name,
|
|
322
|
+
value: e.value
|
|
323
|
+
})),
|
|
324
|
+
styleDeclarations: n,
|
|
325
|
+
text: () => e.textContent ?? "",
|
|
326
|
+
hasVoidContentDescendant: () => Array.from(e.querySelectorAll("*")).some((e) => m.has(e.tagName.toUpperCase()))
|
|
327
|
+
};
|
|
328
|
+
}, S = (e, t) => {
|
|
329
|
+
let n = e.parentElement;
|
|
330
|
+
return n === null || n === t ? [] : [x(n), ...S(n, t)];
|
|
331
|
+
}, C = (e) => {
|
|
332
|
+
let t = e.parentNode;
|
|
333
|
+
if (t !== null) {
|
|
334
|
+
for (; e.firstChild !== null;) t.insertBefore(e.firstChild, e);
|
|
335
|
+
t.removeChild(e);
|
|
336
|
+
}
|
|
337
|
+
}, w = (e) => {
|
|
338
|
+
let t = (t) => e.contains(t), n = (e) => t(e) && f.has(e.tagName.toUpperCase()), r = Array.from(e.querySelectorAll("*")).filter((t) => {
|
|
339
|
+
if (!n(t)) return !1;
|
|
340
|
+
let r = x(t);
|
|
341
|
+
return b(r) || ne(r, S(t, e));
|
|
342
|
+
});
|
|
343
|
+
r.forEach(C);
|
|
344
|
+
let i = (e, t) => {
|
|
345
|
+
let n = e.nextSibling;
|
|
346
|
+
if (n === null || n.nodeType !== Node.ELEMENT_NODE || !te(t, x(n))) return !1;
|
|
347
|
+
let r = n;
|
|
348
|
+
for (; r.firstChild !== null;) e.appendChild(r.firstChild);
|
|
349
|
+
return r.remove(), i(e, t), !0;
|
|
350
|
+
}, a = Array.from(e.querySelectorAll("*")).filter(n).map((e) => t(e) && i(e, x(e))).some(Boolean);
|
|
351
|
+
return r.length > 0 || a;
|
|
352
|
+
}, T = (e) => {
|
|
353
|
+
let t = (n) => n === 0 || !w(e) ? !1 : (t(n - 1), !0), n = t(10);
|
|
354
|
+
return n && e.normalize?.(), n;
|
|
355
|
+
};
|
|
356
|
+
//#endregion
|
|
251
357
|
//#region src/cli/commands/convert-html/preprocessor.ts
|
|
252
|
-
function
|
|
253
|
-
|
|
358
|
+
function E(e) {
|
|
359
|
+
D(e), j(e), F(e), L(e), re(e), z(e);
|
|
254
360
|
}
|
|
255
|
-
function
|
|
361
|
+
function D(e) {
|
|
256
362
|
let t = e.ownerDocument;
|
|
257
363
|
for (let n of Array.from(e.querySelectorAll("div[style]"))) {
|
|
258
364
|
if (n.closest("table")) continue;
|
|
259
|
-
let e =
|
|
260
|
-
if (!e ||
|
|
365
|
+
let e = P(n);
|
|
366
|
+
if (!e || M(e)) continue;
|
|
261
367
|
let r = t.createElement("aside");
|
|
262
|
-
r.style.backgroundColor = e, r.append(...Array.from(n.childNodes)),
|
|
368
|
+
r.style.backgroundColor = e, r.append(...Array.from(n.childNodes)), O(r), k(r), n.replaceWith(r);
|
|
263
369
|
}
|
|
264
370
|
}
|
|
265
|
-
function
|
|
371
|
+
function O(e) {
|
|
266
372
|
for (;;) {
|
|
267
373
|
let t = Array.from(e.querySelectorAll(":scope > div")).filter((e) => !e.getAttribute("style") && !e.getAttribute("class"));
|
|
268
374
|
if (t.length === 0) break;
|
|
269
375
|
for (let e of t) e.replaceWith(...Array.from(e.childNodes));
|
|
270
376
|
}
|
|
271
377
|
}
|
|
272
|
-
function
|
|
378
|
+
function k(e) {
|
|
273
379
|
for (let t of Array.from(e.querySelectorAll("p"))) {
|
|
274
380
|
let e = t.lastElementChild;
|
|
275
381
|
e?.tagName === "BR" && e.remove();
|
|
276
382
|
}
|
|
277
383
|
}
|
|
278
|
-
var
|
|
279
|
-
function
|
|
384
|
+
var A = 250;
|
|
385
|
+
function j(e) {
|
|
280
386
|
let t = e.querySelectorAll("[style*=\"background-color\"]");
|
|
281
|
-
for (let e of Array.from(t))
|
|
387
|
+
for (let e of Array.from(t)) M(e.style.backgroundColor) && (e.style.removeProperty("background-color"), e.getAttribute("style")?.trim() === "" && e.removeAttribute("style"), N(e) && e.replaceWith(...Array.from(e.childNodes)));
|
|
282
388
|
}
|
|
283
|
-
function
|
|
389
|
+
function M(e) {
|
|
284
390
|
if (!e) return !1;
|
|
285
391
|
let t = e.replace(/\s/g, "").toLowerCase();
|
|
286
392
|
if (t === "transparent") return !0;
|
|
@@ -288,12 +394,12 @@ function v(e) {
|
|
|
288
394
|
if (!n) return !1;
|
|
289
395
|
if ((n[4] === void 0 ? 1 : parseFloat(n[4])) === 0) return !0;
|
|
290
396
|
let r = parseInt(n[1], 10), i = parseInt(n[2], 10), a = parseInt(n[3], 10);
|
|
291
|
-
return r >=
|
|
397
|
+
return r >= A && i >= A && a >= A;
|
|
292
398
|
}
|
|
293
|
-
function
|
|
399
|
+
function N(e) {
|
|
294
400
|
return e.attributes.length > 0 ? !1 : (e.textContent ?? "").replace(/[\s\u00A0]/g, "").length === 0;
|
|
295
401
|
}
|
|
296
|
-
function
|
|
402
|
+
function P(e) {
|
|
297
403
|
if (e.style.backgroundColor) return e.style.backgroundColor;
|
|
298
404
|
let t = (e.getAttribute("style") ?? "").match(/background:\s*([^;]+)/i);
|
|
299
405
|
if (t) {
|
|
@@ -302,16 +408,16 @@ function b(e) {
|
|
|
302
408
|
}
|
|
303
409
|
return "";
|
|
304
410
|
}
|
|
305
|
-
function
|
|
411
|
+
function F(e) {
|
|
306
412
|
for (let t of Array.from(e.querySelectorAll("td, th"))) {
|
|
307
413
|
let e = t.querySelectorAll("p");
|
|
308
414
|
if (e.length !== 0) {
|
|
309
|
-
for (let t of Array.from(e))
|
|
310
|
-
|
|
415
|
+
for (let t of Array.from(e)) I(t);
|
|
416
|
+
U(t);
|
|
311
417
|
}
|
|
312
418
|
}
|
|
313
419
|
}
|
|
314
|
-
function
|
|
420
|
+
function I(e) {
|
|
315
421
|
if (e.innerHTML.trim() === "" || e.innerHTML.trim() === " ") {
|
|
316
422
|
e.remove();
|
|
317
423
|
return;
|
|
@@ -319,45 +425,45 @@ function S(e) {
|
|
|
319
425
|
let t = e.ownerDocument, n = t.createDocumentFragment();
|
|
320
426
|
n.append(...Array.from(e.childNodes)), n.append(t.createElement("br")), e.replaceWith(n);
|
|
321
427
|
}
|
|
322
|
-
function
|
|
428
|
+
function L(e) {
|
|
323
429
|
for (let t of Array.from(e.querySelectorAll("p"))) t.closest("td") || t.closest("th") || (t.textContent ?? "").replace(/[\s\u00A0]/g, "").length === 0 && t.remove();
|
|
324
430
|
}
|
|
325
|
-
function
|
|
431
|
+
function re(e) {
|
|
326
432
|
let t = e.ownerDocument;
|
|
327
433
|
for (let n of Array.from(e.querySelectorAll("del, strike"))) {
|
|
328
434
|
let e = t.createElement("s");
|
|
329
435
|
e.append(...Array.from(n.childNodes)), n.replaceWith(e);
|
|
330
436
|
}
|
|
331
437
|
}
|
|
332
|
-
var
|
|
333
|
-
function
|
|
334
|
-
let t = e.ownerDocument, n =
|
|
438
|
+
var R = /^[\u2022\u00B7][\s\u00A0]*|^-\s/;
|
|
439
|
+
function z(e) {
|
|
440
|
+
let t = e.ownerDocument, n = V(e);
|
|
335
441
|
for (let e of n) {
|
|
336
442
|
let n = t.createElement("ul");
|
|
337
443
|
e[0].before(n);
|
|
338
|
-
for (let t of e)
|
|
444
|
+
for (let t of e) B(t, n);
|
|
339
445
|
}
|
|
340
446
|
}
|
|
341
|
-
function
|
|
342
|
-
let n = e.ownerDocument.createElement("li"), r =
|
|
343
|
-
r && (r.textContent = (r.textContent ?? "").replace(
|
|
447
|
+
function B(e, t) {
|
|
448
|
+
let n = e.ownerDocument.createElement("li"), r = W(e);
|
|
449
|
+
r && (r.textContent = (r.textContent ?? "").replace(R, "")), n.append(...Array.from(e.childNodes)), t.appendChild(n), e.remove();
|
|
344
450
|
}
|
|
345
|
-
function
|
|
451
|
+
function V(e) {
|
|
346
452
|
let t = [], n = Array.from(e.childNodes);
|
|
347
453
|
for (let e of n) {
|
|
348
454
|
if (e.nodeType !== Node.ELEMENT_NODE) continue;
|
|
349
455
|
let n = e;
|
|
350
|
-
if (!(n.tagName === "P" &&
|
|
351
|
-
let r = t[t.length - 1], i =
|
|
456
|
+
if (!(n.tagName === "P" && R.test(n.textContent ?? ""))) continue;
|
|
457
|
+
let r = t[t.length - 1], i = H(n);
|
|
352
458
|
r && i !== null && r[r.length - 1] === i ? r.push(n) : t.push([n]);
|
|
353
459
|
}
|
|
354
460
|
return t;
|
|
355
461
|
}
|
|
356
|
-
function
|
|
462
|
+
function H(e) {
|
|
357
463
|
let t = e.previousSibling;
|
|
358
464
|
return t && t.nodeType === Node.ELEMENT_NODE ? t : null;
|
|
359
465
|
}
|
|
360
|
-
function
|
|
466
|
+
function U(e) {
|
|
361
467
|
for (;;) {
|
|
362
468
|
let t = e.lastChild;
|
|
363
469
|
if (!t) break;
|
|
@@ -366,17 +472,17 @@ function A(e) {
|
|
|
366
472
|
t.remove();
|
|
367
473
|
}
|
|
368
474
|
}
|
|
369
|
-
function
|
|
475
|
+
function W(e) {
|
|
370
476
|
if (e.nodeType === Node.TEXT_NODE) return e;
|
|
371
477
|
for (let t of Array.from(e.childNodes)) {
|
|
372
|
-
let e =
|
|
478
|
+
let e = W(t);
|
|
373
479
|
if (e) return e;
|
|
374
480
|
}
|
|
375
481
|
return null;
|
|
376
482
|
}
|
|
377
483
|
//#endregion
|
|
378
484
|
//#region src/cli/commands/convert-html/sanitizer.ts
|
|
379
|
-
var
|
|
485
|
+
var G = {
|
|
380
486
|
B: !0,
|
|
381
487
|
STRONG: !0,
|
|
382
488
|
I: !0,
|
|
@@ -411,32 +517,32 @@ var M = {
|
|
|
411
517
|
SUMMARY: !0,
|
|
412
518
|
IMG: /* @__PURE__ */ new Set(["src", "style"])
|
|
413
519
|
};
|
|
414
|
-
function
|
|
415
|
-
|
|
520
|
+
function K(e) {
|
|
521
|
+
Y(e);
|
|
416
522
|
}
|
|
417
|
-
function
|
|
523
|
+
function q(e) {
|
|
418
524
|
let t = Array.from(e.childNodes);
|
|
419
525
|
for (let n of t) e.before(n);
|
|
420
526
|
return e.remove(), t.filter((e) => e.nodeType === e.ELEMENT_NODE);
|
|
421
527
|
}
|
|
422
|
-
function
|
|
528
|
+
function J(e, t) {
|
|
423
529
|
for (let n of Array.from(e.attributes)) (t === !0 || !t.has(n.name)) && e.removeAttribute(n.name);
|
|
424
530
|
}
|
|
425
|
-
function
|
|
531
|
+
function Y(e) {
|
|
426
532
|
let t = Array.from(e.childNodes);
|
|
427
533
|
for (let e of t) {
|
|
428
534
|
if (e.nodeType !== e.ELEMENT_NODE) continue;
|
|
429
|
-
let n = e, r =
|
|
535
|
+
let n = e, r = G[n.tagName];
|
|
430
536
|
if (r === void 0) {
|
|
431
|
-
t.push(...
|
|
537
|
+
t.push(...q(n));
|
|
432
538
|
continue;
|
|
433
539
|
}
|
|
434
|
-
|
|
540
|
+
J(n, r), Y(n);
|
|
435
541
|
}
|
|
436
542
|
}
|
|
437
543
|
//#endregion
|
|
438
544
|
//#region src/cli/commands/convert-html/id-generator.ts
|
|
439
|
-
function
|
|
545
|
+
function X() {
|
|
440
546
|
let e = /* @__PURE__ */ new Map();
|
|
441
547
|
return (t) => {
|
|
442
548
|
let n = (e.get(t) ?? 0) + 1;
|
|
@@ -445,12 +551,12 @@ function L() {
|
|
|
445
551
|
}
|
|
446
552
|
//#endregion
|
|
447
553
|
//#region src/cli/commands/convert-html/block-builder.ts
|
|
448
|
-
function
|
|
449
|
-
let t =
|
|
450
|
-
for (let r of Array.from(e.childNodes))
|
|
554
|
+
function ie(e) {
|
|
555
|
+
let t = X(), n = [];
|
|
556
|
+
for (let r of Array.from(e.childNodes)) ae(r, n, t);
|
|
451
557
|
return n;
|
|
452
558
|
}
|
|
453
|
-
function
|
|
559
|
+
function ae(e, t, n) {
|
|
454
560
|
if (e.nodeType === Node.TEXT_NODE) {
|
|
455
561
|
let r = e.textContent?.trim() ?? "";
|
|
456
562
|
r && t.push({
|
|
@@ -513,7 +619,7 @@ function z(e, t, n) {
|
|
|
513
619
|
return;
|
|
514
620
|
}
|
|
515
621
|
if (i === "IMG") {
|
|
516
|
-
let e = r.getAttribute("src") ?? "", i =
|
|
622
|
+
let e = r.getAttribute("src") ?? "", i = le(r, "width");
|
|
517
623
|
t.push({
|
|
518
624
|
id: n("image"),
|
|
519
625
|
type: "image",
|
|
@@ -534,15 +640,15 @@ function z(e, t, n) {
|
|
|
534
640
|
return;
|
|
535
641
|
}
|
|
536
642
|
if (i === "UL" || i === "OL") {
|
|
537
|
-
|
|
643
|
+
Z(r, i === "OL" ? "ordered" : "unordered", 0, t, n);
|
|
538
644
|
return;
|
|
539
645
|
}
|
|
540
646
|
if (i === "TABLE") {
|
|
541
|
-
|
|
647
|
+
oe(r, t, n);
|
|
542
648
|
return;
|
|
543
649
|
}
|
|
544
650
|
if (i === "ASIDE") {
|
|
545
|
-
|
|
651
|
+
se(r, t, n);
|
|
546
652
|
return;
|
|
547
653
|
}
|
|
548
654
|
t.push({
|
|
@@ -551,7 +657,7 @@ function z(e, t, n) {
|
|
|
551
657
|
data: { text: r.innerHTML }
|
|
552
658
|
});
|
|
553
659
|
}
|
|
554
|
-
function
|
|
660
|
+
function Z(e, t, n, r, i) {
|
|
555
661
|
let a = e.getAttribute("start"), o = a ? Number(a) : null, s = Array.from(e.children).filter((e) => e.tagName === "LI");
|
|
556
662
|
for (let [e, a] of s.entries()) {
|
|
557
663
|
let s = a.cloneNode(!0), c = [];
|
|
@@ -568,17 +674,17 @@ function B(e, t, n, r, i) {
|
|
|
568
674
|
start: e === 0 && o !== null ? o : null
|
|
569
675
|
}
|
|
570
676
|
});
|
|
571
|
-
for (let e of c)
|
|
677
|
+
for (let e of c) Z(e, e.tagName === "OL" ? "ordered" : "unordered", n + 1, r, i);
|
|
572
678
|
}
|
|
573
679
|
}
|
|
574
|
-
function
|
|
680
|
+
function oe(e, t, n) {
|
|
575
681
|
let r = n("table"), i = Array.from(e.querySelectorAll("tr")), a = [];
|
|
576
682
|
for (let e of i) {
|
|
577
|
-
let i = Array.from(e.querySelectorAll("td, th")).map((e) =>
|
|
683
|
+
let i = Array.from(e.querySelectorAll("td, th")).map((e) => Q(e, r, t, n));
|
|
578
684
|
a.push(i);
|
|
579
685
|
}
|
|
580
686
|
let o = i[0] ? Array.from(i[0].querySelectorAll("td, th")) : [], s = o.some((e) => e.tagName === "TH"), c = o.map((e) => {
|
|
581
|
-
let t =
|
|
687
|
+
let t = $(e, "width");
|
|
582
688
|
if (t) {
|
|
583
689
|
let e = parseInt(t, 10);
|
|
584
690
|
return isNaN(e) ? null : e;
|
|
@@ -596,10 +702,10 @@ function V(e, t, n) {
|
|
|
596
702
|
}, u = t.findIndex((e) => e.parent === r);
|
|
597
703
|
u >= 0 ? t.splice(u, 0, l) : t.push(l);
|
|
598
704
|
}
|
|
599
|
-
function
|
|
600
|
-
let a = r("callout"), o =
|
|
705
|
+
function se(e, t, r) {
|
|
706
|
+
let a = r("callout"), o = $(e, "background-color"), s = o && !n(o) && !i(o) ? d(o, "bg") : null, c = [];
|
|
601
707
|
for (let n of Array.from(e.childNodes)) {
|
|
602
|
-
let e =
|
|
708
|
+
let e = ce(n, a, t, r);
|
|
603
709
|
e && c.push(e);
|
|
604
710
|
}
|
|
605
711
|
let l = t.findIndex((e) => e.parent === a), u = {
|
|
@@ -613,7 +719,7 @@ function H(e, t, r) {
|
|
|
613
719
|
};
|
|
614
720
|
l >= 0 ? t.splice(l, 0, u) : t.push(u);
|
|
615
721
|
}
|
|
616
|
-
function
|
|
722
|
+
function Q(e, t, r, a) {
|
|
617
723
|
let o = e.innerHTML.trim();
|
|
618
724
|
if (!o) return {
|
|
619
725
|
blocks: [],
|
|
@@ -627,14 +733,14 @@ function U(e, t, r, a) {
|
|
|
627
733
|
parent: t,
|
|
628
734
|
data: { text: o }
|
|
629
735
|
});
|
|
630
|
-
let c =
|
|
736
|
+
let c = $(e, "background-color"), l = $(e, "color"), u = c && !n(c) && !i(c) ? d(c, "bg") : null;
|
|
631
737
|
return {
|
|
632
738
|
blocks: [s],
|
|
633
739
|
color: u,
|
|
634
740
|
textColor: l ? d(l, "text") : null
|
|
635
741
|
};
|
|
636
742
|
}
|
|
637
|
-
function
|
|
743
|
+
function ce(e, t, n, r) {
|
|
638
744
|
if (e.nodeType === Node.ELEMENT_NODE) {
|
|
639
745
|
let i = e, a = r("paragraph");
|
|
640
746
|
return n.push({
|
|
@@ -657,17 +763,17 @@ function W(e, t, n, r) {
|
|
|
657
763
|
}
|
|
658
764
|
return null;
|
|
659
765
|
}
|
|
660
|
-
function
|
|
661
|
-
let n =
|
|
766
|
+
function le(e, t) {
|
|
767
|
+
let n = $(e, t);
|
|
662
768
|
if (!n) return null;
|
|
663
769
|
let r = parseInt(n, 10);
|
|
664
770
|
return isNaN(r) ? null : r;
|
|
665
771
|
}
|
|
666
|
-
function
|
|
772
|
+
function $(e, t) {
|
|
667
773
|
let n = e.getAttribute("style");
|
|
668
774
|
if (!n) return null;
|
|
669
775
|
let r = RegExp(`(?<![\\-a-z])${t}\\s*:\\s*([^;]+)`).exec(n);
|
|
670
776
|
return r ? r[1].trim() : null;
|
|
671
777
|
}
|
|
672
778
|
//#endregion
|
|
673
|
-
export {
|
|
779
|
+
export { u as a, T as i, K as n, i as o, E as r, n as s, ie as t };
|
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as e from "node:fs";
|
|
2
2
|
//#region src/cli/commands/migrationContent.ts
|
|
3
|
-
var t = "# Migrating from EditorJS to Blok\n\nThis guide covers the breaking changes when migrating from EditorJS to Blok.\n\n## Table of Contents\n\n- [Supported Editor.js Versions](#supported-editorjs-versions)\n- [Core Changes](#core-changes)\n- [Data Attributes](#data-attributes)\n- [CSS Classes](#css-classes)\n- [Bundled Tools](#bundled-tools)\n - [Tool Configuration](#tool-configuration)\n - [Lifecycle Hooks](#lifecycle-hooks)\n - [Delimiter → Divider](#delimiter--divider)\n- [Migrating Custom Tools](#migrating-custom-tools)\n - [Custom Block Tools (port unchanged)](#custom-block-tools-port-unchanged)\n - [Custom Inline Tools (breaking rewrite)](#custom-inline-tools-breaking-rewrite)\n- [Saved Content / Data Migration](#saved-content--data-migration)\n - [Auto-Migrated on Load](#auto-migrated-on-load)\n - [Dropped Fields](#dropped-fields)\n- [Configuration Defaults](#configuration-defaults)\n- [New API Methods](#new-api-methods)\n- [DOM Selectors](#dom-selectors)\n- [E2E Test Selectors](#e2e-test-selectors)\n\n---\n\n## Supported Editor.js Versions\n\nBlok targets the **Editor.js 2.x** line. The [codemod](./codemod) is built against the package versions a typical Editor.js 2.x project ships — `@editorjs/editorjs@^2.28.x`, `@editorjs/header@^2.8.x`, `@editorjs/paragraph@^2.11.x`, `@editorjs/list@^1.9.x` — and the runtime auto-migration reads the saved-data shapes those versions produce. Older 2.x data formats are still recognized; the matrix below describes coverage per block/tool, not per Editor.js point release.\n\nThere is no pinned single source version: Blok is API-compatible with the Editor.js 2.x `BlockTool` / `InlineTool` contracts, so what matters is **which tools your content uses**, covered below.\n\n### Compatibility Matrix\n\nHow each Editor.js block/tool is handled. \"Auto-migrated at runtime\" means the conversion happens on `render()` with no script needed; \"Codemod\" means the [codemod](./codemod) rewrites saved JSON; \"Drop-in unchanged\" means the data passes through as-is; \"Not bundled\" means Blok ships no equivalent tool, so you must register your own (a ported Editor.js block tool works unchanged — see [Migrating Custom Tools](#migrating-custom-tools)) or the block renders as a preserved stub with a `console.warn` and round-trips untouched on save.\n\n| Editor.js block / tool | Migration status | Notes |\n|------------------------|------------------|-------|\n| `paragraph` | Drop-in unchanged | Bundled as `Blok.Paragraph` (the default block). |\n| `header` | Drop-in unchanged | Bundled as `Blok.Header`. |\n| `list` (nested **and** string-array items) | Auto-migrated at runtime **+** codemod | Each item expands to a flat `list` block. |\n| `checklist` (standalone tool) | Auto-migrated at runtime **+** codemod | Becomes `list` blocks with checklist style. |\n| `toggleList` | Auto-migrated at runtime **+** codemod → `toggle` | Becomes a `toggle` block (or a toggle `header` when `titleVariant` is set); body blocks flatten into parented children. |\n| `callout` (legacy `body`/`variant` shape) | Auto-migrated at runtime **+** codemod | Legacy `{ body, variant, emoji }` → flat `{ emoji, textColor, backgroundColor }`; body blocks flatten into parented children. |\n| `image`, `simple-image` | Auto-migrated at runtime **+** codemod | `file.url` (or `simple-image`'s flat `url`) → `url`; `withBorder` → `frame`, `stretched: true` → `size: full`. `withBackground` is dropped (warns). |\n| `linkTool` | Auto-migrated at runtime **+** codemod → `bookmark` | `meta` flattens onto the bookmark shape; `meta.site_name` is dropped (warns). |\n| `delimiter` | Codemod renames type → `divider` | Data format is identical (`{}`). Also recognized at render, so existing data works without the rename. |\n| `quote` | Auto-migrated at runtime **+** codemod | `caption` becomes a trailing `paragraph` sibling; `alignment` is dropped (warns). The quote text/size render unchanged. |\n| `code` | Drop-in unchanged | Passes through to Blok's bundled `code` tool. |\n| `table` (HTML-string cells) | Auto-migrated at runtime **+** codemod | Each non-empty cell becomes a `{ blocks: [id] }` cell-ref plus a child `paragraph` (`parent` = table id); empty cells become `{ blocks: [] }`. `withHeadings` is preserved. Cells already in block-ref shape pass through. |\n| `embed` | Drop-in unchanged | Bundled as `Blok.Embed`. `{ service, source, embed, width, height, caption }` map 1:1; iframe providers render correctly (Blok-only fields default). |\n| `raw` | Auto-migrated at runtime **+** codemod → `code` | Blok has no raw tool; `{ html }` becomes a `code` block showing the markup as source verbatim. |\n| `warning` | Auto-migrated at runtime **+** codemod → `callout` | Becomes a ⚠️ orange `callout`; `title` and `message` become child `paragraph` blocks. |\n| `attaches` | Auto-migrated at runtime **+** codemod → `bookmark` | `file.url` + `title` map to a `bookmark`; file metadata (`name`/`size`/`extension`) is dropped (warns). |\n| `marker`, `inlineCode` (inline) | Not bundled | Register your own (port unchanged or wrap with `wrapLegacyInlineTool`). See [Custom Inline Tools](#custom-inline-tools). |\n| Other third-party block tools (`personality`, `button`, audio, …) | Not bundled | No built-in equivalent. Register a tool to render them; otherwise the block is preserved as a stub (warns) and survives a save/load round-trip. |\n\n> **Heads-up:** any block type Blok doesn't recognize and has no registered tool for renders as a stub and logs `Tool «<name>» is not found`. Its data is preserved, so registering the tool later restores the block.\n\n---\n\n## Core Changes\n\n### Class Name\n\n```diff\n- import EditorJS from '@editorjs/editorjs';\n+ import Blok from '@bloklabs/core';\n\n- const editor = new EditorJS({ ... });\n+ const editor = new Blok({ ... });\n```\n\n`Blok` is also exported as the **default export** and under an `EditorJS` alias, so you can keep your existing variable name and only change the import path — a minimal-diff, drop-in migration:\n\n```diff\n- import EditorJS from '@editorjs/editorjs';\n+ import { EditorJS } from '@bloklabs/core';\n\n const editor = new EditorJS({ ... }); // unchanged\n```\n\n`import Blok`, `import { Blok }`, and `import { EditorJS }` all resolve to the same class.\n\n### Default Holder\n\nThe default holder ID changed from `editorjs` to `blok`:\n\n```diff\n- <div id=\"editorjs\"></div>\n+ <div id=\"blok\"></div>\n```\n\nOr specify explicitly:\n\n```javascript\nconst editor = new Blok({\n holder: 'my-editor', // works the same as before\n});\n```\n\n### TypeScript Types\n\n```diff\n- import type { EditorConfig, OutputData } from '@editorjs/editorjs';\n+ import type { BlokConfig, OutputData } from '@bloklabs/core';\n```\n\n---\n\n## Data Attributes\n\nBlok uses `data-blok-*` attributes instead of EditorJS's mixed naming conventions.\n\n| EditorJS | Blok |\n|----------|------|\n| `data-id` | `data-blok-id` |\n| `data-item-name` | `data-blok-item-name` |\n| `data-empty` | `data-blok-empty` |\n| `.ce-block--selected` (class) | `data-blok-selected=\"true\"` |\n| — | `data-blok-component` (tool name) |\n| — | `data-blok-interface` (element type) |\n| — | `data-blok-testid` (testing) |\n| — | `data-blok-opened` (toolbar state) |\n| — | `data-blok-placeholder` |\n| — | `data-blok-stretched` |\n| — | `data-blok-focused` |\n| — | `data-blok-popover-opened` |\n| — | `data-blok-tool` (tool name on rendered element) |\n| — | `data-blok-dragging` (block being dragged) |\n| — | `data-blok-hidden` (hidden state) |\n| — | `data-blok-rtl` (RTL mode) |\n| — | `data-blok-drag-handle` (drag handle element) |\n| — | `data-blok-overlay` (selection overlay) |\n| — | `data-blok-overlay-rectangle` (selection rectangle) |\n\n### Querying Blocks\n\n```diff\n- document.querySelector('[data-id=\"abc123\"]');\n+ document.querySelector('[data-blok-id=\"abc123\"]');\n\n- document.querySelector('[data-item-name=\"bold\"]');\n+ document.querySelector('[data-blok-item-name=\"bold\"]');\n```\n\n---\n\n## CSS Classes\n\nBlok replaces BEM class names with data attributes for selection.\n\n### Editor Wrapper\n\n| EditorJS | Blok |\n|----------|------|\n| `.codex-editor` | `[data-blok-editor]` |\n| `.codex-editor__redactor` | `[data-blok-redactor]` |\n| `.codex-editor--rtl` | `[data-blok-rtl=\"true\"]` |\n\n### Block Elements\n\n| EditorJS | Blok |\n|----------|------|\n| `.ce-block` | `[data-blok-element]` |\n| `.ce-block--selected` | `[data-blok-selected=\"true\"]` |\n| `.ce-block--stretched` | `[data-blok-stretched=\"true\"]` |\n| `.ce-block--focused` | `[data-blok-focused=\"true\"]` |\n| `.ce-block__content` | `[data-blok-element-content]` |\n\n### Toolbar\n\n| EditorJS | Blok |\n|----------|------|\n| `.ce-toolbar` | `[data-blok-toolbar]` |\n| `.ce-toolbar__plus` | `[data-blok-testid=\"plus-button\"]` |\n| `.ce-toolbar__settings-btn` | `[data-blok-settings-toggler]` |\n| `.ce-toolbar__actions` | `[data-blok-testid=\"toolbar-actions\"]` |\n| `.ce-toolbox` | `[data-blok-toolbox]` |\n| `.ce-toolbox--opened` | `[data-blok-toolbox][data-blok-opened=\"true\"]` |\n\n### Inline Toolbar\n\n| EditorJS | Blok |\n|----------|------|\n| `.ce-inline-toolbar` | `[data-blok-testid=\"inline-toolbar\"]` |\n| `.ce-inline-tool` | `[data-blok-testid=\"inline-tool\"]` |\n| `.ce-inline-tool--link` | `[data-blok-testid=\"inline-tool-link\"]` |\n| `.ce-inline-tool--bold` | `[data-blok-testid=\"inline-tool-bold\"]` |\n| `.ce-inline-tool--italic` | `[data-blok-testid=\"inline-tool-italic\"]` |\n\n### Popover\n\n| EditorJS | Blok |\n|----------|------|\n| `.ce-popover` | `[data-blok-popover]` |\n| `.ce-popover--opened` | `[data-blok-popover][data-blok-opened=\"true\"]` |\n| `.ce-popover__container` | `[data-blok-popover-container]` |\n| `.ce-popover-item` | `[data-blok-testid=\"popover-item\"]` |\n| `.ce-popover-item--focused` | `[data-blok-focused=\"true\"]` |\n| `.ce-popover-item--confirmation` | `[data-blok-confirmation=\"true\"]` |\n| `.ce-popover-item__icon` | `[data-blok-testid=\"popover-item-icon\"]` |\n| `.ce-popover-item__icon--tool` | `[data-blok-testid=\"popover-item-icon-tool\"]` |\n\n### Tool-Specific Classes\n\n| EditorJS | Blok |\n|----------|------|\n| `.ce-paragraph` | `[data-blok-tool=\"paragraph\"]` |\n| `.ce-header` | `[data-blok-tool=\"header\"]` |\n\n### Conversion Toolbar & Settings\n\n| EditorJS | Blok |\n|----------|------|\n| `.ce-conversion-toolbar` | `[data-blok-testid=\"conversion-toolbar\"]` |\n| `.ce-conversion-tool` | `[data-blok-testid=\"conversion-tool\"]` |\n| `.ce-settings` | `[data-blok-testid=\"block-settings\"]` |\n| `.ce-tune` | `[data-blok-testid=\"block-tune\"]` |\n\n### Other Elements\n\n| EditorJS | Blok |\n|----------|------|\n| `.ce-stub` | `[data-blok-stub]` |\n| `.ce-drag-handle` | `[data-blok-drag-handle]` |\n| `.ce-ragged-right` | `[data-blok-ragged-right=\"true\"]` |\n\n### CDX List Classes\n\n| EditorJS | Blok |\n|----------|------|\n| `.cdx-list` | `[data-blok-list]` |\n| `.cdx-list__item` | `[data-blok-list-item]` |\n| `.cdx-list--ordered` | `[data-blok-list=\"ordered\"]` |\n| `.cdx-list--unordered` | `[data-blok-list=\"unordered\"]` |\n\n### CDX Utility Classes\n\n| EditorJS | Blok |\n|----------|------|\n| `.cdx-button` | `[data-blok-button]` |\n| `.cdx-input` | `[data-blok-input]` |\n| `.cdx-loader` | `[data-blok-loader]` |\n| `.cdx-search-field` | `[data-blok-search-field]` |\n\n---\n\n## Bundled Tools\n\nBlok includes Header and Paragraph tools. No external packages needed:\n\n```diff\n- import Header from '@editorjs/header';\n- import Paragraph from '@editorjs/paragraph';\n\n+ import Blok from '@bloklabs/core';\n\nconst editor = new Blok({\n tools: {\n- header: Header,\n- paragraph: Paragraph,\n+ header: Blok.Header,\n+ paragraph: Blok.Paragraph, // optional, it's the default\n },\n});\n```\n\n### Tool Configuration\n\nBoth bundled tools accept configuration options:\n\n#### HeaderConfig\n\n```typescript\nimport type { HeaderConfig } from '@bloklabs/core';\n\nconst editor = new Blok({\n tools: {\n header: {\n class: Blok.Header,\n config: {\n placeholder: 'Enter a heading',\n levels: [2, 3, 4], // Restrict to H2-H4 only\n defaultLevel: 2, // Default to H2\n } as HeaderConfig,\n },\n },\n});\n```\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `placeholder` | `string` | `''` | Placeholder text for empty header |\n| `levels` | `number[]` | `[1,2,3,4,5,6]` | Available heading levels (1-6) |\n| `defaultLevel` | `number` | `2` | Default heading level |\n\n#### ParagraphConfig\n\n```typescript\nimport type { ParagraphConfig } from '@bloklabs/core';\n\nconst editor = new Blok({\n tools: {\n paragraph: {\n class: Blok.Paragraph,\n config: {\n placeholder: 'Start typing...',\n preserveBlank: true,\n } as ParagraphConfig,\n },\n },\n});\n```\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `placeholder` | `string` | `''` | Placeholder text for empty paragraph |\n| `preserveBlank` | `boolean` | `false` | Keep empty paragraphs when saving |\n\n### Delimiter → Divider\n\nEditor.js's `@editorjs/delimiter` is replaced by Blok's built-in `divider` tool:\n\n```diff\n- import Delimiter from '@editorjs/delimiter';\n\nconst editor = new Blok({\n tools: {\n- delimiter: Delimiter,\n+ divider: Blok.Divider,\n },\n});\n```\n\n**Saved data migration:** The block type name changed from `\"delimiter\"` to `\"divider\"`. The data format is identical (empty `{}`):\n\n```diff\n {\n- \"type\": \"delimiter\",\n+ \"type\": \"divider\",\n \"data\": {}\n }\n```\n\n> **Note:** Blok automatically recognizes `\"delimiter\"` blocks and renders them as dividers, so existing articles work without data migration. However, renaming the type in your database is recommended for consistency.\n\n---\n\n## Migrating Custom Tools\n\nIf your project ships its own tools, here's what changes and what doesn't.\n\n### Custom Block Tools (port unchanged)\n\n**Custom block tools work as-is.** Blok's `BlockTool` interface keeps the same shape as EditorJS, read by the **same keys**:\n\n- Methods: `render()`, `save()`, `validate()`, `renderSettings()`, `merge()`, `onPaste()`, and the lifecycle hooks `rendered()`, `updated()`, `removed()`, `moved()`.\n- Statics: `toolbox`, `conversionConfig`, `pasteConfig`, `isReadOnlySupported`, `enableLineBreaks`.\n\nIn most cases you can drop your existing block tool into Blok without touching its code.\n\n#### New optional `setReadOnly(state)` method\n\nBlok adds an **optional** `setReadOnly(state: boolean)` method. It is not a breaking change:\n\n- **If you omit it**, read-only toggling still works — Blok falls back to a full re-render of the block (graceful degradation).\n- **If you implement it**, Blok performs a cheaper in-place toggle instead of re-rendering.\n\n```typescript\nclass MyTool {\n // ...render / save / validate as before...\n\n // Optional: enables the cheaper in-place read-only toggle\n setReadOnly(state: boolean): void {\n this.input.contentEditable = state ? 'false' : 'true';\n }\n}\n```\n\n#### New optional statics\n\nTwo new **optional** statics exist for advanced cases; existing tools never need them:\n\n- `titleKey` — a translation key for the tool's toolbox title (i18n).\n- `provides` — lets one class register multiple block types.\n\n### Custom Inline Tools\n\n**This is the one API difference for custom tools.** EditorJS inline tools built a DOM button and used imperative `surround` / `checkState` / `renderActions` / `clear` methods. Blok's `InlineTool` (`types/tools/inline-tool.d.ts`) instead expects `render()` to return a declarative `MenuConfig` object with `onActivate` / `isActive` callbacks.\n\nYou have two paths:\n\n#### Fast path — wrap with `wrapLegacyInlineTool`\n\nTo keep an existing EditorJS inline tool working without a rewrite, wrap its class:\n\n```typescript\nimport { wrapLegacyInlineTool } from '@bloklabs/core';\nimport MarkerTool from './marker-tool'; // your existing EditorJS inline tool\n\nconst editor = new Blok({\n tools: {\n marker: wrapLegacyInlineTool(MarkerTool),\n },\n});\n```\n\nThe shim adapts the legacy contract automatically: it calls your tool's `render()` for the icon, routes `onActivate` → `surround(range)` and `isActive` → `checkState(selection)`, and forwards the static `title` / `shortcut` / `sanitize`. Without wrapping, Blok silently skips a legacy `render()` that returns an `HTMLElement`, so the tool would vanish from the inline toolbar. `renderActions()` / `clear()` are not bridged.\n\n#### Native path — rewrite to `MenuConfig` (recommended)\n\nFor full access to Blok features (nested items, confirmation), port to the native shape:\n\n#### Before (EditorJS)\n\n```typescript\nclass MarkerTool {\n static get isInline() { return true; }\n static get title() { return 'Marker'; }\n\n private button: HTMLButtonElement;\n\n render(): HTMLElement {\n this.button = document.createElement('button');\n this.button.innerHTML = '<svg>...</svg>';\n return this.button;\n }\n\n // Apply / remove formatting on the current range\n surround(range: Range): void {\n document.execCommand('backColor', false, '#ffd700');\n }\n\n // Reflect active state by inspecting the selection\n checkState(selection: Selection): boolean {\n const isActive = /* ...inspect selection... */;\n this.button.classList.toggle('active', isActive);\n return isActive;\n }\n\n renderActions(): HTMLElement { /* extra UI */ }\n clear(): void { /* reset extra UI */ }\n}\n```\n\n#### After (Blok)\n\n```typescript\nimport type { InlineTool, MenuConfig } from '@bloklabs/core';\n\nclass MarkerTool implements InlineTool {\n static isInline = true;\n static title = 'Marker';\n static titleKey = 'marker'; // optional i18n key\n\n // render() now returns a MenuConfig object, not an HTMLElement\n render(): MenuConfig {\n return {\n icon: '<svg>...</svg>',\n name: 'marker',\n // surround(range) logic moves here\n onActivate: () => {\n document.execCommand('backColor', false, '#ffd700');\n },\n // checkState(selection) logic moves here\n isActive: () => {\n const selection = window.getSelection();\n return selection ? /* ...inspect selection... */ false : false;\n },\n };\n }\n}\n```\n\n> See `src/components/inline-tools/inline-tool-bold.ts` for Blok's canonical inline tool. Its `render()` returns a `MenuConfig` with `onActivate` (apply/remove bold) and `isActive` (read the selection).\n\n#### Mapping reference\n\n| EditorJS | Blok |\n|----------|------|\n| `render()` returns an `HTMLElement` button | `render()` returns a `MenuConfig` `{ icon, name, onActivate, isActive }` |\n| `surround(range)` | logic moves into `onActivate()` |\n| `checkState(selection)` | logic moves into `isActive()` |\n| `renderActions()` / `clear()` | **removed** — no direct equivalent; use `MenuConfig` `children` for nested items |\n| `static get isInline()` / `static get title()` | still apply (plus optional `titleKey` for i18n) |\n\n---\n\n## Saved Content / Data Migration\n\nExisting EditorJS output data keeps working. Blok converts legacy block shapes to its own format **automatically when the block renders** — no migration script needed.\n\n### Auto-Migrated on Load\n\nThese EditorJS block types and data shapes are recognized and converted on `render()`:\n\n| EditorJS block / shape | Becomes in Blok |\n|------------------------|-----------------|\n| `list` (nested items **and** string-array items) | Blok `list` |\n| `checklist` (standalone tool) | Blok `list` (checklist style) |\n| `image` `{ file: { url }, withBorder, stretched }` | Blok `image` |\n| `table` (HTML-string cells) | Blok `table` |\n| `header` | Blok `header` |\n| `code` | Blok `code` |\n| `delimiter` | Blok `divider` |\n| `linkTool` | Blok `bookmark` |\n\nNo action is required — load your existing data and it renders correctly.\n\n### Dropped Fields\n\nA few EditorJS fields have no Blok equivalent and are **dropped** on load. The block still renders; only these specific fields are lost:\n\n| Block | Dropped field(s) |\n|-------|------------------|\n| `quote` | `caption`, `alignment` |\n| `image` | `withBackground` |\n| `linkTool` | `meta.site_name` |\n| `list` item | `meta` |\n\nWhen migration drops one of these, Blok emits a `console.warn` (prefixed `[Blok migration]`) naming the block type and field, so the loss is visible in the console rather than silent. If you depend on any of these, capture them before migrating.\n\n---\n\n## Configuration Defaults\n\n| Option | EditorJS | Blok |\n|--------|----------|------|\n| `holder` | `\"editorjs\"` | `\"blok\"` |\n| `defaultBlock` | `\"paragraph\"` | `\"paragraph\"` |\n\n---\n\n## New API Methods\n\nBlok exposes shorthand methods directly on the instance:\n\n```javascript\nconst editor = new Blok({ ... });\n\n// Shorthand methods\nawait editor.save(); // Same as editor.saver.save()\neditor.clear(); // Same as editor.blocks.clear()\nawait editor.render(data);// Same as editor.blocks.render(data)\neditor.focus(); // Same as editor.caret.focus()\n\n// Event methods\neditor.on('change', handler); // Same as editor.events.on()\neditor.off('change', handler); // Same as editor.events.off()\neditor.emit('custom', data); // Same as editor.events.emit()\n```\n\n---\n\n## DOM Selectors\n\nBlok uses consistent `data-blok-interface` attributes for major UI components:\n\n```javascript\n// Find the editor wrapper\nconst editorWrapper = document.querySelector('[data-blok-interface=\"blok\"]');\n\n// Find inline toolbar\nconst inlineToolbar = document.querySelector('[data-blok-interface=\"inline-toolbar\"]');\n\n// Find tooltip\nconst tooltip = document.querySelector('[data-blok-interface=\"tooltip\"]');\n```\n\nThese selectors are stable and recommended for programmatic access to Blok's UI components.\n\n### Key Changes\n\n1. **Prefix custom classes** with `blok-` instead of `ce-` or `cdx-`\n2. **Use `data-blok-*`** attributes instead of `data-*`\n\n---\n\n## E2E Test Selectors\n\nUpdate your test selectors to use Blok's `data-blok-testid` attributes:\n\n| EditorJS | Blok |\n|----------|------|\n| `[data-cy=editorjs]` | `[data-blok-testid=\"blok-editor\"]` |\n| `.ce-block` | `[data-blok-element]` |\n| `.ce-block__content` | `[data-blok-element-content]` |\n| `.ce-toolbar` | `[data-blok-toolbar]` |\n| `.ce-toolbar__plus` | `[data-blok-testid=\"plus-button\"]` |\n| `.ce-toolbar__settings-btn` | `[data-blok-settings-toggler]` |\n| `.ce-toolbar__actions` | `[data-blok-testid=\"toolbar-actions\"]` |\n| `.ce-toolbox` | `[data-blok-toolbox]` |\n| `.ce-inline-toolbar` | `[data-blok-testid=\"inline-toolbar\"]` |\n| `.ce-popover` | `[data-blok-popover]` |\n| `.ce-popover-item` | `[data-blok-testid=\"popover-item\"]` |\n| `[data-item-name=\"...\"]` | `[data-blok-item-name=\"...\"]` |\n\n### Playwright Example\n\n```diff\n// EditorJS\n- await page.locator('[data-cy=editorjs] .ce-block').click();\n- await page.locator('.ce-toolbar__settings-btn').click();\n- await page.locator('[data-item-name=\"delete\"]').click();\n\n// Blok\n+ await page.locator('[data-blok-element]').click();\n+ await page.locator('[data-blok-settings-toggler]').click();\n+ await page.locator('[data-blok-item-name=\"delete\"]').click();\n```\n\n### Additional Test Selectors\n\n| Element | Selector |\n|---------|----------|\n| Block tunes popover | `[data-blok-testid=\"block-tunes-popover\"]` |\n| Popover search input | `[data-blok-testid=\"popover-search-input\"]` |\n| Popover item title | `[data-blok-testid=\"popover-item-title\"]` |\n| Popover overlay | `[data-blok-testid=\"popover-overlay\"]` |\n| Inline tool input | `[data-blok-testid=\"inline-tool-input\"]` |\n| Tooltip | `[data-blok-testid=\"tooltip\"]` |\n| Redactor | `[data-blok-testid=\"redactor\"]` |\n| Toolbox | `[data-blok-testid=\"toolbox\"]` |\n| Toolbox popover | `[data-blok-testid=\"toolbox-popover\"]` |\n| Selection overlay | `[data-blok-testid=\"overlay\"]` |\n| Selection rectangle | `[data-blok-testid=\"overlay-rectangle\"]` |\n\n---\n\n### State Selectors\n\n| State | Selector |\n|-------|----------|\n| Toolbar opened | `[data-blok-opened=\"true\"]` |\n| Block selected | `[data-blok-selected=\"true\"]` |\n| Popover opened | `[data-blok-popover-opened=\"true\"]` |\n| Item focused | `[data-blok-focused=\"true\"]` |\n| Item disabled | `[data-blok-disabled=\"true\"]` |\n| Nested popover | `[data-blok-nested=\"true\"]` |\n| Toolbox opened | `[data-blok-toolbox-opened=\"true\"]` |\n| Block dragging | `[data-blok-dragging=\"true\"]` |\n| Element hidden | `[data-blok-hidden=\"true\"]` |\n| RTL mode | `[data-blok-rtl=\"true\"]` |\n\n---\n\n## Quick Checklist\n\n- [ ] Replace `new EditorJS` with `new Blok`\n- [ ] Update imports from `@editorjs/*` to `@bloklabs/core`\n- [ ] Change holder from `editorjs` to `blok` (or specify explicitly)\n- [ ] Use bundled tools: `Blok.Header`, `Blok.Paragraph`\n- [ ] Replace `@editorjs/delimiter` with built-in `Blok.Divider`\n- [ ] Update `EditorConfig` type to `BlokConfig`\n- [ ] Replace `data-id` with `data-blok-id` in queries\n- [ ] Replace `data-item-name` with `data-blok-item-name`\n- [ ] Replace `.ce-*` selectors with `[data-blok-*]` attributes\n- [ ] Replace `.cdx-*` selectors with `[data-blok-*]` attributes\n- [ ] Update E2E test selectors\n", n = (e) => [
|
|
3
|
+
var t = "# Migrating from EditorJS to Blok\n\nThis guide covers the breaking changes when migrating from EditorJS to Blok.\n\n## Table of Contents\n\n- [Supported Editor.js Versions](#supported-editorjs-versions)\n- [Core Changes](#core-changes)\n- [Data Attributes](#data-attributes)\n- [CSS Classes](#css-classes)\n- [Bundled Tools](#bundled-tools)\n - [Tool Configuration](#tool-configuration)\n - [Lifecycle Hooks](#lifecycle-hooks)\n - [Delimiter → Divider](#delimiter--divider)\n- [Migrating Custom Tools](#migrating-custom-tools)\n - [Custom Block Tools (port unchanged)](#custom-block-tools-port-unchanged)\n - [Custom Inline Tools (breaking rewrite)](#custom-inline-tools-breaking-rewrite)\n- [Saved Content / Data Migration](#saved-content--data-migration)\n - [Auto-Migrated on Load](#auto-migrated-on-load)\n - [Dropped Fields](#dropped-fields)\n- [Configuration Defaults](#configuration-defaults)\n- [New API Methods](#new-api-methods)\n- [DOM Selectors](#dom-selectors)\n- [E2E Test Selectors](#e2e-test-selectors)\n\n---\n\n## Supported Editor.js Versions\n\nBlok targets the **Editor.js 2.x** line. The [codemod](./codemod) is built against the package versions a typical Editor.js 2.x project ships — `@editorjs/editorjs@^2.28.x`, `@editorjs/header@^2.8.x`, `@editorjs/paragraph@^2.11.x`, `@editorjs/list@^1.9.x` — and the runtime auto-migration reads the saved-data shapes those versions produce. Older 2.x data formats are still recognized; the matrix below describes coverage per block/tool, not per Editor.js point release.\n\nThere is no pinned single source version: Blok is API-compatible with the Editor.js 2.x `BlockTool` / `InlineTool` contracts, so what matters is **which tools your content uses**, covered below.\n\n### Compatibility Matrix\n\nHow each Editor.js block/tool is handled. \"Auto-migrated at runtime\" means the conversion happens on `render()` with no script needed; \"Codemod\" means the [codemod](./codemod) rewrites saved JSON; \"Drop-in unchanged\" means the data passes through as-is; \"Not bundled\" means Blok ships no equivalent tool, so you must register your own (a ported Editor.js block tool works unchanged — see [Migrating Custom Tools](#migrating-custom-tools)) or the block renders as a preserved stub with a `console.warn` and round-trips untouched on save.\n\n| Editor.js block / tool | Migration status | Notes |\n|------------------------|------------------|-------|\n| `paragraph` | Drop-in unchanged | Bundled as `Blok.Paragraph` (the default block). |\n| `header` | Drop-in unchanged | Bundled as `Blok.Header`. |\n| `list` (nested **and** string-array items) | Auto-migrated at runtime **+** codemod | Each item expands to a flat `list` block. |\n| `checklist` (standalone tool) | Auto-migrated at runtime **+** codemod | Becomes `list` blocks with checklist style. |\n| `toggleList` | Auto-migrated at runtime **+** codemod → `toggle` | Becomes a `toggle` block (or a toggle `header` when `titleVariant` is set); body blocks flatten into parented children. |\n| `callout` (legacy `body`/`variant` shape) | Auto-migrated at runtime **+** codemod | Legacy `{ body, variant, emoji }` → flat `{ emoji, textColor, backgroundColor }`; body blocks flatten into parented children. |\n| `image`, `simple-image` | Auto-migrated at runtime **+** codemod | `file.url` (or `simple-image`'s flat `url`) → `url`; `withBorder` → `frame`, `stretched: true` → `size: full`. `withBackground` is dropped (warns). |\n| `linkTool` | Auto-migrated at runtime **+** codemod → `bookmark` | `meta` flattens onto the bookmark shape; `meta.site_name` is dropped (warns). |\n| `delimiter` | Codemod renames type → `divider` | Data format is identical (`{}`). Also recognized at render, so existing data works without the rename. |\n| `quote` | Auto-migrated at runtime **+** codemod | `caption` becomes a trailing `paragraph` sibling; `alignment` is dropped (warns). The quote text/size render unchanged. |\n| `code` | Drop-in unchanged | Passes through to Blok's bundled `code` tool. |\n| `table` (HTML-string cells) | Auto-migrated at runtime **+** codemod | Each non-empty cell becomes a `{ blocks: [id] }` cell-ref plus a child `paragraph` (`parent` = table id); empty cells become `{ blocks: [] }`. `withHeadings` is preserved. Cells already in block-ref shape pass through. |\n| `embed` | Drop-in unchanged | Bundled as `Blok.Embed`. `{ service, source, embed, width, height, caption }` map 1:1; iframe providers render correctly (Blok-only fields default). |\n| `raw` | Auto-migrated at runtime **+** codemod → `code` | Blok has no raw tool; `{ html }` becomes a `code` block showing the markup as source verbatim. |\n| `warning` | Auto-migrated at runtime **+** codemod → `callout` | Becomes a ⚠️ orange `callout`; `title` and `message` become child `paragraph` blocks. |\n| `attaches` | Auto-migrated at runtime **+** codemod → `bookmark` | `file.url` + `title` map to a `bookmark`; file metadata (`name`/`size`/`extension`) is dropped (warns). |\n| `marker`, `inlineCode` (inline) | Not bundled | Register your own (port unchanged or wrap with `wrapLegacyInlineTool`). See [Custom Inline Tools](#custom-inline-tools). |\n| Other third-party block tools (`personality`, `button`, audio, …) | Not bundled | No built-in equivalent. Register a tool to render them; otherwise the block is preserved as a stub (warns) and survives a save/load round-trip. |\n\n> **Heads-up:** any block type Blok doesn't recognize and has no registered tool for renders as a stub and logs `Tool «<name>» is not found`. Its data is preserved, so registering the tool later restores the block.\n\n---\n\n## Core Changes\n\n### Class Name\n\n```diff\n- import EditorJS from '@editorjs/editorjs';\n+ import Blok from '@bloklabs/core';\n\n- const editor = new EditorJS({ ... });\n+ const editor = new Blok({ ... });\n```\n\n`Blok` is also exported as the **default export** and under an `EditorJS` alias, so you can keep your existing variable name and only change the import path — a minimal-diff, drop-in migration:\n\n```diff\n- import EditorJS from '@editorjs/editorjs';\n+ import { EditorJS } from '@bloklabs/core';\n\n const editor = new EditorJS({ ... }); // unchanged\n```\n\n`import Blok`, `import { Blok }`, and `import { EditorJS }` all resolve to the same class.\n\n### Default Holder\n\nThe default holder ID changed from `editorjs` to `blok`:\n\n```diff\n- <div id=\"editorjs\"></div>\n+ <div id=\"blok\"></div>\n```\n\nOr specify explicitly:\n\n```javascript\nconst editor = new Blok({\n holder: 'my-editor', // works the same as before\n});\n```\n\n### TypeScript Types\n\n```diff\n- import type { EditorConfig, OutputData } from '@editorjs/editorjs';\n+ import type { BlokConfig, OutputData } from '@bloklabs/core';\n```\n\n---\n\n## Data Attributes\n\nBlok uses `data-blok-*` attributes instead of EditorJS's mixed naming conventions.\n\n| EditorJS | Blok |\n|----------|------|\n| `data-id` | `data-blok-id` |\n| `data-item-name` | `data-blok-item-name` |\n| `data-empty` | `data-blok-empty` |\n| `.ce-block--selected` (class) | `data-blok-selected=\"true\"` |\n| — | `data-blok-component` (tool name) |\n| — | `data-blok-interface` (element type) |\n| — | `data-blok-testid` (testing) |\n| — | `data-blok-opened` (toolbar state) |\n| — | `data-blok-placeholder` |\n| — | `data-blok-stretched` |\n| — | `data-blok-focused` |\n| — | `data-blok-popover-opened` |\n| — | `data-blok-tool` (tool name on rendered element) |\n| — | `data-blok-dragging` (block being dragged) |\n| — | `data-blok-hidden` (hidden state) |\n| — | `data-blok-rtl` (RTL mode) |\n| — | `data-blok-drag-handle` (drag handle element) |\n| — | `data-blok-overlay` (selection overlay) |\n| — | `data-blok-overlay-rectangle` (selection rectangle) |\n\n### Querying Blocks\n\n```diff\n- document.querySelector('[data-id=\"abc123\"]');\n+ document.querySelector('[data-blok-id=\"abc123\"]');\n\n- document.querySelector('[data-item-name=\"bold\"]');\n+ document.querySelector('[data-blok-item-name=\"bold\"]');\n```\n\n---\n\n## CSS Classes\n\nBlok replaces BEM class names with data attributes for selection.\n\n### Editor Wrapper\n\n| EditorJS | Blok |\n|----------|------|\n| `.codex-editor` | `[data-blok-editor]` |\n| `.codex-editor__redactor` | `[data-blok-redactor]` |\n| `.codex-editor--rtl` | `[data-blok-rtl=\"true\"]` |\n\n### Block Elements\n\n| EditorJS | Blok |\n|----------|------|\n| `.ce-block` | `[data-blok-element]` |\n| `.ce-block--selected` | `[data-blok-selected=\"true\"]` |\n| `.ce-block--stretched` | `[data-blok-stretched=\"true\"]` |\n| `.ce-block--focused` | `[data-blok-focused=\"true\"]` |\n| `.ce-block__content` | `[data-blok-element-content]` |\n\n### Toolbar\n\n| EditorJS | Blok |\n|----------|------|\n| `.ce-toolbar` | `[data-blok-toolbar]` |\n| `.ce-toolbar__plus` | `[data-blok-testid=\"plus-button\"]` |\n| `.ce-toolbar__settings-btn` | `[data-blok-settings-toggler]` |\n| `.ce-toolbar__actions` | `[data-blok-testid=\"toolbar-actions\"]` |\n| `.ce-toolbox` | `[data-blok-toolbox]` |\n| `.ce-toolbox--opened` | `[data-blok-toolbox][data-blok-opened=\"true\"]` |\n\n### Inline Toolbar\n\n| EditorJS | Blok |\n|----------|------|\n| `.ce-inline-toolbar` | `[data-blok-testid=\"inline-toolbar\"]` |\n| `.ce-inline-tool` | `[data-blok-testid=\"inline-tool\"]` |\n| `.ce-inline-tool--link` | `[data-blok-testid=\"inline-tool-link\"]` |\n| `.ce-inline-tool--bold` | `[data-blok-testid=\"inline-tool-bold\"]` |\n| `.ce-inline-tool--italic` | `[data-blok-testid=\"inline-tool-italic\"]` |\n\n### Popover\n\n| EditorJS | Blok |\n|----------|------|\n| `.ce-popover` | `[data-blok-popover]` |\n| `.ce-popover--opened` | `[data-blok-popover][data-blok-opened=\"true\"]` |\n| `.ce-popover__container` | `[data-blok-popover-container]` |\n| `.ce-popover-item` | `[data-blok-testid=\"popover-item\"]` |\n| `.ce-popover-item--focused` | `[data-blok-focused=\"true\"]` |\n| `.ce-popover-item--confirmation` | `[data-blok-confirmation=\"true\"]` |\n| `.ce-popover-item__icon` | `[data-blok-testid=\"popover-item-icon\"]` |\n| `.ce-popover-item__icon--tool` | `[data-blok-testid=\"popover-item-icon-tool\"]` |\n\n### Tool-Specific Classes\n\n| EditorJS | Blok |\n|----------|------|\n| `.ce-paragraph` | `[data-blok-tool=\"paragraph\"]` |\n| `.ce-header` | `[data-blok-tool=\"header\"]` |\n\n### Conversion Toolbar & Settings\n\n| EditorJS | Blok |\n|----------|------|\n| `.ce-conversion-toolbar` | `[data-blok-testid=\"conversion-toolbar\"]` |\n| `.ce-conversion-tool` | `[data-blok-testid=\"conversion-tool\"]` |\n| `.ce-settings` | `[data-blok-testid=\"block-settings\"]` |\n| `.ce-tune` | `[data-blok-testid=\"block-tune\"]` |\n\n### Other Elements\n\n| EditorJS | Blok |\n|----------|------|\n| `.ce-stub` | `[data-blok-stub]` |\n| `.ce-drag-handle` | `[data-blok-drag-handle]` |\n| `.ce-ragged-right` | `[data-blok-ragged-right=\"true\"]` |\n\n### CDX List Classes\n\n| EditorJS | Blok |\n|----------|------|\n| `.cdx-list` | `[data-blok-list]` |\n| `.cdx-list__item` | `[data-blok-list-item]` |\n| `.cdx-list--ordered` | `[data-blok-list=\"ordered\"]` |\n| `.cdx-list--unordered` | `[data-blok-list=\"unordered\"]` |\n\n### CDX Utility Classes\n\n| EditorJS | Blok |\n|----------|------|\n| `.cdx-button` | `[data-blok-button]` |\n| `.cdx-input` | `[data-blok-input]` |\n| `.cdx-loader` | `[data-blok-loader]` |\n| `.cdx-search-field` | `[data-blok-search-field]` |\n\n---\n\n## Bundled Tools\n\nBlok includes Header and Paragraph tools. No external packages needed:\n\n```diff\n- import Header from '@editorjs/header';\n- import Paragraph from '@editorjs/paragraph';\n\n+ import Blok from '@bloklabs/core';\n\nconst editor = new Blok({\n tools: {\n- header: Header,\n- paragraph: Paragraph,\n+ header: Blok.Header,\n+ paragraph: Blok.Paragraph, // optional, it's the default\n },\n});\n```\n\n### Tool Configuration\n\nBoth bundled tools accept configuration options:\n\n#### HeaderConfig\n\n```typescript\nimport type { HeaderConfig } from '@bloklabs/core';\n\nconst editor = new Blok({\n tools: {\n header: {\n class: Blok.Header,\n config: {\n placeholder: 'Enter a heading',\n levels: [2, 3, 4], // Restrict to H2-H4 only\n defaultLevel: 2, // Default to H2\n } as HeaderConfig,\n },\n },\n});\n```\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `placeholder` | `string` | `''` | Placeholder text for empty header |\n| `levels` | `number[]` | `[1,2,3,4,5,6]` | Available heading levels (1-6) |\n| `defaultLevel` | `number` | `2` | Default heading level |\n\n#### ParagraphConfig\n\n```typescript\nimport type { ParagraphConfig } from '@bloklabs/core';\n\nconst editor = new Blok({\n tools: {\n paragraph: {\n class: Blok.Paragraph,\n config: {\n placeholder: 'Start typing...',\n preserveBlank: true,\n } as ParagraphConfig,\n },\n },\n});\n```\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `placeholder` | `string` | `''` | Placeholder text for empty paragraph |\n| `preserveBlank` | `boolean` | `false` | Keep empty paragraphs when saving |\n\n### Delimiter → Divider\n\nEditor.js's `@editorjs/delimiter` is replaced by Blok's built-in `divider` tool:\n\n```diff\n- import Delimiter from '@editorjs/delimiter';\n\nconst editor = new Blok({\n tools: {\n- delimiter: Delimiter,\n+ divider: Blok.Divider,\n },\n});\n```\n\n**Saved data migration:** The block type name changed from `\"delimiter\"` to `\"divider\"`. The data format is identical (empty `{}`):\n\n```diff\n {\n- \"type\": \"delimiter\",\n+ \"type\": \"divider\",\n \"data\": {}\n }\n```\n\n> **Note:** Blok automatically recognizes `\"delimiter\"` blocks and renders them as dividers, so existing articles work without data migration. However, renaming the type in your database is recommended for consistency.\n\n---\n\n## Migrating Custom Tools\n\nIf your project ships its own tools, here's what changes and what doesn't.\n\n### Custom Block Tools (port unchanged)\n\n**Custom block tools work as-is.** Blok's `BlockTool` interface keeps the same shape as EditorJS, read by the **same keys**:\n\n- Methods: `render()`, `save()`, `validate()`, `renderSettings()`, `merge()`, `onPaste()`, and the lifecycle hooks `rendered()`, `updated()`, `removed()`, `moved()`.\n- Statics: `toolbox`, `conversionConfig`, `pasteConfig`, `isReadOnlySupported`, `enableLineBreaks`.\n\nIn most cases you can drop your existing block tool into Blok without touching its code.\n\n#### New optional `setReadOnly(state)` method\n\nBlok adds an **optional** `setReadOnly(state: boolean)` method. It is not a breaking change:\n\n- **If you omit it**, read-only toggling still works — Blok falls back to a full re-render of the block (graceful degradation).\n- **If you implement it**, Blok performs a cheaper in-place toggle instead of re-rendering.\n\n```typescript\nclass MyTool {\n // ...render / save / validate as before...\n\n // Optional: enables the cheaper in-place read-only toggle\n setReadOnly(state: boolean): void {\n this.input.contentEditable = state ? 'false' : 'true';\n }\n}\n```\n\n#### New optional statics\n\nTwo new **optional** statics exist for advanced cases; existing tools never need them:\n\n- `titleKey` — a translation key for the tool's toolbox title (i18n).\n- `provides` — lets one class register multiple block types.\n\n### Custom Inline Tools\n\n**This is the one API difference for custom tools.** EditorJS inline tools built a DOM button and used imperative `surround` / `checkState` / `renderActions` / `clear` methods. Blok's `InlineTool` (`types/tools/inline-tool.d.ts`) instead expects `render()` to return a declarative `MenuConfig` object with `onActivate` / `isActive` callbacks.\n\nYou have two paths:\n\n#### Fast path — wrap with `wrapLegacyInlineTool`\n\nTo keep an existing EditorJS inline tool working without a rewrite, wrap its class:\n\n```typescript\nimport { wrapLegacyInlineTool } from '@bloklabs/core';\nimport MarkerTool from './marker-tool'; // your existing EditorJS inline tool\n\nconst editor = new Blok({\n tools: {\n marker: wrapLegacyInlineTool(MarkerTool),\n },\n});\n```\n\nThe shim adapts the legacy contract automatically: it calls your tool's `render()` for the icon, routes `onActivate` → `surround(range)` and `isActive` → `checkState(selection)`, and forwards the static `title` / `shortcut` / `sanitize`. Without wrapping, Blok silently skips a legacy `render()` that returns an `HTMLElement`, so the tool would vanish from the inline toolbar. `renderActions()` / `clear()` are not bridged.\n\n#### Native path — rewrite to `MenuConfig` (recommended)\n\nFor full access to Blok features (nested items, confirmation), port to the native shape:\n\n#### Before (EditorJS)\n\n```typescript\nclass MarkerTool {\n static get isInline() { return true; }\n static get title() { return 'Marker'; }\n\n private button: HTMLButtonElement;\n\n render(): HTMLElement {\n this.button = document.createElement('button');\n this.button.innerHTML = '<svg>...</svg>';\n return this.button;\n }\n\n // Apply / remove formatting on the current range\n surround(range: Range): void {\n document.execCommand('backColor', false, '#ffd700');\n }\n\n // Reflect active state by inspecting the selection\n checkState(selection: Selection): boolean {\n const isActive = /* ...inspect selection... */;\n this.button.classList.toggle('active', isActive);\n return isActive;\n }\n\n renderActions(): HTMLElement { /* extra UI */ }\n clear(): void { /* reset extra UI */ }\n}\n```\n\n#### After (Blok)\n\n```typescript\nimport type { InlineTool, MenuConfig } from '@bloklabs/core';\n\nclass MarkerTool implements InlineTool {\n static isInline = true;\n static title = 'Marker';\n static titleKey = 'marker'; // optional i18n key\n\n // render() now returns a MenuConfig object, not an HTMLElement\n render(): MenuConfig {\n return {\n icon: '<svg>...</svg>',\n name: 'marker',\n // surround(range) logic moves here\n onActivate: () => {\n document.execCommand('backColor', false, '#ffd700');\n },\n // checkState(selection) logic moves here\n isActive: () => {\n const selection = window.getSelection();\n return selection ? /* ...inspect selection... */ false : false;\n },\n };\n }\n}\n```\n\n> See `src/components/inline-tools/inline-tool-bold.ts` for Blok's canonical inline tool. Its `render()` returns a `MenuConfig` with `onActivate` (apply/remove bold) and `isActive` (read the selection).\n\n#### Mapping reference\n\n| EditorJS | Blok |\n|----------|------|\n| `render()` returns an `HTMLElement` button | `render()` returns a `MenuConfig` `{ icon, name, onActivate, isActive }` |\n| `surround(range)` | logic moves into `onActivate()` |\n| `checkState(selection)` | logic moves into `isActive()` |\n| `renderActions()` / `clear()` | **removed** — no direct equivalent; use `MenuConfig` `children` for nested items |\n| `static get isInline()` / `static get title()` | still apply (plus optional `titleKey` for i18n) |\n\n#### Mark-shaped tools — skip the DOM plumbing entirely (`api.marks`)\n\nIf your inline tool wraps text in one element (a highlight `<span>`, a colour\n`<mark>`, a comment anchor), you no longer need to write any range logic.\nDescribe the wrapper as a `MarkSpec` and drive it through `api.marks`\n(`has` / `find` / `read` / `apply` / `remove` / `toggle`, all range-aware):\n\n```typescript\nclass HighlightTool {\n static isInline = true;\n static title = 'Highlight';\n static sanitize = markSanitizerConfig(HIGHLIGHT); // derived, stays in sync\n\n constructor({ api }) {\n this.api = api;\n }\n\n render() {\n return {\n icon: '<svg>…</svg>',\n name: 'highlight',\n onActivate: () => this.api.marks.toggle(HIGHLIGHT),\n isActive: () => this.api.marks.has(HIGHLIGHT),\n };\n }\n}\n\nconst HIGHLIGHT = { tag: 'span', className: 'my-highlight' };\n```\n\n(React consumers get the same thing as a single spec field:\n`createReactInlineTool({ …, mark: { tag: 'span', className: 'my-highlight' } })`.)\n\n**Behavioural note when porting hand-rolled range code onto `api.marks`.** The\nengine is *more correct* than typical hand-rolled implementations on\nselections that span a wrapper boundary — expect these deltas:\n\nBefore (typical hand-rolled tool):\n\n- A selection covering half a wrapper plus outside text unwrapped or re-wrapped\n the WHOLE wrapper (formatting outside the selection was lost).\n- Toggling \"off\" on a partially covered wrapper removed the whole run.\n- `Ctrl+A` selections silently dropped trailing spaces from the formatted run\n (Chromium/WebKit place the range end before trailing whitespace).\n\nAfter (`api.marks`):\n\n- Partially covered wrappers are split at the selection boundaries; text\n outside the selection keeps its formatting.\n- `apply` and `remove` extend the range over browser-excluded trailing\n whitespace, so the visible selection and the formatted run agree (and\n toggling off never strands a whitespace-only wrapper).\n- Two specs sharing a wrapper family (same tag/classes, different declared\n style property — e.g. text colour and background) compose on ONE element\n instead of nesting or cancelling each other.\n- Legacy tag variants can be declared as the SAME mark via `aliasTags`\n (e.g. `{ tag: 'strong', aliasTags: ['b'] }`): alias-tagged wrappers are\n recognized, split and stripped like canonical ones, while newly created\n wrappers always use `tag`. Blok's own Bold (`strong`+`b`) and Italic\n (`i`+`em`) run on exactly this.\n\n---\n\n## Saved Content / Data Migration\n\nExisting EditorJS output data keeps working. Blok converts legacy block shapes to its own format **automatically when the block renders** — no migration script needed.\n\n### Auto-Migrated on Load\n\nThese EditorJS block types and data shapes are recognized and converted on `render()`:\n\n| EditorJS block / shape | Becomes in Blok |\n|------------------------|-----------------|\n| `list` (nested items **and** string-array items) | Blok `list` |\n| `checklist` (standalone tool) | Blok `list` (checklist style) |\n| `image` `{ file: { url }, withBorder, stretched }` | Blok `image` |\n| `table` (HTML-string cells) | Blok `table` |\n| `header` | Blok `header` |\n| `code` | Blok `code` |\n| `delimiter` | Blok `divider` |\n| `linkTool` | Blok `bookmark` |\n\nNo action is required — load your existing data and it renders correctly.\n\n### Dropped Fields\n\nA few EditorJS fields have no Blok equivalent and are **dropped** on load. The block still renders; only these specific fields are lost:\n\n| Block | Dropped field(s) |\n|-------|------------------|\n| `quote` | `caption`, `alignment` |\n| `image` | `withBackground` |\n| `linkTool` | `meta.site_name` |\n| `list` item | `meta` |\n\nWhen migration drops one of these, Blok emits a `console.warn` (prefixed `[Blok migration]`) naming the block type and field, so the loss is visible in the console rather than silent. If you depend on any of these, capture them before migrating.\n\n---\n\n## Configuration Defaults\n\n| Option | EditorJS | Blok |\n|--------|----------|------|\n| `holder` | `\"editorjs\"` | `\"blok\"` |\n| `defaultBlock` | `\"paragraph\"` | `\"paragraph\"` |\n\n---\n\n## New API Methods\n\nBlok exposes shorthand methods directly on the instance:\n\n```javascript\nconst editor = new Blok({ ... });\n\n// Shorthand methods\nawait editor.save(); // Same as editor.saver.save()\neditor.clear(); // Same as editor.blocks.clear()\nawait editor.render(data);// Same as editor.blocks.render(data)\neditor.focus(); // Same as editor.caret.focus()\n\n// Event methods\neditor.on('change', handler); // Same as editor.events.on()\neditor.off('change', handler); // Same as editor.events.off()\neditor.emit('custom', data); // Same as editor.events.emit()\n```\n\n---\n\n## DOM Selectors\n\nBlok uses consistent `data-blok-interface` attributes for major UI components:\n\n```javascript\n// Find the editor wrapper\nconst editorWrapper = document.querySelector('[data-blok-interface=\"blok\"]');\n\n// Find inline toolbar\nconst inlineToolbar = document.querySelector('[data-blok-interface=\"inline-toolbar\"]');\n\n// Find tooltip\nconst tooltip = document.querySelector('[data-blok-interface=\"tooltip\"]');\n```\n\nThese selectors are stable and recommended for programmatic access to Blok's UI components.\n\n### Key Changes\n\n1. **Prefix custom classes** with `blok-` instead of `ce-` or `cdx-`\n2. **Use `data-blok-*`** attributes instead of `data-*`\n\n---\n\n## E2E Test Selectors\n\nUpdate your test selectors to use Blok's `data-blok-testid` attributes:\n\n| EditorJS | Blok |\n|----------|------|\n| `[data-cy=editorjs]` | `[data-blok-testid=\"blok-editor\"]` |\n| `.ce-block` | `[data-blok-element]` |\n| `.ce-block__content` | `[data-blok-element-content]` |\n| `.ce-toolbar` | `[data-blok-toolbar]` |\n| `.ce-toolbar__plus` | `[data-blok-testid=\"plus-button\"]` |\n| `.ce-toolbar__settings-btn` | `[data-blok-settings-toggler]` |\n| `.ce-toolbar__actions` | `[data-blok-testid=\"toolbar-actions\"]` |\n| `.ce-toolbox` | `[data-blok-toolbox]` |\n| `.ce-inline-toolbar` | `[data-blok-testid=\"inline-toolbar\"]` |\n| `.ce-popover` | `[data-blok-popover]` |\n| `.ce-popover-item` | `[data-blok-testid=\"popover-item\"]` |\n| `[data-item-name=\"...\"]` | `[data-blok-item-name=\"...\"]` |\n\n### Playwright Example\n\n```diff\n// EditorJS\n- await page.locator('[data-cy=editorjs] .ce-block').click();\n- await page.locator('.ce-toolbar__settings-btn').click();\n- await page.locator('[data-item-name=\"delete\"]').click();\n\n// Blok\n+ await page.locator('[data-blok-element]').click();\n+ await page.locator('[data-blok-settings-toggler]').click();\n+ await page.locator('[data-blok-item-name=\"delete\"]').click();\n```\n\n### Additional Test Selectors\n\n| Element | Selector |\n|---------|----------|\n| Block tunes popover | `[data-blok-testid=\"block-tunes-popover\"]` |\n| Popover search input | `[data-blok-testid=\"popover-search-input\"]` |\n| Popover item title | `[data-blok-testid=\"popover-item-title\"]` |\n| Popover overlay | `[data-blok-testid=\"popover-overlay\"]` |\n| Inline tool input | `[data-blok-testid=\"inline-tool-input\"]` |\n| Tooltip | `[data-blok-testid=\"tooltip\"]` |\n| Redactor | `[data-blok-testid=\"redactor\"]` |\n| Toolbox | `[data-blok-testid=\"toolbox\"]` |\n| Toolbox popover | `[data-blok-testid=\"toolbox-popover\"]` |\n| Selection overlay | `[data-blok-testid=\"overlay\"]` |\n| Selection rectangle | `[data-blok-testid=\"overlay-rectangle\"]` |\n\n---\n\n### State Selectors\n\n| State | Selector |\n|-------|----------|\n| Toolbar opened | `[data-blok-opened=\"true\"]` |\n| Block selected | `[data-blok-selected=\"true\"]` |\n| Popover opened | `[data-blok-popover-opened=\"true\"]` |\n| Item focused | `[data-blok-focused=\"true\"]` |\n| Item disabled | `[data-blok-disabled=\"true\"]` |\n| Nested popover | `[data-blok-nested=\"true\"]` |\n| Toolbox opened | `[data-blok-toolbox-opened=\"true\"]` |\n| Block dragging | `[data-blok-dragging=\"true\"]` |\n| Element hidden | `[data-blok-hidden=\"true\"]` |\n| RTL mode | `[data-blok-rtl=\"true\"]` |\n\n---\n\n## Quick Checklist\n\n- [ ] Replace `new EditorJS` with `new Blok`\n- [ ] Update imports from `@editorjs/*` to `@bloklabs/core`\n- [ ] Change holder from `editorjs` to `blok` (or specify explicitly)\n- [ ] Use bundled tools: `Blok.Header`, `Blok.Paragraph`\n- [ ] Replace `@editorjs/delimiter` with built-in `Blok.Divider`\n- [ ] Update `EditorConfig` type to `BlokConfig`\n- [ ] Replace `data-id` with `data-blok-id` in queries\n- [ ] Replace `data-item-name` with `data-blok-item-name`\n- [ ] Replace `.ce-*` selectors with `[data-blok-*]` attributes\n- [ ] Replace `.cdx-*` selectors with `[data-blok-*]` attributes\n- [ ] Update E2E test selectors\n", n = (e) => [
|
|
4
4
|
"# Blok Migration Guide (for LLM-assisted migration)",
|
|
5
5
|
"",
|
|
6
6
|
"> This document contains everything needed to migrate a project from EditorJS to Blok.",
|
|
@@ -41,14 +41,14 @@ var t = "# Migrating from EditorJS to Blok\n\nThis guide covers the breaking cha
|
|
|
41
41
|
case "convert-html": {
|
|
42
42
|
let e = new (await (import("jsdom"))).JSDOM("");
|
|
43
43
|
globalThis.DOMParser = e.window.DOMParser, globalThis.Node = e.window.Node;
|
|
44
|
-
let { convertHtml: t } = await import("./convert-html-
|
|
44
|
+
let { convertHtml: t } = await import("./convert-html-BxOQfxMe.mjs");
|
|
45
45
|
r(t((await import("node:fs")).readFileSync("/dev/stdin", "utf-8")), s);
|
|
46
46
|
break;
|
|
47
47
|
}
|
|
48
48
|
case "convert-gdocs": {
|
|
49
49
|
let e = new (await (import("jsdom"))).JSDOM("");
|
|
50
50
|
globalThis.DOMParser = e.window.DOMParser, globalThis.Node = e.window.Node, globalThis.document = e.window.document;
|
|
51
|
-
let { convertGdocs: t } = await import("./convert-gdocs-
|
|
51
|
+
let { convertGdocs: t } = await import("./convert-gdocs-DLaumLJ0.mjs");
|
|
52
52
|
r(t((await import("node:fs")).readFileSync("/dev/stdin", "utf-8")), s);
|
|
53
53
|
break;
|
|
54
54
|
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { a as e, i as t, n, o as r, r as i,
|
|
1
|
+
import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./block-builder-Cp5f8JCh.mjs";
|
|
2
2
|
//#region src/components/modules/paste/constants.ts
|
|
3
|
-
var
|
|
3
|
+
var s = "data-blok-columns-candidate";
|
|
4
4
|
//#endregion
|
|
5
5
|
//#region src/components/modules/paste/google-docs-preprocessor.ts
|
|
6
|
-
function
|
|
7
|
-
let
|
|
8
|
-
|
|
9
|
-
let
|
|
10
|
-
return
|
|
6
|
+
function c(e) {
|
|
7
|
+
let n = document.createElement("div");
|
|
8
|
+
n.innerHTML = e;
|
|
9
|
+
let r = h(n);
|
|
10
|
+
return D(n, r), r && (k(n), A(n), m(n)), t(n), n.innerHTML;
|
|
11
11
|
}
|
|
12
|
-
function
|
|
12
|
+
function l(e, t) {
|
|
13
13
|
let n = e.parentElement;
|
|
14
|
-
return n === null ? null : n === t ? e :
|
|
14
|
+
return n === null ? null : n === t ? e : l(n, t);
|
|
15
15
|
}
|
|
16
|
-
function
|
|
16
|
+
function u(e, t, n) {
|
|
17
17
|
let r = e.cloneNode(!1), i = [];
|
|
18
18
|
for (let n of Array.from(e.childNodes)) {
|
|
19
19
|
if (n === t) break;
|
|
@@ -21,11 +21,11 @@ function l(e, t, n) {
|
|
|
21
21
|
}
|
|
22
22
|
return i.forEach((e) => r.appendChild(e)), n !== null && r.appendChild(n), r.childNodes.length > 0 ? r : null;
|
|
23
23
|
}
|
|
24
|
-
function
|
|
24
|
+
function d(e, t, n) {
|
|
25
25
|
let r = e.cloneNode(!1), i = Array.from(e.childNodes), a = i.findIndex((e) => e === t), o = i.slice(a + 1);
|
|
26
26
|
return n !== null && r.appendChild(n), o.forEach((e) => r.appendChild(e)), r.childNodes.length > 0 ? r : null;
|
|
27
27
|
}
|
|
28
|
-
function
|
|
28
|
+
function f(e, t) {
|
|
29
29
|
let n = (e, r, i) => {
|
|
30
30
|
if (e === t) return {
|
|
31
31
|
before: r,
|
|
@@ -36,45 +36,45 @@ function d(e, t) {
|
|
|
36
36
|
before: r,
|
|
37
37
|
after: i
|
|
38
38
|
};
|
|
39
|
-
let o =
|
|
39
|
+
let o = u(a, e, r), s = d(a, e, i);
|
|
40
40
|
return n(a, o, s);
|
|
41
41
|
};
|
|
42
42
|
return n(e, null, null);
|
|
43
43
|
}
|
|
44
|
-
function
|
|
44
|
+
function p(e, t) {
|
|
45
45
|
let n = e.parentElement;
|
|
46
|
-
return n === null || n === t ? !1 : n.tagName === "TABLE" ||
|
|
46
|
+
return n === null || n === t ? !1 : n.tagName === "TABLE" || p(n, t);
|
|
47
47
|
}
|
|
48
|
-
function
|
|
48
|
+
function m(e) {
|
|
49
49
|
let t = Array.from(e.querySelectorAll("img"));
|
|
50
50
|
for (let n of t) {
|
|
51
|
-
if (
|
|
52
|
-
let t =
|
|
51
|
+
if (p(n, e)) continue;
|
|
52
|
+
let t = l(n, e);
|
|
53
53
|
if (!t) continue;
|
|
54
|
-
let { before: r, after: i } =
|
|
54
|
+
let { before: r, after: i } = f(n, t), a = document.createDocumentFragment();
|
|
55
55
|
r && a.appendChild(r), a.appendChild(n), i && a.appendChild(i), t.replaceWith(a);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
function
|
|
58
|
+
function h(e) {
|
|
59
59
|
let t = e.querySelector("b[id^=\"docs-internal-guid-\"]");
|
|
60
60
|
if (!t) return !1;
|
|
61
61
|
let n = document.createDocumentFragment();
|
|
62
62
|
for (; t.firstChild;) n.appendChild(t.firstChild);
|
|
63
63
|
return t.replaceWith(n), !0;
|
|
64
64
|
}
|
|
65
|
-
function
|
|
65
|
+
function g(e, t, n) {
|
|
66
66
|
return e ? `background-color: ${n}` : t ? "background-color: transparent" : "";
|
|
67
67
|
}
|
|
68
|
-
function
|
|
68
|
+
function _(e) {
|
|
69
69
|
let t = e.replace(/\s/g, "");
|
|
70
70
|
return t === "rgb(0,0,0)" || t === "#000000";
|
|
71
71
|
}
|
|
72
|
-
function
|
|
72
|
+
function v(e) {
|
|
73
73
|
if (e.closest("a") !== null) return !0;
|
|
74
74
|
let t = e.querySelector("a");
|
|
75
75
|
return t !== null && t.textContent?.trim() === e.textContent?.trim();
|
|
76
76
|
}
|
|
77
|
-
function
|
|
77
|
+
function y(e) {
|
|
78
78
|
let t = e.replace(/\s/g, "").toLowerCase(), n = /^rgba?\((\d+),(\d+),(\d+)(?:,[\d.]+)?\)$/.exec(t);
|
|
79
79
|
if (n) {
|
|
80
80
|
let e = parseInt(n[1], 10) / 255, t = parseInt(n[2], 10) / 255, r = parseInt(n[3], 10) / 255;
|
|
@@ -105,17 +105,17 @@ function v(e) {
|
|
|
105
105
|
}
|
|
106
106
|
return -1;
|
|
107
107
|
}
|
|
108
|
-
var
|
|
109
|
-
function
|
|
110
|
-
let t =
|
|
108
|
+
var b = a, x = r;
|
|
109
|
+
function S(e) {
|
|
110
|
+
let t = y(e);
|
|
111
111
|
return t >= 0 && t > .75;
|
|
112
112
|
}
|
|
113
|
-
function
|
|
114
|
-
if (!n && !r) return
|
|
115
|
-
let o = n && i !== void 0 ?
|
|
116
|
-
return c ? `<mark style="${c};">${
|
|
113
|
+
function C(t, n, r, i, a) {
|
|
114
|
+
if (!n && !r) return t;
|
|
115
|
+
let o = n && i !== void 0 ? e(i, "text") : "", s = r && a !== void 0 ? e(a, "bg") : "", c = [n ? `color: ${o}` : "", g(r, n, s)].filter(Boolean).join("; ");
|
|
116
|
+
return c ? `<mark style="${c};">${t}</mark>` : t;
|
|
117
117
|
}
|
|
118
|
-
var
|
|
118
|
+
var w = /* @__PURE__ */ new Set([
|
|
119
119
|
"H1",
|
|
120
120
|
"H2",
|
|
121
121
|
"H3",
|
|
@@ -123,39 +123,39 @@ var C = /* @__PURE__ */ new Set([
|
|
|
123
123
|
"H5",
|
|
124
124
|
"H6"
|
|
125
125
|
]);
|
|
126
|
-
function
|
|
126
|
+
function T(e) {
|
|
127
127
|
let t = e.parentElement;
|
|
128
|
-
return t === null ? !1 :
|
|
128
|
+
return t === null ? !1 : w.has(t.tagName) ? !0 : T(t);
|
|
129
129
|
}
|
|
130
|
-
function
|
|
131
|
-
let n = e.getAttribute("style") ?? "", r = /font-weight\s*:\s*(700|bold)/i.test(n) && !
|
|
130
|
+
function E(e, t) {
|
|
131
|
+
let n = e.getAttribute("style") ?? "", r = /font-weight\s*:\s*(700|bold)/i.test(n) && !T(e), i = /font-style\s*:\s*italic/i.test(n), a = /(?<![a-z-])color\s*:\s*([^;]+)/i.exec(n), o = /background-color\s*:\s*([^;]+)/i.exec(n), s = a?.[1]?.trim(), c = o?.[1]?.trim(), l = !(s !== void 0 && v(e)) && (t ? s !== void 0 && !_(s) : s !== void 0 && !_(s) && !S(s)), u = c !== void 0 && c !== "transparent" && !b(c) && !x(c);
|
|
132
132
|
if (!r && !i && !l && !u) return null;
|
|
133
|
-
let d =
|
|
133
|
+
let d = C(e.innerHTML, l, u, s, c), f = i ? `<i>${d}</i>` : d;
|
|
134
134
|
return r ? `<b>${f}</b>` : f;
|
|
135
135
|
}
|
|
136
|
-
function
|
|
136
|
+
function D(e, t) {
|
|
137
137
|
for (let n of Array.from(e.querySelectorAll("span[style]"))) {
|
|
138
|
-
let e =
|
|
138
|
+
let e = E(n, t);
|
|
139
139
|
e !== null && n.replaceWith(document.createRange().createContextualFragment(e));
|
|
140
140
|
}
|
|
141
|
-
t &&
|
|
141
|
+
t && O(e);
|
|
142
142
|
}
|
|
143
|
-
function
|
|
144
|
-
for (let n of Array.from(
|
|
145
|
-
let
|
|
146
|
-
a.style.removeProperty("color"), i && (a.innerHTML = `<mark style="background-color: ${
|
|
143
|
+
function O(t) {
|
|
144
|
+
for (let n of Array.from(t.querySelectorAll("a[style]"))) {
|
|
145
|
+
let t = n.getAttribute("style") ?? "", r = /background-color\s*:\s*([^;]+)/i.exec(t)?.[1]?.trim(), i = r !== void 0 && r !== "transparent" && r !== "inherit", a = n;
|
|
146
|
+
a.style.removeProperty("color"), i && (a.innerHTML = `<mark style="background-color: ${e(r, "bg")};">${a.innerHTML}</mark>`, a.style.removeProperty("background-color"));
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
-
function
|
|
149
|
+
function k(e) {
|
|
150
150
|
if (e.querySelector("google-sheets-html-origin") === null) for (let t of Array.from(e.querySelectorAll("table"))) {
|
|
151
151
|
if (t.parentElement?.closest("table") !== null) continue;
|
|
152
152
|
let e = Array.from(t.querySelectorAll("tr")).filter((e) => e.closest("table") === t);
|
|
153
153
|
if (e.length === 0) continue;
|
|
154
154
|
let n = e.map((e) => Array.from(e.children).filter((e) => e.tagName === "TD" || e.tagName === "TH").length), r = n[0];
|
|
155
|
-
n.every((e) => e === r) && (r === 2 || r === 3) && t.setAttribute(
|
|
155
|
+
n.every((e) => e === r) && (r === 2 || r === 3) && t.setAttribute(s, "");
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
-
function
|
|
158
|
+
function A(e) {
|
|
159
159
|
for (let t of Array.from(e.querySelectorAll("td, th"))) {
|
|
160
160
|
let e = t.querySelectorAll("p");
|
|
161
161
|
if (e.length !== 0) {
|
|
@@ -169,14 +169,14 @@ function k(e) {
|
|
|
169
169
|
}
|
|
170
170
|
//#endregion
|
|
171
171
|
//#region src/cli/commands/convert-gdocs/index.ts
|
|
172
|
-
function
|
|
173
|
-
let t =
|
|
172
|
+
function j(e) {
|
|
173
|
+
let t = c(e), r = new DOMParser().parseFromString(t, "text/html").body;
|
|
174
174
|
i(r), n(r);
|
|
175
|
-
let
|
|
176
|
-
version: "1.
|
|
177
|
-
blocks:
|
|
175
|
+
let a = {
|
|
176
|
+
version: "1.4.0",
|
|
177
|
+
blocks: o(r)
|
|
178
178
|
};
|
|
179
|
-
return JSON.stringify(
|
|
179
|
+
return JSON.stringify(a);
|
|
180
180
|
}
|
|
181
181
|
//#endregion
|
|
182
|
-
export {
|
|
182
|
+
export { j as convertGdocs };
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { a as e, i as t, n, o as r, r as i,
|
|
1
|
+
import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./block-builder-Cp5f8JCh.mjs";
|
|
2
2
|
//#region src/components/modules/paste/constants.ts
|
|
3
|
-
var
|
|
3
|
+
var s = "data-blok-columns-candidate";
|
|
4
4
|
//#endregion
|
|
5
5
|
//#region src/components/modules/paste/google-docs-preprocessor.ts
|
|
6
|
-
function
|
|
7
|
-
let
|
|
8
|
-
|
|
9
|
-
let
|
|
10
|
-
return
|
|
6
|
+
function c(e) {
|
|
7
|
+
let n = document.createElement("div");
|
|
8
|
+
n.innerHTML = e;
|
|
9
|
+
let r = h(n);
|
|
10
|
+
return D(n, r), r && (k(n), A(n), m(n)), t(n), n.innerHTML;
|
|
11
11
|
}
|
|
12
|
-
function
|
|
12
|
+
function l(e, t) {
|
|
13
13
|
let n = e.parentElement;
|
|
14
|
-
return n === null ? null : n === t ? e :
|
|
14
|
+
return n === null ? null : n === t ? e : l(n, t);
|
|
15
15
|
}
|
|
16
|
-
function
|
|
16
|
+
function u(e, t, n) {
|
|
17
17
|
let r = e.cloneNode(!1), i = [];
|
|
18
18
|
for (let n of Array.from(e.childNodes)) {
|
|
19
19
|
if (n === t) break;
|
|
@@ -21,11 +21,11 @@ function l(e, t, n) {
|
|
|
21
21
|
}
|
|
22
22
|
return i.forEach((e) => r.appendChild(e)), n !== null && r.appendChild(n), r.childNodes.length > 0 ? r : null;
|
|
23
23
|
}
|
|
24
|
-
function
|
|
24
|
+
function d(e, t, n) {
|
|
25
25
|
let r = e.cloneNode(!1), i = Array.from(e.childNodes), a = i.findIndex((e) => e === t), o = i.slice(a + 1);
|
|
26
26
|
return n !== null && r.appendChild(n), o.forEach((e) => r.appendChild(e)), r.childNodes.length > 0 ? r : null;
|
|
27
27
|
}
|
|
28
|
-
function
|
|
28
|
+
function f(e, t) {
|
|
29
29
|
let n = (e, r, i) => {
|
|
30
30
|
if (e === t) return {
|
|
31
31
|
before: r,
|
|
@@ -36,45 +36,45 @@ function d(e, t) {
|
|
|
36
36
|
before: r,
|
|
37
37
|
after: i
|
|
38
38
|
};
|
|
39
|
-
let o =
|
|
39
|
+
let o = u(a, e, r), s = d(a, e, i);
|
|
40
40
|
return n(a, o, s);
|
|
41
41
|
};
|
|
42
42
|
return n(e, null, null);
|
|
43
43
|
}
|
|
44
|
-
function
|
|
44
|
+
function p(e, t) {
|
|
45
45
|
let n = e.parentElement;
|
|
46
|
-
return n === null || n === t ? !1 : n.tagName === "TABLE" ||
|
|
46
|
+
return n === null || n === t ? !1 : n.tagName === "TABLE" || p(n, t);
|
|
47
47
|
}
|
|
48
|
-
function
|
|
48
|
+
function m(e) {
|
|
49
49
|
let t = Array.from(e.querySelectorAll("img"));
|
|
50
50
|
for (let n of t) {
|
|
51
|
-
if (
|
|
52
|
-
let t =
|
|
51
|
+
if (p(n, e)) continue;
|
|
52
|
+
let t = l(n, e);
|
|
53
53
|
if (!t) continue;
|
|
54
|
-
let { before: r, after: i } =
|
|
54
|
+
let { before: r, after: i } = f(n, t), a = document.createDocumentFragment();
|
|
55
55
|
r && a.appendChild(r), a.appendChild(n), i && a.appendChild(i), t.replaceWith(a);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
function
|
|
58
|
+
function h(e) {
|
|
59
59
|
let t = e.querySelector("b[id^=\"docs-internal-guid-\"]");
|
|
60
60
|
if (!t) return !1;
|
|
61
61
|
let n = document.createDocumentFragment();
|
|
62
62
|
for (; t.firstChild;) n.appendChild(t.firstChild);
|
|
63
63
|
return t.replaceWith(n), !0;
|
|
64
64
|
}
|
|
65
|
-
function
|
|
65
|
+
function g(e, t, n) {
|
|
66
66
|
return e ? `background-color: ${n}` : t ? "background-color: transparent" : "";
|
|
67
67
|
}
|
|
68
|
-
function
|
|
68
|
+
function _(e) {
|
|
69
69
|
let t = e.replace(/\s/g, "");
|
|
70
70
|
return t === "rgb(0,0,0)" || t === "#000000";
|
|
71
71
|
}
|
|
72
|
-
function
|
|
72
|
+
function v(e) {
|
|
73
73
|
if (e.closest("a") !== null) return !0;
|
|
74
74
|
let t = e.querySelector("a");
|
|
75
75
|
return t !== null && t.textContent?.trim() === e.textContent?.trim();
|
|
76
76
|
}
|
|
77
|
-
function
|
|
77
|
+
function y(e) {
|
|
78
78
|
let t = e.replace(/\s/g, "").toLowerCase(), n = /^rgba?\((\d+),(\d+),(\d+)(?:,[\d.]+)?\)$/.exec(t);
|
|
79
79
|
if (n) {
|
|
80
80
|
let e = parseInt(n[1], 10) / 255, t = parseInt(n[2], 10) / 255, r = parseInt(n[3], 10) / 255;
|
|
@@ -105,17 +105,17 @@ function v(e) {
|
|
|
105
105
|
}
|
|
106
106
|
return -1;
|
|
107
107
|
}
|
|
108
|
-
var
|
|
109
|
-
function
|
|
110
|
-
let t =
|
|
108
|
+
var b = a, x = r;
|
|
109
|
+
function S(e) {
|
|
110
|
+
let t = y(e);
|
|
111
111
|
return t >= 0 && t > .75;
|
|
112
112
|
}
|
|
113
|
-
function
|
|
114
|
-
if (!n && !r) return
|
|
115
|
-
let o = n && i !== void 0 ?
|
|
116
|
-
return c ? `<mark style="${c};">${
|
|
113
|
+
function C(t, n, r, i, a) {
|
|
114
|
+
if (!n && !r) return t;
|
|
115
|
+
let o = n && i !== void 0 ? e(i, "text") : "", s = r && a !== void 0 ? e(a, "bg") : "", c = [n ? `color: ${o}` : "", g(r, n, s)].filter(Boolean).join("; ");
|
|
116
|
+
return c ? `<mark style="${c};">${t}</mark>` : t;
|
|
117
117
|
}
|
|
118
|
-
var
|
|
118
|
+
var w = /* @__PURE__ */ new Set([
|
|
119
119
|
"H1",
|
|
120
120
|
"H2",
|
|
121
121
|
"H3",
|
|
@@ -123,39 +123,39 @@ var C = /* @__PURE__ */ new Set([
|
|
|
123
123
|
"H5",
|
|
124
124
|
"H6"
|
|
125
125
|
]);
|
|
126
|
-
function
|
|
126
|
+
function T(e) {
|
|
127
127
|
let t = e.parentElement;
|
|
128
|
-
return t === null ? !1 :
|
|
128
|
+
return t === null ? !1 : w.has(t.tagName) ? !0 : T(t);
|
|
129
129
|
}
|
|
130
|
-
function
|
|
131
|
-
let n = e.getAttribute("style") ?? "", r = /font-weight\s*:\s*(700|bold)/i.test(n) && !
|
|
130
|
+
function E(e, t) {
|
|
131
|
+
let n = e.getAttribute("style") ?? "", r = /font-weight\s*:\s*(700|bold)/i.test(n) && !T(e), i = /font-style\s*:\s*italic/i.test(n), a = /(?<![a-z-])color\s*:\s*([^;]+)/i.exec(n), o = /background-color\s*:\s*([^;]+)/i.exec(n), s = a?.[1]?.trim(), c = o?.[1]?.trim(), l = !(s !== void 0 && v(e)) && (t ? s !== void 0 && !_(s) : s !== void 0 && !_(s) && !S(s)), u = c !== void 0 && c !== "transparent" && !b(c) && !x(c);
|
|
132
132
|
if (!r && !i && !l && !u) return null;
|
|
133
|
-
let d =
|
|
133
|
+
let d = C(e.innerHTML, l, u, s, c), f = i ? `<i>${d}</i>` : d;
|
|
134
134
|
return r ? `<b>${f}</b>` : f;
|
|
135
135
|
}
|
|
136
|
-
function
|
|
136
|
+
function D(e, t) {
|
|
137
137
|
for (let n of Array.from(e.querySelectorAll("span[style]"))) {
|
|
138
|
-
let e =
|
|
138
|
+
let e = E(n, t);
|
|
139
139
|
e !== null && n.replaceWith(document.createRange().createContextualFragment(e));
|
|
140
140
|
}
|
|
141
|
-
t &&
|
|
141
|
+
t && O(e);
|
|
142
142
|
}
|
|
143
|
-
function
|
|
144
|
-
for (let n of Array.from(
|
|
145
|
-
let
|
|
146
|
-
a.style.removeProperty("color"), i && (a.innerHTML = `<mark style="background-color: ${
|
|
143
|
+
function O(t) {
|
|
144
|
+
for (let n of Array.from(t.querySelectorAll("a[style]"))) {
|
|
145
|
+
let t = n.getAttribute("style") ?? "", r = /background-color\s*:\s*([^;]+)/i.exec(t)?.[1]?.trim(), i = r !== void 0 && r !== "transparent" && r !== "inherit", a = n;
|
|
146
|
+
a.style.removeProperty("color"), i && (a.innerHTML = `<mark style="background-color: ${e(r, "bg")};">${a.innerHTML}</mark>`, a.style.removeProperty("background-color"));
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
-
function
|
|
149
|
+
function k(e) {
|
|
150
150
|
if (e.querySelector("google-sheets-html-origin") === null) for (let t of Array.from(e.querySelectorAll("table"))) {
|
|
151
151
|
if (t.parentElement?.closest("table") !== null) continue;
|
|
152
152
|
let e = Array.from(t.querySelectorAll("tr")).filter((e) => e.closest("table") === t);
|
|
153
153
|
if (e.length === 0) continue;
|
|
154
154
|
let n = e.map((e) => Array.from(e.children).filter((e) => e.tagName === "TD" || e.tagName === "TH").length), r = n[0];
|
|
155
|
-
n.every((e) => e === r) && (r === 2 || r === 3) && t.setAttribute(
|
|
155
|
+
n.every((e) => e === r) && (r === 2 || r === 3) && t.setAttribute(s, "");
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
-
function
|
|
158
|
+
function A(e) {
|
|
159
159
|
for (let t of Array.from(e.querySelectorAll("td, th"))) {
|
|
160
160
|
let e = t.querySelectorAll("p");
|
|
161
161
|
if (e.length !== 0) {
|
|
@@ -169,14 +169,14 @@ function k(e) {
|
|
|
169
169
|
}
|
|
170
170
|
//#endregion
|
|
171
171
|
//#region src/cli/commands/convert-gdocs/index.ts
|
|
172
|
-
function
|
|
173
|
-
let t =
|
|
172
|
+
function j(e) {
|
|
173
|
+
let t = c(e), r = new DOMParser().parseFromString(t, "text/html").body;
|
|
174
174
|
i(r), n(r);
|
|
175
|
-
let
|
|
176
|
-
version: "1.
|
|
177
|
-
blocks:
|
|
175
|
+
let a = {
|
|
176
|
+
version: "1.4.1",
|
|
177
|
+
blocks: o(r)
|
|
178
178
|
};
|
|
179
|
-
return JSON.stringify(
|
|
179
|
+
return JSON.stringify(a);
|
|
180
180
|
}
|
|
181
181
|
//#endregion
|
|
182
|
-
export {
|
|
182
|
+
export { j as convertGdocs };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { i as e, n as t, r as n, t as r } from "./block-builder-Cp5f8JCh.mjs";
|
|
2
|
+
//#region src/cli/commands/convert-html/index.ts
|
|
3
|
+
function i(i) {
|
|
4
|
+
let a = new DOMParser().parseFromString(i, "text/html").body;
|
|
5
|
+
n(a), t(a), e(a);
|
|
6
|
+
let o = {
|
|
7
|
+
version: "1.4.1",
|
|
8
|
+
blocks: r(a)
|
|
9
|
+
};
|
|
10
|
+
return JSON.stringify(o);
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { i as convertHtml };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { i as e, n as t, r as n, t as r } from "./block-builder-Cp5f8JCh.mjs";
|
|
2
|
+
//#region src/cli/commands/convert-html/index.ts
|
|
3
|
+
function i(i) {
|
|
4
|
+
let a = new DOMParser().parseFromString(i, "text/html").body;
|
|
5
|
+
n(a), t(a), e(a);
|
|
6
|
+
let o = {
|
|
7
|
+
version: "1.4.0",
|
|
8
|
+
blocks: r(a)
|
|
9
|
+
};
|
|
10
|
+
return JSON.stringify(o);
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { i as convertHtml };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bloklabs/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "CLI tools for Blok — convert HTML to Blok JSON, generate migration guides",
|
|
5
5
|
"bin": {
|
|
6
6
|
"blok-cli": "bin/blok-cli.mjs"
|
|
@@ -19,17 +19,25 @@
|
|
|
19
19
|
"blok",
|
|
20
20
|
"cli",
|
|
21
21
|
"html-to-json",
|
|
22
|
+
"block-editor",
|
|
23
|
+
"rich-text-editor",
|
|
22
24
|
"editorjs",
|
|
25
|
+
"editorjs-migration",
|
|
23
26
|
"migration",
|
|
27
|
+
"json",
|
|
24
28
|
"rich-text"
|
|
25
29
|
],
|
|
26
30
|
"author": "JackUait",
|
|
27
31
|
"license": "Apache-2.0",
|
|
32
|
+
"homepage": "https://blokeditor.com",
|
|
28
33
|
"repository": {
|
|
29
34
|
"type": "git",
|
|
30
35
|
"url": "git+https://github.com/JackUait/blok.git",
|
|
31
36
|
"directory": "packages/cli"
|
|
32
37
|
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/JackUait/blok/issues"
|
|
40
|
+
},
|
|
33
41
|
"dependencies": {
|
|
34
42
|
"jsdom": "29.1.1"
|
|
35
43
|
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { n as e, r as t, t as n } from "./block-builder-BhbHyKmd.mjs";
|
|
2
|
-
//#region src/cli/commands/convert-html/index.ts
|
|
3
|
-
function r(r) {
|
|
4
|
-
let i = new DOMParser().parseFromString(r, "text/html").body;
|
|
5
|
-
t(i), e(i);
|
|
6
|
-
let a = {
|
|
7
|
-
version: "1.3.0",
|
|
8
|
-
blocks: n(i)
|
|
9
|
-
};
|
|
10
|
-
return JSON.stringify(a);
|
|
11
|
-
}
|
|
12
|
-
//#endregion
|
|
13
|
-
export { r as convertHtml };
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { n as e, r as t, t as n } from "./block-builder-BhbHyKmd.mjs";
|
|
2
|
-
//#region src/cli/commands/convert-html/index.ts
|
|
3
|
-
function r(r) {
|
|
4
|
-
let i = new DOMParser().parseFromString(r, "text/html").body;
|
|
5
|
-
t(i), e(i);
|
|
6
|
-
let a = {
|
|
7
|
-
version: "1.2.6",
|
|
8
|
-
blocks: n(i)
|
|
9
|
-
};
|
|
10
|
-
return JSON.stringify(a);
|
|
11
|
-
}
|
|
12
|
-
//#endregion
|
|
13
|
-
export { r as convertHtml };
|