import { r as registerInstance, h, H as Host, a as getElement } from './index-DBTJNfo7.js';
import { t as translate } from './translations-DVRaJQvC.js';

class Diff {
    diff(oldStr, newStr, 
    // Type below is not accurate/complete - see above for full possibilities - but it compiles
    options = {}) {
        let callback;
        if (typeof options === 'function') {
            callback = options;
            options = {};
        }
        else if ('callback' in options) {
            callback = options.callback;
        }
        // Allow subclasses to massage the input prior to running
        const oldString = this.castInput(oldStr, options);
        const newString = this.castInput(newStr, options);
        const oldTokens = this.removeEmpty(this.tokenize(oldString, options));
        const newTokens = this.removeEmpty(this.tokenize(newString, options));
        return this.diffWithOptionsObj(oldTokens, newTokens, options, callback);
    }
    diffWithOptionsObj(oldTokens, newTokens, options, callback) {
        var _a;
        const done = (value) => {
            value = this.postProcess(value, options);
            if (callback) {
                setTimeout(function () { callback(value); }, 0);
                return undefined;
            }
            else {
                return value;
            }
        };
        const newLen = newTokens.length, oldLen = oldTokens.length;
        let editLength = 1;
        let maxEditLength = newLen + oldLen;
        if (options.maxEditLength != null) {
            maxEditLength = Math.min(maxEditLength, options.maxEditLength);
        }
        const maxExecutionTime = (_a = options.timeout) !== null && _a !== void 0 ? _a : Infinity;
        const abortAfterTimestamp = Date.now() + maxExecutionTime;
        const bestPath = [{ oldPos: -1, lastComponent: undefined }];
        // Seed editLength = 0, i.e. the content starts with the same values
        let newPos = this.extractCommon(bestPath[0], newTokens, oldTokens, 0, options);
        if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
            // Identity per the equality and tokenizer
            return done(this.buildValues(bestPath[0].lastComponent, newTokens, oldTokens));
        }
        // Once we hit the right edge of the edit graph on some diagonal k, we can
        // definitely reach the end of the edit graph in no more than k edits, so
        // there's no point in considering any moves to diagonal k+1 any more (from
        // which we're guaranteed to need at least k+1 more edits).
        // Similarly, once we've reached the bottom of the edit graph, there's no
        // point considering moves to lower diagonals.
        // We record this fact by setting minDiagonalToConsider and
        // maxDiagonalToConsider to some finite value once we've hit the edge of
        // the edit graph.
        // This optimization is not faithful to the original algorithm presented in
        // Myers's paper, which instead pointlessly extends D-paths off the end of
        // the edit graph - see page 7 of Myers's paper which notes this point
        // explicitly and illustrates it with a diagram. This has major performance
        // implications for some common scenarios. For instance, to compute a diff
        // where the new text simply appends d characters on the end of the
        // original text of length n, the true Myers algorithm will take O(n+d^2)
        // time while this optimization needs only O(n+d) time.
        let minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
        // Main worker method. checks all permutations of a given edit length for acceptance.
        const execEditLength = () => {
            for (let diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
                let basePath;
                const removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
                if (removePath) {
                    // No one else is going to attempt to use this value, clear it
                    // @ts-expect-error - perf optimisation. This type-violating value will never be read.
                    bestPath[diagonalPath - 1] = undefined;
                }
                let canAdd = false;
                if (addPath) {
                    // what newPos will be after we do an insertion:
                    const addPathNewPos = addPath.oldPos - diagonalPath;
                    canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
                }
                const canRemove = removePath && removePath.oldPos + 1 < oldLen;
                if (!canAdd && !canRemove) {
                    // If this path is a terminal then prune
                    // @ts-expect-error - perf optimisation. This type-violating value will never be read.
                    bestPath[diagonalPath] = undefined;
                    continue;
                }
                // Select the diagonal that we want to branch from. We select the prior
                // path whose position in the old string is the farthest from the origin
                // and does not pass the bounds of the diff graph
                if (!canRemove || (canAdd && removePath.oldPos < addPath.oldPos)) {
                    basePath = this.addToPath(addPath, true, false, 0, options);
                }
                else {
                    basePath = this.addToPath(removePath, false, true, 1, options);
                }
                newPos = this.extractCommon(basePath, newTokens, oldTokens, diagonalPath, options);
                if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
                    // If we have hit the end of both strings, then we are done
                    return done(this.buildValues(basePath.lastComponent, newTokens, oldTokens)) || true;
                }
                else {
                    bestPath[diagonalPath] = basePath;
                    if (basePath.oldPos + 1 >= oldLen) {
                        maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
                    }
                    if (newPos + 1 >= newLen) {
                        minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
                    }
                }
            }
            editLength++;
        };
        // Performs the length of edit iteration. Is a bit fugly as this has to support the
        // sync and async mode which is never fun. Loops over execEditLength until a value
        // is produced, or until the edit length exceeds options.maxEditLength (if given),
        // in which case it will return undefined.
        if (callback) {
            (function exec() {
                setTimeout(function () {
                    if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
                        return callback(undefined);
                    }
                    if (!execEditLength()) {
                        exec();
                    }
                }, 0);
            }());
        }
        else {
            while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
                const ret = execEditLength();
                if (ret) {
                    return ret;
                }
            }
        }
    }
    addToPath(path, added, removed, oldPosInc, options) {
        const last = path.lastComponent;
        if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
            return {
                oldPos: path.oldPos + oldPosInc,
                lastComponent: { count: last.count + 1, added: added, removed: removed, previousComponent: last.previousComponent }
            };
        }
        else {
            return {
                oldPos: path.oldPos + oldPosInc,
                lastComponent: { count: 1, added: added, removed: removed, previousComponent: last }
            };
        }
    }
    extractCommon(basePath, newTokens, oldTokens, diagonalPath, options) {
        const newLen = newTokens.length, oldLen = oldTokens.length;
        let oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
        while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(oldTokens[oldPos + 1], newTokens[newPos + 1], options)) {
            newPos++;
            oldPos++;
            commonCount++;
            if (options.oneChangePerToken) {
                basePath.lastComponent = { count: 1, previousComponent: basePath.lastComponent, added: false, removed: false };
            }
        }
        if (commonCount && !options.oneChangePerToken) {
            basePath.lastComponent = { count: commonCount, previousComponent: basePath.lastComponent, added: false, removed: false };
        }
        basePath.oldPos = oldPos;
        return newPos;
    }
    equals(left, right, options) {
        if (options.comparator) {
            return options.comparator(left, right);
        }
        else {
            return left === right
                || (!!options.ignoreCase && left.toLowerCase() === right.toLowerCase());
        }
    }
    removeEmpty(array) {
        const ret = [];
        for (let i = 0; i < array.length; i++) {
            if (array[i]) {
                ret.push(array[i]);
            }
        }
        return ret;
    }
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    castInput(value, options) {
        return value;
    }
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    tokenize(value, options) {
        return Array.from(value);
    }
    join(chars) {
        // Assumes ValueT is string, which is the case for most subclasses.
        // When it's false, e.g. in diffArrays, this method needs to be overridden (e.g. with a no-op)
        // Yes, the casts are verbose and ugly, because this pattern - of having the base class SORT OF
        // assume tokens and values are strings, but not completely - is weird and janky.
        return chars.join('');
    }
    postProcess(changeObjects, 
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    options) {
        return changeObjects;
    }
    get useLongestToken() {
        return false;
    }
    buildValues(lastComponent, newTokens, oldTokens) {
        // First we convert our linked list of components in reverse order to an
        // array in the right order:
        const components = [];
        let nextComponent;
        while (lastComponent) {
            components.push(lastComponent);
            nextComponent = lastComponent.previousComponent;
            delete lastComponent.previousComponent;
            lastComponent = nextComponent;
        }
        components.reverse();
        const componentLen = components.length;
        let componentPos = 0, newPos = 0, oldPos = 0;
        for (; componentPos < componentLen; componentPos++) {
            const component = components[componentPos];
            if (!component.removed) {
                if (!component.added && this.useLongestToken) {
                    let value = newTokens.slice(newPos, newPos + component.count);
                    value = value.map(function (value, i) {
                        const oldValue = oldTokens[oldPos + i];
                        return oldValue.length > value.length ? oldValue : value;
                    });
                    component.value = this.join(value);
                }
                else {
                    component.value = this.join(newTokens.slice(newPos, newPos + component.count));
                }
                newPos += component.count;
                // Common case
                if (!component.added) {
                    oldPos += component.count;
                }
            }
            else {
                component.value = this.join(oldTokens.slice(oldPos, oldPos + component.count));
                oldPos += component.count;
            }
        }
        return components;
    }
}

