• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tbranyen / diffhtml / 12189631411

05 Dec 2024 11:09PM CUT coverage: 98.581%. Remained the same
12189631411

Pull #349

github

web-flow
Merge 3ee60590a into f25d1c4ac
Pull Request #349: Bump path-to-regexp and express in /packages/diffhtml-static-sync

840 of 896 branches covered (93.75%)

4795 of 4864 relevant lines covered (98.58%)

427.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.43
/packages/diffhtml/lib/html.js
1
/**
2✔
2
 * @typedef {import('./util/types').VTree} VTree
2✔
3
 * @typedef {import('./util/types').Supplemental} Supplemental
2✔
4
 */
2✔
5
import createTree from './tree/create';
2✔
6
import Internals from './util/internals';
2✔
7
import escape from './util/escape';
2✔
8
import decodeEntities from './util/decode-entities';
2✔
9
import internalProcess from './util/process';
2✔
10
import { EMPTY, NODE_TYPE } from './util/types';
2✔
11

2✔
12
// Magic token used for interpolation.
2✔
13
const TOKEN = '__DIFFHTML__';
2✔
14

2✔
15
const { getOwnPropertyNames } = Object;
2✔
16
const { isArray } = Array;
2✔
17
const tokenEx = new RegExp(`${TOKEN}([^_]*)__`);
2✔
18

2✔
19
// Get the next value from the list. If the next value is a string, make sure
2✔
20
// it is escaped.
2✔
21
const nextValue = (/** @type {any[]} */ values) => {
2✔
22
  const value = values.shift();
106✔
23
  return typeof value === 'string' ? escape(decodeEntities(value)) : value;
106✔
24
};
2✔
25

2✔
26
/**
2✔
27
 * Iterates over the result from the parser and interpolates the supplemental
2✔
28
 * dynamic data into place. This allows parsers to only worry about strings
2✔
29
 * and returning minimal structures.
2✔
30
 *
2✔
31
 * This also flattens fragments.
2✔
32
 *
2✔
33
 * @param {VTree} childNode
2✔
34
 * @param {Supplemental} supplemental
2✔
35
 * @return {VTree}
2✔
36
 */
