1 |
|
2 |
|
3 | /**
|
4 | * # API
|
5 | *
|
6 | * @author Ivan Voischev (@voischev),
|
7 | * Anton Winogradov (@awinogradov),
|
8 | * Alexej Yaroshevich (@zxqfox),
|
9 | * Vasiliy (@Yeti-or)
|
10 | *
|
11 | * @namespace tree
|
12 | */
|
13 | function Api () {
|
14 | this.walk = walk
|
15 | this.match = match
|
16 | }
|
17 |
|
18 | /**
|
19 | * Walks the tree and passes all nodes via a callback
|
20 | *
|
21 | * @memberof tree
|
22 | *
|
23 | * @param {Function} cb Callback
|
24 | * @return {Function} Callback(node)
|
25 | *
|
26 | *@example
|
27 | * ```js
|
28 | * export const walk = (tree) => {
|
29 | * tree.walk((node) => {
|
30 | * let classes = node.attrs && node.attrs.class.split(' ') || []
|
31 | *
|
32 | * if (classes.includes(className)) return cb(node)
|
33 | * return node
|
34 | * })
|
35 | * }
|
36 | * ```
|
37 | */
|
38 | function walk (cb) {
|
39 | return traverse(this, cb)
|
40 | }
|
41 |
|
42 | /**
|
43 | * Matches an expression to search for nodes in the tree
|
44 | *
|
45 | * @memberof tree
|
46 | *
|
47 | * @param {String|RegExp|Object|Array} expression - Matcher(s) to search
|
48 | * @param {Function} cb Callback
|
49 | *
|
50 | * @return {Function} Callback(node)
|
51 | *
|
52 | * @example
|
53 | * ```js
|
54 | * export const match = (tree) => {
|
55 | * // Single matcher
|
56 | * tree.match({ tag: 'custom-tag' }, (node) => {
|
57 | * let tag = node.tag
|
58 | *
|
59 | * Object.assign(node, { tag: 'div', attrs: {class: tag} })
|
60 | *
|
61 | * return node
|
62 | * })
|
63 | * // Multiple matchers
|
64 | * tree.match([{ tag: 'b' }, { tag: 'strong' }], (node) => {
|
65 | * let style = 'font-weight: bold;'
|
66 | *
|
67 | * node.tag = 'span'
|
68 | *
|
69 | * node.attrs
|
70 | * ? ( node.attrs.style
|
71 | * ? ( node.attrs.style += style )
|
72 | * : node.attrs.style = style
|
73 | * )
|
74 | * : node.attrs = { style: style }
|
75 | *
|
76 | * return node
|
77 | * })
|
78 | * }
|
79 | * ```
|
80 | */
|
81 | function match (expression, cb) {
|
82 | return Array.isArray(expression)
|
83 | ? traverse(this, node => {
|
84 | for (let i = 0; i < expression.length; i++) {
|
85 | if (compare(expression[i], node)) return cb(node)
|
86 | }
|
87 |
|
88 | return node
|
89 | })
|
90 | : traverse(this, node => {
|
91 | if (compare(expression, node)) return cb(node)
|
92 |
|
93 | return node
|
94 | })
|
95 | }
|
96 |
|
97 | module.exports = Api
|
98 | module.exports.match = match
|
99 | module.exports.walk = walk
|
100 |
|
101 | /** @private */
|
102 | function traverse (tree, cb) {
|
103 | if (Array.isArray(tree)) {
|
104 | for (let i = 0; i < tree.length; i++) {
|
105 | tree[i] = traverse(cb(tree[i]), cb)
|
106 | }
|
107 | } else if (
|
108 | tree &&
|
109 | typeof tree === 'object' &&
|
110 | Object.prototype.hasOwnProperty.call(tree, 'content')
|
111 | ) traverse(tree.content, cb)
|
112 |
|
113 | return tree
|
114 | }
|
115 |
|
116 | /** @private */
|
117 | function compare (expected, actual) {
|
118 | if (expected instanceof RegExp) {
|
119 | if (typeof actual === 'object') return false
|
120 | if (typeof actual === 'string') return expected.test(actual)
|
121 | }
|
122 |
|
123 | if (typeof expected !== typeof actual) return false
|
124 | if (typeof expected !== 'object' || expected === null) {
|
125 | return expected === actual
|
126 | }
|
127 |
|
128 | if (Array.isArray(expected)) {
|
129 | return expected.every(exp => [].some.call(actual, act => compare(exp, act)))
|
130 | }
|
131 |
|
132 | return Object.keys(expected).every(key => {
|
133 | const ao = actual[key]
|
134 | const eo = expected[key]
|
135 |
|
136 | if (typeof eo === 'object' && eo !== null && ao !== null) {
|
137 | return compare(eo, ao)
|
138 | }
|
139 | if (typeof eo === 'boolean') {
|
140 | return eo !== (ao == null)
|
141 | }
|
142 |
|
143 | return ao === eo
|
144 | })
|
145 | }
|