Skip to content
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
2,023 changes: 1,351 additions & 672 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions packages/ooxml-inspector/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
coverage/
dist/
node_modules/
51 changes: 51 additions & 0 deletions packages/ooxml-inspector/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# OOXML Inspector

**OOXML Inspector** is a developer tool + library for working with [Office Open XML (OOXML)](https://en.wikipedia.org/wiki/Office_Open_XML) schemas.
It bundles the transitional WordprocessingML schema into JSON and provides both:

- a **CLI** (`ooxml`) for inspecting schema relationships, and
- a **JavaScript/TypeScript library** for programmatic access.

## Features

- Query OOXML element children, attributes, and namespaces
- Explore schema relationships and tag hierarchies
- Use as a CLI or import as a library in Node.js/TypeScript

## Installation

```bash
npm install @superdoc-dev/ooxml-inspector
```

## CLI Usage

```bash
npx ooxml children w:p
npx ooxml tags --parents
npx ooxml namespaces
npx ooxml attrs w:p
```

- `children <prefix:local>`: List allowed children for an element
- `tags [prefix] [--parents] [--plain]`: List tags, optionally filtering by namespace or parent status
- `namespaces`: List all namespaces in the schema
- `attrs <prefix:local>`: List attributes for an element

## Library Usage

```js
import { childrenOf } from '@superdoc-dev/ooxml-inspector';

const children = childrenOf('w:p'); // Get children of <w:p>
```

## Development

- Build: `npm run build`
- Test: `npm run test`
- Coverage: `npm run test:cov`

## License

AGPLv3
4 changes: 4 additions & 0 deletions packages/ooxml-inspector/example/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { childrenOf } from '@superdoc-dev/ooxml-inspector';

const children = childrenOf('w:p');
console.debug('Children of w:p', children);
3 changes: 3 additions & 0 deletions packages/ooxml-inspector/generator/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { runGenerator } from './src/index.js';

runGenerator();
13 changes: 13 additions & 0 deletions packages/ooxml-inspector/generator/src/builder/autoprefix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Auto-assign a prefix for a given target namespace.
* @param {string} tns - The target namespace
* @param {Object} nsMap - The namespace map
* @returns {string} - The assigned prefix
*/
export const autoPrefix = (tns, nsMap) => {
if (!tns) return 'unknown';
if (nsMap[tns]) return nsMap[tns];
const newPrefix = 'g' + Object.keys(nsMap).length;
nsMap[tns] = newPrefix;
return newPrefix;
};
43 changes: 43 additions & 0 deletions packages/ooxml-inspector/generator/src/builder/autoprefix.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, it, expect } from 'vitest';
import { autoPrefix } from './index.js';

describe('autoPrefix', () => {
it('returns "unknown" if tns is falsy', () => {
const nsMap = {};
expect(autoPrefix('', nsMap)).toBe('unknown');
expect(autoPrefix(null, nsMap)).toBe('unknown');
expect(autoPrefix(undefined, nsMap)).toBe('unknown');
});

it('returns existing prefix if tns is already in nsMap', () => {
const nsMap = { 'http://example.com': 'ex' };
const result = autoPrefix('http://example.com', nsMap);
expect(result).toBe('ex');
expect(nsMap).toEqual({ 'http://example.com': 'ex' }); // unchanged
});

it('assigns a new prefix if tns is not in nsMap', () => {
const nsMap = {};
const result = autoPrefix('http://new.com', nsMap);
expect(result).toBe('g0');
expect(nsMap).toEqual({ 'http://new.com': 'g0' });
});

it('assigns incrementing prefixes based on nsMap size', () => {
const nsMap = { 'http://one.com': 'g0', 'http://two.com': 'g1' };
const result = autoPrefix('http://three.com', nsMap);
expect(result).toBe('g2');
expect(nsMap).toEqual({
'http://one.com': 'g0',
'http://two.com': 'g1',
'http://three.com': 'g2',
});
});

it('does not overwrite existing mapping for different tns', () => {
const nsMap = { 'http://foo.com': 'g0' };
autoPrefix('http://bar.com', nsMap);
expect(nsMap).toHaveProperty('http://foo.com', 'g0');
expect(nsMap).toHaveProperty('http://bar.com', 'g1');
});
});
61 changes: 61 additions & 0 deletions packages/ooxml-inspector/generator/src/builder/collect-elements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { arr } from './index.js';

/** @type {number} */
const MAX_XSD_DEPTH = 10;

/**
* Recursively collect elements from a given node. Most nodes seem to max out around 7 levels
* so 10 seems like a sufficient max.
*
* @param {Object} node - The XML node to collect elements from.
* @param {string} tns - The target namespace.
* @param {string} prefix - The namespace prefix.
* @param {Map} elements - The map to collect elements.
* @param {Map} elementRefs - The map to collect element references.
* @param {number} depth - The current depth in the XML structure.
*/
export function collectElements(node, tns, prefix, elements, elementRefs, depth = 0) {
if (!node || depth > MAX_XSD_DEPTH) return;

// Direct elements
const els = arr(node['xs:element']).concat(arr(node['xsd:element']));
for (const el of els) {
if (el.name) {
const key = `${tns}::${el.name}`;
if (!elements.has(key)) {
elements.set(key, { tns, prefix, name: el.name, el });
}
} else if (el.ref) {
// Track element reference for later resolution
elementRefs.set(el.ref, { tns, prefix });
}
}

// Recurse into sequences, choices, alls
const containers = []
.concat(arr(node['xs:sequence']), arr(node['xsd:sequence']))
.concat(arr(node['xs:choice']), arr(node['xsd:choice']))
.concat(arr(node['xs:all']), arr(node['xsd:all']));

for (const container of containers) {
collectElements(container, tns, prefix, elements, elementRefs, depth + 1);
}

// Recurse into complex content
const complexContent = node['xs:complexContent'] || node['xsd:complexContent'];
if (complexContent) {
const ext = complexContent['xs:extension'] || complexContent['xsd:extension'];
const res = complexContent['xs:restriction'] || complexContent['xsd:restriction'];
if (ext) collectElements(ext, tns, prefix, elements, elementRefs, depth + 1);
if (res) collectElements(res, tns, prefix, elements, elementRefs, depth + 1);
}

// Recurse into simple content (might have attributes that are elements)
const simpleContent = node['xs:simpleContent'] || node['xsd:simpleContent'];
if (simpleContent) {
const ext = simpleContent['xs:extension'] || simpleContent['xsd:extension'];
const res = simpleContent['xs:restriction'] || simpleContent['xsd:restriction'];
if (ext) collectElements(ext, tns, prefix, elements, elementRefs, depth + 1);
if (res) collectElements(res, tns, prefix, elements, elementRefs, depth + 1);
}
}
Loading
Loading