@inshapardaz/likhari-converters 0.1.9 → 0.1.11-dev.102
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/html/index.d.ts +3 -3
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +7 -9
- package/dist/html/parse.d.ts +4 -0
- package/dist/html/parse.d.ts.map +1 -0
- package/dist/html/parse.js +324 -0
- package/dist/html/serialize.d.ts +8 -0
- package/dist/html/serialize.d.ts.map +1 -0
- package/dist/html/serialize.js +143 -0
- package/dist/shared/serialized.d.ts +31 -0
- package/dist/shared/serialized.d.ts.map +1 -0
- package/dist/shared/serialized.js +32 -0
- package/package.json +1 -1
package/dist/html/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { FormatConverter } from '../types.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* HTML export/import (lexical-editor-spec.md §2.2). Export is pure string
|
|
4
|
+
* generation over the serialized tree; import needs a DOM parser, which the
|
|
5
|
+
* browser (and jsdom in tests) provides.
|
|
6
6
|
*/
|
|
7
7
|
export declare const htmlConverter: FormatConverter;
|
|
8
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/html/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/html/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/html/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAIhD;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,eAI3B,CAAC"}
|
package/dist/html/index.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
+
import { serializeHtml } from './serialize.js';
|
|
2
|
+
import { parseHtml } from './parse.js';
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
4
|
+
* HTML export/import (lexical-editor-spec.md §2.2). Export is pure string
|
|
5
|
+
* generation over the serialized tree; import needs a DOM parser, which the
|
|
6
|
+
* browser (and jsdom in tests) provides.
|
|
5
7
|
*/
|
|
6
8
|
export const htmlConverter = {
|
|
7
9
|
id: 'html',
|
|
8
|
-
serialize()
|
|
9
|
-
|
|
10
|
-
},
|
|
11
|
-
parse() {
|
|
12
|
-
throw new Error('@inshapardaz/likhari-converters: HTML import is not implemented yet (Phase 2, see docs/lexical-editor-spec.md §13).');
|
|
13
|
-
},
|
|
10
|
+
serialize: (state, ctx) => serializeHtml(state, ctx),
|
|
11
|
+
parse: (input, ctx) => parseHtml(input, ctx),
|
|
14
12
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/html/parse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AA+TjD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,qBAAqB,CAMvF"}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { FORMAT_BOLD, FORMAT_CODE, FORMAT_HIGHLIGHT, FORMAT_ITALIC, FORMAT_STRIKETHROUGH, FORMAT_SUBSCRIPT, FORMAT_SUPERSCRIPT, FORMAT_UNDERLINE, INDENT_PX, elementBase, makeState, paragraph, textNode, } from '../shared/serialized.js';
|
|
2
|
+
const ALIGNMENTS = new Set(['left', 'right', 'center', 'justify', 'start', 'end']);
|
|
3
|
+
const TAG_FORMATS = {
|
|
4
|
+
b: FORMAT_BOLD, strong: FORMAT_BOLD,
|
|
5
|
+
i: FORMAT_ITALIC, em: FORMAT_ITALIC,
|
|
6
|
+
u: FORMAT_UNDERLINE, ins: FORMAT_UNDERLINE,
|
|
7
|
+
s: FORMAT_STRIKETHROUGH, strike: FORMAT_STRIKETHROUGH, del: FORMAT_STRIKETHROUGH,
|
|
8
|
+
code: FORMAT_CODE, sub: FORMAT_SUBSCRIPT, sup: FORMAT_SUPERSCRIPT, mark: FORMAT_HIGHLIGHT,
|
|
9
|
+
};
|
|
10
|
+
/** Dropped with their content: never useful in a document. */
|
|
11
|
+
const SKIP_TAGS = new Set(['script', 'style', 'template', 'head', 'title', 'meta', 'link', 'noscript', 'iframe', 'object', 'embed']);
|
|
12
|
+
/** Transparent containers whose children are processed as if in place. */
|
|
13
|
+
const BLOCK_CONTAINERS = new Set(['div', 'section', 'article', 'main', 'header', 'footer', 'aside', 'nav', 'body', 'html', 'figure', 'tbody', 'thead', 'tfoot', 'details', 'summary', 'span-block']);
|
|
14
|
+
const BLOCK_TAGS = new Set([
|
|
15
|
+
'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'ul', 'ol', 'li', 'table', 'tr', 'td', 'th',
|
|
16
|
+
'hr', 'pre', 'img', 'figcaption', 'dl', 'dt', 'dd', ...BLOCK_CONTAINERS,
|
|
17
|
+
]);
|
|
18
|
+
/** Link hrefs with an executable scheme are dropped on import. */
|
|
19
|
+
function safeUrl(url) {
|
|
20
|
+
const trimmed = url.trim();
|
|
21
|
+
if (/^(javascript|vbscript|data:text\/html)/i.test(trimmed.replace(/[\u0000- ]/g, '')))
|
|
22
|
+
return null;
|
|
23
|
+
return trimmed;
|
|
24
|
+
}
|
|
25
|
+
/** Image sources: only http(s), relative and base64 image data URIs. */
|
|
26
|
+
function safeImageSrc(src) {
|
|
27
|
+
const trimmed = src.trim();
|
|
28
|
+
if (!trimmed)
|
|
29
|
+
return null;
|
|
30
|
+
if (/^data:/i.test(trimmed))
|
|
31
|
+
return /^data:image\/(png|jpe?g|gif|webp|svg\+xml|avif|bmp);/i.test(trimmed) ? trimmed : null;
|
|
32
|
+
return safeUrl(trimmed);
|
|
33
|
+
}
|
|
34
|
+
function blockExtras(el) {
|
|
35
|
+
const extra = {};
|
|
36
|
+
const dir = el.getAttribute('dir');
|
|
37
|
+
if (dir === 'rtl' || dir === 'ltr')
|
|
38
|
+
extra.direction = dir;
|
|
39
|
+
const align = el.style?.textAlign;
|
|
40
|
+
if (align && ALIGNMENTS.has(align))
|
|
41
|
+
extra.format = align;
|
|
42
|
+
const padding = el.style?.paddingInlineStart || el.style?.paddingLeft || el.style?.paddingRight;
|
|
43
|
+
const px = padding && /^([\d.]+)px$/.exec(padding);
|
|
44
|
+
if (px)
|
|
45
|
+
extra.indent = Math.min(Math.round(Number(px[1]) / INDENT_PX), 10);
|
|
46
|
+
return extra;
|
|
47
|
+
}
|
|
48
|
+
function inlineStyleOf(el) {
|
|
49
|
+
// Only carry presentation the editor itself produces (font, size, color).
|
|
50
|
+
const keep = [];
|
|
51
|
+
for (const prop of ['font-family', 'font-size', 'color', 'background-color']) {
|
|
52
|
+
const value = el.style?.getPropertyValue(prop);
|
|
53
|
+
if (value)
|
|
54
|
+
keep.push(`${prop}: ${value}`);
|
|
55
|
+
}
|
|
56
|
+
return keep.join('; ');
|
|
57
|
+
}
|
|
58
|
+
function formatsFromElement(el, base) {
|
|
59
|
+
let format = base.format | (TAG_FORMATS[el.tagName.toLowerCase()] ?? 0);
|
|
60
|
+
const weight = el.style?.fontWeight;
|
|
61
|
+
if (weight === 'bold' || Number(weight) >= 600)
|
|
62
|
+
format |= FORMAT_BOLD;
|
|
63
|
+
if (el.style?.fontStyle === 'italic')
|
|
64
|
+
format |= FORMAT_ITALIC;
|
|
65
|
+
const deco = el.style?.textDecoration || el.style?.getPropertyValue('text-decoration-line') || '';
|
|
66
|
+
if (deco.includes('underline'))
|
|
67
|
+
format |= FORMAT_UNDERLINE;
|
|
68
|
+
if (deco.includes('line-through'))
|
|
69
|
+
format |= FORMAT_STRIKETHROUGH;
|
|
70
|
+
const own = inlineStyleOf(el);
|
|
71
|
+
return { format, style: [base.style, own].filter(Boolean).join('; ') };
|
|
72
|
+
}
|
|
73
|
+
/** Collapse HTML whitespace like a browser does inside flowing text. */
|
|
74
|
+
function collapse(text) {
|
|
75
|
+
return text.replace(/[ \t\r\n\f]+/g, ' ');
|
|
76
|
+
}
|
|
77
|
+
function collectInline(node, state, out, preserveWs) {
|
|
78
|
+
if (node.nodeType === 3) {
|
|
79
|
+
const raw = node.nodeValue ?? '';
|
|
80
|
+
const text = preserveWs ? raw : collapse(raw);
|
|
81
|
+
if (text)
|
|
82
|
+
out.push(textNode(text, state.format, state.style));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (node.nodeType !== 1)
|
|
86
|
+
return;
|
|
87
|
+
const el = node;
|
|
88
|
+
const tag = el.tagName.toLowerCase();
|
|
89
|
+
if (SKIP_TAGS.has(tag))
|
|
90
|
+
return;
|
|
91
|
+
if (tag === 'br') {
|
|
92
|
+
out.push({ type: 'linebreak', version: 1 });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (tag === 'a') {
|
|
96
|
+
const url = safeUrl(el.getAttribute('href') ?? '');
|
|
97
|
+
const children = [];
|
|
98
|
+
el.childNodes.forEach((child) => collectInline(child, formatsFromElement(el, state), children, preserveWs));
|
|
99
|
+
if (url === null || url === '') {
|
|
100
|
+
out.push(...children);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
out.push(elementBase('link', children, {
|
|
104
|
+
url,
|
|
105
|
+
rel: el.getAttribute('rel'),
|
|
106
|
+
target: el.getAttribute('target'),
|
|
107
|
+
title: el.getAttribute('title'),
|
|
108
|
+
}));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const next = formatsFromElement(el, state);
|
|
112
|
+
el.childNodes.forEach((child) => collectInline(child, next, out, preserveWs));
|
|
113
|
+
}
|
|
114
|
+
/** Trim the leading/trailing space a block's source whitespace left behind. */
|
|
115
|
+
function trimInline(nodes) {
|
|
116
|
+
const first = nodes[0];
|
|
117
|
+
if (first && first.type === 'text')
|
|
118
|
+
first.text = String(first.text).replace(/^ +/, '');
|
|
119
|
+
const last = nodes[nodes.length - 1];
|
|
120
|
+
if (last && last.type === 'text')
|
|
121
|
+
last.text = String(last.text).replace(/ +$/, '');
|
|
122
|
+
return nodes.filter((n) => n.type !== 'text' || n.text !== '');
|
|
123
|
+
}
|
|
124
|
+
function inlineChildrenOf(el, preserveWs = false) {
|
|
125
|
+
const out = [];
|
|
126
|
+
el.childNodes.forEach((child) => collectInline(child, { format: 0, style: '' }, out, preserveWs));
|
|
127
|
+
return trimInline(out);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Children of a "flow" container (body, div, td, li...): runs of inline
|
|
131
|
+
* content become paragraphs, block children become blocks.
|
|
132
|
+
*/
|
|
133
|
+
function flowBlocks(el) {
|
|
134
|
+
const blocks = [];
|
|
135
|
+
let run = [];
|
|
136
|
+
const flush = () => {
|
|
137
|
+
if (run.length === 0)
|
|
138
|
+
return;
|
|
139
|
+
const out = [];
|
|
140
|
+
for (const n of run)
|
|
141
|
+
collectInline(n, { format: 0, style: '' }, out, false);
|
|
142
|
+
const children = trimInline(out);
|
|
143
|
+
if (children.length > 0)
|
|
144
|
+
blocks.push(paragraph(children));
|
|
145
|
+
run = [];
|
|
146
|
+
};
|
|
147
|
+
el.childNodes.forEach((child) => {
|
|
148
|
+
if (child.nodeType === 1) {
|
|
149
|
+
const tag = child.tagName.toLowerCase();
|
|
150
|
+
if (SKIP_TAGS.has(tag))
|
|
151
|
+
return;
|
|
152
|
+
if (BLOCK_TAGS.has(tag)) {
|
|
153
|
+
flush();
|
|
154
|
+
blocks.push(...convertBlock(child));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
run.push(child);
|
|
159
|
+
});
|
|
160
|
+
flush();
|
|
161
|
+
return blocks;
|
|
162
|
+
}
|
|
163
|
+
function convertList(el) {
|
|
164
|
+
const tag = el.tagName.toLowerCase();
|
|
165
|
+
const isCheck = el.getAttribute('data-list-type') === 'check';
|
|
166
|
+
const listType = isCheck ? 'check' : tag === 'ol' ? 'number' : 'bullet';
|
|
167
|
+
const start = tag === 'ol' ? Number(el.getAttribute('start')) || 1 : 1;
|
|
168
|
+
const items = [];
|
|
169
|
+
el.childNodes.forEach((child) => {
|
|
170
|
+
if (child.nodeType !== 1)
|
|
171
|
+
return;
|
|
172
|
+
const li = child;
|
|
173
|
+
if (li.tagName.toLowerCase() !== 'li')
|
|
174
|
+
return;
|
|
175
|
+
items.push(...convertListItem(li, isCheck));
|
|
176
|
+
});
|
|
177
|
+
return elementBase('list', items, { listType, start, tag: tag === 'ol' ? 'ol' : 'ul', ...blockExtras(el) });
|
|
178
|
+
}
|
|
179
|
+
/** One <li> may yield several listitems (inline content, then nested lists). */
|
|
180
|
+
function convertListItem(li, isCheck) {
|
|
181
|
+
const items = [];
|
|
182
|
+
let run = [];
|
|
183
|
+
const checked = isCheck ? li.getAttribute('data-checked') === 'true' : undefined;
|
|
184
|
+
const makeItem = (children) => elementBase('listitem', children, { value: 1, ...(isCheck ? { checked: !!checked } : {}), ...blockExtras(li) });
|
|
185
|
+
const flush = () => {
|
|
186
|
+
const out = [];
|
|
187
|
+
for (const n of run)
|
|
188
|
+
collectInline(n, { format: 0, style: '' }, out, false);
|
|
189
|
+
const children = trimInline(out);
|
|
190
|
+
if (children.length > 0)
|
|
191
|
+
items.push(makeItem(children));
|
|
192
|
+
run = [];
|
|
193
|
+
};
|
|
194
|
+
li.childNodes.forEach((child) => {
|
|
195
|
+
if (child.nodeType === 1) {
|
|
196
|
+
const tag = child.tagName.toLowerCase();
|
|
197
|
+
if (tag === 'ul' || tag === 'ol') {
|
|
198
|
+
flush();
|
|
199
|
+
items.push(makeItem([convertList(child)]));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (tag === 'p') {
|
|
203
|
+
// <li><p>text</p></li>: treat the paragraph as the item's own text.
|
|
204
|
+
run.push(...Array.from(child.childNodes));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
run.push(child);
|
|
209
|
+
});
|
|
210
|
+
flush();
|
|
211
|
+
return items;
|
|
212
|
+
}
|
|
213
|
+
function convertTable(el) {
|
|
214
|
+
const rows = [];
|
|
215
|
+
el.querySelectorAll('tr').forEach((tr) => {
|
|
216
|
+
// Skip rows of tables nested inside a cell.
|
|
217
|
+
if (tr.closest('table') !== el)
|
|
218
|
+
return;
|
|
219
|
+
const cells = [];
|
|
220
|
+
Array.from(tr.children).forEach((cell) => {
|
|
221
|
+
const tag = cell.tagName.toLowerCase();
|
|
222
|
+
if (tag !== 'td' && tag !== 'th')
|
|
223
|
+
return;
|
|
224
|
+
let children = flowBlocks(cell);
|
|
225
|
+
if (children.length === 0)
|
|
226
|
+
children = [paragraph()];
|
|
227
|
+
cells.push(elementBase('tablecell', children, {
|
|
228
|
+
headerState: tag === 'th' ? 1 : 0,
|
|
229
|
+
colSpan: Number(cell.getAttribute('colspan')) || 1,
|
|
230
|
+
rowSpan: Number(cell.getAttribute('rowspan')) || 1,
|
|
231
|
+
backgroundColor: null,
|
|
232
|
+
width: null,
|
|
233
|
+
}));
|
|
234
|
+
});
|
|
235
|
+
if (cells.length > 0)
|
|
236
|
+
rows.push(elementBase('tablerow', cells, { height: null }));
|
|
237
|
+
});
|
|
238
|
+
return elementBase('table', rows);
|
|
239
|
+
}
|
|
240
|
+
function convertImage(img, caption) {
|
|
241
|
+
const src = safeImageSrc(img.getAttribute('src') ?? '');
|
|
242
|
+
if (!src)
|
|
243
|
+
return null;
|
|
244
|
+
const dim = (name) => {
|
|
245
|
+
const n = Number(img.getAttribute(name));
|
|
246
|
+
return Number.isFinite(n) && n > 0 ? Math.round(n) : null;
|
|
247
|
+
};
|
|
248
|
+
const declared = img.getAttribute('data-link-type');
|
|
249
|
+
return {
|
|
250
|
+
type: 'image',
|
|
251
|
+
version: 1,
|
|
252
|
+
src,
|
|
253
|
+
altText: img.getAttribute('alt') ?? '',
|
|
254
|
+
caption,
|
|
255
|
+
linkType: declared === 'embedded' || /^data:/i.test(src) ? 'embedded' : 'linked',
|
|
256
|
+
width: dim('width'),
|
|
257
|
+
height: dim('height'),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function convertBlock(el) {
|
|
261
|
+
const tag = el.tagName.toLowerCase();
|
|
262
|
+
switch (tag) {
|
|
263
|
+
case 'p':
|
|
264
|
+
return [paragraph(inlineChildrenOf(el), blockExtras(el))];
|
|
265
|
+
case 'h1':
|
|
266
|
+
case 'h2':
|
|
267
|
+
case 'h3':
|
|
268
|
+
case 'h4':
|
|
269
|
+
case 'h5':
|
|
270
|
+
case 'h6':
|
|
271
|
+
return [elementBase('heading', inlineChildrenOf(el), { tag, ...blockExtras(el) })];
|
|
272
|
+
case 'blockquote': {
|
|
273
|
+
const hasBlocks = Array.from(el.children).some((c) => BLOCK_TAGS.has(c.tagName.toLowerCase()));
|
|
274
|
+
if (!hasBlocks)
|
|
275
|
+
return [elementBase('quote', inlineChildrenOf(el), blockExtras(el))];
|
|
276
|
+
// Quote holds inline content only: flatten block children, one quote each.
|
|
277
|
+
return flowBlocks(el).map((block) => (block.type === 'paragraph' ? elementBase('quote', block.children ?? [], blockExtras(el)) : block));
|
|
278
|
+
}
|
|
279
|
+
case 'pre': {
|
|
280
|
+
const text = (el.textContent ?? '').replace(/\n$/, '');
|
|
281
|
+
return [paragraph(text ? [textNode(text, FORMAT_CODE)] : [])];
|
|
282
|
+
}
|
|
283
|
+
case 'ul':
|
|
284
|
+
case 'ol':
|
|
285
|
+
return [convertList(el)];
|
|
286
|
+
case 'li':
|
|
287
|
+
return convertListItem(el, false).length > 0 ? [elementBase('list', convertListItem(el, false), { listType: 'bullet', start: 1, tag: 'ul' })] : [];
|
|
288
|
+
case 'table':
|
|
289
|
+
return [convertTable(el)];
|
|
290
|
+
case 'hr':
|
|
291
|
+
return [{ type: 'horizontalrule', version: 1 }];
|
|
292
|
+
case 'img': {
|
|
293
|
+
const image = convertImage(el, null);
|
|
294
|
+
return image ? [image] : [];
|
|
295
|
+
}
|
|
296
|
+
case 'figure': {
|
|
297
|
+
const img = el.querySelector('img');
|
|
298
|
+
const figcaption = el.querySelector('figcaption');
|
|
299
|
+
if (!img)
|
|
300
|
+
return flowBlocks(el);
|
|
301
|
+
const caption = figcaption ? (figcaption.textContent ?? '').trim() || null : null;
|
|
302
|
+
const image = convertImage(img, caption);
|
|
303
|
+
return image ? [image] : [];
|
|
304
|
+
}
|
|
305
|
+
case 'div':
|
|
306
|
+
if (el.hasAttribute('data-likhari-page-break'))
|
|
307
|
+
return [{ type: 'page-break', version: 1 }];
|
|
308
|
+
return flowBlocks(el);
|
|
309
|
+
case 'figcaption':
|
|
310
|
+
return [];
|
|
311
|
+
case 'dt':
|
|
312
|
+
case 'dd':
|
|
313
|
+
return [paragraph(inlineChildrenOf(el), blockExtras(el))];
|
|
314
|
+
default:
|
|
315
|
+
return flowBlocks(el);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
export function parseHtml(input, _ctx) {
|
|
319
|
+
if (typeof DOMParser === 'undefined') {
|
|
320
|
+
throw new Error('@inshapardaz/likhari-converters: HTML import needs a DOM (DOMParser); run it in a browser or jsdom.');
|
|
321
|
+
}
|
|
322
|
+
const doc = new DOMParser().parseFromString(input, 'text/html');
|
|
323
|
+
return makeState(flowBlocks(doc.body));
|
|
324
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SerializedEditorState } from 'lexical';
|
|
2
|
+
import type { ConverterContext } from '../types.js';
|
|
3
|
+
import { type SNode } from '../shared/serialized.js';
|
|
4
|
+
export declare function escapeHtml(value: string): string;
|
|
5
|
+
export declare function escapeAttr(value: string): string;
|
|
6
|
+
export declare function nodeToHtml(node: SNode, ctx?: ConverterContext): string;
|
|
7
|
+
export declare function serializeHtml(state: SerializedEditorState, ctx?: ConverterContext): string;
|
|
8
|
+
//# sourceMappingURL=serialize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../src/html/serialize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAE4E,KAAK,KAAK,EAC5F,MAAM,sBAAsB,CAAC;AAE9B,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAgGD,wBAAgB,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAuCtE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,qBAAqB,EAAE,GAAG,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAE1F"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { FORMAT_BOLD, FORMAT_CODE, FORMAT_HIGHLIGHT, FORMAT_ITALIC, FORMAT_STRIKETHROUGH, FORMAT_SUBSCRIPT, FORMAT_SUPERSCRIPT, FORMAT_UNDERLINE, INDENT_PX, rootChildren, } from '../shared/serialized.js';
|
|
2
|
+
export function escapeHtml(value) {
|
|
3
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
4
|
+
}
|
|
5
|
+
export function escapeAttr(value) {
|
|
6
|
+
return escapeHtml(value).replace(/"/g, '"');
|
|
7
|
+
}
|
|
8
|
+
/** Wrapping order matters for round-trips: outermost first. */
|
|
9
|
+
const FORMAT_TAGS = [
|
|
10
|
+
[FORMAT_CODE, 'code'],
|
|
11
|
+
[FORMAT_BOLD, 'strong'],
|
|
12
|
+
[FORMAT_ITALIC, 'em'],
|
|
13
|
+
[FORMAT_UNDERLINE, 'u'],
|
|
14
|
+
[FORMAT_STRIKETHROUGH, 's'],
|
|
15
|
+
[FORMAT_SUBSCRIPT, 'sub'],
|
|
16
|
+
[FORMAT_SUPERSCRIPT, 'sup'],
|
|
17
|
+
[FORMAT_HIGHLIGHT, 'mark'],
|
|
18
|
+
];
|
|
19
|
+
const ALIGNMENTS = new Set(['left', 'right', 'center', 'justify', 'start', 'end']);
|
|
20
|
+
function textToHtml(node) {
|
|
21
|
+
const text = String(node.text ?? '');
|
|
22
|
+
let out = escapeHtml(text).replace(/\t/g, '	');
|
|
23
|
+
const format = Number(node.format ?? 0);
|
|
24
|
+
const style = String(node.style ?? '').trim();
|
|
25
|
+
if (style)
|
|
26
|
+
out = `<span style="${escapeAttr(style)}">${out}</span>`;
|
|
27
|
+
for (const [bit, tag] of FORMAT_TAGS) {
|
|
28
|
+
if (format & bit)
|
|
29
|
+
out = `<${tag}>${out}</${tag}>`;
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function inlineChildren(node, ctx) {
|
|
34
|
+
return (node.children ?? []).map((child) => nodeToHtml(child, ctx)).join('');
|
|
35
|
+
}
|
|
36
|
+
/** `dir`, `style` (alignment, indent) and any extra attributes for a block. */
|
|
37
|
+
function blockAttrs(node, extra = '') {
|
|
38
|
+
let attrs = '';
|
|
39
|
+
if (node.direction === 'rtl' || node.direction === 'ltr')
|
|
40
|
+
attrs += ` dir="${node.direction}"`;
|
|
41
|
+
const styles = [];
|
|
42
|
+
const format = String(node.format ?? '');
|
|
43
|
+
if (ALIGNMENTS.has(format))
|
|
44
|
+
styles.push(`text-align: ${format}`);
|
|
45
|
+
const indent = Number(node.indent ?? 0);
|
|
46
|
+
if (indent > 0)
|
|
47
|
+
styles.push(`padding-inline-start: ${indent * INDENT_PX}px`);
|
|
48
|
+
if (styles.length > 0)
|
|
49
|
+
attrs += ` style="${styles.join('; ')}"`;
|
|
50
|
+
return attrs + extra;
|
|
51
|
+
}
|
|
52
|
+
function listToHtml(node, ctx) {
|
|
53
|
+
const listType = String(node.listType ?? 'bullet');
|
|
54
|
+
const tag = listType === 'number' ? 'ol' : 'ul';
|
|
55
|
+
let extra = '';
|
|
56
|
+
if (tag === 'ol' && Number(node.start) > 1)
|
|
57
|
+
extra += ` start="${Number(node.start)}"`;
|
|
58
|
+
if (listType === 'check')
|
|
59
|
+
extra += ' data-list-type="check"';
|
|
60
|
+
const items = (node.children ?? []).map((item) => listItemToHtml(item, listType, ctx)).join('');
|
|
61
|
+
return `<${tag}${blockAttrs(node, extra)}>${items}</${tag}>`;
|
|
62
|
+
}
|
|
63
|
+
function listItemToHtml(item, listType, ctx) {
|
|
64
|
+
const children = item.children ?? [];
|
|
65
|
+
// Lexical nests a sublist as a listitem whose only child is the list.
|
|
66
|
+
if (children.length === 1 && children[0].type === 'list') {
|
|
67
|
+
return `<li style="list-style-type: none">${listToHtml(children[0], ctx)}</li>`;
|
|
68
|
+
}
|
|
69
|
+
const checked = listType === 'check' ? ` data-checked="${item.checked ? 'true' : 'false'}"` : '';
|
|
70
|
+
return `<li${blockAttrs(item, checked)}>${inlineChildren(item, ctx)}</li>`;
|
|
71
|
+
}
|
|
72
|
+
function tableToHtml(node, ctx) {
|
|
73
|
+
const rows = (node.children ?? [])
|
|
74
|
+
.map((row) => {
|
|
75
|
+
const cells = (row.children ?? [])
|
|
76
|
+
.map((cell) => {
|
|
77
|
+
const header = Number(cell.headerState ?? 0) !== 0;
|
|
78
|
+
const tag = header ? 'th' : 'td';
|
|
79
|
+
const span = (Number(cell.colSpan) > 1 ? ` colspan="${Number(cell.colSpan)}"` : '') +
|
|
80
|
+
(Number(cell.rowSpan) > 1 ? ` rowspan="${Number(cell.rowSpan)}"` : '');
|
|
81
|
+
const content = (cell.children ?? []).map((child) => nodeToHtml(child, ctx)).join('');
|
|
82
|
+
return `<${tag}${span}>${content}</${tag}>`;
|
|
83
|
+
})
|
|
84
|
+
.join('');
|
|
85
|
+
return `<tr>${cells}</tr>`;
|
|
86
|
+
})
|
|
87
|
+
.join('');
|
|
88
|
+
return `<table><tbody>${rows}</tbody></table>`;
|
|
89
|
+
}
|
|
90
|
+
function imageToHtml(node, ctx) {
|
|
91
|
+
const rawSrc = String(node.src ?? '');
|
|
92
|
+
const src = ctx?.resolveImageUrl ? ctx.resolveImageUrl(rawSrc) : rawSrc;
|
|
93
|
+
let img = `<img src="${escapeAttr(src)}" alt="${escapeAttr(String(node.altText ?? ''))}"`;
|
|
94
|
+
if (node.width)
|
|
95
|
+
img += ` width="${Number(node.width)}"`;
|
|
96
|
+
if (node.height)
|
|
97
|
+
img += ` height="${Number(node.height)}"`;
|
|
98
|
+
img += ` data-link-type="${node.linkType === 'embedded' ? 'embedded' : 'linked'}">`;
|
|
99
|
+
const caption = typeof node.caption === 'string' && node.caption !== '' ? `<figcaption>${escapeHtml(node.caption)}</figcaption>` : '';
|
|
100
|
+
return `<figure>${img}${caption}</figure>`;
|
|
101
|
+
}
|
|
102
|
+
export function nodeToHtml(node, ctx) {
|
|
103
|
+
switch (node.type) {
|
|
104
|
+
case 'text':
|
|
105
|
+
return textToHtml(node);
|
|
106
|
+
case 'linebreak':
|
|
107
|
+
return '<br>';
|
|
108
|
+
case 'tab':
|
|
109
|
+
return '	';
|
|
110
|
+
case 'paragraph':
|
|
111
|
+
return `<p${blockAttrs(node)}>${inlineChildren(node, ctx)}</p>`;
|
|
112
|
+
case 'heading': {
|
|
113
|
+
const tag = /^h[1-6]$/.test(String(node.tag)) ? String(node.tag) : 'h2';
|
|
114
|
+
return `<${tag}${blockAttrs(node)}>${inlineChildren(node, ctx)}</${tag}>`;
|
|
115
|
+
}
|
|
116
|
+
case 'quote':
|
|
117
|
+
return `<blockquote${blockAttrs(node)}>${inlineChildren(node, ctx)}</blockquote>`;
|
|
118
|
+
case 'list':
|
|
119
|
+
return listToHtml(node, ctx);
|
|
120
|
+
case 'link':
|
|
121
|
+
case 'autolink': {
|
|
122
|
+
const attrs = ` href="${escapeAttr(String(node.url ?? ''))}"` +
|
|
123
|
+
(node.rel ? ` rel="${escapeAttr(String(node.rel))}"` : '') +
|
|
124
|
+
(node.target ? ` target="${escapeAttr(String(node.target))}"` : '') +
|
|
125
|
+
(node.title ? ` title="${escapeAttr(String(node.title))}"` : '');
|
|
126
|
+
return `<a${attrs}>${inlineChildren(node, ctx)}</a>`;
|
|
127
|
+
}
|
|
128
|
+
case 'horizontalrule':
|
|
129
|
+
return '<hr>';
|
|
130
|
+
case 'page-break':
|
|
131
|
+
return '<div data-likhari-page-break style="page-break-after: always"></div>';
|
|
132
|
+
case 'image':
|
|
133
|
+
return imageToHtml(node, ctx);
|
|
134
|
+
case 'table':
|
|
135
|
+
return tableToHtml(node, ctx);
|
|
136
|
+
default:
|
|
137
|
+
// Unknown node (e.g. a future feature): keep its content, drop the wrapper.
|
|
138
|
+
return inlineChildren(node, ctx);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export function serializeHtml(state, ctx) {
|
|
142
|
+
return rootChildren(state).map((node) => nodeToHtml(node, ctx)).join('\n');
|
|
143
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SerializedEditorState } from 'lexical';
|
|
2
|
+
/**
|
|
3
|
+
* Loose structural view of Lexical's serialized nodes. Converters work on
|
|
4
|
+
* plain JSON so they stay free of the editor, the DOM-bound node classes and
|
|
5
|
+
* React (architecture doc §1).
|
|
6
|
+
*/
|
|
7
|
+
export interface SNode {
|
|
8
|
+
type: string;
|
|
9
|
+
version?: number;
|
|
10
|
+
children?: SNode[];
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
export declare const FORMAT_BOLD = 1;
|
|
14
|
+
export declare const FORMAT_ITALIC = 2;
|
|
15
|
+
export declare const FORMAT_STRIKETHROUGH = 4;
|
|
16
|
+
export declare const FORMAT_UNDERLINE = 8;
|
|
17
|
+
export declare const FORMAT_CODE = 16;
|
|
18
|
+
export declare const FORMAT_SUBSCRIPT = 32;
|
|
19
|
+
export declare const FORMAT_SUPERSCRIPT = 64;
|
|
20
|
+
export declare const FORMAT_HIGHLIGHT = 128;
|
|
21
|
+
/** Pixels Lexical's own DOM export uses per indent level. */
|
|
22
|
+
export declare const INDENT_PX = 40;
|
|
23
|
+
export type Direction = 'ltr' | 'rtl' | null;
|
|
24
|
+
export declare function rootChildren(state: SerializedEditorState): SNode[];
|
|
25
|
+
export declare function textNode(text: string, format?: number, style?: string): SNode;
|
|
26
|
+
export declare function elementBase(type: string, children: SNode[], extra?: Record<string, unknown>): SNode;
|
|
27
|
+
export declare function paragraph(children?: SNode[], extra?: Record<string, unknown>): SNode;
|
|
28
|
+
export declare function makeState(blocks: SNode[]): SerializedEditorState;
|
|
29
|
+
/** Inline node types: everything else is a block. */
|
|
30
|
+
export declare function isInlineNode(node: SNode): boolean;
|
|
31
|
+
//# sourceMappingURL=serialized.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialized.d.ts","sourceRoot":"","sources":["../../src/shared/serialized.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAErD;;;;GAIG;AACH,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAClC,eAAO,MAAM,WAAW,KAAK,CAAC;AAC9B,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,6DAA6D;AAC7D,eAAO,MAAM,SAAS,KAAK,CAAC;AAE5B,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,CAAC;AAE7C,wBAAgB,YAAY,CAAC,KAAK,EAAE,qBAAqB,GAAG,KAAK,EAAE,CAElE;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAI,EAAE,KAAK,SAAK,GAAG,KAAK,CAEpE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,KAAK,CAEvG;AAED,wBAAgB,SAAS,CAAC,QAAQ,GAAE,KAAK,EAAO,EAAE,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,KAAK,CAE5F;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,qBAAqB,CAKhE;AAED,qDAAqD;AACrD,wBAAgB,YAAY,CAAC,IAAI,EAAE,KAAK,GAAG,OAAO,CAEjD"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const FORMAT_BOLD = 1;
|
|
2
|
+
export const FORMAT_ITALIC = 2;
|
|
3
|
+
export const FORMAT_STRIKETHROUGH = 4;
|
|
4
|
+
export const FORMAT_UNDERLINE = 8;
|
|
5
|
+
export const FORMAT_CODE = 16;
|
|
6
|
+
export const FORMAT_SUBSCRIPT = 32;
|
|
7
|
+
export const FORMAT_SUPERSCRIPT = 64;
|
|
8
|
+
export const FORMAT_HIGHLIGHT = 128;
|
|
9
|
+
/** Pixels Lexical's own DOM export uses per indent level. */
|
|
10
|
+
export const INDENT_PX = 40;
|
|
11
|
+
export function rootChildren(state) {
|
|
12
|
+
return (state.root.children ?? []);
|
|
13
|
+
}
|
|
14
|
+
export function textNode(text, format = 0, style = '') {
|
|
15
|
+
return { type: 'text', text, detail: 0, format, mode: 'normal', style, version: 1 };
|
|
16
|
+
}
|
|
17
|
+
export function elementBase(type, children, extra = {}) {
|
|
18
|
+
return { type, children, direction: null, format: '', indent: 0, version: 1, ...extra };
|
|
19
|
+
}
|
|
20
|
+
export function paragraph(children = [], extra = {}) {
|
|
21
|
+
return elementBase('paragraph', children, { textFormat: 0, textStyle: '', ...extra });
|
|
22
|
+
}
|
|
23
|
+
export function makeState(blocks) {
|
|
24
|
+
const children = blocks.length > 0 ? blocks : [paragraph()];
|
|
25
|
+
return {
|
|
26
|
+
root: { type: 'root', children, direction: null, format: '', indent: 0, version: 1 },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Inline node types: everything else is a block. */
|
|
30
|
+
export function isInlineNode(node) {
|
|
31
|
+
return node.type === 'text' || node.type === 'linebreak' || node.type === 'tab' || node.type === 'link' || node.type === 'autolink';
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inshapardaz/likhari-converters",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11-dev.102",
|
|
4
4
|
"description": "Format transformers (Markdown/HTML/plain-text/Lexical JSON) against the canonical Lexical EditorState, isolated from UI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|