From 11573ddd16256d37eee482900dd3bb5a1bfe9bc3 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 31 May 2021 13:44:49 +0200 Subject: [PATCH] XFA - Implement usehref support - attribute 'use' was already implemented but not usehref - in general, usehref should make reference to current document - add support for SOM expressions in use and usehref to search a node. - get prototype for all nodes if any. --- src/core/xfa/builder.js | 5 ++ src/core/xfa/template.js | 5 -- src/core/xfa/xfa_object.js | 148 ++++++++++++++++++++++++----------- test/unit/xfa_parser_spec.js | 50 ++++++++++++ 4 files changed, 158 insertions(+), 50 deletions(-) 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 = `