File changed: content.js only. All other files unchanged.
// v0.1 — blind wait, regardless of whether element is ready
run();
setTimeout(run, 500); // on SPA navHardcoded delays were guesswork. Too short → patch fires before element exists. Too long → visible flicker as the original text shows before being replaced.
Each rule now gets its own independent two-phase lifecycle:
Phase 1 — Seeker observer
If the target element isn't in the DOM yet, a MutationObserver watches the
entire document and fires tryPatch() on every DOM change. As soon as the
element appears, it patches immediately and transitions to Phase 2.
// v0.2 — fires the instant the element lands in the DOM
const seeker = new MutationObserver(() => {
if (!tryPatch()) return; // not there yet, keep watching
seeker.disconnect(); // found it — hand off to guard
// ... attach guard observer
});Phase 2 — Guard observer After the initial patch, a second observer keeps watching in case the page's own JavaScript (React, Vue, etc.) re-renders and overwrites the patched text. It re-applies the patch immediately whenever that happens.
// v0.2 — re-patches whenever the framework clobbers our change
const guard = new MutationObserver(() => {
if (guardActive) return; // skip mutations we caused ourselves
tryPatch();
});Prevents infinite loops of the form:
patch DOM → mutation fires → patch DOM → mutation fires → …
When tryPatch() runs, it sets guardActive = true immediately. The guard
observer skips any mutations while the flag is set. A requestAnimationFrame
clears the flag after the browser has finished processing our write, so
future legitimate re-renders by the framework are still caught.
// v0.1 — no guard, potential for thrash loops
node.nodeValue = rule.text;
// v0.2 — guarded write
guardActive = true;
patchNodes(nodes, rule.text);
requestAnimationFrame(() => { guardActive = false; });patchNodes() now checks whether the value is already correct before writing,
avoiding unnecessary DOM mutations.
// v0.1
node.nodeValue = rule.text; // always writes
// v0.2
if (node.nodeValue !== rule.text) { node.nodeValue = rule.text; } // skip if already correctIf a target element never appears (wrong XPath, page didn't load it, etc.),
the seeker disconnects after MAX_WAIT_MS = 10000 to avoid leaking observers.
const deadline = Date.now() + MAX_WAIT_MS;
const seeker = new MutationObserver(() => {
if (Date.now() > deadline) {
seeker.disconnect();
console.warn(`[XPath Patcher] ✗ Gave up waiting after ${MAX_WAIT_MS}ms`);
return;
}
// ...
});All console logs now include the version for easier debugging when comparing behaviour across installs.
// v0.1
[XPath Patcher] ✓ Patched 1 node(s) at "//h1"
// v0.2
[XPath Patcher v0.2] ✓ "//h1" → "My Custom Title"
[XPath Patcher v0.2] ⏳ Waiting for element: "//div[@class='price']"
[XPath Patcher v0.2] URL changed → re-evaluating rules
| Behaviour | v0.1 | v0.2 |
|---|---|---|
| Wait strategy | Fixed setTimeout |
DOM-event driven |
| Patch timing | After arbitrary delay | Instant when element appears |
| Re-render protection | None | Guard observer |
| Infinite loop protection | None | guardActive flag + requestAnimationFrame |
| Stale observer cleanup | None | 10s deadline on seeker |
| Per-rule independence | No (shared single run()) |
Yes (each rule has its own observers) |
| Version in logs | No | Yes |