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/spec/md-assets.mjs
CHANGED
|
@@ -17,9 +17,9 @@ export function installMarkdownImages(md) {
|
|
|
17
17
|
if (md.__rapierImagesInstalled) return md;
|
|
18
18
|
md.__rapierImagesInstalled = true;
|
|
19
19
|
const validate = md.validateLink;
|
|
20
|
-
md.validateLink = url =>
|
|
20
|
+
md.validateLink = url => !!dataImage(url) || validate(url);
|
|
21
21
|
const normalize = md.normalizeLink;
|
|
22
|
-
md.normalizeLink = url =>
|
|
22
|
+
md.normalizeLink = url => dataImage(url) ? url : normalize(url);
|
|
23
23
|
const reference = md.block.ruler.__rules__.find(row => row.name === 'reference').fn;
|
|
24
24
|
md.block.ruler.at('reference', (state, start, end, silent) => {
|
|
25
25
|
if (silent) return reference(state, start, end, silent);
|
|
@@ -179,10 +179,35 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
const PERCENT_PAYLOAD = Array.from({length: 128}, (_, code) => code >= 48 && code <= 57 || code >= 65 && code <= 90 ||
|
|
183
|
+
code >= 97 && code <= 122 || '-._~:/?@+$,;='.includes(String.fromCharCode(code)));
|
|
184
|
+
const isHexUnit = code => code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102;
|
|
185
|
+
let lastScan = null;
|
|
182
186
|
export function dataImage(url) {
|
|
183
187
|
if (typeof url !== 'string' || url.length > IMAGE_LIMITS.sourceChars) return null;
|
|
188
|
+
if (url.length > 4096) {
|
|
189
|
+
if (lastScan?.url !== url) lastScan = {url, image: scanDataImage(url)};
|
|
190
|
+
return lastScan.image && {...lastScan.image};
|
|
191
|
+
}
|
|
192
|
+
return scanDataImage(url);
|
|
193
|
+
}
|
|
194
|
+
function scanDataImage(url) {
|
|
184
195
|
const prepared = preparedDefinitions.find(row => row.image.url === url)?.image;
|
|
185
|
-
if (prepared) return {codec: prepared.codec, byteLength: prepared.byteLength, payloadStart: url.indexOf(',') + 1
|
|
196
|
+
if (prepared) return {codec: prepared.codec, byteLength: prepared.byteLength, payloadStart: url.indexOf(',') + 1,
|
|
197
|
+
...(prepared.encoding ? {encoding: prepared.encoding} : {})};
|
|
198
|
+
if (/^data:image\/svg\+xml,/i.test(url)) {
|
|
199
|
+
const payloadStart = 19;
|
|
200
|
+
let byteLength = 0;
|
|
201
|
+
for (let at = payloadStart; at < url.length; at++, byteLength++) {
|
|
202
|
+
const code = url.charCodeAt(at);
|
|
203
|
+
if (code === 37) {
|
|
204
|
+
if (!isHexUnit(url.charCodeAt(at + 1)) || !isHexUnit(url.charCodeAt(at + 2))) return null;
|
|
205
|
+
at += 2;
|
|
206
|
+
} else if (!(code < 128 && PERCENT_PAYLOAD[code])) return null;
|
|
207
|
+
}
|
|
208
|
+
return byteLength && byteLength <= IMAGE_LIMITS.bytes
|
|
209
|
+
? {codec: 'image/svg+xml', byteLength, payloadStart, encoding: 'percent'} : null;
|
|
210
|
+
}
|
|
186
211
|
const match = /^data:(image\/(?:jxl|png|jpeg|webp|gif|svg\+xml));base64,([A-Za-z0-9+/]+={0,2})$/i.exec(url);
|
|
187
212
|
if (!match || match[2].length % 4) return null;
|
|
188
213
|
const padding = match[2].endsWith('==') ? 2 : match[2].endsWith('=') ? 1 : 0;
|
|
@@ -291,7 +316,8 @@ export function restoreAssetIndex(source, packet, {offset = 0, bodyOffset = 0, f
|
|
|
291
316
|
preparedDefinitions = blocks.filter(row => row.topLevel).map(row => {
|
|
292
317
|
const local = {...row, start: 0, end: row.source.length, active: true, status: 'unverified'};
|
|
293
318
|
for (const key of positions.slice(2)) if (Number.isSafeInteger(local[key])) local[key] -= row.start;
|
|
294
|
-
const image = Object.freeze({url: row.url, codec: row.codec, byteLength: row.byteLength
|
|
319
|
+
const image = Object.freeze({url: row.url, codec: row.codec, byteLength: row.byteLength,
|
|
320
|
+
...(row.encoding ? {encoding: row.encoding} : {})});
|
|
295
321
|
return {source: row.source, factory, image, index: {assets: new Map([[row.id, local]]), blocks: [local],
|
|
296
322
|
references: Object.assign(Object.create(null), {[row.id]: {href: row.url, title: row.title}}),
|
|
297
323
|
duplicateLabels: new Set(), appendixStart: 0}};
|
|
@@ -314,7 +340,7 @@ export function projectImageDefinitions(source, index) {
|
|
|
314
340
|
const references = Object.create(null);
|
|
315
341
|
for (const [label, definition] of Object.entries(index.references)) {
|
|
316
342
|
const image = index.assets.get(label);
|
|
317
|
-
references[label] = {href: image?.url === definition.href ? '
|
|
343
|
+
references[label] = {href: image?.url === definition.href ? image.url.slice(0, image.url.indexOf(',') + 1) + 'AA==' : definition.href,
|
|
318
344
|
title: definition.title};
|
|
319
345
|
}
|
|
320
346
|
return {source: records.length ? parts.join('') : source, records, references};
|
|
@@ -513,12 +539,17 @@ export function retireDeletedImageDefinitions(before, after, splices, factory) {
|
|
|
513
539
|
!remaining.includes('[' + row.id) && !now.html.includes(row.id) && !now.html.includes(row.url.toUpperCase());
|
|
514
540
|
});
|
|
515
541
|
return retired.sort((a, b) => b.start - a.start).map(row => {
|
|
516
|
-
const gap = /(?:\r\n
|
|
542
|
+
const gap = /(?:\r\n|\r(?!\n)|(?<!\r)\n){2}$/.exec(after.slice(Math.max(0, row.start - 5), row.start));
|
|
517
543
|
let tail = after.slice(row.end);
|
|
518
544
|
const last = /^(?:\r\n|\n|\r)?$/.test(tail);
|
|
519
545
|
for (const other of retired) if (other.start >= row.end) tail = tail.replace(other.source, '');
|
|
520
546
|
const whole = gap && !/\S/.test(tail) && row.start - gap[0].length > markdownBodyOffset(after);
|
|
521
|
-
|
|
547
|
+
if (whole) {
|
|
548
|
+
const [first, second] = gap[0].match(/\r\n|\r|\n/g), padding = first === second ? gap[0] : second;
|
|
549
|
+
return {pos: row.start - padding.length, removed: padding + row.source + (last ? after.slice(row.end) : ''), inserted: ''};
|
|
550
|
+
}
|
|
551
|
+
const between = gap && /^(?:\r\n|\r(?!\n)|(?<!\r)\n){2}/.test(tail) && row.start - gap[0].length >= markdownBodyOffset(after);
|
|
552
|
+
return between ? {pos: row.start - gap[0].length, removed: gap[0] + row.source, inserted: ''} : {pos: row.start, removed: row.source, inserted: ''};
|
|
522
553
|
});
|
|
523
554
|
} catch (_) { return []; }
|
|
524
555
|
}
|
|
@@ -551,7 +582,7 @@ function previousOmissions(view, kind) {
|
|
|
551
582
|
rows = rows.flatMap(row => {
|
|
552
583
|
const permitted = kind === 'asset' ? /^[A-Za-z0-9+/=\\;&%#._~-]*$/
|
|
553
584
|
: row.profile === 'base64' ? /^[A-Za-z0-9+/=\s]*$/ : /^[A-Za-z0-9\-._~!$&*+;=:@/?%]*$/;
|
|
554
|
-
if (change.pos >= row.payloadStart && end <= row.end && permitted.test(change.text)) {
|
|
585
|
+
if (!row.percent && change.pos >= row.payloadStart && end <= row.end && permitted.test(change.text)) {
|
|
555
586
|
const next = {...row, end: row.end + delta, chars: row.chars + delta};
|
|
556
587
|
if (kind === 'asset') {
|
|
557
588
|
const was = payloadFacts(change.before.deref().slice(change.pos, end)), now = payloadFacts(change.text);
|
|
@@ -575,6 +606,7 @@ function dataMatches(source, kind, envelopes = []) {
|
|
|
575
606
|
const prior = previousOmissions(view, kind), rows = new Map();
|
|
576
607
|
const pattern = kind === 'asset' ? /data:(image\/[a-z0-9.+-]+);base64,[A-Za-z0-9+/=\\;&%#._~-]*/iy
|
|
577
608
|
: /data:([a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+)(?:;[^,\s"'<>)]*)?,/iy;
|
|
609
|
+
const svgPattern = /data:(image\/svg\+xml),[A-Za-z0-9%\-._~:/?@+$,;=]*/iy;
|
|
578
610
|
let end = 0, envelopeIndex = 0;
|
|
579
611
|
for (const at of measured.markers) {
|
|
580
612
|
if (kind === 'asset' && at < end) continue;
|
|
@@ -583,15 +615,21 @@ function dataMatches(source, kind, envelopes = []) {
|
|
|
583
615
|
let row = prior.get(at);
|
|
584
616
|
if (!row) {
|
|
585
617
|
pattern.lastIndex = at;
|
|
586
|
-
|
|
618
|
+
let match = pattern.exec(source), percent = false;
|
|
619
|
+
if (!match && kind === 'asset') {
|
|
620
|
+
svgPattern.lastIndex = at; match = svgPattern.exec(source);
|
|
621
|
+
if (match && !dataImage(match[0])) match = null;
|
|
622
|
+
percent = !!match;
|
|
623
|
+
if (percent) pattern.lastIndex = svgPattern.lastIndex;
|
|
624
|
+
}
|
|
587
625
|
if (!match) continue;
|
|
588
626
|
if (kind === 'asset') {
|
|
589
627
|
const value = match[0], payload = value.slice(value.indexOf(',') + 1);
|
|
590
628
|
const {bad, equals} = payloadFacts(payload);
|
|
591
|
-
const bytes = payloadBytes(payload.length, bad, equals, equals <= 2 ? payload.slice(payload.length - equals) : '');
|
|
629
|
+
const bytes = percent ? dataImage(value)?.byteLength ?? null : payloadBytes(payload.length, bad, equals, equals <= 2 ? payload.slice(payload.length - equals) : '');
|
|
592
630
|
row = {start: at, end: pattern.lastIndex, chars: value.length,
|
|
593
631
|
domain: 'image_bytes', reason: 'embedded_asset', profile: 'embedded', type: match[1].toLowerCase(), bytes,
|
|
594
|
-
payloadStart: pattern.lastIndex - payload.length, bad, equals};
|
|
632
|
+
payloadStart: pattern.lastIndex - payload.length, bad, equals, ...(percent ? {percent} : {})};
|
|
595
633
|
} else {
|
|
596
634
|
const payloadStart = pattern.lastIndex, base64 = /;base64,/i.test(match[0]);
|
|
597
635
|
let to = payloadStart;
|
|
@@ -608,7 +646,7 @@ function dataMatches(source, kind, envelopes = []) {
|
|
|
608
646
|
}
|
|
609
647
|
export function assetOmissions(source) {
|
|
610
648
|
if (typeof source !== 'string') return [];
|
|
611
|
-
return [...dataMatches(source, 'asset').values()].map(({payloadStart, bad, equals, ...row}) => row);
|
|
649
|
+
return [...dataMatches(source, 'asset').values()].map(({payloadStart, bad, equals, percent, ...row}) => row);
|
|
612
650
|
}
|
|
613
651
|
|
|
614
652
|
export function dataOmissions(source) {
|
|
@@ -667,3 +705,5 @@ export function appendAssetText(source, asset, lineEnding, {preserveLabel = fals
|
|
|
667
705
|
return {source: nextSource, id: asset.id, reference: asset.label, added: true, suffix};
|
|
668
706
|
}
|
|
669
707
|
export async function appendAsset(source, asset) { return appendAssetText(source, asset); }
|
|
708
|
+
|
|
709
|
+
export const imageOnlySourceBlock = source => /^(?:!\[[^\]\n]*\]\(data:image\/(?:(?:png|jpeg|webp|gif|jxl|svg\+xml);base64,[A-Za-z0-9+\/=]+|svg\+xml,[A-Za-z0-9%\-._~:/?@+$,;=]+)(?: "[^"\n]*")?\)(?:[ \t]*<!--md-layout:v1 [^>\r\n]{1,180}-->)?\s*)+$/.test(source);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-markdown-kit",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.40",
|
|
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
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
.md-render p[data-rapier-typography-fit]{text-align:justify!important;text-align-last:start;text-wrap:nowrap;hyphens:none;hanging-punctuation:none}
|
|
143
143
|
.md-render p[data-rapier-typography-fit] :is(a,span,em,i,strong,b,del,s,mark,code,kbd,samp,u,ins,abbr){text-wrap:nowrap;hyphens:none;hanging-punctuation:none}
|
|
144
144
|
.md-render [data-rapier-typography-separator]{font-size:0;line-height:0;word-spacing:0!important;letter-spacing:0!important}
|
|
145
|
-
.md-render [data-rapier-typography-break="hyphen"]::before{content:var(--md-fit-hyphen,'-')}
|
|
145
|
+
.md-render [data-rapier-typography-break="hyphen"]::before{content:var(--md-fit-hyphen,'-');margin-inline-end:var(--md-fit-protrude,0px)}
|
|
146
146
|
.md-render p[data-rapier-typography-ragged]{text-align:start!important}
|
|
147
147
|
.md-render p[data-md-lines]:not([data-rapier-typography-protected-initial])::first-letter{
|
|
148
148
|
initial-letter:var(--md-drop-lines,normal);
|
|
@@ -219,29 +219,31 @@
|
|
|
219
219
|
at admission; only renderer paint omissions are corrected here. */
|
|
220
220
|
:where(.md-render) svg.rapier-native-flowchart{display:block;margin-inline:auto;height:auto}
|
|
221
221
|
:where(.md-render) svg.rapier-diagram{display:block;margin-inline:auto;height:auto;text-rendering:optimizeLegibility}
|
|
222
|
-
/* The same token bridge serves the document, live preview and portable page.
|
|
222
|
+
/* The same token bridge serves the document, live preview and portable page. Pele reads --pele-* names; the
|
|
223
|
+
adapter (_presentPele in shell/plugin-loader.js) points them at these --md-pele-* names, and the app's own sheet
|
|
224
|
+
lets its theme (--color-*, --font-*) lead them in the editor. */
|
|
223
225
|
:where(.md-render) :is(.diagram-block,.diagram-live-preview){
|
|
224
|
-
--pele-bg:transparent;
|
|
225
|
-
--pele-fg:var(--
|
|
226
|
-
--pele-line:var(--
|
|
227
|
-
--pele-muted:var(--
|
|
228
|
-
--pele-surface:var(--
|
|
229
|
-
--pele-surface-alt:var(--
|
|
230
|
-
--pele-border:var(--
|
|
231
|
-
--pele-accent:var(--
|
|
232
|
-
--pele-font:var(--
|
|
233
|
-
--pele-font-mono:var(--
|
|
234
|
-
--pele-radius:0;
|
|
235
|
-
--pele-heading-weight:var(--md-fw-semibold);
|
|
236
|
-
--pele-title-weight:var(--md-fw-semibold);
|
|
237
|
-
--pele-series-1:var(--pele-accent);
|
|
238
|
-
--pele-series-2:#4f91f7;
|
|
239
|
-
--pele-series-3:#35c57a;
|
|
240
|
-
--pele-series-4:#e6b91e;
|
|
241
|
-
--pele-series-5:#9b6dea;
|
|
242
|
-
--pele-series-6:#f05a62;
|
|
243
|
-
--pele-series-7:color-mix(in srgb,var(--pele-accent) 50%,#4f91f7);
|
|
244
|
-
--pele-series-8:color-mix(in srgb,#9b6dea 50%,#f05a62);
|
|
226
|
+
--md-pele-bg:transparent;
|
|
227
|
+
--md-pele-fg:var(--md-color-text);
|
|
228
|
+
--md-pele-line:var(--md-color-text);
|
|
229
|
+
--md-pele-muted:var(--md-color-text-secondary);
|
|
230
|
+
--md-pele-surface:var(--md-color-surface-2);
|
|
231
|
+
--md-pele-surface-alt:var(--md-color-border-focus);
|
|
232
|
+
--md-pele-border:var(--md-color-border);
|
|
233
|
+
--md-pele-accent:var(--md-color-accent);
|
|
234
|
+
--md-pele-font:var(--md-font-sans);
|
|
235
|
+
--md-pele-font-mono:var(--md-font-mono);
|
|
236
|
+
--md-pele-radius:0;
|
|
237
|
+
--md-pele-heading-weight:var(--md-fw-semibold);
|
|
238
|
+
--md-pele-title-weight:var(--md-fw-semibold);
|
|
239
|
+
--md-pele-series-1:var(--md-pele-accent);
|
|
240
|
+
--md-pele-series-2:#4f91f7;
|
|
241
|
+
--md-pele-series-3:#35c57a;
|
|
242
|
+
--md-pele-series-4:#e6b91e;
|
|
243
|
+
--md-pele-series-5:#9b6dea;
|
|
244
|
+
--md-pele-series-6:#f05a62;
|
|
245
|
+
--md-pele-series-7:color-mix(in srgb,var(--md-pele-accent) 50%,#4f91f7);
|
|
246
|
+
--md-pele-series-8:color-mix(in srgb,#9b6dea 50%,#f05a62);
|
|
245
247
|
}
|
|
246
248
|
|
|
247
249
|
/* Radar gives these SVG labels a color declaration; SVG text needs a fill. */
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/*! Pretext Unreleased (bac1774c0) | MIT; BSD/Unicode/ICU notices in LICENSE | Copyright (c) 2026 Pretext contributors */
|
|
2
|
-
// Fresh-line geometry, or entry geometry: the widths a line takes where it starts inside a
|
|
3
|
-
// segment cut between graphemes, at or inside a stretch of it that holds a default-ignorable code
|
|
4
|
-
// point (a ZWNJ, a word joiner, a bidi mark). Shaping looks past such a character, so the text
|
|
5
|
-
// after the cut, shaped afresh at the line's start, can measure otherwise than the segment's own
|
|
6
|
-
// advances give (RESEARCH.md, Widths After A Line Break). For each such start, preparation
|
|
7
|
-
// measures the text from the start through the first whole grapheme past that stretch, the
|
|
8
|
-
// anchor, in at most three prefixes (the entry's head); past the anchor the widths go on by the
|
|
9
|
-
// segment's advances. The walkers (src/line-break.ts) ask an entry whether the segment's whole
|
|
10
|
-
// tail fits the line (admissionFit), which Blink reads from the tail as measured afresh and Gecko
|
|
11
|
-
// from the whole segment's width less the prefixes already placed (the profile's entryFitBasis),
|
|
12
|
-
// and, where it doesn't fit, how many graphemes do by the fresh prefixes. Only the desktop Blink
|
|
13
|
-
// and Gecko profiles observe entries, the browsers they were checked in, and a segment of more
|
|
14
|
-
// than 96 graphemes takes none (RESEARCH.md, Keeping Work Bounded).
|
|
15
|
-
import { findGraphemeEnds } from './graphemes.js'
|
|
16
|
-
import { getEngineProfile } from './measurement.js'
|
|
17
|
-
|
|
18
|
-
const defaultIgnorable = /\p{Default_Ignorable_Code_Point}/u
|
|
19
|
-
const MAX_GRAPHEMES = 96
|
|
20
|
-
const MAX_HEAD_ENDPOINTS = 3
|
|
21
|
-
|
|
22
|
-
// Whether a text holds a default-ignorable code point, without which no segment of it has entry
|
|
23
|
-
// geometry (observeSegmentEntries).
|
|
24
|
-
export function textMayHaveEntryGeometry(text ) {
|
|
25
|
-
return defaultIgnorable.test(text)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// The stretches of a segment that a default-ignorable affects, as intervals of the source text.
|
|
37
|
-
// The text without its default-ignorables (the projection) is cut into grapheme clusters, and the
|
|
38
|
-
// segment's own graphemes that one such cluster spans are joined into one interval. An interval
|
|
39
|
-
// is observed where it holds a default-ignorable, and leading default-ignorables join the first
|
|
40
|
-
// visible interval. The projection only picks the intervals: what is measured is the source text,
|
|
41
|
-
// and the segment's grapheme boundaries and breaks stay its own.
|
|
42
|
-
function affectedIntervals(text , endpoints ) {
|
|
43
|
-
const spans = []
|
|
44
|
-
const projected = []
|
|
45
|
-
const visibleRuns = /[^\p{Default_Ignorable_Code_Point}]+/gu
|
|
46
|
-
for (let match; (match = visibleRuns.exec(text)) !== null;) {
|
|
47
|
-
spans.push({ start: match.index, end: match.index + match[0].length })
|
|
48
|
-
projected.push(match[0])
|
|
49
|
-
}
|
|
50
|
-
// Map only the projected grapheme endpoints through retained source views.
|
|
51
|
-
// The last code unit maps before an ignored gap; the next start maps after it.
|
|
52
|
-
let spanIndex = 0
|
|
53
|
-
let projectedStart = 0
|
|
54
|
-
function sourceOffset(offset ) {
|
|
55
|
-
let span = spans[spanIndex]
|
|
56
|
-
while (offset >= projectedStart + span.end - span.start) {
|
|
57
|
-
projectedStart += span.end - span.start
|
|
58
|
-
span = spans[++spanIndex]
|
|
59
|
-
}
|
|
60
|
-
return span.start + offset - projectedStart
|
|
61
|
-
}
|
|
62
|
-
const joined = new Uint8Array(endpoints.length)
|
|
63
|
-
const visible = projected.join('')
|
|
64
|
-
const ends = new Int32Array(visible.length)
|
|
65
|
-
const count = findGraphemeEnds(getEngineProfile().graphemeTable, visible, 0, visible.length, ends)
|
|
66
|
-
let boundaryIndex = 1
|
|
67
|
-
for (let i = 0, projectedEnd = 0; i < count; i++) {
|
|
68
|
-
const start = sourceOffset(projectedEnd)
|
|
69
|
-
projectedEnd = ends[i]
|
|
70
|
-
const end = sourceOffset(projectedEnd - 1) + 1
|
|
71
|
-
while (boundaryIndex < endpoints.length && endpoints[boundaryIndex] <= start) boundaryIndex++
|
|
72
|
-
while (boundaryIndex < endpoints.length && endpoints[boundaryIndex] < end) joined[boundaryIndex++] = 1
|
|
73
|
-
}
|
|
74
|
-
const parts = []
|
|
75
|
-
let start = 0
|
|
76
|
-
for (let index = 1; index < endpoints.length; index++) {
|
|
77
|
-
if (joined[index] === 1) continue
|
|
78
|
-
const end = endpoints[index]
|
|
79
|
-
parts.push({ start, end, observe: defaultIgnorable.test(text.slice(start, end)) })
|
|
80
|
-
start = end
|
|
81
|
-
}
|
|
82
|
-
const leadingEnd = spans.length === 0 ? text.length : spans[0] .start
|
|
83
|
-
if (leadingEnd > 0 && leadingEnd < text.length) {
|
|
84
|
-
let firstVisiblePart = 0
|
|
85
|
-
while (parts[firstVisiblePart] .end <= leadingEnd) firstVisiblePart++
|
|
86
|
-
parts.splice(0, firstVisiblePart + 1, { start: 0, end: parts[firstVisiblePart] .end, observe: true })
|
|
87
|
-
}
|
|
88
|
-
return parts
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// At most 96 original graphemes and three exact head queries per covered entry.
|
|
92
|
-
// Each source grapheme enters at most 3 + 2 + 1 measured prefixes across all
|
|
93
|
-
// starts, so submitted UTF-16 stays at most six times the source length.
|
|
94
|
-
// Preparation retains only numeric geometry for subsequent line walking.
|
|
95
|
-
export function observeSegmentEntries(
|
|
96
|
-
text ,
|
|
97
|
-
advances ,
|
|
98
|
-
letterSpacing ,
|
|
99
|
-
wholeWidth ,
|
|
100
|
-
fitBasis ,
|
|
101
|
-
measure ,
|
|
102
|
-
) {
|
|
103
|
-
if (!defaultIgnorable.test(text) || advances.length > MAX_GRAPHEMES) return null
|
|
104
|
-
const ends = new Int32Array(text.length)
|
|
105
|
-
const count = findGraphemeEnds(getEngineProfile().graphemeTable, text, 0, text.length, ends)
|
|
106
|
-
if (count > MAX_GRAPHEMES) return null
|
|
107
|
-
const endpoints = [0]
|
|
108
|
-
for (let i = 0; i < count; i++) endpoints.push(ends[i] )
|
|
109
|
-
if (endpoints.length !== advances.length + 1) throw new Error('Entry and release grapheme cursors differ')
|
|
110
|
-
const parts = affectedIntervals(text, endpoints)
|
|
111
|
-
const terminalPrefixes = [0]
|
|
112
|
-
for (const advance of advances) terminalPrefixes.push(terminalPrefixes[terminalPrefixes.length - 1] + advance + letterSpacing)
|
|
113
|
-
const entries = Array.from({ length: advances.length }, () => null)
|
|
114
|
-
let partIndex = 0
|
|
115
|
-
let hasEntries = false
|
|
116
|
-
for (let start = 1; start < advances.length; start++) {
|
|
117
|
-
const sourceStart = endpoints[start]
|
|
118
|
-
while (partIndex < parts.length && parts[partIndex] .end <= sourceStart) partIndex++
|
|
119
|
-
const part = parts[partIndex]
|
|
120
|
-
// A start is observed inside an affected interval. The anchor, the first whole grapheme
|
|
121
|
-
// past that interval, is measured after it as its right context, and a line that starts
|
|
122
|
-
// at the anchor takes an entry only where the anchor's own interval is affected.
|
|
123
|
-
if (part === undefined || !part.observe || sourceStart < part.start) continue
|
|
124
|
-
let anchor = start + 1
|
|
125
|
-
while (anchor < advances.length && endpoints[anchor] <= part.end) anchor++
|
|
126
|
-
// An entry needs that whole grapheme. An interval that ends the segment has none, so
|
|
127
|
-
// its starts take no entry: what a line start measures with nothing after it hasn't
|
|
128
|
-
// been checked against a browser. Nor does a start whose head would take more than
|
|
129
|
-
// three prefixes.
|
|
130
|
-
if (endpoints[anchor] <= part.end || anchor - start > MAX_HEAD_ENDPOINTS) continue
|
|
131
|
-
const head = []
|
|
132
|
-
let complete = true
|
|
133
|
-
for (let end = start + 1; end <= anchor; end++) {
|
|
134
|
-
const query = text.slice(sourceStart, endpoints[end])
|
|
135
|
-
const width = measure(query)
|
|
136
|
-
if (width === null || !Number.isFinite(width)) { complete = false; break }
|
|
137
|
-
head.push(width)
|
|
138
|
-
}
|
|
139
|
-
if (!complete) continue
|
|
140
|
-
let admissionFit
|
|
141
|
-
switch (fitBasis) {
|
|
142
|
-
case 'fresh':
|
|
143
|
-
admissionFit = head[head.length - 1] +
|
|
144
|
-
(terminalPrefixes[advances.length] - terminalPrefixes[anchor] )
|
|
145
|
-
break
|
|
146
|
-
case 'original':
|
|
147
|
-
admissionFit = wholeWidth + letterSpacing - terminalPrefixes[start]
|
|
148
|
-
break
|
|
149
|
-
}
|
|
150
|
-
entries[start] = { head, admissionFit }
|
|
151
|
-
hasEntries = true
|
|
152
|
-
}
|
|
153
|
-
return hasEntries ? { terminalPrefixes, entries } : null
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Where a fresh line that starts at grapheme `start` of a segment ends, by the entry observed
|
|
157
|
-
// there: past the segment's end (end + 1) where the whole tail is admitted and the line goes on
|
|
158
|
-
// with the text after it, else after the last fresh prefix that fits, keeping the first grapheme.
|
|
159
|
-
// `end` then means the prefixes ran out, and the line ends with the segment.
|
|
160
|
-
export function getFreshLineEnd(geometry , start , end , fitLimit ) {
|
|
161
|
-
if (geometry.entries[start] .admissionFit <= fitLimit) return end + 1
|
|
162
|
-
let g = start + 1
|
|
163
|
-
while (g < end && getSegmentEntryWidth(geometry, start, g + 1) <= fitLimit) g++
|
|
164
|
-
return g
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// The width of a segment's graphemes from `start` to `end` on a fresh line that starts at
|
|
168
|
-
// `start`, with the letter spacing after the last of them. Null where no entry was observed,
|
|
169
|
-
// as at the segment's own start; an observed 0 is a width, and the prefixes needn't grow.
|
|
170
|
-
export function getSegmentEntryWidth(
|
|
171
|
-
geometry ,
|
|
172
|
-
start ,
|
|
173
|
-
end ,
|
|
174
|
-
) {
|
|
175
|
-
if (geometry === null) return null
|
|
176
|
-
const entry = geometry.entries[start]
|
|
177
|
-
if (entry === null) return null
|
|
178
|
-
if (end === start) return 0
|
|
179
|
-
const anchor = start + entry.head.length
|
|
180
|
-
if (end <= anchor) return entry.head[end - start - 1]
|
|
181
|
-
return entry.head[entry.head.length - 1] +
|
|
182
|
-
(geometry.terminalPrefixes[end] - geometry.terminalPrefixes[anchor] )
|
|
183
|
-
}
|