diff options
author | Shin'ya Ueoka <ueokande@i-beam.org> | 2019-10-09 11:50:52 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-10-09 11:50:52 +0000 |
commit | 18c72bf15c6bc7e4c88dd06d38ff861f29d66b1b (patch) | |
tree | f46720349e17c57db7bbfc55241b12c4410f2773 /src | |
parent | 8eddcc1785a85bbe74be254d1055ebe5125dad10 (diff) | |
parent | 68f6211aac4177f3a70a40031dabbd1b61840071 (diff) |
Merge pull request #655 from ueokande/partial-blacklist
Partial blacklist
Diffstat (limited to 'src')
-rw-r--r-- | src/content/controllers/SettingController.ts | 3 | ||||
-rw-r--r-- | src/content/di.ts | 2 | ||||
-rw-r--r-- | src/content/repositories/AddressRepository.ts | 9 | ||||
-rw-r--r-- | src/content/usecases/KeymapUseCase.ts | 15 | ||||
-rw-r--r-- | src/settings/components/form/BlacklistForm.tsx | 52 | ||||
-rw-r--r-- | src/settings/components/form/PartialBlacklistForm.scss | 28 | ||||
-rw-r--r-- | src/settings/components/form/PartialBlacklistForm.tsx | 79 | ||||
-rw-r--r-- | src/settings/components/index.tsx | 16 | ||||
-rw-r--r-- | src/shared/settings/Blacklist.ts | 124 | ||||
-rw-r--r-- | src/shared/settings/KeySequence.ts | 2 |
10 files changed, 278 insertions, 52 deletions
diff --git a/src/content/controllers/SettingController.ts b/src/content/controllers/SettingController.ts index 06273a0..e1c7f01 100644 --- a/src/content/controllers/SettingController.ts +++ b/src/content/controllers/SettingController.ts @@ -15,7 +15,8 @@ export default class SettingController { async initSettings(): Promise<void> { try { let current = await this.settingUseCase.reload(); - let disabled = current.blacklist.includes(window.location.href); + let url = new URL(window.location.href); + let disabled = current.blacklist.includesEntireBlacklist(url); if (disabled) { this.addonEnabledUseCase.disable(); } else { diff --git a/src/content/di.ts b/src/content/di.ts index e18806a..63103a1 100644 --- a/src/content/di.ts +++ b/src/content/di.ts @@ -2,6 +2,7 @@ import { AddonEnabledRepositoryImpl } from './repositories/AddonEnabledRepository'; import { AddonIndicatorClientImpl } from './client/AddonIndicatorClient'; +import { AddressRepositoryImpl } from './repositories/AddressRepository'; import { ClipboardRepositoryImpl } from './repositories/ClipboardRepository'; import { ConsoleClientImpl } from './client/ConsoleClient'; import { ConsoleFramePresenterImpl } from './presenters/ConsoleFramePresenter'; @@ -31,6 +32,7 @@ import { container } from 'tsyringe'; container.register('FollowMasterClient', { useValue: new FollowMasterClientImpl(window.top) }); container.register('AddonEnabledRepository', { useClass: AddonEnabledRepositoryImpl }); container.register('AddonIndicatorClient', { useClass: AddonIndicatorClientImpl }); +container.register('AddressRepository', { useClass: AddressRepositoryImpl }); container.register('ClipboardRepository', { useClass: ClipboardRepositoryImpl }); container.register('ConsoleClient', { useClass: ConsoleClientImpl }); container.register('ConsoleFramePresenter', { useClass: ConsoleFramePresenterImpl }); diff --git a/src/content/repositories/AddressRepository.ts b/src/content/repositories/AddressRepository.ts new file mode 100644 index 0000000..6f9487b --- /dev/null +++ b/src/content/repositories/AddressRepository.ts @@ -0,0 +1,9 @@ +export default interface AddressRepository { + getCurrentURL(): URL +} + +export class AddressRepositoryImpl implements AddressRepository { + getCurrentURL(): URL { + return new URL(window.location.href); + } +} diff --git a/src/content/usecases/KeymapUseCase.ts b/src/content/usecases/KeymapUseCase.ts index 495f6d0..67d667d 100644 --- a/src/content/usecases/KeymapUseCase.ts +++ b/src/content/usecases/KeymapUseCase.ts @@ -6,6 +6,7 @@ import * as operations from '../../shared/operations'; import Keymaps from '../../shared/settings/Keymaps'; import Key from '../../shared/settings/Key'; import KeySequence from '../../shared/settings/KeySequence'; +import AddressRepository from '../repositories/AddressRepository'; type KeymapEntityMap = Map<KeySequence, operations.Operation>; @@ -25,11 +26,19 @@ export default class KeymapUseCase { @inject('AddonEnabledRepository') private addonEnabledRepository: AddonEnabledRepository, + + @inject('AddressRepository') + private addressRepository: AddressRepository, ) { } nextOp(key: Key): operations.Operation | null { let sequence = this.repository.enqueueKey(key); + if (sequence.length() === 1 && this.blacklistKey(key)) { + // ignore if the input starts with black list keys + this.repository.clear(); + return null; + } let keymaps = this.keymapEntityMap(); let matched = Array.from(keymaps.keys()).filter( @@ -71,4 +80,10 @@ export default class KeymapUseCase { ) as [KeySequence, operations.Operation][]; return new Map<KeySequence, operations.Operation>(entries); } + + private blacklistKey(key: Key): boolean { + let url = this.addressRepository.getCurrentURL(); + let blacklist = this.settingRepository.get().blacklist; + return blacklist.includeKey(url, key); + } } diff --git a/src/settings/components/form/BlacklistForm.tsx b/src/settings/components/form/BlacklistForm.tsx index f352e41..4e96cbf 100644 --- a/src/settings/components/form/BlacklistForm.tsx +++ b/src/settings/components/form/BlacklistForm.tsx @@ -2,17 +2,17 @@ import './BlacklistForm.scss'; import AddButton from '../ui/AddButton'; import DeleteButton from '../ui/DeleteButton'; import React from 'react'; -import { BlacklistJSON } from '../../../shared/settings/Blacklist'; +import Blacklist, { BlacklistItem } from '../../../shared/settings/Blacklist'; interface Props { - value: BlacklistJSON; - onChange: (value: BlacklistJSON) => void; + value: Blacklist; + onChange: (value: Blacklist) => void; onBlur: () => void; } class BlacklistForm extends React.Component<Props> { public static defaultProps: Props = { - value: [], + value: new Blacklist([]), onChange: () => {}, onBlur: () => {}, }; @@ -20,24 +20,22 @@ class BlacklistForm extends React.Component<Props> { render() { return <div className='form-blacklist-form'> { - 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>; - }) + this.props.value.items.map((item, index) => { + if (item.partial) { + return null; + } + return <div key={index} className='form-blacklist-form-row'> + <input data-index={index} type='text' name='url' + className='column-url' value={item.pattern} + 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)} /> @@ -47,17 +45,17 @@ 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.slice(); + let items = this.props.value.items; if (name === 'url') { - next[index] = e.target.value; + items[index] = new BlacklistItem(e.target.value, false, []); } else if (name === 'add') { - next.push(''); + items.push(new BlacklistItem('', false, [])); } else if (name === 'delete') { - next.splice(index, 1); + items.splice(index, 1); } - this.props.onChange(next); + this.props.onChange(new Blacklist(items)); if (name === 'delete') { this.props.onBlur(); } diff --git a/src/settings/components/form/PartialBlacklistForm.scss b/src/settings/components/form/PartialBlacklistForm.scss new file mode 100644 index 0000000..caf6f93 --- /dev/null +++ b/src/settings/components/form/PartialBlacklistForm.scss @@ -0,0 +1,28 @@ +.form-partial-blacklist-form { + @mixin row-base { + display: flex; + + .column-url { + flex: 5; + min-width: 0; + } + .column-keys { + flex: 1; + min-width: 0; + } + .column-delete { + flex: 1; + min-width: 0; + } + } + + &-header { + @include row-base; + + font-weight: bold; + } + + &-row { + @include row-base; + } +} diff --git a/src/settings/components/form/PartialBlacklistForm.tsx b/src/settings/components/form/PartialBlacklistForm.tsx new file mode 100644 index 0000000..0702913 --- /dev/null +++ b/src/settings/components/form/PartialBlacklistForm.tsx @@ -0,0 +1,79 @@ +import './PartialBlacklistForm.scss'; +import AddButton from '../ui/AddButton'; +import DeleteButton from '../ui/DeleteButton'; +import React from 'react'; +import Blacklist, { BlacklistItem } from '../../../shared/settings/Blacklist'; + +interface Props { + value: Blacklist; + onChange: (value: Blacklist) => void; + onBlur: () => void; +} + +class PartialBlacklistForm extends React.Component<Props> { + public static defaultProps: Props = { + value: new Blacklist([]), + onChange: () => {}, + onBlur: () => {}, + }; + + render() { + return <div className='form-partial-blacklist-form'> + <div className='form-partial-blacklist-form-header'> + <div className='column-url'>URL</div> + <div className='column-keys'>Keys</div> + </div> + { + this.props.value.items.map((item, index) => { + if (!item.partial) { + return null; + } + return <div key={index} className='form-partial-blacklist-form-row'> + <input data-index={index} type='text' name='url' + className='column-url' value={item.pattern} + onChange={this.bindValue.bind(this)} + onBlur={this.props.onBlur} + /> + <input data-index={index} type='text' name='keys' + className='column-keys' value={item.keys.join(',')} + 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)} /> + </div>; + } + + bindValue(e: any) { + let name = e.target.name; + let index = e.target.getAttribute('data-index'); + let items = this.props.value.items; + + if (name === 'url') { + let current = items[index]; + items[index] = new BlacklistItem(e.target.value, true, current.keys); + } else if (name === 'keys') { + let current = items[index]; + items[index] = new BlacklistItem( + current.pattern, true, e.target.value.split(',')); + } else if (name === 'add') { + items.push(new BlacklistItem('', true, [])); + } else if (name === 'delete') { + items.splice(index, 1); + } + + this.props.onChange(new Blacklist(items)); + if (name === 'delete') { + this.props.onBlur(); + } + } +} + +export default PartialBlacklistForm; diff --git a/src/settings/components/index.tsx b/src/settings/components/index.tsx index 160dd9c..3eb2dbe 100644 --- a/src/settings/components/index.tsx +++ b/src/settings/components/index.tsx @@ -6,6 +6,7 @@ import SearchForm from './form/SearchForm'; import KeymapsForm from './form/KeymapsForm'; import BlacklistForm from './form/BlacklistForm'; import PropertiesForm from './form/PropertiesForm'; +import PartialBlacklistForm from './form/PartialBlacklistForm'; import * as settingActions from '../../settings/actions/setting'; import SettingData, { FormKeymaps, FormSearch, FormSettings, JSONTextSettings, @@ -53,7 +54,15 @@ class SettingsComponent extends React.Component<Props> { <fieldset> <legend>Blacklist</legend> <BlacklistForm - value={form.blacklist.toJSON()} + value={form.blacklist} + onChange={this.bindBlacklistForm.bind(this)} + onBlur={this.save.bind(this)} + /> + </fieldset> + <fieldset> + <legend>Partial blacklist</legend> + <PartialBlacklistForm + value={form.blacklist} onChange={this.bindBlacklistForm.bind(this)} onBlur={this.save.bind(this)} /> @@ -138,11 +147,10 @@ class SettingsComponent extends React.Component<Props> { this.props.dispatch(settingActions.set(data)); } - bindBlacklistForm(value: any) { + bindBlacklistForm(blacklist: Blacklist) { let data = new SettingData({ source: this.props.source, - form: (this.props.form as FormSettings).buildWithBlacklist( - Blacklist.fromJSON(value)), + form: (this.props.form as FormSettings).buildWithBlacklist(blacklist), }); this.props.dispatch(settingActions.set(data)); } diff --git a/src/shared/settings/Blacklist.ts b/src/shared/settings/Blacklist.ts index a95b606..0cfbd71 100644 --- a/src/shared/settings/Blacklist.ts +++ b/src/shared/settings/Blacklist.ts @@ -1,39 +1,125 @@ -export type BlacklistJSON = string[]; +import Key from './Key'; -const fromWildcard = (pattern: string): RegExp => { +export type BlacklistItemJSON = string | { + url: string, + keys: string[], +}; + +export type BlacklistJSON = BlacklistItemJSON[]; + +const regexFromWildcard = (pattern: string): RegExp => { let regexStr = '^' + pattern.replace(/\*/g, '.*') + '$'; return new RegExp(regexStr); }; +const isArrayOfString = (raw: any): boolean => { + if (!Array.isArray(raw)) { + return false; + } + for (let x of Array.from(raw)) { + if (typeof x !== 'string') { + return false; + } + } + return true; +}; + +export class BlacklistItem { + public readonly pattern: string; + + private regex: RegExp; + + public readonly partial: boolean; + + public readonly keys: string[]; + + private readonly keyEntities: Key[]; + + constructor( + pattern: string, + partial: boolean, + keys: string[] + ) { + this.pattern = pattern; + this.regex = regexFromWildcard(pattern); + this.partial = partial; + this.keys = keys; + this.keyEntities = this.keys.map(Key.fromMapKey); + } + + static fromJSON(raw: any): BlacklistItem { + if (typeof raw === 'string') { + return new BlacklistItem(raw, false, []); + } else if (typeof raw === 'object' && raw !== null) { + if (!('url' in raw)) { + throw new TypeError( + `missing field "url" of blacklist item: ${JSON.stringify(raw)}`); + } + if (typeof raw.url !== 'string') { + throw new TypeError( + `invalid field "url" of blacklist item: ${JSON.stringify(raw)}`); + } + if (!('keys' in raw)) { + throw new TypeError( + `missing field "keys" of blacklist item: ${JSON.stringify(raw)}`); + } + if (!isArrayOfString(raw.keys)) { + throw new TypeError( + `invalid field "keys" of blacklist item: ${JSON.stringify(raw)}`); + } + return new BlacklistItem(raw.url as string, true, raw.keys as string[]); + } + throw new TypeError( + `invalid format of blacklist item: ${JSON.stringify(raw)}`); + } + + toJSON(): BlacklistItemJSON { + if (!this.partial) { + return this.pattern; + } + return { url: this.pattern, keys: this.keys }; + } + + matches(url: URL): boolean { + return this.pattern.includes('/') + ? this.regex.test(url.host + url.pathname) + : this.regex.test(url.host); + } + + includeKey(url: URL, key: Key): boolean { + if (!this.matches(url)) { + return false; + } + if (!this.partial) { + return true; + } + return this.keyEntities.some(k => k.equals(key)); + } +} + export default class Blacklist { constructor( - private blacklist: string[], + public readonly items: BlacklistItem[], ) { } static fromJSON(json: any): Blacklist { if (!Array.isArray(json)) { - throw new TypeError(`"blacklist" is not an array of string`); + throw new TypeError('blacklist is not an array: ' + JSON.stringify(json)); } - for (let x of json) { - if (typeof x !== 'string') { - throw new TypeError(`"blacklist" is not an array of string`); - } - } - return new Blacklist(json); + let items = Array.from(json).map(item => BlacklistItem.fromJSON(item)); + return new Blacklist(items); } toJSON(): BlacklistJSON { - return this.blacklist; + return this.items.map(item => item.toJSON()); } - 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); - }); + includesEntireBlacklist(url: URL): boolean { + return this.items.some(item => !item.partial && item.matches(url)); + } + + includeKey(url: URL, key: Key) { + return this.items.some(item => item.includeKey(url, key)); } } diff --git a/src/shared/settings/KeySequence.ts b/src/shared/settings/KeySequence.ts index 4955583..abae61a 100644 --- a/src/shared/settings/KeySequence.ts +++ b/src/shared/settings/KeySequence.ts @@ -1,4 +1,4 @@ -import Key from '../../shared/settings/Key'; +import Key from './Key'; export default class KeySequence { constructor( |