├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── example.js
├── index.js
├── lib
└── tokenize.js
├── package.json
└── test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | coverage/
3 | tmp/
4 | dist/
5 | npm-debug.log*
6 | .DS_Store
7 | .nyc_output
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | node_js:
2 | - "4"
3 | - "5"
4 | - "6"
5 | - "7"
6 | sudo: false
7 | language: node_js
8 | script: "npm run test"
9 | # after_success: "npm i -g codecov && npm run coverage && codecov"
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Yoshua Wuyts
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # assert-html [![stability][0]][1]
2 | [![npm version][2]][3] [![build status][4]][5]
3 | [![downloads][8]][9] [![js-standard-style][10]][11]
4 |
5 | Assert two HTML strings are equal. Similar to [spok][spok] but for HTML. Use
6 | [assert-snapshot](https://github.com/yoshuawuyts/assert-snapshot) if you want
7 | an automated workflow.
8 |
9 | ## Usage
10 | ```js
11 | var assertHtml = require('assert-html')
12 | var tape = require('tape')
13 | var html = require('bel')
14 |
15 | tape('compare two DOM strings', function (assert) {
16 | var a, b
17 | a = html``.toString()
18 | b = html``.toString()
19 | assertHtml(assert, a, b)
20 |
21 | a = html`
hello planet
`.toString()
22 | b = html`hello planet
`.toString()
23 | assertHtml(assert, a, b)
24 | assert.end()
25 | })
26 | ```
27 |
28 | Outputs:
29 | ```tap
30 | TAP version 13
31 | # compare two DOM strings
32 | ok 1
33 | not ok 2 ·· hello world
34 | ---
35 | operator: equal
36 | expected: '·· hello world'
37 | actual: '·· hello planet'
38 | at: assertHtml (/Users/anon/src/shama/assert-html/index.js:59:13)
39 | ...
40 | ok 3
41 | ok 4
42 | ok 5 ··
43 | ok 6 ·· ·· hello
44 | ok 7 ··
45 | ok 8 ·· planet
46 | ok 9
47 |
48 | 1..9
49 | # tests 9
50 | # pass 8
51 | # fail 1
52 |
53 | ```
54 |
55 | ## API
56 | ### `assertHtml(assert, actual, expected)`
57 | Assert two DOM strings are equal using a custom assert function. Calls
58 | `assert.equal()` method from the assert function.
59 |
60 | ## See Also
61 | - [thlorenz/spok](https://github.com/thlorenz/spok)
62 | - [yoshuawuyts/assert-snapshot](https://github.com/yoshuawuyts/assert-snapshot)
63 |
64 | ## License
65 | [MIT](https://tldrlegal.com/license/mit-license)
66 |
67 | [0]: https://img.shields.io/badge/stability-experimental-orange.svg?style=flat-square
68 | [1]: https://nodejs.org/api/documentation.html#documentation_stability_index
69 | [2]: https://img.shields.io/npm/v/assert-html.svg?style=flat-square
70 | [3]: https://npmjs.org/package/assert-html
71 | [4]: https://img.shields.io/travis/yoshuawuyts/assert-html/master.svg?style=flat-square
72 | [5]: https://travis-ci.org/yoshuawuyts/assert-html
73 | [6]: https://img.shields.io/codecov/c/github/yoshuawuyts/assert-html/master.svg?style=flat-square
74 | [7]: https://codecov.io/github/yoshuawuyts/assert-html
75 | [8]: http://img.shields.io/npm/dm/assert-html.svg?style=flat-square
76 | [9]: https://npmjs.org/package/assert-html
77 | [10]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square
78 | [11]: https://github.com/feross/standard
79 | [spok]: https://github.com/thlorenz/spok
80 |
--------------------------------------------------------------------------------
/example.js:
--------------------------------------------------------------------------------
1 | var assertHtml = require('./')
2 | var tape = require('tape')
3 | var html = require('bel')
4 |
5 | tape('compare two DOM strings', function (assert) {
6 | var a, b
7 | a = html``.toString()
8 | b = html``.toString()
9 | assertHtml(assert, a, b)
10 |
11 | a = html`hello planet
`.toString()
12 | b = html`hello planet
`.toString()
13 | assertHtml(assert, a, b)
14 |
15 | a = html`hello planet
`.toString()
16 | b = html`hello planet
`.toString()
17 | assertHtml(assert, a, b)
18 |
19 | a = html`hello planet
`.toString()
20 | b = html`hello planet
`.toString()
21 | assertHtml(assert, a, b)
22 |
23 | assert.end()
24 | })
25 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var shallowEqual = require('shallow-equal/objects')
2 | var assert = require('assert')
3 |
4 | var tokenize = require('./lib/tokenize')
5 |
6 | // https://stackoverflow.com/questions/317053/regular-expression-for-extracting-tag-attributes
7 | var matchAttributes = /(\S+)=["']?((?:.(?!["']?\s+(?:\S+)=|[>"']))+.)["']?/g
8 | var matchOpenTags = /(^' && !matchOpenTags.test(leftString)) {
44 | leftDepth += 1
45 | leftClosed = false
46 | }
47 | } else if (leftType === 'text') {
48 | if (!leftClosed) leftDepth += 1
49 | } else if (leftType === 'close') {
50 | leftDepth -= 1
51 | leftClosed = true
52 | }
53 |
54 | if (rightType === 'open') {
55 | if (rightString !== '' && !matchOpenTags.test(rightString)) {
56 | rightDepth += 1
57 | rightClosed = false
58 | }
59 | } else if (rightType === 'text') {
60 | if (!rightClosed) rightDepth += 1
61 | } else if (rightType === 'close') {
62 | rightDepth -= 1
63 | rightClosed = true
64 | }
65 |
66 | if (leftType !== 'text') leftAttrs = getAttributes(leftString)
67 | if (rightType !== 'text') rightAttrs = getAttributes(rightString)
68 |
69 | leftFmt = leftString
70 | for (var j = 0; j < leftDepth; j++) {
71 | leftFmt = indentChars + ' ' + leftFmt
72 | }
73 |
74 | rightFmt = rightString
75 | for (var k = 0; k < rightDepth; k++) {
76 | rightFmt = indentChars + ' ' + rightFmt
77 | }
78 |
79 | // if attributes aren't the same, just compare the two versions
80 | if (!shallowEqual(leftAttrs, rightAttrs) || leftType !== rightType ||
81 | leftType === 'text' || rightType === 'text') {
82 | _assert.equal(leftString, rightString, rightFmt)
83 | } else {
84 | leftTag = leftString.match(/^<[/]{0,1}(\w*)/)[1]
85 | rightTag = rightString.match(/^<[/]{0,1}(\w*)/)[1]
86 | _assert.equal(leftTag, rightTag, rightFmt)
87 | }
88 | }
89 | }
90 |
91 | function getAttributes (str) {
92 | var attrs = str.match(matchAttributes)
93 | if (!attrs) return {}
94 | return attrs.reduce(function (kv, pair) {
95 | var arr = pair.split('=')
96 | var key = arr[0]
97 | var val = arr[1]
98 | if (val) val = val.replace(/['"]/g, '')
99 | kv[key] = val
100 | return kv
101 | }, {})
102 | }
103 |
--------------------------------------------------------------------------------
/lib/tokenize.js:
--------------------------------------------------------------------------------
1 | // synchronous adaptation of:
2 | // https://github.com/substack/html-tokenize
3 |
4 | module.exports = tokenize
5 |
6 | var codes = {
7 | lt: '<'.charCodeAt(0),
8 | gt: '>'.charCodeAt(0),
9 | slash: '/'.charCodeAt(0),
10 | dquote: '"'.charCodeAt(0),
11 | squote: "'".charCodeAt(0),
12 | equal: '='.charCodeAt(0)
13 | }
14 |
15 | var strings = {
16 | endScript: Buffer.from(''),
21 | cdata: Buffer.from('')
23 | }
24 |
25 | var states = {
26 | 'TagNameState': 1,
27 | 'AttributeNameState': 2,
28 | 'BeforeAttributeValueState': 3,
29 | 'AttributeValueState': 4
30 | }
31 |
32 | function tokenize (str) {
33 | var tokenizer = new Tokenize()
34 | return tokenizer.parse(str)
35 | }
36 |
37 | function Tokenize () {
38 | this.state = 'text'
39 | this.tagState = null
40 | this.quoteState = null
41 | this.raw = null
42 | this.buffers = []
43 | this._last = []
44 | this._result = []
45 | }
46 |
47 | Tokenize.prototype.parse = function (str) {
48 | if (typeof str === 'string') str = Buffer.from(str)
49 | this._parse(str)
50 | return this._result
51 | }
52 |
53 | Tokenize.prototype._parse = function (buf) {
54 | var i = 0
55 | var offset = 0
56 |
57 | if (this._prev) {
58 | buf = Buffer.concat([ this._prev, buf ])
59 | i = this._prev.length - 1
60 | offset = this._offset
61 | this._prev = null
62 | this._offset = 0
63 | }
64 |
65 | for (; i < buf.length; i++) {
66 | var b = buf[i]
67 | this._last.push(b)
68 | if (this._last.length > 9) this._last.shift()
69 | // detect end of raw character mode (comment, script,..)
70 | if (this.raw) {
71 | var parts = this._testRaw(buf, offset, i)
72 | if (parts) {
73 | this._last.push([ 'text', parts[0] ])
74 |
75 | if (this.raw === strings.endComment ||
76 | this.raw === strings.endCdata) {
77 | this.state = 'text'
78 | this.buffers = []
79 | this._last.push([ 'close', parts[1] ])
80 | } else {
81 | this.state = 'open'
82 | this.buffers = [ parts[1] ]
83 | }
84 |
85 | this.raw = null
86 | offset = i + 1
87 | }
88 | // ask for more data if last byte is '<'
89 | } else if (this.state === 'text' && b === codes.lt &&
90 | i === buf.length - 1) {
91 | this._prev = buf
92 | this._offset = offset
93 | return
94 | // detect a tag opening
95 | } else if (this.state === 'text' && b === codes.lt &&
96 | !isWhiteSpace(buf[i + 1])) {
97 | if (i > 0 && i - offset > 0) {
98 | this.buffers.push(buf.slice(offset, i))
99 | }
100 | offset = i
101 | this.state = 'open'
102 | this.tagState = states.TagNameState
103 | this._pushState('text')
104 | } else if (
105 | this.tagState === states.TagNameState &&
106 | isWhiteSpace(b)
107 | ) {
108 | this.tagState = states.AttributeNameState
109 | } else if (
110 | this.tagState === states.AttributeNameState &&
111 | b === codes.equal
112 | ) {
113 | this.tagState = states.BeforeAttributeValueState
114 | } else if (
115 | this.tagState === states.BeforeAttributeValueState &&
116 | isWhiteSpace(b)
117 | ) {} else if (
118 | this.tagState === states.BeforeAttributeValueState &&
119 | b !== codes.gt
120 | ) {
121 | this.tagState = states.AttributeValueState
122 | if (b === codes.dquote) {
123 | this.quoteState = 'double'
124 | } else if (b === codes.squote) {
125 | this.quoteState = 'single'
126 | } else {
127 | this.quoteState = null
128 | }
129 | } else if (
130 | this.tagState === states.AttributeValueState &&
131 | !this.quoteState &&
132 | isWhiteSpace(b)
133 | ) {
134 | this.tagState = states.AttributeNameState
135 | } else if (
136 | this.tagState === states.AttributeValueState &&
137 | this.quoteState === 'double' &&
138 | b === codes.dquote
139 | ) {
140 | this.quoteState = null
141 | this.tagState = states.AttributeNameState
142 | } else if (
143 | this.tagState === states.AttributeValueState &&
144 | this.quoteState === 'single' &&
145 | b === codes.squote
146 | ) {
147 | this.quoteState = null
148 | this.tagState = states.AttributeNameState
149 | } else if (this.state === 'open' && b === codes.gt && !this.quoteState) {
150 | this.buffers.push(buf.slice(offset, i + 1))
151 | offset = i + 1
152 | this.state = 'text'
153 | this.tagState = null
154 | if (this._getChar(1) === codes.slash) {
155 | this._pushState('close')
156 | } else {
157 | var tag = this._getTag()
158 | if (tag === 'script') this.raw = strings.endScript
159 | if (tag === 'style') this.raw = strings.endStyle
160 | if (tag === 'title') this.raw = strings.endTitle
161 | this._pushState('open')
162 | }
163 | } else if (this.state === 'open' && compare(this._last, strings.comment)) {
164 | this.buffers.push(buf.slice(offset, i + 1))
165 | offset = i + 1
166 | this.state = 'text'
167 | this.raw = strings.endComment
168 | this._pushState('open')
169 | } else if (this.state === 'open' && compare(this._last, strings.cdata)) {
170 | this.buffers.push(buf.slice(offset, i + 1))
171 | offset = i + 1
172 | this.state = 'text'
173 | this.raw = strings.endCdata
174 | this._pushState('open')
175 | }
176 | }
177 | if (offset < buf.length) this.buffers.push(buf.slice(offset))
178 | }
179 |
180 | Tokenize.prototype._flush = function (next) {
181 | if (this.state === 'text') this._pushState('text')
182 | }
183 |
184 | Tokenize.prototype._pushState = function (ev) {
185 | if (this.buffers.length === 0) return
186 | var buf = Buffer.concat(this.buffers)
187 | this.buffers = []
188 | this._result.push([ ev, buf ])
189 | }
190 |
191 | Tokenize.prototype._getChar = function (i) {
192 | var offset = 0
193 | for (var j = 0; j < this.buffers.length; j++) {
194 | var buf = this.buffers[j]
195 | if (offset + buf.length > i) {
196 | return buf[i - offset]
197 | }
198 | offset += buf
199 | }
200 | }
201 |
202 | Tokenize.prototype._getTag = function () {
203 | var offset = 0
204 | var tag = ''
205 | for (var j = 0; j < this.buffers.length; j++) {
206 | var buf = this.buffers[j]
207 | for (var k = 0; k < buf.length; k++) {
208 | if (offset === 0 && k === 0) continue
209 | var c = String.fromCharCode(buf[k])
210 | if (/[^\w-!\[\]]/.test(c)) { // eslint-disable-line no-useless-escape
211 | return tag.toLowerCase()
212 | } else tag += c
213 | }
214 | offset += buf.length
215 | }
216 | }
217 |
218 | Tokenize.prototype._testRaw = function (buf, offset, index) {
219 | var raw = this.raw
220 | var last = this._last
221 | if (!compare(last, raw)) return
222 |
223 | this.buffers.push(buf.slice(offset, index + 1))
224 | buf = Buffer.concat(this.buffers)
225 | var k = buf.length - raw.length
226 | return [ buf.slice(0, k), buf.slice(k) ]
227 | }
228 |
229 | function compare (a, b) {
230 | if (a.length < b.length) return false
231 | for (var i = a.length - 1, j = b.length - 1; i >= 0 && j >= 0; i--, j--) {
232 | if (lower(a[i]) !== lower(b[j])) return false
233 | }
234 | return true
235 | }
236 |
237 | function lower (n) {
238 | if (n >= 65 && n <= 90) return n + 32
239 | return n
240 | }
241 |
242 | function isWhiteSpace (b) {
243 | return b === 0x20 || b === 0x09 || b === 0x0A || b === 0x0C || b === 0x0D
244 | }
245 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "assert-html",
3 | "description": "Assert two HTML strings are equal",
4 | "repository": "yoshuawuyts/assert-html",
5 | "version": "1.1.5",
6 | "scripts": {
7 | "deps": "dependency-check . && dependency-check . --extra --no-dev",
8 | "start": "node .",
9 | "test": "standard && npm run deps && node test"
10 | },
11 | "dependencies": {
12 | "shallow-equal": "^1.0.0"
13 | },
14 | "devDependencies": {
15 | "bel": "^5.0.0",
16 | "dependency-check": "^2.8.0",
17 | "standard": "^10.0.2",
18 | "tape": "^4.6.3"
19 | },
20 | "keywords": [
21 | "assert",
22 | "html",
23 | "node"
24 | ],
25 | "license": "MIT"
26 | }
27 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var tape = require('tape')
3 | var html = require('bel')
4 |
5 | var assertHtml = require('./')
6 |
7 | tape('compare two DOM strings: OK', function (_assert) {
8 | var a, b
9 |
10 | a = html`hello planet
`.toString()
11 | b = html`hello planet
`.toString()
12 |
13 | _assert.doesNotThrow(function () {
14 | assertHtml(assert, a, b)
15 | })
16 |
17 | _assert.end()
18 | })
19 |
20 | tape('compare two DOM strings: NOT OK', function (_assert) {
21 | var a, b
22 |
23 | a = html`hello planet
`.toString()
24 | b = html`earth
`.toString()
25 |
26 | _assert.throws(function () {
27 | assertHtml(assert, a, b)
28 | })
29 |
30 | _assert.end()
31 | })
32 |
33 | tape('allow random order', function (_assert) {
34 | var a, b
35 | a = html`hello planet
`.toString()
36 | b = html`hello planet
`.toString()
37 |
38 | _assert.doesNotThrow(function () {
39 | assertHtml(assert, a, b)
40 | })
41 |
42 | _assert.end()
43 | })
44 |
45 | tape('allow attributes with only keys', function (_assert) {
46 | var a, b
47 |
48 | a = html`hello planet
`.toString()
49 | b = html`hello planet
`.toString()
50 |
51 | _assert.doesNotThrow(function () {
52 | assertHtml(assert, a, b)
53 | })
54 |
55 | _assert.end()
56 | })
57 |
58 | tape('allow title tag', function (_assert) {
59 | var a, b
60 |
61 | a = html`Hi hello how are you?`.toString()
62 | b = html`Hi hello how are you?`.toString()
63 |
64 | _assert.doesNotThrow(function () {
65 | assertHtml(assert, a, b)
66 | })
67 |
68 | _assert.end()
69 | })
70 |
--------------------------------------------------------------------------------