import { normalizeTextForSpeech } from "../text-processing.js";
const STRUCTURAL_SSML_TAGS = new Set([
    "speak",
    "p",
    "s",
    "emphasis",
    "prosody",
    "say-as",
    "voice",
    "lang",
]);
const UNSUPPORTED_SEMANTIC_SSML_TAGS = new Set(["audio", "phoneme"]);
const decodeXmlEntities = (value) => value
    .replace(/&quot;/g, '"')
    .replace(/&apos;/g, "'")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&nbsp;/g, "\u00a0")
    .replace(/&#160;/g, "\u00a0")
    .replace(/&amp;/g, "&");
const appendSourceText = (chars, value, rawOffset) => {
    for (let i = 0; i < value.length; i++) {
        chars.push({
            char: value[i],
            rawOffset: rawOffset === null ? null : rawOffset + i,
        });
    }
};
const appendDecodedXmlEntity = (chars, source, rawOffset) => {
    const match = source
        .slice(rawOffset)
        .match(/^&(quot|apos|lt|gt|nbsp|amp|#160);/i);
    if (!match)
        return 0;
    appendSourceText(chars, decodeXmlEntities(match[0]), rawOffset);
    return match[0].length;
};
const appendSpace = (chars) => {
    chars.push({ char: " ", rawOffset: null });
};
const normalizeSourceChars = (chars) => {
    const rawToSpokenOffsetMap = new Map();
    const out = [];
    let lastWasWhitespace = true;
    for (const sourceChar of chars) {
        const isWhitespace = /\s/.test(sourceChar.char);
        if (isWhitespace) {
            if (!lastWasWhitespace && out.length > 0) {
                out.push(" ");
                if (sourceChar.rawOffset !== null) {
                    rawToSpokenOffsetMap.set(sourceChar.rawOffset, out.length - 1);
                }
            }
            lastWasWhitespace = true;
            continue;
        }
        out.push(sourceChar.char);
        if (sourceChar.rawOffset !== null) {
            rawToSpokenOffsetMap.set(sourceChar.rawOffset, out.length - 1);
        }
        lastWasWhitespace = false;
    }
    while (out.length > 0 && out[out.length - 1] === " ") {
        out.pop();
    }
    // Drop any raw→spoken entries that now point past the trimmed end; otherwise
    // a boundary at a trailing space would map to an out-of-range spoken offset.
    for (const [rawOffset, spokenOffset] of rawToSpokenOffsetMap) {
        if (spokenOffset >= out.length)
            rawToSpokenOffsetMap.delete(rawOffset);
    }
    return { text: out.join(""), rawToSpokenOffsetMap };
};
const getTagName = (tagSource) => {
    const match = tagSource.match(/^<\/?\s*([A-Za-z0-9:-]+)/);
    return match?.[1]?.toLowerCase() || "";
};
const getAttribute = (tagSource, name) => {
    const pattern = new RegExp(`${name}\\s*=\\s*("([^"]*)"|'([^']*)')`, "i");
    const match = tagSource.match(pattern);
    return decodeXmlEntities(match?.[2] ?? match?.[3] ?? "") || null;
};
export const extractSpokenText = (speechText) => {
    const sourceChars = [];
    let unsupportedSemantic = false;
    let hasMarkup = false;
    let index = 0;
    while (index < speechText.length) {
        const char = speechText[index];
        if (char !== "<") {
            if (char === "&") {
                const consumed = appendDecodedXmlEntity(sourceChars, speechText, index);
                if (consumed > 0) {
                    index += consumed;
                    continue;
                }
            }
            appendSourceText(sourceChars, char, index);
            index++;
            continue;
        }
        const closeIndex = speechText.indexOf(">", index + 1);
        if (closeIndex < 0) {
            appendSourceText(sourceChars, char, index);
            index++;
            continue;
        }
        hasMarkup = true;
        const tagSource = speechText.slice(index, closeIndex + 1);
        const tagName = getTagName(tagSource);
        const isClosing = /^<\s*\//.test(tagSource);
        const isSelfClosing = /\/\s*>$/.test(tagSource);
        if (!isClosing && tagName === "sub") {
            const alias = getAttribute(tagSource, "alias");
            if (!alias) {
                unsupportedSemantic = true;
                index = closeIndex + 1;
                continue;
            }
            appendSourceText(sourceChars, alias, null);
            const closingSub = speechText
                .toLowerCase()
                .indexOf("</sub>", closeIndex + 1);
            index = closingSub >= 0 ? closingSub + "</sub>".length : closeIndex + 1;
            continue;
        }
        if (!isClosing && tagName === "break") {
            appendSpace(sourceChars);
            index = closeIndex + 1;
            continue;
        }
        if (!isClosing && UNSUPPORTED_SEMANTIC_SSML_TAGS.has(tagName)) {
            unsupportedSemantic = true;
            if (isSelfClosing) {
                index = closeIndex + 1;
                continue;
            }
        }
        if (tagName &&
            !STRUCTURAL_SSML_TAGS.has(tagName) &&
            !UNSUPPORTED_SEMANTIC_SSML_TAGS.has(tagName) &&
            tagName !== "break" &&
            tagName !== "sub") {
            unsupportedSemantic = true;
        }
        if (!isClosing && (tagName === "p" || tagName === "s")) {
            appendSpace(sourceChars);
        }
        index = closeIndex + 1;
    }
    const normalized = normalizeSourceChars(sourceChars);
    return {
        spokenText: normalizeTextForSpeech(normalized.text),
        rawToSpokenOffsetMap: normalized.rawToSpokenOffsetMap,
        unsupportedSemantic,
        hasMarkup,
    };
};
//# sourceMappingURL=spoken-text.js.map