rapier-markdown-kit 1.2.8 → 1.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/will.mjs +0 -6
- package/dist/images/gif.mjs +0 -3
- package/dist/images/playback.mjs +0 -10
- package/dist/kit/assets.mjs +0 -1
- package/dist/kit/conformance/measurer.mjs +0 -0
- package/dist/kit/conformance/run.mjs +1 -8
- package/dist/kit/export-fonts.mjs +0 -14
- package/dist/kit/ledger/authored-origin.mjs +0 -5
- package/dist/kit/ledger/authorship.mjs +0 -2
- package/dist/kit/ledger/carried.mjs +4 -7
- package/dist/kit/ledger/data.mjs +0 -8
- package/dist/kit/ledger/format.mjs +0 -25
- package/dist/kit/ledger/hash.mjs +11 -7
- package/dist/kit/ledger/history.mjs +10 -29
- package/dist/kit/ledger/inverse-source.mjs +0 -46
- package/dist/kit/ledger/journal-records.mjs +24 -56
- package/dist/kit/ledger/merge.mjs +0 -27
- package/dist/kit/ledger/text-merge.mjs +0 -20
- package/dist/kit/ledger/transport.mjs +0 -47
- package/dist/kit/ledger.mjs +0 -4
- package/dist/kit/markdown-it.mjs +0 -3
- package/dist/kit/pdf-glyphs.mjs +1 -10
- package/dist/kit/pdf-source.mjs +5 -5
- package/dist/kit/pdf-vectors.mjs +0 -3
- package/dist/kit/print-capitals.mjs +0 -4
- package/dist/kit/render-entry.mjs +0 -1
- package/dist/kit/render-ink.mjs +0 -5
- package/dist/kit/render-markdown.mjs +12 -117
- package/dist/kit/render-print.mjs +30 -61
- package/dist/kit/render-sanitize.mjs +7 -59
- package/dist/kit/render-styles.mjs +0 -7
- package/dist/kit/render-work.mjs +0 -12
- package/dist/kit/render.mjs +46 -176
- 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 +5 -24
- package/dist/spec/frontmatter.mjs +0 -1
- package/dist/spec/html-blocks.mjs +0 -36
- package/dist/spec/md-assets.mjs +2 -61
- package/dist/spec/md-layout.mjs +0 -34
- package/dist/spec/md-marks.mjs +0 -51
- package/dist/spec/md-source.mjs +0 -16
- package/dist/spec/template-page.mjs +6 -0
- package/dist/spec/word-sidecar.mjs +0 -2
- package/package.json +1 -1
- package/style.css +32 -16
package/dist/kit/render.mjs
CHANGED
|
@@ -8,7 +8,7 @@ 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
|
-
|
|
11
|
+
import {isTemplatePage, isPageBorderLayout} from '../spec/template-page.mjs';
|
|
12
12
|
export function positionNativeDiagram(drawing) {
|
|
13
13
|
const surface=drawing?.closest?.('.diagram-block,.diagram-live-preview');
|
|
14
14
|
if(!surface?.clientWidth)return;
|
|
@@ -30,7 +30,6 @@ export function positionNativeDiagram(drawing) {
|
|
|
30
30
|
surface.scrollLeft=Math.max(0,Math.min(surface.scrollWidth-surface.clientWidth,left));
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// The document's renderer. DOM, codecs and host state are explicit inputs.
|
|
34
33
|
function createRenderer(runtime) {
|
|
35
34
|
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
35
|
let md = runtime.md;
|
|
@@ -66,7 +65,6 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
66
65
|
: _rapierProjectPortableRoot(context.semanticRoot, { baseName: base, keepDiagrams: true });
|
|
67
66
|
for (const element of portableRoot.children) if (!element.hasAttribute('lang')) element.setAttribute('lang', language);
|
|
68
67
|
if (fontCss || settings?.defaultfont) {
|
|
69
|
-
// Keep authored font scopes inside this fragment; no page wrapper or global settings rules.
|
|
70
68
|
const {main, sans: heading, mono: code} = documentSettingsFonts(settings), body = main || heading;
|
|
71
69
|
if (body) for (const element of portableRoot.children)
|
|
72
70
|
if (!element.matches('pre,code,kbd,samp,svg,math') && !element.style.fontFamily)
|
|
@@ -80,15 +78,12 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
80
78
|
css: _rapierArtifactStyles('system', false, false, false, '') + '\n' + _rapierDocumentSettingsCss(settings)});
|
|
81
79
|
const html = work ? await runtime._rapierPortableHtmlAsync(portableRoot, work) : _rapierPortableHtml(portableRoot);
|
|
82
80
|
return {
|
|
83
|
-
// Only validated data-font rules bypass the content sanitizer, which still rejects authored styles.
|
|
84
81
|
html: (fontCss ? '<style>' + fontCss + '</style>\n' : '') + html,
|
|
85
82
|
filename: base + '-publishing.html',
|
|
86
83
|
metadata,
|
|
87
84
|
};
|
|
88
85
|
}
|
|
89
86
|
|
|
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
87
|
const unavailable = plugin => Object.assign(new Error('Offline web page not written; install the ' + plugin + ' plug-in before exporting this document.'),
|
|
93
88
|
{code: 'offline_plugin_unavailable', plugin});
|
|
94
89
|
if (completePage && context.semanticRoot.querySelector('.math-placeholder[data-rapier-math-source]')) throw unavailable('math');
|
|
@@ -97,15 +92,11 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
97
92
|
if (!(completePage && retained && _rapierProviders?.mermaid?.status !== 'ready')) await _rapierFillDiagram(diagram, true);
|
|
98
93
|
if (!completePage || diagram.hasAttribute('data-diagram-unclosed')) continue;
|
|
99
94
|
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
95
|
if (diagram.getAttribute('data-diagram-state') !== 'ready' || !diagram.querySelector('.diagram-cache svg'))
|
|
103
96
|
throw Object.assign(new Error('Web page not written; a required diagram could not be rendered'), {code: 'EXPORT_DIAGRAM_UNAVAILABLE'});
|
|
104
97
|
}
|
|
105
98
|
|
|
106
99
|
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
100
|
if (context.semanticRoot.querySelector('span[data-rapier-math-source]'))
|
|
110
101
|
throw new Error('Web page not written; a required equation could not be rendered');
|
|
111
102
|
await _rapierRequireOfflinePageImages(context.semanticRoot);
|
|
@@ -119,11 +110,9 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
119
110
|
const styledRoot = work ? await _rapierProjectStyledRootAsync(context.semanticRoot, styledOptions)
|
|
120
111
|
: _rapierProjectStyledRoot(context.semanticRoot, styledOptions);
|
|
121
112
|
styledRoot.setAttribute('lang', language);
|
|
113
|
+
const templatePage = _rapierTemplatePage(context.canonical, settings, styledRoot);
|
|
122
114
|
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
115
|
if (!opts.print) _rapierAnnotateExportBoxPolygons(styledRoot);
|
|
126
|
-
// Paper keeps each picture's wrap through the same static float layout a page uses.
|
|
127
116
|
if (completePage || opts.print) {
|
|
128
117
|
if (work) await finishAsync(_rapierSharedPageLayoutSteps(styledRoot), work);
|
|
129
118
|
else _rapierSharedPageLayout(styledRoot);
|
|
@@ -134,7 +123,7 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
134
123
|
}
|
|
135
124
|
if (typeof opts.afterRoot === 'function') await opts.afterRoot(styledRoot);
|
|
136
125
|
markPrintCapitals(styledRoot);
|
|
137
|
-
const theme = opts.print
|
|
126
|
+
const theme = opts.print || templatePage
|
|
138
127
|
? 'light'
|
|
139
128
|
: _rapierArtifactPreference('theme', ['system', 'light', 'dark']);
|
|
140
129
|
const highlights = _rapierArtifactPreference('highlights', ['accent', 'standard']);
|
|
@@ -145,11 +134,6 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
145
134
|
const docClass = metadata.docKind === 'markdown'
|
|
146
135
|
? 'rapier-page md-render'
|
|
147
136
|
: '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
137
|
const includeFonts = !!opts.print;
|
|
154
138
|
const willFont = opts.print && opts.willFont ? opts.willFont : null;
|
|
155
139
|
const settingsCss = _rapierDocumentSettingsCss(settings);
|
|
@@ -161,13 +145,12 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
161
145
|
+ (printWidth > 0 ? '.rapier-page.md-render{width:' + printWidth + 'px!important}' : '') + '}' : '';
|
|
162
146
|
const pageTitle = settings && settings.title ? globalThis.RapierMarkdownSpec.documentTitle(settings) : metadata.filename;
|
|
163
147
|
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
148
|
+ (completePage || opts.print ? '\n\n' + _rapierSharedPageFallbackCss(!completePage) : '')
|
|
167
149
|
+ (opts.extraCss ? '\n\n' + opts.extraCss : '')
|
|
168
150
|
+ (willFont ? '\n\n@font-face{font-family:' + willFont.family + ';src:url(' + await _rapierBlobDataUrl(new Blob([willFont.bytes], { type: 'font/ttf' })) + ') format("truetype")}' : '')
|
|
169
151
|
+ (settingsCss ? '\n' + settingsCss : '')
|
|
170
|
-
+ (typographyPrintCss ? '\n' + typographyPrintCss : '')
|
|
152
|
+
+ (typographyPrintCss ? '\n' + typographyPrintCss : '')
|
|
153
|
+
+ (templatePage ? '\n' + templatePage.css : '');
|
|
171
154
|
let factoryFontCss = '';
|
|
172
155
|
if (!opts.print) {
|
|
173
156
|
if (fontIndex) await requireDocumentFonts(context.canonical, {parser: md, index: fontIndex, root: styledRoot, settings, css, fontCss, validateFont: runtime.validateFont});
|
|
@@ -184,13 +167,7 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
184
167
|
const bodyHtml = work
|
|
185
168
|
? await finishAsync(serializeTree(styledRoot), work) : styledRoot.innerHTML;
|
|
186
169
|
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
170
|
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
171
|
const layoutScript = opts.scripts !== false ? _rapierArtifactLayoutScript(styledRoot, nonce, {pictures:!opts.print}) : '';
|
|
195
172
|
const inkScript = !opts.print && opts.scripts !== false ? _rapierArtifactInkScript(styledRoot, nonce) : '';
|
|
196
173
|
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,18 +175,15 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
198
175
|
+ '<html lang="' + escapeRapierHtmlText(language) + '" data-rapier-theme="' + theme + '" data-highlights="' + highlights + '" data-invert-image-alpha="' + (invertImageAlpha ? 'on' : 'off') + '">\n'
|
|
199
176
|
+ '<head>\n<meta charset="UTF-8">\n' + (carrier ? RapierPageReturnAddress.PAGE_SEED + '\n' : '')
|
|
200
177
|
+ '<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
178
|
+ '<meta name="responsive-embedded-sizing" content="allow-origins=*">\n'
|
|
206
179
|
+ '<meta http-equiv="Content-Security-Policy" content="' + _rapierExportedPageCsp(layoutScript || inkScript || htmlScript ? nonce : '', includeFonts || !!fontCss || !!factoryFontCss) + '">\n'
|
|
207
180
|
+ '<meta name="referrer" content="no-referrer">\n'
|
|
208
181
|
+ '<link rel="icon" href="data:,">\n'
|
|
182
|
+
+ _rapierPagePreviewMeta(styledRoot, pageTitle)
|
|
209
183
|
+ '<meta name="generator" content="Rapier ' + escapeRapierHtmlText(version) + '">\n'
|
|
210
184
|
+ '<title>' + escapeRapierHtmlText(pageTitle) + '</title>\n'
|
|
211
185
|
+ '<style>' + css + '</style>\n</head>\n'
|
|
212
|
-
+ '<body class="' + bodyClass + '"
|
|
186
|
+
+ '<body class="' + bodyClass + '">' + (templatePage ? '<div class="rapier-template-border" aria-hidden="true"></div>' : '') + '<main class="' + docClass + '" data-md-theme="' + theme + '">'
|
|
213
187
|
+ bodyHtml + '</main>' + carrier + layoutScript + inkScript + htmlScript + '</body>\n</html>';
|
|
214
188
|
|
|
215
189
|
return {
|
|
@@ -220,6 +194,7 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
220
194
|
fontCss,
|
|
221
195
|
fontRequests: fontCss ? fontRequests(styledRoot, {settings, css, fontCss}) : [],
|
|
222
196
|
settings,
|
|
197
|
+
templatePage: templatePage && {url: templatePage.url, width: templatePage.width, height: templatePage.height, margin: templatePage.margin},
|
|
223
198
|
docClass,
|
|
224
199
|
language,
|
|
225
200
|
metadata,
|
|
@@ -227,11 +202,40 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
227
202
|
};
|
|
228
203
|
}
|
|
229
204
|
|
|
205
|
+
function _rapierTemplatePage(canonical, settings, root) {
|
|
206
|
+
if (!isTemplatePage(canonical) || !settings?.papersize) return null;
|
|
207
|
+
let url = '';
|
|
208
|
+
for (const paragraph of root.querySelectorAll(':scope > p[data-md-layout]')) {
|
|
209
|
+
const image = paragraph.querySelector('img');
|
|
210
|
+
const layout = globalThis.RapierMarkdownLayout.parseLayoutAttribute(paragraph.getAttribute('data-md-layout'));
|
|
211
|
+
const source = image?.getAttribute('src') || '';
|
|
212
|
+
if (isPageBorderLayout(layout) && /^data:image\/svg\+xml;base64,[A-Za-z0-9+/=]+$/.test(source)) { url = source; paragraph.remove(); break; }
|
|
213
|
+
}
|
|
214
|
+
if (!url) return null;
|
|
215
|
+
const width = settings.papersize.width / 15, height = settings.papersize.height / 15, margin = (settings.geometry?.twips ?? 1440) / 15;
|
|
216
|
+
const ground = 'background:var(--rapier-template-border) 0 0/100% 100% no-repeat';
|
|
217
|
+
const css = ':root{--rapier-template-border:url("' + url + '")}\n'
|
|
218
|
+
+ '@media screen{.rapier-template-border{display:none}body{background:#e8e8e8}.rapier-page{box-sizing:border-box;width:min(100%,' + width + 'px);min-height:' + height + 'px;margin:0 auto;padding:' + margin
|
|
219
|
+
+ 'px;background:var(--rapier-template-border) 0 0/100% ' + height + 'px repeat-y,#fff;color:#111}}\n'
|
|
220
|
+
+ '@media print{@page{size:' + width + 'px ' + height + 'px;margin:0}html,body{margin:0;-webkit-print-color-adjust:exact;print-color-adjust:exact}'
|
|
221
|
+
+ '.rapier-page{box-sizing:border-box;width:auto;max-width:none;margin:0;padding:' + margin + 'px!important;position:relative!important;z-index:1;box-decoration-break:clone;-webkit-box-decoration-break:clone}'
|
|
222
|
+
+ '.rapier-template-border{display:block;position:fixed;left:0;top:0;width:' + width + 'px;height:' + height + 'px;z-index:0;' + ground + '}}';
|
|
223
|
+
return {url, width, height, margin, css};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function _rapierPagePreviewMeta(root, fallbackTitle) {
|
|
227
|
+
const words = node => String(node?.textContent || '').replace(/\s+/g, ' ').trim();
|
|
228
|
+
const title = words(root.querySelector('h1,h2,h3,h4,h5,h6')) || String(fallbackTitle || '');
|
|
229
|
+
const text = [...root.querySelectorAll('p')].map(words).find(line => line.length >= 20) || '';
|
|
230
|
+
const sentence = /^.{20,300}?[.!?](?=\s|$)/.exec(text)?.[0] || (text.length <= 200 ? text : text.slice(0, 200).replace(/\s+\S*$/, '') + '…');
|
|
231
|
+
const meta = (key, name, value) => '<meta ' + key + '="' + name + '" content="' + escapeRapierHtmlText(value) + '">\n';
|
|
232
|
+
return (sentence ? meta('name', 'description', sentence) : '') + (title ? meta('property', 'og:title', title) : '') +
|
|
233
|
+
(sentence ? meta('property', 'og:description', sentence) : '');
|
|
234
|
+
}
|
|
235
|
+
|
|
230
236
|
async function _rapierBuildPublishedPage(context) {
|
|
231
237
|
if (typeof context?.canonical !== 'string' || !context.semanticRoot)
|
|
232
238
|
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
239
|
const current = {...context, ledger: null, carried: 'none', comparisonBase: null};
|
|
236
240
|
return _rapierBuildArtifact({
|
|
237
241
|
kind: 'standalone', scripts: false, extraCss: _rapierSharedPageFallbackCss(),
|
|
@@ -250,6 +254,11 @@ async function _rapierProjectStyledRootAsync(semanticRoot, options = {}) { retur
|
|
|
250
254
|
function* _rapierProjectStyledRootSteps(semanticRoot, options) {
|
|
251
255
|
const opts = options || {};
|
|
252
256
|
const root = yield* cloneTree(semanticRoot);
|
|
257
|
+
for (const token of root.querySelectorAll('.rapier-source-token--softbreak')) { yield;
|
|
258
|
+
if (token.getAttribute('data-rapier-source') !== '%0A') token.replaceWith(...token.childNodes);
|
|
259
|
+
else if (token.previousSibling?.classList?.contains('callout__label')) token.remove();
|
|
260
|
+
else token.replaceWith(root.ownerDocument.createTextNode(' '));
|
|
261
|
+
}
|
|
253
262
|
const metadata = opts.metadata || {};
|
|
254
263
|
|
|
255
264
|
if (metadata.docKind !== 'markdown') {
|
|
@@ -299,7 +308,6 @@ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
|
|
|
299
308
|
}
|
|
300
309
|
input.disabled = !!opts.print || !item;
|
|
301
310
|
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
311
|
input.removeAttribute('aria-disabled');
|
|
304
312
|
input.removeAttribute('aria-checked');
|
|
305
313
|
input.setAttribute('autocomplete', 'off');
|
|
@@ -315,8 +323,6 @@ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
|
|
|
315
323
|
}
|
|
316
324
|
|
|
317
325
|
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
326
|
const codes = Array.from(root.querySelectorAll('pre > code'));
|
|
321
327
|
for (const code of codes) {
|
|
322
328
|
const lang = _rapierLanguageClass(code);
|
|
@@ -345,8 +351,6 @@ function _rapierDocxRenderStyles(root) {
|
|
|
345
351
|
if (!paint) throw new Error('Word text colors could not be read.');
|
|
346
352
|
const hex = colour => {
|
|
347
353
|
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
354
|
paint.fillStyle = '#ffffff'; paint.fillRect(0, 0, 1, 1);
|
|
351
355
|
paint.fillStyle = colour; paint.fillRect(0, 0, 1, 1);
|
|
352
356
|
colours.set(colour, '#' + Array.from(paint.getImageData(0, 0, 1, 1).data).slice(0, 3)
|
|
@@ -361,7 +365,6 @@ function _rapierDocxRenderStyles(root) {
|
|
|
361
365
|
Array.from(probe.querySelectorAll('span')).forEach((span, index) => {
|
|
362
366
|
const style = into.defaultView.getComputedStyle(span), target = originals[index];
|
|
363
367
|
target.style.color = hex(style.color);
|
|
364
|
-
// Carry generated syntax emphasis as Word's semantic bold slot.
|
|
365
368
|
target.style.fontWeight ||= style.fontWeight === style.getPropertyValue('--md-fw-semibold').trim() ? 'bold' : style.fontWeight;
|
|
366
369
|
target.style.fontStyle = style.fontStyle;
|
|
367
370
|
});
|
|
@@ -371,8 +374,6 @@ function _rapierDocxRenderStyles(root) {
|
|
|
371
374
|
for (const quote of quotes) {
|
|
372
375
|
const probe = into.createElement('blockquote'); probe.className = quote.className;
|
|
373
376
|
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
377
|
const parent = quoteProbes.get(quote.parentElement?.closest('blockquote')) || surface;
|
|
377
378
|
parent.appendChild(probe); quoteProbes.set(quote, probe);
|
|
378
379
|
}
|
|
@@ -388,22 +389,17 @@ function _rapierPaintCode(el, html) {
|
|
|
388
389
|
|
|
389
390
|
function _rapierPrefixPortableAnchors(root, baseName) { return finish(_rapierPrefixPortableAnchorsSteps(root, baseName)); }
|
|
390
391
|
|
|
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
392
|
function _rapierPortableAnchorId(baseName, id) {
|
|
393
393
|
const slug = String(baseName || 'document').toLowerCase()
|
|
394
394
|
.replace(/[^a-z0-9]+/g, '-')
|
|
395
395
|
.replace(/^-+|-+$/g, '') || 'document';
|
|
396
|
-
return 'rapier-' + slug + '-' + (String(id).replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '') || 'anchor');
|
|
396
|
+
return 'rapier-anchor-' + slug + '-' + (String(id).replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '') || 'anchor');
|
|
397
397
|
}
|
|
398
398
|
|
|
399
399
|
function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
|
|
400
400
|
const ids = new Map(), svgIds = new Map();
|
|
401
401
|
const used = new Set();
|
|
402
402
|
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
403
|
if (typeof element?.closest !== 'function') return null;
|
|
408
404
|
let svg = element.closest('svg');
|
|
409
405
|
while (typeof svg?.parentElement?.closest === 'function' && svg.parentElement.closest('svg')) svg = svg.parentElement.closest('svg');
|
|
@@ -418,8 +414,6 @@ function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
|
|
|
418
414
|
let suffix = 2;
|
|
419
415
|
while (used.has(nextId)) nextId = first + '-' + suffix++;
|
|
420
416
|
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
417
|
if (!ids.has(oldId)) ids.set(oldId, nextId);
|
|
424
418
|
const svg = scopeOf(element);
|
|
425
419
|
if (svg) {
|
|
@@ -433,7 +427,6 @@ function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
|
|
|
433
427
|
for (const element of root.querySelectorAll('*')) { yield;
|
|
434
428
|
const svg = scopeOf(element), local = svgIds.get(svg);
|
|
435
429
|
const rewriteId = value => (svg ? local?.get(value) : ids.get(value)) || '';
|
|
436
|
-
// URL fragments percent-decode once; ARIA/for/headers are literal IDREFs.
|
|
437
430
|
const decodeFragment = value => { try { return decodeURIComponent(value); } catch (_) { return value; } };
|
|
438
431
|
const iriProperty = /^(?:fill|stroke|filter|clip-path|mask|marker(?:-start|-mid|-end)?|cursor)$/;
|
|
439
432
|
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 +455,6 @@ function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
|
|
|
462
455
|
if (next) element.setAttribute(name, next);
|
|
463
456
|
else element.removeAttribute(name);
|
|
464
457
|
} 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
458
|
for (const property of Array.from(element.style)) {
|
|
468
459
|
if (!iriProperty.test(property)) continue;
|
|
469
460
|
const before = element.style.getPropertyValue(property), next = rewriteUrls(before);
|
|
@@ -518,12 +509,6 @@ function _rapierAnnotateExportBoxPolygons(root) {
|
|
|
518
509
|
if (!recipe) continue;
|
|
519
510
|
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
511
|
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
512
|
try {
|
|
528
513
|
let glyphs = null;
|
|
529
514
|
if (recipe.shapes.some(shape => shape.recognized === 'text' || shape.recognized === 'paint' || shape.label)) {
|
|
@@ -572,8 +557,6 @@ function* _rapierSharedSourceFormsSteps(source, root, preparedDestinations = nul
|
|
|
572
557
|
const used = [], definitions = [];
|
|
573
558
|
for (const row of destinations) {
|
|
574
559
|
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
560
|
const image = byUrl.get(row.destination);
|
|
578
561
|
if (!image) continue;
|
|
579
562
|
let id = image.id;
|
|
@@ -608,7 +591,6 @@ async function _rapierSharedSourceCarrier(context, root) {
|
|
|
608
591
|
'"' + (forms.ids.length ? ' data-images="' + forms.ids.join(' ') + '"' : '') +
|
|
609
592
|
(forms.definitions.length ? ' data-image-definitions="' + forms.definitions.map(encodeURIComponent).join(' ') + '"' : '') + '>\n' +
|
|
610
593
|
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
594
|
(RapierLedgerCarried ? RapierLedgerCarried.writeParts(forms.resolved, _rapierLedgerParts(forms.resolved, context.ledger, context.carried)) +
|
|
613
595
|
RapierLedgerCarried.writeBase(forms.resolved, context.comparisonBase) : '');
|
|
614
596
|
}
|
|
@@ -622,10 +604,7 @@ function _rapierSharedResolve(text, ids, images, definitions) {
|
|
|
622
604
|
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
605
|
return found;
|
|
624
606
|
};
|
|
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
607
|
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
608
|
const definition = new RegExp('(^[ \\t>]*(?:(?:[-+*]|\\d{1,9}[.)])[ \\t]+[ \\t>]*)*\\[((?:\\\\.|[^\\]\\\\])+)\\]:[ \\t]*(?:(?:\\r\\n|\\r|\\n)[ \\t>]*)?(?:<)?)#' + id + '(?=>?[ \\t]*$|>?[ \\t]+["\'(])', 'gm');
|
|
630
609
|
const swapInline = (match, lead, value) => { const found = src(value); return found == null ? match : lead + found; };
|
|
631
610
|
const swapDefinition = (match, lead, label, value) => {
|
|
@@ -640,7 +619,6 @@ function _rapierSharedResolve(text, ids, images, definitions) {
|
|
|
640
619
|
async function _rapierReadSharedDocument(text, filename) {
|
|
641
620
|
const html = String(text);
|
|
642
621
|
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
622
|
const template = document.createElement('template');
|
|
645
623
|
template.innerHTML = html;
|
|
646
624
|
const invalid = () => { throw new Error('The editable source in this web page is incomplete or changed'); };
|
|
@@ -652,15 +630,12 @@ async function _rapierReadSharedDocument(text, filename) {
|
|
|
652
630
|
const definitionTokens = (carrier.getAttribute('data-image-definitions') || '').split(/\s+/).filter(Boolean);
|
|
653
631
|
if (!_rapierDocumentNameIsAdmissible(name) || !['markdown', 'text', 'code'].includes(kind) || !/^[0-9a-f]{64}$/.test(sha256) ||
|
|
654
632
|
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
633
|
let definitions;
|
|
657
634
|
try { definitions = new Set(definitionTokens.map(decodeURIComponent)); } catch (_) { return invalid(); }
|
|
658
635
|
let source = carrier.textContent;
|
|
659
636
|
if (source.startsWith('\n')) source = source.slice(1);
|
|
660
637
|
if (source.endsWith('\n')) source = source.slice(0, -1);
|
|
661
638
|
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
639
|
const images = new Map();
|
|
665
640
|
for (const element of template.content.querySelectorAll('img[id]')) images.set(element.id, images.has(element.id) ? null : element.getAttribute('src'));
|
|
666
641
|
const resolved = _rapierSharedResolve(source, ids, images, definitions);
|
|
@@ -686,7 +661,6 @@ function _rapierShareWrapKind(element) {
|
|
|
686
661
|
if (globalThis.RapierMarkdownLayout.wrapTextBlock(element)) return 'prose';
|
|
687
662
|
const image = element.tagName === 'P' ? element.querySelector('img') : null;
|
|
688
663
|
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
664
|
if (element.tagName === 'P' && !element.textContent.trim() && !image) return 'metadata';
|
|
691
665
|
return null;
|
|
692
666
|
}
|
|
@@ -701,14 +675,6 @@ function _rapierSharePictureParagraph(image) {
|
|
|
701
675
|
|
|
702
676
|
function _rapierSharedPageLayout(root) { return finish(_rapierSharedPageLayoutSteps(root)); }
|
|
703
677
|
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
678
|
const assignments = [];
|
|
713
679
|
for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
|
|
714
680
|
yield;
|
|
@@ -716,19 +682,9 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
716
682
|
if (layout?.wrap !== 'around' && layout?.wrap !== 'box') continue;
|
|
717
683
|
const paragraph = _rapierSharePictureParagraph(image);
|
|
718
684
|
if (!paragraph) continue;
|
|
719
|
-
// Prefer following text, then preceding text, looking past pictures and metadata.
|
|
720
685
|
const owner = globalThis.RapierMarkdownLayout.wrapNeighbour(paragraph, _rapierShareWrapKind);
|
|
721
686
|
if (owner) assignments.push({image, paragraph, layout, owner});
|
|
722
687
|
}
|
|
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
688
|
for (const {image, paragraph, layout, owner} of assignments) {
|
|
733
689
|
yield;
|
|
734
690
|
const side = (layout.x ?? 0) < 50 ? 'left' : 'right';
|
|
@@ -738,31 +694,9 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
738
694
|
paragraph.style.setProperty('shape-outside', 'inset(' + layout.y + 'em 0 0)');
|
|
739
695
|
}
|
|
740
696
|
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
697
|
const naturalWidth = Number(image.getAttribute('width')) || 0, naturalHeight = Number(image.getAttribute('height')) || 0;
|
|
757
698
|
if (layout.rotate && naturalWidth > 0 && naturalHeight > 0) {
|
|
758
699
|
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
700
|
image.style.maxWidth = 'none';
|
|
767
701
|
if (layout.width != null) {
|
|
768
702
|
let width = layout.width, height = width * aspect;
|
|
@@ -787,7 +721,6 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
787
721
|
image.style.marginLeft = image.style.marginRight = ((bounds.width - naturalWidth) / 2) + 'px';
|
|
788
722
|
}
|
|
789
723
|
} 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
724
|
image.style.width = 'auto';
|
|
792
725
|
image.style.height = globalThis.RapierMarkdownLayout.linesHeightCss(layout.lines);
|
|
793
726
|
} else {
|
|
@@ -796,32 +729,16 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
796
729
|
if (layout.width != null || image.getAttribute('width')) image.style.width = '100%';
|
|
797
730
|
}
|
|
798
731
|
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
732
|
if (layout.wrap === 'around' && !layout.rotate && !layout.y && !image.dataset.rapierContour?.endsWith('-box')) {
|
|
809
733
|
const src = image.getAttribute('src');
|
|
810
734
|
if (src) {
|
|
811
735
|
paragraph.style.setProperty('shape-outside', 'url("' + src + '")');
|
|
812
|
-
// Standard 10% alpha cutoff (`layout/model.mjs` alphaThreshold = .1).
|
|
813
736
|
paragraph.style.setProperty('shape-image-threshold', '0.1');
|
|
814
737
|
}
|
|
815
738
|
}
|
|
816
739
|
paragraph.classList.add('rapier-wrap-picture');
|
|
817
740
|
owner.before(paragraph);
|
|
818
741
|
}
|
|
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
742
|
const positionedAssignments = [];
|
|
826
743
|
for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
|
|
827
744
|
yield;
|
|
@@ -833,7 +750,6 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
833
750
|
for (const {image, paragraph, layout} of positionedAssignments) {
|
|
834
751
|
yield;
|
|
835
752
|
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
753
|
const x = layout.x, naturalWidth = Number(image.getAttribute('width')) || 0;
|
|
838
754
|
if (layout.width != null) image.style.width = layout.width + '%';
|
|
839
755
|
else if (naturalWidth > 0) image.style.width = naturalWidth + 'px';
|
|
@@ -843,14 +759,9 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
843
759
|
image.style.top = (layout.y || 0) + 'em';
|
|
844
760
|
image.style.margin = '0';
|
|
845
761
|
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
762
|
if (layout.wrap === 'behind') image.style.zIndex = '-1';
|
|
849
763
|
paragraph.classList.add('rapier-positioned-picture');
|
|
850
764
|
}
|
|
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
765
|
const positioned = new Set([...assignments, ...positionedAssignments].map(row => row.image));
|
|
855
766
|
for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
|
|
856
767
|
yield;
|
|
@@ -903,8 +814,6 @@ async function _rapierRequireOfflinePageImages(root) {
|
|
|
903
814
|
const picture = image.parentElement?.tagName.toLowerCase() === 'picture' && image.parentElement;
|
|
904
815
|
const alternative = picture && [...picture.querySelectorAll('source[srcset]')].some(source => source.getAttribute('srcset').trim());
|
|
905
816
|
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
817
|
for (let at = 0; at < srcset.length;) {
|
|
909
818
|
while (at < srcset.length && /[\t\n\f\r ,]/.test(srcset[at])) at++;
|
|
910
819
|
const start = at;
|
|
@@ -922,8 +831,6 @@ async function _rapierRequireOfflinePageImages(root) {
|
|
|
922
831
|
missing.push('Picture ' + (index + 1) + (alt ? ' — ' + alt : '') + ': ' + destination);
|
|
923
832
|
}
|
|
924
833
|
}
|
|
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
834
|
const sourceTags = new Set(['audio', 'video', 'source', 'track', 'input', 'iframe', 'embed', 'script']);
|
|
928
835
|
const cssAttributes = new Set(['style', 'fill', 'stroke', 'filter', 'clip-path', 'mask', 'marker-start', 'marker-mid', 'marker-end', 'cursor', 'color-profile']);
|
|
929
836
|
for (const element of root.querySelectorAll('*')) {
|
|
@@ -940,13 +847,10 @@ async function _rapierRequireOfflinePageImages(root) {
|
|
|
940
847
|
}
|
|
941
848
|
}
|
|
942
849
|
if (!missing.length) return;
|
|
943
|
-
// The writer refuses immediately; interactive callers own any explanatory sheet.
|
|
944
850
|
throw Object.assign(new Error('Web page not written; embed these pictures and try again. ' + missing.join('; ')),
|
|
945
851
|
{code: 'offline_image_unavailable'});
|
|
946
852
|
}
|
|
947
853
|
|
|
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
854
|
function _rapierCssValueUrls(value) {
|
|
951
855
|
const urls = [], functions = [];
|
|
952
856
|
let at = 0;
|
|
@@ -1022,10 +926,7 @@ function _rapierCssValueUrls(value) {
|
|
|
1022
926
|
|
|
1023
927
|
async function _rapierBuildSharedPage(captured) {
|
|
1024
928
|
const context = await _rapierPrepareInterchangeContext({kind: 'share'}, captured);
|
|
1025
|
-
// Share and Export write the same complete page, including native disclosures.
|
|
1026
929
|
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
930
|
const blob = new Blob([artifact.html], {type: 'text/html;charset=utf-8'});
|
|
1030
931
|
return {blob, filename: context.baseName + '.html'};
|
|
1031
932
|
}
|
|
@@ -1067,7 +968,6 @@ async function _rapierDataImageDestinationsAsync(source, work) {
|
|
|
1067
968
|
}
|
|
1068
969
|
|
|
1069
970
|
function _rapierImageDestinations(source, accepts) {
|
|
1070
|
-
// Only a rendered Markdown image consumes a definition; unused definitions stay literal source.
|
|
1071
971
|
if (!source.includes('![')) return [];
|
|
1072
972
|
const env = {}, tokens = md.parse(source, env), offsets = _rapierLineStartOffsets(source);
|
|
1073
973
|
const hrefs = _rapierImageSources(tokens), labels = new Set(), destinations = [], seen = new Set();
|
|
@@ -1354,7 +1254,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1354
1254
|
steps.length = 0;
|
|
1355
1255
|
}
|
|
1356
1256
|
|
|
1357
|
-
// One prepared rich paragraph retains original item text. Unsupported shaping keeps native flow.
|
|
1358
1257
|
function prepare(paragraph) {
|
|
1359
1258
|
const refuse = () => { prepared.forget(paragraph); return null; };
|
|
1360
1259
|
if (!/^(P|H[1-6])$/.test(paragraph.tagName) || !paragraph.textContent.trim() || paragraph.textContent.length > 8192 ||
|
|
@@ -1371,7 +1270,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1371
1270
|
!preparation.supportsRichStyle(computed, language) || computed.whiteSpace !== 'normal' ||
|
|
1372
1271
|
computed.transform !== 'none' ||
|
|
1373
1272
|
['::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
1273
|
['paddingLeft', 'paddingRight', 'paddingTop', 'borderLeftWidth', 'borderRightWidth', 'borderTopWidth']
|
|
1376
1274
|
.some(key => parseFloat(computed[key]) > 0)) return refuse();
|
|
1377
1275
|
const runs = [], items = [], walker = doc.createTreeWalker(paragraph, 4);
|
|
@@ -1417,11 +1315,9 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1417
1315
|
balance: 0};
|
|
1418
1316
|
}
|
|
1419
1317
|
|
|
1420
|
-
// An id survives on the first fragment only.
|
|
1421
1318
|
function fragmentNode(record, fragment, idsKept) {
|
|
1422
1319
|
const run = record.runs[fragment.itemIndex], mapped = geometry.mapFragment(run, fragment, {whiteSpace: record.whiteSpace});
|
|
1423
1320
|
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
1321
|
const kept = run.parents.length ? mapped.text.replace(/[ \t\n\r\f]+$/, '') : mapped.text;
|
|
1426
1322
|
const split = kept && kept.length < mapped.text.length;
|
|
1427
1323
|
let child = doc.createTextNode(split ? kept : mapped.text);
|
|
@@ -1488,8 +1384,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1488
1384
|
return found;
|
|
1489
1385
|
}
|
|
1490
1386
|
|
|
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
1387
|
function profile(image, layout) {
|
|
1494
1388
|
if (layout.wrap === 'around' && image.dataset?.rapierContour?.endsWith('-box')) return null;
|
|
1495
1389
|
const src = image.currentSrc || image.getAttribute('src') || '', rotateRad = (layout.rotate || 0) * Math.PI / 180;
|
|
@@ -1509,7 +1403,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1509
1403
|
return value;
|
|
1510
1404
|
}
|
|
1511
1405
|
|
|
1512
|
-
// wrap=box reads data-rapier-box-polygon; this page never parses a recipe or bundles Draw. No polygon: whole rectangle.
|
|
1513
1406
|
function boxProfile(image) {
|
|
1514
1407
|
const raw = image.getAttribute('data-rapier-box-polygon');
|
|
1515
1408
|
if (!raw) return null;
|
|
@@ -1520,7 +1413,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1520
1413
|
return geometry.polygonProfile(corners);
|
|
1521
1414
|
}
|
|
1522
1415
|
|
|
1523
|
-
// As the live editor (browser.js).
|
|
1524
1416
|
function syncInlineRotatedPictures(exclude) {
|
|
1525
1417
|
for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
|
|
1526
1418
|
if (exclude.has(image) || !image.complete || !image.naturalWidth || !image.naturalHeight) continue;
|
|
@@ -1529,7 +1421,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1529
1421
|
const width = image.offsetWidth, height = image.offsetHeight;
|
|
1530
1422
|
if (!(width > 0 && height > 0)) continue;
|
|
1531
1423
|
const paragraph = image.closest('p');
|
|
1532
|
-
// geometry.imageOnly (layout/model.mjs).
|
|
1533
1424
|
const alone = !!paragraph && imageOnly(paragraph, image);
|
|
1534
1425
|
const baseLeft = parseFloat(css(image).marginLeft) || 0;
|
|
1535
1426
|
const rotated = geometry.rotatedBoundsRad(width, height, layout.rotate * Math.PI / 180);
|
|
@@ -1541,8 +1432,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1541
1432
|
}
|
|
1542
1433
|
}
|
|
1543
1434
|
|
|
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
1435
|
function clearTo(element, bottom, origin) {
|
|
1547
1436
|
for (let pass = 0; pass < 3; pass++) {
|
|
1548
1437
|
const distance = bottom - (box(element).top - origin);
|
|
@@ -1557,17 +1446,13 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1557
1446
|
if (metadata.wrapTextBlock(element)) return 'prose';
|
|
1558
1447
|
const image = element.tagName === 'P' ? element.querySelector('img') : null;
|
|
1559
1448
|
if (image && imageOnly(element, image)) return 'picture';
|
|
1560
|
-
// An empty paragraph is transparent to the owner search, as in the editor.
|
|
1561
1449
|
if (element.tagName === 'P' && !element.textContent.trim() && !image) return 'metadata';
|
|
1562
1450
|
return null;
|
|
1563
1451
|
}
|
|
1564
1452
|
|
|
1565
|
-
// Float fallback as the editor's floatAround.
|
|
1566
1453
|
const FLOAT_BLOCK = /^(P|H[1-6]|UL|OL|BLOCKQUOTE|DETAILS|SUMMARY|DL)$/;
|
|
1567
1454
|
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
1455
|
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
1456
|
if (element.tagName === 'DETAILS') {
|
|
1572
1457
|
const computed = css(element), outer = box(element), floor = metadata.wrapColumnFloor(parseFloat(computed.fontSize) || 16);
|
|
1573
1458
|
const offset = (parseFloat(computed.paddingLeft) || 0) + (parseFloat(computed.borderLeftWidth) || 0);
|
|
@@ -1603,8 +1488,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1603
1488
|
const inside = obstacles.filter(obstacle => obstacle.y < bottom + 4096 && obstacle.y + obstacle.height > top &&
|
|
1604
1489
|
obstacle.x < width && obstacle.x + obstacle.width > 0);
|
|
1605
1490
|
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
1491
|
const start = computed.direction === 'rtl' ? 'right' : 'left';
|
|
1609
1492
|
const startInset = start === 'left' ? (parseFloat(computed.paddingLeft) || 0) + (parseFloat(computed.borderLeftWidth) || 0)
|
|
1610
1493
|
: -((parseFloat(computed.paddingRight) || 0) + (parseFloat(computed.borderRightWidth) || 0));
|
|
@@ -1621,12 +1504,11 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1621
1504
|
const shape = shapes[side];
|
|
1622
1505
|
if (!shape) continue;
|
|
1623
1506
|
const float = doc.createElement('span');
|
|
1507
|
+
float.className = 'rapier-flow-float';
|
|
1624
1508
|
float.setAttribute('aria-hidden', 'true');
|
|
1625
1509
|
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
1510
|
element.prepend(float); floats.push(float); placed.push([float, shape]);
|
|
1627
1511
|
}
|
|
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
1512
|
const contentBottom = () => {
|
|
1631
1513
|
let first = element.firstChild;
|
|
1632
1514
|
while (first && placed.some(([float]) => float === first)) first = first.nextSibling;
|
|
@@ -1644,7 +1526,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1644
1526
|
}
|
|
1645
1527
|
if (!changed) break;
|
|
1646
1528
|
}
|
|
1647
|
-
// As the editor: a list item's marker and a quote's bar move with the words the floats moved.
|
|
1648
1529
|
const {markers, bars, steps: breaks} = geometry.edgeMarks(element, before);
|
|
1649
1530
|
for (const {item, shift} of markers) style(item, {'--md-marker': 'calc(' + (item.style.getPropertyValue('--md-marker') || '0px') + ' + ' + pixels(shift) + ')'});
|
|
1650
1531
|
for (const item of breaks) { item.setAttribute('data-rapier-wrap-step', ''); steps.push(item); }
|
|
@@ -1670,7 +1551,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1670
1551
|
if (!root.isConnected || printing || view.matchMedia?.('print').matches) return;
|
|
1671
1552
|
const children = [...root.children].filter(element => !element.hasAttribute('data-rapier-html-preview'));
|
|
1672
1553
|
if (children.length > 8192) return;
|
|
1673
|
-
// behind/front collapse and place like around/box but are never obstacles.
|
|
1674
1554
|
const outOfFlow = value => value?.wrap === 'behind' || value?.wrap === 'front';
|
|
1675
1555
|
const origin = box(root).top, obstacles = [], anchors = new Map(), wrapped = new Set(), placed = [], positionedImages = new Set();
|
|
1676
1556
|
for (const element of children) {
|
|
@@ -1688,7 +1568,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1688
1568
|
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
1569
|
style(image, {position: 'absolute', margin: '0'});
|
|
1690
1570
|
}
|
|
1691
|
-
// As the live editor.
|
|
1692
1571
|
syncInlineRotatedPictures(positionedImages);
|
|
1693
1572
|
for (const element of children) {
|
|
1694
1573
|
if (wrapped.has(element)) continue;
|
|
@@ -1698,7 +1577,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1698
1577
|
const width = bounds.width - inset - (parseFloat(computed.paddingRight) || 0) - (parseFloat(computed.borderRightWidth) || 0);
|
|
1699
1578
|
|
|
1700
1579
|
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
1580
|
const lines = layout.lines != null ? geometry.lineMetrics(element) : null;
|
|
1703
1581
|
const unrotated = geometry.imageBox(width, image.naturalWidth, image.naturalHeight, layout, imageWidth, lines);
|
|
1704
1582
|
if (!unrotated) { restore(); return; }
|
|
@@ -1707,7 +1585,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1707
1585
|
const line = parseFloat(computed.lineHeight) || (parseFloat(computed.fontSize) || 16) * 1.6;
|
|
1708
1586
|
unrotated.height = metadata.objectHeight(unrotated.width, Number(image.dataset.rapierNaturalHeight) || box(image).height, attrs, line);
|
|
1709
1587
|
}
|
|
1710
|
-
// rasterReserved (layout/model.mjs).
|
|
1711
1588
|
const rasterRad = (layout.rotate || 0) * Math.PI / 180;
|
|
1712
1589
|
const fit = geometry.rasterReserved(width, unrotated, rasterRad);
|
|
1713
1590
|
if (!fit) { restore(); return; }
|
|
@@ -1717,10 +1594,8 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1717
1594
|
const topInset = (parseFloat(computed.paddingTop) || 0) + (parseFloat(computed.borderTopWidth) || 0);
|
|
1718
1595
|
const height = Math.max(0, bounds.height - topInset - (parseFloat(computed.paddingBottom) || 0) - (parseFloat(computed.borderBottomWidth) || 0));
|
|
1719
1596
|
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
1597
|
const scale = geometry.frameScale(image), room = 10 * scale;
|
|
1722
1598
|
if (!outOfFlow(layout)) {
|
|
1723
|
-
// A picture an earlier one stands in moves down alone; its owner keeps its place, as in the editor.
|
|
1724
1599
|
for (const obstacle of [...obstacles].sort((left, right) => left.y - right.y)) {
|
|
1725
1600
|
if (obstacle.x < left + rectangle.width + room && obstacle.x + obstacle.width > left - room &&
|
|
1726
1601
|
obstacle.y + obstacle.height > y - room && obstacle.y < y + rectangle.height + room)
|
|
@@ -1737,7 +1612,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1737
1612
|
const own = active.map(obstacle => ({...obstacle, x: obstacle.x - bounds.left - inset}));
|
|
1738
1613
|
const prepared = prepare(element);
|
|
1739
1614
|
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
1615
|
const overlapping = active.filter(obstacle => obstacle.y < top + bounds.height && obstacle.x < bounds.right && obstacle.x + obstacle.width > bounds.left);
|
|
1742
1616
|
if (overlapping.length) clearTo(element, Math.max(...overlapping.map(obstacle => obstacle.y + obstacle.height)), origin);
|
|
1743
1617
|
}
|
|
@@ -1748,7 +1622,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1748
1622
|
style(image, {position: 'absolute', left: pixels((visualLeft ?? left) - anchor.left), top: pixels((visualTop ?? y) - (anchor.top - origin)),
|
|
1749
1623
|
width: pixels(visualWidth ?? rectangle.width), height: pixels(visualHeight ?? rectangle.height), 'max-width': 'none', margin: '0',
|
|
1750
1624
|
transform: rotateDeg ? `rotate(${rotateDeg}deg)` : 'none', 'transform-origin': '50% 50%',
|
|
1751
|
-
// As the live editor.
|
|
1752
1625
|
...(wrap === 'behind' ? {'z-index': '-1'} : {})});
|
|
1753
1626
|
}
|
|
1754
1627
|
if (obstacles.length) {
|
|
@@ -1807,7 +1680,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1807
1680
|
|
|
1808
1681
|
function _rapierArtifactLayoutScript(root, nonce = '', {pictures = true} = {}) {
|
|
1809
1682
|
const dependencies = globalThis.RapierArtifactLayoutDependencies;
|
|
1810
|
-
// Every wrap value earns the reflow script, and an inline turned raster (keyed off data-rapier-image-layout).
|
|
1811
1683
|
const layoutOf = element => globalThis.RapierMarkdownLayout.parseLayoutAttribute(element.getAttribute('data-md-layout'));
|
|
1812
1684
|
const pictureLayoutOf = image => globalThis.RapierMarkdownLayout.parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
|
|
1813
1685
|
const wrapping = pictures && ([...root.querySelectorAll('p[data-md-layout]')].some(element => ['around', 'box', 'behind', 'front'].includes(layoutOf(element)?.wrap)) ||
|
|
@@ -1843,8 +1715,6 @@ function _rapierArtifactTypographyScript(root) {
|
|
|
1843
1715
|
return '(() => {\nconst typography = modules["layout/typography.mjs"].createTypography();\n' +
|
|
1844
1716
|
(pictures ? 'const capitals = (' + createPrintCapitals.toString() + ')((' + createLineMeasurer.toString() + ')(),(' + linesHeight.toString() + '),(' + followingPrintProse.toString() + '));\n' : '') +
|
|
1845
1717
|
'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
1718
|
'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
1719
|
'const schedule = () => { if (!frame) frame = requestAnimationFrame(apply); };\n' +
|
|
1850
1720
|
'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' +
|