function longestCommonPrefix(str1, str2) {
    let i;
    for (i = 0; i < str1.length && i < str2.length; i++) {
        if (str1[i] != str2[i]) {
            return str1.slice(0, i);
        }
    }
    return str1.slice(0, i);
}
function longestCommonSuffix(str1, str2) {
    let i;
    // Unlike longestCommonPrefix, we need a special case to handle all scenarios
    // where we return the empty string since str1.slice(-0) will return the
    // entire string.
    if (!str1 || !str2 || str1[str1.length - 1] != str2[str2.length - 1]) {
        return '';
    }
    for (i = 0; i < str1.length && i < str2.length; i++) {
        if (str1[str1.length - (i + 1)] != str2[str2.length - (i + 1)]) {
            return str1.slice(-i);
        }
    }
    return str1.slice(-i);
}
function replacePrefix(string, oldPrefix, newPrefix) {
    if (string.slice(0, oldPrefix.length) != oldPrefix) {
        throw Error(`string ${JSON.stringify(string)} doesn't start with prefix ${JSON.stringify(oldPrefix)}; this is a bug`);
    }
    return newPrefix + string.slice(oldPrefix.length);
}
function replaceSuffix(string, oldSuffix, newSuffix) {
    if (!oldSuffix) {
        return string + newSuffix;
    }
    if (string.slice(-oldSuffix.length) != oldSuffix) {
        throw Error(`string ${JSON.stringify(string)} doesn't end with suffix ${JSON.stringify(oldSuffix)}; this is a bug`);
    }
    return string.slice(0, -oldSuffix.length) + newSuffix;
}
function removePrefix(string, oldPrefix) {
    return replacePrefix(string, oldPrefix, '');
}
function removeSuffix(string, oldSuffix) {
    return replaceSuffix(string, oldSuffix, '');
}
function maximumOverlap(string1, string2) {
    return string2.slice(0, overlapCount(string1, string2));
}
// Nicked from https://stackoverflow.com/a/60422853/1709587
function overlapCount(a, b) {
    // Deal with cases where the strings differ in length
    let startA = 0;
    if (a.length > b.length) {
        startA = a.length - b.length;
    }
    let endB = b.length;
    if (a.length < b.length) {
        endB = a.length;
    }
    // Create a back-reference for each index
    //   that should be followed in case of a mismatch.
    //   We only need B to make these references:
    const map = Array(endB);
    let k = 0; // Index that lags behind j
    map[0] = 0;
    for (let j = 1; j < endB; j++) {
        if (b[j] == b[k]) {
            map[j] = map[k]; // skip over the same character (optional optimisation)
        }
        else {
            map[j] = k;
        }
        while (k > 0 && b[j] != b[k]) {
            k = map[k];
        }
        if (b[j] == b[k]) {
            k++;
        }
    }
    // Phase 2: use these references while iterating over A
    k = 0;
    for (let i = startA; i < a.length; i++) {
        while (k > 0 && a[i] != b[k]) {
            k = map[k];
        }
        if (a[i] == b[k]) {
            k++;
        }
    }
    return k;
}
function trailingWs(string) {
    // Yes, this looks overcomplicated and dumb - why not replace the whole function with
    //     return string.match(/\s*$/)[0]
    // you ask? Because:
    // 1. the trap described at https://markamery.com/blog/quadratic-time-regexes/ would mean doing
    //    this would cause this function to take O(n²) time in the worst case (specifically when
    //    there is a massive run of NON-TRAILING whitespace in `string`), and
    // 2. the fix proposed in the same blog post, of using a negative lookbehind, is incompatible
    //    with old Safari versions that we'd like to not break if possible (see
    //    https://github.com/kpdecker/jsdiff/pull/550)
    // It feels absurd to do this with an explicit loop instead of a regex, but I really can't see a
    // better way that doesn't result in broken behaviour.
    let i;
    for (i = string.length - 1; i >= 0; i--) {
        if (!string[i].match(/\s/)) {
            break;
        }
    }
    return string.substring(i + 1);
}
function leadingWs(string) {
    // Thankfully the annoying considerations described in trailingWs don't apply here:
    const match = string.match(/^\s*/);
    return match ? match[0] : '';
}

