Full Code of lusakasa/saka for AI

master 97e1d6fbd2c6 cached
114 files
181.7 KB
53.0k tokens
90 symbols
1 requests
Download .txt
Showing preview only (210K chars total). Download the full file or copy to clipboard to get everything.
Repository: lusakasa/saka
Branch: master
Commit: 97e1d6fbd2c6
Files: 114
Total size: 181.7 KB

Directory structure:
gitextract_snas81cp/

├── .babelrc
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .travis.yml
├── .vscode/
│   └── launch.json
├── LICENSE
├── README.md
├── docs/
│   └── pull_request_template.md
├── images/
│   └── favicons/
│       ├── browserconfig.xml
│       └── manifest.json
├── jest.config.js
├── jest.transform.js
├── manifest/
│   ├── chrome.json
│   ├── common.json
│   └── firefox.json
├── package.json
├── spec/
│   └── support/
│       └── jasmine.json
├── src/
│   ├── background_page/
│   │   ├── index.js
│   │   └── tabHistory.js
│   ├── content_script/
│   │   └── toggle_saka.js
│   ├── lib/
│   │   ├── colors.js
│   │   ├── dom.js
│   │   ├── highlight.jsx
│   │   ├── log.js
│   │   ├── tld.js
│   │   ├── trie.js
│   │   ├── url.js
│   │   └── utils.js
│   ├── msg/
│   │   ├── client.js
│   │   └── server.js
│   ├── options/
│   │   ├── Main/
│   │   │   ├── MainOptions.jsx
│   │   │   ├── OptionsList/
│   │   │   │   ├── DefaultModeSelection.jsx
│   │   │   │   ├── EnableFuzzySearch.jsx
│   │   │   │   ├── OnlyShowSearchBarSelector.jsx
│   │   │   │   ├── ShowSakaHotkeys.jsx
│   │   │   │   └── index.jsx
│   │   │   └── SakaHotkeysList/
│   │   │       ├── HotkeyListRow.jsx
│   │   │       └── index.jsx
│   │   └── saka-options.jsx
│   ├── saka/
│   │   ├── Main/
│   │   │   ├── Components/
│   │   │   │   ├── BackgroundImage/
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── GUIContainer/
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── Icon/
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── ModeSwitcher/
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── PaginationBar/
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── SearchBar/
│   │   │   │   │   ├── Button/
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── Input/
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── SettingsBar/
│   │   │   │   │   └── index.jsx
│   │   │   │   └── SuggestionList/
│   │   │   │       ├── Components/
│   │   │   │       │   └── Suggestion/
│   │   │   │       │       └── index.jsx
│   │   │   │       ├── Containers/
│   │   │   │       │   ├── BookmarkSuggestion/
│   │   │   │       │   │   └── index.jsx
│   │   │   │       │   ├── ClosedTabSuggestion/
│   │   │   │       │   │   └── index.jsx
│   │   │   │       │   ├── CommandSuggestion/
│   │   │   │       │   │   └── index.jsx
│   │   │   │       │   ├── HistorySuggestion/
│   │   │   │       │   │   └── index.jsx
│   │   │   │       │   ├── RecentlyViewedSuggestion/
│   │   │   │       │   │   └── index.jsx
│   │   │   │       │   ├── SearchEngineSuggestion/
│   │   │   │       │   │   └── index.jsx
│   │   │   │       │   ├── SuggestionSelector.jsx
│   │   │   │       │   ├── TabSuggestion/
│   │   │   │       │   │   └── index.jsx
│   │   │   │       │   └── UnknownSuggestion/
│   │   │   │       │       └── index.jsx
│   │   │   │       └── index.jsx
│   │   │   ├── Containers/
│   │   │   │   ├── GeneralSearch/
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── StandardSearch/
│   │   │   │   │   └── index.jsx
│   │   │   │   └── TabSearch/
│   │   │   │       └── index.jsx
│   │   │   └── index.jsx
│   │   └── index.jsx
│   ├── scss/
│   │   ├── options.scss
│   │   └── styles.scss
│   ├── suggestion_engine/
│   │   ├── client/
│   │   │   └── index.js
│   │   └── server/
│   │       ├── index.js
│   │       └── providers/
│   │           ├── bookmark.js
│   │           ├── closedTab.js
│   │           ├── command.js
│   │           ├── history.js
│   │           ├── index.js
│   │           ├── mode.js
│   │           ├── recentlyViewed.js
│   │           ├── searchEngine.js
│   │           └── tab.js
│   └── suggestion_utils/
│       └── index.js
├── static/
│   ├── background_page.html
│   ├── material-icons.css
│   ├── options.html
│   └── saka.html
├── test/
│   ├── Icon.test.js
│   ├── Main.test.js
│   ├── ModeSwitcher.test.js
│   ├── PaginationBar.test.js
│   ├── SearchBar.test.js
│   ├── StandardSearch/
│   │   └── StandardSearch.test.js
│   ├── Suggestion.test.js
│   ├── SuggestionList.test.js
│   ├── __mocks__/
│   │   ├── browser-mocks.js
│   │   └── styleMock.scss
│   ├── __snapshots__/
│   │   ├── ModeSwitcher.test.js.snap
│   │   ├── SearchBar.test.js.snap
│   │   ├── Suggestion.test.js.snap
│   │   └── SuggestionList.test.js.snap
│   ├── lib/
│   │   ├── hightlight.test.js
│   │   ├── log.test.js
│   │   ├── url.test.js
│   │   └── utils.test.js
│   ├── options/
│   │   ├── MainOptions.test.js
│   │   └── __snapshots__/
│   │       └── MainOptions.test.js.snap
│   ├── suggestion_engine/
│   │   ├── providers/
│   │   │   ├── bookmark.test.js
│   │   │   ├── closedTab.test.js
│   │   │   ├── history.test.js
│   │   │   ├── mode.test.js
│   │   │   ├── recentlyViewed.test.js
│   │   │   └── tab.test.js
│   │   └── server/
│   │       └── index.test.js
│   └── suggestion_utils/
│       └── index.test.js
└── webpack.config.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .babelrc
================================================
{
  "sourceMaps": true,
  "plugins": [
    ["transform-react-jsx", { "pragma": "h" }],
    ["transform-object-rest-spread"],
    ["transform-class-properties"]
  ]
}


================================================
FILE: .eslintignore
================================================
test/*

================================================
FILE: .eslintrc.json
================================================
{
  "parser": "babel-eslint",
  "plugins": ["prettier"],
  "extends": ["airbnb", "prettier"],
  "rules": {
    "react/react-in-jsx-scope": "off",
    "import/no-extraneous-dependencies": "off",
    "react/prop-types": "off",
    "react/no-did-mount-set-state": "off",
    "jsx-a11y/no-noninteractive-element-interactions": "off",
    "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],
    "no-prototype-builtins": "off"
  },
  "env": {
    "webextensions": true,
    "browser": true
  },
  "globals": {
    "document": true,
    "navigator": false,
    "window": true,
    "SAKA_DEBUG": true,
    "SAKA_PLATFORM": true,
    "SAKA_VERSION": true,
    "SAKA_BENCHMARK": true
  },
  "settings": {
    "import/resolver": {
      "node": {
        "paths": ["src", "lib", "msg"],
        "extensions": ["js", "jsx"]
      }
    }
  }
}


================================================
FILE: .gitignore
================================================
dist
dist.zip
dist.crx
dist.pem
node_modules
npm-debug.log
debug.log
.DS_Store
yarn-error.log
coverage
saka-*.zip
package-lock.json


================================================
FILE: .prettierignore
================================================
package.json

================================================
FILE: .prettierrc
================================================
{
  "singleQuote": true
}


================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
  - lts/*

install:
  - yarn global add codecov
  - yarn install
script:
  - yarn test --coverage && codecov
  # auto-release from tagged commits
before_deploy:
  - yarn run build:chrome
  - yarn run zip saka-chrome-dev.zip dist/*
  - yarn run build:chrome:prod
  - yarn run zip saka-chrome-prod.zip dist/*
  - yarn run build:firefox
  - yarn run zip saka-firefox-dev.zip dist/*
  - yarn run build:firefox:prod
  - yarn run zip saka-firefox-prod.zip dist/*
deploy:
  provider: releases
  api_key:
    secure: UXpxguYgvL0clkrmjZw9KtLBgRCbXALVi4uz4nU7j96Qcobps2Ui5JFNJc243x2K0oHENYF3GN72j7zw3cclDeGL4/KXa/gOwM/LpHnAD3kJnKlDuVpUTARbp+dm8C2AvcwjtDT/qMY4L+wmfdUEaHKw3DHDJGVWnTEeC35CQ/uAzQJuMd8QGHSUNylY3pcBY2FPXMWaX9JlW7lHjTQW4CY/kBENKmloPrdB2WlnWXlNw7RgkqTk75zLPxUK3VyNW8qH3yInj0vlDD2mfQb4e2s+OjipcGK7Z82fqz4tLEjvi5w/9vIHv3WELF3W36J47LowWUskc+ET3VXULFa2+eMoxtpfyc07KNEVNN9oiEe7jh3zYmvaooq2baBmePBnntmZVPe3zggTvbn7fjqUnp+4oP+KatuPTeuR9QWRvehhO+CftN5m2q66D+L+M4HBQK4SwVt+JR59DMmDja/CfB72Zsz4nSTyFOLRieJLyiM+z4gcCdF78nhKywxrllAmSp+wixW0fnGoTRMyE1YchxSg/uwxvCTQhVZnnIKjm4lxD73VNURsPI7HOyvcy+kxkscB+E6glUQykJfCLdrZZaUa8doVWBI+XNR9IGPhOr+p5jT+w8BRFdwc7VPp4hRsIcWDKXDUMLasyEmQB0H9bgPw27d5R7Lkammud5Sv0Kw=
  file:
    - saka-chrome-dev.zip
    - saka-chrome-prod.zip
    - saka-firefox-dev.zip
    - saka-firefox-prod.zip
  skip_cleanup: true
  on:
    repo: lusakasa/saka
    branch: master
    tags: true


================================================
FILE: .vscode/launch.json
================================================
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Jest Tests",
      "type": "node",
      "request": "launch",
      "runtimeArgs": [
        "--inspect-brk",
        "${workspaceRoot}/node_modules/.bin/jest",
        "--runInBand"
      ],
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen",
      "port": 9229,
      "skipFiles": ["<node_internals>/**"]
    }
  ]
}


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2016 Sufyan Dawoodjee, Uzair Shamim

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================
FILE: README.md
================================================
# Saka [![GitHub license](https://img.shields.io/github/license/lusakasa/saka.svg)](https://github.com/lusakasa/saka/blob/master/LICENSE) [![Build Status](https://travis-ci.org/lusakasa/saka.svg?branch=master&style=popout-square)](https://travis-ci.org/lusakasa/saka) [![codecov.io Code Coverage](https://codecov.io/gh/lusakasa/saka/branch/master/graph/badge.svg?maxAge=2592000)](https://codecov.io/github/lusakasa/saka?branch=master) [![Chrome Web Store](https://img.shields.io/chrome-web-store/stars/nbdfpcokndmapcollfpjdpjlabnibjdi.svg)](https://chrome.google.com/webstore/detail/saka/nbdfpcokndmapcollfpjdpjlabnibjdi)

A browsing assistant for [Firefox](https://addons.mozilla.org/firefox/addon/saka/) and [Chrome](https://chrome.google.com/webstore/detail/saka/nbdfpcokndmapcollfpjdpjlabnibjdi) designed to be fast, intuitive, and beautiful. Inspired by Spotlight. Keyboard-focused but mouse friendly too.

* Lists tabs in order of recency by default, then fuzzy search by title or URL.
* Search recently closed tabs
* Search all bookmarks
* Search all browsing history
* Search all modes at once

<img src="./images/preview.png" width="70%" height="70%">


## Install

Install Saka from the [Firefox Marketplace](https://addons.mozilla.org/firefox/addon/saka/) or [Chrome Webstore](https://chrome.google.com/webstore/detail/saka/nbdfpcokndmapcollfpjdpjlabnibjdi).

## Development
See the [Getting Started](https://github.com/lusakasa/saka/wiki/Getting-Started) page on the project Wiki.

## Release Instructions (for maintainers)

1.  Update the version number in `manifest/common.json`

2.  Make a commit and set the message to the version: `git commit -m "v0.15.2"`

3.  Tag the commit with the version and a message describing changes since the last release: `git tag -a v0.15.2`

4.  Push the commit to github with tags: `git push origin --follow-tags`

5.  View the build status at https://travis-ci.org/lusakasa/saka/ and generated releases at https://github.com/lusakasa/saka/releases

## License

MIT Licensed, Copyright (c) 2017 Sufyan Dawoodjee, Uzair Shamim


================================================
FILE: docs/pull_request_template.md
================================================
## Type of Change
> Put an [x] for the relevant option
- [ ] Bugfix/Cleanup (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)

## Summary Of Changes
#### Why is this change needed?
#### Does this close any open issues?

## Checklist
- [ ] Tests are passing locally
- [ ] Updated the README/Wiki documentation (if relevant)

## Additional Comments
Add any additional comments you may have here.


================================================
FILE: images/favicons/browserconfig.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
    <msapplication>
        <tile>
            <square150x150logo src="/mstile-150x150.png"/>
            <TileColor>#da532c</TileColor>
        </tile>
    </msapplication>
</browserconfig>


================================================
FILE: images/favicons/manifest.json
================================================
{
    "name": "",
    "icons": [
        {
            "src": "/android-chrome-192x192.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "/android-chrome-512x512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ],
    "theme_color": "#ffffff",
    "background_color": "#ffffff",
    "display": "standalone"
}

================================================
FILE: jest.config.js
================================================
module.exports = {
  moduleNameMapper: {
    '@/(.*)$': '<rootDir>/src/$1',
    '^src/(.*)$': '<rootDir>/src/$1',
    '^suggestion_utils/(.*)$': '<rootDir>/src/suggestion_utils/$1',
    '^suggestion_engine/(.*)$': '<rootDir>/src/suggestion_engine/$1',
    '^lib/(.*)$': '<rootDir>/src/lib/$1',
    '^msg/(.*)$': '<rootDir>/src/msg/$1',
    '^.*\\.(css|less|sass|scss)$': '<rootDir>/test/__mocks__/styleMock.scss',
    '^react-dom/server$':
      '<rootDir>/node_modules/preact-render-to-string/dist/index.js',
    '^react-addons-test-utils$':
      '<rootDir>/node_modules/preact-test-utils/lib/index.js',
    '^react$': '<rootDir>/node_modules/preact-compat/lib/index.js',
    '^react-dom$': '<rootDir>/node_modules/preact-compat/lib/index.js'
  },
  moduleFileExtensions: ['js', 'jsx'],
  transform: {
    '^.+\\.(js|jsx)?$': '<rootDir>/jest.transform.js'
  },
  collectCoverage: true,
  collectCoverageFrom: ['src/**/*.{js,jsx}'],
  coverageThreshold: {
    global: {
      branches: 59,
      functions: 65,
      lines: 62
    }
  },
  coverageReporters: ['json', 'lcov', 'html'],
  setupFiles: ['<rootDir>/test/__mocks__/browser-mocks.js']
};


================================================
FILE: jest.transform.js
================================================
const babelOptions = {
  presets: [['env', { targets: { node: '8' } }], 'react'],
  plugins: [
    [
      'transform-react-jsx',
      {
        pragma: 'h'
      }
    ]
  ]
};
module.exports = require('babel-jest').createTransformer(babelOptions);


================================================
FILE: manifest/chrome.json
================================================
{
  "background": {
    "page": "background_page.html",
    "persistent": true
  },
  "commands": {
    "toggleSaka4": {
      "description": "Toggle Saka",
      "global": true
    }
  },
  "permissions": ["chrome://favicon/"],
  "key":
    "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsgFNwezxiHxK/AIKh9K4LVefUPIDisRzQLmmRBjFftr3yrxJuLBaO1LvMISsRlvbFmbZ0B6KHzyGDEnmOMIo3X9wUxBwSV+q3kigp3XFryllhOHbKcl+FUVMRQabBs49CS6ttq2l4Jx+ULEsmprqub+IV+cvY3slXxxuyMF0TjmnRwDYnSREgbfzK7g/msZ8e0gi+hrPETNVKOibbVwd8S7gSy0hDug71WOA338FGe08DiXpnVoKLL0fmd+TO6Z4fAhH+oSqr15WIt6qCglYmZi2BEydL0zhUcidkis0zJjNX5nQi0xzggmxwsMJyfmfGoniwsUg3rAy+M0v5gaSUQIDAQAB"
}


================================================
FILE: manifest/common.json
================================================
{
  "name": "Saka",
  "version": "0.17.3",
  "author": "Sufyan Dawoodjee, Uzair Shamim",
  "description": "Saka - elegent tab search, selection, and beyond",
  "manifest_version": 2,
  "options_ui": {
    "page": "options.html",
    "open_in_tab": true
  },
  "background": {
    "page": "background_page.html"
  },
  "commands": {
    "toggleSaka": {
      "suggested_key": {
        "default": "Ctrl+Space",
        "mac": "Alt+Space"
      },
      "description": "Toggle Saka"
    },
    "toggleSaka2": {
      "suggested_key": {
        "default": "Ctrl+E",
        "mac": "Command+E"
      },
      "description": "Toggle Saka"
    },
    "toggleSaka3": {
      "description": "Toggle Saka"
    },
    "toggleSaka4": {
      "suggested_key": {
        "default": "Ctrl+Shift+1"
      },
      "description": "Toggle Saka"
    }
  },
  "browser_action": {
    "default_icon": {
      "48": "logo.png"
    }
  },
  "icons": {
    "16": "logo.png",
    "48": "logo.png",
    "128": "logo.png"
  },
  "permissions": [
    "<all_urls>",
    "tabs",
    "sessions",
    "history",
    "bookmarks",
    "storage",
    "contextMenus"
  ],
  "web_accessible_resources": [
    "saka.html",
    "material-icons.css",
    "MaterialIcons-Regular.woff2"
  ]
}


================================================
FILE: manifest/firefox.json
================================================
{
  "applications": {
    "gecko": {
      "id": "{7d7cad35-2182-4457-972d-5a41a2051240}"
    }
  }
}


================================================
FILE: package.json
================================================
{
  "name": "saka",
  "description": "A keyboard interface to the web",
  "scripts": {
    "build": "echo \"You must specify the target browser (firefox or chrome). Example: npm run build:firefox\"",
    "build:prod": "echo \"You must specify the target browser (firefox or chrome). Example: npm run build:firefox:prod\"",
    "build:chrome": "npm run clean && webpack --config webpack.config.js --env=dev:chrome:benchmark --progress --colors",
    "build:chrome:prod": "npm run clean && webpack --config webpack.config.js --env=prod:chrome:nobenchmark --progress --colors",
    "build:firefox": "npm run clean && webpack --config webpack.config.js --env=dev:firefox:benchmark --progress --colors",
    "build:firefox:prod": "npm run clean && webpack --config webpack.config.js --env=prod:firefox:nobenchmark --progress --colors",
    "build:profile": "npm run clean && webpack --config webpack.config.js --progress --profile --colors && cp ./static/* ./dist",
    "zip": "bestzip",
    "clean": "rimraf dist",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage --watch --verbose false",
    "storybook": "start-storybook -p 9001 -c .storybook"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/lusakasa/saka.git"
  },
  "license": "MIT",
  "devDependencies": {
    "@material/ripple": "^0.38.1",
    "@types/chrome": "^0.0.75",
    "babel-core": "^6.26.0",
    "babel-eslint": "^8.2.6",
    "babel-jest": "^23.4.2",
    "babel-loader": "^7.1.0",
    "babel-plugin-transform-class-properties": "^6.23.0",
    "babel-plugin-transform-object-rest-spread": "^6.26.0",
    "babel-plugin-transform-react-jsx": "^6.23.0",
    "babel-preset-env": "^1.6.1",
    "babel-preset-react": "^6.24.1",
    "babili": "^0.1.4",
    "babili-webpack-plugin": "^0.1.2",
    "bestzip": "^2.1.2",
    "copy-webpack-plugin": "^4.0.1",
    "css-loader": "^1.0.0",
    "eslint": "^5.4.0",
    "eslint-config-airbnb": "^17.1.0",
    "eslint-config-prettier": "^3.0.1",
    "eslint-plugin-import": "^2.10.0",
    "eslint-plugin-jsx-a11y": "^6.0.3",
    "eslint-plugin-node": "^8.0.0",
    "eslint-plugin-prettier": "^3.0.0",
    "eslint-plugin-promise": "^4.0.0",
    "eslint-plugin-react": "^7.2.0",
    "eslint-plugin-standard": "^4.0.0",
    "extract-loader": "^3.0.0",
    "generate-json-webpack-plugin": "^0.3.1",
    "html-loader": "^0.5.5",
    "jest": "^23.5.0",
    "jest-dom": "^2.1.1",
    "markdown-loader": "^4.0.0",
    "node-sass": "^4.8.3",
    "preact-render-spy": "^1.2.2",
    "preact-render-to-string": "^4.1.0",
    "preact-test-utils": "^0.1.3",
    "preact-testing-library": "^0.3.0",
    "prettier": "^1.14.2",
    "rimraf": "^2.6.2",
    "sass-loader": "^7.1.0",
    "sinon-chrome": "^2.3.1",
    "style-loader": "^0.23.1",
    "webpack": "^4.17.0",
    "webpack-cli": "^3.1.0",
    "webpack-merge": "^4.1.4"
  },
  "dependencies": {
    "fuse.js": "^3.2.0",
    "marked": "^0.6.2",
    "material-components-web": "^0.38.2",
    "msgx": "^1.1.3",
    "preact": "^8.3.0",
    "preact-compat": "^3.18.0",
    "webextension-polyfill": "^0.4.0"
  }
}


================================================
FILE: spec/support/jasmine.json
================================================
{
  "spec_dir": "spec",
  "spec_files": [
    "**/*[sS]pec.js"
  ],
  "helpers": [
    "helpers/**/*.js"
  ],
  "stopSpecOnExpectationFailure": false,
  "random": false
}


================================================
FILE: src/background_page/index.js
================================================
import browser from 'webextension-polyfill';
import 'msg/server.js';
import { tabHistory, recentlyClosed } from './tabHistory.js';

window.tabHistory = tabHistory;
window.recentlyClosed = recentlyClosed;

let lastTabId;

async function toggleSaka(tabId) {
  if (SAKA_DEBUG) console.group('toggleSaka');
  // Get the specified tab, or the current tab if none is specified
  const currentTab =
    tabId === undefined
      ? (await browser.tabs.query({
          active: true,
          currentWindow: true
        }))[0]
      : await browser.tabs.get(tabId);
  if (currentTab) {
    // If the current tab is Saka, switch to the previous tab (if it exists) and close the current tab
    if (currentTab.url === browser.runtime.getURL('saka.html')) {
      if (lastTabId) {
        try {
          const lastTab = await browser.tabs.get(lastTabId);
          if (lastTab) {
            try {
              await browser.tabs.update(lastTabId, { active: true });
              if (SAKA_DEBUG) console.log(`Switched to tab ${lastTab.url}`);
            } catch (e) {
              if (SAKA_DEBUG)
                console.error(`Failed to switch to tab ${lastTab.url}`);
            }
          }
          lastTabId = undefined;
        } catch (e) {
          if (SAKA_DEBUG)
            console.error(
              `Cannot return to tab ${lastTabId} because it no longer exists`
            );
        }
      }
      try {
        await browser.tabs.remove(currentTab.id);
        if (SAKA_DEBUG) console.log(`Removed tab ${currentTab.url}`);
      } catch (e) {
        if (SAKA_DEBUG) console.error(`Failed to remove tab ${currentTab.url}`);
      }
      // Otherwise, try to load Saka into the current tab
    } else {
      try {
        await browser.tabs.executeScript(currentTab.id, {
          file: '/toggle_saka.js',
          runAt: 'document_start',
          matchAboutBlank: true
        });
        if (SAKA_DEBUG) console.log(`Loaded Saka into tab ${currentTab.url}`);
        // If loading Saka into the current tab fails, create a new tab
      } catch (e) {
        try {
          const screenshot = await browser.tabs.captureVisibleTab();
          await browser.storage.local.set({ screenshot });
        } catch (screenshotError) {
          if (SAKA_DEBUG)
            console.error('Failed to capture visible tab: ', screenshotError);
        }
        lastTabId = currentTab.id;
        await browser.tabs.create({
          url: '/saka.html',
          index: currentTab.index,
          active: false
        });
        if (SAKA_DEBUG)
          console.warn(
            `Failed to execute Saka into tab. Instead, created new Saka tab after ${
              currentTab.url
            }`
          );
      }
    }
    // If tab couldn't be found (e.g. because query was made from devtools) create a new tab
  } else {
    await browser.tabs.create({
      url: '/saka.html'
    });
    if (SAKA_DEBUG)
      console.log("Couldn't find tab. Instead, created new Saka tab.");
  }
  const window = await browser.windows.getLastFocused();
  await browser.windows.update(window.id, { focused: true });
  if (SAKA_DEBUG) console.groupEnd();
}

async function closeSaka(tab) {
  if (tab) {
    if (tab.url === browser.runtime.getURL('saka.html')) {
      await browser.tabs.remove(tab.id);
    } else {
      await browser.tabs.executeScript(tab.id, {
        file: '/toggle_saka.js',
        runAt: 'document_start',
        matchAboutBlank: true
      });
    }
  }
}

async function saveSettings(searchHistory) {
  await browser.storage.sync.set({ searchHistory: [...searchHistory] });
}

browser.browserAction.onClicked.addListener(() => {
  toggleSaka();
});

browser.commands.onCommand.addListener(command => {
  switch (command) {
    case 'toggleSaka':
    case 'toggleSaka2':
    case 'toggleSaka3':
    case 'toggleSaka4':
      toggleSaka();
      break;
    default:
      console.error(`Unknown command: '${command}'`);
  }
});

browser.runtime.onMessage.addListener(async (message, sender) => {
  switch (message.key) {
    case 'toggleSaka':
      toggleSaka();
      break;
    case 'closeSaka':
      await saveSettings(message.searchHistory);
      closeSaka(sender.tab);
      break;
    default:
      console.error(`Unknown message: '${message}'`);
  }
});

browser.runtime.onMessageExternal.addListener(message => {
  switch (message) {
    case 'toggleSaka':
      toggleSaka();
      break;
    default:
      console.error(`Unknown message: '${message}'`);
  }
});

browser.contextMenus.create({
  title: 'Saka',
  contexts: ['all'],
  onclick: () => toggleSaka()
});


================================================
FILE: src/background_page/tabHistory.js
================================================
import browser from 'webextension-polyfill';

// list of tab ids in order of increasing age since last visit
export const tabHistory = [];

// list of tab ids in order of increasing age since closed
export const recentlyClosed = [];

const log = listener => (...args) => {
  listener(...args);
  // if (SAKA_DEBUG) console.log(tabHistory);
};

function setMostRecentTab(tabInfo) {
  const tabIndex = tabHistory.findIndex(tab => tab.tabId === tabInfo.tabId);

  if (tabIndex !== -1) {
    tabHistory.splice(tabIndex, 1);
  }
  tabHistory.unshift(tabInfo);
}

function setMostRecentClosedTab(tabInfo) {
  const tabIndex = recentlyClosed.findIndex(tab => tab.tabId === tabInfo.tabId);

  if (tabIndex !== -1) {
    recentlyClosed.splice(tabIndex, 1);
  }
  recentlyClosed.unshift(tabInfo);
}

browser.tabs.onActivated.addListener(
  log(({ tabId }) => {
    setMostRecentTab({ tabId, lastAccessed: Date.now() });
  })
);

browser.tabs.onRemoved.addListener(
  log(tabId => {
    const i = tabHistory.findIndex(tab => tab.tabId === tabId);
    tabHistory.splice(i, 1);
    setMostRecentClosedTab({ tabId, lastAccessed: Date.now() });
  })
);

browser.tabs.onReplaced.addListener(
  log((addedTabId, removedTabId) => {
    const i = tabHistory.findIndex(tab => tab.tabId === removedTabId);
    tabHistory[i] = { tabId: addedTabId, lastAccessed: Date.now() };
  })
);

browser.windows.onFocusChanged.addListener(async windowId => {
  const [tab] = await browser.tabs.query({ currentWindow: true, active: true });
  if (tab && tab.windowId === windowId) {
    setMostRecentTab({ tabId: tab.id, lastAccessed: Date.now() });
  }
});


================================================
FILE: src/content_script/toggle_saka.js
================================================
// this file is dynamically loaded by the event page into the active tab of the active window
// Search for an existing Saka
//   * if found, remove it
//   * if not found, create and show it

const oldSakaRoot = document.querySelector('#saka-root');
if (oldSakaRoot) {
  if (SAKA_DEBUG) console.log('REMOVING SAKA');
  oldSakaRoot.remove();
} else {
  if (SAKA_DEBUG) console.log('APPENDING SAKA');
  // create container div
  const newSakaRoot = document.createElement('div');
  newSakaRoot.id = 'saka-root';
  newSakaRoot.style = `position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 2147483647;
  opacity: 1;
  pointer-events: none;`;
  // create Saka iframe
  const iframe = document.createElement('iframe');
  iframe.id = 'saka';
  iframe.src = chrome.runtime.getURL('saka.html');
  iframe.style = `z-index: 2147483647;
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  border-width: 0;
  pointer-events: all;
  }`;
  iframe.frameBorder = 0;
  // mount to DOM
  newSakaRoot.appendChild(iframe);
  document.documentElement.appendChild(newSakaRoot);
}


================================================
FILE: src/lib/colors.js
================================================
export const colors = {
  red: 'rgba(228,26,28,1)',
  black: 'rgba(0,0,0,1)',
  blue: 'rgba(55,126,184,1)',
  green: 'rgba(77,175,74,1)',
  purple: 'rgba(152,78,163,1)',
  orange: 'rgba(255,127,0,1)',
  yellow: 'rgba(255,255,51,1)',
  brown: 'rgba(166,86,40,1)',
  pink: 'rgba(247,129,191,1)',
  teal: 'rgb(0,77,64,1)',
  gray: 'rgba(153,153,153,1)'
};

