Skip to content

Commit

Permalink
XFA - Implement usehref support
Browse files Browse the repository at this point in the history
  - 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.
  • Loading branch information
calixteman committed Jun 2, 2021
1 parent 8c53bf8 commit 832722c
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 52 deletions.
5 changes: 5 additions & 0 deletions src/core/xfa/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
$nsAttributes,
$onChild,
$resolvePrototypes,
$root,
XFAObject,
} from "./xfa_object.js";
import { NamespaceSetUp } from "./setup.js";
Expand All @@ -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];
}
Expand Down
5 changes: 0 additions & 5 deletions src/core/xfa/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -3902,9 +3902,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];
Expand All @@ -3926,8 +3923,6 @@ class Subform extends XFAObject {
);
}

// TODO: implement usehref (probably in bind.js).

// TODO: incomplete.
fixDimensions(this);
const children = [];
Expand Down
148 changes: 101 additions & 47 deletions src/core/xfa/xfa_object.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
Expand All @@ -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();

Expand Down Expand Up @@ -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"><toto><titi><... name="bar"></titi></toto>...
* is fine because toto and titi are transparent.
*/
[$isTransparent]() {
return this.name === "";
return !this.name;
}

[$lastAttribute]() {
Expand Down Expand Up @@ -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));
}

/**
Expand All @@ -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;
}

if (proto[$nodeName] !== this[$nodeName]) {
warn(
`XFA - Incompatible prototype: ${proto[$nodeName]} !== ${this[$nodeName]}.`
);
return null;
}
const { use, usehref } = this;
if (!use && !usehref) {
return null;
}

if (ancestors.has(proto)) {
// We've a cycle so break it.
warn(`XFA - Cycle detected in prototypes use.`);
return null;
}
let proto = null;
let somExpression = null;
let id = null;
let ref = use;

// 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);
ancestors.delete(proto);
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) {
Expand Down Expand Up @@ -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 (
Expand All @@ -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;
Expand All @@ -480,7 +533,7 @@ class XFAObject {
child[_parent] = this;
this[name] = child;
this[_children].push(child);
child[$resolvePrototypes](ids, newAncestors);
child[_resolvePrototypesHelper](ids, ancestors);
}
}
}
Expand Down Expand Up @@ -924,6 +977,7 @@ export {
$onText,
$removeChild,
$resolvePrototypes,
$root,
$searchNode,
$setId,
$setSetAttributes,
Expand Down
50 changes: 50 additions & 0 deletions test/unit/xfa_parser_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<?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>
<proto>
<draw name="foo">
<font typeface="Foo" size="123pt" weight="bold" posture="italic">
<fill>
<color value="1,2,3"/>
</fill>
</font>
</draw>
</proto>
<field>
<font usehref=".#som($template.#subform.foo.#font)"/>
</field>
<field>
<font usehref=".#som($template.#subform.foo.#font)" size="456pt" weight="bold" posture="normal">
<fill>
<color value="4,5,6"/>
</fill>
<extras id="id2"/>
</font>
</field>
</subform>
</template>
</xdp:xdp>
`;
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 = `
<?xml version="1.0"?>
Expand Down

0 comments on commit 832722c

Please sign in to comment.