2✔
37
const interpolateAndFlatten = (childNode, supplemental) => {
2✔
38
  let match = null;
425✔
39
  let newNodeName = childNode.rawNodeName;
425✔
40

425✔
41
  // Comments
425✔
42
  if (childNode.nodeType === NODE_TYPE.COMMENT) {
425✔
43
    const parts = childNode.nodeValue.split(tokenEx);
2✔
44
    let newValue = '';
2✔
45

2✔
46
    for (let i = 0; i < parts.length; i++) {
2✔
47
      const isDynamic = i % 2 !== 0;
4✔
48

4✔
49
      if (isDynamic) {
4✔
50
        newValue += supplemental.attributes[parts[i]];
1✔
51
      }
1✔
52
      else {
3✔
53
        newValue += parts[i];
3✔
54
      }
3✔
55
    }
4✔
56

2✔
57
    childNode.nodeValue = newValue;
2✔
58
  }
2✔
59

425✔
60
  // Node name
425✔
61
  if (match = tokenEx.exec(childNode.rawNodeName)) {
425✔
62
    newNodeName = supplemental.tags[match[1]];
9✔
63

9✔
64
    childNode = createTree(
9✔
65
      newNodeName,
9✔
66
      childNode.attributes,
9✔
67
      childNode.childNodes,
9✔
68
    );
9✔
69
  }
9✔
70

425✔
71
  // Attributes
425✔
72
  for (const keyName of getOwnPropertyNames(childNode.attributes)) {
425✔
73
    keyName.split(' ').forEach(keyName => {
59✔
74
      const value = childNode.attributes[keyName];
59✔
75
      let newValue = value;
59✔
76
      let newKey = keyName;
59✔
77

59✔
78
      // Check for dynamic value and assign to newValue.
59✔
79
      if (match = tokenEx.exec(value)) {
59✔
80
        const parts = value.split(tokenEx);
15✔
81
        newValue = '';
15✔
82

15✔
83
        // A value is always split between two other elements.
15✔
84
        if (parts.length === 3 && parts[0] === EMPTY.STR && parts[2] === EMPTY.STR) {
15✔
85
          newValue = supplemental.attributes[parts[1]];
10✔
86
        }
10✔
87
        else {
5✔
88
          for (let i = 0; i < parts.length; i++) {
5✔
89
            const isDynamic = i % 2 !== 0;
23✔
90

23✔
91
            if (isDynamic) {
23✔
92
              newValue += supplemental.attributes[parts[i]];
9✔
93
            }
9✔
94
            else {
14✔
95
              newValue += parts[i];
14✔
96
            }
14✔
97
          }
23✔
98
        }
5✔
99
      }
15✔
100

59✔
101
      // Check for dynamic key and assign to newKey.
59✔
102
      if (match = tokenEx.exec(keyName)) {
59✔
103
        const parts = keyName.split(tokenEx);
11✔
104

11✔
105
        for (let i = 0; i < parts.length; i++) {
11✔
106
          if (i % 2 !== 0) {
33✔
107
            newKey = supplemental.attributes[parts[i]];
11✔
108
          }
11✔
109
        }
33✔
110
      }
11✔
111

59✔
112
      if (newKey) {
59✔
113
        if (typeof newKey !== 'string') {
58✔
114
          if (Array.isArray(newKey)) {
6✔
115
            if (internalProcess.env.NODE_ENV !== 'production') {
2✔
116
              throw new Error('Arrays cannot be spread as attributes');
1✔
117
            }
1✔
118

1✔
119
            delete childNode.attributes[keyName];
1✔
120
          }
1✔
121
          else {
4✔
122
            delete childNode.attributes[keyName];
4✔
123
            Object.assign(childNode.attributes, newKey);
4✔
124
          }
4✔
125
        }
6✔
126
        else {
52✔
127
          delete childNode.attributes[keyName];
52✔
128

52✔
129
          if (newKey === 'childNodes') {
52✔
130
            childNode.childNodes.length = 0;
2✔
131
            if (typeof newValue !== 'string') {
2✔
132
              childNode.childNodes.push(createTree(newValue));
2✔
133
            }
2✔
134
            else {
×
135
              childNode.childNodes.push(createTree('#text', newValue));
×
136
            }
×
137
          }
2✔
138

52✔
139
          childNode.attributes[newKey] = newValue === undefined ? true : newValue;
52!
140
        }
52✔
141
      }
58✔
142

58✔
143
      if (childNode.attributes.key) {
59✔
144
        childNode.key = childNode.attributes.key;
11✔
145
      }
11✔
146

58✔
147
      if (childNode.nodeName === 'script' && childNode.attributes.src) {
59!
148
        childNode.key = childNode.attributes.src;
×
149
      }
×
150
    });
59✔
151
  }
59✔
152

424✔
153
  // Node value
424✔
154
  if (match = tokenEx.exec(childNode.nodeValue)) {
425✔
155
    const parts = childNode.nodeValue.split(tokenEx);
45✔
156
    const fragment = createTree();
45✔
157

45✔
158
    for (let i = 0; i < parts.length; i++) {
45✔
159
      if (i % 2 !== 0) {
137✔
160
        fragment.childNodes.push(
46✔
161
          createTree(supplemental.children[parts[i]])
46✔
162
        );
46✔
163
      }
46✔
164
      else if (parts[i]) {
91✔
165
        fragment.childNodes.push(createTree('#text', parts[i]));
8✔
166
      }
8✔
167
    }
137✔
168

45✔
169
    return fragment;
45✔
170
  }
45✔
171

379✔
172
  // Children
379✔
173
  for (let i = 0; i < childNode.childNodes.length; i++) {
425✔
174
    // Flatten fragments.
333✔
175
    const vTree = interpolateAndFlatten(childNode.childNodes[i], supplemental);
333✔
176

333✔
177
    if (vTree.nodeName === '#document-fragment' && vTree.rawNodeName === vTree.nodeName) {
333✔
178
      childNode.childNodes.splice(i, 1, ...vTree.childNodes);
53✔
179
      i -= 1;
53✔
180
    }
53✔
181
    else {
279✔
182
      childNode.childNodes[i] = vTree;
279✔
183
    }
279✔
184
  }
333✔
185

378✔
186
  return childNode;
378✔
187
};
2✔
188

2✔
189
/**
2✔
190
 * Processes a tagged template, or process a string and interpolate
2✔
191
 * associated values. These values can be of any type and can be
2✔
192
 * put in various parts of the markup, such as tag names, attributes,
2✔
193
 * and node values.
2✔
194
 *
2✔
195
 * @param {string | string[] | TemplateStringsArray} strings
2✔
196
 * @param  {...any} values - test
2✔
197
 * @return {VTree} VTree object or null if no input strings
2✔
198
 */
