diff options
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( | 
