diff --git a/lib/index.js b/lib/index.js index 203c2bd..0a496f2 100644 --- a/lib/index.js +++ b/lib/index.js @@ -3,8 +3,8 @@ var objectAssign = require('object-assign') var pkg = require('../package.json') var Api = require('./api.js') -var parser = require('posthtml-parser') -var render = require('posthtml-render') +var parser = require('./parser.js') +var render = require('./render.js') /** * @author Ivan Voischev (@voischev), diff --git a/lib/parser.js b/lib/parser.js new file mode 100644 index 0000000..2bfbb02 --- /dev/null +++ b/lib/parser.js @@ -0,0 +1,152 @@ +'use strict' + +var Parser = require('htmlparser2/lib/Parser') +var objectAssign = require('object-assign') + +/** + * @see https://github.com/fb55/htmlparser2/wiki/Parser-options + */ +var defaultOptions = {lowerCaseTags: false, lowerCaseAttributeNames: false} + +var defaultDirectives = [{name: '!doctype', start: '<', end: '>'}] + +/** + * Parse html to PostHTMLTree + * @param {String} html + * @param {Object} [options=defaultOptions] + * @return {PostHTMLTree} + */ +function postHTMLParser (html, options) { + var bufArray = [] + var results = [] + + bufArray.last = function () { + return this[this.length - 1] + } + + function isDirective (directive, tag) { + if (directive.name instanceof RegExp) { + var regex = RegExp(directive.name.source, 'i') + + return regex.test(tag) + } + + if (tag !== directive.name) { + return false + } + + return true + } + + function parserDirective (name, data) { + var directives = [].concat(defaultDirectives, options.directives || []) + var last = bufArray.last() + + for (var i = 0; i < directives.length; i++) { + var directive = directives[i] + var directiveText = directive.start + data + directive.end + + name = name.toLowerCase() + if (isDirective(directive, name)) { + if (!last) { + results.push(directiveText) + return + } + + last.content || (last.content = []) + last.content.push(directiveText) + } + } + } + + function normalizeArributes (attrs) { + var result = {} + Object.keys(attrs).forEach(function (key) { + var obj = {} + obj[key] = attrs[key].replace(/"/g, '"') + objectAssign(result, obj) + }) + + return result + } + + var parser = new Parser({ + onprocessinginstruction: parserDirective, + oncomment: function (data) { + var comment = '' + var last = bufArray.last() + + if (!last) { + results.push(comment) + return + } + + last.content || (last.content = []) + last.content.push(comment) + }, + onopentag: function (tag, attrs) { + var buf = { tag: tag } + + if (Object.keys(attrs).length) { + buf.attrs = normalizeArributes(attrs) + } + + bufArray.push(buf) + }, + onclosetag: function () { + var buf = bufArray.pop() + + if (!bufArray.length) { + results.push(buf) + return + } + + var last = bufArray.last() + if (!Array.isArray(last.content)) { + last.content = [] + } + + last.content.push(buf) + }, + ontext: function (text) { + var last = bufArray.last() + if (!last) { + results.push(text) + return + } + + last.content || (last.content = []) + last.content.push(text) + } + }, options || defaultOptions) + + parser.write(html) + parser.end() + + return results +} + +function parserWrapper () { + var option + + function parser (html) { + var opt = objectAssign(defaultOptions, option) + return postHTMLParser(html, opt) + } + + if ( + arguments.length === 1 && + Boolean(arguments[0]) && + arguments[0].constructor.name === 'Object' + ) { + option = arguments[0] + return parser + } + + option = arguments[1] + return parser(arguments[0]) +} + +module.exports = parserWrapper +module.exports.defaultOptions = defaultOptions +module.exports.defaultDirectives = defaultDirectives diff --git a/lib/render.js b/lib/render.js new file mode 100644 index 0000000..32f8552 --- /dev/null +++ b/lib/render.js @@ -0,0 +1,168 @@ +var SINGLE_TAGS = [ + 'area', + 'base', + 'br', + 'col', + 'command', + 'embed', + 'hr', + 'img', + 'input', + 'keygen', + 'link', + 'menuitem', + 'meta', + 'param', + 'source', + 'track', + 'wbr', + // Custom (PostHTML) + 'import', + 'include', + 'extend', + 'component' +] + +/** + * Render PostHTML Tree to HTML + * + * @param {Array|Object} tree PostHTML Tree + * @param {Object} options Options + * + * @return {String} HTML + */ +function render (tree, options) { + /** + * Options + * + * @type {Object} + * + * @prop {Array} singleTags Custom single tags (selfClosing) + * @prop {String} closingSingleTag Closing format for single tag + * + * Formats: + * + * ``` tag: `

` ```, slash: `
` ```, ```default: `
` ``` + */ + options = options || {} + + var singleTags = SINGLE_TAGS.concat(options.singleTags || []) + var singleRegExp = singleTags.filter(function (tag) { + return tag instanceof RegExp ? tag : false + }) + + var closingSingleTag = options.closingSingleTag + + return html(tree) + + /** + * HTML Stringifier + * + * @param {Array|Object} tree PostHTML Tree + * + * @return {String} result HTML + */ + function html (tree) { + var result = '' + + traverse([].concat(tree), function (node) { + if (!node) return + + if (typeof node === 'string' || typeof node === 'number') { + result += node + + return + } + + if (typeof node.tag === 'boolean' && !node.tag) { + typeof node.content !== 'object' && (result += node.content) + + return node.content + } + + // treat as new root tree if node is an array + if (Array.isArray(node)) { + result += html(node) + + return + } + + var tag = node.tag || 'div' + + if (isSingleTag(tag, singleTags, singleRegExp)) { + result += '<' + tag + attrs(node.attrs) + + switch (closingSingleTag) { + case 'tag': + result += '>' + + break + case 'slash': + result += ' />' + + break + default: + result += '>' + } + } else { + result += '<' + tag + (node.attrs ? attrs(node.attrs) : '') + '>' + (node.content ? html(node.content) : '') + '' + } + }) + + return result + } +} + +/** + * @module posthtml-render + * + * @version 1.0.7 + * @license MIT + */ +module.exports = render + +/** @private */ +function attrs (obj) { + var attr = '' + + for (var key in obj) { + if (typeof obj[key] === 'boolean' && obj[key]) { + attr += ' ' + key + } else if ( + typeof obj[key] === 'string' || + typeof obj[key] === 'number' + ) { + attr += ' ' + key + '="' + obj[key] + '"' + } + } + + return attr +} + +/** @private */ +function traverse (tree, cb) { + if (Array.isArray(tree)) { + for (var i = 0, length = tree.length; i < length; i++) { + traverse(cb(tree[i]), cb) + } + } else if (typeof tree === 'object' && tree.hasOwnProperty('content')) { + traverse(tree.content, cb) + } + + return tree +} + +/** @private */ +function isSingleTag (tag, singleTags, singleRegExp) { + if (singleRegExp.length) { + for (var i = 0; i < singleRegExp.length; i++) { + return !!tag.match(singleRegExp[i]) + } + } + + if (singleTags.indexOf(tag) === -1) { + return false + } + + return true +} diff --git a/package-lock.json b/package-lock.json index bd8978b..22a6270 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,9 @@ { "name": "posthtml", - "version": "0.11.4", + "version": "0.11.3", "lockfileVersion": 1, "requires": true, "dependencies": { - "JSONStream": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz", - "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=", - "dev": true, - "requires": { - "jsonparse": "1.3.1", - "through": "2.3.8" - } - }, "abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", @@ -835,8 +825,8 @@ "integrity": "sha512-8MD05yN0Zb6aRsZnFX1ET+8rHWfWJk+my7ANCJZBU2mhz7TSB1fk2vZhkrwVy/PCllcTYAP/1T1NiWQ7Z01mKw==", "dev": true, "requires": { - "JSONStream": "1.3.1", "is-text-path": "1.0.1", + "JSONStream": "1.3.1", "lodash": "4.17.4", "meow": "3.7.0", "split2": "2.2.0", @@ -1165,9 +1155,9 @@ } }, "domutils": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.6.2.tgz", - "integrity": "sha1-GVjMC0yUJuntNn+xyOhUiRsPo/8=", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", "requires": { "dom-serializer": "0.1.0", "domelementtype": "1.3.0" @@ -1279,7 +1269,7 @@ "es6-promise": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.1.1.tgz", - "integrity": "sha512-OaU1hHjgJf+b0NzsxCg7NdIYERD6Hy/PEmFLTjw+b65scuisG3Kt4QoTvJ66BBkPZ581gr0kpoVzKnxniM8nng==", + "integrity": "sha1-iBHpCRXZoNujYnTwskLb2nj5ySo=", "dev": true }, "es6-set": { @@ -2269,7 +2259,7 @@ "requires": { "domelementtype": "1.3.0", "domhandler": "2.4.1", - "domutils": "1.6.2", + "domutils": "1.7.0", "entities": "1.1.1", "inherits": "2.0.3", "readable-stream": "2.3.3" @@ -2580,14 +2570,6 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - }, "istanbul": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", @@ -2781,6 +2763,16 @@ "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", "dev": true }, + "JSONStream": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz", + "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=", + "dev": true, + "requires": { + "jsonparse": "1.3.1", + "through": "2.3.8" + } + }, "jsx-ast-utils": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz", @@ -3699,21 +3691,6 @@ "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", "dev": true }, - "posthtml-parser": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.3.3.tgz", - "integrity": "sha512-H/Z/yXGwl49A7hYQLV1iQ3h87NE0aZ/PMZhFwhw3lKeCAN+Ti4idrHvVvh4/GX10I7u77aQw+QB4vV5/Lzvv5A==", - "requires": { - "htmlparser2": "3.9.2", - "isobject": "2.1.0", - "object-assign": "4.1.1" - } - }, - "posthtml-render": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/posthtml-render/-/posthtml-render-1.1.0.tgz", - "integrity": "sha512-EeUf38sN9VOS6sIe8HhgzE1qpZ+2ARXj/b7IJoUi0CQqxH4qeF6ZxAK808YhhWI4FsT3RCNiSKJ7tDSZ4rkd7w==" - }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -4523,6 +4500,14 @@ "integrity": "sha512-DBp0lSvX5G9KGRDTkR/R+a29H+Wk2xItOF+MpZLLNDWbEV9tGPnqLPxHEYjmiz8xGtJHRIqmI+hCjmNzqoA4nQ==", "dev": true }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, "string-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", @@ -4577,14 +4562,6 @@ } } }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "5.1.1" - } - }, "strip-ansi": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", diff --git a/package.json b/package.json index 9f87172..0481954 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,8 @@ "node": ">=0.10.0" }, "dependencies": { - "object-assign": "^4.1.1", - "posthtml-parser": "^0.3.3", - "posthtml-render": "^1.1.0" + "htmlparser2": "^3.9.2", + "object-assign": "^4.1.1" }, "devDependencies": { "chai": "^3.0.0", @@ -34,7 +33,6 @@ "jsdoc-to-markdown": "^3.0.0", "mocha": "^3.4.0", "mversion": "^1.10.0", - "object.assign": "^4.0.3", "standard": "^10.0.2", "standard-version": "^4.2.0" }, diff --git a/test/parser.js b/test/parser.js index 871279c..221ab0e 100644 --- a/test/parser.js +++ b/test/parser.js @@ -5,8 +5,8 @@ var it = require('mocha').it var expect = require('chai').expect var describe = require('mocha').describe -var parser = require('posthtml-parser') -var render = require('posthtml-render') +var parser = require('../lib/parser') +var render = require('../lib/render') var html = fs.readFileSync( path.resolve(__dirname, 'templates/parser.html'), 'utf8'