2✔
199
export default function handleTaggedTemplate(strings, ...values) {
2✔
200
  const empty = createTree('#text', EMPTY.STR);
199✔
201

199✔
202
  // Do not attempt to parse empty strings.
199✔
203
  if (!strings) {
199✔
204
    return empty;
6✔
205
  }
6✔
206

193✔
207
  // If this function is used outside of a tagged template, ensure that flat
193✔
208
  // strings are coerced to arrays, simulating a tagged template call.
193✔
209
  else if (typeof strings === 'string') {
193✔
210
    strings = [strings];
3✔
211
  }
3✔
212

193✔
213
  // Parse only the text, no dynamic bits.
193✔
214
  if (strings.length === 1 && !values.length) {
199✔
215
    if (!strings[0]) {
101✔
216
      return empty;
5✔
217
    }
5✔
218

96✔
219
    let { childNodes } = Internals.parse(strings[0]);
96✔
220

96✔
221
    const startNode = childNodes[0];
96✔
222
    const endNode = childNodes[childNodes.length - 1];
96✔
223
    const isStartText = startNode.nodeType === NODE_TYPE.TEXT;
96✔
224
    const isEndText = endNode.nodeType === NODE_TYPE.TEXT;
96✔
225

96✔
226
    // Trim surrounding text if only one single element was returned.
96✔
227
    if (isStartText || isEndText) {
101✔
228
      /** @type {VTree[]} */
14✔
229
      const trimmedNodes = [].concat(...childNodes);
14✔
230

14✔
231
      for (let i = 0; i < trimmedNodes.length; i++) {
14✔
232
        const node = trimmedNodes[i];
32✔
233

32✔
234
        if (
32✔
235
          (node === startNode || node === endNode) &&
32✔
236
          node.nodeType === NODE_TYPE.TEXT &&
32✔
237
          !node.nodeValue.trim()
25✔
238
        ) {
32✔
239
          trimmedNodes.splice(i, 1);
21✔
240
        }
21✔
241
      }
32✔
242

14✔
243
      if (trimmedNodes.length === 1) {
14✔
244
        childNodes = trimmedNodes;
10✔
245
      }
10✔
246
    }
14✔
247

96✔
248
    return createTree(childNodes.length === 1 ? childNodes[0] : childNodes);
101✔
249
  }
101✔
250

92✔
251
  // Used to store markup and tokens.
92✔
252
  let HTML = EMPTY.STR;
92✔
253

92✔
254
  // We filter the supplemental values by where they are used. Values are
92✔
255
  // either attributes, children, or tags (for components).
92✔
256
  /** @type {Supplemental} */
92✔
257
  const supplemental = ({
92✔
258
    attributes: {},
92✔
259
    children: {},
92✔
260
    tags: {},
92✔
261
   });
92✔
262

92✔
263
  // Loop over the static strings, each break correlates to an interpolated
92✔
264
  // value. As these values can be dynamic, we cannot pass them to the HTML
92✔
265
  // parser inline (it only accepts strings). These dynamic values are indexed
92✔
266
  // in an object called supplemental and keyed by a incremental string token.
92✔
267
  // The following loop instruments the markup with these tokens that the
92✔
268
  // parser then uses to assemble the correct tree.
92✔
269
  strings.forEach((string, i) => {
92✔
270
    // Always add the string, we need it to parse the markup later.
198✔
271
    HTML += string;
198✔
272

198✔
273
    // If there are values, figure out where in the markup they were injected.
198✔
274
    if (values.length) {
198✔
275
      const value = nextValue(values);
106✔
276
      const lastCharacter = HTML.trim().slice(-1);
106✔
277
      const lastTwoCharacters = HTML.trim().slice(-2);
106✔
278
      const isAttribute = HTML.lastIndexOf('>') < HTML.lastIndexOf('<');
106✔
279
      const isTag = Boolean(lastCharacter === '<' || lastTwoCharacters === '</');
106✔
280
      const isObject = typeof value === 'object';
106✔
281
      const token = `${TOKEN}${i}__`;
106✔
282

106✔
283
      // Injected as a tag.
106✔
284
      if (isTag) {
106✔
285
        supplemental.tags[i] = value;
10✔
286
        HTML += token;
10✔
287
      }
10✔
288
      // Injected as attribute.
96✔
289
      else if (isAttribute) {
96✔
290
        supplemental.attributes[i] = value;
31✔
291
        HTML += token;
31✔
292
      }
31✔
293
      // Injected as a child node.
65✔
294
      else if (isArray(value) || isObject) {
65✔
295
        supplemental.children[i] = createTree(value);
46✔
296
        HTML += token;
46✔
297
      }
46✔
298
      // Injected as something else in the markup or undefined, ignore
19✔
299
      // obviously falsy values used with boolean operators.
19✔
300
      else if (value) {
19✔
301
        HTML += value;
18✔
302
      }
18✔
303
    }
106✔
304
  });
92✔
305

92✔
306
  // Parse the instrumented markup to get the Virtual Tree.
92✔
307
  const { childNodes } = Internals.parse(HTML);
92✔
308

92✔
309
  // Pass through flatten and interpolate dynamic data.
92✔
310
  const vTree = interpolateAndFlatten(createTree(childNodes), supplemental);
92✔
311

92✔
312
  // Loop through all nodes and apply the dynamic attributes and flatten
92✔
313
  // fragments.
92✔
314
  if (vTree.nodeType === NODE_TYPE.FRAGMENT && vTree.childNodes.length === 1) {
199✔
315
    return vTree.childNodes[0];
67✔
316
  }
67✔
317

24✔
318
  return vTree;
24✔
319
}
199✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc