fix(opds): send book descriptions as plain text

Descriptions are stored as HTML but the feed sent them escaped under
type="text", so clients that honour the type, such as Foliate, showed the
tags literally. Flatten them with htmlToPlainText, keeping line breaks.

Fixes #1569
This commit is contained in:
neon
2026-10-11 05:40:33 +00:00
parent 3e993af16b
commit 7e11d39724
2 changed files with 43 additions and 2 deletions
@@ -522,4 +522,40 @@ describe('OpdsService', () => {
expect(xml).toContain(''');
});
});
describe('book descriptions', () => {
function feedFor(description: string | null): string {
return makeService().generateAcquisitionFeed(
'Catalog',
'urn:bookorbit:catalog',
[sampleBook({ description })],
1,
1,
50,
`${BASE}/catalog?page=1&size=50`,
'test-token',
);
}
it('sends an HTML description as plain text with its paragraph and line breaks', () => {
const xml = feedFor('<p>The <strong>first</strong> book &amp; its sequel.</p><p>Line one<br>line two</p>');
expect(xml).toContain('<content type="text">The first book &amp; its sequel.\nLine one\nline two</content>');
expect(xml).not.toContain('&lt;p&gt;');
expect(xml).not.toContain('&lt;strong&gt;');
});
it('keeps a plain-text description and its line breaks unchanged', () => {
const xml = feedFor('Part one.\n\nPart two.');
expect(xml).toContain('<content type="text">Part one.\n\nPart two.</content>');
});
it.each([
['no description', null],
['a description holding only markup', '<p> </p><br>'],
])('sends empty content for %s', (_label, description) => {
expect(feedFor(description)).toContain('<content type="text"/>');
});
});
});
+7 -2
View File
@@ -1,6 +1,7 @@
import { Injectable, Optional } from '@nestjs/common';
import { esc, fileMimeType, OPDS_MIME_ACQ, OPDS_MIME_ATOM, OPDS_MIME_NAV, OPDS_MIME_SEARCH, xmlEl, xmlLink } from './opds-xml.helpers';
import { htmlToPlainText } from '../../common/utils/html-to-text.utils';
import { DeliverySelectionService } from '../book/delivery-selection.service';
import type { OpdsRequestUser } from './opds-auth.guard';
import type { OpdsBookEntry } from './opds-book.service';
@@ -172,8 +173,12 @@ export class OpdsService {
lines.push(` <author>${xmlEl('name', author)}</author>`);
}
if (book.description) {
lines.push(` <content type="text">${esc(book.description)}</content>`);
// Descriptions are stored as HTML. Clients that honour type="text" show markup literally,
// and type="html" would hand unsanitized markup from book files and providers to clients
// that render it, so the feed carries the text alone.
const description = book.description ? htmlToPlainText(book.description, { preserveLineBreaks: true }) : '';
if (description) {
lines.push(` <content type="text">${esc(description)}</content>`);
} else {
lines.push(' <content type="text"/>');
}