rapier-markdown-kit 1.1.99 → 1.1.101
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/LICENSE +10 -2
- package/README.md +42 -8
- package/dist/kit/export-fonts.mjs +403 -0
- package/dist/kit/ledger/data.mjs +1 -1
- package/dist/kit/ledger/format.mjs +12 -5
- package/dist/kit/ledger/hash.mjs +16 -22
- package/dist/kit/ledger/history.mjs +59 -276
- package/dist/kit/ledger/inverse-source.mjs +316 -0
- package/dist/kit/ledger/journal-records.mjs +116 -12
- package/dist/kit/ledger/merge.mjs +7 -4
- package/dist/kit/ledger/transport.mjs +123 -19
- package/dist/kit/pdf-glyphs.mjs +159 -0
- package/dist/kit/pdf-vectors.mjs +204 -0
- package/dist/kit/print-capitals.mjs +85 -0
- package/dist/kit/rapier-ledger.mjs +2 -2
- package/dist/kit/render-entry.mjs +1 -0
- package/dist/kit/render-ink.mjs +132 -0
- package/dist/kit/render-markdown.mjs +71 -17
- package/dist/kit/render-print.mjs +208 -60
- package/dist/kit/render-sanitize.mjs +23 -7
- package/dist/kit/render-styles.mjs +8 -12
- package/dist/kit/render.mjs +365 -133
- package/dist/layout/model.mjs +26 -20
- package/dist/layout/typography.mjs +364 -0
- package/dist/packages/rapier-diff/LICENSE +21 -0
- package/dist/packages/rapier-diff/index.mjs +610 -0
- package/dist/spec/document-fonts.mjs +244 -0
- package/dist/spec/document-settings.mjs +494 -0
- package/dist/spec/frontmatter.mjs +30 -0
- package/dist/spec/html-blocks.mjs +143 -0
- package/dist/spec/md-assets.mjs +77 -5
- package/dist/spec/md-layout.mjs +7 -3
- package/dist/spec/md-marks.mjs +114 -8
- package/package.json +1 -1
- package/style.css +110 -52
- package/dist/agent/diff.mjs +0 -52
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Fenced HTML stays source. Its preview has no document authority.
|
|
3
|
+
import {formatLayout, imageStyle} from './md-layout.mjs';
|
|
4
|
+
|
|
5
|
+
export function htmlAttributes(info) {
|
|
6
|
+
const match = /^html(?:\s+\{([^{}]*)\})?\s*$/i.exec(String(info));
|
|
7
|
+
if (!match) return null;
|
|
8
|
+
const attrs = {};
|
|
9
|
+
for (const part of (match[1] || '').match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []) {
|
|
10
|
+
if (part[0] === '#') {if (attrs.id) return null; attrs.id = part.slice(1);}
|
|
11
|
+
else if (part[0] === '.') {if (!/^\.[A-Za-z_][\w-]*$/.test(part)) return null; attrs.class = [attrs.class, part.slice(1)].filter(Boolean).join(' ');}
|
|
12
|
+
else {
|
|
13
|
+
const pair = /^([a-z][a-z-]*)=(.*)$/.exec(part); if (!pair || Object.hasOwn(attrs, pair[1])) return null;
|
|
14
|
+
attrs[pair[1]] = pair[2].replace(/^(["'])(.*)\1$/, '$2');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (attrs.id && !/^[A-Za-z][A-Za-z0-9_-]{0,127}$/.test(attrs.id)) return null;
|
|
18
|
+
if (attrs.height && attrs.height !== 'auto' && (!/^(?:\d+(?:\.\d+)?|\.\d+)$/.test(attrs.height) || +attrs.height < .5 || +attrs.height > 512)) return null;
|
|
19
|
+
if (attrs.width && (!/^(?:\d+(?:\.\d+)?|\.\d+)%$/.test(attrs.width) || parseFloat(attrs.width) <= 0 || parseFloat(attrs.width) > 100)) return null;
|
|
20
|
+
if (attrs.float && !['left', 'right', 'none'].includes(attrs.float)) return null;
|
|
21
|
+
for (const key of ['x', 'y']) if (attrs[key] && (!Number.isFinite(+attrs[key]) || Math.abs(+attrs[key]) > 1000)) return null;
|
|
22
|
+
return attrs;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function htmlFence(raw) {
|
|
26
|
+
const opening = /^( {0,3})(`{3,}|~{3,})([^\r\n]*)\r?\n/.exec(raw);
|
|
27
|
+
if (!opening) return null;
|
|
28
|
+
const attrs = htmlAttributes(opening[3].trim()); if (!attrs) return null;
|
|
29
|
+
const mark = opening[2], close = new RegExp('^ {0,3}' + mark[0] + '{' + mark.length + ',}[ \\t]*\\r?$', 'm');
|
|
30
|
+
const rest = raw.slice(opening[0].length), found = close.exec(rest);
|
|
31
|
+
if (!found) return null;
|
|
32
|
+
const end = opening[0].length + found.index + found[0].length;
|
|
33
|
+
if (raw.slice(end).trim()) return null;
|
|
34
|
+
return {attrs, info: opening[3].trim(), body: rest.slice(0, found.index), infoStart: opening[1].length + mark.length,
|
|
35
|
+
infoEnd: opening[0].replace(/\r?\n$/, '').length, source: raw, start: 0, end: raw.length};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function htmlLayout(attrs) {
|
|
39
|
+
const width = attrs.float && attrs.float !== 'none' ? parseFloat(attrs.width || '40%') : 100;
|
|
40
|
+
return {width, ...(attrs.float && attrs.float !== 'none' ? {wrap: 'box', x: attrs.x == null ? attrs.float === 'right' ? 100 - width / 2 : width / 2 : +attrs.x, y: +(attrs.y || 0)} : {})};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function htmlImageRecord(raw) {
|
|
44
|
+
const fence = htmlFence(raw); if (!fence) return null;
|
|
45
|
+
return {...fence, htmlBlock: true, renderIndex: 0, alt: fence.attrs.id || 'HTML', altSource: '',
|
|
46
|
+
tokenEnd: raw.length, source: raw, layout: htmlLayout(fence.attrs), placement: htmlLayout(fence.attrs), standalone: true, imageOnly: true};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function htmlLayoutEdit(record, value) {
|
|
50
|
+
const fence = htmlFence(record.block.raw); if (!fence) return null;
|
|
51
|
+
const attrs = {...fence.attrs}, previous = htmlLayout(attrs), positioned = ['around', 'box', 'front', 'behind'].includes(value.wrap);
|
|
52
|
+
attrs.float = positioned ? (value.x ?? 0) > 50 ? 'right' : 'left' : 'none';
|
|
53
|
+
if (positioned) {attrs.width = Math.min(100, Math.max(.1, value.width || previous.width)) + '%'; attrs.x = value.x; attrs.y = value.y || 0;}
|
|
54
|
+
else {delete attrs.width; delete attrs.x; delete attrs.y;}
|
|
55
|
+
if (value.lines != null) attrs.height = value.lines;
|
|
56
|
+
else if (attrs.height && attrs.height !== 'auto' && value.width && value.width !== previous.width)
|
|
57
|
+
attrs.height = Math.min(512, Math.max(.5, Math.round(+attrs.height * value.width / previous.width * 10) / 10));
|
|
58
|
+
const fields = Object.entries(attrs).filter(([,v]) => v !== undefined).map(([k,v]) => k === 'id' ? '#' + v : k === 'class' ? String(v).split(/\s+/).map(name => '.' + name).join(' ') : k + '=' + (/^[\w.%+-]+$/.test(String(v)) ? v : JSON.stringify(String(v))));
|
|
59
|
+
return {start: fence.infoStart, end: fence.infoEnd, text: 'html {' + fields.join(' ') + '}'};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function htmlPlaceholder(token, escape) {
|
|
63
|
+
const attrs = htmlAttributes(token.info); if (!attrs || token.content.length > 1048576 || token.meta?.rapierFenceClosed === false) return null;
|
|
64
|
+
const layout = htmlLayout(attrs), marker = encodeURIComponent(formatLayout(layout));
|
|
65
|
+
const height = attrs.height && attrs.height !== 'auto' ? attrs.height : 8;
|
|
66
|
+
const source = encodeURIComponent(JSON.stringify({info: token.info, body: token.content, attrs}));
|
|
67
|
+
// The image is the existing placement owner's measurable box. Preview content is mounted separately.
|
|
68
|
+
const pixel = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII=';
|
|
69
|
+
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';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Self-contained so exported pages run the same policy without fetching a runtime.
|
|
73
|
+
export function mountHtmlBlocks(doc) {
|
|
74
|
+
const win = doc.defaultView, parentNonce = doc.currentScript?.nonce, owned = new Map(); let pending = 0;
|
|
75
|
+
const schedule = () => {if (!pending) pending = win.requestAnimationFrame(update);};
|
|
76
|
+
const update = () => {
|
|
77
|
+
pending = 0;
|
|
78
|
+
for (const [image, frame] of owned) {
|
|
79
|
+
if (!image.isConnected) {resize.unobserve(image); frame.remove(); owned.delete(image); continue;}
|
|
80
|
+
const box = image.getBoundingClientRect(), parent = frame.offsetParent, origin = parent?.getBoundingClientRect() || {left: 0, top: 0};
|
|
81
|
+
frame.style.left = box.left - origin.left + (parent?.scrollLeft || 0) - (parent?.clientLeft || 0) + 'px';
|
|
82
|
+
frame.style.top = box.top - origin.top + (parent?.scrollTop || 0) - (parent?.clientTop || 0) + 'px';
|
|
83
|
+
frame.style.width = box.width + 'px'; frame.style.height = box.height + 'px';
|
|
84
|
+
frame.hidden = !box.width || !box.height;
|
|
85
|
+
frame.style.zIndex = String(Math.max(1, parseInt(win.getComputedStyle(image).zIndex) || 0) + 1);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const resize = new win.ResizeObserver(schedule);
|
|
89
|
+
const install = image => {
|
|
90
|
+
if (owned.has(image)) return;
|
|
91
|
+
let value; try {value = JSON.parse(decodeURIComponent(image.getAttribute('data-rapier-html')));} catch {return;}
|
|
92
|
+
if (typeof value.body !== 'string' || value.body.length > 1048576) return;
|
|
93
|
+
const template = doc.createElement('template'); template.innerHTML = value.body;
|
|
94
|
+
const allowed = new Set('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 footer g h1 h2 h3 h4 h5 h6 header hr i 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'.toLowerCase().split(' '));
|
|
95
|
+
const walk = root => {
|
|
96
|
+
for (const el of [...root.children]) {
|
|
97
|
+
const tag = el.localName.toLowerCase();
|
|
98
|
+
if (!allowed.has(tag)) {el.remove(); continue;}
|
|
99
|
+
for (const attr of [...el.attributes]) {
|
|
100
|
+
const key = attr.name.toLowerCase();
|
|
101
|
+
if (key.startsWith('on') || ['nonce','srcdoc','is','autofocus','form','formaction','action','target','download','ping','xmlns:xlink'].includes(key)) el.removeAttribute(attr.name);
|
|
102
|
+
if (['src','srcset','href','xlink:href'].includes(key) && !(key.endsWith('href') && /^#[A-Za-z][\w.-]*$/.test(attr.value) || key === 'src' && tag === 'img' && /^data:image\/(?:png|jpeg|webp|gif|avif|svg\+xml);/i.test(attr.value))) el.removeAttribute(attr.name);
|
|
103
|
+
}
|
|
104
|
+
if (tag === 'button') el.setAttribute('type', 'button');
|
|
105
|
+
walk(el);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
walk(template.content);
|
|
109
|
+
const nonce = parentNonce || [...win.crypto.getRandomValues(new Uint8Array(16))].map(n => n.toString(16).padStart(2, '0')).join('');
|
|
110
|
+
const policy = "default-src 'none'; connect-src 'none'; img-src data:; font-src data:; media-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-" + nonce + "'; base-uri 'none'; form-action 'none'; frame-src 'none'; object-src 'none'; worker-src 'none'";
|
|
111
|
+
const frame = doc.createElement('iframe'); frame.title = value.attrs?.id || 'HTML block';
|
|
112
|
+
frame.setAttribute('sandbox', 'allow-scripts'); frame.setAttribute('referrerpolicy', 'no-referrer');
|
|
113
|
+
frame.setAttribute('allow', "camera 'none'; microphone 'none'; geolocation 'none'; clipboard-read 'none'; clipboard-write 'none'");
|
|
114
|
+
frame.setAttribute('data-rapier-html-preview', '');
|
|
115
|
+
frame.style.cssText = 'position:absolute;border:0;margin:0;padding:0;pointer-events:none;z-index:1;background:transparent;max-width:none';
|
|
116
|
+
const script = "const report=()=>parent.postMessage({type:'rapier-html-height',height:Math.ceil(document.body.getBoundingClientRect().height)},'*');new ResizeObserver(report).observe(document.body);addEventListener('load',report);report();";
|
|
117
|
+
frame.srcdoc = '<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="' + policy + '"><meta name="referrer" content="no-referrer"><style>html{color-scheme:light dark}body{margin:0;display:flow-root;overflow-wrap:anywhere}*{box-sizing:border-box}img,svg{max-width:100%}</style></head><body>' + template.innerHTML + '<script nonce="' + nonce + '">' + script + '</script></body></html>';
|
|
118
|
+
(image.closest('.block-read') || image.parentElement.parentElement).append(frame);
|
|
119
|
+
owned.set(image, frame); resize.observe(image);
|
|
120
|
+
if (!value.attrs?.height || value.attrs.height === 'auto') frame.dataset.autoHeight = 'true';
|
|
121
|
+
image.dataset.rapierNaturalWidth = String(Math.max(1, image.getBoundingClientRect().width));
|
|
122
|
+
image.dataset.rapierNaturalHeight = String(Math.max(1, image.getBoundingClientRect().height));
|
|
123
|
+
schedule();
|
|
124
|
+
};
|
|
125
|
+
const scan = () => {for (const image of doc.querySelectorAll('img[data-rapier-html]')) install(image); schedule();};
|
|
126
|
+
win.addEventListener('message', event => {
|
|
127
|
+
if (event.data?.type !== 'rapier-html-height' || !Number.isFinite(event.data.height)) return;
|
|
128
|
+
for (const [image, frame] of owned) if (frame.contentWindow === event.source && frame.dataset.autoHeight === 'true') {
|
|
129
|
+
let line = 0;
|
|
130
|
+
for (let parent = image.parentElement; parent && !line; parent = parent.parentElement)
|
|
131
|
+
line = parseFloat(win.getComputedStyle(parent).lineHeight) || 0;
|
|
132
|
+
line ||= 24;
|
|
133
|
+
const height = Math.max(line, Math.min(4096, Math.ceil(event.data.height / line) * line));
|
|
134
|
+
if (image.dataset.rapierNaturalHeight !== String(height)) {
|
|
135
|
+
image.style.height = height + 'px'; image.dataset.rapierNaturalHeight = String(height);
|
|
136
|
+
image.dispatchEvent(new win.Event('rapier-html-resize', {bubbles: true})); schedule();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
new win.MutationObserver(records => {if (records.some(row => row.type === 'childList' || row.target.matches?.('img[data-rapier-html]'))) scan();}).observe(doc.body, {subtree: true, childList: true, attributes: true, attributeFilter: ['src', 'data-rapier-html', 'data-rapier-image-gesture', 'style']});
|
|
141
|
+
win.addEventListener('resize', schedule); doc.addEventListener('scroll', schedule, true);
|
|
142
|
+
scan();
|
|
143
|
+
}
|
package/dist/spec/md-assets.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// Pure text and markdown-it tokens: no DOM, no byte decoding, no globalThis.Rapier* (MIT kit; decoders stay in images/assets.mjs).
|
|
3
3
|
|
|
4
|
-
export const IMAGE_LIMITS = Object.freeze({bytes: 16 * 1024 * 1024, sourceChars:
|
|
4
|
+
export const IMAGE_LIMITS = Object.freeze({bytes: 16 * 1024 * 1024, sourceChars: 64 * 1024 * 1024, dimension: 16384, pixels: 24000000, assets: 1024});
|
|
5
5
|
const parsers = new WeakMap();
|
|
6
6
|
const cached = [];
|
|
7
|
+
let preparedDefinitions = [];
|
|
7
8
|
let configured = null, splitSource = null;
|
|
8
|
-
export function configureParser(parser, split) { configured = parser; splitSource = split; cached.length = 0; }
|
|
9
|
+
export function configureParser(parser, split) { configured = parser; splitSource = split; cached.length = 0; preparedDefinitions = []; }
|
|
9
10
|
function fail(reason) { throw new Error(reason); }
|
|
10
11
|
export const normalizeLabel = label => String(label).trim().replace(/\s+/g, ' ').toLowerCase().toUpperCase();
|
|
11
12
|
// New titles have one identity before hashing, serializing or matching a repeated append.
|
|
@@ -18,6 +19,10 @@ export function installMarkdownImages(md) {
|
|
|
18
19
|
md.__rapierImagesInstalled = true;
|
|
19
20
|
const validate = md.validateLink;
|
|
20
21
|
md.validateLink = url => /^data:image\/(?:jxl|svg\+xml);base64,[A-Za-z0-9+/]+={0,2}$/i.test(url) || validate(url);
|
|
22
|
+
const normalize = md.normalizeLink;
|
|
23
|
+
// An exact base64 data URL contains no URI escapes or host name to normalize.
|
|
24
|
+
// Preserve its spelling and avoid allocating one URI-normalizer item per raster byte.
|
|
25
|
+
md.normalizeLink = url => /^data:image\/(?:jxl|png|jpeg|webp|svg\+xml);base64,[A-Za-z0-9+/]+={0,2}$/i.test(url) ? url : normalize(url);
|
|
21
26
|
const reference = md.block.ruler.__rules__.find(row => row.name === 'reference').fn;
|
|
22
27
|
md.block.ruler.at('reference', (state, start, end, silent) => {
|
|
23
28
|
if (silent) return reference(state, start, end, silent);
|
|
@@ -70,6 +75,8 @@ export const markdownBodyOffset = source => splitSource ? splitSource(source).bo
|
|
|
70
75
|
|
|
71
76
|
export function dataImage(url) {
|
|
72
77
|
if (typeof url !== 'string' || url.length > IMAGE_LIMITS.sourceChars) return null;
|
|
78
|
+
const prepared = preparedDefinitions.find(row => row.image.url === url)?.image;
|
|
79
|
+
if (prepared) return {codec: prepared.codec, byteLength: prepared.byteLength, payloadStart: url.indexOf(',') + 1};
|
|
73
80
|
const match = /^data:(image\/(?:jxl|png|jpeg|webp|svg\+xml));base64,([A-Za-z0-9+/]+={0,2})$/i.exec(url);
|
|
74
81
|
if (!match || match[2].length % 4) return null;
|
|
75
82
|
const padding = match[2].endsWith('==') ? 2 : match[2].endsWith('=') ? 1 : 0;
|
|
@@ -85,6 +92,8 @@ export function parseAssets(source, factory = configured || globalThis.markdowni
|
|
|
85
92
|
if (!source.includes(']:')) return {assets: new Map(), blocks: [], duplicateLabels: new Set(), appendixStart: source.length, references: Object.create(null)};
|
|
86
93
|
const prior = cached.find(row => row.source === source && row.factory === factory && row.bodyOffset === bodyOffset);
|
|
87
94
|
if (prior) return prior.index;
|
|
95
|
+
const definition = !bodyOffset && preparedDefinitions.find(row => row.source === source && row.factory === factory);
|
|
96
|
+
if (definition) return definition.index;
|
|
88
97
|
const parser = parserFor(factory), env = {}, tokens = [];
|
|
89
98
|
const starts = [bodyOffset];
|
|
90
99
|
for (let at = bodyOffset; at < source.length; at++) {
|
|
@@ -129,6 +138,56 @@ export function parseAssets(source, factory = configured || globalThis.markdowni
|
|
|
129
138
|
}
|
|
130
139
|
export function documentAssets(source, factory) { return parseAssets(source, factory, markdownBodyOffset(source)); }
|
|
131
140
|
|
|
141
|
+
// The source worker admits these rows with the same Markdown grammar. Send positions and
|
|
142
|
+
// small facts back, then take the large picture strings from the exact request's source.
|
|
143
|
+
export function packAssetIndex(index, offset = 0) {
|
|
144
|
+
const positions = ['start', 'end', 'urlStart', 'urlEnd', 'payloadStart', 'payloadEnd'];
|
|
145
|
+
const blocks = index.blocks.map(row => {
|
|
146
|
+
const packed = {...row};
|
|
147
|
+
delete packed.source;
|
|
148
|
+
if (Number.isSafeInteger(row.urlStart) && Number.isSafeInteger(row.urlEnd)) delete packed.url;
|
|
149
|
+
for (const key of positions) if (Number.isSafeInteger(packed[key])) packed[key] -= offset;
|
|
150
|
+
return packed;
|
|
151
|
+
});
|
|
152
|
+
const references = Object.create(null);
|
|
153
|
+
for (const [label, definition] of Object.entries(index.references)) {
|
|
154
|
+
const image = index.assets.get(label);
|
|
155
|
+
references[label] = image?.url === definition.href
|
|
156
|
+
? {row: index.blocks.indexOf(image), title: definition.title}
|
|
157
|
+
: {...definition};
|
|
158
|
+
}
|
|
159
|
+
return {blocks, references, duplicateLabels: [...index.duplicateLabels], appendixStart: index.appendixStart - offset};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function restoreAssetIndex(source, packet, {offset = 0, bodyOffset = 0, factory = configured || globalThis.markdownit} = {}) {
|
|
163
|
+
const positions = ['start', 'end', 'urlStart', 'urlEnd', 'payloadStart', 'payloadEnd'];
|
|
164
|
+
const blocks = packet.blocks.map(packed => {
|
|
165
|
+
const row = {...packed};
|
|
166
|
+
for (const key of positions) if (Number.isSafeInteger(row[key])) row[key] += offset;
|
|
167
|
+
row.source = source.slice(row.start, row.end);
|
|
168
|
+
if (Number.isSafeInteger(row.urlStart) && Number.isSafeInteger(row.urlEnd)) row.url = source.slice(row.urlStart, row.urlEnd);
|
|
169
|
+
return row;
|
|
170
|
+
});
|
|
171
|
+
const references = Object.create(null), assets = new Map();
|
|
172
|
+
for (const row of blocks) if (row.active) assets.set(row.id, row);
|
|
173
|
+
for (const [label, definition] of Object.entries(packet.references)) references[label] = Number.isSafeInteger(definition.row)
|
|
174
|
+
? {href: blocks[definition.row].url, title: definition.title} : {...definition};
|
|
175
|
+
const index = {assets, blocks, duplicateLabels: new Set(packet.duplicateLabels), appendixStart: packet.appendixStart + offset, references};
|
|
176
|
+
cached.unshift({source, factory, bodyOffset, index});
|
|
177
|
+
if (cached.length > 3) cached.pop();
|
|
178
|
+
// A top-level reference token is exactly the block the editor keeps. Its own parse has
|
|
179
|
+
// one first definition even when it followed a duplicate in the whole document.
|
|
180
|
+
preparedDefinitions = blocks.filter(row => row.topLevel).map(row => {
|
|
181
|
+
const local = {...row, start: 0, end: row.source.length, active: true, status: 'unverified'};
|
|
182
|
+
for (const key of positions.slice(2)) if (Number.isSafeInteger(local[key])) local[key] -= row.start;
|
|
183
|
+
const image = Object.freeze({url: row.url, codec: row.codec, byteLength: row.byteLength});
|
|
184
|
+
return {source: row.source, factory, image, index: {assets: new Map([[row.id, local]]), blocks: [local],
|
|
185
|
+
references: Object.assign(Object.create(null), {[row.id]: {href: row.url, title: row.title}}),
|
|
186
|
+
duplicateLabels: new Set(), appendixStart: 0}};
|
|
187
|
+
});
|
|
188
|
+
return index;
|
|
189
|
+
}
|
|
190
|
+
|
|
132
191
|
// Project only parser-recognized payload spans. Their ordinary data URL remains a data URL,
|
|
133
192
|
// with every source newline intact; the parser never needs megabytes of picture bytes.
|
|
134
193
|
export function projectImageDefinitions(source, index) {
|
|
@@ -145,8 +204,8 @@ export function projectImageDefinitions(source, index) {
|
|
|
145
204
|
parts.push(source.slice(at));
|
|
146
205
|
const references = Object.create(null);
|
|
147
206
|
for (const [label, definition] of Object.entries(index.references)) {
|
|
148
|
-
const image =
|
|
149
|
-
references[label] = {href: image
|
|
207
|
+
const image = index.assets.get(label);
|
|
208
|
+
references[label] = {href: image?.url === definition.href ? 'data:' + image.codec + ';base64,AA==' : definition.href,
|
|
150
209
|
title: definition.title};
|
|
151
210
|
}
|
|
152
211
|
return {source: records.length ? parts.join('') : source, records, references};
|
|
@@ -274,7 +333,10 @@ function occurrenceChange(image, splices) {
|
|
|
274
333
|
// A permissive gate keeps ordinary typing out of full-document image parsing.
|
|
275
334
|
// Recognition and retirement proof still belong to Markdown-it below.
|
|
276
335
|
export function mayRetireImageDefinitions(source, splices, factory) {
|
|
277
|
-
|
|
336
|
+
const store = typeof source === 'string' ? null : source;
|
|
337
|
+
// A definition's closing bracket is immediately followed by its colon (parseAssets uses the same necessary condition).
|
|
338
|
+
if (store ? !store.contains(']:') || !store.contains('data:image/', true)
|
|
339
|
+
: !source.includes(']:') || !/data:image\//i.test(source)) return false;
|
|
278
340
|
const original = (at, right, count) => {
|
|
279
341
|
for (let index = count - 1; index >= 0; index--) {
|
|
280
342
|
const row = splices[index], end = row.pos + row.inserted.length;
|
|
@@ -286,10 +348,20 @@ export function mayRetireImageDefinitions(source, splices, factory) {
|
|
|
286
348
|
const possible = splices.some((row, index) => {
|
|
287
349
|
if (!row.removed && !row.inserted) return false;
|
|
288
350
|
const start = original(row.pos, false, index), end = original(row.pos + row.removed.length, true, index);
|
|
351
|
+
if (store) {
|
|
352
|
+
const from = Math.max(0, start - 8192);
|
|
353
|
+
const local = store.readSlice(from, Math.min(store.length, end + 4));
|
|
354
|
+
const image = local.lastIndexOf('![', end - from);
|
|
355
|
+
const separator = Math.max(local.lastIndexOf('\n\n', start - from),
|
|
356
|
+
local.lastIndexOf('\r\n\r\n', start - from), local.lastIndexOf('\r\r', start - from));
|
|
357
|
+
if (image >= 0) return image + from >= start || separator < image;
|
|
358
|
+
return from > 0 && separator < 0;
|
|
359
|
+
}
|
|
289
360
|
const image = source.lastIndexOf('![', end);
|
|
290
361
|
return image >= 0 && (image >= start || Math.max(source.lastIndexOf('\n\n', start),
|
|
291
362
|
source.lastIndexOf('\r\n\r\n', start), source.lastIndexOf('\r\r', start)) < image);
|
|
292
363
|
});
|
|
364
|
+
if (store) return possible && mayRetireImageDefinitions(store.read(), splices, factory);
|
|
293
365
|
if (!possible || splices.length !== 1) return possible;
|
|
294
366
|
const row = splices[0];
|
|
295
367
|
if (row.removed.includes('![') || /[\r\n]/.test(row.removed) || /[\r\n]/.test(row.inserted)) return true;
|
package/dist/spec/md-layout.mjs
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
// `rotate`: degrees in (-180, 180], one decimal, omitted when 0; never written for a Rapier drawing (its turn lives in its SVG).
|
|
4
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's
|
|
6
|
-
//
|
|
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
7
|
// `first`/`indent`: signed levels of 2em, at most six decimals, in [-64,64], omitted at zero. `marker` moves a list
|
|
8
8
|
// item's marker independently of its words, on the same scale; only the item's leading paragraph carries it.
|
|
9
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
|
-
|
|
10
|
+
// `hyphens=auto`, `caps=small`, `hanging=on`: paragraph typography; absence preserves ordinary text.
|
|
11
|
+
export const fields = new Set(['align', 'width', 'lines', 'wrap', 'x', 'y', 'rotate', 'opacity', 'first', 'indent', 'marker', 'lock', 'hyphens', 'caps', 'hanging']);
|
|
11
12
|
export const alignments = new Set(['left', 'center', 'right', 'justify']);
|
|
12
13
|
// `behind`/`front`: out of flow; the paragraph lays out as though the picture were absent.
|
|
13
14
|
export const wraps = new Set(['around', 'box', 'behind', 'front']);
|
|
@@ -41,6 +42,9 @@ export function validLayout(value) {
|
|
|
41
42
|
(!has(value, 'first') || indentLevel(value.first)) && (!has(value, 'indent') || indentLevel(value.indent)) &&
|
|
42
43
|
(!has(value, 'marker') || indentLevel(value.marker)) &&
|
|
43
44
|
(!has(value, 'lock') || value.lock === 'on') &&
|
|
45
|
+
(!has(value, 'hyphens') || value.hyphens === 'auto') &&
|
|
46
|
+
(!has(value, 'caps') || value.caps === 'small') &&
|
|
47
|
+
(!has(value, 'hanging') || value.hanging === 'on') &&
|
|
44
48
|
!(has(value, 'align') && (has(value, 'wrap') || has(value, 'x')));
|
|
45
49
|
}
|
|
46
50
|
|
package/dist/spec/md-marks.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// The one
|
|
2
|
+
// The one owner of colour, font, ink and page-break markers.
|
|
3
3
|
|
|
4
4
|
// The exported page retains this reader alone. The editor uses the same closure, then adds its writers and scans.
|
|
5
5
|
const inkReader = () => {
|
|
@@ -134,6 +134,81 @@ export function hasColorMarker(text) {
|
|
|
134
134
|
return new RegExp('<!--(?:c ' + COLOR_VALUE + '|/c)-->').test(String(text == null ? '' : text));
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
export const FONT_CLOSE = '<!--/f-->';
|
|
138
|
+
export const FONT_STACKS = Object.freeze({
|
|
139
|
+
sans: Object.freeze({stack: 'sans', css: '', word: ''}),
|
|
140
|
+
serif: Object.freeze({stack: 'serif', css: "Georgia,'Palatino Linotype',Palatino,'Times New Roman',Times,serif", word: 'Georgia'}),
|
|
141
|
+
mono: Object.freeze({stack: 'mono', css: "ui-monospace,'Courier New',Courier,monospace", word: 'Courier New'}),
|
|
142
|
+
system: Object.freeze({stack: 'system', css: 'system-ui,sans-serif', word: 'Calibri'}),
|
|
143
|
+
hand: Object.freeze({stack: 'hand', css: 'cursive', word: 'Segoe Print'}),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// A family is one font-table name, never a CSS list or an HTML fragment. These readers are also
|
|
147
|
+
// relocated into the parse worker with FONT_CLOSE and their original function names.
|
|
148
|
+
export function normalizeFontFamily(value) {
|
|
149
|
+
if (typeof value !== 'string') return null;
|
|
150
|
+
const family = value.trim();
|
|
151
|
+
return family && family.length <= 256 && family.isWellFormed() && !/[\u0000-\u001f\u007f<>\u2028\u2029]/.test(family) && !family.includes('--') ? family : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function fontFamilyCss(value) {
|
|
155
|
+
const family = normalizeFontFamily(value);
|
|
156
|
+
if (family == null) return '';
|
|
157
|
+
const generic = Object.hasOwn(FONT_STACKS, family.toLowerCase()) ? FONT_STACKS[family.toLowerCase()] : null;
|
|
158
|
+
return generic ? generic.css || "'Geist',system-ui,sans-serif" : '"' + family.replace(/[\\"]/g, '\\$&') + '"';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function formatFontOpen(family) {
|
|
162
|
+
const name = normalizeFontFamily(family);
|
|
163
|
+
if (!name) throw new TypeError('font: use one family name');
|
|
164
|
+
return '<!--f ' + name + '-->';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function formatFontRun(family, content) {
|
|
168
|
+
return formatFontOpen(family) + content + FONT_CLOSE;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function matchFontOpen(text) {
|
|
172
|
+
if (typeof text !== 'string') return null;
|
|
173
|
+
const match = /^<!--f ([^<>\r\n]{1,256})-->/.exec(text);
|
|
174
|
+
const family = match && normalizeFontFamily(match[1]);
|
|
175
|
+
return family ? {family, length: match[0].length} : null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function parseFontOpen(comment) {
|
|
179
|
+
const match = matchFontOpen(comment);
|
|
180
|
+
return match && match.length === comment.length ? match.family : null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function isFontClose(comment) {
|
|
184
|
+
return comment === FONT_CLOSE;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function scanFontMarkers(source) {
|
|
188
|
+
const text = String(source ?? ''), markers = [];
|
|
189
|
+
const scan = /<!--(?:f [^<>\r\n]{1,256}|\/f)-->/g;
|
|
190
|
+
for (const match of text.matchAll(scan)) {
|
|
191
|
+
const family = match[0] === FONT_CLOSE ? null : parseFontOpen(match[0]);
|
|
192
|
+
if (family || match[0] === FONT_CLOSE) markers.push({start: match.index, end: match.index + match[0].length, family});
|
|
193
|
+
}
|
|
194
|
+
return markers;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function pairFontMarkers(source, markers = null) {
|
|
198
|
+
return pairMarkers(source, 'font', markers);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function stripFontMarkers(source) {
|
|
202
|
+
const text = String(source ?? '');
|
|
203
|
+
let result = '', at = 0;
|
|
204
|
+
for (const marker of scanFontMarkers(text)) { result += text.slice(at, marker.start); at = marker.end; }
|
|
205
|
+
return result + text.slice(at);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function hasFontMarker(source) {
|
|
209
|
+
return scanFontMarkers(source).length > 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
137
212
|
const INK_CLOSE = '<!--/ink-->';
|
|
138
213
|
export { INK_KINDS, INK_CLOSE, INK_PATH_MAX, INK_INT_MAX, INK_OPEN_PATTERN };
|
|
139
214
|
|
|
@@ -211,6 +286,37 @@ export function isInkClose(comment) {
|
|
|
211
286
|
|
|
212
287
|
// Every ink marker in order: {start, end, mark} for an opener (mark null for a closer, and an opener whose path
|
|
213
288
|
// overflows is not a marker at all).
|
|
289
|
+
// Without a prior arrow or adjacent marker bytes, only an edit boundary can empty a span.
|
|
290
|
+
// Read those boundaries through the planned rows before asking the full grammar to retire ink.
|
|
291
|
+
export function mayRetireInk(source, rows) {
|
|
292
|
+
if (source.contains('<!--ink arrow ') || source.contains('<!--ink end ') || source.contains('--><!--')) return true;
|
|
293
|
+
let pieces = [{from: 0, length: source.length}], length = source.length;
|
|
294
|
+
const slice = (start, end) => {
|
|
295
|
+
const selected = [];
|
|
296
|
+
let at = 0;
|
|
297
|
+
for (const piece of pieces) {
|
|
298
|
+
const low = Math.max(start, at), high = Math.min(end, at + piece.length);
|
|
299
|
+
if (high > low) selected.push(piece.text === undefined
|
|
300
|
+
? {from: piece.from + low - at, length: high - low}
|
|
301
|
+
: {text: piece.text.slice(low - at, high - at), length: high - low});
|
|
302
|
+
at += piece.length;
|
|
303
|
+
if (at >= end) break;
|
|
304
|
+
}
|
|
305
|
+
return selected;
|
|
306
|
+
};
|
|
307
|
+
const read = (start, end) => slice(start, end).map(piece => piece.text === undefined
|
|
308
|
+
? source.readSlice(piece.from, piece.from + piece.length) : piece.text).join('');
|
|
309
|
+
for (const row of rows) {
|
|
310
|
+
const end = row.pos + row.removed.length;
|
|
311
|
+
if (row.pos < 0 || end > length) return true;
|
|
312
|
+
const local = read(Math.max(0, row.pos - 6), row.pos) + row.inserted + read(end, Math.min(length, end + 6));
|
|
313
|
+
if (local.includes('--><!--')) return true;
|
|
314
|
+
pieces = [...slice(0, row.pos), {text: row.inserted, length: row.inserted.length}, ...slice(end, length)];
|
|
315
|
+
length += row.inserted.length - row.removed.length;
|
|
316
|
+
}
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
214
320
|
export function scanInkMarkers(source) {
|
|
215
321
|
const text = String(source == null ? '' : source);
|
|
216
322
|
const scan = new RegExp(INK_OPEN_PATTERN + '|' + INK_CLOSE.replace(/[-]/g, '\\-'), 'g');
|
|
@@ -236,7 +342,7 @@ export function pairInkSpans(source, markers = null) {
|
|
|
236
342
|
return pairSpanMarkers(source, 'ink', markers);
|
|
237
343
|
}
|
|
238
344
|
|
|
239
|
-
// Colour
|
|
345
|
+
// Colour and font keep their first opener; ink pairs each closer with the innermost opener. The shared range
|
|
240
346
|
// shape lets custody owners keep every independent stroke, including strokes on identical words.
|
|
241
347
|
export function pairMarkers(source, kind, markers = null) {
|
|
242
348
|
const paired = pairSpanMarkers(source, kind, markers);
|
|
@@ -483,19 +589,19 @@ export function quotedWillSpan(source, spans) {
|
|
|
483
589
|
}
|
|
484
590
|
|
|
485
591
|
function pairSpanMarkers(source, kind, markers = null) {
|
|
486
|
-
if (kind !== 'ink' && kind !== 'color') throw new TypeError('Unknown paired marker kind');
|
|
487
|
-
const color = kind === 'color';
|
|
592
|
+
if (kind !== 'ink' && kind !== 'color' && kind !== 'font') throw new TypeError('Unknown paired marker kind');
|
|
593
|
+
const color = kind === 'color', font = kind === 'font';
|
|
488
594
|
const runs = [], strays = [], stack = [];
|
|
489
|
-
for (const marker of markers || (color ? scanColorMarkers(source) : scanInkMarkers(source))) {
|
|
490
|
-
if (color ? marker.hex !== null : !marker.close) {
|
|
491
|
-
if (color && stack.length) { strays.push(marker); continue; }
|
|
595
|
+
for (const marker of markers || (color ? scanColorMarkers(source) : font ? scanFontMarkers(source) : scanInkMarkers(source))) {
|
|
596
|
+
if (color ? marker.hex !== null : font ? marker.family !== null : !marker.close) {
|
|
597
|
+
if ((color || font) && stack.length) { strays.push(marker); continue; }
|
|
492
598
|
stack.push(marker);
|
|
493
599
|
continue;
|
|
494
600
|
}
|
|
495
601
|
const open = stack.pop();
|
|
496
602
|
if (!open) { strays.push(marker); continue; }
|
|
497
603
|
runs.push({ start: open.start, end: marker.end, innerStart: open.end, innerEnd: marker.start,
|
|
498
|
-
...(color ? { hex: open.hex } : { mark: open.mark }) });
|
|
604
|
+
...(color ? { hex: open.hex } : font ? {family: open.family} : { mark: open.mark }) });
|
|
499
605
|
}
|
|
500
606
|
strays.push(...stack);
|
|
501
607
|
runs.sort((a, b) => a.start - b.start);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-markdown-kit",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.101",
|
|
4
4
|
"description": "Read, render and write Self-contained Markdown: one .md file that carries its pictures, drawings and layout, and that any Markdown app can read. HTML renderer, stylesheet, text layout, Will/1 agent permissions, and an edit history with authorship and offline merge. MIT, no dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|