rapier-markdown-kit 1.2.32 → 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 +16 -10
- 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 +191 -28
- 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 +37 -28
- package/dist/agent/vendor/pretext/entry-geometry.js +0 -183
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
/*! Pretext
|
|
1
|
+
/*! Pretext 1.0.0 + main (0d102a5cd) | MIT; BSD/Unicode/ICU notices in LICENSE | Copyright (c) 2026 Pretext contributors */
|
|
2
2
|
// Preparation's measurement phase: each segment of a text analysis measured in a font, with
|
|
3
3
|
// the facts the line walkers read at a line's edges. layout.ts and rich-inline.ts prepare
|
|
4
4
|
// through it, as they walk through line-break.ts.
|
|
5
5
|
|
|
6
|
-
import { observeSegmentEntries, textMayHaveEntryGeometry, } from './entry-geometry.js'
|
|
7
6
|
import { getHanKerningTrims, textMayHanKern, } from './han-kerning.js'
|
|
8
7
|
import { findGraphemeEnds, } from './graphemes.js'
|
|
9
8
|
import { scriptClassFacts } from './generated/engine-break-data.js'
|
|
10
9
|
import { DEFAULT_IGNORABLE, getClass, hasProperty, lazyRegExp, unpackClasses, } from './line-breaks.js'
|
|
11
10
|
import {
|
|
12
11
|
CONTROL,
|
|
12
|
+
DROPPED,
|
|
13
13
|
HARD_BREAK,
|
|
14
14
|
KIND_BITS,
|
|
15
15
|
ONE_CLUSTER,
|
|
@@ -31,19 +31,22 @@ import {
|
|
|
31
31
|
|
|
32
32
|
getCorrectedSegmentWidth,
|
|
33
33
|
getEmojiCorrection,
|
|
34
|
+
getExtraSpacings,
|
|
34
35
|
getFollowingSpaceMetrics,
|
|
36
|
+
getFontKanaKerning,
|
|
35
37
|
getFontMeasurement,
|
|
36
38
|
getFontSpaceKerning,
|
|
37
39
|
getHyphenText,
|
|
40
|
+
getKanaKerning,
|
|
38
41
|
getSegmentFit,
|
|
39
42
|
getSegmentMetrics,
|
|
40
43
|
getSpaceKerning,
|
|
41
44
|
getSpaceWidth,
|
|
42
45
|
getTextWidth,
|
|
43
|
-
|
|
46
|
+
kanaPairRe,
|
|
47
|
+
noExtraSpacings,
|
|
44
48
|
noSpaceKerning,
|
|
45
49
|
textMayContainEmoji,
|
|
46
|
-
|
|
47
50
|
|
|
48
51
|
zeros,
|
|
49
52
|
} from './measurement.js'
|
|
@@ -77,17 +80,7 @@ const extendedPictographicRe = /\p{Extended_Pictographic}/u
|
|
|
77
80
|
const leadingCombiningMarkRe = lazyRegExp(String.raw`^\p{M}`, 'u')
|
|
78
81
|
// Decimal digits and the joiners of numbers, times and dates.
|
|
79
82
|
const numericRunRe = /^[\p{Nd}:\-/×,.+\u2013\u2014]+$/u
|
|
80
|
-
const markRunRe = /^\p{M}+$/u
|
|
81
|
-
const nonspacingMarkRunRe = /^\p{Mn}+$/u
|
|
82
|
-
const controlOrMarkRunRe = /^(?:[\p{Cc}\u2028\u2029]|\p{M}+)$/u
|
|
83
83
|
const controlCharacterRe = /^[\p{Cc}\u2028\u2029]$/u
|
|
84
|
-
// The fewest UTF-16 units of a long chain of mark runs that a run's context keeps after
|
|
85
|
-
// the grapheme (getMarkContext): measured after the whole chain, a long chain prepared in
|
|
86
|
-
// time that grows with the square of its length. Safari gives a run a width that depends
|
|
87
|
-
// on how far it sits from the grapheme, up to 61 units in the chains measured
|
|
88
|
-
// (RESEARCH.md, Break Opportunities From Engine Data), so a context that keeps fewer
|
|
89
|
-
// moves widths there.
|
|
90
|
-
const MARK_CHAIN_CONTEXT_UNITS = 96
|
|
91
84
|
|
|
92
85
|
function needsComplexTextPath(text ) {
|
|
93
86
|
let previousIsEmoji = false
|
|
@@ -103,20 +96,9 @@ function needsComplexTextPath(text ) {
|
|
|
103
96
|
return false
|
|
104
97
|
}
|
|
105
98
|
|
|
106
|
-
|
|
107
|
-
//
|
|
108
|
-
// RLM and ALM, which are strong characters like letters.
|
|
109
|
-
const trailingFormatCharacterRe = /(?![\u200E\u200F\u061C])\p{Cf}$/u
|
|
110
|
-
// Letters in the right-to-left blocks have bidi class R or AL, as do RLM and
|
|
111
|
-
// ALM. Every other letter except modifier letters has class L, as does LRM.
|
|
112
|
-
const rightToLeftLetterRe = /[\u0590-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF\u200F\u{10800}-\u{10FFF}\u{1E800}-\u{1EFFF}]/u
|
|
113
|
-
// Either of the two: a right-to-left letter or an explicit bidi control.
|
|
99
|
+
// A character of the right-to-left blocks, whose letters have bidi class R or AL, as RLM
|
|
100
|
+
// has, or an explicit bidi control.
|
|
114
101
|
const mixedDirectionRe = /[\u0590-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF\u200F\u{10800}-\u{10FFF}\u{1E800}-\u{1EFFF}\u202A-\u202E\u2066-\u2069]/u
|
|
115
|
-
// The last letter or direction mark before format characters other than a soft
|
|
116
|
-
// hyphen, and the first letter, direction mark or ASCII digit after the space,
|
|
117
|
-
// past spaces and format characters.
|
|
118
|
-
const letterBeforeFormatTailRe = lazyRegExp(String.raw`([\p{Lu}\p{Ll}\p{Lt}\p{Lo}\u200E\u200F\u061C])\p{M}*(?:(?![\u00AD\u200E\u200F\u061C])\p{Cf})+$`, 'u')
|
|
119
|
-
const letterAfterSpacesRe = lazyRegExp(String.raw` (?: |(?![\u200E\u200F\u061C])\p{Cf})*([0-9\p{Lu}\p{Ll}\p{Lt}\p{Lo}\u200E\u200F\u061C])`, 'uy')
|
|
120
102
|
|
|
121
103
|
function isSpaceKind(kind ) {
|
|
122
104
|
return kind === SPACE || kind === PRESERVED_SPACE
|
|
@@ -283,11 +265,6 @@ function getUnspacedGraphemes(text , start , end , grapheme
|
|
|
283
265
|
return unspaced
|
|
284
266
|
}
|
|
285
267
|
|
|
286
|
-
// Bidi class B: the characters that end a bidi paragraph.
|
|
287
|
-
function isParagraphSeparatorCode(code ) {
|
|
288
|
-
return code === 0x0a || code === 0x0d || (code >= 0x1c && code <= 0x1e) || code === 0x85 || code === 0x2029
|
|
289
|
-
}
|
|
290
|
-
|
|
291
268
|
// The lists of a rich-inline paragraph's handle that each item's measurement adds its segments
|
|
292
269
|
// to; `segmentFlags` has room for them from `widths.length` on.
|
|
293
270
|
|
|
@@ -296,6 +273,75 @@ function isParagraphSeparatorCode(code ) {
|
|
|
296
273
|
|
|
297
274
|
|
|
298
275
|
|
|
276
|
+
// The functions from here to measureAnalysis() are its loop's. They are given what they read
|
|
277
|
+
// and not nested in it: nested, every call makes an object for the variables they read and
|
|
278
|
+
// one for each of them, in every engine. Where that object is as large as a handle,
|
|
279
|
+
// JavaScriptCore puts it in the handles' cells, one between each two, and Safari 27 ran
|
|
280
|
+
// layout() over 2,000 short labels 14-17% slower. Any object of 13 to 18 fields that every
|
|
281
|
+
// prepare() makes and drops lands there too: with one planted here, 11-13% slower.
|
|
282
|
+
|
|
283
|
+
// A WebKit text item runs to its next break opportunity, so it also owns any
|
|
284
|
+
// zero-width breaks before the space. The item is measured with one following
|
|
285
|
+
// U+0020 minus a space measured alone, which keeps the kerning between the item's
|
|
286
|
+
// end and that space. Under letter spacing both measurements hold the space's
|
|
287
|
+
// spacing (TextUtil::width over singleSpaceWidth, TextUtil.cpp:54-100, in WebKit
|
|
288
|
+
// 7625), so the item keeps that kerning there too. WebKit's item is never narrower
|
|
289
|
+
// than 0 (TextUtil.cpp:103), which the per-grapheme gap model does not represent.
|
|
290
|
+
// A soft hyphen before the space takes no kerning: on an RTL page WebKit needs
|
|
291
|
+
// about a hyphen's width more to fit such an item, and preparation cannot see the
|
|
292
|
+
// page direction. WebKit splits a text item where the bidi level changes before it
|
|
293
|
+
// measures it (breakAndComputeBidiLevels, InlineItemsBuilder.cpp:637-728), and
|
|
294
|
+
// format characters between a word and the space resolve with the space. Premise:
|
|
295
|
+
// the space resolves to the word's direction, so they stay in the word's item and
|
|
296
|
+
// the word keeps its kerning. Its gap is a space that resolves to the other
|
|
297
|
+
// direction, where Safari measures the word alone (ENGINE_FOLLOWUPS.md, Bidi
|
|
298
|
+
// levels, direction and script runs). Returns the zero-width breaks between the
|
|
299
|
+
// text and the space, or null when the text takes no kerning.
|
|
300
|
+
function getFollowingSpaceTail(analysis , analysisIndex , to ) {
|
|
301
|
+
const { normalized, spaceSources, starts, flags } = analysis
|
|
302
|
+
let next = analysisIndex + 1
|
|
303
|
+
while (next < to && (flags[next] & KIND_BITS) === ZERO_WIDTH_BREAK) next++
|
|
304
|
+
if (next >= to) return null
|
|
305
|
+
const nextKind = flags[next] & KIND_BITS
|
|
306
|
+
// A collapsed space's first source character: WebKit looks at the source after a text item.
|
|
307
|
+
const spaceStart = starts[next]
|
|
308
|
+
if ((nextKind !== SPACE && nextKind !== PRESERVED_SPACE) || (spaceSources === null ? normalized.charCodeAt(spaceStart) : spaceSources[spaceStart] ) !== 0x20) return null
|
|
309
|
+
return normalized.slice(starts[analysisIndex + 1], spaceStart)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// A zero-width break before the space ends the measured item, so only the
|
|
313
|
+
// item's kerning with the space is added to the text's own width.
|
|
314
|
+
function getTailKerning(item , measurement , spaceWidth ) {
|
|
315
|
+
return getFollowingSpaceMetrics(item, measurement).width - getSegmentMetrics(item, measurement).width - spaceWidth
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Pieces split by a soft hyphen are measured apart, but Blink and Gecko shape
|
|
319
|
+
// the unbroken text together: cursive joins, marks and kerning across the soft
|
|
320
|
+
// hyphen. Canvas shows how much narrower the neighbors measure joined than
|
|
321
|
+
// apart, which isolated widths can't show when proving that a hyphen overflows.
|
|
322
|
+
// A line counts it for the soft hyphens it passes, and in Gecko for the one it
|
|
323
|
+
// ends at (returnsFromUnfitHyphen in src/line-break.ts). WebKit's return fits each
|
|
324
|
+
// side as measured alone and takes none (unfitHyphenRetreat).
|
|
325
|
+
function getJoinedNarrowing(analysis , analysisIndex , to , before , beforeMetrics , measurement , emojiCorrection , lineFitEpsilon ) {
|
|
326
|
+
if (before === null) return 0
|
|
327
|
+
const { texts, flags } = analysis
|
|
328
|
+
let next = analysisIndex + 1
|
|
329
|
+
while (next < to && (flags[next] & KIND_BITS) === SOFT_HYPHEN) next++
|
|
330
|
+
if (next >= to || (flags[next] & KIND_BITS) !== TEXT) return 0
|
|
331
|
+
const after = texts[next]
|
|
332
|
+
const apart = getCorrectedSegmentWidth(before, beforeMetrics , measurement, emojiCorrection) + getTextWidth(after, measurement, emojiCorrection)
|
|
333
|
+
const together = getTextWidth(before + after, measurement, emojiCorrection)
|
|
334
|
+
return apart - together > lineFitEpsilon ? apart - together : 0
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// A text segment's width as measured: alone, or together with the following space less
|
|
338
|
+
// that space, plus the item's kerning with a space that follows zero-width breaks. Apart
|
|
339
|
+
// from measureAnalysis()'s loop, whose code JavaScriptCore otherwise never optimizes fully
|
|
340
|
+
// on CJK text (RESEARCH.md, Keeping Work Bounded).
|
|
341
|
+
function getTextSegmentWidth(text , textMetrics , measurement , emojiCorrection , followingSpaceWidth , followingSpaceKerning ) {
|
|
342
|
+
return getCorrectedSegmentWidth(text, textMetrics, measurement, emojiCorrection) - followingSpaceWidth + followingSpaceKerning
|
|
343
|
+
}
|
|
344
|
+
|
|
299
345
|
// Measures segments [from, to) of an analysis in a font, alone: all of a text's, or those of one
|
|
300
346
|
// item of a rich-inline paragraph (RESEARCH.md, Rich Inline As One Paragraph), where only the
|
|
301
347
|
// marks Blink halts and a run of U+3000 read the text around the item (getHanKerningTrims,
|
|
@@ -343,7 +389,16 @@ export function measureAnalysis(
|
|
|
343
389
|
const hasLetterSpacing = letterSpacing !== 0
|
|
344
390
|
const fontMeasurement = getFontMeasurement(font, language, hasLetterSpacing)
|
|
345
391
|
const emojiCorrection = textMayContainEmoji(own) ? getEmojiCorrection(font, fontMeasurement) : 0
|
|
346
|
-
|
|
392
|
+
// Whether the text holds a U+0020. Only then can a space follow one of its text segments
|
|
393
|
+
// (getFollowingSpaceTail), so a text of none, as most CJK paragraphs are, doesn't look for
|
|
394
|
+
// one after each segment.
|
|
395
|
+
const hasSpace = own.includes(' ')
|
|
396
|
+
// The font's space, asked for only by a text that holds a space or a tab. A web font split by
|
|
397
|
+
// unicode-range has the part with U+0020 loaded only once a text that holds a character of
|
|
398
|
+
// that part was loaded for (document.fonts.load(font, text)), so a text of other characters
|
|
399
|
+
// alone, a CJK label or one Cyrillic word, would measure the fallback font's space and the
|
|
400
|
+
// font would keep it for every later text.
|
|
401
|
+
const spaceWidth = hasSpace || own.includes('\t') ? getSpaceWidth(fontMeasurement) : 0
|
|
347
402
|
// The advance between tab stops: eight spaces, each with its letter spacing where the
|
|
348
403
|
// engine counts it (EngineProfile's letterSpaceTabStops). Gecko rounds the space and the
|
|
349
404
|
// letter spacing to app units, sixtieths of a pixel, each on its own
|
|
@@ -354,143 +409,13 @@ export function measureAnalysis(
|
|
|
354
409
|
: (spaceWidth + tabStopSpacing) * 8
|
|
355
410
|
// The least a tab advances, measured at a text's first tab (EngineProfile's tabMinimumCharacter).
|
|
356
411
|
let minimumTabAdvance = 0
|
|
357
|
-
// Only a segment holding a default-ignorable code point has entry geometry, so text
|
|
358
|
-
// without one doesn't look for it.
|
|
359
|
-
const entryFitBasis = engineProfile.entryFitBasis !== 'disabled' && textMayHaveEntryGeometry(own) ? engineProfile.entryFitBasis : 'disabled'
|
|
360
|
-
|
|
361
|
-
// A collapsed space's first source character, for engines that look at the
|
|
362
|
-
// source after a text item.
|
|
363
|
-
function getSpaceSourceCode(analysisIndex ) {
|
|
364
|
-
const start = starts[analysisIndex]
|
|
365
|
-
return analysis.spaceSources === null ? normalized.charCodeAt(start) : analysis.spaceSources[start]
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// A WebKit text item runs to its next break opportunity, so it also owns any
|
|
369
|
-
// zero-width breaks before the space. The item is measured with one following
|
|
370
|
-
// U+0020 minus an unshaped space, which keeps the kerning between the item's
|
|
371
|
-
// end and that space. With letter spacing the same measurement also moves the
|
|
372
|
-
// space's gap onto the item and clamps the item at zero, which the
|
|
373
|
-
// per-grapheme gap model does not represent, so only the unspaced case takes
|
|
374
|
-
// the kerning. A soft hyphen before the space also takes none: on an RTL page
|
|
375
|
-
// WebKit needs about a hyphen's width more to fit such an item, and
|
|
376
|
-
// preparation cannot see the page direction. Returns the zero-width breaks
|
|
377
|
-
// between the text and the space, or null when the text takes no kerning.
|
|
378
|
-
function getFollowingSpaceTail(analysisIndex , text ) {
|
|
379
|
-
if (!engineProfile.measureTextWithFollowingSpace || hasLetterSpacing) return null
|
|
380
|
-
let next = analysisIndex + 1
|
|
381
|
-
while (next < to && (flags[next] & KIND_BITS) === ZERO_WIDTH_BREAK) next++
|
|
382
|
-
if (next >= to) return null
|
|
383
|
-
const nextKind = flags[next] & KIND_BITS
|
|
384
|
-
if ((nextKind !== SPACE && nextKind !== PRESERVED_SPACE) || getSpaceSourceCode(next) !== 0x20) return null
|
|
385
|
-
const tail = normalized.slice(starts[analysisIndex + 1], starts[next])
|
|
386
|
-
return formatTailStaysWithWord(tail === '' ? text : text + tail, starts[next] - ownStart) ? tail : null
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Text directly before such a space is measured together with the space
|
|
390
|
-
// instead of alone, so its kerned width costs no extra Canvas call. Other
|
|
391
|
-
// occurrences of the same text measure it alone.
|
|
392
|
-
function getTextMetrics(text , followingSpaceTail ) {
|
|
393
|
-
return followingSpaceTail === '' ? getFollowingSpaceMetrics(text, fontMeasurement) : getSegmentMetrics(text, fontMeasurement)
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// A zero-width break before the space ends the measured item, so only the
|
|
397
|
-
// item's kerning with the space is added to the text's own width.
|
|
398
|
-
function getTailKerning(item ) {
|
|
399
|
-
return getFollowingSpaceMetrics(item, fontMeasurement).width - getSegmentMetrics(item, fontMeasurement).width - spaceWidth
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// WebKit splits text items where resolved bidi levels change before it
|
|
403
|
-
// measures them. Format characters between a word and the space resolve with
|
|
404
|
-
// that space, so they stay in the word's item, and the word's last glyph keeps
|
|
405
|
-
// its kerning with the space, only when the space resolves to the word's
|
|
406
|
-
// direction. Without the paragraph direction that is known when the word's last
|
|
407
|
-
// letter or direction mark and the first one after the space have the same
|
|
408
|
-
// direction, with only spaces and format characters between (UAX #9 N1).
|
|
409
|
-
// Explicit embeddings, overrides and isolates end with their paragraph (UAX #9
|
|
410
|
-
// X8), so only controls in the space's own paragraph leave its direction
|
|
411
|
-
// unknown. Spaces arrive in order, so each paragraph is scanned once.
|
|
412
|
-
let hasExplicitBidiControls = null
|
|
413
|
-
let controlParagraphEnd = -1
|
|
414
|
-
let paragraphHasExplicitBidiControls = false
|
|
415
|
-
function spaceParagraphHasExplicitBidiControls(spaceStart ) {
|
|
416
|
-
hasExplicitBidiControls ??= explicitBidiControlRe.test(own)
|
|
417
|
-
if (!hasExplicitBidiControls) return false
|
|
418
|
-
if (spaceStart < controlParagraphEnd) return paragraphHasExplicitBidiControls
|
|
419
|
-
let start = spaceStart
|
|
420
|
-
while (start > 0 && !isParagraphSeparatorCode(own.charCodeAt(start - 1))) start--
|
|
421
|
-
let end = spaceStart
|
|
422
|
-
while (end < own.length && !isParagraphSeparatorCode(own.charCodeAt(end))) end++
|
|
423
|
-
controlParagraphEnd = end
|
|
424
|
-
paragraphHasExplicitBidiControls = explicitBidiControlRe.test(own.slice(start, end))
|
|
425
|
-
return paragraphHasExplicitBidiControls
|
|
426
|
-
}
|
|
427
|
-
function formatTailStaysWithWord(item , spaceStart ) {
|
|
428
|
-
if (!trailingFormatCharacterRe.test(item)) return true
|
|
429
|
-
const before = letterBeforeFormatTailRe().exec(item)
|
|
430
|
-
if (before === null) return false
|
|
431
|
-
letterAfterSpacesRe().lastIndex = spaceStart
|
|
432
|
-
const after = letterAfterSpacesRe().exec(own)
|
|
433
|
-
if (after === null) return false
|
|
434
|
-
// An ASCII digit takes the direction of the text before it (UAX #9 W7 and N1).
|
|
435
|
-
const next = after[1]
|
|
436
|
-
return (next.charCodeAt(0) <= 0x39 || rightToLeftLetterRe.test(before[1] ) === rightToLeftLetterRe.test(next)) &&
|
|
437
|
-
!spaceParagraphHasExplicitBidiControls(spaceStart)
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// The source a run of combining marks shapes after when only zero-width glue,
|
|
441
|
-
// controls or other such runs, with no break, separate the run from the grapheme
|
|
442
|
-
// before it: that grapheme and what separates them. Without the separators, Canvas
|
|
443
|
-
// can compose the marks with the grapheme or draw both in another font. A walk that
|
|
444
|
-
// reaches the last run that asked takes that run's answer, so each segment is walked
|
|
445
|
-
// and each grapheme found once, however many runs share it. Once what separates them
|
|
446
|
-
// passes MARK_CHAIN_CONTEXT_UNITS, the context keeps the grapheme and the fewest of the
|
|
447
|
-
// chain's last runs, each with the separators before it, that hold at least that many
|
|
448
|
-
// units: in Chrome, Safari and Firefox, runs of 1 to 400 marks then measure as they do
|
|
449
|
-
// after the whole chain, to 0.002px, and without the grapheme some took 25px less
|
|
450
|
-
// (RESEARCH.md, Break Opportunities From Engine Data).
|
|
451
|
-
let markRunIndex = -1
|
|
452
|
-
let markBaseStart = -1 // where that run's grapheme starts in the normalized text, or -1
|
|
453
|
-
let markChainStart = -1 // the segment after that grapheme
|
|
454
|
-
let markChainKept = -1 // the first segment of the chain the context keeps
|
|
455
|
-
// Asked only for a text segment no break comes before.
|
|
456
|
-
function getMarkContext(analysisIndex , text ) {
|
|
457
|
-
if (!markRunRe.test(text)) return null
|
|
458
|
-
let baseStart = -1
|
|
459
|
-
for (let k = analysisIndex - 1; k >= from; k--) {
|
|
460
|
-
const kind = flags[k] & KIND_BITS
|
|
461
|
-
const start = starts[k]
|
|
462
|
-
const end = starts[k + 1]
|
|
463
|
-
if (kind === ZERO_WIDTH_GLUE || ((kind === TEXT || kind === CONTROL) && controlOrMarkRunRe.test(texts[k] ))) {
|
|
464
|
-
if (k !== markRunIndex) continue
|
|
465
|
-
baseStart = markBaseStart
|
|
466
|
-
} else if (kind === TEXT) {
|
|
467
|
-
const ends = new Int32Array(end - start)
|
|
468
|
-
const count = findGraphemeEnds(engineProfile.graphemeTable, normalized, start, end, ends)
|
|
469
|
-
baseStart = count > 1 ? ends[count - 2] : start
|
|
470
|
-
markChainStart = markChainKept = k + 1
|
|
471
|
-
}
|
|
472
|
-
break
|
|
473
|
-
}
|
|
474
|
-
markRunIndex = analysisIndex
|
|
475
|
-
markBaseStart = baseStart
|
|
476
|
-
if (baseStart < 0) return null
|
|
477
|
-
const start = starts[analysisIndex]
|
|
478
|
-
if (start - baseStart <= MARK_CHAIN_CONTEXT_UNITS) return normalized.slice(baseStart, start)
|
|
479
|
-
// The kept part starts after the grapheme or a run of marks, moves only forward and
|
|
480
|
-
// never holds fewer than MARK_CHAIN_CONTEXT_UNITS.
|
|
481
|
-
for (let k = markChainKept + 1; start - starts[k] >= MARK_CHAIN_CONTEXT_UNITS; k++) {
|
|
482
|
-
if (markRunRe.test(texts[k - 1] )) markChainKept = k
|
|
483
|
-
}
|
|
484
|
-
if (markChainKept === markChainStart) return normalized.slice(baseStart, start)
|
|
485
|
-
return normalized.slice(baseStart, starts[markChainStart]) + normalized.slice(starts[markChainKept], start)
|
|
486
|
-
}
|
|
487
412
|
|
|
488
413
|
// The font's kerning with the space where words take Blink's kerning with the spaces beside
|
|
489
414
|
// them, or null. Blink shapes nothing across a change of direction
|
|
490
415
|
// (ShouldBreakShapingBeforeText, inline_node.cc:472-490), and which spaces share a word's
|
|
491
416
|
// direction depends on the paragraph's, which preparation can't see, so text with a
|
|
492
417
|
// right-to-left letter or an explicit bidi control takes none.
|
|
493
|
-
let fontSpaceKerning = engineProfile.
|
|
418
|
+
let fontSpaceKerning = engineProfile.kernsAcrossCanvasWords && own.includes(' ') ? getFontSpaceKerning(fontMeasurement) : null
|
|
494
419
|
if (fontSpaceKerning !== null && mixedDirectionRe.test(own)) fontSpaceKerning = null
|
|
495
420
|
const scriptRuns = { read: 0, scripts: ANY_SCRIPT, openBrackets: [] }
|
|
496
421
|
// What the word before a space adds to that space, the next segment.
|
|
@@ -503,53 +428,12 @@ export function measureAnalysis(
|
|
|
503
428
|
// the scan breaks at every segment boundary, and layout() counts it with the
|
|
504
429
|
// simple stepper where it doesn't (countPreparedLines).
|
|
505
430
|
let simpleKinds = !hasLetterSpacing
|
|
506
|
-
let entryGeometry = null
|
|
507
|
-
let lineStartProhibitions = null
|
|
508
431
|
let breakableLineStartExtras = null
|
|
509
|
-
// WebKit's line-start rule applies only in text holding a code unit above U+00FF.
|
|
510
|
-
const keepsLineStartPunctuation = engineProfile.keepsLineStartPunctuation && /[\u0100-\uFFFF]/.test(own)
|
|
511
432
|
let discretionaryHyphenContexts = null
|
|
512
433
|
let previousJoinablePiece = null
|
|
513
434
|
let previousJoinableMetrics = null
|
|
514
|
-
|
|
515
|
-
// Pieces split by a soft hyphen are measured apart, but Blink and Gecko shape
|
|
516
|
-
// the unbroken text together: cursive joins, marks and kerning across the soft
|
|
517
|
-
// hyphen. Canvas shows how much narrower the neighbors measure joined than
|
|
518
|
-
// apart, which isolated widths can't show when proving that a hyphen overflows.
|
|
519
|
-
// WebKit's return fits each side as measured alone and takes none (unfitHyphenRetreat).
|
|
435
|
+
// WebKit's return from an unfit hyphen fits each side as measured alone (getJoinedNarrowing).
|
|
520
436
|
const returnFitsEachSideAlone = engineProfile.unfitHyphenRetreat === 'full-width-or-first'
|
|
521
|
-
function getJoinedNarrowing(analysisIndex , before , beforeMetrics ) {
|
|
522
|
-
if (before === null) return 0
|
|
523
|
-
let next = analysisIndex + 1
|
|
524
|
-
while (next < to && (flags[next] & KIND_BITS) === SOFT_HYPHEN) next++
|
|
525
|
-
if (next >= to || (flags[next] & KIND_BITS) !== TEXT) return 0
|
|
526
|
-
const after = texts[next]
|
|
527
|
-
const apart = getCorrectedSegmentWidth(before, beforeMetrics , fontMeasurement, emojiCorrection) + getTextWidth(after, fontMeasurement, emojiCorrection)
|
|
528
|
-
const together = getTextWidth(before + after, fontMeasurement, emojiCorrection)
|
|
529
|
-
return apart - together > engineProfile.lineFitEpsilon ? apart - together : 0
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function getEntryGeometry(text , fit , width , fitBasis ) {
|
|
533
|
-
// The fit fixes the text, font and advances, and Pretext sets no other context state.
|
|
534
|
-
// The WebKit profile moves the advances by a following space, and it observes no
|
|
535
|
-
// entries; the Chromium profile's kerning with a space is in the width alone, which
|
|
536
|
-
// its fresh entries don't read.
|
|
537
|
-
const cached = fit.entryGeometry
|
|
538
|
-
if (cached !== null && cached.letterSpacing === letterSpacing && cached.emojiCorrection === emojiCorrection) return cached.geometry
|
|
539
|
-
const geometry = observeSegmentEntries(text, fit.advances , letterSpacing, width, fitBasis,
|
|
540
|
-
source => measureWithLetterSpacing(source, letterSpacing, emojiCorrection, fontMeasurement))
|
|
541
|
-
// Replacing this last observation leaves prepared copies intact.
|
|
542
|
-
fit.entryGeometry = { letterSpacing, emojiCorrection, geometry }
|
|
543
|
-
return geometry
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// A text segment's width as measured: alone, or together with the following space less
|
|
547
|
-
// that space, plus the item's kerning with a space that follows zero-width breaks. Apart
|
|
548
|
-
// from the loop below, whose code JavaScriptCore otherwise never optimizes fully on CJK
|
|
549
|
-
// text (RESEARCH.md, Keeping Work Bounded).
|
|
550
|
-
function getTextSegmentWidth(text , textMetrics , measuredWithSpace , followingSpaceKerning ) {
|
|
551
|
-
return getCorrectedSegmentWidth(text, textMetrics, fontMeasurement, emojiCorrection) - (measuredWithSpace ? spaceWidth : 0) + followingSpaceKerning
|
|
552
|
-
}
|
|
553
437
|
|
|
554
438
|
// Each segment is stored at its index: `at` in the text's or the paragraph's three lists,
|
|
555
439
|
// `mi - from` in the lists of the measured segments alone. Stored and not pushed, for V8:
|
|
@@ -567,8 +451,6 @@ export function measureAnalysis(
|
|
|
567
451
|
// Graphemes that take letter spacing after them.
|
|
568
452
|
let spacingGraphemeCount = 0
|
|
569
453
|
let fitAdvances = null
|
|
570
|
-
let entry = null
|
|
571
|
-
let prohibitions = null
|
|
572
454
|
switch (kind) {
|
|
573
455
|
case TEXT: {
|
|
574
456
|
// A control the engine hides takes no advance, only letter spacing.
|
|
@@ -576,27 +458,15 @@ export function measureAnalysis(
|
|
|
576
458
|
spacingGraphemeCount = 1
|
|
577
459
|
break
|
|
578
460
|
}
|
|
579
|
-
//
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
|
|
583
|
-
// V8 didn't inline (RESEARCH.md, Keeping Work Bounded, JavaScript Engines, under V8's
|
|
584
|
-
// inlining budgets and the mark context).
|
|
585
|
-
const markContext = (segment & UNBROKEN) === 0 ? null : getMarkContext(mi, text)
|
|
586
|
-
if (markContext !== null) {
|
|
587
|
-
width = engineProfile.shapesMarksAcrossSoftHyphen && texts[mi - 1] === '\u00AD' && nonspacingMarkRunRe.test(text)
|
|
588
|
-
? 0
|
|
589
|
-
: getTextWidth(markContext + text, fontMeasurement, emojiCorrection) - getTextWidth(markContext, fontMeasurement, emojiCorrection)
|
|
590
|
-
break
|
|
591
|
-
}
|
|
592
|
-
// With an empty following-space tail, the text is measured together with the
|
|
593
|
-
// space; with a zero-width tail, the item's kerning is added.
|
|
594
|
-
const followingSpaceTail = getFollowingSpaceTail(mi, text)
|
|
461
|
+
// With an empty following-space tail, the text is measured together with the space,
|
|
462
|
+
// so its kerned width costs no extra Canvas call, and other occurrences of the same
|
|
463
|
+
// text measure it alone; with a zero-width tail, the item's kerning is added.
|
|
464
|
+
const followingSpaceTail = engineProfile.measureTextWithFollowingSpace && hasSpace ? getFollowingSpaceTail(analysis, mi, to) : null
|
|
595
465
|
const measuredWithSpace = followingSpaceTail === ''
|
|
596
|
-
const textMetrics =
|
|
466
|
+
const textMetrics = measuredWithSpace ? getFollowingSpaceMetrics(text, fontMeasurement) : getSegmentMetrics(text, fontMeasurement)
|
|
597
467
|
previousJoinablePiece = text
|
|
598
468
|
previousJoinableMetrics = textMetrics
|
|
599
|
-
let followingSpaceKerning = followingSpaceTail === null || measuredWithSpace ? 0 : getTailKerning(text + followingSpaceTail)
|
|
469
|
+
let followingSpaceKerning = followingSpaceTail === null || measuredWithSpace ? 0 : getTailKerning(text + followingSpaceTail, fontMeasurement, spaceWidth)
|
|
600
470
|
if (fontSpaceKerning !== null && textMetrics.spaceKerning !== noSpaceKerning) {
|
|
601
471
|
const spaceBefore = mi > from && isSpaceKind(flags[mi - 1] & KIND_BITS)
|
|
602
472
|
const spaceAfter = mi + 1 < to && isSpaceKind(flags[mi + 1] & KIND_BITS)
|
|
@@ -625,17 +495,24 @@ export function measureAnalysis(
|
|
|
625
495
|
}
|
|
626
496
|
}
|
|
627
497
|
}
|
|
628
|
-
width = getTextSegmentWidth(text, textMetrics, measuredWithSpace, followingSpaceKerning)
|
|
498
|
+
width = getTextSegmentWidth(text, textMetrics, fontMeasurement, emojiCorrection, measuredWithSpace ? spaceWidth : 0, followingSpaceKerning)
|
|
629
499
|
// The walkers put a gap after every grapheme of a spaced segment, so a grapheme
|
|
630
500
|
// the engine gives none takes one back from its advance, here and in the advances
|
|
631
|
-
// a break inside the segment falls by
|
|
501
|
+
// a break inside the segment falls by, and one whose glyphs WebKit gives more or
|
|
502
|
+
// fewer takes the difference (EngineProfile's letterSpacesGlyphs), in text of its
|
|
503
|
+
// complex path.
|
|
632
504
|
let unspaced = null
|
|
505
|
+
let extraSpacings = noExtraSpacings
|
|
633
506
|
if (hasLetterSpacing) {
|
|
634
507
|
spacingGraphemeCount = countRenderedSpacingGraphemes(text, kind, engineProfile.graphemeTable)
|
|
635
508
|
if (cursiveSpacing) {
|
|
636
509
|
unspaced = getUnspacedGraphemes(own, starts[mi] - ownStart, starts[mi] - ownStart + text.length, engineProfile.graphemeTable, engineProfile.unspacedCursive === 'run' ? scriptRuns : null)
|
|
637
510
|
if (unspaced !== null) width -= unspaced.length * letterSpacing
|
|
638
511
|
}
|
|
512
|
+
if (engineProfile.letterSpacesGlyphs) {
|
|
513
|
+
extraSpacings = textMetrics.extraSpacings ??= needsComplexTextPath(text) ? getExtraSpacings(text, fontMeasurement) : noExtraSpacings
|
|
514
|
+
for (let k = 0; k < extraSpacings.length; k++) width += extraSpacings[k] * letterSpacing
|
|
515
|
+
}
|
|
639
516
|
}
|
|
640
517
|
// Under break-word, Blink retries an overflowing line with a break allowed between
|
|
641
518
|
// any two graphemes (line_breaker.cc), WebKit searches the word's grapheme prefixes
|
|
@@ -647,8 +524,7 @@ export function measureAnalysis(
|
|
|
647
524
|
: numericRunRe.test(text) ? 'pair-context'
|
|
648
525
|
: textMetrics.width < engineProfile.prefixFitMinWidth ? 'sum-graphemes'
|
|
649
526
|
: engineProfile.cutWordFit
|
|
650
|
-
const fit = getSegmentFit(text, textMetrics, fontMeasurement, emojiCorrection, fitMode,
|
|
651
|
-
measuredWithSpace ? spaceWidth : null, engineProfile.keepsLineStartPunctuation)
|
|
527
|
+
const fit = getSegmentFit(text, textMetrics, fontMeasurement, emojiCorrection, fitMode, measuredWithSpace ? spaceWidth : null)
|
|
652
528
|
fitAdvances = fit.advances
|
|
653
529
|
if (fitAdvances === null) break
|
|
654
530
|
// A difference from the advance, so it holds whatever the advances take below.
|
|
@@ -659,15 +535,14 @@ export function measureAnalysis(
|
|
|
659
535
|
fitAdvances = fitAdvances.slice()
|
|
660
536
|
fitAdvances[fitAdvances.length - 1] = fitAdvances[fitAdvances.length - 1] + followingSpaceKerning
|
|
661
537
|
}
|
|
662
|
-
|
|
663
|
-
|
|
538
|
+
if (extraSpacings.length !== 0) {
|
|
539
|
+
fitAdvances = fitAdvances.slice()
|
|
540
|
+
for (let k = 0; k < extraSpacings.length; k++) fitAdvances[k] = fitAdvances[k] + extraSpacings[k] * letterSpacing
|
|
541
|
+
}
|
|
664
542
|
if (unspaced !== null) {
|
|
665
543
|
fitAdvances = fitAdvances.slice()
|
|
666
544
|
for (let k = 0; k < unspaced.length; k++) fitAdvances[unspaced[k] ] = fitAdvances[unspaced[k] ] - letterSpacing
|
|
667
|
-
} else if (entryFitBasis !== 'disabled') {
|
|
668
|
-
entry = getEntryGeometry(text, fit, addInternalLetterSpacing(width, spacingGraphemeCount, letterSpacing), entryFitBasis)
|
|
669
545
|
}
|
|
670
|
-
if (keepsLineStartPunctuation) prohibitions = fit.lineStartProhibitions
|
|
671
546
|
break
|
|
672
547
|
}
|
|
673
548
|
case SPACE:
|
|
@@ -704,17 +579,19 @@ export function measureAnalysis(
|
|
|
704
579
|
segmentFlags[at] = (segment & ~ONE_CLUSTER) | (hasLetterSpacing && spacingGraphemeCount > 0 ? SPACED : 0)
|
|
705
580
|
widths[at] = addInternalLetterSpacing(width, spacingGraphemeCount, letterSpacing)
|
|
706
581
|
breakableFitAdvances[at] = fitAdvances
|
|
707
|
-
if (entry !== null && entryGeometry === null) entryGeometry = Array.from({ length: mi - from }, () => null)
|
|
708
|
-
if (entryGeometry !== null) entryGeometry[mi - from] = entry
|
|
709
|
-
if (prohibitions !== null && lineStartProhibitions === null) lineStartProhibitions = Array.from({ length: mi - from }, () => null)
|
|
710
|
-
if (lineStartProhibitions !== null) lineStartProhibitions[mi - from] = prohibitions
|
|
711
582
|
if (segments !== null) segments[mi - from] = text
|
|
712
583
|
// Contexts for every segment of soft hyphens, whatever its kind here, one that is glue
|
|
713
584
|
// too, where the scan gives no break after it: a handle with contexts is one whose lines
|
|
714
|
-
// return from an unfit hyphen, measured below.
|
|
585
|
+
// return from an unfit hyphen, measured below. One that is a zero-width break is marked
|
|
586
|
+
// (DROPPED), which tells it from a zero-width space, and its text is the full walker's: the
|
|
587
|
+
// simple walkers take every zero-width break for a zero-width space.
|
|
715
588
|
if (kind !== TEXT && text.charCodeAt(0) === 0xAD) {
|
|
589
|
+
if (kind === ZERO_WIDTH_BREAK) {
|
|
590
|
+
segmentFlags[at] = segmentFlags[at] | DROPPED
|
|
591
|
+
simpleKinds = false
|
|
592
|
+
}
|
|
716
593
|
discretionaryHyphenContexts ??= zeros(mi - from)
|
|
717
|
-
discretionaryHyphenContexts[mi - from] = returnFitsEachSideAlone ? 0 : getJoinedNarrowing(mi, previousJoinablePiece, previousJoinableMetrics)
|
|
594
|
+
discretionaryHyphenContexts[mi - from] = returnFitsEachSideAlone ? 0 : getJoinedNarrowing(analysis, mi, to, previousJoinablePiece, previousJoinableMetrics, fontMeasurement, emojiCorrection, engineProfile.lineFitEpsilon)
|
|
718
595
|
} else if (discretionaryHyphenContexts !== null) {
|
|
719
596
|
discretionaryHyphenContexts[mi - from] = 0
|
|
720
597
|
}
|
|
@@ -734,6 +611,13 @@ export function measureAnalysis(
|
|
|
734
611
|
const trims = hanKerning.widthTrims
|
|
735
612
|
if (trims !== null) for (let i = 0; i < trims.length; i++) widths[base + i] = widths[base + i] - trims[i]
|
|
736
613
|
}
|
|
614
|
+
// The loop above has measured every segment in the font, so text in a font that has measured
|
|
615
|
+
// no kana, as in one that has answered that it kerns none, isn't looked through for kana.
|
|
616
|
+
let lineStartExtras = hanKerning.lineStartExtras
|
|
617
|
+
if (engineProfile.kernsAcrossCanvasWords && fontMeasurement.measuredKana && fontMeasurement.kanaKerning !== null && kanaPairRe.test(own)) {
|
|
618
|
+
const kanaKerning = getFontKanaKerning(fontMeasurement)
|
|
619
|
+
if (kanaKerning !== null) lineStartExtras = addKanaKerning(lineStartExtras, widths, base, analysis, from, to, fontMeasurement, kanaKerning)
|
|
620
|
+
}
|
|
737
621
|
let lineEndTrims = hanKerning.lineEndTrims
|
|
738
622
|
if (engineProfile.hangsIdeographicSpace && own.includes('\u3000')) {
|
|
739
623
|
lineEndTrims = addIdeographicSpaceHangs(lineEndTrims, analysis, fontMeasurement, letterSpacing, discretionaryHyphenWidth, from, to)
|
|
@@ -744,13 +628,11 @@ export function measureAnalysis(
|
|
|
744
628
|
simpleLineWalkFastPath: simpleKinds && !analysis.hasUnbroken,
|
|
745
629
|
simpleLineCountFastPath: simpleKinds,
|
|
746
630
|
breakableFitAdvances,
|
|
747
|
-
entryGeometry,
|
|
748
631
|
letterSpacing,
|
|
749
632
|
discretionaryHyphenWidth,
|
|
750
633
|
discretionaryHyphenContexts,
|
|
751
|
-
lineStartProhibitions,
|
|
752
634
|
breakableLineStartExtras,
|
|
753
|
-
lineStartExtras
|
|
635
|
+
lineStartExtras,
|
|
754
636
|
lineEndTrims,
|
|
755
637
|
overflowLineEndTrims: hanKerning.overflowLineEndTrims,
|
|
756
638
|
tabStopAdvance,
|
|
@@ -760,6 +642,44 @@ export function measureAnalysis(
|
|
|
760
642
|
return prepared
|
|
761
643
|
}
|
|
762
644
|
|
|
645
|
+
// Blink's layout shapes a run of one script in one call and takes katakana for hiragana, so
|
|
646
|
+
// that the two stay in one run (GetScriptForOpenType, script_run_iterator.cc:20-36), so a kana
|
|
647
|
+
// kerns with the kana after it in fonts that pair them, as Hiragino Sans and Hiragino Mincho
|
|
648
|
+
// ProN do (GPOS pair positioning). Canvas cuts the two apart and preparation measures segments
|
|
649
|
+
// apart, so each such pair's kerning (getKanaKerning) is added here, for a font that kerns
|
|
650
|
+
// kana (getFontKanaKerning). The adjustment sits on the pair's first glyph, and a line that
|
|
651
|
+
// breaks between the two is shaped again without it (ShapingLineBreaker::ShapeLine,
|
|
652
|
+
// shaping_line_breaker.cc:511-584): one that tightens the pair goes on the second kana's
|
|
653
|
+
// segment, which gives it back where it starts a line. One that widens it stays on the first
|
|
654
|
+
// kana's, since Blink looks for a line's end in the run shaped whole
|
|
655
|
+
// (shaping_line_breaker.cc:325-333), where the first kana has it. A segment cut between two of
|
|
656
|
+
// its kana takes the same in its advances (getSegmentFit). `widths` holds segments [from, to)
|
|
657
|
+
// of the analysis from `base`, a rich-inline item's in its paragraph's, and `extras` holds
|
|
658
|
+
// them from index 0; a kana that starts an item takes no kerning with the item before it.
|
|
659
|
+
function addKanaKerning(extras , widths , base , analysis , from , to , measurement , kernings ) {
|
|
660
|
+
const { normalized, starts, flags } = analysis
|
|
661
|
+
for (let i = from; i < to; i++) {
|
|
662
|
+
if ((flags[i] & KIND_BITS) !== TEXT) continue
|
|
663
|
+
const start = starts[i]
|
|
664
|
+
const end = i + 1 < flags.length ? starts[i + 1] : normalized.length
|
|
665
|
+
const at = base + i - from
|
|
666
|
+
for (let k = i > from ? start : start + 1; k < end; k++) {
|
|
667
|
+
const kerning = getKanaKerning(normalized, start, k, measurement, kernings)
|
|
668
|
+
if (kerning === 0) continue
|
|
669
|
+
if (k > start) {
|
|
670
|
+
widths[at] = widths[at] + kerning
|
|
671
|
+
} else if (kerning > 0) {
|
|
672
|
+
widths[at - 1] = widths[at - 1] + kerning
|
|
673
|
+
} else {
|
|
674
|
+
widths[at] = widths[at] + kerning
|
|
675
|
+
extras ??= zeros(to - from)
|
|
676
|
+
extras[i - from] = extras[i - from] - kerning
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return extras
|
|
681
|
+
}
|
|
682
|
+
|
|
763
683
|
// Blink (Chrome 153) hangs a run of U+3000 that ends a line, as it hangs spaces: ShapingLineBreaker
|
|
764
684
|
// counts U+3000 as a breakable space (IsBreakableSpace, shaping_line_breaker.cc:38-41, with
|
|
765
685
|
// Character::IsOtherSpaceSeparator, character.h:156-158), so a line whose width runs out on
|