const collapsibleWhitespaceRunRe = /[ \t\n\r\f]+/g;
const needsWhitespaceNormalizationRe = /[\t\n\r\f]| {2,}|^ | $/;
function getWhiteSpaceProfile(whiteSpace) {
    const mode = whiteSpace ?? 'normal';
    return mode === 'pre-wrap'
        ? { mode, preserveOrdinarySpaces: true, preserveHardBreaks: true }
        : { mode, preserveOrdinarySpaces: false, preserveHardBreaks: false };
}
export function normalizeWhitespaceNormal(text) {
    if (!needsWhitespaceNormalizationRe.test(text))
        return text;
    let normalized = text.replace(collapsibleWhitespaceRunRe, ' ');
    if (normalized.charCodeAt(0) === 0x20) {
        normalized = normalized.slice(1);
    }
    if (normalized.length > 0 && normalized.charCodeAt(normalized.length - 1) === 0x20) {
        normalized = normalized.slice(0, -1);
    }
    return normalized;
}
function normalizeWhitespacePreWrap(text) {
    if (!/[\r\f]/.test(text))
        return text;
    return text
        .replace(/\r\n/g, '\n')
        .replace(/[\r\f]/g, '\n');
}
let sharedWordSegmenter = null;
let segmenterLocale;
function getSharedWordSegmenter() {
    if (sharedWordSegmenter === null) {
        sharedWordSegmenter = new Intl.Segmenter(segmenterLocale, { granularity: 'word' });
    }
    return sharedWordSegmenter;
}
export function clearAnalysisCaches() {
    sharedWordSegmenter = null;
}
export function setAnalysisLocale(locale) {
    const nextLocale = locale && locale.length > 0 ? locale : undefined;
    if (segmenterLocale === nextLocale)
        return;
    segmenterLocale = nextLocale;
    sharedWordSegmenter = null;
}
const arabicScriptRe = /\p{Script=Arabic}/u;
const combiningMarkRe = /\p{M}/u;
const decimalDigitRe = /\p{Nd}/u;
function containsArabicScript(text) {
    return arabicScriptRe.test(text);
}
function isCJKCodePoint(codePoint) {
    return ((codePoint >= 0x4E00 && codePoint <= 0x9FFF) ||
        (codePoint >= 0x3400 && codePoint <= 0x4DBF) ||
        (codePoint >= 0x20000 && codePoint <= 0x2A6DF) ||
        (codePoint >= 0x2A700 && codePoint <= 0x2B73F) ||
        (codePoint >= 0x2B740 && codePoint <= 0x2B81F) ||
        (codePoint >= 0x2B820 && codePoint <= 0x2CEAF) ||
        (codePoint >= 0x2CEB0 && codePoint <= 0x2EBEF) ||
        (codePoint >= 0x2EBF0 && codePoint <= 0x2EE5D) ||
        (codePoint >= 0x2F800 && codePoint <= 0x2FA1F) ||
        (codePoint >= 0x30000 && codePoint <= 0x3134F) ||
        (codePoint >= 0x31350 && codePoint <= 0x323AF) ||
        (codePoint >= 0x323B0 && codePoint <= 0x33479) ||
        (codePoint >= 0xF900 && codePoint <= 0xFAFF) ||
        (codePoint >= 0x3000 && codePoint <= 0x303F) ||
        (codePoint >= 0x3040 && codePoint <= 0x309F) ||
        (codePoint >= 0x30A0 && codePoint <= 0x30FF) ||
        (codePoint >= 0x3130 && codePoint <= 0x318F) ||
        (codePoint >= 0xAC00 && codePoint <= 0xD7AF) ||
        (codePoint >= 0xFF00 && codePoint <= 0xFFEF));
}
export function isCJK(s) {
    for (let i = 0; i < s.length; i++) {
        const first = s.charCodeAt(i);
        if (first < 0x3000)
            continue;
        if (first >= 0xD800 && first <= 0xDBFF && i + 1 < s.length) {
            const second = s.charCodeAt(i + 1);
            if (second >= 0xDC00 && second <= 0xDFFF) {
                const codePoint = ((first - 0xD800) << 10) + (second - 0xDC00) + 0x10000;
                if (isCJKCodePoint(codePoint))
                    return true;
                i++;
                continue;
            }
        }
        if (isCJKCodePoint(first))
            return true;
    }
    return false;
}
function endsWithLineStartProhibitedText(text) {
    const last = getLastCodePoint(text);
    return last !== null && (kinsokuStart.has(last) || leftStickyPunctuation.has(last));
}
const keepAllGlueChars = new Set([
    '\u00A0',
    '\u202F',
    '\u2060',
    '\uFEFF',
]);
function containsCJKText(text) {
    return isCJK(text);
}
function endsWithKeepAllGlueText(text) {
    const last = getLastCodePoint(text);
    return last !== null && keepAllGlueChars.has(last);
}
export function canContinueKeepAllTextRun(previousText) {
    return (!endsWithLineStartProhibitedText(previousText) &&
        !endsWithKeepAllGlueText(previousText));
}
export const kinsokuStart = new Set([
    '\uFF0C',
    '\uFF0E',
    '\uFF01',
    '\uFF1A',
    '\uFF1B',
    '\uFF1F',
    '\u3001',
    '\u3002',
    '\u30FB',
    '\uFF09',
    '\u3015',
    '\u3009',
    '\u300B',
    '\u300D',
    '\u300F',
    '\u3011',
    '\u3017',
    '\u3019',
    '\u301B',
    '\u30FC',
    '\u3005',
    '\u303B',
    '\u309D',
    '\u309E',
    '\u30FD',
    '\u30FE',
]);
export const kinsokuEnd = new Set([
    '"',
    '(', '[', '{',
    '“', '‘', '«', '‹',
    '\uFF08',
    '\u3014',
    '\u3008',
    '\u300A',
    '\u300C',
    '\u300E',
    '\u3010',
    '\u3016',
    '\u3018',
    '\u301A',
]);
const forwardStickyGlue = new Set([
    "'", '’',
]);
export const leftStickyPunctuation = new Set([
    '.', ',', '!', '?', ':', ';',
    '\u060C',
    '\u061B',
    '\u061F',
    '\u0964',
    '\u0965',
    '\u104A',
    '\u104B',
    '\u104C',
    '\u104D',
    '\u104F',
    ')', ']', '}',
    '%',
    '"',
    '”', '’', '»', '›',
    '…',
]);
const arabicNoSpaceTrailingPunctuation = new Set([
    ':',
    '.',
    '\u060C',
    '\u061B',
]);
const myanmarMedialGlue = new Set([
    '\u104F',
]);
const closingQuoteChars = new Set([
    '”', '’', '»', '›',
    '\u300D',
    '\u300F',
    '\u3011',
    '\u300B',
    '\u3009',
    '\u3015',
    '\uFF09',
]);
function isLeftStickyPunctuationSegment(segment) {
    if (isEscapedQuoteClusterSegment(segment))
        return true;
    let sawPunctuation = false;
    for (const ch of segment) {
        if (leftStickyPunctuation.has(ch)) {
            sawPunctuation = true;
            continue;
        }
        if (sawPunctuation && combiningMarkRe.test(ch))
            continue;
        return false;
    }
    return sawPunctuation;
}
function isCJKLineStartProhibitedSegment(segment) {
    for (const ch of segment) {
        if (!kinsokuStart.has(ch) && !leftStickyPunctuation.has(ch))
            return false;
    }
    return segment.length > 0;
}
function isForwardStickyClusterSegment(segment) {
    if (isEscapedQuoteClusterSegment(segment))
        return true;
    for (const ch of segment) {
        if (!kinsokuEnd.has(ch) && !forwardStickyGlue.has(ch) && !combiningMarkRe.test(ch))
            return false;
    }
    return segment.length > 0;
}
function isEscapedQuoteClusterSegment(segment) {
    let sawQuote = false;
    for (const ch of segment) {
        if (ch === '\\' || combiningMarkRe.test(ch))
            continue;
        if (kinsokuEnd.has(ch) || leftStickyPunctuation.has(ch) || forwardStickyGlue.has(ch)) {
            sawQuote = true;
            continue;
        }
        return false;
    }
    return sawQuote;
}
function previousCodePointStart(text, end) {
    const last = end - 1;
    if (last <= 0)
        return Math.max(last, 0);
    const lastCodeUnit = text.charCodeAt(last);
    if (lastCodeUnit < 0xDC00 || lastCodeUnit > 0xDFFF)
        return last;
    const maybeHigh = last - 1;
    if (maybeHigh < 0)
        return last;
    const highCodeUnit = text.charCodeAt(maybeHigh);
    return highCodeUnit >= 0xD800 && highCodeUnit <= 0xDBFF ? maybeHigh : last;
}
function getLastCodePoint(text) {
    if (text.length === 0)
        return null;
    const start = previousCodePointStart(text, text.length);
    return text.slice(start);
}
function splitTrailingForwardStickyCluster(text) {
    const chars = Array.from(text);
    let splitIndex = chars.length;
    while (splitIndex > 0) {
        const ch = chars[splitIndex - 1];
        if (combiningMarkRe.test(ch)) {
            splitIndex--;
            continue;
        }
        if (kinsokuEnd.has(ch) || forwardStickyGlue.has(ch)) {
            splitIndex--;
            continue;
        }
        break;
    }
    if (splitIndex <= 0 || splitIndex === chars.length)
        return null;
    return {
        head: chars.slice(0, splitIndex).join(''),
        tail: chars.slice(splitIndex).join(''),
    };
}
function getRepeatableSingleCharRunChar(text, isWordLike, kind) {
    return kind === 'text' && !isWordLike && text.length === 1 && text !== '-' && text !== '—'
        ? text
        : null;
}
function materializeDeferredSingleCharRun(texts, chars, lengths, index) {
    const ch = chars[index];
    const text = texts[index];
    if (ch == null)
        return text;
    const length = lengths[index];
    if (text.length === length)
        return text;
    const materialized = ch.repeat(length);
    texts[index] = materialized;
    return materialized;
}
function hasArabicNoSpacePunctuation(containsArabic, lastCodePoint) {
    return containsArabic && lastCodePoint !== null && arabicNoSpaceTrailingPunctuation.has(lastCodePoint);
}
function endsWithMyanmarMedialGlue(segment) {
    const lastCodePoint = getLastCodePoint(segment);
    return lastCodePoint !== null && myanmarMedialGlue.has(lastCodePoint);
}
function splitLeadingSpaceAndMarks(segment) {
    if (segment.length < 2 || segment[0] !== ' ')
        return null;
    const marks = segment.slice(1);
    if (/^\p{M}+$/u.test(marks)) {
        return { space: ' ', marks };
    }
    return null;
}
export function endsWithClosingQuote(text) {
    let end = text.length;
    while (end > 0) {
        const start = previousCodePointStart(text, end);
        const ch = text.slice(start, end);
        if (closingQuoteChars.has(ch))
            return true;
        if (!leftStickyPunctuation.has(ch))
            return false;
        end = start;
    }
    return false;
}
function classifySegmentBreakChar(ch, whiteSpaceProfile) {
    if (whiteSpaceProfile.preserveOrdinarySpaces || whiteSpaceProfile.preserveHardBreaks) {
        if (ch === ' ')
            return 'preserved-space';
        if (ch === '\t')
            return 'tab';
        if (whiteSpaceProfile.preserveHardBreaks && ch === '\n')
            return 'hard-break';
    }
    if (ch === ' ')
        return 'space';
    if (ch === '\u00A0' || ch === '\u202F' || ch === '\u2060' || ch === '\uFEFF') {
        return 'glue';
    }
    if (ch === '\u200B')
        return 'zero-width-break';
    if (ch === '\u00AD')
        return 'soft-hyphen';
    return 'text';
}
// All characters that classifySegmentBreakChar maps to a non-'text' kind.
const breakCharRe = /[\x20\t\n\xA0\xAD\u200B\u202F\u2060\uFEFF]/;
function joinTextParts(parts) {
    return parts.length === 1 ? parts[0] : parts.join('');
}
function joinReversedPrefixParts(prefixParts, tail) {
    const parts = [];
    for (let i = prefixParts.length - 1; i >= 0; i--) {
        parts.push(prefixParts[i]);
    }
    parts.push(tail);
    return joinTextParts(parts);
}
function splitSegmentByBreakKind(segment, isWordLike, start, whiteSpaceProfile) {
    if (!breakCharRe.test(segment)) {
        return [{ text: segment, isWordLike, kind: 'text', start }];
    }
    const pieces = [];
    let currentKind = null;
    let currentTextParts = [];
    let currentStart = start;
    let currentWordLike = false;
    let offset = 0;
    for (const ch of segment) {
        const kind = classifySegmentBreakChar(ch, whiteSpaceProfile);
        const wordLike = kind === 'text' && isWordLike;
        if (currentKind !== null && kind === currentKind && wordLike === currentWordLike) {
            currentTextParts.push(ch);
            offset += ch.length;
            continue;
        }
        if (currentKind !== null) {
            pieces.push({
                text: joinTextParts(currentTextParts),
                isWordLike: currentWordLike,
                kind: currentKind,
                start: currentStart,
            });
        }
        currentKind = kind;
        currentTextParts = [ch];
        currentStart = start + offset;
        currentWordLike = wordLike;
        offset += ch.length;
    }
    if (currentKind !== null) {
        pieces.push({
            text: joinTextParts(currentTextParts),
            isWordLike: currentWordLike,
            kind: currentKind,
            start: currentStart,
        });
    }
    return pieces;
}
function isTextRunBoundary(kind) {
    return (kind === 'space' ||
        kind === 'preserved-space' ||
        kind === 'zero-width-break' ||
        kind === 'hard-break');
}
const urlSchemeSegmentRe = /^[A-Za-z][A-Za-z0-9+.-]*:$/;
function isUrlLikeRunStart(segmentation, index) {
    const text = segmentation.texts[index];
    if (text.startsWith('www.'))
        return true;
    return (urlSchemeSegmentRe.test(text) &&
        index + 1 < segmentation.len &&
        segmentation.kinds[index + 1] === 'text' &&
        segmentation.texts[index + 1] === '//');
}
function isUrlQueryBoundarySegment(text) {
    return text.includes('?') && (text.includes('://') || text.startsWith('www.'));
}
function mergeUrlLikeRuns(segmentation) {
    const texts = segmentation.texts.slice();
    const isWordLike = segmentation.isWordLike.slice();
    const kinds = segmentation.kinds.slice();
    const starts = segmentation.starts.slice();
    for (let i = 0; i < segmentation.len; i++) {
        if (kinds[i] !== 'text' || !isUrlLikeRunStart(segmentation, i))
            continue;
        const mergedParts = [texts[i]];
        let j = i + 1;
        while (j < segmentation.len && !isTextRunBoundary(kinds[j])) {
            mergedParts.push(texts[j]);
            isWordLike[i] = true;
            const endsQueryPrefix = texts[j].includes('?');
            kinds[j] = 'text';
            texts[j] = '';
            j++;
            if (endsQueryPrefix)
                break;
        }
        texts[i] = joinTextParts(mergedParts);
    }
    let compactLen = 0;
    for (let read = 0; read < texts.length; read++) {
        const text = texts[read];
        if (text.length === 0)
            continue;
        if (compactLen !== read) {
            texts[compactLen] = text;
            isWordLike[compactLen] = isWordLike[read];
            kinds[compactLen] = kinds[read];
            starts[compactLen] = starts[read];
        }
        compactLen++;
    }
    texts.length = compactLen;
    isWordLike.length = compactLen;
    kinds.length = compactLen;
    starts.length = compactLen;
    return {
        len: compactLen,
        texts,
        isWordLike,
        kinds,
        starts,
    };
}
function mergeUrlQueryRuns(segmentation) {
    const texts = [];
    const isWordLike = [];
    const kinds = [];
    const starts = [];
    for (let i = 0; i < segmentation.len; i++) {
        const text = segmentation.texts[i];
        texts.push(text);
        isWordLike.push(segmentation.isWordLike[i]);
        kinds.push(segmentation.kinds[i]);
        starts.push(segmentation.starts[i]);
        if (!isUrlQueryBoundarySegment(text))
            continue;
        const nextIndex = i + 1;
        if (nextIndex >= segmentation.len ||
            isTextRunBoundary(segmentation.kinds[nextIndex])) {
            continue;
        }
        const queryParts = [];
        const queryStart = segmentation.starts[nextIndex];
        let j = nextIndex;
        while (j < segmentation.len && !isTextRunBoundary(segmentation.kinds[j])) {
            queryParts.push(segmentation.texts[j]);
            j++;
        }
        if (queryParts.length > 0) {
            texts.push(joinTextParts(queryParts));
            isWordLike.push(true);
            kinds.push('text');
            starts.push(queryStart);
            i = j - 1;
        }
    }
    return {
        len: texts.length,
        texts,
        isWordLike,
        kinds,
        starts,
    };
}
const numericJoinerChars = new Set([
    ':', '-', '/', '×', ',', '.', '+',
    '\u2013',
    '\u2014',
]);
const asciiPunctuationChainSegmentRe = /^[A-Za-z0-9_]+[,:;]*$/;
const asciiPunctuationChainTrailingJoinersRe = /[,:;]+$/;
function segmentContainsDecimalDigit(text) {
    for (const ch of text) {
        if (decimalDigitRe.test(ch))
            return true;
    }
    return false;
}
export function isNumericRunSegment(text) {
    if (text.length === 0)
        return false;
    for (const ch of text) {
        if (decimalDigitRe.test(ch) || numericJoinerChars.has(ch))
            continue;
        return false;
    }
    return true;
}
function mergeNumericRuns(segmentation) {
    const texts = [];
    const isWordLike = [];
    const kinds = [];
    const starts = [];
    for (let i = 0; i < segmentation.len; i++) {
        const text = segmentation.texts[i];
        const kind = segmentation.kinds[i];
        if (kind === 'text' && isNumericRunSegment(text) && segmentContainsDecimalDigit(text)) {
            const mergedParts = [text];
            let j = i + 1;
            while (j < segmentation.len &&
                segmentation.kinds[j] === 'text' &&
                isNumericRunSegment(segmentation.texts[j])) {
                mergedParts.push(segmentation.texts[j]);
                j++;
            }
            texts.push(joinTextParts(mergedParts));
            isWordLike.push(true);
            kinds.push('text');
            starts.push(segmentation.starts[i]);
            i = j - 1;
            continue;
        }
        texts.push(text);
        isWordLike.push(segmentation.isWordLike[i]);
        kinds.push(kind);
        starts.push(segmentation.starts[i]);
    }
    return {
        len: texts.length,
        texts,
        isWordLike,
        kinds,
        starts,
    };
}
function mergeAsciiPunctuationChains(segmentation) {
    const texts = [];
    const isWordLike = [];
    const kinds = [];
    const starts = [];
    for (let i = 0; i < segmentation.len; i++) {
        const text = segmentation.texts[i];
        const kind = segmentation.kinds[i];
        const wordLike = segmentation.isWordLike[i];
        if (kind === 'text' && wordLike && asciiPunctuationChainSegmentRe.test(text)) {
            const mergedParts = [text];
            let endsWithJoiners = asciiPunctuationChainTrailingJoinersRe.test(text);
            let j = i + 1;
            while (endsWithJoiners &&
                j < segmentation.len &&
                segmentation.kinds[j] === 'text' &&
                segmentation.isWordLike[j] &&
                asciiPunctuationChainSegmentRe.test(segmentation.texts[j])) {
                const nextText = segmentation.texts[j];
                mergedParts.push(nextText);
                endsWithJoiners = asciiPunctuationChainTrailingJoinersRe.test(nextText);
                j++;
            }
            texts.push(joinTextParts(mergedParts));
            isWordLike.push(true);
            kinds.push('text');
            starts.push(segmentation.starts[i]);
            i = j - 1;
            continue;
        }
        texts.push(text);
        isWordLike.push(wordLike);
        kinds.push(kind);
        starts.push(segmentation.starts[i]);
    }
    return {
        len: texts.length,
        texts,
        isWordLike,
        kinds,
        starts,
    };
}
function splitHyphenatedNumericRuns(segmentation) {
    const texts = [];
    const isWordLike = [];
    const kinds = [];
    const starts = [];
    for (let i = 0; i < segmentation.len; i++) {
        const text = segmentation.texts[i];
        if (segmentation.kinds[i] === 'text' && text.includes('-')) {
            const parts = text.split('-');
            let shouldSplit = parts.length > 1;
            for (let j = 0; j < parts.length; j++) {
                const part = parts[j];
                if (!shouldSplit)
                    break;
                if (part.length === 0 ||
                    !segmentContainsDecimalDigit(part) ||
                    !isNumericRunSegment(part)) {
                    shouldSplit = false;
                }
            }
            if (shouldSplit) {
                let offset = 0;
                for (let j = 0; j < parts.length; j++) {
                    const part = parts[j];
                    const splitText = j < parts.length - 1 ? `${part}-` : part;
                    texts.push(splitText);
                    isWordLike.push(true);
                    kinds.push('text');
                    starts.push(segmentation.starts[i] + offset);
                    offset += splitText.length;
                }
                continue;
            }
        }
        texts.push(text);
        isWordLike.push(segmentation.isWordLike[i]);
        kinds.push(segmentation.kinds[i]);
        starts.push(segmentation.starts[i]);
    }
    return {
        len: texts.length,
        texts,
        isWordLike,
        kinds,
        starts,
    };
}
function mergeGlueConnectedTextRuns(segmentation) {
    const texts = [];
    const isWordLike = [];
    const kinds = [];
    const starts = [];
    let read = 0;
    while (read < segmentation.len) {
        const textParts = [segmentation.texts[read]];
        let wordLike = segmentation.isWordLike[read];
        let kind = segmentation.kinds[read];
        let start = segmentation.starts[read];
        if (kind === 'glue') {
            const glueParts = [textParts[0]];
            const glueStart = start;
            read++;
            while (read < segmentation.len && segmentation.kinds[read] === 'glue') {
                glueParts.push(segmentation.texts[read]);
                read++;
            }
            const glueText = joinTextParts(glueParts);
            if (read < segmentation.len && segmentation.kinds[read] === 'text') {
                textParts[0] = glueText;
                textParts.push(segmentation.texts[read]);
                wordLike = segmentation.isWordLike[read];
                kind = 'text';
                start = glueStart;
                read++;
            }
            else {
                texts.push(glueText);
                isWordLike.push(false);
                kinds.push('glue');
                starts.push(glueStart);
                continue;
            }
        }
        else {
            read++;
        }
        if (kind === 'text') {
            while (read < segmentation.len && segmentation.kinds[read] === 'glue') {
                const glueParts = [];
                while (read < segmentation.len && segmentation.kinds[read] === 'glue') {
                    glueParts.push(segmentation.texts[read]);
                    read++;
                }
                const glueText = joinTextParts(glueParts);
                if (read < segmentation.len && segmentation.kinds[read] === 'text') {
                    textParts.push(glueText, segmentation.texts[read]);
                    wordLike = wordLike || segmentation.isWordLike[read];
                    read++;
                    continue;
                }
                textParts.push(glueText);
            }
        }
        texts.push(joinTextParts(textParts));
        isWordLike.push(wordLike);
        kinds.push(kind);
        starts.push(start);
    }
    return {
        len: texts.length,
        texts,
        isWordLike,
        kinds,
        starts,
    };
}
function carryTrailingForwardStickyAcrossCJKBoundary(segmentation) {
    const texts = segmentation.texts.slice();
    const isWordLike = segmentation.isWordLike.slice();
    const kinds = segmentation.kinds.slice();
    const starts = segmentation.starts.slice();
    for (let i = 0; i < texts.length - 1; i++) {
        if (kinds[i] !== 'text' || kinds[i + 1] !== 'text')
            continue;
        if (!isCJK(texts[i]) || !isCJK(texts[i + 1]))
            continue;
        const split = splitTrailingForwardStickyCluster(texts[i]);
        if (split === null)
            continue;
        texts[i] = split.head;
        texts[i + 1] = split.tail + texts[i + 1];
        starts[i + 1] = starts[i] + split.head.length;
    }
    return {
        len: texts.length,
        texts,
        isWordLike,
        kinds,
        starts,
    };
}
function buildMergedSegmentation(normalized, profile, whiteSpaceProfile) {
    const wordSegmenter = getSharedWordSegmenter();
    let mergedLen = 0;
    const mergedTexts = [];
    const mergedTextParts = [];
    const mergedWordLike = [];
    const mergedKinds = [];
    const mergedStarts = [];
    // Track repeatable single-char punctuation runs structurally so identical
    // merges stay O(1) instead of re-scanning the accumulated segment each time.
    const mergedSingleCharRunChars = [];
    const mergedSingleCharRunLengths = [];
    const mergedContainsCJK = [];
    const mergedContainsArabicScript = [];
    const mergedEndsWithClosingQuote = [];
    const mergedEndsWithMyanmarMedialGlue = [];
    const mergedHasArabicNoSpacePunctuation = [];
    for (const s of wordSegmenter.segment(normalized)) {
        for (const piece of splitSegmentByBreakKind(s.segment, s.isWordLike ?? false, s.index, whiteSpaceProfile)) {
            const isText = piece.kind === 'text';
            const repeatableSingleCharRunChar = getRepeatableSingleCharRunChar(piece.text, piece.isWordLike, piece.kind);
            const pieceContainsCJK = isCJK(piece.text);
            const pieceContainsArabicScript = containsArabicScript(piece.text);
            const pieceLastCodePoint = getLastCodePoint(piece.text);
            const pieceEndsWithClosingQuote = endsWithClosingQuote(piece.text);
            const pieceEndsWithMyanmarMedialGlue = endsWithMyanmarMedialGlue(piece.text);
            const prevIndex = mergedLen - 1;
            function appendPieceToPrevious() {
                if (mergedSingleCharRunChars[prevIndex] !== null) {
                    mergedTextParts[prevIndex] = [
                        materializeDeferredSingleCharRun(mergedTexts, mergedSingleCharRunChars, mergedSingleCharRunLengths, prevIndex),
                    ];
                    mergedSingleCharRunChars[prevIndex] = null;
                }
                mergedTextParts[prevIndex].push(piece.text);
                mergedWordLike[prevIndex] = mergedWordLike[prevIndex] || piece.isWordLike;
                mergedContainsCJK[prevIndex] = mergedContainsCJK[prevIndex] || pieceContainsCJK;
                mergedContainsArabicScript[prevIndex] =
                    mergedContainsArabicScript[prevIndex] || pieceContainsArabicScript;
                mergedEndsWithClosingQuote[prevIndex] = pieceEndsWithClosingQuote;
                mergedEndsWithMyanmarMedialGlue[prevIndex] = pieceEndsWithMyanmarMedialGlue;
                mergedHasArabicNoSpacePunctuation[prevIndex] = hasArabicNoSpacePunctuation(mergedContainsArabicScript[prevIndex], pieceLastCodePoint);
            }
            // First-pass keeps: no-space script-specific joins and punctuation glue
            // that depend on the immediately preceding text run.
            if (profile.carryCJKAfterClosingQuote &&
                isText &&
                mergedLen > 0 &&
                mergedKinds[prevIndex] === 'text' &&
                pieceContainsCJK &&
                mergedContainsCJK[prevIndex] &&
                mergedEndsWithClosingQuote[prevIndex]) {
                appendPieceToPrevious();
            }
            else if (isText &&
                mergedLen > 0 &&
                mergedKinds[prevIndex] === 'text' &&
                isCJKLineStartProhibitedSegment(piece.text) &&
                mergedContainsCJK[prevIndex]) {
                appendPieceToPrevious();
            }
            else if (isText &&
                mergedLen > 0 &&
                mergedKinds[prevIndex] === 'text' &&
                mergedEndsWithMyanmarMedialGlue[prevIndex]) {
                appendPieceToPrevious();
            }
            else if (isText &&
                mergedLen > 0 &&
                mergedKinds[prevIndex] === 'text' &&
                piece.isWordLike &&
                pieceContainsArabicScript &&
                mergedHasArabicNoSpacePunctuation[prevIndex]) {
                appendPieceToPrevious();
                mergedWordLike[prevIndex] = true;
            }
            else if (repeatableSingleCharRunChar !== null &&
                mergedLen > 0 &&
                mergedKinds[prevIndex] === 'text' &&
                mergedSingleCharRunChars[prevIndex] === repeatableSingleCharRunChar) {
                mergedSingleCharRunLengths[prevIndex] = (mergedSingleCharRunLengths[prevIndex] ?? 1) + 1;
            }
            else if (isText &&
                !piece.isWordLike &&
                mergedLen > 0 &&
                mergedKinds[prevIndex] === 'text' &&
                !mergedContainsCJK[prevIndex] &&
                (isLeftStickyPunctuationSegment(piece.text) ||
                    (piece.text === '-' && mergedWordLike[prevIndex]))) {
                appendPieceToPrevious();
            }
            else {
                mergedTexts[mergedLen] = piece.text;
                mergedTextParts[mergedLen] = [piece.text];
                mergedWordLike[mergedLen] = piece.isWordLike;
                mergedKinds[mergedLen] = piece.kind;
                mergedStarts[mergedLen] = piece.start;
                mergedSingleCharRunChars[mergedLen] = repeatableSingleCharRunChar;
                mergedSingleCharRunLengths[mergedLen] = repeatableSingleCharRunChar === null ? 0 : 1;
                mergedContainsCJK[mergedLen] = pieceContainsCJK;
                mergedContainsArabicScript[mergedLen] = pieceContainsArabicScript;
                mergedEndsWithClosingQuote[mergedLen] = pieceEndsWithClosingQuote;
                mergedEndsWithMyanmarMedialGlue[mergedLen] = pieceEndsWithMyanmarMedialGlue;
                mergedHasArabicNoSpacePunctuation[mergedLen] = hasArabicNoSpacePunctuation(pieceContainsArabicScript, pieceLastCodePoint);
                mergedLen++;
            }
        }
    }
    for (let i = 0; i < mergedLen; i++) {
        if (mergedSingleCharRunChars[i] !== null) {
            mergedTexts[i] = materializeDeferredSingleCharRun(mergedTexts, mergedSingleCharRunChars, mergedSingleCharRunLengths, i);
            continue;
        }
        mergedTexts[i] = joinTextParts(mergedTextParts[i]);
    }
    // Later passes operate on the merged text stream itself: contextual escaped
    // quote glue, forward-sticky carry, compaction, then the broader URL/numeric
    // and Arabic-leading-mark fixes.
    for (let i = 1; i < mergedLen; i++) {
        if (mergedKinds[i] === 'text' &&
            !mergedWordLike[i] &&
            isEscapedQuoteClusterSegment(mergedTexts[i]) &&
            mergedKinds[i - 1] === 'text' &&
            !mergedContainsCJK[i - 1]) {
            mergedTexts[i - 1] += mergedTexts[i];
            mergedWordLike[i - 1] = mergedWordLike[i - 1] || mergedWordLike[i];
            mergedTexts[i] = '';
        }
    }
    const forwardStickyPrefixParts = Array.from({ length: mergedLen }, () => null);
    let nextLiveIndex = -1;
    for (let i = mergedLen - 1; i >= 0; i--) {
        const text = mergedTexts[i];
        if (text.length === 0)
            continue;
        if (mergedKinds[i] === 'text' &&
            !mergedWordLike[i] &&
            isForwardStickyClusterSegment(text) &&
            nextLiveIndex >= 0 &&
            mergedKinds[nextLiveIndex] === 'text') {
            const prefixParts = forwardStickyPrefixParts[nextLiveIndex] ?? [];
            prefixParts.push(text);
            forwardStickyPrefixParts[nextLiveIndex] = prefixParts;
            mergedStarts[nextLiveIndex] = mergedStarts[i];
            mergedTexts[i] = '';
            continue;
        }
        nextLiveIndex = i;
    }
    for (let i = 0; i < mergedLen; i++) {
        const prefixParts = forwardStickyPrefixParts[i];
        if (prefixParts == null)
            continue;
        mergedTexts[i] = joinReversedPrefixParts(prefixParts, mergedTexts[i]);
    }
    let compactLen = 0;
    for (let read = 0; read < mergedLen; read++) {
        const text = mergedTexts[read];
        if (text.length === 0)
            continue;
        if (compactLen !== read) {
            mergedTexts[compactLen] = text;
            mergedWordLike[compactLen] = mergedWordLike[read];
            mergedKinds[compactLen] = mergedKinds[read];
            mergedStarts[compactLen] = mergedStarts[read];
        }
        compactLen++;
    }
    mergedTexts.length = compactLen;
    mergedWordLike.length = compactLen;
    mergedKinds.length = compactLen;
    mergedStarts.length = compactLen;
    const compacted = mergeGlueConnectedTextRuns({
        len: compactLen,
        texts: mergedTexts,
        isWordLike: mergedWordLike,
        kinds: mergedKinds,
        starts: mergedStarts,
    });
    const withMergedUrls = carryTrailingForwardStickyAcrossCJKBoundary(mergeAsciiPunctuationChains(splitHyphenatedNumericRuns(mergeNumericRuns(mergeUrlQueryRuns(mergeUrlLikeRuns(compacted))))));
    for (let i = 0; i < withMergedUrls.len - 1; i++) {
        const split = splitLeadingSpaceAndMarks(withMergedUrls.texts[i]);
        if (split === null)
            continue;
        if ((withMergedUrls.kinds[i] !== 'space' && withMergedUrls.kinds[i] !== 'preserved-space') ||
            withMergedUrls.kinds[i + 1] !== 'text' ||
            !containsArabicScript(withMergedUrls.texts[i + 1])) {
            continue;
        }
        withMergedUrls.texts[i] = split.space;
        withMergedUrls.isWordLike[i] = false;
        withMergedUrls.kinds[i] = withMergedUrls.kinds[i] === 'preserved-space' ? 'preserved-space' : 'space';
        withMergedUrls.texts[i + 1] = split.marks + withMergedUrls.texts[i + 1];
        withMergedUrls.starts[i + 1] = withMergedUrls.starts[i] + split.space.length;
    }
    return withMergedUrls;
}
function compileAnalysisChunks(segmentation, whiteSpaceProfile) {
    if (segmentation.len === 0)
        return [];
    if (!whiteSpaceProfile.preserveHardBreaks) {
        return [{
                startSegmentIndex: 0,
                endSegmentIndex: segmentation.len,
                consumedEndSegmentIndex: segmentation.len,
            }];
    }
    const chunks = [];
    let startSegmentIndex = 0;
    for (let i = 0; i < segmentation.len; i++) {
        if (segmentation.kinds[i] !== 'hard-break')
            continue;
        chunks.push({
            startSegmentIndex,
            endSegmentIndex: i,
            consumedEndSegmentIndex: i + 1,
        });
        startSegmentIndex = i + 1;
    }
    if (startSegmentIndex < segmentation.len) {
        chunks.push({
            startSegmentIndex,
            endSegmentIndex: segmentation.len,
            consumedEndSegmentIndex: segmentation.len,
        });
    }
    return chunks;
}
function mergeKeepAllTextSegments(segmentation) {
    if (segmentation.len <= 1)
        return segmentation;
    const texts = [];
    const isWordLike = [];
    const kinds = [];
    const starts = [];
    let pendingTextParts = null;
    let pendingWordLike = false;
    let pendingStart = 0;
    let pendingContainsCJK = false;
    let pendingCanContinue = false;
    function flushPendingText() {
        if (pendingTextParts === null)
            return;
        texts.push(joinTextParts(pendingTextParts));
        isWordLike.push(pendingWordLike);
        kinds.push('text');
        starts.push(pendingStart);
        pendingTextParts = null;
    }
    for (let i = 0; i < segmentation.len; i++) {
        const text = segmentation.texts[i];
        const kind = segmentation.kinds[i];
        const wordLike = segmentation.isWordLike[i];
        const start = segmentation.starts[i];
        if (kind === 'text') {
            const textContainsCJK = containsCJKText(text);
            const textCanContinue = canContinueKeepAllTextRun(text);
            if (pendingTextParts !== null && pendingContainsCJK && pendingCanContinue) {
                pendingTextParts.push(text);
                pendingWordLike = pendingWordLike || wordLike;
                pendingContainsCJK = pendingContainsCJK || textContainsCJK;
                pendingCanContinue = textCanContinue;
                continue;
            }
            flushPendingText();
            pendingTextParts = [text];
            pendingWordLike = wordLike;
            pendingStart = start;
            pendingContainsCJK = textContainsCJK;
            pendingCanContinue = textCanContinue;
            continue;
        }
        flushPendingText();
        texts.push(text);
        isWordLike.push(wordLike);
        kinds.push(kind);
        starts.push(start);
    }
    flushPendingText();
    return {
        len: texts.length,
        texts,
        isWordLike,
        kinds,
        starts,
    };
}
export function analyzeText(text, profile, whiteSpace = 'normal', wordBreak = 'normal') {
    const whiteSpaceProfile = getWhiteSpaceProfile(whiteSpace);
    const normalized = whiteSpaceProfile.mode === 'pre-wrap'
        ? normalizeWhitespacePreWrap(text)
        : normalizeWhitespaceNormal(text);
    if (normalized.length === 0) {
        return {
            normalized,
            chunks: [],
            len: 0,
            texts: [],
            isWordLike: [],
            kinds: [],
            starts: [],
        };
    }
    const segmentation = wordBreak === 'keep-all'
        ? mergeKeepAllTextSegments(buildMergedSegmentation(normalized, profile, whiteSpaceProfile))
        : buildMergedSegmentation(normalized, profile, whiteSpaceProfile);
    return {
        normalized,
        chunks: compileAnalysisChunks(segmentation, whiteSpaceProfile),
        ...segmentation,
    };
}
