Skip to content

Commit

Permalink
WIP: support multiple triggers
Browse files Browse the repository at this point in the history
  • Loading branch information
eight04 committed Feb 13, 2024
1 parent 9105e83 commit 12fc5cd
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 163 deletions.
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"event-lite": "^0.1.2",
"gm-webext-pref": "^0.4.2",
"linkify-plus-plus-core": "^0.5.3",
"sentinel-js": "^0.0.7",
"webext-dialog": "^0.1.1",
"webext-pref": "^0.6.0",
"webextension-polyfill": "^0.8.0"
Expand Down
286 changes: 123 additions & 163 deletions src/lib/main.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,128 @@
const {linkify, UrlMatcher, INVALID_TAGS} = require("linkify-plus-plus-core");
const sentinel = require('sentinel-js');

const MAX_PROCESSES = 100;
const processedNodes = new WeakSet;
const nodeValidationCache = new WeakMap; // Node -> boolean
let processes = 0;
const triggers = [
{
enabled: pref => pref.get("triggerByPageLoad"),
trigger: async options => {
await prepareDocument();
processedNodes.add(document.body);
await linkify({...options, root: document.body, recursive: true});
}
},
{
enabled: pref => pref.get("triggerByNewNode"),
trigger: async options => {
await prepareDocument();
const observer = new MutationObserver(function(mutations){
// Filter out mutations generated by LPP
var lastRecord = mutations[mutations.length - 1],
nodes = lastRecord.addedNodes,
i;

if (nodes.length >= 2) {
for (i = 0; i < 2; i++) {
if (nodes[i].className == "linkifyplus") {
return;
}
}
}

for (var record of mutations) {
for (const node of record.addedNodes) {
if (node.nodeType === 1 && !processedNodes.has(node)) {
if (processes >= MAX_PROCESSES) {
throw new Error("Too many processes");
}
if (processedNodes.has(node)) {
continue;
}
processedNodes.add(node);
processes++;
linkify({...options, root: node, recursive: true})
.finally(() => {
processes--;
});
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
},
{
enabled: pref => pref.get("triggerByHover"),
trigger: options => {
sentinel.on(":hover", el => {
if (validRoot(el, options.validator)) {
processedNodes.add(el);
linkify({...options, root: el, recursive: false});
}
});
}
},
{
enabled: pref => pref.get("triggerByClick"),
trigger: options => {
document.addEventListener("click", function(e){
var el = e.target;
if (validRoot(el, options.validator)) {
processedNodes.add(el);
linkify({...options, root: el, recursive: false});
}
}, {
passive: true
});
}
}
]

// Valid root node before linkifing
function validRoot(node, validator) {
// Cache valid state in node.VALID
if (node.VALID !== undefined) {
return node.VALID;
if (processedNodes.has(node)) {
return false;
}
return getValidation(node);

// Loop through ancestor
var cache = [], isValid;
while (node != document.documentElement) {
cache.push(node);

// It is invalid if it has invalid ancestor
if (!validator(node) || INVALID_TAGS[node.localName]) {
isValid = false;
break;
}

// The node was removed from DOM tree
if (!node.parentNode) {
function getValidation(p) {
if (!p.parentNode) {
return false;
}

node = node.parentNode;

if (node.VALID !== undefined) {
isValid = node.VALID;
break;
let r = nodeValidationCache.get(p);
if (r === undefined) {
if (validator.isIncluded(p)) {
r = true;
} else if (validator.isExcluded(p)) {
r = false;
} else if (p.parentNode != document.documentElement) {
r = getValidation(p.parentNode);
} else {
r = true;
}
nodeValidationCache.set(p, r);
}
return r;
}

// All ancestors are fine
if (isValid === undefined) {
isValid = true;
}

// Cache the result
var i;
for (i = 0; i < cache.length; i++) {
cache[i].VALID = isValid;
}

return isValid;
}

function createValidator({includeElement, excludeElement}) {
return function(node) {
const f = function(node) {
if (processedNodes.has(node)) {
return false;
}
processedNodes.add(node);

if (node.isContentEditable) {
return false;
}

if (node.matches) {
if (includeElement && node.matches(includeElement)) {
return true;
Expand All @@ -60,98 +133,13 @@ function createValidator({includeElement, excludeElement}) {
}
return true;
};
}

function createBuffer(size) {
const set = new Set;
const buff = Array(size);
const eventBus = document.createElement("span");
let start = 0;
let end = 0;
return {push, eventBus, shift};

function push(item) {
if (set.has(item)) {
return;
}
if (set.size && start === end) {
// overflow
eventBus.dispatchEvent(new CustomEvent("overflow"));
set.clear();
return;
}
set.add(item);
buff[end] = item;
end = (end + 1) % size;
eventBus.dispatchEvent(new CustomEvent("add"));
}

function shift() {
if (!set.size) {
return;
}
const item = buff[start];
set.delete(item);
buff[start] = null;
start = (start + 1) % size;
return item;
}
}

function createLinkifyProcess({options, bufferSize}) {
const buffer = createBuffer(bufferSize);
let overflowed = false;
let started = false;
buffer.eventBus.addEventListener("add", start);
buffer.eventBus.addEventListener("overflow", () => overflowed = true);
return {process};

function process(root) {
if (overflowed) {
return false
}
if (validRoot(root, options.validator)) {
buffer.push(root);
}
return true;
}

function start() {
if (started) {
return;
}
started = true;
deque();
}

function deque() {
let root;
if (overflowed) {
root = document.body;
overflowed = false;
} else {
root = buffer.shift();
}
if (!root) {
started = false;
return;
}

linkify(root, options)
.then(() => {
var p = Promise.resolve();
if (options.includeElement) {
for (var node of root.querySelectorAll(options.includeElement)) {
p = p.then(linkify.bind(null, node, options));
}
}
return p;
})
.catch(err => {
console.error(err);
})
.then(deque);
}
f.isIncluded = node => {
return includeElement && node.matches(includeElement);
};
f.isExcluded = node => {
return excludeElement && node.matches(excludeElement);
};
return f;
}

function stringToList(value) {
Expand All @@ -170,9 +158,7 @@ function createOptions(pref) {

function update(changes) {
Object.assign(options, changes);
if (changes.includeElement != null || changes.excludeElement != null) {
options.validator = createValidator(options);
}
options.validator = createValidator(options);
if (typeof options.customRules === "string") {
options.customRules = stringToList(options.customRules);
}
Expand Down Expand Up @@ -207,39 +193,13 @@ async function startLinkifyPlusPlus(getPref) {
}

const pref = await getPref();
const linkifyProcess = createLinkifyProcess({
options: createOptions(pref),
bufferSize: 100
});
const observer = new MutationObserver(function(mutations){
// Filter out mutations generated by LPP
var lastRecord = mutations[mutations.length - 1],
nodes = lastRecord.addedNodes,
i;

if (nodes.length >= 2) {
for (i = 0; i < 2; i++) {
if (nodes[i].className == "linkifyplus") {
return;
}
}
}

for (var record of mutations) {
if (record.addedNodes.length) {
if (!linkifyProcess.process(record.target)) {
// it's full
break;
}
}
const options = createOptions(pref);
for (const trigger of triggers) {
if (trigger.enabled(pref)) {
trigger.trigger(options);
}
});
await prepareDocument();
observer.observe(document.body, {
childList: true,
subtree: true
});
linkifyProcess.process(document.body);
}
// TODO: disable trigger when options change?
}

function prepareDocument() {
Expand Down
26 changes: 26 additions & 0 deletions src/lib/pref-body.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
module.exports = getMessage => {
return [
{
type: "section",
label: getMessage("optionsTriggerLabel"),
children: [
{
key: "triggerByPageLoad",
type: "checkbox",
label: getMessage("optionsTriggerByPageLoadLabel")
},
{
key: "triggerByNewNode",
type: "checkbox",
label: getMessage("optionsTriggerByNewNodeLabel")
},
{
key: "triggerByHover",
type: "checkbox",
label: getMessage("optionsTriggerByHoverLabel")
},
{
key: "triggerByClick",
type: "checkbox",
label: getMessage("optionsTriggerByClickLabel")
}
]
},
{
key: "fuzzyIp",
type: "checkbox",
Expand Down
Loading

0 comments on commit 12fc5cd

Please sign in to comment.