export const fadedColors = {
  red: 'rgba(228,26,28,0.44)',
  black: 'rgba(0,0,0,0.44)',
  blue: 'rgba(55,126,184,0.44)',
  green: 'rgba(77,175,74,0.44)',
  purple: 'rgba(152,78,163,0.44)',
  orange: 'rgba(255,127,0,0.44)',
  yellow: 'rgba(255,255,51,0.44)',
  brown: 'rgba(166,86,40,0.44)',
  pink: 'rgba(247,129,191,0.44)',
  teal: 'rgb(0,77,64,0.44)',
  gray: 'rgba(153,153,153,0.44)'
};

export const colorMap = {
  tab: colors.blue,
  closedTab: colors.black,
  search: colors.green,
  history: colors.red,
  recentlyViewed: colors.purple,
  bookmark: colors.orange,
  mode: colors.purple,
  // dictionary: colors.purple,
  calculator: colors.brown,
  // command: colors.pink,
  unknown: colors.gray
};

export const fadedColorMap = {
  tab: fadedColors.blue,
  closedTab: fadedColors.black,
  search: fadedColors.green,
  history: fadedColors.red,
  recentlyViewed: fadedColors.purple,
  bookmark: fadedColors.orange,
  mode: fadedColors.purple,
  // dictionary: colors.purple,
  calculator: fadedColors.brown,
  // command: fadedColors.pink,
  unknown: fadedColors.gray
};


================================================
FILE: src/lib/dom.js
================================================
export function slowWheelEvent(
  threshold,
  onPositiveThreshold,
  onNegativeThreshold,
  value = 0
) {
  return e => {
    e.preventDefault();
    let val = value;
    val += e.deltaY;
    if (val >= threshold) {
      val = 0;
      onPositiveThreshold(e);
    } else if (val <= -threshold) {
      val = 0;
      onNegativeThreshold(e);
    }
  };
}

/**
 * Return whether the cursor is at the far right end of the
 * provided HTML input
 * @param {HTMLInputElement} input
 */
export function cursorAtEnd(input) {
  return input && input.selectionStart === input.value.length;
}


================================================
FILE: src/lib/highlight.jsx
================================================
import { h } from 'preact';

function highlighted(text, indices) {
  const out = [];
  let unit = '';
  let pairIndex = 0;
  let pair = indices[pairIndex];
  pairIndex += 1;
  for (let i = 0; i < text.length; i++) {
    const char = text[i];
    if (pair && i === pair[0]) {
      out.push(unit);
      unit = '';
    }
    unit += char;
    if (pair && i === pair[1]) {
      out.push(<span style={{ 'font-weight': 'bold' }}>{unit}</span>);
      unit = '';
      pair = indices[pairIndex];
      pairIndex += 1;
    }
  }
  if (unit !== '') out.push(unit);
  return out;
}

export default function highlight(text, key, matches) {
  const matchesForKey = matches && matches.find(match => match.key === key);
  return matchesForKey ? highlighted(text, matchesForKey.indices) : text;
}


================================================
FILE: src/lib/log.js
================================================
export default (thing, ...things) => {
  if (SAKA_DEBUG) {
    console.log(thing, ...things);
  }
  return thing;
};


================================================
FILE: src/lib/tld.js
================================================
export default [
  'abogado',
  'ac',
  'academy',
  'accountants',
  'active',
  'actor',
  'ad',
  'adult',
  'ae',
  'aero',
  'af',
  'ag',
  'agency',
  'ai',
  'airforce',
  'al',
  'allfinanz',
  'alsace',
  'am',
  'amsterdam',
  'an',
  'android',
  'ao',
  'aq',
  'aquarelle',
  'ar',
  'archi',
  'army',
  'arpa',
  'as',
  'asia',
  'associates',
  'at',
  'attorney',
  'au',
  'auction',
  'audio',
  'autos',
  'aw',
  'ax',
  'axa',
  'az',
  'ba',
  'band',
  'bank',
  'bar',
  'barclaycard',
  'barclays',
  'bargains',
  'bayern',
  'bb',
  'bd',
  'be',
  'beer',
  'berlin',
  'best',
  'bf',
  'bg',
  'bh',
  'bi',
  'bid',
  'bike',
  'bio',
  'biz',
  'bj',
  'black',
  'blackfriday',
  'bloomberg',
  'blue',
  'bm',
  'bmw',
  'bn',
  'bnpparibas',
  'bo',
  'boo',
  'boutique',
  'br',
  'brussels',
  'bs',
  'bt',
  'budapest',
  'build',
  'builders',
  'business',
  'buzz',
  'bv',
  'bw',
  'by',
  'bz',
  'bzh',
  'ca',
  'cab',
  'cal',
  'camera',
  'camp',
  'cancerresearch',
  'capetown',
  'capital',
  'caravan',
  'cards',
  'care',
  'career',
  'careers',
  'cartier',
  'casa',
  'cash',
  'cat',
  'catering',
  'cc',
  'cd',
  'center',
  'ceo',
  'cern',
  'cf',
  'cg',
  'ch',
  'channel',
  'cheap',
  'christmas',
  'chrome',
  'church',
  'ci',
  'citic',
  'city',
  'ck',
  'cl',
  'claims',
  'cleaning',
  'click',
  'clinic',
  'clothing',
  'club',
  'cm',
  'cn',
  'co',
  'coach',
  'codes',
  'coffee',
  'college',
  'cologne',
  'com',
  'community',
  'company',
  'computer',
  'condos',
  'construction',
  'consulting',
  'contractors',
  'cooking',
  'cool',
  'coop',
  'country',
  'cr',
  'credit',
  'creditcard',
  'cricket',
  'crs',
  'cruises',
  'cu',
  'cuisinella',
  'cv',
  'cw',
  'cx',
  'cy',
  'cymru',
  'cz',
  'dabur',
  'dad',
  'dance',
  'dating',
  'day',
  'dclk',
  'de',
  'deals',
  'degree',
  'delivery',
  'democrat',
  'dental',
  'dentist',
  'desi',
  'design',
  'dev',
  'diamonds',
  'diet',
  'digital',
  'direct',
  'directory',
  'discount',
  'dj',
  'dk',
  'dm',
  'dnp',
  'do',
  'docs',
  'domains',
  'doosan',
  'durban',
  'dvag',
  'dz',
  'eat',
  'ec',
  'edu',
  'education',
  'ee',
  'eg',
  'email',
  'emerck',
  'energy',
  'engineer',
  'engineering',
  'enterprises',
  'equipment',
  'er',
  'es',
  'esq',
  'estate',
  'et',
  'eu',
  'eurovision',
  'eus',
  'events',
  'everbank',
  'exchange',
  'expert',
  'exposed',
  'fail',
  'farm',
  'fashion',
  'feedback',
  'fi',
  'finance',
  'financial',
  'firmdale',
  'fish',
  'fishing',
  'fit',
  'fitness',
  'fj',
  'fk',
  'flights',
  'florist',
  'flowers',
  'flsmidth',
  'fly',
  'fm',
  'fo',
  'foo',
  'forsale',
  'foundation',
  'fr',
  'frl',
  'frogans',
  'fund',
  'furniture',
  'futbol',
  'ga',
  'gal',
  'gallery',
  'garden',
  'gb',
  'gbiz',
  'gd',
  'ge',
  'gent',
  'gf',
  'gg',
  'ggee',
  'gh',
  'gi',
  'gift',
  'gifts',
  'gives',
  'gl',
  'glass',
  'gle',
  'global',
  'globo',
  'gm',
  'gmail',
  'gmo',
  'gmx',
  'gn',
  'goog',
  'google',
  'gop',
  'gov',
  'gp',
  'gq',
  'gr',
  'graphics',
  'gratis',
  'green',
  'gripe',
  'gs',
  'gt',
  'gu',
  'guide',
  'guitars',
  'guru',
  'gw',
  'gy',
  'hamburg',
  'hangout',
  'haus',
  'healthcare',
  'help',
  'here',
  'hermes',
  'hiphop',
  'hiv',
  'hk',
  'hm',
  'hn',
  'holdings',
  'holiday',
  'homes',
  'horse',
  'host',
  'hosting',
  'house',
  'how',
  'hr',
  'ht',
  'hu',
  'ibm',
  'id',
  'ie',
  'ifm',
  'il',
  'im',
  'immo',
  'immobilien',
  'in',
  'industries',
  'info',
  'ing',
  'ink',
  'institute',
  'insure',
  'int',
  'international',
  'investments',
  'io',
  'iq',
  'ir',
  'irish',
  'is',
  'it',
  'iwc',
  'jcb',
  'je',
  'jetzt',
  'jm',
  'jo',
  'jobs',
  'joburg',
  'jp',
  'juegos',
  'kaufen',
  'kddi',
  'ke',
  'kg',
  'kh',
  'ki',
  'kim',
  'kitchen',
  'kiwi',
  'km',
  'kn',
  'koeln',
  'kp',
  'kr',
  'krd',
  'kred',
  'kw',
  'ky',
  'kyoto',
  'kz',
  'la',
  'lacaixa',
  'land',
  'lat',
  'latrobe',
  'lawyer',
  'lb',
  'lc',
  'lds',
  'lease',
  'legal',
  'lgbt',
  'li',
  'lidl',
  'life',
  'lighting',
  'limited',
  'limo',
  'link',
  'lk',
  'loans',
  'london',
  'lotte',
  'lotto',
  'lr',
  'ls',
  'lt',
  'ltda',
  'lu',
  'luxe',
  'luxury',
  'lv',
  'ly',
  'ma',
  'madrid',
  'maison',
  'management',
  'mango',
  'market',
  'marketing',
  'marriott',
  'mc',
  'md',
  'me',
  'media',
  'meet',
  'melbourne',
  'meme',
  'memorial',
  'menu',
  'mg',
  'mh',
  'miami',
  'mil',
  'mini',
  'mk',
  'ml',
  'mm',
  'mn',
  'mo',
  'mobi',
  'moda',
  'moe',
  'monash',
  'money',
  'mormon',
  'mortgage',
  'moscow',
  'motorcycles',
  'mov',
  'mp',
  'mq',
  'mr',
  'ms',
  'mt',
  'mu',
  'museum',
  'mv',
  'mw',
  'mx',
  'my',
  'mz',
  'na',
  'nagoya',
  'name',
  'navy',
  'nc',
  'ne',
  'net',
  'network',
  'neustar',
  'new',
  'nexus',
  'nf',
  'ng',
  'ngo',
  'nhk',
  'ni',
  'ninja',
  'nl',
  'no',
  'np',
  'nr',
  'nra',
  'nrw',
  'nu',
  'nyc',
  'nz',
  'okinawa',
  'om',
  'one',
  'ong',
  'onl',
  'ooo',
  'org',
  'organic',
  'osaka',
  'otsuka',
  'ovh',
  'pa',
  'paris',
  'partners',
  'parts',
  'party',
  'pe',
  'pf',
  'pg',
  'ph',
  'pharmacy',
  'photo',
  'photography',
  'photos',
  'physio',
  'pics',
  'pictures',
  'pink',
  'pizza',
  'pk',
  'pl',
  'place',
  'plumbing',
  'pm',
  'pn',
  'pohl',
  'poker',
  'porn',
  'post',
  'pr',
  'praxi',
  'press',
  'pro',
  'prod',
  'productions',
  'prof',
  'properties',
  'property',
  'ps',
  'pt',
  'pub',
  'pw',
  'py',
  'qa',
  'qpon',
  'quebec',
  're',
  'realtor',
  'recipes',
  'red',
  'rehab',
  'reise',
  'reisen',
  'reit',
  'ren',
  'rentals',
  'repair',
  'report',
  'republican',
  'rest',
  'restaurant',
  'reviews',
  'rich',
  'rio',
  'rip',
  'ro',
  'rocks',
  'rodeo',
  'rs',
  'rsvp',
  'ru',
  'ruhr',
  'rw',
  'ryukyu',
  'sa',
  'saarland',
  'sale',
  'samsung',
  'sarl',
  'sb',
  'sc',
  'sca',
  'scb',
  'schmidt',
  'schule',
  'schwarz',
  'science',
  'scot',
  'sd',
  'se',
  'services',
  'sew',
  'sexy',
  'sg',
  'sh',
  'shiksha',
  'shoes',
  'shriram',
  'si',
  'singles',
  'sj',
  'sk',
  'sky',
  'sl',
  'sm',
  'sn',
  'so',
  'social',
  'software',
  'sohu',
  'solar',
  'solutions',
  'soy',
  'space',
  'spiegel',
  'sr',
  'st',
  'su',
  'supplies',
  'supply',
  'support',
  'surf',
  'surgery',
  'suzuki',
  'sv',
  'sx',
  'sy',
  'sydney',
  'systems',
  'sz',
  'taipei',
  'tatar',
  'tattoo',
  'tax',
  'tc',
  'td',
  'technology',
  'tel',
  'temasek',
  'tf',
  'tg',
  'th',
  'tienda',
  'tips',
  'tires',
  'tirol',
  'tj',
  'tk',
  'tl',
  'tm',
  'tn',
  'to',
  'today',
  'tokyo',
  'tools',
  'top',
  'town',
  'toys',
  'tp',
  'tr',
  'trade',
  'training',
  'travel',
  'trust',
  'tt',
  'tui',
  'tv',
  'tw',
  'tz',
  'ua',
  'ug',
  'uk',
  'university',
  'uno',
  'uol',
  'us',
  'uy',
  'uz',
  'va',
  'vacations',
  'vc',
  've',
  'vegas',
  'ventures',
  'versicherung',
  'vet',
  'vg',
  'vi',
  'viajes',
  'video',
  'villas',
  'vision',
  'vlaanderen',
  'vn',
  'vodka',
  'vote',
  'voting',
  'voto',
  'voyage',
  'vu',
  'wales',
  'wang',
  'watch',
  'webcam',
  'website',
  'wed',
  'wedding',
  'wf',
  'whoswho',
  'wien',
  'wiki',
  'williamhill',
  'wme',
  'work',
  'works',
  'world',
  'ws',
  'wtc',
  'wtf',
  'xn--1qqw23a',
  'xn--3bst00m',
  'xn--3ds443g',
  'xn--3e0b707e',
  'xn--45brj9c',
  'xn--45q11c',
  'xn--4gbrim',
  'xn--55qw42g',
  'xn--55qx5d',
  'xn--6frz82g',
  'xn--6qq986b3xl',
  'xn--80adxhks',
  'xn--80ao21a',
  'xn--80asehdb',
  'xn--80aswg',
  'xn--90a3ac',
  'xn--b4w605ferd',
  'xn--c1avg',
  'xn--cg4bki',
  'xn--clchc0ea0b2g2a9gcd',
  'xn--czr694b',
  'xn--czrs0t',
  'xn--czru2d',
  'xn--d1acj3b',
  'xn--d1alf',
  'xn--fiq228c5hs',
  'xn--fiq64b',
  'xn--fiqs8s',
  'xn--fiqz9s',
  'xn--flw351e',
  'xn--fpcrj9c3d',
  'xn--fzc2c9e2c',
  'xn--gecrj9c',
  'xn--h2brj9c',
  'xn--hxt814e',
  'xn--i1b6b1a6a2e',
  'xn--io0a7i',
  'xn--j1amh',
  'xn--j6w193g',
  'xn--kprw13d',
  'xn--kpry57d',
  'xn--kput3i',
  'xn--l1acc',
  'xn--lgbbat1ad8j',
  'xn--mgb9awbf',
  'xn--mgba3a4f16a',
  'xn--mgbaam7a8h',
  'xn--mgbab2bd',
  'xn--mgbayh7gpa',
  'xn--mgbbh1a71e',
  'xn--mgbc0a9azcg',
  'xn--mgberp4a5d4ar',
  'xn--mgbx4cd0ab',
  'xn--ngbc5azd',
  'xn--node',
  'xn--nqv7f',
  'xn--nqv7fs00ema',
  'xn--o3cw4h',
  'xn--ogbpf8fl',
  'xn--p1acf',
  'xn--p1ai',
  'xn--pgbs0dh',
  'xn--q9jyb4c',
  'xn--qcka1pmc',
  'xn--rhqv96g',
  'xn--s9brj9c',
  'xn--ses554g',
  'xn--unup4y',
  'xn--vermgensberater-ctb',
  'xn--vermgensberatung-pwb',
  'xn--vhquv',
  'xn--wgbh1c',
  'xn--wgbl6a',
  'xn--xhq521b',
  'xn--xkc2al3hye2a',
  'xn--xkc2dl3a5ee0h',
  'xn--yfro4i67o',
  'xn--ygbi2ammx',
  'xn--zfr164b',
  'xxx',
  'xyz',
  'yachts',
  'yandex',
  'ye',
  'yoga',
  'yokohama',
  'youtube',
  'yt',
  'za',
  'zip',
  'zm',
  'zone',
  'zuerich',
  'zw'
];


================================================
FILE: src/lib/trie.js
================================================
/**
 * A Trie datastructure that uses a simple javascript object for its underlying storage.
 * It doesn't generate the input tree for you, you MUST generate it yourself.
 * This trie is designed for Saka Key's needs and ISN'T general purpose.
 * An example input tree is:
 * {
 *   "c": {
 *     "a": {
 *       "t": "Tom",
 *       "r": "Tarzan"
 *     }
 *   },
 *   "d": {
 *     "o": {
 *       "g": () => console.log("My dog's name is Harris")
 *     }
 *   }
 * }
 */

export default class Trie {
  /**
   * Creates a trie
   * @param {Object} root - A simple javascript object representing a trie
   */
  init = root => {
    this.root = root;
    this.curNode = root;
  };

  /** Sets the root to current node to the root node */
  reset = () => {
    this.curNode = this.root;
  };

  /**
   * Advances the command trie based on the command key event.
   * If a leaf node, corresponding to a command, has been reached,
   * returns the command.
   * Otherwise returns undefined
   */
  advance = input => {
    // TODO: Update to use longest viable prefix by trying
    // longest prefix until a valid path is found
    const next = this.curNode[input] || this.root[input] || this.root;
    // Case 1. A trie node
    if (typeof next === 'object') {
      this.curNode = next;
      return undefined;
      // Case 2. A trie leaf corresponding to the command reached
    }

    this.curNode = this.root;
    return next;
  };
}


================================================
FILE: src/lib/url.js
================================================
import browser from 'webextension-polyfill';
import knownTLDs from './tld.js';
/**
 * Given the URL of a suggestion and the search text, makes the URL nicer
 * @param {string} url - the suggestion URL
 * @param {string} searchString - the text in the search bar
 */
export function prettifyURL(url, searchString) {
  let prettifiedUrl = url;
  if (url.endsWith('/')) {
    prettifiedUrl = url.substr(0, url.length - 1);
  }

  // TODO add support for any protocol
  if (
    !searchString.startsWith('http://') &&
    prettifiedUrl.startsWith('http://')
  ) {
    prettifiedUrl = prettifiedUrl.substr(7);
  }
  return prettifiedUrl;
}

/**
 * Returns true only if str is a valid url
 * @param {string} str
 */
export function isURL(str) {
  let isValidUrl;

  try {
    isValidUrl = Boolean(new URL(str));
  } catch (e) {
    isValidUrl = false;
  }
  return isValidUrl;
}

export function extractProtocol(url) {
  if (url) {
    return url.match(/^\w+:/, '') ? url.match(/^\w+:/, '')[0] : '';
  }

  return '';
}

