diff --git a/src/core/xfa/builder.js b/src/core/xfa/builder.js
index 89fe62482..d010b2052 100644
--- a/src/core/xfa/builder.js
+++ b/src/core/xfa/builder.js
@@ -21,6 +21,7 @@ import {
$nsAttributes,
$onChild,
$resolvePrototypes,
+ $root,
XFAObject,
} from "./xfa_object.js";
import { NamespaceSetUp } from "./setup.js";
@@ -43,6 +44,10 @@ class Root extends XFAObject {
[$finalize]() {
super[$finalize]();
if (this.element.template instanceof Template) {
+ // Set the root element in $ids using a symbol in order
+ // to avoid conflict with real IDs.
+ this[$ids].set($root, this.element);
+
this.element.template[$resolvePrototypes](this[$ids]);
this.element.template[$ids] = this[$ids];
}
diff --git a/src/core/xfa/template.js b/src/core/xfa/template.js
index 527e2c842..1906bb1de 100644
--- a/src/core/xfa/template.js
+++ b/src/core/xfa/template.js
@@ -3866,9 +3866,6 @@ class Subform extends XFAObject {
}
[$toHTML](availableSpace) {
- if (this.name === "helpText") {
- return HTMLResult.EMPTY;
- }
if (this[$extra] && this[$extra].afterBreakAfter) {
const ret = this[$extra].afterBreakAfter;
delete this[$extra];
@@ -3890,8 +3887,6 @@ class Subform extends XFAObject {
);
}
- // TODO: implement usehref (probably in bind.js).
-
// TODO: incomplete.
fixDimensions(this);
const children = [];
diff --git a/src/core/xfa/xfa_object.js b/src/core/xfa/xfa_object.js
index 4d62c20ea..1234ae91e 100644
--- a/src/core/xfa/xfa_object.js
+++ b/src/core/xfa/xfa_object.js
@@ -16,6 +16,7 @@
import { getInteger, getKeyword, HTMLResult } from "./utils.js";
import { shadow, warn } from "../../shared/util.js";
import { NamespaceIds } from "./namespaces.js";
+import { searchNode } from "./som.js";
// We use these symbols to avoid name conflict between tags
// and properties/methods names.
@@ -61,6 +62,7 @@ const $onChild = Symbol();
const $onChildCheck = Symbol();
const $onText = Symbol();
const $removeChild = Symbol();
+const $root = Symbol("root");
const $resolvePrototypes = Symbol();
const $searchNode = Symbol();
const $setId = Symbol();
@@ -85,6 +87,7 @@ const _hasChildren = Symbol();
const _max = Symbol();
const _options = Symbol();
const _parent = Symbol("parent");
+const _resolvePrototypesHelper = Symbol();
const _setAttributes = Symbol();
const _validator = Symbol();
@@ -192,8 +195,14 @@ class XFAObject {
this[_children].splice(i, 0, child);
}
+ /**
+ * If true the element is transparent when searching a node using
+ * a SOM expression which means that looking for "foo.bar" in
+ * <... name="foo"><... name="bar">...
+ * is fine because toto and titi are transparent.
+ */
[$isTransparent]() {
- return this.name === "";
+ return !this.name;
}
[$lastAttribute]() {
@@ -343,10 +352,8 @@ class XFAObject {
}
[$setSetAttributes](attributes) {
- if (attributes.use || attributes.id) {
- // Just keep set attributes because this node uses a proto or is a proto.
- this[_setAttributes] = new Set(Object.keys(attributes));
- }
+ // Just keep set attributes because it can be used in a proto.
+ this[_setAttributes] = new Set(Object.keys(attributes));
}
/**
@@ -364,57 +371,103 @@ class XFAObject {
*/
[$resolvePrototypes](ids, ancestors = new Set()) {
for (const child of this[_children]) {
- const proto = child[_getPrototype](ids, ancestors);
- if (proto) {
- // _applyPrototype will apply $resolvePrototypes with correct ancestors
- // to avoid infinite loop.
- child[_applyPrototype](proto, ids, ancestors);
- } else {
- child[$resolvePrototypes](ids, ancestors);
- }
+ child[_resolvePrototypesHelper](ids, ancestors);
+ }
+ }
+
+ [_resolvePrototypesHelper](ids, ancestors) {
+ const proto = this[_getPrototype](ids, ancestors);
+ if (proto) {
+ // _applyPrototype will apply $resolvePrototypes with correct ancestors
+ // to avoid infinite loop.
+ this[_applyPrototype](proto, ids, ancestors);
+ } else {
+ this[$resolvePrototypes](ids, ancestors);
}
}
[_getPrototype](ids, ancestors) {
- const { use } = this;
- if (use && use.startsWith("#")) {
- const id = use.slice(1);
- const proto = ids.get(id);
- this.use = "";
- if (!proto) {
- warn(`XFA - Invalid prototype id: ${id}.`);
- return null;
- }
+ const { use, usehref } = this;
+ if (!use && !usehref) {
+ return null;
+ }
- if (proto[$nodeName] !== this[$nodeName]) {
- warn(
- `XFA - Incompatible prototype: ${proto[$nodeName]} !== ${this[$nodeName]}.`
- );
- return null;
- }
+ let proto = null;
+ let somExpression = null;
+ let id = null;
+ let ref = use;
- if (ancestors.has(proto)) {
- // We've a cycle so break it.
- warn(`XFA - Cycle detected in prototypes use.`);
- return null;
+ // If usehref and use are non-empty then use usehref.
+ if (usehref) {
+ ref = usehref;
+ // Href can be one of the following:
+ // - #ID
+ // - URI#ID
+ // - #som(expression)
+ // - URI#som(expression)
+ // - URI
+ // For now we don't handle URI other than "." (current document).
+ if (usehref.startsWith("#som(") && usehref.endsWith(")")) {
+ somExpression = usehref.slice("#som(".length, usehref.length - 1);
+ } else if (usehref.startsWith(".#som(") && usehref.endsWith(")")) {
+ somExpression = usehref.slice(".#som(".length, usehref.length - 1);
+ } else if (usehref.startsWith("#")) {
+ id = usehref.slice(1);
+ } else if (usehref.startsWith(".#")) {
+ id = usehref.slice(2);
}
+ } else if (use.startsWith("#")) {
+ id = use.slice(1);
+ } else {
+ somExpression = use;
+ }
- ancestors.add(proto);
- // The prototype can have a "use" attribute itself.
- const protoProto = proto[_getPrototype](ids, ancestors);
- if (!protoProto) {
- ancestors.delete(proto);
- return proto;
+ this.use = this.usehref = "";
+ if (id) {
+ proto = ids.get(id);
+ } else {
+ proto = searchNode(
+ ids.get($root),
+ this,
+ somExpression,
+ true /* = dotDotAllowed */,
+ false /* = useCache */
+ );
+ if (proto) {
+ proto = proto[0];
}
+ }
- proto[_applyPrototype](protoProto, ids, ancestors);
+ if (!proto) {
+ warn(`XFA - Invalid prototype reference: ${ref}.`);
+ return null;
+ }
+
+ if (proto[$nodeName] !== this[$nodeName]) {
+ warn(
+ `XFA - Incompatible prototype: ${proto[$nodeName]} !== ${this[$nodeName]}.`
+ );
+ return null;
+ }
+
+ if (ancestors.has(proto)) {
+ // We've a cycle so break it.
+ warn(`XFA - Cycle detected in prototypes use.`);
+ return null;
+ }
+
+ ancestors.add(proto);
+ // The prototype can have a "use" attribute itself.
+ const protoProto = proto[_getPrototype](ids, ancestors);
+ if (!protoProto) {
ancestors.delete(proto);
-
return proto;
}
- // TODO: handle SOM expressions.
- return null;
+ proto[_applyPrototype](protoProto, ids, ancestors);
+ ancestors.delete(proto);
+
+ return proto;
}
[_applyPrototype](proto, ids, ancestors) {
@@ -449,7 +502,7 @@ class XFAObject {
if (value instanceof XFAObjectArray) {
for (const child of value[_children]) {
- child[$resolvePrototypes](ids, ancestors);
+ child[_resolvePrototypesHelper](ids, ancestors);
}
for (
@@ -461,7 +514,7 @@ class XFAObject {
if (value.push(child)) {
child[_parent] = this;
this[_children].push(child);
- child[$resolvePrototypes](ids, newAncestors);
+ child[_resolvePrototypesHelper](ids, ancestors);
} else {
// No need to continue: other nodes will be rejected.
break;
@@ -472,6 +525,10 @@ class XFAObject {
if (value !== null) {
value[$resolvePrototypes](ids, ancestors);
+ if (protoValue) {
+ // protoValue must be treated as a prototype for value.
+ value[_applyPrototype](protoValue, ids, ancestors);
+ }
continue;
}
@@ -480,7 +537,7 @@ class XFAObject {
child[_parent] = this;
this[name] = child;
this[_children].push(child);
- child[$resolvePrototypes](ids, newAncestors);
+ child[_resolvePrototypesHelper](ids, ancestors);
}
}
}
@@ -924,6 +981,7 @@ export {
$onText,
$removeChild,
$resolvePrototypes,
+ $root,
$searchNode,
$setId,
$setSetAttributes,
diff --git a/test/unit/xfa_parser_spec.js b/test/unit/xfa_parser_spec.js
index 603933d7d..6ff3e5b22 100644
--- a/test/unit/xfa_parser_spec.js
+++ b/test/unit/xfa_parser_spec.js
@@ -304,6 +304,56 @@ describe("XFAParser", function () {
expect(font.extras.id).toEqual("id2");
});
+ it("should parse a xfa document and apply some prototypes through usehref", function () {
+ const xml = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ const root = new XFAParser().parse(xml)[$dump]();
+ let font = root.template.subform.field[0].font;
+ expect(font.typeface).toEqual("Foo");
+ expect(font.overline).toEqual(0);
+ expect(font.size).toEqual(123);
+ expect(font.weight).toEqual("bold");
+ expect(font.posture).toEqual("italic");
+ expect(font.fill.color.value).toEqual({ r: 1, g: 2, b: 3 });
+ expect(font.extras).toEqual(undefined);
+
+ font = root.template.subform.field[1].font;
+ expect(font.typeface).toEqual("Foo");
+ expect(font.overline).toEqual(0);
+ expect(font.size).toEqual(456);
+ expect(font.weight).toEqual("bold");
+ expect(font.posture).toEqual("normal");
+ expect(font.fill.color.value).toEqual({ r: 4, g: 5, b: 6 });
+ expect(font.extras.id).toEqual("id2");
+ });
+
it("should parse a xfa document with xhtml", function () {
const xml = `