rapier-markdown-kit 1.2.33 → 1.2.40
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 +15 -9
- package/dist/agent/vendor/pretext/NOTICE +2 -2
- package/dist/agent/vendor/pretext/SOURCE.json +62 -78
- package/dist/agent/vendor/pretext/analysis.js +24 -6
- package/dist/agent/vendor/pretext/gecko-line-breaks.js +1 -1
- package/dist/agent/vendor/pretext/generated/engine-break-data.js +1 -1
- package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +1 -1
- package/dist/agent/vendor/pretext/graphemes.js +1 -1
- package/dist/agent/vendor/pretext/han-kerning.js +1 -1
- package/dist/agent/vendor/pretext/layout.js +14 -11
- package/dist/agent/vendor/pretext/line-break.js +170 -195
- package/dist/agent/vendor/pretext/line-breaks.js +1 -7
- package/dist/agent/vendor/pretext/line-text.js +2 -2
- package/dist/agent/vendor/pretext/measurement.js +454 -113
- package/dist/agent/vendor/pretext/prepare.js +164 -244
- package/dist/agent/vendor/pretext/rich-inline.js +57 -37
- package/dist/agent/will.mjs +73 -0
- package/dist/kit/ledger/data.mjs +2 -2
- package/dist/kit/ledger/format.mjs +86 -42
- package/dist/kit/ledger/hash.mjs +43 -2
- package/dist/kit/ledger/history.mjs +6 -0
- package/dist/kit/ledger/inverse-source.mjs +6 -2
- package/dist/kit/ledger/journal-records.mjs +4 -2
- package/dist/kit/ledger/merge.mjs +3 -17
- package/dist/kit/ledger/text-merge.mjs +28 -2
- package/dist/kit/rapier-ledger.mjs +2 -2
- package/dist/kit/render-markdown.mjs +133 -10
- package/dist/kit/render-print.mjs +3 -3
- package/dist/kit/render-sanitize.mjs +66 -23
- package/dist/kit/render.mjs +10 -10
- package/dist/kit/wiki.mjs +34 -0
- package/dist/kit/will.d.mts +3 -0
- package/dist/layout/hyphenation.mjs +6 -4
- package/dist/layout/line-plan.mjs +96 -0
- package/dist/layout/model.mjs +2 -1
- package/dist/layout/paragraph-fit.mjs +4 -4
- package/dist/layout/prepared-paragraph.mjs +19 -2
- package/dist/layout/typography.mjs +57 -63
- package/dist/spec/document-fonts.mjs +1 -0
- package/dist/spec/document-settings.mjs +1 -0
- package/dist/spec/html-blocks.mjs +10 -7
- package/dist/spec/md-assets.mjs +52 -12
- package/package.json +1 -1
- package/style.css +25 -23
- package/dist/agent/vendor/pretext/entry-geometry.js +0 -183
package/dist/kit/render.mjs
CHANGED
|
@@ -212,14 +212,14 @@ function _rapierTemplatePage(canonical, settings, root, darkPaper = false) {
|
|
|
212
212
|
const image = paragraph.querySelector('img');
|
|
213
213
|
const layout = globalThis.RapierMarkdownLayout.parseLayoutAttribute(paragraph.getAttribute('data-md-layout'));
|
|
214
214
|
const source = image?.getAttribute('src') || '';
|
|
215
|
-
if (isPageBorderLayout(layout) &&
|
|
215
|
+
if (isPageBorderLayout(layout) && globalThis.RapierImageAssets.dataImage(source)) { url = source; paragraph.remove(); break; }
|
|
216
216
|
}
|
|
217
217
|
if (!url) return null;
|
|
218
218
|
const width = paper.width / 15, height = paper.height / 15;
|
|
219
219
|
const margins = ['top', 'right', 'bottom', 'left'].map(side => (settings?.geometry?.[side] ?? 1440) / 15);
|
|
220
220
|
const padding = margins.map(value => value + 'px').join(' ');
|
|
221
221
|
const ground = 'background:var(--rapier-template-border) 0 0/100% 100% no-repeat';
|
|
222
|
-
const shown = darkPaper &&
|
|
222
|
+
const shown = darkPaper && globalThis.RapierImageAssets.dataImage(url)?.codec === 'image/svg+xml' ? _rapierDarkPaperSvgUrl(url, '#fafafa') : url;
|
|
223
223
|
const css = ':root{--rapier-template-border:url("' + url + '")}\n'
|
|
224
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
|
|
225
225
|
+ ';background:var(--rapier-template-border) 0 0/100% ' + height + 'px repeat-y,var(--md-color-bg);color:var(--md-color-text)}}\n'
|
|
@@ -235,7 +235,7 @@ function _rapierDarkPaperSvgUrl(url, paperInk) {
|
|
|
235
235
|
const ink = globalThis.RapierImageInspection?.inkForPaper;
|
|
236
236
|
if (typeof ink !== 'function') return url;
|
|
237
237
|
let text;
|
|
238
|
-
try { text = new TextDecoder('utf-8', {fatal: true}).decode(
|
|
238
|
+
try { text = new TextDecoder('utf-8', {fatal: true}).decode(globalThis.RapierImageAssets.decodeDataImageBytes(url).bytes); }
|
|
239
239
|
catch (_) { return url; }
|
|
240
240
|
const inked = ink(text, {dark: true, paperInk});
|
|
241
241
|
if (inked === text) return url;
|
|
@@ -274,7 +274,7 @@ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
|
|
|
274
274
|
for (const token of root.querySelectorAll('.rapier-source-token--softbreak')) { yield;
|
|
275
275
|
if (token.getAttribute('data-rapier-source') !== '%0A') token.replaceWith(...token.childNodes);
|
|
276
276
|
else if (token.previousSibling?.classList?.contains('callout__label')) token.remove();
|
|
277
|
-
else token.replaceWith(root.ownerDocument.createTextNode(' '));
|
|
277
|
+
else token.replaceWith(root.ownerDocument.createTextNode(token.closest('pre') ? '\n' : ' '));
|
|
278
278
|
}
|
|
279
279
|
const metadata = opts.metadata || {};
|
|
280
280
|
|
|
@@ -532,10 +532,10 @@ function _rapierAnnotateExportBoxPolygons(root) {
|
|
|
532
532
|
for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
|
|
533
533
|
const layout = globalThis.RapierMarkdownLayout.parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
|
|
534
534
|
if (layout?.wrap !== 'box' && layout?.wrap !== 'around') continue;
|
|
535
|
-
const
|
|
536
|
-
if (
|
|
535
|
+
const url = image.getAttribute('src') || '';
|
|
536
|
+
if (globalThis.RapierImageAssets.dataImage(url)?.codec !== 'image/svg+xml') continue;
|
|
537
537
|
let text;
|
|
538
|
-
try { text = new TextDecoder('utf-8', { fatal: true }).decode(
|
|
538
|
+
try { text = new TextDecoder('utf-8', { fatal: true }).decode(globalThis.RapierImageAssets.decodeDataImageBytes(url).bytes); }
|
|
539
539
|
catch (_) { continue; }
|
|
540
540
|
let recipe;
|
|
541
541
|
try { recipe = _rapierDrawReadSVGRecipe(text); } catch (_) { continue; }
|
|
@@ -634,7 +634,7 @@ function _rapierSharedResolve(text, ids, images, definitions) {
|
|
|
634
634
|
let broken = false;
|
|
635
635
|
const src = value => {
|
|
636
636
|
const found = images.get(value);
|
|
637
|
-
if (
|
|
637
|
+
if (!globalThis.RapierImageAssets.dataImage(found)) { broken = true; return null; }
|
|
638
638
|
return found;
|
|
639
639
|
};
|
|
640
640
|
const inline = new RegExp('(\\]\\([ \\t\\r\\n]*(?:<)?)#' + id + '(?=>?[ \\t\\r\\n]*\\)|>?[ \\t\\r\\n]+["\'(])', 'g');
|
|
@@ -896,7 +896,7 @@ async function _rapierRequireOfflinePageImages(root) {
|
|
|
896
896
|
}
|
|
897
897
|
if (!missing.length) return;
|
|
898
898
|
throw Object.assign(new Error('Web page not written; embed these pictures and try again. ' + missing.join('; ')),
|
|
899
|
-
{code: 'offline_image_unavailable'});
|
|
899
|
+
{code: 'offline_image_unavailable', missing});
|
|
900
900
|
}
|
|
901
901
|
|
|
902
902
|
function _rapierCssValueUrls(value) {
|
|
@@ -1741,7 +1741,7 @@ function _rapierArtifactLayoutScript(root, nonce = '', {pictures = true} = {}) {
|
|
|
1741
1741
|
throw new Error('Offline typography requires the complete retained MIT layout factories');
|
|
1742
1742
|
const modules = _rapierArtifactFactoryModules(dependencies, 'layout');
|
|
1743
1743
|
const script = '/* Rapier export layout: SPDX-License-Identifier: MIT */\n' +
|
|
1744
|
-
'/* Pretext
|
|
1744
|
+
'/* Pretext 1.0.0 + main 0d102a5cd933611e86a7e60bda7b4baa5995c6b0\n' + dependencies.license + '\n*/\n' +
|
|
1745
1745
|
'(() => {\nconst modules = Object.create(null);\n' + modules +
|
|
1746
1746
|
(wrapping ? '\n(' + _rapierProjectArtifactLayout.toString() +
|
|
1747
1747
|
')(document.querySelector("main.rapier-page"), modules["spec/md-layout.mjs"], modules["layout/model.mjs"], modules["layout/preparation.mjs"]);\n' : '') +
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
export const WIKI_PREFIX = '?rapier-wiki=';
|
|
3
|
+
export function wikiAddress(target) {
|
|
4
|
+
const at = target.indexOf('#'), path = at < 0 ? target : target.slice(0, at);
|
|
5
|
+
return WIKI_PREFIX + encodeURIComponent(path) + (at < 0 ? '' : '#' + encodeURIComponent(target.slice(at + 1)));
|
|
6
|
+
}
|
|
7
|
+
export function wikiTarget(address) {
|
|
8
|
+
if (typeof address !== 'string' || !address.startsWith(WIKI_PREFIX)) return null;
|
|
9
|
+
try { return decodeURIComponent(address.slice(WIKI_PREFIX.length).split('#')[0]); }
|
|
10
|
+
catch { return null; }
|
|
11
|
+
}
|
|
12
|
+
export function installWikiParser(md, render) {
|
|
13
|
+
md.inline.ruler.before('image', 'rapier_wiki', (state, silent) => {
|
|
14
|
+
if (!state.env?.rapierWikiLinks) return false;
|
|
15
|
+
const start = state.pos, embed = state.src[start] === '!';
|
|
16
|
+
const open = start + (embed ? 1 : 0);
|
|
17
|
+
if (state.linkLevel || state.src.slice(open, open + 2) !== '[[') return false;
|
|
18
|
+
const end = state.src.indexOf(']]', open + 2);
|
|
19
|
+
if (end < 0 || end + 2 > state.posMax || end - start > 4096) return false;
|
|
20
|
+
const body = state.src.slice(open + 2, end);
|
|
21
|
+
if (!body || /[\[\]\n\r\0]/.test(body)) return false;
|
|
22
|
+
const pipe = body.indexOf('|'), target = (pipe < 0 ? body : body.slice(0, pipe)).trim();
|
|
23
|
+
if (!target || /^(?:[a-z][a-z0-9+.-]*:|\/|\\)/i.test(target)) return false;
|
|
24
|
+
if (!silent) {
|
|
25
|
+
const token = state.push('rapier_wiki', '', 0);
|
|
26
|
+
token.content = state.src.slice(start, end + 2);
|
|
27
|
+
token.meta = {target, label: pipe < 0 ? target : body.slice(pipe + 1), embed};
|
|
28
|
+
}
|
|
29
|
+
state.pos = end + 2;
|
|
30
|
+
return true;
|
|
31
|
+
});
|
|
32
|
+
md.renderer.rules.rapier_wiki = (tokens, at, options, env) => render(tokens[at], env);
|
|
33
|
+
}
|
|
34
|
+
|
package/dist/kit/will.d.mts
CHANGED
|
@@ -40,3 +40,6 @@ export function willMarkerOf(line: string): WillMarker | null;
|
|
|
40
40
|
export function terminatorWidth(last: number, prior: number): number;
|
|
41
41
|
/** The text without one trailing line terminator. */
|
|
42
42
|
export function stripOneTerminator(text: string): string;
|
|
43
|
+
/** Judges sequential UTF-16 source splices, including the resulting marker identities and protected text. */
|
|
44
|
+
export function evaluateWill(beforeText: string, splices: {pos: number; removed: string; inserted: string}[]):
|
|
45
|
+
{outcome: 'applied'} | {outcome: 'invalid' | 'refused'; reason: string; rule: string; law?: WillLaw; region?: number};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
import {next, weight, exceptions, maximum} from '../agent/vendor/hyphen-en-us/patterns.js';
|
|
3
3
|
|
|
4
|
+
export const hyphenMinimums = {left: 2, right: 3, word: 5};
|
|
5
|
+
|
|
4
6
|
export function englishCuts(word) {
|
|
5
7
|
if (!/^[A-Za-z]+$/.test(word)) return null;
|
|
6
|
-
if (word.length <
|
|
8
|
+
if (word.length < hyphenMinimums.word) return [];
|
|
7
9
|
const cuts = [];
|
|
8
10
|
let offset = 0, before = 0;
|
|
9
11
|
for (const part of word.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z]+/g)) {
|
|
@@ -11,11 +13,11 @@ export function englishCuts(word) {
|
|
|
11
13
|
for (const at of partCuts(part)) cuts.push(offset+at);
|
|
12
14
|
offset += part.length; before = part.length;
|
|
13
15
|
}
|
|
14
|
-
return cuts.filter(at => at >=
|
|
16
|
+
return cuts.filter(at => at >= hyphenMinimums.left && at <= word.length-hyphenMinimums.right).sort((a, b) => a-b).filter((at, index, all) => at !== all[index-1]);
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
function partCuts(word) {
|
|
18
|
-
if (word.length <
|
|
20
|
+
if (word.length < hyphenMinimums.word) return [];
|
|
19
21
|
const lower = word.toLowerCase(), known = exceptions[lower];
|
|
20
22
|
if (Array.isArray(known)) return known;
|
|
21
23
|
const source = '.'+lower+'.', weights = new Uint8Array(source.length+1);
|
|
@@ -30,7 +32,7 @@ function partCuts(word) {
|
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
const cuts = [];
|
|
33
|
-
for (let at =
|
|
35
|
+
for (let at = hyphenMinimums.left; at <= word.length-hyphenMinimums.right; at++) if (weights[at+1]&1) cuts.push(at);
|
|
34
36
|
return cuts;
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -55,9 +55,105 @@ function laterWholeWord(flow, cursor, line, items, bands, slot, next, materializ
|
|
|
55
55
|
return false;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
export function slotSequence(width, top, obstacles, lineHeight, minWidth, slotsForBand, direction = 'ltr', order = 'columns') {
|
|
59
|
+
const bottom = obstacles.length ? Math.max(...obstacles.map(item => item.y + item.height)) - top : 0, rows = [];
|
|
60
|
+
let y = 0;
|
|
61
|
+
while (y < bottom && rows.length < 768) {
|
|
62
|
+
const bands = slotsForBand(width, obstacles, top + y, lineHeight, Math.min(minWidth, width));
|
|
63
|
+
if (!bands) return null;
|
|
64
|
+
if (bands.length) { rows.push({y, bands}); y += lineHeight; continue; }
|
|
65
|
+
let next = Infinity;
|
|
66
|
+
for (const obstacle of obstacles) if (obstacle.x < width && obstacle.x + obstacle.width > 0 &&
|
|
67
|
+
obstacle.y < top + y + lineHeight && obstacle.y + obstacle.height > top + y) next = Math.min(next, obstacle.y + obstacle.height - top);
|
|
68
|
+
if (!Number.isFinite(next) || next <= y) return null;
|
|
69
|
+
y = next;
|
|
70
|
+
}
|
|
71
|
+
const multiple = rows.some(row => row.bands.length > 1);
|
|
72
|
+
if (!multiple) return {slots: rows.flatMap(row => row.bands.map(band => ({...band, y: row.y}))), end: y, multiple};
|
|
73
|
+
const free = row => row.bands.length === 1 && row.bands[0].x < .5 && row.bands[0].width > width - .5;
|
|
74
|
+
const slots = [];
|
|
75
|
+
let block = 0;
|
|
76
|
+
for (let at = 0; at < rows.length;) {
|
|
77
|
+
if (free(rows[at])) { slots.push({...rows[at].bands[0], y: rows[at].y}); at++; continue; }
|
|
78
|
+
let stop = at;
|
|
79
|
+
while (stop < rows.length && !free(rows[stop])) stop++;
|
|
80
|
+
block++;
|
|
81
|
+
const channels = [];
|
|
82
|
+
for (let index = at; index < stop; index++) for (const band of rows[index].bands) {
|
|
83
|
+
const owner = channels.find(channel => channel.row === index - 1 &&
|
|
84
|
+
Math.min(channel.last.x + channel.last.width, band.x + band.width) - Math.max(channel.last.x, band.x) > 0);
|
|
85
|
+
const slot = {...band, y: rows[index].y};
|
|
86
|
+
if (owner) { owner.slots.push(slot); owner.row = index; owner.last = slot; }
|
|
87
|
+
else channels.push({slots: [slot], row: index, last: slot, x: band.x});
|
|
88
|
+
}
|
|
89
|
+
channels.sort((a, b) => direction === 'rtl' ? b.x - a.x : a.x - b.x);
|
|
90
|
+
const gaps = rows.slice(at, stop).filter(row => row.bands.length > 1).map(row => row.bands[1].x - row.bands[0].x - row.bands[0].width).sort((a, b) => a - b);
|
|
91
|
+
const thin = gaps[gaps.length >> 1] < .15 * width;
|
|
92
|
+
if (!thin) channels.forEach((channel, index) => { for (const slot of channel.slots) { slot.block = block; slot.channel = index; } });
|
|
93
|
+
if (order === 'rows' || thin) slots.push(...channels.flatMap(channel => channel.slots).sort((a, b) => a.y - b.y || (direction === 'rtl' ? b.x - a.x : a.x - b.x)));
|
|
94
|
+
else for (const channel of channels) slots.push(...channel.slots);
|
|
95
|
+
at = stop;
|
|
96
|
+
}
|
|
97
|
+
return {slots, end: y, multiple};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function channelPlan(flow, width, lineHeight, sequence, layoutNextRichInlineLineRange, materializeRichInlineLineRange, items) {
|
|
101
|
+
const lines = [], slots = sequence.slots;
|
|
102
|
+
let cursor, previous = null;
|
|
103
|
+
const place = (band, later) => {
|
|
104
|
+
let range = layoutNextRichInlineLineRange(flow, band.width, cursor);
|
|
105
|
+
if (!range) return 'end';
|
|
106
|
+
if (!Number.isFinite(range.width) || range.width > width + .5) return 'fail';
|
|
107
|
+
if (range.width > band.width + .5) return 'skip';
|
|
108
|
+
let line = materializeRichInlineLineRange(flow, range);
|
|
109
|
+
if (!afterSource(lastSource(line), previous)) return 'fail';
|
|
110
|
+
if (laterWholeWord(flow, cursor, line, items, [band, ...later], 0, layoutNextRichInlineLineRange, materializeRichInlineLineRange)) return 'skip';
|
|
111
|
+
if (later.some(other => other.y === band.y) && insideChip(items, line)) {
|
|
112
|
+
const whole = beforeChip(flow, range, line, cursor, items, layoutNextRichInlineLineRange, materializeRichInlineLineRange);
|
|
113
|
+
if (whole) { range = whole.range; line = whole.line; }
|
|
114
|
+
}
|
|
115
|
+
if (!afterSource(lastSource(line), previous) || lines.length === 4096) return 'fail';
|
|
116
|
+
lines.push({...line, ...band});
|
|
117
|
+
previous = lastSource(line); cursor = range.end;
|
|
118
|
+
return 'placed';
|
|
119
|
+
};
|
|
120
|
+
const done = () => lines.length ? {lines, height: Math.max(lineHeight, ...lines.map(line => line.y + lineHeight))} : null;
|
|
121
|
+
for (let index = 0; index < slots.length; index++) {
|
|
122
|
+
const result = place(slots[index], [...slots.slice(index + 1, index + 9), {x: 0, width, y: sequence.end}]);
|
|
123
|
+
if (result === 'end') return done();
|
|
124
|
+
if (result === 'fail') return null;
|
|
125
|
+
}
|
|
126
|
+
for (let row = 0; row < 768; row++) {
|
|
127
|
+
const result = place({x: 0, width, y: sequence.end + row * lineHeight}, []);
|
|
128
|
+
if (result === 'end') return done();
|
|
129
|
+
if (result !== 'placed') return null;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function columnsFilled(slots, placed) {
|
|
135
|
+
const total = new Map(), used = new Map(), reached = new Set();
|
|
136
|
+
for (const slot of slots) if (slot.block) total.set(slot.block + ':' + slot.channel, (total.get(slot.block + ':' + slot.channel) || 0) + 1);
|
|
137
|
+
for (const slot of placed) if (slot.block) { used.set(slot.block + ':' + slot.channel, (used.get(slot.block + ':' + slot.channel) || 0) + 1); reached.add(slot.block); }
|
|
138
|
+
for (const count of total.values()) if (count < 4) return false;
|
|
139
|
+
for (const block of reached) {
|
|
140
|
+
let last = -1;
|
|
141
|
+
for (const slot of slots) if (slot.block === block && slot.channel > last) last = slot.channel;
|
|
142
|
+
if ((used.get(block + ':' + last) || 0) < .6 * total.get(block + ':' + last)) return false;
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
58
147
|
export function linePlan(flow, width, top, obstacles, lineHeight, minWidth, direction, balance,
|
|
59
148
|
slotsForBand, layoutNextRichInlineLineRange, materializeRichInlineLineRange, items = null) {
|
|
60
149
|
if (!flow || balance) return null;
|
|
150
|
+
const sequence = obstacles.length ? slotSequence(width, top, obstacles, lineHeight, minWidth, slotsForBand, direction) : null;
|
|
151
|
+
if (sequence?.multiple) {
|
|
152
|
+
const columns = channelPlan(flow, width, lineHeight, sequence, layoutNextRichInlineLineRange, materializeRichInlineLineRange, items);
|
|
153
|
+
if (!columns || columnsFilled(sequence.slots, columns.lines)) return columns;
|
|
154
|
+
return channelPlan(flow, width, lineHeight, slotSequence(width, top, obstacles, lineHeight, minWidth, slotsForBand, direction, 'rows'),
|
|
155
|
+
layoutNextRichInlineLineRange, materializeRichInlineLineRange, items);
|
|
156
|
+
}
|
|
61
157
|
const lines = [];
|
|
62
158
|
let cursor, previous = null, y = 0, bottom = 0, rows = 0;
|
|
63
159
|
for (let step = 0; step < 4096 && rows < 768; step++) {
|
package/dist/layout/model.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
import {layoutNextRichInlineLineRange, materializeRichInlineLineRange} from '../agent/vendor/pretext/rich-inline.js';
|
|
3
3
|
import {validLayout} from '../spec/md-layout.mjs';
|
|
4
|
-
import {linePlan} from './line-plan.mjs';
|
|
4
|
+
import {linePlan, slotSequence, columnsFilled} from './line-plan.mjs';
|
|
5
5
|
import {captureFontAuthority, createPreparationScope, fontReady, invalidateRichMetrics, paragraphItems,
|
|
6
6
|
prepareRichParagraph, supportsRichStyle} from './preparation.mjs';
|
|
7
7
|
export {captureFontAuthority, createPreparationScope, invalidateRichMetrics, prepareRichParagraph, supportsRichStyle};
|
|
@@ -309,6 +309,7 @@ export function pictureSlices(profile, x, y, width, height, scale = 1) {
|
|
|
309
309
|
return slices;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
export {slotSequence, columnsFilled};
|
|
312
313
|
export function slotsForBand(width, obstacles, top, height, minWidth = 40) {
|
|
313
314
|
if (!finite(width) || width <= 0 || !finite(top) || !finite(height) || height <= 0 ||
|
|
314
315
|
!finite(top + height) || !finite(minWidth) || minWidth <= 0 || !Array.isArray(obstacles)) return null;
|
|
@@ -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, shrink = .
|
|
25
|
+
function* solveSteps(handle, width, {indent = 0, left = 0, right = 0, widths = null, shrink = .5, stretch = 1.2, hyphenCost = 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 : [];
|
|
@@ -102,7 +102,7 @@ export function createParagraphFitter() {
|
|
|
102
102
|
if (!Number.isFinite(natural)) continue;
|
|
103
103
|
const count = lastWord - start.word, gapWidth = gaps[lastWord] - gaps[start.word];
|
|
104
104
|
const opening = start.cut ? 0 : Math.min(Math.max(0, left), words[start.word].opening);
|
|
105
|
-
const closing = end.cut ? 0 : Math.min(Math.max(0, right), words[lastWord].closing);
|
|
105
|
+
const closing = end.cut ? Math.min(Math.max(0, right), end.cut.protrude || 0) : Math.min(Math.max(0, right), words[lastWord].closing);
|
|
106
106
|
const priorDiscretionary = start.kind === 'soft' || start.kind === 'auto';
|
|
107
107
|
if (final && priorDiscretionary) continue;
|
|
108
108
|
for (let lineClass = 0; lineClass < classes; lineClass++) {
|
|
@@ -120,7 +120,7 @@ export function createParagraphFitter() {
|
|
|
120
120
|
let local = 1 + 100 * Math.max(0, overflow / target) ** 2;
|
|
121
121
|
if (final && startIndex) local += 100 * Math.max(0, .45 - natural / target) ** 2 + (count === 0 ? 25 : 0);
|
|
122
122
|
else if (!final) local += (ratio < 0 ? 100 : 12.5) * ratio ** 2 + 2400 * (residual / target) ** 2;
|
|
123
|
-
if (discretionary) local +=
|
|
123
|
+
if (discretionary) local += hyphenCost + (priorDiscretionary ? hyphenCost : 0);
|
|
124
124
|
const set = !final && count && residual > 0 ? remaining / count : delta;
|
|
125
125
|
const nextClass = Math.min(lineClass + 1, classes - 1);
|
|
126
126
|
for (let active = available; active; active &= active - 1) {
|
|
@@ -153,7 +153,7 @@ export function createParagraphFitter() {
|
|
|
153
153
|
const start = points[startIndex], end = points[endIndex], lastWord = end.cut ? end.word : end.word - 1;
|
|
154
154
|
chosen.push({start: start.next, end: end.end, next: end.next, kind: end.kind, glyph: end.cut?.glyph || '',
|
|
155
155
|
wordSpacing: spacing[best], opening: start.cut ? 0 : Math.min(Math.max(0, left), words[start.word].opening),
|
|
156
|
-
closing: end.cut ? 0 : Math.min(Math.max(0, right), words[lastWord].closing),
|
|
156
|
+
closing: end.cut ? Math.min(Math.max(0, right), end.cut.protrude || 0) : Math.min(Math.max(0, right), words[lastWord].closing),
|
|
157
157
|
firstWord: start.word, lastWord, segment: segmentIndex});
|
|
158
158
|
best = priorIndex; endIndex = startIndex;
|
|
159
159
|
}
|
|
@@ -2,9 +2,25 @@
|
|
|
2
2
|
import {paragraphHyphens} from './hyphenation.mjs';
|
|
3
3
|
import {prepareRichInlineSegments} from '../agent/vendor/pretext/rich-inline.js';
|
|
4
4
|
import {getEngineProfile, getPreparationLanguage, getFontMeasurement, parseFontSize,
|
|
5
|
-
getTextWidth, getEmojiCorrection, textMayContainEmoji,
|
|
5
|
+
getTextWidth, getEmojiCorrection, textMayContainEmoji, getSegmentMetrics, getContext} from '../agent/vendor/pretext/measurement.js';
|
|
6
6
|
import {KIND_BITS, TEXT, SPACE, SOFT_HYPHEN, ZERO_WIDTH_BREAK, ZERO_WIDTH_GLUE, OBJECT, SPACED, UNBROKEN} from '../agent/vendor/pretext/analysis.js';
|
|
7
7
|
|
|
8
|
+
function measureWithLetterSpacing(text, letterSpacing, emojiCorrection, measurement) {
|
|
9
|
+
if (!measurement.state.takesLetterSpacing) return null;
|
|
10
|
+
const base = getTextWidth(text, measurement, emojiCorrection);
|
|
11
|
+
if (letterSpacing === 0) return base;
|
|
12
|
+
const corrected = getSegmentMetrics(text, measurement).width - base;
|
|
13
|
+
const context = getContext(measurement), previous = context.letterSpacing;
|
|
14
|
+
try {
|
|
15
|
+
context.letterSpacing = `${letterSpacing}px`;
|
|
16
|
+
if (Number.parseFloat(context.letterSpacing) !== letterSpacing) return null;
|
|
17
|
+
const width = context.measureText(text).width - corrected;
|
|
18
|
+
return Number.isFinite(width) ? width : null;
|
|
19
|
+
} finally {
|
|
20
|
+
context.letterSpacing = previous;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
8
24
|
const opening = /^[\p{Ps}\p{Pi}\p{Pf}"']+/u;
|
|
9
25
|
const closing = /[\p{Pe}\p{Pi}\p{Pf}"'.,;:!?]+$/u;
|
|
10
26
|
const empty = values => values == null || !values.some(value => value !== null && value !== 0);
|
|
@@ -141,7 +157,7 @@ export function* prepareParagraphCandidateSteps(items, options) {
|
|
|
141
157
|
if (joined) {
|
|
142
158
|
for (let at = word.first; at <= word.last; at++) {
|
|
143
159
|
const unit = units[at];
|
|
144
|
-
if (unit.kind !== TEXT
|
|
160
|
+
if (unit.kind !== TEXT) continue;
|
|
145
161
|
const delta = unit.width - measure(unit.start, unit.end);
|
|
146
162
|
if (delta !== 0) contexts.push(Object.freeze({start: unit.start, end: unit.end, delta}));
|
|
147
163
|
if (++measuredEdges % 8 === 0) yield;
|
|
@@ -152,6 +168,7 @@ export function* prepareParagraphCandidateSteps(items, options) {
|
|
|
152
168
|
for (const cut of word.breaks) {
|
|
153
169
|
if (cut.kind === 'soft' || cut.kind === 'auto') {
|
|
154
170
|
cut.glyph = '-';
|
|
171
|
+
if (options.hanging && !word.protected) cut.protrude = parseFontSize(items[units[word.first].itemIndex].font) * .25;
|
|
155
172
|
}
|
|
156
173
|
let before = cut.at, after = cut.at + 1;
|
|
157
174
|
if (cut.kind === 'auto') {
|
|
@@ -464,6 +464,7 @@ export function createTypography() {
|
|
|
464
464
|
function updateProjection(projection, plan) {
|
|
465
465
|
for (const {node,property,line,sign} of projection.bindings) {
|
|
466
466
|
const value = sign * plan.lines[line][property] + 'px';
|
|
467
|
+
if (node.hasAttribute(boundary)) { if (node.style.getPropertyValue('--md-fit-protrude') !== value) node.style.setProperty('--md-fit-protrude', value); continue; }
|
|
467
468
|
const name = property === 'opening' ? 'marginInlineStart' : property === 'closing' ? 'marginInlineEnd' : 'wordSpacing';
|
|
468
469
|
if (node.style[name] !== value) node.style[name] = value;
|
|
469
470
|
}
|
|
@@ -487,7 +488,7 @@ export function createTypography() {
|
|
|
487
488
|
add(line.end);
|
|
488
489
|
}
|
|
489
490
|
if (line.kind === 'space') operations.push({start:line.end,end:line.next,boundary:true});
|
|
490
|
-
else if (line.kind !== 'end') inserts.push({offset:line.end,glyph:line.glyph});
|
|
491
|
+
else if (line.kind !== 'end') inserts.push({offset:line.end,glyph:line.glyph,line:lineIndex,protrudes:line.closing>0});
|
|
491
492
|
const first = words[line.firstWord], last = words[line.lastWord];
|
|
492
493
|
if (first.opening && line.start === first.start) operations.push({start:first.start,end:first.openEnd,opening:true,line:lineIndex});
|
|
493
494
|
if (last.closing && line.end === last.end) operations.push({start:last.closeStart,end:last.end,closing:true,line:lineIndex});
|
|
@@ -538,6 +539,7 @@ export function createTypography() {
|
|
|
538
539
|
const at=ends.get(insert.offset);
|
|
539
540
|
const marker=paragraph.ownerDocument.createElement('span');marker.setAttribute(boundary,insert.glyph?'hyphen':'');
|
|
540
541
|
if (insert.glyph) marker.style.setProperty('--md-fit-hyphen',JSON.stringify(insert.glyph));
|
|
542
|
+
if (insert.protrudes) bindings.push({node:marker,property:'closing',line:insert.line,sign:-1});
|
|
541
543
|
at.after(marker);
|
|
542
544
|
}
|
|
543
545
|
const projection = {bindings,topology:topology(plan)};
|
|
@@ -689,77 +691,69 @@ export function createTypography() {
|
|
|
689
691
|
slotPlans.set(paragraph, {key, plan});
|
|
690
692
|
return plan;
|
|
691
693
|
}
|
|
692
|
-
function slotLines(paragraph, items, options, {width, top, obstacles, lineHeight, minWidth, slotsForBand, justify, hyphenate}) {
|
|
694
|
+
function slotLines(paragraph, items, options, {width, top, obstacles, lineHeight, minWidth, slotsForBand, slotSequence, columnsFilled, justify, hyphenate}) {
|
|
693
695
|
if (!obstacles.length) return null;
|
|
694
696
|
const doc = paragraph.ownerDocument, words = scope(doc);
|
|
695
697
|
const source = items.map(item => ({...item, hyphenate}));
|
|
696
698
|
const geometry = words.prepare(paragraph, source, {policy: 'paragraph', language: options.language, whiteSpace: 'normal',
|
|
697
699
|
wordBreak: options.wordBreak, hanging: false, paragraphBreaks: [0, source.length]});
|
|
698
700
|
if (!geometry) return null;
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
701
|
+
const build = order => {
|
|
702
|
+
const found = slotSequence(width, top, obstacles, lineHeight, minWidth, slotsForBand, 'ltr', order);
|
|
703
|
+
if (!found?.multiple) return null;
|
|
704
|
+
const sequence = found.slots, y = found.end;
|
|
705
|
+
const prepared = fitter.prepare(geometry);
|
|
706
|
+
let slots = sequence, plan = null;
|
|
707
|
+
for (let pass = 0; prepared && pass < 8; pass++) {
|
|
708
|
+
const measures = slots.map(slot => Math.floor(slot.width * 64) / 64);
|
|
709
|
+
if (!measures.length) return null;
|
|
710
|
+
plan = words.fit(paragraph, geometry, known => fitter.solveSteps(prepared, width, {widths: measures, shrink: justify ? undefined : 0}, known));
|
|
711
|
+
if (!plan) return null;
|
|
712
|
+
const wide = new Set();
|
|
713
|
+
plan.lines.forEach((line, index) => {
|
|
714
|
+
if (index >= slots.length || line.firstWord !== line.lastWord || line.kind === 'soft' || line.kind === 'auto') return;
|
|
715
|
+
const word = geometry.segments[line.segment].words[line.firstWord];
|
|
716
|
+
if (line.start === word.start && line.end === word.end && word.width > measures[index] + .5) wide.add(slots[index]);
|
|
717
|
+
});
|
|
718
|
+
if (!wide.size) break;
|
|
719
|
+
slots = slots.filter(slot => !wide.has(slot)); plan = null;
|
|
707
720
|
}
|
|
708
|
-
|
|
709
|
-
for (const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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;
|
|
721
|
+
const perRow = new Map();
|
|
722
|
+
for (const slot of slots) perRow.set(slot.y, (perRow.get(slot.y) || 0) + 1);
|
|
723
|
+
if (!plan || ![...perRow.values()].some(count => count > 1)) return null;
|
|
724
|
+
const lines = [], text = geometry.text, offsets = geometry.offsets, space = /^[\t\n\f\r ]*$/;
|
|
725
|
+
for (let index = 0; index < plan.lines.length; index++) {
|
|
726
|
+
const line = plan.lines[index], slot = sequence[index] || {x: 0, y: y + (index - sequence.length) * lineHeight, width};
|
|
727
|
+
const spacing = justify ? line.wordSpacing : 0, fragments = [];
|
|
728
|
+
const segment = geometry.segments[line.segment].words.slice(line.firstWord, line.lastWord + 1);
|
|
729
|
+
let at = 0, previous = null;
|
|
730
|
+
while (at < source.length - 1 && offsets[at + 1] <= line.start) at++;
|
|
731
|
+
for (; at < source.length && offsets[at] < line.end; at++) {
|
|
732
|
+
let from = Math.max(line.start, offsets[at]), to = Math.min(line.end, offsets[at + 1]);
|
|
733
|
+
if (previous) while (from < to && /[\t\n\f\r ]/.test(text[from])) from++;
|
|
734
|
+
if (offsets[at + 1] < line.end) while (to > from && /[\t\n\f\r ]/.test(text[to - 1])) to--;
|
|
735
|
+
if (to <= from) continue;
|
|
736
|
+
const fragment = {itemIndex: at, sourceStart: from - offsets[at], sourceEnd: to - offsets[at], derive: true, occupiedWidth: 0, gapItemIndex: at, gapBefore: 0};
|
|
737
|
+
if (previous) {
|
|
738
|
+
fragment.gapItemIndex = previous.itemIndex;
|
|
739
|
+
if (previous.end < from) {
|
|
740
|
+
if (!space.test(text.slice(previous.end, from))) return null;
|
|
741
|
+
const before = segment.find(word => word.end === previous.end);
|
|
742
|
+
if (!before?.gap) return null;
|
|
743
|
+
fragment.gapBefore = before.gap.width + spacing;
|
|
744
|
+
} else fragment.gapBefore = 0;
|
|
745
|
+
}
|
|
746
|
+
fragments.push(fragment); previous = {itemIndex: at, end: to};
|
|
754
747
|
}
|
|
755
|
-
fragments.
|
|
748
|
+
if (!fragments.length) return null;
|
|
749
|
+
if (line.kind === 'soft' || line.kind === 'auto') fragments.at(-1).glyph = line.glyph || '-';
|
|
750
|
+
lines.push({x: slot.x, y: slot.y, width: slot.width, fragments, wordSpacing: spacing});
|
|
756
751
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
return {lines, height: last ? Math.max(lineHeight, last.y + lineHeight) : lineHeight};
|
|
752
|
+
return {plan: {lines, height: Math.max(lineHeight, ...lines.map(line => line.y + lineHeight))}, filled: columnsFilled(slots, slots.slice(0, plan.lines.length))};
|
|
753
|
+
};
|
|
754
|
+
const columns = build('columns');
|
|
755
|
+
if (!columns || columns.filled) return columns?.plan || null;
|
|
756
|
+
return build('rows')?.plan || columns.plan;
|
|
763
757
|
}
|
|
764
758
|
|
|
765
759
|
function retire(root, {documentChanged=false}={}) {
|
|
@@ -211,6 +211,7 @@ export function documentFontUsage(markdown, parser, defaultFont) {
|
|
|
211
211
|
const inherited = defaultFont === undefined ? settings.defaultfont : documentFont(defaultFont);
|
|
212
212
|
const main = family(settings.mainfont || inherited || settings.sansfont), sans = family(settings.sansfont || settings.mainfont || inherited), mono = family(settings.monofont);
|
|
213
213
|
const add = (name, text) => { if (name && !Object.hasOwn(FONT_STACKS, name.toLowerCase()) && text) use.set(name, (use.get(name) || '') + text + '\n'); };
|
|
214
|
+
if (!main && !sans && !mono && !source.includes('<')) return use;
|
|
214
215
|
const plain = html => md.utils.unescapeAll(String(html).replace(/<!--[^]*?-->/g, '').replace(/<(script|style)\b[^>]*>[^]*?<\/\1\s*>/gi, '').replace(/<[^>]*>/g, '').replace(/\\/g, '\\\\'));
|
|
215
216
|
let blockFamily = main;
|
|
216
217
|
for (const token of md.parse(splitOpeningFrontmatter(source).body, {})) {
|
|
@@ -129,6 +129,7 @@ function paperOf(value) {
|
|
|
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
131
|
export const pageSizeOf = settings => settings?.papersize || PAPERS.letter;
|
|
132
|
+
export const paperForLanguage = language => /(?:^|[-_])(?:US|CA|MX|PH|CL|CO|VE|CR|GT)$/i.test(String(language || '')) ? PAPERS.letter : PAPERS.a4;
|
|
132
133
|
export const PAGE_SIZES = Object.freeze(['letter', 'legal', 'tabloid', 'a3', 'a4', 'a5', '1080x1080px', '1080x1350px', '1920x1080px', '1200x630px']);
|
|
133
134
|
const EMPTY = Object.freeze({
|
|
134
135
|
title: null, subtitle: null, lang: null, fontsize: null, mainfont: null, sansfont: null, monofont: null, defaultfont: null, linestretch: null,
|
|
@@ -425,7 +425,7 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
425
425
|
const name = attr.name.toLowerCase(), value = attr.value;
|
|
426
426
|
if (name.startsWith('on') || name.startsWith('data-rapier-') || ['nonce','srcdoc','is','autofocus','form','formaction','action','target','download','ping','xmlns:xlink'].includes(name)) el.removeAttribute(attr.name);
|
|
427
427
|
if (['src','srcset','href'].includes(attr.localName.toLowerCase()) && !(name.endsWith('href') && /^#[A-Za-z][\w.-]*$/.test(value) ||
|
|
428
|
-
(name === 'src' && tag === 'img' || name.endsWith('href') && tag === 'image') && /^data:image\/(?:png|jpeg|webp|gif|jxl|svg\+xml)
|
|
428
|
+
(name === 'src' && tag === 'img' || name.endsWith('href') && tag === 'image') && /^data:image\/(?:(?:png|jpeg|webp|gif|jxl|svg\+xml);|svg\+xml,)/i.test(value))) el.removeAttribute(attr.name);
|
|
429
429
|
}
|
|
430
430
|
if (tag.startsWith('animate') || tag === 'set') {
|
|
431
431
|
const attribute = (el.getAttribute('attributeName') || '').trim().toLowerCase().split(':').pop();
|
|
@@ -437,13 +437,16 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
437
437
|
const name = tag === 'img' ? 'src' : el.hasAttribute('href') ? 'href' : 'xlink:href', original = el.getAttribute(name);
|
|
438
438
|
if (original) {
|
|
439
439
|
const match = /^data:(image\/(?:png|jpeg|webp|gif|jxl|svg\+xml));(?:charset=[^;,]+;)?base64,([A-Za-z0-9+/]+={0,2})$/i.exec(original);
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
const
|
|
444
|
-
|
|
440
|
+
const percent = /^data:image\/svg\+xml,/i.test(original);
|
|
441
|
+
if ((!match && !percent) || original.length > 24 * 1024 * 1024) throw new Error('Image requires a bounded base64 data URL');
|
|
442
|
+
const mime = percent ? 'image/svg+xml' : match[1].toLowerCase();
|
|
443
|
+
const url = percent ? original : 'data:' + mime + ';base64,' + match[2];
|
|
444
|
+
const bytes = percent ? new win.TextEncoder().encode(decodeURIComponent(original.slice(19)))
|
|
445
|
+
: Uint8Array.from(win.atob(match[2]), c => c.charCodeAt(0));
|
|
446
|
+
const info = mime === 'image/gif' ? media.inspectGIF(bytes) : imageInfo(url) || admitted[url];
|
|
447
|
+
if (!info && mime !== 'image/svg+xml') {el.removeAttribute(name); continue;}
|
|
445
448
|
if (info) {pixels += info.width * info.height; if (pixels > 24000000) throw new Error('HTML image pixel budget'); admitted[url] = {width: info.width, height: info.height, animated: !!info.animated};}
|
|
446
|
-
if (
|
|
449
|
+
if (mime === 'image/svg+xml') {
|
|
447
450
|
const svgText = new win.TextDecoder('utf-8', {fatal: true}).decode(bytes);
|
|
448
451
|
if (/<!\s*(?:DOCTYPE|ENTITY)\b/i.test(svgText)) throw new Error('SVG declarations are unsupported');
|
|
449
452
|
const parsed = new win.DOMParser().parseFromString(svgText, 'image/svg+xml');
|