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

hybridsjs / hybrids / 9387127968

05 Jun 2024 03:38PM UTC coverage: 46.684% (-53.3%) from 99.956%
9387127968

Pull #258

github

web-flow
Merge e936aa704 into 36d6e398d
Pull Request #258: feat: remove `content` property & add shadow mode detection to render property

611 of 1778 branches covered (34.36%)

69 of 97 new or added lines in 9 files covered. (71.13%)

1169 existing lines in 25 files now uncovered.

1049 of 2247 relevant lines covered (46.68%)

32.63 hits per line

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

48.0
/src/template/core.js
1
import { stringifyElement } from "../utils.js";
2
import { get as getMessage, isLocalizeEnabled } from "../localize.js";
3

4
import * as layout from "./layout.js";
5
import {
6
  getMeta,
7
  getPlaceholder,
8
  getTemplateEnd,
9
  removeTemplate,
10
} from "./utils.js";
11

12
import resolveValue from "./resolvers/value.js";
13
import resolveProperty from "./resolvers/property.js";
14

15
const PLACEHOLDER_REGEXP_TEXT = getPlaceholder("(\\d+)");
2✔
16
const PLACEHOLDER_REGEXP_EQUAL = new RegExp(`^${PLACEHOLDER_REGEXP_TEXT}$`);
2✔
17
const PLACEHOLDER_REGEXP_ALL = new RegExp(PLACEHOLDER_REGEXP_TEXT, "g");
2✔
18
const PLACEHOLDER_REGEXP_ONLY = /^[^A-Za-z]+$/;
2✔
19

20
function createContent(parts) {
21
  let signature = parts[0];
18✔
22
  let tableMode = false;
18✔
23
  for (let index = 1; index < parts.length; index += 1) {
18✔
24
    tableMode =
34✔
25
      tableMode ||
68✔
26
      signature.match(
27
        /<\s*(table|th|tr|td|thead|tbody|tfoot|caption|colgroup)([^<>]|"[^"]*"|'[^']*')*>\s*$/,
28
      );
29

30
    signature +=
34✔
31
      (tableMode
34!
32
        ? `<!--${getPlaceholder(index - 1)}-->`
33
        : getPlaceholder(index - 1)) + parts[index];
34

35
    tableMode =
34✔
36
      tableMode &&
34!
37
      !signature.match(
38
        /<\/\s*(table|th|tr|td|thead|tbody|tfoot|caption|colgroup)\s*>/,
39
      );
40
  }
41

42
  return signature;
18✔
43
}
44

