diff options
author | Shin'ya Ueoka <ueokande@i-beam.org> | 2019-10-07 12:54:32 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-10-07 12:54:32 +0000 |
commit | 8eddcc1785a85bbe74be254d1055ebe5125dad10 (patch) | |
tree | f3f51320d12a90a1b421ed8b1f811c576996ea8e /src | |
parent | 7fc2bb615f530fc6adfade54b9553568f5d50ceb (diff) | |
parent | b77a4734985722e96066e713f3b1b9e81a6e1811 (diff) |
Merge pull request #654 from ueokande/settings-as-a-class
Refactor settings on shared logics
Diffstat (limited to 'src')
44 files changed, 741 insertions, 718 deletions
diff --git a/src/background/controllers/SettingController.ts b/src/background/controllers/SettingController.ts index 34951ff..8d05852 100644 --- a/src/background/controllers/SettingController.ts +++ b/src/background/controllers/SettingController.ts @@ -1,7 +1,7 @@ import { injectable } from 'tsyringe'; import SettingUseCase from '../usecases/SettingUseCase'; import ContentMessageClient from '../infrastructures/ContentMessageClient'; -import Settings from '../../shared/Settings'; +import Settings from '../../shared/settings/Settings'; @injectable() export default class SettingController { diff --git a/src/background/infrastructures/ContentMessageListener.ts b/src/background/infrastructures/ContentMessageListener.ts index 3d24741..f80d686 100644 --- a/src/background/infrastructures/ContentMessageListener.ts +++ b/src/background/infrastructures/ContentMessageListener.ts @@ -101,8 +101,8 @@ export default class ContentMessageListener { return this.commandController.exec(text); } - onSettingsQuery(): Promise<any> { - return this.settingController.getSetting(); + async onSettingsQuery(): Promise<any> { + return (await this.settingController.getSetting()).toJSON(); } onFindGetKeyword(): Promise<string> { diff --git a/src/background/repositories/PersistentSettingRepository.ts b/src/background/repositories/PersistentSettingRepository.ts index 927bce9..e3b78b3 100644 --- a/src/background/repositories/PersistentSettingRepository.ts +++ b/src/background/repositories/PersistentSettingRepository.ts @@ -8,7 +8,7 @@ export default class SettingRepository { if (!settings) { return null; } - return SettingData.valueOf(settings as any); + return SettingData.fromJSON(settings as any); } } diff --git a/src/background/repositories/SettingRepository.ts b/src/background/repositories/SettingRepository.ts index 2f159e5..e775a32 100644 --- a/src/background/repositories/SettingRepository.ts +++ b/src/background/repositories/SettingRepository.ts @@ -1,7 +1,7 @@ import { injectable } from 'tsyringe'; import MemoryStorage from '../infrastructures/MemoryStorage'; -import Settings from '../../shared/Settings'; -import * as PropertyDefs from '../../shared/property-defs'; +import Settings from '../../shared/settings/Settings'; +import Properties from '../../shared/settings/Properties'; const CACHED_SETTING_KEY = 'setting'; @@ -14,17 +14,18 @@ export default class SettingRepository { } get(): Promise<Settings> { - return Promise.resolve(this.cache.get(CACHED_SETTING_KEY)); + let data = this.cache.get(CACHED_SETTING_KEY); + return Promise.resolve(Settings.fromJSON(data)); } update(value: Settings): void { - return this.cache.set(CACHED_SETTING_KEY, value); + return this.cache.set(CACHED_SETTING_KEY, value.toJSON()); } async setProperty( name: string, value: string | number | boolean, ): Promise<void> { - let def = PropertyDefs.defs.find(d => name === d.name); + let def = Properties.def(name); if (!def) { throw new Error('unknown property: ' + name); } diff --git a/src/background/usecases/CompletionsUseCase.ts b/src/background/usecases/CompletionsUseCase.ts index 8cd4f32..bfff1e6 100644 --- a/src/background/usecases/CompletionsUseCase.ts +++ b/src/background/usecases/CompletionsUseCase.ts @@ -5,7 +5,7 @@ import CompletionsRepository from '../repositories/CompletionsRepository'; import * as filters from './filters'; import SettingRepository from '../repositories/SettingRepository'; import TabPresenter from '../presenters/TabPresenter'; -import * as PropertyDefs from '../../shared/property-defs'; +import Properties from '../../shared/settings/Properties'; const COMPLETION_ITEM_LIMIT = 10; @@ -129,7 +129,7 @@ export default class CompletionsUseCase { } querySet(name: string, keywords: string): Promise<CompletionGroup[]> { - let items = PropertyDefs.defs.map((def) => { + let items = Properties.defs().map((def) => { if (def.type === 'boolean') { return [ { diff --git a/src/background/usecases/SettingUseCase.ts b/src/background/usecases/SettingUseCase.ts index d73521f..d78d440 100644 --- a/src/background/usecases/SettingUseCase.ts +++ b/src/background/usecases/SettingUseCase.ts @@ -3,7 +3,7 @@ import PersistentSettingRepository from '../repositories/PersistentSettingRepository'; import SettingRepository from '../repositories/SettingRepository'; import { DefaultSettingData } from '../../shared/SettingData'; -import Settings from '../../shared/Settings'; +import Settings from '../../shared/settings/Settings'; import NotifyPresenter from '../presenters/NotifyPresenter'; @injectable() diff --git a/src/background/usecases/parsers.ts b/src/background/usecases/parsers.ts index 6135fd8..e8a1149 100644 --- a/src/background/usecases/parsers.ts +++ b/src/background/usecases/parsers.ts @@ -1,4 +1,4 @@ -import * as PropertyDefs from '../../shared//property-defs'; +import Properties from '../../shared/settings/Properties'; const mustNumber = (v: any): number => { let num = Number(v); @@ -16,7 +16,7 @@ const parseSetOption = ( value = !key.startsWith('no'); key = value ? key : key.slice(2); } - let def = PropertyDefs.defs.find(d => d.name === key); + let def = Properties.def(key); if (!def) { throw new Error('Unknown property: ' + key); } diff --git a/src/content/InputDriver.ts b/src/content/InputDriver.ts index 0472088..bc184d2 100644 --- a/src/content/InputDriver.ts +++ b/src/content/InputDriver.ts @@ -1,5 +1,5 @@ import * as dom from '../shared/utils/dom'; -import Key, * as keys from './domains/Key'; +import Key from '../shared/settings/Key'; const cancelKey = (e: KeyboardEvent): boolean => { if (e.key === 'Escape') { @@ -11,6 +11,38 @@ const cancelKey = (e: KeyboardEvent): boolean => { return false; }; +const modifiedKeyName = (name: string): string => { + if (name === ' ') { + return 'Space'; + } + if (name.length === 1) { + return name; + } else if (name === 'Escape') { + return 'Esc'; + } + return name; +}; + +// visible for testing +export const keyFromKeyboardEvent = (e: KeyboardEvent): Key => { + let key = modifiedKeyName(e.key); + let shift = e.shiftKey; + if (key.length === 1 && key.toUpperCase() === key.toLowerCase()) { + // make shift false for symbols to enable key bindings by symbold keys. + // But this limits key bindings by symbol keys with Shift + // (such as Shift+$>. + shift = false; + } + + return new Key({ + key: modifiedKeyName(e.key), + shift: shift, + ctrl: e.ctrlKey, + alt: e.altKey, + meta: e.metaKey, + }); +}; + export default class InputDriver { private pressed: {[key: string]: string} = {}; @@ -66,7 +98,7 @@ export default class InputDriver { return; } - let key = keys.fromKeyboardEvent(e); + let key = keyFromKeyboardEvent(e); for (let listener of this.onKeyListeners) { let stop = listener(key); if (stop) { diff --git a/src/content/client/FollowMasterClient.ts b/src/content/client/FollowMasterClient.ts index da75308..6681e8a 100644 --- a/src/content/client/FollowMasterClient.ts +++ b/src/content/client/FollowMasterClient.ts @@ -1,5 +1,5 @@ import * as messages from '../../shared/messages'; -import Key from '../domains/Key'; +import Key from '../../shared/settings/Key'; export default interface FollowMasterClient { startFollow(newTab: boolean, background: boolean): void; @@ -35,7 +35,7 @@ export class FollowMasterClientImpl implements FollowMasterClient { this.postMessage({ type: messages.FOLLOW_KEY_PRESS, key: key.key, - ctrlKey: key.ctrlKey || false, + ctrlKey: key.ctrl || false, }); } diff --git a/src/content/client/SettingClient.ts b/src/content/client/SettingClient.ts index 0850f11..fc62720 100644 --- a/src/content/client/SettingClient.ts +++ b/src/content/client/SettingClient.ts @@ -1,4 +1,4 @@ -import Settings from '../../shared/Settings'; +import Settings from '../../shared/settings/Settings'; import * as messages from '../../shared/messages'; export default interface SettingClient { @@ -10,6 +10,6 @@ export class SettingClientImpl { let settings = await browser.runtime.sendMessage({ type: messages.SETTINGS_QUERY, }); - return settings as Settings; + return Settings.fromJSON(settings); } } diff --git a/src/content/controllers/FollowKeyController.ts b/src/content/controllers/FollowKeyController.ts index 59d2271..0fd94ff 100644 --- a/src/content/controllers/FollowKeyController.ts +++ b/src/content/controllers/FollowKeyController.ts @@ -1,6 +1,6 @@ import { injectable } from 'tsyringe'; import FollowSlaveUseCase from '../usecases/FollowSlaveUseCase'; -import Key from '../domains/Key'; +import Key from '../../shared/settings/Key'; @injectable() export default class FollowKeyController { diff --git a/src/content/controllers/KeymapController.ts b/src/content/controllers/KeymapController.ts index fcfaff1..6157a71 100644 --- a/src/content/controllers/KeymapController.ts +++ b/src/content/controllers/KeymapController.ts @@ -9,7 +9,7 @@ import ClipboardUseCase from '../usecases/ClipboardUseCase'; import OperationClient from '../client/OperationClient'; import MarkKeyyUseCase from '../usecases/MarkKeyUseCase'; import FollowMasterClient from '../client/FollowMasterClient'; -import Key from '../domains/Key'; +import Key from '../../shared/settings/Key'; @injectable() export default class KeymapController { diff --git a/src/content/controllers/MarkKeyController.ts b/src/content/controllers/MarkKeyController.ts index 886e5ff..e7653ee 100644 --- a/src/content/controllers/MarkKeyController.ts +++ b/src/content/controllers/MarkKeyController.ts @@ -1,7 +1,7 @@ import { injectable } from 'tsyringe'; import MarkUseCase from '../usecases/MarkUseCase'; import MarkKeyyUseCase from '../usecases/MarkKeyUseCase'; -import Key from '../domains/Key'; +import Key from '../../shared/settings/Key'; @injectable() export default class MarkKeyController { diff --git a/src/content/controllers/SettingController.ts b/src/content/controllers/SettingController.ts index 7fb045b..06273a0 100644 --- a/src/content/controllers/SettingController.ts +++ b/src/content/controllers/SettingController.ts @@ -1,8 +1,6 @@ import { injectable } from 'tsyringe'; import AddonEnabledUseCase from '../usecases/AddonEnabledUseCase'; import SettingUseCase from '../usecases/SettingUseCase'; -import * as blacklists from '../../shared/blacklists'; - import * as messages from '../../shared/messages'; @injectable() @@ -17,9 +15,7 @@ export default class SettingController { async initSettings(): Promise<void> { try { let current = await this.settingUseCase.reload(); - let disabled = blacklists.includes( - current.blacklist, window.location.href, - ); + let disabled = current.blacklist.includes(window.location.href); if (disabled) { this.addonEnabledUseCase.disable(); } else { diff --git a/src/content/domains/Key.ts b/src/content/domains/Key.ts deleted file mode 100644 index b25616e..0000000 --- a/src/content/domains/Key.ts +++ /dev/null @@ -1,72 +0,0 @@ -export default interface Key { - key: string; - shiftKey?: boolean; - ctrlKey?: boolean; - altKey?: boolean; - metaKey?: boolean; -} - -const modifiedKeyName = (name: string): string => { - if (name === ' ') { - return 'Space'; - } - if (name.length === 1) { - return name; - } else if (name === 'Escape') { - return 'Esc'; - } - return name; -}; - -export const fromKeyboardEvent = (e: KeyboardEvent): Key => { - let key = modifiedKeyName(e.key); - let shift = e.shiftKey; - if (key.length === 1 && key.toUpperCase() === key.toLowerCase()) { - // make shift false for symbols to enable key bindings by symbold keys. - // But this limits key bindings by symbol keys with Shift (such as Shift+$>. - shift = false; - } - - return { - key: modifiedKeyName(e.key), - shiftKey: shift, - ctrlKey: e.ctrlKey, - altKey: e.altKey, - metaKey: e.metaKey, - }; -}; - -export const fromMapKey = (key: string): Key => { - if (key.startsWith('<') && key.endsWith('>')) { - let inner = key.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 { - key: base, - shiftKey: inner.includes('S-'), - ctrlKey: inner.includes('C-'), - altKey: inner.includes('A-'), - metaKey: inner.includes('M-'), - }; - } - return { - key: key, - shiftKey: key.toLowerCase() !== key, - ctrlKey: false, - altKey: false, - metaKey: false, - }; -}; - -export const equals = (e1: Key, e2: Key): boolean => { - return e1.key === e2.key && - e1.ctrlKey === e2.ctrlKey && - e1.metaKey === e2.metaKey && - e1.altKey === e2.altKey && - e1.shiftKey === e2.shiftKey; -}; diff --git a/src/content/domains/KeySequence.ts b/src/content/domains/KeySequence.ts deleted file mode 100644 index 6a05c2f..0000000 --- a/src/content/domains/KeySequence.ts +++ /dev/null @@ -1,64 +0,0 @@ -import Key, * as keyUtils from './Key'; - -export default class KeySequence { - private keys: Key[]; - - private constructor(keys: Key[]) { - this.keys = keys; - } - - static from(keys: Key[]): KeySequence { - return new KeySequence(keys); - } - - 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 (!keyUtils.equals(this.keys[i], o.keys[i])) { - return false; - } - } - return true; - } - - getKeyArray(): Key[] { - return this.keys; - } -} - -export const fromMapKeys = (keys: string): KeySequence => { - const fromMapKeysRecursive = ( - remainings: string, mappedKeys: Key[], - ): Key[] => { - if (remainings.length === 0) { - return mappedKeys; - } - - let nextPos = 1; - if (remainings.startsWith('<')) { - let ltPos = remainings.indexOf('>'); - if (ltPos > 0) { - nextPos = ltPos + 1; - } - } - - return fromMapKeysRecursive( - remainings.slice(nextPos), - mappedKeys.concat([keyUtils.fromMapKey(remainings.slice(0, nextPos))]) - ); - }; - - let data = fromMapKeysRecursive(keys, []); - return KeySequence.from(data); -}; - diff --git a/src/content/repositories/KeymapRepository.ts b/src/content/repositories/KeymapRepository.ts index d7b5b7d..3391229 100644 --- a/src/content/repositories/KeymapRepository.ts +++ b/src/content/repositories/KeymapRepository.ts @@ -1,5 +1,5 @@ -import Key from '../domains/Key'; -import KeySequence from '../domains/KeySequence'; +import Key from '../../shared/settings/Key'; +import KeySequence from '../../shared/settings/KeySequence'; export default interface KeymapRepository { enqueueKey(key: Key): KeySequence; @@ -7,7 +7,7 @@ export default interface KeymapRepository { clear(): void; } -let current: KeySequence = KeySequence.from([]); +let current: KeySequence = new KeySequence([]); export class KeymapRepositoryImpl { @@ -17,6 +17,6 @@ export class KeymapRepositoryImpl { } clear(): void { - current = KeySequence.from([]); + current = new KeySequence([]); } } diff --git a/src/content/repositories/SettingRepository.ts b/src/content/repositories/SettingRepository.ts index d718794..4ba26e0 100644 --- a/src/content/repositories/SettingRepository.ts +++ b/src/content/repositories/SettingRepository.ts @@ -1,4 +1,4 @@ -import Settings, { DefaultSetting } from '../../shared/Settings'; +import Settings, { DefaultSetting } from '../../shared/settings/Settings'; let current: Settings = DefaultSetting; diff --git a/src/content/usecases/FollowSlaveUseCase.ts b/src/content/usecases/FollowSlaveUseCase.ts index 2bd16ee..d471adb 100644 --- a/src/content/usecases/FollowSlaveUseCase.ts +++ b/src/content/usecases/FollowSlaveUseCase.ts @@ -4,7 +4,7 @@ import FollowPresenter from '../presenters/FollowPresenter'; import TabsClient from '../client/TabsClient'; import FollowMasterClient from '../client/FollowMasterClient'; import { LinkHint, InputHint } from '../presenters/Hint'; -import Key from '../domains/Key'; +import Key from '../../shared/settings/Key'; interface Size { width: number; diff --git a/src/content/usecases/KeymapUseCase.ts b/src/content/usecases/KeymapUseCase.ts index d0d039a..495f6d0 100644 --- a/src/content/usecases/KeymapUseCase.ts +++ b/src/content/usecases/KeymapUseCase.ts @@ -3,16 +3,16 @@ import KeymapRepository from '../repositories/KeymapRepository'; import SettingRepository from '../repositories/SettingRepository'; import AddonEnabledRepository from '../repositories/AddonEnabledRepository'; import * as operations from '../../shared/operations'; -import { Keymaps } from '../../shared/Settings'; -import Key from '../domains/Key'; -import KeySequence, * as keySequenceUtils from '../domains/KeySequence'; +import Keymaps from '../../shared/settings/Keymaps'; +import Key from '../../shared/settings/Key'; +import KeySequence from '../../shared/settings/KeySequence'; type KeymapEntityMap = Map<KeySequence, operations.Operation>; -const reservedKeymaps: Keymaps = { +const reservedKeymaps = Keymaps.fromJSON({ '<Esc>': { type: operations.CANCEL }, '<C-[>': { type: operations.CANCEL }, -}; +}); @injectable() export default class KeymapUseCase { @@ -65,16 +65,10 @@ export default class KeymapUseCase { } private keymapEntityMap(): KeymapEntityMap { - let keymaps = { - ...this.settingRepository.get().keymaps, - ...reservedKeymaps, - }; - let entries = Object.entries(keymaps).map((entry) => { - return [ - keySequenceUtils.fromMapKeys(entry[0]), - entry[1], - ]; - }) as [KeySequence, operations.Operation][]; + let keymaps = this.settingRepository.get().keymaps.combine(reservedKeymaps); + let entries = keymaps.entries().map( + ([keys, op]) => [KeySequence.fromMapKeys(keys), op] + ) as [KeySequence, operations.Operation][]; return new Map<KeySequence, operations.Operation>(entries); } } diff --git a/src/content/usecases/SettingUseCase.ts b/src/content/usecases/SettingUseCase.ts index d5f66c6..4608039 100644 --- a/src/content/usecases/SettingUseCase.ts +++ b/src/content/usecases/SettingUseCase.ts @@ -1,7 +1,7 @@ import { injectable, inject } from 'tsyringe'; import SettingRepository from '../repositories/SettingRepository'; import SettingClient from '../client/SettingClient'; -import Settings from '../../shared/Settings'; +import Settings from '../../shared/settings/Settings'; @injectable() export default class SettingUseCase { diff --git a/src/settings/actions/index.ts b/src/settings/actions/index.ts index b1e996e..dfa41c4 100644 --- a/src/settings/actions/index.ts +++ b/src/settings/actions/index.ts @@ -1,5 +1,5 @@ import { - JSONSettings, FormSettings, SettingSource, + JSONTextSettings, FormSettings, SettingSource, } from '../../shared/SettingData'; // Settings @@ -11,14 +11,14 @@ export const SETTING_SWITCH_TO_JSON = 'setting.switch.to.json'; interface SettingSetSettingsAcion { type: typeof SETTING_SET_SETTINGS; source: SettingSource; - json?: JSONSettings; + json?: JSONTextSettings; form?: FormSettings; } interface SettingShowErrorAction { type: typeof SETTING_SHOW_ERROR; error: string; - json: JSONSettings; + json: JSONTextSettings; } interface SettingSwitchToFormAction { @@ -28,7 +28,7 @@ interface SettingSwitchToFormAction { interface SettingSwitchToJsonAction { type: typeof SETTING_SWITCH_TO_JSON; - json: JSONSettings, + json: JSONTextSettings, } export type SettingAction = diff --git a/src/settings/actions/setting.ts b/src/settings/actions/setting.ts index 9eb416e..9404791 100644 --- a/src/settings/actions/setting.ts +++ b/src/settings/actions/setting.ts @@ -1,7 +1,7 @@ import * as actions from './index'; import * as storages from '../storage'; import SettingData, { - JSONSettings, FormSettings, SettingSource, + JSONTextSettings, FormSettings, SettingSource, } from '../../shared/SettingData'; const load = async(): Promise<actions.SettingAction> => { @@ -26,7 +26,7 @@ const save = async(data: SettingData): Promise<actions.SettingAction> => { return set(data); }; -const switchToForm = (json: JSONSettings): actions.SettingAction => { +const switchToForm = (json: JSONTextSettings): actions.SettingAction => { try { // toSettings exercise validation let form = FormSettings.fromSettings(json.toSettings()); @@ -44,7 +44,7 @@ const switchToForm = (json: JSONSettings): actions.SettingAction => { }; const switchToJson = (form: FormSettings): actions.SettingAction => { - let json = JSONSettings.fromSettings(form.toSettings()); + let json = JSONTextSettings.fromSettings(form.toSettings()); return { type: actions.SETTING_SWITCH_TO_JSON, json, diff --git a/src/settings/components/form/BlacklistForm.tsx b/src/settings/components/form/BlacklistForm.tsx index 637bc1e..f352e41 100644 --- a/src/settings/components/form/BlacklistForm.tsx +++ b/src/settings/components/form/BlacklistForm.tsx @@ -2,10 +2,11 @@ import './BlacklistForm.scss'; import AddButton from '../ui/AddButton'; import DeleteButton from '../ui/DeleteButton'; import React from 'react'; +import { BlacklistJSON } from '../../../shared/settings/Blacklist'; interface Props { - value: string[]; - onChange: (value: string[]) => void; + value: BlacklistJSON; + onChange: (value: BlacklistJSON) => void; onBlur: () => void; } @@ -19,19 +20,24 @@ class BlacklistForm extends React.Component<Props> { render() { return <div className='form-blacklist-form'> { - this.props.value.map((url, index) => { - return <div key={index} className='form-blacklist-form-row'> - <input data-index={index} type='text' name='url' - className='column-url' value={url} - onChange={this.bindValue.bind(this)} - onBlur={this.props.onBlur} - /> - <DeleteButton data-index={index} name='delete' - onClick={this.bindValue.bind(this)} - onBlur={this.props.onBlur} - /> - </div>; - }) + this.props.value + .map((item, index) => { + if (typeof item !== 'string') { + // TODO support partial blacklist; + return null; + } + return <div key={index} className='form-blacklist-form-row'> + <input data-index={index} type='text' name='url' + className='column-url' value={item} + onChange={this.bindValue.bind(this)} + onBlur={this.props.onBlur} + /> + <DeleteButton data-index={index} name='delete' + onClick={this.bindValue.bind(this)} + onBlur={this.props.onBlur} + /> + </div>; + }) } <AddButton name='add' style={{ float: 'right' }} onClick={this.bindValue.bind(this)} /> @@ -41,7 +47,7 @@ class BlacklistForm extends React.Component<Props> { bindValue(e: any) { let name = e.target.name; let index = e.target.getAttribute('data-index'); - let next = this.props.value ? this.props.value.slice() : []; + let next = this.props.value.slice(); if (name === 'url') { next[index] = e.target.value; diff --git a/src/settings/components/form/KeymapsForm.tsx b/src/settings/components/form/KeymapsForm.tsx index 3cba0c0..94934ae 100644 --- a/src/settings/components/form/KeymapsForm.tsx +++ b/src/settings/components/form/KeymapsForm.tsx @@ -12,7 +12,7 @@ interface Props { class KeymapsForm extends React.Component<Props> { public static defaultProps: Props = { - value: FormKeymaps.valueOf({}), + value: FormKeymaps.fromJSON({}), onChange: () => {}, onBlur: () => {}, }; diff --git a/src/settings/components/form/PropertiesForm.tsx b/src/settings/components/form/PropertiesForm.tsx index ee98b7e..db8c8e5 100644 --- a/src/settings/components/form/PropertiesForm.tsx +++ b/src/settings/components/form/PropertiesForm.tsx @@ -18,7 +18,7 @@ class PropertiesForm extends React.Component<Props> { render() { let types = this.props.types; - let value = this.props.value; + let values = this.props.value; return <div className='form-properties-form'> { @@ -46,10 +46,10 @@ class PropertiesForm extends React.Component<Props> { <span className='column-name'>{name}</span> <input type={inputType} name={name} className='column-input' - value={value[name] ? value[name] : ''} + value={values[name] ? values[name] : ''} onChange={onChange} onBlur={this.props.onBlur} - checked={value[name]} + checked={values[name]} /> </label> </div>; diff --git a/src/settings/components/form/SearchForm.tsx b/src/settings/components/form/SearchForm.tsx index 6ba6cfb..0aaf6fd 100644 --- a/src/settings/components/form/SearchForm.tsx +++ b/src/settings/components/form/SearchForm.tsx @@ -12,7 +12,7 @@ interface Props { class SearchForm extends React.Component<Props> { public static defaultProps: Props = { - value: FormSearch.valueOf({ default: '', engines: []}), + value: FormSearch.fromJSON({ default: '', engines: []}), onChange: () => {}, onBlur: () => {}, }; @@ -81,7 +81,7 @@ class SearchForm extends React.Component<Props> { } } - this.props.onChange(FormSearch.valueOf(next)); + this.props.onChange(FormSearch.fromJSON(next)); if (name === 'delete' || name === 'default') { this.props.onBlur(); } diff --git a/src/settings/components/index.tsx b/src/settings/components/index.tsx index eeac2cf..160dd9c 100644 --- a/src/settings/components/index.tsx +++ b/src/settings/components/index.tsx @@ -8,11 +8,11 @@ import BlacklistForm from './form/BlacklistForm'; import PropertiesForm from './form/PropertiesForm'; import * as settingActions from '../../settings/actions/setting'; import SettingData, { - JSONSettings, FormKeymaps, FormSearch, FormSettings, + FormKeymaps, FormSearch, FormSettings, JSONTextSettings, } from '../../shared/SettingData'; import { State as AppState } from '../reducers/setting'; -import * as settings from '../../shared/Settings'; -import * as PropertyDefs from '../../shared/property-defs'; +import Properties from '../../shared/settings/Properties'; +import Blacklist from '../../shared/settings/Blacklist'; const DO_YOU_WANT_TO_CONTINUE = 'Some settings in JSON can be lost when migrating. ' + @@ -32,12 +32,7 @@ class SettingsComponent extends React.Component<Props> { this.props.dispatch(settingActions.load()); } - renderFormFields(form: any) { - let types = PropertyDefs.defs.reduce( - (o: {[key: string]: string}, def) => { - o[def.name] = def.type; - return o; - }, {}); + renderFormFields(form: FormSettings) { return <div> <fieldset> <legend>Keybindings</legend> @@ -58,7 +53,7 @@ class SettingsComponent extends React.Component<Props> { <fieldset> <legend>Blacklist</legend> <BlacklistForm - value={form.blacklist} + value={form.blacklist.toJSON()} onChange={this.bindBlacklistForm.bind(this)} onBlur={this.save.bind(this)} /> @@ -66,8 +61,8 @@ class SettingsComponent extends React.Component<Props> { <fieldset> <legend>Properties</legend> <PropertiesForm - types={types} - value={form.properties} + types={Properties.types()} + value={form.properties.toJSON()} onChange={this.bindPropertiesForm.bind(this)} onBlur={this.save.bind(this)} /> @@ -75,7 +70,7 @@ class SettingsComponent extends React.Component<Props> { </div>; } - renderJsonFields(json: JSONSettings, error: string) { + renderJsonFields(json: JSONTextSettings, error: string) { return <div> <Input type='textarea' @@ -85,7 +80,7 @@ class SettingsComponent extends React.Component<Props> { error={error} onValueChange={this.bindJson.bind(this)} onBlur={this.save.bind(this)} - value={json.toJSON()} + value={json.toJSONText()} /> </div>; } @@ -94,10 +89,9 @@ class SettingsComponent extends React.Component<Props> { let fields = null; let disabled = this.props.error.length > 0; if (this.props.source === 'form') { - fields = this.renderFormFields(this.props.form); + fields = this.renderFormFields(this.props.form!!); } else if (this.props.source === 'json') { - fields = this.renderJsonFields( - this.props.json as JSONSettings, this.props.error); + fields = this.renderJsonFields(this.props.json!!, this.props.error); } return ( <div> @@ -139,7 +133,7 @@ class SettingsComponent extends React.Component<Props> { let data = new SettingData({ source: this.props.source, form: (this.props.form as FormSettings).buildWithSearch( - FormSearch.valueOf(value)), + FormSearch.fromJSON(value)), }); this.props.dispatch(settingActions.set(data)); } @@ -148,7 +142,7 @@ class SettingsComponent extends React.Component<Props> { let data = new SettingData({ source: this.props.source, form: (this.props.form as FormSettings).buildWithBlacklist( - settings.blacklistValueOf(value)), + Blacklist.fromJSON(value)), }); this.props.dispatch(settingActions.set(data)); } @@ -157,7 +151,7 @@ class SettingsComponent extends React.Component<Props> { let data = new SettingData({ source: this.props.source, form: (this.props.form as FormSettings).buildWithProperties( - settings.propertiesValueOf(value)), + Properties.fromJSON(value)) }); this.props.dispatch(settingActions.set(data)); } @@ -165,7 +159,7 @@ class SettingsComponent extends React.Component<Props> { bindJson(_name: string, value: string) { let data = new SettingData({ source: this.props.source, - json: JSONSettings.valueOf(value), + json: JSONTextSettings.fromText(value), }); this.props.dispatch(settingActions.set(data)); } @@ -183,7 +177,7 @@ class SettingsComponent extends React.Component<Props> { return; } this.props.dispatch( - settingActions.switchToForm(this.props.json as JSONSettings)); + settingActions.switchToForm(this.props.json as JSONTextSettings)); this.save(); } } diff --git a/src/settings/reducers/setting.ts b/src/settings/reducers/setting.ts index c4a21c7..804853f 100644 --- a/src/settings/reducers/setting.ts +++ b/src/settings/reducers/setting.ts @@ -1,18 +1,20 @@ import * as actions from '../actions'; import { - JSONSettings, FormSettings, SettingSource, + JSONTextSettings, FormSettings, SettingSource, } from '../../shared/SettingData'; +import { DefaultSetting } from '../../shared/settings/Settings'; export interface State { source: SettingSource; - json?: JSONSettings; + json?: JSONTextSettings; form?: FormSettings; error: string; } const defaultState: State = { source: SettingSource.JSON, - json: JSONSettings.valueOf(''), + json: JSONTextSettings.fromText(''), + form: FormSettings.fromSettings(DefaultSetting), error: '', }; diff --git a/src/settings/storage.ts b/src/settings/storage.ts index 32b6351..2a983df 100644 --- a/src/settings/storage.ts +++ b/src/settings/storage.ts @@ -6,7 +6,7 @@ export const load = async(): Promise<SettingData> => { return DefaultSettingData; } try { - return SettingData.valueOf(settings as any); + return SettingData.fromJSON(settings as any); } catch (e) { console.error('unable to load settings', e); return DefaultSettingData; 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 }; |