Add Reddit comments and X definitions
This commit is contained in:
parent
40895e05db
commit
1a9ab6ebdf
7 changed files with 129 additions and 2 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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/\"]"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
13
src/content/definitions/sites/reddit-old.json
Normal file
13
src/content/definitions/sites/reddit-old.json
Normal file
|
|
@ -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]"]
|
||||
}
|
||||
}
|
||||
16
src/content/definitions/sites/x.json
Normal file
16
src/content/definitions/sites/x.json
Normal file
|
|
@ -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/\"]"]
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = '<div data-pressable-container="true"></div>';
|
||||
|
|
@ -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 = '<div id="siteTable"><div class="thing link"></div></div><article data-testid="tweet"></article>';
|
||||
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 = "";
|
||||
|
|
|
|||
9
tests/fixtures/reddit-old-feed.html
vendored
Normal file
9
tests/fixtures/reddit-old-feed.html
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<div id="siteTable">
|
||||
<div class="thing link" data-fullname="t3_abc123">
|
||||
<p class="title"><a class="title" href="https://old.reddit.com/r/example/comments/abc123/example_post/">A useful Reddit post</a></p>
|
||||
<div class="entry"><div class="expando"><div class="md">Hidden expanded media controls</div></div><div class="md"><p>The post body with visible text.</p></div></div>
|
||||
</div>
|
||||
<div class="thing comment" data-fullname="t1_def456">
|
||||
<div class="entry"><div class="md"><p>A useful comment with visible text.</p></div><a class="bylink" href="https://old.reddit.com/r/example/comments/abc123/example_post/def456/">permalink</a></div>
|
||||
</div>
|
||||
</div>
|
||||
7
tests/fixtures/x-feed.html
vendored
Normal file
7
tests/fixtures/x-feed.html
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<main>
|
||||
<article data-testid="tweet">
|
||||
<div data-testid="User-Name"><span>alice</span></div>
|
||||
<a href="/alice/status/1234567890"><time datetime="2026-08-25T00:00:00.000Z">1h</time></a>
|
||||
<div data-testid="tweetText">A useful post on X.</div>
|
||||
</article>
|
||||
</main>
|
||||
Loading…
Reference in a new issue