@jackuait/blok 0.10.0-beta.9 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/blok.mjs +2 -2
  2. package/dist/chunks/{blok-DDu252IK.mjs → blok-BfcBwAfE.mjs} +1211 -1159
  3. package/dist/chunks/{constants-DMW9a31I.mjs → constants-QNVyXALL.mjs} +49 -48
  4. package/dist/chunks/{tools-XmzH2rgQ.mjs → tools-DHtzbrxy.mjs} +1403 -1220
  5. package/dist/full.mjs +3 -3
  6. package/dist/react.mjs +2 -2
  7. package/dist/tools.mjs +2 -2
  8. package/package.json +3 -5
  9. package/src/cli/commands/convert-gdocs/index.ts +26 -0
  10. package/src/cli/commands/convert-html/block-builder.ts +392 -0
  11. package/src/cli/commands/convert-html/id-generator.ts +11 -0
  12. package/src/cli/commands/convert-html/index.ts +23 -0
  13. package/src/cli/commands/convert-html/preprocessor.ts +422 -0
  14. package/src/cli/commands/convert-html/sanitizer.ts +93 -0
  15. package/src/cli/commands/convert-html/types.ts +15 -0
  16. package/src/cli/index.ts +56 -5
  17. package/src/components/block/index.ts +44 -10
  18. package/src/components/constants/data-attributes.ts +10 -0
  19. package/src/components/icons/index.ts +16 -0
  20. package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +18 -0
  21. package/src/components/modules/blockManager/hierarchy.ts +4 -1
  22. package/src/components/modules/readonly.ts +46 -0
  23. package/src/components/modules/rectangleSelection.ts +25 -5
  24. package/src/components/modules/toolbar/index.ts +96 -19
  25. package/src/components/modules/toolbar/styles.ts +0 -2
  26. package/src/components/modules/uiControllers/controllers/blockHover.ts +44 -1
  27. package/src/components/tools/block.ts +10 -0
  28. package/src/components/utils/placeholder.ts +9 -2
  29. package/src/styles/main.css +16 -0
  30. package/src/tools/callout/constants.ts +2 -1
  31. package/src/tools/callout/dom-builder.ts +13 -1
  32. package/src/tools/callout/index.ts +21 -7
  33. package/src/tools/code/constants.ts +9 -1
  34. package/src/tools/code/dom-builder.ts +90 -54
  35. package/src/tools/code/index.ts +73 -31
  36. package/src/tools/divider/index.ts +5 -0
  37. package/src/tools/header/index.ts +47 -1
  38. package/src/tools/list/dom-builder.ts +3 -1
  39. package/src/tools/list/index.ts +55 -3
  40. package/src/tools/list/list-helpers.ts +2 -2
  41. package/src/tools/nested-blocks.ts +25 -0
  42. package/src/tools/paragraph/index.ts +47 -6
  43. package/src/tools/quote/index.ts +43 -8
  44. package/src/tools/stub/index.ts +10 -0
  45. package/src/tools/table/index.ts +238 -6
  46. package/src/tools/table/table-add-controls.ts +37 -5
  47. package/src/tools/table/table-cell-blocks.ts +57 -18
  48. package/src/tools/table/table-core.ts +2 -0
  49. package/src/tools/table/table-corner-drag.ts +247 -0
  50. package/src/tools/table/table-operations.ts +41 -14
  51. package/src/tools/toggle/dom-builder.ts +1 -0
  52. package/src/tools/toggle/index.ts +25 -0
  53. package/src/tools/toggle/toggle-lifecycle.ts +5 -4
  54. package/src/types-internal/jsdom.d.ts +9 -0
  55. package/types/tools/adapters/block-tool-adapter.d.ts +6 -0
  56. package/types/tools/block-tool.d.ts +10 -0
  57. package/bin/blok.mjs +0 -10
  58. package/dist/cli.mjs +0 -37
  59. package/src/tools/code/language-picker.ts +0 -241
