commit
f5dfdb0bd7
25 changed files with 691 additions and 201 deletions
79
src/background/actions/command.js
Normal file
79
src/background/actions/command.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
import actions from '../actions';
|
||||
import * as tabs from 'background/tabs';
|
||||
import * as parsers from 'shared/commands/parsers';
|
||||
import * as properties from 'shared/settings/properties';
|
||||
|
||||
const openCommand = (url) => {
|
||||
return browser.tabs.query({
|
||||
active: true, currentWindow: true
|
||||
}).then((gotTabs) => {
|
||||
if (gotTabs.length > 0) {
|
||||
return browser.tabs.update(gotTabs[0].id, { url: url });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const tabopenCommand = (url) => {
|
||||
return browser.tabs.create({ url: url });
|
||||
};
|
||||
|
||||
const winopenCommand = (url) => {
|
||||
return browser.windows.create({ url });
|
||||
};
|
||||
|
||||
const bufferCommand = (keywords) => {
|
||||
if (keywords.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
let keywordsStr = keywords.join(' ');
|
||||
return browser.tabs.query({
|
||||
active: true, currentWindow: true
|
||||
}).then((gotTabs) => {
|
||||
if (gotTabs.length > 0) {
|
||||
if (isNaN(keywordsStr)) {
|
||||
return tabs.selectByKeyword(gotTabs[0], keywordsStr);
|
||||
}
|
||||
let index = parseInt(keywordsStr, 10) - 1;
|
||||
return tabs.selectAt(index);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setCommand = (args) => {
|
||||
if (!args[0]) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let [name, value] = parsers.parseSetOption(args[0], properties.types);
|
||||
return {
|
||||
type: actions.SETTING_SET_PROPERTY,
|
||||
name,
|
||||
value
|
||||
};
|
||||
};
|
||||
|
||||
const exec = (line, settings) => {
|
||||
let [name, args] = parsers.parseCommandLine(line);
|
||||
|
||||
switch (name) {
|
||||
case 'o':
|
||||
case 'open':
|
||||
return openCommand(parsers.normalizeUrl(args, settings.search));
|
||||
case 't':
|
||||
case 'tabopen':
|
||||
return tabopenCommand(parsers.normalizeUrl(args, settings.search));
|
||||
case 'w':
|
||||
case 'winopen':
|
||||
return winopenCommand(parsers.normalizeUrl(args, settings.search));
|
||||
case 'b':
|
||||
case 'buffer':
|
||||
return bufferCommand(args);
|
||||
case 'set':
|
||||
return setCommand(args);
|
||||
case '':
|
||||
return Promise.resolve();
|
||||
}
|
||||
throw new Error(name + ' command is not defined');
|
||||
};
|
||||
|
||||
export { exec };
|
5
src/background/actions/index.js
Normal file
5
src/background/actions/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
// Settings
|
||||
SETTING_SET_SETTINGS: 'setting.set.settings',
|
||||
SETTING_SET_PROPERTY: 'setting.set.property',
|
||||
};
|
21
src/background/actions/setting.js
Normal file
21
src/background/actions/setting.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import actions from '../actions';
|
||||
import * as settingsStorage from 'shared/settings/storage';
|
||||
|
||||
const load = () => {
|
||||
return settingsStorage.loadValue().then((value) => {
|
||||
return {
|
||||
type: actions.SETTING_SET_SETTINGS,
|
||||
value,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const setProperty = (name, value) => {
|
||||
return {
|
||||
type: actions.SETTING_SET_PROPERTY,
|
||||
name,
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
export { load, setProperty };
|
|
@ -1,6 +1,7 @@
|
|||
import messages from 'shared/messages';
|
||||
import * as operationActions from 'background/actions/operation';
|
||||
import * as settingsActions from 'settings/actions/setting';
|
||||
import * as commandActions from 'background/actions/command';
|
||||
import * as settingActions from 'background/actions/setting';
|
||||
import * as tabActions from 'background/actions/tab';
|
||||
import * as commands from 'shared/commands';
|
||||
|
||||
|
@ -35,18 +36,17 @@ export default class BackgroundComponent {
|
|||
return this.store.dispatch(
|
||||
tabActions.openToTab(message.url, sender.tab), sender);
|
||||
case messages.CONSOLE_ENTER_COMMAND:
|
||||
return commands.exec(message.text, settings.value).catch((e) => {
|
||||
return browser.tabs.sendMessage(sender.tab.id, {
|
||||
type: messages.CONSOLE_SHOW_ERROR,
|
||||
text: e.message,
|
||||
});
|
||||
});
|
||||
this.store.dispatch(
|
||||
commandActions.exec(message.text, settings.value),
|
||||
sender
|
||||
);
|
||||
return this.broadcastSettingsChanged();
|
||||
case messages.SETTINGS_QUERY:
|
||||
return Promise.resolve(this.store.getState().setting.value);
|
||||
case messages.CONSOLE_QUERY_COMPLETIONS:
|
||||
return commands.complete(message.text, settings.value);
|
||||
case messages.SETTINGS_RELOAD:
|
||||
this.store.dispatch(settingsActions.load());
|
||||
this.store.dispatch(settingActions.load());
|
||||
return this.broadcastSettingsChanged();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as settingsActions from 'settings/actions/setting';
|
||||
import * as settingActions from 'background/actions/setting';
|
||||
import messages from 'shared/messages';
|
||||
import BackgroundComponent from 'background/components/background';
|
||||
import reducers from 'background/reducers';
|
||||
|
@ -16,4 +16,4 @@ const store = createStore(reducers, (e, sender) => {
|
|||
// eslint-disable-next-line no-unused-vars
|
||||
const backgroundComponent = new BackgroundComponent(store);
|
||||
|
||||
store.dispatch(settingsActions.load());
|
||||
store.dispatch(settingActions.load());
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import settingReducer from 'settings/reducers/setting';
|
||||
import settingReducer from './setting';
|
||||
|
||||
// Make setting reducer instead of re-use
|
||||
const defaultState = {
|
||||
|
|
24
src/background/reducers/setting.js
Normal file
24
src/background/reducers/setting.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import actions from 'background/actions';
|
||||
|
||||
const defaultState = {
|
||||
value: {},
|
||||
};
|
||||
|
||||
export default function reducer(state = defaultState, action = {}) {
|
||||
switch (action.type) {
|
||||
case actions.SETTING_SET_SETTINGS:
|
||||
return {
|
||||
value: action.value,
|
||||
};
|
||||
case actions.SETTING_SET_PROPERTY:
|
||||
return {
|
||||
value: Object.assign({}, state.value, {
|
||||
properties: Object.assign({}, state.value.properties,
|
||||
{ [action.name]: action.value })
|
||||
})
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +1,23 @@
|
|||
import actions from 'settings/actions';
|
||||
import messages from 'shared/messages';
|
||||
import DefaultSettings from 'shared/settings/default';
|
||||
import * as settingsStorage from 'shared/settings/storage';
|
||||
import * as settingsValues from 'shared/settings/values';
|
||||
|
||||
const load = () => {
|
||||
return browser.storage.local.get('settings').then(({ settings }) => {
|
||||
if (!settings) {
|
||||
return set(DefaultSettings);
|
||||
}
|
||||
return set(Object.assign({}, DefaultSettings, settings));
|
||||
}, console.error);
|
||||
return settingsStorage.loadRaw().then((settings) => {
|
||||
return set(settings);
|
||||
});
|
||||
};
|
||||
|
||||
const save = (settings) => {
|
||||
return browser.storage.local.set({
|
||||
settings,
|
||||
}).then(() => {
|
||||
return settingsStorage.save(settings).then(() => {
|
||||
return browser.runtime.sendMessage({
|
||||
type: messages.SETTINGS_RELOAD
|
||||
});
|
||||
}).then(() => {
|
||||
return set(settings);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const set = (settings) => {
|
||||
|
|
60
src/settings/components/form/properties-form.jsx
Normal file
60
src/settings/components/form/properties-form.jsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import './properties-form.scss';
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
class PropertiesForm extends Component {
|
||||
|
||||
render() {
|
||||
let types = this.props.types;
|
||||
let value = this.props.value;
|
||||
if (!value) {
|
||||
value = {};
|
||||
}
|
||||
|
||||
return <div className='form-properties-form'>
|
||||
{
|
||||
Object.keys(types).map((name) => {
|
||||
let type = types[name];
|
||||
let inputType = null;
|
||||
if (type === 'string') {
|
||||
inputType = 'text';
|
||||
} else if (type === 'number') {
|
||||
inputType = 'number';
|
||||
} else if (type === 'boolean') {
|
||||
inputType = 'checkbox';
|
||||
}
|
||||
return <div key={name} className='form-properties-form-row'>
|
||||
<label>
|
||||
<span className='column-name'>{name}</span>
|
||||
<input type={inputType} name={name}
|
||||
className='column-input'
|
||||
value={value[name] ? value[name] : ''}
|
||||
onChange={this.bindValue.bind(this)}
|
||||
checked={value[name]}
|
||||
/>
|
||||
</label>
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
|
||||
bindValue(e) {
|
||||
if (!this.props.onChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
let name = e.target.name;
|
||||
let next = Object.assign({}, this.props.value);
|
||||
if (e.target.type.toLowerCase() === 'checkbox') {
|
||||
next[name] = e.target.checked;
|
||||
} else if (e.target.type.toLowerCase() === 'number') {
|
||||
next[name] = Number(e.target.value);
|
||||
} else {
|
||||
next[name] = e.target.value;
|
||||
}
|
||||
|
||||
this.props.onChange(next);
|
||||
}
|
||||
}
|
||||
|
||||
export default PropertiesForm;
|
12
src/settings/components/form/properties-form.scss
Normal file
12
src/settings/components/form/properties-form.scss
Normal file
|
@ -0,0 +1,12 @@
|
|||
.form-properties-form {
|
||||
&-row {
|
||||
.column-name {
|
||||
display: inline-block;
|
||||
min-width: 5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.column-input {
|
||||
line-height: 2.2rem;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,8 +4,10 @@ import Input from './ui/input';
|
|||
import SearchForm from './form/search-form';
|
||||
import KeymapsForm from './form/keymaps-form';
|
||||
import BlacklistForm from './form/blacklist-form';
|
||||
import PropertiesForm from './form/properties-form';
|
||||
import * as properties from 'shared/settings/properties';
|
||||
import * as settingActions from 'settings/actions/setting';
|
||||
import * as validator from 'shared/validators/setting';
|
||||
import * as validator from 'shared/settings/validator';
|
||||
import * as settingsValues from 'shared/settings/values';
|
||||
|
||||
const DO_YOU_WANT_TO_CONTINUE =
|
||||
|
@ -65,6 +67,14 @@ class SettingsComponent extends Component {
|
|||
onChange={value => this.bindForm('blacklist', value)}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Properties</legend>
|
||||
<PropertiesForm
|
||||
types={properties.types}
|
||||
value={this.state.settings.form.properties}
|
||||
onChange={value => this.bindForm('properties', value)}
|
||||
/>
|
||||
</fieldset>
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,169 +0,0 @@
|
|||
import * as tabs from 'background/tabs';
|
||||
import * as histories from 'background/histories';
|
||||
|
||||
const normalizeUrl = (args, searchConfig) => {
|
||||
let concat = args.join(' ');
|
||||
try {
|
||||
return new URL(concat).href;
|
||||
} catch (e) {
|
||||
if (concat.includes('.') && !concat.includes(' ')) {
|
||||
return 'http://' + concat;
|
||||
}
|
||||
let query = concat;
|
||||
let template = searchConfig.engines[
|
||||
searchConfig.default
|
||||
];
|
||||
for (let key in searchConfig.engines) {
|
||||
if (args[0] === key) {
|
||||
query = args.slice(1).join(' ');
|
||||
template = searchConfig.engines[key];
|
||||
}
|
||||
}
|
||||
return template.replace('{}', encodeURIComponent(query));
|
||||
}
|
||||
};
|
||||
|
||||
const openCommand = (url) => {
|
||||
return browser.tabs.query({
|
||||
active: true, currentWindow: true
|
||||
}).then((gotTabs) => {
|
||||
if (gotTabs.length > 0) {
|
||||
return browser.tabs.update(gotTabs[0].id, { url: url });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const tabopenCommand = (url) => {
|
||||
return browser.tabs.create({ url: url });
|
||||
};
|
||||
|
||||
const winopenCommand = (url) => {
|
||||
return browser.windows.create({ url });
|
||||
};
|
||||
|
||||
const bufferCommand = (keywords) => {
|
||||
if (keywords.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
let keywordsStr = keywords.join(' ');
|
||||
return browser.tabs.query({
|
||||
active: true, currentWindow: true
|
||||
}).then((gotTabs) => {
|
||||
if (gotTabs.length > 0) {
|
||||
if (isNaN(keywordsStr)) {
|
||||
return tabs.selectByKeyword(gotTabs[0], keywordsStr);
|
||||
}
|
||||
let index = parseInt(keywordsStr, 10) - 1;
|
||||
return tabs.selectAt(index);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getOpenCompletions = (command, keywords, searchConfig) => {
|
||||
return histories.getCompletions(keywords).then((pages) => {
|
||||
let historyItems = pages.map((page) => {
|
||||
return {
|
||||
caption: page.title,
|
||||
content: command + ' ' + page.url,
|
||||
url: page.url
|
||||
};
|
||||
});
|
||||
let engineNames = Object.keys(searchConfig.engines);
|
||||
let engineItems = engineNames.filter(name => name.startsWith(keywords))
|
||||
.map(name => ({
|
||||
caption: name,
|
||||
content: command + ' ' + name
|
||||
}));
|
||||
|
||||
let completions = [];
|
||||
if (engineItems.length > 0) {
|
||||
completions.push({
|
||||
name: 'Search Engines',
|
||||
items: engineItems
|
||||
});
|
||||
}
|
||||
if (historyItems.length > 0) {
|
||||
completions.push({
|
||||
name: 'History',
|
||||
items: historyItems
|
||||
});
|
||||
}
|
||||
return completions;
|
||||
});
|
||||
};
|
||||
|
||||
const doCommand = (line, settings) => {
|
||||
let words = line.trim().split(/ +/);
|
||||
let name = words.shift();
|
||||
|
||||
switch (name) {
|
||||
case 'o':
|
||||
case 'open':
|
||||
return openCommand(normalizeUrl(words, settings.search));
|
||||
case 't':
|
||||
case 'tabopen':
|
||||
return tabopenCommand(normalizeUrl(words, settings.search));
|
||||
case 'w':
|
||||
case 'winopen':
|
||||
return winopenCommand(normalizeUrl(words, settings.search));
|
||||
case 'b':
|
||||
case 'buffer':
|
||||
return bufferCommand(words);
|
||||
case '':
|
||||
return Promise.resolve();
|
||||
}
|
||||
throw new Error(name + ' command is not defined');
|
||||
};
|
||||
|
||||
const getCompletions = (line, settings) => {
|
||||
let typedWords = line.trim().split(/ +/);
|
||||
let typing = '';
|
||||
if (!line.endsWith(' ')) {
|
||||
typing = typedWords.pop();
|
||||
}
|
||||
|
||||
if (typedWords.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
let name = typedWords.shift();
|
||||
let keywords = typedWords.concat(typing).join(' ');
|
||||
|
||||
switch (name) {
|
||||
case 'o':
|
||||
case 'open':
|
||||
case 't':
|
||||
case 'tabopen':
|
||||
case 'w':
|
||||
case 'winopen':
|
||||
return getOpenCompletions(name, keywords, settings.search);
|
||||
case 'b':
|
||||
case 'buffer':
|
||||
return tabs.getCompletions(keywords).then((gotTabs) => {
|
||||
let items = gotTabs.map((tab) => {
|
||||
return {
|
||||
caption: tab.title,
|
||||
content: name + ' ' + tab.title,
|
||||
url: tab.url,
|
||||
icon: tab.favIconUrl
|
||||
};
|
||||
});
|
||||
return [
|
||||
{
|
||||
name: 'Buffers',
|
||||
items: items
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
};
|
||||
|
||||
const exec = (line, settings) => {
|
||||
return doCommand(line, settings);
|
||||
};
|
||||
|
||||
const complete = (line, settings) => {
|
||||
return getCompletions(line, settings);
|
||||
};
|
||||
|
||||
export { exec, complete };
|
84
src/shared/commands/complete.js
Normal file
84
src/shared/commands/complete.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
import * as tabs from 'background/tabs';
|
||||
import * as histories from 'background/histories';
|
||||
|
||||
const getOpenCompletions = (command, keywords, searchConfig) => {
|
||||
return histories.getCompletions(keywords).then((pages) => {
|
||||
let historyItems = pages.map((page) => {
|
||||
return {
|
||||
caption: page.title,
|
||||
content: command + ' ' + page.url,
|
||||
url: page.url
|
||||
};
|
||||
});
|
||||
let engineNames = Object.keys(searchConfig.engines);
|
||||
let engineItems = engineNames.filter(name => name.startsWith(keywords))
|
||||
.map(name => ({
|
||||
caption: name,
|
||||
content: command + ' ' + name
|
||||
}));
|
||||
|
||||
let completions = [];
|
||||
if (engineItems.length > 0) {
|
||||
completions.push({
|
||||
name: 'Search Engines',
|
||||
items: engineItems
|
||||
});
|
||||
}
|
||||
if (historyItems.length > 0) {
|
||||
completions.push({
|
||||
name: 'History',
|
||||
items: historyItems
|
||||
});
|
||||
}
|
||||
return completions;
|
||||
});
|
||||
};
|
||||
|
||||
const getCompletions = (line, settings) => {
|
||||
let typedWords = line.trim().split(/ +/);
|
||||
let typing = '';
|
||||
if (!line.endsWith(' ')) {
|
||||
typing = typedWords.pop();
|
||||
}
|
||||
|
||||
if (typedWords.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
let name = typedWords.shift();
|
||||
let keywords = typedWords.concat(typing).join(' ');
|
||||
|
||||
switch (name) {
|
||||
case 'o':
|
||||
case 'open':
|
||||
case 't':
|
||||
case 'tabopen':
|
||||
case 'w':
|
||||
case 'winopen':
|
||||
return getOpenCompletions(name, keywords, settings.search);
|
||||
case 'b':
|
||||
case 'buffer':
|
||||
return tabs.getCompletions(keywords).then((gotTabs) => {
|
||||
let items = gotTabs.map((tab) => {
|
||||
return {
|
||||
caption: tab.title,
|
||||
content: name + ' ' + tab.title,
|
||||
url: tab.url,
|
||||
icon: tab.favIconUrl
|
||||
};
|
||||
});
|
||||
return [
|
||||
{
|
||||
name: 'Buffers',
|
||||
items: items
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
};
|
||||
|
||||
const complete = (line, settings) => {
|
||||
return getCompletions(line, settings);
|
||||
};
|
||||
|
||||
export default complete;
|
3
src/shared/commands/index.js
Normal file
3
src/shared/commands/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import complete from './complete';
|
||||
|
||||
export { complete };
|
59
src/shared/commands/parsers.js
Normal file
59
src/shared/commands/parsers.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
const normalizeUrl = (args, searchConfig) => {
|
||||
let concat = args.join(' ');
|
||||
try {
|
||||
return new URL(concat).href;
|
||||
} catch (e) {
|
||||
if (concat.includes('.') && !concat.includes(' ')) {
|
||||
return 'http://' + concat;
|
||||
}
|
||||
let query = concat;
|
||||
let template = searchConfig.engines[
|
||||
searchConfig.default
|
||||
];
|
||||
for (let key in searchConfig.engines) {
|
||||
if (args[0] === key) {
|
||||
query = args.slice(1).join(' ');
|
||||
template = searchConfig.engines[key];
|
||||
}
|
||||
}
|
||||
return template.replace('{}', encodeURIComponent(query));
|
||||
}
|
||||
};
|
||||
|
||||
const mustNumber = (v) => {
|
||||
let num = Number(v);
|
||||
if (isNaN(num)) {
|
||||
throw new Error('Not number: ' + v);
|
||||
}
|
||||
return num;
|
||||
};
|
||||
|
||||
const parseSetOption = (word, types) => {
|
||||
let [key, value] = word.split('=');
|
||||
if (value === undefined) {
|
||||
value = !key.startsWith('no');
|
||||
key = value ? key : key.slice(2);
|
||||
}
|
||||
let type = types[key];
|
||||
if (!type) {
|
||||
throw new Error('Unknown property: ' + key);
|
||||
}
|
||||
if (type === 'boolean' && typeof value !== 'boolean' ||
|
||||
type !== 'boolean' && typeof value === 'boolean') {
|
||||
throw new Error('Invalid argument: ' + word);
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'string': return [key, value];
|
||||
case 'number': return [key, mustNumber(value)];
|
||||
case 'boolean': return [key, value];
|
||||
}
|
||||
};
|
||||
|
||||
const parseCommandLine = (line) => {
|
||||
let words = line.trim().split(/ +/);
|
||||
let name = words.shift();
|
||||
return [name, words];
|
||||
};
|
||||
|
||||
export { normalizeUrl, parseCommandLine, parseSetOption };
|
|
@ -58,6 +58,8 @@ export default {
|
|||
"duckduckgo": "https://duckduckgo.com/?q={}",
|
||||
"twitter": "https://twitter.com/search?q={}",
|
||||
"wikipedia": "https://en.wikipedia.org/w/index.php?search={}"
|
||||
},
|
||||
"properties": {
|
||||
}
|
||||
}
|
||||
}`,
|
||||
|
|
15
src/shared/settings/properties.js
Normal file
15
src/shared/settings/properties.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
const types = {
|
||||
// TODO describe property types here
|
||||
// mystr: 'string',
|
||||
// mynum: 'number',
|
||||
// mybool: 'boolean',
|
||||
};
|
||||
|
||||
const defaults = {
|
||||
// TODO describe property defaults values
|
||||
// mystr: 'hello',
|
||||
// mynum: 123,
|
||||
// mybool: true,
|
||||
};
|
||||
|
||||
export { types, defaults };
|
31
src/shared/settings/storage.js
Normal file
31
src/shared/settings/storage.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import DefaultSettings from './default';
|
||||
import * as settingsValues from './values';
|
||||
|
||||
const loadRaw = () => {
|
||||
return browser.storage.local.get('settings').then(({ settings }) => {
|
||||
if (!settings) {
|
||||
return DefaultSettings;
|
||||
}
|
||||
return Object.assign({}, DefaultSettings, settings);
|
||||
});
|
||||
};
|
||||
|
||||
const loadValue = () => {
|
||||
return loadRaw().then((settings) => {
|
||||
let value = JSON.parse(DefaultSettings.json);
|
||||
if (settings.source === 'json') {
|
||||
value = settingsValues.valueFromJson(settings.json);
|
||||
} else if (settings.source === 'form') {
|
||||
value = settingsValues.valueFromForm(settings.form);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
};
|
||||
|
||||
const save = (settings) => {
|
||||
return browser.storage.local.set({
|
||||
settings,
|
||||
});
|
||||
};
|
||||
|
||||
export { loadRaw, loadValue, save };
|
|
@ -1,6 +1,7 @@
|
|||
import operations from 'shared/operations';
|
||||
import * as properties from './properties';
|
||||
|
||||
const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist'];
|
||||
const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist', 'properties'];
|
||||
const VALID_OPERATION_VALUES = Object.keys(operations).map((key) => {
|
||||
return operations[key];
|
||||
});
|
||||
|
@ -48,6 +49,17 @@ const validateSearch = (search) => {
|
|||
}
|
||||
};
|
||||
|
||||
const validateProperties = (props) => {
|
||||
for (let name of Object.keys(props)) {
|
||||
if (!properties.types[name]) {
|
||||
throw new Error(`Unknown property name: "${name}"`);
|
||||
}
|
||||
if (typeof props[name] !== properties.types[name]) {
|
||||
throw new Error(`Invalid type for property: "${name}"`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validate = (settings) => {
|
||||
validateInvalidTopKeys(settings);
|
||||
if (settings.keymaps) {
|
||||
|
@ -56,6 +68,9 @@ const validate = (settings) => {
|
|||
if (settings.search) {
|
||||
validateSearch(settings.search);
|
||||
}
|
||||
if (settings.properties) {
|
||||
validateProperties(settings.properties);
|
||||
}
|
||||
};
|
||||
|
||||
export { validate };
|
|
@ -1,3 +1,5 @@
|
|||
import * as properties from './properties';
|
||||
|
||||
const operationFromFormName = (name) => {
|
||||
let [type, argStr] = name.split('?');
|
||||
let args = {};
|
||||
|
@ -44,9 +46,12 @@ const valueFromForm = (form) => {
|
|||
}
|
||||
}
|
||||
|
||||
let blacklist = form.blacklist;
|
||||
|
||||
return { keymaps, search, blacklist };
|
||||
return {
|
||||
keymaps,
|
||||
search,
|
||||
blacklist: form.blacklist,
|
||||
properties: form.properties
|
||||
};
|
||||
};
|
||||
|
||||
const jsonFromValue = (value) => {
|
||||
|
@ -78,9 +83,14 @@ const formFromValue = (value, allowedOps) => {
|
|||
}
|
||||
}
|
||||
|
||||
let blacklist = value.blacklist;
|
||||
let formProperties = Object.assign({}, properties.defaults, value.properties);
|
||||
|
||||
return { keymaps, search, blacklist };
|
||||
return {
|
||||
keymaps,
|
||||
search,
|
||||
blacklist: value.blacklist,
|
||||
properties: formProperties,
|
||||
};
|
||||
};
|
||||
|
||||
const jsonFromForm = (form) => {
|
||||
|
|
37
test/background/reducers/setting.test.js
Normal file
37
test/background/reducers/setting.test.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { expect } from "chai";
|
||||
import actions from 'background/actions';
|
||||
import settingReducer from 'background/reducers/setting';
|
||||
|
||||
describe("setting reducer", () => {
|
||||
it('return the initial state', () => {
|
||||
let state = settingReducer(undefined, {});
|
||||
expect(state).to.have.deep.property('value', {});
|
||||
});
|
||||
|
||||
it('return next state for SETTING_SET_SETTINGS', () => {
|
||||
let action = {
|
||||
type: actions.SETTING_SET_SETTINGS,
|
||||
value: { key: 123 },
|
||||
};
|
||||
let state = settingReducer(undefined, action);
|
||||
expect(state).to.have.deep.property('value', { key: 123 });
|
||||
});
|
||||
|
||||
it('return next state for SETTING_SET_PROPERTY', () => {
|
||||
let state = {
|
||||
value: {
|
||||
properties: { smoothscroll: true }
|
||||
}
|
||||
}
|
||||
let action = {
|
||||
type: actions.SETTING_SET_PROPERTY,
|
||||
name: 'encoding',
|
||||
value: 'utf-8',
|
||||
};
|
||||
state = settingReducer(state, action);
|
||||
|
||||
console.log(state);
|
||||
expect(state.value.properties).to.have.property('smoothscroll', true);
|
||||
expect(state.value.properties).to.have.property('encoding', 'utf-8');
|
||||
});
|
||||
});
|
86
test/settings/components/form/properties-form.test.jsx
Normal file
86
test/settings/components/form/properties-form.test.jsx
Normal file
|
@ -0,0 +1,86 @@
|
|||
import { expect } from 'chai';
|
||||
import { h, render } from 'preact';
|
||||
import PropertiesForm from 'settings/components/form/properties-form'
|
||||
|
||||
describe("settings/form/PropertiesForm", () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
describe('render', () => {
|
||||
it('renders PropertiesForm', () => {
|
||||
let types = {
|
||||
mystr: 'string',
|
||||
mynum: 'number',
|
||||
mybool: 'boolean',
|
||||
empty: 'string',
|
||||
}
|
||||
let value = {
|
||||
mystr: 'abc',
|
||||
mynum: 123,
|
||||
mybool: true,
|
||||
};
|
||||
render(<PropertiesForm types={types} value={value} />, document.body);
|
||||
|
||||
let strInput = document.querySelector('input[name=mystr]');
|
||||
let numInput = document.querySelector('input[name=mynum]');
|
||||
let boolInput = document.querySelector('input[name=mybool]');
|
||||
let emptyInput = document.querySelector('input[name=empty]');
|
||||
|
||||
expect(strInput.type).to.equals('text');
|
||||
expect(strInput.value).to.equal('abc');
|
||||
expect(numInput.type).to.equals('number');
|
||||
expect(numInput.value).to.equal('123');
|
||||
expect(boolInput.type).to.equals('checkbox');
|
||||
expect(boolInput.checked).to.be.true;
|
||||
expect(emptyInput.type).to.equals('text');
|
||||
expect(emptyInput.value).to.be.empty;
|
||||
});
|
||||
});
|
||||
|
||||
describe('onChange', () => {
|
||||
it('invokes onChange event on text changed', (done) => {
|
||||
render(<PropertiesForm
|
||||
types={{ 'myvalue': 'string' }}
|
||||
value={{ 'myvalue': 'abc' }}
|
||||
onChange={value => {
|
||||
expect(value).to.have.property('myvalue', 'abcd');
|
||||
done();
|
||||
}}
|
||||
/>, document.body);
|
||||
|
||||
let input = document.querySelector('input[name=myvalue]');
|
||||
input.value = 'abcd'
|
||||
input.dispatchEvent(new Event('change'))
|
||||
});
|
||||
|
||||
it('invokes onChange event on number changeed', (done) => {
|
||||
render(<PropertiesForm
|
||||
types={{ 'myvalue': 'number' }}
|
||||
value={{ '': 123 }}
|
||||
onChange={value => {
|
||||
expect(value).to.have.property('myvalue', 1234);
|
||||
done();
|
||||
}}
|
||||
/>, document.body);
|
||||
|
||||
let input = document.querySelector('input[name=myvalue]');
|
||||
input.value = '1234'
|
||||
input.dispatchEvent(new Event('change'))
|
||||
});
|
||||
|
||||
it('invokes onChange event on checkbox changed', (done) => {
|
||||
render(<PropertiesForm
|
||||
types={{ 'myvalue': 'boolean' }}
|
||||
value={{ 'myvalue': false }}
|
||||
onChange={value => {
|
||||
expect(value).to.have.property('myvalue', true);
|
||||
done();
|
||||
}}
|
||||
/>, document.body);
|
||||
|
||||
let input = document.querySelector('input[name=myvalue]');
|
||||
input.click();
|
||||
});
|
||||
});
|
||||
});
|
85
test/shared/commands/parsers.test.js
Normal file
85
test/shared/commands/parsers.test.js
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { expect } from "chai";
|
||||
import * as parsers from 'shared/commands/parsers';
|
||||
|
||||
describe("shared/commands/parsers", () => {
|
||||
describe("#parsers.parseSetOption", () => {
|
||||
it('parse set string', () => {
|
||||
let [key, value] = parsers.parseSetOption('encoding=utf-8', { encoding: 'string' });
|
||||
expect(key).to.equal('encoding');
|
||||
expect(value).to.equal('utf-8');
|
||||
});
|
||||
|
||||
it('parse set empty string', () => {
|
||||
let [key, value] = parsers.parseSetOption('encoding=', { encoding: 'string' });
|
||||
expect(key).to.equal('encoding');
|
||||
expect(value).to.equal('');
|
||||
});
|
||||
|
||||
it('parse set string', () => {
|
||||
let [key, value] = parsers.parseSetOption('history=50', { history: 'number' });
|
||||
expect(key).to.equal('history');
|
||||
expect(value).to.equal(50);
|
||||
});
|
||||
|
||||
it('parse set boolean', () => {
|
||||
let [key, value] = parsers.parseSetOption('paste', { paste: 'boolean' });
|
||||
expect(key).to.equal('paste');
|
||||
expect(value).to.be.true;
|
||||
|
||||
[key, value] = parsers.parseSetOption('nopaste', { paste: 'boolean' });
|
||||
expect(key).to.equal('paste');
|
||||
expect(value).to.be.false;
|
||||
});
|
||||
|
||||
it('throws error on unknown property', () => {
|
||||
expect(() => parsers.parseSetOption('charset=utf-8', {})).to.throw(Error, 'Unknown');
|
||||
expect(() => parsers.parseSetOption('smoothscroll', {})).to.throw(Error, 'Unknown');
|
||||
expect(() => parsers.parseSetOption('nosmoothscroll', {})).to.throw(Error, 'Unknown');
|
||||
})
|
||||
|
||||
it('throws error on invalid property', () => {
|
||||
expect(() => parsers.parseSetOption('charset=utf-8', { charset: 'number' })).to.throw(Error, 'Not number');
|
||||
expect(() => parsers.parseSetOption('charset=utf-8', { charset: 'boolean' })).to.throw(Error, 'Invalid');
|
||||
expect(() => parsers.parseSetOption('charset=', { charset: 'boolean' })).to.throw(Error, 'Invalid');
|
||||
expect(() => parsers.parseSetOption('smoothscroll', { smoothscroll: 'string' })).to.throw(Error, 'Invalid');
|
||||
expect(() => parsers.parseSetOption('smoothscroll', { smoothscroll: 'number' })).to.throw(Error, 'Invalid');
|
||||
})
|
||||
});
|
||||
|
||||
describe('#normalizeUrl', () => {
|
||||
const config = {
|
||||
default: 'google',
|
||||
engines: {
|
||||
google: 'https://google.com/search?q={}',
|
||||
yahoo: 'https://yahoo.com/search?q={}',
|
||||
}
|
||||
};
|
||||
|
||||
it('convertes search url', () => {
|
||||
expect(parsers.normalizeUrl(['google', 'apple'], config))
|
||||
.to.equal('https://google.com/search?q=apple');
|
||||
expect(parsers.normalizeUrl(['yahoo', 'apple'], config))
|
||||
.to.equal('https://yahoo.com/search?q=apple');
|
||||
expect(parsers.normalizeUrl(['google', 'apple', 'banana'], config))
|
||||
.to.equal('https://google.com/search?q=apple%20banana');
|
||||
expect(parsers.normalizeUrl(['yahoo', 'C++CLI'], config))
|
||||
.to.equal('https://yahoo.com/search?q=C%2B%2BCLI');
|
||||
});
|
||||
|
||||
it('user default search engine', () => {
|
||||
expect(parsers.normalizeUrl(['apple', 'banana'], config))
|
||||
.to.equal('https://google.com/search?q=apple%20banana');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#parseCommandLine', () => {
|
||||
it('parse command line as name and args', () => {
|
||||
expect(parsers.parseCommandLine('open google apple')).to.deep.equal(['open', ['google', 'apple']]);
|
||||
expect(parsers.parseCommandLine(' open google apple ')).to.deep.equal(['open', ['google', 'apple']]);
|
||||
expect(parsers.parseCommandLine('')).to.deep.equal(['', []]);
|
||||
expect(parsers.parseCommandLine(' ')).to.deep.equal(['', []]);
|
||||
expect(parsers.parseCommandLine('exit')).to.deep.equal(['exit', []]);
|
||||
expect(parsers.parseCommandLine(' exit ')).to.deep.equal(['exit', []]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
import { expect } from "chai";
|
||||
import { validate } from 'shared/validators/setting';
|
||||
import { validate } from 'shared/settings/validator';
|
||||
|
||||
describe("setting validator", () => {
|
||||
describe("unknown top keys", () => {
|
|
@ -7,13 +7,21 @@ describe("settings values", () => {
|
|||
let json = `{
|
||||
"keymaps": { "0": {"type": "scroll.home"}},
|
||||
"search": { "default": "google", "engines": { "google": "https://google.com/search?q={}" }},
|
||||
"blacklist": [ "*.slack.com"]
|
||||
"blacklist": [ "*.slack.com"],
|
||||
"properties": {
|
||||
"mystr": "value",
|
||||
"mynum": 123,
|
||||
"mybool": true
|
||||
}
|
||||
}`;
|
||||
let value = values.valueFromJson(json);
|
||||
|
||||
expect(value.keymaps).to.deep.equal({ 0: {type: "scroll.home"}});
|
||||
expect(value.search).to.deep.equal({ default: "google", engines: { google: "https://google.com/search?q={}"} });
|
||||
expect(value.blacklist).to.deep.equal(["*.slack.com"]);
|
||||
expect(value.properties).to.have.property('mystr', 'value');
|
||||
expect(value.properties).to.have.property('mynum', 123);
|
||||
expect(value.properties).to.have.property('mybool', true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -29,6 +37,11 @@ describe("settings values", () => {
|
|||
engines: [['google', 'https://google.com/search?q={}']],
|
||||
},
|
||||
blacklist: ['*.slack.com'],
|
||||
"properties": {
|
||||
"mystr": "value",
|
||||
"mynum": 123,
|
||||
"mybool": true,
|
||||
}
|
||||
};
|
||||
let value = values.valueFromForm(form);
|
||||
|
||||
|
@ -37,6 +50,9 @@ describe("settings values", () => {
|
|||
expect(JSON.stringify(value.search)).to.deep.equal(JSON.stringify({ default: "google", engines: { google: "https://google.com/search?q={}"} }));
|
||||
expect(value.search).to.deep.equal({ default: "google", engines: { google: "https://google.com/search?q={}"} });
|
||||
expect(value.blacklist).to.deep.equal(["*.slack.com"]);
|
||||
expect(value.properties).to.have.property('mystr', 'value');
|
||||
expect(value.properties).to.have.property('mynum', 123);
|
||||
expect(value.properties).to.have.property('mybool', true);
|
||||
});
|
||||
|
||||
it('convert from empty form', () => {
|
||||
|
@ -45,6 +61,7 @@ describe("settings values", () => {
|
|||
expect(value).to.not.have.key('keymaps');
|
||||
expect(value).to.not.have.key('search');
|
||||
expect(value).to.not.have.key('blacklist');
|
||||
expect(value).to.not.have.key('properties');
|
||||
});
|
||||
|
||||
it('override keymaps', () => {
|
||||
|
@ -96,7 +113,12 @@ describe("settings values", () => {
|
|||
0: { type: 'scroll.home' },
|
||||
},
|
||||
search: { default: 'google', engines: { google: 'https://google.com/search?q={}' }},
|
||||
blacklist: [ '*.slack.com']
|
||||
blacklist: [ '*.slack.com'],
|
||||
properties: {
|
||||
"mystr": "value",
|
||||
"mynum": 123,
|
||||
"mybool": true,
|
||||
}
|
||||
};
|
||||
let allowed = ['scroll.vertically?{"count":1}', 'scroll.home' ];
|
||||
let form = values.formFromValue(value, allowed);
|
||||
|
@ -109,6 +131,9 @@ describe("settings values", () => {
|
|||
expect(form.search).to.have.deep.property('engines', [['google', 'https://google.com/search?q={}']]);
|
||||
expect(form.blacklist).to.have.lengthOf(1);
|
||||
expect(form.blacklist).to.include('*.slack.com');
|
||||
expect(form.properties).to.have.property('mystr', 'value');
|
||||
expect(form.properties).to.have.property('mynum', 123);
|
||||
expect(form.properties).to.have.property('mybool', true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Reference in a new issue