"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Annotation
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.TextBlock = exports.List = exports.Paragraph = exports.FieldRun = exports.TabRun = exports.LineBreakRun = exports.FractionRun = exports.TextRun = exports.Run = exports.TextBlockComponent = void 0;
exports.traverseTextBlockComponent = traverseTextBlockComponent;
exports.getMarkerText = getMarkerText;
const TextStyle_1 = require("./TextStyle");
function clearStyleOverrides(component, options) {
    component.styleOverrides = {};
    if (!options?.preserveChildrenOverrides) {
        for (const child of component.children) {
            child.clearStyleOverrides(options);
        }
    }
}
/**
 * Abstract representation of any of the building blocks that make up a [[TextBlock]] document - namely [[Run]]s and [[StructuralTextBlockComponent]]s.
 * The [[TextBlock]] can specify an [AnnotationTextStyle]($backend) that formats its contents.
 * Each component can specify an optional [[styleOverrides]] to customize that formatting.
 * @beta
 */
class TextBlockComponent {
    _styleOverrides;
    /** @internal */
    constructor(props) {
        this._styleOverrides = structuredClone(props?.styleOverrides ?? {});
    }
    /** Deviations in individual properties of the [[TextStyleSettings]] in the [AnnotationTextStyle]($backend).
     * For example, if the style uses the "Arial" font, you can override that by settings `styleOverrides.font.name` to "Comic Sans".
     * @see [[clearStyleOverrides]] to reset this to an empty object.
     */
    get styleOverrides() {
        return this._styleOverrides;
    }
    set styleOverrides(overrides) {
        this._styleOverrides = structuredClone(overrides);
    }
    /** Reset any [[styleOverrides]] applied to this component. */
    clearStyleOverrides(_options) {
        this.styleOverrides = {};
    }
    /** Returns true if [[styleOverrides]] specifies any deviations from the [[TextBlock]]'s [AnnotationTextStyle]($backend). */
    get overridesStyle() {
        return Object.keys(this.styleOverrides).length > 0;
    }
    /**
    * Returns true if the string representation of this component consists only of whitespace characters.
    * Useful for checking if the component is visually empty (producing no graphics) or contains only spaces, tabs, or line breaks.
    */
    get isWhitespace() {
        return /^\s*$/g.test(this.stringify());
    }
    ;
    /** Convert this component to its JSON representation. */
    toJSON() {
        return {
            styleOverrides: structuredClone(this.styleOverrides),
        };
    }
    /** Returns true if `this` is equivalent to `other`. */
    equals(other) {
        const mySettings = TextStyle_1.TextStyleSettings.fromJSON(this.styleOverrides);
        const otherSettings = TextStyle_1.TextStyleSettings.fromJSON(other.styleOverrides);
        return mySettings.equals(otherSettings);
    }
}
exports.TextBlockComponent = TextBlockComponent;
/** A sequence of characters within a [[Paragraph]] that share a single style. Runs are the leaf nodes of a [[TextBlock]] document. When laid out for display, a single run may span
 * multiple lines, but it will never contain different styling.
 * Use the `type` field to discriminate between the different kinds of runs.
 * @beta
 */
var Run;
(function (Run) {
    /** Create a run from its JSON representation.
     * @see [[TextRun.create]], [[FractionRun.create]], [[FieldRun.create]], [[TabRun.create]], and [[LineBreakRun.create]] to create a run directly.
     */
    function fromJSON(props) {
        switch (props.type) {
            case "field": return FieldRun.create(props);
            case "fraction": return FractionRun.create(props);
            case "linebreak": return LineBreakRun.create(props);
            case "tab": return TabRun.create(props);
            case "text": return TextRun.create(props);
        }
    }
    Run.fromJSON = fromJSON;
})(Run || (exports.Run = Run = {}));
/** The most common type of [[Run]], containing a sequence of characters to be displayed using a single style.
 * @beta
 */
