diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13203c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ + +node_modules/ + +*.bak +*~ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a96b06a --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +test: + ./node_modules/.bin/mocha --reporter spec + +.PHONY: test diff --git a/README.md b/README.md index 8499aa4..8033897 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,105 @@ -# Pure JavaScript HTML5 Parser # +# Neutron HTML5 Parser # - -A working demo can be seen [here](http://htmlpreview.github.io/?https://github.com/blowsie/Pure-JavaScript-HTML-Parser/blob/master/demo.html). +Here is a small pure-JavaScript HTML5 parser that can run on browsers as well as NodeJS with [jsdom](https://github.com/tmpvar/jsdom). _Credit goes to John Resig for his [code](http://ejohn.org/blog/pure-javascript-html-parser/) written back in 2008 and Erik Arvidsson for his [code](http://erik.eae.net/simplehtmlparser/simplehtmlparser.js) written prior to that._ This code has been updated to work with HTML 5 to fix several problems. +## Use case +For parsing templates on both client and server side. +This library may soon be used internally in [htmlizer](https://github.com/Munawwar/htmlizer). +For only server-side use case, you may like to use [htmlparser2](https://github.com/fb55/htmlparser2) or [high5](https://github.com/fb55/high5). Note: DOCTYPE gets ignored by htmlparser2. -## 4 Libraries in One! ## - -### A SAX-style API ### - -Handles tag, text, and comments with callbacks. For example, let’s say you wanted to implement a simple HTML to XML serialization scheme – you could do so using the following: +For only client-side use case, you can look into jQuery.parseHTML() or native DOMParser (IE10+). - var results = ""; - - HTMLParser("

hello world", { - start: function( tag, attrs, unary ) { - results += "<" + tag; - - for ( var i = 0; i < attrs.length; i++ ) - results += " " + attrs[i].name + '="' + attrs[i].escaped + '"'; - - results += ">"; - }, - end: function( tag ) { - results += ""; - }, - chars: function( text ) { - results += text; - }, - comment: function( text ) { - results += ""; - } - }); - - results == '

hello world

" +## Usage -### XML Serializer ### +Add htmlparser.js to head tag or require with nodejs. -Now, there’s no need to worry about implementing the above, since it’s included directly in the library, as well. Just feed in HTML and it spits back an XML string. +### DOM Builder ### - var results = HTMLtoXML("

Data: ") - results == '

Data:

' + //Returns DocumentFragment. + var documentfragment = HTMLtoDOM("

Hello World"); + //If doctype is given then returns HTMLDocument + var doc = HTMLtoDOM("test"); -### DOM Builder ### + //on NodeJS + var factory = require('neutron-html5parser'), + jsdom = require('jsdom'), + HTMLtoDOM = factory(jsdom.jsdom('').parentWindow); -If you’re using the HTML parser to inject into an existing DOM document (or within an existing DOM element) then htmlparser.js provides a simple method for handling that: +While this library doesn’t cover the full gamut of possible weirdness that HTML provides, it does handle a lot of the most obvious stuff. All of the following are accounted for: - // The following is appended into the document body - HTMLtoDOM("

Hello World", document) - - // The follow is appended into the specified element - HTMLtoDOM("

Hello World", document.getElementById("test")) +**Unclosed Tags:** + HTMLtoDOM("

Hello") == '

Hello

' +**Empty Elements:** -### DOM Document Creator ### + HTMLtoDOM("") == '' -This is a more-advanced version of the DOM builder – it includes logic for handling the overall structure of a web page, returning a new DOM document. +**Block vs. Inline Elements:** -A couple points are enforced by this method: + HTMLtoDOM("Hello

John") == 'Hello

John

' +**Self-closing Elements:** - - There will always be a html, head, body, and title element. - - There will only be one html, head, body, and title element (if the user specifies more, then will be moved to the appropriate locations and merged). -link and base elements are forced into the head. + HTMLtoDOM("

Hello

World") == '

Hello

World

' +**Attributes Without Values:** -You would use the method like so: + HTMLtoDOM("") == '' - var dom = HTMLtoDOM("

