Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/__tests__/__snapshots__/atom1.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ exports[`atom 1.0 should generate a valid feed 1`] = `
<title type=\\"html\\"><![CDATA[Hello World]]></title>
<id>https://example.com/hello-world?id=this&amp;that=true</id>
<link href=\\"https://example.com/hello-world?link=sanitized&amp;value=2\\"/>
<link rel=\\"enclosure\\" href=\\"https://example.com/hello-world.jpg\\" type=\\"image/jpg\\" length=\\"12665\\"/>
<link rel=\\"enclosure\\" href=\\"https://example.com/hello-world.jpg\\" type=\\"image/jpeg\\" length=\\"12665\\"/>
<link rel=\\"enclosure\\" href=\\"https://example.com/hello-world.jpg\\" type=\\"image/jpg\\"/>
<updated>2013-07-13T23:00:00.000Z</updated>
<summary type=\\"html\\"><![CDATA[This is an article about Hello World.]]></summary>
Expand Down
114 changes: 114 additions & 0 deletions src/__tests__/atom1.enclosure.test.ts
Original file line number Diff line number Diff line change
@@ -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"`);
});
114 changes: 114 additions & 0 deletions src/__tests__/rss2.enclosure.test.ts
Original file line number Diff line number Diff line change
@@ -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(`<enclosure`);
expect(xml).toContain(`url="${proxied}"`);
expect(xml).toContain(`type="image/jpeg"`);
});

test("RSS2 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.rss2();
expect(xml).toContain(`<enclosure`);
expect(xml).toContain(`url="https://example.com/image.png"`);
expect(xml).toContain(`type="image/png"`); // auto-detected from URL
});

test("RSS2 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.rss2();
expect(xml).toContain(`<enclosure`);
expect(xml).toContain(`url="https://example.com/image.webp"`);
expect(xml).toContain(`type="image/webp"`); // auto-detected from URL
});

test("RSS2 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.rss2();
expect(xml).toContain(`<enclosure`);
expect(xml).toContain(`url="https://example.com/video.mov"`);
expect(xml).toContain(`type="video/quicktime"`);
});

test("RSS2 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.rss2();
expect(xml).toContain(`<enclosure`);
expect(xml).toContain(`url="https://example.com/audio.ogg"`);
expect(xml).toContain(`type="audio/ogg"`);
});
15 changes: 10 additions & 5 deletions src/atom1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,17 +212,22 @@ const formatAuthor = (author: Author) => {
*/
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,
},
};
Expand Down
18 changes: 14 additions & 4 deletions src/rss2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } };
};

/**
Expand Down