export function stripProtocol(url) {
  return url.replace(/(^\w+:|^)\/\//, '');
}

export function stripWWW(url) {
  return url.replace(/^www\./, '');
}

export function startsWithProtocol(str) {
  return str.match(/(^\w+:|^)\/\//) !== null;
}

export function startsWithWWW(str) {
  return str.match(/^www\./, '') !== null;
}

/** Returns whether the provided text is a known TLD (top-level domain) */
export function isTLD(text) {
  return knownTLDs.indexOf(text) !== -1;
}

const knownProtocols = [
  'http:',
  'https:',
  'file:',
  'ftp:',
  'about:',
  'chrome:',
  'chrome-extension:',
  'moz-extension:'
];
/** Returns whether the provided text is a known protocol */
export function isProtocol(text) {
  return knownProtocols.indexOf(text) !== -1;
}

export function isLikeURL(url) {
  let trimmedUrl = url.trim();
  if (trimmedUrl.indexOf(' ') !== -1) {
    return false;
  }
  if (trimmedUrl.search(/^(about|file):[^:]/) !== -1) {
    return true;
  }
  const protocol = (trimmedUrl.match(/^([a-zA-Z-]+:)[^:]/) || [''])[0].slice(
    0,
    -1
  );
  const protocolMatch = isProtocol(protocol);
  if (protocolMatch) {
    trimmedUrl = trimmedUrl.replace(/^[a-zA-Z-]+:\/*/, '');
  }
  const hasPath = /.*[a-zA-Z].*\//.test(trimmedUrl);
  trimmedUrl = trimmedUrl.replace(/(:[0-9]+)?([#/].*|$)/g, '').split('.');
  if (protocolMatch && /^[a-zA-Z0-9@!]+$/.test(trimmedUrl)) {
    return true;
  }

  if (protocol && !protocolMatch && protocol !== 'localhost:') {
    return false;
  }
  // IP addresses
  const isIP = trimmedUrl.every(e => /^[0-9]+$/.test(e) && +e >= 0 && +e < 256);
  if (
    (isIP && !protocol && trimmedUrl.length === 4) ||
    (isIP && protocolMatch)
  ) {
    return true;
  }
  return (
    (trimmedUrl.every(e => /^[a-z0-9-]+$/i.test(e)) &&
      (trimmedUrl.length > 1 && isTLD(trimmedUrl[trimmedUrl.length - 1]))) ||
    (trimmedUrl.length === 1 && trimmedUrl[0] === 'localhost') ||
    hasPath
  );
}

export async function isSakaUrl(url) {
  if (url !== undefined) {
    const sakaUrl = browser.runtime.getURL('saka.html');
    const sakaId = sakaUrl.substring(0, sakaUrl.indexOf('/'));
    return url.includes(sakaId);
  }

  return false;
}


================================================
FILE: src/lib/utils.js
================================================
import Fuse from 'fuse.js';

export const isMac = navigator.appVersion.indexOf('Mac') !== -1;
export const ctrlChar = isMac ? '⌘' : 'ctrl';

export function rangedIncrement(value, increment, min, max) {
  const result = value + increment;

  if (result < min) {
    return min;
  } else if (result > max) {
    return max;
  }

  return result;
}

/**
 * @param {KeyboardEvent} e
 */
export function ctrlKey(e) {
  return isMac ? e.metaKey : e.ctrlKey;
}

export function objectFromArray(array, key) {
  const out = {};
  array.forEach(e => {
    out[e[key]] = e;
  });
  return out;
}

export async function getFilteredSuggestions(
  searchString,
  { getSuggestions, threshold, keys }
) {
  const suggestions = await getSuggestions(searchString);
  const fuse = new Fuse(suggestions, {
    shouldSort: true,
    threshold,
    minMatchCharLength: 1,
    includeMatches: true,
    keys,
    distance: 500
  });

  return fuse.search(searchString).map(({ item, matches, score }) => ({
    ...item,
    score,
    matches
  }));
}


================================================
FILE: src/msg/client.js
================================================
import client from 'msgx/client.js';

const msg = client({
  zoom: zoom => {
    window.dispatchEvent(new CustomEvent('zoom', { detail: { zoom } }));
  }
});

export default msg;


================================================
FILE: src/msg/server.js
================================================
import browser from 'webextension-polyfill';
import server from 'msgx/server.js';

import {
  getSuggestions,
  activateSuggestion,
  closeTab
} from 'suggestion_engine/server/index.js';

const actions = {
  // endpoints client queries with msg()
  sg: getSuggestions,
  zoom: (_, sender) => browser.tabs.getZoom(sender.tab.id),
  focusTab: (_, sender) => browser.tabs.update(sender.tab.id, { active: true }),
  activateSuggestion,
  closeTab
};

const onConnect = (sender, msg, data) => {
  const onZoomChange = ({ tabId, newZoomFactor }) => {
    if (sender.tab.id === tabId) {
      msg('zoom', newZoomFactor);
    }
  };
  browser.tabs.onZoomChange.addListener(onZoomChange);
  data.onZoomChange = onZoomChange;
};

const onDisconnect = (sender, data) => {
  browser.tabs.onZoomChange.removeListener(data.onZoomChange);
};

server(actions, onConnect, onDisconnect);


================================================
FILE: src/options/Main/MainOptions.jsx
================================================
import { Component, h } from 'preact';
import 'material-components-web/dist/material-components-web.css';
import 'scss/options.scss';

import OptionsList from './OptionsList/index.jsx';
import SakaHotkeysList from './SakaHotkeysList/index.jsx';

export default class MainOptions extends Component {
  constructor(props) {
    super(props);

    this.state = {
      showSakaKeybindings: false
    };
  }

  handleOpenSakaKeybindings = () => {
    this.setState({
      showSakaKeybindings: !this.state.showSakaKeybindings
    });
  };

  render() {
    return (
      <body>
        <header className="mdc-top-app-bar mdc-top-app-bar--short">
          <div className="mdc-top-app-bar__row">
            <section className="mdc-top-app-bar__section mdc-top-app-bar__section--align-start">
              <span className="mdc-top-app-bar__title">Saka Options</span>
            </section>
          </div>
        </header>
        <div
          id="background-image"
          className="mdc-elevation--z1 options-container"
        >
          {this.state.showSakaKeybindings ? (
            <SakaHotkeysList
              handleOpenSakaKeybindings={this.handleOpenSakaKeybindings}
            />
          ) : (
            <OptionsList
              handleOpenSakaKeybindings={this.handleOpenSakaKeybindings}
            />
          )}
        </div>
      </body>
    );
  }
}


================================================
FILE: src/options/Main/OptionsList/DefaultModeSelection.jsx
================================================
import { h } from 'preact';

// import { Component, h } from 'preact';
import 'material-components-web/dist/material-components-web.css';

const DefaultModeSelection = function DefaultModeSelection({
  mode,
  handleModeChange
}) {
  return (
    <li className="mdc-list-item option">
      <span className="mdc-list-item__text">
        Default Mode
        <span className="mdc-list-item__secondary-text">
          Select the default mode Saka opens with
        </span>
      </span>
      <div className="mdc-select mdc-list-item__meta">
        <select
          value={mode}
          id="defaultModeSelect"
          aria-label="Select default mode"
          className=" mdc-select__native-control"
          onChange={handleModeChange}
        >
          <option value="tab" selected="">
            Tabs
          </option>
          <option value="closedTab" selected="">
            Recently Closed
          </option>
          <option value="bookmark" selected="">
            Bookmarks
          </option>
          <option value="history" selected="">
            History
          </option>
          <option value="recentlyViewed" selected="">
            Recently Viewed
          </option>
          <option value="mode" selected="">
            Modes
          </option>
        </select>
      </div>
    </li>
  );
};

export default DefaultModeSelection;


================================================
FILE: src/options/Main/OptionsList/EnableFuzzySearch.jsx
================================================
import { h } from 'preact';

const EnableFuzzySearch = function EnableFuzzySearch() {
  const { checked, handleEnableFuzzySearch } = this.props;
  return (
    <li className="mdc-list-item option">
      <span className="mdc-list-item__text">
        Enable fuzzy search
        <span className="mdc-list-item__secondary-text">
          Enable fuzzy search for bookmarks and history search
        </span>
      </span>
      <div className="mdc-list-item__meta mdc-switch">
        <input
          type="checkbox"
          id="basic-switch"
          aria-label="Enable fuzzy search"
          className="mdc-switch__native-control"
          checked={checked}
          onChange={handleEnableFuzzySearch}
        />
        <div className="mdc-switch__background">
          <div className="mdc-switch__knob" />
        </div>
      </div>
    </li>
  );
};

export default EnableFuzzySearch;


================================================
FILE: src/options/Main/OptionsList/OnlyShowSearchBarSelector.jsx
================================================
import { h } from 'preact';

const OnlyShowSearchBarSelector = function OnlyShowSearchBarSelector() {
  const { checked, handleShowSearchSuggestionsChange } = this.props;
  return (
    <li className="mdc-list-item option">
      <span className="mdc-list-item__text">
        Suggestions on load
        <span className="mdc-list-item__secondary-text">
          Show suggestions when there is no text is the Saka search bar
        </span>
      </span>
      <div className="mdc-list-item__meta mdc-switch">
        <input
          type="checkbox"
          id="basic-switch"
          className="mdc-switch__native-control"
          aria-label="Suggestions on load"
          checked={checked}
          onChange={handleShowSearchSuggestionsChange}
        />
        <div className="mdc-switch__background">
          <div className="mdc-switch__knob" />
        </div>
      </div>
    </li>
  );
};

export default OnlyShowSearchBarSelector;


================================================
FILE: src/options/Main/OptionsList/ShowSakaHotkeys.jsx
================================================
import { h } from 'preact';

import 'material-components-web/dist/material-components-web.css';

const ShowSakaHotkeys = function ShowSakaHotkeys({
  handleOpenSakaKeybindings
}) {
  return (
    <li className="mdc-list-item option">
      <span className="mdc-list-item__text">Saka Hotkeys</span>
      <i
        className="mdc-list-item__meta mdc-icon-toggle material-icons"
        role="button"
        aria-pressed="false"
        aria-label="View Saka hotkeys"
        onClick={handleOpenSakaKeybindings}
      >
        keyboard
      </i>
    </li>
  );
};

export default ShowSakaHotkeys;


================================================
FILE: src/options/Main/OptionsList/index.jsx
================================================
import browser from 'webextension-polyfill';
import { Component, h } from 'preact';
import DefaultModeSelection from './DefaultModeSelection.jsx';
import OnlyShowSearchBarSelector from './OnlyShowSearchBarSelector.jsx';
import ShowSakaHotkeys from './ShowSakaHotkeys.jsx';
import EnableFuzzySearch from './EnableFuzzySearch.jsx';

export default class OptionsList extends Component {
  constructor(props) {
    super(props);

    this.state = {
      isLoading: true,
      mode: 'tab',
      showEmptySearchSuggestions: true,
      enableFuzzySearch: true
    };
  }

  async componentDidMount() {
    const sakaSettings = await this.fetchSakaSettings();
    this.setState(sakaSettings);
  }

  fetchSakaSettings = async function fetchSakaSettings() {
    const { sakaSettings } = await browser.storage.sync.get(['sakaSettings']);

    if (sakaSettings !== undefined) {
      return {
        isLoading: false,
        mode: sakaSettings.mode,
        showEmptySearchSuggestions: sakaSettings.showEmptySearchSuggestions,
        enableFuzzySearch: sakaSettings.enableFuzzySearch
      };
    }

    return {
      isLoading: false
    };
  };

  handleOptionsSave = () => {
    const settingsStore = {
      mode: this.state.mode,
      showEmptySearchSuggestions: this.state.showEmptySearchSuggestions,
      enableFuzzySearch: this.state.enableFuzzySearch
    };

    browser.storage.sync.set({ sakaSettings: settingsStore });
  };

  handleModeChange = e => {
    this.setState({
      mode: e.target.value
    });
  };

  handleShowSearchSuggestionsChange = () => {
    this.setState({
      showEmptySearchSuggestions: !this.state.showEmptySearchSuggestions
    });
  };

  handleEnableFuzzySearch = () => {
    this.setState({
      enableFuzzySearch: !this.state.enableFuzzySearch
    });
  };

  render() {
    const { handleOpenSakaKeybindings } = this.props;

    if (!this.state.isLoading) {
      return (
        <div className="options-form">
          <div className="mdc-list-group">
            <h3 className="mdc-list-group__subheader">General Settings</h3>
            <ul className="mdc-list mdc-list--non-interactive mdc-list--dense">
              <DefaultModeSelection
                handleModeChange={this.handleModeChange}
                mode={this.state.mode}
              />
              <li
                role="separator"
                className="mdc-list-divider mdc-list-divider--padded options-separator"
              />
              <OnlyShowSearchBarSelector
                checked={this.state.showEmptySearchSuggestions}
                handleShowSearchSuggestionsChange={
                  this.handleShowSearchSuggestionsChange
                }
              />
              <li
                role="separator"
                className="mdc-list-divider mdc-list-divider--padded options-separator"
              />
              <EnableFuzzySearch
                checked={this.state.enableFuzzySearch}
                handleEnableFuzzySearch={this.handleEnableFuzzySearch}
              />
              <li
                role="separator"
                className="mdc-list-divider mdc-list-divider--padded options-separator"
              />
              <ShowSakaHotkeys
                handleOpenSakaKeybindings={handleOpenSakaKeybindings}
              />
              <li
                role="separator"
                className="mdc-list-divider mdc-list-divider--padded options-separator"
              />
            </ul>
          </div>
          <div dir="rtl" className="options-save">
            <input
              type="submit"
              value="Save"
              className="mdc-button mdc-button--raised mdc-button--dense options-save-button"
              onClick={this.handleOptionsSave}
            />
          </div>
        </div>
      );
    }
    return <div />;
  }
}


================================================
FILE: src/options/Main/SakaHotkeysList/HotkeyListRow.jsx
================================================
import { h } from 'preact';

import 'material-components-web/dist/material-components-web.css';

const HotkeyListRow = function HotkeyListRow({ title, keys }) {
  const hotkeyShortcut = keys.map((key, index, keysArray) => (
    <span>
      <kbd>{key}</kbd>
      {keysArray.length === index + 1 ? '' : '+'}
    </span>
  ));
  return (
    <li className="mdc-list-item option">
      <span className="mdc-list-item__text">{title}</span>
      <div className="mdc-list-item__meta mdc-switch">{hotkeyShortcut}</div>
    </li>
  );
};

export default HotkeyListRow;


================================================
FILE: src/options/Main/SakaHotkeysList/index.jsx
================================================
import { h } from 'preact';
import HotkeyListRow from './HotkeyListRow.jsx';
import 'material-components-web/dist/material-components-web.css';
import { ctrlChar } from 'lib/utils';

const SakaHotkeysList = function SakaHotkeysList({
  handleOpenSakaKeybindings
}) {
  return (
    <div className="saka-hotkey-list">
      <div id="top-bar">
        <i
          className="mdc-icon-toggle material-icons"
          role="button"
          aria-pressed="false"
          aria-label="Back to Saka settings"
          onClick={handleOpenSakaKeybindings}
          onKeyDown={handleOpenSakaKeybindings}
        >
          arrow_back
        </i>
        <div className="tooltip">
          <i
            id="custom-hotkey-info"
            className="mdc-icon-toggle material-icons"
            aria-pressed="false"
            aria-label="Info about Saka custom hotkeys"
          >
            info
          </i>
          {SAKA_PLATFORM === 'chrome' ? (
            <span className="tooltiptext">
              To modify the Saka hotkeys, please visit
              chrome://extensions/shortcuts
            </span>
          ) : (
            <span className="tooltiptext">
              It is currently not possible to modify hotkeys in firefox
            </span>
          )}
        </div>
      </div>
      <h3 className="mdc-list-group__subheader">Keyboard Shortcuts</h3>
      <div className="mdc-list-group">
        <ul className="mdc-list mdc-list--non-interactive mdc-list--dense">
          <HotkeyListRow title="Open Saka" keys={[ctrlChar, 'space']} />
          <li role="separator" className="mdc-list-divider options-separator" />
          <HotkeyListRow title="Close Saka" keys={['esc']} />
          <li role="separator" className="mdc-list-divider options-separator" />
          <HotkeyListRow
            title="Close Saka (when search bar is empty and focused)"
            keys={['← backspace']}
          />
          <li role="separator" className="mdc-list-divider options-separator" />
          <HotkeyListRow title="Next Result" keys={['tab']} />
          <li role="separator" className="mdc-list-divider options-separator" />
          <HotkeyListRow title="Previous Result" keys={['shift', 'tab']} />
          <li role="separator" className="mdc-list-divider options-separator" />
          <HotkeyListRow title="Clear Search" keys={[ctrlChar, 'k']} />
          <li role="separator" className="mdc-list-divider options-separator" />
          <HotkeyListRow title="View previous search" keys={[ctrlChar, 'z']} />
          <li role="separator" className="mdc-list-divider options-separator" />
          <HotkeyListRow title="View next search" keys={[ctrlChar, 'y']} />
          <li role="separator" className="mdc-list-divider options-separator" />
          <HotkeyListRow
            title="Switch to next page of results"
            keys={[ctrlChar, 'd']}
          />
          <li role="separator" className="mdc-list-divider options-separator" />
          <HotkeyListRow
            title="Switch To previous page of results"
            keys={[ctrlChar, 's']}
          />
          <li role="separator" className="mdc-list-divider options-separator" />
          <HotkeyListRow
            title="Switch Modes (when search bar is empty)"
            keys={['space']}
          />
          <li role="separator" className="mdc-list-divider options-separator" />
          <HotkeyListRow
            title="Switch Modes (when search bar not empty) "
            keys={['shift', 'space']}
          />
          <li role="separator" className="mdc-list-divider options-separator" />
          <HotkeyListRow
            title="Switch To Tabs Search"
            keys={[ctrlChar, 'shift', 'a']}
          />
          <li role="separator" className="mdc-list-divider options-separator" />
          <HotkeyListRow
            title="Switch To Recently Closed Tabs Search"
            keys={[ctrlChar, 'shift', 'c']}
          />
          <li role="separator" className="mdc-list-divider options-separator" />
          <HotkeyListRow
            title="Switch To Bookmark Search"
            keys={[ctrlChar, 'b']}
          />
          <li role="separator" className="mdc-list-divider options-separator" />
          <HotkeyListRow
            title="Switch To History Search"
            keys={[ctrlChar, 'shift', 'e']}
          />
        </ul>
      </div>
    </div>
  );
};

export default SakaHotkeysList;


================================================
FILE: src/options/saka-options.jsx
================================================
import { render, h } from 'preact';
import Main from './Main/MainOptions.jsx';

render(<Main />, document.body);


================================================
FILE: src/saka/Main/Components/BackgroundImage/index.jsx
================================================
import browser from 'webextension-polyfill';
import { h, Component } from 'preact';
import msg from 'msg/client.js';
import 'scss/styles.scss';

export default class BackgroundImage extends Component {
  state = {
    screenshot: undefined
  };

  componentDidMount() {
    (async () => {
      const { screenshot } = await browser.storage.local.get('screenshot');
      this.setState({ screenshot });
      await msg('focusTab');
      await browser.storage.local.remove('screenshot');
    })();
  }

  render() {
    const { children } = this.props;
    const { screenshot } = this.state;
    return (
      <div
        id="background-image"
        style={screenshot && `background-image: url("${screenshot}")`}
      >
        {children}
      </div>
    );
  }

  // componentWillReceiveProps (nextProps) {
  //   if (nextProps.suggestion.tabId !== this.props.suggestion.tabId) {
  //     this.fetchImage(nextProps.suggestion.tabId);
  //   }
  // }
  // shouldComponentUpdate (nextProps, nextState) {
  //   return nextState.image !== this.state.image;
  // }
  // fetchImage = async () => {

  // }
}


================================================
FILE: src/saka/Main/Components/GUIContainer/index.jsx
================================================
import { h, Component } from 'preact';
import msg from 'msg/client.js';
import 'scss/styles.scss';

// Makes GUI constant size
export default class GUIContainer extends Component {
  state = {
    zoom: 0
  };

  componentWillMount() {
    window.addEventListener('zoom', this.onZoomChange);
    msg('zoom').then(this.setZoom);
  }

  componentWillUnmount() {
    window.removeEventListener('zoom', this.onZoomChange);
  }

  onZoomChange = event => {
    this.setZoom(event.detail.zoom);
  };

  setZoom = zoom => {
    this.setState({ zoom });
  };

  render() {
    const { children, onWheel } = this.props;
    const { zoom } = this.state;
    // opacity: 0.01 is just a trick to hide the component and not prevent it from
    // from rendering/mounting in the DOM, which would preven the search bar from focusing
    return (
      <main
        id="GUIContainer"
        onWheel={onWheel}
        style={
          zoom === 0
            ? {
                opacity: '0.01'
              }
            : {
                transform: `translateX(-50%) scale(${1 / zoom})`, // firefox can't handle calculated css scale props
                maxWidth: `${100 * zoom}%`,
                top: `${Math.max(0, (window.innerHeight - 504 / zoom) / 2)}px`
                // WARNING: 504 is the height of the GUI container with all 6 suggestions in pixels at the default zoom
                // This may change in future updates, so the constant will have to be updated accordingly
                // TODO: on each extension update, calculate the height of the Saka GUI with all 6 suggestions
                // and use that instead of the constant 504
              }
        }
      >
        {children}
      </main>
    );
  }
}


================================================
FILE: src/saka/Main/Components/Icon/index.jsx
================================================
import { h } from 'preact';
import 'scss/styles.scss';

export default ({ icon, color }) => {
  return (
    <i
      id="icon"
      className="material-icons"
      aria-hidden="true"
      style={{ color }}
    >
      {icon}
    </i>
  );
};


================================================
FILE: src/saka/Main/Components/ModeSwitcher/index.jsx
================================================
import { h } from 'preact';
import { suggestions } from 'src/suggestion_engine/server/providers/mode.js';
import Icon from 'src/saka/Main/Components/Icon/index.jsx';
import { fadedColorMap } from 'lib/colors.js';
import 'scss/styles.scss';

export default ({ mode, setMode }) => {
  const validModes = suggestions.map(suggestion => {
    const color =
      suggestion.mode === mode ? suggestion.fadedColor : fadedColorMap.unknown;

    return (
      <div
        className="mode-switcher-icon"
        style={
          suggestion.mode === mode
            ? `border-top: 3px solid  ${suggestion.fadedColor};`
            : {}
        }
        onClick={() => setMode(suggestion.mode)}
      >
        <Icon icon={suggestion.icon} color={color} />
      </div>
    );
  });

  return <div className="mode-switcher-wrapper">{validModes}</div>;
};


================================================
FILE: src/saka/Main/Components/PaginationBar/index.jsx
================================================
import { h } from 'preact';
import { ctrlChar } from 'lib/utils.js';
import 'scss/styles.scss';

export default ({
  firstVisibleIndex,
  suggestions,
  maxSuggestions,
  onClickPrevious,
  onClickNext
}) =>
  suggestions.length === 0 ? (
    undefined
  ) : (
    <section id="pagination-bar">
      <div
        role="button"
        onClick={onClickPrevious}
        onKeyPress={onClickPrevious}
        className="paginator-next-button"
        tabIndex={0}
      >
        <span className="arrow-normalizer">◄</span> {ctrlChar}-S
      </div>
      <div className="paginator-text-info">
        {`${firstVisibleIndex + 1} - ${firstVisibleIndex +
          Math.min(suggestions.length, maxSuggestions)} / ${
          suggestions.length
        }`}
      </div>
      <div
        role="button"
        onClick={onClickNext}
        onKeyPress={onClickNext}
        className="paginator-next-button"
        tabIndex={0}
      >
        {ctrlChar}-D ►
      </div>
    </section>
  );


================================================
FILE: src/saka/Main/Components/SearchBar/Button/index.jsx
================================================
import { h, Component } from 'preact';
import '@material/button/dist/mdc.button.min.css';
import { icons } from 'suggestion_utils/index.js';
import { colorMap, fadedColorMap } from 'lib/colors.js';
import 'scss/styles.scss';

// 1. Reload
// 2. Search
// 3. History
// 4. Calculate
// 5. Activate
// 6. Go
// 7. Command

// function icon (searchText, searchValue, tabURL, modifiers) {
//   return searchText === tabURL
//     ? 'refresh'
//     : iconForType[isURL(searchValue) ? 'url' : 'search'];
// }

// function icon (suggestion) {
//   if (suggestion) {
//     switch (suggestion.type) {
//       case 'tab':
//         return 'tab';
//       case 'closedTab':
//         return 'restore';
//     }
//   }
//   return 'error';
// }

export default class extends Component {
  state = {
    hovered: false
  };

  handleMouseEnter = () => {
    this.setState({ hovered: true });
  };

  handleMouseLeave = () => {
    this.setState({ hovered: false });
  };

  render() {
    const { mode, onClick } = this.props;
    const { hovered } = this.state;
    const { handleMouseEnter, handleMouseLeave } = this;
    return (
      <div
        role="button"
        id="action-button"
        onClick={onClick}
        onKeyPress={onClick}
        onMouseEnter={handleMouseEnter}
        onMouseLeave={handleMouseLeave}
        tabIndex={0}
      >
        <i
          className="material-icons"
          aria-hidden="true"
          style={{
            color: hovered ? colorMap.mode : fadedColorMap[mode]
          }}
        >
          {hovered ? icons.mode : icons[mode]}
        </i>
      </div>
    );
  }
}


================================================
FILE: src/saka/Main/Components/SearchBar/Input/index.jsx
================================================
import { Component, h } from 'preact';

// import '@material/textfield/dist/mdc.textfield.min.css';
import 'scss/styles.scss';

export default class Input extends Component {
  render() {
    const {
      placeholder,
      searchString,
      onKeyDown,
      onInput,
      onBlur
    } = this.props;

    return (
      <section className="mdc-text-field mdc-text-field--fullwidth search-field-wrapper">
        <input
          id="search-bar"
          className="mdc-text-field__input search-field-input"
          type="text"
          placeholder={placeholder}
          aria-label={placeholder}
          onKeyDown={onKeyDown}
          onInput={onInput}
          value={searchString}
          onBlur={onBlur}
          ref={input => input && input.focus()}
        />
      </section>
    );
  }
}


================================================
FILE: src/saka/Main/Components/SearchBar/index.jsx
================================================
import { h } from 'preact';
import 'scss/styles.scss';
import Input from './Input/index.jsx';

export default ({
  placeholder,
  searchString,
  suggestion,
  onKeyDown,
  onInput,
  onBlur
}) => (
  <form className="search-bar-container">
    <Input
      placeholder={placeholder}
      searchString={searchString}
      suggestion={suggestion}
      onKeyDown={onKeyDown}
      onInput={onInput}
      onBlur={onBlur}
    />
  </form>
);


================================================
FILE: src/saka/Main/Components/SettingsBar/index.jsx
================================================
import { h } from 'preact';
import { colorMap } from 'lib/colors.js';
import 'scss/styles.scss';

const Item = ({ label, color }) => (
  <span
    role="button"
    aria-pressed="false"
    style={{ color }}
    className="settings-item"
  >
    {label}
  </span>
);

export default () => (
  <section id="settings-bar">
    <Item label="Commands" color={colorMap.command} />
    <Item label="Tabs" color={colorMap.tabs} />
    <Item label="Search" color={colorMap.search} />
    <Item label="History" color={colorMap.history} />
    <Item label="Bookmarks" color={colorMap.bookmark} />
    <Item label="Dictionary" color={colorMap.dictionary} />
    <Item label="Calculator" color={colorMap.calculator} />
  </section>
);


================================================
FILE: src/saka/Main/Components/SuggestionList/Components/Suggestion/index.jsx
================================================
import { h } from 'preact';
import { fadedColorMap } from 'lib/colors.js';
import { ctrlChar } from 'lib/utils.js';
import { icons } from 'suggestion_utils/index.js';
import 'scss/styles.scss';

export default ({
  type,
  title,
  titleColor,
  secondary,
  secondaryColor,
  url,
  favIconUrl,
  incognito,
  selected,
  index,
  onClick
}) => {
  const color = fadedColorMap[type];
  const icon = icons[type];
  const incognitoIcon = icons.incognito;
  let suggestionIcon;

  if (incognito === true) {
    suggestionIcon = (
      <i className="material-icons" aria-hidden="true" style={{ color }}>
        {incognitoIcon}
      </i>
    );
  } else if (SAKA_PLATFORM === 'chrome' && url) {
    suggestionIcon = (
      <div
        className="suggestion-icon"
        style={`content: -webkit-image-set(url(chrome://favicon/size/16@1x/${url}) 1x, url(chrome://favicon/size/16@2x/${url}) 2x)`}
      />
    );
  } else if (SAKA_PLATFORM === 'firefox' && favIconUrl) {
    suggestionIcon = (
      <img className="suggestion-icon" src={favIconUrl} alt="" />
    );
  } else {
    suggestionIcon = (
      <i className="material-icons" aria-hidden="true" style={{ color }}>
        {icon}
      </i>
    );
  }

  return (
    <li
      className="mdc-list-item search-item"
      style={{
        backgroundColor: selected ? 'rgb(237, 237, 237)' : '#ffffff',
        borderLeftColor: color
      }}
      onKeyPress={() => onClick(index)}
      onClick={() => onClick(index)}
    >
      <span className="mdc-list-item__graphic search-icon" role="presentation">
        {suggestionIcon}
      </span>
      <span className="mdc-list-item__text suggestion-text">
        <span className="suggestion-wrap-text" style={{ color: titleColor }}>
          {title}
        </span>

        {secondary && (
          <span
            className="mdc-list-item__secondary-text suggestion-wrap-text"
            style={{ color: secondaryColor || 'inherit' }}
          >
            {secondary}
          </span>
        )}
      </span>
      <span className="mdc-list-item__meta kbd-end-detail">
        {selected ? (
          <i className="material-icons" aria-hidden="true" style={{ color }}>
            {icon}
          </i>
        ) : (
          `${ctrlChar}-${index + 1}`
        )}
      </span>
    </li>
  );
};


================================================
FILE: src/saka/Main/Components/SuggestionList/Containers/BookmarkSuggestion/index.jsx
================================================
import { h } from 'preact';
import highlight from 'lib/highlight.jsx';
import Suggestion from '../../Components/Suggestion/index.jsx';

export default ({
  suggestion: { title, url, matches },
  selected,
  index,
  onClick
}) => (
  <Suggestion
    type="bookmark"
    title={highlight(title, 'title', matches)}
    titleColor="#000000"
    secondary={highlight(url, 'url', matches)}
    secondaryColor="rgba(63, 81, 245, 1.0)"
    url={url}
    icon="star_border"
    selected={selected}
    index={index}
    onClick={onClick}
  />
);


================================================
FILE: src/saka/Main/Components/SuggestionList/Containers/ClosedTabSuggestion/index.jsx
================================================
import { h } from 'preact';
import highlight from 'lib/highlight.jsx';
import Suggestion from '../../Components/Suggestion/index.jsx';

export default ({
  suggestion: { title, url, matches, favIconUrl, incognito },
  selected,
  index,
  onClick
}) => (
  <Suggestion
    type="closedTab"
    title={highlight(title, 'title', matches)}
    titleColor="#000000"
    secondary={highlight(url, 'url', matches)}
    secondaryColor="rgba(63, 81, 245, 1.0)"
    url={url}
    favIconUrl={favIconUrl}
    incognito={incognito}
    icon="star_border"
    selected={selected}
    index={index}
    onClick={onClick}
  />
);


================================================
FILE: src/saka/Main/Components/SuggestionList/Containers/CommandSuggestion/index.jsx
================================================
import { h } from 'preact';
import Suggestion from '../../Components/Suggestion/index.jsx';

export default ({ suggestion: { title }, selected, index, onClick }) => (
  <Suggestion
    type="command"
    title={title}
    icon="input"
    titleColor="rgb(75, 165, 75)"
    selected={selected}
    index={index}
    onClick={onClick}
  />
);


================================================
FILE: src/saka/Main/Components/SuggestionList/Containers/HistorySuggestion/index.jsx
================================================
import { h } from 'preact';
import highlight from 'lib/highlight.jsx';
import Suggestion from '../../Components/Suggestion/index.jsx';

export default ({
  suggestion: { title, url, matches },
  selected,
  index,
  onClick
}) => (
  <Suggestion
    type="history"
    title={highlight(title, 'title', matches)}
    titleColor="#000000"
    secondary={highlight(url, 'url', matches)}
    secondaryColor="rgba(63, 81, 245, 1.0)"
    icon="history"
    url={url}
    selected={selected}
    index={index}
    onClick={onClick}
  />
);


================================================
FILE: src/saka/Main/Components/SuggestionList/Containers/RecentlyViewedSuggestion/index.jsx
================================================
import { h } from 'preact';
import highlight from 'lib/highlight.jsx';
import Suggestion from '../../Components/Suggestion/index.jsx';

export default ({
  suggestion: { title, url, matches, favIconUrl, incognito },
  selected,
  index,
  onClick
}) => (
  <Suggestion
    type="recentlyViewed"
    title={highlight(title, 'title', matches)}
    titleColor="#000000"
    secondary={highlight(url, 'url', matches)} // TODO: highlight matches are for normal URL not pretty URL
    secondaryColor="rgba(63, 81, 245, 1.0)"
    icon="recentlyViewed"
    favIconUrl={favIconUrl}
    incognito={incognito}
    url={url}
    selected={selected}
    index={index}
    onClick={onClick}
  />
);


================================================
FILE: src/saka/Main/Components/SuggestionList/Containers/SearchEngineSuggestion/index.jsx
================================================
import { h } from 'preact';
import Suggestion from '../../Components/Suggestion/index.jsx';

export default ({
  suggestion: { title, isURL, prettyURL },
  selected,
  index,
  onClick
}) => (
  <Suggestion
    type="search"
    title={isURL ? prettyURL : title}
    titleColor={isURL ? 'rgba(63, 81, 245, 1.0)' : 'rgba(0, 0, 00, 0.87)'}
    secondary={isURL ? title : undefined}
    icon={isURL ? 'insert_drive_file' : 'search'}
    selected={selected}
    index={index}
    onClick={onClick}
  />
);


================================================
FILE: src/saka/Main/Components/SuggestionList/Containers/SuggestionSelector.jsx
================================================
import { h, Component } from 'preact';
import TabSuggestion from './TabSuggestion/index.jsx';
import ClosedTabSuggestion from './ClosedTabSuggestion/index.jsx';
import BookmarkSuggestion from './BookmarkSuggestion/index.jsx';
import HistorySuggestion from './HistorySuggestion/index.jsx';
import RecentlyViewedSuggestion from './RecentlyViewedSuggestion/index.jsx';
import CommandSuggestion from './CommandSuggestion/index.jsx';
import SearchEngineSuggestion from './SearchEngineSuggestion/index.jsx';
import UnknownSuggestion from './UnknownSuggestion/index.jsx';

export default props => {
  switch (props.suggestion.type) {
    case 'tab':
      return <TabSuggestion {...props} />;
    case 'closedTab':
      return <ClosedTabSuggestion {...props} />;
    case 'bookmark':
      return <BookmarkSuggestion {...props} />;
    case 'history':
      return <HistorySuggestion {...props} />;
    case 'recentlyViewed':
      return <RecentlyViewedSuggestion {...props} />;
    case 'command':
      return <CommandSuggestion {...props} />;
    case 'searchEngine':
      return <SearchEngineSuggestion {...props} />;
    default:
      return <UnknownSuggestion {...props} />;
  }
};


================================================
FILE: src/saka/Main/Components/SuggestionList/Containers/TabSuggestion/index.jsx
================================================
import { h } from 'preact';
import highlight from 'lib/highlight.jsx';
import Suggestion from '../../Components/Suggestion/index.jsx';

export default ({
  suggestion: { title, url, matches, favIconUrl, incognito },
  selected,
  index,
  onClick
}) => (
  <Suggestion
    type="tab"
    title={highlight(title, 'title', matches)}
    titleColor="#000000"
    secondary={highlight(url, 'url', matches)} // TODO: highlight matches are for normal URL not pretty URL
    // secondary={prettyURL}
    secondaryColor="rgba(63, 81, 245, 1.0)"
    url={url}
    favIconUrl={favIconUrl}
    incognito={incognito}
    icon="star_border"
    selected={selected}
    index={index}
    onClick={onClick}
    class="tab-suggestion"
  />
);


================================================
FILE: src/saka/Main/Components/SuggestionList/Containers/UnknownSuggestion/index.jsx
================================================
import { h } from 'preact';
import Suggestion from '../../Components/Suggestion/index.jsx';

export default ({ suggestion: { title }, selected, index, onClick }) => (
  <Suggestion
    type="unknown"
    title={title}
    icon="error_outline"
    titleColor="red"
    selected={selected}
    index={index}
    onClick={onClick}
  />
);


================================================
FILE: src/saka/Main/Components/SuggestionList/index.jsx
================================================
import 'material-components-web/dist/material-components-web.css';
import 'scss/styles.scss';
import { h, Component } from 'preact';
import Suggestion from './Containers/SuggestionSelector.jsx';

export default ({
  searchString,
  suggestions,
  selectedIndex,
  firstVisibleIndex,
  maxSuggestions,
  onSuggestionClick
}) => (
  <ul className="mdc-list mdc-list--two-line mdc-list--avatar-list two-line-avatar-text-icon-demo list-container">
    {suggestions
      .slice(firstVisibleIndex, firstVisibleIndex + maxSuggestions)
      .map((suggestion, index) => {
        return (
          <Suggestion
            suggestion={suggestion}
            searchString={searchString}
            selected={index === selectedIndex}
            index={index}
            onClick={onSuggestionClick}
          />
        );
      })}
  </ul>
);


================================================
FILE: src/saka/Main/Containers/GeneralSearch/index.jsx
================================================
import { h } from 'preact';

export default () => <h1>General Search</h1>;


================================================
FILE: src/saka/Main/Containers/StandardSearch/index.jsx
================================================
import browser from 'webextension-polyfill';
import { Component, h } from 'preact';
import {
  getSuggestions,
  activateSuggestion,
  closeTab
} from 'suggestion_engine/client/index.js';
import { preprocessSuggestion } from 'suggestion_utils/index.js';
import { ctrlKey } from 'lib/utils.js';
import { slowWheelEvent } from 'lib/dom.js';
import SearchBar from '../../Components/SearchBar/index.jsx';
import SuggestionList from '../../Components/SuggestionList/index.jsx';
import PaginationBar from '../../Components/PaginationBar/index.jsx';
import GUIContainer from '../../Components/GUIContainer/index.jsx';
import BackgroundImage from '../../Components/BackgroundImage/index.jsx';
import ModeSwitcher from '../../Components/ModeSwitcher/index.jsx';

// provides suggestions but doesn't autocomplete input

export default class extends Component {
  state = {
    searchString: '',
    suggestions: [],
    selectedIndex: 0, // 0 <= selectedIndex < maxSuggestions
    firstVisibleIndex: 0, // 0 <= firstVisibleIndex < suggestion.length
    maxSuggestions: 6,
    undoIndex: this.props.searchHistory.size - 1
  };

  componentDidMount() {
    this.updateAutocompleteSuggestions('').then(() => {
      const { suggestions } = this.state;
      if (suggestions.length > 1) {
        this.setState({
          selectedIndex: 1
        });
      }
    });
  }

  componentDidUpdate(prevProps) {
    if (this.props.mode !== prevProps.mode) {
      this.updateAutocompleteSuggestions(this.state.searchString);
    }
  }

  getPreviousSearchString = () => {
    if (this.state.undoIndex !== 0) {
      this.setState({
        searchString: [...this.props.searchHistory][this.state.undoIndex],
        undoIndex: this.state.undoIndex - 1
      });
      this.updateAutocompleteSuggestions(this.state.searchString);
    }
  };

  getNextSearchString = () => {
    if (this.state.undoIndex < this.props.searchHistory.size) {
      this.setState({
        searchString: [...this.props.searchHistory][this.state.undoIndex],
        undoIndex: this.state.undoIndex + 1
      });
      this.updateAutocompleteSuggestions(this.state.searchString);
    }
  };

  handleWheel = slowWheelEvent(
    50,
    () => {
      this.incrementSelectedIndex(1);
    },
    () => {
      this.incrementSelectedIndex(-1);
    }
  );

  handleKeyDown = e => {
    switch (e.key) {
      case 'Escape':
        browser.runtime.sendMessage({
          key: 'closeSaka',
          searchHistory: [...this.props.searchHistory]
        });
        break;
      case 'Backspace':
        if (ctrlKey(e)) {
          e.preventDefault();
          this.closeTab();
        } else if (!e.repeat && e.target.value === '') {
          browser.runtime.sendMessage({
            key: 'closeSaka',
            searchHistory: [...this.props.searchHistory]
          });
        }
        break;
      case 'ArrowLeft':
      case 'ArrowRight':
        break;
      case 'ArrowDown':
        e.preventDefault();
        this.props.updateSearchHistory(this.state.searchString);
        this.incrementSelectedIndex(1);
        break;
      case 'ArrowUp':
        e.preventDefault();
        this.props.updateSearchHistory(this.state.searchString);
        this.incrementSelectedIndex(-1);
        break;
      case 'Tab':
        e.preventDefault();
        this.props.updateSearchHistory(this.state.searchString);
        e.shiftKey
          ? this.incrementSelectedIndex(-1)
          : this.incrementSelectedIndex(1);
        break;
      case '1':
      case '2':
      case '3':
      case '4':
      case '5':
      case '6':
        if (ctrlKey(e)) {
          e.preventDefault();
          this.tryActivateSuggestion(Number.parseInt(10, e.key) - 1);
        }
        break;
      case 'Enter':
        e.preventDefault();
        this.props.updateSearchHistory(
          this.state.searchString,
          this.tryActivateSuggestion
        );
        break;
      case 'k':
        if (ctrlKey(e)) {
          e.preventDefault();
          this.setState({ searchString: '' });
          this.updateAutocompleteSuggestions('');
        }
        break;
      case 's':
        if (ctrlKey(e)) {
          e.preventDefault();
          this.previousPage();
        }
        break;
      case 'd':
        if (ctrlKey(e)) {
          e.preventDefault();
          this.nextPage();
        }
        break;
      case ' ':
        if (e.shiftKey || this.state.searchString === '') {
          e.preventDefault();
          this.props.shuffleMode();
        }
        break;
      case 'A':
        if (ctrlKey(e)) {
          e.preventDefault();
          this.props.setMode('tab');
        }
        break;
      case 'C':
        if (ctrlKey(e)) {
          e.preventDefault();
          this.props.setMode('closedTab');
        }
        break;
      case 'M':
        if (ctrlKey(e)) {
          e.preventDefault();
          this.props.setMode('mode');
        }
        break;
      case 'b':
        if (ctrlKey(e)) {
          e.preventDefault();
          this.props.setMode('bookmark');
        }
        break;
      case 'E':
        if (ctrlKey(e)) {
          e.preventDefault();
          this.props.setMode('history');
        }
        break;
      case 'z':
        if (ctrlKey(e)) {
          e.preventDefault();
          this.getPreviousSearchString();
        }
        break;
      case 'y':
        if (ctrlKey(e)) {
          e.preventDefault();
          this.getNextSearchString();
        }
        break;
      case 'X':
        if (ctrlKey(e)) {
          e.preventDefault();
          this.props.setMode('recentlyViewed');
        }
        break;
      default:
        this.setState({
          undoIndex: this.props.searchHistory.size - 1
        });
        break;
    }
  };

  nextPage = () => {
    const {
      firstVisibleIndex,
      maxSuggestions,
      suggestions: { length: numSuggestions }
    } = this.state;
    const newFirstVisibleIndex = Math.max(
      0,
      Math.min(
        firstVisibleIndex + maxSuggestions,
        numSuggestions - maxSuggestions
      )
    );
    this.setState({
      firstVisibleIndex: newFirstVisibleIndex,
      selectedIndex: 0
    });
  };

  previousPage = () => {
    const { firstVisibleIndex, maxSuggestions } = this.state;
    const newFirstVisibleIndex = Math.max(
      0,
      firstVisibleIndex - maxSuggestions
    );
    this.setState({
      firstVisibleIndex: newFirstVisibleIndex,
      selectedIndex: 0
    });
  };

  incrementSelectedIndex = increment => {
    const { selectedIndex } = this.state;
    this.trySetIndex(selectedIndex + increment);
  };

  trySetIndex = index => {
    if (this.indexInRange(index)) {
      this.setState({ selectedIndex: index });
    } else {
      const { firstVisibleIndex, maxSuggestions, suggestions } = this.state;
      if (index < 0 && firstVisibleIndex > 0) {
        this.setState({ firstVisibleIndex: firstVisibleIndex - 1 });
      } else if (
        index >= maxSuggestions &&
        firstVisibleIndex + maxSuggestions < suggestions.length
      ) {
        this.setState({ firstVisibleIndex: firstVisibleIndex + 1 });
      }
    }
  };

  indexInRange = index => {
    const { suggestions, maxSuggestions } = this.state;
    return (
      index >= 0 &&
      index <= Math.max(0, Math.min(suggestions.length, maxSuggestions) - 1)
    );
  };

  closeTab = async (index = this.state.selectedIndex) => {
    const { suggestions, firstVisibleIndex } = this.state;
    const suggestion = suggestions[firstVisibleIndex + index];
    if (suggestion && this.props.mode === 'tab') {
      await closeTab(suggestion);
      suggestions.splice(firstVisibleIndex + index, 1);
      this.setState({ suggestions });
    }
  };

  tryActivateSuggestion = async (index = this.state.selectedIndex) => {
    const { suggestions, firstVisibleIndex } = this.state;
    const suggestion = suggestions[firstVisibleIndex + index];
    if (suggestion) {
      if (suggestion.type === 'mode') {
        this.props.setMode(suggestion.mode);
      } else {
        activateSuggestion(suggestion);
        await browser.runtime.sendMessage({
          key: 'closeSaka',
          searchHistory: [...this.props.searchHistory]
        });
      }
    }
  };

  handleInput = e => {
    const newSearchString = e.target.value;
    const { oldSearchString } = this.state;
    this.setState({ searchString: newSearchString });
    if (newSearchString !== oldSearchString) {
      this.setState({
        selectedIndex: 0,
        searchString: newSearchString
      });
      this.updateAutocompleteSuggestions(newSearchString);
    }
  };

  updateAutocompleteSuggestions = async searchStringAtLookup => {
    const suggestions = await getSuggestions(
      this.props.mode,
      searchStringAtLookup
    );

    const { searchString: searchStringNow } = this.state;
    if (searchStringNow === searchStringAtLookup) {
      this.setState({
        suggestions: suggestions.map(suggestion =>
          preprocessSuggestion(suggestion, searchStringAtLookup)
        ),
        firstVisibleIndex: 0,
        selectedIndex: 0
      });
    }
  };

  handleBlur = e => {
    this.props.updateSearchHistory(e.target.value);
  };

  handleButtonClick = () => {
    this.props.setMode('mode');
  };

  handleSuggestionClick = index => {
    this.tryActivateSuggestion(index);
  };

  render() {
    const { placeholder, mode, showEmptySearchSuggestions } = this.props;
    const {
      searchString,
      suggestions,
      selectedIndex,
      firstVisibleIndex,
      maxSuggestions
    } = this.state;
    const suggestion = suggestions[firstVisibleIndex + selectedIndex];

    if (!showEmptySearchSuggestions && !searchString) {
      return (
        <BackgroundImage suggestion={suggestion}>
          <GUIContainer onWheel={this.handleWheel}>
            <ModeSwitcher setMode={this.props.setMode} />
            <SearchBar
              placeholder={placeholder}
              searchString={searchString}
              suggestion={suggestion}
              onKeyDown={this.handleKeyDown}
              onInput={this.handleInput}
              onBlur={this.handleBlur}
              onButtonClick={this.handleButtonClick}
              onSuggestionClick={this.handleSuggestionClick}
              mode={mode}
            />
          </GUIContainer>
        </BackgroundImage>
      );
    }

    // TODO: Rename suggestions and suggestion
    return (
      <BackgroundImage suggestion={suggestion}>
        <GUIContainer onWheel={this.handleWheel}>
          <ModeSwitcher mode={mode} setMode={this.props.setMode} />
          <SearchBar
            placeholder={placeholder}
            searchString={searchString}
            suggestion={suggestion}
            onKeyDown={this.handleKeyDown}
            onInput={this.handleInput}
            onBlur={this.handleBlur}
            onButtonClick={this.handleButtonClick}
            onSuggestionClick={this.handleSuggestionClick}
            mode={mode}
          />
          <SuggestionList
            searchString={searchString}
            suggestions={suggestions}
            selectedIndex={selectedIndex}
            firstVisibleIndex={firstVisibleIndex}
            maxSuggestions={maxSuggestions}
            onSuggestionClick={this.handleSuggestionClick}
          />
          <PaginationBar
            selectedIndex={selectedIndex}
            suggestions={suggestions}
            firstVisibleIndex={firstVisibleIndex}
            maxSuggestions={maxSuggestions}
            onClickPrevious={this.previousPage}
            onClickNext={this.nextPage}
          />
        </GUIContainer>
      </BackgroundImage>
    );
  }
}


================================================
FILE: src/saka/Main/Containers/TabSearch/index.jsx
================================================
import { h } from 'preact';

export default () => <h1>Tab Search</h1>;


================================================
FILE: src/saka/Main/index.jsx
================================================
import 'material-components-web/dist/material-components-web.css';
import 'scss/styles.scss';
import browser from 'webextension-polyfill';
import { Component, h } from 'preact';
import StandardSearch from './Containers/StandardSearch/index.jsx';

export default class Main extends Component {
  constructor(props) {
    super(props);

    this.state = {
      mode: 'tab',
      modes: ['tab', 'closedTab', 'bookmark', 'history', 'recentlyViewed'],
      isLoading: true,
      showEmptySearchSuggestions: true,
      searchHistory: new Set([])
    };
  }

  async componentDidMount() {
    const sakaSettings = await this.fetchSakaSettings();
    this.setState(sakaSettings);
  }

  setMode = mode => {
    this.setState({ mode });
  };

  shuffleMode = () => {
    const { mode, modes } = this.state;
    const nextIndex = modes.indexOf(mode) + 1;
    const nextModeIndex = nextIndex >= modes.length ? 0 : nextIndex;
    this.setMode(modes[nextModeIndex]);
  };

  fetchSakaSettings = async function fetchSakaSettings() {
    const { sakaSettings } = await browser.storage.sync.get(['sakaSettings']);
    let { searchHistory } = await browser.storage.sync.get(['searchHistory']);
    searchHistory =
      searchHistory !== undefined && searchHistory.length > 0
        ? new Set(searchHistory)
        : new Set(['']);

    if (sakaSettings !== undefined) {
      const { mode, showEmptySearchSuggestions } = sakaSettings;
      return {
        isLoading: false,
        mode,
        showEmptySearchSuggestions,
        searchHistory
      };
    }

    return {
      isLoading: false,
      searchHistory
    };
  };

  updateSearchHistory = (searchString, callback) => {
    const { searchHistory } = this.state;
    searchHistory.delete(searchString);
    searchHistory.add(searchString);

    this.setState({ searchHistory }, callback);
  };

  render() {
    const {
      mode,
      isLoading,
      showEmptySearchSuggestions,
      searchHistory
    } = this.state;
    const { setMode, shuffleMode } = this;

    if (!isLoading) {
      switch (mode) {
        case 'tab':
          return (
            <StandardSearch
              mode={mode}
              placeholder="Tabs"
              setMode={setMode}
              shuffleMode={shuffleMode}
              showEmptySearchSuggestions={showEmptySearchSuggestions}
              searchHistory={searchHistory}
              updateSearchHistory={this.updateSearchHistory}
            />
          );
        case 'closedTab':
          return (
            <StandardSearch
              mode={mode}
              placeholder="Recently Closed"
              setMode={setMode}
              shuffleMode={shuffleMode}
              showEmptySearchSuggestions={showEmptySearchSuggestions}
              searchHistory={searchHistory}
              updateSearchHistory={this.updateSearchHistory}
            />
          );
        case 'bookmark':
          return (
            <StandardSearch
              mode={mode}
              placeholder="Bookmarks"
              setMode={setMode}
              shuffleMode={shuffleMode}
              showEmptySearchSuggestions={showEmptySearchSuggestions}
              searchHistory={searchHistory}
              updateSearchHistory={this.updateSearchHistory}
            />
          );
        case 'history':
          return (
            <StandardSearch
              mode={mode}
              placeholder="History"
              setMode={setMode}
              shuffleMode={shuffleMode}
              showEmptySearchSuggestions={showEmptySearchSuggestions}
              searchHistory={searchHistory}
              updateSearchHistory={this.updateSearchHistory}
            />
          );
        case 'recentlyViewed':
          return (
            <StandardSearch
              mode={mode}
              placeholder="Recently Viewed"
              setMode={setMode}
              shuffleMode={shuffleMode}
              showEmptySearchSuggestions={showEmptySearchSuggestions}
              searchHistory={searchHistory}
              updateSearchHistory={this.updateSearchHistory}
            />
          );
        default:
          return <div>Error, invalid mode</div>;
      }
    } else {
      return <div />;
    }
  }
}


================================================
FILE: src/saka/index.jsx
================================================
import { render, h } from 'preact';
import 'material-components-web/dist/material-components-web.css';

import Main from './Main/index.jsx';

render(<Main />, document.body);


================================================
FILE: src/scss/options.scss
================================================
$mdc-theme-primary: #00796b; // Purple 500
$mdc-theme-secondary: #4db6ac; // Orange A200

@import '@material/animation/functions';
@import '@material/theme/mdc-theme';

html {
  background-color: #eeeeee;
}

.options-container {
  background-color: #ffffff;
  margin: auto;
  position: relative;
  top: 50%;
  width: 60%;
  margin-top: 80px;
}

.options-form {
  padding: 10px;
}

.option {
  padding-bottom: 10px;
}

.options-separator {
  margin-bottom: 5px;
  color: #f1f1f1;
}

.options-save {
  margin-top: 5px;
  margin-right: 10px;
}

.options-icon {
  margin-top: 5px;
}

// http://www.jimmyscode.com/css-styling-for-kbd-tags/
kbd {
  padding: 0.1em 0.6em;
  border: 1px solid #ccc;
  font-size: 11px;
  font-family: Arial, Helvetica, sans-serif;
  background-color: #f7f7f7;
  color: #333;
  -moz-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset;
  -webkit-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset;
  box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset;
  -moz-border-radius: 3px;
  -webkit-border-radius: 3px;
  border-radius: 3px;
  display: inline-block;
  margin: 0 0.1em;
  text-shadow: 0 1px 0 #fff;
  line-height: 1.4;
  white-space: nowrap;
}

.tooltip {
  float: right;
}

.tooltip .tooltiptext {
  visibility: hidden;
  background-color: black;
  color: #fff;
  text-align: center;
  border-radius: 6px;
  padding: 5px 5px;
  position: absolute;
  z-index: 3;
}

.tooltip:hover .tooltiptext {
  visibility: visible;
}

.keyboard-shortcut-heading {
  padding-bottom: 20px;
}


================================================
FILE: src/scss/styles.scss
================================================
* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
}

#GUIContainer {
  background-color: #ffffff;
  position: absolute;
  left: 50%;
  right: 0;
  width: 680px;
  border-width: 0;
  transform-origin: 50% 0%;
  -webkit-box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2),
    0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12);
  box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2),
    0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12);
  overflow: hidden;
}

