@prose-reader/archive-reader 1.344.0 → 1.346.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.
Files changed (48) hide show
  1. package/dist/cover/resolveArchiveCover.d.ts +1 -1
  2. package/dist/index/index.cjs +4 -4
  3. package/dist/index/index.cjs.map +1 -1
  4. package/dist/index/index.js +534 -484
  5. package/dist/index/index.js.map +1 -1
  6. package/dist/index.d.ts +25 -24
  7. package/dist/{apple → metadata/apple}/readArchiveApple.d.ts +1 -1
  8. package/dist/{apple → metadata/apple}/resolve.d.ts +1 -1
  9. package/dist/{comicInfo → metadata/comicInfo}/getArchiveHasComicInfo.d.ts +1 -1
  10. package/dist/{comicInfo → metadata/comicInfo}/readArchiveComicInfo.d.ts +1 -1
  11. package/dist/{comicInfo → metadata/comicInfo}/resolve.d.ts +2 -2
  12. package/dist/{kobo → metadata/kobo}/readArchiveKobo.d.ts +1 -1
  13. package/dist/{kobo → metadata/kobo}/resolve.d.ts +1 -1
  14. package/dist/metadata/opf/getArchiveOpfInfo.d.ts +5 -0
  15. package/dist/{opf → metadata/opf}/getSpineItemFilesFromArchive.d.ts +2 -2
  16. package/dist/{opf → metadata/opf}/isArchiveEpub.d.ts +1 -1
  17. package/dist/{opf → metadata/opf}/parse.d.ts +5 -1
  18. package/dist/{opf → metadata/opf}/readArchiveOpf.d.ts +1 -1
  19. package/dist/{opf → metadata/opf}/resolve.d.ts +2 -2
  20. package/dist/readingOrder/resolveArchiveReadingOrder.d.ts +1 -1
  21. package/dist/resolve.d.ts +4 -4
  22. package/dist/resolveArchive.d.ts +4 -4
  23. package/dist/resolveMetadata.d.ts +4 -4
  24. package/dist/toc/nav.d.ts +1 -1
  25. package/dist/toc/ncx.d.ts +1 -1
  26. package/dist/toc/resolveArchiveToc.d.ts +1 -1
  27. package/dist/types/resolvedMetadata.d.ts +21 -9
  28. package/dist/utils/mainTitle.d.ts +2 -0
  29. package/package.json +3 -3
  30. package/dist/opf/getArchiveOpfInfo.d.ts +0 -5
  31. /package/dist/{apple → metadata/apple}/parse.d.ts +0 -0
  32. /package/dist/{apple → metadata/apple}/parse.test.d.ts +0 -0
  33. /package/dist/{apple → metadata/apple}/readArchiveApple.test.d.ts +0 -0
  34. /package/dist/{apple → metadata/apple}/resolve.test.d.ts +0 -0
  35. /package/dist/{comicInfo → metadata/comicInfo}/manga.d.ts +0 -0
  36. /package/dist/{comicInfo → metadata/comicInfo}/manga.test.d.ts +0 -0
  37. /package/dist/{comicInfo → metadata/comicInfo}/parse.d.ts +0 -0
  38. /package/dist/{comicInfo → metadata/comicInfo}/parse.test.d.ts +0 -0
  39. /package/dist/{comicInfo → metadata/comicInfo}/readArchiveComicInfo.test.d.ts +0 -0
  40. /package/dist/{comicInfo → metadata/comicInfo}/resolve.test.d.ts +0 -0
  41. /package/dist/{kobo → metadata/kobo}/parse.d.ts +0 -0
  42. /package/dist/{kobo → metadata/kobo}/parse.test.d.ts +0 -0
  43. /package/dist/{kobo → metadata/kobo}/readArchiveKobo.test.d.ts +0 -0
  44. /package/dist/{kobo → metadata/kobo}/resolve.test.d.ts +0 -0
  45. /package/dist/{opf → metadata/opf}/parse.test.d.ts +0 -0
  46. /package/dist/{opf → metadata/opf}/resolve.test.d.ts +0 -0
  47. /package/dist/{opf → metadata/opf}/spineItemrefProperties.d.ts +0 -0
  48. /package/dist/{opf → metadata/opf}/spineItemrefProperties.test.d.ts +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/apple/parse.ts","../../src/archives/readRecordAsText.ts","../../src/archives/types.ts","../../src/apple/readArchiveApple.ts","../../src/utils/omitUndefined.ts","../../src/apple/resolve.ts","../../src/archives/createArchiveFromArrayBufferList.ts","../../src/archives/createArchiveFromText.ts","../../src/archives/createArchiveFromUrls.ts","../../src/archives/images.ts","../../src/comicInfo/parse.ts","../../src/comicInfo/getArchiveHasComicInfo.ts","../../src/comicInfo/manga.ts","../../src/comicInfo/readArchiveComicInfo.ts","../../src/utils/normalizeGtin.ts","../../src/utils/normalizeIsbn.ts","../../src/comicInfo/resolve.ts","../../src/opf/getArchiveOpfInfo.ts","../../src/utils/tokenizeXmlSpaceSeparatedList.ts","../../src/opf/spineItemrefProperties.ts","../../src/opf/parse.ts","../../src/opf/readArchiveOpf.ts","../../src/kobo/parse.ts","../../src/utils/toContainerUri.ts","../../src/readingOrder/resolveArchiveReadingOrder.ts","../../src/cover/resolveArchiveCover.ts","../../src/kobo/readArchiveKobo.ts","../../src/kobo/resolve.ts","../../src/layout/scanReadingOrderDocumentsViewport.ts","../../src/opf/getSpineItemFilesFromArchive.ts","../../src/opf/isArchiveEpub.ts","../../src/utils/parseW3cDtfDate.ts","../../src/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"],"sourcesContent":["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 { 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","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 { 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","/**\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","import type {\n ResolvedMetadata,\n ResolvedMetadataHome,\n} from \"../types/resolvedMetadata.ts\"\nimport { omitUndefined } from \"../utils/omitUndefined.ts\"\nimport type { AppleMetadata } from \"./parse.ts\"\n\n/**\n * Losslessness contract: the display options list is copied verbatim into\n * the `apple` corner (`apple.options`), with `fixed-layout` additionally\n * normalized into `apple.fixedLayout` and promoted to `renditionLayout`.\n */\nexport const appleMetadataHomes = {\n displayOptions: \"apple.options\",\n} as const satisfies Record<\n Exclude<keyof AppleMetadata, \"kind\">,\n ResolvedMetadataHome\n>\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 })\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 { 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","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 { 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 {\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","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","const ISBN_CANDIDATE_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 a lax regex scan so publishers that stuff free text\n * around the number still yield a usable value.\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 const match = stripped.match(ISBN_CANDIDATE_PATTERN)\n if (match) return match[0].toUpperCase()\n\n return undefined\n}\n","import type {\n ResolvedCollection,\n ResolvedContributor,\n ResolvedContributorRole,\n ResolvedDate,\n ResolvedMetadata,\n ResolvedMetadataHome,\n} from \"../types/resolvedMetadata.ts\"\nimport { normalizeGtin } from \"../utils/normalizeGtin.ts\"\nimport { normalizeIsbn } from \"../utils/normalizeIsbn.ts\"\nimport { omitUndefined } from \"../utils/omitUndefined.ts\"\nimport type { ComicInfo, ComicInfoKnownField } from \"./parse.ts\"\n\n/**\n * Losslessness contract: where every ComicInfo field lands in\n * {@link ResolvedMetadata} (dotted paths address the `comic` corner and\n * `belongsTo`). Compile-enforced against {@link ComicInfoKnownField} —\n * adding a field to the parser without declaring its home here is a type\n * error. This table doubles as the per-format mapping documentation.\n */\nexport const comicInfoMetadataHomes = {\n AgeRating: \"comic.ageRating\",\n AlternateCount: \"comic.alternateSeries\",\n AlternateNumber: \"comic.alternateSeries\",\n AlternateSeries: \"comic.alternateSeries\",\n BlackAndWhite: \"comic.blackAndWhite\",\n Characters: \"comic.characters\",\n Colorist: \"contributors\",\n CommunityRating: \"comic.communityRating\",\n Count: \"belongsTo.series\",\n CoverArtist: \"contributors\",\n Day: \"publication.edition.date\",\n Editor: \"contributors\",\n Format: \"comic.format\",\n Genre: \"subjects\",\n GTIN: \"identifiers\",\n Imprint: \"publication.edition.imprint\",\n Inker: \"contributors\",\n LanguageISO: \"languages\",\n Letterer: \"contributors\",\n Locations: \"comic.locations\",\n MainCharacterOrTeam: \"comic.mainCharacterOrTeam\",\n Manga: \"readingDirection\",\n Month: \"publication.edition.date\",\n Notes: \"comic.notes\",\n Number: \"belongsTo.series\",\n PageCount: \"numberOfPages\",\n Penciller: \"contributors\",\n Publisher: \"publication.edition.publisher\",\n Review: \"comic.review\",\n ScanInformation: \"comic.scanInformation\",\n Series: \"belongsTo.series\",\n SeriesGroup: \"belongsTo.collection\",\n StoryArc: \"comic.storyArcs\",\n StoryArcNumber: \"comic.storyArcs\",\n Summary: \"description\",\n Tags: \"subjects\",\n Teams: \"comic.teams\",\n Title: \"title\",\n Translator: \"contributors\",\n Volume: \"comic.volume\",\n Web: \"comic.web\",\n Writer: \"contributors\",\n Year: \"publication.edition.date\",\n} as const satisfies Record<ComicInfoKnownField, ResolvedMetadataHome>\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\nconst seriesFromComicInfo = (\n info: ComicInfo,\n): ResolvedCollection | undefined => {\n const name = trimToUndefined(info.Series)\n if (name === undefined) return undefined\n\n return omitUndefined({\n name,\n position: parseDecimal(info.Number),\n total: parseNonNegativeInt(info.Count),\n })\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 comicCornerFromComicInfo = (\n info: ComicInfo,\n): ResolvedMetadata[\"comic\"] => {\n const web = emptyToUndefined(\n (info.Web ?? \"\").split(/\\s+/).filter((token) => token.length > 0),\n )\n\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 communityRating: parseDecimal(info.CommunityRating),\n notes: trimToUndefined(info.Notes),\n review: trimToUndefined(info.Review),\n web,\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 })\n\n return Object.keys(corner).length > 0 ? corner : 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 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 return omitUndefined({\n title: trimToUndefined(info.Title),\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 gtin: normalizeGtin(rawGtin),\n isbn: normalizeIsbn(rawGtin),\n identifiers:\n gtinTrimmed !== undefined\n ? [{ value: gtinTrimmed, scheme: \"GTIN\" }]\n : undefined,\n belongsTo: belongsToFromComicInfo(info),\n comic: comicCornerFromComicInfo(info),\n })\n}\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","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 { 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 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): 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 =\n child.attr[\"opf:scheme\"] ?? child.attr[\"opf:Scheme\"] ?? child.attr.scheme\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 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\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): 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 attributeRole =\n trimmedAttr(child, \"opf:role\") ?? trimmedAttr(child, \"role\")\n const roles = [\n ...(attributeRole !== undefined ? [attributeRole] : []),\n ...refinesFor(\"role\"),\n ]\n\n const fileAs =\n trimmedAttr(child, \"opf:file-as\") ??\n trimmedAttr(child, \"file-as\") ??\n 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 readonly title: string | undefined\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 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 title: string | undefined\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 title = firstTextByLocalName(metadataEl, \"title\")\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)\n identifiers.push(...identifiersFromMetadata(metadataEl, uniqueIdentifierId))\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 title,\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 { 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 * 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 { APPLE_IBOOKS_DISPLAY_OPTIONS_FILENAME } from \"../apple/parse.ts\"\nimport type { Archive, FileRecord } from \"../archives/types.ts\"\nimport { getArchiveFileRecordByUri, isFileRecord } from \"../archives/types.ts\"\nimport { COMIC_INFO_FILENAME } from \"../comicInfo/parse.ts\"\nimport { KOBO_DISPLAY_OPTIONS_FILENAME } from \"../kobo/parse.ts\"\nimport type { ArchiveOpfParsed } from \"../opf/readArchiveOpf.ts\"\nimport { readArchiveOpf } from \"../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 \"../opf/readArchiveOpf.ts\"\nimport { readArchiveOpf } from \"../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 { 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 {\n ResolvedMetadata,\n ResolvedMetadataHome,\n} from \"../types/resolvedMetadata.ts\"\nimport { omitUndefined } from \"../utils/omitUndefined.ts\"\nimport type { KoboMetadata } from \"./parse.ts\"\n\n/** Losslessness contract for the Kobo sidecar fields. */\nexport const koboMetadataHomes = {\n renditionLayout: \"renditionLayout\",\n} as const satisfies Record<\n Exclude<keyof KoboMetadata, \"kind\">,\n ResolvedMetadataHome\n>\n\nexport const resolveKobo = (input: KoboMetadata): ResolvedMetadata =>\n omitUndefined({\n renditionLayout: input.renditionLayout,\n kobo:\n input.renditionLayout !== undefined\n ? { fixedLayout: input.renditionLayout === \"pre-paginated\" }\n : undefined,\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 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","/**\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 ResolvedCollection,\n ResolvedContributor,\n ResolvedContributorRole,\n ResolvedMetadata,\n ResolvedMetadataHome,\n} from \"../types/resolvedMetadata.ts\"\nimport { normalizeGtin } from \"../utils/normalizeGtin.ts\"\nimport { normalizeIsbn } from \"../utils/normalizeIsbn.ts\"\nimport { omitUndefined } from \"../utils/omitUndefined.ts\"\nimport { parseW3cDtfDate } from \"../utils/parseW3cDtfDate.ts\"\nimport type { OpfIdentifier, OpfMetadata, OpfMetaEntry } from \"./parse.ts\"\n\n/**\n * Losslessness contract: where every parsed OPF field lands. Structural\n * fields belong to sibling parts of the resolved archive entity\n * (`readingOrder`, `toc`); `cover` and `guide` are reserved homes not yet\n * promoted out of `sources`. `metas` is the open-world case: the whole list\n * is copied verbatim into `properties`, so unknown vocabularies (calibre\n * columns, vendor namespaces…) are lossless by construction while known\n * properties (`rendition:*`, `belongs-to-collection`, roles…) additionally\n * get promoted into real vocabulary fields.\n */\nexport const opfMetadataHomes = {\n manifestItems: \"readingOrder\",\n spineRows: \"readingOrder\",\n spineTocIdref: \"toc\",\n identifiers: \"identifiers\",\n title: \"title\",\n creators: \"contributors\",\n contributors: \"contributors\",\n publisher: \"publication.edition.publisher\",\n description: \"description\",\n rights: \"rights\",\n languages: \"languages\",\n subjects: \"subjects\",\n date: \"publication.edition.date\",\n coverHref: \"cover\",\n renditionLayoutMeta: \"renditionLayout\",\n renditionFlowMeta: \"renditionFlow\",\n renditionSpreadMeta: \"renditionSpread\",\n pageProgressionDirection: \"readingDirection\",\n guide: \"guide\",\n metas: \"properties\",\n} as const satisfies Record<\n Exclude<keyof OpfMetadata, \"kind\">,\n ResolvedMetadataHome\n>\n\nconst rawIdentifierValueForIsbn = (\n identifiers: ReadonlyArray<OpfIdentifier>,\n): string | undefined => {\n for (const i of identifiers) {\n if (i.scheme !== undefined && i.scheme.trim().toLowerCase() === \"isbn\") {\n if (normalizeIsbn(i.value) !== undefined) return i.value\n }\n }\n\n for (const i of identifiers) {\n if (normalizeIsbn(i.value) !== undefined) return i.value\n }\n\n return undefined\n}\n\nconst inferredIdentifierScheme = (value: string): string | undefined => {\n const trimmed = value.trim()\n\n if (!/^https?:\\/\\//i.test(trimmed)) return undefined\n\n try {\n const url = new URL(trimmed)\n\n return (url.protocol === \"http:\" || url.protocol === \"https:\") &&\n url.hostname.length > 0\n ? \"URL\"\n : undefined\n } catch {\n return undefined\n }\n}\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 = (input: OpfMetadata): ResolvedContributor[] =>\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 input.contributors.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 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\nconst normalizedIdentifierType = (\n meta: OpfMetaEntry | 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\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 normalizedIdentifierType(\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\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 id === undefined\n ? undefined\n : metas.find(\n (candidate) =>\n metaRefinesId(candidate, id) &&\n candidate.property === property &&\n candidate.value !== undefined,\n )?.value\n\n const type = refining(\"collection-type\")?.trim().toLowerCase()\n const entry = omitUndefined({\n name: meta.value,\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 ppd = input.pageProgressionDirection?.trim().toLowerCase()\n const readingDirection = ppd === \"ltr\" || ppd === \"rtl\" ? ppd : undefined\n\n const rl = input.renditionLayoutMeta?.trim().toLowerCase()\n const renditionLayout =\n rl === \"reflowable\" || rl === \"pre-paginated\" ? rl : undefined\n\n const rawIsbn = rawIdentifierValueForIsbn(input.identifiers)\n const contributors = contributorsFromOpf(input)\n const { series, collection } = collectionsFromOpfMetas(input.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(input.date),\n publisher: input.publisher,\n })\n\n return omitUndefined({\n title: input.title,\n description: input.description,\n publication:\n edition.date !== undefined || edition.publisher !== undefined\n ? { edition }\n : undefined,\n rights: input.rights,\n languages: input.languages.length > 0 ? [...input.languages] : undefined,\n subjects: input.subjects.length > 0 ? [...input.subjects] : undefined,\n contributors: contributors.length > 0 ? contributors : undefined,\n readingDirection,\n renditionLayout,\n renditionFlow: validatedRenditionFlow(input.renditionFlowMeta),\n renditionSpread: validatedRenditionSpread(input.renditionSpreadMeta),\n gtin: normalizeGtin(rawIsbn),\n isbn: normalizeIsbn(rawIsbn),\n identifiers:\n input.identifiers.length > 0\n ? input.identifiers.map((identifier) =>\n omitUndefined({\n value: identifier.value,\n scheme:\n identifier.scheme ??\n refinedIdentifierType(identifier, input.metas) ??\n inferredIdentifierScheme(identifier.value),\n unique: identifier.unique,\n }),\n )\n : undefined,\n belongsTo,\n properties: input.metas.length > 0 ? [...input.metas] : undefined,\n })\n}\n","import type { AppleMetadata } from \"./apple/parse.ts\"\nimport { resolveApple } from \"./apple/resolve.ts\"\nimport type { ComicInfo } from \"./comicInfo/parse.ts\"\nimport { resolveComicInfo } from \"./comicInfo/resolve.ts\"\nimport type { KoboMetadata } from \"./kobo/parse.ts\"\nimport { resolveKobo } from \"./kobo/resolve.ts\"\nimport type { OpfMetadata } from \"./opf/parse.ts\"\nimport { resolveOpf } from \"./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 \"./apple/parse.ts\"\nimport { resolveApple } from \"./apple/resolve.ts\"\nimport type { ComicInfo } from \"./comicInfo/parse.ts\"\nimport { resolveComicInfo } from \"./comicInfo/resolve.ts\"\nimport type { KoboMetadata } from \"./kobo/parse.ts\"\nimport { resolveKobo } from \"./kobo/resolve.ts\"\nimport type { OpfMetadata } from \"./opf/parse.ts\"\nimport { resolveOpf } from \"./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 (`title`, `description`, `publication`, `languages`,\n * `subjects`, `contributors`, `belongsTo`, `gtin`/`isbn`):\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 (`comic`, `apple`, `kobo`) and single-source\n * fields (`rights`, `properties`, `numberOfPages`…) come from\n * 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 title: opf?.title ?? comicInfo?.title,\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 gtin: opf?.gtin ?? comicInfo?.gtin,\n isbn: opf?.isbn ?? comicInfo?.isbn,\n identifiers: identifiers.length > 0 ? identifiers : undefined,\n belongsTo: opf?.belongsTo ?? comicInfo?.belongsTo,\n properties: opf?.properties,\n comic: comicInfo?.comic,\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 \"../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 \"../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 { type ArchiveOpfParsed, readArchiveOpf } from \"../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 { AppleMetadata } from \"./apple/parse.ts\"\nimport { readArchiveApple } from \"./apple/readArchiveApple.ts\"\nimport type { Archive } from \"./archives/types.ts\"\nimport type { ComicInfo } from \"./comicInfo/parse.ts\"\nimport { readArchiveComicInfo } from \"./comicInfo/readArchiveComicInfo.ts\"\nimport { resolveArchiveCover } from \"./cover/resolveArchiveCover.ts\"\nimport type { KoboMetadata } from \"./kobo/parse.ts\"\nimport { readArchiveKobo } from \"./kobo/readArchiveKobo.ts\"\nimport { readingOrderDocumentsAllHaveViewport } from \"./layout/scanReadingOrderDocumentsViewport.ts\"\nimport { isArchiveEpub } from \"./opf/isArchiveEpub.ts\"\nimport type { ArchiveOpfParsed } from \"./opf/readArchiveOpf.ts\"\nimport { readArchiveOpf } from \"./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 = 2\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: metadata.title, 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 isbn = comicInfo?.isbn ?? opf?.isbn // ComicInfo wins, your call\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"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","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/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/apple/resolve.ts","../../src/metadata/comicInfo/getArchiveHasComicInfo.ts","../../src/metadata/comicInfo/manga.ts","../../src/metadata/comicInfo/readArchiveComicInfo.ts","../../src/metadata/comicInfo/resolve.ts","../../src/metadata/kobo/readArchiveKobo.ts","../../src/metadata/kobo/resolve.ts","../../src/metadata/opf/getSpineItemFilesFromArchive.ts","../../src/metadata/opf/isArchiveEpub.ts","../../src/utils/normalizeGtin.ts","../../src/utils/normalizeIsbn.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/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","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 { 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): 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 =\n child.attr[\"opf:scheme\"] ?? child.attr[\"opf:Scheme\"] ?? child.attr.scheme\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\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): 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 attributeRole =\n trimmedAttr(child, \"opf:role\") ?? trimmedAttr(child, \"role\")\n const roles = [\n ...(attributeRole !== undefined ? [attributeRole] : []),\n ...refinesFor(\"role\"),\n ]\n\n const fileAs =\n trimmedAttr(child, \"opf:file-as\") ??\n trimmedAttr(child, \"file-as\") ??\n 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 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 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)\n identifiers.push(...identifiersFromMetadata(metadataEl, uniqueIdentifierId))\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 type {\n ResolvedMetadata,\n ResolvedMetadataHome,\n} from \"../../types/resolvedMetadata.ts\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport type { AppleMetadata } from \"./parse.ts\"\n\n/**\n * Losslessness contract: the display options list is copied verbatim into\n * the `apple` corner (`apple.options`), with `fixed-layout` additionally\n * normalized into `apple.fixedLayout` and promoted to `renditionLayout`.\n */\nexport const appleMetadataHomes = {\n displayOptions: \"apple.options\",\n} as const satisfies Record<\n Exclude<keyof AppleMetadata, \"kind\">,\n ResolvedMetadataHome\n>\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 })\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 {\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 type {\n ResolvedCollection,\n ResolvedContributor,\n ResolvedContributorRole,\n ResolvedDate,\n ResolvedMetadata,\n ResolvedMetadataHomes,\n ResolvedMetadataIdentifier,\n} from \"../../types/resolvedMetadata.ts\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport type { ComicInfo, ComicInfoKnownField } from \"./parse.ts\"\n\n/**\n * Losslessness contract: where every ComicInfo field lands in\n * {@link ResolvedMetadata} (dotted paths address the `comic` corner and\n * `belongsTo`). Compile-enforced against {@link ComicInfoKnownField} —\n * adding a field to the parser without declaring its home here is a type\n * error. This table doubles as the per-format mapping documentation.\n */\nexport const comicInfoMetadataHomes = {\n AgeRating: \"comic.ageRating\",\n AlternateCount: \"comic.alternateSeries\",\n AlternateNumber: \"comic.alternateSeries\",\n AlternateSeries: \"comic.alternateSeries\",\n BlackAndWhite: \"comic.blackAndWhite\",\n Characters: \"comic.characters\",\n Colorist: \"contributors\",\n CommunityRating: \"comic.communityRating\",\n Count: \"belongsTo.series\",\n CoverArtist: \"contributors\",\n Day: \"publication.edition.date\",\n Editor: \"contributors\",\n Format: \"comic.format\",\n Genre: \"subjects\",\n GTIN: \"identifiers\",\n Imprint: \"publication.edition.imprint\",\n Inker: \"contributors\",\n LanguageISO: \"languages\",\n Letterer: \"contributors\",\n Locations: \"comic.locations\",\n MainCharacterOrTeam: \"comic.mainCharacterOrTeam\",\n Manga: \"readingDirection\",\n Month: \"publication.edition.date\",\n Notes: \"comic.notes\",\n Number: \"belongsTo.series\",\n PageCount: \"numberOfPages\",\n Penciller: \"contributors\",\n Publisher: \"publication.edition.publisher\",\n Review: \"comic.review\",\n ScanInformation: \"comic.scanInformation\",\n Series: \"belongsTo.series\",\n SeriesGroup: \"belongsTo.collection\",\n StoryArc: \"comic.storyArcs\",\n StoryArcNumber: \"comic.storyArcs\",\n Summary: \"description\",\n Tags: \"subjects\",\n Teams: \"comic.teams\",\n Title: \"titles\",\n Translator: \"contributors\",\n Volume: \"comic.volume\",\n Web: \"comic.web\",\n Writer: \"contributors\",\n Year: \"publication.edition.date\",\n} as const satisfies Record<ComicInfoKnownField, ResolvedMetadataHomes>\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[\"comic\"] => {\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 communityRating: parseDecimal(info.CommunityRating),\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 })\n\n return Object.keys(corner).length > 0 ? corner : 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 identifiers: emptyToUndefined(identifiers),\n belongsTo: belongsToFromComicInfo(info),\n comic: comicCornerFromComicInfo(info, web),\n })\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 {\n ResolvedMetadata,\n ResolvedMetadataHome,\n} from \"../../types/resolvedMetadata.ts\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport type { KoboMetadata } from \"./parse.ts\"\n\n/** Losslessness contract for the Kobo sidecar fields. */\nexport const koboMetadataHomes = {\n renditionLayout: \"renditionLayout\",\n} as const satisfies Record<\n Exclude<keyof KoboMetadata, \"kind\">,\n ResolvedMetadataHome\n>\n\nexport const resolveKobo = (input: KoboMetadata): ResolvedMetadata =>\n omitUndefined({\n renditionLayout: input.renditionLayout,\n kobo:\n input.renditionLayout !== undefined\n ? { fixedLayout: input.renditionLayout === \"pre-paginated\" }\n : undefined,\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","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","const ISBN_CANDIDATE_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 a lax regex scan so publishers that stuff free text\n * around the number still yield a usable value.\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 const match = stripped.match(ISBN_CANDIDATE_PATTERN)\n if (match) return match[0].toUpperCase()\n\n return undefined\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 ResolvedMetadataHomes,\n ResolvedTitle,\n} from \"../../types/resolvedMetadata.ts\"\nimport { normalizeGtin } from \"../../utils/normalizeGtin.ts\"\nimport { normalizeIsbn } from \"../../utils/normalizeIsbn.ts\"\nimport { omitUndefined } from \"../../utils/omitUndefined.ts\"\nimport { parseW3cDtfDate } from \"../../utils/parseW3cDtfDate.ts\"\nimport type { OpfIdentifier, OpfMetadata, OpfMetaEntry } from \"./parse.ts\"\n\n/**\n * Losslessness contract: where every parsed OPF field lands. Structural\n * fields belong to sibling parts of the resolved archive entity\n * (`readingOrder`, `toc`); `cover` and `guide` are reserved homes not yet\n * promoted out of `sources`. `metas` is the open-world case: the whole list\n * is copied verbatim into `properties`, so unknown vocabularies (calibre\n * columns, vendor namespaces…) are lossless by construction while known\n * properties (`rendition:*`, `belongs-to-collection`, roles…) additionally\n * get promoted into real vocabulary fields.\n */\nexport const opfMetadataHomes = {\n manifestItems: \"readingOrder\",\n spineRows: \"readingOrder\",\n spineTocIdref: \"toc\",\n identifiers: \"identifiers\",\n titles: \"titles\",\n creators: \"contributors\",\n contributors: \"contributors\",\n publisher: \"publication.edition.publisher\",\n description: \"description\",\n rights: \"rights\",\n languages: \"languages\",\n subjects: \"subjects\",\n date: \"publication.edition.date\",\n coverHref: \"cover\",\n renditionLayoutMeta: \"renditionLayout\",\n renditionFlowMeta: \"renditionFlow\",\n renditionSpreadMeta: \"renditionSpread\",\n pageProgressionDirection: \"readingDirection\",\n guide: \"guide\",\n metas: \"properties\",\n} as const satisfies Record<\n Exclude<keyof OpfMetadata, \"kind\">,\n ResolvedMetadataHomes\n>\n\nconst inferredIdentifierScheme = (value: string): string => {\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 const isbn = normalizeIsbn(trimmed)\n\n if (isbn !== undefined && (isbn.length === 10 || /^97[89]/.test(isbn))) {\n return \"ISBN\"\n }\n if (normalizeGtin(trimmed) !== undefined) return \"GTIN\"\n\n return \"Unknown\"\n}\n\nconst normalizedIdentifierScheme = (scheme: string): string => {\n switch (scheme.trim().toLowerCase()) {\n case \"isbn\":\n return \"ISBN\"\n case \"gtin\":\n return \"GTIN\"\n case \"doi\":\n return \"DOI\"\n case \"googlebooks\":\n return \"GoogleBooks\"\n case \"openlibrary\":\n return \"OpenLibrary\"\n case \"projectgutenberg\":\n return \"ProjectGutenberg\"\n case \"url\":\n return \"URL\"\n case \"unknown\":\n return \"Unknown\"\n default:\n return scheme.trim()\n }\n}\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 = (input: OpfMetadata): ResolvedContributor[] =>\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 input.contributors.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 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\nconst normalizedIdentifierType = (\n meta: OpfMetaEntry | 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\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 normalizedIdentifierType(\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 = (input: OpfMetadata): ResolvedTitle[] =>\n input.titles.map((title) =>\n omitUndefined({\n value: title.value,\n type: trimToUndefined(\n refiningValue(input.metas, title.id, \"title-type\"),\n )?.toLowerCase(),\n displaySeq: parseNonNegativeInt(\n refiningValue(input.metas, title.id, \"display-seq\"),\n ),\n sortAs: trimToUndefined(refiningValue(input.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 = normalizedIdentifierType(\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: normalizedIdentifierScheme(\n declaredType ?? inferredIdentifierScheme(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 ppd = input.pageProgressionDirection?.trim().toLowerCase()\n const readingDirection = ppd === \"ltr\" || ppd === \"rtl\" ? ppd : undefined\n\n const rl = input.renditionLayoutMeta?.trim().toLowerCase()\n const renditionLayout =\n rl === \"reflowable\" || rl === \"pre-paginated\" ? rl : undefined\n\n const contributors = contributorsFromOpf(input)\n const titles = titlesFromOpf(input)\n const { series, collection } = collectionsFromOpfMetas(input.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(input.date),\n publisher: input.publisher,\n })\n\n return omitUndefined({\n titles: titles.length > 0 ? titles : undefined,\n description: input.description,\n publication:\n edition.date !== undefined || edition.publisher !== undefined\n ? { edition }\n : undefined,\n rights: input.rights,\n languages: input.languages.length > 0 ? [...input.languages] : undefined,\n subjects: input.subjects.length > 0 ? [...input.subjects] : undefined,\n contributors: contributors.length > 0 ? contributors : undefined,\n readingDirection,\n renditionLayout,\n renditionFlow: validatedRenditionFlow(input.renditionFlowMeta),\n renditionSpread: validatedRenditionSpread(input.renditionSpreadMeta),\n identifiers:\n input.identifiers.length > 0\n ? input.identifiers.map((identifier) =>\n omitUndefined({\n value: identifier.value,\n scheme: normalizedIdentifierScheme(\n identifier.scheme ??\n refinedIdentifierType(identifier, input.metas) ??\n inferredIdentifierScheme(identifier.value),\n ),\n unique: identifier.unique,\n }),\n )\n : undefined,\n belongsTo,\n properties: input.metas.length > 0 ? [...input.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 (`comic`, `apple`, `kobo`) and single-source\n * fields (`rights`, `properties`, `numberOfPages`…) come from\n * 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 identifiers: identifiers.length > 0 ? identifiers : undefined,\n belongsTo: opf?.belongsTo ?? comicInfo?.belongsTo,\n properties: opf?.properties,\n comic: comicInfo?.comic,\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 { 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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"}