diff options
67 files changed, 1315 insertions, 1288 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 }; diff --git a/test/content/InputDriver.test.ts b/test/content/InputDriver.test.ts index b9f2c28..441d107 100644 --- a/test/content/InputDriver.test.ts +++ b/test/content/InputDriver.test.ts @@ -1,6 +1,6 @@ -import InputDriver from '../../src/content/InputDriver'; +import InputDriver, {keyFromKeyboardEvent} from '../../src/content/InputDriver'; import { expect } from 'chai'; -import Key from '../../src/content/domains/Key'; +import Key from '../../src/shared/settings/Key'; describe('InputDriver', () => { let target: HTMLElement; @@ -21,10 +21,10 @@ describe('InputDriver', () => { it('register callbacks', (done) => { driver.onKey((key: Key): boolean => { expect(key.key).to.equal('a'); - expect(key.ctrlKey).to.be.true; - expect(key.shiftKey).to.be.false; - expect(key.altKey).to.be.false; - expect(key.metaKey).to.be.false; + expect(key.ctrl).to.be.true; + expect(key.shift).to.be.false; + expect(key.alt).to.be.false; + expect(key.meta).to.be.false; done(); return true; }); @@ -68,15 +68,15 @@ describe('InputDriver', () => { it('propagates and stop handler chain', () => { let a = 0, b = 0, c = 0; - driver.onKey((key: Key): boolean => { + driver.onKey((_key: Key): boolean => { a++; return false; }); - driver.onKey((key: Key): boolean => { + driver.onKey((_key: Key): boolean => { b++; return true; }); - driver.onKey((key: Key): boolean => { + driver.onKey((_key: Key): boolean => { c++; return true; }); @@ -89,7 +89,7 @@ describe('InputDriver', () => { }) it('does not invoke only meta keys', () => { - driver.onKey((key: Key): boolean=> { + driver.onKey((_key: Key): boolean=> { expect.fail(); return false; }); @@ -115,7 +115,7 @@ describe('InputDriver', () => { it('ignores events from contenteditable elements', () => { let div = window.document.createElement('div'); let driver = new InputDriver(div); - driver.onKey((key: Key): boolean => { + driver.onKey((_key: Key): boolean => { expect.fail(); return false; }); @@ -127,3 +127,50 @@ describe('InputDriver', () => { div.dispatchEvent(new KeyboardEvent('keydown', { key: 'x' })); }); }); + +describe("#keyFromKeyboardEvent", () => { + it('returns from keyboard input Ctrl+X', () => { + let k = keyFromKeyboardEvent(new KeyboardEvent('keydown', { + key: 'x', shiftKey: false, ctrlKey: true, altKey: false, metaKey: true, + })); + expect(k.key).to.equal('x'); + expect(k.shift).to.be.false; + expect(k.ctrl).to.be.true; + expect(k.alt).to.be.false; + expect(k.meta).to.be.true; + }); + + it('returns from keyboard input Shift+Esc', () => { + let k = keyFromKeyboardEvent(new KeyboardEvent('keydown', { + key: 'Escape', shiftKey: true, ctrlKey: false, altKey: false, metaKey: true + })); + expect(k.key).to.equal('Esc'); + expect(k.shift).to.be.true; + expect(k.ctrl).to.be.false; + expect(k.alt).to.be.false; + expect(k.meta).to.be.true; + }); + + it('returns from keyboard input Ctrl+$', () => { + // $ required shift pressing on most keyboards + let k = keyFromKeyboardEvent(new KeyboardEvent('keydown', { + key: '$', shiftKey: true, ctrlKey: true, altKey: false, metaKey: false + })); + expect(k.key).to.equal('$'); + expect(k.shift).to.be.false; + expect(k.ctrl).to.be.true; + expect(k.alt).to.be.false; + expect(k.meta).to.be.false; + }); + + it('returns from keyboard input Crtl+Space', () => { + let k = keyFromKeyboardEvent(new KeyboardEvent('keydown', { + key: ' ', shiftKey: false, ctrlKey: true, altKey: false, metaKey: false + })); + expect(k.key).to.equal('Space'); + expect(k.shift).to.be.false; + expect(k.ctrl).to.be.true; + expect(k.alt).to.be.false; + expect(k.meta).to.be.false; + }); +}); diff --git a/test/content/domains/Key.test.ts b/test/content/domains/Key.test.ts deleted file mode 100644 index b3f9fb6..0000000 --- a/test/content/domains/Key.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import Key, * as keys from '../../../src/content/domains/Key'; -import { expect } from 'chai' - -describe("Key", () => { - describe('fromKeyboardEvent', () => { - it('returns from keyboard input Ctrl+X', () => { - let k = keys.fromKeyboardEvent(new KeyboardEvent('keydown', { - key: 'x', shiftKey: false, ctrlKey: true, altKey: false, metaKey: true, - })); - expect(k.key).to.equal('x'); - expect(k.shiftKey).to.be.false; - expect(k.ctrlKey).to.be.true; - expect(k.altKey).to.be.false; - expect(k.metaKey).to.be.true; - }); - - it('returns from keyboard input Shift+Esc', () => { - let k = keys.fromKeyboardEvent(new KeyboardEvent('keydown', { - key: 'Escape', shiftKey: true, ctrlKey: false, altKey: false, metaKey: true - })); - expect(k.key).to.equal('Esc'); - expect(k.shiftKey).to.be.true; - expect(k.ctrlKey).to.be.false; - expect(k.altKey).to.be.false; - expect(k.metaKey).to.be.true; - }); - - it('returns from keyboard input Ctrl+$', () => { - // $ required shift pressing on most keyboards - let k = keys.fromKeyboardEvent(new KeyboardEvent('keydown', { - key: '$', shiftKey: true, ctrlKey: true, altKey: false, metaKey: false - })); - expect(k.key).to.equal('$'); - expect(k.shiftKey).to.be.false; - expect(k.ctrlKey).to.be.true; - expect(k.altKey).to.be.false; - expect(k.metaKey).to.be.false; - }); - - it('returns from keyboard input Crtl+Space', () => { - let k = keys.fromKeyboardEvent(new KeyboardEvent('keydown', { - key: ' ', shiftKey: false, ctrlKey: true, altKey: false, metaKey: false - })); - expect(k.key).to.equal('Space'); - expect(k.shiftKey).to.be.false; - expect(k.ctrlKey).to.be.true; - expect(k.altKey).to.be.false; - expect(k.metaKey).to.be.false; - }); - }); - - describe('fromMapKey', () => { - it('return for X', () => { - let key = keys.fromMapKey('x'); - expect(key.key).to.equal('x'); - expect(key.shiftKey).to.be.false; - expect(key.ctrlKey).to.be.false; - expect(key.altKey).to.be.false; - expect(key.metaKey).to.be.false; - }); - - it('return for Shift+X', () => { - let key = keys.fromMapKey('X'); - expect(key.key).to.equal('X'); - expect(key.shiftKey).to.be.true; - expect(key.ctrlKey).to.be.false; - expect(key.altKey).to.be.false; - expect(key.metaKey).to.be.false; - }); - - it('return for Ctrl+X', () => { - let key = keys.fromMapKey('<C-X>'); - expect(key.key).to.equal('x'); - expect(key.shiftKey).to.be.false; - expect(key.ctrlKey).to.be.true; - expect(key.altKey).to.be.false; - expect(key.metaKey).to.be.false; - }); - - it('returns for Ctrl+Meta+X', () => { - let key = keys.fromMapKey('<C-M-X>'); - expect(key.key).to.equal('x'); - expect(key.shiftKey).to.be.false; - expect(key.ctrlKey).to.be.true; - expect(key.altKey).to.be.false; - expect(key.metaKey).to.be.true; - }); - - it('returns for Ctrl+Shift+x', () => { - let key = keys.fromMapKey('<C-S-x>'); - expect(key.key).to.equal('X'); - expect(key.shiftKey).to.be.true; - expect(key.ctrlKey).to.be.true; - expect(key.altKey).to.be.false; - expect(key.metaKey).to.be.false; - }); - - it('returns for Shift+Esc', () => { - let key = keys.fromMapKey('<S-Esc>'); - expect(key.key).to.equal('Esc'); - expect(key.shiftKey).to.be.true; - expect(key.ctrlKey).to.be.false; - expect(key.altKey).to.be.false; - expect(key.metaKey).to.be.false; - }); - - it('returns for Ctrl+Esc', () => { - let key = keys.fromMapKey('<C-Esc>'); - expect(key.key).to.equal('Esc'); - expect(key.shiftKey).to.be.false; - expect(key.ctrlKey).to.be.true; - expect(key.altKey).to.be.false; - expect(key.metaKey).to.be.false; - }); - - it('returns for Ctrl+Esc', () => { - let key = keys.fromMapKey('<C-Space>'); - expect(key.key).to.equal('Space'); - expect(key.shiftKey).to.be.false; - expect(key.ctrlKey).to.be.true; - expect(key.altKey).to.be.false; - expect(key.metaKey).to.be.false; - }); - }); - - describe('equals', () => { - expect(keys.equals( - { key: 'x', ctrlKey: true, }, - { key: 'x', ctrlKey: true, }, - )).to.be.true; - - expect(keys.equals( - { key: 'X', shiftKey: true, }, - { key: 'x', ctrlKey: true, }, - )).to.be.false; - }); -}); diff --git a/test/content/domains/KeySequence.test.ts b/test/content/domains/KeySequence.test.ts deleted file mode 100644 index 7387c06..0000000 --- a/test/content/domains/KeySequence.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import KeySequence, * as utils from '../../../src/content/domains/KeySequence'; -import { expect } from 'chai' - -describe("KeySequence", () => { - describe('#push', () => { - it('append a key to the sequence', () => { - let seq = KeySequence.from([]); - seq.push({ key: 'g' }); - seq.push({ key: 'u', shiftKey: true }); - - let array = seq.getKeyArray(); - expect(array[0]).to.deep.equal({ key: 'g' }); - expect(array[1]).to.deep.equal({ key: 'u', shiftKey: true }); - }) - }); - - describe('#startsWith', () => { - it('returns true if the key sequence starts with param', () => { - let seq = KeySequence.from([ - { key: 'g' }, - { key: 'u', shiftKey: true }, - ]); - - expect(seq.startsWith(KeySequence.from([ - ]))).to.be.true; - expect(seq.startsWith(KeySequence.from([ - { key: 'g' }, - ]))).to.be.true; - expect(seq.startsWith(KeySequence.from([ - { key: 'g' }, { key: 'u', shiftKey: true }, - ]))).to.be.true; - expect(seq.startsWith(KeySequence.from([ - { key: 'g' }, { key: 'u', shiftKey: true }, { key: 'x' }, - ]))).to.be.false; - expect(seq.startsWith(KeySequence.from([ - { key: 'h' }, - ]))).to.be.false; - }) - - it('returns true if the empty sequence starts with an empty sequence', () => { - let seq = KeySequence.from([]); - - expect(seq.startsWith(KeySequence.from([]))).to.be.true; - expect(seq.startsWith(KeySequence.from([ - { key: 'h' }, - ]))).to.be.false; - }) - }); - - describe('#fromMapKeys', () => { - it('returns mapped keys for Shift+Esc', () => { - let keyArray = utils.fromMapKeys('<S-Esc>').getKeyArray(); - expect(keyArray).to.have.lengthOf(1); - expect(keyArray[0].key).to.equal('Esc'); - expect(keyArray[0].shiftKey).to.be.true; - }); - - it('returns mapped keys for a<C-B><A-C>d<M-e>', () => { - let keyArray = utils.fromMapKeys('a<C-B><A-C>d<M-e>').getKeyArray(); - expect(keyArray).to.have.lengthOf(5); - expect(keyArray[0].key).to.equal('a'); - expect(keyArray[1].ctrlKey).to.be.true; - expect(keyArray[1].key).to.equal('b'); - expect(keyArray[2].altKey).to.be.true; - expect(keyArray[2].key).to.equal('c'); - expect(keyArray[3].key).to.equal('d'); - expect(keyArray[4].metaKey).to.be.true; - expect(keyArray[4].key).to.equal('e'); - }); - }) - -}); diff --git a/test/content/repositories/KeymapRepository.test.ts b/test/content/repositories/KeymapRepository.test.ts index 34704d9..df013df 100644 --- a/test/content/repositories/KeymapRepository.test.ts +++ b/test/content/repositories/KeymapRepository.test.ts @@ -1,6 +1,7 @@ import KeymapRepository, { KeymapRepositoryImpl } from '../../../src/content/repositories/KeymapRepository'; import { expect } from 'chai'; +import Key from "../../../src/shared/settings/Key"; describe('KeymapRepositoryImpl', () => { let sut: KeymapRepository; @@ -11,24 +12,25 @@ describe('KeymapRepositoryImpl', () => { describe('#enqueueKey()', () => { it('enqueues keys', () => { - sut.enqueueKey({ key: 'a' }); - sut.enqueueKey({ key: 'b' }); - let sequence = sut.enqueueKey({ key: 'c' }); - - expect(sequence.getKeyArray()).deep.equals([ - { key: 'a' }, { key: 'b' }, { key: 'c' }, - ]); + sut.enqueueKey(Key.fromMapKey('a'); + sut.enqueueKey(Key.fromMapKey('b'); + let sequence = sut.enqueueKey(Key.fromMapKey('c')); + + let keys = sequence.keys; + expect(keys[0].equals(Key.fromMapKey('a'))).to.be.true; + expect(keys[1].equals(Key.fromMapKey('b'))).to.be.true; + expect(keys[2].equals(Key.fromMapKey('c'))).to.be.true; }); }); describe('#clear()', () => { it('clears keys', () => { - sut.enqueueKey({ key: 'a' }); - sut.enqueueKey({ key: 'b' }); - sut.enqueueKey({ key: 'c' }); + sut.enqueueKey(Key.fromMapKey('a')); + sut.enqueueKey(Key.fromMapKey('b')); + sut.enqueueKey(Key.fromMapKey('c')); sut.clear(); - let sequence = sut.enqueueKey({ key: 'a' }); + let sequence = sut.enqueueKey(Key.fromMapKey('a')); expect(sequence.length()).to.equal(1); }); }); diff --git a/test/content/repositories/SettingRepository.test.ts b/test/content/repositories/SettingRepository.test.ts index fea70b7..db4c528 100644 --- a/test/content/repositories/SettingRepository.test.ts +++ b/test/content/repositories/SettingRepository.test.ts @@ -1,13 +1,14 @@ import { SettingRepositoryImpl } from '../../../src/content/repositories/SettingRepository'; import { expect } from 'chai'; +import Settings from '../../../src/shared/settings/Settings'; describe('SettingRepositoryImpl', () => { it('updates and gets current value', () => { let sut = new SettingRepositoryImpl(); - let settings = { + let settings = Settings.fromJSON({ keymaps: {}, - search: { + search:{ default: 'google', engines: { google: 'https://google.com/?q={}', @@ -19,7 +20,7 @@ describe('SettingRepositoryImpl', () => { complete: 'sbh', }, blacklist: [], - } + }); sut.set(settings); @@ -27,4 +28,3 @@ describe('SettingRepositoryImpl', () => { expect(actual.properties.hintchars).to.equal('abcd1234'); }); }); - diff --git a/test/content/usecases/SettingUseCaase.test.ts b/test/content/usecases/SettingUseCaase.test.ts index e9633f4..136c5af 100644 --- a/test/content/usecases/SettingUseCaase.test.ts +++ b/test/content/usecases/SettingUseCaase.test.ts @@ -1,7 +1,7 @@ import SettingRepository from '../../../src/content/repositories/SettingRepository'; import SettingClient from '../../../src/content/client/SettingClient'; import SettingUseCase from '../../../src/content/usecases/SettingUseCase'; -import Settings, { DefaultSetting } from '../../../src/shared/Settings'; +import Settings, { DefaultSetting } from '../../../src/shared/settings/Settings'; import { expect } from 'chai'; class MockSettingRepository implements SettingRepository { diff --git a/test/settings/components/form/KeymapsForm.test.tsx b/test/settings/components/form/KeymapsForm.test.tsx index dc2322b..1d1e77c 100644 --- a/test/settings/components/form/KeymapsForm.test.tsx +++ b/test/settings/components/form/KeymapsForm.test.tsx @@ -9,7 +9,7 @@ import { expect } from 'chai'; describe("settings/form/KeymapsForm", () => { describe('render', () => { it('renders keymap fields', () => { - let root = ReactTestRenderer.create(<KeymapsForm value={FormKeymaps.valueOf({ + let root = ReactTestRenderer.create(<KeymapsForm value={FormKeymaps.fromJSON({ 'scroll.vertically?{"count":1}': 'j', 'scroll.vertically?{"count":-1}': 'k', })} />).root @@ -48,7 +48,7 @@ describe("settings/form/KeymapsForm", () => { it('invokes onChange event on edit', (done) => { ReactTestUtils.act(() => { ReactDOM.render(<KeymapsForm - value={FormKeymaps.valueOf({ + value={FormKeymaps.fromJSON({ 'scroll.vertically?{"count":1}': 'j', 'scroll.vertically?{"count":-1}': 'k', })} diff --git a/test/settings/components/form/PropertiesForm.test.tsx b/test/settings/components/form/PropertiesForm.test.tsx index 80f60d2..0e33cc8 100644 --- a/test/settings/components/form/PropertiesForm.test.tsx +++ b/test/settings/components/form/PropertiesForm.test.tsx @@ -13,14 +13,14 @@ describe("settings/form/PropertiesForm", () => { mybool: 'boolean', empty: 'string', } - let value = { + let values = { mystr: 'abc', mynum: 123, mybool: true, }; let root = ReactTestRenderer.create( - <PropertiesForm types={types} value={value} />, + <PropertiesForm types={types} value={values} />, ).root let input = root.findByProps({ name: 'mystr' }); diff --git a/test/settings/components/form/SearchEngineForm.test.tsx b/test/settings/components/form/SearchEngineForm.test.tsx index 0e6b17d..1f0420d 100644 --- a/test/settings/components/form/SearchEngineForm.test.tsx +++ b/test/settings/components/form/SearchEngineForm.test.tsx @@ -8,7 +8,7 @@ import { FormSearch } from 'shared/SettingData'; describe("settings/form/SearchForm", () => { describe('render', () => { it('renders SearchForm', () => { - let root = ReactTestRenderer.create(<SearchForm value={FormSearch.valueOf({ + let root = ReactTestRenderer.create(<SearchForm value={FormSearch.fromJSON({ default: 'google', engines: [['google', 'google.com'], ['yahoo', 'yahoo.com']], })} />).root; @@ -41,7 +41,7 @@ describe("settings/form/SearchForm", () => { it('invokes onChange event on edit', (done) => { ReactTestUtils.act(() => { ReactDOM.render(<SearchForm - value={FormSearch.valueOf({ + value={FormSearch.fromJSON({ default: 'google', engines: [['google', 'google.com'], ['yahoo', 'yahoo.com']] })} @@ -67,7 +67,7 @@ describe("settings/form/SearchForm", () => { it('invokes onChange event on delete', (done) => { ReactTestUtils.act(() => { - ReactDOM.render(<SearchForm value={FormSearch.valueOf({ + ReactDOM.render(<SearchForm value={FormSearch.fromJSON({ default: 'yahoo', engines: [['louvre', 'google.com'], ['yahoo', 'yahoo.com']] })} @@ -88,7 +88,7 @@ describe("settings/form/SearchForm", () => { it('invokes onChange event on add', (done) => { ReactTestUtils.act(() => { - ReactDOM.render(<SearchForm value={FormSearch.valueOf({ + ReactDOM.render(<SearchForm value={FormSearch.fromJSON({ default: 'yahoo', engines: [['google', 'google.com']] })} diff --git a/test/shared/SettingData.test.ts b/test/shared/SettingData.test.ts index 8736ecb..5de7770 100644 --- a/test/shared/SettingData.test.ts +++ b/test/shared/SettingData.test.ts @@ -1,8 +1,9 @@ import SettingData, { - FormKeymaps, JSONSettings, FormSettings, + FormKeymaps, JSONTextSettings, FormSettings, } from '../../src/shared/SettingData'; -import Settings, { Keymaps } from '../../src/shared/Settings'; +import Settings from '../../src/shared/settings/Settings'; import { expect } from 'chai'; +import Keymaps from '../../src/shared/settings/Keymaps'; describe('shared/SettingData', () => { describe('FormKeymaps', () => { @@ -11,9 +12,9 @@ describe('shared/SettingData', () => { let data = { 'scroll.vertically?{"count":1}': 'j', 'scroll.home': '0', - } + }; - let keymaps = FormKeymaps.valueOf(data).toKeymaps(); + let keymaps = FormKeymaps.fromJSON(data).toKeymaps().toJSON(); expect(keymaps).to.deep.equal({ 'j': { type: 'scroll.vertically', count: 1 }, '0': { type: 'scroll.home' }, @@ -23,13 +24,13 @@ describe('shared/SettingData', () => { describe('#fromKeymaps to #toJSON', () => { it('create from a Keymaps and create a JSON object', () => { - let data: Keymaps = { + let keymaps: Keymaps = Keymaps.fromJSON({ 'j': { type: 'scroll.vertically', count: 1 }, '0': { type: 'scroll.home' }, - } + }); - let keymaps = FormKeymaps.fromKeymaps(data).toJSON(); - expect(keymaps).to.deep.equal({ + let form = FormKeymaps.fromKeymaps(keymaps).toJSON(); + expect(form).to.deep.equal({ 'scroll.vertically?{"count":1}': 'j', 'scroll.home': '0', }); @@ -56,14 +57,14 @@ describe('shared/SettingData', () => { "blacklist": [] }`; - let settings = JSONSettings.valueOf(o).toSettings(); - expect(settings).to.deep.equal(JSON.parse(o)); + let settings = JSONTextSettings.fromText(o).toSettings(); + expect(settings.toJSON()).to.deep.equal(JSON.parse(o)); }); }); describe('#fromSettings to #toJSON', () => { it('create from a Settings and create a JSON string', () => { - let o = { + let o = Settings.fromJSON({ keymaps: {}, search: { default: "google", @@ -77,10 +78,10 @@ describe('shared/SettingData', () => { complete: "sbh" }, blacklist: [], - }; + }); - let json = JSONSettings.fromSettings(o).toJSON(); - expect(JSON.parse(json)).to.deep.equal(o); + let json = JSONTextSettings.fromSettings(o).toJSONText(); + expect(JSON.parse(json)).to.deep.equal(o.toJSON()); }); }); }); @@ -107,8 +108,8 @@ describe('shared/SettingData', () => { blacklist: [] }; - let settings = FormSettings.valueOf(data).toSettings(); - expect(settings).to.deep.equal({ + let settings = FormSettings.fromJSON(data).toSettings(); + expect(settings.toJSON()).to.deep.equal({ keymaps: { 'j': { type: 'scroll.vertically', count: 1 }, '0': { type: 'scroll.home' }, @@ -131,7 +132,7 @@ describe('shared/SettingData', () => { describe('#fromSettings to #toJSON', () => { it('create from a Settings and create a JSON string', () => { - let data: Settings = { + let data: Settings = Settings.fromJSON({ keymaps: { 'j': { type: 'scroll.vertically', count: 1 }, '0': { type: 'scroll.home' }, @@ -147,8 +148,8 @@ describe('shared/SettingData', () => { smoothscroll: false, complete: "sbh" }, - blacklist: [] - }; + blacklist: [], + }); let json = FormSettings.fromSettings(data).toJSON(); expect(json).to.deep.equal({ @@ -195,7 +196,7 @@ describe('shared/SettingData', () => { }`, }; - let j = SettingData.valueOf(data).toJSON(); + let j = SettingData.fromJSON(data).toJSON(); expect(j.source).to.equal('json'); expect(j.json).to.be.a('string'); }); @@ -220,7 +221,7 @@ describe('shared/SettingData', () => { }, }; - let j = SettingData.valueOf(data).toJSON(); + let j = SettingData.fromJSON(data).toJSON(); expect(j.source).to.equal('form'); expect(j.form).to.deep.equal({ keymaps: {}, @@ -261,8 +262,8 @@ describe('shared/SettingData', () => { }`, }; - let settings = SettingData.valueOf(data).toSettings(); - expect(settings.search.default).to.equal('google'); + let settings = SettingData.fromJSON(data).toSettings(); + expect(settings.search.defaultEngine).to.equal('google'); }); it('parse object from form source', () => { @@ -285,8 +286,8 @@ describe('shared/SettingData', () => { }, }; - let settings = SettingData.valueOf(data).toSettings(); - expect(settings.search.default).to.equal('yahoo'); + let settings = SettingData.fromJSON(data).toSettings(); + expect(settings.search.defaultEngine).to.equal('yahoo'); }); }); }); diff --git a/test/shared/Settings.test.ts b/test/shared/Settings.test.ts deleted file mode 100644 index 04b28c4..0000000 --- a/test/shared/Settings.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -import * as settings from '../../src/shared/Settings'; -import { expect } from 'chai'; - -describe('Settings', () => { - describe('#keymapsValueOf', () => { - it('returns empty object by empty settings', () => { - let keymaps = settings.keymapsValueOf({}); - expect(keymaps).to.be.empty; - }); - - it('returns keymaps by valid settings', () => { - let keymaps = settings.keymapsValueOf({ - k: { type: "scroll.vertically", count: -1 }, - j: { type: "scroll.vertically", count: 1 }, - }); - - expect(keymaps['k']).to.deep.equal({ type: "scroll.vertically", count: -1 }); - expect(keymaps['j']).to.deep.equal({ type: "scroll.vertically", count: 1 }); - }); - - it('throws a TypeError by invalid settings', () => { - expect(() => settings.keymapsValueOf(null)).to.throw(TypeError); - expect(() => settings.keymapsValueOf({ - k: { type: "invalid.operation" }, - })).to.throw(TypeError); - }); - }); - - describe('#searchValueOf', () => { - it('returns search settings by valid settings', () => { - let search = settings.searchValueOf({ - default: "google", - engines: { - "google": "https://google.com/search?q={}", - "yahoo": "https://search.yahoo.com/search?p={}", - } - }); - - expect(search).to.deep.equal({ - default: "google", - engines: { - "google": "https://google.com/search?q={}", - "yahoo": "https://search.yahoo.com/search?p={}", - } - }); - }); - - it('throws a TypeError by invalid settings', () => { - expect(() => settings.searchValueOf(null)).to.throw(TypeError); - expect(() => settings.searchValueOf({})).to.throw(TypeError); - expect(() => settings.searchValueOf([])).to.throw(TypeError); - expect(() => settings.searchValueOf({ - default: 123, - engines: {} - })).to.throw(TypeError); - expect(() => settings.searchValueOf({ - default: "google", - engines: { - "google": 123456, - } - })).to.throw(TypeError); - expect(() => settings.searchValueOf({ - default: "wikipedia", - engines: { - "google": "https://google.com/search?q={}", - "yahoo": "https://search.yahoo.com/search?p={}", - } - })).to.throw(TypeError); - expect(() => settings.searchValueOf({ - default: "g o o g l e", - engines: { - "g o o g l e": "https://google.com/search?q={}", - } - })).to.throw(TypeError); - expect(() => settings.searchValueOf({ - default: "google", - engines: { - "google": "https://google.com/search", - } - })).to.throw(TypeError); - expect(() => settings.searchValueOf({ - default: "google", - engines: { - "google": "https://google.com/search?q={}&r={}", - } - })).to.throw(TypeError); - }); - }); - - describe('#propertiesValueOf', () => { - it('returns with default properties by empty settings', () => { - let props = settings.propertiesValueOf({}); - expect(props).to.deep.equal({ - hintchars: "abcdefghijklmnopqrstuvwxyz", - smoothscroll: false, - complete: "sbh" - }) - }); - - it('returns properties by valid settings', () => { - let props = settings.propertiesValueOf({ - hintchars: "abcdefgh", - smoothscroll: false, - complete: "sbh" - }); - - expect(props).to.deep.equal({ - hintchars: "abcdefgh", - smoothscroll: false, - complete: "sbh" - }); - }); - - it('throws a TypeError by invalid settings', () => { - expect(() => settings.keymapsValueOf(null)).to.throw(TypeError); - expect(() => settings.keymapsValueOf({ - smoothscroll: 'false', - })).to.throw(TypeError); - expect(() => settings.keymapsValueOf({ - unknown: 'xyz' - })).to.throw(TypeError); - }); - }); - - describe('#blacklistValueOf', () => { - it('returns empty array by empty settings', () => { - let blacklist = settings.blacklistValueOf([]); - expect(blacklist).to.be.empty; - }); - - it('returns blacklist by valid settings', () => { - let blacklist = settings.blacklistValueOf([ - "github.com", - "circleci.com", - ]); - - expect(blacklist).to.deep.equal([ - "github.com", - "circleci.com", - ]); - }); - - it('throws a TypeError by invalid settings', () => { - expect(() => settings.blacklistValueOf(null)).to.throw(TypeError); - expect(() => settings.blacklistValueOf({})).to.throw(TypeError); - expect(() => settings.blacklistValueOf([1,2,3])).to.throw(TypeError); - }); - }); - - describe('#valueOf', () => { - it('returns settings by valid settings', () => { - let x = settings.valueOf({ - keymaps: {}, - "search": { - "default": "google", - "engines": { - "google": "https://google.com/search?q={}", - } - }, - "properties": {}, - "blacklist": [] - }); - - expect(x).to.deep.equal({ - keymaps: {}, - search: { - default: "google", - engines: { - google: "https://google.com/search?q={}", - } - }, - properties: { - hintchars: "abcdefghijklmnopqrstuvwxyz", - smoothscroll: false, - complete: "sbh" - }, - blacklist: [] - }); - }); - - it('sets default settings', () => { - let value = settings.valueOf({}); - expect(value.keymaps).to.not.be.empty; - expect(value.properties).to.not.be.empty; - expect(value.search.default).to.be.a('string'); - expect(value.search.engines).to.be.an('object'); - expect(value.blacklist).to.be.empty; - }); - - it('throws a TypeError with an unknown field', () => { - expect(() => settings.valueOf({ name: 'alice' })).to.throw(TypeError) - }); - }); -}); diff --git a/test/shared/blacklists.test.ts b/test/shared/blacklists.test.ts deleted file mode 100644 index 289ea0f..0000000 --- a/test/shared/blacklists.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { includes } from 'shared/blacklists'; - -describe("shared/blacklist", () => { - it('matches by *', () => { - let blacklist = ['*']; - - expect(includes(blacklist, 'https://github.com/abc')).to.be.true; - }) - - it('matches by hostname', () => { - let blacklist = ['github.com']; - - expect(includes(blacklist, 'https://github.com')).to.be.true; - expect(includes(blacklist, 'https://gist.github.com')).to.be.false; - expect(includes(blacklist, 'https://github.com/ueokande')).to.be.true; - expect(includes(blacklist, 'https://github.org')).to.be.false; - expect(includes(blacklist, 'https://google.com/search?q=github.org')).to.be.false; - }) - - it('matches by hostname with wildcard', () => { - let blacklist = ['*.github.com']; - - expect(includes(blacklist, 'https://github.com')).to.be.false; - expect(includes(blacklist, 'https://gist.github.com')).to.be.true; - }) - - it('matches by path', () => { - let blacklist = ['github.com/abc']; - - expect(includes(blacklist, 'https://github.com/abc')).to.be.true; - expect(includes(blacklist, 'https://github.com/abcdef')).to.be.false; - expect(includes(blacklist, 'https://gist.github.com/abc')).to.be.false; - }) - - it('matches by path with wildcard', () => { - let blacklist = ['github.com/abc*']; - - expect(includes(blacklist, 'https://github.com/abc')).to.be.true; - expect(includes(blacklist, 'https://github.com/abcdef')).to.be.true; - expect(includes(blacklist, 'https://gist.github.com/abc')).to.be.false; - }) - - it('matches address and port', () => { - let blacklist = ['127.0.0.1:8888']; - - expect(includes(blacklist, 'http://127.0.0.1:8888/')).to.be.true; - expect(includes(blacklist, 'http://127.0.0.1:8888/hello')).to.be.true; - }) -}); diff --git a/test/shared/properties.test.js b/test/shared/properties.test.js deleted file mode 100644 index 37903d8..0000000 --- a/test/shared/properties.test.js +++ /dev/null @@ -1,18 +0,0 @@ -import * as settings from 'shared/settings'; - -describe('properties', () => { - describe('Def class', () => { - it('returns property definitions', () => { - let def = new proerties.Def( - 'smoothscroll', - 'smooth scroll', - false); - - expect(def.name).to.equal('smoothscroll'); - expect(def.describe).to.equal('smooth scroll'); - expect(def.defaultValue).to.equal(false); - expect(def.type).to.equal('boolean'); - }); - }); -}); - diff --git a/test/shared/property-defs.test.js b/test/shared/property-defs.test.js deleted file mode 100644 index 37903d8..0000000 --- a/test/shared/property-defs.test.js +++ /dev/null @@ -1,18 +0,0 @@ -import * as settings from 'shared/settings'; - -describe('properties', () => { - describe('Def class', () => { - it('returns property definitions', () => { - let def = new proerties.Def( - 'smoothscroll', - 'smooth scroll', - false); - - expect(def.name).to.equal('smoothscroll'); - expect(def.describe).to.equal('smooth scroll'); - expect(def.defaultValue).to.equal(false); - expect(def.type).to.equal('boolean'); - }); - }); -}); - diff --git a/test/shared/settings/Blacklist.test.ts b/test/shared/settings/Blacklist.test.ts new file mode 100644 index 0000000..fbacf5d --- /dev/null +++ b/test/shared/settings/Blacklist.test.ts @@ -0,0 +1,77 @@ +import Blacklist from '../../../src/shared/settings/Blacklist'; +import { expect } from 'chai'; + +describe('Blacklist', () => { + describe('fromJSON', () => { + it('returns empty array by empty settings', () => { + let blacklist = Blacklist.fromJSON([]); + expect(blacklist.toJSON()).to.be.empty; + }); + + it('returns blacklist by valid settings', () => { + let blacklist = Blacklist.fromJSON([ + 'github.com', + 'circleci.com', + ]); + + expect(blacklist.toJSON()).to.deep.equal([ + 'github.com', + 'circleci.com', + ]); + }); + + it('throws a TypeError by invalid settings', () => { + expect(() => Blacklist.fromJSON(null)).to.throw(TypeError); + expect(() => Blacklist.fromJSON({})).to.throw(TypeError); + expect(() => Blacklist.fromJSON([1,2,3])).to.throw(TypeError); + }); + }); + + describe('#includes', () => { + it('matches by *', () => { + let blacklist = new Blacklist(['*']); + + expect(blacklist.includes('https://github.com/abc')).to.be.true; + }); + + it('matches by hostname', () => { + let blacklist = new Blacklist(['github.com']); + + expect(blacklist.includes('https://github.com')).to.be.true; + expect(blacklist.includes('https://gist.github.com')).to.be.false; + expect(blacklist.includes('https://github.com/ueokande')).to.be.true; + expect(blacklist.includes('https://github.org')).to.be.false; + expect(blacklist.includes('https://google.com/search?q=github.org')).to.be.false; + }); + + it('matches by hostname with wildcard', () => { + let blacklist = new Blacklist(['*.github.com']); + + expect(blacklist.includes('https://github.com')).to.be.false; + expect(blacklist.includes('https://gist.github.com')).to.be.true; + }) + + it('matches by path', () => { + let blacklist = new Blacklist(['github.com/abc']); + + expect(blacklist.includes('https://github.com/abc')).to.be.true; + expect(blacklist.includes('https://github.com/abcdef')).to.be.false; + expect(blacklist.includes('https://gist.github.com/abc')).to.be.false; + }) + + it('matches by path with wildcard', () => { + let blacklist = new Blacklist(['github.com/abc*']); + + expect(blacklist.includes('https://github.com/abc')).to.be.true; + expect(blacklist.includes('https://github.com/abcdef')).to.be.true; + expect(blacklist.includes('https://gist.github.com/abc')).to.be.false; + }) + + it('matches address and port', () => { + let blacklist = new Blacklist(['127.0.0.1:8888']); + + expect(blacklist.includes('http://127.0.0.1:8888/')).to.be.true; + expect(blacklist.includes('http://127.0.0.1:8888/hello')).to.be.true; + }) + }) +}); diff --git a/test/shared/settings/Key.test.ts b/test/shared/settings/Key.test.ts new file mode 100644 index 0000000..8222d5a --- /dev/null +++ b/test/shared/settings/Key.test.ts @@ -0,0 +1,92 @@ +import { expect } from 'chai' +import Key from '../../../src/shared/settings/Key'; + +describe("Key", () => { + describe('fromMapKey', () => { + it('return for X', () => { + let key = Key.fromMapKey('x'); + expect(key.key).to.equal('x'); + expect(key.shift).to.be.false; + expect(key.ctrl).to.be.false; + expect(key.alt).to.be.false; + expect(key.meta).to.be.false; + }); + + it('return for Shift+X', () => { + let key = Key.fromMapKey('X'); + expect(key.key).to.equal('X'); + expect(key.shift).to.be.true; + expect(key.ctrl).to.be.false; + expect(key.alt).to.be.false; + expect(key.meta).to.be.false; + }); + + it('return for Ctrl+X', () => { + let key = Key.fromMapKey('<C-X>'); + expect(key.key).to.equal('x'); + expect(key.shift).to.be.false; + expect(key.ctrl).to.be.true; + expect(key.alt).to.be.false; + expect(key.meta).to.be.false; + }); + + it('returns for Ctrl+Meta+X', () => { + let key = Key.fromMapKey('<C-M-X>'); + expect(key.key).to.equal('x'); + expect(key.shift).to.be.false; + expect(key.ctrl).to.be.true; + expect(key.alt).to.be.false; + expect(key.meta).to.be.true; + }); + + it('returns for Ctrl+Shift+x', () => { + let key = Key.fromMapKey('<C-S-x>'); + expect(key.key).to.equal('X'); + expect(key.shift).to.be.true; + expect(key.ctrl).to.be.true; + expect(key.alt).to.be.false; + expect(key.meta).to.be.false; + }); + + it('returns for Shift+Esc', () => { + let key = Key.fromMapKey('<S-Esc>'); + expect(key.key).to.equal('Esc'); + expect(key.shift).to.be.true; + expect(key.ctrl).to.be.false; + expect(key.alt).to.be.false; + expect(key.meta).to.be.false; + }); + + it('returns for Ctrl+Esc', () => { + let key = Key.fromMapKey('<C-Esc>'); + expect(key.key).to.equal('Esc'); + expect(key.shift).to.be.false; + expect(key.ctrl).to.be.true; + expect(key.alt).to.be.false; + expect(key.meta).to.be.false; + }); + + it('returns for Ctrl+Esc', () => { + let key = Key.fromMapKey('<C-Space>'); + expect(key.key).to.equal('Space'); + expect(key.shift).to.be.false; + expect(key.ctrl).to.be.true; + expect(key.alt).to.be.false; + expect(key.meta).to.be.false; + }); + }); + + describe('equals', () => { + expect(new Key({ + key: 'x', shift: false, ctrl: true, alt: false, meta: false, + }).equals(new Key({ + key: 'x', shift: false, ctrl: true, alt: false, meta: false, + }))).to.be.true; + + expect(new Key({ + key: 'x', shift: false, ctrl: false, alt: false, meta: false, + }).equals(new Key({ + key: 'X', shift: true, ctrl: false, alt: false, meta: false, + }))).to.be.false; + }); +}); diff --git a/test/shared/settings/KeySequence.test.ts b/test/shared/settings/KeySequence.test.ts new file mode 100644 index 0000000..361cbd1 --- /dev/null +++ b/test/shared/settings/KeySequence.test.ts @@ -0,0 +1,72 @@ +import KeySequence from '../../../src/shared/settings/KeySequence'; +import { expect } from 'chai' +import Key from "../../../src/shared/settings/Key"; + +describe("KeySequence", () => { + describe('#push', () => { + it('append a key to the sequence', () => { + let seq = new KeySequence([]); + seq.push(Key.fromMapKey('g')); + seq.push(Key.fromMapKey('<S-U>')); + + expect(seq.keys[0].key).to.equal('g'); + expect(seq.keys[1].key).to.equal('U'); + expect(seq.keys[1].shift).to.be.true; + }) + }); + + describe('#startsWith', () => { + it('returns true if the key sequence starts with param', () => { + let seq = new KeySequence([ + Key.fromMapKey('g'), + Key.fromMapKey('<S-U>'), + ]); + + expect(seq.startsWith(new KeySequence([ + ]))).to.be.true; + expect(seq.startsWith(new KeySequence([ + Key.fromMapKey('g'), + ]))).to.be.true; + expect(seq.startsWith(new KeySequence([ + Key.fromMapKey('g'), Key.fromMapKey('<S-U>'), + ]))).to.be.true; + expect(seq.startsWith(new KeySequence([ + Key.fromMapKey('g'), Key.fromMapKey('<S-U>'), Key.fromMapKey('x'), + ]))).to.be.false; + expect(seq.startsWith(new KeySequence([ + Key.fromMapKey('h'), + ]))).to.be.false; + }); + + it('returns true if the empty sequence starts with an empty sequence', () => { + let seq = new KeySequence([]); + + expect(seq.startsWith(new KeySequence([]))).to.be.true; + expect(seq.startsWith(new KeySequence([ + Key.fromMapKey('h'), + ]))).to.be.false; + }) + }); + + describe('#fromMapKeys', () => { + it('returns mapped keys for Shift+Esc', () => { + let keys = KeySequence.fromMapKeys('<S-Esc>').keys; + expect(keys).to.have.lengthOf(1); + expect(keys[0].key).to.equal('Esc'); + expect(keys[0].shift).to.be.true; + }); + + it('returns mapped keys for a<C-B><A-C>d<M-e>', () => { + let keys = KeySequence.fromMapKeys('a<C-B><A-C>d<M-e>').keys; + expect(keys).to.have.lengthOf(5); + expect(keys[0].key).to.equal('a'); + expect(keys[1].ctrl).to.be.true; + expect(keys[1].key).to.equal('b'); + expect(keys[2].alt).to.be.true; + expect(keys[2].key).to.equal('c'); + expect(keys[3].key).to.equal('d'); + expect(keys[4].meta).to.be.true; + expect(keys[4].key).to.equal('e'); + }); + }) +}); diff --git a/test/shared/settings/Keymaps.test.ts b/test/shared/settings/Keymaps.test.ts new file mode 100644 index 0000000..7896a63 --- /dev/null +++ b/test/shared/settings/Keymaps.test.ts @@ -0,0 +1,66 @@ +import Keymaps from '../../../src/shared/settings/Keymaps'; +import { expect } from 'chai'; + +describe('Keymaps', () => { + describe('#valueOf', () => { + it('returns empty object by empty settings', () => { + let keymaps = Keymaps.fromJSON({}).toJSON(); + expect(keymaps).to.be.empty; + }); + + it('returns keymaps by valid settings', () => { + let keymaps = Keymaps.fromJSON({ + k: { type: "scroll.vertically", count: -1 }, + j: { type: "scroll.vertically", count: 1 }, + }).toJSON(); + + expect(keymaps['k']).to.deep.equal({ type: "scroll.vertically", count: -1 }); + expect(keymaps['j']).to.deep.equal({ type: "scroll.vertically", count: 1 }); + }); + + it('throws a TypeError by invalid settings', () => { + expect(() => Keymaps.fromJSON(null)).to.throw(TypeError); + expect(() => Keymaps.fromJSON({ + k: { type: "invalid.operation" }, + })).to.throw(TypeError); + }); + }); + + describe('#combine', () => { + it('returns combined keymaps', () => { + let keymaps = Keymaps.fromJSON({ + k: { type: "scroll.vertically", count: -1 }, + j: { type: "scroll.vertically", count: 1 }, + }).combine(Keymaps.fromJSON({ + n: { type: "find.next" }, + N: { type: "find.prev" }, + })); + + let entries = keymaps.entries().sort(([name1], [name2]) => name1.localeCompare(name2)); + expect(entries).deep.equals([ + ['j', { type: "scroll.vertically", count: 1 }], + ['k', { type: "scroll.vertically", count: -1 }], + ['n', { type: "find.next" }], + ['N', { type: "find.prev" }], + ]); + }); + + it('overrides current keymaps', () => { + let keymaps = Keymaps.fromJSON({ + k: { type: "scroll.vertically", count: -1 }, + j: { type: "scroll.vertically", count: 1 }, + }).combine(Keymaps.fromJSON({ + n: { type: "find.next" }, + j: { type: "find.prev" }, + })); + + let entries = keymaps.entries().sort(([name1], [name2]) => name1.localeCompare(name2)); + expect(entries).deep.equals([ + ['j', { type: "find.prev" }], + ['k', { type: "scroll.vertically", count: -1 }], + ['n', { type: "find.next" }], + ]); + }); + }); +}); + diff --git a/test/shared/settings/Properties.test.ts b/test/shared/settings/Properties.test.ts new file mode 100644 index 0000000..609a565 --- /dev/null +++ b/test/shared/settings/Properties.test.ts @@ -0,0 +1,30 @@ +import Properties from '../../../src/shared/settings/Properties'; +import { expect } from 'chai'; + +describe('Properties', () => { + describe('#propertiesValueOf', () => { + it('returns with default properties by empty settings', () => { + let props = Properties.fromJSON({}); + expect(props).to.deep.equal({ + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }) + }); + + it('returns properties by valid settings', () => { + let props = Properties.fromJSON({ + hintchars: "abcdefgh", + smoothscroll: false, + complete: "sbh" + }); + + expect(props).to.deep.equal({ + hintchars: "abcdefgh", + smoothscroll: false, + complete: "sbh" + }); + }); + }); +}); + diff --git a/test/shared/settings/Search.test.ts b/test/shared/settings/Search.test.ts new file mode 100644 index 0000000..7c9134d --- /dev/null +++ b/test/shared/settings/Search.test.ts @@ -0,0 +1,68 @@ +import Search from '../../../src/shared/settings/Search'; +import { expect } from 'chai'; + +describe('Search', () => { + it('returns search settings by valid settings', () => { + let search = Search.fromJSON({ + default: 'google', + engines: { + 'google': 'https://google.com/search?q={}', + 'yahoo': 'https://search.yahoo.com/search?p={}', + } + }); + + expect(search.defaultEngine).to.equal('google') + expect(search.engines).to.deep.equals({ + 'google': 'https://google.com/search?q={}', + 'yahoo': 'https://search.yahoo.com/search?p={}', + }); + expect(search.toJSON()).to.deep.equal({ + default: 'google', + engines: { + 'google': 'https://google.com/search?q={}', + 'yahoo': 'https://search.yahoo.com/search?p={}', + } + }); + }); + + it('throws a TypeError by invalid settings', () => { + expect(() => Search.fromJSON(null)).to.throw(TypeError); + expect(() => Search.fromJSON({})).to.throw(TypeError); + expect(() => Search.fromJSON([])).to.throw(TypeError); + expect(() => Search.fromJSON({ + default: 123, + engines: {} + })).to.throw(TypeError); + expect(() => Search.fromJSON({ + default: 'google', + engines: { + 'google': 123456, + } + })).to.throw(TypeError); + expect(() => Search.fromJSON({ + default: 'wikipedia', + engines: { + 'google': 'https://google.com/search?q={}', + 'yahoo': 'https://search.yahoo.com/search?p={}', + } + })).to.throw(TypeError); + expect(() => Search.fromJSON({ + default: 'g o o g l e', + engines: { + 'g o o g l e': 'https://google.com/search?q={}', + } + })).to.throw(TypeError); + expect(() => Search.fromJSON({ + default: 'google', + engines: { + 'google': 'https://google.com/search', + } + })).to.throw(TypeError); + expect(() => Search.fromJSON({ + default: 'google', + engines: { + 'google': 'https://google.com/search?q={}&r={}', + } + })).to.throw(TypeError); + }); +}); diff --git a/test/shared/settings/Settings.test.ts b/test/shared/settings/Settings.test.ts new file mode 100644 index 0000000..ab6af04 --- /dev/null +++ b/test/shared/settings/Settings.test.ts @@ -0,0 +1,54 @@ +import Settings from '../../../src/shared/settings/Settings'; +import { expect } from 'chai'; + +describe('Settings', () => { + describe('#valueOf', () => { + it('returns settings by valid settings', () => { + let x = Settings.fromJSON({ + keymaps: {}, + "search": { + "default": "google", + "engines": { + "google": "https://google.com/search?q={}", + } + }, + "properties": {}, + "blacklist": [] + }); + + expect({ + keymaps: x.keymaps.toJSON(), + search: x.search.toJSON(), + properties: x.properties.toJSON(), + blacklist: x.blacklist.toJSON(), + }).to.deep.equal({ + keymaps: {}, + search: { + default: "google", + engines: { + google: "https://google.com/search?q={}", + } + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [] + }); + }); + + it('sets default settings', () => { + let value = Settings.fromJSON({}); + expect(value.keymaps.toJSON()).to.not.be.empty; + expect(value.properties.toJSON()).to.not.be.empty; + expect(value.search.defaultEngine).to.be.a('string'); + expect(value.search.engines).to.be.an('object'); + expect(value.blacklist.toJSON()).to.be.empty; + }); + + it('throws a TypeError with an unknown field', () => { + expect(() => Settings.fromJSON({ name: 'alice' })).to.throw(TypeError) + }); + }); +}); diff --git a/test/shared/urls.test.ts b/test/shared/urls.test.ts index f2950b6..3a3eea6 100644 --- a/test/shared/urls.test.ts +++ b/test/shared/urls.test.ts @@ -1,14 +1,16 @@ -import * as parsers from 'shared/urls'; +import * as parsers from '../../src/shared/urls'; +import { expect } from 'chai'; +import Search from '../../src/shared/settings/Search'; describe("shared/commands/parsers", () => { describe('#searchUrl', () => { - const config = { + const config = Search.fromJSON({ default: 'google', engines: { google: 'https://google.com/search?q={}', yahoo: 'https://yahoo.com/search?q={}', } - }; + }); it('convertes search url', () => { expect(parsers.searchUrl('google.com', config)) diff --git a/test/shared/utils/re.test.ts b/test/shared/utils/re.test.ts deleted file mode 100644 index d12ceb7..0000000 --- a/test/shared/utils/re.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as re from 'shared/utils/re'; - -describe("re util", () => { - it('matches by pattern', () => { - let regex = re.fromWildcard('*.example.com/*'); - expect('foo.example.com/bar').to.match(regex); - expect('foo.example.com').not.to.match(regex); - expect('example.com/bar').not.to.match(regex); - - regex = re.fromWildcard('example.com/*') - expect('example.com/foo').to.match(regex); - expect('example.com/').to.match(regex); - - regex = re.fromWildcard('example.com/*bar') - expect('example.com/foobar').to.match(regex); - expect('example.com/bar').to.match(regex); - expect('example.com/foobarfoo').not.to.match(regex); - }) -}); |