#background-image {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  background-size: cover;
}

#pagination-bar {
  display: flex;
  flex-flow: row no-wrap;
  justify-content: space-around;
  width: 100%;
  height: 14px;
  font-size: 12px;
  line-height: 14px;
  color: gray;
}

.pagination-item {
  cursor: pointer;
  opacity: 0.44;
}

.pagination-item:hover {
  opacity: 1;
}

.paginator-next-button {
  flex-grow: 3;
  opacity: 0.6;
  text-align: center;
  cursor: pointer;
}

.paginator-next-button:hover {
  color: white;
  background-color: gray;
}

.paginator-text-info {
  flex-grow: 2;
  text-align: center;
}

.arrow-normalizer {
  font-size: 15px;
  line-height: 9px;
  margin-right: 1px;
}

.search-bar-container {
  width: 100%;
  display: flex;
  border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}

section.search-field-wrapper {
  font-size: 26px;
  padding-right: 16px;
  display: flex;
  width: 624px;
  border-bottom: none;
}

input.search-field-input {
  font-size: inherit;
}

span.suggestion-text {
  align-self: center;
}

#search-bar {
  color: #000000;
  padding-left: 20px;
}

#search-bar::selection {
  background-color: rgba(63, 81, 245, 0.15);
}

#action-button {
  height: 56px;
  width: 56px;
  min-width: 56px;
  border-radius: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(63, 81, 245, 0.6);
  background-color: rgba(63, 81, 245, 0);
  cursor: pointer;
}

#action-button:hover {
  /* color: #ffffff;
  background-color: rgba(63, 81, 245, 1.0); */
  /* -webkit-box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12);
  box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12); */
}

#action-button > i {
  font-size: 32px;
}

#settings-bar {
  display: flex;
  flex-flow: row no-wrap;
  justify-content: space-around;
  width: 100%;
  height: 12px;
  font-size: 10px;
  line-height: 10px;
  color: rgba(0, 0, 0, 0.34);
  border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}

.settings-item {
  cursor: pointer;
  opacity: 0.44;
}

.settings-item:hover {
  opacity: 1;
}

ul.list-container {
  padding: 0px;
}
.search-item {
  padding: 0px 16px;
}

.search-icon {
  color: rgba(0, 0, 0, 0.26);
}

.two-line-avatar-text-icon-demo .mdc-list-item__graphic {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /*color: white;*/
}

kbd {
  color: #3f51f5;
  font-family: roboto, sans-serif;
  font-weight: normal;
  letter-spacing: 0.56px;
  -webkit-font-smoothing: antialiased;
  border: 1px solid #3f51f5;
  border-bottom: 5px solid;
  border-left: 3px solid;
  padding: 0px 4px;
  border-radius: 4px;
  border-bottom-left-radius: 3px;
  box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14),
    0 1px 5px 0 rgba(0, 0, 0, 0.12);
  margin: auto 1px;
}

.grey-bg {
  background: rgba(0, 0, 0, 0.26);
}

span.kbd-end-detail {
  white-space: nowrap;
  color: gray;
  text-decoration: none;
  margin-right: 6px;
}

.search-item {
  padding-left: 10px !important;
  border-left: 6px solid;
  cursor: pointer;
}

.search-item:hover {
  background-color: rgb(245, 245, 245) !important;
}

.suggestion-wrap-text {
  white-space: nowrap;
  text-overflow: ellipsis;
  max-width: 530px;
  overflow-x: hidden;
}

.suggestion-icon {
  width: 25px;
  height: 25px;
}

.mode-switcher-wrapper {
  width: 100%;
  display: flex;
  flex-direction: row;
  border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}

.mode-switcher-icon {
  flex-grow: 1;
  flex-wrap: nowrap;
  background-color: white;
  text-align: center;
  padding: 2px 0px 2px 0px;
  border-top: 3px solid transparent;
  cursor: pointer;
}

.mode-switcher-icon + .mode-switcher-icon {
  border-left: 2px solid rgba(0, 0, 0, 0.05);
}

.mode-switcher-icon:hover {
  background-color: rgb(245, 245, 245);
}


================================================
FILE: src/suggestion_engine/client/index.js
================================================
import msg from 'msg/client.js';

export async function getSuggestions(mode, searchString) {
  return msg('sg', [mode, searchString]);
}

export async function activateSuggestion(suggestion) {
  return msg('activateSuggestion', suggestion);
}

export async function closeTab(suggestion) {
  return msg('closeTab', suggestion);
}


================================================
FILE: src/suggestion_engine/server/index.js
================================================
import browser from 'webextension-polyfill';
import * as providers from './providers/index.js';

export async function getSuggestions([mode, searchString]) {
  return providers[mode](searchString);
}

async function focusOrCreateTab(url) {
  const matchingTabs = await browser.tabs.query({ url });

  if (matchingTabs && matchingTabs.length > 0) {
    // If multiple matching tabs then just focus the first one
    const existingTab = matchingTabs[0];

    await browser.tabs.update(existingTab.id, { active: true });
    await browser.windows.update(existingTab.windowId, { focused: true });
  } else {
    await browser.tabs.create({ url });
  }
}

export async function activateSuggestion(suggestion) {
  switch (suggestion.type) {
    case 'tab':
      await browser.tabs.update(suggestion.tabId, { active: true });
      await browser.windows.update(suggestion.windowId, { focused: true });
      break;
    case 'closedTab':
      await browser.sessions.restore(suggestion.sessionId);
      break;
    case 'bookmark':
      await focusOrCreateTab(suggestion.url);
      break;
    case 'history':
      await focusOrCreateTab(suggestion.url);
      break;
    case 'recentlyViewed':
      await activateSuggestion({
        ...suggestion,
        type: suggestion.originalType
      });
      break;
    default:
      console.error(
        `activation not yet implemented for suggestions of type ${
          suggestion.type
        }`
      );
  }
}

export async function closeTab(suggestion) {
  await browser.tabs.remove(suggestion.tabId);
}


================================================
FILE: src/suggestion_engine/server/providers/bookmark.js
================================================
import browser from 'webextension-polyfill';
import { isURL, extractProtocol, isProtocol } from 'lib/url.js';
import { getFilteredSuggestions } from 'lib/utils.js';

// https://github.com/nwjs/chromium.src/blob/45886148c94c59f45f14a9dc7b9a60624cfa626a/components/omnibox/browser/bookmark_provider.cc
async function allBookmarkSuggestions(searchText) {
  const searchCriteria = searchText === '' ? {} : searchText;
  const searchResults = await browser.bookmarks.search(searchCriteria);

  const validResults = [];
  searchResults.forEach(({ url, title }) => {
    const protocol = extractProtocol(url);
    if (isURL(url) && isProtocol(protocol)) {
      validResults.push({
        type: 'bookmark',
        score: -1,
        title,
        url
      });
    }
  });

  return validResults;
}

export default async function bookmarkSuggestions(searchString) {
  const { sakaSettings } = await browser.storage.sync.get(['sakaSettings']);

  const enableFuzzySearch =
    sakaSettings && sakaSettings.enableFuzzySearch !== undefined
      ? sakaSettings.enableFuzzySearch
      : true;

  if (searchString && enableFuzzySearch) {
    return getFilteredSuggestions(searchString, {
      getSuggestions: allBookmarkSuggestions,
      threshold: 1,
      keys: ['title', 'url']
    });
  }

  return allBookmarkSuggestions(searchString);
}


================================================
FILE: src/suggestion_engine/server/providers/closedTab.js
================================================
import browser from 'webextension-polyfill';
import { isSakaUrl } from 'lib/url.js';
// import { filter } from 'rxjs/operator/filter';
import { allTabSuggestions } from './tab.js';
import { getFilteredSuggestions } from 'lib/utils.js';

export async function getAllSuggestions() {
  const sessions = await browser.sessions.getRecentlyClosed();
  const filteredSessions = [];

  // TODO: This for loop is currently flagged by the airbnb eslint rules.
  // See: https://github.com/airbnb/javascript/issues/1271
  // Not disabling the rule as this might be fixable in the future using filter.
  // This for loop is needed at the moment as a workaround since filter does not support async.
  for (const session of sessions) {
    if (session.tab && !(await isSakaUrl(session.tab.url))) {
      filteredSessions.push(session);
    } else if (session.window && session.window.tabs) {
      for (const tabSession of session.window.tabs) {
        if (tabSession && !(await isSakaUrl(tabSession.url))) {
          filteredSessions.push({
            lastModified: session.window.lastModified,
            tab: {
              ...tabSession,
              sessionId: session.window.sessionId
            }
          });
        }
      }
    }
  }

  return filteredSessions.map(session => {
    const { lastModified } = session;
    const { id, sessionId, title, url, favIconUrl, incognito } = session.tab;
    return {
      type: 'closedTab',
      tabId: id,
      sessionId,
      score: undefined,
      title,
      url,
      favIconUrl: incognito ? null : favIconUrl,
      incognito,
      lastAccessed: lastModified
    };
  });
}

// TODO: Remove when Chrome gets proper timestamp
export async function recentlyClosedTabSuggestions() {
  const { recentlyClosed } = await browser.runtime.getBackgroundPage();
  const sessions = await browser.sessions.getRecentlyClosed();
  const filteredSessions = [];

  // TODO: This for loop is currently flagged by the airbnb eslint rules.
  // See: https://github.com/airbnb/javascript/issues/1271
  // Not disabling the rule as this might be fixable in the future using filter.
  // This for loop is needed at the moment as a workaround since filter does not support async.
  for (const session of sessions) {
    if (session.tab && !(await isSakaUrl(session.tab.url))) {
      filteredSessions.push(session);
    }
  }

  return filteredSessions
    .map(session => {
      const foundTab = recentlyClosed.findIndex(tab => {
        return tab.tabId === session.tab.id;
      });

      if (foundTab !== -1) {
        return { ...session, lastModified: recentlyClosed.tab.lastAccessed };
      }

      return session;
    })
    .map(session => {
      const { lastModified } = session;
      const { id, sessionId, title, url, favIconUrl, incognito } = session.tab;
      return {
        type: 'closedTab',
        tabId: id,
        sessionId,
        score: undefined,
        title,
        url,
        favIconUrl: incognito ? null : favIconUrl,
        incognito,
        lastAccessed: lastModified
      };
    });
}

export default async function closedTabSuggestions(searchString) {
  return searchString === ''
    ? getAllSuggestions()
    : getFilteredSuggestions(searchString, {
        getSuggestions: getAllSuggestions,
        threshold: 0.5,
        keys: ['title', 'url']
      });
}


================================================
FILE: src/suggestion_engine/server/providers/command.js
================================================
import { MAX_RESULTS } from './index.js';

const commands = ['search', 'help', 'history', 'tabs', 'define'];

export default function commandSuggestions(searchText) {
  return commands
    .filter(command => command.startsWith(searchText))
    .slice(0, MAX_RESULTS)
    .map(command => ({
      type: 'command',
      score: -1,
      title: command
    }));
}


================================================
FILE: src/suggestion_engine/server/providers/history.js
================================================
import browser from 'webextension-polyfill';
import { isSakaUrl } from 'lib/url.js';
import { getFilteredSuggestions } from 'lib/utils.js';

export async function allHistorySuggestions(searchText) {
  const results = await browser.history.search({
    text: searchText
  });

  const filteredResults = [];

  for (const result of results) {
    const sakaUrl = await isSakaUrl(result.url);
    !sakaUrl ? filteredResults.push(result) : null;
  }

  return filteredResults.map(
    ({ url, title, lastVisitTime, visitCount, typedCount }) => ({
      type: 'history',
      score: visitCount + typedCount,
      lastAccessed: lastVisitTime * 0.001,
      title,
      url
    })
  );
}

export default async function historySuggestions(searchString) {
  const { sakaSettings } = await browser.storage.sync.get(['sakaSettings']);
  const enableFuzzySearch =
    sakaSettings && sakaSettings.enableFuzzySearch !== undefined
      ? sakaSettings.enableFuzzySearch
      : true;

  if (searchString && enableFuzzySearch) {
    return getFilteredSuggestions(searchString, {
      getSuggestions: allHistorySuggestions,
      threshold: 1,
      keys: ['title', 'url']
    });
  }

  return allHistorySuggestions(searchString);
}


================================================
FILE: src/suggestion_engine/server/providers/index.js
================================================
export { default as tab } from './tab.js';
export { default as closedTab } from './closedTab.js';
export { default as mode } from './mode.js';
// export { default as commands } from './commands';
export { default as history } from './history.js';
export { default as bookmark } from './bookmark.js';
export { default as recentlyViewed } from './recentlyViewed.js';
// export { default as searchEngine } from './searchEngine';


================================================
FILE: src/suggestion_engine/server/providers/mode.js
================================================
import { ctrlChar } from 'lib/utils.js';
import { colorMap, fadedColorMap } from 'lib/colors.js';
import Fuse from 'fuse.js';

export const suggestions = [
  {
    type: 'mode',
    mode: 'tab',
    label: 'Tabs',
    shortcut: `${ctrlChar}-shift-a`,
    color: colorMap.tab,
    fadedColor: fadedColorMap.tab,
    icon: 'tab'
  },
  {
    type: 'mode',
    mode: 'closedTab',
    label: 'Recently Closed Tabs',
    shortcut: `${ctrlChar}-shift-c`,
    color: colorMap.closedTab,
    fadedColor: fadedColorMap.closedTab,
    icon: 'restore_page'
  },
  {
    type: 'mode',
    label: 'Bookmarks',
    mode: 'bookmark',
    shortcut: `${ctrlChar}-b`,
    color: colorMap.bookmark,
    fadedColor: fadedColorMap.bookmark,
    icon: 'bookmark_border'
  },
  {
    type: 'mode',
    label: 'History',
    mode: 'history',
    shortcut: `${ctrlChar}-shift-e`,
    color: colorMap.history,
    fadedColor: fadedColorMap.history,
    icon: 'history'
  },
  {
    type: 'mode',
    label: 'Recently Viewed',
    mode: 'recentlyViewed',
    shortcut: `${ctrlChar}-shift-x`,
    color: colorMap.recentlyViewed,
    fadedColor: fadedColorMap.recentlyViewed,
    icon: 'timelapse'
  }
];

const fuse = new Fuse(suggestions, {
  shouldSort: true,
  threshold: 1.0,
  includeMatches: true,
  keys: ['label']
});

export default async function modeSuggestions(searchString) {
  return searchString === ''
    ? suggestions
    : fuse.search(searchString).map(({ item, matches, score }) => ({
        ...item,
        score,
        matches
      }));
}