Data: "); - dom.getElementsByTagName("body").length == 1 - dom.getElementsByTagName("p").length == 1 +Following should be supported again in future: +~~A couple points are enforced by this method:~~ +~~- There will always be a html, head, body, and title element.~~ +~~- There will only be one html, head, body, and title element (if the user specifies more, then will be moved to the appropriate locations and merged).~~ +~~link and base elements are forced into the head.~~ -While this library doesn’t cover the full gamut of possible weirdness that HTML provides, it does handle a lot of the most obvious stuff. All of the following are accounted for: +### Advanced: SAX-style API ### -**Unclosed Tags:** +Handles tag, text, and comments with callbacks. For example, let’s say you wanted to implement a simple HTML to XML serialization scheme – you could do so using the following: - HTMLtoXML("

Hello") == '

Hello

' -**Empty Elements:** + var results = ""; - HTMLtoXML("") == '' + HTMLtoDOM.Parser("

hello world", { + start: function( tag, attrs, unary ) { + results += "<" + tag; -**Block vs. Inline Elements:** + for ( var i = 0; i < attrs.length; i++ ) + results += " " + attrs[i].name + '="' + attrs[i].escaped + '"'; - HTMLtoXML("Hello

John") == 'Hello

John

' -**Self-closing Elements:** + results += ">"; + }, + end: function( tag ) { + results += ""; + }, + chars: function( text ) { + results += text; + }, + comment: function( text ) { + results += ""; + } + }); - HTMLtoXML("

Hello

World") == '

Hello

World

' -**Attributes Without Values:** + results == '

hello world

