Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XFA - Implement usehref support #13473

Merged
merged 1 commit into from
Jun 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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];
Expand All @@ -3890,8 +3887,6 @@ class Subform extends XFAObject {
);
}

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

// TODO: incomplete.
fixDimensions(this);
const children = [];
Expand Down
152 changes: 105 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 @@ -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;
}

Expand All @@ -480,7 +537,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 +981,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