rapier-markdown-kit 1.2.30 → 1.2.32
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 +1 -1
- package/dist/kit/conformance/README.md +1 -1
- package/dist/kit/ledger/carried.mjs +14 -2
- package/dist/kit/markdown-it.mjs +19 -5
- package/dist/kit/pdf-glyphs.mjs +2 -2
- package/dist/kit/pdf-vectors.mjs +29 -9
- package/dist/kit/render-markdown.mjs +8 -1
- package/dist/kit/render-print.mjs +67 -26
- package/dist/kit/render.mjs +36 -19
- package/dist/layout/hyphenation.mjs +15 -3
- package/dist/layout/model.mjs +9 -1
- package/dist/layout/paragraph-fit.mjs +3 -3
- package/dist/layout/typography.mjs +149 -29
- package/dist/spec/document-fonts.mjs +5 -0
- package/dist/spec/document-settings.mjs +7 -5
- package/dist/spec/html-blocks.mjs +30 -11
- package/dist/spec/md-assets.mjs +2 -1
- package/dist/spec/template-page.mjs +2 -4
- package/package.json +1 -1
- package/style.css +17 -0
package/README.md
CHANGED
|
@@ -164,7 +164,7 @@ family name share the host's font namespace. Print retains its existing factory
|
|
|
164
164
|
|
|
165
165
|
| Import | What it is |
|
|
166
166
|
| --- | --- |
|
|
167
|
-
| `rapier-markdown-kit/markdown-it` | `md.use(rapierMarkdown)`: the reference style's markup in markdown-it, in one line. |
|
|
167
|
+
| `rapier-markdown-kit/markdown-it` | `md.use(rapierMarkdown)`: the reference style's markup in markdown-it, in one line, with rung 1 of `md-layout:v1` (align, indents, list markers, picture width, lines and opacity, paragraph typography). |
|
|
168
168
|
| `rapier-markdown-kit/parsed` | A frozen, serializable view of completed Markdown tokens, bound to source, revision and grammar. |
|
|
169
169
|
| `rapier-markdown-kit/layout` | The `md-layout:v1` comment a picture carries: parse it, validate it, write it back. |
|
|
170
170
|
| `rapier-markdown-kit/marks` | Unified typography values, span writer and parser installer; preserved color/font comments, ink and page breaks. |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# The conformance kit
|
|
2
2
|
|
|
3
3
|
Thirteen small documents, each covering one placement fact from [the Markdown
|
|
4
|
-
standard](https://
|
|
4
|
+
standard](https://github.com/jackskip22/rapier/blob/main/docs/markdown-standard.md): inline (no layout comment), width/x/align, `wrap=around`
|
|
5
5
|
with a real alpha silhouette, `wrap=box`, `behind`, `front`, `rotate=` on a plain raster, a foreign
|
|
6
6
|
SVG "drawing" already turned in its own bytes, a ring drawing whose unfilled interior takes a line of
|
|
7
7
|
text, both-sides wrapping around a centred picture, the neighbour search skipping a second, unrelated
|
|
@@ -50,15 +50,27 @@ const OPEN = '\n\n<!-- rapier-carried/1\n', CLOSE = '\n-->\n';
|
|
|
50
50
|
export function documentTrailer(text, options) {
|
|
51
51
|
const parts = validateParts(text, options);
|
|
52
52
|
if (!parts.ledger && !parts.authorship) return '';
|
|
53
|
-
const json = JSON.stringify(parts).replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
|
|
53
|
+
const json = JSON.stringify(parts).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/-/g, '\\u002d');
|
|
54
54
|
return OPEN + json + CLOSE;
|
|
55
55
|
}
|
|
56
56
|
export function writeDocument(text, options) {
|
|
57
57
|
return text + documentTrailer(text, options);
|
|
58
58
|
}
|
|
59
|
+
function unfinishedTrailer(source) {
|
|
60
|
+
const at = source.lastIndexOf(OPEN);
|
|
61
|
+
if (at < 0) return false;
|
|
62
|
+
let fence = null;
|
|
63
|
+
for (const line of source.slice(0, at).split('\n')) {
|
|
64
|
+
const found = /^ {0,3}(`{3,}|~{3,})/.exec(line);
|
|
65
|
+
if (!found) continue;
|
|
66
|
+
if (!fence) fence = found[1];
|
|
67
|
+
else if (found[1][0] === fence[0] && found[1].length >= fence.length && !line.slice(found[0].length).trim()) fence = null;
|
|
68
|
+
}
|
|
69
|
+
return !fence;
|
|
70
|
+
}
|
|
59
71
|
export function readDocument(source) {
|
|
60
72
|
if (!source.endsWith(CLOSE)) {
|
|
61
|
-
if (source
|
|
73
|
+
if (unfinishedTrailer(source)) throw fault('unfinished document metadata');
|
|
62
74
|
return {text: source, ledger: null, authorship: null};
|
|
63
75
|
}
|
|
64
76
|
const at = source.lastIndexOf(OPEN);
|
package/dist/kit/markdown-it.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
import {parseLayout, imageStyle} from './layout.mjs';
|
|
2
|
+
import {parseLayout, imageStyle, textStyle} from './layout.mjs';
|
|
3
3
|
import {typography, isPageBreakBlock} from './marks.mjs';
|
|
4
4
|
import {installMarkdownImages} from './assets.mjs';
|
|
5
5
|
import {tuneMarkdownEngine} from '../spec/md-engine.mjs';
|
|
@@ -26,11 +26,25 @@ export function rapierMarkdown(md) {
|
|
|
26
26
|
const layout = parseLayout(marker.content);
|
|
27
27
|
if (!layout) continue;
|
|
28
28
|
children.pop();
|
|
29
|
-
if (layout.align) owner.attrSet('data-md-align', layout.align);
|
|
29
|
+
if (layout.align && (layout.align !== 'justify' || owner.type === 'paragraph_open')) owner.attrSet('data-md-align', layout.align);
|
|
30
30
|
const visible = children.filter(token => token.type !== 'text' || token.content.trim());
|
|
31
|
-
|
|
32
|
-
visible[0].
|
|
33
|
-
|
|
31
|
+
const picture = visible.length === 1 && visible[0].type === 'image' ? visible[0]
|
|
32
|
+
: visible.length === 3 && visible[0].type === 'link_open' && visible[1].type === 'image' && visible[2].type === 'link_close' ? visible[1] : null;
|
|
33
|
+
if (picture) {
|
|
34
|
+
if (layout.width != null || layout.lines != null || layout.opacity != null) {
|
|
35
|
+
const style = imageStyle(layout);
|
|
36
|
+
if (style) picture.attrSet('style', style);
|
|
37
|
+
if (layout.width != null) picture.attrSet('data-md-image-width', String(layout.width));
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
for (const key of ['first', 'indent', 'before', 'after', 'lines', 'hyphens', 'caps', 'hanging']) if (layout[key]) owner.attrSet('data-md-' + key, String(layout[key]));
|
|
42
|
+
const style = [textStyle(layout), layout.lines != null ? '--md-drop-lines:' + layout.lines : ''].filter(Boolean).join(';');
|
|
43
|
+
if (style) owner.attrSet('style', style);
|
|
44
|
+
const item = state.tokens[i - 2];
|
|
45
|
+
if (layout.marker && item?.type === 'list_item_open') {
|
|
46
|
+
item.attrSet('data-md-marker', String(layout.marker));
|
|
47
|
+
item.attrSet('style', (item.attrGet('style') ? item.attrGet('style') + ';' : '') + textStyle({marker: layout.marker}));
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
50
|
});
|
package/dist/kit/pdf-glyphs.mjs
CHANGED
|
@@ -337,8 +337,8 @@ export async function collectPdfGlyphs(main, {width, check, faces = [], prepareF
|
|
|
337
337
|
probe.style.setProperty('visibility', 'hidden', 'important'); probe.textContent = String(value); item.prepend(probe);
|
|
338
338
|
const parts = await measure(probe.firstChild, style, font); probe.remove();
|
|
339
339
|
if (!parts.length || parts.some(part => !part.font)) continue;
|
|
340
|
-
parts.at(-1).text += ' ';
|
|
341
|
-
markers.set(item, {text: String(value) + ' ', parts, ...Object.fromEntries(['x','y','width','height','page'].map(key => [key, parts[0][key]]))});
|
|
340
|
+
parts.at(-1).text += '. ';
|
|
341
|
+
markers.set(item, {text: String(value) + '. ', parts, ...Object.fromEntries(['x','y','width','height','page'].map(key => [key, parts[0][key]]))});
|
|
342
342
|
const id = 'm' + markers.size, old = item.getAttribute('data-rapier-pdf-marker'); item.setAttribute('data-rapier-pdf-marker', id);
|
|
343
343
|
restoreSteps.push(() => old == null ? item.removeAttribute('data-rapier-pdf-marker') : item.setAttribute('data-rapier-pdf-marker', old));
|
|
344
344
|
markerRules.push('[data-rapier-pdf-marker="' + id + '"]::before{-webkit-text-fill-color:transparent!important}');
|
package/dist/kit/pdf-vectors.mjs
CHANGED
|
@@ -92,7 +92,7 @@ function geometry(node) {
|
|
|
92
92
|
unsupported();
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
export async function collectPdfVectors(main, {width, height, count, capture, signal, current, check, maxBytes, sanitizeSvg, collectText}) {
|
|
95
|
+
export async function collectPdfVectors(main, {width, height, count, capture, signal, current, check, maxBytes, sanitizeSvg, collectText, pictures = false, underlay = false}) {
|
|
96
96
|
const document = main.ownerDocument, window = document.defaultView, origin = main.getBoundingClientRect();
|
|
97
97
|
const pages = Array.from({length: count}, () => []), hidden = [], temporary = [], textRoots = [], textRestores = [];
|
|
98
98
|
const colorCanvas = document.createElement('canvas'); colorCanvas.width = colorCanvas.height = 1;
|
|
@@ -117,20 +117,33 @@ export async function collectPdfVectors(main, {width, height, count, capture, si
|
|
|
117
117
|
try {
|
|
118
118
|
for (const original of roots) {
|
|
119
119
|
check();
|
|
120
|
-
let root = original, mount = null;
|
|
120
|
+
let root = original, mount = null, turned = false, groupOpacity = 1, asImage = false, under = false;
|
|
121
121
|
const originalBox = original.getBoundingClientRect();
|
|
122
122
|
if (!(originalBox.width > 0 && originalBox.height > 0)) continue;
|
|
123
123
|
if (original.localName === 'img') {
|
|
124
124
|
if (typeof sanitizeSvg !== 'function') continue;
|
|
125
125
|
const address = original.getAttribute('src') || '', match = /^data:image\/svg\+xml(?:;charset=[^;,]+)?(;base64)?,([\s\S]*)$/i.exec(address);
|
|
126
|
-
if (!match) continue;
|
|
126
|
+
if (!match && !(pictures && /^data:image\//i.test(address))) continue;
|
|
127
|
+
asImage = !match;
|
|
128
|
+
}
|
|
129
|
+
if (original.localName === 'img' && !asImage) {
|
|
130
|
+
const address = original.getAttribute('src') || '', match = /^data:image\/svg\+xml(?:;charset=[^;,]+)?(;base64)?,([\s\S]*)$/i.exec(address);
|
|
127
131
|
let text;
|
|
128
132
|
try {text = match[1] ? new TextDecoder('utf-8', {fatal: true}).decode(Uint8Array.from(atob(match[2]), ch => ch.charCodeAt(0))) : decodeURIComponent(match[2]);} catch (_) {continue;}
|
|
129
133
|
const parsed = new window.DOMParser().parseFromString(sanitizeSvg(text), 'image/svg+xml');
|
|
130
134
|
if (parsed.querySelector('parsererror') || parsed.documentElement.namespaceURI !== SVG) continue;
|
|
135
|
+
const shown = window.getComputedStyle(original);
|
|
136
|
+
under = Number(shown.zIndex) < 0;
|
|
137
|
+
if (under && !underlay) continue;
|
|
138
|
+
turned = shown.transform !== 'none'; groupOpacity = Number(shown.opacity);
|
|
139
|
+
const unturnedWidth = turned ? Number.parseFloat(shown.width) : originalBox.width, unturnedHeight = turned ? Number.parseFloat(shown.height) : originalBox.height;
|
|
140
|
+
if (!(unturnedWidth > 0 && unturnedHeight > 0)) continue;
|
|
141
|
+
const [originX, originY] = shown.transformOrigin.split(' ').map(Number.parseFloat);
|
|
142
|
+
if (turned && !(Math.abs(originX - unturnedWidth / 2) < .5 && Math.abs(originY - unturnedHeight / 2) < .5)) continue;
|
|
143
|
+
const left = originalBox.left + originalBox.width / 2 - unturnedWidth / 2, top = originalBox.top + originalBox.height / 2 - unturnedHeight / 2;
|
|
131
144
|
root = document.importNode(parsed.documentElement, true);
|
|
132
|
-
root.style.setProperty('width',
|
|
133
|
-
mount = document.createElement('div'); mount.style.cssText = `position:fixed;left:${
|
|
145
|
+
root.style.setProperty('width', unturnedWidth + 'px', 'important'); root.style.setProperty('height', unturnedHeight + 'px', 'important');
|
|
146
|
+
mount = document.createElement('div'); mount.style.cssText = `position:fixed;left:${left}px;top:${top}px;width:${unturnedWidth}px;height:${unturnedHeight}px;pointer-events:none` + (turned ? `;transform:${shown.transform};transform-origin:50% 50%` : '');
|
|
134
147
|
mount.attachShadow({mode: 'open'}).append(root); document.body.append(mount); temporary.push(mount); await document.fonts.ready;
|
|
135
148
|
const glyphs = await collectText?.(root); if (glyphs) textRestores.push(glyphs.restore);
|
|
136
149
|
textRoots.push({root, original, text: glyphs});
|
|
@@ -181,7 +194,7 @@ export async function collectPdfVectors(main, {width, height, count, capture, si
|
|
|
181
194
|
} catch (_) { sequence.push({raster: node}); return; }
|
|
182
195
|
if (!path) return;
|
|
183
196
|
const matrix = node.getScreenCTM(); if (!matrix || ![matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f].every(Number.isFinite)) {sequence.push({raster: node}); return;}
|
|
184
|
-
const opacity = Number(style.opacity), fillAlpha = fill ? fill[3] * Number(style.fillOpacity) * opacity : 1, strokeAlpha = stroke ? stroke[3] * Number(style.strokeOpacity) * opacity : 1;
|
|
197
|
+
const opacity = Number(style.opacity) * groupOpacity, fillAlpha = fill ? fill[3] * Number(style.fillOpacity) * opacity : 1, strokeAlpha = stroke ? stroke[3] * Number(style.strokeOpacity) * opacity : 1;
|
|
185
198
|
let commands = nums([matrix.a, matrix.b, matrix.c, matrix.d, matrix.e - origin.left, matrix.f - origin.top]) + ' cm\n';
|
|
186
199
|
if (fill) commands += nums(fill.slice(0, 3)) + ' rg\n';
|
|
187
200
|
if (stroke) {
|
|
@@ -193,12 +206,19 @@ export async function collectPdfVectors(main, {width, height, count, capture, si
|
|
|
193
206
|
commands += path + (fill && stroke ? style.fillRule === 'evenodd' ? 'B*' : 'B' : fill ? style.fillRule === 'evenodd' ? 'f*' : 'f' : stroke ? 'S' : 'n') + '\n';
|
|
194
207
|
sequence.push({commands, fillAlpha, strokeAlpha}); geometries++;
|
|
195
208
|
};
|
|
196
|
-
await walk(root);
|
|
197
|
-
if (
|
|
209
|
+
if (asImage) sequence.push({raster: root}); else await walk(root);
|
|
210
|
+
if ((turned || groupOpacity !== 1) && (sequence.some(piece => piece.raster) || textRoots.find(entry => entry.root === root)?.text?.nodes.size)) {
|
|
211
|
+
const entry = textRoots.find(row => row.root === root);
|
|
212
|
+
if (entry?.text) textRestores.pop()?.();
|
|
213
|
+
if (entry) textRoots.splice(textRoots.indexOf(entry), 1);
|
|
214
|
+
mount?.remove(); temporary.splice(temporary.indexOf(mount), 1);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (!geometries && !asImage && !textRoots.find(entry => entry.root === root)?.text?.nodes.size) continue;
|
|
198
218
|
const painted = [];
|
|
199
219
|
for (const piece of sequence) {const held = piece.raster ? await raster(piece.raster) : piece; if (held) painted.push(held);}
|
|
200
220
|
const first = Math.max(0, Math.floor((box.left - origin.left + .01) / width)), last = Math.min(count - 1, Math.floor((box.right - origin.left - .01) / width));
|
|
201
|
-
for (let page = first; page <= last; page++) pages[page].push({sequence: painted, x: -page * width, clip: {x: box.left - origin.left, y: box.top - origin.top, width: box.width, height: box.height}});
|
|
221
|
+
for (let page = first; page <= last; page++) pages[page].push({sequence: painted, x: -page * width, clip: {x: box.left - origin.left, y: box.top - origin.top, width: box.width, height: box.height}, ...(under && {under: true})});
|
|
202
222
|
hidden.push({node: original, style: original.getAttribute('style')}); original.style.setProperty('visibility', 'hidden', 'important');
|
|
203
223
|
}
|
|
204
224
|
return {pages, textRoots, restore};
|
|
@@ -1243,7 +1243,7 @@ function* _rapierProjectPortableNodesSteps(semanticRoot, options) {
|
|
|
1243
1243
|
? (element.style.backgroundColor || RAPIER_PORTABLE_HIGHLIGHT_HEX[String(element.getAttribute('data-rapier-highlight') || '').toLowerCase()] || RAPIER_PORTABLE_HIGHLIGHT_HEX.yellow)
|
|
1244
1244
|
: null;
|
|
1245
1245
|
|
|
1246
|
-
const isTableCaption = element.tagName === 'P' && element.classList.contains('rapier-table-caption');
|
|
1246
|
+
const isTableCaption = element.tagName === 'P' && (element.classList.contains('rapier-table-caption') || element.classList.contains('rapier-figure-caption'));
|
|
1247
1247
|
|
|
1248
1248
|
const isPageBreak = element.hasAttribute('data-md-break');
|
|
1249
1249
|
for (const attribute of Array.from(element.attributes)) { yield;
|
|
@@ -1271,6 +1271,13 @@ function* _rapierProjectPortableNodesSteps(semanticRoot, options) {
|
|
|
1271
1271
|
if (paragraphLayout.before != null) element.style.marginTop = paragraphLayout.before + 'pt';
|
|
1272
1272
|
if (paragraphLayout.after != null) element.style.marginBottom = paragraphLayout.after + 'pt';
|
|
1273
1273
|
}
|
|
1274
|
+
if (!keepLayoutData && paragraphLayout && /^(?:P|H[1-6])$/.test(element.tagName) && !element.closest('table,pre') && (paragraphLayout.first != null || paragraphLayout.indent != null)) {
|
|
1275
|
+
const indents = {...(paragraphLayout.first != null && {first: paragraphLayout.first}), ...(paragraphLayout.indent != null && {indent: paragraphLayout.indent})};
|
|
1276
|
+
for (const declaration of globalThis.RapierMarkdownLayout.textStyle(indents, {portable: true}).split(';').filter(Boolean)) {
|
|
1277
|
+
const at = declaration.indexOf(':');
|
|
1278
|
+
element.style.setProperty(declaration.slice(0, at), declaration.slice(at + 1));
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1274
1281
|
if (!keepLayoutData && element.tagName === 'P' && !element.closest('table,pre')) {
|
|
1275
1282
|
if (paragraphLayout?.hyphens === 'auto') { element.style.hyphens = 'auto'; element.style.textWrap = 'pretty'; }
|
|
1276
1283
|
if (paragraphLayout?.hanging === 'on') element.style.setProperty('hanging-punctuation', 'first allow-end');
|
|
@@ -3,7 +3,7 @@ import {collectPdfVectors} from './pdf-vectors.mjs';
|
|
|
3
3
|
import {collectPdfGlyphs} from './pdf-glyphs.mjs';
|
|
4
4
|
import {pdfFontSources, waitForFonts} from './export-fonts.mjs';
|
|
5
5
|
import {apply as applyTypography} from '../layout/typography.mjs';
|
|
6
|
-
import {documentLanguage} from '../spec/document-settings.mjs';
|
|
6
|
+
import {documentLanguage, pageSizeOf} 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';
|
|
@@ -196,14 +196,31 @@ function wrapMasks(main) {
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
const REFUSALS = {
|
|
200
|
+
visual_too_large: 'This document is too large for a PDF. Use Word or HTML, or split it.',
|
|
201
|
+
visual_resources_unavailable: 'A picture or font could not be read for the PDF.',
|
|
202
|
+
visual_capture_expired: 'The PDF took too long to draw. Try again, or split the document.',
|
|
203
|
+
pdf_render_timeout: 'The PDF took too long to draw. Try again, or split the document.',
|
|
204
|
+
pdf_render_unavailable: 'This browser could not draw the PDF.',
|
|
205
|
+
pdf_page_geometry_invalid: 'The page size is not one a PDF can use.',
|
|
206
|
+
pdf_text_glyph_limit: 'The PDF cannot hold this many different letters. Use Word or HTML.',
|
|
207
|
+
pdf_template_border_unavailable: 'The page border could not be drawn.',
|
|
208
|
+
document_changed: 'The document changed while the PDF was drawn. Try again.',
|
|
209
|
+
export_too_large: 'The file is larger than the limit. Split the document.',
|
|
210
|
+
};
|
|
211
|
+
|
|
199
212
|
async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = false, filename, title, settings, templatePage = null}, {capture, signal, current, maxBytes = 8 * 1024 * 1024, film, ink} = {}) {
|
|
200
213
|
const refuse = code => { throw Object.assign(new Error(code), {code}); };
|
|
214
|
+
const spoken = error => {
|
|
215
|
+
const code = error?.code || error?.message, words = REFUSALS[code];
|
|
216
|
+
return words && error.message !== words ? Object.assign(new Error(words), {code}) : error;
|
|
217
|
+
};
|
|
201
218
|
const check = () => {
|
|
202
219
|
if (signal?.aborted) refuse('cancelled');
|
|
203
220
|
if (current?.() === false) refuse('document_changed');
|
|
204
221
|
};
|
|
205
222
|
if (!root?.ownerDocument || typeof capture !== 'function') refuse('pdf_render_unavailable');
|
|
206
|
-
const paper = settings?.papersize || {width: 11906, height: 16838};
|
|
223
|
+
const paper = settings?.papersize || (templatePage ? pageSizeOf(settings) : {width: 11906, height: 16838});
|
|
207
224
|
const width = paper.width / 20, height = paper.height / 20;
|
|
208
225
|
const margins = Object.fromEntries(['top', 'right', 'bottom', 'left'].map(side =>
|
|
209
226
|
[side, settings?.geometry ? settings.geometry[side] / 20 : (side === 'top' ? 12 : 14) * 72 / 25.4]));
|
|
@@ -301,7 +318,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
301
318
|
nativeGlyphs = await collectPdfGlyphs(main, glyphOptions);
|
|
302
319
|
const textPages = await _rapierPrintTextPages(main, bodyWidth, count, check, undefined, nativeGlyphs.nodes, nativeGlyphs.markers);
|
|
303
320
|
const linkPages = _rapierPrintLinkPages(main, bodyWidth, bodyHeight, count);
|
|
304
|
-
vectors = await collectPdfVectors(main, {width: bodyWidth, height: bodyHeight, count, capture, signal, current, check, maxBytes,
|
|
321
|
+
vectors = await collectPdfVectors(main, {width: bodyWidth, height: bodyHeight, count, capture, signal, current, check, maxBytes, underlay: true,
|
|
305
322
|
sanitizeSvg: globalThis.RapierImageAssets?.sanitizeSvgText,
|
|
306
323
|
collectText: root => collectPdfGlyphs(root, {...glyphOptions, origin: main.getBoundingClientRect()})});
|
|
307
324
|
for (const {root, original, text} of vectors.textRoots) {
|
|
@@ -322,6 +339,8 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
322
339
|
strip.style.width = count * bodyWidth + 'px';
|
|
323
340
|
strip.style.transform = 'none';
|
|
324
341
|
if (typeof film !== 'function') refuse('pdf_render_unavailable');
|
|
342
|
+
const underlay = vectors.pages.some(items => items.some(drawing => drawing.under));
|
|
343
|
+
if (underlay) surface.style.setProperty('background', 'transparent');
|
|
325
344
|
let pendingFilm = null, abandonFilm = false;
|
|
326
345
|
const trackedFilm = Promise.resolve(film({root: surface, signal, current, frame: {width: count * bodyWidth, height: bodyHeight}})).then(session => {
|
|
327
346
|
if (abandonFilm) session?.close?.();
|
|
@@ -340,22 +359,23 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
340
359
|
let share = Math.max(1, Math.min(Math.floor(CAPTURE_EDGE / bodyWidth), Math.floor(CAPTURE_PIXELS / (bodyWidth * bodyHeight))));
|
|
341
360
|
const pages = [];
|
|
342
361
|
let imageBytes = 0;
|
|
343
|
-
const
|
|
344
|
-
const packed = new Uint8Array(await step(new Response(new Blob([
|
|
362
|
+
const deflate = async bytes => {
|
|
363
|
+
const packed = new Uint8Array(await step(new Response(new Blob([bytes]).stream().pipeThrough(new CompressionStream('deflate'))).arrayBuffer()));
|
|
345
364
|
imageBytes += packed.byteLength;
|
|
346
365
|
if (imageBytes > maxBytes) refuse('export_too_large');
|
|
347
|
-
|
|
366
|
+
return packed;
|
|
348
367
|
};
|
|
368
|
+
const pack = async (rgb, alpha = null) => pages.push({rgb: await deflate(rgb), ...(alpha && {alpha: await deflate(alpha)})});
|
|
349
369
|
const tileWidth = Math.min(bodyWidth, CAPTURE_EDGE), tileHeight = Math.min(bodyHeight, CAPTURE_EDGE, Math.floor(CAPTURE_PIXELS / tileWidth));
|
|
350
370
|
for (let index = 0; tiled && index < count; index++) {
|
|
351
371
|
const tiles = [];
|
|
352
372
|
for (let y = 0; y < bodyHeight; y += tileHeight) for (let x = 0; x < bodyWidth; x += tileWidth) {
|
|
353
373
|
check();
|
|
354
374
|
const width = Math.min(tileWidth, bodyWidth - x), height = Math.min(tileHeight, bodyHeight - y);
|
|
355
|
-
const shot = await step(filmSession.raster({x: index * bodyWidth + x, y, width, height}));
|
|
356
|
-
if (shot?.width !== width || shot?.height !== height || !(shot.rgb instanceof Uint8Array)) refuse('pdf_render_unavailable');
|
|
357
|
-
await pack(shot.rgb);
|
|
358
|
-
shot.rgb = null;
|
|
375
|
+
const shot = await step(filmSession.raster({x: index * bodyWidth + x, y, width, height}, {transparent: underlay}));
|
|
376
|
+
if (shot?.width !== width || shot?.height !== height || !(shot.rgb instanceof Uint8Array) || underlay && !(shot.alpha instanceof Uint8Array)) refuse('pdf_render_unavailable');
|
|
377
|
+
await pack(shot.rgb, shot.alpha);
|
|
378
|
+
shot.rgb = shot.alpha = null;
|
|
359
379
|
tiles.push({...pages.pop(), x, y, width, height});
|
|
360
380
|
}
|
|
361
381
|
pages.push({tiles});
|
|
@@ -364,21 +384,22 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
364
384
|
check();
|
|
365
385
|
const span = Math.min(share, count - first);
|
|
366
386
|
try {
|
|
367
|
-
const shot = await step(filmSession.raster({x: first * bodyWidth, y: 0, width: span * bodyWidth, height: bodyHeight}));
|
|
368
|
-
if (shot?.width !== span * bodyWidth || shot?.height !== bodyHeight || !(shot.rgb instanceof Uint8Array)) refuse('pdf_render_unavailable');
|
|
369
|
-
if (span === 1) await pack(shot.rgb);
|
|
387
|
+
const shot = await step(filmSession.raster({x: first * bodyWidth, y: 0, width: span * bodyWidth, height: bodyHeight}, {transparent: underlay}));
|
|
388
|
+
if (shot?.width !== span * bodyWidth || shot?.height !== bodyHeight || !(shot.rgb instanceof Uint8Array) || underlay && !(shot.alpha instanceof Uint8Array)) refuse('pdf_render_unavailable');
|
|
389
|
+
if (span === 1) await pack(shot.rgb, shot.alpha);
|
|
370
390
|
else {
|
|
371
391
|
const row = bodyWidth * 3;
|
|
372
392
|
for (let offset = 0; offset < span; offset++) {
|
|
373
|
-
const rgb = new Uint8Array(bodyWidth * bodyHeight * 3);
|
|
393
|
+
const rgb = new Uint8Array(bodyWidth * bodyHeight * 3), alpha = shot.alpha ? new Uint8Array(bodyWidth * bodyHeight) : null;
|
|
374
394
|
for (let y = 0; y < bodyHeight; y++) {
|
|
375
395
|
const from = (y * shot.width + offset * bodyWidth) * 3;
|
|
376
396
|
rgb.set(shot.rgb.subarray(from, from + row), y * row);
|
|
397
|
+
if (alpha) alpha.set(shot.alpha.subarray(y * shot.width + offset * bodyWidth, y * shot.width + (offset + 1) * bodyWidth), y * bodyWidth);
|
|
377
398
|
}
|
|
378
|
-
await pack(rgb);
|
|
399
|
+
await pack(rgb, alpha);
|
|
379
400
|
}
|
|
380
401
|
}
|
|
381
|
-
shot.rgb = null;
|
|
402
|
+
shot.rgb = shot.alpha = null;
|
|
382
403
|
} catch (error) {
|
|
383
404
|
if (error?.code === 'visual_too_large' && span > 1) { share = Math.ceil(span / 2); continue; }
|
|
384
405
|
throw error;
|
|
@@ -394,14 +415,15 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
394
415
|
holder.append(picture); into.body.append(holder);
|
|
395
416
|
await step(picture.decode());
|
|
396
417
|
backdrop = await collectPdfVectors(holder, {width: sheetWidth, height: sheetHeight, count: 1, capture, signal, current, check, maxBytes,
|
|
397
|
-
sanitizeSvg: globalThis.RapierImageAssets?.sanitizeSvgText});
|
|
418
|
+
sanitizeSvg: globalThis.RapierImageAssets?.sanitizeSvgText, pictures: true});
|
|
398
419
|
if (!backdrop.pages[0].length) refuse('pdf_template_border_unavailable');
|
|
399
420
|
}
|
|
400
421
|
const bytes = await writeRasterPdf({pages, textPages, linkPages, vectorPages: vectors.pages, canonical, bom, filename, title, font, width, height, margins, backdrop: backdrop?.pages[0] || null,
|
|
401
422
|
pixelWidth: bodyWidth, pixelHeight: bodyHeight, pageNumbers: settings?.pagestyle === 'plain', maxBytes, check});
|
|
402
423
|
check();
|
|
403
424
|
return {bytes, pages: count, painted: nativeGlyphs.painted};
|
|
404
|
-
}
|
|
425
|
+
} catch (error) { throw spoken(error); }
|
|
426
|
+
finally { filmSession?.close(); backdrop?.restore(); vectors?.restore(); nativeGlyphs?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
|
|
405
427
|
}
|
|
406
428
|
|
|
407
429
|
async function _rapierPrintTextPages(main, pageWidth, count, check, origin = main.getBoundingClientRect(), nativeNodes = null, markerNodes = null) {
|
|
@@ -471,14 +493,14 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
|
|
|
471
493
|
parts = joined;
|
|
472
494
|
}
|
|
473
495
|
const last = pages[page].at(-1);
|
|
474
|
-
if (last && !formula && !last.math && last.block === block && last.carrier === carrier &&
|
|
496
|
+
if (last && !formula && !last.math && !last.marker && last.block === block && last.carrier === carrier && Math.abs(last.y - y) < Math.max(2, Math.min(last.height, bottom - y) * .2)) {
|
|
475
497
|
if (!preserve && last.text.endsWith(' ') && text.startsWith(' ') && parts.length) {
|
|
476
498
|
const first = parts[0];
|
|
477
499
|
if (first.text === ' ') parts.shift();
|
|
478
500
|
else {const advance = first.width / Array.from(first.text).length; first.text = first.text.slice(1); first.x += advance; first.width -= advance;}
|
|
479
501
|
}
|
|
480
502
|
last.text = preserve ? last.text + text : (last.text + text).replace(/[ \t\r\n\f]+/g, ' ');
|
|
481
|
-
last.parts.push(...parts);
|
|
503
|
+
last.parts.push(...parts);
|
|
482
504
|
last.width = Math.max(last.x + last.width, right) - Math.min(last.x, x);
|
|
483
505
|
last.height = Math.max(last.y + last.height, bottom) - Math.min(last.y, y);
|
|
484
506
|
last.x = Math.min(last.x, x); last.y = Math.min(last.y, y);
|
|
@@ -512,13 +534,29 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
|
|
|
512
534
|
}
|
|
513
535
|
const ROLES = {TABLE: 'Table', TR: 'TR', TD: 'TD', TH: 'TH', UL: 'L', OL: 'L', LI: 'LI', BLOCKQUOTE: 'BlockQuote', P: 'P', PRE: 'Code', FIGCAPTION: 'Caption',
|
|
514
536
|
H1: 'H1', H2: 'H2', H3: 'H3', H4: 'H4', H5: 'H5', H6: 'H6'};
|
|
515
|
-
const
|
|
537
|
+
const bodies = new WeakMap(), labels = new WeakMap();
|
|
538
|
+
const tags = (block, marker) => {
|
|
516
539
|
const chain = [];
|
|
517
540
|
for (let node = block; node && node !== main; node = node.parentElement)
|
|
518
541
|
if (ROLES[node.tagName] || node === block && node.matches('.rapier-will-carrier')) chain.unshift({node, role: ROLES[node.tagName] || 'P'});
|
|
519
|
-
|
|
542
|
+
const out = [];
|
|
543
|
+
chain.forEach((entry, at) => {
|
|
544
|
+
const note = entry.node.localName === 'li' && entry.node.classList.contains('footnote-item');
|
|
545
|
+
out.push(note ? {...entry, role: 'Note'} : entry);
|
|
546
|
+
if (entry.node.localName !== 'li') return;
|
|
547
|
+
const last = !chain.slice(at + 1).some(inner => inner.node.localName === 'li');
|
|
548
|
+
if (marker && last) {
|
|
549
|
+
if (!labels.has(entry.node)) labels.set(entry.node, {});
|
|
550
|
+
out.push({node: labels.get(entry.node), role: 'Lbl'});
|
|
551
|
+
}
|
|
552
|
+
else if (!note) {
|
|
553
|
+
if (!bodies.has(entry.node)) bodies.set(entry.node, {});
|
|
554
|
+
out.push({node: bodies.get(entry.node), role: 'LBody'});
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
return out;
|
|
520
558
|
};
|
|
521
|
-
return pages.map(page => page.map(({block, ...run}) => ({...run, tags: tags(block)})));
|
|
559
|
+
return pages.map(page => page.map(({block, ...run}) => ({...run, tags: tags(block, run.marker)})));
|
|
522
560
|
}
|
|
523
561
|
|
|
524
562
|
function _rapierPrintLinkPages(main, width, height, count) {
|
|
@@ -595,7 +633,7 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
595
633
|
const hex = value => Array.from({length: value.length}, (_, index) => value.charCodeAt(index).toString(16).padStart(4, '0')).join('');
|
|
596
634
|
const literal = value => '(' + String(value).replace(/[\\()\r\n]/g, character => ({'\\': '\\\\', '(': '\\(', ')': '\\)', '\r': '\\r', '\n': '\\n'}[character])) + ')';
|
|
597
635
|
const source = encoder.encode((bom ? '\uFEFF' : '') + canonical);
|
|
598
|
-
const embedded = stream('/Type /EmbeddedFile /Subtype /text#
|
|
636
|
+
const embedded = stream('/Type /EmbeddedFile /Subtype /text#2Fmarkdown /Params <</Size ' + source.length + '>> /Filter /FlateDecode', await compressed(source));
|
|
599
637
|
const file = add('<< /Type /Filespec /F ' + literal(String(filename).replace(/[^\x20-\x7e]/g, '_')) + ' /UF <feff' + hex(filename) + '> /AFRelationship /Source /EF << /F ' + embedded + ' 0 R /UF ' + embedded + ' 0 R >> >>');
|
|
600
638
|
for (const page of textPages) for (const run of page) {
|
|
601
639
|
let previous = null;
|
|
@@ -741,7 +779,9 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
741
779
|
const held = pages[index], flate = !!held?.rgb;
|
|
742
780
|
const tiles = held?.tiles || [{...(flate ? held : {jpeg: held}), x: 0, y: 0, width: pixelWidth, height: pixelHeight}];
|
|
743
781
|
const pictures = tiles.map((tile, at) => '/Im0_' + at + ' ' + stream('/Type /XObject /Subtype /Image /Width ' + tile.width + ' /Height ' + tile.height +
|
|
744
|
-
' /ColorSpace /DeviceRGB /BitsPerComponent 8
|
|
782
|
+
' /ColorSpace /DeviceRGB /BitsPerComponent 8' + (tile.alpha ? ' /SMask ' + stream('/Type /XObject /Subtype /Image /Width ' + tile.width + ' /Height ' + tile.height +
|
|
783
|
+
' /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode', tile.alpha) + ' 0 R' : '') +
|
|
784
|
+
' /Filter /' + (tile.rgb ? 'FlateDecode' : 'DCTDecode'), tile.rgb || tile.jpeg) + ' 0 R'), states = [];
|
|
745
785
|
pages[index] = null;
|
|
746
786
|
const emit = (drawing, left, top, clipWidth, clipHeight) => {
|
|
747
787
|
let out = `q\n.75 0 0 -.75 ${decimal(left)} ${decimal(top)} cm\n0 0 ${clipWidth} ${clipHeight} re W n\n1 0 0 1 ${decimal(drawing.x)} 0 cm\n`;
|
|
@@ -770,9 +810,10 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
770
810
|
};
|
|
771
811
|
let commands = '';
|
|
772
812
|
for (const drawing of backdrop || []) commands += emit(drawing, 0, height, Math.round(width * 4 / 3), Math.round(height * 4 / 3));
|
|
813
|
+
for (const drawing of vectorPages[index] || []) if (drawing.under) commands += emit(drawing, margins.left, height - margins.top, pixelWidth, pixelHeight);
|
|
773
814
|
commands += '/Artifact BMC\n' + tiles.map((tile, at) => 'q\n' + [decimal(tile.width * .75), 0, 0, decimal(tile.height * .75), decimal(margins.left + tile.x * .75),
|
|
774
815
|
decimal(height - margins.top - (tile.y + tile.height) * .75)].join(' ') + ' cm\n/Im0_' + at + ' Do\nQ\n').join('') + 'EMC\n';
|
|
775
|
-
for (const drawing of vectorPages[index] || []) commands += emit(drawing, margins.left, height - margins.top, pixelWidth, pixelHeight);
|
|
816
|
+
for (const drawing of vectorPages[index] || []) if (!drawing.under) commands += emit(drawing, margins.left, height - margins.top, pixelWidth, pixelHeight);
|
|
776
817
|
const textAlphas = new Map();
|
|
777
818
|
for (const run of textPages[index]) {
|
|
778
819
|
if (!run.text && !run.parts?.some(part => part.font)) continue;
|
package/dist/kit/render.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import {finish, finishAsync, cloneTree, cleanTree, serializeTree, encodeUtf8Step
|
|
|
4
4
|
import {fontFamilyCss} from '../spec/md-marks.mjs';
|
|
5
5
|
import {readFontAssets, documentFontsCss} from '../spec/document-fonts.mjs';
|
|
6
6
|
import {fontRequests, requireDocumentFonts} from './export-fonts.mjs';
|
|
7
|
-
import {documentLanguage, documentSettingsFonts} from '../spec/document-settings.mjs';
|
|
7
|
+
import {documentLanguage, documentSettingsFonts, pageSizeOf} from '../spec/document-settings.mjs';
|
|
8
8
|
import {createLineMeasurer, linesHeight} from '../layout/model.mjs';
|
|
9
9
|
import {markPrintCapitals, createPrintCapitals, followingPrintProse} from './print-capitals.mjs';
|
|
10
10
|
import {freezePublishedInk} from './render-ink.mjs';
|
|
@@ -205,23 +205,26 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
function _rapierTemplatePage(canonical, settings, root, darkPaper = false) {
|
|
208
|
-
if (!isTemplatePage(canonical)
|
|
208
|
+
if (!isTemplatePage(canonical)) return null;
|
|
209
|
+
const paper = pageSizeOf(settings);
|
|
209
210
|
let url = '';
|
|
210
211
|
for (const paragraph of root.querySelectorAll(':scope > p[data-md-layout]')) {
|
|
211
212
|
const image = paragraph.querySelector('img');
|
|
212
213
|
const layout = globalThis.RapierMarkdownLayout.parseLayoutAttribute(paragraph.getAttribute('data-md-layout'));
|
|
213
214
|
const source = image?.getAttribute('src') || '';
|
|
214
|
-
if (isPageBorderLayout(layout) && /^data:image\/svg\+xml;base64,[A-Za-z0-9+/=]
|
|
215
|
+
if (isPageBorderLayout(layout) && /^data:image\/(?:png|jpeg|webp|gif|svg\+xml|jxl);base64,[A-Za-z0-9+/=]+$/i.test(source)) { url = source; paragraph.remove(); break; }
|
|
215
216
|
}
|
|
216
217
|
if (!url) return null;
|
|
217
|
-
const width =
|
|
218
|
-
const margins = ['top', 'right', 'bottom', 'left'].map(side => (settings
|
|
218
|
+
const width = paper.width / 15, height = paper.height / 15;
|
|
219
|
+
const margins = ['top', 'right', 'bottom', 'left'].map(side => (settings?.geometry?.[side] ?? 1440) / 15);
|
|
219
220
|
const padding = margins.map(value => value + 'px').join(' ');
|
|
220
221
|
const ground = 'background:var(--rapier-template-border) 0 0/100% 100% no-repeat';
|
|
221
|
-
const shown = darkPaper ? _rapierDarkPaperSvgUrl(url, '#fafafa') : url;
|
|
222
|
+
const shown = darkPaper && url.startsWith('data:image/svg+xml;base64,') ? _rapierDarkPaperSvgUrl(url, '#fafafa') : url;
|
|
222
223
|
const css = ':root{--rapier-template-border:url("' + url + '")}\n'
|
|
223
224
|
+ '@media screen{' + (shown !== url ? ':root{--rapier-template-border:url("' + shown + '")}' : '') + '.rapier-template-border{display:none}body{background:var(--md-color-inset)}.rapier-page{box-sizing:border-box;width:min(100%,' + width + 'px);min-height:' + height + 'px;margin:0 auto;padding:' + padding
|
|
224
225
|
+ ';background:var(--rapier-template-border) 0 0/100% ' + height + 'px repeat-y,var(--md-color-bg);color:var(--md-color-text)}}\n'
|
|
226
|
+
+ '@media screen and (max-width:' + width + 'px){.rapier-page{min-height:auto;aspect-ratio:' + width + '/' + height + ';padding:'
|
|
227
|
+
+ margins.map(value => 'min(' + value + 'px,' + Number((value / width * 100).toFixed(4)) + '%)').join(' ') + ';background-size:100% auto,auto}}\n'
|
|
225
228
|
+ '@media print{@page{size:' + width + 'px ' + height + 'px;margin:0}html,body{margin:0;-webkit-print-color-adjust:exact;print-color-adjust:exact}'
|
|
226
229
|
+ '.rapier-page{box-sizing:border-box;width:auto;max-width:none;margin:0;padding:' + padding + '!important;position:relative!important;z-index:1;box-decoration-break:clone;-webkit-box-decoration-break:clone}'
|
|
227
230
|
+ '.rapier-template-border{display:block;position:fixed;left:0;top:0;width:' + width + 'px;height:' + height + 'px;z-index:0;' + ground + '}}';
|
|
@@ -256,11 +259,7 @@ async function _rapierBuildPublishedPage(context) {
|
|
|
256
259
|
throw new TypeError('Publishing requires a captured document.');
|
|
257
260
|
const current = {...context, ledger: null, carried: 'none', comparisonBase: null};
|
|
258
261
|
return _rapierBuildArtifact({
|
|
259
|
-
kind: 'standalone', scripts: false,
|
|
260
|
-
async afterRoot(root) {
|
|
261
|
-
if (current.work) await finishAsync(_rapierSharedPageLayoutSteps(root), current.work);
|
|
262
|
-
else _rapierSharedPageLayout(root);
|
|
263
|
-
},
|
|
262
|
+
kind: 'standalone', scripts: false,
|
|
264
263
|
afterFonts: (root, style) => freezePublishedInk(root, {...style, document, work: current.work,
|
|
265
264
|
spec: globalThis.RapierMarkdownSpec, ink: globalThis.RapierInk, draw: globalThis.RapierInkDraw}),
|
|
266
265
|
}, current);
|
|
@@ -729,6 +728,14 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
729
728
|
}
|
|
730
729
|
image.style.margin = '0';
|
|
731
730
|
const naturalWidth = Number(image.getAttribute('width')) || 0, naturalHeight = Number(image.getAttribute('height')) || 0;
|
|
731
|
+
let turned = '';
|
|
732
|
+
const turnedCorners = (bounds, width, height, rad) => {
|
|
733
|
+
const cos = Math.cos(rad), sin = Math.sin(rad), round = value => Math.round(value * 100) / 100 + '%';
|
|
734
|
+
return [[-1, -1], [1, -1], [1, 1], [-1, 1]].map(([x, y]) => {
|
|
735
|
+
const dx = x * width / 2, dy = y * height / 2;
|
|
736
|
+
return round(50 + (dx * cos - dy * sin) / bounds.width * 100) + ' ' + round(50 + (dx * sin + dy * cos) / bounds.height * 100);
|
|
737
|
+
}).join(', ');
|
|
738
|
+
};
|
|
732
739
|
if (layout.rotate && naturalWidth > 0 && naturalHeight > 0) {
|
|
733
740
|
const rad = layout.rotate * Math.PI / 180, aspect = naturalHeight / naturalWidth;
|
|
734
741
|
image.style.maxWidth = 'none';
|
|
@@ -741,6 +748,7 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
741
748
|
height *= scale;
|
|
742
749
|
bounds = globalThis.RapierImageLayout.rotatedBoundsRad(width, height, rad);
|
|
743
750
|
}
|
|
751
|
+
turned = turnedCorners(bounds, width, height, rad);
|
|
744
752
|
paragraph.style.width = bounds.width + '%';
|
|
745
753
|
paragraph.style.paddingTop = ((bounds.height - height) / 2) + '%';
|
|
746
754
|
paragraph.style.paddingBottom = ((bounds.height - height) / 2) + '%';
|
|
@@ -748,6 +756,7 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
748
756
|
image.style.marginLeft = image.style.marginRight = (((bounds.width - width) / 2) / bounds.width * 100) + '%';
|
|
749
757
|
} else {
|
|
750
758
|
const bounds = globalThis.RapierImageLayout.rotatedBoundsRad(naturalWidth, naturalHeight, rad);
|
|
759
|
+
turned = turnedCorners(bounds, naturalWidth, naturalHeight, rad);
|
|
751
760
|
paragraph.style.width = bounds.width + 'px';
|
|
752
761
|
paragraph.style.paddingTop = ((bounds.height - naturalHeight) / 2) + 'px';
|
|
753
762
|
paragraph.style.paddingBottom = ((bounds.height - naturalHeight) / 2) + 'px';
|
|
@@ -763,6 +772,7 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
763
772
|
if (layout.width != null || image.getAttribute('width')) image.style.width = '100%';
|
|
764
773
|
}
|
|
765
774
|
if (layout.rotate) { image.style.transform = 'rotate(' + layout.rotate + 'deg)'; image.style.transformOrigin = '50% 50%'; }
|
|
775
|
+
if (turned) paragraph.style.setProperty('shape-outside', 'polygon(' + turned + ') padding-box');
|
|
766
776
|
if (layout.wrap === 'around' && !layout.rotate && !layout.y && layout.width == null && !image.dataset.rapierContour?.endsWith('-box')) {
|
|
767
777
|
const src = image.getAttribute('src');
|
|
768
778
|
if (src) {
|
|
@@ -779,15 +789,16 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
779
789
|
const layout = globalThis.RapierMarkdownLayout.parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
|
|
780
790
|
if (layout?.wrap !== 'behind' && layout?.wrap !== 'front') continue;
|
|
781
791
|
const paragraph = _rapierSharePictureParagraph(image);
|
|
782
|
-
if (paragraph) positionedAssignments.push({image, paragraph, layout});
|
|
792
|
+
if (paragraph) positionedAssignments.push({image, paragraph, layout, owner: globalThis.RapierMarkdownLayout.wrapNeighbour(paragraph, _rapierShareWrapKind)});
|
|
783
793
|
}
|
|
784
|
-
for (const {image, paragraph, layout} of positionedAssignments) {
|
|
794
|
+
for (const {image, paragraph, layout, owner} of positionedAssignments) {
|
|
785
795
|
yield;
|
|
786
796
|
paragraph.style.cssText = 'position:relative;height:0;margin:0;padding:0;line-height:0';
|
|
787
797
|
const x = layout.x, naturalWidth = Number(image.getAttribute('width')) || 0;
|
|
788
|
-
if (layout.
|
|
798
|
+
if (layout.lines != null) { image.style.width = 'auto'; image.style.height = globalThis.RapierMarkdownLayout.linesHeightCss(layout.lines); }
|
|
799
|
+
else if (layout.width != null) image.style.width = layout.width + '%';
|
|
789
800
|
else if (naturalWidth > 0) image.style.width = naturalWidth + 'px';
|
|
790
|
-
image.style.left = x == null ? '0' : layout.width != null ? (x - layout.width / 2) + '%'
|
|
801
|
+
image.style.left = x == null ? '0' : layout.width != null ? Math.max(0, Math.min(100 - layout.width, x - layout.width / 2)) + '%'
|
|
791
802
|
: naturalWidth > 0 ? 'calc(' + x + '% - ' + (naturalWidth / 2) + 'px)' : x + '%';
|
|
792
803
|
image.style.position = 'absolute';
|
|
793
804
|
image.style.top = (layout.y || 0) + 'em';
|
|
@@ -795,6 +806,7 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
795
806
|
if (layout.rotate) { image.style.transform = 'rotate(' + layout.rotate + 'deg)'; image.style.transformOrigin = '50% 50%'; }
|
|
796
807
|
if (layout.wrap === 'behind') image.style.zIndex = '-1';
|
|
797
808
|
paragraph.classList.add('rapier-positioned-picture');
|
|
809
|
+
if (owner && owner !== paragraph.nextElementSibling) owner.before(paragraph);
|
|
798
810
|
}
|
|
799
811
|
const positioned = new Set([...assignments, ...positionedAssignments].map(row => row.image));
|
|
800
812
|
for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
|
|
@@ -807,11 +819,13 @@ function* _rapierSharedPageLayoutSteps(root) {
|
|
|
807
819
|
const naturalWidth = Number(image.getAttribute('width')) || 0, naturalHeight = Number(image.getAttribute('height')) || 0;
|
|
808
820
|
if (naturalWidth > 0 && naturalHeight > 0) {
|
|
809
821
|
const rad = layout.rotate * Math.PI / 180;
|
|
810
|
-
const
|
|
811
|
-
const
|
|
822
|
+
const width = layout.width != null && !layout.lines ? layout.width : naturalWidth, height = layout.width != null && !layout.lines ? layout.width * naturalHeight / naturalWidth : naturalHeight;
|
|
823
|
+
const unit = layout.width != null && !layout.lines ? '%' : 'px';
|
|
824
|
+
const boundWidth = width * Math.abs(Math.cos(rad)) + height * Math.abs(Math.sin(rad));
|
|
825
|
+
const boundHeight = width * Math.abs(Math.sin(rad)) + height * Math.abs(Math.cos(rad));
|
|
812
826
|
image.style.display = 'block';
|
|
813
|
-
image.style.marginLeft = image.style.marginRight = ((boundWidth -
|
|
814
|
-
image.style.marginTop = image.style.marginBottom = ((boundHeight -
|
|
827
|
+
image.style.marginLeft = image.style.marginRight = ((boundWidth - width) / 2) + unit;
|
|
828
|
+
image.style.marginTop = image.style.marginBottom = ((boundHeight - height) / 2) + unit;
|
|
815
829
|
}
|
|
816
830
|
}
|
|
817
831
|
}
|
|
@@ -1349,6 +1363,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1349
1363
|
balance: 0};
|
|
1350
1364
|
}
|
|
1351
1365
|
|
|
1366
|
+
let inkIds = 0;
|
|
1352
1367
|
function fragmentNode(record, fragment, idsKept) {
|
|
1353
1368
|
const run = record.runs[fragment.itemIndex], mapped = geometry.mapFragment(run, fragment, {whiteSpace: record.whiteSpace});
|
|
1354
1369
|
if (!mapped) return null;
|
|
@@ -1374,6 +1389,8 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
|
|
|
1374
1389
|
if (!plan) return false;
|
|
1375
1390
|
const output = doc.createDocumentFragment();
|
|
1376
1391
|
const idsKept = new Set();
|
|
1392
|
+
for (const run of record.runs) for (const parent of run.parents)
|
|
1393
|
+
if (parent.matches?.('span.rapier-ink-mark[data-rapier-ink]') && !parent.hasAttribute('data-rapier-ink-id')) parent.setAttribute('data-rapier-ink-id', String(++inkIds));
|
|
1377
1394
|
let previous = null;
|
|
1378
1395
|
for (const line of plan.lines) {
|
|
1379
1396
|
const element = doc.createElement('span');
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
import {next, weight, exceptions, maximum} from '../agent/vendor/hyphen-en-us/patterns.js';
|
|
3
3
|
|
|
4
|
-
function englishCuts(word) {
|
|
5
|
-
if (!/^[A-
|
|
4
|
+
export function englishCuts(word) {
|
|
5
|
+
if (!/^[A-Za-z]+$/.test(word)) return null;
|
|
6
|
+
if (word.length < 6) return [];
|
|
7
|
+
const cuts = [];
|
|
8
|
+
let offset = 0, before = 0;
|
|
9
|
+
for (const part of word.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z]+/g)) {
|
|
10
|
+
if (before >= 3 && part.length >= 3) cuts.push(offset);
|
|
11
|
+
for (const at of partCuts(part)) cuts.push(offset+at);
|
|
12
|
+
offset += part.length; before = part.length;
|
|
13
|
+
}
|
|
14
|
+
return cuts.filter(at => at >= 3 && at <= word.length-3).sort((a, b) => a-b).filter((at, index, all) => at !== all[index-1]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function partCuts(word) {
|
|
6
18
|
if (word.length < 6) return [];
|
|
7
19
|
const lower = word.toLowerCase(), known = exceptions[lower];
|
|
8
|
-
if (Array.isArray(known)) return known
|
|
20
|
+
if (Array.isArray(known)) return known;
|
|
9
21
|
const source = '.'+lower+'.', weights = new Uint8Array(source.length+1);
|
|
10
22
|
for (let start = 0; start < source.length; start++) {
|
|
11
23
|
let node = 0;
|
package/dist/layout/model.mjs
CHANGED
|
@@ -12,7 +12,8 @@ export function mapFragment(run, fragment, {whiteSpace = 'normal'} = {}) {
|
|
|
12
12
|
const raw = typeof run === 'string' ? run : run?.raw;
|
|
13
13
|
if (run?.break === 'never') whiteSpace = 'normal';
|
|
14
14
|
const start = fragment?.sourceStart, end = fragment?.sourceEnd;
|
|
15
|
-
|
|
15
|
+
const derived = fragment?.derive === true;
|
|
16
|
+
if (typeof raw !== 'string' || !derived && typeof fragment?.text !== 'string' ||
|
|
16
17
|
!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > raw.length ||
|
|
17
18
|
whiteSpace !== 'normal' && whiteSpace !== 'pre-wrap') return null;
|
|
18
19
|
const pieces = [], offsets = [start];
|
|
@@ -33,6 +34,13 @@ export function mapFragment(run, fragment, {whiteSpace = 'normal'} = {}) {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
let text = pieces.join('');
|
|
37
|
+
if (derived) {
|
|
38
|
+
if (fragment.glyph) {
|
|
39
|
+
if (lastSoftHyphen === end - 1) offsets[offsets.length - 1] = lastSoftHyphen;
|
|
40
|
+
offsets.push(end); text += fragment.glyph;
|
|
41
|
+
}
|
|
42
|
+
return {text, offsets, start, end};
|
|
43
|
+
}
|
|
36
44
|
if (lastSoftHyphen === end - 1 && fragment.text === text + '-') {
|
|
37
45
|
offsets[offsets.length - 1] = lastSoftHyphen;
|
|
38
46
|
offsets.push(end); text += '-';
|
|
@@ -22,7 +22,7 @@ export function createParagraphFitter() {
|
|
|
22
22
|
return handle;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function* solveSteps(handle, width, {indent = 0, left = 0, right = 0, widths = null} = {}, known) {
|
|
25
|
+
function* solveSteps(handle, width, {indent = 0, left = 0, right = 0, widths = null, shrink = .4, stretch = .8} = {}, known) {
|
|
26
26
|
const prepared = indexes.get(handle);
|
|
27
27
|
if (!prepared || ![width, indent, left, right].every(Number.isFinite) || width <= 0) return null;
|
|
28
28
|
const measures = Array.isArray(widths) && widths.every(value => Number.isFinite(value) && value > 0) ? widths : [];
|
|
@@ -44,7 +44,7 @@ export function createParagraphFitter() {
|
|
|
44
44
|
const closing = Math.min(Math.max(0, right), words.at(-1).closing);
|
|
45
45
|
const target = measure(base) - (segmentIndex === 0 ? indent : 0) + opening + closing;
|
|
46
46
|
const count = words.length - 1;
|
|
47
|
-
const delta = !count ? 0 : Math.max(-smallest *
|
|
47
|
+
const delta = !count ? 0 : Math.max(-smallest * shrink, Math.min(0, (target - natural) / count));
|
|
48
48
|
const overflow = natural + delta * count - target;
|
|
49
49
|
const cost = 1 + 100 * Math.max(0, overflow / target) ** 2;
|
|
50
50
|
if (Number.isFinite(natural) && target > 0 && cost < 2 && minimum[words.length - 1] - minimum[Math.min(words.length - 1, 1)] <= ceiling + .02 &&
|
|
@@ -111,7 +111,7 @@ export function createParagraphFitter() {
|
|
|
111
111
|
const target = measure(base + lineClass) - (segmentIndex === 0 && startIndex === 0 ? indent : 0) + opening + closing;
|
|
112
112
|
if (target <= 0) continue;
|
|
113
113
|
const remaining = target - natural;
|
|
114
|
-
const delta = !count ? 0 : Math.max(-smallest *
|
|
114
|
+
const delta = !count ? 0 : Math.max(-smallest * shrink, Math.min(final ? 0 : smallest * stretch, remaining / count));
|
|
115
115
|
const overflow = natural + delta * count - target;
|
|
116
116
|
if (overflow > .02 && (count || start.cut || end.cut || words[lastWord].breaks.length)) continue;
|
|
117
117
|
const residual = Math.max(0, remaining - delta * count);
|
|
@@ -308,10 +308,27 @@ export function createTypography() {
|
|
|
308
308
|
return {key: keys.join('\u0000'), generation:keys[0], styles, parents, supported};
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
function smallCapsAsPlanned(paragraph, text, inspection) {
|
|
312
|
+
if (paragraph.getAttribute('data-md-caps') !== 'small') return true;
|
|
313
|
+
const doc = paragraph.ownerDocument, style = inspection.styles.get(paragraph);
|
|
314
|
+
if (!style || !doc.body) return false;
|
|
315
|
+
const probe = caps => {
|
|
316
|
+
const span = doc.createElement('span');
|
|
317
|
+
span.style.cssText = 'position:absolute;visibility:hidden;white-space:pre;left:0;top:0;font-synthesis-small-caps:none;font-variant-caps:' + caps;
|
|
318
|
+
for (const name of ['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'fontStretch', 'letterSpacing', 'fontKerning', 'fontFeatureSettings'])
|
|
319
|
+
span.style[name] = style[name];
|
|
320
|
+
span.textContent = text.trim().slice(0, 80);
|
|
321
|
+
doc.body.append(span);
|
|
322
|
+
const width = span.getBoundingClientRect().width;
|
|
323
|
+
span.remove();
|
|
324
|
+
return width;
|
|
325
|
+
};
|
|
326
|
+
return Math.abs(probe('small-caps') - probe('normal')) < .01;
|
|
327
|
+
}
|
|
328
|
+
|
|
311
329
|
function snapshot(paragraph, inspection) {
|
|
312
330
|
const text = paragraph.textContent, language = paragraph.closest('[lang]')?.getAttribute('lang') ?? '';
|
|
313
|
-
if (!inspection.supported || !text.trim() || paragraph.
|
|
314
|
-
paragraph.getAttribute('data-md-caps') === 'small' || paragraph.closest('[data-rapier-published-ink]') ||
|
|
331
|
+
if (!inspection.supported || !text.trim() || !smallCapsAsPlanned(paragraph, text, inspection) || paragraph.closest('[data-rapier-published-ink]') ||
|
|
315
332
|
/[^\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}\p{Script=Common}\p{Script=Inherited}]/u.test(text) ||
|
|
316
333
|
/[\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/u.test(text)) return null;
|
|
317
334
|
const hard = [], items = runs(paragraph, hard), atoms = [];
|
|
@@ -425,6 +442,24 @@ export function createTypography() {
|
|
|
425
442
|
shape.box.height, shape.points.join(' ')].join(':')).join('|')};
|
|
426
443
|
}
|
|
427
444
|
|
|
445
|
+
function dropMeasure(paragraph, view, width) {
|
|
446
|
+
if (!paragraph.hasAttribute('data-md-lines')) return null;
|
|
447
|
+
const letter = view.getComputedStyle(paragraph, '::first-letter');
|
|
448
|
+
const lines = Math.ceil(parseFloat(letter.initialLetter)), initial = initialRange(paragraph);
|
|
449
|
+
const advance = ['width', 'marginLeft', 'marginRight', 'paddingLeft', 'paddingRight', 'borderLeftWidth', 'borderRightWidth']
|
|
450
|
+
.reduce((sum, name) => sum + (parseFloat(letter[name]) || 0), 0);
|
|
451
|
+
const at = initial && point(runs(paragraph), initial.start);
|
|
452
|
+
if (!letter.initialLetter) return {refuse: true};
|
|
453
|
+
if (!(lines > 1)) return null;
|
|
454
|
+
if (!(advance > 0 && advance < width) || !at?.node.parentElement) return {refuse: true};
|
|
455
|
+
const style = view.getComputedStyle(at.node.parentElement), context = paragraph.ownerDocument.createElement('canvas').getContext('2d');
|
|
456
|
+
context.font = style.fontStyle + ' ' + style.fontWeight + ' ' + style.fontSize + ' ' + style.fontFamily;
|
|
457
|
+
context.letterSpacing = style.letterSpacing;
|
|
458
|
+
const text = paragraph.textContent, word = text.slice(initial.start, initial.start + (/\s/u.exec(text.slice(initial.start))?.index ?? text.length));
|
|
459
|
+
const lead = context.measureText(word).width - context.measureText(word.slice(initial.text.length)).width;
|
|
460
|
+
return {lines, advance, lead};
|
|
461
|
+
}
|
|
462
|
+
|
|
428
463
|
const topology = plan => plan && plan.lines.map(line => [line.start,line.end,line.next,line.kind,line.glyph].join(':')).join('|');
|
|
429
464
|
function updateProjection(projection, plan) {
|
|
430
465
|
for (const {node,property,line,sign} of projection.bindings) {
|
|
@@ -517,13 +552,6 @@ export function createTypography() {
|
|
|
517
552
|
return Math.abs(inner - job.plan.lines.length * job.beside.lineHeight) < 1;
|
|
518
553
|
}
|
|
519
554
|
|
|
520
|
-
function editKey(paragraph, generation) {
|
|
521
|
-
const page = paragraph.ownerDocument.documentElement;
|
|
522
|
-
return [generation, paragraph.innerHTML, ...[...paragraph.attributes].map(item => item.name + '=' + item.value),
|
|
523
|
-
...[...page.attributes].map(item => item.name + '=' + item.value), paragraph.ownerDocument.body?.className,
|
|
524
|
-
paragraph.ownerDocument.defaultView.innerWidth].join('\u0000');
|
|
525
|
-
}
|
|
526
|
-
|
|
527
555
|
function apply(root, {all=false, editing=false}={}) {
|
|
528
556
|
const started = performance.now();
|
|
529
557
|
const generation = captureFontAuthority(root?.ownerDocument || globalThis.document);
|
|
@@ -533,31 +561,40 @@ export function createTypography() {
|
|
|
533
561
|
}
|
|
534
562
|
const jobs=[];let pending=false,changed=0,released=0,preparedCount=0;
|
|
535
563
|
for (const paragraph of paragraphs(root)) {
|
|
564
|
+
if (paragraph.dataset.rapierFlow==='true') continue;
|
|
536
565
|
if (paragraph.closest('table,pre,code,h1,h2,h3,h4,h5,h6')) {clear(paragraph);continue;}
|
|
537
566
|
if (paragraph.closest('.block-edit')) {
|
|
538
567
|
if (!editing) continue;
|
|
539
|
-
if (paragraph.
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
568
|
+
if (paragraph.hasAttribute(mark)) clear(paragraph);
|
|
569
|
+
protectInitial(paragraph);
|
|
570
|
+
continue;
|
|
543
571
|
}
|
|
544
572
|
else if (paragraph.isContentEditable && !paragraph.closest('.block-read')) {clear(paragraph);continue;}
|
|
545
573
|
if (!paragraph.isConnected || !all && paragraph.checkVisibility && !paragraph.checkVisibility({contentVisibilityAuto:true})) continue;
|
|
546
574
|
const view=paragraph.ownerDocument.defaultView;
|
|
547
575
|
if (!view || !paragraph.getClientRects().length) continue;
|
|
548
|
-
if (editing && states.get(paragraph)?.editKey===editKey(paragraph,generation)) continue;
|
|
549
576
|
const style=view.getComputedStyle(paragraph),box=paragraph.getBoundingClientRect(),prior=states.get(paragraph);
|
|
550
577
|
if (!all && (box.bottom < -100 || box.top > view.innerHeight+100)) continue;
|
|
551
578
|
const hanging=paragraph.getAttribute('data-md-hanging')==='on',size=dimensions(paragraph,style,box,hanging);
|
|
552
579
|
if (!(size.width>0)) continue;
|
|
580
|
+
if (paragraph.getAttribute('data-md-align')!=='justify') {size.shrink=0;size.stretch=0;}
|
|
553
581
|
const beside=floatMeasures(paragraph,style,box,size.width);
|
|
554
582
|
if (beside?.refuse) {clear(paragraph);continue;}
|
|
555
583
|
if (beside?.widths.some(width=>!(width>0))) {clear(paragraph);continue;}
|
|
556
584
|
if (beside) size.widths=beside.widths;
|
|
585
|
+
const drop=dropMeasure(paragraph,view,size.width);
|
|
586
|
+
if (drop?.refuse) {clear(paragraph);continue;}
|
|
587
|
+
if (drop) {
|
|
588
|
+
const widths=[...(size.widths||[])];
|
|
589
|
+
while (widths.length<drop.lines) widths.push(size.width);
|
|
590
|
+
for (let line=0;line<drop.lines;line++) widths[line]=Math.floor((widths[line]-drop.advance+(line?0:drop.lead))*64)/64;
|
|
591
|
+
if (widths.some(value=>!(value>0))) {clear(paragraph);continue;}
|
|
592
|
+
size.widths=widths;
|
|
593
|
+
}
|
|
557
594
|
const parents=prior?.parents || own(paragraph);
|
|
558
595
|
const inspection=inspect(paragraph,parents,prior,style);
|
|
559
596
|
const reusable=prior?.html===paragraph.innerHTML && prior.key===inspection.key && (!prior.geometry || scope(paragraph.ownerDocument).current(prior.geometry));
|
|
560
|
-
const sizeKey=[size.width,size.indent,size.left,size.right,beside?.key||''].join('|');
|
|
597
|
+
const sizeKey=[size.width,size.indent,size.left,size.right,beside?.key||'',drop?drop.lines+'/'+drop.advance+'/'+drop.lead:''].join('|');
|
|
561
598
|
if (refused.get(paragraph)===sizeKey+'\u0000'+paragraph.textContent) {if (paragraph.hasAttribute(mark)) clear(paragraph);continue;}
|
|
562
599
|
if (reusable && !prior.pending && prior.sizeKey===sizeKey) continue;
|
|
563
600
|
const fitWork=reusable && prior.sizeKey===sizeKey?prior.fitWork:null;
|
|
@@ -565,12 +602,12 @@ export function createTypography() {
|
|
|
565
602
|
if (!all && jobs.length===4) {if (!reusable) clear(paragraph);pending=true;continue;}
|
|
566
603
|
jobs.push({paragraph,view,style,inspection,size,sizeKey,hanging,beside,selected:selectionBefore(paragraph),
|
|
567
604
|
cap:paragraph.hasAttribute('data-md-lines'),justified:paragraph.getAttribute('data-md-align')==='justify' && paragraph.getAttribute('data-md-hyphens')==='auto',
|
|
605
|
+
ragged:paragraph.getAttribute('data-md-align')!=='justify' && paragraph.getAttribute('data-md-hyphens')==='auto' && /^(start|left)$/.test(style.textAlign),
|
|
568
606
|
saved:reusable?prior.saved:null,geometry:reusable?prior.geometry:null,prepared:reusable?prior.prepared:null,
|
|
569
607
|
work:reusable?prior.work:null,fitWork,
|
|
570
608
|
prior,reusable,needsClear:paragraph.hasAttribute(mark)});
|
|
571
609
|
}
|
|
572
|
-
|
|
573
|
-
for (const job of jobs) if (job.needsClear && !job.saved) clear(job.paragraph,{keepSelection:!editing});
|
|
610
|
+
for (const job of jobs) if (job.needsClear && !job.saved) clear(job.paragraph);
|
|
574
611
|
function stateFor(job) {
|
|
575
612
|
return {properties:new Map(),moves:[],saved:job.saved,geometry:job.geometry,prepared:job.prepared,plan:job.plan,
|
|
576
613
|
work:job.work,fitWork:job.fitWork,parents:job.inspection.parents,key:job.inspection.key,generation:job.inspection.generation,sizeKey:job.sizeKey};
|
|
@@ -584,12 +621,8 @@ export function createTypography() {
|
|
|
584
621
|
for (let jobIndex = 0; jobIndex < jobs.length; jobIndex++) {
|
|
585
622
|
if (!all && jobIndex && performance.now() - started >= 8) {pending=true;break;}
|
|
586
623
|
const job = jobs[jobIndex];
|
|
587
|
-
if (!job.saved && job.justified) {
|
|
588
|
-
if (job.needsClear)
|
|
589
|
-
const elements=own(job.paragraph),parents=job.inspection.parents;
|
|
590
|
-
if (!editing || !job.prior?.saved || elements.length!==parents.length || elements.some((element,index)=>element!==parents[index]))
|
|
591
|
-
job.inspection=inspect(job.paragraph,elements,null,job.view.getComputedStyle(job.paragraph));
|
|
592
|
-
}
|
|
624
|
+
if (!job.saved && (job.justified || job.ragged)) {
|
|
625
|
+
if (job.needsClear) job.inspection=inspect(job.paragraph,own(job.paragraph),null,job.view.getComputedStyle(job.paragraph));
|
|
593
626
|
job.saved=snapshot(job.paragraph,job.inspection);
|
|
594
627
|
if (job.saved) {
|
|
595
628
|
job.work=scope(job.paragraph.ownerDocument).prepareSteps(job.paragraph,job.saved.inline,{policy:'paragraph',language:job.saved.language,
|
|
@@ -611,15 +644,15 @@ export function createTypography() {
|
|
|
611
644
|
if (job.fitWork) {
|
|
612
645
|
for (;;) {
|
|
613
646
|
const step=job.fitWork.next();
|
|
614
|
-
if (step.done) {job.plan=step.value;job.fitWork=null;break;}
|
|
647
|
+
if (step.done) {job.plan=job.ragged && step.value ? Object.freeze({...step.value,lines:Object.freeze(step.value.lines.map(line=>Object.freeze({...line,wordSpacing:0})))}) : step.value;job.fitWork=null;break;}
|
|
615
648
|
if (!all && performance.now()-started>=8) break;
|
|
616
649
|
}
|
|
617
650
|
if (job.fitWork) {suspend(job);continue;}
|
|
618
651
|
}
|
|
619
652
|
job.projection=job.reusable && job.plan && job.prior?.projection?.topology===topology(job.plan) ? job.prior.projection : null;
|
|
620
|
-
if (job.needsClear && job.saved && !job.projection) clear(job.paragraph,{forget:false
|
|
621
|
-
const {paragraph,view,hanging,cap,justified}=job;
|
|
622
|
-
if (!cap && !hanging && !justified) {restoreSelection(paragraph,job.selected);continue;}
|
|
653
|
+
if (job.needsClear && job.saved && !job.projection) clear(job.paragraph,{forget:false});
|
|
654
|
+
const {paragraph,view,hanging,cap,justified,ragged}=job;
|
|
655
|
+
if (!cap && !hanging && !justified && !ragged) {restoreSelection(paragraph,job.selected);continue;}
|
|
623
656
|
const state=stateFor(job);
|
|
624
657
|
states.set(paragraph,state);live.add(paragraph);paragraph.setAttribute(mark,'');
|
|
625
658
|
if (cap || hanging) protectInitial(paragraph);
|
|
@@ -631,6 +664,7 @@ export function createTypography() {
|
|
|
631
664
|
}
|
|
632
665
|
if (job.projection) {updateProjection(job.projection,job.plan);state.projection=job.projection;}
|
|
633
666
|
else if (job.plan) state.projection=project(paragraph,job.saved,job.geometry,job.plan);
|
|
667
|
+
paragraph.toggleAttribute(mark+'-ragged',!!ragged && !!state.projection);
|
|
634
668
|
if (state.projection && job.beside && !laidAsPlanned(paragraph,job)) {
|
|
635
669
|
refused.set(paragraph,job.sizeKey+'\u0000'+paragraph.textContent);clear(paragraph);restoreSelection(paragraph,job.selected);continue;
|
|
636
670
|
}
|
|
@@ -638,18 +672,103 @@ export function createTypography() {
|
|
|
638
672
|
else if (hanging) hangPunctuation(paragraph,state,job.style,hangingEdge(paragraph,job.style,paragraph.getBoundingClientRect()));
|
|
639
673
|
state.html=paragraph.innerHTML;
|
|
640
674
|
restoreSelection(paragraph,job.selected);changed++;
|
|
641
|
-
if (editing) state.editKey=editKey(paragraph,generation);
|
|
642
675
|
}
|
|
643
676
|
return {changed,released,pending,prepared:preparedCount};
|
|
644
677
|
}
|
|
645
678
|
|
|
679
|
+
const slotPlans = new WeakMap();
|
|
680
|
+
function slotPlan(paragraph, items, options, input) {
|
|
681
|
+
const {width, top, obstacles, lineHeight, minWidth, justify, hyphenate} = input;
|
|
682
|
+
const round = value => Math.round(value * 100) / 100;
|
|
683
|
+
const key = [captureFontAuthority(paragraph.ownerDocument), width, lineHeight, minWidth, justify, hyphenate, options.language, options.wordBreak,
|
|
684
|
+
items.map(item => [item.text, item.font, item.letterSpacing, item.break, item.extraWidth].join('\u0001')).join('\u0002'),
|
|
685
|
+
obstacles.map(item => [item.x, item.y - top, item.width, item.height].map(round).join(',')).join(';')].join('\u0000');
|
|
686
|
+
const known = slotPlans.get(paragraph);
|
|
687
|
+
if (known?.key === key) return known.plan;
|
|
688
|
+
const plan = slotLines(paragraph, items, options, input);
|
|
689
|
+
slotPlans.set(paragraph, {key, plan});
|
|
690
|
+
return plan;
|
|
691
|
+
}
|
|
692
|
+
function slotLines(paragraph, items, options, {width, top, obstacles, lineHeight, minWidth, slotsForBand, justify, hyphenate}) {
|
|
693
|
+
if (!obstacles.length) return null;
|
|
694
|
+
const doc = paragraph.ownerDocument, words = scope(doc);
|
|
695
|
+
const source = items.map(item => ({...item, hyphenate}));
|
|
696
|
+
const geometry = words.prepare(paragraph, source, {policy: 'paragraph', language: options.language, whiteSpace: 'normal',
|
|
697
|
+
wordBreak: options.wordBreak, hanging: false, paragraphBreaks: [0, source.length]});
|
|
698
|
+
if (!geometry) return null;
|
|
699
|
+
const sequence = [], bottom = Math.max(...obstacles.map(item => item.y + item.height)) - top;
|
|
700
|
+
let y = 0, rows = 0;
|
|
701
|
+
while (y < bottom && rows < 512 && sequence.length < 1024) {
|
|
702
|
+
const bands = slotsForBand(width, obstacles, top + y, lineHeight, Math.min(minWidth, width));
|
|
703
|
+
if (!bands) return null;
|
|
704
|
+
if (bands.length) {
|
|
705
|
+
for (const band of bands) sequence.push({x: band.x, y, width: band.width});
|
|
706
|
+
y += lineHeight; rows++; continue;
|
|
707
|
+
}
|
|
708
|
+
let next = Infinity;
|
|
709
|
+
for (const obstacle of obstacles) if (obstacle.x < width && obstacle.x + obstacle.width > 0 &&
|
|
710
|
+
obstacle.y < top + y + lineHeight && obstacle.y + obstacle.height > top + y) next = Math.min(next, obstacle.y + obstacle.height - top);
|
|
711
|
+
if (!Number.isFinite(next) || next <= y) return null;
|
|
712
|
+
y = next;
|
|
713
|
+
}
|
|
714
|
+
const prepared = fitter.prepare(geometry);
|
|
715
|
+
let slots = sequence, plan = null;
|
|
716
|
+
for (let pass = 0; prepared && pass < 8; pass++) {
|
|
717
|
+
const measures = slots.map(slot => Math.floor(slot.width * 64) / 64);
|
|
718
|
+
if (!measures.length) return null;
|
|
719
|
+
plan = words.fit(paragraph, geometry, known => fitter.solveSteps(prepared, width, {widths: measures, shrink: justify ? undefined : 0}, known));
|
|
720
|
+
if (!plan) return null;
|
|
721
|
+
const wide = new Set();
|
|
722
|
+
plan.lines.forEach((line, index) => {
|
|
723
|
+
if (index >= slots.length || line.firstWord !== line.lastWord || line.kind === 'soft' || line.kind === 'auto') return;
|
|
724
|
+
const word = geometry.segments[line.segment].words[line.firstWord];
|
|
725
|
+
if (line.start === word.start && line.end === word.end && word.width > measures[index] + .5) wide.add(slots[index]);
|
|
726
|
+
});
|
|
727
|
+
if (!wide.size) break;
|
|
728
|
+
slots = slots.filter(slot => !wide.has(slot)); plan = null;
|
|
729
|
+
}
|
|
730
|
+
const perRow = new Map();
|
|
731
|
+
for (const slot of slots) perRow.set(slot.y, (perRow.get(slot.y) || 0) + 1);
|
|
732
|
+
if (!plan || ![...perRow.values()].some(count => count > 1)) return null;
|
|
733
|
+
const lines = [], text = geometry.text, offsets = geometry.offsets, space = /^[\t\n\f\r ]*$/;
|
|
734
|
+
for (let index = 0; index < plan.lines.length; index++) {
|
|
735
|
+
const line = plan.lines[index], slot = sequence[index] || {x: 0, y: y + (index - sequence.length) * lineHeight, width};
|
|
736
|
+
const spacing = justify ? line.wordSpacing : 0, fragments = [];
|
|
737
|
+
const segment = geometry.segments[line.segment].words.slice(line.firstWord, line.lastWord + 1);
|
|
738
|
+
let at = 0, previous = null;
|
|
739
|
+
while (at < source.length - 1 && offsets[at + 1] <= line.start) at++;
|
|
740
|
+
for (; at < source.length && offsets[at] < line.end; at++) {
|
|
741
|
+
let from = Math.max(line.start, offsets[at]), to = Math.min(line.end, offsets[at + 1]);
|
|
742
|
+
if (previous) while (from < to && /[\t\n\f\r ]/.test(text[from])) from++;
|
|
743
|
+
if (offsets[at + 1] < line.end) while (to > from && /[\t\n\f\r ]/.test(text[to - 1])) to--;
|
|
744
|
+
if (to <= from) continue;
|
|
745
|
+
const fragment = {itemIndex: at, sourceStart: from - offsets[at], sourceEnd: to - offsets[at], derive: true, occupiedWidth: 0, gapItemIndex: at, gapBefore: 0};
|
|
746
|
+
if (previous) {
|
|
747
|
+
fragment.gapItemIndex = previous.itemIndex;
|
|
748
|
+
if (previous.end < from) {
|
|
749
|
+
if (!space.test(text.slice(previous.end, from))) return null;
|
|
750
|
+
const before = segment.find(word => word.end === previous.end);
|
|
751
|
+
if (!before?.gap) return null;
|
|
752
|
+
fragment.gapBefore = before.gap.width + spacing;
|
|
753
|
+
} else fragment.gapBefore = 0;
|
|
754
|
+
}
|
|
755
|
+
fragments.push(fragment); previous = {itemIndex: at, end: to};
|
|
756
|
+
}
|
|
757
|
+
if (!fragments.length) return null;
|
|
758
|
+
if (line.kind === 'soft' || line.kind === 'auto') fragments.at(-1).glyph = line.glyph || '-';
|
|
759
|
+
lines.push({x: slot.x, y: slot.y, width: slot.width, fragments, wordSpacing: spacing});
|
|
760
|
+
}
|
|
761
|
+
const last = lines.at(-1);
|
|
762
|
+
return {lines, height: last ? Math.max(lineHeight, last.y + lineHeight) : lineHeight};
|
|
763
|
+
}
|
|
764
|
+
|
|
646
765
|
function retire(root, {documentChanged=false}={}) {
|
|
647
766
|
clear(root);
|
|
648
767
|
for (const paragraph of live) if (!paragraph.isConnected) clear(paragraph);
|
|
649
768
|
scope(root?.ownerDocument || root || globalThis.document).retire({documentChanged});
|
|
650
769
|
}
|
|
651
770
|
const invalidateFonts = document => scope(document || globalThis.document).invalidateFonts();
|
|
652
|
-
return {apply,clear,clearSelection,firstLineEnd,initialRange,protectInitial,invalidateFonts,retire};
|
|
771
|
+
return {apply,clear,clearSelection,firstLineEnd,initialRange,protectInitial,invalidateFonts,retire,slotPlan};
|
|
653
772
|
}
|
|
654
773
|
|
|
655
774
|
const typography = createTypography();
|
|
@@ -661,3 +780,4 @@ export const initialRange = typography.initialRange;
|
|
|
661
780
|
export const protectInitial = typography.protectInitial;
|
|
662
781
|
export const invalidateFonts = typography.invalidateFonts;
|
|
663
782
|
export const retire = typography.retire;
|
|
783
|
+
export const slotPlan = typography.slotPlan;
|
|
@@ -216,6 +216,11 @@ export function documentFontUsage(markdown, parser, defaultFont) {
|
|
|
216
216
|
for (const token of md.parse(splitOpeningFrontmatter(source).body, {})) {
|
|
217
217
|
if (token.type === 'heading_open') blockFamily = sans;
|
|
218
218
|
else if (token.type === 'heading_close' || token.type === 'paragraph_open') blockFamily = main;
|
|
219
|
+
else if (token.type === 'fence' && /^html(?:\s+\{[^{}]*\})?\s*$/i.test(token.info)) {
|
|
220
|
+
const text = plain(token.content);
|
|
221
|
+
add(blockFamily, text);
|
|
222
|
+
for (const name of htmlFontFamilies(token.content, md)) add(name, text);
|
|
223
|
+
}
|
|
219
224
|
else if (token.type === 'fence' || token.type === 'code_block') add(mono, token.content);
|
|
220
225
|
else if (token.type === 'html_block') {
|
|
221
226
|
const text = plain(token.content);
|
|
@@ -128,10 +128,11 @@ function paperOf(value) {
|
|
|
128
128
|
const pixels = /^(\d{3,5})x(\d{3,5})px$/.exec(name), width = Number(pixels?.[1]), height = Number(pixels?.[2]);
|
|
129
129
|
return pixels && width <= 10000 && height <= 10000 ? Object.freeze({name, width: width * 15, height: height * 15, css: width + 'px ' + height + 'px'}) : null;
|
|
130
130
|
}
|
|
131
|
+
export const pageSizeOf = settings => settings?.papersize || PAPERS.letter;
|
|
131
132
|
export const PAGE_SIZES = Object.freeze(['letter', 'legal', 'tabloid', 'a3', 'a4', 'a5', '1080x1080px', '1080x1350px', '1920x1080px', '1200x630px']);
|
|
132
133
|
const EMPTY = Object.freeze({
|
|
133
134
|
title: null, subtitle: null, lang: null, fontsize: null, mainfont: null, sansfont: null, monofont: null, defaultfont: null, linestretch: null,
|
|
134
|
-
papersize: null, geometry: null, pagestyle: null, spacing: null,
|
|
135
|
+
papersize: null, geometry: null, pagestyle: null, spacing: null, template: null,
|
|
135
136
|
});
|
|
136
137
|
|
|
137
138
|
function unquote(value) {
|
|
@@ -351,6 +352,7 @@ export function readDocumentSettings(markdown) {
|
|
|
351
352
|
fontsize, mainfont, sansfont, monofont, defaultfont, linestretch, papersize, geometry,
|
|
352
353
|
spacing: normalizeDocumentSpacing(take('spacing')),
|
|
353
354
|
pagestyle: page === 'plain' || page === 'empty' ? page : null,
|
|
355
|
+
template: String(take('template') || '').toLowerCase() === 'page' ? 'page' : null,
|
|
354
356
|
});
|
|
355
357
|
}
|
|
356
358
|
|
|
@@ -368,14 +370,14 @@ export function updateDocumentSettings(markdown, patch) {
|
|
|
368
370
|
text: (source[field.start - 1] === ':' ? ' ' : '') + text + (source[field.end] === '#' ? ' ' : '')});
|
|
369
371
|
for (const [key, supplied] of Object.entries(patch || {})) {
|
|
370
372
|
const font = ['mainfont', 'sansfont', 'monofont', 'defaultfont'].includes(key);
|
|
371
|
-
if (!font && !['fontsize', 'linestretch', 'spacing', 'geometry', 'papersize'].includes(key)) throw new TypeError('Unknown document setting: ' + key);
|
|
373
|
+
if (!font && !['fontsize', 'linestretch', 'spacing', 'geometry', 'papersize', 'pagestyle'].includes(key)) throw new TypeError('Unknown document setting: ' + key);
|
|
372
374
|
const matches = fields.filter(item => item.key === key);
|
|
373
375
|
if (matches.length > 1) throw new RangeError('Duplicate document setting: ' + key);
|
|
374
376
|
const field = matches[0];
|
|
375
|
-
const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) : key === 'spacing' ? normalizeDocumentSpacing(supplied) : key === 'geometry' ? marginOf(supplied) : key === 'papersize' ? paperOf(supplied) : leading(supplied);
|
|
377
|
+
const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) : key === 'spacing' ? normalizeDocumentSpacing(supplied) : key === 'geometry' ? marginOf(supplied) : key === 'papersize' ? paperOf(supplied) : key === 'pagestyle' ? (['plain', 'empty'].includes(supplied) ? {name: supplied} : null) : leading(supplied);
|
|
376
378
|
if (supplied !== null && !value) throw new TypeError('Invalid document setting: ' + key);
|
|
377
|
-
const encoded = value === null ? '' : font ? JSON.stringify(value.family) : key === 'fontsize' ? value.step : key === 'spacing' ? JSON.stringify(value) : key === 'geometry' ? unquote(supplied).trim() : key === 'papersize' ? value.name : String(value.factor);
|
|
378
|
-
if (value !== null && before[key] && (font ? before[key].family === value.family : key === 'fontsize' ? same(before.fontsize.unit, value.unit) : key === 'spacing' ? JSON.stringify(before.spacing) === encoded : key === 'geometry' ? ['top', 'right', 'bottom', 'left'].every(side => before.geometry[side] === value[side]) : key === 'papersize' ? before.papersize.name === value.name : before.linestretch.factor === value.factor)) continue;
|
|
379
|
+
const encoded = value === null ? '' : font ? JSON.stringify(value.family) : key === 'fontsize' ? value.step : key === 'spacing' ? JSON.stringify(value) : key === 'geometry' ? unquote(supplied).trim() : key === 'papersize' || key === 'pagestyle' ? value.name : String(value.factor);
|
|
380
|
+
if (value !== null && before[key] && (font ? before[key].family === value.family : key === 'fontsize' ? same(before.fontsize.unit, value.unit) : key === 'spacing' ? JSON.stringify(before.spacing) === encoded : key === 'geometry' ? ['top', 'right', 'bottom', 'left'].every(side => before.geometry[side] === value[side]) : key === 'papersize' ? before.papersize.name === value.name : key === 'pagestyle' ? before.pagestyle === value.name : before.linestretch.factor === value.factor)) continue;
|
|
379
381
|
if (key === 'fontsize' && value !== null && fontSize(encoded)?.unit !== value.unit) refuse(key);
|
|
380
382
|
if (!field) { if (value !== null) append.push(key + ': ' + encoded + eol); continue; }
|
|
381
383
|
if (field.opaque || field.invalid || field.multiline || field.block) refuse(key);
|
|
@@ -53,7 +53,7 @@ export function htmlFence(raw) {
|
|
|
53
53
|
const tail = raw.slice(end), attached = /^(\r?\n)(<!--md-layout:v1[^\r\n]*-->)(?:\r?\n)?$/.exec(tail);
|
|
54
54
|
const layout = attached && parseLayout(attached[2]);
|
|
55
55
|
if (tail.trim() && !layout) return null;
|
|
56
|
-
if (layout && ['align', 'first', 'indent', 'marker', 'hyphens', 'caps', 'hanging'].some(key => key in layout)) return null;
|
|
56
|
+
if (layout && ['align', 'first', 'indent', 'marker', 'hyphens', 'caps', 'hanging', 'lines', 'before', 'after', 'line'].some(key => key in layout)) return null;
|
|
57
57
|
return {attrs, info: opening[3].trim(), body: rest.slice(0, found.index), infoStart: opening[1].length + mark.length,
|
|
58
58
|
infoEnd: opening[0].replace(/\r?\n$/, '').length, source: raw, start: 0, end: raw.length, fenceEnd: end,
|
|
59
59
|
layout: layout || HTML_FLOW, marker: attached ? {start: end + attached[1].length, end: end + attached[1].length + attached[2].length, text: attached[2]} : null};
|
|
@@ -545,24 +545,43 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
545
545
|
value.width < 1 || value.viewportHeight < 1 || value.width > 4096 || value.viewportHeight > 4096) return;
|
|
546
546
|
if (Math.abs(row.frame.clientWidth - value.width) > 1 || Math.abs(row.frame.clientHeight - value.viewportHeight) > 1) {message(row, 'snapshot'); return;}
|
|
547
547
|
const stamp = ++row.generation, scale = Math.min(1, 1024 / Math.max(value.width, value.viewportHeight));
|
|
548
|
-
const
|
|
549
|
-
const
|
|
548
|
+
const ratio = win.devicePixelRatio || 1, sharp = Math.min(ratio, 4096 / Math.max(value.width, value.viewportHeight), Math.sqrt(16777216 / (value.width * value.viewportHeight)));
|
|
549
|
+
const current = () => !dead && owned.get(row.image) === row && stamp === row.generation && keyOf(row.image) === row.key;
|
|
550
|
+
const draw = async factor => {
|
|
551
|
+
const canvas = doc.createElement('canvas'), snapshot = new win.Image();
|
|
552
|
+
canvas.width = Math.max(1, Math.ceil(value.width * factor)); canvas.height = Math.max(1, Math.ceil(value.viewportHeight * factor));
|
|
553
|
+
try {
|
|
554
|
+
snapshot.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="' + canvas.width + '" height="' + canvas.height + '" viewBox="0 0 ' + value.width + ' ' + value.viewportHeight + '"><foreignObject width="100%" height="100%">' + value.markup + '</foreignObject></svg>');
|
|
555
|
+
await Promise.race([snapshot.decode(), new Promise((_, reject) => win.setTimeout(() => reject(new Error('Snapshot decode timeout')), 2000))]);
|
|
556
|
+
if (!current()) return null;
|
|
557
|
+
const ctx = canvas.getContext('2d'); ctx.drawImage(snapshot, 0, 0); ctx.getImageData(0, 0, 1, 1);
|
|
558
|
+
return canvas.toDataURL('image/png');
|
|
559
|
+
} finally {snapshot.removeAttribute('src'); canvas.width = canvas.height = 0;}
|
|
560
|
+
};
|
|
550
561
|
try {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
if (dead || owned.get(row.image) !== row || stamp !== row.generation || keyOf(row.image) !== row.key) return;
|
|
562
|
+
const first = await draw(scale);
|
|
563
|
+
if (first == null) return;
|
|
554
564
|
if (Math.abs(row.frame.clientWidth - value.width) > 1 || Math.abs(row.frame.clientHeight - value.viewportHeight) > 1) {message(row, 'snapshot'); return;}
|
|
555
|
-
|
|
556
|
-
const
|
|
557
|
-
|
|
565
|
+
if (first.length > 4 * 1024 * 1024) throw new Error('Snapshot byte budget');
|
|
566
|
+
const shown = sharp > scale ? await draw(sharp).catch(() => first) : first;
|
|
567
|
+
if (shown == null) return;
|
|
568
|
+
row.ratio = ratio;
|
|
569
|
+
row.image.dataset.rapierFirstFrame = first; row.image.src = shown;
|
|
558
570
|
row.snapshot = true;
|
|
559
571
|
row.image.dataset.rapierLiveSurface = '';
|
|
560
572
|
row.image.dataset.rapierContour = row.animated ? 'animation-box' : value.snapshotStatus === 'sampled' ? 'sampled' : 'unavailable-box';
|
|
561
573
|
row.image.dataset.rapierNaturalWidth = String(value.width); row.image.dataset.rapierNaturalHeight = String(value.viewportHeight);
|
|
562
574
|
row.image.dispatchEvent(new win.Event('rapier-html-resize', {bubbles: true})); schedule();
|
|
563
575
|
} catch {if (owned.get(row.image) === row) {row.image.dataset.rapierContour = 'unavailable-box'; row.image.dataset.rapierSnapshot = 'unavailable';}}
|
|
564
|
-
finally {snapshot.removeAttribute('src'); canvas.width = canvas.height = 0;}
|
|
565
576
|
}
|
|
577
|
+
let density = null;
|
|
578
|
+
const redraw = () => {
|
|
579
|
+
density?.removeEventListener('change', redraw);
|
|
580
|
+
if (dead) return;
|
|
581
|
+
density = win.matchMedia('(resolution: ' + (win.devicePixelRatio || 1) + 'dppx)'); density.addEventListener('change', redraw);
|
|
582
|
+
for (const row of owned.values()) if (row.snapshot && row.ratio !== (win.devicePixelRatio || 1)) {row.generation++; message(row, 'snapshot');}
|
|
583
|
+
};
|
|
584
|
+
redraw();
|
|
566
585
|
const changedVisibility = () => {for (const row of owned.values()) applyPlayback(row);};
|
|
567
586
|
const hidden = event => {if (!event.persisted) {api.close(); return;} for (const row of owned.values()) {row.visible = false; applyPlayback(row);}};
|
|
568
587
|
const shown = () => {scan(); for (const row of owned.values()) {const box = row.image.getBoundingClientRect();
|
|
@@ -580,7 +599,7 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
580
599
|
snapshot: image.dataset.rapierSnapshot || null, contour: image.dataset.rapierContour || null, status: image.dataset.rapierPlayback || 'ready'} : null;},
|
|
581
600
|
close() {dead = true; win.cancelAnimationFrame(pending); observer.disconnect(); resize.disconnect(); visibility?.disconnect();
|
|
582
601
|
win.removeEventListener('message', receive); win.removeEventListener('resize', schedule); doc.removeEventListener('scroll', schedule, true);
|
|
583
|
-
doc.removeEventListener('visibilitychange', changedVisibility); reduced.removeEventListener('change', changedMotion);
|
|
602
|
+
doc.removeEventListener('visibilitychange', changedVisibility); reduced.removeEventListener('change', changedMotion); density?.removeEventListener('change', redraw);
|
|
584
603
|
for (const row of [...owned.values()]) release(row); sheet.remove(); local.clear();
|
|
585
604
|
win.removeEventListener('pagehide', hidden); win.removeEventListener('pageshow', shown); if (doc.rapierObjects === api) delete doc.rapierObjects;}};
|
|
586
605
|
win.addEventListener('pagehide', hidden); win.addEventListener('pageshow', shown); if (!options.root) doc.rapierObjects = api; scan(); return api;
|
package/dist/spec/md-assets.mjs
CHANGED
|
@@ -515,9 +515,10 @@ export function retireDeletedImageDefinitions(before, after, splices, factory) {
|
|
|
515
515
|
return retired.sort((a, b) => b.start - a.start).map(row => {
|
|
516
516
|
const gap = /(?:\r\n\r\n|\n\n|\r\r)$/.exec(after.slice(Math.max(0, row.start - 4), row.start));
|
|
517
517
|
let tail = after.slice(row.end);
|
|
518
|
+
const last = /^(?:\r\n|\n|\r)?$/.test(tail);
|
|
518
519
|
for (const other of retired) if (other.start >= row.end) tail = tail.replace(other.source, '');
|
|
519
520
|
const whole = gap && !/\S/.test(tail) && row.start - gap[0].length > markdownBodyOffset(after);
|
|
520
|
-
return whole ? {pos: row.start - gap[0].length, removed: gap[0] + row.source, inserted: ''} : {pos: row.start, removed: row.source, inserted: ''};
|
|
521
|
+
return whole ? {pos: row.start - gap[0].length, removed: gap[0] + row.source + (last ? after.slice(row.end) : ''), inserted: ''} : {pos: row.start, removed: row.source, inserted: ''};
|
|
521
522
|
});
|
|
522
523
|
} catch (_) { return []; }
|
|
523
524
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
return !!match && /^template:[ \t]*page[ \t]*(?:#.*)?$/m.test(match[1]);
|
|
5
|
-
}
|
|
2
|
+
import {readDocumentSettings} from './document-settings.mjs';
|
|
3
|
+
export const isTemplatePage = source => readDocumentSettings(source).template === 'page';
|
|
6
4
|
export const isPageBorderLayout = layout => layout?.wrap === 'behind' && layout.lock === 'on';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-markdown-kit",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.32",
|
|
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
|
@@ -326,6 +326,23 @@
|
|
|
326
326
|
position: relative;
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
/* The line that holds the box hangs: its later lines start under the first line's words, not under the box. The box takes
|
|
330
|
+
16px and a space of the first line (the 48px target less its overhangs), which the hang gives back. */
|
|
331
|
+
.md-render .task-list-item > p:has(> input[type="checkbox"]:first-child),
|
|
332
|
+
.md-render .task-list-item:has(> input[type="checkbox"]:first-child) {
|
|
333
|
+
/* The space the source keeps between the box and the words (`- [ ] words`), so the later lines meet the words. */
|
|
334
|
+
--md-task-space: .24em;
|
|
335
|
+
padding-inline-start: calc(16px + var(--md-space-1) + var(--md-task-space));
|
|
336
|
+
text-indent: calc(-16px - var(--md-space-1) - var(--md-task-space));
|
|
337
|
+
}
|
|
338
|
+
/* The hang is the line's alone: nothing inside it (a formula, a code chip) or after it takes the indent, and a tight item's
|
|
339
|
+
later blocks (a nested list, a paragraph) keep their own place. */
|
|
340
|
+
.md-render .task-list-item > p:has(> input[type="checkbox"]:first-child) *,
|
|
341
|
+
.md-render .task-list-item:has(> input[type="checkbox"]:first-child) * { text-indent: 0; }
|
|
342
|
+
.md-render .task-list-item:has(> input[type="checkbox"]:first-child) > :is(p, ul, ol, pre, blockquote, table, div, dl, details, figure, hr) {
|
|
343
|
+
margin-inline-start: calc(-16px - var(--md-space-1) - var(--md-task-space));
|
|
344
|
+
}
|
|
345
|
+
|
|
329
346
|
.md-render .task-list-item input[type="checkbox"] {
|
|
330
347
|
--md-task-box: clamp(14px, .92em, 24px);
|
|
331
348
|
/* A form control does not inherit color on its own; the shell's own app sheet gives it one
|