Skip to content

fix(archive-reader): read OPF attributes under any prefix bound to its namespace - #330

Merged
mbret merged 2 commits into
masterfrom
claude/opf-namespace-prefix-aliases
Aug 26, 2026
Merged

fix(archive-reader): read OPF attributes under any prefix bound to its namespace#330
mbret merged 2 commits into
masterfrom
claude/opf-namespace-prefix-aliases

Conversation

@mbret

@mbret mbret commented Aug 26, 2026

Copy link
Copy Markdown
Owner

The bug

An XML prefix is arbitrary — what names an attribute is its (namespace, local name) pair. opf is the convention, but a package may bind the OPF namespace to any prefix, and it names the same attributes:

<package xmlns="http://www.idpf.org/2007/opf"
         xmlns:pkg="http://www.idpf.org/2007/opf"
         xmlns:dc="http://purl.org/dc/elements/1.1/" ...>
  <metadata>
    <dc:identifier pkg:scheme="GoogleBooks">zyTCAlFPjgYC</dc:identifier>

The parser matched the literal opf: spellings, so this identifier was read as having no scheme and fell back to whatever its value could be inferred as — Unknown here, since a Google Books id has no recognizable syntax. An ISBN-shaped value masked the problem by inferring correctly anyway.

Reported by Codex on #329.

Bindings are element-scoped

The first commit resolved prefixes from <package>. That is wrong in both directions, as Codex pointed out on this PR: XML scopes bindings to the element declaring them, inherited by descendants. So it missed a prefix declared on <metadata> or on the dc:identifier itself, and it trusted a prefix the root bound to the OPF namespace even where a descendant had rebound it.

Bindings now accumulate while descending. xmlNamespaceScope layers one element's declarations over the scope it was reached under, and the prefixes naming the namespace are resolved at the element whose attribute is being read.

Scope

Three attributes, all defined by the package's own vocabulary: the identifier scheme (scheme / Scheme), role, and file-as.

rendition:layout and its siblings are deliberately untouched. Those are <meta property="rendition:layout"> values, governed by EPUB 3's reserved vocabulary prefixes declared with <package prefix="…"> — a different mechanism from XML namespaces, and matching the literal string is correct by default. Worth its own change if you want it.

Element names already matched by local name (localNameEq), so <opf:package> and <pkg:metadata> were never affected.

Two rules kept deliberately

  • opf is accepted when nothing binds it. Using it undeclared is invalid but common in the wild. A document that explicitly binds opf to another namespace is honoured, and it is then not read.
  • The unprefixed spellings stay a fallback, not an equivalent. An unprefixed attribute is in no namespace at all in XML — unlike an element, which takes the default namespace — so scheme is read after every prefixed form, as the EPUB 2 legacy shape it is.

A prefix bound to some other namespace is never read.

New exports

OPF_NAMESPACE the namespace URI
XmlNamespaceScope / xmlNamespaceScope(attributes, inherited?) prefix bindings at an element, layered over its parent's
opfNamespacePrefixes(scope) the prefixes naming the OPF namespace there
opfNamespacedAttribute(attributes, prefixes, localNames) an attribute under any of them, then the unprefixed fallback
OPF_IDENTIFIER_SCHEME_LOCAL_NAMES scheme, Scheme — for a consumer resolving namespaces itself, as anything with a DOM can via getAttributeNS

opfIdentifierSchemeAttribute gains an optional second argument for the prefixes, defaulting to ["opf"], so the signature stays backward compatible. OPF_IDENTIFIER_SCHEME_ATTRIBUTES is unchanged and now documented as not covering an aliased prefix.

Testing

348 tests pass, up from 324, with none modified — no behaviour change for documents using the conventional prefix.

Parser cases, each verified to fail before the corresponding fix:

  • aliased prefix for the scheme; aliased prefix for role and file-as
  • prefix declared on <metadata>; declared on the identifier itself; declared on a creator itself
  • a descendant rebinding the conventional prefix elsewhere; a descendant rebinding an aliased prefix elsewhere
  • a prefix bound elsewhere ignored; opf: undeclared still read; the unprefixed EPUB 2 spellings still read

Plus 14 unit cases for xmlNamespaceScope / opfNamespacePrefixes / opfNamespacedAttribute, including that layering a scope does not mutate the inherited one.

tsc and biome clean.

Docs

gitbook/archive-reader/identifiers.md gains the aliased-prefix explanation with the XML above, the element-scoping model with an example, and both rules.

🤖 Generated with Claude Code

…s namespace

An XML prefix is arbitrary: what names an attribute is its namespace and
local name. `opf` is the convention, but a package binding the OPF
namespace to another prefix names the same attributes, and the parser
matched the literal `opf:` spellings — so `pkg:scheme="GoogleBooks"` was
read as no scheme at all, and the identifier fell back to whatever its
value could be inferred as.

The three attributes the package's own vocabulary defines are now read
under every prefix the document binds to the namespace: the identifier
scheme, `role` and `file-as`. `opf` stays accepted whether or not it is
declared, since using it undeclared is invalid but common, and the
unprefixed spellings EPUB 2 uses stay a fallback rather than an
equivalent — an unprefixed attribute is in no namespace.

`rendition:layout` and its siblings are deliberately untouched. Those are
`<meta property>` values governed by EPUB's reserved vocabulary prefixes,
declared with `<package prefix>`, which is a different mechanism from XML
namespaces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
prose-reader-demo Ready Ready Preview Aug 26, 2026 11:14pm
prose-reader-front Ready Ready Preview Aug 26, 2026 11:14pm

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 69e250b5c5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/archive-reader/src/metadata/opf/parse.ts Outdated
… read

Namespace bindings are scoped to the element declaring them, so reading
them off `<package>` alone missed a prefix a package declares on
`<metadata>` or on the identifier itself, and trusted one the root bound
to the OPF namespace even where a descendant had rebound it.

Bindings now accumulate while descending: `xmlNamespaceScope` layers an
element's declarations over the scope it was reached under, and the
prefixes naming the namespace are resolved at the element whose attribute
is being read. `opfNamespacePrefixes` takes that scope rather than raw
attributes, and a document that explicitly binds `opf` elsewhere is
honoured — the tolerance for an undeclared `opf` only applies when
nothing binds it.

Caught by Codex on the first version of this fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mbret
mbret merged commit 99a156c into master Aug 26, 2026
11 checks passed
@mbret
mbret deleted the claude/opf-namespace-prefix-aliases branch August 26, 2026 23:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant