All files / utils templater.js

98.05% Statements 101/103
84.78% Branches 39/46
100% Functions 4/4
98.05% Lines 101/103

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 1042x 2x 2x 2x 2x 2x 1625x 1625x 1625x 2x 2x 2x 2x 2x 2x 2x 2813x 2813x 2813x 690x 690x 275x 275x 275x 275x 275x 275x 690x 690x 2813x 67x 67x 67x 67x 67x 67x 67x 67x 67x 67x 2813x 2813x 2807x 1350x 1350x 4425x 438x 19x 18x 420x 420x 2631x 2639x 2x 2794x 2807x 2812x 2813x 2803x 2803x 2812x 2x 2x 2x 2x 2x 2x 548x 548x 548x 10x 10x   10x 10x 10x 10x 548x 10x 10x   10x 10x 10x 10x 548x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 548x 548x 548x 548x 548x 548x 548x  
import { getFromStringPath } from './string-path.js'
 
/**
 * @param {unknown} value - value to normalize
 * @returns {string} normalized value
 */
function normalizeValue (value) {
  return value === undefined ? '' : typeof value === 'object' ? JSON.stringify(value) : String(value)
}
 
/**
 * Auxiliary method of `applyTemplate`, applies the template on the current element
 *
 * @param {Element} currentElement - target element
 * @param {Record<string, unknown>} currentData - data to apply
 */
function applyTemplateAux (currentElement, currentData) {
  switch (currentElement.tagName) {
    case 'SLOT': {
      const name = currentElement.getAttribute('name') ?? ''
      if (name.startsWith('$.')) {
        currentElement.removeAttribute('name')
        const content = normalizeValue(getFromStringPath(currentData, name.slice(2)))
        const parent = currentElement.parentNode
        currentElement.replaceWith(content)
        parent?.normalize()
      }
      break
    }
    case 'TEMPLATE': {
      const loopPath = currentElement.getAttribute('data-each') ?? ''
      if (loopPath.startsWith('$.')) {
        const data = getFromStringPath(currentData, loopPath.slice(2))
        if (Array.isArray(data)) {
          const newContent = data.map(newData => applyTemplate(/** @type {HTMLTemplateElement} */ (currentElement), newData))
          currentElement.replaceWith(...newContent)
          return
        }
      }
    }
  }
  for (const { name, value } of currentElement.attributes) {
    if (value.startsWith('$.')) {
      const newValue = normalizeValue(getFromStringPath(currentData, value.slice(2)))
      currentElement.setAttribute(name, newValue)
    } else if (value.startsWith('$?.')) {
      const newValue = !!getFromStringPath(currentData, value.slice(3))
      if (newValue) {
        currentElement.setAttribute(name, '')
      } else {
        currentElement.removeAttribute(name)
      }
    } else if (value.startsWith('$$')) {
      currentElement.setAttribute(name, value.slice(1))
    }
  }
 
  for (const child of currentElement.children) {
    applyTemplateAux(child, currentData)
  }
}
 
/**
 * Trims document fragment of whitespace characters of text nodes
 * from the beginning and end of the document fragment
 * @param {DocumentFragment} documentFragment - target document fragment
 */
function trimDocumentFragment (documentFragment) {
  const { lastChild, firstChild } = documentFragment
  if (lastChild?.nodeType === document.TEXT_NODE) {
    const trimmedText = lastChild.nodeValue?.trimEnd() ?? ''
    if (trimmedText) {
      lastChild.textContent = trimmedText
    } else {
      lastChild.remove()
    }
  }
  if (firstChild?.nodeType === document.TEXT_NODE) {
    const trimmedText = firstChild.nodeValue?.trimStart() ?? ''
    if (trimmedText) {
      firstChild.textContent = trimmedText
    } else {
      firstChild.remove()
    }
  }
}
 
/**
 * Create a DOM tree based on the structure in <template>
 * and the data sent to generate it
 *
 * @param {HTMLTemplateElement} template - template source
 * @param {Record<string, unknown>} data - data to apply
 * @returns {DocumentFragment} Create document fragment
 */
export function applyTemplate (template, data) {
  const clone = /** @type {DocumentFragment} */(template.content.cloneNode(true))
  for (const child of [...clone.children]) {
    applyTemplateAux(child, data)
  }
  trimDocumentFragment(clone)
  return clone
}