rapier-markdown-kit 1.2.7 → 1.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -30
- package/dist/agent/will.mjs +1 -6
- package/dist/images/gif.mjs +0 -3
- package/dist/images/playback.mjs +1 -10
- package/dist/kit/assets.mjs +0 -1
- package/dist/kit/conformance/measurer.mjs +0 -0
- package/dist/kit/conformance/run.mjs +1 -8
- package/dist/kit/export-fonts.mjs +0 -14
- package/dist/kit/index.mjs +1 -1
- package/dist/kit/ledger/authored-origin.mjs +0 -5
- package/dist/kit/ledger/authorship.mjs +0 -2
- package/dist/kit/ledger/carried.d.mts +20 -0
- package/dist/kit/ledger/carried.mjs +4 -7
- package/dist/kit/ledger/data.mjs +0 -8
- package/dist/kit/ledger/format.mjs +0 -25
- package/dist/kit/ledger/hash.mjs +11 -7
- package/dist/kit/ledger/history.mjs +10 -29
- package/dist/kit/ledger/inverse-source.mjs +0 -46
- package/dist/kit/ledger/journal-records.mjs +24 -56
- package/dist/kit/ledger/merge.mjs +0 -27
- package/dist/kit/ledger/text-merge.mjs +0 -20
- package/dist/kit/ledger/transport.mjs +0 -47
- package/dist/kit/ledger.d.mts +88 -0
- package/dist/kit/ledger.mjs +14 -0
- package/dist/kit/markdown-it.d.mts +4 -0
- package/dist/kit/markdown-it.mjs +36 -0
- package/dist/kit/pdf-glyphs.mjs +39 -45
- package/dist/kit/pdf-source.d.mts +5 -0
- package/dist/kit/pdf-source.mjs +125 -0
- package/dist/kit/pdf-vectors.mjs +0 -3
- package/dist/kit/print-capitals.mjs +0 -4
- package/dist/kit/render-entry.mjs +0 -1
- package/dist/kit/render-ink.mjs +0 -5
- package/dist/kit/render-markdown.mjs +15 -119
- package/dist/kit/render-print.mjs +4 -49
- package/dist/kit/render-sanitize.mjs +22 -58
- package/dist/kit/render-styles.mjs +0 -7
- package/dist/kit/render-work.mjs +0 -12
- package/dist/kit/render.mjs +18 -173
- package/dist/kit/will.d.mts +42 -0
- package/dist/layout/hyphenation.mjs +0 -23
- package/dist/layout/line-plan.mjs +0 -20
- package/dist/layout/model.mjs +0 -55
- package/dist/layout/paragraph-fit.mjs +50 -60
- package/dist/layout/preparation.mjs +23 -56
- package/dist/layout/prepared-paragraph.mjs +0 -25
- package/dist/layout/typography.mjs +103 -41
- package/dist/packages/rapier-diff/concerns.mjs +0 -16
- package/dist/packages/rapier-diff/diff.mjs +0 -7
- package/dist/packages/rapier-diff/index.mjs +0 -1
- package/dist/packages/rapier-diff/matching.mjs +0 -19
- package/dist/packages/rapier-diff/merge.mjs +0 -9
- package/dist/packages/rapier-diff/moves.mjs +0 -7
- package/dist/packages/rapier-diff/structure.mjs +0 -2
- package/dist/packages/rapier-diff/tokens.mjs +0 -6
- package/dist/packages/rapier-diff/walk.mjs +0 -12
- package/dist/spec/document-fonts.mjs +0 -9
- package/dist/spec/document-settings.mjs +6 -25
- package/dist/spec/frontmatter.mjs +0 -1
- package/dist/spec/html-blocks.mjs +3 -37
- package/dist/spec/md-assets.mjs +2 -61
- package/dist/spec/md-layout.mjs +0 -34
- package/dist/spec/md-marks.mjs +0 -51
- package/dist/spec/md-source.mjs +0 -16
- package/dist/spec/word-sidecar.mjs +0 -2
- package/package.json +21 -4
- package/style.css +38 -21
package/dist/kit/render.mjs
CHANGED
|
@@ -8,7 +8,6 @@ import {documentLanguage, documentSettingsFonts} from '../spec/document-settings
|
|
|
8
8
|
import {createLineMeasurer, linesHeight} from '../layout/model.mjs';
|
|
9
9
|
import {markPrintCapitals, createPrintCapitals, followingPrintProse} from './print-capitals.mjs';
|
|
10
10
|
import {freezePublishedInk} from './render-ink.mjs';
|
|
11
|
-
// Set the first view once. Subsequent rendering keeps the person's horizontal position.
|
|
12
11
|
export function positionNativeDiagram(drawing) {
|
|
13
12
|
const surface=drawing?.closest?.('.diagram-block,.diagram-live-preview');
|
|
14
13
|
if(!surface?.clientWidth)return;
|
|
@@ -30,7 +29,6 @@ export function positionNativeDiagram(drawing) {
|
|
|
30
29
|
surface.scrollLeft=Math.max(0,Math.min(surface.scrollWidth-surface.clientWidth,left));
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
// The document's renderer. DOM, codecs and host state are explicit inputs.
|
|
34
32
|
function createRenderer(runtime) {
|
|
35
33
|
const {RapierLedgerCarried, RapierPageReturnAddress, RapierTextCodec, _rapierArtifactHighlight, _rapierArtifactPreference, _rapierArtifactStyles, _rapierBlobDataUrl, _rapierBuildInterchangeContext, _rapierDocumentNameIsAdmissible, _rapierLedgerParts, _rapierDrawReadSVGRecipe, _rapierDrawShapeProfileFor, _rapierExportFontCss, _rapierFillDiagram, _rapierFinalizeExportCode, _rapierLanguageClass, _rapierLineStartOffsets, _rapierMarkdownEnvironment, _rapierPortableHtml, _rapierPrepareInterchangeContext, _rapierProjectPortableRoot, _rapierProviders, _rapierSourceCharEscaped, _rapierSourceLineSpan, crypto, document, escapeRapierHtmlText, globalThis, sanitizeRapierHtml} = runtime;
|
|
36
34
|
let md = runtime.md;
|
|
@@ -66,7 +64,6 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
66
64
|
: _rapierProjectPortableRoot(context.semanticRoot, { baseName: base, keepDiagrams: true });
|
|
67
65
|
for (const element of portableRoot.children) if (!element.hasAttribute('lang')) element.setAttribute('lang', language);
|
|
68
66
|
if (fontCss || settings?.defaultfont) {
|
|
69
|
-
// Keep authored font scopes inside this fragment; no page wrapper or global settings rules.
|
|
70
67
|
const {main, sans: heading, mono: code} = documentSettingsFonts(settings), body = main || heading;
|
|
71
68
|
if (body) for (const element of portableRoot.children)
|
|
72
69
|
if (!element.matches('pre,code,kbd,samp,svg,math') && !element.style.fontFamily)
|
|
@@ -80,15 +77,12 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
80
77
|
css: _rapierArtifactStyles('system', false, false, false, '') + '\n' + _rapierDocumentSettingsCss(settings)});
|
|
81
78
|
const html = work ? await runtime._rapierPortableHtmlAsync(portableRoot, work) : _rapierPortableHtml(portableRoot);
|
|
82
79
|
return {
|
|
83
|
-
// Only validated data-font rules bypass the content sanitizer, which still rejects authored styles.
|
|
84
80
|
html: (fontCss ? '<style>' + fontCss + '</style>\n' : '') + html,
|
|
85
81
|
filename: base + '-publishing.html',
|
|
86
82
|
metadata,
|
|
87
83
|
};
|
|
88
84
|
}
|
|
89
85
|
|
|
90
|
-
// The semantic export root is detached and has never entered the live visibility queue.
|
|
91
|
-
// Await the same materialization owner before any projection clones or serializes it.
|
|
92
86
|
const unavailable = plugin => Object.assign(new Error('Offline web page not written; install the ' + plugin + ' plug-in before exporting this document.'),
|
|
93
87
|
{code: 'offline_plugin_unavailable', plugin});
|
|
94
88
|
if (completePage && context.semanticRoot.querySelector('.math-placeholder[data-rapier-math-source]')) throw unavailable('math');
|
|
@@ -97,15 +91,11 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
97
91
|
if (!(completePage && retained && _rapierProviders?.mermaid?.status !== 'ready')) await _rapierFillDiagram(diagram, true);
|
|
98
92
|
if (!completePage || diagram.hasAttribute('data-diagram-unclosed')) continue;
|
|
99
93
|
if (diagram.getAttribute('data-diagram-state') === 'absent') throw unavailable('mermaid');
|
|
100
|
-
// A completed diagram must have its drawing. Syntax and renderer failures both refuse
|
|
101
|
-
// the file; an unclosed fence above is still authored source, not a completed diagram.
|
|
102
94
|
if (diagram.getAttribute('data-diagram-state') !== 'ready' || !diagram.querySelector('.diagram-cache svg'))
|
|
103
95
|
throw Object.assign(new Error('Web page not written; a required diagram could not be rendered'), {code: 'EXPORT_DIAGRAM_UNAVAILABLE'});
|
|
104
96
|
}
|
|
105
97
|
|
|
106
98
|
if (completePage) {
|
|
107
|
-
// Invalid TeX renders as a static error SVG. A source-only span means
|
|
108
|
-
// rendering itself failed, so publishing it would silently omit the equation.
|
|
109
99
|
if (context.semanticRoot.querySelector('span[data-rapier-math-source]'))
|
|
110
100
|
throw new Error('Web page not written; a required equation could not be rendered');
|
|
111
101
|
await _rapierRequireOfflinePageImages(context.semanticRoot);
|
|
@@ -120,10 +110,7 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
120
110
|
: _rapierProjectStyledRoot(context.semanticRoot, styledOptions);
|
|
121
111
|
styledRoot.setAttribute('lang', language);
|
|
122
112
|
if (completePage && typeof _rapierFinalizeExportCode === 'function') await _rapierFinalizeExportCode(styledRoot);
|
|
123
|
-
// _rapierProjectStyledRoot already returns the sanitized projection.
|
|
124
|
-
// Picture-shape annotations have no consumer in a printed artifact.
|
|
125
113
|
if (!opts.print) _rapierAnnotateExportBoxPolygons(styledRoot);
|
|
126
|
-
// Paper keeps each picture's wrap through the same static float layout a page uses.
|
|
127
114
|
if (completePage || opts.print) {
|
|
128
115
|
if (work) await finishAsync(_rapierSharedPageLayoutSteps(styledRoot), work);
|
|
129
116
|
else _rapierSharedPageLayout(styledRoot);
|
|
@@ -145,11 +132,6 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
145
132
|
const docClass = metadata.docKind === 'markdown'
|
|
146
133
|
? 'rapier-page md-render'
|
|
147
134
|
: 'rapier-page rapier-document--flat';
|
|
148
|
-
// Rapier's own stylesheet, not a lookalike: the theme frame, spec/markdown-style.css's whole
|
|
149
|
-
// .md-render set and the highlight rules, so a numbered list, a checkbox, a callout and a
|
|
150
|
-
// table are drawn in an exported page exactly as they are drawn here. The one caller with
|
|
151
|
-
// rules of its own -- Share, for the no-script float it alone writes -- appends them.
|
|
152
|
-
// Print retains its existing factory resources. HTML adds only the optional small Sans subset below.
|
|
153
135
|
const includeFonts = !!opts.print;
|
|
154
136
|
const willFont = opts.print && opts.willFont ? opts.willFont : null;
|
|
155
137
|
const settingsCss = _rapierDocumentSettingsCss(settings);
|
|
@@ -161,8 +143,6 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
161
143
|
+ (printWidth > 0 ? '.rapier-page.md-render{width:' + printWidth + 'px!important}' : '') + '}' : '';
|
|
162
144
|
const pageTitle = settings && settings.title ? globalThis.RapierMarkdownSpec.documentTitle(settings) : metadata.filename;
|
|
163
145
|
let css = _rapierArtifactStyles(theme, includeFonts, !!opts.print || completePage, false, opts.print && opts.kind === 'standalone' ? '' : null)
|
|
164
|
-
// Print already floats pictures. Tables, code, figures and rules clear them
|
|
165
|
-
// through the same clearing rule a complete page uses; print keeps its own page flow.
|
|
166
146
|
+ (completePage || opts.print ? '\n\n' + _rapierSharedPageFallbackCss(!completePage) : '')
|
|
167
147
|
+ (opts.extraCss ? '\n\n' + opts.extraCss : '')
|
|
168
148
|
+ (willFont ? '\n\n@font-face{font-family:' + willFont.family + ';src:url(' + await _rapierBlobDataUrl(new Blob([willFont.bytes], { type: 'font/ttf' })) + ') format("truetype")}' : '')
|
|
@@ -184,13 +164,7 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
184
164
|
const bodyHtml = work
|
|
185
165
|
? await finishAsync(serializeTree(styledRoot), work) : styledRoot.innerHTML;
|
|
186
166
|
if (work) await pause(work, .95);
|
|
187
|
-
// Every written page is offline. Only the writer's nonce script may run; it, pictures,
|
|
188
|
-
// styles and other page resources get no network authority. Following an ordinary link
|
|
189
|
-
// is the reader's navigation, with no Referer, not a background resource request.
|
|
190
167
|
const nonce = _rapierArtifactNonce();
|
|
191
|
-
// The reflow script is written first, because whether it exists decides the policy. A page that carries no script says so: script-src 'none' refuses every
|
|
192
|
-
// script outright. Declaring a nonce a page has no use for is a widening, small but real -- anything that could inject markup into the file could read the
|
|
193
|
-
// nonce out of it and be admitted.
|
|
194
168
|
const layoutScript = opts.scripts !== false ? _rapierArtifactLayoutScript(styledRoot, nonce, {pictures:!opts.print}) : '';
|
|
195
169
|
const inkScript = !opts.print && opts.scripts !== false ? _rapierArtifactInkScript(styledRoot, nonce) : '';
|
|
196
170
|
const htmlScript = !opts.print && opts.scripts !== false && styledRoot.querySelector('[data-rapier-html],[data-rapier-animation]') ? '<script nonce="' + nonce + '">' + htmlRuntimeSource().replace(/<\/script/gi, '<\\/script') + '</script>' : '';
|
|
@@ -198,14 +172,11 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
198
172
|
+ '<html lang="' + escapeRapierHtmlText(language) + '" data-rapier-theme="' + theme + '" data-highlights="' + highlights + '" data-invert-image-alpha="' + (invertImageAlpha ? 'on' : 'off') + '">\n'
|
|
199
173
|
+ '<head>\n<meta charset="UTF-8">\n' + (carrier ? RapierPageReturnAddress.PAGE_SEED + '\n' : '')
|
|
200
174
|
+ '<meta name="viewport" content="width=device-width, initial-scale=1">\n'
|
|
201
|
-
// Chrome 154's responsively sized frames: a host frame styled `frame-sizing: content-height` sizes
|
|
202
|
-
// itself to a page that opts in with this meta, so a document exported from Rapier and carried in
|
|
203
|
-
// someone's page shows whole, with no inner scroll. Any origin may read the height: the page is the
|
|
204
|
-
// document, published by the person who exported it.
|
|
205
175
|
+ '<meta name="responsive-embedded-sizing" content="allow-origins=*">\n'
|
|
206
176
|
+ '<meta http-equiv="Content-Security-Policy" content="' + _rapierExportedPageCsp(layoutScript || inkScript || htmlScript ? nonce : '', includeFonts || !!fontCss || !!factoryFontCss) + '">\n'
|
|
207
177
|
+ '<meta name="referrer" content="no-referrer">\n'
|
|
208
178
|
+ '<link rel="icon" href="data:,">\n'
|
|
179
|
+
+ _rapierPagePreviewMeta(styledRoot, pageTitle)
|
|
209
180
|
+ '<meta name="generator" content="Rapier ' + escapeRapierHtmlText(version) + '">\n'
|
|
210
181
|
+ '<title>' + escapeRapierHtmlText(pageTitle) + '</title>\n'
|
|
211
182
|
+ '<style>' + css + '</style>\n</head>\n'
|
|
@@ -227,11 +198,19 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
227
198
|
};
|
|
228
199
|
}
|
|
229
200
|
|
|
201
|
+
function _rapierPagePreviewMeta(root, fallbackTitle) {
|
|
202
|
+
const words = node => String(node?.textContent || '').replace(/\s+/g, ' ').trim();
|
|
203
|
+
const title = words(root.querySelector('h1,h2,h3,h4,h5,h6')) || String(fallbackTitle || '');
|
|
204
|
+
const text = [...root.querySelectorAll('p')].map(words).find(line => line.length >= 20) || '';
|
|
205
|
+
const sentence = /^.{20,300}?[.!?](?=\s|$)/.exec(text)?.[0] || (text.length <= 200 ? text : text.slice(0, 200).replace(/\s+\S*$/, '') + '…');
|
|
206
|
+
const meta = (key, name, value) => '<meta ' + key + '="' + name + '" content="' + escapeRapierHtmlText(value) + '">\n';
|
|
207
|
+
return (sentence ? meta('name', 'description', sentence) : '') + (title ? meta('property', 'og:title', title) : '') +
|
|
208
|
+
(sentence ? meta('property', 'og:description', sentence) : '');
|
|
209
|
+
}
|
|
210
|
+
|
|
230
211
|
async function _rapierBuildPublishedPage(context) {
|
|
231
212
|
if (typeof context?.canonical !== 'string' || !context.semanticRoot)
|
|
232
213
|
throw new TypeError('Publishing requires a captured document.');
|
|
233
|
-
// Public readers receive only the current document. The editing session's previous
|
|
234
|
-
// source and attribution ledger never acquire public custody through an export option.
|
|
235
214
|
const current = {...context, ledger: null, carried: 'none', comparisonBase: null};
|
|
236
215
|
return _rapierBuildArtifact({
|
|
237
216
|
kind: 'standalone', scripts: false, extraCss: _rapierSharedPageFallbackCss(),
|
|
@@ -250,6 +229,11 @@ async function _rapierProjectStyledRootAsync(semanticRoot, options = {}) { retur
|
|
|
250
229
|
function* _rapierProjectStyledRootSteps(semanticRoot, options) {
|
|
251
230
|
const opts = options || {};
|
|
252
231
|
const root = yield* cloneTree(semanticRoot);
|
|
232
|
+
for (const token of root.querySelectorAll('.rapier-source-token--softbreak')) { yield;
|
|
233
|
+
if (token.getAttribute('data-rapier-source') !== '%0A') token.replaceWith(...token.childNodes);
|
|
234
|
+
else if (token.previousSibling?.classList?.contains('callout__label')) token.remove();
|
|
235
|
+
else token.replaceWith(root.ownerDocument.createTextNode(' '));
|
|
236
|
+
}
|
|
253
237
|
const metadata = opts.metadata || {};
|
|
254
238
|
|
|
255
239
|
if (metadata.docKind !== 'markdown') {
|
|
@@ -299,7 +283,6 @@ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
|
|
|
299
283
|
}
|
|
300
284
|
input.disabled = !!opts.print || !item;
|
|
301
285
|
if (input.disabled) { input.setAttribute('aria-disabled', 'true'); continue; }
|
|
302
|
-
// Native checked state changes this reader's view, never the carried source or disk.
|
|
303
286
|
input.removeAttribute('aria-disabled');
|
|
304
287
|
input.removeAttribute('aria-checked');
|
|
305
288
|
input.setAttribute('autocomplete', 'off');
|
|
@@ -315,8 +298,6 @@ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
|
|
|
315
298
|
}
|
|
316
299
|
|
|
317
300
|
function _rapierDocxRenderStyles(root) {
|
|
318
|
-
// The semantic root is detached and its code is normalized text. Use the same
|
|
319
|
-
// highlighter and light reference sheet that paint a page, then retain native run facts.
|
|
320
301
|
const codes = Array.from(root.querySelectorAll('pre > code'));
|
|
321
302
|
for (const code of codes) {
|
|
322
303
|
const lang = _rapierLanguageClass(code);
|
|
@@ -345,8 +326,6 @@ function _rapierDocxRenderStyles(root) {
|
|
|
345
326
|
if (!paint) throw new Error('Word text colors could not be read.');
|
|
346
327
|
const hex = colour => {
|
|
347
328
|
if (!colours.has(colour)) {
|
|
348
|
-
// CSS may resolve an accent to oklch/color-mix. The browser supplies its
|
|
349
|
-
// sRGB paper colour; the writer does not invent a second colour parser.
|
|
350
329
|
paint.fillStyle = '#ffffff'; paint.fillRect(0, 0, 1, 1);
|
|
351
330
|
paint.fillStyle = colour; paint.fillRect(0, 0, 1, 1);
|
|
352
331
|
colours.set(colour, '#' + Array.from(paint.getImageData(0, 0, 1, 1).data).slice(0, 3)
|
|
@@ -361,7 +340,6 @@ function _rapierDocxRenderStyles(root) {
|
|
|
361
340
|
Array.from(probe.querySelectorAll('span')).forEach((span, index) => {
|
|
362
341
|
const style = into.defaultView.getComputedStyle(span), target = originals[index];
|
|
363
342
|
target.style.color = hex(style.color);
|
|
364
|
-
// Carry generated syntax emphasis as Word's semantic bold slot.
|
|
365
343
|
target.style.fontWeight ||= style.fontWeight === style.getPropertyValue('--md-fw-semibold').trim() ? 'bold' : style.fontWeight;
|
|
366
344
|
target.style.fontStyle = style.fontStyle;
|
|
367
345
|
});
|
|
@@ -371,8 +349,6 @@ function _rapierDocxRenderStyles(root) {
|
|
|
371
349
|
for (const quote of quotes) {
|
|
372
350
|
const probe = into.createElement('blockquote'); probe.className = quote.className;
|
|
373
351
|
probe.style.borderInlineStartColor = quote.style.borderInlineStartColor;
|
|
374
|
-
// Nested quotes have their own reference border colour. Preserve the quote
|
|
375
|
-
// ancestry without cloning the document's pictures or other resources.
|
|
376
352
|
const parent = quoteProbes.get(quote.parentElement?.closest('blockquote')) || surface;
|
|
377
353
|
parent.appendChild(probe); quoteProbes.set(quote, probe);
|
|
378
354
|
}
|
|
@@ -388,22 +364,17 @@ function _rapierPaintCode(el, html) {
|
|
|
388
364
|
|
|
389
365
|
function _rapierPrefixPortableAnchors(root, baseName) { return finish(_rapierPrefixPortableAnchorsSteps(root, baseName)); }
|
|
390
366
|
|
|
391
|
-
// The id an anchor carries on a portable page: the document's name, then the anchor with what an id cannot hold made a hyphen.
|
|
392
367
|
function _rapierPortableAnchorId(baseName, id) {
|
|
393
368
|
const slug = String(baseName || 'document').toLowerCase()
|
|
394
369
|
.replace(/[^a-z0-9]+/g, '-')
|
|
395
370
|
.replace(/^-+|-+$/g, '') || 'document';
|
|
396
|
-
return 'rapier-' + slug + '-' + (String(id).replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '') || 'anchor');
|
|
371
|
+
return 'rapier-anchor-' + slug + '-' + (String(id).replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '') || 'anchor');
|
|
397
372
|
}
|
|
398
373
|
|
|
399
374
|
function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
|
|
400
375
|
const ids = new Map(), svgIds = new Map();
|
|
401
376
|
const used = new Set();
|
|
402
377
|
const scopeOf = element => {
|
|
403
|
-
// querySelectorAll('*') hands back whatever the host document holds, and not every host gives
|
|
404
|
-
// each node Element.closest -- the export writer runs against parsed and stubbed documents as
|
|
405
|
-
// well as a browser's. Without this guard the whole anchor pass would throw TypeError. No scope
|
|
406
|
-
// simply means that node's references resolve document-wide, as an unscoped drawing's do.
|
|
407
378
|
if (typeof element?.closest !== 'function') return null;
|
|
408
379
|
let svg = element.closest('svg');
|
|
409
380
|
while (typeof svg?.parentElement?.closest === 'function' && svg.parentElement.closest('svg')) svg = svg.parentElement.closest('svg');
|
|
@@ -418,8 +389,6 @@ function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
|
|
|
418
389
|
let suffix = 2;
|
|
419
390
|
while (used.has(nextId)) nextId = first + '-' + suffix++;
|
|
420
391
|
used.add(nextId);
|
|
421
|
-
// A document link resolves the first matching anchor, as it did before renaming.
|
|
422
|
-
// SVG references resolve within their own drawing, never the last repeated drawing.
|
|
423
392
|
if (!ids.has(oldId)) ids.set(oldId, nextId);
|
|
424
393
|
const svg = scopeOf(element);
|
|
425
394
|
if (svg) {
|
|
@@ -433,7 +402,6 @@ function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
|
|
|
433
402
|
for (const element of root.querySelectorAll('*')) { yield;
|
|
434
403
|
const svg = scopeOf(element), local = svgIds.get(svg);
|
|
435
404
|
const rewriteId = value => (svg ? local?.get(value) : ids.get(value)) || '';
|
|
436
|
-
// URL fragments percent-decode once; ARIA/for/headers are literal IDREFs.
|
|
437
405
|
const decodeFragment = value => { try { return decodeURIComponent(value); } catch (_) { return value; } };
|
|
438
406
|
const iriProperty = /^(?:fill|stroke|filter|clip-path|mask|marker(?:-start|-mid|-end)?|cursor)$/;
|
|
439
407
|
const rewriteUrls = value => value.replace(/url\(\s*(?:"((?:\\[\s\S]|[^"\\])*)"|'((?:\\[\s\S]|[^'\\])*)'|((?:\\(?:[0-9a-f]{1,6}\s?|[^\r\n\f])|[^\s\\()])+))\s*\)/gi, (whole, double, single, bare) => {
|
|
@@ -462,8 +430,6 @@ function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
|
|
|
462
430
|
if (next) element.setAttribute(name, next);
|
|
463
431
|
else element.removeAttribute(name);
|
|
464
432
|
} else if (svg && name === 'style') {
|
|
465
|
-
// Interpret only URL-bearing presentation properties: content/metadata strings
|
|
466
|
-
// that merely SAY "url(#id)" are not a reference and retain their own words.
|
|
467
433
|
for (const property of Array.from(element.style)) {
|
|
468
434
|
if (!iriProperty.test(property)) continue;
|
|
469
435
|
const before = element.style.getPropertyValue(property), next = rewriteUrls(before);
|
|
@@ -518,12 +484,6 @@ function _rapierAnnotateExportBoxPolygons(root) {
|
|
|
518
484
|
if (!recipe) continue;
|
|
519
485
|
const view = recipe.view && recipe.view.w > 0 && recipe.view.h > 0 ? recipe.view : { x: 0, y: 0, w: recipe.canvas.w, h: recipe.canvas.h };
|
|
520
486
|
if (layout.wrap === 'around') {
|
|
521
|
-
// One occupancy for live view and export: the drawing's own profile -- closed faces reserved,
|
|
522
|
-
// open strokes as ribbons, labels as boxes -- computed from the same recipe the live view
|
|
523
|
-
// reads, sampled into bands the exported page reads back (layout/model.mjs parseProfile). Text
|
|
524
|
-
// and paint shapes need rendered pixels for their glyph silhouettes, so the live editor's own
|
|
525
|
-
// <img> supplies those when it can be found; a raster or a foreign SVG writes nothing and the
|
|
526
|
-
// export measures alpha, as the live view does.
|
|
527
487
|
try {
|
|
528
488
|
let glyphs = null;
|
|
529
489
|
if (recipe.shapes.some(shape => shape.recognized === 'text' || shape.recognized === 'paint' || shape.label)) {
|
|
@@ -572,8 +532,6 @@ function* _rapierSharedSourceFormsSteps(source, root, preparedDestinations = nul
|
|
|
572
532
|
const used = [], definitions = [];
|
|
573
533
|
for (const row of destinations) {
|
|
574
534
|
yield;
|
|
575
|
-
// Share can display a converted picture. Only identical authored bytes can be
|
|
576
|
-
// borrowed from that picture; otherwise the source keeps its own data URL.
|
|
577
535
|
const image = byUrl.get(row.destination);
|
|
578
536
|
if (!image) continue;
|
|
579
537
|
let id = image.id;
|
|
@@ -608,7 +566,6 @@ async function _rapierSharedSourceCarrier(context, root) {
|
|
|
608
566
|
'"' + (forms.ids.length ? ' data-images="' + forms.ids.join(' ') + '"' : '') +
|
|
609
567
|
(forms.definitions.length ? ' data-image-definitions="' + forms.definitions.map(encodeURIComponent).join(' ') + '"' : '') + '>\n' +
|
|
610
568
|
forms.carried + '\n</script>\n' +
|
|
611
|
-
// The optional edit ledger: carried only when the host binds its owner; a host without one writes none.
|
|
612
569
|
(RapierLedgerCarried ? RapierLedgerCarried.writeParts(forms.resolved, _rapierLedgerParts(forms.resolved, context.ledger, context.carried)) +
|
|
613
570
|
RapierLedgerCarried.writeBase(forms.resolved, context.comparisonBase) : '');
|
|
614
571
|
}
|
|
@@ -622,10 +579,7 @@ function _rapierSharedResolve(text, ids, images, definitions) {
|
|
|
622
579
|
if (typeof found !== 'string' || !/^data:image\/(?:png|jpeg|webp|gif|svg\+xml|jxl);base64,[A-Za-z0-9+/]+={0,2}$/i.test(found)) { broken = true; return null; }
|
|
623
580
|
return found;
|
|
624
581
|
};
|
|
625
|
-
// Authored # characters were encoded. A raw id after ]( can only be a structural
|
|
626
|
-
// substitution; matching its destination lets nested alt brackets survive unchanged.
|
|
627
582
|
const inline = new RegExp('(\\]\\([ \\t\\r\\n]*(?:<)?)#' + id + '(?=>?[ \\t\\r\\n]*\\)|>?[ \\t\\r\\n]+["\'(])', 'g');
|
|
628
|
-
// A definition may sit in a quote or a list item, as the writer found it: its line starts with their markers.
|
|
629
583
|
const definition = new RegExp('(^[ \\t>]*(?:(?:[-+*]|\\d{1,9}[.)])[ \\t]+[ \\t>]*)*\\[((?:\\\\.|[^\\]\\\\])+)\\]:[ \\t]*(?:(?:\\r\\n|\\r|\\n)[ \\t>]*)?(?:<)?)#' + id + '(?=>?[ \\t]*$|>?[ \\t]+["\'(])', 'gm');
|
|
630
584
|
const swapInline = (match, lead, value) => { const found = src(value); return found == null ? match : lead + found; };
|
|
631
585
|
const swapDefinition = (match, lead, label, value) => {
|
|
@@ -640,7 +594,6 @@ function _rapierSharedResolve(text, ids, images, definitions) {
|
|
|
640
594
|
async function _rapierReadSharedDocument(text, filename) {
|
|
641
595
|
const html = String(text);
|
|
642
596
|
if (!/\.html?$/i.test(filename || '') || !html.includes('type="' + _RAPIER_SHARED_SOURCE_TYPE + '"')) return null;
|
|
643
|
-
// Template contents remain inert, including pictures and scripts in ordinary HTML.
|
|
644
597
|
const template = document.createElement('template');
|
|
645
598
|
template.innerHTML = html;
|
|
646
599
|
const invalid = () => { throw new Error('The editable source in this web page is incomplete or changed'); };
|
|
@@ -652,15 +605,12 @@ async function _rapierReadSharedDocument(text, filename) {
|
|
|
652
605
|
const definitionTokens = (carrier.getAttribute('data-image-definitions') || '').split(/\s+/).filter(Boolean);
|
|
653
606
|
if (!_rapierDocumentNameIsAdmissible(name) || !['markdown', 'text', 'code'].includes(kind) || !/^[0-9a-f]{64}$/.test(sha256) ||
|
|
654
607
|
ids.some(id => !_RAPIER_SHARED_ID.test(id)) || definitionTokens.some(token => !_RAPIER_SHARED_DEFINITION.test(token))) return invalid();
|
|
655
|
-
// Only a definition the page declares is an image definition.
|
|
656
608
|
let definitions;
|
|
657
609
|
try { definitions = new Set(definitionTokens.map(decodeURIComponent)); } catch (_) { return invalid(); }
|
|
658
610
|
let source = carrier.textContent;
|
|
659
611
|
if (source.startsWith('\n')) source = source.slice(1);
|
|
660
612
|
if (source.endsWith('\n')) source = source.slice(0, -1);
|
|
661
613
|
source = source.replace(/ /g, '\r');
|
|
662
|
-
// A duplicate id (two <img id="…">) is ambiguous, so it is recorded as null rather than either
|
|
663
|
-
// src -- ties into _rapierSharedResolve's `broken` check the same way a missing image does.
|
|
664
614
|
const images = new Map();
|
|
665
615
|
for (const element of template.content.querySelectorAll('img[id]')) images.set(element.id, images.has(element.id) ? null : element.getAttribute('src'));
|
|
666
616
|
const resolved = _rapierSharedResolve(source, ids, images, definitions);
|
|
@@ -686,7 +636,6 @@ function _rapierShareWrapKind(element) {
|
|
|
686
636
|
if (globalThis.RapierMarkdownLayout.wrapTextBlock(element)) return 'prose';
|
|
687
637
|
const image = element.tagName === 'P' ? element.querySelector('img') : null;
|
|
688
638
|
if (image && globalThis.RapierImageLayout.imageOnly(element, image)) return 'picture';
|
|
689
|
-
// An empty paragraph is transparent to the owner search, as in the editor and the export.
|
|
690
639
|
if (element.tagName === 'P' && !element.textContent.trim() && !image) return 'metadata';
|
|
691
640
|
return null;
|
|
692
641
|
}
|
|
@@ -701,14 +650,6 @@ function _rapierSharePictureParagraph(image) {
|
|
|
701
650
|
|
|
702
651
|
function _rapierSharedPageLayout(root) { return finish(_rapierSharedPageLayoutSteps(root)); }
|
|
703
652
|
function* _rapierSharedPageLayoutSteps(root) {
|
|
704
|
-
// Owner search (wrapNeighbour) reads the sibling chain live, and applying one assignment
|
|
705
|
-
// splices the tree -- moving the owner into a new container moves it out of the very chain
|
|
706
|
-
// a later picture's own search would walk, and a barrier-free skip past an earlier picture
|
|
707
|
-
// can land on that new container instead (it is not a <p>, so it reads as a barrier). So
|
|
708
|
-
// every owner is resolved first, against the one original, unspliced tree, in a pass that
|
|
709
|
-
// only reads; only once all of them are known does a second pass apply any of them. Same
|
|
710
|
-
// two-phase shape as layout/interchange.js's reflow(), which resolves every picture's owner
|
|
711
|
-
// from one upfront `[...root.children]` before it changes anything about any of them.
|
|
712
653
|
const assignments = [];
|
|
713
654
|
for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
|
|
714
655
|
yield;
|
|
@@ -716,19 +657,9 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
716
657
|
if (layout?.wrap !== 'around' && layout?.wrap !== 'box') continue;
|
|
717
658
|
const paragraph = _rapierSharePictureParagraph(image);
|
|
718
659
|
if (!paragraph) continue;
|
|
719
|
-
// Prefer following text, then preceding text, looking past pictures and metadata.
|
|
720
660
|
const owner = globalThis.RapierMarkdownLayout.wrapNeighbour(paragraph, _rapierShareWrapKind);
|
|
721
661
|
if (owner) assignments.push({image, paragraph, layout, owner});
|
|
722
662
|
}
|
|
723
|
-
// A float affects every line box after it in the same flow, which is the wrap law: prose and
|
|
724
|
-
// headings carry on beside the picture across a heading or section boundary as though the blocks
|
|
725
|
-
// between were not there. So the picture paragraph is placed right before its owner's text (a
|
|
726
|
-
// float only reaches what follows it) and left in the page's own flow, uncontained; the blocks
|
|
727
|
-
// that never wrap -- tables, code, figures and rules -- clear it through the page
|
|
728
|
-
// stylesheet instead, and a bordered h1 forms its own block so its rule does not run under the
|
|
729
|
-
// picture. Static CSS, no script: the no-script fallback only -- a reader with scripting off
|
|
730
|
-
// still gets a sensible nearest-side float; the inlined planner (same owner as standalone)
|
|
731
|
-
// replaces it when scripts run.
|
|
732
663
|
for (const {image, paragraph, layout, owner} of assignments) {
|
|
733
664
|
yield;
|
|
734
665
|
const side = (layout.x ?? 0) < 50 ? 'left' : 'right';
|
|
@@ -738,31 +669,9 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
738
669
|
paragraph.style.setProperty('shape-outside', 'inset(' + layout.y + 'em 0 0)');
|
|
739
670
|
}
|
|
740
671
|
image.style.margin = '0';
|
|
741
|
-
// The reserved box comes from the same rotated-bounds geometry every other consumer uses
|
|
742
|
-
// (layout/browser.js, layout/interchange.js's `geometry.rotatedBoundsRad`, layout/model.mjs's
|
|
743
|
-
// one owner), expressed as a percentage of the column the way the unrotated branch below already
|
|
744
|
-
// is -- never the picture's own pixel dimensions, which have no relation to a requested
|
|
745
|
-
// percentage width (a 4000px-wide source at width=40% would float 1788px wide in a 600px
|
|
746
|
-
// column). Intrinsic width/height supply only the aspect ratio; a percentage padding-top/bottom
|
|
747
|
-
// resolves against the same containing-block width CSS already resolves the float's own
|
|
748
|
-
// percentage width against, so one basis serves both the box and its vertical reservation, and
|
|
749
|
-
// the box stays exactly as wide as the column at every viewport width. The turned box is
|
|
750
|
-
// centred, horizontally and vertically, in the reserved (wider, taller) box rather than
|
|
751
|
-
// left-flush inside it, so the space this paragraph clears for it (the float's own width) is
|
|
752
|
-
// evenly split around the picture on every side, not just above and below. A missing `width`
|
|
753
|
-
// falls back to the picture's own natural pixel size in pixels, the same fallback the unrotated
|
|
754
|
-
// branch below uses; without either width/height attribute the picture still turns, just without
|
|
755
|
-
// the extra reservation (the same graceful narrowing Share already accepts elsewhere).
|
|
756
672
|
const naturalWidth = Number(image.getAttribute('width')) || 0, naturalHeight = Number(image.getAttribute('height')) || 0;
|
|
757
673
|
if (layout.rotate && naturalWidth > 0 && naturalHeight > 0) {
|
|
758
674
|
const rad = layout.rotate * Math.PI / 180, aspect = naturalHeight / naturalWidth;
|
|
759
|
-
// A steep turn (e.g. 90deg on a landscape source) can need the image WIDER than its own
|
|
760
|
-
// reserved box's width -- the rotated bounding box is narrower than the picture's own
|
|
761
|
-
// unrotated width whenever the turn swaps a wide dimension for a narrow one. Share's own
|
|
762
|
-
// global `img{max-width:100%}` (a sensible default everywhere else) would silently clamp
|
|
763
|
-
// exactly that case, shrinking the image and, with it, its auto height -- `max-width:none`
|
|
764
|
-
// here overrides that single rule with a more specific one, only for a picture this pass
|
|
765
|
-
// already sizes deliberately.
|
|
766
675
|
image.style.maxWidth = 'none';
|
|
767
676
|
if (layout.width != null) {
|
|
768
677
|
let width = layout.width, height = width * aspect;
|
|
@@ -787,7 +696,6 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
787
696
|
image.style.marginLeft = image.style.marginRight = ((bounds.width - naturalWidth) / 2) + 'px';
|
|
788
697
|
}
|
|
789
698
|
} else if (layout.lines != null) {
|
|
790
|
-
// Sized in lines: the float shrinks to the picture, whose height is the lines (spec/md-layout.mjs imageStyle).
|
|
791
699
|
image.style.width = 'auto';
|
|
792
700
|
image.style.height = globalThis.RapierMarkdownLayout.linesHeightCss(layout.lines);
|
|
793
701
|
} else {
|
|
@@ -796,32 +704,16 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
796
704
|
if (layout.width != null || image.getAttribute('width')) image.style.width = '100%';
|
|
797
705
|
}
|
|
798
706
|
if (layout.rotate) { image.style.transform = 'rotate(' + layout.rotate + 'deg)'; image.style.transformOrigin = '50% 50%'; }
|
|
799
|
-
// `wrap=around` wraps to the picture's own alpha silhouette the way standard-adoption.md's
|
|
800
|
-
// rung-2 paragraph promises -- `shape-outside:url(<the same picture>)`, which every engine that
|
|
801
|
-
// reads shape-outside re-samples from the referenced image's own pixels, no script required. A
|
|
802
|
-
// CSS transform does not turn the shape a `shape-outside` url() samples (it stays the picture's
|
|
803
|
-
// unturned alpha), so a rotated `around` picture keeps the plain rectangular reservation above
|
|
804
|
-
// instead -- an honest conservative box rather than a silhouette shaped for the wrong angle.
|
|
805
|
-
// `layout.y` already owns `shape-outside` for its own inset reservation (below); the two are not
|
|
806
|
-
// combinable (a float has exactly one `shape-outside` value), so a `y`-offset `around` picture
|
|
807
|
-
// keeps that inset instead of silhouette wrapping -- a documented, narrow degradation.
|
|
808
707
|
if (layout.wrap === 'around' && !layout.rotate && !layout.y && !image.dataset.rapierContour?.endsWith('-box')) {
|
|
809
708
|
const src = image.getAttribute('src');
|
|
810
709
|
if (src) {
|
|
811
710
|
paragraph.style.setProperty('shape-outside', 'url("' + src + '")');
|
|
812
|
-
// Standard 10% alpha cutoff (`layout/model.mjs` alphaThreshold = .1).
|
|
813
711
|
paragraph.style.setProperty('shape-image-threshold', '0.1');
|
|
814
712
|
}
|
|
815
713
|
}
|
|
816
714
|
paragraph.classList.add('rapier-wrap-picture');
|
|
817
715
|
owner.before(paragraph);
|
|
818
716
|
}
|
|
819
|
-
// `behind`/`front` position the picture exactly where `x`/`y`/`width` say, out of the words' flow
|
|
820
|
-
// entirely (z-index below or above them) -- the absolutely positioned picture
|
|
821
|
-
// standard-adoption.md's rung-2 paragraph promises for these two placements, no obstacle
|
|
822
|
-
// avoidance, no script. The picture's own paragraph collapses to zero height in place (the same
|
|
823
|
-
// collapse-in-place anchor layout/interchange.js's own reflow uses for every positioned kind) so
|
|
824
|
-
// removing the picture from flow costs the surrounding text nothing.
|
|
825
717
|
const positionedAssignments = [];
|
|
826
718
|
for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
|
|
827
719
|
yield;
|
|
@@ -833,7 +725,6 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
833
725
|
for (const {image, paragraph, layout} of positionedAssignments) {
|
|
834
726
|
yield;
|
|
835
727
|
paragraph.style.cssText = 'position:relative;height:0;margin:0;padding:0;line-height:0';
|
|
836
|
-
// Without x the picture starts at the left edge, as everywhere else (layout/model.mjs imageBox).
|
|
837
728
|
const x = layout.x, naturalWidth = Number(image.getAttribute('width')) || 0;
|
|
838
729
|
if (layout.width != null) image.style.width = layout.width + '%';
|
|
839
730
|
else if (naturalWidth > 0) image.style.width = naturalWidth + 'px';
|
|
@@ -843,14 +734,9 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
843
734
|
image.style.top = (layout.y || 0) + 'em';
|
|
844
735
|
image.style.margin = '0';
|
|
845
736
|
if (layout.rotate) { image.style.transform = 'rotate(' + layout.rotate + 'deg)'; image.style.transformOrigin = '50% 50%'; }
|
|
846
|
-
// Same rule as the live editor and the styled export: `behind` sinks under the owner's
|
|
847
|
-
// own in-flow, non-positioned text; `front` keeps the default stack, already above it.
|
|
848
737
|
if (layout.wrap === 'behind') image.style.zIndex = '-1';
|
|
849
738
|
paragraph.classList.add('rapier-positioned-picture');
|
|
850
739
|
}
|
|
851
|
-
// A turned picture Share does not otherwise position (inline, which this static page does not
|
|
852
|
-
// float at all) still turns -- the same picture, painted in place, never upright just because
|
|
853
|
-
// Share's own float model has nothing else to say about it.
|
|
854
740
|
const positioned = new Set([...assignments, ...positionedAssignments].map(row => row.image));
|
|
855
741
|
for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
|
|
856
742
|
yield;
|
|
@@ -903,8 +789,6 @@ async function _rapierRequireOfflinePageImages(root) {
|
|
|
903
789
|
const picture = image.parentElement?.tagName.toLowerCase() === 'picture' && image.parentElement;
|
|
904
790
|
const alternative = picture && [...picture.querySelectorAll('source[srcset]')].some(source => source.getAttribute('srcset').trim());
|
|
905
791
|
if (tag !== 'source' && (src || !srcset && !alternative)) sources.push(src);
|
|
906
|
-
// A data URL contains its own comma. Read each URL before its optional
|
|
907
|
-
// density/width descriptor, as HTML's image candidate list does.
|
|
908
792
|
for (let at = 0; at < srcset.length;) {
|
|
909
793
|
while (at < srcset.length && /[\t\n\f\r ,]/.test(srcset[at])) at++;
|
|
910
794
|
const start = at;
|
|
@@ -922,8 +806,6 @@ async function _rapierRequireOfflinePageImages(root) {
|
|
|
922
806
|
missing.push('Picture ' + (index + 1) + (alt ? ' — ' + alt : '') + ': ' + destination);
|
|
923
807
|
}
|
|
924
808
|
}
|
|
925
|
-
// A source candidate or an SVG/CSS picture is still an automatic resource. Inspect
|
|
926
|
-
// the retained data; ordinary links remain navigation and local fragment references stay local.
|
|
927
809
|
const sourceTags = new Set(['audio', 'video', 'source', 'track', 'input', 'iframe', 'embed', 'script']);
|
|
928
810
|
const cssAttributes = new Set(['style', 'fill', 'stroke', 'filter', 'clip-path', 'mask', 'marker-start', 'marker-mid', 'marker-end', 'cursor', 'color-profile']);
|
|
929
811
|
for (const element of root.querySelectorAll('*')) {
|
|
@@ -940,13 +822,10 @@ async function _rapierRequireOfflinePageImages(root) {
|
|
|
940
822
|
}
|
|
941
823
|
}
|
|
942
824
|
if (!missing.length) return;
|
|
943
|
-
// The writer refuses immediately; interactive callers own any explanatory sheet.
|
|
944
825
|
throw Object.assign(new Error('Web page not written; embed these pictures and try again. ' + missing.join('; ')),
|
|
945
826
|
{code: 'offline_image_unavailable'});
|
|
946
827
|
}
|
|
947
828
|
|
|
948
|
-
// Shared by live CSS admission and offline resource admission. Quoted strings are
|
|
949
|
-
// resource URLs only inside url() or as an image-set option, never arbitrary CSS text.
|
|
950
829
|
function _rapierCssValueUrls(value) {
|
|
951
830
|
const urls = [], functions = [];
|
|
952
831
|
let at = 0;
|
|
@@ -1022,10 +901,7 @@ function _rapierCssValueUrls(value) {
|
|
|
1022
901
|
|
|
1023
902
|
async function _rapierBuildSharedPage(captured) {
|
|
1024
903
|
const context = await _rapierPrepareInterchangeContext({kind: 'share'}, captured);
|
|
1025
|
-
// Share and Export write the same complete page, including native disclosures.
|
|
1026
904
|
const artifact = await _rapierBuildArtifact({kind: 'standalone'}, context);
|
|
1027
|
-
// No cap and no warning: the page is as large as the document is. A page over the editor's own 25
|
|
1028
|
-
// MiB open limit still opens in any browser.
|
|
1029
905
|
const blob = new Blob([artifact.html], {type: 'text/html;charset=utf-8'});
|
|
1030
906
|
return {blob, filename: context.baseName + '.html'};
|
|
1031
907
|
}
|
|
@@ -1067,7 +943,6 @@ async function _rapierDataImageDestinationsAsync(source, work) {
|
|
|
1067
943
|
}
|
|
1068
944
|
|
|
1069
945
|
function _rapierImageDestinations(source, accepts) {
|
|
1070
|
-
// Only a rendered Markdown image consumes a definition; unused definitions stay literal source.
|
|
1071
946
|
if (!source.includes('![')) return [];
|
|
1072
947
|
const env = {}, tokens = md.parse(source, env), offsets = _rapierLineStartOffsets(source);
|
|
1073
948
|
const hrefs = _rapierImageSources(tokens), labels = new Set(), destinations = [], seen = new Set();
|
|
@@ -1354,7 +1229,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1354
1229
|
steps.length = 0;
|
|
1355
1230
|
}
|
|
1356
1231
|
|
|
1357
|
-
// One prepared rich paragraph retains original item text. Unsupported shaping keeps native flow.
|
|
1358
1232
|
function prepare(paragraph) {
|
|
1359
1233
|
const refuse = () => { prepared.forget(paragraph); return null; };
|
|
1360
1234
|
if (!/^(P|H[1-6])$/.test(paragraph.tagName) || !paragraph.textContent.trim() || paragraph.textContent.length > 8192 ||
|
|
@@ -1371,7 +1245,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1371
1245
|
!preparation.supportsRichStyle(computed, language) || computed.whiteSpace !== 'normal' ||
|
|
1372
1246
|
computed.transform !== 'none' ||
|
|
1373
1247
|
['::before', '::after'].some(pseudo => !['none', 'normal'].includes(view.getComputedStyle(paragraph, pseudo).content)) ||
|
|
1374
|
-
// Lines start at the padding box's top-left: left, right or top inset is refused.
|
|
1375
1248
|
['paddingLeft', 'paddingRight', 'paddingTop', 'borderLeftWidth', 'borderRightWidth', 'borderTopWidth']
|
|
1376
1249
|
.some(key => parseFloat(computed[key]) > 0)) return refuse();
|
|
1377
1250
|
const runs = [], items = [], walker = doc.createTreeWalker(paragraph, 4);
|
|
@@ -1417,11 +1290,9 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1417
1290
|
balance: 0};
|
|
1418
1291
|
}
|
|
1419
1292
|
|
|
1420
|
-
// An id survives on the first fragment only.
|
|
1421
1293
|
function fragmentNode(record, fragment, idsKept) {
|
|
1422
1294
|
const run = record.runs[fragment.itemIndex], mapped = geometry.mapFragment(run, fragment, {whiteSpace: record.whiteSpace});
|
|
1423
1295
|
if (!mapped) return null;
|
|
1424
|
-
// A line ends on the white space it broke at. A chip, a link or a highlight must not paint that space, so it stands after the shell.
|
|
1425
1296
|
const kept = run.parents.length ? mapped.text.replace(/[ \t\n\r\f]+$/, '') : mapped.text;
|
|
1426
1297
|
const split = kept && kept.length < mapped.text.length;
|
|
1427
1298
|
let child = doc.createTextNode(split ? kept : mapped.text);
|
|
@@ -1488,8 +1359,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1488
1359
|
return found;
|
|
1489
1360
|
}
|
|
1490
1361
|
|
|
1491
|
-
// wrap=around reads data-rapier-occupancy, else alpha. Cache key: src, dimensions, layout.wrap, rotate angle.
|
|
1492
|
-
// A turned raster's profile is recomputed here as pictureProfile does, never shipped.
|
|
1493
1362
|
function profile(image, layout) {
|
|
1494
1363
|
if (layout.wrap === 'around' && image.dataset?.rapierContour?.endsWith('-box')) return null;
|
|
1495
1364
|
const src = image.currentSrc || image.getAttribute('src') || '', rotateRad = (layout.rotate || 0) * Math.PI / 180;
|
|
@@ -1509,7 +1378,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1509
1378
|
return value;
|
|
1510
1379
|
}
|
|
1511
1380
|
|
|
1512
|
-
// wrap=box reads data-rapier-box-polygon; this page never parses a recipe or bundles Draw. No polygon: whole rectangle.
|
|
1513
1381
|
function boxProfile(image) {
|
|
1514
1382
|
const raw = image.getAttribute('data-rapier-box-polygon');
|
|
1515
1383
|
if (!raw) return null;
|
|
@@ -1520,7 +1388,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1520
1388
|
return geometry.polygonProfile(corners);
|
|
1521
1389
|
}
|
|
1522
1390
|
|
|
1523
|
-
// As the live editor (browser.js).
|
|
1524
1391
|
function syncInlineRotatedPictures(exclude) {
|
|
1525
1392
|
for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
|
|
1526
1393
|
if (exclude.has(image) || !image.complete || !image.naturalWidth || !image.naturalHeight) continue;
|
|
@@ -1529,7 +1396,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1529
1396
|
const width = image.offsetWidth, height = image.offsetHeight;
|
|
1530
1397
|
if (!(width > 0 && height > 0)) continue;
|
|
1531
1398
|
const paragraph = image.closest('p');
|
|
1532
|
-
// geometry.imageOnly (layout/model.mjs).
|
|
1533
1399
|
const alone = !!paragraph && imageOnly(paragraph, image);
|
|
1534
1400
|
const baseLeft = parseFloat(css(image).marginLeft) || 0;
|
|
1535
1401
|
const rotated = geometry.rotatedBoundsRad(width, height, layout.rotate * Math.PI / 180);
|
|
@@ -1541,8 +1407,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1541
1407
|
}
|
|
1542
1408
|
}
|
|
1543
1409
|
|
|
1544
|
-
// A block that cannot wrap stands with its box just below the outline, as in the editor. Its own margin moves it, so
|
|
1545
|
-
// nothing is inserted between a picture and its words (a reader and the witnesses read nextElementSibling).
|
|
1546
1410
|
function clearTo(element, bottom, origin) {
|
|
1547
1411
|
for (let pass = 0; pass < 3; pass++) {
|
|
1548
1412
|
const distance = bottom - (box(element).top - origin);
|
|
@@ -1557,17 +1421,13 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1557
1421
|
if (metadata.wrapTextBlock(element)) return 'prose';
|
|
1558
1422
|
const image = element.tagName === 'P' ? element.querySelector('img') : null;
|
|
1559
1423
|
if (image && imageOnly(element, image)) return 'picture';
|
|
1560
|
-
// An empty paragraph is transparent to the owner search, as in the editor.
|
|
1561
1424
|
if (element.tagName === 'P' && !element.textContent.trim() && !image) return 'metadata';
|
|
1562
1425
|
return null;
|
|
1563
1426
|
}
|
|
1564
1427
|
|
|
1565
|
-
// Float fallback as the editor's floatAround.
|
|
1566
1428
|
const FLOAT_BLOCK = /^(P|H[1-6]|UL|OL|BLOCKQUOTE|DETAILS|SUMMARY|DL)$/;
|
|
1567
1429
|
function floatAround(element, width, height, top, obstacles) {
|
|
1568
|
-
// As the editor: a paragraph or heading floats whatever it holds; a list, quote or disclosure holding a rigid block does not.
|
|
1569
1430
|
if (!FLOAT_BLOCK.test(element.tagName) || !/^(P|H[1-6])$/.test(element.tagName) && element.querySelector('table, pre, figure, img, .math-rendered')) return false;
|
|
1570
|
-
// As the editor: a disclosure's box ends where the outline begins (layout/model.mjs boxClearance).
|
|
1571
1431
|
if (element.tagName === 'DETAILS') {
|
|
1572
1432
|
const computed = css(element), outer = box(element), floor = metadata.wrapColumnFloor(parseFloat(computed.fontSize) || 16);
|
|
1573
1433
|
const offset = (parseFloat(computed.paddingLeft) || 0) + (parseFloat(computed.borderLeftWidth) || 0);
|
|
@@ -1603,8 +1463,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1603
1463
|
const inside = obstacles.filter(obstacle => obstacle.y < bottom + 4096 && obstacle.y + obstacle.height > top &&
|
|
1604
1464
|
obstacle.x < width && obstacle.x + obstacle.width > 0);
|
|
1605
1465
|
if (!inside.length) return false;
|
|
1606
|
-
// layout/model.mjs wrapShape; `geometry` is modules["layout/model.mjs"]. As the editor, the start side reaches from
|
|
1607
|
-
// the box's own start edge.
|
|
1608
1466
|
const start = computed.direction === 'rtl' ? 'right' : 'left';
|
|
1609
1467
|
const startInset = start === 'left' ? (parseFloat(computed.paddingLeft) || 0) + (parseFloat(computed.borderLeftWidth) || 0)
|
|
1610
1468
|
: -((parseFloat(computed.paddingRight) || 0) + (parseFloat(computed.borderRightWidth) || 0));
|
|
@@ -1621,12 +1479,11 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1621
1479
|
const shape = shapes[side];
|
|
1622
1480
|
if (!shape) continue;
|
|
1623
1481
|
const float = doc.createElement('span');
|
|
1482
|
+
float.className = 'rapier-flow-float';
|
|
1624
1483
|
float.setAttribute('aria-hidden', 'true');
|
|
1625
1484
|
float.style.cssText = `float:${side};width:${pixels(shape.boxWidth)};height:${pixels(shape.boxHeight)};margin-top:${pixels(shape.startY)};shape-outside:${shape.shape};pointer-events:none`;
|
|
1626
1485
|
element.prepend(float); floats.push(float); placed.push([float, shape]);
|
|
1627
1486
|
}
|
|
1628
|
-
// As the editor: each float ends at the block's own content bottom and the block keeps its own display, so its
|
|
1629
|
-
// margins collapse as they do with no picture. A float the content does not reach goes.
|
|
1630
1487
|
const contentBottom = () => {
|
|
1631
1488
|
let first = element.firstChild;
|
|
1632
1489
|
while (first && placed.some(([float]) => float === first)) first = first.nextSibling;
|
|
@@ -1644,7 +1501,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1644
1501
|
}
|
|
1645
1502
|
if (!changed) break;
|
|
1646
1503
|
}
|
|
1647
|
-
// As the editor: a list item's marker and a quote's bar move with the words the floats moved.
|
|
1648
1504
|
const {markers, bars, steps: breaks} = geometry.edgeMarks(element, before);
|
|
1649
1505
|
for (const {item, shift} of markers) style(item, {'--md-marker': 'calc(' + (item.style.getPropertyValue('--md-marker') || '0px') + ' + ' + pixels(shift) + ')'});
|
|
1650
1506
|
for (const item of breaks) { item.setAttribute('data-rapier-wrap-step', ''); steps.push(item); }
|
|
@@ -1670,7 +1526,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1670
1526
|
if (!root.isConnected || printing || view.matchMedia?.('print').matches) return;
|
|
1671
1527
|
const children = [...root.children].filter(element => !element.hasAttribute('data-rapier-html-preview'));
|
|
1672
1528
|
if (children.length > 8192) return;
|
|
1673
|
-
// behind/front collapse and place like around/box but are never obstacles.
|
|
1674
1529
|
const outOfFlow = value => value?.wrap === 'behind' || value?.wrap === 'front';
|
|
1675
1530
|
const origin = box(root).top, obstacles = [], anchors = new Map(), wrapped = new Set(), placed = [], positionedImages = new Set();
|
|
1676
1531
|
for (const element of children) {
|
|
@@ -1688,7 +1543,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1688
1543
|
style(element, {position: 'relative', height: '0', 'min-height': '0', 'margin-top': '0', 'margin-bottom': '0', 'padding-top': '0', 'padding-bottom': '0', 'line-height': '0'});
|
|
1689
1544
|
style(image, {position: 'absolute', margin: '0'});
|
|
1690
1545
|
}
|
|
1691
|
-
// As the live editor.
|
|
1692
1546
|
syncInlineRotatedPictures(positionedImages);
|
|
1693
1547
|
for (const element of children) {
|
|
1694
1548
|
if (wrapped.has(element)) continue;
|
|
@@ -1698,7 +1552,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1698
1552
|
const width = bounds.width - inset - (parseFloat(computed.paddingRight) || 0) - (parseFloat(computed.borderRightWidth) || 0);
|
|
1699
1553
|
|
|
1700
1554
|
for (const {element: source, image, layout, width: imageWidth} of anchors.get(element) || []) {
|
|
1701
|
-
// A picture sized in lines (`lines`) reads its anchor's line and stands on the first line's cap height, as the live editor.
|
|
1702
1555
|
const lines = layout.lines != null ? geometry.lineMetrics(element) : null;
|
|
1703
1556
|
const unrotated = geometry.imageBox(width, image.naturalWidth, image.naturalHeight, layout, imageWidth, lines);
|
|
1704
1557
|
if (!unrotated) { restore(); return; }
|
|
@@ -1707,7 +1560,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1707
1560
|
const line = parseFloat(computed.lineHeight) || (parseFloat(computed.fontSize) || 16) * 1.6;
|
|
1708
1561
|
unrotated.height = metadata.objectHeight(unrotated.width, Number(image.dataset.rapierNaturalHeight) || box(image).height, attrs, line);
|
|
1709
1562
|
}
|
|
1710
|
-
// rasterReserved (layout/model.mjs).
|
|
1711
1563
|
const rasterRad = (layout.rotate || 0) * Math.PI / 180;
|
|
1712
1564
|
const fit = geometry.rasterReserved(width, unrotated, rasterRad);
|
|
1713
1565
|
if (!fit) { restore(); return; }
|
|
@@ -1717,10 +1569,8 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1717
1569
|
const topInset = (parseFloat(computed.paddingTop) || 0) + (parseFloat(computed.borderTopWidth) || 0);
|
|
1718
1570
|
const height = Math.max(0, bounds.height - topInset - (parseFloat(computed.paddingBottom) || 0) - (parseFloat(computed.borderBottomWidth) || 0));
|
|
1719
1571
|
let y = bounds.top - origin + topInset + geometry.linesTop(layout, lines) + Math.min(layout.y || 0, height / em) * em;
|
|
1720
|
-
// The standoff and the room between pictures, in the reference scale's pixels, as the live editor.
|
|
1721
1572
|
const scale = geometry.frameScale(image), room = 10 * scale;
|
|
1722
1573
|
if (!outOfFlow(layout)) {
|
|
1723
|
-
// A picture an earlier one stands in moves down alone; its owner keeps its place, as in the editor.
|
|
1724
1574
|
for (const obstacle of [...obstacles].sort((left, right) => left.y - right.y)) {
|
|
1725
1575
|
if (obstacle.x < left + rectangle.width + room && obstacle.x + obstacle.width > left - room &&
|
|
1726
1576
|
obstacle.y + obstacle.height > y - room && obstacle.y < y + rectangle.height + room)
|
|
@@ -1737,7 +1587,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1737
1587
|
const own = active.map(obstacle => ({...obstacle, x: obstacle.x - bounds.left - inset}));
|
|
1738
1588
|
const prepared = prepare(element);
|
|
1739
1589
|
if (!(prepared && width > 0 && project(prepared, width, top, own)) && !(width > 0 && floatAround(element, width, bounds.height, top, own))) {
|
|
1740
|
-
// As the editor: only an outline that meets the block's box moves it.
|
|
1741
1590
|
const overlapping = active.filter(obstacle => obstacle.y < top + bounds.height && obstacle.x < bounds.right && obstacle.x + obstacle.width > bounds.left);
|
|
1742
1591
|
if (overlapping.length) clearTo(element, Math.max(...overlapping.map(obstacle => obstacle.y + obstacle.height)), origin);
|
|
1743
1592
|
}
|
|
@@ -1748,7 +1597,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1748
1597
|
style(image, {position: 'absolute', left: pixels((visualLeft ?? left) - anchor.left), top: pixels((visualTop ?? y) - (anchor.top - origin)),
|
|
1749
1598
|
width: pixels(visualWidth ?? rectangle.width), height: pixels(visualHeight ?? rectangle.height), 'max-width': 'none', margin: '0',
|
|
1750
1599
|
transform: rotateDeg ? `rotate(${rotateDeg}deg)` : 'none', 'transform-origin': '50% 50%',
|
|
1751
|
-
// As the live editor.
|
|
1752
1600
|
...(wrap === 'behind' ? {'z-index': '-1'} : {})});
|
|
1753
1601
|
}
|
|
1754
1602
|
if (obstacles.length) {
|
|
@@ -1807,7 +1655,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1807
1655
|
|
|
1808
1656
|
function _rapierArtifactLayoutScript(root, nonce = '', {pictures = true} = {}) {
|
|
1809
1657
|
const dependencies = globalThis.RapierArtifactLayoutDependencies;
|
|
1810
|
-
// Every wrap value earns the reflow script, and an inline turned raster (keyed off data-rapier-image-layout).
|
|
1811
1658
|
const layoutOf = element => globalThis.RapierMarkdownLayout.parseLayoutAttribute(element.getAttribute('data-md-layout'));
|
|
1812
1659
|
const pictureLayoutOf = image => globalThis.RapierMarkdownLayout.parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
|
|
1813
1660
|
const wrapping = pictures && ([...root.querySelectorAll('p[data-md-layout]')].some(element => ['around', 'box', 'behind', 'front'].includes(layoutOf(element)?.wrap)) ||
|
|
@@ -1843,8 +1690,6 @@ function _rapierArtifactTypographyScript(root) {
|
|
|
1843
1690
|
return '(() => {\nconst typography = modules["layout/typography.mjs"].createTypography();\n' +
|
|
1844
1691
|
(pictures ? 'const capitals = (' + createPrintCapitals.toString() + ')((' + createLineMeasurer.toString() + ')(),(' + linesHeight.toString() + '),(' + followingPrintProse.toString() + '));\n' : '') +
|
|
1845
1692
|
'const root = document.querySelector("main.rapier-page"); const printMedia = matchMedia("print"); let frame = 0, width = -1, ready = null, printing = printMedia.matches;\n' +
|
|
1846
|
-
// beforeprint can still expose the screen viewport, not the selected paper's content width.
|
|
1847
|
-
// Only an authored paper width supplies a fixed target; automatic paper keeps native wrapping.
|
|
1848
1693
|
'const apply = () => { cancelAnimationFrame(frame); frame = 0; ' + (pictures ? 'if (printing) capitals.apply(root); else capitals.clear(root); ' : '') + 'if (printing && !(parseFloat(getComputedStyle(root).getPropertyValue("--rapier-print-width")) > 0)) { typography.clear(root); ready?.(); ready = null; return; } if (typography.apply(root, {all:printing}).pending) schedule(); else { ready?.(); ready = null; } };\n' +
|
|
1849
1694
|
'const schedule = () => { if (!frame) frame = requestAnimationFrame(apply); };\n' +
|
|
1850
1695
|
'globalThis.__RAPIER_EXPORT_READY__ = Promise.all([document.fonts?.ready, ...Array.from(root.querySelectorAll("img"), image => image.decode?.().catch(() => {}))]).then(() => new Promise(resolve => { ready = resolve; schedule(); }));\n' +
|