aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorShin'ya Ueoka <ueokande@i-beam.org>2019-10-09 11:50:52 +0000
committerGitHub <noreply@github.com>2019-10-09 11:50:52 +0000
commit18c72bf15c6bc7e4c88dd06d38ff861f29d66b1b (patch)
treef46720349e17c57db7bbfc55241b12c4410f2773 /src
parent8eddcc1785a85bbe74be254d1055ebe5125dad10 (diff)
parent68f6211aac4177f3a70a40031dabbd1b61840071 (diff)
Merge pull request #655 from ueokande/partial-blacklist
Partial blacklist
Diffstat (limited to 'src')
-rw-r--r--src/content/controllers/SettingController.ts3
-rw-r--r--src/content/di.ts2
-rw-r--r--src/content/repositories/AddressRepository.ts9
-rw-r--r--src/content/usecases/KeymapUseCase.ts15
-rw-r--r--src/settings/components/form/BlacklistForm.tsx52
-rw-r--r--src/settings/components/form/PartialBlacklistForm.scss28
-rw-r--r--src/settings/components/form/PartialBlacklistForm.tsx79
-rw-r--r--src/settings/components/index.tsx16
-rw-r--r--src/shared/settings/Blacklist.ts124
-rw-r--r--src/shared/settings/KeySequence.ts2
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(