rapier-markdown-kit 1.1.104 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +8 -4
- package/README.md +50 -7
- package/dist/images/gif.mjs +49 -0
- package/dist/images/playback.mjs +103 -0
- package/dist/kit/conformance/03-wrap-around-silhouette.md +1 -1
- package/dist/kit/conformance/04-wrap-box.md +1 -1
- package/dist/kit/conformance/05-behind.md +1 -1
- package/dist/kit/conformance/06-front.md +1 -1
- package/dist/kit/conformance/07-rotate-raster.md +1 -1
- package/dist/kit/conformance/08-drawing-rotation.md +1 -1
- package/dist/kit/conformance/09-drawing-ring-interior.expected.json +18 -18
- package/dist/kit/conformance/09-drawing-ring-interior.md +1 -1
- package/dist/kit/conformance/10-both-sides.md +1 -1
- package/dist/kit/conformance/11-neighbour-skips-image.md +1 -1
- package/dist/kit/conformance/{12-heading-barrier.expected.json → 12-rule-barrier.expected.json} +1 -1
- package/dist/kit/conformance/{12-heading-barrier.md → 12-rule-barrier.md} +2 -2
- package/dist/kit/conformance/README.md +19 -28
- package/dist/kit/conformance/run.mjs +10 -8
- package/dist/kit/ledger/authored-origin.mjs +49 -0
- package/dist/kit/ledger/authorship.mjs +18 -9
- package/dist/kit/ledger/carried.mjs +10 -5
- package/dist/kit/ledger/data.mjs +7 -4
- package/dist/kit/ledger/format.mjs +37 -11
- package/dist/kit/ledger/hash.mjs +31 -15
- package/dist/kit/ledger/history.mjs +48 -10
- package/dist/kit/ledger/inverse-source.mjs +111 -49
- package/dist/kit/ledger/journal-records.mjs +24 -29
- package/dist/kit/ledger/merge.mjs +51 -15
- package/dist/kit/ledger/text-merge.mjs +75 -29
- package/dist/kit/ledger/transport.mjs +30 -45
- package/dist/kit/pdf-vectors.mjs +6 -2
- package/dist/kit/print-capitals.mjs +0 -1
- package/dist/kit/rapier-ledger.mjs +4 -2
- package/dist/kit/render-ink.mjs +1 -1
- package/dist/kit/render-markdown.mjs +155 -31
- package/dist/kit/render-print.mjs +173 -61
- package/dist/kit/render-sanitize.mjs +25 -15
- package/dist/kit/render-styles.mjs +3 -3
- package/dist/kit/render.mjs +37 -115
- package/dist/layout/model.mjs +10 -8
- package/dist/layout/paragraph-fit.mjs +1 -15
- package/dist/layout/typography.mjs +2 -1
- package/dist/packages/rapier-diff/concerns.mjs +163 -0
- package/dist/packages/rapier-diff/diff.mjs +68 -0
- package/dist/packages/rapier-diff/index.mjs +4 -701
- package/dist/packages/rapier-diff/matching.mjs +157 -0
- package/dist/packages/rapier-diff/merge.mjs +226 -0
- package/dist/packages/rapier-diff/moves.mjs +130 -0
- package/dist/packages/rapier-diff/structure.mjs +50 -0
- package/dist/packages/rapier-diff/tokens.mjs +72 -0
- package/dist/packages/rapier-diff/walk.mjs +219 -0
- package/dist/spec/document-fonts.mjs +1 -1
- package/dist/spec/document-settings.mjs +11 -164
- package/dist/spec/html-blocks.mjs +618 -79
- package/dist/spec/md-assets.mjs +29 -6
- package/dist/spec/md-layout.mjs +9 -1
- package/dist/spec/md-marks.mjs +25 -147
- package/dist/spec/md-source.mjs +69 -26
- package/package.json +1 -1
- package/style.css +7 -51
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Paired source placement, shared by live editing and canonical-history proof.
|
|
3
3
|
import {_rapierTransformSplices, _rapierValidAuthored, _rapierHistoryEffects} from './journal-records.mjs';
|
|
4
4
|
import {projectInverseSource} from './inverse-source.mjs';
|
|
5
|
+
import {authoredBasis, authoredPlacement, authoredPlacements} from './authored-origin.mjs';
|
|
5
6
|
import {sha256} from './hash.mjs';
|
|
6
7
|
import {canonicalJSON} from './data.mjs';
|
|
7
8
|
|
|
@@ -74,6 +75,18 @@ export function transposeRecord(base, splices, id, records) {
|
|
|
74
75
|
if (next === null) throw new RangeError('canonical_transport_invalid');
|
|
75
76
|
current = next;
|
|
76
77
|
}
|
|
78
|
+
// A concurrent act that already removed these same authored units and wrote the
|
|
79
|
+
// same replacement is one physical edit. This act stays in history with no second
|
|
80
|
+
// deletion. Pure insertions are never collapsed: each one is its own unit. An
|
|
81
|
+
// inverse of that act means the replacement is not still in force.
|
|
82
|
+
const sameRows = (left, right) => left.length === right.length && left.every((row, index) =>
|
|
83
|
+
row.pos === right[index].pos && row.removed === right[index].removed && row.inserted === right[index].inserted);
|
|
84
|
+
const matched = crossed.filter(record => sameRows(record.splices, splices));
|
|
85
|
+
const targetsOf = record => record.transaction.sourceTransactionIds || (record.transaction.sourceTransactionId ? [record.transaction.sourceTransactionId] : []);
|
|
86
|
+
const withdrawn = matched.some(record => crossed.some(other => targetsOf(other).includes(record.transaction.id) ||
|
|
87
|
+
other.transaction.reverts === record.transaction.id || other.transaction.reapplies === record.transaction.id));
|
|
88
|
+
if (splices.some(row => row.removed.length) && matched.length && !withdrawn)
|
|
89
|
+
return {splices: [], sourceIndices: [], against: crossed, text: current};
|
|
77
90
|
const moved = [], sourceIndices = [];
|
|
78
91
|
for (const [sourceIndex, row] of splices.entries()) {
|
|
79
92
|
const submitted = _rapierTransformSplices(source, [row]);
|
|
@@ -120,49 +133,7 @@ export function transposeSource(base, splices, client, log) {
|
|
|
120
133
|
return transposeRecord(base, splices, client, records);
|
|
121
134
|
}
|
|
122
135
|
|
|
123
|
-
|
|
124
|
-
// and concurrent predecessors remain separate until an act observes both.
|
|
125
|
-
function authoredFrontier(records, visit = null) {
|
|
126
|
-
const dependencies = new Map(), frontier = new Set();
|
|
127
|
-
for (const record of records) {
|
|
128
|
-
const id = record.transaction.id, basis = record.authored?.basis;
|
|
129
|
-
if (visit) visit(record, basis ?? [...frontier].sort());
|
|
130
|
-
if (!basis) { dependencies.set(id, [...frontier]); frontier.clear(); }
|
|
131
|
-
else {
|
|
132
|
-
const pending = [...basis], seen = new Set();
|
|
133
|
-
while (pending.length) {
|
|
134
|
-
const prior = pending.pop();
|
|
135
|
-
if (seen.has(prior)) continue;
|
|
136
|
-
if (!dependencies.has(prior)) throw new RangeError('canonical_origin_invalid');
|
|
137
|
-
seen.add(prior); frontier.delete(prior); pending.push(...dependencies.get(prior));
|
|
138
|
-
}
|
|
139
|
-
dependencies.set(id, basis);
|
|
140
|
-
}
|
|
141
|
-
frontier.add(id);
|
|
142
|
-
}
|
|
143
|
-
return [...frontier].sort();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function authoredBasis(records) { return authoredFrontier(records); }
|
|
147
|
-
|
|
148
|
-
export function authoredPlacements(base, records) {
|
|
149
|
-
const origins = new Map(); let source = base;
|
|
150
|
-
authoredFrontier(records, (record, basis) => {
|
|
151
|
-
origins.set(record.transaction.id, record.authored ?? authoredPlacement(source, record.splices, basis));
|
|
152
|
-
source = _rapierTransformSplices(source, record.splices);
|
|
153
|
-
if (source === null) throw new RangeError('canonical_origin_invalid');
|
|
154
|
-
});
|
|
155
|
-
return origins;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Immutable authored placement stays on its own act. Basis IDs refer only to this
|
|
159
|
-
// canonical history; no source snapshot or duplicate history is stored.
|
|
160
|
-
export function authoredPlacement(source, splices, basis) {
|
|
161
|
-
const origin = {basis: basis.slice().sort(), source: sha256(source), splices: splices.map(row => ({...row}))};
|
|
162
|
-
if (!_rapierValidAuthored(origin) || _rapierTransformSplices(source, splices) === null)
|
|
163
|
-
throw new RangeError('canonical_origin_invalid');
|
|
164
|
-
return origin;
|
|
165
|
-
}
|
|
136
|
+
export {authoredBasis, authoredPlacement, authoredPlacements};
|
|
166
137
|
|
|
167
138
|
// Reconstruct an authored view from the same records, then prove its exact physical
|
|
168
139
|
// placement. Equality of resulting text alone cannot prove a repeated-text anchor.
|
|
@@ -233,6 +204,19 @@ function authoredReplay(base, records, append = null) {
|
|
|
233
204
|
if (source === null) throw new RangeError('canonical_origin_invalid');
|
|
234
205
|
}
|
|
235
206
|
}
|
|
207
|
+
const coverKey = origin => origin?.splices.some(row => row.removed.length)
|
|
208
|
+
? canonicalJSON({basis: origin.basis, splices: origin.splices}) : null;
|
|
209
|
+
const coverCounts = new Map();
|
|
210
|
+
for (const [id, origin] of origins) {
|
|
211
|
+
if (effects.references.get(id)?.length) continue;
|
|
212
|
+
const key = coverKey(origin);
|
|
213
|
+
if (key) coverCounts.set(key, (coverCounts.get(key) || 0) + 1);
|
|
214
|
+
}
|
|
215
|
+
// Two inverses can name different acts and still withdraw one physical replacement.
|
|
216
|
+
const sharesCover = id => (effects.references.get(id) || []).some(ref => {
|
|
217
|
+
const key = coverKey(origins.get(ref));
|
|
218
|
+
return !!key && coverCounts.get(key) > 1;
|
|
219
|
+
});
|
|
236
220
|
const ordered = (ids, selected = null) => {
|
|
237
221
|
const order = [], known = new Set(ids), waiting = new Map(), dependents = new Map(), ready = [];
|
|
238
222
|
const rank = id => selected ? Number(selected.has(id)) : Number(origins.get(id).splices.some(row => row.removed.length));
|
|
@@ -351,10 +335,11 @@ function authoredReplay(base, records, append = null) {
|
|
|
351
335
|
if (aligned.text !== text) throw new RangeError('canonical_origin_invalid');
|
|
352
336
|
const crossed = aligned.records.slice(basisOrder.length);
|
|
353
337
|
let moved;
|
|
354
|
-
if (
|
|
338
|
+
if (effects.references.get(id)?.length && (sharedInverses || sharesCover(id))) {
|
|
355
339
|
// Each inverse retains its own targets. First prove the exact intention
|
|
356
340
|
// in its causal source, then place that choice in the accepted effect
|
|
357
|
-
// graph. Another inverse may already suppress the same original bytes
|
|
341
|
+
// graph. Another inverse may already suppress the same original bytes,
|
|
342
|
+
// including when the two inverses name different authors of one replacement.
|
|
358
343
|
const authored = projectInverse(basis.text, basis.records, id);
|
|
359
344
|
if (!same(origin.splices, authored.splices) ||
|
|
360
345
|
_rapierTransformSplices(basis.text, origin.splices) !== authored.after)
|
package/dist/kit/pdf-vectors.mjs
CHANGED
|
@@ -132,7 +132,8 @@ export async function collectPdfVectors(main, {width, height, count, capture, si
|
|
|
132
132
|
root = document.importNode(parsed.documentElement, true);
|
|
133
133
|
root.style.setProperty('width', originalBox.width + 'px', 'important'); root.style.setProperty('height', originalBox.height + 'px', 'important');
|
|
134
134
|
mount = document.createElement('div'); mount.style.cssText = `position:fixed;left:${originalBox.left}px;top:${originalBox.top}px;width:${originalBox.width}px;height:${originalBox.height}px;pointer-events:none`;
|
|
135
|
-
|
|
135
|
+
// A shadow root keeps the picture's own <style> on the picture, as an <img> does.
|
|
136
|
+
mount.attachShadow({mode: 'open'}).append(root); document.body.append(mount); temporary.push(mount); await document.fonts.ready;
|
|
136
137
|
textRoots.push(root);
|
|
137
138
|
}
|
|
138
139
|
const box = root.getBoundingClientRect(), nodes = [root, ...root.querySelectorAll('*')], sequence = [];
|
|
@@ -151,7 +152,10 @@ export async function collectPdfVectors(main, {width, height, count, capture, si
|
|
|
151
152
|
const left = Math.max(0, bounds.left - box.left - 2), top = Math.max(0, bounds.top - box.top - 2);
|
|
152
153
|
const right = Math.min(box.width, bounds.right - box.left + 2), bottom = Math.min(box.height, bounds.bottom - box.top + 2);
|
|
153
154
|
if (!(right > left && bottom > top)) return;
|
|
154
|
-
const result = await capture({root: wrap, clip: {x: left, y: top, width: right - left, height: bottom - top}, transparent: true, scale: 2, signal, current}); check();
|
|
155
|
+
const result = await capture({root: wrap, clip: {x: left, y: top, width: right - left, height: bottom - top}, transparent: true, scale: 2, signal, current, pixels: true}); check();
|
|
156
|
+
if (result?.rgb instanceof Uint8Array && result?.alpha instanceof Uint8Array) {
|
|
157
|
+
return {image: {rgb: await compress(result.rgb), alpha: await compress(result.alpha), width: result.width, height: result.height}, x: box.left - origin.left + left, y: box.top - origin.top + top, width: right - left, height: bottom - top};
|
|
158
|
+
}
|
|
155
159
|
const image = new window.Image(); image.src = 'data:' + result.mimeType + ';base64,' + result.data; await image.decode(); check();
|
|
156
160
|
const canvas = document.createElement('canvas'); canvas.width = image.naturalWidth; canvas.height = image.naturalHeight;
|
|
157
161
|
try {
|
|
@@ -82,4 +82,3 @@ export function createPrintCapitals(lineMetrics, linesHeight, followingProse) {
|
|
|
82
82
|
|
|
83
83
|
const capitals = createPrintCapitals(lineMetrics, linesHeight, followingPrintProse);
|
|
84
84
|
export const applyPrintCapitals = capitals.apply;
|
|
85
|
-
export const clearPrintCapitals = capitals.clear;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
import {exportLedger, replaceLedgerText, readLedger, textRoot, rootAfter, historyEnvelope, agentActorId} from './ledger/format.mjs';
|
|
3
|
-
export {exportLedger, replaceLedgerText, readLedger, textRoot, rootAfter, historyEnvelope, agentActorId};
|
|
2
|
+
import {exportLedger, replaceLedgerText, readLedger, ledgerWithStart, textRoot, rootAfter, historyEnvelope, agentActorId} from './ledger/format.mjs';
|
|
3
|
+
export {exportLedger, replaceLedgerText, readLedger, ledgerWithStart, textRoot, rootAfter, historyEnvelope, agentActorId};
|
|
4
4
|
import {authorship, readAuthorship, ledgerFromAuthorship} from './ledger/authorship.mjs';
|
|
5
5
|
export {authorship, readAuthorship, ledgerFromAuthorship};
|
|
6
6
|
import {merge, transportInterval, transportTouchedInterval} from './ledger/merge.mjs';
|
|
@@ -10,6 +10,8 @@ export {_rapierTransformSplices, _rapierRecordSplices, _rapierValidLedgerRecord,
|
|
|
10
10
|
import {replayHistory, sourceBefore, historyProjection, historyProjections, historyPlaces, selectiveUndo, groupHistoryActs, pictureHistorySplices} from './ledger/history.mjs';
|
|
11
11
|
export {replayHistory, sourceBefore, historyProjection, historyProjections, historyPlaces, selectiveUndo, groupHistoryActs, pictureHistorySplices};
|
|
12
12
|
|
|
13
|
+
import {sha256State} from './ledger/hash.mjs';
|
|
14
|
+
export {sha256State};
|
|
13
15
|
import {transposeSource} from './ledger/transport.mjs';
|
|
14
16
|
export {transposeSource};
|
|
15
17
|
|
package/dist/kit/render-ink.mjs
CHANGED
|
@@ -63,7 +63,7 @@ export async function freezePublishedInk(root, {document: doc, css, fontCss = ''
|
|
|
63
63
|
frame.srcdoc = '<!doctype html><html lang="' + documentLanguage(settings, language) + '" data-rapier-theme="' + theme + '"><head><meta charset="utf-8">'
|
|
64
64
|
+ '<meta http-equiv="Content-Security-Policy" content="' + policy + '"><meta name="referrer" content="no-referrer">'
|
|
65
65
|
+ '<style>' + css + '</style></head><body class="' + (theme === 'light' ? 'light' : '')
|
|
66
|
-
+ '"><main class="rapier-page md-render" data-md-theme="' + theme + '"
|
|
66
|
+
+ '"><main class="rapier-page md-render" data-md-theme="' + theme + '">' + root.innerHTML + '</main></body></html>';
|
|
67
67
|
doc.body.appendChild(frame);
|
|
68
68
|
await wait(loaded);
|
|
69
69
|
const paper = frame.contentDocument, main = paper?.querySelector('main.rapier-page');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {htmlPlaceholder, mountHtmlBlocks, htmlImageRecord, htmlLayoutEdit} from '../spec/html-blocks.mjs';
|
|
2
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {htmlPlaceholder, mountHtmlBlocks, materializeObjects, objectPosterLabel, htmlImageRecord, htmlLayoutEdit, htmlModeEdit, mergeHtmlObjectBlocks} from '../spec/html-blocks.mjs';
|
|
3
3
|
import {finish, finishAsync, cloneTree, cleanTree, serializeTree, htmlParts, admitHtml, htmlBoundary, pause, check} from './render-work.mjs';
|
|
4
4
|
const RAPIER_TABLE_CAPTION_PREFIX_RE = /^(?:Table:|:) /;
|
|
5
5
|
const RAPIER_PORTABLE_HIGHLIGHT_HEX = Object.freeze({
|
|
@@ -72,10 +72,8 @@ function initMarkdownIt() {
|
|
|
72
72
|
'data-md-typography-close="' + md.utils.escapeHtml(encodeURIComponent(held.close)) + '"');
|
|
73
73
|
return '<span ' + attrs.join(' ') + ' style="' + md.utils.escapeHtml(css.join(';')) + '">';
|
|
74
74
|
};
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
md.renderer.rules['rapier_' + kind + '_close'] = () => '</span>';
|
|
78
|
-
}
|
|
75
|
+
md.renderer.rules.rapier_typography_open = renderTypography;
|
|
76
|
+
md.renderer.rules.rapier_typography_close = () => '</span>';
|
|
79
77
|
// Ink: the span carries its opener's body (the grammar's inkOpenBody: a sanitizer drops an attribute holding the
|
|
80
78
|
// comment's close); the ink layer reads it back through the one grammar and draws the stroke over the words' own boxes.
|
|
81
79
|
// The span itself shows nothing.
|
|
@@ -138,8 +136,8 @@ function initMarkdownIt() {
|
|
|
138
136
|
const asset = globalThis.RapierImageAssets.dataImage(src), reference = token.meta?.mdImage?.reference;
|
|
139
137
|
// A connected host's own asset; a host without one (the server) supplies no port.
|
|
140
138
|
if (!env.rapierPage && typeof globalThis.RapierEmbeddedImages?.imageHtml === 'function' &&
|
|
141
|
-
(_rapierEmbedAssetSource?.(src) || asset && (reference || asset.codec === 'image/jxl' || asset.codec === 'image/svg+xml'))) return globalThis.RapierEmbeddedImages.imageHtml(
|
|
142
|
-
reference, alt, title, size, layout, rawAlt, token.meta?.mdImage?.source, src);
|
|
139
|
+
(_rapierEmbedAssetSource?.(src) || asset && (reference || asset.codec === 'image/jxl' || asset.codec === 'image/gif' || asset.codec === 'image/svg+xml'))) return globalThis.RapierEmbeddedImages.imageHtml(
|
|
140
|
+
reference, alt, title, size, layout, rawAlt, token.meta?.mdImage?.source, src, env.rapierImagePresentation !== false);
|
|
143
141
|
const drawingAlt = typeof rawAlt === 'string' && alt === '' && asset?.codec === 'image/svg+xml' ? globalThis.RapierImageAssets.drawingImageAlt(src) : null;
|
|
144
142
|
const shownAlt = drawingAlt ?? alt;
|
|
145
143
|
let loadable = false;
|
|
@@ -158,19 +156,28 @@ function initMarkdownIt() {
|
|
|
158
156
|
return html.replace('<span ', '<span data-rapier-markdown-image="" ' +
|
|
159
157
|
(token.meta?.mdImage?.source ? 'data-rapier-image-source="' + md.utils.escapeHtml(encodeURIComponent(token.meta.mdImage.source)) + '" ' : ''));
|
|
160
158
|
}
|
|
159
|
+
let animation = null;
|
|
160
|
+
try {animation = globalThis.RapierImageAssets.imageAnimation?.(src);} catch (_) {}
|
|
161
161
|
const attrs = [
|
|
162
162
|
'data-rapier-markdown-image=""',
|
|
163
|
-
'src="' + md.utils.escapeHtml(src) + '"',
|
|
163
|
+
'src="' + md.utils.escapeHtml(animation?.first || src) + '"',
|
|
164
164
|
'alt="' + md.utils.escapeHtml(shownAlt) + '"',
|
|
165
165
|
'loading="lazy"',
|
|
166
166
|
'decoding="async"',
|
|
167
167
|
];
|
|
168
|
+
if (animation) attrs.push('data-rapier-animation="' + md.utils.escapeHtml(src) + '"',
|
|
169
|
+
'data-rapier-natural-width="' + animation.width + '"', 'data-rapier-natural-height="' + animation.height + '"');
|
|
168
170
|
if (token.meta?.mdImage?.source && token.meta.mdImage.source.length <= 4096)
|
|
169
171
|
attrs.push('data-rapier-image-source="' + md.utils.escapeHtml(encodeURIComponent(token.meta.mdImage.source)) + '"');
|
|
170
172
|
if (title) attrs.push('title="' + md.utils.escapeHtml(title) + '"');
|
|
171
173
|
if (shownAlt !== alt || size && !token.meta?.mdLayout?.layout?.width) attrs.push('data-rapier-image-alt-source="' + md.utils.escapeHtml(rawAlt) + '"');
|
|
172
174
|
if (layout) attrs.push('data-rapier-image-layout="' + md.utils.escapeHtml(encodeURIComponent(layout)) + '"');
|
|
173
|
-
|
|
175
|
+
// Portable semantic consumers keep their existing native image attributes. Only
|
|
176
|
+
// live editor/reader presentation needs provisional geometry and its load consumer.
|
|
177
|
+
if (env.rapierImagePresentation === false) return '<img ' + attrs.join(' ') + _rapierImageSizeAttributes(size, layout) + '>';
|
|
178
|
+
const geometry = !env.rapierPage && globalThis.RapierEmbeddedImages?.geometry
|
|
179
|
+
? globalThis.RapierEmbeddedImages.geometry(src) : globalThis.RapierImageAssets.imagePlaceholderDimensions(src);
|
|
180
|
+
return '<img ' + attrs.join(' ') + _rapierImageSizeAttributes(size, layout, geometry || {}) + '>';
|
|
174
181
|
};
|
|
175
182
|
|
|
176
183
|
md.renderer.rules.hr = function (tokens, idx) {
|
|
@@ -186,7 +193,9 @@ function initMarkdownIt() {
|
|
|
186
193
|
// markup reaches DOMPurify: sanitizing each HTML token closes containers before their body.
|
|
187
194
|
// The harness runs this installer alone for the parser's acceptance; without the page's sanitizer the tokens pass.
|
|
188
195
|
const inPage = typeof sanitizeRapierHtml === 'function' && typeof _rapierChromeOwnsId === 'function';
|
|
189
|
-
|
|
196
|
+
// HTML separates attributes and ends an unquoted value only at ASCII whitespace: JavaScript's \s also takes U+00A0,
|
|
197
|
+
// which would read a quoted value the browser never sees and keep the attributes hidden inside it.
|
|
198
|
+
const rawAttribute = /[\t\n\f\r ]+([A-Za-z_:][A-Za-z0-9_.:-]*)(?:[\t\n\f\r ]*=[\t\n\f\r ]*("[^"]*"|'[^']*'|[^"'=<>`\t\n\f\r ]+))?/g;
|
|
190
199
|
// The id is read as HTML reads it: a numeric character reference (`source-textarea` is `source-textarea`,
|
|
191
200
|
// `s` the same) and the named references that spell a character an id can hold are decoded before the
|
|
192
201
|
// chrome's ids are asked; every other name stays literal (no named reference spells a letter, a digit or a
|
|
@@ -196,7 +205,7 @@ function initMarkdownIt() {
|
|
|
196
205
|
equals: '=', comma: ',', lpar: '(', rpar: ')', lsqb: '[', lbrack: '[', rsqb: ']', rbrack: ']', lcub: '{', lbrace: '{',
|
|
197
206
|
rcub: '}', rbrace: '}', verbar: '|', vert: '|', VerticalLine: '|', bsol: '\\', Hat: '^', grave: '`', DiacriticalGrave: '`', Tab: '\t', NewLine: '\n' };
|
|
198
207
|
const attributeText = value => String(value || '').replace(/^["']|["']$/g, '')
|
|
199
|
-
.replace(/&(?:#(\d
|
|
208
|
+
.replace(/&(?:#(\d+)|#[xX]([0-9a-fA-F]+)|([A-Za-z][A-Za-z0-9]{0,31}));?/g, (whole, dec, hex, name) => {
|
|
200
209
|
const code = dec ? Number(dec) : hex ? parseInt(hex, 16) : -1;
|
|
201
210
|
if (code >= 0) return code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : '\ufffd';
|
|
202
211
|
return Object.hasOwn(RAW_ID_NAMED, name) ? RAW_ID_NAMED[name] : whole;
|
|
@@ -213,9 +222,15 @@ function initMarkdownIt() {
|
|
|
213
222
|
return key.startsWith('data-') || key === 'for' || key === 'id' && _rapierChromeOwnsId(attributeText(value)) ? '' : whole;
|
|
214
223
|
}) : tag;
|
|
215
224
|
md.renderer.rules.html_inline = (tokens, idx) => cleanTag(tokens[idx].content);
|
|
216
|
-
md.renderer.rules.html_block = (tokens, idx
|
|
217
|
-
tag => env?.rapierDocumentSpace ? cleanTag(tag).replace(/^<([A-Za-z][A-Za-z0-9:-]*)(?=[\s/>])/, '<$1 data-md-html-block="' + idx + '"') : cleanTag(tag), md);
|
|
225
|
+
md.renderer.rules.html_block = (tokens, idx) => _rapierRawHtmlFragment(tokens[idx].content, cleanTag, md);
|
|
218
226
|
}
|
|
227
|
+
// A highlight written in one of the picker's colours is that swatch, as `==🟢x==` is, and follows the theme.
|
|
228
|
+
const inlineHtml = md.renderer.rules.html_inline || ((tokens, idx) => tokens[idx].content);
|
|
229
|
+
md.renderer.rules.html_inline = (tokens, idx, ...rest) => {
|
|
230
|
+
const swatch = /^<mark style="background-color:(#[0-9a-f]{6});?">$/i.exec(tokens[idx].content);
|
|
231
|
+
const colour = swatch && Object.keys(RAPIER_PORTABLE_HIGHLIGHT_HEX).find(name => RAPIER_PORTABLE_HIGHLIGHT_HEX[name] === swatch[1].toLowerCase());
|
|
232
|
+
return colour ? '<mark data-rapier-highlight="' + colour + '">' : inlineHtml(tokens, idx, ...rest);
|
|
233
|
+
};
|
|
219
234
|
|
|
220
235
|
const _ulOpenDefault = md.renderer.rules.bullet_list_open
|
|
221
236
|
|| function (tokens, idx, opts, _env, self) { return self.renderToken(tokens, idx, opts); };
|
|
@@ -308,8 +323,7 @@ function initMarkdownIt() {
|
|
|
308
323
|
'</code> fence. Text below is still inside the block.</p>' + sourceHtml + '</figure>\n';
|
|
309
324
|
}
|
|
310
325
|
const provider = env.rapierProviders === false ? null : _rapierProviders.mermaid;
|
|
311
|
-
const native = globalThis.RapierFlowchart?.parseFlowchart(src).ok === true
|
|
312
|
-
provider?.status === 'ready' && globalThis.RapierDiagrams?.parseDiagram(src).ok === true;
|
|
326
|
+
const native = globalThis.RapierFlowchart?.parseFlowchart(src).ok === true;
|
|
313
327
|
const ready = native || !!(provider && provider.status === 'ready' && typeof provider.renderToString === 'function');
|
|
314
328
|
if (!ready && env.rapierProviders !== false && !_diagramPromptedOnce) {
|
|
315
329
|
_diagramPromptedOnce = true;
|
|
@@ -360,11 +374,33 @@ function initMarkdownIt() {
|
|
|
360
374
|
+ literal + '</span>';
|
|
361
375
|
}
|
|
362
376
|
try {
|
|
363
|
-
|
|
377
|
+
// The explicit output-only call keeps structured results beside the unchanged SVG
|
|
378
|
+
// islands. MathML never enters editable text projections or source serialization.
|
|
379
|
+
let rendered;
|
|
380
|
+
if (env.rapierMathEquations) {
|
|
381
|
+
const combined = mathProvider.renderWithMathML(src, {displayMode});
|
|
382
|
+
if (!combined || typeof combined.svg !== 'string' || !combined.semantics ||
|
|
383
|
+
typeof combined.semantics !== 'object' || Array.isArray(combined.semantics) || !Array.isArray(combined.errors))
|
|
384
|
+
throw new TypeError('The Rapier Math shared result is invalid.');
|
|
385
|
+
rendered = combined.svg;
|
|
386
|
+
env.rapierMathEquations.push({source, tex: src, displayMode,
|
|
387
|
+
semantics: combined.semantics, errors: combined.errors});
|
|
388
|
+
} else rendered = mathProvider.renderToString(src, {displayMode});
|
|
389
|
+
let encoded;
|
|
390
|
+
try { encoded = encodeURIComponent(source); }
|
|
391
|
+
catch (error) {
|
|
392
|
+
// An ill-formed UTF-16 source cannot enter the URI-encoded edit marker.
|
|
393
|
+
// Keep the ordinary source-only HTML and the collected refusal diagnostic.
|
|
394
|
+
if (!(error instanceof URIError)) throw error;
|
|
395
|
+
return '<span data-rapier-math-source="">' + literal + '</span>';
|
|
396
|
+
}
|
|
364
397
|
return '<span class="math-rendered" data-math-src="'
|
|
365
|
-
+
|
|
398
|
+
+ encoded + '">' + rendered + '</span>';
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
if (env.rapierMathEquations) throw error;
|
|
402
|
+
return '<span data-rapier-math-source="">' + literal + '</span>';
|
|
366
403
|
}
|
|
367
|
-
catch (_) { return '<span data-rapier-math-source="">' + literal + '</span>'; }
|
|
368
404
|
};
|
|
369
405
|
md.renderer.rules.math_inline = (tokens, idx, _opts, env) => _renderMath(tokens[idx].content, false, env, tokens[idx].meta?.mathSource);
|
|
370
406
|
md.renderer.rules.math_block = (tokens, idx, _opts, env) => wrapDisplayMath(_renderMath(tokens[idx].content, true, env, tokens[idx].meta?.mathSource)) + '\n';
|
|
@@ -393,9 +429,10 @@ function _rapierRenderSemanticRoot(canonical, metadata, options = {}) {
|
|
|
393
429
|
const markdown = _rapierSplitOpeningFrontmatter(canonical).body;
|
|
394
430
|
// Source-semantic output is independent of installed rendering providers. Complete
|
|
395
431
|
// artifacts keep the default provider path and materialize their visual content.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
432
|
+
// A portable projection keeps natural sizing. Live readers opt into the same transient
|
|
433
|
+
// AUTO policy as the editor; neither a saved page nor its source inherits that choice.
|
|
434
|
+
const env = {rapierImagePresentation: options.imagePresentation === true, ...(options.page ? {rapierPage: true} : {}), ...(options.providers === false ? {rapierProviders: false} : {})};
|
|
435
|
+
if (options.mathEquations) env.rapierMathEquations = options.mathEquations;
|
|
399
436
|
let rendered = md
|
|
400
437
|
? md.render(markdown, env)
|
|
401
438
|
: '<pre><code>' + escapeRapierHtmlText(markdown) + '</code></pre>';
|
|
@@ -430,11 +467,10 @@ async function _rapierRenderSemanticRootAsync(canonical, metadata, options = {})
|
|
|
430
467
|
const fallbackEnv = options.page ? {rapierPage: true} : {};
|
|
431
468
|
let parsed = options.parsed || (runtime.parseAsync ? await runtime.parseAsync(markdown, options) : {tokens: md.parse(markdown, fallbackEnv), env: fallbackEnv});
|
|
432
469
|
let {tokens, env} = parsed, labels = env.footnotes?.list?.map((item, index) => String(item?.label || ('inline-' + (index + 1))));
|
|
470
|
+
env = {...env, rapierImagePresentation: options.imagePresentation === true};
|
|
433
471
|
await pause(work, .3);
|
|
434
|
-
env.rapierDocumentSpace = !!globalThis.RapierMarkdownSpec?.readDocumentSettings?.(canonical)?.space;
|
|
435
472
|
const root = document.createElement('div'), part = document.createElement('div'), boundary = htmlBoundary();
|
|
436
473
|
root.setAttribute('data-rapier-semantic-root', 'true');
|
|
437
|
-
if (env.rapierDocumentSpace) root.setAttribute('data-md-space', '');
|
|
438
474
|
let html = '', began = performance.now(), complete = false;
|
|
439
475
|
const append = () => {
|
|
440
476
|
if (md.renderer.rapierTransformHtml) html = md.renderer.rapierTransformHtml(html);
|
|
@@ -653,16 +689,81 @@ function _rapierImageAltText(value, env = {}) {
|
|
|
653
689
|
return md.renderer.renderInlineAsText(tokens[0]?.children || [], md.options, env);
|
|
654
690
|
}
|
|
655
691
|
|
|
656
|
-
|
|
692
|
+
// Intrinsic facts are a presentation input, never an authored width or a resource admission.
|
|
693
|
+
// Contain both edges within the column, sharing a 1.6-column sum-of-edges budget.
|
|
694
|
+
// Reciprocal portraits/landscapes have equal weight; the native-width cap never enlarges art.
|
|
695
|
+
function _rapierImageGeometryFacts(geometry, automatic) {
|
|
696
|
+
const w = Number(geometry?.width), h = Number(geometry?.height);
|
|
697
|
+
const known = Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0 && Number.isFinite(w / h) && w / h > 0;
|
|
698
|
+
const width = known ? w : 0, height = known ? h : 0;
|
|
699
|
+
const styles = known ? [['aspect-ratio', width + '/' + height]] : automatic ? [['aspect-ratio', '1/1']] : [];
|
|
700
|
+
if (automatic) {
|
|
701
|
+
const ratio = known ? width / height : 1;
|
|
702
|
+
const share = Math.min(1, ratio, 1.6 / (1 + 1 / ratio));
|
|
703
|
+
styles.push(['--rapier-image-auto-width', Number((share * 100).toFixed(6)) + '%'],
|
|
704
|
+
['--rapier-image-natural-width', known ? width + 'px' : '100%']);
|
|
705
|
+
}
|
|
706
|
+
return {width, height, styles};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Native loads supply their current element's dimensions synchronously. Embedded images
|
|
710
|
+
// supply an admitted row only after their existing document/resource ticket checks.
|
|
711
|
+
function _rapierApplyImageGeometry(image, geometry) {
|
|
712
|
+
if (image?.tagName !== 'IMG' || image.hasAttribute('data-rapier-html')) return false;
|
|
713
|
+
const mode = image.getAttribute('data-rapier-image-geometry');
|
|
714
|
+
if (!['auto', 'sized', 'natural'].includes(mode)) return false;
|
|
715
|
+
const native = geometry === undefined;
|
|
716
|
+
if (native) {
|
|
717
|
+
if (!image.isConnected || !image.complete || image.hasAttribute('data-rapier-asset') || image.hasAttribute('data-rapier-image-url')) return false;
|
|
718
|
+
geometry = {width: image.naturalWidth, height: image.naturalHeight};
|
|
719
|
+
}
|
|
720
|
+
const facts = _rapierImageGeometryFacts(geometry, mode === 'auto');
|
|
721
|
+
if (!facts.width || !facts.height) return false;
|
|
722
|
+
const attributes = [
|
|
723
|
+
['data-rapier-natural-width', String(facts.width)], ['data-rapier-natural-height', String(facts.height)],
|
|
724
|
+
...(!image.hasAttribute('data-rapier-image-size') ? [['width', String(facts.width)]] : []), ['height', String(facts.height)],
|
|
725
|
+
].filter(([name, value]) => image.getAttribute(name) !== value);
|
|
726
|
+
const styles = facts.styles.filter(([name, value]) => (name === 'aspect-ratio'
|
|
727
|
+
? image.style.getPropertyValue(name).replace(/\s/g, '') : image.style.getPropertyValue(name)) !== value);
|
|
728
|
+
// Ordinary mutations revisit mounted images. Settled facts need no source-URL
|
|
729
|
+
// read or parse (which can be the entire inline image), nor any DOM writes.
|
|
730
|
+
if (!attributes.length && !styles.length) return false;
|
|
731
|
+
if (native) {
|
|
732
|
+
const src = image.getAttribute('src'), current = image.currentSrc;
|
|
733
|
+
if (!src || !current) return false;
|
|
734
|
+
try { if (src !== current && new URL(src, image.ownerDocument.baseURI).href !== current) return false; }
|
|
735
|
+
catch (_) { return false; }
|
|
736
|
+
}
|
|
737
|
+
for (const [name, value] of attributes) image.setAttribute(name, value);
|
|
738
|
+
for (const [name, value] of styles) image.style.setProperty(name, value);
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function _rapierImageSizeAttributes(size, marker, geometry, automatic = true) {
|
|
657
743
|
const layout = globalThis.RapierMarkdownLayout.parseLayout(marker || '');
|
|
658
744
|
// Plain (rung 0, docs/standard-adoption.md): every picture renders at its natural width, the
|
|
659
745
|
// same as a reader with no layout facts at all -- Obsidian's `![alt|N]` alt-text width (the
|
|
660
746
|
// `size` fallback below) is a different, established convention and is unaffected.
|
|
661
|
-
|
|
662
|
-
|
|
747
|
+
let style = layout && !_rapierPlainLayout() ? globalThis.RapierMarkdownLayout.imageStyle(layout) : '';
|
|
748
|
+
let attributes = '';
|
|
749
|
+
if (layout?.width != null && style) attributes = ' data-md-image-width="' + layout.width + '"';
|
|
663
750
|
// A fade rides beside any width, the alt-text's own included.
|
|
664
|
-
|
|
665
|
-
'
|
|
751
|
+
else if (size) {
|
|
752
|
+
attributes = ' width="' + size.width + '" data-rapier-image-size="' + size.width + '"';
|
|
753
|
+
style = '--md-image-width:' + size.width + 'px' + (style ? ';' + style : '');
|
|
754
|
+
}
|
|
755
|
+
if (geometry !== undefined) {
|
|
756
|
+
const mode = size || layout?.width != null || layout?.lines != null ? 'sized' : automatic ? 'auto' : 'natural';
|
|
757
|
+
const facts = _rapierImageGeometryFacts(geometry, mode === 'auto');
|
|
758
|
+
attributes += ' data-rapier-image-geometry="' + mode + '"';
|
|
759
|
+
if (facts.width) {
|
|
760
|
+
attributes += ' data-rapier-natural-width="' + facts.width + '" data-rapier-natural-height="' + facts.height + '"';
|
|
761
|
+
if (!size || !attributes.includes(' width=')) attributes += ' width="' + facts.width + '"';
|
|
762
|
+
attributes += ' height="' + facts.height + '"';
|
|
763
|
+
}
|
|
764
|
+
style = [...facts.styles.map(([name, value]) => name + ':' + value), ...(style ? [style] : [])].join(';');
|
|
765
|
+
}
|
|
766
|
+
return attributes + (style ? ' style="' + style + '"' : '');
|
|
666
767
|
}
|
|
667
768
|
|
|
668
769
|
function _rapierRawHtmlFragment(source, rewrite, parser) {
|
|
@@ -1216,7 +1317,8 @@ function* _rapierProjectPortableNodesSteps(semanticRoot, options) {
|
|
|
1216
1317
|
(name === 'data-rapier-remote-src' || name === 'data-rapier-remote-alt')) continue;
|
|
1217
1318
|
if (keepLayoutData && (name === 'data-rapier-image-layout' || name === 'data-md-layout' || name === 'data-md-align' || name === 'data-rapier-ink'
|
|
1218
1319
|
|| name === 'data-md-first-line-end' || name === 'data-md-initial-font' || name === 'data-md-initial-size'
|
|
1219
|
-
|| name === 'data-md-picture-cap-height' || name === 'data-md-picture-cap-top'
|
|
1320
|
+
|| name === 'data-md-picture-cap-height' || name === 'data-md-picture-cap-top' || name === 'data-rapier-highlight' || name === 'data-rapier-animation'
|
|
1321
|
+
|| name === 'data-rapier-list-ordinal' || name === 'data-rapier-list-delimiter' || name === 'data-md-layout-tight')) continue;
|
|
1220
1322
|
if (name === 'class' || name === 'style' || name.startsWith('data-')
|
|
1221
1323
|
|| name === 'contenteditable' || name === 'spellcheck'
|
|
1222
1324
|
|| name === 'tabindex' || name === 'disabled' || name === 'aria-disabled') {
|
|
@@ -1614,6 +1716,15 @@ function* _rapierResolveSoftBreakTokensSteps(root) {
|
|
|
1614
1716
|
return root;
|
|
1615
1717
|
}
|
|
1616
1718
|
|
|
1719
|
+
// Markup parsed in a document of its own, which runs no script and loads nothing (Copy's clipboard tree): one element per
|
|
1720
|
+
// markup, or no boxes where the joined parse did not keep them apart.
|
|
1721
|
+
function _rapierInertParse(markups) {
|
|
1722
|
+
const doc = new DOMParser().parseFromString(markups.map((html, at) => '<div data-rapier-batch="' + at + '">' + html + '</div>').join(''), 'text/html');
|
|
1723
|
+
const boxes = [...doc.body.childNodes];
|
|
1724
|
+
const intact = boxes.length === markups.length && boxes.every((box, at) => box.getAttribute?.('data-rapier-batch') === String(at));
|
|
1725
|
+
return {doc, boxes: intact ? boxes : null};
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1617
1728
|
function _rapierUnwrapElement(element) {
|
|
1618
1729
|
while (element.firstChild) element.parentNode.insertBefore(element.firstChild, element);
|
|
1619
1730
|
element.remove();
|
|
@@ -1627,8 +1738,21 @@ function _rapierBidiStrong(cp) {
|
|
|
1627
1738
|
return null;
|
|
1628
1739
|
}
|
|
1629
1740
|
|
|
1630
|
-
|
|
1741
|
+
// A consuming operation, not an editable DOM mode. Both outputs share one admitted
|
|
1742
|
+
// parse per equation; callers decide whether and how to expose the MathML to people.
|
|
1743
|
+
function renderWithMathML(source, metadata = {}) {
|
|
1744
|
+
const provider = _rapierProviders.math;
|
|
1745
|
+
if (!provider || provider.status !== 'ready' || typeof provider.renderToString !== 'function' ||
|
|
1746
|
+
typeof provider.renderWithMathML !== 'function')
|
|
1747
|
+
throw new Error('The Rapier Math shared-result renderer is unavailable.');
|
|
1748
|
+
if (!md) initMarkdownIt();
|
|
1749
|
+
const equations = [];
|
|
1750
|
+
const html = _rapierRenderSemanticRoot(source, metadata, {mathEquations: equations}).innerHTML;
|
|
1751
|
+
return {html, equations};
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
return {renderWithMathML, _rapierClipboardPayloadAsync, _rapierPlainTextFileAsync, _rapierCloneRootAsync, _rapierPortablePlainTextAsync, _rapierHeldPicturesAsWordsAsync, _rapierRenderSemanticRootAsync, _rapierProjectPortableRootAsync, _rapierPortableHtmlAsync, _rapierCalloutInfo, _rapierRemoveCalloutMarker, _rapierMathSource, _rapierFlattenPortableDetails, _rapierProjectPortableRoot, _rapierPortableHtml, _rapierNormalizeInlineText, _rapierPortableInlineText, _rapierPortableListText, _rapierPortableTableText, _rapierPortablePlainText, _rapierPlainTextFile, _rapierHeldPicturesAsWords, _rapierSourceTokenValue, _rapierResolveSoftBreakTokens, _rapierInertParse, _rapierUnwrapElement, _rapierBidiStrong, render: (source, metadata = {}) => { if (!md) initMarkdownIt(); return _rapierRenderSemanticRoot(source, metadata).innerHTML; }, initMarkdownIt, _rapierRenderSemanticRoot, _rapierInstallSemanticProbe, _rapierApplyImageGeometry, _rapierImageAltSize, _rapierImageAltSourceParts, _rapierImageAltText, _rapierImageSizeAttributes, _rapierRawHtmlFragment, _rapierApplyBlockDirection, _rapierFirstStrongDir, _rapierAssignHeadingSlugs, _rapierDisambiguateRenderedAnchors, _rapierDecodeHash, _rapierDecorateJumpLists, _rapierMarkFigureCaptions, _rapierMarkListContinuations, _rapierMarkTableCaptions, _rapierNormalizeCodeElement, _rapierLanguageClass, _relabelFootnoteHtml, _footnoteIdFromLabel, wrapDisplayMath, parser: () => md};
|
|
1631
1755
|
}
|
|
1632
1756
|
export {RAPIER_TABLE_CAPTION_PREFIX_RE as tableCaptionPrefix, RAPIER_PORTABLE_HIGHLIGHT_HEX as portableHighlightHex, _RAPIER_RTL_SCRIPT as rtlScripts, createMarkdownRenderer};
|
|
1633
1757
|
|
|
1634
|
-
export {mountHtmlBlocks, htmlImageRecord, htmlLayoutEdit};
|
|
1758
|
+
export {mountHtmlBlocks, materializeObjects, objectPosterLabel, htmlImageRecord, htmlLayoutEdit, htmlModeEdit, mergeHtmlObjectBlocks};
|