class TextRun extends TextBlockComponent {
    /** Discriminator field for the [[Run]] union. */
    type = "text";
    /** The sequence of characters to be displayed by the run. */
    content;
    /** Whether to display [[content]] as a subscript, superscript, or normally. */
    baselineShift;
    constructor(props) {
        super(props);
        this.content = props?.content ?? "";
        this.baselineShift = props?.baselineShift ?? "none";
    }
    clone() {
        return new TextRun(this.toJSON());
    }
    toJSON() {
        return {
            ...super.toJSON(),
            type: "text",
            content: this.content,
            baselineShift: this.baselineShift,
        };
    }
    static create(props) {
        return new TextRun({ ...props, type: "text" });
    }
    get isEmpty() {
        return this.stringify().length === 0;
    }
    /** Simply returns [[content]]. */
    stringify() {
        return this.content;
    }
    equals(other) {
        return other instanceof TextRun && this.content === other.content && this.baselineShift === other.baselineShift && super.equals(other);
    }
}
exports.TextRun = TextRun;
/** A [[Run]] containing a numeric ratio to be displayed as a numerator and denominator separated by a horizontal or diagonal bar.
 * @note The [[numerator]] and [[denominator]] are stored as strings. They are not technically required to contain a numeric representation.
 * @beta
 */
class FractionRun extends TextBlockComponent {
    /** Discriminator field for the [[Run]] union. */
    type = "fraction";
    /** The fraction's numerator. */
    numerator;
    /** The fraction's denominator. */
    denominator;
    constructor(props) {
        super(props);
        this.numerator = props?.numerator ?? "";
        this.denominator = props?.denominator ?? "";
    }
    toJSON() {
        return {
            ...super.toJSON(),
            type: "fraction",
            numerator: this.numerator,
            denominator: this.denominator,
        };
    }
    clone() {
        return new FractionRun(this.toJSON());
    }
    static create(props) {
        return new FractionRun({ ...props, type: "fraction" });
    }
    get isEmpty() {
        return this.stringify().length === 0;
    }
    /** Formats the fraction as a string with the [[numerator]] and [[denominator]] separated by [[TextBlockStringifyOptions.fractionSeparator]]. */
    stringify(options) {
        const sep = options?.fractionSeparator ?? "/";
        return `${this.numerator}${sep}${this.denominator}`;
    }
    equals(other) {
        return other instanceof FractionRun && this.numerator === other.numerator && this.denominator === other.denominator && super.equals(other);
    }
}
exports.FractionRun = FractionRun;
/** A [[Run]] that represents the end of a line of text within a [[Paragraph]]. It contains no content of its own - it simply causes subsequent content to display on a new line.
 * @beta
 */
class LineBreakRun extends TextBlockComponent {
    /** Discriminator field for the [[Run]] union. */
    type = "linebreak";
    constructor(props) {
        super(props);
    }
    toJSON() {
        return {
            ...super.toJSON(),
            type: "linebreak",
        };
    }
    static create(props) {
        return new LineBreakRun({ ...props, type: "linebreak" });
    }
    clone() {
        return new LineBreakRun(this.toJSON());
    }
    get isEmpty() {
        return this.stringify().length === 0;
    }
    /** Simply returns [[TextBlockStringifyOptions.lineBreak]]. */
    stringify(options) {
        return options?.lineBreak ?? " ";
    }
    equals(other) {
        return other instanceof LineBreakRun && super.equals(other);
    }
}
exports.LineBreakRun = LineBreakRun;
/** A [[TabRun]] is used to shift the next tab stop.
 * @note Only left-justified tabs are supported at this tab.
 * @beta
 */
class TabRun extends TextBlockComponent {
    /** Discriminator field for the [[Run]] union. */
    type = "tab";
    toJSON() {
        return {
            ...super.toJSON(),
            type: "tab",
        };
    }
    clone() {
        return new TabRun(this.toJSON());
    }
    static create(props) {
        return new TabRun(props);
    }
    get isEmpty() {
        return this.stringify().length === 0;
    }
    /**
   * Converts a [[TabRun]] to its string representation.
   * If the `tabsAsSpaces` option is provided, returns a string of spaces of the specified length.
   * Otherwise, returns a tab character ("\t").
   */
    stringify(options) {
        if (options?.tabsAsSpaces) {
            return " ".repeat(options.tabsAsSpaces);
        }
        return "\t";
    }
    equals(other) {
        return other instanceof TabRun && super.equals(other);
    }
}
exports.TabRun = TabRun;
/** A [[Run]] that displays the formatted value of a property of some [Element]($backend).
 * When a [[TextBlock]] containing a [[FieldRun]] is written into the iModel as an [ITextAnnotation]($backend) element,
 * a dependency is established between the two elements via the [ElementDrivesTextAnnotation]($backend) relationship such that
 * whenever the source element specified by [[propertyHost]] is modified or the `ITextAnnotation` element is inserted or updated in the iModel,
 * the field(s) in the `ITextAnnotation` element are automatically
 * recalculated, causing their [[cachedContent]] to update. If the field's display string cannot be evaluated (for example, because the specified element or
 * property does not exist), then its cached content is set to [[FieldRun.invalidContentIndicator]].
 * A [[FieldRun]] displays its [[cachedContent]] in the same way that [[TextRun]]s display their `content`, including word wrapping where appropriate.
 * @beta
 */
