htmljs-parser
Advanced tools
Comparing version 1.2.1 to 1.3.0
'use strict'; | ||
var CODE_NEWLINE = 10; | ||
var CODE_CARRIAGE_RETURN = 13; | ||
@@ -27,5 +28,2 @@ class Parser { | ||
this.data = null; | ||
// The 1-based line number | ||
this.lineNumber = 1; | ||
} | ||
@@ -41,3 +39,3 @@ | ||
// so we should throw error to catch these types of mistakes | ||
throw new Error('Re-entering the current state is illegal'); | ||
throw new Error('Re-entering the current state is illegal - ' + state.name); | ||
} | ||
@@ -64,5 +62,7 @@ | ||
*/ | ||
lookAheadFor(str, callback) { | ||
lookAheadFor(str, startPos) { | ||
// Have we read enough chunks to read the string that we need? | ||
var startPos = this.pos + 1; // Move past the current character | ||
if (startPos == null) { | ||
startPos = this.pos + 1; | ||
} | ||
var len = str.length; | ||
@@ -75,4 +75,2 @@ var endPos = startPos + len; | ||
var found = this.data.substring(startPos, endPos); | ||
@@ -87,20 +85,27 @@ return (found === str) ? str : undefined; | ||
*/ | ||
lookAtCharAhead(offset, callback) { | ||
return this.data.charAt(this.pos + offset); | ||
lookAtCharAhead(offset, startPos) { | ||
if (startPos == null) { | ||
startPos = this.pos; | ||
} | ||
return this.data.charAt(startPos + offset); | ||
} | ||
lookAtCharCodeAhead(offset, callback) { | ||
return this.data.charCodeAt(this.pos + offset); | ||
lookAtCharCodeAhead(offset, startPos) { | ||
if (startPos == null) { | ||
startPos = this.pos; | ||
} | ||
return this.data.charCodeAt(startPos + offset); | ||
} | ||
rewind(offset) { | ||
this.pos -= offset; | ||
} | ||
skip(offset) { | ||
// console.log('-- ' + JSON.stringify(this.data.substring(this.pos, this.pos + offset)) + ' -- ' + 'SKIPPED'.gray); | ||
var i = this.pos; | ||
this.pos += offset; | ||
} | ||
for (; i < this.pos; i++) { | ||
if (this.data.charCodeAt(this.pos) === CODE_NEWLINE) { | ||
this.lineNumber++; | ||
} | ||
} | ||
end() { | ||
this.pos = this.maxPos + 1; | ||
} | ||
@@ -139,7 +144,21 @@ | ||
while ((pos = this.pos) <= this.maxPos) { | ||
var ch = data[pos]; | ||
var code = ch.charCodeAt(0); | ||
let ch = data[pos]; | ||
let code = ch.charCodeAt(0); | ||
let state = this.state; | ||
if (code === CODE_NEWLINE) { | ||
this.lineNumber++; | ||
if (state.eol) { | ||
state.eol.call(this, ch); | ||
} | ||
this.pos++; | ||
continue; | ||
} else if (code === CODE_CARRIAGE_RETURN) { | ||
let nextPos = pos + 1; | ||
if (nextPos < data.length && data.charChodeAt(nextPos) === CODE_NEWLINE) { | ||
if (state.eol) { | ||
state.eol.call(this, '\r\n'); | ||
} | ||
this.pos+=2; | ||
continue; | ||
} | ||
} | ||
@@ -150,3 +169,3 @@ | ||
// We assume that every state will have "char" function | ||
this.state.char.call(this, ch, code); | ||
state.char.call(this, ch, code); | ||
@@ -157,4 +176,4 @@ // move to next position | ||
var state; | ||
if ((state = this.state) && state.eof) { | ||
let state = this.state; | ||
if (state && state.eof) { | ||
state.eof.call(this); | ||
@@ -161,0 +180,0 @@ } |
206
html-tags.js
var openTagOnly = {}; | ||
var requireClosingTag = {}; | ||
@@ -23,103 +22,106 @@ [ | ||
[ | ||
'a', | ||
'abbr', | ||
'address', | ||
'area', | ||
'article', | ||
'aside', | ||
'audio', | ||
'b', | ||
'bdi', | ||
'bdo', | ||
'blockquote', | ||
'body', | ||
'button', | ||
'canvas', | ||
'caption', | ||
'cite', | ||
'code', | ||
'colgroup', | ||
'command', | ||
'datalist', | ||
'dd', | ||
'del', | ||
'details', | ||
'dfn', | ||
'div', | ||
'dl', | ||
'dt', | ||
'em', | ||
'fieldset', | ||
'figcaption', | ||
'figure', | ||
'footer', | ||
'form', | ||
'h1', | ||
'h2', | ||
'h3', | ||
'h4', | ||
'h5', | ||
'h6', | ||
'head', | ||
'header', | ||
'hgroup', | ||
'html', | ||
'i', | ||
'iframe', | ||
'ins', | ||
'kbd', | ||
'label', | ||
'legend', | ||
'li', | ||
'map', | ||
'mark', | ||
'menu', | ||
'meter', | ||
'nav', | ||
'noscript', | ||
'object', | ||
'ol', | ||
'optgroup', | ||
'option', | ||
'output', | ||
'p', | ||
'pre', | ||
'progress', | ||
'q', | ||
'rp', | ||
'rt', | ||
'ruby', | ||
's', | ||
'samp', | ||
'script', | ||
'section', | ||
'select', | ||
'small', | ||
'span', | ||
'strong', | ||
'style', | ||
'sub', | ||
'summary', | ||
'sup', | ||
'table', | ||
'tbody', | ||
'td', | ||
'textarea', | ||
'tfoot', | ||
'th', | ||
'thead', | ||
'time', | ||
'title', | ||
'tr', | ||
'u', | ||
'ul', | ||
'var', | ||
'video', | ||
'wbr' | ||
].forEach(function(tagName) { | ||
requireClosingTag[tagName] = true; | ||
}); | ||
// [ | ||
// 'a', | ||
// 'abbr', | ||
// 'address', | ||
// 'area', | ||
// 'article', | ||
// 'aside', | ||
// 'audio', | ||
// 'b', | ||
// 'bdi', | ||
// 'bdo', | ||
// 'blockquote', | ||
// 'body', | ||
// 'button', | ||
// 'canvas', | ||
// 'caption', | ||
// 'cite', | ||
// 'code', | ||
// 'colgroup', | ||
// 'command', | ||
// 'datalist', | ||
// 'dd', | ||
// 'del', | ||
// 'details', | ||
// 'dfn', | ||
// 'div', | ||
// 'dl', | ||
// 'dt', | ||
// 'em', | ||
// 'fieldset', | ||
// 'figcaption', | ||
// 'figure', | ||
// 'footer', | ||
// 'form', | ||
// 'h1', | ||
// 'h2', | ||
// 'h3', | ||
// 'h4', | ||
// 'h5', | ||
// 'h6', | ||
// 'head', | ||
// 'header', | ||
// 'hgroup', | ||
// 'html', | ||
// 'i', | ||
// 'iframe', | ||
// 'ins', | ||
// 'kbd', | ||
// 'label', | ||
// 'legend', | ||
// 'li', | ||
// 'map', | ||
// 'mark', | ||
// 'menu', | ||
// 'meter', | ||
// 'nav', | ||
// 'noscript', | ||
// 'object', | ||
// 'ol', | ||
// 'optgroup', | ||
// 'option', | ||
// 'output', | ||
// 'p', | ||
// 'pre', | ||
// 'progress', | ||
// 'q', | ||
// 'rp', | ||
// 'rt', | ||
// 'ruby', | ||
// 's', | ||
// 'samp', | ||
// 'script', | ||
// 'section', | ||
// 'select', | ||
// 'small', | ||
// 'span', | ||
// 'strong', | ||
// 'style', | ||
// 'sub', | ||
// 'summary', | ||
// 'sup', | ||
// 'table', | ||
// 'tbody', | ||
// 'td', | ||
// 'textarea', | ||
// 'tfoot', | ||
// 'th', | ||
// 'thead', | ||
// 'time', | ||
// 'title', | ||
// 'tr', | ||
// 'u', | ||
// 'ul', | ||
// 'var', | ||
// 'video', | ||
// 'wbr' | ||
// ].forEach(function(tagName) { | ||
// openTagOnly[tagName] = { | ||
// requireClosingTag: true | ||
// }; | ||
// }); | ||
exports.openTagOnly = openTagOnly; | ||
exports.requireClosingTag = requireClosingTag; | ||
exports.isOpenTagOnly = function(tagName) { | ||
return openTagOnly.hasOwnProperty(tagName); | ||
}; |
10
index.js
var Parser = require('./Parser'); | ||
var ValidatingParser = require('./ValidatingParser'); | ||
exports.createNonValidatingParser = function(listeners, options) { | ||
exports.createParser = function(listeners, options) { | ||
var parser = new Parser(listeners, options); | ||
return parser; | ||
}; | ||
exports.createParser = function(listeners, options) { | ||
var parser = new ValidatingParser(listeners, options); | ||
return parser; | ||
}; | ||
}; |
@@ -1,75 +0,34 @@ | ||
var CODE_NEWLINE = 10; | ||
var NUMBER_REGEX = /^[\-\+]?\d*(?:\.\d+)?(?:e[\-\+]?\d+)?$/; | ||
exports.createNotifiers = function(parser, listeners) { | ||
var hasError = false; | ||
function _removeDelimitersFromArgument(arg) { | ||
return arg.substring(1, arg.length - 1); | ||
} | ||
function _updateAttributeLiteralValue(attr) { | ||
var expression = attr.expression; | ||
if (expression.length === 0) { | ||
attr.literalValue = ''; | ||
} else if (expression === 'true') { | ||
attr.literalValue = true; | ||
} else if (expression === 'false') { | ||
attr.literalValue = false; | ||
} else if (expression === 'null') { | ||
attr.literalValue = null; | ||
} else if (expression === 'undefined') { | ||
attr.literalValue = undefined; | ||
} else if (NUMBER_REGEX.test(expression)) { | ||
attr.literalValue = Number(expression); | ||
} | ||
} | ||
/** | ||
* Takes a string expression such as `"foo"` or `'foo "bar"'` | ||
* and returns the literal String value. | ||
*/ | ||
function evaluateStringExpression(expression) { | ||
// We could just use eval(expression) to get the literal String value, | ||
// but there is a small chance we could be introducing a security threat | ||
// by accidently running malicous code. Instead, we will use | ||
// JSON.parse(expression). JSON.parse() only allows strings | ||
// that use double quotes so we have to do extra processing if | ||
// we detect that the String uses single quotes | ||
if (expression.charAt(0) === "'") { | ||
expression = expression.substring(1, expression.length - 1); | ||
// Make sure there are no unescaped double quotes in the string expression... | ||
expression = expression.replace(/\\\\|\\["]|["]/g, function(match) { | ||
if (match === '"'){ | ||
// Return an escaped double quote if we encounter an | ||
// unescaped double quote | ||
return '\\"'; | ||
} else { | ||
// Return the escape sequence | ||
return match; | ||
return { | ||
notifyText(value) { | ||
if (hasError) { | ||
return; | ||
} | ||
}); | ||
expression = '"' + expression + '"'; | ||
} | ||
var eventFunc = listeners.onText; | ||
return JSON.parse(expression); | ||
} | ||
exports.createNotifiers = function(parser, listeners) { | ||
return { | ||
notifyText(txt) { | ||
if (listeners.ontext && (txt.length > 0)) { | ||
listeners.ontext({ | ||
if (eventFunc && (value.length > 0)) { | ||
eventFunc.call(parser, { | ||
type: 'text', | ||
text: txt | ||
}); | ||
value: value | ||
}, parser); | ||
} | ||
}, | ||
notifyCDATA(txt) { | ||
if (listeners.oncdata && txt) { | ||
listeners.oncdata({ | ||
notifyCDATA(value, pos, endPos) { | ||
if (hasError) { | ||
return; | ||
} | ||
var eventFunc = listeners.onCDATA; | ||
if (eventFunc && value) { | ||
eventFunc.call(parser, { | ||
type: 'cdata', | ||
text: txt | ||
}); | ||
value: value, | ||
pos: pos, | ||
endPos: endPos | ||
}, parser); | ||
} | ||
@@ -79,4 +38,12 @@ }, | ||
notifyError(pos, errorCode, message) { | ||
if (listeners.onerror) { | ||
listeners.onerror({ | ||
if (hasError) { | ||
return; | ||
} | ||
hasError = true; | ||
var eventFunc = listeners.onError; | ||
if (eventFunc) { | ||
eventFunc.call(parser, { | ||
type: 'error', | ||
@@ -87,107 +54,159 @@ code: errorCode, | ||
endPos: parser.pos | ||
}); | ||
}, parser); | ||
} | ||
}, | ||
notifyOpenTag(tagName, attributes, elementArguments, selfClosed, pos) { | ||
if (listeners.onopentag) { | ||
if (elementArguments) { | ||
elementArguments = _removeDelimitersFromArgument(elementArguments); | ||
} | ||
notifyOpenTag(tagInfo) { | ||
if (hasError) { | ||
return; | ||
} | ||
var eventFunc = listeners.onOpenTag; | ||
if (eventFunc) { | ||
// set the literalValue property for attributes that are simple | ||
// string simple values or simple literal values | ||
var i = attributes.length; | ||
while(--i >= 0) { | ||
var attr = attributes[i]; | ||
// If the expression evaluates to a literal value then add the | ||
// `literalValue` property to the attribute | ||
if (attr.isStringLiteral) { | ||
var expression = attr.expression; | ||
attr.literalValue = evaluateStringExpression(expression); | ||
} else if (attr.isSimpleLiteral) { | ||
_updateAttributeLiteralValue(attr); | ||
} | ||
var event = { | ||
type: 'openTag', | ||
tagName: tagInfo.tagName, | ||
argument: tagInfo.argument, | ||
pos: tagInfo.pos, | ||
endPos: tagInfo.endPos, | ||
openTagOnly: tagInfo.openTagOnly, | ||
selfClosed: tagInfo.selfClosed, | ||
concise: tagInfo.concise | ||
}; | ||
if (attr.argument) { | ||
attr.argument = _removeDelimitersFromArgument(attr.argument); | ||
event.attributes = tagInfo.attributes.map((attr) => { | ||
var newAttr = { | ||
name: attr.name, | ||
value: attr.value, | ||
pos: attr.pos, | ||
endPos: attr.endPos, | ||
argument: attr.argument | ||
}; | ||
if (attr.hasOwnProperty('literalValue')) { | ||
newAttr.literalValue = attr.literalValue; | ||
} | ||
delete attr.isStringLiteral; | ||
delete attr.isSimpleLiteral; | ||
} | ||
return newAttr; | ||
}); | ||
eventFunc.call(parser, event, parser); | ||
} | ||
}, | ||
notifyCloseTag(tagName, pos, endPos) { | ||
if (hasError) { | ||
return; | ||
} | ||
var eventFunc = listeners.onCloseTag; | ||
if (eventFunc) { | ||
var event = { | ||
type: 'opentag', | ||
type: 'closeTag', | ||
tagName: tagName, | ||
attributes: attributes, | ||
pos: pos | ||
pos: pos, | ||
endPos: endPos | ||
}; | ||
if (elementArguments) { | ||
event.argument = elementArguments; | ||
} | ||
if (selfClosed) { | ||
event.selfClosed = true; | ||
} | ||
listeners.onopentag.call(parser, event); | ||
eventFunc.call(parser, event, parser); | ||
} | ||
}, | ||
notifyCloseTag(tagName, selfClosed) { | ||
if (listeners.onclosetag) { | ||
var event = { | ||
type: 'closetag', | ||
tagName: tagName | ||
}; | ||
notifyDocumentType(documentType) { | ||
if (hasError) { | ||
return; | ||
} | ||
if (selfClosed) { | ||
event.selfClosed = true; | ||
} | ||
var eventFunc = listeners.onDocumentType; | ||
listeners.onclosetag.call(parser, event); | ||
if (eventFunc) { | ||
eventFunc.call(this, { | ||
type: 'documentType', | ||
value: documentType.value, | ||
pos: documentType.pos, | ||
endPos: documentType.endPos | ||
}, parser); | ||
} | ||
}, | ||
notifyDTD(dtd) { | ||
if (listeners.ondtd) { | ||
listeners.ondtd({ | ||
type: 'dtd', | ||
dtd: dtd | ||
}); | ||
notifyDeclaration(declaration) { | ||
if (hasError) { | ||
return; | ||
} | ||
}, | ||
notifyDeclaration(declaration) { | ||
if (listeners.ondeclaration) { | ||
listeners.ondeclaration.call(parser, { | ||
var eventFunc = listeners.onDeclaration; | ||
if (eventFunc) { | ||
eventFunc.call(parser, { | ||
type: 'declaration', | ||
declaration: declaration | ||
}); | ||
value: declaration.value, | ||
pos: declaration.pos, | ||
endPos: declaration.endPos | ||
}, parser); | ||
} | ||
}, | ||
notifyCommentText(txt) { | ||
if (listeners.oncomment && txt) { | ||
listeners.oncomment.call(parser, { | ||
notifyComment(comment) { | ||
if (hasError) { | ||
return; | ||
} | ||
var eventFunc = listeners.onComment; | ||
if (eventFunc && comment.value) { | ||
eventFunc.call(parser, { | ||
type: 'comment', | ||
comment: txt | ||
}); | ||
value: comment.value, | ||
pos: comment.pos, | ||
endPos: comment.endPos | ||
}, parser); | ||
} | ||
}, | ||
notifyScriptlet(scriptlet) { | ||
if (hasError) { | ||
return; | ||
} | ||
var eventFunc = listeners.onScriptlet; | ||
if (eventFunc && scriptlet.value) { | ||
eventFunc.call(parser, { | ||
type: 'scriptlet', | ||
value: scriptlet.value, | ||
pos: scriptlet.pos, | ||
endPos: scriptlet.endPos | ||
}, parser); | ||
} | ||
}, | ||
notifyPlaceholder(placeholder) { | ||
var eventFunc = listeners['on' + placeholder.type]; | ||
if (hasError) { | ||
return; | ||
} | ||
var eventFunc = listeners.onPlaceholder; | ||
if (eventFunc) { | ||
// remove unnecessary properties | ||
['depth', 'stringDelimiter', 'delimiterDepth', 'parentState', 'handler'] | ||
.forEach(function(key) { | ||
delete placeholder[key]; | ||
}); | ||
eventFunc.call(parser, placeholder); | ||
var placeholderEvent = { | ||
type: 'placeholder', | ||
value: placeholder.value, | ||
pos: placeholder.pos, | ||
endPos: placeholder.endPos, | ||
escape: placeholder.escape !== false, | ||
withinBody: placeholder.withinBody === true, | ||
withinAttribute: placeholder.withinAttribute === true, | ||
withinString: placeholder.withinString === true, | ||
withinOpenTag: placeholder.withinOpenTag === true, | ||
}; | ||
eventFunc.call(parser, placeholderEvent, parser); | ||
return placeholderEvent.value; | ||
} | ||
return placeholder.value; | ||
}, | ||
@@ -197,3 +216,3 @@ | ||
if (listeners.onfinish) { | ||
listeners.onfinish.call(parser, {}); | ||
listeners.onfinish.call(parser, {}, parser); | ||
} | ||
@@ -200,0 +219,0 @@ } |
@@ -36,3 +36,3 @@ { | ||
}, | ||
"version": "1.2.1" | ||
"version": "1.3.0" | ||
} |
302
README.md
@@ -25,2 +25,3 @@ htmljs-parser | ||
This parser extends the HTML grammar to add these important features: | ||
- JavaScript expressions as attribute values | ||
@@ -60,46 +61,72 @@ ```html | ||
```javascript | ||
var htmljs = require('htmljs-parser'); | ||
var parser = htmljs.createParser({ | ||
ontext: function(event) { | ||
// text | ||
var parser = require('htmljs-parser').createParser({ | ||
onText: function(event) { | ||
// Text within an HTML element | ||
var value = event.value; | ||
}, | ||
oncontentplaceholder: function(event) { | ||
// placeholder within content | ||
onPlaceholder: function(event) { | ||
// ${<value>]} // escape = true | ||
// $!{<value>]} // escape = false | ||
var value = event.value; // String | ||
var escaped = event.escaped; // boolean | ||
var withinBody = event.withinBody; // boolean | ||
var withinAttribute = event.withinAttribute; // boolean | ||
var withinString = event.withinString; // boolean | ||
var withinOpenTag = event.withinOpenTag; // boolean | ||
var pos = event.pos; // Integer | ||
}, | ||
onnestedcontentplaceholder: function(event) { | ||
// placeholder within string that is within content placeholder | ||
onCDATA: function(event) { | ||
// <![CDATA[<value>]]> | ||
var value = event.value; // String | ||
var pos = event.pos; // Integer | ||
}, | ||
onattributeplaceholder: function(event) { | ||
// placeholder within attribute | ||
onOpenTag: function(event) { | ||
var tagName = event.tagName; // String | ||
var attributes = event.attributes; // Array | ||
var argument = event.argument; // Object | ||
var pos = event.pos; // Integer | ||
}, | ||
oncdata: function(event) { | ||
// CDATA | ||
onCloseTag: function(event) { | ||
// close tag | ||
var tagName = event.tagName; // String | ||
var pos = event.pos; // Integer | ||
}, | ||
onopentag: function(event) { | ||
// open tag | ||
onDocumentType: function(event) { | ||
// Document Type/DTD | ||
// <!<value>> | ||
// Example: <!DOCTYPE html> | ||
var value = event.value; // String | ||
var pos = event.pos; // Integer | ||
}, | ||
onclosetag: function(event) { | ||
// close tag | ||
onDeclaration: function(event) { | ||
// Declaration | ||
// <?<value>?> | ||
// Example: <?xml version="1.0" encoding="UTF-8" ?> | ||
var value = event.value; // String | ||
var pos = event.pos; // Integer | ||
}, | ||
ondtd: function(event) { | ||
// DTD (e.g. <DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0//EN">) | ||
onComment: function(event) { | ||
// Text within XML comment | ||
var value = event.value; // String | ||
var pos = event.pos; // Integer | ||
}, | ||
ondeclaration: function(event) { | ||
// Declaration (e.g. <?xml version="1.0" encoding="UTF-8" ?>) | ||
onScriptlet: function(event) { | ||
// Text within <% %> | ||
var value = event.value; // String | ||
var pos = event.pos; // Integer | ||
}, | ||
oncomment: function(event) { | ||
// Text within XML comment | ||
}, | ||
onerror: function(event) { | ||
onError: function(event) { | ||
// Error | ||
var message = event.message; // String | ||
var code = event.code; // String | ||
var pos = event.pos; // Integer | ||
} | ||
@@ -115,3 +142,3 @@ }); | ||
might not be desirable for certain tags, so the parser allows the parsing mode | ||
to be changed (usually in response to an `onopentag` event). | ||
to be changed (usually in response to an `onOpenTag` event). | ||
@@ -134,3 +161,3 @@ There are three content parsing modes: | ||
var parser = htmljs.createParser({ | ||
onopentag: function(event) { | ||
onOpenTag: function(event) { | ||
// open tag | ||
@@ -155,3 +182,3 @@ switch(event.tagName) { | ||
// if the parsing mode is not explicitly changed by | ||
// "onopentag" function. | ||
// "onOpenTag" function. | ||
} | ||
@@ -171,5 +198,5 @@ } | ||
### onopentag | ||
### onOpenTag | ||
The `onopentag` function will be called each time an opening tag is | ||
The `onOpenTag` function will be called each time an opening tag is | ||
encountered. | ||
@@ -189,3 +216,3 @@ | ||
{ | ||
type: 'opentag', | ||
type: 'openTag', | ||
tagName: 'div', | ||
@@ -208,3 +235,3 @@ attributes: [] | ||
{ | ||
type: 'opentag', | ||
type: 'openTag', | ||
tagName: 'div', | ||
@@ -214,3 +241,3 @@ attributes: [ | ||
name: 'class', | ||
expression: '"demo"', | ||
value: '"demo"', | ||
literalValue: 'demo' | ||
@@ -220,3 +247,3 @@ }, | ||
name: 'disabled', | ||
expression: 'false', | ||
value: 'false', | ||
literalValue: false | ||
@@ -226,3 +253,3 @@ }, | ||
name: 'data-number', | ||
expression: '123', | ||
value: '123', | ||
literalValue: 123 | ||
@@ -246,3 +273,3 @@ } | ||
{ | ||
type: 'opentag', | ||
type: 'openTag', | ||
tagName: 'div', | ||
@@ -252,3 +279,3 @@ attributes: [ | ||
name: 'message', | ||
expression: '"Hello "+data.name' | ||
value: '"Hello "+data.name' | ||
} | ||
@@ -271,5 +298,8 @@ ] | ||
{ | ||
type: 'opentag', | ||
type: 'openTag', | ||
tagName: 'for', | ||
argument: 'var i = 0; i < 10; i++', | ||
argument: { | ||
value: 'var i = 0; i < 10; i++', | ||
pos: ... // Integer | ||
}, | ||
attributes: [] | ||
@@ -291,3 +321,3 @@ } | ||
{ | ||
type: 'opentag', | ||
type: 'openTag', | ||
tagName: 'div', | ||
@@ -297,3 +327,6 @@ attributes: [ | ||
name: 'if', | ||
argument: 'x > y' | ||
argument: { | ||
value: 'x > y', | ||
pos: ... // Integer | ||
} | ||
} | ||
@@ -304,5 +337,5 @@ ] | ||
### onclosetag | ||
### onCloseTag | ||
The `onclosetag` function will be called each time a closing tag is | ||
The `onCloseTag` function will be called each time a closing tag is | ||
encountered. | ||
@@ -322,3 +355,3 @@ | ||
{ | ||
type: 'closetag', | ||
type: 'closeTag', | ||
tagName: 'div' | ||
@@ -328,9 +361,9 @@ } | ||
### ontext | ||
### onText | ||
The `ontext` function will be called each time within an element | ||
The `onText` function will be called each time within an element | ||
when textual data is encountered. | ||
**NOTE:** Text within `<![CDATA[` `]]>` will be emitted via call | ||
to `oncdata`. | ||
to `onCDATA`. | ||
@@ -353,9 +386,9 @@ **EXAMPLE** | ||
type: 'text', | ||
text: 'Simple text' | ||
value: 'Simple text' | ||
} | ||
``` | ||
### oncdata | ||
### onCDATA | ||
The `oncdata` function will be called when text within `<![CDATA[` `]]>` | ||
The `onCDATA` function will be called when text within `<![CDATA[` `]]>` | ||
is encountered. | ||
@@ -376,10 +409,10 @@ | ||
type: 'cdata', | ||
text: 'This is text' | ||
value: 'This is text' | ||
} | ||
``` | ||
### oncontentplaceholder | ||
### onPlaceholder | ||
The `oncontentplaceholder` function will be called each time a placeholder | ||
is encountered within parsed textual content within elements. | ||
The `onPlaceholder` function will be called each time a placeholder | ||
is encountered. | ||
@@ -406,8 +439,4 @@ If the placeholder starts with the `$!{` sequence then `event.escape` | ||
```javascript | ||
{ | ||
type: 'contentplaceholder', | ||
expression: '"This is an escaped placeholder"', | ||
escape: true | ||
} | ||
```html | ||
${name} | ||
``` | ||
@@ -417,55 +446,18 @@ | ||
{ | ||
type: 'contentplaceholder', | ||
expression: '"This is a non-escaped placeholder"', | ||
escape: false | ||
type: 'placeholder', | ||
value: 'name', | ||
escape: true | ||
} | ||
``` | ||
**NOTE:** | ||
The `escape` flag is merely informational. The application code is responsible | ||
for interpreting this flag to properly escape the expression. | ||
-------- | ||
### onnestedcontentplaceholder | ||
The `onnestedcontentplaceholder` function will be called each time a placeholder | ||
is encountered within a string that is also within another content placeholder. | ||
If the placeholder starts with the `$!{` sequence then `event.escape` | ||
will be `false`. | ||
If the placeholder starts with the `${` sequence then `event.escape` will be | ||
`true` unless the placeholder is nested within another placeholder that is | ||
already escaped. | ||
The `event.expression` property can be changed which will cause corresponding | ||
change to ancestor content placeholder expression. | ||
Here's an example of modifying the expression based on the `event.escape` flag: | ||
```javascript | ||
onnestedcontentplaceholder: function(event) { | ||
if (event.escape) { | ||
event.expression = 'escapeXml(' + event.expression + ')'; | ||
} | ||
} | ||
``` | ||
**EXAMPLE:** | ||
INPUT: | ||
```html | ||
${"Hello ${data.name}"} | ||
$!{name} | ||
``` | ||
The `${data.name}` sequence will trigger the call to | ||
`onnestedcontentplaceholder`. | ||
OUTPUT EVENTS | ||
```javascript | ||
{ | ||
type: 'nestedcontentplaceholder', | ||
expression: 'data.name', | ||
type: 'placeholder', | ||
value: 'name', | ||
escape: true | ||
@@ -475,10 +467,2 @@ } | ||
```javascript | ||
{ | ||
type: 'contentplaceholder', | ||
expression: '"Hello "+(data.name)+"!"', | ||
escape: true | ||
} | ||
``` | ||
**NOTE:** | ||
@@ -488,15 +472,8 @@ The `escape` flag is merely informational. The application code is responsible | ||
### onattributeplaceholder | ||
The `onattributeplaceholder` function will be called each time a placeholder | ||
is encountered within an attribute string value. This event will be emitted | ||
before `onopentag` so by changing the `expression` property of the event, | ||
the resultant attribute can be changed. | ||
Here's an example of modifying the expression based on the `event.escape` flag: | ||
```javascript | ||
onattributeplaceholder: function(event) { | ||
onPlaceholder: function(event) { | ||
if (event.escape) { | ||
event.expression = 'escapeAttr(' + event.expression + ')'; | ||
event.value = 'escapeXml(' + event.value + ')'; | ||
} | ||
@@ -506,8 +483,6 @@ } | ||
If the placeholder starts with the `$!{` sequence then `event.escape` | ||
will be `false`. | ||
### onDocumentType | ||
If the placeholder starts with the `${` sequence then `event.escape` will be | ||
`true` unless the placeholder is nested within another placeholder that is | ||
already escaped. | ||
The `onDocumentType` function will be called when the document type declaration | ||
is encountered _anywhere_ in the content. | ||
@@ -519,3 +494,3 @@ **EXAMPLE:** | ||
```html | ||
<div class="${data.className}"><div> | ||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0//EN"> | ||
``` | ||
@@ -527,17 +502,10 @@ | ||
{ | ||
type: 'attributeplaceholder', | ||
expression: 'data.className', | ||
escape: true | ||
type: 'documentType', | ||
value: 'DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0//EN"' | ||
} | ||
``` | ||
**NOTE:** | ||
The `escape` flag is merely informational. The application code is responsible | ||
for interpreting this flag to properly escape the expression. The `expression` | ||
property can be altered by the `onattributeplaceholder` function and the | ||
attribute information emitted via `onopentag` will reflect this change. | ||
### onDeclaration | ||
### ondtd | ||
The `ondtd` function will be called when the document type declaration | ||
The `onDeclaration` function will be called when an XML declaration | ||
is encountered _anywhere_ in the content. | ||
@@ -550,3 +518,3 @@ | ||
```html | ||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0//EN"> | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
``` | ||
@@ -558,11 +526,11 @@ | ||
{ | ||
type: 'dtd', | ||
dtd: 'DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0//EN"' | ||
type: 'declaration', | ||
value: 'xml version="1.0" encoding="UTF-8"' | ||
} | ||
``` | ||
### ondeclaration | ||
### onComment | ||
The `ondeclaration` function will be called when an XML declaration | ||
is encountered _anywhere_ in the content. | ||
The `onComment` function will be called when text within `<!--` `-->` | ||
is encountered. | ||
@@ -574,3 +542,3 @@ **EXAMPLE:** | ||
```html | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!--This is a comment--> | ||
``` | ||
@@ -582,10 +550,10 @@ | ||
{ | ||
type: 'declaration', | ||
declaration: 'xml version="1.0" encoding="UTF-8"' | ||
type: 'comment', | ||
value: 'This is a comment' | ||
} | ||
``` | ||
### oncomment | ||
### onScriptlet | ||
The `oncomment` function will be called when text within `<!--` `-->` | ||
The `onScriptlet` function will be called when text within `<%` `%>` | ||
is encountered. | ||
@@ -598,3 +566,3 @@ | ||
```html | ||
<!--This is a comment--> | ||
<% console.log("Hello World!"); %> | ||
``` | ||
@@ -606,10 +574,10 @@ | ||
{ | ||
type: 'comment', | ||
text: 'This is a comment' | ||
type: 'scriptlet', | ||
value: ' console.log("Hello World!"); ' | ||
} | ||
``` | ||
### onerror | ||
### onError | ||
The `onerror` function will be called when malformed content is detected. | ||
The `onError` function will be called when malformed content is detected. | ||
The most common cause for an error is due to reaching the end of the | ||
@@ -619,13 +587,16 @@ input while still parsing an open tag, close tag, XML comment, CDATA section, | ||
Possible errors: | ||
Possible error codes: | ||
- `ILLEGAL_ELEMENT_ARGUMENT`: Element can only have one argument | ||
- `ILLEGAL_ATTRIBUTE_ARGUMENT`: Attribute can only have one argument | ||
- `MALFORMED_OPEN_TAG`: EOF reached while parsing open tag | ||
- `MALFORMED_CLOSE_TAG`: EOF reached while parsing closing element | ||
- `MALFORMED_CDATA`: EOF reached while parsing CDATA | ||
- `MALFORMED_PLACEHOLDER`: EOF reached while parsing placeholder | ||
- `MALFORMED_DTD`: EOF reached while parsing DTD | ||
- `MALFORMED_DECLARATION`: EOF reached while parsing declaration | ||
- `MALFORMED_COMMENT`: EOF reached while parsing comment | ||
- `MISSING_END_TAG` | ||
- `MISSING_END_DELIMITER` | ||
- `MALFORMED_OPEN_TAG` | ||
- `MALFORMED_CLOSE_TAG` | ||
- `MALFORMED_CDATA` | ||
- `MALFORMED_PLACEHOLDER` | ||
- `MALFORMED_DOCUMENT_TYPE` | ||
- `MALFORMED_DECLARATION` | ||
- `MALFORMED_COMMENT` | ||
- `EXTRA_CLOSING_TAG` | ||
- `MISMATCHED_CLOSING_TAG` | ||
- ... | ||
@@ -647,6 +618,5 @@ **EXAMPLE:** | ||
message: 'EOF reached while parsing open tag.', | ||
lineNumber: 1, | ||
startPos: 0, | ||
pos: 0, | ||
endPos: 9 | ||
} | ||
``` |
@@ -6,27 +6,43 @@ var fs = require('fs'); | ||
function autoTest(name, dir, run) { | ||
var actualPath = path.join(dir, 'actual.json'); | ||
var expectedPath = path.join(dir, 'expected.json'); | ||
function autoTest(name, dir, run, options) { | ||
var ext = options.ext || '.json'; | ||
var actualPath = path.join(dir, 'actual' + ext); | ||
var expectedPath = path.join(dir, 'expected' + ext); | ||
var actual = run(dir); | ||
var actualJSON = JSON.stringify(actual, null, 4); | ||
fs.writeFileSync(actualPath, actualJSON, {encoding: 'utf8'}); | ||
var expectedJSON; | ||
fs.writeFileSync(actualPath, ext === '.json' ? JSON.stringify(actual, null, 4) : actual, {encoding: 'utf8'}); | ||
var expected; | ||
try { | ||
expectedJSON = fs.readFileSync(expectedPath, { encoding: 'utf8' }); | ||
expected = fs.readFileSync(expectedPath, { encoding: 'utf8' }); | ||
} catch(e) { | ||
expectedJSON = '"TBD"'; | ||
fs.writeFileSync(expectedPath, expectedJSON, {encoding: 'utf8'}); | ||
expected = ext === '.json' ? '"TBD"' : 'TBD'; | ||
fs.writeFileSync(expectedPath, expected, {encoding: 'utf8'}); | ||
} | ||
var expected = JSON.parse(expectedJSON); | ||
if (ext === '.json') { | ||
var expectedObject = JSON.parse(expected); | ||
assert.deepEqual( | ||
actual, | ||
expected, | ||
'Unexpected output for "' + name + '":\nEXPECTED (' + expectedPath + '):\n---------\n' + expectedJSON + | ||
'\n---------\nACTUAL (' + actualPath + '):\n---------\n' + actualJSON + '\n---------'); | ||
try { | ||
assert.deepEqual( | ||
actual, | ||
expectedObject); | ||
} catch(e) { | ||
// console.error('Unexpected output for "' + name + '":\nEXPECTED (' + expectedPath + '):\n---------\n' + expectedJSON + | ||
// '\n---------\nACTUAL (' + actualPath + '):\n---------\n' + actualJSON + '\n---------'); | ||
throw new Error('Unexpected output for "' + name + '"'); | ||
} | ||
} else { | ||
if (actual !== expected) { | ||
throw new Error('Unexpected output for "' + name + '"'); | ||
} | ||
} | ||
// assert.deepEqual( | ||
// actual, | ||
// expected, | ||
// 'Unexpected output for "' + name + '":\nEXPECTED (' + expectedPath + '):\n---------\n' + expectedJSON + | ||
// '\n---------\nACTUAL (' + actualPath + '):\n---------\n' + actualJSON + '\n---------'); | ||
} | ||
@@ -33,0 +49,0 @@ |
var chai = require('chai'); | ||
chai.config.includeStack = true; | ||
require('chai').should(); | ||
var expect = require('chai').expect; | ||
var path = require('path'); | ||
var fs = require('fs'); | ||
var htmljs = require('../'); | ||
var TreeBuilder = require('./TreeBuilder'); | ||
require('colors'); | ||
function parse(text, options, expectedEvents) { | ||
if (Array.isArray(options)) { | ||
expectedEvents = arguments[1]; | ||
options = undefined; | ||
function extend(target, source) { //A simple function to copy properties from one object to another | ||
if (!target) { //Check if a target was provided, otherwise create a new empty object to return | ||
target = {}; | ||
} | ||
var actualEvents = []; | ||
var listeners = { | ||
ontext: function(event) { | ||
actualEvents.push(event); | ||
}, | ||
oncontentplaceholder: function(event) { | ||
actualEvents.push(event); | ||
}, | ||
onnestedcontentplaceholder: function(event) { | ||
actualEvents.push(event); | ||
}, | ||
onattributeplaceholder: function(event) { | ||
// ignore this event because it is | ||
// emitted to give listeners a chance | ||
// to transform content | ||
}, | ||
oncdata: function(event) { | ||
actualEvents.push(event); | ||
}, | ||
onopentag: function(event) { | ||
actualEvents.push(event); | ||
}, | ||
onclosetag: function(event) { | ||
actualEvents.push(event); | ||
}, | ||
ondtd: function(event) { | ||
actualEvents.push(event); | ||
}, | ||
ondeclaration: function(event) { | ||
actualEvents.push(event); | ||
}, | ||
oncomment: function(event) { | ||
actualEvents.push(event); | ||
}, | ||
onerror: function(event) { | ||
actualEvents.push(event); | ||
} | ||
}; | ||
if (options) { | ||
for (var key in options) { | ||
if (options.hasOwnProperty(key)) { | ||
listeners[key] = options[key]; | ||
if (source) { | ||
for (var propName in source) { | ||
if (source.hasOwnProperty(propName)) { //Only look at source properties that are not inherited | ||
target[propName] = source[propName]; //Copy the property | ||
} | ||
@@ -76,1733 +25,62 @@ } | ||
var parser = htmljs.createNonValidatingParser(listeners); | ||
if (Array.isArray(text)) { | ||
text = text.join(''); | ||
} | ||
parser.parse(text); | ||
if (expectedEvents) { | ||
expect(actualEvents).to.deep.equal(expectedEvents); | ||
} | ||
return actualEvents; | ||
return target; | ||
} | ||
describe('htmljs parser', function() { | ||
function parse(text, options) { | ||
var treeBuilder = new TreeBuilder(text, options); | ||
it('should follow instructions on how to parse expression of tag', function() { | ||
var actualEvents = []; | ||
var opentagHandlers = { | ||
html: function(event) { | ||
this.enterHtmlContentState(); | ||
}, | ||
javascript: function(event) { | ||
this.enterJsContentState(); | ||
}, | ||
css: function(event) { | ||
this.enterCssContentState(); | ||
}, | ||
text: function(event) { | ||
this.enterStaticTextContentState(); | ||
}, | ||
parsedtext: function(event) { | ||
this.enterParsedTextContentState(); | ||
var parserOptions = { | ||
isOpenTagOnly: function(tagName) { | ||
if (tagName === 'foo-img') { | ||
return true; | ||
} | ||
}; | ||
} | ||
}; | ||
var parser = htmljs.createNonValidatingParser({ | ||
onopentag: function(event) { | ||
var tagName = event.tagName; | ||
actualEvents.push(event); | ||
var handler = opentagHandlers[tagName]; | ||
if (handler) { | ||
handler.call(this, event); | ||
} else { | ||
throw new Error('No opentag handler for tag ' + tagName); | ||
} | ||
}, | ||
extend(parserOptions, options); | ||
ontext: function(event) { | ||
actualEvents.push(event); | ||
}, | ||
var parser = htmljs.createParser(treeBuilder.listeners, parserOptions); | ||
oncontentplaceholder: function(event) { | ||
actualEvents.push(event); | ||
}, | ||
parser.parse(text); | ||
onnestedcontentplaceholder: function(event) { | ||
actualEvents.push(event); | ||
}, | ||
return treeBuilder.toString(); | ||
} | ||
onattributeplaceholder: function(event) { | ||
// ignore this one | ||
}, | ||
describe('parser', function() { | ||
oncdata: function(event) { | ||
actualEvents.push(event); | ||
}, | ||
require('./autotest').scanDir( | ||
path.join(__dirname, 'fixtures/autotest'), | ||
function (dir) { | ||
var inputPath = path.join(dir, 'input.htmljs'); | ||
var inputHtmlJs = fs.readFileSync(inputPath, {encoding: 'utf8'}); | ||
var testOptionsPath = path.join(dir, 'test.js'); | ||
var options; | ||
onclosetag: function(event) { | ||
actualEvents.push(event); | ||
}, | ||
ondtd: function(event) { | ||
actualEvents.push(event); | ||
}, | ||
ondeclaration: function(event) { | ||
actualEvents.push(event); | ||
}, | ||
oncomment: function(event) { | ||
actualEvents.push(event); | ||
if (fs.existsSync(testOptionsPath)) { | ||
options = require(testOptionsPath); | ||
} | ||
}); | ||
parser.parse([ | ||
'<html>', | ||
if (options && options.checkThrownError) { | ||
var error; | ||
// The <javascript> tag will be parsed in JavaScript mode | ||
'<javascript>/* This <javascript> is ignored */ // this is javascript <a></a></javascript>', | ||
// The <css> tag will be parsed in CSS mode | ||
'<css>/* CSS */\n.a {image: url("<a></a>")}</css>', | ||
// The <text> tag will be parsed as raw text | ||
'<text>This is raw ${text} so nothing should be parsed</text>', | ||
// The <parsedtext> tag will be parsed as raw text | ||
'<parsedtext>This is parsed ${text}!</parsedtext>', | ||
'</html>' | ||
]); | ||
expect(actualEvents).to.deep.equal([ | ||
{ | ||
type: 'opentag', | ||
tagName: 'html', | ||
attributes: [], | ||
pos: 0 | ||
}, | ||
{ | ||
type: 'opentag', | ||
tagName: 'javascript', | ||
attributes: [], | ||
pos: 6 | ||
}, | ||
{ | ||
type: 'text', | ||
text: '/* This <javascript> is ignored */ // this is javascript <a></a>' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'javascript' | ||
}, | ||
{ | ||
type: 'opentag', | ||
tagName: 'css', | ||
attributes: [], | ||
pos: 95 | ||
}, | ||
{ | ||
type: 'text', | ||
text: '/* CSS */\n.a {image: url("<a></a>")}' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'css' | ||
}, | ||
{ | ||
type: 'opentag', | ||
tagName: 'text', | ||
attributes: [], | ||
pos: 142 | ||
}, | ||
{ | ||
type: 'text', | ||
text: 'This is raw ${text} so nothing should be parsed' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'text' | ||
}, | ||
{ | ||
type: 'opentag', | ||
tagName: 'parsedtext', | ||
attributes: [], | ||
pos: 202 | ||
}, | ||
{ | ||
type: 'text', | ||
text: 'This is parsed ' | ||
}, | ||
{ | ||
type: 'contentplaceholder', | ||
expression: 'text', | ||
escape: true, | ||
pos: 229 | ||
}, | ||
{ | ||
type: 'text', | ||
text: '!' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'parsedtext' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'html' | ||
} | ||
]); | ||
}); | ||
describe('XML declarations', function() { | ||
it('should handle xml declaration <?xml version="1.0" encoding="UTF-8" ?>', function() { | ||
// <?xml version="1.0" encoding="UTF-8" ?> | ||
parse([ | ||
'<', '?', 'xml version="1.0" encoding="UTF-8" ?>' | ||
], [ | ||
{ | ||
type: 'declaration', | ||
declaration: 'xml version="1.0" encoding="UTF-8" ' | ||
try { | ||
parse(inputHtmlJs); | ||
} catch(e) { | ||
error = e; | ||
} | ||
]); | ||
}); | ||
it('should handle xml declaration <?xml version="1.0" encoding="UTF-8">', function() { | ||
parse([ | ||
'<', '?', 'xml version="1.0" encoding="UTF-8">' | ||
], [ | ||
{ | ||
type: 'declaration', | ||
declaration: 'xml version="1.0" encoding="UTF-8"' | ||
} | ||
]); | ||
}); | ||
}); | ||
if (!error) { | ||
throw new Error('Error expected!'); | ||
} else { | ||
describe('DTD', function() { | ||
it('should handle HTML doctype', function() { | ||
// <?xml version="1.0" encoding="UTF-8" ?> | ||
parse([ | ||
'<', '!', 'DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0//EN">' | ||
], [ | ||
{ | ||
type: 'dtd', | ||
dtd: 'DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0//EN"' | ||
} | ||
]); | ||
}); | ||
}); | ||
describe('Parsed text content', function() { | ||
it('should handle script tag', function() { | ||
var scriptInnerText = [ | ||
'// line comment within <script>\n', | ||
'/* block comment within <script> */', | ||
'"string within \\\"<script>\\\""', | ||
'\'string within \\\'<script>\\\'\'' | ||
].join(''); | ||
parse([ | ||
'<script>', | ||
scriptInnerText, | ||
'</script>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'script', | ||
attributes: [] | ||
}, | ||
{ | ||
type: 'text', | ||
text: scriptInnerText | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'script' | ||
} | ||
]); | ||
}); | ||
it('should handle closing script tag after single-line comment', function() { | ||
parse([ | ||
'<script>// this is a comment</script>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'script', | ||
attributes: [] | ||
}, | ||
{ | ||
type: 'text', | ||
text: '// this is a comment' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'script' | ||
} | ||
]); | ||
}); | ||
it('should handle style tag', function() { | ||
var styleInnerText = [ | ||
'/* block comment within <style> */', | ||
'"string within \\\"<style>\\\""', | ||
'\'string within \\\'<style>\\\'\'' | ||
].join(''); | ||
parse([ | ||
'<style>', | ||
styleInnerText, | ||
'</style>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'style', | ||
attributes: [] | ||
}, | ||
{ | ||
type: 'text', | ||
text: styleInnerText | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'style' | ||
} | ||
]); | ||
}); | ||
}); | ||
describe('Attribute parsing', function() { | ||
it('should handle parsing element with attribute that contains multi-line comment', function() { | ||
parse([ | ||
'<a a=123+456/* test */ b=a+\'123\'>test</a>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'a', | ||
attributes: [ | ||
{ | ||
name: 'a', | ||
expression: '123+456/* test */' | ||
}, | ||
{ | ||
name: 'b', | ||
expression: 'a+\'123\'' | ||
} | ||
] | ||
}, | ||
{ | ||
type: 'text', | ||
text: 'test' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'a' | ||
} | ||
]); | ||
}); | ||
it('should handle parsing element with complex attributes', function() { | ||
parse([ | ||
'<a a=123+256 b c= d=(a + (1/2) /* comment */)>test</a>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'a', | ||
attributes: [ | ||
{ | ||
name: 'a', | ||
expression: '123+256' | ||
}, | ||
{ | ||
name: 'b' | ||
}, | ||
{ | ||
name: 'c', | ||
expression: '', | ||
literalValue: '' | ||
}, | ||
{ | ||
name: 'd', | ||
expression: '(a + (1/2) /* comment */)' | ||
} | ||
] | ||
}, | ||
{ | ||
type: 'text', | ||
text: 'test' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'a' | ||
} | ||
]); | ||
}); | ||
it('should handle parsing element with attribute with no value', function() { | ||
parse([ | ||
'<a b>test</a>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'a', | ||
attributes: [ | ||
{ | ||
name: 'b' | ||
} | ||
] | ||
}, | ||
{ | ||
type: 'text', | ||
text: 'test' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'a' | ||
} | ||
]); | ||
}); | ||
it('should handle parsing attributes with simple expressions', function() { | ||
parse([ | ||
'<a a=1/2>test</a>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'a', | ||
attributes: [ | ||
{ | ||
name: 'a', | ||
expression: '1/2' | ||
} | ||
] | ||
}, | ||
{ | ||
type: 'text', | ||
text: 'test' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'a' | ||
} | ||
]); | ||
}); | ||
it('should handle parsing attributes with simple expressions that contain ">"', function() { | ||
parse([ | ||
'<a a=1>2>test</a>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'a', | ||
attributes: [ | ||
{ | ||
name: 'a', | ||
expression: '1', | ||
literalValue: 1 | ||
} | ||
] | ||
}, | ||
{ | ||
type: 'text', | ||
text: '2>test' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'a' | ||
} | ||
]); | ||
}); | ||
it('should handle parsing attributes with paranthese delimited expressions and double-quoted strings', function() { | ||
parse([ | ||
'<a data=((a-b)/2 + ")")></a>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'a', | ||
attributes: [ | ||
{ | ||
name: 'data', | ||
expression: '((a-b)/2 + ")")' | ||
} | ||
] | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'a' | ||
} | ||
]); | ||
}); | ||
it('should handle parsing attributes with expressions and single-quoted strings', function() { | ||
parse([ | ||
'<a data=((a-b)/2 + \')\')></a>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'a', | ||
attributes: [ | ||
{ | ||
name: 'data', | ||
expression: '((a-b)/2 + \')\')' | ||
} | ||
] | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'a' | ||
} | ||
]); | ||
}); | ||
it('should handle parsing attributes with object expressions', function() { | ||
parse([ | ||
'<a data={\n' + | ||
' "a": "{b}"\n', | ||
'}></a>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'a', | ||
attributes: [ | ||
{ | ||
name: 'data', | ||
expression: '{\n \"a\": \"{b}\"\n}' | ||
} | ||
] | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'a' | ||
} | ||
]); | ||
}); | ||
it('should handle parsing attributes without delimiters', function() { | ||
parse([ | ||
'<a data=123"abc"></a>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'a', | ||
attributes: [ | ||
{ | ||
name: 'data', | ||
expression: '123"abc"' | ||
} | ||
] | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'a' | ||
} | ||
]); | ||
parse([ | ||
'<a data=123 data=abc></a>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'a', | ||
attributes: [ | ||
{ | ||
name: 'data', | ||
expression: '123', | ||
literalValue: 123 | ||
}, | ||
{ | ||
name: 'data', | ||
expression: 'abc' | ||
} | ||
] | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'a' | ||
} | ||
]); | ||
}); | ||
it('should handle multi-line string attributes', function() { | ||
parse([ | ||
'<div data="\nabc\n124">' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'div', | ||
attributes: [ | ||
{ | ||
name: 'data', | ||
expression: '"\\nabc\\n124"', | ||
literalValue: '\nabc\n124' | ||
} | ||
] | ||
} | ||
]); | ||
}); | ||
describe('Attribute Literal Values', function() { | ||
it('should recognize true literal', function() { | ||
parse([ | ||
'<div data=true>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'div', | ||
attributes: [ | ||
{ | ||
name: 'data', | ||
expression: 'true', | ||
literalValue: true | ||
} | ||
] | ||
} | ||
]); | ||
}); | ||
it('should recognize false literal', function() { | ||
parse([ | ||
'<div data=false>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'div', | ||
attributes: [ | ||
{ | ||
name: 'data', | ||
expression: 'false', | ||
literalValue: false | ||
} | ||
] | ||
} | ||
]); | ||
}); | ||
it('should recognize undefined literal', function() { | ||
parse([ | ||
'<div data=undefined>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'div', | ||
attributes: [ | ||
{ | ||
name: 'data', | ||
expression: 'undefined', | ||
literalValue: undefined | ||
} | ||
] | ||
} | ||
]); | ||
}); | ||
it('should recognize null literal', function() { | ||
parse([ | ||
'<div data=null>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'div', | ||
attributes: [ | ||
{ | ||
name: 'data', | ||
expression: 'null', | ||
literalValue: null | ||
} | ||
] | ||
} | ||
]); | ||
}); | ||
it('should recognize number literal', function() { | ||
parse([ | ||
'<div data=1 data=.5 data=1.5 data=1.5e10 data=1.5e+10 data=1.5e-10 data=-1 data=-.5 data=-1.5 data=-1.5e10 data=-1.5e+10 data=-1.5e-10>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'div', | ||
attributes: [ | ||
{ | ||
name: 'data', | ||
expression: '1', | ||
literalValue: 1 | ||
}, | ||
{ | ||
name: 'data', | ||
expression: '.5', | ||
literalValue: 0.5 | ||
}, | ||
{ | ||
name: 'data', | ||
expression: '1.5', | ||
literalValue: 1.5 | ||
}, | ||
{ | ||
name: 'data', | ||
expression: '1.5e10', | ||
literalValue: 1.5e10 | ||
}, | ||
{ | ||
name: 'data', | ||
expression: '1.5e+10', | ||
literalValue: 1.5e+10 | ||
}, | ||
{ | ||
name: 'data', | ||
expression: '1.5e-10', | ||
literalValue: 1.5e-10 | ||
}, | ||
{ | ||
name: 'data', | ||
expression: '-1', | ||
literalValue: -1 | ||
}, | ||
{ | ||
name: 'data', | ||
expression: '-.5', | ||
literalValue: -0.5 | ||
}, | ||
{ | ||
name: 'data', | ||
expression: '-1.5', | ||
literalValue: -1.5 | ||
}, | ||
{ | ||
name: 'data', | ||
expression: '-1.5e10', | ||
literalValue: -1.5e10 | ||
}, | ||
{ | ||
name: 'data', | ||
expression: '-1.5e+10', | ||
literalValue: -1.5e+10 | ||
}, | ||
{ | ||
name: 'data', | ||
expression: '-1.5e-10', | ||
literalValue: -1.5e-10 | ||
} | ||
] | ||
} | ||
]); | ||
}); | ||
}); | ||
}); | ||
describe('CDATA', function() { | ||
it('should handle CDATA', function() { | ||
parse([ | ||
'BEFORE<![CDATA[<within><!-- just text -->]]>AFTER' | ||
], [ | ||
{ | ||
type: 'text', | ||
text: 'BEFORE' | ||
}, | ||
{ | ||
type: 'cdata', | ||
text: '<within><!-- just text -->' | ||
}, | ||
{ | ||
type: 'text', | ||
text: 'AFTER' | ||
} | ||
]); | ||
}); | ||
}); | ||
describe('Stray special characters', function() { | ||
it('should handle stray "<" and ">"', function() { | ||
parse([ | ||
'<a>1 < > <> </> 2<</a>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'a', | ||
attributes: [] | ||
}, | ||
{ | ||
type: 'text', | ||
text: '1 ' | ||
}, | ||
{ | ||
type: 'text', | ||
text: '< ' | ||
}, | ||
{ | ||
type: 'text', | ||
text: '> ' | ||
}, | ||
{ | ||
type: 'text', | ||
text: '<>' | ||
}, | ||
{ | ||
type: 'text', | ||
text: ' ' | ||
}, | ||
{ | ||
type: 'text', | ||
text: '</>' | ||
}, | ||
{ | ||
type: 'text', | ||
text: ' 2' | ||
}, | ||
{ | ||
type: 'text', | ||
text: '<' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'a' | ||
} | ||
]); | ||
}); | ||
it('should handle parsing element with stray /', function() { | ||
parse([ | ||
'<a / >test</a>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'a', | ||
attributes: [] | ||
}, | ||
{ | ||
type: 'text', | ||
text: 'test' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'a' | ||
} | ||
]); | ||
}); | ||
}); | ||
describe('XML comments', function() { | ||
it('should handle XML comments', function() { | ||
parse([ | ||
'<a><!--<b></b>--></a>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'a', | ||
attributes: [] | ||
}, | ||
{ | ||
type: 'comment', | ||
comment: '<b></b>' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'a' | ||
} | ||
]); | ||
}); | ||
}); | ||
it('should handle self-closing tags', function() { | ||
parse([ | ||
'<a />' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
tagName: 'a', | ||
attributes: [], | ||
selfClosed: true, | ||
pos: 0 | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'a', | ||
selfClosed: true | ||
} else { | ||
return parse(inputHtmlJs, options); | ||
} | ||
]); | ||
}); | ||
describe('Placeholders', function() { | ||
it('should handle placeholder expressions in normal text with surrounding curly braces', function() { | ||
parse([ | ||
'Hello ${xyz}!' | ||
], [ | ||
{ | ||
type: 'text', | ||
text: 'Hello ' | ||
}, | ||
{ | ||
type: 'contentplaceholder', | ||
expression: 'xyz', | ||
escape: true, | ||
pos: 6 | ||
}, | ||
{ | ||
type: 'text', | ||
text: '!' | ||
} | ||
]); | ||
}); | ||
it('should handle placeholder expressions in scripts with surrounding curly braces', function() { | ||
parse([ | ||
'<script>Hello ${xyz}!</script>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'script', | ||
attributes: [] | ||
}, | ||
{ | ||
type: 'text', | ||
text: 'Hello ' | ||
}, | ||
{ | ||
type: 'contentplaceholder', | ||
expression: 'xyz', | ||
escape: true, | ||
pos: 14 | ||
}, | ||
{ | ||
type: 'text', | ||
text: '!' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'script' | ||
} | ||
]); | ||
}); | ||
it('should handle placeholder expressions in strings in scripts with surrounding curly braces', function() { | ||
parse([ | ||
'<script>alert("Hello ${xyz}!")</script>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'script', | ||
attributes: [] | ||
}, | ||
{ | ||
type: 'text', | ||
text: 'alert("Hello ' | ||
}, | ||
{ | ||
type: 'contentplaceholder', | ||
expression: 'xyz', | ||
escape: true, | ||
pos: 21 | ||
}, | ||
{ | ||
type: 'text', | ||
text: '!")' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'script' | ||
} | ||
]); | ||
}); | ||
it('should handle placeholder expressions within non-delimited attributes', function() { | ||
parse([ | ||
'<custom name="Hello ${name}!">TEST</custom>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'custom', | ||
attributes: [ | ||
{ | ||
name: 'name', | ||
expression: '("Hello "+(name)+"!")' | ||
} | ||
] | ||
}, | ||
{ | ||
type: 'text', | ||
text: 'TEST' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'custom' | ||
} | ||
]); | ||
}); | ||
it('should handle placeholder expressions within delimited expression attributes', function() { | ||
parse([ | ||
'<custom name=("Hello ${name}!")>TEST</custom>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'custom', | ||
attributes: [ | ||
{ | ||
name: 'name', | ||
expression: '(("Hello "+(name)+"!"))' | ||
} | ||
] | ||
}, | ||
{ | ||
type: 'text', | ||
text: 'TEST' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'custom' | ||
} | ||
]); | ||
}); | ||
it('should handle placeholder expressions within string within delimited expression attributes', function() { | ||
parse([ | ||
'<custom name="${\'some text\'}">TEST</custom>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'custom', | ||
attributes: [ | ||
{ | ||
name: 'name', | ||
expression: '(\'some text\')', | ||
} | ||
] | ||
}, | ||
{ | ||
type: 'text', | ||
text: 'TEST' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'custom' | ||
} | ||
]); | ||
}); | ||
it('should ignore placeholders in XML comments', function() { | ||
parse([ | ||
'<!-- Copyright ${date} -->' | ||
], [ | ||
{ | ||
type: 'comment', | ||
comment: ' Copyright ${date} ' | ||
} | ||
]); | ||
}); | ||
it('should handle placeholders in JavaScript single-line comments', function() { | ||
parse([ | ||
'<script>// Copyright ${date}\n</script>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'script', | ||
attributes: [] | ||
}, | ||
{ | ||
type: 'text', | ||
text: '// Copyright ' | ||
}, | ||
{ | ||
type: 'contentplaceholder', | ||
expression: 'date', | ||
escape: true, | ||
pos: 21 | ||
}, | ||
{ | ||
type: 'text', | ||
text: '\n' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'script' | ||
} | ||
]); | ||
}); | ||
it('should handle placeholders in JavaScript multi-line comments', function() { | ||
parse([ | ||
'<script>/* Copyright $!{date} */</script>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'script', | ||
attributes: [] | ||
}, | ||
{ | ||
type: 'text', | ||
text: '/* Copyright ' | ||
}, | ||
{ | ||
type: 'contentplaceholder', | ||
expression: 'date', | ||
escape: false, | ||
pos: 21 | ||
}, | ||
{ | ||
type: 'text', | ||
text: ' */' | ||
}, | ||
{ | ||
type: 'closetag', | ||
tagName: 'script' | ||
} | ||
]); | ||
}); | ||
it('should handle placeholders in string attributes', function() { | ||
parse([ | ||
'<custom data="${\nabc\n}">' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'custom', | ||
attributes: [ | ||
{ | ||
name: 'data', | ||
expression: '(\nabc\n)' | ||
} | ||
] | ||
} | ||
]); | ||
}); | ||
it('should handle placeholders in complex attribute', function() { | ||
parse([ | ||
'<custom data=("Hello $!{name}!" + " This is a test.")>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'custom', | ||
attributes: [ | ||
{ | ||
name: 'data', | ||
expression: '(("Hello "+(name)+"!") + " This is a test.")' | ||
} | ||
] | ||
} | ||
]); | ||
}); | ||
it('should handle simple placeholders in string attributes', function() { | ||
parse([ | ||
'<custom data="${abc}">' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'custom', | ||
attributes: [ | ||
{ | ||
name: 'data', | ||
expression: '(abc)' | ||
} | ||
] | ||
} | ||
]); | ||
}); | ||
it('should handle placeholder inside attribute placeholder', function() { | ||
parse([ | ||
'<custom data="${"Hello ${data.firstName + data.lastName}"}">' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'custom', | ||
attributes: [ | ||
{ | ||
name: 'data', | ||
expression: '(("Hello "+(data.firstName + data.lastName)))' | ||
} | ||
] | ||
} | ||
]); | ||
}); | ||
it('should handle placeholder inside content placeholder', function() { | ||
parse([ | ||
'${"Hello ${data.name}!"}' | ||
], [ | ||
{ | ||
type: 'nestedcontentplaceholder', | ||
expression: 'data.name', | ||
escape: true, | ||
pos: 9 | ||
}, | ||
{ | ||
type: 'contentplaceholder', | ||
expression: '("Hello "+(data.name)+"!")', | ||
escape: true, | ||
pos: 0 | ||
} | ||
]); | ||
}); | ||
it('should handle placeholder inside content placeholder and escaping', function() { | ||
parse([ | ||
'$!{"Hello ${data.name}!"}' | ||
], { | ||
onnestedcontentplaceholder: function(event) { | ||
if (event.escape) { | ||
event.expression = 'escapeXml(' + event.expression + ')'; | ||
} | ||
} | ||
}, [ | ||
{ | ||
type: 'contentplaceholder', | ||
expression: '("Hello "+(escapeXml(data.name))+"!")', | ||
escape: false, | ||
pos: 0 | ||
} | ||
]); | ||
}); | ||
it('should allow attribute placeholder expression to be escaped', function() { | ||
parse([ | ||
'<custom data="${abc}">' | ||
], { | ||
onattributeplaceholder: function(event) { | ||
if (event.escape) { | ||
event.expression = 'escapeAttr(' + event.expression + ')'; | ||
} | ||
} | ||
}, [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'custom', | ||
attributes: [ | ||
{ | ||
name: 'data', | ||
expression: '(escapeAttr(abc))' | ||
} | ||
] | ||
} | ||
]); | ||
}); | ||
}, | ||
{ | ||
ext: '.html' | ||
}); | ||
describe('Static text attributes', function() { | ||
it('should recognize static text attributes', function() { | ||
parse([ | ||
'<div class="simple">' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'div', | ||
attributes: [ | ||
{ | ||
name: 'class', | ||
expression: '"simple"', | ||
literalValue: 'simple' | ||
} | ||
] | ||
} | ||
]); | ||
}); | ||
}); | ||
describe('Element and element arguments', function() { | ||
it('should recognize arguments to element with whitespace after tag name', function() { | ||
parse([ | ||
'<for (x in y)>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'for', | ||
argument: 'x in y', | ||
attributes: [] | ||
} | ||
]); | ||
}); | ||
it('should recognize arguments to element without whitespace after tag name', function() { | ||
parse([ | ||
'<for(x in y)>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'for', | ||
argument: 'x in y', | ||
attributes: [] | ||
} | ||
]); | ||
}); | ||
it('should recognize arguments to element that also contain strings with placeholders', function() { | ||
parse([ | ||
'<for (x in ["Hello ${name}!", "(World)"])>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'for', | ||
argument: 'x in [("Hello "+(name)+"!"), "(World)"]', | ||
attributes: [] | ||
} | ||
]); | ||
}); | ||
it('should recognize arguments for attributes with whitespace', function() { | ||
parse([ | ||
'<div if (x > y)>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'div', | ||
attributes: [ | ||
{ | ||
name: 'if', | ||
argument: 'x > y' | ||
} | ||
] | ||
} | ||
]); | ||
}); | ||
it('should recognize arguments for attributes without whitespace', function() { | ||
parse([ | ||
'<div if(x > y)>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'div', | ||
attributes: [ | ||
{ | ||
name: 'if', | ||
argument: 'x > y' | ||
} | ||
] | ||
} | ||
]); | ||
}); | ||
it('should recognize arguments for both element and attributes', function() { | ||
parse([ | ||
'<for(var i = 0; i < 10; i++) if(x > y)>' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'for', | ||
argument: 'var i = 0; i < 10; i++', | ||
attributes: [ | ||
{ | ||
name: 'if', | ||
argument: 'x > y' | ||
} | ||
] | ||
} | ||
]); | ||
}); | ||
it('should allow only one argument per tag', function() { | ||
parse([ | ||
'<for(var i = 0; i < 10; i++) (nonsense!)>' | ||
], [ | ||
{ | ||
code: 'ILLEGAL_ELEMENT_ARGUMENT', | ||
endPos: 29, | ||
message: 'Element can only have one argument.', | ||
pos: 0, | ||
type: 'error' | ||
}, | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'for', | ||
argument: 'var i = 0; i < 10; i++', | ||
attributes: [] | ||
} | ||
]); | ||
}); | ||
it('should allow only one argument per attribute', function() { | ||
parse([ | ||
'<div for(var i = 0; i < 10; i++) (nonsense!)>' | ||
], [ | ||
{ | ||
code: 'ILLEGAL_ATTRIBUTE_ARGUMENT', | ||
message: 'Attribute can only have one argument.', | ||
pos: 0, | ||
endPos: 33, | ||
type: 'error' | ||
}, | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'div', | ||
attributes: [ | ||
{ | ||
name: 'for', | ||
argument: 'var i = 0; i < 10; i++' | ||
} | ||
] | ||
} | ||
]); | ||
}); | ||
}); | ||
describe('EOF handling', function() { | ||
it('should handle EOF while parsing element', function() { | ||
parse([ | ||
'<a><b' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'a', | ||
attributes: [] | ||
}, | ||
{ | ||
type: 'error', | ||
code: 'MALFORMED_OPEN_TAG', | ||
message: 'EOF reached while parsing open tag.', | ||
pos:3, | ||
endPos: 5 | ||
} | ||
]); | ||
parse([ | ||
'<a><b selected' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'a', | ||
attributes: [] | ||
}, | ||
{ | ||
type: 'error', | ||
code: 'MALFORMED_OPEN_TAG', | ||
message: 'EOF reached while parsing open tag.', | ||
pos:3, | ||
endPos: 14 | ||
} | ||
]); | ||
parse([ | ||
'<a><b selected something= test=123' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'a', | ||
attributes: [] | ||
}, | ||
{ | ||
type: 'error', | ||
code: 'MALFORMED_OPEN_TAG', | ||
message: 'EOF reached while parsing open tag.', | ||
pos:3, | ||
endPos: 34 | ||
} | ||
]); | ||
parse([ | ||
'<a><b selected something= test=/*' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'a', | ||
attributes: [] | ||
}, | ||
{ | ||
type: 'error', | ||
code: 'MALFORMED_OPEN_TAG', | ||
message: 'EOF reached while parsing open tag.', | ||
pos:3, | ||
endPos: 33 | ||
} | ||
]); | ||
parse([ | ||
'<a href="' | ||
], [ | ||
{ | ||
type: 'error', | ||
code: 'MALFORMED_OPEN_TAG', | ||
message: 'EOF reached while parsing open tag.', | ||
pos: 0, | ||
endPos: 9 | ||
} | ||
]); | ||
}); | ||
it('should handle EOF while parsing script tag', function() { | ||
var scriptInnerText = [ | ||
'// line comment within <script>\n', | ||
'/* block comment within <script> */', | ||
'"string within \\\"<script>\\\""', | ||
'\'string within \\\'<script>\\\'\'' | ||
].join(''); | ||
parse([ | ||
'<script a=b>', | ||
scriptInnerText | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'script', | ||
attributes: [ | ||
{ | ||
name: 'a', | ||
expression: 'b' | ||
} | ||
] | ||
}, | ||
{ | ||
type: 'text', | ||
text: scriptInnerText | ||
} | ||
]); | ||
}); | ||
it('should handle EOF while parsing HTML doctype', function() { | ||
parse([ | ||
'<', '!', 'DOCTYPE html PUBLIC' | ||
], [ | ||
{ | ||
type: 'error', | ||
code: 'MALFORMED_DTD', | ||
message: 'EOF reached while parsing DTD.', | ||
pos: 0, | ||
endPos: 21 | ||
} | ||
]); | ||
}); | ||
it('should handle EOF while parsing xml declaration', function() { | ||
parse([ | ||
'<', '?', 'xml version="1.0"' | ||
], [ | ||
{ | ||
type: 'error', | ||
code: 'MALFORMED_DECLARATION', | ||
message: 'EOF reached while parsing declaration.', | ||
pos: 0, | ||
endPos: 19 | ||
} | ||
]); | ||
}); | ||
it('should handle EOF while parsing style tag', function() { | ||
var styleInnerText = [ | ||
'/* block comment within <style> */', | ||
'"string within \\\"<style>\\\""', | ||
'\'string within \\\'<style>\\\'\'' | ||
].join(''); | ||
parse([ | ||
'<style a=b>', | ||
styleInnerText | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'style', | ||
attributes: [ | ||
{ | ||
name: 'a', | ||
expression: 'b' | ||
} | ||
] | ||
}, | ||
{ | ||
type: 'text', | ||
text: styleInnerText | ||
} | ||
]); | ||
}); | ||
it('should handle EOF while parsing non-escaped content placeholder', function() { | ||
parse([ | ||
'Hello\n$!{abc' | ||
], [ | ||
{ | ||
type: 'text', | ||
text: 'Hello\n' | ||
}, | ||
{ | ||
type: 'error', | ||
code: 'MALFORMED_PLACEHOLDER', | ||
message: 'EOF reached while parsing placeholder.', | ||
pos:6, | ||
endPos: 12 | ||
} | ||
]); | ||
}); | ||
it('should handle EOF while parsing escaped content placeholder', function() { | ||
parse([ | ||
'Hello ${abc' | ||
], [ | ||
{ | ||
type: 'text', | ||
text: 'Hello ' | ||
}, | ||
{ | ||
type: 'error', | ||
code: 'MALFORMED_PLACEHOLDER', | ||
message: 'EOF reached while parsing placeholder.', | ||
pos:6, | ||
endPos: 11 | ||
} | ||
]); | ||
}); | ||
it('should handle EOF while parsing non-escaped <script> content placeholder', function() { | ||
parse([ | ||
'<script>Hello $!{abc' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'script', | ||
attributes: [] | ||
}, | ||
{ | ||
type: 'text', | ||
text: 'Hello ' | ||
}, | ||
{ | ||
type: 'error', | ||
code: 'MALFORMED_PLACEHOLDER', | ||
message: 'EOF reached while parsing placeholder.', | ||
pos:14, | ||
endPos: 20 | ||
} | ||
]); | ||
}); | ||
it('should handle EOF while parsing escaped <script> content placeholder', function() { | ||
parse([ | ||
'<script>Hello ${abc' | ||
], [ | ||
{ | ||
type: 'opentag', | ||
pos: 0, | ||
tagName: 'script', | ||
attributes: [] | ||
}, | ||
{ | ||
type: 'text', | ||
text: 'Hello ' | ||
}, | ||
{ | ||
type: 'error', | ||
code: 'MALFORMED_PLACEHOLDER', | ||
message: 'EOF reached while parsing placeholder.', | ||
pos:14, | ||
endPos: 19 | ||
} | ||
]); | ||
}); | ||
it('should handle EOF while parsing delimited expression inside placeholder', function() { | ||
parse([ | ||
'Hello ${(' | ||
], [ | ||
{ | ||
type: 'text', | ||
text: 'Hello ' | ||
}, | ||
{ | ||
type: 'error', | ||
code: 'MALFORMED_PLACEHOLDER', | ||
message: 'EOF reached while parsing placeholder.', | ||
pos: 6, | ||
endPos: 9 | ||
} | ||
]); | ||
}); | ||
it('should handle EOF while parsing attributes with arguments', function() { | ||
parse([ | ||
'<div if(a==b' | ||
], [ | ||
{ | ||
type: 'error', | ||
code: 'MALFORMED_OPEN_TAG', | ||
message: 'EOF reached while parsing open tag.', | ||
pos:0, | ||
endPos: 12 | ||
} | ||
]); | ||
}); | ||
it('should handle an attribute with an invalid placeholder', function() { | ||
parse([ | ||
'<foo invalid="${;"></foo>' | ||
], [ | ||
{ | ||
type: 'error', | ||
code: 'MALFORMED_PLACEHOLDER', | ||
message: 'EOF reached while parsing placeholder.', | ||
pos: 14, | ||
endPos: 25 | ||
} | ||
]); | ||
}); | ||
}); | ||
require('./autotest').scanDir( | ||
path.join(__dirname, 'fixtures/autotest'), | ||
function (dir) { | ||
var inputPath = path.join(dir, 'input.htmljs'); | ||
var inputHtmlJs = fs.readFileSync(inputPath, {encoding: 'utf8'}); | ||
var parserEvents = parse(inputHtmlJs); | ||
return parserEvents; | ||
}); | ||
}); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
300
146340
2692
586
4
1