Merge pull request #13473 from calixteman/usehref

XFA - Implement usehref support
This commit is contained in:
calixteman 2021-06-04 20:13:22 +02:00 committed by GitHub
commit e0676ec298
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 158 additions and 50 deletions

View File

@ -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];
}

View File

@ -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 = [];

View File

@ -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"><toto><titi><... name="bar"></titi></toto>...
* 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,

View File

@ -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"?>