rapier-markdown-kit 1.2.8 → 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/dist/agent/will.mjs +0 -6
- package/dist/images/gif.mjs +0 -3
- package/dist/images/playback.mjs +0 -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/ledger/authored-origin.mjs +0 -5
- package/dist/kit/ledger/authorship.mjs +0 -2
- 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.mjs +0 -4
- package/dist/kit/markdown-it.mjs +0 -3
- package/dist/kit/pdf-glyphs.mjs +1 -10
- package/dist/kit/pdf-source.mjs +5 -5
- 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 +12 -117
- package/dist/kit/render-print.mjs +1 -47
- package/dist/kit/render-sanitize.mjs +7 -59
- 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/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 +4 -23
- package/dist/spec/frontmatter.mjs +0 -1
- package/dist/spec/html-blocks.mjs +0 -36
- 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 +1 -1
- package/style.css +32 -16
|
@@ -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');
|
|
@@ -506,7 +477,6 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
506
477
|
const row = {image, key, parent, id: prefix + '-' + ++sequence, generation: 0, attrs: {}, visible: !visibility, paused: local.get(key) ?? reduced.matches};
|
|
507
478
|
try {
|
|
508
479
|
if (image.hasAttribute('data-rapier-html') || /^data:image\/svg\+xml;/i.test(key)) {
|
|
509
|
-
// A body over 1 MiB is refused below; refusing its base64 first keeps every synchronous decode that small.
|
|
510
480
|
if (!image.hasAttribute('data-rapier-html') && key.length > 1398200) return;
|
|
511
481
|
const value = image.hasAttribute('data-rapier-html') ? JSON.parse(decodeURIComponent(key)) : {
|
|
512
482
|
body: new win.TextDecoder('utf-8', {fatal: true}).decode(Uint8Array.from(win.atob(key.slice(key.indexOf(',') + 1)), c => c.charCodeAt(0))),
|
|
@@ -562,7 +532,6 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
562
532
|
const row = [...owned.values()].find(item => item.id === value.id && item.frame?.contentWindow === event.source);
|
|
563
533
|
if (!row || keyOf(row.image) !== row.key || !row.image.isConnected) return;
|
|
564
534
|
if (Number.isFinite(value.height) && value.height > 0 && value.height <= 4096) {
|
|
565
|
-
// A box whose own media queries answer its height turns back and forth forever: after a few turns it only grows.
|
|
566
535
|
const turn = value.height > row.measured ? 1 : value.height < row.measured ? -1 : 0;
|
|
567
536
|
if (turn && row.turn && turn !== row.turn) row.turns = (row.turns || 0) + 1;
|
|
568
537
|
if (turn) row.turn = turn;
|
|
@@ -617,10 +586,7 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
617
586
|
win.addEventListener('pagehide', hidden); win.addEventListener('pageshow', shown); if (!options.root) doc.rapierObjects = api; scan(); return api;
|
|
618
587
|
}
|
|
619
588
|
|
|
620
|
-
// Cold export uses the same opaque renderer, independently of the visible editor
|
|
621
|
-
// and its caches. The caller owns original bytes; only detached display nodes change.
|
|
622
589
|
export async function materializeObjects(root, {signal, width = 640, imageInfo, poster, bestEffort = false, issues = [], document: live} = {}) {
|
|
623
|
-
// An export root is parsed in an inert document (no window, no body); the caller names the page whose sandboxed frames draw its objects.
|
|
624
590
|
const doc = root.ownerDocument.defaultView ? root.ownerDocument : live, win = doc.defaultView;
|
|
625
591
|
const images = [...root.querySelectorAll('img[data-rapier-html],img[data-rapier-animation]')];
|
|
626
592
|
if (images.length > 64) throw new Error('Object export count budget');
|
|
@@ -648,7 +614,6 @@ export async function materializeObjects(root, {signal, width = 640, imageInfo,
|
|
|
648
614
|
}
|
|
649
615
|
await api.first(clone);
|
|
650
616
|
const started = win.performance.now();
|
|
651
|
-
// A block that takes its content's height is captured at that height: a frame taken before it settled shows only its top.
|
|
652
617
|
const settles = image.hasAttribute('data-rapier-html') && (() => {const attrs = JSON.parse(decodeURIComponent(image.dataset.rapierHtml)).attrs || {};
|
|
653
618
|
return (!attrs.height || attrs.height === 'auto') && attrs.resize !== 'scale';})();
|
|
654
619
|
const first = new win.Image();
|
|
@@ -664,7 +629,6 @@ export async function materializeObjects(root, {signal, width = 640, imageInfo,
|
|
|
664
629
|
const frame = clone.dataset.rapierFirstFrame; first.src = frame;
|
|
665
630
|
await first.decode(); signal?.throwIfAborted();
|
|
666
631
|
if (!settles || !first.naturalWidth || Math.abs(clone.offsetWidth * first.naturalHeight / first.naturalWidth - clone.offsetHeight) <= 2) break;
|
|
667
|
-
// The next snapshot follows the settled viewport; wait for it.
|
|
668
632
|
if (clone.dataset.rapierFirstFrame === frame) delete clone.dataset.rapierFirstFrame;
|
|
669
633
|
}
|
|
670
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);
|
package/dist/spec/md-layout.mjs
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
|
|
3
|
-
// `rotate`: degrees in (-180, 180], one decimal, omitted when 0; never written for a Rapier drawing (its turn lives in its SVG).
|
|
4
|
-
// `opacity`: a picture's fade, a whole percent from 5 to 100, omitted at 100; the picture's bytes never change.
|
|
5
|
-
// `lines`: a picture or paragraph's initial letter spans 1 to 12 lines, from the first line's cap height to the Nth
|
|
6
|
-
// baseline. A picture keeps its aspect ratio; never with `width`.
|
|
7
|
-
// `first`/`indent`: signed levels of 2em, at most six decimals, in [-64,64], omitted at zero. `marker` moves a list
|
|
8
|
-
// item's marker independently of its words, on the same scale; only the item's leading paragraph carries it.
|
|
9
|
-
// `lock=on`: a picture locked in place: a tap goes to the words, never the picture; a hold takes it to unlock it. Omitted when off.
|
|
10
|
-
// `hyphens=auto`, `caps=small`, `hanging=on`: paragraph typography; absence preserves ordinary text.
|
|
11
3
|
export const fields = new Set(['align', 'width', 'lines', 'wrap', 'x', 'y', 'rotate', 'opacity', 'first', 'indent', 'marker', 'lock', 'hyphens', 'caps', 'hanging']);
|
|
12
4
|
export const alignments = new Set(['left', 'center', 'right', 'justify']);
|
|
13
|
-
// `behind`/`front`: out of flow; the paragraph lays out as though the picture were absent.
|
|
14
5
|
export const wraps = new Set(['around', 'box', 'behind', 'front']);
|
|
15
6
|
const has = (value, key) => Object.hasOwn(value, key);
|
|
16
7
|
const percent = value => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 100;
|
|
@@ -18,7 +9,6 @@ const lineCount = value => Number.isInteger(value) && value >= 1 && value <= 12;
|
|
|
18
9
|
export const INDENT_LIMIT = 64;
|
|
19
10
|
export const indentLevel = value => typeof value === 'number' && Number.isFinite(value) && value !== 0 &&
|
|
20
11
|
Math.abs(value) <= INDENT_LIMIT && Number(value.toFixed(6)) === value;
|
|
21
|
-
// A key press advances an imported measure by one level without truncating its fraction or jumping across zero.
|
|
22
12
|
export function stepIndent(value, step) {
|
|
23
13
|
const next = Number(((value || 0) + step).toFixed(6));
|
|
24
14
|
if (Math.abs(next) > INDENT_LIMIT) return value || 0;
|
|
@@ -27,7 +17,6 @@ export function stepIndent(value, step) {
|
|
|
27
17
|
const oneDecimalDegrees = value => typeof value === 'number' && Number.isFinite(value) &&
|
|
28
18
|
value > -180 && value <= 180 && Math.abs(value * 10 - Math.round(value * 10)) < 1e-9;
|
|
29
19
|
|
|
30
|
-
/** Validate values independently of their Markdown attachment context. */
|
|
31
20
|
export function validLayout(value) {
|
|
32
21
|
if (!value || typeof value !== 'object' || Array.isArray(value) ||
|
|
33
22
|
Object.keys(value).some(key => !fields.has(key))) return false;
|
|
@@ -48,7 +37,6 @@ export function validLayout(value) {
|
|
|
48
37
|
!(has(value, 'align') && (has(value, 'wrap') || has(value, 'x')));
|
|
49
38
|
}
|
|
50
39
|
|
|
51
|
-
/** Parse the closed v1 grammar. Geometry values are returned as numbers. */
|
|
52
40
|
export function parseLayout(comment) {
|
|
53
41
|
if (typeof comment !== 'string' || /[^\t\x20-\x7e]/.test(comment)) return null;
|
|
54
42
|
const match = /^<!--md-layout:v1[ \t]+([^\r\n]*?)[ \t]*-->$/.exec(comment);
|
|
@@ -65,12 +53,10 @@ export function parseLayout(comment) {
|
|
|
65
53
|
number[1] === '100' && /[1-9]/.test(number[2] || '')) return null;
|
|
66
54
|
value[key] = Number(raw.slice(0, -1));
|
|
67
55
|
} else if (key === 'y') {
|
|
68
|
-
// Negative allowed; signed like rotate, never "-0em".
|
|
69
56
|
const number = /^(-)?(0|[1-9]\d*)(?:\.\d+)?em$/.exec(raw);
|
|
70
57
|
if (!number || (number[1] && Number(raw.slice(1, -2)) === 0)) return null;
|
|
71
58
|
value.y = Number(raw.slice(0, -2));
|
|
72
59
|
} else if (key === 'rotate') {
|
|
73
|
-
// No '+', exponent, '°' or "-0deg"; 0 is unsigned.
|
|
74
60
|
const number = /^(-)?(0|[1-9]\d*)(?:\.(\d))?deg$/.exec(raw);
|
|
75
61
|
if (!number) return null;
|
|
76
62
|
const magnitude = Number(number[2] + (number[3] ? '.' + number[3] : ''));
|
|
@@ -90,16 +76,12 @@ export function parseLayout(comment) {
|
|
|
90
76
|
return validLayout(value) ? value : null;
|
|
91
77
|
}
|
|
92
78
|
|
|
93
|
-
// Comment delimiters are unsafe in sanitized attributes. Markdown stays literal;
|
|
94
|
-
// rendered attributes carry encodeURIComponent(marker), including its '%' signs.
|
|
95
79
|
export function decodeLayoutAttribute(value) {
|
|
96
80
|
try { return decodeURIComponent(value || ''); } catch (_) { return ''; }
|
|
97
81
|
}
|
|
98
82
|
|
|
99
83
|
export const parseLayoutAttribute = value => parseLayout(decodeLayoutAttribute(value));
|
|
100
84
|
|
|
101
|
-
// Indentation uses the document's body type, including on headings. Native table measurements
|
|
102
|
-
// become ordinary CSS points; an item's margin is compensated by its paragraph and nested list.
|
|
103
85
|
export function textStyle(value, {portable = false, pointSize, indentOffset = 0} = {}) {
|
|
104
86
|
if (!validLayout(value)) return '';
|
|
105
87
|
const property = {first: 'text-indent', indent: 'margin-inline-start'};
|
|
@@ -115,9 +97,6 @@ export function textStyle(value, {portable = false, pointSize, indentOffset = 0}
|
|
|
115
97
|
return css.join(';');
|
|
116
98
|
}
|
|
117
99
|
|
|
118
|
-
// Raw HTML tables carry ordinary paragraph CSS. Read the browser's parsed declarations on the same
|
|
119
|
-
// admitted scale; the native caller supplies the body's point size. A list marker belongs to its own li,
|
|
120
|
-
// never to a paragraph or a picture.
|
|
121
100
|
export function textLayout(style, {listMarker = false, pointSize, offset = 0} = {}) {
|
|
122
101
|
const value = {};
|
|
123
102
|
for (const [key, property] of (listMarker ? [['marker', 'margin-inline-start']] : [['first', 'text-indent'], ['indent', 'margin-inline-start']])) {
|
|
@@ -125,16 +104,12 @@ export function textLayout(style, {listMarker = false, pointSize, offset = 0} =
|
|
|
125
104
|
const match = /^(-?(?:0|[1-9]\d*)(?:\.\d{1,6})?)(em|pt)$/.exec(raw);
|
|
126
105
|
const level = Number(((match ? Number(match[1]) / (match[2] === 'pt' ? pointSize : 1) / 2 : 0) +
|
|
127
106
|
(key === 'first' ? 0 : offset)).toFixed(6));
|
|
128
|
-
// Public CSS may compensate an enclosing item's margin; the resolved native coordinate,
|
|
129
|
-
// rather than each intermediate declaration, is the measurement the writer must keep.
|
|
130
107
|
if (Number.isFinite(level) && level) value[key] = level;
|
|
131
108
|
}
|
|
132
109
|
if (!listMarker && style?.getPropertyValue('hyphens').trim() === 'auto') value.hyphens = 'auto';
|
|
133
110
|
return value;
|
|
134
111
|
}
|
|
135
112
|
|
|
136
|
-
// The picture's own CSS: its normal-flow width and x, its height in lines, and its fade. `1cap` is the cap height of the
|
|
137
|
-
// picture's own text; a renderer that sets the picture beside its anchor measures that paragraph's line and cap instead.
|
|
138
113
|
export function imageStyle(value) {
|
|
139
114
|
if (!validLayout(value)) return '';
|
|
140
115
|
const parts = [];
|
|
@@ -144,7 +119,6 @@ export function imageStyle(value) {
|
|
|
144
119
|
decimal(Number(Math.max(0, Math.min(100 - width, value.x - width / 2)).toFixed(6))) + '%;margin-right:0';
|
|
145
120
|
parts.push('width:' + decimal(width) + '%;height:auto' + left);
|
|
146
121
|
}
|
|
147
|
-
// Wider than the column at its own aspect, it is contained in the N-line box, never stretched.
|
|
148
122
|
if (value.lines != null) parts.push('width:auto;max-width:100%;height:' + linesHeightCss(value.lines) + ';object-fit:contain');
|
|
149
123
|
if (value.opacity != null && value.opacity < 100) parts.push('opacity:' + decimal(value.opacity / 100));
|
|
150
124
|
return parts.join(';');
|
|
@@ -152,23 +126,17 @@ export function imageStyle(value) {
|
|
|
152
126
|
|
|
153
127
|
export const linesHeightCss = lines => lines === 1 ? '1cap' : 'calc(' + (lines - 1) + ' * var(--md-line, 1.6em) + 1cap)';
|
|
154
128
|
|
|
155
|
-
// An HTML object has an outer clip box and an independent browser viewport.
|
|
156
|
-
// SCALE keeps that authored viewport; REFLOW keeps measured content height.
|
|
157
129
|
export function objectHeight(width, naturalHeight, attrs, line) {
|
|
158
130
|
if (attrs.height && attrs.height !== 'auto') return Math.min(4096, Number(attrs.height) * line);
|
|
159
131
|
const [w, h] = (attrs.viewport || '640x360').split('x').map(Number);
|
|
160
132
|
return attrs.resize === 'scale' ? Math.min(4096, width * h / w) : naturalHeight;
|
|
161
133
|
}
|
|
162
134
|
|
|
163
|
-
// Controls remain real DOM nodes while their surrounding text flows. A checkbox, link or
|
|
164
|
-
// disclosure is not a reason to exclude its entire block from picture placement.
|
|
165
135
|
export function wrapTextBlock(element) {
|
|
166
136
|
return element && /^(P|H[1-6]|UL|OL|BLOCKQUOTE|DETAILS|DL)$/.test(element.tagName) &&
|
|
167
137
|
element.textContent.trim() && !element.querySelector('img,table,pre,figure,math,.math-rendered') ? element : null;
|
|
168
138
|
}
|
|
169
139
|
|
|
170
|
-
// The one wrap-owner rule (layout/browser.js, kit/render.mjs, editor/share.js): text after, else before, skipping pictures,
|
|
171
|
-
// metadata and empty paragraphs; any other block is a barrier in that direction.
|
|
172
140
|
export function wrapNeighbour(node, classify, step = (n, direction) => n && n[direction > 0 ? 'nextElementSibling' : 'previousElementSibling']) {
|
|
173
141
|
for (const direction of [1, -1]) {
|
|
174
142
|
for (let sibling = step(node, direction); sibling; sibling = step(sibling, direction)) {
|
|
@@ -182,7 +150,6 @@ export function wrapNeighbour(node, classify, step = (n, direction) => n && n[di
|
|
|
182
150
|
return null;
|
|
183
151
|
}
|
|
184
152
|
|
|
185
|
-
// Narrower than this, a line breaker splits words letter by letter.
|
|
186
153
|
export function wrapColumnFloor(fontSize) {
|
|
187
154
|
return Math.max(40, 3.5 * (fontSize || 16));
|
|
188
155
|
}
|
|
@@ -199,7 +166,6 @@ function decimal(value) {
|
|
|
199
166
|
return digits.slice(0, point) + '.' + digits.slice(point);
|
|
200
167
|
}
|
|
201
168
|
|
|
202
|
-
/** Write only the supplied facts; no alignment is implied by absence. */
|
|
203
169
|
export function formatLayout(value) {
|
|
204
170
|
if (!validLayout(value)) throw new TypeError('invalid_markdown_layout');
|
|
205
171
|
const pairs = [];
|