• 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.71
/packages/diffhtml/lib/tree/create.js
1
/**
2✔
2
 * @typedef {import('../util/types').ValidInput} ValidInput
2✔
3
 * @typedef {import('../util/types').VTree} VTree
2✔
4
 * @typedef {import('../util/types').VTreeLike} VTreeLike
2✔
5
 */
2✔
6
import Pool from '../util/pool';
2✔
7
import {
2✔
8
  EMPTY,
2✔
9
  NODE_TYPE,
2✔
10
  NodeCache,
2✔
11
  CreateTreeHookCache,
2✔
12
} from '../util/types';
2✔
13

2✔
14
const { isArray } = Array;
2✔
15
const { memory } = Pool;
2✔
16
const fragmentName = '#document-fragment';
2✔
17
const textName = '#text';
2✔
18

2✔
19
/**
2✔
20
 * Will flatten fragments that should remain invisible. If a fragment was
2✔
21
 * intentionally created to be diffed, such as a component, the rawNodeName
2✔
22
 * will not match and will be preserved.
2✔
23
 *
2✔
24
 * @param {VTree[]} vTrees
2✔
25
 * @param {VTree[]} retVal
2✔
26
 *
2✔
27
 * @return {VTree[]}
2✔
28
 */
2✔
29
function flatten(vTrees, retVal = []) {
8,191✔
30
  for (let i = 0; i < vTrees.length; i++) {
8,191✔
31
    const vTree = vTrees[i];
7,364✔
32

7,364✔
33
    if (vTree && vTree.rawNodeName === fragmentName) {
7,364✔
34
      flatten(vTree.childNodes, retVal);
2✔
35
    }
2✔
36
    else if (vTree) {
7,362✔
37
      retVal.push(vTree);
2,004✔
38
    }
2,004✔
39
  }
7,364✔
40

8,191✔
41
  return retVal;
8,191✔
42
}
8,191✔
43

2✔
44
/**
2✔
45
 * Typically passed either a single or list of DOM Nodes or a VTreeLike object.
2✔
46
 *
2✔
47
 * @param {ValidInput=} input
2✔
48
 * @param {any=} attributes
2✔
49
 * @param {any=} childNodes
2✔
50
 * @param  {...any} rest
2✔
51
 *
2✔
52
 * @return {VTree}
2✔
53
 */
