import { describe, expect, it } from "vitest"; import { itemDescription, rssItem, toRss } from "../src/rss.js"; // shaped like diigo's export (field layout observed 2026-10-07), made-up values describe("itemDescription", () => { it("packs tags, description, and annotations the way diigo's export does", () => { expect( itemDescription({ tags: "ocap,js", description: "a note", annotations: [ { content: "first highlight", stickyNotes: [] }, { content: "second", stickyNotes: [{ content: "a sticky\n\nwith lines" }, { content: "another" }] }, ], }), ).toBe( "Tags:ocap,js;Description:a note;Annotations and Sticky notes:" + "Highlight:first highlight\n" + "Highlight:second\nSticky notes:a sticky\n\nwith lines\nSticky notes:another\n", ); }); it("says no_tag when there are no tags, and leaves empty parts empty", () => { expect(itemDescription({ tags: "", description: "", annotations: [] })).toBe( "Tags:no_tag;Description:;Annotations and Sticky notes:", ); }); }); describe("rssItem / toRss", () => { it("is well-formed even when diigo's text isn't: forbidden characters dropped, ]]> split", () => { const xml = rssItem({ title: "bad\u0003char", url: "https://a.example/", tags: "", description: "x]]>y" }); expect(xml).not.toContain("\u0003"); expect(xml).toContain("<![CDATA[badchar]]>"); expect(xml).toContain("Description:x]]]]>y;"); }); it("keeps the whole title (diigo's export cuts at 255)", () => { const title = "t".repeat(300); expect(rssItem({ title, url: "u" })).toContain(title); }); it("wraps items in an RSS 2.0 channel, dated by the caller", () => { const feed = toRss([{ title: "a", url: "u" }], { title: "my items", pubDate: "Wed, 07 Oct 2026 12:00:00 +0000" }); expect(feed).toMatch(/^<\?xml version="1.0"\?>\n\n/); expect(feed).toContain("Wed, 07 Oct 2026 12:00:00 +0000"); expect(feed).toContain("<![CDATA[a]]>"); }); });