From fbdec04786e28bad45021bef4a74e7077e34282f Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Thu, 4 Jan 2018 18:55:24 +0900 Subject: move settings validator --- src/settings/components/index.jsx | 2 +- src/shared/settings/validator.js | 61 +++++++++++++++++++++++++++++++++++++++ src/shared/validators/setting.js | 61 --------------------------------------- 3 files changed, 62 insertions(+), 62 deletions(-) create mode 100644 src/shared/settings/validator.js delete mode 100644 src/shared/validators/setting.js (limited to 'src') diff --git a/src/settings/components/index.jsx b/src/settings/components/index.jsx index 73520ca..074c4c7 100644 --- a/src/settings/components/index.jsx +++ b/src/settings/components/index.jsx @@ -5,7 +5,7 @@ import SearchForm from './form/search-form'; import KeymapsForm from './form/keymaps-form'; import BlacklistForm from './form/blacklist-form'; 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 = diff --git a/src/shared/settings/validator.js b/src/shared/settings/validator.js new file mode 100644 index 0000000..949ab29 --- /dev/null +++ b/src/shared/settings/validator.js @@ -0,0 +1,61 @@ +import operations from 'shared/operations'; + +const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist']; +const VALID_OPERATION_VALUES = Object.keys(operations).map((key) => { + return operations[key]; +}); + +const validateInvalidTopKeys = (settings) => { + let invalidKey = Object.keys(settings).find((key) => { + return !VALID_TOP_KEYS.includes(key); + }); + if (invalidKey) { + throw Error(`Unknown key: "${invalidKey}"`); + } +}; + +const validateKeymaps = (keymaps) => { + for (let key of Object.keys(keymaps)) { + let value = keymaps[key]; + if (!VALID_OPERATION_VALUES.includes(value.type)) { + throw Error(`Unknown operation: "${value.type}"`); + } + } +}; + +const validateSearch = (search) => { + let engines = search.engines; + for (let key of Object.keys(engines)) { + if (/\s/.test(key)) { + throw new Error( + `While space in search engine name is not allowed: "${key}"` + ); + } + let url = engines[key]; + if (!url.match(/{}/)) { + throw new Error(`No {}-placeholders in URL of "${key}"`); + } + if (url.match(/{}/g).length > 1) { + throw new Error(`Multiple {}-placeholders in URL of "${key}"`); + } + } + + if (!search.default) { + throw new Error(`Default engine is not set`); + } + if (!Object.keys(engines).includes(search.default)) { + throw new Error(`Default engine "${search.default}" not found`); + } +}; + +const validate = (settings) => { + validateInvalidTopKeys(settings); + if (settings.keymaps) { + validateKeymaps(settings.keymaps); + } + if (settings.search) { + validateSearch(settings.search); + } +}; + +export { validate }; diff --git a/src/shared/validators/setting.js b/src/shared/validators/setting.js deleted file mode 100644 index 949ab29..0000000 --- a/src/shared/validators/setting.js +++ /dev/null @@ -1,61 +0,0 @@ -import operations from 'shared/operations'; - -const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist']; -const VALID_OPERATION_VALUES = Object.keys(operations).map((key) => { - return operations[key]; -}); - -const validateInvalidTopKeys = (settings) => { - let invalidKey = Object.keys(settings).find((key) => { - return !VALID_TOP_KEYS.includes(key); - }); - if (invalidKey) { - throw Error(`Unknown key: "${invalidKey}"`); - } -}; - -const validateKeymaps = (keymaps) => { - for (let key of Object.keys(keymaps)) { - let value = keymaps[key]; - if (!VALID_OPERATION_VALUES.includes(value.type)) { - throw Error(`Unknown operation: "${value.type}"`); - } - } -}; - -const validateSearch = (search) => { - let engines = search.engines; - for (let key of Object.keys(engines)) { - if (/\s/.test(key)) { - throw new Error( - `While space in search engine name is not allowed: "${key}"` - ); - } - let url = engines[key]; - if (!url.match(/{}/)) { - throw new Error(`No {}-placeholders in URL of "${key}"`); - } - if (url.match(/{}/g).length > 1) { - throw new Error(`Multiple {}-placeholders in URL of "${key}"`); - } - } - - if (!search.default) { - throw new Error(`Default engine is not set`); - } - if (!Object.keys(engines).includes(search.default)) { - throw new Error(`Default engine "${search.default}" not found`); - } -}; - -const validate = (settings) => { - validateInvalidTopKeys(settings); - if (settings.keymaps) { - validateKeymaps(settings.keymaps); - } - if (settings.search) { - validateSearch(settings.search); - } -}; - -export { validate }; -- cgit v1.2.3 From e19f89f16248ed4bf28deaa9ab4b5204061df5eb Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Thu, 4 Jan 2018 20:34:33 +0900 Subject: add property to settings --- src/shared/settings/property-types.js | 6 ++++++ src/shared/settings/validator.js | 17 ++++++++++++++++- src/shared/settings/values.js | 18 ++++++++++++------ test/shared/settings/values.test.js | 29 +++++++++++++++++++++++++++-- 4 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 src/shared/settings/property-types.js (limited to 'src') diff --git a/src/shared/settings/property-types.js b/src/shared/settings/property-types.js new file mode 100644 index 0000000..bcfa809 --- /dev/null +++ b/src/shared/settings/property-types.js @@ -0,0 +1,6 @@ +export default { + // TODO describe property types here + // mystr: 'string', + // mynum: 'number', + // mybool: 'boolean', +}; diff --git a/src/shared/settings/validator.js b/src/shared/settings/validator.js index 949ab29..6fadac7 100644 --- a/src/shared/settings/validator.js +++ b/src/shared/settings/validator.js @@ -1,6 +1,7 @@ import operations from 'shared/operations'; +import propertyTypes from './property-types'; -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 = (properties) => { + for (let name of Object.keys(properties)) { + if (!propertyTypes[name]) { + throw new Error(`Unknown property name: "${name}"`); + } + if (typeof properties[name] !== propertyTypes[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 }; diff --git a/src/shared/settings/values.js b/src/shared/settings/values.js index 4e55fa0..5027ba5 100644 --- a/src/shared/settings/values.js +++ b/src/shared/settings/values.js @@ -44,9 +44,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 +81,12 @@ const formFromValue = (value, allowedOps) => { } } - let blacklist = value.blacklist; - - return { keymaps, search, blacklist }; + return { + keymaps, + search, + blacklist: value.blacklist, + properties: value.properties, + }; }; const jsonFromForm = (form) => { diff --git a/test/shared/settings/values.test.js b/test/shared/settings/values.test.js index 2632cd7..62cfb5f 100644 --- a/test/shared/settings/values.test.js +++ b/test/shared/settings/values.test.js @@ -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); }); }); }); -- cgit v1.2.3 From 86df54067f8a105264a6816e115fd65efa75fb5b Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Fri, 5 Jan 2018 22:54:36 +0900 Subject: Add PropertiesForm --- src/settings/components/form/properties-form.jsx | 60 +++++++++++++++ src/settings/components/form/properties-form.scss | 12 +++ src/settings/components/index.jsx | 10 +++ .../components/form/properties-form.test.jsx | 86 ++++++++++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 src/settings/components/form/properties-form.jsx create mode 100644 src/settings/components/form/properties-form.scss create mode 100644 test/settings/components/form/properties-form.test.jsx (limited to 'src') diff --git a/src/settings/components/form/properties-form.jsx b/src/settings/components/form/properties-form.jsx new file mode 100644 index 0000000..55c8512 --- /dev/null +++ b/src/settings/components/form/properties-form.jsx @@ -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
+ { + 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
+ +
; + }) + } +
; + } + + 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; diff --git a/src/settings/components/form/properties-form.scss b/src/settings/components/form/properties-form.scss new file mode 100644 index 0000000..7c9e167 --- /dev/null +++ b/src/settings/components/form/properties-form.scss @@ -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; + } + } +} diff --git a/src/settings/components/index.jsx b/src/settings/components/index.jsx index 074c4c7..c41aa6b 100644 --- a/src/settings/components/index.jsx +++ b/src/settings/components/index.jsx @@ -4,6 +4,8 @@ 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 PropertyTypes from 'shared/settings/property-types'; import * as settingActions from 'settings/actions/setting'; import * as validator from 'shared/settings/validator'; import * as settingsValues from 'shared/settings/values'; @@ -65,6 +67,14 @@ class SettingsComponent extends Component { onChange={value => this.bindForm('blacklist', value)} /> +
+ Properties + this.bindForm('properties', value)} + /> +
; } diff --git a/test/settings/components/form/properties-form.test.jsx b/test/settings/components/form/properties-form.test.jsx new file mode 100644 index 0000000..4807361 --- /dev/null +++ b/test/settings/components/form/properties-form.test.jsx @@ -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(, 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( { + 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( { + 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( { + expect(value).to.have.property('myvalue', true); + done(); + }} + />, document.body); + + let input = document.querySelector('input[name=myvalue]'); + input.click(); + }); + }); +}); -- cgit v1.2.3 From f4499b840c73c8872c69a53ef7a4c2b9dca977a6 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Mon, 8 Jan 2018 13:54:18 +0900 Subject: Set default properties --- src/shared/settings/default.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/shared/settings/default.js b/src/shared/settings/default.js index d187565..6425354 100644 --- a/src/shared/settings/default.js +++ b/src/shared/settings/default.js @@ -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": { } } }`, -- cgit v1.2.3 From fe48dce1c9b6f003c669cb19542063c8ac0c91ba Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Mon, 8 Jan 2018 16:25:09 +0900 Subject: default property values --- src/settings/components/index.jsx | 4 ++-- src/shared/settings/properties.js | 15 +++++++++++++++ src/shared/settings/property-types.js | 6 ------ src/shared/settings/validator.js | 5 ++--- src/shared/settings/values.js | 6 +++++- 5 files changed, 24 insertions(+), 12 deletions(-) create mode 100644 src/shared/settings/properties.js delete mode 100644 src/shared/settings/property-types.js (limited to 'src') diff --git a/src/settings/components/index.jsx b/src/settings/components/index.jsx index c41aa6b..d7696a1 100644 --- a/src/settings/components/index.jsx +++ b/src/settings/components/index.jsx @@ -5,7 +5,7 @@ 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 PropertyTypes from 'shared/settings/property-types'; +import * as properties from 'shared/settings/properties'; import * as settingActions from 'settings/actions/setting'; import * as validator from 'shared/settings/validator'; import * as settingsValues from 'shared/settings/values'; @@ -70,7 +70,7 @@ class SettingsComponent extends Component {
Properties this.bindForm('properties', value)} /> diff --git a/src/shared/settings/properties.js b/src/shared/settings/properties.js new file mode 100644 index 0000000..ff8039b --- /dev/null +++ b/src/shared/settings/properties.js @@ -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 }; diff --git a/src/shared/settings/property-types.js b/src/shared/settings/property-types.js deleted file mode 100644 index bcfa809..0000000 --- a/src/shared/settings/property-types.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - // TODO describe property types here - // mystr: 'string', - // mynum: 'number', - // mybool: 'boolean', -}; diff --git a/src/shared/settings/validator.js b/src/shared/settings/validator.js index 6fadac7..744f63d 100644 --- a/src/shared/settings/validator.js +++ b/src/shared/settings/validator.js @@ -1,5 +1,4 @@ import operations from 'shared/operations'; -import propertyTypes from './property-types'; const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist', 'properties']; const VALID_OPERATION_VALUES = Object.keys(operations).map((key) => { @@ -51,10 +50,10 @@ const validateSearch = (search) => { const validateProperties = (properties) => { for (let name of Object.keys(properties)) { - if (!propertyTypes[name]) { + if (!properties.types[name]) { throw new Error(`Unknown property name: "${name}"`); } - if (typeof properties[name] !== propertyTypes[name]) { + if (typeof properties[name] !== properties.types[name]) { throw new Error(`Invalid type for property: "${name}"`); } } diff --git a/src/shared/settings/values.js b/src/shared/settings/values.js index 5027ba5..bd03be2 100644 --- a/src/shared/settings/values.js +++ b/src/shared/settings/values.js @@ -1,3 +1,5 @@ +import * as properties from './properties'; + const operationFromFormName = (name) => { let [type, argStr] = name.split('?'); let args = {}; @@ -81,11 +83,13 @@ const formFromValue = (value, allowedOps) => { } } + let formProperties = Object.assign({}, properties.defaults, value.properties); + return { keymaps, search, blacklist: value.blacklist, - properties: value.properties, + properties: formProperties, }; }; -- cgit v1.2.3 From 5c449df9b6f084d4e71627b1434154ebd7c30bc6 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Mon, 8 Jan 2018 16:08:55 +0900 Subject: separate command --- src/shared/commands.js | 169 ---------------------------------------- src/shared/commands/complete.js | 84 ++++++++++++++++++++ src/shared/commands/exec.js | 85 ++++++++++++++++++++ src/shared/commands/index.js | 4 + 4 files changed, 173 insertions(+), 169 deletions(-) delete mode 100644 src/shared/commands.js create mode 100644 src/shared/commands/complete.js create mode 100644 src/shared/commands/exec.js create mode 100644 src/shared/commands/index.js (limited to 'src') diff --git a/src/shared/commands.js b/src/shared/commands.js deleted file mode 100644 index ed64a63..0000000 --- a/src/shared/commands.js +++ /dev/null @@ -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 }; diff --git a/src/shared/commands/complete.js b/src/shared/commands/complete.js new file mode 100644 index 0000000..0bdbab8 --- /dev/null +++ b/src/shared/commands/complete.js @@ -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; diff --git a/src/shared/commands/exec.js b/src/shared/commands/exec.js new file mode 100644 index 0000000..7248827 --- /dev/null +++ b/src/shared/commands/exec.js @@ -0,0 +1,85 @@ +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 exec = (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'); +}; + +export default exec; diff --git a/src/shared/commands/index.js b/src/shared/commands/index.js new file mode 100644 index 0000000..c2cea3e --- /dev/null +++ b/src/shared/commands/index.js @@ -0,0 +1,4 @@ +import exec from './exec'; +import complete from './complete'; + +export { exec, complete }; -- cgit v1.2.3 From 3dbdcdba074f020f88bd4d1f5554d49549973ccd Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Mon, 8 Jan 2018 18:37:29 +0900 Subject: property parser --- src/shared/commands/properties.js | 31 ++++++++++++++++++++++++++ test/shared/commands/property.test.js | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/shared/commands/properties.js create mode 100644 test/shared/commands/property.test.js (limited to 'src') diff --git a/src/shared/commands/properties.js b/src/shared/commands/properties.js new file mode 100644 index 0000000..8a3213d --- /dev/null +++ b/src/shared/commands/properties.js @@ -0,0 +1,31 @@ +const mustNumber = (v) => { + let num = Number(v); + if (isNaN(num)) { + throw new Error('Not number: ' + v); + } + return num; +}; + +const parseProperty = (word, types) => { + let [key, value] = word.split('='); + if (!value) { + 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]; + } +}; + +export { parseProperty }; diff --git a/test/shared/commands/property.test.js b/test/shared/commands/property.test.js new file mode 100644 index 0000000..d949482 --- /dev/null +++ b/test/shared/commands/property.test.js @@ -0,0 +1,41 @@ +import { expect } from "chai"; +import { parseProperty } from 'shared/commands/properties'; + +describe("shared/commands/properties", () => { + describe("#parseProperty", () => { + it('parse set string', () => { + let [key, value] = parseProperty('encoding=utf-8', { encoding: 'string' }); + expect(key).to.equal('encoding'); + expect(value).to.equal('utf-8'); + }); + + it('parse set string', () => { + let [key, value] = parseProperty('history=50', { history: 'number' }); + expect(key).to.equal('history'); + expect(value).to.equal(50); + }); + + it('parse set boolean', () => { + let [key, value] = parseProperty('paste', { paste: 'boolean' }); + expect(key).to.equal('paste'); + expect(value).to.be.true; + + [key, value] = parseProperty('nopaste', { paste: 'boolean' }); + expect(key).to.equal('paste'); + expect(value).to.be.false; + }); + + it('throws error on unknown property', () => { + expect(() => parseProperty('charset=utf-8', {})).to.throw(Error, 'Unknown'); + expect(() => parseProperty('smoothscroll', {})).to.throw(Error, 'Unknown'); + expect(() => parseProperty('nosmoothscroll', {})).to.throw(Error, 'Unknown'); + }) + + it('throws error on invalid property', () => { + expect(() => parseProperty('charset=utf-8', { charset: 'number' })).to.throw(Error, 'Not number'); + expect(() => parseProperty('charset=utf-8', { charset: 'boolean' })).to.throw(Error, 'Invalid'); + expect(() => parseProperty('smoothscroll', { smoothscroll: 'string' })).to.throw(Error, 'Invalid'); + expect(() => parseProperty('smoothscroll', { smoothscroll: 'number' })).to.throw(Error, 'Invalid'); + }) + }); +}); -- cgit v1.2.3 From d23c190cad81ec2109f69b4afa957194d029caaf Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Mon, 8 Jan 2018 21:40:12 +0900 Subject: default properties --- src/shared/settings/validator.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/shared/settings/validator.js b/src/shared/settings/validator.js index 744f63d..1589420 100644 --- a/src/shared/settings/validator.js +++ b/src/shared/settings/validator.js @@ -1,4 +1,5 @@ import operations from 'shared/operations'; +import * as properties from './properties'; const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist', 'properties']; const VALID_OPERATION_VALUES = Object.keys(operations).map((key) => { @@ -48,12 +49,12 @@ const validateSearch = (search) => { } }; -const validateProperties = (properties) => { - for (let name of Object.keys(properties)) { +const validateProperties = (props) => { + for (let name of Object.keys(props)) { if (!properties.types[name]) { throw new Error(`Unknown property name: "${name}"`); } - if (typeof properties[name] !== properties.types[name]) { + if (typeof props[name] !== properties.types[name]) { throw new Error(`Invalid type for property: "${name}"`); } } -- cgit v1.2.3 From 6083e70ea089fa2683741a1118be0e4e6b76f858 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Mon, 8 Jan 2018 21:54:16 +0900 Subject: separate setting actions and reducers --- src/background/actions/index.js | 4 ++++ src/background/actions/setting.js | 13 +++++++++++++ src/background/components/background.js | 4 ++-- src/background/index.js | 4 ++-- src/background/reducers/index.js | 2 +- src/background/reducers/setting.js | 17 +++++++++++++++++ src/settings/actions/setting.js | 18 +++++++----------- src/shared/settings/storage.js | 31 +++++++++++++++++++++++++++++++ test/background/reducers/setting.test.js | 19 +++++++++++++++++++ 9 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 src/background/actions/index.js create mode 100644 src/background/actions/setting.js create mode 100644 src/background/reducers/setting.js create mode 100644 src/shared/settings/storage.js create mode 100644 test/background/reducers/setting.test.js (limited to 'src') diff --git a/src/background/actions/index.js b/src/background/actions/index.js new file mode 100644 index 0000000..8c212c2 --- /dev/null +++ b/src/background/actions/index.js @@ -0,0 +1,4 @@ +export default { + // Settings + SETTING_SET_SETTINGS: 'setting.set.settings', +}; diff --git a/src/background/actions/setting.js b/src/background/actions/setting.js new file mode 100644 index 0000000..0454a68 --- /dev/null +++ b/src/background/actions/setting.js @@ -0,0 +1,13 @@ +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, + }; + }); +}; + +export { load }; diff --git a/src/background/components/background.js b/src/background/components/background.js index 2d94310..22c6693 100644 --- a/src/background/components/background.js +++ b/src/background/components/background.js @@ -1,6 +1,6 @@ import messages from 'shared/messages'; import * as operationActions from 'background/actions/operation'; -import * as settingsActions from 'settings/actions/setting'; +import * as settingActions from 'background/actions/setting'; import * as tabActions from 'background/actions/tab'; import * as commands from 'shared/commands'; @@ -46,7 +46,7 @@ export default class BackgroundComponent { 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(); } } diff --git a/src/background/index.js b/src/background/index.js index 8a68767..3ef712f 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -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()); diff --git a/src/background/reducers/index.js b/src/background/reducers/index.js index 4be8fac..dab0c62 100644 --- a/src/background/reducers/index.js +++ b/src/background/reducers/index.js @@ -1,4 +1,4 @@ -import settingReducer from 'settings/reducers/setting'; +import settingReducer from './setting'; // Make setting reducer instead of re-use const defaultState = { diff --git a/src/background/reducers/setting.js b/src/background/reducers/setting.js new file mode 100644 index 0000000..70bf8ea --- /dev/null +++ b/src/background/reducers/setting.js @@ -0,0 +1,17 @@ +import actions from 'settings/actions'; + +const defaultState = { + value: {}, +}; + +export default function reducer(state = defaultState, action = {}) { + switch (action.type) { + case actions.SETTING_SET_SETTINGS: + return { + value: action.value, + }; + default: + return state; + } +} + diff --git a/src/settings/actions/setting.js b/src/settings/actions/setting.js index 1d01fda..92c9f8a 100644 --- a/src/settings/actions/setting.js +++ b/src/settings/actions/setting.js @@ -1,26 +1,22 @@ 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); }); + }).then(() => { + return set(settings); }); }; diff --git a/src/shared/settings/storage.js b/src/shared/settings/storage.js new file mode 100644 index 0000000..1edb441 --- /dev/null +++ b/src/shared/settings/storage.js @@ -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 }; diff --git a/test/background/reducers/setting.test.js b/test/background/reducers/setting.test.js new file mode 100644 index 0000000..a6a5573 --- /dev/null +++ b/test/background/reducers/setting.test.js @@ -0,0 +1,19 @@ +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 }); + }); +}); -- cgit v1.2.3 From 22c34a0a6f9721fb9d907ab10de91cbbc40d6bbe Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Wed, 10 Jan 2018 21:34:40 +0900 Subject: add set property action in background --- src/background/actions/index.js | 1 + src/background/actions/setting.js | 10 +++++++++- src/background/reducers/setting.js | 9 ++++++++- test/background/reducers/setting.test.js | 18 ++++++++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/background/actions/index.js b/src/background/actions/index.js index 8c212c2..efe4074 100644 --- a/src/background/actions/index.js +++ b/src/background/actions/index.js @@ -1,4 +1,5 @@ export default { // Settings SETTING_SET_SETTINGS: 'setting.set.settings', + SETTING_SET_PROPERTY: 'setting.set.property', }; diff --git a/src/background/actions/setting.js b/src/background/actions/setting.js index 0454a68..773142f 100644 --- a/src/background/actions/setting.js +++ b/src/background/actions/setting.js @@ -10,4 +10,12 @@ const load = () => { }); }; -export { load }; +const setProperty = (name, value) => { + return { + type: actions.SETTING_SET_PROPERTY, + name, + value, + }; +}; + +export { load, setProperty }; diff --git a/src/background/reducers/setting.js b/src/background/reducers/setting.js index 70bf8ea..045a654 100644 --- a/src/background/reducers/setting.js +++ b/src/background/reducers/setting.js @@ -1,4 +1,4 @@ -import actions from 'settings/actions'; +import actions from 'background/actions'; const defaultState = { value: {}, @@ -10,6 +10,13 @@ export default function reducer(state = defaultState, action = {}) { 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; } diff --git a/test/background/reducers/setting.test.js b/test/background/reducers/setting.test.js index a6a5573..2ef98cb 100644 --- a/test/background/reducers/setting.test.js +++ b/test/background/reducers/setting.test.js @@ -16,4 +16,22 @@ describe("setting reducer", () => { 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'); + }); }); -- cgit v1.2.3 From dda4e7475cdd092d00441c7cd0ceb194ee5dee3d Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Thu, 11 Jan 2018 20:07:25 +0900 Subject: move commands to background action --- src/background/actions/command.js | 62 ++++++++++++++++++++++++ src/background/components/background.js | 3 +- src/shared/commands/exec.js | 85 --------------------------------- src/shared/commands/index.js | 3 +- src/shared/commands/parsers.js | 59 +++++++++++++++++++++++ src/shared/commands/properties.js | 31 ------------ test/shared/commands/parsers.test.js | 78 ++++++++++++++++++++++++++++++ test/shared/commands/property.test.js | 41 ---------------- 8 files changed, 202 insertions(+), 160 deletions(-) create mode 100644 src/background/actions/command.js delete mode 100644 src/shared/commands/exec.js create mode 100644 src/shared/commands/parsers.js delete mode 100644 src/shared/commands/properties.js create mode 100644 test/shared/commands/parsers.test.js delete mode 100644 test/shared/commands/property.test.js (limited to 'src') diff --git a/src/background/actions/command.js b/src/background/actions/command.js new file mode 100644 index 0000000..f11c61b --- /dev/null +++ b/src/background/actions/command.js @@ -0,0 +1,62 @@ +import * as tabs from 'background/tabs'; +import * as parsers from 'shared/commands/parsers'; + +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 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 '': + return Promise.resolve(); + } + throw new Error(name + ' command is not defined'); +}; + +export { exec }; diff --git a/src/background/components/background.js b/src/background/components/background.js index 22c6693..19bf27f 100644 --- a/src/background/components/background.js +++ b/src/background/components/background.js @@ -1,5 +1,6 @@ import messages from 'shared/messages'; import * as operationActions from 'background/actions/operation'; +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,7 +36,7 @@ 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 commandActions.exec(message.text, settings.value).catch((e) => { return browser.tabs.sendMessage(sender.tab.id, { type: messages.CONSOLE_SHOW_ERROR, text: e.message, diff --git a/src/shared/commands/exec.js b/src/shared/commands/exec.js deleted file mode 100644 index 7248827..0000000 --- a/src/shared/commands/exec.js +++ /dev/null @@ -1,85 +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 exec = (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'); -}; - -export default exec; diff --git a/src/shared/commands/index.js b/src/shared/commands/index.js index c2cea3e..78cb4df 100644 --- a/src/shared/commands/index.js +++ b/src/shared/commands/index.js @@ -1,4 +1,3 @@ -import exec from './exec'; import complete from './complete'; -export { exec, complete }; +export { complete }; diff --git a/src/shared/commands/parsers.js b/src/shared/commands/parsers.js new file mode 100644 index 0000000..af51338 --- /dev/null +++ b/src/shared/commands/parsers.js @@ -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) { + 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 }; diff --git a/src/shared/commands/properties.js b/src/shared/commands/properties.js deleted file mode 100644 index 8a3213d..0000000 --- a/src/shared/commands/properties.js +++ /dev/null @@ -1,31 +0,0 @@ -const mustNumber = (v) => { - let num = Number(v); - if (isNaN(num)) { - throw new Error('Not number: ' + v); - } - return num; -}; - -const parseProperty = (word, types) => { - let [key, value] = word.split('='); - if (!value) { - 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]; - } -}; - -export { parseProperty }; diff --git a/test/shared/commands/parsers.test.js b/test/shared/commands/parsers.test.js new file mode 100644 index 0000000..200323c --- /dev/null +++ b/test/shared/commands/parsers.test.js @@ -0,0 +1,78 @@ +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 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('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', []]); + }); + }); +}); diff --git a/test/shared/commands/property.test.js b/test/shared/commands/property.test.js deleted file mode 100644 index d949482..0000000 --- a/test/shared/commands/property.test.js +++ /dev/null @@ -1,41 +0,0 @@ -import { expect } from "chai"; -import { parseProperty } from 'shared/commands/properties'; - -describe("shared/commands/properties", () => { - describe("#parseProperty", () => { - it('parse set string', () => { - let [key, value] = parseProperty('encoding=utf-8', { encoding: 'string' }); - expect(key).to.equal('encoding'); - expect(value).to.equal('utf-8'); - }); - - it('parse set string', () => { - let [key, value] = parseProperty('history=50', { history: 'number' }); - expect(key).to.equal('history'); - expect(value).to.equal(50); - }); - - it('parse set boolean', () => { - let [key, value] = parseProperty('paste', { paste: 'boolean' }); - expect(key).to.equal('paste'); - expect(value).to.be.true; - - [key, value] = parseProperty('nopaste', { paste: 'boolean' }); - expect(key).to.equal('paste'); - expect(value).to.be.false; - }); - - it('throws error on unknown property', () => { - expect(() => parseProperty('charset=utf-8', {})).to.throw(Error, 'Unknown'); - expect(() => parseProperty('smoothscroll', {})).to.throw(Error, 'Unknown'); - expect(() => parseProperty('nosmoothscroll', {})).to.throw(Error, 'Unknown'); - }) - - it('throws error on invalid property', () => { - expect(() => parseProperty('charset=utf-8', { charset: 'number' })).to.throw(Error, 'Not number'); - expect(() => parseProperty('charset=utf-8', { charset: 'boolean' })).to.throw(Error, 'Invalid'); - expect(() => parseProperty('smoothscroll', { smoothscroll: 'string' })).to.throw(Error, 'Invalid'); - expect(() => parseProperty('smoothscroll', { smoothscroll: 'number' })).to.throw(Error, 'Invalid'); - }) - }); -}); -- cgit v1.2.3 From befcff973aa0fd9b0f3a73932a39d36e9eb85bf2 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Thu, 11 Jan 2018 20:14:56 +0900 Subject: fix property parser --- src/shared/commands/parsers.js | 2 +- test/shared/commands/parsers.test.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/shared/commands/parsers.js b/src/shared/commands/parsers.js index af51338..fb37d2a 100644 --- a/src/shared/commands/parsers.js +++ b/src/shared/commands/parsers.js @@ -30,7 +30,7 @@ const mustNumber = (v) => { const parseSetOption = (word, types) => { let [key, value] = word.split('='); - if (!value) { + if (value === undefined) { value = !key.startsWith('no'); key = value ? key : key.slice(2); } diff --git a/test/shared/commands/parsers.test.js b/test/shared/commands/parsers.test.js index 200323c..0a1960c 100644 --- a/test/shared/commands/parsers.test.js +++ b/test/shared/commands/parsers.test.js @@ -9,6 +9,12 @@ describe("shared/commands/parsers", () => { 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'); @@ -34,6 +40,7 @@ describe("shared/commands/parsers", () => { 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'); }) -- cgit v1.2.3 From fad8f96a663d83792138cc986474ec4228b6c6c9 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Thu, 11 Jan 2018 20:22:49 +0900 Subject: implement set option --- src/background/actions/command.js | 17 +++++++++++++++++ src/background/components/background.js | 11 +++++------ 2 files changed, 22 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/background/actions/command.js b/src/background/actions/command.js index f11c61b..4c52bca 100644 --- a/src/background/actions/command.js +++ b/src/background/actions/command.js @@ -1,5 +1,7 @@ +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({ @@ -37,6 +39,19 @@ const bufferCommand = (keywords) => { }); }; +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); @@ -53,6 +68,8 @@ const exec = (line, settings) => { case 'b': case 'buffer': return bufferCommand(args); + case 'set': + return setCommand(args); case '': return Promise.resolve(); } diff --git a/src/background/components/background.js b/src/background/components/background.js index 19bf27f..9578e78 100644 --- a/src/background/components/background.js +++ b/src/background/components/background.js @@ -36,12 +36,11 @@ export default class BackgroundComponent { return this.store.dispatch( tabActions.openToTab(message.url, sender.tab), sender); case messages.CONSOLE_ENTER_COMMAND: - return commandActions.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: -- cgit v1.2.3