_bindElement()

in src/core/xfa/bind.js [480:671]


  _bindElement(formNode, dataNode) {
    // Some nodes can be useless because min=0 so remove them
    // after the loop to avoid bad things.

    const uselessNodes = [];

    this._createOccurrences(formNode);

    for (const child of formNode[$getChildren]()) {
      if (child[$data]) {
        // Already bound.
        continue;
      }

      if (this._mergeMode === undefined && child[$nodeName] === "subform") {
        this._mergeMode = child.mergeMode === "consumeData";

        // XFA specs p. 182:
        // The highest-level subform and the data node representing
        // the current record are special; they are always
        // bound even if their names don't match.
        const dataChildren = dataNode[$getChildren]();
        if (dataChildren.length > 0) {
          this._bindOccurrences(child, [dataChildren[0]], null);
        } else if (this.emptyMerge) {
          const nsId =
            dataNode[$namespaceId] === NS_DATASETS
              ? -1
              : dataNode[$namespaceId];
          const dataChild = (child[$data] = new XmlObject(
            nsId,
            child.name || "root"
          ));
          dataNode[$appendChild](dataChild);
          this._bindElement(child, dataChild);
        }
        continue;
      }

      if (!child[$isBindable]()) {
        // The node cannot contain some new data so there is nothing
        // to create in the data node.
        continue;
      }

      let global = false;
      let picture = null;
      let ref = null;
      let match = null;
      if (child.bind) {
        switch (child.bind.match) {
          case "none":
            this._setAndBind(child, dataNode);
            continue;
          case "global":
            global = true;
            break;
          case "dataRef":
            if (!child.bind.ref) {
              warn(`XFA - ref is empty in node ${child[$nodeName]}.`);
              this._setAndBind(child, dataNode);
              continue;
            }
            ref = child.bind.ref;
            break;
          default:
            break;
        }
        if (child.bind.picture) {
          picture = child.bind.picture[$content];
        }
      }

      const [min, max] = this._getOccurInfo(child);

      if (ref) {
        // Don't use a cache for searching: nodes can change during binding.
        match = searchNode(
          this.root,
          dataNode,
          ref,
          true /* = dotDotAllowed */,
          false /* = useCache */
        );
        if (match === null) {
          // Nothing found: we must create some nodes in data in order
          // to have something to match with the given expression.
          // See http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.364.2157&rep=rep1&type=pdf#page=199
          match = createDataNode(this.data, dataNode, ref);
          if (!match) {
            // For example if the node contains a .(...) then it isn't
            // findable.
            // TODO: remove this when .(...) is implemented.
            continue;
          }
          if (this._isConsumeData()) {
            match[$consumed] = true;
          }

          // Don't bind the value in newly created node because it's empty.
          this._setAndBind(child, match);
          continue;
        } else {
          if (this._isConsumeData()) {
            // Filter out consumed nodes.
            match = match.filter(node => !node[$consumed]);
          }
          if (match.length > max) {
            match = match.slice(0, max);
          } else if (match.length === 0) {
            match = null;
          }
          if (match && this._isConsumeData()) {
            match.forEach(node => {
              node[$consumed] = true;
            });
          }
        }
      } else {
        if (!child.name) {
          this._setAndBind(child, dataNode);
          continue;
        }
        if (this._isConsumeData()) {
          // In consumeData mode, search for the next node with the given name.
          // occurs.max gives us the max number of node to match.
          const matches = [];
          while (matches.length < max) {
            const found = this._findDataByNameToConsume(
              child.name,
              child[$hasSettableValue](),
              dataNode,
              global
            );

            if (!found) {
              break;
            }
            found[$consumed] = true;
            matches.push(found);
          }
          match = matches.length > 0 ? matches : null;
        } else {
          // If we've an empty merge, there are no reason
          // to make multiple bind so skip consumed nodes.
          match = dataNode[$getRealChildrenByNameIt](
            child.name,
            /* allTransparent = */ false,
            /* skipConsumed = */ this.emptyMerge
          ).next().value;
          if (!match) {
            // If there is no match (no data) and `min === 0` then
            // the container is entirely excluded.
            // https://www.pdfa.org/norm-refs/XFA-3_3.pdf#G12.1428332
            if (min === 0) {
              uselessNodes.push(child);
              continue;
            }
            // We're in matchTemplate mode so create a node in data to reflect
            // what we've in template.
            const nsId =
              dataNode[$namespaceId] === NS_DATASETS
                ? -1
                : dataNode[$namespaceId];
            match = child[$data] = new XmlObject(nsId, child.name);
            if (this.emptyMerge) {
              match[$consumed] = true;
            }
            dataNode[$appendChild](match);

            // Don't bind the value in newly created node because it's empty.
            this._setAndBind(child, match);
            continue;
          }
          if (this.emptyMerge) {
            match[$consumed] = true;
          }
          match = [match];
        }
      }

      if (match) {
        this._bindOccurrences(child, match, picture);
      } else if (min > 0) {
        this._setAndBind(child, dataNode);
      } else {
        uselessNodes.push(child);
      }
    }

    uselessNodes.forEach(node => node[$getParent]()[$removeChild](node));
  }