aboutsummaryrefslogtreecommitdiff
path: root/src/shared/settings
diff options
context:
space:
mode:
Diffstat (limited to 'src/shared/settings')
-rw-r--r--src/shared/settings/Blacklist.ts39
-rw-r--r--src/shared/settings/Key.ts61
-rw-r--r--src/shared/settings/KeySequence.ts54
-rw-r--r--src/shared/settings/Keymaps.ts37
-rw-r--r--src/shared/settings/Properties.ts110
-rw-r--r--src/shared/settings/Search.ts76
-rw-r--r--src/shared/settings/Settings.ts158
7 files changed, 535 insertions, 0 deletions
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));