diff --git a/src/core/xfa/bind.js b/src/core/xfa/bind.js
new file mode 100644
index 000000000..209c837bd
--- /dev/null
+++ b/src/core/xfa/bind.js
@@ -0,0 +1,593 @@
+/* Copyright 2021 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ $appendChild,
+ $clone,
+ $consumed,
+ $content,
+ $data,
+ $finalize,
+ $getAttributeIt,
+ $getChildren,
+ $getParent,
+ $getRealChildrenByNameIt,
+ $global,
+ $hasSettableValue,
+ $indexOf,
+ $insertAt,
+ $isDataValue,
+ $isDescendent,
+ $namespaceId,
+ $nodeName,
+ $removeChild,
+ $setValue,
+ $text,
+ XFAAttribute,
+ XmlObject,
+} from "./xfa_object.js";
+import { BindItems, Field, Items, SetProperty, Text } from "./template.js";
+import { createDataNode, searchNode } from "./som.js";
+import { NamespaceIds } from "./namespaces.js";
+import { warn } from "../../shared/util.js";
+
+function createText(content) {
+ const node = new Text({});
+ node[$content] = content;
+ return node;
+}
+
+class Binder {
+ constructor(root) {
+ this.root = root;
+ this.datasets = root.datasets;
+ if (root.datasets && root.datasets.data) {
+ this.emptyMerge = false;
+ this.data = root.datasets.data;
+ } else {
+ this.emptyMerge = true;
+ this.data = new XmlObject(NamespaceIds.datasets.id, "data");
+ }
+ this.root.form = this.form = root.template[$clone]();
+ }
+
+ _isConsumeData() {
+ return !this.emptyMerge && this._mergeMode;
+ }
+
+ _isMatchTemplate() {
+ return !this._isConsumeData();
+ }
+
+ bind() {
+ this._bindElement(this.form, this.data);
+ return this.form;
+ }
+
+ getData() {
+ return this.data;
+ }
+
+ _bindValue(formNode, data, picture) {
+ // Nodes must have the same "type": container or value.
+ // Here we make the link between form node and
+ // data node (through $data property): we'll use it
+ // to save form data.
+
+ if (formNode[$hasSettableValue]()) {
+ if (data[$isDataValue]()) {
+ const value = data[$content].trim();
+ // TODO: use picture.
+ formNode[$setValue](createText(value));
+ formNode[$data] = data;
+ } else if (
+ formNode instanceof Field &&
+ formNode.ui &&
+ formNode.ui.choiceList &&
+ formNode.ui.choiceList.open === "multiSelect"
+ ) {
+ const value = data[$getChildren]()
+ .map(child => child[$content].trim())
+ .join("\n");
+ formNode[$setValue](createText(value));
+ formNode[$data] = data;
+ } else if (this._isConsumeData()) {
+ warn(`XFA - Nodes haven't the same type.`);
+ }
+ } else if (!data[$isDataValue]() || this._isMatchTemplate()) {
+ this._bindElement(formNode, data);
+ formNode[$data] = data;
+ } else {
+ warn(`XFA - Nodes haven't the same type.`);
+ }
+ }
+
+ _findDataByNameToConsume(name, dataNode, global) {
+ if (!name) {
+ return null;
+ }
+
+ // Firstly, we try to find a node with the given name:
+ // - in dataNode;
+ // - if not found, then in parent;
+ // - and if not in found, then in grand-parent.
+ let generator, match;
+ for (let i = 0; i < 3; i++) {
+ generator = dataNode[$getRealChildrenByNameIt](
+ name,
+ /* allTransparent = */ false,
+ /* skipConsumed = */ true
+ );
+ match = generator.next().value;
+ if (match) {
+ return match;
+ }
+ if (
+ dataNode[$namespaceId] === NamespaceIds.datasets.id &&
+ dataNode[$nodeName] === "data"
+ ) {
+ break;
+ }
+ dataNode = dataNode[$getParent]();
+ }
+
+ if (!global) {
+ return null;
+ }
+
+ // Secondly, if global try to find it just under the root of datasets
+ // (which is the location of global variables).
+ generator = this.datasets[$getRealChildrenByNameIt](
+ name,
+ /* allTransparent = */ false,
+ /* skipConsumed = */ false
+ );
+
+ while (true) {
+ match = generator.next().value;
+ if (!match) {
+ break;
+ }
+
+ if (match[$global]) {
+ return match;
+ }
+ }
+
+ // Thirdly, try to find it in attributes.
+ generator = this.data[$getAttributeIt](name, /* skipConsumed = */ true);
+ match = generator.next().value;
+ if (match && match[$isDataValue]()) {
+ return match;
+ }
+
+ return null;
+ }
+
+ _setProperties(formNode, dataNode) {
+ // For example:
+ //
+ //
+ //
+ //
+ //
+
+ if (!formNode.hasOwnProperty("setProperty")) {
+ return;
+ }
+
+ for (const { ref, target, connection } of formNode.setProperty.children) {
+ if (connection) {
+ // TODO: evaluate if we should implement this feature.
+ // Skip for security reasons.
+ continue;
+ }
+ if (!ref) {
+ continue;
+ }
+
+ const [node] = searchNode(
+ this.root,
+ dataNode,
+ ref,
+ false /* = dotDotAllowed */,
+ false /* = useCache */
+ );
+ if (!node) {
+ warn(`XFA - Invalid reference: ${ref}.`);
+ continue;
+ }
+
+ if (!node[$isDescendent](this.data)) {
+ warn(`XFA - Invalid node: must be a data node.`);
+ continue;
+ }
+
+ const [targetNode] = searchNode(
+ this.root,
+ formNode,
+ target,
+ false /* = dotDotAllowed */,
+ false /* = useCache */
+ );
+ if (!targetNode) {
+ warn(`XFA - Invalid target: ${target}.`);
+ continue;
+ }
+
+ if (!targetNode[$isDescendent](formNode)) {
+ warn(`XFA - Invalid target: must be a property or subproperty.`);
+ continue;
+ }
+
+ const targetParent = targetNode[$getParent]();
+ if (
+ targetNode instanceof SetProperty ||
+ targetParent instanceof SetProperty
+ ) {
+ warn(
+ `XFA - Invalid target: cannot be a setProperty or one of its properties.`
+ );
+ continue;
+ }
+
+ if (
+ targetNode instanceof BindItems ||
+ targetParent instanceof BindItems
+ ) {
+ warn(
+ `XFA - Invalid target: cannot be a bindItems or one of its properties.`
+ );
+ continue;
+ }
+
+ const content = node[$text]();
+ const name = targetNode[$nodeName];
+
+ if (targetNode instanceof XFAAttribute) {
+ const attrs = Object.create(null);
+ attrs[name] = content;
+ const obj = Reflect.construct(
+ Object.getPrototypeOf(targetParent).constructor,
+ [attrs]
+ );
+ targetParent[name] = obj[name];
+ continue;
+ }
+
+ if (!targetNode.hasOwnProperty($content)) {
+ warn(`XFA - Invalid node to use in setProperty`);
+ continue;
+ }
+
+ targetNode[$data] = node;
+ targetNode[$content] = content;
+ targetNode[$finalize]();
+ }
+ }
+
+ _bindItems(formNode, dataNode) {
+ // For example:
+ //
+ //
+ //
+ //
+
+ if (
+ !formNode.hasOwnProperty("items") ||
+ !formNode.hasOwnProperty("bindItems") ||
+ formNode.bindItems.isEmpty()
+ ) {
+ return;
+ }
+
+ for (const item of formNode.items.children) {
+ formNode[$removeChild](item);
+ }
+
+ formNode.items.clear();
+
+ const labels = new Items({});
+ const values = new Items({});
+
+ formNode[$appendChild](labels);
+ formNode.items.push(labels);
+
+ formNode[$appendChild](values);
+ formNode.items.push(values);
+
+ for (const { ref, labelRef, valueRef, connection } of formNode.bindItems
+ .children) {
+ if (connection) {
+ // TODO: evaluate if we should implement this feature.
+ // Skip for security reasons.
+ continue;
+ }
+ if (!ref) {
+ continue;
+ }
+
+ const nodes = searchNode(
+ this.root,
+ dataNode,
+ ref,
+ false /* = dotDotAllowed */,
+ false /* = useCache */
+ );
+ if (!nodes) {
+ warn(`XFA - Invalid reference: ${ref}.`);
+ continue;
+ }
+ for (const node of nodes) {
+ if (!node[$isDescendent](this.datasets)) {
+ warn(`XFA - Invalid ref (${ref}): must be a datasets child.`);
+ continue;
+ }
+
+ const [labelNode] = searchNode(
+ this.root,
+ node,
+ labelRef,
+ true /* = dotDotAllowed */,
+ false /* = useCache */
+ );
+ if (!labelNode) {
+ warn(`XFA - Invalid label: ${labelRef}.`);
+ continue;
+ }
+
+ if (!labelNode[$isDescendent](this.datasets)) {
+ warn(`XFA - Invalid label: must be a datasets child.`);
+ continue;
+ }
+
+ const [valueNode] = searchNode(
+ this.root,
+ node,
+ valueRef,
+ true /* = dotDotAllowed */,
+ false /* = useCache */
+ );
+ if (!valueNode) {
+ warn(`XFA - Invalid value: ${valueRef}.`);
+ continue;
+ }
+
+ if (!valueNode[$isDescendent](this.datasets)) {
+ warn(`XFA - Invalid value: must be a datasets child.`);
+ continue;
+ }
+
+ const label = createText(labelNode[$text]());
+ const value = createText(valueNode[$text]());
+
+ labels[$appendChild](label);
+ labels.text.push(label);
+
+ values[$appendChild](value);
+ values.text.push(value);
+ }
+ }
+ }
+
+ _bindOccurrences(formNode, matches, picture) {
+ // Insert nodes which are not in the template but reflect
+ // what we've in data tree.
+
+ let baseClone;
+ if (matches.length > 1) {
+ // Clone before binding to avoid bad state.
+ baseClone = formNode[$clone]();
+ }
+
+ this._bindValue(formNode, matches[0], picture);
+ this._setProperties(formNode, matches[0]);
+ this._bindItems(formNode, matches[0]);
+
+ if (matches.length === 1) {
+ return;
+ }
+
+ const parent = formNode[$getParent]();
+ const name = formNode[$nodeName];
+ const pos = parent[$indexOf](formNode);
+
+ for (let i = 1, ii = matches.length; i < ii; i++) {
+ const match = matches[i];
+ const clone = baseClone[$clone]();
+ clone.occur.min = 1;
+ clone.occur.max = 1;
+ clone.occur.initial = 1;
+ parent[name].push(clone);
+ parent[$insertAt](pos + i, clone);
+
+ this._bindValue(clone, match, picture);
+ this._setProperties(clone, match);
+ this._bindItems(clone, match);
+ }
+ }
+
+ _createOccurrences(formNode) {
+ if (!this.emptyMerge) {
+ return;
+ }
+
+ const { occur } = formNode;
+ if (!occur || occur.initial <= 1) {
+ return;
+ }
+
+ const parent = formNode[$getParent]();
+ const name = formNode[$nodeName];
+
+ for (let i = 0, ii = occur.initial; i < ii; i++) {
+ const clone = formNode[$clone]();
+ clone.occur.min = 1;
+ clone.occur.max = 1;
+ clone.occur.initial = 1;
+ parent[name].push(clone);
+ parent[$appendChild](clone);
+ }
+ }
+
+ _getOccurInfo(formNode) {
+ const { occur } = formNode;
+ const dataName = formNode.name;
+ if (!occur || !dataName) {
+ return [1, 1];
+ }
+ const max = occur.max === -1 ? Infinity : occur.max;
+ return [occur.min, max];
+ }
+
+ _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";
+ }
+
+ let global = false;
+ let picture = null;
+ let ref = null;
+ let match = null;
+ if (child.bind) {
+ switch (child.bind.match) {
+ case "none":
+ continue;
+ case "global":
+ global = true;
+ break;
+ case "dataRef":
+ if (!child.bind.ref) {
+ warn(`XFA - ref is empty in node ${child[$nodeName]}.`);
+ 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 (this._isConsumeData()) {
+ match[$consumed] = true;
+ }
+ match = [match];
+ } 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._bindElement(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,
+ dataNode,
+ global
+ );
+ if (!found) {
+ break;
+ }
+ found[$consumed] = true;
+ matches.push(found);
+ }
+ match = matches.length > 0 ? matches : null;
+ } else {
+ match = dataNode[$getRealChildrenByNameIt](
+ child.name,
+ /* allTransparent = */ false,
+ /* skipConsumed = */ false
+ ).next().value;
+ if (!match) {
+ // We're in matchTemplate mode so create a node in data to reflect
+ // what we've in template.
+ match = new XmlObject(dataNode[$namespaceId], child.name);
+ dataNode[$appendChild](match);
+ }
+ match = [match];
+ }
+ }
+
+ if (match) {
+ if (match.length < min) {
+ warn(
+ `XFA - Must have at least ${min} occurrences: ${formNode[$nodeName]}.`
+ );
+ continue;
+ }
+ this._bindOccurrences(child, match, picture);
+ } else if (min > 0) {
+ this._bindElement(child, dataNode);
+ } else {
+ uselessNodes.push(child);
+ }
+ }
+
+ uselessNodes.forEach(node => node[$getParent]()[$removeChild](node));
+ }
+}
+
+export { Binder };
diff --git a/src/core/xfa/builder.js b/src/core/xfa/builder.js
index aaed18ec8..511a814bf 100644
--- a/src/core/xfa/builder.js
+++ b/src/core/xfa/builder.js
@@ -17,6 +17,7 @@ import { $buildXFAObject, NamespaceIds } from "./namespaces.js";
import {
$cleanup,
$finalize,
+ $nsAttributes,
$onChild,
$resolvePrototypes,
XFAObject,
@@ -88,6 +89,25 @@ class Builder {
this._addNamespacePrefix(prefixes);
}
+ if (attributes.hasOwnProperty($nsAttributes)) {
+ // Only support xfa-data namespace.
+ const dataTemplate = NamespaceSetUp.datasets;
+ const nsAttrs = attributes[$nsAttributes];
+ let xfaAttrs = null;
+ for (const [ns, attrs] of Object.entries(nsAttrs)) {
+ const nsToUse = this._getNamespaceToUse(ns);
+ if (nsToUse === dataTemplate) {
+ xfaAttrs = { xfa: attrs };
+ break;
+ }
+ }
+ if (xfaAttrs) {
+ attributes[$nsAttributes] = xfaAttrs;
+ } else {
+ delete attributes[$nsAttributes];
+ }
+ }
+
const namespaceToUse = this._getNamespaceToUse(nsPrefix);
const node =
(namespaceToUse && namespaceToUse[$buildXFAObject](name, attributes)) ||
diff --git a/src/core/xfa/datasets.js b/src/core/xfa/datasets.js
index b7ad09699..33f80ecc9 100644
--- a/src/core/xfa/datasets.js
+++ b/src/core/xfa/datasets.js
@@ -13,14 +13,16 @@
* limitations under the License.
*/
-import { $buildXFAObject, NamespaceIds } from "./namespaces.js";
import {
+ $appendChild,
+ $global,
$namespaceId,
$nodeName,
- $onChildCheck,
+ $onChild,
XFAObject,
XmlObject,
} from "./xfa_object.js";
+import { $buildXFAObject, NamespaceIds } from "./namespaces.js";
const DATASETS_NS_ID = NamespaceIds.datasets.id;
@@ -37,15 +39,18 @@ class Datasets extends XFAObject {
this.Signature = null;
}
- [$onChildCheck](child) {
+ [$onChild](child) {
const name = child[$nodeName];
- if (name === "data") {
- return child[$namespaceId] === DATASETS_NS_ID;
+ if (
+ (name === "data" && child[$namespaceId] === DATASETS_NS_ID) ||
+ (name === "Signature" &&
+ child[$namespaceId] === NamespaceIds.signature.id)
+ ) {
+ this[name] = child;
+ } else {
+ child[$global] = true;
}
- if (name === "Signature") {
- return child[$namespaceId] === NamespaceIds.signature.id;
- }
- return false;
+ this[$appendChild](child);
}
}
diff --git a/src/core/xfa/factory.js b/src/core/xfa/factory.js
new file mode 100644
index 000000000..e78bdf044
--- /dev/null
+++ b/src/core/xfa/factory.js
@@ -0,0 +1,33 @@
+/* Copyright 2021 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Binder } from "./bind.js";
+import { XFAParser } from "./parser.js";
+
+class XFAFactory {
+ constructor(data) {
+ this.root = new XFAParser().parse(XFAFactory._createDocument(data));
+ this.form = new Binder(this.root).bind();
+ }
+
+ static _createDocument(data) {
+ if (!data["/xdp:xdp"]) {
+ return data["xdp:xdp"];
+ }
+ return Object.values(data).join("");
+ }
+}
+
+export { XFAFactory };
diff --git a/src/core/xfa/parser.js b/src/core/xfa/parser.js
index d0e492d04..ebc83e827 100644
--- a/src/core/xfa/parser.js
+++ b/src/core/xfa/parser.js
@@ -13,7 +13,14 @@
* limitations under the License.
*/
-import { $clean, $finalize, $onChild, $onText, $setId } from "./xfa_object.js";
+import {
+ $clean,
+ $finalize,
+ $nsAttributes,
+ $onChild,
+ $onText,
+ $setId,
+} from "./xfa_object.js";
import { XMLParserBase, XMLParserErrorCode } from "../xml_parser.js";
import { Builder } from "./builder.js";
import { warn } from "../../shared/util.js";
@@ -57,7 +64,7 @@ class XFAParser extends XMLParserBase {
// namespaces information.
let namespace = null;
let prefixes = null;
- const attributeObj = Object.create(null);
+ const attributeObj = Object.create({});
for (const { name, value } of attributes) {
if (name === "xmlns") {
if (!namespace) {
@@ -72,7 +79,23 @@ class XFAParser extends XMLParserBase {
}
prefixes.push({ prefix, value });
} else {
- attributeObj[name] = value;
+ const i = name.indexOf(":");
+ if (i === -1) {
+ attributeObj[name] = value;
+ } else {
+ // Attributes can have their own namespace.
+ // For example in data, we can have
+ let nsAttrs = attributeObj[$nsAttributes];
+ if (!nsAttrs) {
+ nsAttrs = attributeObj[$nsAttributes] = Object.create(null);
+ }
+ const [ns, attrName] = [name.slice(0, i), name.slice(i + 1)];
+ let attrs = nsAttrs[ns];
+ if (!attrs) {
+ attrs = nsAttrs[ns] = Object.create(null);
+ }
+ attrs[attrName] = value;
+ }
}
}
diff --git a/src/core/xfa/som.js b/src/core/xfa/som.js
index d0d394fa1..3fb2d04f5 100644
--- a/src/core/xfa/som.js
+++ b/src/core/xfa/som.js
@@ -14,11 +14,14 @@
*/
import {
+ $appendChild,
$getChildrenByClass,
$getChildrenByName,
$getParent,
+ $namespaceId,
XFAObject,
XFAObjectArray,
+ XmlObject,
} from "./xfa_object.js";
import { warn } from "../../shared/util.js";
@@ -33,17 +36,18 @@ const operators = {
};
const shortcuts = new Map([
- ["$data", root => root.datasets.data],
- ["$template", root => root.template],
- ["$connectionSet", root => root.connectionSet],
- ["$form", root => root.form],
- ["$layout", root => root.layout],
- ["$host", root => root.host],
- ["$dataWindow", root => root.dataWindow],
- ["$event", root => root.event],
- ["!", root => root.datasets],
- ["$xfa", root => root],
- ["xfa", root => root],
+ ["$data", (root, current) => root.datasets.data],
+ ["$template", (root, current) => root.template],
+ ["$connectionSet", (root, current) => root.connectionSet],
+ ["$form", (root, current) => root.form],
+ ["$layout", (root, current) => root.layout],
+ ["$host", (root, current) => root.host],
+ ["$dataWindow", (root, current) => root.dataWindow],
+ ["$event", (root, current) => root.event],
+ ["!", (root, current) => root.datasets],
+ ["$xfa", (root, current) => root],
+ ["xfa", (root, current) => root],
+ ["$", (root, current) => current],
]);
const somCache = new WeakMap();
@@ -138,17 +142,24 @@ function parseExpression(expr, dotDotAllowed) {
return parsed;
}
-function searchNode(root, container, expr, dotDotAllowed = true) {
+function searchNode(
+ root,
+ container,
+ expr,
+ dotDotAllowed = true,
+ useCache = true
+) {
const parsed = parseExpression(expr, dotDotAllowed);
if (!parsed) {
return null;
}
+
const fn = shortcuts.get(parsed[0].name);
let i = 0;
let isQualified;
if (fn) {
isQualified = true;
- root = [fn(root)];
+ root = [fn(root, container)];
i = 1;
} else {
isQualified = container === null;
@@ -163,13 +174,17 @@ function searchNode(root, container, expr, dotDotAllowed = true) {
continue;
}
- let cached = somCache.get(node);
- if (!cached) {
- cached = new Map();
- somCache.set(node, cached);
+ let children, cached;
+
+ if (useCache) {
+ cached = somCache.get(node);
+ if (!cached) {
+ cached = new Map();
+ somCache.set(node, cached);
+ }
+ children = cached.get(cacheName);
}
- let children = cached.get(cacheName);
if (!children) {
switch (operator) {
case operators.dot:
@@ -189,7 +204,9 @@ function searchNode(root, container, expr, dotDotAllowed = true) {
default:
break;
}
- cached.set(cacheName, children);
+ if (useCache) {
+ cached.set(cacheName, children);
+ }
}
if (children.length > 0) {
@@ -222,11 +239,72 @@ function searchNode(root, container, expr, dotDotAllowed = true) {
return null;
}
- if (root.length === 1) {
- return root[0];
- }
-
return root;
}
-export { searchNode };
+function createNodes(root, path) {
+ let node = null;
+ for (const { name, index } of path) {
+ for (let i = 0; i <= index; i++) {
+ node = new XmlObject(root[$namespaceId], name);
+ root[$appendChild](node);
+ }
+
+ root = node;
+ }
+ return node;
+}
+
+function createDataNode(root, container, expr) {
+ const parsed = parseExpression(expr);
+ if (!parsed) {
+ return null;
+ }
+
+ if (parsed.some(x => x.operator === operators.dotDot)) {
+ return null;
+ }
+
+ const fn = shortcuts.get(parsed[0].name);
+ let i = 0;
+ if (fn) {
+ root = fn(root, container);
+ i = 1;
+ } else {
+ root = container || root;
+ }
+
+ for (let ii = parsed.length; i < ii; i++) {
+ const { cacheName, index } = parsed[i];
+ if (!isFinite(index)) {
+ parsed[i].index = 0;
+ return createNodes(root, parsed.slice(i));
+ }
+
+ const cached = somCache.get(root);
+ if (!cached) {
+ warn(`XFA - createDataNode must be called after searchNode.`);
+ return null;
+ }
+
+ const children = cached.get(cacheName);
+ if (children.length === 0) {
+ return createNodes(root, parsed.slice(i));
+ }
+
+ if (index < children.length) {
+ const child = children[index];
+ if (!(child instanceof XFAObject)) {
+ warn(`XFA - Cannot create a node.`);
+ return null;
+ }
+ root = child;
+ } else {
+ parsed[i].index = children.length - index;
+ return createNodes(root, parsed.slice(i));
+ }
+ }
+ return null;
+}
+
+export { createDataNode, searchNode };
diff --git a/src/core/xfa/template.js b/src/core/xfa/template.js
index c9808cb03..0c09abaee 100644
--- a/src/core/xfa/template.js
+++ b/src/core/xfa/template.js
@@ -13,15 +13,19 @@
* limitations under the License.
*/
-import { $buildXFAObject, NamespaceIds } from "./namespaces.js";
import {
+ $appendChild,
$content,
$finalize,
+ $hasItem,
+ $hasSettableValue,
$isTransparent,
$namespaceId,
$nodeName,
$onChild,
+ $removeChild,
$setSetAttributes,
+ $setValue,
ContentObject,
Option01,
OptionObject,
@@ -29,6 +33,7 @@ import {
XFAObject,
XFAObjectArray,
} from "./xfa_object.js";
+import { $buildXFAObject, NamespaceIds } from "./namespaces.js";
import {
getBBox,
getColor,
@@ -44,6 +49,15 @@ import { warn } from "../../shared/util.js";
const TEMPLATE_NS_ID = NamespaceIds.template.id;
+function _setValue(templateNode, value) {
+ if (!templateNode.value) {
+ const nodeValue = new Value({});
+ templateNode[$appendChild](nodeValue);
+ templateNode.value = nodeValue;
+ }
+ templateNode.value[$setValue](value);
+}
+
class AppearanceFilter extends StringObject {
constructor(attributes) {
super(TEMPLATE_NS_ID, "appearanceFilter");
@@ -496,6 +510,10 @@ class Caption extends XFAObject {
this.para = null;
this.value = null;
}
+
+ [$setValue](value) {
+ _setValue(this, value);
+ }
}
class Certificate extends StringObject {
@@ -586,6 +604,10 @@ class Color extends XFAObject {
this.value = getColor(attributes.value);
this.extras = null;
}
+
+ [$hasSettableValue]() {
+ return false;
+ }
}
class Comb extends XFAObject {
@@ -869,6 +891,10 @@ class Draw extends XFAObject {
this.value = null;
this.setProperty = new XFAObjectArray();
}
+
+ [$setValue](value) {
+ _setValue(this, value);
+ }
}
class Edge extends XFAObject {
@@ -1045,7 +1071,7 @@ class Event extends XFAObject {
}
class ExData extends ContentObject {
- constructor(builder, attributes) {
+ constructor(attributes) {
super(TEMPLATE_NS_ID, "exData");
this.contentType = attributes.contentType || "";
this.href = attributes.href || "";
@@ -1188,6 +1214,32 @@ class ExclGroup extends XFAObject {
this.field = new XFAObjectArray();
this.setProperty = new XFAObjectArray();
}
+
+ [$hasSettableValue]() {
+ return true;
+ }
+
+ [$setValue](value) {
+ for (const field of this.field.children) {
+ if (!field.value) {
+ const nodeValue = new Value({});
+ field[$appendChild](nodeValue);
+ field.value = nodeValue;
+ }
+
+ const nodeBoolean = new BooleanElement({});
+ nodeBoolean[$content] = 0;
+
+ for (const item of field.items.children) {
+ if (item[$hasItem](value)) {
+ nodeBoolean[$content] = 1;
+ break;
+ }
+ }
+
+ field.value[$setValue](nodeBoolean);
+ }
+ }
}
class Execute extends XFAObject {
@@ -1294,6 +1346,8 @@ class Field extends XFAObject {
this.extras = null;
this.font = null;
this.format = null;
+ // For a choice list, one list is used to have display entries
+ // and the other for the exported values
this.items = new XFAObjectArray(2);
this.keep = null;
this.margin = null;
@@ -1307,6 +1361,10 @@ class Field extends XFAObject {
this.event = new XFAObjectArray();
this.setProperty = new XFAObjectArray();
}
+
+ [$setValue](value) {
+ _setValue(this, value);
+ }
}
class Fill extends XFAObject {
@@ -1590,6 +1648,15 @@ class Items extends XFAObject {
this.text = new XFAObjectArray();
this.time = new XFAObjectArray();
}
+
+ [$hasItem](value) {
+ return (
+ this.hasOwnProperty(value[$nodeName]) &&
+ this[value[$nodeName]].children.some(
+ node => node[$content] === value[$content]
+ )
+ );
+ }
}
class Keep extends XFAObject {
@@ -1917,10 +1984,7 @@ class Para extends XFAObject {
"right",
]);
this.id = attributes.id || "";
- this.lineHeight = getMeasurement(attributes.lineHeight, [
- "0pt",
- "measurement",
- ]);
+ this.lineHeight = getMeasurement(attributes.lineHeight, "0pt");
this.marginLeft = getMeasurement(attributes.marginLeft, "0");
this.marginRight = getMeasurement(attributes.marginRight, "0");
this.orphans = getInteger({
@@ -2735,6 +2799,26 @@ class Value extends XFAObject {
this.text = null;
this.time = null;
}
+
+ [$setValue](value) {
+ const valueName = value[$nodeName];
+ if (this[valueName] !== null) {
+ this[valueName][$content] = value[$content];
+ return;
+ }
+
+ // Reset all the properties.
+ for (const name of Object.getOwnPropertyNames(this)) {
+ const obj = this[name];
+ if (obj instanceof XFAObject) {
+ this[name] = null;
+ this[$removeChild](obj);
+ }
+ }
+
+ this[value[$nodeName]] = value;
+ this[$appendChild](value);
+ }
}
class Variables extends XFAObject {
@@ -3225,4 +3309,13 @@ class TemplateNamespace {
}
}
-export { Template, TemplateNamespace };
+export {
+ BindItems,
+ Field,
+ Items,
+ SetProperty,
+ Template,
+ TemplateNamespace,
+ Text,
+ Value,
+};
diff --git a/src/core/xfa/xfa_object.js b/src/core/xfa/xfa_object.js
index e081b71b8..37a0fd7ca 100644
--- a/src/core/xfa/xfa_object.js
+++ b/src/core/xfa/xfa_object.js
@@ -19,43 +19,57 @@ import { NamespaceIds } from "./namespaces.js";
// We use these symbols to avoid name conflict between tags
// and properties/methods names.
+const $appendChild = Symbol();
const $clean = Symbol();
const $cleanup = Symbol();
+const $clone = Symbol();
+const $consumed = Symbol();
const $content = Symbol("content");
+const $data = Symbol("data");
const $dump = Symbol();
const $finalize = Symbol();
-const $isDataValue = Symbol();
const $getAttributeIt = Symbol();
const $getChildrenByClass = Symbol();
const $getChildrenByName = Symbol();
const $getChildrenByNameIt = Symbol();
+const $getRealChildrenByNameIt = Symbol();
const $getChildren = Symbol();
const $getParent = Symbol();
+const $global = Symbol();
+const $hasItem = Symbol();
+const $hasSettableValue = Symbol();
+const $indexOf = Symbol();
+const $insertAt = Symbol();
+const $isDataValue = Symbol();
+const $isDescendent = Symbol();
const $isTransparent = Symbol();
const $lastAttribute = Symbol();
const $namespaceId = Symbol("namespaceId");
const $nodeName = Symbol("nodeName");
+const $nsAttributes = Symbol();
const $onChild = Symbol();
const $onChildCheck = Symbol();
const $onText = Symbol();
+const $removeChild = Symbol();
const $resolvePrototypes = Symbol();
const $setId = Symbol();
const $setSetAttributes = Symbol();
+const $setValue = Symbol();
const $text = Symbol();
const _applyPrototype = Symbol();
const _attributes = Symbol();
const _attributeNames = Symbol();
-const _children = Symbol();
-const _clone = Symbol();
+const _children = Symbol("_children");
const _cloneAttribute = Symbol();
+const _dataValue = Symbol();
const _defaultValue = Symbol();
const _getPrototype = Symbol();
const _getUnsetAttributes = Symbol();
const _hasChildren = Symbol();
const _max = Symbol();
const _options = Symbol();
-const _parent = Symbol();
+const _parent = Symbol("parent");
const _setAttributes = Symbol();
const _validator = Symbol();
@@ -78,18 +92,27 @@ class XFAObject {
if (node instanceof XFAObjectArray) {
if (node.push(child)) {
- child[_parent] = this;
- this[_children].push(child);
+ this[$appendChild](child);
return true;
}
- } else if (node === null) {
+ } else {
+ // IRL it's possible to already have a node.
+ // So just replace it with the last version.
+ if (node !== null) {
+ this[$removeChild](node);
+ }
this[name] = child;
- child[_parent] = this;
- this[_children].push(child);
+ this[$appendChild](child);
return true;
}
- warn(`XFA - node "${this[$nodeName]}" has already enough "${name}"!`);
+ let id = "";
+ if (this.id) {
+ id = ` (id: ${this.id})`;
+ } else if (this.name) {
+ id = ` (name: ${this.name} ${this.h.value})`;
+ }
+ warn(`XFA - node "${this[$nodeName]}"${id} has already enough "${name}"!`);
return false;
}
@@ -106,6 +129,22 @@ class XFAObject {
}
}
+ [$appendChild](child) {
+ child[_parent] = this;
+ this[_children].push(child);
+ }
+
+ [$removeChild](child) {
+ const i = this[_children].indexOf(child);
+ this[_children].splice(i, 1);
+ }
+
+ [$hasSettableValue]() {
+ return this.hasOwnProperty("value");
+ }
+
+ [$setValue](_) {}
+
[$onText](_) {}
[$finalize]() {}
@@ -118,6 +157,19 @@ class XFAObject {
}
}
+ [$hasItem]() {
+ return false;
+ }
+
+ [$indexOf](child) {
+ return this[_children].indexOf(child);
+ }
+
+ [$insertAt](i, child) {
+ child[_parent] = this;
+ this[_children].splice(i, 0, child);
+ }
+
[$isTransparent]() {
return this.name === "";
}
@@ -126,6 +178,13 @@ class XFAObject {
return "";
}
+ [$text]() {
+ if (this[_children].length === 0) {
+ return this[$content];
+ }
+ return this[_children].map(c => c[$text]()).join("");
+ }
+
get [_attributeNames]() {
// Lazily get attributes names
const proto = Object.getPrototypeOf(this);
@@ -145,6 +204,17 @@ class XFAObject {
return shadow(this, _attributeNames, proto._attributes);
}
+ [$isDescendent](parent) {
+ let node = this;
+ while (node) {
+ if (node === parent) {
+ return true;
+ }
+ node = node[$getParent]();
+ }
+ return false;
+ }
+
[$getParent]() {
return this[_parent];
}
@@ -297,7 +367,7 @@ class XFAObject {
i < ii;
i++
) {
- const child = proto[_children][i][_clone]();
+ const child = proto[_children][i][$clone]();
if (value.push(child)) {
child[_parent] = this;
this[_children].push(child);
@@ -316,7 +386,7 @@ class XFAObject {
}
if (protoValue !== null) {
- const child = protoValue[_clone]();
+ const child = protoValue[$clone]();
child[_parent] = this;
this[name] = child;
this[_children].push(child);
@@ -335,7 +405,7 @@ class XFAObject {
return obj;
}
- [_clone]() {
+ [$clone]() {
const clone = Object.create(Object.getPrototypeOf(this));
for (const $symbol of Object.getOwnPropertySymbols(this)) {
try {
@@ -361,7 +431,7 @@ class XFAObject {
for (const child of this[_children]) {
const name = child[$nodeName];
- const clonedChild = child[_clone]();
+ const clonedChild = child[$clone]();
clone[_children].push(clonedChild);
clonedChild[_parent] = clone;
if (clone[name] === null) {
@@ -444,15 +514,19 @@ class XFAObjectArray {
: this[_children].map(x => x[$dump]());
}
- [_clone]() {
+ [$clone]() {
const clone = new XFAObjectArray(this[_max]);
- clone[_children] = this[_children].map(c => c[_clone]());
+ clone[_children] = this[_children].map(c => c[$clone]());
return clone;
}
get children() {
return this[_children];
}
+
+ clear() {
+ this[_children].length = 0;
+ }
}
class XFAAttribute {
@@ -460,6 +534,7 @@ class XFAAttribute {
this[_parent] = node;
this[$nodeName] = name;
this[$content] = value;
+ this[$consumed] = false;
}
[$getParent]() {
@@ -473,27 +548,46 @@ class XFAAttribute {
[$text]() {
return this[$content];
}
+
+ [$isDescendent](parent) {
+ return this[_parent] === parent || this[_parent][$isDescendent](parent);
+ }
}
class XmlObject extends XFAObject {
- constructor(nsId, name, attributes = null) {
+ constructor(nsId, name, attributes = {}) {
super(nsId, name);
this[$content] = "";
+ this[_dataValue] = null;
if (name !== "#text") {
- this[_attributes] = attributes;
+ const map = new Map();
+ this[_attributes] = map;
+ for (const [attrName, value] of Object.entries(attributes)) {
+ map.set(attrName, new XFAAttribute(this, attrName, value));
+ }
+ if (attributes.hasOwnProperty($nsAttributes)) {
+ // XFA attributes.
+ const dataNode = attributes[$nsAttributes].xfa.dataNode;
+ if (dataNode !== undefined) {
+ if (dataNode === "dataGroup") {
+ this[_dataValue] = false;
+ } else if (dataNode === "dataValue") {
+ this[_dataValue] = true;
+ }
+ }
+ }
}
+ this[$consumed] = false;
}
[$onChild](child) {
if (this[$content]) {
const node = new XmlObject(this[$namespaceId], "#text");
- node[_parent] = this;
+ this[$appendChild](node);
node[$content] = this[$content];
this[$content] = "";
- this[_children].push(node);
}
- child[_parent] = this;
- this[_children].push(child);
+ this[$appendChild](child);
return true;
}
@@ -504,20 +598,12 @@ class XmlObject extends XFAObject {
[$finalize]() {
if (this[$content] && this[_children].length > 0) {
const node = new XmlObject(this[$namespaceId], "#text");
- node[_parent] = this;
+ this[$appendChild](node);
node[$content] = this[$content];
- this[_children].push(node);
delete this[$content];
}
}
- [$text]() {
- if (this[_children].length === 0) {
- return this[$content];
- }
- return this[_children].map(c => c[$text]()).join("");
- }
-
[$getChildren](name = null) {
if (!name) {
return this[_children];
@@ -527,7 +613,7 @@ class XmlObject extends XFAObject {
}
[$getChildrenByClass](name) {
- const value = this[_attributes][name];
+ const value = this[_attributes].get(name);
if (value !== undefined) {
return value;
}
@@ -535,9 +621,9 @@ class XmlObject extends XFAObject {
}
*[$getChildrenByNameIt](name, allTransparent) {
- const value = this[_attributes][name];
- if (value !== undefined) {
- yield new XFAAttribute(this, name, value);
+ const value = this[_attributes].get(name);
+ if (value) {
+ yield value;
}
for (const child of this[_children]) {
@@ -551,19 +637,57 @@ class XmlObject extends XFAObject {
}
}
- *[$getAttributeIt](name) {
- const value = this[_attributes][name];
- if (value !== undefined) {
- yield new XFAAttribute(this, name, value);
+ *[$getAttributeIt](name, skipConsumed) {
+ const value = this[_attributes].get(name);
+ if (value && (!skipConsumed || !value[$consumed])) {
+ yield value;
}
-
for (const child of this[_children]) {
- yield* child[$getAttributeIt](name);
+ yield* child[$getAttributeIt](name, skipConsumed);
+ }
+ }
+
+ *[$getRealChildrenByNameIt](name, allTransparent, skipConsumed) {
+ for (const child of this[_children]) {
+ if (child[$nodeName] === name && (!skipConsumed || !child[$consumed])) {
+ yield child;
+ }
+
+ if (allTransparent) {
+ yield* child[$getRealChildrenByNameIt](
+ name,
+ allTransparent,
+ skipConsumed
+ );
+ }
}
}
[$isDataValue]() {
- return this[_children].length === 0;
+ if (this[_dataValue] === null) {
+ return this[_children].length === 0;
+ }
+ return this[_dataValue];
+ }
+
+ [$dump]() {
+ const dumped = Object.create(null);
+ if (this[$content]) {
+ dumped.$content = this[$content];
+ }
+ dumped.$name = this[$nodeName];
+
+ dumped.children = [];
+ for (const child of this[_children]) {
+ dumped.children.push(child[$dump]());
+ }
+
+ dumped.attributes = Object.create(null);
+ for (const [name, value] of this[_attributes]) {
+ dumped.attributes[name] = value[$content];
+ }
+
+ return dumped;
}
}
@@ -641,9 +765,13 @@ class Option10 extends IntegerObject {
}
export {
+ $appendChild,
$clean,
$cleanup,
+ $clone,
+ $consumed,
$content,
+ $data,
$dump,
$finalize,
$getAttributeIt,
@@ -652,16 +780,26 @@ export {
$getChildrenByName,
$getChildrenByNameIt,
$getParent,
+ $getRealChildrenByNameIt,
+ $global,
+ $hasItem,
+ $hasSettableValue,
+ $indexOf,
+ $insertAt,
$isDataValue,
+ $isDescendent,
$isTransparent,
$namespaceId,
$nodeName,
+ $nsAttributes,
$onChild,
$onChildCheck,
$onText,
+ $removeChild,
$resolvePrototypes,
$setId,
$setSetAttributes,
+ $setValue,
$text,
ContentObject,
IntegerObject,
diff --git a/test/unit/xfa_parser_spec.js b/test/unit/xfa_parser_spec.js
index a8eba122a..e84574288 100644
--- a/test/unit/xfa_parser_spec.js
+++ b/test/unit/xfa_parser_spec.js
@@ -20,6 +20,7 @@ import {
$getChildrenByName,
$text,
} from "../../src/core/xfa/xfa_object.js";
+import { Binder } from "../../src/core/xfa/bind.js";
import { searchNode } from "../../src/core/xfa/som.js";
import { XFAParser } from "../../src/core/xfa/parser.js";
@@ -507,39 +508,45 @@ describe("XFAParser", function () {
`;
const root = new XFAParser().parse(xml);
- expect(searchNode(root, null, "$template..Description.id")[$text]()).toBe(
- "a"
- );
- expect(searchNode(root, null, "$template..Description.id")[$text]()).toBe(
- "a"
- );
expect(
- searchNode(root, null, "$template..Description[0].id")[$text]()
+ searchNode(root, null, "$template..Description.id")[0][$text]()
).toBe("a");
expect(
- searchNode(root, null, "$template..Description[1].id")[$text]()
+ searchNode(root, null, "$template..Description.id")[0][$text]()
+ ).toBe("a");
+ expect(
+ searchNode(root, null, "$template..Description[0].id")[0][$text]()
+ ).toBe("a");
+ expect(
+ searchNode(root, null, "$template..Description[1].id")[0][$text]()
).toBe("e");
expect(
- searchNode(root, null, "$template..Description[2].id")[$text]()
+ searchNode(root, null, "$template..Description[2].id")[0][$text]()
).toBe("p");
- expect(searchNode(root, null, "$template.Receipt.id")[$text]()).toBe("l");
+ expect(searchNode(root, null, "$template.Receipt.id")[0][$text]()).toBe(
+ "l"
+ );
expect(
- searchNode(root, null, "$template.Receipt.Description[1].id")[$text]()
+ searchNode(root, null, "$template.Receipt.Description[1].id")[0][
+ $text
+ ]()
).toBe("e");
expect(searchNode(root, null, "$template.Receipt.Description[2]")).toBe(
null
);
expect(
- searchNode(root, null, "$template.Receipt.foo.Description.id")[$text]()
+ searchNode(root, null, "$template.Receipt.foo.Description.id")[0][
+ $text
+ ]()
).toBe("p");
expect(
- searchNode(root, null, "$template.#subform.Sub_Total.id")[$text]()
+ searchNode(root, null, "$template.#subform.Sub_Total.id")[0][$text]()
).toBe("i");
expect(
- searchNode(root, null, "$template.#subform.Units.id")[$text]()
+ searchNode(root, null, "$template.#subform.Units.id")[0][$text]()
).toBe("b");
expect(
- searchNode(root, null, "$template.#subform.Units.parent.id")[$text]()
+ searchNode(root, null, "$template.#subform.Units.parent.id")[0][$text]()
).toBe("m");
});
@@ -620,10 +627,10 @@ describe("XFAParser", function () {
searchNode(root, receipt, "Detail[*].Total_Price").map(x => x[$text]())
).toEqual(["250.00", "60.00"]);
- const units = searchNode(root, receipt, "Detail[1].Units");
+ const [units] = searchNode(root, receipt, "Detail[1].Units");
expect(units[$text]()).toBe("5");
- let found = searchNode(root, units, "Total_Price");
+ let [found] = searchNode(root, units, "Total_Price");
expect(found[$text]()).toBe("60.00");
found = searchNode(root, units, "Total_Pric");
@@ -645,18 +652,503 @@ describe("XFAParser", function () {
`;
const root = new XFAParser().parse(xml);
- expect(searchNode(root, null, "$data.Receipt.Detail")[$text]()).toBe(
+ expect(searchNode(root, null, "$data.Receipt.Detail")[0][$text]()).toBe(
"Acme"
);
- expect(searchNode(root, null, "$data.Receipt.Detail[0]")[$text]()).toBe(
- "Acme"
+ expect(
+ searchNode(root, null, "$data.Receipt.Detail[0]")[0][$text]()
+ ).toBe("Acme");
+ expect(
+ searchNode(root, null, "$data.Receipt.Detail[1]")[0][$text]()
+ ).toBe("foo");
+ expect(
+ searchNode(root, null, "$data.Receipt.Detail[2]")[0][$text]()
+ ).toBe("bar");
+ });
+ });
+
+ describe("Bind data into form", function () {
+ it("should make a basic binding", function () {
+ const xml = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xyz
+
+
+
+
+ `;
+ const root = new XFAParser().parse(xml);
+ const form = new Binder(root).bind();
+
+ expect(
+ searchNode(form, form, "A.B.C.value.text")[0][$dump]().$content
+ ).toBe("xyz");
+ });
+
+ it("should make another basic binding", function () {
+ const xml = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jack
+ Spratt
+
+ 99 Candlestick Lane
+ London
+ UK
+ SW1
+
+
+
+
+ `;
+ const root = new XFAParser().parse(xml);
+ const form = new Binder(root).bind();
+
+ expect(
+ searchNode(form, form, "registration.first..text")[0][$dump]().$content
+ ).toBe("Jack");
+ expect(
+ searchNode(form, form, "registration.last..text")[0][$dump]().$content
+ ).toBe("Spratt");
+ expect(
+ searchNode(form, form, "registration.apt..text")[0][$dump]().$content
+ ).toBe(undefined);
+ expect(
+ searchNode(form, form, "registration.street..text")[0][$dump]().$content
+ ).toBe("99 Candlestick Lane");
+ expect(
+ searchNode(form, form, "registration.city..text")[0][$dump]().$content
+ ).toBe("London");
+ expect(
+ searchNode(form, form, "registration.country..text")[0][$dump]()
+ .$content
+ ).toBe("UK");
+ expect(
+ searchNode(form, form, "registration.postalcode..text")[0][$dump]()
+ .$content
+ ).toBe("SW1");
+ });
+
+ it("should make basic binding with extra subform", function () {
+ const xml = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jack
+ Spratt
+
+ 99 Candlestick Lane
+ London
+ UK
+ SW1
+
+
+
+
+ `;
+ const root = new XFAParser().parse(xml);
+ const form = new Binder(root).bind();
+
+ expect(
+ searchNode(form, form, "registration..first..text")[0][$dump]().$content
+ ).toBe("Jack");
+ expect(
+ searchNode(form, form, "registration..last..text")[0][$dump]().$content
+ ).toBe("Spratt");
+ expect(
+ searchNode(form, form, "registration..apt..text")[0][$dump]().$content
+ ).toBe(undefined);
+ expect(
+ searchNode(form, form, "registration..street..text")[0][$dump]()
+ .$content
+ ).toBe("99 Candlestick Lane");
+ expect(
+ searchNode(form, form, "registration..city..text")[0][$dump]().$content
+ ).toBe("London");
+ expect(
+ searchNode(form, form, "registration..country..text")[0][$dump]()
+ .$content
+ ).toBe("UK");
+ expect(
+ searchNode(form, form, "registration..postalcode..text")[0][$dump]()
+ .$content
+ ).toBe("SW1");
+ });
+
+ it("should make basic binding with extra subform", function () {
+ const xml = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jack
+ Spratt
+
+ 7
+ 99 Candlestick Lane
+ London
+
+
+
+
+
+ `;
+ const root = new XFAParser().parse(xml);
+ const form = new Binder(root).bind();
+
+ expect(
+ searchNode(form, form, "registration..first..text")[0][$dump]().$content
+ ).toBe("Jack");
+ expect(
+ searchNode(form, form, "registration..last..text")[0][$dump]().$content
+ ).toBe("Spratt");
+ expect(
+ searchNode(form, form, "registration..apt..text")[0][$dump]().$content
+ ).toBe("7");
+ expect(
+ searchNode(form, form, "registration..street..text")[0][$dump]()
+ .$content
+ ).toBe("99 Candlestick Lane");
+ expect(
+ searchNode(form, form, "registration..city..text")[0][$dump]().$content
+ ).toBe("London");
+ });
+
+ it("should make basic binding with same names in different parts", function () {
+ const xml = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Abott
+
+ Costello
+
+
+
+
+
+ `;
+ const root = new XFAParser().parse(xml);
+ const form = new Binder(root).bind();
+
+ expect(
+ searchNode(form, form, "application.sponsor.lastname..text")[0][$dump]()
+ .$content
+ ).toBe("Costello");
+ expect(
+ searchNode(form, form, "application.lastname..text")[0][$dump]()
+ .$content
+ ).toBe("Abott");
+ });
+
+ it("should make binding and create nodes in data", function () {
+ const xml = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+ `;
+ const root = new XFAParser().parse(xml);
+ const binder = new Binder(root);
+ const form = binder.bind();
+ const data = binder.getData();
+
+ expect(searchNode(form, form, "root..b..text")[0][$dump]().$content).toBe(
+ "1"
);
- expect(searchNode(root, null, "$data.Receipt.Detail[1]")[$text]()).toBe(
- "foo"
+ expect(searchNode(data, data, "root.A.a")[0][$dump]().$name).toBe("a");
+ expect(searchNode(data, data, "root.A.B.c")[0][$dump]().$name).toBe("c");
+ expect(searchNode(data, data, "root.A.B.d")[0][$dump]().$name).toBe("d");
+ expect(searchNode(data, data, "root.A.B.C.e")[0][$dump]().$name).toBe(
+ "e"
);
- expect(searchNode(root, null, "$data.Receipt.Detail[2]")[$text]()).toBe(
- "bar"
+ expect(searchNode(data, data, "root.A.B.C.f")[0][$dump]().$name).toBe(
+ "f"
);
});
+
+ it("should make binding and set properties", function () {
+ const xml = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ foo
+
+
+
+
+ Give the name!
+
+
+
+
+
+ `;
+ const root = new XFAParser().parse(xml);
+ const form = new Binder(root).bind();
+
+ expect(
+ searchNode(form, form, "Id.LastName..text")[0][$dump]().$content
+ ).toBe("foo");
+ expect(
+ searchNode(form, form, "Id.LastName.font.typeface")[0][$text]()
+ ).toBe("myfont");
+ expect(
+ searchNode(form, form, "Id.LastName.font.size")[0][$text]()
+ ).toEqual({
+ value: 123.4,
+ unit: "pt",
+ });
+ expect(
+ searchNode(form, form, "Id.LastName.assist.toolTip")[0][$dump]()
+ .$content
+ ).toBe("Give the name!");
+ });
+
+ it("should make binding and bind items", function () {
+ const xml = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MC
+
+
+
+
+ `;
+ const root = new XFAParser().parse(xml);
+ const form = new Binder(root).bind();
+ expect(
+ searchNode(form, form, "subform.CardName.items[*].text[*]").map(x =>
+ x[$text]()
+ )
+ ).toEqual([
+ "Visa",
+ "Mastercard",
+ "American Express",
+ "VISA",
+ "MC",
+ "AMEX",
+ ]);
+ });
+
+ it("should make binding with occurrences in consumeData mode", function () {
+ const xml = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ const root = new XFAParser().parse(xml);
+ const form = new Binder(root).bind();
+
+ expect(
+ searchNode(form, form, "root.section[*].id").map(x => x[$text]())
+ ).toEqual(["section1", "section1"]);
+
+ expect(
+ searchNode(form, form, "root.section[*].line-item..text").map(x =>
+ x[$text]()
+ )
+ ).toEqual(["item1", "item2"]);
+ });
+
+ it("should make binding with occurrences in matchTemplate mode", function () {
+ const xml = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ const root = new XFAParser().parse(xml);
+ const form = new Binder(root).bind();
+
+ expect(
+ searchNode(form, form, "root.section[*].id").map(x => x[$text]())
+ ).toEqual(["section1", "section1", "section2", "section2"]);
+
+ expect(
+ searchNode(form, form, "root.section[*].line-item..text").map(x =>
+ x[$text]()
+ )
+ ).toEqual(["item1", "item2", "item1", "item2"]);
+ });
});
});