diff --git a/src/__tests__/__snapshots__/atom1.spec.ts.snap b/src/__tests__/__snapshots__/atom1.spec.ts.snap index a5a080a..7d8775e 100644 --- a/src/__tests__/__snapshots__/atom1.spec.ts.snap +++ b/src/__tests__/__snapshots__/atom1.spec.ts.snap @@ -29,7 +29,7 @@ exports[`atom 1.0 should generate a valid feed 1`] = ` <![CDATA[Hello World]]> https://example.com/hello-world?id=this&that=true - + 2013-07-13T23:00:00.000Z diff --git a/src/__tests__/atom1.enclosure.test.ts b/src/__tests__/atom1.enclosure.test.ts new file mode 100644 index 0000000..560acba --- /dev/null +++ b/src/__tests__/atom1.enclosure.test.ts @@ -0,0 +1,114 @@ +import { Feed } from "../feed"; + +test("Atom uses explicit enclosure.type when provided", () => { + const feed = new Feed({ + title: "t", + id: "https://example.com", + link: "https://example.com", + copyright: "c", + }); + + // Proxy URL with no file extension -> auto-detection would fail + const proxied = "https://wsrv.nl/?url=https%3A%2F%2Fexample.com%2Fimg.jpg&w=1000"; + + feed.addItem({ + title: "post", + id: "https://example.com/p", + link: "https://example.com/p", + date: new Date("2025-01-01"), + image: { url: proxied, type: "image/jpeg" }, // <— explicit type + }); + + const xml = feed.atom1(); + expect(xml).toContain(`rel="enclosure"`); + expect(xml).toContain(`href="${proxied}"`); + expect(xml).toContain(`type="image/jpeg"`); +}); + +test("Atom falls back to auto-detection when no type provided", () => { + const feed = new Feed({ + title: "t", + id: "https://example.com", + link: "https://example.com", + copyright: "c", + }); + + feed.addItem({ + title: "post", + id: "https://example.com/p", + link: "https://example.com/p", + date: new Date("2025-01-01"), + image: { url: "https://example.com/image.png", length: 12345 }, // no explicit type + }); + + const xml = feed.atom1(); + expect(xml).toContain(`rel="enclosure"`); + expect(xml).toContain(`href="https://example.com/image.png"`); + expect(xml).toContain(`type="image/png"`); // auto-detected from URL +}); + +test("Atom handles empty string type by falling back to auto-detection", () => { + const feed = new Feed({ + title: "t", + id: "https://example.com", + link: "https://example.com", + copyright: "c", + }); + + feed.addItem({ + title: "post", + id: "https://example.com/p", + link: "https://example.com/p", + date: new Date("2025-01-01"), + image: { url: "https://example.com/image.webp", type: "" }, // empty type + }); + + const xml = feed.atom1(); + expect(xml).toContain(`rel="enclosure"`); + expect(xml).toContain(`href="https://example.com/image.webp"`); + expect(xml).toContain(`type="image/webp"`); // auto-detected from URL +}); + +test("Atom handles video enclosures with explicit type", () => { + const feed = new Feed({ + title: "t", + id: "https://example.com", + link: "https://example.com", + copyright: "c", + }); + + feed.addItem({ + title: "post", + id: "https://example.com/p", + link: "https://example.com/p", + date: new Date("2025-01-01"), + video: { url: "https://example.com/video.mov", type: "video/quicktime" }, + }); + + const xml = feed.atom1(); + expect(xml).toContain(`rel="enclosure"`); + expect(xml).toContain(`href="https://example.com/video.mov"`); + expect(xml).toContain(`type="video/quicktime"`); +}); + +test("Atom handles audio enclosures with explicit type", () => { + const feed = new Feed({ + title: "t", + id: "https://example.com", + link: "https://example.com", + copyright: "c", + }); + + feed.addItem({ + title: "post", + id: "https://example.com/p", + link: "https://example.com/p", + date: new Date("2025-01-01"), + audio: { url: "https://example.com/audio.ogg", type: "audio/ogg" }, + }); + + const xml = feed.atom1(); + expect(xml).toContain(`rel="enclosure"`); + expect(xml).toContain(`href="https://example.com/audio.ogg"`); + expect(xml).toContain(`type="audio/ogg"`); +}); diff --git a/src/__tests__/rss2.enclosure.test.ts b/src/__tests__/rss2.enclosure.test.ts new file mode 100644 index 0000000..cd56c7d --- /dev/null +++ b/src/__tests__/rss2.enclosure.test.ts @@ -0,0 +1,114 @@ +import { Feed } from "../feed"; + +test("RSS2 uses explicit enclosure.type when provided", () => { + const feed = new Feed({ + title: "t", + id: "https://example.com", + link: "https://example.com", + copyright: "c", + }); + + // Proxy URL with no file extension -> auto-detection would fail + const proxied = "https://wsrv.nl/?url=https%3A%2F%2Fexample.com%2Fimg.jpg&w=1000"; + + feed.addItem({ + title: "post", + id: "https://example.com/p", + link: "https://example.com/p", + date: new Date("2025-01-01"), + image: { url: proxied, type: "image/jpeg" }, // <— explicit type + }); + + const xml = feed.rss2(); + expect(xml).toContain(` { + const feed = new Feed({ + title: "t", + id: "https://example.com", + link: "https://example.com", + copyright: "c", + }); + + feed.addItem({ + title: "post", + id: "https://example.com/p", + link: "https://example.com/p", + date: new Date("2025-01-01"), + image: { url: "https://example.com/image.png", length: 12345 }, // no explicit type + }); + + const xml = feed.rss2(); + expect(xml).toContain(` { + const feed = new Feed({ + title: "t", + id: "https://example.com", + link: "https://example.com", + copyright: "c", + }); + + feed.addItem({ + title: "post", + id: "https://example.com/p", + link: "https://example.com/p", + date: new Date("2025-01-01"), + image: { url: "https://example.com/image.webp", type: "" }, // empty type + }); + + const xml = feed.rss2(); + expect(xml).toContain(` { + const feed = new Feed({ + title: "t", + id: "https://example.com", + link: "https://example.com", + copyright: "c", + }); + + feed.addItem({ + title: "post", + id: "https://example.com/p", + link: "https://example.com/p", + date: new Date("2025-01-01"), + video: { url: "https://example.com/video.mov", type: "video/quicktime" }, + }); + + const xml = feed.rss2(); + expect(xml).toContain(` { + const feed = new Feed({ + title: "t", + id: "https://example.com", + link: "https://example.com", + copyright: "c", + }); + + feed.addItem({ + title: "post", + id: "https://example.com/p", + link: "https://example.com/p", + date: new Date("2025-01-01"), + audio: { url: "https://example.com/audio.ogg", type: "audio/ogg" }, + }); + + const xml = feed.rss2(); + expect(xml).toContain(` { */ const formatEnclosure = (enclosure: string | Enclosure, mimeCategory = "image") => { if (typeof enclosure === "string") { - const type = new URL(enclosure).pathname.split(".").slice(-1)[0]; - return { _attributes: { rel: "enclosure", href: enclosure, type: `${mimeCategory}/${type}` } }; + const detectedType = new URL(enclosure).pathname.split(".").slice(-1)[0]; + return { _attributes: { rel: "enclosure", href: enclosure, type: `${mimeCategory}/${detectedType}` } }; + } + // For object enclosures, respect the explicit type if provided. + // Otherwise fall back to MIME category plus detected extension. + let type: string | undefined = enclosure.type; + if (!type || type.trim() === "") { + const detectedType = new URL(enclosure.url).pathname.split(".").slice(-1)[0]; + type = `${mimeCategory}/${detectedType}`; } - - const type = new URL(enclosure.url).pathname.split(".").slice(-1)[0]; return { _attributes: { rel: "enclosure", href: enclosure.url, title: enclosure.title, - type: `${mimeCategory}/${type}`, + type, length: enclosure.length, }, }; diff --git a/src/rss2.ts b/src/rss2.ts index 397a1ff..5b42d17 100644 --- a/src/rss2.ts +++ b/src/rss2.ts @@ -264,12 +264,22 @@ export default (ins: Feed) => { */ const formatEnclosure = (enclosure: string | Enclosure, mimeCategory = "image") => { if (typeof enclosure === "string") { - const type = new URL(sanitize(enclosure)!).pathname.split(".").slice(-1)[0]; - return { _attributes: { url: enclosure, length: 0, type: `${mimeCategory}/${type}` } }; + const detectedType = new URL(sanitize(enclosure)!).pathname.split(".").slice(-1)[0]; + return { _attributes: { url: enclosure, length: 0, type: `${mimeCategory}/${detectedType}` } }; } - const type = new URL(sanitize(enclosure.url)!).pathname.split(".").slice(-1)[0]; - return { _attributes: { length: 0, type: `${mimeCategory}/${type}`, ...enclosure } }; + // For object enclosures, respect the explicit type if provided. + // Otherwise fall back to MIME category plus detected extension. + let type: string | undefined = enclosure.type; + if (!type || type.trim() === "") { + const detectedType = new URL(sanitize(enclosure.url)!).pathname.split(".").slice(-1)[0]; + type = `${mimeCategory}/${detectedType}`; + } + + // Create the attributes, ensuring computed type takes precedence over enclosure.type + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { type: _, ...otherEnclosureProps } = enclosure; + return { _attributes: { length: 0, type, ...otherEnclosureProps } }; }; /**