Fix Threads post text extraction
This commit is contained in:
parent
bedeb8495d
commit
ece11a8352
4 changed files with 52 additions and 2 deletions
|
|
@ -8,7 +8,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.
|
- Configurable toxicity threshold and filtering mode: collapse posts with a Show button or hide them completely.
|
||||||
- Optional toxicity scores on filtered-post placeholders.
|
- Optional toxicity scores on filtered-post placeholders.
|
||||||
- Declarative site definitions with URL patterns, selectors, and stable identifiers.
|
- Declarative site definitions with URL patterns, selectors, and stable identifiers.
|
||||||
- Built-in support for the official Mastodon web interface.
|
- Built-in support for the official Mastodon web interface and Threads.
|
||||||
- Versioned JSON definition collections with weekly subscriptions and manual refresh.
|
- Versioned JSON definition collections with weekly subscriptions and manual refresh.
|
||||||
- Toolbar popup with current-site status, matched definition, pause/resume control, and settings access.
|
- Toolbar popup with current-site status, matched definition, pause/resume control, and settings access.
|
||||||
- Firefox Manifest V2 and Chromium Manifest V3 builds.
|
- Firefox Manifest V2 and Chromium Manifest V3 builds.
|
||||||
|
|
@ -47,7 +47,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.
|
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. 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 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.
|
||||||
|
|
||||||
## Runtime architecture
|
## Runtime architecture
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,23 @@
|
||||||
"idAttributes": ["data-id"],
|
"idAttributes": ["data-id"],
|
||||||
"permalinkSelectors": ["a.status__relative-time[href]", "a.detailed-status__datetime[href]", "a.u-url.u-uid[href]"]
|
"permalinkSelectors": ["a.status__relative-time[href]", "a.detailed-status__datetime[href]", "a.u-url.u-uid[href]"]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "threads",
|
||||||
|
"name": "Threads",
|
||||||
|
"urlPatterns": [
|
||||||
|
"https://threads.com/*",
|
||||||
|
"https://www.threads.com/*",
|
||||||
|
"https://threads.net/*",
|
||||||
|
"https://www.threads.net/*"
|
||||||
|
],
|
||||||
|
"requiredSelectors": ["[data-pressable-container=\"true\"], [role=\"article\"], article"],
|
||||||
|
"post": {
|
||||||
|
"rootSelectors": ["[data-pressable-container=\"true\"]", "[role=\"article\"]", "article"],
|
||||||
|
"textSelectors": ["span[dir=\"auto\"]:not(a span):not(time span):not([role=\"button\"] span)"],
|
||||||
|
"excludedSelectors": ["button", "[role=\"button\"]", "time", "svg"],
|
||||||
|
"permalinkSelectors": ["a[href*=\"/post/\"]"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
import { DefinitionEngine, matchesUrlPattern, selectDefinition } from "../src/content/definition-engine";
|
import { DefinitionEngine, matchesUrlPattern, selectDefinition } from "../src/content/definition-engine";
|
||||||
import { BUILTIN_DEFINITIONS } from "../src/content/definitions";
|
import { BUILTIN_DEFINITIONS } from "../src/content/definitions";
|
||||||
import type { SiteDefinition } from "../src/shared/types";
|
import type { SiteDefinition } from "../src/shared/types";
|
||||||
|
|
@ -59,4 +60,21 @@ describe("DefinitionEngine", () => {
|
||||||
stop();
|
stop();
|
||||||
expect(observed).toContain("content warning revealed text");
|
expect(observed).toContain("content warning revealed text");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("discovers Threads posts from stable semantic/data selectors and permalink IDs", () => {
|
||||||
|
document.body.innerHTML = readFileSync("tests/fixtures/threads-feed.html", "utf8");
|
||||||
|
const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!;
|
||||||
|
const posts = new DefinitionEngine(threads).discover(document);
|
||||||
|
expect(posts.map((post) => post.id)).toEqual(["threads:http://localhost:3000/@alice/post/ABC123", "threads:http://localhost:3000/@bob/post/DEF456"]);
|
||||||
|
expect(posts.map((post) => post.text)).toEqual(["first threads post with visible text", "a reply in the conversation"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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>';
|
||||||
|
for (const url of ["https://threads.com/home", "https://www.threads.com/@alice/post/ABC123", "https://threads.net/home", "https://www.threads.net/@alice/post/ABC123"]) {
|
||||||
|
expect(selectDefinition([threads], url)).toBe(threads);
|
||||||
|
}
|
||||||
|
expect(selectDefinition([threads], "https://example.com/home")).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
15
tests/fixtures/threads-feed.html
vendored
Normal file
15
tests/fixtures/threads-feed.html
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<main>
|
||||||
|
<div data-pressable-container="true">
|
||||||
|
<a href="/@alice"><span dir="auto">alice</span></a>
|
||||||
|
<a href="/@alice/post/ABC123"><time><span dir="auto">12h</span></time></a>
|
||||||
|
<span dir="auto">First Threads post with <span>visible text</span></span>
|
||||||
|
<button><span dir="auto">Like</span></button>
|
||||||
|
<time><span dir="auto">1h</span></time>
|
||||||
|
</div>
|
||||||
|
<div role="article">
|
||||||
|
<a href="/@bob"><span dir="auto">bob</span></a>
|
||||||
|
<a href="/@bob/post/DEF456"><time><span dir="auto">1d</span></time></a>
|
||||||
|
<span dir="auto">A reply in the conversation</span>
|
||||||
|
<div aria-hidden="true">Decorative content</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
Loading…
Reference in a new issue