" - HTMLtoXML("") == '' +### Benchmarking + +Benchmark done using [htmlparser-benchmark](https://github.com/AndreasMadsen/htmlparser-benchmark). +``` +htmlparser2 : 3.77256 ms/file ± 2.29339 +high5 : 4.96011 ms/file ± 2.71494 +neutron-html5parser : 5.41695 ms/file ± 3.26307 +htmlparser2-dom : 6.43134 ms/file ± 3.63845 +libxmljs : 7.37534 ms/file ± 9.60274 +parse5 : 12.2405 ms/file ± 7.82065 +html-parser : 12.6268 ms/file ± 8.30923 +hubbub : 15.0666 ms/file ± 7.80456 +htmlparser : 28.7801 ms/file ± 178.500 +gumbo-parser : 29.8096 ms/file ± 15.5291 +html5 : 196.083 ms/file ± 248.159 +sax : +``` diff --git a/benchmark.js b/benchmark.js new file mode 100644 index 0000000..954ffdc --- /dev/null +++ b/benchmark.js @@ -0,0 +1,23 @@ +var benchmark = require('htmlparser-benchmark'); +var HTMLtoDOM = require("./htmlparser.js")(); + +var bench = benchmark(function (html, callback) { + var noop = function () {}; + HTMLtoDOM.Parser(html, { + start: noop, + end: noop, + chars: noop, + comment: noop, + doctype: noop + }); + + callback(null, 'Great'); +}); + +bench.on('progress', function (key) { + console.log('finished parsing ' + key + '.html'); +}); + +bench.on('result', function (stat) { + console.log(stat.mean().toPrecision(6) + ' ms/file ± ' + stat.sd().toPrecision(6)); +}); diff --git a/htmlparser.js b/htmlparser.js index 625eee2..90e8e56 100644 --- a/htmlparser.js +++ b/htmlparser.js @@ -1,8 +1,9 @@ /* - * HTML5 Parser By Sam Blowes + * HTML5 Parser * * Designed for HTML5 documents * + * Original Code from HTML5 Parser By Sam Blowes (https://github.com/blowsie/Pure-JavaScript-HTML5-Parser) * Original code by John Resig (ejohn.org) * http://ejohn.org/blog/pure-javascript-html-parser/ * Original code by Erik Arvidsson, Mozilla Public License @@ -59,66 +60,90 @@ * Usage * ---------------------------------------------------------------------------- * - * // Use like so: - * HTMLParser(htmlString, { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * }); - * - * // or to get an XML string: - * HTMLtoXML(htmlString); - * - * // or to get an XML DOM Document + * // To get a DocumentFragment. If doctype is defined then it returns a Document. * HTMLtoDOM(htmlString); - * - * // or to inject into an existing document/DOM node - * HTMLtoDOM(htmlString, document); - * HTMLtoDOM(htmlString, document.body); - * */ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define(factory.bind(this)); + } else if (typeof exports === 'object') { //nodejs + module.exports = factory; //Need to pass jsdom window to initialize + } else { + root.HTMLtoDOM = factory(); + } +}(this, function (window) { + //browser and jsdom compatibility + window = window || this; + var document = window.document; + + var HTMLParser = (function () { + // Regular Expressions for parsing tags and attributes + var startTag = /^<([-\w:]+)((?:\s+[^\s\/>"'=]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, + endTag = /^<\/([-\w:]+)[^>]*>/, + cdataTag = /^/i, + attr = /^\s+([^\s\/>"'=]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/; + + // Empty Elements - HTML 5 + var empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr"), + + // Block Elements - HTML 5 + block = makeMap("address,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video"), + + // Inline Elements - HTML 5 + inline = makeMap("a,abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var"), + + // Elements that you can, intentionally, leave open + // (and which close themselves) + closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr"), + + // Special Elements (can contain anything) + special = { + script: /^([\s\S]*?)<\/script[^>]*>/i, + style: /^([\s\S]*?)<\/style[^>]*>/i + }; + + /** + * This class parses an HTML/XML document. + * + * @param {String} html HTML/XML document + * @param {Object} handler Plain object + * + * @return {HTMLParser} + */ + return function Parser(html, handler) { + //remove trailing spaces + html = html.trim(); + + var index, chars, match, stack = [], last = html, lastTag; + + var specialReplacer = function (all, text) { + if (handler.chars) + handler.chars(text); + return ""; + }; + + while (html) { + chars = true; + + //Handle script and style tags + if (special[lastTag]) { + html = html.replace(special[lastTag], specialReplacer); + chars = false; + + parseEndTag("", lastTag); -(function () { - - // Regular Expressions for parsing tags and attributes - var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, - endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, - attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; - - // Empty Elements - HTML 5 - var empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr"); - - // Block Elements - HTML 5 - var block = makeMap("a,address,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video"); - - // Inline Elements - HTML 5 - var inline = makeMap("abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var"); - - // Elements that you can, intentionally, leave open - // (and which close themselves) - var closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr"); - - // Attributes that have their values filled in disabled="disabled" - var fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected"); - - // Special Elements (can contain anything) - var special = makeMap("script,style"); - - var HTMLParser = this.HTMLParser = function (html, handler) { - var index, chars, match, stack = [], last = html; - stack.last = function () { - return this[this.length - 1]; - }; - - while (html) { - chars = true; + // end tag + } else if (html.substring(0, 2) === ""); if (index >= 0) { @@ -128,23 +153,40 @@ chars = false; } - // end tag - } else if (html.indexOf(""); + + if (index >= 0) { + if (handler.doctype) + handler.doctype(html.substring(0, index)); + html = html.substring(index + 1); + chars = false; + } // start tag - } else if (html.indexOf("<") == 0) { + } else if (html[0] === "<") { match = html.match(startTag); if (match) { html = html.substring(match[0].length); - match[0].replace(startTag, parseStartTag); + parseStartTag.apply(this, match); + chars = false; + } else { //ignore the angle bracket + html = html.substring(1); + if (handler.chars) { + handler.chars('<'); + } chars = false; } } @@ -155,182 +197,116 @@ var text = index < 0 ? html : html.substring(0, index); html = index < 0 ? "" : html.substring(index); - if (handler.chars) + if (handler.chars) { handler.chars(text); + } } - } else { - html = html.replace(new RegExp("([\\s\\S]*?)<\/" + stack.last() + "[^>]*>"), function (all, text) { - text = text.replace(/|/g, "$1$2"); - if (handler.chars) - handler.chars(text); - - return ""; - }); - - parseEndTag("", stack.last()); + if (html === last) + throw "Parse Error: " + html; + last = html; } - if (html == last) - throw "Parse Error: " + html; - last = html; - } + // Clean up any remaining tags + parseEndTag(); - // Clean up any remaining tags - parseEndTag(); + function parseStartTag(tag, tagName, rest, unary) { + var casePreservedTagName = tagName; + tagName = tagName.toLowerCase(); - function parseStartTag(tag, tagName, rest, unary) { - tagName = tagName.toLowerCase(); + if (block[tagName]) { + while (lastTag && inline[lastTag]) { + parseEndTag("", lastTag); + } + } - if (block[tagName]) { - while (stack.last() && inline[stack.last()]) { - parseEndTag("", stack.last()); + //TODO: In addition to lastTag === tagName, also check special case for th, td, tfoot, tbody, thead + if (closeSelf[tagName] && lastTag === tagName) { + parseEndTag("", tagName); } - } - if (closeSelf[tagName] && stack.last() == tagName) { - parseEndTag("", tagName); - } + unary = empty[tagName] || !!unary; - unary = empty[tagName] || !!unary; + if (!unary) { + stack.push(tagName); + lastTag = tagName; + } - if (!unary) - stack.push(tagName); + if (handler.start) { + var attrs = [], match, name, value; - if (handler.start) { - var attrs = []; + while ((match = rest.match(attr))) { + rest = rest.substr(match[0].length); - rest.replace(attr, function (match, name) { - var value = arguments[2] ? arguments[2] : - arguments[3] ? arguments[3] : - arguments[4] ? arguments[4] : - fillAttrs[name] ? name : ""; + name = match[1]; + value = match[2] || match[3] || match[4] || ''; - attrs.push({ - name: name, - value: value, - escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') //" - }); - }); + attrs.push({ + name: name, + value: value, + escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') //" + }); + } - if (handler.start) - handler.start(tagName, attrs, unary); - } - } - - function parseEndTag(tag, tagName) { - // If no tag name is provided, clean shop - if (!tagName) - var pos = 0; - - // Find the closest opened tag of the same type - else - for (var pos = stack.length - 1; pos >= 0; pos--) - if (stack[pos] == tagName) - break; - - if (pos >= 0) { - // Close all the open elements, up the stack - for (var i = stack.length - 1; i >= pos; i--) - if (handler.end) - handler.end(stack[i]); - - // Remove the open elements from the stack - stack.length = pos; + if (handler.start) + handler.start(casePreservedTagName, attrs, unary); + } } - } - }; - - this.HTMLtoXML = function (html) { - var results = ""; - - HTMLParser(html, { - start: function (tag, attrs, unary) { - results += "<" + tag; - for (var i = 0; i < attrs.length; i++) - results += " " + attrs[i].name + '="' + attrs[i].escaped + '"'; - results += ">"; - }, - end: function (tag) { - results += ""; - }, - chars: function (text) { - results += text; - }, - comment: function (text) { - results += ""; + function parseEndTag(tag, tagName) { + var pos; + // If no tag name is provided, clean shop + if (!tagName) + pos = 0; + + // Find the closest opened tag of the same type + else + for (pos = stack.length - 1; pos >= 0; pos -= 1) + if (stack[pos] === tagName) + break; + + if (pos >= 0) { + // Close all the open elements, up the stack + for (var i = stack.length - 1; i >= pos; i -= 1) + if (handler.end) + handler.end(stack[i]); + + // Remove the open elements from the stack + stack.length = pos; + lastTag = stack[pos - 1]; + } } - }); - - return results; - }; - - this.HTMLtoDOM = function (html, doc) { - // There can be only one of these elements - var one = makeMap("html,head,body,title"); - - // Enforce a structure for the document - var structure = { - link: "head", - base: "head" - }; - - if (!doc) { - if (typeof DOMDocument != "undefined") - doc = new DOMDocument(); - else if (typeof document != "undefined" && document.implementation && document.implementation.createDocument) - doc = document.implementation.createDocument("", "", null); - else if (typeof ActiveX != "undefined") - doc = new ActiveXObject("Msxml.DOMDocument"); - - } else - doc = doc.ownerDocument || - doc.getOwnerDocument && doc.getOwnerDocument() || - doc; - - var elems = [], - documentElement = doc.documentElement || - doc.getDocumentElement && doc.getDocumentElement(); - - // If we're dealing with an empty document then we - // need to pre-populate it with the HTML document structure - if (!documentElement && doc.createElement) (function () { - var html = doc.createElement("html"); - var head = doc.createElement("head"); - head.appendChild(doc.createElement("title")); - html.appendChild(head); - html.appendChild(doc.createElement("body")); - doc.appendChild(html); - })(); - - // Find all the unique elements - if (doc.getElementsByTagName) - for (var i in one) - one[i] = doc.getElementsByTagName(i)[0]; - - // If we're working with a document, inject contents into - // the body element - var curParentNode = one.body; + }; + + }()); + + var HTMLtoDOM = function (html) { + var doc = document, + newDoc = doc.createDocumentFragment(), + // There can be only one of these elements + one = makeMap("html,head,body,title"), + // Enforce a structure for the document + structure = { + link: "head", + base: "head" + }, + elems = [newDoc], + curParentNode = newDoc; HTMLParser(html, { start: function (tagName, attrs, unary) { - // If it's a pre-built element, then we can ignore - // its construction - if (one[tagName]) { - curParentNode = one[tagName]; - if (!unary) { - elems.push(curParentNode); + var elem = doc.createElement(tagName); + if (tagName in one) { + if (one[tagName] !== true) { + return; } - return; + one[tagName] = elem; //remember important tags } - var elem = doc.createElement(tagName); - - for (var attr in attrs) + for (var attr = 0; attr < attrs.length; attr += 1) elem.setAttribute(attrs[attr].name, attrs[attr].value); - if (structure[tagName] && typeof one[structure[tagName]] != "boolean") + if (structure[tagName] && typeof one[structure[tagName]] !== "boolean") one[structure[tagName]].appendChild(elem); else if (curParentNode && curParentNode.appendChild) @@ -341,21 +317,36 @@ curParentNode = elem; } }, - end: function (tag) { + end: function () { elems.length -= 1; // Init the new parentNode curParentNode = elems[elems.length - 1]; }, chars: function (text) { - curParentNode.appendChild(doc.createTextNode(text)); + if (newDoc.nodeType === 11 || curParentNode !== newDoc) { //webkit throws error when trying to add text directly to a document. + curParentNode.appendChild(doc.createTextNode(text)); + } }, comment: function (text) { // create comment node + curParentNode.appendChild(doc.createComment(text)); + }, + doctype: function () { + if (!newDoc.firstChild) { + newDoc = doc = document.implementation.createDocument("", "", null); //create empty document + + elems = [newDoc]; + curParentNode = newDoc; + + //Since we support only HTML5 we create HTML5 doctype. This won't work on IE8-. + newDoc.insertBefore(newDoc.implementation.createDocumentType('html', '', ''), newDoc.firstChild); + } } }); - return doc; + newDoc.normalize(); + return newDoc; }; function makeMap(str) { @@ -364,4 +355,7 @@ obj[items[i]] = true; return obj; } -})(); + + HTMLtoDOM.Parser = HTMLParser; + return HTMLtoDOM; +})); diff --git a/package.json b/package.json new file mode 100644 index 0000000..0c157a5 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "neutron-html5parser", + "author": "munawwar", + "description": "Small Pure-JS HTML5 Parser", + "version": "0.2.0", + "main": "htmlparser.js", + "dependencies": {}, + "devDependencies": { + "htmlparser-benchmark": "^1.1.3", + "jsdom": "9.5.0", + "mocha": "3.0.2" + }, + "directories": { + "test": "test" + }, + "scripts": { + "test": "make test" + }, + "repository": { + "type": "git", + "url": "https://github.com/Munawwar/neutron-html5parser" + }, + "keywords": [ + "html parser" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/Munawwar/neutron-html5parser/issues" + }, + "homepage": "https://github.com/Munawwar/neutron-html5parser" +} diff --git a/test/01-simple.json b/test/01-simple.json new file mode 100644 index 0000000..bc9fa15 --- /dev/null +++ b/test/01-simple.json @@ -0,0 +1,25 @@ +{ + "name": "simple", + "html": "

