import he from 'he';
import _ from 'lodash';
import '../../../assert/js/assert.js';
import '../../../query-string-machine/js/QueryStringMachine.js';
import '../../../globals.js';
// Copyright 2017-2024, University of Colorado Boulder
/**
 * Displays rich text by interpreting the input text as HTML, supporting a limited set of tags that prevent any
 * security vulnerabilities. It does this by parsing the input HTML and splitting it into multiple Text children
 * recursively.
 *
 * NOTE: Encoding HTML entities is required, and malformed HTML is not accepted.
 *
 * NOTE: Currently it can line-wrap at the start and end of tags. This will probably be fixed in the future to only
 *       potentially break on whitespace.
 *
 * It supports the following markup and features in the string content (in addition to other options as listed in
 * RICH_TEXT_OPTION_KEYS):
 * - <a href="{{placeholder}}"> for links (pass in { links: { placeholder: ACTUAL_HREF } })
 * - <b> and <strong> for bold text
 * - <i> and <em> for italic text
 * - <sub> and <sup> for subscripts / superscripts
 * - <u> for underlined text
 * - <s> for strikethrough text
 * - <span> tags with a dir="ltr" / dir="rtl" attribute
 * - <br> for explicit line breaks
 * - <node id="id"> for embedding a Node into the text (pass in { nodes: { id: NODE } }), with optional align attribute
 * - Custom Scenery wrapping around arbitrary tags, e.g. <blur>...</blur>, pass in { tags: { blur: ... } }, see below
 * - Unicode bidirectional marks (present in PhET strings) for full RTL support
 * - CSS style="..." attributes, with color and font settings, see https://github.com/phetsims/scenery/issues/807
 *
 * Examples from the scenery-phet demo:
 *
 * new RichText( 'RichText can have <b>bold</b> and <i>italic</i> text.' ),
 * new RichText( 'Can do H<sub>2</sub>O (A<sub>sub</sub> and A<sup>sup</sup>), or nesting: x<sup>2<sup>2</sup></sup>' ),
 * new RichText( 'Additionally: <span style="color: blue;">color</span>, <span style="font-size: 30px;">sizes</span>, <span style="font-family: serif;">faces</span>, <s>strikethrough</s>, and <u>underline</u>' ),
 * new RichText( 'These <b><em>can</em> <u><span style="color: red;">be</span> mixed<sup>1</sup></u></b>.' ),
 * new RichText( '\u202aHandles bidirectional text: \u202b<span style="color: #0a0;">مقابض</span> النص ثنائي <b>الاتجاه</b><sub>2</sub>\u202c\u202c' ),
 * new RichText( '\u202b\u062a\u0633\u062a (\u0632\u0628\u0627\u0646)\u202c' ),
 * new RichText( 'HTML entities need to be escaped, like &amp; and &lt;.' ),
 * new RichText( 'Supports <a href="{{phetWebsite}}"><em>links</em> with <b>markup</b></a>, and <a href="{{callback}}">links that call functions</a>.', {
 *   links: {
 *     phetWebsite: 'https://phet.colorado.edu',
 *     callback: function() {
 *       console.log( 'Link was clicked' );
 *     }
 *   }
 * } ),
 * new RichText( 'Or also <a href="https://phet.colorado.edu">links directly in the string</a>.', {
 *   links: true
 * } ),
 * new RichText( 'Links not found <a href="{{bogus}}">are ignored</a> for security.' ),
 * new HBox( {
 *   spacing: 30,
 *   children: [
 *     new RichText( 'Multi-line text with the<br>separator &lt;br&gt; and <a href="https://phet.colorado.edu">handles<br>links</a> and other <b>tags<br>across lines</b>', {
 *       links: true
 *     } ),
 *     new RichText( 'Supposedly RichText supports line wrapping. Here is a lineWrap of 300, which should probably wrap multiple times here', { lineWrap: 300 } )
 *   ]
 * } )
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */
import DerivedStringProperty from '../../../axon/js/DerivedStringProperty.js';
import StringProperty from '../../../axon/js/StringProperty.js';
import TinyForwardingProperty from '../../../axon/js/TinyForwardingProperty.js';
import cleanArray from '../../../phet-core/js/cleanArray.js';
import optionize, { combineOptions } from '../../../phet-core/js/optionize.js';
import StringUtils from '../../../phetcommon/js/util/StringUtils.js';
import phetioElementSelectionProperty from '../../../tandem/js/phetioElementSelectionProperty.js';
import '../../../sherpa/lib/himalaya-1.1.0.js';
import PhetioObject from '../../../tandem/js/PhetioObject.js';
import Tandem from '../../../tandem/js/Tandem.js';
import IOType from '../../../tandem/js/types/IOType.js';
import { allowLinksProperty, assertNoAdditionalChildren, Color, Font, getLineBreakRanges, isHimalayaElementNode, isHimalayaTextNode, Line, Node, RichTextElement, RichTextLeaf, RichTextLink, RichTextNode, RichTextUtils, RichTextVerticalSpacer, scenery, Text, WidthSizable } from '../imports.js';
// @ts-expect-error - Since himalaya isn't in tsconfig
const himalayaVar = himalaya;
assert && assert(himalayaVar, 'himalaya dependency needed for RichText.');
// Options that can be used in the constructor, with mutate(), or directly as setters/getters
// each of these options has an associated setter, see setter methods for more documentation
const RICH_TEXT_OPTION_KEYS = [
    'boundsMethod',
    'font',
    'fill',
    'stroke',
    'lineWidth',
    'subScale',
    'subXSpacing',
    'subYOffset',
    'supScale',
    'supXSpacing',
    'supYOffset',
    'capHeightScale',
    'underlineLineWidth',
    'underlineHeightScale',
    'strikethroughLineWidth',
    'strikethroughHeightScale',
    'linkFill',
    'linkEventsHandled',
    'links',
    'nodes',
    'tags',
    'replaceNewlines',
    'align',
    'leading',
    'lineWrap',
    Text.STRING_PROPERTY_NAME,
    'string'
];
// Used only for guarding against assertions, we want to know that we aren't in stringTesting mode
const isStringTest = window.QueryStringMachine && QueryStringMachine.containsKey('stringTest');
const DEFAULT_FONT = new Font({
    size: 20
});
// Tags that should be included in accessible innerContent, see https://github.com/phetsims/joist/issues/430
const ACCESSIBLE_TAGS = [
    'b', 'strong', 'i', 'em', 'sub', 'sup', 'u', 's'
];
// What type of line-break situations we can be in during our recursive process
const LineBreakState = {
    // There was a line break, but it was at the end of the element (or was a <br>). The relevant element can be fully
    // removed from the tree.
    COMPLETE: 'COMPLETE',
    // There was a line break, but there is some content left in this element after the line break. DO NOT remove it.
    INCOMPLETE: 'INCOMPLETE',
    // There was NO line break
    NONE: 'NONE'
};
// We'll store an array here that will record which links/nodes were used in the last rebuild (so we can assert out if
// there were some that were not used).
const usedLinks = [];
const usedNodes = [];
// himalaya converts dash separated CSS to camel case - use CSS compatible style with dashes, see above for examples
const FONT_STYLE_MAP = {
    'font-family': 'family',
    'font-size': 'size',
    'font-stretch': 'stretch',
    'font-style': 'style',
    'font-variant': 'variant',
    'font-weight': 'weight',
    'line-height': 'lineHeight'
};
const FONT_STYLE_KEYS = Object.keys(FONT_STYLE_MAP);
const STYLE_KEYS = ['color'].concat(FONT_STYLE_KEYS);
class RichText extends WidthSizable(Node) {
    constructor(string, providedOptions) {
        // We only fill in some defaults, since the other defaults are defined below (and mutate is relied on)
        const options = optionize()({
            fill: '#000000',
            // phet-io
            tandemNameSuffix: 'Text',
            phetioType: RichText.RichTextIO,
            phetioVisiblePropertyInstrumented: false
        }, providedOptions);
        if (typeof string === 'string' || typeof string === 'number') {
            options.string = string;
        }
        else {
            options.stringProperty = string;
        }
        super();
        this._font = DEFAULT_FONT;
        this._boundsMethod = 'hybrid';
        this._fill = '#000000';
        this._stroke = null;
        this._lineWidth = 1;
        this._subScale = 0.75;
        this._subXSpacing = 0;
        this._subYOffset = 0;
        this._supScale = 0.75;
        this._supXSpacing = 0;
        this._supYOffset = 0;
        this._capHeightScale = 0.75;
        this._underlineLineWidth = 1;
        this._underlineHeightScale = 0.15;
        this._strikethroughLineWidth = 1;
        this._strikethroughHeightScale = 0.3;
        this._linkFill = 'rgb(27,0,241)';
        this._linkEventsHandled = false;
        // If an object, values are either {string} or {function}
        this._links = {};
        this._nodes = {};
        this._tags = {};
        this._replaceNewlines = false;
        this._align = 'left';
        this._leading = 0;
        this._lineWrap = null;
        // We need to consolidate links (that could be split across multiple lines) under one "link" node, so we track created
        // link fragments here so they can get pieced together later.
        this._linkItems = [];
        // Whether something has been added to this line yet. We don't want to infinite-loop out if something is longer than
        // our lineWrap, so we'll place one item on its own on an otherwise empty line.
        this._hasAddedLeafToLine = false;
        // For lineWrap:stretch, we'll need to compute a new minimum width for the RichText, so these control
        // (a) whether we're computing this (it does a lot of unnecessary work otherwise if we don't need it), and (b)
        // the actual minimumWidth that we'll have.
        this.needPendingMinimumWidth = false;
        this.pendingMinimumWidth = 0;
        this._stringProperty = new TinyForwardingProperty('', true, this.onStringPropertyChange.bind(this));
        this.lineContainer = new Node({});
        this.addChild(this.lineContainer);
        // Initialize to an empty state, so we are immediately valid (since now we need to create an empty leaf even if we
        // have empty text).
        this.rebuildRichText();
        this.localPreferredWidthProperty.lazyLink(() => this.rebuildRichText());
        this.mutate(options);
        // Decorating with additional content is an anti-pattern, see https://github.com/phetsims/sun/issues/860
        assert && assertNoAdditionalChildren(this);
    }
    /**
     * Called when our stringProperty changes values.
     */
    onStringPropertyChange() {
        this.rebuildRichText();
    }
    /**
     * See documentation for Node.setVisibleProperty, except this is for the text string.
     *
     * NOTE: Setting the .string after passing a truly read-only Property will fail at runtime. We choose to allow passing
     * in read-only Properties for convenience.
     */
    setStringProperty(newTarget) {
        return this._stringProperty.setTargetProperty(newTarget, this, RichText.STRING_PROPERTY_TANDEM_NAME);
    }
    set stringProperty(property) { this.setStringProperty(property); }
    get stringProperty() { return this.getStringProperty(); }
    /**
     * Like Node.getVisibleProperty, but for the text string. Note this is not the same as the Property provided in
     * setStringProperty. Thus is the nature of TinyForwardingProperty.
     */
    getStringProperty() {
        return this._stringProperty;
    }
    /**
     * RichText supports a "string" selection mode, in which it will map to its stringProperty (if applicable), otherwise is
     * uses the default mouse-hit target from the supertype.
     */
    getPhetioMouseHitTarget(fromLinking = false) {
        return phetioElementSelectionProperty.value === 'string' ?
            this.getStringPropertyPhetioMouseHitTarget(fromLinking) :
            super.getPhetioMouseHitTarget(fromLinking);
    }
    getStringPropertyPhetioMouseHitTarget(fromLinking = false) {
        const targetStringProperty = this._stringProperty.getTargetProperty();
        // Even if this isn't PhET-iO instrumented, it still qualifies as this RichText's hit
        return targetStringProperty instanceof PhetioObject ?
            targetStringProperty.getPhetioMouseHitTarget(fromLinking) :
            'phetioNotSelectable';
    }
    /**
     * See documentation and comments in Node.initializePhetioObject
     */
    initializePhetioObject(baseOptions, providedOptions) {
        const options = optionize()({}, providedOptions);
        // Track this, so we only override our stringProperty once.
        const wasInstrumented = this.isPhetioInstrumented();
        super.initializePhetioObject(baseOptions, options);
        if (Tandem.PHET_IO_ENABLED && !wasInstrumented && this.isPhetioInstrumented()) {
            this._stringProperty.initializePhetio(this, RichText.STRING_PROPERTY_TANDEM_NAME, () => {
                return new StringProperty(this.string, combineOptions({
                    // by default, texts should be readonly. Editable texts most likely pass in editable Properties from i18n model Properties, see https://github.com/phetsims/scenery/issues/1443
                    phetioReadOnly: true,
                    tandem: this.tandem.createTandem(RichText.STRING_PROPERTY_TANDEM_NAME),
                    phetioDocumentation: 'Property for the displayed text'
                }, options.stringPropertyOptions));
            });
        }
    }
    /**
     * When called, will rebuild the node structure for this RichText
     */
    rebuildRichText() {
        assert && cleanArray(usedLinks);
        assert && cleanArray(usedNodes);
        const hasDynamicWidth = this._lineWrap === 'stretch';
        this.widthSizable = hasDynamicWidth;
        this.pendingMinimumWidth = 0;
        this.needPendingMinimumWidth = hasDynamicWidth;
        // NOTE: can't use hasDynamicWidth here, since TypeScript isn't inferring it yet
        const effectiveLineWrap = this._lineWrap === 'stretch' ? this.localPreferredWidth : this._lineWrap;
        this.freeChildrenToPool();
        // Bail early, particularly if we are being constructed.
        if (this.string === '') {
            this.appendEmptyLeaf();
            return;
        }
        sceneryLog && sceneryLog.RichText && sceneryLog.RichText(`RichText#${this.id} rebuild`);
        sceneryLog && sceneryLog.RichText && sceneryLog.push();
        // Turn bidirectional marks into explicit elements, so that the nesting is applied correctly.
        let mappedText = this.string.replace(/\u202a/g, '<span dir="ltr">')
            .replace(/\u202b/g, '<span dir="rtl">')
            .replace(/\u202c/g, '</span>');
        // Optional replacement of newlines, see https://github.com/phetsims/scenery/issues/1542
        if (this._replaceNewlines) {
            mappedText = mappedText.replace(/\n/g, '<br>');
        }
        let rootElements;
        // Start appending all top-level elements
        try {
            rootElements = himalayaVar.parse(mappedText);
        }
        catch (e) {
            // If we error out, don't kill the sim. Instead, replace the string with something that looks obviously like an
            // error. See https://github.com/phetsims/chipper/issues/1361 (we don't want translations to error out our
            // build process).
            rootElements = himalayaVar.parse('INVALID TRANSLATION');
        }
        // Clear out link items, as we'll need to reconstruct them later
        this._linkItems.length = 0;
        const widthAvailable = effectiveLineWrap === null ? Number.POSITIVE_INFINITY : effectiveLineWrap;
        const isRootLTR = true;
        let currentLine = RichTextElement.pool.create(isRootLTR);
        this._hasAddedLeafToLine = false; // notify that if nothing has been added, the first leaf always gets added.
        // Himalaya can give us multiple top-level items, so we need to iterate over those
        while (rootElements.length) {
            const element = rootElements[0];
            // How long our current line is already
            const currentLineWidth = currentLine.bounds.isValid() ? currentLine.width : 0;
            // Add the element in
            const lineBreakState = this.appendElement(currentLine, element, this._font, this._fill, isRootLTR, widthAvailable - currentLineWidth, 1);
            sceneryLog && sceneryLog.RichText && sceneryLog.RichText(`lineBreakState: ${lineBreakState}`);
            // If there was a line break (we'll need to swap to a new line node)
            if (lineBreakState !== LineBreakState.NONE) {
                // Add the line if it works
                if (currentLine.bounds.isValid()) {
                    sceneryLog && sceneryLog.RichText && sceneryLog.RichText('Adding line due to lineBreak');
                    this.appendLine(currentLine);
                }
                // Otherwise if it's a blank line, add in a strut (<br><br> should result in a blank line)
                else {
                    this.appendLine(RichTextVerticalSpacer.pool.create(RichTextUtils.scratchText.setString('X').setFont(this._font).height));
                }
                // Set up a new line
                currentLine = RichTextElement.pool.create(isRootLTR);
                this._hasAddedLeafToLine = false;
            }
            // If it's COMPLETE or NONE, then we fully processed the line
            if (lineBreakState !== LineBreakState.INCOMPLETE) {
                sceneryLog && sceneryLog.RichText && sceneryLog.RichText('Finished root element');
                rootElements.splice(0, 1);
            }
        }
        // Only add the final line if it's valid (we don't want to add unnecessary padding at the bottom)
        if (currentLine.bounds.isValid()) {
            sceneryLog && sceneryLog.RichText && sceneryLog.RichText('Adding final line');
            this.appendLine(currentLine);
        }
        // If we reached here and have no children, we probably ran into a degenerate "no layout" case like `' '`. Add in
        // the empty leaf.
        if (this.lineContainer.getChildrenCount() === 0) {
            this.appendEmptyLeaf();
        }
        // All lines are constructed, so we can align them now
        this.alignLines();
        // Handle regrouping of links (so that all fragments of a link across multiple lines are contained under a single
        // ancestor that has listeners and a11y)
        while (this._linkItems.length) {
            // Close over the href and other references
            (() => {
                const linkElement = this._linkItems[0].element;
                const href = this._linkItems[0].href;
                let i;
                // Find all nodes that are for the same link
                const nodes = [];
                for (i = this._linkItems.length - 1; i >= 0; i--) {
                    const item = this._linkItems[i];
                    if (item.element === linkElement) {
                        nodes.push(item.node);
                        this._linkItems.splice(i, 1);
                    }
                }
                const linkRootNode = RichTextLink.pool.create(linkElement.innerContent, href);
                this.lineContainer.addChild(linkRootNode);
                // Detach the node from its location, adjust its transform, and reattach under the link. This should keep each
                // fragment in the same place, but changes its parent.
                for (i = 0; i < nodes.length; i++) {
                    const node = nodes[i];
                    const matrix = node.getUniqueTrailTo(this.lineContainer).getMatrix();
                    node.detach();
                    node.matrix = matrix;
                    linkRootNode.addChild(node);
                }
            })();
        }
        // Clear them out afterwards, for memory purposes
        this._linkItems.length = 0;
        if (assert) {
            if (this._links && this._links !== true) {
                Object.keys(this._links).forEach(link => {
                    assert && allowLinksProperty.value && !isStringTest && assert(usedLinks.includes(link), `Unused RichText link: ${link}`);
                });
            }
            if (this._nodes) {
                Object.keys(this._nodes).forEach(node => {
                    assert && allowLinksProperty.value && !isStringTest && assert(usedNodes.includes(node), `Unused RichText node: ${node}`);
                });
            }
        }
        // NOTE: If this is failing or causing infinite loops in the future, refactor RichText to use a LayoutConstraint.
        this.localMinimumWidth = hasDynamicWidth ? this.pendingMinimumWidth : this.localBounds.width;
        sceneryLog && sceneryLog.RichText && sceneryLog.pop();
    }
    /**
     * Cleans "recursively temporary disposes" the children.
     */
    freeChildrenToPool() {
        // Clear any existing lines or link fragments (higher performance, and return them to pools also)
        while (this.lineContainer._children.length) {
            const child = this.lineContainer._children[this.lineContainer._children.length - 1];
            this.lineContainer.removeChild(child);
            child.clean();
        }
    }
    /**
     * Releases references.
     */
    dispose() {
        this.freeChildrenToPool();
        super.dispose();
        this._stringProperty.dispose();
    }
    /**
     * Appends a finished line, applying any necessary leading.
     */
    appendLine(lineNode) {
        // Apply leading
        if (this.lineContainer.bounds.isValid()) {
            lineNode.top = this.lineContainer.bottom + this._leading;
            // This ensures RTL lines will still be laid out properly with the main origin (handled by alignLines later)
            lineNode.left = 0;
        }
        this.lineContainer.addChild(lineNode);
    }
    /**
     * If we end up with the equivalent of "no" content, toss in a basically empty leaf so that we get valid bounds
     * (0 width, correctly-positioned height). See https://github.com/phetsims/scenery/issues/769.
     */
    appendEmptyLeaf() {
        assert && assert(this.lineContainer.getChildrenCount() === 0);
        this.appendLine(RichTextLeaf.pool.create('', true, this._font, this._boundsMethod, this._fill, this._stroke, this._lineWidth));
    }
    /**
     * Aligns all lines attached to the lineContainer.
     */
    alignLines() {
        // All nodes will either share a 'left', 'centerX' or 'right'.
        const coordinateName = this._align === 'center' ? 'centerX' : this._align;
        const ideal = this.lineContainer[coordinateName];
        for (let i = 0; i < this.lineContainer.getChildrenCount(); i++) {
            this.lineContainer.getChildAt(i)[coordinateName] = ideal;
        }
    }
    /**
     * Main recursive function for constructing the RichText Node tree.
     *
     * We'll add any relevant content to the containerNode. The element will be mutated as things are added, so that
     * whenever content is added to the Node tree it will be removed from the element tree. This means we can pause
     * whenever (e.g. when a line-break is encountered) and the rest will be ready for parsing the next line.
     *
     * @param containerNode - The node where child elements should be placed
     * @param element - See Himalaya's element specification
     *                      (https://github.com/andrejewski/himalaya/blob/master/text/ast-spec-v0.md)
     * @param font - The font to apply at this level
     * @param fill - Fill to apply
     * @param isLTR - True if LTR, false if RTL (handles RTL strings properly)
     * @param widthAvailable - How much width we have available before forcing a line break (for lineWrap)
     * @returns - Whether a line break was reached
     */
    appendElement(containerNode, element, font, fill, isLTR, widthAvailable, appliedScale) {
        let lineBreakState = LineBreakState.NONE;
        // The main Node for the element that we are adding
        let node;
        // If this content gets added, it will need to be pushed over by this amount
        const containerSpacing = isLTR ? containerNode.rightSpacing : containerNode.leftSpacing;
        // Container spacing cuts into our effective available width
        const widthAvailableWithSpacing = widthAvailable - containerSpacing;
        // If we're a leaf
        if (isHimalayaTextNode(element)) {
            sceneryLog && sceneryLog.RichText && sceneryLog.RichText(`appending leaf: ${element.content}`);
            sceneryLog && sceneryLog.RichText && sceneryLog.push();
            node = RichTextLeaf.pool.create(element.content, isLTR, font, this._boundsMethod, fill, this._stroke, this._lineWidth);
            if (this.needPendingMinimumWidth) {
                this.pendingMinimumWidth = Math.max(this.pendingMinimumWidth, Math.max(...getLineBreakRanges(element.content).map(range => {
                    const string = element.content.slice(range.min, range.max);
                    const temporaryNode = RichTextLeaf.pool.create(string, isLTR, font, this._boundsMethod, fill, this._stroke, this._lineWidth);
                    const localMininumWidth = temporaryNode.width * appliedScale;
                    temporaryNode.dispose();
                    return localMininumWidth;
                })));
            }
            // Handle wrapping if required. Container spacing cuts into our available width
            if (!node.fitsIn(widthAvailableWithSpacing, this._hasAddedLeafToLine, isLTR)) {
                // Didn't fit, lets break into words to see what we can fit. We'll create ranges for all the individual
                // elements we could split the lines into. If we split into different lines, we can ignore the characters
                // in-between, however if not, we need to include them.
                const ranges = getLineBreakRanges(element.content);
                // Convert a group of ranges into a string (grab the content from the string).
                const rangesToString = (ranges) => {
                    if (ranges.length === 0) {
                        return '';
                    }
                    else {
                        return element.content.slice(ranges[0].min, ranges[ranges.length - 1].max);
                    }
                };
                sceneryLog && sceneryLog.RichText && sceneryLog.RichText(`Overflow leafAdded:${this._hasAddedLeafToLine}, words: ${ranges.length}`);
                // If we need to add something (and there is only a single word), then add it
                if (this._hasAddedLeafToLine || ranges.length > 1) {
                    sceneryLog && sceneryLog.RichText && sceneryLog.RichText('Skipping words');
                    const skippedRanges = [];
                    let success = false;
                    skippedRanges.unshift(ranges.pop()); // We didn't fit with the last one!
                    // Keep shortening by removing words until it fits (or if we NEED to fit it) or it doesn't fit.
                    while (ranges.length) {
                        node.clean(); // We're tossing the old one, so we'll free up memory for the new one
                        node = RichTextLeaf.pool.create(rangesToString(ranges), isLTR, font, this._boundsMethod, fill, this._stroke, this._lineWidth);
                        // If we haven't added anything to the line AND we are down to the first word, we need to just add it.
                        if (!node.fitsIn(widthAvailableWithSpacing, this._hasAddedLeafToLine, isLTR) &&
                            (this._hasAddedLeafToLine || ranges.length > 1)) {
                            sceneryLog && sceneryLog.RichText && sceneryLog.RichText(`Skipping word ${rangesToString([ranges[ranges.length - 1]])}`);
                            skippedRanges.unshift(ranges.pop());
                        }
                        else {
                            sceneryLog && sceneryLog.RichText && sceneryLog.RichText(`Success with ${rangesToString(ranges)}`);
                            success = true;
                            break;
                        }
                    }
                    // If we haven't added anything yet to this line, we'll permit the overflow
                    if (success) {
                        lineBreakState = LineBreakState.INCOMPLETE;
                        element.content = rangesToString(skippedRanges);
                        sceneryLog && sceneryLog.RichText && sceneryLog.RichText(`Remaining content: ${element.content}`);
                    }
                    else {
                        // We won't use this one, so we'll free it back to the pool
                        node.clean();
                        return LineBreakState.INCOMPLETE;
                    }
                }
            }
            this._hasAddedLeafToLine = true;
            sceneryLog && sceneryLog.RichText && sceneryLog.pop();
        }
        // Otherwise presumably an element with content
        else if (isHimalayaElementNode(element)) {
            // Bail out quickly for a line break
            if (element.tagName === 'br') {
                sceneryLog && sceneryLog.RichText && sceneryLog.RichText('manual line break');
                return LineBreakState.COMPLETE;
            }
            // Span (dir attribute) -- we need the LTR/RTL knowledge before most other operations
            if (element.tagName === 'span') {
                const dirAttributeString = RichTextUtils.himalayaGetAttribute('dir', element);
                if (dirAttributeString) {
                    assert && assert(dirAttributeString === 'ltr' || dirAttributeString === 'rtl', 'Span dir attributes should be ltr or rtl.');
                    isLTR = dirAttributeString === 'ltr';
                }
            }
            // Handle <node> tags, which should not have content
            if (element.tagName === 'node') {
                const referencedId = RichTextUtils.himalayaGetAttribute('id', element);
                const referencedNode = referencedId ? (this._nodes[referencedId] || null) : null;
                assert && assert(referencedNode, referencedId
                    ? `Could not find a matching item in RichText's nodes for ${referencedId}. It should be provided in the nodes option`
                    : 'No id attribute provided for a given <node> element');
                if (referencedNode) {
                    assert && usedNodes.push(referencedId);
                    node = RichTextNode.pool.create(referencedNode);
                    if (this._hasAddedLeafToLine && !node.fitsIn(widthAvailableWithSpacing)) {
                        // If we don't fit, we'll toss this node to the pool and create it on the next line
                        node.clean();
                        return LineBreakState.INCOMPLETE;
                    }
                    const nodeAlign = RichTextUtils.himalayaGetAttribute('align', element);
                    if (nodeAlign === 'center' || nodeAlign === 'top' || nodeAlign === 'bottom') {
                        const textBounds = RichTextUtils.scratchText.setString('Test').setFont(font).bounds;
                        if (nodeAlign === 'center') {
                            node.centerY = textBounds.centerY;
                        }
                        else if (nodeAlign === 'top') {
                            node.top = textBounds.top;
                        }
                        else if (nodeAlign === 'bottom') {
                            node.bottom = textBounds.bottom;
                        }
                    }
                }
                else {
                    // If there is no node in our map, we'll just skip it
                    return lineBreakState;
                }
                this._hasAddedLeafToLine = true;
            }
            // If not a <node> tag
            else {
                node = RichTextElement.pool.create(isLTR);
            }
            sceneryLog && sceneryLog.RichText && sceneryLog.RichText('appending element');
            sceneryLog && sceneryLog.RichText && sceneryLog.push();
            const styleAttributeString = RichTextUtils.himalayaGetAttribute('style', element);
            if (styleAttributeString) {
                const css = RichTextUtils.himalayaStyleStringToMap(styleAttributeString);
                assert && Object.keys(css).forEach(key => {
                    assert(_.includes(STYLE_KEYS, key), 'See supported style CSS keys');
                });
                // Fill
                if (css.color) {
                    fill = new Color(css.color);
                }
                // Font
                const fontOptions = {};
                for (let i = 0; i < FONT_STYLE_KEYS.length; i++) {
                    const styleKey = FONT_STYLE_KEYS[i];
                    if (css[styleKey]) {
                        fontOptions[FONT_STYLE_MAP[styleKey]] = css[styleKey];
                    }
                }
                font = (typeof font === 'string' ? Font.fromCSS(font) : font).copy(fontOptions);
            }
            // Anchor (link)
            if (element.tagName === 'a') {
                let href = RichTextUtils.himalayaGetAttribute('href', element);
                const originalHref = href;
                // Try extracting the href from the links object
                if (href !== null && this._links !== true) {
                    if (href.startsWith('{{') && href.indexOf('}}') === href.length - 2) {
                        const linkName = href.slice(2, -2);
                        href = this._links[linkName];
                        assert && usedLinks.push(linkName);
                    }
                    else {
                        href = null;
                    }
                }
                // Ignore things if there is no matching href
                assert && assert(href, `Could not find a matching item in RichText's links for ${originalHref}. It should be provided in the links option, or links should be turned to true (to allow the string to create its own urls`);
                if (href) {
                    if (this._linkFill !== null) {
                        fill = this._linkFill; // Link color
                    }
                    // Don't overwrite only innerContents once things have been "torn down"
                    if (!element.innerContent) {
                        element.innerContent = RichText.himalayaElementToAccessibleString(element);
                    }
                    // Store information about it for the "regroup links" step
                    this._linkItems.push({
                        element: element,
                        node: node,
                        href: href
                    });
                }
            }
            // Bold
            else if (element.tagName === 'b' || element.tagName === 'strong') {
                font = (typeof font === 'string' ? Font.fromCSS(font) : font).copy({
                    weight: 'bold'
                });
            }
            // Italic
            else if (element.tagName === 'i' || element.tagName === 'em') {
                font = (typeof font === 'string' ? Font.fromCSS(font) : font).copy({
                    style: 'italic'
                });
            }
            // Subscript
            else if (element.tagName === 'sub') {
                node.scale(this._subScale);
                node.addExtraBeforeSpacing(this._subXSpacing);
                node.y += this._subYOffset;
            }
            // Superscript
            else if (element.tagName === 'sup') {
                node.scale(this._supScale);
                node.addExtraBeforeSpacing(this._supXSpacing);
                node.y += this._supYOffset;
            }
            // If we've added extra spacing, we'll need to subtract it off of our available width
            const scale = node.getScaleVector().x;
            // Process children
            if (element.tagName !== 'node') {
                while (lineBreakState === LineBreakState.NONE && element.children.length) {
                    const widthBefore = node.bounds.isValid() ? node.width : 0;
                    const childElement = element.children[0];
                    lineBreakState = this.appendElement(node, childElement, font, fill, isLTR, widthAvailable / scale, appliedScale * scale);
                    // for COMPLETE or NONE, we'll want to remove the childElement from the tree (we fully processed it)
                    if (lineBreakState !== LineBreakState.INCOMPLETE) {
                        element.children.splice(0, 1);
                    }
                    const widthAfter = node.bounds.isValid() ? node.width : 0;
                    // Remove the amount of width taken up by the child
                    widthAvailable += widthBefore - widthAfter;
                }
                // If there is a line break and there are still more things to process, we are incomplete
                if (lineBreakState === LineBreakState.COMPLETE && element.children.length) {
                    lineBreakState = LineBreakState.INCOMPLETE;
                }
            }
            // Subscript positioning
            if (element.tagName === 'sub') {
                if (isFinite(node.height)) {
                    node.centerY = 0;
                }
            }
            // Superscript positioning
            else if (element.tagName === 'sup') {
                if (isFinite(node.height)) {
                    node.centerY = RichTextUtils.scratchText.setString('X').setFont(font).top * this._capHeightScale;
                }
            }
            // Underline
            else if (element.tagName === 'u') {
                const underlineY = -node.top * this._underlineHeightScale;
                if (isFinite(node.top)) {
                    node.addChild(new Line(node.localLeft, underlineY, node.localRight, underlineY, {
                        stroke: fill,
                        lineWidth: this._underlineLineWidth
                    }));
                }
            }
            // Strikethrough
            else if (element.tagName === 's') {
                const strikethroughY = node.top * this._strikethroughHeightScale;
                if (isFinite(node.top)) {
                    node.addChild(new Line(node.localLeft, strikethroughY, node.localRight, strikethroughY, {
                        stroke: fill,
                        lineWidth: this._strikethroughLineWidth
                    }));
                }
            }
            if (this._tags && this._tags[element.tagName] && node.bounds.isValid()) {
                const originalNode = node;
                const originalBounds = node.bounds;
                node = RichTextNode.pool.create(this._tags[element.tagName](node));
                if (originalNode !== node) {
                    node.localBounds = originalBounds;
                }
            }
            sceneryLog && sceneryLog.RichText && sceneryLog.pop();
        }
        if (node) {
            const wasAdded = containerNode.addElement(node);
            if (!wasAdded) {
                // Remove it from the linkItems if we didn't actually add it.
                this._linkItems = this._linkItems.filter(item => item.node !== node);
                // And since we won't dispose it (since it's not a child), clean it here
                node.clean();
            }
        }
        return lineBreakState;
    }
    /**
     * Sets the string displayed by our node.
     *
     * NOTE: Encoding HTML entities is required, and malformed HTML is not accepted.
     *
     * @param string - The string to display. If it's a number, it will be cast to a string
     */
    setString(string) {
        assert && assert(string !== null && string !== undefined, 'String should be defined and non-null. Use the empty string if needed.');
        // cast it to a string (for numbers, etc., and do it before the change guard so we don't accidentally trigger on non-changed string)
        string = `${string}`;
        this._stringProperty.set(string);
        return this;
    }
    set string(value) { this.setString(value); }
    get string() { return this.getString(); }
    /**
     * Returns the string displayed by our text Node.
     */
    getString() {
        return this._stringProperty.value;
    }
    /**
     * Sets the method that is used to determine bounds from the text. See Text.setBoundsMethod for details
     */
    setBoundsMethod(method) {
        assert && assert(method === 'fast' || method === 'fastCanvas' || method === 'accurate' || method === 'hybrid', 'Unknown Text boundsMethod');
        if (method !== this._boundsMethod) {
            this._boundsMethod = method;
            this.rebuildRichText();
        }
        return this;
    }
    set boundsMethod(value) { this.setBoundsMethod(value); }
    get boundsMethod() { return this.getBoundsMethod(); }
    /**
     * Returns the current method to estimate the bounds of the text. See setBoundsMethod() for more information.
     */
    getBoundsMethod() {
        return this._boundsMethod;
    }
    /**
     * Sets the font of our node.
     */
    setFont(font) {
        if (this._font !== font) {
            this._font = font;
            this.rebuildRichText();
        }
        return this;
    }
    set font(value) { this.setFont(value); }
    get font() { return this.getFont(); }
    /**
     * Returns the current Font
     */
    getFont() {
        return this._font;
    }
    /**
     * Sets the fill of our text.
     */
    setFill(fill) {
        if (this._fill !== fill) {
            this._fill = fill;
            this.rebuildRichText();
        }
        return this;
    }
    set fill(value) { this.setFill(value); }
    get fill() { return this.getFill(); }
    /**
     * Returns the current fill.
     */
    getFill() {
        return this._fill;
    }
    /**
     * Sets the stroke of our text.
     */
    setStroke(stroke) {
        if (this._stroke !== stroke) {
            this._stroke = stroke;
            this.rebuildRichText();
        }
        return this;
    }
    set stroke(value) { this.setStroke(value); }
    get stroke() { return this.getStroke(); }
    /**
     * Returns the current stroke.
     */
    getStroke() {
        return this._stroke;
    }
    /**
     * Sets the lineWidth of our text.
     */
    setLineWidth(lineWidth) {
        if (this._lineWidth !== lineWidth) {
            this._lineWidth = lineWidth;
            this.rebuildRichText();
        }
        return this;
    }
    set lineWidth(value) { this.setLineWidth(value); }
    get lineWidth() { return this.getLineWidth(); }
    /**
     * Returns the current lineWidth.
     */
    getLineWidth() {
        return this._lineWidth;
    }
    /**
     * Sets the scale (relative to 1) of any string under subscript (<sub>) elements.
     */
    setSubScale(subScale) {
        assert && assert(isFinite(subScale) && subScale > 0);
        if (this._subScale !== subScale) {
            this._subScale = subScale;
            this.rebuildRichText();
        }
        return this;
    }
    set subScale(value) { this.setSubScale(value); }
    get subScale() { return this.getSubScale(); }
    /**
     * Returns the scale (relative to 1) of any string under subscript (<sub>) elements.
     */
    getSubScale() {
        return this._subScale;
    }
    /**
     * Sets the horizontal spacing before any subscript (<sub>) elements.
     */
    setSubXSpacing(subXSpacing) {
        assert && assert(isFinite(subXSpacing));
        if (this._subXSpacing !== subXSpacing) {
            this._subXSpacing = subXSpacing;
            this.rebuildRichText();
        }
        return this;
    }
    set subXSpacing(value) { this.setSubXSpacing(value); }
    get subXSpacing() { return this.getSubXSpacing(); }
    /**
     * Returns the horizontal spacing before any subscript (<sub>) elements.
     */
    getSubXSpacing() {
        return this._subXSpacing;
    }
    /**
     * Sets the adjustment offset to the vertical placement of any subscript (<sub>) elements.
     */
    setSubYOffset(subYOffset) {
        assert && assert(isFinite(subYOffset));
        if (this._subYOffset !== subYOffset) {
            this._subYOffset = subYOffset;
            this.rebuildRichText();
        }
        return this;
    }
    set subYOffset(value) { this.setSubYOffset(value); }
    get subYOffset() { return this.getSubYOffset(); }
    /**
     * Returns the adjustment offset to the vertical placement of any subscript (<sub>) elements.
     */
    getSubYOffset() {
        return this._subYOffset;
    }
    /**
     * Sets the scale (relative to 1) of any string under superscript (<sup>) elements.
     */
    setSupScale(supScale) {
        assert && assert(isFinite(supScale) && supScale > 0);
        if (this._supScale !== supScale) {
            this._supScale = supScale;
            this.rebuildRichText();
        }
        return this;
    }
    set supScale(value) { this.setSupScale(value); }
    get supScale() { return this.getSupScale(); }
    /**
     * Returns the scale (relative to 1) of any string under superscript (<sup>) elements.
     */
    getSupScale() {
        return this._supScale;
    }
    /**
     * Sets the horizontal spacing before any superscript (<sup>) elements.
     */
    setSupXSpacing(supXSpacing) {
        assert && assert(isFinite(supXSpacing));
        if (this._supXSpacing !== supXSpacing) {
            this._supXSpacing = supXSpacing;
            this.rebuildRichText();
        }
        return this;
    }
    set supXSpacing(value) { this.setSupXSpacing(value); }
    get supXSpacing() { return this.getSupXSpacing(); }
    /**
     * Returns the horizontal spacing before any superscript (<sup>) elements.
     */
    getSupXSpacing() {
        return this._supXSpacing;
    }
    /**
     * Sets the adjustment offset to the vertical placement of any superscript (<sup>) elements.
     */
    setSupYOffset(supYOffset) {
        assert && assert(isFinite(supYOffset));
        if (this._supYOffset !== supYOffset) {
            this._supYOffset = supYOffset;
            this.rebuildRichText();
        }
        return this;
    }
    set supYOffset(value) { this.setSupYOffset(value); }
    get supYOffset() { return this.getSupYOffset(); }
    /**
     * Returns the adjustment offset to the vertical placement of any superscript (<sup>) elements.
     */
    getSupYOffset() {
        return this._supYOffset;
    }
    /**
     * Sets the expected cap height (baseline to top of capital letters) as a scale of the detected distance from the
     * baseline to the top of the text bounds.
     */
    setCapHeightScale(capHeightScale) {
        assert && assert(isFinite(capHeightScale) && capHeightScale > 0);
        if (this._capHeightScale !== capHeightScale) {
            this._capHeightScale = capHeightScale;
            this.rebuildRichText();
        }
        return this;
    }
    set capHeightScale(value) { this.setCapHeightScale(value); }
    get capHeightScale() { return this.getCapHeightScale(); }
    /**
     * Returns the expected cap height (baseline to top of capital letters) as a scale of the detected distance from the
     * baseline to the top of the text bounds.
     */
    getCapHeightScale() {
        return this._capHeightScale;
    }
    /**
     * Sets the lineWidth of underline lines.
     */
    setUnderlineLineWidth(underlineLineWidth) {
        assert && assert(isFinite(underlineLineWidth) && underlineLineWidth > 0);
        if (this._underlineLineWidth !== underlineLineWidth) {
            this._underlineLineWidth = underlineLineWidth;
            this.rebuildRichText();
        }
        return this;
    }
    set underlineLineWidth(value) { this.setUnderlineLineWidth(value); }
    get underlineLineWidth() { return this.getUnderlineLineWidth(); }
    /**
     * Returns the lineWidth of underline lines.
     */
    getUnderlineLineWidth() {
        return this._underlineLineWidth;
    }
    /**
     * Sets the underline height adjustment as a proportion of the detected distance from the baseline to the top of the
     * text bounds.
     */
    setUnderlineHeightScale(underlineHeightScale) {
        assert && assert(isFinite(underlineHeightScale) && underlineHeightScale > 0);
        if (this._underlineHeightScale !== underlineHeightScale) {
            this._underlineHeightScale = underlineHeightScale;
            this.rebuildRichText();
        }
        return this;
    }
    set underlineHeightScale(value) { this.setUnderlineHeightScale(value); }
    get underlineHeightScale() { return this.getUnderlineHeightScale(); }
    /**
     * Returns the underline height adjustment as a proportion of the detected distance from the baseline to the top of the
     * text bounds.
     */
    getUnderlineHeightScale() {
        return this._underlineHeightScale;
    }
    /**
     * Sets the lineWidth of strikethrough lines.
     */
    setStrikethroughLineWidth(strikethroughLineWidth) {
        assert && assert(isFinite(strikethroughLineWidth) && strikethroughLineWidth > 0);
        if (this._strikethroughLineWidth !== strikethroughLineWidth) {
            this._strikethroughLineWidth = strikethroughLineWidth;
            this.rebuildRichText();
        }
        return this;
    }
    set strikethroughLineWidth(value) { this.setStrikethroughLineWidth(value); }
    get strikethroughLineWidth() { return this.getStrikethroughLineWidth(); }
    /**
     * Returns the lineWidth of strikethrough lines.
     */
    getStrikethroughLineWidth() {
        return this._strikethroughLineWidth;
    }
    /**
     * Sets the strikethrough height adjustment as a proportion of the detected distance from the baseline to the top of the
     * text bounds.
     */
    setStrikethroughHeightScale(strikethroughHeightScale) {
        assert && assert(isFinite(strikethroughHeightScale) && strikethroughHeightScale > 0);
        if (this._strikethroughHeightScale !== strikethroughHeightScale) {
            this._strikethroughHeightScale = strikethroughHeightScale;
            this.rebuildRichText();
        }
        return this;
    }
    set strikethroughHeightScale(value) { this.setStrikethroughHeightScale(value); }
    get strikethroughHeightScale() { return this.getStrikethroughHeightScale(); }
    /**
     * Returns the strikethrough height adjustment as a proportion of the detected distance from the baseline to the top of the
     * text bounds.
     */
    getStrikethroughHeightScale() {
        return this._strikethroughHeightScale;
    }
    /**
     * Sets the color of links. If null, no fill will be overridden.
     */
    setLinkFill(linkFill) {
        if (this._linkFill !== linkFill) {
            this._linkFill = linkFill;
            this.rebuildRichText();
        }
        return this;
    }
    set linkFill(value) { this.setLinkFill(value); }
    get linkFill() { return this.getLinkFill(); }
    /**
     * Returns the color of links.
     */
    getLinkFill() {
        return this._linkFill;
    }
    /**
     * Sets whether link clicks will call event.handle().
     */
    setLinkEventsHandled(linkEventsHandled) {
        if (this._linkEventsHandled !== linkEventsHandled) {
            this._linkEventsHandled = linkEventsHandled;
            this.rebuildRichText();
        }
        return this;
    }
    set linkEventsHandled(value) { this.setLinkEventsHandled(value); }
    get linkEventsHandled() { return this.getLinkEventsHandled(); }
    /**
     * Returns whether link events will be handled.
     */
    getLinkEventsHandled() {
        return this._linkEventsHandled;
    }
    setLinks(links) {
        assert && assert(links === true || Object.getPrototypeOf(links) === Object.prototype);
        if (this._links !== links) {
            this._links = links;
            this.rebuildRichText();
        }
        return this;
    }
    /**
     * Returns whether link events will be handled.
     */
    getLinks() {
        return this._links;
    }
    set links(value) { this.setLinks(value); }
    get links() { return this.getLinks(); }
    setNodes(nodes) {
        assert && assert(Object.getPrototypeOf(nodes) === Object.prototype);
        if (this._nodes !== nodes) {
            this._nodes = nodes;
            this.rebuildRichText();
        }
        return this;
    }
    getNodes() {
        return this._nodes;
    }
    set nodes(value) { this.setNodes(value); }
    get nodes() { return this.getNodes(); }
    setTags(tags) {
        assert && assert(Object.getPrototypeOf(tags) === Object.prototype);
        if (this._tags !== tags) {
            this._tags = tags;
            this.rebuildRichText();
        }
        return this;
    }
    getTags() {
        return this._tags;
    }
    set tags(value) { this.setTags(value); }
    get tags() { return this.getTags(); }
    /**
     * Sets whether newlines are replaced with <br>
     */
    setReplaceNewlines(replaceNewlines) {
        if (this._replaceNewlines !== replaceNewlines) {
            this._replaceNewlines = replaceNewlines;
            this.rebuildRichText();
        }
        return this;
    }
    set replaceNewlines(value) { this.setReplaceNewlines(value); }
    get replaceNewlines() { return this.getReplaceNewlines(); }
    getReplaceNewlines() {
        return this._replaceNewlines;
    }
    /**
     * Sets the alignment of text (only relevant if there are multiple lines).
     */
    setAlign(align) {
        assert && assert(align === 'left' || align === 'center' || align === 'right');
        if (this._align !== align) {
            this._align = align;
            this.rebuildRichText();
        }
        return this;
    }
    set align(value) { this.setAlign(value); }
    get align() { return this.getAlign(); }
    /**
     * Returns the current alignment of the text (only relevant if there are multiple lines).
     */
    getAlign() {
        return this._align;
    }
    /**
     * Sets the leading (spacing between lines)
     */
    setLeading(leading) {
        assert && assert(isFinite(leading));
        if (this._leading !== leading) {
            this._leading = leading;
            this.rebuildRichText();
        }
        return this;
    }
    set leading(value) { this.setLeading(value); }
    get leading() { return this.getLeading(); }
    /**
     * Returns the leading (spacing between lines)
     */
    getLeading() {
        return this._leading;
    }
    /**
     * Sets the line wrap width for the text (or null if none is desired). Lines longer than this length will wrap
     * automatically to the next line.
     *
     * @param lineWrap - If it's a number, it should be greater than 0.
     */
    setLineWrap(lineWrap) {
        assert && assert(lineWrap === null || lineWrap === 'stretch' || (isFinite(lineWrap) && lineWrap > 0));
        if (this._lineWrap !== lineWrap) {
            this._lineWrap = lineWrap;
            this.rebuildRichText();
        }
        return this;
    }
    set lineWrap(value) { this.setLineWrap(value); }
    get lineWrap() { return this.getLineWrap(); }
    /**
     * Returns the line wrap width.
     */
    getLineWrap() {
        return this._lineWrap;
    }
    mutate(options) {
        if (assert && options && options.hasOwnProperty('string') && options.hasOwnProperty(Text.STRING_PROPERTY_NAME) && options.stringProperty) {
            assert && assert(options.stringProperty.value === options.string, 'If both string and stringProperty are provided, then values should match');
        }
        return super.mutate(options);
    }
    /**
     * Returns a wrapped version of the string with a font specifier that uses the given font object.
     *
     * NOTE: Does an approximation of some font values (using <b> or <i>), and cannot force the lack of those if it is
     * included in bold/italic exterior tags.
     */
    static stringWithFont(str, font) {
        // TODO: ES6 string interpolation. https://github.com/phetsims/scenery/issues/1581
        return `${'<span style=\'' +
            'font-style: '}${font.style};` +
            `font-variant: ${font.variant};` +
            `font-weight: ${font.weight};` +
            `font-stretch: ${font.stretch};` +
            `font-size: ${font.size};` +
            `font-family: ${font.family};` +
            `line-height: ${font.lineHeight};` +
            `'>${str}</span>`;
    }
    /**
     * Stringifies an HTML subtree defined by the given element.
     */
    static himalayaElementToString(element) {
        if (isHimalayaTextNode(element)) {
            return RichText.contentToString(element.content);
        }
        else if (isHimalayaElementNode(element)) {
            const content = element.children.map(child => RichText.himalayaElementToString(child)).join('');
            const dir = RichTextUtils.himalayaGetAttribute('dir', element);
            if (dir === 'ltr' || dir === 'rtl') {
                return StringUtils.wrapDirection(content, dir);
            }
            else {
                return content;
            }
        }
        else {
            return '';
        }
    }
    /**
     * Stringifies an HTML subtree defined by the given element, but removing certain tags that we don't need for
     * accessibility (like <a>, <span>, etc.), and adding in tags we do want (see ACCESSIBLE_TAGS).
     */
    static himalayaElementToAccessibleString(element) {
        if (isHimalayaTextNode(element)) {
            return RichText.contentToString(element.content);
        }
        else if (isHimalayaElementNode(element)) {
            let content = element.children.map(child => RichText.himalayaElementToAccessibleString(child)).join('');
            const dir = RichTextUtils.himalayaGetAttribute('dir', element);
            if (dir === 'ltr' || dir === 'rtl') {
                content = StringUtils.wrapDirection(content, dir);
            }
            if (_.includes(ACCESSIBLE_TAGS, element.tagName)) {
                return `<${element.tagName}>${content}</${element.tagName}>`;
            }
            else {
                return content;
            }
        }
        else {
            return '';
        }
    }
    /**
     * Transforms a given string with HTML markup into a string suitable for screen readers.
     * Preserves basic styling tags while removing non-accessible markup.
     */
    static getAccessibleStringProperty(stringProperty) {
        return new DerivedStringProperty([stringProperty], string => {
            const rootElements = himalayaVar.parse(string);
            let accessibleString = '';
            rootElements.forEach(element => {
                accessibleString += RichText.himalayaElementToAccessibleString(element);
            });
            return accessibleString;
        });
    }
    /**
     * Takes the element.content from himalaya, unescapes HTML entities, and applies the proper directional tags.
     *
     * See https://github.com/phetsims/scenery-phet/issues/315
     */
    static contentToString(content, isLTR) {
        const unescapedContent = he.decode(content);
        return isLTR === undefined ? unescapedContent : `${isLTR ? '\u202a' : '\u202b'}${unescapedContent}\u202c`;
    }
}
// Text and RichText currently use the same tandem name for their stringProperty.
RichText.STRING_PROPERTY_TANDEM_NAME = Text.STRING_PROPERTY_TANDEM_NAME;
export default RichText;
/**
 * {Array.<string>} - String keys for all the allowed options that will be set by node.mutate( options ), in the
 * order they will be evaluated in.
 *
 * NOTE: See Node's _mutatorKeys documentation for more information on how this operates, and potential special
 *       cases that may apply.
 */
RichText.prototype._mutatorKeys = RICH_TEXT_OPTION_KEYS.concat(Node.prototype._mutatorKeys);
scenery.register('RichText', RichText);
RichText.RichTextIO = new IOType('RichTextIO', {
    valueType: RichText,
    supertype: Node.NodeIO,
    documentation: 'The PhET-iO Type for the scenery RichText node'
});
//# sourceMappingURL=RichText.js.map