Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

htmljs-parser

Package Overview
Dependencies
Maintainers
2
Versions
109
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

htmljs-parser - npm Package Compare versions

Comparing version 1.2.1 to 1.3.0

test/fixtures/autotest/argument-attr-extra-whitespace/expected.html

67

BaseParser.js
'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 @@ }

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);
};
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"
}

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc