aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--QA.md57
-rw-r--r--README.md7
-rw-r--r--manifest.json5
-rw-r--r--package-lock.json319
-rw-r--r--package.json9
-rwxr-xr-xpackage.sh20
-rw-r--r--script/package.js55
-rw-r--r--src/background/actions/command.js79
-rw-r--r--src/background/actions/index.js5
-rw-r--r--src/background/actions/setting.js21
-rw-r--r--src/background/components/background.js16
-rw-r--r--src/background/histories.js1
-rw-r--r--src/background/index.js4
-rw-r--r--src/background/reducers/index.js2
-rw-r--r--src/background/reducers/setting.js24
-rw-r--r--src/background/tabs.js2
-rw-r--r--src/content/actions/operation.js21
-rw-r--r--src/content/components/common/follow.js16
-rw-r--r--src/content/components/common/keymapper.js3
-rw-r--r--src/content/components/top-content/follow-controller.js12
-rw-r--r--src/content/navigates.js49
-rw-r--r--src/content/scrolls.js163
-rw-r--r--src/content/urls.js26
-rw-r--r--src/settings/actions/setting.js18
-rw-r--r--src/settings/components/form/keymaps-form.jsx23
-rw-r--r--src/settings/components/form/keymaps-form.scss8
-rw-r--r--src/settings/components/form/properties-form.jsx60
-rw-r--r--src/settings/components/form/properties-form.scss12
-rw-r--r--src/settings/components/index.jsx79
-rw-r--r--src/shared/commands.js169
-rw-r--r--src/shared/commands/complete.js84
-rw-r--r--src/shared/commands/index.js3
-rw-r--r--src/shared/commands/parsers.js59
-rw-r--r--src/shared/operations.js3
-rw-r--r--src/shared/settings/default.js71
-rw-r--r--src/shared/settings/properties.js16
-rw-r--r--src/shared/settings/storage.js33
-rw-r--r--src/shared/settings/validator.js (renamed from src/shared/validators/setting.js)17
-rw-r--r--src/shared/settings/values.js32
-rw-r--r--src/shared/utils/keys.js1
-rw-r--r--test/background/reducers/setting.test.js37
-rw-r--r--test/content/navigates.test.js6
-rw-r--r--test/settings/components/form/properties-form.test.jsx86
-rw-r--r--test/shared/commands/parsers.test.js85
-rw-r--r--test/shared/settings/validator.test.js (renamed from test/shared/validators/setting.test.js)2
-rw-r--r--test/shared/settings/values.test.js33
46 files changed, 1305 insertions, 548 deletions
diff --git a/QA.md b/QA.md
index 8cba39f..1c06861 100644
--- a/QA.md
+++ b/QA.md
@@ -1,12 +1,12 @@
## Checklist for testing Vim Vixen
-### Operations
+### Keybindings in JSON settings
Test operations with default key maps.
#### Scrolling
-- [ ] <kbd>k</kbd> or <kbd>Ctrl</kbd>+<kbd>Y</kbd>, <kbd>j</kbd> or <kbd>Ctrl</kbd>+<kbd>E</kbd>: scroll up and down
+- [ ] <kbd>k</kbd>, <kbd>j</kbd>: scroll up and down
- [ ] <kbd>h</kbd>, <kbd>l</kbd>: scroll left and right
- [ ] <kbd>Ctrl</kbd>+<kbd>U</kbd>, <kbd>Ctrl</kbd>+<kbd>D</kbd>: scroll up and down by half of screen
- [ ] <kbd>Ctrl</kbd>+<kbd>B</kbd>, <kbd>Ctrl</kbd>+<kbd>F</kbd>: scroll up and down by a screen
@@ -35,7 +35,7 @@ The behaviors of the console are tested in [Console section](#consoles).
#### Navigation
-- [ ] <kbd>H</kbd>, <kbd>L</kbd>: go back and forward in histories
+- [ ] <kbd>H</kbd>, <kbd>L</kbd>: go back and forward in history
- [ ] <kbd>[</kbd><kbd>[</kbd>, <kbd>]</kbd><kbd>]</kbd>: Open next/prev link in `<link>` tags.
- [ ] <kbd>[</kbd><kbd>[</kbd>, <kbd>]</kbd><kbd>]</kbd>: find prev and next links and open it
- [ ] <kbd>g</kbd><kbd>u</kbd>: go to parent directory
@@ -46,6 +46,8 @@ The behaviors of the console are tested in [Console section](#consoles).
- [ ] <kbd>z</kbd><kbd>i</kbd>, <kbd>z</kbd><kbd>o</kbd>: zoom-in and zoom-out
- [ ] <kbd>z</kbd><kbd>z</kbd>: set zoom level as default
- [ ] <kbd>y</kbd>: yank current URL and show a message
+- [ ] <kbd>p</kbd>: open clipbord's URL in current tab
+- [ ] <kbd>P</kbd>: open clipbord's URL in new tab
- [ ] Toggle enabled/disabled of plugin bu <kbd>Shift</kbd>+<kbd>Esc</kbd>
### Following links
@@ -83,7 +85,7 @@ The behaviors of the console are tested in [Console section](#consoles).
- [ ] `buffer`,`buffer<SP>`: do nothing
- [ ] `buffer <title>`, `buffer <url>`: select tab which has an title matched with
- [ ] `buffer 1`: select leftmost tab
-- [ ] `buffer 0`, `buffer 99`: shows an error
+- [ ] `buffer 0`, `buffer <a number more than count of tabs>`: shows an error
- [ ] select tabs rotationally when more than two tabs are matched
### Completions
@@ -110,20 +112,22 @@ The behaviors of the console are tested in [Console section](#consoles).
### Settings
-#### Validations
+#### JSON Settings
+
+##### Validations
- [ ] show error on invalid json
- [ ] show error when top-level keys has keys other than `keymaps`, `search`, and `blacklist`
-##### `"keymaps"` section
+###### `"keymaps"` section
- [ ] show error on unknown operation name in `"keymaps"`
-##### `"search"` section
+###### `"search"` section
- validations in `"search"` section are not tested in this release
-#### `"blacklist"` section
+##### `"blacklist"` section
- [ ] `github.com/a` blocks `github.com/a`, and not blocks `github.com/aa`
- [ ] `github.com/a*` blocks both `github.com/a` and `github.com/aa`
@@ -131,18 +135,49 @@ The behaviors of the console are tested in [Console section](#consoles).
- [ ] `github.com` blocks both `github.com/` and `github.com/a`
- [ ] `*.github.com` blocks `gist.github.com/`, and not `github.com`
-#### Updating
+##### Updating
- [ ] changes are updated on textarea blure when no errors
- [ ] changes are not updated on textarea blure when errors occurs
- [ ] keymap settings are applied to open tabs without reload
- [ ] search settings are applied to open tabs without reload
+#### Form Settings
+
+<!-- validation on form settings does not implement in 0.7 -->
+
+##### Search Engines
+
+- [ ] able to change default
+- [ ] able to remove item
+- [ ] able to add item
+
+##### `"blacklist"` section
+
+- [ ] able to add item
+- [ ] able to remove item
+- [ ] `github.com/a` blocks `github.com/a`, and not blocks `github.com/aa`
+- [ ] `github.com/a*` blocks both `github.com/a` and `github.com/aa`
+- [ ] `github.com/` blocks `github.com/`, and not blocks `github.com/a`
+- [ ] `github.com` blocks both `github.com/` and `github.com/a`
+- [ ] `*.github.com` blocks `gist.github.com/`, and not `github.com`
+
+##### Updating
+
+- [ ] keymap settings are applied to open tabs without reload
+- [ ] search settings are applied to open tabs without reload
+
+### Settings source
+
+- [ ] show confirmation dialog on switched from json to form
+- [ ] state is saved on source changed
+- [ ] on switching form -> json -> form, first and last form setting is equivalent to first one
+
### For certain sites
-- [ ] scoll on Hacker News
+- [ ] scroll on Hacker News
- [ ] able to scroll on Gmail and Slack
-- [ ] Fucus text box on Twitter or Slack, press <kbd>j</kbd>, then <kbd>j</kbd> is typed in the box
+- [ ] Focus text box on Twitter or Slack, press <kbd>j</kbd>, then <kbd>j</kbd> is typed in the box
- [ ] Focus the text box on Twitter or Slack on following mode
- [ ] Tha pages is shown in https://pitchify.com/
- [ ] Open console in http://www.espncricinfo.com/
diff --git a/README.md b/README.md
index 2228f69..5703a25 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,7 @@
[![Join the chat room on Gitter for vim-vixen/vim-vixen](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/vim-vixen/vim-vixen)
[![Build Status](https://travis-ci.org/ueokande/vim-vixen.svg?branch=kaizen)](https://travis-ci.org/ueokande/vim-vixen)
+[![devDependencies Status](https://david-dm.org/ueokande/vim-vixen/dev-status.svg)](https://david-dm.org/ueokande/vim-vixen?type=dev)
Vim Vixen is a Firefox add-on which allows you to navigate with keyboard on the browser.
Firefox started to support WebExtensions API and will stop supporting add-ons using legacy APIs from version 57.
@@ -44,8 +45,8 @@ The default mappings are as follows:
### Navigation
- <kbd>f</kbd>: start following links in the page
-- <kbd>H</kbd>: go back in histories
-- <kbd>L</kbd>: go forward in histories
+- <kbd>H</kbd>: go back in history
+- <kbd>L</kbd>: go forward in history
- <kbd>[</kbd><kbd>[</kbd>, <kbd>]</kbd><kbd>]</kbd>: find prev or next links and open it
- <kbd>g</kbd><kbd>u</kbd>: go to parent directory
- <kbd>g</kbd><kbd>U</kbd>: go to root directory
@@ -54,6 +55,8 @@ The default mappings are as follows:
- <kbd>z</kbd><kbd>i</kbd>, <kbd>z</kbd><kbd>o</kbd>: zoom-in/zoom-out
- <kbd>z</kbd><kbd>z</kbd>: Set default zoom level
- <kbd>y</kbd>: copy URL in current tab
+- <kbd>p</kbd>: open clipbord's URL in current tab
+- <kbd>P</kbd>: open clipbord's URL in new tab
- <kbd>Shift</kbd>+<kbd>Esc</kbd>: enable or disable the add-on in current tab.
### Console commands
diff --git a/manifest.json b/manifest.json
index 532eaac..02d8589 100644
--- a/manifest.json
+++ b/manifest.json
@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "Vim Vixen",
"description": "Vim Vixen",
- "version": "0.6",
+ "version": "0.8",
"icons": {
"48": "resources/icon_48x48.png",
"96": "resources/icon_96x96.png"
@@ -30,7 +30,8 @@
"history",
"sessions",
"storage",
- "tabs"
+ "tabs",
+ "clipboardRead"
],
"web_accessible_resources": [
"build/console.html",
diff --git a/package-lock.json b/package-lock.json
index 0a50bf0..0d5ffd8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3,6 +3,146 @@
"requires": true,
"lockfileVersion": 1,
"dependencies": {
+ "@babel/code-frame": {
+ "version": "7.0.0-beta.32",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.32.tgz",
+ "integrity": "sha512-EVq4T1a2GviKiQ75OfxNrGPPhJyXzg9jjORuuwhloZbFdrhT4FHa73sv9OFWBwX7rl2b6bxBVmfxrBQYWYz9tA==",
+ "dev": true,
+ "requires": {
+ "chalk": "2.3.0",
+ "esutils": "2.0.2",
+ "js-tokens": "3.0.2"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
+ "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
+ "dev": true,
+ "requires": {
+ "color-convert": "1.9.0"
+ }
+ },
+ "chalk": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz",
+ "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "3.2.0",
+ "escape-string-regexp": "1.0.5",
+ "supports-color": "4.5.0"
+ }
+ },
+ "supports-color": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz",
+ "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=",
+ "dev": true,
+ "requires": {
+ "has-flag": "2.0.0"
+ }
+ }
+ }
+ },
+ "@babel/helper-function-name": {
+ "version": "7.0.0-beta.32",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.32.tgz",
+ "integrity": "sha512-ysfIt7p72xm5fjSJsv7fMVN/j+EwIdqu8/MJjt6TqB4wM2r6rFRi0ujBTWDkLGQkRB/P5uDV8qcFCHAHnNzmsg==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-get-function-arity": "7.0.0-beta.32",
+ "@babel/template": "7.0.0-beta.32",
+ "@babel/types": "7.0.0-beta.32"
+ }
+ },
+ "@babel/helper-get-function-arity": {
+ "version": "7.0.0-beta.32",
+ "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.32.tgz",
+ "integrity": "sha512-bm7lIlizycJQY5SJ3HXWJV4XjSrOt1onzrDcOxUo9FEnKRZDEr/zfi5ar2s5tvvZvve/jGHwZKVKekRw2cjPCQ==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "7.0.0-beta.32"
+ }
+ },
+ "@babel/template": {
+ "version": "7.0.0-beta.32",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.32.tgz",
+ "integrity": "sha512-DB9sLgX2mfE29vjAkxHlzLyWr31EO9HaYoAM/UsPSsL70Eudl0i25URwIfQT6S6ckeVFnFP1t6PhERVeV4EAHA==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "7.0.0-beta.32",
+ "@babel/types": "7.0.0-beta.32",
+ "babylon": "7.0.0-beta.32",
+ "lodash": "4.17.4"
+ },
+ "dependencies": {
+ "babylon": {
+ "version": "7.0.0-beta.32",
+ "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.32.tgz",
+ "integrity": "sha512-PvAmyP2IJEBVAuE5yVzrTSWCCN9VMa1eGns8w3w6FYD/ivHSUmS7n+F40Fmjn+0nCQSUFR96wP0CqQ4jxTnF4Q==",
+ "dev": true
+ }
+ }
+ },
+ "@babel/traverse": {
+ "version": "7.0.0-beta.32",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.32.tgz",
+ "integrity": "sha512-dGe2CLduCIZ/iDkbmnqspQguRy5ARvI+zC8TiwFnsJ2YYO2TWK7x2aEwrbkSmi0iPlBP+Syiag7Idc1qNQq74g==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "7.0.0-beta.32",
+ "@babel/helper-function-name": "7.0.0-beta.32",
+ "@babel/types": "7.0.0-beta.32",
+ "babylon": "7.0.0-beta.32",
+ "debug": "3.1.0",
+ "globals": "10.4.0",
+ "invariant": "2.2.2",
+ "lodash": "4.17.4"
+ },
+ "dependencies": {
+ "babylon": {
+ "version": "7.0.0-beta.32",
+ "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.32.tgz",
+ "integrity": "sha512-PvAmyP2IJEBVAuE5yVzrTSWCCN9VMa1eGns8w3w6FYD/ivHSUmS7n+F40Fmjn+0nCQSUFR96wP0CqQ4jxTnF4Q==",
+ "dev": true
+ },
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "globals": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-10.4.0.tgz",
+ "integrity": "sha512-uNUtxIZpGyuaq+5BqGGQHsL4wUlJAXRqOm6g3Y48/CWNGTLONgBibI0lh6lGxjR2HljFYUfszb+mk4WkgMntsA==",
+ "dev": true
+ }
+ }
+ },
+ "@babel/types": {
+ "version": "7.0.0-beta.32",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.32.tgz",
+ "integrity": "sha512-w8+wzVcYCMb9OfaBfay2Vg5hyj7UfBX6qQtA+kB0qsW1h1NH/7xHMwvTZNqkuFBwjz5wxGS2QmaIcC3HH+UoxA==",
+ "dev": true,
+ "requires": {
+ "esutils": "2.0.2",
+ "lodash": "4.17.4",
+ "to-fast-properties": "2.0.0"
+ },
+ "dependencies": {
+ "to-fast-properties": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+ "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
+ "dev": true
+ }
+ }
+ },
"abbrev": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz",
@@ -59,6 +199,12 @@
}
}
},
+ "adm-zip": {
+ "version": "0.4.7",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.7.tgz",
+ "integrity": "sha1-hgbCy/HEJs6MjsABdER/1Jtur8E=",
+ "dev": true
+ },
"after": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
@@ -388,15 +534,23 @@
}
},
"babel-eslint": {
- "version": "7.2.3",
- "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-7.2.3.tgz",
- "integrity": "sha1-sv4tgBJkcPXBlELcdXJTqJdxCCc=",
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-8.0.2.tgz",
+ "integrity": "sha512-yyl5U088oE+419+BNLJDKVWkUokuPLQeQt9ZTy9uM9kAzbtQgyYL3JkG425B8jxXA7MwTxnDAtRLMKJNH36qjA==",
"dev": true,
"requires": {
- "babel-code-frame": "6.22.0",
- "babel-traverse": "6.25.0",
- "babel-types": "6.25.0",
- "babylon": "6.17.4"
+ "@babel/code-frame": "7.0.0-beta.32",
+ "@babel/traverse": "7.0.0-beta.32",
+ "@babel/types": "7.0.0-beta.32",
+ "babylon": "7.0.0-beta.32"
+ },
+ "dependencies": {
+ "babylon": {
+ "version": "7.0.0-beta.32",
+ "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.32.tgz",
+ "integrity": "sha512-PvAmyP2IJEBVAuE5yVzrTSWCCN9VMa1eGns8w3w6FYD/ivHSUmS7n+F40Fmjn+0nCQSUFR96wP0CqQ4jxTnF4Q==",
+ "dev": true
+ }
}
},
"babel-generator": {
@@ -2315,9 +2469,9 @@
"dev": true
},
"diff": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz",
- "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz",
+ "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==",
"dev": true
},
"diffie-hellman": {
@@ -3421,16 +3575,10 @@
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
"dev": true
},
- "graceful-readlink": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
- "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=",
- "dev": true
- },
"growl": {
- "version": "1.9.2",
- "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz",
- "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=",
+ "version": "1.10.3",
+ "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz",
+ "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==",
"dev": true
},
"har-schema": {
@@ -4553,40 +4701,6 @@
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
"dev": true
},
- "lodash._baseassign": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz",
- "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=",
- "dev": true,
- "requires": {
- "lodash._basecopy": "3.0.1",
- "lodash.keys": "3.1.2"
- }
- },
- "lodash._basecopy": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz",
- "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=",
- "dev": true
- },
- "lodash._basecreate": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz",
- "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=",
- "dev": true
- },
- "lodash._getnative": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz",
- "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=",
- "dev": true
- },
- "lodash._isiterateecall": {
- "version": "3.0.9",
- "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz",
- "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=",
- "dev": true
- },
"lodash.assign": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
@@ -4605,46 +4719,12 @@
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
"dev": true
},
- "lodash.create": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz",
- "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=",
- "dev": true,
- "requires": {
- "lodash._baseassign": "3.2.0",
- "lodash._basecreate": "3.0.3",
- "lodash._isiterateecall": "3.0.9"
- }
- },
- "lodash.isarguments": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
- "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=",
- "dev": true
- },
- "lodash.isarray": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
- "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=",
- "dev": true
- },
"lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=",
"dev": true
},
- "lodash.keys": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
- "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=",
- "dev": true,
- "requires": {
- "lodash._getnative": "3.9.1",
- "lodash.isarguments": "3.1.0",
- "lodash.isarray": "3.0.4"
- }
- },
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -5031,60 +5111,39 @@
}
},
"mocha": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.0.tgz",
- "integrity": "sha512-pIU2PJjrPYvYRqVpjXzj76qltO9uBYI7woYAMoxbSefsa+vqAfptjoeevd6bUgwD0mPIO+hv9f7ltvsNreL2PA==",
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.0.1.tgz",
+ "integrity": "sha512-evDmhkoA+cBNiQQQdSKZa2b9+W2mpLoj50367lhy+Klnx9OV8XlCIhigUnn1gaTFLQCa0kdNhEGDr0hCXOQFDw==",
"dev": true,
"requires": {
"browser-stdout": "1.3.0",
- "commander": "2.9.0",
- "debug": "2.6.8",
- "diff": "3.2.0",
+ "commander": "2.11.0",
+ "debug": "3.1.0",
+ "diff": "3.3.1",
"escape-string-regexp": "1.0.5",
- "glob": "7.1.1",
- "growl": "1.9.2",
- "json3": "3.3.2",
- "lodash.create": "3.1.1",
+ "glob": "7.1.2",
+ "growl": "1.10.3",
+ "he": "1.1.1",
"mkdirp": "0.5.1",
- "supports-color": "3.1.2"
+ "supports-color": "4.4.0"
},
"dependencies": {
- "commander": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
- "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=",
- "dev": true,
- "requires": {
- "graceful-readlink": "1.0.1"
- }
- },
- "glob": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz",
- "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=",
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
- "fs.realpath": "1.0.0",
- "inflight": "1.0.6",
- "inherits": "2.0.3",
- "minimatch": "3.0.4",
- "once": "1.4.0",
- "path-is-absolute": "1.0.1"
+ "ms": "2.0.0"
}
},
- "has-flag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
- "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
- "dev": true
- },
"supports-color": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz",
- "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
+ "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==",
"dev": true,
"requires": {
- "has-flag": "1.0.0"
+ "has-flag": "2.0.0"
}
}
}
@@ -7465,9 +7524,9 @@
"dev": true
},
"style-loader": {
- "version": "0.18.2",
- "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.18.2.tgz",
- "integrity": "sha512-WPpJPZGUxWYHWIUMNNOYqql7zh85zGmr84FdTVWq52WTIkqlW9xSxD3QYWi/T31cqn9UNSsietVEgGn2aaSCzw==",
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.19.0.tgz",
+ "integrity": "sha512-9mx9sC9nX1dgP96MZOODpGC6l1RzQBITI2D5WJhu+wnbrSYVKLGuy14XJSLVQih/0GFrPpjelt+s//VcZQ2Evw==",
"dev": true,
"requires": {
"loader-utils": "1.1.0",
diff --git a/package.json b/package.json
index 5606b79..05e3ff5 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"scripts": {
"start": "webpack -w --debug --devtool inline-source-map",
"build": "NODE_ENV=production webpack --progress --display-error-details",
- "package": "npm run build && ./package.sh",
+ "package": "npm run build && node script/package.js",
"lint": "eslint --ext .jsx,.js src",
"test": "karma start"
},
@@ -19,8 +19,9 @@
},
"homepage": "https://github.com/ueokande/vim-vixen",
"devDependencies": {
+ "adm-zip": "^0.4.7",
"babel-cli": "^6.24.1",
- "babel-eslint": "^7.2.3",
+ "babel-eslint": "^8.0.2",
"babel-loader": "^7.1.1",
"babel-minify-webpack-plugin": "^0.2.0",
"babel-preset-es2015": "^6.24.1",
@@ -37,11 +38,11 @@
"karma-mocha-reporter": "^2.2.3",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^2.0.4",
- "mocha": "^3.5.0",
+ "mocha": "^4.0.1",
"node-sass": "^4.5.3",
"preact": "^8.2.6",
"sass-loader": "^6.0.6",
- "style-loader": "^0.18.2",
+ "style-loader": "^0.19.0",
"webpack": "^3.5.3"
}
}
diff --git a/package.sh b/package.sh
deleted file mode 100755
index 34535ba..0000000
--- a/package.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/sh
-
-MANIFEST="manifest.json"
-
-version=$(jq -r '.version' $MANIFEST)
-
-icons=$(jq -r '.icons[]' $MANIFEST)
-content_scripts=$(jq -r '.content_scripts[].js[]' $MANIFEST)
-background_scripts=$(jq -r '.background.scripts[]' $MANIFEST)
-web_accessible_resources=$(jq -r '.web_accessible_resources[]' $MANIFEST)
-options_ui=$(jq -r '.options_ui.page' $MANIFEST)
-options_scripts=""
-for html in $options_ui; do
- scripts=$(grep -Po "(?<=src=['\"])[^'\"]*" "$html")
- for js in $scripts; do
- options_scripts="$options_scripts $(dirname $html)/$js"
- done
-done
-
-zip ${version}.zip $MANIFEST $icons $content_scripts $background_scripts $web_accessible_resources $options_ui $options_scripts
diff --git a/script/package.js b/script/package.js
new file mode 100644
index 0000000..0128f0f
--- /dev/null
+++ b/script/package.js
@@ -0,0 +1,55 @@
+let path = require('path');
+let fs = require('fs');
+let AdmZip = require('adm-zip');
+let manifest = require('../manifest');
+
+manifest.iconFiles = function() {
+ return Object.keys(this.icons).map(key => this.icons[key]);
+};
+
+manifest.contentScriptFiles = function() {
+ let files = this.content_scripts.map(entry => entry.js);
+ return [].concat.apply([], files);
+};
+
+manifest.backgroundScriptFiles = function() {
+ return this.background.scripts;
+
+};
+
+manifest.webAccessibleResourceFiles = function() {
+ return this.web_accessible_resources;
+};
+
+manifest.optionFiles = function() {
+ let uiFile = this.options_ui.page;
+ let dir = path.dirname(uiFile);
+ let html = fs.readFileSync(uiFile, 'utf-8');
+
+ let files = [uiFile];
+ let regex = /<\s*script\s+src\s*=\s*'(.*)'\s*>/g;
+ let match = regex.exec(html);
+ while (match) {
+ files.push(path.join(dir, match[1]));
+ match = regex.exec(html);
+ }
+ return files;
+};
+
+let files = []
+ .concat('manifest.json')
+ .concat(manifest.iconFiles())
+ .concat(manifest.contentScriptFiles())
+ .concat(manifest.backgroundScriptFiles())
+ .concat(manifest.webAccessibleResourceFiles())
+ .concat(manifest.optionFiles());
+let zip = new AdmZip();
+let output = `${manifest.version}.zip`;
+console.log(output);
+for (let f of files) {
+ let dir = path.dirname(f);
+ zip.addLocalFile(f, dir);
+ console.log('=>', path.join(dir, f));
+}
+
+zip.writeZip(output);
diff --git a/src/background/actions/command.js b/src/background/actions/command.js
new file mode 100644
index 0000000..4c52bca
--- /dev/null
+++ b/src/background/actions/command.js
@@ -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 };
diff --git a/src/background/actions/index.js b/src/background/actions/index.js
new file mode 100644
index 0000000..efe4074
--- /dev/null
+++ b/src/background/actions/index.js
@@ -0,0 +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
new file mode 100644
index 0000000..773142f
--- /dev/null
+++ b/src/background/actions/setting.js
@@ -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 };
diff --git a/src/background/components/background.js b/src/background/components/background.js
index 2d94310..9578e78 100644
--- a/src/background/components/background.js
+++ b/src/background/components/background.js
@@ -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();
}
}
diff --git a/src/background/histories.js b/src/background/histories.js
index 6b6e4c6..a7d3d47 100644
--- a/src/background/histories.js
+++ b/src/background/histories.js
@@ -76,7 +76,6 @@ const getCompletions = (keyword) => {
.sort((x, y) => x[0].visitCount < y[0].visitCount)
.slice(0, 10)
.map(item => item[0])
- .sort((x, y) => x.url > y.url)
)[0];
});
};
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..045a654
--- /dev/null
+++ b/src/background/reducers/setting.js
@@ -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;
+ }
+}
+
diff --git a/src/background/tabs.js b/src/background/tabs.js
index 5ed5bdf..d50d8e5 100644
--- a/src/background/tabs.js
+++ b/src/background/tabs.js
@@ -61,7 +61,7 @@ const selectByKeyword = (current, keyword) => {
const getCompletions = (keyword) => {
return browser.tabs.query({ currentWindow: true }).then((tabs) => {
let matched = tabs.filter((t) => {
- return t.url.includes(keyword) || t.title.includes(keyword);
+ return t.url.includes(keyword) || t.title && t.title.includes(keyword);
});
return matched;
});
diff --git a/src/content/actions/operation.js b/src/content/actions/operation.js
index 767f14b..b4b2e38 100644
--- a/src/content/actions/operation.js
+++ b/src/content/actions/operation.js
@@ -5,9 +5,12 @@ import * as navigates from 'content/navigates';
import * as urls from 'content/urls';
import * as consoleFrames from 'content/console-frames';
import * as addonActions from './addon';
+import * as properties from 'shared/settings/properties';
// eslint-disable-next-line complexity
-const exec = (operation) => {
+const exec = (operation, repeat, settings) => {
+ let smoothscroll = settings.properties.smoothscroll ||
+ properties.defaults.smoothscroll;
switch (operation.type) {
case operations.ADDON_ENABLE:
return addonActions.enable();
@@ -24,19 +27,19 @@ const exec = (operation) => {
type: messages.FIND_PREV,
}), '*');
case operations.SCROLL_VERTICALLY:
- return scrolls.scrollVertically(window, operation.count);
+ return scrolls.scrollVertically(operation.count, smoothscroll, repeat);
case operations.SCROLL_HORIZONALLY:
- return scrolls.scrollHorizonally(window, operation.count);
+ return scrolls.scrollHorizonally(operation.count, smoothscroll, repeat);
case operations.SCROLL_PAGES:
- return scrolls.scrollPages(window, operation.count);
+ return scrolls.scrollPages(operation.count, smoothscroll, repeat);
case operations.SCROLL_TOP:
- return scrolls.scrollTop(window);
+ return scrolls.scrollTop(smoothscroll, repeat);
case operations.SCROLL_BOTTOM:
- return scrolls.scrollBottom(window);
+ return scrolls.scrollBottom(smoothscroll, repeat);
case operations.SCROLL_HOME:
- return scrolls.scrollHome(window);
+ return scrolls.scrollHome(smoothscroll, repeat);
case operations.SCROLL_END:
- return scrolls.scrollEnd(window);
+ return scrolls.scrollEnd(smoothscroll, repeat);
case operations.FOLLOW_START:
return window.top.postMessage(JSON.stringify({
type: messages.FOLLOW_START,
@@ -60,6 +63,8 @@ const exec = (operation) => {
type: messages.CONSOLE_SHOW_INFO,
text: 'Current url yanked',
});
+ case operations.URLS_PASTE:
+ return urls.paste(window, operation.newTab ? operation.newTab : false);
default:
browser.runtime.sendMessage({
type: messages.BACKGROUND_OPERATION,
diff --git a/src/content/components/common/follow.js b/src/content/components/common/follow.js
index 7717154..42dd897 100644
--- a/src/content/components/common/follow.js
+++ b/src/content/components/common/follow.js
@@ -30,6 +30,21 @@ const inViewport = (win, element, viewSize, framePosition) => {
return true;
};
+const isAriaHiddenOrAriaDisabled = (win, element) => {
+ if (!element || win.document.documentElement === element) {
+ return false;
+ }
+ for (let attr of ['aria-hidden', 'aria-disabled']) {
+ if (element.hasAttribute(attr)) {
+ let hidden = element.getAttribute(attr).toLowerCase();
+ if (hidden === '' || hidden === 'true') {
+ return true;
+ }
+ }
+ }
+ return isAriaHiddenOrAriaDisabled(win, element.parentNode);
+};
+
export default class Follow {
constructor(win, store) {
this.win = win;
@@ -171,6 +186,7 @@ export default class Follow {
style.visibility !== 'hidden' &&
element.type !== 'hidden' &&
element.offsetHeight > 0 &&
+ !isAriaHiddenOrAriaDisabled(win, element) &&
inViewport(win, element, viewSize, framePosition);
});
return filtered;
diff --git a/src/content/components/common/keymapper.js b/src/content/components/common/keymapper.js
index fb8fabe..d9d1b2f 100644
--- a/src/content/components/common/keymapper.js
+++ b/src/content/components/common/keymapper.js
@@ -47,7 +47,8 @@ export default class KeymapperComponent {
return true;
}
let operation = keymaps.get(matched[0]);
- this.store.dispatch(operationActions.exec(operation));
+ this.store.dispatch(operationActions.exec(
+ operation, key.repeat, state.setting));
this.store.dispatch(inputActions.clearKeys());
return true;
}
diff --git a/src/content/components/top-content/follow-controller.js b/src/content/components/top-content/follow-controller.js
index d373177..1e7f3cd 100644
--- a/src/content/components/top-content/follow-controller.js
+++ b/src/content/components/top-content/follow-controller.js
@@ -1,8 +1,7 @@
import * as followControllerActions from 'content/actions/follow-controller';
import messages from 'shared/messages';
import HintKeyProducer from 'content/hint-key-producer';
-
-const DEFAULT_HINT_CHARSET = 'abcdefghijklmnopqrstuvwxyz';
+import * as properties from 'shared/settings/properties';
const broadcastMessage = (win, message) => {
let json = JSON.stringify(message);
@@ -84,7 +83,7 @@ export default class FollowController {
this.store.dispatch(followControllerActions.backspace());
break;
default:
- if (DEFAULT_HINT_CHARSET.includes(key)) {
+ if (this.hintchars().includes(key)) {
this.store.dispatch(followControllerActions.keyPress(key));
}
break;
@@ -93,7 +92,7 @@ export default class FollowController {
}
count() {
- this.producer = new HintKeyProducer(DEFAULT_HINT_CHARSET);
+ this.producer = new HintKeyProducer(this.hintchars());
let doc = this.win.document;
let viewWidth = this.win.innerWidth || doc.documentElement.clientWidth;
let viewHeight = this.win.innerHeight || doc.documentElement.clientHeight;
@@ -135,4 +134,9 @@ export default class FollowController {
type: messages.FOLLOW_REMOVE_HINTS,
});
}
+
+ hintchars() {
+ return this.store.getState().setting.properties.hintchars ||
+ properties.defaults.hintchars;
+ }
}
diff --git a/src/content/navigates.js b/src/content/navigates.js
index 3e12a6f..c9baa30 100644
--- a/src/content/navigates.js
+++ b/src/content/navigates.js
@@ -1,18 +1,18 @@
-const PREV_LINK_PATTERNS = [
- /\bprev\b/i, /\bprevious\b/i, /\bback\b/i,
- /</, /\u2039/, /\u2190/, /\xab/, /\u226a/, /<</
-];
-
-const NEXT_LINK_PATTERNS = [
- /\bnext\b/i,
- />/, /\u203a/, /\u2192/, /\xbb/, /\u226b/, />>/
-];
-
-const findLinkByPatterns = (win, patterns) => {
- const links = win.document.getElementsByTagName('a');
- return Array.prototype.find.call(links, (link) => {
- return patterns.some(ptn => ptn.test(link.textContent));
- });
+const REL_PATTERN = {
+ prev: /^(?:prev(?:ious)?|older)\b|\u2039|\u2190|\xab|\u226a|<</i,
+ next: /^(?:next|newer)\b|\u203a|\u2192|\xbb|\u226b|>>/i,
+};
+
+// Return the last element in the document matching the supplied selector
+// and the optional filter, or null if there are no matches.
+const selectLast = (win, selector, filter) => {
+ let nodes = win.document.querySelectorAll(selector);
+
+ if (filter) {
+ nodes = Array.from(nodes).filter(filter);
+ }
+
+ return nodes.length ? nodes[nodes.length - 1] : null;
};
const historyPrev = (win) => {
@@ -23,16 +23,21 @@ const historyNext = (win) => {
win.history.forward();
};
-const linkCommon = (win, rel, patterns) => {
- let link = win.document.querySelector(`link[rel~=${rel}][href]`);
+// Code common to linkPrev and linkNext which navigates to the specified page.
+const linkRel = (win, rel) => {
+ let link = selectLast(win, `link[rel~=${rel}][href]`);
if (link) {
- win.location = link.getAttribute('href');
+ win.location = link.href;
return;
}
- link = win.document.querySelector(`a[rel~=${rel}]`) ||
- findLinkByPatterns(win, patterns);
+ const pattern = REL_PATTERN[rel];
+
+ link = selectLast(win, `a[rel~=${rel}][href]`) ||
+ // `innerText` is much slower than `textContent`, but produces much better
+ // (i.e. less unexpected) results
+ selectLast(win, 'a[href]', lnk => pattern.test(lnk.innerText));
if (link) {
link.click();
@@ -40,11 +45,11 @@ const linkCommon = (win, rel, patterns) => {
};
const linkPrev = (win) => {
- linkCommon(win, 'prev', PREV_LINK_PATTERNS);
+ linkRel(win, 'prev');
};
const linkNext = (win) => {
- linkCommon(win, 'next', NEXT_LINK_PATTERNS);
+ linkRel(win, 'next');
};
const parent = (win) => {
diff --git a/src/content/scrolls.js b/src/content/scrolls.js
index ef38273..e8e9642 100644
--- a/src/content/scrolls.js
+++ b/src/content/scrolls.js
@@ -1,7 +1,11 @@
const SCROLL_DELTA_X = 48;
const SCROLL_DELTA_Y = 48;
+const SMOOTH_SCROLL_DURATION = 150;
-const isVisible = (win, element) => {
+// dirty way to store scrolling state on globally
+let scrolling = [false];
+
+const isVisible = (element) => {
let rect = element.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
return false;
@@ -9,19 +13,19 @@ const isVisible = (win, element) => {
if (rect.right < 0 && rect.bottom < 0) {
return false;
}
- if (win.innerWidth < rect.left && win.innerHeight < rect.top) {
+ if (window.innerWidth < rect.left && window.innerHeight < rect.top) {
return false;
}
- let { display, visibility } = win.getComputedStyle(element);
+ let { display, visibility } = window.getComputedStyle(element);
if (display === 'none' || visibility === 'hidden') {
return false;
}
return true;
};
-const isScrollableStyle = (win, element) => {
- let { overflowX, overflowY } = win.getComputedStyle(element);
+const isScrollableStyle = (element) => {
+ let { overflowX, overflowY } = window.getComputedStyle(element);
return !(overflowX !== 'scroll' && overflowX !== 'auto' &&
overflowY !== 'scroll' && overflowY !== 'auto');
};
@@ -35,15 +39,15 @@ const isOverflowed = (element) => {
// this method is called by each scrolling, and the returned value of this
// method is not cached. That does not cause performance issue because in the
// most pages, the window is root element i,e, documentElement.
-const findScrollable = (win, element) => {
- if (isScrollableStyle(win, element) && isOverflowed(element)) {
+const findScrollable = (element) => {
+ if (isScrollableStyle(element) && isOverflowed(element)) {
return element;
}
let children = Array.prototype
- .filter.call(element.children, e => isVisible(win, e));
+ .filter.call(element.children, e => isVisible(e));
for (let child of children) {
- let scrollable = findScrollable(win, child);
+ let scrollable = findScrollable(child);
if (scrollable) {
return scrollable;
}
@@ -51,68 +55,153 @@ const findScrollable = (win, element) => {
return null;
};
-const scrollTarget = (win) => {
- if (isOverflowed(win.document.documentElement)) {
- return win.document.documentElement;
+const scrollTarget = () => {
+ if (isOverflowed(window.document.documentElement)) {
+ return window.document.documentElement;
}
- if (isOverflowed(win.document.body)) {
- return win.document.body;
+ if (isOverflowed(window.document.body)) {
+ return window.document.body;
}
- let target = findScrollable(win, win.document.documentElement);
+ let target = findScrollable(window.document.documentElement);
if (target) {
return target;
}
- return win.document.documentElement;
+ return window.document.documentElement;
};
-const scrollVertically = (win, count) => {
- let target = scrollTarget(win);
+class SmoothScroller {
+ constructor(element, repeat) {
+ this.element = element;
+ this.repeat = repeat;
+ this.scrolling = scrolling;
+ if (repeat) {
+ this.easing = SmoothScroller.linearEasing;
+ } else {
+ this.easing = SmoothScroller.inOutQuadEasing;
+ }
+ }
+
+ scroll(x, y) {
+ if (this.scrolling[0]) {
+ return;
+ }
+ scrolling[0] = true;
+
+ this.startX = this.element.scrollLeft;
+ this.startY = this.element.scrollTop;
+
+ this.targetX = x;
+ this.targetY = y;
+ this.distanceX = x - this.startX;
+ this.distanceY = y - this.startY;
+ this.timeStart = 0;
+
+ window.requestAnimationFrame(this.loop.bind(this));
+ }
+
+ loop(time) {
+ if (!this.timeStart) {
+ this.timeStart = time;
+ }
+
+ let elapsed = time - this.timeStart;
+ let v = this.easing(elapsed / SMOOTH_SCROLL_DURATION);
+ let nextX = this.startX + this.distanceX * v;
+ let nextY = this.startY + this.distanceY * v;
+
+ window.scrollTo(nextX, nextY);
+
+ if (elapsed < SMOOTH_SCROLL_DURATION) {
+ window.requestAnimationFrame(this.loop.bind(this));
+ } else {
+ scrolling[0] = false;
+ this.element.scrollTo(this.targetX, this.targetY);
+ }
+ }
+
+ static inOutQuadEasing(t) {
+ if (t < 1) {
+ return t * t;
+ }
+ return -(t - 1) * (t - 1) + 1;
+ }
+
+ static linearEasing(t) {
+ return t;
+ }
+}
+
+class RoughtScroller {
+ constructor(element) {
+ this.element = element;
+ }
+
+ scroll(x, y) {
+ this.element.scrollTo(x, y);
+ }
+}
+
+const scroller = (element, smooth, repeat) => {
+ if (smooth) {
+ return new SmoothScroller(element, repeat);
+ }
+ return new RoughtScroller(element);
+};
+
+const scrollVertically = (count, smooth, repeat) => {
+ let target = scrollTarget();
let x = target.scrollLeft;
let y = target.scrollTop + SCROLL_DELTA_Y * count;
- target.scrollTo(x, y);
+ if (repeat && smooth) {
+ y = target.scrollTop + SCROLL_DELTA_Y * count * 4;
+ }
+ scroller(target, smooth, repeat).scroll(x, y);
};
-const scrollHorizonally = (win, count) => {
- let target = scrollTarget(win);
+const scrollHorizonally = (count, smooth, repeat) => {
+ let target = scrollTarget();
let x = target.scrollLeft + SCROLL_DELTA_X * count;
let y = target.scrollTop;
- target.scrollTo(x, y);
+ if (repeat && smooth) {
+ y = target.scrollTop + SCROLL_DELTA_Y * count * 4;
+ }
+ scroller(target, smooth, repeat).scroll(x, y);
};
-const scrollPages = (win, count) => {
- let target = scrollTarget(win);
+const scrollPages = (count, smooth, repeat) => {
+ let target = scrollTarget();
let height = target.clientHeight;
let x = target.scrollLeft;
let y = target.scrollTop + height * count;
- target.scrollTo(x, y);
+ scroller(target, smooth, repeat).scroll(x, y);
};
-const scrollTop = (win) => {
- let target = scrollTarget(win);
+const scrollTop = (smooth, repeat) => {
+ let target = scrollTarget();
let x = target.scrollLeft;
let y = 0;
- target.scrollTo(x, y);
+ scroller(target, smooth, repeat).scroll(x, y);
};
-const scrollBottom = (win) => {
- let target = scrollTarget(win);
+const scrollBottom = (smooth, repeat) => {
+ let target = scrollTarget();
let x = target.scrollLeft;
let y = target.scrollHeight;
- target.scrollTo(x, y);
+ scroller(target, smooth, repeat).scroll(x, y);
};
-const scrollHome = (win) => {
- let target = scrollTarget(win);
+const scrollHome = (smooth, repeat) => {
+ let target = scrollTarget();
let x = 0;
let y = target.scrollTop;
- target.scrollTo(x, y);
+ scroller(target, smooth, repeat).scroll(x, y);
};
-const scrollEnd = (win) => {
- let target = scrollTarget(win);
+const scrollEnd = (smooth, repeat) => {
+ let target = scrollTarget();
let x = target.scrollWidth;
let y = target.scrollTop;
- target.scrollTo(x, y);
+ scroller(target, smooth, repeat).scroll(x, y);
};
export {
diff --git a/src/content/urls.js b/src/content/urls.js
index 8f8a1ac..9b7b284 100644
--- a/src/content/urls.js
+++ b/src/content/urls.js
@@ -1,3 +1,5 @@
+import messages from 'shared/messages';
+
const yank = (win) => {
let input = win.document.createElement('input');
win.document.body.append(input);
@@ -12,4 +14,26 @@ const yank = (win) => {
input.remove();
};
-export { yank };
+const paste = (win, newTab) => {
+ let textarea = win.document.createElement('textarea');
+ win.document.body.append(textarea);
+
+ textarea.style.position = 'fixed';
+ textarea.style.top = '-100px';
+ textarea.contentEditable = 'true';
+ textarea.focus();
+
+ if (win.document.execCommand('paste')) {
+ if (/^(https?|ftp):\/\//.test(textarea.textContent)) {
+ browser.runtime.sendMessage({
+ type: messages.OPEN_URL,
+ url: textarea.textContent,
+ newTab: newTab ? newTab : false,
+ });
+ }
+ }
+
+ textarea.remove();
+};
+
+export { yank, paste };
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/settings/components/form/keymaps-form.jsx b/src/settings/components/form/keymaps-form.jsx
index f64320c..eb77e52 100644
--- a/src/settings/components/form/keymaps-form.jsx
+++ b/src/settings/components/form/keymaps-form.jsx
@@ -8,12 +8,14 @@ const KeyMapFields = [
['scroll.vertically?{"count":-1}', 'Scroll up'],
['scroll.horizonally?{"count":-1}', 'Scroll left'],
['scroll.horizonally?{"count":1}', 'Scroll right'],
- ['scroll.home', 'Scroll leftmost'],
- ['scroll.end', 'Scroll last'],
+ ['scroll.home', 'Scroll to leftmost'],
+ ['scroll.end', 'Scroll to rightmost'],
+ ['scroll.top', 'Scroll to top'],
+ ['scroll.bottom', 'Scroll to bottom'],
['scroll.pages?{"count":-0.5}', 'Scroll up by half of screen'],
- ['scroll.pages?{"count":0.5}', 'Scroll up by half of screen'],
+ ['scroll.pages?{"count":0.5}', 'Scroll down by half of screen'],
['scroll.pages?{"count":-1}', 'Scroll up by a screen'],
- ['scroll.pages?{"count":1}', 'Scroll up by a screen'],
+ ['scroll.pages?{"count":1}', 'Scroll down by a screen'],
], [
['tabs.close', 'Close a tab'],
['tabs.reopen', 'Reopen closed tab'],
@@ -21,7 +23,8 @@ const KeyMapFields = [
['tabs.prev?{"count":1}', 'Select prev Tab'],
['tabs.first', 'Select first tab'],
['tabs.last', 'Select last tab'],
- ['tabs.reload?{"cache":true}', 'Reload current tab'],
+ ['tabs.reload?{"cache":false}', 'Reload current tab'],
+ ['tabs.reload?{"cache":true}', 'Reload with no caches'],
['tabs.pin.toggle', 'Toggle pinned state'],
['tabs.duplicate', 'Dupplicate a tab'],
], [
@@ -49,12 +52,16 @@ const KeyMapFields = [
], [
['addon.toggle.enabled', 'Enable or disable'],
['urls.yank', 'Copy current URL'],
+ ['urls.paste?{"newTab":false}', 'Open clipboard\'s URL in current tab'],
+ ['urls.paste?{"newTab":true}', 'Open clipboard\'s URL in new tab'],
['zoom.in', 'Zoom-in'],
['zoom.out', 'Zoom-out'],
['zoom.neutral', 'Reset zoom level'],
]
];
+const AllowdOps = [].concat(...KeyMapFields.map(group => group.map(e => e[0])));
+
class KeymapsForm extends Component {
render() {
@@ -62,10 +69,10 @@ class KeymapsForm extends Component {
if (!values) {
values = {};
}
- return <div className='keymap-fields'>
+ return <div className='form-keymaps-form'>
{
KeyMapFields.map((group, index) => {
- return <div key={index} className='form-keymaps-form'>
+ return <div key={index} className='form-keymaps-form-field-group'>
{
group.map((field) => {
let name = field[0];
@@ -96,4 +103,6 @@ class KeymapsForm extends Component {
}
}
+KeymapsForm.AllowdOps = AllowdOps;
+
export default KeymapsForm;
diff --git a/src/settings/components/form/keymaps-form.scss b/src/settings/components/form/keymaps-form.scss
index 3a83910..1a4e5cd 100644
--- a/src/settings/components/form/keymaps-form.scss
+++ b/src/settings/components/form/keymaps-form.scss
@@ -1,9 +1,11 @@
.form-keymaps-form {
column-count: 3;
- .keymap-fields-group {
+
+ &-field-group {
margin-top: 24px;
}
- .keymap-fields-group:first-of-type {
- margin-top: 0;
+
+ &-field-group:first-of-type {
+ margin-top: 24px;
}
}
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 <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;
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 3961982..d7696a1 100644
--- a/src/settings/components/index.jsx
+++ b/src/settings/components/index.jsx
@@ -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>;
}
@@ -123,6 +133,18 @@ class SettingsComponent extends Component {
}
}
+ validateValue(e) {
+ let next = Object.assign({}, this.state);
+
+ next.errors.json = '';
+ try {
+ this.validate(e.target);
+ } catch (err) {
+ next.errors.json = err.message;
+ }
+ next.settings[e.target.name] = e.target.value;
+ }
+
bindForm(name, value) {
let next = Object.assign({}, this.state, {
settings: Object.assign({}, this.state.settings, {
@@ -136,15 +158,54 @@ class SettingsComponent extends Component {
bindValue(e) {
let next = Object.assign({}, this.state);
+ let error = false;
next.errors.json = '';
try {
this.validate(e.target);
} catch (err) {
next.errors.json = err.message;
+ error = true;
}
next.settings[e.target.name] = e.target.value;
+ this.setState(this.state);
+ if (!error) {
+ this.context.store.dispatch(settingActions.save(next.settings));
+ }
+ }
+
+ migrateToForm() {
+ let b = window.confirm(DO_YOU_WANT_TO_CONTINUE);
+ if (!b) {
+ this.setState(this.state);
+ return;
+ }
+ try {
+ validator.validate(JSON.parse(this.state.settings.json));
+ } catch (err) {
+ this.setState(this.state);
+ return;
+ }
+
+ let form = settingsValues.formFromJson(
+ this.state.settings.json, KeymapsForm.AllowdOps);
+ let next = Object.assign({}, this.state);
+ next.settings.form = form;
+ next.settings.source = 'form';
+ next.errors.json = '';
+
+ this.setState(next);
+ this.context.store.dispatch(settingActions.save(next.settings));
+ }
+
+ migrateToJson() {
+ let json = settingsValues.jsonFromForm(this.state.settings.form);
+ let next = Object.assign({}, this.state);
+ next.settings.json = json;
+ next.settings.source = 'json';
+ next.errors.json = '';
+
this.setState(next);
this.context.store.dispatch(settingActions.save(next.settings));
}
@@ -153,23 +214,11 @@ class SettingsComponent extends Component {
let from = this.state.settings.source;
let to = e.target.value;
- let next = Object.assign({}, this.state);
if (from === 'form' && to === 'json') {
- next.settings.json =
- settingsValues.jsonFromForm(this.state.settings.form);
+ this.migrateToJson();
} else if (from === 'json' && to === 'form') {
- let b = window.confirm(DO_YOU_WANT_TO_CONTINUE);
- if (!b) {
- this.setState(this.state);
- return;
- }
- next.settings.form =
- settingsValues.formFromJson(this.state.settings.json);
+ this.migrateToForm();
}
- next.settings.source = to;
-
- this.setState(next);
- this.context.store.dispatch(settingActions.save(next.settings));
}
}
diff --git a/src/shared/commands.js b/src/shared/commands.js
deleted file mode 100644
index 8edeb5c..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 = encodeURI(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('{}', 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/index.js b/src/shared/commands/index.js
new file mode 100644
index 0000000..78cb4df
--- /dev/null
+++ b/src/shared/commands/index.js
@@ -0,0 +1,3 @@
+import complete from './complete';
+
+export { complete };
diff --git a/src/shared/commands/parsers.js b/src/shared/commands/parsers.js
new file mode 100644
index 0000000..fb37d2a
--- /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 === 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 };
diff --git a/src/shared/operations.js b/src/shared/operations.js
index 235793a..19466df 100644
--- a/src/shared/operations.js
+++ b/src/shared/operations.js
@@ -50,8 +50,9 @@ export default {
ZOOM_OUT: 'zoom.out',
ZOOM_NEUTRAL: 'zoom.neutral',
- // Url yank
+ // Url yank/paste
URLS_YANK: 'urls.yank',
+ URLS_PASTE: 'urls.paste',
// Find
FIND_START: 'find.start',
diff --git a/src/shared/settings/default.js b/src/shared/settings/default.js
index a9c363b..e81df2b 100644
--- a/src/shared/settings/default.js
+++ b/src/shared/settings/default.js
@@ -15,8 +15,6 @@ export default {
"j": { "type": "scroll.vertically", "count": 1 },
"h": { "type": "scroll.horizonally", "count": -1 },
"l": { "type": "scroll.horizonally", "count": 1 },
- "<C-Y>": { "type": "scroll.vertically", "count": -1 },
- "<C-E>": { "type": "scroll.vertically", "count": 1 },
"<C-U>": { "type": "scroll.pages", "count": -0.5 },
"<C-D>": { "type": "scroll.pages", "count": 0.5 },
"<C-B>": { "type": "scroll.pages", "count": -1 },
@@ -47,6 +45,8 @@ export default {
"gu": { "type": "navigate.parent" },
"gU": { "type": "navigate.root" },
"y": { "type": "urls.yank" },
+ "p": { "type": "urls.paste", "newTab": false },
+ "P": { "type": "urls.paste", "newTab": true },
"/": { "type": "find.start" },
"n": { "type": "find.next" },
"N": { "type": "find.prev" },
@@ -62,71 +62,8 @@ export default {
"twitter": "https://twitter.com/search?q={}",
"wikipedia": "https://en.wikipedia.org/w/index.php?search={}"
}
+ },
+ "properties": {
}
}`,
-
- 'form': {
- 'keymaps': {
- 'scroll.vertically?{"count":1}': 'j',
- 'scroll.vertically?{"count":-1}': 'k',
- 'scroll.horizonally?{"count":-1}': 'h',
- 'scroll.horizonally?{"count":1}': 'l',
- 'scroll.home': '0',
- 'scroll.end': '$',
- 'scroll.pages?{"count":-0.5}': '<C-U>',
- 'scroll.pages?{"count":0.5}': '<C-D>',
- 'scroll.pages?{"count":-1}': '<C-B>',
- 'scroll.pages?{"count":1}': '<C-F>',
-
- 'tabs.close': 'd',
- 'tabs.reopen': 'u',
- 'tabs.next?{"count":1}': 'J',
- 'tabs.prev?{"count":1}': 'K',
- 'tabs.first': 'g0',
- 'tabs.last': 'g$',
- 'tabs.reload?{"cache":true}': 'r',
- 'tabs.pin.toggle': 'zp',
- 'tabs.duplicate': 'zd',
-
- 'follow.start?{"newTab":false}': 'f',
- 'follow.start?{"newTab":true}': 'F',
- 'navigate.history.prev': 'H',
- 'navigate.history.next': 'L',
- 'navigate.link.next': ']]',
- 'navigate.link.prev': '[[',
- 'navigate.parent': 'gu',
- 'navigate.root': 'gU',
-
- 'find.start': '/',
- 'find.next': 'n',
- 'find.prev': 'N',
-
- 'command.show': ':',
- 'command.show.open?{"alter":false}': 'o',
- 'command.show.open?{"alter":true}': 'O',
- 'command.show.tabopen?{"alter":false}': 't',
- 'command.show.tabopen?{"alter":true}': 'T',
- 'command.show.winopen?{"alter":false}': 'w',
- 'command.show.winopen?{"alter":true}': 'W',
- 'command.show.buffer': 'b',
-
- 'addon.toggle.enabled': '<S-Esc>',
- 'urls.yank': 'y',
- 'zoom.in': 'zi',
- 'zoom.out': 'zo',
- 'zoom.neutral': 'zz',
- },
- 'search': {
- 'default': 'google',
- 'engines': [
- ['google', 'https,//google.com/search?q={}'],
- ['yahoo', 'https,//search.yahoo.com/search?p={}'],
- ['bing', 'https,//www.bing.com/search?q={}'],
- ['duckduckgo', 'https,//duckduckgo.com/?q={}'],
- ['twitter', 'https,//twitter.com/search?q={}'],
- ['wikipedia', 'https,//en.wikipedia.org/w/index.php?search={}'],
- ]
- },
- 'blacklist': [],
- }
};
diff --git a/src/shared/settings/properties.js b/src/shared/settings/properties.js
new file mode 100644
index 0000000..37dc881
--- /dev/null
+++ b/src/shared/settings/properties.js
@@ -0,0 +1,16 @@
+// describe types of a propety as:
+// mystr: 'string',
+// mynum: 'number',
+// mybool: 'boolean',
+const types = {
+ hintchars: 'string',
+ smoothscroll: 'boolean',
+};
+
+// describe default values of a property
+const defaults = {
+ hintchars: 'abcdefghijklmnopqrstuvwxyz',
+ smoothscroll: false,
+};
+
+export { types, defaults };
diff --git a/src/shared/settings/storage.js b/src/shared/settings/storage.js
new file mode 100644
index 0000000..25ebfcd
--- /dev/null
+++ b/src/shared/settings/storage.js
@@ -0,0 +1,33 @@
+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 Object.assign({},
+ settingsValues.valueFromJson(DefaultSettings.json),
+ value);
+ });
+};
+
+const save = (settings) => {
+ return browser.storage.local.set({
+ settings,
+ });
+};
+
+export { loadRaw, loadValue, save };
diff --git a/src/shared/validators/setting.js b/src/shared/settings/validator.js
index 949ab29..1589420 100644
--- a/src/shared/validators/setting.js
+++ b/src/shared/settings/validator.js
@@ -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 };
diff --git a/src/shared/settings/values.js b/src/shared/settings/values.js
index 4482fbb..bd03be2 100644
--- a/src/shared/settings/values.js
+++ b/src/shared/settings/values.js
@@ -1,4 +1,4 @@
-import DefaultSettings from './default';
+import * as properties from './properties';
const operationFromFormName = (name) => {
let [type, argStr] = name.split('?');
@@ -46,25 +46,28 @@ const valueFromForm = (form) => {
}
}
- let blacklist = form.blacklist;
-
- return { keymaps, search, blacklist };
+ return {
+ keymaps,
+ search,
+ blacklist: form.blacklist,
+ properties: form.properties
+ };
};
const jsonFromValue = (value) => {
return JSON.stringify(value, undefined, 2);
};
-const formFromValue = (value) => {
-
+const formFromValue = (value, allowedOps) => {
let keymaps = undefined;
+
if (value.keymaps) {
- let allowedOps = new Set(Object.keys(DefaultSettings.form.keymaps));
+ let allowedSet = new Set(allowedOps);
keymaps = {};
for (let keys of Object.keys(value.keymaps)) {
let op = operationToFormName(value.keymaps[keys]);
- if (allowedOps.has(op)) {
+ if (allowedSet.has(op)) {
keymaps[op] = keys;
}
}
@@ -80,18 +83,23 @@ const formFromValue = (value) => {
}
}
- 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) => {
return jsonFromValue(valueFromForm(form));
};
-const formFromJson = (json) => {
+const formFromJson = (json, allowedOps) => {
let value = valueFromJson(json);
- return formFromValue(value);
+ return formFromValue(value, allowedOps);
};
export {
diff --git a/src/shared/utils/keys.js b/src/shared/utils/keys.js
index fba8ce3..8a86dfb 100644
--- a/src/shared/utils/keys.js
+++ b/src/shared/utils/keys.js
@@ -18,6 +18,7 @@ const fromKeyboardEvent = (e) => {
return {
key: modifierdKeyName(e.key),
+ repeat: e.repeat,
shiftKey: shift,
ctrlKey: e.ctrlKey,
altKey: e.altKey,
diff --git a/test/background/reducers/setting.test.js b/test/background/reducers/setting.test.js
new file mode 100644
index 0000000..2ef98cb
--- /dev/null
+++ b/test/background/reducers/setting.test.js
@@ -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');
+ });
+});
diff --git a/test/content/navigates.test.js b/test/content/navigates.test.js
index d8a3316..f1f0741 100644
--- a/test/content/navigates.test.js
+++ b/test/content/navigates.test.js
@@ -53,7 +53,7 @@ describe('navigates module', () => {
));
it('navigates to <a> elements whose text matches "previous"', testPrev(
- '<a href="#dummy">preview</a><a href="#prev">go to previous</a>'
+ '<a href="#dummy">previously</a><a href="#prev">previous page</a>'
));
it('navigates to <a> elements whose decoded text matches "<<"', testPrev(
@@ -119,11 +119,11 @@ describe('navigates module', () => {
));
it('prefers link[rel~=next] to a[rel~=next]', testNext(
- '<a rel="next" href="#dummy">click me<><link rel="next" href="#next" />'
+ '<a rel="next" href="#dummy">click me</a><link rel="next" href="#next" />'
));
it('prefers a[rel~=next] to a::text(pattern)', testNext(
- '<a href="#dummy">go to next</a><a rel="next" href="#next">click me</a>'
+ '<a href="#dummy">next page</a><a rel="next" href="#next">click me</a>'
));
});
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(<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();
+ });
+ });
+});
diff --git a/test/shared/commands/parsers.test.js b/test/shared/commands/parsers.test.js
new file mode 100644
index 0000000..0a1960c
--- /dev/null
+++ b/test/shared/commands/parsers.test.js
@@ -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', []]);
+ });
+ });
+});
diff --git a/test/shared/validators/setting.test.js b/test/shared/settings/validator.test.js
index 15d6a10..61d976a 100644
--- a/test/shared/validators/setting.test.js
+++ b/test/shared/settings/validator.test.js
@@ -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", () => {
diff --git a/test/shared/settings/values.test.js b/test/shared/settings/values.test.js
index 2c222b6..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,17 +113,27 @@ 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 form = values.formFromValue(value);
+ let allowed = ['scroll.vertically?{"count":1}', 'scroll.home' ];
+ let form = values.formFromValue(value, allowed);
expect(form.keymaps).to.have.property('scroll.vertically?{"count":1}', 'j');
+ expect(form.keymaps).to.not.have.property('scroll.vertically?{"count":100}');
expect(form.keymaps).to.have.property('scroll.home', '0');
expect(Object.keys(form.keymaps)).to.have.lengthOf(2);
expect(form.search).to.have.property('default', 'google');
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);
});
});
});