================================================
FILE: src/suggestion_engine/server/providers/recentlyViewed.js
================================================
import { getFilteredSuggestions } from 'lib/utils.js';
import tabSuggestions, {
  allTabSuggestions,
  recentVisitedTabSuggestions
} from './tab.js';
import {
  getAllSuggestions as getAllClosedTabs,
  recentlyClosedTabSuggestions
} from './closedTab.js';
import { allHistorySuggestions as getAllHistoryTabs } from './history.js';

function compareRecentlyViewedSuggestions(suggestion1, suggestion2) {
  return suggestion2.lastAccessed - suggestion1.lastAccessed;
}

async function allRecentlyViewedSuggestions(searchString) {
  const historyTabs = await getAllHistoryTabs(searchString);
  let openTabs = null;
  let closedTabs = null;

  if (SAKA_PLATFORM === 'chrome') {
    openTabs = await recentVisitedTabSuggestions(searchString);
    closedTabs = await recentlyClosedTabSuggestions(searchString);
  } else {
    openTabs = await tabSuggestions(searchString);
    closedTabs = await getAllClosedTabs(searchString);
  }

  const filteredClosedTabs = closedTabs.filter(tab =>
    openTabs.every(openTab => openTab.url !== tab.url)
  );

  const filteredHistoryTabs = historyTabs.filter(tab =>
    [...openTabs, ...filteredClosedTabs].every(
      openOrClosedTab => openOrClosedTab.url !== tab.url
    )
  );

  return [...openTabs, ...filteredClosedTabs, ...filteredHistoryTabs]
    .map(tab => ({ ...tab, originalType: tab.type, type: 'recentlyViewed' }))
    .sort(compareRecentlyViewedSuggestions);
}

async function filteredRecentlyViewedSuggestions(searchString) {
  const tabs = await allTabSuggestions();
  const closedTabs = await getAllClosedTabs(searchString);
  const historyTabs = await getAllHistoryTabs(searchString);

  return [
    ...tabs,
    ...Object.values(closedTabs),
    ...Object.values(historyTabs)
  ].map(tab => ({ ...tab, originalType: tab.type, type: 'recentlyViewed' }));
}

async function getFilteredRecentlyViewedSuggestions(searchString) {
  const filteredSuggestions = await getFilteredSuggestions(searchString, {
    getSuggestions: filteredRecentlyViewedSuggestions,
    threshold: 0.5,
    keys: ['title', 'url']
  });

  return filteredSuggestions.filter(
    (suggestion, index) =>
      filteredSuggestions.findIndex(
        filteredSuggestion =>
          filteredSuggestion.url === suggestion.url &&
          filteredSuggestion.title === suggestion.title
      ) === index
  );
}

export default async function recentlyViewedSuggestions(searchString) {
  if (searchString === '') {
    return allRecentlyViewedSuggestions(searchString, SAKA_PLATFORM);
  }

  return getFilteredRecentlyViewedSuggestions(searchString);
}


================================================
FILE: src/suggestion_engine/server/providers/searchEngine.js
================================================
import { MAX_RESULTS } from './index.js';

export default async function searchEngineSuggestions(searchText) {
  try {
    const baseURL =
      'https://www.google.com/complete/search?client=chrome-omni&q=';
    const response = await fetch(`${baseURL}${encodeURIComponent(searchText)}`);
    const json = await response.json();
    return json[1].slice(0, MAX_RESULTS).map(result => ({
      type: 'searchEngine',
      score: -1,
      title: result
    }));
  } catch (e) {
    return [];
  }
}


================================================
FILE: src/suggestion_engine/server/providers/tab.js
================================================
import browser from 'webextension-polyfill';
import { getFilteredSuggestions, objectFromArray } from 'lib/utils.js';

export async function allTabSuggestions() {
  const tabs = await browser.tabs.query({});
  return tabs.map(
    ({
      id: tabId,
      windowId,
      title,
      url,
      favIconUrl,
      incognito,
      lastAccessed
    }) => ({
      type: 'tab',
      tabId,
      windowId,
      title,
      url,
      favIconUrl: incognito ? null : favIconUrl,
      incognito,
      lastAccessed: lastAccessed * 0.001
    })
  );
}

async function recentTabSuggestions() {
  const tabs = await allTabSuggestions();
  const tabsMap = objectFromArray(tabs, 'tabId');
  const { tabHistory } = await browser.runtime.getBackgroundPage();

  const recentTabs = tabHistory.map(recentlyUsedTab => {
    const tab = tabsMap[recentlyUsedTab.tabId];
    delete tabsMap[recentlyUsedTab.tabId];
    return { ...tab, lastAccessed: recentlyUsedTab.lastAccessed };
  });

  return [...recentTabs, ...Object.values(tabsMap)];
}

// TODO: Remove this once chrome tab API provides recently viewed
export async function recentVisitedTabSuggestions() {
  const tabs = await allTabSuggestions();
  const tabsMap = objectFromArray(tabs, 'tabId');
  const { tabHistory } = await browser.runtime.getBackgroundPage();

  const recentTabs = tabHistory.map(recentlyUsedTab => {
    const tab = tabsMap[recentlyUsedTab.tabId];
    delete tabsMap[recentlyUsedTab.tabId];
    return { ...tab, lastAccessed: recentlyUsedTab.lastAccessed * 0.001 };
  });

  return [...recentTabs];
}

export default async function tabSuggestions(searchString) {
  return searchString === ''
    ? recentTabSuggestions()
    : getFilteredSuggestions(searchString, {
        getSuggestions: allTabSuggestions,
        threshold: 0.5,
        keys: ['title', 'url']
      });
}


================================================
FILE: src/suggestion_utils/index.js
================================================
import { prettifyURL, isURL } from 'lib/url.js';

export const icons = {
  mode: 'apps',
  tab: 'tab',
  closedTab: 'restore_page',
  history: 'history',
  recentlyViewed: 'timelapse',
  bookmark: 'bookmark_border',
  incognito: 'visibility_off'
};

export function preprocessSuggestion(suggestion, searchText) {
  switch (suggestion.type) {
    case 'tab': {
      const prettyURL = prettifyURL(suggestion.url, searchText);
      return {
        ...suggestion,
        prettyURL,
        text: suggestion.title
      };
    }
    case 'closedTab': {
      const prettyURL = prettifyURL(suggestion.url, searchText);
      return {
        ...suggestion,
        prettyURL,
        text: suggestion.title
      };
    }
    case 'mode':
      return suggestion;
    case 'bookmark': {
      const prettyURL = prettifyURL(suggestion.url, searchText);
      return {
        ...suggestion,
        prettyURL,
        text: prettyURL
      };
    }
    case 'history': {
      const prettyURL = prettifyURL(suggestion.url, searchText);
      return {
        ...suggestion,
        prettyURL,
        text: prettyURL
      };
    }
    case 'recentlyViewed': {
      const prettyURL = prettifyURL(suggestion.url, searchText);
      return {
        ...suggestion,
        prettyURL,
        text: prettyURL
      };
    }
    default:
      return {
        type: 'error',
        title: `Error. Unknown Suggestion type: ${suggestion.type}`,
        text: `Error. Unknown Suggestion type: ${suggestion.type}`
      };
  }
}


================================================
FILE: static/background_page.html
================================================
<!doctype html>
<html>
    <meta charset="UTF-8">
<body>
    <script src="vendor.js"></script>
    <script src="background_page.js"></script>
</body>

</html>

================================================
FILE: static/material-icons.css
================================================
@font-face {
  font-family: 'Material Icons';
  font-style: normal;
  font-weight: 400;
  src: local('Material Icons'),
    local('MaterialIcons-Regular'),
    url(./MaterialIcons-Regular.woff2) format('woff2');
}

.material-icons {
  font-family: 'Material Icons';
  font-weight: normal;
  font-style: normal;
  font-size: 24px;  /* Preferred icon size */
  display: inline-block;
  line-height: 1;
  text-transform: none;
  letter-spacing: normal;
  word-wrap: normal;
  white-space: nowrap;
  direction: ltr;

  /* Support for all WebKit browsers. */
  -webkit-font-smoothing: antialiased;
  /* Support for Safari and Chrome. */
  text-rendering: optimizeLegibility;

  /* Support for Firefox. */
  -moz-osx-font-smoothing: grayscale;

  /* Support for IE. */
  font-feature-settings: 'liga';
}

================================================
FILE: static/options.html
================================================
<!doctype html>
<html>

<head>
    <title>Saka Options</title>
    <meta charset="UTF-8">
    <link rel='stylesheet' type='text/css' href='material-icons.css'>
</head>

<body>
    <script src="vendor.js"></script>
    <script src="saka-options.js"></script>
</body>

</html>

================================================
FILE: static/saka.html
================================================
<!doctype html>
<html>

<head>
  <title>Saka</title>
  <meta charset="UTF-8">
  <link rel='stylesheet' type='text/css' href='material-icons.css'>
</head>

<body>
  <script src="vendor.js"></script>
  <script src="saka.js"></script>
</body>

</html>

================================================
FILE: test/Icon.test.js
================================================
import Icon from '../src/saka/Main/Components/Icon/index.jsx';
import { render } from 'preact-render-spy';
import { h } from 'preact';

describe('Icon component ', () => {
  it('should render while enabled', () => {
    const props = {
      icon: '',
      color: 'rgba(1,1,1,0.44)'
    };

    const iconRender = render(<Icon {...props} />);
    const icon = iconRender.find('#icon');

    expect(icon).toBeTruthy();
  });
});


================================================
FILE: test/Main.test.js
================================================
import { h } from 'preact';
import {
  render,
  cleanup,
  wait,
  fireEvent,
  flushPromises
} from 'preact-testing-library';
import 'jest-dom/extend-expect';
import Main from '@/saka/Main/index.jsx';

beforeEach(() => {
  browser.flush();
  // browser.storage.sync.get.returns({
  //   sakaSettings: {
  //     mode: 'tab',
  //     showEmptySearchSuggestions: false
  //   },
  //   searchHistory: []
  // });
  browser.storage.local.get.resolves(
    Promise.resolve({
      screenshot:
        'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB4AAAAPWCAYAAAABOoU/AAAgAElEQVR4nOzdeXzbd2H/8e9XcqDOXdrC2OAH/W1l49rooMAgvQsbg0F}'
    })
  );
  browser.storage.local.remove.returns('');
  browser.runtime.sendMessage.returns('');
});

test('should show all options when not showing key bindings', async () => {
  render(<Main />);
});

afterEach(cleanup);


================================================
FILE: test/ModeSwitcher.test.js
================================================
import ModeSwitcher from '@/saka/Main/Components/ModeSwitcher/index.jsx';
import {
  render,
  cleanup,
  fireEvent,
  flushPromises
} from 'preact-testing-library';
import { fadedColorMap } from 'lib/colors.js';
import { h } from 'preact';

describe('ModeSwitcher component ', () => {
  it('should render tabs with selected tab colored, rest of tabs gray', async () => {
    const setMode = jest.fn();

    const props = {
      mode: 'tab',
      setMode
    };

    const { getByText } = render(<ModeSwitcher {...props} />);

    expect(getByText('tab')).toMatchSnapshot();
    expect(getByText('restore_page')).toMatchSnapshot();
    expect(getByText('bookmark_border')).toMatchSnapshot();
    expect(getByText('history')).toMatchSnapshot();
    expect(getByText('timelapse')).toMatchSnapshot();

    fireEvent.click(getByText('restore_page'), 'click');
    await flushPromises();
    expect(setMode.mock.calls.length).toBe(1);
  });
});

afterEach(cleanup);


================================================
FILE: test/PaginationBar.test.js
================================================
import PaginationBar from '@/saka/Main/Components/PaginationBar/index.jsx';
import { render, getByText } from 'preact-testing-library';
import { h } from 'preact';

describe('PaginationBar component ', () => {
  it('should be empty when no there are no suggestions', () => {
    const props = {
      firstVisibleIndex: 0,
      suggestions: [],
      maxSuggestions: 6,
      onClickPrevious() {},
      onClickNext() {}
    };

    const { queryByText } = render(<PaginationBar {...props} />);
    expect(queryByText('◄')).toBeNull();
    expect(queryByText('ctrl-S')).toBeNull();
    expect(queryByText('ctrl-D ►')).toBeNull();
  });

  it('should show correct amount of suggestions when there are suggestions found', () => {
    const props = {
      firstVisibleIndex: 0,
      suggestions: [
        {
          type: 'tab',
          title: 'lusakasa/saka: Elegant tab search',
          url: 'https://github.com/lusakasa/saka'
        },
        {
          type: 'tab',
          title: 'Google',
          url: 'https://google.com'
        }
      ],
      maxSuggestions: 6,
      onClickPrevious() {},
      onClickNext() {}
    };

    const { getByText } = render(<PaginationBar {...props} />);
    getByText('◄');
    getByText('ctrl-S');
    getByText('1 - 2 / 2');
    getByText('ctrl-D ►');
  });
});


================================================
FILE: test/SearchBar.test.js
================================================
import { h } from 'preact';
import { render, cleanup, flushPromises } from 'preact-testing-library';
import SearchBar from '@/saka/Main/Components/SearchBar/index';

afterEach(cleanup);

test('should be empty when no there is no search string provided', async () => {
  const props = {
    placeholder: 'Tabs',
    searchString: '',
    suggestion: {},
    mode: 'tab',
    onKeyDown() {},
    onInput() {},
    onBlur() {},
    onButtonClick() {}
  };

  const { container } = render(<SearchBar {...props} />);
  expect(container).toMatchSnapshot();
});

test('should show the search string when search string is provided', async () => {
  const props = {
    placeholder: 'Tabs',
    searchString: 'Saka github',
    suggestion: {},
    mode: 'tab',
    onKeyDown() {},
    onInput() {},
    onBlur() {},
    onButtonClick() {}
  };

  const { getByPlaceholderText } = render(<SearchBar {...props} />);

  expect(getByPlaceholderText('Tabs').value).toBe('Saka github');
});


================================================
FILE: test/StandardSearch/StandardSearch.test.js
================================================
import { h } from 'preact';
import {
  render,
  cleanup,
  flushPromises,
  fireEvent,
  wait
} from 'preact-testing-library';

import StandardSearch from '@/saka/Main/Containers/StandardSearch/index.jsx';

beforeEach(() => {
  browser.flush();
  browser.storage.local.get.resolves(
    Promise.resolve({
      screenshot:
        'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB4AAAAPWCAYAAAABOoU/AAAgAElEQVR4nOzdeXzbd2H/8e9XcqDOXdrC2OAH/W1l49rooMAgvQsbg0F}'
    })
  );
  browser.storage.local.remove.returns('');
  browser.runtime.sendMessage.returns('');
});

test('should not show suggestion list when showEmptySearchSuggestions false and no search string', () => {
  const props = {
    placeholder: 'Tabs',
    mode: 'tab',
    showEmptySearchSuggestions: false,
    searchHistory: ['first', 'second', 'third'],
    updateSearchHistory: jest.fn()
  };

  const { getByPlaceholderText } = render(<StandardSearch {...props} />);
  expect(getByPlaceholderText('Tabs').value).toBe('');
});

test('should render and allow user input to search for suggestion', () => {
  const props = {
    placeholder: 'Tabs',
    mode: 'tab',
    showEmptySearchSuggestions: true,
    searchHistory: [],
    updateSearchHistory: jest.fn(),
    shuffleMode: jest.fn()
  };

  const { getByPlaceholderText } = render(<StandardSearch {...props} />);

  getByPlaceholderText('Tabs').value = 'Test input';
  fireEvent.input(getByPlaceholderText('Tabs'));
  expect(getByPlaceholderText('Tabs').value).toBe('Test input');

  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: ' ',
    shiftKey: true
  });
  expect(props.shuffleMode.mock.calls.length).toBe(1);

  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'k',
    keyCode: 75,
    which: 75,
    ctrlKey: true
  });
});

test('should allow keyboard navigation of Saka search results', () => {
  const props = {
    placeholder: 'Tabs',
    mode: 'tab',
    showEmptySearchSuggestions: true,
    searchHistory: [],
    updateSearchHistory: jest.fn()
  };

  const { getByPlaceholderText } = render(<StandardSearch {...props} />);

  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'Tab'
  });

  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'Tab',
    keyCode: 9,
    which: 9,
    shiftKey: true
  });

  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'd',
    keyCode: 68,
    which: 68,
    shiftKey: true,
    ctrlKey: true
  });

  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 's',
    keyCode: 83,
    which: 83,
    shiftKey: true,
    ctrlKey: true
  });
});

test('should allow going back and forward through search history', () => {
  const props = {
    placeholder: 'Tabs',
    mode: 'tab',
    showEmptySearchSuggestions: true,
    searchHistory: ['first', 'second', 'third'],
    updateSearchHistory: jest.fn()
  };

  const { getByPlaceholderText } = render(<StandardSearch {...props} />);
  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'z',
    keyCode: 90,
    which: 90,
    ctrlKey: true
  });

  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'y',
    keyCode: 89,
    which: 89,
    ctrlKey: true
  });
});

test('should allow switching between search modes', async () => {
  const props = {
    placeholder: 'Tabs',
    mode: 'tab',
    showEmptySearchSuggestions: true,
    searchHistory: ['first', 'second', 'third'],
    updateSearchHistory: jest.fn(),
    shuffleMode: jest.fn(),
    setMode: jest.fn()
  };

  const { getByPlaceholderText } = render(<StandardSearch {...props} />);

  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: ' '
  });
  expect(props.shuffleMode.mock.calls.length).toBe(1);

  // Recently Closed
  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'C',
    ctrlKey: true
  });

  // Tabs
  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'A',
    ctrlKey: true
  });

  // Bookmarks
  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'b',
    ctrlKey: true
  });

  // History
  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'E',
    ctrlKey: true
  });

  // Recently Viewed
  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'X',
    ctrlKey: true
  });

  // Modes
  // TODO: Deprecate this feature
  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'M',
    ctrlKey: true
  });

  expect(props.setMode.mock.calls.length).toBe(6);
});

test('should close saka on Enter key', () => {
  const props = {
    placeholder: 'Tabs',
    mode: 'tab',
    showEmptySearchSuggestions: true,
    searchHistory: [],
    updateSearchHistory: jest.fn(),
    shuffleMode: jest.fn()
  };

  const { getByPlaceholderText } = render(<StandardSearch {...props} />);

  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'Enter'
  });

  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'Backspace'
  });

  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'Escape'
  });
});

test('should allow navigation via arrow keys', () => {
  const props = {
    placeholder: 'Tabs',
    mode: 'tab',
    showEmptySearchSuggestions: true,
    searchHistory: [],
    updateSearchHistory: jest.fn(),
    shuffleMode: jest.fn()
  };

  const { getByPlaceholderText } = render(<StandardSearch {...props} />);

  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'ArrowLeft'
  });

  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'ArrowRight'
  });

  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'ArrowDown'
  });

  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'ArrowUp'
  });
});

test('should select suggestion based on key press', () => {
  const props = {
    placeholder: 'Tabs',
    mode: 'tab',
    showEmptySearchSuggestions: true,
    searchHistory: [],
    updateSearchHistory: jest.fn(),
    shuffleMode: jest.fn()
  };

  const { getByPlaceholderText } = render(<StandardSearch {...props} />);

  [1, 2, 3, 4, 5, 6].map(num => {
    fireEvent.keyDown(getByPlaceholderText('Tabs'), {
      key: `${num}`,
      ctrlKey: true
    });
  });
});

test('should close tab and delete suggestion when key pressed', async () => {
  browser.tabs.query.returns([
    {
      title: 'adjksdhk',
      url: 'adasd',
      incgnito: false
    },
    { title: 'adjksdhk', url: 'adasd', incgnito: false }
  ]);
  const props = {
    placeholder: 'Tabs',
    mode: 'tab',
    showEmptySearchSuggestions: true,
    searchHistory: [],
    updateSearchHistory: jest.fn(),
    shuffleMode: jest.fn()
  };

  const { getByPlaceholderText } = render(<StandardSearch {...props} />);

  await flushPromises();

  fireEvent.keyDown(getByPlaceholderText('Tabs'), {
    key: 'Backspace',
    ctrlKey: true
  });
});

afterEach(cleanup);


================================================
FILE: test/Suggestion.test.js
================================================
import { h } from 'preact';
import {
  render,
  cleanup,
  fireEvent,
  flushPromises
} from 'preact-testing-library';
import Suggestion from '@/saka/Main/Components/SuggestionList/Components/Suggestion';

test('should render when props passed in', async () => {
  global.SAKA_PLATFORM = 'chrome';

  const props = {
    type: 'tab',
    title: 'Test Title',
    titleColor: 'fafafa',
    secondary: 'Secondary Test Title',
    secondaryColor: 'ffffff',
    url: 'https://example.com',
    favIconUrl: 'localhost:1234/path/to/icon',
    incognito: false,
    selected: 'false',
    index: 0,
    onClick: () => {}
  };

  const { getByText } = render(<Suggestion {...props} />);

  getByText('Test Title');
  getByText('Secondary Test Title');
  getByText('tab');
});

test('should call onClick when onClick or onKeyPress event', async () => {
  global.SAKA_PLATFORM = 'chrome';

  const onClick = jest.fn();
  const props = {
    type: 'tab',
    title: 'Test Title',
    titleColor: 'fafafa',
    secondary: 'Secondary Test Title',
    secondaryColor: 'ffffff',
    url: 'https://example.com',
    favIconUrl: 'localhost:1234/path/to/icon',
    incognito: false,
    selected: 'false',
    index: 0,
    onClick
  };

  const { getByText } = render(<Suggestion {...props} />);

  fireEvent.click(getByText('Test Title'));
  fireEvent.keyPress(getByText('Test Title'));
  await flushPromises();
  expect(onClick.mock.calls.length).toBe(2);
});

test('should hide icon when suggestion is from incognito', () => {
  global.SAKA_PLATFORM = 'chrome';

  const onClick = jest.fn();
  const props = {
    type: 'tab',
    title: 'Test Title',
    titleColor: 'fafafa',
    secondary: 'Secondary Test Title',
    secondaryColor: 'ffffff',
    url: 'https://example.com',
    favIconUrl: 'localhost:1234/path/to/icon',
    incognito: true,
    selected: 'false',
    index: 0,
    onClick
  };

  const { getByText } = render(<Suggestion {...props} />);
});

test('should use correct favicon path when using firefox', () => {
  global.SAKA_PLATFORM = 'firefox';

  const onClick = jest.fn();
  const props = {
    type: 'tab',
    title: 'Test Title',
    titleColor: 'fafafa',
    secondary: 'Secondary Test Title',
    secondaryColor: 'ffffff',
    url: 'https://example.com',
    favIconUrl: 'localhost:1234/path/to/icon',
    incognito: false,
    selected: 'false',
    index: 0,
    onClick
  };

  const { getByText } = render(<Suggestion {...props} />);
  expect(getByText('tab')).toMatchSnapshot();
});

test('should use default favicon when no url to favicon', () => {
  global.SAKA_PLATFORM = 'firefox';

  const onClick = jest.fn();
  const props = {
    type: 'tab',
    title: 'Test Title',
    titleColor: 'fafafa',
    secondary: 'Secondary Test Title',
    secondaryColor: 'ffffff',
    incognito: false,
    selected: 'false',
    index: 0,
    onClick
  };

  const { getByText } = render(<Suggestion {...props} />);
  expect(getByText('tab')).toMatchSnapshot();
});
afterEach(cleanup);


================================================
FILE: test/SuggestionList.test.js
================================================
import SuggestionList from '@/saka/Main/Components/SuggestionList/index.jsx';
import { render } from 'preact-testing-library';
import { h } from 'preact';

const MAX_SUGGESTIONS = 6;

beforeEach(() => {
  // Clears the database and adds some testing data.
  // Jest will wait for this promise to resolve before running tests.
  global.SAKA_PLATFORM = 'chrome';
});

describe('SuggestionList component ', () => {
  it('should be empty when no values provided', () => {
    const suggestions = [];

    const searchString = {};

    const { container } = render(
      <SuggestionList
        searchString={searchString}
        suggestions={suggestions}
        selectedIndex={0}
        firstVisibleIndex={0}
        maxSuggestions={MAX_SUGGESTIONS}
      />
    );

    expect(container).toMatchSnapshot();
  });

  it('should display suggestions when provided', () => {
    const suggestions = [
      {
        type: 'tab',
        title: 'lusakasa/saka: Elegant tab search',
        url: 'https://github.com/lusakasa/saka'
      },
      {
        type: 'tab',
        title: 'Google',
        url: 'https://google.com'
      }
    ];

    const searchString = '';

    const { getByText } = render(
      <SuggestionList
        searchString={searchString}
        suggestions={suggestions}
        selectedIndex={0}
        firstVisibleIndex={0}
        maxSuggestions={MAX_SUGGESTIONS}
        onSuggestionClick={() => {}}
      />
    );

    suggestions.map(suggestion => {
      getByText(suggestion.title);
      getByText(suggestion.url);
    });
  });
});


================================================
FILE: test/__mocks__/browser-mocks.js
================================================
const chrome = require('sinon-chrome/extensions');
const browser = require('sinon-chrome/webextensions');
global.chrome = chrome;
global.browser = browser;

jest.mock('msgx/client.js', () =>
  jest.fn().mockImplementation(() => {
    return jest.fn().mockImplementation(mode => {
      let api = {
        zoom: Promise.resolve(1),
        sg: []
      };

      return api[mode];
    });
  })
);

// ({ default: jest.fn() })


================================================
FILE: test/__mocks__/styleMock.scss
================================================


================================================
FILE: test/__snapshots__/ModeSwitcher.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ModeSwitcher component  should render tabs with selected tab colored, rest of tabs gray 1`] = `
<i
  aria-hidden="true"
  class="material-icons"
  id="icon"
  style="color: rgba(55, 126, 184, 0.44);"
>
  tab
</i>
`;

exports[`ModeSwitcher component  should render tabs with selected tab colored, rest of tabs gray 2`] = `
<i
  aria-hidden="true"
  class="material-icons"
  id="icon"
  style="color: rgba(153, 153, 153, 0.44);"
>
  restore_page
</i>
`;

exports[`ModeSwitcher component  should render tabs with selected tab colored, rest of tabs gray 3`] = `
<i
  aria-hidden="true"
  class="material-icons"
  id="icon"
  style="color: rgba(153, 153, 153, 0.44);"
>
  bookmark_border
</i>
`;

exports[`ModeSwitcher component  should render tabs with selected tab colored, rest of tabs gray 4`] = `
<i
  aria-hidden="true"
  class="material-icons"
  id="icon"
  style="color: rgba(153, 153, 153, 0.44);"
>
  history
</i>
`;

exports[`ModeSwitcher component  should render tabs with selected tab colored, rest of tabs gray 5`] = `
<i
  aria-hidden="true"
  class="material-icons"
  id="icon"
  style="color: rgba(153, 153, 153, 0.44);"
>
  timelapse
</i>
`;


================================================
FILE: test/__snapshots__/SearchBar.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should be empty when no there is no search string provided 1`] = `
<div>
  <form
    class="search-bar-container"
  >
    <section
      class="mdc-text-field mdc-text-field--fullwidth search-field-wrapper"
    >
      <input
        aria-label="Tabs"
        class="mdc-text-field__input search-field-input"
        id="search-bar"
        placeholder="Tabs"
        type="text"
      />
    </section>
  </form>
</div>
`;


================================================
FILE: test/__snapshots__/Suggestion.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should use correct favicon path when using firefox 1`] = `
<i
  aria-hidden="true"
  class="material-icons"
  style="color: rgba(55, 126, 184, 0.44);"
>
  tab
</i>
`;

exports[`should use default favicon when no url to favicon 1`] = `
<i
  aria-hidden="true"
  class="material-icons"
  style="color: rgba(55, 126, 184, 0.44);"
>
  tab
</i>
`;


================================================
FILE: test/__snapshots__/SuggestionList.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SuggestionList component  should be empty when no values provided 1`] = `
<div>
  <ul
    class="mdc-list mdc-list--two-line mdc-list--avatar-list two-line-avatar-text-icon-demo list-container"
  />
</div>
`;


================================================
FILE: test/lib/hightlight.test.js
================================================
import highlight from 'lib/highlight';

test('should return source text when matches is undefined', async () => {
  const text = 'http://www.example.com';
  const key = '';
  const matches = undefined;
  const result = highlight(text, key, matches);

  expect(result).toBe(text);
});

test('should return source text when matches is defined but empty', async () => {
  const text = 'http://www.example.com';
  const key = '';
  const matches = [];
  const result = highlight(text, key, matches);

  expect(result).toBe(text);
});

test('should return highlighted text when matches is defined and not empty', async () => {
  const text = 'http://www.example.com';
  const key = 'title';
  const matches = [
    {
      indices: [[13, 16]],
      value: 'example',
      key: 'title',
      arrayIndex: 0
    }
  ];

  const result = highlight(text, key, matches);

  expect(result).toEqual([
    'http://www.ex',
    {
      attributes: { style: { 'font-weight': 'bold' } },
      children: ['ampl'],
      key: undefined,
      nodeName: 'span'
    },
    'e.com'
  ]);
});


================================================
FILE: test/lib/log.test.js
================================================
import log from 'lib/log';

test('should not log when debug mode is disabled', async () => {
  global.SAKA_DEBUG = false;
  global.console = { log: jest.fn() };
  const result = log(1, 2, 3, 4);

  expect(result).toBe(1);
  expect(global.console.log.mock.calls.length).toBe(0);
});

test('should log when debug mode is enabled', async () => {
  global.SAKA_DEBUG = true;
  global.console = { log: jest.fn() };
  const result = log(1, 2, 3, 4);

  expect(result).toBe(1);
  expect(global.console.log.mock.calls.length).toBe(1);
});


================================================
FILE: test/lib/url.test.js
================================================
const browser = require('sinon-chrome/webextensions');

import * as libUrl from 'lib/url';

