All files / lib/compiler templateEvents.js

98.08% Statements 154/157
81.08% Branches 30/37
100% Functions 1/1
98.08% Lines 154/157

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158323x 323x 323x 323x 323x 323x 323x 323x 323x 323x 323x 323x 323x 323x 323x 323x 323x 323x 323x 323x 323x 323x 323x 323x 323x 323x 841x 841x 841x 841x 841x 841x 841x 841x 841x 841x 841x 841x 841x 841x 841x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x       81x 81x 841x 841x 841x 81x 81x 81x 81x 81x 81x 841x 841x 841x 841x 1119x 1119x 1119x 1119x 1119x 1119x 1119x 841x 841x 841x 841x 841x 841x 841x 841x 841x 841x 841x 841x 10566x 2979x 2979x 2979x 2979x 2979x 2979x 2979x 10566x 10566x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 2948x 10566x 3189x 1900x 3189x 518x 518x 518x 518x 518x 518x 518x 518x 518x 1382x 3189x 43x 43x 43x 43x 43x 43x 43x 43x 43x 1339x 3189x 29x 29x 29x 29x 29x 29x 29x 3189x 2948x 841x 841x 841x 841x 323x 323x  
/**
 * @file templateEvents.js
 * @description One walk over a template, shared by everything that needs it.
 * @module lib/compiler/templateEvents
 */
 
import { scanTagEnd } from './parser/htmlTree.js';
import { parseForHeader } from './ir/build.js';
import { tokenizeMarkup } from '../core/utils/markupLexer.js';
 
/**
 * Collects every expression-bearing construct in a template, with its offset.
 *
 * Two callers need exactly this walk and must not drift apart:
 * `validateTemplate` runs it over the processed template to report undeclared
 * references, and Atlas runs it over the original source — where the offsets
 * still point at what the developer wrote — to record the relationships each
 * construct creates.
 *
 * Offsets are relative to whatever string is passed in. Nothing here assumes
 * the template has been transformed, so it is safe on raw source.
 * @param {string} template - The template text to walk.
 * @returns {Array<object>} Events ordered by offset. Each carries `type`,
 *   `index` and `length`; expression-bearing types also carry `expr`.
 */
export function collectTemplateEvents(template) {
  const events = [];
  if (!template) return events;
 
  // 1. Loop starts and ends. The item binding is in scope for everything
  //    between them, which is why the ends are events rather than being
  //    ignored.
  //    The header is scanned and parsed by the same code the IR uses. This
  //    module used to carry a third copy of the pattern, and like the others it
  //    ended the header at the first `>` -- so `<@for r in rows.filter(x =>
  //    x.n > 2)>` reached the validator as `rows.filter(x =` and was reported
  //    as an undeclared reference to `x`. Atlas reads these events too, so the
  //    disagreement reached its edges as well.
  const forStartRegex = /<@for\b/gi;
  let match;
  while ((match = forStartRegex.exec(template)) !== null) {
    const end = scanTagEnd(template, match.index);
    if (end === -1) break;
 
    try {
      const parts = parseForHeader(template.slice(match.index + '<@for'.length, end).trim());
      events.push({
        type: 'loop_start',
        index: match.index,
        length: end + 1 - match.index,
        item: parts.item || (parts.destructure || []).join(', '),
        bindings: parts.item ? [parts.item] : parts.destructure || [],
        list: parts.list,
        key: parts.key,
      });
    } catch {
      // A malformed header is reported by the IR builder, with a location and
      // a reason. A second, vaguer complaint from here would not help.
    }
    forStartRegex.lastIndex = end + 1;
  }
 
  const forEndRegex = /<\/ ?@for>/gi;
  while ((match = forEndRegex.exec(template)) !== null) {
    events.push({
      type: 'loop_end',
      index: match.index,
      length: match[0].length,
    });
  }
 
  // 2. Interpolations.
  const interpRegex = /\{\{([\s\S]*?)\}\}/g;
  while ((match = interpRegex.exec(template)) !== null) {
    events.push({
      type: 'interpolation',
      index: match.index,
      length: match[0].length,
      expr: match[1],
    });
  }
 
  // 3. Conditions, attributes carrying expressions, and static ids for the
  //    duplicate-id check -- all read from the shared markup lexer.
  //
  //    This used to be `/<([a-zA-Z0-9@/!-][^>]*?)>/g` plus three attribute
  //    regexes run over whatever that matched, which is the pattern the lexer
  //    exists to replace: it ends a tag at the first `>` wherever it appears.
  //    `<div title="a > b" @click="go()">` was cut after `a >`, so the handler
  //    was invisible to both consumers of this walk -- the validator never
  //    checked its identifiers, and Atlas recorded no invocation, which made
  //    AVX_W41 claim the action it calls is never invoked.
  for (const token of tokenizeMarkup(template)) {
    if (token.type !== 'open') continue;
 
    // `<@if …>` and `<@elseif …>` carry an expression header rather than
    // attributes, and that header was never collected at all. Nothing read the
    // condition, so a typo in it was never reported (AVX_W03 fires for an
    // interpolation but not for a condition), and state read only by a
    // condition was reported as read nowhere (AVX_W40) -- the false absence
    // claim that Atlas's own diagnostics module forbids.
    const lowerName = String(token.name || '').toLowerCase();
    if (lowerName === '@if' || lowerName === '@elseif') {
      const header = template.slice(token.nameEnd, token.attrEnd);
      if (header.trim()) {
        events.push({
          type: 'condition',
          index: token.start,
          length: token.end - token.start,
          name: lowerName.slice(1),
          expr: header,
        });
      }
      continue;
    }
 
    for (const attr of token.attrs || []) {
      if (attr.value === null) continue;
 
      if (attr.name.startsWith('@')) {
        events.push({
          type: 'event',
          index: attr.start,
          length: attr.end - attr.start,
          name: attr.name.slice(1),
          expr: attr.value,
        });
        continue;
      }
 
      if (attr.name.startsWith('data-ax-')) {
        events.push({
          type: 'directive',
          index: attr.start,
          length: attr.end - attr.start,
          name: attr.name,
          expr: attr.value,
        });
        continue;
      }
 
      if (attr.name.toLowerCase() === 'id' && !attr.value.includes('{{')) {
        events.push({
          type: 'id_attribute',
          index: attr.start,
          length: attr.end - attr.start,
          idValue: attr.value,
        });
      }
    }
  }
 
  events.sort((a, b) => a.index - b.index);
  return events;
}
 
export default collectTemplateEvents;