/*
* 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 "