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.
This commit is contained in:
parent
e962d7787e
commit
11573ddd16
@ -21,6 +21,7 @@ import {
|
|||||||
$nsAttributes,
|
$nsAttributes,
|
||||||
$onChild,
|
$onChild,
|
||||||
$resolvePrototypes,
|
$resolvePrototypes,
|
||||||
|
$root,
|
||||||
XFAObject,
|
XFAObject,
|
||||||
} from "./xfa_object.js";
|
} from "./xfa_object.js";
|
||||||
import { NamespaceSetUp } from "./setup.js";
|
import { NamespaceSetUp } from "./setup.js";
|
||||||
@ -43,6 +44,10 @@ class Root extends XFAObject {
|
|||||||
[$finalize]() {
|
[$finalize]() {
|
||||||
super[$finalize]();
|
super[$finalize]();
|
||||||
if (this.element.template instanceof Template) {
|
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[$resolvePrototypes](this[$ids]);
|
||||||
this.element.template[$ids] = this[$ids];
|
this.element.template[$ids] = this[$ids];
|
||||||
}
|
}
|
||||||
|
@ -3866,9 +3866,6 @@ class Subform extends XFAObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[$toHTML](availableSpace) {
|
[$toHTML](availableSpace) {
|
||||||
if (this.name === "helpText") {
|
|
||||||
return HTMLResult.EMPTY;
|
|
||||||
}
|
|
||||||
if (this[$extra] && this[$extra].afterBreakAfter) {
|
if (this[$extra] && this[$extra].afterBreakAfter) {
|
||||||
const ret = this[$extra].afterBreakAfter;
|
const ret = this[$extra].afterBreakAfter;
|
||||||
delete this[$extra];
|
delete this[$extra];
|
||||||
@ -3890,8 +3887,6 @@ class Subform extends XFAObject {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: implement usehref (probably in bind.js).
|
|
||||||
|
|
||||||
// TODO: incomplete.
|
// TODO: incomplete.
|
||||||
fixDimensions(this);
|
fixDimensions(this);
|
||||||
const children = [];
|
const children = [];
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
import { getInteger, getKeyword, HTMLResult } from "./utils.js";
|
import { getInteger, getKeyword, HTMLResult } from "./utils.js";
|
||||||
import { shadow, warn } from "../../shared/util.js";
|
import { shadow, warn } from "../../shared/util.js";
|
||||||
import { NamespaceIds } from "./namespaces.js";
|
import { NamespaceIds } from "./namespaces.js";
|
||||||
|
import { searchNode } from "./som.js";
|
||||||
|
|
||||||
// We use these symbols to avoid name conflict between tags
|
// We use these symbols to avoid name conflict between tags
|
||||||
// and properties/methods names.
|
// and properties/methods names.
|
||||||
@ -61,6 +62,7 @@ const $onChild = Symbol();
|
|||||||
const $onChildCheck = Symbol();
|
const $onChildCheck = Symbol();
|
||||||
const $onText = Symbol();
|
const $onText = Symbol();
|
||||||
const $removeChild = Symbol();
|
const $removeChild = Symbol();
|
||||||
|
const $root = Symbol("root");
|
||||||
const $resolvePrototypes = Symbol();
|
const $resolvePrototypes = Symbol();
|
||||||
const $searchNode = Symbol();
|
const $searchNode = Symbol();
|
||||||
const $setId = Symbol();
|
const $setId = Symbol();
|
||||||
@ -85,6 +87,7 @@ const _hasChildren = Symbol();
|
|||||||
const _max = Symbol();
|
const _max = Symbol();
|
||||||
const _options = Symbol();
|
const _options = Symbol();
|
||||||
const _parent = Symbol("parent");
|
const _parent = Symbol("parent");
|
||||||
|
const _resolvePrototypesHelper = Symbol();
|
||||||
const _setAttributes = Symbol();
|
const _setAttributes = Symbol();
|
||||||
const _validator = Symbol();
|
const _validator = Symbol();
|
||||||
|
|
||||||
@ -192,8 +195,14 @@ class XFAObject {
|
|||||||
this[_children].splice(i, 0, child);
|
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]() {
|
[$isTransparent]() {
|
||||||
return this.name === "";
|
return !this.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
[$lastAttribute]() {
|
[$lastAttribute]() {
|
||||||
@ -343,10 +352,8 @@ class XFAObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[$setSetAttributes](attributes) {
|
[$setSetAttributes](attributes) {
|
||||||
if (attributes.use || attributes.id) {
|
// Just keep set attributes because it can be used in a proto.
|
||||||
// Just keep set attributes because this node uses a proto or is a proto.
|
this[_setAttributes] = new Set(Object.keys(attributes));
|
||||||
this[_setAttributes] = new Set(Object.keys(attributes));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -364,57 +371,103 @@ class XFAObject {
|
|||||||
*/
|
*/
|
||||||
[$resolvePrototypes](ids, ancestors = new Set()) {
|
[$resolvePrototypes](ids, ancestors = new Set()) {
|
||||||
for (const child of this[_children]) {
|
for (const child of this[_children]) {
|
||||||
const proto = child[_getPrototype](ids, ancestors);
|
child[_resolvePrototypesHelper](ids, ancestors);
|
||||||
if (proto) {
|
}
|
||||||
// _applyPrototype will apply $resolvePrototypes with correct ancestors
|
}
|
||||||
// to avoid infinite loop.
|
|
||||||
child[_applyPrototype](proto, ids, ancestors);
|
[_resolvePrototypesHelper](ids, ancestors) {
|
||||||
} else {
|
const proto = this[_getPrototype](ids, ancestors);
|
||||||
child[$resolvePrototypes](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) {
|
[_getPrototype](ids, ancestors) {
|
||||||
const { use } = this;
|
const { use, usehref } = this;
|
||||||
if (use && use.startsWith("#")) {
|
if (!use && !usehref) {
|
||||||
const id = use.slice(1);
|
return null;
|
||||||
const proto = ids.get(id);
|
}
|
||||||
this.use = "";
|
|
||||||
if (!proto) {
|
|
||||||
warn(`XFA - Invalid prototype id: ${id}.`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (proto[$nodeName] !== this[$nodeName]) {
|
let proto = null;
|
||||||
warn(
|
let somExpression = null;
|
||||||
`XFA - Incompatible prototype: ${proto[$nodeName]} !== ${this[$nodeName]}.`
|
let id = null;
|
||||||
);
|
let ref = use;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ancestors.has(proto)) {
|
// If usehref and use are non-empty then use usehref.
|
||||||
// We've a cycle so break it.
|
if (usehref) {
|
||||||
warn(`XFA - Cycle detected in prototypes use.`);
|
ref = usehref;
|
||||||
return null;
|
// 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);
|
this.use = this.usehref = "";
|
||||||
// The prototype can have a "use" attribute itself.
|
if (id) {
|
||||||
const protoProto = proto[_getPrototype](ids, ancestors);
|
proto = ids.get(id);
|
||||||
if (!protoProto) {
|
} else {
|
||||||
ancestors.delete(proto);
|
proto = searchNode(
|
||||||
return proto;
|
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);
|
ancestors.delete(proto);
|
||||||
|
|
||||||
return proto;
|
return proto;
|
||||||
}
|
}
|
||||||
// TODO: handle SOM expressions.
|
|
||||||
|
|
||||||
return null;
|
proto[_applyPrototype](protoProto, ids, ancestors);
|
||||||
|
ancestors.delete(proto);
|
||||||
|
|
||||||
|
return proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
[_applyPrototype](proto, ids, ancestors) {
|
[_applyPrototype](proto, ids, ancestors) {
|
||||||
@ -449,7 +502,7 @@ class XFAObject {
|
|||||||
|
|
||||||
if (value instanceof XFAObjectArray) {
|
if (value instanceof XFAObjectArray) {
|
||||||
for (const child of value[_children]) {
|
for (const child of value[_children]) {
|
||||||
child[$resolvePrototypes](ids, ancestors);
|
child[_resolvePrototypesHelper](ids, ancestors);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (
|
for (
|
||||||
@ -461,7 +514,7 @@ class XFAObject {
|
|||||||
if (value.push(child)) {
|
if (value.push(child)) {
|
||||||
child[_parent] = this;
|
child[_parent] = this;
|
||||||
this[_children].push(child);
|
this[_children].push(child);
|
||||||
child[$resolvePrototypes](ids, newAncestors);
|
child[_resolvePrototypesHelper](ids, ancestors);
|
||||||
} else {
|
} else {
|
||||||
// No need to continue: other nodes will be rejected.
|
// No need to continue: other nodes will be rejected.
|
||||||
break;
|
break;
|
||||||
@ -472,6 +525,10 @@ class XFAObject {
|
|||||||
|
|
||||||
if (value !== null) {
|
if (value !== null) {
|
||||||
value[$resolvePrototypes](ids, ancestors);
|
value[$resolvePrototypes](ids, ancestors);
|
||||||
|
if (protoValue) {
|
||||||
|
// protoValue must be treated as a prototype for value.
|
||||||
|
value[_applyPrototype](protoValue, ids, ancestors);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -480,7 +537,7 @@ class XFAObject {
|
|||||||
child[_parent] = this;
|
child[_parent] = this;
|
||||||
this[name] = child;
|
this[name] = child;
|
||||||
this[_children].push(child);
|
this[_children].push(child);
|
||||||
child[$resolvePrototypes](ids, newAncestors);
|
child[_resolvePrototypesHelper](ids, ancestors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -924,6 +981,7 @@ export {
|
|||||||
$onText,
|
$onText,
|
||||||
$removeChild,
|
$removeChild,
|
||||||
$resolvePrototypes,
|
$resolvePrototypes,
|
||||||
|
$root,
|
||||||
$searchNode,
|
$searchNode,
|
||||||
$setId,
|
$setId,
|
||||||
$setSetAttributes,
|
$setSetAttributes,
|
||||||
|
@ -304,6 +304,56 @@ describe("XFAParser", function () {
|
|||||||
expect(font.extras.id).toEqual("id2");
|
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 () {
|
it("should parse a xfa document with xhtml", function () {
|
||||||
const xml = `
|
const xml = `
|
||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user