fix(opds): show summary as book description, closes #4156 (#4162)

The TypeScript types in `src/types/opds.ts` declared fresh
`Symbol('content')` / `Symbol('summary')` instances. foliate-js's
`opds.js` declared its own distinct ones, and since Symbols are unique
per call, `metadata[SYMBOL.CONTENT]` always returned undefined — even
though the parser had written the value under a same-named Symbol.

This broke silently in 0.11.1 after foliate-js #14 stopped also setting
a plain `content: string` fallback. For OPDS 1.x feeds (e.g. CWA) the
book description lives in `<entry><summary>`, which foliate-js exposes
only via `[SYMBOL.CONTENT]` — so the description vanished.

Re-export the SYMBOL from foliate-js so consumers read the same Symbol
identities the parser writes.
This commit is contained in:
Huang Xin
2026-05-15 01:23:32 +08:00
committed by GitHub
parent 244b3fd994
commit 708e06a46e
2 changed files with 36 additions and 8 deletions
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { getFeed, getPublication } from 'foliate-js/opds.js';
import type { OPDSFeed, OPDSPublication } from '@/types/opds';
import { SYMBOL, type OPDSFeed, type OPDSPublication } from '@/types/opds';
const MIME_XML = 'application/xml';
@@ -222,6 +222,33 @@ describe('OPDS feed parsing', () => {
expect(pub.metadata.updated).toBe('2026-01-15T10:30:00.000Z');
});
// Regression test for https://github.com/readest/readest/issues/4156
// CWA (and other OPDS 1.x servers) place the book description in
// <entry><summary>. foliate-js attaches it under SYMBOL.CONTENT, so the
// SYMBOL exported from @/types/opds must be the same Symbol instance the
// parser writes — otherwise PublicationView reads undefined and the
// description disappears from the book details page.
it('should expose <summary> via SYMBOL.CONTENT for OPDS 1.x feeds', () => {
const opds1Feed = `<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>CWA Library</title>
<entry>
<title>A Book With A Summary</title>
<summary>A short blurb describing the book, set by the OPDS server in &lt;summary&gt;.</summary>
<link rel="http://opds-spec.org/acquisition"
href="book.epub" type="application/epub+zip"/>
</entry>
</feed>`;
const doc = parseXML(opds1Feed);
const feed = getFeed(doc) as OPDSFeed;
expect(feed.publications).toHaveLength(1);
const metadata = feed.publications![0]!.metadata;
const content = metadata[SYMBOL.CONTENT];
expect(content).toBeDefined();
expect(content!.value).toContain('A short blurb describing the book');
});
it('should handle entries without id or updated gracefully', () => {
const minimalFeed = `<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
+8 -7
View File
@@ -1,3 +1,10 @@
// SYMBOL must be re-exported from foliate-js so consumers read the same Symbol
// instances that the parser writes onto publication metadata. Declaring fresh
// `Symbol('content')` calls here would produce different identities, and
// `metadata[SYMBOL.CONTENT]` would silently return undefined — losing the book
// description for OPDS 1.x feeds where it lives in <summary>.
import { SYMBOL as FOLIATE_SYMBOL } from 'foliate-js/opds.js';
export const REL = {
ACQ: 'http://opds-spec.org/acquisition',
FACET: 'http://opds-spec.org/facet',
@@ -11,13 +18,7 @@ export const REL = {
STREAM: 'http://vaemendis.net/opds-pse/stream',
} as const;
const SUMMARY = Symbol('summary');
const CONTENT = Symbol('content');
export const SYMBOL = {
SUMMARY,
CONTENT,
} as const;
export const SYMBOL = FOLIATE_SYMBOL as { SUMMARY: symbol; CONTENT: symbol };
export interface OPDSCatalog {
id: string;