describe('lib/url ', () => {
  beforeAll(() => {
    global.browser = browser;
  });

  beforeEach(() => {
    browser.flush();
  });

  describe('isURL ', () => {
    it('should return false when URL is empty', () => {
      expect(libUrl.isURL('')).toBe(false);
      expect(libUrl.isURL(undefined)).toBe(false);
      expect(libUrl.isURL(null)).toBe(false);
    });

    it('should return false when URL is invalid', () => {
      const url = 'http:';

      expect(libUrl.isURL(url)).toBe(false);
    });

    it('should return true when URL is valid with standard protocol', () => {
      expect(libUrl.isURL('https://github.com/lusakasa/saka')).toBe(true);
      expect(libUrl.isURL('ftp://localhost')).toBe(true);
      expect(libUrl.isURL('file://home/testing')).toBe(true);
    });

    it('should return true when URL is valid with custom protocol', () => {
      expect(libUrl.isURL('about:blank')).toBe(true);
      expect(libUrl.isURL('chrome:addons')).toBe(true);
    });
  });

  describe('extractProtocol ', () => {
    it('should empty string when no protocol provided', () => {
      expect(libUrl.extractProtocol('this is a string with no protocol')).toBe(
        ''
      );
    });

    it('should return protocol when provided in a normal URL', () => {
      expect(libUrl.extractProtocol('https://github.com/lusakasa/saka')).toBe(
        'https:'
      );
    });

    it('should return protocol when provided URL with port', () => {
      expect(libUrl.extractProtocol('https://localhost:1234')).toBe('https:');
    });
  });

  describe('stripProtocol ', () => {
    it('should strip nothing when no protocol in string', () => {
      expect(libUrl.stripProtocol('string with no url')).toBe(
        'string with no url'
      );
    });

    it('should strip protocol when given a valid url', () => {
      expect(libUrl.stripProtocol('https://github.com/lusakasa/saka')).toBe(
        'github.com/lusakasa/saka'
      );
    });

    it('should strip protocol when given a valid url with ports', () => {
      expect(libUrl.stripProtocol('https://localhost:12345')).toBe(
        'localhost:12345'
      );
    });
  });

  describe('stripWWW ', () => {
    it('should strip nothing when no www in string', () => {
      expect(libUrl.stripWWW('https://github.com/lusakasa/saka')).toBe(
        'https://github.com/lusakasa/saka'
      );
    });

    it('should strip www when provided url with www', () => {
      expect(libUrl.stripWWW('www.github.com/lusakasa/saka')).toBe(
        'github.com/lusakasa/saka'
      );
    });

    it('should not strip www when provided url with protocol', () => {
      expect(libUrl.stripWWW('https://www.github.com/lusakasa/saka')).toBe(
        'https://www.github.com/lusakasa/saka'
      );
    });
  });

  describe('startsWithProtocol ', () => {
    it('should return false when does not start with a protocol', () => {
      expect(libUrl.startsWithProtocol('github.com/lusakasa/saka')).toBe(false);
    });

    it('should return false when does not start with a protocol and has a port', () => {
      expect(libUrl.startsWithProtocol('localhost:12345')).toBe(false);
    });

    it('should return true when does start with a protocol', () => {
      expect(
        libUrl.startsWithProtocol('https://github.com/lusakasa/saka')
      ).toBe(true);
    });

    it('should return true when does start with a protocol and has a port', () => {
      expect(libUrl.startsWithProtocol('https://localhost:12345')).toBe(true);
    });
  });

  describe('startsWithWWW ', () => {
    it('should return false when does not start with a www', () => {
      expect(libUrl.startsWithWWW('github.com/lusakasa/saka')).toBe(false);
    });

    it('should return false when does not start with a www and has a port', () => {
      expect(libUrl.startsWithWWW('myserver:12345')).toBe(false);
    });

    it('should return true when does start with a www', () => {
      expect(libUrl.startsWithWWW('www.github.com/lusakasa/saka')).toBe(true);
    });

    it('should return true when does start with a www and has a port', () => {
      expect(libUrl.startsWithWWW('www.myserver:12345')).toBe(true);
    });
  });

  describe('isTLD ', () => {
    it('should return false when not given a valid tld', () => {
      expect(libUrl.isTLD('faketld.test')).toBe(false);
    });

    it('should return true when given a valid tld', () => {
      expect(libUrl.isTLD('com')).toBe(true);
    });
  });

  describe('isProtocol ', () => {
    it('should return false when not given a valid protocol', () => {
      expect(libUrl.isProtocol('fakeprotocol:')).toBe(false);
    });

    it('should return true when given a valid protocol', () => {
      expect(libUrl.isProtocol('https:')).toBe(true);
    });
  });

  describe('isLikeURL ', () => {
    it('should return false when not given a url like string', () => {
      expect(libUrl.isLikeURL('nonurlstring')).toBe(false);
    });

    it('should return true when given a url like string', () => {
      expect(libUrl.isLikeURL('https://github.com/lusakasa/saka')).toBe(true);
    });

    it('should return true when given an ip address', () => {
      expect(libUrl.isLikeURL('127.0.0.1')).toBe(true);
    });
  });

  describe('prettifyURL ', () => {
    it('should return empty string when input empty string', () => {
      expect(libUrl.prettifyURL('', '')).toBe('');
    });

    it('should return prettified string when input prettified url', () => {
      expect(libUrl.prettifyURL('github.com/lusakasa/saka', '')).toBe(
        'github.com/lusakasa/saka'
      );
    });

    it('should return prettified string when input url', () => {
      expect(libUrl.prettifyURL('http://github.com/lusakasa/saka/', '')).toBe(
        'github.com/lusakasa/saka'
      );
    });
  });

  describe('isSakaUrl ', () => {
    it('should return false when URL is empty', async () => {
      const sakaId = 'abcdefg/saka.html';
      browser.runtime.getURL.returns(sakaId);

      expect(await libUrl.isSakaUrl()).toBe(false);
    });

    it('should return false when URL does not contain saka ID', async () => {
      const sakaId = 'abcdefg/saka.html';
      browser.runtime.getURL.returns(sakaId);

      expect(await libUrl.isSakaUrl('https://github.com/lusakasa')).toBe(false);
    });

    it('should return true when URL contains saka ID', async () => {
      const sakaId = 'abcdefg/saka.html';
      browser.runtime.getURL.returns(sakaId);

      expect(await libUrl.isSakaUrl('http://abcdefg/saka.html')).toBe(true);
    });
  });

  afterAll(() => {
    browser.flush();
    delete global.browser;
  });
});


================================================
FILE: test/lib/utils.test.js
================================================
import * as libUtil from 'lib/utils';

describe('lib/util ', () => {
  describe('objectFromArray ', () => {
    it('should return empty object when empty list passed in', () => {
      expect(libUtil.objectFromArray([], 1)).toEqual({});
    });

    it('should return corresponding object when empty list passed in', () => {
      expect(
        libUtil.objectFromArray([{ hello: 'world', index: 0 }], 'index')
      ).toEqual({ 0: { hello: 'world', index: 0 } });
    });

    it('should return object with undefined key when target key not found', () => {
      expect(
        libUtil.objectFromArray([{ hello: 'world', index: 0 }], 'randomKey')
      ).toEqual({ undefined: { hello: 'world', index: 0 } });
    });
  });

  describe('rangedIncrement ', () => {
    it('should return min when sum(value, increment) < min', () => {
      expect(libUtil.rangedIncrement(1, 1, 3, 4)).toEqual(3);
    });

    it('should return sum(value, increment) when min < sum(value, increment) < max', () => {
      expect(libUtil.rangedIncrement(2, 2, 2, 5)).toEqual(4);
    });

    it('should return max when max < sum(value, increment)', () => {
      expect(libUtil.rangedIncrement(10, 10, 3, 8)).toEqual(8);
    });
  });

  describe('ctrlKey ', () => {
    it('should return metaKey when is mac', () => {
      const isMac = true;
      const kbEvent = new KeyboardEvent('ctrl');
      expect(libUtil.ctrlKey(kbEvent)).toEqual(kbEvent.metaKey);
    });

    it('should return ctrlKey when is not mac', () => {
      const isMac = false;
      const kbEvent = new KeyboardEvent('ctrl');
      expect(libUtil.ctrlKey(kbEvent)).toEqual(kbEvent.ctrlKey);
    });
  });

  describe('getFilteredSuggestions ', () => {
    it('should only return valid matches to search string', async () => {
      const searchString = 'hello';
      const getSuggestions = function() {
        return [
          {
            title: 'Hello',
            url: 'http://www.hello.com'
          },
          {
            title: 'testing saka',
            url: 'http://www.saka.io'
          }
        ];
      };

      const expectedResults = [
        {
          title: 'Hello',
          url: 'http://www.hello.com',
          score: undefined,
          matches: [
            {
              indices: [[0, 4]],
              value: 'Hello',
              key: 'title',
              arrayIndex: 0
            },
            {
              indices: [[0, 0], [11, 15]],
              value: 'http://www.hello.com',
              key: 'url',
              arrayIndex: 0
            }
          ]
        }
      ];

      const results = await libUtil.getFilteredSuggestions(searchString, {
        getSuggestions,
        threshold: 0.6,
        keys: ['title', 'url']
      });
      expect(results).toEqual(expectedResults);
    });
  });
});


================================================
FILE: test/options/MainOptions.test.js
================================================
import { h } from 'preact';
import {
  render,
  cleanup,
  wait,
  fireEvent,
  flushPromises,
  getByValue
} from 'preact-testing-library';
import 'jest-dom/extend-expect';
import MainOptions from '@/options/Main/MainOptions.jsx';

beforeEach(() => {
  browser.flush();
  browser.storage.sync.set.returns({});
});

test('should show all options when not showing key bindings', async () => {
  browser.storage.sync.get.returns({});
  const { getByText, queryByText } = render(<MainOptions />);

  getByText('Saka Options');
  await wait(() => getByText('General Settings'));
  //DefaultModeSelection
  getByText('Default Mode');
  getByText('Select the default mode Saka opens with');
  //OnlyShowSearchBarSelector
  getByText('Suggestions on load');
  getByText('Show suggestions when there is no text is the Saka search bar');
  //EnableFuzzySearch
  getByText('Enable fuzzy search');
  getByText('Enable fuzzy search for bookmarks and history search');

  expect(queryByText('Keyboard Shortcuts')).not.toBeInTheDocument();
});

test('should only show key bindings when setting is true', async () => {
  browser.storage.sync.get.returns({});
  global.SAKA_PLATFORM = 'chrome';
  const { debug, getByText, getByLabelText } = render(<MainOptions />);

  await wait(() => getByText('General Settings'));
  getByText('Saka Hotkeys');
  fireEvent.click(getByText('keyboard'), { button: 0 });
  await flushPromises();

  getByText('Saka Options');
  expect(getByText('arrow_back'));
  expect(getByLabelText('Back to Saka settings')).toMatchSnapshot();
  expect(getByLabelText('Info about Saka custom hotkeys')).toMatchSnapshot();
  getByText(
    'To modify the Saka hotkeys, please visit chrome://extensions/shortcuts'
  );
  getByText('Keyboard Shortcuts');
  getByText('Open Saka');
});

test('should save settings when save button clicked', async () => {
  browser.storage.sync.get.returns({
    sakaSettings: {}
  });
  const { getByText, getByLabelText, getByValue } = render(<MainOptions />);

  await wait(() => getByText('General Settings'));

  fireEvent.change(getByLabelText('Select default mode'), {
    target: { value: 'history' }
  });
  await flushPromises();

  fireEvent.click(getByLabelText('Suggestions on load'), { button: 0 });
  await flushPromises();

  fireEvent.click(getByLabelText('Enable fuzzy search'), { button: 0 });
  await flushPromises();

  fireEvent.click(getByValue('Save'), { button: 0 });
  await flushPromises();

  expect(browser.storage.sync.get.calledOnce);
});

afterEach(cleanup);


================================================
FILE: test/options/__snapshots__/MainOptions.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should only show key bindings when setting is true 1`] = `
<i
  aria-label="Back to Saka settings"
  aria-pressed="false"
  class="mdc-icon-toggle material-icons"
  role="button"
>
  arrow_back
</i>
`;

exports[`should only show key bindings when setting is true 2`] = `
<i
  aria-label="Info about Saka custom hotkeys"
  aria-pressed="false"
  class="mdc-icon-toggle material-icons"
  id="custom-hotkey-info"
>
  info
