@prose-reader/archive-reader 1.371.0 → 1.373.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../src/archives/createArchiveFromArrayBufferList.ts","../../src/archives/createArchiveFromText.ts","../../src/archives/createArchiveFromUrls.ts","../../src/archives/types.ts","../../src/archives/images.ts","../../src/archives/readRecordAsText.ts","../../src/metadata/opf/getArchiveOpfInfo.ts","../../src/utils/tokenizeXmlSpaceSeparatedList.ts","../../src/metadata/opf/opfNamespace.ts","../../src/metadata/opf/identifierScheme.ts","../../src/metadata/opf/spineItemrefProperties.ts","../../src/metadata/opf/parse.ts","../../src/metadata/opf/readArchiveOpf.ts","../../src/metadata/apple/parse.ts","../../src/metadata/comicInfo/parse.ts","../../src/metadata/kobo/parse.ts","../../src/utils/omitUndefined.ts","../../src/utils/toContainerUri.ts","../../src/readingOrder/resolveArchiveReadingOrder.ts","../../src/cover/resolveArchiveCover.ts","../../src/layout/scanReadingOrderDocumentsViewport.ts","../../src/metadata/apple/readArchiveApple.ts","../../src/metadata/comicInfo/getArchiveHasComicInfo.ts","../../src/metadata/comicInfo/manga.ts","../../src/metadata/comicInfo/readArchiveComicInfo.ts","../../src/metadata/kobo/readArchiveKobo.ts","../../src/metadata/opf/getSpineItemFilesFromArchive.ts","../../src/metadata/opf/isArchiveEpub.ts","../../src/metadata/apple/resolve.ts","../../src/metadata/comicInfo/resolve.ts","../../src/metadata/kobo/resolve.ts","../../src/utils/normalizeIsbn.ts","../../src/utils/booklandIsbn.ts","../../src/utils/normalizeGtin.ts","../../src/utils/inferIdentifierScheme.ts","../../src/utils/normalizeIdentifierScheme.ts","../../src/utils/parseW3cDtfDate.ts","../../src/metadata/opf/resolve.ts","../../src/resolve.ts","../../src/resolveMetadata.ts","../../src/toc/folders.ts","../../src/utils/getXmlElementInnerText.ts","../../src/toc/nav.ts","../../src/toc/ncx.ts","../../src/toc/resolveArchiveToc.ts","../../src/resolveArchive.ts","../../src/utils/catalogIdentifiers.ts","../../src/utils/identifierValues.ts","../../src/utils/mainTitle.ts"],"sourcesContent":["import { detectMimeTypeFromName } from \"@prose-reader/shared\"\nimport { createArchiveFromEntries } from \"./createArchiveFromEntries.ts\"\nimport { arrayBufferFileAccessors } from \"./fileAccessors.ts\"\nimport type { Archive } from \"./types.ts\"\n\nexport const createArchiveFromArrayBufferList = async (\n list: {\n isDir: boolean\n name: string\n size: number\n data: () => Promise<ArrayBuffer>\n }[],\n options: {\n orderByAlpha?: boolean\n name?: string\n encodingFormat?: string\n } = {},\n): Promise<Archive> =>\n createArchiveFromEntries(\n list,\n (file) =>\n file.isDir\n ? { dir: true, uri: file.name }\n : {\n dir: false,\n uri: file.name,\n size: file.size,\n ...arrayBufferFileAccessors(\n file.data,\n detectMimeTypeFromName(file.name) ?? ``,\n ),\n },\n { ...options, close: () => Promise.resolve() },\n )\n","import { getUriBasename } from \"@prose-reader/shared\"\nimport { createArchive } from \"./createArchive.ts\"\nimport { blobFileAccessors } from \"./fileAccessors.ts\"\n\n/**\n * Useful to create archive from txt content\n */\nexport const createArchiveFromText = async (\n content: string | Blob,\n {\n mimeType,\n direction,\n }: {\n direction?: `ltr` | `rtl`\n mimeType?: string\n } = { mimeType: \"text/plain\" },\n) => {\n const txtOpfContent = `\n <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n <package xmlns=\"http://www.idpf.org/2007/opf\" version=\"3.0\" xml:lang=\"ja\" prefix=\"rendition: http://www.idpf.org/vocab/rendition/#\"\n unique-identifier=\"ootuya-id\">\n <metadata xmlns:opf=\"http://www.idpf.org/2007/opf\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n xmlns:dcterms=\"http://purl.org/dc/terms/\">\n <meta property=\"rendition:layout\">reflowable</meta>\n </metadata>\n <manifest>\n <item id=\"p01\" href=\"p01.txt\" media-type=\"text/plain\"/>\n </manifest>\n <spine page-progression-direction=\"${direction ?? `ltr`}\">\n <itemref idref=\"p01\" />\n </spine>\n </package>\n `\n\n const archive = createArchive({\n filename: `content.txt`,\n encodingFormat: mimeType,\n records: [\n {\n dir: false,\n basename: getUriBasename(`generated.opf`),\n uri: `generated.opf`,\n size: 0,\n ...blobFileAccessors(async () => new Blob([txtOpfContent])),\n },\n {\n dir: false,\n basename: getUriBasename(`p01.txt`),\n uri: `p01.txt`,\n size:\n typeof content === \"string\" ? new Blob([content]).size : content.size,\n encodingFormat: mimeType,\n ...blobFileAccessors(async () =>\n typeof content === \"string\" ? new Blob([content]) : content,\n ),\n },\n ],\n close: () => Promise.resolve(),\n })\n\n return archive\n}\n","import {\n createXmlSafeIdFactory,\n detectMimeTypeFromName,\n escapeXmlAttributeValue,\n getUriBasename,\n} from \"@prose-reader/shared\"\nimport { createArchive } from \"./createArchive.ts\"\nimport { blobFileAccessors } from \"./fileAccessors.ts\"\nimport type { Archive } from \"./types.ts\"\n\n/**\n * @important\n * Make sure the urls are on the same origin or the cors header is set otherwise\n * the resource cannot be consumed as it is on the web.\n */\nexport const createArchiveFromUrls = async (\n urls: string[],\n options?: { useRenditionFlow: boolean },\n): Promise<Archive> => {\n const createSafeId = createXmlSafeIdFactory()\n const resources = urls.map((url) => ({\n id: createSafeId(url),\n url,\n }))\n const opfFileData = `\n <?xml version=\"1.0\" encoding=\"UTF-8\"?><package xmlns=\"http://www.idpf.org/2007/opf\" version=\"2.0\" unique-identifier=\"bookid\">\n <metadata>\n <meta property=\"rendition:layout\">${options?.useRenditionFlow ? `reflowable` : `pre-paginated`}</meta>\n ${options?.useRenditionFlow ? `<meta property=\"rendition:flow\">scrolled-continuous</meta>` : ``}\n </metadata>\n <manifest>\n ${resources\n .map(({ id, url }) => {\n const mediaType = detectMimeTypeFromName(url)\n\n return `<item id=\"${id}\" href=\"${escapeXmlAttributeValue(url)}\" media-type=\"${escapeXmlAttributeValue(mediaType ?? ``)}\"/>`\n })\n .join(`\\n`)}\n </manifest>\n <spine>\n ${resources.map(({ id }) => `<itemref idref=\"${id}\" />`).join(`\\n`)}\n </spine>\n </package>\n `\n\n const filesFromUrl: Archive[`records`] = urls.map((url) => ({\n dir: false,\n basename: getUriBasename(url),\n encodingFormat: detectMimeTypeFromName(url),\n uri: url,\n size: 0,\n ...blobFileAccessors(async () => {\n const response = await fetch(url)\n\n return response.blob()\n }),\n }))\n\n const opfFile: Archive[`records`][number] = {\n dir: false,\n basename: `content.opf`,\n uri: `content.opf`,\n size: 0,\n ...blobFileAccessors(async () => new Blob([opfFileData])),\n }\n\n return createArchive({\n records: [opfFile, ...filesFromUrl],\n close: () => Promise.resolve(),\n })\n}\n","export type FileRecord = {\n dir: false\n basename: string\n uri: string\n /** Uncompressed byte length of the record, or `0` when unknown. */\n size: number\n // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types\n encodingFormat?: string\n blob: () => Promise<Blob>\n arrayBuffer: () => Promise<ArrayBuffer>\n}\n\nexport type DirectoryRecord = {\n dir: true\n basename: string\n uri: string\n}\n\nexport type ArchiveRecord = FileRecord | DirectoryRecord\n\nexport type Archive = {\n /**\n * Container-level filename when known (e.g. the originating `.cbz`/`.epub`\n * file name). Absent for synthetic archives that have no real source file\n * (URL lists, raw array buffers). Consumers use it as a detection signal, so\n * it must never be fabricated.\n */\n filename?: string\n /**\n * Archive-level media type when known (e.g. `application/vnd.comicbook+zip`\n * for a CBZ). This is the mime type of the container itself, not of any\n * individual record; record-level mime types live on {@link FileRecord}.\n */\n encodingFormat?: string\n records: ArchiveRecord[]\n /**\n * `uri` → record index built once at construction (see `createArchive`).\n * Resource resolution is a per-fetched-file hot path, so prefer this (or\n * {@link getArchiveFileRecordByUri}) over scanning {@link Archive.records}.\n */\n recordsByUri: ReadonlyMap<string, ArchiveRecord>\n close: () => Promise<void>\n}\n\nexport const isFileRecord = (record: ArchiveRecord): record is FileRecord =>\n !record.dir\n\nexport const isDirectoryRecord = (\n record: ArchiveRecord,\n): record is DirectoryRecord => record.dir\n\nexport const getArchiveFileRecordByUri = (\n archive: Pick<Archive, \"recordsByUri\">,\n uri: string,\n): FileRecord | undefined => {\n const record = archive.recordsByUri.get(uri)\n\n return record && isFileRecord(record) ? record : undefined\n}\n","import { detectMimeTypeFromName, parseContentType } from \"@prose-reader/shared\"\nimport type { Archive, ArchiveRecord, FileRecord } from \"./types.ts\"\nimport { isFileRecord } from \"./types.ts\"\n\n/**\n * Best-effort media type of a file record: the archive's own encoding format\n * when known, else detected from the filename. Same derivation the reading\n * order and cover use.\n */\nconst mediaTypeForRecord = (record: FileRecord): string | undefined =>\n parseContentType(record.encodingFormat ?? \"\") ||\n detectMimeTypeFromName(record.basename)\n\n/**\n * Whether a record is an image resource — an `image/*` media type by encoding\n * format or filename (the image formats real CBZ/CBR/EPUB producers use).\n * Directories are never images.\n */\nexport const isImageRecord = (record: ArchiveRecord): boolean =>\n isFileRecord(record) &&\n mediaTypeForRecord(record)?.startsWith(\"image/\") === true\n\n/**\n * Every image file record of the archive, in record order. The page listing\n * of a comic or image container, and the raster asset inventory of any book.\n */\nexport const getArchiveImageRecords = (archive: Archive): FileRecord[] =>\n archive.records.filter(isFileRecord).filter(isImageRecord)\n","import type { FileRecord } from \"./types.ts\"\n\n/**\n * Decodes a record's bytes as UTF-8. The caller asserts the record is text;\n * there is intentionally no `string()` accessor on {@link FileRecord} so that\n * decoding a binary record is always a deliberate act at the call site.\n */\nexport const readRecordAsText = async (record: FileRecord): Promise<string> =>\n new TextDecoder().decode(await record.arrayBuffer())\n","import type { Archive } from \"../../archives/types.ts\"\n\nexport const getArchiveOpfInfo = (archive: Archive) => {\n const filesAsArray = archive.records.filter((file) => !file.dir)\n const file = filesAsArray.find((file) => file.uri.endsWith(`.opf`))\n\n return {\n data: file,\n basePath: file?.uri.substring(0, file.uri.lastIndexOf(`/`)) || ``,\n }\n}\n","/**\n * EPUB/XML space-separated token lists (e.g. `properties` on `item` / `itemref`).\n * Trims the raw string, splits on ASCII whitespace, drops empty segments.\n */\nexport const tokenizeXmlSpaceSeparatedList = (\n raw: string | undefined,\n): readonly string[] => {\n if (raw === undefined || raw.trim().length === 0) {\n return []\n }\n\n return raw\n .trim()\n .split(/\\s+/)\n .filter((t) => t.length > 0)\n}\n","/** The OPF namespace every package document's own vocabulary lives in. */\nexport const OPF_NAMESPACE = \"http://www.idpf.org/2007/opf\"\n\n/** The conventional prefix, accepted even when a package never declares it. */\nconst CONVENTIONAL_OPF_PREFIX = \"opf\"\n\nconst XMLNS_PREFIX = \"xmlns:\"\n\n/**\n * The prefix bindings in force at one element: every `xmlns:*` declaration on\n * it and on its ancestors, nearest winning. XML scopes bindings per element, so\n * a descendant can introduce a prefix its ancestors never had and rebind one\n * they did.\n */\nexport type XmlNamespaceScope = Readonly<Record<string, string>>\n\n/**\n * The scope at an element, given the one its parent was read under. Call it\n * while descending so each element is read under its own bindings.\n */\nexport const xmlNamespaceScope = (\n attributes: Readonly<Record<string, string | undefined>>,\n inherited: XmlNamespaceScope = {},\n): XmlNamespaceScope => {\n let scope: Record<string, string> | undefined\n\n for (const [name, value] of Object.entries(attributes)) {\n if (!name.startsWith(XMLNS_PREFIX)) continue\n\n const prefix = name.slice(XMLNS_PREFIX.length)\n const namespace = value?.trim()\n\n if (prefix.length === 0 || namespace === undefined) continue\n\n scope ??= { ...inherited }\n scope[prefix] = namespace\n }\n\n return scope ?? inherited\n}\n\n/**\n * The prefixes naming the OPF namespace in a scope. The conventional `opf` is\n * included unless the document bound it to something else, since using it\n * undeclared is invalid but common — a rebinding is explicit and is honoured.\n */\nexport const opfNamespacePrefixes = (\n scope: XmlNamespaceScope,\n): ReadonlyArray<string> => {\n const prefixes = Object.keys(scope).filter(function namesOpf(prefix) {\n return scope[prefix] === OPF_NAMESPACE\n })\n\n return scope[CONVENTIONAL_OPF_PREFIX] === undefined\n ? [CONVENTIONAL_OPF_PREFIX, ...prefixes]\n : prefixes\n}\n\n/**\n * An attribute in the OPF namespace, tried under every prefix naming it before\n * the unprefixed spelling EPUB 2 documents use. An unprefixed attribute is in\n * no namespace at all, so that is a deliberate fallback rather than an\n * equivalent name.\n */\nexport const opfNamespacedAttribute = (\n attributes: Readonly<Record<string, string | undefined>>,\n prefixes: ReadonlyArray<string>,\n localNames: ReadonlyArray<string>,\n): string | undefined => {\n for (const localName of localNames) {\n for (const prefix of prefixes) {\n const value = attributes[`${prefix}:${localName}`]\n\n if (value !== undefined) return value\n }\n }\n\n for (const localName of localNames) {\n const value = attributes[localName]\n\n if (value !== undefined) return value\n }\n\n return undefined\n}\n","import { opfNamespacedAttribute } from \"./opfNamespace.ts\"\n\n/**\n * How an OPF states a `dc:identifier`'s scheme. Both halves are facts about\n * what a package document can say, so the parser and the resolver read them\n * from here rather than each spelling them out.\n */\n\n/**\n * Local names an identifier's scheme can be stated under, in the order a read\n * prefers them. `Scheme` is not a second name the spec defines — it is the\n * capitalization some producers emit, read because the value it carries is\n * unambiguous.\n */\nexport const OPF_IDENTIFIER_SCHEME_LOCAL_NAMES: ReadonlyArray<string> =\n Object.freeze([\"scheme\", \"Scheme\"])\n\n/**\n * Attribute names an identifier's scheme can be stated on under the\n * conventional `opf` prefix, in the order a read prefers them, ending with the\n * bare EPUB 2 form.\n *\n * Prefer {@link opfIdentifierSchemeAttribute} with the document's own prefixes:\n * a package may bind the OPF namespace to another prefix, which these literal\n * names do not cover.\n */\nexport const OPF_IDENTIFIER_SCHEME_ATTRIBUTES: ReadonlyArray<string> =\n Object.freeze([\"opf:scheme\", \"opf:Scheme\", \"scheme\"])\n\n/**\n * ONIX codelist 5 codes, which is the list an `identifier-type` refinement\n * states its value against when it names `onix:codelist5` as its scheme.\n */\nconst ONIX_CODE_LIST_5_IDENTIFIER_TYPES: Readonly<Record<string, string>> = {\n \"02\": \"ISBN\",\n \"03\": \"GTIN\",\n \"04\": \"UPC\",\n \"05\": \"ISMN\",\n \"06\": \"DOI\",\n \"13\": \"LCCN\",\n \"14\": \"GTIN\",\n \"15\": \"ISBN\",\n \"22\": \"URN\",\n \"23\": \"OCLC\",\n \"24\": \"ISBN\",\n \"25\": \"ISMN\",\n \"26\": \"DOI\",\n \"34\": \"GTIN\",\n \"35\": \"ARK\",\n}\n\n/** The `<meta>` fields an identifier's type is read from. */\nexport type OpfIdentifierTypeMeta = {\n readonly value?: string\n readonly scheme?: string\n}\n\n/**\n * The scheme an EPUB 3 `identifier-type` refinement names. A value stated\n * against `onix:codelist5` is a code from that list — `15` is an ISBN — and is\n * translated; a code the list does not define, and any value stated against\n * another scheme or none, is already a scheme name and passes through\n * verbatim.\n */\nexport const opfIdentifierTypeScheme = (\n meta: OpfIdentifierTypeMeta | undefined,\n): string | undefined => {\n const value = meta?.value?.trim()\n\n if (value === undefined || value.length === 0) return undefined\n if (meta?.scheme?.trim().toLowerCase() !== \"onix:codelist5\") return value\n\n return ONIX_CODE_LIST_5_IDENTIFIER_TYPES[value] ?? value\n}\n\n/**\n * The scheme attribute an element carries, if any. `prefixes` are the ones the\n * document binds to the OPF namespace — `opfNamespacePrefixes` reads them off\n * the package element — and default to the conventional `opf` alone.\n */\nexport const opfIdentifierSchemeAttribute = (\n attributes: Readonly<Record<string, string | undefined>>,\n prefixes: ReadonlyArray<string> = [\"opf\"],\n): string | undefined =>\n opfNamespacedAttribute(\n attributes,\n prefixes,\n OPF_IDENTIFIER_SCHEME_LOCAL_NAMES,\n )\n","import { tokenizeXmlSpaceSeparatedList } from \"../../utils/tokenizeXmlSpaceSeparatedList.ts\"\n\nexport type OpfItemrefLayoutHints = {\n readonly renditionLayout?: `reflowable` | `pre-paginated`\n readonly renditionFlow?:\n | `scrolled-continuous`\n | `scrolled-doc`\n | `paginated`\n | `auto`\n readonly pageSpreadLeft?: true\n readonly pageSpreadRight?: true\n}\n\n/**\n * EPUB `itemref` `properties` attribute (space-separated tokens).\n *\n * @see https://www.w3.org/TR/epub/#attrdef-properties\n */\nexport const layoutHintsFromItemrefProperties = (\n properties: string | undefined,\n): OpfItemrefLayoutHints => {\n const tokens = tokenizeXmlSpaceSeparatedList(properties)\n if (tokens.length === 0) {\n return {}\n }\n\n let renditionLayout: `reflowable` | `pre-paginated` | undefined\n if (tokens.includes(`rendition:layout-reflowable`)) {\n renditionLayout = `reflowable`\n }\n if (tokens.includes(`rendition:layout-pre-paginated`)) {\n renditionLayout = `pre-paginated`\n }\n\n let renditionFlow: OpfItemrefLayoutHints[\"renditionFlow\"]\n if (tokens.includes(`rendition:flow-auto`)) {\n renditionFlow = `auto`\n }\n if (tokens.includes(`rendition:flow-paginated`)) {\n renditionFlow = `paginated`\n }\n if (tokens.includes(`rendition:flow-scrolled-doc`)) {\n renditionFlow = `scrolled-doc`\n }\n if (tokens.includes(`rendition:flow-scrolled-continuous`)) {\n renditionFlow = `scrolled-continuous`\n }\n\n return {\n ...(renditionLayout !== undefined ? { renditionLayout } : {}),\n ...(renditionFlow !== undefined ? { renditionFlow } : {}),\n ...(tokens.includes(`page-spread-left`)\n ? { pageSpreadLeft: true as const }\n : {}),\n ...(tokens.includes(`page-spread-right`)\n ? { pageSpreadRight: true as const }\n : {}),\n }\n}\n","import type { XmlElement, XmlNodeBase } from \"xmldoc\"\nimport { XmlDocument } from \"xmldoc\"\nimport { tokenizeXmlSpaceSeparatedList } from \"../../utils/tokenizeXmlSpaceSeparatedList.ts\"\nimport { opfIdentifierSchemeAttribute } from \"./identifierScheme.ts\"\nimport {\n opfNamespacedAttribute,\n opfNamespacePrefixes,\n type XmlNamespaceScope,\n xmlNamespaceScope,\n} from \"./opfNamespace.ts\"\nimport { layoutHintsFromItemrefProperties } from \"./spineItemrefProperties.ts\"\n\nexport type OpfSpineManifestItem = {\n readonly id: string\n readonly href: string\n readonly mediaType?: string\n readonly properties?: string\n}\n\nexport type OpfIdentifier = {\n readonly value: string\n readonly scheme?: string\n /** Element `id`, which can be targeted by `package@unique-identifier`. */\n readonly id?: string\n /** Present when `package@unique-identifier` targets this identifier. */\n readonly unique?: true\n}\n\nexport type OpfTitle = {\n readonly value: string\n /** Element `id`, which `meta refines` entries target. */\n readonly id?: string\n}\n\nexport type OpfSpineRow = {\n readonly idref: string\n readonly id: string\n readonly href: string\n readonly mediaType?: string\n readonly properties?: string\n readonly renditionLayout?: `reflowable` | `pre-paginated`\n readonly renditionFlow?:\n | `scrolled-continuous`\n | `scrolled-doc`\n | `paginated`\n | `auto`\n readonly pageSpreadLeft?: true\n readonly pageSpreadRight?: true\n}\n\nexport type OpfGuideReference = {\n readonly href: string\n readonly title: string\n readonly type: string\n}\n\n/**\n * One `<meta>` child of `metadata`, captured verbatim whatever its shape:\n * the EPUB 3 `property`/`refines` form, the EPUB 2 `name`/`content` pair\n * form (including vendor vocabularies such as `calibre:series`), or any\n * mix. Attributes and text are trimmed; blank ones are omitted. This is the\n * open-world channel — OPF `meta` is an open vocabulary, so everything is\n * kept rather than a hand-picked subset.\n */\nexport type OpfMetaEntry = {\n /** EPUB 3 `property` attribute (e.g. `dcterms:modified`, `belongs-to-collection`). */\n readonly property?: string\n /** EPUB 3 `refines` attribute, verbatim (usually `#some-id`). */\n readonly refines?: string\n readonly scheme?: string\n readonly id?: string\n /** EPUB 2 `name` attribute (name/content pair form). */\n readonly name?: string\n /** EPUB 2 `content` attribute. */\n readonly content?: string\n /** Trimmed element text (EPUB 3 form), when non-empty. */\n readonly value?: string\n}\n\nexport type OpfContributor = {\n /** Trimmed element text. */\n readonly name: string\n /** Which element the entry was authored as. */\n readonly source: \"creator\" | \"contributor\"\n /** Element `id`, the anchor for EPUB 3 `refines` metadata. */\n readonly id?: string\n /**\n * Role tokens (MARC relator codes in well-formed books, e.g. `aut`,\n * `ill`): the EPUB 2 `opf:role` attribute first, then every EPUB 3\n * `meta refines property=\"role\"` value in document order. Verbatim,\n * not validated against the MARC list.\n */\n readonly roles: ReadonlyArray<string>\n /** EPUB 2 `opf:file-as` attribute, or the `file-as` refines when absent. */\n readonly fileAs?: string\n}\n\nconst elementLocalName = (name: string): string =>\n name.includes(\":\") ? name.slice(name.lastIndexOf(\":\") + 1) : name\n\nconst localNameEq = (elementName: string, wantLocal: string): boolean =>\n elementLocalName(elementName).toLowerCase() === wantLocal.toLowerCase()\n\nconst isXmlElement = (node: XmlNodeBase): node is XmlElement =>\n node.type === \"element\"\n\nconst childNamedLocal = (\n parent: XmlElement,\n localName: string,\n): XmlElement | undefined => {\n for (const node of parent.children) {\n if (!isXmlElement(node)) continue\n if (localNameEq(node.name, localName)) return node\n }\n return undefined\n}\n\nconst childrenNamedLocal = (\n parent: XmlElement,\n localName: string,\n): XmlElement[] => {\n const out: XmlElement[] = []\n for (const node of parent.children) {\n if (!isXmlElement(node)) continue\n if (localNameEq(node.name, localName)) out.push(node)\n }\n return out\n}\n\nconst identifiersFromMetadata = (\n metadataEl: XmlElement,\n uniqueIdentifierId: string | undefined,\n metadataScope: XmlNamespaceScope,\n): OpfIdentifier[] => {\n const identifiers: OpfIdentifier[] = []\n\n metadataEl.eachChild((child) => {\n if (elementLocalName(child.name).toLowerCase() !== \"identifier\") return\n\n const value = child.val.trim()\n if (value.length === 0) return\n\n const scheme = opfIdentifierSchemeAttribute(\n child.attr,\n opfNamespacePrefixes(xmlNamespaceScope(child.attr, metadataScope)),\n )\n const schemeTrimmed = scheme?.trim()\n const idTrimmed = child.attr.id?.trim()\n const id =\n idTrimmed !== undefined && idTrimmed.length > 0 ? idTrimmed : undefined\n\n identifiers.push({\n value,\n ...(schemeTrimmed !== undefined && schemeTrimmed.length > 0\n ? { scheme: schemeTrimmed }\n : {}),\n ...(id !== undefined ? { id } : {}),\n ...(id !== undefined && id === uniqueIdentifierId\n ? { unique: true }\n : {}),\n })\n })\n\n return identifiers\n}\n\nconst titlesFromMetadata = (metadataEl: XmlElement): OpfTitle[] => {\n const titles: OpfTitle[] = []\n\n metadataEl.eachChild((child) => {\n if (elementLocalName(child.name).toLowerCase() !== \"title\") return\n\n const value = child.val.trim()\n if (value.length === 0) return\n\n const id = child.attr.id?.trim()\n\n titles.push({\n value,\n ...(id !== undefined && id.length > 0 ? { id } : {}),\n })\n })\n\n return titles\n}\n\nconst firstTextByLocalName = (\n metadataEl: XmlElement,\n localName: string,\n): string | undefined => {\n let found: string | undefined\n metadataEl.eachChild((child) => {\n if (found !== undefined) return\n if (elementLocalName(child.name).toLowerCase() !== localName.toLowerCase())\n return\n const t = child.val.trim()\n if (t.length > 0) found = t\n })\n return found\n}\n\nconst textsByLocalName = (\n metadataEl: XmlElement,\n localName: string,\n): string[] => {\n const out: string[] = []\n metadataEl.eachChild((child) => {\n if (elementLocalName(child.name).toLowerCase() !== localName.toLowerCase())\n return\n const t = child.val.trim()\n if (t.length > 0) out.push(t)\n })\n return out\n}\n\nconst coverContentIdFromMetadata = (\n metadataEl: XmlElement,\n): string | undefined => {\n let coverId: string | undefined\n metadataEl.eachChild((child) => {\n if (coverId !== undefined) return\n if (elementLocalName(child.name).toLowerCase() !== \"meta\") return\n if (child.attr.name?.toLowerCase() !== \"cover\") return\n const content = child.attr.content?.trim()\n if (content !== undefined && content.length > 0) coverId = content\n })\n return coverId\n}\n\n/**\n * EPUB cover image inside the manifest. Resolution order, matching\n * what the spec lays out and what the bulk of EPUB producers in the\n * wild rely on:\n *\n * 1. EPUB 3 — the manifest item carrying the `cover-image` token in\n * its `properties` attribute (§ D.6.1).\n * 2. EPUB 2 — `<meta name=\"cover\" content=\"ID\"/>` in `metadata`,\n * resolved to the manifest item with that `id`.\n * 3. Last-resort fallback — any image manifest item whose `id`\n * contains the substring `cover` (case-insensitive); covers the\n * long tail of producers that emit neither the EPUB 3 property\n * nor the EPUB 2 meta.\n *\n * Each step requires the candidate manifest item to advertise an\n * `image/*` media type so non-image artefacts named `cover` (XHTML\n * cover pages, NCX entries) don't slip through.\n */\nconst coverHrefFromManifestAndMetadata = ({\n manifestItems,\n metadataEl,\n}: {\n manifestItems: ReadonlyArray<OpfSpineManifestItem>\n metadataEl: XmlElement | undefined\n}): string | undefined => {\n const isImage = (item: OpfSpineManifestItem): boolean =>\n item.mediaType?.toLowerCase().includes(\"image/\") === true\n\n const byCoverImageProperty = manifestItems.find((item) => {\n if (!isImage(item)) return false\n return tokenizeXmlSpaceSeparatedList(item.properties).includes(\n \"cover-image\",\n )\n })\n if (byCoverImageProperty !== undefined) return byCoverImageProperty.href\n\n if (metadataEl !== undefined) {\n const coverContentId = coverContentIdFromMetadata(metadataEl)\n if (coverContentId !== undefined) {\n const match = manifestItems.find(\n (item) => item.id === coverContentId && isImage(item),\n )\n if (match !== undefined) return match.href\n }\n }\n\n return manifestItems.find(\n (item) => item.id.toLowerCase().includes(\"cover\") && isImage(item),\n )?.href\n}\n\nconst trimmedAttr = (el: XmlElement, name: string): string | undefined => {\n const trimmed = el.attr[name]?.trim()\n return trimmed !== undefined && trimmed.length > 0 ? trimmed : undefined\n}\n\n/** {@link trimmedAttr} for an attribute in the OPF namespace. */\nconst trimmedOpfAttr = (\n el: XmlElement,\n opfPrefixes: ReadonlyArray<string>,\n localName: string,\n): string | undefined => {\n const trimmed = opfNamespacedAttribute(el.attr, opfPrefixes, [\n localName,\n ])?.trim()\n\n return trimmed !== undefined && trimmed.length > 0 ? trimmed : undefined\n}\n\nconst metasFromMetadata = (metadataEl: XmlElement): OpfMetaEntry[] => {\n const metas: OpfMetaEntry[] = []\n\n metadataEl.eachChild((child) => {\n if (elementLocalName(child.name).toLowerCase() !== \"meta\") return\n\n const property = trimmedAttr(child, \"property\")\n const refines = trimmedAttr(child, \"refines\")\n const scheme = trimmedAttr(child, \"scheme\")\n const id = trimmedAttr(child, \"id\")\n const name = trimmedAttr(child, \"name\")\n const content = trimmedAttr(child, \"content\")\n const valueTrimmed = child.val.trim()\n const value = valueTrimmed.length > 0 ? valueTrimmed : undefined\n\n const entry: OpfMetaEntry = {\n ...(property !== undefined ? { property } : {}),\n ...(refines !== undefined ? { refines } : {}),\n ...(scheme !== undefined ? { scheme } : {}),\n ...(id !== undefined ? { id } : {}),\n ...(name !== undefined ? { name } : {}),\n ...(content !== undefined ? { content } : {}),\n ...(value !== undefined ? { value } : {}),\n }\n\n if (Object.keys(entry).length === 0) return\n\n metas.push(entry)\n })\n\n return metas\n}\n\n/** `refines` is a relative IRI, in practice `#id`; a missing `#` is tolerated. */\nconst refinesTargetsId = (refines: string, id: string): boolean =>\n refines.replace(/^#/, \"\") === id\n\nconst contributorsFromMetadata = (\n metadataEl: XmlElement,\n metas: ReadonlyArray<OpfMetaEntry>,\n metadataScope: XmlNamespaceScope,\n): OpfContributor[] => {\n const contributors: OpfContributor[] = []\n\n metadataEl.eachChild((child) => {\n const local = elementLocalName(child.name).toLowerCase()\n if (local !== \"creator\" && local !== \"contributor\") return\n\n const name = child.val.trim()\n if (name.length === 0) return\n\n const id = trimmedAttr(child, \"id\")\n const refinesFor = (property: string): string[] =>\n id === undefined\n ? []\n : metas.flatMap((meta) =>\n meta.refines !== undefined &&\n refinesTargetsId(meta.refines, id) &&\n meta.property === property &&\n meta.value !== undefined\n ? [meta.value]\n : [],\n )\n\n const opfPrefixes = opfNamespacePrefixes(\n xmlNamespaceScope(child.attr, metadataScope),\n )\n const attributeRole = trimmedOpfAttr(child, opfPrefixes, \"role\")\n const roles = [\n ...(attributeRole !== undefined ? [attributeRole] : []),\n ...refinesFor(\"role\"),\n ]\n\n const fileAs =\n trimmedOpfAttr(child, opfPrefixes, \"file-as\") ?? refinesFor(\"file-as\")[0]\n\n contributors.push({\n name,\n source: local,\n ...(id !== undefined ? { id } : {}),\n roles,\n ...(fileAs !== undefined ? { fileAs } : {}),\n })\n })\n\n return contributors\n}\n\nconst metaValByProperty = (\n metadataEl: XmlElement,\n property: string,\n): string | undefined => {\n const meta = childrenNamedLocal(metadataEl, \"meta\").find(\n (m) => m.attr.property === property,\n )\n const raw = meta?.val\n if (raw === undefined || raw.trim().length === 0) return undefined\n return raw\n}\n\nconst guideFromPackage = (doc: XmlElement): OpfGuideReference[] => {\n const guideEl = childNamedLocal(doc, \"guide\")\n if (guideEl === undefined) return []\n\n const refs: OpfGuideReference[] = []\n\n for (const ref of childrenNamedLocal(guideEl, \"reference\")) {\n const href = ref.attr.href?.trim()\n if (href === undefined || href.length === 0) continue\n refs.push({\n href,\n title: ref.attr.title?.trim() ?? ``,\n type: ref.attr.type?.trim() ?? ``,\n })\n }\n\n return refs\n}\n\nconst manifestItemFromXmlElement = (\n item: XmlElement,\n): OpfSpineManifestItem | undefined => {\n const id = item.attr.id\n const href = item.attr.href\n if (id === undefined || id.length === 0) return undefined\n if (href === undefined || href.length === 0) return undefined\n\n const mediaType = item.attr[\"media-type\"]\n const properties = item.attr.properties?.trim()\n return {\n id,\n href,\n ...(mediaType !== undefined && mediaType.length > 0 ? { mediaType } : {}),\n ...(properties !== undefined && properties.length > 0\n ? { properties }\n : {}),\n }\n}\n\nconst manifestItemsAndById = (\n manifestEl: XmlElement,\n): {\n items: OpfSpineManifestItem[]\n byId: Map<string, OpfSpineManifestItem>\n} => {\n const items: OpfSpineManifestItem[] = []\n const byId = new Map<string, OpfSpineManifestItem>()\n\n for (const el of childrenNamedLocal(manifestEl, \"item\")) {\n const parsed = manifestItemFromXmlElement(el)\n if (parsed === undefined) continue\n items.push(parsed)\n byId.set(parsed.id, parsed)\n }\n\n return { items, byId }\n}\n\nconst spineRowsFromByIdAndSpine = (\n byId: Map<string, OpfSpineManifestItem>,\n spineEl: XmlElement,\n): OpfSpineRow[] => {\n const rows: OpfSpineRow[] = []\n\n for (const itemref of childrenNamedLocal(spineEl, \"itemref\")) {\n const idref = itemref.attr.idref\n if (idref === undefined || idref.trim().length === 0) continue\n\n const manifestItem = byId.get(idref)\n if (manifestItem === undefined) continue\n\n const hints = layoutHintsFromItemrefProperties(itemref.attr.properties)\n\n rows.push({\n idref,\n id: manifestItem.id,\n href: manifestItem.href,\n ...(manifestItem.mediaType !== undefined\n ? { mediaType: manifestItem.mediaType }\n : {}),\n ...(manifestItem.properties !== undefined\n ? { properties: manifestItem.properties }\n : {}),\n ...(hints.renditionLayout !== undefined\n ? { renditionLayout: hints.renditionLayout }\n : {}),\n ...(hints.renditionFlow !== undefined\n ? { renditionFlow: hints.renditionFlow }\n : {}),\n ...(hints.pageSpreadLeft !== undefined\n ? { pageSpreadLeft: hints.pageSpreadLeft }\n : {}),\n ...(hints.pageSpreadRight !== undefined\n ? { pageSpreadRight: hints.pageSpreadRight }\n : {}),\n })\n }\n\n return rows\n}\n\nexport type OpfMetadata = {\n readonly kind: \"opf\"\n readonly manifestItems: ReadonlyArray<OpfSpineManifestItem>\n readonly spineRows: ReadonlyArray<OpfSpineRow>\n readonly spineTocIdref: string | undefined\n readonly identifiers: ReadonlyArray<OpfIdentifier>\n /**\n * Every non-empty `dc:title`, in document order — the first is the main\n * title per EPUB 3. Ids are retained so `meta refines` entries\n * (`title-type`, `display-seq`, `file-as`) can be attached to their title.\n */\n readonly titles: ReadonlyArray<OpfTitle>\n /** `dc:creator` values, in document order, trimmed; empty when none. */\n readonly creators: ReadonlyArray<string>\n /**\n * Every `dc:creator` and `dc:contributor`, in document order, with role\n * attribution from both conventions (EPUB 2 `opf:role` attribute, EPUB 3\n * `meta refines property=\"role\"`). `creators` stays the plain-text view\n * of the `dc:creator` subset.\n */\n readonly contributors: ReadonlyArray<OpfContributor>\n /** First non-empty `dc:publisher`, trimmed. */\n readonly publisher: string | undefined\n /** First non-empty `dc:description`, trimmed. */\n readonly description: string | undefined\n /** First non-empty `dc:rights`, trimmed. */\n readonly rights: string | undefined\n /** `dc:language` values, in document order, trimmed; empty when none. */\n readonly languages: ReadonlyArray<string>\n /** `dc:subject` values, in document order, trimmed; empty when none. */\n readonly subjects: ReadonlyArray<string>\n /**\n * Raw `dc:date` value as authored. EPUB 3 requires W3CDTF (a profile\n * of ISO 8601), but real-world publishers also ship free text here,\n * so the value is exposed verbatim and consumers normalize as needed.\n */\n readonly date: string | undefined\n /**\n * Manifest-relative `href` of the cover image, when one can be\n * resolved from `cover-image` properties (EPUB 3), the EPUB 2\n * `<meta name=\"cover\">` convention, or an `id` that contains\n * `cover` on an image manifest item. The href is returned exactly\n * as it appears in the manifest — callers own folder-prefix\n * resolution against the OPF's location in the archive.\n */\n readonly coverHref: string | undefined\n readonly renditionLayoutMeta: string | undefined\n readonly renditionFlowMeta: string | undefined\n readonly renditionSpreadMeta: string | undefined\n readonly pageProgressionDirection: string | undefined\n readonly guide: ReadonlyArray<OpfGuideReference>\n /**\n * Every `<meta>` element of `metadata`, captured verbatim (see\n * {@link OpfMetaEntry}). Known properties (`rendition:*`, roles…) are\n * also exposed as dedicated fields; this list is the lossless record.\n */\n readonly metas: ReadonlyArray<OpfMetaEntry>\n}\n\n/**\n * Parses an EPUB package document (OPF) into structured metadata.\n *\n * Direct children of `package` (`metadata`, `manifest`, `spine`, `guide`) and\n * their structural children (`item`, `itemref`, `reference`, `meta`) are\n * matched by **local name** (ASCII case-insensitive), so prefixed tags such as\n * `opf:manifest` are supported the same as unprefixed `manifest`.\n *\n * Attribute names on `spine` / `itemref` are still read as emitted by xmldoc\n * (no QName normalization).\n */\nexport const parseOpf = (opfXml: string): OpfMetadata => {\n const doc = new XmlDocument(opfXml)\n const manifestEl = childNamedLocal(doc, \"manifest\")\n const spineEl = childNamedLocal(doc, \"spine\")\n const metadataEl = childNamedLocal(doc, \"metadata\")\n const packageScope = xmlNamespaceScope(doc.attr)\n const uniqueIdentifierIdRaw = doc.attr[\"unique-identifier\"]?.trim()\n const uniqueIdentifierId =\n uniqueIdentifierIdRaw !== undefined && uniqueIdentifierIdRaw.length > 0\n ? uniqueIdentifierIdRaw\n : undefined\n\n let manifestItems: OpfSpineManifestItem[] = []\n let spineRows: OpfSpineRow[] = []\n\n if (manifestEl !== undefined) {\n const { items, byId } = manifestItemsAndById(manifestEl)\n manifestItems = items\n if (spineEl !== undefined) {\n spineRows = spineRowsFromByIdAndSpine(byId, spineEl)\n }\n }\n\n const pageProgressionDirectionRaw =\n spineEl?.attr[\"page-progression-direction\"]\n const pageProgressionDirection =\n pageProgressionDirectionRaw !== undefined &&\n pageProgressionDirectionRaw.trim().length > 0\n ? pageProgressionDirectionRaw\n : undefined\n\n const spineTocRaw = spineEl?.attr.toc\n const spineTocIdref =\n spineTocRaw !== undefined && spineTocRaw.trim().length > 0\n ? spineTocRaw.trim()\n : undefined\n\n let titles: OpfTitle[] = []\n let publisher: string | undefined\n let description: string | undefined\n let rights: string | undefined\n let date: string | undefined\n let creators: string[] = []\n let contributors: OpfContributor[] = []\n let languages: string[] = []\n let subjects: string[] = []\n let renditionLayoutMeta: string | undefined\n let renditionFlowMeta: string | undefined\n let renditionSpreadMeta: string | undefined\n let metas: OpfMetaEntry[] = []\n const identifiers: OpfIdentifier[] = []\n\n if (metadataEl !== undefined) {\n const metadataScope = xmlNamespaceScope(metadataEl.attr, packageScope)\n\n titles = titlesFromMetadata(metadataEl)\n publisher = firstTextByLocalName(metadataEl, \"publisher\")\n description = firstTextByLocalName(metadataEl, \"description\")\n rights = firstTextByLocalName(metadataEl, \"rights\")\n date = firstTextByLocalName(metadataEl, \"date\")\n creators = textsByLocalName(metadataEl, \"creator\")\n languages = textsByLocalName(metadataEl, \"language\")\n subjects = textsByLocalName(metadataEl, \"subject\")\n renditionLayoutMeta = metaValByProperty(metadataEl, \"rendition:layout\")\n renditionFlowMeta = metaValByProperty(metadataEl, \"rendition:flow\")\n renditionSpreadMeta = metaValByProperty(metadataEl, \"rendition:spread\")\n metas = metasFromMetadata(metadataEl)\n contributors = contributorsFromMetadata(metadataEl, metas, metadataScope)\n identifiers.push(\n ...identifiersFromMetadata(metadataEl, uniqueIdentifierId, metadataScope),\n )\n }\n\n const coverHref = coverHrefFromManifestAndMetadata({\n manifestItems,\n metadataEl,\n })\n\n const guide = guideFromPackage(doc)\n\n return {\n kind: \"opf\",\n manifestItems,\n spineRows,\n spineTocIdref,\n identifiers,\n titles,\n creators,\n contributors,\n publisher,\n description,\n rights,\n languages,\n subjects,\n date,\n coverHref,\n renditionLayoutMeta,\n renditionFlowMeta,\n renditionSpreadMeta,\n pageProgressionDirection,\n guide,\n metas,\n }\n}\n","import { readRecordAsText } from \"../../archives/readRecordAsText.ts\"\nimport type { Archive } from \"../../archives/types.ts\"\nimport { getArchiveOpfInfo } from \"./getArchiveOpfInfo.ts\"\nimport { type OpfMetadata, parseOpf } from \"./parse.ts\"\n\nexport type ArchiveOpfParsed = {\n opf: OpfMetadata\n basePath: string\n}\n\n/**\n * Loads and parses the package OPF from `archive`.\n *\n * **Attention — not memoized:** every call re-reads the OPF record and runs\n * `parseOpf`. Manifest generation already avoids redundant parses by awaiting\n * this once and passing `archiveOpf` into manifest hooks. The resource pipeline\n * (`generateResourceFromArchive` → `defaultHook`) invokes this again per\n * resource unless callers later thread parsed OPF in explicitly (optional\n * parameter, archive open hook, etc.).\n */\nexport async function readArchiveOpf(\n archive: Archive,\n): Promise<ArchiveOpfParsed | undefined> {\n const { data: opsFile, basePath } = getArchiveOpfInfo(archive) || {}\n\n if (!opsFile || opsFile.dir) {\n return undefined\n }\n\n const raw = await readRecordAsText(opsFile)\n\n return {\n opf: parseOpf(raw),\n basePath,\n }\n}\n","import type { XmlElement } from \"xmldoc\"\nimport { XmlDocument } from \"xmldoc\"\n\nexport const APPLE_IBOOKS_DISPLAY_OPTIONS_FILENAME =\n \"com.apple.ibooks.display-options.xml\"\n\nexport type AppleDisplayOption = {\n readonly name?: string\n readonly value: string\n}\n\nexport type AppleMetadata = {\n readonly kind: \"apple\"\n readonly displayOptions?: {\n readonly platform?: {\n readonly options: ReadonlyArray<AppleDisplayOption>\n }\n }\n}\n\nconst platformOptionsFromElement = (\n platform: XmlElement,\n): ReadonlyArray<AppleDisplayOption> =>\n platform.childrenNamed(\"option\").map((option) => ({\n name: option.attr?.name,\n value: option.val,\n }))\n\nexport const parseAppleDisplayOptionsXml = (xml: string): AppleMetadata => {\n const doc = new XmlDocument(xml)\n const root = doc.name?.toLowerCase()\n\n if (root !== \"display_options\") {\n return { kind: \"apple\" }\n }\n\n const platformEl = doc.childNamed(\"platform\")\n\n return {\n kind: \"apple\",\n displayOptions: {\n ...(platformEl !== undefined\n ? { platform: { options: platformOptionsFromElement(platformEl) } }\n : {}),\n },\n }\n}\n","import type { XmlElement } from \"xmldoc\"\nimport { XmlDocument } from \"xmldoc\"\nimport type { ComicInfoManga } from \"./manga.ts\"\n\n/** Canonical top-level filename; real archives may use any casing. */\nexport const COMIC_INFO_FILENAME = \"ComicInfo.xml\"\n\n/**\n * Every simple child element of the ComicInfo schema (v2.1 draft), audited\n * against the XSD. `Pages` is deliberately absent (nested block, skipped by\n * the parser). This list is the single source of truth for the known keys\n * of {@link ComicInfo} and the domain of the losslessness mapping table in\n * `resolve.ts` — adding a field here without declaring its metadata home\n * there is a type error.\n *\n * @see https://github.com/anansi-project/comicinfo/blob/main/drafts/v2.1/ComicInfo.xsd\n */\nexport const COMIC_INFO_KNOWN_FIELDS = [\n \"AgeRating\",\n \"AlternateCount\",\n \"AlternateNumber\",\n \"AlternateSeries\",\n \"BlackAndWhite\",\n \"Characters\",\n \"Colorist\",\n \"CommunityRating\",\n \"Count\",\n \"CoverArtist\",\n \"Day\",\n \"Editor\",\n \"Format\",\n \"Genre\",\n \"GTIN\",\n \"Imprint\",\n \"Inker\",\n \"LanguageISO\",\n \"Letterer\",\n \"Locations\",\n \"MainCharacterOrTeam\",\n \"Manga\",\n \"Month\",\n \"Notes\",\n \"Number\",\n \"PageCount\",\n \"Penciller\",\n \"Publisher\",\n \"Review\",\n \"ScanInformation\",\n \"Series\",\n \"SeriesGroup\",\n \"StoryArc\",\n \"StoryArcNumber\",\n \"Summary\",\n \"Tags\",\n \"Teams\",\n \"Title\",\n \"Translator\",\n \"Volume\",\n \"Web\",\n \"Writer\",\n \"Year\",\n] as const\n\nexport type ComicInfoKnownField = (typeof COMIC_INFO_KNOWN_FIELDS)[number]\n\n/**\n * Parsed `ComicInfo.xml` root: one optional string property per child element,\n * using the same names as in the file (e.g. `Title`, `GTIN`, `LanguageISO`).\n * Nested blocks such as `Pages` are skipped. Other simple child elements are\n * still copied onto this object under their tag name.\n *\n * @see https://anansi-project.github.io/docs/comicinfo/intro\n * @see https://github.com/anansi-project/comicinfo/blob/main/drafts/v2.1/ComicInfo.xsd for schema\n */\nexport interface ComicInfo\n extends Partial<Record<ComicInfoKnownField, string>> {\n readonly kind: \"comicInfo\"\n /** Schema literals per {@link ComicInfoManga}; files may still use other strings. */\n Manga?: ComicInfoManga | (string & {})\n [tag: string]: string | undefined\n}\n\nconst SKIP_ELEMENT_CHILDREN = new Set([\"Pages\", \"ComicInfo\"])\n\nconst hasNestedElement = (el: XmlElement) =>\n el.children.some((c) => c.type === \"element\")\n\nconst trimmedText = (el: XmlElement): string | undefined => {\n const t = el.val.trim()\n return t.length > 0 ? t : undefined\n}\n\n/**\n * Parse a raw `ComicInfo.xml` body. Each direct child element with plain text\n * becomes a property named after that tag. Malformed XML throws; the parser\n * error is attached as `cause`.\n */\nexport const parseComicInfo = (xml: string): ComicInfo => {\n let doc: XmlDocument\n try {\n doc = new XmlDocument(xml)\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : String(cause)\n throw new Error(`${COMIC_INFO_FILENAME} is malformed: ${message}`, {\n cause,\n })\n }\n\n const fields: Record<string, string> = {}\n\n doc.eachChild((child) => {\n if (child.type !== \"element\") return\n if (SKIP_ELEMENT_CHILDREN.has(child.name)) return\n if (hasNestedElement(child)) return\n\n const text = trimmedText(child)\n if (text === undefined) return\n if (fields[child.name] !== undefined) return\n\n fields[child.name] = text\n })\n\n // `as ComicInfo`: TS cannot infer that spreading `Record<string, string>` into `{ kind }` satisfies the named optional fields plus `[tag: string]: string | undefined` on `ComicInfo`.\n return { kind: \"comicInfo\", ...fields } as ComicInfo\n}\n","import { XmlDocument } from \"xmldoc\"\n\nexport const KOBO_DISPLAY_OPTIONS_FILENAME = \"com.kobobooks.display-options.xml\"\n\n/**\n * Normalized fields from Kobo-specific XML sidecars. Grows as new Kobo\n * document kinds are supported; absent keys mean not present or unknown.\n */\nexport type KoboMetadata = {\n readonly kind: \"kobo\"\n renditionLayout?: `reflowable` | `pre-paginated`\n}\n\nconst parseKoboDisplayOptionsDocument = (\n doc: XmlDocument,\n): { renditionLayout?: \"pre-paginated\" } => {\n const platform = doc.childNamed(\"platform\")\n if (!platform) return {}\n\n for (const option of platform.childrenNamed(\"option\")) {\n if (option.attr?.name !== \"fixed-layout\") continue\n if (option.val.trim().toLowerCase() === \"true\") {\n return { renditionLayout: \"pre-paginated\" }\n }\n return {}\n }\n\n return {}\n}\n\n/**\n * Parse a Kobo-related XML document. Unsupported or unrecognized roots\n * yield an empty object. Malformed XML throws from the underlying parser.\n */\nexport const parseKoboXml = (xml: string): KoboMetadata => {\n const doc = new XmlDocument(xml)\n const root = doc.name?.toLowerCase()\n\n if (root === \"display_options\") {\n return { kind: \"kobo\", ...parseKoboDisplayOptionsDocument(doc) }\n }\n\n return { kind: \"kobo\" }\n}\n","/**\n * Returns a copy of `obj` without the keys whose value is `undefined`, so\n * resolver results stay sparse (an absent key and an `undefined` value are\n * the same statement: \"this source had nothing to say\").\n */\nexport const omitUndefined = <T extends object>(obj: T): T => {\n const entries = Object.entries(obj).filter(([, value]) => value !== undefined)\n\n // `as T`: Object.entries/fromEntries erase the key–value pairing; filtering\n // only drops `undefined` values, which `T`'s optional fields already allow,\n // so the runtime shape is a subset of `T` with no way to express it inline.\n return Object.fromEntries(entries) as T\n}\n","/**\n * Resolves the `.`/`..` segments of a container path (RFC 3986 §5.2.4\n * \"remove dot segments\"), so a manifest href authored relative to a nested\n * package document lands on the real record uri. Operates on the raw path\n * (no percent-decoding) so the result still matches the archive's record uris\n * verbatim. A `..` past the root is clamped, matching URL resolution.\n */\nconst removeDotSegments = (path: string): string => {\n const out: string[] = []\n\n for (const segment of path.split(\"/\")) {\n if (segment === \".\") continue\n if (segment === \"..\") {\n out.pop()\n continue\n }\n out.push(segment)\n }\n\n return out.join(\"/\")\n}\n\n/**\n * Rebases an OPF manifest-relative `href` (spine item, cover image…) onto the\n * package document's `basePath` to produce a container-relative uri — the\n * coordinate space every resolved archive part addresses. The href is a\n * relative reference resolved against the package document's directory, so\n * dot segments are collapsed (`OEBPS/Text` + `../Images/cover.jpg` →\n * `OEBPS/Images/cover.jpg`), not concatenated verbatim. Absolute http(s) urls\n * (the URL-list pseudo-archives author these) are their own record uri and\n * pass through untouched.\n */\nexport const toContainerUri = (href: string, basePath: string): string => {\n if (/^https?:\\/\\//.test(href)) return href\n\n return removeDotSegments(basePath ? `${basePath}/${href}` : href)\n}\n","import {\n detectMimeTypeFromName,\n isMediaContentMimeType,\n parseContentType,\n} from \"@prose-reader/shared\"\nimport type { Archive, FileRecord } from \"../archives/types.ts\"\nimport { getArchiveFileRecordByUri, isFileRecord } from \"../archives/types.ts\"\nimport { APPLE_IBOOKS_DISPLAY_OPTIONS_FILENAME } from \"../metadata/apple/parse.ts\"\nimport { COMIC_INFO_FILENAME } from \"../metadata/comicInfo/parse.ts\"\nimport { KOBO_DISPLAY_OPTIONS_FILENAME } from \"../metadata/kobo/parse.ts\"\nimport type { ArchiveOpfParsed } from \"../metadata/opf/readArchiveOpf.ts\"\nimport { readArchiveOpf } from \"../metadata/opf/readArchiveOpf.ts\"\nimport { omitUndefined } from \"../utils/omitUndefined.ts\"\nimport { toContainerUri } from \"../utils/toContainerUri.ts\"\n\n/**\n * One resource of the publication's default reading sequence, in the\n * container's coordinate space: `uri` addresses the archive record (no\n * serving base baked in — consumers rebase into their own space), except\n * for remote resources authored as absolute http(s) URLs, which pass\n * through as-is.\n */\nexport type ArchiveReadingOrderItem = {\n readonly uri: string\n /** OPF manifest item id, when the container has a package document. */\n readonly id?: string\n readonly mediaType?: string\n /** Uncompressed byte length from the archive record, when known. */\n readonly size?: number\n /**\n * Resolved per-item layout: the itemref override when authored, else the\n * package-level `rendition:layout`, else (non-EPUB) `pre-paginated` for\n * discrete media (images, audio, video). Container sidecars (Apple/Kobo\n * display options) deliberately do not contribute here — they only inform\n * the publication-level layout in the resolved metadata.\n */\n readonly renditionLayout?: \"reflowable\" | \"pre-paginated\"\n readonly renditionFlow?:\n | \"scrolled-continuous\"\n | \"scrolled-doc\"\n | \"paginated\"\n | \"auto\"\n readonly pageSpreadLeft?: true\n readonly pageSpreadRight?: true\n /**\n * Share of the publication's total progression, in [0, 1], summing to ~1:\n * size-proportional for EPUBs (document length is a fair pagination\n * proxy), an equal split otherwise (an image's byte size says nothing\n * about its reading time).\n */\n readonly progressionWeight: number\n}\n\nconst validatedPackageLayout = (\n raw: string | undefined,\n): \"reflowable\" | \"pre-paginated\" | undefined => {\n const v = raw?.trim().toLowerCase()\n return v === \"reflowable\" || v === \"pre-paginated\" ? v : undefined\n}\n\nconst readingOrderFromOpf = (\n archive: Archive,\n { opf, basePath }: ArchiveOpfParsed,\n): ArchiveReadingOrderItem[] => {\n const packageLayout = validatedPackageLayout(opf.renditionLayoutMeta)\n\n const rows = opf.spineRows.map((row) => {\n const uri = toContainerUri(row.href, basePath)\n const record = getArchiveFileRecordByUri(archive, uri)\n\n return { row, uri, record }\n })\n\n const totalSize = rows.reduce(\n (total, { record }) => total + (record?.size ?? 0),\n 0,\n )\n\n return rows.map(({ row, uri, record }) =>\n omitUndefined({\n uri,\n id: row.id,\n mediaType: row.mediaType ?? record?.encodingFormat,\n size: record?.size,\n renditionLayout: row.renditionLayout ?? packageLayout,\n renditionFlow: row.renditionFlow,\n pageSpreadLeft: row.pageSpreadLeft,\n pageSpreadRight: row.pageSpreadRight,\n progressionWeight:\n totalSize > 0\n ? (record?.size ?? 0) / totalSize\n : 1 / opf.spineRows.length,\n }),\n )\n}\n\nconst comicInfoBasenameLower = COMIC_INFO_FILENAME.toLowerCase()\nconst appleDisplayOptionsBasenameLower =\n APPLE_IBOOKS_DISPLAY_OPTIONS_FILENAME.toLowerCase()\n\n/**\n * Container sidecars describe the publication, they are not content: they\n * never belong to the reading order of a sidecar-driven archive (CBZ with\n * ComicInfo.xml, Kobo/Apple-flagged containers), and neither do OS litter\n * files such as `Thumbs.db`.\n */\nconst isSidecarOrLitterRecord = (record: FileRecord): boolean => {\n const basenameLower = record.basename.toLowerCase()\n\n return (\n basenameLower === comicInfoBasenameLower ||\n basenameLower === appleDisplayOptionsBasenameLower ||\n record.uri.endsWith(KOBO_DISPLAY_OPTIONS_FILENAME) ||\n basenameLower.endsWith(`.db`)\n )\n}\n\nconst readingOrderFromRecords = (\n archive: Archive,\n): ArchiveReadingOrderItem[] => {\n const files = archive.records\n .filter(isFileRecord)\n .filter((record) => !isSidecarOrLitterRecord(record))\n\n return files.map((record) => {\n const mediaType =\n parseContentType(record.encodingFormat ?? \"\") ||\n detectMimeTypeFromName(record.basename)\n\n return omitUndefined({\n uri: record.uri,\n mediaType,\n size: record.size,\n renditionLayout:\n mediaType !== undefined && isMediaContentMimeType(mediaType)\n ? (\"pre-paginated\" as const)\n : undefined,\n progressionWeight: 1 / files.length,\n })\n })\n}\n\n/**\n * Resolves the publication's default reading sequence from the container:\n * the OPF spine when a package document exists, the archive's file listing\n * otherwise (sidecars and OS litter excluded). Costs one OPF read at most —\n * pass `opf` (an already parsed `readArchiveOpf` result) to skip the\n * internal lookup, e.g. alongside `resolveArchiveToc`.\n */\nexport const resolveArchiveReadingOrder = async (\n archive: Archive,\n { opf }: { opf?: ArchiveOpfParsed } = {},\n): Promise<ArchiveReadingOrderItem[]> => {\n // A malformed package document is treated as no package document: the\n // reading order always degrades to the archive's file listing rather than\n // throwing (books in the wild are dirty). `resolveArchive` reads and\n // reports the OPF once, then threads it here — passing `undefined` when\n // that read failed — so this fallback read must swallow the same parse\n // error too, otherwise it would re-surface and escape the lenient resolve.\n const archiveOpf =\n opf ?? (await readArchiveOpf(archive).catch(() => undefined))\n\n if (archiveOpf) return readingOrderFromOpf(archive, archiveOpf)\n\n return readingOrderFromRecords(archive)\n}\n","import { detectMimeTypeFromName, parseContentType } from \"@prose-reader/shared\"\nimport type { Archive } from \"../archives/types.ts\"\nimport { getArchiveFileRecordByUri } from \"../archives/types.ts\"\nimport type { ArchiveOpfParsed } from \"../metadata/opf/readArchiveOpf.ts\"\nimport { readArchiveOpf } from \"../metadata/opf/readArchiveOpf.ts\"\nimport type { ArchiveReadingOrderItem } from \"../readingOrder/resolveArchiveReadingOrder.ts\"\nimport { resolveArchiveReadingOrder } from \"../readingOrder/resolveArchiveReadingOrder.ts\"\nimport type { ResolvedCover } from \"../types/resolvedMetadata.ts\"\nimport { omitUndefined } from \"../utils/omitUndefined.ts\"\nimport { toContainerUri } from \"../utils/toContainerUri.ts\"\n\nconst mediaTypeForUri = (archive: Archive, uri: string): string | undefined => {\n const record = getArchiveFileRecordByUri(archive, uri)\n\n if (record === undefined) return undefined\n\n return (\n parseContentType(record.encodingFormat ?? \"\") ||\n detectMimeTypeFromName(record.basename)\n )\n}\n\n/**\n * Resolves the publication's cover resource.\n *\n * Resolution order:\n *\n * 1. **OPF cover** — the manifest cover image (`cover-image` property, the\n * EPUB 2 `<meta name=\"cover\">` convention, or a `cover`-ish image id; see\n * {@link OpfMetadata.coverHref}), rebased onto the package document's base\n * path. A package document that declares no cover is authoritative: no\n * interior image is promoted to cover, so the result is `undefined`.\n * 2. **Package-less containers** (CBZ, folder of images, URL list) — the\n * first reading-order resource, the conventional first page. Sidecars and\n * OS litter are already excluded from the reading order, so this never\n * picks `ComicInfo.xml` or `Thumbs.db`.\n *\n * `undefined` when neither applies (an empty container, or an OPF without a\n * declared cover). Costs one OPF read at most — pass an already parsed `opf`\n * and/or the already resolved `readingOrder` to skip the internal lookups\n * (e.g. alongside `resolveArchive`).\n */\nexport const resolveArchiveCover = async (\n archive: Archive,\n {\n opf,\n readingOrder,\n }: {\n opf?: ArchiveOpfParsed\n readingOrder?: ArchiveReadingOrderItem[]\n } = {},\n): Promise<ResolvedCover | undefined> => {\n // A malformed package document is treated as no package document, mirroring\n // resolveArchiveReadingOrder: the cover degrades to the first-page fallback\n // rather than throwing (books in the wild are dirty).\n const archiveOpf =\n opf ?? (await readArchiveOpf(archive).catch(() => undefined))\n\n // 1. an OPF-declared cover is authoritative.\n if (archiveOpf !== undefined && archiveOpf.opf.coverHref !== undefined) {\n const uri = toContainerUri(archiveOpf.opf.coverHref, archiveOpf.basePath)\n\n return omitUndefined({\n uri,\n mediaType: mediaTypeForUri(archive, uri),\n confidence: \"derived\" as const,\n })\n }\n\n // 2. no declared cover — assume the first image of the reading order. This\n // holds whether or not a package document exists: an authored reflowable\n // EPUB has a text spine with no image items, so it stays undefined rather\n // than promoting an interior illustration (a manifest resource, not a spine\n // item); only image-content publications resolve a cover here — comics,\n // image archives, and synthetic image-spine OPFs (e.g. createArchiveFromUrls\n // lists, whose generated OPF carries no cover marker). Audio/video are\n // pre-paginated discrete media but not images, so they are skipped too.\n const order =\n readingOrder ??\n (await resolveArchiveReadingOrder(archive, { opf: archiveOpf }))\n const firstImage = order.find(\n (item) => item.mediaType?.startsWith(\"image/\") === true,\n )\n\n if (firstImage === undefined) return undefined\n\n // the reading order already computed the media type the same way\n return omitUndefined({\n uri: firstImage.uri,\n mediaType: firstImage.mediaType,\n confidence: \"assumed\" as const,\n })\n}\n","import { isXmlBasedMimeType } from \"@prose-reader/shared\"\nimport { XmlDocument } from \"xmldoc\"\nimport { readRecordAsText } from \"../archives/readRecordAsText.ts\"\nimport type { Archive } from \"../archives/types.ts\"\nimport { getArchiveFileRecordByUri, isFileRecord } from \"../archives/types.ts\"\nimport type { ArchiveReadingOrderItem } from \"../readingOrder/resolveArchiveReadingOrder.ts\"\nimport { Report } from \"../report.ts\"\n\nconst hasDocMetaViewport = (doc: XmlDocument) => {\n const metaElm = doc\n .descendantWithPath(\"head\")\n ?.childrenNamed(\"meta\")\n .find((node) => node.attr.name === \"viewport\")\n\n return !!(metaElm && metaElm.attr.name === \"viewport\")\n}\n\n/**\n * The industry heuristic behind the `layoutScan` effort modifier: many\n * fixed-layout books in the wild declare `rendition:layout` reflowable (or\n * nothing) while every spine document carries a `head > meta[name=viewport]`\n * — the fixed-layout signal renderers actually honor.\n *\n * Returns `true` only when EVERY reading-order document is XML-based\n * (by media type or extension) AND declares a viewport; any non-XML entry\n * (an image in the spine…) or viewport-less document returns `false`.\n * A document that fails to read or parse also returns `false` (logged via\n * Report) — an unreadable book must never be promoted.\n *\n * **Cost: O(reading order)** — reads and XML-parses every document. This is\n * the expensive tier of archive resolution; keep it opt-in.\n */\nexport const readingOrderDocumentsAllHaveViewport = async (\n archive: Archive,\n readingOrder: ReadonlyArray<ArchiveReadingOrderItem>,\n): Promise<boolean> => {\n if (readingOrder.length === 0) return false\n\n for (const item of readingOrder) {\n if (!isXmlBasedMimeType({ mimeType: item.mediaType, uri: item.uri })) {\n return false\n }\n\n const record = getArchiveFileRecordByUri(archive, item.uri)\n\n if (!record || !isFileRecord(record)) return false\n\n try {\n const content = await readRecordAsText(record)\n\n if (!hasDocMetaViewport(new XmlDocument(content))) return false\n } catch (e) {\n Report.error(`layout scan: unable to inspect ${item.uri}`, e)\n\n return false\n }\n }\n\n return true\n}\n","import { readRecordAsText } from \"../../archives/readRecordAsText.ts\"\nimport type { Archive, FileRecord } from \"../../archives/types.ts\"\nimport { isFileRecord } from \"../../archives/types.ts\"\nimport {\n APPLE_IBOOKS_DISPLAY_OPTIONS_FILENAME,\n type AppleMetadata,\n parseAppleDisplayOptionsXml,\n} from \"./parse.ts\"\n\nconst appleDisplayOptionsBasenameLower =\n APPLE_IBOOKS_DISPLAY_OPTIONS_FILENAME.toLowerCase()\n\n/**\n * Loads and parses the Apple iBooks display options sidecar\n * (`META-INF/com.apple.ibooks.display-options.xml`) from `archive`.\n *\n * Returns `undefined` when the archive has no such file (matched on\n * basename, ASCII case-insensitive, anywhere in the container). Malformed\n * XML throws — lenient callers own the catch.\n */\nexport async function readArchiveApple(\n archive: Archive,\n): Promise<AppleMetadata | undefined> {\n const record = archive.records.find(\n (file): file is FileRecord =>\n isFileRecord(file) &&\n file.basename.toLowerCase() === appleDisplayOptionsBasenameLower,\n )\n\n if (!record) {\n return undefined\n }\n\n return parseAppleDisplayOptionsXml(await readRecordAsText(record))\n}\n","import {\n type Archive,\n type FileRecord,\n isFileRecord,\n} from \"../../archives/types.ts\"\nimport { COMIC_INFO_FILENAME } from \"./parse.ts\"\n\nconst comicInfoFilenameLower = COMIC_INFO_FILENAME.toLowerCase()\n\nexport const getArchiveHasComicInfo = (archive: Archive) =>\n archive.records.find(\n (record): record is FileRecord =>\n isFileRecord(record) &&\n record.basename.toLowerCase() === comicInfoFilenameLower,\n )\n","/**\n * Allowed `Manga` element values (ComicInfo 2.0 `Manga` simpleType).\n *\n * @see https://anansi-project.github.io/docs/comicinfo/documentation#manga\n */\nexport const COMIC_INFO_MANGA_VALUES = [\n \"Unknown\",\n \"No\",\n \"Yes\",\n \"YesAndRightToLeft\",\n] as const\n\nexport type ComicInfoManga = (typeof COMIC_INFO_MANGA_VALUES)[number]\n\nexport const isComicInfoManga = (value: string): value is ComicInfoManga => {\n for (const v of COMIC_INFO_MANGA_VALUES) {\n if (v === value) return true\n }\n return false\n}\n","import { readRecordAsText } from \"../../archives/readRecordAsText.ts\"\nimport type { Archive } from \"../../archives/types.ts\"\nimport { getArchiveHasComicInfo } from \"./getArchiveHasComicInfo.ts\"\nimport { type ComicInfo, parseComicInfo } from \"./parse.ts\"\n\n/**\n * Loads and parses the `ComicInfo.xml` sidecar from `archive`.\n *\n * Returns `undefined` when the archive has no ComicInfo file (matched on\n * basename, ASCII case-insensitive, anywhere in the container). Malformed\n * XML throws, same as {@link parseComicInfo} — lenient callers (a whole\n * archive resolve that treats books in the wild as dirty) own the catch.\n */\nexport async function readArchiveComicInfo(\n archive: Archive,\n): Promise<ComicInfo | undefined> {\n const record = getArchiveHasComicInfo(archive)\n\n if (!record) {\n return undefined\n }\n\n return parseComicInfo(await readRecordAsText(record))\n}\n","import { readRecordAsText } from \"../../archives/readRecordAsText.ts\"\nimport type { Archive, FileRecord } from \"../../archives/types.ts\"\nimport { isFileRecord } from \"../../archives/types.ts\"\nimport { Report } from \"../../report.ts\"\nimport {\n KOBO_DISPLAY_OPTIONS_FILENAME,\n type KoboMetadata,\n parseKoboXml,\n} from \"./parse.ts\"\n\n/**\n * Loads and parses Kobo XML sidecars (`com.kobobooks.display-options.xml`)\n * from `archive`, merging them into a single {@link KoboMetadata}.\n *\n * Files are matched anywhere in the container (`uri` suffix). When several\n * match, they are read in record order and the last parsed value for a field\n * wins. A file that fails to parse is skipped (logged via Report) — per-file\n * resilience is part of the merge semantics, unlike the single-file readers\n * which throw. Returns `undefined` when no Kobo file is found.\n */\nexport async function readArchiveKobo(\n archive: Archive,\n): Promise<KoboMetadata | undefined> {\n const records = archive.records.filter(\n (file): file is FileRecord =>\n isFileRecord(file) && file.uri.endsWith(KOBO_DISPLAY_OPTIONS_FILENAME),\n )\n\n if (records.length === 0) {\n return undefined\n }\n\n let renditionLayout: KoboMetadata[\"renditionLayout\"]\n\n for (const record of records) {\n const content = await readRecordAsText(record)\n\n try {\n const { renditionLayout: layout } = parseKoboXml(content)\n if (layout) renditionLayout = layout\n } catch (e) {\n Report.error(\n `Unable to parse ${KOBO_DISPLAY_OPTIONS_FILENAME} (${record.uri})`,\n e,\n )\n }\n }\n\n return {\n kind: `kobo`,\n ...(renditionLayout !== undefined ? { renditionLayout } : {}),\n }\n}\n","import type { Archive } from \"../../archives/types.ts\"\nimport type { ArchiveOpfParsed } from \"./readArchiveOpf.ts\"\n\nexport const getSpineItemFilesFromArchive = async ({\n archive,\n archiveOpf,\n}: {\n archive: Archive\n archiveOpf: ArchiveOpfParsed | undefined\n}) => {\n if (!archiveOpf) return []\n\n const { opf, basePath: opfBasePath } = archiveOpf\n const { spineRows } = opf\n\n const archiveSpineItems = archive.records.filter((file) => {\n return spineRows.find((item) => {\n if (!opfBasePath) return `${item.href}` === file.uri\n return `${opfBasePath}/${item.href}` === file.uri\n })\n })\n\n return archiveSpineItems\n}\n","import type { Archive } from \"../../archives/types.ts\"\n\nconst hasOpfExtension = (path: string) => path.toLowerCase().endsWith(`.opf`)\n\nexport const isArchiveEpub = (archive: Archive) => {\n return archive.records.some(\n (file) =>\n !file.dir &&\n (hasOpfExtension(file.basename) || hasOpfExtension(file.uri)),\n )\n}\n","import type { Exhaustive } from \"../../types/exhaustive.ts\"\nimport type {\n ResolvedAppleMetadata,\n ResolvedMetadata,\n} from \"../../types/resolvedMetadata.ts\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport type { AppleMetadata } from \"./parse.ts\"\n\nexport const resolveApple = (input: AppleMetadata): ResolvedMetadata => {\n const options = input.displayOptions?.platform?.options\n\n const fixedLayoutValue = options\n ?.find((option) => option.name === \"fixed-layout\")\n ?.value.trim()\n .toLowerCase()\n const fixedLayout =\n fixedLayoutValue === \"true\"\n ? true\n : fixedLayoutValue === \"false\"\n ? false\n : undefined\n\n const apple = omitUndefined({\n fixedLayout,\n options:\n options !== undefined && options.length > 0\n ? options.map(({ name, value }) => omitUndefined({ name, value }))\n : undefined,\n } satisfies Exhaustive<ResolvedAppleMetadata>)\n\n return omitUndefined({\n renditionLayout:\n fixedLayout === true ? (\"pre-paginated\" as const) : undefined,\n apple: Object.keys(apple).length > 0 ? apple : undefined,\n })\n}\n","import type { Exhaustive } from \"../../types/exhaustive.ts\"\nimport type {\n ResolvedAggregateRating,\n ResolvedCollection,\n ResolvedComicInfoMetadata,\n ResolvedContributor,\n ResolvedContributorRole,\n ResolvedDate,\n ResolvedMetadata,\n ResolvedMetadataIdentifier,\n} from \"../../types/resolvedMetadata.ts\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport type { ComicInfo, ComicInfoKnownField } from \"./parse.ts\"\n\nconst readingDirection = (info: ComicInfo): \"ltr\" | \"rtl\" | undefined => {\n switch (info.Manga) {\n case \"YesAndRightToLeft\":\n return \"rtl\"\n case \"Yes\":\n case \"No\":\n return \"ltr\"\n default:\n return undefined\n }\n}\n\nconst trimToUndefined = (raw: string | undefined): string | undefined => {\n if (raw === undefined) return undefined\n const trimmed = raw.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\n/**\n * Split a comma-separated ComicInfo value into its individual tokens.\n * `Writer`, `Genre`, `Tags`, `Characters`… all follow the same de facto\n * schema convention: tokens separated by `,`, with whitespace trimmed and\n * empty tokens dropped (real-world files leave trailing commas around).\n */\nconst splitCommaList = (raw: string | undefined): string[] => {\n if (raw === undefined) return []\n return raw\n .split(\",\")\n .map((token) => token.trim())\n .filter((token) => token.length > 0)\n}\n\nconst parseNonNegativeInt = (raw: string | undefined): number | undefined => {\n const trimmed = trimToUndefined(raw)\n if (trimmed === undefined) return undefined\n if (!/^\\d+$/.test(trimmed)) return undefined\n const n = Number.parseInt(trimmed, 10)\n return Number.isFinite(n) ? n : undefined\n}\n\n/** Non-negative decimal (\"1\", \"1.5\"); anything else is undefined. */\nconst parseDecimal = (raw: string | undefined): number | undefined => {\n const trimmed = trimToUndefined(raw)\n if (trimmed === undefined) return undefined\n if (!/^\\d+(\\.\\d+)?$/.test(trimmed)) return undefined\n const n = Number.parseFloat(trimmed)\n return Number.isFinite(n) ? n : undefined\n}\n\n/** Schema yes/no enums (`Yes`/`No`/`Unknown`), tolerant on casing. */\nconst parseYesNo = (raw: string | undefined): boolean | undefined => {\n const normalized = trimToUndefined(raw)?.toLowerCase()\n if (normalized === \"yes\") return true\n if (normalized === \"no\") return false\n return undefined\n}\n\n/** Same schema-literal matching as {@link readingDirection}. */\nconst mangaFlag = (info: ComicInfo): boolean | undefined => {\n switch (info.Manga) {\n case \"Yes\":\n case \"YesAndRightToLeft\":\n return true\n case \"No\":\n return false\n default:\n return undefined\n }\n}\n\nconst dateFromYearMonthDay = (info: ComicInfo): ResolvedDate | undefined => {\n const year = parseNonNegativeInt(info.Year)\n const month = parseNonNegativeInt(info.Month)\n const day = parseNonNegativeInt(info.Day)\n\n if (year === undefined && month === undefined && day === undefined) {\n return undefined\n }\n\n return {\n ...(year !== undefined ? { year } : {}),\n ...(month !== undefined ? { month } : {}),\n ...(day !== undefined ? { day } : {}),\n }\n}\n\nconst COMIC_INFO_ROLE_FIELDS = [\n [\"Writer\", \"author\"],\n [\"Penciller\", \"penciler\"],\n [\"Inker\", \"inker\"],\n [\"Colorist\", \"colorist\"],\n [\"Letterer\", \"letterer\"],\n [\"CoverArtist\", \"coverArtist\"],\n [\"Editor\", \"editor\"],\n [\"Translator\", \"translator\"],\n] as const satisfies ReadonlyArray<\n [ComicInfoKnownField, ResolvedContributorRole]\n>\n\n/**\n * One entry per person: a name appearing in several role fields (writes and\n * inks their own book) gets its roles merged, in field order above.\n */\nconst contributorsFromComicInfo = (info: ComicInfo): ResolvedContributor[] => {\n const byName = new Map<string, { name: string; roles: string[] }>()\n\n for (const [field, role] of COMIC_INFO_ROLE_FIELDS) {\n for (const name of splitCommaList(info[field])) {\n const existing = byName.get(name)\n if (existing) {\n if (!existing.roles.includes(role)) existing.roles.push(role)\n } else {\n byName.set(name, { name, roles: [role] })\n }\n }\n }\n\n return [...byName.values()]\n}\n\n/**\n * `Number` and `Count` are facts about this issue's place in its series,\n * which a sidecar can state without naming the series — so a nameless entry\n * is still emitted rather than dropping what the file says.\n */\nconst seriesFromComicInfo = (\n info: ComicInfo,\n): ResolvedCollection | undefined => {\n const series = omitUndefined({\n name: trimToUndefined(info.Series),\n position: parseDecimal(info.Number),\n total: parseNonNegativeInt(info.Count),\n })\n\n return Object.keys(series).length > 0 ? series : undefined\n}\n\nconst alternateSeriesFromComicInfo = (\n info: ComicInfo,\n): ResolvedCollection | undefined => {\n const name = trimToUndefined(info.AlternateSeries)\n if (name === undefined) return undefined\n\n return omitUndefined({\n name,\n position: parseDecimal(info.AlternateNumber),\n total: parseNonNegativeInt(info.AlternateCount),\n })\n}\n\n/** `StoryArc`/`StoryArcNumber` are comma lists zipped positionally. */\nconst storyArcsFromComicInfo = (info: ComicInfo): ResolvedCollection[] => {\n const names = splitCommaList(info.StoryArc)\n if (names.length === 0) return []\n\n const positions = splitCommaList(info.StoryArcNumber)\n\n return names.map((name, index) =>\n omitUndefined({ name, position: parseDecimal(positions[index]) }),\n )\n}\n\nconst emptyToUndefined = <T>(\n values: ReadonlyArray<T>,\n): ReadonlyArray<T> | undefined => (values.length > 0 ? values : undefined)\n\nconst webFromComicInfo = (info: ComicInfo): ReadonlyArray<string> =>\n (info.Web ?? \"\").split(/\\s+/).filter((token) => token.length > 0)\n\nconst isAbsoluteHttpUrl = (value: string): boolean => {\n try {\n const url = new URL(value)\n\n return (\n (url.protocol === \"http:\" || url.protocol === \"https:\") &&\n url.hostname.length > 0\n )\n } catch {\n return false\n }\n}\n\nconst webIdentifiers = (\n values: ReadonlyArray<string>,\n): ReadonlyArray<ResolvedMetadataIdentifier> =>\n values\n .filter(\n (value, index, all) =>\n isAbsoluteHttpUrl(value) && all.indexOf(value) === index,\n )\n .map((value) => ({ value, scheme: \"URL\" }))\n\nconst comicCornerFromComicInfo = (\n info: ComicInfo,\n webValues: ReadonlyArray<string>,\n): ResolvedMetadata[\"comicInfo\"] => {\n const corner = omitUndefined({\n manga: mangaFlag(info),\n blackAndWhite: parseYesNo(info.BlackAndWhite),\n volume: parseNonNegativeInt(info.Volume),\n format: trimToUndefined(info.Format),\n ageRating: trimToUndefined(info.AgeRating),\n notes: trimToUndefined(info.Notes),\n review: trimToUndefined(info.Review),\n web: emptyToUndefined(webValues),\n scanInformation: trimToUndefined(info.ScanInformation),\n mainCharacterOrTeam: trimToUndefined(info.MainCharacterOrTeam),\n characters: emptyToUndefined(splitCommaList(info.Characters)),\n teams: emptyToUndefined(splitCommaList(info.Teams)),\n locations: emptyToUndefined(splitCommaList(info.Locations)),\n storyArcs: emptyToUndefined(storyArcsFromComicInfo(info)),\n alternateSeries: alternateSeriesFromComicInfo(info),\n } satisfies Exhaustive<ResolvedComicInfoMetadata>)\n\n return Object.keys(corner).length > 0 ? corner : undefined\n}\n\n/**\n * `CommunityRating` is already the schema's 0–5 star mean, so it needs no\n * rescaling — only the range check the field promises. The sidecar states no\n * number of ratings.\n */\nconst aggregateRatingFromComicInfo = (\n info: ComicInfo,\n): ResolvedAggregateRating | undefined => {\n const value = parseDecimal(info.CommunityRating)\n\n return value !== undefined && value <= 5 ? { value } : undefined\n}\n\nconst belongsToFromComicInfo = (\n info: ComicInfo,\n): ResolvedMetadata[\"belongsTo\"] => {\n const series = seriesFromComicInfo(info)\n const collection = emptyToUndefined(\n splitCommaList(info.SeriesGroup).map((name) => ({ name })),\n )\n\n if (series === undefined && collection === undefined) return undefined\n\n return omitUndefined({\n series: series !== undefined ? [series] : undefined,\n collection,\n })\n}\n\nexport const resolveComicInfo = (info: ComicInfo): ResolvedMetadata => {\n const rawGtin = info.GTIN\n const gtinTrimmed = trimToUndefined(rawGtin)\n const web = webFromComicInfo(info)\n const identifiers: ReadonlyArray<ResolvedMetadataIdentifier> = [\n ...(gtinTrimmed !== undefined\n ? [{ value: gtinTrimmed, scheme: \"GTIN\" }]\n : []),\n ...webIdentifiers(web),\n ]\n const languageIso = trimToUndefined(info.LanguageISO)\n const subjects = [...splitCommaList(info.Genre), ...splitCommaList(info.Tags)]\n const edition = omitUndefined({\n date: dateFromYearMonthDay(info),\n publisher: trimToUndefined(info.Publisher),\n imprint: trimToUndefined(info.Imprint),\n })\n\n const title = trimToUndefined(info.Title)\n\n return omitUndefined({\n titles: title !== undefined ? [{ value: title }] : undefined,\n description: trimToUndefined(info.Summary),\n publication:\n edition.date !== undefined ||\n edition.publisher !== undefined ||\n edition.imprint !== undefined\n ? { edition }\n : undefined,\n languages: languageIso !== undefined ? [languageIso] : undefined,\n subjects: emptyToUndefined(subjects),\n contributors: emptyToUndefined(contributorsFromComicInfo(info)),\n readingDirection: readingDirection(info),\n numberOfPages: parseNonNegativeInt(info.PageCount),\n aggregateRating: aggregateRatingFromComicInfo(info),\n identifiers: emptyToUndefined(identifiers),\n belongsTo: belongsToFromComicInfo(info),\n comicInfo: comicCornerFromComicInfo(info, web),\n })\n}\n","import type { Exhaustive } from \"../../types/exhaustive.ts\"\nimport type {\n ResolvedKoboMetadata,\n ResolvedMetadata,\n} from \"../../types/resolvedMetadata.ts\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport type { KoboMetadata } from \"./parse.ts\"\n\nexport const resolveKobo = (input: KoboMetadata): ResolvedMetadata =>\n omitUndefined({\n renditionLayout: input.renditionLayout,\n kobo:\n input.renditionLayout !== undefined\n ? ({\n fixedLayout: input.renditionLayout === \"pre-paginated\",\n } satisfies Exhaustive<ResolvedKoboMetadata>)\n : undefined,\n })\n","const NUMBER_PATTERN = /[0-9Xx]+/g\nconst ISBN_NUMBER_PATTERN = /^(?:97[89])?\\d{9}[\\dXx]$/\n\n/**\n * Normalize a raw ISBN-ish string into a canonical 10- or 13-character\n * form, or `undefined` when no recognisable ISBN can be recovered.\n *\n * - Strips the common `urn:isbn:` / `isbn:` prefixes.\n * - Drops everything that isn't a digit or `X`.\n * - Validates the resulting length (10 or 13).\n * - Falls back to scanning the numbers in the string, so publishers that\n * stuff free text around the ISBN still yield a usable value.\n *\n * Only a *whole* number is a candidate in that fallback: carving ten digits\n * out of a longer barcode (`036180592125` — a GTIN-12) would invent an ISBN\n * the publication never printed.\n */\nexport const normalizeIsbn = (\n raw: string | number | undefined | null,\n): string | undefined => {\n if (raw === undefined || raw === null) return undefined\n\n const stripped = String(raw)\n .trim()\n .replace(/^urn:isbn:/i, \"\")\n .replace(/^isbn[:\\s-]*/i, \"\")\n\n const digitsOnly = stripped.replace(/[^0-9Xx]/g, \"\")\n\n if (digitsOnly.length === 10 || digitsOnly.length === 13) {\n return digitsOnly.toUpperCase()\n }\n\n for (const number of stripped.match(NUMBER_PATTERN) ?? []) {\n if (ISBN_NUMBER_PATTERN.test(number)) return number.toUpperCase()\n }\n\n return undefined\n}\n","import { normalizeIsbn } from \"./normalizeIsbn.ts\"\n\n/**\n * The ISBN a raw value denotes, or `undefined` when the value is not one.\n *\n * `normalizeIsbn` recovers ISBN *syntax* only: any 10- or 13-character digit\n * run satisfies it, so a scanned comic's retail barcode (`4006381333931`)\n * comes back looking like an ISBN. Books occupy the Bookland range of the GTIN\n * space — an ISBN-13 always starts with `978` or `979` — so requiring that\n * prefix is what separates a real ISBN from any other EAN. ISBN-10 predates\n * Bookland and is accepted on its length alone (it converts into a `978`\n * ISBN-13).\n *\n * Check digits are not verified: a mistyped one still identifies the intended\n * book, and rejecting it would lose more than it protects.\n */\nexport const booklandIsbn = (\n raw: string | number | undefined | null,\n): string | undefined => {\n const isbn = normalizeIsbn(raw)\n\n if (isbn === undefined) return undefined\n\n return isbn.length === 10 || /^97[89]/.test(isbn) ? isbn : undefined\n}\n","const GTIN_LENGTHS = new Set([8, 12, 13, 14])\n\n/**\n * Normalize a raw GTIN / EAN / UPC string to digits only when the length\n * matches a GS1 GTIN family size (8, 12, 13, or 14). Does not verify check digits.\n */\nexport const normalizeGtin = (\n raw: string | number | undefined | null,\n): string | undefined => {\n if (raw === undefined || raw === null) return undefined\n\n const digits = String(raw).replace(/\\D/g, \"\")\n if (digits.length === 0 || !GTIN_LENGTHS.has(digits.length)) return undefined\n\n return digits\n}\n","import type { KnownMetadataIdentifierScheme } from \"../types/resolvedMetadata.ts\"\nimport { booklandIsbn } from \"./booklandIsbn.ts\"\nimport { normalizeGtin } from \"./normalizeGtin.ts\"\n\n/**\n * The schemes a value can announce by itself. Drawn from\n * {@link KnownMetadataIdentifierScheme} rather than restated, so renaming one\n * there fails to compile here.\n */\nexport type InferredIdentifierScheme = Extract<\n KnownMetadataIdentifierScheme,\n \"ISBN\" | \"GTIN\" | \"URL\" | \"Unknown\"\n>\n\n/**\n * The scheme a value announces by its own syntax, for an identifier that\n * announced none: an absolute http(s) link is a `URL`, a Bookland number is an\n * `ISBN`, another well-formed barcode is a `GTIN`, and anything else is\n * `Unknown`.\n *\n * This is what resolution believes about an untyped identifier, so a consumer\n * holding the container itself — one editing it in place — reads it from here\n * rather than restating it, and cannot disagree with `identifiers` about which\n * element carries which scheme.\n */\nexport const inferIdentifierScheme = (\n value: string,\n): InferredIdentifierScheme => {\n const trimmed = value.trim()\n\n if (/^https?:\\/\\//i.test(trimmed)) {\n try {\n const url = new URL(trimmed)\n\n if (\n (url.protocol === \"http:\" || url.protocol === \"https:\") &&\n url.hostname.length > 0\n ) {\n return \"URL\"\n }\n } catch {\n // Continue with identifier-specific inference.\n }\n }\n\n if (booklandIsbn(trimmed) !== undefined) return \"ISBN\"\n if (normalizeGtin(trimmed) !== undefined) return \"GTIN\"\n\n return \"Unknown\"\n}\n","import type {\n KnownMetadataIdentifierScheme,\n MetadataIdentifierScheme,\n} from \"../types/resolvedMetadata.ts\"\n\n/**\n * Keyed by the scheme union so a scheme added to the vocabulary fails to\n * compile until it is spelled here too.\n */\nconst CANONICAL_SPELLINGS: Record<\n Lowercase<KnownMetadataIdentifierScheme>,\n KnownMetadataIdentifierScheme\n> = {\n isbn: \"ISBN\",\n gtin: \"GTIN\",\n doi: \"DOI\",\n googlebooks: \"GoogleBooks\",\n openlibrary: \"OpenLibrary\",\n projectgutenberg: \"ProjectGutenberg\",\n url: \"URL\",\n unknown: \"Unknown\",\n}\n\nconst SPELLINGS_BY_LOWERCASE = new Map<string, KnownMetadataIdentifierScheme>(\n Object.entries(CANONICAL_SPELLINGS),\n)\n\n/**\n * A scheme in the spelling the vocabulary uses, so `opf:scheme=\"isbn\"` and an\n * `identifier-type` of `ISBN` describe the one namespace. Publications author\n * these by hand and their case is not significant.\n *\n * A scheme outside the vocabulary is returned trimmed but otherwise untouched:\n * a custom namespace is a supported case, and its spelling is the\n * publication's to choose.\n */\nexport const normalizeIdentifierScheme = (\n scheme: MetadataIdentifierScheme,\n): MetadataIdentifierScheme => {\n const trimmed = scheme.trim()\n\n return SPELLINGS_BY_LOWERCASE.get(trimmed.toLowerCase()) ?? trimmed\n}\n","/**\n * Extract the calendar components of a W3CDTF literal — the\n * date subset of ISO 8601 that EPUB 3.3 § 5.5.3.2.4 mandates for\n * `dc:date`. Accepted forms: `YYYY`, `YYYY-MM`, `YYYY-MM-DD`, and\n * any of the above followed by a `Thh:mm[:ss[.s]][TZD]` time\n * portion that we ignore.\n *\n * Components are returned as plain integers (`month` is 1-12,\n * `day` is 1-31), independent of the host timezone — using\n * `Date.parse` would shift `2011-01-01` by a day in negative-offset\n * locales, which is the exact bug this regex-based approach exists\n * to avoid. Returns `undefined` when the input doesn't even match\n * a leading 4-digit year so consumers can fall back without\n * branching on partial shapes.\n */\nexport const parseW3cDtfDate = (\n raw: string | undefined,\n):\n | {\n year?: number\n month?: number\n day?: number\n }\n | undefined => {\n if (raw === undefined) return undefined\n\n const match = raw.trim().match(/^(\\d{4})(?:-(\\d{2})(?:-(\\d{2}))?)?/)\n if (match === null) return undefined\n\n const [, yearRaw, monthRaw, dayRaw] = match\n\n return {\n ...(yearRaw !== undefined ? { year: Number.parseInt(yearRaw, 10) } : {}),\n ...(monthRaw !== undefined ? { month: Number.parseInt(monthRaw, 10) } : {}),\n ...(dayRaw !== undefined ? { day: Number.parseInt(dayRaw, 10) } : {}),\n }\n}\n","import type {\n MetadataIdentifier,\n ResolvedCollection,\n ResolvedContributor,\n ResolvedContributorRole,\n ResolvedMetadata,\n ResolvedTitle,\n} from \"../../types/resolvedMetadata.ts\"\nimport { inferIdentifierScheme } from \"../../utils/inferIdentifierScheme.ts\"\nimport { normalizeIdentifierScheme } from \"../../utils/normalizeIdentifierScheme.ts\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport { parseW3cDtfDate } from \"../../utils/parseW3cDtfDate.ts\"\nimport { opfIdentifierTypeScheme } from \"./identifierScheme.ts\"\nimport type {\n OpfContributor,\n OpfIdentifier,\n OpfMetadata,\n OpfMetaEntry,\n OpfTitle,\n} from \"./parse.ts\"\n\n/**\n * Common MARC relator codes normalized into the Readium role vocabulary;\n * anything else passes through verbatim (losslessness beats guessing).\n * @see https://www.loc.gov/marc/relators/relaterm.html\n */\nconst MARC_RELATOR_TO_ROLE: Readonly<\n Partial<Record<string, ResolvedContributorRole>>\n> = {\n art: \"artist\",\n aut: \"author\",\n clr: \"colorist\",\n ctb: \"contributor\",\n edt: \"editor\",\n ill: \"illustrator\",\n nrt: \"narrator\",\n trl: \"translator\",\n}\n\nconst contributorsFromOpf = (\n // `contributors` already covers every dc:creator (the `creators` list is\n // the plain-text view of the same elements), so it is the single source.\n parsed: ReadonlyArray<OpfContributor>,\n): ResolvedContributor[] =>\n parsed.map((contributor) => {\n const roles = contributor.roles.map(\n (token) => MARC_RELATOR_TO_ROLE[token.trim().toLowerCase()] ?? token,\n )\n\n return omitUndefined({\n name: contributor.name,\n roles:\n roles.length > 0\n ? roles\n : // a role-less dc:creator means author per EPUB 2 semantics; a\n // role-less dc:contributor is the generic Readium `contributor`\n [\n contributor.source === \"creator\"\n ? (\"author\" as const)\n : (\"contributor\" as const),\n ],\n sortAs: contributor.fileAs,\n })\n })\n\n/** Same missing-`#` tolerance as the refines matching in parse.ts. */\nconst metaRefinesId = (meta: OpfMetaEntry, id: string): boolean =>\n meta.refines !== undefined && meta.refines.replace(/^#/, \"\") === id\n\nconst refinedIdentifierType = (\n identifier: OpfIdentifier,\n metas: ReadonlyArray<OpfMetaEntry>,\n): string | undefined => {\n const id = identifier.id\n\n if (id === undefined) return undefined\n\n return opfIdentifierTypeScheme(\n metas.find(\n (meta) =>\n meta.property === \"identifier-type\" &&\n metaRefinesId(meta, id) &&\n meta.value !== undefined,\n ),\n )\n}\n\nconst parseDecimal = (raw: string | undefined): number | undefined => {\n const trimmed = raw?.trim()\n if (trimmed === undefined || !/^\\d+(\\.\\d+)?$/.test(trimmed)) return undefined\n const n = Number.parseFloat(trimmed)\n return Number.isFinite(n) ? n : undefined\n}\n\nconst parseNonNegativeInt = (raw: string | undefined): number | undefined => {\n const trimmed = raw?.trim()\n if (trimmed === undefined || !/^\\d+$/.test(trimmed)) return undefined\n const n = Number.parseInt(trimmed, 10)\n return Number.isFinite(n) ? n : undefined\n}\n\nconst trimToUndefined = (raw: string | undefined): string | undefined => {\n const trimmed = raw?.trim()\n\n return trimmed !== undefined && trimmed.length > 0 ? trimmed : undefined\n}\n\n/** The value of the `meta refines=\"#id\"` entry stating `property`. */\nconst refiningValue = (\n metas: ReadonlyArray<OpfMetaEntry>,\n id: string | undefined,\n property: string,\n): string | undefined =>\n id === undefined\n ? undefined\n : metas.find(\n (meta) =>\n metaRefinesId(meta, id) &&\n meta.property === property &&\n meta.value !== undefined,\n )?.value\n\n/**\n * EPUB 3 states a multipart title as several `dc:title` elements refined\n * with `title-type`, `display-seq` and `file-as`. All of them are kept, in\n * document order — the order matters, since the spec makes the first one the\n * main title whatever the refinements say (see `mainTitle`).\n */\nconst titlesFromOpf = (\n parsed: ReadonlyArray<OpfTitle>,\n metas: ReadonlyArray<OpfMetaEntry>,\n): ResolvedTitle[] =>\n parsed.map((title) =>\n omitUndefined({\n value: title.value,\n type: trimToUndefined(\n refiningValue(metas, title.id, \"title-type\"),\n )?.toLowerCase(),\n displaySeq: parseNonNegativeInt(\n refiningValue(metas, title.id, \"display-seq\"),\n ),\n sortAs: trimToUndefined(refiningValue(metas, title.id, \"file-as\")),\n }),\n )\n\n/**\n * A collection can be identified as well as named: `dcterms:identifier`\n * refines the `belongs-to-collection` meta, and an `identifier-type`\n * refinement of that identifier names its scheme.\n */\nconst collectionIdentifiers = (\n metas: ReadonlyArray<OpfMetaEntry>,\n id: string | undefined,\n): ReadonlyArray<MetadataIdentifier> | undefined => {\n if (id === undefined) return undefined\n\n const identifiers = metas.flatMap<MetadataIdentifier>((meta) => {\n if (!metaRefinesId(meta, id) || meta.property !== \"dcterms:identifier\") {\n return []\n }\n\n const value = trimToUndefined(meta.value)\n\n if (value === undefined) return []\n\n const declaredType = opfIdentifierTypeScheme(\n metas.find(\n (candidate) =>\n meta.id !== undefined &&\n metaRefinesId(candidate, meta.id) &&\n candidate.property === \"identifier-type\",\n ),\n )\n\n return [\n {\n value,\n scheme: normalizeIdentifierScheme(\n declaredType ?? inferIdentifierScheme(value),\n ),\n },\n ]\n })\n\n return identifiers.length > 0 ? identifiers : undefined\n}\n\n/**\n * Series/collection membership from the metas: EPUB 3\n * `belongs-to-collection` (a `collection-type` refine of `series` selects\n * the series bucket, anything else — including untyped — is a plain\n * collection), with the calibre `calibre:series`/`calibre:series_index`\n * name/content pair as fallback when no EPUB 3 series entry exists.\n */\nconst collectionsFromOpfMetas = (\n metas: ReadonlyArray<OpfMetaEntry>,\n): { series: ResolvedCollection[]; collection: ResolvedCollection[] } => {\n const series: ResolvedCollection[] = []\n const collection: ResolvedCollection[] = []\n\n for (const meta of metas) {\n if (meta.property !== \"belongs-to-collection\" || meta.value === undefined)\n continue\n\n const id = meta.id\n const refining = (property: string): string | undefined =>\n refiningValue(metas, id, property)\n\n const type = refining(\"collection-type\")?.trim().toLowerCase()\n const entry = omitUndefined({\n name: meta.value,\n identifiers: collectionIdentifiers(metas, id),\n position: parseDecimal(refining(\"group-position\")),\n })\n\n if (type === \"series\") series.push(entry)\n else collection.push(entry)\n }\n\n if (series.length === 0) {\n const calibreSeries = metas.find(\n (meta) =>\n meta.name?.toLowerCase() === \"calibre:series\" &&\n meta.content !== undefined,\n )?.content\n\n if (calibreSeries !== undefined) {\n series.push(\n omitUndefined({\n name: calibreSeries,\n position: parseDecimal(\n metas.find(\n (meta) => meta.name?.toLowerCase() === \"calibre:series_index\",\n )?.content,\n ),\n }),\n )\n }\n }\n\n return { series, collection }\n}\n\nconst validatedRenditionFlow = (\n raw: string | undefined,\n): ResolvedMetadata[\"renditionFlow\"] => {\n const v = raw?.trim()\n if (\n v === \"scrolled-continuous\" ||\n v === \"scrolled-doc\" ||\n v === \"paginated\" ||\n v === \"auto\"\n ) {\n return v\n }\n return undefined\n}\n\nconst validatedRenditionSpread = (\n raw: string | undefined,\n): ResolvedMetadata[\"renditionSpread\"] => {\n const v = raw?.trim()\n if (\n v === \"none\" ||\n v === \"landscape\" ||\n v === \"portrait\" ||\n v === \"both\" ||\n v === \"auto\"\n ) {\n return v\n }\n return undefined\n}\n\nexport const resolveOpf = (input: OpfMetadata): ResolvedMetadata => {\n const {\n titles: parsedTitles,\n contributors: parsedContributors,\n publisher,\n description,\n rights,\n languages,\n subjects,\n date,\n renditionLayoutMeta,\n renditionFlowMeta,\n renditionSpreadMeta,\n pageProgressionDirection,\n identifiers: parsedIdentifiers,\n metas,\n // resolved elsewhere in the archive entity, not from the package alone:\n // the reading order, the toc, the cover and the guide\n kind: _kind,\n creators: _creators,\n manifestItems: _manifestItems,\n spineRows: _spineRows,\n spineTocIdref: _spineTocIdref,\n coverHref: _coverHref,\n guide: _guide,\n ...unhandled\n } = input\n\n // Naming every parsed field above is the contract: adding one to\n // `OpfMetadata` fails here until someone decides what becomes of it.\n unhandled satisfies Record<string, never>\n\n const ppd = pageProgressionDirection?.trim().toLowerCase()\n const readingDirection = ppd === \"ltr\" || ppd === \"rtl\" ? ppd : undefined\n\n const rl = renditionLayoutMeta?.trim().toLowerCase()\n const renditionLayout =\n rl === \"reflowable\" || rl === \"pre-paginated\" ? rl : undefined\n\n const contributors = contributorsFromOpf(parsedContributors)\n const titles = titlesFromOpf(parsedTitles, metas)\n const { series, collection } = collectionsFromOpfMetas(metas)\n\n const belongsTo =\n series.length > 0 || collection.length > 0\n ? omitUndefined({\n series: series.length > 0 ? series : undefined,\n collection: collection.length > 0 ? collection : undefined,\n })\n : undefined\n const edition = omitUndefined({\n date: parseW3cDtfDate(date),\n publisher,\n })\n\n return omitUndefined({\n titles: titles.length > 0 ? titles : undefined,\n description,\n publication:\n edition.date !== undefined || edition.publisher !== undefined\n ? { edition }\n : undefined,\n rights,\n languages: languages.length > 0 ? [...languages] : undefined,\n subjects: subjects.length > 0 ? [...subjects] : undefined,\n contributors: contributors.length > 0 ? contributors : undefined,\n readingDirection,\n renditionLayout,\n renditionFlow: validatedRenditionFlow(renditionFlowMeta),\n renditionSpread: validatedRenditionSpread(renditionSpreadMeta),\n identifiers:\n parsedIdentifiers.length > 0\n ? parsedIdentifiers.map((identifier) =>\n omitUndefined({\n value: identifier.value,\n scheme: normalizeIdentifierScheme(\n identifier.scheme ??\n refinedIdentifierType(identifier, metas) ??\n inferIdentifierScheme(identifier.value),\n ),\n unique: identifier.unique,\n }),\n )\n : undefined,\n belongsTo,\n properties: metas.length > 0 ? [...metas] : undefined,\n })\n}\n","import type { AppleMetadata } from \"./metadata/apple/parse.ts\"\nimport { resolveApple } from \"./metadata/apple/resolve.ts\"\nimport type { ComicInfo } from \"./metadata/comicInfo/parse.ts\"\nimport { resolveComicInfo } from \"./metadata/comicInfo/resolve.ts\"\nimport type { KoboMetadata } from \"./metadata/kobo/parse.ts\"\nimport { resolveKobo } from \"./metadata/kobo/resolve.ts\"\nimport type { OpfMetadata } from \"./metadata/opf/parse.ts\"\nimport { resolveOpf } from \"./metadata/opf/resolve.ts\"\nimport type { ResolvedMetadata } from \"./types/resolvedMetadata.ts\"\n\nexport type ResolvedArchiveInput =\n | ComicInfo\n | KoboMetadata\n | AppleMetadata\n | OpfMetadata\n\n/**\n * Normalizes a single parsed source into the cross-format\n * {@link ResolvedMetadata} vocabulary. To combine several sources with the\n * documented precedence rules, use `resolveMetadata` instead.\n */\nexport const resolveArchiveMetadata = (\n input: ResolvedArchiveInput,\n): ResolvedMetadata => {\n if (input.kind === \"comicInfo\") return resolveComicInfo(input)\n if (input.kind === \"kobo\") return resolveKobo(input)\n if (input.kind === \"apple\") return resolveApple(input)\n return resolveOpf(input)\n}\n","import type { AppleMetadata } from \"./metadata/apple/parse.ts\"\nimport { resolveApple } from \"./metadata/apple/resolve.ts\"\nimport type { ComicInfo } from \"./metadata/comicInfo/parse.ts\"\nimport { resolveComicInfo } from \"./metadata/comicInfo/resolve.ts\"\nimport type { KoboMetadata } from \"./metadata/kobo/parse.ts\"\nimport { resolveKobo } from \"./metadata/kobo/resolve.ts\"\nimport type { OpfMetadata } from \"./metadata/opf/parse.ts\"\nimport { resolveOpf } from \"./metadata/opf/resolve.ts\"\nimport type { ResolvedMetadata } from \"./types/resolvedMetadata.ts\"\nimport { omitUndefined } from \"./utils/omitUndefined.ts\"\n\nexport type ResolveMetadataSources = {\n opf?: OpfMetadata\n comicInfo?: ComicInfo\n apple?: AppleMetadata\n kobo?: KoboMetadata\n}\n\n/**\n * Merges the per-source resolved metadata of every parsed source into one\n * {@link ResolvedMetadata}, with explicit precedence:\n *\n * - Descriptive fields (`titles`, `description`, `publication`,\n * `languages`, `subjects`, `contributors`, `belongsTo`):\n * **OPF wins over ComicInfo** — the package document is the publication's\n * own metadata; a ComicInfo sidecar fills the gaps.\n * - `readingDirection`: **ComicInfo wins over OPF** (`Manga` beats\n * `page-progression-direction`). Deliberate: this preserves the behavior\n * the streamer has always had, and for actual comic archives there is no\n * OPF anyway — see the merge tests for the recorded decision.\n * - `renditionLayout`: **OPF explicit → Apple → Kobo**, first defined wins.\n * Kobo ranks last on purpose: in the manifest pipeline it historically\n * ran after the viewport-promotion optimizer, and since promotion only\n * ever rewrites an already-defined layout, \"after promotion\" and \"after\n * OPF/Apple\" are equivalent ranks.\n * - `identifiers` concatenates (OPF first) — different identifier systems\n * coexist rather than compete.\n * - Format-scoped corners (`comicInfo`, `apple`, `kobo`) and single-source\n * fields (`rights`, `properties`, `numberOfPages`, `aggregateRating`…) come\n * from their only producer.\n */\nexport const resolveMetadata = (\n sources: ResolveMetadataSources,\n): ResolvedMetadata => {\n const opf = sources.opf !== undefined ? resolveOpf(sources.opf) : undefined\n const comicInfo =\n sources.comicInfo !== undefined\n ? resolveComicInfo(sources.comicInfo)\n : undefined\n const apple =\n sources.apple !== undefined ? resolveApple(sources.apple) : undefined\n const kobo =\n sources.kobo !== undefined ? resolveKobo(sources.kobo) : undefined\n\n const identifiers = [\n ...(opf?.identifiers ?? []),\n ...(comicInfo?.identifiers ?? []),\n ]\n const edition = omitUndefined({\n date:\n opf?.publication?.edition?.date ?? comicInfo?.publication?.edition?.date,\n publisher:\n opf?.publication?.edition?.publisher ??\n comicInfo?.publication?.edition?.publisher,\n imprint:\n opf?.publication?.edition?.imprint ??\n comicInfo?.publication?.edition?.imprint,\n })\n\n return omitUndefined({\n titles: opf?.titles ?? comicInfo?.titles,\n description: opf?.description ?? comicInfo?.description,\n publication:\n edition.date !== undefined ||\n edition.publisher !== undefined ||\n edition.imprint !== undefined\n ? { edition }\n : undefined,\n rights: opf?.rights,\n languages: opf?.languages ?? comicInfo?.languages,\n subjects: opf?.subjects ?? comicInfo?.subjects,\n contributors: opf?.contributors ?? comicInfo?.contributors,\n readingDirection: comicInfo?.readingDirection ?? opf?.readingDirection,\n renditionLayout:\n opf?.renditionLayout ?? apple?.renditionLayout ?? kobo?.renditionLayout,\n renditionFlow: opf?.renditionFlow,\n renditionSpread: opf?.renditionSpread,\n numberOfPages: comicInfo?.numberOfPages,\n aggregateRating: comicInfo?.aggregateRating,\n identifiers: identifiers.length > 0 ? identifiers : undefined,\n belongsTo: opf?.belongsTo ?? comicInfo?.belongsTo,\n properties: opf?.properties,\n comicInfo: comicInfo?.comicInfo,\n apple: apple?.apple,\n kobo: kobo?.kobo,\n })\n}\n","import { sortByTitleComparator } from \"@prose-reader/shared\"\nimport type { Archive } from \"../archives/types.ts\"\nimport type { ArchiveTocItem } from \"./types.ts\"\n\n/**\n * Derive a TOC from the folder hierarchy of the archive (e.g. a CBZ with one\n * folder per chapter). Each folder becomes an entry pointing to its first\n * file in natural sort order.\n */\nexport const buildTocFromFolders = (archive: Archive): ArchiveTocItem[] => {\n const filesSortedByAlpha = [...archive.records].sort((a, b) =>\n sortByTitleComparator(a.uri, b.uri),\n )\n\n const combineWith = (\n toc: ArchiveTocItem[],\n folder: string,\n subFolders: string[],\n containerHref: string,\n path: string,\n ): ArchiveTocItem[] => {\n const foundEntry = toc.find((entry) => entry.title === folder)\n const [nextFolderCursor, ...nextSubFolders] = subFolders\n\n if (foundEntry) {\n if (nextFolderCursor) {\n return [\n ...toc.filter((entry) => entry !== foundEntry),\n {\n ...foundEntry,\n // combineWith returns the full merged children list already\n contents: combineWith(\n foundEntry.contents,\n nextFolderCursor,\n nextSubFolders,\n containerHref,\n path,\n ),\n } satisfies ArchiveTocItem,\n ]\n }\n\n const previousRegisteredPathWasLonger =\n foundEntry.path.split(\"/\").length > path.split(\"/\").length\n\n if (previousRegisteredPathWasLonger) {\n return [\n ...toc.filter((entry) => entry !== foundEntry),\n {\n ...foundEntry,\n path,\n containerHref,\n } satisfies ArchiveTocItem,\n ]\n }\n\n return toc\n }\n\n if (nextFolderCursor) {\n return [\n ...toc,\n {\n contents: combineWith(\n [],\n nextFolderCursor,\n nextSubFolders,\n containerHref,\n path,\n ),\n containerHref,\n path,\n title: folder,\n },\n ]\n }\n\n return [\n ...toc,\n {\n contents: [],\n containerHref,\n path,\n title: folder,\n },\n ]\n }\n\n return filesSortedByAlpha.reduce<ArchiveTocItem[]>((acc, file) => {\n if (file.dir) return acc\n\n const folders = file.uri.split(\"/\").slice(0, -1)\n const [firstFolder, ...restFolders] = folders\n\n if (!firstFolder) return acc\n\n // encodeURI leaves `#` and `?` untouched (reserved URI delimiters), but in\n // a raw archive filename they are data and would otherwise be parsed as\n // fragment/query once joined onto a base URL.\n const containerHref = encodeURI(file.uri)\n .replace(/#/g, `%23`)\n .replace(/\\?/g, `%3F`)\n .replace(/\\/$/, \"\")\n const path = file.uri.replace(/\\/$/, \"\")\n\n return combineWith(acc, firstFolder, restFolders, containerHref, path)\n }, [])\n}\n","import { XmlElement, XmlTextNode } from \"xmldoc\"\n\nexport const getXmlElementInnerText = (\n node: XmlElement | undefined,\n): string => {\n if (!node) return \"\"\n\n return node.children\n .map((child) => {\n if (child instanceof XmlTextNode) return child.text\n if (child instanceof XmlElement) return getXmlElementInnerText(child)\n return \"\"\n })\n .join(\"\")\n .trim()\n}\n","import { getUriBasePath, urlJoin } from \"@prose-reader/shared\"\nimport { XmlDocument, XmlElement, type XmlNodeBase } from \"xmldoc\"\nimport { readRecordAsText } from \"../archives/readRecordAsText.ts\"\nimport type { Archive } from \"../archives/types.ts\"\nimport type { OpfMetadata } from \"../metadata/opf/parse.ts\"\nimport { getXmlElementInnerText } from \"../utils/getXmlElementInnerText.ts\"\nimport { tokenizeXmlSpaceSeparatedList } from \"../utils/tokenizeXmlSpaceSeparatedList.ts\"\nimport type { ArchiveTocItem } from \"./types.ts\"\n\nconst manifestItemIsNavDocument = (item: {\n readonly properties?: string\n}): boolean => tokenizeXmlSpaceSeparatedList(item.properties).includes(`nav`)\n\n/**\n * @see https://www.w3.org/TR/epub-33/#sec-nav-def-model\n */\nconst extractNavChapter = (\n li: XmlElement,\n { basePath }: { basePath: string },\n) => {\n const chp: ArchiveTocItem = {\n contents: [],\n path: ``,\n containerHref: ``,\n title: ``,\n }\n\n let contentNode = li.childNamed(`span`) || li.childNamed(`a`)\n\n chp.title =\n (contentNode?.attr.title ||\n contentNode?.val.trim() ||\n getXmlElementInnerText(contentNode)) ??\n ``\n\n let node = contentNode?.name\n\n if (node !== `a`) {\n contentNode = li.descendantWithPath(`${node}.a`)\n if (contentNode) {\n node = contentNode.name.toLowerCase()\n }\n }\n\n if (node === `a` && contentNode?.attr.href) {\n chp.path = urlJoin(basePath, contentNode.attr.href)\n chp.containerHref = chp.path\n }\n const sublistNode = li.childNamed(`ol`)\n if (sublistNode) {\n const children = sublistNode.childrenNamed(`li`)\n if (children && children.length > 0) {\n chp.contents = children.map((child) =>\n extractNavChapter(child, { basePath }),\n )\n }\n }\n\n return chp\n}\n\nconst buildTocFromNavDocument = (\n doc: XmlDocument,\n { basePath }: { basePath: string },\n) => {\n const toc: ArchiveTocItem[] = []\n\n let navDataChildren: XmlNodeBase[] | undefined\n\n if (doc.descendantWithPath(`body.nav.ol`)) {\n navDataChildren = doc.descendantWithPath(`body.nav.ol`)?.children\n } else if (doc.descendantWithPath(`body.section.nav.ol`)) {\n navDataChildren = doc.descendantWithPath(`body.section.nav.ol`)?.children\n }\n\n if (navDataChildren && navDataChildren.length > 0) {\n navDataChildren\n .filter(\n (li): li is XmlElement => li instanceof XmlElement && li.name === `li`,\n )\n .forEach((li) => {\n toc.push(extractNavChapter(li, { basePath }))\n })\n }\n\n return toc\n}\n\nexport const resolveTocFromNav = async (\n opf: OpfMetadata,\n archive: Archive,\n): Promise<ArchiveTocItem[] | undefined> => {\n const navItem = opf.manifestItems.find(manifestItemIsNavDocument)\n\n if (navItem?.href) {\n const tocFile = archive.records.find((item) =>\n item.uri.endsWith(navItem.href),\n )\n\n if (tocFile && !tocFile.dir) {\n const doc = new XmlDocument(await readRecordAsText(tocFile))\n\n const tocFileBasePath = getUriBasePath(tocFile.uri)\n\n /**\n * links inside toc.xhtml are relative to the toc.xhtml file,\n * not the opf file anymore\n */\n return buildTocFromNavDocument(doc, { basePath: tocFileBasePath })\n }\n }\n\n return undefined\n}\n","import { getUriBasePath, urlJoin } from \"@prose-reader/shared\"\nimport { XmlDocument, type XmlElement } from \"xmldoc\"\nimport { readRecordAsText } from \"../archives/readRecordAsText.ts\"\nimport type { Archive } from \"../archives/types.ts\"\nimport type { OpfMetadata } from \"../metadata/opf/parse.ts\"\nimport type { ArchiveTocItem } from \"./types.ts\"\n\nconst mapNcxChapter = (\n point: XmlElement,\n { basePath, prefix }: { basePath: string; prefix: string },\n) => {\n const src = point?.childNamed(`${prefix}content`)?.attr.src || ``\n const path = urlJoin(basePath, src)\n\n const out: ArchiveTocItem = {\n title:\n point?.descendantWithPath(`${prefix}navLabel.${prefix}text`)?.val || ``,\n path,\n containerHref: path,\n contents: [],\n }\n const children = point.childrenNamed(`${prefix}navPoint`)\n if (children && children.length > 0) {\n out.contents = children.map((pt) => mapNcxChapter(pt, { basePath, prefix }))\n }\n\n return out\n}\n\nconst buildTocFromNcxDocument = (\n ncxData: XmlDocument,\n { basePath }: { basePath: string },\n) => {\n const toc: ArchiveTocItem[] = []\n\n const rootTagName = ncxData.name\n let prefix = ``\n if (rootTagName.indexOf(`:`) !== -1) {\n prefix = `${rootTagName.split(`:`)[0]}:`\n }\n\n ncxData\n .childNamed(`${prefix}navMap`)\n ?.childrenNamed(`${prefix}navPoint`)\n .forEach((point) => {\n toc.push(mapNcxChapter(point, { basePath, prefix }))\n })\n\n return toc\n}\n\nexport const resolveTocFromNcx = async ({\n opf,\n opfBasePath,\n archive,\n}: {\n opf: OpfMetadata\n opfBasePath: string\n archive: Archive\n}): Promise<ArchiveTocItem[] | undefined> => {\n const ncxId = opf.spineTocIdref\n\n if (ncxId) {\n const ncxItem = opf.manifestItems.find((item) => item.id === ncxId)\n\n if (ncxItem) {\n const ncxPath = `${opfBasePath}${opfBasePath === `` ? `` : `/`}${ncxItem.href}`\n\n const file = archive.records.find((item) => item.uri.endsWith(ncxPath))\n\n if (file && !file.dir) {\n const ncxData = new XmlDocument(await readRecordAsText(file))\n\n /**\n * content src inside the ncx are relative to the ncx file,\n * not the opf file\n */\n return buildTocFromNcxDocument(ncxData, {\n basePath: getUriBasePath(file.uri),\n })\n }\n }\n }\n\n return undefined\n}\n","import type { Archive } from \"../archives/types.ts\"\nimport {\n type ArchiveOpfParsed,\n readArchiveOpf,\n} from \"../metadata/opf/readArchiveOpf.ts\"\nimport { buildTocFromFolders } from \"./folders.ts\"\nimport { resolveTocFromNav } from \"./nav.ts\"\nimport { resolveTocFromNcx } from \"./ncx.ts\"\nimport type { ArchiveTocItem } from \"./types.ts\"\n\n/**\n * Resolve the table of contents of an archive into a generic, container\n * relative JSON structure ({@link ArchiveTocItem}). Consumers never have to\n * deal with the underlying XML (EPUB nav document, NCX) or archive layout.\n *\n * Strategies, in order:\n * - EPUB nav document (`properties=\"nav\"` manifest item)\n * - NCX (`spine@toc` idref)\n * - EPUB-like containers with neither resolve to an explicit empty TOC — the\n * folder layout of an EPUB zip is not a meaningful TOC.\n * - Anything else falls back to the folder hierarchy, or `undefined` when the\n * archive is flat (no folder to derive entries from).\n *\n * Pass `opf` when you already hold the parsed OPF (e.g. during manifest\n * generation) to skip the internal lookup.\n */\nexport const resolveArchiveToc = async (\n archive: Archive,\n { opf }: { opf?: ArchiveOpfParsed } = {},\n): Promise<ArchiveTocItem[] | undefined> => {\n const archiveOpf = opf ?? (await readArchiveOpf(archive))\n\n if (archiveOpf) {\n const tocFromNav = await resolveTocFromNav(archiveOpf.opf, archive)\n\n if (tocFromNav) return tocFromNav\n\n const tocFromNcx = await resolveTocFromNcx({\n opf: archiveOpf.opf,\n opfBasePath: archiveOpf.basePath,\n archive,\n })\n\n if (tocFromNcx) return tocFromNcx\n\n return []\n }\n\n const tocFromFolders = buildTocFromFolders(archive)\n\n return tocFromFolders.length === 0 ? undefined : tocFromFolders\n}\n","import type { Archive } from \"./archives/types.ts\"\nimport { resolveArchiveCover } from \"./cover/resolveArchiveCover.ts\"\nimport { readingOrderDocumentsAllHaveViewport } from \"./layout/scanReadingOrderDocumentsViewport.ts\"\nimport type { AppleMetadata } from \"./metadata/apple/parse.ts\"\nimport { readArchiveApple } from \"./metadata/apple/readArchiveApple.ts\"\nimport type { ComicInfo } from \"./metadata/comicInfo/parse.ts\"\nimport { readArchiveComicInfo } from \"./metadata/comicInfo/readArchiveComicInfo.ts\"\nimport type { KoboMetadata } from \"./metadata/kobo/parse.ts\"\nimport { readArchiveKobo } from \"./metadata/kobo/readArchiveKobo.ts\"\nimport { isArchiveEpub } from \"./metadata/opf/isArchiveEpub.ts\"\nimport type { ArchiveOpfParsed } from \"./metadata/opf/readArchiveOpf.ts\"\nimport { readArchiveOpf } from \"./metadata/opf/readArchiveOpf.ts\"\nimport type { ArchiveReadingOrderItem } from \"./readingOrder/resolveArchiveReadingOrder.ts\"\nimport { resolveArchiveReadingOrder } from \"./readingOrder/resolveArchiveReadingOrder.ts\"\nimport { Report } from \"./report.ts\"\nimport { resolveMetadata } from \"./resolveMetadata.ts\"\nimport { resolveArchiveToc } from \"./toc/resolveArchiveToc.ts\"\nimport type { ArchiveTocItem } from \"./toc/types.ts\"\nimport type { ResolvedMetadata } from \"./types/resolvedMetadata.ts\"\nimport { omitUndefined } from \"./utils/omitUndefined.ts\"\n\n/**\n * Verbatim parser outputs, keyed by source — provenance for the normalized\n * `metadata` and the escape hatch for single-format consumers. Everything in\n * here is also represented, normalized, in `metadata` (the mapping tables\n * next to each resolver enforce it); duplication between the two is the\n * contract, not a smell: `metadata` says what the resolver believes, sources\n * say what the book said, so a wrong mapping opinion is revisable in a\n * release without the raw value ever having left the entity.\n */\nexport type ResolvedArchiveSources = {\n readonly opf?: ArchiveOpfParsed\n readonly comicInfo?: ComicInfo\n readonly apple?: AppleMetadata\n readonly kobo?: KoboMetadata\n}\n\nexport type ResolvedArchiveSourceKind = keyof ResolvedArchiveSources\n\n/**\n * The fully resolved, plain-JSON view of a book container: everything a\n * reading or library app needs to present the publication, with no archive\n * handle attached — structured-clone-able, persistable, cacheable.\n *\n * Everything is container-relative (reading order `uri`s, toc\n * `containerHref`s): the entity carries no serving concern, consumers rebase\n * into their own space (the prose streamer does exactly that to produce its\n * `Manifest`).\n */\nexport type ResolvedArchive = {\n /**\n * Schema version of this entity, for consumers persisting it. Bumped only\n * when the shape or meaning of existing fields changes incompatibly;\n * additive growth (new optional fields, vocabulary extensions) does not\n * bump it.\n */\n readonly version: number\n /**\n * Cross-format normalized metadata (see {@link ResolvedMetadata}),\n * including the resolved `cover` — metadata is resolved for the whole\n * container, not just its descriptive sidecars.\n */\n readonly metadata: ResolvedMetadata\n readonly readingOrder: ArchiveReadingOrderItem[]\n /**\n * Absent when no toc is derivable (a flat, package-less container). An\n * EPUB without a nav document or NCX resolves to an explicit empty list —\n * the folder layout of an EPUB zip is not a meaningful toc.\n */\n readonly toc?: ArchiveTocItem[]\n readonly sources: ResolvedArchiveSources\n /**\n * Sources the container carries but that yielded no parsed value: declared\n * yet broken, which `sources` alone cannot express — an absent source and an\n * unreadable one both read as `undefined` there.\n *\n * Always present (empty when every carried source parsed), whatever the\n * projection: a consumer refusing to accept a corrupt publication should not\n * have to ask for `sources`, nor re-derive the container format from the\n * archive records to tell the two cases apart.\n *\n * ```ts\n * const { metadata, unreadableSources } = await resolveArchive(archive)\n *\n * if (unreadableSources.includes(\"opf\")) {\n * throw new Error(\"Archive carries an OPF package document it cannot parse\")\n * }\n * ```\n *\n * A source counts as unreadable when its document (or, for the package\n * document, any `.opf` record in the container) is there and reading or\n * parsing it did not produce a value — the resolve itself never fails, so\n * this is the only trace left. A projection that reads nothing from the book\n * reports nothing.\n */\n readonly unreadableSources: ResolvedArchiveSourceKind[]\n}\n\n/**\n * Projection tokens are simply the keys of the result. Cost classes:\n *\n * - `metadata`, `sources`: sidecar XML reads (OPF, ComicInfo.xml,\n * Apple/Kobo display options) — cheap. `metadata` also derives the cover:\n * free for an OPF cover, and for a package-less container it computes the\n * reading-order listing (an in-memory records scan) for the first-page\n * fallback.\n * - `readingOrder`: the OPF read at most — cheap.\n * - `toc`: one nav or NCX document read+parse on top — medium.\n * - `version`, `unreadableSources`: free, and always present regardless of\n * projection.\n *\n * The default (`metadata`, `readingOrder`, `toc`) costs O(sidecar files) +\n * one navigation document: anything that reads the whole book is opt-in\n * (see `layoutScan`). `sources` is opt-in too — it rides for free on the\n * reads its neighbors already do.\n */\nexport type ResolveArchiveToken = keyof ResolvedArchive\n\nconst DEFAULT_INCLUDE = [\"metadata\", \"readingOrder\", \"toc\"] as const\n\nexport type ResolveArchiveOptions<\n T extends ReadonlyArray<ResolveArchiveToken>,\n> = {\n /**\n * Projection: which parts of {@link ResolvedArchive} to resolve and\n * return. Defaults to `[\"metadata\", \"readingOrder\", \"toc\"]`. `sources` is\n * deliberately not in the default set — it roughly doubles the persisted\n * entity for provenance most consumers don't need; requesting it is free\n * when it rides along tokens that parse the sidecars anyway.\n */\n include?: T\n /**\n * Effort modifier, not a projection token: reads and XML-parses **every**\n * reading-order document (O(book)) to apply the industry viewport\n * heuristic — an explicitly-reflowable publication whose spine documents\n * all declare `head > meta[name=viewport]` is promoted to `pre-paginated`\n * (metadata and per-item). The refinement is merged into the result here,\n * inside the resolver; consumers never re-implement the merge. Defaults\n * to false, honoring the \"nothing reads the whole book by default\" rule.\n */\n layoutScan?: boolean\n}\n\n/**\n * `failed` separates \"the read threw\" from \"the read found nothing\", which\n * `value === undefined` conflates — the distinction `unreadableSources`\n * reports.\n */\ntype SafeRead<T> = { value: T | undefined; failed: boolean }\n\nconst safeRead = async <T>(\n read: () => Promise<T>,\n source: string,\n): Promise<SafeRead<T>> => {\n try {\n return { value: await read(), failed: false }\n } catch (e) {\n // books in the wild are dirty: a malformed source never fails the\n // resolve, it just doesn't contribute\n Report.error(`resolveArchive: unable to read ${source}`, e)\n\n return { value: undefined, failed: true }\n }\n}\n\n/**\n * The counted page count: one page per pre-paginated *page-like* reading-order\n * item — an image or a fixed-layout document (comics, image archives, fixed\n * layout). Reflowable documents are not pre-paginated, so they never count.\n * Audio and video are pre-paginated discrete media too, but a track or clip is\n * not a page, so they are excluded — an audiobook or video archive has no page\n * count. Absent when nothing page-like is present.\n */\nconst countPages = (\n readingOrder: ArchiveReadingOrderItem[],\n): number | undefined => {\n const isAudioOrVideo = (mediaType: string | undefined): boolean =>\n mediaType?.startsWith(\"audio/\") === true ||\n mediaType?.startsWith(\"video/\") === true\n\n const count = readingOrder.filter(\n (item) =>\n item.renditionLayout === \"pre-paginated\" &&\n !isAudioOrVideo(item.mediaType),\n ).length\n\n return count > 0 ? count : undefined\n}\n\n/**\n * A sidecar reader returns `undefined` only when the container does not carry\n * the document at all, so a throw is the whole broken signal. The package\n * document has one more broken shape: a container carrying an `.opf` record\n * whose package document never reaches the parser (a name the OPF discovery\n * does not match, a directory record). `isArchiveEpub` is the carrier test\n * there, which is exactly what consumers had to reach for themselves.\n */\nconst collectUnreadableSources = ({\n archive,\n opf,\n comicInfo,\n apple,\n kobo,\n}: {\n archive: Archive\n opf: SafeRead<ArchiveOpfParsed | undefined> | undefined\n comicInfo: SafeRead<ComicInfo | undefined> | undefined\n apple: SafeRead<AppleMetadata | undefined> | undefined\n kobo: SafeRead<KoboMetadata | undefined> | undefined\n}): ResolvedArchiveSourceKind[] => {\n const unreadable: ResolvedArchiveSourceKind[] = []\n\n if (opf !== undefined && opf.value === undefined) {\n if (opf.failed || isArchiveEpub(archive)) unreadable.push(\"opf\")\n }\n\n if (comicInfo?.failed) unreadable.push(\"comicInfo\")\n if (apple?.failed) unreadable.push(\"apple\")\n if (kobo?.failed) unreadable.push(\"kobo\")\n\n return unreadable\n}\n\nconst RESOLVED_ARCHIVE_VERSION = 3\n\n/**\n * Resolves a book container into a single enriched, plain-JSON entity:\n * normalized cross-format {@link ResolvedMetadata}, the container-relative\n * reading order and toc, and optionally the verbatim per-source outputs.\n *\n * This is the flagship entry point of the package — parsers and per-source\n * readers stay exported as the advanced layer underneath.\n *\n * ```ts\n * const { metadata, readingOrder, toc } = await resolveArchive(archive)\n *\n * // library-shelf scan: metadata (incl. cover) only, skip the toc read\n * const { metadata } = await resolveArchive(archive, { include: [\"metadata\"] })\n * renderShelfItem({ title: mainTitle(metadata), cover: metadata.cover?.uri })\n *\n * // your own per-source precedence via the raw sources escape hatch\n * // (resolveArchiveMetadata normalizes a single source at a time)\n * const { sources } = await resolveArchive(archive, { include: [\"sources\"] })\n * const opf = sources.opf && resolveArchiveMetadata(sources.opf.opf)\n * const comicInfo = sources.comicInfo && resolveArchiveMetadata(sources.comicInfo)\n * const identifiers = comicInfo?.identifiers ?? opf?.identifiers\n *\n * // reject a publication whose package document is there but broken\n * const { unreadableSources } = await resolveArchive(archive)\n * if (unreadableSources.includes(\"opf\")) throw new Error(\"broken OPF\")\n *\n * // full fidelity, including the O(book) layout refinement\n * const resolved = await resolveArchive(archive, {\n * include: [\"metadata\", \"readingOrder\", \"toc\", \"sources\"],\n * layoutScan: true,\n * })\n * ```\n */\nexport const resolveArchive = async <\n T extends ReadonlyArray<ResolveArchiveToken> = typeof DEFAULT_INCLUDE,\n>(\n archive: Archive,\n options: ResolveArchiveOptions<T> = {},\n): Promise<\n Pick<ResolvedArchive, T[number] | \"version\" | \"unreadableSources\">\n> => {\n const tokens = new Set<ResolveArchiveToken>(\n options.include ?? DEFAULT_INCLUDE,\n )\n const wantsMetadata = tokens.has(\"metadata\")\n const wantsSources = tokens.has(\"sources\")\n const wantsReadingOrder = tokens.has(\"readingOrder\")\n const wantsToc = tokens.has(\"toc\")\n\n // the scan refines metadata AND per-item layouts, so it runs whenever\n // either projection could reflect it — and needs both computed internally\n const runLayoutScan =\n options.layoutScan === true && (wantsMetadata || wantsReadingOrder)\n\n // the package document backs every archive-derived token, but a\n // version-only (or empty) projection returns nothing read from the book —\n // skip the read entirely rather than open and parse the OPF for nothing\n // (runLayoutScan implies metadata or readingOrder, so it is covered)\n const wantsOpf =\n wantsMetadata || wantsSources || wantsReadingOrder || wantsToc\n const opfRead = wantsOpf\n ? await safeRead(() => readArchiveOpf(archive), \"opf\")\n : undefined\n const opf = opfRead?.value\n\n const wantsSidecars = wantsMetadata || wantsSources || runLayoutScan\n const comicInfoRead = wantsSidecars\n ? await safeRead(() => readArchiveComicInfo(archive), \"comicInfo\")\n : undefined\n const comicInfo = comicInfoRead?.value\n const appleRead = wantsSidecars\n ? await safeRead(() => readArchiveApple(archive), \"apple\")\n : undefined\n const apple = appleRead?.value\n const koboRead = wantsSidecars\n ? await safeRead(() => readArchiveKobo(archive), \"kobo\")\n : undefined\n const kobo = koboRead?.value\n\n let metadata =\n wantsMetadata || runLayoutScan\n ? resolveMetadata({ opf: opf?.opf, comicInfo, apple, kobo })\n : undefined\n\n let readingOrder =\n wantsReadingOrder || runLayoutScan\n ? await resolveArchiveReadingOrder(archive, { opf })\n : undefined\n\n if (\n runLayoutScan &&\n metadata !== undefined &&\n readingOrder !== undefined &&\n metadata.renditionLayout === \"reflowable\" &&\n readingOrder.every((item) => item.renditionLayout === \"reflowable\") &&\n (await readingOrderDocumentsAllHaveViewport(archive, readingOrder))\n ) {\n metadata = { ...metadata, renditionLayout: \"pre-paginated\" }\n readingOrder = readingOrder.map((item) => ({\n ...item,\n renditionLayout: \"pre-paginated\" as const,\n }))\n }\n\n // cover and the counted page count are part of the resolved metadata but\n // are container-level derivations (basePath join, first-page fallback, page\n // counting), so they are resolved here where the archive is available and\n // folded into `metadata`. After the scan so both ride the finalized reading\n // order (its layout drives the page count, and the scan can promote it).\n if (wantsMetadata && metadata !== undefined) {\n const order =\n readingOrder ?? (await resolveArchiveReadingOrder(archive, { opf }))\n const cover = await resolveArchiveCover(archive, {\n opf,\n readingOrder: order,\n })\n const numberOfPages = metadata.numberOfPages ?? countPages(order)\n\n metadata = omitUndefined({ ...metadata, cover, numberOfPages })\n }\n\n const toc = wantsToc\n ? (await safeRead(() => resolveArchiveToc(archive, { opf }), \"toc\")).value\n : undefined\n\n const result: {\n -readonly [K in keyof ResolvedArchive]?: ResolvedArchive[K]\n } & Pick<ResolvedArchive, \"version\" | \"unreadableSources\"> = {\n version: RESOLVED_ARCHIVE_VERSION,\n unreadableSources: collectUnreadableSources({\n archive,\n opf: opfRead,\n comicInfo: comicInfoRead,\n apple: appleRead,\n kobo: koboRead,\n }),\n }\n\n if (wantsMetadata && metadata !== undefined) result.metadata = metadata\n if (wantsReadingOrder && readingOrder !== undefined)\n result.readingOrder = readingOrder\n if (wantsToc && toc !== undefined) result.toc = toc\n if (wantsSources)\n result.sources = omitUndefined({ opf, comicInfo, apple, kobo })\n\n // `as`: the runtime projection above mirrors the type-level Pick over the\n // same token set; TypeScript cannot connect Set membership checks to the\n // computed Pick keys.\n return result as Pick<\n ResolvedArchive,\n T[number] | \"version\" | \"unreadableSources\"\n >\n}\n","import type {\n MetadataIdentifier,\n MetadataIdentifierScheme,\n} from \"../types/resolvedMetadata.ts\"\n\n/**\n * Schemes whose catalog addresses a record by URL, so a publication can state\n * the same identifier either explicitly or as a reference link.\n */\nexport type MetadataCatalogScheme = Extract<\n MetadataIdentifierScheme,\n \"GoogleBooks\" | \"OpenLibrary\" | \"ProjectGutenberg\" | \"DOI\"\n>\n\n/**\n * Schemes that say nothing about what assigned the value, which is what makes\n * a reference URL worth reading: the publication stated a link, not a\n * namespace. An explicitly typed identifier of another catalog is left alone.\n */\nconst isUntypedReferenceScheme = (\n scheme: MetadataIdentifierScheme,\n): boolean => {\n const normalized = scheme.trim().toLowerCase()\n\n return (\n normalized === \"url\" || normalized === \"uri\" || normalized === \"unknown\"\n )\n}\n\n/**\n * `new URL` accepts a malformed percent escape that `decodeURIComponent`\n * rejects, and these helpers read authored metadata — a book carrying\n * `https://doi.org/10.1000/%` must decline, not throw.\n */\nconst decodePercentEncoding = (value: string): string | undefined => {\n try {\n return decodeURIComponent(value)\n } catch {\n return undefined\n }\n}\n\nconst httpUrl = (value: string): URL | undefined => {\n try {\n const url = new URL(value.trim())\n\n return url.protocol === \"http:\" || url.protocol === \"https:\"\n ? url\n : undefined\n } catch {\n return undefined\n }\n}\n\nconst urlOnHost = (\n value: string,\n matchesHost: (hostname: string) => boolean,\n): URL | undefined => {\n const url = httpUrl(value)\n\n return url !== undefined && matchesHost(url.hostname.toLowerCase())\n ? url\n : undefined\n}\n\nconst GOOGLE_BOOKS_SITE_HOST =\n /^books\\.google\\.(?:com|[a-z]{2}|com\\.[a-z]{2}|co\\.[a-z]{2})$/i\nconst GOOGLE_BOOKS_API_HOSTS = new Set([\n \"books.googleapis.com\",\n \"www.googleapis.com\",\n])\nconst GOOGLE_BOOKS_API_PATH = /^\\/books\\/v1\\/volumes\\/([^/]+)\\/?$/i\n\nconst googleBooksId = (value: string): string | undefined => {\n const trimmed = value.trim()\n\n return /^[a-z0-9_-]+$/i.test(trimmed) ? trimmed : undefined\n}\n\nconst googleBooksIdFromUrl = (value: string): string | undefined => {\n const url = urlOnHost(\n value,\n (hostname) =>\n GOOGLE_BOOKS_SITE_HOST.test(hostname) ||\n GOOGLE_BOOKS_API_HOSTS.has(hostname),\n )\n\n if (url === undefined) return undefined\n\n if (GOOGLE_BOOKS_API_HOSTS.has(url.hostname.toLowerCase())) {\n const id = GOOGLE_BOOKS_API_PATH.exec(url.pathname)?.[1]\n const decoded = id === undefined ? undefined : decodePercentEncoding(id)\n\n return decoded === undefined ? undefined : googleBooksId(decoded)\n }\n\n return googleBooksId(url.searchParams.get(\"id\") ?? \"\")\n}\n\nconst GUTENBERG_HOSTS = new Set([\"gutenberg.org\", \"www.gutenberg.org\"])\nconst GUTENBERG_PATHS: ReadonlyArray<RegExp> = [\n /^\\/ebooks\\/(\\d+)(?:[./]|$)/i,\n /^\\/files\\/(\\d+)(?:\\/|$)/i,\n /^\\/cache\\/epub\\/(\\d+)(?:\\/|$)/i,\n /^\\/(\\d+)(?:\\/|$)/,\n]\n\nconst projectGutenbergId = (value: string): string | undefined => {\n const trimmed = value.trim()\n\n if (!/^\\d+$/.test(trimmed)) return undefined\n\n const withoutLeadingZeros = trimmed.replace(/^0+(?=\\d)/, \"\")\n\n return withoutLeadingZeros === \"0\" ? undefined : withoutLeadingZeros\n}\n\nconst projectGutenbergIdFromUrl = (value: string): string | undefined => {\n const url = urlOnHost(value, (hostname) => GUTENBERG_HOSTS.has(hostname))\n\n if (url === undefined) return undefined\n\n for (const pattern of GUTENBERG_PATHS) {\n const id = pattern.exec(url.pathname)?.[1]\n\n if (id !== undefined) return projectGutenbergId(id)\n }\n\n return undefined\n}\n\nconst OPEN_LIBRARY_HOSTS = new Set([\"openlibrary.org\", \"www.openlibrary.org\"])\nconst OPEN_LIBRARY_KEY = /^\\/(works|books|authors)\\/(OL\\d+[A-Z])(?:\\/|$)/i\nconst OPEN_LIBRARY_BARE_ID = /^OL\\d+([MWA])$/i\n\nconst OPEN_LIBRARY_COLLECTIONS: Record<string, string> = {\n M: \"books\",\n W: \"works\",\n A: \"authors\",\n}\n\n/**\n * Open Library addresses a record by its key path, which is the value this\n * scheme carries. A bare id is accepted too, since its suffix letter says\n * which collection it belongs to.\n */\nconst openLibraryKey = (value: string): string | undefined => {\n const trimmed = value.trim()\n const key = OPEN_LIBRARY_KEY.exec(trimmed)\n\n if (key?.[1] !== undefined && key[2] !== undefined) {\n return `/${key[1].toLowerCase()}/${key[2].toUpperCase()}`\n }\n\n const bare = OPEN_LIBRARY_BARE_ID.exec(trimmed)\n const collection = OPEN_LIBRARY_COLLECTIONS[bare?.[1]?.toUpperCase() ?? \"\"]\n\n return collection === undefined\n ? undefined\n : `/${collection}/${trimmed.toUpperCase()}`\n}\n\nconst openLibraryKeyFromUrl = (value: string): string | undefined => {\n const url = urlOnHost(value, (hostname) => OPEN_LIBRARY_HOSTS.has(hostname))\n\n return url === undefined ? undefined : openLibraryKey(url.pathname)\n}\n\nconst DOI_HOSTS = new Set([\"doi.org\", \"dx.doi.org\", \"www.doi.org\"])\nconst DOI_NAME = /^10\\.\\d{4,9}\\/\\S+$/\n\nconst doiName = (value: string): string | undefined => {\n const trimmed = value.trim().replace(/^doi:/i, \"\")\n\n return DOI_NAME.test(trimmed) ? trimmed : undefined\n}\n\n/**\n * DOI suffixes are opaque and may contain characters a URL reserves — a `?`\n * would otherwise start a query string and drop the rest of the name. Each\n * path segment is encoded while the separators stay literal, since doi.org\n * addresses a name by path rather than as one encoded blob.\n */\nconst doiUrlPath = (name: string): string =>\n name.split(\"/\").map(encodeURIComponent).join(\"/\")\n\nconst doiNameFromUrl = (value: string): string | undefined => {\n const url = urlOnHost(value, (hostname) => DOI_HOSTS.has(hostname))\n const decoded =\n url === undefined\n ? undefined\n : decodePercentEncoding(url.pathname.replace(/^\\//, \"\"))\n\n return decoded === undefined ? undefined : doiName(decoded)\n}\n\ntype Catalog = {\n readonly scheme: MetadataCatalogScheme\n readonly normalizeValue: (value: string) => string | undefined\n readonly valueFromUrl: (value: string) => string | undefined\n /** Builds the catalog's own URL for an already-normalized value. */\n readonly toUrl: (value: string) => string\n}\n\nconst CATALOGS: ReadonlyArray<Catalog> = [\n {\n scheme: \"GoogleBooks\",\n normalizeValue: googleBooksId,\n valueFromUrl: googleBooksIdFromUrl,\n toUrl: (value) =>\n `https://books.google.com/books?id=${encodeURIComponent(value)}`,\n },\n {\n scheme: \"ProjectGutenberg\",\n normalizeValue: projectGutenbergId,\n valueFromUrl: projectGutenbergIdFromUrl,\n toUrl: (value) => `https://www.gutenberg.org/ebooks/${value}`,\n },\n {\n scheme: \"OpenLibrary\",\n normalizeValue: openLibraryKey,\n valueFromUrl: openLibraryKeyFromUrl,\n toUrl: (value) => `https://openlibrary.org${value}`,\n },\n {\n scheme: \"DOI\",\n normalizeValue: doiName,\n valueFromUrl: doiNameFromUrl,\n toUrl: (value) => `https://doi.org/${doiUrlPath(value)}`,\n },\n]\n\n/**\n * Every scheme whose catalog addresses a record by URL, so a publication can\n * state the identifier either explicitly or as a reference link.\n */\nexport const METADATA_CATALOG_SCHEMES: ReadonlyArray<MetadataCatalogScheme> =\n CATALOGS.map(function toScheme(catalog) {\n return catalog.scheme\n })\n\nconst catalogFor = (scheme: MetadataIdentifierScheme): Catalog | undefined => {\n const schemeKey = scheme.trim().toLowerCase()\n\n return CATALOGS.find(function matchesScheme(catalog) {\n return catalog.scheme.toLowerCase() === schemeKey\n })\n}\n\n/** Whether a scheme's catalog addresses its records by URL. */\nexport const isMetadataCatalogScheme = (\n scheme: MetadataIdentifierScheme,\n): boolean => catalogFor(scheme) !== undefined\n\n/**\n * The catalog identifier a reference URL stands for, or `undefined` when no\n * known catalog addresses that URL.\n *\n * This derives — it does not reinterpret. Resolved metadata keeps the authored\n * URL as a `URL` identifier whatever this returns, so a publication that\n * stated a link still reports a link; deciding to treat that link as the\n * publication's catalog identifier is the caller's call.\n */\nexport const catalogIdentifierFromUrl = (\n url: string,\n): MetadataIdentifier | undefined => {\n for (const catalog of CATALOGS) {\n const value = catalog.valueFromUrl(url)\n\n if (value !== undefined) return { value, scheme: catalog.scheme }\n }\n\n return undefined\n}\n\n/**\n * The catalog's own URL for an identifier — the inverse of\n * {@link catalogIdentifierFromUrl}, for writing a reference into a container\n * whose only identifier slot is a list of links.\n *\n * `undefined` when the scheme has no catalog, or when the value is not one\n * that catalog can address. The pair is checked rather than assumed: a URL is\n * only returned once reading it back yields the value it was built from, so\n * the two directions cannot drift apart unnoticed.\n */\nexport const catalogUrlFromIdentifier = ({\n scheme,\n value,\n}: MetadataIdentifier): string | undefined => {\n const catalog = catalogFor(scheme)\n const normalized = catalog?.normalizeValue(value)\n\n if (catalog === undefined || normalized === undefined) return undefined\n\n const url = catalog.toUrl(normalized)\n\n return catalog.valueFromUrl(url) === normalized ? url : undefined\n}\n\n/**\n * How one catalog recognizes its own identifiers, for the derivation table in\n * `identifierValues.ts` to read.\n */\nexport const catalogRule = (\n scheme: MetadataCatalogScheme,\n): Pick<Catalog, \"normalizeValue\" | \"valueFromUrl\"> => {\n const catalog = CATALOGS.find(function matchesScheme(candidate) {\n return candidate.scheme === scheme\n })\n\n if (catalog === undefined) {\n throw new Error(`No catalog rule for scheme ${scheme}`)\n }\n\n return catalog\n}\n\nexport { isUntypedReferenceScheme }\n","import type {\n MetadataIdentifier,\n MetadataIdentifierScheme,\n} from \"../types/resolvedMetadata.ts\"\nimport { booklandIsbn } from \"./booklandIsbn.ts\"\nimport {\n catalogRule,\n isUntypedReferenceScheme,\n type MetadataCatalogScheme,\n} from \"./catalogIdentifiers.ts\"\nimport { normalizeGtin } from \"./normalizeGtin.ts\"\nimport { normalizeIdentifierScheme } from \"./normalizeIdentifierScheme.ts\"\n\n/**\n * Whether a scheme can carry an ISBN or a GTIN. The two are one namespace in\n * practice: an ISBN-13 *is* a GTIN-13 in the Bookland range, and ComicInfo has\n * no ISBN field at all — it announces one through `GTIN`, which stays labelled\n * `GTIN` because that is what the source said. Filtering on `ISBN` alone\n * therefore misses every comic ISBN, and filtering on `GTIN` alone misses\n * every EPUB one.\n *\n * Resolving metadata canonicalizes spellings, but this also answers for a\n * scheme a caller assembled by hand.\n */\nexport const isIsbnBearingScheme = (\n scheme: MetadataIdentifierScheme,\n): boolean => {\n const normalized = normalizeIdentifierScheme(scheme)\n\n return normalized === \"ISBN\" || normalized === \"GTIN\"\n}\n\n/**\n * Schemes a publication's value can be derived for. Narrower than\n * {@link MetadataIdentifierScheme}: a scheme is here only when knowing it lets\n * us both recognize the identifiers that carry it and canonicalize what they\n * authored. `URL` and `Unknown` describe how a value was stated rather than\n * what assigned it, so there is nothing to derive for them.\n */\nexport type DerivableIdentifierScheme =\n | Extract<MetadataIdentifierScheme, \"ISBN\" | \"GTIN\">\n | MetadataCatalogScheme\n\ntype DerivationRule = {\n /** Whether an identifier of this scheme can carry the derived value. */\n readonly carries: (scheme: MetadataIdentifierScheme) => boolean\n readonly normalizeValue: (value: string) => string | undefined\n /** Reads the value out of an untyped reference URL, for catalogs that have one. */\n readonly valueFromUrl?: (value: string) => string | undefined\n}\n\nconst isbnBearingRule = (\n normalizeValue: (value: string) => string | undefined,\n): DerivationRule => ({ carries: isIsbnBearingScheme, normalizeValue })\n\nconst catalogDerivationRule = (\n scheme: MetadataCatalogScheme,\n): DerivationRule => ({\n carries: (candidate) => normalizeIdentifierScheme(candidate) === scheme,\n ...catalogRule(scheme),\n})\n\nconst DERIVATION_RULES: Record<DerivableIdentifierScheme, DerivationRule> = {\n ISBN: isbnBearingRule(booklandIsbn),\n GTIN: isbnBearingRule(normalizeGtin),\n GoogleBooks: catalogDerivationRule(\"GoogleBooks\"),\n OpenLibrary: catalogDerivationRule(\"OpenLibrary\"),\n ProjectGutenberg: catalogDerivationRule(\"ProjectGutenberg\"),\n DOI: catalogDerivationRule(\"DOI\"),\n}\n\n/**\n * The publication's value for one scheme, canonicalized — the first identifier\n * that can carry it and whose value really is one. Use this rather than\n * filtering `identifiers` yourself: which identifiers can answer for a scheme\n * is rarely just the ones labelled with it.\n *\n * - `ISBN` and `GTIN` read each other, because they are one namespace: an\n * ISBN-13 is a GTIN-13 in the Bookland range, and a comic announces its\n * ISBN through ComicInfo's `GTIN` field. A value the scheme claims is an\n * ISBN but which is some other barcode — a retail EAN scanned off a cover —\n * is not reported as one.\n * - A catalog scheme also reads an untyped reference URL it addresses, since a\n * comic can only state one as a `Web` link. An identifier another catalog\n * already claims is left alone.\n *\n * A derivation that declines to answer loses nothing: the identifier itself\n * stays in `identifiers` exactly as authored.\n */\nexport const identifierValue = (\n identifiers: ReadonlyArray<MetadataIdentifier> | undefined,\n scheme: DerivableIdentifierScheme,\n): string | undefined => {\n const rule = DERIVATION_RULES[scheme]\n\n for (const identifier of identifiers ?? []) {\n if (rule.carries(identifier.scheme)) {\n /**\n * A publication that states the scheme may still write the value as the\n * catalog's own resolver URL, so both spellings answer once the scheme\n * says which catalog assigned it.\n */\n const value =\n rule.normalizeValue(identifier.value) ??\n rule.valueFromUrl?.(identifier.value)\n\n if (value !== undefined) return value\n\n continue\n }\n\n /**\n * Untyped identifiers only answer through a URL: a bare opaque string is\n * never reinterpreted as some catalog's id just because it could be one.\n */\n if (rule.valueFromUrl === undefined) continue\n if (!isUntypedReferenceScheme(identifier.scheme)) continue\n\n const value = rule.valueFromUrl(identifier.value)\n\n if (value !== undefined) return value\n }\n\n return undefined\n}\n","import type { ResolvedMetadata } from \"../types/resolvedMetadata.ts\"\n\n/**\n * The publication's main title: the first title the source states, in\n * document order.\n *\n * EPUB 3 requires reading systems to present the first `dc:title` as the\n * title of the publication, so a `title-type` of `main` on a later element\n * does not displace it — searching the list for that type is the mistake this\n * helper exists to prevent.\n */\nexport const mainTitle = (\n metadata: Pick<ResolvedMetadata, \"titles\"> | undefined,\n): string | undefined => metadata?.titles?.[0]?.value\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/archives/createArchiveFromArrayBufferList.ts","../../src/archives/createArchiveFromText.ts","../../src/archives/createArchiveFromUrls.ts","../../src/archives/types.ts","../../src/archives/images.ts","../../src/archives/readRecordAsText.ts","../../src/metadata/opf/getArchiveOpfInfo.ts","../../src/utils/tokenizeXmlSpaceSeparatedList.ts","../../src/metadata/opf/opfNamespace.ts","../../src/metadata/opf/identifierScheme.ts","../../src/metadata/opf/spineItemrefProperties.ts","../../src/metadata/opf/parse.ts","../../src/metadata/opf/readArchiveOpf.ts","../../src/metadata/apple/parse.ts","../../src/metadata/comicInfo/parse.ts","../../src/metadata/kobo/parse.ts","../../src/utils/omitUndefined.ts","../../src/utils/toContainerUri.ts","../../src/readingOrder/resolveArchiveReadingOrder.ts","../../src/cover/resolveArchiveCover.ts","../../src/layout/scanReadingOrderDocumentsViewport.ts","../../src/metadata/apple/readArchiveApple.ts","../../src/metadata/comicInfo/getArchiveHasComicInfo.ts","../../src/metadata/comicInfo/manga.ts","../../src/metadata/comicInfo/readArchiveComicInfo.ts","../../src/metadata/kobo/readArchiveKobo.ts","../../src/metadata/opf/getSpineItemFilesFromArchive.ts","../../src/metadata/opf/isArchiveEpub.ts","../../src/metadata/apple/resolve.ts","../../src/metadata/comicInfo/resolve.ts","../../src/metadata/kobo/resolve.ts","../../src/utils/normalizeIsbn.ts","../../src/utils/booklandIsbn.ts","../../src/utils/normalizeGtin.ts","../../src/utils/inferIdentifierScheme.ts","../../src/utils/normalizeIdentifierScheme.ts","../../src/utils/parseW3cDtfDate.ts","../../src/metadata/opf/resolve.ts","../../src/resolve.ts","../../src/resolveMetadata.ts","../../src/toc/folders.ts","../../src/utils/getXmlElementInnerText.ts","../../src/toc/nav.ts","../../src/toc/ncx.ts","../../src/toc/resolveArchiveToc.ts","../../src/resolveArchive.ts","../../src/utils/catalogIdentifiers.ts","../../src/utils/identifierValues.ts","../../src/utils/mainTitle.ts"],"sourcesContent":["import { detectMimeTypeFromName } from \"@prose-reader/shared\"\nimport { createArchiveFromEntries } from \"./createArchiveFromEntries.ts\"\nimport { arrayBufferFileAccessors } from \"./fileAccessors.ts\"\nimport type { Archive } from \"./types.ts\"\n\nexport const createArchiveFromArrayBufferList = async (\n list: {\n isDir: boolean\n name: string\n size: number\n data: () => Promise<ArrayBuffer>\n }[],\n options: {\n orderByAlpha?: boolean\n name?: string\n encodingFormat?: string\n } = {},\n): Promise<Archive> =>\n createArchiveFromEntries(\n list,\n (file) =>\n file.isDir\n ? { dir: true, uri: file.name }\n : {\n dir: false,\n uri: file.name,\n size: file.size,\n ...arrayBufferFileAccessors(\n file.data,\n detectMimeTypeFromName(file.name) ?? ``,\n ),\n },\n { ...options, close: () => Promise.resolve() },\n )\n","import { getUriBasename } from \"@prose-reader/shared\"\nimport { createArchive } from \"./createArchive.ts\"\nimport { blobFileAccessors } from \"./fileAccessors.ts\"\n\n/**\n * Useful to create archive from txt content\n */\nexport const createArchiveFromText = async (\n content: string | Blob,\n {\n mimeType,\n direction,\n }: {\n direction?: `ltr` | `rtl`\n mimeType?: string\n } = { mimeType: \"text/plain\" },\n) => {\n const txtOpfContent = `\n <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n <package xmlns=\"http://www.idpf.org/2007/opf\" version=\"3.0\" xml:lang=\"ja\" prefix=\"rendition: http://www.idpf.org/vocab/rendition/#\"\n unique-identifier=\"ootuya-id\">\n <metadata xmlns:opf=\"http://www.idpf.org/2007/opf\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n xmlns:dcterms=\"http://purl.org/dc/terms/\">\n <meta property=\"rendition:layout\">reflowable</meta>\n </metadata>\n <manifest>\n <item id=\"p01\" href=\"p01.txt\" media-type=\"text/plain\"/>\n </manifest>\n <spine page-progression-direction=\"${direction ?? `ltr`}\">\n <itemref idref=\"p01\" />\n </spine>\n </package>\n `\n\n const archive = createArchive({\n filename: `content.txt`,\n encodingFormat: mimeType,\n records: [\n {\n dir: false,\n basename: getUriBasename(`generated.opf`),\n uri: `generated.opf`,\n size: 0,\n ...blobFileAccessors(async () => new Blob([txtOpfContent])),\n },\n {\n dir: false,\n basename: getUriBasename(`p01.txt`),\n uri: `p01.txt`,\n size:\n typeof content === \"string\" ? new Blob([content]).size : content.size,\n encodingFormat: mimeType,\n ...blobFileAccessors(async () =>\n typeof content === \"string\" ? new Blob([content]) : content,\n ),\n },\n ],\n close: () => Promise.resolve(),\n })\n\n return archive\n}\n","import {\n createXmlSafeIdFactory,\n detectMimeTypeFromName,\n escapeXmlAttributeValue,\n getUriBasename,\n} from \"@prose-reader/shared\"\nimport { createArchive } from \"./createArchive.ts\"\nimport { blobFileAccessors } from \"./fileAccessors.ts\"\nimport type { Archive } from \"./types.ts\"\n\n/**\n * @important\n * Make sure the urls are on the same origin or the cors header is set otherwise\n * the resource cannot be consumed as it is on the web.\n */\nexport const createArchiveFromUrls = async (\n urls: string[],\n options?: { useRenditionFlow: boolean },\n): Promise<Archive> => {\n const createSafeId = createXmlSafeIdFactory()\n const resources = urls.map((url) => ({\n id: createSafeId(url),\n url,\n }))\n const opfFileData = `\n <?xml version=\"1.0\" encoding=\"UTF-8\"?><package xmlns=\"http://www.idpf.org/2007/opf\" version=\"2.0\" unique-identifier=\"bookid\">\n <metadata>\n <meta property=\"rendition:layout\">${options?.useRenditionFlow ? `reflowable` : `pre-paginated`}</meta>\n ${options?.useRenditionFlow ? `<meta property=\"rendition:flow\">scrolled-continuous</meta>` : ``}\n </metadata>\n <manifest>\n ${resources\n .map(({ id, url }) => {\n const mediaType = detectMimeTypeFromName(url)\n\n return `<item id=\"${id}\" href=\"${escapeXmlAttributeValue(url)}\" media-type=\"${escapeXmlAttributeValue(mediaType ?? ``)}\"/>`\n })\n .join(`\\n`)}\n </manifest>\n <spine>\n ${resources.map(({ id }) => `<itemref idref=\"${id}\" />`).join(`\\n`)}\n </spine>\n </package>\n `\n\n const filesFromUrl: Archive[`records`] = urls.map((url) => ({\n dir: false,\n basename: getUriBasename(url),\n encodingFormat: detectMimeTypeFromName(url),\n uri: url,\n size: 0,\n ...blobFileAccessors(async () => {\n const response = await fetch(url)\n\n return response.blob()\n }),\n }))\n\n const opfFile: Archive[`records`][number] = {\n dir: false,\n basename: `content.opf`,\n uri: `content.opf`,\n size: 0,\n ...blobFileAccessors(async () => new Blob([opfFileData])),\n }\n\n return createArchive({\n records: [opfFile, ...filesFromUrl],\n close: () => Promise.resolve(),\n })\n}\n","export type FileRecord = {\n dir: false\n basename: string\n uri: string\n /** Uncompressed byte length of the record, or `0` when unknown. */\n size: number\n // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types\n encodingFormat?: string\n blob: () => Promise<Blob>\n arrayBuffer: () => Promise<ArrayBuffer>\n}\n\nexport type DirectoryRecord = {\n dir: true\n basename: string\n uri: string\n}\n\nexport type ArchiveRecord = FileRecord | DirectoryRecord\n\nexport type Archive = {\n /**\n * Container-level filename when known (e.g. the originating `.cbz`/`.epub`\n * file name). Absent for synthetic archives that have no real source file\n * (URL lists, raw array buffers). Consumers use it as a detection signal, so\n * it must never be fabricated.\n */\n filename?: string\n /**\n * Archive-level media type when known (e.g. `application/vnd.comicbook+zip`\n * for a CBZ). This is the mime type of the container itself, not of any\n * individual record; record-level mime types live on {@link FileRecord}.\n */\n encodingFormat?: string\n records: ArchiveRecord[]\n /**\n * `uri` → record index built once at construction (see `createArchive`).\n * Resource resolution is a per-fetched-file hot path, so prefer this (or\n * {@link getArchiveFileRecordByUri}) over scanning {@link Archive.records}.\n */\n recordsByUri: ReadonlyMap<string, ArchiveRecord>\n close: () => Promise<void>\n}\n\nexport const isFileRecord = (record: ArchiveRecord): record is FileRecord =>\n !record.dir\n\nexport const isDirectoryRecord = (\n record: ArchiveRecord,\n): record is DirectoryRecord => record.dir\n\nexport const getArchiveFileRecordByUri = (\n archive: Pick<Archive, \"recordsByUri\">,\n uri: string,\n): FileRecord | undefined => {\n const record = archive.recordsByUri.get(uri)\n\n return record && isFileRecord(record) ? record : undefined\n}\n","import { detectMimeTypeFromName, parseContentType } from \"@prose-reader/shared\"\nimport type { Archive, ArchiveRecord, FileRecord } from \"./types.ts\"\nimport { isFileRecord } from \"./types.ts\"\n\n/**\n * Best-effort media type of a file record: the archive's own encoding format\n * when known, else detected from the filename. Same derivation the reading\n * order and cover use.\n */\nconst mediaTypeForRecord = (record: FileRecord): string | undefined =>\n parseContentType(record.encodingFormat ?? \"\") ||\n detectMimeTypeFromName(record.basename)\n\n/**\n * Whether a record is an image resource — an `image/*` media type by encoding\n * format or filename (the image formats real CBZ/CBR/EPUB producers use).\n * Directories are never images.\n */\nexport const isImageRecord = (record: ArchiveRecord): boolean =>\n isFileRecord(record) &&\n mediaTypeForRecord(record)?.startsWith(\"image/\") === true\n\n/**\n * Every image file record of the archive, in record order. The page listing\n * of a comic or image container, and the raster asset inventory of any book.\n */\nexport const getArchiveImageRecords = (archive: Archive): FileRecord[] =>\n archive.records.filter(isFileRecord).filter(isImageRecord)\n","import type { FileRecord } from \"./types.ts\"\n\n/**\n * Decodes a record's bytes as UTF-8. The caller asserts the record is text;\n * there is intentionally no `string()` accessor on {@link FileRecord} so that\n * decoding a binary record is always a deliberate act at the call site.\n */\nexport const readRecordAsText = async (record: FileRecord): Promise<string> =>\n new TextDecoder().decode(await record.arrayBuffer())\n","import type { Archive } from \"../../archives/types.ts\"\n\nexport const getArchiveOpfInfo = (archive: Archive) => {\n const filesAsArray = archive.records.filter((file) => !file.dir)\n const file = filesAsArray.find((file) => file.uri.endsWith(`.opf`))\n\n return {\n data: file,\n basePath: file?.uri.substring(0, file.uri.lastIndexOf(`/`)) || ``,\n }\n}\n","/**\n * EPUB/XML space-separated token lists (e.g. `properties` on `item` / `itemref`).\n * Trims the raw string, splits on ASCII whitespace, drops empty segments.\n */\nexport const tokenizeXmlSpaceSeparatedList = (\n raw: string | undefined,\n): readonly string[] => {\n if (raw === undefined || raw.trim().length === 0) {\n return []\n }\n\n return raw\n .trim()\n .split(/\\s+/)\n .filter((t) => t.length > 0)\n}\n","/** The OPF namespace every package document's own vocabulary lives in. */\nexport const OPF_NAMESPACE = \"http://www.idpf.org/2007/opf\"\n\n/** The conventional prefix, accepted even when a package never declares it. */\nconst CONVENTIONAL_OPF_PREFIX = \"opf\"\n\nconst XMLNS_PREFIX = \"xmlns:\"\n\n/**\n * The prefix bindings in force at one element: every `xmlns:*` declaration on\n * it and on its ancestors, nearest winning. XML scopes bindings per element, so\n * a descendant can introduce a prefix its ancestors never had and rebind one\n * they did.\n */\nexport type XmlNamespaceScope = Readonly<Record<string, string>>\n\n/**\n * The scope at an element, given the one its parent was read under. Call it\n * while descending so each element is read under its own bindings.\n */\nexport const xmlNamespaceScope = (\n attributes: Readonly<Record<string, string | undefined>>,\n inherited: XmlNamespaceScope = {},\n): XmlNamespaceScope => {\n let scope: Record<string, string> | undefined\n\n for (const [name, value] of Object.entries(attributes)) {\n if (!name.startsWith(XMLNS_PREFIX)) continue\n\n const prefix = name.slice(XMLNS_PREFIX.length)\n const namespace = value?.trim()\n\n if (prefix.length === 0 || namespace === undefined) continue\n\n scope ??= { ...inherited }\n scope[prefix] = namespace\n }\n\n return scope ?? inherited\n}\n\n/**\n * The prefixes naming the OPF namespace in a scope. The conventional `opf` is\n * included unless the document bound it to something else, since using it\n * undeclared is invalid but common — a rebinding is explicit and is honoured.\n */\nexport const opfNamespacePrefixes = (\n scope: XmlNamespaceScope,\n): ReadonlyArray<string> => {\n const prefixes = Object.keys(scope).filter(function namesOpf(prefix) {\n return scope[prefix] === OPF_NAMESPACE\n })\n\n return scope[CONVENTIONAL_OPF_PREFIX] === undefined\n ? [CONVENTIONAL_OPF_PREFIX, ...prefixes]\n : prefixes\n}\n\n/**\n * An attribute in the OPF namespace, tried under every prefix naming it before\n * the unprefixed spelling EPUB 2 documents use. An unprefixed attribute is in\n * no namespace at all, so that is a deliberate fallback rather than an\n * equivalent name.\n */\nexport const opfNamespacedAttribute = (\n attributes: Readonly<Record<string, string | undefined>>,\n prefixes: ReadonlyArray<string>,\n localNames: ReadonlyArray<string>,\n): string | undefined => {\n for (const localName of localNames) {\n for (const prefix of prefixes) {\n const value = attributes[`${prefix}:${localName}`]\n\n if (value !== undefined) return value\n }\n }\n\n for (const localName of localNames) {\n const value = attributes[localName]\n\n if (value !== undefined) return value\n }\n\n return undefined\n}\n","import { opfNamespacedAttribute } from \"./opfNamespace.ts\"\n\n/**\n * How an OPF states a `dc:identifier`'s scheme. Both halves are facts about\n * what a package document can say, so the parser and the resolver read them\n * from here rather than each spelling them out.\n */\n\n/**\n * Local names an identifier's scheme can be stated under, in the order a read\n * prefers them. `Scheme` is not a second name the spec defines — it is the\n * capitalization some producers emit, read because the value it carries is\n * unambiguous.\n */\nexport const OPF_IDENTIFIER_SCHEME_LOCAL_NAMES: ReadonlyArray<string> =\n Object.freeze([\"scheme\", \"Scheme\"])\n\n/**\n * Attribute names an identifier's scheme can be stated on under the\n * conventional `opf` prefix, in the order a read prefers them, ending with the\n * bare EPUB 2 form.\n *\n * Prefer {@link opfIdentifierSchemeAttribute} with the document's own prefixes:\n * a package may bind the OPF namespace to another prefix, which these literal\n * names do not cover.\n */\nexport const OPF_IDENTIFIER_SCHEME_ATTRIBUTES: ReadonlyArray<string> =\n Object.freeze([\"opf:scheme\", \"opf:Scheme\", \"scheme\"])\n\n/**\n * ONIX codelist 5 codes, which is the list an `identifier-type` refinement\n * states its value against when it names `onix:codelist5` as its scheme.\n */\nconst ONIX_CODE_LIST_5_IDENTIFIER_TYPES: Readonly<Record<string, string>> = {\n \"02\": \"ISBN\",\n \"03\": \"GTIN\",\n \"04\": \"UPC\",\n \"05\": \"ISMN\",\n \"06\": \"DOI\",\n \"13\": \"LCCN\",\n \"14\": \"GTIN\",\n \"15\": \"ISBN\",\n \"22\": \"URN\",\n \"23\": \"OCLC\",\n \"24\": \"ISBN\",\n \"25\": \"ISMN\",\n \"26\": \"DOI\",\n \"34\": \"GTIN\",\n \"35\": \"ARK\",\n}\n\n/** The `<meta>` fields an identifier's type is read from. */\nexport type OpfIdentifierTypeMeta = {\n readonly value?: string\n readonly scheme?: string\n}\n\n/**\n * The scheme an EPUB 3 `identifier-type` refinement names. A value stated\n * against `onix:codelist5` is a code from that list — `15` is an ISBN — and is\n * translated; a code the list does not define, and any value stated against\n * another scheme or none, is already a scheme name and passes through\n * verbatim.\n */\nexport const opfIdentifierTypeScheme = (\n meta: OpfIdentifierTypeMeta | undefined,\n): string | undefined => {\n const value = meta?.value?.trim()\n\n if (value === undefined || value.length === 0) return undefined\n if (meta?.scheme?.trim().toLowerCase() !== \"onix:codelist5\") return value\n\n return ONIX_CODE_LIST_5_IDENTIFIER_TYPES[value] ?? value\n}\n\n/**\n * The scheme attribute an element carries, if any. `prefixes` are the ones the\n * document binds to the OPF namespace — `opfNamespacePrefixes` reads them off\n * the package element — and default to the conventional `opf` alone.\n */\nexport const opfIdentifierSchemeAttribute = (\n attributes: Readonly<Record<string, string | undefined>>,\n prefixes: ReadonlyArray<string> = [\"opf\"],\n): string | undefined =>\n opfNamespacedAttribute(\n attributes,\n prefixes,\n OPF_IDENTIFIER_SCHEME_LOCAL_NAMES,\n )\n","import { tokenizeXmlSpaceSeparatedList } from \"../../utils/tokenizeXmlSpaceSeparatedList.ts\"\n\nexport type OpfItemrefLayoutHints = {\n readonly renditionLayout?: `reflowable` | `pre-paginated`\n readonly renditionFlow?:\n | `scrolled-continuous`\n | `scrolled-doc`\n | `paginated`\n | `auto`\n readonly pageSpreadLeft?: true\n readonly pageSpreadRight?: true\n}\n\n/**\n * EPUB `itemref` `properties` attribute (space-separated tokens).\n *\n * @see https://www.w3.org/TR/epub/#attrdef-properties\n */\nexport const layoutHintsFromItemrefProperties = (\n properties: string | undefined,\n): OpfItemrefLayoutHints => {\n const tokens = tokenizeXmlSpaceSeparatedList(properties)\n if (tokens.length === 0) {\n return {}\n }\n\n let renditionLayout: `reflowable` | `pre-paginated` | undefined\n if (tokens.includes(`rendition:layout-reflowable`)) {\n renditionLayout = `reflowable`\n }\n if (tokens.includes(`rendition:layout-pre-paginated`)) {\n renditionLayout = `pre-paginated`\n }\n\n let renditionFlow: OpfItemrefLayoutHints[\"renditionFlow\"]\n if (tokens.includes(`rendition:flow-auto`)) {\n renditionFlow = `auto`\n }\n if (tokens.includes(`rendition:flow-paginated`)) {\n renditionFlow = `paginated`\n }\n if (tokens.includes(`rendition:flow-scrolled-doc`)) {\n renditionFlow = `scrolled-doc`\n }\n if (tokens.includes(`rendition:flow-scrolled-continuous`)) {\n renditionFlow = `scrolled-continuous`\n }\n\n return {\n ...(renditionLayout !== undefined ? { renditionLayout } : {}),\n ...(renditionFlow !== undefined ? { renditionFlow } : {}),\n ...(tokens.includes(`page-spread-left`)\n ? { pageSpreadLeft: true as const }\n : {}),\n ...(tokens.includes(`page-spread-right`)\n ? { pageSpreadRight: true as const }\n : {}),\n }\n}\n","import type { XmlElement, XmlNodeBase } from \"xmldoc\"\nimport { XmlDocument } from \"xmldoc\"\nimport { tokenizeXmlSpaceSeparatedList } from \"../../utils/tokenizeXmlSpaceSeparatedList.ts\"\nimport { opfIdentifierSchemeAttribute } from \"./identifierScheme.ts\"\nimport {\n opfNamespacedAttribute,\n opfNamespacePrefixes,\n type XmlNamespaceScope,\n xmlNamespaceScope,\n} from \"./opfNamespace.ts\"\nimport { layoutHintsFromItemrefProperties } from \"./spineItemrefProperties.ts\"\n\nexport type OpfSpineManifestItem = {\n readonly id: string\n readonly href: string\n readonly mediaType?: string\n readonly properties?: string\n}\n\nexport type OpfIdentifier = {\n readonly value: string\n readonly scheme?: string\n /** Element `id`, which can be targeted by `package@unique-identifier`. */\n readonly id?: string\n /** Present when `package@unique-identifier` targets this identifier. */\n readonly unique?: true\n}\n\nexport type OpfTitle = {\n readonly value: string\n /** Element `id`, which `meta refines` entries target. */\n readonly id?: string\n}\n\nexport type OpfSpineRow = {\n readonly idref: string\n readonly id: string\n readonly href: string\n readonly mediaType?: string\n readonly properties?: string\n readonly renditionLayout?: `reflowable` | `pre-paginated`\n readonly renditionFlow?:\n | `scrolled-continuous`\n | `scrolled-doc`\n | `paginated`\n | `auto`\n readonly pageSpreadLeft?: true\n readonly pageSpreadRight?: true\n}\n\nexport type OpfGuideReference = {\n readonly href: string\n readonly title: string\n readonly type: string\n}\n\n/**\n * One `<meta>` child of `metadata`, captured verbatim whatever its shape:\n * the EPUB 3 `property`/`refines` form, the EPUB 2 `name`/`content` pair\n * form (including vendor vocabularies such as `calibre:series`), or any\n * mix. Attributes and text are trimmed; blank ones are omitted. This is the\n * open-world channel — OPF `meta` is an open vocabulary, so everything is\n * kept rather than a hand-picked subset.\n */\nexport type OpfMetaEntry = {\n /** EPUB 3 `property` attribute (e.g. `dcterms:modified`, `belongs-to-collection`). */\n readonly property?: string\n /** EPUB 3 `refines` attribute, verbatim (usually `#some-id`). */\n readonly refines?: string\n readonly scheme?: string\n readonly id?: string\n /** EPUB 2 `name` attribute (name/content pair form). */\n readonly name?: string\n /** EPUB 2 `content` attribute. */\n readonly content?: string\n /** Trimmed element text (EPUB 3 form), when non-empty. */\n readonly value?: string\n}\n\nexport type OpfContributor = {\n /** Trimmed element text. */\n readonly name: string\n /** Which element the entry was authored as. */\n readonly source: \"creator\" | \"contributor\"\n /** Element `id`, the anchor for EPUB 3 `refines` metadata. */\n readonly id?: string\n /**\n * Role tokens (MARC relator codes in well-formed books, e.g. `aut`,\n * `ill`): the EPUB 2 `opf:role` attribute first, then every EPUB 3\n * `meta refines property=\"role\"` value in document order. Verbatim,\n * not validated against the MARC list.\n */\n readonly roles: ReadonlyArray<string>\n /** EPUB 2 `opf:file-as` attribute, or the `file-as` refines when absent. */\n readonly fileAs?: string\n}\n\nconst elementLocalName = (name: string): string =>\n name.includes(\":\") ? name.slice(name.lastIndexOf(\":\") + 1) : name\n\nconst localNameEq = (elementName: string, wantLocal: string): boolean =>\n elementLocalName(elementName).toLowerCase() === wantLocal.toLowerCase()\n\nconst isXmlElement = (node: XmlNodeBase): node is XmlElement =>\n node.type === \"element\"\n\nconst childNamedLocal = (\n parent: XmlElement,\n localName: string,\n): XmlElement | undefined => {\n for (const node of parent.children) {\n if (!isXmlElement(node)) continue\n if (localNameEq(node.name, localName)) return node\n }\n return undefined\n}\n\nconst childrenNamedLocal = (\n parent: XmlElement,\n localName: string,\n): XmlElement[] => {\n const out: XmlElement[] = []\n for (const node of parent.children) {\n if (!isXmlElement(node)) continue\n if (localNameEq(node.name, localName)) out.push(node)\n }\n return out\n}\n\nconst identifiersFromMetadata = (\n metadataEl: XmlElement,\n uniqueIdentifierId: string | undefined,\n metadataScope: XmlNamespaceScope,\n): OpfIdentifier[] => {\n const identifiers: OpfIdentifier[] = []\n\n metadataEl.eachChild((child) => {\n if (elementLocalName(child.name).toLowerCase() !== \"identifier\") return\n\n const value = child.val.trim()\n if (value.length === 0) return\n\n const scheme = opfIdentifierSchemeAttribute(\n child.attr,\n opfNamespacePrefixes(xmlNamespaceScope(child.attr, metadataScope)),\n )\n const schemeTrimmed = scheme?.trim()\n const idTrimmed = child.attr.id?.trim()\n const id =\n idTrimmed !== undefined && idTrimmed.length > 0 ? idTrimmed : undefined\n\n identifiers.push({\n value,\n ...(schemeTrimmed !== undefined && schemeTrimmed.length > 0\n ? { scheme: schemeTrimmed }\n : {}),\n ...(id !== undefined ? { id } : {}),\n ...(id !== undefined && id === uniqueIdentifierId\n ? { unique: true }\n : {}),\n })\n })\n\n return identifiers\n}\n\nconst titlesFromMetadata = (metadataEl: XmlElement): OpfTitle[] => {\n const titles: OpfTitle[] = []\n\n metadataEl.eachChild((child) => {\n if (elementLocalName(child.name).toLowerCase() !== \"title\") return\n\n const value = child.val.trim()\n if (value.length === 0) return\n\n const id = child.attr.id?.trim()\n\n titles.push({\n value,\n ...(id !== undefined && id.length > 0 ? { id } : {}),\n })\n })\n\n return titles\n}\n\nconst firstTextByLocalName = (\n metadataEl: XmlElement,\n localName: string,\n): string | undefined => {\n let found: string | undefined\n metadataEl.eachChild((child) => {\n if (found !== undefined) return\n if (elementLocalName(child.name).toLowerCase() !== localName.toLowerCase())\n return\n const t = child.val.trim()\n if (t.length > 0) found = t\n })\n return found\n}\n\nconst textsByLocalName = (\n metadataEl: XmlElement,\n localName: string,\n): string[] => {\n const out: string[] = []\n metadataEl.eachChild((child) => {\n if (elementLocalName(child.name).toLowerCase() !== localName.toLowerCase())\n return\n const t = child.val.trim()\n if (t.length > 0) out.push(t)\n })\n return out\n}\n\nconst coverContentIdFromMetadata = (\n metadataEl: XmlElement,\n): string | undefined => {\n let coverId: string | undefined\n metadataEl.eachChild((child) => {\n if (coverId !== undefined) return\n if (elementLocalName(child.name).toLowerCase() !== \"meta\") return\n if (child.attr.name?.toLowerCase() !== \"cover\") return\n const content = child.attr.content?.trim()\n if (content !== undefined && content.length > 0) coverId = content\n })\n return coverId\n}\n\n/**\n * EPUB cover image inside the manifest. Resolution order, matching\n * what the spec lays out and what the bulk of EPUB producers in the\n * wild rely on:\n *\n * 1. EPUB 3 — the manifest item carrying the `cover-image` token in\n * its `properties` attribute (§ D.6.1).\n * 2. EPUB 2 — `<meta name=\"cover\" content=\"ID\"/>` in `metadata`,\n * resolved to the manifest item with that `id`.\n * 3. Last-resort fallback — any image manifest item whose `id`\n * contains the substring `cover` (case-insensitive); covers the\n * long tail of producers that emit neither the EPUB 3 property\n * nor the EPUB 2 meta.\n *\n * Each step requires the candidate manifest item to advertise an\n * `image/*` media type so non-image artefacts named `cover` (XHTML\n * cover pages, NCX entries) don't slip through.\n */\nconst coverHrefFromManifestAndMetadata = ({\n manifestItems,\n metadataEl,\n}: {\n manifestItems: ReadonlyArray<OpfSpineManifestItem>\n metadataEl: XmlElement | undefined\n}): string | undefined => {\n const isImage = (item: OpfSpineManifestItem): boolean =>\n item.mediaType?.toLowerCase().includes(\"image/\") === true\n\n const byCoverImageProperty = manifestItems.find((item) => {\n if (!isImage(item)) return false\n return tokenizeXmlSpaceSeparatedList(item.properties).includes(\n \"cover-image\",\n )\n })\n if (byCoverImageProperty !== undefined) return byCoverImageProperty.href\n\n if (metadataEl !== undefined) {\n const coverContentId = coverContentIdFromMetadata(metadataEl)\n if (coverContentId !== undefined) {\n const match = manifestItems.find(\n (item) => item.id === coverContentId && isImage(item),\n )\n if (match !== undefined) return match.href\n }\n }\n\n return manifestItems.find(\n (item) => item.id.toLowerCase().includes(\"cover\") && isImage(item),\n )?.href\n}\n\nconst trimmedAttr = (el: XmlElement, name: string): string | undefined => {\n const trimmed = el.attr[name]?.trim()\n return trimmed !== undefined && trimmed.length > 0 ? trimmed : undefined\n}\n\n/** {@link trimmedAttr} for an attribute in the OPF namespace. */\nconst trimmedOpfAttr = (\n el: XmlElement,\n opfPrefixes: ReadonlyArray<string>,\n localName: string,\n): string | undefined => {\n const trimmed = opfNamespacedAttribute(el.attr, opfPrefixes, [\n localName,\n ])?.trim()\n\n return trimmed !== undefined && trimmed.length > 0 ? trimmed : undefined\n}\n\nconst metasFromMetadata = (metadataEl: XmlElement): OpfMetaEntry[] => {\n const metas: OpfMetaEntry[] = []\n\n metadataEl.eachChild((child) => {\n if (elementLocalName(child.name).toLowerCase() !== \"meta\") return\n\n const property = trimmedAttr(child, \"property\")\n const refines = trimmedAttr(child, \"refines\")\n const scheme = trimmedAttr(child, \"scheme\")\n const id = trimmedAttr(child, \"id\")\n const name = trimmedAttr(child, \"name\")\n const content = trimmedAttr(child, \"content\")\n const valueTrimmed = child.val.trim()\n const value = valueTrimmed.length > 0 ? valueTrimmed : undefined\n\n const entry: OpfMetaEntry = {\n ...(property !== undefined ? { property } : {}),\n ...(refines !== undefined ? { refines } : {}),\n ...(scheme !== undefined ? { scheme } : {}),\n ...(id !== undefined ? { id } : {}),\n ...(name !== undefined ? { name } : {}),\n ...(content !== undefined ? { content } : {}),\n ...(value !== undefined ? { value } : {}),\n }\n\n if (Object.keys(entry).length === 0) return\n\n metas.push(entry)\n })\n\n return metas\n}\n\n/** `refines` is a relative IRI, in practice `#id`; a missing `#` is tolerated. */\nconst refinesTargetsId = (refines: string, id: string): boolean =>\n refines.replace(/^#/, \"\") === id\n\nconst contributorsFromMetadata = (\n metadataEl: XmlElement,\n metas: ReadonlyArray<OpfMetaEntry>,\n metadataScope: XmlNamespaceScope,\n): OpfContributor[] => {\n const contributors: OpfContributor[] = []\n\n metadataEl.eachChild((child) => {\n const local = elementLocalName(child.name).toLowerCase()\n if (local !== \"creator\" && local !== \"contributor\") return\n\n const name = child.val.trim()\n if (name.length === 0) return\n\n const id = trimmedAttr(child, \"id\")\n const refinesFor = (property: string): string[] =>\n id === undefined\n ? []\n : metas.flatMap((meta) =>\n meta.refines !== undefined &&\n refinesTargetsId(meta.refines, id) &&\n meta.property === property &&\n meta.value !== undefined\n ? [meta.value]\n : [],\n )\n\n const opfPrefixes = opfNamespacePrefixes(\n xmlNamespaceScope(child.attr, metadataScope),\n )\n const attributeRole = trimmedOpfAttr(child, opfPrefixes, \"role\")\n const roles = [\n ...(attributeRole !== undefined ? [attributeRole] : []),\n ...refinesFor(\"role\"),\n ]\n\n const fileAs =\n trimmedOpfAttr(child, opfPrefixes, \"file-as\") ?? refinesFor(\"file-as\")[0]\n\n contributors.push({\n name,\n source: local,\n ...(id !== undefined ? { id } : {}),\n roles,\n ...(fileAs !== undefined ? { fileAs } : {}),\n })\n })\n\n return contributors\n}\n\nconst metaValByProperty = (\n metadataEl: XmlElement,\n property: string,\n): string | undefined => {\n const meta = childrenNamedLocal(metadataEl, \"meta\").find(\n (m) => m.attr.property === property,\n )\n const raw = meta?.val\n if (raw === undefined || raw.trim().length === 0) return undefined\n return raw\n}\n\nconst guideFromPackage = (doc: XmlElement): OpfGuideReference[] => {\n const guideEl = childNamedLocal(doc, \"guide\")\n if (guideEl === undefined) return []\n\n const refs: OpfGuideReference[] = []\n\n for (const ref of childrenNamedLocal(guideEl, \"reference\")) {\n const href = ref.attr.href?.trim()\n if (href === undefined || href.length === 0) continue\n refs.push({\n href,\n title: ref.attr.title?.trim() ?? ``,\n type: ref.attr.type?.trim() ?? ``,\n })\n }\n\n return refs\n}\n\nconst manifestItemFromXmlElement = (\n item: XmlElement,\n): OpfSpineManifestItem | undefined => {\n const id = item.attr.id\n const href = item.attr.href\n if (id === undefined || id.length === 0) return undefined\n if (href === undefined || href.length === 0) return undefined\n\n const mediaType = item.attr[\"media-type\"]\n const properties = item.attr.properties?.trim()\n return {\n id,\n href,\n ...(mediaType !== undefined && mediaType.length > 0 ? { mediaType } : {}),\n ...(properties !== undefined && properties.length > 0\n ? { properties }\n : {}),\n }\n}\n\nconst manifestItemsAndById = (\n manifestEl: XmlElement,\n): {\n items: OpfSpineManifestItem[]\n byId: Map<string, OpfSpineManifestItem>\n} => {\n const items: OpfSpineManifestItem[] = []\n const byId = new Map<string, OpfSpineManifestItem>()\n\n for (const el of childrenNamedLocal(manifestEl, \"item\")) {\n const parsed = manifestItemFromXmlElement(el)\n if (parsed === undefined) continue\n items.push(parsed)\n byId.set(parsed.id, parsed)\n }\n\n return { items, byId }\n}\n\nconst spineRowsFromByIdAndSpine = (\n byId: Map<string, OpfSpineManifestItem>,\n spineEl: XmlElement,\n): OpfSpineRow[] => {\n const rows: OpfSpineRow[] = []\n\n for (const itemref of childrenNamedLocal(spineEl, \"itemref\")) {\n const idref = itemref.attr.idref\n if (idref === undefined || idref.trim().length === 0) continue\n\n const manifestItem = byId.get(idref)\n if (manifestItem === undefined) continue\n\n const hints = layoutHintsFromItemrefProperties(itemref.attr.properties)\n\n rows.push({\n idref,\n id: manifestItem.id,\n href: manifestItem.href,\n ...(manifestItem.mediaType !== undefined\n ? { mediaType: manifestItem.mediaType }\n : {}),\n ...(manifestItem.properties !== undefined\n ? { properties: manifestItem.properties }\n : {}),\n ...(hints.renditionLayout !== undefined\n ? { renditionLayout: hints.renditionLayout }\n : {}),\n ...(hints.renditionFlow !== undefined\n ? { renditionFlow: hints.renditionFlow }\n : {}),\n ...(hints.pageSpreadLeft !== undefined\n ? { pageSpreadLeft: hints.pageSpreadLeft }\n : {}),\n ...(hints.pageSpreadRight !== undefined\n ? { pageSpreadRight: hints.pageSpreadRight }\n : {}),\n })\n }\n\n return rows\n}\n\nexport type OpfMetadata = {\n readonly kind: \"opf\"\n readonly manifestItems: ReadonlyArray<OpfSpineManifestItem>\n readonly spineRows: ReadonlyArray<OpfSpineRow>\n readonly spineTocIdref: string | undefined\n readonly identifiers: ReadonlyArray<OpfIdentifier>\n /**\n * Every non-empty `dc:title`, in document order — the first is the main\n * title per EPUB 3. Ids are retained so `meta refines` entries\n * (`title-type`, `display-seq`, `file-as`) can be attached to their title.\n */\n readonly titles: ReadonlyArray<OpfTitle>\n /** `dc:creator` values, in document order, trimmed; empty when none. */\n readonly creators: ReadonlyArray<string>\n /**\n * Every `dc:creator` and `dc:contributor`, in document order, with role\n * attribution from both conventions (EPUB 2 `opf:role` attribute, EPUB 3\n * `meta refines property=\"role\"`). `creators` stays the plain-text view\n * of the `dc:creator` subset.\n */\n readonly contributors: ReadonlyArray<OpfContributor>\n /** First non-empty `dc:publisher`, trimmed. */\n readonly publisher: string | undefined\n /** First non-empty `dc:description`, trimmed. */\n readonly description: string | undefined\n /** First non-empty `dc:rights`, trimmed. */\n readonly rights: string | undefined\n /** `dc:language` values, in document order, trimmed; empty when none. */\n readonly languages: ReadonlyArray<string>\n /** `dc:subject` values, in document order, trimmed; empty when none. */\n readonly subjects: ReadonlyArray<string>\n /**\n * Raw `dc:date` value as authored. EPUB 3 requires W3CDTF (a profile\n * of ISO 8601), but real-world publishers also ship free text here,\n * so the value is exposed verbatim and consumers normalize as needed.\n */\n readonly date: string | undefined\n /**\n * Manifest-relative `href` of the cover image, when one can be\n * resolved from `cover-image` properties (EPUB 3), the EPUB 2\n * `<meta name=\"cover\">` convention, or an `id` that contains\n * `cover` on an image manifest item. The href is returned exactly\n * as it appears in the manifest — callers own folder-prefix\n * resolution against the OPF's location in the archive.\n */\n readonly coverHref: string | undefined\n readonly renditionLayoutMeta: string | undefined\n readonly renditionFlowMeta: string | undefined\n readonly renditionSpreadMeta: string | undefined\n readonly pageProgressionDirection: string | undefined\n readonly guide: ReadonlyArray<OpfGuideReference>\n /**\n * Every `<meta>` element of `metadata`, captured verbatim (see\n * {@link OpfMetaEntry}). Known properties (`rendition:*`, roles…) are\n * also exposed as dedicated fields; this list is the lossless record.\n */\n readonly metas: ReadonlyArray<OpfMetaEntry>\n}\n\n/**\n * Parses an EPUB package document (OPF) into structured metadata.\n *\n * Direct children of `package` (`metadata`, `manifest`, `spine`, `guide`) and\n * their structural children (`item`, `itemref`, `reference`, `meta`) are\n * matched by **local name** (ASCII case-insensitive), so prefixed tags such as\n * `opf:manifest` are supported the same as unprefixed `manifest`.\n *\n * Attribute names on `spine` / `itemref` are still read as emitted by xmldoc\n * (no QName normalization).\n */\nexport const parseOpf = (opfXml: string): OpfMetadata => {\n const doc = new XmlDocument(opfXml)\n const manifestEl = childNamedLocal(doc, \"manifest\")\n const spineEl = childNamedLocal(doc, \"spine\")\n const metadataEl = childNamedLocal(doc, \"metadata\")\n const packageScope = xmlNamespaceScope(doc.attr)\n const uniqueIdentifierIdRaw = doc.attr[\"unique-identifier\"]?.trim()\n const uniqueIdentifierId =\n uniqueIdentifierIdRaw !== undefined && uniqueIdentifierIdRaw.length > 0\n ? uniqueIdentifierIdRaw\n : undefined\n\n let manifestItems: OpfSpineManifestItem[] = []\n let spineRows: OpfSpineRow[] = []\n\n if (manifestEl !== undefined) {\n const { items, byId } = manifestItemsAndById(manifestEl)\n manifestItems = items\n if (spineEl !== undefined) {\n spineRows = spineRowsFromByIdAndSpine(byId, spineEl)\n }\n }\n\n const pageProgressionDirectionRaw =\n spineEl?.attr[\"page-progression-direction\"]\n const pageProgressionDirection =\n pageProgressionDirectionRaw !== undefined &&\n pageProgressionDirectionRaw.trim().length > 0\n ? pageProgressionDirectionRaw\n : undefined\n\n const spineTocRaw = spineEl?.attr.toc\n const spineTocIdref =\n spineTocRaw !== undefined && spineTocRaw.trim().length > 0\n ? spineTocRaw.trim()\n : undefined\n\n let titles: OpfTitle[] = []\n let publisher: string | undefined\n let description: string | undefined\n let rights: string | undefined\n let date: string | undefined\n let creators: string[] = []\n let contributors: OpfContributor[] = []\n let languages: string[] = []\n let subjects: string[] = []\n let renditionLayoutMeta: string | undefined\n let renditionFlowMeta: string | undefined\n let renditionSpreadMeta: string | undefined\n let metas: OpfMetaEntry[] = []\n const identifiers: OpfIdentifier[] = []\n\n if (metadataEl !== undefined) {\n const metadataScope = xmlNamespaceScope(metadataEl.attr, packageScope)\n\n titles = titlesFromMetadata(metadataEl)\n publisher = firstTextByLocalName(metadataEl, \"publisher\")\n description = firstTextByLocalName(metadataEl, \"description\")\n rights = firstTextByLocalName(metadataEl, \"rights\")\n date = firstTextByLocalName(metadataEl, \"date\")\n creators = textsByLocalName(metadataEl, \"creator\")\n languages = textsByLocalName(metadataEl, \"language\")\n subjects = textsByLocalName(metadataEl, \"subject\")\n renditionLayoutMeta = metaValByProperty(metadataEl, \"rendition:layout\")\n renditionFlowMeta = metaValByProperty(metadataEl, \"rendition:flow\")\n renditionSpreadMeta = metaValByProperty(metadataEl, \"rendition:spread\")\n metas = metasFromMetadata(metadataEl)\n contributors = contributorsFromMetadata(metadataEl, metas, metadataScope)\n identifiers.push(\n ...identifiersFromMetadata(metadataEl, uniqueIdentifierId, metadataScope),\n )\n }\n\n const coverHref = coverHrefFromManifestAndMetadata({\n manifestItems,\n metadataEl,\n })\n\n const guide = guideFromPackage(doc)\n\n return {\n kind: \"opf\",\n manifestItems,\n spineRows,\n spineTocIdref,\n identifiers,\n titles,\n creators,\n contributors,\n publisher,\n description,\n rights,\n languages,\n subjects,\n date,\n coverHref,\n renditionLayoutMeta,\n renditionFlowMeta,\n renditionSpreadMeta,\n pageProgressionDirection,\n guide,\n metas,\n }\n}\n","import { readRecordAsText } from \"../../archives/readRecordAsText.ts\"\nimport type { Archive } from \"../../archives/types.ts\"\nimport { getArchiveOpfInfo } from \"./getArchiveOpfInfo.ts\"\nimport { type OpfMetadata, parseOpf } from \"./parse.ts\"\n\nexport type ArchiveOpfParsed = {\n opf: OpfMetadata\n basePath: string\n}\n\n/**\n * Loads and parses the package OPF from `archive`.\n *\n * **Attention — not memoized:** every call re-reads the OPF record and runs\n * `parseOpf`. Manifest generation already avoids redundant parses by awaiting\n * this once and passing `archiveOpf` into manifest hooks. The resource pipeline\n * (`generateResourceFromArchive` → `defaultHook`) invokes this again per\n * resource unless callers later thread parsed OPF in explicitly (optional\n * parameter, archive open hook, etc.).\n */\nexport async function readArchiveOpf(\n archive: Archive,\n): Promise<ArchiveOpfParsed | undefined> {\n const { data: opsFile, basePath } = getArchiveOpfInfo(archive) || {}\n\n if (!opsFile || opsFile.dir) {\n return undefined\n }\n\n const raw = await readRecordAsText(opsFile)\n\n return {\n opf: parseOpf(raw),\n basePath,\n }\n}\n","import type { XmlElement } from \"xmldoc\"\nimport { XmlDocument } from \"xmldoc\"\n\nexport const APPLE_IBOOKS_DISPLAY_OPTIONS_FILENAME =\n \"com.apple.ibooks.display-options.xml\"\n\nexport type AppleDisplayOption = {\n readonly name?: string\n readonly value: string\n}\n\nexport type AppleMetadata = {\n readonly kind: \"apple\"\n readonly displayOptions?: {\n readonly platform?: {\n readonly options: ReadonlyArray<AppleDisplayOption>\n }\n }\n}\n\nconst platformOptionsFromElement = (\n platform: XmlElement,\n): ReadonlyArray<AppleDisplayOption> =>\n platform.childrenNamed(\"option\").map((option) => ({\n name: option.attr?.name,\n value: option.val,\n }))\n\nexport const parseAppleDisplayOptionsXml = (xml: string): AppleMetadata => {\n const doc = new XmlDocument(xml)\n const root = doc.name?.toLowerCase()\n\n if (root !== \"display_options\") {\n return { kind: \"apple\" }\n }\n\n const platformEl = doc.childNamed(\"platform\")\n\n return {\n kind: \"apple\",\n displayOptions: {\n ...(platformEl !== undefined\n ? { platform: { options: platformOptionsFromElement(platformEl) } }\n : {}),\n },\n }\n}\n","import type { XmlElement } from \"xmldoc\"\nimport { XmlDocument } from \"xmldoc\"\nimport type { ComicInfoManga } from \"./manga.ts\"\n\n/** Canonical top-level filename; real archives may use any casing. */\nexport const COMIC_INFO_FILENAME = \"ComicInfo.xml\"\n\n/**\n * Every simple child element of the ComicInfo schema (v2.1 draft), audited\n * against the XSD. `Pages` is deliberately absent (nested block, skipped by\n * the parser). This list is the single source of truth for the known keys\n * of {@link ComicInfo} and the domain of the losslessness mapping table in\n * `resolve.ts` — adding a field here without declaring its metadata home\n * there is a type error.\n *\n * @see https://github.com/anansi-project/comicinfo/blob/main/drafts/v2.1/ComicInfo.xsd\n */\nexport const COMIC_INFO_KNOWN_FIELDS = [\n \"AgeRating\",\n \"AlternateCount\",\n \"AlternateNumber\",\n \"AlternateSeries\",\n \"BlackAndWhite\",\n \"Characters\",\n \"Colorist\",\n \"CommunityRating\",\n \"Count\",\n \"CoverArtist\",\n \"Day\",\n \"Editor\",\n \"Format\",\n \"Genre\",\n \"GTIN\",\n \"Imprint\",\n \"Inker\",\n \"LanguageISO\",\n \"Letterer\",\n \"Locations\",\n \"MainCharacterOrTeam\",\n \"Manga\",\n \"Month\",\n \"Notes\",\n \"Number\",\n \"PageCount\",\n \"Penciller\",\n \"Publisher\",\n \"Review\",\n \"ScanInformation\",\n \"Series\",\n \"SeriesGroup\",\n \"StoryArc\",\n \"StoryArcNumber\",\n \"Summary\",\n \"Tags\",\n \"Teams\",\n \"Title\",\n \"Translator\",\n \"Volume\",\n \"Web\",\n \"Writer\",\n \"Year\",\n] as const\n\nexport type ComicInfoKnownField = (typeof COMIC_INFO_KNOWN_FIELDS)[number]\n\n/**\n * Parsed `ComicInfo.xml` root: one optional string property per child element,\n * using the same names as in the file (e.g. `Title`, `GTIN`, `LanguageISO`).\n * Nested blocks such as `Pages` are skipped. Other simple child elements are\n * still copied onto this object under their tag name.\n *\n * @see https://anansi-project.github.io/docs/comicinfo/intro\n * @see https://github.com/anansi-project/comicinfo/blob/main/drafts/v2.1/ComicInfo.xsd for schema\n */\nexport interface ComicInfo\n extends Partial<Record<ComicInfoKnownField, string>> {\n readonly kind: \"comicInfo\"\n /** Schema literals per {@link ComicInfoManga}; files may still use other strings. */\n Manga?: ComicInfoManga | (string & {})\n [tag: string]: string | undefined\n}\n\nconst SKIP_ELEMENT_CHILDREN = new Set([\"Pages\", \"ComicInfo\"])\n\nconst hasNestedElement = (el: XmlElement) =>\n el.children.some((c) => c.type === \"element\")\n\nconst trimmedText = (el: XmlElement): string | undefined => {\n const t = el.val.trim()\n return t.length > 0 ? t : undefined\n}\n\n/**\n * Parse a raw `ComicInfo.xml` body. Each direct child element with plain text\n * becomes a property named after that tag. Malformed XML throws; the parser\n * error is attached as `cause`.\n */\nexport const parseComicInfo = (xml: string): ComicInfo => {\n let doc: XmlDocument\n try {\n doc = new XmlDocument(xml)\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : String(cause)\n throw new Error(`${COMIC_INFO_FILENAME} is malformed: ${message}`, {\n cause,\n })\n }\n\n const fields: Record<string, string> = {}\n\n doc.eachChild((child) => {\n if (child.type !== \"element\") return\n if (SKIP_ELEMENT_CHILDREN.has(child.name)) return\n if (hasNestedElement(child)) return\n\n const text = trimmedText(child)\n if (text === undefined) return\n if (fields[child.name] !== undefined) return\n\n fields[child.name] = text\n })\n\n // `as ComicInfo`: TS cannot infer that spreading `Record<string, string>` into `{ kind }` satisfies the named optional fields plus `[tag: string]: string | undefined` on `ComicInfo`.\n return { kind: \"comicInfo\", ...fields } as ComicInfo\n}\n","import { XmlDocument } from \"xmldoc\"\n\nexport const KOBO_DISPLAY_OPTIONS_FILENAME = \"com.kobobooks.display-options.xml\"\n\n/**\n * Normalized fields from Kobo-specific XML sidecars. Grows as new Kobo\n * document kinds are supported; absent keys mean not present or unknown.\n */\nexport type KoboMetadata = {\n readonly kind: \"kobo\"\n renditionLayout?: `reflowable` | `pre-paginated`\n}\n\nconst parseKoboDisplayOptionsDocument = (\n doc: XmlDocument,\n): { renditionLayout?: \"pre-paginated\" } => {\n const platform = doc.childNamed(\"platform\")\n if (!platform) return {}\n\n for (const option of platform.childrenNamed(\"option\")) {\n if (option.attr?.name !== \"fixed-layout\") continue\n if (option.val.trim().toLowerCase() === \"true\") {\n return { renditionLayout: \"pre-paginated\" }\n }\n return {}\n }\n\n return {}\n}\n\n/**\n * Parse a Kobo-related XML document. Unsupported or unrecognized roots\n * yield an empty object. Malformed XML throws from the underlying parser.\n */\nexport const parseKoboXml = (xml: string): KoboMetadata => {\n const doc = new XmlDocument(xml)\n const root = doc.name?.toLowerCase()\n\n if (root === \"display_options\") {\n return { kind: \"kobo\", ...parseKoboDisplayOptionsDocument(doc) }\n }\n\n return { kind: \"kobo\" }\n}\n","/**\n * Returns a copy of `obj` without the keys whose value is `undefined`, so\n * resolver results stay sparse (an absent key and an `undefined` value are\n * the same statement: \"this source had nothing to say\").\n */\nexport const omitUndefined = <T extends object>(obj: T): T => {\n const entries = Object.entries(obj).filter(([, value]) => value !== undefined)\n\n // `as T`: Object.entries/fromEntries erase the key–value pairing; filtering\n // only drops `undefined` values, which `T`'s optional fields already allow,\n // so the runtime shape is a subset of `T` with no way to express it inline.\n return Object.fromEntries(entries) as T\n}\n","/**\n * Resolves the `.`/`..` segments of a container path (RFC 3986 §5.2.4\n * \"remove dot segments\"), so a manifest href authored relative to a nested\n * package document lands on the real record uri. Operates on the raw path\n * (no percent-decoding) so the result still matches the archive's record uris\n * verbatim. A `..` past the root is clamped, matching URL resolution.\n */\nconst removeDotSegments = (path: string): string => {\n const out: string[] = []\n\n for (const segment of path.split(\"/\")) {\n if (segment === \".\") continue\n if (segment === \"..\") {\n out.pop()\n continue\n }\n out.push(segment)\n }\n\n return out.join(\"/\")\n}\n\n/**\n * Rebases an OPF manifest-relative `href` (spine item, cover image…) onto the\n * package document's `basePath` to produce a container-relative uri — the\n * coordinate space every resolved archive part addresses. The href is a\n * relative reference resolved against the package document's directory, so\n * dot segments are collapsed (`OEBPS/Text` + `../Images/cover.jpg` →\n * `OEBPS/Images/cover.jpg`), not concatenated verbatim. Absolute http(s) urls\n * (the URL-list pseudo-archives author these) are their own record uri and\n * pass through untouched.\n */\nexport const toContainerUri = (href: string, basePath: string): string => {\n if (/^https?:\\/\\//.test(href)) return href\n\n return removeDotSegments(basePath ? `${basePath}/${href}` : href)\n}\n","import {\n detectMimeTypeFromName,\n isMediaContentMimeType,\n parseContentType,\n} from \"@prose-reader/shared\"\nimport type { Archive, FileRecord } from \"../archives/types.ts\"\nimport { getArchiveFileRecordByUri, isFileRecord } from \"../archives/types.ts\"\nimport { APPLE_IBOOKS_DISPLAY_OPTIONS_FILENAME } from \"../metadata/apple/parse.ts\"\nimport { COMIC_INFO_FILENAME } from \"../metadata/comicInfo/parse.ts\"\nimport { KOBO_DISPLAY_OPTIONS_FILENAME } from \"../metadata/kobo/parse.ts\"\nimport type { ArchiveOpfParsed } from \"../metadata/opf/readArchiveOpf.ts\"\nimport { readArchiveOpf } from \"../metadata/opf/readArchiveOpf.ts\"\nimport { omitUndefined } from \"../utils/omitUndefined.ts\"\nimport { toContainerUri } from \"../utils/toContainerUri.ts\"\n\n/**\n * One resource of the publication's default reading sequence, in the\n * container's coordinate space: `uri` addresses the archive record (no\n * serving base baked in — consumers rebase into their own space), except\n * for remote resources authored as absolute http(s) URLs, which pass\n * through as-is.\n */\nexport type ArchiveReadingOrderItem = {\n readonly uri: string\n /** OPF manifest item id, when the container has a package document. */\n readonly id?: string\n readonly mediaType?: string\n /** Uncompressed byte length from the archive record, when known. */\n readonly size?: number\n /**\n * Resolved per-item layout: the itemref override when authored, else the\n * package-level `rendition:layout`, else (non-EPUB) `pre-paginated` for\n * discrete media (images, audio, video). Container sidecars (Apple/Kobo\n * display options) deliberately do not contribute here — they only inform\n * the publication-level layout in the resolved metadata.\n */\n readonly renditionLayout?: \"reflowable\" | \"pre-paginated\"\n readonly renditionFlow?:\n | \"scrolled-continuous\"\n | \"scrolled-doc\"\n | \"paginated\"\n | \"auto\"\n readonly pageSpreadLeft?: true\n readonly pageSpreadRight?: true\n /**\n * Share of the publication's total progression, in [0, 1], summing to ~1:\n * size-proportional for EPUBs (document length is a fair pagination\n * proxy), an equal split otherwise (an image's byte size says nothing\n * about its reading time).\n */\n readonly progressionWeight: number\n}\n\nconst validatedPackageLayout = (\n raw: string | undefined,\n): \"reflowable\" | \"pre-paginated\" | undefined => {\n const v = raw?.trim().toLowerCase()\n return v === \"reflowable\" || v === \"pre-paginated\" ? v : undefined\n}\n\nconst readingOrderFromOpf = (\n archive: Archive,\n { opf, basePath }: ArchiveOpfParsed,\n): ArchiveReadingOrderItem[] => {\n const packageLayout = validatedPackageLayout(opf.renditionLayoutMeta)\n\n const rows = opf.spineRows.map((row) => {\n const uri = toContainerUri(row.href, basePath)\n const record = getArchiveFileRecordByUri(archive, uri)\n\n return { row, uri, record }\n })\n\n const totalSize = rows.reduce(\n (total, { record }) => total + (record?.size ?? 0),\n 0,\n )\n\n return rows.map(({ row, uri, record }) =>\n omitUndefined({\n uri,\n id: row.id,\n mediaType: row.mediaType ?? record?.encodingFormat,\n size: record?.size,\n renditionLayout: row.renditionLayout ?? packageLayout,\n renditionFlow: row.renditionFlow,\n pageSpreadLeft: row.pageSpreadLeft,\n pageSpreadRight: row.pageSpreadRight,\n progressionWeight:\n totalSize > 0\n ? (record?.size ?? 0) / totalSize\n : 1 / opf.spineRows.length,\n }),\n )\n}\n\nconst comicInfoBasenameLower = COMIC_INFO_FILENAME.toLowerCase()\nconst appleDisplayOptionsBasenameLower =\n APPLE_IBOOKS_DISPLAY_OPTIONS_FILENAME.toLowerCase()\n\n/**\n * Container sidecars describe the publication, they are not content: they\n * never belong to the reading order of a sidecar-driven archive (CBZ with\n * ComicInfo.xml, Kobo/Apple-flagged containers), and neither do OS litter\n * files such as `Thumbs.db`.\n */\nconst isSidecarOrLitterRecord = (record: FileRecord): boolean => {\n const basenameLower = record.basename.toLowerCase()\n\n return (\n basenameLower === comicInfoBasenameLower ||\n basenameLower === appleDisplayOptionsBasenameLower ||\n record.uri.endsWith(KOBO_DISPLAY_OPTIONS_FILENAME) ||\n basenameLower.endsWith(`.db`)\n )\n}\n\nconst readingOrderFromRecords = (\n archive: Archive,\n): ArchiveReadingOrderItem[] => {\n const files = archive.records\n .filter(isFileRecord)\n .filter((record) => !isSidecarOrLitterRecord(record))\n\n return files.map((record) => {\n const mediaType =\n parseContentType(record.encodingFormat ?? \"\") ||\n detectMimeTypeFromName(record.basename)\n\n return omitUndefined({\n uri: record.uri,\n mediaType,\n size: record.size,\n renditionLayout:\n mediaType !== undefined && isMediaContentMimeType(mediaType)\n ? (\"pre-paginated\" as const)\n : undefined,\n progressionWeight: 1 / files.length,\n })\n })\n}\n\n/**\n * Resolves the publication's default reading sequence from the container:\n * the OPF spine when a package document exists, the archive's file listing\n * otherwise (sidecars and OS litter excluded). Costs one OPF read at most —\n * pass `opf` (an already parsed `readArchiveOpf` result) to skip the\n * internal lookup, e.g. alongside `resolveArchiveToc`.\n */\nexport const resolveArchiveReadingOrder = async (\n archive: Archive,\n { opf }: { opf?: ArchiveOpfParsed } = {},\n): Promise<ArchiveReadingOrderItem[]> => {\n // A malformed package document is treated as no package document: the\n // reading order always degrades to the archive's file listing rather than\n // throwing (books in the wild are dirty). `resolveArchive` reads and\n // reports the OPF once, then threads it here — passing `undefined` when\n // that read failed — so this fallback read must swallow the same parse\n // error too, otherwise it would re-surface and escape the lenient resolve.\n const archiveOpf =\n opf ?? (await readArchiveOpf(archive).catch(() => undefined))\n\n if (archiveOpf) return readingOrderFromOpf(archive, archiveOpf)\n\n return readingOrderFromRecords(archive)\n}\n","import { detectMimeTypeFromName, parseContentType } from \"@prose-reader/shared\"\nimport type { Archive } from \"../archives/types.ts\"\nimport { getArchiveFileRecordByUri } from \"../archives/types.ts\"\nimport type { ArchiveOpfParsed } from \"../metadata/opf/readArchiveOpf.ts\"\nimport { readArchiveOpf } from \"../metadata/opf/readArchiveOpf.ts\"\nimport type { ArchiveReadingOrderItem } from \"../readingOrder/resolveArchiveReadingOrder.ts\"\nimport { resolveArchiveReadingOrder } from \"../readingOrder/resolveArchiveReadingOrder.ts\"\nimport type { ResolvedCover } from \"../types/resolvedMetadata.ts\"\nimport { omitUndefined } from \"../utils/omitUndefined.ts\"\nimport { toContainerUri } from \"../utils/toContainerUri.ts\"\n\nconst mediaTypeForUri = (archive: Archive, uri: string): string | undefined => {\n const record = getArchiveFileRecordByUri(archive, uri)\n\n if (record === undefined) return undefined\n\n return (\n parseContentType(record.encodingFormat ?? \"\") ||\n detectMimeTypeFromName(record.basename)\n )\n}\n\n/**\n * Resolves the publication's cover resource.\n *\n * Resolution order:\n *\n * 1. **OPF cover** — the manifest cover image (`cover-image` property, the\n * EPUB 2 `<meta name=\"cover\">` convention, or a `cover`-ish image id; see\n * {@link OpfMetadata.coverHref}), rebased onto the package document's base\n * path. A package document that declares no cover is authoritative: no\n * interior image is promoted to cover, so the result is `undefined`.\n * 2. **Package-less containers** (CBZ, folder of images, URL list) — the\n * first reading-order resource, the conventional first page. Sidecars and\n * OS litter are already excluded from the reading order, so this never\n * picks `ComicInfo.xml` or `Thumbs.db`.\n *\n * `undefined` when neither applies (an empty container, or an OPF without a\n * declared cover). Costs one OPF read at most — pass an already parsed `opf`\n * and/or the already resolved `readingOrder` to skip the internal lookups\n * (e.g. alongside `resolveArchive`).\n */\nexport const resolveArchiveCover = async (\n archive: Archive,\n {\n opf,\n readingOrder,\n }: {\n opf?: ArchiveOpfParsed\n readingOrder?: ArchiveReadingOrderItem[]\n } = {},\n): Promise<ResolvedCover | undefined> => {\n // A malformed package document is treated as no package document, mirroring\n // resolveArchiveReadingOrder: the cover degrades to the first-page fallback\n // rather than throwing (books in the wild are dirty).\n const archiveOpf =\n opf ?? (await readArchiveOpf(archive).catch(() => undefined))\n\n // 1. an OPF-declared cover is authoritative.\n if (archiveOpf !== undefined && archiveOpf.opf.coverHref !== undefined) {\n const uri = toContainerUri(archiveOpf.opf.coverHref, archiveOpf.basePath)\n\n return omitUndefined({\n uri,\n mediaType: mediaTypeForUri(archive, uri),\n confidence: \"derived\" as const,\n })\n }\n\n // 2. no declared cover — assume the first image of the reading order. This\n // holds whether or not a package document exists: an authored reflowable\n // EPUB has a text spine with no image items, so it stays undefined rather\n // than promoting an interior illustration (a manifest resource, not a spine\n // item); only image-content publications resolve a cover here — comics,\n // image archives, and synthetic image-spine OPFs (e.g. createArchiveFromUrls\n // lists, whose generated OPF carries no cover marker). Audio/video are\n // pre-paginated discrete media but not images, so they are skipped too.\n const order =\n readingOrder ??\n (await resolveArchiveReadingOrder(archive, { opf: archiveOpf }))\n const firstImage = order.find(\n (item) => item.mediaType?.startsWith(\"image/\") === true,\n )\n\n if (firstImage === undefined) return undefined\n\n // the reading order already computed the media type the same way\n return omitUndefined({\n uri: firstImage.uri,\n mediaType: firstImage.mediaType,\n confidence: \"assumed\" as const,\n })\n}\n","import { isXmlBasedMimeType } from \"@prose-reader/shared\"\nimport { XmlDocument } from \"xmldoc\"\nimport { readRecordAsText } from \"../archives/readRecordAsText.ts\"\nimport type { Archive } from \"../archives/types.ts\"\nimport { getArchiveFileRecordByUri, isFileRecord } from \"../archives/types.ts\"\nimport type { ArchiveReadingOrderItem } from \"../readingOrder/resolveArchiveReadingOrder.ts\"\nimport { Report } from \"../report.ts\"\n\nconst hasDocMetaViewport = (doc: XmlDocument) => {\n const metaElm = doc\n .descendantWithPath(\"head\")\n ?.childrenNamed(\"meta\")\n .find((node) => node.attr.name === \"viewport\")\n\n return !!(metaElm && metaElm.attr.name === \"viewport\")\n}\n\n/**\n * The industry heuristic behind the `layoutScan` effort modifier: many\n * fixed-layout books in the wild declare `rendition:layout` reflowable (or\n * nothing) while every spine document carries a `head > meta[name=viewport]`\n * — the fixed-layout signal renderers actually honor.\n *\n * Returns `true` only when EVERY reading-order document is XML-based\n * (by media type or extension) AND declares a viewport; any non-XML entry\n * (an image in the spine…) or viewport-less document returns `false`.\n * A document that fails to read or parse also returns `false` (logged via\n * Report) — an unreadable book must never be promoted.\n *\n * **Cost: O(reading order)** — reads and XML-parses every document. This is\n * the expensive tier of archive resolution; keep it opt-in.\n */\nexport const readingOrderDocumentsAllHaveViewport = async (\n archive: Archive,\n readingOrder: ReadonlyArray<ArchiveReadingOrderItem>,\n): Promise<boolean> => {\n if (readingOrder.length === 0) return false\n\n for (const item of readingOrder) {\n if (!isXmlBasedMimeType({ mimeType: item.mediaType, uri: item.uri })) {\n return false\n }\n\n const record = getArchiveFileRecordByUri(archive, item.uri)\n\n if (!record || !isFileRecord(record)) return false\n\n try {\n const content = await readRecordAsText(record)\n\n if (!hasDocMetaViewport(new XmlDocument(content))) return false\n } catch (e) {\n Report.error(`layout scan: unable to inspect ${item.uri}`, e)\n\n return false\n }\n }\n\n return true\n}\n","import { readRecordAsText } from \"../../archives/readRecordAsText.ts\"\nimport type { Archive, FileRecord } from \"../../archives/types.ts\"\nimport { isFileRecord } from \"../../archives/types.ts\"\nimport {\n APPLE_IBOOKS_DISPLAY_OPTIONS_FILENAME,\n type AppleMetadata,\n parseAppleDisplayOptionsXml,\n} from \"./parse.ts\"\n\nconst appleDisplayOptionsBasenameLower =\n APPLE_IBOOKS_DISPLAY_OPTIONS_FILENAME.toLowerCase()\n\n/**\n * Loads and parses the Apple iBooks display options sidecar\n * (`META-INF/com.apple.ibooks.display-options.xml`) from `archive`.\n *\n * Returns `undefined` when the archive has no such file (matched on\n * basename, ASCII case-insensitive, anywhere in the container). Malformed\n * XML throws — lenient callers own the catch.\n */\nexport async function readArchiveApple(\n archive: Archive,\n): Promise<AppleMetadata | undefined> {\n const record = archive.records.find(\n (file): file is FileRecord =>\n isFileRecord(file) &&\n file.basename.toLowerCase() === appleDisplayOptionsBasenameLower,\n )\n\n if (!record) {\n return undefined\n }\n\n return parseAppleDisplayOptionsXml(await readRecordAsText(record))\n}\n","import {\n type Archive,\n type FileRecord,\n isFileRecord,\n} from \"../../archives/types.ts\"\nimport { COMIC_INFO_FILENAME } from \"./parse.ts\"\n\nconst comicInfoFilenameLower = COMIC_INFO_FILENAME.toLowerCase()\n\nexport const getArchiveHasComicInfo = (archive: Archive) =>\n archive.records.find(\n (record): record is FileRecord =>\n isFileRecord(record) &&\n record.basename.toLowerCase() === comicInfoFilenameLower,\n )\n","/**\n * Allowed `Manga` element values (ComicInfo 2.0 `Manga` simpleType).\n *\n * @see https://anansi-project.github.io/docs/comicinfo/documentation#manga\n */\nexport const COMIC_INFO_MANGA_VALUES = [\n \"Unknown\",\n \"No\",\n \"Yes\",\n \"YesAndRightToLeft\",\n] as const\n\nexport type ComicInfoManga = (typeof COMIC_INFO_MANGA_VALUES)[number]\n\nexport const isComicInfoManga = (value: string): value is ComicInfoManga => {\n for (const v of COMIC_INFO_MANGA_VALUES) {\n if (v === value) return true\n }\n return false\n}\n","import { readRecordAsText } from \"../../archives/readRecordAsText.ts\"\nimport type { Archive } from \"../../archives/types.ts\"\nimport { getArchiveHasComicInfo } from \"./getArchiveHasComicInfo.ts\"\nimport { type ComicInfo, parseComicInfo } from \"./parse.ts\"\n\n/**\n * Loads and parses the `ComicInfo.xml` sidecar from `archive`.\n *\n * Returns `undefined` when the archive has no ComicInfo file (matched on\n * basename, ASCII case-insensitive, anywhere in the container). Malformed\n * XML throws, same as {@link parseComicInfo} — lenient callers (a whole\n * archive resolve that treats books in the wild as dirty) own the catch.\n */\nexport async function readArchiveComicInfo(\n archive: Archive,\n): Promise<ComicInfo | undefined> {\n const record = getArchiveHasComicInfo(archive)\n\n if (!record) {\n return undefined\n }\n\n return parseComicInfo(await readRecordAsText(record))\n}\n","import { readRecordAsText } from \"../../archives/readRecordAsText.ts\"\nimport type { Archive, FileRecord } from \"../../archives/types.ts\"\nimport { isFileRecord } from \"../../archives/types.ts\"\nimport { Report } from \"../../report.ts\"\nimport {\n KOBO_DISPLAY_OPTIONS_FILENAME,\n type KoboMetadata,\n parseKoboXml,\n} from \"./parse.ts\"\n\n/**\n * Loads and parses Kobo XML sidecars (`com.kobobooks.display-options.xml`)\n * from `archive`, merging them into a single {@link KoboMetadata}.\n *\n * Files are matched anywhere in the container (`uri` suffix). When several\n * match, they are read in record order and the last parsed value for a field\n * wins. A file that fails to parse is skipped (logged via Report) — per-file\n * resilience is part of the merge semantics, unlike the single-file readers\n * which throw. Returns `undefined` when no Kobo file is found.\n */\nexport async function readArchiveKobo(\n archive: Archive,\n): Promise<KoboMetadata | undefined> {\n const records = archive.records.filter(\n (file): file is FileRecord =>\n isFileRecord(file) && file.uri.endsWith(KOBO_DISPLAY_OPTIONS_FILENAME),\n )\n\n if (records.length === 0) {\n return undefined\n }\n\n let renditionLayout: KoboMetadata[\"renditionLayout\"]\n\n for (const record of records) {\n const content = await readRecordAsText(record)\n\n try {\n const { renditionLayout: layout } = parseKoboXml(content)\n if (layout) renditionLayout = layout\n } catch (e) {\n Report.error(\n `Unable to parse ${KOBO_DISPLAY_OPTIONS_FILENAME} (${record.uri})`,\n e,\n )\n }\n }\n\n return {\n kind: `kobo`,\n ...(renditionLayout !== undefined ? { renditionLayout } : {}),\n }\n}\n","import type { Archive } from \"../../archives/types.ts\"\nimport type { ArchiveOpfParsed } from \"./readArchiveOpf.ts\"\n\nexport const getSpineItemFilesFromArchive = async ({\n archive,\n archiveOpf,\n}: {\n archive: Archive\n archiveOpf: ArchiveOpfParsed | undefined\n}) => {\n if (!archiveOpf) return []\n\n const { opf, basePath: opfBasePath } = archiveOpf\n const { spineRows } = opf\n\n const archiveSpineItems = archive.records.filter((file) => {\n return spineRows.find((item) => {\n if (!opfBasePath) return `${item.href}` === file.uri\n return `${opfBasePath}/${item.href}` === file.uri\n })\n })\n\n return archiveSpineItems\n}\n","import type { Archive } from \"../../archives/types.ts\"\n\nconst hasOpfExtension = (path: string) => path.toLowerCase().endsWith(`.opf`)\n\nexport const isArchiveEpub = (archive: Archive) => {\n return archive.records.some(\n (file) =>\n !file.dir &&\n (hasOpfExtension(file.basename) || hasOpfExtension(file.uri)),\n )\n}\n","import type { Exhaustive } from \"../../types/exhaustive.ts\"\nimport type {\n ResolvedAppleMetadata,\n ResolvedMetadata,\n} from \"../../types/resolvedMetadata.ts\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport type { AppleMetadata } from \"./parse.ts\"\n\nexport const resolveApple = (input: AppleMetadata): ResolvedMetadata => {\n const options = input.displayOptions?.platform?.options\n\n const fixedLayoutValue = options\n ?.find((option) => option.name === \"fixed-layout\")\n ?.value.trim()\n .toLowerCase()\n const fixedLayout =\n fixedLayoutValue === \"true\"\n ? true\n : fixedLayoutValue === \"false\"\n ? false\n : undefined\n\n const apple = omitUndefined({\n fixedLayout,\n options:\n options !== undefined && options.length > 0\n ? options.map(({ name, value }) => omitUndefined({ name, value }))\n : undefined,\n } satisfies Exhaustive<ResolvedAppleMetadata>)\n\n return omitUndefined({\n renditionLayout:\n fixedLayout === true ? (\"pre-paginated\" as const) : undefined,\n apple: Object.keys(apple).length > 0 ? apple : undefined,\n })\n}\n","import type { Exhaustive } from \"../../types/exhaustive.ts\"\nimport type {\n ResolvedAggregateRating,\n ResolvedCollection,\n ResolvedComicInfoMetadata,\n ResolvedContributor,\n ResolvedContributorRole,\n ResolvedDate,\n ResolvedMetadata,\n ResolvedMetadataIdentifier,\n} from \"../../types/resolvedMetadata.ts\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport type { ComicInfo, ComicInfoKnownField } from \"./parse.ts\"\n\nconst readingDirection = (info: ComicInfo): \"ltr\" | \"rtl\" | undefined => {\n switch (info.Manga) {\n case \"YesAndRightToLeft\":\n return \"rtl\"\n case \"Yes\":\n case \"No\":\n return \"ltr\"\n default:\n return undefined\n }\n}\n\nconst trimToUndefined = (raw: string | undefined): string | undefined => {\n if (raw === undefined) return undefined\n const trimmed = raw.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\n/**\n * Split a comma-separated ComicInfo value into its individual tokens.\n * `Writer`, `Genre`, `Tags`, `Characters`… all follow the same de facto\n * schema convention: tokens separated by `,`, with whitespace trimmed and\n * empty tokens dropped (real-world files leave trailing commas around).\n */\nconst splitCommaList = (raw: string | undefined): string[] => {\n if (raw === undefined) return []\n return raw\n .split(\",\")\n .map((token) => token.trim())\n .filter((token) => token.length > 0)\n}\n\nconst parseNonNegativeInt = (raw: string | undefined): number | undefined => {\n const trimmed = trimToUndefined(raw)\n if (trimmed === undefined) return undefined\n if (!/^\\d+$/.test(trimmed)) return undefined\n const n = Number.parseInt(trimmed, 10)\n return Number.isFinite(n) ? n : undefined\n}\n\n/** Non-negative decimal (\"1\", \"1.5\"); anything else is undefined. */\nconst parseDecimal = (raw: string | undefined): number | undefined => {\n const trimmed = trimToUndefined(raw)\n if (trimmed === undefined) return undefined\n if (!/^\\d+(\\.\\d+)?$/.test(trimmed)) return undefined\n const n = Number.parseFloat(trimmed)\n return Number.isFinite(n) ? n : undefined\n}\n\n/** Schema yes/no enums (`Yes`/`No`/`Unknown`), tolerant on casing. */\nconst parseYesNo = (raw: string | undefined): boolean | undefined => {\n const normalized = trimToUndefined(raw)?.toLowerCase()\n if (normalized === \"yes\") return true\n if (normalized === \"no\") return false\n return undefined\n}\n\n/** Same schema-literal matching as {@link readingDirection}. */\nconst mangaFlag = (info: ComicInfo): boolean | undefined => {\n switch (info.Manga) {\n case \"Yes\":\n case \"YesAndRightToLeft\":\n return true\n case \"No\":\n return false\n default:\n return undefined\n }\n}\n\nconst dateFromYearMonthDay = (info: ComicInfo): ResolvedDate | undefined => {\n const year = parseNonNegativeInt(info.Year)\n const month = parseNonNegativeInt(info.Month)\n const day = parseNonNegativeInt(info.Day)\n\n if (year === undefined && month === undefined && day === undefined) {\n return undefined\n }\n\n return {\n ...(year !== undefined ? { year } : {}),\n ...(month !== undefined ? { month } : {}),\n ...(day !== undefined ? { day } : {}),\n }\n}\n\nconst COMIC_INFO_ROLE_FIELDS = [\n [\"Writer\", \"author\"],\n [\"Penciller\", \"penciler\"],\n [\"Inker\", \"inker\"],\n [\"Colorist\", \"colorist\"],\n [\"Letterer\", \"letterer\"],\n [\"CoverArtist\", \"coverArtist\"],\n [\"Editor\", \"editor\"],\n [\"Translator\", \"translator\"],\n] as const satisfies ReadonlyArray<\n [ComicInfoKnownField, ResolvedContributorRole]\n>\n\n/**\n * One entry per person: a name appearing in several role fields (writes and\n * inks their own book) gets its roles merged, in field order above.\n */\nconst contributorsFromComicInfo = (info: ComicInfo): ResolvedContributor[] => {\n const byName = new Map<string, { name: string; roles: string[] }>()\n\n for (const [field, role] of COMIC_INFO_ROLE_FIELDS) {\n for (const name of splitCommaList(info[field])) {\n const existing = byName.get(name)\n if (existing) {\n if (!existing.roles.includes(role)) existing.roles.push(role)\n } else {\n byName.set(name, { name, roles: [role] })\n }\n }\n }\n\n return [...byName.values()]\n}\n\n/**\n * `Number` and `Count` are facts about this issue's place in its series,\n * which a sidecar can state without naming the series — so a nameless entry\n * is still emitted rather than dropping what the file says.\n */\nconst seriesFromComicInfo = (\n info: ComicInfo,\n): ResolvedCollection | undefined => {\n const series = omitUndefined({\n name: trimToUndefined(info.Series),\n position: parseDecimal(info.Number),\n total: parseNonNegativeInt(info.Count),\n })\n\n return Object.keys(series).length > 0 ? series : undefined\n}\n\nconst alternateSeriesFromComicInfo = (\n info: ComicInfo,\n): ResolvedCollection | undefined => {\n const name = trimToUndefined(info.AlternateSeries)\n if (name === undefined) return undefined\n\n return omitUndefined({\n name,\n position: parseDecimal(info.AlternateNumber),\n total: parseNonNegativeInt(info.AlternateCount),\n })\n}\n\n/** `StoryArc`/`StoryArcNumber` are comma lists zipped positionally. */\nconst storyArcsFromComicInfo = (info: ComicInfo): ResolvedCollection[] => {\n const names = splitCommaList(info.StoryArc)\n if (names.length === 0) return []\n\n const positions = splitCommaList(info.StoryArcNumber)\n\n return names.map((name, index) =>\n omitUndefined({ name, position: parseDecimal(positions[index]) }),\n )\n}\n\nconst emptyToUndefined = <T>(\n values: ReadonlyArray<T>,\n): ReadonlyArray<T> | undefined => (values.length > 0 ? values : undefined)\n\nconst webFromComicInfo = (info: ComicInfo): ReadonlyArray<string> =>\n (info.Web ?? \"\").split(/\\s+/).filter((token) => token.length > 0)\n\nconst isAbsoluteHttpUrl = (value: string): boolean => {\n try {\n const url = new URL(value)\n\n return (\n (url.protocol === \"http:\" || url.protocol === \"https:\") &&\n url.hostname.length > 0\n )\n } catch {\n return false\n }\n}\n\nconst webIdentifiers = (\n values: ReadonlyArray<string>,\n): ReadonlyArray<ResolvedMetadataIdentifier> =>\n values\n .filter(\n (value, index, all) =>\n isAbsoluteHttpUrl(value) && all.indexOf(value) === index,\n )\n .map((value) => ({ value, scheme: \"URL\" }))\n\nconst comicCornerFromComicInfo = (\n info: ComicInfo,\n webValues: ReadonlyArray<string>,\n): ResolvedMetadata[\"comicInfo\"] => {\n const corner = omitUndefined({\n manga: mangaFlag(info),\n blackAndWhite: parseYesNo(info.BlackAndWhite),\n volume: parseNonNegativeInt(info.Volume),\n format: trimToUndefined(info.Format),\n ageRating: trimToUndefined(info.AgeRating),\n notes: trimToUndefined(info.Notes),\n review: trimToUndefined(info.Review),\n web: emptyToUndefined(webValues),\n scanInformation: trimToUndefined(info.ScanInformation),\n mainCharacterOrTeam: trimToUndefined(info.MainCharacterOrTeam),\n characters: emptyToUndefined(splitCommaList(info.Characters)),\n teams: emptyToUndefined(splitCommaList(info.Teams)),\n locations: emptyToUndefined(splitCommaList(info.Locations)),\n storyArcs: emptyToUndefined(storyArcsFromComicInfo(info)),\n alternateSeries: alternateSeriesFromComicInfo(info),\n } satisfies Exhaustive<ResolvedComicInfoMetadata>)\n\n return Object.keys(corner).length > 0 ? corner : undefined\n}\n\n/**\n * `CommunityRating` is already the schema's 0–5 star mean, so it needs no\n * rescaling — only the range check the field promises. The sidecar states no\n * number of ratings.\n */\nconst aggregateRatingFromComicInfo = (\n info: ComicInfo,\n): ResolvedAggregateRating | undefined => {\n const value = parseDecimal(info.CommunityRating)\n\n return value !== undefined && value <= 5 ? { value } : undefined\n}\n\nconst belongsToFromComicInfo = (\n info: ComicInfo,\n): ResolvedMetadata[\"belongsTo\"] => {\n const series = seriesFromComicInfo(info)\n const collection = emptyToUndefined(\n splitCommaList(info.SeriesGroup).map((name) => ({ name })),\n )\n\n if (series === undefined && collection === undefined) return undefined\n\n return omitUndefined({\n series: series !== undefined ? [series] : undefined,\n collection,\n })\n}\n\nexport const resolveComicInfo = (info: ComicInfo): ResolvedMetadata => {\n const rawGtin = info.GTIN\n const gtinTrimmed = trimToUndefined(rawGtin)\n const web = webFromComicInfo(info)\n const identifiers: ReadonlyArray<ResolvedMetadataIdentifier> = [\n ...(gtinTrimmed !== undefined\n ? [{ value: gtinTrimmed, scheme: \"GTIN\" }]\n : []),\n ...webIdentifiers(web),\n ]\n const languageIso = trimToUndefined(info.LanguageISO)\n const subjects = [...splitCommaList(info.Genre), ...splitCommaList(info.Tags)]\n const edition = omitUndefined({\n date: dateFromYearMonthDay(info),\n publisher: trimToUndefined(info.Publisher),\n imprint: trimToUndefined(info.Imprint),\n })\n\n const title = trimToUndefined(info.Title)\n\n return omitUndefined({\n titles: title !== undefined ? [{ value: title }] : undefined,\n description: trimToUndefined(info.Summary),\n publication:\n edition.date !== undefined ||\n edition.publisher !== undefined ||\n edition.imprint !== undefined\n ? { edition }\n : undefined,\n languages: languageIso !== undefined ? [languageIso] : undefined,\n subjects: emptyToUndefined(subjects),\n contributors: emptyToUndefined(contributorsFromComicInfo(info)),\n readingDirection: readingDirection(info),\n numberOfPages: parseNonNegativeInt(info.PageCount),\n aggregateRating: aggregateRatingFromComicInfo(info),\n identifiers: emptyToUndefined(identifiers),\n belongsTo: belongsToFromComicInfo(info),\n comicInfo: comicCornerFromComicInfo(info, web),\n })\n}\n","import type { Exhaustive } from \"../../types/exhaustive.ts\"\nimport type {\n ResolvedKoboMetadata,\n ResolvedMetadata,\n} from \"../../types/resolvedMetadata.ts\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport type { KoboMetadata } from \"./parse.ts\"\n\nexport const resolveKobo = (input: KoboMetadata): ResolvedMetadata =>\n omitUndefined({\n renditionLayout: input.renditionLayout,\n kobo:\n input.renditionLayout !== undefined\n ? ({\n fixedLayout: input.renditionLayout === \"pre-paginated\",\n } satisfies Exhaustive<ResolvedKoboMetadata>)\n : undefined,\n })\n","const NUMBER_PATTERN = /[0-9Xx]+/g\nconst ISBN_NUMBER_PATTERN = /^(?:97[89])?\\d{9}[\\dXx]$/\n\n/**\n * Normalize a raw ISBN-ish string into a canonical 10- or 13-character\n * form, or `undefined` when no recognisable ISBN can be recovered.\n *\n * - Strips the common `urn:isbn:` / `isbn:` prefixes.\n * - Drops everything that isn't a digit or `X`.\n * - Validates the resulting length (10 or 13).\n * - Falls back to scanning the numbers in the string, so publishers that\n * stuff free text around the ISBN still yield a usable value.\n *\n * Only a *whole* number is a candidate in that fallback: carving ten digits\n * out of a longer barcode (`036180592125` — a GTIN-12) would invent an ISBN\n * the publication never printed.\n */\nexport const normalizeIsbn = (\n raw: string | number | undefined | null,\n): string | undefined => {\n if (raw === undefined || raw === null) return undefined\n\n const stripped = String(raw)\n .trim()\n .replace(/^urn:isbn:/i, \"\")\n .replace(/^isbn[:\\s-]*/i, \"\")\n\n const digitsOnly = stripped.replace(/[^0-9Xx]/g, \"\")\n\n if (digitsOnly.length === 10 || digitsOnly.length === 13) {\n return digitsOnly.toUpperCase()\n }\n\n for (const number of stripped.match(NUMBER_PATTERN) ?? []) {\n if (ISBN_NUMBER_PATTERN.test(number)) return number.toUpperCase()\n }\n\n return undefined\n}\n","import { normalizeIsbn } from \"./normalizeIsbn.ts\"\n\n/**\n * The ISBN a raw value denotes, or `undefined` when the value is not one.\n *\n * `normalizeIsbn` recovers ISBN *syntax* only: any 10- or 13-character digit\n * run satisfies it, so a scanned comic's retail barcode (`4006381333931`)\n * comes back looking like an ISBN. Books occupy the Bookland range of the GTIN\n * space — an ISBN-13 always starts with `978` or `979` — so requiring that\n * prefix is what separates a real ISBN from any other EAN. ISBN-10 predates\n * Bookland and is accepted on its length alone (it converts into a `978`\n * ISBN-13).\n *\n * Check digits are not verified: a mistyped one still identifies the intended\n * book, and rejecting it would lose more than it protects.\n */\nexport const booklandIsbn = (\n raw: string | number | undefined | null,\n): string | undefined => {\n const isbn = normalizeIsbn(raw)\n\n if (isbn === undefined) return undefined\n\n return isbn.length === 10 || /^97[89]/.test(isbn) ? isbn : undefined\n}\n","const GTIN_LENGTHS = new Set([8, 12, 13, 14])\n\n/**\n * Normalize a raw GTIN / EAN / UPC string to digits only when the length\n * matches a GS1 GTIN family size (8, 12, 13, or 14). Does not verify check digits.\n */\nexport const normalizeGtin = (\n raw: string | number | undefined | null,\n): string | undefined => {\n if (raw === undefined || raw === null) return undefined\n\n const digits = String(raw).replace(/\\D/g, \"\")\n if (digits.length === 0 || !GTIN_LENGTHS.has(digits.length)) return undefined\n\n return digits\n}\n","import type { KnownMetadataIdentifierScheme } from \"../types/resolvedMetadata.ts\"\nimport { booklandIsbn } from \"./booklandIsbn.ts\"\nimport { normalizeGtin } from \"./normalizeGtin.ts\"\n\n/**\n * The schemes a value can announce by itself. Drawn from\n * {@link KnownMetadataIdentifierScheme} rather than restated, so renaming one\n * there fails to compile here.\n */\nexport type InferredIdentifierScheme = Extract<\n KnownMetadataIdentifierScheme,\n \"ISBN\" | \"GTIN\" | \"URL\" | \"Unknown\"\n>\n\n/**\n * The scheme a value announces by its own syntax, for an identifier that\n * announced none: an absolute http(s) link is a `URL`, a Bookland number is an\n * `ISBN`, another well-formed barcode is a `GTIN`, and anything else is\n * `Unknown`.\n *\n * This is what resolution believes about an untyped identifier, so a consumer\n * holding the container itself — one editing it in place — reads it from here\n * rather than restating it, and cannot disagree with `identifiers` about which\n * element carries which scheme.\n */\nexport const inferIdentifierScheme = (\n value: string,\n): InferredIdentifierScheme => {\n const trimmed = value.trim()\n\n if (/^https?:\\/\\//i.test(trimmed)) {\n try {\n const url = new URL(trimmed)\n\n if (\n (url.protocol === \"http:\" || url.protocol === \"https:\") &&\n url.hostname.length > 0\n ) {\n return \"URL\"\n }\n } catch {\n // Continue with identifier-specific inference.\n }\n }\n\n if (booklandIsbn(trimmed) !== undefined) return \"ISBN\"\n if (normalizeGtin(trimmed) !== undefined) return \"GTIN\"\n\n return \"Unknown\"\n}\n","import type {\n KnownMetadataIdentifierScheme,\n MetadataIdentifierScheme,\n} from \"../types/resolvedMetadata.ts\"\n\n/**\n * Keyed by the scheme union so a scheme added to the vocabulary fails to\n * compile until it is spelled here too.\n */\nconst CANONICAL_SPELLINGS: Record<\n Lowercase<KnownMetadataIdentifierScheme>,\n KnownMetadataIdentifierScheme\n> = {\n isbn: \"ISBN\",\n gtin: \"GTIN\",\n doi: \"DOI\",\n googlebooks: \"GoogleBooks\",\n openlibrary: \"OpenLibrary\",\n projectgutenberg: \"ProjectGutenberg\",\n url: \"URL\",\n unknown: \"Unknown\",\n}\n\nconst SPELLINGS_BY_LOWERCASE = new Map<string, KnownMetadataIdentifierScheme>(\n Object.entries(CANONICAL_SPELLINGS),\n)\n\n/**\n * A scheme in the spelling the vocabulary uses, so `opf:scheme=\"isbn\"` and an\n * `identifier-type` of `ISBN` describe the one namespace. Publications author\n * these by hand and their case is not significant.\n *\n * A scheme outside the vocabulary is returned trimmed but otherwise untouched:\n * a custom namespace is a supported case, and its spelling is the\n * publication's to choose.\n */\nexport const normalizeIdentifierScheme = (\n scheme: MetadataIdentifierScheme,\n): MetadataIdentifierScheme => {\n const trimmed = scheme.trim()\n\n return SPELLINGS_BY_LOWERCASE.get(trimmed.toLowerCase()) ?? trimmed\n}\n","/**\n * Extract the calendar components of a W3CDTF literal — the\n * date subset of ISO 8601 that EPUB 3.3 § 5.5.3.2.4 mandates for\n * `dc:date`. Accepted forms: `YYYY`, `YYYY-MM`, `YYYY-MM-DD`, and\n * any of the above followed by a `Thh:mm[:ss[.s]][TZD]` time\n * portion that we ignore.\n *\n * Components are returned as plain integers (`month` is 1-12,\n * `day` is 1-31), independent of the host timezone — using\n * `Date.parse` would shift `2011-01-01` by a day in negative-offset\n * locales, which is the exact bug this regex-based approach exists\n * to avoid. Returns `undefined` when the input doesn't even match\n * a leading 4-digit year so consumers can fall back without\n * branching on partial shapes.\n */\nexport const parseW3cDtfDate = (\n raw: string | undefined,\n):\n | {\n year?: number\n month?: number\n day?: number\n }\n | undefined => {\n if (raw === undefined) return undefined\n\n const match = raw.trim().match(/^(\\d{4})(?:-(\\d{2})(?:-(\\d{2}))?)?/)\n if (match === null) return undefined\n\n const [, yearRaw, monthRaw, dayRaw] = match\n\n return {\n ...(yearRaw !== undefined ? { year: Number.parseInt(yearRaw, 10) } : {}),\n ...(monthRaw !== undefined ? { month: Number.parseInt(monthRaw, 10) } : {}),\n ...(dayRaw !== undefined ? { day: Number.parseInt(dayRaw, 10) } : {}),\n }\n}\n","import type {\n MetadataIdentifier,\n ResolvedCollection,\n ResolvedContributor,\n ResolvedContributorRole,\n ResolvedMetadata,\n ResolvedTitle,\n} from \"../../types/resolvedMetadata.ts\"\nimport { inferIdentifierScheme } from \"../../utils/inferIdentifierScheme.ts\"\nimport { normalizeIdentifierScheme } from \"../../utils/normalizeIdentifierScheme.ts\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport { parseW3cDtfDate } from \"../../utils/parseW3cDtfDate.ts\"\nimport { opfIdentifierTypeScheme } from \"./identifierScheme.ts\"\nimport type {\n OpfContributor,\n OpfIdentifier,\n OpfMetadata,\n OpfMetaEntry,\n OpfTitle,\n} from \"./parse.ts\"\n\n/**\n * Common MARC relator codes normalized into the Readium role vocabulary;\n * anything else passes through verbatim (losslessness beats guessing).\n * @see https://www.loc.gov/marc/relators/relaterm.html\n */\nconst MARC_RELATOR_TO_ROLE: Readonly<\n Partial<Record<string, ResolvedContributorRole>>\n> = {\n art: \"artist\",\n aut: \"author\",\n clr: \"colorist\",\n ctb: \"contributor\",\n edt: \"editor\",\n ill: \"illustrator\",\n nrt: \"narrator\",\n trl: \"translator\",\n}\n\nconst contributorsFromOpf = (\n // `contributors` already covers every dc:creator (the `creators` list is\n // the plain-text view of the same elements), so it is the single source.\n parsed: ReadonlyArray<OpfContributor>,\n): ResolvedContributor[] =>\n parsed.map((contributor) => {\n const roles = contributor.roles.map(\n (token) => MARC_RELATOR_TO_ROLE[token.trim().toLowerCase()] ?? token,\n )\n\n return omitUndefined({\n name: contributor.name,\n roles:\n roles.length > 0\n ? roles\n : // a role-less dc:creator means author per EPUB 2 semantics; a\n // role-less dc:contributor is the generic Readium `contributor`\n [\n contributor.source === \"creator\"\n ? (\"author\" as const)\n : (\"contributor\" as const),\n ],\n sortAs: contributor.fileAs,\n })\n })\n\n/** Same missing-`#` tolerance as the refines matching in parse.ts. */\nconst metaRefinesId = (meta: OpfMetaEntry, id: string): boolean =>\n meta.refines !== undefined && meta.refines.replace(/^#/, \"\") === id\n\nconst refinedIdentifierType = (\n identifier: OpfIdentifier,\n metas: ReadonlyArray<OpfMetaEntry>,\n): string | undefined => {\n const id = identifier.id\n\n if (id === undefined) return undefined\n\n return opfIdentifierTypeScheme(\n metas.find(\n (meta) =>\n meta.property === \"identifier-type\" &&\n metaRefinesId(meta, id) &&\n meta.value !== undefined,\n ),\n )\n}\n\nconst parseDecimal = (raw: string | undefined): number | undefined => {\n const trimmed = raw?.trim()\n if (trimmed === undefined || !/^\\d+(\\.\\d+)?$/.test(trimmed)) return undefined\n const n = Number.parseFloat(trimmed)\n return Number.isFinite(n) ? n : undefined\n}\n\nconst parseNonNegativeInt = (raw: string | undefined): number | undefined => {\n const trimmed = raw?.trim()\n if (trimmed === undefined || !/^\\d+$/.test(trimmed)) return undefined\n const n = Number.parseInt(trimmed, 10)\n return Number.isFinite(n) ? n : undefined\n}\n\nconst trimToUndefined = (raw: string | undefined): string | undefined => {\n const trimmed = raw?.trim()\n\n return trimmed !== undefined && trimmed.length > 0 ? trimmed : undefined\n}\n\n/** The value of the `meta refines=\"#id\"` entry stating `property`. */\nconst refiningValue = (\n metas: ReadonlyArray<OpfMetaEntry>,\n id: string | undefined,\n property: string,\n): string | undefined =>\n id === undefined\n ? undefined\n : metas.find(\n (meta) =>\n metaRefinesId(meta, id) &&\n meta.property === property &&\n meta.value !== undefined,\n )?.value\n\n/**\n * EPUB 3 states a multipart title as several `dc:title` elements refined\n * with `title-type`, `display-seq` and `file-as`. All of them are kept, in\n * document order — the order matters, since the spec makes the first one the\n * main title whatever the refinements say (see `mainTitle`).\n */\nconst titlesFromOpf = (\n parsed: ReadonlyArray<OpfTitle>,\n metas: ReadonlyArray<OpfMetaEntry>,\n): ResolvedTitle[] =>\n parsed.map((title) =>\n omitUndefined({\n value: title.value,\n type: trimToUndefined(\n refiningValue(metas, title.id, \"title-type\"),\n )?.toLowerCase(),\n displaySeq: parseNonNegativeInt(\n refiningValue(metas, title.id, \"display-seq\"),\n ),\n sortAs: trimToUndefined(refiningValue(metas, title.id, \"file-as\")),\n }),\n )\n\n/**\n * A collection can be identified as well as named: `dcterms:identifier`\n * refines the `belongs-to-collection` meta, and an `identifier-type`\n * refinement of that identifier names its scheme.\n */\nconst collectionIdentifiers = (\n metas: ReadonlyArray<OpfMetaEntry>,\n id: string | undefined,\n): ReadonlyArray<MetadataIdentifier> | undefined => {\n if (id === undefined) return undefined\n\n const identifiers = metas.flatMap<MetadataIdentifier>((meta) => {\n if (!metaRefinesId(meta, id) || meta.property !== \"dcterms:identifier\") {\n return []\n }\n\n const value = trimToUndefined(meta.value)\n\n if (value === undefined) return []\n\n const declaredType = opfIdentifierTypeScheme(\n metas.find(\n (candidate) =>\n meta.id !== undefined &&\n metaRefinesId(candidate, meta.id) &&\n candidate.property === \"identifier-type\",\n ),\n )\n\n return [\n {\n value,\n scheme: normalizeIdentifierScheme(\n declaredType ?? inferIdentifierScheme(value),\n ),\n },\n ]\n })\n\n return identifiers.length > 0 ? identifiers : undefined\n}\n\n/**\n * Series/collection membership from the metas: EPUB 3\n * `belongs-to-collection` (a `collection-type` refine of `series` selects\n * the series bucket, anything else — including untyped — is a plain\n * collection), with the calibre `calibre:series`/`calibre:series_index`\n * name/content pair as fallback when no EPUB 3 series entry exists.\n */\nconst collectionsFromOpfMetas = (\n metas: ReadonlyArray<OpfMetaEntry>,\n): { series: ResolvedCollection[]; collection: ResolvedCollection[] } => {\n const series: ResolvedCollection[] = []\n const collection: ResolvedCollection[] = []\n\n for (const meta of metas) {\n if (meta.property !== \"belongs-to-collection\" || meta.value === undefined)\n continue\n\n const id = meta.id\n const refining = (property: string): string | undefined =>\n refiningValue(metas, id, property)\n\n const type = refining(\"collection-type\")?.trim().toLowerCase()\n const entry = omitUndefined({\n name: meta.value,\n identifiers: collectionIdentifiers(metas, id),\n position: parseDecimal(refining(\"group-position\")),\n })\n\n if (type === \"series\") series.push(entry)\n else collection.push(entry)\n }\n\n if (series.length === 0) {\n const calibreSeries = metas.find(\n (meta) =>\n meta.name?.toLowerCase() === \"calibre:series\" &&\n meta.content !== undefined,\n )?.content\n\n if (calibreSeries !== undefined) {\n series.push(\n omitUndefined({\n name: calibreSeries,\n position: parseDecimal(\n metas.find(\n (meta) => meta.name?.toLowerCase() === \"calibre:series_index\",\n )?.content,\n ),\n }),\n )\n }\n }\n\n return { series, collection }\n}\n\nconst validatedRenditionFlow = (\n raw: string | undefined,\n): ResolvedMetadata[\"renditionFlow\"] => {\n const v = raw?.trim()\n if (\n v === \"scrolled-continuous\" ||\n v === \"scrolled-doc\" ||\n v === \"paginated\" ||\n v === \"auto\"\n ) {\n return v\n }\n return undefined\n}\n\nconst validatedRenditionSpread = (\n raw: string | undefined,\n): ResolvedMetadata[\"renditionSpread\"] => {\n const v = raw?.trim()\n if (\n v === \"none\" ||\n v === \"landscape\" ||\n v === \"portrait\" ||\n v === \"both\" ||\n v === \"auto\"\n ) {\n return v\n }\n return undefined\n}\n\nexport const resolveOpf = (input: OpfMetadata): ResolvedMetadata => {\n const {\n titles: parsedTitles,\n contributors: parsedContributors,\n publisher,\n description,\n rights,\n languages,\n subjects,\n date,\n renditionLayoutMeta,\n renditionFlowMeta,\n renditionSpreadMeta,\n pageProgressionDirection,\n identifiers: parsedIdentifiers,\n metas,\n // resolved elsewhere in the archive entity, not from the package alone:\n // the reading order, the toc, the cover and the guide\n kind: _kind,\n creators: _creators,\n manifestItems: _manifestItems,\n spineRows: _spineRows,\n spineTocIdref: _spineTocIdref,\n coverHref: _coverHref,\n guide: _guide,\n ...unhandled\n } = input\n\n // Naming every parsed field above is the contract: adding one to\n // `OpfMetadata` fails here until someone decides what becomes of it.\n unhandled satisfies Record<string, never>\n\n const ppd = pageProgressionDirection?.trim().toLowerCase()\n const readingDirection = ppd === \"ltr\" || ppd === \"rtl\" ? ppd : undefined\n\n const rl = renditionLayoutMeta?.trim().toLowerCase()\n const renditionLayout =\n rl === \"reflowable\" || rl === \"pre-paginated\" ? rl : undefined\n\n const contributors = contributorsFromOpf(parsedContributors)\n const titles = titlesFromOpf(parsedTitles, metas)\n const { series, collection } = collectionsFromOpfMetas(metas)\n\n const belongsTo =\n series.length > 0 || collection.length > 0\n ? omitUndefined({\n series: series.length > 0 ? series : undefined,\n collection: collection.length > 0 ? collection : undefined,\n })\n : undefined\n const edition = omitUndefined({\n date: parseW3cDtfDate(date),\n publisher,\n })\n\n return omitUndefined({\n titles: titles.length > 0 ? titles : undefined,\n description,\n publication:\n edition.date !== undefined || edition.publisher !== undefined\n ? { edition }\n : undefined,\n rights,\n languages: languages.length > 0 ? [...languages] : undefined,\n subjects: subjects.length > 0 ? [...subjects] : undefined,\n contributors: contributors.length > 0 ? contributors : undefined,\n readingDirection,\n renditionLayout,\n renditionFlow: validatedRenditionFlow(renditionFlowMeta),\n renditionSpread: validatedRenditionSpread(renditionSpreadMeta),\n identifiers:\n parsedIdentifiers.length > 0\n ? parsedIdentifiers.map((identifier) =>\n omitUndefined({\n value: identifier.value,\n scheme: normalizeIdentifierScheme(\n identifier.scheme ??\n refinedIdentifierType(identifier, metas) ??\n inferIdentifierScheme(identifier.value),\n ),\n unique: identifier.unique,\n }),\n )\n : undefined,\n belongsTo,\n properties: metas.length > 0 ? [...metas] : undefined,\n })\n}\n","import type { AppleMetadata } from \"./metadata/apple/parse.ts\"\nimport { resolveApple } from \"./metadata/apple/resolve.ts\"\nimport type { ComicInfo } from \"./metadata/comicInfo/parse.ts\"\nimport { resolveComicInfo } from \"./metadata/comicInfo/resolve.ts\"\nimport type { KoboMetadata } from \"./metadata/kobo/parse.ts\"\nimport { resolveKobo } from \"./metadata/kobo/resolve.ts\"\nimport type { OpfMetadata } from \"./metadata/opf/parse.ts\"\nimport { resolveOpf } from \"./metadata/opf/resolve.ts\"\nimport type { ResolvedMetadata } from \"./types/resolvedMetadata.ts\"\n\nexport type ResolvedArchiveInput =\n | ComicInfo\n | KoboMetadata\n | AppleMetadata\n | OpfMetadata\n\n/**\n * Normalizes a single parsed source into the cross-format\n * {@link ResolvedMetadata} vocabulary. To combine several sources with the\n * documented precedence rules, use `resolveMetadata` instead.\n */\nexport const resolveArchiveMetadata = (\n input: ResolvedArchiveInput,\n): ResolvedMetadata => {\n if (input.kind === \"comicInfo\") return resolveComicInfo(input)\n if (input.kind === \"kobo\") return resolveKobo(input)\n if (input.kind === \"apple\") return resolveApple(input)\n return resolveOpf(input)\n}\n","import type { AppleMetadata } from \"./metadata/apple/parse.ts\"\nimport { resolveApple } from \"./metadata/apple/resolve.ts\"\nimport type { ComicInfo } from \"./metadata/comicInfo/parse.ts\"\nimport { resolveComicInfo } from \"./metadata/comicInfo/resolve.ts\"\nimport type { KoboMetadata } from \"./metadata/kobo/parse.ts\"\nimport { resolveKobo } from \"./metadata/kobo/resolve.ts\"\nimport type { OpfMetadata } from \"./metadata/opf/parse.ts\"\nimport { resolveOpf } from \"./metadata/opf/resolve.ts\"\nimport type { ResolvedMetadata } from \"./types/resolvedMetadata.ts\"\nimport { omitUndefined } from \"./utils/omitUndefined.ts\"\n\nexport type ResolveMetadataSources = {\n opf?: OpfMetadata\n comicInfo?: ComicInfo\n apple?: AppleMetadata\n kobo?: KoboMetadata\n}\n\n/**\n * Merges the per-source resolved metadata of every parsed source into one\n * {@link ResolvedMetadata}, with explicit precedence:\n *\n * - Descriptive fields (`titles`, `description`, `publication`,\n * `languages`, `subjects`, `contributors`, `belongsTo`):\n * **OPF wins over ComicInfo** — the package document is the publication's\n * own metadata; a ComicInfo sidecar fills the gaps.\n * - `readingDirection`: **ComicInfo wins over OPF** (`Manga` beats\n * `page-progression-direction`). Deliberate: this preserves the behavior\n * the streamer has always had, and for actual comic archives there is no\n * OPF anyway — see the merge tests for the recorded decision.\n * - `renditionLayout`: **OPF explicit → Apple → Kobo**, first defined wins.\n * Kobo ranks last on purpose: in the manifest pipeline it historically\n * ran after the viewport-promotion optimizer, and since promotion only\n * ever rewrites an already-defined layout, \"after promotion\" and \"after\n * OPF/Apple\" are equivalent ranks.\n * - `identifiers` concatenates (OPF first) — different identifier systems\n * coexist rather than compete.\n * - Format-scoped corners (`comicInfo`, `apple`, `kobo`) and single-source\n * fields (`rights`, `properties`, `numberOfPages`, `aggregateRating`…) come\n * from their only producer.\n */\nexport const resolveMetadata = (\n sources: ResolveMetadataSources,\n): ResolvedMetadata => {\n const opf = sources.opf !== undefined ? resolveOpf(sources.opf) : undefined\n const comicInfo =\n sources.comicInfo !== undefined\n ? resolveComicInfo(sources.comicInfo)\n : undefined\n const apple =\n sources.apple !== undefined ? resolveApple(sources.apple) : undefined\n const kobo =\n sources.kobo !== undefined ? resolveKobo(sources.kobo) : undefined\n\n const identifiers = [\n ...(opf?.identifiers ?? []),\n ...(comicInfo?.identifiers ?? []),\n ]\n const edition = omitUndefined({\n date:\n opf?.publication?.edition?.date ?? comicInfo?.publication?.edition?.date,\n publisher:\n opf?.publication?.edition?.publisher ??\n comicInfo?.publication?.edition?.publisher,\n imprint:\n opf?.publication?.edition?.imprint ??\n comicInfo?.publication?.edition?.imprint,\n })\n\n return omitUndefined({\n titles: opf?.titles ?? comicInfo?.titles,\n description: opf?.description ?? comicInfo?.description,\n publication:\n edition.date !== undefined ||\n edition.publisher !== undefined ||\n edition.imprint !== undefined\n ? { edition }\n : undefined,\n rights: opf?.rights,\n languages: opf?.languages ?? comicInfo?.languages,\n subjects: opf?.subjects ?? comicInfo?.subjects,\n contributors: opf?.contributors ?? comicInfo?.contributors,\n readingDirection: comicInfo?.readingDirection ?? opf?.readingDirection,\n renditionLayout:\n opf?.renditionLayout ?? apple?.renditionLayout ?? kobo?.renditionLayout,\n renditionFlow: opf?.renditionFlow,\n renditionSpread: opf?.renditionSpread,\n numberOfPages: comicInfo?.numberOfPages,\n aggregateRating: comicInfo?.aggregateRating,\n identifiers: identifiers.length > 0 ? identifiers : undefined,\n belongsTo: opf?.belongsTo ?? comicInfo?.belongsTo,\n properties: opf?.properties,\n comicInfo: comicInfo?.comicInfo,\n apple: apple?.apple,\n kobo: kobo?.kobo,\n })\n}\n","import { sortByTitleComparator } from \"@prose-reader/shared\"\nimport type { Archive } from \"../archives/types.ts\"\nimport type { ArchiveTocItem } from \"./types.ts\"\n\n/**\n * Derive a TOC from the folder hierarchy of the archive (e.g. a CBZ with one\n * folder per chapter). Each folder becomes an entry pointing to its first\n * file in natural sort order.\n */\nexport const buildTocFromFolders = (archive: Archive): ArchiveTocItem[] => {\n const filesSortedByAlpha = [...archive.records].sort((a, b) =>\n sortByTitleComparator(a.uri, b.uri),\n )\n\n const combineWith = (\n toc: ArchiveTocItem[],\n folder: string,\n subFolders: string[],\n containerHref: string,\n path: string,\n ): ArchiveTocItem[] => {\n const foundEntry = toc.find((entry) => entry.title === folder)\n const [nextFolderCursor, ...nextSubFolders] = subFolders\n\n if (foundEntry) {\n if (nextFolderCursor) {\n return [\n ...toc.filter((entry) => entry !== foundEntry),\n {\n ...foundEntry,\n // combineWith returns the full merged children list already\n contents: combineWith(\n foundEntry.contents,\n nextFolderCursor,\n nextSubFolders,\n containerHref,\n path,\n ),\n } satisfies ArchiveTocItem,\n ]\n }\n\n const previousRegisteredPathWasLonger =\n foundEntry.path.split(\"/\").length > path.split(\"/\").length\n\n if (previousRegisteredPathWasLonger) {\n return [\n ...toc.filter((entry) => entry !== foundEntry),\n {\n ...foundEntry,\n path,\n containerHref,\n } satisfies ArchiveTocItem,\n ]\n }\n\n return toc\n }\n\n if (nextFolderCursor) {\n return [\n ...toc,\n {\n contents: combineWith(\n [],\n nextFolderCursor,\n nextSubFolders,\n containerHref,\n path,\n ),\n containerHref,\n path,\n title: folder,\n },\n ]\n }\n\n return [\n ...toc,\n {\n contents: [],\n containerHref,\n path,\n title: folder,\n },\n ]\n }\n\n return filesSortedByAlpha.reduce<ArchiveTocItem[]>((acc, file) => {\n if (file.dir) return acc\n\n const folders = file.uri.split(\"/\").slice(0, -1)\n const [firstFolder, ...restFolders] = folders\n\n if (!firstFolder) return acc\n\n // encodeURI leaves `#` and `?` untouched (reserved URI delimiters), but in\n // a raw archive filename they are data and would otherwise be parsed as\n // fragment/query once joined onto a base URL.\n const containerHref = encodeURI(file.uri)\n .replace(/#/g, `%23`)\n .replace(/\\?/g, `%3F`)\n .replace(/\\/$/, \"\")\n const path = file.uri.replace(/\\/$/, \"\")\n\n return combineWith(acc, firstFolder, restFolders, containerHref, path)\n }, [])\n}\n","import { XmlElement, XmlTextNode } from \"xmldoc\"\n\nexport const getXmlElementInnerText = (\n node: XmlElement | undefined,\n): string => {\n if (!node) return \"\"\n\n return node.children\n .map((child) => {\n if (child instanceof XmlTextNode) return child.text\n if (child instanceof XmlElement) return getXmlElementInnerText(child)\n return \"\"\n })\n .join(\"\")\n .trim()\n}\n","import { getUriBasePath, urlJoin } from \"@prose-reader/shared\"\nimport { XmlDocument, XmlElement, type XmlNodeBase } from \"xmldoc\"\nimport { readRecordAsText } from \"../archives/readRecordAsText.ts\"\nimport type { Archive } from \"../archives/types.ts\"\nimport type { OpfMetadata } from \"../metadata/opf/parse.ts\"\nimport { getXmlElementInnerText } from \"../utils/getXmlElementInnerText.ts\"\nimport { tokenizeXmlSpaceSeparatedList } from \"../utils/tokenizeXmlSpaceSeparatedList.ts\"\nimport type { ArchiveTocItem } from \"./types.ts\"\n\nconst manifestItemIsNavDocument = (item: {\n readonly properties?: string\n}): boolean => tokenizeXmlSpaceSeparatedList(item.properties).includes(`nav`)\n\n/**\n * @see https://www.w3.org/TR/epub-33/#sec-nav-def-model\n */\nconst extractNavChapter = (\n li: XmlElement,\n { basePath }: { basePath: string },\n) => {\n const chp: ArchiveTocItem = {\n contents: [],\n path: ``,\n containerHref: ``,\n title: ``,\n }\n\n let contentNode = li.childNamed(`span`) || li.childNamed(`a`)\n\n chp.title =\n (contentNode?.attr.title ||\n contentNode?.val.trim() ||\n getXmlElementInnerText(contentNode)) ??\n ``\n\n let node = contentNode?.name\n\n if (node !== `a`) {\n contentNode = li.descendantWithPath(`${node}.a`)\n if (contentNode) {\n node = contentNode.name.toLowerCase()\n }\n }\n\n if (node === `a` && contentNode?.attr.href) {\n chp.path = urlJoin(basePath, contentNode.attr.href)\n chp.containerHref = chp.path\n }\n const sublistNode = li.childNamed(`ol`)\n if (sublistNode) {\n const children = sublistNode.childrenNamed(`li`)\n if (children && children.length > 0) {\n chp.contents = children.map((child) =>\n extractNavChapter(child, { basePath }),\n )\n }\n }\n\n return chp\n}\n\nconst buildTocFromNavDocument = (\n doc: XmlDocument,\n { basePath }: { basePath: string },\n) => {\n const toc: ArchiveTocItem[] = []\n\n let navDataChildren: XmlNodeBase[] | undefined\n\n if (doc.descendantWithPath(`body.nav.ol`)) {\n navDataChildren = doc.descendantWithPath(`body.nav.ol`)?.children\n } else if (doc.descendantWithPath(`body.section.nav.ol`)) {\n navDataChildren = doc.descendantWithPath(`body.section.nav.ol`)?.children\n }\n\n if (navDataChildren && navDataChildren.length > 0) {\n navDataChildren\n .filter(\n (li): li is XmlElement => li instanceof XmlElement && li.name === `li`,\n )\n .forEach((li) => {\n toc.push(extractNavChapter(li, { basePath }))\n })\n }\n\n return toc\n}\n\nexport const resolveTocFromNav = async (\n opf: OpfMetadata,\n archive: Archive,\n): Promise<ArchiveTocItem[] | undefined> => {\n const navItem = opf.manifestItems.find(manifestItemIsNavDocument)\n\n if (navItem?.href) {\n const tocFile = archive.records.find((item) =>\n item.uri.endsWith(navItem.href),\n )\n\n if (tocFile && !tocFile.dir) {\n const doc = new XmlDocument(await readRecordAsText(tocFile))\n\n const tocFileBasePath = getUriBasePath(tocFile.uri)\n\n /**\n * links inside toc.xhtml are relative to the toc.xhtml file,\n * not the opf file anymore\n */\n return buildTocFromNavDocument(doc, { basePath: tocFileBasePath })\n }\n }\n\n return undefined\n}\n","import { getUriBasePath, urlJoin } from \"@prose-reader/shared\"\nimport { XmlDocument, type XmlElement } from \"xmldoc\"\nimport { readRecordAsText } from \"../archives/readRecordAsText.ts\"\nimport type { Archive } from \"../archives/types.ts\"\nimport type { OpfMetadata } from \"../metadata/opf/parse.ts\"\nimport type { ArchiveTocItem } from \"./types.ts\"\n\nconst mapNcxChapter = (\n point: XmlElement,\n { basePath, prefix }: { basePath: string; prefix: string },\n) => {\n const src = point?.childNamed(`${prefix}content`)?.attr.src || ``\n const path = urlJoin(basePath, src)\n\n const out: ArchiveTocItem = {\n title:\n point?.descendantWithPath(`${prefix}navLabel.${prefix}text`)?.val || ``,\n path,\n containerHref: path,\n contents: [],\n }\n const children = point.childrenNamed(`${prefix}navPoint`)\n if (children && children.length > 0) {\n out.contents = children.map((pt) => mapNcxChapter(pt, { basePath, prefix }))\n }\n\n return out\n}\n\nconst buildTocFromNcxDocument = (\n ncxData: XmlDocument,\n { basePath }: { basePath: string },\n) => {\n const toc: ArchiveTocItem[] = []\n\n const rootTagName = ncxData.name\n let prefix = ``\n if (rootTagName.indexOf(`:`) !== -1) {\n prefix = `${rootTagName.split(`:`)[0]}:`\n }\n\n ncxData\n .childNamed(`${prefix}navMap`)\n ?.childrenNamed(`${prefix}navPoint`)\n .forEach((point) => {\n toc.push(mapNcxChapter(point, { basePath, prefix }))\n })\n\n return toc\n}\n\nexport const resolveTocFromNcx = async ({\n opf,\n opfBasePath,\n archive,\n}: {\n opf: OpfMetadata\n opfBasePath: string\n archive: Archive\n}): Promise<ArchiveTocItem[] | undefined> => {\n const ncxId = opf.spineTocIdref\n\n if (ncxId) {\n const ncxItem = opf.manifestItems.find((item) => item.id === ncxId)\n\n if (ncxItem) {\n const ncxPath = `${opfBasePath}${opfBasePath === `` ? `` : `/`}${ncxItem.href}`\n\n const file = archive.records.find((item) => item.uri.endsWith(ncxPath))\n\n if (file && !file.dir) {\n const ncxData = new XmlDocument(await readRecordAsText(file))\n\n /**\n * content src inside the ncx are relative to the ncx file,\n * not the opf file\n */\n return buildTocFromNcxDocument(ncxData, {\n basePath: getUriBasePath(file.uri),\n })\n }\n }\n }\n\n return undefined\n}\n","import type { Archive } from \"../archives/types.ts\"\nimport {\n type ArchiveOpfParsed,\n readArchiveOpf,\n} from \"../metadata/opf/readArchiveOpf.ts\"\nimport { buildTocFromFolders } from \"./folders.ts\"\nimport { resolveTocFromNav } from \"./nav.ts\"\nimport { resolveTocFromNcx } from \"./ncx.ts\"\nimport type { ArchiveTocItem } from \"./types.ts\"\n\n/**\n * Resolve the table of contents of an archive into a generic, container\n * relative JSON structure ({@link ArchiveTocItem}). Consumers never have to\n * deal with the underlying XML (EPUB nav document, NCX) or archive layout.\n *\n * Strategies, in order:\n * - EPUB nav document (`properties=\"nav\"` manifest item)\n * - NCX (`spine@toc` idref)\n * - EPUB-like containers with neither resolve to an explicit empty TOC — the\n * folder layout of an EPUB zip is not a meaningful TOC.\n * - Anything else falls back to the folder hierarchy, or `undefined` when the\n * archive is flat (no folder to derive entries from).\n *\n * Pass `opf` when you already hold the parsed OPF (e.g. during manifest\n * generation) to skip the internal lookup.\n */\nexport const resolveArchiveToc = async (\n archive: Archive,\n { opf }: { opf?: ArchiveOpfParsed } = {},\n): Promise<ArchiveTocItem[] | undefined> => {\n const archiveOpf = opf ?? (await readArchiveOpf(archive))\n\n if (archiveOpf) {\n const tocFromNav = await resolveTocFromNav(archiveOpf.opf, archive)\n\n if (tocFromNav) return tocFromNav\n\n const tocFromNcx = await resolveTocFromNcx({\n opf: archiveOpf.opf,\n opfBasePath: archiveOpf.basePath,\n archive,\n })\n\n if (tocFromNcx) return tocFromNcx\n\n return []\n }\n\n const tocFromFolders = buildTocFromFolders(archive)\n\n return tocFromFolders.length === 0 ? undefined : tocFromFolders\n}\n","import type { Archive } from \"./archives/types.ts\"\nimport { resolveArchiveCover } from \"./cover/resolveArchiveCover.ts\"\nimport { readingOrderDocumentsAllHaveViewport } from \"./layout/scanReadingOrderDocumentsViewport.ts\"\nimport type { AppleMetadata } from \"./metadata/apple/parse.ts\"\nimport { readArchiveApple } from \"./metadata/apple/readArchiveApple.ts\"\nimport type { ComicInfo } from \"./metadata/comicInfo/parse.ts\"\nimport { readArchiveComicInfo } from \"./metadata/comicInfo/readArchiveComicInfo.ts\"\nimport type { KoboMetadata } from \"./metadata/kobo/parse.ts\"\nimport { readArchiveKobo } from \"./metadata/kobo/readArchiveKobo.ts\"\nimport { isArchiveEpub } from \"./metadata/opf/isArchiveEpub.ts\"\nimport type { ArchiveOpfParsed } from \"./metadata/opf/readArchiveOpf.ts\"\nimport { readArchiveOpf } from \"./metadata/opf/readArchiveOpf.ts\"\nimport type { ArchiveReadingOrderItem } from \"./readingOrder/resolveArchiveReadingOrder.ts\"\nimport { resolveArchiveReadingOrder } from \"./readingOrder/resolveArchiveReadingOrder.ts\"\nimport { Report } from \"./report.ts\"\nimport { resolveMetadata } from \"./resolveMetadata.ts\"\nimport { resolveArchiveToc } from \"./toc/resolveArchiveToc.ts\"\nimport type { ArchiveTocItem } from \"./toc/types.ts\"\nimport type { ResolvedMetadata } from \"./types/resolvedMetadata.ts\"\nimport { omitUndefined } from \"./utils/omitUndefined.ts\"\n\n/**\n * Verbatim parser outputs, keyed by source — provenance for the normalized\n * `metadata` and the escape hatch for single-format consumers. Everything in\n * here is also represented, normalized, in `metadata` (the mapping tables\n * next to each resolver enforce it); duplication between the two is the\n * contract, not a smell: `metadata` says what the resolver believes, sources\n * say what the book said, so a wrong mapping opinion is revisable in a\n * release without the raw value ever having left the entity.\n */\nexport type ResolvedArchiveSources = {\n readonly opf?: ArchiveOpfParsed\n readonly comicInfo?: ComicInfo\n readonly apple?: AppleMetadata\n readonly kobo?: KoboMetadata\n}\n\nexport type ResolvedArchiveSourceKind = keyof ResolvedArchiveSources\n\n/**\n * The fully resolved, plain-JSON view of a book container: everything a\n * reading or library app needs to present the publication, with no archive\n * handle attached — structured-clone-able, persistable, cacheable.\n *\n * Everything is container-relative (reading order `uri`s, toc\n * `containerHref`s): the entity carries no serving concern, consumers rebase\n * into their own space (the prose streamer does exactly that to produce its\n * `Manifest`).\n */\nexport type ResolvedArchive = {\n /**\n * Schema version of this entity, for consumers persisting it. Bumped only\n * when the shape or meaning of existing fields changes incompatibly;\n * additive growth (new optional fields, vocabulary extensions) does not\n * bump it.\n */\n readonly version: number\n /**\n * Cross-format normalized metadata (see {@link ResolvedMetadata}),\n * including the resolved `cover` — metadata is resolved for the whole\n * container, not just its descriptive sidecars.\n */\n readonly metadata: ResolvedMetadata\n readonly readingOrder: ArchiveReadingOrderItem[]\n /**\n * Absent when no toc is derivable (a flat, package-less container). An\n * EPUB without a nav document or NCX resolves to an explicit empty list —\n * the folder layout of an EPUB zip is not a meaningful toc.\n */\n readonly toc?: ArchiveTocItem[]\n readonly sources: ResolvedArchiveSources\n /**\n * Sources the container carries but that yielded no parsed value: declared\n * yet broken, which `sources` alone cannot express — an absent source and an\n * unreadable one both read as `undefined` there.\n *\n * Always present (empty when every carried source parsed), whatever the\n * projection: a consumer refusing to accept a corrupt publication should not\n * have to ask for `sources`, nor re-derive the container format from the\n * archive records to tell the two cases apart.\n *\n * ```ts\n * const { metadata, unreadableSources } = await resolveArchive(archive)\n *\n * if (unreadableSources.includes(\"opf\")) {\n * throw new Error(\"Archive carries an OPF package document it cannot parse\")\n * }\n * ```\n *\n * A source counts as unreadable when its document (or, for the package\n * document, any `.opf` record in the container) is there and reading or\n * parsing it did not produce a value — the resolve itself never fails, so\n * this is the only trace left. A projection that reads nothing from the book\n * reports nothing.\n */\n readonly unreadableSources: ResolvedArchiveSourceKind[]\n}\n\n/**\n * Projection tokens are simply the keys of the result. Cost classes:\n *\n * - `metadata`, `sources`: sidecar XML reads (OPF, ComicInfo.xml,\n * Apple/Kobo display options) — cheap. `metadata` also derives the cover:\n * free for an OPF cover, and for a package-less container it computes the\n * reading-order listing (an in-memory records scan) for the first-page\n * fallback.\n * - `readingOrder`: the OPF read at most — cheap.\n * - `toc`: one nav or NCX document read+parse on top — medium.\n * - `version`, `unreadableSources`: free, and always present regardless of\n * projection.\n *\n * The default (`metadata`, `readingOrder`, `toc`) costs O(sidecar files) +\n * one navigation document: anything that reads the whole book is opt-in\n * (see `layoutScan`). `sources` is opt-in too — it rides for free on the\n * reads its neighbors already do.\n */\nexport type ResolveArchiveToken = keyof ResolvedArchive\n\nconst DEFAULT_INCLUDE = [\"metadata\", \"readingOrder\", \"toc\"] as const\n\nexport type ResolveArchiveOptions<\n T extends ReadonlyArray<ResolveArchiveToken>,\n> = {\n /**\n * Projection: which parts of {@link ResolvedArchive} to resolve and\n * return. Defaults to `[\"metadata\", \"readingOrder\", \"toc\"]`. `sources` is\n * deliberately not in the default set — it roughly doubles the persisted\n * entity for provenance most consumers don't need; requesting it is free\n * when it rides along tokens that parse the sidecars anyway.\n */\n include?: T\n /**\n * Effort modifier, not a projection token: reads and XML-parses **every**\n * reading-order document (O(book)) to apply the industry viewport\n * heuristic — an explicitly-reflowable publication whose spine documents\n * all declare `head > meta[name=viewport]` is promoted to `pre-paginated`\n * (metadata and per-item). The refinement is merged into the result here,\n * inside the resolver; consumers never re-implement the merge. Defaults\n * to false, honoring the \"nothing reads the whole book by default\" rule.\n */\n layoutScan?: boolean\n}\n\n/**\n * `failed` separates \"the read threw\" from \"the read found nothing\", which\n * `value === undefined` conflates — the distinction `unreadableSources`\n * reports.\n */\ntype SafeRead<T> = { value: T | undefined; failed: boolean }\n\nconst safeRead = async <T>(\n read: () => Promise<T>,\n source: string,\n): Promise<SafeRead<T>> => {\n try {\n return { value: await read(), failed: false }\n } catch (e) {\n // books in the wild are dirty: a malformed source never fails the\n // resolve, it just doesn't contribute\n Report.error(`resolveArchive: unable to read ${source}`, e)\n\n return { value: undefined, failed: true }\n }\n}\n\n/**\n * The counted page count: one page per pre-paginated *page-like* reading-order\n * item — an image or a fixed-layout document (comics, image archives, fixed\n * layout). Reflowable documents are not pre-paginated, so they never count.\n * Audio and video are pre-paginated discrete media too, but a track or clip is\n * not a page, so they are excluded — an audiobook or video archive has no page\n * count. Absent when nothing page-like is present.\n */\nconst countPages = (\n readingOrder: ArchiveReadingOrderItem[],\n): number | undefined => {\n const isAudioOrVideo = (mediaType: string | undefined): boolean =>\n mediaType?.startsWith(\"audio/\") === true ||\n mediaType?.startsWith(\"video/\") === true\n\n const count = readingOrder.filter(\n (item) =>\n item.renditionLayout === \"pre-paginated\" &&\n !isAudioOrVideo(item.mediaType),\n ).length\n\n return count > 0 ? count : undefined\n}\n\n/**\n * A sidecar reader returns `undefined` only when the container does not carry\n * the document at all, so a throw is the whole broken signal. The package\n * document has one more broken shape: a container carrying an `.opf` record\n * whose package document never reaches the parser (a name the OPF discovery\n * does not match, a directory record). `isArchiveEpub` is the carrier test\n * there, which is exactly what consumers had to reach for themselves.\n */\nconst collectUnreadableSources = ({\n archive,\n opf,\n comicInfo,\n apple,\n kobo,\n}: {\n archive: Archive\n opf: SafeRead<ArchiveOpfParsed | undefined> | undefined\n comicInfo: SafeRead<ComicInfo | undefined> | undefined\n apple: SafeRead<AppleMetadata | undefined> | undefined\n kobo: SafeRead<KoboMetadata | undefined> | undefined\n}): ResolvedArchiveSourceKind[] => {\n const unreadable: ResolvedArchiveSourceKind[] = []\n\n if (opf !== undefined && opf.value === undefined) {\n if (opf.failed || isArchiveEpub(archive)) unreadable.push(\"opf\")\n }\n\n if (comicInfo?.failed) unreadable.push(\"comicInfo\")\n if (apple?.failed) unreadable.push(\"apple\")\n if (kobo?.failed) unreadable.push(\"kobo\")\n\n return unreadable\n}\n\nconst RESOLVED_ARCHIVE_VERSION = 3\n\n/**\n * Resolves a book container into a single enriched, plain-JSON entity:\n * normalized cross-format {@link ResolvedMetadata}, the container-relative\n * reading order and toc, and optionally the verbatim per-source outputs.\n *\n * This is the flagship entry point of the package — parsers and per-source\n * readers stay exported as the advanced layer underneath.\n *\n * ```ts\n * const { metadata, readingOrder, toc } = await resolveArchive(archive)\n *\n * // library-shelf scan: metadata (incl. cover) only, skip the toc read\n * const { metadata } = await resolveArchive(archive, { include: [\"metadata\"] })\n * renderShelfItem({ title: mainTitle(metadata), cover: metadata.cover?.uri })\n *\n * // your own per-source precedence via the raw sources escape hatch\n * // (resolveArchiveMetadata normalizes a single source at a time)\n * const { sources } = await resolveArchive(archive, { include: [\"sources\"] })\n * const opf = sources.opf && resolveArchiveMetadata(sources.opf.opf)\n * const comicInfo = sources.comicInfo && resolveArchiveMetadata(sources.comicInfo)\n * const identifiers = comicInfo?.identifiers ?? opf?.identifiers\n *\n * // reject a publication whose package document is there but broken\n * const { unreadableSources } = await resolveArchive(archive)\n * if (unreadableSources.includes(\"opf\")) throw new Error(\"broken OPF\")\n *\n * // full fidelity, including the O(book) layout refinement\n * const resolved = await resolveArchive(archive, {\n * include: [\"metadata\", \"readingOrder\", \"toc\", \"sources\"],\n * layoutScan: true,\n * })\n * ```\n */\nexport const resolveArchive = async <\n T extends ReadonlyArray<ResolveArchiveToken> = typeof DEFAULT_INCLUDE,\n>(\n archive: Archive,\n options: ResolveArchiveOptions<T> = {},\n): Promise<\n Pick<ResolvedArchive, T[number] | \"version\" | \"unreadableSources\">\n> => {\n const tokens = new Set<ResolveArchiveToken>(\n options.include ?? DEFAULT_INCLUDE,\n )\n const wantsMetadata = tokens.has(\"metadata\")\n const wantsSources = tokens.has(\"sources\")\n const wantsReadingOrder = tokens.has(\"readingOrder\")\n const wantsToc = tokens.has(\"toc\")\n\n // the scan refines metadata AND per-item layouts, so it runs whenever\n // either projection could reflect it — and needs both computed internally\n const runLayoutScan =\n options.layoutScan === true && (wantsMetadata || wantsReadingOrder)\n\n // the package document backs every archive-derived token, but a\n // version-only (or empty) projection returns nothing read from the book —\n // skip the read entirely rather than open and parse the OPF for nothing\n // (runLayoutScan implies metadata or readingOrder, so it is covered)\n const wantsOpf =\n wantsMetadata || wantsSources || wantsReadingOrder || wantsToc\n const opfRead = wantsOpf\n ? await safeRead(() => readArchiveOpf(archive), \"opf\")\n : undefined\n const opf = opfRead?.value\n\n const wantsSidecars = wantsMetadata || wantsSources || runLayoutScan\n const comicInfoRead = wantsSidecars\n ? await safeRead(() => readArchiveComicInfo(archive), \"comicInfo\")\n : undefined\n const comicInfo = comicInfoRead?.value\n const appleRead = wantsSidecars\n ? await safeRead(() => readArchiveApple(archive), \"apple\")\n : undefined\n const apple = appleRead?.value\n const koboRead = wantsSidecars\n ? await safeRead(() => readArchiveKobo(archive), \"kobo\")\n : undefined\n const kobo = koboRead?.value\n\n let metadata =\n wantsMetadata || runLayoutScan\n ? resolveMetadata({ opf: opf?.opf, comicInfo, apple, kobo })\n : undefined\n\n let readingOrder =\n wantsReadingOrder || runLayoutScan\n ? await resolveArchiveReadingOrder(archive, { opf })\n : undefined\n\n if (\n runLayoutScan &&\n metadata !== undefined &&\n readingOrder !== undefined &&\n metadata.renditionLayout === \"reflowable\" &&\n readingOrder.every((item) => item.renditionLayout === \"reflowable\") &&\n (await readingOrderDocumentsAllHaveViewport(archive, readingOrder))\n ) {\n metadata = { ...metadata, renditionLayout: \"pre-paginated\" }\n readingOrder = readingOrder.map((item) => ({\n ...item,\n renditionLayout: \"pre-paginated\" as const,\n }))\n }\n\n // cover and the counted page count are part of the resolved metadata but\n // are container-level derivations (basePath join, first-page fallback, page\n // counting), so they are resolved here where the archive is available and\n // folded into `metadata`. After the scan so both ride the finalized reading\n // order (its layout drives the page count, and the scan can promote it).\n if (wantsMetadata && metadata !== undefined) {\n const order =\n readingOrder ?? (await resolveArchiveReadingOrder(archive, { opf }))\n const cover = await resolveArchiveCover(archive, {\n opf,\n readingOrder: order,\n })\n const numberOfPages = metadata.numberOfPages ?? countPages(order)\n\n metadata = omitUndefined({ ...metadata, cover, numberOfPages })\n }\n\n const toc = wantsToc\n ? (await safeRead(() => resolveArchiveToc(archive, { opf }), \"toc\")).value\n : undefined\n\n const result: {\n -readonly [K in keyof ResolvedArchive]?: ResolvedArchive[K]\n } & Pick<ResolvedArchive, \"version\" | \"unreadableSources\"> = {\n version: RESOLVED_ARCHIVE_VERSION,\n unreadableSources: collectUnreadableSources({\n archive,\n opf: opfRead,\n comicInfo: comicInfoRead,\n apple: appleRead,\n kobo: koboRead,\n }),\n }\n\n if (wantsMetadata && metadata !== undefined) result.metadata = metadata\n if (wantsReadingOrder && readingOrder !== undefined)\n result.readingOrder = readingOrder\n if (wantsToc && toc !== undefined) result.toc = toc\n if (wantsSources)\n result.sources = omitUndefined({ opf, comicInfo, apple, kobo })\n\n // `as`: the runtime projection above mirrors the type-level Pick over the\n // same token set; TypeScript cannot connect Set membership checks to the\n // computed Pick keys.\n return result as Pick<\n ResolvedArchive,\n T[number] | \"version\" | \"unreadableSources\"\n >\n}\n","import type {\n MetadataIdentifier,\n MetadataIdentifierScheme,\n} from \"../types/resolvedMetadata.ts\"\n\n/**\n * Schemes whose catalog addresses a record by URL, so a publication can state\n * the same identifier either explicitly or as a reference link.\n */\nexport type MetadataCatalogScheme = Extract<\n MetadataIdentifierScheme,\n \"GoogleBooks\" | \"OpenLibrary\" | \"ProjectGutenberg\" | \"DOI\"\n>\n\n/**\n * Schemes that say nothing about what assigned the value, which is what makes\n * a reference URL worth reading: the publication stated a link, not a\n * namespace. An explicitly typed identifier of another catalog is left alone.\n */\nconst isUntypedReferenceScheme = (\n scheme: MetadataIdentifierScheme,\n): boolean => {\n const normalized = scheme.trim().toLowerCase()\n\n return (\n normalized === \"url\" || normalized === \"uri\" || normalized === \"unknown\"\n )\n}\n\n/**\n * `new URL` accepts a malformed percent escape that `decodeURIComponent`\n * rejects, and these helpers read authored metadata — a book carrying\n * `https://doi.org/10.1000/%` must decline, not throw.\n */\nconst decodePercentEncoding = (value: string): string | undefined => {\n try {\n return decodeURIComponent(value)\n } catch {\n return undefined\n }\n}\n\nconst httpUrl = (value: string): URL | undefined => {\n try {\n const url = new URL(value.trim())\n\n return url.protocol === \"http:\" || url.protocol === \"https:\"\n ? url\n : undefined\n } catch {\n return undefined\n }\n}\n\nconst urlOnHost = (\n value: string,\n matchesHost: (hostname: string) => boolean,\n): URL | undefined => {\n const url = httpUrl(value)\n\n return url !== undefined && matchesHost(url.hostname.toLowerCase())\n ? url\n : undefined\n}\n\nconst GOOGLE_BOOKS_SITE_HOST =\n /^books\\.google\\.(?:com|[a-z]{2}|com\\.[a-z]{2}|co\\.[a-z]{2})$/i\nconst GOOGLE_BOOKS_API_HOSTS = new Set([\n \"books.googleapis.com\",\n \"www.googleapis.com\",\n])\nconst GOOGLE_BOOKS_API_PATH = /^\\/books\\/v1\\/volumes\\/([^/]+)\\/?$/i\n\nconst googleBooksId = (value: string): string | undefined => {\n const trimmed = value.trim()\n\n return /^[a-z0-9_-]+$/i.test(trimmed) ? trimmed : undefined\n}\n\nconst googleBooksIdFromUrl = (value: string): string | undefined => {\n const url = urlOnHost(\n value,\n (hostname) =>\n GOOGLE_BOOKS_SITE_HOST.test(hostname) ||\n GOOGLE_BOOKS_API_HOSTS.has(hostname),\n )\n\n if (url === undefined) return undefined\n\n if (GOOGLE_BOOKS_API_HOSTS.has(url.hostname.toLowerCase())) {\n const id = GOOGLE_BOOKS_API_PATH.exec(url.pathname)?.[1]\n const decoded = id === undefined ? undefined : decodePercentEncoding(id)\n\n return decoded === undefined ? undefined : googleBooksId(decoded)\n }\n\n return googleBooksId(url.searchParams.get(\"id\") ?? \"\")\n}\n\nconst GUTENBERG_HOSTS = new Set([\"gutenberg.org\", \"www.gutenberg.org\"])\nconst GUTENBERG_PATHS: ReadonlyArray<RegExp> = [\n /^\\/ebooks\\/(\\d+)(?:[./]|$)/i,\n /^\\/files\\/(\\d+)(?:\\/|$)/i,\n /^\\/cache\\/epub\\/(\\d+)(?:\\/|$)/i,\n /^\\/(\\d+)(?:\\/|$)/,\n]\n\nconst projectGutenbergId = (value: string): string | undefined => {\n const trimmed = value.trim()\n\n if (!/^\\d+$/.test(trimmed)) return undefined\n\n const withoutLeadingZeros = trimmed.replace(/^0+(?=\\d)/, \"\")\n\n return withoutLeadingZeros === \"0\" ? undefined : withoutLeadingZeros\n}\n\nconst projectGutenbergIdFromUrl = (value: string): string | undefined => {\n const url = urlOnHost(value, (hostname) => GUTENBERG_HOSTS.has(hostname))\n\n if (url === undefined) return undefined\n\n for (const pattern of GUTENBERG_PATHS) {\n const id = pattern.exec(url.pathname)?.[1]\n\n if (id !== undefined) return projectGutenbergId(id)\n }\n\n return undefined\n}\n\nconst OPEN_LIBRARY_HOSTS = new Set([\"openlibrary.org\", \"www.openlibrary.org\"])\nconst OPEN_LIBRARY_KEY = /^\\/(works|books|authors)\\/(OL\\d+[A-Z])(?:\\/|$)/i\nconst OPEN_LIBRARY_BARE_ID = /^OL\\d+([MWA])$/i\n\nconst OPEN_LIBRARY_COLLECTIONS: Record<string, string> = {\n M: \"books\",\n W: \"works\",\n A: \"authors\",\n}\n\n/**\n * Open Library addresses a record by its key path, which is the value this\n * scheme carries. A bare id is accepted too, since its suffix letter says\n * which collection it belongs to.\n */\nconst openLibraryKey = (value: string): string | undefined => {\n const trimmed = value.trim()\n const key = OPEN_LIBRARY_KEY.exec(trimmed)\n\n if (key?.[1] !== undefined && key[2] !== undefined) {\n return `/${key[1].toLowerCase()}/${key[2].toUpperCase()}`\n }\n\n const bare = OPEN_LIBRARY_BARE_ID.exec(trimmed)\n const collection = OPEN_LIBRARY_COLLECTIONS[bare?.[1]?.toUpperCase() ?? \"\"]\n\n return collection === undefined\n ? undefined\n : `/${collection}/${trimmed.toUpperCase()}`\n}\n\nconst openLibraryKeyFromUrl = (value: string): string | undefined => {\n const url = urlOnHost(value, (hostname) => OPEN_LIBRARY_HOSTS.has(hostname))\n\n return url === undefined ? undefined : openLibraryKey(url.pathname)\n}\n\nconst DOI_HOSTS = new Set([\"doi.org\", \"dx.doi.org\", \"www.doi.org\"])\nconst DOI_NAME = /^10\\.\\d{4,9}\\/\\S+$/\n\nconst doiName = (value: string): string | undefined => {\n const trimmed = value.trim().replace(/^doi:/i, \"\")\n\n return DOI_NAME.test(trimmed) ? trimmed : undefined\n}\n\n/**\n * DOI suffixes are opaque and may contain characters a URL reserves — a `?`\n * would otherwise start a query string and drop the rest of the name. Each\n * path segment is encoded while the separators stay literal, since doi.org\n * addresses a name by path rather than as one encoded blob.\n */\nconst doiUrlPath = (name: string): string =>\n name.split(\"/\").map(encodeURIComponent).join(\"/\")\n\nconst doiNameFromUrl = (value: string): string | undefined => {\n const url = urlOnHost(value, (hostname) => DOI_HOSTS.has(hostname))\n const decoded =\n url === undefined\n ? undefined\n : decodePercentEncoding(url.pathname.replace(/^\\//, \"\"))\n\n return decoded === undefined ? undefined : doiName(decoded)\n}\n\ntype Catalog = {\n readonly scheme: MetadataCatalogScheme\n readonly normalizeValue: (value: string) => string | undefined\n readonly valueFromUrl: (value: string) => string | undefined\n /** Builds the catalog's own URL for an already-normalized value. */\n readonly toUrl: (value: string) => string\n}\n\nconst CATALOGS: ReadonlyArray<Catalog> = [\n {\n scheme: \"GoogleBooks\",\n normalizeValue: googleBooksId,\n valueFromUrl: googleBooksIdFromUrl,\n toUrl: (value) =>\n `https://books.google.com/books?id=${encodeURIComponent(value)}`,\n },\n {\n scheme: \"ProjectGutenberg\",\n normalizeValue: projectGutenbergId,\n valueFromUrl: projectGutenbergIdFromUrl,\n toUrl: (value) => `https://www.gutenberg.org/ebooks/${value}`,\n },\n {\n scheme: \"OpenLibrary\",\n normalizeValue: openLibraryKey,\n valueFromUrl: openLibraryKeyFromUrl,\n toUrl: (value) => `https://openlibrary.org${value}`,\n },\n {\n scheme: \"DOI\",\n normalizeValue: doiName,\n valueFromUrl: doiNameFromUrl,\n toUrl: (value) => `https://doi.org/${doiUrlPath(value)}`,\n },\n]\n\n/**\n * Every scheme whose catalog addresses a record by URL, so a publication can\n * state the identifier either explicitly or as a reference link.\n */\nexport const METADATA_CATALOG_SCHEMES: ReadonlyArray<MetadataCatalogScheme> =\n CATALOGS.map(function toScheme(catalog) {\n return catalog.scheme\n })\n\nconst catalogFor = (scheme: MetadataIdentifierScheme): Catalog | undefined => {\n const schemeKey = scheme.trim().toLowerCase()\n\n return CATALOGS.find(function matchesScheme(catalog) {\n return catalog.scheme.toLowerCase() === schemeKey\n })\n}\n\n/** Whether a scheme's catalog addresses its records by URL. */\nexport const isMetadataCatalogScheme = (\n scheme: MetadataIdentifierScheme,\n): boolean => catalogFor(scheme) !== undefined\n\n/**\n * The catalog identifier a reference URL stands for, or `undefined` when no\n * known catalog addresses that URL.\n *\n * This derives — it does not reinterpret. Resolved metadata keeps the authored\n * URL as a `URL` identifier whatever this returns, so a publication that\n * stated a link still reports a link; deciding to treat that link as the\n * publication's catalog identifier is the caller's call.\n */\nexport const catalogIdentifierFromUrl = (\n url: string,\n): MetadataIdentifier | undefined => {\n for (const catalog of CATALOGS) {\n const value = catalog.valueFromUrl(url)\n\n if (value !== undefined) return { value, scheme: catalog.scheme }\n }\n\n return undefined\n}\n\n/**\n * The catalog's own URL for an identifier — the inverse of\n * {@link catalogIdentifierFromUrl}, for writing a reference into a container\n * whose only identifier slot is a list of links.\n *\n * `undefined` when the scheme has no catalog, or when the value is not one\n * that catalog can address. The pair is checked rather than assumed: a URL is\n * only returned once reading it back yields the value it was built from, so\n * the two directions cannot drift apart unnoticed.\n */\nexport const catalogUrlFromIdentifier = ({\n scheme,\n value,\n}: MetadataIdentifier): string | undefined => {\n const catalog = catalogFor(scheme)\n const normalized = catalog?.normalizeValue(value)\n\n if (catalog === undefined || normalized === undefined) return undefined\n\n const url = catalog.toUrl(normalized)\n\n return catalog.valueFromUrl(url) === normalized ? url : undefined\n}\n\n/**\n * How one catalog recognizes its own identifiers, for the derivation table in\n * `identifierValues.ts` to read.\n */\nexport const catalogRule = (\n scheme: MetadataCatalogScheme,\n): Pick<Catalog, \"normalizeValue\" | \"valueFromUrl\"> => {\n const catalog = CATALOGS.find(function matchesScheme(candidate) {\n return candidate.scheme === scheme\n })\n\n if (catalog === undefined) {\n throw new Error(`No catalog rule for scheme ${scheme}`)\n }\n\n return catalog\n}\n\nexport { isUntypedReferenceScheme }\n","import type {\n MetadataIdentifier,\n MetadataIdentifierScheme,\n} from \"../types/resolvedMetadata.ts\"\nimport { booklandIsbn } from \"./booklandIsbn.ts\"\nimport {\n catalogRule,\n isUntypedReferenceScheme,\n type MetadataCatalogScheme,\n} from \"./catalogIdentifiers.ts\"\nimport { normalizeGtin } from \"./normalizeGtin.ts\"\nimport { normalizeIdentifierScheme } from \"./normalizeIdentifierScheme.ts\"\n\n/**\n * Whether a scheme can carry an ISBN or a GTIN. The two are one namespace in\n * practice: an ISBN-13 *is* a GTIN-13 in the Bookland range, and ComicInfo has\n * no ISBN field at all — it announces one through `GTIN`, which stays labelled\n * `GTIN` because that is what the source said. Filtering on `ISBN` alone\n * therefore misses every comic ISBN, and filtering on `GTIN` alone misses\n * every EPUB one.\n *\n * Resolving metadata canonicalizes spellings, but this also answers for a\n * scheme a caller assembled by hand.\n */\nexport const isIsbnBearingScheme = (\n scheme: MetadataIdentifierScheme,\n): boolean => {\n const normalized = normalizeIdentifierScheme(scheme)\n\n return normalized === \"ISBN\" || normalized === \"GTIN\"\n}\n\n/**\n * Schemes a publication's value can be derived for. Narrower than\n * {@link MetadataIdentifierScheme}: a scheme is here only when knowing it lets\n * us both recognize the identifiers that carry it and canonicalize what they\n * authored. `URL` and `Unknown` describe how a value was stated rather than\n * what assigned it, so there is nothing to derive for them.\n */\nexport type DerivableIdentifierScheme =\n | Extract<MetadataIdentifierScheme, \"ISBN\" | \"GTIN\">\n | MetadataCatalogScheme\n\ntype DerivationRule = {\n /** Whether an identifier of this scheme can carry the derived value. */\n readonly carries: (scheme: MetadataIdentifierScheme) => boolean\n readonly normalizeValue: (value: string) => string | undefined\n /** Reads the value out of an untyped reference URL, for catalogs that have one. */\n readonly valueFromUrl?: (value: string) => string | undefined\n}\n\nconst isbnBearingRule = (\n normalizeValue: (value: string) => string | undefined,\n): DerivationRule => ({ carries: isIsbnBearingScheme, normalizeValue })\n\nconst catalogDerivationRule = (\n scheme: MetadataCatalogScheme,\n): DerivationRule => ({\n carries: (candidate) => normalizeIdentifierScheme(candidate) === scheme,\n ...catalogRule(scheme),\n})\n\nconst DERIVATION_RULES: Record<DerivableIdentifierScheme, DerivationRule> = {\n ISBN: isbnBearingRule(booklandIsbn),\n GTIN: isbnBearingRule(normalizeGtin),\n GoogleBooks: catalogDerivationRule(\"GoogleBooks\"),\n OpenLibrary: catalogDerivationRule(\"OpenLibrary\"),\n ProjectGutenberg: catalogDerivationRule(\"ProjectGutenberg\"),\n DOI: catalogDerivationRule(\"DOI\"),\n}\n\n/**\n * The publication's value for one scheme, canonicalized — the first identifier\n * that can carry it and whose value really is one. Use this rather than\n * filtering `identifiers` yourself: which identifiers can answer for a scheme\n * is rarely just the ones labelled with it.\n *\n * - `ISBN` and `GTIN` read each other, because they are one namespace: an\n * ISBN-13 is a GTIN-13 in the Bookland range, and a comic announces its\n * ISBN through ComicInfo's `GTIN` field. A value the scheme claims is an\n * ISBN but which is some other barcode — a retail EAN scanned off a cover —\n * is not reported as one.\n * - A catalog scheme also reads an untyped reference URL it addresses, since a\n * comic can only state one as a `Web` link. An identifier another catalog\n * already claims is left alone.\n *\n * A derivation that declines to answer loses nothing: the identifier itself\n * stays in `identifiers` exactly as authored.\n */\nexport const identifierValue = (\n identifiers: ReadonlyArray<MetadataIdentifier> | undefined,\n scheme: DerivableIdentifierScheme,\n): string | undefined => {\n const rule = DERIVATION_RULES[scheme]\n\n for (const identifier of identifiers ?? []) {\n if (rule.carries(identifier.scheme)) {\n /**\n * A publication that states the scheme may still write the value as the\n * catalog's own resolver URL, so both spellings answer once the scheme\n * says which catalog assigned it.\n */\n const value =\n rule.normalizeValue(identifier.value) ??\n rule.valueFromUrl?.(identifier.value)\n\n if (value !== undefined) return value\n\n continue\n }\n\n /**\n * Untyped identifiers only answer through a URL: a bare opaque string is\n * never reinterpreted as some catalog's id just because it could be one.\n */\n if (rule.valueFromUrl === undefined) continue\n if (!isUntypedReferenceScheme(identifier.scheme)) continue\n\n const value = rule.valueFromUrl(identifier.value)\n\n if (value !== undefined) return value\n }\n\n return undefined\n}\n","import type { ResolvedMetadata } from \"../types/resolvedMetadata.ts\"\n\n/**\n * The publication's main title: the first title the source states, in\n * document order.\n *\n * EPUB 3 requires reading systems to present the first `dc:title` as the\n * title of the publication, so a `title-type` of `main` on a later element\n * does not displace it — searching the list for that type is the mistake this\n * helper exists to prevent.\n */\nexport const mainTitle = (\n metadata: Pick<ResolvedMetadata, \"titles\"> | undefined,\n): string | undefined => metadata?.titles?.[0]?.value\n"],"mappings":"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"}