// Based on https://en.wikipedia.org/wiki/Latin_script_in_Unicode
//
// Chars/ranges counted as "word" characters by this regex are as follows:
//
// + U+00AD  Soft hyphen
// + 00C0–00FF (letters with diacritics from the Latin-1 Supplement), except:
//   - U+00D7  × Multiplication sign
//   - U+00F7  ÷ Division sign
// + Latin Extended-A, 0100–017F
// + Latin Extended-B, 0180–024F
// + IPA Extensions, 0250–02AF
// + Spacing Modifier Letters, 02B0–02FF, except:
//   - U+02C7  ˇ &#711;  Caron
//   - U+02D8  ˘ &#728;  Breve
//   - U+02D9  ˙ &#729;  Dot Above
//   - U+02DA  ˚ &#730;  Ring Above
//   - U+02DB  ˛ &#731;  Ogonek
//   - U+02DC  ˜ &#732;  Small Tilde
//   - U+02DD  ˝ &#733;  Double Acute Accent
// + Latin Extended Additional, 1E00–1EFF
const extendedWordChars = 'a-zA-Z0-9_\\u{AD}\\u{C0}-\\u{D6}\\u{D8}-\\u{F6}\\u{F8}-\\u{2C6}\\u{2C8}-\\u{2D7}\\u{2DE}-\\u{2FF}\\u{1E00}-\\u{1EFF}';
// Each token is one of the following:
// - A punctuation mark plus the surrounding whitespace
// - A word plus the surrounding whitespace
// - Pure whitespace (but only in the special case where the entire text
//   is just whitespace)
//
// We have to include surrounding whitespace in the tokens because the two
// alternative approaches produce horribly broken results:
// * If we just discard the whitespace, we can't fully reproduce the original
//   text from the sequence of tokens and any attempt to render the diff will
//   get the whitespace wrong.
// * If we have separate tokens for whitespace, then in a typical text every
//   second token will be a single space character. But this often results in
//   the optimal diff between two texts being a perverse one that preserves
//   the spaces between words but deletes and reinserts actual common words.
//   See https://github.com/kpdecker/jsdiff/issues/160#issuecomment-1866099640
//   for an example.
//
// Keeping the surrounding whitespace of course has implications for .equals
// and .join, not just .tokenize.
// This regex does NOT fully implement the tokenization rules described above.
// Instead, it gives runs of whitespace their own "token". The tokenize method
// then handles stitching whitespace tokens onto adjacent word or punctuation
// tokens.
const tokenizeIncludingWhitespace = new RegExp(`[${extendedWordChars}]+|\\s+|[^${extendedWordChars}]`, 'ug');
class WordDiff extends Diff {
    equals(left, right, options) {
        if (options.ignoreCase) {
            left = left.toLowerCase();
            right = right.toLowerCase();
        }
        return left.trim() === right.trim();
    }
    tokenize(value, options = {}) {
        let parts;
        if (options.intlSegmenter) {
            const segmenter = options.intlSegmenter;
            if (segmenter.resolvedOptions().granularity != 'word') {
                throw new Error('The segmenter passed must have a granularity of "word"');
            }
            // We want `parts` to be an array whose elements alternate between being
            // pure whitespace and being pure non-whitespace. This is ALMOST what the
            // segments returned by a word-based Intl.Segmenter already look like,
            // and therefore we can ALMOST get what we want by simply doing...
            //     parts = Array.from(segmenter.segment(value), segment => segment.segment);
            // ... but not QUITE, because there's of one annoying special case: every
            // newline character gets its own segment, instead of sharing a segment
            // with other surrounding whitespace. We therefore need to manually merge
            // consecutive segments of whitespace into a single part:
            parts = [];
            for (const segmentObj of Array.from(segmenter.segment(value))) {
                const segment = segmentObj.segment;
                if (parts.length && (/\s/).test(parts[parts.length - 1]) && (/\s/).test(segment)) {
                    parts[parts.length - 1] += segment;
                }
                else {
                    parts.push(segment);
                }
            }
        }
        else {
            parts = value.match(tokenizeIncludingWhitespace) || [];
        }
        const tokens = [];
        let prevPart = null;
        parts.forEach(part => {
            if ((/\s/).test(part)) {
                if (prevPart == null) {
                    tokens.push(part);
                }
                else {
                    tokens.push(tokens.pop() + part);
                }
            }
            else if (prevPart != null && (/\s/).test(prevPart)) {
                if (tokens[tokens.length - 1] == prevPart) {
                    tokens.push(tokens.pop() + part);
                }
                else {
                    tokens.push(prevPart + part);
                }
            }
            else {
                tokens.push(part);
            }
            prevPart = part;
        });
        return tokens;
    }
    join(tokens) {
        // Tokens being joined here will always have appeared consecutively in the
        // same text, so we can simply strip off the leading whitespace from all the
        // tokens except the first (and except any whitespace-only tokens - but such
        // a token will always be the first and only token anyway) and then join them
        // and the whitespace around words and punctuation will end up correct.
        return tokens.map((token, i) => {
            if (i == 0) {
                return token;
            }
            else {
                return token.replace((/^\s+/), '');
            }
        }).join('');
    }
    postProcess(changes, options) {
        if (!changes || options.oneChangePerToken) {
            return changes;
        }
        let lastKeep = null;
        // Change objects representing any insertion or deletion since the last
        // "keep" change object. There can be at most one of each.
        let insertion = null;
        let deletion = null;
        changes.forEach(change => {
            if (change.added) {
                insertion = change;
            }
            else if (change.removed) {
                deletion = change;
            }
            else {
                if (insertion || deletion) { // May be false at start of text
                    dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, change);
                }
                lastKeep = change;
                insertion = null;
                deletion = null;
            }
        });
        if (insertion || deletion) {
            dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, null);
        }
        return changes;
    }
}
const wordDiff = new WordDiff();
function diffWords(oldStr, newStr, options) {
    return wordDiff.diff(oldStr, newStr, options);
}
function dedupeWhitespaceInChangeObjects(startKeep, deletion, insertion, endKeep) {
    // Before returning, we tidy up the leading and trailing whitespace of the
    // change objects to eliminate cases where trailing whitespace in one object
    // is repeated as leading whitespace in the next.
    // Below are examples of the outcomes we want here to explain the code.
    // I=insert, K=keep, D=delete
    // 1. diffing 'foo bar baz' vs 'foo baz'
    //    Prior to cleanup, we have K:'foo ' D:' bar ' K:' baz'
    //    After cleanup, we want:   K:'foo ' D:'bar ' K:'baz'
    //
    // 2. Diffing 'foo bar baz' vs 'foo qux baz'
    //    Prior to cleanup, we have K:'foo ' D:' bar ' I:' qux ' K:' baz'
    //    After cleanup, we want K:'foo ' D:'bar' I:'qux' K:' baz'
    //
    // 3. Diffing 'foo\nbar baz' vs 'foo baz'
    //    Prior to cleanup, we have K:'foo ' D:'\nbar ' K:' baz'
    //    After cleanup, we want K'foo' D:'\nbar' K:' baz'
    //
    // 4. Diffing 'foo baz' vs 'foo\nbar baz'
    //    Prior to cleanup, we have K:'foo\n' I:'\nbar ' K:' baz'
    //    After cleanup, we ideally want K'foo' I:'\nbar' K:' baz'
    //    but don't actually manage this currently (the pre-cleanup change
    //    objects don't contain enough information to make it possible).
    //
    // 5. Diffing 'foo   bar baz' vs 'foo  baz'
    //    Prior to cleanup, we have K:'foo  ' D:'   bar ' K:'  baz'
    //    After cleanup, we want K:'foo  ' D:' bar ' K:'baz'
    //
    // Our handling is unavoidably imperfect in the case where there's a single
    // indel between keeps and the whitespace has changed. For instance, consider
    // diffing 'foo\tbar\nbaz' vs 'foo baz'. Unless we create an extra change
    // object to represent the insertion of the space character (which isn't even
    // a token), we have no way to avoid losing information about the texts'
    // original whitespace in the result we return. Still, we do our best to
    // output something that will look sensible if we e.g. print it with
    // insertions in green and deletions in red.
    // Between two "keep" change objects (or before the first or after the last
    // change object), we can have either:
    // * A "delete" followed by an "insert"
    // * Just an "insert"
    // * Just a "delete"
    // We handle the three cases separately.
    if (deletion && insertion) {
        const oldWsPrefix = leadingWs(deletion.value);
        const oldWsSuffix = trailingWs(deletion.value);
        const newWsPrefix = leadingWs(insertion.value);
        const newWsSuffix = trailingWs(insertion.value);
        if (startKeep) {
            const commonWsPrefix = longestCommonPrefix(oldWsPrefix, newWsPrefix);
            startKeep.value = replaceSuffix(startKeep.value, newWsPrefix, commonWsPrefix);
            deletion.value = removePrefix(deletion.value, commonWsPrefix);
            insertion.value = removePrefix(insertion.value, commonWsPrefix);
        }
        if (endKeep) {
            const commonWsSuffix = longestCommonSuffix(oldWsSuffix, newWsSuffix);
            endKeep.value = replacePrefix(endKeep.value, newWsSuffix, commonWsSuffix);
            deletion.value = removeSuffix(deletion.value, commonWsSuffix);
            insertion.value = removeSuffix(insertion.value, commonWsSuffix);
        }
    }
    else if (insertion) {
        // The whitespaces all reflect what was in the new text rather than
        // the old, so we essentially have no information about whitespace
        // insertion or deletion. We just want to dedupe the whitespace.
        // We do that by having each change object keep its trailing
        // whitespace and deleting duplicate leading whitespace where
        // present.
        if (startKeep) {
            const ws = leadingWs(insertion.value);
            insertion.value = insertion.value.substring(ws.length);
        }
        if (endKeep) {
            const ws = leadingWs(endKeep.value);
            endKeep.value = endKeep.value.substring(ws.length);
        }
        // otherwise we've got a deletion and no insertion
    }
    else if (startKeep && endKeep) {
        const newWsFull = leadingWs(endKeep.value), delWsStart = leadingWs(deletion.value), delWsEnd = trailingWs(deletion.value);
        // Any whitespace that comes straight after startKeep in both the old and
        // new texts, assign to startKeep and remove from the deletion.
        const newWsStart = longestCommonPrefix(newWsFull, delWsStart);
        deletion.value = removePrefix(deletion.value, newWsStart);
        // Any whitespace that comes straight before endKeep in both the old and
        // new texts, and hasn't already been assigned to startKeep, assign to
        // endKeep and remove from the deletion.
        const newWsEnd = longestCommonSuffix(removePrefix(newWsFull, newWsStart), delWsEnd);
        deletion.value = removeSuffix(deletion.value, newWsEnd);
        endKeep.value = replacePrefix(endKeep.value, newWsFull, newWsEnd);
        // If there's any whitespace from the new text that HASN'T already been
        // assigned, assign it to the start:
        startKeep.value = replaceSuffix(startKeep.value, newWsFull, newWsFull.slice(0, newWsFull.length - newWsEnd.length));
    }
    else if (endKeep) {
        // We are at the start of the text. Preserve all the whitespace on
        // endKeep, and just remove whitespace from the end of deletion to the
        // extent that it overlaps with the start of endKeep.
        const endKeepWsPrefix = leadingWs(endKeep.value);
        const deletionWsSuffix = trailingWs(deletion.value);
        const overlap = maximumOverlap(deletionWsSuffix, endKeepWsPrefix);
        deletion.value = removeSuffix(deletion.value, overlap);
    }
    else if (startKeep) {
        // We are at the END of the text. Preserve all the whitespace on
        // startKeep, and just remove whitespace from the start of deletion to
        // the extent that it overlaps with the end of startKeep.
        const startKeepWsSuffix = trailingWs(startKeep.value);
        const deletionWsPrefix = leadingWs(deletion.value);
        const overlap = maximumOverlap(startKeepWsSuffix, deletionWsPrefix);
        deletion.value = removePrefix(deletion.value, overlap);
    }
}

class LineDiff extends Diff {
    constructor() {
        super(...arguments);
        this.tokenize = tokenize$1;
    }
    equals(left, right, options) {
        // If we're ignoring whitespace, we need to normalise lines by stripping
        // whitespace before checking equality. (This has an annoying interaction
        // with newlineIsToken that requires special handling: if newlines get their
        // own token, then we DON'T want to trim the *newline* tokens down to empty
        // strings, since this would cause us to treat whitespace-only line content
        // as equal to a separator between lines, which would be weird and
        // inconsistent with the documented behavior of the options.)
        if (options.ignoreWhitespace) {
            if (!options.newlineIsToken || !left.includes('\n')) {
                left = left.trim();
            }
            if (!options.newlineIsToken || !right.includes('\n')) {
                right = right.trim();
            }
        }
        else if (options.ignoreNewlineAtEof && !options.newlineIsToken) {
            if (left.endsWith('\n')) {
                left = left.slice(0, -1);
            }
            if (right.endsWith('\n')) {
                right = right.slice(0, -1);
            }
        }
        return super.equals(left, right, options);
    }
}
const lineDiff = new LineDiff();
function diffLines(oldStr, newStr, options) {
    return lineDiff.diff(oldStr, newStr, options);
}
// Exported standalone so it can be used from jsonDiff too.
function tokenize$1(value, options) {
    if (options.stripTrailingCr) {
        // remove one \r before \n to match GNU diff's --strip-trailing-cr behavior
        value = value.replace(/\r\n/g, '\n');
    }
    const retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
    // Ignore the final empty token that occurs if the string ends with a new line
    if (!linesAndNewlines[linesAndNewlines.length - 1]) {
        linesAndNewlines.pop();
    }
    // Merge the content and line separators into single tokens
    for (let i = 0; i < linesAndNewlines.length; i++) {
        const line = linesAndNewlines[i];
        if (i % 2 && !options.newlineIsToken) {
            retLines[retLines.length - 1] += line;
        }
        else {
            retLines.push(line);
        }
    }
    return retLines;
}

/**
 * Compute a structured diff between two strings.
 *
 * @param oldText - the "before" text
 * @param newText - the "after" text
 * @param contextLines - number of unchanged lines to show around each change
 * @returns a DiffResult with hunks, additions, and deletions counts
 */
function computeDiff(oldText, newText, contextLines = 3) {
    const allLines = buildDiffLines(oldText, newText);
    return groupIntoHunks(allLines, contextLines);
}
/**
 * Build paired rows for split (side-by-side) view from flat diff lines.
 * Context lines appear on both sides. Adjacent removed+added lines
 * are paired into the same row.
 *
 * @param lines - flat diff lines
 * @returns paired rows for split rendering
 */
function buildSplitLines(lines) {
    const rows = [];
    let i = 0;
    while (i < lines.length) {
        const line = lines[i];
        if (line.type === 'context') {
            rows.push({ left: line, right: line });
            i++;
            continue;
        }
        i = collectAndPairChanges(lines, i, rows);
    }
    return rows;
}
/**
 * Collect consecutive removed then added lines starting at `index`,
 * pair them into split rows, and return the new index.
 * @param lines - flat diff lines
 * @param index - starting index
 * @param rows - output array to push paired rows into
 */
function collectAndPairChanges(lines, index, rows) {
    const removed = [];
    while (index < lines.length && lines[index].type === 'removed') {
        removed.push(lines[index]);
        index++;
    }
    const added = [];
    while (index < lines.length && lines[index].type === 'added') {
        added.push(lines[index]);
        index++;
    }
    const maxPairs = Math.max(removed.length, added.length);
    for (let j = 0; j < maxPairs; j++) {
        rows.push({
            left: j < removed.length ? removed[j] : undefined,
            right: j < added.length ? added[j] : undefined,
        });
    }
    return index;
}
/**
 * Normalize values for diffing. If `reformatJson` is true,
 * parse and re-stringify with sorted keys and consistent indentation.
 * @param value
 * @param reformatJson
 */
function normalizeForDiff(value, reformatJson = false) {
    if (typeof value === 'object' && value !== null) {
        return JSON.stringify(sortKeysDeep(value), null, 4);
    }
    if (typeof value === 'string' && reformatJson) {
        try {
            const parsed = JSON.parse(value);
            return JSON.stringify(sortKeysDeep(parsed), null, 4);
        }
        catch (_a) {
            return value;
        }
    }
    return String(value !== null && value !== void 0 ? value : '');
}
function sortKeysDeep(obj) {
    if (Array.isArray(obj)) {
        return obj.map(sortKeysDeep);
    }
    if (obj !== null && typeof obj === 'object') {
        const sorted = {};
        const keys = Object.keys(obj).sort((a, b) => a.localeCompare(b));
        for (const key of keys) {
            sorted[key] = sortKeysDeep(obj[key]);
        }
        return sorted;
    }
    return obj;
}
/**
 * Build a flat list of DiffLines from two text strings.
 * @param oldText
 * @param newText
 */
function buildDiffLines(oldText, newText) {
    const changes = diffLines(oldText, newText);
    const lines = [];
    let oldLineNum = 1;
    let newLineNum = 1;
    for (const change of changes) {
        const changeLines = splitIntoLines(change.value);
        for (const line of changeLines) {
            if (change.added) {
                lines.push({
                    type: 'added',
                    content: line,
                    newLineNumber: newLineNum++,
                });
            }
            else if (change.removed) {
                lines.push({
                    type: 'removed',
                    content: line,
                    oldLineNumber: oldLineNum++,
                });
            }
            else {
                lines.push({
                    type: 'context',
                    content: line,
                    oldLineNumber: oldLineNum++,
                    newLineNumber: newLineNum++,
                });
            }
        }
    }
    addWordLevelHighlighting(lines);
    return lines;
}
/**
 * Split a string into lines, handling the trailing newline
 * that jsdiff includes in each change value.
 * @param text
 */
function splitIntoLines(text) {
    if (!text) {
        return [];
    }
    const lines = text.split('\n');
    // jsdiff includes a trailing newline, producing an empty last element
    if (lines.length > 0 && lines.at(-1) === '') {
        lines.pop();
    }
    return lines;
}
/**
 * Pair adjacent removed+added lines and compute word-level diffs
 * to highlight only the specific segments that changed.
 * @param lines
 */
function addWordLevelHighlighting(lines) {
    let i = 0;
    while (i < lines.length) {
        // Find consecutive removed lines
        const removedStart = i;
        while (i < lines.length && lines[i].type === 'removed') {
            i++;
        }
        const removedEnd = i;
        // Find consecutive added lines right after
        const addedStart = i;
        while (i < lines.length && lines[i].type === 'added') {
            i++;
        }
        const addedEnd = i;
        const removedCount = removedEnd - removedStart;
        const addedCount = addedEnd - addedStart;
        // Pair them up for word-level highlighting
        if (removedCount > 0 && addedCount > 0) {
            const pairCount = Math.min(removedCount, addedCount);
            for (let j = 0; j < pairCount; j++) {
                const removedLine = lines[removedStart + j];
                const addedLine = lines[addedStart + j];
                const [removedSegments, addedSegments] = computeWordSegments(removedLine.content, addedLine.content);
                removedLine.segments = removedSegments;
                addedLine.segments = addedSegments;
            }
        }
        // Skip context lines
        if (i === removedStart) {
            i++;
        }
    }
}
/**
 * Compute word-level diff segments for a pair of lines.
 * @param oldContent
 * @param newContent
 */
function computeWordSegments(oldContent, newContent) {
    const wordChanges = diffWords(oldContent, newContent);
    const removedSegments = [];
    const addedSegments = [];
    for (const change of wordChanges) {
        if (change.added) {
            addedSegments.push({ value: change.value, type: 'added' });
        }
        else if (change.removed) {
            removedSegments.push({ value: change.value, type: 'removed' });
        }
        else {
            removedSegments.push({ value: change.value, type: 'equal' });
            addedSegments.push({ value: change.value, type: 'equal' });
        }
    }
    return [removedSegments, addedSegments];
}
/**
 * Group a flat list of diff lines into hunks with context.
 * @param lines
 * @param contextLines
 */
function groupIntoHunks(lines, contextLines) {
    if (lines.length === 0) {
        return { hunks: [], additions: 0, deletions: 0, allLines: lines };
    }
    let additions = 0;
    let deletions = 0;
    for (const line of lines) {
        if (line.type === 'added') {
            additions++;
        }
        else if (line.type === 'removed') {
            deletions++;
        }
    }
    // If there are no changes, return a single empty result
    if (additions === 0 && deletions === 0) {
        return { hunks: [], additions: 0, deletions: 0, allLines: lines };
    }
    // Find ranges of changed lines with their context
    const changeIndices = [];
    for (const [i, line] of lines.entries()) {
        if (line.type !== 'context') {
            changeIndices.push(i);
        }
    }
    // Build hunk boundaries
    const hunkBoundaries = buildHunkBoundaries(changeIndices, lines.length, contextLines);
    const hunks = [];
    let prevEnd = 0;
    for (const boundary of hunkBoundaries) {
        const hunkLines = lines.slice(boundary.start, boundary.end);
        const hiddenBefore = boundary.start - prevEnd;
        const collapsedBefore = hiddenBefore > 0 ? hiddenBefore : undefined;
        hunks.push({
            lines: hunkLines,
            collapsedBefore,
            startIndex: boundary.start,
        });
        prevEnd = boundary.end;
    }
    // Calculate collapsed lines after the last hunk
    const lastBoundary = hunkBoundaries.at(-1);
    const collapsedAfter = lastBoundary.end < lines.length
        ? lines.length - lastBoundary.end
        : undefined;
    return { hunks, additions, deletions, collapsedAfter, allLines: lines };
}
/**
 * Build the start/end boundaries of each hunk based on change positions.
 * Merges hunks that overlap or are adjacent.
 * @param changeIndices
 * @param totalLines
 * @param contextLines
 */
function buildHunkBoundaries(changeIndices, totalLines, contextLines) {
    if (changeIndices.length === 0) {
        return [];
    }
    const boundaries = [];
    let currentStart = Math.max(0, changeIndices[0] - contextLines);
    let currentEnd = Math.min(totalLines, changeIndices[0] + contextLines + 1);
    for (let i = 1; i < changeIndices.length; i++) {
        const changeStart = Math.max(0, changeIndices[i] - contextLines);
        const changeEnd = Math.min(totalLines, changeIndices[i] + contextLines + 1);
        if (changeStart <= currentEnd) {
            // Merge overlapping hunks
            currentEnd = Math.max(currentEnd, changeEnd);
        }
        else {
            boundaries.push({ start: currentStart, end: currentEnd });
            currentStart = changeStart;
            currentEnd = changeEnd;
        }
    }
    boundaries.push({ start: currentStart, end: currentEnd });
    return boundaries;
}

/**
 * Tokenize a text fragment for syntax highlighting.
 * Returns the original text as a single plain token when the
 * language is not supported.
 *
 * @param text - the text to tokenize
 * @param language - the language identifier (e.g. "json")
 * @returns array of syntax tokens
 */
function tokenize(text, language) {
    if (!language || text.length === 0) {
        return [{ value: text, type: 'plain' }];
    }
    if (language === 'json') {
        return tokenizeJson(text);
    }
    return [{ value: text, type: 'plain' }];
}
// ─── JSON tokenizer ─────────────────────────────────────────────────
/**
 * Regex-based JSON tokenizer.
 * Handles partial lines (individual lines of a JSON document).
 */
