mirror of
https://github.com/vale981/tridactyl
synced 2025-03-06 10:01:39 -05:00
Implement site-specific settings
This PR implements site-specific settings. It requires multiple changes and new features. First, I needed to create a new value in `window.tri` named `contentLocation`. This contentLocation object is either a Location or URL object containing the URL of the currently selected tab of the currently selected window. This is required because config.get() is not asynchronous and we probably want to keep it that way, thus we can't message the content script just to get the current url. Then, we create a new object, URLCONFIGS, in content.ts. It behaves exactly the same as USERCONFIG, except it has a level of indirection, matching url patterns to config objects. This requires creating new seturl, geturl and unseturl functions which behave mostly the same as set, get and unset. Last, we create a `seturl` ex command in order to interact with this new object.
This commit is contained in:
parent
7bf5198e65
commit
1a1688a83f
4 changed files with 169 additions and 35 deletions
|
@ -33,8 +33,29 @@ import { AutoContain } from "./lib/autocontainers"
|
|||
state,
|
||||
webext,
|
||||
l: prom => prom.then(console.log).catch(console.error),
|
||||
contentLocation: window.location,
|
||||
})
|
||||
|
||||
// {{{ tri.contentLocation
|
||||
// When loading the background, use the active tab to know what the current content url is
|
||||
let dateUpdated
|
||||
browser.tabs.query({ currentWindow: true, active: true }).then(t => {
|
||||
;(window as any).tri.contentLocation = new URL(t[0].url)
|
||||
dateUpdated = performance.now()
|
||||
})
|
||||
// After that, on every tab change, update the current url
|
||||
// Experiments show that context switching+performing the api call costs more than 2ms so performance.now()'s resolution should be precise enough for it to be used when we need to protect ourselves against race conditions
|
||||
browser.tabs.onActivated.addListener(ev => {
|
||||
browser.tabs.get(ev.tabId).then(t => {
|
||||
let perf = performance.now()
|
||||
if (dateUpdated <= perf) {
|
||||
;(window as any).tri.contentLocation = new URL(t.url)
|
||||
dateUpdated = perf
|
||||
}
|
||||
})
|
||||
})
|
||||
//
|
||||
|
||||
// Send commandline to controller
|
||||
commandline_background.onLine.addListener(BackgroundController.acceptExCmd)
|
||||
|
||||
|
@ -88,6 +109,9 @@ browser.runtime.onStartup.addListener(_ => {
|
|||
})
|
||||
})
|
||||
|
||||
// }}}
|
||||
|
||||
// {{{ AUTOCOMMANDS
|
||||
let curTab = null
|
||||
browser.tabs.onActivated.addListener(ev => {
|
||||
let ignore = _ => _
|
||||
|
@ -103,6 +127,8 @@ browser.tabs.onActivated.addListener(ev => {
|
|||
.catch(ignore)
|
||||
})
|
||||
|
||||
// }}}
|
||||
|
||||
// {{{ AUTOCONTAINERS
|
||||
|
||||
let aucon = new AutoContain()
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
/** @hidden */
|
||||
const CONFIGNAME = "userconfig"
|
||||
/** @hidden */
|
||||
const URLCONFIGNAME = "URLCONFIGS"
|
||||
/** @hidden */
|
||||
const WAITERS = []
|
||||
/** @hidden */
|
||||
let INITIALISED = false
|
||||
|
@ -37,6 +39,11 @@ function schlepp(settings) {
|
|||
/** @hidden */
|
||||
let USERCONFIG = o({})
|
||||
|
||||
/** @hidden
|
||||
* Site-specific configs. The key should be a string representing a regex that matches an URL, the value should be an object that contains the keys that can be found in default_config.
|
||||
**/
|
||||
let URLCONFIGS = o({})
|
||||
|
||||
/** @hidden
|
||||
* Ideally, LoggingLevel should be in logging.ts and imported from there. However this would cause a circular dependency, which webpack can't deal with
|
||||
*/
|
||||
|
@ -685,6 +692,23 @@ function setDeepProperty(obj, value, target) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getUrl(url, target) {
|
||||
let url_match =
|
||||
// For each key
|
||||
Object.keys(URLCONFIGS)
|
||||
// Create a tuple containing the key and its match
|
||||
.map(k => [k, url.match(k)])
|
||||
// Keep only the ones that have a match
|
||||
.filter(([_, m]) => m)
|
||||
// Sort them from the longest match to the shortest
|
||||
.sort(([k1, m1], [k2, m2]) => m2.length - m1.length)
|
||||
// Get the first config name that has `target`
|
||||
.find(([k, _]) => getDeepProperty(URLCONFIGS[k], target))
|
||||
|
||||
if (url_match && url_match[0])
|
||||
return getDeepProperty(URLCONFIGS[url_match[0]], target)
|
||||
}
|
||||
|
||||
/** Get the value of the key target.
|
||||
|
||||
If the user has not specified a key, use the corresponding key from
|
||||
|
@ -692,14 +716,21 @@ function setDeepProperty(obj, value, target) {
|
|||
@hidden
|
||||
*/
|
||||
export function get(...target) {
|
||||
// Window.tri might not be defined when called from the untrusted page context
|
||||
let loc = window.location
|
||||
if ((window as any).tri) loc = (window as any).tri.contentLocation
|
||||
// If there's a site-specifing setting, it overrides global settings
|
||||
const site = getUrl(loc.href, target)
|
||||
const user = getDeepProperty(USERCONFIG, target)
|
||||
const defult = getDeepProperty(DEFAULTS, target)
|
||||
|
||||
// Merge results if there's a default value and it's not an Array or primitive.
|
||||
if (defult && (!Array.isArray(defult) && typeof defult === "object")) {
|
||||
return Object.assign(o({}), defult, user)
|
||||
return Object.assign(Object.assign(o({}), defult, user), site)
|
||||
} else {
|
||||
if (user !== undefined) {
|
||||
if (site !== undefined) {
|
||||
return site
|
||||
} else if (user !== undefined) {
|
||||
return user
|
||||
} else {
|
||||
return defult
|
||||
|
@ -723,6 +754,30 @@ export async function getAsync(...target) {
|
|||
}
|
||||
}
|
||||
|
||||
function genericSet(config, args) {
|
||||
if (args.length < 2) {
|
||||
throw "You must provide at least two arguments!"
|
||||
}
|
||||
|
||||
const target = args.slice(0, args.length - 1)
|
||||
const value = args[args.length - 1]
|
||||
|
||||
setDeepProperty(config, value, target)
|
||||
save()
|
||||
}
|
||||
|
||||
/** Same as [[set]], but for URLCONFIGS[pattern] instead of USERCONFIG. */
|
||||
export function setUrl(pattern, ...args) {
|
||||
if (!URLCONFIGS[pattern]) URLCONFIGS[pattern] = {}
|
||||
|
||||
try {
|
||||
genericSet(URLCONFIGS[pattern], args)
|
||||
} catch (e) {
|
||||
if (e == "You must provide at least two arguments!")
|
||||
throw e.replace("two", "three")
|
||||
}
|
||||
}
|
||||
|
||||
/** Full target specification, then value
|
||||
|
||||
e.g.
|
||||
|
@ -733,23 +788,27 @@ export async function getAsync(...target) {
|
|||
@hidden
|
||||
*/
|
||||
export function set(...args) {
|
||||
if (args.length < 2) {
|
||||
throw "You must provide at least two arguments!"
|
||||
}
|
||||
genericSet(USERCONFIG, args)
|
||||
}
|
||||
|
||||
const target = args.slice(0, args.length - 1)
|
||||
const value = args[args.length - 1]
|
||||
|
||||
setDeepProperty(USERCONFIG, value, target)
|
||||
/** Delete the key at target in config if it exists
|
||||
* @hidden */
|
||||
function genericUnset(config, target) {
|
||||
const parent = getDeepProperty(config, target.slice(0, -1))
|
||||
if (parent !== undefined) delete parent[target[target.length - 1]]
|
||||
save()
|
||||
}
|
||||
|
||||
/** Delete the key at target if it exists
|
||||
/** Delete the key at pattern.target in URLCONFIGS if it exists
|
||||
* @hidden */
|
||||
export function unsetURL(pattern, ...target) {
|
||||
genericUnset(URLCONFIGS[pattern], target)
|
||||
}
|
||||
|
||||
/** Delete the key at target in USERCONFIG if it exists
|
||||
* @hidden */
|
||||
export function unset(...target) {
|
||||
const parent = getDeepProperty(USERCONFIG, target.slice(0, -1))
|
||||
if (parent !== undefined) delete parent[target[target.length - 1]]
|
||||
save()
|
||||
genericUnset(USERCONFIG, target)
|
||||
}
|
||||
|
||||
/** Save the config back to storage API.
|
||||
|
@ -764,6 +823,7 @@ export async function save(storage: "local" | "sync" = get("storageloc")) {
|
|||
// storageobj.set({CONFIGNAME: USERCONFIG})
|
||||
let settingsobj = o({})
|
||||
settingsobj[CONFIGNAME] = USERCONFIG
|
||||
settingsobj[URLCONFIGNAME] = URLCONFIGS
|
||||
if (storage == "local") browser.storage.local.set(settingsobj)
|
||||
else browser.storage.sync.set(settingsobj)
|
||||
}
|
||||
|
@ -860,6 +920,16 @@ browser.storage.onChanged.addListener(async (changes, areaname) => {
|
|||
USERCONFIG = o({})
|
||||
}
|
||||
}
|
||||
// TODO: Find a way to remove duplication
|
||||
if (URLCONFIGNAME in changes) {
|
||||
// newValue is undefined when calling browser.storage.AREANAME.clear()
|
||||
if (changes[URLCONFIGNAME].newValue !== undefined) {
|
||||
URLCONFIGS = changes[URLCONFIGNAME].newValue
|
||||
} else if (areaname === (await get("storageloc"))) {
|
||||
// If newValue is undefined and AREANAME is the same value as STORAGELOC, the user wants to clean their config
|
||||
URLCONFIGS = o({})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
init()
|
||||
|
|
|
@ -68,6 +68,7 @@ import * as styling from "./styling"
|
|||
l: prom => prom.then(console.log).catch(console.error),
|
||||
native,
|
||||
styling,
|
||||
contentLocation: window.location,
|
||||
})
|
||||
|
||||
logger.info("Loaded commandline content?", commandline_content)
|
||||
|
|
|
@ -108,6 +108,8 @@ import * as Logging from "./logging"
|
|||
const logger = new Logging.Logger("excmds")
|
||||
import Mark from "mark.js"
|
||||
import * as CSS from "css"
|
||||
import * as Metadata from "./.metadata.generated"
|
||||
import { fitsType, typeToString } from "./metadata"
|
||||
|
||||
//#content_helper
|
||||
// {
|
||||
|
@ -133,8 +135,6 @@ import { mapstrToKeyseq } from "./keyseq"
|
|||
|
||||
//#background_helper
|
||||
import * as Native from "./native_background"
|
||||
import * as Metadata from "./.metadata.generated"
|
||||
import { fitsType, typeToString } from "./metadata"
|
||||
|
||||
/** @hidden */
|
||||
export const cmd_params = new Map<string, Map<string, string>>()
|
||||
|
@ -2529,6 +2529,62 @@ export function searchsetkeyword(keyword: string, url: string) {
|
|||
config.set("searchurls", keyword, forceURI(url))
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates arguments for set/seturl
|
||||
* @hidden
|
||||
*/
|
||||
function validateSetArgs(key: string, values: string[]) {
|
||||
const target = key.split(".")
|
||||
const currentValue = config.get(...target)
|
||||
const last = target[target.length - 1]
|
||||
|
||||
let value: string | string[] = values
|
||||
if (Array.isArray(currentValue)) {
|
||||
// Do nothing
|
||||
} else if (currentValue === undefined || typeof currentValue === "string") {
|
||||
value = values.join(" ")
|
||||
} else {
|
||||
throw "Unsupported setting type!"
|
||||
}
|
||||
|
||||
let md = Metadata.everything["src/config.ts"].classes.default_config[last]
|
||||
if (md) {
|
||||
if (md.type && !fitsType(value, md.type)) throw `Given type does not match expected type (given: ${value}, expected: ${typeToString(md.type)})`
|
||||
}
|
||||
|
||||
return target.concat(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Usage: `seturl [pattern] key values`
|
||||
*
|
||||
* @param pattern Optional. The URL pattern the setting should be set for, e.g. `en.wikipedia.org` or `/index.html`
|
||||
* @param key The name of the setting you want to set, e.g. `followpagepatterns.next`
|
||||
* @param values The value you wish for, e.g. `next`
|
||||
*
|
||||
* Example:
|
||||
* `seturl .*\.fr followpagepatterns.next suivant`
|
||||
* `seturl website.fr followpagepatterns.next next`
|
||||
*
|
||||
* When multiple patterns can apply to a same URL, the pattern that gives the largest match is selected. For example, in the previous example, `followpagepatterns.next` would be set to `suivant` on `http://website.fr` because `.*\.fr` matches the whole url while `website.fr` only matches the domain name.
|
||||
*
|
||||
* Note that the patterns a regex-like, not glob-like. This means that if you want to match everything, you need to use `.*` instead of `*`.
|
||||
*/
|
||||
//#content
|
||||
export function seturl(pattern: string, key: string, ...values: string[]) {
|
||||
if (values.length == 0 && key) {
|
||||
values = [key]
|
||||
key = pattern
|
||||
pattern = window.location.href
|
||||
}
|
||||
|
||||
if (!pattern || !key || !values.length) {
|
||||
throw "seturl syntax: [pattern] key value"
|
||||
}
|
||||
|
||||
config.setUrl(pattern, ...validateSetArgs(key, values))
|
||||
}
|
||||
|
||||
/** Set a key value pair in config.
|
||||
|
||||
Use to set any string values found [here](/static/docs/classes/_src_config_.default_config.html).
|
||||
|
@ -2548,26 +2604,7 @@ export function set(key: string, ...values: string[]) {
|
|||
return
|
||||
}
|
||||
|
||||
const target = key.split(".")
|
||||
const last = target[target.length - 1]
|
||||
|
||||
const currentValue = config.get(...target)
|
||||
|
||||
let value: string | string[] = values
|
||||
if (Array.isArray(currentValue)) {
|
||||
// Do nothing
|
||||
} else if (currentValue === undefined || typeof currentValue === "string") {
|
||||
value = values.join(" ")
|
||||
} else {
|
||||
throw "Unsupported setting type!"
|
||||
}
|
||||
|
||||
let md = Metadata.everything["src/config.ts"].classes.default_config[last]
|
||||
if (md) {
|
||||
if (md.type && !fitsType(value, md.type)) throw `Given type does not match expected type (given: ${value}, expected: ${typeToString(md.type)})`
|
||||
}
|
||||
|
||||
config.set(...target, value)
|
||||
config.set(...validateSetArgs(key, values))
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
|
|
Loading…
Add table
Reference in a new issue