package/dist/full.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { n as e, t } from "./chunks/blok-DDu252IK.mjs";
2
- import { $n as n } from "./chunks/constants-DMW9a31I.mjs";
1
+ import { n as e, t } from "./chunks/blok-BfcBwAfE.mjs";
2
+ import { tr as n } from "./chunks/constants-QNVyXALL.mjs";
3
3
  import { t as r } from "./chunks/objectSpread2-CWwMYL_U.mjs";
4
- import { _ as i, a, c as o, g as s, i as c, l, m as u, n as d, o as f, s as p, t as m, v as h } from "./chunks/tools-XmzH2rgQ.mjs";
4
+ import { _ as i, a, c as o, g as s, i as c, l, m as u, n as d, o as f, s as p, t as m, v as h } from "./chunks/tools-DHtzbrxy.mjs";
5
5
  //#region src/full.ts
6
6
  var g = {
7
7
  paragraph: {
package/dist/react.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { t as e } from "./chunks/blok-DDu252IK.mjs";
2
- import "./chunks/constants-DMW9a31I.mjs";
1
+ import { t as e } from "./chunks/blok-BfcBwAfE.mjs";
2
+ import "./chunks/constants-QNVyXALL.mjs";
3
3
  import { t } from "./chunks/objectSpread2-CWwMYL_U.mjs";
4
4
  import { t as n } from "./chunks/objectWithoutProperties-D0XxKB4n.mjs";
5
5
  import { forwardRef as r, useEffect as i, useMemo as a, useRef as o, useState as s } from "react";
package/dist/tools.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { m as e } from "./chunks/constants-DMW9a31I.mjs";
2
- import { _ as t, a as n, c as r, d as i, f as a, g as o, h as s, i as c, l, m as u, n as d, o as f, p, r as m, s as h, t as g, u as _, v } from "./chunks/tools-XmzH2rgQ.mjs";
1
+ import { m as e } from "./chunks/constants-QNVyXALL.mjs";
2
+ import { _ as t, a as n, c as r, d as i, f as a, g as o, h as s, i as c, l, m as u, n as d, o as f, p, r as m, s as h, t as g, u as _, v } from "./chunks/tools-DHtzbrxy.mjs";
3
3
  export { l as Bold, p as Callout, _ as Code, e as Convert, a as Divider, t as Header, m as InlineCode, r as Italic, h as Link, o as List, f as Marker, v as Paragraph, i as Quote, c as Strikethrough, s as Table, u as Toggle, n as Underline, g as defaultBlockTools, d as defaultInlineTools };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackuait/blok",
3
- "version": "0.10.0-beta.9",
3
+ "version": "0.10.0",
4
4
  "description": "Blok — headless, highly extensible rich text editor built for developers who need to implement a block-based editing experience (similar to Notion) without building it from scratch",
5
5
  "module": "dist/blok.mjs",
6
6
  "types": "./types/index.d.ts",
@@ -50,15 +50,13 @@
50
50
  }
51
51
  },
52
52
  "bin": {
53
- "blok": "./bin/blok.mjs",
54
53
  "migrate-from-editorjs": "./codemod/migrate-editorjs-to-blok.js"
55
54
  },
56
55
  "files": [
57
56
  "dist",
58
57
  "types",
59
58
  "src",
60
- "codemod",
61
- "bin"
59
+ "codemod"
62
60
  ],
