Newspaper (Syndication Feed Reader)

This software renders syndication feeds as XHTML; It supports Atom Activity Streams (Friendica, Nostr, OStatus), Atom Over XMPP (Blasta, Libervia, Movim, Rivista), BitTorrent RSS, JSON Feed, OPML, RDF (DOAP, FOAF, RSS, XMPP), RSS-in-JSON, Simple Machines Forum (SMF), Sitemap, The Atom Syndication Format, and Twtxt; and it also supports navigation (RFC 5005).

Old: v26.01.02 - 2025-12-01 - Disable notification of "rendering an XHTML document".
New: v26.01.03 - 2025-12-01 - Enhance preference menu.

  • --- /tmp/diffy20260212-2003408-6ymc2e 2026-02-12 22:05:11.461835580 +0000
  • +++ /tmp/diffy20260212-2003408-vw15ha 2026-02-12 22:05:11.462835598 +0000
  • @@ -19,7 +19,7 @@
  • // @noframes
  • // @exclude *?streamburner=0*
  • // @exclude *&streamburner=0*
  • -// @version 26.01.02
  • +// @version 26.01.03
  • // @run-at document-end
  • // @grant GM_addStyle
  • // @grant GM.setValue
  • @@ -5566,73 +5566,7 @@
  • // Scan for possible syndication feeds.
  • if (isHtml && detectionScan) {
  • - console.info("📰 Greasemonkey Newspaper: Scanning for syndicated documents…");
  • - if (gmNotification && detectionNotification) {
  • - await GM.notification("Scanning for syndicated documents…", "Greasemonkey Newspaper", "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48dGV4dCB5PSIuOWVtIiBmb250LXNpemU9IjkwIj7wn5OwPC90ZXh0Pjwvc3ZnPgo=")
  • - }
  • - let subscriptions = 0,
  • - mimeTypes = [
  • - "application/activity+xml",
  • - "application/activitystream+xml",
  • - "application/atom+xml",
  • - "application/feed+json",
  • - "application/gemini+text",
  • - "application/pubsub+xml",
  • - "application/rdf+xml",
  • - "application/rss+json",
  • - "application/rss+xml",
  • - "application/smf+xml",
  • - "application/stream+xml",
  • - "application/twtxt+text",
  • - "text/twtxt+plain",
  • - "text/gemini"
  • - ];
  • - for (mimeType of mimeTypes) {
  • - results = document.head.querySelectorAll(`link[type="${mimeType}"`);
  • - /*
  • - results = document.head.queryPathAll(
  • - null,
  • - //`link[@rel="alternate" and contains(@type, "${mimeType}")]`);
  • - `link[@rel="alternate" and @type="${mimeType}"]`);
  • - */
  • - for (result of results) {
  • - let a = document.createElement("a");
  • - if (result.href.startsWith("feed:")) {
  • - result.href = result.href.replace("feed:", "http:");
  • - } else
  • - if (result.href.startsWith("itpc:")) {
  • - result.href = result.href.replace("itpc:", "http:");
  • - } else {
  • - }
  • - a.href = result.href;
  • - a.title = mimeType;
  • - a.textContent = result.title || result.href;
  • - a.style.color = "#eee";
  • - for (let i = 0; i < a.style.length; i++) {
  • - a.style.setProperty(
  • - a.style[i],
  • - a.style.getPropertyValue(a.style[i]),
  • - "important"
  • - );
  • - }
  • - subscriptions += 1;
  • - console.info(`📰 Greasemonkey Newspaper: Subscription "${result.title}" ${result.href}`);
  • - if (gmRegisterMenuCommand) {
  • - GM.registerMenuCommand(
  • - `📰 ${result.title}`,
  • - () => navigateToUri(result.href));
  • - }
  • - }
  • - }
  • - if (subscriptions) {
  • - console.info(`📰 Greasemonkey Newspaper: This site has ${subscriptions} subscriptions.`);
  • - if (detectionNotification) {
  • - if (gmNotification && detectionNotification) {
  • - await GM.notification(`This site offers ${subscriptions} subscriptions.`, "Greasemonkey Newspaper", "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48dGV4dCB5PSIuOWVtIiBmb250LXNpemU9IjkwIj7wn5OwPC90ZXh0Pjwvc3ZnPgo=")
  • - //GM_notification("This site has syndicated feeds.", "Greasemonkey Newspaper", "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48dGV4dCB5PSIuOWVtIiBmb250LXNpemU9IjkwIj7wn5OwPC90ZXh0Pjwvc3ZnPgo=")
  • - }
  • - }
  • - }
  • + scanDocument();
  • } else
  • if (isJson) { // Attempt to parse JSON.
  • console.info("📰 Greasemonkey Newspaper: Collecting JSON data…");
  • @@ -8490,12 +8424,13 @@
  • return await GM.getValue("font-size", 20);
  • }
  • -async function setSettingValue(key, message) {
  • +async function setSettingValue(title, message, key) {
  • let value;
  • value = prompt(message);
  • value = parseInt(value);
  • if (typeof value == "number") {
  • await GM.setValue(key, value);
  • + GM.notification(`${title} (${value})`, "Greasemonkey Newspaper", "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48dGV4dCB5PSIuOWVtIiBmb250LXNpemU9IjkwIj7wn5OwPC90ZXh0Pjwvc3ZnPgo=")
  • } else {
  • alert("Value must be numeric.");
  • }
  • @@ -10832,6 +10767,7 @@
  • "a", "body", "code", ".enclosures", ".resources", "#control-bar-container",
  • "#empty-feed", "#info-square"];
  • if (gmGetValue && gmSetValue) {
  • + //GM.unregisterMenuCommand(menuView);
  • if (await GM.getValue("view-mode") == "dark") {
  • await GM.setValue("view-mode", "bright");
  • //mode.textContent = "Light View";
  • @@ -10845,6 +10781,9 @@
  • element.classList.remove("dark");
  • }
  • }
  • + //menuView = await GM.registerMenuCommand(
  • + // "🌙 Dark mode, () => toggleMode(),
  • + // "T");
  • } else {
  • await GM.setValue("view-mode", "dark");
  • //mode.textContent = "Dark View";
  • @@ -10858,6 +10797,9 @@
  • element.classList.add("dark");
  • }
  • }
  • + //menuView = await GM.registerMenuCommand(
  • + // "☀ Bright mode, () => toggleMode(),
  • + // "T");
  • }
  • } else {
  • if (document.querySelector("#feed .dark")) {
  • @@ -11156,91 +11098,73 @@
  • // playEnclosure = await getAudioEnclosureMode();
  • // subscriptionHandler = await getSubscriptionHandler();
  • await GM.registerMenuCommand(
  • - "Start setup wizard",
  • - () => callibratePreferences(),
  • + "📡 Detect subscriptions",
  • + () => scanDocument(),
  • "S");
  • await GM.registerMenuCommand(
  • - "Set font size", // 𝙵
  • - () => setSettingValue("font-size", "Set font size"),
  • - "F");
  • -// await GM.registerMenuCommand(
  • -// "Set font type", // 𐍆
  • -// () => setSettingValue("font-type", "Set font type (e.g. arial, sans, serif)"),
  • -// "T");
  • - await GM.registerMenuCommand(
  • - "Set number of items",
  • - () => setSettingValue("item-number", "Set a routine maximal number of items to display"),
  • - "E");
  • - await GM.registerMenuCommand(
  • - "Toggle view mode", () => toggleMode(),
  • - "T");
  • + "🎛 Display preferences",
  • + () => displayPreferences(),
  • + "P");
  • }
  • })();
  • -async function callibratePreferences () {
  • - let preferencesBoolean = [
  • - {"key" : "detection-scan",
  • - "query" : "Do you want to enable automatic detection of feeds?"},
  • - {"key" : "detection-notification",
  • - "query" : "Do you want to enable notifications of detected feeds?"},
  • - {"key" : "filter-blacklist",
  • - "query" : "Do you want to enable filter 'blacklist'?"},
  • - {"key" : "filter-whitelist",
  • - "query" : "Do you want to enable filter 'whitelist'?"},
  • - {"key" : "motd",
  • - "query" : "Do you want to enable our special announcement?"},
  • - {"key" : "play-enclosure",
  • - "query" : "Do you want to enable playback of audible attachments?"},
  • - {"key" : "show-icon",
  • - "query" : "Do you want to enable the display of graphical icons and logos?"}
  • - ];
  • - for (let preference of preferencesBoolean) {
  • - let value;
  • - if (confirm(preference.query)) {
  • - value = true;
  • - } else {
  • - value = false;
  • - }
  • - await GM.setValue(preference.key, value);
  • - }
  • - let preferencesNumeric = [
  • - {"key" : "font-size",
  • - "query" : "Please. Input value (pixel) of font size."},
  • - {"key" : "item-number",
  • - "query" : "Please. Input value of routine maximal number of items to display."}
  • - ];
  • - for (let preference of preferencesNumeric) {
  • - let value = prompt(preference.query);
  • - value = parseInt(value);
  • - if (typeof value == "number") {
  • - await GM.setValue(preference.key, value);
  • - } else {
  • - alert("Preference was not changed. Value must be numeric.");
  • - }
  • - }
  • - let preferencesFixed = [
  • - {"key" : "content-mode",
  • - "query" : "Do you prefer to display the whole content?",
  • - "value" : "content-complete",
  • - "routine" : "content-summary"},
  • - {"key" : "enclosure-view",
  • - "query" : "Do you want to display attachments in a table?",
  • - "value" : "table",
  • - "routine" : "list"},
  • - {"key" : "view-mode",
  • - "query" : "Do you want to enable dark-mode?",
  • - "value" : "dark",
  • - "routine" : "bright"}
  • - ];
  • - for (let preference of preferencesFixed) {
  • - let value;
  • - if (confirm(preference.query)) {
  • - value = preference.value;
  • - } else {
  • - value = preference.routine;
  • - }
  • - await GM.setValue(preference.key, value);
  • +async function displayPreferences() {
  • + await GM.registerMenuCommand(
  • + "📶 Automatic detection",
  • + () => togglePreference("Automatic detection", "detection-scan"),
  • + "A");
  • + await GM.registerMenuCommand(
  • + "🎫 Detection notification",
  • + () => togglePreference("Detection notification", "detection-notification"),
  • + "N");
  • + await GM.registerMenuCommand(
  • + "📈 Filter (Allow)",
  • + () => togglePreference("Filter (Allow)", "filter-whitelist"),
  • + "W");
  • + await GM.registerMenuCommand(
  • + "📉 Filter (Deny)",
  • + () => togglePreference("Filter (Deny)", "filter-blacklist"),
  • + "D");
  • + await GM.registerMenuCommand(
  • + "🖊 Font size", // 𝙵
  • + () => setSettingValue("Font size", "Set font size (pixel).", "font-size"),
  • + "F");
  • +//await GM.registerMenuCommand(
  • +// "Font type", // 𐍆
  • +// () => setSettingValue("Font type", "Set font type (e.g. arial, sans, serif).", "font-type"),
  • +// "T");
  • + await GM.registerMenuCommand(
  • + "🖼 Graphics",
  • + () => togglePreference("Graphic", "show-icon"),
  • + "G");
  • + await GM.registerMenuCommand(
  • + "📝 Items",
  • + () => setSettingValue("Routine number of items", "Set a routine number of items to display.", "item-number"),
  • + "N");
  • +// await GM.registerMenuCommand(
  • +// "💡 Mode",
  • +// () => togglePreference("view-mode", "dark", "bright"),
  • +// "M");
  • + await GM.registerMenuCommand(
  • + "📢 MOTD",
  • + () => togglePreference("MOTD", "motd"),
  • + "T");
  • + await GM.registerMenuCommand(
  • + "🎹 Playback",
  • + () => togglePreference("Playback", "play-enclosure"),
  • + "B");
  • +}
  • +
  • +async function togglePreference(title, key) {
  • + if (await GM.getValue(key)) {
  • + message = `${title} is disabled.`;
  • + value = false;
  • + } else {
  • + message = `${title} is enabled.`;
  • + value = true;
  • }
  • + await GM.setValue(key, value);
  • + GM.notification(message, "Greasemonkey Newspaper", "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48dGV4dCB5PSIuOWVtIiBmb250LXNpemU9IjkwIj7wn5OwPC90ZXh0Pjwvc3ZnPgo=")
  • }
  • function isListed(content, keywords) {
  • @@ -11324,12 +11248,78 @@
  • } while (currentDate - anchorDate < milliseconds);
  • }
  • -function navigateToUri(url) {
  • - //window.open(url, "_blank");
  • - window.open(url, "_self");
  • -
  • +function scanDocument() {
  • + console.info("📰 Greasemonkey Newspaper: Scanning for syndicated documents…");
  • + if (gmNotification && detectionNotification) {
  • + GM.notification("Scanning for syndicated documents…", "Greasemonkey Newspaper", "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48dGV4dCB5PSIuOWVtIiBmb250LXNpemU9IjkwIj7wn5OwPC90ZXh0Pjwvc3ZnPgo=")
  • + }
  • + let subscriptions = 0,
  • + mimeTypes = [
  • + "application/activity+xml",
  • + "application/activitystream+xml",
  • + "application/atom+xml",
  • + "application/feed+json",
  • + "application/gemini+text",
  • + "application/pubsub+xml",
  • + "application/rdf+xml",
  • + "application/rss+json",
  • + "application/rss+xml",
  • + "application/smf+xml",
  • + "application/stream+xml",
  • + "application/twtxt+text",
  • + "text/twtxt+plain",
  • + "text/gemini"
  • + ];
  • + for (let mimeType of mimeTypes) {
  • + let results = document.head.querySelectorAll(`link[type="${mimeType}"`);
  • + /*
  • + results = document.head.queryPathAll(
  • + null,
  • + //`link[@rel="alternate" and contains(@type, "${mimeType}")]`);
  • + `link[@rel="alternate" and @type="${mimeType}"]`);
  • + */
  • + for (let result of results) {
  • + let a = document.createElement("a");
  • + if (result.href.startsWith("feed:")) {
  • + result.href = result.href.replace("feed:", "http:");
  • + } else
  • + if (result.href.startsWith("itpc:")) {
  • + result.href = result.href.replace("itpc:", "http:");
  • + } else {
  • + }
  • + a.href = result.href;
  • + a.title = mimeType;
  • + a.textContent = result.title || result.href;
  • + a.style.color = "#eee";
  • + for (let i = 0; i < a.style.length; i++) {
  • + a.style.setProperty(
  • + a.style[i],
  • + a.style.getPropertyValue(a.style[i]),
  • + "important"
  • + );
  • + }
  • + subscriptions += 1;
  • + console.info(`📰 Greasemonkey Newspaper: Subscription "${result.title}" ${result.href}`);
  • + if (gmRegisterMenuCommand) {
  • + GM.registerMenuCommand(
  • + `📰 ${result.title}`,
  • + //() => window.open(result.href, "_blank"));
  • + () => window.open(result.href, "_self")
  • + );
  • + }
  • + }
  • + }
  • + if (subscriptions) {
  • + console.info(`📰 Greasemonkey Newspaper: This site has ${subscriptions} subscriptions.`);
  • + if (detectionNotification) {
  • + if (gmNotification && detectionNotification) {
  • + GM.notification(`This site offers ${subscriptions} subscriptions.`, "Greasemonkey Newspaper", "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48dGV4dCB5PSIuOWVtIiBmb250LXNpemU9IjkwIj7wn5OwPC90ZXh0Pjwvc3ZnPgo=")
  • + //GM_notification("This site has syndicated feeds.", "Greasemonkey Newspaper", "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48dGV4dCB5PSIuOWVtIiBmb250LXNpemU9IjkwIj7wn5OwPC90ZXh0Pjwvc3ZnPgo=")
  • + }
  • + }
  • + }
  • +
  • }
  • -
  • function getNodeByXPath(node, query) { // FIXME
  • res = document.evaluate(
  • query, node, null,