Merge pull request #13018 from calixteman/xfa_bind
XFA - Create Form DOM in merging template and data trees
This commit is contained in:
commit
3d4bb5e5c5
593
src/core/xfa/bind.js
Normal file
593
src/core/xfa/bind.js
Normal 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 };
|
@ -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)) ||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
33
src/core/xfa/factory.js
Normal file
33
src/core/xfa/factory.js
Normal 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 };
|
@ -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 <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 };
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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 () {
|
||||
</xdp:xdp>
|
||||
`;
|
||||
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 () {
|
||||
</xdp:xdp>
|
||||
`;
|
||||
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 = `
|
||||
<?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(
|
||||
"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 = `
|
||||
<?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"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user