Skip to content

Commit d94cf2f

Browse files
committed
feat: add a deep descendant-element search to the ODF XML query helpers
elementsWithTag walks a node forest depth-first, pre-order, returning every element anywhere in it (not just direct children) with a given tag, in document order -- mirroring ooxml.js's own elementsWithTag (src/typed/util.ts). childrenWithTag's direct-children-only search isn't enough for a schema position that varies relative to its container, e.g. a text:p that may sit directly under a draw:text-box or be nested one or more levels inside a text:list/text:list-item.
1 parent 0b1444e commit d94cf2f

3 files changed

Lines changed: 43 additions & 2 deletions

File tree

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export type { StyleFamily, InternRequest, OtherPartRef, StyleRegistryOptions } f
8080

8181
export { ensureSpan } from './styles/span';
8282

83-
export { rootElement, findChildElement, childrenWithTag, attrValue } from './xml/query';
83+
export { rootElement, findChildElement, childrenWithTag, elementsWithTag, attrValue } from './xml/query';
8484
export { decodeXmlText } from './xml/entities';
8585

8686
export { parseOdfLength, formatOdfLength } from './typed/shared/units';

src/xml/query.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from 'vitest';
22
import { el, txt } from './fragment';
3-
import { rootElement, findChildElement, childrenWithTag, attrValue } from './query';
3+
import { rootElement, findChildElement, childrenWithTag, elementsWithTag, attrValue } from './query';
44

55
describe('rootElement', () => {
66
it('returns the first element node, skipping a leading declaration', () => {
@@ -44,6 +44,26 @@ describe('childrenWithTag', () => {
4444
});
4545
});
4646

47+
describe('elementsWithTag', () => {
48+
it('finds a matching element at any depth, in document order', () => {
49+
const deep = el('text:p', {}, [txt('deep')]);
50+
const shallow = el('text:p', {}, [txt('shallow')]);
51+
const list = el('text:list', {}, [el('text:list-item', {}, [deep])]);
52+
const container = el('draw:text-box', {}, [shallow, list]);
53+
expect(elementsWithTag([container], 'text:p')).toEqual([shallow, deep]);
54+
});
55+
56+
it('returns an empty array when nothing matches', () => {
57+
expect(elementsWithTag([el('draw:text-box')], 'text:p')).toEqual([]);
58+
});
59+
60+
it('does not match the forest root itself unless it is passed as one of the input nodes', () => {
61+
const root = el('text:p', {}, [txt('x')]);
62+
expect(elementsWithTag(root.children, 'text:p')).toEqual([]);
63+
expect(elementsWithTag([root], 'text:p')).toEqual([root]);
64+
});
65+
});
66+
4767
describe('attrValue', () => {
4868
it('returns the value of a present attribute', () => {
4969
expect(attrValue(el('style:style', { 'style:name': 'P1' }), 'style:name')).toBe('P1');

src/xml/query.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,27 @@ export function childrenWithTag(element: XmlElement, tag: string): XmlElement[]
3333
return out;
3434
}
3535

36+
// Depth-first pre-order walk over a node forest, descending into every element's own children -- the shared traversal every descendant-search helper below builds on.
37+
function* walk(nodes: readonly XmlNode[]): Generator<XmlNode> {
38+
for (const node of nodes) {
39+
yield node;
40+
if (node.type === 'element') {
41+
yield* walk(node.children);
42+
}
43+
}
44+
}
45+
46+
// Every element anywhere in the given forest (not just direct children) with the given tag, in document order -- for a schema position that isn't fixed relative to its container (e.g. a text:p that may sit directly under a draw:text-box or be nested inside a text:list-item), where childrenWithTag's direct-children-only search isn't enough. Mirrors ooxml.js's own elementsWithTag (src/typed/util.ts), which establishes the same descendant-search convention for the equivalent OOXML concept.
47+
export function elementsWithTag(nodes: readonly XmlNode[], tag: string): XmlElement[] {
48+
const out: XmlElement[] = [];
49+
for (const node of walk(nodes)) {
50+
if (node.type === 'element' && node.tag === tag) {
51+
out.push(node);
52+
}
53+
}
54+
return out;
55+
}
56+
3657
export function attrValue(element: XmlElement, name: string): string | undefined {
3758
return element.attributes.find((attribute) => attribute.name === name)?.value;
3859
}

0 commit comments

Comments
 (0)