diff options
Diffstat (limited to 'src/shared')
-rw-r--r-- | src/shared/SettingData.ts | 224 | ||||
-rw-r--r-- | src/shared/Settings.ts | 200 | ||||
-rw-r--r-- | src/shared/blacklists.ts | 13 | ||||
-rw-r--r-- | src/shared/properties.ts | 50 | ||||
-rw-r--r-- | src/shared/property-defs.ts | 56 | ||||
-rw-r--r-- | src/shared/settings/Blacklist.ts | 39 | ||||
-rw-r--r-- | src/shared/settings/Key.ts | 61 | ||||
-rw-r--r-- | src/shared/settings/KeySequence.ts | 54 | ||||
-rw-r--r-- | src/shared/settings/Keymaps.ts | 37 | ||||
-rw-r--r-- | src/shared/settings/Properties.ts | 110 | ||||
-rw-r--r-- | src/shared/settings/Search.ts | 76 | ||||
-rw-r--r-- | src/shared/settings/Settings.ts | 158 | ||||
-rw-r--r-- | src/shared/urls.ts | 4 | ||||
-rw-r--r-- | src/shared/utils/re.ts | 6 |
14 files changed, 611 insertions, 477 deletions
diff --git a/src/shared/SettingData.ts b/src/shared/SettingData.ts index 14a7d35..532570e 100644 --- a/src/shared/SettingData.ts +++ b/src/shared/SettingData.ts @@ -1,15 +1,19 @@ import * as operations from './operations'; -import Settings, * as settings from './Settings'; +import Settings, { DefaultSettingJSONText } from './settings/Settings'; +import Keymaps from './settings/Keymaps'; +import Search from './settings/Search'; +import Properties from './settings/Properties'; +import Blacklist from './settings/Blacklist'; export class FormKeymaps { - private data: {[op: string]: string}; + private readonly data: {[op: string]: string}; - constructor(data: {[op: string]: string}) { + private constructor(data: {[op: string]: string}) { this.data = data; } - toKeymaps(): settings.Keymaps { - let keymaps: settings.Keymaps = {}; + toKeymaps(): Keymaps { + let keymaps: { [key: string]: operations.Operation } = {}; for (let name of Object.keys(this.data)) { let [type, argStr] = name.split('?'); let args = {}; @@ -19,7 +23,7 @@ export class FormKeymaps { let key = this.data[name]; keymaps[key] = operations.valueOf({ type, ...args }); } - return keymaps; + return Keymaps.fromJSON(keymaps); } toJSON(): {[op: string]: string} { @@ -34,7 +38,7 @@ export class FormKeymaps { return new FormKeymaps(newData); } - static valueOf(o: ReturnType<FormKeymaps['toJSON']>): FormKeymaps { + static fromJSON(o: ReturnType<FormKeymaps['toJSON']>): FormKeymaps { let data: {[op: string]: string} = {}; for (let op of Object.keys(o)) { data[op] = o[op] as string; @@ -42,10 +46,11 @@ export class FormKeymaps { return new FormKeymaps(data); } - static fromKeymaps(keymaps: settings.Keymaps): FormKeymaps { + static fromKeymaps(keymaps: Keymaps): FormKeymaps { + let json = keymaps.toJSON(); let data: {[op: string]: string} = {}; - for (let key of Object.keys(keymaps)) { - let op = keymaps[key]; + for (let key of Object.keys(json)) { + let op = json[key]; let args = { ...op }; delete args.type; @@ -60,24 +65,21 @@ export class FormKeymaps { } export class FormSearch { - private default: string; + private readonly default: string; - private engines: string[][]; + private readonly engines: string[][]; constructor(defaultEngine: string, engines: string[][]) { this.default = defaultEngine; this.engines = engines; } - toSearchSettings(): settings.Search { - return { - default: this.default, - engines: this.engines.reduce( - (o: {[key: string]: string}, [name, url]) => { - o[name] = url; - return o; - }, {}), - }; + toSearchSettings(): Search { + let engines: { [name: string]: string } = {}; + for (let entry of this.engines) { + engines[entry[0]] = entry[1]; + } + return new Search(this.default, engines); } toJSON(): { @@ -90,7 +92,7 @@ export class FormSearch { }; } - static valueOf(o: ReturnType<FormSearch['toJSON']>): FormSearch { + static fromJSON(o: ReturnType<FormSearch['toJSON']>): FormSearch { if (!Object.prototype.hasOwnProperty.call(o, 'default')) { throw new TypeError(`"default" field not set`); } @@ -100,53 +102,58 @@ export class FormSearch { return new FormSearch(o.default, o.engines); } - static fromSearch(search: settings.Search): FormSearch { + static fromSearch(search: Search): FormSearch { let engines = Object.entries(search.engines).reduce( (o: string[][], [name, url]) => { return o.concat([[name, url]]); }, []); - return new FormSearch(search.default, engines); + return new FormSearch(search.defaultEngine, engines); } } -export class JSONSettings { - private json: string; - - constructor(json: any) { - this.json = json; +export class JSONTextSettings { + constructor( + private json: string, + ) { } toSettings(): Settings { - return settings.valueOf(JSON.parse(this.json)); + return Settings.fromJSON(JSON.parse(this.json)); } - toJSON(): string { + toJSONText(): string { return this.json; } - static valueOf(o: ReturnType<JSONSettings['toJSON']>): JSONSettings { - return new JSONSettings(o); + static fromText(o: string): JSONTextSettings { + return new JSONTextSettings(o); } - static fromSettings(data: Settings): JSONSettings { - return new JSONSettings(JSON.stringify(data, undefined, 2)); + static fromSettings(data: Settings): JSONTextSettings { + let json = { + keymaps: data.keymaps.toJSON(), + search: data.search, + properties: data.properties, + blacklist: data.blacklist, + }; + return new JSONTextSettings(JSON.stringify(json, undefined, 2)); } } export class FormSettings { - private keymaps: FormKeymaps; + public readonly keymaps: FormKeymaps; - private search: FormSearch; + public readonly search: FormSearch; - private properties: settings.Properties; + public readonly properties: Properties; - private blacklist: string[]; + public readonly blacklist: Blacklist; constructor( keymaps: FormKeymaps, search: FormSearch, - properties: settings.Properties, - blacklist: string[], + properties: Properties, + blacklist: Blacklist, ) { this.keymaps = keymaps; this.search = search; @@ -172,7 +179,7 @@ export class FormSettings { ); } - buildWithProperties(props: settings.Properties): FormSettings { + buildWithProperties(props: Properties): FormSettings { return new FormSettings( this.keymaps, this.search, @@ -181,7 +188,7 @@ export class FormSettings { ); } - buildWithBlacklist(blacklist: string[]): FormSettings { + buildWithBlacklist(blacklist: Blacklist): FormSettings { return new FormSettings( this.keymaps, this.search, @@ -191,39 +198,39 @@ export class FormSettings { } toSettings(): Settings { - return settings.valueOf({ - keymaps: this.keymaps.toKeymaps(), - search: this.search.toSearchSettings(), - properties: this.properties, - blacklist: this.blacklist, + return Settings.fromJSON({ + keymaps: this.keymaps.toKeymaps().toJSON(), + search: this.search.toSearchSettings().toJSON(), + properties: this.properties.toJSON(), + blacklist: this.blacklist.toJSON(), }); } toJSON(): { keymaps: ReturnType<FormKeymaps['toJSON']>; search: ReturnType<FormSearch['toJSON']>; - properties: settings.Properties; - blacklist: string[]; + properties: ReturnType<Properties['toJSON']>; + blacklist: ReturnType<Blacklist['toJSON']>; } { return { keymaps: this.keymaps.toJSON(), search: this.search.toJSON(), - properties: this.properties, - blacklist: this.blacklist, + properties: this.properties.toJSON(), + blacklist: this.blacklist.toJSON(), }; } - static valueOf(o: ReturnType<FormSettings['toJSON']>): FormSettings { + static fromJSON(o: ReturnType<FormSettings['toJSON']>): FormSettings { for (let name of ['keymaps', 'search', 'properties', 'blacklist']) { if (!Object.prototype.hasOwnProperty.call(o, name)) { throw new Error(`"${name}" field not set`); } } return new FormSettings( - FormKeymaps.valueOf(o.keymaps), - FormSearch.valueOf(o.search), - settings.propertiesValueOf(o.properties), - settings.blacklistValueOf(o.blacklist), + FormKeymaps.fromJSON(o.keymaps), + FormSearch.fromJSON(o.search), + Properties.fromJSON(o.properties), + Blacklist.fromJSON(o.blacklist), ); } @@ -244,7 +251,7 @@ export enum SettingSource { export default class SettingData { private source: SettingSource; - private json?: JSONSettings; + private json?: JSONTextSettings; private form?: FormSettings; @@ -252,7 +259,7 @@ export default class SettingData { source, json, form }: { source: SettingSource, - json?: JSONSettings, + json?: JSONTextSettings, form?: FormSettings, }) { this.source = source; @@ -264,7 +271,7 @@ export default class SettingData { return this.source; } - getJSON(): JSONSettings { + getJSON(): JSONTextSettings { if (!this.json) { throw new TypeError('json settings not set'); } @@ -283,7 +290,7 @@ export default class SettingData { case SettingSource.JSON: return { source: this.source, - json: (this.json as JSONSettings).toJSON(), + json: (this.json as JSONTextSettings).toJSONText(), }; case SettingSource.Form: return { @@ -304,7 +311,7 @@ export default class SettingData { throw new Error(`unknown settings source: ${this.source}`); } - static valueOf(o: { + static fromJSON(o: { source: string; json?: string; form?: ReturnType<FormSettings['toJSON']>; @@ -313,13 +320,13 @@ export default class SettingData { case SettingSource.JSON: return new SettingData({ source: o.source, - json: JSONSettings.valueOf( - o.json as ReturnType<JSONSettings['toJSON']>), + json: JSONTextSettings.fromText( + o.json as ReturnType<JSONTextSettings['toJSONText']>), }); case SettingSource.Form: return new SettingData({ source: o.source, - form: FormSettings.valueOf( + form: FormSettings.fromJSON( o.form as ReturnType<FormSettings['toJSON']>), }); } @@ -327,90 +334,7 @@ export default class SettingData { } } -export const DefaultSettingData: SettingData = SettingData.valueOf({ +export const DefaultSettingData: SettingData = SettingData.fromJSON({ source: 'json', - json: `{ - "keymaps": { - "0": { "type": "scroll.home" }, - ":": { "type": "command.show" }, - "o": { "type": "command.show.open", "alter": false }, - "O": { "type": "command.show.open", "alter": true }, - "t": { "type": "command.show.tabopen", "alter": false }, - "T": { "type": "command.show.tabopen", "alter": true }, - "w": { "type": "command.show.winopen", "alter": false }, - "W": { "type": "command.show.winopen", "alter": true }, - "b": { "type": "command.show.buffer" }, - "a": { "type": "command.show.addbookmark", "alter": true }, - "k": { "type": "scroll.vertically", "count": -1 }, - "j": { "type": "scroll.vertically", "count": 1 }, - "h": { "type": "scroll.horizonally", "count": -1 }, - "l": { "type": "scroll.horizonally", "count": 1 }, - "<C-U>": { "type": "scroll.pages", "count": -0.5 }, - "<C-D>": { "type": "scroll.pages", "count": 0.5 }, - "<C-B>": { "type": "scroll.pages", "count": -1 }, - "<C-F>": { "type": "scroll.pages", "count": 1 }, - "gg": { "type": "scroll.top" }, - "G": { "type": "scroll.bottom" }, - "$": { "type": "scroll.end" }, - "d": { "type": "tabs.close" }, - "D": { "type": "tabs.close", "select": "left" }, - "x$": { "type": "tabs.close.right" }, - "!d": { "type": "tabs.close.force" }, - "u": { "type": "tabs.reopen" }, - "K": { "type": "tabs.prev" }, - "J": { "type": "tabs.next" }, - "gT": { "type": "tabs.prev" }, - "gt": { "type": "tabs.next" }, - "g0": { "type": "tabs.first" }, - "g$": { "type": "tabs.last" }, - "<C-6>": { "type": "tabs.prevsel" }, - "r": { "type": "tabs.reload", "cache": false }, - "R": { "type": "tabs.reload", "cache": true }, - "zp": { "type": "tabs.pin.toggle" }, - "zd": { "type": "tabs.duplicate" }, - "zi": { "type": "zoom.in" }, - "zo": { "type": "zoom.out" }, - "zz": { "type": "zoom.neutral" }, - "f": { "type": "follow.start", "newTab": false }, - "F": { "type": "follow.start", "newTab": true, "background": false }, - "m": { "type": "mark.set.prefix" }, - "'": { "type": "mark.jump.prefix" }, - "H": { "type": "navigate.history.prev" }, - "L": { "type": "navigate.history.next" }, - "[[": { "type": "navigate.link.prev" }, - "]]": { "type": "navigate.link.next" }, - "gu": { "type": "navigate.parent" }, - "gU": { "type": "navigate.root" }, - "gi": { "type": "focus.input" }, - "gf": { "type": "page.source" }, - "gh": { "type": "page.home" }, - "gH": { "type": "page.home", "newTab": true }, - "y": { "type": "urls.yank" }, - "p": { "type": "urls.paste", "newTab": false }, - "P": { "type": "urls.paste", "newTab": true }, - "/": { "type": "find.start" }, - "n": { "type": "find.next" }, - "N": { "type": "find.prev" }, - ".": { "type": "repeat.last" }, - "<S-Esc>": { "type": "addon.toggle.enabled" } - }, - "search": { - "default": "google", - "engines": { - "google": "https://google.com/search?q={}", - "yahoo": "https://search.yahoo.com/search?p={}", - "bing": "https://www.bing.com/search?q={}", - "duckduckgo": "https://duckduckgo.com/?q={}", - "twitter": "https://twitter.com/search?q={}", - "wikipedia": "https://en.wikipedia.org/w/index.php?search={}" - } - }, - "properties": { - "hintchars": "abcdefghijklmnopqrstuvwxyz", - "smoothscroll": false, - "complete": "sbh" - }, - "blacklist": [ - ] -}`, + json: DefaultSettingJSONText, }); diff --git a/src/shared/Settings.ts b/src/shared/Settings.ts deleted file mode 100644 index d338e2a..0000000 --- a/src/shared/Settings.ts +++ /dev/null @@ -1,200 +0,0 @@ -import * as operations from './operations'; -import * as PropertyDefs from './property-defs'; - -export type Keymaps = {[key: string]: operations.Operation}; - -export interface Search { - default: string; - engines: { [key: string]: string }; -} - -export interface Properties { - hintchars: string; - smoothscroll: boolean; - complete: string; -} - -export default interface Settings { - keymaps: Keymaps; - search: Search; - properties: Properties; - blacklist: string[]; -} - -export const keymapsValueOf = (o: any): Keymaps => { - return Object.keys(o).reduce((keymaps: Keymaps, key: string): Keymaps => { - let op = operations.valueOf(o[key]); - keymaps[key] = op; - return keymaps; - }, {}); -}; - -export const searchValueOf = (o: any): Search => { - if (typeof o.default !== 'string') { - throw new TypeError('string field "default" not set"'); - } - for (let name of Object.keys(o.engines)) { - if ((/\s/).test(name)) { - throw new TypeError( - `While space in the search engine not allowed: "${name}"`); - } - let url = o.engines[name]; - if (typeof url !== 'string') { - throw new TypeError('"engines" not an object of string'); - } - let matches = url.match(/{}/g); - if (matches === null) { - throw new TypeError(`No {}-placeholders in URL of "${name}"`); - } else if (matches.length > 1) { - throw new TypeError(`Multiple {}-placeholders in URL of "${name}"`); - } - - } - if (!Object.prototype.hasOwnProperty.call(o.engines, o.default)) { - throw new TypeError(`Default engine "${o.default}" not found`); - } - return { - default: o.default as string, - engines: { ...o.engines }, - }; -}; - -export const propertiesValueOf = (o: any): Properties => { - let defNames = new Set(PropertyDefs.defs.map(def => def.name)); - let unknownName = Object.keys(o).find(name => !defNames.has(name)); - if (unknownName) { - throw new TypeError(`Unknown property name: "${unknownName}"`); - } - - for (let def of PropertyDefs.defs) { - if (!Object.prototype.hasOwnProperty.call(o, def.name)) { - continue; - } - if (typeof o[def.name] !== def.type) { - throw new TypeError(`property "${def.name}" is not ${def.type}`); - } - } - return { - ...PropertyDefs.defaultValues, - ...o, - }; -}; - -export const blacklistValueOf = (o: any): string[] => { - if (!Array.isArray(o)) { - throw new TypeError(`"blacklist" is not an array of string`); - } - for (let x of o) { - if (typeof x !== 'string') { - throw new TypeError(`"blacklist" is not an array of string`); - } - } - return o as string[]; -}; - -export const valueOf = (o: any): Settings => { - let settings = { ...DefaultSetting }; - for (let key of Object.keys(o)) { - switch (key) { - case 'keymaps': - settings.keymaps = keymapsValueOf(o.keymaps); - break; - case 'search': - settings.search = searchValueOf(o.search); - break; - case 'properties': - settings.properties = propertiesValueOf(o.properties); - break; - case 'blacklist': - settings.blacklist = blacklistValueOf(o.blacklist); - break; - default: - throw new TypeError('unknown setting: ' + key); - } - } - return settings; -}; - -export const DefaultSetting: Settings = { - keymaps: { - '0': { 'type': 'scroll.home' }, - ':': { 'type': 'command.show' }, - 'o': { 'type': 'command.show.open', 'alter': false }, - 'O': { 'type': 'command.show.open', 'alter': true }, - 't': { 'type': 'command.show.tabopen', 'alter': false }, - 'T': { 'type': 'command.show.tabopen', 'alter': true }, - 'w': { 'type': 'command.show.winopen', 'alter': false }, - 'W': { 'type': 'command.show.winopen', 'alter': true }, - 'b': { 'type': 'command.show.buffer' }, - 'a': { 'type': 'command.show.addbookmark', 'alter': true }, - 'k': { 'type': 'scroll.vertically', 'count': -1 }, - 'j': { 'type': 'scroll.vertically', 'count': 1 }, - 'h': { 'type': 'scroll.horizonally', 'count': -1 }, - 'l': { 'type': 'scroll.horizonally', 'count': 1 }, - '<C-U>': { 'type': 'scroll.pages', 'count': -0.5 }, - '<C-D>': { 'type': 'scroll.pages', 'count': 0.5 }, - '<C-B>': { 'type': 'scroll.pages', 'count': -1 }, - '<C-F>': { 'type': 'scroll.pages', 'count': 1 }, - 'gg': { 'type': 'scroll.top' }, - 'G': { 'type': 'scroll.bottom' }, - '$': { 'type': 'scroll.end' }, - 'd': { 'type': 'tabs.close' }, - 'D': { 'type': 'tabs.close', 'select': 'left' }, - 'x$': { 'type': 'tabs.close.right' }, - '!d': { 'type': 'tabs.close.force' }, - 'u': { 'type': 'tabs.reopen' }, - 'K': { 'type': 'tabs.prev' }, - 'J': { 'type': 'tabs.next' }, - 'gT': { 'type': 'tabs.prev' }, - 'gt': { 'type': 'tabs.next' }, - 'g0': { 'type': 'tabs.first' }, - 'g$': { 'type': 'tabs.last' }, - '<C-6>': { 'type': 'tabs.prevsel' }, - 'r': { 'type': 'tabs.reload', 'cache': false }, - 'R': { 'type': 'tabs.reload', 'cache': true }, - 'zp': { 'type': 'tabs.pin.toggle' }, - 'zd': { 'type': 'tabs.duplicate' }, - 'zi': { 'type': 'zoom.in' }, - 'zo': { 'type': 'zoom.out' }, - 'zz': { 'type': 'zoom.neutral' }, - 'f': { 'type': 'follow.start', 'newTab': false, 'background': false }, - 'F': { 'type': 'follow.start', 'newTab': true, 'background': false }, - 'm': { 'type': 'mark.set.prefix' }, - '\'': { 'type': 'mark.jump.prefix' }, - 'H': { 'type': 'navigate.history.prev' }, - 'L': { 'type': 'navigate.history.next' }, - '[[': { 'type': 'navigate.link.prev' }, - ']]': { 'type': 'navigate.link.next' }, - 'gu': { 'type': 'navigate.parent' }, - 'gU': { 'type': 'navigate.root' }, - 'gi': { 'type': 'focus.input' }, - 'gf': { 'type': 'page.source' }, - 'gh': { 'type': 'page.home', 'newTab': false }, - 'gH': { 'type': 'page.home', 'newTab': true }, - 'y': { 'type': 'urls.yank' }, - 'p': { 'type': 'urls.paste', 'newTab': false }, - 'P': { 'type': 'urls.paste', 'newTab': true }, - '/': { 'type': 'find.start' }, - 'n': { 'type': 'find.next' }, - 'N': { 'type': 'find.prev' }, - '.': { 'type': 'repeat.last' }, - '<S-Esc>': { 'type': 'addon.toggle.enabled' } - }, - search: { - default: 'google', - engines: { - 'google': 'https://google.com/search?q={}', - 'yahoo': 'https://search.yahoo.com/search?p={}', - 'bing': 'https://www.bing.com/search?q={}', - 'duckduckgo': 'https://duckduckgo.com/?q={}', - 'twitter': 'https://twitter.com/search?q={}', - 'wikipedia': 'https://en.wikipedia.org/w/index.php?search={}' - } - }, - properties: { - hintchars: 'abcdefghijklmnopqrstuvwxyz', - smoothscroll: false, - complete: 'sbh' - }, - blacklist: [] -}; diff --git a/src/shared/blacklists.ts b/src/shared/blacklists.ts deleted file mode 100644 index 61ee4de..0000000 --- a/src/shared/blacklists.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as re from './utils/re'; - -const includes = (blacklist: string[], url: string): boolean => { - let u = new URL(url); - return blacklist.some((item) => { - if (!item.includes('/')) { - return re.fromWildcard(item).test(u.host); - } - return re.fromWildcard(item).test(u.host + u.pathname); - }); -}; - -export { includes }; diff --git a/src/shared/properties.ts b/src/shared/properties.ts deleted file mode 100644 index 6315030..0000000 --- a/src/shared/properties.ts +++ /dev/null @@ -1,50 +0,0 @@ -export type Type = string | number | boolean; - -export class Def { - private name0: string; - - private description0: string; - - private defaultValue0: Type; - - constructor( - name: string, - description: string, - defaultValue: Type, - ) { - this.name0 = name; - this.description0 = description; - this.defaultValue0 = defaultValue; - } - - public get name(): string { - return this.name0; - } - - public get defaultValue(): Type { - return this.defaultValue0; - } - - public get description(): Type { - return this.description0; - } - - public get type(): string { - return typeof this.defaultValue; - } -} - -export const defs: Def[] = [ - new Def( - 'hintchars', - 'hint characters on follow mode', - 'abcdefghijklmnopqrstuvwxyz'), - new Def( - 'smoothscroll', - 'smooth scroll', - false), - new Def( - 'complete', - 'which are completed at the open page', - 'sbh'), -]; diff --git a/src/shared/property-defs.ts b/src/shared/property-defs.ts deleted file mode 100644 index fec9f80..0000000 --- a/src/shared/property-defs.ts +++ /dev/null @@ -1,56 +0,0 @@ -export type Type = string | number | boolean; - -export class Def { - private name0: string; - - private description0: string; - - private defaultValue0: Type; - - constructor( - name: string, - description: string, - defaultValue: Type, - ) { - this.name0 = name; - this.description0 = description; - this.defaultValue0 = defaultValue; - } - - public get name(): string { - return this.name0; - } - - public get defaultValue(): Type { - return this.defaultValue0; - } - - public get description(): Type { - return this.description0; - } - - public get type(): string { - return typeof this.defaultValue; - } -} - -export const defs: Def[] = [ - new Def( - 'hintchars', - 'hint characters on follow mode', - 'abcdefghijklmnopqrstuvwxyz'), - new Def( - 'smoothscroll', - 'smooth scroll', - false), - new Def( - 'complete', - 'which are completed at the open page', - 'sbh'), -]; - -export const defaultValues = { - hintchars: 'abcdefghijklmnopqrstuvwxyz', - smoothscroll: false, - complete: 'sbh', -}; diff --git a/src/shared/settings/Blacklist.ts b/src/shared/settings/Blacklist.ts new file mode 100644 index 0000000..a95b606 --- /dev/null +++ b/src/shared/settings/Blacklist.ts @@ -0,0 +1,39 @@ +export type BlacklistJSON = string[]; + +const fromWildcard = (pattern: string): RegExp => { + let regexStr = '^' + pattern.replace(/\*/g, '.*') + '$'; + return new RegExp(regexStr); +}; + +export default class Blacklist { + constructor( + private blacklist: string[], + ) { + } + + static fromJSON(json: any): Blacklist { + if (!Array.isArray(json)) { + throw new TypeError(`"blacklist" is not an array of string`); + } + for (let x of json) { + if (typeof x !== 'string') { + throw new TypeError(`"blacklist" is not an array of string`); + } + } + return new Blacklist(json); + } + + toJSON(): BlacklistJSON { + return this.blacklist; + } + + includes(url: string): boolean { + let u = new URL(url); + return this.blacklist.some((item) => { + if (!item.includes('/')) { + return fromWildcard(item).test(u.host); + } + return fromWildcard(item).test(u.host + u.pathname); + }); + } +} diff --git a/src/shared/settings/Key.ts b/src/shared/settings/Key.ts new file mode 100644 index 0000000..b11eeb2 --- /dev/null +++ b/src/shared/settings/Key.ts @@ -0,0 +1,61 @@ +export default class Key { + public readonly key: string; + + public readonly shift: boolean; + + public readonly ctrl: boolean; + + public readonly alt: boolean; + + public readonly meta: boolean; + + constructor({ key, shift, ctrl, alt, meta }: { + key: string; + shift: boolean; + ctrl: boolean; + alt: boolean; + meta: boolean; + }) { + this.key = key; + this.shift = shift; + this.ctrl = ctrl; + this.alt = alt; + this.meta = meta; + } + + static fromMapKey(str: string): Key { + if (str.startsWith('<') && str.endsWith('>')) { + let inner = str.slice(1, -1); + let shift = inner.includes('S-'); + let base = inner.slice(inner.lastIndexOf('-') + 1); + if (shift && base.length === 1) { + base = base.toUpperCase(); + } else if (!shift && base.length === 1) { + base = base.toLowerCase(); + } + return new Key({ + key: base, + shift: shift, + ctrl: inner.includes('C-'), + alt: inner.includes('A-'), + meta: inner.includes('M-'), + }); + } + + return new Key({ + key: str, + shift: str.toLowerCase() !== str, + ctrl: false, + alt: false, + meta: false, + }); + } + + equals(key: Key) { + return this.key === key.key && + this.ctrl === key.ctrl && + this.meta === key.meta && + this.alt === key.alt && + this.shift === key.shift; + } +} diff --git a/src/shared/settings/KeySequence.ts b/src/shared/settings/KeySequence.ts new file mode 100644 index 0000000..4955583 --- /dev/null +++ b/src/shared/settings/KeySequence.ts @@ -0,0 +1,54 @@ +import Key from '../../shared/settings/Key'; + +export default class KeySequence { + constructor( + public readonly keys: Key[], + ) { + } + + push(key: Key): number { + return this.keys.push(key); + } + + length(): number { + return this.keys.length; + } + + startsWith(o: KeySequence): boolean { + if (this.keys.length < o.keys.length) { + return false; + } + for (let i = 0; i < o.keys.length; ++i) { + if (!this.keys[i].equals(o.keys[i])) { + return false; + } + } + return true; + } + + static fromMapKeys(keys: string): KeySequence { + const fromMapKeysRecursive = ( + remaining: string, mappedKeys: Key[], + ): Key[] => { + if (remaining.length === 0) { + return mappedKeys; + } + + let nextPos = 1; + if (remaining.startsWith('<')) { + let ltPos = remaining.indexOf('>'); + if (ltPos > 0) { + nextPos = ltPos + 1; + } + } + + return fromMapKeysRecursive( + remaining.slice(nextPos), + mappedKeys.concat([Key.fromMapKey(remaining.slice(0, nextPos))]) + ); + }; + + let data = fromMapKeysRecursive(keys, []); + return new KeySequence(data); + } +} diff --git a/src/shared/settings/Keymaps.ts b/src/shared/settings/Keymaps.ts new file mode 100644 index 0000000..a5558b0 --- /dev/null +++ b/src/shared/settings/Keymaps.ts @@ -0,0 +1,37 @@ +import * as operations from '../operations'; + +export type KeymapsJSON = { [key: string]: operations.Operation }; + +export default class Keymaps { + constructor( + private readonly data: KeymapsJSON, + ) { + } + + static fromJSON(json: any): Keymaps { + if (typeof json !== 'object' || json === null) { + throw new TypeError('invalid keymaps type: ' + JSON.stringify(json)); + } + + let data: KeymapsJSON = {}; + for (let key of Object.keys(json)) { + data[key] = operations.valueOf(json[key]); + } + return new Keymaps(data); + } + + combine(other: Keymaps): Keymaps { + return new Keymaps({ + ...this.data, + ...other.data, + }); + } + + toJSON(): KeymapsJSON { + return this.data; + } + + entries(): [string, operations.Operation][] { + return Object.entries(this.data); + } +} diff --git a/src/shared/settings/Properties.ts b/src/shared/settings/Properties.ts new file mode 100644 index 0000000..63ff991 --- /dev/null +++ b/src/shared/settings/Properties.ts @@ -0,0 +1,110 @@ +export type PropertiesJSON = { + hintchars?: string; + smoothscroll?: boolean; + complete?: string; +}; + +export type PropertyTypes = { + hintchars: string; + smoothscroll: string; + complete: string; +}; + +type PropertyName = 'hintchars' | 'smoothscroll' | 'complete'; + +type PropertyDef = { + name: PropertyName; + description: string; + defaultValue: string | number | boolean; + type: 'string' | 'number' | 'boolean'; +}; + +const defs: PropertyDef[] = [ + { + name: 'hintchars', + description: 'hint characters on follow mode', + defaultValue: 'abcdefghijklmnopqrstuvwxyz', + type: 'string', + }, { + name: 'smoothscroll', + description: 'smooth scroll', + defaultValue: false, + type: 'boolean', + }, { + name: 'complete', + description: 'which are completed at the open page', + defaultValue: 'sbh', + type: 'string', + } +]; + +const defaultValues = { + hintchars: 'abcdefghijklmnopqrstuvwxyz', + smoothscroll: false, + complete: 'sbh', +}; + +export default class Properties { + public hintchars: string; + + public smoothscroll: boolean; + + public complete: string; + + constructor({ + hintchars, + smoothscroll, + complete, + }: { + hintchars?: string; + smoothscroll?: boolean; + complete?: string; + } = {}) { + this.hintchars = hintchars || defaultValues.hintchars; + this.smoothscroll = smoothscroll || defaultValues.smoothscroll; + this.complete = complete || defaultValues.complete; + } + + static fromJSON(json: any): Properties { + let defNames: Set<string> = new Set(defs.map(def => def.name)); + let unknownName = Object.keys(json).find(name => !defNames.has(name)); + if (unknownName) { + throw new TypeError(`Unknown property name: "${unknownName}"`); + } + + for (let def of defs) { + if (!Object.prototype.hasOwnProperty.call(json, def.name)) { + continue; + } + if (typeof json[def.name] !== def.type) { + throw new TypeError( + `property "${def.name}" is not ${def.type}`); + } + } + return new Properties(json); + } + + static types(): PropertyTypes { + return { + hintchars: 'string', + smoothscroll: 'boolean', + complete: 'string', + }; + } + + static def(name: string): PropertyDef | undefined { + return defs.find(p => p.name === name); + } + + static defs(): PropertyDef[] { + return defs; + } + + toJSON(): PropertiesJSON { + return { + hintchars: this.hintchars, + smoothscroll: this.smoothscroll, + complete: this.complete, + }; + } +} diff --git a/src/shared/settings/Search.ts b/src/shared/settings/Search.ts new file mode 100644 index 0000000..4580236 --- /dev/null +++ b/src/shared/settings/Search.ts @@ -0,0 +1,76 @@ +type Entries = { [name: string]: string }; + +export type SearchJSON = { + default: string; + engines: { [key: string]: string }; +}; + +export default class Search { + constructor( + public defaultEngine: string, + public engines: Entries, + ) { + } + + static fromJSON(json: any): Search { + let defaultEngine = Search.getStringField(json, 'default'); + let engines = Search.getObjectField(json, 'engines'); + + for (let [name, url] of Object.entries(engines)) { + if ((/\s/).test(name)) { + throw new TypeError( + `While space in the search engine not allowed: "${name}"`); + } + if (typeof url !== 'string') { + throw new TypeError( + `Invalid type of value in filed "engines": ${JSON.stringify(json)}`); + } + let matches = url.match(/{}/g); + if (matches === null) { + throw new TypeError(`No {}-placeholders in URL of "${name}"`); + } else if (matches.length > 1) { + throw new TypeError(`Multiple {}-placeholders in URL of "${name}"`); + } + } + + if (!Object.keys(engines).includes(defaultEngine)) { + throw new TypeError(`Default engine "${defaultEngine}" not found`); + } + + return new Search( + json.default as string, + json.engines, + ); + } + + toJSON(): SearchJSON { + return { + default: this.defaultEngine, + engines: this.engines, + }; + } + + private static getStringField(json: any, name: string): string { + if (!Object.prototype.hasOwnProperty.call(json, name)) { + throw new TypeError( + `missing field "${name}" on search: ${JSON.stringify(json)}`); + } + if (typeof json[name] !== 'string') { + throw new TypeError( + `invalid type of filed "${name}" on search: ${JSON.stringify(json)}`); + } + return json[name]; + } + + private static getObjectField(json: any, name: string): Object { + if (!Object.prototype.hasOwnProperty.call(json, name)) { + throw new TypeError( + `missing field "${name}" on search: ${JSON.stringify(json)}`); + } + if (typeof json[name] !== 'object' || json[name] === null) { + throw new TypeError( + `invalid type of filed "${name}" on search: ${JSON.stringify(json)}`); + } + return json[name]; + } +} diff --git a/src/shared/settings/Settings.ts b/src/shared/settings/Settings.ts new file mode 100644 index 0000000..2c9e37f --- /dev/null +++ b/src/shared/settings/Settings.ts @@ -0,0 +1,158 @@ +import Keymaps, { KeymapsJSON } from './Keymaps'; +import Search, { SearchJSON } from './Search'; +import Properties, { PropertiesJSON } from './Properties'; +import Blacklist, { BlacklistJSON } from './Blacklist'; + +export type SettingsJSON = { + keymaps: KeymapsJSON, + search: SearchJSON, + properties: PropertiesJSON, + blacklist: BlacklistJSON, +}; + +export default class Settings { + public keymaps: Keymaps; + + public search: Search; + + public properties: Properties; + + public blacklist: Blacklist; + + constructor({ + keymaps, + search, + properties, + blacklist, + }: { + keymaps: Keymaps; + search: Search; + properties: Properties; + blacklist: Blacklist; + }) { + this.keymaps = keymaps; + this.search = search; + this.properties = properties; + this.blacklist = blacklist; + } + + static fromJSON(json: any): Settings { + let settings = { ...DefaultSetting }; + for (let key of Object.keys(json)) { + switch (key) { + case 'keymaps': + settings.keymaps = Keymaps.fromJSON(json.keymaps); + break; + case 'search': + settings.search = Search.fromJSON(json.search); + break; + case 'properties': + settings.properties = Properties.fromJSON(json.properties); + break; + case 'blacklist': + settings.blacklist = Blacklist.fromJSON(json.blacklist); + break; + default: + throw new TypeError('unknown setting: ' + key); + } + } + return new Settings(settings); + } + + toJSON(): SettingsJSON { + return { + keymaps: this.keymaps.toJSON(), + search: this.search.toJSON(), + properties: this.properties.toJSON(), + blacklist: this.blacklist.toJSON(), + }; + } +} + +export const DefaultSettingJSONText = `{ + "keymaps": { + "0": { "type": "scroll.home" }, + ":": { "type": "command.show" }, + "o": { "type": "command.show.open", "alter": false }, + "O": { "type": "command.show.open", "alter": true }, + "t": { "type": "command.show.tabopen", "alter": false }, + "T": { "type": "command.show.tabopen", "alter": true }, + "w": { "type": "command.show.winopen", "alter": false }, + "W": { "type": "command.show.winopen", "alter": true }, + "b": { "type": "command.show.buffer" }, + "a": { "type": "command.show.addbookmark", "alter": true }, + "k": { "type": "scroll.vertically", "count": -1 }, + "j": { "type": "scroll.vertically", "count": 1 }, + "h": { "type": "scroll.horizonally", "count": -1 }, + "l": { "type": "scroll.horizonally", "count": 1 }, + "<C-U>": { "type": "scroll.pages", "count": -0.5 }, + "<C-D>": { "type": "scroll.pages", "count": 0.5 }, + "<C-B>": { "type": "scroll.pages", "count": -1 }, + "<C-F>": { "type": "scroll.pages", "count": 1 }, + "gg": { "type": "scroll.top" }, + "G": { "type": "scroll.bottom" }, + "$": { "type": "scroll.end" }, + "d": { "type": "tabs.close" }, + "D": { "type": "tabs.close", "select": "left" }, + "x$": { "type": "tabs.close.right" }, + "!d": { "type": "tabs.close.force" }, + "u": { "type": "tabs.reopen" }, + "K": { "type": "tabs.prev" }, + "J": { "type": "tabs.next" }, + "gT": { "type": "tabs.prev" }, + "gt": { "type": "tabs.next" }, + "g0": { "type": "tabs.first" }, + "g$": { "type": "tabs.last" }, + "<C-6>": { "type": "tabs.prevsel" }, + "r": { "type": "tabs.reload", "cache": false }, + "R": { "type": "tabs.reload", "cache": true }, + "zp": { "type": "tabs.pin.toggle" }, + "zd": { "type": "tabs.duplicate" }, + "zi": { "type": "zoom.in" }, + "zo": { "type": "zoom.out" }, + "zz": { "type": "zoom.neutral" }, + "f": { "type": "follow.start", "newTab": false }, + "F": { "type": "follow.start", "newTab": true, "background": false }, + "m": { "type": "mark.set.prefix" }, + "'": { "type": "mark.jump.prefix" }, + "H": { "type": "navigate.history.prev" }, + "L": { "type": "navigate.history.next" }, + "[[": { "type": "navigate.link.prev" }, + "]]": { "type": "navigate.link.next" }, + "gu": { "type": "navigate.parent" }, + "gU": { "type": "navigate.root" }, + "gi": { "type": "focus.input" }, + "gf": { "type": "page.source" }, + "gh": { "type": "page.home" }, + "gH": { "type": "page.home", "newTab": true }, + "y": { "type": "urls.yank" }, + "p": { "type": "urls.paste", "newTab": false }, + "P": { "type": "urls.paste", "newTab": true }, + "/": { "type": "find.start" }, + "n": { "type": "find.next" }, + "N": { "type": "find.prev" }, + ".": { "type": "repeat.last" }, + "<S-Esc>": { "type": "addon.toggle.enabled" } + }, + "search": { + "default": "google", + "engines": { + "google": "https://google.com/search?q={}", + "yahoo": "https://search.yahoo.com/search?p={}", + "bing": "https://www.bing.com/search?q={}", + "duckduckgo": "https://duckduckgo.com/?q={}", + "twitter": "https://twitter.com/search?q={}", + "wikipedia": "https://en.wikipedia.org/w/index.php?search={}" + } + }, + "properties": { + "hintchars": "abcdefghijklmnopqrstuvwxyz", + "smoothscroll": false, + "complete": "sbh" + }, + "blacklist": [ + ] +}`; + +export const DefaultSetting: Settings = + Settings.fromJSON(JSON.parse(DefaultSettingJSONText)); diff --git a/src/shared/urls.ts b/src/shared/urls.ts index bbdb1ea..64ea4f2 100644 --- a/src/shared/urls.ts +++ b/src/shared/urls.ts @@ -1,4 +1,4 @@ -import { Search } from './Settings'; +import Search from './settings/Search'; const trimStart = (str: string): string => { // NOTE String.trimStart is available on Firefox 61 @@ -19,7 +19,7 @@ const searchUrl = (keywords: string, search: Search): string => { if (keywords.includes('.') && !keywords.includes(' ')) { return 'http://' + keywords; } - let template = search.engines[search.default]; + let template = search.engines[search.defaultEngine]; let query = keywords; let first = trimStart(keywords).split(' ')[0]; diff --git a/src/shared/utils/re.ts b/src/shared/utils/re.ts deleted file mode 100644 index 34f4fa6..0000000 --- a/src/shared/utils/re.ts +++ /dev/null @@ -1,6 +0,0 @@ -const fromWildcard = (pattern: string): RegExp => { - let regexStr = '^' + pattern.replace(/\*/g, '.*') + '$'; - return new RegExp(regexStr); -}; - -export { fromWildcard }; |