riot/compiler

View on GitHub
src/generators/template/builder.js

Summary

Maintainability
A
2 hrs
Test Coverage
import {
  cloneNodeWithoutSelectorAttribute,
  closeTag,
  createBindingSelector,
  createNestedRootNode,
  getChildrenNodes,
  getNodeAttributes,
  nodeToString,
} from './utils.js'
import {
  hasEachAttribute,
  hasIfAttribute,
  hasItsOwnTemplate,
  isCustomNode,
  isRemovableNode,
  isRootNode,
  isSlotNode,
  isStaticNode,
  isTagNode,
  isTextNode,
  isVoidNode,
} from './checks.js'
import cloneDeep from '../../utils/clone-deep.js'
import eachBinding from './bindings/each.js'
import ifBinding from './bindings/if.js'
import { panic } from '@riotjs/util/misc.js'
import simpleBinding from './bindings/simple.js'
import slotBinding from './bindings/slot.js'
import tagBinding from './bindings/tag.js'

const BuildingState = Object.freeze({
  html: [],
  bindings: [],
  parent: null,
})

/**
 * Nodes having bindings should be cloned and new selector properties should be added to them
 * @param   {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
 * @param   {string} bindingsSelector - temporary string to identify the current node
 * @returns {RiotParser.Node} the original node parsed having the new binding selector attribute
 */
function createBindingsTag(sourceNode, bindingsSelector) {
  if (!bindingsSelector) return sourceNode

  return {
    ...sourceNode,
    // inject the selector bindings into the node attributes
    attributes: [
      {
        name: bindingsSelector,
        value: bindingsSelector,
      },
      ...getNodeAttributes(sourceNode),
    ],
  }
}

/**
 * Create a generic dynamic node (text or tag) and generate its bindings
 * @param   {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
 * @param   {string} sourceFile - source file path
 * @param   {string} sourceCode - original source
 * @param   {BuildingState} state - state representing the current building tree state during the recursion
 * @returns {Array} array containing the html output and bindings for the current node
 */
function createDynamicNode(sourceNode, sourceFile, sourceCode, state) {
  switch (true) {
    case isTextNode(sourceNode):
      // text nodes will not have any bindings
      return [nodeToString(sourceNode), []]
    default:
      return createTagWithBindings(sourceNode, sourceFile, sourceCode, state)
  }
}

/**
 * Create only a dynamic tag node with generating a custom selector and its bindings
 * @param   {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
 * @param   {string} sourceFile - source file path
 * @param   {string} sourceCode - original source
 * @param   {BuildingState} state - state representing the current building tree state during the recursion
 * @returns {Array} array containing the html output and bindings for the current node
 */
function createTagWithBindings(sourceNode, sourceFile, sourceCode) {
  const bindingsSelector = isRootNode(sourceNode)
    ? null
    : createBindingSelector()
  const cloneNode = createBindingsTag(sourceNode, bindingsSelector)
  const tagOpeningHTML = nodeToString(cloneNode)

  switch (true) {
    case hasEachAttribute(cloneNode):
      // EACH bindings have prio 1
      return [
        tagOpeningHTML,
        [eachBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)],
      ]
    case hasIfAttribute(cloneNode):
      // IF bindings have prio 2
      return [
        tagOpeningHTML,
        [ifBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)],
      ]
    case isCustomNode(cloneNode):
      // TAG bindings have prio 3
      return [
        tagOpeningHTML,
        [tagBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)],
      ]
    case isSlotNode(cloneNode):
      // slot tag
      return [tagOpeningHTML, [slotBinding(cloneNode, bindingsSelector)]]
    default:
      // this node has expressions bound to it
      return [
        tagOpeningHTML,
        [simpleBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)],
      ]
  }
}

/**
 * Parse a node trying to extract its template and bindings
 * @param   {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
 * @param   {string} sourceFile - source file path
 * @param   {string} sourceCode - original source
 * @param   {BuildingState} state - state representing the current building tree state during the recursion
 * @returns {Array} array containing the html output and bindings for the current node
 */
function parseNode(sourceNode, sourceFile, sourceCode, state) {
  // static nodes have no bindings
  if (isStaticNode(sourceNode)) return [nodeToString(sourceNode), []]
  return createDynamicNode(sourceNode, sourceFile, sourceCode, state)
}

/**
 * Create the tag binding
 * @param   { RiotParser.Node.Tag } sourceNode - tag containing the each attribute
 * @param   { string } sourceFile - source file path
 * @param   { string } sourceCode - original source
 * @param   { string } selector - binding selector
 * @returns { Array } array with only the tag binding AST
 */
export function createNestedBindings(
  sourceNode,
  sourceFile,
  sourceCode,
  selector,
) {
  const mightBeARiotComponent = isCustomNode(sourceNode)
  const node = cloneNodeWithoutSelectorAttribute(sourceNode, selector)

  return mightBeARiotComponent
    ? [null, [tagBinding(node, null, sourceFile, sourceCode)]]
    : build(createNestedRootNode(node), sourceFile, sourceCode)
}

/**
 * Build the template and the bindings
 * @param   {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
 * @param   {string} sourceFile - source file path
 * @param   {string} sourceCode - original source
 * @param   {BuildingState} state - state representing the current building tree state during the recursion
 * @returns {Array} array containing the html output and the dom bindings
 */
export default function build(sourceNode, sourceFile, sourceCode, state) {
  if (!sourceNode)
    panic(
      "Something went wrong with your tag DOM parsing, your tag template can't be created",
    )

  const [nodeHTML, nodeBindings] = parseNode(
    sourceNode,
    sourceFile,
    sourceCode,
    state,
  )
  const childrenNodes = getChildrenNodes(sourceNode)
  const canRenderNodeHTML = isRemovableNode(sourceNode) === false
  const currentState = { ...cloneDeep(BuildingState), ...state }

  // mutate the original arrays
  canRenderNodeHTML && currentState.html.push(...nodeHTML)
  currentState.bindings.push(...nodeBindings)

  // do recursion if
  // this tag has children and it has no special directives bound to it
  if (childrenNodes.length && !hasItsOwnTemplate(sourceNode)) {
    childrenNodes.forEach((node) =>
      build(node, sourceFile, sourceCode, {
        parent: sourceNode,
        ...currentState,
      }),
    )
  }

  // close the tag if it's not a void one
  if (canRenderNodeHTML && isTagNode(sourceNode) && !isVoidNode(sourceNode)) {
    currentState.html.push(closeTag(sourceNode))
  }

  return [currentState.html.join(''), currentState.bindings]
}