[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/phcode-dev/phoenix/main/src/language/HTMLSimpleDOM.js [Back]  [Original]

/*
 * GNU AGPL-3.0 License
 *
 * Copyright (c) 2021 - present core.ai . All rights reserved.
 * Original work Copyright (c) 2013 - 2021 Adobe Systems Incorporated. All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
 *
 */

// @INCLUDE_IN_API_DOCS

/*unittests: HTML Instrumentation*/

define(function (require, exports, module) {


    var Tokenizer   = require("language/HTMLTokenizer").Tokenizer,
        MurmurHash3 = require("thirdparty/murmurhash3_gc"),
        PerfUtils   = require("utils/PerfUtils");

    var seed = Math.floor(Math.random() * 65535);

    var tagID = 1;

    /**
     * A list of tags whose start causes any of a given set of immediate parent
     * tags to close. This mostly comes from the HTML5 spec section on omitted close tags:
     * http://www.w3.org/html/wg/drafts/html/master/syntax.html#optional-tags
     * This doesn't handle general content model violations.
     *
     * @private
     */
    var openImpliesClose = {
        li: { li: true },
        dt: { dd: true, dt: true },
        dd: { dd: true, dt: true },
        address: { p: true },
        article: { p: true },
        aside: { p: true },
        blockquote: { p: true },
        colgroup: { caption: true },
        details: { p: true },
        dir: { p: true },
        div: { p: true },
        dl: { p: true },
        fieldset: { p: true },
        figcaption: { p: true },
        figure: { p: true },
        footer: { p: true },
        form: { p: true },
        h1: { p: true },
        h2: { p: true },
        h3: { p: true },
        h4: { p: true },
        h5: { p: true },
        h6: { p: true },
        header: { p: true },
        hgroup: { p: true },
        hr: { p: true },
        main: { p: true },
        menu: { p: true },
        nav: { p: true },
        ol: { p: true },
        p: { p: true },
        pre: { p: true },
        section: { p: true },
        table: { p: true },
        ul: { p: true },
        rb: { rb: true, rt: true, rtc: true, rp: true },
        rp: { rb: true, rt: true, rp: true },
        rt: { rb: true, rt: true, rp: true },
        rtc: { rb: true, rt: true, rtc: true, rp: true },
        optgroup: { optgroup: true, option: true },
        option: { option: true },
        tbody: { caption: true, colgroup: true, thead: true, tbody: true, tfoot: true },
        tfoot: { caption: true, colgroup: true, thead: true, tbody: true },
        thead: { caption: true, colgroup: true },
        tr: { tr: true, th: true, td: true, caption: true },
        th: { th: true, td: true },
        td: { th: true, td: true },
        body: { head: true }
    };

    /**
     * A list of elements which are automatically closed when their parent is closed:
     * http://www.w3.org/html/wg/drafts/html/master/syntax.html#optional-tags
     *
     * @private
     */
    var optionalClose = {
        html: true,
        body: true,
        li: true,
        dd: true,
        dt: true, // This is not actually correct, but showing a syntax error is not helpful
        p: true,
        rb: true,
        rt: true,
        rtc: true,
        rp: true,
        optgroup: true,
        option: true,
        colgroup: true,
        caption: true,
        tbody: true,
        tfoot: true,
        tr: true,
        td: true,
        th: true
    };

    // TODO: handle optional start tags

    /**
     * A list of tags that are self-closing (do not contain other elements).
     * Mostly taken from http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements
     *
     * @private
     */
    var voidElements = {
        area: true,
        base: true,
        basefont: true,
        br: true,
        col: true,
        command: true,
        embed: true,
        frame: true,
        hr: true,
        img: true,
        input: true,
        isindex: true,
        keygen: true,
        link: true,
        menuitem: true,
        meta: true,
        param: true,
        source: true,
        track: true,
        wbr: true
    };

    /**
     * A SimpleNode represents one node in a SimpleDOM tree. Each node can have
     * any set of properties on it, though there are a couple of assumptions made.
     * Elements will have `children` and `attributes` properties. Text nodes will have a `content`
     * property. All Elements will have a `tagID` and text nodes *can* have one.
     *
     * @constructor
     *
     * @param {Object} properties the properties provided will be set on the new object.
     */
    function SimpleNode(properties) {
        $.extend(this, properties);
    }

    SimpleNode.prototype = {

        /**
         * Updates signatures used to optimize the number of comparisons done during
         * diffing. This is important to call if you change:
         *
         * * children
         * * child node attributes
         * * text content of a text node
         * * child node text
         */
        update: function () {
            if (this.isElement()) {
                var i,
                    subtreeHashes = "",
                    childHashes = "",
                    child;
                for (i = 0; i < this.children.length; i++) {
                    child = this.children[i];
                    if (child.isElement()) {
                        childHashes += String(child.tagID);
                        subtreeHashes += String(child.tagID) + child.attributeSignature + child.subtreeSignature;
                    } else {
                        childHashes += child.textSignature;
                        subtreeHashes += child.textSignature;
                    }
                }
                this.childSignature = MurmurHash3.hashString(childHashes, childHashes.length, seed);
                this.subtreeSignature = MurmurHash3.hashString(subtreeHashes, subtreeHashes.length, seed);
            } else {
                this.textSignature = MurmurHash3.hashString(this.content, this.content.length, seed);
            }
        },

        /**
         * Updates the signature of this node's attributes. Call this after making attribute changes.
         */
        updateAttributeSignature: function () {
            var attributeString = JSON.stringify(this.attributes);
            this.attributeSignature = MurmurHash3.hashString(attributeString, attributeString.length, seed);
        },

        /**
         * Is this node an element node?
         *
         * @return {bool} true if it is an element
         */
        isElement: function () {
            return !!this.children;
        },

        /**
         * Is this node a text node?
         *
         * @return {bool} true if it is text
         */
        isText: function () {
            return !this.children;
        }
    };

    /**
     * @private
     *
     * Generates a synthetic ID for text nodes. These IDs are only used
     * for convenience when reading a SimpleDOM that is dumped to the console.
     *
     * @param {Object} textNode new node for which we are generating an ID
     * @return {string} ID for the node
     */
    function getTextNodeID(textNode) {
        var childIndex = textNode.parent.children.indexOf(textNode);
        if (childIndex === 0) {
            return textNode.parent.tagID + ".0";
        }
        return textNode.parent.children[childIndex - 1].tagID + "t";
    }

    /**
     * @private
     *
     * Adds two {line, ch}-style positions, returning a new pos.
     */
    function _addPos(pos1, pos2) {
        return {line: pos1.line + pos2.line, ch: (pos2.line === 0 ? pos1.ch + pos2.ch : pos2.ch)};
    }

    /**
     * @private
     *
     * Offsets the character offset of the given {line, ch} pos by the given amount and returns a new
     * pos. Not for general purpose use as it does not account for line boundaries.
     */
    function _offsetPos(pos, offset) {
        return {line: pos.line, ch: pos.ch + offset};
    }

    /**
     * A Builder creates a SimpleDOM tree of SimpleNode objects representing the
     * "important" contents of an HTML document. It does not include things like comments.
     * The nodes include information about their position in the text provided.
     *
     * @constructor
     *
     * @param {string} text The text to parse
     * @param {?int} startOffset starting offset in the text
     * @param {?{line: int, ch: int}} startOffsetPos line/ch position in the text
     */
    function Builder(text, startOffset, startOffsetPos) {
        this.stack = [];
        this.text = text;
        this.t = new Tokenizer(text);
        this.currentTag = null;
        this.startOffset = startOffset || 0;
        this.startOffsetPos = startOffsetPos || {line: 0, ch: 0};
    }

    Builder.prototype._logError = function (token) {
        var error       = { token: token },
            startPos    = token ? (token.startPos || token.endPos) : this.startOffsetPos,
            endPos      = token ? token.endPos : this.startOffsetPos;

        error.startPos = _addPos(this.startOffsetPos, startPos);
        error.endPos = _addPos(this.startOffsetPos, endPos);

        if (!this.errors) {
            this.errors = [];
        }

        this.errors.push(error);
    };

    /**
     * Builds the SimpleDOM.
     *
     * @param {?bool} strict if errors are detected, halt and return null
     * @param {?Object} markCache a cache that can be used in ID generation (is passed to `getID`)
     * @return {SimpleNode} root of tree or null if parsing failed
     */
    Builder.prototype.build = function (strict, markCache) {
        var self = this;
        var token, lastClosedTag, lastTextNode;
        var stack = this.stack;
        var attributeName = null;
        var nodeMap = {};

        markCache = markCache || {};

        // Start timers for building full and partial DOMs.
        // Appropriate timer is used, and the other is discarded.
        var timerBuildFull = "HTMLInstr. Build DOM Full";
        var timerBuildPart = "HTMLInstr. Build DOM Partial";
        var timers; // timer handles
        timers = PerfUtils.markStart([timerBuildFull, timerBuildPart]);
        timerBuildFull = timers[0];
        timerBuildPart = timers[1];

        function closeTag(endIndex, endPos) {
            lastClosedTag = stack[stack.length - 1];
            stack.pop();
            lastClosedTag.update();

            lastClosedTag.end = self.startOffset + endIndex;
            lastClosedTag.endPos = _addPos(self.startOffsetPos, endPos);
        }

        while ((token = this.t.nextToken()) !== null) {
            // lastTextNode is used to glue text nodes together
            // If the last node we saw was text but this one is not, then we're done gluing.
            // If this node is a comment, we might still encounter more text.
            if (token.type !== "text" && token.type !== "comment" && lastTextNode) {
                lastTextNode = null;
            }

            if (token.type === "error") {
                PerfUtils.finalizeMeasurement(timerBuildFull);  // discard
                PerfUtils.addMeasurement(timerBuildPart);       // use
                this._logError(token);
                return null;
            } else if (token.type === "opentagname") {
                var newTagName = token.contents.toLowerCase(),
                    newTag;

                if (openImpliesClose.hasOwnProperty(newTagName)) {
                    var closable = openImpliesClose[newTagName];
                    while (stack.length > 0 && closable.hasOwnProperty(stack[stack.length - 1].tag)) {
                        // Close the previous tag at the start of this tag.
                        // Adjust backwards for the < before the tag name.
                        closeTag(token.start - 1, _offsetPos(token.startPos, -1));
                    }
                }

                newTag = new SimpleNode({
                    tag: token.contents.toLowerCase(),
                    children: [],
                    attributes: {},
                    parent: (stack.length ? stack[stack.length - 1] : null),
                    start: this.startOffset + token.start - 1,
                    startPos: _addPos(this.startOffsetPos, _offsetPos(token.startPos, -1)) // ok because we know the previous char was a "

Web Proxy Viewer  |  New URL  |  Original Page