[
  {
    "path": ".babelrc",
    "content": "{\n  \"sourceMaps\": true,\n  \"plugins\": [\n    [\"transform-react-jsx\", { \"pragma\": \"h\" }],\n    [\"transform-object-rest-spread\"],\n    [\"transform-class-properties\"]\n  ]\n}\n"
  },
  {
    "path": ".eslintignore",
    "content": "test/*"
  },
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"parser\": \"babel-eslint\",\n  \"plugins\": [\"prettier\"],\n  \"extends\": [\"airbnb\", \"prettier\"],\n  \"rules\": {\n    \"react/react-in-jsx-scope\": \"off\",\n    \"import/no-extraneous-dependencies\": \"off\",\n    \"react/prop-types\": \"off\",\n    \"react/no-did-mount-set-state\": \"off\",\n    \"jsx-a11y/no-noninteractive-element-interactions\": \"off\",\n    \"no-plusplus\": [\"error\", { \"allowForLoopAfterthoughts\": true }],\n    \"no-prototype-builtins\": \"off\"\n  },\n  \"env\": {\n    \"webextensions\": true,\n    \"browser\": true\n  },\n  \"globals\": {\n    \"document\": true,\n    \"navigator\": false,\n    \"window\": true,\n    \"SAKA_DEBUG\": true,\n    \"SAKA_PLATFORM\": true,\n    \"SAKA_VERSION\": true,\n    \"SAKA_BENCHMARK\": true\n  },\n  \"settings\": {\n    \"import/resolver\": {\n      \"node\": {\n        \"paths\": [\"src\", \"lib\", \"msg\"],\n        \"extensions\": [\"js\", \"jsx\"]\n      }\n    }\n  }\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "dist\ndist.zip\ndist.crx\ndist.pem\nnode_modules\nnpm-debug.log\ndebug.log\n.DS_Store\nyarn-error.log\ncoverage\nsaka-*.zip\npackage-lock.json\n"
  },
  {
    "path": ".prettierignore",
    "content": "package.json"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"singleQuote\": true\n}\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n  - lts/*\n\ninstall:\n  - yarn global add codecov\n  - yarn install\nscript:\n  - yarn test --coverage && codecov\n  # auto-release from tagged commits\nbefore_deploy:\n  - yarn run build:chrome\n  - yarn run zip saka-chrome-dev.zip dist/*\n  - yarn run build:chrome:prod\n  - yarn run zip saka-chrome-prod.zip dist/*\n  - yarn run build:firefox\n  - yarn run zip saka-firefox-dev.zip dist/*\n  - yarn run build:firefox:prod\n  - yarn run zip saka-firefox-prod.zip dist/*\ndeploy:\n  provider: releases\n  api_key:\n    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=\n  file:\n    - saka-chrome-dev.zip\n    - saka-chrome-prod.zip\n    - saka-firefox-dev.zip\n    - saka-firefox-prod.zip\n  skip_cleanup: true\n  on:\n    repo: lusakasa/saka\n    branch: master\n    tags: true\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"name\": \"Debug Jest Tests\",\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"runtimeArgs\": [\n        \"--inspect-brk\",\n        \"${workspaceRoot}/node_modules/.bin/jest\",\n        \"--runInBand\"\n      ],\n      \"console\": \"integratedTerminal\",\n      \"internalConsoleOptions\": \"neverOpen\",\n      \"port\": 9229,\n      \"skipFiles\": [\"<node_internals>/**\"]\n    }\n  ]\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 Sufyan Dawoodjee, Uzair Shamim\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# 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)\n\nA 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.\n\n* Lists tabs in order of recency by default, then fuzzy search by title or URL.\n* Search recently closed tabs\n* Search all bookmarks\n* Search all browsing history\n* Search all modes at once\n\n<img src=\"./images/preview.png\" width=\"70%\" height=\"70%\">\n\n\n## Install\n\nInstall Saka from the [Firefox Marketplace](https://addons.mozilla.org/firefox/addon/saka/) or [Chrome Webstore](https://chrome.google.com/webstore/detail/saka/nbdfpcokndmapcollfpjdpjlabnibjdi).\n\n## Development\nSee the [Getting Started](https://github.com/lusakasa/saka/wiki/Getting-Started) page on the project Wiki.\n\n## Release Instructions (for maintainers)\n\n1.  Update the version number in `manifest/common.json`\n\n2.  Make a commit and set the message to the version: `git commit -m \"v0.15.2\"`\n\n3.  Tag the commit with the version and a message describing changes since the last release: `git tag -a v0.15.2`\n\n4.  Push the commit to github with tags: `git push origin --follow-tags`\n\n5.  View the build status at https://travis-ci.org/lusakasa/saka/ and generated releases at https://github.com/lusakasa/saka/releases\n\n## License\n\nMIT Licensed, Copyright (c) 2017 Sufyan Dawoodjee, Uzair Shamim\n"
  },
  {
    "path": "docs/pull_request_template.md",
    "content": "## Type of Change\n> Put an [x] for the relevant option\n- [ ] Bugfix/Cleanup (non-breaking change which fixes an issue)\n- [ ] New feature (non-breaking change which adds functionality)\n- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)\n\n## Summary Of Changes\n#### Why is this change needed?\n#### Does this close any open issues?\n\n## Checklist\n- [ ] Tests are passing locally\n- [ ] Updated the README/Wiki documentation (if relevant)\n\n## Additional Comments\nAdd any additional comments you may have here.\n"
  },
  {
    "path": "images/favicons/browserconfig.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig>\n    <msapplication>\n        <tile>\n            <square150x150logo src=\"/mstile-150x150.png\"/>\n            <TileColor>#da532c</TileColor>\n        </tile>\n    </msapplication>\n</browserconfig>\n"
  },
  {
    "path": "images/favicons/manifest.json",
    "content": "{\n    \"name\": \"\",\n    \"icons\": [\n        {\n            \"src\": \"/android-chrome-192x192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"/android-chrome-512x512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\"\n        }\n    ],\n    \"theme_color\": \"#ffffff\",\n    \"background_color\": \"#ffffff\",\n    \"display\": \"standalone\"\n}"
  },
  {
    "path": "jest.config.js",
    "content": "module.exports = {\n  moduleNameMapper: {\n    '@/(.*)$': '<rootDir>/src/$1',\n    '^src/(.*)$': '<rootDir>/src/$1',\n    '^suggestion_utils/(.*)$': '<rootDir>/src/suggestion_utils/$1',\n    '^suggestion_engine/(.*)$': '<rootDir>/src/suggestion_engine/$1',\n    '^lib/(.*)$': '<rootDir>/src/lib/$1',\n    '^msg/(.*)$': '<rootDir>/src/msg/$1',\n    '^.*\\\\.(css|less|sass|scss)$': '<rootDir>/test/__mocks__/styleMock.scss',\n    '^react-dom/server$':\n      '<rootDir>/node_modules/preact-render-to-string/dist/index.js',\n    '^react-addons-test-utils$':\n      '<rootDir>/node_modules/preact-test-utils/lib/index.js',\n    '^react$': '<rootDir>/node_modules/preact-compat/lib/index.js',\n    '^react-dom$': '<rootDir>/node_modules/preact-compat/lib/index.js'\n  },\n  moduleFileExtensions: ['js', 'jsx'],\n  transform: {\n    '^.+\\\\.(js|jsx)?$': '<rootDir>/jest.transform.js'\n  },\n  collectCoverage: true,\n  collectCoverageFrom: ['src/**/*.{js,jsx}'],\n  coverageThreshold: {\n    global: {\n      branches: 59,\n      functions: 65,\n      lines: 62\n    }\n  },\n  coverageReporters: ['json', 'lcov', 'html'],\n  setupFiles: ['<rootDir>/test/__mocks__/browser-mocks.js']\n};\n"
  },
  {
    "path": "jest.transform.js",
    "content": "const babelOptions = {\n  presets: [['env', { targets: { node: '8' } }], 'react'],\n  plugins: [\n    [\n      'transform-react-jsx',\n      {\n        pragma: 'h'\n      }\n    ]\n  ]\n};\nmodule.exports = require('babel-jest').createTransformer(babelOptions);\n"
  },
  {
    "path": "manifest/chrome.json",
    "content": "{\n  \"background\": {\n    \"page\": \"background_page.html\",\n    \"persistent\": true\n  },\n  \"commands\": {\n    \"toggleSaka4\": {\n      \"description\": \"Toggle Saka\",\n      \"global\": true\n    }\n  },\n  \"permissions\": [\"chrome://favicon/\"],\n  \"key\":\n    \"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsgFNwezxiHxK/AIKh9K4LVefUPIDisRzQLmmRBjFftr3yrxJuLBaO1LvMISsRlvbFmbZ0B6KHzyGDEnmOMIo3X9wUxBwSV+q3kigp3XFryllhOHbKcl+FUVMRQabBs49CS6ttq2l4Jx+ULEsmprqub+IV+cvY3slXxxuyMF0TjmnRwDYnSREgbfzK7g/msZ8e0gi+hrPETNVKOibbVwd8S7gSy0hDug71WOA338FGe08DiXpnVoKLL0fmd+TO6Z4fAhH+oSqr15WIt6qCglYmZi2BEydL0zhUcidkis0zJjNX5nQi0xzggmxwsMJyfmfGoniwsUg3rAy+M0v5gaSUQIDAQAB\"\n}\n"
  },
  {
    "path": "manifest/common.json",
    "content": "{\n  \"name\": \"Saka\",\n  \"version\": \"0.17.3\",\n  \"author\": \"Sufyan Dawoodjee, Uzair Shamim\",\n  \"description\": \"Saka - elegent tab search, selection, and beyond\",\n  \"manifest_version\": 2,\n  \"options_ui\": {\n    \"page\": \"options.html\",\n    \"open_in_tab\": true\n  },\n  \"background\": {\n    \"page\": \"background_page.html\"\n  },\n  \"commands\": {\n    \"toggleSaka\": {\n      \"suggested_key\": {\n        \"default\": \"Ctrl+Space\",\n        \"mac\": \"Alt+Space\"\n      },\n      \"description\": \"Toggle Saka\"\n    },\n    \"toggleSaka2\": {\n      \"suggested_key\": {\n        \"default\": \"Ctrl+E\",\n        \"mac\": \"Command+E\"\n      },\n      \"description\": \"Toggle Saka\"\n    },\n    \"toggleSaka3\": {\n      \"description\": \"Toggle Saka\"\n    },\n    \"toggleSaka4\": {\n      \"suggested_key\": {\n        \"default\": \"Ctrl+Shift+1\"\n      },\n      \"description\": \"Toggle Saka\"\n    }\n  },\n  \"browser_action\": {\n    \"default_icon\": {\n      \"48\": \"logo.png\"\n    }\n  },\n  \"icons\": {\n    \"16\": \"logo.png\",\n    \"48\": \"logo.png\",\n    \"128\": \"logo.png\"\n  },\n  \"permissions\": [\n    \"<all_urls>\",\n    \"tabs\",\n    \"sessions\",\n    \"history\",\n    \"bookmarks\",\n    \"storage\",\n    \"contextMenus\"\n  ],\n  \"web_accessible_resources\": [\n    \"saka.html\",\n    \"material-icons.css\",\n    \"MaterialIcons-Regular.woff2\"\n  ]\n}\n"
  },
  {
    "path": "manifest/firefox.json",
    "content": "{\n  \"applications\": {\n    \"gecko\": {\n      \"id\": \"{7d7cad35-2182-4457-972d-5a41a2051240}\"\n    }\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"saka\",\n  \"description\": \"A keyboard interface to the web\",\n  \"scripts\": {\n    \"build\": \"echo \\\"You must specify the target browser (firefox or chrome). Example: npm run build:firefox\\\"\",\n    \"build:prod\": \"echo \\\"You must specify the target browser (firefox or chrome). Example: npm run build:firefox:prod\\\"\",\n    \"build:chrome\": \"npm run clean && webpack --config webpack.config.js --env=dev:chrome:benchmark --progress --colors\",\n    \"build:chrome:prod\": \"npm run clean && webpack --config webpack.config.js --env=prod:chrome:nobenchmark --progress --colors\",\n    \"build:firefox\": \"npm run clean && webpack --config webpack.config.js --env=dev:firefox:benchmark --progress --colors\",\n    \"build:firefox:prod\": \"npm run clean && webpack --config webpack.config.js --env=prod:firefox:nobenchmark --progress --colors\",\n    \"build:profile\": \"npm run clean && webpack --config webpack.config.js --progress --profile --colors && cp ./static/* ./dist\",\n    \"zip\": \"bestzip\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"jest\",\n    \"test:watch\": \"jest --watch\",\n    \"test:coverage\": \"jest --coverage --watch --verbose false\",\n    \"storybook\": \"start-storybook -p 9001 -c .storybook\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/lusakasa/saka.git\"\n  },\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"@material/ripple\": \"^0.38.1\",\n    \"@types/chrome\": \"^0.0.75\",\n    \"babel-core\": \"^6.26.0\",\n    \"babel-eslint\": \"^8.2.6\",\n    \"babel-jest\": \"^23.4.2\",\n    \"babel-loader\": \"^7.1.0\",\n    \"babel-plugin-transform-class-properties\": \"^6.23.0\",\n    \"babel-plugin-transform-object-rest-spread\": \"^6.26.0\",\n    \"babel-plugin-transform-react-jsx\": \"^6.23.0\",\n    \"babel-preset-env\": \"^1.6.1\",\n    \"babel-preset-react\": \"^6.24.1\",\n    \"babili\": \"^0.1.4\",\n    \"babili-webpack-plugin\": \"^0.1.2\",\n    \"bestzip\": \"^2.1.2\",\n    \"copy-webpack-plugin\": \"^4.0.1\",\n    \"css-loader\": \"^1.0.0\",\n    \"eslint\": \"^5.4.0\",\n    \"eslint-config-airbnb\": \"^17.1.0\",\n    \"eslint-config-prettier\": \"^3.0.1\",\n    \"eslint-plugin-import\": \"^2.10.0\",\n    \"eslint-plugin-jsx-a11y\": \"^6.0.3\",\n    \"eslint-plugin-node\": \"^8.0.0\",\n    \"eslint-plugin-prettier\": \"^3.0.0\",\n    \"eslint-plugin-promise\": \"^4.0.0\",\n    \"eslint-plugin-react\": \"^7.2.0\",\n    \"eslint-plugin-standard\": \"^4.0.0\",\n    \"extract-loader\": \"^3.0.0\",\n    \"generate-json-webpack-plugin\": \"^0.3.1\",\n    \"html-loader\": \"^0.5.5\",\n    \"jest\": \"^23.5.0\",\n    \"jest-dom\": \"^2.1.1\",\n    \"markdown-loader\": \"^4.0.0\",\n    \"node-sass\": \"^4.8.3\",\n    \"preact-render-spy\": \"^1.2.2\",\n    \"preact-render-to-string\": \"^4.1.0\",\n    \"preact-test-utils\": \"^0.1.3\",\n    \"preact-testing-library\": \"^0.3.0\",\n    \"prettier\": \"^1.14.2\",\n    \"rimraf\": \"^2.6.2\",\n    \"sass-loader\": \"^7.1.0\",\n    \"sinon-chrome\": \"^2.3.1\",\n    \"style-loader\": \"^0.23.1\",\n    \"webpack\": \"^4.17.0\",\n    \"webpack-cli\": \"^3.1.0\",\n    \"webpack-merge\": \"^4.1.4\"\n  },\n  \"dependencies\": {\n    \"fuse.js\": \"^3.2.0\",\n    \"marked\": \"^0.6.2\",\n    \"material-components-web\": \"^0.38.2\",\n    \"msgx\": \"^1.1.3\",\n    \"preact\": \"^8.3.0\",\n    \"preact-compat\": \"^3.18.0\",\n    \"webextension-polyfill\": \"^0.4.0\"\n  }\n}\n"
  },
  {
    "path": "spec/support/jasmine.json",
    "content": "{\n  \"spec_dir\": \"spec\",\n  \"spec_files\": [\n    \"**/*[sS]pec.js\"\n  ],\n  \"helpers\": [\n    \"helpers/**/*.js\"\n  ],\n  \"stopSpecOnExpectationFailure\": false,\n  \"random\": false\n}\n"
  },
  {
    "path": "src/background_page/index.js",
    "content": "import browser from 'webextension-polyfill';\nimport 'msg/server.js';\nimport { tabHistory, recentlyClosed } from './tabHistory.js';\n\nwindow.tabHistory = tabHistory;\nwindow.recentlyClosed = recentlyClosed;\n\nlet lastTabId;\n\nasync function toggleSaka(tabId) {\n  if (SAKA_DEBUG) console.group('toggleSaka');\n  // Get the specified tab, or the current tab if none is specified\n  const currentTab =\n    tabId === undefined\n      ? (await browser.tabs.query({\n          active: true,\n          currentWindow: true\n        }))[0]\n      : await browser.tabs.get(tabId);\n  if (currentTab) {\n    // If the current tab is Saka, switch to the previous tab (if it exists) and close the current tab\n    if (currentTab.url === browser.runtime.getURL('saka.html')) {\n      if (lastTabId) {\n        try {\n          const lastTab = await browser.tabs.get(lastTabId);\n          if (lastTab) {\n            try {\n              await browser.tabs.update(lastTabId, { active: true });\n              if (SAKA_DEBUG) console.log(`Switched to tab ${lastTab.url}`);\n            } catch (e) {\n              if (SAKA_DEBUG)\n                console.error(`Failed to switch to tab ${lastTab.url}`);\n            }\n          }\n          lastTabId = undefined;\n        } catch (e) {\n          if (SAKA_DEBUG)\n            console.error(\n              `Cannot return to tab ${lastTabId} because it no longer exists`\n            );\n        }\n      }\n      try {\n        await browser.tabs.remove(currentTab.id);\n        if (SAKA_DEBUG) console.log(`Removed tab ${currentTab.url}`);\n      } catch (e) {\n        if (SAKA_DEBUG) console.error(`Failed to remove tab ${currentTab.url}`);\n      }\n      // Otherwise, try to load Saka into the current tab\n    } else {\n      try {\n        await browser.tabs.executeScript(currentTab.id, {\n          file: '/toggle_saka.js',\n          runAt: 'document_start',\n          matchAboutBlank: true\n        });\n        if (SAKA_DEBUG) console.log(`Loaded Saka into tab ${currentTab.url}`);\n        // If loading Saka into the current tab fails, create a new tab\n      } catch (e) {\n        try {\n          const screenshot = await browser.tabs.captureVisibleTab();\n          await browser.storage.local.set({ screenshot });\n        } catch (screenshotError) {\n          if (SAKA_DEBUG)\n            console.error('Failed to capture visible tab: ', screenshotError);\n        }\n        lastTabId = currentTab.id;\n        await browser.tabs.create({\n          url: '/saka.html',\n          index: currentTab.index,\n          active: false\n        });\n        if (SAKA_DEBUG)\n          console.warn(\n            `Failed to execute Saka into tab. Instead, created new Saka tab after ${\n              currentTab.url\n            }`\n          );\n      }\n    }\n    // If tab couldn't be found (e.g. because query was made from devtools) create a new tab\n  } else {\n    await browser.tabs.create({\n      url: '/saka.html'\n    });\n    if (SAKA_DEBUG)\n      console.log(\"Couldn't find tab. Instead, created new Saka tab.\");\n  }\n  const window = await browser.windows.getLastFocused();\n  await browser.windows.update(window.id, { focused: true });\n  if (SAKA_DEBUG) console.groupEnd();\n}\n\nasync function closeSaka(tab) {\n  if (tab) {\n    if (tab.url === browser.runtime.getURL('saka.html')) {\n      await browser.tabs.remove(tab.id);\n    } else {\n      await browser.tabs.executeScript(tab.id, {\n        file: '/toggle_saka.js',\n        runAt: 'document_start',\n        matchAboutBlank: true\n      });\n    }\n  }\n}\n\nasync function saveSettings(searchHistory) {\n  await browser.storage.sync.set({ searchHistory: [...searchHistory] });\n}\n\nbrowser.browserAction.onClicked.addListener(() => {\n  toggleSaka();\n});\n\nbrowser.commands.onCommand.addListener(command => {\n  switch (command) {\n    case 'toggleSaka':\n    case 'toggleSaka2':\n    case 'toggleSaka3':\n    case 'toggleSaka4':\n      toggleSaka();\n      break;\n    default:\n      console.error(`Unknown command: '${command}'`);\n  }\n});\n\nbrowser.runtime.onMessage.addListener(async (message, sender) => {\n  switch (message.key) {\n    case 'toggleSaka':\n      toggleSaka();\n      break;\n    case 'closeSaka':\n      await saveSettings(message.searchHistory);\n      closeSaka(sender.tab);\n      break;\n    default:\n      console.error(`Unknown message: '${message}'`);\n  }\n});\n\nbrowser.runtime.onMessageExternal.addListener(message => {\n  switch (message) {\n    case 'toggleSaka':\n      toggleSaka();\n      break;\n    default:\n      console.error(`Unknown message: '${message}'`);\n  }\n});\n\nbrowser.contextMenus.create({\n  title: 'Saka',\n  contexts: ['all'],\n  onclick: () => toggleSaka()\n});\n"
  },
  {
    "path": "src/background_page/tabHistory.js",
    "content": "import browser from 'webextension-polyfill';\n\n// list of tab ids in order of increasing age since last visit\nexport const tabHistory = [];\n\n// list of tab ids in order of increasing age since closed\nexport const recentlyClosed = [];\n\nconst log = listener => (...args) => {\n  listener(...args);\n  // if (SAKA_DEBUG) console.log(tabHistory);\n};\n\nfunction setMostRecentTab(tabInfo) {\n  const tabIndex = tabHistory.findIndex(tab => tab.tabId === tabInfo.tabId);\n\n  if (tabIndex !== -1) {\n    tabHistory.splice(tabIndex, 1);\n  }\n  tabHistory.unshift(tabInfo);\n}\n\nfunction setMostRecentClosedTab(tabInfo) {\n  const tabIndex = recentlyClosed.findIndex(tab => tab.tabId === tabInfo.tabId);\n\n  if (tabIndex !== -1) {\n    recentlyClosed.splice(tabIndex, 1);\n  }\n  recentlyClosed.unshift(tabInfo);\n}\n\nbrowser.tabs.onActivated.addListener(\n  log(({ tabId }) => {\n    setMostRecentTab({ tabId, lastAccessed: Date.now() });\n  })\n);\n\nbrowser.tabs.onRemoved.addListener(\n  log(tabId => {\n    const i = tabHistory.findIndex(tab => tab.tabId === tabId);\n    tabHistory.splice(i, 1);\n    setMostRecentClosedTab({ tabId, lastAccessed: Date.now() });\n  })\n);\n\nbrowser.tabs.onReplaced.addListener(\n  log((addedTabId, removedTabId) => {\n    const i = tabHistory.findIndex(tab => tab.tabId === removedTabId);\n    tabHistory[i] = { tabId: addedTabId, lastAccessed: Date.now() };\n  })\n);\n\nbrowser.windows.onFocusChanged.addListener(async windowId => {\n  const [tab] = await browser.tabs.query({ currentWindow: true, active: true });\n  if (tab && tab.windowId === windowId) {\n    setMostRecentTab({ tabId: tab.id, lastAccessed: Date.now() });\n  }\n});\n"
  },
  {
    "path": "src/content_script/toggle_saka.js",
    "content": "// this file is dynamically loaded by the event page into the active tab of the active window\n// Search for an existing Saka\n//   * if found, remove it\n//   * if not found, create and show it\n\nconst oldSakaRoot = document.querySelector('#saka-root');\nif (oldSakaRoot) {\n  if (SAKA_DEBUG) console.log('REMOVING SAKA');\n  oldSakaRoot.remove();\n} else {\n  if (SAKA_DEBUG) console.log('APPENDING SAKA');\n  // create container div\n  const newSakaRoot = document.createElement('div');\n  newSakaRoot.id = 'saka-root';\n  newSakaRoot.style = `position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  z-index: 2147483647;\n  opacity: 1;\n  pointer-events: none;`;\n  // create Saka iframe\n  const iframe = document.createElement('iframe');\n  iframe.id = 'saka';\n  iframe.src = chrome.runtime.getURL('saka.html');\n  iframe.style = `z-index: 2147483647;\n  position: fixed;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  border-width: 0;\n  pointer-events: all;\n  }`;\n  iframe.frameBorder = 0;\n  // mount to DOM\n  newSakaRoot.appendChild(iframe);\n  document.documentElement.appendChild(newSakaRoot);\n}\n"
  },
  {
    "path": "src/lib/colors.js",
    "content": "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: 'rgba(77,175,74,1)',\n  purple: 'rgba(152,78,163,1)',\n  orange: 'rgba(255,127,0,1)',\n  yellow: 'rgba(255,255,51,1)',\n  brown: 'rgba(166,86,40,1)',\n  pink: 'rgba(247,129,191,1)',\n  teal: 'rgb(0,77,64,1)',\n  gray: 'rgba(153,153,153,1)'\n};\n\nexport const fadedColors = {\n  red: 'rgba(228,26,28,0.44)',\n  black: 'rgba(0,0,0,0.44)',\n  blue: 'rgba(55,126,184,0.44)',\n  green: 'rgba(77,175,74,0.44)',\n  purple: 'rgba(152,78,163,0.44)',\n  orange: 'rgba(255,127,0,0.44)',\n  yellow: 'rgba(255,255,51,0.44)',\n  brown: 'rgba(166,86,40,0.44)',\n  pink: 'rgba(247,129,191,0.44)',\n  teal: 'rgb(0,77,64,0.44)',\n  gray: 'rgba(153,153,153,0.44)'\n};\n\nexport const colorMap = {\n  tab: colors.blue,\n  closedTab: colors.black,\n  search: colors.green,\n  history: colors.red,\n  recentlyViewed: colors.purple,\n  bookmark: colors.orange,\n  mode: colors.purple,\n  // dictionary: colors.purple,\n  calculator: colors.brown,\n  // command: colors.pink,\n  unknown: colors.gray\n};\n\nexport const fadedColorMap = {\n  tab: fadedColors.blue,\n  closedTab: fadedColors.black,\n  search: fadedColors.green,\n  history: fadedColors.red,\n  recentlyViewed: fadedColors.purple,\n  bookmark: fadedColors.orange,\n  mode: fadedColors.purple,\n  // dictionary: colors.purple,\n  calculator: fadedColors.brown,\n  // command: fadedColors.pink,\n  unknown: fadedColors.gray\n};\n"
  },
  {
    "path": "src/lib/dom.js",
    "content": "export function slowWheelEvent(\n  threshold,\n  onPositiveThreshold,\n  onNegativeThreshold,\n  value = 0\n) {\n  return e => {\n    e.preventDefault();\n    let val = value;\n    val += e.deltaY;\n    if (val >= threshold) {\n      val = 0;\n      onPositiveThreshold(e);\n    } else if (val <= -threshold) {\n      val = 0;\n      onNegativeThreshold(e);\n    }\n  };\n}\n\n/**\n * Return whether the cursor is at the far right end of the\n * provided HTML input\n * @param {HTMLInputElement} input\n */\nexport function cursorAtEnd(input) {\n  return input && input.selectionStart === input.value.length;\n}\n"
  },
  {
    "path": "src/lib/highlight.jsx",
    "content": "import { h } from 'preact';\n\nfunction highlighted(text, indices) {\n  const out = [];\n  let unit = '';\n  let pairIndex = 0;\n  let pair = indices[pairIndex];\n  pairIndex += 1;\n  for (let i = 0; i < text.length; i++) {\n    const char = text[i];\n    if (pair && i === pair[0]) {\n      out.push(unit);\n      unit = '';\n    }\n    unit += char;\n    if (pair && i === pair[1]) {\n      out.push(<span style={{ 'font-weight': 'bold' }}>{unit}</span>);\n      unit = '';\n      pair = indices[pairIndex];\n      pairIndex += 1;\n    }\n  }\n  if (unit !== '') out.push(unit);\n  return out;\n}\n\nexport default function highlight(text, key, matches) {\n  const matchesForKey = matches && matches.find(match => match.key === key);\n  return matchesForKey ? highlighted(text, matchesForKey.indices) : text;\n}\n"
  },
  {
    "path": "src/lib/log.js",
    "content": "export default (thing, ...things) => {\n  if (SAKA_DEBUG) {\n    console.log(thing, ...things);\n  }\n  return thing;\n};\n"
  },
  {
    "path": "src/lib/tld.js",
    "content": "export default [\n  'abogado',\n  'ac',\n  'academy',\n  'accountants',\n  'active',\n  'actor',\n  'ad',\n  'adult',\n  'ae',\n  'aero',\n  'af',\n  'ag',\n  'agency',\n  'ai',\n  'airforce',\n  'al',\n  'allfinanz',\n  'alsace',\n  'am',\n  'amsterdam',\n  'an',\n  'android',\n  'ao',\n  'aq',\n  'aquarelle',\n  'ar',\n  'archi',\n  'army',\n  'arpa',\n  'as',\n  'asia',\n  'associates',\n  'at',\n  'attorney',\n  'au',\n  'auction',\n  'audio',\n  'autos',\n  'aw',\n  'ax',\n  'axa',\n  'az',\n  'ba',\n  'band',\n  'bank',\n  'bar',\n  'barclaycard',\n  'barclays',\n  'bargains',\n  'bayern',\n  'bb',\n  'bd',\n  'be',\n  'beer',\n  'berlin',\n  'best',\n  'bf',\n  'bg',\n  'bh',\n  'bi',\n  'bid',\n  'bike',\n  'bio',\n  'biz',\n  'bj',\n  'black',\n  'blackfriday',\n  'bloomberg',\n  'blue',\n  'bm',\n  'bmw',\n  'bn',\n  'bnpparibas',\n  'bo',\n  'boo',\n  'boutique',\n  'br',\n  'brussels',\n  'bs',\n  'bt',\n  'budapest',\n  'build',\n  'builders',\n  'business',\n  'buzz',\n  'bv',\n  'bw',\n  'by',\n  'bz',\n  'bzh',\n  'ca',\n  'cab',\n  'cal',\n  'camera',\n  'camp',\n  'cancerresearch',\n  'capetown',\n  'capital',\n  'caravan',\n  'cards',\n  'care',\n  'career',\n  'careers',\n  'cartier',\n  'casa',\n  'cash',\n  'cat',\n  'catering',\n  'cc',\n  'cd',\n  'center',\n  'ceo',\n  'cern',\n  'cf',\n  'cg',\n  'ch',\n  'channel',\n  'cheap',\n  'christmas',\n  'chrome',\n  'church',\n  'ci',\n  'citic',\n  'city',\n  'ck',\n  'cl',\n  'claims',\n  'cleaning',\n  'click',\n  'clinic',\n  'clothing',\n  'club',\n  'cm',\n  'cn',\n  'co',\n  'coach',\n  'codes',\n  'coffee',\n  'college',\n  'cologne',\n  'com',\n  'community',\n  'company',\n  'computer',\n  'condos',\n  'construction',\n  'consulting',\n  'contractors',\n  'cooking',\n  'cool',\n  'coop',\n  'country',\n  'cr',\n  'credit',\n  'creditcard',\n  'cricket',\n  'crs',\n  'cruises',\n  'cu',\n  'cuisinella',\n  'cv',\n  'cw',\n  'cx',\n  'cy',\n  'cymru',\n  'cz',\n  'dabur',\n  'dad',\n  'dance',\n  'dating',\n  'day',\n  'dclk',\n  'de',\n  'deals',\n  'degree',\n  'delivery',\n  'democrat',\n  'dental',\n  'dentist',\n  'desi',\n  'design',\n  'dev',\n  'diamonds',\n  'diet',\n  'digital',\n  'direct',\n  'directory',\n  'discount',\n  'dj',\n  'dk',\n  'dm',\n  'dnp',\n  'do',\n  'docs',\n  'domains',\n  'doosan',\n  'durban',\n  'dvag',\n  'dz',\n  'eat',\n  'ec',\n  'edu',\n  'education',\n  'ee',\n  'eg',\n  'email',\n  'emerck',\n  'energy',\n  'engineer',\n  'engineering',\n  'enterprises',\n  'equipment',\n  'er',\n  'es',\n  'esq',\n  'estate',\n  'et',\n  'eu',\n  'eurovision',\n  'eus',\n  'events',\n  'everbank',\n  'exchange',\n  'expert',\n  'exposed',\n  'fail',\n  'farm',\n  'fashion',\n  'feedback',\n  'fi',\n  'finance',\n  'financial',\n  'firmdale',\n  'fish',\n  'fishing',\n  'fit',\n  'fitness',\n  'fj',\n  'fk',\n  'flights',\n  'florist',\n  'flowers',\n  'flsmidth',\n  'fly',\n  'fm',\n  'fo',\n  'foo',\n  'forsale',\n  'foundation',\n  'fr',\n  'frl',\n  'frogans',\n  'fund',\n  'furniture',\n  'futbol',\n  'ga',\n  'gal',\n  'gallery',\n  'garden',\n  'gb',\n  'gbiz',\n  'gd',\n  'ge',\n  'gent',\n  'gf',\n  'gg',\n  'ggee',\n  'gh',\n  'gi',\n  'gift',\n  'gifts',\n  'gives',\n  'gl',\n  'glass',\n  'gle',\n  'global',\n  'globo',\n  'gm',\n  'gmail',\n  'gmo',\n  'gmx',\n  'gn',\n  'goog',\n  'google',\n  'gop',\n  'gov',\n  'gp',\n  'gq',\n  'gr',\n  'graphics',\n  'gratis',\n  'green',\n  'gripe',\n  'gs',\n  'gt',\n  'gu',\n  'guide',\n  'guitars',\n  'guru',\n  'gw',\n  'gy',\n  'hamburg',\n  'hangout',\n  'haus',\n  'healthcare',\n  'help',\n  'here',\n  'hermes',\n  'hiphop',\n  'hiv',\n  'hk',\n  'hm',\n  'hn',\n  'holdings',\n  'holiday',\n  'homes',\n  'horse',\n  'host',\n  'hosting',\n  'house',\n  'how',\n  'hr',\n  'ht',\n  'hu',\n  'ibm',\n  'id',\n  'ie',\n  'ifm',\n  'il',\n  'im',\n  'immo',\n  'immobilien',\n  'in',\n  'industries',\n  'info',\n  'ing',\n  'ink',\n  'institute',\n  'insure',\n  'int',\n  'international',\n  'investments',\n  'io',\n  'iq',\n  'ir',\n  'irish',\n  'is',\n  'it',\n  'iwc',\n  'jcb',\n  'je',\n  'jetzt',\n  'jm',\n  'jo',\n  'jobs',\n  'joburg',\n  'jp',\n  'juegos',\n  'kaufen',\n  'kddi',\n  'ke',\n  'kg',\n  'kh',\n  'ki',\n  'kim',\n  'kitchen',\n  'kiwi',\n  'km',\n  'kn',\n  'koeln',\n  'kp',\n  'kr',\n  'krd',\n  'kred',\n  'kw',\n  'ky',\n  'kyoto',\n  'kz',\n  'la',\n  'lacaixa',\n  'land',\n  'lat',\n  'latrobe',\n  'lawyer',\n  'lb',\n  'lc',\n  'lds',\n  'lease',\n  'legal',\n  'lgbt',\n  'li',\n  'lidl',\n  'life',\n  'lighting',\n  'limited',\n  'limo',\n  'link',\n  'lk',\n  'loans',\n  'london',\n  'lotte',\n  'lotto',\n  'lr',\n  'ls',\n  'lt',\n  'ltda',\n  'lu',\n  'luxe',\n  'luxury',\n  'lv',\n  'ly',\n  'ma',\n  'madrid',\n  'maison',\n  'management',\n  'mango',\n  'market',\n  'marketing',\n  'marriott',\n  'mc',\n  'md',\n  'me',\n  'media',\n  'meet',\n  'melbourne',\n  'meme',\n  'memorial',\n  'menu',\n  'mg',\n  'mh',\n  'miami',\n  'mil',\n  'mini',\n  'mk',\n  'ml',\n  'mm',\n  'mn',\n  'mo',\n  'mobi',\n  'moda',\n  'moe',\n  'monash',\n  'money',\n  'mormon',\n  'mortgage',\n  'moscow',\n  'motorcycles',\n  'mov',\n  'mp',\n  'mq',\n  'mr',\n  'ms',\n  'mt',\n  'mu',\n  'museum',\n  'mv',\n  'mw',\n  'mx',\n  'my',\n  'mz',\n  'na',\n  'nagoya',\n  'name',\n  'navy',\n  'nc',\n  'ne',\n  'net',\n  'network',\n  'neustar',\n  'new',\n  'nexus',\n  'nf',\n  'ng',\n  'ngo',\n  'nhk',\n  'ni',\n  'ninja',\n  'nl',\n  'no',\n  'np',\n  'nr',\n  'nra',\n  'nrw',\n  'nu',\n  'nyc',\n  'nz',\n  'okinawa',\n  'om',\n  'one',\n  'ong',\n  'onl',\n  'ooo',\n  'org',\n  'organic',\n  'osaka',\n  'otsuka',\n  'ovh',\n  'pa',\n  'paris',\n  'partners',\n  'parts',\n  'party',\n  'pe',\n  'pf',\n  'pg',\n  'ph',\n  'pharmacy',\n  'photo',\n  'photography',\n  'photos',\n  'physio',\n  'pics',\n  'pictures',\n  'pink',\n  'pizza',\n  'pk',\n  'pl',\n  'place',\n  'plumbing',\n  'pm',\n  'pn',\n  'pohl',\n  'poker',\n  'porn',\n  'post',\n  'pr',\n  'praxi',\n  'press',\n  'pro',\n  'prod',\n  'productions',\n  'prof',\n  'properties',\n  'property',\n  'ps',\n  'pt',\n  'pub',\n  'pw',\n  'py',\n  'qa',\n  'qpon',\n  'quebec',\n  're',\n  'realtor',\n  'recipes',\n  'red',\n  'rehab',\n  'reise',\n  'reisen',\n  'reit',\n  'ren',\n  'rentals',\n  'repair',\n  'report',\n  'republican',\n  'rest',\n  'restaurant',\n  'reviews',\n  'rich',\n  'rio',\n  'rip',\n  'ro',\n  'rocks',\n  'rodeo',\n  'rs',\n  'rsvp',\n  'ru',\n  'ruhr',\n  'rw',\n  'ryukyu',\n  'sa',\n  'saarland',\n  'sale',\n  'samsung',\n  'sarl',\n  'sb',\n  'sc',\n  'sca',\n  'scb',\n  'schmidt',\n  'schule',\n  'schwarz',\n  'science',\n  'scot',\n  'sd',\n  'se',\n  'services',\n  'sew',\n  'sexy',\n  'sg',\n  'sh',\n  'shiksha',\n  'shoes',\n  'shriram',\n  'si',\n  'singles',\n  'sj',\n  'sk',\n  'sky',\n  'sl',\n  'sm',\n  'sn',\n  'so',\n  'social',\n  'software',\n  'sohu',\n  'solar',\n  'solutions',\n  'soy',\n  'space',\n  'spiegel',\n  'sr',\n  'st',\n  'su',\n  'supplies',\n  'supply',\n  'support',\n  'surf',\n  'surgery',\n  'suzuki',\n  'sv',\n  'sx',\n  'sy',\n  'sydney',\n  'systems',\n  'sz',\n  'taipei',\n  'tatar',\n  'tattoo',\n  'tax',\n  'tc',\n  'td',\n  'technology',\n  'tel',\n  'temasek',\n  'tf',\n  'tg',\n  'th',\n  'tienda',\n  'tips',\n  'tires',\n  'tirol',\n  'tj',\n  'tk',\n  'tl',\n  'tm',\n  'tn',\n  'to',\n  'today',\n  'tokyo',\n  'tools',\n  'top',\n  'town',\n  'toys',\n  'tp',\n  'tr',\n  'trade',\n  'training',\n  'travel',\n  'trust',\n  'tt',\n  'tui',\n  'tv',\n  'tw',\n  'tz',\n  'ua',\n  'ug',\n  'uk',\n  'university',\n  'uno',\n  'uol',\n  'us',\n  'uy',\n  'uz',\n  'va',\n  'vacations',\n  'vc',\n  've',\n  'vegas',\n  'ventures',\n  'versicherung',\n  'vet',\n  'vg',\n  'vi',\n  'viajes',\n  'video',\n  'villas',\n  'vision',\n  'vlaanderen',\n  'vn',\n  'vodka',\n  'vote',\n  'voting',\n  'voto',\n  'voyage',\n  'vu',\n  'wales',\n  'wang',\n  'watch',\n  'webcam',\n  'website',\n  'wed',\n  'wedding',\n  'wf',\n  'whoswho',\n  'wien',\n  'wiki',\n  'williamhill',\n  'wme',\n  'work',\n  'works',\n  'world',\n  'ws',\n  'wtc',\n  'wtf',\n  'xn--1qqw23a',\n  'xn--3bst00m',\n  'xn--3ds443g',\n  'xn--3e0b707e',\n  'xn--45brj9c',\n  'xn--45q11c',\n  'xn--4gbrim',\n  'xn--55qw42g',\n  'xn--55qx5d',\n  'xn--6frz82g',\n  'xn--6qq986b3xl',\n  'xn--80adxhks',\n  'xn--80ao21a',\n  'xn--80asehdb',\n  'xn--80aswg',\n  'xn--90a3ac',\n  'xn--b4w605ferd',\n  'xn--c1avg',\n  'xn--cg4bki',\n  'xn--clchc0ea0b2g2a9gcd',\n  'xn--czr694b',\n  'xn--czrs0t',\n  'xn--czru2d',\n  'xn--d1acj3b',\n  'xn--d1alf',\n  'xn--fiq228c5hs',\n  'xn--fiq64b',\n  'xn--fiqs8s',\n  'xn--fiqz9s',\n  'xn--flw351e',\n  'xn--fpcrj9c3d',\n  'xn--fzc2c9e2c',\n  'xn--gecrj9c',\n  'xn--h2brj9c',\n  'xn--hxt814e',\n  'xn--i1b6b1a6a2e',\n  'xn--io0a7i',\n  'xn--j1amh',\n  'xn--j6w193g',\n  'xn--kprw13d',\n  'xn--kpry57d',\n  'xn--kput3i',\n  'xn--l1acc',\n  'xn--lgbbat1ad8j',\n  'xn--mgb9awbf',\n  'xn--mgba3a4f16a',\n  'xn--mgbaam7a8h',\n  'xn--mgbab2bd',\n  'xn--mgbayh7gpa',\n  'xn--mgbbh1a71e',\n  'xn--mgbc0a9azcg',\n  'xn--mgberp4a5d4ar',\n  'xn--mgbx4cd0ab',\n  'xn--ngbc5azd',\n  'xn--node',\n  'xn--nqv7f',\n  'xn--nqv7fs00ema',\n  'xn--o3cw4h',\n  'xn--ogbpf8fl',\n  'xn--p1acf',\n  'xn--p1ai',\n  'xn--pgbs0dh',\n  'xn--q9jyb4c',\n  'xn--qcka1pmc',\n  'xn--rhqv96g',\n  'xn--s9brj9c',\n  'xn--ses554g',\n  'xn--unup4y',\n  'xn--vermgensberater-ctb',\n  'xn--vermgensberatung-pwb',\n  'xn--vhquv',\n  'xn--wgbh1c',\n  'xn--wgbl6a',\n  'xn--xhq521b',\n  'xn--xkc2al3hye2a',\n  'xn--xkc2dl3a5ee0h',\n  'xn--yfro4i67o',\n  'xn--ygbi2ammx',\n  'xn--zfr164b',\n  'xxx',\n  'xyz',\n  'yachts',\n  'yandex',\n  'ye',\n  'yoga',\n  'yokohama',\n  'youtube',\n  'yt',\n  'za',\n  'zip',\n  'zm',\n  'zone',\n  'zuerich',\n  'zw'\n];\n"
  },
  {
    "path": "src/lib/trie.js",
    "content": "/**\n * A Trie datastructure that uses a simple javascript object for its underlying storage.\n * It doesn't generate the input tree for you, you MUST generate it yourself.\n * This trie is designed for Saka Key's needs and ISN'T general purpose.\n * An example input tree is:\n * {\n *   \"c\": {\n *     \"a\": {\n *       \"t\": \"Tom\",\n *       \"r\": \"Tarzan\"\n *     }\n *   },\n *   \"d\": {\n *     \"o\": {\n *       \"g\": () => console.log(\"My dog's name is Harris\")\n *     }\n *   }\n * }\n */\n\nexport default class Trie {\n  /**\n   * Creates a trie\n   * @param {Object} root - A simple javascript object representing a trie\n   */\n  init = root => {\n    this.root = root;\n    this.curNode = root;\n  };\n\n  /** Sets the root to current node to the root node */\n  reset = () => {\n    this.curNode = this.root;\n  };\n\n  /**\n   * Advances the command trie based on the command key event.\n   * If a leaf node, corresponding to a command, has been reached,\n   * returns the command.\n   * Otherwise returns undefined\n   */\n  advance = input => {\n    // TODO: Update to use longest viable prefix by trying\n    // longest prefix until a valid path is found\n    const next = this.curNode[input] || this.root[input] || this.root;\n    // Case 1. A trie node\n    if (typeof next === 'object') {\n      this.curNode = next;\n      return undefined;\n      // Case 2. A trie leaf corresponding to the command reached\n    }\n\n    this.curNode = this.root;\n    return next;\n  };\n}\n"
  },
  {
    "path": "src/lib/url.js",
    "content": "import browser from 'webextension-polyfill';\nimport knownTLDs from './tld.js';\n/**\n * Given the URL of a suggestion and the search text, makes the URL nicer\n * @param {string} url - the suggestion URL\n * @param {string} searchString - the text in the search bar\n */\nexport function prettifyURL(url, searchString) {\n  let prettifiedUrl = url;\n  if (url.endsWith('/')) {\n    prettifiedUrl = url.substr(0, url.length - 1);\n  }\n\n  // TODO add support for any protocol\n  if (\n    !searchString.startsWith('http://') &&\n    prettifiedUrl.startsWith('http://')\n  ) {\n    prettifiedUrl = prettifiedUrl.substr(7);\n  }\n  return prettifiedUrl;\n}\n\n/**\n * Returns true only if str is a valid url\n * @param {string} str\n */\nexport function isURL(str) {\n  let isValidUrl;\n\n  try {\n    isValidUrl = Boolean(new URL(str));\n  } catch (e) {\n    isValidUrl = false;\n  }\n  return isValidUrl;\n}\n\nexport function extractProtocol(url) {\n  if (url) {\n    return url.match(/^\\w+:/, '') ? url.match(/^\\w+:/, '')[0] : '';\n  }\n\n  return '';\n}\n\nexport function stripProtocol(url) {\n  return url.replace(/(^\\w+:|^)\\/\\//, '');\n}\n\nexport function stripWWW(url) {\n  return url.replace(/^www\\./, '');\n}\n\nexport function startsWithProtocol(str) {\n  return str.match(/(^\\w+:|^)\\/\\//) !== null;\n}\n\nexport function startsWithWWW(str) {\n  return str.match(/^www\\./, '') !== null;\n}\n\n/** Returns whether the provided text is a known TLD (top-level domain) */\nexport function isTLD(text) {\n  return knownTLDs.indexOf(text) !== -1;\n}\n\nconst knownProtocols = [\n  'http:',\n  'https:',\n  'file:',\n  'ftp:',\n  'about:',\n  'chrome:',\n  'chrome-extension:',\n  'moz-extension:'\n];\n/** Returns whether the provided text is a known protocol */\nexport function isProtocol(text) {\n  return knownProtocols.indexOf(text) !== -1;\n}\n\nexport function isLikeURL(url) {\n  let trimmedUrl = url.trim();\n  if (trimmedUrl.indexOf(' ') !== -1) {\n    return false;\n  }\n  if (trimmedUrl.search(/^(about|file):[^:]/) !== -1) {\n    return true;\n  }\n  const protocol = (trimmedUrl.match(/^([a-zA-Z-]+:)[^:]/) || [''])[0].slice(\n    0,\n    -1\n  );\n  const protocolMatch = isProtocol(protocol);\n  if (protocolMatch) {\n    trimmedUrl = trimmedUrl.replace(/^[a-zA-Z-]+:\\/*/, '');\n  }\n  const hasPath = /.*[a-zA-Z].*\\//.test(trimmedUrl);\n  trimmedUrl = trimmedUrl.replace(/(:[0-9]+)?([#/].*|$)/g, '').split('.');\n  if (protocolMatch && /^[a-zA-Z0-9@!]+$/.test(trimmedUrl)) {\n    return true;\n  }\n\n  if (protocol && !protocolMatch && protocol !== 'localhost:') {\n    return false;\n  }\n  // IP addresses\n  const isIP = trimmedUrl.every(e => /^[0-9]+$/.test(e) && +e >= 0 && +e < 256);\n  if (\n    (isIP && !protocol && trimmedUrl.length === 4) ||\n    (isIP && protocolMatch)\n  ) {\n    return true;\n  }\n  return (\n    (trimmedUrl.every(e => /^[a-z0-9-]+$/i.test(e)) &&\n      (trimmedUrl.length > 1 && isTLD(trimmedUrl[trimmedUrl.length - 1]))) ||\n    (trimmedUrl.length === 1 && trimmedUrl[0] === 'localhost') ||\n    hasPath\n  );\n}\n\nexport async function isSakaUrl(url) {\n  if (url !== undefined) {\n    const sakaUrl = browser.runtime.getURL('saka.html');\n    const sakaId = sakaUrl.substring(0, sakaUrl.indexOf('/'));\n    return url.includes(sakaId);\n  }\n\n  return false;\n}\n"
  },
  {
    "path": "src/lib/utils.js",
    "content": "import Fuse from 'fuse.js';\n\nexport const isMac = navigator.appVersion.indexOf('Mac') !== -1;\nexport const ctrlChar = isMac ? '⌘' : 'ctrl';\n\nexport function rangedIncrement(value, increment, min, max) {\n  const result = value + increment;\n\n  if (result < min) {\n    return min;\n  } else if (result > max) {\n    return max;\n  }\n\n  return result;\n}\n\n/**\n * @param {KeyboardEvent} e\n */\nexport function ctrlKey(e) {\n  return isMac ? e.metaKey : e.ctrlKey;\n}\n\nexport function objectFromArray(array, key) {\n  const out = {};\n  array.forEach(e => {\n    out[e[key]] = e;\n  });\n  return out;\n}\n\nexport async function getFilteredSuggestions(\n  searchString,\n  { getSuggestions, threshold, keys }\n) {\n  const suggestions = await getSuggestions(searchString);\n  const fuse = new Fuse(suggestions, {\n    shouldSort: true,\n    threshold,\n    minMatchCharLength: 1,\n    includeMatches: true,\n    keys,\n    distance: 500\n  });\n\n  return fuse.search(searchString).map(({ item, matches, score }) => ({\n    ...item,\n    score,\n    matches\n  }));\n}\n"
  },
  {
    "path": "src/msg/client.js",
    "content": "import client from 'msgx/client.js';\n\nconst msg = client({\n  zoom: zoom => {\n    window.dispatchEvent(new CustomEvent('zoom', { detail: { zoom } }));\n  }\n});\n\nexport default msg;\n"
  },
  {
    "path": "src/msg/server.js",
    "content": "import browser from 'webextension-polyfill';\nimport server from 'msgx/server.js';\n\nimport {\n  getSuggestions,\n  activateSuggestion,\n  closeTab\n} from 'suggestion_engine/server/index.js';\n\nconst actions = {\n  // endpoints client queries with msg()\n  sg: getSuggestions,\n  zoom: (_, sender) => browser.tabs.getZoom(sender.tab.id),\n  focusTab: (_, sender) => browser.tabs.update(sender.tab.id, { active: true }),\n  activateSuggestion,\n  closeTab\n};\n\nconst onConnect = (sender, msg, data) => {\n  const onZoomChange = ({ tabId, newZoomFactor }) => {\n    if (sender.tab.id === tabId) {\n      msg('zoom', newZoomFactor);\n    }\n  };\n  browser.tabs.onZoomChange.addListener(onZoomChange);\n  data.onZoomChange = onZoomChange;\n};\n\nconst onDisconnect = (sender, data) => {\n  browser.tabs.onZoomChange.removeListener(data.onZoomChange);\n};\n\nserver(actions, onConnect, onDisconnect);\n"
  },
  {
    "path": "src/options/Main/MainOptions.jsx",
    "content": "import { Component, h } from 'preact';\nimport 'material-components-web/dist/material-components-web.css';\nimport 'scss/options.scss';\n\nimport OptionsList from './OptionsList/index.jsx';\nimport SakaHotkeysList from './SakaHotkeysList/index.jsx';\n\nexport default class MainOptions extends Component {\n  constructor(props) {\n    super(props);\n\n    this.state = {\n      showSakaKeybindings: false\n    };\n  }\n\n  handleOpenSakaKeybindings = () => {\n    this.setState({\n      showSakaKeybindings: !this.state.showSakaKeybindings\n    });\n  };\n\n  render() {\n    return (\n      <body>\n        <header className=\"mdc-top-app-bar mdc-top-app-bar--short\">\n          <div className=\"mdc-top-app-bar__row\">\n            <section className=\"mdc-top-app-bar__section mdc-top-app-bar__section--align-start\">\n              <span className=\"mdc-top-app-bar__title\">Saka Options</span>\n            </section>\n          </div>\n        </header>\n        <div\n          id=\"background-image\"\n          className=\"mdc-elevation--z1 options-container\"\n        >\n          {this.state.showSakaKeybindings ? (\n            <SakaHotkeysList\n              handleOpenSakaKeybindings={this.handleOpenSakaKeybindings}\n            />\n          ) : (\n            <OptionsList\n              handleOpenSakaKeybindings={this.handleOpenSakaKeybindings}\n            />\n          )}\n        </div>\n      </body>\n    );\n  }\n}\n"
  },
  {
    "path": "src/options/Main/OptionsList/DefaultModeSelection.jsx",
    "content": "import { h } from 'preact';\n\n// import { Component, h } from 'preact';\nimport 'material-components-web/dist/material-components-web.css';\n\nconst DefaultModeSelection = function DefaultModeSelection({\n  mode,\n  handleModeChange\n}) {\n  return (\n    <li className=\"mdc-list-item option\">\n      <span className=\"mdc-list-item__text\">\n        Default Mode\n        <span className=\"mdc-list-item__secondary-text\">\n          Select the default mode Saka opens with\n        </span>\n      </span>\n      <div className=\"mdc-select mdc-list-item__meta\">\n        <select\n          value={mode}\n          id=\"defaultModeSelect\"\n          aria-label=\"Select default mode\"\n          className=\" mdc-select__native-control\"\n          onChange={handleModeChange}\n        >\n          <option value=\"tab\" selected=\"\">\n            Tabs\n          </option>\n          <option value=\"closedTab\" selected=\"\">\n            Recently Closed\n          </option>\n          <option value=\"bookmark\" selected=\"\">\n            Bookmarks\n          </option>\n          <option value=\"history\" selected=\"\">\n            History\n          </option>\n          <option value=\"recentlyViewed\" selected=\"\">\n            Recently Viewed\n          </option>\n          <option value=\"mode\" selected=\"\">\n            Modes\n          </option>\n        </select>\n      </div>\n    </li>\n  );\n};\n\nexport default DefaultModeSelection;\n"
  },
  {
    "path": "src/options/Main/OptionsList/EnableFuzzySearch.jsx",
    "content": "import { h } from 'preact';\n\nconst EnableFuzzySearch = function EnableFuzzySearch() {\n  const { checked, handleEnableFuzzySearch } = this.props;\n  return (\n    <li className=\"mdc-list-item option\">\n      <span className=\"mdc-list-item__text\">\n        Enable fuzzy search\n        <span className=\"mdc-list-item__secondary-text\">\n          Enable fuzzy search for bookmarks and history search\n        </span>\n      </span>\n      <div className=\"mdc-list-item__meta mdc-switch\">\n        <input\n          type=\"checkbox\"\n          id=\"basic-switch\"\n          aria-label=\"Enable fuzzy search\"\n          className=\"mdc-switch__native-control\"\n          checked={checked}\n          onChange={handleEnableFuzzySearch}\n        />\n        <div className=\"mdc-switch__background\">\n          <div className=\"mdc-switch__knob\" />\n        </div>\n      </div>\n    </li>\n  );\n};\n\nexport default EnableFuzzySearch;\n"
  },
  {
    "path": "src/options/Main/OptionsList/OnlyShowSearchBarSelector.jsx",
    "content": "import { h } from 'preact';\n\nconst OnlyShowSearchBarSelector = function OnlyShowSearchBarSelector() {\n  const { checked, handleShowSearchSuggestionsChange } = this.props;\n  return (\n    <li className=\"mdc-list-item option\">\n      <span className=\"mdc-list-item__text\">\n        Suggestions on load\n        <span className=\"mdc-list-item__secondary-text\">\n          Show suggestions when there is no text is the Saka search bar\n        </span>\n      </span>\n      <div className=\"mdc-list-item__meta mdc-switch\">\n        <input\n          type=\"checkbox\"\n          id=\"basic-switch\"\n          className=\"mdc-switch__native-control\"\n          aria-label=\"Suggestions on load\"\n          checked={checked}\n          onChange={handleShowSearchSuggestionsChange}\n        />\n        <div className=\"mdc-switch__background\">\n          <div className=\"mdc-switch__knob\" />\n        </div>\n      </div>\n    </li>\n  );\n};\n\nexport default OnlyShowSearchBarSelector;\n"
  },
  {
    "path": "src/options/Main/OptionsList/ShowSakaHotkeys.jsx",
    "content": "import { h } from 'preact';\n\nimport 'material-components-web/dist/material-components-web.css';\n\nconst ShowSakaHotkeys = function ShowSakaHotkeys({\n  handleOpenSakaKeybindings\n}) {\n  return (\n    <li className=\"mdc-list-item option\">\n      <span className=\"mdc-list-item__text\">Saka Hotkeys</span>\n      <i\n        className=\"mdc-list-item__meta mdc-icon-toggle material-icons\"\n        role=\"button\"\n        aria-pressed=\"false\"\n        aria-label=\"View Saka hotkeys\"\n        onClick={handleOpenSakaKeybindings}\n      >\n        keyboard\n      </i>\n    </li>\n  );\n};\n\nexport default ShowSakaHotkeys;\n"
  },
  {
    "path": "src/options/Main/OptionsList/index.jsx",
    "content": "import browser from 'webextension-polyfill';\nimport { Component, h } from 'preact';\nimport DefaultModeSelection from './DefaultModeSelection.jsx';\nimport OnlyShowSearchBarSelector from './OnlyShowSearchBarSelector.jsx';\nimport ShowSakaHotkeys from './ShowSakaHotkeys.jsx';\nimport EnableFuzzySearch from './EnableFuzzySearch.jsx';\n\nexport default class OptionsList extends Component {\n  constructor(props) {\n    super(props);\n\n    this.state = {\n      isLoading: true,\n      mode: 'tab',\n      showEmptySearchSuggestions: true,\n      enableFuzzySearch: true\n    };\n  }\n\n  async componentDidMount() {\n    const sakaSettings = await this.fetchSakaSettings();\n    this.setState(sakaSettings);\n  }\n\n  fetchSakaSettings = async function fetchSakaSettings() {\n    const { sakaSettings } = await browser.storage.sync.get(['sakaSettings']);\n\n    if (sakaSettings !== undefined) {\n      return {\n        isLoading: false,\n        mode: sakaSettings.mode,\n        showEmptySearchSuggestions: sakaSettings.showEmptySearchSuggestions,\n        enableFuzzySearch: sakaSettings.enableFuzzySearch\n      };\n    }\n\n    return {\n      isLoading: false\n    };\n  };\n\n  handleOptionsSave = () => {\n    const settingsStore = {\n      mode: this.state.mode,\n      showEmptySearchSuggestions: this.state.showEmptySearchSuggestions,\n      enableFuzzySearch: this.state.enableFuzzySearch\n    };\n\n    browser.storage.sync.set({ sakaSettings: settingsStore });\n  };\n\n  handleModeChange = e => {\n    this.setState({\n      mode: e.target.value\n    });\n  };\n\n  handleShowSearchSuggestionsChange = () => {\n    this.setState({\n      showEmptySearchSuggestions: !this.state.showEmptySearchSuggestions\n    });\n  };\n\n  handleEnableFuzzySearch = () => {\n    this.setState({\n      enableFuzzySearch: !this.state.enableFuzzySearch\n    });\n  };\n\n  render() {\n    const { handleOpenSakaKeybindings } = this.props;\n\n    if (!this.state.isLoading) {\n      return (\n        <div className=\"options-form\">\n          <div className=\"mdc-list-group\">\n            <h3 className=\"mdc-list-group__subheader\">General Settings</h3>\n            <ul className=\"mdc-list mdc-list--non-interactive mdc-list--dense\">\n              <DefaultModeSelection\n                handleModeChange={this.handleModeChange}\n                mode={this.state.mode}\n              />\n              <li\n                role=\"separator\"\n                className=\"mdc-list-divider mdc-list-divider--padded options-separator\"\n              />\n              <OnlyShowSearchBarSelector\n                checked={this.state.showEmptySearchSuggestions}\n                handleShowSearchSuggestionsChange={\n                  this.handleShowSearchSuggestionsChange\n                }\n              />\n              <li\n                role=\"separator\"\n                className=\"mdc-list-divider mdc-list-divider--padded options-separator\"\n              />\n              <EnableFuzzySearch\n                checked={this.state.enableFuzzySearch}\n                handleEnableFuzzySearch={this.handleEnableFuzzySearch}\n              />\n              <li\n                role=\"separator\"\n                className=\"mdc-list-divider mdc-list-divider--padded options-separator\"\n              />\n              <ShowSakaHotkeys\n                handleOpenSakaKeybindings={handleOpenSakaKeybindings}\n              />\n              <li\n                role=\"separator\"\n                className=\"mdc-list-divider mdc-list-divider--padded options-separator\"\n              />\n            </ul>\n          </div>\n          <div dir=\"rtl\" className=\"options-save\">\n            <input\n              type=\"submit\"\n              value=\"Save\"\n              className=\"mdc-button mdc-button--raised mdc-button--dense options-save-button\"\n              onClick={this.handleOptionsSave}\n            />\n          </div>\n        </div>\n      );\n    }\n    return <div />;\n  }\n}\n"
  },
  {
    "path": "src/options/Main/SakaHotkeysList/HotkeyListRow.jsx",
    "content": "import { h } from 'preact';\n\nimport 'material-components-web/dist/material-components-web.css';\n\nconst HotkeyListRow = function HotkeyListRow({ title, keys }) {\n  const hotkeyShortcut = keys.map((key, index, keysArray) => (\n    <span>\n      <kbd>{key}</kbd>\n      {keysArray.length === index + 1 ? '' : '+'}\n    </span>\n  ));\n  return (\n    <li className=\"mdc-list-item option\">\n      <span className=\"mdc-list-item__text\">{title}</span>\n      <div className=\"mdc-list-item__meta mdc-switch\">{hotkeyShortcut}</div>\n    </li>\n  );\n};\n\nexport default HotkeyListRow;\n"
  },
  {
    "path": "src/options/Main/SakaHotkeysList/index.jsx",
    "content": "import { h } from 'preact';\nimport HotkeyListRow from './HotkeyListRow.jsx';\nimport 'material-components-web/dist/material-components-web.css';\nimport { ctrlChar } from 'lib/utils';\n\nconst SakaHotkeysList = function SakaHotkeysList({\n  handleOpenSakaKeybindings\n}) {\n  return (\n    <div className=\"saka-hotkey-list\">\n      <div id=\"top-bar\">\n        <i\n          className=\"mdc-icon-toggle material-icons\"\n          role=\"button\"\n          aria-pressed=\"false\"\n          aria-label=\"Back to Saka settings\"\n          onClick={handleOpenSakaKeybindings}\n          onKeyDown={handleOpenSakaKeybindings}\n        >\n          arrow_back\n        </i>\n        <div className=\"tooltip\">\n          <i\n            id=\"custom-hotkey-info\"\n            className=\"mdc-icon-toggle material-icons\"\n            aria-pressed=\"false\"\n            aria-label=\"Info about Saka custom hotkeys\"\n          >\n            info\n          </i>\n          {SAKA_PLATFORM === 'chrome' ? (\n            <span className=\"tooltiptext\">\n              To modify the Saka hotkeys, please visit\n              chrome://extensions/shortcuts\n            </span>\n          ) : (\n            <span className=\"tooltiptext\">\n              It is currently not possible to modify hotkeys in firefox\n            </span>\n          )}\n        </div>\n      </div>\n      <h3 className=\"mdc-list-group__subheader\">Keyboard Shortcuts</h3>\n      <div className=\"mdc-list-group\">\n        <ul className=\"mdc-list mdc-list--non-interactive mdc-list--dense\">\n          <HotkeyListRow title=\"Open Saka\" keys={[ctrlChar, 'space']} />\n          <li role=\"separator\" className=\"mdc-list-divider options-separator\" />\n          <HotkeyListRow title=\"Close Saka\" keys={['esc']} />\n          <li role=\"separator\" className=\"mdc-list-divider options-separator\" />\n          <HotkeyListRow\n            title=\"Close Saka (when search bar is empty and focused)\"\n            keys={['← backspace']}\n          />\n          <li role=\"separator\" className=\"mdc-list-divider options-separator\" />\n          <HotkeyListRow title=\"Next Result\" keys={['tab']} />\n          <li role=\"separator\" className=\"mdc-list-divider options-separator\" />\n          <HotkeyListRow title=\"Previous Result\" keys={['shift', 'tab']} />\n          <li role=\"separator\" className=\"mdc-list-divider options-separator\" />\n          <HotkeyListRow title=\"Clear Search\" keys={[ctrlChar, 'k']} />\n          <li role=\"separator\" className=\"mdc-list-divider options-separator\" />\n          <HotkeyListRow title=\"View previous search\" keys={[ctrlChar, 'z']} />\n          <li role=\"separator\" className=\"mdc-list-divider options-separator\" />\n          <HotkeyListRow title=\"View next search\" keys={[ctrlChar, 'y']} />\n          <li role=\"separator\" className=\"mdc-list-divider options-separator\" />\n          <HotkeyListRow\n            title=\"Switch to next page of results\"\n            keys={[ctrlChar, 'd']}\n          />\n          <li role=\"separator\" className=\"mdc-list-divider options-separator\" />\n          <HotkeyListRow\n            title=\"Switch To previous page of results\"\n            keys={[ctrlChar, 's']}\n          />\n          <li role=\"separator\" className=\"mdc-list-divider options-separator\" />\n          <HotkeyListRow\n            title=\"Switch Modes (when search bar is empty)\"\n            keys={['space']}\n          />\n          <li role=\"separator\" className=\"mdc-list-divider options-separator\" />\n          <HotkeyListRow\n            title=\"Switch Modes (when search bar not empty) \"\n            keys={['shift', 'space']}\n          />\n          <li role=\"separator\" className=\"mdc-list-divider options-separator\" />\n          <HotkeyListRow\n            title=\"Switch To Tabs Search\"\n            keys={[ctrlChar, 'shift', 'a']}\n          />\n          <li role=\"separator\" className=\"mdc-list-divider options-separator\" />\n          <HotkeyListRow\n            title=\"Switch To Recently Closed Tabs Search\"\n            keys={[ctrlChar, 'shift', 'c']}\n          />\n          <li role=\"separator\" className=\"mdc-list-divider options-separator\" />\n          <HotkeyListRow\n            title=\"Switch To Bookmark Search\"\n            keys={[ctrlChar, 'b']}\n          />\n          <li role=\"separator\" className=\"mdc-list-divider options-separator\" />\n          <HotkeyListRow\n            title=\"Switch To History Search\"\n            keys={[ctrlChar, 'shift', 'e']}\n          />\n        </ul>\n      </div>\n    </div>\n  );\n};\n\nexport default SakaHotkeysList;\n"
  },
  {
    "path": "src/options/saka-options.jsx",
    "content": "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",
    "content": "import browser from 'webextension-polyfill';\nimport { h, Component } from 'preact';\nimport msg from 'msg/client.js';\nimport 'scss/styles.scss';\n\nexport default class BackgroundImage extends Component {\n  state = {\n    screenshot: undefined\n  };\n\n  componentDidMount() {\n    (async () => {\n      const { screenshot } = await browser.storage.local.get('screenshot');\n      this.setState({ screenshot });\n      await msg('focusTab');\n      await browser.storage.local.remove('screenshot');\n    })();\n  }\n\n  render() {\n    const { children } = this.props;\n    const { screenshot } = this.state;\n    return (\n      <div\n        id=\"background-image\"\n        style={screenshot && `background-image: url(\"${screenshot}\")`}\n      >\n        {children}\n      </div>\n    );\n  }\n\n  // componentWillReceiveProps (nextProps) {\n  //   if (nextProps.suggestion.tabId !== this.props.suggestion.tabId) {\n  //     this.fetchImage(nextProps.suggestion.tabId);\n  //   }\n  // }\n  // shouldComponentUpdate (nextProps, nextState) {\n  //   return nextState.image !== this.state.image;\n  // }\n  // fetchImage = async () => {\n\n  // }\n}\n"
  },
  {
    "path": "src/saka/Main/Components/GUIContainer/index.jsx",
    "content": "import { h, Component } from 'preact';\nimport msg from 'msg/client.js';\nimport 'scss/styles.scss';\n\n// Makes GUI constant size\nexport default class GUIContainer extends Component {\n  state = {\n    zoom: 0\n  };\n\n  componentWillMount() {\n    window.addEventListener('zoom', this.onZoomChange);\n    msg('zoom').then(this.setZoom);\n  }\n\n  componentWillUnmount() {\n    window.removeEventListener('zoom', this.onZoomChange);\n  }\n\n  onZoomChange = event => {\n    this.setZoom(event.detail.zoom);\n  };\n\n  setZoom = zoom => {\n    this.setState({ zoom });\n  };\n\n  render() {\n    const { children, onWheel } = this.props;\n    const { zoom } = this.state;\n    // opacity: 0.01 is just a trick to hide the component and not prevent it from\n    // from rendering/mounting in the DOM, which would preven the search bar from focusing\n    return (\n      <main\n        id=\"GUIContainer\"\n        onWheel={onWheel}\n        style={\n          zoom === 0\n            ? {\n                opacity: '0.01'\n              }\n            : {\n                transform: `translateX(-50%) scale(${1 / zoom})`, // firefox can't handle calculated css scale props\n                maxWidth: `${100 * zoom}%`,\n                top: `${Math.max(0, (window.innerHeight - 504 / zoom) / 2)}px`\n                // WARNING: 504 is the height of the GUI container with all 6 suggestions in pixels at the default zoom\n                // This may change in future updates, so the constant will have to be updated accordingly\n                // TODO: on each extension update, calculate the height of the Saka GUI with all 6 suggestions\n                // and use that instead of the constant 504\n              }\n        }\n      >\n        {children}\n      </main>\n    );\n  }\n}\n"
  },
  {
    "path": "src/saka/Main/Components/Icon/index.jsx",
    "content": "import { h } from 'preact';\nimport 'scss/styles.scss';\n\nexport default ({ icon, color }) => {\n  return (\n    <i\n      id=\"icon\"\n      className=\"material-icons\"\n      aria-hidden=\"true\"\n      style={{ color }}\n    >\n      {icon}\n    </i>\n  );\n};\n"
  },
  {
    "path": "src/saka/Main/Components/ModeSwitcher/index.jsx",
    "content": "import { h } from 'preact';\nimport { suggestions } from 'src/suggestion_engine/server/providers/mode.js';\nimport Icon from 'src/saka/Main/Components/Icon/index.jsx';\nimport { fadedColorMap } from 'lib/colors.js';\nimport 'scss/styles.scss';\n\nexport default ({ mode, setMode }) => {\n  const validModes = suggestions.map(suggestion => {\n    const color =\n      suggestion.mode === mode ? suggestion.fadedColor : fadedColorMap.unknown;\n\n    return (\n      <div\n        className=\"mode-switcher-icon\"\n        style={\n          suggestion.mode === mode\n            ? `border-top: 3px solid  ${suggestion.fadedColor};`\n            : {}\n        }\n        onClick={() => setMode(suggestion.mode)}\n      >\n        <Icon icon={suggestion.icon} color={color} />\n      </div>\n    );\n  });\n\n  return <div className=\"mode-switcher-wrapper\">{validModes}</div>;\n};\n"
  },
  {
    "path": "src/saka/Main/Components/PaginationBar/index.jsx",
    "content": "import { h } from 'preact';\nimport { ctrlChar } from 'lib/utils.js';\nimport 'scss/styles.scss';\n\nexport default ({\n  firstVisibleIndex,\n  suggestions,\n  maxSuggestions,\n  onClickPrevious,\n  onClickNext\n}) =>\n  suggestions.length === 0 ? (\n    undefined\n  ) : (\n    <section id=\"pagination-bar\">\n      <div\n        role=\"button\"\n        onClick={onClickPrevious}\n        onKeyPress={onClickPrevious}\n        className=\"paginator-next-button\"\n        tabIndex={0}\n      >\n        <span className=\"arrow-normalizer\">◄</span> {ctrlChar}-S\n      </div>\n      <div className=\"paginator-text-info\">\n        {`${firstVisibleIndex + 1} - ${firstVisibleIndex +\n          Math.min(suggestions.length, maxSuggestions)} / ${\n          suggestions.length\n        }`}\n      </div>\n      <div\n        role=\"button\"\n        onClick={onClickNext}\n        onKeyPress={onClickNext}\n        className=\"paginator-next-button\"\n        tabIndex={0}\n      >\n        {ctrlChar}-D ►\n      </div>\n    </section>\n  );\n"
  },
  {
    "path": "src/saka/Main/Components/SearchBar/Button/index.jsx",
    "content": "import { h, Component } from 'preact';\nimport '@material/button/dist/mdc.button.min.css';\nimport { icons } from 'suggestion_utils/index.js';\nimport { colorMap, fadedColorMap } from 'lib/colors.js';\nimport 'scss/styles.scss';\n\n// 1. Reload\n// 2. Search\n// 3. History\n// 4. Calculate\n// 5. Activate\n// 6. Go\n// 7. Command\n\n// function icon (searchText, searchValue, tabURL, modifiers) {\n//   return searchText === tabURL\n//     ? 'refresh'\n//     : iconForType[isURL(searchValue) ? 'url' : 'search'];\n// }\n\n// function icon (suggestion) {\n//   if (suggestion) {\n//     switch (suggestion.type) {\n//       case 'tab':\n//         return 'tab';\n//       case 'closedTab':\n//         return 'restore';\n//     }\n//   }\n//   return 'error';\n// }\n\nexport default class extends Component {\n  state = {\n    hovered: false\n  };\n\n  handleMouseEnter = () => {\n    this.setState({ hovered: true });\n  };\n\n  handleMouseLeave = () => {\n    this.setState({ hovered: false });\n  };\n\n  render() {\n    const { mode, onClick } = this.props;\n    const { hovered } = this.state;\n    const { handleMouseEnter, handleMouseLeave } = this;\n    return (\n      <div\n        role=\"button\"\n        id=\"action-button\"\n        onClick={onClick}\n        onKeyPress={onClick}\n        onMouseEnter={handleMouseEnter}\n        onMouseLeave={handleMouseLeave}\n        tabIndex={0}\n      >\n        <i\n          className=\"material-icons\"\n          aria-hidden=\"true\"\n          style={{\n            color: hovered ? colorMap.mode : fadedColorMap[mode]\n          }}\n        >\n          {hovered ? icons.mode : icons[mode]}\n        </i>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "src/saka/Main/Components/SearchBar/Input/index.jsx",
    "content": "import { Component, h } from 'preact';\n\n// import '@material/textfield/dist/mdc.textfield.min.css';\nimport 'scss/styles.scss';\n\nexport default class Input extends Component {\n  render() {\n    const {\n      placeholder,\n      searchString,\n      onKeyDown,\n      onInput,\n      onBlur\n    } = this.props;\n\n    return (\n      <section className=\"mdc-text-field mdc-text-field--fullwidth search-field-wrapper\">\n        <input\n          id=\"search-bar\"\n          className=\"mdc-text-field__input search-field-input\"\n          type=\"text\"\n          placeholder={placeholder}\n          aria-label={placeholder}\n          onKeyDown={onKeyDown}\n          onInput={onInput}\n          value={searchString}\n          onBlur={onBlur}\n          ref={input => input && input.focus()}\n        />\n      </section>\n    );\n  }\n}\n"
  },
  {
    "path": "src/saka/Main/Components/SearchBar/index.jsx",
    "content": "import { h } from 'preact';\nimport 'scss/styles.scss';\nimport Input from './Input/index.jsx';\n\nexport default ({\n  placeholder,\n  searchString,\n  suggestion,\n  onKeyDown,\n  onInput,\n  onBlur\n}) => (\n  <form className=\"search-bar-container\">\n    <Input\n      placeholder={placeholder}\n      searchString={searchString}\n      suggestion={suggestion}\n      onKeyDown={onKeyDown}\n      onInput={onInput}\n      onBlur={onBlur}\n    />\n  </form>\n);\n"
  },
  {
    "path": "src/saka/Main/Components/SettingsBar/index.jsx",
    "content": "import { h } from 'preact';\nimport { colorMap } from 'lib/colors.js';\nimport 'scss/styles.scss';\n\nconst Item = ({ label, color }) => (\n  <span\n    role=\"button\"\n    aria-pressed=\"false\"\n    style={{ color }}\n    className=\"settings-item\"\n  >\n    {label}\n  </span>\n);\n\nexport default () => (\n  <section id=\"settings-bar\">\n    <Item label=\"Commands\" color={colorMap.command} />\n    <Item label=\"Tabs\" color={colorMap.tabs} />\n    <Item label=\"Search\" color={colorMap.search} />\n    <Item label=\"History\" color={colorMap.history} />\n    <Item label=\"Bookmarks\" color={colorMap.bookmark} />\n    <Item label=\"Dictionary\" color={colorMap.dictionary} />\n    <Item label=\"Calculator\" color={colorMap.calculator} />\n  </section>\n);\n"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Components/Suggestion/index.jsx",
    "content": "import { h } from 'preact';\nimport { fadedColorMap } from 'lib/colors.js';\nimport { ctrlChar } from 'lib/utils.js';\nimport { icons } from 'suggestion_utils/index.js';\nimport 'scss/styles.scss';\n\nexport default ({\n  type,\n  title,\n  titleColor,\n  secondary,\n  secondaryColor,\n  url,\n  favIconUrl,\n  incognito,\n  selected,\n  index,\n  onClick\n}) => {\n  const color = fadedColorMap[type];\n  const icon = icons[type];\n  const incognitoIcon = icons.incognito;\n  let suggestionIcon;\n\n  if (incognito === true) {\n    suggestionIcon = (\n      <i className=\"material-icons\" aria-hidden=\"true\" style={{ color }}>\n        {incognitoIcon}\n      </i>\n    );\n  } else if (SAKA_PLATFORM === 'chrome' && url) {\n    suggestionIcon = (\n      <div\n        className=\"suggestion-icon\"\n        style={`content: -webkit-image-set(url(chrome://favicon/size/16@1x/${url}) 1x, url(chrome://favicon/size/16@2x/${url}) 2x)`}\n      />\n    );\n  } else if (SAKA_PLATFORM === 'firefox' && favIconUrl) {\n    suggestionIcon = (\n      <img className=\"suggestion-icon\" src={favIconUrl} alt=\"\" />\n    );\n  } else {\n    suggestionIcon = (\n      <i className=\"material-icons\" aria-hidden=\"true\" style={{ color }}>\n        {icon}\n      </i>\n    );\n  }\n\n  return (\n    <li\n      className=\"mdc-list-item search-item\"\n      style={{\n        backgroundColor: selected ? 'rgb(237, 237, 237)' : '#ffffff',\n        borderLeftColor: color\n      }}\n      onKeyPress={() => onClick(index)}\n      onClick={() => onClick(index)}\n    >\n      <span className=\"mdc-list-item__graphic search-icon\" role=\"presentation\">\n        {suggestionIcon}\n      </span>\n      <span className=\"mdc-list-item__text suggestion-text\">\n        <span className=\"suggestion-wrap-text\" style={{ color: titleColor }}>\n          {title}\n        </span>\n\n        {secondary && (\n          <span\n            className=\"mdc-list-item__secondary-text suggestion-wrap-text\"\n            style={{ color: secondaryColor || 'inherit' }}\n          >\n            {secondary}\n          </span>\n        )}\n      </span>\n      <span className=\"mdc-list-item__meta kbd-end-detail\">\n        {selected ? (\n          <i className=\"material-icons\" aria-hidden=\"true\" style={{ color }}>\n            {icon}\n          </i>\n        ) : (\n          `${ctrlChar}-${index + 1}`\n        )}\n      </span>\n    </li>\n  );\n};\n"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Containers/BookmarkSuggestion/index.jsx",
    "content": "import { h } from 'preact';\nimport highlight from 'lib/highlight.jsx';\nimport Suggestion from '../../Components/Suggestion/index.jsx';\n\nexport default ({\n  suggestion: { title, url, matches },\n  selected,\n  index,\n  onClick\n}) => (\n  <Suggestion\n    type=\"bookmark\"\n    title={highlight(title, 'title', matches)}\n    titleColor=\"#000000\"\n    secondary={highlight(url, 'url', matches)}\n    secondaryColor=\"rgba(63, 81, 245, 1.0)\"\n    url={url}\n    icon=\"star_border\"\n    selected={selected}\n    index={index}\n    onClick={onClick}\n  />\n);\n"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Containers/ClosedTabSuggestion/index.jsx",
    "content": "import { h } from 'preact';\nimport highlight from 'lib/highlight.jsx';\nimport Suggestion from '../../Components/Suggestion/index.jsx';\n\nexport default ({\n  suggestion: { title, url, matches, favIconUrl, incognito },\n  selected,\n  index,\n  onClick\n}) => (\n  <Suggestion\n    type=\"closedTab\"\n    title={highlight(title, 'title', matches)}\n    titleColor=\"#000000\"\n    secondary={highlight(url, 'url', matches)}\n    secondaryColor=\"rgba(63, 81, 245, 1.0)\"\n    url={url}\n    favIconUrl={favIconUrl}\n    incognito={incognito}\n    icon=\"star_border\"\n    selected={selected}\n    index={index}\n    onClick={onClick}\n  />\n);\n"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Containers/CommandSuggestion/index.jsx",
    "content": "import { h } from 'preact';\nimport Suggestion from '../../Components/Suggestion/index.jsx';\n\nexport default ({ suggestion: { title }, selected, index, onClick }) => (\n  <Suggestion\n    type=\"command\"\n    title={title}\n    icon=\"input\"\n    titleColor=\"rgb(75, 165, 75)\"\n    selected={selected}\n    index={index}\n    onClick={onClick}\n  />\n);\n"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Containers/HistorySuggestion/index.jsx",
    "content": "import { h } from 'preact';\nimport highlight from 'lib/highlight.jsx';\nimport Suggestion from '../../Components/Suggestion/index.jsx';\n\nexport default ({\n  suggestion: { title, url, matches },\n  selected,\n  index,\n  onClick\n}) => (\n  <Suggestion\n    type=\"history\"\n    title={highlight(title, 'title', matches)}\n    titleColor=\"#000000\"\n    secondary={highlight(url, 'url', matches)}\n    secondaryColor=\"rgba(63, 81, 245, 1.0)\"\n    icon=\"history\"\n    url={url}\n    selected={selected}\n    index={index}\n    onClick={onClick}\n  />\n);\n"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Containers/RecentlyViewedSuggestion/index.jsx",
    "content": "import { h } from 'preact';\nimport highlight from 'lib/highlight.jsx';\nimport Suggestion from '../../Components/Suggestion/index.jsx';\n\nexport default ({\n  suggestion: { title, url, matches, favIconUrl, incognito },\n  selected,\n  index,\n  onClick\n}) => (\n  <Suggestion\n    type=\"recentlyViewed\"\n    title={highlight(title, 'title', matches)}\n    titleColor=\"#000000\"\n    secondary={highlight(url, 'url', matches)} // TODO: highlight matches are for normal URL not pretty URL\n    secondaryColor=\"rgba(63, 81, 245, 1.0)\"\n    icon=\"recentlyViewed\"\n    favIconUrl={favIconUrl}\n    incognito={incognito}\n    url={url}\n    selected={selected}\n    index={index}\n    onClick={onClick}\n  />\n);\n"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Containers/SearchEngineSuggestion/index.jsx",
    "content": "import { h } from 'preact';\nimport Suggestion from '../../Components/Suggestion/index.jsx';\n\nexport default ({\n  suggestion: { title, isURL, prettyURL },\n  selected,\n  index,\n  onClick\n}) => (\n  <Suggestion\n    type=\"search\"\n    title={isURL ? prettyURL : title}\n    titleColor={isURL ? 'rgba(63, 81, 245, 1.0)' : 'rgba(0, 0, 00, 0.87)'}\n    secondary={isURL ? title : undefined}\n    icon={isURL ? 'insert_drive_file' : 'search'}\n    selected={selected}\n    index={index}\n    onClick={onClick}\n  />\n);\n"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Containers/SuggestionSelector.jsx",
    "content": "import { h, Component } from 'preact';\nimport TabSuggestion from './TabSuggestion/index.jsx';\nimport ClosedTabSuggestion from './ClosedTabSuggestion/index.jsx';\nimport BookmarkSuggestion from './BookmarkSuggestion/index.jsx';\nimport HistorySuggestion from './HistorySuggestion/index.jsx';\nimport RecentlyViewedSuggestion from './RecentlyViewedSuggestion/index.jsx';\nimport CommandSuggestion from './CommandSuggestion/index.jsx';\nimport SearchEngineSuggestion from './SearchEngineSuggestion/index.jsx';\nimport UnknownSuggestion from './UnknownSuggestion/index.jsx';\n\nexport default props => {\n  switch (props.suggestion.type) {\n    case 'tab':\n      return <TabSuggestion {...props} />;\n    case 'closedTab':\n      return <ClosedTabSuggestion {...props} />;\n    case 'bookmark':\n      return <BookmarkSuggestion {...props} />;\n    case 'history':\n      return <HistorySuggestion {...props} />;\n    case 'recentlyViewed':\n      return <RecentlyViewedSuggestion {...props} />;\n    case 'command':\n      return <CommandSuggestion {...props} />;\n    case 'searchEngine':\n      return <SearchEngineSuggestion {...props} />;\n    default:\n      return <UnknownSuggestion {...props} />;\n  }\n};\n"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Containers/TabSuggestion/index.jsx",
    "content": "import { h } from 'preact';\nimport highlight from 'lib/highlight.jsx';\nimport Suggestion from '../../Components/Suggestion/index.jsx';\n\nexport default ({\n  suggestion: { title, url, matches, favIconUrl, incognito },\n  selected,\n  index,\n  onClick\n}) => (\n  <Suggestion\n    type=\"tab\"\n    title={highlight(title, 'title', matches)}\n    titleColor=\"#000000\"\n    secondary={highlight(url, 'url', matches)} // TODO: highlight matches are for normal URL not pretty URL\n    // secondary={prettyURL}\n    secondaryColor=\"rgba(63, 81, 245, 1.0)\"\n    url={url}\n    favIconUrl={favIconUrl}\n    incognito={incognito}\n    icon=\"star_border\"\n    selected={selected}\n    index={index}\n    onClick={onClick}\n    class=\"tab-suggestion\"\n  />\n);\n"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/Containers/UnknownSuggestion/index.jsx",
    "content": "import { h } from 'preact';\nimport Suggestion from '../../Components/Suggestion/index.jsx';\n\nexport default ({ suggestion: { title }, selected, index, onClick }) => (\n  <Suggestion\n    type=\"unknown\"\n    title={title}\n    icon=\"error_outline\"\n    titleColor=\"red\"\n    selected={selected}\n    index={index}\n    onClick={onClick}\n  />\n);\n"
  },
  {
    "path": "src/saka/Main/Components/SuggestionList/index.jsx",
    "content": "import 'material-components-web/dist/material-components-web.css';\nimport 'scss/styles.scss';\nimport { h, Component } from 'preact';\nimport Suggestion from './Containers/SuggestionSelector.jsx';\n\nexport default ({\n  searchString,\n  suggestions,\n  selectedIndex,\n  firstVisibleIndex,\n  maxSuggestions,\n  onSuggestionClick\n}) => (\n  <ul className=\"mdc-list mdc-list--two-line mdc-list--avatar-list two-line-avatar-text-icon-demo list-container\">\n    {suggestions\n      .slice(firstVisibleIndex, firstVisibleIndex + maxSuggestions)\n      .map((suggestion, index) => {\n        return (\n          <Suggestion\n            suggestion={suggestion}\n            searchString={searchString}\n            selected={index === selectedIndex}\n            index={index}\n            onClick={onSuggestionClick}\n          />\n        );\n      })}\n  </ul>\n);\n"
  },
  {
    "path": "src/saka/Main/Containers/GeneralSearch/index.jsx",
    "content": "import { h } from 'preact';\n\nexport default () => <h1>General Search</h1>;\n"
  },
  {
    "path": "src/saka/Main/Containers/StandardSearch/index.jsx",
    "content": "import browser from 'webextension-polyfill';\nimport { Component, h } from 'preact';\nimport {\n  getSuggestions,\n  activateSuggestion,\n  closeTab\n} from 'suggestion_engine/client/index.js';\nimport { preprocessSuggestion } from 'suggestion_utils/index.js';\nimport { ctrlKey } from 'lib/utils.js';\nimport { slowWheelEvent } from 'lib/dom.js';\nimport SearchBar from '../../Components/SearchBar/index.jsx';\nimport SuggestionList from '../../Components/SuggestionList/index.jsx';\nimport PaginationBar from '../../Components/PaginationBar/index.jsx';\nimport GUIContainer from '../../Components/GUIContainer/index.jsx';\nimport BackgroundImage from '../../Components/BackgroundImage/index.jsx';\nimport ModeSwitcher from '../../Components/ModeSwitcher/index.jsx';\n\n// provides suggestions but doesn't autocomplete input\n\nexport default class extends Component {\n  state = {\n    searchString: '',\n    suggestions: [],\n    selectedIndex: 0, // 0 <= selectedIndex < maxSuggestions\n    firstVisibleIndex: 0, // 0 <= firstVisibleIndex < suggestion.length\n    maxSuggestions: 6,\n    undoIndex: this.props.searchHistory.size - 1\n  };\n\n  componentDidMount() {\n    this.updateAutocompleteSuggestions('').then(() => {\n      const { suggestions } = this.state;\n      if (suggestions.length > 1) {\n        this.setState({\n          selectedIndex: 1\n        });\n      }\n    });\n  }\n\n  componentDidUpdate(prevProps) {\n    if (this.props.mode !== prevProps.mode) {\n      this.updateAutocompleteSuggestions(this.state.searchString);\n    }\n  }\n\n  getPreviousSearchString = () => {\n    if (this.state.undoIndex !== 0) {\n      this.setState({\n        searchString: [...this.props.searchHistory][this.state.undoIndex],\n        undoIndex: this.state.undoIndex - 1\n      });\n      this.updateAutocompleteSuggestions(this.state.searchString);\n    }\n  };\n\n  getNextSearchString = () => {\n    if (this.state.undoIndex < this.props.searchHistory.size) {\n      this.setState({\n        searchString: [...this.props.searchHistory][this.state.undoIndex],\n        undoIndex: this.state.undoIndex + 1\n      });\n      this.updateAutocompleteSuggestions(this.state.searchString);\n    }\n  };\n\n  handleWheel = slowWheelEvent(\n    50,\n    () => {\n      this.incrementSelectedIndex(1);\n    },\n    () => {\n      this.incrementSelectedIndex(-1);\n    }\n  );\n\n  handleKeyDown = e => {\n    switch (e.key) {\n      case 'Escape':\n        browser.runtime.sendMessage({\n          key: 'closeSaka',\n          searchHistory: [...this.props.searchHistory]\n        });\n        break;\n      case 'Backspace':\n        if (ctrlKey(e)) {\n          e.preventDefault();\n          this.closeTab();\n        } else if (!e.repeat && e.target.value === '') {\n          browser.runtime.sendMessage({\n            key: 'closeSaka',\n            searchHistory: [...this.props.searchHistory]\n          });\n        }\n        break;\n      case 'ArrowLeft':\n      case 'ArrowRight':\n        break;\n      case 'ArrowDown':\n        e.preventDefault();\n        this.props.updateSearchHistory(this.state.searchString);\n        this.incrementSelectedIndex(1);\n        break;\n      case 'ArrowUp':\n        e.preventDefault();\n        this.props.updateSearchHistory(this.state.searchString);\n        this.incrementSelectedIndex(-1);\n        break;\n      case 'Tab':\n        e.preventDefault();\n        this.props.updateSearchHistory(this.state.searchString);\n        e.shiftKey\n          ? this.incrementSelectedIndex(-1)\n          : this.incrementSelectedIndex(1);\n        break;\n      case '1':\n      case '2':\n      case '3':\n      case '4':\n      case '5':\n      case '6':\n        if (ctrlKey(e)) {\n          e.preventDefault();\n          this.tryActivateSuggestion(Number.parseInt(10, e.key) - 1);\n        }\n        break;\n      case 'Enter':\n        e.preventDefault();\n        this.props.updateSearchHistory(\n          this.state.searchString,\n          this.tryActivateSuggestion\n        );\n        break;\n      case 'k':\n        if (ctrlKey(e)) {\n          e.preventDefault();\n          this.setState({ searchString: '' });\n          this.updateAutocompleteSuggestions('');\n        }\n        break;\n      case 's':\n        if (ctrlKey(e)) {\n          e.preventDefault();\n          this.previousPage();\n        }\n        break;\n      case 'd':\n        if (ctrlKey(e)) {\n          e.preventDefault();\n          this.nextPage();\n        }\n        break;\n      case ' ':\n        if (e.shiftKey || this.state.searchString === '') {\n          e.preventDefault();\n          this.props.shuffleMode();\n        }\n        break;\n      case 'A':\n        if (ctrlKey(e)) {\n          e.preventDefault();\n          this.props.setMode('tab');\n        }\n        break;\n      case 'C':\n        if (ctrlKey(e)) {\n          e.preventDefault();\n          this.props.setMode('closedTab');\n        }\n        break;\n      case 'M':\n        if (ctrlKey(e)) {\n          e.preventDefault();\n          this.props.setMode('mode');\n        }\n        break;\n      case 'b':\n        if (ctrlKey(e)) {\n          e.preventDefault();\n          this.props.setMode('bookmark');\n        }\n        break;\n      case 'E':\n        if (ctrlKey(e)) {\n          e.preventDefault();\n          this.props.setMode('history');\n        }\n        break;\n      case 'z':\n        if (ctrlKey(e)) {\n          e.preventDefault();\n          this.getPreviousSearchString();\n        }\n        break;\n      case 'y':\n        if (ctrlKey(e)) {\n          e.preventDefault();\n          this.getNextSearchString();\n        }\n        break;\n      case 'X':\n        if (ctrlKey(e)) {\n          e.preventDefault();\n          this.props.setMode('recentlyViewed');\n        }\n        break;\n      default:\n        this.setState({\n          undoIndex: this.props.searchHistory.size - 1\n        });\n        break;\n    }\n  };\n\n  nextPage = () => {\n    const {\n      firstVisibleIndex,\n      maxSuggestions,\n      suggestions: { length: numSuggestions }\n    } = this.state;\n    const newFirstVisibleIndex = Math.max(\n      0,\n      Math.min(\n        firstVisibleIndex + maxSuggestions,\n        numSuggestions - maxSuggestions\n      )\n    );\n    this.setState({\n      firstVisibleIndex: newFirstVisibleIndex,\n      selectedIndex: 0\n    });\n  };\n\n  previousPage = () => {\n    const { firstVisibleIndex, maxSuggestions } = this.state;\n    const newFirstVisibleIndex = Math.max(\n      0,\n      firstVisibleIndex - maxSuggestions\n    );\n    this.setState({\n      firstVisibleIndex: newFirstVisibleIndex,\n      selectedIndex: 0\n    });\n  };\n\n  incrementSelectedIndex = increment => {\n    const { selectedIndex } = this.state;\n    this.trySetIndex(selectedIndex + increment);\n  };\n\n  trySetIndex = index => {\n    if (this.indexInRange(index)) {\n      this.setState({ selectedIndex: index });\n    } else {\n      const { firstVisibleIndex, maxSuggestions, suggestions } = this.state;\n      if (index < 0 && firstVisibleIndex > 0) {\n        this.setState({ firstVisibleIndex: firstVisibleIndex - 1 });\n      } else if (\n        index >= maxSuggestions &&\n        firstVisibleIndex + maxSuggestions < suggestions.length\n      ) {\n        this.setState({ firstVisibleIndex: firstVisibleIndex + 1 });\n      }\n    }\n  };\n\n  indexInRange = index => {\n    const { suggestions, maxSuggestions } = this.state;\n    return (\n      index >= 0 &&\n      index <= Math.max(0, Math.min(suggestions.length, maxSuggestions) - 1)\n    );\n  };\n\n  closeTab = async (index = this.state.selectedIndex) => {\n    const { suggestions, firstVisibleIndex } = this.state;\n    const suggestion = suggestions[firstVisibleIndex + index];\n    if (suggestion && this.props.mode === 'tab') {\n      await closeTab(suggestion);\n      suggestions.splice(firstVisibleIndex + index, 1);\n      this.setState({ suggestions });\n    }\n  };\n\n  tryActivateSuggestion = async (index = this.state.selectedIndex) => {\n    const { suggestions, firstVisibleIndex } = this.state;\n    const suggestion = suggestions[firstVisibleIndex + index];\n    if (suggestion) {\n      if (suggestion.type === 'mode') {\n        this.props.setMode(suggestion.mode);\n      } else {\n        activateSuggestion(suggestion);\n        await browser.runtime.sendMessage({\n          key: 'closeSaka',\n          searchHistory: [...this.props.searchHistory]\n        });\n      }\n    }\n  };\n\n  handleInput = e => {\n    const newSearchString = e.target.value;\n    const { oldSearchString } = this.state;\n    this.setState({ searchString: newSearchString });\n    if (newSearchString !== oldSearchString) {\n      this.setState({\n        selectedIndex: 0,\n        searchString: newSearchString\n      });\n      this.updateAutocompleteSuggestions(newSearchString);\n    }\n  };\n\n  updateAutocompleteSuggestions = async searchStringAtLookup => {\n    const suggestions = await getSuggestions(\n      this.props.mode,\n      searchStringAtLookup\n    );\n\n    const { searchString: searchStringNow } = this.state;\n    if (searchStringNow === searchStringAtLookup) {\n      this.setState({\n        suggestions: suggestions.map(suggestion =>\n          preprocessSuggestion(suggestion, searchStringAtLookup)\n        ),\n        firstVisibleIndex: 0,\n        selectedIndex: 0\n      });\n    }\n  };\n\n  handleBlur = e => {\n    this.props.updateSearchHistory(e.target.value);\n  };\n\n  handleButtonClick = () => {\n    this.props.setMode('mode');\n  };\n\n  handleSuggestionClick = index => {\n    this.tryActivateSuggestion(index);\n  };\n\n  render() {\n    const { placeholder, mode, showEmptySearchSuggestions } = this.props;\n    const {\n      searchString,\n      suggestions,\n      selectedIndex,\n      firstVisibleIndex,\n      maxSuggestions\n    } = this.state;\n    const suggestion = suggestions[firstVisibleIndex + selectedIndex];\n\n    if (!showEmptySearchSuggestions && !searchString) {\n      return (\n        <BackgroundImage suggestion={suggestion}>\n          <GUIContainer onWheel={this.handleWheel}>\n            <ModeSwitcher setMode={this.props.setMode} />\n            <SearchBar\n              placeholder={placeholder}\n              searchString={searchString}\n              suggestion={suggestion}\n              onKeyDown={this.handleKeyDown}\n              onInput={this.handleInput}\n              onBlur={this.handleBlur}\n              onButtonClick={this.handleButtonClick}\n              onSuggestionClick={this.handleSuggestionClick}\n              mode={mode}\n            />\n          </GUIContainer>\n        </BackgroundImage>\n      );\n    }\n\n    // TODO: Rename suggestions and suggestion\n    return (\n      <BackgroundImage suggestion={suggestion}>\n        <GUIContainer onWheel={this.handleWheel}>\n          <ModeSwitcher mode={mode} setMode={this.props.setMode} />\n          <SearchBar\n            placeholder={placeholder}\n            searchString={searchString}\n            suggestion={suggestion}\n            onKeyDown={this.handleKeyDown}\n            onInput={this.handleInput}\n            onBlur={this.handleBlur}\n            onButtonClick={this.handleButtonClick}\n            onSuggestionClick={this.handleSuggestionClick}\n            mode={mode}\n          />\n          <SuggestionList\n            searchString={searchString}\n            suggestions={suggestions}\n            selectedIndex={selectedIndex}\n            firstVisibleIndex={firstVisibleIndex}\n            maxSuggestions={maxSuggestions}\n            onSuggestionClick={this.handleSuggestionClick}\n          />\n          <PaginationBar\n            selectedIndex={selectedIndex}\n            suggestions={suggestions}\n            firstVisibleIndex={firstVisibleIndex}\n            maxSuggestions={maxSuggestions}\n            onClickPrevious={this.previousPage}\n            onClickNext={this.nextPage}\n          />\n        </GUIContainer>\n      </BackgroundImage>\n    );\n  }\n}\n"
  },
  {
    "path": "src/saka/Main/Containers/TabSearch/index.jsx",
    "content": "import { h } from 'preact';\n\nexport default () => <h1>Tab Search</h1>;\n"
  },
  {
    "path": "src/saka/Main/index.jsx",
    "content": "import 'material-components-web/dist/material-components-web.css';\nimport 'scss/styles.scss';\nimport browser from 'webextension-polyfill';\nimport { Component, h } from 'preact';\nimport StandardSearch from './Containers/StandardSearch/index.jsx';\n\nexport default class Main extends Component {\n  constructor(props) {\n    super(props);\n\n    this.state = {\n      mode: 'tab',\n      modes: ['tab', 'closedTab', 'bookmark', 'history', 'recentlyViewed'],\n      isLoading: true,\n      showEmptySearchSuggestions: true,\n      searchHistory: new Set([])\n    };\n  }\n\n  async componentDidMount() {\n    const sakaSettings = await this.fetchSakaSettings();\n    this.setState(sakaSettings);\n  }\n\n  setMode = mode => {\n    this.setState({ mode });\n  };\n\n  shuffleMode = () => {\n    const { mode, modes } = this.state;\n    const nextIndex = modes.indexOf(mode) + 1;\n    const nextModeIndex = nextIndex >= modes.length ? 0 : nextIndex;\n    this.setMode(modes[nextModeIndex]);\n  };\n\n  fetchSakaSettings = async function fetchSakaSettings() {\n    const { sakaSettings } = await browser.storage.sync.get(['sakaSettings']);\n    let { searchHistory } = await browser.storage.sync.get(['searchHistory']);\n    searchHistory =\n      searchHistory !== undefined && searchHistory.length > 0\n        ? new Set(searchHistory)\n        : new Set(['']);\n\n    if (sakaSettings !== undefined) {\n      const { mode, showEmptySearchSuggestions } = sakaSettings;\n      return {\n        isLoading: false,\n        mode,\n        showEmptySearchSuggestions,\n        searchHistory\n      };\n    }\n\n    return {\n      isLoading: false,\n      searchHistory\n    };\n  };\n\n  updateSearchHistory = (searchString, callback) => {\n    const { searchHistory } = this.state;\n    searchHistory.delete(searchString);\n    searchHistory.add(searchString);\n\n    this.setState({ searchHistory }, callback);\n  };\n\n  render() {\n    const {\n      mode,\n      isLoading,\n      showEmptySearchSuggestions,\n      searchHistory\n    } = this.state;\n    const { setMode, shuffleMode } = this;\n\n    if (!isLoading) {\n      switch (mode) {\n        case 'tab':\n          return (\n            <StandardSearch\n              mode={mode}\n              placeholder=\"Tabs\"\n              setMode={setMode}\n              shuffleMode={shuffleMode}\n              showEmptySearchSuggestions={showEmptySearchSuggestions}\n              searchHistory={searchHistory}\n              updateSearchHistory={this.updateSearchHistory}\n            />\n          );\n        case 'closedTab':\n          return (\n            <StandardSearch\n              mode={mode}\n              placeholder=\"Recently Closed\"\n              setMode={setMode}\n              shuffleMode={shuffleMode}\n              showEmptySearchSuggestions={showEmptySearchSuggestions}\n              searchHistory={searchHistory}\n              updateSearchHistory={this.updateSearchHistory}\n            />\n          );\n        case 'bookmark':\n          return (\n            <StandardSearch\n              mode={mode}\n              placeholder=\"Bookmarks\"\n              setMode={setMode}\n              shuffleMode={shuffleMode}\n              showEmptySearchSuggestions={showEmptySearchSuggestions}\n              searchHistory={searchHistory}\n              updateSearchHistory={this.updateSearchHistory}\n            />\n          );\n        case 'history':\n          return (\n            <StandardSearch\n              mode={mode}\n              placeholder=\"History\"\n              setMode={setMode}\n              shuffleMode={shuffleMode}\n              showEmptySearchSuggestions={showEmptySearchSuggestions}\n              searchHistory={searchHistory}\n              updateSearchHistory={this.updateSearchHistory}\n            />\n          );\n        case 'recentlyViewed':\n          return (\n            <StandardSearch\n              mode={mode}\n              placeholder=\"Recently Viewed\"\n              setMode={setMode}\n              shuffleMode={shuffleMode}\n              showEmptySearchSuggestions={showEmptySearchSuggestions}\n              searchHistory={searchHistory}\n              updateSearchHistory={this.updateSearchHistory}\n            />\n          );\n        default:\n          return <div>Error, invalid mode</div>;\n      }\n    } else {\n      return <div />;\n    }\n  }\n}\n"
  },
  {
    "path": "src/saka/index.jsx",
    "content": "import { render, h } from 'preact';\nimport 'material-components-web/dist/material-components-web.css';\n\nimport Main from './Main/index.jsx';\n\nrender(<Main />, document.body);\n"
  },
  {
    "path": "src/scss/options.scss",
    "content": "$mdc-theme-primary: #00796b; // Purple 500\n$mdc-theme-secondary: #4db6ac; // Orange A200\n\n@import '@material/animation/functions';\n@import '@material/theme/mdc-theme';\n\nhtml {\n  background-color: #eeeeee;\n}\n\n.options-container {\n  background-color: #ffffff;\n  margin: auto;\n  position: relative;\n  top: 50%;\n  width: 60%;\n  margin-top: 80px;\n}\n\n.options-form {\n  padding: 10px;\n}\n\n.option {\n  padding-bottom: 10px;\n}\n\n.options-separator {\n  margin-bottom: 5px;\n  color: #f1f1f1;\n}\n\n.options-save {\n  margin-top: 5px;\n  margin-right: 10px;\n}\n\n.options-icon {\n  margin-top: 5px;\n}\n\n// http://www.jimmyscode.com/css-styling-for-kbd-tags/\nkbd {\n  padding: 0.1em 0.6em;\n  border: 1px solid #ccc;\n  font-size: 11px;\n  font-family: Arial, Helvetica, sans-serif;\n  background-color: #f7f7f7;\n  color: #333;\n  -moz-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset;\n  -webkit-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset;\n  box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset;\n  -moz-border-radius: 3px;\n  -webkit-border-radius: 3px;\n  border-radius: 3px;\n  display: inline-block;\n  margin: 0 0.1em;\n  text-shadow: 0 1px 0 #fff;\n  line-height: 1.4;\n  white-space: nowrap;\n}\n\n.tooltip {\n  float: right;\n}\n\n.tooltip .tooltiptext {\n  visibility: hidden;\n  background-color: black;\n  color: #fff;\n  text-align: center;\n  border-radius: 6px;\n  padding: 5px 5px;\n  position: absolute;\n  z-index: 3;\n}\n\n.tooltip:hover .tooltiptext {\n  visibility: visible;\n}\n\n.keyboard-shortcut-heading {\n  padding-bottom: 20px;\n}\n"
  },
  {
    "path": "src/scss/styles.scss",
    "content": "* {\n  box-sizing: border-box;\n}\n\nhtml,\nbody {\n  margin: 0;\n}\n\n#GUIContainer {\n  background-color: #ffffff;\n  position: absolute;\n  left: 50%;\n  right: 0;\n  width: 680px;\n  border-width: 0;\n  transform-origin: 50% 0%;\n  -webkit-box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2),\n    0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12);\n  box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2),\n    0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12);\n  overflow: hidden;\n}\n\n#background-image {\n  position: absolute;\n  left: 0;\n  right: 0;\n  top: 0;\n  bottom: 0;\n  background-size: cover;\n}\n\n#pagination-bar {\n  display: flex;\n  flex-flow: row no-wrap;\n  justify-content: space-around;\n  width: 100%;\n  height: 14px;\n  font-size: 12px;\n  line-height: 14px;\n  color: gray;\n}\n\n.pagination-item {\n  cursor: pointer;\n  opacity: 0.44;\n}\n\n.pagination-item:hover {\n  opacity: 1;\n}\n\n.paginator-next-button {\n  flex-grow: 3;\n  opacity: 0.6;\n  text-align: center;\n  cursor: pointer;\n}\n\n.paginator-next-button:hover {\n  color: white;\n  background-color: gray;\n}\n\n.paginator-text-info {\n  flex-grow: 2;\n  text-align: center;\n}\n\n.arrow-normalizer {\n  font-size: 15px;\n  line-height: 9px;\n  margin-right: 1px;\n}\n\n.search-bar-container {\n  width: 100%;\n  display: flex;\n  border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n}\n\nsection.search-field-wrapper {\n  font-size: 26px;\n  padding-right: 16px;\n  display: flex;\n  width: 624px;\n  border-bottom: none;\n}\n\ninput.search-field-input {\n  font-size: inherit;\n}\n\nspan.suggestion-text {\n  align-self: center;\n}\n\n#search-bar {\n  color: #000000;\n  padding-left: 20px;\n}\n\n#search-bar::selection {\n  background-color: rgba(63, 81, 245, 0.15);\n}\n\n#action-button {\n  height: 56px;\n  width: 56px;\n  min-width: 56px;\n  border-radius: 0;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  color: rgba(63, 81, 245, 0.6);\n  background-color: rgba(63, 81, 245, 0);\n  cursor: pointer;\n}\n\n#action-button:hover {\n  /* color: #ffffff;\n  background-color: rgba(63, 81, 245, 1.0); */\n  /* -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);\n  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); */\n}\n\n#action-button > i {\n  font-size: 32px;\n}\n\n#settings-bar {\n  display: flex;\n  flex-flow: row no-wrap;\n  justify-content: space-around;\n  width: 100%;\n  height: 12px;\n  font-size: 10px;\n  line-height: 10px;\n  color: rgba(0, 0, 0, 0.34);\n  border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n}\n\n.settings-item {\n  cursor: pointer;\n  opacity: 0.44;\n}\n\n.settings-item:hover {\n  opacity: 1;\n}\n\nul.list-container {\n  padding: 0px;\n}\n.search-item {\n  padding: 0px 16px;\n}\n\n.search-icon {\n  color: rgba(0, 0, 0, 0.26);\n}\n\n.two-line-avatar-text-icon-demo .mdc-list-item__graphic {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  /*color: white;*/\n}\n\nkbd {\n  color: #3f51f5;\n  font-family: roboto, sans-serif;\n  font-weight: normal;\n  letter-spacing: 0.56px;\n  -webkit-font-smoothing: antialiased;\n  border: 1px solid #3f51f5;\n  border-bottom: 5px solid;\n  border-left: 3px solid;\n  padding: 0px 4px;\n  border-radius: 4px;\n  border-bottom-left-radius: 3px;\n  box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14),\n    0 1px 5px 0 rgba(0, 0, 0, 0.12);\n  margin: auto 1px;\n}\n\n.grey-bg {\n  background: rgba(0, 0, 0, 0.26);\n}\n\nspan.kbd-end-detail {\n  white-space: nowrap;\n  color: gray;\n  text-decoration: none;\n  margin-right: 6px;\n}\n\n.search-item {\n  padding-left: 10px !important;\n  border-left: 6px solid;\n  cursor: pointer;\n}\n\n.search-item:hover {\n  background-color: rgb(245, 245, 245) !important;\n}\n\n.suggestion-wrap-text {\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  max-width: 530px;\n  overflow-x: hidden;\n}\n\n.suggestion-icon {\n  width: 25px;\n  height: 25px;\n}\n\n.mode-switcher-wrapper {\n  width: 100%;\n  display: flex;\n  flex-direction: row;\n  border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n}\n\n.mode-switcher-icon {\n  flex-grow: 1;\n  flex-wrap: nowrap;\n  background-color: white;\n  text-align: center;\n  padding: 2px 0px 2px 0px;\n  border-top: 3px solid transparent;\n  cursor: pointer;\n}\n\n.mode-switcher-icon + .mode-switcher-icon {\n  border-left: 2px solid rgba(0, 0, 0, 0.05);\n}\n\n.mode-switcher-icon:hover {\n  background-color: rgb(245, 245, 245);\n}\n"
  },
  {
    "path": "src/suggestion_engine/client/index.js",
    "content": "import msg from 'msg/client.js';\n\nexport async function getSuggestions(mode, searchString) {\n  return msg('sg', [mode, searchString]);\n}\n\nexport async function activateSuggestion(suggestion) {\n  return msg('activateSuggestion', suggestion);\n}\n\nexport async function closeTab(suggestion) {\n  return msg('closeTab', suggestion);\n}\n"
  },
  {
    "path": "src/suggestion_engine/server/index.js",
    "content": "import browser from 'webextension-polyfill';\nimport * as providers from './providers/index.js';\n\nexport async function getSuggestions([mode, searchString]) {\n  return providers[mode](searchString);\n}\n\nasync function focusOrCreateTab(url) {\n  const matchingTabs = await browser.tabs.query({ url });\n\n  if (matchingTabs && matchingTabs.length > 0) {\n    // If multiple matching tabs then just focus the first one\n    const existingTab = matchingTabs[0];\n\n    await browser.tabs.update(existingTab.id, { active: true });\n    await browser.windows.update(existingTab.windowId, { focused: true });\n  } else {\n    await browser.tabs.create({ url });\n  }\n}\n\nexport async function activateSuggestion(suggestion) {\n  switch (suggestion.type) {\n    case 'tab':\n      await browser.tabs.update(suggestion.tabId, { active: true });\n      await browser.windows.update(suggestion.windowId, { focused: true });\n      break;\n    case 'closedTab':\n      await browser.sessions.restore(suggestion.sessionId);\n      break;\n    case 'bookmark':\n      await focusOrCreateTab(suggestion.url);\n      break;\n    case 'history':\n      await focusOrCreateTab(suggestion.url);\n      break;\n    case 'recentlyViewed':\n      await activateSuggestion({\n        ...suggestion,\n        type: suggestion.originalType\n      });\n      break;\n    default:\n      console.error(\n        `activation not yet implemented for suggestions of type ${\n          suggestion.type\n        }`\n      );\n  }\n}\n\nexport async function closeTab(suggestion) {\n  await browser.tabs.remove(suggestion.tabId);\n}\n"
  },
  {
    "path": "src/suggestion_engine/server/providers/bookmark.js",
    "content": "import browser from 'webextension-polyfill';\nimport { isURL, extractProtocol, isProtocol } from 'lib/url.js';\nimport { getFilteredSuggestions } from 'lib/utils.js';\n\n// https://github.com/nwjs/chromium.src/blob/45886148c94c59f45f14a9dc7b9a60624cfa626a/components/omnibox/browser/bookmark_provider.cc\nasync function allBookmarkSuggestions(searchText) {\n  const searchCriteria = searchText === '' ? {} : searchText;\n  const searchResults = await browser.bookmarks.search(searchCriteria);\n\n  const validResults = [];\n  searchResults.forEach(({ url, title }) => {\n    const protocol = extractProtocol(url);\n    if (isURL(url) && isProtocol(protocol)) {\n      validResults.push({\n        type: 'bookmark',\n        score: -1,\n        title,\n        url\n      });\n    }\n  });\n\n  return validResults;\n}\n\nexport default async function bookmarkSuggestions(searchString) {\n  const { sakaSettings } = await browser.storage.sync.get(['sakaSettings']);\n\n  const enableFuzzySearch =\n    sakaSettings && sakaSettings.enableFuzzySearch !== undefined\n      ? sakaSettings.enableFuzzySearch\n      : true;\n\n  if (searchString && enableFuzzySearch) {\n    return getFilteredSuggestions(searchString, {\n      getSuggestions: allBookmarkSuggestions,\n      threshold: 1,\n      keys: ['title', 'url']\n    });\n  }\n\n  return allBookmarkSuggestions(searchString);\n}\n"
  },
  {
    "path": "src/suggestion_engine/server/providers/closedTab.js",
    "content": "import browser from 'webextension-polyfill';\nimport { isSakaUrl } from 'lib/url.js';\n// import { filter } from 'rxjs/operator/filter';\nimport { allTabSuggestions } from './tab.js';\nimport { getFilteredSuggestions } from 'lib/utils.js';\n\nexport async function getAllSuggestions() {\n  const sessions = await browser.sessions.getRecentlyClosed();\n  const filteredSessions = [];\n\n  // TODO: This for loop is currently flagged by the airbnb eslint rules.\n  // See: https://github.com/airbnb/javascript/issues/1271\n  // Not disabling the rule as this might be fixable in the future using filter.\n  // This for loop is needed at the moment as a workaround since filter does not support async.\n  for (const session of sessions) {\n    if (session.tab && !(await isSakaUrl(session.tab.url))) {\n      filteredSessions.push(session);\n    } else if (session.window && session.window.tabs) {\n      for (const tabSession of session.window.tabs) {\n        if (tabSession && !(await isSakaUrl(tabSession.url))) {\n          filteredSessions.push({\n            lastModified: session.window.lastModified,\n            tab: {\n              ...tabSession,\n              sessionId: session.window.sessionId\n            }\n          });\n        }\n      }\n    }\n  }\n\n  return filteredSessions.map(session => {\n    const { lastModified } = session;\n    const { id, sessionId, title, url, favIconUrl, incognito } = session.tab;\n    return {\n      type: 'closedTab',\n      tabId: id,\n      sessionId,\n      score: undefined,\n      title,\n      url,\n      favIconUrl: incognito ? null : favIconUrl,\n      incognito,\n      lastAccessed: lastModified\n    };\n  });\n}\n\n// TODO: Remove when Chrome gets proper timestamp\nexport async function recentlyClosedTabSuggestions() {\n  const { recentlyClosed } = await browser.runtime.getBackgroundPage();\n  const sessions = await browser.sessions.getRecentlyClosed();\n  const filteredSessions = [];\n\n  // TODO: This for loop is currently flagged by the airbnb eslint rules.\n  // See: https://github.com/airbnb/javascript/issues/1271\n  // Not disabling the rule as this might be fixable in the future using filter.\n  // This for loop is needed at the moment as a workaround since filter does not support async.\n  for (const session of sessions) {\n    if (session.tab && !(await isSakaUrl(session.tab.url))) {\n      filteredSessions.push(session);\n    }\n  }\n\n  return filteredSessions\n    .map(session => {\n      const foundTab = recentlyClosed.findIndex(tab => {\n        return tab.tabId === session.tab.id;\n      });\n\n      if (foundTab !== -1) {\n        return { ...session, lastModified: recentlyClosed.tab.lastAccessed };\n      }\n\n      return session;\n    })\n    .map(session => {\n      const { lastModified } = session;\n      const { id, sessionId, title, url, favIconUrl, incognito } = session.tab;\n      return {\n        type: 'closedTab',\n        tabId: id,\n        sessionId,\n        score: undefined,\n        title,\n        url,\n        favIconUrl: incognito ? null : favIconUrl,\n        incognito,\n        lastAccessed: lastModified\n      };\n    });\n}\n\nexport default async function closedTabSuggestions(searchString) {\n  return searchString === ''\n    ? getAllSuggestions()\n    : getFilteredSuggestions(searchString, {\n        getSuggestions: getAllSuggestions,\n        threshold: 0.5,\n        keys: ['title', 'url']\n      });\n}\n"
  },
  {
    "path": "src/suggestion_engine/server/providers/command.js",
    "content": "import { MAX_RESULTS } from './index.js';\n\nconst commands = ['search', 'help', 'history', 'tabs', 'define'];\n\nexport default function commandSuggestions(searchText) {\n  return commands\n    .filter(command => command.startsWith(searchText))\n    .slice(0, MAX_RESULTS)\n    .map(command => ({\n      type: 'command',\n      score: -1,\n      title: command\n    }));\n}\n"
  },
  {
    "path": "src/suggestion_engine/server/providers/history.js",
    "content": "import browser from 'webextension-polyfill';\nimport { isSakaUrl } from 'lib/url.js';\nimport { getFilteredSuggestions } from 'lib/utils.js';\n\nexport async function allHistorySuggestions(searchText) {\n  const results = await browser.history.search({\n    text: searchText\n  });\n\n  const filteredResults = [];\n\n  for (const result of results) {\n    const sakaUrl = await isSakaUrl(result.url);\n    !sakaUrl ? filteredResults.push(result) : null;\n  }\n\n  return filteredResults.map(\n    ({ url, title, lastVisitTime, visitCount, typedCount }) => ({\n      type: 'history',\n      score: visitCount + typedCount,\n      lastAccessed: lastVisitTime * 0.001,\n      title,\n      url\n    })\n  );\n}\n\nexport default async function historySuggestions(searchString) {\n  const { sakaSettings } = await browser.storage.sync.get(['sakaSettings']);\n  const enableFuzzySearch =\n    sakaSettings && sakaSettings.enableFuzzySearch !== undefined\n      ? sakaSettings.enableFuzzySearch\n      : true;\n\n  if (searchString && enableFuzzySearch) {\n    return getFilteredSuggestions(searchString, {\n      getSuggestions: allHistorySuggestions,\n      threshold: 1,\n      keys: ['title', 'url']\n    });\n  }\n\n  return allHistorySuggestions(searchString);\n}\n"
  },
  {
    "path": "src/suggestion_engine/server/providers/index.js",
    "content": "export { default as tab } from './tab.js';\nexport { default as closedTab } from './closedTab.js';\nexport { default as mode } from './mode.js';\n// export { default as commands } from './commands';\nexport { default as history } from './history.js';\nexport { default as bookmark } from './bookmark.js';\nexport { default as recentlyViewed } from './recentlyViewed.js';\n// export { default as searchEngine } from './searchEngine';\n"
  },
  {
    "path": "src/suggestion_engine/server/providers/mode.js",
    "content": "import { ctrlChar } from 'lib/utils.js';\nimport { colorMap, fadedColorMap } from 'lib/colors.js';\nimport Fuse from 'fuse.js';\n\nexport const suggestions = [\n  {\n    type: 'mode',\n    mode: 'tab',\n    label: 'Tabs',\n    shortcut: `${ctrlChar}-shift-a`,\n    color: colorMap.tab,\n    fadedColor: fadedColorMap.tab,\n    icon: 'tab'\n  },\n  {\n    type: 'mode',\n    mode: 'closedTab',\n    label: 'Recently Closed Tabs',\n    shortcut: `${ctrlChar}-shift-c`,\n    color: colorMap.closedTab,\n    fadedColor: fadedColorMap.closedTab,\n    icon: 'restore_page'\n  },\n  {\n    type: 'mode',\n    label: 'Bookmarks',\n    mode: 'bookmark',\n    shortcut: `${ctrlChar}-b`,\n    color: colorMap.bookmark,\n    fadedColor: fadedColorMap.bookmark,\n    icon: 'bookmark_border'\n  },\n  {\n    type: 'mode',\n    label: 'History',\n    mode: 'history',\n    shortcut: `${ctrlChar}-shift-e`,\n    color: colorMap.history,\n    fadedColor: fadedColorMap.history,\n    icon: 'history'\n  },\n  {\n    type: 'mode',\n    label: 'Recently Viewed',\n    mode: 'recentlyViewed',\n    shortcut: `${ctrlChar}-shift-x`,\n    color: colorMap.recentlyViewed,\n    fadedColor: fadedColorMap.recentlyViewed,\n    icon: 'timelapse'\n  }\n];\n\nconst fuse = new Fuse(suggestions, {\n  shouldSort: true,\n  threshold: 1.0,\n  includeMatches: true,\n  keys: ['label']\n});\n\nexport default async function modeSuggestions(searchString) {\n  return searchString === ''\n    ? suggestions\n    : fuse.search(searchString).map(({ item, matches, score }) => ({\n        ...item,\n        score,\n        matches\n      }));\n}\n"
  },
  {
    "path": "src/suggestion_engine/server/providers/recentlyViewed.js",
    "content": "import { getFilteredSuggestions } from 'lib/utils.js';\nimport tabSuggestions, {\n  allTabSuggestions,\n  recentVisitedTabSuggestions\n} from './tab.js';\nimport {\n  getAllSuggestions as getAllClosedTabs,\n  recentlyClosedTabSuggestions\n} from './closedTab.js';\nimport { allHistorySuggestions as getAllHistoryTabs } from './history.js';\n\nfunction compareRecentlyViewedSuggestions(suggestion1, suggestion2) {\n  return suggestion2.lastAccessed - suggestion1.lastAccessed;\n}\n\nasync function allRecentlyViewedSuggestions(searchString) {\n  const historyTabs = await getAllHistoryTabs(searchString);\n  let openTabs = null;\n  let closedTabs = null;\n\n  if (SAKA_PLATFORM === 'chrome') {\n    openTabs = await recentVisitedTabSuggestions(searchString);\n    closedTabs = await recentlyClosedTabSuggestions(searchString);\n  } else {\n    openTabs = await tabSuggestions(searchString);\n    closedTabs = await getAllClosedTabs(searchString);\n  }\n\n  const filteredClosedTabs = closedTabs.filter(tab =>\n    openTabs.every(openTab => openTab.url !== tab.url)\n  );\n\n  const filteredHistoryTabs = historyTabs.filter(tab =>\n    [...openTabs, ...filteredClosedTabs].every(\n      openOrClosedTab => openOrClosedTab.url !== tab.url\n    )\n  );\n\n  return [...openTabs, ...filteredClosedTabs, ...filteredHistoryTabs]\n    .map(tab => ({ ...tab, originalType: tab.type, type: 'recentlyViewed' }))\n    .sort(compareRecentlyViewedSuggestions);\n}\n\nasync function filteredRecentlyViewedSuggestions(searchString) {\n  const tabs = await allTabSuggestions();\n  const closedTabs = await getAllClosedTabs(searchString);\n  const historyTabs = await getAllHistoryTabs(searchString);\n\n  return [\n    ...tabs,\n    ...Object.values(closedTabs),\n    ...Object.values(historyTabs)\n  ].map(tab => ({ ...tab, originalType: tab.type, type: 'recentlyViewed' }));\n}\n\nasync function getFilteredRecentlyViewedSuggestions(searchString) {\n  const filteredSuggestions = await getFilteredSuggestions(searchString, {\n    getSuggestions: filteredRecentlyViewedSuggestions,\n    threshold: 0.5,\n    keys: ['title', 'url']\n  });\n\n  return filteredSuggestions.filter(\n    (suggestion, index) =>\n      filteredSuggestions.findIndex(\n        filteredSuggestion =>\n          filteredSuggestion.url === suggestion.url &&\n          filteredSuggestion.title === suggestion.title\n      ) === index\n  );\n}\n\nexport default async function recentlyViewedSuggestions(searchString) {\n  if (searchString === '') {\n    return allRecentlyViewedSuggestions(searchString, SAKA_PLATFORM);\n  }\n\n  return getFilteredRecentlyViewedSuggestions(searchString);\n}\n"
  },
  {
    "path": "src/suggestion_engine/server/providers/searchEngine.js",
    "content": "import { MAX_RESULTS } from './index.js';\n\nexport default async function searchEngineSuggestions(searchText) {\n  try {\n    const baseURL =\n      'https://www.google.com/complete/search?client=chrome-omni&q=';\n    const response = await fetch(`${baseURL}${encodeURIComponent(searchText)}`);\n    const json = await response.json();\n    return json[1].slice(0, MAX_RESULTS).map(result => ({\n      type: 'searchEngine',\n      score: -1,\n      title: result\n    }));\n  } catch (e) {\n    return [];\n  }\n}\n"
  },
  {
    "path": "src/suggestion_engine/server/providers/tab.js",
    "content": "import browser from 'webextension-polyfill';\nimport { getFilteredSuggestions, objectFromArray } from 'lib/utils.js';\n\nexport async function allTabSuggestions() {\n  const tabs = await browser.tabs.query({});\n  return tabs.map(\n    ({\n      id: tabId,\n      windowId,\n      title,\n      url,\n      favIconUrl,\n      incognito,\n      lastAccessed\n    }) => ({\n      type: 'tab',\n      tabId,\n      windowId,\n      title,\n      url,\n      favIconUrl: incognito ? null : favIconUrl,\n      incognito,\n      lastAccessed: lastAccessed * 0.001\n    })\n  );\n}\n\nasync function recentTabSuggestions() {\n  const tabs = await allTabSuggestions();\n  const tabsMap = objectFromArray(tabs, 'tabId');\n  const { tabHistory } = await browser.runtime.getBackgroundPage();\n\n  const recentTabs = tabHistory.map(recentlyUsedTab => {\n    const tab = tabsMap[recentlyUsedTab.tabId];\n    delete tabsMap[recentlyUsedTab.tabId];\n    return { ...tab, lastAccessed: recentlyUsedTab.lastAccessed };\n  });\n\n  return [...recentTabs, ...Object.values(tabsMap)];\n}\n\n// TODO: Remove this once chrome tab API provides recently viewed\nexport async function recentVisitedTabSuggestions() {\n  const tabs = await allTabSuggestions();\n  const tabsMap = objectFromArray(tabs, 'tabId');\n  const { tabHistory } = await browser.runtime.getBackgroundPage();\n\n  const recentTabs = tabHistory.map(recentlyUsedTab => {\n    const tab = tabsMap[recentlyUsedTab.tabId];\n    delete tabsMap[recentlyUsedTab.tabId];\n    return { ...tab, lastAccessed: recentlyUsedTab.lastAccessed * 0.001 };\n  });\n\n  return [...recentTabs];\n}\n\nexport default async function tabSuggestions(searchString) {\n  return searchString === ''\n    ? recentTabSuggestions()\n    : getFilteredSuggestions(searchString, {\n        getSuggestions: allTabSuggestions,\n        threshold: 0.5,\n        keys: ['title', 'url']\n      });\n}\n"
  },
  {
    "path": "src/suggestion_utils/index.js",
    "content": "import { prettifyURL, isURL } from 'lib/url.js';\n\nexport const icons = {\n  mode: 'apps',\n  tab: 'tab',\n  closedTab: 'restore_page',\n  history: 'history',\n  recentlyViewed: 'timelapse',\n  bookmark: 'bookmark_border',\n  incognito: 'visibility_off'\n};\n\nexport function preprocessSuggestion(suggestion, searchText) {\n  switch (suggestion.type) {\n    case 'tab': {\n      const prettyURL = prettifyURL(suggestion.url, searchText);\n      return {\n        ...suggestion,\n        prettyURL,\n        text: suggestion.title\n      };\n    }\n    case 'closedTab': {\n      const prettyURL = prettifyURL(suggestion.url, searchText);\n      return {\n        ...suggestion,\n        prettyURL,\n        text: suggestion.title\n      };\n    }\n    case 'mode':\n      return suggestion;\n    case 'bookmark': {\n      const prettyURL = prettifyURL(suggestion.url, searchText);\n      return {\n        ...suggestion,\n        prettyURL,\n        text: prettyURL\n      };\n    }\n    case 'history': {\n      const prettyURL = prettifyURL(suggestion.url, searchText);\n      return {\n        ...suggestion,\n        prettyURL,\n        text: prettyURL\n      };\n    }\n    case 'recentlyViewed': {\n      const prettyURL = prettifyURL(suggestion.url, searchText);\n      return {\n        ...suggestion,\n        prettyURL,\n        text: prettyURL\n      };\n    }\n    default:\n      return {\n        type: 'error',\n        title: `Error. Unknown Suggestion type: ${suggestion.type}`,\n        text: `Error. Unknown Suggestion type: ${suggestion.type}`\n      };\n  }\n}\n"
  },
  {
    "path": "static/background_page.html",
    "content": "<!doctype html>\n<html>\n    <meta charset=\"UTF-8\">\n<body>\n    <script src=\"vendor.js\"></script>\n    <script src=\"background_page.js\"></script>\n</body>\n\n</html>"
  },
  {
    "path": "static/material-icons.css",
    "content": "@font-face {\n  font-family: 'Material Icons';\n  font-style: normal;\n  font-weight: 400;\n  src: local('Material Icons'),\n    local('MaterialIcons-Regular'),\n    url(./MaterialIcons-Regular.woff2) format('woff2');\n}\n\n.material-icons {\n  font-family: 'Material Icons';\n  font-weight: normal;\n  font-style: normal;\n  font-size: 24px;  /* Preferred icon size */\n  display: inline-block;\n  line-height: 1;\n  text-transform: none;\n  letter-spacing: normal;\n  word-wrap: normal;\n  white-space: nowrap;\n  direction: ltr;\n\n  /* Support for all WebKit browsers. */\n  -webkit-font-smoothing: antialiased;\n  /* Support for Safari and Chrome. */\n  text-rendering: optimizeLegibility;\n\n  /* Support for Firefox. */\n  -moz-osx-font-smoothing: grayscale;\n\n  /* Support for IE. */\n  font-feature-settings: 'liga';\n}"
  },
  {
    "path": "static/options.html",
    "content": "<!doctype html>\n<html>\n\n<head>\n    <title>Saka Options</title>\n    <meta charset=\"UTF-8\">\n    <link rel='stylesheet' type='text/css' href='material-icons.css'>\n</head>\n\n<body>\n    <script src=\"vendor.js\"></script>\n    <script src=\"saka-options.js\"></script>\n</body>\n\n</html>"
  },
  {
    "path": "static/saka.html",
    "content": "<!doctype html>\n<html>\n\n<head>\n  <title>Saka</title>\n  <meta charset=\"UTF-8\">\n  <link rel='stylesheet' type='text/css' href='material-icons.css'>\n</head>\n\n<body>\n  <script src=\"vendor.js\"></script>\n  <script src=\"saka.js\"></script>\n</body>\n\n</html>"
  },
  {
    "path": "test/Icon.test.js",
    "content": "import Icon from '../src/saka/Main/Components/Icon/index.jsx';\nimport { render } from 'preact-render-spy';\nimport { h } from 'preact';\n\ndescribe('Icon component ', () => {\n  it('should render while enabled', () => {\n    const props = {\n      icon: '',\n      color: 'rgba(1,1,1,0.44)'\n    };\n\n    const iconRender = render(<Icon {...props} />);\n    const icon = iconRender.find('#icon');\n\n    expect(icon).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "test/Main.test.js",
    "content": "import { h } from 'preact';\nimport {\n  render,\n  cleanup,\n  wait,\n  fireEvent,\n  flushPromises\n} from 'preact-testing-library';\nimport 'jest-dom/extend-expect';\nimport Main from '@/saka/Main/index.jsx';\n\nbeforeEach(() => {\n  browser.flush();\n  // browser.storage.sync.get.returns({\n  //   sakaSettings: {\n  //     mode: 'tab',\n  //     showEmptySearchSuggestions: false\n  //   },\n  //   searchHistory: []\n  // });\n  browser.storage.local.get.resolves(\n    Promise.resolve({\n      screenshot:\n        'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB4AAAAPWCAYAAAABOoU/AAAgAElEQVR4nOzdeXzbd2H/8e9XcqDOXdrC2OAH/W1l49rooMAgvQsbg0F}'\n    })\n  );\n  browser.storage.local.remove.returns('');\n  browser.runtime.sendMessage.returns('');\n});\n\ntest('should show all options when not showing key bindings', async () => {\n  render(<Main />);\n});\n\nafterEach(cleanup);\n"
  },
  {
    "path": "test/ModeSwitcher.test.js",
    "content": "import ModeSwitcher from '@/saka/Main/Components/ModeSwitcher/index.jsx';\nimport {\n  render,\n  cleanup,\n  fireEvent,\n  flushPromises\n} from 'preact-testing-library';\nimport { fadedColorMap } from 'lib/colors.js';\nimport { h } from 'preact';\n\ndescribe('ModeSwitcher component ', () => {\n  it('should render tabs with selected tab colored, rest of tabs gray', async () => {\n    const setMode = jest.fn();\n\n    const props = {\n      mode: 'tab',\n      setMode\n    };\n\n    const { getByText } = render(<ModeSwitcher {...props} />);\n\n    expect(getByText('tab')).toMatchSnapshot();\n    expect(getByText('restore_page')).toMatchSnapshot();\n    expect(getByText('bookmark_border')).toMatchSnapshot();\n    expect(getByText('history')).toMatchSnapshot();\n    expect(getByText('timelapse')).toMatchSnapshot();\n\n    fireEvent.click(getByText('restore_page'), 'click');\n    await flushPromises();\n    expect(setMode.mock.calls.length).toBe(1);\n  });\n});\n\nafterEach(cleanup);\n"
  },
  {
    "path": "test/PaginationBar.test.js",
    "content": "import PaginationBar from '@/saka/Main/Components/PaginationBar/index.jsx';\nimport { render, getByText } from 'preact-testing-library';\nimport { h } from 'preact';\n\ndescribe('PaginationBar component ', () => {\n  it('should be empty when no there are no suggestions', () => {\n    const props = {\n      firstVisibleIndex: 0,\n      suggestions: [],\n      maxSuggestions: 6,\n      onClickPrevious() {},\n      onClickNext() {}\n    };\n\n    const { queryByText } = render(<PaginationBar {...props} />);\n    expect(queryByText('◄')).toBeNull();\n    expect(queryByText('ctrl-S')).toBeNull();\n    expect(queryByText('ctrl-D ►')).toBeNull();\n  });\n\n  it('should show correct amount of suggestions when there are suggestions found', () => {\n    const props = {\n      firstVisibleIndex: 0,\n      suggestions: [\n        {\n          type: 'tab',\n          title: 'lusakasa/saka: Elegant tab search',\n          url: 'https://github.com/lusakasa/saka'\n        },\n        {\n          type: 'tab',\n          title: 'Google',\n          url: 'https://google.com'\n        }\n      ],\n      maxSuggestions: 6,\n      onClickPrevious() {},\n      onClickNext() {}\n    };\n\n    const { getByText } = render(<PaginationBar {...props} />);\n    getByText('◄');\n    getByText('ctrl-S');\n    getByText('1 - 2 / 2');\n    getByText('ctrl-D ►');\n  });\n});\n"
  },
  {
    "path": "test/SearchBar.test.js",
    "content": "import { h } from 'preact';\nimport { render, cleanup, flushPromises } from 'preact-testing-library';\nimport SearchBar from '@/saka/Main/Components/SearchBar/index';\n\nafterEach(cleanup);\n\ntest('should be empty when no there is no search string provided', async () => {\n  const props = {\n    placeholder: 'Tabs',\n    searchString: '',\n    suggestion: {},\n    mode: 'tab',\n    onKeyDown() {},\n    onInput() {},\n    onBlur() {},\n    onButtonClick() {}\n  };\n\n  const { container } = render(<SearchBar {...props} />);\n  expect(container).toMatchSnapshot();\n});\n\ntest('should show the search string when search string is provided', async () => {\n  const props = {\n    placeholder: 'Tabs',\n    searchString: 'Saka github',\n    suggestion: {},\n    mode: 'tab',\n    onKeyDown() {},\n    onInput() {},\n    onBlur() {},\n    onButtonClick() {}\n  };\n\n  const { getByPlaceholderText } = render(<SearchBar {...props} />);\n\n  expect(getByPlaceholderText('Tabs').value).toBe('Saka github');\n});\n"
  },
  {
    "path": "test/StandardSearch/StandardSearch.test.js",
    "content": "import { h } from 'preact';\nimport {\n  render,\n  cleanup,\n  flushPromises,\n  fireEvent,\n  wait\n} from 'preact-testing-library';\n\nimport StandardSearch from '@/saka/Main/Containers/StandardSearch/index.jsx';\n\nbeforeEach(() => {\n  browser.flush();\n  browser.storage.local.get.resolves(\n    Promise.resolve({\n      screenshot:\n        'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB4AAAAPWCAYAAAABOoU/AAAgAElEQVR4nOzdeXzbd2H/8e9XcqDOXdrC2OAH/W1l49rooMAgvQsbg0F}'\n    })\n  );\n  browser.storage.local.remove.returns('');\n  browser.runtime.sendMessage.returns('');\n});\n\ntest('should not show suggestion list when showEmptySearchSuggestions false and no search string', () => {\n  const props = {\n    placeholder: 'Tabs',\n    mode: 'tab',\n    showEmptySearchSuggestions: false,\n    searchHistory: ['first', 'second', 'third'],\n    updateSearchHistory: jest.fn()\n  };\n\n  const { getByPlaceholderText } = render(<StandardSearch {...props} />);\n  expect(getByPlaceholderText('Tabs').value).toBe('');\n});\n\ntest('should render and allow user input to search for suggestion', () => {\n  const props = {\n    placeholder: 'Tabs',\n    mode: 'tab',\n    showEmptySearchSuggestions: true,\n    searchHistory: [],\n    updateSearchHistory: jest.fn(),\n    shuffleMode: jest.fn()\n  };\n\n  const { getByPlaceholderText } = render(<StandardSearch {...props} />);\n\n  getByPlaceholderText('Tabs').value = 'Test input';\n  fireEvent.input(getByPlaceholderText('Tabs'));\n  expect(getByPlaceholderText('Tabs').value).toBe('Test input');\n\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: ' ',\n    shiftKey: true\n  });\n  expect(props.shuffleMode.mock.calls.length).toBe(1);\n\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'k',\n    keyCode: 75,\n    which: 75,\n    ctrlKey: true\n  });\n});\n\ntest('should allow keyboard navigation of Saka search results', () => {\n  const props = {\n    placeholder: 'Tabs',\n    mode: 'tab',\n    showEmptySearchSuggestions: true,\n    searchHistory: [],\n    updateSearchHistory: jest.fn()\n  };\n\n  const { getByPlaceholderText } = render(<StandardSearch {...props} />);\n\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'Tab'\n  });\n\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'Tab',\n    keyCode: 9,\n    which: 9,\n    shiftKey: true\n  });\n\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'd',\n    keyCode: 68,\n    which: 68,\n    shiftKey: true,\n    ctrlKey: true\n  });\n\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 's',\n    keyCode: 83,\n    which: 83,\n    shiftKey: true,\n    ctrlKey: true\n  });\n});\n\ntest('should allow going back and forward through search history', () => {\n  const props = {\n    placeholder: 'Tabs',\n    mode: 'tab',\n    showEmptySearchSuggestions: true,\n    searchHistory: ['first', 'second', 'third'],\n    updateSearchHistory: jest.fn()\n  };\n\n  const { getByPlaceholderText } = render(<StandardSearch {...props} />);\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'z',\n    keyCode: 90,\n    which: 90,\n    ctrlKey: true\n  });\n\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'y',\n    keyCode: 89,\n    which: 89,\n    ctrlKey: true\n  });\n});\n\ntest('should allow switching between search modes', async () => {\n  const props = {\n    placeholder: 'Tabs',\n    mode: 'tab',\n    showEmptySearchSuggestions: true,\n    searchHistory: ['first', 'second', 'third'],\n    updateSearchHistory: jest.fn(),\n    shuffleMode: jest.fn(),\n    setMode: jest.fn()\n  };\n\n  const { getByPlaceholderText } = render(<StandardSearch {...props} />);\n\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: ' '\n  });\n  expect(props.shuffleMode.mock.calls.length).toBe(1);\n\n  // Recently Closed\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'C',\n    ctrlKey: true\n  });\n\n  // Tabs\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'A',\n    ctrlKey: true\n  });\n\n  // Bookmarks\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'b',\n    ctrlKey: true\n  });\n\n  // History\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'E',\n    ctrlKey: true\n  });\n\n  // Recently Viewed\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'X',\n    ctrlKey: true\n  });\n\n  // Modes\n  // TODO: Deprecate this feature\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'M',\n    ctrlKey: true\n  });\n\n  expect(props.setMode.mock.calls.length).toBe(6);\n});\n\ntest('should close saka on Enter key', () => {\n  const props = {\n    placeholder: 'Tabs',\n    mode: 'tab',\n    showEmptySearchSuggestions: true,\n    searchHistory: [],\n    updateSearchHistory: jest.fn(),\n    shuffleMode: jest.fn()\n  };\n\n  const { getByPlaceholderText } = render(<StandardSearch {...props} />);\n\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'Enter'\n  });\n\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'Backspace'\n  });\n\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'Escape'\n  });\n});\n\ntest('should allow navigation via arrow keys', () => {\n  const props = {\n    placeholder: 'Tabs',\n    mode: 'tab',\n    showEmptySearchSuggestions: true,\n    searchHistory: [],\n    updateSearchHistory: jest.fn(),\n    shuffleMode: jest.fn()\n  };\n\n  const { getByPlaceholderText } = render(<StandardSearch {...props} />);\n\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'ArrowLeft'\n  });\n\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'ArrowRight'\n  });\n\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'ArrowDown'\n  });\n\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'ArrowUp'\n  });\n});\n\ntest('should select suggestion based on key press', () => {\n  const props = {\n    placeholder: 'Tabs',\n    mode: 'tab',\n    showEmptySearchSuggestions: true,\n    searchHistory: [],\n    updateSearchHistory: jest.fn(),\n    shuffleMode: jest.fn()\n  };\n\n  const { getByPlaceholderText } = render(<StandardSearch {...props} />);\n\n  [1, 2, 3, 4, 5, 6].map(num => {\n    fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n      key: `${num}`,\n      ctrlKey: true\n    });\n  });\n});\n\ntest('should close tab and delete suggestion when key pressed', async () => {\n  browser.tabs.query.returns([\n    {\n      title: 'adjksdhk',\n      url: 'adasd',\n      incgnito: false\n    },\n    { title: 'adjksdhk', url: 'adasd', incgnito: false }\n  ]);\n  const props = {\n    placeholder: 'Tabs',\n    mode: 'tab',\n    showEmptySearchSuggestions: true,\n    searchHistory: [],\n    updateSearchHistory: jest.fn(),\n    shuffleMode: jest.fn()\n  };\n\n  const { getByPlaceholderText } = render(<StandardSearch {...props} />);\n\n  await flushPromises();\n\n  fireEvent.keyDown(getByPlaceholderText('Tabs'), {\n    key: 'Backspace',\n    ctrlKey: true\n  });\n});\n\nafterEach(cleanup);\n"
  },
  {
    "path": "test/Suggestion.test.js",
    "content": "import { h } from 'preact';\nimport {\n  render,\n  cleanup,\n  fireEvent,\n  flushPromises\n} from 'preact-testing-library';\nimport Suggestion from '@/saka/Main/Components/SuggestionList/Components/Suggestion';\n\ntest('should render when props passed in', async () => {\n  global.SAKA_PLATFORM = 'chrome';\n\n  const props = {\n    type: 'tab',\n    title: 'Test Title',\n    titleColor: 'fafafa',\n    secondary: 'Secondary Test Title',\n    secondaryColor: 'ffffff',\n    url: 'https://example.com',\n    favIconUrl: 'localhost:1234/path/to/icon',\n    incognito: false,\n    selected: 'false',\n    index: 0,\n    onClick: () => {}\n  };\n\n  const { getByText } = render(<Suggestion {...props} />);\n\n  getByText('Test Title');\n  getByText('Secondary Test Title');\n  getByText('tab');\n});\n\ntest('should call onClick when onClick or onKeyPress event', async () => {\n  global.SAKA_PLATFORM = 'chrome';\n\n  const onClick = jest.fn();\n  const props = {\n    type: 'tab',\n    title: 'Test Title',\n    titleColor: 'fafafa',\n    secondary: 'Secondary Test Title',\n    secondaryColor: 'ffffff',\n    url: 'https://example.com',\n    favIconUrl: 'localhost:1234/path/to/icon',\n    incognito: false,\n    selected: 'false',\n    index: 0,\n    onClick\n  };\n\n  const { getByText } = render(<Suggestion {...props} />);\n\n  fireEvent.click(getByText('Test Title'));\n  fireEvent.keyPress(getByText('Test Title'));\n  await flushPromises();\n  expect(onClick.mock.calls.length).toBe(2);\n});\n\ntest('should hide icon when suggestion is from incognito', () => {\n  global.SAKA_PLATFORM = 'chrome';\n\n  const onClick = jest.fn();\n  const props = {\n    type: 'tab',\n    title: 'Test Title',\n    titleColor: 'fafafa',\n    secondary: 'Secondary Test Title',\n    secondaryColor: 'ffffff',\n    url: 'https://example.com',\n    favIconUrl: 'localhost:1234/path/to/icon',\n    incognito: true,\n    selected: 'false',\n    index: 0,\n    onClick\n  };\n\n  const { getByText } = render(<Suggestion {...props} />);\n});\n\ntest('should use correct favicon path when using firefox', () => {\n  global.SAKA_PLATFORM = 'firefox';\n\n  const onClick = jest.fn();\n  const props = {\n    type: 'tab',\n    title: 'Test Title',\n    titleColor: 'fafafa',\n    secondary: 'Secondary Test Title',\n    secondaryColor: 'ffffff',\n    url: 'https://example.com',\n    favIconUrl: 'localhost:1234/path/to/icon',\n    incognito: false,\n    selected: 'false',\n    index: 0,\n    onClick\n  };\n\n  const { getByText } = render(<Suggestion {...props} />);\n  expect(getByText('tab')).toMatchSnapshot();\n});\n\ntest('should use default favicon when no url to favicon', () => {\n  global.SAKA_PLATFORM = 'firefox';\n\n  const onClick = jest.fn();\n  const props = {\n    type: 'tab',\n    title: 'Test Title',\n    titleColor: 'fafafa',\n    secondary: 'Secondary Test Title',\n    secondaryColor: 'ffffff',\n    incognito: false,\n    selected: 'false',\n    index: 0,\n    onClick\n  };\n\n  const { getByText } = render(<Suggestion {...props} />);\n  expect(getByText('tab')).toMatchSnapshot();\n});\nafterEach(cleanup);\n"
  },
  {
    "path": "test/SuggestionList.test.js",
    "content": "import SuggestionList from '@/saka/Main/Components/SuggestionList/index.jsx';\nimport { render } from 'preact-testing-library';\nimport { h } from 'preact';\n\nconst MAX_SUGGESTIONS = 6;\n\nbeforeEach(() => {\n  // Clears the database and adds some testing data.\n  // Jest will wait for this promise to resolve before running tests.\n  global.SAKA_PLATFORM = 'chrome';\n});\n\ndescribe('SuggestionList component ', () => {\n  it('should be empty when no values provided', () => {\n    const suggestions = [];\n\n    const searchString = {};\n\n    const { container } = render(\n      <SuggestionList\n        searchString={searchString}\n        suggestions={suggestions}\n        selectedIndex={0}\n        firstVisibleIndex={0}\n        maxSuggestions={MAX_SUGGESTIONS}\n      />\n    );\n\n    expect(container).toMatchSnapshot();\n  });\n\n  it('should display suggestions when provided', () => {\n    const suggestions = [\n      {\n        type: 'tab',\n        title: 'lusakasa/saka: Elegant tab search',\n        url: 'https://github.com/lusakasa/saka'\n      },\n      {\n        type: 'tab',\n        title: 'Google',\n        url: 'https://google.com'\n      }\n    ];\n\n    const searchString = '';\n\n    const { getByText } = render(\n      <SuggestionList\n        searchString={searchString}\n        suggestions={suggestions}\n        selectedIndex={0}\n        firstVisibleIndex={0}\n        maxSuggestions={MAX_SUGGESTIONS}\n        onSuggestionClick={() => {}}\n      />\n    );\n\n    suggestions.map(suggestion => {\n      getByText(suggestion.title);\n      getByText(suggestion.url);\n    });\n  });\n});\n"
  },
  {
    "path": "test/__mocks__/browser-mocks.js",
    "content": "const chrome = require('sinon-chrome/extensions');\nconst browser = require('sinon-chrome/webextensions');\nglobal.chrome = chrome;\nglobal.browser = browser;\n\njest.mock('msgx/client.js', () =>\n  jest.fn().mockImplementation(() => {\n    return jest.fn().mockImplementation(mode => {\n      let api = {\n        zoom: Promise.resolve(1),\n        sg: []\n      };\n\n      return api[mode];\n    });\n  })\n);\n\n// ({ default: jest.fn() })\n"
  },
  {
    "path": "test/__mocks__/styleMock.scss",
    "content": ""
  },
  {
    "path": "test/__snapshots__/ModeSwitcher.test.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`ModeSwitcher component  should render tabs with selected tab colored, rest of tabs gray 1`] = `\n<i\n  aria-hidden=\"true\"\n  class=\"material-icons\"\n  id=\"icon\"\n  style=\"color: rgba(55, 126, 184, 0.44);\"\n>\n  tab\n</i>\n`;\n\nexports[`ModeSwitcher component  should render tabs with selected tab colored, rest of tabs gray 2`] = `\n<i\n  aria-hidden=\"true\"\n  class=\"material-icons\"\n  id=\"icon\"\n  style=\"color: rgba(153, 153, 153, 0.44);\"\n>\n  restore_page\n</i>\n`;\n\nexports[`ModeSwitcher component  should render tabs with selected tab colored, rest of tabs gray 3`] = `\n<i\n  aria-hidden=\"true\"\n  class=\"material-icons\"\n  id=\"icon\"\n  style=\"color: rgba(153, 153, 153, 0.44);\"\n>\n  bookmark_border\n</i>\n`;\n\nexports[`ModeSwitcher component  should render tabs with selected tab colored, rest of tabs gray 4`] = `\n<i\n  aria-hidden=\"true\"\n  class=\"material-icons\"\n  id=\"icon\"\n  style=\"color: rgba(153, 153, 153, 0.44);\"\n>\n  history\n</i>\n`;\n\nexports[`ModeSwitcher component  should render tabs with selected tab colored, rest of tabs gray 5`] = `\n<i\n  aria-hidden=\"true\"\n  class=\"material-icons\"\n  id=\"icon\"\n  style=\"color: rgba(153, 153, 153, 0.44);\"\n>\n  timelapse\n</i>\n`;\n"
  },
  {
    "path": "test/__snapshots__/SearchBar.test.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`should be empty when no there is no search string provided 1`] = `\n<div>\n  <form\n    class=\"search-bar-container\"\n  >\n    <section\n      class=\"mdc-text-field mdc-text-field--fullwidth search-field-wrapper\"\n    >\n      <input\n        aria-label=\"Tabs\"\n        class=\"mdc-text-field__input search-field-input\"\n        id=\"search-bar\"\n        placeholder=\"Tabs\"\n        type=\"text\"\n      />\n    </section>\n  </form>\n</div>\n`;\n"
  },
  {
    "path": "test/__snapshots__/Suggestion.test.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`should use correct favicon path when using firefox 1`] = `\n<i\n  aria-hidden=\"true\"\n  class=\"material-icons\"\n  style=\"color: rgba(55, 126, 184, 0.44);\"\n>\n  tab\n</i>\n`;\n\nexports[`should use default favicon when no url to favicon 1`] = `\n<i\n  aria-hidden=\"true\"\n  class=\"material-icons\"\n  style=\"color: rgba(55, 126, 184, 0.44);\"\n>\n  tab\n</i>\n`;\n"
  },
  {
    "path": "test/__snapshots__/SuggestionList.test.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`SuggestionList component  should be empty when no values provided 1`] = `\n<div>\n  <ul\n    class=\"mdc-list mdc-list--two-line mdc-list--avatar-list two-line-avatar-text-icon-demo list-container\"\n  />\n</div>\n`;\n"
  },
  {
    "path": "test/lib/hightlight.test.js",
    "content": "import highlight from 'lib/highlight';\n\ntest('should return source text when matches is undefined', async () => {\n  const text = 'http://www.example.com';\n  const key = '';\n  const matches = undefined;\n  const result = highlight(text, key, matches);\n\n  expect(result).toBe(text);\n});\n\ntest('should return source text when matches is defined but empty', async () => {\n  const text = 'http://www.example.com';\n  const key = '';\n  const matches = [];\n  const result = highlight(text, key, matches);\n\n  expect(result).toBe(text);\n});\n\ntest('should return highlighted text when matches is defined and not empty', async () => {\n  const text = 'http://www.example.com';\n  const key = 'title';\n  const matches = [\n    {\n      indices: [[13, 16]],\n      value: 'example',\n      key: 'title',\n      arrayIndex: 0\n    }\n  ];\n\n  const result = highlight(text, key, matches);\n\n  expect(result).toEqual([\n    'http://www.ex',\n    {\n      attributes: { style: { 'font-weight': 'bold' } },\n      children: ['ampl'],\n      key: undefined,\n      nodeName: 'span'\n    },\n    'e.com'\n  ]);\n});\n"
  },
  {
    "path": "test/lib/log.test.js",
    "content": "import log from 'lib/log';\n\ntest('should not log when debug mode is disabled', async () => {\n  global.SAKA_DEBUG = false;\n  global.console = { log: jest.fn() };\n  const result = log(1, 2, 3, 4);\n\n  expect(result).toBe(1);\n  expect(global.console.log.mock.calls.length).toBe(0);\n});\n\ntest('should log when debug mode is enabled', async () => {\n  global.SAKA_DEBUG = true;\n  global.console = { log: jest.fn() };\n  const result = log(1, 2, 3, 4);\n\n  expect(result).toBe(1);\n  expect(global.console.log.mock.calls.length).toBe(1);\n});\n"
  },
  {
    "path": "test/lib/url.test.js",
    "content": "const browser = require('sinon-chrome/webextensions');\n\nimport * as libUrl from 'lib/url';\n\ndescribe('lib/url ', () => {\n  beforeAll(() => {\n    global.browser = browser;\n  });\n\n  beforeEach(() => {\n    browser.flush();\n  });\n\n  describe('isURL ', () => {\n    it('should return false when URL is empty', () => {\n      expect(libUrl.isURL('')).toBe(false);\n      expect(libUrl.isURL(undefined)).toBe(false);\n      expect(libUrl.isURL(null)).toBe(false);\n    });\n\n    it('should return false when URL is invalid', () => {\n      const url = 'http:';\n\n      expect(libUrl.isURL(url)).toBe(false);\n    });\n\n    it('should return true when URL is valid with standard protocol', () => {\n      expect(libUrl.isURL('https://github.com/lusakasa/saka')).toBe(true);\n      expect(libUrl.isURL('ftp://localhost')).toBe(true);\n      expect(libUrl.isURL('file://home/testing')).toBe(true);\n    });\n\n    it('should return true when URL is valid with custom protocol', () => {\n      expect(libUrl.isURL('about:blank')).toBe(true);\n      expect(libUrl.isURL('chrome:addons')).toBe(true);\n    });\n  });\n\n  describe('extractProtocol ', () => {\n    it('should empty string when no protocol provided', () => {\n      expect(libUrl.extractProtocol('this is a string with no protocol')).toBe(\n        ''\n      );\n    });\n\n    it('should return protocol when provided in a normal URL', () => {\n      expect(libUrl.extractProtocol('https://github.com/lusakasa/saka')).toBe(\n        'https:'\n      );\n    });\n\n    it('should return protocol when provided URL with port', () => {\n      expect(libUrl.extractProtocol('https://localhost:1234')).toBe('https:');\n    });\n  });\n\n  describe('stripProtocol ', () => {\n    it('should strip nothing when no protocol in string', () => {\n      expect(libUrl.stripProtocol('string with no url')).toBe(\n        'string with no url'\n      );\n    });\n\n    it('should strip protocol when given a valid url', () => {\n      expect(libUrl.stripProtocol('https://github.com/lusakasa/saka')).toBe(\n        'github.com/lusakasa/saka'\n      );\n    });\n\n    it('should strip protocol when given a valid url with ports', () => {\n      expect(libUrl.stripProtocol('https://localhost:12345')).toBe(\n        'localhost:12345'\n      );\n    });\n  });\n\n  describe('stripWWW ', () => {\n    it('should strip nothing when no www in string', () => {\n      expect(libUrl.stripWWW('https://github.com/lusakasa/saka')).toBe(\n        'https://github.com/lusakasa/saka'\n      );\n    });\n\n    it('should strip www when provided url with www', () => {\n      expect(libUrl.stripWWW('www.github.com/lusakasa/saka')).toBe(\n        'github.com/lusakasa/saka'\n      );\n    });\n\n    it('should not strip www when provided url with protocol', () => {\n      expect(libUrl.stripWWW('https://www.github.com/lusakasa/saka')).toBe(\n        'https://www.github.com/lusakasa/saka'\n      );\n    });\n  });\n\n  describe('startsWithProtocol ', () => {\n    it('should return false when does not start with a protocol', () => {\n      expect(libUrl.startsWithProtocol('github.com/lusakasa/saka')).toBe(false);\n    });\n\n    it('should return false when does not start with a protocol and has a port', () => {\n      expect(libUrl.startsWithProtocol('localhost:12345')).toBe(false);\n    });\n\n    it('should return true when does start with a protocol', () => {\n      expect(\n        libUrl.startsWithProtocol('https://github.com/lusakasa/saka')\n      ).toBe(true);\n    });\n\n    it('should return true when does start with a protocol and has a port', () => {\n      expect(libUrl.startsWithProtocol('https://localhost:12345')).toBe(true);\n    });\n  });\n\n  describe('startsWithWWW ', () => {\n    it('should return false when does not start with a www', () => {\n      expect(libUrl.startsWithWWW('github.com/lusakasa/saka')).toBe(false);\n    });\n\n    it('should return false when does not start with a www and has a port', () => {\n      expect(libUrl.startsWithWWW('myserver:12345')).toBe(false);\n    });\n\n    it('should return true when does start with a www', () => {\n      expect(libUrl.startsWithWWW('www.github.com/lusakasa/saka')).toBe(true);\n    });\n\n    it('should return true when does start with a www and has a port', () => {\n      expect(libUrl.startsWithWWW('www.myserver:12345')).toBe(true);\n    });\n  });\n\n  describe('isTLD ', () => {\n    it('should return false when not given a valid tld', () => {\n      expect(libUrl.isTLD('faketld.test')).toBe(false);\n    });\n\n    it('should return true when given a valid tld', () => {\n      expect(libUrl.isTLD('com')).toBe(true);\n    });\n  });\n\n  describe('isProtocol ', () => {\n    it('should return false when not given a valid protocol', () => {\n      expect(libUrl.isProtocol('fakeprotocol:')).toBe(false);\n    });\n\n    it('should return true when given a valid protocol', () => {\n      expect(libUrl.isProtocol('https:')).toBe(true);\n    });\n  });\n\n  describe('isLikeURL ', () => {\n    it('should return false when not given a url like string', () => {\n      expect(libUrl.isLikeURL('nonurlstring')).toBe(false);\n    });\n\n    it('should return true when given a url like string', () => {\n      expect(libUrl.isLikeURL('https://github.com/lusakasa/saka')).toBe(true);\n    });\n\n    it('should return true when given an ip address', () => {\n      expect(libUrl.isLikeURL('127.0.0.1')).toBe(true);\n    });\n  });\n\n  describe('prettifyURL ', () => {\n    it('should return empty string when input empty string', () => {\n      expect(libUrl.prettifyURL('', '')).toBe('');\n    });\n\n    it('should return prettified string when input prettified url', () => {\n      expect(libUrl.prettifyURL('github.com/lusakasa/saka', '')).toBe(\n        'github.com/lusakasa/saka'\n      );\n    });\n\n    it('should return prettified string when input url', () => {\n      expect(libUrl.prettifyURL('http://github.com/lusakasa/saka/', '')).toBe(\n        'github.com/lusakasa/saka'\n      );\n    });\n  });\n\n  describe('isSakaUrl ', () => {\n    it('should return false when URL is empty', async () => {\n      const sakaId = 'abcdefg/saka.html';\n      browser.runtime.getURL.returns(sakaId);\n\n      expect(await libUrl.isSakaUrl()).toBe(false);\n    });\n\n    it('should return false when URL does not contain saka ID', async () => {\n      const sakaId = 'abcdefg/saka.html';\n      browser.runtime.getURL.returns(sakaId);\n\n      expect(await libUrl.isSakaUrl('https://github.com/lusakasa')).toBe(false);\n    });\n\n    it('should return true when URL contains saka ID', async () => {\n      const sakaId = 'abcdefg/saka.html';\n      browser.runtime.getURL.returns(sakaId);\n\n      expect(await libUrl.isSakaUrl('http://abcdefg/saka.html')).toBe(true);\n    });\n  });\n\n  afterAll(() => {\n    browser.flush();\n    delete global.browser;\n  });\n});\n"
  },
  {
    "path": "test/lib/utils.test.js",
    "content": "import * as libUtil from 'lib/utils';\n\ndescribe('lib/util ', () => {\n  describe('objectFromArray ', () => {\n    it('should return empty object when empty list passed in', () => {\n      expect(libUtil.objectFromArray([], 1)).toEqual({});\n    });\n\n    it('should return corresponding object when empty list passed in', () => {\n      expect(\n        libUtil.objectFromArray([{ hello: 'world', index: 0 }], 'index')\n      ).toEqual({ 0: { hello: 'world', index: 0 } });\n    });\n\n    it('should return object with undefined key when target key not found', () => {\n      expect(\n        libUtil.objectFromArray([{ hello: 'world', index: 0 }], 'randomKey')\n      ).toEqual({ undefined: { hello: 'world', index: 0 } });\n    });\n  });\n\n  describe('rangedIncrement ', () => {\n    it('should return min when sum(value, increment) < min', () => {\n      expect(libUtil.rangedIncrement(1, 1, 3, 4)).toEqual(3);\n    });\n\n    it('should return sum(value, increment) when min < sum(value, increment) < max', () => {\n      expect(libUtil.rangedIncrement(2, 2, 2, 5)).toEqual(4);\n    });\n\n    it('should return max when max < sum(value, increment)', () => {\n      expect(libUtil.rangedIncrement(10, 10, 3, 8)).toEqual(8);\n    });\n  });\n\n  describe('ctrlKey ', () => {\n    it('should return metaKey when is mac', () => {\n      const isMac = true;\n      const kbEvent = new KeyboardEvent('ctrl');\n      expect(libUtil.ctrlKey(kbEvent)).toEqual(kbEvent.metaKey);\n    });\n\n    it('should return ctrlKey when is not mac', () => {\n      const isMac = false;\n      const kbEvent = new KeyboardEvent('ctrl');\n      expect(libUtil.ctrlKey(kbEvent)).toEqual(kbEvent.ctrlKey);\n    });\n  });\n\n  describe('getFilteredSuggestions ', () => {\n    it('should only return valid matches to search string', async () => {\n      const searchString = 'hello';\n      const getSuggestions = function() {\n        return [\n          {\n            title: 'Hello',\n            url: 'http://www.hello.com'\n          },\n          {\n            title: 'testing saka',\n            url: 'http://www.saka.io'\n          }\n        ];\n      };\n\n      const expectedResults = [\n        {\n          title: 'Hello',\n          url: 'http://www.hello.com',\n          score: undefined,\n          matches: [\n            {\n              indices: [[0, 4]],\n              value: 'Hello',\n              key: 'title',\n              arrayIndex: 0\n            },\n            {\n              indices: [[0, 0], [11, 15]],\n              value: 'http://www.hello.com',\n              key: 'url',\n              arrayIndex: 0\n            }\n          ]\n        }\n      ];\n\n      const results = await libUtil.getFilteredSuggestions(searchString, {\n        getSuggestions,\n        threshold: 0.6,\n        keys: ['title', 'url']\n      });\n      expect(results).toEqual(expectedResults);\n    });\n  });\n});\n"
  },
  {
    "path": "test/options/MainOptions.test.js",
    "content": "import { h } from 'preact';\nimport {\n  render,\n  cleanup,\n  wait,\n  fireEvent,\n  flushPromises,\n  getByValue\n} from 'preact-testing-library';\nimport 'jest-dom/extend-expect';\nimport MainOptions from '@/options/Main/MainOptions.jsx';\n\nbeforeEach(() => {\n  browser.flush();\n  browser.storage.sync.set.returns({});\n});\n\ntest('should show all options when not showing key bindings', async () => {\n  browser.storage.sync.get.returns({});\n  const { getByText, queryByText } = render(<MainOptions />);\n\n  getByText('Saka Options');\n  await wait(() => getByText('General Settings'));\n  //DefaultModeSelection\n  getByText('Default Mode');\n  getByText('Select the default mode Saka opens with');\n  //OnlyShowSearchBarSelector\n  getByText('Suggestions on load');\n  getByText('Show suggestions when there is no text is the Saka search bar');\n  //EnableFuzzySearch\n  getByText('Enable fuzzy search');\n  getByText('Enable fuzzy search for bookmarks and history search');\n\n  expect(queryByText('Keyboard Shortcuts')).not.toBeInTheDocument();\n});\n\ntest('should only show key bindings when setting is true', async () => {\n  browser.storage.sync.get.returns({});\n  global.SAKA_PLATFORM = 'chrome';\n  const { debug, getByText, getByLabelText } = render(<MainOptions />);\n\n  await wait(() => getByText('General Settings'));\n  getByText('Saka Hotkeys');\n  fireEvent.click(getByText('keyboard'), { button: 0 });\n  await flushPromises();\n\n  getByText('Saka Options');\n  expect(getByText('arrow_back'));\n  expect(getByLabelText('Back to Saka settings')).toMatchSnapshot();\n  expect(getByLabelText('Info about Saka custom hotkeys')).toMatchSnapshot();\n  getByText(\n    'To modify the Saka hotkeys, please visit chrome://extensions/shortcuts'\n  );\n  getByText('Keyboard Shortcuts');\n  getByText('Open Saka');\n});\n\ntest('should save settings when save button clicked', async () => {\n  browser.storage.sync.get.returns({\n    sakaSettings: {}\n  });\n  const { getByText, getByLabelText, getByValue } = render(<MainOptions />);\n\n  await wait(() => getByText('General Settings'));\n\n  fireEvent.change(getByLabelText('Select default mode'), {\n    target: { value: 'history' }\n  });\n  await flushPromises();\n\n  fireEvent.click(getByLabelText('Suggestions on load'), { button: 0 });\n  await flushPromises();\n\n  fireEvent.click(getByLabelText('Enable fuzzy search'), { button: 0 });\n  await flushPromises();\n\n  fireEvent.click(getByValue('Save'), { button: 0 });\n  await flushPromises();\n\n  expect(browser.storage.sync.get.calledOnce);\n});\n\nafterEach(cleanup);\n"
  },
  {
    "path": "test/options/__snapshots__/MainOptions.test.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`should only show key bindings when setting is true 1`] = `\n<i\n  aria-label=\"Back to Saka settings\"\n  aria-pressed=\"false\"\n  class=\"mdc-icon-toggle material-icons\"\n  role=\"button\"\n>\n  arrow_back\n</i>\n`;\n\nexports[`should only show key bindings when setting is true 2`] = `\n<i\n  aria-label=\"Info about Saka custom hotkeys\"\n  aria-pressed=\"false\"\n  class=\"mdc-icon-toggle material-icons\"\n  id=\"custom-hotkey-info\"\n>\n  info\n</i>\n`;\n"
  },
  {
    "path": "test/suggestion_engine/providers/bookmark.test.js",
    "content": "const browser = require('sinon-chrome/webextensions');\n\nimport bookmarkSuggestions from 'suggestion_engine/server/providers/bookmark.js';\n\ndescribe('server/providers/bookmark ', () => {\n  beforeAll(() => {\n    global.browser = browser;\n  });\n\n  beforeEach(() => {\n    browser.flush();\n  });\n\n  describe('bookmarkSuggestions ', () => {\n    it('should return all valid bookmarks when search string is empty', async () => {\n      const settingsStore = {\n        sakaSettings: {\n          enableFuzzySearch: true\n        }\n      };\n\n      const queryResults = [\n        {\n          url: 'https://google.com',\n          title: 'Google',\n          dateAdded: '2018-01-01'\n        },\n        {\n          url: 'https://github.com/lusakasa/saka',\n          title: 'Saka',\n          dateAdded: '2018-02-01'\n        }\n      ];\n\n      const expectedResult = [\n        {\n          type: 'bookmark',\n          score: -1,\n          url: 'https://google.com',\n          title: 'Google'\n        },\n        {\n          type: 'bookmark',\n          score: -1,\n          url: 'https://github.com/lusakasa/saka',\n          title: 'Saka'\n        }\n      ];\n\n      const searchString = '';\n      browser.bookmarks.search.returns(queryResults);\n      browser.storage.sync.get.returns(settingsStore);\n      expect(await bookmarkSuggestions(searchString)).toEqual(expectedResult);\n    });\n\n    it('should filter all bookmarks with unknown protocol', async () => {\n      const settingsStore = {\n        sakaSettings: {\n          enableFuzzySearch: true\n        }\n      };\n\n      const queryResults = [\n        {\n          url: 'ssh://myhost.net',\n          title: 'My Site',\n          dateAdded: '2018-01-01'\n        },\n        {\n          url: 'https://github.com/lusakasa/saka',\n          title: 'Saka',\n          dateAdded: '2018-02-01'\n        }\n      ];\n\n      const expectedResult = [\n        {\n          type: 'bookmark',\n          score: -1,\n          url: 'https://github.com/lusakasa/saka',\n          title: 'Saka'\n        }\n      ];\n\n      const searchString = '';\n      browser.bookmarks.search.returns(queryResults);\n      browser.storage.sync.get.returns(settingsStore);\n      expect(await bookmarkSuggestions(searchString)).toEqual(expectedResult);\n    });\n\n    it('should filter all bookmarks with invalid URL', async () => {\n      const settingsStore = {\n        sakaSettings: {\n          enableFuzzySearch: true\n        }\n      };\n\n      const queryResults = [\n        {\n          url: 'myhostnet',\n          title: 'My Site',\n          dateAdded: '2018-01-01'\n        },\n        {\n          url: 'https://github.com/lusakasa/saka',\n          title: 'Saka',\n          dateAdded: '2018-02-01'\n        }\n      ];\n\n      const expectedResult = [\n        {\n          type: 'bookmark',\n          score: -1,\n          url: 'https://github.com/lusakasa/saka',\n          title: 'Saka'\n        }\n      ];\n\n      const searchString = '';\n      browser.bookmarks.search.returns(queryResults);\n      browser.storage.sync.get.returns(settingsStore);\n      expect(await bookmarkSuggestions(searchString)).toEqual(expectedResult);\n    });\n\n    it('should return fuzzy search matching results', async () => {\n      const settingsStore = {\n        sakaSettings: {\n          enableFuzzySearch: true\n        }\n      };\n\n      const queryResults = [\n        {\n          url: 'myhostnet',\n          title: 'My Site',\n          dateAdded: '2018-01-01'\n        },\n        {\n          url: 'https://github.com/lusakasa/saka',\n          title: 'Saka',\n          dateAdded: '2018-02-01'\n        }\n      ];\n\n      const expectedResult = [\n        {\n          type: 'bookmark',\n          url: 'https://github.com/lusakasa/saka',\n          title: 'Saka',\n          score: undefined,\n          matches: [\n            {\n              indices: [[0, 3]],\n              value: 'Saka',\n              key: 'title',\n              arrayIndex: 0\n            },\n            {\n              indices: [[4, 4], [21, 24]],\n              value: 'https://github.com/lusakasa/saka',\n              key: 'url',\n              arrayIndex: 0\n            }\n          ]\n        }\n      ];\n\n      const searchString = 'Saka';\n      browser.bookmarks.search.returns(queryResults);\n      browser.storage.sync.get.returns(settingsStore);\n      expect(await bookmarkSuggestions(searchString)).toEqual(expectedResult);\n    });\n  });\n\n  afterAll(() => {\n    browser.flush();\n    delete global.browser;\n  });\n});\n"
  },
  {
    "path": "test/suggestion_engine/providers/closedTab.test.js",
    "content": "const browser = require('sinon-chrome/webextensions');\n\nimport closedTabSuggestions, {\n  getAllSuggestions\n} from 'suggestion_engine/server/providers/closedTab.js';\n\ndescribe('server/providers/closedTabs ', () => {\n  beforeAll(() => {\n    global.browser = browser;\n  });\n\n  beforeEach(() => {\n    browser.flush();\n  });\n\n  describe('closedTabSuggestions ', () => {\n    it('should return all closed tabs when no search string provided', async () => {\n      const queryResults = [\n        {\n          lastModified: 123456,\n          tab: {\n            id: 1,\n            windowId: 0,\n            title: 'Saka',\n            url: 'https://github.com/lusakasa/saka',\n            favIconUrl: 'https://github.com/lusakasa/saka/icon.png',\n            incognito: false\n          }\n        }\n      ];\n      const expectedResult = [\n        {\n          type: 'closedTab',\n          tabId: 1,\n          title: 'Saka',\n          url: 'https://github.com/lusakasa/saka',\n          favIconUrl: 'https://github.com/lusakasa/saka/icon.png',\n          sessionId: undefined,\n          score: undefined,\n          incognito: false,\n          lastAccessed: 123456\n        }\n      ];\n\n      const searchString = '';\n      const sakaId = 'abcdefg/saka.html';\n      browser.runtime.getURL.returns(sakaId);\n      browser.sessions.getRecentlyClosed.returns(queryResults);\n      expect(await closedTabSuggestions(searchString)).toEqual(expectedResult);\n    });\n\n    it('should filter out entries for saka in recently closed tabs', async () => {\n      const queryResults = [\n        {\n          lastModified: 123456,\n          tab: {\n            id: 1,\n            windowId: 0,\n            title: 'Saka',\n            url: 'https://github.com/lusakasa/saka',\n            favIconUrl: 'https://github.com/lusakasa/saka/icon.png',\n            incognito: true\n          }\n        },\n        {\n          lastModified: 654321,\n          tab: {\n            id: 2,\n            windowId: 0,\n            title: 'Saka Extension',\n            url: 'chrome-extension://abcdefg/saka.html',\n            favIconUrl: '',\n            incognito: false\n          }\n        }\n      ];\n      const expectedResult = [\n        {\n          type: 'closedTab',\n          tabId: 1,\n          title: 'Saka',\n          url: 'https://github.com/lusakasa/saka',\n          favIconUrl: null,\n          sessionId: undefined,\n          score: undefined,\n          incognito: true,\n          lastAccessed: 123456\n        }\n      ];\n\n      const searchString = '';\n      const sakaId = 'abcdefg/saka.html';\n      browser.runtime.getURL.returns(sakaId);\n      browser.sessions.getRecentlyClosed.returns(queryResults);\n      expect(await closedTabSuggestions(searchString)).toEqual(expectedResult);\n    });\n\n    it('should return all closed tabs matching searchString', async () => {\n      const queryResults = [\n        {\n          lastModified: 123456,\n          tab: {\n            id: 1,\n            windowId: 0,\n            title: 'Saka',\n            url: 'https://github.com/lusakasa/saka',\n            favIconUrl: 'https://github.com/lusakasa/saka/icon.png',\n            incognito: false\n          }\n        },\n        {\n          lastModified: 654321,\n          tab: {\n            id: 2,\n            windowId: 0,\n            title: 'Google',\n            url: 'https://google.com',\n            favIconUrl: 'https://google.com/icon.png',\n            incognito: true\n          }\n        }\n      ];\n      const expectedResult = [\n        {\n          type: 'closedTab',\n          tabId: 2,\n          title: 'Google',\n          url: 'https://google.com',\n          favIconUrl: null,\n          sessionId: undefined,\n          score: undefined,\n          incognito: true,\n          lastAccessed: 654321,\n          matches: [\n            {\n              indices: [[0, 3]],\n              value: 'Google',\n              key: 'title',\n              arrayIndex: 0\n            },\n            {\n              indices: [[8, 11]],\n              value: 'https://google.com',\n              key: 'url',\n              arrayIndex: 0\n            }\n          ]\n        }\n      ];\n\n      const searchString = 'Goog';\n      const sakaId = 'abcdefg/saka.html';\n      browser.runtime.getURL.returns(sakaId);\n      browser.sessions.getRecentlyClosed.returns(queryResults);\n      expect(await closedTabSuggestions(searchString)).toEqual(expectedResult);\n    });\n  });\n\n  describe('getAllSuggestions', () => {\n    it('should work for window sessions', async () => {\n      const queryResults = [\n        {\n          window: {\n            lastModified: 123456,\n            tabs: [\n              {\n                id: 1,\n                windowId: 0,\n                title: 'Saka',\n                url: 'https://github.com/lusakasa/saka',\n                favIconUrl: 'https://github.com/lusakasa/saka/icon.png',\n                incognito: false\n              }\n            ]\n          }\n        }\n      ];\n      const expectedResult = [\n        {\n          type: 'closedTab',\n          tabId: 1,\n          title: 'Saka',\n          url: 'https://github.com/lusakasa/saka',\n          favIconUrl: 'https://github.com/lusakasa/saka/icon.png',\n          sessionId: undefined,\n          score: undefined,\n          incognito: false,\n          lastAccessed: 123456\n        }\n      ];\n\n      const sakaId = 'abcdefg/saka.html';\n      browser.runtime.getURL.returns(sakaId);\n      browser.sessions.getRecentlyClosed.returns(queryResults);\n      expect(await getAllSuggestions()).toEqual(expectedResult);\n    });\n  });\n\n  afterAll(() => {\n    browser.flush();\n    delete global.browser;\n  });\n});\n"
  },
  {
    "path": "test/suggestion_engine/providers/history.test.js",
    "content": "const browser = require('sinon-chrome/webextensions');\n\nimport getHistorySuggestions from 'suggestion_engine/server/providers/history.js';\n\ndescribe('server/providers/history ', () => {\n  beforeAll(() => {\n    global.browser = browser;\n  });\n\n  beforeEach(() => {\n    browser.flush();\n  });\n\n  describe('getHistorySuggestions ', () => {\n    it('should not use fuzzy search when enableFuzzySearch setting is set to false', async () => {\n      const settingsStore = {\n        sakaSettings: {\n          enableFuzzySearch: false\n        }\n      };\n      const queryResults = [\n        {\n          id: 1,\n          url: 'https://github.com/lusakasa/saka',\n          title: 'Saka Github',\n          lastVisitTime: 1524795334,\n          visitCount: 5,\n          typedCount: 10\n        },\n        {\n          id: 2,\n          url: 'https://example.com',\n          title: 'Example',\n          lastVisitTime: 1524794200,\n          visitCount: 1,\n          typedCount: 0\n        }\n      ];\n      const expectedResult = [\n        {\n          type: 'history',\n          url: 'https://github.com/lusakasa/saka',\n          title: 'Saka Github',\n          lastAccessed: 1524795.334,\n          score: 15\n        },\n        {\n          type: 'history',\n          title: 'Example',\n          url: 'https://example.com',\n          lastAccessed: 1524794.2,\n          score: 1\n        }\n      ];\n\n      const searchString = 'Saka';\n      const sakaURL = 'nbdfpcokndmap/saka.html';\n      browser.runtime.getURL.returns(sakaURL);\n      browser.history.search.returns(queryResults);\n      browser.storage.sync.get.returns(settingsStore);\n      expect(await getHistorySuggestions(searchString)).toEqual(expectedResult);\n    });\n\n    it('should use fuzzy search when enableFuzzySearch setting is set to true', async () => {\n      const settingsStore = {\n        sakaSettings: {\n          enableFuzzySearch: true\n        }\n      };\n      const queryResults = [\n        {\n          id: 1,\n          url: 'https://github.com/lusakasa/saka',\n          title: 'Saka Github',\n          lastVisitTime: 1524795334,\n          visitCount: 5,\n          typedCount: 10\n        },\n        {\n          id: 2,\n          url: 'https://example.com',\n          title: 'Example',\n          lastVisitTime: 1524794200,\n          visitCount: 1,\n          typedCount: 0\n        }\n      ];\n\n      const expectedResult = [\n        {\n          type: 'history',\n          url: 'https://github.com/lusakasa/saka',\n          title: 'Saka Github',\n          lastAccessed: 1524795.334,\n          score: undefined,\n          matches: [\n            {\n              indices: [[0, 3]],\n              value: 'Saka Github',\n              key: 'title',\n              arrayIndex: 0\n            },\n            {\n              indices: [[4, 4], [21, 24]],\n              value: 'https://github.com/lusakasa/saka',\n              key: 'url',\n              arrayIndex: 0\n            }\n          ]\n        },\n        {\n          type: 'history',\n          title: 'Example',\n          url: 'https://example.com',\n          lastAccessed: 1524794.2,\n          score: undefined,\n          matches: [\n            {\n              indices: [[2, 2]],\n              value: 'Example',\n              key: 'title',\n              arrayIndex: 0\n            },\n            {\n              indices: [[4, 4], [10, 10]],\n              value: 'https://example.com',\n              key: 'url',\n              arrayIndex: 0\n            }\n          ]\n        }\n      ];\n\n      const searchString = 'Saka';\n      const sakaURL = 'nbdfpcokndmap/saka.html';\n      browser.runtime.getURL.returns(sakaURL);\n      browser.history.search.returns(queryResults);\n      browser.storage.sync.get.returns(settingsStore);\n      expect(await getHistorySuggestions(searchString)).toEqual(expectedResult);\n    });\n\n    it('should return all visited site history except Saka Options URL', async () => {\n      const settingsStore = {\n        sakaSettings: {\n          enableFuzzySearch: true\n        }\n      };\n      const queryResults = [\n        {\n          id: 1,\n          url: 'https://github.com/lusakasa/saka',\n          title: 'Saka Github',\n          lastVisitTime: 1524795334,\n          visitCount: 5,\n          typedCount: 10\n        },\n        {\n          id: 2,\n          url: 'chrome://nbdfpcokndmap/options.html',\n          title: 'Options',\n          lastVisitTime: 1524794200,\n          visitCount: 1,\n          typedCount: 0\n        }\n      ];\n      const expectedResult = [\n        {\n          type: 'history',\n          url: 'https://github.com/lusakasa/saka',\n          title: 'Saka Github',\n          lastAccessed: 1524795.334,\n          score: 15\n        }\n      ];\n\n      const searchString = '';\n      const sakaURL = 'nbdfpcokndmap/saka.html';\n      browser.runtime.getURL.returns(sakaURL);\n      browser.history.search.returns(queryResults);\n      browser.storage.sync.get.returns(settingsStore);\n      expect(await getHistorySuggestions(searchString)).toEqual(expectedResult);\n    });\n  });\n\n  afterAll(() => {\n    browser.flush();\n    delete global.browser;\n  });\n});\n"
  },
  {
    "path": "test/suggestion_engine/providers/mode.test.js",
    "content": "import modeSuggestions from 'suggestion_engine/server/providers/mode.js';\nimport { ctrlChar } from 'lib/utils';\nimport { colorMap, fadedColorMap } from 'lib/colors';\n\ndescribe('server/providers/mode ', () => {\n  describe('modeSuggestions ', () => {\n    it('should return all modes when no search string provided', async () => {\n      const expectedResult = [\n        {\n          type: 'mode',\n          mode: 'tab',\n          label: 'Tabs',\n          shortcut: `${ctrlChar}-shift-a`,\n          color: colorMap.tab,\n          fadedColor: fadedColorMap.tab,\n          icon: 'tab'\n        },\n        {\n          type: 'mode',\n          mode: 'closedTab',\n          label: 'Recently Closed Tabs',\n          shortcut: `${ctrlChar}-shift-c`,\n          color: colorMap.closedTab,\n          fadedColor: fadedColorMap.closedTab,\n          icon: 'restore_page'\n        },\n        {\n          type: 'mode',\n          label: 'Bookmarks',\n          mode: 'bookmark',\n          shortcut: `${ctrlChar}-b`,\n          color: colorMap.bookmark,\n          fadedColor: fadedColorMap.bookmark,\n          icon: 'bookmark_border'\n        },\n        {\n          type: 'mode',\n          label: 'History',\n          mode: 'history',\n          shortcut: `${ctrlChar}-shift-e`,\n          color: colorMap.history,\n          fadedColor: fadedColorMap.history,\n          icon: 'history'\n        },\n        {\n          type: 'mode',\n          label: 'Recently Viewed',\n          mode: 'recentlyViewed',\n          shortcut: `${ctrlChar}-shift-x`,\n          color: colorMap.recentlyViewed,\n          fadedColor: fadedColorMap.recentlyViewed,\n          icon: 'timelapse'\n        }\n      ];\n\n      const searchString = '';\n      expect(await modeSuggestions(searchString)).toEqual(expectedResult);\n    });\n\n    it('should return closedTab search mode when search string `cl` provided', async () => {\n      const expectedResult = [\n        {\n          type: 'mode',\n          mode: 'closedTab',\n          label: 'Recently Closed Tabs',\n          shortcut: `${ctrlChar}-shift-c`,\n          color: 'rgba(0,0,0,1)',\n          fadedColor: 'rgba(0,0,0,0.44)',\n          icon: 'restore_page',\n          score: undefined,\n          matches: [\n            {\n              indices: [[2, 2], [6, 6], [9, 10]],\n              value: 'Recently Closed Tabs',\n              key: 'label',\n              arrayIndex: 0\n            }\n          ]\n        },\n        {\n          type: 'mode',\n          label: 'Recently Viewed',\n          mode: 'recentlyViewed',\n          shortcut: `${ctrlChar}-shift-x`,\n          color: 'rgba(152,78,163,1)',\n          fadedColor: 'rgba(152,78,163,0.44)',\n          icon: 'timelapse',\n          score: undefined,\n          matches: [\n            {\n              indices: [[2, 2], [6, 6]],\n              value: 'Recently Viewed',\n              key: 'label',\n              arrayIndex: 0\n            }\n          ]\n        }\n      ];\n\n      const searchString = 'cl';\n      expect(await modeSuggestions(searchString)).toEqual(expectedResult);\n    });\n  });\n});\n"
  },
  {
    "path": "test/suggestion_engine/providers/recentlyViewed.test.js",
    "content": "const browser = require('sinon-chrome/webextensions');\n\nimport recentlyViewedSuggestions from 'suggestion_engine/server/providers/recentlyViewed.js';\n\ndescribe('server/providers/recentlyViewed ', () => {\n  beforeAll(() => {\n    global.browser = browser;\n  });\n\n  beforeEach(() => {\n    browser.flush();\n    // Clears the database and adds some testing data.\n    // Jest will wait for this promise to resolve before running tests.\n    global.SAKA_PLATFORM = 'chrome';\n  });\n\n  describe('recentlyViewedSuggestions ', () => {\n    it('should return all recently viewed tabs when search string is empty', async () => {\n      const trackedHistory = {\n        tabHistory: [\n          { tabId: 1, lastAccessed: 123456 },\n          { tabId: 0, lastAccessed: 654321 }\n        ],\n        recentlyClosed: [\n          { tab: { tabId: 1, lastAccessed: 111111 } },\n          { tab: { tabId: 4, lastAccessed: 222222 } }\n        ]\n      };\n\n      const tabResults = [\n        {\n          id: 1,\n          windowId: 0,\n          title: 'Google',\n          url: 'https://google.com',\n          favIconUrl: 'https://google.com/icon.png',\n          incognito: false,\n          lastAccessed: 123456\n        },\n        {\n          id: 0,\n          windowId: 0,\n          title: 'Saka',\n          url: 'https://github.com/lusakasa/saka',\n          favIconUrl: 'https://github.com/lusakasa/saka/icon.png',\n          incognito: true,\n          lastAccessed: 654321\n        }\n      ];\n\n      const recentlyClosedResults = [\n        {\n          lastModified: 123456,\n          tab: {\n            id: 1,\n            windowId: 0,\n            title: 'Saka',\n            url: 'https://github.com/lusakasa/saka',\n            favIconUrl: 'https://github.com/lusakasa/saka/icon.png',\n            incognito: false,\n            sessionId: 'abc123'\n          }\n        },\n        {\n          lastModified: 19191,\n          tab: {\n            id: 2,\n            windowId: 0,\n            title: 'Recently Viewed Mode',\n            url: 'https://github.com/lusakasa/saka/pull/45',\n            favIconUrl: 'https://github.com/icon.png',\n            incognito: true,\n            sessionId: '123abc'\n          }\n        }\n      ];\n\n      const historyResults = [\n        {\n          id: 1,\n          url: 'https://github.com/lusakasa/saka',\n          title: 'Saka Github',\n          lastVisitTime: 1524795334,\n          visitCount: 5,\n          typedCount: 10\n        },\n        {\n          id: 3,\n          url: 'https://example.com',\n          title: 'Example',\n          lastVisitTime: 1524794200,\n          visitCount: 1,\n          typedCount: 0\n        }\n      ];\n\n      const expectedResult = [\n        {\n          type: 'recentlyViewed',\n          url: 'https://example.com',\n          title: 'Example',\n          lastAccessed: 1524794.2,\n          score: 1,\n          originalType: 'history'\n        },\n        {\n          lastAccessed: 19191,\n          tabId: 2,\n          title: 'Recently Viewed Mode',\n          url: 'https://github.com/lusakasa/saka/pull/45',\n          favIconUrl: null,\n          incognito: true,\n          sessionId: '123abc',\n          score: undefined,\n          type: 'recentlyViewed',\n          originalType: 'closedTab'\n        },\n        {\n          type: 'recentlyViewed',\n          tabId: 0,\n          windowId: 0,\n          title: 'Saka',\n          url: 'https://github.com/lusakasa/saka',\n          favIconUrl: null,\n          incognito: true,\n          lastAccessed: 654.321,\n          originalType: 'tab'\n        },\n        {\n          type: 'recentlyViewed',\n          tabId: 1,\n          windowId: 0,\n          title: 'Google',\n          url: 'https://google.com',\n          favIconUrl: 'https://google.com/icon.png',\n          incognito: false,\n          lastAccessed: 123.456,\n          originalType: 'tab'\n        }\n      ];\n\n      const searchString = '';\n      const sakaId = 'abcdefg/saka.html';\n      browser.tabs.query.returns(tabResults);\n      browser.runtime.getURL.returns(sakaId);\n      browser.runtime.getBackgroundPage.returns(trackedHistory);\n      browser.sessions.getRecentlyClosed.returns(recentlyClosedResults);\n      browser.history.search.returns(historyResults);\n      expect(await recentlyViewedSuggestions(searchString)).toEqual(\n        expectedResult\n      );\n    });\n\n    it('should return matching recently viewed tabs when search string is not empty', async () => {\n      const tabHistory = { tabHistory: [0] };\n      const tabResults = [\n        {\n          id: 1,\n          windowId: 0,\n          title: 'Google',\n          url: 'https://google.com',\n          favIconUrl: 'https://google.com/icon.png',\n          incognito: false,\n          lastAccessed: 123456\n        },\n        {\n          id: 0,\n          windowId: 0,\n          title: 'Saka',\n          url: 'https://github.com/lusakasa/saka',\n          favIconUrl: 'https://github.com/lusakasa/saka/icon.png',\n          incognito: true,\n          lastAccessed: 654321\n        }\n      ];\n\n      const recentlyClosedResults = [\n        {\n          lastModified: 123456,\n          tab: {\n            id: 1,\n            windowId: 0,\n            title: 'Saka',\n            url: 'https://github.com/lusakasa/saka',\n            favIconUrl: 'https://github.com/lusakasa/saka/icon.png',\n            incognito: false,\n            sessionId: 'abc123'\n          }\n        },\n        {\n          lastModified: 19191,\n          tab: {\n            id: 2,\n            windowId: 0,\n            title: 'Recently Viewed Mode',\n            url: 'https://github.com/lusakasa/saka/pull/45',\n            favIconUrl: 'https://github.com/icon.png',\n            incognito: true,\n            sessionId: '123abc'\n          }\n        }\n      ];\n\n      const historyResults = [\n        {\n          id: 1,\n          url: 'https://github.com/lusakasa/saka',\n          title: 'Saka Github',\n          lastVisitTime: 1524795334,\n          visitCount: 5,\n          typedCount: 10\n        },\n        {\n          id: 3,\n          url: 'https://example.com',\n          title: 'Example',\n          lastVisitTime: 1524794200,\n          visitCount: 1,\n          typedCount: 0\n        }\n      ];\n\n      const expectedResult = [\n        {\n          type: 'recentlyViewed',\n          tabId: 0,\n          windowId: 0,\n          title: 'Saka',\n          url: 'https://github.com/lusakasa/saka',\n          favIconUrl: null,\n          incognito: true,\n          lastAccessed: 654.321,\n          originalType: 'tab',\n          score: undefined,\n          matches: [\n            Object({\n              indices: [[0, 3]],\n              value: 'Saka',\n              key: 'title',\n              arrayIndex: 0\n            }),\n            Object({\n              indices: [[4, 4], [21, 24]],\n              value: 'https://github.com/lusakasa/saka',\n              key: 'url',\n              arrayIndex: 0\n            })\n          ]\n        },\n        {\n          type: 'recentlyViewed',\n          url: 'https://github.com/lusakasa/saka',\n          title: 'Saka Github',\n          lastAccessed: 1524795.334,\n          score: undefined,\n          originalType: 'history',\n          matches: [\n            Object({\n              indices: [[0, 3]],\n              value: 'Saka Github',\n              key: 'title',\n              arrayIndex: 0\n            }),\n            Object({\n              indices: [[4, 4], [21, 24]],\n              value: 'https://github.com/lusakasa/saka',\n              key: 'url',\n              arrayIndex: 0\n            })\n          ]\n        },\n        {\n          type: 'recentlyViewed',\n          tabId: 2,\n          title: 'Recently Viewed Mode',\n          url: 'https://github.com/lusakasa/saka/pull/45',\n          favIconUrl: null,\n          incognito: true,\n          lastAccessed: 19191,\n          sessionId: '123abc',\n          score: undefined,\n          originalType: 'closedTab',\n          matches: [\n            Object({\n              indices: [[4, 4], [21, 24]],\n              value: 'https://github.com/lusakasa/saka/pull/45',\n              key: 'url',\n              arrayIndex: 0\n            })\n          ]\n        }\n      ];\n\n      const searchString = 'saka';\n      const sakaId = 'abcdefg/saka.html';\n      browser.tabs.query.returns(tabResults);\n      browser.runtime.getURL.returns(sakaId);\n      browser.runtime.getBackgroundPage.returns(tabHistory);\n      browser.sessions.getRecentlyClosed.returns(recentlyClosedResults);\n      browser.history.search.returns(historyResults);\n      expect(await recentlyViewedSuggestions(searchString)).toEqual(\n        expectedResult\n      );\n    });\n  });\n\n  afterAll(() => {\n    browser.flush();\n    delete global.browser;\n  });\n});\n"
  },
  {
    "path": "test/suggestion_engine/providers/tab.test.js",
    "content": "const browser = require('sinon-chrome/webextensions');\n\nimport tabSuggestions from 'suggestion_engine/server/providers/tab.js';\n\ndescribe('server/providers/tab ', () => {\n  beforeAll(() => {\n    global.browser = browser;\n  });\n\n  beforeEach(() => {\n    browser.flush();\n  });\n\n  describe('tabSuggestions ', () => {\n    it('should return all recent tabs when search string is empty', async () => {\n      const queryResults = [\n        {\n          id: 1,\n          windowId: 0,\n          title: 'Google',\n          url: 'https://google.com',\n          favIconUrl: 'https://google.com/icon.png',\n          incognito: false,\n          lastAccessed: 123456\n        },\n        {\n          id: 0,\n          windowId: 0,\n          title: 'Saka',\n          url: 'https://github.com/lusakasa/saka',\n          favIconUrl: 'https://github.com/lusakasa/saka/icon.png',\n          incognito: true,\n          lastAccessed: 654321\n        }\n      ];\n\n      const tabHistory = {\n        tabHistory: [\n          { tabId: 1, lastAccessed: 123456 },\n          { tabId: 0, lastAccessed: 654321 }\n        ]\n      };\n\n      const expectedResult = [\n        {\n          type: 'tab',\n          tabId: 1,\n          windowId: 0,\n          title: 'Google',\n          url: 'https://google.com',\n          favIconUrl: 'https://google.com/icon.png',\n          incognito: false,\n          lastAccessed: 123456\n        },\n        {\n          type: 'tab',\n          tabId: 0,\n          windowId: 0,\n          title: 'Saka',\n          url: 'https://github.com/lusakasa/saka',\n          favIconUrl: null,\n          incognito: true,\n          lastAccessed: 654321\n        }\n      ];\n\n      const searchString = '';\n      browser.tabs.query.returns(queryResults);\n      browser.runtime.getBackgroundPage.returns(tabHistory);\n      expect(await tabSuggestions(searchString)).toEqual(expectedResult);\n    });\n\n    it('should return all tabs matching searchString', async () => {\n      const queryResults = [\n        {\n          id: 0,\n          windowId: 0,\n          title: 'Saka',\n          url: 'https://github.com/lusakasa/saka',\n          favIconUrl: 'https://github.com/lusakasa/saka/icon.png',\n          incognito: false,\n          lastAccessed: 123456\n        },\n        {\n          id: 0,\n          windowId: 0,\n          title: 'Google',\n          url: 'https://google.com',\n          favIconUrl: 'https://google.com/icon.png',\n          incognito: false,\n          lastAccessed: 654321\n        }\n      ];\n\n      const expectedResult = [\n        {\n          type: 'tab',\n          tabId: 0,\n          windowId: 0,\n          title: 'Saka',\n          url: 'https://github.com/lusakasa/saka',\n          favIconUrl: 'https://github.com/lusakasa/saka/icon.png',\n          incognito: false,\n          lastAccessed: 123.456,\n          matches: [\n            {\n              indices: [[0, 3]],\n              value: 'Saka',\n              key: 'title',\n              arrayIndex: 0\n            },\n            {\n              indices: [[4, 4], [21, 24]],\n              value: 'https://github.com/lusakasa/saka',\n              key: 'url',\n              arrayIndex: 0\n            }\n          ],\n          score: undefined\n        }\n      ];\n\n      const searchString = 'saka';\n      browser.tabs.query.returns(queryResults);\n      expect(await tabSuggestions(searchString)).toEqual(expectedResult);\n    });\n  });\n\n  afterAll(() => {\n    browser.flush();\n    delete global.browser;\n  });\n});\n"
  },
  {
    "path": "test/suggestion_engine/server/index.test.js",
    "content": "import {\n  getSuggestions,\n  activateSuggestion,\n  closeTab\n} from 'suggestion_engine/server/index.js';\nimport * as providers from 'suggestion_engine/server/providers/index.js';\n\njest.mock('suggestion_engine/server/providers/index.js', () => ({\n  tab: jest.fn().mockImplementation(() => [\n    {\n      type: 'tab'\n    }\n  ]),\n  closedTab: jest.fn(),\n  mode: jest.fn(),\n  history: jest.fn(),\n  bookmark: jest.fn(),\n  recentlyViewed: jest.fn()\n}));\n\ntest('should show all options when not showing key bindings', async () => {\n  const mockexpectedResult = [\n    {\n      type: 'tab'\n    }\n  ];\n\n  const suggestions = await getSuggestions(['tab', 'saka']);\n  expect(suggestions).toEqual(mockexpectedResult);\n});\n\ntest('should call appropriate activation methods', async () => {\n  browser.flush();\n  await activateSuggestion({\n    type: 'tab'\n  });\n  expect(browser.tabs.update.calledOnce).toEqual(true);\n  expect(browser.windows.update.calledOnce).toEqual(true);\n\n  browser.flush();\n  await activateSuggestion({\n    type: 'closedTab'\n  });\n  expect(browser.sessions.restore.calledOnce).toEqual(true);\n\n  browser.flush();\n  await activateSuggestion({\n    type: 'bookmark'\n  });\n  expect(browser.tabs.create.calledOnce).toEqual(true);\n\n  browser.flush();\n  await activateSuggestion({\n    type: 'history'\n  });\n  expect(browser.tabs.create.calledOnce).toEqual(true);\n\n  browser.flush();\n  await activateSuggestion({\n    type: 'recentlyViewed',\n    originalType: 'tab'\n  });\n  expect(browser.tabs.update.calledOnce).toEqual(true);\n});\n\ntest('should focus bookmark and history tabs if already open', async () => {\n  browser.flush();\n  browser.tabs.query.resolves([{ id: '1', windowId: '1' }]);\n  await activateSuggestion({\n    type: 'bookmark'\n  });\n  expect(browser.tabs.update.calledOnce).toEqual(true);\n  expect(browser.windows.update.calledOnce).toEqual(true);\n\n  browser.flush();\n  browser.tabs.query.resolves([{ id: '1', windowId: '1' }]);\n  await activateSuggestion({\n    type: 'history'\n  });\n  expect(browser.tabs.update.calledOnce).toEqual(true);\n  expect(browser.windows.update.calledOnce).toEqual(true);\n});\n\ntest('should call close tab API', async () => {\n  const suggestion = {\n    tabId: 1\n  };\n\n  await closeTab(suggestion);\n});\n"
  },
  {
    "path": "test/suggestion_utils/index.test.js",
    "content": "import { preprocessSuggestion } from '@/suggestion_utils/index.js';\nimport { colorMap, fadedColorMap } from 'lib/colors';\nimport * as url from 'lib/url.js';\n\ntest('should return suggestion with pretty URL when type is tab', () => {\n  url.prettifyURL = jest\n    .fn()\n    .mockImplementation(() => 'https://github.com/lusakasa/saka');\n\n  const suggestion = {\n    type: 'tab',\n    tabId: 0,\n    windowId: 0,\n    title: 'Saka',\n    url: 'https://github.com/lusakasa/saka?stuffInUrl=true',\n    favIconUrl: 'https://github.com/lusakasa/saka/icon.png',\n    incognito: false,\n    lastAccessed: 123.456,\n    matches: [],\n    score: undefined\n  };\n  const searchText = 'saka';\n  const result = preprocessSuggestion(suggestion, searchText);\n  expect(result).toEqual({\n    ...suggestion,\n    prettyURL: 'https://github.com/lusakasa/saka',\n    text: suggestion.title\n  });\n});\n\ntest('should return suggestion with pretty URL when type is closedTab', () => {\n  url.prettifyURL = jest\n    .fn()\n    .mockImplementation(() => 'https://github.com/lusakasa/saka');\n\n  const suggestion = {\n    type: 'closedTab',\n    tabId: 1,\n    title: 'Saka',\n    url: 'https://github.com/lusakasa/saka?stuffInUrl=true',\n    favIconUrl: 'https://github.com/lusakasa/saka/icon.png',\n    sessionId: undefined,\n    score: undefined,\n    incognito: false,\n    lastAccessed: 123456\n  };\n  const searchText = 'saka';\n  const result = preprocessSuggestion(suggestion, searchText);\n  expect(result).toEqual({\n    ...suggestion,\n    prettyURL: 'https://github.com/lusakasa/saka',\n    text: suggestion.title\n  });\n});\n\ntest('should return suggestion when type is mode', () => {\n  url.prettifyURL = jest.fn().mockImplementation(suggestion => suggestion);\n\n  const suggestion = {\n    type: 'mode',\n    mode: 'tab',\n    label: 'Tabs',\n    shortcut: `ctrl-shift-a`,\n    color: colorMap.tab,\n    fadedColor: fadedColorMap.tab,\n    icon: 'tab'\n  };\n\n  const searchText = 'saka';\n  const result = preprocessSuggestion(suggestion, searchText);\n  expect(result).toEqual(suggestion);\n});\n\ntest('should return suggestion with pretty URL when type is bookmark', () => {\n  url.prettifyURL = jest\n    .fn()\n    .mockImplementation(() => 'https://github.com/lusakasa/saka');\n\n  const suggestion = {\n    type: 'bookmark',\n    score: -1,\n    url: 'https://github.com/lusakasa/saka',\n    title: 'Saka'\n  };\n\n  const searchText = 'saka';\n  const result = preprocessSuggestion(suggestion, searchText);\n  expect(result).toEqual({\n    ...suggestion,\n    prettyURL: 'https://github.com/lusakasa/saka',\n    text: 'https://github.com/lusakasa/saka'\n  });\n});\n\ntest('should return suggestion with pretty URL when type is history', () => {\n  url.prettifyURL = jest\n    .fn()\n    .mockImplementation(() => 'https://github.com/lusakasa/saka');\n\n  const suggestion = {\n    type: 'history',\n    url: 'https://github.com/lusakasa/saka',\n    title: 'Saka Github',\n    lastAccessed: 1524795.334,\n    score: 15\n  };\n\n  const searchText = 'saka';\n  const result = preprocessSuggestion(suggestion, searchText);\n  expect(result).toEqual({\n    ...suggestion,\n    prettyURL: 'https://github.com/lusakasa/saka',\n    text: 'https://github.com/lusakasa/saka'\n  });\n});\n\ntest('should return suggestion with pretty URL when type is recentlyViewed', () => {\n  url.prettifyURL = jest\n    .fn()\n    .mockImplementation(() => 'https://github.com/lusakasa/saka');\n\n  const suggestion = {\n    type: 'recentlyViewed',\n    url: 'https://example.com',\n    title: 'Example',\n    lastAccessed: 1524794.2,\n    score: 1,\n    originalType: 'history'\n  };\n\n  const searchText = 'saka';\n  const result = preprocessSuggestion(suggestion, searchText);\n  expect(result).toEqual({\n    ...suggestion,\n    prettyURL: 'https://github.com/lusakasa/saka',\n    text: 'https://github.com/lusakasa/saka'\n  });\n});\n\ntest('should return error when type is unexpected', () => {\n  const suggestion = {\n    type: 'adsknajksfnasjfnjas',\n    url: 'https://example.com',\n    title: 'Example',\n    lastAccessed: 1524794.2,\n    score: 1,\n    originalType: 'history'\n  };\n\n  const searchText = 'saka';\n  const result = preprocessSuggestion(suggestion, searchText);\n  expect(result).toEqual({\n    type: 'error',\n    title: `Error. Unknown Suggestion type: ${suggestion.type}`,\n    text: `Error. Unknown Suggestion type: ${suggestion.type}`\n  });\n});\n"
  },
  {
    "path": "webpack.config.js",
    "content": "const webpack = require('webpack');\nconst BabiliPlugin = require('babili-webpack-plugin');\nconst CopyWebpackPlugin = require('copy-webpack-plugin');\nconst GenerateJsonPlugin = require('generate-json-webpack-plugin');\nconst marked = require('marked');\nconst path = require('path');\nconst merge = require('webpack-merge');\nconst { version } = require('./manifest/common.json'); // mode controls:\n\nconst renderer = new marked.Renderer();\n// process.traceDeprecation = true;\n// markdown convert to html\n\nmodule.exports = function webpackConfig(env) {\n  const [mode, platform, benchmark] = env.split(':');\n\n  const config = {\n    resolve: {\n      alias: {\n        react: 'preact-compat',\n        'react-dom': 'preact-compat',\n        src: path.join(__dirname, 'src'),\n        msg: path.join(__dirname, 'src/msg'),\n        suggestion_engine: path.join(__dirname, 'src/suggestion_engine'),\n        suggestion_utils: path.join(__dirname, 'src/suggestion_utils'),\n        lib: path.join(__dirname, 'src/lib'),\n        scss: path.join(__dirname, 'src/scss')\n      },\n      modules: ['./src', './node_modules']\n    },\n    entry: {\n      background_page: 'src/background_page/index.js',\n      toggle_saka: 'src/content_script/toggle_saka.js',\n      // 'extensions': './src/pages/extensions/index.js',\n      // 'info': './src/pages/info/index.js',\n      // 'options': './src/pages/options/index.js',\n      saka: 'src/saka/index.jsx',\n      'saka-options': 'src/options/saka-options.jsx'\n    },\n    optimization: {\n      splitChunks: {\n        cacheGroups: {\n          commons: {\n            test: /[\\\\/]node_modules[\\\\/]/,\n            name: 'vendor',\n            chunks: 'all'\n          }\n        }\n      }\n    },\n    output: {\n      path: `${__dirname}/dist`,\n      filename: '[name].js'\n      // sourceMapFilename: '[name].js.map'\n    },\n    module: {\n      rules: [\n        {\n          test: /\\.(jsx|js)$/,\n          exclude: /node_modules/,\n          loaders: ['babel-loader']\n        },\n        {\n          test: /\\.(sc|c)ss$/,\n          loaders: [\n            'style-loader',\n            'css-loader',\n            {\n              loader: 'sass-loader',\n              options: {\n                importer(url, prev) {\n                  if (url.indexOf('@material') === 0) {\n                    const filePath = url.split('@material')[1];\n                    const nodeModulePath = `./node_modules/@material/${filePath}`;\n                    return { file: path.resolve(nodeModulePath) };\n                  }\n                  return { file: url };\n                }\n              }\n            }\n          ]\n        },\n        {\n          test: /\\.md$/,\n          loaders: [\n            'html-loader',\n            {\n              loader: 'markdown-loader',\n              options: {\n                renderer\n              }\n            }\n          ]\n        }\n      ]\n    },\n    plugins: [\n      new webpack.optimize.ModuleConcatenationPlugin(),\n      new CopyWebpackPlugin([\n        {\n          from: 'static'\n        },\n        {\n          context: 'src/modes',\n          from: '**/default.json',\n          to: 'default_[folder].json'\n        },\n        {\n          context: 'src/modes',\n          from: '**/config.json',\n          to: 'config_[folder].json'\n        }\n      ]),\n      new GenerateJsonPlugin(\n        'manifest.json',\n        merge(\n          require('./manifest/common.json'),\n          require(`./manifest/${platform}.json`),\n          { version }\n        ),\n        null,\n        2\n      )\n    ]\n  };\n\n  // 1. SAKA_DEBUG: boolean(true | false)\n  //   * true for development builds\n  //   * false for production build\n  //   If you want something to run only in testing/development, use\n  //     if (SAKA_DEBUG) { console.log(variable); }.\n  //   All code within will be removed at build time in production builds.\n  // platform controls:\n  // 1. SAKA_PLATFORM: string('chrome' | 'firefox' | 'edge')\n  //   Use this to provide platform specific features, e.g. use shadow DOM\n  //   on chrome but css selectors on firefox and edge for link hint styling\n\n  if (mode === 'prod') {\n    config.plugins = config.plugins.concat([\n      new BabiliPlugin(),\n      new webpack.DefinePlugin({\n        'process.env.NODE_ENV': JSON.stringify('production'),\n        SAKA_DEBUG: JSON.stringify(false),\n        SAKA_VERSION: JSON.stringify(version),\n        SAKA_PLATFORM: JSON.stringify(platform),\n        SAKA_BENCHMARK: JSON.stringify(true)\n      })\n    ]);\n  } else {\n    config.devtool = 'source-map';\n    config.plugins = config.plugins.concat([\n      new webpack.DefinePlugin({\n        'process.env.NODE_ENV': JSON.stringify('development'),\n        SAKA_DEBUG: JSON.stringify(true),\n        SAKA_VERSION: JSON.stringify(`${version} dev`),\n        SAKA_PLATFORM: JSON.stringify(platform),\n        SAKA_BENCHMARK: JSON.stringify(benchmark === 'benchmark')\n      })\n    ]);\n  }\n  return config;\n};\n"
  }
]