rapier-markdown-kit 1.1.99 → 1.1.102
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -2
- package/README.md +42 -8
- package/dist/kit/export-fonts.mjs +403 -0
- package/dist/kit/ledger/data.mjs +1 -1
- package/dist/kit/ledger/format.mjs +12 -5
- package/dist/kit/ledger/hash.mjs +16 -22
- package/dist/kit/ledger/history.mjs +59 -276
- package/dist/kit/ledger/inverse-source.mjs +316 -0
- package/dist/kit/ledger/journal-records.mjs +116 -12
- package/dist/kit/ledger/merge.mjs +7 -4
- package/dist/kit/ledger/transport.mjs +123 -19
- package/dist/kit/pdf-glyphs.mjs +159 -0
- package/dist/kit/pdf-vectors.mjs +204 -0
- package/dist/kit/print-capitals.mjs +85 -0
- package/dist/kit/rapier-ledger.mjs +2 -2
- package/dist/kit/render-entry.mjs +1 -0
- package/dist/kit/render-ink.mjs +132 -0
- package/dist/kit/render-markdown.mjs +71 -17
- package/dist/kit/render-print.mjs +208 -60
- package/dist/kit/render-sanitize.mjs +23 -7
- package/dist/kit/render-styles.mjs +8 -12
- package/dist/kit/render.mjs +365 -133
- package/dist/layout/model.mjs +26 -20
- package/dist/layout/typography.mjs +364 -0
- package/dist/packages/rapier-diff/LICENSE +21 -0
- package/dist/packages/rapier-diff/index.mjs +610 -0
- package/dist/spec/document-fonts.mjs +244 -0
- package/dist/spec/document-settings.mjs +494 -0
- package/dist/spec/frontmatter.mjs +30 -0
- package/dist/spec/html-blocks.mjs +143 -0
- package/dist/spec/md-assets.mjs +77 -5
- package/dist/spec/md-layout.mjs +7 -3
- package/dist/spec/md-marks.mjs +114 -8
- package/package.json +1 -1
- package/style.css +110 -52
- package/dist/agent/diff.mjs +0 -52
package/dist/kit/render.mjs
CHANGED
|
@@ -1,6 +1,73 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {htmlImageRecord, mountHtmlBlocks} from '../spec/html-blocks.mjs';
|
|
2
3
|
import {finish, finishAsync, cloneTree, cleanTree, serializeTree, encodeUtf8Steps, pause} from './render-work.mjs';
|
|
4
|
+
import {formatFontOpen, fontFamilyCss, FONT_CLOSE} from '../spec/md-marks.mjs';
|
|
5
|
+
import {readFontAssets, documentFontsCss} from '../spec/document-fonts.mjs';
|
|
6
|
+
import {fontRequests, requireDocumentFonts} from './export-fonts.mjs';
|
|
7
|
+
import {documentLanguage, documentSettingsFonts} from '../spec/document-settings.mjs';
|
|
8
|
+
import {createTypography} from '../layout/typography.mjs';
|
|
9
|
+
import {createLineMeasurer, linesHeight} from '../layout/model.mjs';
|
|
10
|
+
import {markPrintCapitals, createPrintCapitals, followingPrintProse} from './print-capitals.mjs';
|
|
11
|
+
import {freezePublishedInk} from './render-ink.mjs';
|
|
3
12
|
// The document's renderer. DOM, codecs and host state are explicit inputs.
|
|
13
|
+
// Rapier generates attribute selectors for dark paper. Project those owned rules onto their
|
|
14
|
+
// SVG elements before export removes stylesheets; the inert host and the editor share this pass.
|
|
15
|
+
export function presentNativeDiagram(drawing) {
|
|
16
|
+
const winners=new Map();
|
|
17
|
+
for(const style of drawing.querySelectorAll('style')){
|
|
18
|
+
const media=/^\s*@media\s*\(\s*prefers-color-scheme\s*:\s*dark\s*\)\s*\{([\s\S]*)\}\s*$/.exec(style.textContent);
|
|
19
|
+
if(media)for(const rule of media[1].matchAll(/([^{}]+)\{([^{}]*)\}/g)){
|
|
20
|
+
// The browser's CSS sanitizer serializes owned hex colours as rgb()/rgba().
|
|
21
|
+
const properties=[...rule[2].matchAll(/(?:^|;)\s*(fill|stroke|color|stop-color)\s*:\s*(#[\da-f]{3,8}|rgba?\([\d.,%/\s]+\)|none)\s*(?:!important)?(?=;|$)/gi)];
|
|
22
|
+
for(const selector of rule[1].split(',')){
|
|
23
|
+
// Draw's selectors contain only attributes, descendants and immediate children.
|
|
24
|
+
if(!/^\s*\[[^\[\]{};]+\](?:\s*(?:>\s*)?\[[^\[\]{};]+\])*\s*$/.test(selector))continue;
|
|
25
|
+
const rank=(selector.match(/\[/g)||[]).length;
|
|
26
|
+
for(const element of [...(drawing.matches(selector)?[drawing]:[]),...drawing.querySelectorAll(selector)]){
|
|
27
|
+
let paints=winners.get(element);if(!paints)winners.set(element,paints=new Map());
|
|
28
|
+
for(const [,property,dark] of properties){const light=element.getAttribute(property);
|
|
29
|
+
if(light&&(paints.get(property)?.rank??-1)<=rank)paints.set(property,{light,dark,rank});}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
style.remove();
|
|
34
|
+
}
|
|
35
|
+
for(const [element,paints] of winners)for(const [property,{light,dark}] of paints)
|
|
36
|
+
element.style.setProperty(property,'light-dark('+light+', '+dark+')','important');
|
|
37
|
+
drawing.classList.add('rapier-native-flowchart');drawing.setAttribute('role','img');
|
|
38
|
+
drawing.setAttribute('aria-roledescription',drawing.getAttribute('data-rapier-diagram-kind')||'flowchart');
|
|
39
|
+
const width=Number(drawing.getAttribute('width')),
|
|
40
|
+
sizes=[...drawing.querySelectorAll('text[font-size]')].map(text=>Number(text.getAttribute('font-size'))).filter(size=>Number.isFinite(size)&&size>0),
|
|
41
|
+
minimum=sizes.reduce((smallest,size)=>Math.min(smallest,size),Infinity);
|
|
42
|
+
if(Number.isFinite(width)&&width>0){
|
|
43
|
+
const readable=Math.ceil(width*14/(Number.isFinite(minimum)?minimum:14)*1000)/1000;
|
|
44
|
+
drawing.style.width='clamp('+readable+'px, 100%, '+width+'px)';
|
|
45
|
+
}
|
|
46
|
+
drawing.style.maxWidth='none';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Set the first view once. Subsequent rendering keeps the person's horizontal position.
|
|
50
|
+
export function positionNativeDiagram(drawing) {
|
|
51
|
+
const surface=drawing?.closest?.('.diagram-block,.diagram-live-preview');
|
|
52
|
+
if(!surface?.clientWidth)return;
|
|
53
|
+
if(surface._rapierDiagramScroll){
|
|
54
|
+
surface.scrollLeft=surface._rapierDiagramScroll.left;
|
|
55
|
+
surface.scrollTop=surface._rapierDiagramScroll.top;
|
|
56
|
+
delete surface._rapierDiagramScroll;
|
|
57
|
+
surface._rapierDiagramPositioned=true;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if(surface._rapierDiagramPositioned)return;
|
|
61
|
+
surface._rapierDiagramPositioned=true;
|
|
62
|
+
if(surface.scrollLeft||!drawing.hasAttribute('data-rapier-diagram-start-x'))return;
|
|
63
|
+
const start=Number(drawing.getAttribute('data-rapier-diagram-start-x')),
|
|
64
|
+
view=(drawing.getAttribute('viewBox')||'').trim().split(/[\s,]+/).map(Number);
|
|
65
|
+
if(!Number.isFinite(start)||view.length!==4||!(view[2]>0))return;
|
|
66
|
+
const bounds=drawing.getBoundingClientRect(),host=surface.getBoundingClientRect(),
|
|
67
|
+
left=bounds.left-host.left+(start-view[0])*bounds.width/view[2]-surface.clientWidth/2;
|
|
68
|
+
surface.scrollLeft=Math.max(0,Math.min(surface.scrollWidth-surface.clientWidth,left));
|
|
69
|
+
}
|
|
70
|
+
|
|
4
71
|
function createRenderer(runtime) {
|
|
5
72
|
const {RAPIER_COLOR_CLOSE, RapierLedgerCarried, RapierPageReturnAddress, RapierTextCodec, _rapierArtifactHighlight, _rapierArtifactPreference, _rapierArtifactStyles, _rapierBlobDataUrl, _rapierBuildInterchangeContext, _rapierDocumentNameIsAdmissible, _rapierLedgerParts, _rapierDrawReadSVGRecipe, _rapierDrawShapeProfileFor, _rapierExportFontCss, _rapierFillDiagram, _rapierFinalizeExportCode, _rapierFormatColorOpen, _rapierLanguageClass, _rapierLineStartOffsets, _rapierMarkdownEnvironment, _rapierPortableHtml, _rapierPrepareInterchangeContext, _rapierProjectPortableRoot, _rapierProviders, _rapierSourceCharEscaped, _rapierSourceLineSpan, crypto, document, escapeRapierHtmlText, globalThis, rapierConfirm, sanitizeRapierHtml} = runtime;
|
|
6
73
|
let md = runtime.md;
|
|
@@ -9,7 +76,9 @@ function createRenderer(runtime) {
|
|
|
9
76
|
const filename = options.filename || 'document.md';
|
|
10
77
|
if (typeof filename !== 'string' || !filename || /[\\/\u0000-\u001f\u007f]/.test(filename)) throw new TypeError('Invalid document filename');
|
|
11
78
|
const bom = source.charCodeAt(0) === 0xfeff;
|
|
12
|
-
|
|
79
|
+
let canonical = RapierTextCodec.normalizeDocument(source);
|
|
80
|
+
if ((!options.docKind || options.docKind === 'markdown') && runtime.materializeDocumentFonts)
|
|
81
|
+
canonical = await runtime.materializeDocumentFonts(canonical, {parser: md});
|
|
13
82
|
const captured = {canonical, metadata: {filename, docKind: options.docKind || 'markdown', codeLang: options.codeLang || '', bom}, baseName: filename.replace(/\.[^.]*$/, '') || 'document'};
|
|
14
83
|
const written = await _rapierBuildSharedPage(captured);
|
|
15
84
|
return {html: await written.blob.text(), filename: written.filename};
|
|
@@ -20,14 +89,36 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
20
89
|
const metadata = context.metadata;
|
|
21
90
|
const base = context.baseName;
|
|
22
91
|
const work = context.work || opts.work;
|
|
92
|
+
const completePage = opts.kind === 'standalone' || opts.kind === 'page';
|
|
93
|
+
const settings = _rapierDocumentSettingsOf(context.canonical);
|
|
94
|
+
const language = documentLanguage(settings, globalThis.navigator?.language);
|
|
95
|
+
const fontIndex = metadata.docKind === 'markdown' ? readFontAssets(context.canonical, md) : null;
|
|
96
|
+
const fontAssets = fontIndex?.assets || [];
|
|
97
|
+
const fontCss = documentFontsCss(fontAssets);
|
|
23
98
|
|
|
24
99
|
if (opts.kind === 'publishing' || opts.kind === 'fragment') {
|
|
25
100
|
for (const diagram of context.semanticRoot.querySelectorAll('.diagram-block[data-diagram-native]'))
|
|
26
101
|
await _rapierFillDiagram(diagram, true);
|
|
27
102
|
const portableRoot = work ? await runtime._rapierProjectPortableRootAsync(context.semanticRoot, {baseName: base, keepDiagrams: true, work})
|
|
28
103
|
: _rapierProjectPortableRoot(context.semanticRoot, { baseName: base, keepDiagrams: true });
|
|
104
|
+
for (const element of portableRoot.children) if (!element.hasAttribute('lang')) element.setAttribute('lang', language);
|
|
105
|
+
if (fontCss || settings?.defaultfont) {
|
|
106
|
+
// Keep authored font scopes inside this fragment; no page wrapper or global settings rules.
|
|
107
|
+
const {main, sans: heading, mono: code} = documentSettingsFonts(settings), body = main || heading;
|
|
108
|
+
if (body) for (const element of portableRoot.children)
|
|
109
|
+
if (!element.matches('pre,code,kbd,samp,svg,math') && !element.style.fontFamily)
|
|
110
|
+
element.style.fontFamily = body.css || fontFamilyCss(body.family);
|
|
111
|
+
if (heading) for (const element of portableRoot.querySelectorAll('h1,h2,h3,h4,h5,h6'))
|
|
112
|
+
element.style.fontFamily = heading.css || fontFamilyCss(heading.family);
|
|
113
|
+
if (code) for (const element of portableRoot.querySelectorAll('pre,code,kbd,samp'))
|
|
114
|
+
element.style.fontFamily = code.css || fontFamilyCss(code.family);
|
|
115
|
+
}
|
|
116
|
+
if (fontIndex) await requireDocumentFonts(context.canonical, {parser: md, index: fontIndex, root: portableRoot, settings, fontCss, validateFont: runtime.validateFont,
|
|
117
|
+
css: _rapierArtifactStyles('system', false, false, false, '') + '\n' + _rapierDocumentSettingsCss(settings)});
|
|
118
|
+
const html = work ? await runtime._rapierPortableHtmlAsync(portableRoot, work) : _rapierPortableHtml(portableRoot);
|
|
29
119
|
return {
|
|
30
|
-
|
|
120
|
+
// Only validated data-font rules bypass the content sanitizer, which still rejects authored styles.
|
|
121
|
+
html: (fontCss ? '<style>' + fontCss + '</style>\n' : '') + html,
|
|
31
122
|
filename: base + '-publishing.html',
|
|
32
123
|
metadata,
|
|
33
124
|
};
|
|
@@ -35,46 +126,53 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
35
126
|
|
|
36
127
|
// The semantic export root is detached and has never entered the live visibility queue.
|
|
37
128
|
// Await the same materialization owner before any projection clones or serializes it.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
129
|
+
const unavailable = plugin => Object.assign(new Error('Offline web page not written; install the ' + plugin + ' plug-in before exporting this document.'),
|
|
130
|
+
{code: 'offline_plugin_unavailable', plugin});
|
|
131
|
+
if (completePage && context.semanticRoot.querySelector('.math-placeholder[data-rapier-math-source]')) throw unavailable('math');
|
|
132
|
+
for (const diagram of context.semanticRoot.querySelectorAll('.diagram-block[data-diagram-src]')) {
|
|
133
|
+
const retained = diagram.getAttribute('data-diagram-state') === 'ready' && diagram.querySelector('.diagram-cache')?.querySelector('svg');
|
|
134
|
+
if (!(completePage && retained && _rapierProviders?.mermaid?.status !== 'ready')) await _rapierFillDiagram(diagram, true);
|
|
135
|
+
if (!completePage || diagram.hasAttribute('data-diagram-unclosed')) continue;
|
|
136
|
+
if (diagram.getAttribute('data-diagram-state') === 'absent') throw unavailable('mermaid');
|
|
42
137
|
// A completed diagram must have its drawing. Syntax and renderer failures both refuse
|
|
43
138
|
// the file; an unclosed fence above is still authored source, not a completed diagram.
|
|
44
|
-
if (state !== 'ready' || !diagram.querySelector('.diagram-cache svg'))
|
|
139
|
+
if (diagram.getAttribute('data-diagram-state') !== 'ready' || !diagram.querySelector('.diagram-cache svg'))
|
|
45
140
|
throw Object.assign(new Error('Web page not written; a required diagram could not be rendered'), {code: 'EXPORT_DIAGRAM_UNAVAILABLE'});
|
|
46
141
|
}
|
|
47
142
|
|
|
48
|
-
if (
|
|
49
|
-
if (context.semanticRoot.querySelector('.math-placeholder'))
|
|
50
|
-
throw new Error('Web page not written; the math plug-in is required to include its equations');
|
|
143
|
+
if (completePage) {
|
|
51
144
|
// Invalid TeX renders as a static error SVG. A source-only span means
|
|
52
145
|
// rendering itself failed, so publishing it would silently omit the equation.
|
|
53
146
|
if (context.semanticRoot.querySelector('span[data-rapier-math-source]'))
|
|
54
147
|
throw new Error('Web page not written; a required equation could not be rendered');
|
|
148
|
+
await _rapierRequireOfflinePageImages(context.semanticRoot);
|
|
149
|
+
}
|
|
150
|
+
if (!completePage) for (const image of context.semanticRoot.querySelectorAll('svg image')) {
|
|
151
|
+
const href = image.getAttribute('data-rapier-remote-src') || image.getAttribute('href') || image.getAttribute('xlink:href') || '';
|
|
152
|
+
if (!globalThis.RapierImageAssets.dataImage(href)) throw new Error('Embed the diagram picture before exporting an offline page: ' + href);
|
|
55
153
|
}
|
|
56
|
-
if (opts.kind === 'standalone' || opts.kind === 'page') await _rapierRequireOfflinePageImages(context.semanticRoot);
|
|
57
154
|
|
|
58
|
-
const styledOptions = {metadata, print: !!opts.print,
|
|
155
|
+
const styledOptions = {metadata, print: !!opts.print, baseName: base, work};
|
|
59
156
|
const styledRoot = work ? await _rapierProjectStyledRootAsync(context.semanticRoot, styledOptions)
|
|
60
157
|
: _rapierProjectStyledRoot(context.semanticRoot, styledOptions);
|
|
61
|
-
|
|
158
|
+
styledRoot.setAttribute('lang', language);
|
|
159
|
+
if (completePage && typeof _rapierFinalizeExportCode === 'function') await _rapierFinalizeExportCode(styledRoot);
|
|
62
160
|
// _rapierProjectStyledRoot already returns the sanitized projection.
|
|
63
161
|
// Print never emits the layout reflow script (see below), so a print artifact has no use for
|
|
64
162
|
// this annotation -- skip it there rather than pay the cost for numbers nothing will read.
|
|
65
|
-
if (!opts.print
|
|
66
|
-
|
|
67
|
-
|
|
163
|
+
if (!opts.print) _rapierAnnotateExportBoxPolygons(styledRoot);
|
|
164
|
+
if (completePage && !opts.print) {
|
|
165
|
+
if (work) {
|
|
166
|
+
await finishAsync(_rapierSharedPageLayoutSteps(styledRoot), work);
|
|
167
|
+
await finishAsync(_rapierShareCapLongCodeBlocksSteps(styledRoot), work);
|
|
168
|
+
} else { _rapierSharedPageLayout(styledRoot); _rapierShareCapLongCodeBlocks(styledRoot); }
|
|
169
|
+
}
|
|
68
170
|
if (typeof opts.afterRoot === 'function') await opts.afterRoot(styledRoot);
|
|
69
|
-
|
|
70
|
-
const bodyHtml = opts.kind === 'page' ? _rapierPageHtml(styledRoot) : work
|
|
71
|
-
? await finishAsync(serializeTree(styledRoot), work) : styledRoot.innerHTML;
|
|
72
|
-
if (work) await pause(work, .95);
|
|
171
|
+
markPrintCapitals(styledRoot);
|
|
73
172
|
const theme = opts.print
|
|
74
173
|
? 'light'
|
|
75
|
-
: opts.kind === 'page' ? 'system'
|
|
76
174
|
: _rapierArtifactPreference('theme', ['system', 'light', 'dark']);
|
|
77
|
-
const highlights =
|
|
175
|
+
const highlights = _rapierArtifactPreference('highlights', ['accent', 'standard']);
|
|
78
176
|
const versionMeta = document.querySelector('meta[name="rapier-version"]');
|
|
79
177
|
const version = versionMeta && versionMeta.content ? versionMeta.content.trim() : '0.0.0';
|
|
80
178
|
const bodyClass = theme === 'light' ? 'light' : '';
|
|
@@ -85,23 +183,41 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
85
183
|
// .md-render set and the highlight rules, so a numbered list, a checkbox, a callout and a
|
|
86
184
|
// table are drawn in an exported page exactly as they are drawn here. The one caller with
|
|
87
185
|
// rules of its own -- Share, for the no-script float it alone writes -- appends them.
|
|
88
|
-
|
|
186
|
+
// Print retains its existing factory resources. HTML adds only the optional small Sans subset below.
|
|
187
|
+
const includeFonts = !!opts.print;
|
|
89
188
|
const zenFontCss = opts.print && styledRoot.querySelector('svg[aria-roledescription="zenuml"]')
|
|
90
189
|
? (_rapierProviders?.mermaid?.exportFontCss?.() || '') : '';
|
|
91
190
|
const willFont = opts.print && opts.willFont ? opts.willFont : null;
|
|
92
|
-
const settings = _rapierDocumentSettingsOf(context.canonical);
|
|
93
191
|
const settingsCss = _rapierDocumentSettingsCss(settings);
|
|
192
|
+
const refined = styledRoot.querySelector('p[data-md-hyphens="auto"],p[data-md-lines],p[data-md-caps="small"],p[data-md-hanging="on"],img[data-rapier-print-lines]');
|
|
193
|
+
const printMargin = settings?.geometry ? settings.geometry.twips / 15 : 14 * 96 / 25.4;
|
|
194
|
+
const printWidth = settings?.papersize ? settings.papersize.width / 15 - 2 * printMargin : null;
|
|
195
|
+
const typographyPrintCss = refined ? '@media print{.md-render{--rapier-print-margin-left:' + printMargin + 'px;--rapier-print-margin-right:' + printMargin + 'px'
|
|
196
|
+
+ (printWidth > 0 ? ';--rapier-print-width:' + printWidth + 'px' : '') + '}'
|
|
197
|
+
+ (printWidth > 0 ? '.rapier-page.md-render{width:' + printWidth + 'px!important}' : '') + '}' : '';
|
|
94
198
|
const pageTitle = settings && settings.title ? globalThis.RapierMarkdownSpec.documentTitle(settings) : metadata.filename;
|
|
95
|
-
let css = _rapierArtifactStyles(theme, includeFonts, !!opts.print ||
|
|
96
|
-
+ (
|
|
199
|
+
let css = _rapierArtifactStyles(theme, includeFonts, !!opts.print || completePage, false, opts.print && opts.kind === 'standalone' ? '' : null)
|
|
200
|
+
+ (completePage ? '\n\n' + _rapierSharedPageFallbackCss() : '')
|
|
97
201
|
+ (opts.extraCss ? '\n\n' + opts.extraCss : '')
|
|
98
202
|
+ (willFont ? '\n\n@font-face{font-family:' + willFont.family + ';src:url(' + await _rapierBlobDataUrl(new Blob([willFont.bytes], { type: 'font/ttf' })) + ') format("truetype")}' : '')
|
|
99
|
-
+ (settingsCss ? '\n' + settingsCss : '')
|
|
100
|
-
|
|
101
|
-
|
|
203
|
+
+ (settingsCss ? '\n' + settingsCss : '')
|
|
204
|
+
+ (typographyPrintCss ? '\n' + typographyPrintCss : '');
|
|
205
|
+
let factoryFontCss = '';
|
|
206
|
+
if (!opts.print) {
|
|
207
|
+
if (fontIndex) await requireDocumentFonts(context.canonical, {parser: md, index: fontIndex, root: styledRoot, settings, css, fontCss, validateFont: runtime.validateFont});
|
|
208
|
+
if (typeof _rapierExportFontCss === 'function')
|
|
209
|
+
factoryFontCss = await _rapierExportFontCss(styledRoot, {settings, css, theme, portableFactory: true, customFontCss: fontCss});
|
|
210
|
+
if (factoryFontCss) css += '\n' + factoryFontCss;
|
|
211
|
+
} else if (opts.kind === 'standalone') {
|
|
102
212
|
const fonts = await _rapierExportFontCss(styledRoot, {settings, css, theme});
|
|
103
213
|
if (fonts) css += '\n' + fonts;
|
|
104
214
|
}
|
|
215
|
+
if (fontCss) css += '\n' + fontCss;
|
|
216
|
+
if (typeof opts.afterFonts === 'function') await opts.afterFonts(styledRoot, {css, fontCss, settings, language, theme});
|
|
217
|
+
const carrier = completePage ? await _rapierSharedSourceCarrier(context, styledRoot) : '';
|
|
218
|
+
const bodyHtml = work
|
|
219
|
+
? await finishAsync(serializeTree(styledRoot), work) : styledRoot.innerHTML;
|
|
220
|
+
if (work) await pause(work, .95);
|
|
105
221
|
// Every written page is offline. Only the writer's nonce script may run; it, pictures,
|
|
106
222
|
// styles and other page resources get no network authority. Following an ordinary link
|
|
107
223
|
// is the reader's navigation, with no Referer, not a background resource request.
|
|
@@ -109,10 +225,12 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
109
225
|
// 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
|
|
110
226
|
// 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
|
|
111
227
|
// nonce out of it and be admitted.
|
|
112
|
-
const layoutScript = !opts.print && opts.
|
|
113
|
-
const inkScript = !opts.print && opts.
|
|
228
|
+
const layoutScript = !opts.print && opts.scripts !== false ? _rapierArtifactLayoutScript(styledRoot, nonce) : '';
|
|
229
|
+
const inkScript = !opts.print && opts.scripts !== false ? _rapierArtifactInkScript(styledRoot, nonce) : '';
|
|
230
|
+
const typographyScript = opts.scripts !== false ? _rapierArtifactTypographyScript(styledRoot, nonce) : '';
|
|
231
|
+
const htmlScript = opts.scripts !== false && styledRoot.querySelector('[data-rapier-html]') ? '<script nonce="' + nonce + '">(' + mountHtmlBlocks.toString().replace(/<\/script/gi, '<\\/script') + ')(document);</script>' : '';
|
|
114
232
|
const page = '<!DOCTYPE html>\n'
|
|
115
|
-
+ '<html lang="
|
|
233
|
+
+ '<html lang="' + escapeRapierHtmlText(language) + '" data-rapier-theme="' + theme + '" data-highlights="' + highlights + '">\n'
|
|
116
234
|
+ '<head>\n<meta charset="UTF-8">\n' + (carrier ? RapierPageReturnAddress.PAGE_SEED + '\n' : '')
|
|
117
235
|
+ '<meta name="viewport" content="width=device-width, initial-scale=1">\n'
|
|
118
236
|
// Chrome 154's responsively sized frames: a host frame styled `frame-sizing: content-height` sizes
|
|
@@ -120,42 +238,45 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
120
238
|
// someone's page shows whole, with no inner scroll. Any origin may read the height: the page is the
|
|
121
239
|
// document, published by the person who exported it.
|
|
122
240
|
+ '<meta name="responsive-embedded-sizing" content="allow-origins=*">\n'
|
|
123
|
-
+ '<meta http-equiv="Content-Security-Policy" content="' + _rapierExportedPageCsp(layoutScript || inkScript ? nonce : '', includeFonts) + '">\n'
|
|
241
|
+
+ '<meta http-equiv="Content-Security-Policy" content="' + _rapierExportedPageCsp(layoutScript || inkScript || typographyScript || htmlScript ? nonce : '', includeFonts || !!fontCss || !!factoryFontCss) + '">\n'
|
|
124
242
|
+ '<meta name="referrer" content="no-referrer">\n'
|
|
125
243
|
+ '<link rel="icon" href="data:,">\n'
|
|
126
244
|
+ '<meta name="generator" content="Rapier ' + escapeRapierHtmlText(version) + '">\n'
|
|
127
245
|
+ '<title>' + escapeRapierHtmlText(pageTitle) + '</title>\n'
|
|
128
246
|
+ '<style>' + css + '</style>\n</head>\n'
|
|
129
|
-
+ '<body class="' + bodyClass + '"><main class="' + docClass + '" data-md-theme="' + theme + '">'
|
|
130
|
-
+ bodyHtml + '</main>' + carrier + layoutScript + inkScript + '</body>\n</html>';
|
|
247
|
+
+ '<body class="' + bodyClass + '"><main class="' + docClass + '" data-md-theme="' + theme + '"' + (settings?.space ? ' data-md-space' : '') + '>'
|
|
248
|
+
+ bodyHtml + '</main>' + carrier + layoutScript + inkScript + typographyScript + htmlScript + '</body>\n</html>';
|
|
131
249
|
|
|
132
250
|
return {
|
|
133
251
|
html: page,
|
|
134
252
|
filename: base + '.html',
|
|
135
253
|
bodyHtml,
|
|
136
254
|
css,
|
|
255
|
+
fontCss,
|
|
256
|
+
fontRequests: fontCss ? fontRequests(styledRoot, {settings, css, fontCss}) : [],
|
|
257
|
+
settings,
|
|
137
258
|
docClass,
|
|
259
|
+
language,
|
|
138
260
|
metadata,
|
|
139
261
|
documentTitle: settings && settings.title ? pageTitle : '',
|
|
140
262
|
};
|
|
141
263
|
}
|
|
142
264
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
return children(root);
|
|
265
|
+
async function _rapierBuildPublishedPage(context) {
|
|
266
|
+
if (typeof context?.canonical !== 'string' || !context.semanticRoot)
|
|
267
|
+
throw new TypeError('Publishing requires a captured document.');
|
|
268
|
+
// Public readers receive only the current document. The editing session's previous
|
|
269
|
+
// source and attribution ledger never acquire public custody through an export option.
|
|
270
|
+
const current = {...context, ledger: null, carried: 'none', comparisonBase: null};
|
|
271
|
+
return _rapierBuildArtifact({
|
|
272
|
+
kind: 'standalone', scripts: false, extraCss: _rapierSharedPageFallbackCss(),
|
|
273
|
+
async afterRoot(root) {
|
|
274
|
+
if (current.work) await finishAsync(_rapierSharedPageLayoutSteps(root), current.work);
|
|
275
|
+
else _rapierSharedPageLayout(root);
|
|
276
|
+
},
|
|
277
|
+
afterFonts: (root, style) => freezePublishedInk(root, {...style, document, work: current.work,
|
|
278
|
+
spec: globalThis.RapierMarkdownSpec, ink: globalThis.RapierInk, draw: globalThis.RapierInkDraw}),
|
|
279
|
+
}, current);
|
|
159
280
|
}
|
|
160
281
|
|
|
161
282
|
function _rapierProjectStyledRoot(semanticRoot, options) { return finish(_rapierProjectStyledRootSteps(semanticRoot, options)); }
|
|
@@ -177,12 +298,55 @@ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
|
|
|
177
298
|
if (code) {
|
|
178
299
|
const lang = _rapierLanguageClass(code) || String(metadata.codeLang || '');
|
|
179
300
|
code.classList.add('artifact-code');
|
|
180
|
-
if (metadata.docKind === 'code'
|
|
301
|
+
if (metadata.docKind === 'code') _rapierPaintCode(code, _rapierArtifactHighlight(code.textContent || '', lang));
|
|
181
302
|
if (lang) code.classList.add('language-' + String(lang).replace(/[^a-z0-9_-]/gi, ''));
|
|
182
303
|
}
|
|
183
304
|
} else {
|
|
305
|
+
if (root.hasAttribute('data-md-space')) {
|
|
306
|
+
// One raw HTML token may have several siblings. Its authored heights stay inside one line-sized box.
|
|
307
|
+
for (let element = root.firstElementChild; element;) {
|
|
308
|
+
const group = element.getAttribute('data-md-html-block');
|
|
309
|
+
if (group === null) { element = element.nextElementSibling; continue; }
|
|
310
|
+
let first = element;
|
|
311
|
+
// Sanitizing a container or repairing a table can leave its text before the first surviving tag.
|
|
312
|
+
while (first.previousSibling && first.previousSibling.nodeType !== 1) { first = first.previousSibling; yield; }
|
|
313
|
+
const wrap = document.createElement('div');
|
|
314
|
+
wrap.className = 'md-space-block';
|
|
315
|
+
root.insertBefore(wrap, first);
|
|
316
|
+
let node = first;
|
|
317
|
+
while (node && (node.nodeType !== 1 || node.getAttribute('data-md-html-block') === group)) {
|
|
318
|
+
const next = node.nextSibling;
|
|
319
|
+
wrap.appendChild(node);
|
|
320
|
+
node = next;
|
|
321
|
+
yield;
|
|
322
|
+
}
|
|
323
|
+
element = wrap.nextElementSibling;
|
|
324
|
+
}
|
|
325
|
+
// A removed raw container can leave only text. Reserve that surviving content in this projection.
|
|
326
|
+
for (let first = root.firstChild; first;) {
|
|
327
|
+
if (first.nodeType === 1) { first = first.nextSibling; continue; }
|
|
328
|
+
let end = first, hasText = false;
|
|
329
|
+
while (end && end.nodeType !== 1) {
|
|
330
|
+
if (end.nodeType === 3 && end.nodeValue.trim()) hasText = true;
|
|
331
|
+
end = end.nextSibling;
|
|
332
|
+
yield;
|
|
333
|
+
}
|
|
334
|
+
if (hasText) {
|
|
335
|
+
const wrap = document.createElement('div');
|
|
336
|
+
wrap.className = 'md-space-block';
|
|
337
|
+
root.insertBefore(wrap, first);
|
|
338
|
+
for (let node = first; node !== end;) {
|
|
339
|
+
const next = node.nextSibling;
|
|
340
|
+
wrap.appendChild(node);
|
|
341
|
+
node = next;
|
|
342
|
+
yield;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
first = end;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
184
348
|
for (const code of root.querySelectorAll('pre > code')) { yield;
|
|
185
|
-
if (
|
|
349
|
+
if (code.parentElement?.classList.contains('diagram-source') &&
|
|
186
350
|
code.closest('.diagram-block')?.getAttribute('data-diagram-state') === 'ready') continue;
|
|
187
351
|
const lang = _rapierLanguageClass(code) || 'text';
|
|
188
352
|
_rapierPaintCode(code, _rapierArtifactHighlight(code.textContent || '', lang));
|
|
@@ -206,6 +370,11 @@ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
|
|
|
206
370
|
|
|
207
371
|
for (const input of root.querySelectorAll('input[type="checkbox"]')) { yield;
|
|
208
372
|
const item = input.closest('.task-list-item');
|
|
373
|
+
if (item) {
|
|
374
|
+
const label = item.cloneNode(true);
|
|
375
|
+
for (const node of label.querySelectorAll('input,ul,ol')) { yield; node.remove(); }
|
|
376
|
+
input.setAttribute('aria-label', String(label.textContent || '').replace(/\s+/g, ' ').trim() || 'Task');
|
|
377
|
+
}
|
|
209
378
|
input.disabled = !!opts.print || !item;
|
|
210
379
|
if (input.disabled) { input.setAttribute('aria-disabled', 'true'); continue; }
|
|
211
380
|
// Native checked state changes this reader's view, never the carried source or disk.
|
|
@@ -213,9 +382,6 @@ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
|
|
|
213
382
|
input.removeAttribute('aria-checked');
|
|
214
383
|
input.setAttribute('autocomplete', 'off');
|
|
215
384
|
input.setAttribute('title', 'Ticks change this view, not the saved file.');
|
|
216
|
-
const label = item.cloneNode(true);
|
|
217
|
-
for (const node of label.querySelectorAll('input,ul,ol')) { yield;node.remove();}
|
|
218
|
-
input.setAttribute('aria-label', String(label.textContent || '').replace(/\s+/g, ' ').trim() || 'Task');
|
|
219
385
|
}
|
|
220
386
|
|
|
221
387
|
if (opts.print) for (const details of root.querySelectorAll('details')) { yield; details.open = true; }
|
|
@@ -223,7 +389,74 @@ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
|
|
|
223
389
|
|
|
224
390
|
yield* _rapierPrefixPortableAnchorsSteps(root, opts.baseName || metadata.filename || 'document');
|
|
225
391
|
|
|
226
|
-
return yield* cleanTree(root, sanitizeRapierHtml,
|
|
392
|
+
return yield* cleanTree(root, sanitizeRapierHtml, 'export');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function _rapierDocxRenderStyles(root) {
|
|
396
|
+
// The semantic root is detached and its code is normalized text. Use the same
|
|
397
|
+
// highlighter and light reference sheet that paint a page, then retain native run facts.
|
|
398
|
+
const codes = Array.from(root.querySelectorAll('pre > code'));
|
|
399
|
+
for (const code of codes) {
|
|
400
|
+
const lang = _rapierLanguageClass(code);
|
|
401
|
+
if (lang) _rapierPaintCode(code, _rapierArtifactHighlight(code.textContent || '', lang));
|
|
402
|
+
}
|
|
403
|
+
const coloured = codes.filter(code => code.querySelector('span'));
|
|
404
|
+
const quotes = Array.from(root.querySelectorAll('blockquote'));
|
|
405
|
+
if (!coloured.length && !quotes.length) return;
|
|
406
|
+
const frame = document.createElement('iframe');
|
|
407
|
+
frame.setAttribute('aria-hidden', 'true');
|
|
408
|
+
frame.style.cssText = 'position:fixed;left:-100000px;top:0;width:1px;height:1px;border:0;pointer-events:none';
|
|
409
|
+
document.body.appendChild(frame);
|
|
410
|
+
try {
|
|
411
|
+
const into = frame.contentDocument;
|
|
412
|
+
if (!into?.body) throw new Error('Word text styles could not be read.');
|
|
413
|
+
into.open(); into.write('<!doctype html><html><head></head><body></body></html>'); into.close();
|
|
414
|
+
into.documentElement.setAttribute('data-rapier-theme', 'light');
|
|
415
|
+
into.documentElement.setAttribute('data-highlights', _rapierArtifactPreference('highlights', ['accent', 'standard']));
|
|
416
|
+
into.body.className = 'light';
|
|
417
|
+
const sheet = into.createElement('style');
|
|
418
|
+
sheet.textContent = _rapierArtifactStyles('light', false, false);
|
|
419
|
+
into.head.appendChild(sheet);
|
|
420
|
+
const surface = into.createElement('div'); surface.className = 'md-render'; into.body.appendChild(surface);
|
|
421
|
+
const canvas = into.createElement('canvas'); canvas.width = canvas.height = 1;
|
|
422
|
+
const paint = canvas.getContext('2d'), colours = new Map();
|
|
423
|
+
if (!paint) throw new Error('Word text colours could not be read.');
|
|
424
|
+
const hex = colour => {
|
|
425
|
+
if (!colours.has(colour)) {
|
|
426
|
+
// CSS may resolve an accent to oklch/color-mix. The browser supplies its
|
|
427
|
+
// sRGB paper colour; the writer does not invent a second colour parser.
|
|
428
|
+
paint.fillStyle = '#ffffff'; paint.fillRect(0, 0, 1, 1);
|
|
429
|
+
paint.fillStyle = colour; paint.fillRect(0, 0, 1, 1);
|
|
430
|
+
colours.set(colour, '#' + Array.from(paint.getImageData(0, 0, 1, 1).data).slice(0, 3)
|
|
431
|
+
.map(byte => byte.toString(16).padStart(2, '0')).join(''));
|
|
432
|
+
}
|
|
433
|
+
return colours.get(colour);
|
|
434
|
+
};
|
|
435
|
+
for (const code of coloured) {
|
|
436
|
+
const pre = into.createElement('pre'), probe = into.importNode(code, true);
|
|
437
|
+
pre.appendChild(probe); surface.appendChild(pre);
|
|
438
|
+
const originals = Array.from(code.querySelectorAll('span'));
|
|
439
|
+
Array.from(probe.querySelectorAll('span')).forEach((span, index) => {
|
|
440
|
+
const style = into.defaultView.getComputedStyle(span), target = originals[index];
|
|
441
|
+
target.style.color = hex(style.color);
|
|
442
|
+
// Carry generated syntax emphasis as Word's semantic bold slot.
|
|
443
|
+
target.style.fontWeight ||= style.fontWeight === style.getPropertyValue('--md-fw-semibold').trim() ? 'bold' : style.fontWeight;
|
|
444
|
+
target.style.fontStyle = style.fontStyle;
|
|
445
|
+
});
|
|
446
|
+
pre.remove();
|
|
447
|
+
}
|
|
448
|
+
const quoteProbes = new Map();
|
|
449
|
+
for (const quote of quotes) {
|
|
450
|
+
const probe = into.createElement('blockquote'); probe.className = quote.className;
|
|
451
|
+
probe.style.borderInlineStartColor = quote.style.borderInlineStartColor;
|
|
452
|
+
// Nested quotes have their own reference border colour. Preserve the quote
|
|
453
|
+
// ancestry without cloning the document's pictures or other resources.
|
|
454
|
+
const parent = quoteProbes.get(quote.parentElement?.closest('blockquote')) || surface;
|
|
455
|
+
parent.appendChild(probe); quoteProbes.set(quote, probe);
|
|
456
|
+
}
|
|
457
|
+
for (const [quote, probe] of quoteProbes)
|
|
458
|
+
quote.style.borderInlineStartColor = hex(into.defaultView.getComputedStyle(probe).borderInlineStartColor);
|
|
459
|
+
} finally { frame.remove(); }
|
|
227
460
|
}
|
|
228
461
|
|
|
229
462
|
function _rapierPaintCode(el, html) {
|
|
@@ -335,7 +568,7 @@ function _rapierDocumentSettingsCss(settings) {
|
|
|
335
568
|
}
|
|
336
569
|
|
|
337
570
|
function _rapierExportedPageCsp(nonce, fonts) {
|
|
338
|
-
return "default-src 'none'; connect-src 'none'; img-src data:; style-src 'unsafe-inline'; "
|
|
571
|
+
return "default-src 'none'; connect-src 'none'; frame-src 'self'; img-src data:; style-src 'unsafe-inline'; "
|
|
339
572
|
+ (fonts ? "font-src data:; " : '')
|
|
340
573
|
+ "script-src " + (nonce ? "'nonce-" + nonce + "'" : "'none'") + "; base-uri 'none'; form-action 'none'";
|
|
341
574
|
}
|
|
@@ -394,12 +627,12 @@ async function _rapierSharedSourceHash(source, work = null) {
|
|
|
394
627
|
return Array.from(hash, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
395
628
|
}
|
|
396
629
|
|
|
397
|
-
function _rapierSharedSourceForms(source, root
|
|
398
|
-
async function _rapierSharedSourceFormsAsync(source, root,
|
|
630
|
+
function _rapierSharedSourceForms(source, root) { return finish(_rapierSharedSourceFormsSteps(source, root)); }
|
|
631
|
+
async function _rapierSharedSourceFormsAsync(source, root, work) {
|
|
399
632
|
const destinations = await _rapierDataImageDestinationsAsync(source, work);
|
|
400
|
-
return finishAsync(_rapierSharedSourceFormsSteps(source, root,
|
|
633
|
+
return finishAsync(_rapierSharedSourceFormsSteps(source, root, destinations), work);
|
|
401
634
|
}
|
|
402
|
-
function* _rapierSharedSourceFormsSteps(source, root,
|
|
635
|
+
function* _rapierSharedSourceFormsSteps(source, root, preparedDestinations = null) {
|
|
403
636
|
const assets = globalThis.RapierImageAssets;
|
|
404
637
|
const destinations = (preparedDestinations || _rapierImageDestinations(source, url => !!assets.dataImage(url))).sort((a, b) => b.start - a.start);
|
|
405
638
|
const byUrl = new Map(), ids = new Map(), labels = new Map();
|
|
@@ -410,14 +643,14 @@ function* _rapierSharedSourceFormsSteps(source, root, substitutions, preparedDes
|
|
|
410
643
|
if (!assets.dataImage(url) || byUrl.has(url)) continue;
|
|
411
644
|
byUrl.set(url, image);
|
|
412
645
|
}
|
|
413
|
-
let resolved = source;
|
|
414
646
|
const carrierEdits = [];
|
|
415
647
|
const used = [], definitions = [];
|
|
416
648
|
for (const row of destinations) {
|
|
417
649
|
yield;
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
650
|
+
// Share can display a converted picture. Only identical authored bytes can be
|
|
651
|
+
// borrowed from that picture; otherwise the source keeps its own data URL.
|
|
652
|
+
const image = byUrl.get(row.destination);
|
|
653
|
+
if (!image) continue;
|
|
421
654
|
let id = image.id;
|
|
422
655
|
if (!id || ids.get(id) !== 1 || !_RAPIER_SHARED_ID.test(id)) {
|
|
423
656
|
const label = labels.get(row.destination);
|
|
@@ -428,7 +661,6 @@ function* _rapierSharedSourceFormsSteps(source, root, substitutions, preparedDes
|
|
|
428
661
|
}
|
|
429
662
|
if (!used.includes(id)) used.push(id);
|
|
430
663
|
if (row.reference && !definitions.includes(row.reference)) definitions.push(row.reference);
|
|
431
|
-
resolved = resolved.slice(0, row.start) + shownUrl + resolved.slice(row.end);
|
|
432
664
|
carrierEdits.push({start: row.start, end: row.end, text: '#' + id});
|
|
433
665
|
}
|
|
434
666
|
let carried = '', cursor = source.length;
|
|
@@ -438,13 +670,13 @@ function* _rapierSharedSourceFormsSteps(source, root, substitutions, preparedDes
|
|
|
438
670
|
cursor = edit.start;
|
|
439
671
|
}
|
|
440
672
|
carried = (yield* _rapierSharedEncodeSteps(source.slice(0, cursor))) + carried;
|
|
441
|
-
return { resolved, carried, ids: used, definitions };
|
|
673
|
+
return { resolved: source, carried, ids: used, definitions };
|
|
442
674
|
}
|
|
443
675
|
|
|
444
676
|
async function _rapierSharedSourceCarrier(context, root) {
|
|
445
677
|
const source = (context.metadata.bom ? '\uFEFF' : '') + context.canonical;
|
|
446
|
-
const forms = context.work ? await _rapierSharedSourceFormsAsync(source, root, context.
|
|
447
|
-
: _rapierSharedSourceForms(source, root
|
|
678
|
+
const forms = context.work ? await _rapierSharedSourceFormsAsync(source, root, context.work)
|
|
679
|
+
: _rapierSharedSourceForms(source, root);
|
|
448
680
|
const esc = escapeRapierHtmlText;
|
|
449
681
|
return '<script type="' + _RAPIER_SHARED_SOURCE_TYPE + '" data-filename="' + esc(context.metadata.filename) +
|
|
450
682
|
'" data-kind="' + esc(context.metadata.docKind) + '" data-sha256="' + await _rapierSharedSourceHash(forms.resolved, context.work) +
|
|
@@ -740,12 +972,32 @@ async function _rapierRequireOfflinePageImages(root) {
|
|
|
740
972
|
if (!source || source.startsWith('#') || embedded(source)) return;
|
|
741
973
|
missing.push(label + ': ' + (source.startsWith('data:') ? '(unsupported embedded resource)' : source));
|
|
742
974
|
};
|
|
743
|
-
for (const [index, image] of [...root.querySelectorAll('img,[data-rapier-remote-src]')].entries()) {
|
|
744
|
-
const
|
|
745
|
-
|
|
975
|
+
for (const [index, image] of [...root.querySelectorAll('img,[data-rapier-remote-src],image,source[srcset]')].entries()) {
|
|
976
|
+
const tag = image.tagName.toLowerCase();
|
|
977
|
+
const src = image.getAttribute('data-rapier-remote-src') ?? image.getAttribute(tag === 'image' ? 'href' : 'src') ?? image.getAttribute('xlink:href') ?? '';
|
|
978
|
+
const srcset = image.getAttribute('srcset') || '';
|
|
979
|
+
const sources = [];
|
|
980
|
+
const picture = image.parentElement?.tagName.toLowerCase() === 'picture' && image.parentElement;
|
|
981
|
+
const alternative = picture && [...picture.querySelectorAll('source[srcset]')].some(source => source.getAttribute('srcset').trim());
|
|
982
|
+
if (tag !== 'source' && (src || !srcset && !alternative)) sources.push(src);
|
|
983
|
+
// A data URL contains its own comma. Read each URL before its optional
|
|
984
|
+
// density/width descriptor, as HTML's image candidate list does.
|
|
985
|
+
for (let at = 0; at < srcset.length;) {
|
|
986
|
+
while (at < srcset.length && /[\t\n\f\r ,]/.test(srcset[at])) at++;
|
|
987
|
+
const start = at;
|
|
988
|
+
while (at < srcset.length && !/[\t\n\f\r ]/.test(srcset[at])) at++;
|
|
989
|
+
const candidate = srcset.slice(start, at);
|
|
990
|
+
if (!candidate) break;
|
|
991
|
+
sources.push(candidate.replace(/,+$/, ''));
|
|
992
|
+
if (candidate.endsWith(',')) continue;
|
|
993
|
+
while (at < srcset.length && srcset[at++] !== ',') {}
|
|
994
|
+
}
|
|
746
995
|
const alt = image.getAttribute('data-rapier-remote-alt') ?? image.getAttribute('alt') ?? '';
|
|
747
|
-
const
|
|
748
|
-
|
|
996
|
+
for (const source of new Set(sources)) {
|
|
997
|
+
if (globalThis.RapierImageAssets.dataImage(source)) continue;
|
|
998
|
+
const destination = /^data:/i.test(source) ? '(unsupported embedded image)' : source || '(missing image source)';
|
|
999
|
+
missing.push('Picture ' + (index + 1) + (alt ? ' — ' + alt : '') + ': ' + destination);
|
|
1000
|
+
}
|
|
749
1001
|
}
|
|
750
1002
|
// A source candidate or an SVG/CSS picture is still an automatic resource. Inspect
|
|
751
1003
|
// the retained data; ordinary links remain navigation and local fragment references stay local.
|
|
@@ -758,8 +1010,6 @@ async function _rapierRequireOfflinePageImages(root) {
|
|
|
758
1010
|
if (tag === 'object') requireResource(element.getAttribute('data'), 'object source');
|
|
759
1011
|
if (tag === 'link' || element.namespaceURI === 'http://www.w3.org/2000/svg' && tag !== 'a')
|
|
760
1012
|
for (const attribute of ['href', 'xlink:href']) requireResource(element.getAttribute(attribute), tag + ' ' + attribute);
|
|
761
|
-
if (tag === 'img' || tag === 'source') for (const source of _rapierSrcsetUrls(element.getAttribute('srcset') || ''))
|
|
762
|
-
requireResource(source, tag + ' source candidate');
|
|
763
1013
|
for (const attribute of element.attributes || []) if (cssAttributes.has(attribute.name.toLowerCase())) {
|
|
764
1014
|
const urls = _rapierCssValueUrls(attribute.value);
|
|
765
1015
|
if (urls === null) missing.push(tag + ' ' + attribute.name + ': (unresolved CSS resource)');
|
|
@@ -767,39 +1017,9 @@ async function _rapierRequireOfflinePageImages(root) {
|
|
|
767
1017
|
}
|
|
768
1018
|
}
|
|
769
1019
|
if (!missing.length) return;
|
|
770
|
-
//
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
title: 'pictures not included',
|
|
774
|
-
message: 'No web page was written. Embed these pictures before exporting or sharing an offline page. '
|
|
775
|
-
+ 'Your document is unchanged. ' + missing.join('; '),
|
|
776
|
-
confirmLabel: 'close',
|
|
777
|
-
});
|
|
778
|
-
throw Object.assign(new Error('Web page not written; embed the listed pictures and try again'), {code: 'EXPORT_IMAGE_UNAVAILABLE'});
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
function _rapierSrcsetUrls(value) {
|
|
782
|
-
const urls = [];
|
|
783
|
-
let at = 0;
|
|
784
|
-
while (at < value.length) {
|
|
785
|
-
while (at < value.length && /[\t\n\f\r ,]/.test(value[at])) at++;
|
|
786
|
-
const start = at;
|
|
787
|
-
while (at < value.length && !/[\t\n\f\r ]/.test(value[at])) at++;
|
|
788
|
-
const source = value.slice(start, at);
|
|
789
|
-
if (!source) break;
|
|
790
|
-
// A data URL's comma belongs to its URL. Only trailing separators or the
|
|
791
|
-
// comma after a descriptor finish a candidate.
|
|
792
|
-
if (source.endsWith(',')) { urls.push(source.replace(/,+$/, '')); continue; }
|
|
793
|
-
urls.push(source);
|
|
794
|
-
let parentheses = 0;
|
|
795
|
-
while (at < value.length) {
|
|
796
|
-
const char = value[at++];
|
|
797
|
-
if (char === '(') parentheses++;
|
|
798
|
-
else if (char === ')' && parentheses) parentheses--;
|
|
799
|
-
else if (char === ',' && !parentheses) break;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
return urls;
|
|
1020
|
+
// The writer refuses immediately; interactive callers own any explanatory sheet.
|
|
1021
|
+
throw Object.assign(new Error('Web page not written; embed these pictures and try again. ' + missing.join('; ')),
|
|
1022
|
+
{code: 'offline_image_unavailable'});
|
|
803
1023
|
}
|
|
804
1024
|
|
|
805
1025
|
// Shared by live CSS admission and offline resource admission. Quoted strings are
|
|
@@ -879,27 +1099,8 @@ function _rapierCssValueUrls(value) {
|
|
|
879
1099
|
|
|
880
1100
|
async function _rapierBuildSharedPage(captured) {
|
|
881
1101
|
const context = await _rapierPrepareInterchangeContext({kind: 'share'}, captured);
|
|
882
|
-
//
|
|
883
|
-
|
|
884
|
-
const artifact = await _rapierBuildArtifact({
|
|
885
|
-
kind: 'standalone',
|
|
886
|
-
extraCss: _rapierSharedPageFallbackCss(),
|
|
887
|
-
async afterRoot(root) {
|
|
888
|
-
/* Unlike TXT/DOCX (which route through _rapierProjectPortableRoot and have no
|
|
889
|
-
native disclosure widget to fall back on), the shared page is a real HTML
|
|
890
|
-
document a real browser renders — the same styled root the standalone HTML
|
|
891
|
-
export already leaves untouched (_rapierBuildArtifact never flattens
|
|
892
|
-
details for kind:'standalone'). <details>/<summary> pass the 'export'
|
|
893
|
-
sanitize profile unchanged (only <form> is forbidden), so flattening here
|
|
894
|
-
only threw away a native, JS-free expand/collapse the architecture's own
|
|
895
|
-
fidelity table promises ("Complete shared/styled HTML: Exact recoverable
|
|
896
|
-
source"). Leave it as authored, exactly like standalone HTML export. */
|
|
897
|
-
if (context.work) {
|
|
898
|
-
await finishAsync(_rapierSharedPageLayoutSteps(root), context.work);
|
|
899
|
-
await finishAsync(_rapierShareCapLongCodeBlocksSteps(root), context.work);
|
|
900
|
-
} else { _rapierSharedPageLayout(root); _rapierShareCapLongCodeBlocks(root); }
|
|
901
|
-
},
|
|
902
|
-
}, context);
|
|
1102
|
+
// Share and Export write the same complete page, including native disclosures.
|
|
1103
|
+
const artifact = await _rapierBuildArtifact({kind: 'standalone'}, context);
|
|
903
1104
|
// No cap and no warning: the page is as large as the document is. A page over the editor's own 25
|
|
904
1105
|
// MiB open limit still opens in any browser.
|
|
905
1106
|
const blob = new Blob([artifact.html], {type: 'text/html;charset=utf-8'});
|
|
@@ -1004,6 +1205,7 @@ function _rapierImageDestinations(source, accepts) {
|
|
|
1004
1205
|
|
|
1005
1206
|
function _rapierScanMarkdownImages(raw, parsedTokens = null) {
|
|
1006
1207
|
const source = String(raw || '');
|
|
1208
|
+
const html = htmlImageRecord(source); if (html) return [html];
|
|
1007
1209
|
const found = [];
|
|
1008
1210
|
|
|
1009
1211
|
if (source.indexOf('![') < 0) return found;
|
|
@@ -1185,6 +1387,11 @@ function _rapierImageOpenRunsList(tokens) {
|
|
|
1185
1387
|
continue;
|
|
1186
1388
|
}
|
|
1187
1389
|
if (token.type === 'rapier_color_close') { stack.pop(); continue; }
|
|
1390
|
+
if (token.type === 'rapier_font_open') {
|
|
1391
|
+
stack.push({open: token.meta?.mdFont?.source || formatFontOpen(token.attrGet('data-md-font')), close: FONT_CLOSE});
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
if (token.type === 'rapier_font_close') { stack.pop(); continue; }
|
|
1188
1395
|
if (token.type === 'strong_open' || token.type === 'em_open' || token.type === 's_open') {
|
|
1189
1396
|
stack.push({open: token.markup, close: token.markup});
|
|
1190
1397
|
continue;
|
|
@@ -1512,7 +1719,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1512
1719
|
restore();
|
|
1513
1720
|
|
|
1514
1721
|
if (!root.isConnected || printing || view.matchMedia?.('print').matches) return;
|
|
1515
|
-
const children = [...root.children];
|
|
1722
|
+
const children = [...root.children].filter(element => !element.hasAttribute('data-rapier-html-preview'));
|
|
1516
1723
|
if (children.length > 8192) return;
|
|
1517
1724
|
// behind/front collapse and place like around/box but are never obstacles.
|
|
1518
1725
|
const outOfFlow = value => value?.wrap === 'behind' || value?.wrap === 'front';
|
|
@@ -1546,6 +1753,12 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1546
1753
|
const lines = layout.lines != null ? geometry.lineMetrics(element) : null;
|
|
1547
1754
|
const unrotated = geometry.imageBox(width, image.naturalWidth, image.naturalHeight, layout, imageWidth, lines);
|
|
1548
1755
|
if (!unrotated) { restore(); return; }
|
|
1756
|
+
if (image.hasAttribute('data-rapier-html')) {
|
|
1757
|
+
const attrs = JSON.parse(decodeURIComponent(image.getAttribute('data-rapier-html'))).attrs;
|
|
1758
|
+
const line = parseFloat(computed.lineHeight) || (parseFloat(computed.fontSize) || 16) * 1.6;
|
|
1759
|
+
unrotated.height = !attrs.height || attrs.height === 'auto'
|
|
1760
|
+
? Number(image.dataset.rapierNaturalHeight) || box(image).height : Number(attrs.height) * line;
|
|
1761
|
+
}
|
|
1549
1762
|
// rasterReserved (layout/model.mjs).
|
|
1550
1763
|
const rasterRad = (layout.rotate || 0) * Math.PI / 180;
|
|
1551
1764
|
const fit = geometry.rasterReserved(width, unrotated, rasterRad);
|
|
@@ -1618,6 +1831,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1618
1831
|
observer?.observe(root);
|
|
1619
1832
|
root.addEventListener('load', schedule, true);
|
|
1620
1833
|
root.addEventListener('error', schedule, true);
|
|
1834
|
+
root.addEventListener('rapier-html-resize', schedule);
|
|
1621
1835
|
root.addEventListener('toggle', schedule, true);
|
|
1622
1836
|
view.addEventListener('resize', schedule);
|
|
1623
1837
|
view.addEventListener('pageshow', schedule);
|
|
@@ -1632,6 +1846,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1632
1846
|
observer?.disconnect(); restore();
|
|
1633
1847
|
root.removeEventListener('load', schedule, true);
|
|
1634
1848
|
root.removeEventListener('error', schedule, true);
|
|
1849
|
+
root.removeEventListener('rapier-html-resize', schedule);
|
|
1635
1850
|
root.removeEventListener('toggle', schedule, true);
|
|
1636
1851
|
view.removeEventListener('resize', schedule);
|
|
1637
1852
|
view.removeEventListener('pageshow', schedule);
|
|
@@ -1668,6 +1883,23 @@ function _rapierArtifactInkScript(root, nonce = '') {
|
|
|
1668
1883
|
return '<script' + (nonce ? ' nonce="' + nonce + '"' : '') + '>\n' + script.replace(/<\/script/gi, '<\\/script') + '\n</script>';
|
|
1669
1884
|
}
|
|
1670
1885
|
|
|
1671
|
-
|
|
1886
|
+
function _rapierArtifactTypographyScript(root, nonce = '') {
|
|
1887
|
+
const pictures = root.querySelector('img[data-rapier-print-lines]');
|
|
1888
|
+
if (!pictures && !root.querySelector('p[data-md-hyphens="auto"],p[data-md-lines],p[data-md-caps="small"],p[data-md-hanging="on"]')) return '';
|
|
1889
|
+
const script = '(() => {\nconst typography = (' + createTypography.toString() + ')();\n' +
|
|
1890
|
+
(pictures ? 'const capitals = (' + createPrintCapitals.toString() + ')((' + createLineMeasurer.toString() + ')(),(' + linesHeight.toString() + '),(' + followingPrintProse.toString() + '));\n' : '') +
|
|
1891
|
+
'const root = document.querySelector("main.rapier-page"); let frame = 0, width = -1, printing = matchMedia("print").matches;\n' +
|
|
1892
|
+
'const apply = () => { frame = 0; ' + (pictures ? 'if (printing) capitals.apply(root); else capitals.clear(root); ' : '') + 'typography.apply(root, {all:true}); };\n' +
|
|
1893
|
+
'const schedule = () => { if (!frame) frame = requestAnimationFrame(apply); };\n' +
|
|
1894
|
+
'globalThis.__RAPIER_EXPORT_READY__ = Promise.all([document.fonts?.ready, ...Array.from(root.querySelectorAll("img"), image => image.decode?.().catch(() => {}))]).then(() => new Promise(resolve => requestAnimationFrame(() => { apply(); resolve(); })));\n' +
|
|
1895
|
+
'addEventListener("resize", schedule); addEventListener("beforeprint", () => { printing = true; apply(); });\n' +
|
|
1896
|
+
'addEventListener("afterprint", () => { printing = false; ' + (pictures ? 'capitals.clear(root); ' : '') + 'schedule(); });\n' +
|
|
1897
|
+
'root.addEventListener("load", schedule, true);\n' +
|
|
1898
|
+
'if (typeof ResizeObserver === "function") new ResizeObserver(() => { const next = root.getBoundingClientRect().width; if (next !== width) { width = next; schedule(); } }).observe(root);\n' +
|
|
1899
|
+
'})();';
|
|
1900
|
+
return '<script' + (nonce ? ' nonce="' + nonce + '"' : '') + '>\n' + script.replace(/<\/script/gi, '<\\/script') + '\n</script>';
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
return {_rapierDataImageDestinationsAsync, _rapierSharedSourceFormsAsync, _rapierPrefixPortableAnchorsSteps, _rapierProjectStyledRootAsync, _rapierBuildArtifact, _rapierBuildPublishedPage, _rapierProjectStyledRoot, _rapierDocxRenderStyles, _rapierPaintCode, _rapierPrefixPortableAnchors, _rapierDocumentSettingsOf, _rapierDocumentSettingsCss, _rapierExportedPageCsp, _rapierArtifactNonce, _rapierAnnotateExportBoxPolygons, _rapierSharedSourceHash, _rapierSharedSourceForms, _rapierSharedSourceCarrier, _rapierSharedResolve, _rapierReadSharedDocument, _rapierSharedPageFallbackCss, _rapierShareWrapKind, _rapierSharePictureParagraph, _rapierSharedPageLayout, _rapierShareCapLongCodeBlocks, _rapierRequireOfflinePageImages, _rapierCssValueUrls, _rapierBuildSharedPage, _rapierImageSources, _rapierImageDestinations, _rapierScanMarkdownImages, _rapierParseMarkdownImageDestination, _rapierImageOpenRunsList, _rapierArtifactFactoryModules, _rapierProjectArtifactLayout, _rapierArtifactLayoutScript, _rapierArtifactInkScript , render};
|
|
1672
1904
|
}
|
|
1673
1905
|
export {createRenderer};
|