2✔
54
export default function createTree(input, attributes, childNodes, ...rest) {
2✔
55
  /** @type {VTree | null} */
11,986✔
56
  let entry = null;
11,986✔
57

11,986✔
58
  // Reuse a VTree if it has already been created and in the pool. This is an
11,986✔
59
  // optimization and reliability check to ensure repeated calls to this
11,986✔
60
  // function with the same reference produces consistent results.
11,986✔
61
  if (memory.protected.has(input) || memory.allocated.has(input)) {
11,986✔
62
    entry = /** @type {VTree } */ (input);
2,186✔
63
  }
2,186✔
64

9,800✔
65
  // A fragment is used whenever an array is passed directly into createTree.
9,800✔
66
  // This is also what is returned when no input is passed.
9,800✔
67
  else if (!input || isArray(input)) {
9,800✔
68
    const length = input ? input.length : 0;
506✔
69

506✔
70
    childNodes = [];
506✔
71

506✔
72
    // When using an Array copy the Nodes in and ensure a valid top-level tree.
506✔
73
    for (let i = 0; i < length; i++) {
506✔
74
      const hasInput = input && !input[i];
616✔
75
      if (hasInput) continue;
616✔
76
      input && childNodes.push(input[i]);
616✔
77
    }
616✔
78

506✔
79
    entry = createTree(fragmentName, null, childNodes);
506✔
80
  }
506✔
81

11,986✔
82
  // If a return value was found, end this function early.
11,986✔
83
  if (entry) {
11,986✔
84
    return entry;
2,692✔
85
  }
2,692✔
86

9,294✔
87
  const isObject = typeof input === 'object';
9,294✔
88
  const inputAsHTMLEl = /** @type {HTMLElement} */ (input);
9,294✔
89

9,294✔
90
  // If the input passed has an 'ownerDocument' property, then assume it is a
9,294✔
91
  // DOM-like Node object. This means we need to synchronize the DOM and this is
9,294✔
92
  // an expensive operation.
9,294✔
93
  if (input && isObject && 'ownerDocument' in inputAsHTMLEl) {
11,986✔
94
    const { nodeType } = inputAsHTMLEl;
340✔
95

340✔
96
    // When passed a text node, simply migrate the value over into the new VTree
340✔
97
    // associate in the NodeCache.
340✔
98
    if (nodeType === NODE_TYPE.TEXT) {
340✔
99
      const vTree = createTree(textName, inputAsHTMLEl.nodeValue);
38✔
100
      NodeCache.set(vTree, inputAsHTMLEl);
38✔
101
      return vTree;
38✔
102
    }
38✔
103

302✔
104
    attributes = {};
302✔
105
    childNodes = [];
302✔
106

302✔
107
    const inputAttrs = inputAsHTMLEl.attributes;
302✔
108

302✔
109
    // We only scrape attributes from element nodes if they are available.
302✔
110
    if (inputAsHTMLEl.nodeType === NODE_TYPE.ELEMENT && inputAttrs && inputAttrs.length) {
340✔
111
      for (let i = 0; i < inputAttrs.length; i++) {
27✔
112
        const { name, value } = inputAttrs[i];
27✔
113

27✔
114
        // If the attribute's value is empty, seek out the property instead.
27✔
115
        if (value === EMPTY.STR && name in inputAsHTMLEl) {
27✔
116
          attributes[name] = /** @type {any} */ (input)[name];
2✔
117
          continue;
2✔
118
        }
2✔
119

25✔
120
        attributes[name] = value;
25✔
121
      }
25✔
122
    }
27✔
123

302✔
124
    // Get the child nodes from an Element or Fragment/Shadow Root.
302✔
125
    if (inputAsHTMLEl.nodeType === NODE_TYPE.ELEMENT || inputAsHTMLEl.nodeType === NODE_TYPE.FRAGMENT) {
340✔
126
      childNodes = [];
301✔
127

301✔
128
      for (let i = 0; i < inputAsHTMLEl.childNodes.length; i++) {
301✔
129
        /** @type {ValidInput} */
160✔
130
        const childNodeElement = (inputAsHTMLEl.childNodes[i]);
160✔
131
        childNodes.push(createTree(childNodeElement));
160✔
132
      }
160✔
133
    }
301✔
134

302✔
135
    // FIXME This is going to hurt performance. Is there a better way to find a
302✔
136
    // VTree from a DOM Node?
302✔
137
    NodeCache.forEach((domNode, vTree) => {
302✔
138
      if (domNode === input) {
338✔
139
        entry = vTree;
17✔
140
      }
17✔
141
    });
302✔
142

302✔
143
    /**
302✔
144
     * If no VTree was previously bound this to DOM Node, create a brand new
302✔
145
     * tree.
302✔
146
     *
302✔
147
     * @type {VTree} */
302✔
148
    entry = entry || createTree(
340✔
149
      inputAsHTMLEl.nodeName,
285✔
150
      attributes,
285✔
151
      childNodes,
285✔
152
    );
340✔
153

340✔
154
    entry.attributes = { ...entry.attributes, ...attributes };
340✔
155

340✔
156
    // Use childNodes that are passed in, otherwise keep the existing nodes.
340✔
157
    entry.childNodes = childNodes;
340✔
158

340✔
159
    NodeCache.set(entry, inputAsHTMLEl);
340✔
160
    return entry;
340✔
161
  }
340✔
162

8,954✔
163
  // Assume any remaining objects are VTree-like.
8,954✔
164
  if (isObject && !attributes) {
11,986✔
165
    /** @type {VTreeLike} */
765✔
166
    const {
765✔
167
      rawNodeName,
765✔
168
      nodeName,
765✔
169
      nodeValue,
765✔
170
      attributes,
765✔
171
      childNodes,
765✔
172
      children,
765✔
173
    } = (/** @type {any} */(input));
765✔
174

765✔
175
    const treeName = rawNodeName || nodeName;
765✔
176

765✔
177
    // The priority of a VTreeLike input is nodeValue above all else. If this
765✔
178
    // value is present, we assume a text-based element and that the intentions
765✔
179
    // are setting the children to this value.
765✔
180
    const vTree = createTree(
765✔
181
      treeName,
765✔
182
      attributes || null,
765✔
183
      children || childNodes,
765✔
184
    );
765✔
185

765✔
186
    // Ensure nodeValue is properly copied over.
765✔
187
    if (nodeValue) {
765✔
188
      vTree.nodeValue = nodeValue;
7✔
189
    }
7✔
190

765✔
191
    return vTree;
765✔
192
  }
765✔
193

8,189✔
194
  // Support JSX-style children being passed.
8,189✔
195
  if (rest.length) {
11,986✔
196
    childNodes = [childNodes, ...rest];
1✔
197
  }
1✔
198

8,189✔
199
  // Allocate a new VTree from the pool.
8,189✔
200
  entry = Pool.get();
8,189✔
201

8,189✔
202
  const isTextNode = input === textName;
8,189✔
203
  const isString = typeof input === 'string';
8,189✔
204

8,189✔
205
  // This is a standard HTML element.
8,189✔
206
  if (isString) {
11,986✔
207
    entry.rawNodeName = input;
8,181✔
208
    entry.nodeName = entry.rawNodeName.toLowerCase();
8,181✔
209
  }
8,181✔
210
  // Otherwise treat this as a fragment, since we have no idea what type of
8✔
211
  // element it is.
8✔
212
  else {
8✔
213
    entry.rawNodeName = input;
8✔
214
    entry.nodeName = fragmentName;
8✔
215
  }
8✔
216

8,189✔
217
  // Clear out and reset the remaining VTree attributes.
8,189✔
218
  entry.nodeValue = EMPTY.STR;
8,189✔
219
  entry.key = EMPTY.STR;
8,189✔
220
  entry.childNodes.length = 0;
8,189✔
221
  entry.attributes = {};
8,189✔
222

8,189✔
223
  // Were childNodes passed as attributes? If so, use the attributes parameter
8,189✔
224
  // instead.
8,189✔
225
  const useAttributes = isArray(attributes) || typeof attributes !== 'object';
11,986✔
226
  const useNodes = useAttributes ? attributes : childNodes;
11,986✔
227
  const allNodes = flatten(isArray(useNodes) ? useNodes : [useNodes]);
11,986✔
228

11,986✔
229
  // Ensure nodeType is set correctly, and if this is a text node, return early.
11,986✔
230
  if (isTextNode) {
11,986✔
231
    const nodeValue = allNodes.join(EMPTY.STR);
744✔
232

744✔
233
    entry.nodeType = NODE_TYPE.TEXT;
744✔
234
    entry.nodeValue = String(nodeValue);
744✔
235

744✔
236
    return entry;
744✔
237
  }
744✔
238
  else if (entry.nodeName === fragmentName) {
7,445✔
239
    entry.nodeType = NODE_TYPE.FRAGMENT;
1,111✔
240
  }
1,111✔
241
  else if (input === '#comment') {
6,334✔
242
    entry.nodeType = NODE_TYPE.COMMENT;
17✔
243
  }
17✔
244
  else {
6,317✔
245
    entry.nodeType = NODE_TYPE.ELEMENT;
6,317✔
246
  }
6,317✔
247

7,445✔
248
  // Parse out the child nodes if they exist and are not overwritten by an
7,445✔
249
  // attribute.
7,445✔
250
  if (useNodes && allNodes.length && (!attributes || !attributes.childNodes)) {
11,986✔
251
    for (let i = 0; i < allNodes.length; i++) {
1,113✔
252
      const newNode = allNodes[i];
1,478✔
253

1,478✔
254
      // Merge in arrays.
1,478✔
255
      if (isArray(newNode)) {
1,478✔
256
        entry.childNodes.push(...newNode);
1✔
257
      }
1✔
258
      // Skip over `null` nodes.
1,477✔
259
      else if (!newNode) {
1,477!
260
        continue;
×
261
      }
×
262
      // Merge in true fragments, but not components or unknowns.
1,477✔
263
      else if (newNode.nodeType === NODE_TYPE.FRAGMENT && typeof newNode.rawNodeName === 'string') {
1,477!
264
        entry.childNodes.push(...newNode.childNodes);
×
265
      }
×
266
      // Assume objects are vTrees.
1,477✔
267
      else if (newNode && typeof newNode === 'object') {
1,477✔
268
        entry.childNodes.push(createTree(newNode));
1,470✔
269
      }
1,470✔
270
      // Last resort treat as text.
7✔
271
      else {
7✔
272
        entry.childNodes.push(createTree(textName, null, newNode));
7✔
273
      }
7✔
274
    }
1,478✔
275
  }
1,113✔
276

7,445✔
277
  if (attributes && typeof attributes === 'object' && !isArray(attributes)) {
11,986✔
278
    entry.attributes = { ...attributes };
1,224✔
279

1,224✔
280
    // Use childNodes directly from the attributes.
1,224✔
281
    if (attributes.childNodes) {
1,224✔
282
      const isObject = typeof attributes.childNodes === 'object';
3✔
283
      entry.childNodes.push(isObject ? createTree(attributes.childNodes) : createTree(textName, attributes.childNodes));
3!
284
    }
3✔
285
  }
1,224✔
286

7,445✔
287
  // If is a script tag and has a src attribute, key off that. We have a special
7,445✔
288
  // handling of scripts to avoid accidentally re-executing a script when
7,445✔
289
  // shifting position.
7,445✔
290
  if (entry.nodeName === 'script' && entry.attributes.src) {
11,986✔
291
    entry.key = String(entry.attributes.src);
2✔
292
  }
2✔
293

7,445✔
294
  // Set the `key` prop if passed as an attr, overrides `script[src]`.
7,445✔
295
  if (entry.attributes && 'key' in entry.attributes) {
11,986✔
296
    entry.key = String(entry.attributes.key);
64✔
297
  }
64✔
298

7,445✔
299
  // Only run the `forEach` if there are hooks to run.
7,445✔
300
  if (CreateTreeHookCache.size) {
11,986✔
301
    CreateTreeHookCache.forEach((fn, retVal) => {
197✔
302
      // If the hook returns a value, replace the active entry.
197✔
303
      if (retVal = fn(entry)) {
197✔
304
        entry = createTree(retVal);
2✔
305
      }
2✔
306
    });
197✔
307
  }
197✔
308

7,445✔
309
  return entry;
7,445✔
310
}
11,986✔
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