class FieldRun extends TextBlockComponent {
    /** Display string used to signal an error in computing the field's value. */
    static invalidContentIndicator = "####";
    /** Discriminator field for the [[Run]] union. */
    type = "field";
    /** The element and BIS class containing the property described by [[propertyPath]]. */
    propertyHost;
    /** Describes how to obtain the property value from [[propertyHost]]. */
    propertyPath;
    /** Specifies how to format the property value obtained from [[propertyPath]] into a string to be stored in [[cachedContent]].
     * The specific options used depend upon the [[FieldPropertyType]].
     */
    formatOptions;
    _cachedContent;
    /** The field's most recently evaluated display string. */
    get cachedContent() {
        return this._cachedContent;
    }
    /** @internal Used by core-backend when re-evaluating field content. */
    setCachedContent(content) {
        this._cachedContent = content ?? FieldRun.invalidContentIndicator;
    }
    constructor(props) {
        super(props);
        this._cachedContent = props.cachedContent ?? FieldRun.invalidContentIndicator;
        this.propertyHost = props.propertyHost;
        this.propertyPath = props.propertyPath;
        this.formatOptions = props.formatOptions;
    }
    /** Create a FieldRun from its JSON representation. */
    static create(props) {
        return new FieldRun({
            ...props,
            propertyHost: { ...props.propertyHost },
            propertyPath: structuredClone(props.propertyPath),
            formatOptions: structuredClone(props.formatOptions),
            type: "field",
        });
    }
    /** Convert the FieldRun to its JSON representation. */
    toJSON() {
        const json = {
            ...super.toJSON(),
            type: "field",
            propertyHost: { ...this.propertyHost },
            propertyPath: structuredClone(this.propertyPath),
        };
        if (this.cachedContent !== FieldRun.invalidContentIndicator) {
            json.cachedContent = this.cachedContent;
        }
        if (this.formatOptions) {
            json.formatOptions = structuredClone(this.formatOptions);
        }
        return json;
    }
    /** Create a deep copy of this FieldRun. */
    clone() {
        return new FieldRun(this.toJSON());
    }
    get isEmpty() {
        return this.stringify().length === 0;
    }
    /** Convert this FieldRun to a simple string representation. */
    stringify() {
        return this.cachedContent;
    }
    /** Returns true if `this` is equivalent to `other`. */
    equals(other) {
        if (!(other instanceof FieldRun) || !super.equals(other)) {
            return false;
        }
        if (this.propertyHost.elementId !== other.propertyHost.elementId ||
            this.propertyHost.className !== other.propertyHost.className ||
            this.propertyHost.schemaName !== other.propertyHost.schemaName) {
            return false;
        }
        if (this.propertyPath.propertyName !== other.propertyPath.propertyName) {
            return false;
        }
        const thisAccessors = this.propertyPath.accessors ?? [];
        const otherAccessors = other.propertyPath.accessors ?? [];
        if (thisAccessors.length !== otherAccessors.length) {
            return false;
        }
        if (!thisAccessors.every((value, index) => value === otherAccessors[index])) {
            return false;
        }
        if (this.formatOptions && other.formatOptions) {
            // We anticipate new formatting options being added in the future.
            // So to account for properties we don't know about, just compare the string representations.
            if (JSON.stringify(this.formatOptions) !== JSON.stringify(other.formatOptions)) {
                return false;
            }
        }
        else if (this.formatOptions || other.formatOptions) {
            return false;
        }
        return true;
    }
}
exports.FieldRun = FieldRun;
/** A collection of [[Run]]s and [[List]]s. Paragraphs can be appended to [[List]]s and [[TextBlock]]s.
 * Each paragraph is laid out on a separate line. If included in a [[List]], the paragraph will be treated as a list item.
 * @beta
 */
class Paragraph extends TextBlockComponent {
    type = "paragraph";
    children;
    constructor(props) {
        super(props);
        this.children = props?.children?.map((child) => child.type === "list" ? List.create(child) : Run.fromJSON(child)) ?? [];
    }
    /** Create a paragraph from its JSON representation. */
    static create(props) {
        return new Paragraph(props);
    }
    clearStyleOverrides(options) {
        clearStyleOverrides(this, options);
    }
    get isEmpty() {
        return this.children.length === 0;
    }
    clone() {
        return new Paragraph(this.toJSON());
    }
    toJSON() {
        return {
            ...super.toJSON(),
            children: this.children.map((child) => child.toJSON()),
        };
    }
    /** Compute a string representation of this paragraph by concatenating the string representations of all of its children. */
    stringify(options, context) {
        return this.children.map((x, index) => (index > 0 && x.type === "list") ? `${options?.paragraphBreak ?? " "}${x.stringify(options, context)}` : x.stringify(options, context)).join("") ?? "";
    }
    equals(other) {
        return (other instanceof Paragraph) && super.equals(other);
    }
}
exports.Paragraph = Paragraph;
/** A collection of list items ([[Paragraph]]s). Lists can be appended to [[Paragraph]]s.
 * Lists will be laid out on a new line. Each item in a list is laid out on a separate line.
 * @beta
 */
class List extends TextBlockComponent {
    type = "list";
    children;
    constructor(props) {
        super(props);
        this.children = props?.children?.map((child) => Paragraph.create(child)) ?? [];
    }
    /** Create a list from its JSON representation. */
    static create(props) {
        return new List({ ...props, type: "list" });
    }
    clearStyleOverrides(options) {
        clearStyleOverrides(this, options);
    }
    get isEmpty() {
        return this.children.length === 0;
    }
    clone() {
        return new List(this.toJSON());
    }
    toJSON() {
        return {
            ...super.toJSON(),
            type: "list",
            children: this.children.map((run) => run.toJSON()),
        };
    }
    /** Compute a string representation of this list by concatenating the string representations of all of its [[children]]. */
    stringify(options, context) {
        const children = this.children.map((x, index) => {
            const depth = context?.depth ?? 0;
            const marker = getMarkerText(this.styleOverrides.listMarker ?? TextStyle_1.TextStyleSettings.defaultProps.listMarker, index + 1);
            const tab = (options?.tabsAsSpaces ? " ".repeat(options.tabsAsSpaces) : "\t").repeat(depth);
            return `${tab}${marker}${options?.listMarkerBreak ?? " "}${x.stringify(options, { depth: depth + 1 })}`;
        });
        return children.join(options?.paragraphBreak ?? " ") ?? "";
    }
    equals(other) {
        return (other instanceof List) && super.equals(other);
    }
}
exports.List = List;
/** Represents a formatted text document consisting of a series of [[Paragraph]]s, each laid out on a separate line and containing their own content.
 * No word-wrapping is applied to the document unless a [[width]] greater than zero is specified.
 * @see [[TextAnnotation]] to position a text block as an annotation in 2d or 3d space.
 * @beta
 */
class TextBlock extends TextBlockComponent {
    children;
    /** The width of the document in meters. Lines that would exceed this width are instead wrapped around to the next line if possible.
     * A value less than or equal to zero indicates no wrapping is to be applied.
     * Default: 0
     */
    width;
    constructor(props) {
        super(props);
        this.width = props.width ?? 0;
        this.children = props?.children?.map((para) => Paragraph.create(para)) ?? [];
    }
    clearStyleOverrides(options) {
        clearStyleOverrides(this, options);
    }
    toJSON() {
        return {
            ...super.toJSON(),
            width: this.width,
            children: this.children.map((x) => x.toJSON()),
        };
    }
    /** Create a text block from its JSON representation. */
    static create(props) {
        return new TextBlock(props ?? {});
    }
    /** Returns true if every paragraph in this text block is empty. */
    get isEmpty() {
        return !this.children || this.children.every((child) => child.isEmpty);
    }
    clone() {
        return new TextBlock(this.toJSON());
    }
    /** Compute a string representation of the document's contents by concatenating the string representations of each of its [[children]], separated by [[TextBlockStringifyOptions.paragraphBreak]]. */
    stringify(options) {
        return this.children.map((x) => x.stringify(options)).join(options?.paragraphBreak ?? " ") || "";
    }
    /** Add and return a new paragraph.
     * By default, the paragraph will be created with no [[styleOverrides]], so that it inherits the style of this block.
     * @param seedFromLast If true and [[children]] is not empty, the new paragraph will inherit the style overrides of the last child in this block.
     * @note Be sure you pass in [[ParagraphProps]] and not [[Paragraph]] or style overrides will be ignored.
     */
    appendParagraph(props, seedFromLast = false) {
        const seed = seedFromLast ? this.children[this.children.length - 1] : undefined;
        const styleOverrides = { ...seed?.styleOverrides, ...props?.styleOverrides };
        const paragraph = Paragraph.create({ ...props, styleOverrides });
        this.children.push(paragraph);
        return paragraph;
    }
    /** Append a run to the last [[Paragraph]] in this block.
     * If the block contains no [[children]], a new [[Paragraph]] will first be created using [[appendParagraph]].
     */
    appendRun(run) {
        const paragraph = this.children[this.children.length - 1] ?? this.appendParagraph();
        paragraph.children.push(run);
    }
    equals(other) {
        if (!(other instanceof TextBlock)) {
            return false;
        }
        if (!super.equals(other)) {
            return false;
        }
        if (this.width !== other.width) {
            return false;
        }
        if (this.children && other.children) {
            if (this.children.length !== other.children.length) {
                return false;
            }
            return this.children.every((child, index) => other.children && child.equals(other.children[index]));
        }
        return true;
    }
}
exports.TextBlock = TextBlock;
/**
 * Recursively traverses a [[StructuralTextBlockComponent]] tree, yielding each child component along with its parent container.
 * This generator enables depth-first iteration over all components in a text block structure, including paragraphs, lists, and runs.
 *
 * @param parent The root container whose children should be traversed.
 * @returns An IterableIterator yielding objects with the child component and its parent container.
 * @beta
 */
function* traverseTextBlockComponent(parent) {
    for (const child of parent.children) {
        yield { parent, child };
        if (child.type === "list" || child.type === "paragraph") {
            yield* traverseTextBlockComponent(child);
        }
    }
}
/**
 * Returns the formatted marker text for a list item based on the marker type and item number.
 * Supports ordered and unordered list markers, including alphabetic, Roman numeral, and numeric formats.
 * @param marker The type of list marker to use.
 * @param num The item number in the list.
 * @returns The formatted marker string for the list item.
 * @beta
 */
function getMarkerText(marker, num) {
    let markerString = "";
    switch (marker.enumerator) {
        case undefined:
        case TextStyle_1.ListMarkerEnumerator.Number:
            markerString = `${num}`;
            break;
        case TextStyle_1.ListMarkerEnumerator.Letter:
            markerString = integerToAlpha(num);
            break;
        case TextStyle_1.ListMarkerEnumerator.RomanNumeral:
            markerString = integerToRoman(num);
            break;
        default:
            markerString = marker.enumerator;
            break;
    }
    if (marker.case) {
        markerString = marker.case === "upper" ? markerString.toUpperCase() : markerString.toLowerCase();
    }
    const terminator = marker.terminator === "period" ? "." : marker.terminator === "parenthesis" ? ")" : "";
    return `${markerString}${terminator}`;
}
/**
 * Converts an integer to its Roman numeral representation.
 * Supports numbers from 1 and above.
 * @param num The integer to convert.
 * @returns The Roman numeral string.
 */
function integerToRoman(num) {
    const values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
    const symbols = ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I'];
    let roman = '';
    for (let i = 0; i < values.length; i++) {
        while (num >= values[i]) {
            roman += symbols[i];
            num -= values[i];
        }
    }
    return roman;
}
/**
 * Converts an integer to its alphabetic representation (A-Z, AA-ZZ, etc.).
 * Used for ordered list markers with alphabetic styles.
 * @param num The integer to convert (1-based).
 * @returns The alphabetic string for the given number.
 */
function integerToAlpha(num) {
    const letterOffset = (num - 1) % 26;
    const letter = String.fromCharCode(65 + letterOffset);
    const depth = Math.ceil(num / 26);
    return letter.repeat(depth);
}
//# sourceMappingURL=TextBlock.js.map