fix(server): bind dc namespace during epub metadata writes

Preserve well-formed OPF output when publishers scope Dublin Core namespace declarations to individual metadata elements.

Fixes #1207
This commit is contained in:
neon
2026-08-30 16:28:27 +00:00
parent 26503bc9e6
commit cf84e37b2b
2 changed files with 58 additions and 0 deletions
@@ -131,6 +131,58 @@ describe('epub-opf-builder', () => {
expect(count).toBe(1);
});
it('adds xmlns:dc to <package> when source declarations are scoped to replaced metadata elements', () => {
const opf = `
<package version="3.0" unique-identifier="uid" xmlns="http://www.idpf.org/2007/opf">
<metadata xmlns:opf="http://www.idpf.org/2007/opf">
<dc:identifier xmlns:dc="http://purl.org/dc/elements/1.1/" id="uid">urn:uuid:abc</dc:identifier>
<dc:title xmlns:dc="http://purl.org/dc/elements/1.1/">Old title</dc:title>
<dc:publisher xmlns:dc="http://purl.org/dc/elements/1.1/">Old publisher</dc:publisher>
</metadata>
</package>
`;
const result = build(opf, { title: 'Replacement', publisher: 'New publisher', language: 'en' });
const packageTag = result.newOpfXml.match(/<package\b[^>]*>/)?.[0];
expect(packageTag).toContain('xmlns:dc="http://purl.org/dc/elements/1.1/"');
expect(result.newOpfXml).toContain('<dc:title id="t-main">Replacement</dc:title>');
expect(result.newOpfXml).toContain('<dc:publisher>New publisher</dc:publisher>');
expect(result.newOpfXml).toContain('<dc:language>en</dc:language>');
});
it('does not duplicate xmlns:dc when already declared on <package>', () => {
const opf = `
<package version="3.0" unique-identifier="uid" xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata>
<dc:identifier id="uid">urn:uuid:abc</dc:identifier>
<dc:title>Old title</dc:title>
</metadata>
</package>
`;
const result = build(opf, { title: 'Replacement' });
expect(result.newOpfXml.match(/xmlns:dc=/g)).toHaveLength(1);
});
it('preserves an existing xmlns:dc binding on <package>', () => {
const opf = `
<package version="2.0" unique-identifier="uid" xmlns:dc="urn:publisher:dc">
<metadata>
<dc:identifier id="uid">urn:uuid:abc</dc:identifier>
<dc:title>Old title</dc:title>
</metadata>
</package>
`;
const result = build(opf, { title: 'Replacement' });
const packageTag = result.newOpfXml.match(/<package\b[^>]*>/)?.[0];
expect(packageTag).toContain('xmlns:dc="urn:publisher:dc"');
expect(packageTag).not.toContain('xmlns:dc="http://purl.org/dc/elements/1.1/"');
});
it('strips legacy urn: provider identifiers and replaces with opf:scheme format', () => {
const opf = `
<package version="2.0" unique-identifier="uid" xmlns:opf="http://www.idpf.org/2007/opf">
@@ -371,6 +371,12 @@ export function build(opfXml: string, payload: BookWritePayload): { newOpfXml: s
pkgAttrs['@_xmlns:opf'] = 'http://www.idpf.org/2007/opf';
}
// Some EPUBs scope xmlns:dc to each dc:* element. Rebuilding metadata removes those
// declarations, so bind the regenerated elements from their common package ancestor.
if (!attr(pkgAttrs, '@_xmlns:dc')) {
pkgAttrs['@_xmlns:dc'] = 'http://purl.org/dc/elements/1.1/';
}
const pkgContent = getPkgContentAll(pkgNode, pkgTag);
const metadata = findMetadata(pkgContent);
if (!metadata) throw new Error('Cannot find <metadata> element in OPF');