Skip to content

Commit

Permalink
Reuse validation code.
Browse files Browse the repository at this point in the history
Javascript imports in options.js.
Reuse submitting code.
  • Loading branch information
basilevs committed Feb 10, 2020
1 parent 589882a commit 0617133
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 69 deletions.
11 changes: 5 additions & 6 deletions background.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import {RegexMatcher} from "./matcher.mjs";


// noinspection JSUnresolvedVariable
const tabs = browser.tabs;

if (!tabs)
Expand All @@ -17,9 +17,6 @@ const backgroundPage = browser.runtime.getBackgroundPage();
if (!backgroundPage)
throw new Error("Background page is not found");

const defaultPatterns = RegexMatcher.defaultPatterns();
window.defaultPatterns = defaultPatterns;

function debug() {
console.debug.apply(console, arguments);
}
Expand Down Expand Up @@ -57,9 +54,11 @@ async function getMatcher() {
const data = await sync.get("patterns");
let patterns = data.patterns;
if (!patterns) {
patterns = defaultPatterns;
patterns = RegexMatcher.defaultPatterns();
}
patterns = RegexMatcher.сonvertLinesToRegExp(patterns);
patterns = RegexMatcher.convertLinesToRegExp(
patterns,
(line, error, pattern) => handleError('Error on line', line, pattern, error));
return new RegexMatcher(patterns, debug);
}

Expand Down
25 changes: 18 additions & 7 deletions matcher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,39 @@ class RegexMatcher {
}

static defaultPatterns() {
RegexMatcher.convertLinesToRegExp(defaultConfiguration, (line, error, pattern) => {
throw new Error(`Pattern ${pattern} is invalid: ${error.message}`)
});
return defaultConfiguration;
}

// Returns an error message if any
static validatePattern(line) {
let error = "";
RegexMatcher.convertLinesToRegExp([line], (lineNumber, e) => error = e.message);
return error;
}

static parsePatterns(regexPatternsAsText, handleErrors) {
if (typeof regexPatternsAsText !== "string")
throw new Error("Regex matcher only accepts a list of patterns separated by a newline symbol");
let lines = regexPatternsAsText.split("\n");
return сonvertLinesToRegExp(lines, handleErrors);
return RegexMatcher.convertLinesToRegExp(lines, handleErrors);
}