const JSON_PATTERNS = [
    // String literals (keys and values)
    [/"(?:[^"\\]|\\.)*"/, 'string'],
    // Numbers
    [/-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/, 'number'],
    // Booleans
    [/\b(?:true|false)\b/, 'boolean'],
    // Null
    [/\bnull\b/, 'null'],
    // Punctuation
    [/[{}[\]:,]/, 'punctuation'],
];
const JSON_REGEX = new RegExp(JSON_PATTERNS.map(([re]) => `(${re.source})`).join('|'), 'g');
function tokenizeJson(text) {
    const tokens = [];
    let lastIndex = 0;
    JSON_REGEX.lastIndex = 0;
    let match = JSON_REGEX.exec(text);
    while (match !== null) {
        // Plain text before this match
        if (match.index > lastIndex) {
            tokens.push({
                value: text.slice(lastIndex, match.index),
                type: 'plain',
            });
        }
        // Determine which capture group matched
        const tokenType = getMatchedTokenType(match);
        const value = match[0];
        // Distinguish JSON keys from string values:
        // A key is a string followed by optional whitespace and a colon
        if (tokenType === 'string') {
            const afterMatch = text.slice(match.index + value.length);
            if (/^\s*:/.test(afterMatch)) {
                tokens.push({ value, type: 'key' });
            }
            else {
                tokens.push({ value, type: 'string' });
            }
        }
        else {
            tokens.push({ value, type: tokenType });
        }
        lastIndex = match.index + value.length;
        match = JSON_REGEX.exec(text);
    }
    // Remaining plain text
    if (lastIndex < text.length) {
        tokens.push({ value: text.slice(lastIndex), type: 'plain' });
    }
    return tokens;
}
/**
 * Determine which pattern matched by checking capture groups.
 * @param match - the regex match result
 */
function getMatchedTokenType(match) {
    for (const [index, [, type]] of JSON_PATTERNS.entries()) {
        if (match[index + 1] !== undefined) {
            return type;
        }
    }
    return 'plain';
}

/**
 * Pure utility functions for search-within-diff functionality.
 */
/**
 * Escape special regex characters in a search term so it can
 * be used as a literal pattern in a RegExp constructor.
 *
 * @param term - the raw search string
 * @returns regex-safe string
 */
function escapeRegex(term) {
    return term.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
}
/**
 * Build a case-insensitive regex that captures the search term.
 * Returns `null` when the term is empty.
 *
 * @param term - the raw search string
 * @returns a RegExp with a single capture group, or null
 */
function buildSearchRegex(term) {
    if (!term) {
        return null;
    }
    return new RegExp(`(${escapeRegex(term)})`, 'gi');
}
/**
 * Calculate the next match index when navigating forward or backward,
 * wrapping around at the boundaries.
 *
 * @param currentIndex - current zero-based match index
 * @param direction - +1 for next, -1 for previous
 * @param total - total number of matches
 * @returns the new match index
 */
function navigateMatchIndex(currentIndex, direction, total) {
    if (total === 0) {
        return 0;
    }
    return (currentIndex + direction + total) % total;
}

/**
 * Pure utility functions for extracting text content from diff structures.
 */
/**
 * Extract the text content of removed lines from a unified change block.
 *
 * @param lines - consecutive changed lines from a unified diff hunk
 * @returns the removed lines joined by newlines, or empty string if none
 */
function extractRemovedContent(lines) {
    return lines
        .filter((line) => line.type === 'removed')
        .map((line) => line.content)
        .join('\n');
}
/**
 * Extract the text content of removed lines from a split change block.
 *
 * @param rows - consecutive changed rows from a split diff hunk
 * @returns the removed lines joined by newlines, or empty string if none
 */
function extractRemovedContentFromSplit(rows) {
    return rows
        .filter((row) => { var _a; return ((_a = row.left) === null || _a === void 0 ? void 0 : _a.type) === 'removed'; })
        .map((row) => row.left.content)
        .join('\n');
}

const codeDiffCss = () => `@charset "UTF-8";*,*::before,*::after{box-sizing:border-box;min-width:0;min-height:0}:host(limel-code-diff){--diff-added-bg:rgb(var(--color-green-default), 0.1);--diff-added-bg-hover:rgb(var(--color-green-default), 0.3);--diff-added-highlight-bg:rgb(var(--color-green-default), 0.3);--diff-removed-bg:rgb(var(--color-red-default), 0.1);--diff-removed-bg-hover:rgb(var(--color-red-default), 0.3);--diff-removed-highlight-bg:rgb(var(--color-red-default), 0.3);--diff-context-bg:transparent;--diff-indicator-added-color:rgb(var(--color-green-default));--diff-indicator-removed-color:rgb(var(--color-red-default));--diff-stat-added-color:rgb(var(--color-green-default));--diff-stat-removed-color:rgb(var(--color-red-default));--search-match-bg:rgb(var(--color-amber-default), 0.3);--search-match-current-bg:rgb(var(--color-amber-default), 0.6);--diff-line-hover-bg:rgb(var(--contrast-800), 0.08);--diff-gutter-bg:rgb(var(--contrast-200));--diff-gutter-text-color:rgb(var(--contrast-700));--diff-text-color:rgb(var(--contrast-1100));--diff-border-color:rgb(var(--contrast-400));--diff-collapsed-bg:rgb(var(--contrast-200));--diff-collapsed-text-color:rgb(var(--contrast-800));--diff-header-bg:rgb(var(--contrast-200));--diff-empty-text-color:rgb(var(--contrast-700));--diff-split-divider-color:rgb(var(--contrast-400));--diff-empty-cell-bg:rgb(var(--contrast-100));--syntax-string-color:rgb(var(--color-green-dark));--syntax-number-color:rgb(var(--color-blue-default));--syntax-boolean-color:rgb(var(--color-amber-darker));--syntax-key-color:rgb(var(--color-sky-dark));--syntax-null-color:rgb(var(--contrast-700));--syntax-punctuation-color:rgb(var(--contrast-700));--search-bar-bg:rgb(var(--contrast-100));--search-bar-border:rgb(var(--contrast-400));--limel-code-diff-line-number-padding:0.25rem;font-family:ui-sans-serif, system-ui, sans-serif;display:flex;flex-direction:column;width:100%;height:100%;color:var(--diff-text-color);border:1px solid var(--diff-border-color);border-radius:0.5rem;max-height:var(--code-diff-max-height, none)}.screen-reader-only{position:absolute;width:0;height:0;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0, 0, 0, 0);clip-path:inset(50%);white-space:nowrap}.diff-header{flex-shrink:0;display:flex;align-items:center;justify-content:space-between;padding:0.125rem 0.125rem 0.125rem 0.5rem;background:var(--diff-header-bg);border-bottom:1px solid var(--diff-border-color);font-family:ui-sans-serif, system-ui, sans-serif;font-size:0.75rem;border-radius:0.5rem 0.5rem 0 0}.diff-header__labels{display:flex;gap:0.75rem;font-weight:500}.diff-header__old,.diff-header__new{padding:0.125rem 0.25rem;border-radius:0.25rem;box-shadow:var(--shadow-brighten-edges-outside)}.diff-header__old{background-color:var(--diff-removed-bg)}.diff-header__new{background-color:var(--diff-added-bg)}.diff-header__actions{display:flex;align-items:center;gap:0.25rem}.search-toggle--active{--limel-theme-on-surface-color:var(--mdc-theme-primary)}.diff-header__stats{font-family:ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace;display:flex;gap:0.5rem;margin-right:0.5rem}.stat{font-family:ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace;font-size:0.8125rem;font-weight:600}.stat--added{color:var(--diff-stat-added-color)}.stat--removed{color:var(--diff-stat-removed-color)}.search-bar{flex-shrink:0;display:flex;align-items:center;gap:0.25rem;padding:0.25rem 0 0.25rem 0.25rem;background:var(--search-bar-bg);border-bottom:1px solid var(--search-bar-border)}.search-bar limel-action-bar{--action-bar-background-color:transparent;min-width:2.5rem;flex-shrink:0}.search-bar limel-input-field{flex-grow:1}.search-bar__count{color:var(--diff-collapsed-text-color);white-space:nowrap;min-width:4rem;text-align:center;font-size:0.75rem}.search-match{background:var(--search-match-bg);color:inherit;border-radius:0.125rem}.search-match--current{background:var(--search-match-current-bg);outline:1px solid rgb(var(--color-amber-dark), 0.5)}.change-group{position:relative}.change-group__copy{--icon-background-color:rgb(var(--contrast-100));scale:0.9;position:absolute;top:0.125rem;right:0.5rem;display:none}.change-group:hover .change-group__copy{display:inline-flex}.diff-body{flex-grow:1;overflow-x:auto;font-family:ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace;font-size:var(--code-diff-font-size, 0.75rem)}.diff-body:focus{outline:none}.diff-body:focus-visible{outline:0.125rem solid rgb(var(--color-blue-default));outline-offset:0.125rem;border-radius:0.25rem}.diff-line{display:flex;align-items:stretch;min-height:1.25rem;line-height:1.25rem}.diff-line:hover .line-content{transition-duration:0.2s;background:var(--diff-line-hover-bg)}.diff-line--added .line-content{background:var(--diff-added-bg)}.diff-line--added .line-indicator{color:var(--diff-indicator-added-color)}.diff-line--added:hover .line-content{background:var(--diff-added-bg-hover)}.diff-line--removed .line-content{background:var(--diff-removed-bg)}.diff-line--removed .line-indicator{color:var(--diff-indicator-removed-color)}.diff-line--removed:hover .line-content{background:var(--diff-removed-bg-hover)}.diff-line--context .line-content{background:var(--diff-context-bg)}.diff-line--collapsed{display:flex;justify-content:center;background:var(--diff-collapsed-bg);border-top:1px solid var(--diff-border-color);border-bottom:1px solid var(--diff-border-color)}.diff-line--focused{outline:0.125rem solid rgb(var(--color-blue-default));outline-offset:-0.125rem;z-index:1}.line-number{display:inline-flex;align-items:center;justify-content:flex-end;min-width:calc(1ch + 2 * var(--limel-code-diff-line-number-padding));width:var(--limel-line-number-min-width);padding:0 var(--limel-code-diff-line-number-padding);background:var(--diff-gutter-bg);color:var(--diff-gutter-text-color);user-select:none;text-align:right;flex-shrink:0}.line-number--old{border-right:1px solid var(--diff-border-color)}.line-indicator{display:inline-flex;align-items:center;justify-content:center;width:1.25rem;flex-shrink:0;user-select:none;font-weight:600;background:var(--diff-gutter-bg)}.line-content{flex:1;padding:0 0.75rem;white-space:pre;overflow-x:visible;min-width:0;transition:background-color 0.6s ease}mark{background:transparent;color:inherit;border-radius:0.125rem}mark.segment--added{background:var(--diff-added-highlight-bg)}mark.segment--removed{background:var(--diff-removed-highlight-bg)}.syntax--string{color:var(--syntax-string-color)}.syntax--number{color:var(--syntax-number-color)}.syntax--boolean{color:var(--syntax-boolean-color)}.syntax--null{color:var(--syntax-null-color);font-style:italic}.syntax--key{color:var(--syntax-key-color)}.syntax--punctuation{color:var(--syntax-punctuation-color)}.expand-button{all:unset;transition:color var(--limel-clickable-transition-speed, 0.4s) ease, background-color var(--limel-clickable-transition-speed, 0.4s) ease, box-shadow var(--limel-clickable-transform-speed, 0.4s) ease, transform var(--limel-clickable-transform-speed, 0.4s) var(--limel-clickable-transform-timing-function, ease);cursor:pointer;color:var(--limel-theme-on-surface-color);background-color:transparent}.expand-button:hover,.expand-button:focus,.expand-button:focus-visible{will-change:color, background-color, box-shadow, transform}.expand-button:hover,.expand-button:focus-visible{transform:translate3d(0, 0.01rem, 0);color:var(--limel-theme-on-surface-color);background-color:var(--lime-elevated-surface-background-color)}.expand-button:hover{box-shadow:var(--button-shadow-hovered)}.expand-button:active{--limel-clickable-transform-timing-function:cubic-bezier(       0.83,       -0.15,       0.49,       1.16   );transform:translate3d(0, 0.05rem, 0);box-shadow:var(--button-shadow-pressed)}.expand-button:hover,.expand-button:active{--limel-clickable-transition-speed:0.2s;--limel-clickable-transform-speed:0.16s}.expand-button:focus{outline:none}.expand-button:focus-visible{outline:none;box-shadow:var(--shadow-depth-8-focused)}.expand-button{padding:0 0.75rem;border-radius:0.25rem;margin:0.125rem;width:calc(100% - 0.25rem);text-align:center}.diff-empty{padding:2rem;text-align:center;color:var(--diff-empty-text-color);font-style:italic}.diff-line--split{display:flex;align-items:stretch;min-height:1.25rem;line-height:1.25rem}.diff-line--split:hover .split-content--removed{background:var(--diff-removed-bg-hover)}.diff-line--split:hover .split-content--added{background:var(--diff-added-bg-hover)}.diff-line--split:hover .split-content--context{background:var(--diff-line-hover-bg)}.split-content{flex:1;padding:0 0.75rem;white-space:pre;overflow-x:visible;min-width:0}.split-content--left{border-right:1px solid var(--diff-split-divider-color)}.split-content--removed{background:var(--diff-removed-bg)}.split-content--added{background:var(--diff-added-bg)}.split-content--context{background:var(--diff-context-bg)}.split-content--empty{background:var(--diff-empty-cell-bg)}.split-content--left{overflow-x:hidden;text-overflow:ellipsis}.change-group:has(.diff-line--split)>.change-group__copy{right:calc(50% - var(--limel-line-number-min-width))}:host(limel-code-diff[line-wrapping]) .diff-body{overflow-x:hidden}:host(limel-code-diff[line-wrapping]) .line-content,:host(limel-code-diff[line-wrapping]) .split-content{white-space:pre-wrap;overflow-wrap:break-word;word-break:break-all}:host(limel-code-diff[line-wrapping]) .diff-line--split .split-content{flex:1 1 0%;max-width:calc(50% - var(--limel-line-number-min-width))}`;

