@aicayzer/inkkit 0.0.4 → 0.0.6

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/print.js ADDED
@@ -0,0 +1,377 @@
1
+ import { clipboardContent } from './clipboard.js';
2
+ import { shareableFragment } from './comments.js';
3
+ import { splitAlt } from './images.js';
4
+ import { sourceFrontmatter } from './literals.js';
5
+ import { diagramImage, renderDiagram } from './mermaid.js';
6
+ import { InkKitError, } from './types.js';
7
+ const styles = `
8
+ :root { color-scheme: light; font: 11pt/1.5 Arial, sans-serif; }
9
+ html, body { margin: 0; padding: 0; height: auto; overflow: visible; background: white; color: #222; }
10
+ .inkkit-print-document { padding: 1rem; overflow-wrap: anywhere; }
11
+ h1, h2, h3, h4, h5, h6 { line-height: 1.25; break-after: avoid; }
12
+ p, ul, ol, blockquote, pre, table, figure { margin-block: 0 1em; }
13
+ li > p { margin-bottom: .35em; }
14
+ li > ul, li > ol { margin-bottom: .35em; }
15
+ a { color: inherit; text-decoration: underline; }
16
+ mark { color: inherit; background: #fff0a8; print-color-adjust: exact; }
17
+ code, pre { font-family: ui-monospace, Menlo, monospace; }
18
+ pre { white-space: pre-wrap; overflow-wrap: anywhere; overflow: visible; padding: .65em; border: 1px solid #ddd; font-size: .9em; }
19
+ .inkkit-print-literal { padding: 0; border: 0; font-size: 1em; tab-size: 4; }
20
+ blockquote { margin-inline: 0; padding-inline-start: 1em; border-inline-start: 3px solid #aaa; }
21
+ .inkkit-callout { padding: .65em 1em; border-inline-start: 3px solid #777; }
22
+ .inkkit-callout-title { display: block; margin-bottom: .4em; }
23
+ .inkkit-callout-body { display: block; }
24
+ .inkkit-print-footnote { border-inline-start: 2px solid #aaa; padding-inline-start: 1em; margin-bottom: 1em; }
25
+ sup { font-size: .75em; }
26
+ .inkkit-print-task-marker { font-family: ui-monospace, Menlo, monospace; }
27
+ table { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: .95em; }
28
+ th, td { border: 1px solid #aaa; padding: .4em .6em; vertical-align: top; overflow-wrap: anywhere; }
29
+ th { font-weight: bold; }
30
+ th p, td p { margin: 0; }
31
+ thead { display: table-header-group; }
32
+ tfoot { display: table-footer-group; }
33
+ tr { break-inside: avoid; }
34
+ img { max-width: 100%; height: auto; }
35
+ figure { margin-inline: 0; }
36
+ .inkkit-print-diagram img { display: block; }
37
+ figure, img { break-inside: avoid; }
38
+ hr { border: 0; border-top: 1px solid #aaa; }
39
+ @page { margin: 18mm; }
40
+ @media print {
41
+ .inkkit-print-document { padding: 0; }
42
+ img { max-height: 90vh; object-fit: contain; }
43
+ }
44
+ `.trim();
45
+ function documentOutput(content, assets = [], warnings = []) {
46
+ const page = document.implementation.createHTMLDocument('');
47
+ page.head.textContent = '';
48
+ const charset = page.createElement('meta');
49
+ charset.setAttribute('charset', 'utf-8');
50
+ const style = page.createElement('style');
51
+ style.textContent = styles;
52
+ page.head.append(charset, style);
53
+ const main = page.createElement('main');
54
+ main.className = 'inkkit-print-document';
55
+ while (content.firstChild)
56
+ main.appendChild(content.firstChild);
57
+ page.body.append(main);
58
+ return {
59
+ html: '<!DOCTYPE html>\n' + page.documentElement.outerHTML,
60
+ styles,
61
+ assets,
62
+ warnings,
63
+ };
64
+ }
65
+ export function printableText(text) {
66
+ const container = document.createElement('div');
67
+ const pre = document.createElement('pre');
68
+ pre.className = 'inkkit-print-literal';
69
+ pre.textContent = text;
70
+ container.append(pre);
71
+ return documentOutput(container);
72
+ }
73
+ export function hasAuthoredImagesInLiterals(content, parse) {
74
+ const hasImage = (value) => {
75
+ if (!value || typeof value !== 'object')
76
+ return false;
77
+ const node = value;
78
+ if (node.type === 'image')
79
+ return true;
80
+ if (['html', 'code', 'inlineCode', 'imageReference'].includes(String(node.type)))
81
+ return false;
82
+ return Array.isArray(node.children) && node.children.some(hasImage);
83
+ };
84
+ let found = false;
85
+ shareableFragment(content).descendants((node) => {
86
+ if (found)
87
+ return false;
88
+ if (node.type.name !== 'literal_markdown')
89
+ return true;
90
+ const source = node.textContent.replace(/^\uFEFF/, '');
91
+ const body = source.slice(sourceFrontmatter(source)?.length ?? 0);
92
+ if (body && hasImage(parse(body)))
93
+ found = true;
94
+ return false;
95
+ });
96
+ return found;
97
+ }
98
+ function matchesSignature(bytes, mimeType) {
99
+ const ascii = (start, end) => String.fromCharCode(...bytes.subarray(start, end));
100
+ switch (mimeType) {
101
+ case 'image/png':
102
+ return (bytes.length >= 24 &&
103
+ [137, 80, 78, 71, 13, 10, 26, 10].every((byte, index) => bytes[index] === byte) &&
104
+ ascii(12, 16) === 'IHDR');
105
+ case 'image/jpeg':
106
+ return (bytes.length >= 4 &&
107
+ bytes[0] === 255 &&
108
+ bytes[1] === 216 &&
109
+ bytes[2] === 255);
110
+ case 'image/gif':
111
+ return bytes.length >= 10 && /^(GIF87a|GIF89a)$/.test(ascii(0, 6));
112
+ case 'image/webp':
113
+ return (bytes.length >= 12 && ascii(0, 4) === 'RIFF' && ascii(8, 12) === 'WEBP');
114
+ case 'image/avif':
115
+ return (bytes.length >= 16 &&
116
+ ascii(4, 8) === 'ftyp' &&
117
+ /avif|avis/.test(ascii(8, 32)));
118
+ case 'image/bmp':
119
+ return bytes.length >= 26 && ascii(0, 2) === 'BM';
120
+ case 'image/tiff':
121
+ return (bytes.length >= 8 &&
122
+ (ascii(0, 4) === 'II*\0' || ascii(0, 4) === 'MM\0*'));
123
+ default:
124
+ return false;
125
+ }
126
+ }
127
+ async function validatedImage(input) {
128
+ const image = {
129
+ bytes: new Uint8Array(input.bytes),
130
+ mimeType: input.mimeType.toLowerCase(),
131
+ };
132
+ if (!matchesSignature(image.bytes, image.mimeType))
133
+ throw new Error('The portable image bytes do not match a supported image format.');
134
+ const url = URL.createObjectURL(new Blob([image.bytes], { type: image.mimeType }));
135
+ try {
136
+ await new Promise((resolve, reject) => {
137
+ const decoder = new Image();
138
+ const timeout = setTimeout(() => {
139
+ decoder.onload = decoder.onerror = null;
140
+ decoder.removeAttribute('src');
141
+ reject(new Error('The portable image could not be decoded.'));
142
+ }, 10000);
143
+ const finish = (error) => {
144
+ clearTimeout(timeout);
145
+ decoder.onload = decoder.onerror = null;
146
+ if (error)
147
+ reject(error);
148
+ else
149
+ resolve();
150
+ };
151
+ decoder.onload = () => finish(decoder.naturalWidth > 0 && decoder.naturalHeight > 0
152
+ ? undefined
153
+ : new Error('The portable image has no usable dimensions.'));
154
+ decoder.onerror = () => finish(new Error('The portable image could not be decoded.'));
155
+ decoder.src = url;
156
+ });
157
+ return image;
158
+ }
159
+ finally {
160
+ URL.revokeObjectURL(url);
161
+ }
162
+ }
163
+ function dataURL(image) {
164
+ let binary = '';
165
+ for (const byte of image.bytes)
166
+ binary += String.fromCharCode(byte);
167
+ return `data:${image.mimeType};base64,${btoa(binary)}`;
168
+ }
169
+ const semanticTags = new Set([
170
+ 'DIV',
171
+ 'P',
172
+ 'SPAN',
173
+ 'STRONG',
174
+ 'EM',
175
+ 'DEL',
176
+ 'S',
177
+ 'MARK',
178
+ 'A',
179
+ 'SUP',
180
+ 'SUB',
181
+ 'PRE',
182
+ 'CODE',
183
+ 'BLOCKQUOTE',
184
+ 'H1',
185
+ 'H2',
186
+ 'H3',
187
+ 'H4',
188
+ 'H5',
189
+ 'H6',
190
+ 'UL',
191
+ 'OL',
192
+ 'LI',
193
+ 'TABLE',
194
+ 'THEAD',
195
+ 'TBODY',
196
+ 'TFOOT',
197
+ 'TR',
198
+ 'TH',
199
+ 'TD',
200
+ 'HR',
201
+ 'BR',
202
+ 'IMG',
203
+ 'FIGURE',
204
+ 'FIGCAPTION',
205
+ ]);
206
+ const printClasses = new Set([
207
+ 'inkkit-callout',
208
+ 'inkkit-callout-title',
209
+ 'inkkit-callout-body',
210
+ 'inkkit-print-footnote',
211
+ 'inkkit-print-task-marker',
212
+ 'inkkit-print-diagram',
213
+ ]);
214
+ function safeLink(value) {
215
+ if (/[\u0000-\u001f\u007f\\]/.test(value))
216
+ return undefined;
217
+ const href = value.trim();
218
+ if (!href)
219
+ return undefined;
220
+ // A colon in the first path component denotes a scheme, including an
221
+ // obfuscated one. Relative paths can still contain colons after a slash.
222
+ if (/^[^/?#]*:/.test(href) && !/^(https?:|mailto:)/i.test(href))
223
+ return undefined;
224
+ return href.replaceAll(' ', '%20');
225
+ }
226
+ function cleanSemantics(container, localIDs) {
227
+ for (const element of container.querySelectorAll('*')) {
228
+ if (!semanticTags.has(element.tagName)) {
229
+ if (/^(SCRIPT|STYLE|LINK|META|IFRAME|OBJECT|EMBED|BUTTON|INPUT|TEXTAREA|SELECT)$/.test(element.tagName))
230
+ element.remove();
231
+ else
232
+ element.replaceWith(...Array.from(element.childNodes));
233
+ continue;
234
+ }
235
+ const classes = Array.from(element.classList).filter((name) => printClasses.has(name));
236
+ const href = element.tagName === 'A' ? element.getAttribute('href') : null;
237
+ const src = element.tagName === 'IMG' ? element.getAttribute('src') : null;
238
+ const alt = element.tagName === 'IMG' ? element.getAttribute('alt') : null;
239
+ const width = element.tagName === 'IMG' ? element.getAttribute('width') : null;
240
+ const title = element.getAttribute('title');
241
+ const id = element.getAttribute('id');
242
+ const order = element.tagName === 'OL' ? element.getAttribute('start') : null;
243
+ const alignment = element instanceof HTMLElement ? element.style.textAlign : '';
244
+ for (const attribute of [...element.attributes])
245
+ element.removeAttribute(attribute.name);
246
+ if (classes.length)
247
+ element.setAttribute('class', classes.join(' '));
248
+ const destination = href == null ? undefined : safeLink(href);
249
+ if (destination != null)
250
+ element.setAttribute('href', destination);
251
+ if (src &&
252
+ /^data:image\/(png|jpeg|gif|webp|avif|bmp|tiff);base64,[A-Za-z0-9+/=]+$/.test(src))
253
+ element.setAttribute('src', src);
254
+ if (alt != null)
255
+ element.setAttribute('alt', alt);
256
+ if (width && /^\d+$/.test(width))
257
+ element.setAttribute('width', width);
258
+ if (title)
259
+ element.setAttribute('title', title);
260
+ if (id && localIDs.has(id))
261
+ element.setAttribute('id', id);
262
+ if (order && /^\d+$/.test(order))
263
+ element.setAttribute('start', order);
264
+ if (/^(left|center|right)$/.test(alignment) &&
265
+ /^(TH|TD)$/.test(element.tagName))
266
+ element.setAttribute('style', `text-align: ${alignment}`);
267
+ }
268
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_COMMENT);
269
+ const comments = [];
270
+ while (walker.nextNode())
271
+ comments.push(walker.currentNode);
272
+ for (const comment of comments)
273
+ comment.parentNode?.removeChild(comment);
274
+ }
275
+ export async function printableMarkdown(content, schema, adapter, context) {
276
+ content = shareableFragment(content);
277
+ const container = document.createElement('div');
278
+ container.innerHTML = clipboardContent(content, schema).html;
279
+ const references = [];
280
+ const tasks = [];
281
+ content.descendants((node) => {
282
+ if (node.type.name === 'image')
283
+ references.push({
284
+ reference: String(node.attrs.src ?? ''),
285
+ ...splitAlt(String(node.attrs.alt ?? '')),
286
+ });
287
+ if (node.type.name === 'list_item')
288
+ tasks.push(node.attrs.checked ?? null);
289
+ });
290
+ for (const [index, item] of [...container.querySelectorAll('li')].entries()) {
291
+ if (tasks[index] == null)
292
+ continue;
293
+ const marker = document.createElement('span');
294
+ marker.className = 'inkkit-print-task-marker';
295
+ marker.textContent = tasks[index] ? '[x] ' : '[ ] ';
296
+ (item.firstElementChild?.tagName === 'P'
297
+ ? item.firstElementChild
298
+ : item).prepend(marker);
299
+ }
300
+ const footnotes = new Map();
301
+ const localIDs = new Set();
302
+ for (const [index, definition] of [
303
+ ...container.querySelectorAll('[data-inkkit-footnote-definition]'),
304
+ ].entries()) {
305
+ const identifier = definition.getAttribute('data-inkkit-footnote-definition');
306
+ const id = `inkkit-print-note-${index + 1}`;
307
+ localIDs.add(id);
308
+ if (!footnotes.has(identifier))
309
+ footnotes.set(identifier, id);
310
+ definition.setAttribute('id', id);
311
+ definition.className = 'inkkit-print-footnote';
312
+ }
313
+ for (const reference of container.querySelectorAll('[data-inkkit-footnote-reference]')) {
314
+ const id = footnotes.get(reference.getAttribute('data-inkkit-footnote-reference'));
315
+ if (!id)
316
+ continue;
317
+ const link = document.createElement('a');
318
+ link.setAttribute('href', `#${id}`);
319
+ link.textContent = reference.textContent;
320
+ reference.replaceChildren(link);
321
+ }
322
+ const images = new Map();
323
+ for (const [index, element] of [
324
+ ...container.querySelectorAll('img'),
325
+ ].entries()) {
326
+ const reference = references[index];
327
+ try {
328
+ if (!adapter || !reference)
329
+ throw new Error('The authored image is unavailable.');
330
+ const image = await validatedImage(await adapter.exportImage(reference.reference, { ...context }));
331
+ images.set(element, image);
332
+ element.setAttribute('src', dataURL(image));
333
+ element.setAttribute('alt', reference.alt);
334
+ if (reference.width != null && reference.width > 0)
335
+ element.setAttribute('width', String(Math.min(4096, reference.width)));
336
+ }
337
+ catch (error) {
338
+ throw new InkKitError('image-unavailable', error instanceof Error
339
+ ? error.message
340
+ : 'The authored image is unavailable.');
341
+ }
342
+ }
343
+ const warnings = [];
344
+ for (const code of container.querySelectorAll('pre[data-language="mermaid"] > code')) {
345
+ const source = code.textContent ?? '';
346
+ try {
347
+ await renderDiagram(source);
348
+ }
349
+ catch (error) {
350
+ const message = `${error instanceof Error ? error.message : 'This diagram could not be rendered.'} The source is printed below.`;
351
+ warnings.push({ code: 'diagram-unavailable', message });
352
+ const warning = document.createElement('p');
353
+ warning.textContent = message;
354
+ code.parentElement.before(warning);
355
+ continue;
356
+ }
357
+ try {
358
+ const image = await validatedImage(await diagramImage(source));
359
+ const figure = document.createElement('figure');
360
+ figure.className = 'inkkit-print-diagram';
361
+ const element = document.createElement('img');
362
+ element.setAttribute('src', dataURL(image));
363
+ element.setAttribute('alt', 'Mermaid diagram');
364
+ figure.append(element);
365
+ images.set(element, image);
366
+ code.parentElement.replaceWith(figure);
367
+ }
368
+ catch (error) {
369
+ throw new InkKitError('diagram-unavailable', error instanceof Error
370
+ ? error.message
371
+ : 'The diagram image is unavailable.');
372
+ }
373
+ }
374
+ cleanSemantics(container, localIDs);
375
+ const assets = [...container.querySelectorAll('img')].map((element) => images.get(element));
376
+ return documentOutput(container, assets, warnings);
377
+ }
@@ -0,0 +1,3 @@
1
+ import type { Ctx } from '@milkdown/kit/ctx';
2
+ import { type ClipboardInput } from './types.js';
3
+ export declare function pasteSpreadsheet(ctx: Ctx, input: ClipboardInput): boolean;
@@ -0,0 +1,275 @@
1
+ import { editorViewCtx } from '@milkdown/kit/core';
2
+ import { DOMParser as ProseParser, Fragment, } from '@milkdown/kit/prose/model';
3
+ import { isInTable, selectedRect } from '@milkdown/kit/prose/tables';
4
+ import { InkKitError } from './types.js';
5
+ import { literalCommentSpans } from './comments.js';
6
+ import { normalizeTableAlignment } from './table-alignment.js';
7
+ import { dispatchTableOperation, editableTable, selectTableRectangle, } from './tables.js';
8
+ export function pasteSpreadsheet(ctx, input) {
9
+ const view = ctx.get(editorViewCtx);
10
+ if (input.plainText || input.markdown != null)
11
+ return false;
12
+ const inTable = isInTable(view.state);
13
+ if (input.html &&
14
+ /data-inkkit-(?:markdown|reference|footnote|comment)/i.test(input.html)) {
15
+ if (inTable && /<table[\s>]/i.test(input.html))
16
+ reject();
17
+ return false;
18
+ }
19
+ if (input.images?.length || (input.html && /<img[\s>]/i.test(input.html))) {
20
+ if (!input.html || !/<table[\s>]/i.test(input.html))
21
+ return false;
22
+ const template = document.createElement('template');
23
+ template.innerHTML = input.html;
24
+ for (const image of template.content.querySelectorAll('img'))
25
+ image.replaceWith(document.createTextNode(image.getAttribute('alt') ?? 'Image'));
26
+ try {
27
+ htmlGrid(view.state.schema, template.innerHTML);
28
+ return false;
29
+ }
30
+ catch {
31
+ if (!inTable && !standaloneTable(input.html))
32
+ return false;
33
+ }
34
+ }
35
+ let grid;
36
+ try {
37
+ grid = input.html?.match(/<table[\s>]/i)
38
+ ? htmlGrid(view.state.schema, input.html)
39
+ : input.text.includes('\t')
40
+ ? textGrid(view.state.schema, input.text)
41
+ : undefined;
42
+ }
43
+ catch (error) {
44
+ if (inTable || !input.html)
45
+ throw error;
46
+ if (!standaloneTable(input.html))
47
+ return false;
48
+ const schema = view.state.schema;
49
+ const children = [];
50
+ let cursor = 0;
51
+ for (const span of literalCommentSpans(input.html)) {
52
+ if (span.start > cursor)
53
+ children.push(schema.text(input.html.slice(cursor, span.start)));
54
+ children.push(schema.nodes.comment_inline.create({ syntax: span.syntax, block: Boolean(span.block) }, span.value ? schema.text(span.value) : null));
55
+ cursor = span.end;
56
+ }
57
+ if (cursor < input.html.length)
58
+ children.push(schema.text(input.html.slice(cursor)));
59
+ const literal = schema.nodes.literal_markdown.create(null, children);
60
+ const selection = view.state.selection;
61
+ const tr = selection.$from.parent.type.name === 'paragraph' &&
62
+ selection.empty &&
63
+ selection.$from.parent.content.size === 0
64
+ ? view.state.tr.replaceWith(selection.$from.before(), selection.$from.after(), literal)
65
+ : view.state.tr.replaceSelectionWith(literal);
66
+ dispatchTableOperation(view, tr);
67
+ return true;
68
+ }
69
+ if (!grid)
70
+ return false;
71
+ if (!inTable) {
72
+ const schema = view.state.schema;
73
+ const rows = grid.cells.map((row, index) => schema.nodes[index ? 'table_row' : 'table_header_row'].create(null, row.map((content, column) => schema.nodes[index ? 'table_cell' : 'table_header'].create({ alignment: grid.alignment?.[column] ?? 'left' }, schema.nodes.paragraph.create(null, content)))));
74
+ const table = schema.nodes.table.create(null, rows);
75
+ const selection = view.state.selection;
76
+ const tr = selection.$from.parent.type.name === 'paragraph' &&
77
+ selection.empty &&
78
+ selection.$from.parent.content.size === 0
79
+ ? view.state.tr.replaceWith(selection.$from.before(), selection.$from.after(), table)
80
+ : view.state.tr.replaceSelectionWith(table);
81
+ let tableStart = -1;
82
+ tr.doc.descendants((node, pos) => {
83
+ if (node === table)
84
+ tableStart = pos + 1;
85
+ });
86
+ if (tableStart < 0)
87
+ reject();
88
+ selectTableRectangle(tr, tableStart, table, 0, 0, grid.cells.length, grid.width);
89
+ dispatchTableOperation(view, tr);
90
+ return true;
91
+ }
92
+ const rect = selectedRect(view.state);
93
+ if (!editableTable(rect.table))
94
+ reject();
95
+ const width = Math.max(rect.map.width, rect.left + grid.width);
96
+ const height = Math.max(rect.map.height, rect.top + grid.cells.length);
97
+ if (width > 100 || height > 100)
98
+ reject();
99
+ const schema = view.state.schema;
100
+ const alignment = Array.from({ length: width }, (_, column) => column < rect.map.width
101
+ ? rect.table.firstChild.child(column).attrs.alignment
102
+ : 'left');
103
+ const rows = [];
104
+ for (let row = 0; row < height; row++) {
105
+ const cells = [];
106
+ for (let column = 0; column < width; column++) {
107
+ const existing = row < rect.map.height && column < rect.map.width
108
+ ? rect.table.child(row).child(column)
109
+ : undefined;
110
+ const incoming = grid.cells[row - rect.top]?.[column - rect.left];
111
+ const cleared = row >= rect.top &&
112
+ row < rect.bottom &&
113
+ column >= rect.left &&
114
+ column < rect.right;
115
+ if (existing && !incoming && !cleared)
116
+ cells.push(existing);
117
+ else {
118
+ const type = schema.nodes[row ? 'table_cell' : 'table_header'];
119
+ cells.push(type.create({ ...(existing?.attrs ?? {}), alignment: alignment[column] }, schema.nodes.paragraph.create(null, incoming)));
120
+ }
121
+ }
122
+ rows.push(schema.nodes[row ? 'table_row' : 'table_header_row'].create(rect.table.maybeChild(row)?.attrs, cells));
123
+ }
124
+ const table = rect.table.copy(Fragment.fromArray(rows));
125
+ const tr = view.state.tr.replaceWith(rect.tableStart - 1, rect.tableStart - 1 + rect.table.nodeSize, table);
126
+ selectTableRectangle(tr, rect.tableStart, table, rect.top, rect.left, rect.top + grid.cells.length, rect.left + grid.width);
127
+ dispatchTableOperation(view, tr);
128
+ return true;
129
+ }
130
+ function standaloneTable(html) {
131
+ const template = document.createElement('template');
132
+ template.innerHTML = html;
133
+ for (const metadata of template.content.querySelectorAll('meta[charset]'))
134
+ metadata.remove();
135
+ const elements = [...template.content.childNodes].filter((node) => node.nodeType !== 3 || node.textContent?.trim());
136
+ return elements.length === 1 && elements[0] instanceof HTMLTableElement;
137
+ }
138
+ function reject() {
139
+ throw new InkKitError('preservation', 'This spreadsheet cannot be represented safely in a Markdown table. Paste as plain text outside the table to retain its source.');
140
+ }
141
+ function textGrid(schema, input) {
142
+ const text = input.replace(/\r\n?/g, '\n').replace(/\n$/, '');
143
+ const rows = [[]];
144
+ let value = '', quoted = false, closed = false, start = true;
145
+ for (let index = 0; index <= text.length; index++) {
146
+ const character = text[index];
147
+ if (quoted) {
148
+ if (character == null)
149
+ reject();
150
+ if (character === '"') {
151
+ if (text[index + 1] === '"') {
152
+ value += '"';
153
+ index++;
154
+ }
155
+ else {
156
+ quoted = false;
157
+ closed = true;
158
+ }
159
+ }
160
+ else
161
+ value += character;
162
+ continue;
163
+ }
164
+ if (character === '"' && start) {
165
+ quoted = true;
166
+ start = false;
167
+ }
168
+ else if (character === '\t' || character === '\n' || character == null) {
169
+ rows.at(-1).push(value);
170
+ value = '';
171
+ start = true;
172
+ closed = false;
173
+ if (character === '\n')
174
+ rows.push([]);
175
+ }
176
+ else {
177
+ if (closed)
178
+ reject();
179
+ value += character;
180
+ start = false;
181
+ }
182
+ }
183
+ const width = Math.max(...rows.map((row) => row.length));
184
+ if (width > 100 || rows.length > 100)
185
+ reject();
186
+ if (rows.some((row) => row.some((value) => /[\n\t]/.test(value))))
187
+ reject();
188
+ return {
189
+ width,
190
+ cells: rows.map((row) => Array.from({ length: width }, (_, column) => {
191
+ const value = row[column] ?? '';
192
+ return value ? Fragment.from(schema.text(value)) : Fragment.empty;
193
+ })),
194
+ };
195
+ }
196
+ function htmlGrid(schema, html) {
197
+ const template = document.createElement('template');
198
+ template.innerHTML = html;
199
+ const fragment = template.content;
200
+ for (const metadata of fragment.querySelectorAll('meta[charset]'))
201
+ metadata.remove();
202
+ const table = fragment.querySelector('table');
203
+ if (!table || fragment.querySelectorAll('table').length !== 1)
204
+ reject();
205
+ const wrapper = table.parentElement;
206
+ if (wrapper && wrapper.tagName !== 'BODY')
207
+ reject();
208
+ for (const sibling of fragment.childNodes)
209
+ if (sibling !== table &&
210
+ (sibling.textContent?.trim() || sibling.nodeType !== 3))
211
+ reject();
212
+ const rows = [...table.querySelectorAll('tr')];
213
+ if (document.createTreeWalker(table, NodeFilter.SHOW_COMMENT).nextNode())
214
+ reject();
215
+ for (const element of table.children) {
216
+ if (!['TR', 'THEAD', 'TBODY', 'TFOOT'].includes(element.tagName))
217
+ reject();
218
+ if (element.tagName !== 'TR')
219
+ for (const row of element.children)
220
+ if (row.tagName !== 'TR')
221
+ reject();
222
+ }
223
+ const width = rows[0]?.children.length ?? 0;
224
+ if (!width || width > 100 || !rows.length || rows.length > 100)
225
+ reject();
226
+ const parser = ProseParser.fromSchema(schema);
227
+ const inline = new Set([
228
+ 'STRONG',
229
+ 'B',
230
+ 'EM',
231
+ 'I',
232
+ 'S',
233
+ 'DEL',
234
+ 'STRIKE',
235
+ 'CODE',
236
+ 'A',
237
+ 'MARK',
238
+ 'SPAN',
239
+ ]);
240
+ const cells = rows.map((row) => {
241
+ if (row.children.length !== width)
242
+ reject();
243
+ return [...row.children].map((cell) => {
244
+ if (!['TH', 'TD'].includes(cell.tagName))
245
+ reject();
246
+ if ((cell.hasAttribute('rowspan') &&
247
+ cell.getAttribute('rowspan') !== '1') ||
248
+ (cell.hasAttribute('colspan') && cell.getAttribute('colspan') !== '1'))
249
+ reject();
250
+ for (const element of cell.querySelectorAll('*'))
251
+ if (!inline.has(element.tagName) &&
252
+ !(element.tagName === 'P' &&
253
+ element.parentElement === cell &&
254
+ cell.children.length === 1))
255
+ reject();
256
+ const walker = document.createTreeWalker(cell, NodeFilter.SHOW_COMMENT);
257
+ if (walker.nextNode())
258
+ reject();
259
+ if (/[\n\r\t]/.test(cell.textContent ?? ''))
260
+ reject();
261
+ for (const link of cell.querySelectorAll('a'))
262
+ if (!link.hasAttribute('href'))
263
+ reject();
264
+ const parsed = parser.parse(cell, { preserveWhitespace: true });
265
+ if (parsed.childCount === 0)
266
+ return Fragment.empty;
267
+ if (parsed.childCount !== 1 ||
268
+ parsed.firstChild.type.name !== 'paragraph')
269
+ reject();
270
+ return parsed.firstChild.content;
271
+ });
272
+ });
273
+ const alignment = [...rows[0].children].map((cell) => normalizeTableAlignment(cell) ?? 'left');
274
+ return { width, cells, alignment };
275
+ }
@@ -0,0 +1,2 @@
1
+ export declare function normalizeTableAlignment(element: HTMLElement): 'left' | 'center' | 'right' | null;
2
+ export declare function normalizeTableColumnAlignment(element: HTMLElement): 'left' | 'center' | 'right' | null;