clone

a desktop tv and program guide for over-the-air broadcasts: what's on air, what your tuner gets, live tv, recordings

main
airwaves / frontend / dist / vendor / pretext / analysis.js
analysis.js 47.5K
1const collapsibleWhitespaceRunRe = /[ \t\n\r\f]+/g;2const needsWhitespaceNormalizationRe = /[\t\n\r\f]| {2,}|^ | $/;3export function normalizeWhitespaceNormal(text) {4 if (!needsWhitespaceNormalizationRe.test(text))5 return text;6 let normalized = text.replace(collapsibleWhitespaceRunRe, ' ');7 if (normalized.charCodeAt(0) === 0x20) {8 normalized = normalized.slice(1);9 }10 if (normalized.length > 0 && normalized.charCodeAt(normalized.length - 1) === 0x20) {11 normalized = normalized.slice(0, -1);12 }13 return normalized;14}15function normalizeWhitespacePreWrap(text) {16 if (!/[\r\f]/.test(text))17 return text;18 return text19 .replace(/\r\n/g, '\n')20 .replace(/[\r\f]/g, '\n');21}22let sharedGraphemeSegmenter = null;23export function getSharedGraphemeSegmenter() {24 if (sharedGraphemeSegmenter === null) {25 sharedGraphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });26 }27 return sharedGraphemeSegmenter;28}29let sharedWordSegmenter = null;30let segmenterLocale;31function getSharedWordSegmenter() {32 if (sharedWordSegmenter === null) {33 sharedWordSegmenter = new Intl.Segmenter(segmenterLocale, { granularity: 'word' });34 }35 return sharedWordSegmenter;36}37export function clearAnalysisCaches() {38 sharedGraphemeSegmenter = null;39 sharedWordSegmenter = null;40}41export function setAnalysisLocale(locale) {42 const nextLocale = locale && locale.length > 0 ? locale : undefined;43 if (segmenterLocale === nextLocale)44 return;45 segmenterLocale = nextLocale;46 sharedWordSegmenter = null;47}48const arabicScriptRe = /\p{Script=Arabic}/u;49const combiningMarkRe = /\p{M}/u;50const decimalDigitRe = /\p{Nd}/u;51function containsArabicScript(text) {52 return arabicScriptRe.test(text);53}54// CJK ranges used by the wrapping policy, including supplementary ideographs.55const cjkRe = /[\u3000-\u30FF\u3130-\u318F\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF\uFF00-\uFFEF\u{20000}-\u{2A6DF}\u{2A700}-\u{2EE5D}\u{2F800}-\u{2FA1F}\u{30000}-\u{33479}]/u;56export function isCJK(s) {57 return cjkRe.test(s);58}59function endsWithLineStartProhibitedText(text) {60 const last = getLastCodePoint(text);61 return last !== null && (kinsokuStart.has(last) || leftStickyPunctuation.has(last));62}63const keepAllGlueChars = new Set([64 '\u00A0',65 '\u202F',66 '\u2060',67 '\uFEFF',68]);69const keepAllDashBreakChars = new Set([70 '-',71 '\u2010',72 '\u2013',73 '\u2014',74]);75function endsWithKeepAllGlueText(text) {76 const last = getLastCodePoint(text);77 return last !== null && keepAllGlueChars.has(last);78}79function endsWithKeepAllDashBreakText(text) {80 const last = getLastCodePoint(text);81 return last !== null && keepAllDashBreakChars.has(last);82}83export function canContinueKeepAllTextRun(previousText, breakAfterPunctuation) {84 if (endsWithKeepAllGlueText(previousText))85 return false;86 if (!breakAfterPunctuation)87 return true;88 if (endsWithLineStartProhibitedText(previousText))89 return false;90 if (endsWithKeepAllDashBreakText(previousText))91 return false;92 return true;93}94export const kinsokuStart = new Set([95 '\uFF0C',96 '\uFF0E',97 '\uFF01',98 '\uFF1A',99 '\uFF1B',100 '\uFF1F',101 '\u3001',102 '\u3002',103 '\u30FB',104 '\uFF09',105 '\u3015',106 '\u3009',107 '\u300B',108 '\u300D',109 '\u300F',110 '\u3011',111 '\u3017',112 '\u3019',113 '\u301B',114 '\u30FC',115 '\u3005',116 '\u303B',117 '\u309D',118 '\u309E',119 '\u30FD',120 '\u30FE',121]);122export const kinsokuEnd = new Set([123 '"',124 '(', '[', '{',125 '¡', '¿',126 '“', '‘', '‚', '„', '«', '‹',127 '\u2E18',128 '\uFF08',129 '\u3014',130 '\u3008',131 '\u300A',132 '\u300C',133 '\u300E',134 '\u3010',135 '\u3016',136 '\u3018',137 '\u301A',138]);139const forwardStickyGlue = new Set([140 "'", '’',141]);142export const leftStickyPunctuation = new Set([143 '.', ',', '!', '?', ':', ';',144 '\u060C',145 '\u061B',146 '\u061F',147 '\u0964',148 '\u0965',149 '\u104A',150 '\u104B',151 '\u104C',152 '\u104D',153 '\u104F',154 ')', ']', '}',155 '%',156 '"',157 '”', '’', '»', '›',158 '…',159]);160const arabicNoSpaceTrailingPunctuation = new Set([161 ':',162 '.',163 '\u060C',164 '\u061B',165]);166const myanmarMedialGlue = new Set([167 '\u104F',168]);169const closingQuoteChars = new Set([170 '”', '’', '»', '›',171 '\u300D',172 '\u300F',173 '\u3011',174 '\u300B',175 '\u3009',176 '\u3015',177 '\uFF09',178]);179function isLeftStickyPunctuationSegment(segment) {180 if (isPunctuationGlueCluster(segment))181 return true;182 let sawPunctuation = false;183 for (const ch of segment) {184 if (leftStickyPunctuation.has(ch) || isLineBreakNumericAffix(ch)) {185 sawPunctuation = true;186 continue;187 }188 if (sawPunctuation && combiningMarkRe.test(ch))189 continue;190 return false;191 }192 return sawPunctuation;193}194function isCJKLineStartProhibitedSegment(segment) {195 for (const ch of segment) {196 if (!kinsokuStart.has(ch) && !leftStickyPunctuation.has(ch))197 return false;198 }199 return segment.length > 0;200}201function isForwardStickyClusterSegment(segment) {202 if (isPunctuationGlueCluster(segment))203 return true;204 for (const ch of segment) {205 if (!kinsokuEnd.has(ch) &&206 !forwardStickyGlue.has(ch) &&207 !combiningMarkRe.test(ch) &&208 !isLineBreakNumericAffix(ch)) {209 return false;210 }211 }212 return segment.length > 0;213}214function isPunctuationGlueCluster(segment) {215 let sawPunctuation = false;216 for (const ch of segment) {217 if (ch === '\\' || combiningMarkRe.test(ch))218 continue;219 if (kinsokuEnd.has(ch) || leftStickyPunctuation.has(ch) || forwardStickyGlue.has(ch)) {220 sawPunctuation = true;221 continue;222 }223 return false;224 }225 return sawPunctuation;226}227function previousCodePointStart(text, end) {228 const last = end - 1;229 if (last <= 0)230 return Math.max(last, 0);231 const lastCodeUnit = text.charCodeAt(last);232 if (lastCodeUnit < 0xDC00 || lastCodeUnit > 0xDFFF)233 return last;234 const maybeHigh = last - 1;235 if (maybeHigh < 0)236 return last;237 const highCodeUnit = text.charCodeAt(maybeHigh);238 return highCodeUnit >= 0xD800 && highCodeUnit <= 0xDBFF ? maybeHigh : last;239}240function getLastCodePoint(text) {241 if (text.length === 0)242 return null;243 const start = previousCodePointStart(text, text.length);244 return text.slice(start);245}246function getFirstSignificantCodePoint(text) {247 for (const ch of text) {248 if (!combiningMarkRe.test(ch))249 return ch;250 }251 return null;252}253function getLastSignificantCodePoint(text, end = text.length) {254 for (; end > 0;) {255 const start = previousCodePointStart(text, end);256 const ch = text.slice(start, end);257 if (!combiningMarkRe.test(ch))258 return ch;259 end = start;260 }261 return null;262}263// Unicode line-break PR/PO classes from UAX #14, stored as start/end pairs.264const lineBreakNumericAffixRanges = [265 0x0024, 0x0025, 0x002B, 0x002B, 0x005C, 0x005C, 0x00A2, 0x00A5, 0x00B0, 0x00B1,266 0x058F, 0x058F, 0x0609, 0x060B, 0x066A, 0x066A, 0x07FE, 0x07FF, 0x09F2, 0x09F3,267 0x09F9, 0x09FB, 0x0AF1, 0x0AF1, 0x0BF9, 0x0BF9, 0x0D79, 0x0D79, 0x0E3F, 0x0E3F,268 0x17DB, 0x17DB, 0x2030, 0x2037, 0x2057, 0x2057, 0x20A0, 0x20CF, 0x2103, 0x2103,269 0x2109, 0x2109, 0x2116, 0x2116, 0x2212, 0x2213, 0xA838, 0xA838, 0xFDFC, 0xFDFC,270 0xFE69, 0xFE6A, 0xFF04, 0xFF05, 0xFFE0, 0xFFE1, 0xFFE5, 0xFFE6,271 0x11FDD, 0x11FE0, 0x1E2FF, 0x1E2FF, 0x1ECAC, 0x1ECAC, 0x1ECB0, 0x1ECB0,272];273function isCodePointInRanges(codePoint, ranges) {274 for (let i = 0; i < ranges.length; i += 2) {275 if (codePoint >= ranges[i] && codePoint <= ranges[i + 1])276 return true;277 }278 return false;279}280function isLineBreakNumericAffix(ch) {281 const codePoint = ch.codePointAt(0);282 return codePoint !== undefined && isCodePointInRanges(codePoint, lineBreakNumericAffixRanges);283}284function endsWithLineBreakNumericAffix(text) {285 const last = getLastSignificantCodePoint(text);286 return last !== null && isLineBreakNumericAffix(last);287}288function startsWithDecimalDigit(text) {289 const first = getFirstSignificantCodePoint(text);290 return first !== null && decimalDigitRe.test(first);291}292function splitTrailingForwardStickyCluster(text) {293 let splitIndex = text.length;294 while (splitIndex > 0) {295 const start = previousCodePointStart(text, splitIndex);296 const ch = text.slice(start, splitIndex);297 if (combiningMarkRe.test(ch) || kinsokuEnd.has(ch) || forwardStickyGlue.has(ch)) {298 splitIndex = start;299 continue;300 }301 break;302 }303 if (splitIndex <= 0 || splitIndex === text.length)304 return null;305 return {306 head: text.slice(0, splitIndex),307 tail: text.slice(splitIndex),308 };309}310function getRepeatableSingleCharRunChar(text, isWordLike, kind) {311 return kind === 'text' && !isWordLike && text.length === 1 && text !== '-' && text !== '—'312 ? text313 : null;314}315function hasArabicNoSpacePunctuation(containsArabic, lastCodePoint) {316 return containsArabic && lastCodePoint !== null && arabicNoSpaceTrailingPunctuation.has(lastCodePoint);317}318function endsWithMyanmarMedialGlue(segment) {319 const lastCodePoint = getLastCodePoint(segment);320 return lastCodePoint !== null && myanmarMedialGlue.has(lastCodePoint);321}322function splitLeadingSpaceAndMarks(segment) {323 if (segment.length < 2 || segment[0] !== ' ')324 return null;325 const marks = segment.slice(1);326 if (/^\p{M}+$/u.test(marks)) {327 return { space: ' ', marks };328 }329 return null;330}331export function endsWithClosingQuote(text) {332 let end = text.length;333 while (end > 0) {334 const start = previousCodePointStart(text, end);335 const ch = text.slice(start, end);336 if (closingQuoteChars.has(ch))337 return true;338 if (!leftStickyPunctuation.has(ch))339 return false;340 end = start;341 }342 return false;343}344function classifySegmentBreakChar(ch, whiteSpace) {345 if (whiteSpace === 'pre-wrap') {346 if (ch === ' ')347 return 'preserved-space';348 if (ch === '\t')349 return 'tab';350 if (ch === '\n')351 return 'hard-break';352 }353 if (ch === ' ')354 return 'space';355 if (ch === '\u00A0' || ch === '\u202F' || ch === '\u2060' || ch === '\uFEFF') {356 return 'glue';357 }358 if (ch === '\u200B')359 return 'zero-width-break';360 if (ch === '\u00AD')361 return 'soft-hyphen';362 return 'text';363}364// All characters that classifySegmentBreakChar maps to a non-'text' kind.365const breakCharRe = /[\x20\t\n\xA0\xAD\u200B\u202F\u2060\uFEFF]/;366function joinTextParts(parts) {367 return parts.length === 1 ? parts[0] : parts.join('');368}369function joinReversedPrefixParts(prefixParts, tail) {370 const parts = [];371 for (let i = prefixParts.length - 1; i >= 0; i--) {372 parts.push(prefixParts[i]);373 }374 parts.push(tail);375 return joinTextParts(parts);376}377function splitSegmentByBreakKind(segment, isWordLike, start, whiteSpace) {378 if (!breakCharRe.test(segment)) {379 return [{ text: segment, isWordLike, kind: 'text', start }];380 }381 const pieces = [];382 let currentKind = null;383 let currentStart = 0;384 let currentWordLike = false;385 let offset = 0;386 for (const ch of segment) {387 const kind = classifySegmentBreakChar(ch, whiteSpace);388 const wordLike = kind === 'text' && isWordLike;389 if (currentKind !== null && kind === currentKind && wordLike === currentWordLike) {390 offset += ch.length;391 continue;392 }393 if (currentKind !== null) {394 pieces.push({395 text: segment.slice(currentStart, offset),396 isWordLike: currentWordLike,397 kind: currentKind,398 start: start + currentStart,399 });400 }401 currentKind = kind;402 currentStart = offset;403 currentWordLike = wordLike;404 offset += ch.length;405 }406 if (currentKind !== null) {407 pieces.push({408 text: segment.slice(currentStart),409 isWordLike: currentWordLike,410 kind: currentKind,411 start: start + currentStart,412 });413 }414 return pieces;415}416function isTextRunBoundary(kind) {417 return (kind === 'space' ||418 kind === 'preserved-space' ||419 kind === 'zero-width-break' ||420 kind === 'hard-break');421}422const urlSchemeSegmentRe = /^[A-Za-z][A-Za-z0-9+.-]*:$/;423function isUrlLikeRunStart(segmentation, index) {424 const text = segmentation.texts[index];425 if (text.startsWith('www.'))426 return true;427 return (urlSchemeSegmentRe.test(text) &&428 index + 1 < segmentation.len &&429 segmentation.kinds[index + 1] === 'text' &&430 segmentation.texts[index + 1] === '//');431}432function isUrlQueryBoundarySegment(text) {433 return text.includes('?') && (text.includes('://') || text.startsWith('www.'));434}435function mergeUrlRuns(segmentation, normalized, profile) {436 const texts = [];437 const isWordLike = [];438 const kinds = [];439 const starts = [];440 for (let i = 0; i < segmentation.len; i++) {441 const start = segmentation.starts[i];442 let text = segmentation.texts[i];443 let wordLike = segmentation.isWordLike[i];444 const kind = segmentation.kinds[i];445 let queryStartOverride = -1;446 if (kind === 'text' && isUrlLikeRunStart(segmentation, i)) {447 const urlParts = [text];448 let j = i + 1;449 while (j < segmentation.len &&450 !isTextRunBoundary(segmentation.kinds[j]) &&451 numericAffixBoundary(normalized, segmentation.starts[j], profile) !== false) {452 if (queryStartOverride < 0 && isUrlLikeRunStart(segmentation, j)) {453 queryStartOverride = segmentation.starts[j];454 }455 const nextText = segmentation.texts[j];456 urlParts.push(nextText);457 wordLike = true;458 j++;459 if (nextText.includes('?'))460 break;461 }462 text = joinTextParts(urlParts);463 i = j - 1;464 }465 texts.push(text);466 isWordLike.push(wordLike);467 kinds.push(kind);468 starts.push(start);469 if (!isUrlQueryBoundarySegment(text))470 continue;471 const nextIndex = i + 1;472 if (nextIndex >= segmentation.len ||473 isTextRunBoundary(segmentation.kinds[nextIndex])) {474 continue;475 }476 const queryParts = [];477 const queryStart = queryStartOverride < 0478 ? segmentation.starts[nextIndex]479 : queryStartOverride;480 let j = nextIndex;481 while (j < segmentation.len &&482 !isTextRunBoundary(segmentation.kinds[j]) &&483 numericAffixBoundary(normalized, segmentation.starts[j], profile) !== false) {484 queryParts.push(segmentation.texts[j]);485 j++;486 }487 if (queryParts.length > 0) {488 texts.push(joinTextParts(queryParts));489 isWordLike.push(true);490 kinds.push('text');491 starts.push(queryStart);492 i = j - 1;493 }494 }495 return {496 len: texts.length,497 texts,498 isWordLike,499 kinds,500 starts,501 };502}503const numericJoinerChars = new Set([504 ':', '-', '/', '×', ',', '.', '+',505 '\u2013',506 '\u2014',507]);508const wordInternalSymbolRe = /[\p{P}\p{S}\p{Co}]/u;509const emojiPresentationRe = /\p{Emoji_Presentation}/u;510const noSpaceWordBreakAfterChars = new Set([511 '?',512 '\u058A',513 '-',514 '\u2010',515 '\u2012',516 '\u2013',517 '\u2014',518 '\u2026',519 '\u203C',520 '\u203D',521 '\u2049',522]);523function isAsciiWordInternalSymbolCode(code) {524 return ((code >= 0x21 && code <= 0x2F && code !== 0x2D) ||525 (code >= 0x3A && code <= 0x40 && code !== 0x3F) ||526 (code >= 0x5B && code <= 0x60) ||527 (code >= 0x7B && code <= 0x7E));528}529function isNoSpaceWordInternalSymbol(ch) {530 const code = ch.charCodeAt(0);531 if (code < 0x80)532 return isAsciiWordInternalSymbolCode(code);533 return (!noSpaceWordBreakAfterChars.has(ch) &&534 !emojiPresentationRe.test(ch) &&535 wordInternalSymbolRe.test(ch));536}537function isNoSpaceWordInternalSymbolSegment(text) {538 let sawSymbol = false;539 for (const ch of text) {540 if (combiningMarkRe.test(ch))541 continue;542 if (!isNoSpaceWordInternalSymbol(ch))543 return false;544 sawSymbol = true;545 }546 return sawSymbol;547}548function endsWithNoSpaceWordJoiner(text) {549 for (let end = text.length; end > 0;) {550 const start = previousCodePointStart(text, end);551 const ch = text.slice(start, end);552 if (combiningMarkRe.test(ch)) {553 end = start;554 continue;555 }556 return isNoSpaceWordInternalSymbol(ch) || isLineBreakNumericAffix(ch);557 }558 return false;559}560const asciiAlphabeticBoundaryRe = /[A-Za-z#&*<=>@^_`~]/;561function isAsciiBoundary(left, right) {562 return left.charCodeAt(0) < 0x80 && right.charCodeAt(0) < 0x80;563}564// The observed Gecko ASCII model owns directional PR/PO seams and ordinary565// opener attachment. Unicode neighbors retain the existing compatibility tier.566function numericAffixBoundary(source, boundary, profile) {567 if (!profile.geckoAsciiLineBreaks || boundary <= 0 || boundary >= source.length)568 return null;569 const left = getLastSignificantCodePoint(source, boundary);570 const right = String.fromCodePoint(source.codePointAt(boundary));571 if (left === null || !isAsciiBoundary(left, right))572 return null;573 const leftAffix = isLineBreakNumericAffix(left);574 const rightAffix = isLineBreakNumericAffix(right);575 if (!leftAffix && !rightAffix)576 return null;577 if (right === ')' || right === ']' || right === '}' || '!,.:;?/'.includes(right))578 return true;579 if ('([{'.includes(left) || left === '"' || left === "'" || right === '"' || right === "'")580 return true;581 if (right === '|' || right === '-')582 return true;583 if (leftAffix && asciiAlphabeticBoundaryRe.test(right) || asciiAlphabeticBoundaryRe.test(left) && rightAffix)584 return true;585 if (rightAffix && (')]}'.includes(left) || /[0-9]/.test(left)))586 return true;587 if (leftAffix && ('([{'.includes(right) || /[0-9]/.test(right)))588 return true;589 return false;590}591// Browser line breakers tailor ASCII opener boundaries beyond Unicode classes.592// Preserve that boundary before symbol-chain compaction can erase it.593function openingPunctuationJoinsPrevious(left, right, profile, leftEnd = left.length) {594 const first = right[0];595 if (first === undefined || !'([{'.includes(first))596 return null;597 const last = getLastSignificantCodePoint(left, leftEnd);598 if (last === null)599 return null;600 if (profile.geckoAsciiLineBreaks && isAsciiBoundary(last, first)) {601 return asciiAlphabeticBoundaryRe.test(last) || decimalDigitRe.test(last) ||602 '"\'([{'.includes(last) || isLineBreakNumericAffix(last);603 }604 if (last.charCodeAt(0) >= 0x80) {605 // Non-CJK letters and numbers retain alphabetic opener attachment (LB30).606 return !isCJK(last) && /[\p{L}\p{N}]/u.test(last) ? true : null;607 }608 return /[A-Za-z0-9]/.test(last) || "$'(/<@[^_`{".includes(last);609}610function canJoinNoSpaceWordBoundary(leftText, leftWordLike, rightText, rightWordLike, profile) {611 // The forward-sticky pass joins a sign to its numeric suffix. Preserve its612 // CJK left edge so final unit construction can place the ordinary boundary.613 if (rightText[0] === '-' && isCJK(leftText))614 return true;615 // CJK-leading mixed runs retain their own annotation/kinsoku boundaries,616 // even when the last scalar before an opener is an ASCII letter.617 if (isCJK(leftText) || isCJK(rightText))618 return false;619 const openingJoin = openingPunctuationJoinsPrevious(leftText, rightText, profile);620 if (openingJoin !== null)621 return openingJoin;622 const leftSymbol = !leftWordLike && isNoSpaceWordInternalSymbolSegment(leftText);623 const rightSymbol = !rightWordLike && isNoSpaceWordInternalSymbolSegment(rightText);624 const leftAffix = endsWithLineBreakNumericAffix(leftText);625 const leftEndsJoiner = (leftWordLike || leftAffix) && endsWithNoSpaceWordJoiner(leftText);626 if (!leftSymbol && !rightSymbol && !leftEndsJoiner)627 return false;628 return (leftWordLike || leftSymbol || leftAffix) && (rightWordLike || rightSymbol);629}630function segmentContainsDecimalDigit(text) {631 for (const ch of text) {632 if (decimalDigitRe.test(ch))633 return true;634 }635 return false;636}637export function isNumericRunSegment(text) {638 if (text.length === 0)639 return false;640 for (const ch of text) {641 if (decimalDigitRe.test(ch) || numericJoinerChars.has(ch))642 continue;643 return false;644 }645 return true;646}647function mergeNumericRuns(segmentation, normalized, profile) {648 const texts = [];649 const isWordLike = [];650 const kinds = [];651 const starts = [];652 function pushNumericRun(text, start) {653 if (text.includes('-')) {654 const parts = text.split('-');655 let shouldSplit = parts.length > 1;656 for (let i = 0; i < parts.length; i++) {657 const part = parts[i];658 if (!shouldSplit)659 break;660 if (part.length === 0 ||661 !segmentContainsDecimalDigit(part) ||662 !isNumericRunSegment(part)) {663 shouldSplit = false;664 }665 }666 if (shouldSplit) {667 let offset = 0;668 for (let i = 0; i < parts.length; i++) {669 const part = parts[i];670 const splitText = i < parts.length - 1 ? `${part}-` : part;671 texts.push(splitText);672 isWordLike.push(true);673 kinds.push('text');674 starts.push(start + offset);675 offset += splitText.length;676 }677 return;678 }679 }680 texts.push(text);681 isWordLike.push(true);682 kinds.push('text');683 starts.push(start);684 }685 for (let i = 0; i < segmentation.len; i++) {686 const text = segmentation.texts[i];687 const kind = segmentation.kinds[i];688 if (kind === 'text' && isNumericRunSegment(text) && segmentContainsDecimalDigit(text)) {689 const mergedParts = [text];690 let j = i + 1;691 while (j < segmentation.len &&692 segmentation.kinds[j] === 'text' &&693 isNumericRunSegment(segmentation.texts[j]) &&694 numericAffixBoundary(normalized, segmentation.starts[j], profile) !== false) {695 mergedParts.push(segmentation.texts[j]);696 j++;697 }698 pushNumericRun(joinTextParts(mergedParts), segmentation.starts[i]);699 i = j - 1;700 continue;701 }702 texts.push(text);703 isWordLike.push(segmentation.isWordLike[i]);704 kinds.push(kind);705 starts.push(segmentation.starts[i]);706 }707 return {708 len: texts.length,709 texts,710 isWordLike,711 kinds,712 starts,713 };714}715function mergeNoSpaceWordChains(segmentation, normalized, profile) {716 const texts = [];717 const isWordLike = [];718 const kinds = [];719 const starts = [];720 let i = 0;721 while (i < segmentation.len) {722 const text = segmentation.texts[i];723 const kind = segmentation.kinds[i];724 const wordLike = segmentation.isWordLike[i];725 if (kind === 'text') {726 const mergedParts = [text];727 let j = i + 1;728 let mergedWordLike = wordLike;729 while (j < segmentation.len &&730 segmentation.kinds[j] === 'text' &&731 (numericAffixBoundary(normalized, segmentation.starts[j], profile) ?? canJoinNoSpaceWordBoundary(segmentation.texts[j - 1], segmentation.isWordLike[j - 1], segmentation.texts[j], segmentation.isWordLike[j], profile))) {732 const nextText = segmentation.texts[j];733 mergedParts.push(nextText);734 mergedWordLike = mergedWordLike || segmentation.isWordLike[j];735 j++;736 }737 if (j > i + 1) {738 texts.push(joinTextParts(mergedParts));739 isWordLike.push(mergedWordLike);740 kinds.push('text');741 starts.push(segmentation.starts[i]);742 i = j;743 continue;744 }745 }746 texts.push(text);747 isWordLike.push(wordLike);748 kinds.push(kind);749 starts.push(segmentation.starts[i]);750 i++;751 }752 return {753 len: texts.length,754 texts,755 isWordLike,756 kinds,757 starts,758 };759}760function mergeGlueConnectedTextRuns(segmentation) {761 const texts = [];762 const isWordLike = [];763 const kinds = [];764 const starts = [];765 let read = 0;766 while (read < segmentation.len) {767 const textParts = [segmentation.texts[read]];768 let wordLike = segmentation.isWordLike[read];769 let kind = segmentation.kinds[read];770 let start = segmentation.starts[read];771 if (kind === 'glue') {772 const glueParts = [textParts[0]];773 const glueStart = start;774 read++;775 while (read < segmentation.len && segmentation.kinds[read] === 'glue') {776 glueParts.push(segmentation.texts[read]);777 read++;778 }779 const glueText = joinTextParts(glueParts);780 if (read < segmentation.len && segmentation.kinds[read] === 'text') {781 textParts[0] = glueText;782 textParts.push(segmentation.texts[read]);783 wordLike = segmentation.isWordLike[read];784 kind = 'text';785 start = glueStart;786 read++;787 }788 else {789 texts.push(glueText);790 isWordLike.push(false);791 kinds.push('glue');792 starts.push(glueStart);793 continue;794 }795 }796 else {797 read++;798 }799 if (kind === 'text') {800 while (read < segmentation.len && segmentation.kinds[read] === 'glue') {801 const glueParts = [];802 while (read < segmentation.len && segmentation.kinds[read] === 'glue') {803 glueParts.push(segmentation.texts[read]);804 read++;805 }806 const glueText = joinTextParts(glueParts);807 if (read < segmentation.len && segmentation.kinds[read] === 'text') {808 textParts.push(glueText, segmentation.texts[read]);809 wordLike = wordLike || segmentation.isWordLike[read];810 read++;811 continue;812 }813 textParts.push(glueText);814 }815 }816 texts.push(joinTextParts(textParts));817 isWordLike.push(wordLike);818 kinds.push(kind);819 starts.push(start);820 }821 return {822 len: texts.length,823 texts,824 isWordLike,825 kinds,826 starts,827 };828}829function carryTrailingForwardStickyAcrossCJKBoundary(segmentation) {830 const { texts, kinds, starts } = segmentation;831 for (let i = 0; i < texts.length - 1; i++) {832 if (kinds[i] !== 'text' || kinds[i + 1] !== 'text')833 continue;834 if (!isCJK(texts[i]) || !isCJK(texts[i + 1]))835 continue;836 const split = splitTrailingForwardStickyCluster(texts[i]);837 if (split === null)838 continue;839 texts[i] = split.head;840 texts[i + 1] = split.tail + texts[i + 1];841 starts[i + 1] = starts[i] + split.head.length;842 }843}844function buildMergedSegmentation(normalized, profile, whiteSpace) {845 const wordSegmenter = getSharedWordSegmenter();846 let mergedLen = 0;847 const mergedTexts = [];848 const mergedWordLike = [];849 const mergedKinds = [];850 const mergedStarts = [];851 // First-pass merges only extend the immediately adjacent text run. Keep that852 // live tail as a source range, then materialize it once at the next boundary.853 let hasTail = false;854 let tailStart = 0;855 let tailEnd = 0;856 let tailWordLike = false;857 let tailKind = 'text';858 let tailSingleCharRunChar = null;859 let tailContainsCJK = false;860 let tailContainsArabicScript = false;861 let tailEndsWithClosingQuote = false;862 let tailEndsWithMyanmarMedialGlue = false;863 let tailHasArabicNoSpacePunctuation = false;864 for (const s of wordSegmenter.segment(normalized)) {865 for (const piece of splitSegmentByBreakKind(s.segment, s.isWordLike ?? false, s.index, whiteSpace)) {866 const isText = piece.kind === 'text';867 const repeatableSingleCharRunChar = getRepeatableSingleCharRunChar(piece.text, piece.isWordLike, piece.kind);868 const pieceContainsCJK = isCJK(piece.text);869 const pieceContainsArabicScript = containsArabicScript(piece.text);870 const pieceLastCodePoint = getLastCodePoint(piece.text);871 const pieceEndsWithClosingQuote = endsWithClosingQuote(piece.text);872 const pieceEndsWithMyanmarMedialGlue = endsWithMyanmarMedialGlue(piece.text);873 const pieceEnd = piece.start + piece.text.length;874 const boundaryJoin = numericAffixBoundary(normalized, piece.start, profile) ??875 (tailContainsCJK || pieceContainsCJK ? null :876 openingPunctuationJoinsPrevious(normalized, piece.text, profile, piece.start));877 let appendToTail = false;878 // First-pass keeps: no-space script-specific joins and punctuation glue879 // that depend on the immediately preceding text run.880 if (profile.carryCJKAfterClosingQuote &&881 isText &&882 hasTail &&883 tailKind === 'text' &&884 pieceContainsCJK &&885 tailContainsCJK &&886 tailEndsWithClosingQuote) {887 appendToTail = true;888 }889 else if (isText &&890 hasTail &&891 tailKind === 'text' &&892 isCJKLineStartProhibitedSegment(piece.text) &&893 tailContainsCJK) {894 appendToTail = true;895 }896 else if (isText &&897 hasTail &&898 tailKind === 'text' &&899 tailEndsWithMyanmarMedialGlue) {900 appendToTail = true;901 }902 else if (isText &&903 hasTail &&904 tailKind === 'text' &&905 piece.isWordLike &&906 pieceContainsArabicScript &&907 tailHasArabicNoSpacePunctuation) {908 appendToTail = true;909 }910 else if (repeatableSingleCharRunChar !== null &&911 hasTail &&912 tailKind === 'text' &&913 tailSingleCharRunChar === repeatableSingleCharRunChar &&914 boundaryJoin !== false) {915 tailEnd = pieceEnd;916 continue;917 }918 else if (isText &&919 !piece.isWordLike &&920 hasTail &&921 tailKind === 'text' &&922 !tailContainsCJK &&923 (isLeftStickyPunctuationSegment(piece.text) ||924 (piece.text === '-' && tailWordLike))) {925 appendToTail = true;926 }927 if (isText && hasTail && tailKind === 'text' && boundaryJoin !== null)928 appendToTail = boundaryJoin;929 if (appendToTail) {930 tailEnd = pieceEnd;931 tailWordLike = tailWordLike || piece.isWordLike;932 tailSingleCharRunChar = null;933 tailContainsCJK = tailContainsCJK || pieceContainsCJK;934 tailContainsArabicScript = tailContainsArabicScript || pieceContainsArabicScript;935 tailEndsWithClosingQuote = pieceEndsWithClosingQuote;936 tailEndsWithMyanmarMedialGlue = pieceEndsWithMyanmarMedialGlue;937 tailHasArabicNoSpacePunctuation = hasArabicNoSpacePunctuation(tailContainsArabicScript, pieceLastCodePoint);938 }939 else {940 if (hasTail) {941 mergedTexts[mergedLen] = normalized.slice(tailStart, tailEnd);942 mergedWordLike[mergedLen] = tailWordLike;943 mergedKinds[mergedLen] = tailKind;944 mergedStarts[mergedLen] = tailStart;945 mergedLen++;946 }947 hasTail = true;948 tailStart = piece.start;949 tailEnd = pieceEnd;950 tailWordLike = piece.isWordLike;951 tailKind = piece.kind;952 tailSingleCharRunChar = repeatableSingleCharRunChar;953 tailContainsCJK = pieceContainsCJK;954 tailContainsArabicScript = pieceContainsArabicScript;955 tailEndsWithClosingQuote = pieceEndsWithClosingQuote;956 tailEndsWithMyanmarMedialGlue = pieceEndsWithMyanmarMedialGlue;957 tailHasArabicNoSpacePunctuation = hasArabicNoSpacePunctuation(pieceContainsArabicScript, pieceLastCodePoint);958 }959 }960 }961 if (hasTail) {962 mergedTexts[mergedLen] = normalized.slice(tailStart, tailEnd);963 mergedWordLike[mergedLen] = tailWordLike;964 mergedKinds[mergedLen] = tailKind;965 mergedStarts[mergedLen] = tailStart;966 mergedLen++;967 }968 // Later passes operate on the merged text stream itself: contextual escaped969 // quote glue, forward-sticky carry, compaction, then the broader URL/numeric970 // and Arabic-leading-mark fixes.971 for (let i = 1; i < mergedLen; i++) {972 if (mergedKinds[i] === 'text' &&973 !mergedWordLike[i] &&974 isPunctuationGlueCluster(mergedTexts[i]) &&975 mergedKinds[i - 1] === 'text' &&976 !isCJK(mergedTexts[i - 1]) &&977 (numericAffixBoundary(normalized, mergedStarts[i], profile) ??978 openingPunctuationJoinsPrevious(normalized, mergedTexts[i], profile, mergedStarts[i])) !== false) {979 mergedTexts[i - 1] += mergedTexts[i];980 mergedWordLike[i - 1] = mergedWordLike[i - 1] || mergedWordLike[i];981 mergedTexts[i] = '';982 }983 }984 let nextLiveIndex = -1;985 let forwardStickyPrefixParts = null;986 for (let i = mergedLen - 1; i >= 0; i--) {987 const text = mergedTexts[i];988 if (text.length === 0)989 continue;990 const nextText = forwardStickyPrefixParts?.at(-1) ?? (nextLiveIndex >= 0 ? mergedTexts[nextLiveIndex] : null);991 if (mergedKinds[i] === 'text' &&992 !mergedWordLike[i] &&993 nextText !== null &&994 mergedKinds[nextLiveIndex] === 'text' &&995 ((numericAffixBoundary(normalized, mergedStarts[i] + text.length, profile) ??996 (isForwardStickyClusterSegment(text) && openingPunctuationJoinsPrevious(text, nextText, profile) !== false)) ||997 (text === '-' && startsWithDecimalDigit(nextText)))) {998 if (forwardStickyPrefixParts === null)999 forwardStickyPrefixParts = [];1000 forwardStickyPrefixParts.push(text);1001 mergedStarts[nextLiveIndex] = mergedStarts[i];1002 mergedTexts[i] = '';1003 continue;1004 }1005 if (forwardStickyPrefixParts !== null) {1006 mergedTexts[nextLiveIndex] = joinReversedPrefixParts(forwardStickyPrefixParts, mergedTexts[nextLiveIndex]);1007 forwardStickyPrefixParts = null;1008 }1009 nextLiveIndex = i;1010 }1011 if (forwardStickyPrefixParts !== null) {1012 mergedTexts[nextLiveIndex] = joinReversedPrefixParts(forwardStickyPrefixParts, mergedTexts[nextLiveIndex]);1013 }1014 let compactLen = 0;1015 for (let read = 0; read < mergedLen; read++) {1016 const text = mergedTexts[read];1017 if (text.length === 0)1018 continue;1019 if (compactLen !== read) {1020 mergedTexts[compactLen] = text;1021 mergedWordLike[compactLen] = mergedWordLike[read];1022 mergedKinds[compactLen] = mergedKinds[read];1023 mergedStarts[compactLen] = mergedStarts[read];1024 }1025 compactLen++;1026 }1027 mergedTexts.length = compactLen;1028 mergedWordLike.length = compactLen;1029 mergedKinds.length = compactLen;1030 mergedStarts.length = compactLen;1031 const compacted = mergeGlueConnectedTextRuns({1032 len: compactLen,1033 texts: mergedTexts,1034 isWordLike: mergedWordLike,1035 kinds: mergedKinds,1036 starts: mergedStarts,1037 });1038 const mergedRuns = mergeNoSpaceWordChains(mergeNumericRuns(mergeUrlRuns(compacted, normalized, profile), normalized, profile), normalized, profile);1039 carryTrailingForwardStickyAcrossCJKBoundary(mergedRuns);1040 for (let i = 0; i < mergedRuns.len - 1; i++) {1041 const split = splitLeadingSpaceAndMarks(mergedRuns.texts[i]);1042 if (split === null)1043 continue;1044 if ((mergedRuns.kinds[i] !== 'space' && mergedRuns.kinds[i] !== 'preserved-space') ||1045 mergedRuns.kinds[i + 1] !== 'text' ||1046 !containsArabicScript(mergedRuns.texts[i + 1])) {1047 continue;1048 }1049 mergedRuns.texts[i] = split.space;1050 mergedRuns.isWordLike[i] = false;1051 mergedRuns.kinds[i] = mergedRuns.kinds[i] === 'preserved-space' ? 'preserved-space' : 'space';1052 mergedRuns.texts[i + 1] = split.marks + mergedRuns.texts[i + 1];1053 mergedRuns.starts[i + 1] = mergedRuns.starts[i] + split.space.length;1054 }1055 return mergedRuns;1056}1057function mergeKeepAllTextSegments(normalized, segmentation, profile) {1058 if (segmentation.len <= 1)1059 return segmentation;1060 const texts = [];1061 const isWordLike = [];1062 const kinds = [];1063 const starts = [];1064 let groupStart = -1;1065 let groupContainsCJK = false;1066 function pushOriginalText(index) {1067 texts.push(segmentation.texts[index]);1068 isWordLike.push(segmentation.isWordLike[index]);1069 kinds.push('text');1070 starts.push(segmentation.starts[index]);1071 }1072 function pushMergedText(start, end) {1073 let wordLike = false;1074 for (let i = start; i < end; i++) {1075 wordLike = wordLike || segmentation.isWordLike[i];1076 }1077 const sourceStart = segmentation.starts[start];1078 const sourceEnd = end < segmentation.len ? segmentation.starts[end] : normalized.length;1079 texts.push(normalized.slice(sourceStart, sourceEnd));1080 isWordLike.push(wordLike);1081 kinds.push('text');1082 starts.push(sourceStart);1083 }1084 function flushGroup(end) {1085 if (groupStart < 0)1086 return;1087 if (groupContainsCJK) {1088 if (groupStart + 1 === end) {1089 pushOriginalText(groupStart);1090 }1091 else {1092 pushMergedText(groupStart, end);1093 }1094 }1095 else {1096 for (let i = groupStart; i < end; i++)1097 pushOriginalText(i);1098 }1099 groupStart = -1;1100 groupContainsCJK = false;1101 }1102 for (let i = 0; i < segmentation.len; i++) {1103 const text = segmentation.texts[i];1104 const kind = segmentation.kinds[i];1105 if (kind === 'text') {1106 if (groupStart >= 0 &&1107 (!canContinueKeepAllTextRun(segmentation.texts[i - 1], profile.breakKeepAllAfterPunctuation) ||1108 numericAffixBoundary(normalized, segmentation.starts[i], profile) === false)) {1109 flushGroup(i);1110 }1111 if (groupStart < 0)1112 groupStart = i;1113 groupContainsCJK = groupContainsCJK || isCJK(text);1114 continue;1115 }1116 flushGroup(i);1117 texts.push(text);1118 isWordLike.push(segmentation.isWordLike[i]);1119 kinds.push(kind);1120 starts.push(segmentation.starts[i]);1121 }1122 flushGroup(segmentation.len);1123 return {1124 len: texts.length,1125 texts,1126 isWordLike,1127 kinds,1128 starts,1129 };1130}1131// A numeric sign stays with its number. Latin letter/number hyphens remain1132// preferred boundaries; CJK-adjacent signs also stay attached on their left.1133function isNumericHyphen(text, index) {1134 const next = text.charCodeAt(index + 1);1135 if (next < 0x80) {1136 if (next < 0x30 || next > 0x39)1137 return false;1138 }1139 else {1140 const codePoint = text.codePointAt(index + 1);1141 if (codePoint === undefined || !decimalDigitRe.test(String.fromCodePoint(codePoint)))1142 return false;1143 }1144 for (let end = index; end > 0;) {1145 const start = previousCodePointStart(text, end);1146 const previous = text.slice(start, end);1147 if (!combiningMarkRe.test(previous))1148 return isCJK(previous) || !/[\p{L}\p{N}]/u.test(previous);1149 end = start;1150 }1151 return true;1152}1153function buildBaseCjkUnits(segText, profile) {1154 const units = [];1155 let unitStart = 0;1156 let unitEnd = 0;1157 let unitContainsCJK = false;1158 let unitEndsWithClosingQuote = false;1159 let unitIsSingleKinsokuEnd = false;1160 let unitHasHyphen = false;1161 let unitHasNumericHyphen = false;1162 function pushUnit() {1163 if (unitEnd === unitStart)1164 return;1165 units.push({1166 text: segText.slice(unitStart, unitEnd),1167 start: unitStart,1168 overflow: unitContainsCJK ? (unitHasHyphen ? 'grapheme' : 'none') : 'word-like',1169 });1170 unitStart = unitEnd;1171 unitContainsCJK = false;1172 unitEndsWithClosingQuote = false;1173 unitIsSingleKinsokuEnd = false;1174 unitHasHyphen = false;1175 unitHasNumericHyphen = false;1176 }1177 function startUnit(grapheme, start, graphemeContainsCJK) {1178 unitStart = start;1179 unitEnd = start + grapheme.length;1180 unitContainsCJK = graphemeContainsCJK;1181 unitHasHyphen = grapheme === '-';1182 unitEndsWithClosingQuote = endsWithClosingQuote(grapheme);1183 unitIsSingleKinsokuEnd = kinsokuEnd.has(grapheme);1184 }1185 function appendToUnit(grapheme, graphemeContainsCJK) {1186 unitEnd += grapheme.length;1187 unitContainsCJK = unitContainsCJK || graphemeContainsCJK;1188 unitHasHyphen = unitHasHyphen || grapheme === '-';1189 const graphemeEndsWithClosingQuote = endsWithClosingQuote(grapheme);1190 if (grapheme.length === 1 && leftStickyPunctuation.has(grapheme)) {1191 unitEndsWithClosingQuote = unitEndsWithClosingQuote || graphemeEndsWithClosingQuote;1192 }1193 else {1194 unitEndsWithClosingQuote = graphemeEndsWithClosingQuote;1195 }1196 unitIsSingleKinsokuEnd = false;1197 }1198 for (const gs of getSharedGraphemeSegmenter().segment(segText)) {1199 const grapheme = gs.segment;1200 const graphemeContainsCJK = isCJK(grapheme);1201 if (unitEnd === unitStart) {1202 startUnit(grapheme, gs.index, graphemeContainsCJK);1203 continue;1204 }1205 const attachHyphen = grapheme === '-' && unitContainsCJK;1206 if (attachHyphen && isNumericHyphen(segText, gs.index))1207 unitHasNumericHyphen = true;1208 if (unitIsSingleKinsokuEnd ||1209 kinsokuStart.has(grapheme) ||1210 leftStickyPunctuation.has(grapheme) ||1211 attachHyphen ||1212 (unitHasNumericHyphen && !graphemeContainsCJK) ||1213 (profile.carryCJKAfterClosingQuote &&1214 graphemeContainsCJK &&1215 unitEndsWithClosingQuote)) {1216 appendToUnit(grapheme, graphemeContainsCJK);1217 continue;1218 }1219 if (!unitContainsCJK && !graphemeContainsCJK) {1220 appendToUnit(grapheme, graphemeContainsCJK);1221 continue;1222 }1223 pushUnit();1224 startUnit(grapheme, gs.index, graphemeContainsCJK);1225 }1226 pushUnit();1227 return units;1228}1229function mergeKeepAllTextUnits(segText, units, profile) {1230 if (units.length <= 1)1231 return units;1232 const merged = [];1233 let groupStart = -1;1234 let groupContainsCJK = false;1235 function pushMergedUnit(start, end) {1236 const sourceStart = units[start].start;1237 const sourceEnd = end < units.length ? units[end].start : segText.length;1238 merged.push({1239 text: segText.slice(sourceStart, sourceEnd),1240 start: sourceStart,1241 overflow: 'word-like',1242 });1243 }1244 function flushGroup(end) {1245 if (groupStart < 0)1246 return;1247 if (groupContainsCJK) {1248 if (groupStart + 1 === end) {1249 merged.push(units[groupStart]);1250 }1251 else {1252 pushMergedUnit(groupStart, end);1253 }1254 }1255 else {1256 for (let i = groupStart; i < end; i++)1257 merged.push(units[i]);1258 }1259 groupStart = -1;1260 groupContainsCJK = false;1261 }1262 for (let i = 0; i < units.length; i++) {1263 const unit = units[i];1264 if (groupStart >= 0 &&1265 (!canContinueKeepAllTextRun(units[i - 1].text, profile.breakKeepAllAfterPunctuation) ||1266 numericAffixBoundary(segText, unit.start, profile) === false)) {1267 flushGroup(i);1268 }1269 if (groupStart < 0)1270 groupStart = i;1271 groupContainsCJK = groupContainsCJK || isCJK(unit.text);1272 }1273 flushGroup(units.length);1274 return merged;1275}1276// Ordinary CJK boundaries and emergency overflow permission are separate facts.1277// Keep these decisions in preprocessing; measurement only observes their units.1278export function getCjkTextUnits(text, profile, wordBreak) {1279 const units = buildBaseCjkUnits(text, profile);1280 return wordBreak === 'keep-all'1281 ? mergeKeepAllTextUnits(text, units, profile)1282 : units;1283}1284function isPreferredBreakGrapheme(grapheme) {1285 return (grapheme === '-' ||1286 grapheme === '\u058A' ||1287 grapheme === '\u2010' ||1288 grapheme === '\u2012' ||1289 grapheme === '\u2013' ||1290 grapheme === '\u2014');1291}1292// Intl word-likeness is not overflow permission: independent punctuation and1293// symbol graphemes can also break. Emoji retain their separate ordinary1294// boundary policy, like no-space compaction above. Control-bearing fragments1295// and standalone extenders also retain their existing source-shaping policy.1296export function isIndependentSymbolRun(text) {1297 if (text.length === 0 || /\p{Cf}/u.test(text))1298 return false;1299 for (const { segment } of getSharedGraphemeSegmenter().segment(text)) {1300 const base = String.fromCodePoint(segment.codePointAt(0));1301 if (!/[\p{P}\p{S}]/u.test(base) || emojiPresentationRe.test(base) || segment.includes('\uFE0F') || /\p{Emoji_Modifier}/u.test(base))1302 return false;1303 }1304 return true;1305}1306export function getBreakablePreferredBreaks(text) {1307 if (!/[-\u058A\u2010\u2012\u2013\u2014]/u.test(text))1308 return null;1309 const breaks = [];1310 let graphemeIndex = 0;1311 for (const gs of getSharedGraphemeSegmenter().segment(text)) {1312 graphemeIndex++;1313 const numericSign = gs.segment === '-' && isNumericHyphen(text, gs.index);1314 if (isPreferredBreakGrapheme(gs.segment) && !numericSign)1315 breaks.push(graphemeIndex);1316 }1317 return breaks.length === 0 ? null : breaks;1318}1319export function analyzeText(text, profile, whiteSpace = 'normal', wordBreak = 'normal') {1320 const normalized = whiteSpace === 'pre-wrap'1321 ? normalizeWhitespacePreWrap(text)1322 : normalizeWhitespaceNormal(text);1323 if (normalized.length === 0) {1324 return {1325 normalized,1326 len: 0,1327 texts: [],1328 isWordLike: [],1329 kinds: [],1330 starts: [],1331 };1332 }1333 const mergedSegmentation = buildMergedSegmentation(normalized, profile, whiteSpace);1334 const segmentation = wordBreak === 'keep-all'1335 ? mergeKeepAllTextSegments(normalized, mergedSegmentation, profile)1336 : mergedSegmentation;1337 return {1338 normalized,1339 ...segmentation,1340 };1341}