const CodeDiff = class {
    constructor(hostRef) {
        registerInstance(this, hostRef);
        /**
         * The "before" value to compare.
         * Can be a string or an object (which will be serialized to JSON).
         */
        this.oldValue = '';
        /**
         * The "after" value to compare.
         * Can be a string or an object (which will be serialized to JSON).
         */
        this.newValue = '';
        /**
         * The layout of the diff view.
         * - `unified` — single column with interleaved additions and removals
         * - `split` — side-by-side comparison with old on left, new on right
         */
        this.layout = 'unified';
        /**
         * Number of unchanged context lines to display around each change.
         */
        this.contextLines = 3;
        /**
         * When `true`, long lines are wrapped instead of causing
         * horizontal scrolling. Useful when comparing prose or
         * config files with long values.
         */
        this.lineWrapping = true;
        /**
         * When `true`, JSON values are parsed, keys are sorted,
         * and indentation is normalized before diffing.
         * This eliminates noise from formatting or key-order differences.
         */
        this.reformatJson = false;
        /**
         * Defines the language for translations.
         * Will translate all visible labels and announcements.
         */
        this.translationLanguage = 'en';
        this.diffResult = {
            hunks: [],
            additions: 0,
            deletions: 0,
            allLines: [],
        };
        this.liveAnnouncement = '';
        this.copyState = 'idle';
        this.searchVisible = false;
        this.searchTerm = '';
        this.currentMatchIndex = 0;
        this.focusedRowIndex = -1;
        this.normalizedOldText = '';
        /**
         * Render-time counter that increments for each search match
         * found while rendering removed lines. Used to determine which
         * match is the "current" one for navigation highlighting.
         */
        this.searchMatchCounter = 0;
        /**
         * Total search matches found during the last render pass.
         */
        this.totalSearchMatches = 0;
        /**
         * Whether the current render is inside a removed line,
         * so search highlighting knows when to activate.
         */
        this.isRenderingRemovedLine = false;
        /**
         * Cached search regex for the current render pass.
         * Built once in render() and reused across all renderSearchableText calls.
         */
        this.activeSearchRegex = null;
        this.prevSearchVisible = false;
    }
    componentWillLoad() {
        this.recomputeDiff();
    }
    componentDidRender() {
        var _a, _b;
        if (this.searchVisible && !this.prevSearchVisible) {
            (_a = this.searchInputEl) === null || _a === void 0 ? void 0 : _a.focus();
        }
        this.prevSearchVisible = this.searchVisible;
        if (this.searchTerm && this.totalSearchMatches > 0) {
            const current = (_b = this.host.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('.search-match--current');
            current === null || current === void 0 ? void 0 : current.scrollIntoView({ block: 'center', behavior: 'smooth' });
        }
    }
    render() {
        this.searchMatchCounter = 0;
        this.activeSearchRegex = buildSearchRegex(this.searchTerm);
        const diffContent = this.renderDiff();
        // Capture total matches after rendering completes
        this.totalSearchMatches = this.searchMatchCounter;
        const lineNumberWidth = this.computeLineNumberWidth();
        return (h(Host, { key: '8122ede0d323b9021dae44cd5f65703d03083617', style: { '--limel-line-number-min-width': lineNumberWidth } }, this.renderHeader(), this.renderScreenReaderSummary(), this.searchVisible && this.renderSearchBar(), h("div", { key: 'fefe6e4898cbc82b6eb87f7dff7af71a14c7c91e', class: "diff-body", role: "table", "aria-label": this.getTranslation('code-diff.table-label'), tabindex: "0", onKeyDown: (event) => this.handleKeyDown(event) }, diffContent), h("div", { key: '2af1c7b29f2060a58af13d67fb174cb4de085efb', class: "screen-reader-only", role: "status", "aria-live": "polite", "aria-atomic": "true" }, this.liveAnnouncement)));
    }
    watchInputs() {
        this.recomputeDiff();
    }
    recomputeDiff() {
        const oldText = normalizeForDiff(this.oldValue, this.reformatJson);
        const newText = normalizeForDiff(this.newValue, this.reformatJson);
        this.normalizedOldText = oldText;
        this.diffResult = computeDiff(oldText, newText, this.contextLines);
        this.focusedRowIndex = -1;
    }
    formatSrSummary() {
        const { additions, deletions } = this.diffResult;
        if (additions === 0 && deletions === 0) {
            return null;
        }
        const parts = [];
        if (additions > 0) {
            const key = additions === 1
                ? 'code-diff.diff-addition'
                : 'code-diff.diff-additions';
            parts.push(this.getTranslation(key, { count: additions }));
        }
        if (deletions > 0) {
            const key = deletions === 1
                ? 'code-diff.diff-deletion'
                : 'code-diff.diff-deletions';
            parts.push(this.getTranslation(key, { count: deletions }));
        }
        return this.getTranslation('code-diff.diff-summary', {
            parts: parts.join(', '),
        });
    }
    renderScreenReaderSummary() {
        const summary = this.formatSrSummary();
        return (h("div", { class: "screen-reader-only", role: "status", "aria-live": "polite" }, summary !== null && summary !== void 0 ? summary : this.getTranslation('code-diff.no-differences-found')));
    }
    handleKeyDown(event) {
        if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
            return;
        }
        event.preventDefault();
        const rows = this.getDiffRows();
        if (rows.length === 0) {
            return;
        }
        if (event.key === 'ArrowDown') {
            this.focusedRowIndex = Math.min(this.focusedRowIndex + 1, rows.length - 1);
        }
        else {
            this.focusedRowIndex = Math.max(this.focusedRowIndex - 1, 0);
        }
        this.updateRowFocus(rows);
    }
    getDiffRows() {
        var _a;
        const body = (_a = this.host.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.diff-body');
        if (!body) {
            return [];
        }
        return [
            ...body.querySelectorAll('.diff-line:not(.diff-line--collapsed)'),
        ];
    }
    updateRowFocus(rows) {
        for (const row of rows) {
            row.removeAttribute('tabindex');
            row.classList.remove('diff-line--focused');
        }
        const target = rows[this.focusedRowIndex];
        if (target) {
            target.setAttribute('tabindex', '-1');
            target.classList.add('diff-line--focused');
            target.focus();
            this.announceLine(target);
        }
    }
    announceLine(row) {
        var _a, _b;
        let lineType = this.getTranslation('code-diff.line-context');
        if (row.classList.contains('diff-line--added')) {
            lineType = this.getTranslation('code-diff.line-added');
        }
        else if (row.classList.contains('diff-line--removed')) {
            lineType = this.getTranslation('code-diff.line-removed');
        }
        const content = (_b = (_a = row.querySelector('.line-content, .split-content')) === null || _a === void 0 ? void 0 : _a.textContent) !== null && _b !== void 0 ? _b : '';
        const trimmed = content.length > 80 ? content.slice(0, 80) + '…' : content;
        this.liveAnnouncement = `${lineType}: ${trimmed}`;
    }
    renderHeader() {
        var _a, _b;
        const oldHeading = (_a = this.oldHeading) !== null && _a !== void 0 ? _a : this.getTranslation('code-diff.old-heading');
        const newHeading = (_b = this.newHeading) !== null && _b !== void 0 ? _b : this.getTranslation('code-diff.new-heading');
        const { additions, deletions } = this.diffResult;
        const hasDiff = additions > 0 || deletions > 0;
        return (h("div", { class: "diff-header" }, h("div", { class: "diff-header__labels" }, h("span", { class: "diff-header__old" }, oldHeading), h("span", { class: "diff-header__new" }, newHeading)), h("div", { class: "diff-header__actions" }, h("div", { class: "diff-header__stats" }, additions > 0 && (h("span", { class: "stat stat--added" }, "+", additions)), deletions > 0 && (h("span", { class: "stat stat--removed" }, "-", deletions))), hasDiff && this.renderCopyButton(), deletions > 0 && this.renderSearchToggle())));
    }
    renderCopyButton() {
        const label = this.copyState === 'copied'
            ? this.getTranslation('code-diff.copied')
            : this.getTranslation('code-diff.copy-old-version');
        const icon = this.copyState === 'copied' ? 'checkmark' : 'copy';
        return (h("limel-icon-button", { label: label, icon: icon, onClick: () => this.copyToClipboard(this.normalizedOldText) }));
    }
    async copyToClipboard(text) {
        try {
            await navigator.clipboard.writeText(text);
            this.copyState = 'copied';
            this.liveAnnouncement = this.getTranslation('code-diff.copied-to-clipboard');
            setTimeout(() => {
                this.copyState = 'idle';
            }, 2000);
        }
        catch (_a) {
            // Clipboard API may fail in insecure contexts
        }
    }
    renderSearchToggle() {
        return (h("limel-icon-button", { class: { 'search-toggle--active': this.searchVisible }, label: this.getTranslation('code-diff.search'), icon: "search", onClick: () => this.toggleSearch() }));
    }
    renderSearchBar() {
        const matchInfo = this.totalSearchMatches === 0
            ? this.getTranslation('code-diff.no-matches')
            : this.getTranslation('code-diff.match-count', {
                current: this.currentMatchIndex + 1,
                total: this.totalSearchMatches,
            });
        return (h("div", { class: "search-bar" }, h("limel-input-field", { class: "search-bar__input", type: "search", placeholder: this.getTranslation('code-diff.search') + '…', value: this.searchTerm, onChange: (e) => this.onSearchInput(e), onKeyDown: (e) => this.onSearchKeyDown(e), ref: (el) => (this.searchInputEl = el) }), h("span", { class: "search-bar__count" }, matchInfo), h("limel-action-bar", { actions: this.getSearchActions(), onItemSelected: (e) => this.onSearchAction(e) })));
    }
    toggleSearch() {
        this.searchVisible = !this.searchVisible;
        if (!this.searchVisible) {
            this.searchTerm = '';
            this.currentMatchIndex = 0;
        }
    }
    onSearchInput(event) {
        this.searchTerm = event.detail;
        this.currentMatchIndex = 0;
    }
    onSearchKeyDown(event) {
        if (event.key === 'Enter') {
            event.preventDefault();
            if (event.shiftKey) {
                this.navigateSearch(-1);
            }
            else {
                this.navigateSearch(1);
            }
        }
        else if (event.key === 'Escape') {
            this.toggleSearch();
        }
    }
    computeLineNumberWidth() {
        const maxLineNumber = this.diffResult.allLines.length;
        const digits = String(maxLineNumber).length;
        return `calc(${digits}ch + 2 * var(--limel-code-diff-line-number-padding))`;
    }
    getSearchActions() {
        const noMatches = this.totalSearchMatches === 0;
        return [
            {
                text: this.getTranslation('code-diff.previous-match'),
                icon: '-lime-caret-top',
                iconOnly: true,
                disabled: noMatches,
                value: 'prev',
            },
            {
                text: this.getTranslation('code-diff.next-match'),
                icon: '-lime-caret-bottom',
                iconOnly: true,
                disabled: noMatches,
                value: 'next',
            },
            {
                text: this.getTranslation('code-diff.close-search'),
                icon: 'cancel',
                iconOnly: true,
                value: 'close',
            },
        ];
    }
    onSearchAction(event) {
        const { value } = event.detail;
        if (value === 'prev') {
            this.navigateSearch(-1);
        }
        else if (value === 'next') {
            this.navigateSearch(1);
        }
        else if (value === 'close') {
            this.toggleSearch();
        }
    }
    navigateSearch(direction) {
        this.currentMatchIndex = navigateMatchIndex(this.currentMatchIndex, direction, this.totalSearchMatches);
    }
    renderDiff() {
        const { hunks, collapsedAfter } = this.diffResult;
        if (hunks.length === 0) {
            return (h("div", { class: "diff-empty" }, this.getTranslation('code-diff.no-differences')));
        }
        const lineRenderer = this.layout === 'split'
            ? (hunk) => this.renderSplitHunkRows(hunk)
            : (hunk) => this.renderHunkLines(hunk);
        return this.renderHunks(hunks, collapsedAfter, lineRenderer);
    }
    renderHunks(hunks, collapsedAfter, lineRenderer) {
        const elements = [];
        for (const [i, hunk] of hunks.entries()) {
            if (hunk.collapsedBefore) {
                elements.push(this.renderCollapsedRow(hunk.collapsedBefore, i));
            }
            elements.push(...lineRenderer(hunk));
        }
        if (collapsedAfter) {
            elements.push(this.renderCollapsedAfterRow(collapsedAfter));
        }
        return elements;
    }
    renderHunkLines(hunk) {
        const elements = [];
        let i = 0;
        while (i < hunk.lines.length) {
            const line = hunk.lines[i];
            if (line.type === 'context') {
                elements.push(this.renderLine(line));
                i++;
                continue;
            }
            // Collect consecutive changed lines as a change block
            const blockLines = [];
            while (i < hunk.lines.length && hunk.lines[i].type !== 'context') {
                blockLines.push(hunk.lines[i]);
                i++;
            }
            elements.push(this.renderChangeBlock(blockLines));
        }
        return elements;
    }
    renderChangeBlock(lines) {
        const removedContent = extractRemovedContent(lines);
        return (h("div", { class: "change-group" }, lines.map((line) => this.renderLine(line)), removedContent && this.renderBlockCopyButton(removedContent)));
    }
    renderLine(line) {
        var _a, _b;
        const lineClass = {
            'diff-line': true,
            [`diff-line--${line.type}`]: true,
        };
        const indicatorMap = {
            added: '+',
            removed: '-',
            context: ' ',
        };
        const indicator = indicatorMap[line.type];
        return (h("div", { class: lineClass, role: "row" }, h("span", { class: "line-number line-number--old", role: "cell", "aria-label": line.oldLineNumber
                ? this.getTranslation('code-diff.old-line', {
                    number: line.oldLineNumber,
                })
                : undefined }, (_a = line.oldLineNumber) !== null && _a !== void 0 ? _a : ''), h("span", { class: "line-number line-number--new", role: "cell", "aria-label": line.newLineNumber
                ? this.getTranslation('code-diff.new-line', {
                    number: line.newLineNumber,
                })
                : undefined }, (_b = line.newLineNumber) !== null && _b !== void 0 ? _b : ''), h("span", { class: "line-indicator", role: "cell" }, indicator), h("span", { class: "line-content", role: "cell" }, this.renderContent(line))));
    }
    renderSplitHunkRows(hunk) {
        var _a, _b, _c, _d;
        const splitRows = buildSplitLines(hunk.lines);
        const elements = [];
        let i = 0;
        while (i < splitRows.length) {
            const row = splitRows[i];
            const isContext = ((_a = row.left) === null || _a === void 0 ? void 0 : _a.type) === 'context' && ((_b = row.right) === null || _b === void 0 ? void 0 : _b.type) === 'context';
            if (isContext) {
                elements.push(this.renderSplitRow(row));
                i++;
                continue;
            }
            // Collect consecutive changed rows
            const blockRows = [];
            while (i < splitRows.length) {
                const r = splitRows[i];
                const rIsContext = ((_c = r.left) === null || _c === void 0 ? void 0 : _c.type) === 'context' && ((_d = r.right) === null || _d === void 0 ? void 0 : _d.type) === 'context';
                if (rIsContext) {
                    break;
                }
                blockRows.push(r);
                i++;
            }
            elements.push(this.renderSplitChangeBlock(blockRows));
        }
        return elements;
    }
    renderSplitChangeBlock(rows) {
        const removedContent = extractRemovedContentFromSplit(rows);
        return (h("div", { class: "change-group" }, rows.map((row) => this.renderSplitRow(row)), removedContent && this.renderBlockCopyButton(removedContent)));
    }
    renderSplitRow(row) {
        var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
        const leftType = (_b = (_a = row.left) === null || _a === void 0 ? void 0 : _a.type) !== null && _b !== void 0 ? _b : 'empty';
        const rightType = (_d = (_c = row.right) === null || _c === void 0 ? void 0 : _c.type) !== null && _d !== void 0 ? _d : 'empty';
        const oldLineLabel = ((_e = row.left) === null || _e === void 0 ? void 0 : _e.oldLineNumber)
            ? this.getTranslation('code-diff.old-line', {
                number: row.left.oldLineNumber,
            })
            : undefined;
        const newLineLabel = ((_f = row.right) === null || _f === void 0 ? void 0 : _f.newLineNumber)
            ? this.getTranslation('code-diff.new-line', {
                number: row.right.newLineNumber,
            })
            : undefined;
        return (h("div", { class: "diff-line diff-line--split", role: "row" }, h("span", { class: "line-number line-number--old", role: "cell", "aria-label": oldLineLabel }, (_h = (_g = row.left) === null || _g === void 0 ? void 0 : _g.oldLineNumber) !== null && _h !== void 0 ? _h : ''), h("span", { class: `split-content split-content--left split-content--${leftType}`, role: "cell" }, row.left ? this.renderContent(row.left) : ''), h("span", { class: "line-number line-number--new", role: "cell", "aria-label": newLineLabel }, (_k = (_j = row.right) === null || _j === void 0 ? void 0 : _j.newLineNumber) !== null && _k !== void 0 ? _k : ''), h("span", { class: `split-content split-content--right split-content--${rightType}`, role: "cell" }, row.right ? this.renderContent(row.right) : '')));
    }
    renderBlockCopyButton(removedContent) {
        return (h("limel-icon-button", { class: "change-group__copy", elevated: true, label: this.getTranslation('code-diff.copy-change'), icon: "copy", onClick: () => this.copyToClipboard(removedContent) }));
    }
    renderContent(line) {
        this.isRenderingRemovedLine =
            line.type === 'removed' && this.searchTerm.length > 0;
        if (!line.segments || line.segments.length === 0) {
            return this.renderSyntaxTokens(line.content);
        }
        return line.segments.map((segment) => this.renderSegment(segment, line.type));
    }
    renderSegment(segment, lineType) {
        const content = this.renderSyntaxTokens(segment.value);
        if (segment.type === 'equal') {
            return content;
        }
        const segmentClass = lineType === 'removed' ? 'segment--removed' : 'segment--added';
        return h("mark", { class: segmentClass }, content);
    }
    renderSyntaxTokens(text) {
        const tokens = tokenize(text, this.language);
        if (tokens.length === 1 && tokens[0].type === 'plain') {
            return this.renderSearchableText(text);
        }
        return tokens.map((token) => this.renderSyntaxToken(token));
    }
    renderSyntaxToken(token) {
        const text = this.renderSearchableText(token.value);
        if (token.type === 'plain') {
            return text;
        }
        return h("span", { class: `syntax--${token.type}` }, text);
    }
    renderSearchableText(text) {
        if (!this.isRenderingRemovedLine || !this.activeSearchRegex) {
            return text;
        }
        const parts = text.split(this.activeSearchRegex);
        if (parts.length === 1) {
            return text;
        }
        return parts.map((part, i) => {
            // Odd indices are the captured matches from split
            if (i % 2 === 0) {
                return part;
            }
            const matchIndex = this.searchMatchCounter++;
            const isCurrent = matchIndex === this.currentMatchIndex;
            const cls = {
                'search-match': true,
                'search-match--current': isCurrent,
            };
            return (h("mark", { key: `match-${matchIndex}`, class: cls }, part));
        });
    }
    renderCollapsedRow(count, hunkIndex) {
        return (h("div", { class: "diff-line diff-line--collapsed", role: "row" }, h("button", { class: "expand-button", type: "button", onClick: () => this.expandHunk(hunkIndex), "aria-label": this.getTranslation('code-diff.show-hidden-lines', {
                count,
            }) }, this.getTranslation('code-diff.hidden-lines', { count }))));
    }
    renderCollapsedAfterRow(count) {
        return (h("div", { class: "diff-line diff-line--collapsed", role: "row" }, h("button", { class: "expand-button", type: "button", onClick: () => this.expandAfter(), "aria-label": this.getTranslation('code-diff.show-hidden-lines', {
                count,
            }) }, this.getTranslation('code-diff.hidden-lines', { count }))));
    }
    expandHunk(hunkIndex) {
        const hunks = [...this.diffResult.hunks];
        const hunk = hunks[hunkIndex];
        const prevHunkEnd = hunkIndex > 0
            ? hunks[hunkIndex - 1].startIndex +
                hunks[hunkIndex - 1].lines.length
            : 0;
        const hiddenLines = this.diffResult.allLines.slice(prevHunkEnd, hunk.startIndex);
        hunks[hunkIndex] = Object.assign(Object.assign({}, hunk), { lines: [...hiddenLines, ...hunk.lines], collapsedBefore: undefined, startIndex: prevHunkEnd });
        this.diffResult = Object.assign(Object.assign({}, this.diffResult), { hunks });
        this.liveAnnouncement = this.getTranslation('code-diff.expanded-lines');
    }
    expandAfter() {
        const hunks = [...this.diffResult.hunks];
        const lastIndex = hunks.length - 1;
        const lastHunk = hunks[lastIndex];
        const lastHunkEnd = lastHunk.startIndex + lastHunk.lines.length;
        const hiddenLines = this.diffResult.allLines.slice(lastHunkEnd);
        hunks[lastIndex] = Object.assign(Object.assign({}, lastHunk), { lines: [...lastHunk.lines, ...hiddenLines] });
        this.diffResult = Object.assign(Object.assign({}, this.diffResult), { hunks, collapsedAfter: undefined });
        this.liveAnnouncement = this.getTranslation('code-diff.expanded-lines-end');
    }
    getTranslation(key, params) {
        return translate.get(key, this.translationLanguage, params);
    }
    get host() { return getElement(this); }
    static get watchers() { return {
        "oldValue": [{
                "watchInputs": 0
            }],
        "newValue": [{
                "watchInputs": 0
            }],
        "contextLines": [{
                "watchInputs": 0
            }],
        "reformatJson": [{
                "watchInputs": 0
            }],
        "layout": [{
                "watchInputs": 0
            }]
    }; }
};
CodeDiff.style = codeDiffCss();

export { CodeDiff as limel_code_diff };
