rapier-markdown-kit 1.2.7 → 1.2.9
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 +32 -30
- package/dist/agent/will.mjs +1 -6
- package/dist/images/gif.mjs +0 -3
- package/dist/images/playback.mjs +1 -10
- package/dist/kit/assets.mjs +0 -1
- package/dist/kit/conformance/measurer.mjs +0 -0
- package/dist/kit/conformance/run.mjs +1 -8
- package/dist/kit/export-fonts.mjs +0 -14
- package/dist/kit/index.mjs +1 -1
- package/dist/kit/ledger/authored-origin.mjs +0 -5
- package/dist/kit/ledger/authorship.mjs +0 -2
- package/dist/kit/ledger/carried.d.mts +20 -0
- package/dist/kit/ledger/carried.mjs +4 -7
- package/dist/kit/ledger/data.mjs +0 -8
- package/dist/kit/ledger/format.mjs +0 -25
- package/dist/kit/ledger/hash.mjs +11 -7
- package/dist/kit/ledger/history.mjs +10 -29
- package/dist/kit/ledger/inverse-source.mjs +0 -46
- package/dist/kit/ledger/journal-records.mjs +24 -56
- package/dist/kit/ledger/merge.mjs +0 -27
- package/dist/kit/ledger/text-merge.mjs +0 -20
- package/dist/kit/ledger/transport.mjs +0 -47
- package/dist/kit/ledger.d.mts +88 -0
- package/dist/kit/ledger.mjs +14 -0
- package/dist/kit/markdown-it.d.mts +4 -0
- package/dist/kit/markdown-it.mjs +36 -0
- package/dist/kit/pdf-glyphs.mjs +39 -45
- package/dist/kit/pdf-source.d.mts +5 -0
- package/dist/kit/pdf-source.mjs +125 -0
- package/dist/kit/pdf-vectors.mjs +0 -3
- package/dist/kit/print-capitals.mjs +0 -4
- package/dist/kit/render-entry.mjs +0 -1
- package/dist/kit/render-ink.mjs +0 -5
- package/dist/kit/render-markdown.mjs +15 -119
- package/dist/kit/render-print.mjs +4 -49
- package/dist/kit/render-sanitize.mjs +22 -58
- package/dist/kit/render-styles.mjs +0 -7
- package/dist/kit/render-work.mjs +0 -12
- package/dist/kit/render.mjs +18 -173
- package/dist/kit/will.d.mts +42 -0
- package/dist/layout/hyphenation.mjs +0 -23
- package/dist/layout/line-plan.mjs +0 -20
- package/dist/layout/model.mjs +0 -55
- package/dist/layout/paragraph-fit.mjs +50 -60
- package/dist/layout/preparation.mjs +23 -56
- package/dist/layout/prepared-paragraph.mjs +0 -25
- package/dist/layout/typography.mjs +103 -41
- package/dist/packages/rapier-diff/concerns.mjs +0 -16
- package/dist/packages/rapier-diff/diff.mjs +0 -7
- package/dist/packages/rapier-diff/index.mjs +0 -1
- package/dist/packages/rapier-diff/matching.mjs +0 -19
- package/dist/packages/rapier-diff/merge.mjs +0 -9
- package/dist/packages/rapier-diff/moves.mjs +0 -7
- package/dist/packages/rapier-diff/structure.mjs +0 -2
- package/dist/packages/rapier-diff/tokens.mjs +0 -6
- package/dist/packages/rapier-diff/walk.mjs +0 -12
- package/dist/spec/document-fonts.mjs +0 -9
- package/dist/spec/document-settings.mjs +6 -25
- package/dist/spec/frontmatter.mjs +0 -1
- package/dist/spec/html-blocks.mjs +3 -37
- package/dist/spec/md-assets.mjs +2 -61
- package/dist/spec/md-layout.mjs +0 -34
- package/dist/spec/md-marks.mjs +0 -51
- package/dist/spec/md-source.mjs +0 -16
- package/dist/spec/word-sidecar.mjs +0 -2
- package/package.json +21 -4
- package/style.css +38 -21
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// One reader of document-wide settings and the inherited font snapshot.
|
|
3
|
-
// Framing stays in spec/frontmatter.mjs. Reading never rewrites source.
|
|
4
|
-
// The live view reads fontsize, the three font scopes and linestretch. It ignores papersize,
|
|
5
|
-
// geometry and pagestyle: a scroll has no page. Word and the PDF read all of them.
|
|
6
2
|
import {frontMatterFrame, frontMatterLine} from './frontmatter.mjs';
|
|
7
3
|
import {normalizeFontFamily, fontFamilyCss, FONT_STACKS} from './md-marks.mjs';
|
|
8
4
|
|
|
9
|
-
// Twelve points are one reference unit (16 px at the default reading scale).
|
|
10
5
|
const NUMBER = /^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
11
6
|
export const DOCUMENT_LEADING = 1.75 / 1.1;
|
|
12
7
|
|
|
13
|
-
// Before/after are points; line is a multiple of the element's own type size.
|
|
14
|
-
// Presets and native exports read this same table. Explicit measures do not grow with font size.
|
|
15
8
|
export const SPACING_TYPES = Object.freeze(['paragraph', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
16
9
|
'list', 'item', 'table', 'picture', 'drawing', 'quote', 'code', 'rule', 'footnote', 'toc']);
|
|
17
10
|
export const SPACING_PRESETS = Object.freeze({compact: .7, standard: 1, roomy: 1.5});
|
|
@@ -19,7 +12,7 @@ const SPACING_SELECTORS = Object.freeze({paragraph: 'p', h1: 'h1', h2: 'h2', h3:
|
|
|
19
12
|
list: ':is(ul,ol)', item: 'li', table: ':is(table,.table-scroll-wrap)', picture: 'p[data-md-picture]',
|
|
20
13
|
drawing: ':is(.diagram-block,p[data-md-picture]:has(img[data-md-svg],img[src^="data:image/svg+xml"]))', quote: 'blockquote',
|
|
21
14
|
code: 'pre', rule: 'hr:not(.footnotes-sep):not([data-md-break])', footnote: '.footnotes', toc: '.rapier-jump-list'});
|
|
22
|
-
const HEADING_LINE = [
|
|
15
|
+
const HEADING_LINE = [1.75 / 2.2, 1.5 / 1.75, 1.2 / 1.5, 1 / 1.25, .9 / 1.1, .82];
|
|
23
16
|
|
|
24
17
|
export function normalizeDocumentSpacing(value) {
|
|
25
18
|
if (typeof value === 'string') {
|
|
@@ -60,7 +53,6 @@ export function documentSpacing(settings, kind = 'paragraph') {
|
|
|
60
53
|
return {before: before * scale * size, after: after * scale * size, line, ...spacing?.[type]};
|
|
61
54
|
}
|
|
62
55
|
|
|
63
|
-
// Word's widely supported Calibri/11 pt Normal template. Explicit document settings always win.
|
|
64
56
|
export function wordDocumentSettings(settings) {
|
|
65
57
|
return {...settings, word: true, fontsize: settings?.fontsize || fontSize('11pt'),
|
|
66
58
|
mainfont: settings?.mainfont || documentFont('Calibri'), sansfont: settings?.sansfont || documentFont('Calibri'),
|
|
@@ -68,7 +60,7 @@ export function wordDocumentSettings(settings) {
|
|
|
68
60
|
}
|
|
69
61
|
export function documentHeadingSize(settings, level) {
|
|
70
62
|
const body = (settings?.fontsize?.unit || 1.1) * 12;
|
|
71
|
-
return body * (settings?.word ? [16/11,13/11,12/11,1,1,1] : [2.
|
|
63
|
+
return body * (settings?.word ? [16/11,13/11,12/11,1,1,1] : [2.2,1.75,1.5,1.25,1.1,1])[level - 1];
|
|
72
64
|
}
|
|
73
65
|
|
|
74
66
|
export function documentSpacingCss(settings, scope = '.md-render', defaults = false) {
|
|
@@ -80,7 +72,6 @@ export function documentSpacingCss(settings, scope = '.md-render', defaults = fa
|
|
|
80
72
|
css += selector + '{margin-block:' + length(row.before) + ' ' + length(row.after) + ';line-height:' + row.line + '}';
|
|
81
73
|
if (settings?.word && /^h[1-6]$/.test(type)) css += selector + '{font-size:' + length(documentHeadingSize(settings, Number(type[1]))) + '}';
|
|
82
74
|
}
|
|
83
|
-
// Compound blocks own the outside gap; the last child does not add it again.
|
|
84
75
|
css += scope + ' :is(li,td,th,blockquote)>:last-child{margin-bottom:0}' +
|
|
85
76
|
scope + ' li:last-child{margin-bottom:0}' + scope + ' li>:is(ul,ol){margin-bottom:0}' +
|
|
86
77
|
scope + ' .table-scroll-wrap>table{margin-block:0}' +
|
|
@@ -88,7 +79,10 @@ export function documentSpacingCss(settings, scope = '.md-render', defaults = fa
|
|
|
88
79
|
scope + ' pre code{line-height:inherit}' + scope + ' .footnotes{padding-top:0}' +
|
|
89
80
|
scope + ' .footnotes-list{margin-block:0}' + scope + ' blockquote:not(.callout){padding-block:0}';
|
|
90
81
|
const item = documentSpacing(settings, 'item');
|
|
91
|
-
css += scope + '
|
|
82
|
+
css += scope + ' li{--md-item-gap:' + length(Math.max(item.before, item.after)) + '}';
|
|
83
|
+
const list = documentSpacing(settings, 'list'), heading = documentSpacing(settings, 'h2');
|
|
84
|
+
css += scope + '{--md-list-heading-before:' + length(Math.max(list.after, heading.before)) + ';--md-list-heading-after:' + length(Math.max(heading.after, list.before)) + '}';
|
|
85
|
+
css += scope + ' ul.rapier-jump-list>li{--md-jump-pad:0px;margin-block:' + length(item.before) + ' ' + length(item.after) + '}' +
|
|
92
86
|
scope + ' ul.rapier-jump-list>li:last-child{margin-bottom:0}';
|
|
93
87
|
for (const [type, selector] of [['item','li>p'], ['table',':is(td,th)>p'], ['quote','blockquote>p'], ['footnote','.footnotes p'], ['toc','.rapier-jump-list li']])
|
|
94
88
|
css += scope + ' ' + selector + '{line-height:' + documentSpacing(settings, type).line + '}';
|
|
@@ -144,7 +138,6 @@ function marginOf(value) {
|
|
|
144
138
|
return Object.freeze({margin: match[1] + unit, twips});
|
|
145
139
|
}
|
|
146
140
|
|
|
147
|
-
// Token spans retain the source's quotes, comments and collection punctuation.
|
|
148
141
|
function quotedEnd(text, start = 0, limit = text.length) {
|
|
149
142
|
const quote = text[start];
|
|
150
143
|
for (let at = start + 1; at < limit; at++) {
|
|
@@ -223,7 +216,6 @@ function fieldLine(line, indented = false) {
|
|
|
223
216
|
function valueSpan(source, field, limit) {
|
|
224
217
|
let at = field.start;
|
|
225
218
|
const first = source[at];
|
|
226
|
-
// Tags and anchors may name other YAML values. They are retained, never rewritten by a control.
|
|
227
219
|
if (first === '&' || first === '*' || first === '!') {
|
|
228
220
|
field.opaque = true;
|
|
229
221
|
while (at < field.line.end && /[&*!]/.test(source[at])) {
|
|
@@ -268,7 +260,6 @@ function mapFields(source, lines, indent) {
|
|
|
268
260
|
return {fields, invalid};
|
|
269
261
|
}
|
|
270
262
|
|
|
271
|
-
// An append must never enter an unclosed collection or quoted value, including one under an unrelated key.
|
|
272
263
|
function structureIsClosed(source, lines) {
|
|
273
264
|
let blockIndent = null, through = 0;
|
|
274
265
|
const limit = lines.at(-1)?.end ?? 0;
|
|
@@ -305,7 +296,6 @@ export function documentFont(value) {
|
|
|
305
296
|
return Object.freeze(named ? {...named, family} : {family, stack: 'named', css: fontFamilyCss(family), word: family});
|
|
306
297
|
}
|
|
307
298
|
|
|
308
|
-
// Font controls use the same source spans as the other document settings.
|
|
309
299
|
export function writeDocumentFontSetting(markdown, key, value) {
|
|
310
300
|
if (!['mainfont', 'sansfont', 'monofont', 'defaultfont'].includes(key)) throw new TypeError('font: unknown document key');
|
|
311
301
|
const family = value == null ? null : normalizeFontFamily(value);
|
|
@@ -313,7 +303,6 @@ export function writeDocumentFontSetting(markdown, key, value) {
|
|
|
313
303
|
return updateDocumentSettings(markdown, {[key]: family});
|
|
314
304
|
}
|
|
315
305
|
|
|
316
|
-
// A language tag is metadata, never CSS or markup. Its source spelling stays untouched.
|
|
317
306
|
export function languageTag(value) {
|
|
318
307
|
if (typeof value !== 'string' || value.length > 128 || !/^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/i.test(value)) return null;
|
|
319
308
|
try { return Intl.getCanonicalLocales(value)[0] || null; }
|
|
@@ -324,7 +313,6 @@ export function documentLanguage(settings, device = globalThis.navigator?.langua
|
|
|
324
313
|
return languageTag(settings?.lang) || languageTag(device) || 'en';
|
|
325
314
|
}
|
|
326
315
|
|
|
327
|
-
// Unrecognized fields remain authored bytes. A duplicate key has no single value.
|
|
328
316
|
export function readDocumentSettings(markdown) {
|
|
329
317
|
const source = String(markdown ?? ''), {frame, fields} = fieldsOf(source);
|
|
330
318
|
if (!frame.closing) return EMPTY;
|
|
@@ -347,7 +335,6 @@ export function readDocumentSettings(markdown) {
|
|
|
347
335
|
});
|
|
348
336
|
}
|
|
349
337
|
|
|
350
|
-
// Change only supplied settings. Unrelated YAML, comments, line endings, BOM and body are never reprinted.
|
|
351
338
|
export function updateDocumentSettings(markdown, patch) {
|
|
352
339
|
const source = String(markdown ?? ''), parsed = fieldsOf(source), {frame, fields} = parsed, before = readDocumentSettings(source);
|
|
353
340
|
const changes = [], append = [];
|
|
@@ -389,15 +376,11 @@ export function updateDocumentSettings(markdown, patch) {
|
|
|
389
376
|
return next;
|
|
390
377
|
}
|
|
391
378
|
|
|
392
|
-
// The page's <title>, the PDF's document title. Subtitle follows the title.
|
|
393
|
-
// An absent title returns '' so the caller keeps the filename.
|
|
394
379
|
export function documentTitle(settings) {
|
|
395
380
|
if (!settings || !settings.title) return '';
|
|
396
381
|
return settings.subtitle ? settings.title + ' \u2014 ' + settings.subtitle : settings.title;
|
|
397
382
|
}
|
|
398
383
|
|
|
399
|
-
// A supplied personal default replaces only the inherited snapshot. Explicit scopes win.
|
|
400
|
-
// Omitted means use the portable source; null means the reader's factory default.
|
|
401
384
|
export function documentSettingsFonts(settings, defaultFont) {
|
|
402
385
|
const inherited = defaultFont === undefined ? settings?.defaultfont : documentFont(defaultFont);
|
|
403
386
|
const main = settings?.mainfont || inherited;
|
|
@@ -416,8 +399,6 @@ export function documentSettingsStyle(settings, defaultFont) {
|
|
|
416
399
|
return (decls.length ? '.md-render{' + decls.join(';') + '}' : '') + documentSpacingCss(settings);
|
|
417
400
|
}
|
|
418
401
|
|
|
419
|
-
// Null when the page keys do not change the print sheet. The view never asks.
|
|
420
|
-
// The default rule stays the writer's own: size:auto; margin:12mm 14mm 14mm.
|
|
421
402
|
export function documentSettingsPageRule(settings) {
|
|
422
403
|
if (!settings || (!settings.papersize && !settings.geometry && settings.pagestyle !== 'plain')) return null;
|
|
423
404
|
const size = settings.papersize ? settings.papersize.css : 'auto';
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Framing owns only offsets. No field interpretation or source normalization belongs here.
|
|
3
2
|
export function frontMatterLine(text, start) {
|
|
4
3
|
let end = start;
|
|
5
4
|
while (end < text.length && text[end] !== '\r' && text[end] !== '\n') end++;
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Fenced HTML stays source. Its preview has no document authority.
|
|
3
2
|
import {inspectGIF} from '../images/gif.mjs';
|
|
4
3
|
import {createMediaPlayback} from '../images/playback.mjs';
|
|
5
4
|
import {sha256} from '../kit/ledger/hash.mjs';
|
|
6
5
|
import {formatLayout, parseLayout, validLayout, imageStyle, objectHeight} from './md-layout.mjs';
|
|
7
6
|
|
|
8
|
-
// The elements a preview keeps; every other element is removed with its content.
|
|
9
7
|
export const HTML_ELEMENTS = Object.freeze('a abbr address article aside b bdi bdo blockquote br button caption circle clipPath code col colgroup dd defs del details dfn div dl dt ellipse em fieldset figcaption figure filter feBlend feColorMatrix feComponentTransfer feComposite feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur feMerge feMergeNode feOffset footer g h1 h2 h3 h4 h5 h6 header hr i image img input kbd label legend li line linearGradient main mark mask meter nav ol optgroup option p path pattern picture polygon polyline pre progress q radialGradient rect s samp section select small source span stop strong style sub summary sup svg table tbody td text textarea th thead time title tr tspan u ul use var animate animateTransform animateMotion set mpath'.toLowerCase().split(' '));
|
|
10
8
|
const VOID = new Set('area base br col embed hr img input link meta source track wbr'.split(' '));
|
|
11
9
|
|
|
12
|
-
// The element names a preview removes from this body, in first-seen order. Content inside a removed element goes with it.
|
|
13
10
|
export function htmlRemoved(body) {
|
|
14
11
|
const allowed = new Set(HTML_ELEMENTS), removed = new Set(), tag = /<(\/?)([A-Za-z][\w:-]*)[^>]*?(\/?)>/g;
|
|
15
12
|
for (let found; (found = tag.exec(body));) {
|
|
@@ -17,7 +14,6 @@ export function htmlRemoved(body) {
|
|
|
17
14
|
if (found[1] || allowed.has(name)) continue;
|
|
18
15
|
removed.add(name);
|
|
19
16
|
if (VOID.has(name) || found[3]) continue;
|
|
20
|
-
// Skip to this element's own end tag; nested same-name elements stay counted.
|
|
21
17
|
const inner = new RegExp('<(/?)' + name + '\\b[^>]*>', 'gi'); inner.lastIndex = tag.lastIndex;
|
|
22
18
|
for (let depth = 1, next; depth && (next = inner.exec(body));) {depth += next[1] ? -1 : 1; tag.lastIndex = inner.lastIndex;}
|
|
23
19
|
if (!new RegExp('</' + name + '\\b', 'i').test(body.slice(found.index))) break;
|
|
@@ -25,7 +21,6 @@ export function htmlRemoved(body) {
|
|
|
25
21
|
return [...removed];
|
|
26
22
|
}
|
|
27
23
|
|
|
28
|
-
// Placement belongs to the trailing md-layout comment alone. Unknown keys are kept and ignored.
|
|
29
24
|
export function htmlAttributes(info) {
|
|
30
25
|
const match = /^html(?:\s+\{([^{}]*)\})?\s*$/i.exec(String(info));
|
|
31
26
|
if (!match) return null;
|
|
@@ -64,7 +59,6 @@ export function htmlFence(raw) {
|
|
|
64
59
|
layout: layout || HTML_FLOW, marker: attached ? {start: end + attached[1].length, end: end + attached[1].length + attached[2].length, text: attached[2]} : null};
|
|
65
60
|
}
|
|
66
61
|
|
|
67
|
-
// Without a layout comment a block is in normal flow across the column.
|
|
68
62
|
const HTML_FLOW = Object.freeze({width: 100});
|
|
69
63
|
|
|
70
64
|
export function htmlImageRecord(raw) {
|
|
@@ -73,8 +67,6 @@ export function htmlImageRecord(raw) {
|
|
|
73
67
|
tokenEnd: raw.length, source: raw, placement: fence.layout, standalone: true, imageOnly: true};
|
|
74
68
|
}
|
|
75
69
|
|
|
76
|
-
// The first phase of the editor's bounded worker splits before layout annotation. Join only
|
|
77
|
-
// the exact adjacent marker proven by this same source grammar, preserving offsets.
|
|
78
70
|
export function mergeHtmlObjectBlocks(blocks, source) {
|
|
79
71
|
for (let index = 0; index + 1 < blocks.length; index++) {
|
|
80
72
|
const block = blocks[index], next = blocks[index + 1];
|
|
@@ -91,7 +83,6 @@ export function mergeHtmlObjectBlocks(blocks, source) {
|
|
|
91
83
|
export function htmlLayoutEdit(record, value) {
|
|
92
84
|
const fence = htmlFence(record.block.raw); if (!fence) return null;
|
|
93
85
|
const layout = Object.fromEntries(Object.entries(value).filter(([,v]) => v != null));
|
|
94
|
-
// HTML height is its clipping viewport in document lines, not drop-cap sizing.
|
|
95
86
|
const height = layout.lines; delete layout.lines;
|
|
96
87
|
if (!validLayout(layout)) return null;
|
|
97
88
|
const resizedHeight = height ?? (fence.attrs.resize === 'scale' && fence.attrs.height && fence.attrs.height !== 'auto' && layout.width && fence.layout.width
|
|
@@ -103,7 +94,6 @@ export function htmlLayoutEdit(record, value) {
|
|
|
103
94
|
text: info + fence.source.slice(fence.infoEnd, fence.fenceEnd) + (marker ? line + marker : '') + tail};
|
|
104
95
|
}
|
|
105
96
|
|
|
106
|
-
// Change only owned header tokens. Body, fence spelling, class/id and unrelated attributes stay exact.
|
|
107
97
|
function htmlInfoEdit(info, changes) {
|
|
108
98
|
let text = info;
|
|
109
99
|
for (const [key, value] of Object.entries(changes)) {
|
|
@@ -125,13 +115,10 @@ export function htmlPlaceholder(token, escape) {
|
|
|
125
115
|
const layout = token.meta?.mdLayout?.layout || HTML_FLOW, marker = encodeURIComponent(formatLayout(layout));
|
|
126
116
|
const height = attrs.height && attrs.height !== 'auto' ? attrs.height : 8;
|
|
127
117
|
const source = encodeURIComponent(JSON.stringify({info: token.info, body: token.content, attrs, layout}));
|
|
128
|
-
// The image is the existing placement owner's measurable box. Preview content is mounted separately.
|
|
129
118
|
const pixel = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII=';
|
|
130
119
|
return '<p class="rapier-html-block" data-md-layout="' + escape(marker) + '"><img data-rapier-markdown-image="" data-rapier-image-index="0" data-rapier-html="' + escape(source) + '" data-rapier-image-layout="' + escape(marker) + '" src="' + pixel + '" alt="' + escape(attrs.id || 'HTML block') + '" style="' + imageStyle(layout) + ';height:calc(' + height + ' * var(--md-line, 1.6em));object-fit:fill" /></p>\n';
|
|
131
120
|
}
|
|
132
121
|
|
|
133
|
-
// The frame owns CSS layout and pixels only. All commands are scoped to this
|
|
134
|
-
// opaque window instance; no measurement or playback reply can edit source.
|
|
135
122
|
function htmlFrameRuntime(config, media, win) {
|
|
136
123
|
const send = value => win.parent.postMessage({type: 'rapier-object-state', id: config.id, ...value}, '*');
|
|
137
124
|
let paused = true, disposed = false, generation = 0, timer = 0, motion = 0, signature = '', busy = false;
|
|
@@ -219,7 +206,6 @@ function htmlFrameRuntime(config, media, win) {
|
|
|
219
206
|
for (const property of style) copy.style.setProperty(property, style.getPropertyValue(property), 'important');
|
|
220
207
|
if (element.tagName === 'IMG' && element.dataset.rapierAnimation) copy.style.setProperty('visibility', rasters.find(row => row.image === element)?.visibility || 'visible', 'important');
|
|
221
208
|
copy.style.setProperty('animation', 'none', 'important'); copy.style.setProperty('transition', 'none', 'important');
|
|
222
|
-
// SVG animated geometry is not represented by serialized timeline state.
|
|
223
209
|
for (const attr of element.attributes) {
|
|
224
210
|
const value = element[attr.name]?.animVal;
|
|
225
211
|
if (value && typeof value.value === 'number') copy.setAttribute(attr.name, String(value.value));
|
|
@@ -236,8 +222,6 @@ function htmlFrameRuntime(config, media, win) {
|
|
|
236
222
|
const p = element.animatedPoints.getItem(index); return p.x + ',' + p.y;
|
|
237
223
|
}).join(' '));
|
|
238
224
|
if (element.tagName === 'IMG' && element.dataset.rapierFirstFrame) copy.src = element.dataset.rapierFirstFrame;
|
|
239
|
-
// Pseudo content is a separate painted box; materialize it before removing
|
|
240
|
-
// stylesheets so media queries cannot run again in the smaller raster SVG.
|
|
241
225
|
for (const pseudo of ['::before', '::after']) {
|
|
242
226
|
const computed = win.getComputedStyle(element, pseudo), content = computed.content;
|
|
243
227
|
if (!content || content === 'none' || content === 'normal') continue;
|
|
@@ -266,7 +250,6 @@ function htmlFrameRuntime(config, media, win) {
|
|
|
266
250
|
if (!disposed && (width !== Math.ceil(win.innerWidth) || height !== Math.ceil(win.innerHeight))) {signature = ''; schedule();}
|
|
267
251
|
}
|
|
268
252
|
}
|
|
269
|
-
// A pending snapshot is never pushed back: a steady stream of resize notices (Firefox sends one per frame) must not starve it.
|
|
270
253
|
const schedule = () => {if (!timer) timer = win.setTimeout(snapshot, 80); for (const row of rasters) rasterPosition(row);};
|
|
271
254
|
const observer = new win.ResizeObserver(schedule); observer.observe(root);
|
|
272
255
|
win.addEventListener('resize', schedule);
|
|
@@ -286,8 +269,6 @@ export function htmlRuntimeSource() {
|
|
|
286
269
|
return '{const HTML_ELEMENTS=' + JSON.stringify(HTML_ELEMENTS) + ';(' + mountHtmlBlocks.toString() + ')(document,{media:(' + createMediaPlayback.toString() + ')(window,' + inspectGIF.toString() + '),frameRuntime:' + htmlFrameRuntime.toString() + ',heightOf:' + objectHeight.toString() + '});}';
|
|
287
270
|
}
|
|
288
271
|
|
|
289
|
-
// A poster is an ordinary existing Markdown image definition, bound to source
|
|
290
|
-
// and the canonical export viewport. Placement still belongs only to md-layout.
|
|
291
272
|
export function objectPosterLabel(image, width = 640) {
|
|
292
273
|
if (!image.hasAttribute('data-rapier-html')) return posterLabel(image.getAttribute('data-rapier-animation'));
|
|
293
274
|
const source = JSON.parse(decodeURIComponent(image.dataset.rapierHtml));
|
|
@@ -302,12 +283,8 @@ const posterLabel = (object, layout, width = 640) => {
|
|
|
302
283
|
return 'rapier-poster-v1-' + sha256('rapier-object-poster-v1\nlight\n' + value);
|
|
303
284
|
};
|
|
304
285
|
|
|
305
|
-
// An act that removes or changes an object removes the still that belonged to it, in the same act.
|
|
306
|
-
// Only objects the act touched are hashed; an identical untouched object keeps the shared still.
|
|
307
|
-
// Picture definitions the same act retires (`retired`, offsets in after) take their stills with them.
|
|
308
286
|
export function retiredPosterSplices(before, after, splices, retired = []) {
|
|
309
287
|
if (!splices.length || !after.includes('rapier-poster-v1-')) return [];
|
|
310
|
-
// The changed window: the one splice itself, or the common prefix and suffix of a several-splice act.
|
|
311
288
|
let start = 0, endBefore = before.length, endAfter = after.length;
|
|
312
289
|
if (splices.length === 1) {start = splices[0].pos; endBefore = start + splices[0].removed.length; endAfter = start + splices[0].inserted.length;}
|
|
313
290
|
else {
|
|
@@ -340,7 +317,6 @@ export function retiredPosterSplices(before, after, splices, retired = []) {
|
|
|
340
317
|
return rows.reverse();
|
|
341
318
|
}
|
|
342
319
|
|
|
343
|
-
// One host presentation adapter: existing image geometry remains the authority.
|
|
344
320
|
export function mountHtmlBlocks(doc, options = {}) {
|
|
345
321
|
if (!options.root && doc.rapierObjects) return doc.rapierObjects;
|
|
346
322
|
const root = options.root || doc.body;
|
|
@@ -351,7 +327,6 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
351
327
|
const owned = new Map(), local = new Map(), reduced = win.matchMedia('(prefers-reduced-motion: reduce)');
|
|
352
328
|
let pending = 0, dead = false, sequence = 0, noticed = false, identity = options.identity?.();
|
|
353
329
|
const pixel = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII=';
|
|
354
|
-
// Each frame gets its own nonce; the page's script nonce never enters a preview.
|
|
355
330
|
const random = () => [...win.crypto.getRandomValues(new Uint8Array(16))].map(n => n.toString(16).padStart(2, '0')).join(''), prefix = random();
|
|
356
331
|
const sheet = doc.createElement('style');
|
|
357
332
|
sheet.textContent = 'img[data-rapier-live-surface]{filter:opacity(0)!important}@media print{[data-rapier-html-preview],[data-rapier-object-preview],[data-rapier-playback-control]{display:none!important}img[data-rapier-live-surface]{filter:none!important}}';
|
|
@@ -414,7 +389,6 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
414
389
|
image.dispatchEvent(new win.Event('rapier-html-resize', {bubbles: true}));
|
|
415
390
|
}
|
|
416
391
|
}
|
|
417
|
-
// The control stacks just above its own object, never over the editor's toolbars, menus or notices.
|
|
418
392
|
if (row.button) {row.button.hidden = !width || !height; row.button.style.zIndex = String((parseInt(css.zIndex) || 0) + 2); row.button.style.left = box.right - origin.left + parent.scrollLeft - parent.clientLeft - 44 + 'px'; row.button.style.top = box.top - origin.top + parent.scrollTop - parent.clientTop + 'px';}
|
|
419
393
|
}
|
|
420
394
|
}
|
|
@@ -429,7 +403,6 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
429
403
|
const decoded = value.replace(/\\([0-9a-f]{1,6})\s?|\\([^\r\n])/gi, (_, hex, char) => hex ? String.fromCodePoint(Math.min(0x10ffff, parseInt(hex,16))) : char);
|
|
430
404
|
if (value.includes('\\') && /(?:url|image-set|image|src)\s*\(|@import/i.test(decoded)) throw new Error('Escaped CSS resource is unsupported');
|
|
431
405
|
if (/(?:image-set|image|src)\s*\(/i.test(decoded)) throw new Error('CSS image resources require an image element');
|
|
432
|
-
// A space keeps the token boundary: removing an import must not join image-@import x;set( into a new function.
|
|
433
406
|
return value.replace(/@import(?![\w-])[^;{}]*;?/gi, ' ').replace(/url\(\s*(?:"([^"]*)"|'([^']*)'|([^)]*))\s*\)/gi, (_, a,b,c) => {
|
|
434
407
|
const url = (a || b || c || '').trim();
|
|
435
408
|
if (/^#[A-Za-z][\w.-]*$/.test(url)) return 'url("' + url + '")';
|
|
@@ -451,14 +424,12 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
451
424
|
for (const attr of [...el.attributes]) {
|
|
452
425
|
const name = attr.name.toLowerCase(), value = attr.value;
|
|
453
426
|
if (name.startsWith('on') || name.startsWith('data-rapier-') || ['nonce','srcdoc','is','autofocus','form','formaction','action','target','download','ping','xmlns:xlink'].includes(name)) el.removeAttribute(attr.name);
|
|
454
|
-
// The local name: an SVG parsed as XML may bind XLink to any prefix (x:href).
|
|
455
427
|
if (['src','srcset','href'].includes(attr.localName.toLowerCase()) && !(name.endsWith('href') && /^#[A-Za-z][\w.-]*$/.test(value) ||
|
|
456
428
|
(name === 'src' && tag === 'img' || name.endsWith('href') && tag === 'image') && /^data:image\/(?:png|jpeg|webp|gif|jxl|svg\+xml);/i.test(value))) el.removeAttribute(attr.name);
|
|
457
429
|
}
|
|
458
430
|
if (tag.startsWith('animate') || tag === 'set') {
|
|
459
431
|
const attribute = (el.getAttribute('attributeName') || '').trim().toLowerCase().split(':').pop();
|
|
460
432
|
if (/^(?:href|base|on.*|style|src)$/.test(attribute)) {el.remove(); continue;}
|
|
461
|
-
// Event-triggered timelines have no interactive authority in this preview.
|
|
462
433
|
if (/[a-z_]\w*\./i.test(el.getAttribute('begin') || '')) el.setAttribute('begin', '0s');
|
|
463
434
|
}
|
|
464
435
|
if (tag === 'button') el.setAttribute('type', 'button');
|
|
@@ -474,6 +445,7 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
474
445
|
if (info) {pixels += info.width * info.height; if (pixels > 24000000) throw new Error('HTML image pixel budget'); admitted[url] = {width: info.width, height: info.height, animated: !!info.animated};}
|
|
475
446
|
if (match[1].toLowerCase() === 'image/svg+xml') {
|
|
476
447
|
const svgText = new win.TextDecoder('utf-8', {fatal: true}).decode(bytes);
|
|
448
|
+
if (/<!\s*(?:DOCTYPE|ENTITY)\b/i.test(svgText)) throw new Error('SVG declarations are unsupported');
|
|
477
449
|
const parsed = new win.DOMParser().parseFromString(svgText, 'image/svg+xml');
|
|
478
450
|
if (parsed.querySelector('parsererror') || parsed.documentElement.localName !== 'svg') throw new Error('Invalid SVG image');
|
|
479
451
|
const svg = doc.importNode(parsed.documentElement, true);
|
|
@@ -505,7 +477,6 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
505
477
|
const row = {image, key, parent, id: prefix + '-' + ++sequence, generation: 0, attrs: {}, visible: !visibility, paused: local.get(key) ?? reduced.matches};
|
|
506
478
|
try {
|
|
507
479
|
if (image.hasAttribute('data-rapier-html') || /^data:image\/svg\+xml;/i.test(key)) {
|
|
508
|
-
// A body over 1 MiB is refused below; refusing its base64 first keeps every synchronous decode that small.
|
|
509
480
|
if (!image.hasAttribute('data-rapier-html') && key.length > 1398200) return;
|
|
510
481
|
const value = image.hasAttribute('data-rapier-html') ? JSON.parse(decodeURIComponent(key)) : {
|
|
511
482
|
body: new win.TextDecoder('utf-8', {fatal: true}).decode(Uint8Array.from(win.atob(key.slice(key.indexOf(',') + 1)), c => c.charCodeAt(0))),
|
|
@@ -561,7 +532,6 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
561
532
|
const row = [...owned.values()].find(item => item.id === value.id && item.frame?.contentWindow === event.source);
|
|
562
533
|
if (!row || keyOf(row.image) !== row.key || !row.image.isConnected) return;
|
|
563
534
|
if (Number.isFinite(value.height) && value.height > 0 && value.height <= 4096) {
|
|
564
|
-
// A box whose own media queries answer its height turns back and forth forever: after a few turns it only grows.
|
|
565
535
|
const turn = value.height > row.measured ? 1 : value.height < row.measured ? -1 : 0;
|
|
566
536
|
if (turn && row.turn && turn !== row.turn) row.turns = (row.turns || 0) + 1;
|
|
567
537
|
if (turn) row.turn = turn;
|
|
@@ -616,10 +586,8 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
616
586
|
win.addEventListener('pagehide', hidden); win.addEventListener('pageshow', shown); if (!options.root) doc.rapierObjects = api; scan(); return api;
|
|
617
587
|
}
|
|
618
588
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
export async function materializeObjects(root, {signal, width = 640, imageInfo, poster, bestEffort = false, issues = []} = {}) {
|
|
622
|
-
const doc = root.ownerDocument, win = doc.defaultView;
|
|
589
|
+
export async function materializeObjects(root, {signal, width = 640, imageInfo, poster, bestEffort = false, issues = [], document: live} = {}) {
|
|
590
|
+
const doc = root.ownerDocument.defaultView ? root.ownerDocument : live, win = doc.defaultView;
|
|
623
591
|
const images = [...root.querySelectorAll('img[data-rapier-html],img[data-rapier-animation]')];
|
|
624
592
|
if (images.length > 64) throw new Error('Object export count budget');
|
|
625
593
|
for (const image of images) {
|
|
@@ -646,7 +614,6 @@ export async function materializeObjects(root, {signal, width = 640, imageInfo,
|
|
|
646
614
|
}
|
|
647
615
|
await api.first(clone);
|
|
648
616
|
const started = win.performance.now();
|
|
649
|
-
// A block that takes its content's height is captured at that height: a frame taken before it settled shows only its top.
|
|
650
617
|
const settles = image.hasAttribute('data-rapier-html') && (() => {const attrs = JSON.parse(decodeURIComponent(image.dataset.rapierHtml)).attrs || {};
|
|
651
618
|
return (!attrs.height || attrs.height === 'auto') && attrs.resize !== 'scale';})();
|
|
652
619
|
const first = new win.Image();
|
|
@@ -662,7 +629,6 @@ export async function materializeObjects(root, {signal, width = 640, imageInfo,
|
|
|
662
629
|
const frame = clone.dataset.rapierFirstFrame; first.src = frame;
|
|
663
630
|
await first.decode(); signal?.throwIfAborted();
|
|
664
631
|
if (!settles || !first.naturalWidth || Math.abs(clone.offsetWidth * first.naturalHeight / first.naturalWidth - clone.offsetHeight) <= 2) break;
|
|
665
|
-
// The next snapshot follows the settled viewport; wait for it.
|
|
666
632
|
if (clone.dataset.rapierFirstFrame === frame) delete clone.dataset.rapierFirstFrame;
|
|
667
633
|
}
|
|
668
634
|
if (!first.naturalWidth || !first.naturalHeight || first.naturalWidth * first.naturalHeight > 24000000) throw new Error('First-frame image budget');
|
package/dist/spec/md-assets.mjs
CHANGED
|
@@ -10,19 +10,14 @@ let configured = null, splitSource = null;
|
|
|
10
10
|
export function configureParser(parser, split) { configured = parser; splitSource = split; cached.length = 0; preparedDefinitions = []; }
|
|
11
11
|
function fail(reason) { throw new Error(reason); }
|
|
12
12
|
export const normalizeLabel = label => String(label).trim().replace(/\s+/g, ' ').toLowerCase().toUpperCase();
|
|
13
|
-
// New titles have one identity before hashing, serializing or matching a repeated append.
|
|
14
|
-
// Existing document bytes are never rewritten; CommonMark entity decoding must not change a title.
|
|
15
13
|
export const assetTitle = value => String(value || '').replace(/[\r\n]/g, ' ').replace(/\0/g, '\ufffd');
|
|
16
14
|
|
|
17
|
-
// The same hook runs in the editor, its parse worker and the agent's parser.
|
|
18
15
|
export function installMarkdownImages(md) {
|
|
19
16
|
if (md.__rapierImagesInstalled) return md;
|
|
20
17
|
md.__rapierImagesInstalled = true;
|
|
21
18
|
const validate = md.validateLink;
|
|
22
19
|
md.validateLink = url => /^data:image\/(?:jxl|svg\+xml);base64,[A-Za-z0-9+/]+={0,2}$/i.test(url) || validate(url);
|
|
23
20
|
const normalize = md.normalizeLink;
|
|
24
|
-
// An exact base64 data URL contains no URI escapes or host name to normalize.
|
|
25
|
-
// Preserve its spelling and avoid allocating one URI-normalizer item per raster byte.
|
|
26
21
|
md.normalizeLink = url => /^data:image\/(?:jxl|png|jpeg|webp|gif|svg\+xml);base64,[A-Za-z0-9+/]+={0,2}$/i.test(url) ? url : normalize(url);
|
|
27
22
|
const reference = md.block.ruler.__rules__.find(row => row.name === 'reference').fn;
|
|
28
23
|
md.block.ruler.at('reference', (state, start, end, silent) => {
|
|
@@ -74,34 +69,21 @@ function parserFor(factory = configured || globalThis.markdownit) {
|
|
|
74
69
|
export {parserFor as markdownParser};
|
|
75
70
|
export const markdownBodyOffset = source => splitSource ? splitSource(source).bodyOffset : 0;
|
|
76
71
|
|
|
77
|
-
// markdown-it's block state builds five tables with one entry per line of its whole input, and its rules index
|
|
78
|
-
// them by line. A long body gets the parser's one block pass with tables that hold a run of lines instead: a read
|
|
79
|
-
// past the lines they hold fills them further from the parser's own line scan, and they are emptied between
|
|
80
|
-
// top-level blocks. No line is parsed twice, so every token and `env` are the ones a whole-input state yields.
|
|
81
72
|
const BLOCK_WINDOW = 1024, TABLE_STEP = 128, WINDOW_END = Symbol('block window end');
|
|
82
73
|
const LINE_TABLES = ['bMarks', 'eMarks', 'tShift', 'sCount', 'bsCount'];
|
|
83
74
|
const lineIndex = key => typeof key === 'string' && /^(?:0|[1-9]\d*)$/.test(key);
|
|
84
|
-
// An array reaches its prototype only for an index it does not hold. One prototype serves every pass, so the
|
|
85
|
-
// parser's table reads see one more array shape, not one per pass.
|
|
86
75
|
let unheldLine = null;
|
|
87
76
|
const unheldLines = new Proxy(Array.prototype, {
|
|
88
77
|
get: (target, key, table) => unheldLine && lineIndex(key) ? unheldLine(table, Number(key)) : Reflect.get(target, key, table),
|
|
89
78
|
set: (target, key, value, table) => unheldLine && lineIndex(key) ? unheldLine(table, Number(key), true, value) : Reflect.set(target, key, value, table),
|
|
90
79
|
});
|
|
91
80
|
|
|
92
|
-
// The parser's block pass over source.slice(bodyOffset). `visit` receives top-level blocks as they finish:
|
|
93
|
-
// `tokens` with line numbers counted from `firstLine`, and lineStart(line), the source offset where one of those
|
|
94
|
-
// lines begins (undefined when the source ends before it). `env` is the whole pass's. The tables hold at most
|
|
95
|
-
// the lines between two visits: BLOCK_WINDOW of them, or as many as one block's rules read.
|
|
96
81
|
export function eachBlockWindow(source, bodyOffset, parser, env, visit, keepNul = false) {
|
|
97
82
|
for (const window of blockWindows(source, bodyOffset, parser, env, keepNul)) visit(window);
|
|
98
83
|
}
|
|
99
84
|
|
|
100
|
-
// The same tokenizer can pause between complete top-level windows; no grammar rule is suspended.
|
|
101
85
|
export function* blockWindows(source, bodyOffset, parser, env, keepNul = false) {
|
|
102
86
|
const lineEnds = source.slice(bodyOffset).replace(/\r\n?/g, '\n'), body = keepNul ? lineEnds : lineEnds.replace(/\0/g, '\ufffd');
|
|
103
|
-
// The body is the source with every CRLF one unit shorter: count the pairs before a body offset, from the last
|
|
104
|
-
// offset asked.
|
|
105
87
|
let pair = source.indexOf('\r\n', bodyOffset), shift = bodyOffset;
|
|
106
88
|
const sourceOffset = at => {
|
|
107
89
|
while (pair >= 0 && at + shift > pair) { shift++; pair = source.indexOf('\r\n', pair + 2); }
|
|
@@ -110,7 +92,6 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
|
|
|
110
92
|
};
|
|
111
93
|
const tokens = [];
|
|
112
94
|
let origin = 0, first = 0, state;
|
|
113
|
-
// A line begins after the newline that ends the line before it. No rule writes eMarks.
|
|
114
95
|
const report = () => ({tokens, firstLine: first, lineStart: line => {
|
|
115
96
|
if (!line) return sourceOffset(origin);
|
|
116
97
|
const end = Object.hasOwn(state.eMarks, line - 1) ? state.eMarks[line - 1] : body.length;
|
|
@@ -120,18 +101,17 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
|
|
|
120
101
|
for (let at = 0; at < body.length; at++) if (body.charCodeAt(at) === 10) lines++;
|
|
121
102
|
if (lines <= BLOCK_WINDOW) {
|
|
122
103
|
state = new parser.block.State(body, parser, env, tokens);
|
|
104
|
+
state.rapierSourceSlice = (from, to) => source.slice(sourceOffset(from), sourceOffset(to));
|
|
123
105
|
parser.block.tokenize(state, state.line, state.lineMax);
|
|
124
106
|
yield report(); return;
|
|
125
107
|
}
|
|
126
|
-
// The parser's own count of a last line that no newline ends: it does not count one of only spaces.
|
|
127
108
|
state = new parser.block.State(body.slice(body.lastIndexOf('\n') + 1), parser, env, tokens);
|
|
109
|
+
state.rapierSourceSlice = (from, to) => source.slice(sourceOffset(from), sourceOffset(to));
|
|
128
110
|
lines += state.lineMax;
|
|
129
111
|
state.src = body;
|
|
130
112
|
const tables = new WeakMap();
|
|
131
113
|
for (const name of LINE_TABLES) { state[name].length = 0; tables.set(Object.setPrototypeOf(state[name], unheldLines), name); }
|
|
132
114
|
let frontier = 0, ended = false, betweenBlocks = false;
|
|
133
|
-
// A rule may be reading a table when it grows, so the five arrays are replaced whole; an array a rule still
|
|
134
|
-
// holds is left empty, and `answer` reads and writes the state's current one in its place.
|
|
135
115
|
const fill = line => {
|
|
136
116
|
while (!ended && line >= state.bMarks.length) {
|
|
137
117
|
const held = state.bMarks.length;
|
|
@@ -142,7 +122,6 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
|
|
|
142
122
|
}
|
|
143
123
|
const scanned = new parser.block.State(body.slice(frontier, until), parser, env, []);
|
|
144
124
|
ended = until === body.length;
|
|
145
|
-
// The scan's last entry stands for the end of its input, which is the body's end only for the last scan.
|
|
146
125
|
const added = ended ? scanned.lineMax + 1 : scanned.lineMax;
|
|
147
126
|
for (const name of LINE_TABLES) {
|
|
148
127
|
const old = state[name], next = scanned[name], offset = name === 'bMarks' || name === 'eMarks' ? frontier : 0, table = [];
|
|
@@ -167,8 +146,6 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
|
|
|
167
146
|
else Object.defineProperty(current, line, {value, writable: true, enumerable: true, configurable: true});
|
|
168
147
|
return true;
|
|
169
148
|
};
|
|
170
|
-
// The top-level loop calls this between two blocks with the line it stands on. There every token is a finished
|
|
171
|
-
// block and no rule is running, so the pass can leave the tokenizer and enter it again at that line.
|
|
172
149
|
const skipEmptyLines = state.skipEmptyLines;
|
|
173
150
|
state.skipEmptyLines = function (line) {
|
|
174
151
|
if (this.level || line !== this.line || !line) return skipEmptyLines.call(this, line);
|
|
@@ -179,7 +156,6 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
|
|
|
179
156
|
return line;
|
|
180
157
|
};
|
|
181
158
|
for (;;) {
|
|
182
|
-
// Between top-level blocks the parser's loop only steps over empty lines, so a run of them is never tabled.
|
|
183
159
|
for (let at = origin; at < body.length; at++) {
|
|
184
160
|
const code = body.charCodeAt(at);
|
|
185
161
|
if (code === 10) { origin = at + 1; first++; }
|
|
@@ -213,7 +189,6 @@ export function dataImage(url) {
|
|
|
213
189
|
return {codec: match[1].toLowerCase(), byteLength, payloadStart: match[0].length - match[2].length};
|
|
214
190
|
}
|
|
215
191
|
|
|
216
|
-
// Markdown-it owns recognition, nesting, duplicate precedence and escapes; block parse only.
|
|
217
192
|
export function parseAssets(source, factory = configured || globalThis.markdownit, bodyOffset = 0) {
|
|
218
193
|
const steps = assetSteps(source, factory, bodyOffset);
|
|
219
194
|
let next = steps.next();
|
|
@@ -249,7 +224,6 @@ function* assetSteps(source, factory = configured || globalThis.markdownit, body
|
|
|
249
224
|
let end = stop;
|
|
250
225
|
if (source[end - 1] === '\n') end--;
|
|
251
226
|
if (source[end - 1] === '\r') end--;
|
|
252
|
-
// Repeated definitions are source, not additional image owners.
|
|
253
227
|
const raw = source.slice(start, end), found = raw.indexOf(definition.href);
|
|
254
228
|
const repeated = found >= 0 ? raw.indexOf(definition.href, found + 1) : -1;
|
|
255
229
|
const labelContainsUrl = definition.href.length <= id.length && id.includes(parser.utils.normalizeReference(definition.href));
|
|
@@ -277,8 +251,6 @@ function* assetSteps(source, factory = configured || globalThis.markdownit, body
|
|
|
277
251
|
}
|
|
278
252
|
export function documentAssets(source, factory) { return parseAssets(source, factory, markdownBodyOffset(source)); }
|
|
279
253
|
|
|
280
|
-
// The source worker admits these rows with the same Markdown grammar. Send positions and
|
|
281
|
-
// small facts back, then take the large picture strings from the exact request's source.
|
|
282
254
|
export function packAssetIndex(index, offset = 0) {
|
|
283
255
|
const positions = ['start', 'end', 'urlStart', 'urlEnd', 'payloadStart', 'payloadEnd'];
|
|
284
256
|
const blocks = index.blocks.map(row => {
|
|
@@ -314,8 +286,6 @@ export function restoreAssetIndex(source, packet, {offset = 0, bodyOffset = 0, f
|
|
|
314
286
|
const index = {assets, blocks, duplicateLabels: new Set(packet.duplicateLabels), appendixStart: packet.appendixStart + offset, references};
|
|
315
287
|
cached.unshift({source, factory, bodyOffset, index});
|
|
316
288
|
if (cached.length > 3) cached.pop();
|
|
317
|
-
// A top-level reference token is exactly the block the editor keeps. Its own parse has
|
|
318
|
-
// one first definition even when it followed a duplicate in the whole document.
|
|
319
289
|
preparedDefinitions = blocks.filter(row => row.topLevel).map(row => {
|
|
320
290
|
const local = {...row, start: 0, end: row.source.length, active: true, status: 'unverified'};
|
|
321
291
|
for (const key of positions.slice(2)) if (Number.isSafeInteger(local[key])) local[key] -= row.start;
|
|
@@ -327,8 +297,6 @@ export function restoreAssetIndex(source, packet, {offset = 0, bodyOffset = 0, f
|
|
|
327
297
|
return index;
|
|
328
298
|
}
|
|
329
299
|
|
|
330
|
-
// Project only parser-recognized payload spans. Their ordinary data URL remains a data URL,
|
|
331
|
-
// with every source newline intact; the parser never needs megabytes of picture bytes.
|
|
332
300
|
export function projectImageDefinitions(source, index) {
|
|
333
301
|
const records = [], parts = [];
|
|
334
302
|
let at = 0, length = 0;
|
|
@@ -350,14 +318,6 @@ export function projectImageDefinitions(source, index) {
|
|
|
350
318
|
return {source: records.length ? parts.join('') : source, records, references};
|
|
351
319
|
}
|
|
352
320
|
|
|
353
|
-
// The same index folded from the document's top-level blocks: `rows` are the blocks as markdown-it's block parse
|
|
354
|
-
// of the body cuts them, each {start, raw} with `start` absolute in `source`, and `parse(raw, row)` is parseAssets
|
|
355
|
-
// over one block (the caller may cache it by the row). A definition the whole parse finds is one a block's own parse
|
|
356
|
-
// finds at the block's place, because a top-level block's lines parse the same alone as in the document; labels,
|
|
357
|
-
// duplicates, the first definition's precedence, the count limit and the appendix fold as parseAssets folds them.
|
|
358
|
-
// Null when the rows are not this source: a block's bytes elsewhere than its start, rows out of order or overlapping,
|
|
359
|
-
// or text that could hold a definition (`]:`) between the body's start, the rows and the end, where no row parses
|
|
360
|
-
// it. Positions are the source's own, never normalized.
|
|
361
321
|
export function blockwiseAssets(source, rows, parse = raw => parseAssets(raw), bodyOffset = markdownBodyOffset(source)) {
|
|
362
322
|
if (typeof source !== 'string' || !Array.isArray(rows)) return null;
|
|
363
323
|
const blocks = [], references = Object.create(null), duplicateLabels = new Set(), seen = new Set(), assets = new Map();
|
|
@@ -368,8 +328,6 @@ export function blockwiseAssets(source, rows, parse = raw => parseAssets(raw), b
|
|
|
368
328
|
cursor = start + raw.length;
|
|
369
329
|
if (!raw.includes(']:')) continue;
|
|
370
330
|
const index = parse(raw, row);
|
|
371
|
-
// Every label the block defines, picture or not: an earlier definition of a label, of any kind, is the one
|
|
372
|
-
// the document resolves, so a later picture under it is inactive and the label a duplicate.
|
|
373
331
|
const labels = Object.keys(index.references);
|
|
374
332
|
for (const label of index.duplicateLabels) duplicateLabels.add(label);
|
|
375
333
|
for (const label of labels) {
|
|
@@ -439,7 +397,6 @@ function imageUses(source, factory, env = {}) {
|
|
|
439
397
|
for (const [raw, rows] of groups) {
|
|
440
398
|
const found = [];
|
|
441
399
|
for (let at = body.indexOf(raw, start); at >= 0 && at + raw.length <= end; at = body.indexOf(raw, at + raw.length)) found.push(at);
|
|
442
|
-
// Repeated text in code or an unmapped container is not deletion proof.
|
|
443
400
|
if (found.length !== rows.length) continue;
|
|
444
401
|
found.forEach((at, index) => images.push({...rows[index], start: originalOffset(at), end: originalOffset(at + raw.length)}));
|
|
445
402
|
}
|
|
@@ -449,7 +406,6 @@ function imageUses(source, factory, env = {}) {
|
|
|
449
406
|
return {urls, images, simple, html: html.join('\n').toUpperCase()};
|
|
450
407
|
}
|
|
451
408
|
|
|
452
|
-
// A parse failure answers true: uncertainty keeps the definition.
|
|
453
409
|
export function referenceOccurs(source, id, factory) {
|
|
454
410
|
try { return imageUses(source, factory).images.some(image => image.id === id); }
|
|
455
411
|
catch (_) { return true; }
|
|
@@ -469,11 +425,8 @@ function occurrenceChange(image, splices) {
|
|
|
469
425
|
return changed ? {start, end} : null;
|
|
470
426
|
}
|
|
471
427
|
|
|
472
|
-
// A permissive gate keeps ordinary typing out of full-document image parsing.
|
|
473
|
-
// Recognition and retirement proof still belong to Markdown-it below.
|
|
474
428
|
export function mayRetireImageDefinitions(source, splices, factory, hint) {
|
|
475
429
|
const store = typeof source === 'string' ? null : source;
|
|
476
|
-
// A definition's closing bracket is immediately followed by its colon (parseAssets uses the same necessary condition).
|
|
477
430
|
if (store ? !store.contains(']:') || !store.contains('data:image/', true)
|
|
478
431
|
: !source.includes(']:') || !/data:image\//i.test(source)) return false;
|
|
479
432
|
const original = (at, right, count) => {
|
|
@@ -488,8 +441,6 @@ export function mayRetireImageDefinitions(source, splices, factory, hint) {
|
|
|
488
441
|
if (!row.removed && !row.inserted) return false;
|
|
489
442
|
const start = original(row.pos, false, index), end = original(row.pos + row.removed.length, true, index);
|
|
490
443
|
if (store) {
|
|
491
|
-
// No image use exists without this marker. A definition appendix alone must
|
|
492
|
-
// not make a prose keystroke join the document to prove that.
|
|
493
444
|
if (!store.contains('![')) return false;
|
|
494
445
|
const from = Math.max(0, start - 8192);
|
|
495
446
|
const local = store.readSlice(from, Math.min(store.length, end + 4));
|
|
@@ -502,9 +453,6 @@ export function mayRetireImageDefinitions(source, splices, factory, hint) {
|
|
|
502
453
|
return false;
|
|
503
454
|
};
|
|
504
455
|
if (separator >= 0 || from === 0) return exclude(separator >= 0 ? from + separator + 4 : 0);
|
|
505
|
-
// Still inside a longer paragraph. Walk back in slices to the blank line.
|
|
506
|
-
// Finding an image there is the same "possible" answer as the 8KiB window;
|
|
507
|
-
// finding the break, or none, does not read the joined document.
|
|
508
456
|
let cursor = from;
|
|
509
457
|
while (cursor > 0) {
|
|
510
458
|
const sliceFrom = Math.max(0, cursor - 8192);
|
|
@@ -540,8 +488,6 @@ export function mayRetireImageDefinitions(source, splices, factory, hint) {
|
|
|
540
488
|
} catch (_) { return true; }
|
|
541
489
|
}
|
|
542
490
|
|
|
543
|
-
// Whole deletion or a valid replacement at the rebased occurrence may retire
|
|
544
|
-
// bytes. Unfinished source edits and pre-existing unused definitions do not.
|
|
545
491
|
export function retireDeletedImageDefinitions(before, after, splices, factory) {
|
|
546
492
|
if (before === after || !mayRetireImageDefinitions(before, splices, factory)) return [];
|
|
547
493
|
try {
|
|
@@ -567,8 +513,6 @@ export function retireDeletedImageDefinitions(before, after, splices, factory) {
|
|
|
567
513
|
} catch (_) { return []; }
|
|
568
514
|
}
|
|
569
515
|
|
|
570
|
-
// Disclosure stays conservative even for code examples and malformed definitions.
|
|
571
|
-
// Keep labels and readable prose; omit only the binary destination.
|
|
572
516
|
export function assetOmissions(source) {
|
|
573
517
|
if (typeof source !== 'string') return [];
|
|
574
518
|
const pattern = /data:(image\/[a-z0-9.+-]+);base64,[A-Za-z0-9+/=\\;&%#._~-]*/ig, spans = [];
|
|
@@ -594,7 +538,6 @@ export function isAssetBlock(raw) {
|
|
|
594
538
|
return /^[ \t\r\n]*$/.test(raw.slice(at));
|
|
595
539
|
}
|
|
596
540
|
|
|
597
|
-
// Mirrors engine.js _rapierEscapeImageAlt without importing the editor bundle.
|
|
598
541
|
export function escapeImageAlt(value) {
|
|
599
542
|
return String(value ?? '').replace(/[\r\n]+/g, ' ')
|
|
600
543
|
.replace(/[\\[\]*_`~^+$=<&]/g, '\\$&').replace(/\|(?=[1-9]\d{1,3}$)/, '\\|');
|
|
@@ -604,8 +547,6 @@ export function serializeAsset(asset) {
|
|
|
604
547
|
const value = assetTitle(asset.title), title = value ? ' "' + value.replace(/&/g, '&').replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"' : '';
|
|
605
548
|
return '[' + asset.label + ']: ' + asset.url + title;
|
|
606
549
|
}
|
|
607
|
-
// Pure: re-derived at commit against the moved document; appends only at the text's end.
|
|
608
|
-
// A replay supplies the retained definition's line ending instead of choosing one from later prose.
|
|
609
550
|
export function appendAssetText(source, asset, lineEnding, {preserveLabel = false} = {}) {
|
|
610
551
|
if (typeof source !== 'string' || !asset || normalizeLabel(asset.label) !== asset.id) return fail('image_source_invalid');
|
|
611
552
|
const parsed = documentAssets(source), title = assetTitle(asset.title);
|