rapier-markdown-kit 1.2.46 → 1.2.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -81,7 +81,9 @@ editor's own seam: each needs a `host` that supplies its DOM, Markdown, picture,
81
81
  `createRenderStyles`, `createRenderSanitizer` and `createPrintRenderer`, plus the explicit font helpers
82
82
  `exportFontCss`, `requireDocumentFonts`, `fontRequests`, `waitForFonts` and `waitForFontRequests`. These are the
83
83
  functions used by the editor and exported pages. Each factory takes explicit host dependencies
84
- in its first argument. `style.css` is the
84
+ in its first argument. Refined print paragraphs use the host's retained `RapierArtifactLayoutDependencies`
85
+ (the same factory graph used by standalone HTML). They initialize in the print document so its own font set
86
+ owns every measurement. Missing factories or a blocked initialization refuse the export. `style.css` is the
85
87
  MIT `spec/markdown-style.css`, including the numbered-list, checkbox, callout, table,
86
88
  footnote and picture-layout treatment.
87
89
 
@@ -0,0 +1,19 @@
1
+ // SPDX-License-Identifier: MIT
2
+ export function _rapierArtifactFactoryModules(dependencies, group) {
3
+ const paths = dependencies.groups && dependencies.groups[group] ? dependencies.groups[group] : Object.keys(dependencies.factories);
4
+ return paths.map(path => 'modules[' + JSON.stringify(path) + '] = (' + Function.prototype.toString.call(dependencies.factories[path]) + ')();').join('\n');
5
+ }
6
+
7
+ export function printTypography(root, dependencies) {
8
+ if (!root.querySelector('p[data-md-hyphens="auto"],p[data-md-lines],p[data-md-hanging="on"],p[data-rapier-typography]')) return null;
9
+ const fail = () => Object.assign(new Error('The print document could not initialize its typography.'), {code: 'pdf_typography_unavailable'});
10
+ if (!dependencies?.factories?.['layout/typography.mjs']) throw fail();
11
+ const document = root.ownerDocument, script = document.createElement('script');
12
+ script.textContent = '(() => { const modules = Object.create(null);\n' + _rapierArtifactFactoryModules(dependencies, 'layout') +
13
+ '\ndocument.currentScript.rapierTypography = modules["layout/typography.mjs"].createTypography();\n})();';
14
+ try {
15
+ document.head.append(script);
16
+ if (!script.rapierTypography) throw fail();
17
+ return script.rapierTypography;
18
+ } finally { script.remove(); }
19
+ }
@@ -165,7 +165,6 @@ function fontSource(bytes) {
165
165
 
166
166
  function writeFace(face, source) {
167
167
  const properties = {...face.properties, src: source};
168
- delete properties['unicode-range'];
169
168
  return '@font-face{' + Object.entries(properties).map(([name, value]) => name + ':' + value).join(';') + '}';
170
169
  }
171
170
 
@@ -188,8 +187,29 @@ function fontRuns(root, {settings, css = '', fontCss = '', faces = fontFaces(fon
188
187
  '--md-font-main': settings?.mainfont?.css || settings?.sansfont?.css || FAMILY,
189
188
  '--md-font-mono': settings?.monofont?.css || MONO}};
190
189
  const computed = new WeakMap();
191
- let priorFace = null, priorStyle = '';
192
- const breakRun = () => { priorFace = null; priorStyle = ''; };
190
+ let pending = null, segmenter;
191
+ const breakRun = () => {
192
+ if (!pending) return;
193
+ const {text, families, state} = pending;
194
+ let prior = new Set();
195
+ for (const {segment} of (segmenter ||= new Intl.Segmenter(undefined, {granularity: 'grapheme'})).segment(text)) {
196
+ const selected = new Set();
197
+ for (const character of segment) for (const family of families) {
198
+ const candidates = faces.filter(face => face.family === family && unicodeIncludes(face, character));
199
+ if (!candidates.length) continue;
200
+ candidates.sort((a, b) => stretchOrder(a, b, state['font-stretch']) ||
201
+ styleDistance(a, state['font-style']) - styleDistance(b, state['font-style']) ||
202
+ weightDistance(a, state['font-weight']) - weightDistance(b, state['font-weight']));
203
+ selected.add(candidates[0]); break;
204
+ }
205
+ for (const face of selected) {
206
+ if (prior.has(face)) face.runs[face.runs.length - 1] += segment;
207
+ else face.runs.push(segment);
208
+ }
209
+ prior = selected;
210
+ }
211
+ pending = null;
212
+ };
193
213
  function styleOf(element) {
194
214
  if (computed.has(element)) return computed.get(element);
195
215
  const parent = element === root ? initial : styleOf(element.parentElement || root);
@@ -231,22 +251,10 @@ function fontRuns(root, {settings, css = '', fontCss = '', faces = fontFaces(fon
231
251
  else if (state['text-transform'] === 'capitalize') text = text.replace(/(^|\s)(\p{L})/gu, (_, gap, letter) => gap + letter.toUpperCase());
232
252
  const families = splitCss(state['font-family'], ',').filter(name => /^\s*['"]/.test(name) || !CSS_GENERIC_FONT_FAMILIES.includes(unquote(name).toLowerCase()))
233
253
  .map(name => unquote(name).toLowerCase());
234
- const runStyle = [state['font-weight'], state['font-style'], state['font-stretch']].join('/');
235
- for (const character of text) {
236
- let selected = null;
237
- for (const family of families) {
238
- const candidates = faces.filter(face => face.family === family && unicodeIncludes(face, character));
239
- if (!candidates.length) continue;
240
- candidates.sort((a, b) => stretchOrder(a, b, state['font-stretch']) ||
241
- styleDistance(a, state['font-style']) - styleDistance(b, state['font-style']) ||
242
- weightDistance(a, state['font-weight']) - weightDistance(b, state['font-weight']));
243
- selected = candidates[0]; break;
244
- }
245
- if (!selected) { breakRun(); continue; }
246
- if (selected === priorFace && runStyle === priorStyle) selected.runs[selected.runs.length - 1] += character;
247
- else selected.runs.push(character);
248
- priorFace = selected; priorStyle = runStyle;
249
- }
254
+ const key = JSON.stringify([families, state['font-weight'], state['font-style'], state['font-stretch']]);
255
+ if (pending?.key !== key) breakRun();
256
+ if (pending) pending.text += text;
257
+ else pending = {text, families, state, key};
250
258
  }
251
259
  function walk(element) {
252
260
  const name = element.localName?.toLowerCase();
@@ -277,6 +285,7 @@ function fontRuns(root, {settings, css = '', fontCss = '', faces = fontFaces(fon
277
285
  if (boundary) breakRun();
278
286
  }
279
287
  walk(root);
288
+ breakRun();
280
289
  } finally {
281
290
  if (oldClass == null) root.removeAttribute('class'); else root.setAttribute('class', oldClass);
282
291
  }
@@ -56,11 +56,19 @@ export function documentTrailer(text, options) {
56
56
  export function writeDocument(text, options) {
57
57
  return text + documentTrailer(text, options);
58
58
  }
59
+ function trailerOpening(source) {
60
+ const marker = '<!-- rapier-carried/1', at = source.lastIndexOf(marker);
61
+ if (at < 0) return null;
62
+ if (source.slice(at - 2, at + marker.length + 1) === OPEN) return {start: at - 2, body: at + marker.length + 1};
63
+ const before = /(?:\r\n|\r|\n){2}$/.exec(source.slice(Math.max(0, at - 4), at));
64
+ const after = /^(?:\r\n|\r|\n)/.exec(source.slice(at + marker.length, at + marker.length + 2));
65
+ return before && after ? {start: at - before[0].length, body: at + marker.length + after[0].length} : null;
66
+ }
59
67
  function unfinishedTrailer(source) {
60
- if (!source.includes(OPEN)) return false;
61
- const at = source.lastIndexOf(OPEN);
68
+ const opening = trailerOpening(source);
69
+ if (!opening) return false;
62
70
  let fence = null;
63
- for (const line of source.slice(0, at).split('\n')) {
71
+ for (const line of source.slice(0, opening.start).split(/\r\n?|\n/)) {
64
72
  const found = /^ {0,3}(`{3,}|~{3,})/.exec(line);
65
73
  if (!found) continue;
66
74
  if (!fence) fence = found[1];
@@ -69,17 +77,18 @@ function unfinishedTrailer(source) {
69
77
  return !fence;
70
78
  }
71
79
  export function readDocument(source) {
72
- if (!source.endsWith(CLOSE)) {
80
+ const closing = /(?:\r\n|\r|\n)-->(?:\r\n|\r|\n)$/.exec(source.slice(-7));
81
+ if (!closing) {
73
82
  if (unfinishedTrailer(source)) throw fault('unfinished document metadata');
74
83
  return {text: source, ledger: null, authorship: null};
75
84
  }
76
- const at = source.lastIndexOf(OPEN);
77
- if (at < 0) return {text: source, ledger: null, authorship: null};
85
+ const opening = trailerOpening(source);
86
+ if (!opening) return {text: source, ledger: null, authorship: null};
78
87
  let parts;
79
- try { parts = JSON.parse(source.slice(at + OPEN.length, -CLOSE.length)); }
88
+ try { parts = JSON.parse(source.slice(opening.body, -closing[0].length)); }
80
89
  catch (_) { throw fault('invalid document metadata'); }
81
90
  if (!parts || Object.keys(parts).length !== 2 || !Object.hasOwn(parts, 'ledger') || !Object.hasOwn(parts, 'authorship')) throw fault('invalid document parts');
82
- const text = source.slice(0, at);
91
+ const text = source.slice(0, opening.start);
83
92
  return {text, ...validateParts(text, parts)};
84
93
  }
85
94
 
@@ -70,13 +70,42 @@ function sourceString(node) {
70
70
  }
71
71
  const replacements = new WeakMap(), texts = new WeakMap(), edits = new WeakMap();
72
72
  export const sourceEdit = source => edits.get(source) || null;
73
+ const INITIAL_LEAF_CHARS = 16384;
73
74
  export class HistorySource {
74
75
  #root;
75
- constructor(text) { this.#root = sourceLeaf(text); Object.freeze(this); }
76
+ constructor(text) {
77
+ if (typeof text === 'string' && text.length > INITIAL_LEAF_CHARS) {
78
+ const leaves = [];
79
+ for (let from = 0; from < text.length;) {
80
+ let to = Math.min(text.length, from + INITIAL_LEAF_CHARS);
81
+ if (to < text.length && (text.charCodeAt(to - 1) & 0xfc00) === 0xd800 && (text.charCodeAt(to) & 0xfc00) === 0xdc00) to++;
82
+ leaves.push(sourceLeaf(text, from, to));
83
+ from = to;
84
+ }
85
+ const build = (from, to) => from === to ? null : to - from === 1 ? leaves[from]
86
+ : sourceNode(build(from, (from + to) >> 1), build((from + to) >> 1, to));
87
+ this.#root = build(0, leaves.length);
88
+ } else this.#root = sourceLeaf(text);
89
+ Object.freeze(this);
90
+ }
76
91
  static owns(value) { return value !== null && typeof value === 'object' && #root in value; }
77
92
  static #fromRoot(root) { const source = new HistorySource(''); source.#root = root; return source; }
78
93
  static fromParts(parts) {
79
- const leaves = Array.from(parts, text => sourceLeaf(text)).filter(Boolean);
94
+ const leaves = [];
95
+ for (const text of parts) {
96
+ if (typeof text !== 'string') throw new TypeError('source string required');
97
+ if (text.length <= INITIAL_LEAF_CHARS) {
98
+ const leaf = sourceLeaf(text);
99
+ if (leaf) leaves.push(leaf);
100
+ continue;
101
+ }
102
+ for (let from = 0; from < text.length;) {
103
+ let to = Math.min(text.length, from + INITIAL_LEAF_CHARS);
104
+ if (to < text.length && (text.charCodeAt(to - 1) & 0xfc00) === 0xd800 && (text.charCodeAt(to) & 0xfc00) === 0xdc00) to++;
105
+ leaves.push(sourceLeaf(text, from, to));
106
+ from = to;
107
+ }
108
+ }
80
109
  const build = (from, to) => from === to ? null : to - from === 1 ? leaves[from]
81
110
  : sourceNode(build(from, Math.floor((from + to) / 2)), build(Math.floor((from + to) / 2), to));
82
111
  return HistorySource.#fromRoot(build(0, leaves.length));
@@ -0,0 +1,10 @@
1
+ // SPDX-License-Identifier: MIT
2
+ export const PDF_SOURCE_DIGEST = '/RapierSourceDigest <' + '0'.repeat(64) + '>';
3
+
4
+ export async function pdfSourceDigest(bytes, start, end) {
5
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || start + 64 > end || end > bytes.length)
6
+ throw new Error('Invalid PDF source digest range.');
7
+ const bound = bytes.slice(0, end);
8
+ bound.fill(48, start, start + 64);
9
+ return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', bound)), byte => byte.toString(16).padStart(2, '0')).join('');
10
+ }
@@ -1,4 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
+ import {PDF_SOURCE_DIGEST, pdfSourceDigest} from './pdf-source-digest.mjs';
2
3
  function pdfStructure(input) {
3
4
  const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
4
5
  const fail = () => { throw Object.assign(new Error('The PDF source could not be attached.'), {code: 'pdf_source_invalid'}); };
@@ -116,10 +117,12 @@ export async function attachPdfSource(input, {source, filename, maxBytes = Infin
116
117
  table += place ? String(place.at).padStart(10, '0') + ' ' + String(place.generation).padStart(5, '0') + ' n \n' : '0000000000 00000 f \n';
117
118
  }
118
119
  const identity = trailer.fields.get('ID');
119
- table += 'trailer\n<< /Size ' + totalSize + ' /Root ' + rootId + ' ' + generation + ' R /Info ' + infoId + ' 0 R' + (identity ? ' /ID ' + identity.value : '') + ' >>\nstartxref\n' + length + '\n%%EOF\n';
120
+ table += 'trailer\n<< /Size ' + totalSize + ' /Root ' + rootId + ' ' + generation + ' R /Info ' + infoId + ' 0 R' + (identity ? ' /ID ' + identity.value : '') + ' ' + PDF_SOURCE_DIGEST + ' >>\nstartxref\n' + length + '\n%%EOF\n';
121
+ const digestAt = length + table.indexOf(PDF_SOURCE_DIGEST) + PDF_SOURCE_DIGEST.indexOf('<') + 1;
120
122
  parts.push(ascii(table)); length += table.length;
121
123
  if (length > maxBytes) throw Object.assign(new Error('The exported file exceeds the file limit.'), {code: 'export_too_large'});
122
124
  const result = new Uint8Array(length); let at = 0;
123
125
  for (const chunk of parts) { result.set(chunk, at); at += chunk.length; }
126
+ result.set(encode.encode(await pdfSourceDigest(result, digestAt, result.length - 1)), digestAt);
124
127
  check(); return result;
125
128
  }
@@ -157,14 +157,15 @@ export async function collectPdfVectors(main, {width, height, count, capture, si
157
157
  const wrap = document.createElement('div');
158
158
  wrap.style.cssText = `position:fixed;left:0;top:0;width:${box.width}px;height:${box.height}px;pointer-events:none;background:transparent`;
159
159
  clone.style.setProperty('width', box.width + 'px', 'important'); clone.style.setProperty('height', box.height + 'px', 'important');
160
- wrap.append(clone); document.body.append(wrap);
160
+ const surface = document.createElement('div'); surface.style.cssText = 'width:100%;height:100%';
161
+ surface.append(clone); wrap.attachShadow({mode: 'open'}).append(surface); document.body.append(wrap);
161
162
  try {
162
163
  const plainText = node.localName === 'text' && window.getComputedStyle(node).filter === 'none' && window.getComputedStyle(node).textShadow === 'none';
163
164
  const bounds = plainText ? node.getBoundingClientRect() : box;
164
165
  const left = Math.max(0, bounds.left - box.left - 2), top = Math.max(0, bounds.top - box.top - 2);
165
166
  const right = Math.min(box.width, bounds.right - box.left + 2), bottom = Math.min(box.height, bounds.bottom - box.top + 2);
166
167
  if (!(right > left && bottom > top)) return;
167
- const result = await capture({root: wrap, clip: {x: left, y: top, width: right - left, height: bottom - top}, transparent: true, scale: 2, signal, current, pixels: true}); check();
168
+ const result = await capture({root: surface, clip: {x: left, y: top, width: right - left, height: bottom - top}, transparent: true, scale: 2, signal, current, pixels: true}); check();
168
169
  if (result?.rgb instanceof Uint8Array && result?.alpha instanceof Uint8Array) {
169
170
  return {image: {rgb: await compress(result.rgb), alpha: await compress(result.alpha), width: result.width, height: result.height}, x: box.left - origin.left + left, y: box.top - origin.top + top, width: right - left, height: bottom - top};
170
171
  }
@@ -715,7 +715,7 @@ function _rapierRenderSemanticRoot(canonical, metadata, options = {}) {
715
715
  }
716
716
  root.innerHTML = sanitizeRapierHtml(rendered, options.page ? 'source' : 'export');
717
717
  if (cutAt >= 0) _rapierAppendNestingCut(root, markdown, cutAt);
718
- _rapierApplyBlockDirection(root);
718
+ _rapierApplyBlockDirection(root, false, options);
719
719
 
720
720
  _rapierAssignHeadingSlugs(root, RAPIER_RENDERED_HEADING_SELECTOR, false, options.carry?.slugs, options.carry?.headingIds);
721
721
  _rapierDisambiguateRenderedAnchors(root);
@@ -791,7 +791,7 @@ async function _rapierRenderSemanticRootAsync(canonical, metadata, options = {})
791
791
  if (html) append();
792
792
  if (cutAt >= 0) _rapierAppendNestingCut(root, markdown, cutAt);
793
793
  parsed = tokens = env = labels = null;
794
- await finishAsync(_rapierSemanticDecorations(root, options.carry), work);
794
+ await finishAsync(_rapierSemanticDecorations(root, options.carry, options), work);
795
795
  await pause(work, .6);
796
796
  complete = true;
797
797
  return root;
@@ -846,8 +846,8 @@ function _rapierReadPartDecorations(canonical, metadata, options = {}) {
846
846
  return Object.freeze({headingIds, captionStart});
847
847
  }
848
848
 
849
- function* _rapierSemanticDecorations(root, carry = null) {
850
- yield* _rapierApplyBlockDirectionSteps(root); yield;
849
+ function* _rapierSemanticDecorations(root, carry = null, options = {}) {
850
+ yield* _rapierApplyBlockDirectionSteps(root, false, options); yield;
851
851
  yield* _rapierAssignHeadingSlugsSteps(root, RAPIER_RENDERED_HEADING_SELECTOR, false, carry?.slugs, carry?.headingIds); yield;
852
852
  yield* _rapierDisambiguateRenderedAnchorsSteps(root); yield;
853
853
  for (const code of root.querySelectorAll('pre > code')) { _rapierNormalizeCodeElement(code); yield; }
@@ -1159,14 +1159,20 @@ function _rapierRawHtmlFragment(source, rewrite, parser) {
1159
1159
  return out;
1160
1160
  }
1161
1161
 
1162
- function _rapierApplyBlockDirection(root) { return finish(_rapierApplyBlockDirectionSteps(root)); }
1162
+ function _rapierApplyBlockDirection(root, includeRoot = false, options = {}) { return finish(_rapierApplyBlockDirectionSteps(root, includeRoot, options)); }
1163
1163
 
1164
- function* _rapierApplyBlockDirectionSteps(root) {
1164
+ function* _rapierApplyBlockDirectionSteps(root, includeRoot = false, {authoredTableDirection = false} = {}) {
1165
1165
  if (!root || typeof root.querySelectorAll !== 'function') return;
1166
+ const tableContainers = new Set();
1167
+ if (authoredTableDirection) for (const table of root.querySelectorAll('table')) {
1168
+ for (let parent = table.parentElement; parent && parent !== root && !tableContainers.has(parent); parent = parent.parentElement) {
1169
+ tableContainers.add(parent); yield;
1170
+ }
1171
+ }
1166
1172
  const blocks = new Set(_RAPIER_DIR_BLOCKS.split(',')), elements = root.ownerDocument.createTreeWalker(root, 1);
1167
- for (let el = elements.nextNode(); el; el = elements.nextNode()) {
1173
+ for (let el = includeRoot ? root : elements.nextNode(); el; el = elements.nextNode()) {
1168
1174
  yield;
1169
- if (!blocks.has(el.localName)) continue;
1175
+ if (!blocks.has(el.localName) || tableContainers.has(el)) continue;
1170
1176
  let literal = false;
1171
1177
  for (let parent = el.parentElement; parent; parent = parent.parentElement)
1172
1178
  if (parent.localName === 'pre' || parent.localName === 'code') { literal = true; break; }
@@ -2,11 +2,12 @@
2
2
  import {collectPdfVectors} from './pdf-vectors.mjs';
3
3
  import {collectPdfGlyphs} from './pdf-glyphs.mjs';
4
4
  import {pdfFontSources, waitForFonts} from './export-fonts.mjs';
5
- import {apply as applyTypography} from '../layout/typography.mjs';
5
+ import {printTypography} from './artifact-modules.mjs';
6
6
  import {documentLanguage, pageSizeOf, paperForLanguage} from '../spec/document-settings.mjs';
7
7
  import {applyPrintCapitals} from './print-capitals.mjs';
8
8
  import {stripOneTerminator as _rapierWillStripOneTerminator} from '../agent/will.mjs';
9
9
  import {quotedWillSpan} from '../spec/md-marks.mjs';
10
+ import {PDF_SOURCE_DIGEST, pdfSourceDigest} from './pdf-source-digest.mjs';
10
11
  const RAPIER_WILL_SENTINEL_MARK = '⁣';
11
12
  // SPDX-License-Identifier: MIT
12
13
  function createPrintRenderer(runtime) {
@@ -209,6 +210,24 @@ const REFUSALS = {
209
210
  export_too_large: 'The file is larger than the limit. Split the document.',
210
211
  };
211
212
 
213
+ function sliceTallDrawings(main, pageHeight) {
214
+ const document = main.ownerDocument;
215
+ for (const svg of main.querySelectorAll('svg.rapier-native-flowchart, svg.rapier-diagram')) {
216
+ const box = svg.getBoundingClientRect(), view = (svg.getAttribute('viewBox') || '').trim().split(/[\s,]+/).map(Number);
217
+ if (!(box.height > pageHeight) || view.length !== 4 || !view.every(Number.isFinite) || !(view[2] > 0) || !(view[3] > 0)) continue;
218
+ const unit = view[3] / box.height, windows = [];
219
+ for (let offset = 0; offset < box.height; offset += pageHeight) {
220
+ const shown = Math.min(pageHeight, box.height - offset), part = svg.cloneNode(true);
221
+ part.setAttribute('viewBox', [view[0], view[1] + offset * unit, view[2], shown * unit].join(' '));
222
+ part.setAttribute('preserveAspectRatio', 'xMidYMin meet');
223
+ part.style.setProperty('width', box.width + 'px', 'important'); part.style.setProperty('height', shown + 'px', 'important');
224
+ part.style.setProperty('break-before', 'column', 'important'); part.style.setProperty('break-inside', 'avoid', 'important');
225
+ windows.push(part);
226
+ }
227
+ svg.replaceWith(...windows);
228
+ }
229
+ }
230
+
212
231
  async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = false, filename, title, settings, templatePage = null}, {capture, signal, current, maxBytes = 8 * 1024 * 1024, film, ink} = {}) {
213
232
  const refuse = code => { throw Object.assign(new Error(code), {code}); };
214
233
  const spoken = error => {
@@ -229,7 +248,8 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
229
248
  if (bodyWidth < 1 || bodyHeight < 1) refuse('pdf_page_geometry_invalid');
230
249
  const tiled = bodyWidth > CAPTURE_EDGE || bodyHeight > CAPTURE_EDGE || bodyWidth * bodyHeight > CAPTURE_PIXELS;
231
250
  const frame = document.createElement('iframe');
232
- let vectors, backdrop = null, nativeGlyphs, filmSession;
251
+ let vectors, backdrop = null, nativeGlyphs, filmSession, typography;
252
+ const omitted = [];
233
253
  let rejectAbort;
234
254
  const aborted = new Promise((_, reject) => { rejectAbort = reject; });
235
255
  aborted.catch(() => {});
@@ -281,7 +301,19 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
281
301
  main.getBoundingClientRect();
282
302
  if (fontCss) await step(waitForFonts(main, {fontCss, css, settings}));
283
303
  await step(into.fonts.ready);
284
- for (const image of main.querySelectorAll('img')) { image.loading = 'eager'; await step(image.decode()); }
304
+ for (const image of main.querySelectorAll('img')) {
305
+ image.loading = 'eager';
306
+ try { await step(image.decode()); }
307
+ catch (error) {
308
+ if (error?.name !== 'EncodingError') throw error;
309
+ const source = image.getAttribute('src') || '';
310
+ omitted.push(source.startsWith('data:') ? image.getAttribute('alt') || 'a picture' : source.slice(0, 120));
311
+ const holder = image.closest('p,figure');
312
+ image.remove();
313
+ if (holder && !holder.textContent.trim() && !holder.querySelector('img,svg,math,table,pre')) holder.remove();
314
+ }
315
+ }
316
+ sliceTallDrawings(main, bodyHeight);
285
317
  wrapMasks(main);
286
318
  check();
287
319
  for (const node of [main, ...main.querySelectorAll('*')]) {
@@ -303,7 +335,8 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
303
335
  main.style.setProperty(name, value, 'important');
304
336
  globalThis.RapierMath?.fit(main);
305
337
  applyPrintCapitals(main);
306
- applyTypography(main, {all: true});
338
+ typography = printTypography(main, globalThis.RapierArtifactLayoutDependencies);
339
+ typography?.apply(main, {all: true});
307
340
  ink?.(main);
308
341
  const count = Math.max(1, Math.ceil((main.scrollWidth - 1) / bodyWidth));
309
342
  if (!Number.isSafeInteger(count)) refuse('pdf_page_geometry_invalid');
@@ -421,9 +454,9 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
421
454
  const bytes = await writeRasterPdf({pages, textPages, linkPages, vectorPages: vectors.pages, canonical, bom, filename, title, font, width, height, margins, backdrop: backdrop?.pages[0] || null,
422
455
  pixelWidth: bodyWidth, pixelHeight: bodyHeight, pageNumbers: settings?.pagestyle === 'plain', maxBytes, check});
423
456
  check();
424
- return {bytes, pages: count, painted: nativeGlyphs.painted};
457
+ return {bytes, pages: count, painted: nativeGlyphs.painted, omitted};
425
458
  } catch (error) { throw spoken(error); }
426
- finally { filmSession?.close(); backdrop?.restore(); vectors?.restore(); nativeGlyphs?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
459
+ finally { filmSession?.close(); backdrop?.restore(); vectors?.restore(); nativeGlyphs?.restore(); typography?.retire(frame.contentDocument); signal?.removeEventListener('abort', abort); frame.remove(); }
427
460
  }
428
461
 
429
462
  async function _rapierPrintTextPages(main, pageWidth, count, check, origin = main.getBoundingClientRect(), nativeNodes = null, markerNodes = null) {
@@ -442,6 +475,18 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
442
475
  }
443
476
  const marker = markerNodes?.get(node);
444
477
  if (marker && marker.page < count) pages[marker.page].push({...marker, block: node, carrier: null, marker: true});
478
+ const taskItem = node.nodeType === 1 && node.matches('input[type="checkbox"]') && node.closest('li');
479
+ const taskParent = taskItem && node.parentElement;
480
+ if (taskItem && taskParent.firstElementChild === node && (taskParent === taskItem || taskParent.localName === 'p' && taskParent.parentElement === taskItem && taskItem.firstElementChild === taskParent)) {
481
+ const box = node.getBoundingClientRect(), style = into.defaultView.getComputedStyle(node);
482
+ if (box.width > 0 && box.height > 0 && style.visibility !== 'hidden' && style.visibility !== 'collapse') {
483
+ const page = Math.max(0, Math.min(count - 1, Math.floor((box.left - origin.left + .01) / pageWidth)));
484
+ const size = parseFloat(style.fontSize) || 16, width = Math.min(size, box.width), height = Math.min(size, box.height);
485
+ const text = node.checked ? '[x] ' : '[ ] ', part = {text, x: box.left - origin.left - page * pageWidth + (box.width - width) / 2,
486
+ y: box.top - origin.top + (box.height - height) / 2, width, height};
487
+ pages[page].push({...part, parts: [part], block: taskItem, carrier: null, marker: true});
488
+ }
489
+ }
445
490
  const formula = node.nodeType === 1 && node.matches('.math-rendered[data-math-src]') && node.querySelector('svg');
446
491
  if (node.nodeType === 1 && !formula || node.nodeType === 3 && element.closest('.math-rendered[data-math-src]')) continue;
447
492
  let value;
@@ -885,7 +930,8 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
885
930
  cursor += (id + ' 0 obj\n').length + objects[id].length + 8;
886
931
  }
887
932
  const xrefAt = cursor;
888
- const xref = encode('xref\n0 ' + objects.length + '\n0000000000 65535 f \n' + offsets.slice(1).map(at => String(at).padStart(10, '0') + ' 00000 n \n').join('') + 'trailer\n<< /Size ' + objects.length + ' /Root 1 0 R /Info ' + info + ' 0 R >>\nstartxref\n' + xrefAt + '\n%%EOF\n');
933
+ const table = 'xref\n0 ' + objects.length + '\n0000000000 65535 f \n' + offsets.slice(1).map(at => String(at).padStart(10, '0') + ' 00000 n \n').join('') + 'trailer\n<< /Size ' + objects.length + ' /Root 1 0 R /Info ' + info + ' 0 R ' + PDF_SOURCE_DIGEST + ' >>\nstartxref\n' + xrefAt + '\n%%EOF\n';
934
+ const xref = encode(table), digestAt = xrefAt + table.indexOf(PDF_SOURCE_DIGEST) + PDF_SOURCE_DIGEST.indexOf('<') + 1;
889
935
  if (cursor + xref.length > maxBytes) fail('export_too_large');
890
936
  const bytes = new Uint8Array(cursor + xref.length);
891
937
  bytes.set(header, 0);
@@ -899,6 +945,8 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
899
945
  bytes.set(suffix, at); at += suffix.length;
900
946
  }
901
947
  bytes.set(xref, at);
948
+ bytes.set(encode(await pdfSourceDigest(bytes, digestAt, bytes.length - 1)), digestAt);
949
+ check();
902
950
  return bytes;
903
951
  }
904
952
  export {createPrintRenderer, applyPrintCapitals, RAPIER_WILL_SENTINEL_MARK as sentinel};
@@ -9,6 +9,7 @@ import {createLineMeasurer, linesHeight} from '../layout/model.mjs';
9
9
  import {markPrintCapitals, createPrintCapitals, followingPrintProse} from './print-capitals.mjs';
10
10
  import {freezePublishedInk} from './render-ink.mjs';
11
11
  import {isTemplatePage, isPageBorderLayout} from '../spec/template-page.mjs';
12
+ import {_rapierArtifactFactoryModules} from './artifact-modules.mjs';
12
13
  export function positionNativeDiagram(drawing) {
13
14
  const surface=drawing?.closest?.('.diagram-block,.diagram-live-preview');
14
15
  if(!surface?.clientWidth)return;
@@ -348,9 +349,14 @@ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
348
349
  yield;
349
350
  cache.innerHTML = sanitizeRapierHtml(cache.innerHTML, 'export');
350
351
  }
351
- return root;
352
352
  }
353
- return yield* cleanTree(root, sanitizeRapierHtml, 'export');
353
+ const admitted = opts.sanitized ? root : yield* cleanTree(root, sanitizeRapierHtml, 'export');
354
+ for (const list of admitted.querySelectorAll('ol[start]')) {
355
+ yield;
356
+ const start = list.getAttribute('start');
357
+ if (/^-?\d{1,9}$/.test(start)) list.style.setProperty('counter-reset', 'rapier-ol ' + (Number(start) - 1));
358
+ }
359
+ return admitted;
354
360
  }
355
361
 
356
362
  function _rapierDocxRenderStyles(root) {
@@ -1280,11 +1286,6 @@ function _rapierImageOpenRunsList(tokens) {
1280
1286
  return list;
1281
1287
  }
1282
1288
 
1283
- function _rapierArtifactFactoryModules(dependencies, group) {
1284
- const paths = dependencies.groups && dependencies.groups[group] ? dependencies.groups[group] : Object.keys(dependencies.factories);
1285
- return paths.map(path => 'modules[' + JSON.stringify(path) + '] = (' + Function.prototype.toString.call(dependencies.factories[path]) + ')();').join('\n');
1286
- }
1287
-
1288
1289
  function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1289
1290
  if (!root || !metadata || !geometry || !preparation) return null;
1290
1291
  const doc = root.ownerDocument, view = doc.defaultView;
@@ -0,0 +1,18 @@
1
+ // SPDX-License-Identifier: MIT
2
+ let graphemes;
3
+ export const segment = text => (graphemes ||= new Intl.Segmenter(undefined, {granularity: 'grapheme'})).segment(text);
4
+ export const opening = /^[\p{Ps}\p{Pi}\p{Pf}"']+$/u;
5
+
6
+ export function initialRange(input) {
7
+ const text = typeof input === 'string' ? input : input?.textContent;
8
+ if (typeof text !== 'string') return null;
9
+ const start = /^\s*/u.exec(text)[0].length;
10
+ if (start === text.length) return null;
11
+ let end = start;
12
+ for (const part of segment(text.slice(start))) {
13
+ if (/^\s/u.test(part.segment)) break;
14
+ end = start + part.index + part.segment.length;
15
+ if (!opening.test(part.segment)) break;
16
+ }
17
+ return end > start ? {start, end, text: text.slice(start, end)} : null;
18
+ }
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import {createPreparationScope, supportsRichStyle, captureFontAuthority} from './preparation.mjs';
3
3
  import {createParagraphFitter} from './paragraph-fit.mjs';
4
+ import {initialRange, segment, opening} from './initial-range.mjs';
4
5
 
5
6
  export function createTypography() {
6
7
  const fitter = createParagraphFitter();
@@ -16,25 +17,8 @@ export function createTypography() {
16
17
  if (!value) { value = createPreparationScope(document); documents.set(document, value); }
17
18
  return value;
18
19
  }
19
- let graphemes;
20
- const segment = text => (graphemes ||= new Intl.Segmenter(undefined, {granularity: 'grapheme'})).segment(text);
21
- const opening = /^[\p{Ps}\p{Pi}\p{Pf}"']+$/u;
22
20
  const closing = /^[\p{Pe}\p{Pi}\p{Pf}"'.,;:!?]+$/u;
23
21
 
24
- function initialRange(input) {
25
- const text = typeof input === 'string' ? input : input?.textContent;
26
- if (typeof text !== 'string') return null;
27
- const start = /^\s*/u.exec(text)[0].length;
28
- if (start === text.length) return null;
29
- let end = start;
30
- for (const part of segment(text.slice(start))) {
31
- if (/^\s/u.test(part.segment)) break;
32
- end = start + part.index + part.segment.length;
33
- if (!opening.test(part.segment)) break;
34
- }
35
- return end > start ? {start, end, text: text.slice(start, end)} : null;
36
- }
37
-
38
22
  const flowFloat = '.rapier-flow-float';
39
23
  const own = paragraph => [paragraph, ...[...paragraph.querySelectorAll('*')].filter(element => !element.closest(flowFloat))];
40
24
 
@@ -770,7 +754,7 @@ export const apply = typography.apply;
770
754
  export const clear = typography.clear;
771
755
  export const clearSelection = typography.clearSelection;
772
756
  export const firstLineEnd = typography.firstLineEnd;
773
- export const initialRange = typography.initialRange;
757
+ export {initialRange};
774
758
  export const protectInitial = typography.protectInitial;
775
759
  export const invalidateFonts = typography.invalidateFonts;
776
760
  export const retire = typography.retire;
@@ -5,7 +5,7 @@ export function separate(c, endA = c.before.length, endB = c.after.length) {
5
5
  const {before, after, safe, raw, spend} = c;
6
6
  if (!raw) for (let i = c.result.length - 1; i > 0; i--) {
7
7
  const p = c.result[i - 1], q = c.result[i], side = p[0][0] === p[0][1] ? 1 : p[1][0] === p[1][1] ? 0 : -1, text = side ? after : before;
8
- if (side >= 0 && !/\S/.test(text.slice(...p[side]))) {
8
+ if (!c.evidence && side >= 0 && !/\S/.test(text.slice(...p[side]))) {
9
9
  let n = 0;
10
10
  while (p[0][1] + n < q[0][0] && text[p[side][0] + n] === text[p[side][1] + n] && spend()) n++;
11
11
  if (p[0][1] + n === q[0][0] && p.slice(0, 2).every((span, k) => safe(k ? after : before, span[0] + n) && safe(k ? after : before, span[1] + n)))
@@ -20,7 +20,7 @@ export function separate(c, endA = c.before.length, endB = c.after.length) {
20
20
  keep[3][0] = x[0] === x[1] ? 2 : y[0] === y[1] ? 0 : 1;
21
21
  c.result.splice(i - 1, 2, [x, y, false, keep[3]]);
22
22
  }
23
- if (!raw) for (let i = 0; i < c.result.length; i++) {
23
+ if (!raw && !c.evidence) for (let i = 0; i < c.result.length; i++) {
24
24
  const p = c.result[i], q = c.result[i + 1], side = p[0][0] === p[0][1] ? 1 : p[1][0] === p[1][1] ? 0 : -1;
25
25
  if (side < 0 || p[2]) continue;
26
26
  const text = side ? after : before;
@@ -18,12 +18,19 @@ function parserFor(parser) {
18
18
  return md;
19
19
  }
20
20
 
21
+ const sourceMetadataFields = ['license', 'origin'];
22
+ function sourceMetadata(value) {
23
+ return Object.fromEntries(sourceMetadataFields.filter(field => value[field] != null).map(field => [field, value[field]]));
24
+ }
25
+
21
26
  function faceOf(value) {
22
27
  const family = normalizeFontFamily(value?.family), style = value?.style || 'normal';
23
28
  const weight = value?.weight ?? 400, stretch = value?.stretch ?? 100;
24
29
  if (!family || !['normal', 'italic', 'oblique'].includes(style) || !Number.isFinite(weight) || weight < 1 || weight > 1000 ||
25
30
  !Number.isFinite(stretch) || stretch <= 0 || stretch > 1000) return null;
26
31
  const result = {family, style, weight, stretch};
32
+ if (value.subsetted != null && typeof value.subsetted !== 'boolean') return null;
33
+ if (value.subsetted === true || value.subset?.subset === true) result.subsetted = true;
27
34
  if (value.weightRange != null) {
28
35
  const range = value.weightRange;
29
36
  if (!Array.isArray(range) || range.length !== 2 || range.some(number => !Number.isFinite(number) || number < 1 || number > 1000) || range[0] > range[1]) return null;
@@ -44,6 +51,25 @@ function faceOf(value) {
44
51
  if (!/^f[0-9a-f]{24}$/.test(value.sourceId)) return null;
45
52
  result.sourceId = value.sourceId;
46
53
  }
54
+ if (value.license != null) {
55
+ const license = value.license;
56
+ if (!license || !['OFL-1.0', 'OFL-1.1', 'Apache-2.0', 'Ubuntu-font-1.0'].includes(license.id) ||
57
+ typeof license.text !== 'string' || !license.text.trim() || new TextEncoder().encode(license.text).length > 128 * 1024 ||
58
+ typeof license.url !== 'string' || typeof license.sri !== 'string' || !/^sha384-[A-Za-z0-9+/]{64}$/.test(license.sri)) return null;
59
+ let url;
60
+ try { url = new URL(license.url); } catch (_) { return null; }
61
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) return null;
62
+ result.license = {id: license.id, text: license.text, url: license.url, sri: license.sri};
63
+ }
64
+ if (value.origin != null) {
65
+ const origin = value.origin;
66
+ if (!result.license || result.subsetted || !origin || origin.catalog !== 'google-fonts' ||
67
+ typeof origin.family !== 'string' || !/^[a-z0-9][a-z0-9-]{0,95}$/.test(origin.family) ||
68
+ typeof origin.version !== 'string' || !origin.version || origin.version.length > 128 || /[\u0000-\u001f\u007f]/u.test(origin.version) ||
69
+ typeof origin.file !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\.woff2$/.test(origin.file) ||
70
+ typeof origin.sri !== 'string' || !/^sha384-[A-Za-z0-9+/]{64}$/.test(origin.sri)) return null;
71
+ result.origin = {catalog: origin.catalog, family: origin.family, version: origin.version, file: origin.file, sri: origin.sri};
72
+ }
47
73
  return result;
48
74
  }
49
75
 
@@ -63,7 +89,7 @@ export function readFontAssets(markdown, parser) {
63
89
  else if (source[at] === '\n') starts.push(at + 1);
64
90
  }
65
91
  md.block.parse(frame.body.replace(/\r\n?/g, '\n').replace(/\0/g, '\ufffd'), md, env, tokens);
66
- const labels = new Set(), faces = new Set();
92
+ const labels = new Set(), faces = new Set(), metadata = new Map(), faceMetadata = new Map(), subsetUrls = new Set();
67
93
  for (const token of tokens) {
68
94
  if (token.type !== 'reference_definition' || !token.map) continue;
69
95
  const id = token.meta.label, first = !labels.has(id);
@@ -79,18 +105,33 @@ export function readFontAssets(markdown, parser) {
79
105
  if (!match) continue;
80
106
  const label = md.utils.unescapeAll(match[1]);
81
107
  if (!data) { refused.push({name: label, reason: 'The font reference must contain an embedded font.'}); continue; }
82
- let metadata = {family: label.slice(5)};
108
+ let definitionMetadata = {family: label.slice(5)};
83
109
  if (definition.title) {
84
- try { metadata = JSON.parse(definition.title); }
110
+ try { definitionMetadata = JSON.parse(definition.title); }
85
111
  catch (_) { refused.push({name: label, reason: 'The font reference has invalid metadata.'}); continue; }
86
112
  }
87
- const face = faceOf(metadata);
113
+ const face = faceOf(definitionMetadata);
88
114
  if (!face) { refused.push({name: label, reason: 'The font reference has invalid metadata.'}); continue; }
89
115
  const key = fontAssetKey(face), active = first && !faces.has(key);
116
+ const conflict = sourceMetadataFields.find(field => face[field] && [metadata.get(url)?.[field], faceMetadata.get(key)?.[field]]
117
+ .some(prior => prior && JSON.stringify(prior) !== JSON.stringify(face[field])));
118
+ if (conflict) { refused.push({name: label, reason: 'The font references have conflicting ' + conflict + ' metadata.'}); continue; }
119
+ if (face.subsetted && metadata.get(url)?.origin || face.origin && subsetUrls.has(url)) {
120
+ refused.push({name: label, reason: 'A font subset cannot claim an original catalog file.'}); continue;
121
+ }
122
+ metadata.set(url, {...metadata.get(url), ...sourceMetadata(face)});
123
+ faceMetadata.set(key, {...faceMetadata.get(key), ...sourceMetadata(face)});
124
+ if (face.subsetted) subsetUrls.add(url);
90
125
  const row = {...face, ...data, url, label, id, key, source: raw, start, end, next, active, topLevel: token.level === 0};
91
126
  blocks.push(row);
92
127
  if (active) { assets.push(row); faces.add(key); }
93
128
  }
129
+ for (let index = 0; index < assets.length; index++) {
130
+ const asset = assets[index], declared = metadata.get(asset.url);
131
+ for (const field of sourceMetadataFields) if (!asset[field] && declared?.[field])
132
+ assets[index] = {...assets[index], [field]: declared[field]};
133
+ if (subsetUrls.has(asset.url) && !asset.subsetted) assets[index] = {...assets[index], subsetted: true};
134
+ }
94
135
  return {assets, blocks, refused, references: env.references || Object.create(null)};
95
136
  }
96
137
 
@@ -113,13 +154,26 @@ function escapeFontLabel(label) {
113
154
  }
114
155
 
115
156
  export function writeFontAssets(markdown, assets, parser) {
116
- const source = String(markdown ?? ''), md = parserFor(parser), index = readFontAssets(source, md), wanted = new Map();
157
+ const source = String(markdown ?? ''), md = parserFor(parser), index = readFontAssets(source, md), wanted = new Map(), metadata = new Map(), subsetUrls = new Set();
117
158
  for (const asset of assets || []) {
118
- const key = fontAssetKey(asset);
159
+ const key = fontAssetKey(asset), face = faceOf(asset);
119
160
  if (!key || !fontDataUrl(asset?.url)) throw new TypeError('font: invalid embedded asset');
120
161
  if (wanted.has(key) && wanted.get(key).url !== asset.url) throw new TypeError('font: one file owns each family face');
162
+ const conflict = sourceMetadataFields.find(field => face[field] && metadata.get(asset.url)?.[field] &&
163
+ JSON.stringify(metadata.get(asset.url)[field]) !== JSON.stringify(face[field]));
164
+ if (conflict) throw new TypeError('font: conflicting ' + conflict + ' metadata');
165
+ if (face.subsetted && metadata.get(asset.url)?.origin || face.origin && subsetUrls.has(asset.url))
166
+ throw new TypeError('font: a subset cannot claim an original catalog file');
167
+ metadata.set(asset.url, {...metadata.get(asset.url), ...sourceMetadata(face)});
168
+ if (face.subsetted) subsetUrls.add(asset.url);
121
169
  wanted.set(key, asset);
122
170
  }
171
+ for (const [key, asset] of wanted) {
172
+ const declared = metadata.get(asset.url);
173
+ for (const field of sourceMetadataFields) if (!asset[field] && declared?.[field])
174
+ wanted.set(key, {...wanted.get(key), [field]: declared[field]});
175
+ if (subsetUrls.has(asset.url) && !asset.subsetted) wanted.set(key, {...wanted.get(key), subsetted: true});
176
+ }
123
177
  const edits = [], consumed = new Set(), labels = new Set(Object.keys(index.references));
124
178
  const retired = index.assets.filter(row => !wanted.has(row.key));
125
179
  for (const row of index.blocks) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-markdown-kit",
3
- "version": "1.2.46",
3
+ "version": "1.2.49",
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",
package/style.css CHANGED
@@ -264,9 +264,12 @@
264
264
  @media (prefers-reduced-motion:reduce){
265
265
  :where(.md-render) svg.rapier-diagram[aria-roledescription^="flowchart"] :is(path,line,polyline):is(.edge-animation-fast,.edge-animation-slow){animation:none!important;stroke-dashoffset:0!important}
266
266
  }
267
- /* Printed diagrams fit the page. */
267
+ /* Printed diagrams fit the page's width, at their own size; a drawing taller than a page continues onto the next (the
268
+ export slices it, the browser's print fragments it) and the figure's screen height is not held. */
268
269
  @media print{
269
- :where(.md-render) .diagram-block{overflow:visible!important}
270
+ :where(.md-render) .diagram-block{overflow:visible!important;min-height:0!important}
271
+ /* The frame's rules are a background, not ink, and the figure's box and its cache hold no words: the drawing alone is the block. */
272
+ :where(.md-render) .diagram-block:is([data-diagram-state="ready"],[data-diagram-state="idle"]),:where(.md-render) .diagram-block .diagram-cache{display:contents!important}
270
273
  :where(.md-render) svg.rapier-diagram,:where(.md-render) svg.rapier-native-flowchart{width:auto!important;max-width:100%!important}
271
274
  }
272
275
  :where(.md-render) .diagram-reason {