|
| 1 | +var utils = require('./utils'), |
| 2 | + |
| 3 | + diff = require('diff'), |
| 4 | + htmlParser = require('htmlparser'), |
| 5 | + AST2Html = require('htmlparser-to-html'), |
| 6 | + _ = require('lodash'); |
| 7 | + |
| 8 | +/** |
| 9 | + * Converts HTML to DOM Tree |
| 10 | + * @param {String} HTML |
| 11 | + * @param {Object} options |
| 12 | + * @returns {AST} |
| 13 | + */ |
| 14 | +function htmlToAST(HTML, options) { |
| 15 | + var parser, |
| 16 | + parserHandler; |
| 17 | + |
| 18 | + parserHandler = new htmlParser.DefaultHandler(function(err) { |
| 19 | + if (err) console.log(err); |
| 20 | + }, options); |
| 21 | + |
| 22 | + parser = new htmlParser.Parser(parserHandler); |
| 23 | + parser.parseComplete(HTML); |
| 24 | + |
| 25 | + return parserHandler.dom; |
| 26 | +} |
| 27 | + |
| 28 | +/** |
| 29 | + * |
| 30 | + * @param {AST} tree |
| 31 | + * @param {Object} options |
| 32 | + * @returns {AST} |
| 33 | + */ |
| 34 | +function modifyASTTree(tree, options) { |
| 35 | + _.each(tree, function(node) { |
| 36 | + Object.keys(node).forEach(function(leaf) { |
| 37 | + if (leaf === 'attribs') { |
| 38 | + var attrs = utils.sortObj(node[leaf]); |
| 39 | + |
| 40 | + if (attrs.hasOwnProperty('class')) { |
| 41 | + attrs['class'] = utils.sortCssClasses(attrs['class']); |
| 42 | + } |
| 43 | + |
| 44 | + _.each(options.compareHtmlAttrsAsJSON, function(attr) { |
| 45 | + var attrValue, |
| 46 | + isFunction = (attr === 'onclick' || attr === 'ondblclick'); // @FIXME: should be configurable |
| 47 | + |
| 48 | + if (attrs.hasOwnProperty(attr)) { |
| 49 | + |
| 50 | + attrValue = utils.parseAttr(attrs[attr].replace(/"/g, '"'), isFunction); |
| 51 | + attrValue = utils.sortObj(attrValue); |
| 52 | + attrValue = JSON.stringify(attrValue); |
| 53 | + |
| 54 | + attrs[attr] = (isFunction ? 'return ' : '') + attrValue.replace(/"/g, '"') |
| 55 | + } |
| 56 | + }); |
| 57 | + |
| 58 | + _.each(options.ignoreHtmlAttrs, function(attr) { |
| 59 | + attrs.hasOwnProperty(attr) && (attrs[attr] = ''); |
| 60 | + }); |
| 61 | + |
| 62 | + node[leaf] = attrs; |
| 63 | + } |
| 64 | + else if (leaf === 'children') { |
| 65 | + modifyASTTree(node.children, options); |
| 66 | + } |
| 67 | + }); |
| 68 | + }); |
| 69 | + |
| 70 | + return tree; |
| 71 | +} |
| 72 | + |
| 73 | +/** |
| 74 | + * |
| 75 | + * @param [options] |
| 76 | + * @param {String[]} [options.ignoreHtmlAttrs] |
| 77 | + * @param {String[]} [options.compareHtmlAttrsAsJSON] |
| 78 | + * @param {Boolean} [options.verbose] |
| 79 | + * @param {Boolean} [options.ignoreWhitespace=true] |
| 80 | + * @param {Boolean} [options.bem=true] |
| 81 | + * @constructor |
| 82 | + */ |
| 83 | +var HtmlDiff = function(options) { |
| 84 | + this.options = utils.defaults(options); |
| 85 | +}; |
| 86 | + |
| 87 | +var Diff = diff.Diff; |
| 88 | + |
| 89 | +HtmlDiff.prototype = Diff.prototype; |
| 90 | + |
| 91 | +/** |
| 92 | + * Tokenizes a given string |
| 93 | + * @param {String} value |
| 94 | + * @returns {Array} |
| 95 | + */ |
| 96 | +HtmlDiff.prototype.tokenize = function(value) { |
| 97 | + var options = this.options, |
| 98 | + ASTTree = htmlToAST(value, options); |
| 99 | + |
| 100 | + ASTTree = modifyASTTree(ASTTree, options); |
| 101 | + |
| 102 | + /* |
| 103 | + * Bug in 'html-parser-to-html' |
| 104 | + * String '"' is converted into '"' |
| 105 | + * Added issue => github.com/mixu/htmlparser-to-html/issues/1 |
| 106 | + */ |
| 107 | + value = AST2Html(ASTTree).replace(/"/g, '"'); |
| 108 | + |
| 109 | + return _.filter(value.split(/(\s+|\b)/)); |
| 110 | +}; |
| 111 | + |
| 112 | +/** |
| 113 | + * |
| 114 | + * @param [options] |
| 115 | + * @param {String[]} [options.ignoreHtmlAttrs] |
| 116 | + * @param {String[]} [options.compareHtmlAttrsAsJSON] |
| 117 | + * @param {Boolean} [options.verbose] |
| 118 | + * @param {Boolean} [options.ignoreWhitespace=true] |
| 119 | + * @param {Boolean} [options.bem=true] |
| 120 | + * @constructor |
| 121 | + */ |
| 122 | +var HtmlDiffer = function(options) { |
| 123 | + options = utils.defaults(options); |
| 124 | + |
| 125 | + if (options['bem']) { |
| 126 | + options.ignoreHtmlAttrs = ['id', 'for']; |
| 127 | + options.compareHtmlAttrsAsJSON = ['data-bem', 'onclick', 'ondblclick']; |
| 128 | + } |
| 129 | + |
| 130 | + this.options = options; |
| 131 | +}; |
| 132 | + |
| 133 | +/** |
| 134 | + * |
| 135 | + * @param {String} html1 |
| 136 | + * @param {String} html2 |
| 137 | + * @param {Object} [options] |
| 138 | + * @returns {Diff} |
| 139 | + */ |
| 140 | +HtmlDiffer.prototype.diffHtml = function(html1, html2, options) { |
| 141 | + if (options) { |
| 142 | + console.warn('WARNING! The third param of "diffHtml" method is deprecated!'); |
| 143 | + } |
| 144 | + |
| 145 | + var htmlDiffer = new HtmlDiff(options); |
| 146 | + |
| 147 | + return htmlDiffer.diff(html1, html2); |
| 148 | +}; |
| 149 | + |
| 150 | +/** |
| 151 | + * Compares two given chunks of HTML |
| 152 | + * @param {String} html1 |
| 153 | + * @param {String} html2 |
| 154 | + * @param {Object} [options] |
| 155 | + * @returns {Boolean} |
| 156 | + */ |
| 157 | +HtmlDiffer.prototype.isEqual = function(html1, html2, options) { |
| 158 | + if (options) { |
| 159 | + console.warn('WARNING! The third param of "isEqual" method is deprecated!'); |
| 160 | + } |
| 161 | + |
| 162 | + options = _.defaults(this.options, options); |
| 163 | + |
| 164 | + var htmlDiffer = new HtmlDiff(options), |
| 165 | + diff = htmlDiffer.diff(html1, html2); |
| 166 | + |
| 167 | + return (diff.length === 1 && !diff[0].added && !diff[0].removed); |
| 168 | +}; |
| 169 | + |
| 170 | +/** |
| 171 | + * @deprecated |
| 172 | + * @param {String} html1 |
| 173 | + * @param {String} html2 |
| 174 | + */ |
| 175 | +function bemDiff(html1, html2) { |
| 176 | + console.warn('WARNING! You use deprecated method \'bemDiff\'!'); |
| 177 | + |
| 178 | + var logger = require('./diff-logger'), |
| 179 | + options = { |
| 180 | + ignoreHtmlAttrs: ['id', 'for'], |
| 181 | + compareHtmlAttrsAsJSON: ['data-bem', 'onclick', 'ondblclick'] |
| 182 | + }, |
| 183 | + loggerOptions = { |
| 184 | + showCharacters: 20 |
| 185 | + }, |
| 186 | + |
| 187 | + htmlDiffer = new HtmlDiff(options); |
| 188 | + |
| 189 | + logger.log(htmlDiffer.diff(html1, html2), loggerOptions); |
| 190 | +} |
| 191 | + |
| 192 | + |
| 193 | +var htmlDiffer = new HtmlDiffer(); |
| 194 | + |
| 195 | +module.exports = { |
| 196 | + HtmlDiff: HtmlDiff, |
| 197 | + HtmlDiffer: HtmlDiffer, |
| 198 | + diffHtml: htmlDiffer.diffHtml.bind(htmlDiffer), |
| 199 | + isEqual: htmlDiffer.isEqual.bind(htmlDiffer), |
| 200 | + |
| 201 | + bemDiff: bemDiff |
| 202 | +}; |
0 commit comments