Untitled diff

作成日 差分は期限切れになりません
51 削除
736
120 追加
805
// This is a version of slowparse modified for KA. It is based off this file:
// https://github.com/mozilla/slowparse/blob/c0ede5d6d65c50f707b20f5036316b13bf85c80d/slowparse.js
// If you want to know the KA-specific changes, I recommend you run a diff.

// Slowparse is a token stream parser for HTML and CSS text,
// Slowparse is a token stream parser for HTML and CSS text,
// recording regions of interest during the parse run and
// recording regions of interest during the parse run and
// signaling any errors detected accompanied by relevant
// signaling any errors detected accompanied by relevant
// regions in the text stream, to make debugging easy. Each
// regions in the text stream, to make debugging easy. Each
// error type is documented in the [error specification][].
// error type is documented in the [error specification][].
//
//
// Slowparse also builds a DOM as it goes, attaching metadata
// Slowparse also builds a DOM as it goes, attaching metadata
// to each node build that points to where it came from in
// to each node build that points to where it came from in
// the original source.
// the original source.
//
//
// For more information on the rationale behind Slowparse, as
// For more information on the rationale behind Slowparse, as
// well as its design goals, see the [README][].
// well as its design goals, see the [README][].
//
//
// If [RequireJS] is detected, this file is defined as a module via
// If [RequireJS] is detected, this file is defined as a module via
// `define()`. Otherwise, a global called `Slowparse` is exposed.
// `define()`. Otherwise, a global called `Slowparse` is exposed.
//
//
// ## Implementation
// ## Implementation
//
//
// Slowparse is effectively a finite state machine for
// Slowparse is effectively a finite state machine for
// HTML and CSS strings, and will switch between the HTML
// HTML and CSS strings, and will switch between the HTML
// and CSS parsers while maintaining a single token stream.
// and CSS parsers while maintaining a single token stream.
//
//
// [RequireJS]: http://requirejs.org/
// [RequireJS]: http://requirejs.org/
// [error specification]: spec/
// [error specification]: spec/
// [README]: https://github.com/mozilla/slowparse#readme
// [README]: https://github.com/mozilla/slowparse#readme
(function() {
(function() {
"use strict";
"use strict";


// ### Character Entity Parsing
// ### Character Entity Parsing
//
//
// We currently only parse the most common named character entities.
// We currently only parse the most common named character entities.
var CHARACTER_ENTITY_REFS = {
var CHARACTER_ENTITY_REFS = {
lt: "<",
lt: "<",
gt: ">",
gt: ">",
apos: "'",
apos: "'",
quot: '"',
quot: '"',
amp: "&"
amp: "&"
};
};


// HTML attribute parsing rules are based on
// HTML attribute parsing rules are based on
// http://www.w3.org/TR/2011/WD-html5-20110525/elements.html#attr-data
// http://www.w3.org/TR/2011/WD-html5-20110525/elements.html#attr-data
// -> ref http://www.w3.org/TR/2011/WD-html5-20110525/infrastructure.html#xml-compatible
// -> ref http://www.w3.org/TR/2011/WD-html5-20110525/infrastructure.html#xml-compatible
// -> ref http://www.w3.org/TR/REC-xml/#NT-NameChar
// -> ref http://www.w3.org/TR/REC-xml/#NT-NameChar
// note: this lacks the final \\u10000-\\uEFFFF in the startchar set, because JavaScript
// note: this lacks the final \\u10000-\\uEFFFF in the startchar set, because JavaScript
// cannot cope with unciode characters with points over 0xFFFF.
// cannot cope with unciode characters with points over 0xFFFF.
var attributeNameStartChar = "A-Za-z_\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD";
var attributeNameStartChar = "A-Za-z_\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD";
var nameStartChar = new RegExp("[" + attributeNameStartChar + "]");
var nameStartChar = new RegExp("[" + attributeNameStartChar + "]");
var attributeNameChar = attributeNameStartChar + "0-9\\-\\.\\u00B7\\u0300-\\u036F\\u203F-\\u2040:";
var attributeNameChar = attributeNameStartChar + "0-9\\-\\.\\u00B7\\u0300-\\u036F\\u203F-\\u2040:";
var nameChar = new RegExp("[" + attributeNameChar + "]");
var nameChar = new RegExp("[" + attributeNameChar + "]");


//Define a property checker for https page
//Define a property checker for https page
var checkMixedContent = (typeof window !== "undefined" ? (window.location.protocol === "https:") : false);
var checkMixedContent = (typeof window !== "undefined" ? (window.location.protocol === "https:") : false);


//Define activeContent with tag-attribute pairs
//Define activeContent with tag-attribute pairs
function isActiveContent (tagName, attrName) {
function isActiveContent (tagName, attrName) {
if (attrName === "href") {
if (attrName === "href") {
return ["link"].indexOf(tagName) > -1;
return ["link"].indexOf(tagName) > -1;
}
}
if (attrName === "src") {
if (attrName === "src") {
return ["script", "iframe"].indexOf(tagName) > -1;
return ["script", "iframe"].indexOf(tagName) > -1;
}
}
if (attrName === "data") {
if (attrName === "data") {
return ["object"].indexOf(tagName) > -1;
return ["object"].indexOf(tagName) > -1;
}
}
return false;
return false;
}
}


// the current active omittable html Element
// the current active omittable html Element
var activeTagNode = false;
var activeTagNode = false;


// the parent html Element for optional closing tag tags
// the parent html Element for optional closing tag tags
var parentTagNode = false;
var parentTagNode = false;


// 'foresee' if there is no more content in the parent element, and the
// 'foresee' if there is no more content in the parent element, and the
// parent element is not an a element in the case of activeTag is a p element.
// parent element is not an a element in the case of activeTag is a p element.
function isNextTagParent(stream, parentTagName) {
function isNextTagParent(stream, parentTagName) {
return stream.findNext(/<\/([\w\-]+)\s*>/, 1) === parentTagName;
return stream.findNext(/<\/([\w\-]+)\s*>/, 1) === parentTagName;
}
}


// 'foresee' if the next tag is a close tag
// 'foresee' if the next tag is a close tag
function isNextCloseTag(stream) {
function isNextCloseTag(stream) {
return stream.findNext(/<\/([\w\-]+)\s*>/, 1);
return stream.findNext(/<\/([\w\-]+)\s*>/, 1);
}
}


// Check exception for Tag omission rules: for p tag, if there is no more
// Check exception for Tag omission rules: for p tag, if there is no more
// content in the parent element and the parent element is not an a element.
// content in the parent element and the parent element is not an a element.
function allowsOmmitedEndTag(parentTagName, tagName) {
function allowsOmmitedEndTag(parentTagName, tagName) {
if (tagName === "p") {
if (tagName === "p") {
return ["a"].indexOf(parentTagName) > -1;
return ["a"].indexOf(parentTagName) > -1;
}
}
return false;
return false;
}
}


// `replaceEntityRefs()` will replace named character entity references
// `replaceEntityRefs()` will replace named character entity references
// (e.g. `&lt;`) in the given text string and return the result. If an
// (e.g. `&lt;`) in the given text string and return the result. If an
// entity name is unrecognized, don't replace it at all. Writing HTML
// entity name is unrecognized, don't replace it at all. Writing HTML
// would be surprisingly painful without this forgiving behavior.
// would be surprisingly painful without this forgiving behavior.
//
//
// This function does not currently replace numeric character entity
// This function does not currently replace numeric character entity
// references (e.g., `&#160;`).
// references (e.g., `&#160;`).
function replaceEntityRefs(text) {
function replaceEntityRefs(text) {
return text.replace(/&([A-Za-z]+);/g, function(ref, name) {
return text.replace(/&([A-Za-z]+);/g, function(ref, name) {
name = name.toLowerCase();
name = name.toLowerCase();
if (name in CHARACTER_ENTITY_REFS)
if (name in CHARACTER_ENTITY_REFS)
return CHARACTER_ENTITY_REFS[name];
return CHARACTER_ENTITY_REFS[name];
return ref;
return ref;
});
});
}
}




// ### Errors
// ### Errors
//
//
// `ParseError` is an internal error class used to indicate a parsing error.
// `ParseError` is an internal error class used to indicate a parsing error.
// It never gets seen by Slowparse clients, as parse errors are an
// It never gets seen by Slowparse clients, as parse errors are an
// expected occurrence. However, they are used internally to simplify
// expected occurrence. However, they are used internally to simplify
// flow control.
// flow control.
//
//
// The first argument is the name of an error type, followed by
// The first argument is the name of an error type, followed by
// arbitrary positional arguments specific to that error type. Every
// arbitrary positional arguments specific to that error type. Every
// instance has a `parseInfo` property which contains the error
// instance has a `parseInfo` property which contains the error
// object that will be exposed to Slowparse clients when parsing errors
// object that will be exposed to Slowparse clients when parsing errors
// occur.
// occur.
function ParseError(type) {
function ParseError(type) {
this.name = "ParseError";
this.name = "ParseError";
if (!(type in ParseErrorBuilders))
if (!(type in ParseErrorBuilders))
throw new Error("Unknown ParseError type: " + type);
throw new Error("Unknown ParseError type: " + type);
var args = [];
var args = [];
for (var i = 1; i < arguments.length; i++)
for (var i = 1; i < arguments.length; i++)
args.push(arguments[i]);
args.push(arguments[i]);
var parseInfo = ParseErrorBuilders[type].apply(ParseErrorBuilders, args);
var parseInfo = ParseErrorBuilders[type].apply(ParseErrorBuilders, args);


/* This may seem a weird way of setting an attribute, but we want
/* This may seem a weird way of setting an attribute, but we want
* to make the JSON serialize so the 'type' appears first, as it
* to make the JSON serialize so the 'type' appears first, as it
* makes our documentation read better. */
* makes our documentation read better. */
parseInfo = ParseErrorBuilders._combine({
parseInfo = ParseErrorBuilders._combine({
type: type
type: type
}, parseInfo);
}, parseInfo);
this.message = type;
this.message = type;
this.parseInfo = parseInfo;
this.parseInfo = parseInfo;
}
}


ParseError.prototype = Error.prototype;
ParseError.prototype = Error.prototype;


// `ParseErrorBuilders` contains Factory functions for all our types of
// `ParseErrorBuilders` contains Factory functions for all our types of
// parse errors, indexed by error type.
// parse errors, indexed by error type.
//
//
// Each public factory function returns a `parseInfo` object, sans the
// Each public factory function returns a `parseInfo` object, sans the
// `type` property. For more information on each type of error,
// `type` property. For more information on each type of error,
// see the [error specification][].
// see the [error specification][].
//
//
// [error specification]: spec/
// [error specification]: spec/
var ParseErrorBuilders = {
var ParseErrorBuilders = {
/* Create a new object that has the properties of both arguments
/* Create a new object that has the properties of both arguments
* and return it. */
* and return it. */
_combine: function(a, b) {
_combine: function(a, b) {
var obj = {}, name;
var obj = {}, name;
for (name in a) {
for (name in a) {
obj[name] = a[name];
obj[name] = a[name];
}
}
for (name in b) {
for (name in b) {
obj[name] = b[name];
obj[name] = b[name];
}
}
return obj;
return obj;
},
},
// These are HTML errors.
// These are HTML errors.
NO_DOCTYPE_FOUND: function() {},
HTML_NOT_ROOT_ELEMENT: function(parser) {
var currentNode = parser.domBuilder.currentNode.firstElementChild,
openTag = this._combine({
name: currentNode.nodeName.toLowerCase()
}, currentNode.parseInfo.openTag);
return {
openTag: openTag,
cursor: openTag.start
};
},
UNCLOSED_TAG: function(parser) {
UNCLOSED_TAG: function(parser) {
var currentNode = parser.domBuilder.currentNode,
var currentNode = parser.domBuilder.currentNode,
openTag = this._combine({
openTag = this._combine({
name: currentNode.nodeName.toLowerCase()
name: currentNode.nodeName.toLowerCase()
}, currentNode.parseInfo.openTag);
}, currentNode.parseInfo.openTag);
return {
return {
openTag: openTag,
openTag: openTag,
cursor: openTag.start
cursor: openTag.start
};
};
},
},
INVALID_TAG_NAME: function(tagName, token) {
INVALID_TAG_NAME: function(tagName, token) {
var openTag = this._combine({
var openTag = this._combine({
name: tagName
name: tagName
}, token.interval);
}, token.interval);
return {
return {
openTag: openTag,
openTag: openTag,
cursor: openTag.start
cursor: openTag.start
};
};
},
},
SCRIPT_ELEMENT_NOT_ALLOWED: function(tagName, token) {
var openTag = this._combine({
name: tagName
}, token.interval);
return {
openTag: openTag,
cursor: openTag.start
};
},
OBSOLETE_HTML_TAG: function(tagName, token) {
var openTag = this._combine({
name: tagName
}, token.interval);
return {
openTag: openTag,
cursor: openTag.start
}
},
ELEMENT_NOT_ALLOWED: function(tagName, token) {
var openTag = this._combine({
name: tagName
}, token.interval);
return {
openTag: openTag,
cursor: openTag.start
};
},
UNEXPECTED_CLOSE_TAG: function(parser, closeTagName, token) {
UNEXPECTED_CLOSE_TAG: function(parser, closeTagName, token) {
var closeTag = this._combine({
var closeTag = this._combine({
name: closeTagName
name: closeTagName
}, token.interval);
}, token.interval);
return {
return {
closeTag: closeTag,
closeTag: closeTag,
cursor: closeTag.start
cursor: closeTag.start
};
};
},
},
MISMATCHED_CLOSE_TAG: function(parser, openTagName, closeTagName, token) {
MISMATCHED_CLOSE_TAG: function(parser, openTagName, closeTagName, token) {
var openTag = this._combine({
var openTag = this._combine({
name: openTagName
name: openTagName
}, parser.domBuilder.currentNode.parseInfo.openTag),
}, parser.domBuilder.currentNode.parseInfo.openTag),
closeTag = this._combine({
closeTag = this._combine({
name: closeTagName
name: closeTagName
}, token.interval);
}, token.interval);
return {
return {
openTag: openTag,
openTag: openTag,
closeTag: closeTag,
closeTag: closeTag,
cursor: closeTag.start
cursor: closeTag.start
};
};
},
},
ATTRIBUTE_IN_CLOSING_TAG: function(parser) {
ATTRIBUTE_IN_CLOSING_TAG: function(parser) {
var currentNode = parser.domBuilder.currentNode;
var currentNode = parser.domBuilder.currentNode;
var end = parser.stream.pos;
var end = parser.stream.pos;
if (!parser.stream.end()) {
if (!parser.stream.end()) {
end = parser.stream.makeToken().interval.start;
end = parser.stream.makeToken().interval.start;
}
}
var closeTag = {
var closeTag = {
name: currentNode.nodeName.toLowerCase(),
name: currentNode.nodeName.toLowerCase(),
start: currentNode.parseInfo.closeTag.start,
start: currentNode.parseInfo.closeTag.start,
end: end
end: end
};
};
return {
return {
closeTag: closeTag,
closeTag: closeTag,
cursor: closeTag.start
cursor: closeTag.start
};
};
},
},
CLOSE_TAG_FOR_VOID_ELEMENT: function(parser, closeTagName, token) {
CLOSE_TAG_FOR_VOID_ELEMENT: function(parser, closeTagName, token) {
var closeTag = this._combine({
var closeTag = this._combine({
name: closeTagName
name: closeTagName
}, token.interval);
}, token.interval);
return {
return {
closeTag: closeTag,
closeTag: closeTag,
cursor: closeTag.start
cursor: closeTag.start
};
};
},
},
UNTERMINATED_COMMENT: function(token) {
UNTERMINATED_COMMENT: function(token) {
var commentStart = token.interval.start;
var commentStart = token.interval.start;
return {
return {
start: commentStart,
start: commentStart,
cursor: commentStart
cursor: commentStart
};
};
},
},
UNTERMINATED_ATTR_VALUE: function(parser, nameTok) {
UNTERMINATED_ATTR_VALUE: function(parser, nameTok) {
var currentNode = parser.domBuilder.currentNode,
var currentNode = parser.domBuilder.currentNode,
openTag = this._combine({
openTag = this._combine({
name: currentNode.nodeName.toLowerCase()
name: currentNode.nodeName.toLowerCase()
}, currentNode.parseInfo.openTag),
}, currentNode.parseInfo.openTag),
valueTok = parser.stream.makeToken(),
valueTok = parser.stream.makeToken(),
attribute = {
attribute = {
name: {
name: {
value: nameTok.value,
value: nameTok.value,
start: nameTok.interval.start,
start: nameTok.interval.start,
end: nameTok.interval.end
end: nameTok.interval.end
},
},
value: {
value: {
start: valueTok.interval.start
start: valueTok.interval.start
}
}
};
};
return {
return {
openTag: openTag,
openTag: openTag,
attribute: attribute,
attribute: attribute,
cursor: attribute.value.start
cursor: attribute.value.start
};
};
},
},
UNQUOTED_ATTR_VALUE: function(parser) {
UNQUOTED_ATTR_VALUE: function(parser) {
var pos = parser.stream.pos;
var pos = parser.stream.pos;
if (!parser.stream.end()) {
if (!parser.stream.end()) {
pos = parser.stream.makeToken().interval.start;
pos = parser.stream.makeToken().interval.start;
}
}
return {
return {
start: pos,
start: pos,
cursor: pos
cursor: pos
};
};
},
},
INVALID_ATTR_NAME: function(parser, attrToken) {
INVALID_ATTR_NAME: function(parser, attrToken) {
return {
return {
start: attrToken.interval.start,
start: attrToken.interval.start,
end: attrToken.interval.end,
end: attrToken.interval.end,
attribute: {
attribute: {
name: {
name: {
value: attrToken.value
value: attrToken.value
}
}
},
},
cursor: attrToken.interval.start
cursor: attrToken.interval.start
};
};
},
},
EVENT_HANDLER_ATTR_NOT_ALLOWED: function(parser, attrToken) {
return {
start: attrToken.interval.start,
end: attrToken.interval.end,
attribute: {
name: {
value: attrToken.value
}
},
cursor: attrToken.interval.start
};
},
JAVASCRIPT_URL_NOT_ALLOWED: function(parser, nameTok, valueTok) {
var currentNode = parser.domBuilder.currentNode,
openTag = this._combine({
name: currentNode.nodeName.toLowerCase()
}, currentNode.parseInfo.openTag),
attribute = {
name: {
value: nameTok.value,
start: nameTok.interval.start,
end: nameTok.interval.end
},
value: {
start: valueTok.interval.start + 1,
end: valueTok.interval.end - 1
}
};
return {
openTag: openTag,
attribute: attribute,
cursor: attribute.value.start
};
},
MULTIPLE_ATTR_NAMESPACES: function(parser, attrToken) {
MULTIPLE_ATTR_NAMESPACES: function(parser, attrToken) {
return {
return {
start: attrToken.interval.start,
start: attrToken.interval.start,
end: attrToken.interval.end,
end: attrToken.interval.end,
attribute: {
attribute: {
name: {
name: {
value: attrToken.value
value: attrToken.value
}
}
},
},
cursor: attrToken.interval.start
cursor: attrToken.interval.start
};
};
},
},
UNSUPPORTED_ATTR_NAMESPACE: function(parser, attrToken) {
UNSUPPORTED_ATTR_NAMESPACE: function(parser, attrToken) {
return {
return {
start: attrToken.interval.start,
start: attrToken.interval.start,
end: attrToken.interval.end,
end: attrToken.interval.end,
attribute: {
attribute: {
name: {
name: {
value: attrToken.value
value: attrToken.value
}
}
},
},
cursor: attrToken.interval.start
cursor: attrToken.interval.start
};
};
},
},
UNTERMINATED_OPEN_TAG: function(parser) {
UNTERMINATED_OPEN_TAG: function(parser) {
var currentNode = parser.domBuilder.currentNode,
var currentNode = parser.domBuilder.currentNode,
openTag = {
openTag = {
start: currentNode.parseInfo.openTag.start,
start: currentNode.parseInfo.openTag.start,
end: parser.stream.pos,
end: parser.stream.pos,
name: currentNode.nodeName.toLowerCase()
name: currentNode.nodeName.toLowerCase()
};
};
return {
return {
openTag: openTag,
openTag: openTag,
cursor: openTag.start
cursor: openTag.start
};
};
},
},
SELF_CLOSING_NON_VOID_ELEMENT: function(parser, tagName) {
SELF_CLOSING_NON_VOID_ELEMENT: function(parser, tagName) {
var start = parser.domBuilder.currentNode.parseInfo.openTag.start,
var start = parser.domBuilder.currentNode.parseInfo.openTag.start,
end = parser.stream.makeToken().interval.end;
end = parser.stream.makeToken().interval.end;
return {
return {
name: tagName,
name: tagName,
start: start,
start: start,
end: end,
end: end,
cursor: start
cursor: start
};
};
},
},
UNTERMINATED_CLOSE_TAG: function(parser) {
UNTERMINATED_CLOSE_TAG: function(parser) {
var currentNode = parser.domBuilder.currentNode;
var currentNode = parser.domBuilder.currentNode;
var end = parser.stream.pos;
var end = parser.stream.pos;
if (!parser.stream.end()) {
if (!parser.stream.end()) {
end = parser.stream.makeToken().interval.start;
end = parser.stream.makeToken().interval.start;
}
}
var closeTag = {
var closeTag = {
name: currentNode.nodeName.toLowerCase(),
name: currentNode.nodeName.toLowerCase(),
start: currentNode.parseInfo.closeTag.start,
start: currentNode.parseInfo.closeTag.start,
end: end
end: end
};
};
return {
return {
closeTag: closeTag,
closeTag: closeTag,
cursor: closeTag.start
cursor: closeTag.start
};
};
},
},
//Special error type for a http link does not work in a https page
//Special error type for a http link does not work in a https page
HTTP_LINK_FROM_HTTPS_PAGE: function(parser, nameTok, valueTok) {
HTTP_LINK_FROM_HTTPS_PAGE: function(parser, nameTok, valueTok) {
var currentNode = parser.domBuilder.currentNode,
var currentNode = parser.domBuilder.currentNode,
openTag = this._combine({
openTag = this._combine({
name: currentNode.nodeName.toLowerCase()
name: currentNode.nodeName.toLowerCase()
}, currentNode.parseInfo.openTag),
}, currentNode.parseInfo.openTag),
attribute = {
attribute = {
name: {
name: {
value: nameTok.value,
value: nameTok.value,
start: nameTok.interval.start,
start: nameTok.interval.start,
end: nameTok.interval.end
end: nameTok.interval.end
},
},
value: {
value: {
start: valueTok.interval.start + 1,
start: valueTok.interval.start + 1,
end: valueTok.interval.end - 1
end: valueTok.interval.end - 1
}
}
};
};
return {
return {
openTag: openTag,
openTag: openTag,
attribute: attribute,
attribute: attribute,
cursor: attribute.value.start
cursor: attribute.value.start
};
};
},
},
//Special error type for urls that start with www
INVALID_URL: function(parser, nameTok, valueTok) {
var currentNode = parser.domBuilder.currentNode,
openTag = this._combine({
name: currentNode.nodeName.toLowerCase()
}, currentNode.parseInfo.openTag),
attribute = {
name: {
value: nameTok.value,
start: nameTok.interval.start,
end: nameTok.interval.end
},
value: {
start: valueTok.interval.start + 1,
end: valueTok.interval.end - 1
}
};
return {
openTag: openTag,
attribute: attribute,
cursor: attribute.value.start
};
},
// These are CSS errors.
// These are CSS errors.
UNKOWN_CSS_KEYWORD: function(parser, start, end, value) {
UNKOWN_CSS_KEYWORD: function(parser, start, end, value) {
return {
return {
cssKeyword: {
cssKeyword: {
start: start,
start: start,
end: end,
end: end,
value: value
value: value
},
},
cursor: start
cursor: start
};
};
},
},
MISSING_CSS_SELECTOR: function(parser, start, end) {
MISSING_CSS_SELECTOR: function(parser, start, end) {
return {
return {
cssBlock: {
cssBlock: {
start: start,
start: start,
end: end
end: end
},
},
cursor: start
cursor: start
};
};
},
},
UNFINISHED_CSS_SELECTOR: function(parser, start, end, selector) {
UNFINISHED_CSS_SELECTOR: function(parser, start, end, selector) {
return {
return {
cssSelector: {
cssSelector: {
start: start,
start: start,
end: end,
end: end,
selector: selector
selector: selector
},
},
cursor: start
cursor: start
};
};
},
},
MISSING_CSS_BLOCK_OPENER: function(parser, start, end, selector) {
MISSING_CSS_BLOCK_OPENER: function(parser, start, end, selector) {
return {
return {
cssSelector: {
cssSelector: {
start: start,
start: start,
end: end,
end: end,
selector: selector
selector: selector
},
},
cursor: start
cursor: start
};
};
},
},
UNKNOWN_CSS_PROPERTY_NAME: function(parser, start, end, property) {
return {
cssProperty: {
start: start,
end: end,
property: property
},
cursor: start
};
},
INVALID_CSS_PROPERTY_NAME: function(parser, start, end, property) {
INVALID_CSS_PROPERTY_NAME: function(parser, start, end, property) {
return {
return {
cssProperty: {
cssProperty: {
start: start,
start: start,
end: end,
end: end,
property: property
property: property
},
},
cursor: start
cursor: start
};
};
},
},
MISSING_CSS_PROPERTY: function(parser, start, end, selector) {
MISSING_CSS_PROPERTY: function(parser, start, end, selector) {
return {
return {
cssSelector: {
cssSelector: {
start: start,
start: start,
end: end,
end: end,
selector: selector
selector: selector
},
},
cursor: start
cursor: start
};
};
},
},
UNFINISHED_CSS_PROPERTY: function(parser, start, end, property) {
UNFINISHED_CSS_PROPERTY: function(parser, start, end, property) {
return {
return {
cssProperty: {
cssProperty: {
start: start,
start: start,
end: end,
end: end,
property: property
property: property
},
},
cursor: start
cursor: start
};
};
},
},
MISSING_CSS_VALUE: function(parser, start, end, property) {
MISSING_CSS_VALUE: function(parser, start, end, property) {
return {
return {
cssProperty: {
cssProperty: {
start: start,
start: start,
end: end,
end: end,
property: property
property: property
},
},
cursor: start
cursor: start
};
};
},
},
UNFINISHED_CSS_VALUE: function(parser, start, end, value) {
UNFINISHED_CSS_VALUE: function(parser, start, end, value) {
return {
return {
cssValue: {
cssValue: {
start: start,
start: start,
end: end,
end: end,
value: value
value: value
},
},
cursor: start
cursor: start
};
};
},
},
IMPROPER_CSS_VALUE: function(parser, start, end, value) {
return {
cssValue: {
start: start,
end: end,
value: value
},
cursor: start
};
},
CSS_MIXED_ACTIVECONTENT: function(parser, property, propertyStart, value, valueStart, valueEnd) {
CSS_MIXED_ACTIVECONTENT: function(parser, property, propertyStart, value, valueStart, valueEnd) {
var cssProperty = {
var cssProperty = {
property: property,
property: property,
start: propertyStart,
start: propertyStart,
end: propertyStart + property.length
end: propertyStart + property.length
},
},
cssValue = {
cssValue = {
value: value,
value: value,
start: valueStart,
start: valueStart,
end: valueEnd
end: valueEnd
};
};
return {
return {
cssProperty: cssProperty,
cssProperty: cssProperty,
cssValue: cssValue,
cssValue: cssValue,
cursor: cssValue.start
cursor: cssValue.start
};
};
},
},
MISSING_CSS_BLOCK_CLOSER: function(parser, start, end, value) {
MISSING_CSS_BLOCK_CLOSER: function(parser, start, end, value) {
return {
return {
cssValue: {
cssValue: {
start: start,
start: start,
end: end,
end: end,
value: value
value: value
},
},
cursor: start
cursor: start
};
};
},
},
UNCAUGHT_CSS_PARSE_ERROR: function(parser, start, end, msg) {
UNCAUGHT_CSS_PARSE_ERROR: function(parser, start, end, msg) {
return {
return {
error: {
error: {
start: start,
start: start,
end: end,
end: end,
msg: msg
msg: msg
},
},
cursor: start
cursor: start
};
};
},
},
UNTERMINATED_CSS_COMMENT: function(start) {
UNTERMINATED_CSS_COMMENT: function(start) {
return {
return {
start: start,
start: start,
cursor: start
cursor: start
};
};
},
},
HTML_CODE_IN_CSS_BLOCK: function(parser, start, end) {
HTML_CODE_IN_CSS_BLOCK: function(parser, start, end) {
return {
return {
html: {
html: {
start: start,
start: start,
end: end
end: end
},
},
cursor: start
cursor: start
};
};
}
}
};
};


// ### Streams
// ### Streams
//
//
// `Stream` is an internal class used for tokenization. The interface for
// `Stream` is an internal class used for tokenization. The interface for
// this class is inspired by the analogous class in [CodeMirror][].
// this class is inspired by the analogous class in [CodeMirror][].
//
//
// [CodeMirror]: http://codemirror.net/doc/manual.html#modeapi
// [CodeMirror]: http://codemirror.net/doc/manual.html#modeapi
function Stream(text) {
function Stream(text) {
this.text = text;
this.text = text;
this.pos = 0;
this.pos = 0;
this.tokenStart = 0;
this.tokenStart = 0;
}
}


Stream.prototype = {
Stream.prototype = {
// `Stream.peek()` returns the next character in the stream without
// `Stream.peek()` returns the next character in the stream without
// advancing it. It will return `undefined` at the end of the text.
// advancing it. It will return `undefined` at the end of the text.
peek: function() {
peek: function() {
return this.text[this.pos];
return this.text[this.pos];
},
},
// `Stream.substream(len)` returns a substream from the stream
// `Stream.substream(len)` returns a substream from the stream
// without advancing it, with length `len`.
// without advancing it, with length `len`.
substream: function(len) {
substream: function(len) {
return this.text.substring(this.pos, this.pos + len);
return this.text.substring(this.pos, this.pos + len);
},
},
// `Stream.next()` returns the next character in the stream and advances
// `Stream.next()` returns the next character in the stream and advances
// it. It also returns `undefined` when no more characters are available.
// it. It also returns `undefined` when no more characters are available.
next: function() {
next: function() {
if (!this.end())
if (!this.end())
return this.text[this.pos++];
return this.text[this.pos++];
},
},
// `Stream.rewind()` rewinds the stream position by X places.
// `Stream.rewind()` rewinds the stream position by X places.
rewind: function(x) {
rewind: function(x) {
this.pos -= x;
this.pos -= x;
if (this.pos < 0) {
if (this.pos < 0) {
this.pos = 0;
this.pos = 0;
}
}
},
},
// `Stream.end()` returns true only if the stream is at the end of the
// `Stream.end()` returns true only if the stream is at the end of the
// text.
// text.
end: function() {
end: function() {
return (this.pos == this.text.length);
return (this.pos == this.text.length);
},
},
// `Stream.eat()` takes a regular expression. If the next character in
// `Stream.eat()` takes a regular expression. If the next character in
// the stream matches the given argument, it is consumed and returned.
// the stream matches the given argument, it is consumed and returned.
// Otherwise, `undefined` is returned.
// Otherwise, `undefined` is returned.
eat: function(match) {
eat: function(match) {
if (!this.end() && this.peek().match(match))
if (!this.end() && this.peek().match(match))
return this.next();
return this.next();
},
},
// `Stream.eatWhile()` repeatedly calls `eat()` with the given argument,
// `Stream.eatWhile()` repeatedly calls `eat()` with the given argument,
// until it fails. Returns `true` if any characters were eaten.
// until it fails. Returns `true` if any characters were eaten.
eatWhile: function(matcher) {
eatWhile: function(matcher) {
var wereAnyEaten = false;
var wereAnyEaten = false;
while (!this.end()) {
while (!this.end()) {
if (this.eat(matcher))
if (this.eat(matcher))
wereAnyEaten = true;
wereAnyEaten = true;
else
else
return wereAnyEaten;
return wereAnyEaten;
}
}
},
},
// `Stream.eatSpace()` is a shortcut for `eatWhile()` when matching
// `Stream.eatSpace()` is a shortcut for `eatWhile()` when matching
// white-space (including newlines).
// white-space (including newlines).
eatSpace: function() {
eatSpace: function() {
return this.eatWhile(/[\s\n]/);
return this.eatWhile(/[\s\n]/);
},
},
// `Stream.eatCSSWhile()` is like `eatWhile()`, but it
// `Stream.eatCSSWhile()` is like `eatWhile()`, but it
// automatically deals with eating block comments like `/* foo */`.
// automatically deals with eating block comments like `/* foo */`.
eatCSSWhile: function(matcher) {
eatCSSWhile: function(matcher) {
var wereAnyEaten = false,
var wereAnyEaten = false,
chr = '',
chr = '',
peek = '',
peek = '',
next = '';
next = '';
while (!this.end()) {
while (!this.end()) {
chr = this.eat(matcher);
chr = this.eat(matcher);
if (chr)
if (chr)
wereAnyEaten = true;
wereAnyEaten = true;
else
else
return wereAnyEaten;
return wereAnyEaten;
if (chr === '/') {
if (chr === '/') {
peek = this.peek();
peek = this.peek();
if (peek === '*') {
if (peek === '*') {
/* Block comment found. Gobble until resolved. */
/* Block comment found. Gobble until resolved. */
while(next !== '/' && !this.end()) {
while(next !== '/' && !this.end()) {
this.eatWhile(/[^*]/);
this.eatWhile(/[^*]/);
this.next();
this.next();
next = this.next();
next = this.next();
}
}
next = '';
next = '';
}
}
}
}
}
}
},
},
// `Stream.markTokenStart()` will set the start for the next token to
// `Stream.markTokenStart()` will set the start for the next token to
// the current stream position (i.e., "where we are now").
// the current stream position (i.e., "where we are now").
markTokenStart: function() {
markTokenStart: function() {
this.tokenStart = this.pos;
this.tokenStart = this.pos;
},
},
// `Stream.markTokenStartAfterSpace()` is a wrapper function for eating
// `Stream.markTokenStartAfterSpace()` is a wrapper function for eating
// up space, then marking the start for a new token.
// up space, then marking the start for a new token.
markTokenStartAfterSpace: function() {
markTokenStartAfterSpace: function() {
this.eatSpace();
this.eatSpace();
this.markTokenStart();
this.markTokenStart();
},
},
// `Stream.makeToken()` generates a JSON-serializable token object
// `Stream.makeToken()` generates a JSON-serializable token object
// representing the interval of text between the end of the last
// representing the interval of text between the end of the last
// generated token and the current stream position.
// generated token and the current stream position.
makeToken: function() {
makeToken: function() {
if (this.pos == this.tokenStart)
if (this.pos == this.tokenStart)
return null;
return null;
var token = {
var token = {
value: this.text.slice(this.tokenStart, this.pos),
value: this.text.slice(this.tokenStart, this.pos),
interval: {
interval: {
start: this.tokenStart,
start: this.tokenStart,
end: this.pos
end: this.pos
}
}
};
};
this.tokenStart = this.pos;
this.tokenStart = this.pos;
return token;
return token;
},
},
// `Stream.match()` acts like a multi-character eat—if *consume* is `true`
// `Stream.match()` acts like a multi-character eat—if *consume* is `true`
// or not given—or a look-ahead that doesn't update the stream
// or not given—or a look-ahead that doesn't update the stream
// position—if it is `false`. *string* must be a string. *caseFold* can
// position—if it is `false`. *string* must be a string. *caseFold* can
// be set to `true` to make the match case-insensitive.
// be set to `true` to make the match case-insensitive.
match: function(string, consume, caseFold) {
match: function(string, consume, caseFold) {
var substring = this.text.slice(this.pos, this.pos + string.length);
var substring = this.text.slice(this.pos, this.pos + string.length);
if (caseFold) {
if (caseFold) {
string = string.toLowerCase();
string = string.toLowerCase();
substring = substring.toLowerCase();
substring = substring.toLowerCase();
}
}
if (string == substring) {
if (string == substring) {
if (consume)
if (consume)
this.pos += string.length;
this.pos += string.length;
return true;
return true;
}
}
return false;
return false;
},
},
// `Stream.findNext()` is a look-ahead match that doesn't update the stream position
// `Stream.findNext()` is a look-ahead match that doesn't update the stream position
// by a given regular expression
// by a given regular expression
findNext: function(pattern, groupNumber) {
findNext: function(pattern, groupNumber) {
var currentPos = this.pos;
var currentPos = this.pos;
this.eatWhile(/[^>]/);
this.eatWhile(/[^>]/);
this.next();
this.next();
var nextPos = this.pos;
var nextPos = this.pos;
this.pos = currentPos;
this.pos = currentPos;
var token = this.substream(nextPos - currentPos);
var token = this.substream(nextPos - currentPos);
var captureGroups = token.match(pattern);
var captureGroups = token.match(pattern);
this.pos = currentPos;
this.pos = currentPos;
if(captureGroups) {
if(captureGroups) {
return captureGroups[groupNumber];
return captureGroups[groupNumber];
}
}
return false;
return false;
}
}
};
};




// ### CSS Parsing
// ### CSS Parsing
//
//
// `CSSParser` is our internal CSS token stream parser object. This object
// `CSSParser` is our internal CSS token stream parser object. This object
// has references to the stream, as well as the HTML DOM builder that is
// has references to the stream, as well as the HTML DOM builder that is
// used by the HTML parser.
// used by the HTML parser.
function CSSParser(stream, domBuilder, warnings) {
function CSSParser(stream, domBuilder, warnings) {
this.stream = stream;
this.stream = stream;
this.domBuilder = domBuilder;
this.domBuilder = domBuilder;
this.warnings = warnings;
this.warnings = warnings;
}
}


CSSParser.prototype = {
CSSParser.prototype = {
// We keep a list of all currently valid CSS properties (CSS1-CSS3).
// We keep a list of all currently valid CSS properties (CSS1-CSS3).
// This list does not contain vendor prefixes.
// This list does not contain vendor prefixes.
cssProperties: [
cssProperties: [
"alignment-adjust","alignment-baseline","animation","animation-delay",
"alignment-adjust","alignment-baseline","animation","animation-delay",
"animation-direction","animation-duration","animation-iteration-count",
"animation-direction","animation-duration","animation-iteration-count",
"animation-name","animation-play-state","animation-timing-function",
"animation-name","animation-play-state","animation-timing-function",
"appearance","azimuth","backface-visibility","background",
"appearance","azimuth","backface-visibility","background",
"background-attachment","background-clip","background-color",
"background-attachment","background-clip","background-color",
"background-image","background-origin","background-position",

"background-repeat","background-size","baseline-shift","binding",
"bleed","bookmark-label","bookmark-level","bookmark-state",
"bookmark-target","border","border-bottom","border-bottom-color",
"border-bottom-left-radius","border-bottom-right-radius",
"border-bottom-style","border-bottom-width","border-collapse",
"border-color","border-image","border-image-outset",
"border-image-repeat","border-image-slice","border-image-source",
"border-image-width","border-left","border-left-color",
"border-left-style","border-left-width","border-radius","border-right",
"border-right-color","border-right-style","border-right-width",
"border-spacing","border-style","border-top","border-top-color",
"border-top-left-radius","border-top-right-radius","border-top-style",
"border-top-width","border-width","bottom","box-decoration-break",
"box-shadow","box-sizing","break-after","break-before","break-inside",
"caption-side","clear","clip","color","color-profile","column-count",
"column-fill","column-gap","column-rule","column-rule-color",
"column-rule-style","column-rule-width","column-span","column-width",
"columns","content","counter-increment","counter-reset","crop","cue",
"cue-after","cue-before","cursor","direction","display",
"dominant-baseline","drop-initial-after-adjust",
"drop-initial-after-align","drop-initial-before-adjust",
"drop-initial-before-align","drop-initial-size","drop-initial-value",
"elevation","empty-cells","filter","fit","fit-position","flex-align",
"flex-flow","flex-line-pack","flex-order","flex-pack","float","float-offset",
"font","font-family","font-size","font-size-adjust","font-stretch",
"font-style","font-variant","font-weight","grid-columns","grid-rows",
"hanging-punctuation","height","hyphenate-after","hyphenate-before",
"hyphenate-character","hyphenate-lines","hyphenate-resource","hyphens",
"icon","image-orientation","image-rendering","image-resolution",
"inline-box-align","left","letter-spacing","line-break","line-height",
"line-stacking","line-stacking-ruby","line-stacking-shift",
"line-stacking-strategy","list-style","list-style-image",
"list-style-position","list-style-type","margin","margin-bottom",
"margin-left","margin-right","margin-top","marker-offset","marks",
"marquee-direction","marquee-loop","marquee-play-count","marquee-speed",
"marquee-style","max-height","max-width","min-height","min-width",
"move-to","nav-down","nav-index","nav-left","nav-right","nav-up",
"opacity","orphans","outline","outline-color","outline-offset",
"outline-style","outline-width","overflow","overflow-style",
"overflow-wrap","overflow-x","overflow-y","padding","padding-bottom",
"padding-left","padding-right","padding-top","page","page-break-after",
"page-break-before","page-break-inside","page-policy","pause",
"pause-after","pause-before","perspective","perspective-origin",
"phonemes","pitch","pitch-range","play-during","pointer-events",
"position",
"presentation-level","punctuation-trim","quotes","rendering-intent",
"resize","rest","rest-after","rest-before","richness","right",
"rotation","rotation-point","ruby-align","ruby-overhang",
"ruby-position","ruby-span","src","size","speak","speak-header",
"speak-numeral","