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.
Files changed (45) hide show
  1. package/README.md +15 -9
  2. package/dist/agent/vendor/pretext/NOTICE +2 -2
  3. package/dist/agent/vendor/pretext/SOURCE.json +62 -78
  4. package/dist/agent/vendor/pretext/analysis.js +24 -6
  5. package/dist/agent/vendor/pretext/gecko-line-breaks.js +1 -1
  6. package/dist/agent/vendor/pretext/generated/engine-break-data.js +1 -1
  7. package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +1 -1
  8. package/dist/agent/vendor/pretext/graphemes.js +1 -1
  9. package/dist/agent/vendor/pretext/han-kerning.js +1 -1
  10. package/dist/agent/vendor/pretext/layout.js +14 -11
  11. package/dist/agent/vendor/pretext/line-break.js +170 -195
  12. package/dist/agent/vendor/pretext/line-breaks.js +1 -7
  13. package/dist/agent/vendor/pretext/line-text.js +2 -2
  14. package/dist/agent/vendor/pretext/measurement.js +454 -113
  15. package/dist/agent/vendor/pretext/prepare.js +164 -244
  16. package/dist/agent/vendor/pretext/rich-inline.js +57 -37
  17. package/dist/agent/will.mjs +73 -0
  18. package/dist/kit/ledger/data.mjs +2 -2
  19. package/dist/kit/ledger/format.mjs +86 -42
  20. package/dist/kit/ledger/hash.mjs +43 -2
  21. package/dist/kit/ledger/history.mjs +6 -0
  22. package/dist/kit/ledger/inverse-source.mjs +6 -2
  23. package/dist/kit/ledger/journal-records.mjs +4 -2
  24. package/dist/kit/ledger/merge.mjs +3 -17
  25. package/dist/kit/ledger/text-merge.mjs +28 -2
  26. package/dist/kit/rapier-ledger.mjs +2 -2
  27. package/dist/kit/render-markdown.mjs +133 -10
  28. package/dist/kit/render-print.mjs +3 -3
  29. package/dist/kit/render-sanitize.mjs +66 -23
  30. package/dist/kit/render.mjs +10 -10
  31. package/dist/kit/wiki.mjs +34 -0
  32. package/dist/kit/will.d.mts +3 -0
  33. package/dist/layout/hyphenation.mjs +6 -4
  34. package/dist/layout/line-plan.mjs +96 -0
  35. package/dist/layout/model.mjs +2 -1
  36. package/dist/layout/paragraph-fit.mjs +4 -4
  37. package/dist/layout/prepared-paragraph.mjs +19 -2
  38. package/dist/layout/typography.mjs +57 -63
  39. package/dist/spec/document-fonts.mjs +1 -0
  40. package/dist/spec/document-settings.mjs +1 -0
  41. package/dist/spec/html-blocks.mjs +10 -7
  42. package/dist/spec/md-assets.mjs +52 -12
  43. package/package.json +1 -1
  44. package/style.css +25 -23
  45. package/dist/agent/vendor/pretext/entry-geometry.js +0 -183
@@ -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 => /^data:image\/(?:jxl|svg\+xml);base64,[A-Za-z0-9+/]+={0,2}$/i.test(url) || validate(url);
20
+ md.validateLink = url => !!dataImage(url) || validate(url);
21
21
  const normalize = md.normalizeLink;
22
- md.normalizeLink = url => /^data:image\/(?:jxl|png|jpeg|webp|gif|svg\+xml);base64,[A-Za-z0-9+/]+={0,2}$/i.test(url) ? url : normalize(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 ? 'data:' + image.codec + ';base64,AA==' : 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\r\n|\n\n|\r\r)$/.exec(after.slice(Math.max(0, row.start - 4), row.start));
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
- 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: ''};
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
- const match = pattern.exec(source);
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.33",
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(--color-text,var(--md-color-text));
226
- --pele-line:var(--color-text,var(--md-color-text));
227
- --pele-muted:var(--color-text-secondary,var(--md-color-text-secondary));
228
- --pele-surface:var(--color-surface-2,var(--md-color-surface-2));
229
- --pele-surface-alt:var(--color-surface-hover,var(--md-color-border-focus));
230
- --pele-border:var(--color-border,var(--color-surface-hover,var(--md-color-border)));
231
- --pele-accent:var(--color-accent,var(--md-color-accent));
232
- --pele-font:var(--font-sans,var(--md-font-sans));
233
- --pele-font-mono:var(--font-mono,var(--md-font-mono));
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
- }