63
61
  "publishConfig": {
64
62
  "access": "public"
@@ -83,7 +81,7 @@
83
81
  "scripts": {
84
82
  "serve": "vite --no-open",
85
83
  "serve:docs": "cd docs && vite --port 8080",
86
- "build": "vite build --mode production && node scripts/build-locales.mjs && node scripts/build-cli.mjs",
84
+ "build": "vite build --mode production && node scripts/build-locales.mjs",
87
85
  "build:cli": "node scripts/build-cli.mjs",
88
86
  "build:test": "vite build --mode test && node scripts/build-locales.mjs test && node scripts/build-react-vendor.mjs",
89
87
  "lint": "sh -c 'eslint .; ESLINT_EXIT=$?; tsc --noEmit; TSC_EXIT=$?; if [ $ESLINT_EXIT -ne 0 ] || [ $TSC_EXIT -ne 0 ]; then exit 1; fi'",
@@ -0,0 +1,26 @@
1
+ import { preprocessGoogleDocsHtml } from '../../../components/modules/paste/google-docs-preprocessor';
2
+ import { preprocess } from '../convert-html/preprocessor';
3
+ import { sanitize } from '../convert-html/sanitizer';
4
+ import { buildBlocks } from '../convert-html/block-builder';
5
+ import type { OutputData } from '../convert-html/types';
6
+
7
+ declare const __CLI_VERSION__: string;
8
+
9
+ /**
10
+ * Convert Google Docs HTML to Blok JSON.
11
+ * Runs: Google Docs preprocess -> general preprocess -> sanitize -> build blocks -> serialize.
12
+ */
13
+ export function convertGdocs(html: string): string {
14
+ const preprocessed = preprocessGoogleDocsHtml(html);
15
+
16
+ const dom = new DOMParser().parseFromString(preprocessed, 'text/html');
17
+ const wrapper = dom.body;
18
+
19
+ preprocess(wrapper);
20
+ sanitize(wrapper);
21
+
22
+ const blocks = buildBlocks(wrapper);
23
+ const output: OutputData = { version: typeof __CLI_VERSION__ !== 'undefined' ? __CLI_VERSION__ : 'dev', blocks };
24
+
25
+ return JSON.stringify(output);
26
+ }
@@ -0,0 +1,392 @@
1
+ import { createIdGenerator } from './id-generator';
2
+ import { mapToNearestPresetName } from '../../../components/utils/color-mapping';
3
+ import type { OutputBlockData } from './types';
4
+
5
+ /**
6
+ * Walk the wrapper's top-level children and convert each block-level HTML
7
+ * element into one or more Blok JSON blocks.
8
+ */
9
+ export function buildBlocks(wrapper: HTMLElement): OutputBlockData[] {
10
+ const nextId = createIdGenerator();
11
+ const blocks: OutputBlockData[] = [];
12
+
13
+ for (const node of Array.from(wrapper.childNodes)) {
14
+ convertNode(node, blocks, nextId);
15
+ }
16
+
17
+ return blocks;
18
+ }
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Converters
22
+ // ---------------------------------------------------------------------------
23
+
24
+ function convertNode(
25
+ node: Node,
26
+ blocks: OutputBlockData[],
27
+ nextId: (prefix: string) => string
28
+ ): void {
29
+ if (node.nodeType === Node.TEXT_NODE) {
30
+ const text = node.textContent?.trim() ?? '';
31
+
32
+ if (text) {
33
+ blocks.push({ id: nextId('paragraph'), type: 'paragraph', data: { text } });
34
+ }
35
+
36
+ return;
37
+ }
38
+
39
+ if (node.nodeType !== Node.ELEMENT_NODE) {
40
+ return;
41
+ }
42
+
43
+ const el = node as HTMLElement;
44
+ const tag = el.tagName;
45
+
46
+ if (tag === 'P') {
47
+ blocks.push({ id: nextId('paragraph'), type: 'paragraph', data: { text: el.innerHTML } });
48
+
49
+ return;
50
+ }
51
+
52
+ if (/^H[1-6]$/.test(tag)) {
53
+ const level = Number(tag[1]);
54
+
55
+ blocks.push({ id: nextId('header'), type: 'header', data: { text: el.innerHTML, level } });
56
+
57
+ return;
58
+ }
59
+
60
+ if (tag === 'BLOCKQUOTE') {
61
+ blocks.push({
62
+ id: nextId('quote'),
63
+ type: 'quote',
64
+ data: { text: el.innerHTML, size: 'default' },
65
+ });
66
+
67
+ return;
68
+ }
69
+
70
+ if (tag === 'PRE') {
71
+ blocks.push({
72
+ id: nextId('code'),
73
+ type: 'code',
74
+ data: { code: el.textContent ?? '', language: 'plain-text' },
75
+ });
76
+
77
+ return;
78
+ }
79
+
80
+ if (tag === 'HR') {
81
+ blocks.push({ id: nextId('divider'), type: 'divider', data: {} });
82
+
83
+ return;
84
+ }
85
+
86
+ if (tag === 'IMG') {
87
+ const src = el.getAttribute('src') ?? '';
88
+ const width = parseIntFromStyle(el, 'width');
89
+
90
+ blocks.push({
91
+ id: nextId('image'),
92
+ type: 'image',
93
+ data: { url: src },
94
+ stretched: null,
95
+ key: null,
96
+ width,
97
+ });
98
+
99
+ return;
100
+ }
101
+
102
+ if (tag === 'DETAILS') {
103
+ const summary = el.querySelector('summary');
104
+ const text = summary ? summary.innerHTML : el.innerHTML;
105
+
106
+ blocks.push({ id: nextId('toggle'), type: 'toggle', data: { text } });
107
+
108
+ return;
109
+ }
110
+
111
+ if (tag === 'UL' || tag === 'OL') {
112
+ flattenList(el, tag === 'OL' ? 'ordered' : 'unordered', 0, blocks, nextId);
113
+
114
+ return;
115
+ }
116
+
117
+ if (tag === 'TABLE') {
118
+ convertTable(el, blocks, nextId);
119
+
120
+ return;
121
+ }
122
+
123
+ if (tag === 'ASIDE') {
124
+ convertCallout(el, blocks, nextId);
125
+
126
+ return;
127
+ }
128
+
129
+ // Unknown block element: extract innerHTML as paragraph
130
+ blocks.push({
131
+ id: nextId('paragraph'),
132
+ type: 'paragraph',
133
+ data: { text: el.innerHTML },
134
+ });
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // List flattening
139
+ // ---------------------------------------------------------------------------
140
+
141
+ function flattenList(
142
+ listEl: HTMLElement,
143
+ style: 'ordered' | 'unordered',
144
+ depth: number,
145
+ blocks: OutputBlockData[],
146
+ nextId: (prefix: string) => string
147
+ ): void {
148
+ const startAttr = listEl.getAttribute('start');
149
+ const startValue = startAttr ? Number(startAttr) : null;
150
+ const listItems = Array.from(listEl.children).filter((child) => child.tagName === 'LI');
151
+
152
+ for (const [index, child] of listItems.entries()) {
153
+ // Clone the li so we can remove nested lists without mutating DOM
154
+ const clone = child.cloneNode(true) as HTMLElement;
155
+ const nestedLists: HTMLElement[] = [];
156
+
157
+ for (const nested of Array.from(clone.querySelectorAll('ul, ol'))) {
158
+ nestedLists.push(nested.cloneNode(true) as HTMLElement);
159
+ nested.remove();
160
+ }
161
+
162
+ const text = clone.innerHTML.trim();
163
+
164
+ // Use aria-level if present (1-based → 0-based), otherwise use nesting depth
165
+ const ariaLevel = (child as HTMLElement).getAttribute('aria-level');
166
+ const itemDepth = ariaLevel
167
+ ? Math.max(0, parseInt(ariaLevel, 10) - 1)
168
+ : depth;
169
+
170
+ blocks.push({
171
+ id: nextId('list'),
172
+ type: 'list',
173
+ data: {
174
+ text,
175
+ style,
176
+ depth: itemDepth === 0 ? null : itemDepth,
177
+ checked: null,
178
+ start: index === 0 && startValue !== null ? startValue : null,
179
+ },
180
+ });
181
+
182
+ // Recursively process nested lists
183
+ for (const nested of nestedLists) {
184
+ const nestedStyle = nested.tagName === 'OL' ? 'ordered' : 'unordered';
185
+
186
+ flattenList(nested, nestedStyle, depth + 1, blocks, nextId);
187
+ }
188
+ }
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Table conversion
193
+ // ---------------------------------------------------------------------------
194
+
195
+ function convertTable(
196
+ tableEl: HTMLElement,
197
+ blocks: OutputBlockData[],
198
+ nextId: (prefix: string) => string
199
+ ): void {
200
+ const tableId = nextId('table');
201
+ const rows = Array.from(tableEl.querySelectorAll('tr'));
202
+ const content: Record<string, unknown>[][] = [];
203
+
204
+ for (const row of rows) {
205
+ const cells = Array.from(row.querySelectorAll('td, th'));
206
+ const rowData = cells.map((cell) => convertTableCell(cell as HTMLElement, tableId, blocks, nextId));
207
+
208
+ content.push(rowData);
209
+ }
210
+
211
+ // Parse column widths and headings from first row cells
212
+ const firstRowCells = rows[0] ? Array.from(rows[0].querySelectorAll('td, th')) : [];
213
+ const withHeadings = firstRowCells.some((c) => c.tagName === 'TH');
214
+ const colWidths = firstRowCells.map((cell) => {
215
+ const width = parseCssProperty(cell as HTMLElement, 'width');
216
+
217
+ if (width) {
218
+ const px = parseInt(width, 10);
219
+
220
+ return isNaN(px) ? null : px;
221
+ }
222
+
223
+ return null;
224
+ });
225
+ const hasWidths = colWidths.some((w) => w !== null);
226
+
227
+ // Insert table block before its child paragraph blocks
228
+ const tableBlock: OutputBlockData = {
229
+ id: tableId,
230
+ type: 'table',
231
+ data: {
232
+ withHeadings,
233
+ withHeadingColumn: false,
234
+ content,
235
+ ...(hasWidths ? { colWidths } : {}),
236
+ },
237
+ };
238
+
239
+ // Find first child block index to insert table before its children
240
+ const firstChildIdx = blocks.findIndex((b) => b.parent === tableId);
241
+
242
+ if (firstChildIdx >= 0) {
243
+ blocks.splice(firstChildIdx, 0, tableBlock);
244
+ } else {
245
+ blocks.push(tableBlock);
246
+ }
247
+ }
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Callout conversion
251
+ // ---------------------------------------------------------------------------
252
+
253
+ function convertCallout(
254
+ asideEl: HTMLElement,
255
+ blocks: OutputBlockData[],
256
+ nextId: (prefix: string) => string
257
+ ): void {
258
+ const calloutId = nextId('callout');
259
+ const bgColor = parseCssProperty(asideEl, 'background-color');
260
+ const backgroundColor = bgColor ? mapToNearestPresetName(bgColor, 'bg') : null;
261
+
262
+ const childIds: string[] = [];
263
+
264
+ for (const child of Array.from(asideEl.childNodes)) {
265
+ const childId = convertCalloutChild(child, calloutId, blocks, nextId);
266
+
267
+ if (childId) {
268
+ childIds.push(childId);
269
+ }
270
+ }
271
+
272
+ // Insert callout block before its children
273
+ const firstChildIdx = blocks.findIndex((b) => b.parent === calloutId);
274
+
275
+ const calloutBlock: OutputBlockData = {
276
+ id: calloutId,
277
+ type: 'callout',
278
+ data: {
279
+ emoji: '\u{1F4A1}',
280
+ backgroundColor: backgroundColor ?? 'gray',
281
+ },
282
+ content: childIds,
283
+ };
284
+
285
+ if (firstChildIdx >= 0) {
286
+ blocks.splice(firstChildIdx, 0, calloutBlock);
287
+ } else {
288
+ blocks.push(calloutBlock);
289
+ }
290
+ }
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // Helpers
294
+ // ---------------------------------------------------------------------------
295
+
296
+ function convertTableCell(
297
+ cellEl: HTMLElement,
298
+ tableId: string,
299
+ blocks: OutputBlockData[],
300
+ nextId: (prefix: string) => string
301
+ ): Record<string, unknown> {
302
+ const cellText = cellEl.innerHTML.trim();
303
+
304
+ if (!cellText) {
305
+ return { blocks: [], color: null, textColor: null };
306
+ }
307
+
308
+ const childId = nextId('paragraph');
309
+
310
+ blocks.push({
311
+ id: childId,
312
+ type: 'paragraph',
313
+ parent: tableId,
314
+ data: { text: cellText },
315
+ });
316
+
317
+ const bgColor = parseCssProperty(cellEl, 'background-color');
318
+ const textColor = parseCssProperty(cellEl, 'color');
319
+
320
+ return {
321
+ blocks: [childId],
322
+ color: bgColor ? mapToNearestPresetName(bgColor, 'bg') : null,
323
+ textColor: textColor ? mapToNearestPresetName(textColor, 'text') : null,
324
+ };
325
+ }
326
+
327
+ function convertCalloutChild(
328
+ child: ChildNode,
329
+ calloutId: string,
330
+ blocks: OutputBlockData[],
331
+ nextId: (prefix: string) => string
332
+ ): string | null {
333
+ if (child.nodeType === Node.ELEMENT_NODE) {
334
+ const childEl = child as HTMLElement;
335
+ const childId = nextId('paragraph');
336
+
337
+ blocks.push({
338
+ id: childId,
339
+ type: 'paragraph',
340
+ parent: calloutId,
341
+ data: { text: childEl.innerHTML },
342
+ });
343
+
344
+ return childId;
345
+ }
346
+
347
+ if (child.nodeType === Node.TEXT_NODE) {
348
+ const text = child.textContent?.trim() ?? '';
349
+
350
+ if (!text) {
351
+ return null;
352
+ }
353
+
354
+ const childId = nextId('paragraph');
355
+
356
+ blocks.push({
357
+ id: childId,
358
+ type: 'paragraph',
359
+ parent: calloutId,
360
+ data: { text },
361
+ });
362
+
363
+ return childId;
364
+ }
365
+
366
+ return null;
367
+ }
368
+
369
+ function parseIntFromStyle(el: HTMLElement, property: string): number | null {
370
+ const value = parseCssProperty(el, property);
371
+
372
+ if (!value) {
373
+ return null;
374
+ }
375
+
376
+ const parsed = parseInt(value, 10);
377
+
378
+ return isNaN(parsed) ? null : parsed;
379
+ }
380
+
381
+ function parseCssProperty(el: HTMLElement, property: string): string | null {
382
+ const style = el.getAttribute('style');
383
+
384
+ if (!style) {
385
+ return null;
386
+ }
387
+
388
+ const regex = new RegExp(`(?<![\\-a-z])${property}\\s*:\\s*([^;]+)`);
389
+ const match = regex.exec(style);
390
+
391
+ return match ? match[1].trim() : null;
392
+ }
@@ -0,0 +1,11 @@
1
+ export function createIdGenerator(): (prefix: string) => string {
2
+ const counters = new Map<string, number>();
3
+
4
+ return (prefix: string): string => {
5
+ const count = (counters.get(prefix) ?? 0) + 1;
6
+
7
+ counters.set(prefix, count);
8
+
9
+ return `${prefix}-${count}`;
10
+ };
11
+ }
@@ -0,0 +1,23 @@
1
+ import { preprocess } from './preprocessor';
2
+ import { sanitize } from './sanitizer';
3
+ import { buildBlocks } from './block-builder';
4
+ import type { OutputData } from './types';
5
+
6
+ declare const __CLI_VERSION__: string;
7
+
8
+ /**
9
+ * Convert HTML to Blok JSON.
10
+ * Runs: preprocess → sanitize → build blocks → serialize.
11
+ */
12
+ export function convertHtml(html: string): string {
13
+ const dom = new DOMParser().parseFromString(html, 'text/html');
14
+ const wrapper = dom.body;
15
+
16
+ preprocess(wrapper);
17
+ sanitize(wrapper);
18
+
19
+ const blocks = buildBlocks(wrapper);
20
+ const output: OutputData = { version: typeof __CLI_VERSION__ !== 'undefined' ? __CLI_VERSION__ : 'dev', blocks };
21
+
22
+ return JSON.stringify(output);
23
+ }