@jackuait/blok 0.10.0-beta.8 → 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.
- package/dist/blok.mjs +2 -2
- package/dist/chunks/{blok-CRvF-xVm.mjs → blok-BfcBwAfE.mjs} +1211 -1159
- package/dist/chunks/{constants-BOZ5plBi.mjs → constants-QNVyXALL.mjs} +49 -48
- package/dist/chunks/{tools-CnqCfv2L.mjs → tools-DHtzbrxy.mjs} +1411 -1220
- package/dist/full.mjs +3 -3
- package/dist/react.mjs +2 -2
- package/dist/tools.mjs +2 -2
- package/package.json +3 -5
- package/src/cli/commands/convert-gdocs/index.ts +26 -0
- package/src/cli/commands/convert-html/block-builder.ts +392 -0
- package/src/cli/commands/convert-html/id-generator.ts +11 -0
- package/src/cli/commands/convert-html/index.ts +23 -0
- package/src/cli/commands/convert-html/preprocessor.ts +422 -0
- package/src/cli/commands/convert-html/sanitizer.ts +93 -0
- package/src/cli/commands/convert-html/types.ts +15 -0
- package/src/cli/index.ts +56 -5
- package/src/components/block/index.ts +44 -10
- package/src/components/constants/data-attributes.ts +10 -0
- package/src/components/icons/index.ts +16 -0
- package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +18 -0
- package/src/components/modules/blockManager/hierarchy.ts +4 -1
- package/src/components/modules/readonly.ts +46 -0
- package/src/components/modules/rectangleSelection.ts +25 -5
- package/src/components/modules/toolbar/index.ts +96 -19
- package/src/components/modules/toolbar/styles.ts +0 -2
- package/src/components/modules/uiControllers/controllers/blockHover.ts +44 -1
- package/src/components/tools/block.ts +10 -0
- package/src/components/utils/placeholder.ts +9 -2
- package/src/styles/main.css +16 -0
- package/src/tools/callout/constants.ts +2 -1
- package/src/tools/callout/dom-builder.ts +13 -1
- package/src/tools/callout/index.ts +21 -7
- package/src/tools/code/constants.ts +9 -1
- package/src/tools/code/dom-builder.ts +90 -54
- package/src/tools/code/index.ts +73 -31
- package/src/tools/divider/index.ts +5 -0
- package/src/tools/header/index.ts +47 -1
- package/src/tools/list/dom-builder.ts +3 -1
- package/src/tools/list/index.ts +55 -3
- package/src/tools/list/list-helpers.ts +2 -2
- package/src/tools/nested-blocks.ts +25 -0
- package/src/tools/paragraph/index.ts +47 -6
- package/src/tools/quote/index.ts +43 -8
- package/src/tools/stub/index.ts +10 -0
- package/src/tools/table/index.ts +238 -6
- package/src/tools/table/table-add-controls.ts +37 -5
- package/src/tools/table/table-cell-blocks.ts +87 -18
- package/src/tools/table/table-core.ts +2 -0
- package/src/tools/table/table-corner-drag.ts +247 -0
- package/src/tools/table/table-operations.ts +45 -9
- package/src/tools/toggle/dom-builder.ts +1 -0
- package/src/tools/toggle/index.ts +25 -0
- package/src/tools/toggle/toggle-lifecycle.ts +5 -4
- package/src/types-internal/jsdom.d.ts +9 -0
- package/types/tools/adapters/block-tool-adapter.d.ts +6 -0
- package/types/tools/block-tool.d.ts +10 -0
- package/bin/blok.mjs +0 -10
- package/dist/cli.mjs +0 -37
- 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-
|
|
2
|
-
import {
|
|
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-
|
|
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-
|
|
2
|
-
import "./chunks/constants-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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
|
+
}
|