From 1a9ab6ebdf3fcd22543a91363567d4f781ce0c81 Mon Sep 17 00:00:00 2001 From: wagesj45 Date: Tue, 25 Aug 2026 22:34:25 -0500 Subject: [PATCH] Add Reddit comments and X definitions --- README.md | 4 +- src/content/definitions/default.json | 55 +++++++++++++++++++ src/content/definitions/sites/reddit-old.json | 13 +++++ src/content/definitions/sites/x.json | 16 ++++++ tests/definition-engine.test.ts | 27 +++++++++ tests/fixtures/reddit-old-feed.html | 9 +++ tests/fixtures/x-feed.html | 7 +++ 7 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 src/content/definitions/sites/reddit-old.json create mode 100644 src/content/definitions/sites/x.json create mode 100644 tests/fixtures/reddit-old-feed.html create mode 100644 tests/fixtures/x-feed.html diff --git a/README.md b/README.md index b6dedcd..bc9a87a 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ VibeGuard is a local-first browser extension that hides toxic social-media posts - Configurable toxicity threshold and filtering mode: collapse posts with a Show button or hide them completely. - Optional toxicity scores on filtered-post placeholders. - Declarative site definitions with URL patterns, selectors, and stable identifiers. -- Built-in support for the official Mastodon web interface and Threads. +- Built-in support for the official Mastodon web interface, Threads, old Reddit, and X. - Versioned JSON definition collections with weekly subscriptions and manual refresh. - Toolbar popup with current-site status, matched definition, pause/resume control, and settings access. - Firefox Manifest V2 and Chromium Manifest V3 builds. @@ -48,7 +48,7 @@ Preparation writes the Transformers.js-compatible files and `model-manifest.json If the model uses generic labels such as `LABEL_0` and `LABEL_1`, pass `--toxic-index` and `--non-toxic-index` to `prepare`; the command refuses to guess an ambiguous mapping. -VibeGuard injects a lightweight content script on ordinary web pages, but reads and processes content only when a validated site definition matches the page. The extension includes a bundled definition collection for the official Mastodon web interface and Threads. The Threads definition covers both `threads.com` and legacy `threads.net` URLs using semantic and data-attribute selectors. The default subscription follows the repository's raw JSON collection and refreshes weekly, while the bundled copy remains available offline. Additional JSON subscriptions, local overrides, and disabled definitions are managed in the options page. Definitions are declarative selectors and URL patterns, never executable code. +VibeGuard injects a lightweight content script on ordinary web pages, but reads and processes content only when a validated site definition matches the page. The extension includes a bundled definition collection for the official Mastodon web interface, Threads, old Reddit, and X. The Threads definition covers both `threads.com` and legacy `threads.net` URLs using semantic and data-attribute selectors; the Reddit definition targets `old.reddit.com`, while the X definition covers both `x.com` and legacy `twitter.com` URLs. The default subscription follows the repository's raw JSON collection and refreshes weekly, while the bundled copy remains available offline. Additional JSON subscriptions, local overrides, and disabled definitions are managed in the options page. Definitions are declarative selectors and URL patterns, never executable code. ## Runtime architecture diff --git a/src/content/definitions/default.json b/src/content/definitions/default.json index 66a8b8b..a2fd27b 100644 --- a/src/content/definitions/default.json +++ b/src/content/definitions/default.json @@ -37,6 +37,37 @@ ] } }, + { + "id": "reddit-old", + "name": "Reddit (old interface)", + "urlPatterns": [ + "https://old.reddit.com/*" + ], + "requiredSelectors": [ + "#siteTable .thing.link, #siteTable .thing.comment" + ], + "post": { + "rootSelectors": [ + "#siteTable > .thing.link", + "#siteTable .thing.comment" + ], + "textSelectors": [ + ".md", + ".title" + ], + "excludedSelectors": [ + ".expando", + ".child" + ], + "idAttributes": [ + "data-fullname" + ], + "permalinkSelectors": [ + "a.title[href]", + "a.bylink[href]" + ] + } + }, { "id": "threads", "name": "Threads", @@ -72,6 +103,30 @@ "a[href*=\"/post/\"]" ] } + }, + { + "id": "x", + "name": "X (formerly Twitter)", + "urlPatterns": [ + "https://x.com/*", + "https://www.x.com/*", + "https://twitter.com/*", + "https://www.twitter.com/*" + ], + "requiredSelectors": [ + "article[data-testid=\"tweet\"]" + ], + "post": { + "rootSelectors": [ + "article[data-testid=\"tweet\"]" + ], + "textSelectors": [ + "[data-testid=\"tweetText\"]" + ], + "permalinkSelectors": [ + "a[href*=\"/status/\"]" + ] + } } ] } diff --git a/src/content/definitions/sites/reddit-old.json b/src/content/definitions/sites/reddit-old.json new file mode 100644 index 0000000..a7998a8 --- /dev/null +++ b/src/content/definitions/sites/reddit-old.json @@ -0,0 +1,13 @@ +{ + "id": "reddit-old", + "name": "Reddit (old interface)", + "urlPatterns": ["https://old.reddit.com/*"], + "requiredSelectors": ["#siteTable .thing.link, #siteTable .thing.comment"], + "post": { + "rootSelectors": ["#siteTable > .thing.link", "#siteTable .thing.comment"], + "textSelectors": [".md", ".title"], + "excludedSelectors": [".expando", ".child"], + "idAttributes": ["data-fullname"], + "permalinkSelectors": ["a.title[href]", "a.bylink[href]"] + } +} diff --git a/src/content/definitions/sites/x.json b/src/content/definitions/sites/x.json new file mode 100644 index 0000000..1f8f66d --- /dev/null +++ b/src/content/definitions/sites/x.json @@ -0,0 +1,16 @@ +{ + "id": "x", + "name": "X (formerly Twitter)", + "urlPatterns": [ + "https://x.com/*", + "https://www.x.com/*", + "https://twitter.com/*", + "https://www.twitter.com/*" + ], + "requiredSelectors": ["article[data-testid=\"tweet\"]"], + "post": { + "rootSelectors": ["article[data-testid=\"tweet\"]"], + "textSelectors": ["[data-testid=\"tweetText\"]"], + "permalinkSelectors": ["a[href*=\"/status/\"]"] + } +} diff --git a/tests/definition-engine.test.ts b/tests/definition-engine.test.ts index 4ce1a10..c0708cc 100644 --- a/tests/definition-engine.test.ts +++ b/tests/definition-engine.test.ts @@ -95,6 +95,22 @@ describe("DefinitionEngine", () => { expect(posts.map((post) => post.text)).toEqual(["the main post on the threads detail page", "a reply on the threads detail page"]); }); + it("discovers posts from the old Reddit interface using fullname IDs", () => { + document.body.innerHTML = readFileSync("tests/fixtures/reddit-old-feed.html", "utf8"); + const reddit = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "reddit-old")!; + const posts = new DefinitionEngine(reddit).discover(document); + expect(posts.map((post) => post.id)).toEqual(["reddit-old:t3_abc123", "reddit-old:t1_def456"]); + expect(posts.map((post) => post.text)).toEqual(["the post body with visible text.", "a useful comment with visible text."]); + }); + + it("discovers X posts using semantic tweet markers and permalink IDs", () => { + document.body.innerHTML = readFileSync("tests/fixtures/x-feed.html", "utf8"); + const x = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "x")!; + const posts = new DefinitionEngine(x).discover(document); + expect(posts.map((post) => post.id)).toEqual(["x:http://localhost:3000/alice/status/1234567890"]); + expect(posts[0]?.text).toBe("a useful post on x."); + }); + it("selects Threads on current and legacy domains", () => { const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!; document.body.innerHTML = '
'; @@ -104,6 +120,17 @@ describe("DefinitionEngine", () => { expect(selectDefinition([threads], "https://example.com/home")).toBeUndefined(); }); + it("selects Reddit old and X only on their supported domains", () => { + const reddit = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "reddit-old")!; + const x = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "x")!; + document.body.innerHTML = '
'; + expect(selectDefinition([reddit], "https://old.reddit.com/r/all")).toBe(reddit); + expect(selectDefinition([reddit], "https://www.reddit.com/r/all")).toBeUndefined(); + expect(selectDefinition([x], "https://x.com/home")).toBe(x); + expect(selectDefinition([x], "https://twitter.com/alice/status/123")).toBe(x); + expect(selectDefinition([x], "https://example.com/home")).toBeUndefined(); + }); + it("can identify a specific site while its SPA markers are still loading", () => { const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!; document.body.innerHTML = ""; diff --git a/tests/fixtures/reddit-old-feed.html b/tests/fixtures/reddit-old-feed.html new file mode 100644 index 0000000..b28e289 --- /dev/null +++ b/tests/fixtures/reddit-old-feed.html @@ -0,0 +1,9 @@ +
+ +
+

A useful comment with visible text.

permalink
+
+
diff --git a/tests/fixtures/x-feed.html b/tests/fixtures/x-feed.html new file mode 100644 index 0000000..62417bc --- /dev/null +++ b/tests/fixtures/x-feed.html @@ -0,0 +1,7 @@ +
+
+
alice
+ +
A useful post on X.
+
+