static сonvertLinesToRegExp(lines, handleErrors) {
static convertLinesToRegExp(lines, handleErrors) {
if (!handleErrors)
throw new Error("Error handler is missing");
let result = [];
for (const i in lines) {
lines.forEach((line, i) => {
try {
const line = lines[i];
if (!line)
continue;
return;
result.push(new RegExp(line));
} catch (e) {
handleErrors(i, e);
handleErrors(i, e, line);
}
}
});
return result;
}

Expand Down
9 changes: 5 additions & 4 deletions options.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<title>ReuseTab Options</title>
<script src="matcher.mjs" type="module"></script>
</head>
<body>
<h1>Blacklist</h1>
Expand All @@ -11,15 +12,15 @@ <h1>Blacklist</h1>
placeholder="Blacklist an URL to require exact match for its tab to be closed or reused."
required
style="width: 100%"
type="URL">
<button id="blacklist_button" disabled type="submit">Blacklist</button>
type="text">
<button id="blacklist_button" disabled type="button">Blacklist</button>
</form>
<h2>Multidomain</h2>
<form id="multidomain_form">
<textarea placeholder="Enter two or more URLs to treat as the same site.&#10;One URL per line.&#10;Then hit &quot;Apply&quot;."
required
rows="3" style="width: 100%"></textarea>
<button accesskey="a" disabled type="submit">Apply</button>
<button accesskey="a" disabled type="button">Apply</button>
</form>
<form id="patterns_form">
<h1>Matching Patterns</h1>
Expand All @@ -29,7 +30,7 @@ <h1>Matching Patterns</h1>
<button accesskey="r" id="restore" type="button">Load</button>
<button accesskey="s" id="save" type="button">Save</button>
</form>
<script src="options.js"></script>
<script src="options.js" type="module"></script>
<h2>Technical information</h2>
<p>
Each line of configuration above is a regular expression. Tab is reused if a regular expression matches both target
Expand Down
163 changes: 112 additions & 51 deletions options.js
Original file line number Diff line number Diff line change
@@ -1,74 +1,135 @@
"use strict";

import {RegexMatcher} from "./matcher.mjs";

const storage = browser.storage;
const sync = storage.sync;
if (!sync)
throw new Error("Storage is not available");

const backgroundPage = browser.runtime.getBackgroundPage();
const defaultPatternsPromise = backgroundPage.then(x => x.defaultPatterns);
function debug(...args) {
console.debug(...args);
}

function debug() {
console.debug.apply(console, arguments);
function functionEntry(functionName, ...args) {
debug("Entered function: ", functionName, ...args);
}

function onError(error) {
console.error(error);
}


async function loadPatterns() {
let result = await defaultPatternsPromise;
functionEntry("loadPatterns");
let result = RegexMatcher.defaultPatterns();
if (!result)
throw new Error("No default patterns found");
const data = await sync.get("patterns");
debug("Storage now: ", data);
if (data && data.patterns) {
result = data.patterns;
}
debug("Resolved current patterns:", result);
if (!result instanceof Array)
throw new Error("Array of strings is expected");
return result;
}

async function addPattern(...patterns) {
functionEntry("addPattern", ...patterns);
for (let pattern of patterns) {
const error = RegexMatcher.validatePattern(pattern);
if (error)
throw new Error(`Pattern ${pattern} is invalid: ${error}`);
}
const stored = await loadPatterns();
debug("old pattern set: ", ...stored);
for (let pattern of patterns) {
stored.unshift(pattern);
}
if (!stored instanceof Array)
throw new Error("Array of strings is expected");
debug("new pattern set: ", ...stored);
await sync.set({
"patterns": stored
});
}

function wrapErrors(asyncFunction) {
function wrapper(...args) {
asyncFunction(...args).catch(onError);
try {
Promise.resolve(asyncFunction(...args)).catch(onError);
} catch (e) {
onError(e);
}
}

return wrapper;
}

function disableIfInvalid(input, button) {
if (!button)
throw new Error("Button is null");
if (!input.validity)
throw new Error("Input does not support validation");
input.addEventListener("input", () => {
button.disabled = !input.validity.valid;
});
function installValidator(input, validityConsumer, validate) {
if (!input)
throw new Error("null input");
if (!validityConsumer)
throw new Error("null button");
if (!validate)
throw new Error("null validate");
function report(value, result) {
debug("Validating ", value, ":", result);
input.setCustomValidity(result);
validityConsumer(!result);
}
function check() {
const value = input.value;
Promise.resolve(validate(value))
.then(e => {
report(value, e);
})
.catch(e => {
report(value, e);
onError(e);
});
}
input.addEventListener("input", check);
}

function installAddingSubmitter(button, input, newPatternsProvider) {
const form = button.closest("form");
if (!form)
throw new Error("Can't find form");

button.addEventListener("click", wrapErrors(async () => {
if (!input.validity.valid)
return;
const patterns = await newPatternsProvider();
await addPattern(patterns);
form.reset();
}));
}

function checkURL(string) {
try {
new URL(string)
} catch (e) {
return e.message;
}
return "";
}

{
const form = document.querySelector('form#blacklist_form');
const blacklist_text = form.querySelector("input");
const blacklist = form.querySelector("button");
disableIfInvalid(blacklist_text, blacklist);
blacklist.addEventListener("click", wrapErrors(async () => {
if (!blacklist_text.validity.valid)
return;
installValidator(blacklist_text,
isValid => blacklist.disabled = !isValid,
value => {
return checkURL(value);
});
installAddingSubmitter(blacklist, blacklist_text, async () => {
let choice = blacklist_text.value;
choice = choice.replace(/[.]/g, "\\.");
await addPattern(choice + "(.*)");
}));
return [choice + "(.*)"];
});
}


Expand All @@ -77,47 +138,34 @@ function disableIfInvalid(input, button) {
const multidomain_text = form.querySelector("textarea");
const apply_multidomain = form.querySelector("button");

function checkURL(string) {
try {
new URL(string)
} catch (e) {
return e.message;
}
return "";
}

function massage(array) {
function uniq(array) {
array = array.filter(s => !!s);
array = Array.from(new Set(array));
return array;
}

multidomain_text.addEventListener("input", () => {
let array = multidomain_text.value.split("\n");
array = massage(array);
let error = "";
installValidator(multidomain_text,
isValid => apply_multidomain.disabled = !isValid,
value => {
let array = value.split("\n");
array = uniq(array);
for (const url of array) {
const e = checkURL(url);
if (e) {
error = e;
break;
return e;
}
}
if (!error) {
if (array.length < 2) {
error = "Add another URL";
}
if (array.length < 2) {
return "Add another URL";
}
multidomain_text.setCustomValidity(error);
return "";
});
apply_multidomain.addEventListener("click", wrapErrors(async () => {
installAddingSubmitter(apply_multidomain,multidomain_text, async () => {
let choice = multidomain_text.value.split("\n");
choice = massage(choice);
choice = uniq(choice);
choice = choice.join("|");
choice = choice.replace(/[.]/g, "\\.");
await addPattern(`(?:${choice}).*`);
}));
disableIfInvalid(multidomain_text, apply_multidomain);
return [`(?:${choice}).*`];
});
}

{
Expand All @@ -126,18 +174,31 @@ function disableIfInvalid(input, button) {
if (!matchingPatternsText)
throw new Error("Patterns textarea is not found");

function fireInputEvent(input) {
const event = input.ownerDocument.createEvent("Event");
event.initEvent("input", true, true);
event.synthetic = true;
input.dispatchEvent(event);
}

async function setPatterns(data) {
debug("Resetting patterns", data);
if (!data)
throw new Error("Empty argument");
matchingPatternsText.value = data.join("\n");
fireInputEvent(matchingPatternsText);
}

async function restore() {
await setPatterns(await loadPatterns());
}


installValidator(matchingPatternsText, () => {}, async value => {
let result = [];
RegexMatcher.parsePatterns(value, (lineNumber, error, pattern) => {
result.push(`Line ${lineNumber}: ${pattern} : ${error.message}`);
});
return result.join("\n");
});
form.querySelector("#save").addEventListener("click", wrapErrors(async event => {
debug("Saving", event, matchingPatternsText.value);
await sync.set({
Expand All @@ -146,7 +207,7 @@ function disableIfInvalid(input, button) {
}));
form.querySelector("#default").addEventListener("click", wrapErrors(async event => {
debug("Form set to default", event, matchingPatternsText.value);
await setPatterns(await defaultPatternsPromise);
await setPatterns(RegexMatcher.defaultPatterns());
}));
form.querySelector("#restore").addEventListener("click", wrapErrors(restore));
const storageListener = wrapErrors(async (changes, areaName) => {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"web-ext": "^4.0.0"
},
"scripts": {
"start": "web-ext run --firefox=firefoxdeveloperedition --source-dir ./",
"start": "web-ext run --verbose --firefox=firefoxdeveloperedition --source-dir ./",
"build": "web-ext build --source-dir ./"
},
"dependencies": {},
Expand Down

0 comments on commit 0617133

Please sign in to comment.