45
function getPropertyName(string) {
46
  return string
32✔
47
    .replace(/\s*=\s*['"]*$/g, "")
48
    .split(/\s+/)
49
    .pop();
50
}
51

52
function createWalker(context) {
53
  return globalThis.document.createTreeWalker(
120✔
54
    context,
55
    globalThis.NodeFilter.SHOW_ELEMENT |
56
      globalThis.NodeFilter.SHOW_TEXT |
57
      globalThis.NodeFilter.SHOW_COMMENT,
58
    null,
59
    false,
60
  );
61
}
62

63
function normalizeWhitespace(input, startIndent = 0) {
×
UNCOV
64
  input = input.replace(/(^[\n\s\t ]+)|([\n\s\t ]+$)+/g, "");
×
65

UNCOV
66
  let i = input.indexOf("\n");
×
UNCOV
67
  if (i > -1) {
×
UNCOV
68
    let indent = 0 - startIndent - 2;
×
UNCOV
69
    for (i += 1; input[i] === " " && i < input.length; i += 1) {
×
UNCOV
70
      indent += 1;
×
71
    }
UNCOV
72
    return input.replace(/\n +/g, (t) =>
×
UNCOV
73
      t.substr(0, Math.max(t.length - indent, 1)),
×
74
    );
75
  }
76

UNCOV
77
  return input;
×
78
}
79

80
function beautifyTemplateLog(input, index) {
UNCOV
81
  const placeholder = getPlaceholder(index);
×
82

UNCOV
83
  const output = normalizeWhitespace(input)
×
84
    .split("\n")
UNCOV
85
    .filter((i) => i)
×
86
    .map((line) => {
UNCOV
87
      const startIndex = line.indexOf(placeholder);
×
88

UNCOV
89
      if (startIndex > -1) {
×
UNCOV
90
        return `| ${line}\n--${"-".repeat(startIndex)}${"^".repeat(6)}`;
×
91
      }
92

UNCOV
93
      return `| ${line}`;
×
94
    })
95
    .join("\n")
96
    .replace(PLACEHOLDER_REGEXP_ALL, "${...}");
97

UNCOV
98
  return `${output}`;
×
99
}
100

101
const styleSheetsMap = new Map();
2✔
102
const prevStylesMap = new WeakMap();
2✔
103
const prevStyleSheetsMap = new WeakMap();
2✔
104
function updateAdoptedStylesheets(target, styles) {
105
  const prevStyles = prevStylesMap.get(target);
12✔
106

107
  if (
12!
108
    (!prevStyles && !styles) ||
24!
109
    (styles?.length &&
110
      prevStyles?.length &&
UNCOV
111
      styles?.every((s, i) => prevStyles[i] === s))
×
112
  ) {
113
    return;
12✔
114
  }
115

UNCOV
116
  let styleSheets = null;
×
UNCOV
117
  if (styles) {
×
UNCOV
118
    styleSheets = [];
×
UNCOV
119
    for (const style of styles) {
×
UNCOV
120
      let styleSheet = style;
×
UNCOV
121
      if (!(styleSheet instanceof globalThis.CSSStyleSheet)) {
×
UNCOV
122
        styleSheet = styleSheetsMap.get(style);
×
UNCOV
123
        if (!styleSheet) {
×
UNCOV
124
          styleSheet = new globalThis.CSSStyleSheet();
×
UNCOV
125
          styleSheet.replaceSync(style);
×
UNCOV
126
          styleSheetsMap.set(style, styleSheet);
×
127
        }
128
      }
UNCOV
129
      styleSheets.push(styleSheet);
×
130
    }
131
  }
132

133
  let adoptedStyleSheets;
UNCOV
134
  const prevStyleSheets = prevStyleSheetsMap.get(target);
×
135

UNCOV
136
  if (prevStyleSheets) {
×
UNCOV
137
    adoptedStyleSheets = [];
×
138

UNCOV
139
    for (const styleSheet of target.adoptedStyleSheets) {
×
UNCOV
140
      if (!prevStyleSheets.includes(styleSheet)) {
×
UNCOV
141
        adoptedStyleSheets.push(styleSheet);
×
142
      }
143
    }
144
  }
145

UNCOV
146
  if (styleSheets) {
×
UNCOV
147
    adoptedStyleSheets =
×
148
      adoptedStyleSheets || target.adoptedStyleSheets.length
×
149
        ? [...target.adoptedStyleSheets]
150
        : [];
151

UNCOV
152
    for (const styleSheet of styleSheets) {
×
UNCOV
153
      adoptedStyleSheets.push(styleSheet);
×
154
    }
155
  }
156

UNCOV
157
  target.adoptedStyleSheets = adoptedStyleSheets;
×
158

UNCOV
159
  prevStylesMap.set(target, styles);
×
UNCOV
160
  prevStyleSheetsMap.set(target, styleSheets);
×
161
}
162

163
const styleElementMap = new WeakMap();
2✔
164
function updateStyleElement(target, styles) {
165
  let styleEl = styleElementMap.get(target);
162✔
166

167
  if (styles) {
162!
UNCOV
168
    const prevStyles = prevStylesMap.get(target);
×
UNCOV
169
    if (prevStyles && styles.every((s, i) => prevStyles[i] === s)) return;
×
170

UNCOV
171
    if (!styleEl || styleEl.parentNode !== target) {
×
UNCOV
172
      styleEl = globalThis.document.createElement("style");
×
UNCOV
173
      styleElementMap.set(target, styleEl);
×
174

UNCOV
175
      target = getTemplateEnd(target);
×
UNCOV
176
      if (target.nodeType === globalThis.Node.TEXT_NODE) {
×
UNCOV
177
        target.parentNode.insertBefore(styleEl, target.nextSibling);
×
178
      } else {
UNCOV
179
        target.appendChild(styleEl);
×
180
      }
181
    }
182

UNCOV
183
    styleEl.textContent = styles.join("\n/*------*/\n");
×
184

UNCOV
185
    prevStylesMap.set(target, styles);
×
186
  } else if (styleEl) {
162!
UNCOV
187
    styleEl.parentNode.removeChild(styleEl);
×
UNCOV
188
    styleElementMap.set(target, null);
×
189
  }
190
}
191

192
export function compileTemplate(rawParts, isSVG, isMsg, useLayout) {
193
  const content = isMsg ? rawParts : createContent(rawParts);
18!
194

195
  let template = globalThis.document.createElement("template");
18✔
196
  template.innerHTML = isSVG ? `<svg>${content}</svg>` : content;
18!
197

198
  if (isSVG) {
18!
UNCOV
199
    const svgRoot = template.content.firstChild;
×
UNCOV
200
    template.content.removeChild(svgRoot);
×
UNCOV
201
    for (const node of Array.from(svgRoot.childNodes)) {
×
UNCOV
202
      template.content.appendChild(node);
×
203
    }
204
  }
205

206
  let hostLayout;
207
  const layoutTemplate = template.content.children[0];
18✔
208
  if (layoutTemplate instanceof globalThis.HTMLTemplateElement) {
18!
UNCOV
209
    for (const attr of Array.from(layoutTemplate.attributes)) {
×
UNCOV
210
      const value = attr.value.trim();
×
UNCOV
211
      if (value && attr.name.startsWith("layout")) {
×
UNCOV
212
        if (value.match(PLACEHOLDER_REGEXP_ALL)) {
×
UNCOV
213
          throw Error("Layout attribute cannot contain expressions");
×
214
        }
215

UNCOV
216
        hostLayout = layout.insertRule(
×
217
          layoutTemplate,
218
          attr.name.substr(6),
219
          value,
220
          true,
221
        );
222
      }
223
    }
224

UNCOV
225
    if (hostLayout !== undefined && template.content.children.length > 1) {
×
UNCOV
226
      throw Error(
×
227
        "Template, which uses layout system must have only the '<template>' root element",
228
      );
229
    }
230

UNCOV
231
    useLayout = hostLayout || layoutTemplate.hasAttribute("layout");
×
UNCOV
232
    template = layoutTemplate;
×
233
  }
234

235
  const compileWalker = createWalker(template.content);
18✔
236
  const parts = {};
18✔
237
  const notDefinedElements = [];
18✔
238

239
  let compileIndex = 0;
18✔
240
  let noTranslate = null;
18✔
241
  let useShadow = false;
18✔
242

243
  while (compileWalker.nextNode()) {
18✔
244
    let node = compileWalker.currentNode;
130✔
245

246
    if (noTranslate && !noTranslate.contains(node)) {
130!
UNCOV
247
      noTranslate = null;
×
248
    }
249

250
    if (node.nodeType === globalThis.Node.COMMENT_NODE) {
130!
UNCOV
251
      if (PLACEHOLDER_REGEXP_EQUAL.test(node.textContent)) {
×
UNCOV
252
        node.parentNode.insertBefore(
×
253
          globalThis.document.createTextNode(node.textContent),
254
          node.nextSibling,
255
        );
256

UNCOV
257
        compileWalker.nextNode();
×
UNCOV
258
        node.parentNode.removeChild(node);
×
UNCOV
259
        node = compileWalker.currentNode;
×
260
      }
261
    }
262

263
    if (node.nodeType === globalThis.Node.TEXT_NODE) {
130✔
264
      let text = node.textContent;
88✔
265
      const equal = text.match(PLACEHOLDER_REGEXP_EQUAL);
88✔
266

267
      if (equal) {
88✔
268
        node.textContent = "";
1✔
269
        parts[equal[1]] = [compileIndex, resolveValue];
1✔
270
      } else {
271
        if (
87!
272
          isLocalizeEnabled() &&
87!
273
          !isMsg &&
274
          !noTranslate &&
275
          !text.match(/^\s*$/)
276
        ) {
277
          let offset;
UNCOV
278
          const key = text.trim();
×
UNCOV
279
          const localizedKey = key
×
280
            .replace(/\s+/g, " ")
281
            .replace(PLACEHOLDER_REGEXP_ALL, (_, index) => {
UNCOV
282
              index = Number(index);
×
UNCOV
283
              if (offset === undefined) offset = index;
×
UNCOV
284
              return `\${${index - offset}}`;
×
285
            });
286

UNCOV
287
          if (!localizedKey.match(PLACEHOLDER_REGEXP_ONLY)) {
×
288
            let context =
UNCOV
289
              node.previousSibling &&
×
290
              node.previousSibling.nodeType === globalThis.Node.COMMENT_NODE
291
                ? node.previousSibling
292
                : "";
UNCOV
293
            if (context) {
×
UNCOV
294
              context.parentNode.removeChild(context);
×
UNCOV
295
              compileIndex -= 1;
×
UNCOV
296
              context = (context.textContent.split("|")[1] || "")
×
297
                .trim()
298
                .replace(/\s+/g, " ");
299
            }
300

UNCOV
301
            const resultKey = getMessage(localizedKey, context).replace(
×
302
              /\${(\d+)}/g,
UNCOV
303
              (_, index) => getPlaceholder(Number(index) + offset),
×
304
            );
305

UNCOV
306
            text = text.replace(key, resultKey);
×
UNCOV
307
            node.textContent = text;
×
308
          }
309
        }
310

311
        const results = text.match(PLACEHOLDER_REGEXP_ALL);
87✔
312
        if (results) {
87✔
313
          let currentNode = node;
1✔
314
          results
1✔
315
            .reduce(
316
              (acc, placeholder) => {
317
                const [before, next] = acc.pop().split(placeholder);
1✔
318
                if (before) acc.push(before);
1!
319
                acc.push(placeholder);
1✔
320
                if (next) acc.push(next);
1!
321
                return acc;
1✔
322
              },
323
              [text],
324
            )
325
            .forEach((part, index) => {
326
              if (index === 0) {
3✔
327
                currentNode.textContent = part;
1✔
328
              } else {
329
                currentNode = currentNode.parentNode.insertBefore(
2✔
330
                  globalThis.document.createTextNode(part),
331
                  currentNode.nextSibling,
332
                );
333

334
                compileWalker.currentNode = currentNode;
2✔
335
                compileIndex += 1;
2✔
336
              }
337

338
              const equal = currentNode.textContent.match(
3✔
339
                PLACEHOLDER_REGEXP_EQUAL,
340
              );
341
              if (equal) {
3✔
342
                currentNode.textContent = "";
1✔
343
                parts[equal[1]] = [compileIndex, resolveValue];
1✔
344
              }
345
            });
346
        }
347
      }
348
    } else {
349
      /* istanbul ignore else */
350
      if (node.nodeType === globalThis.Node.ELEMENT_NODE) {
42✔
351
        if (node.tagName === "STYLE" || node.tagName === "SLOT") {
42✔
352
          useShadow = true;
1✔
353
        }
354

355
        if (
42!
356
          !noTranslate &&
168✔
357
          (node.getAttribute("translate") === "no" ||
358
            node.tagName === "SCRIPT" ||
359
            node.tagName === "STYLE")
360
        ) {
UNCOV
361
          noTranslate = node;
×
362
        }
363

364
        const tagName = node.tagName.toLowerCase();
42✔
365
        if (
42!
366
          tagName.match(/.+-.+/) &&
43!
367
          !globalThis.customElements.get(tagName) &&
368
          !notDefinedElements.includes(tagName)
369
        ) {
NEW
370
          notDefinedElements.push(tagName);
×
371
        }
372

373
        for (const attr of Array.from(node.attributes)) {
42✔
374
          const value = attr.value.trim();
72✔
375
          /* istanbul ignore next */
376
          const name = attr.name;
377

378
          if (useLayout && name.startsWith("layout") && value) {
72!
UNCOV
379
            if (value.match(PLACEHOLDER_REGEXP_ALL)) {
×
UNCOV
380
              throw Error("Layout attribute cannot contain expressions");
×
381
            }
382

UNCOV
383
            const className = layout.insertRule(node, name.substr(6), value);
×
UNCOV
384
            node.removeAttribute(name);
×
UNCOV
385
            node.classList.add(className);
×
386

UNCOV
387
            continue;
×
388
          }
389

390
          const equal = value.match(PLACEHOLDER_REGEXP_EQUAL);
72✔
391
          if (equal) {
72✔
392
            const propertyName = getPropertyName(rawParts[equal[1]]);
32✔
393
            parts[equal[1]] = [
32✔
394
              compileIndex,
395
              resolveProperty(name, propertyName, isSVG),
396
            ];
397
            node.removeAttribute(attr.name);
32✔
398
          } else {
399
            const results = value.match(PLACEHOLDER_REGEXP_ALL);
40✔
400
            if (results) {
40!
UNCOV
401
              const partialName = `attr__${name}`;
×
402

UNCOV
403
              for (const [index, placeholder] of results.entries()) {
×
UNCOV
404
                const [, id] = placeholder.match(PLACEHOLDER_REGEXP_EQUAL);
×
UNCOV
405
                let isProp = false;
×
UNCOV
406
                parts[id] = [
×
407
                  compileIndex,
408
                  (host, target, attrValue) => {
UNCOV
409
                    const meta = getMeta(target);
×
UNCOV
410
                    meta[partialName] = (meta[partialName] || value).replace(
×
411
                      placeholder,
412
                      attrValue == null ? "" : attrValue,
×
413
                    );
414

UNCOV
415
                    if (results.length === 1 || index + 1 === results.length) {
×
UNCOV
416
                      isProp =
×
417
                        isProp ||
×
418
                        (!isSVG &&
419
                          !(target instanceof globalThis.SVGElement) &&
420
                          name in target);
UNCOV
421
                      if (isProp) {
×
UNCOV
422
                        target[name] = meta[partialName];
×
423
                      } else {
UNCOV
424
                        target.setAttribute(name, meta[partialName]);
×
425
                      }
UNCOV
426
                      meta[partialName] = undefined;
×
427
                    }
428
                  },
429
                ];
430
              }
431

UNCOV
432
              attr.value = "";
×
433
            }
434
          }
435
        }
436
      }
437
    }
438

439
    compileIndex += 1;
130✔
440
  }
441

442
  if (notDefinedElements.length) {
18!
UNCOV
443
    console.warn(
×
444
      `Not defined ${notDefinedElements
UNCOV
445
        .map((e) => `<${e}>`)
×
446
        .join(", ")} element${
447
        notDefinedElements.length > 1 ? "s" : ""
×
448
      } found in the template:\n${beautifyTemplateLog(content, -1)}`,
449
    );
450
  }
451

452
  const partsKeys = Object.keys(parts);
18✔
453
  return Object.assign(
18✔
454
    function updateTemplateInstance(host, target, args, styleSheets, shadow) {
455
      let meta = getMeta(target);
174✔
456

457
      if (template !== meta.template) {
174✔
458
        const fragment = globalThis.document.importNode(template.content, true);
102✔
459
        const renderWalker = createWalker(fragment);
102✔
460
        const markers = [];
102✔
461

462
        let renderIndex = 0;
102✔
463
        let keyIndex = 0;
102✔
464
        let currentPart = parts[partsKeys[keyIndex]];
102✔
465

466
        while (renderWalker.nextNode()) {
102✔
467
          const node = renderWalker.currentNode;
770✔
468

469
          while (currentPart && currentPart[0] === renderIndex) {
770✔
470
            markers.push({
180✔
471
              index: partsKeys[keyIndex],
472
              node,
473
              fn: currentPart[1],
474
            });
475
            keyIndex += 1;
180✔
476
            currentPart = parts[partsKeys[keyIndex]];
180✔
477
          }
478

479
          renderIndex += 1;
770✔
480
        }
481

482
        if (meta.hostLayout) {
102!
NEW
483
          host.classList.remove(meta.hostLayout);
×
484
        }
485

486
        removeTemplate(target);
102✔
487

488
        meta = getMeta(target);
102✔
489

490
        meta.template = template;
102✔
491
        meta.markers = markers;
102✔
492

493
        if (target.nodeType === globalThis.Node.TEXT_NODE) {
102!
NEW
494
          updateStyleElement(target);
×
495

NEW
496
          meta.startNode = fragment.childNodes[0];
×
NEW
497
          meta.endNode = fragment.childNodes[fragment.childNodes.length - 1];
×
498

NEW
499
          let previousChild = target;
×
500

NEW
501
          let child = fragment.childNodes[0];
×
NEW
502
          while (child) {
×
NEW
503
            target.parentNode.insertBefore(child, previousChild.nextSibling);
×
NEW
504
            previousChild = child;
×
NEW
505
            child = fragment.childNodes[0];
×
506
          }
507
        } else {
508
          if (useLayout) {
102!
NEW
509
            const className = `${hostLayout}-${host === target ? "c" : "s"}`;
×
NEW
510
            host.classList.add(className);
×
NEW
511
            meta.hostLayout = className;
×
512
          }
513

514
          target.appendChild(fragment);
102✔
515
        }
516

517
        if (useLayout) layout.inject(target);
102!
518
      }
519

520
      if (target.adoptedStyleSheets) {
174✔
521
        updateAdoptedStylesheets(target, styleSheets);
12✔
522
      } else {
523
        updateStyleElement(target, styleSheets);
162✔
524
      }
525

526
      for (const marker of meta.markers) {
174✔
527
        const value = args[marker.index];
394✔
528
        let prevValue = undefined;
394✔
529

530
        if (meta.prevArgs) {
394✔
531
          prevValue = meta.prevArgs[marker.index];
214✔
532
          if (prevValue === value) continue;
214✔
533
        }
534

535
        try {
358✔
536
          marker.fn(host, marker.node, value, prevValue, useLayout, shadow);
358✔
537
        } catch (error) {
NEW
538
          console.error(
×
539
            `Error while updating template expression in ${stringifyElement(
540
              host,
541
            )}:\n${beautifyTemplateLog(content, marker.index)}`,
542
          );
543

NEW
544
          throw error;
×
545
        }
546
      }
547

548
      meta.prevArgs = args;
174✔
549
    },
550
    { useShadow },
551
  );
552
}
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

© 2026 Coveralls, Inc