rapier-markdown-kit 1.1.102 → 1.1.104
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/LICENSE +2 -2
- package/README.md +213 -53
- package/dist/agent/vendor/hyphen-en-us/LICENSE +14 -0
- package/dist/agent/vendor/hyphen-en-us/SOURCE.json +33 -0
- package/dist/agent/vendor/hyphen-en-us/hyph-en-us.hyp.txt +14 -0
- package/dist/agent/vendor/hyphen-en-us/hyph-en-us.pat.txt +4938 -0
- package/dist/agent/vendor/hyphen-en-us/patterns.js +25 -0
- package/dist/agent/vendor/pretext/LICENSE +208 -0
- package/dist/agent/vendor/pretext/NOTICE +26 -13
- package/dist/agent/vendor/pretext/SOURCE.json +213 -45
- package/dist/agent/vendor/pretext/analysis.js +675 -1224
- package/dist/agent/vendor/pretext/entry-geometry.js +183 -0
- package/dist/agent/vendor/pretext/gecko-line-breaks.js +688 -0
- package/dist/agent/vendor/pretext/generated/engine-break-data.js +57 -0
- package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +80 -0
- package/dist/agent/vendor/pretext/graphemes.js +98 -0
- package/dist/agent/vendor/pretext/han-kerning.js +253 -0
- package/dist/agent/vendor/pretext/layout.js +352 -290
- package/dist/agent/vendor/pretext/line-break.js +1154 -588
- package/dist/agent/vendor/pretext/line-breaks.js +892 -0
- package/dist/agent/vendor/pretext/line-text.js +81 -39
- package/dist/agent/vendor/pretext/measurement.js +1105 -176
- package/dist/agent/vendor/pretext/prepare.js +807 -0
- package/dist/agent/vendor/pretext/rich-inline.js +1242 -273
- package/dist/agent/will.mjs +2 -0
- package/dist/kit/assets.mjs +5 -1
- package/dist/kit/conformance/run.mjs +4 -4
- package/dist/kit/export-fonts.mjs +26 -11
- package/dist/kit/index.mjs +1 -0
- package/dist/kit/ledger/carried.mjs +9 -7
- package/dist/kit/ledger/data.mjs +73 -13
- package/dist/kit/ledger/format.mjs +166 -53
- package/dist/kit/ledger/hash.mjs +75 -18
- package/dist/kit/ledger/history.mjs +143 -115
- package/dist/kit/ledger/inverse-source.mjs +158 -19
- package/dist/kit/ledger/journal-records.mjs +292 -67
- package/dist/kit/model.mjs +1 -1
- package/dist/kit/pdf-glyphs.mjs +4 -4
- package/dist/kit/render-markdown.mjs +259 -122
- package/dist/kit/render-sanitize.mjs +24 -4
- package/dist/kit/render-work.mjs +99 -20
- package/dist/kit/render.mjs +101 -65
- package/dist/kit/typography.mjs +2 -0
- package/dist/layout/hyphenation.mjs +127 -0
- package/dist/layout/line-plan.mjs +88 -96
- package/dist/layout/model.mjs +61 -80
- package/dist/layout/paragraph-fit.mjs +191 -0
- package/dist/layout/preparation.mjs +373 -0
- package/dist/layout/prepared-paragraph.mjs +223 -0
- package/dist/layout/typography.mjs +345 -111
- package/dist/packages/rapier-diff/index.mjs +288 -196
- package/dist/spec/document-fonts.mjs +13 -4
- package/dist/spec/md-assets.mjs +149 -30
- package/dist/spec/md-layout.mjs +5 -2
- package/dist/spec/md-marks.mjs +218 -3
- package/dist/spec/md-source.mjs +122 -0
- package/package.json +2 -1
- package/style.css +9 -0
- package/dist/agent/vendor/pretext/bidi.js +0 -151
- package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
/*! Pretext Unreleased (bac1774c0) | MIT; BSD/Unicode/ICU notices in LICENSE | Copyright (c) 2026 Pretext contributors */
|
|
2
|
+
// Break opportunities as Firefox finds them: a port of how Gecko transforms a text node's
|
|
3
|
+
// white space, sets up its text run and breaks its words with nsLineBreaker over ICU4X's
|
|
4
|
+
// line iterator, with Firefox's baked line data and the Unicode properties Gecko reads,
|
|
5
|
+
// which scripts/generate-engine-break-data.ts writes to src/generated/engine-break-data.ts.
|
|
6
|
+
//
|
|
7
|
+
// Sources, cited as file:line:
|
|
8
|
+
// - Gecko in mozilla-firefox at Firefox 156.0 (3bf8f4682).
|
|
9
|
+
// - icu_segmenter 2.1.2 src/line.rs, byte-identical to Firefox's third_party/rust copy.
|
|
10
|
+
//
|
|
11
|
+
// Deliberate differences:
|
|
12
|
+
// - Grapheme clusters come from the profile's ICU character rules (src/graphemes.ts), Chrome's,
|
|
13
|
+
// which give the clusters of icu_segmenter's GraphemeClusterSegmenter over Firefox's data, and
|
|
14
|
+
// Unicode properties from RegExp \p{...} and generated tables.
|
|
15
|
+
// - Text runs don't split where the script changes (gfxScriptItemizer.cpp), nor where the bidi
|
|
16
|
+
// level does: bidi resolution makes a text frame of each level run (nsBidiPresUtils.cpp:
|
|
17
|
+
// 1037-1053), and a text run doesn't go on across two (ContinueTextRunAcrossFrames,
|
|
18
|
+
// nsTextFrame.cpp:2023-2030). Such a split only adds a cluster start, restarts clusters within
|
|
19
|
+
// its word and ends a word after a space before a cluster extender, so a level split is missed
|
|
20
|
+
// only inside a cluster, or after a space inside one: for example at a vowel killer of a
|
|
21
|
+
// left-to-right script after an Arabic letter, or at a ZWJ that ends the paragraph after a
|
|
22
|
+
// Hebrew letter, which rule L1 puts at the paragraph's level. Both ports were removed on
|
|
23
|
+
// purpose (RESEARCH.md, Decisions Log, 2026-09-24 and 2026-10-01; ENGINE_FOLLOWUPS.md, Bidi
|
|
24
|
+
// levels, direction and script runs).
|
|
25
|
+
// - Inside runs of Thai, Lao, Khmer and Myanmar letters, Intl.Segmenter word boundaries stand
|
|
26
|
+
// in for ICU4X's LSTM models (line.rs:445-451, complex/mod.rs:135-156). Firefox's own
|
|
27
|
+
// Intl.Segmenter answers as those models there once breaks inside grapheme clusters are
|
|
28
|
+
// dropped, which setPotentialLineBreaks does.
|
|
29
|
+
// - The paragraph is left-to-right, since Pretext takes no direction, and word-break is
|
|
30
|
+
// normal or keep-all under Strict line breaking, which line-break: auto selects
|
|
31
|
+
// (intl/lwbrk/LineBreaker.cpp:26-31).
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
geckoLineBreakStatesPacked,
|
|
35
|
+
geckoLineEotProperty,
|
|
36
|
+
geckoLineLastCodepointProperty,
|
|
37
|
+
geckoLinePropertyCount,
|
|
38
|
+
} from './generated/engine-break-data.js'
|
|
39
|
+
import { findGraphemeEnds, isBidiControl, } from './graphemes.js'
|
|
40
|
+
import {
|
|
41
|
+
BREAK as OPPORTUNITY,
|
|
42
|
+
CLUSTER_START,
|
|
43
|
+
DEFAULT_IGNORABLE,
|
|
44
|
+
EMOJI,
|
|
45
|
+
HANGUL,
|
|
46
|
+
MARK,
|
|
47
|
+
PUNCTUATION,
|
|
48
|
+
SOFT_HYPHEN_BREAK,
|
|
49
|
+
getBreakLanguage,
|
|
50
|
+
getClass,
|
|
51
|
+
getWordSegmenter,
|
|
52
|
+
hasProperty,
|
|
53
|
+
unpackClasses,
|
|
54
|
+
unpackTable,
|
|
55
|
+
|
|
56
|
+
} from './line-breaks.js'
|
|
57
|
+
|
|
58
|
+
const CH_SHY = 0x00ad
|
|
59
|
+
|
|
60
|
+
const isSurrogatePair = (a , b ) => (a & 0xfc00) === 0xd800 && (b & 0xfc00) === 0xdc00
|
|
61
|
+
const combine = (a , b ) => 0x10000 + ((a - 0xd800) << 10) + (b - 0xdc00)
|
|
62
|
+
|
|
63
|
+
// East_Asian_Width H (2), F (3) or W (5), and 0 for any other value, which the first East Asian
|
|
64
|
+
// segment break test unpacks (isEastAsianSegmentBreak).
|
|
65
|
+
let eastAsianWidths = null
|
|
66
|
+
|
|
67
|
+
// --- Character classes ---
|
|
68
|
+
|
|
69
|
+
// nsUnicodeProperties.h:202-207, nsUnicodeProperties.cpp:130-138
|
|
70
|
+
function isClusterExtender(cp ) {
|
|
71
|
+
return cp >= 0x0300 && (hasProperty(cp, MARK) || cp === 0x200c || cp === 0x200d || (cp >= 0xff9e && cp <= 0xff9f) ||
|
|
72
|
+
(cp >= 0x1f3fb && cp <= 0x1f3ff) || (cp >= 0xe0020 && cp <= 0xe007f))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// nsUnicodeProperties.h:211-214, nsUnicodeProperties.cpp:140-147
|
|
76
|
+
function isClusterExtenderExcludingJoiners(cp ) {
|
|
77
|
+
return cp >= 0x0300 && (hasProperty(cp, MARK) || (cp >= 0xff9e && cp <= 0xff9f) || (cp >= 0x1f3fb && cp <= 0x1f3ff) ||
|
|
78
|
+
(cp >= 0xe0020 && cp <= 0xe007f))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// UnicodeProperties.h:205-218
|
|
82
|
+
function isEastAsianWidthFHWExcludingEmoji(widths , cp ) {
|
|
83
|
+
const width = getClass(widths, cp)
|
|
84
|
+
return width === 2 || width === 3 || (width === 5 && !hasProperty(cp, EMOJI))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// nsUnicharUtils.cpp:500-504
|
|
88
|
+
function isSegmentBreakSkipChar(widths , cp ) {
|
|
89
|
+
return isEastAsianWidthFHWExcludingEmoji(widths, cp) && !hasProperty(cp, HANGUL) && cp !== 0x20a9
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// nsUnicharUtils.cpp:506-527, with UnicodeProperties.h:187-199
|
|
93
|
+
function isEastAsianPunctuation(widths , cp ) {
|
|
94
|
+
return getClass(widths, cp) !== 0 && ((hasProperty(cp, PUNCTUATION) && cp !== 0x20a9) || cp === 0xff5e || cp === 0x3000)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// gfxFontGroup::IsInvalidChar(char16_t), gfxTextRun.h:975-992. Below U+0100 it answers as
|
|
98
|
+
// IsInvalidChar(uint8_t), gfxTextRun.h:971-973.
|
|
99
|
+
function isInvalidChar(ch ) {
|
|
100
|
+
if (ch >= 0x20 && ch < 0x7f) return false
|
|
101
|
+
if (ch <= 0x9f) return true
|
|
102
|
+
return ((ch & 0xff00) === 0x2000 && (ch === 0x200b || ch === 0x2028 || ch === 0x2029 || ch === 0x2060)) ||
|
|
103
|
+
ch === 0xfeff || isBidiControl(ch)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- 1. TransformText (nsTextFrameUtils.cpp:84-401) ---
|
|
107
|
+
|
|
108
|
+
// Whether the text run leaves out a bidi control, which only 16-bit text does (IsDiscardable), is
|
|
109
|
+
// noted as it leaves them out, as IsDiscardable notes a soft hyphen (HasShy): testing the text for
|
|
110
|
+
// one before the scan costs every text a pass (RESEARCH.md, Keeping Work Bounded). `leftOut` is
|
|
111
|
+
// GeckoLineBreaks'.
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
// IsDiscardable, nsTextFrameUtils.cpp:32-49
|
|
115
|
+
export function isDiscardable(ch , is8bit ) {
|
|
116
|
+
return ch === CH_SHY || (!is8bit && isBidiControl(ch))
|
|
117
|
+
}
|
|
118
|
+
const isSpaceOrTab = (ch ) => ch === 0x20 || ch === 0x09
|
|
119
|
+
// IsSpaceOrTabOrSegmentBreak, nsTextFrameUtils.cpp:51-57: a white-space run's white space, which a CR or FF isn't.
|
|
120
|
+
export const isSpaceOrTabOrSegmentBreak = (ch ) => ch === 0x20 || ch === 0x09 || ch === 0x0a
|
|
121
|
+
|
|
122
|
+
// IsSpaceCombiningSequenceTail(const char16_t*, int32_t), nsTextFrameUtils.cpp:24-30, on code units.
|
|
123
|
+
export function isSpaceCombiningSequenceTail(text , from ) {
|
|
124
|
+
for (let i = from; i < text.length; i++) {
|
|
125
|
+
const ch = text.charCodeAt(i)
|
|
126
|
+
if (isClusterExtenderExcludingJoiners(ch)) return true
|
|
127
|
+
if (!isBidiControl(ch)) return false
|
|
128
|
+
}
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// aLangIsJapaneseOrChinese, nsTextFrameUtils.cpp:273-285, which takes only `-` after the subtag.
|
|
133
|
+
export function isJapaneseOrChinese(language ) {
|
|
134
|
+
const breakLanguage = getBreakLanguage(language)
|
|
135
|
+
return (breakLanguage === 'ja' || breakLanguage === 'zh') && language .charCodeAt(2) !== 0x5f
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Gecko's East Asian test for the white-space run [start, end) (TransformWhiteSpaces,
|
|
139
|
+
// nsTextFrameUtils.cpp:120-150): the code points before and after it, past default-ignorable
|
|
140
|
+
// ones, both segment-break skip characters, or on a `ja` or `zh` page either one East Asian
|
|
141
|
+
// punctuation. Only an interior run qualifies.
|
|
142
|
+
export function isEastAsianSegmentBreak(text , start , end , japaneseOrChinese ) {
|
|
143
|
+
if (start === 0 || end >= text.length) return false
|
|
144
|
+
const widths = eastAsianWidths ??= unpackClasses('gecko/east_asian_width')
|
|
145
|
+
let before
|
|
146
|
+
let pos = start
|
|
147
|
+
do {
|
|
148
|
+
const low = text.charCodeAt(pos - 1)
|
|
149
|
+
const high = pos > 1 ? text.charCodeAt(pos - 2) : 0
|
|
150
|
+
if (isSurrogatePair(high, low)) { before = combine(high, low); pos -= 2 } else { before = low; pos-- }
|
|
151
|
+
} while (hasProperty(before, DEFAULT_IGNORABLE) && pos > 0)
|
|
152
|
+
let after
|
|
153
|
+
pos = end
|
|
154
|
+
do {
|
|
155
|
+
after = text.codePointAt(pos)
|
|
156
|
+
pos += after > 0xffff ? 2 : 1
|
|
157
|
+
} while (hasProperty(after, DEFAULT_IGNORABLE) && pos < text.length)
|
|
158
|
+
return (isSegmentBreakSkipChar(widths, before) && isSegmentBreakSkipChar(widths, after)) ||
|
|
159
|
+
(japaneseOrChinese && (isEastAsianPunctuation(widths, before) || isEastAsianPunctuation(widths, after)))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The scan transforms a text node's text as one text frame's. Firefox transforms a frame at a
|
|
163
|
+
// time, each from the white-space state the frame before it left (mNextRunContextInfo,
|
|
164
|
+
// nsTextFrame.cpp:2515-2518), and bidi resolution splits a text node into a frame for each level
|
|
165
|
+
// run (nsBidiPresUtils.cpp:1037-1053). A frame that starts at a character the text run drops ends
|
|
166
|
+
// the white-space run there (nsTextFrameUtils.cpp:370-379), and a line's start trims the white
|
|
167
|
+
// space a frame starts with while the line holds nothing (nsTextFrame.cpp:10904-10944). A
|
|
168
|
+
// dropped character starts a frame only where it is a bidi control that starts a level run, and
|
|
169
|
+
// whether one does turns on the paragraph's direction, which Pretext doesn't take: after a
|
|
170
|
+
// space, U+200F between Latin words starts one in a left-to-right paragraph and none in a
|
|
171
|
+
// right-to-left one, and U+200E between Hebrew words the other way around. So a white-space run
|
|
172
|
+
// reads through every character the text run drops. That is Firefox's run wherever the control
|
|
173
|
+
// keeps the level of the white space before it, as a mark of the paragraph's own direction does
|
|
174
|
+
// outside an embedding or isolate; ENGINE_FOLLOWUPS.md, White space and controls, has the rest.
|
|
175
|
+
// A rich-inline paragraph's items are text frames too: `frameStarts` has where each starts in the
|
|
176
|
+
// input, in order, or is null for one text. A white-space run goes on from one frame into white
|
|
177
|
+
// space that starts the next (INCOMING_WHITESPACE, :286, :382-386), whose own scan ends with its
|
|
178
|
+
// frame, and a dropped character that starts a frame ends it, as it follows no white space in
|
|
179
|
+
// that frame. In 16px Arial at 56px Firefox fits `see this` of items `see`, ` \u00AD`,
|
|
180
|
+
// ` this word` on a 55.15px line, and not of `see `, `\u00AD `, `this word`, whose text in one node
|
|
181
|
+
// it fits.
|
|
182
|
+
function transformText(input , is8bit , preserveWhiteSpace , frameStarts ) {
|
|
183
|
+
const len = input.length
|
|
184
|
+
// The source index of each kept unit.
|
|
185
|
+
const orig = new Int32Array(len)
|
|
186
|
+
const skipped = new Uint8Array(len)
|
|
187
|
+
let leftOut = null
|
|
188
|
+
let n = 0
|
|
189
|
+
let dropsBidiControl = false
|
|
190
|
+
if (preserveWhiteSpace) {
|
|
191
|
+
// COMPRESS_NONE, nsTextFrameUtils.cpp:222-271
|
|
192
|
+
for (let i = 0; i < len; i++) {
|
|
193
|
+
const ch = input.charCodeAt(i)
|
|
194
|
+
if (!isDiscardable(ch, is8bit)) {
|
|
195
|
+
orig[n++] = i
|
|
196
|
+
} else {
|
|
197
|
+
skipped[i] = 1
|
|
198
|
+
dropsBidiControl ||= ch !== CH_SHY
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
// COMPRESS_WHITESPACE_NEWLINE, :272-387
|
|
203
|
+
let inWhitespace = false
|
|
204
|
+
let i = 0
|
|
205
|
+
// Where the frame a white-space run starts in ends, the next frame start past the run's start:
|
|
206
|
+
// the input's end for one text. Only a run reads it, so it is found where one starts.
|
|
207
|
+
let frame = 0
|
|
208
|
+
let frameEnd = frameStarts === null ? len : 0
|
|
209
|
+
while (i < len) {
|
|
210
|
+
const ch = input.charCodeAt(i)
|
|
211
|
+
if (!isSpaceOrTabOrSegmentBreak(ch) && !isDiscardable(ch, is8bit)) {
|
|
212
|
+
// A CR or FF is no white space to TransformText (:51-57), so it ends a white-space run and
|
|
213
|
+
// stays in the text run, which gives it no glyph and no advance: it skips a control
|
|
214
|
+
// character it doesn't draw as a hexbox, which is never CR and, in release and beta builds,
|
|
215
|
+
// no other one either (gfxFont::SplitAndInitTextRun, gfxFont.cpp:3620-3627 and :3874-3892;
|
|
216
|
+
// layout.css.control-characters.visible, StaticPrefList.yaml:10926-10929). Every Canvas
|
|
217
|
+
// measures it as a space (Firefox's: CanvasRenderingContext2D.cpp:4634-4637), so a layout
|
|
218
|
+
// of the source leaves it out. Kept as a control of no advance, as the profile keeps the
|
|
219
|
+
// other controls Firefox hides (hidesControlCharacters), it is text at a line's edges,
|
|
220
|
+
// where Firefox trims it, and takes text with CRLF off the simple line walk (RESEARCH.md,
|
|
221
|
+
// Dead Ends). A CR right before a line feed needs no mark: the run that starts there keeps
|
|
222
|
+
// that line feed, and the CR, a breakable space to the scan too, collapses into it.
|
|
223
|
+
if (ch === 0x0c || (ch === 0x0d && input.charCodeAt(i + 1) !== 0x0a)) (leftOut ??= new Uint8Array(len))[i] = 1
|
|
224
|
+
orig[n++] = i
|
|
225
|
+
inWhitespace = false
|
|
226
|
+
i++
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
if (isSpaceOrTabOrSegmentBreak(ch)) {
|
|
230
|
+
if (i >= frameEnd) {
|
|
231
|
+
while (frame < frameStarts .length && frameStarts [frame] <= i) frame++
|
|
232
|
+
frameEnd = frame < frameStarts .length ? frameStarts [frame] : len
|
|
233
|
+
}
|
|
234
|
+
let keepLastSpace = false
|
|
235
|
+
let hasSegmentBreak = ch === 0x0a
|
|
236
|
+
let trailingDiscardables = 0
|
|
237
|
+
// How many characters the run holds that the text run drops.
|
|
238
|
+
let dropped = 0
|
|
239
|
+
// A run that goes on from the frame before, after the characters that frame's run dropped at
|
|
240
|
+
// its end, leaves out white space that doesn't touch the unit the run kept.
|
|
241
|
+
const goesOnPastDropped = inWhitespace && i > 0 && isDiscardable(input.charCodeAt(i - 1), is8bit)
|
|
242
|
+
let j = i + 1
|
|
243
|
+
for (; j < frameEnd; j++) {
|
|
244
|
+
const c = input.charCodeAt(j)
|
|
245
|
+
if (c === 0x0a) hasSegmentBreak = true
|
|
246
|
+
else if (isDiscardable(c, is8bit)) dropped++
|
|
247
|
+
else if (!isSpaceOrTab(c)) break
|
|
248
|
+
}
|
|
249
|
+
while (isDiscardable(input.charCodeAt(j - 1), is8bit)) { j--; trailingDiscardables++ } // :334-336
|
|
250
|
+
if (!is8bit && input.charCodeAt(j - 1) === 0x20 && j < frameEnd && isSpaceCombiningSequenceTail(input, j)) { keepLastSpace = true; j-- } // :339-345
|
|
251
|
+
// TransformWhiteSpaces over [i, j), :84-209. The runs it deletes whole, next to a ZWSP or
|
|
252
|
+
// between East Asian characters (:120-150), are gone already (removeSkippableSegmentBreaks in
|
|
253
|
+
// src/analysis.ts), so a run keeps one space. A space or tab in a run with a segment break
|
|
254
|
+
// goes (:152-162), and the run's first segment break stays as a space (:181-193).
|
|
255
|
+
for (let k = i; k < j; k++) {
|
|
256
|
+
const c = input.charCodeAt(k)
|
|
257
|
+
if (isDiscardable(c, is8bit)) {
|
|
258
|
+
skipped[k] = 1
|
|
259
|
+
dropsBidiControl ||= c !== CH_SHY
|
|
260
|
+
} else if (inWhitespace || (hasSegmentBreak && isSpaceOrTab(c))) {
|
|
261
|
+
skipped[k] = 1
|
|
262
|
+
// Where the run reads through a dropped character, white space it leaves out may not
|
|
263
|
+
// touch the unit it keeps, so collapsing adjacent white space wouldn't drop it.
|
|
264
|
+
if (dropped > trailingDiscardables || goesOnPastDropped) (leftOut ??= new Uint8Array(len))[k] = 1
|
|
265
|
+
} else {
|
|
266
|
+
orig[n++] = k
|
|
267
|
+
inWhitespace = true
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (keepLastSpace) orig[n++] = j++
|
|
271
|
+
for (let k = 0; k < trailingDiscardables; k++) {
|
|
272
|
+
dropsBidiControl ||= input.charCodeAt(j) !== CH_SHY
|
|
273
|
+
skipped[j++] = 1
|
|
274
|
+
}
|
|
275
|
+
i = j
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
278
|
+
skipped[i] = 1 // :370-379
|
|
279
|
+
dropsBidiControl ||= ch !== CH_SHY
|
|
280
|
+
inWhitespace = false
|
|
281
|
+
i++
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// The kept units, as slices of the input where they run unchanged: in collapsed white space, a kept
|
|
285
|
+
// tab or segment break becomes a space.
|
|
286
|
+
let text = ''
|
|
287
|
+
let sliceStart = 0
|
|
288
|
+
let sliceEnd = 0
|
|
289
|
+
for (let k = 0; k < n; k++) {
|
|
290
|
+
const i = orig[k]
|
|
291
|
+
const ch = input.charCodeAt(i)
|
|
292
|
+
if (!preserveWhiteSpace && (ch === 0x09 || ch === 0x0a)) {
|
|
293
|
+
text += input.slice(sliceStart, sliceEnd) + ' '
|
|
294
|
+
sliceStart = sliceEnd = i + 1
|
|
295
|
+
} else if (i !== sliceEnd) {
|
|
296
|
+
text += input.slice(sliceStart, sliceEnd)
|
|
297
|
+
sliceStart = i
|
|
298
|
+
sliceEnd = i + 1
|
|
299
|
+
} else {
|
|
300
|
+
sliceEnd++
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
text += input.slice(sliceStart, sliceEnd)
|
|
304
|
+
return { text, orig: orig.subarray(0, n), skipped, leftOut, dropsBidiControl }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// IsTrimmableSpace, nsTextFrame.cpp:921-942, in normal white space.
|
|
308
|
+
function isTrimmableSpace(source , pos , is8bit ) {
|
|
309
|
+
switch (source.charCodeAt(pos)) {
|
|
310
|
+
case 0x20: case 0x1680: return is8bit || !isSpaceCombiningSequenceTail(source, pos + 1)
|
|
311
|
+
case 0x0a: case 0x09: case 0x0d: case 0x0c: return true
|
|
312
|
+
default: return false
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// HasCompressedLeadingWhitespace, nsTextFrame.cpp:2869-2887
|
|
317
|
+
function hasCompressedLeadingWhitespace(source , skipped , is8bit , preserveWhiteSpace ) {
|
|
318
|
+
if (preserveWhiteSpace || skipped[0] === 0) return false
|
|
319
|
+
for (let k = 0; k < source.length && skipped[k] === 1; k++) if (isTrimmableSpace(source, k, is8bit)) return true
|
|
320
|
+
return false
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// --- 2. Text-run glyph records (gfxTextRun.cpp, gfxFont.cpp) ---
|
|
324
|
+
//
|
|
325
|
+
// Only the facts SetPotentialLineBreaks reads are recorded: which positions start a cluster and
|
|
326
|
+
// which are spaces. Every position belongs to one shaped word, space or invalid character.
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
// CompressedGlyph::MakeComplex(false, true), gfxFont.cpp:716
|
|
331
|
+
function extendCluster(g , i ) {
|
|
332
|
+
g.clusterStart[i] = 0
|
|
333
|
+
g.isSpace[i] = 0
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// gfxShapedText::SetupClusterBoundaries(uint32_t, const char16_t*, uint32_t), gfxFont.cpp:708-769,
|
|
337
|
+
// for one shaped word, without the emergency wraps after hyphens. GraphemeClusterBreakIteratorUtf16
|
|
338
|
+
// reads the word alone (intl/lwbrk/Segmenter.cpp:174-187). Below U+0300 only CR and LF share a
|
|
339
|
+
// cluster, which the generator checks, and words hold neither, so a word of such units has a
|
|
340
|
+
// cluster at every unit. `ends` has room for the word's clusters.
|
|
341
|
+
function setupClusterBoundaries(g , text , from , to , graphemeTable , ends ) {
|
|
342
|
+
let ch0 = text.charCodeAt(from)
|
|
343
|
+
if (to - from > 1 && isSurrogatePair(ch0, text.charCodeAt(from + 1))) ch0 = combine(ch0, text.charCodeAt(from + 1))
|
|
344
|
+
if (isClusterExtender(ch0)) extendCluster(g, from)
|
|
345
|
+
let low = from
|
|
346
|
+
while (low < to && text.charCodeAt(low) < 0x300) low++
|
|
347
|
+
const count = low === to ? 0 : findGraphemeEnds(graphemeTable, text, from, to, ends)
|
|
348
|
+
for (let k = 0, pos = from; pos < to; k++) {
|
|
349
|
+
const ch = text.charCodeAt(pos)
|
|
350
|
+
if (ch === 0x20 || ch === 0x3000) g.isSpace[pos] = 1
|
|
351
|
+
else if (ch === 0x09af && pos > from && text.charCodeAt(pos - 1) === 0x09cd) extendCluster(g, pos) // BENGALI_YA after BENGALI_VIRAMA
|
|
352
|
+
const end = count === 0 ? pos + 1 : ends[k]
|
|
353
|
+
for (pos++; pos < end; pos++) extendCluster(g, pos)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// gfxFont::SplitAndInitTextRun, gfxFont.cpp:3707-3900: sets up the shaped words of the text run. A
|
|
358
|
+
// word ends at a space or NBSP before no cluster extender (IsBoundarySpace, :3317-3330) or at an
|
|
359
|
+
// invalid character (:3778-3790). A space glyph (SetSpaceGlyphIfSimple, gfxTextRun.cpp:1612-1619)
|
|
360
|
+
// only sets isSpace, and an invalid character (:3877-3892) keeps a zero record. Below U+0100,
|
|
361
|
+
// IsBoundarySpace and SetupClusterBoundaries(uint8_t) (gfxFont.cpp:771-795) answer as the char16_t
|
|
362
|
+
// versions, so 8-bit text and 8-bit words take the char16_t path, at any word length (:3569-3577,
|
|
363
|
+
// :3817-3821).
|
|
364
|
+
function splitAndInitTextRun(g , text , graphemeTable , ends ) {
|
|
365
|
+
const end = text.length
|
|
366
|
+
let wordStart = 0
|
|
367
|
+
for (let i = 0; i < end; i++) {
|
|
368
|
+
const ch = text.charCodeAt(i)
|
|
369
|
+
const boundary = (ch === 0x20 || ch === 0xa0) && !(i + 1 < end && isClusterExtender(text.charCodeAt(i + 1)))
|
|
370
|
+
if (!boundary && !isInvalidChar(ch)) continue
|
|
371
|
+
if (i > wordStart) setupClusterBoundaries(g, text, wordStart, i, graphemeTable, ends)
|
|
372
|
+
if (ch === 0x20) g.isSpace[i] = 1
|
|
373
|
+
wordStart = i + 1
|
|
374
|
+
}
|
|
375
|
+
if (end > wordStart) setupClusterBoundaries(g, text, wordStart, end, graphemeTable, ends)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// --- 3. ICU4X 2.1.2's line iterator for one word (icu_segmenter src/line.rs) ---
|
|
379
|
+
|
|
380
|
+
// Firefox's line data, which the first scan unpacks (getGeckoLineBreaks).
|
|
381
|
+
|
|
382
|
+
let lineData = null
|
|
383
|
+
|
|
384
|
+
// The Line_Break value, with error value 0 above U+10FFFF.
|
|
385
|
+
function getLineBreakClass(line , c ) {
|
|
386
|
+
return c > 0x10ffff ? 0 : getClass(line.classes, c)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Line_Break property values of the data (line.rs:18-128).
|
|
390
|
+
const AI = 1, AL = 3, BK = 10, CJ = 12, CM = 14, CR = 16, H2 = 21, H3 = 22, HY = 24, ID = 25, JL = 29,
|
|
391
|
+
JT = 30, JV = 31, LF = 32, NL = 33, NU = 35, SA = 46, SP = 47, ZW = 53, ZWJ = 54
|
|
392
|
+
// BreakState bytes (provider/mod.rs:288-310).
|
|
393
|
+
const BREAK = 253, NO_MATCH = 254, KEEP = 255, INTERMEDIATE = 120
|
|
394
|
+
|
|
395
|
+
// LanguageIteratorUtf16 (complex/language.rs:17-45, 78-102) reads single code units: Thai, Lao,
|
|
396
|
+
// Burmese, Khmer, or 0 for Unknown, and Chinese and Japanese, for which the LSTM segmenter has no
|
|
397
|
+
// model (complex/mod.rs:161-180).
|
|
398
|
+
function getComplexLanguage(u ) {
|
|
399
|
+
if (u >= 0xe01 && u <= 0xe7f) return 1
|
|
400
|
+
if (u >= 0xe80 && u <= 0xeff) return 2
|
|
401
|
+
if ((u >= 0x1000 && u <= 0x109f) || (u >= 0xa9e0 && u <= 0xa9ff) || (u >= 0xaa60 && u <= 0xaa7f)) return 3
|
|
402
|
+
if ((u >= 0x1780 && u <= 0x17ff) || (u >= 0x19e0 && u <= 0x19ff)) return 4
|
|
403
|
+
return 0
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// complex_language_segment_utf16 (complex/mod.rs:135-156): splits a run of SA code units by
|
|
407
|
+
// language, and Intl.Segmenter words supply boundaries inside each Thai, Lao, Burmese and Khmer
|
|
408
|
+
// slice. Every slice reports its end; other languages report nothing else (:149-151).
|
|
409
|
+
function segmentComplex(units ) {
|
|
410
|
+
const result = []
|
|
411
|
+
for (let i = 0; i < units.length;) {
|
|
412
|
+
const language = getComplexLanguage(units[i] )
|
|
413
|
+
let j = i + 1
|
|
414
|
+
while (j < units.length && getComplexLanguage(units[j] ) === language) j++
|
|
415
|
+
if (language !== 0) {
|
|
416
|
+
let slice = ''
|
|
417
|
+
for (let k = i; k < j; k += 4096) slice += String.fromCharCode(...units.slice(k, Math.min(j, k + 4096)))
|
|
418
|
+
for (const part of getWordSegmenter().segment(slice)) if (part.index > 0) result.push(i + part.index)
|
|
419
|
+
}
|
|
420
|
+
result.push(j)
|
|
421
|
+
i = j
|
|
422
|
+
}
|
|
423
|
+
return result
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// KeepAll pairs (line.rs:901-907).
|
|
427
|
+
function isKeepAllLetter(p ) {
|
|
428
|
+
return p === AI || p === AL || p === ID || p === NU || p === HY || p === H2 || p === H3 || p === JL ||
|
|
429
|
+
p === JV || p === JT || p === CJ
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Utf16Indices::next (indices.rs:58-83): the code point at text[i], in a pair that ends before `end`,
|
|
433
|
+
// or an unpaired surrogate as a code point of its own.
|
|
434
|
+
function readCodePoint(text , i , end ) {
|
|
435
|
+
const c = text.charCodeAt(i)
|
|
436
|
+
if ((c & 0xfc00) === 0xd800 && i + 1 < end) {
|
|
437
|
+
const next = text.charCodeAt(i + 1)
|
|
438
|
+
if ((next & 0xfc00) === 0xdc00) return ((c & 0x3ff) << 10) + (next & 0x3ff) + 0x10000
|
|
439
|
+
}
|
|
440
|
+
return c
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// LineBreakIterator (line.rs:820-1130) over the word text[start, end), drained: state[start + p] = 1
|
|
444
|
+
// at each break p inside the word that Iterator::next returns (:833-1067), which starts at the
|
|
445
|
+
// word's first code point after the break at 0 and stops at the word's end. The position is
|
|
446
|
+
// Utf16Indices' front_offset and current_pos_data (:821-823): `pos` is the current code point's
|
|
447
|
+
// offset in the word, `cp` its code point and `front` the next one's offset, and `pos` reaches
|
|
448
|
+
// the word's length past its end.
|
|
449
|
+
function markWordBreaks(line , text , start , end , keepAll , state ) {
|
|
450
|
+
const len = end - start
|
|
451
|
+
const states = line.states
|
|
452
|
+
let pos = 0
|
|
453
|
+
let cp = readCodePoint(text, start, end)
|
|
454
|
+
let front = cp > 0xffff ? 2 : 1
|
|
455
|
+
// Break points cached by a complex-script run, each less `cacheOffset`, from `cacheAt`.
|
|
456
|
+
let cache = []
|
|
457
|
+
let cacheAt = 0
|
|
458
|
+
let cacheOffset = 0
|
|
459
|
+
|
|
460
|
+
for (;;) {
|
|
461
|
+
// Break points cached by a complex-script run (:840-855).
|
|
462
|
+
if (cacheAt < cache.length) {
|
|
463
|
+
const firstPos = cache[cacheAt++] - cacheOffset
|
|
464
|
+
let i = 0
|
|
465
|
+
while (i !== firstPos) {
|
|
466
|
+
i += cp > 0xffff ? 2 : 1 // Utf16::char_len (rule_segmenter.rs:335-341)
|
|
467
|
+
pos = front
|
|
468
|
+
if (pos >= len) return
|
|
469
|
+
cp = readCodePoint(text, start + pos, end)
|
|
470
|
+
front += cp > 0xffff ? 2 : 1
|
|
471
|
+
}
|
|
472
|
+
cacheOffset += i
|
|
473
|
+
state[start + pos] = 1
|
|
474
|
+
continue
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
let lb9Left = -1 // (:858)
|
|
478
|
+
let lb8aAfterLb9 = false // (:861)
|
|
479
|
+
outer: for (;;) {
|
|
480
|
+
const leftCodepoint = cp
|
|
481
|
+
const leftProp = lb9Left >= 0 ? lb9Left : getLineBreakClass(line, leftCodepoint)
|
|
482
|
+
const afterZwj = lb8aAfterLb9 || (lb9Left < 0 && leftProp === ZWJ)
|
|
483
|
+
pos = front
|
|
484
|
+
if (pos >= len) return
|
|
485
|
+
cp = readCodePoint(text, start + pos, end)
|
|
486
|
+
front += cp > 0xffff ? 2 : 1
|
|
487
|
+
const rightProp = getLineBreakClass(line, cp)
|
|
488
|
+
|
|
489
|
+
// LB9 (:878-893)
|
|
490
|
+
if ((rightProp === CM || rightProp === ZWJ) && leftProp !== BK && leftProp !== CR && leftProp !== LF &&
|
|
491
|
+
leftProp !== NL && leftProp !== SP && leftProp !== ZW) {
|
|
492
|
+
lb9Left = leftProp
|
|
493
|
+
lb8aAfterLb9 = rightProp === ZWJ
|
|
494
|
+
continue
|
|
495
|
+
}
|
|
496
|
+
lb9Left = -1
|
|
497
|
+
lb8aAfterLb9 = false
|
|
498
|
+
|
|
499
|
+
// CSS word-break (:896-909)
|
|
500
|
+
if (keepAll && isKeepAllLetter(leftProp) && isKeepAllLetter(rightProp)) continue
|
|
501
|
+
|
|
502
|
+
// Complex scripts (:941-950), Utf16 line_handle_complex_language (:1263-1316), which counts
|
|
503
|
+
// code points one unit each: they are truncated to u16 there.
|
|
504
|
+
if (getLineBreakClass(line, leftCodepoint) === SA && rightProp === SA) {
|
|
505
|
+
const units = [leftCodepoint & 0xffff]
|
|
506
|
+
for (let p = pos, c = cp; ;) {
|
|
507
|
+
units.push(c & 0xffff)
|
|
508
|
+
p += c > 0xffff ? 2 : 1
|
|
509
|
+
if (p >= len) break
|
|
510
|
+
c = readCodePoint(text, start + p, end)
|
|
511
|
+
if (getLineBreakClass(line, c) !== SA) break
|
|
512
|
+
}
|
|
513
|
+
cache = segmentComplex(units)
|
|
514
|
+
cacheAt = 1
|
|
515
|
+
let i = 1
|
|
516
|
+
while (i !== cache[0]) {
|
|
517
|
+
i++
|
|
518
|
+
pos = front
|
|
519
|
+
if (pos >= len) return
|
|
520
|
+
cp = readCodePoint(text, start + pos, end)
|
|
521
|
+
front += cp > 0xffff ? 2 : 1
|
|
522
|
+
}
|
|
523
|
+
cacheOffset = i
|
|
524
|
+
break
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const breakState = states[leftProp * geckoLinePropertyCount + rightProp] // (:702-706, 953)
|
|
528
|
+
if (breakState === BREAK || breakState === NO_MATCH) {
|
|
529
|
+
if (afterZwj) continue
|
|
530
|
+
break
|
|
531
|
+
}
|
|
532
|
+
if (breakState === KEEP) continue
|
|
533
|
+
|
|
534
|
+
let index = breakState >= INTERMEDIATE ? breakState - INTERMEDIATE : breakState
|
|
535
|
+
let prevFront = front
|
|
536
|
+
let prevPos = pos
|
|
537
|
+
let prevCp = cp
|
|
538
|
+
let previousIsAfterZwj = afterZwj
|
|
539
|
+
let leftPropPreLb9 = rightProp
|
|
540
|
+
// (:976-981), where lb8a_after_lb9 is false once LB9 has passed.
|
|
541
|
+
const isIntermediateRuleNoMatch = index > geckoLineLastCodepointProperty
|
|
542
|
+
|
|
543
|
+
for (;;) {
|
|
544
|
+
const innerAfterZwj = leftPropPreLb9 === ZWJ
|
|
545
|
+
const previousBreakStateIsCpProp = index <= geckoLineLastCodepointProperty
|
|
546
|
+
pos = front
|
|
547
|
+
if (pos >= len) { // (:990-1007)
|
|
548
|
+
if (states[index * geckoLinePropertyCount + geckoLineEotProperty] !== NO_MATCH) return
|
|
549
|
+
front = prevFront; pos = prevPos; cp = prevCp
|
|
550
|
+
if (previousIsAfterZwj) continue outer
|
|
551
|
+
break outer
|
|
552
|
+
}
|
|
553
|
+
cp = readCodePoint(text, start + pos, end)
|
|
554
|
+
front += cp > 0xffff ? 2 : 1
|
|
555
|
+
const prop = getLineBreakClass(line, cp)
|
|
556
|
+
|
|
557
|
+
if ((prop === CM || prop === ZWJ) && leftPropPreLb9 !== BK && leftPropPreLb9 !== CR && leftPropPreLb9 !== LF &&
|
|
558
|
+
leftPropPreLb9 !== NL && leftPropPreLb9 !== SP && leftPropPreLb9 !== ZW) { // (:1009-1019)
|
|
559
|
+
leftPropPreLb9 = prop
|
|
560
|
+
continue
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const next = states[index * geckoLinePropertyCount + prop] // (:1021-1061)
|
|
564
|
+
if (next === KEEP) continue outer
|
|
565
|
+
if (next === NO_MATCH) {
|
|
566
|
+
front = prevFront; pos = prevPos; cp = prevCp
|
|
567
|
+
if (innerAfterZwj) {
|
|
568
|
+
if (isIntermediateRuleNoMatch && !previousIsAfterZwj) break outer
|
|
569
|
+
continue outer
|
|
570
|
+
}
|
|
571
|
+
if (previousIsAfterZwj) continue outer
|
|
572
|
+
break outer
|
|
573
|
+
}
|
|
574
|
+
if (next === BREAK) {
|
|
575
|
+
if (innerAfterZwj) continue outer
|
|
576
|
+
break outer
|
|
577
|
+
}
|
|
578
|
+
if (next >= INTERMEDIATE) {
|
|
579
|
+
index = next - INTERMEDIATE
|
|
580
|
+
prevFront = front; prevPos = pos; prevCp = cp
|
|
581
|
+
previousIsAfterZwj = innerAfterZwj
|
|
582
|
+
} else {
|
|
583
|
+
index = next
|
|
584
|
+
if (previousBreakStateIsCpProp) {
|
|
585
|
+
prevFront = front; prevPos = pos; prevCp = cp
|
|
586
|
+
previousIsAfterZwj = innerAfterZwj
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
leftPropPreLb9 = prop
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
state[start + pos] = 1
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// --- 4. nsLineBreaker (dom/base/nsLineBreaker.cpp) ---
|
|
597
|
+
|
|
598
|
+
// kNonBreakableASCII, nsLineBreaker.cpp:33-48
|
|
599
|
+
const NON_BREAKABLE_ASCII = new Uint8Array([
|
|
600
|
+
0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0,
|
|
601
|
+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
|
|
602
|
+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
|
603
|
+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1,
|
|
604
|
+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
|
605
|
+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0,
|
|
606
|
+
])
|
|
607
|
+
|
|
608
|
+
// The break states of one text node: nsLineBreaker::AppendText with flags 0 while no word is
|
|
609
|
+
// buffered (16-bit :235-400, 8-bit :505-650), after AppendInvisibleWhitespace when leading white
|
|
610
|
+
// space was compressed (nsTextFrame.cpp:2978-2981, nsLineBreaker.cpp:695-708), then the last
|
|
611
|
+
// word's FlushCurrentWord from Reset (nsLineBreaker.cpp:134-226, 710-720). Each word of more than
|
|
612
|
+
// ASCII letters goes to LineBreaker::ComputeBreakPositions (intl/lwbrk/LineBreaker.cpp:112-194),
|
|
613
|
+
// which keeps the state before its first unit (AutoRestore, :342, :604; skipSet = 1, :200-206).
|
|
614
|
+
function getBreakStates(line , text , is8bit , afterLeadingWhitespace , keepAll ) {
|
|
615
|
+
const len = text.length
|
|
616
|
+
const state = new Uint8Array(len)
|
|
617
|
+
let afterBreakableSpace = afterLeadingWhitespace
|
|
618
|
+
let wordStart = 0
|
|
619
|
+
let wordMightBeBreakable = false
|
|
620
|
+
for (let offset = 0; offset <= len; offset++) {
|
|
621
|
+
const ch = offset < len ? text.charCodeAt(offset) : -1
|
|
622
|
+
// nsLineBreaker::IsSegmentSpace, nsLineBreaker.h:260-264
|
|
623
|
+
const isSpace = ch === 0x20 || ch === 0x09 || ch === 0x0d
|
|
624
|
+
if (afterBreakableSpace && !isSpace && ch >= 0) state[offset] = 1
|
|
625
|
+
afterBreakableSpace = isSpace
|
|
626
|
+
if (ch >= 0 && !isSpace && (is8bit || ch !== 0x0a)) { // :332, :595
|
|
627
|
+
// IsNonBreakableChar, nsLineBreaker.cpp:50-56
|
|
628
|
+
if (!(ch >= 0x20 && ch <= 0x7f && NON_BREAKABLE_ASCII[ch - 0x20] === 1)) wordMightBeBreakable = true
|
|
629
|
+
continue
|
|
630
|
+
}
|
|
631
|
+
if (offset > wordStart && wordMightBeBreakable) markWordBreaks(line, text, wordStart, offset, keepAll, state)
|
|
632
|
+
wordMightBeBreakable = false
|
|
633
|
+
wordStart = offset + 1
|
|
634
|
+
}
|
|
635
|
+
return state
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Where a line may start in a text node's source, after the segment break transformation that
|
|
639
|
+
// removeSkippableSegmentBreaks applies: a BREAK at flags[i] for 0 < i < source.length, at a normal
|
|
640
|
+
// break (FLAG_BREAK_TYPE_NORMAL) or after a soft hyphen. gfxTextRun::SetPotentialLineBreaks
|
|
641
|
+
// (gfxTextRun.cpp:210-236) keeps a break only at a cluster start or after a space. A normal break
|
|
642
|
+
// right after a soft hyphen adds SOFT_HYPHEN_BREAK: BreakAndMeasureText takes it as the normal
|
|
643
|
+
// break, which neither fits nor draws a hyphen (gfxTextRun.cpp:1053-1063). A CLUSTER_START marks
|
|
644
|
+
// where a cluster starts without a break, where only break-word can wrap (gfxTextRun.cpp:1068-1074).
|
|
645
|
+
// The breaks, whether the text run left out a bidi control, and 1 at each unit that a layout of
|
|
646
|
+
// the source leaves out where collapsing each run of adjacent white space wouldn't, or null where
|
|
647
|
+
// there is none: white space the text run collapsed in a run that read past a character it drops,
|
|
648
|
+
// and, in normal white space, an FF or a CR that no line feed follows, which the text run keeps
|
|
649
|
+
// with no advance (transformText).
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
export function getGeckoLineBreaks(
|
|
653
|
+
source ,
|
|
654
|
+
preserveWhiteSpace ,
|
|
655
|
+
keepAll ,
|
|
656
|
+
graphemeTable ,
|
|
657
|
+
// Where each item of a rich-inline paragraph starts in the source, or null for one text (transformText).
|
|
658
|
+
frameStarts = null,
|
|
659
|
+
) {
|
|
660
|
+
const len = source.length
|
|
661
|
+
const flags = new Uint8Array(len + 1)
|
|
662
|
+
if (len === 0) return { breaks: flags, leftOut: null, dropsBidiControl: false }
|
|
663
|
+
let is8bit = true
|
|
664
|
+
for (let i = 0; i < len && is8bit; i++) is8bit = source.charCodeAt(i) < 0x100
|
|
665
|
+
// CharacterDataBuffer::SetTo stores 1b text when every unit is below 256 (CharacterDataBuffer.cpp:235-286).
|
|
666
|
+
const tr = transformText(source, is8bit, preserveWhiteSpace, frameStarts)
|
|
667
|
+
const n = tr.text.length
|
|
668
|
+
if (n === 0) return { breaks: flags, leftOut: null, dropsBidiControl: tr.dropsBidiControl }
|
|
669
|
+
|
|
670
|
+
// The text run is built before the break sinks are set up (nsTextFrame.cpp:2860-2864), in one
|
|
671
|
+
// piece: Firefox splits it where the bidi level changes, which the scan leaves out (above).
|
|
672
|
+
const g = { clusterStart: new Uint8Array(n).fill(1), isSpace: new Uint8Array(n) }
|
|
673
|
+
splitAndInitTextRun(g, tr.text, graphemeTable, new Int32Array(n))
|
|
674
|
+
|
|
675
|
+
const line = lineData ??= { classes: unpackClasses('gecko/line'), states: unpackTable(geckoLineBreakStatesPacked) }
|
|
676
|
+
const state = getBreakStates(line, tr.text, is8bit, hasCompressedLeadingWhitespace(source, tr.skipped, is8bit, preserveWhiteSpace), keepAll)
|
|
677
|
+
for (let t = 1; t < n; t++) {
|
|
678
|
+
const rawPos = tr.orig[t]
|
|
679
|
+
const normal = state[t] === 1 && (g.clusterStart[t] === 1 || g.isSpace[t - 1] === 1)
|
|
680
|
+
const afterSoftHyphen = tr.skipped[rawPos - 1] === 1 && source.charCodeAt(rawPos - 1) === CH_SHY
|
|
681
|
+
if (normal || afterSoftHyphen) {
|
|
682
|
+
flags[rawPos] = normal && afterSoftHyphen ? OPPORTUNITY | SOFT_HYPHEN_BREAK : OPPORTUNITY
|
|
683
|
+
} else if (g.clusterStart[t] === 1) {
|
|
684
|
+
flags[rawPos] = CLUSTER_START
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return { breaks: flags, leftOut: tr.leftOut, dropsBidiControl: tr.dropsBidiControl }
|
|
688
|
+
}
|