adsf

", + "expected": [ + { + "event": "opentag", + "data": [ + "h1", + ["class", "test"] + ] + }, + { + "event": "text", + "data": [ + "adsf" + ] + }, + { + "event": "closetag", + "data": [ + "h1" + ] + } + ] +} diff --git a/test/02-template.json b/test/02-template.json new file mode 100644 index 0000000..2b19dd4 --- /dev/null +++ b/test/02-template.json @@ -0,0 +1,41 @@ +{ + "name": "Template script tags", + "options": { + "handler": {}, + "parser": {} + }, + "html": "

", + "expected": [ + { + "event": "opentag", + "data": [ + "p" + ] + }, + { + "event": "opentag", + "data": [ + "script", + ["type", "text/template"] + ] + }, + { + "event": "text", + "data": [ + "

Heading1

" + ] + }, + { + "event": "closetag", + "data": [ + "script" + ] + }, + { + "event": "closetag", + "data": [ + "p" + ] + } + ] +} diff --git a/test/04-cdata.json b/test/04-cdata.json new file mode 100644 index 0000000..d0f1472 --- /dev/null +++ b/test/04-cdata.json @@ -0,0 +1,36 @@ +{ + "name": "CDATA", + "html": "<> fo]]>", + "expected": [ + { + "event": "opentag", + "data": [ + "tag" + ] + }, + { + "event": "cdata", + "data": [ + " asdf ><> fo" + ] + }, + { + "event": "closetag", + "data": [ + "tag" + ] + }, + { + "event": "text", + "data": [ + "<" + ] + }, + { + "event": "text", + "data": [ + "![CD>" + ] + } + ] +} diff --git a/test/05-cdata-special.json b/test/05-cdata-special.json new file mode 100644 index 0000000..1022e12 --- /dev/null +++ b/test/05-cdata-special.json @@ -0,0 +1,28 @@ +{ + "name": "CDATA (inside special)", + "options": { + "handler": {}, + "parser": {} + }, + "html": "", + "expected": [ + { + "event": "opentag", + "data": [ + "script" + ] + }, + { + "event": "text", + "data": [ + "/*<> fo/*]]>*/" + ] + }, + { + "event": "closetag", + "data": [ + "script" + ] + } + ] +} diff --git a/test/06-leading-lt.json b/test/06-leading-lt.json new file mode 100644 index 0000000..ac0b521 --- /dev/null +++ b/test/06-leading-lt.json @@ -0,0 +1,16 @@ +{ + "name": "leading lt", + "options": { + "handler": {}, + "parser": {} + }, + "html": ">a>", + "expected": [ + { + "event": "text", + "data": [ + ">a>" + ] + } + ] +} diff --git a/test/07-self-closing.json b/test/07-self-closing.json new file mode 100644 index 0000000..1129ead --- /dev/null +++ b/test/07-self-closing.json @@ -0,0 +1,37 @@ +{ + "name": "Self-closing tags", + "html": "Foo
", + "expected": [ + { + "event": "opentag", + "data": [ + "a", + ["href", "http://test.com/"] + ] + }, + { + "event": "text", + "data": [ + "Foo" + ] + }, + { + "event": "closetag", + "data": [ + "a" + ] + }, + { + "event": "opentag", + "data": [ + "hr" + ] + }, + { + "event": "closetag", + "data": [ + "hr" + ] + } + ] +} diff --git a/test/08-implicit-close-tags.json b/test/08-implicit-close-tags.json new file mode 100644 index 0000000..f52a050 --- /dev/null +++ b/test/08-implicit-close-tags.json @@ -0,0 +1,50 @@ +{ + "name": "Implicit close tags", + "options": {}, + "html": "
  1. TH

    Heading

    Div
    Div2
  2. Heading 2

Para

Heading 4

", + "expected": [ + { "event": "opentag", "data": [ "ol" ] }, + { "event": "opentag", "data": [ "li", [ "class", "test" ] ] }, + { "event": "opentag", "data": [ "div" ] }, + { "event": "opentag", "data": [ "table", [ "style", "width:100%" ] ] }, + { "event": "opentag", "data": [ "tr" ] }, + { "event": "opentag", "data": [ "th" ] }, + { "event": "text", "data": [ "TH" ] }, + { "event": "closetag", "data": [ "th" ] }, + { "event": "opentag", "data": [ "td", [ "colspan", "2" ] ] }, + { "event": "opentag", "data": [ "h3" ] }, + { "event": "text", "data": [ "Heading" ] }, + { "event": "closetag", "data": [ "h3" ] }, + { "event": "closetag", "data": [ "td" ] }, + { "event": "closetag", "data": [ "tr" ] }, + { "event": "opentag", "data": [ "tr" ] }, + { "event": "opentag", "data": [ "td" ] }, + { "event": "opentag", "data": [ "div" ] }, + { "event": "text", "data": [ "Div" ] }, + { "event": "closetag", "data": [ "div" ] }, + { "event": "closetag", "data": [ "td" ] }, + { "event": "opentag", "data": [ "td" ] }, + { "event": "opentag", "data": [ "div" ] }, + { "event": "text", "data": [ "Div2" ] }, + { "event": "closetag", "data": [ "div" ] }, + { "event": "closetag", "data": [ "td" ] }, + { "event": "closetag", "data": [ "tr" ] }, + { "event": "closetag", "data": [ "table" ] }, + { "event": "closetag", "data": [ "div" ] }, + { "event": "closetag", "data": [ "li" ] }, + { "event": "opentag", "data": [ "li" ] }, + { "event": "opentag", "data": [ "div" ] }, + { "event": "opentag", "data": [ "h3" ] }, + { "event": "text", "data": [ "Heading 2" ] }, + { "event": "closetag", "data": [ "h3" ] }, + { "event": "closetag", "data": [ "div" ] }, + { "event": "closetag", "data": [ "li" ] }, + { "event": "closetag", "data": [ "ol" ] }, + { "event": "opentag", "data": [ "p" ] }, + { "event": "text", "data": [ "Para" ] }, + { "event": "closetag", "data": [ "p" ] }, + { "event": "opentag", "data": [ "h4" ] }, + { "event": "text", "data": [ "Heading 4" ] }, + { "event": "closetag", "data": [ "h4" ] } + ] +} diff --git a/test/09-attributes.json b/test/09-attributes.json new file mode 100644 index 0000000..0824b62 --- /dev/null +++ b/test/09-attributes.json @@ -0,0 +1,32 @@ +{ + "name": "attributes (no white space, no value, no quotes)", + "options": { + "handler": {}, + "parser": {} + }, + "html": "", + "expected": [ + { + "event": "opentag", + "data": [ + "button", + ["class", "test0"], + ["title", "test1"], + ["disabled", ""], + ["value", "test2"] + ] + }, + { + "event": "text", + "data": [ + "adsf" + ] + }, + { + "event": "closetag", + "data": [ + "button" + ] + } + ] +} diff --git a/test/15-lt-whitespace.html b/test/15-lt-whitespace.html new file mode 100644 index 0000000..ec87be7 --- /dev/null +++ b/test/15-lt-whitespace.html @@ -0,0 +1 @@ +a < b diff --git a/test/15-lt-whitespace.json b/test/15-lt-whitespace.json new file mode 100644 index 0000000..5b44a6a --- /dev/null +++ b/test/15-lt-whitespace.json @@ -0,0 +1,24 @@ +{ + "name": "lt followed by whitespace", + "html": "a < b", + "expected": [ + { + "event": "text", + "data": [ + "a " + ] + }, + { + "event": "text", + "data": [ + "<" + ] + }, + { + "event": "text", + "data": [ + " b" + ] + } + ] +} diff --git a/test/browser.html b/test/browser.html new file mode 100644 index 0000000..e251e5e --- /dev/null +++ b/test/browser.html @@ -0,0 +1,61 @@ + + + + + + + + + diff --git a/test/multiple.html b/test/multiple.html new file mode 100644 index 0000000..9a5c993 --- /dev/null +++ b/test/multiple.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + < div> +
+
+
+ + +
+ test +
+ + + + + diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..9fdd57b --- /dev/null +++ b/test/test.js @@ -0,0 +1,161 @@ +/*global describe, it*/ + +var assert = require("assert"), + fs = require('fs'), + factory = require('../htmlparser.js'), + jsdom = require('jsdom'), + HTMLtoDOM = factory(jsdom.jsdom('').parentWindow); + +describe('run consolidated HTMLtoDOM test - multiple.html', function () { + var html = fetch('test/multiple.html'), + doc = HTMLtoDOM(html); + it('it should have two nodes - doctype and html tag', function () { + assert.equal(2, doc.childNodes.length); + }); + it('html tag should have two child elements - head and body tag', function () { + assert.equal(2, doc.childNodes[1].children.length); + }); +}); + + + +describe('run element and text test - 01-simple.json', function () { + runJSONTest('test/01-simple.json'); +}); + +describe('run script templates test - 02-template.json', function () { + runJSONTest('test/02-template.json'); +}); + +describe('run CDATA test - 04-cdata.json', function () { + runJSONTest('test/04-cdata.json'); +}); + +describe('run script templates test - 05-cdata-special.json', function () { + runJSONTest('test/05-cdata-special.json'); +}); + +describe('run script templates test - 06-leading-lt.json', function () { + runJSONTest('test/06-leading-lt.json'); +}); + +describe('run self-closing tag test - 07-self-closing.json', function () { + runJSONTest('test/07-self-closing.json'); +}); + +describe('run self-closing tag test - 09-attributes.json', function () { + runJSONTest('test/09-attributes.json'); +}); + +describe('run isolated less than angle bracket - 15-lt-whitespace.json', function () { + runJSONTest('test/15-lt-whitespace.json'); +}); + +/*Utility functions*/ +function fetch(pathToTextFile) { + return fs.readFileSync(pathToTextFile, {encoding: 'utf8'}); +} + +function runJSONTest(filePath) { + var json = JSON.parse(fetch(filePath)), + next = 0, + meta = json.expected[next++]; + + var handlers = { + start: function (tagName, attrs, unary) { + if (!meta) { + throw new Error('More nodes than expected'); + } + //it() calls seems to be running asyncronously. So make a closure to current test. + var exp = meta; + it('next should be an open tag event', function () { + assert.equal('opentag', exp.event); + }); + + it('it should be \'' + exp.data[0] + '\' tag', function () { + assert.equal(exp.data[0], tagName); + }); + + it('it should have ' + (exp.data.length - 1) + ' number of attrbute(s)', function () { + assert.equal(exp.data.length - 1, attrs.length); + }); + + exp.data.slice(1).forEach(function (expAttr, index) { + it('attribute number ' + (index + 1) + '\'s name should be ' + expAttr[0], function () { + assert.equal(expAttr[0], attrs[index].name); + }); + it('attribute number ' + (index + 1) + '\'s value should be ' + expAttr[1], function () { + assert.equal(expAttr[1], attrs[index].value); + }); + }); + + meta = json.expected[next++]; //Increment + }, + end: function (tagName) { + if (!meta) { + throw new Error('More nodes than expected'); + } + //it() calls seems to be running asyncronously. So make a closure to current test. + var exp = meta; + it('next should be a close tag event', function () { + assert.equal('closetag', exp.event); + }); + + it('it should be \'' + exp.data[0] + '\' tag', function () { + assert.equal(exp.data[0], tagName); + }); + + meta = json.expected[next++]; //Increment + }, + chars: function (text) { + if (!meta) { + throw new Error('More nodes than expected'); + } + //it() calls seems to be running asyncronously. So make a closure to current test. + var exp = meta; + it('next should be a text event ', function () { + assert.equal('text', exp.event); + }); + + it('it should have value ' + exp.data[0], function () { + assert.equal(exp.data[0], text); + }); + + meta = json.expected[next++]; //Increment + }, + cdata: function (text) { + if (!meta) { + throw new Error('More nodes than expected'); + } + //it() calls seems to be running asyncronously. So make a closure to current test. + var exp = meta; + it('next should be a cdata event ', function () { + assert.equal('cdata', exp.event); + }); + + it('it should have value ' + exp.data[0], function () { + assert.equal(exp.data[0], text); + }); + + meta = json.expected[next++]; //Increment + }, + comment: function (text) { + if (!meta) { + throw new Error('More nodes than expected'); + } + //it() calls seems to be running asyncronously. So make a closure to current test. + var exp = meta; + it('next should be a comment event ', function () { + assert.equal('comment', exp.event); + }); + + it('it should have value ' + exp.data[0], function () { + assert.equal(exp.data[0], text); + }); + + meta = json.expected[next++]; //Increment + } + }; + + HTMLtoDOM.Parser(json.html, handlers); +}