@aicayzer/inkkit 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/editor.d.ts +2 -1
- package/dist/editor.js +38 -1
- package/dist/index.d.ts +1 -1
- package/dist/literals.d.ts +1 -0
- package/dist/literals.js +7 -4
- package/dist/print.d.ts +7 -0
- package/dist/print.js +377 -0
- package/dist/types.d.ts +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,10 +33,12 @@ editor.loadDocument({
|
|
|
33
33
|
const snapshot = editor.snapshot(1)
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Markdown includes CommonMark, task lists, strikethrough, autolinks, GFM tables, editable footnotes and reference-style links, bounded callouts, highlights and
|
|
36
|
+
Markdown includes CommonMark, task lists, strikethrough, autolinks, GFM tables, editable footnotes and reference-style links, bounded callouts, highlights, author comments and offline Mermaid diagrams. Unsupported constructs remain literal Markdown. TXT mode treats all text literally. Images require a host adapter. RTF files remain outside the package scope.
|
|
37
|
+
|
|
38
|
+
`await editor.printableSnapshot(1)` returns a complete printable HTML document, print styles and portable image bytes. It excludes author comments, includes folded content and reports unavailable assets or a changed document. Client apps own Print/PDF commands, dialogs and file generation.
|
|
37
39
|
|
|
38
40
|
See [supported syntax](docs/supported-syntax.md), [the host API](docs/api.md), [preservation and clipboard behavior](docs/preservation.md), [development](docs/development.md), and [releases](docs/releasing.md).
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
Delivery scope and release status are tracked in the [InkKit Project](https://github.com/users/aicayzer/projects/3) and [milestones](https://github.com/aicayzer/inkkit/milestones).
|
|
41
43
|
|
|
42
44
|
MIT licensed. InkKit builds on the public PadPad and Memos editors; see [NOTICE](NOTICE).
|
package/dist/editor.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type TableCommand } from './tables.js';
|
|
2
|
-
import { type DocumentInput, type DocumentSnapshot, type ImageAdapter, type ClipboardInput, type ClipboardOutput } from './types.js';
|
|
2
|
+
import { type DocumentInput, type DocumentSnapshot, type ImageAdapter, type ClipboardInput, type ClipboardOutput, type PrintableDocument } from './types.js';
|
|
3
3
|
export type Mark = 'bold' | 'italic' | 'strikethrough' | 'highlight' | 'code' | 'link';
|
|
4
4
|
export type Block = {
|
|
5
5
|
type: 'paragraph';
|
|
@@ -65,6 +65,7 @@ export declare class InkKitEditor {
|
|
|
65
65
|
private assertCurrent;
|
|
66
66
|
private plainSource;
|
|
67
67
|
snapshot(expectedGeneration?: number): DocumentSnapshot;
|
|
68
|
+
printableSnapshot(expectedGeneration?: number): Promise<PrintableDocument>;
|
|
68
69
|
clipboardSnapshot(all?: boolean): Promise<ClipboardOutput>;
|
|
69
70
|
paste(input: ClipboardInput): Promise<void>;
|
|
70
71
|
setCommentsVisible(visible: boolean): void;
|
package/dist/editor.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Editor, defaultValueCtx, editorViewCtx, editorViewOptionsCtx, remarkStringifyOptionsCtx, rootCtx, } from '@milkdown/kit/core';
|
|
1
|
+
import { Editor, defaultValueCtx, editorViewCtx, editorViewOptionsCtx, remarkCtx, remarkStringifyOptionsCtx, rootCtx, } from '@milkdown/kit/core';
|
|
2
2
|
import { visibleClipboard, portableClipboard, clipboardText, clipboardContent, } from './clipboard.js';
|
|
3
3
|
import { clipboard } from '@milkdown/kit/plugin/clipboard';
|
|
4
4
|
import { history } from '@milkdown/kit/plugin/history';
|
|
@@ -27,6 +27,7 @@ import { selectionPlugin } from './selection.js';
|
|
|
27
27
|
import { search, SearchQuery, getSearchState, setSearchState, } from 'prosemirror-search';
|
|
28
28
|
import { taskListPlugin, toggleTaskList } from './tasks.js';
|
|
29
29
|
import { isMermaid, mermaidPreview } from './mermaid.js';
|
|
30
|
+
import { hasAuthoredImagesInLiterals, printableMarkdown, printableText, } from './print.js';
|
|
30
31
|
const shortcutCommands = {
|
|
31
32
|
heading1: ['heading', 1],
|
|
32
33
|
heading2: ['heading', 2],
|
|
@@ -334,6 +335,42 @@ export class InkKitEditor {
|
|
|
334
335
|
dirty: text !== this.originalSource,
|
|
335
336
|
};
|
|
336
337
|
}
|
|
338
|
+
async printableSnapshot(expectedGeneration) {
|
|
339
|
+
const snapshot = this.snapshot(expectedGeneration);
|
|
340
|
+
const epoch = this.documentEpoch;
|
|
341
|
+
const state = this.editor.ctx.get(editorViewCtx).state;
|
|
342
|
+
let output;
|
|
343
|
+
try {
|
|
344
|
+
if (snapshot.format === 'md' &&
|
|
345
|
+
!this.options.images &&
|
|
346
|
+
hasAuthoredImagesInLiterals(state.doc.content, (source) => this.editor.ctx.get(remarkCtx).parse(source)))
|
|
347
|
+
throw new InkKitError('image-unavailable', 'No image adapter is configured for the authored images.');
|
|
348
|
+
output =
|
|
349
|
+
snapshot.format === 'txt'
|
|
350
|
+
? printableText(snapshot.text)
|
|
351
|
+
: await printableMarkdown(state.doc.content, state.schema, this.options.images, this.context());
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
this.assertCurrent(snapshot.generation);
|
|
355
|
+
if (snapshot.documentId !== this.documentId ||
|
|
356
|
+
epoch !== this.documentEpoch ||
|
|
357
|
+
snapshot.revision !== this.revision)
|
|
358
|
+
throw new InkKitError('stale-document', 'Document changed while preparing printable content');
|
|
359
|
+
}
|
|
360
|
+
return Object.freeze({
|
|
361
|
+
documentId: snapshot.documentId,
|
|
362
|
+
generation: snapshot.generation,
|
|
363
|
+
revision: snapshot.revision,
|
|
364
|
+
format: snapshot.format,
|
|
365
|
+
html: output.html,
|
|
366
|
+
styles: output.styles,
|
|
367
|
+
assets: Object.freeze(output.assets.map((asset) => Object.freeze({
|
|
368
|
+
...asset,
|
|
369
|
+
bytes: new Uint8Array(asset.bytes),
|
|
370
|
+
}))),
|
|
371
|
+
warnings: Object.freeze(output.warnings.map((warning) => Object.freeze({ ...warning }))),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
337
374
|
async clipboardSnapshot(all = true) {
|
|
338
375
|
const snapshot = this.snapshot();
|
|
339
376
|
const epoch = this.documentEpoch;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ export { InkKitEditor } from './editor.js';
|
|
|
2
2
|
export type { EditorEvents, CaretState, Mark, Block, FormatCommand, Keymap, } from './editor.js';
|
|
3
3
|
export type { TableCommand } from './tables.js';
|
|
4
4
|
export { InkKitError } from './types.js';
|
|
5
|
-
export type { DocumentFormat, DocumentContext, DocumentInput, DocumentSnapshot, EditorErrorCode, ImageAdapter, CapturedImage, PortableImage, ClipboardInput, ClipboardOutput, ClipboardImage, ClipboardDiagram, } from './types.js';
|
|
5
|
+
export type { DocumentFormat, DocumentContext, DocumentInput, DocumentSnapshot, EditorErrorCode, ImageAdapter, CapturedImage, PortableImage, ClipboardInput, ClipboardOutput, ClipboardImage, ClipboardDiagram, PrintableDocument, PrintableWarning, } from './types.js';
|
package/dist/literals.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ declare module 'mdast' {
|
|
|
12
12
|
inkkitLiteral: InkKitLiteral;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
export declare function sourceFrontmatter(source: string): string | undefined;
|
|
15
16
|
export declare function createLiteralPreservation(images: boolean): import("@milkdown/kit/utils").$Remark<"preserveLiterals", unknown>;
|
|
16
17
|
export declare const preserveLiterals: import("@milkdown/kit/utils").$Remark<"preserveLiterals", unknown>;
|
|
17
18
|
export declare const literalBlock: import("@milkdown/kit/utils").$Node;
|
package/dist/literals.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { $node, $remark } from '@milkdown/kit/utils';
|
|
2
2
|
import { commentSource, literalCommentSpans, } from './comments.js';
|
|
3
3
|
const unsupported = new Set(['html', 'image', 'imageReference']);
|
|
4
|
+
export function sourceFrontmatter(source) {
|
|
5
|
+
return /^(---|\+\+\+)\r?\n[\s\S]*?\r?\n\1(?:\r?\n|$)/.exec(source)?.[0];
|
|
6
|
+
}
|
|
4
7
|
function restoreInlineBreaks(node) {
|
|
5
8
|
if ((node.type === 'paragraph' || node.type === 'heading') &&
|
|
6
9
|
node.children.length > 1) {
|
|
@@ -51,8 +54,8 @@ export function createLiteralPreservation(images) {
|
|
|
51
54
|
restoreInlineBreaks(tree);
|
|
52
55
|
// Micromark's positions exclude an initial byte-order mark.
|
|
53
56
|
const source = String(file.value).replace(/^\uFEFF/, '');
|
|
54
|
-
const frontmatter =
|
|
55
|
-
const frontmatterEnd = frontmatter?.
|
|
57
|
+
const frontmatter = sourceFrontmatter(source);
|
|
58
|
+
const frontmatterEnd = frontmatter?.length ?? 0;
|
|
56
59
|
const preserveBlock = (node, depth = 0) => {
|
|
57
60
|
if (node.type === 'footnoteDefinition') {
|
|
58
61
|
node.children = node.children.map((child) => preserveBlock(child, depth + 1));
|
|
@@ -80,13 +83,13 @@ export function createLiteralPreservation(images) {
|
|
|
80
83
|
if (frontmatter)
|
|
81
84
|
blocks.push({
|
|
82
85
|
type: 'inkkitLiteral',
|
|
83
|
-
value: frontmatter
|
|
86
|
+
value: frontmatter.trimEnd(),
|
|
84
87
|
position: {
|
|
85
88
|
start: { line: 1, column: 1, offset: 0 },
|
|
86
89
|
end: {
|
|
87
90
|
line: 1,
|
|
88
91
|
column: 1,
|
|
89
|
-
offset: frontmatter
|
|
92
|
+
offset: frontmatter.trimEnd().length,
|
|
90
93
|
},
|
|
91
94
|
},
|
|
92
95
|
});
|
package/dist/print.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Fragment, Schema } from '@milkdown/kit/prose/model';
|
|
2
|
+
import { type DocumentContext, type ImageAdapter, type PrintableDocument } from './types.js';
|
|
3
|
+
type PrintableContent = Pick<PrintableDocument, 'html' | 'styles' | 'assets' | 'warnings'>;
|
|
4
|
+
export declare function printableText(text: string): PrintableContent;
|
|
5
|
+
export declare function hasAuthoredImagesInLiterals(content: Fragment, parse: (source: string) => unknown): boolean;
|
|
6
|
+
export declare function printableMarkdown(content: Fragment, schema: Schema, adapter: ImageAdapter | undefined, context: DocumentContext): Promise<PrintableContent>;
|
|
7
|
+
export {};
|
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
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -64,3 +64,17 @@ export interface ClipboardDiagram {
|
|
|
64
64
|
image?: PortableImage;
|
|
65
65
|
error?: string;
|
|
66
66
|
}
|
|
67
|
+
export interface PrintableWarning {
|
|
68
|
+
code: 'diagram-unavailable';
|
|
69
|
+
message: string;
|
|
70
|
+
}
|
|
71
|
+
export interface PrintableDocument {
|
|
72
|
+
documentId: string;
|
|
73
|
+
generation: number;
|
|
74
|
+
revision: number;
|
|
75
|
+
format: DocumentFormat;
|
|
76
|
+
html: string;
|
|
77
|
+
styles: string;
|
|
78
|
+
assets: readonly PortableImage[];
|
|
79
|
+
warnings: readonly PrintableWarning[];
|
|
80
|
+
}
|