rapier-markdown-kit 1.1.90 → 1.1.92

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.
@@ -1,7 +1,8 @@
1
1
  // SPDX-License-Identifier: MIT
2
+ import {finish, finishAsync, cloneTree, cleanTree, serializeTree, encodeUtf8Steps, pause} from './render-work.mjs';
2
3
  // The document's renderer. DOM, codecs and host state are explicit inputs.
3
4
  function createRenderer(runtime) {
4
- const {RAPIER_COLOR_CLOSE, RapierLedgerCarried, RapierPageReturnAddress, RapierTextCodec, _rapierArtifactHighlight, _rapierArtifactLexerScript, _rapierArtifactMarkLexed, _rapierArtifactPreference, _rapierArtifactStyles, _rapierBlobDataUrl, _rapierBuildInterchangeContext, _rapierDocumentNameIsAdmissible, _rapierLedgerParts, _rapierDrawReadSVGRecipe, _rapierDrawShapeProfileFor, _rapierFillDiagram, _rapierFormatColorOpen, _rapierLanguageClass, _rapierLineStartOffsets, _rapierMarkdownEnvironment, _rapierPortableHtml, _rapierPrepareInterchangeContext, _rapierProjectPortableRoot, _rapierProviders, _rapierSourceCharEscaped, _rapierSourceLineSpan, crypto, document, escapeRapierHtmlText, globalThis, rapierConfirm, sanitizeRapierHtml} = runtime;
5
+ 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;
5
6
  let md = runtime.md;
6
7
  async function render(source, options = {}) {
7
8
  if (typeof source !== 'string') throw new TypeError('Document source must be a string');
@@ -18,13 +19,15 @@ async function _rapierBuildArtifact(options, providedContext) {
18
19
  const context = providedContext || _rapierBuildInterchangeContext(opts);
19
20
  const metadata = context.metadata;
20
21
  const base = context.baseName;
22
+ const work = context.work || opts.work;
21
23
 
22
24
  if (opts.kind === 'publishing' || opts.kind === 'fragment') {
23
25
  for (const diagram of context.semanticRoot.querySelectorAll('.diagram-block[data-diagram-native]'))
24
26
  await _rapierFillDiagram(diagram, true);
25
- const portableRoot = _rapierProjectPortableRoot(context.semanticRoot, { baseName: base, keepDiagrams: true });
27
+ const portableRoot = work ? await runtime._rapierProjectPortableRootAsync(context.semanticRoot, {baseName: base, keepDiagrams: true, work})
28
+ : _rapierProjectPortableRoot(context.semanticRoot, { baseName: base, keepDiagrams: true });
26
29
  return {
27
- html: _rapierPortableHtml(portableRoot),
30
+ html: work ? await runtime._rapierPortableHtmlAsync(portableRoot, work) : _rapierPortableHtml(portableRoot),
28
31
  filename: base + '-publishing.html',
29
32
  metadata,
30
33
  };
@@ -34,25 +37,39 @@ async function _rapierBuildArtifact(options, providedContext) {
34
37
  // Await the same materialization owner before any projection clones or serializes it.
35
38
  for (const diagram of opts.kind === 'page' ? [] : context.semanticRoot.querySelectorAll('.diagram-block[data-diagram-src]')) {
36
39
  await _rapierFillDiagram(diagram, true);
40
+ if (opts.kind !== 'standalone' || diagram.hasAttribute('data-diagram-unclosed')) continue;
41
+ const state = diagram.getAttribute('data-diagram-state');
42
+ // A completed diagram must have its drawing. Syntax and renderer failures both refuse
43
+ // the file; an unclosed fence above is still authored source, not a completed diagram.
44
+ if (state !== 'ready' || !diagram.querySelector('.diagram-cache svg'))
45
+ throw Object.assign(new Error('Web page not written; a required diagram could not be rendered'), {code: 'EXPORT_DIAGRAM_UNAVAILABLE'});
37
46
  }
38
47
 
39
- if (opts.kind === 'standalone') await _rapierRequireOfflinePageImages(context.semanticRoot);
48
+ if (opts.kind === 'standalone') {
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');
51
+ // MathJax represents invalid TeX as a static error SVG. A source-only span means
52
+ // rendering itself failed, so publishing it would silently omit the equation.
53
+ if (context.semanticRoot.querySelector('span[data-rapier-math-source]'))
54
+ throw new Error('Web page not written; a required equation could not be rendered');
55
+ }
56
+ if (opts.kind === 'standalone' || opts.kind === 'page') await _rapierRequireOfflinePageImages(context.semanticRoot);
40
57
 
41
- const styledRoot = _rapierProjectStyledRoot(context.semanticRoot, {
42
- metadata,
43
- print: !!opts.print,
44
- sourceCode: opts.kind === 'page',
45
- baseName: base,
46
- });
58
+ const styledOptions = {metadata, print: !!opts.print, sourceCode: opts.kind === 'page', baseName: base, work};
59
+ const styledRoot = work ? await _rapierProjectStyledRootAsync(context.semanticRoot, styledOptions)
60
+ : _rapierProjectStyledRoot(context.semanticRoot, styledOptions);
61
+ if (opts.kind === 'standalone' && typeof _rapierFinalizeExportCode === 'function') await _rapierFinalizeExportCode(styledRoot);
47
62
  // _rapierProjectStyledRoot already returns the sanitized projection.
48
63
  // Print never emits the layout reflow script (see below), so a print artifact has no use for
49
64
  // this annotation -- skip it there rather than pay the cost for numbers nothing will read.
50
65
  if (!opts.print && opts.kind !== 'page') _rapierAnnotateExportBoxPolygons(styledRoot);
51
66
  // Share adds its nearest-side no-script float and long-code cap here. Offline-image
52
67
  // admission above and the page policy below belong to this common writer, not its callers.
53
- if (typeof opts.afterRoot === 'function') opts.afterRoot(styledRoot);
68
+ if (typeof opts.afterRoot === 'function') await opts.afterRoot(styledRoot);
54
69
  const carrier = opts.kind === 'standalone' ? await _rapierSharedSourceCarrier(context, styledRoot) : '';
55
- const bodyHtml = opts.kind === 'page' ? _rapierPageHtml(styledRoot) : styledRoot.innerHTML;
70
+ const bodyHtml = opts.kind === 'page' ? _rapierPageHtml(styledRoot) : work
71
+ ? await finishAsync(serializeTree(styledRoot), work) : styledRoot.innerHTML;
72
+ if (work) await pause(work, .95);
56
73
  const theme = opts.print
57
74
  ? 'light'
58
75
  : opts.kind === 'page' ? 'system'
@@ -68,21 +85,23 @@ async function _rapierBuildArtifact(options, providedContext) {
68
85
  // .md-render set and the highlight rules, so a numbered list, a checkbox, a callout and a
69
86
  // table are drawn in an exported page exactly as they are drawn here. The one caller with
70
87
  // rules of its own -- Share, for the no-script float it alone writes -- appends them.
71
- // Diagram geometry was measured with these exact fonts. Carry the same data fonts into
72
- // offline pages; a fallback face would change labels inside fixed renderer geometry.
73
- const diagramFonts = !!styledRoot.querySelector('svg.rapier-diagram,svg.rapier-native-flowchart');
74
- const includeFonts = !!opts.print || diagramFonts;
75
- const zenFontCss = styledRoot.querySelector('svg[aria-roledescription="zenuml"]')
88
+ const includeFonts = !!opts.print || opts.kind === 'standalone';
89
+ const zenFontCss = opts.print && styledRoot.querySelector('svg[aria-roledescription="zenuml"]')
76
90
  ? (_rapierProviders?.mermaid?.exportFontCss?.() || '') : '';
77
91
  const willFont = opts.print && opts.willFont ? opts.willFont : null;
78
92
  const settings = _rapierDocumentSettingsOf(context.canonical);
79
93
  const settingsCss = _rapierDocumentSettingsCss(settings);
80
94
  const pageTitle = settings && settings.title ? globalThis.RapierMarkdownSpec.documentTitle(settings) : metadata.filename;
81
- const css = _rapierArtifactStyles(theme, includeFonts, !!opts.print || opts.kind === 'standalone', opts.kind === 'page')
95
+ let css = _rapierArtifactStyles(theme, includeFonts, !!opts.print || opts.kind === 'standalone', opts.kind === 'page', opts.kind === 'standalone' ? '' : null)
82
96
  + (zenFontCss ? '\n\n' + zenFontCss : '')
83
97
  + (opts.extraCss ? '\n\n' + opts.extraCss : '')
84
98
  + (willFont ? '\n\n@font-face{font-family:' + willFont.family + ';src:url(' + await _rapierBlobDataUrl(new Blob([willFont.bytes], { type: 'font/ttf' })) + ') format("truetype")}' : '')
85
99
  + (settingsCss ? '\n' + settingsCss : '');
100
+ // Match the document's measured faces with only the glyphs this immutable page uses.
101
+ if (opts.kind === 'standalone') {
102
+ const fonts = await _rapierExportFontCss(styledRoot, {settings, css, theme});
103
+ if (fonts) css += '\n' + fonts;
104
+ }
86
105
  // Every written page is offline. Only the writer's nonce script may run; it, pictures,
87
106
  // styles and other page resources get no network authority. Following an ordinary link
88
107
  // is the reader's navigation, with no Referer, not a background resource request.
@@ -91,9 +110,6 @@ async function _rapierBuildArtifact(options, providedContext) {
91
110
  // 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
92
111
  // nonce out of it and be admitted.
93
112
  const layoutScript = !opts.print && opts.kind !== 'page' ? _rapierArtifactLayoutScript(styledRoot, nonce) : '';
94
- // The lexer for the page's code, under the same nonce, only when a block earned it: the CPU
95
- // spans are the first paint, and where the reader's browser has WebGPU the lexer repaints them.
96
- const lexerScript = !opts.print && opts.kind !== 'page' ? _rapierArtifactLexerScript(styledRoot, nonce) : '';
97
113
  const inkScript = !opts.print && opts.kind !== 'page' ? _rapierArtifactInkScript(styledRoot, nonce) : '';
98
114
  const page = '<!DOCTYPE html>\n'
99
115
  + '<html lang="en" data-rapier-theme="' + theme + '" data-highlights="' + highlights + '">\n'
@@ -104,13 +120,14 @@ async function _rapierBuildArtifact(options, providedContext) {
104
120
  // someone's page shows whole, with no inner scroll. Any origin may read the height: the page is the
105
121
  // document, published by the person who exported it.
106
122
  + '<meta name="responsive-embedded-sizing" content="allow-origins=*">\n'
107
- + '<meta http-equiv="Content-Security-Policy" content="' + _rapierExportedPageCsp(layoutScript || lexerScript || inkScript ? nonce : '', includeFonts) + '">\n'
123
+ + '<meta http-equiv="Content-Security-Policy" content="' + _rapierExportedPageCsp(layoutScript || inkScript ? nonce : '', includeFonts) + '">\n'
108
124
  + '<meta name="referrer" content="no-referrer">\n'
125
+ + '<link rel="icon" href="data:,">\n'
109
126
  + '<meta name="generator" content="Rapier ' + escapeRapierHtmlText(version) + '">\n'
110
127
  + '<title>' + escapeRapierHtmlText(pageTitle) + '</title>\n'
111
128
  + '<style>' + css + '</style>\n</head>\n'
112
129
  + '<body class="' + bodyClass + '"><main class="' + docClass + '" data-md-theme="' + theme + '">'
113
- + bodyHtml + '</main>' + carrier + layoutScript + lexerScript + inkScript + '</body>\n</html>';
130
+ + bodyHtml + '</main>' + carrier + layoutScript + inkScript + '</body>\n</html>';
114
131
 
115
132
  return {
116
133
  html: page,
@@ -141,9 +158,12 @@ function _rapierPageHtml(root) {
141
158
  return children(root);
142
159
  }
143
160
 
144
- function _rapierProjectStyledRoot(semanticRoot, options) {
161
+ function _rapierProjectStyledRoot(semanticRoot, options) { return finish(_rapierProjectStyledRootSteps(semanticRoot, options)); }
162
+ async function _rapierProjectStyledRootAsync(semanticRoot, options = {}) { return finishAsync(_rapierProjectStyledRootSteps(semanticRoot, options), options.work); }
163
+
164
+ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
145
165
  const opts = options || {};
146
- const root = semanticRoot.cloneNode(true);
166
+ const root = yield* cloneTree(semanticRoot);
147
167
  const metadata = opts.metadata || {};
148
168
 
149
169
  if (metadata.docKind !== 'markdown') {
@@ -157,55 +177,53 @@ function _rapierProjectStyledRoot(semanticRoot, options) {
157
177
  if (code) {
158
178
  const lang = _rapierLanguageClass(code) || String(metadata.codeLang || '');
159
179
  code.classList.add('artifact-code');
160
- if (metadata.docKind === 'code' && !opts.sourceCode) { _rapierPaintCode(code, _rapierArtifactHighlight(code.textContent || '', lang)); _rapierArtifactMarkLexed(code, lang); }
180
+ if (metadata.docKind === 'code' && !opts.sourceCode) _rapierPaintCode(code, _rapierArtifactHighlight(code.textContent || '', lang));
161
181
  if (lang) code.classList.add('language-' + String(lang).replace(/[^a-z0-9_-]/gi, ''));
162
182
  }
163
183
  } else {
164
- root.querySelectorAll('pre > code').forEach(code => {
165
- if (opts.sourceCode) return;
184
+ for (const code of root.querySelectorAll('pre > code')) { yield;
185
+ if (opts.sourceCode || code.parentElement?.classList.contains('diagram-source') &&
186
+ code.closest('.diagram-block')?.getAttribute('data-diagram-state') === 'ready') continue;
166
187
  const lang = _rapierLanguageClass(code) || 'text';
167
188
  _rapierPaintCode(code, _rapierArtifactHighlight(code.textContent || '', lang));
168
- _rapierArtifactMarkLexed(code, lang);
169
- });
189
+ }
170
190
 
171
- root.querySelectorAll('table').forEach(table => {
172
- if (table.parentElement && table.parentElement.classList.contains('table-scroll-wrap')) return;
191
+ for (const table of root.querySelectorAll('table')) { yield;
192
+ if (table.parentElement && table.parentElement.classList.contains('table-scroll-wrap')) continue;
173
193
  const wrap = document.createElement('div');
174
194
  wrap.className = 'table-scroll-wrap';
175
195
  table.parentNode.insertBefore(wrap, table);
176
196
  wrap.appendChild(table);
177
- });
197
+ }
178
198
 
179
- root.querySelectorAll('math[display="block"]').forEach(math => {
180
- if (math.closest('svg') || math.closest('.math-display-wrap')) return;
199
+ for (const math of root.querySelectorAll('math[display="block"]')) { yield;
200
+ if (math.closest('svg') || math.closest('.math-display-wrap')) continue;
181
201
  const wrap = document.createElement('span');
182
202
  wrap.className = 'math-display-wrap';
183
203
  math.parentNode.insertBefore(wrap, math);
184
204
  wrap.appendChild(math);
185
- });
205
+ }
186
206
 
187
- root.querySelectorAll('input[type="checkbox"]').forEach(input => {
207
+ for (const input of root.querySelectorAll('input[type="checkbox"]')) { yield;
188
208
  const item = input.closest('.task-list-item');
189
209
  input.disabled = !!opts.print || !item;
190
- if (input.disabled) { input.setAttribute('aria-disabled', 'true'); return; }
210
+ if (input.disabled) { input.setAttribute('aria-disabled', 'true'); continue; }
191
211
  // Native checked state changes this reader's view, never the carried source or disk.
192
212
  input.removeAttribute('aria-disabled');
193
213
  input.removeAttribute('aria-checked');
194
214
  input.setAttribute('autocomplete', 'off');
195
215
  input.setAttribute('title', 'Ticks change this view, not the saved file.');
196
216
  const label = item.cloneNode(true);
197
- label.querySelectorAll('input,ul,ol').forEach(node => node.remove());
217
+ for (const node of label.querySelectorAll('input,ul,ol')) { yield;node.remove();}
198
218
  input.setAttribute('aria-label', String(label.textContent || '').replace(/\s+/g, ' ').trim() || 'Task');
199
- });
219
+ }
200
220
 
201
- if (opts.print) root.querySelectorAll('details').forEach(details => { details.open = true; });
221
+ if (opts.print) for (const details of root.querySelectorAll('details')) { yield; details.open = true; }
202
222
  }
203
223
 
204
- _rapierPrefixPortableAnchors(root, opts.baseName || metadata.filename || 'document');
224
+ yield* _rapierPrefixPortableAnchorsSteps(root, opts.baseName || metadata.filename || 'document');
205
225
 
206
- const clean = document.createElement('div');
207
- clean.innerHTML = sanitizeRapierHtml(root.innerHTML, opts.sourceCode ? 'source' : 'export');
208
- return clean;
226
+ return yield* cleanTree(root, sanitizeRapierHtml, opts.sourceCode ? 'source' : 'export');
209
227
  }
210
228
 
211
229
  function _rapierPaintCode(el, html) {
@@ -213,7 +231,9 @@ function _rapierPaintCode(el, html) {
213
231
  el.classList.add('tok');
214
232
  }
215
233
 
216
- function _rapierPrefixPortableAnchors(root, baseName) {
234
+ function _rapierPrefixPortableAnchors(root, baseName) { return finish(_rapierPrefixPortableAnchorsSteps(root, baseName)); }
235
+
236
+ function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
217
237
  const slug = String(baseName || 'document').toLowerCase()
218
238
  .replace(/[^a-z0-9]+/g, '-')
219
239
  .replace(/^-+|-+$/g, '') || 'document';
@@ -231,9 +251,9 @@ function _rapierPrefixPortableAnchors(root, baseName) {
231
251
  return svg;
232
252
  };
233
253
 
234
- root.querySelectorAll('[id]').forEach(element => {
254
+ for (const element of root.querySelectorAll('[id]')) { yield;
235
255
  const oldId = element.id;
236
- if (!oldId) return;
256
+ if (!oldId) continue;
237
257
  const base = oldId.replace(/[^a-z0-9_-]+/gi, '-')
238
258
  .replace(/^-+|-+$/g, '') || 'anchor';
239
259
  let nextId = prefix + base;
@@ -250,9 +270,9 @@ function _rapierPrefixPortableAnchors(root, baseName) {
250
270
  if (!local.has(oldId)) local.set(oldId, nextId);
251
271
  }
252
272
  element.id = nextId;
253
- });
273
+ }
254
274
 
255
- root.querySelectorAll('*').forEach(element => {
275
+ for (const element of root.querySelectorAll('*')) { yield;
256
276
  const svg = scopeOf(element), local = svgIds.get(svg);
257
277
  const rewriteId = value => (svg ? local?.get(value) : ids.get(value)) || '';
258
278
  // URL fragments percent-decode once; ARIA/for/headers are literal IDREFs.
@@ -296,7 +316,7 @@ function _rapierPrefixPortableAnchors(root, baseName) {
296
316
  if (next !== value) element.setAttribute(name, next);
297
317
  }
298
318
  }
299
- });
319
+ }
300
320
  }
301
321
 
302
322
  function _rapierDocumentSettingsOf(canonical) {
@@ -315,7 +335,7 @@ function _rapierDocumentSettingsCss(settings) {
315
335
  }
316
336
 
317
337
  function _rapierExportedPageCsp(nonce, fonts) {
318
- return "default-src 'none'; img-src data: https:; style-src 'unsafe-inline'; "
338
+ return "default-src 'none'; connect-src 'none'; img-src data:; style-src 'unsafe-inline'; "
319
339
  + (fonts ? "font-src data:; " : '')
320
340
  + "script-src " + (nonce ? "'nonce-" + nonce + "'" : "'none'") + "; base-uri 'none'; form-action 'none'";
321
341
  }
@@ -368,15 +388,20 @@ function _rapierAnnotateExportBoxPolygons(root) {
368
388
  }
369
389
  }
370
390
 
371
- async function _rapierSharedSourceHash(source) {
372
- const bytes = new TextEncoder().encode(source);
391
+ async function _rapierSharedSourceHash(source, work = null) {
392
+ const bytes = work ? await finishAsync(encodeUtf8Steps(source), work) : new TextEncoder().encode(source);
373
393
  const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes));
374
394
  return Array.from(hash, byte => byte.toString(16).padStart(2, '0')).join('');
375
395
  }
376
396
 
377
- function _rapierSharedSourceForms(source, root, substitutions) {
397
+ function _rapierSharedSourceForms(source, root, substitutions) { return finish(_rapierSharedSourceFormsSteps(source, root, substitutions)); }
398
+ async function _rapierSharedSourceFormsAsync(source, root, substitutions, work) {
399
+ const destinations = await _rapierDataImageDestinationsAsync(source, work);
400
+ return finishAsync(_rapierSharedSourceFormsSteps(source, root, substitutions, destinations), work);
401
+ }
402
+ function* _rapierSharedSourceFormsSteps(source, root, substitutions, preparedDestinations = null) {
378
403
  const assets = globalThis.RapierImageAssets;
379
- const destinations = _rapierImageDestinations(source, url => !!assets.dataImage(url)).sort((a, b) => b.start - a.start);
404
+ const destinations = (preparedDestinations || _rapierImageDestinations(source, url => !!assets.dataImage(url))).sort((a, b) => b.start - a.start);
380
405
  const byUrl = new Map(), ids = new Map(), labels = new Map();
381
406
  for (const node of root.querySelectorAll('[id]')) ids.set(node.id, (ids.get(node.id) || 0) + 1);
382
407
  for (const match of source.matchAll(/^ {0,3}\[([^\]\r\n]+)\]:[ \t]*(data:image\/\S+)/gim)) if (!labels.has(match[2])) labels.set(match[2], match[1]);
@@ -389,6 +414,7 @@ function _rapierSharedSourceForms(source, root, substitutions) {
389
414
  const carrierEdits = [];
390
415
  const used = [], definitions = [];
391
416
  for (const row of destinations) {
417
+ yield;
392
418
  const shownUrl = substitutions?.get(row.destination) || row.destination;
393
419
  const image = byUrl.get(shownUrl);
394
420
  if (!image) { if (shownUrl !== row.destination) { resolved = resolved.slice(0, row.start) + shownUrl + resolved.slice(row.end); carrierEdits.push({start: row.start, end: row.end, text: _rapierSharedEncode(shownUrl)}); } continue; }
@@ -407,25 +433,27 @@ function _rapierSharedSourceForms(source, root, substitutions) {
407
433
  }
408
434
  let carried = '', cursor = source.length;
409
435
  for (const edit of carrierEdits) {
410
- carried = edit.text + _rapierSharedEncode(source.slice(edit.end, cursor)) + carried;
436
+ yield;
437
+ carried = edit.text + (yield* _rapierSharedEncodeSteps(source.slice(edit.end, cursor))) + carried;
411
438
  cursor = edit.start;
412
439
  }
413
- carried = _rapierSharedEncode(source.slice(0, cursor)) + carried;
440
+ carried = (yield* _rapierSharedEncodeSteps(source.slice(0, cursor))) + carried;
414
441
  return { resolved, carried, ids: used, definitions };
415
442
  }
416
443
 
417
444
  async function _rapierSharedSourceCarrier(context, root) {
418
445
  const source = (context.metadata.bom ? '\uFEFF' : '') + context.canonical;
419
- const forms = _rapierSharedSourceForms(source, root, context.imageSubstitutions);
446
+ const forms = context.work ? await _rapierSharedSourceFormsAsync(source, root, context.imageSubstitutions, context.work)
447
+ : _rapierSharedSourceForms(source, root, context.imageSubstitutions);
420
448
  const esc = escapeRapierHtmlText;
421
449
  return '<script type="' + _RAPIER_SHARED_SOURCE_TYPE + '" data-filename="' + esc(context.metadata.filename) +
422
- '" data-kind="' + esc(context.metadata.docKind) + '" data-sha256="' + await _rapierSharedSourceHash(forms.resolved) +
450
+ '" data-kind="' + esc(context.metadata.docKind) + '" data-sha256="' + await _rapierSharedSourceHash(forms.resolved, context.work) +
423
451
  '"' + (forms.ids.length ? ' data-images="' + forms.ids.join(' ') + '"' : '') +
424
452
  (forms.definitions.length ? ' data-image-definitions="' + forms.definitions.map(encodeURIComponent).join(' ') + '"' : '') + '>\n' +
425
453
  forms.carried + '\n</script>\n' +
426
454
  // The optional edit ledger: carried only when the host binds its owner; a host without one writes none.
427
455
  (RapierLedgerCarried ? RapierLedgerCarried.writeParts(forms.resolved, _rapierLedgerParts(forms.resolved, context.ledger, context.carried)) +
428
- RapierLedgerCarried.writeBase(forms.resolved, context.proposalBase) : '');
456
+ RapierLedgerCarried.writeBase(forms.resolved, context.comparisonBase) : '');
429
457
  }
430
458
 
431
459
  function _rapierSharedResolve(text, ids, images, definitions) {
@@ -482,8 +510,8 @@ async function _rapierReadSharedDocument(text, filename) {
482
510
  const bom = source.charCodeAt(0) === 0xFEFF;
483
511
  if (RapierTextCodec.normalizeDocument(source) !== (bom ? source.slice(1) : source)) return invalid();
484
512
  const parts = RapierLedgerCarried ? RapierLedgerCarried.readParts(source, Array.from(template.content.querySelectorAll('#rapier-ledger, #rapier-authorship'))) : {};
485
- const proposalBase = RapierLedgerCarried?.readBaseElements(Array.from(template.content.querySelectorAll('#rapier-base'))) || null;
486
- return {source, filename: name, kind, bom, ...parts, proposalBase};
513
+ const comparisonBase = RapierLedgerCarried?.readBaseElements(Array.from(template.content.querySelectorAll('#rapier-base'))) || null;
514
+ return {source, filename: name, kind, bom, ...parts, comparisonBase};
487
515
  }
488
516
 
489
517
  function _rapierSharedPageFallbackCss() {
@@ -512,7 +540,8 @@ function _rapierSharePictureParagraph(image) {
512
540
  return paragraph;
513
541
  }
514
542
 
515
- function _rapierSharedPageLayout(root) {
543
+ function _rapierSharedPageLayout(root) { return finish(_rapierSharedPageLayoutSteps(root)); }
544
+ function* _rapierSharedPageLayoutSteps(root) {
516
545
  // Owner search (wrapNeighbour) reads the sibling chain live, and applying one assignment
517
546
  // splices the tree -- moving the owner into a new container moves it out of the very chain
518
547
  // a later picture's own search would walk, and a barrier-free skip past an earlier picture
@@ -523,6 +552,7 @@ function _rapierSharedPageLayout(root) {
523
552
  // from one upfront `[...root.children]` before it changes anything about any of them.
524
553
  const assignments = [];
525
554
  for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
555
+ yield;
526
556
  const layout = globalThis.RapierMarkdownLayout.parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
527
557
  if (layout?.wrap !== 'around' && layout?.wrap !== 'box') continue;
528
558
  const paragraph = _rapierSharePictureParagraph(image);
@@ -541,6 +571,7 @@ function _rapierSharedPageLayout(root) {
541
571
  // still gets a sensible nearest-side float; the inlined planner (same owner as standalone)
542
572
  // replaces it when scripts run.
543
573
  for (const {image, paragraph, layout, owner} of assignments) {
574
+ yield;
544
575
  const side = (layout.x ?? 0) < 50 ? 'left' : 'right';
545
576
  paragraph.style.cssText = 'float:' + side + ';max-width:100%;margin:0 ' + (side === 'left' ? '1rem 1rem 0' : '0 1rem 1rem');
546
577
  if (layout.y > 0) {
@@ -634,12 +665,14 @@ function _rapierSharedPageLayout(root) {
634
665
  // removing the picture from flow costs the surrounding text nothing.
635
666
  const positionedAssignments = [];
636
667
  for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
668
+ yield;
637
669
  const layout = globalThis.RapierMarkdownLayout.parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
638
670
  if (layout?.wrap !== 'behind' && layout?.wrap !== 'front') continue;
639
671
  const paragraph = _rapierSharePictureParagraph(image);
640
672
  if (paragraph) positionedAssignments.push({image, paragraph, layout});
641
673
  }
642
674
  for (const {image, paragraph, layout} of positionedAssignments) {
675
+ yield;
643
676
  paragraph.style.cssText = 'position:relative;height:0;margin:0;padding:0;line-height:0';
644
677
  const x = layout.x ?? 50, naturalWidth = Number(image.getAttribute('width')) || 0;
645
678
  if (layout.width != null) {
@@ -665,6 +698,7 @@ function _rapierSharedPageLayout(root) {
665
698
  // Share's own float model has nothing else to say about it.
666
699
  const positioned = new Set([...assignments, ...positionedAssignments].map(row => row.image));
667
700
  for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
701
+ yield;
668
702
  if (positioned.has(image)) continue;
669
703
  const layout = globalThis.RapierMarkdownLayout.parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
670
704
  if (!layout?.rotate) continue;
@@ -682,50 +716,175 @@ function _rapierSharedPageLayout(root) {
682
716
  }
683
717
  }
684
718
 
685
- function _rapierShareCapLongCodeBlocks(root) {
686
- root.querySelectorAll('pre').forEach(pre => {
719
+ function _rapierShareCapLongCodeBlocks(root) { return finish(_rapierShareCapLongCodeBlocksSteps(root)); }
720
+ function* _rapierShareCapLongCodeBlocksSteps(root) {
721
+ for (const pre of root.querySelectorAll('pre')) { yield;
687
722
  const code = pre.querySelector(':scope > code') || pre;
688
723
  const text = String(code.textContent || '').replace(/\n$/, '');
689
724
  const lineCount = text === '' ? 0 : text.split('\n').length;
690
- if (lineCount <= RAPIER_SHARE_LONG_CODE_LINES) return;
725
+ if (lineCount <= RAPIER_SHARE_LONG_CODE_LINES) continue;
691
726
  pre.setAttribute('data-rapier-code-lines', String(lineCount));
692
727
  const note = document.createElement('span');
693
728
  note.className = 'rapier-code-lines-note';
694
729
  note.setAttribute('contenteditable', 'false');
695
730
  note.textContent = lineCount + ' lines';
696
731
  pre.appendChild(note);
697
- });
732
+ }
698
733
  }
699
734
 
700
735
  async function _rapierRequireOfflinePageImages(root) {
701
736
  const missing = [];
737
+ const embedded = value => globalThis.RapierImageAssets.dataImage(String(value || '').trim());
738
+ const requireResource = (value, label) => {
739
+ const source = String(value || '').trim();
740
+ if (!source || source.startsWith('#') || embedded(source)) return;
741
+ missing.push(label + ': ' + (source.startsWith('data:') ? '(unsupported embedded resource)' : source));
742
+ };
702
743
  for (const [index, image] of [...root.querySelectorAll('img,[data-rapier-remote-src]')].entries()) {
703
744
  const src = image.getAttribute('data-rapier-remote-src') ?? image.getAttribute('src') ?? '';
704
- if (globalThis.RapierImageAssets.dataImage(src)) continue;
745
+ if (embedded(src)) continue;
705
746
  const alt = image.getAttribute('data-rapier-remote-alt') ?? image.getAttribute('alt') ?? '';
706
747
  const destination = /^data:/i.test(src) ? '(unsupported embedded image)' : src || '(missing image source)';
707
748
  missing.push('Picture ' + (index + 1) + (alt ? ' — ' + alt : '') + ': ' + destination);
708
749
  }
750
+ // A source candidate or an SVG/CSS picture is still an automatic resource. Inspect
751
+ // the retained data; ordinary links remain navigation and local fragment references stay local.
752
+ const sourceTags = new Set(['audio', 'video', 'source', 'track', 'input', 'iframe', 'embed', 'script']);
753
+ const cssAttributes = new Set(['style', 'fill', 'stroke', 'filter', 'clip-path', 'mask', 'marker-start', 'marker-mid', 'marker-end', 'cursor', 'color-profile']);
754
+ for (const element of root.querySelectorAll('*')) {
755
+ const tag = String(element.localName || element.tagName || '').toLowerCase();
756
+ if (sourceTags.has(tag)) requireResource(element.getAttribute('src'), tag + ' source');
757
+ for (const attribute of ['poster', 'background']) requireResource(element.getAttribute(attribute), tag + ' ' + attribute);
758
+ if (tag === 'object') requireResource(element.getAttribute('data'), 'object source');
759
+ if (tag === 'link' || element.namespaceURI === 'http://www.w3.org/2000/svg' && tag !== 'a')
760
+ 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
+ for (const attribute of element.attributes || []) if (cssAttributes.has(attribute.name.toLowerCase())) {
764
+ const urls = _rapierCssValueUrls(attribute.value);
765
+ if (urls === null) missing.push(tag + ' ' + attribute.name + ': (unresolved CSS resource)');
766
+ else for (const url of urls) requireResource(url, tag + ' ' + attribute.name);
767
+ }
768
+ }
709
769
  if (!missing.length) return;
710
770
  // An acknowledged, scrollable sheet, not a disappearing toast or a partial-file opt-in.
711
771
  // Both Close and Cancel below refuse; neither can authorize a page with missing pictures.
712
- await rapierConfirm({
772
+ if (typeof rapierConfirm === 'function') await rapierConfirm({
713
773
  title: 'pictures not included',
714
774
  message: 'No web page was written. Embed these pictures before exporting or sharing an offline page. '
715
775
  + 'Your document is unchanged. ' + missing.join('; '),
716
776
  confirmLabel: 'close',
717
777
  });
718
- throw new Error('Web page not written; embed the listed pictures and try again');
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;
803
+ }
804
+
805
+ // Shared by live CSS admission and offline resource admission. Quoted strings are
806
+ // resource URLs only inside url() or as an image-set option, never arbitrary CSS text.
807
+ function _rapierCssValueUrls(value) {
808
+ const urls = [], functions = [];
809
+ let at = 0;
810
+ const space = char => char !== undefined && /[\t\n\f\r ]/.test(char);
811
+ const escape = () => {
812
+ at++;
813
+ if (value[at] === '\r' || value[at] === '\n' || value[at] === '\f') {
814
+ if (value[at++] === '\r' && value[at] === '\n') at++;
815
+ return '';
816
+ }
817
+ const hex = /^[\da-f]{1,6}/i.exec(value.slice(at));
818
+ if (!hex) return value[at++] || '';
819
+ at += hex[0].length;
820
+ if (space(value[at]) && value[at++] === '\r' && value[at] === '\n') at++;
821
+ const code = parseInt(hex[0], 16);
822
+ return String.fromCodePoint(!code || code > 0x10ffff || code >= 0xd800 && code <= 0xdfff ? 0xfffd : code);
823
+ };
824
+ const string = () => {
825
+ const quote = value[at++];
826
+ let text = '';
827
+ while (at < value.length) {
828
+ if (value[at] === quote) { at++; return text; }
829
+ if (/[\r\n\f]/.test(value[at])) return null;
830
+ text += value[at] === '\\' ? escape() : value[at++];
831
+ }
832
+ return null;
833
+ };
834
+ const whitespace = () => {
835
+ for (;;) {
836
+ while (space(value[at])) at++;
837
+ if (value.slice(at, at + 2) !== '/*') return;
838
+ const end = value.indexOf('*/', at + 2);
839
+ at = end < 0 ? value.length : end + 2;
840
+ }
841
+ };
842
+ while (at < value.length) {
843
+ whitespace();
844
+ if (at >= value.length) break;
845
+ const parent = functions.at(-1), char = value[at];
846
+ if (char === '"' || char === "'") {
847
+ const text = string();
848
+ if (parent?.imageSet && parent.option) {
849
+ if (text === null) return null;
850
+ urls.push(text);
851
+ parent.option = false;
852
+ }
853
+ continue;
854
+ }
855
+ if (char === ',') { if (parent?.imageSet) parent.option = true; at++; continue; }
856
+ if (char === ')') { functions.pop(); at++; continue; }
857
+ if (parent?.imageSet) parent.option = false;
858
+ if (/[-\w\u0080-\uffff\\]/.test(char)) {
859
+ let name = '';
860
+ while (at < value.length && /[-\w\u0080-\uffff\\]/.test(value[at]))
861
+ name += value[at] === '\\' ? escape() : value[at++];
862
+ if (value[at] !== '(') continue;
863
+ at++;
864
+ name = name.toLowerCase();
865
+ if (name !== 'url') { functions.push({imageSet: name === 'image-set' || name === '-webkit-image-set', option: true}); continue; }
866
+ while (space(value[at])) at++;
867
+ let url = '';
868
+ if (value[at] === '"' || value[at] === "'") { url = string(); whitespace(); }
869
+ else while (at < value.length && value[at] !== ')') url += value[at] === '\\' ? escape() : value[at++];
870
+ if (url === null || value[at++] !== ')') return null;
871
+ urls.push(url.trim());
872
+ continue;
873
+ }
874
+ if (char === '(') functions.push({imageSet: false});
875
+ at++;
876
+ }
877
+ return urls;
719
878
  }
720
879
 
721
880
  async function _rapierBuildSharedPage(captured) {
722
- const context = await _rapierPrepareInterchangeContext({kind: 'share'}, captured?.proposalText == null ? captured : {...captured, canonical: captured.proposalText});
881
+ const context = await _rapierPrepareInterchangeContext({kind: 'share'}, captured);
723
882
  // One owner for every HTML file Rapier writes, including offline-image admission and CSP.
724
883
  // Share adds only the nearest-side no-script float and the long-code treatment.
725
884
  const artifact = await _rapierBuildArtifact({
726
885
  kind: 'standalone',
727
886
  extraCss: _rapierSharedPageFallbackCss(),
728
- afterRoot(root) {
887
+ async afterRoot(root) {
729
888
  /* Unlike TXT/DOCX (which route through _rapierProjectPortableRoot and have no
730
889
  native disclosure widget to fall back on), the shared page is a real HTML
731
890
  document a real browser renders — the same styled root the standalone HTML
@@ -735,8 +894,10 @@ async function _rapierBuildSharedPage(captured) {
735
894
  only threw away a native, JS-free expand/collapse the architecture's own
736
895
  fidelity table promises ("Complete shared/styled HTML: Exact recoverable
737
896
  source"). Leave it as authored, exactly like standalone HTML export. */
738
- _rapierSharedPageLayout(root);
739
- _rapierShareCapLongCodeBlocks(root);
897
+ if (context.work) {
898
+ await finishAsync(_rapierSharedPageLayoutSteps(root), context.work);
899
+ await finishAsync(_rapierShareCapLongCodeBlocksSteps(root), context.work);
900
+ } else { _rapierSharedPageLayout(root); _rapierShareCapLongCodeBlocks(root); }
740
901
  },
741
902
  }, context);
742
903
  // No cap and no warning: the page is as large as the document is. A page over the editor's own 25
@@ -753,6 +914,12 @@ const _RAPIER_SHARED_DEFINITION = /^(?:[A-Za-z0-9\-_.!~*'()]|%[0-9A-Fa-f]{2})+$/
753
914
 
754
915
  const _rapierSharedEncode = text => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/#/g, '&#35;').replace(/\r/g, '&#13;');
755
916
 
917
+ function* _rapierSharedEncodeSteps(text) {
918
+ const parts = [];
919
+ for (let at = 0; at < text.length; at += 16384) { parts.push(_rapierSharedEncode(text.slice(at, at + 16384))); yield; }
920
+ return parts.join('');
921
+ }
922
+
756
923
  const _rapierSharedDecode = text => text.replace(/&#35;/g, '#').replace(/&#13;/g, '\r').replace(/&lt;/g, '<').replace(/&amp;/g, '&');
757
924
 
758
925
  const RAPIER_SHARE_LONG_CODE_LINES = 40;
@@ -769,6 +936,12 @@ function _rapierImageSources(tokens) {
769
936
  return sources;
770
937
  }
771
938
 
939
+ async function _rapierDataImageDestinationsAsync(source, work) {
940
+ if (!source.includes('![')) return [];
941
+ return runtime.dataImageDestinationsAsync ? runtime.dataImageDestinationsAsync(source, work)
942
+ : _rapierImageDestinations(source, url => !!globalThis.RapierImageAssets.dataImage(url));
943
+ }
944
+
772
945
  function _rapierImageDestinations(source, accepts) {
773
946
  // Only a rendered Markdown image consumes a definition; unused definitions stay literal source.
774
947
  if (!source.includes('![')) return [];
@@ -1495,6 +1668,6 @@ function _rapierArtifactInkScript(root, nonce = '') {
1495
1668
  return '<script' + (nonce ? ' nonce="' + nonce + '"' : '') + '>\n' + script.replace(/<\/script/gi, '<\\/script') + '\n</script>';
1496
1669
  }
1497
1670
 
1498
- return {_rapierBuildArtifact, _rapierProjectStyledRoot, _rapierPaintCode, _rapierPrefixPortableAnchors, _rapierDocumentSettingsOf, _rapierDocumentSettingsCss, _rapierExportedPageCsp, _rapierArtifactNonce, _rapierAnnotateExportBoxPolygons, _rapierSharedSourceHash, _rapierSharedSourceForms, _rapierSharedSourceCarrier, _rapierSharedResolve, _rapierReadSharedDocument, _rapierSharedPageFallbackCss, _rapierShareWrapKind, _rapierSharePictureParagraph, _rapierSharedPageLayout, _rapierShareCapLongCodeBlocks, _rapierRequireOfflinePageImages, _rapierBuildSharedPage, _rapierImageSources, _rapierImageDestinations, _rapierScanMarkdownImages, _rapierParseMarkdownImageDestination, _rapierImageOpenRunsList, _rapierArtifactFactoryModules, _rapierProjectArtifactLayout, _rapierArtifactLayoutScript, _rapierArtifactInkScript , render};
1671
+ return {_rapierDataImageDestinationsAsync, _rapierSharedSourceFormsAsync, _rapierPrefixPortableAnchorsSteps, _rapierProjectStyledRootAsync, _rapierBuildArtifact, _rapierProjectStyledRoot, _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};
1499
1672
  }
1500
1673
  export {createRenderer};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-markdown-kit",
3
- "version": "1.1.90",
3
+ "version": "1.1.92",
4
4
  "description": "Read, render and write Self-contained Markdown: one .md file that carries its pictures, drawings and layout, and that any Markdown app can read. HTML renderer, stylesheet, text layout, Will/1 agent permissions, and an edit history with authorship and offline merge. MIT, no dependencies.",
5
5
  "keywords": [
6
6
  "markdown",