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,892 @@
|
|
|
1
|
+
/*! Pretext Unreleased (bac1774c0) | MIT; BSD/Unicode/ICU notices in LICENSE | Copyright (c) 2026 Pretext contributors */
|
|
2
|
+
// Break opportunities as Chrome and Safari find them: ports of Blink's and WebKit's
|
|
3
|
+
// line-break scans over their own pair tables and ICU line rules, which
|
|
4
|
+
// scripts/generate-engine-break-data.ts writes to src/generated/engine-break-data.ts.
|
|
5
|
+
// The tables' bundle cost is accepted (RESEARCH.md, Decisions Log, 2026-09-23).
|
|
6
|
+
//
|
|
7
|
+
// Sources, cited as file:line:
|
|
8
|
+
// - ICU 78.2 as vendored in Chromium 152, under third_party/icu/source/common. Chrome
|
|
9
|
+
// 153 runs the same code. apple-rbbi.cpp and apple-brkiter.cpp are Apple's
|
|
10
|
+
// ICU-76142.5.1.200 sources, behind libicucore 78.1 on macOS 26.5.2 and macOS 27.
|
|
11
|
+
// - Blink in Chromium 152, under third_party/blink/renderer/platform/text/:
|
|
12
|
+
// tbi.cc = text_break_iterator.cc, tbi.h = text_break_iterator.h,
|
|
13
|
+
// tbi_icu.cc = text_break_iterator_icu.cc, gen.cc = character_property_data_generator.cc.
|
|
14
|
+
// - WebKit safari-7625.1.29.11-branch (Safari 27.0), under Source/. Only Safari 27's rules
|
|
15
|
+
// are ported, not Safari 26's (RESEARCH.md, Decisions Log, 2026-09-16):
|
|
16
|
+
// BP.h = WebCore/rendering/BreakablePositions.h,
|
|
17
|
+
// IIB = WebCore/layout/formattingContexts/inline/InlineItemsBuilder.cpp,
|
|
18
|
+
// IFU = WebCore/layout/formattingContexts/inline/InlineFormattingUtils.cpp,
|
|
19
|
+
// TU = WebCore/layout/formattingContexts/inline/text/TextUtil.cpp,
|
|
20
|
+
// TBI.h = WTF/wtf/text/TextBreakIterator.h, TBIICU.h = WTF/wtf/text/icu/TextBreakIteratorICU.h.
|
|
21
|
+
//
|
|
22
|
+
// Deliberate differences:
|
|
23
|
+
// - ICU runs forward from the start of the text, without its boundary cache, reverse
|
|
24
|
+
// table or dictionary engines. Inside runs of Thai, Lao, Khmer and Myanmar letters,
|
|
25
|
+
// Intl.Segmenter word boundaries stand in for the dictionaries.
|
|
26
|
+
// - Blink restarts ICU at each line start, which drops the context before the line
|
|
27
|
+
// (tbi.h:159-163, tbi_icu.cc:771-810). These scans read each text once.
|
|
28
|
+
// - Chrome opens line_normal_cj.brk for zh content, and for content without a language
|
|
29
|
+
// under a Chinese UI. The scan takes preparation's language, which on a page without one
|
|
30
|
+
// is the language V8 shows as its default locale, Chrome's UI language (getBlinkLineBreaks).
|
|
31
|
+
// Content-Language headers and an element's own lang aren't read.
|
|
32
|
+
// - WebKit splits items where bidi levels change (IIB:637-775), and swaps a Han-script
|
|
33
|
+
// locale for the user's first Chinese language (FontDescription.cpp:74-83, 107-113).
|
|
34
|
+
// Pretext resolves no bidi levels here and takes preparation's language as it is.
|
|
35
|
+
|
|
36
|
+
import {
|
|
37
|
+
appleQuoteRemaps,
|
|
38
|
+
blinkLinePairsPacked,
|
|
39
|
+
classMaps,
|
|
40
|
+
classRemapsPacked,
|
|
41
|
+
classRunsVarints,
|
|
42
|
+
jointClassCount,
|
|
43
|
+
ruleTables,
|
|
44
|
+
webkitLinePairsPacked,
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
} from './generated/engine-break-data.js'
|
|
48
|
+
|
|
49
|
+
// What a scan marks at a position of its text, as bits. A line may start at a BREAK. The WebKit
|
|
50
|
+
// scan marks a FORCED_BREAK after a U+2028 or U+2029 that starts an item; the Gecko scan marks a
|
|
51
|
+
// CLUSTER_START where a cluster starts without a break, and a SOFT_HYPHEN_BREAK with the BREAK
|
|
52
|
+
// right after a soft hyphen. A rich-inline paragraph's analysis marks an ITEM_START where one of
|
|
53
|
+
// its items starts, which starts a segment whether or not a line may start there.
|
|
54
|
+
export const BREAK = 1
|
|
55
|
+
export const CLUSTER_START = 2
|
|
56
|
+
export const FORCED_BREAK = 4
|
|
57
|
+
export const SOFT_HYPHEN_BREAK = 8
|
|
58
|
+
export const ITEM_START = 16
|
|
59
|
+
|
|
60
|
+
// Page languages whose line-break rules differ in some engine. Every other
|
|
61
|
+
// language, an empty or missing one, and no document read as root.
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
// The primary language subtag, ASCII case-insensitively, up to `-`, `_` or the
|
|
65
|
+
// end. No allocation: preparation calls this once per text.
|
|
66
|
+
export function getBreakLanguage(tag ) {
|
|
67
|
+
if (tag === null || tag.length < 2) return 'root'
|
|
68
|
+
if (tag.length > 2 && tag.charCodeAt(2) !== 0x2D && tag.charCodeAt(2) !== 0x5F) return 'root'
|
|
69
|
+
const first = tag.charCodeAt(0) | 0x20
|
|
70
|
+
const second = tag.charCodeAt(1) | 0x20
|
|
71
|
+
if (first === 0x6A && second === 0x61) return 'ja'
|
|
72
|
+
if (first === 0x6B && second === 0x6F) return 'ko'
|
|
73
|
+
if (first === 0x7A && second === 0x68) return 'zh'
|
|
74
|
+
return 'root'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The generated tables of bytes ship packed, in base64: the unpacked length, then runs of literal
|
|
78
|
+
// bytes, each followed by a copy of earlier bytes (length - 4, then distance back), every count a
|
|
79
|
+
// little-endian base-128 varint. Packing keeps the tables a page parses small; a page unpacks only
|
|
80
|
+
// its engine's tables.
|
|
81
|
+
export function unpackTable(packed ) {
|
|
82
|
+
const input = atob(packed)
|
|
83
|
+
let at = 0
|
|
84
|
+
const varint = () => {
|
|
85
|
+
let value = 0
|
|
86
|
+
let scale = 1
|
|
87
|
+
let byte
|
|
88
|
+
do {
|
|
89
|
+
byte = input.charCodeAt(at++)
|
|
90
|
+
value += (byte & 0x7f) * scale
|
|
91
|
+
scale *= 0x80
|
|
92
|
+
} while (byte >= 0x80)
|
|
93
|
+
return value
|
|
94
|
+
}
|
|
95
|
+
const bytes = new Uint8Array(varint())
|
|
96
|
+
let out = 0
|
|
97
|
+
while (at < input.length) {
|
|
98
|
+
for (let n = varint(); n > 0; n--) bytes[out++] = input.charCodeAt(at++)
|
|
99
|
+
if (at >= input.length) break
|
|
100
|
+
const length = varint() + 4
|
|
101
|
+
const from = out - varint()
|
|
102
|
+
// A copy that overlaps the bytes it writes repeats them, so it goes one byte at a time.
|
|
103
|
+
if (from + length <= out) bytes.copyWithin(out, from, from + length)
|
|
104
|
+
else for (let k = 0; k < length; k++) bytes[out + k] = bytes[from + k]
|
|
105
|
+
out += length
|
|
106
|
+
}
|
|
107
|
+
if (out !== bytes.length) throw new Error('A packed table unpacked to the wrong length')
|
|
108
|
+
return bytes
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The little-endian base-128 varints a table in base64 holds. Tables of varints ship without
|
|
112
|
+
// unpackTable's packing, which saves them nothing once the bundle is compressed.
|
|
113
|
+
export function unpackVarints(base64 ) {
|
|
114
|
+
const input = atob(base64)
|
|
115
|
+
const values = new Int32Array(input.length)
|
|
116
|
+
let count = 0
|
|
117
|
+
for (let i = 0; i < input.length;) {
|
|
118
|
+
let byte = input.charCodeAt(i++)
|
|
119
|
+
let value = byte & 0x7f
|
|
120
|
+
for (let shift = 7; byte >= 0x80; shift += 7) {
|
|
121
|
+
byte = input.charCodeAt(i++)
|
|
122
|
+
value |= (byte & 0x7f) << shift
|
|
123
|
+
}
|
|
124
|
+
values[count++] = value
|
|
125
|
+
}
|
|
126
|
+
return values.subarray(0, count)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// A code point's class in one of the generated maps, in two loads: `index` has, for every 256 code
|
|
130
|
+
// points up to U+10FFFF, their block of `values`. Code points that share a class through a whole
|
|
131
|
+
// block share one block.
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
// For c in U+0000..U+10FFFF, which is every code point a scan decodes from UTF-16: `index` has no
|
|
135
|
+
// entry above that, where the engines' tries give their error value (unicode/ucptrie.h:615-620;
|
|
136
|
+
// getLineBreakClass in src/gecko-line-breaks.ts gives ICU4X's itself).
|
|
137
|
+
export function getClass(table , c ) {
|
|
138
|
+
return table.values[(table.index[c >> 8] << 8) | (c & 0xff)]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The maps ship as one list of runs over U+0000..U+10FFFF, each run a length - 1 and a joint class:
|
|
142
|
+
// the joint classes are the classes all the maps together tell apart, so code points that the
|
|
143
|
+
// engines class alike are stored once. `remap` has one map's class for each joint class, and
|
|
144
|
+
// `blocks` is how many blocks its table takes. Throws for a list that doesn't end at U+10FFFF, an
|
|
145
|
+
// empty one included.
|
|
146
|
+
export function unpackClassRuns(runs , remap , blocks ) {
|
|
147
|
+
const index = new Uint16Array(0x1100)
|
|
148
|
+
const values = new Uint8Array(blocks << 8)
|
|
149
|
+
// The block each class fills whole, once a run covers one.
|
|
150
|
+
const whole = new Int32Array(0x100).fill(-1)
|
|
151
|
+
let count = 0
|
|
152
|
+
let c = 0
|
|
153
|
+
let end = 0
|
|
154
|
+
let value = -1
|
|
155
|
+
for (let r = 0; r <= runs.length; r += 2) {
|
|
156
|
+
// Runs of joint classes that are one class in this map make one run, [c, end), written once
|
|
157
|
+
// the next run's class differs or no run is left.
|
|
158
|
+
const following = r < runs.length ? remap[runs[r + 1] ] : -1
|
|
159
|
+
if (following !== value) {
|
|
160
|
+
while (c < end) {
|
|
161
|
+
const block = c >> 8
|
|
162
|
+
if ((c & 0xff) === 0 && end - c >= 0x100) {
|
|
163
|
+
if (whole[value] < 0) {
|
|
164
|
+
whole[value] = count
|
|
165
|
+
values.fill(value, count << 8, ++count << 8)
|
|
166
|
+
}
|
|
167
|
+
index.fill(whole[value] , block, end >> 8)
|
|
168
|
+
c = end & ~0xff
|
|
169
|
+
} else {
|
|
170
|
+
// A block that several runs share is its own: the first of them starts it.
|
|
171
|
+
if ((c & 0xff) === 0) index[block] = count++
|
|
172
|
+
const stop = Math.min(end, (block + 1) << 8)
|
|
173
|
+
const shift = (index[block] - block) << 8
|
|
174
|
+
values.fill(value, c + shift, stop + shift)
|
|
175
|
+
c = stop
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
value = following
|
|
179
|
+
}
|
|
180
|
+
if (r < runs.length) end += runs[r] + 1
|
|
181
|
+
}
|
|
182
|
+
if (end !== 0x110000) throw new Error('The class runs don\'t cover U+0000..U+10FFFF')
|
|
183
|
+
return { index, values }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The run list and the remaps, kept for the other maps the engine reads.
|
|
187
|
+
let classRuns = null
|
|
188
|
+
let classRemaps = null
|
|
189
|
+
|
|
190
|
+
export function unpackClasses(map ) {
|
|
191
|
+
const [row, blocks] = classMaps[map]
|
|
192
|
+
classRuns ??= unpackVarints(classRunsVarints)
|
|
193
|
+
classRemaps ??= unpackTable(classRemapsPacked)
|
|
194
|
+
return unpackClassRuns(classRuns, classRemaps.subarray(row * jointClassCount), blocks)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// A regular expression built at its first use, for the ones most text never reaches. Where V8 or
|
|
198
|
+
// SpiderMonkey parses a literal with a \p{...} class of a general category or a script, it builds the
|
|
199
|
+
// class's set, in a function that never runs too, so each such literal costs every page as it loads.
|
|
200
|
+
// One that every text tests stays a literal (RESEARCH.md, Keeping Work Bounded, JavaScript Engines).
|
|
201
|
+
export function lazyRegExp(source , flags ) {
|
|
202
|
+
let built = null
|
|
203
|
+
return () => built ??= new RegExp(source, flags)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Unicode properties the scans read with RegExp's \p{...}, as two bits per property and code point:
|
|
207
|
+
// whether it was tested, and whether the code point has it. A scan tests a property the first time it
|
|
208
|
+
// asks about a code point, so a page builds and runs only the tests its engine's scan makes. Code
|
|
209
|
+
// points below U+10000 keep their bits in one table, the others in a map.
|
|
210
|
+
const PROPERTY_TESTS = [
|
|
211
|
+
lazyRegExp(String.raw`^[\p{L}\p{N}]$`, 'u'),
|
|
212
|
+
lazyRegExp(String.raw`^\p{M}$`, 'u'),
|
|
213
|
+
lazyRegExp(String.raw`^\p{P}$`, 'u'),
|
|
214
|
+
lazyRegExp(String.raw`^[\p{Ps}\p{Pe}\p{Pi}\p{Pf}\p{Po}]$`, 'u'),
|
|
215
|
+
lazyRegExp(String.raw`^\p{Default_Ignorable_Code_Point}$`, 'u'),
|
|
216
|
+
lazyRegExp(String.raw`^\p{Emoji}$`, 'u'),
|
|
217
|
+
lazyRegExp(String.raw`^\p{sc=Hangul}$`, 'u'),
|
|
218
|
+
]
|
|
219
|
+
export const LETTER_OR_NUMBER = 0
|
|
220
|
+
export const MARK = 1
|
|
221
|
+
export const PUNCTUATION = 2
|
|
222
|
+
export const PUNCTUATION_BUT_DASH_OR_CONNECTOR = 3
|
|
223
|
+
export const DEFAULT_IGNORABLE = 4
|
|
224
|
+
export const EMOJI = 5
|
|
225
|
+
export const HANGUL = 6
|
|
226
|
+
let bmpProperties = null
|
|
227
|
+
const astralProperties = new Map ()
|
|
228
|
+
|
|
229
|
+
export function hasProperty(cp , property ) {
|
|
230
|
+
const bmp = bmpProperties ??= new Uint16Array(0x10000)
|
|
231
|
+
const has = 1 << (property * 2)
|
|
232
|
+
let bits = cp < 0x10000 ? bmp[cp] : astralProperties.get(cp) ?? 0
|
|
233
|
+
if ((bits & has << 1) === 0) {
|
|
234
|
+
bits |= has << 1 | (PROPERTY_TESTS[property] ().test(String.fromCodePoint(cp)) ? has : 0)
|
|
235
|
+
if (cp < 0x10000) bmp[cp] = bits
|
|
236
|
+
else astralProperties.set(cp, bits)
|
|
237
|
+
}
|
|
238
|
+
return (bits & has) !== 0
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// The scans read word boundaries only inside runs of Thai, Lao, Khmer and Myanmar
|
|
242
|
+
// letters, where no locale changes them. They ask for the segmenter only when such a
|
|
243
|
+
// run shows up (Chrome's and Safari's scans also in Tai Le, New Tai Lue, Tai Tham, Tai
|
|
244
|
+
// Viet and Ahom runs), so other text prepares without Intl.Segmenter.
|
|
245
|
+
let wordSegmenter = null
|
|
246
|
+
|
|
247
|
+
export function getWordSegmenter() {
|
|
248
|
+
return wordSegmenter ??= new Intl.Segmenter(undefined, { granularity: 'word' })
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function clearWordSegmenter() {
|
|
252
|
+
wordSegmenter = null
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// --- ICU's rule-based iterator over compiled line rules ---
|
|
256
|
+
|
|
257
|
+
export const START_STATE = 1 // rbbi.cpp:48
|
|
258
|
+
const STOP_STATE = 0 // rbbi.cpp:51
|
|
259
|
+
const ACCEPTING_UNCONDITIONAL = 1 // rbbidata.h:127
|
|
260
|
+
|
|
261
|
+
// ICU's compiled rules (RBBIDataHeader, rbbidata.h:67-94) as the forward iterator reads them: the
|
|
262
|
+
// state table's sizes (RBBIStateTable, rbbidata.h:134-148), its rows of fAccepting, fLookAhead,
|
|
263
|
+
// fTagsIdx and fNextState[catCount] (rbbidata.h:98-125), 16 bits each, and the category of every
|
|
264
|
+
// code point, which ICU keeps in a UCPTrie.
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
// A state table ships as its rows' differences from rows it repeats. The rows start as zeros or
|
|
275
|
+
// as `base`, another table's rows of the same shape. Then, for each row that differs from how it
|
|
276
|
+
// starts: the rows skipped since the last such row, how many rows back the row it copies is, or 0
|
|
277
|
+
// for none, how many cells differ after that, and for each the cells skipped since the last one
|
|
278
|
+
// and its value.
|
|
279
|
+
export function unpackStateRows(width , states , base , differences ) {
|
|
280
|
+
const rows = new Uint16Array(states * width)
|
|
281
|
+
if (base !== null) rows.set(base)
|
|
282
|
+
for (let i = 0, row = -width; i < differences.length;) {
|
|
283
|
+
row += (differences[i++] + 1) * width
|
|
284
|
+
const back = differences[i++] * width
|
|
285
|
+
if (back > 0) rows.copyWithin(row, row - back, row - back + width)
|
|
286
|
+
let at = row - 1
|
|
287
|
+
for (let n = differences[i++] ; n > 0; n--) {
|
|
288
|
+
at += 1 + differences[i++]
|
|
289
|
+
rows[at] = differences[i++]
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return rows
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// A table's rows. The rows of the table they start from are unpacked for it and not kept.
|
|
296
|
+
function unpackTableRows(table ) {
|
|
297
|
+
const [catCount, , , states, base, differences] = ruleTables[table]
|
|
298
|
+
return unpackStateRows(catCount + 3, states, base === null ? null : unpackTableRows(base), unpackVarints(differences))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const breakRules = {}
|
|
302
|
+
|
|
303
|
+
// A line or character table's rules, unpacked the first time a scan asks for them.
|
|
304
|
+
export function getBreakRules(table ) {
|
|
305
|
+
let rules = breakRules[table]
|
|
306
|
+
if (rules === undefined) {
|
|
307
|
+
const [catCount, dictCategoriesStart, lookAheadResultsSize] = ruleTables[table]
|
|
308
|
+
rules = breakRules[table] = {
|
|
309
|
+
catCount, dictCategoriesStart, rowWidth: catCount + 3, rows: unpackTableRows(table), lookAheadResultsSize,
|
|
310
|
+
classes: unpackClasses(table),
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return rules
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Apple's quotation remap for a locale: code points the line rules read as another category
|
|
317
|
+
// (apple-rbbi.cpp:1061-1084).
|
|
318
|
+
|
|
319
|
+
const NO_OVERRIDES = { chars: [], categories: [] }
|
|
320
|
+
|
|
321
|
+
// ICU's handleNext() (rbbi.cpp:779-952) from each boundary to the next over the whole text:
|
|
322
|
+
// flags[b] = 1 at every boundary 0 < b <= text.length, and at Intl.Segmenter word boundaries
|
|
323
|
+
// strictly inside each run of dictionary characters of a segment that ICU would give to a
|
|
324
|
+
// dictionary: one with a character in a dictionary category since its start (rbbi.cpp:854,
|
|
325
|
+
// rbbi_cache.cpp:486-489). The line rules say $dictionary = [$SA]. The text is read like
|
|
326
|
+
// utext_next32() over UTF-16 (utext.cpp:272-308), with unpaired surrogates as code points. ICU
|
|
327
|
+
// allocates the look-ahead slots uninitialized (rbbi.cpp:122-129) and never resets them between
|
|
328
|
+
// calls, since the rules read a slot only after setting it; here they start at -1 for each text.
|
|
329
|
+
export function markRuleBoundaries(r , text , flags , overrides = NO_OVERRIDES) {
|
|
330
|
+
const rows = r.rows
|
|
331
|
+
const width = r.rowWidth
|
|
332
|
+
const dictionaryStart = r.dictCategoriesStart
|
|
333
|
+
const length = text.length
|
|
334
|
+
const matches = new Int32Array(r.lookAheadResultsSize).fill(-1)
|
|
335
|
+
const overrideChars = overrides.chars
|
|
336
|
+
const overrideCount = overrideChars.length
|
|
337
|
+
|
|
338
|
+
for (let start = 0; start < length;) { // rbbi.cpp:809-813
|
|
339
|
+
let dictionaryCharCount = 0
|
|
340
|
+
let result = start
|
|
341
|
+
let boundary = -1
|
|
342
|
+
let pos = start
|
|
343
|
+
let c = text.charCodeAt(pos++)
|
|
344
|
+
if ((c & 0xfc00) === 0xd800 && pos < length) {
|
|
345
|
+
const trail = text.charCodeAt(pos)
|
|
346
|
+
if ((trail & 0xfc00) === 0xdc00) { pos++; c = ((c - 0xd800) << 10) + trail - 0xdc00 + 0x10000 }
|
|
347
|
+
}
|
|
348
|
+
let atEnd = false
|
|
349
|
+
let ended = false
|
|
350
|
+
let row = START_STATE * width
|
|
351
|
+
let category = 0
|
|
352
|
+
|
|
353
|
+
for (;;) {
|
|
354
|
+
if (atEnd) { // rbbi.cpp:832-843
|
|
355
|
+
if (ended) break
|
|
356
|
+
ended = true
|
|
357
|
+
category = 1
|
|
358
|
+
} else { // rbbi.cpp:850-855, with Apple's overrides (apple-rbbi.cpp:1061-1084)
|
|
359
|
+
let overridden = false
|
|
360
|
+
for (let i = 0; i < overrideCount; i++) {
|
|
361
|
+
if (c === overrideChars[i]) { category = overrides.categories[i] ; overridden = true; break }
|
|
362
|
+
}
|
|
363
|
+
if (!overridden) {
|
|
364
|
+
category = getClass(r.classes, c)
|
|
365
|
+
if (category >= dictionaryStart) dictionaryCharCount++
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const state = rows[row + 3 + category] // rbbi.cpp:874-877
|
|
369
|
+
row = state * width
|
|
370
|
+
|
|
371
|
+
const accepting = rows[row] // rbbi.cpp:880-896
|
|
372
|
+
if (accepting === ACCEPTING_UNCONDITIONAL) {
|
|
373
|
+
result = pos
|
|
374
|
+
} else if (accepting > ACCEPTING_UNCONDITIONAL && matches[accepting] >= 0) {
|
|
375
|
+
boundary = matches[accepting]
|
|
376
|
+
break
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const rule = rows[row + 1] // rbbi.cpp:904-910
|
|
380
|
+
if (rule > ACCEPTING_UNCONDITIONAL) matches[rule] = pos
|
|
381
|
+
|
|
382
|
+
if (state === STOP_STATE) break // rbbi.cpp:912-917
|
|
383
|
+
|
|
384
|
+
if (!ended) { // rbbi.cpp:923-929
|
|
385
|
+
if (pos >= length) {
|
|
386
|
+
atEnd = true
|
|
387
|
+
} else {
|
|
388
|
+
c = text.charCodeAt(pos++)
|
|
389
|
+
if ((c & 0xfc00) === 0xd800 && pos < length) {
|
|
390
|
+
const trail = text.charCodeAt(pos)
|
|
391
|
+
if ((trail & 0xfc00) === 0xdc00) { pos++; c = ((c - 0xd800) << 10) + trail - 0xdc00 + 0x10000 }
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (boundary < 0) {
|
|
398
|
+
boundary = result
|
|
399
|
+
if (result === start) { // rbbi.cpp:937-942
|
|
400
|
+
boundary = start + 1
|
|
401
|
+
if ((text.charCodeAt(start) & 0xfc00) === 0xd800 && boundary < length &&
|
|
402
|
+
(text.charCodeAt(boundary) & 0xfc00) === 0xdc00) boundary++
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
flags[boundary] = 1
|
|
406
|
+
if (dictionaryCharCount > 0) markDictionaryWords(r, text, start, boundary, flags)
|
|
407
|
+
start = boundary // rbbi.cpp:945
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// --- Line tables ---
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
// Line_Break=SA for one code point: the line rules' dictionary categories.
|
|
416
|
+
function isComplexContext(rules , c ) {
|
|
417
|
+
return getClass(rules.classes, c) >= rules.dictCategoriesStart
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function markDictionaryWords(rules , text , start , end , flags ) {
|
|
421
|
+
let runStart = -1
|
|
422
|
+
for (let i = start; i <= end;) {
|
|
423
|
+
let dictionary = false
|
|
424
|
+
let size = 1
|
|
425
|
+
if (i < end) {
|
|
426
|
+
let c = text.charCodeAt(i)
|
|
427
|
+
if ((c & 0xfc00) === 0xd800 && i + 1 < text.length && (text.charCodeAt(i + 1) & 0xfc00) === 0xdc00) {
|
|
428
|
+
c = ((c - 0xd800) << 10) + text.charCodeAt(i + 1) - 0xdc00 + 0x10000
|
|
429
|
+
size = 2
|
|
430
|
+
}
|
|
431
|
+
dictionary = isComplexContext(rules, c)
|
|
432
|
+
}
|
|
433
|
+
if (dictionary) {
|
|
434
|
+
if (runStart < 0) runStart = i
|
|
435
|
+
} else if (runStart >= 0) {
|
|
436
|
+
if (i - runStart > 1) {
|
|
437
|
+
for (const segment of getWordSegmenter().segment(text.slice(runStart, i))) {
|
|
438
|
+
if (segment.index > 0) flags[runStart + segment.index] = 1
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
runStart = -1
|
|
442
|
+
}
|
|
443
|
+
i += size
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const SPACE = 0x20
|
|
448
|
+
const TAB = 0x09
|
|
449
|
+
const LF = 0x0a
|
|
450
|
+
const ZWSP = 0x200b
|
|
451
|
+
const LINE_SEPARATOR = 0x2028
|
|
452
|
+
const PARAGRAPH_SEPARATOR = 0x2029
|
|
453
|
+
const IDEOGRAPHIC_SPACE = 0x3000
|
|
454
|
+
|
|
455
|
+
// --- Blink ---
|
|
456
|
+
|
|
457
|
+
let blinkPairs = null
|
|
458
|
+
let blinkDefaultLocale
|
|
459
|
+
|
|
460
|
+
// Blink's default language as Intl shows it (getBlinkLineBreaks), read once.
|
|
461
|
+
export function getBlinkDefaultLocale() {
|
|
462
|
+
return blinkDefaultLocale ??= new Intl.DateTimeFormat().resolvedOptions().locale
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const NO_BREAK = 0
|
|
466
|
+
const CAN_BREAK = 1
|
|
467
|
+
const UNKNOWN = 2
|
|
468
|
+
|
|
469
|
+
// tbi.h:203-205
|
|
470
|
+
function isBlinkBreakableSpace(c ) {
|
|
471
|
+
return c === SPACE || c === TAB || c === LF
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Context::ShouldBreakFast, tbi.cc:216-260, with soft hyphens enabled (hyphens: manual).
|
|
475
|
+
// The pair table is Chromium's generated kFastLineBreakTable for U+0021..U+00FF
|
|
476
|
+
// (gen.cc:422-551).
|
|
477
|
+
function shouldBreakFast(pairs , lastLast , last , ch ) {
|
|
478
|
+
if (last < 0x21 || ch < 0x21) return NO_BREAK
|
|
479
|
+
if (last === 0x2d) {
|
|
480
|
+
if (ch <= 0x7f) {
|
|
481
|
+
if (ch >= 0x30 && ch <= 0x39) {
|
|
482
|
+
const lower = lastLast | 0x20
|
|
483
|
+
return (lastLast >= 0x30 && lastLast <= 0x39) || (lower >= 0x61 && lower <= 0x7a) ? CAN_BREAK : NO_BREAK
|
|
484
|
+
}
|
|
485
|
+
} else {
|
|
486
|
+
return UNKNOWN
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (last <= 0xff && ch <= 0xff) {
|
|
490
|
+
const x = ch - 0x21
|
|
491
|
+
return (pairs[(last - 0x21) * 28 + (x >> 3)] & (1 << (x & 7))) !== 0 ? CAN_BREAK : NO_BREAK
|
|
492
|
+
}
|
|
493
|
+
return UNKNOWN
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ShouldKeepAfterKeepAll, tbi.cc:157-165, per UTF-16 code unit.
|
|
497
|
+
function shouldKeepAfterKeepAll(rules , lastLast , last , ch ) {
|
|
498
|
+
const pre = hasProperty(last, MARK) ? lastLast : last
|
|
499
|
+
return hasProperty(pre, LETTER_OR_NUMBER) && !isComplexContext(rules, pre) &&
|
|
500
|
+
hasProperty(ch, LETTER_OR_NUMBER) && !isComplexContext(rules, ch)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Where a line may start in text Blink collapsed as Pretext does: flags[i] = 1 for
|
|
504
|
+
// 0 < i < text.length. NextBreakablePosition (tbi.cc:270-387) for LineBreakType kNormal
|
|
505
|
+
// or kKeepAll and BreakSpaceType kAfterSpaceRun, asked at every offset. Each answer
|
|
506
|
+
// depends only on the two units before the offset, the unit at it, and whether ICU has
|
|
507
|
+
// a boundary there.
|
|
508
|
+
// Under line-break: auto, Chrome opens ICU's line iterator with the plain locale
|
|
509
|
+
// (line_breaker.cc:57-71 in core/layout/inline; LazyLineBreakIterator::LocaleWithKeyword,
|
|
510
|
+
// tbi.h:274-288; tbi_icu.cc:59-94), and ICU's break-iterator data picks the table by locale,
|
|
511
|
+
// falling back by truncation (brkiter.cpp:57-144, 433-452): data/brkitr/zh.txt:6 and
|
|
512
|
+
// zh_Hant.txt:6 map `line` to line_normal_cj.brk, and root.txt:8, ja.txt:6 and ko.txt:6 to
|
|
513
|
+
// line_normal.brk.
|
|
514
|
+
// Content without a language opens the table of Blink's default language, the renderer's
|
|
515
|
+
// --lang, which is Chrome's UI language (tbi_icu.cc:71-75, text_break_iterator_internal_icu.cc:31-45,
|
|
516
|
+
// platform/language.cc:94-99). Chrome also makes it the renderer's ICU default locale, which
|
|
517
|
+
// V8's Intl reads (ui/base/l10n/l10n_util.cc:392-398, chrome/app/chrome_main_delegate.cc:1474-1476),
|
|
518
|
+
// so preparation gives the scan that language from Intl for a page without one
|
|
519
|
+
// (getPreparationLanguage in src/measurement.ts); navigator.language follows the accept
|
|
520
|
+
// languages instead. Without a document the scan reads root.
|
|
521
|
+
export function getBlinkLineBreaks(text , keepAll , language ) {
|
|
522
|
+
const length = text.length
|
|
523
|
+
const breaks = new Uint8Array(length + 1)
|
|
524
|
+
if (length < 2) return breaks
|
|
525
|
+
const pairs = blinkPairs ??= unpackTable(blinkLinePairsPacked)
|
|
526
|
+
const table = getBreakLanguage(language) === 'zh' ? 'line_normal_cj' : 'line_normal'
|
|
527
|
+
const rules = getBreakRules(`chromium/${table}`)
|
|
528
|
+
let icu = null
|
|
529
|
+
let lastLast = 0
|
|
530
|
+
let last = text.charCodeAt(0)
|
|
531
|
+
for (let i = 1; i < length; lastLast = last, last = text.charCodeAt(i), i++) {
|
|
532
|
+
const ch = text.charCodeAt(i)
|
|
533
|
+
if (isBlinkBreakableSpace(ch)) continue // tbi.cc:284-291
|
|
534
|
+
if (isBlinkBreakableSpace(last)) { breaks[i] = 1; continue }
|
|
535
|
+
const fast = shouldBreakFast(pairs, lastLast, last, ch)
|
|
536
|
+
if (fast === CAN_BREAK) { breaks[i] = 1; continue } // tbi.cc:305-309
|
|
537
|
+
if (keepAll && shouldKeepAfterKeepAll(rules, lastLast, last, ch)) continue // tbi.cc:338-344
|
|
538
|
+
if (fast === NO_BREAK) continue // tbi.cc:346-348
|
|
539
|
+
// tbi.cc:350-383: ICU's first boundary after i - 1 is i exactly when i is a boundary,
|
|
540
|
+
// since the unit before it isn't a space here.
|
|
541
|
+
if (icu === null) {
|
|
542
|
+
icu = new Uint8Array(length + 1)
|
|
543
|
+
markRuleBoundaries(rules, text, icu)
|
|
544
|
+
}
|
|
545
|
+
if (icu[i] === 1) breaks[i] = 1
|
|
546
|
+
}
|
|
547
|
+
return breaks
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// --- WebKit ---
|
|
551
|
+
|
|
552
|
+
// BreakClass, BP.h:80-102. PI and PF mark opening and closing quotation marks, always
|
|
553
|
+
// together with QU.
|
|
554
|
+
const AL = 1
|
|
555
|
+
const ID = 2
|
|
556
|
+
const CM = 4
|
|
557
|
+
const OP = 8
|
|
558
|
+
const CP = 16
|
|
559
|
+
const CL = 32
|
|
560
|
+
const GL = 64
|
|
561
|
+
const QU = 128
|
|
562
|
+
const SP = 256
|
|
563
|
+
const PI = 512
|
|
564
|
+
const PF = 1024
|
|
565
|
+
const WEIRD = 32768
|
|
566
|
+
|
|
567
|
+
let webkitPairs = null
|
|
568
|
+
|
|
569
|
+
const webkitLineRules = new Map ()
|
|
570
|
+
|
|
571
|
+
// BP.h:125-139 with NoBreakSpaceBehavior::Normal.
|
|
572
|
+
function isWebKitBreakableSpace(c ) {
|
|
573
|
+
return c === SPACE || c === LF || c === TAB || c === LINE_SEPARATOR || c === PARAGRAPH_SEPARATOR
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function isASCIIDigit(c ) {
|
|
577
|
+
return c >= 0x30 && c <= 0x39
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function isASCIIAlpha(c ) {
|
|
581
|
+
const lower = c | 0x20
|
|
582
|
+
return lower >= 0x61 && lower <= 0x7a
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// BP.h:330-539. The 0x3000..0x303F switch reads only the low five bits, so it answers for
|
|
586
|
+
// 0x3020..0x303F too.
|
|
587
|
+
function classify(c ) {
|
|
588
|
+
switch (c >> 7) {
|
|
589
|
+
case 0:
|
|
590
|
+
switch (c >> 4) {
|
|
591
|
+
case 0: return WEIRD
|
|
592
|
+
case 1: return CM
|
|
593
|
+
case 2: return c === 0x20 ? SP : c === 0x22 || c === 0x27 ? QU : c === 0x28 ? OP : c === 0x29 ? CP : WEIRD
|
|
594
|
+
case 3: return c <= 0x39 ? AL : WEIRD
|
|
595
|
+
case 4: return AL
|
|
596
|
+
case 5: return c <= 0x5a ? AL : c === 0x5b ? OP : c === 0x5d ? CP : WEIRD
|
|
597
|
+
case 6: return AL
|
|
598
|
+
default: return c <= 0x7a ? AL : c === 0x7b ? OP : c === 0x7d ? CL : WEIRD
|
|
599
|
+
}
|
|
600
|
+
case 1:
|
|
601
|
+
if (c === 0xa0) return GL
|
|
602
|
+
if (c > 0xc0) return AL
|
|
603
|
+
if (c === 0xa1 || c === 0xbf) return OP
|
|
604
|
+
if (c === 0xab) return QU | PI
|
|
605
|
+
if (c === 0xbb) return QU | PF
|
|
606
|
+
return WEIRD
|
|
607
|
+
case 2: case 3: case 4: return AL
|
|
608
|
+
case 5: return c === 0x2c8 || c === 0x2cc || c === 0x2df ? WEIRD : AL
|
|
609
|
+
case 6:
|
|
610
|
+
if (c === 0x34f || (c >= 0x35c && c <= 0x362)) return GL
|
|
611
|
+
if (c < 0x370) return CM
|
|
612
|
+
if (c === 0x37e) return WEIRD
|
|
613
|
+
return AL
|
|
614
|
+
case 7: case 8: case 10: return AL
|
|
615
|
+
case 9: return c >= 0x483 && c <= 0x489 ? CM : AL
|
|
616
|
+
case 11:
|
|
617
|
+
if (c <= 0x588 || c >= 0x5c8) return AL
|
|
618
|
+
if (c >= 0x591 && c <= 0x5bd) return CM
|
|
619
|
+
return c === 0x5bf || c === 0x5c1 || c === 0x5c2 || c === 0x5c4 || c === 0x5c5 || c === 0x5c7 ? CM : WEIRD
|
|
620
|
+
case 64:
|
|
621
|
+
if (c === 0x2018 || c === 0x201c) return QU | PI
|
|
622
|
+
if (c === 0x2019 || c === 0x201d) return QU | PF
|
|
623
|
+
return WEIRD
|
|
624
|
+
default:
|
|
625
|
+
if (c >= 0x2e80 && c <= 0xa4cf) {
|
|
626
|
+
if ((c & 0xff00) === 0x3000) {
|
|
627
|
+
if (c <= 0x303f) {
|
|
628
|
+
switch (c & 0x1f) {
|
|
629
|
+
case 0x01: case 0x02: case 0x09: case 0x0b: case 0x0d: case 0x0f: case 0x11: case 0x15: case 0x17:
|
|
630
|
+
case 0x19: case 0x1b: case 0x1e: case 0x1f:
|
|
631
|
+
return CL
|
|
632
|
+
case 0x08: case 0x0a: case 0x0c: case 0x0e: case 0x10: case 0x16: case 0x14: case 0x18: case 0x1a:
|
|
633
|
+
case 0x1d:
|
|
634
|
+
return OP
|
|
635
|
+
default:
|
|
636
|
+
return WEIRD
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return WEIRD
|
|
640
|
+
}
|
|
641
|
+
if ((c & 0xfff0) === 0x31f0) return WEIRD
|
|
642
|
+
if ((c & 0xfff8) === 0x3248) return AL
|
|
643
|
+
if ((c & 0xffc0) === 0x4dc0) return AL
|
|
644
|
+
if (c === 0xa015) return WEIRD
|
|
645
|
+
return ID
|
|
646
|
+
}
|
|
647
|
+
if (c >= 0xac00 && c <= 0xd7af) return ID
|
|
648
|
+
if (c >= 0xf900 && c <= 0xfaff) return ID
|
|
649
|
+
return WEIRD
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// CachedLineBreakIteratorFactory (TBI.h:236-351) with two characters of prior context
|
|
654
|
+
// (TBI.h:239-290), and ubrk_following over the prior context followed by the text
|
|
655
|
+
// (TBIICU.h:99-124, 136-142).
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
function createFactory(text , line ) {
|
|
667
|
+
return { text, secondToLast: 0, last: 0, priorLength: 0, line, nextBoundary: null }
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// The first ICU boundary after `location`, or -1. `location` is -1 only with a prior
|
|
671
|
+
// context; WebKit's unsigned arithmetic then lands on the last prior character
|
|
672
|
+
// (TBIICU.h:136-142).
|
|
673
|
+
function following(f , location ) {
|
|
674
|
+
let next = f.nextBoundary
|
|
675
|
+
if (next === null) {
|
|
676
|
+
const prior = f.priorLength === 2
|
|
677
|
+
? String.fromCharCode(f.secondToLast, f.last)
|
|
678
|
+
: f.priorLength === 1 ? String.fromCharCode(f.last) : ''
|
|
679
|
+
const text = prior + f.text
|
|
680
|
+
const flags = new Uint8Array(text.length + 1)
|
|
681
|
+
markRuleBoundaries(f.line.rules, text, flags, f.line.overrides)
|
|
682
|
+
next = new Int32Array(text.length + 2)
|
|
683
|
+
next[text.length + 1] = -1
|
|
684
|
+
for (let p = text.length; p >= 0; p--) next[p] = flags[p] === 1 ? p : next[p + 1]
|
|
685
|
+
f.nextBoundary = next
|
|
686
|
+
}
|
|
687
|
+
const o = location + f.priorLength + 1
|
|
688
|
+
if (o >= next.length) return -1
|
|
689
|
+
const b = next[o]
|
|
690
|
+
return b < 0 ? -1 : b - f.priorLength
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// BP.h:142-255 for LineBreakRules::Normal, WordBreakBehavior::Normal and
|
|
694
|
+
// NoBreakSpaceBehavior::Normal. During the fast-forward over units where ICU agrees
|
|
695
|
+
// (BP.h:241-249) the characters before are not read again.
|
|
696
|
+
function nextBreakablePosition(pairs , f , startPosition ) {
|
|
697
|
+
const s = f.text
|
|
698
|
+
const length = s.length
|
|
699
|
+
if (startPosition === 0 && f.priorLength === 0) {
|
|
700
|
+
if (length <= 1) return length
|
|
701
|
+
startPosition++
|
|
702
|
+
}
|
|
703
|
+
let beforeBefore = startPosition > 1 ? s.charCodeAt(startPosition - 2) : f.secondToLast
|
|
704
|
+
let before = startPosition > 0 ? s.charCodeAt(startPosition - 1) : f.last
|
|
705
|
+
let beforeType = 0
|
|
706
|
+
let after = 0
|
|
707
|
+
let afterType = 0
|
|
708
|
+
let nextBreak = -1
|
|
709
|
+
for (let i = startPosition; i < length; beforeBefore = before, before = after, beforeType = afterType, i++) {
|
|
710
|
+
after = s.charCodeAt(i)
|
|
711
|
+
afterType = 0
|
|
712
|
+
if (isWebKitBreakableSpace(after)) return i
|
|
713
|
+
// ASCII rapid lookup.
|
|
714
|
+
if (before === 0x2d && isASCIIDigit(after)) {
|
|
715
|
+
if (isASCIIDigit(beforeBefore) || isASCIIAlpha(beforeBefore)) return i
|
|
716
|
+
continue
|
|
717
|
+
}
|
|
718
|
+
if (before <= 0xff && after <= 0xff) {
|
|
719
|
+
if (before >= 0x21 && after >= 0x21) {
|
|
720
|
+
const x = after - 0x21
|
|
721
|
+
if ((pairs[(before - 0x21) * 28 + (x >> 3)] & (1 << (x & 7))) !== 0) return i
|
|
722
|
+
}
|
|
723
|
+
continue
|
|
724
|
+
}
|
|
725
|
+
// Non-ASCII rapid lookup.
|
|
726
|
+
if (beforeType === 0) beforeType = classify(before)
|
|
727
|
+
afterType = classify(after)
|
|
728
|
+
const pair = beforeType | afterType
|
|
729
|
+
if ((pair & ~(SP | AL | QU | PI | PF)) === 0) continue
|
|
730
|
+
if ((pair | AL) === (ID | AL)) return i
|
|
731
|
+
if ((pair & (GL | QU)) !== 0 && (pair & WEIRD) === 0) {
|
|
732
|
+
// A quotation mark next to East Asian text breaks before it when it opens and
|
|
733
|
+
// after it when it closes (LB19a).
|
|
734
|
+
if ((pair & ID) !== 0 && (pair & QU) !== 0 && ((afterType & PI) !== 0 || (beforeType & PF) !== 0)) return i
|
|
735
|
+
continue
|
|
736
|
+
}
|
|
737
|
+
if (afterType === CM) {
|
|
738
|
+
afterType = beforeType
|
|
739
|
+
continue
|
|
740
|
+
}
|
|
741
|
+
if ((pair & WEIRD) === 0 && (pair & (CL | CP | OP)) !== 0) {
|
|
742
|
+
if (afterType === CL || afterType === CP || beforeType === OP) continue
|
|
743
|
+
if ((pair & ID) !== 0) return i
|
|
744
|
+
}
|
|
745
|
+
// ICU lookup.
|
|
746
|
+
if (nextBreak < i) nextBreak = following(f, i - 1)
|
|
747
|
+
if (nextBreak >= 0 && i < nextBreak) {
|
|
748
|
+
for (const max = Math.min(nextBreak, length - 1); i < max; beforeBefore = before, before = after, beforeType = afterType, i++) {
|
|
749
|
+
const lookahead = s.charCodeAt(i + 1)
|
|
750
|
+
if (lookahead <= 0xff && !isASCIIAlpha(lookahead)) break
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (i === nextBreak && !isWebKitBreakableSpace(before)) return i
|
|
754
|
+
}
|
|
755
|
+
return length
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// BP.h:258-274: keep-all breaks at spaces, before ZWSP and after U+3000, and with
|
|
759
|
+
// punctuation breaks after any punctuation but the text's last character.
|
|
760
|
+
function nextBreakableSpace(s , startPosition , punctuationBreaks ) {
|
|
761
|
+
for (let i = startPosition; i < s.length; i++) {
|
|
762
|
+
const c = s.charCodeAt(i)
|
|
763
|
+
if (isWebKitBreakableSpace(c) || c === ZWSP) return i
|
|
764
|
+
if (c === IDEOGRAPHIC_SPACE) return i + 1
|
|
765
|
+
if (punctuationBreaks && hasProperty(c, PUNCTUATION_BUT_DASH_OR_CONNECTOR) && i + 1 < s.length) return i + 1
|
|
766
|
+
}
|
|
767
|
+
return s.length
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// ubrk_open(UBRK_LINE, locale) in libicucore (TBIICU.h:63-67): line_normal.brk for ja and
|
|
771
|
+
// ko, line_cj.brk for zh and line.brk otherwise, plus the locale's quotation remap
|
|
772
|
+
// (apple-brkiter.cpp:458-473, apple-rbbi.cpp:406-487), looked up with ICU's parent fallback
|
|
773
|
+
// under ICU's case: lowercase language, title-case script, uppercase region (uloc_getName).
|
|
774
|
+
export function getWebKitLineRules(language ) {
|
|
775
|
+
const locale = language ?? ''
|
|
776
|
+
let line = webkitLineRules.get(locale)
|
|
777
|
+
if (line !== undefined) return line
|
|
778
|
+
const breakLanguage = getBreakLanguage(locale)
|
|
779
|
+
const rules = getBreakRules(breakLanguage === 'ja' || breakLanguage === 'ko' ? 'apple/line_normal' : breakLanguage === 'zh' ? 'apple/line_cj' : 'apple/line')
|
|
780
|
+
const subtags = locale.split(/[-_]/)
|
|
781
|
+
let name = subtags[0] .toLowerCase()
|
|
782
|
+
for (let k = 1; k < subtags.length; k++) {
|
|
783
|
+
const subtag = subtags[k]
|
|
784
|
+
name += '_' + (subtag.length === 4 ? subtag[0] .toUpperCase() + subtag.slice(1).toLowerCase() : subtag.toUpperCase())
|
|
785
|
+
}
|
|
786
|
+
let remap = appleQuoteRemaps[name]
|
|
787
|
+
while (remap === undefined) {
|
|
788
|
+
const cut = name.lastIndexOf('_')
|
|
789
|
+
name = cut < 0 ? '' : name.slice(0, cut)
|
|
790
|
+
remap = appleQuoteRemaps[name]
|
|
791
|
+
}
|
|
792
|
+
const chars = []
|
|
793
|
+
const categories = []
|
|
794
|
+
for (let k = 0; k < remap.length; k += 2) {
|
|
795
|
+
chars.push(remap[k] )
|
|
796
|
+
categories.push(getClass(rules.classes, remap[k + 1] === 0 ? 0x7b : 0x7d))
|
|
797
|
+
}
|
|
798
|
+
line = { rules, overrides: { chars, categories } }
|
|
799
|
+
webkitLineRules.set(locale, line)
|
|
800
|
+
return line
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const TEXT = 0
|
|
804
|
+
const WHITESPACE = 1
|
|
805
|
+
const SOFT_LINE_BREAK = 2
|
|
806
|
+
|
|
807
|
+
// Where a line may start in a text node's source: flags[i] = 1 for 0 < i < source.length.
|
|
808
|
+
// These are the soft wrap opportunities between the items InlineItemsBuilder::build
|
|
809
|
+
// makes (IIB:122-130), as the soft wrap index loop finds them (IFU:456-510): every item
|
|
810
|
+
// boundary that isn't next to a forced break. A U+2028 or U+2029 that starts an item
|
|
811
|
+
// forces a break after it, marked FORCED_BREAK; one that ICU's fast-forward passed stays
|
|
812
|
+
// inside a text item and doesn't.
|
|
813
|
+
export function getWebKitLineBreaks(
|
|
814
|
+
source ,
|
|
815
|
+
preserveNewlines ,
|
|
816
|
+
keepAll ,
|
|
817
|
+
language ,
|
|
818
|
+
) {
|
|
819
|
+
const pairs = webkitPairs ??= unpackTable(webkitLinePairsPacked)
|
|
820
|
+
const f = createFactory(source, getWebKitLineRules(language))
|
|
821
|
+
const length = source.length
|
|
822
|
+
const breaks = new Uint8Array(length + 1)
|
|
823
|
+
// WebKit stores a text holding a code unit above U+00FF in 16 bits, where keep-all also
|
|
824
|
+
// breaks after punctuation.
|
|
825
|
+
let sixteenBit = false
|
|
826
|
+
for (let i = 0; keepAll && i < length && !sixteenBit; i++) sixteenBit = source.charCodeAt(i) > 0xff
|
|
827
|
+
let previousKind = -1
|
|
828
|
+
// handleTextContent, IIB:924-1051, for hyphens manual and nbsp-mode normal.
|
|
829
|
+
for (let position = 0; position < length;) {
|
|
830
|
+
let kind = WHITESPACE
|
|
831
|
+
let end = position
|
|
832
|
+
const c = source.charCodeAt(position)
|
|
833
|
+
if (c === LINE_SEPARATOR || c === PARAGRAPH_SEPARATOR || (preserveNewlines && c === LF)) {
|
|
834
|
+
// handleSegmentBreak, IIB:954-962.
|
|
835
|
+
kind = SOFT_LINE_BREAK
|
|
836
|
+
end++
|
|
837
|
+
if (c !== LF) breaks[end] = FORCED_BREAK
|
|
838
|
+
} else {
|
|
839
|
+
// handleWhitespace, IIB:963-992, with moveToNextNonWhitespacePosition (IIB:55-73).
|
|
840
|
+
for (; end < length; end++) {
|
|
841
|
+
const c = source.charCodeAt(end)
|
|
842
|
+
if (c !== SPACE && c !== TAB && (preserveNewlines || c !== LF)) break
|
|
843
|
+
}
|
|
844
|
+
if (end === position) {
|
|
845
|
+
// handleNonWhitespace, IIB:1012-1038, with moveToNextBreakablePosition (IIB:75-87),
|
|
846
|
+
// TU:398-422 with line-break auto (LineMode Default, TU:450-466) and BP.h:287-300.
|
|
847
|
+
kind = TEXT
|
|
848
|
+
end = length
|
|
849
|
+
for (let p = position; p < length; p++) {
|
|
850
|
+
const next = keepAll ? nextBreakableSpace(source, p, sixteenBit) : nextBreakablePosition(pairs, f, p)
|
|
851
|
+
if (next !== position) { end = next; break }
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
// IFU:336-355, 406-418: the scan split same-level items, and wrapping is allowed next
|
|
856
|
+
// to white space.
|
|
857
|
+
if (previousKind >= 0 && previousKind !== SOFT_LINE_BREAK && kind !== SOFT_LINE_BREAK) breaks[position] = 1
|
|
858
|
+
previousKind = kind
|
|
859
|
+
position = end
|
|
860
|
+
}
|
|
861
|
+
return breaks
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// TU:374-396: whether a line may start where the next inline box starts, from a fresh
|
|
865
|
+
// factory on that box with the previous box's last two characters as prior context.
|
|
866
|
+
// hyphens: manual, so a trailing soft hyphen doesn't block the break. Keep-all reads no
|
|
867
|
+
// prior context (TU:403-407, BP.h:288-300): the box starts at a break only where it
|
|
868
|
+
// starts with a breakable space or ZWSP. The soft wrap index loop skips a soft line break
|
|
869
|
+
// item (IFU:456-510), a preserved line feed or a line or paragraph separator
|
|
870
|
+
// (IIB:954-962), so no break comes before one, and wrapping is allowed next to a
|
|
871
|
+
// white-space item (IFU:406-418), which a box's preserved spaces and tabs make
|
|
872
|
+
// (IIB:963-992).
|
|
873
|
+
export function getWebKitBreakBetweenItems(previous , next , keepAll , language ) {
|
|
874
|
+
const last = previous.charCodeAt(previous.length - 1)
|
|
875
|
+
const first = next.charCodeAt(0)
|
|
876
|
+
if (first === LF || first === LINE_SEPARATOR || first === PARAGRAPH_SEPARATOR) return false
|
|
877
|
+
if (last === SPACE || last === TAB || first === SPACE || first === TAB) return true
|
|
878
|
+
if (keepAll) return nextBreakableSpace(next, 0, false) === 0
|
|
879
|
+
const pairs = webkitPairs ??= unpackTable(webkitLinePairsPacked)
|
|
880
|
+
const f = createFactory(next, getWebKitLineRules(language))
|
|
881
|
+
const n = previous.length
|
|
882
|
+
f.secondToLast = n > 1 ? previous.charCodeAt(n - 2) : 0
|
|
883
|
+
f.last = n > 0 ? previous.charCodeAt(n - 1) : 0
|
|
884
|
+
f.priorLength = f.last === 0 ? 0 : f.secondToLast === 0 ? 1 : 2
|
|
885
|
+
return nextBreakablePosition(pairs, f, 0) === 0
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// canBreakBefore, InlineContentBreaker.cpp:124-137, for line-break auto: whether a line
|
|
889
|
+
// that holds only an overflowing first character ends before this code unit.
|
|
890
|
+
export function canWebKitLineStartWith(unit ) {
|
|
891
|
+
return unit === 0x5c || (unit !== 0xa0 && unit !== 0x2010 && unit !== 0x2013 && !hasProperty(unit, PUNCTUATION_BUT_DASH_OR_CONNECTOR))
|
|
892
|
+
}
|