Merge pull request #13018 from calixteman/xfa_bind

XFA - Create Form DOM in merging template and data trees
This commit is contained in:
Brendan Dahl 2021-03-10 10:27:06 -08:00 committed by GitHub
commit 3d4bb5e5c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1584 additions and 109 deletions

593
src/core/xfa/bind.js Normal file
View File

@ -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:
// <field name="LastName" ...>
// <setProperty ref="$data.Main.Style.NameFont" target="font.typeface"/>
// <setProperty ref="$data.Main.Style.NameSize" target="font.size"/>
// <setProperty ref="$data.Main.Help.LastName" target="assist.toolTip"/>
// </field>
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:
// <field name="CardName"...>
// <bindItems ref="$data.main.ccs.cc[*]" labelRef="uiname"
// valueRef="token"/>
// <ui><choiceList/></ui>
// </field>
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 };

View File

@ -17,6 +17,7 @@ import { $buildXFAObject, NamespaceIds } from "./namespaces.js";
import { import {
$cleanup, $cleanup,
$finalize, $finalize,
$nsAttributes,
$onChild, $onChild,
$resolvePrototypes, $resolvePrototypes,
XFAObject, XFAObject,
@ -88,6 +89,25 @@ class Builder {
this._addNamespacePrefix(prefixes); 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 namespaceToUse = this._getNamespaceToUse(nsPrefix);
const node = const node =
(namespaceToUse && namespaceToUse[$buildXFAObject](name, attributes)) || (namespaceToUse && namespaceToUse[$buildXFAObject](name, attributes)) ||

View File

@ -13,14 +13,16 @@
* limitations under the License. * limitations under the License.
*/ */
import { $buildXFAObject, NamespaceIds } from "./namespaces.js";
import { import {
$appendChild,
$global,
$namespaceId, $namespaceId,
$nodeName, $nodeName,
$onChildCheck, $onChild,
XFAObject, XFAObject,
XmlObject, XmlObject,
} from "./xfa_object.js"; } from "./xfa_object.js";
import { $buildXFAObject, NamespaceIds } from "./namespaces.js";
const DATASETS_NS_ID = NamespaceIds.datasets.id; const DATASETS_NS_ID = NamespaceIds.datasets.id;
@ -37,15 +39,18 @@ class Datasets extends XFAObject {
this.Signature = null; this.Signature = null;
} }
[$onChildCheck](child) { [$onChild](child) {
const name = child[$nodeName]; const name = child[$nodeName];
if (name === "data") { if (
return child[$namespaceId] === DATASETS_NS_ID; (name === "data" && child[$namespaceId] === DATASETS_NS_ID) ||
(name === "Signature" &&
child[$namespaceId] === NamespaceIds.signature.id)
) {
this[name] = child;
} else {
child[$global] = true;
} }
if (name === "Signature") { this[$appendChild](child);
return child[$namespaceId] === NamespaceIds.signature.id;
}
return false;
} }
} }

33
src/core/xfa/factory.js Normal file
View File

@ -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 };

View File

@ -13,7 +13,14 @@
* limitations under the License. * 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 { XMLParserBase, XMLParserErrorCode } from "../xml_parser.js";
import { Builder } from "./builder.js"; import { Builder } from "./builder.js";
import { warn } from "../../shared/util.js"; import { warn } from "../../shared/util.js";
@ -57,7 +64,7 @@ class XFAParser extends XMLParserBase {
// namespaces information. // namespaces information.
let namespace = null; let namespace = null;
let prefixes = null; let prefixes = null;
const attributeObj = Object.create(null); const attributeObj = Object.create({});
for (const { name, value } of attributes) { for (const { name, value } of attributes) {
if (name === "xmlns") { if (name === "xmlns") {
if (!namespace) { if (!namespace) {
@ -72,7 +79,23 @@ class XFAParser extends XMLParserBase {
} }
prefixes.push({ prefix, value }); prefixes.push({ prefix, value });
} else { } 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 <foo xfa:dataNode="dataGroup"/>
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;
}
} }
} }

View File

@ -14,11 +14,14 @@
*/ */
import { import {
$appendChild,
$getChildrenByClass, $getChildrenByClass,
$getChildrenByName, $getChildrenByName,
$getParent, $getParent,
$namespaceId,
XFAObject, XFAObject,
XFAObjectArray, XFAObjectArray,
XmlObject,
} from "./xfa_object.js"; } from "./xfa_object.js";
import { warn } from "../../shared/util.js"; import { warn } from "../../shared/util.js";
@ -33,17 +36,18 @@ const operators = {
}; };
const shortcuts = new Map([ const shortcuts = new Map([
["$data", root => root.datasets.data], ["$data", (root, current) => root.datasets.data],
["$template", root => root.template], ["$template", (root, current) => root.template],
["$connectionSet", root => root.connectionSet], ["$connectionSet", (root, current) => root.connectionSet],
["$form", root => root.form], ["$form", (root, current) => root.form],
["$layout", root => root.layout], ["$layout", (root, current) => root.layout],
["$host", root => root.host], ["$host", (root, current) => root.host],
["$dataWindow", root => root.dataWindow], ["$dataWindow", (root, current) => root.dataWindow],
["$event", root => root.event], ["$event", (root, current) => root.event],
["!", root => root.datasets], ["!", (root, current) => root.datasets],
["$xfa", root => root], ["$xfa", (root, current) => root],
["xfa", root => root], ["xfa", (root, current) => root],
["$", (root, current) => current],
]); ]);
const somCache = new WeakMap(); const somCache = new WeakMap();
@ -138,17 +142,24 @@ function parseExpression(expr, dotDotAllowed) {
return parsed; return parsed;
} }
function searchNode(root, container, expr, dotDotAllowed = true) { function searchNode(
root,
container,
expr,
dotDotAllowed = true,
useCache = true
) {
const parsed = parseExpression(expr, dotDotAllowed); const parsed = parseExpression(expr, dotDotAllowed);
if (!parsed) { if (!parsed) {
return null; return null;
} }
const fn = shortcuts.get(parsed[0].name); const fn = shortcuts.get(parsed[0].name);
let i = 0; let i = 0;
let isQualified; let isQualified;
if (fn) { if (fn) {
isQualified = true; isQualified = true;
root = [fn(root)]; root = [fn(root, container)];
i = 1; i = 1;
} else { } else {
isQualified = container === null; isQualified = container === null;
@ -163,13 +174,17 @@ function searchNode(root, container, expr, dotDotAllowed = true) {
continue; continue;
} }
let cached = somCache.get(node); let children, cached;
if (!cached) {
cached = new Map(); if (useCache) {
somCache.set(node, cached); cached = somCache.get(node);
if (!cached) {
cached = new Map();
somCache.set(node, cached);
}
children = cached.get(cacheName);
} }
let children = cached.get(cacheName);
if (!children) { if (!children) {
switch (operator) { switch (operator) {
case operators.dot: case operators.dot:
@ -189,7 +204,9 @@ function searchNode(root, container, expr, dotDotAllowed = true) {
default: default:
break; break;
} }
cached.set(cacheName, children); if (useCache) {
cached.set(cacheName, children);
}
} }
if (children.length > 0) { if (children.length > 0) {
@ -222,11 +239,72 @@ function searchNode(root, container, expr, dotDotAllowed = true) {
return null; return null;
} }
if (root.length === 1) {
return root[0];
}
return root; 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 };

View File

@ -13,15 +13,19 @@
* limitations under the License. * limitations under the License.
*/ */
import { $buildXFAObject, NamespaceIds } from "./namespaces.js";
import { import {
$appendChild,
$content, $content,
$finalize, $finalize,
$hasItem,
$hasSettableValue,
$isTransparent, $isTransparent,
$namespaceId, $namespaceId,
$nodeName, $nodeName,
$onChild, $onChild,
$removeChild,
$setSetAttributes, $setSetAttributes,
$setValue,
ContentObject, ContentObject,
Option01, Option01,
OptionObject, OptionObject,
@ -29,6 +33,7 @@ import {
XFAObject, XFAObject,
XFAObjectArray, XFAObjectArray,
} from "./xfa_object.js"; } from "./xfa_object.js";
import { $buildXFAObject, NamespaceIds } from "./namespaces.js";
import { import {
getBBox, getBBox,
getColor, getColor,
@ -44,6 +49,15 @@ import { warn } from "../../shared/util.js";
const TEMPLATE_NS_ID = NamespaceIds.template.id; 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 { class AppearanceFilter extends StringObject {
constructor(attributes) { constructor(attributes) {
super(TEMPLATE_NS_ID, "appearanceFilter"); super(TEMPLATE_NS_ID, "appearanceFilter");
@ -496,6 +510,10 @@ class Caption extends XFAObject {
this.para = null; this.para = null;
this.value = null; this.value = null;
} }
[$setValue](value) {
_setValue(this, value);
}
} }
class Certificate extends StringObject { class Certificate extends StringObject {
@ -586,6 +604,10 @@ class Color extends XFAObject {
this.value = getColor(attributes.value); this.value = getColor(attributes.value);
this.extras = null; this.extras = null;
} }
[$hasSettableValue]() {
return false;
}
} }
class Comb extends XFAObject { class Comb extends XFAObject {
@ -869,6 +891,10 @@ class Draw extends XFAObject {
this.value = null; this.value = null;
this.setProperty = new XFAObjectArray(); this.setProperty = new XFAObjectArray();
} }
[$setValue](value) {
_setValue(this, value);
}
} }
class Edge extends XFAObject { class Edge extends XFAObject {
@ -1045,7 +1071,7 @@ class Event extends XFAObject {
} }
class ExData extends ContentObject { class ExData extends ContentObject {
constructor(builder, attributes) { constructor(attributes) {
super(TEMPLATE_NS_ID, "exData"); super(TEMPLATE_NS_ID, "exData");
this.contentType = attributes.contentType || ""; this.contentType = attributes.contentType || "";
this.href = attributes.href || ""; this.href = attributes.href || "";
@ -1188,6 +1214,32 @@ class ExclGroup extends XFAObject {
this.field = new XFAObjectArray(); this.field = new XFAObjectArray();
this.setProperty = 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 { class Execute extends XFAObject {
@ -1294,6 +1346,8 @@ class Field extends XFAObject {
this.extras = null; this.extras = null;
this.font = null; this.font = null;
this.format = 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.items = new XFAObjectArray(2);
this.keep = null; this.keep = null;
this.margin = null; this.margin = null;
@ -1307,6 +1361,10 @@ class Field extends XFAObject {
this.event = new XFAObjectArray(); this.event = new XFAObjectArray();
this.setProperty = new XFAObjectArray(); this.setProperty = new XFAObjectArray();
} }
[$setValue](value) {
_setValue(this, value);
}
} }
class Fill extends XFAObject { class Fill extends XFAObject {
@ -1590,6 +1648,15 @@ class Items extends XFAObject {
this.text = new XFAObjectArray(); this.text = new XFAObjectArray();
this.time = 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 { class Keep extends XFAObject {
@ -1917,10 +1984,7 @@ class Para extends XFAObject {
"right", "right",
]); ]);
this.id = attributes.id || ""; this.id = attributes.id || "";
this.lineHeight = getMeasurement(attributes.lineHeight, [ this.lineHeight = getMeasurement(attributes.lineHeight, "0pt");
"0pt",
"measurement",
]);
this.marginLeft = getMeasurement(attributes.marginLeft, "0"); this.marginLeft = getMeasurement(attributes.marginLeft, "0");
this.marginRight = getMeasurement(attributes.marginRight, "0"); this.marginRight = getMeasurement(attributes.marginRight, "0");
this.orphans = getInteger({ this.orphans = getInteger({
@ -2735,6 +2799,26 @@ class Value extends XFAObject {
this.text = null; this.text = null;
this.time = 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 { class Variables extends XFAObject {
@ -3225,4 +3309,13 @@ class TemplateNamespace {
} }
} }
export { Template, TemplateNamespace }; export {
BindItems,
Field,
Items,
SetProperty,
Template,
TemplateNamespace,
Text,
Value,
};

View File

@ -19,43 +19,57 @@ import { NamespaceIds } from "./namespaces.js";
// We use these symbols to avoid name conflict between tags // We use these symbols to avoid name conflict between tags
// and properties/methods names. // and properties/methods names.
const $appendChild = Symbol();
const $clean = Symbol(); const $clean = Symbol();
const $cleanup = Symbol(); const $cleanup = Symbol();
const $clone = Symbol();
const $consumed = Symbol();
const $content = Symbol("content"); const $content = Symbol("content");
const $data = Symbol("data");
const $dump = Symbol(); const $dump = Symbol();
const $finalize = Symbol(); const $finalize = Symbol();
const $isDataValue = Symbol();
const $getAttributeIt = Symbol(); const $getAttributeIt = Symbol();
const $getChildrenByClass = Symbol(); const $getChildrenByClass = Symbol();
const $getChildrenByName = Symbol(); const $getChildrenByName = Symbol();
const $getChildrenByNameIt = Symbol(); const $getChildrenByNameIt = Symbol();
const $getRealChildrenByNameIt = Symbol();
const $getChildren = Symbol(); const $getChildren = Symbol();
const $getParent = 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 $isTransparent = Symbol();
const $lastAttribute = Symbol(); const $lastAttribute = Symbol();
const $namespaceId = Symbol("namespaceId"); const $namespaceId = Symbol("namespaceId");
const $nodeName = Symbol("nodeName"); const $nodeName = Symbol("nodeName");
const $nsAttributes = Symbol();
const $onChild = Symbol(); const $onChild = Symbol();
const $onChildCheck = Symbol(); const $onChildCheck = Symbol();
const $onText = Symbol(); const $onText = Symbol();
const $removeChild = Symbol();
const $resolvePrototypes = Symbol(); const $resolvePrototypes = Symbol();
const $setId = Symbol(); const $setId = Symbol();
const $setSetAttributes = Symbol(); const $setSetAttributes = Symbol();
const $setValue = Symbol();
const $text = Symbol(); const $text = Symbol();
const _applyPrototype = Symbol(); const _applyPrototype = Symbol();
const _attributes = Symbol(); const _attributes = Symbol();
const _attributeNames = Symbol(); const _attributeNames = Symbol();
const _children = Symbol(); const _children = Symbol("_children");
const _clone = Symbol();
const _cloneAttribute = Symbol(); const _cloneAttribute = Symbol();
const _dataValue = Symbol();
const _defaultValue = Symbol(); const _defaultValue = Symbol();
const _getPrototype = Symbol(); const _getPrototype = Symbol();
const _getUnsetAttributes = Symbol(); const _getUnsetAttributes = Symbol();
const _hasChildren = Symbol(); const _hasChildren = Symbol();
const _max = Symbol(); const _max = Symbol();
const _options = Symbol(); const _options = Symbol();
const _parent = Symbol(); const _parent = Symbol("parent");
const _setAttributes = Symbol(); const _setAttributes = Symbol();
const _validator = Symbol(); const _validator = Symbol();
@ -78,18 +92,27 @@ class XFAObject {
if (node instanceof XFAObjectArray) { if (node instanceof XFAObjectArray) {
if (node.push(child)) { if (node.push(child)) {
child[_parent] = this; this[$appendChild](child);
this[_children].push(child);
return true; 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; this[name] = child;
child[_parent] = this; this[$appendChild](child);
this[_children].push(child);
return true; 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; 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](_) {} [$onText](_) {}
[$finalize]() {} [$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]() { [$isTransparent]() {
return this.name === ""; return this.name === "";
} }
@ -126,6 +178,13 @@ class XFAObject {
return ""; return "";
} }
[$text]() {
if (this[_children].length === 0) {
return this[$content];
}
return this[_children].map(c => c[$text]()).join("");
}
get [_attributeNames]() { get [_attributeNames]() {
// Lazily get attributes names // Lazily get attributes names
const proto = Object.getPrototypeOf(this); const proto = Object.getPrototypeOf(this);
@ -145,6 +204,17 @@ class XFAObject {
return shadow(this, _attributeNames, proto._attributes); return shadow(this, _attributeNames, proto._attributes);
} }
[$isDescendent](parent) {
let node = this;
while (node) {
if (node === parent) {
return true;
}
node = node[$getParent]();
}
return false;
}
[$getParent]() { [$getParent]() {
return this[_parent]; return this[_parent];
} }
@ -297,7 +367,7 @@ class XFAObject {
i < ii; i < ii;
i++ i++
) { ) {
const child = proto[_children][i][_clone](); const child = proto[_children][i][$clone]();
if (value.push(child)) { if (value.push(child)) {
child[_parent] = this; child[_parent] = this;
this[_children].push(child); this[_children].push(child);
@ -316,7 +386,7 @@ class XFAObject {
} }
if (protoValue !== null) { if (protoValue !== null) {
const child = protoValue[_clone](); const child = protoValue[$clone]();
child[_parent] = this; child[_parent] = this;
this[name] = child; this[name] = child;
this[_children].push(child); this[_children].push(child);
@ -335,7 +405,7 @@ class XFAObject {
return obj; return obj;
} }
[_clone]() { [$clone]() {
const clone = Object.create(Object.getPrototypeOf(this)); const clone = Object.create(Object.getPrototypeOf(this));
for (const $symbol of Object.getOwnPropertySymbols(this)) { for (const $symbol of Object.getOwnPropertySymbols(this)) {
try { try {
@ -361,7 +431,7 @@ class XFAObject {
for (const child of this[_children]) { for (const child of this[_children]) {
const name = child[$nodeName]; const name = child[$nodeName];
const clonedChild = child[_clone](); const clonedChild = child[$clone]();
clone[_children].push(clonedChild); clone[_children].push(clonedChild);
clonedChild[_parent] = clone; clonedChild[_parent] = clone;
if (clone[name] === null) { if (clone[name] === null) {
@ -444,15 +514,19 @@ class XFAObjectArray {
: this[_children].map(x => x[$dump]()); : this[_children].map(x => x[$dump]());
} }
[_clone]() { [$clone]() {
const clone = new XFAObjectArray(this[_max]); const clone = new XFAObjectArray(this[_max]);
clone[_children] = this[_children].map(c => c[_clone]()); clone[_children] = this[_children].map(c => c[$clone]());
return clone; return clone;
} }
get children() { get children() {
return this[_children]; return this[_children];
} }
clear() {
this[_children].length = 0;
}
} }
class XFAAttribute { class XFAAttribute {
@ -460,6 +534,7 @@ class XFAAttribute {
this[_parent] = node; this[_parent] = node;
this[$nodeName] = name; this[$nodeName] = name;
this[$content] = value; this[$content] = value;
this[$consumed] = false;
} }
[$getParent]() { [$getParent]() {
@ -473,27 +548,46 @@ class XFAAttribute {
[$text]() { [$text]() {
return this[$content]; return this[$content];
} }
[$isDescendent](parent) {
return this[_parent] === parent || this[_parent][$isDescendent](parent);
}
} }
class XmlObject extends XFAObject { class XmlObject extends XFAObject {
constructor(nsId, name, attributes = null) { constructor(nsId, name, attributes = {}) {
super(nsId, name); super(nsId, name);
this[$content] = ""; this[$content] = "";
this[_dataValue] = null;
if (name !== "#text") { 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) { [$onChild](child) {
if (this[$content]) { if (this[$content]) {
const node = new XmlObject(this[$namespaceId], "#text"); const node = new XmlObject(this[$namespaceId], "#text");
node[_parent] = this; this[$appendChild](node);
node[$content] = this[$content]; node[$content] = this[$content];
this[$content] = ""; this[$content] = "";
this[_children].push(node);
} }
child[_parent] = this; this[$appendChild](child);
this[_children].push(child);
return true; return true;
} }
@ -504,20 +598,12 @@ class XmlObject extends XFAObject {
[$finalize]() { [$finalize]() {
if (this[$content] && this[_children].length > 0) { if (this[$content] && this[_children].length > 0) {
const node = new XmlObject(this[$namespaceId], "#text"); const node = new XmlObject(this[$namespaceId], "#text");
node[_parent] = this; this[$appendChild](node);
node[$content] = this[$content]; node[$content] = this[$content];
this[_children].push(node);
delete this[$content]; delete this[$content];
} }
} }
[$text]() {
if (this[_children].length === 0) {
return this[$content];
}
return this[_children].map(c => c[$text]()).join("");
}
[$getChildren](name = null) { [$getChildren](name = null) {
if (!name) { if (!name) {
return this[_children]; return this[_children];
@ -527,7 +613,7 @@ class XmlObject extends XFAObject {
} }
[$getChildrenByClass](name) { [$getChildrenByClass](name) {
const value = this[_attributes][name]; const value = this[_attributes].get(name);
if (value !== undefined) { if (value !== undefined) {
return value; return value;
} }
@ -535,9 +621,9 @@ class XmlObject extends XFAObject {
} }
*[$getChildrenByNameIt](name, allTransparent) { *[$getChildrenByNameIt](name, allTransparent) {
const value = this[_attributes][name]; const value = this[_attributes].get(name);
if (value !== undefined) { if (value) {
yield new XFAAttribute(this, name, value); yield value;
} }
for (const child of this[_children]) { for (const child of this[_children]) {
@ -551,19 +637,57 @@ class XmlObject extends XFAObject {
} }
} }
*[$getAttributeIt](name) { *[$getAttributeIt](name, skipConsumed) {
const value = this[_attributes][name]; const value = this[_attributes].get(name);
if (value !== undefined) { if (value && (!skipConsumed || !value[$consumed])) {
yield new XFAAttribute(this, name, value); yield value;
} }
for (const child of this[_children]) { 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]() { [$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 { export {
$appendChild,
$clean, $clean,
$cleanup, $cleanup,
$clone,
$consumed,
$content, $content,
$data,
$dump, $dump,
$finalize, $finalize,
$getAttributeIt, $getAttributeIt,
@ -652,16 +780,26 @@ export {
$getChildrenByName, $getChildrenByName,
$getChildrenByNameIt, $getChildrenByNameIt,
$getParent, $getParent,
$getRealChildrenByNameIt,
$global,
$hasItem,
$hasSettableValue,
$indexOf,
$insertAt,
$isDataValue, $isDataValue,
$isDescendent,
$isTransparent, $isTransparent,
$namespaceId, $namespaceId,
$nodeName, $nodeName,
$nsAttributes,
$onChild, $onChild,
$onChildCheck, $onChildCheck,
$onText, $onText,
$removeChild,
$resolvePrototypes, $resolvePrototypes,
$setId, $setId,
$setSetAttributes, $setSetAttributes,
$setValue,
$text, $text,
ContentObject, ContentObject,
IntegerObject, IntegerObject,

View File

@ -20,6 +20,7 @@ import {
$getChildrenByName, $getChildrenByName,
$text, $text,
} from "../../src/core/xfa/xfa_object.js"; } from "../../src/core/xfa/xfa_object.js";
import { Binder } from "../../src/core/xfa/bind.js";
import { searchNode } from "../../src/core/xfa/som.js"; import { searchNode } from "../../src/core/xfa/som.js";
import { XFAParser } from "../../src/core/xfa/parser.js"; import { XFAParser } from "../../src/core/xfa/parser.js";
@ -507,39 +508,45 @@ describe("XFAParser", function () {
</xdp:xdp> </xdp:xdp>
`; `;
const root = new XFAParser().parse(xml); 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( expect(
searchNode(root, null, "$template..Description[0].id")[$text]() searchNode(root, null, "$template..Description.id")[0][$text]()
).toBe("a"); ).toBe("a");
expect( 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"); ).toBe("e");
expect( expect(
searchNode(root, null, "$template..Description[2].id")[$text]() searchNode(root, null, "$template..Description[2].id")[0][$text]()
).toBe("p"); ).toBe("p");
expect(searchNode(root, null, "$template.Receipt.id")[$text]()).toBe("l"); expect(searchNode(root, null, "$template.Receipt.id")[0][$text]()).toBe(
"l"
);
expect( expect(
searchNode(root, null, "$template.Receipt.Description[1].id")[$text]() searchNode(root, null, "$template.Receipt.Description[1].id")[0][
$text
]()
).toBe("e"); ).toBe("e");
expect(searchNode(root, null, "$template.Receipt.Description[2]")).toBe( expect(searchNode(root, null, "$template.Receipt.Description[2]")).toBe(
null null
); );
expect( expect(
searchNode(root, null, "$template.Receipt.foo.Description.id")[$text]() searchNode(root, null, "$template.Receipt.foo.Description.id")[0][
$text
]()
).toBe("p"); ).toBe("p");
expect( expect(
searchNode(root, null, "$template.#subform.Sub_Total.id")[$text]() searchNode(root, null, "$template.#subform.Sub_Total.id")[0][$text]()
).toBe("i"); ).toBe("i");
expect( expect(
searchNode(root, null, "$template.#subform.Units.id")[$text]() searchNode(root, null, "$template.#subform.Units.id")[0][$text]()
).toBe("b"); ).toBe("b");
expect( expect(
searchNode(root, null, "$template.#subform.Units.parent.id")[$text]() searchNode(root, null, "$template.#subform.Units.parent.id")[0][$text]()
).toBe("m"); ).toBe("m");
}); });
@ -620,10 +627,10 @@ describe("XFAParser", function () {
searchNode(root, receipt, "Detail[*].Total_Price").map(x => x[$text]()) searchNode(root, receipt, "Detail[*].Total_Price").map(x => x[$text]())
).toEqual(["250.00", "60.00"]); ).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"); 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"); expect(found[$text]()).toBe("60.00");
found = searchNode(root, units, "Total_Pric"); found = searchNode(root, units, "Total_Pric");
@ -645,18 +652,503 @@ describe("XFAParser", function () {
</xdp:xdp> </xdp:xdp>
`; `;
const root = new XFAParser().parse(xml); 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" "Acme"
); );
expect(searchNode(root, null, "$data.Receipt.Detail[0]")[$text]()).toBe( expect(
"Acme" 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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3">
<subform name="A">
<subform name="B">
<field name="C">
</field>
<field name="D">
</field>
</subform>
</subform>
</template>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<A>
<C>xyz</C>
</A>
</xfa:data>
</xfa:datasets>
</xdp:xdp>
`;
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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3">
<subform name="registration">
<field name="first"> </field>
<field name="last"> </field>
<field name="apt"> </field>
<field name="street"> </field>
<field name="city"> </field>
<field name="country"> </field>
<field name="postalcode"/>
</subform>
</template>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<registration>
<first>Jack</first>
<last>Spratt</last>
<apt/>
<street>99 Candlestick Lane</street>
<city>London</city>
<country>UK</country>
<postalcode>SW1</postalcode>
</registration>
</xfa:data>
</xfa:datasets>
</xdp:xdp>
`;
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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3">
<subform name="registration">
<field name="first"> </field>
<field name="last"> </field>
<subform name="address">
<field name="apt"> </field>
<field name="street"> </field>
<field name="city"> </field>
<field name="country"> </field>
<field name="postalcode"> </field>
</subform>
</subform>
</template>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<registration>
<first>Jack</first>
<last>Spratt</last>
<apt/>
<street>99 Candlestick Lane</street>
<city>London</city>
<country>UK</country>
<postalcode>SW1</postalcode>
</registration>
</xfa:data>
</xfa:datasets>
</xdp:xdp>
`;
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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3">
<subform name="registration" mergeMode="consumeData">
<subform name="address">
<field name="first"/>
<field name="last"/>
<field name="apt"/>
<field name="street"/>
<field name="city"/>
</subform>
</subform>
</template>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<registration>
<first>Jack</first>
<last>Spratt</last>
<address>
<apt>7</apt>
<street>99 Candlestick Lane</street>
<city>London</city>
</address>
</registration>
</xfa:data>
</xfa:datasets>
</xdp:xdp>
`;
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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3">
<subform name="application" mergeMode="consumeData">
<subform name="sponsor">
<field name="lastname"> </field>
<!-- sponsor's last name -->
</subform>
<field name="lastname"> </field>
<!-- applicant's last name -->
</subform>
</template>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<application>
<lastname>Abott</lastname>
<sponsor>
<lastname>Costello</lastname>
</sponsor>
</application>
</xfa:data>
</xfa:datasets>
</xdp:xdp>
`;
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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3">
<subform name="root" mergeMode="matchTemplate">
<subform name="A">
<field name="a"/>
<field name="b"/>
<subform name="B">
<field name="c"/>
<field name="d"/>
<subform name="C">
<field name="e"/>
<field name="f"/>
</subform>
</subform>
</subform>
</subform>
</template>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<root>
<A>
<b>1</b>
</A>
</root>
</xfa:data>
</xfa:datasets>
</xdp:xdp>
`;
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( expect(searchNode(data, data, "root.A.a")[0][$dump]().$name).toBe("a");
"foo" 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( expect(searchNode(data, data, "root.A.B.C.f")[0][$dump]().$name).toBe(
"bar" "f"
); );
}); });
it("should make binding and set properties", function () {
const xml = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3">
<subform name="Id">
<field name="LastName">
<setProperty ref="$data.Main.Style.NameFont" target="font.typeface"/>
<setProperty ref="$data.Main.Style.NameSize" target="font.size"/>
<setProperty ref="$data.Main.Help.LastName" target="assist.toolTip"/>
<font></font>
<assist>
<toolTip>
</toolTip>
</assist>
</field>
</subform>
</template>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<Id>
<LastName>foo</LastName>
</Id>
<Main>
<Style>
<NameFont>myfont</NameFont>
<NameSize>123.4pt</NameSize>
</Style>
<Help>
<LastName>Give the name!</LastName>
</Help>
</Main>
</xfa:data>
</xfa:datasets>
</xdp:xdp>
`;
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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3">
<subform name="main">
<field name="CardName">
<bindItems ref="$data.main.ccs.cc[*]" labelRef="uiname" valueRef="token"/>
<ui>
<choiceList/>
</ui>
</field>
</subform>
</template>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<main>
<ccs>
<cc uiname="Visa" token="VISA"/>
<cc uiname="Mastercard" token="MC"/>
<cc uiname="American Express" token="AMEX"/>
</ccs>
<CardName>MC</CardName>
</main>
</xfa:data>
</xfa:datasets>
</xdp:xdp>
`;
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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3">
<subform name="root" mergeMode="consumeData">
<subform name="section" id="section1">
<occur min="0" max="-1"/>
<bind match="dataRef" ref="$.section[*]"/>
<field name="line-item"/>
</subform>
<subform name="section" id="section2">
<occur min="0" max="-1"/>
<bind match="dataRef" ref="$.section[*]"/>
<field name="line-item"/>
</subform>
</subform>
</template>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<root>
<section>
<line-item>item1</line-item>
</section>
<section>
<line-item>item2</line-item>
</section>
</root>
</xfa:data>
</xfa:datasets>
</xdp:xdp>
`;
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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3">
<subform name="root" mergeMode="matchTemplate">
<subform name="section" id="section1">
<occur min="0" max="-1"/>
<bind match="dataRef" ref="$.section[*]"/>
<field name="line-item"/>
</subform>
<subform name="section" id="section2">
<occur min="0" max="-1"/>
<bind match="dataRef" ref="$.section[*]"/>
<field name="line-item"/>
</subform>
</subform>
</template>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
<root>
<section>
<line-item>item1</line-item>
</section>
<section>
<line-item>item2</line-item>
</section>
</root>
</xfa:data>
</xfa:datasets>
</xdp:xdp>
`;
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"]);
});
}); });
}); });