</i>
`;


================================================
FILE: test/suggestion_engine/providers/bookmark.test.js
================================================
const browser = require('sinon-chrome/webextensions');

import bookmarkSuggestions from 'suggestion_engine/server/providers/bookmark.js';

describe('server/providers/bookmark ', () => {
  beforeAll(() => {
    global.browser = browser;
  });

  beforeEach(() => {
    browser.flush();
  });

  describe('bookmarkSuggestions ', () => {
    it('should return all valid bookmarks when search string is empty', async () => {
      const settingsStore = {
        sakaSettings: {
          enableFuzzySearch: true
        }
      };

      const queryResults = [
        {
          url: 'https://google.com',
          title: 'Google',
          dateAdded: '2018-01-01'
        },
        {
          url: 'https://github.com/lusakasa/saka',
          title: 'Saka',
          dateAdded: '2018-02-01'
        }
      ];

      const expectedResult = [
        {
          type: 'bookmark',
          score: -1,
          url: 'https://google.com',
          title: 'Google'
        },
        {
          type: 'bookmark',
          score: -1,
          url: 'https://github.com/lusakasa/saka',
          title: 'Saka'
        }
      ];

      const searchString = '';
      browser.bookmarks.search.returns(queryResults);
      browser.storage.sync.get.returns(settingsStore);
      expect(await bookmarkSuggestions(searchString)).toEqual(expectedResult);
    });

    it('should filter all bookmarks with unknown protocol', async () => {
      const settingsStore = {
        sakaSettings: {
          enableFuzzySearch: true
        }
      };

      const queryResults = [
        {
          url: 'ssh://myhost.net',
          title: 'My Site',
          dateAdded: '2018-01-01'
        },
        {
          url: 'https://github.com/lusakasa/saka',
          title: 'Saka',
          dateAdded: '2018-02-01'
        }
      ];

      const expectedResult = [
        {
          type: 'bookmark',
          score: -1,
          url: 'https://github.com/lusakasa/saka',
          title: 'Saka'
        }
      ];

      const searchString = '';
      browser.bookmarks.search.returns(queryResults);
      browser.storage.sync.get.returns(settingsStore);
      expect(await bookmarkSuggestions(searchString)).toEqual(expectedResult);
    });

    it('should filter all bookmarks with invalid URL', async () => {
      const settingsStore = {
        sakaSettings: {
          enableFuzzySearch: true
        }
      };

      const queryResults = [
        {
          url: 'myhostnet',
          title: 'My Site',
          dateAdded: '2018-01-01'
        },
        {
          url: 'https://github.com/lusakasa/saka',
          title: 'Saka',
          dateAdded: '2018-02-01'
        }
      ];

      const expectedResult = [
        {
          type: 'bookmark',
          score: -1,
          url: 'https://github.com/lusakasa/saka',
          title: 'Saka'
        }
      ];

      const searchString = '';
      browser.bookmarks.search.returns(queryResults);
      browser.storage.sync.get.returns(settingsStore);
      expect(await bookmarkSuggestions(searchString)).toEqual(expectedResult);
    });

    it('should return fuzzy search matching results', async () => {
      const settingsStore = {
        sakaSettings: {
          enableFuzzySearch: true
        }
      };

      const queryResults = [
        {
          url: 'myhostnet',
          title: 'My Site',
          dateAdded: '2018-01-01'
        },
        {
          url: 'https://github.com/lusakasa/saka',
          title: 'Saka',
          dateAdded: '2018-02-01'
        }
      ];

      const expectedResult = [
        {
          type: 'bookmark',
          url: 'https://github.com/lusakasa/saka',
          title: 'Saka',
          score: undefined,
          matches: [
            {
              indices: [[0, 3]],
              value: 'Saka',
              key: 'title',
              arrayIndex: 0
            },
            {
              indices: [[4, 4], [21, 24]],
              value: 'https://github.com/lusakasa/saka',
              key: 'url',
              arrayIndex: 0
            }
          ]
        }
      ];

      const searchString = 'Saka';
      browser.bookmarks.search.returns(queryResults);
      browser.storage.sync.get.returns(settingsStore);
      expect(await bookmarkSuggestions(searchString)).toEqual(expectedResult);
    });
  });

  afterAll(() => {
    browser.flush();
    delete global.browser;
  });
});


================================================
FILE: test/suggestion_engine/providers/closedTab.test.js
================================================
const browser = require('sinon-chrome/webextensions');

import closedTabSuggestions, {
  getAllSuggestions
} from 'suggestion_engine/server/providers/closedTab.js';

describe('server/providers/closedTabs ', () => {
  beforeAll(() => {
    global.browser = browser;
  });

  beforeEach(() => {
    browser.flush();
  });

  describe('closedTabSuggestions ', () => {
    it('should return all closed tabs when no search string provided', async () => {
      const queryResults = [
        {
          lastModified: 123456,
          tab: {
            id: 1,
            windowId: 0,
            title: 'Saka',
            url: 'https://github.com/lusakasa/saka',
            favIconUrl: 'https://github.com/lusakasa/saka/icon.png',
            incognito: false
          }
        }
      ];
      const expectedResult = [
        {
          type: 'closedTab',
          tabId: 1,
          title: 'Saka',
          url: 'https://github.com/lusakasa/saka',
          favIconUrl: 'https://github.com/lusakasa/saka/icon.png',
          sessionId: undefined,
          score: undefined,
          incognito: false,
          lastAccessed: 123456
        }
      ];

      const searchString = '';
      const sakaId = 'abcdefg/saka.html';
      browser.runtime.getURL.returns(sakaId);
      browser.sessions.getRecentlyClosed.returns(queryResults);
      expect(await closedTabSuggestions(searchString)).toEqual(expectedResult);
    });

    it('should filter out entries for saka in recently closed tabs', async () => {
      const queryResults = [
        {
          lastModified: 123456,
          tab: {
            id: 1,
            windowId: 0,
            title: 'Saka',
            url: 'https://github.com/lusakasa/saka',
            favIconUrl: 'https://github.com/lusakasa/saka/icon.png',
            incognito: true
          }
        },
        {
          lastModified: 654321,
          tab: {
            id: 2,
            windowId: 0,
            title: 'Saka Extension',
            url: 'chrome-extension://abcdefg/saka.html',
            favIconUrl: '',
            incognito: false
          }
        }
      ];
      const expectedResult = [
        {
          type: 'closedTab',
          tabId: 1,
          title: 'Saka',
          url: 'https://github.com/lusakasa/saka',
          favIconUrl: null,
          sessionId: undefined,
          score: undefined,
          incognito: true,
          lastAccessed: 123456
        }
      ];

      const searchString = '';
      const sakaId = 'abcdefg/saka.html';
      browser.runtime.getURL.returns(sakaId);
      browser.sessions.getRecentlyClosed.returns(queryResults);
      expect(await closedTabSuggestions(searchString)).toEqual(expectedResult);
    });

    it('should return all closed tabs matching searchString', async () => {
      const queryResults = [
        {
          lastModified: 123456,
          tab: {
            id: 1,
            windowId: 0,
            title: 'Saka',
            url: 'https://github.com/lusakasa/saka',
            favIconUrl: 'https://github.com/lusakasa/saka/icon.png',
            incognito: false
          }
        },
        {
          lastModified: 654321,
          tab: {
            id: 2,
            windowId: 0,
            title: 'Google',
            url: 'https://google.com',
            favIconUrl: 'https://google.com/icon.png',
            incognito: true
          }
        }
      ];
      const expectedResult = [
        {
          type: 'closedTab',
          tabId: 2,
          title: 'Google',
          url: 'https://google.com',
          favIconUrl: null,
          sessionId: undefined,
          score: undefined,
          incognito: true,
          lastAccessed: 654321,
          matches: [
            {
              indices: [[0, 3]],
              value: 'Google',
              key: 'title',
              arrayIndex: 0
            },
            {
              indices: [[8, 11]],
              value: 'https://google.com',
              key: 'url',
              arrayIndex: 0
            }
          ]
        }
      ];

      const searchString = 'Goog';
      const sakaId = 'abcdefg/saka.html';
      browser.runtime.getURL.returns(sakaId);
      browser.sessions.getRecentlyClosed.returns(queryResults);
      expect(await closedTabSuggestions(searchString)).toEqual(expectedResult);
    });
  });

  describe('getAllSuggestions', () => {
    it('should work for window sessions', async () => {
      const queryResults = [
        {
          window: {
            lastModified: 123456,
            tabs: [
              {
                id: 1,
                windowId: 0,
                title: 'Saka',
                url: 'https://github.com/lusakasa/saka',
                favIconUrl: 'https://github.com/lusakasa/saka/icon.png',
                incognito: false
              }
            ]
          }
        }
      ];
      const expectedResult = [
        {
          type: 'closedTab',
          tabId: 1,
          title: 'Saka',
          url: 'https://github.com/lusakasa/saka',
          favIconUrl: 'https://github.com/lusakasa/saka/icon.png',
          sessionId: undefined,
          score: undefined,
          incognito: false,
          lastAccessed: 123456
        }
      ];

      const sakaId = 'abcdefg/saka.html';
      browser.runtime.getURL.returns(sakaId);
      browser.sessions.getRecentlyClosed.returns(queryResults);
      expect(await getAllSuggestions()).toEqual(expectedResult);
    });
  });

  afterAll(() => {
    browser.flush();
    delete global.browser;
  });
});


================================================
FILE: test/suggestion_engine/providers/history.test.js
================================================
const browser = require('sinon-chrome/webextensions');

import getHistorySuggestions from 'suggestion_engine/server/providers/history.js';

describe('server/providers/history ', () => {
  beforeAll(() => {
    global.browser = browser;
  });

  beforeEach(() => {
    browser.flush();
  });

  describe('getHistorySuggestions ', () => {
    it('should not use fuzzy search when enableFuzzySearch setting is set to false', async () => {
      const settingsStore = {
        sakaSettings: {
          enableFuzzySearch: false
        }
      };
      const queryResults = [
        {
          id: 1,
          url: 'https://github.com/lusakasa/saka',
          title: 'Saka Github',
          lastVisitTime: 1524795334,
          visitCount: 5,
          typedCount: 10
        },
        {
          id: 2,
          url: 'https://example.com',
          title: 'Example',
          lastVisitTime: 1524794200,
          visitCount: 1,
          typedCount: 0
        }
      ];
      const expectedResult = [
        {
          type: 'history',
          url: 'https://github.com/lusakasa/saka',
          title: 'Saka Github',
          lastAccessed: 1524795.334,
          score: 15
        },
        {
          type: 'history',
          title: 'Example',
          url: 'https://example.com',
          lastAccessed: 1524794.2,
          score: 1
        }
      ];

      const searchString = 'Saka';
      const sakaURL = 'nbdfpcokndmap/saka.html';
      browser.runtime.getURL.returns(sakaURL);
      browser.history.search.returns(queryResults);
      browser.storage.sync.get.returns(settingsStore);
      expect(await getHistorySuggestions(searchString)).toEqual(expectedResult);
    });

    it('should use fuzzy search when enableFuzzySearch setting is set to true', async () => {
      const settingsStore = {
        sakaSettings: {
          enableFuzzySearch: true
        }
      };
      const queryResults = [
        {
          id: 1,
          url: 'https://github.com/lusakasa/saka',
          title: 'Saka Github',
          lastVisitTime: 1524795334,
          visitCount: 5,
          typedCount: 10
        },
        {
          id: 2,
          url: 'https://example.com',
          title: 'Example',
          lastVisitTime: 1524794200,
          visitCount: 1,
          typedCount: 0
        }
      ];

      const expectedResult = [
        {
          type: 'history',
          url: 'https://github.com/lusakasa/saka',
          title: 'Saka Github',
          lastAccessed: 1524795.334,
          score: undefined,
          matches: [
            {
              indices: [[0, 3]],
              value: 'Saka Github',
              key: 'title',
              arrayIndex: 0
            },
            {
              indices: [[4, 4], [21, 24]],
              value: 'https://github.com/lusakasa/saka',
              key: 'url',
              arrayIndex: 0
            }
          ]
        },
        {
          type: 'history',
          title: 'Example',
          url: 'https://example.com',
          lastAccessed: 1524794.2,
          score: undefined,
          matches: [
            {
              indices: [[2, 2]],
              value: 'Example',
              key: 'title',
              arrayIndex: 0
            },
            {
              indices: [[4, 4], [10, 10]],
              value: 'https://example.com',
              key: 'url',
              arrayIndex: 0
            }
          ]
        }
      ];

      const searchString = 'Saka';
      const sakaURL = 'nbdfpcokndmap/saka.html';
      browser.runtime.getURL.returns(sakaURL);
      browser.history.search.returns(queryResults);
      browser.storage.sync.get.returns(settingsStore);
      expect(await getHistorySuggestions(searchString)).toEqual(expectedResult);
    });

    it('should return all visited site history except Saka Options URL', async () => {
      const settingsStore = {
        sakaSettings: {
          enableFuzzySearch: true
        }
      };
      const queryResults = [
        {
          id: 1,
          url: 'https://github.com/lusakasa/saka',
          title: 'Saka Github',
          lastVisitTime: 1524795334,
          visitCount: 5,
          typedCount: 10
        },
        {
          id: 2,
          url: 'chrome://nbdfpcokndmap/options.html',
          title: 'Options',
          lastVisitTime: 1524794200,
          visitCount: 1,
          typedCount: 0
        }
      ];
      const expectedResult = [
        {
          type: 'history',
          url: 'https://github.com/lusakasa/saka',
          title: 'Saka Github',
          lastAccessed: 1524795.334,
          score: 15
        }
      ];

      const searchString = '';
      const sakaURL = 'nbdfpcokndmap/saka.html';
      browser.runtime.getURL.returns(sakaURL);
      browser.history.search.returns(queryResults);
      browser.storage.sync.get.returns(settingsStore);
      expect(await getHistorySuggestions(searchString)).toEqual(expectedResult);
    });
  });

  afterAll(() => {
    browser.flush();
    delete global.browser;
  });
});


================================================
FILE: test/suggestion_engine/providers/mode.test.js
================================================
import modeSuggestions from 'suggestion_engine/server/providers/mode.js';
import { ctrlChar } from 'lib/utils';
import { colorMap, fadedColorMap } from 'lib/colors';

describe('server/providers/mode ', () => {
  describe('modeSuggestions ', () => {
    it('should return all modes when no search string provided', async () => {
      const expectedResult = [
        {
          type: 'mode',
          mode: 'tab',
          label: 'Tabs',
          shortcut: `${ctrlChar}-shift-a`,
          color: colorMap.tab,
          fadedColor: fadedColorMap.tab,
          icon: 'tab'
        },
        {
          type: 'mode',
          mode: 'closedTab',
          label: 'Recently Closed Tabs',
          shortcut: `${ctrlChar}-shift-c`,
          color: colorMap.closedTab,
          fadedColor: fadedColorMap.closedTab,
          icon: 'restore_page'
        },
        {
          type: 'mode',
          label: 'Bookmarks',
          mode: 'bookmark',
          shortcut: `${ctrlChar}-b`,
          color: colorMap.bookmark,
          fadedColor: fadedColorMap.bookmark,
          icon: 'bookmark_border'
        },
        {
          type: 'mode',
          label: 'History',
          mode: 'history',
          shortcut: `${ctrlChar}-shift-e`,
          color: colorMap.history,
          fadedColor: fadedColorMap.history,
          icon: 'history'
        },
        {
          type: 'mode',
          label: 'Recently Viewed',
          mode: 'recentlyViewed',
          shortcut: `${ctrlChar}-shift-x`,
          color: colorMap.recentlyViewed,
          fadedColor: fadedColorMap.recentlyViewed,
          icon: 'timelapse'
        }
      ];

      const searchString = '';
      expect(await modeSuggestions(searchString)).toEqual(expectedResult);
    });

    it('should return closedTab search mode when search string `cl` provided', async () => {
      const expectedResult = [
        {
          type: 'mode',
          mode: 'closedTab',
          label: 'Recently Closed Tabs',
          shortcut: `${ctrlChar}-shift-c`,
          color: 'rgba(0,0,0,1)',
          fadedColor: 'rgba(0,0,0,0.44)',
          icon: 'restore_page',
          score: undefined,
          matches: [
            {
              indices: [[2, 2], [6, 6], [9, 10]],
              value: 'Recently Closed Tabs',
              key: 'label',
              arrayIndex: 0
            }
          ]
        },
        {
          type: 'mode',
          label: 'Recently Viewed',
          mode: 'recentlyViewed',
          shortcut: `${ctrlChar}-shift-x`,
          color: 'rgba(152,78,163,1)',
          fadedColor: 'rgba(152,78,163,0.44)',
          icon: 'timelapse',
          score: undefined,
          matches: [
            {
              indices: [[2, 2], [6, 6]],
              value: 'Recently Viewed',
              key: 'label',
              arrayIndex: 0
            }
          ]
        }
      ];

      const searchString = 'cl';
      expect(await modeSuggestions(searchString)).toEqual(expectedResult);
    });
  });
});


================================================
FILE: test/suggestion_engine/providers/recentlyViewed.test.js
================================================
const browser = require('sinon-chrome/webextensions');

import recentlyViewedSuggestions from 'suggestion_engine/server/providers/recentlyViewed.js';

describe('server/providers/recentlyViewed ', () => {
  beforeAll(() => {
    global.browser = browser;
  });

  beforeEach(() => {
    browser.flush();
    // Clears the database and adds some testing data.
    // Jest will wait for this promise to resolve before running tests.
    global.SAKA_PLATFORM = 'chrome';
  });

  describe('recentlyViewedSuggestions ', () => {
    it('should return all recently viewed tabs when search string is empty', async () => {
      const trackedHistory = {
        tabHistory: [
          { tabId: 1, lastAccessed: 123456 },
          { tabId: 0, lastAccessed: 654321 }
        ],
        recentlyClosed: [
          { tab: { tabId: 1, lastAccessed: 111111 } },
          { tab: { tabId: 4, lastAccessed: 222222 } }
        ]
      };

      const tabResults = [
        {
          id: 1,
          windowId: 0,
          title: 'Google',
          url: 'https://google.com',
          favIconUrl: 'https://google.com/icon.png',
          incognito: false,
          lastAccessed: 123456
        },
        {
          id: 0,
          windowId: 0,
          title: 'Saka',
          url: 'https://github.com/lusakasa/saka',
          favIconUrl: 'https://github.com/lusakasa/saka/icon.png',
          incognito: true,
          lastAccessed: 654321
        }
      ];

      const recentlyClosedResults = [
        {
          lastModified: 123456,
          tab: {
            id: 1,
            windowId: 0,
            title: 'Saka',
            url: 'https://github.com/lusakasa/saka',
            favIconUrl: 'https://github.com/lusakasa/saka/icon.png',
            incognito: false,
            sessionId: 'abc123'
          }
        },
        {
          lastModified: 19191,
          tab: {
            id: 2,
            windowId: 0,
            title: 'Recently Viewed Mode',
            url: 'https://github.com/lusakasa/saka/pull/45',
            favIconUrl: 'https://github.com/icon.png',
            incognito: true,
            sessionId: '123abc'
          }
        }
      ];

      const historyResults = [
        {
          id: 1,
          url: 'https://github.com/lusakasa/saka',
          title: 'Saka Github',
          lastVisitTime: 1524795334,
          visitCount: 5,
          typedCount: 10
        },
        {
          id: 3,
          url: 'https://example.com',
          title: 'Example',
          lastVisitTime: 1524794200,
          visitCount: 1,
          typedCount: 0
        }
      ];

      const expectedResult = [
        {
          type: 'recentlyViewed',
          url: 'https://example.com',
          title: 'Example',
          lastAccessed: 1524794.2,
          score: 1,
          originalType: 'history'
        },
        {
          lastAccessed: 19191,
          tabId: 2,
          title: 'Recently Viewed Mode',
          url: 'https://github.com/lusakasa/saka/pull/45',
          favIconUrl: null,
          incognito: true,
          sessionId: '123abc',
          score: undefined,
          type: 'recentlyViewed',
          originalType: 'closedTab'
        },
        {
          type: 'recentlyViewed',
          tabId: 0,
          windowId: 0,
          title: 'Saka',
          url: 'https://github.com/lusakasa/saka',
          favIconUrl: null,
          incognito: true,
          lastAccessed: 654.321,
          originalType: 'tab'
        },
        {
          type: 'recentlyViewed',
          tabId: 1,
          windowId: 0,
          title: 'Google',
          url: 'https://google.com',
          favIconUrl: 'https://google.com/icon.png',
          incognito: false,
          lastAccessed: 123.456,
          originalType: 'tab'
        }
      ];

      const searchString = '';
      const sakaId = 'abcdefg/saka.html';
      browser.tabs.query.returns(tabResults);
      browser.runtime.getURL.returns(sakaId);
      browser.runtime.getBackgroundPage.returns(trackedHistory);
      browser.sessions.getRecentlyClosed.returns(recentlyClosedResults);
      browser.history.search.returns(historyResults);
      expect(await recentlyViewedSuggestions(searchString)).toEqual(
        expectedResult
      );
    });

    it('should return matching recently viewed tabs when search string is not empty', async () => {
      const tabHistory = { tabHistory: [0] };
      const tabResults = [
        {
          id: 1,
          windowId: 0,
          title: 'Google',
          url: 'https://google.com',
          favIconUrl: 'https://google.com/icon.png',
          incognito: false,
          lastAccessed: 123456
        },
        {
          id: 0,
          windowId: 0,
          title: 'Saka',
          url: 'https://github.com/lusakasa/saka',
          favIconUrl: 'https://github.com/lusakasa/saka/icon.png',
          incognito: true,
          lastAccessed: 654321
        }
      ];

      const recentlyClosedResults = [
        {
          lastModified: 123456,
          tab: {
            id: 1,
            windowId: 0,
            title: 'Saka',
            url: 'https://github.com/lusakasa/saka',
            favIconUrl: 'https://github.com/lusakasa/saka/icon.png',
            incognito: false,
            sessionId: 'abc123'
          }
        },
        {
          lastModified: 19191,
          tab: {
            id: 2,
            windowId: 0,
            title: 'Recently Viewed Mode',
            url: 'https://github.com/lusakasa/saka/pull/45',
            favIconUrl: 'https://github.com/icon.png',
            incognito: true,
            sessionId: '123abc'
          }
        }
      ];

      const historyResults = [
        {
          id: 1,
          url: 'https://github.com/lusakasa/saka',
          title: 'Saka Github',
          lastVisitTime: 1524795334,
          visitCount: 5,
          typedCount: 10
        },
        {
          id: 3,
          url: 'https://example.com',
          title: 'Example',
          lastVisitTime: 1524794200,
          visitCount: 1,
          typedCount: 0
        }
      ];

      const expectedResult = [
        {
          type: 'recentlyViewed',
          tabId: 0,
          windowId: 0,
          title: 'Saka',
          url: 'https://github.com/lusakasa/saka',
          favIconUrl: null,
          incognito: true,
          lastAccessed: 654.321,
          originalType: 'tab',
          score: undefined,
          matches: [
            Object({
              indices: [[0, 3]],
              value: 'Saka',
              key: 'title',
              arrayIndex: 0
            }),
            Object({
              indices: [[4, 4], [21, 24]],
              value: 'https://github.com/lusakasa/saka',
              key: 'url',
              arrayIndex: 0
            })
          ]
        },
        {
          type: 'recentlyViewed',
          url: 'https://github.com/lusakasa/saka',
          title: 'Saka Github',
          lastAccessed: 1524795.334,
          score: undefined,
          originalType: 'history',
          matches: [
            Object({
              indices: [[0, 3]],
              value: 'Saka Github',
              key: 'title',
              arrayIndex: 0
            }),
            Object({
              indices: [[4, 4], [21, 24]],
              value: 'https://github.com/lusakasa/saka',
              key: 'url',
              arrayIndex: 0
            })
          ]
        },
        {
          type: 'recentlyViewed',
          tabId: 2,
          title: 'Recently Viewed Mode',
          url: 'https://github.com/lusakasa/saka/pull/45',
          favIconUrl: null,
          incognito: true,
          lastAccessed: 19191,
          sessionId: '123abc',
          score: undefined,
          originalType: 'closedTab',
          matches: [
            Object({
              indices: [[4, 4], [21, 24]],
              value: 'https://github.com/lusakasa/saka/pull/45',
              key: 'url',
              arrayIndex: 0
            })
          ]
        }
      ];

      const searchString = 'saka';
      const sakaId = 'abcdefg/saka.html';
      browser.tabs.query.returns(tabResults);
      browser.runtime.getURL.returns(sakaId);
      browser.runtime.getBackgroundPage.returns(tabHistory);
      browser.sessions.getRecentlyClosed.returns(recentlyClosedResults);
      browser.history.search.returns(historyResults);
      expect(await recentlyViewedSuggestions(searchString)).toEqual(
        expectedResult
      );
    });
  });

  afterAll(() => {
    browser.flush();
    delete global.browser;
  });
});


================================================
FILE: test/suggestion_engine/providers/tab.test.js
================================================
const browser = require('sinon-chrome/webextensions');

import tabSuggestions from 'suggestion_engine/server/providers/tab.js';

describe('server/providers/tab ', () => {
  beforeAll(() => {
    global.browser = browser;
  });

  beforeEach(() => {
    browser.flush();
  });

  describe('tabSuggestions ', () => {
    it('should return all recent tabs when search string is empty', async () => {
      const queryResults = [
        {
          id: 1,
          windowId: 0,
          title: 'Google',
          url: 'https://google.com',
          favIconUrl: 'https://google.com/icon.png',
          incognito: false,
          lastAccessed: 123456
        },
        {
          id: 0,
          windowId: 0,
          title: 'Saka',
          url: 'https://github.com/lusakasa/saka',
          favIconUrl: 'https://github.com/lusakasa/saka/icon.png',
          incognito: true,
          lastAccessed: 654321
        }
      ];

      const tabHistory = {
        tabHistory: [
          { tabId: 1, lastAccessed: 123456 },
          { tabId: 0, lastAccessed: 654321 }
        ]
      };

      const expectedResult = [
        {
          type: 'tab',
          tabId: 1,
          windowId: 0,
          title: 'Google',
          url: 'https://google.com',
          favIconUrl: 'https://google.com/icon.png',
          incognito: false,
          lastAccessed: 123456
        },
        {
          type: 'tab',
          tabId: 0,
          windowId: 0,
          title: 'Saka',
          url: 'https://github.com/lusakasa/saka',
          favIconUrl: null,
          incognito: true,
          lastAccessed: 654321
        }
      ];

      const searchString = '';
      browser.tabs.query.returns(queryResults);
      browser.runtime.getBackgroundPage.returns(tabHistory);
      expect(await tabSuggestions(searchString)).toEqual(expectedResult);
    });

    it('should return all tabs matching searchString', async () => {
      const queryResults = [
        {
          id: 0,
          windowId: 0,
          title: 'Saka',
          url: 'https://github.com/lusakasa/saka',
          favIconUrl: 'https://github.com/lusakasa/saka/icon.png',
          incognito: false,
          lastAccessed: 123456
        },
        {
          id: 0,
          windowId: 0,
          title: 'Google',
          url: 'https://google.com',
          favIconUrl: 'https://google.com/icon.png',
          incognito: false,
          lastAccessed: 654321
        }
      ];

      const expectedResult = [
        {
          type: 'tab',
          tabId: 0,
          windowId: 0,
          title: 'Saka',
          url: 'https://github.com/lusakasa/saka',
          favIconUrl: 'https://github.com/lusakasa/saka/icon.png',
          incognito: false,
          lastAccessed: 123.456,
          matches: [
            {
              indices: [[0, 3]],
              value: 'Saka',
              key: 'title',
              arrayIndex: 0
            },
            {
              indices: [[4, 4], [21, 24]],
              value: 'https://github.com/lusakasa/saka',
              key: 'url',
              arrayIndex: 0
            }
          ],
          score: undefined
        }
      ];

      const searchString = 'saka';
      browser.tabs.query.returns(queryResults);
      expect(await tabSuggestions(searchString)).toEqual(expectedResult);
    });
  });

  afterAll(() => {
    browser.flush();
    delete global.browser;
  });
});


================================================
FILE: test/suggestion_engine/server/index.test.js
================================================
import {
  getSuggestions,
  activateSuggestion,
  closeTab
} from 'suggestion_engine/server/index.js';
import * as providers from 'suggestion_engine/server/providers/index.js';

jest.mock('suggestion_engine/server/providers/index.js', () => ({
  tab: jest.fn().mockImplementation(() => [
    {
      type: 'tab'
    }
  ]),
  closedTab: jest.fn(),
  mode: jest.fn(),
  history: jest.fn(),
  bookmark: jest.fn(),
  recentlyViewed: jest.fn()
}));

test('should show all options when not showing key bindings', async () => {
  const mockexpectedResult = [
    {
      type: 'tab'
    }
  ];

  const suggestions = await getSuggestions(['tab', 'saka']);
  expect(suggestions).toEqual(mockexpectedResult);
});

test('should call appropriate activation methods', async () => {
  browser.flush();
  await activateSuggestion({
    type: 'tab'
  });
  expect(browser.tabs.update.calledOnce).toEqual(true);
  expect(browser.windows.update.calledOnce).toEqual(true);

  browser.flush();
  await activateSuggestion({
    type: 'closedTab'
  });
  expect(browser.sessions.restore.calledOnce).toEqual(true);

  browser.flush();
  await activateSuggestion({
    type: 'bookmark'
  });
  expect(browser.tabs.create.calledOnce).toEqual(true);

  browser.flush();
  await activateSuggestion({
    type: 'history'
  });
  expect(browser.tabs.create.calledOnce).toEqual(true);

  browser.flush();
  await activateSuggestion({
    type: 'recentlyViewed',
    originalType: 'tab'
  });
  expect(browser.tabs.update.calledOnce).toEqual(true);
});

test('should focus bookmark and history tabs if already open', async () => {
  browser.flush();
  browser.tabs.query.resolves([{ id: '1', windowId: '1' }]);
  await activateSuggestion({
    type: 'bookmark'
  });
  expect(browser.tabs.update.calledOnce).toEqual(true);
  expect(browser.windows.update.calledOnce).toEqual(true);

  browser.flush();
  browser.tabs.query.resolves([{ id: '1', windowId: '1' }]);
  await activateSuggestion({
    type: 'history'
  });
  expect(browser.tabs.update.calledOnce).toEqual(true);
  expect(browser.windows.update.calledOnce).toEqual(true);
});

test('should call close tab API', async () => {
  const suggestion = {
    tabId: 1
  };

  await closeTab(suggestion);
});


================================================
FILE: test/suggestion_utils/index.test.js
================================================
import { preprocessSuggestion } from '@/suggestion_utils/index.js';
import { colorMap, fadedColorMap } from 'lib/colors';
import * as url from 'lib/url.js';

test('should return suggestion with pretty URL when type is tab', () => {
  url.prettifyURL = jest
    .fn()
    .mockImplementation(() => 'https://github.com/lusakasa/saka');

  const suggestion = {
    type: 'tab',
    tabId: 0,
    windowId: 0,
    title: 'Saka',
    url: 'https://github.com/lusakasa/saka?stuffInUrl=true',
    favIconUrl: 'https://github.com/lusakasa/saka/icon.png',
    incognito: false,
    lastAccessed: 123.456,
    matches: [],
    score: undefined
  };
  const searchText = 'saka';
  const result = preprocessSuggestion(suggestion, searchText);
  expect(result).toEqual({
    ...suggestion,
    prettyURL: 'https://github.com/lusakasa/saka',
    text: suggestion.title
  });
});

test('should return suggestion with pretty URL when type is closedTab', () => {
  url.prettifyURL = jest
    .fn()
    .mockImplementation(() => 'https://github.com/lusakasa/saka');

  const suggestion = {
    type: 'closedTab',
    tabId: 1,
    title: 'Saka',
    url: 'https://github.com/lusakasa/saka?stuffInUrl=true',
    favIconUrl: 'https://github.com/lusakasa/saka/icon.png',
    sessionId: undefined,
    score: undefined,
    incognito: false,
    lastAccessed: 123456
  };
  const searchText = 'saka';
  const result = preprocessSuggestion(suggestion, searchText);
  expect(result).toEqual({
    ...suggestion,
    prettyURL: 'https://github.com/lusakasa/saka',
    text: suggestion.title
  });
});

test('should return suggestion when type is mode', () => {
  url.prettifyURL = jest.fn().mockImplementation(suggestion => suggestion);

  const suggestion = {
    type: 'mode',
    mode: 'tab',
    label: 'Tabs',
    shortcut: `ctrl-shift-a`,
    color: colorMap.tab,
    fadedColor: fadedColorMap.tab,
    icon: 'tab'
  };

  const searchText = 'saka';
  const result = preprocessSuggestion(suggestion, searchText);
  expect(result).toEqual(suggestion);
});

test('should return suggestion with pretty URL when type is bookmark', () => {
  url.prettifyURL = jest
    .fn()
    .mockImplementation(() => 'https://github.com/lusakasa/saka');

  const suggestion = {
    type: 'bookmark',
    score: -1,
    url: 'https://github.com/lusakasa/saka',
    title: 'Saka'
  };

  const searchText = 'saka';
  const result = preprocessSuggestion(suggestion, searchText);
  expect(result).toEqual({
    ...suggestion,
    prettyURL: 'https://github.com/lusakasa/saka',
    text: 'https://github.com/lusakasa/saka'
  });
});

test('should return suggestion with pretty URL when type is history', () => {
  url.prettifyURL = jest
    .fn()
    .mockImplementation(() => 'https://github.com/lusakasa/saka');

  const suggestion = {
    type: 'history',
    url: 'https://github.com/lusakasa/saka',
    title: 'Saka Github',
    lastAccessed: 1524795.334,
    score: 15
  };

  const searchText = 'saka';
  const result = preprocessSuggestion(suggestion, searchText);
  expect(result).toEqual({
    ...suggestion,
    prettyURL: 'https://github.com/lusakasa/saka',
    text: 'https://github.com/lusakasa/saka'
  });
});

test('should return suggestion with pretty URL when type is recentlyViewed', () => {
  url.prettifyURL = jest
    .fn()
    .mockImplementation(() => 'https://github.com/lusakasa/saka');

  const suggestion = {
    type: 'recentlyViewed',
    url: 'https://example.com',
    title: 'Example',
    lastAccessed: 1524794.2,
    score: 1,
    originalType: 'history'
  };

  const searchText = 'saka';
  const result = preprocessSuggestion(suggestion, searchText);
  expect(result).toEqual({
    ...suggestion,
    prettyURL: 'https://github.com/lusakasa/saka',
    text: 'https://github.com/lusakasa/saka'
  });
});

test('should return error when type is unexpected', () => {
  const suggestion = {
    type: 'adsknajksfnasjfnjas',
    url: 'https://example.com',
    title: 'Example',
    lastAccessed: 1524794.2,
    score: 1,
    originalType: 'history'
  };

  const searchText = 'saka';
  const result = preprocessSuggestion(suggestion, searchText);
  expect(result).toEqual({
    type: 'error',
    title: `Error. Unknown Suggestion type: ${suggestion.type}`,
    text: `Error. Unknown Suggestion type: ${suggestion.type}`
  });
});


================================================
FILE: webpack.config.js
================================================
const webpack = require('webpack');
const BabiliPlugin = require('babili-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const GenerateJsonPlugin = require('generate-json-webpack-plugin');
const marked = require('marked');
const path = require('path');
const merge = require('webpack-merge');
const { version } = require('./manifest/common.json'); // mode controls:

const renderer = new marked.Renderer();
// process.traceDeprecation = true;
// markdown convert to html

module.exports = function webpackConfig(env) {
  const [mode, platform, benchmark] = env.split(':');

  const config = {
    resolve: {
      alias: {
        react: 'preact-compat',
        'react-dom': 'preact-compat',
        src: path.join(__dirname, 'src'),
        msg: path.join(__dirname, 'src/msg'),
        suggestion_engine: path.join(__dirname, 'src/suggestion_engine'),
        suggestion_utils: path.join(__dirname, 'src/suggestion_utils'),
        lib: path.join(__dirname, 'src/lib'),
        scss: path.join(__dirname, 'src/scss')
      },
      modules: ['./src', './node_modules']
    },
    entry: {
      background_page: 'src/background_page/index.js',
      toggle_saka: 'src/content_script/toggle_saka.js',
      // 'extensions': './src/pages/extensions/index.js',
      // 'info': './src/pages/info/index.js',
      // 'options': './src/pages/options/index.js',
      saka: 'src/saka/index.jsx',
      'saka-options': 'src/options/saka-options.jsx'
    },
    optimization: {
      splitChunks: {
        cacheGroups: {
          commons: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendor',
            chunks: 'all'
          }
        }
      }
    },
    output: {
      path: `${__dirname}/dist`,
      filename: '[name].js'
      // sourceMapFilename: '[name].js.map'
    },
    module: {
      rules: [
        {
          test: /\.(jsx|js)$/,
          exclude: /node_modules/,
          loaders: ['babel-loader']
        },
        {
          test: /\.(sc|c)ss$/,
          loaders: [
            'style-loader',
            'css-loader',
            {
              loader: 'sass-loader',
              options: {
                importer(url, prev) {
                  if (url.indexOf('@material') === 0) {
                    const filePath = url.split('@material')[1];
                    const nodeModulePath = `./node_modules/@material/${filePath}`;
                    return { file: path.resolve(nodeModulePath) };
                  }
                  r
Download .txt
gitextract_snas81cp/

├── .babelrc
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .travis.yml
├── .vscode/
│   └── launch.json
├── LICENSE
├── README.md
├── docs/
│   └── pull_request_template.md
├── images/
│   └── favicons/
│       ├── browserconfig.xml
│       └── manifest.json
├── jest.config.js
├── jest.transform.js
├── manifest/
│   ├── chrome.json
│   ├── common.json
│   └── firefox.json
├── package.json
├── spec/
│   └── support/
│       └── jasmine.json
├── src/
│   ├── background_page/
│   │   ├── index.js
│   │   └── tabHistory.js
│   ├── content_script/
│   │   └── toggle_saka.js
│   ├── lib/
│   │   ├── colors.js
│   │   ├── dom.js
│   │   ├── highlight.jsx
│   │   ├── log.js
│   │   ├── tld.js
│   │   ├── trie.js
│   │   ├── url.js
│   │   └── utils.js
│   ├── msg/
│   │   ├── client.js
│   │   └── server.js
│   ├── options/
│   │   ├── Main/
│   │   │   ├── MainOptions.jsx
│   │   │   ├── OptionsList/
│   │   │   │   ├── DefaultModeSelection.jsx
│   │   │   │   ├── EnableFuzzySearch.jsx
│   │   │   │   ├── OnlyShowSearchBarSelector.jsx
│   │   │   │   ├── ShowSakaHotkeys.jsx
│   │   │   │   └── index.jsx
│   │   │   └── SakaHotkeysList/
│   │   │       ├── HotkeyListRow.jsx
│   │   │       └── index.jsx
│   │   └── saka-options.jsx
│   ├── saka/
│   │   ├── Main/
│   │   │   ├── Components/
│   │   │   │   ├── BackgroundImage/
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── GUIContainer/
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── Icon/
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── ModeSwitcher/
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── PaginationBar/
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── SearchBar/
│   │   │   │   │   ├── Button/
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── Input/
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── SettingsBar/
│   │   │   │   │   └── index.jsx
│   │   │   │   └── SuggestionList/
│   │   │   │       ├── Components/
│   │   │   │       │   └── Suggestion/
│   │   │   │       │       └── index.jsx
│   │   │   │       ├── Containers/
│   │   │   │       │   ├── BookmarkSuggestion/
│   │   │   │       │   │   └── index.jsx
│   │   │   │       │   ├── ClosedTabSuggestion/
│   │   │   │       │   │   └── index.jsx
│   │   │   │       │   ├── CommandSuggestion/
│   │   │   │       │   │   └── index.jsx
│   │   │   │       │   ├── HistorySuggestion/
│   │   │   │       │   │   └── index.jsx
│   │   │   │       │   ├── RecentlyViewedSuggestion/
│   │   │   │       │   │   └── index.jsx
│   │   │   │       │   ├── SearchEngineSuggestion/
│   │   │   │       │   │   └── index.jsx
│   │   │   │       │   ├── SuggestionSelector.jsx
│   │   │   │       │   ├── TabSuggestion/
│   │   │   │       │   │   └── index.jsx
│   │   │   │       │   └── UnknownSuggestion/
│   │   │   │       │       └── index.jsx
│   │   │   │       └── index.jsx
│   │   │   ├── Containers/
│   │   │   │   ├── GeneralSearch/
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── StandardSearch/
│   │   │   │   │   └── index.jsx
│   │   │   │   └── TabSearch/
│   │   │   │       └── index.jsx
│   │   │   └── index.jsx
│   │   └── index.jsx
│   ├── scss/
│   │   ├── options.scss
│   │   └── styles.scss
│   ├── suggestion_engine/
│   │   ├── client/
│   │   │   └── index.js
│   │   └── server/
│   │       ├── index.js
│   │       └── providers/
│   │           ├── bookmark.js
│   │           ├── closedTab.js
│   │           ├── command.js
│   │           ├── history.js
│   │           ├── index.js
│   │           ├── mode.js
│   │           ├── recentlyViewed.js
│   │           ├── searchEngine.js
│   │           └── tab.js
│   └── suggestion_utils/
│       └── index.js
├── static/
│   ├── background_page.html
│   ├── material-icons.css
│   ├── options.html
│   └── saka.html
├── test/
│   ├── Icon.test.js
│   ├── Main.test.js
│   ├── ModeSwitcher.test.js
│   ├── PaginationBar.test.js
│   ├── SearchBar.test.js
│   ├── StandardSearch/
│   │   └── StandardSearch.test.js
│   ├── Suggestion.test.js
│   ├── SuggestionList.test.js
│   ├── __mocks__/
│   │   ├── browser-mocks.js
│   │   └── styleMock.scss
│   ├── __snapshots__/
│   │   ├── ModeSwitcher.test.js.snap
│   │   ├── SearchBar.test.js.snap
│   │   ├── Suggestion.test.js.snap
│   │   └── SuggestionList.test.js.snap
│   ├── lib/
│   │   ├── hightlight.test.js
│   │   ├── log.test.js
│   │   ├── url.test.js
│   │   └── utils.test.js
│   ├── options/
│   │   ├── MainOptions.test.js
│   │   └── __snapshots__/
│   │       └── MainOptions.test.js.snap
│   ├── suggestion_engine/
│   │   ├── providers/
│   │   │   ├── bookmark.test.js
│   │   │   ├── closedTab.test.js
│   │   │   ├── history.test.js
│   │   │   ├── mode.test.js
│   │   │   ├── recentlyViewed.test.js
│   │   │   └── tab.test.js
│   │   └── server/
│   │       └── index.test.js
│   └── suggestion_utils/
│       └── index.test.js
└── webpack.config.js
Download .txt
SYMBOL INDEX (90 symbols across 30 files)

FILE: src/background_page/index.js
  function toggleSaka (line 10) | async function toggleSaka(tabId) {
  function closeSaka (line 94) | async function closeSaka(tab) {
  function saveSettings (line 108) | async function saveSettings(searchHistory) {

FILE: src/background_page/tabHistory.js
  function setMostRecentTab (line 14) | function setMostRecentTab(tabInfo) {
  function setMostRecentClosedTab (line 23) | function setMostRecentClosedTab(tabInfo) {

FILE: src/lib/dom.js
  function slowWheelEvent (line 1) | function slowWheelEvent(
  function cursorAtEnd (line 26) | function cursorAtEnd(input) {

FILE: src/lib/highlight.jsx
  function highlighted (line 3) | function highlighted(text, indices) {
  function highlight (line 27) | function highlight(text, key, matches) {

FILE: src/lib/trie.js
  class Trie (line 21) | class Trie {

FILE: src/lib/url.js
  function prettifyURL (line 8) | function prettifyURL(url, searchString) {
  function isURL (line 28) | function isURL(str) {
  function extractProtocol (line 39) | function extractProtocol(url) {
  function stripProtocol (line 47) | function stripProtocol(url) {
  function stripWWW (line 51) | function stripWWW(url) {
  function startsWithProtocol (line 55) | function startsWithProtocol(str) {
  function startsWithWWW (line 59) | function startsWithWWW(str) {
  function isTLD (line 64) | function isTLD(text) {
  function isProtocol (line 79) | function isProtocol(text) {
  function isLikeURL (line 83) | function isLikeURL(url) {
  function isSakaUrl (line 124) | async function isSakaUrl(url) {

FILE: src/lib/utils.js
  function rangedIncrement (line 6) | function rangedIncrement(value, increment, min, max) {
  function ctrlKey (line 21) | function ctrlKey(e) {
  function objectFromArray (line 25) | function objectFromArray(array, key) {
  function getFilteredSuggestions (line 33) | async function getFilteredSuggestions(

FILE: src/options/Main/MainOptions.jsx
  class MainOptions (line 8) | class MainOptions extends Component {
    method constructor (line 9) | constructor(props) {
    method render (line 23) | render() {

FILE: src/options/Main/OptionsList/index.jsx
  class OptionsList (line 8) | class OptionsList extends Component {
    method constructor (line 9) | constructor(props) {
    method componentDidMount (line 20) | async componentDidMount() {
    method render (line 70) | render() {

FILE: src/saka/Main/Components/BackgroundImage/index.jsx
  class BackgroundImage (line 6) | class BackgroundImage extends Component {
    method componentDidMount (line 11) | componentDidMount() {
    method render (line 20) | render() {

FILE: src/saka/Main/Components/GUIContainer/index.jsx
  class GUIContainer (line 6) | class GUIContainer extends Component {
    method componentWillMount (line 11) | componentWillMount() {
    method componentWillUnmount (line 16) | componentWillUnmount() {
    method render (line 28) | render() {

FILE: src/saka/Main/Components/SearchBar/Button/index.jsx
  method render (line 46) | render() {

FILE: src/saka/Main/Components/SearchBar/Input/index.jsx
  class Input (line 6) | class Input extends Component {
    method render (line 7) | render() {

FILE: src/saka/Main/Containers/StandardSearch/index.jsx
  method componentDidMount (line 30) | componentDidMount() {
  method componentDidUpdate (line 41) | componentDidUpdate(prevProps) {
  method render (line 344) | render() {

FILE: src/saka/Main/index.jsx
  class Main (line 7) | class Main extends Component {
    method constructor (line 8) | constructor(props) {
    method componentDidMount (line 20) | async componentDidMount() {
    method render (line 68) | render() {

FILE: src/suggestion_engine/client/index.js
  function getSuggestions (line 3) | async function getSuggestions(mode, searchString) {
  function activateSuggestion (line 7) | async function activateSuggestion(suggestion) {
  function closeTab (line 11) | async function closeTab(suggestion) {

FILE: src/suggestion_engine/server/index.js
  function getSuggestions (line 4) | async function getSuggestions([mode, searchString]) {
  function focusOrCreateTab (line 8) | async function focusOrCreateTab(url) {
  function activateSuggestion (line 22) | async function activateSuggestion(suggestion) {
  function closeTab (line 52) | async function closeTab(suggestion) {

FILE: src/suggestion_engine/server/providers/bookmark.js
  function allBookmarkSuggestions (line 6) | async function allBookmarkSuggestions(searchText) {
  function bookmarkSuggestions (line 26) | async function bookmarkSuggestions(searchString) {

FILE: src/suggestion_engine/server/providers/closedTab.js
  function getAllSuggestions (line 7) | async function getAllSuggestions() {
  function recentlyClosedTabSuggestions (line 51) | async function recentlyClosedTabSuggestions() {
  function closedTabSuggestions (line 95) | async function closedTabSuggestions(searchString) {

FILE: src/suggestion_engine/server/providers/command.js
  function commandSuggestions (line 5) | function commandSuggestions(searchText) {

FILE: src/suggestion_engine/server/providers/history.js
  function allHistorySuggestions (line 5) | async function allHistorySuggestions(searchText) {
  function historySuggestions (line 28) | async function historySuggestions(searchString) {

FILE: src/suggestion_engine/server/providers/mode.js
  function modeSuggestions (line 60) | async function modeSuggestions(searchString) {

FILE: src/suggestion_engine/server/providers/recentlyViewed.js
  function compareRecentlyViewedSuggestions (line 12) | function compareRecentlyViewedSuggestions(suggestion1, suggestion2) {
  function allRecentlyViewedSuggestions (line 16) | async function allRecentlyViewedSuggestions(searchString) {
  function filteredRecentlyViewedSuggestions (line 44) | async function filteredRecentlyViewedSuggestions(searchString) {
  function getFilteredRecentlyViewedSuggestions (line 56) | async function getFilteredRecentlyViewedSuggestions(searchString) {
  function recentlyViewedSuggestions (line 73) | async function recentlyViewedSuggestions(searchString) {

FILE: src/suggestion_engine/server/providers/searchEngine.js
  function searchEngineSuggestions (line 3) | async function searchEngineSuggestions(searchText) {

FILE: src/suggestion_engine/server/providers/tab.js
  function allTabSuggestions (line 4) | async function allTabSuggestions() {
  function recentTabSuggestions (line 28) | async function recentTabSuggestions() {
  function recentVisitedTabSuggestions (line 43) | async function recentVisitedTabSuggestions() {
  function tabSuggestions (line 57) | async function tabSuggestions(searchString) {

FILE: src/suggestion_utils/index.js
  function preprocessSuggestion (line 13) | function preprocessSuggestion(suggestion, searchText) {

FILE: test/PaginationBar.test.js
  method onClickPrevious (line 11) | onClickPrevious() {}
  method onClickNext (line 12) | onClickNext() {}
  method onClickPrevious (line 37) | onClickPrevious() {}
  method onClickNext (line 38) | onClickNext() {}

FILE: test/SearchBar.test.js
  method onKeyDown (line 13) | onKeyDown() {}
  method onInput (line 14) | onInput() {}
  method onBlur (line 15) | onBlur() {}
  method onButtonClick (line 16) | onButtonClick() {}
  method onKeyDown (line 29) | onKeyDown() {}
  method onInput (line 30) | onInput() {}
  method onBlur (line 31) | onBlur() {}
  method onButtonClick (line 32) | onButtonClick() {}

FILE: test/SuggestionList.test.js
  constant MAX_SUGGESTIONS (line 5) | const MAX_SUGGESTIONS = 6;

FILE: webpack.config.js
  method importer (line 71) | importer(url, prev) {
Condensed preview — 114 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (202K chars).
[
  {
    "path": ".babelrc",
    "chars": 166,
    "preview": "{\n  \"sourceMaps\": true,\n  \"plugins\": [\n    [\"transform-react-jsx\", { \"pragma\": \"h\" }],\n    [\"transform-object-rest-sprea"
  },
  {
    "path": ".eslintignore",
    "chars": 6,
    "preview": "test/*"
  },
  {
    "path": ".eslintrc.json",
    "chars": 845,
    "preview": "{\n  \"parser\": \"babel-eslint\",\n  \"plugins\": [\"prettier\"],\n  \"extends\": [\"airbnb\", \"prettier\"],\n  \"rules\": {\n    \"react/re"
  },
  {
    "path": ".gitignore",
    "chars": 132,
    "preview": "dist\ndist.zip\ndist.crx\ndist.pem\nnode_modules\nnpm-debug.log\ndebug.log\n.DS_Store\nyarn-error.log\ncoverage\nsaka-*.zip\npackag"
  },
  {
    "path": ".prettierignore",
    "chars": 12,
    "preview": "package.json"
  },
  {
    "path": ".prettierrc",
    "chars": 26,
    "preview": "{\n  \"singleQuote\": true\n}\n"
  },
  {
    "path": ".travis.yml",
    "chars": 1422,
    "preview": "language: node_js\nnode_js:\n  - lts/*\n\ninstall:\n  - yarn global add codecov\n  - yarn install\nscript:\n  - yarn test --cove"
  },
  {
    "path": ".vscode/launch.json",
    "chars": 424,
    "preview": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"name\": \"Debug Jest Tests\",\n      \"type\": \"node\",\n      \"reque"
  },
  {
    "path": "LICENSE",
    "chars": 1096,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Sufyan Dawoodjee, Uzair Shamim\n\nPermission is hereby granted, free of charge, "
  },
  {
    "path": "README.md",
    "chars": 2075,
    "preview": "# Saka [![GitHub license](https://img.shields.io/github/license/lusakasa/saka.svg)](https://github.com/lusakasa/saka/blo"
  },
  {
    "path": "docs/pull_request_template.md",
    "chars": 555,
    "preview": "## Type of Change\n> Put an [x] for the relevant option\n- [ ] Bugfix/Cleanup (non-breaking change which fixes an issue)\n-"
  },
  {
    "path": "images/favicons/browserconfig.xml",
    "chars": 246,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig>\n    <msapplication>\n        <tile>\n            <square150x150logo"
  },
  {
    "path": "images/favicons/manifest.json",
    "chars": 403,
    "preview": "{\n    \"name\": \"\",\n    \"icons\": [\n        {\n            \"src\": \"/android-chrome-192x192.png\",\n            \"sizes\": \"192x1"
  },
  {
    "path": "jest.config.js",
    "chars": 1149,
    "preview": "module.exports = {\n  moduleNameMapper: {\n    '@/(.*)$': '<rootDir>/src/$1',\n    '^src/(.*)$': '<rootDir>/src/$1',\n    '^"
  },
  {
    "path": "jest.transform.js",
    "chars": 251,
    "preview": "const babelOptions = {\n  presets: [['env', { targets: { node: '8' } }], 'react'],\n  plugins: [\n    [\n      'transform-re"
  },
  {
    "path": "manifest/chrome.json",
    "chars": 639,
    "preview": "{\n  \"background\": {\n    \"page\": \"background_page.html\",\n    \"persistent\": true\n  },\n  \"commands\": {\n    \"toggleSaka4\": {"
  },
  {
    "path": "manifest/common.json",
    "chars": 1252,
    "preview": "{\n  \"name\": \"Saka\",\n  \"version\": \"0.17.3\",\n  \"author\": \"Sufyan Dawoodjee, Uzair Shamim\",\n  \"description\": \"Saka - elegen"
  },
  {
    "path": "manifest/firefox.json",
    "chars": 102,
    "preview": "{\n  \"applications\": {\n    \"gecko\": {\n      \"id\": \"{7d7cad35-2182-4457-972d-5a41a2051240}\"\n    }\n  }\n}\n"
  },
  {
    "path": "package.json",
    "chars": 3113,
    "preview": "{\n  \"name\": \"saka\",\n  \"description\": \"A keyboard interface to the web\",\n  \"scripts\": {\n    \"build\": \"echo \\\"You must spe"
  },
  {
    "path": "spec/support/jasmine.json",
    "chars": 171,
    "preview": "{\n  \"spec_dir\": \"spec\",\n  \"spec_files\": [\n    \"**/*[sS]pec.js\"\n  ],\n  \"helpers\": [\n    \"helpers/**/*.js\"\n  ],\n  \"stopSpe"
  },
  {
    "path": "src/background_page/index.js",
    "chars": 4618,
    "preview": "import browser from 'webextension-polyfill';\nimport 'msg/server.js';\nimport { tabHistory, recentlyClosed } from './tabHi"
  },
  {
    "path": "src/background_page/tabHistory.js",
    "chars": 1624,
    "preview": "import browser from 'webextension-polyfill';\n\n// list of tab ids in order of increasing age since last visit\nexport cons"
  },
  {
    "path": "src/content_script/toggle_saka.js",
    "chars": 1109,
    "preview": "// this file is dynamically loaded by the event page into the active tab of the active window\n// Search for an existing "
  },
  {
    "path": "src/lib/colors.js",
    "chars": 1434,
    "preview": "export const colors = {\n  red: 'rgba(228,26,28,1)',\n  black: 'rgba(0,0,0,1)',\n  blue: 'rgba(55,126,184,1)',\n  green: 'rg"
  },
  {
    "path": "src/lib/dom.js",
    "chars": 585,
    "preview": "export function slowWheelEvent(\n  threshold,\n  onPositiveThreshold,\n  onNegativeThreshold,\n  value = 0\n) {\n  return e =>"
  },
  {
    "path": "src/lib/highlight.jsx",
    "chars": 785,
    "preview": "import { h } from 'preact';\n\nfunction highlighted(text, indices) {\n  const out = [];\n  let unit = '';\n  let pairIndex = "
  },
  {
    "path": "src/lib/log.js",
    "chars": 117,
    "preview": "export default (thing, ...things) => {\n  if (SAKA_DEBUG) {\n    console.log(thing, ...things);\n  }\n  return thing;\n};\n"
  },
  {
    "path": "src/lib/tld.js",
    "chars": 9029,
    "preview": "export default [\n  'abogado',\n  'ac',\n  'academy',\n  'accountants',\n  'active',\n  'actor',\n  'ad',\n  'adult',\n  'ae',\n  "
  },
  {
    "path": "src/lib/trie.js",
    "chars": 1438,
    "preview": "/**\n * A Trie datastructure that uses a simple javascript object for its underlying storage.\n * It doesn't generate the "
  },
  {
    "path": "src/lib/url.js",
    "chars": 3195,
    "preview": "import browser from 'webextension-polyfill';\nimport knownTLDs from './tld.js';\n/**\n * Given the URL of a suggestion and "
  },
  {
    "path": "src/lib/utils.js",
    "chars": 1030,
    "preview": "import Fuse from 'fuse.js';\n\nexport const isMac = navigator.appVersion.indexOf('Mac') !== -1;\nexport const ctrlChar = is"
  },
  {
    "path": "src/msg/client.js",
    "chars": 179,
    "preview": "import client from 'msgx/client.js';\n\nconst msg = client({\n  zoom: zoom => {\n    window.dispatchEvent(new CustomEvent('z"
  },
  {
    "path": "src/msg/server.js",
    "chars": 870,
    "preview": "import browser from 'webextension-polyfill';\nimport server from 'msgx/server.js';\n\nimport {\n  getSuggestions,\n  activate"
  },
  {
    "path": "src/options/Main/MainOptions.jsx",
    "chars": 1382,
    "preview": "import { Component, h } from 'preact';\nimport 'material-components-web/dist/material-components-web.css';\nimport 'scss/o"
  },
  {
    "path": "src/options/Main/OptionsList/DefaultModeSelection.jsx",
    "chars": 1381,
    "preview": "import { h } from 'preact';\n\n// import { Component, h } from 'preact';\nimport 'material-components-web/dist/material-com"
  },
  {
    "path": "src/options/Main/OptionsList/EnableFuzzySearch.jsx",
    "chars": 898,
    "preview": "import { h } from 'preact';\n\nconst EnableFuzzySearch = function EnableFuzzySearch() {\n  const { checked, handleEnableFuz"
  },
  {
    "path": "src/options/Main/OptionsList/OnlyShowSearchBarSelector.jsx",
    "chars": 951,
    "preview": "import { h } from 'preact';\n\nconst OnlyShowSearchBarSelector = function OnlyShowSearchBarSelector() {\n  const { checked,"
  },
  {
    "path": "src/options/Main/OptionsList/ShowSakaHotkeys.jsx",
    "chars": 599,
    "preview": "import { h } from 'preact';\n\nimport 'material-components-web/dist/material-components-web.css';\n\nconst ShowSakaHotkeys ="
  },
  {
    "path": "src/options/Main/OptionsList/index.jsx",
    "chars": 3862,
    "preview": "import browser from 'webextension-polyfill';\nimport { Component, h } from 'preact';\nimport DefaultModeSelection from './"
  },
  {
    "path": "src/options/Main/SakaHotkeysList/HotkeyListRow.jsx",
    "chars": 564,
    "preview": "import { h } from 'preact';\n\nimport 'material-components-web/dist/material-components-web.css';\n\nconst HotkeyListRow = f"
  },
  {
    "path": "src/options/Main/SakaHotkeysList/index.jsx",
    "chars": 4464,
    "preview": "import { h } from 'preact';\nimport HotkeyListRow from './HotkeyListRow.jsx';\nimport 'material-components-web/dist/materi"
  },
  {
    "path": "src/options/saka-options.jsx",
    "chars": 113,
    "preview": "import { render, h } from 'preact';\nimport Main from './Main/MainOptions.jsx';\n\nrender(<Main />, document.body);\n"
  },
  {
    "path": "src/saka/Main/Components/BackgroundImage/index.jsx",
    "chars": 1109,
    "preview": "import browser from 'webextension-polyfill';\nimport { h, Component } from 'preact';\nimport msg from 'msg/client.js';\nimp"
  },
  {
    "path": "src/saka/Main/Components/GUIContainer/index.jsx",
    "chars": 1729,
    "preview": "import { h, Component } from 'preact';\nimport msg from 'msg/client.js';\nimport 'scss/styles.scss';\n\n// Makes GUI constan"
  },
  {
    "path": "src/saka/Main/Components/Icon/index.jsx",
    "chars": 246,
    "preview": "import { h } from 'preact';\nimport 'scss/styles.scss';\n\nexport default ({ icon, color }) => {\n  return (\n    <i\n      id"
  },
  {
    "path": "src/saka/Main/Components/ModeSwitcher/index.jsx",
    "chars": 848,
    "preview": "import { h } from 'preact';\nimport { suggestions } from 'src/suggestion_engine/server/providers/mode.js';\nimport Icon fr"
  },
  {
    "path": "src/saka/Main/Components/PaginationBar/index.jsx",
    "chars": 989,
    "preview": "import { h } from 'preact';\nimport { ctrlChar } from 'lib/utils.js';\nimport 'scss/styles.scss';\n\nexport default ({\n  fir"
  },
  {
    "path": "src/saka/Main/Components/SearchBar/Button/index.jsx",
    "chars": 1619,
    "preview": "import { h, Component } from 'preact';\nimport '@material/button/dist/mdc.button.min.css';\nimport { icons } from 'suggest"
  },
  {
    "path": "src/saka/Main/Components/SearchBar/Input/index.jsx",
    "chars": 811,
    "preview": "import { Component, h } from 'preact';\n\n// import '@material/textfield/dist/mdc.textfield.min.css';\nimport 'scss/styles."
  },
  {
    "path": "src/saka/Main/Components/SearchBar/index.jsx",
    "chars": 442,
    "preview": "import { h } from 'preact';\nimport 'scss/styles.scss';\nimport Input from './Input/index.jsx';\n\nexport default ({\n  place"
  },
  {
    "path": "src/saka/Main/Components/SettingsBar/index.jsx",
    "chars": 723,
    "preview": "import { h } from 'preact';\nimport { colorMap } from 'lib/colors.js';\nimport 'scss/styles.scss';\n\nconst Item = ({ label,"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Components/Suggestion/index.jsx",
    "chars": 2318,
    "preview": "import { h } from 'preact';\nimport { fadedColorMap } from 'lib/colors.js';\nimport { ctrlChar } from 'lib/utils.js';\nimpo"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Containers/BookmarkSuggestion/index.jsx",
    "chars": 538,
    "preview": "import { h } from 'preact';\nimport highlight from 'lib/highlight.jsx';\nimport Suggestion from '../../Components/Suggesti"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Containers/ClosedTabSuggestion/index.jsx",
    "chars": 616,
    "preview": "import { h } from 'preact';\nimport highlight from 'lib/highlight.jsx';\nimport Suggestion from '../../Components/Suggesti"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Containers/CommandSuggestion/index.jsx",
    "chars": 341,
    "preview": "import { h } from 'preact';\nimport Suggestion from '../../Components/Suggestion/index.jsx';\n\nexport default ({ suggestio"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Containers/HistorySuggestion/index.jsx",
    "chars": 533,
    "preview": "import { h } from 'preact';\nimport highlight from 'lib/highlight.jsx';\nimport Suggestion from '../../Components/Suggesti"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Containers/RecentlyViewedSuggestion/index.jsx",
    "chars": 685,
    "preview": "import { h } from 'preact';\nimport highlight from 'lib/highlight.jsx';\nimport Suggestion from '../../Components/Suggesti"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Containers/SearchEngineSuggestion/index.jsx",
    "chars": 502,
    "preview": "import { h } from 'preact';\nimport Suggestion from '../../Components/Suggestion/index.jsx';\n\nexport default ({\n  suggest"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Containers/SuggestionSelector.jsx",
    "chars": 1185,
    "preview": "import { h, Component } from 'preact';\nimport TabSuggestion from './TabSuggestion/index.jsx';\nimport ClosedTabSuggestion"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Containers/TabSuggestion/index.jsx",
    "chars": 727,
    "preview": "import { h } from 'preact';\nimport highlight from 'lib/highlight.jsx';\nimport Suggestion from '../../Components/Suggesti"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Containers/UnknownSuggestion/index.jsx",
    "chars": 336,
    "preview": "import { h } from 'preact';\nimport Suggestion from '../../Components/Suggestion/index.jsx';\n\nexport default ({ suggestio"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/index.jsx",
    "chars": 838,
    "preview": "import 'material-components-web/dist/material-components-web.css';\nimport 'scss/styles.scss';\nimport { h, Component } fr"
  },
  {
    "path": "src/saka/Main/Containers/GeneralSearch/index.jsx",
    "chars": 75,
    "preview": "import { h } from 'preact';\n\nexport default () => <h1>General Search</h1>;\n"
  },
  {
    "path": "src/saka/Main/Containers/StandardSearch/index.jsx",
    "chars": 11686,
    "preview": "import browser from 'webextension-polyfill';\nimport { Component, h } from 'preact';\nimport {\n  getSuggestions,\n  activat"
  },
  {
    "path": "src/saka/Main/Containers/TabSearch/index.jsx",
    "chars": 71,
    "preview": "import { h } from 'preact';\n\nexport default () => <h1>Tab Search</h1>;\n"
  },
  {
    "path": "src/saka/Main/index.jsx",
    "chars": 4251,
    "preview": "import 'material-components-web/dist/material-components-web.css';\nimport 'scss/styles.scss';\nimport browser from 'webex"
  },
  {
    "path": "src/saka/index.jsx",
    "chars": 175,
    "preview": "import { render, h } from 'preact';\nimport 'material-components-web/dist/material-components-web.css';\n\nimport Main from"
  },
  {
    "path": "src/scss/options.scss",
    "chars": 1549,
    "preview": "$mdc-theme-primary: #00796b; // Purple 500\n$mdc-theme-secondary: #4db6ac; // Orange A200\n\n@import '@material/animation/f"
  },
  {
    "path": "src/scss/styles.scss",
    "chars": 4363,
    "preview": "* {\n  box-sizing: border-box;\n}\n\nhtml,\nbody {\n  margin: 0;\n}\n\n#GUIContainer {\n  background-color: #ffffff;\n  position: a"
  },
  {
    "path": "src/suggestion_engine/client/index.js",
    "chars": 329,
    "preview": "import msg from 'msg/client.js';\n\nexport async function getSuggestions(mode, searchString) {\n  return msg('sg', [mode, s"
  },
  {
    "path": "src/suggestion_engine/server/index.js",
    "chars": 1555,
    "preview": "import browser from 'webextension-polyfill';\nimport * as providers from './providers/index.js';\n\nexport async function g"
  },
  {
    "path": "src/suggestion_engine/server/providers/bookmark.js",
    "chars": 1337,
    "preview": "import browser from 'webextension-polyfill';\nimport { isURL, extractProtocol, isProtocol } from 'lib/url.js';\nimport { g"
  },
  {
    "path": "src/suggestion_engine/server/providers/closedTab.js",
    "chars": 3347,
    "preview": "import browser from 'webextension-polyfill';\nimport { isSakaUrl } from 'lib/url.js';\n// import { filter } from 'rxjs/ope"
  },
  {
    "path": "src/suggestion_engine/server/providers/command.js",
    "chars": 362,
    "preview": "import { MAX_RESULTS } from './index.js';\n\nconst commands = ['search', 'help', 'history', 'tabs', 'define'];\n\nexport def"
  },
  {
    "path": "src/suggestion_engine/server/providers/history.js",
    "chars": 1222,
    "preview": "import browser from 'webextension-polyfill';\nimport { isSakaUrl } from 'lib/url.js';\nimport { getFilteredSuggestions } f"
  },
  {
    "path": "src/suggestion_engine/server/providers/index.js",
    "chars": 426,
    "preview": "export { default as tab } from './tab.js';\nexport { default as closedTab } from './closedTab.js';\nexport { default as mo"
  },
  {
    "path": "src/suggestion_engine/server/providers/mode.js",
    "chars": 1538,
    "preview": "import { ctrlChar } from 'lib/utils.js';\nimport { colorMap, fadedColorMap } from 'lib/colors.js';\nimport Fuse from 'fuse"
  },
  {
    "path": "src/suggestion_engine/server/providers/recentlyViewed.js",
    "chars": 2570,
    "preview": "import { getFilteredSuggestions } from 'lib/utils.js';\nimport tabSuggestions, {\n  allTabSuggestions,\n  recentVisitedTabS"
  },
  {
    "path": "src/suggestion_engine/server/providers/searchEngine.js",
    "chars": 499,
    "preview": "import { MAX_RESULTS } from './index.js';\n\nexport default async function searchEngineSuggestions(searchText) {\n  try {\n "
  },
  {
    "path": "src/suggestion_engine/server/providers/tab.js",
    "chars": 1844,
    "preview": "import browser from 'webextension-polyfill';\nimport { getFilteredSuggestions, objectFromArray } from 'lib/utils.js';\n\nex"
  },
  {
    "path": "src/suggestion_utils/index.js",
    "chars": 1521,
    "preview": "import { prettifyURL, isURL } from 'lib/url.js';\n\nexport const icons = {\n  mode: 'apps',\n  tab: 'tab',\n  closedTab: 'res"
  },
  {
    "path": "static/background_page.html",
    "chars": 158,
    "preview": "<!doctype html>\n<html>\n    <meta charset=\"UTF-8\">\n<body>\n    <script src=\"vendor.js\"></script>\n    <script src=\"backgrou"
  },
  {
    "path": "static/material-icons.css",
    "chars": 797,
    "preview": "@font-face {\n  font-family: 'Material Icons';\n  font-style: normal;\n  font-weight: 400;\n  src: local('Material Icons'),\n"
  },
  {
    "path": "static/options.html",
    "chars": 274,
    "preview": "<!doctype html>\n<html>\n\n<head>\n    <title>Saka Options</title>\n    <meta charset=\"UTF-8\">\n    <link rel='stylesheet' typ"
  },
  {
    "path": "static/saka.html",
    "chars": 248,
    "preview": "<!doctype html>\n<html>\n\n<head>\n  <title>Saka</title>\n  <meta charset=\"UTF-8\">\n  <link rel='stylesheet' type='text/css' h"
  },
  {
    "path": "test/Icon.test.js",
    "chars": 429,
    "preview": "import Icon from '../src/saka/Main/Components/Icon/index.jsx';\nimport { render } from 'preact-render-spy';\nimport { h } "
  },
  {
    "path": "test/Main.test.js",
    "chars": 854,
    "preview": "import { h } from 'preact';\nimport {\n  render,\n  cleanup,\n  wait,\n  fireEvent,\n  flushPromises\n} from 'preact-testing-li"
  },
  {
    "path": "test/ModeSwitcher.test.js",
    "chars": 963,
    "preview": "import ModeSwitcher from '@/saka/Main/Components/ModeSwitcher/index.jsx';\nimport {\n  render,\n  cleanup,\n  fireEvent,\n  f"
  },
  {
    "path": "test/PaginationBar.test.js",
    "chars": 1319,
    "preview": "import PaginationBar from '@/saka/Main/Components/PaginationBar/index.jsx';\nimport { render, getByText } from 'preact-te"
  },
  {
    "path": "test/SearchBar.test.js",
    "chars": 976,
    "preview": "import { h } from 'preact';\nimport { render, cleanup, flushPromises } from 'preact-testing-library';\nimport SearchBar fr"
  },
  {
    "path": "test/StandardSearch/StandardSearch.test.js",
    "chars": 6728,
    "preview": "import { h } from 'preact';\nimport {\n  render,\n  cleanup,\n  flushPromises,\n  fireEvent,\n  wait\n} from 'preact-testing-li"
  },
  {
    "path": "test/Suggestion.test.js",
    "chars": 3000,
    "preview": "import { h } from 'preact';\nimport {\n  render,\n  cleanup,\n  fireEvent,\n  flushPromises\n} from 'preact-testing-library';\n"
  },
  {
    "path": "test/SuggestionList.test.js",
    "chars": 1569,
    "preview": "import SuggestionList from '@/saka/Main/Components/SuggestionList/index.jsx';\nimport { render } from 'preact-testing-lib"
  },
  {
    "path": "test/__mocks__/browser-mocks.js",
    "chars": 426,
    "preview": "const chrome = require('sinon-chrome/extensions');\nconst browser = require('sinon-chrome/webextensions');\nglobal.chrome "
  },
  {
    "path": "test/__mocks__/styleMock.scss",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/__snapshots__/ModeSwitcher.test.js.snap",
    "chars": 1208,
    "preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`ModeSwitcher component  should render tabs with selected tab colore"
  },
  {
    "path": "test/__snapshots__/SearchBar.test.js.snap",
    "chars": 477,
    "preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`should be empty when no there is no search string provided 1`] = `\n"
  },
  {
    "path": "test/__snapshots__/Suggestion.test.js.snap",
    "chars": 396,
    "preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`should use correct favicon path when using firefox 1`] = `\n<i\n  ari"
  },
  {
    "path": "test/__snapshots__/SuggestionList.test.js.snap",
    "chars": 262,
    "preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`SuggestionList component  should be empty when no values provided 1"
  },
  {
    "path": "test/lib/hightlight.test.js",
    "chars": 1074,
    "preview": "import highlight from 'lib/highlight';\n\ntest('should return source text when matches is undefined', async () => {\n  cons"
  },
  {
    "path": "test/lib/log.test.js",
    "chars": 531,
    "preview": "import log from 'lib/log';\n\ntest('should not log when debug mode is disabled', async () => {\n  global.SAKA_DEBUG = false"
  },
  {
    "path": "test/lib/url.test.js",
    "chars": 6716,
    "preview": "const browser = require('sinon-chrome/webextensions');\n\nimport * as libUrl from 'lib/url';\n\ndescribe('lib/url ', () => {"
  },
  {
    "path": "test/lib/utils.test.js",
    "chars": 2824,
    "preview": "import * as libUtil from 'lib/utils';\n\ndescribe('lib/util ', () => {\n  describe('objectFromArray ', () => {\n    it('shou"
  },
  {
    "path": "test/options/MainOptions.test.js",
    "chars": 2525,
    "preview": "import { h } from 'preact';\nimport {\n  render,\n  cleanup,\n  wait,\n  fireEvent,\n  flushPromises,\n  getByValue\n} from 'pre"
  },
  {
    "path": "test/options/__snapshots__/MainOptions.test.js.snap",
    "chars": 480,
    "preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`should only show key bindings when setting is true 1`] = `\n<i\n  ari"
  },
  {
    "path": "test/suggestion_engine/providers/bookmark.test.js",
    "chars": 4441,
    "preview": "const browser = require('sinon-chrome/webextensions');\n\nimport bookmarkSuggestions from 'suggestion_engine/server/provid"
  },
  {
    "path": "test/suggestion_engine/providers/closedTab.test.js",
    "chars": 5590,
    "preview": "const browser = require('sinon-chrome/webextensions');\n\nimport closedTabSuggestions, {\n  getAllSuggestions\n} from 'sugge"
  },
  {
    "path": "test/suggestion_engine/providers/history.test.js",
    "chars": 5082,
    "preview": "const browser = require('sinon-chrome/webextensions');\n\nimport getHistorySuggestions from 'suggestion_engine/server/prov"
  },
  {
    "path": "test/suggestion_engine/providers/mode.test.js",
    "chars": 3035,
    "preview": "import modeSuggestions from 'suggestion_engine/server/providers/mode.js';\nimport { ctrlChar } from 'lib/utils';\nimport {"
  },
  {
    "path": "test/suggestion_engine/providers/recentlyViewed.test.js",
    "chars": 8645,
    "preview": "const browser = require('sinon-chrome/webextensions');\n\nimport recentlyViewedSuggestions from 'suggestion_engine/server/"
  },
  {
    "path": "test/suggestion_engine/providers/tab.test.js",
    "chars": 3446,
    "preview": "const browser = require('sinon-chrome/webextensions');\n\nimport tabSuggestions from 'suggestion_engine/server/providers/t"
  },
  {
    "path": "test/suggestion_engine/server/index.test.js",
    "chars": 2234,
    "preview": "import {\n  getSuggestions,\n  activateSuggestion,\n  closeTab\n} from 'suggestion_engine/server/index.js';\nimport * as prov"
  },
  {
    "path": "test/suggestion_utils/index.test.js",
    "chars": 4323,
    "preview": "import { preprocessSuggestion } from '@/suggestion_utils/index.js';\nimport { colorMap, fadedColorMap } from 'lib/colors'"
  },
  {
    "path": "webpack.config.js",
    "chars": 4896,
    "preview": "const webpack = require('webpack');\nconst BabiliPlugin = require('babili-webpack-plugin');\nconst CopyWebpackPlugin = req"
  }
]

About this extraction

This page contains the full source code of the lusakasa/saka GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 114 files (181.7 KB), approximately 53.0k tokens, and a symbol index with 90 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!