[
  {
    "path": ".browserslistrc",
    "content": "Firefox > 67\nChrome >= 63\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n  env: {\n    browser: true,\n    node: true,\n    'jest/globals': true\n  },\n  extends: [\n    'standard',\n    'plugin:prettier/recommended',\n    'plugin:react/recommended'\n  ],\n  plugins: ['@typescript-eslint', 'jest'],\n  parser: '@typescript-eslint/parser',\n  rules: {\n    '@typescript-eslint/adjacent-overload-signatures': 'error',\n    '@typescript-eslint/no-unused-vars': [\n      'error',\n      { args: 'none', ignoreRestSiblings: true }\n    ],\n    'dot-notation': 'off',\n    'import/first': 'off',\n    'import/no-webpack-loader-syntax': 'off',\n    'no-dupe-class-members': 'off',\n    'no-unused-vars': 'off',\n    'no-useless-return': 'off',\n    'prefer-promise-reject-errors': 'off',\n    'prettier/prettier': ['error', { singleQuote: true, semi: false }],\n    'react/display-name': 'off',\n    'react/prop-types': 'off',\n    'standard/computed-property-even-spacing': 'off',\n    'standard/no-callback-literal': 'off',\n    camelcase: 'off',\n    yoda: 'off'\n  },\n  globals: {\n    browser: true\n  },\n  settings: {\n    react: {\n      version: 'detect'\n    }\n  }\n}\n"
  },
  {
    "path": ".gitattributes",
    "content": "*.min.js binary\n/public/** binary\n\n# For Github language details\n/test/**/response/*.html linguist-vendored\n/public/** linguist-vendored\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: saladict\nopen_collective: # Replace with a single Open Collective username\nko_fi: saladict\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: ['https://saladict.crimx.com/support.html']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/_bug_report_chs.md",
    "content": "---\nname: 反馈 Bug\nabout: 沙拉查词运行出现不正确行为。\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n<!--\n反馈前请阅读\n\n- 使用说明： https://saladict.crimx.com/manual.html\n- 常见问题以及答复： https://saladict.crimx.com/q&a.html\n- 请先在 issues 页面搜索你的问题，很可能已被解决。\n-->\n\n<!-- 这是隐藏的信息 -->\n<!-- 👆这样括起来的信息将被隐藏，填写时注意不要写在里面。 -->\n\n<!-- 点击编辑器上方的 preview 可预览效果 -->\n\n<!--\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n\n（重要事情已经说了十遍😅）\n-->\n\n## 设备信息\n- 操作系统: [] <!-- 如 [Win10] -->\n- 浏览器版本: [] <!-- 如 [Chrome77] -->\n- 沙拉查词版本: [] <!-- 如 [v7.0.0] （在沙拉查词设置页面左上角查看） -->\n\n<!-- 请在下方 ## 开头行之间的空白处填写，点击编辑器上方的 preview 预览效果 -->\n\n## 描述问题\n<!-- 客观描述出现了什么问题 -->\n\n\n\n## 复现步骤\n<!--\n如何重复触发这个不正确的行为，如：\n\n1. 打开某某某......\n2. 点击某某某......\n3. 滚动到某某某......\n4. 问题出现\n\n请提供具体页面和具体操作，而不是「任意页面」「选任一单词」，即便问题确实在多处出现。\n-->\n\n\n\n## 期待的正常行为\n<!-- 请描述正常情况下应该出现什么结果 -->\n\n\n\n## 截图\n<!-- 可选，需要情况下，可借助截图描述问题 -->\n\n\n\n## 额外信息\n<!-- 可选，更多有助于理解问题的描述和资料 -->\n\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/_feature_request_chs.md",
    "content": "---\nname: 功能建议\nabout: 请求实现新功能或改进已有功能。\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n<!--\n反馈前请阅读\n\n- 使用说明： https://saladict.crimx.com/manual.html\n- 常见问题以及答复： https://saladict.crimx.com/q&a.html\n- 请先在 issues 页面搜索你的问题，很可能已被解决。\n-->\n\n<!-- 这是隐藏的信息 -->\n<!-- 👆这样括起来的信息将被隐藏，填写时注意不要写在里面。 -->\n\n<!-- 点击编辑器上方的 preview 可预览效果 -->\n\n<!--\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n⚠️请_完整_填写以下模板描述问题，否则反馈将会被系统关闭。\n\n（重要事情已经说了十遍😅）\n-->\n\n## 设备信息\n- 操作系统: [] <!-- 如 [Window10] -->\n- 浏览器版本: [] <!-- 如 [Chrome77] -->\n- 沙拉查词版本: [] <!-- 如 [v7.0.0] -->\n\n<!-- 请在下方 ## 开头行之间的空白处填写，点击编辑器上方的 preview 预览效果 -->\n\n## 请描述目前使用沙拉查词遇到什么不便\n<!-- 清晰描述使用过程中遇到的问题 -->\n\n\n\n## 理想情况下，沙拉查词应该怎么做\n<!-- 清晰描述期待发生的行为 -->\n\n\n\n## 替代方案\n<!-- (可选)如果你已经有了能用的替代方案，或者对沙拉查词具体如何实现有建议 -->\n\n\n\n## 额外信息\n<!-- (可选)更多有助于理解问题的描述和资料 -->\n\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/_new_dict_chs.md",
    "content": "---\nname: 词典推荐\nabout: 请求沙拉查词添加新词典。\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n<!--\n- 使用说明： https://saladict.crimx.com/manual.html\n- 常见问题以及答复： https://saladict.crimx.com/q&a.html\n- 在 issues 页面搜索你的问题，很可能已被解决。\n\n目前沙拉查词已经支持了大量主流词典，推荐前请请在沙拉查词设置中查看是否已经支持。\n\n每维护多一个词典都需要不少工作量，故功能雷同的词典不会轻易考虑添加，请完整填写下方模板并提供充分的推荐理由！\n-->\n\n<!-- 这是隐藏的信息 -->\n<!-- 👆这样括起来的信息将被隐藏，填写时注意不要写在里面。 -->\n\n<!-- 请在下方 ## 开头行之间的空白处填写，点击编辑器上方的 preview 预览效果 -->\n\n## 词典名称以及链接\n\n\n\n## 沙拉查词的已有的词典为什么不能满足？\n<!-- 推荐词典有什么特殊功能是当前词典无法满足的 -->\n\n\n\n## 单词举例\n<!--\n列出几个当前词典无法找到释义的单词，或者能体现推荐词典特性的单词\n\n- 单词1，在已有词典下无法找到 xxxx 的用法。\n- 单词2，推荐词典可以显示 xxxx 功能。\n-->\n\n\n\n## 截图\n<!-- 需要情况下，可借助截图描述问题 -->\n\n\n\n## 额外信息\n<!-- 更多有助于理解问题的描述和资料 -->\n\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Bug related issue.\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n## Device info\n - OS: [e.g. Window10]\n - Browser Version [e.g. Chrome77]\n - Saladict Version [e.g. v7.0.0]\n\n## Describe the bug\n<!-- A clear and concise description of what the bug is. -->\n\n## To Reproduce\n<!-- Steps to reproduce the behavior: -->\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n## Expected behavior\n<!-- A clear and concise description of what you expected to happen. -->\n\n## Screenshots\n<!-- If applicable, add screenshots to help explain your problem. -->\n\n## Additional context\n<!-- Add any other context about the problem here. -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for Saladict\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\n<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->\n\n**Describe the solution you'd like**\n<!-- A clear and concise description of what you want to happen. -->\n\n**Describe alternatives you've considered**\n<!-- A clear and concise description of any alternative solutions or features you've considered. -->\n\n**Additional context**\n<!-- Add any other context or screenshots about the feature request here. -->\n"
  },
  {
    "path": ".github/config.yml",
    "content": "# Configuration for request-info - https://github.com/behaviorbot/request-info\n\n# *OPTIONAL* Comment to reply with\n# Can be either a string :\nrequestInfoReplyComment: |\n  请填写模板描述问题，以便别人理解、定位和解决问题。\n\n  We would appreciate it if you could provide more info about this issue.\n\n# Or an array:\n# requestInfoReplyComment:\n#  - Ah no! young blade! That was a trifle short!\n#  - Tell me more !\n#  - I am sure you can be more effusive\n\n# *OPTIONAL* default titles to check against for lack of descriptiveness\n# MUST BE ALL LOWERCASE\n# requestInfoDefaultTitles:\n#   - update readme.md\n#   - updates\n\n# *OPTIONAL* Label to be added to Issues and Pull Requests with insufficient information given\nrequestInfoLabelToAdd: needs-more-info\n\n# *OPTIONAL* Require Issues to contain more information than what is provided in the issue templates\n# Will fail if the issue's body is equal to a provided template\ncheckIssueTemplate: true\n\n# *OPTIONAL* Require Pull Requests to contain more information than what is provided in the PR template\n# Will fail if the pull request's body is equal to the provided template\ncheckPullRequestTemplate: false\n\n# *OPTIONAL* Only warn about insufficient information on these events type\n# Keys must be lowercase. Valid values are 'issue' and 'pullRequest'\nrequestInfoOn:\n  pullRequest: false\n  issue: true\n# *OPTIONAL* Add a list of people whose Issues/PRs will not be commented on\n# keys must be GitHub usernames\n# requestInfoUserstoExclude:\n#   - hiimbex\n#   - bexo\n"
  },
  {
    "path": ".github/no-response.yml",
    "content": "# Configuration for probot-no-response - https://github.com/probot/no-response\n\n# Number of days of inactivity before an Issue is closed for lack of response\ndaysUntilClose: 14\n# Label requiring a response\nresponseRequiredLabel: needs-more-info\n# Comment to post when closing an Issue for lack of response. Set to `false` to disable\ncloseComment: >\n  This issue has been automatically closed because there has been no response\n  to our request for more information from the original author. With only the\n  information that is currently in the issue, we don't have enough information\n  to take action. Please reach out if you have or find the answers we need so\n  that we can investigate further.\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n\n# next.js build output\n.next\n\n# nuxt.js build output\n.nuxt\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# Neutrino build directory\nbuild\n\nassets/pdf/\ndeps/\n\ntest/**/response\n.idea\n\n.webext_tmp/*\n\n\n**/.DS_Store\n"
  },
  {
    "path": ".neutrinorc.js",
    "content": "const path = require('path')\nconst fs = require('fs')\nconst webpack = require('webpack')\nconst react = require('@neutrinojs/react')\nconst copy = require('@neutrinojs/copy')\nconst jest = require('@neutrinojs/jest')\nconst wext = require('neutrino-webextension')\nconst { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')\nconst MomentLocalesPlugin = require('moment-locales-webpack-plugin')\nconst dotenv = require('dotenv')\nconst argv = require('yargs').argv\nconst AfterBuildPlugin = require('./scripts/after-build')\nconst svgToMiniDataURI = require('mini-svg-data-uri')\nconst isAnalyze = argv.analyze || argv.analyse\n\nmodule.exports = {\n  options: {\n    mains: {\n      content: {\n        entry: 'content',\n        webext: {\n          type: 'content_scripts',\n          manifest: {\n            css: ['assets/content.css'],\n            matches: ['<all_urls>']\n          },\n          setup: 'content/__fake__/env.ts'\n        }\n      },\n\n      selection: {\n        entry: 'selection',\n        webext: {\n          type: 'content_scripts',\n          manifest: {\n            match_about_blank: true,\n            all_frames: true,\n            matches: ['<all_urls>']\n          }\n        }\n      },\n\n      popup: {\n        entry: 'popup',\n        webext: {\n          type: 'browser_action',\n          manifest: {\n            default_icon: {\n              '16': 'assets/icon-16.png',\n              '19': 'assets/icon-19.png',\n              '24': 'assets/icon-24.png',\n              '38': 'assets/icon-38.png',\n              '48': 'assets/icon-48.png',\n              '128': 'assets/icon-128.png'\n            }\n          },\n          setup: 'popup/__fake__/env.ts'\n        }\n      },\n\n      options: {\n        entry: 'options',\n        webext: {\n          type: 'options_ui',\n          manifest: {\n            open_in_tab: true\n          },\n          setup: 'options/__fake__/env.ts'\n        }\n      },\n\n      background: {\n        entry: 'background',\n        webext: {\n          type: 'background',\n          setup: 'background/__fake__/env.ts'\n        }\n      },\n\n      notebook: {\n        entry: 'notebook'\n      },\n\n      history: {\n        entry: 'history'\n      },\n\n      'quick-search': {\n        entry: 'quick-search'\n      },\n\n      'word-editor': {\n        entry: 'word-editor'\n      },\n\n      'audio-control': {\n        entry: 'audio-control'\n      }\n    }\n  },\n  use: [\n    react({\n      html: {\n        title: 'Saladict'\n      },\n      image: false,\n      style: {\n        test: /\\.(css|scss)$/,\n        modulesTest: /\\.module\\.(css|scss)$/,\n        loaders: [\n          // Define loaders as objects. Note: loaders must be specified in reverse order.\n          // ie: for the loaders below the actual execution order would be:\n          // input file -> sass-loader -> postcss-loader -> css-loader -> style-loader/mini-css-extract-plugin\n          {\n            loader: 'postcss-loader',\n            options: {\n              plugins: [require('autoprefixer')]\n            },\n            useId: 'postcss'\n          },\n          {\n            loader: 'sass-loader',\n            useId: 'scss'\n          },\n          {\n            loader: 'sass-resources-loader',\n            useId: 'sass-resources',\n            options: {\n              sourceMap: process.env.NODE_ENV !== 'production',\n              resources: [\n                path.join(__dirname, 'src/_sass_shared/_namespace.scss'),\n                ...fs\n                .readdirSync(path.join(__dirname, 'src/_sass_shared/_global/'))\n                .map(filename =>\n                  path.join(__dirname, 'src/_sass_shared/_global/', filename)\n                )\n              ]\n            }\n          }\n        ]\n      },\n      babel: {\n        presets: [\n          [\n            '@babel/preset-env',\n            {\n              /* remove targets set by neutrino web preset preferring browserslistrc */\n            }\n          ],\n          [\n            '@babel/preset-typescript',\n            {\n              isTSX: true,\n              allExtensions: true\n            }\n          ]\n        ],\n        plugins: [\n          [\n            'import',\n            {\n              libraryName: 'antd'\n            },\n            'antd'\n          ],\n          [\n            'import',\n            {\n              libraryName: '@ant-design/icons',\n              libraryDirectory: '',\n              camel2DashComponentName: false,\n              style: false\n            },\n            '@ant-design/icons'\n          ]\n        ]\n      }\n    }),\n    copy({\n      patterns: [\n        { context: 'assets', from: '**/*', to: 'assets/', toType: 'dir' },\n        {\n          context: 'src/_locales/manifest',\n          from: '**/*',\n          to: '_locales/',\n          toType: 'dir'\n        },\n        {\n          context: 'node_modules/antd/dist/',\n          from: '+(antd|antd.dark).min.css',\n          to: 'assets/',\n          toType: 'dir'\n        },\n        // caiyunapp\n        {\n          context: 'node_modules/trsjs/build/sala',\n          from: 'trs.js',\n          to: 'assets/',\n          toType: 'dir'\n        }\n      ]\n    }),\n    neutrino => {\n      /* eslint-disable indent */\n\n      // images\n      neutrino.config.module.rules\n        .delete('image')\n        .end()\n        .rule('svg')\n        .test(/\\.(svg)(\\?v=\\d+\\.\\d+\\.\\d+)?$/)\n        .use('svg-url')\n        .loader(require.resolve('url-loader'))\n        .options({\n          limit: 8192,\n          // remove `default` when `require` image\n          // due to legacy code\n          esModule: false,\n          generator: content => svgToMiniDataURI(content.toString())\n        })\n        .end()\n        .end()\n        .rule('pixel')\n        .test(/\\.(ico|png|jpg|jpeg|gif|webp)(\\?v=\\d+\\.\\d+\\.\\d+)?$/)\n        .use('img-url')\n        .loader(require.resolve('file-loader'))\n        .options({\n          // dev-server image name collision\n          name: resourcePath => {\n            if (process.env.NODE_ENV === 'development') {\n              return '[path]/[name].[ext]'\n            }\n\n            const dictMatch = /\\/dictionaries\\/([^/]+)\\/favicon.png/.exec(\n              resourcePath\n            )\n            if (dictMatch) {\n              return `assets/favicon-${dictMatch[1]}.[contenthash:8].[ext]`\n            }\n\n            return 'assets/[name].[contenthash:8].[ext]'\n          },\n          limit: 0,\n          esModule: false\n        })\n\n      // avoid collision\n      neutrino.config.output.jsonpFunction('saladictEntry')\n\n      // transform *.shadow.(css|scss) to string\n      // this will be injected into shadow-dom style tag\n      // prettier-ignore\n      const shadowStyleRules = neutrino.config.module\n        .rule('style')\n          .oneOf('shadow')\n            .before('normal')\n            .test(/\\.shadow\\.(css|scss)$/)\n              .use('tostring')\n                .loader('to-string-loader')\n                .end()\n              .use('minify')\n              .after('css')\n                .loader('clean-css-loader')\n                .options({\n                  level: 1,\n                })\n                .end()\n      // copy loaders from normal to shadow\n      // prettier-ignore\n      neutrino.config.module\n        .rule('style')\n          .oneOf('normal')\n            .uses.values()\n              .filter(rule => !/^(extract|style)$/.test(rule.name))\n              .forEach(rule => {\n                shadowStyleRules\n                  .use(rule.name)\n                    .loader(rule.get('loader'))\n                    .options(rule.get('options'))\n              })\n\n      // prettier-ignore\n      neutrino.config\n        .module\n          .rule('compile') // add ts extensions for babel ect\n            .test(/\\.(mjs|jsx|js|ts|tsx)$/)\n            .end()\n          .end()\n        .resolve\n          .extensions // typescript extensions\n            .add('.ts')\n            .add('.tsx')\n            .end()\n          .alias // '@' src alias\n            .set('@', path.join(__dirname, 'src'))\n            .end()\n          .end()\n\n      // remove locales\n      neutrino.config\n        .plugin('momentjs')\n          .use(MomentLocalesPlugin, [{ localesToKeep: ['zh-cn', 'zh-tw'] }])\n          .end()\n\n      // prettier-ignore\n      neutrino.config\n        .plugin('process.env')\n          .use(webpack.DefinePlugin, [{\n            'process.env': JSON.stringify(Object.assign(\n                { DEBUG: !!argv.debug },\n                dotenv.config().parsed\n              ))\n          }])\n      /* eslint-enable indent */\n\n      if (argv.mode === 'production') {\n        // prettier-ignore\n        neutrino.config\n          .performance\n            .hints(false)\n            .end()\n          .optimization\n            .merge({\n              splitChunks: {\n                cacheGroups: {\n                  react: {\n                    test: /[\\\\/]node_modules[\\\\/](react|react-dom|i18next)[\\\\/]/,\n                    name: 'view-vendor',\n                    chunks: 'all',\n                    priority: 100\n                  },\n                  franc: {\n                    test: /[\\\\/]node_modules[\\\\/]franc/,\n                    name: 'franc',\n                    chunks: 'all',\n                    priority: 100\n                  },\n                  dexie: {\n                    test: /[\\\\/]node_modules[\\\\/]dexie/,\n                    name: 'dexie',\n                    chunks: 'all',\n                    priority: 100\n                  },\n                  wordpage: {\n                    test: (module, chunks) => module.resource &&\n                      module.resource.includes(`${path.sep}src${path.sep}`) &&\n                      !module.resource.includes(`${path.sep}node_modules${path.sep}`),\n                    name: 'wordpage',\n                    chunks: ({ name }) => /^(notebook|history)$/.test(name),\n                  },\n                  antd: {\n                    test: /[\\\\/]node_modules[\\\\/]/,\n                    name: 'antd',\n                    chunks: ({ name }) => /^(options|notebook|history)$/.test(name),\n                  }\n                }\n              },\n            })\n      }\n\n      if (argv.debug) {\n        // prettier-ignore\n        neutrino.config\n          .devtool('inline-source-map')\n          .optimization\n            .minimize(false)\n      }\n\n      if (isAnalyze) {\n        // prettier-ignore\n        neutrino.config\n          .plugin('bundle-analyze')\n          .use(BundleAnalyzerPlugin);\n      }\n    },\n    jest({\n      testRegex: ['test/specs/.*\\\\.spec\\\\.(ts|tsx|js|jsx)'],\n      setupFilesAfterEnv: ['<rootDir>/config/jest/setupTests.js'],\n      moduleNameMapper: {\n        '^@/(.*)$': '<rootDir>/src/$1'\n      },\n      transform: {\n        '\\\\.(mjs|jsx|js|ts|tsx)$': require.resolve(\n          '@neutrinojs/jest/src/transformer'\n        )\n      },\n      testTimeout: 20000\n    }),\n    wext({\n      polyfill: true\n    }),\n    neutrino => {\n      // prettier-ignore\n      neutrino.config\n        .plugin('after-build')\n        .use(AfterBuildPlugin);\n    }\n  ]\n}\n"
  },
  {
    "path": ".prettierrc",
    "content": "tabWidth: 2\nsemi: false\nsingleQuote: true\n"
  },
  {
    "path": ".storybook/addons.ts",
    "content": "import '@storybook/addon-knobs/register'\nimport '@storybook/addon-contexts/register'\nimport '@storybook/addon-actions/register'\nimport '@storybook/addon-backgrounds/register'\nimport 'storybook-addon-jsx/register'\nimport 'storybook-addon-react-docgen/register'\n\nimport addons from '@storybook/addons'\nimport { STORY_RENDERED } from '@storybook/core-events'\n\naddons.register('TitleAddon', api => {\n  api.on(STORY_RENDERED, () => {\n    const storyData = api.getCurrentStoryData()\n    document.title = `${storyData.name} - Saladict Storybook`\n  })\n})\n"
  },
  {
    "path": ".storybook/config.ts",
    "content": "import {\n  configure,\n  addDecorator,\n  StoryDecorator,\n  addParameters\n} from '@storybook/react'\nimport { withContexts } from '@storybook/addon-contexts/react'\nimport { i18nContexts } from './configs/contexts'\nimport { StyleWrap } from '../src/_helpers/storybook'\n\nimport './style.css'\n\naddParameters({\n  options: {\n    // bug https://github.com/storybookjs/storybook/issues/6569\n    enableShortcuts: false\n  },\n  props: {\n    propTablesExclude: [StyleWrap],\n    styles: styles => ({\n      ...styles,\n      infoBody: {\n        ...styles.infoBody,\n        marginTop: 0,\n        padding: '0 40px'\n      },\n      propTableHead: {\n        ...styles.propTableHead,\n        margin: 0\n      },\n      h1: {\n        display: 'none'\n      }\n    })\n  },\n  jsx: {\n    functionValue: (fn: Function) => `${fn.name}()`\n  }\n})\n\n// place after the info addon so that wrappers get removed\naddDecorator(withContexts(i18nContexts) as StoryDecorator)\n\nfunction loadStories() {\n  const req = require.context('../src', true, /\\.stories\\.tsx$/)\n  let files = req.keys()\n  if (process.env.STORYBOOK_PATH_PATTERN) {\n    const tester = new RegExp(process.env.STORYBOOK_PATH_PATTERN)\n    files = files.filter(filename => tester.test(filename))\n  }\n  files.forEach(filename => req(filename))\n}\n\nconfigure(loadStories, module)\n"
  },
  {
    "path": ".storybook/configs/contexts.tsx",
    "content": "import React, { FC, useContext, useEffect } from 'react'\nimport {\n  I18nContextProvider,\n  I18nContext,\n  i18nLoader\n} from '../../src/_helpers/i18n'\nimport i18next from 'i18next'\n\ninterface I18nWrapProps {\n  lang: string\n}\n\nconst I18nWrapInner: FC<I18nWrapProps> = props => {\n  const lang = useContext(I18nContext)\n  useEffect(() => {\n    if (lang) {\n      if (lang && props.lang !== lang) {\n        i18next.changeLanguage(props.lang)\n      }\n    } else {\n      i18nLoader()\n    }\n  }, [lang, props.lang])\n  return <>{props.children}</>\n}\n\nconst I18nWrap: FC<I18nWrapProps> = props => (\n  <I18nContextProvider>\n    <I18nWrapInner {...props}>{props.children}</I18nWrapInner>\n  </I18nContextProvider>\n)\n\nexport const i18nContexts = [\n  {\n    // https://storybooks-official.netlify.com/?path=/story/basics-icon--labels\n    icon: 'globe',\n    title: 'i18n',\n    components: [I18nWrap],\n    params: [\n      {\n        name: 'English',\n        props: { lang: 'en' },\n        default: 'en' === navigator.language\n      },\n      {\n        name: '简体中文',\n        props: { lang: 'zh-CN' },\n        default: 'zh-CN' === navigator.language\n      },\n      {\n        name: '繁体中文',\n        props: { lang: 'zh-TW' },\n        default: 'zh-TW' === navigator.language\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": ".storybook/manager-head.html",
    "content": "<style>\n  /* disable upgrade floaty */\n  [href='/?path=/settings/about'] {\n    display: none !important;\n  }\n</style>\n"
  },
  {
    "path": ".storybook/preview-head.html",
    "content": "<style>\n  #story-root {\n    min-height: 100px;\n  }\n</style>\n"
  },
  {
    "path": ".storybook/style.css",
    "content": "body {\n  width: unset;\n  height: unset;\n  overflow-y: scroll;\n  margin: 0;\n  padding: 0;\n  box-sizing: border-box;\n}\n"
  },
  {
    "path": ".storybook/webpack.config.js",
    "content": "const path = require('path')\nconst fs = require('fs')\nconst Neutrino = require('neutrino/Neutrino')\nconst neutrinorc = require('../.neutrinorc.js')\nconst neutrino = new Neutrino(neutrinorc.options)\nneutrinorc.use.forEach(middleware => neutrino.use(middleware))\n\nconst babelOptions = neutrino.config.module\n  .rule('compile')\n  .use('babel')\n  .get('options')\n\n// babelOptions.plugins.push([\n//   'babel-plugin-react-docgen-typescript',\n//   {\n//     docgenCollectionName: 'STORYBOOK_REACT_CLASSES',\n//     include: 'components.*\\\\.tsx$',\n//     exclude: '__mocks__|(\\\\.stories\\\\.tsx$)'\n//   }\n// ])\n\nconst sassGlobals = [\n  path.join(__dirname, '../src/_sass_shared/_namespace.scss'),\n  ...fs\n    .readdirSync(path.join(__dirname, '../src/_sass_shared/_global/'))\n    .map(filename => path.join(__dirname, '../src/_sass_shared/_global/', filename))\n]\n\nmodule.exports = ({ config }) => {\n  config.module.rules.push({\n    test: /\\.mjs$/,\n    type: 'javascript/auto'\n  })\n  config.module.rules.push({\n    test: /\\.(ts|tsx)$/,\n    use: [\n      {\n        loader: require.resolve('babel-loader'),\n        options: babelOptions\n      },\n      {\n        loader: require.resolve('react-docgen-typescript-loader'),\n        options: {\n          tsconfigPath: path.join(__dirname, '../tsconfig.json')\n        }\n      }\n    ]\n  })\n  config.module.rules.push({\n    oneOf: [\n      {\n        test: /\\.module\\.(css|scss)$/,\n        use: [\n          'to-string-loader',\n          {\n            loader: 'css-loader',\n            options: {\n              importLoaders: 2,\n              modules: true\n            }\n          },\n          {\n            loader: 'postcss-loader',\n            options: {\n              plugins: [require('autoprefixer')]\n            }\n          },\n          'sass-loader'\n        ],\n        include: path.resolve(__dirname, '../src')\n      },\n      {\n        test: /\\.shadow\\.(css|scss)$/,\n        use: [\n          'to-string-loader',\n          {\n            loader: 'css-loader',\n            options: {\n              importLoaders: 2\n            }\n          },\n          {\n            loader: 'clean-css-loader',\n            options: {\n              level: 1\n            }\n          },\n          {\n            loader: 'postcss-loader',\n            options: {\n              plugins: [require('autoprefixer')]\n            }\n          },\n          'sass-loader',\n          {\n            loader: 'sass-resources-loader',\n            options: {\n              sourceMap: true,\n              resources: sassGlobals\n            }\n          }\n        ],\n        include: path.resolve(__dirname, '../src')\n      },\n      {\n        test: /\\.(css|scss)$/,\n        use: [\n          'to-string-loader',\n          {\n            loader: 'css-loader',\n            options: {\n              importLoaders: 2\n            }\n          },\n          {\n            loader: 'postcss-loader',\n            options: {\n              plugins: [require('autoprefixer')]\n            }\n          },\n          'sass-loader',\n          {\n            loader: 'sass-resources-loader',\n            options: {\n              sourceMap: true,\n              resources: sassGlobals\n            }\n          }\n        ],\n        include: path.resolve(__dirname, '../src')\n      }\n    ]\n  })\n\n  if (Array.isArray(config.entry)) {\n    config.entry.unshift('webextensions-emulator/dist/core')\n  } else {\n    Object.keys(config.entry).forEach(id => {\n      if (!Array.isArray(config.entry[id])) {\n        config.entry[id] = [config.entry[id]]\n      }\n      config.entry[id].unshift('webextensions-emulator/dist/core')\n    })\n  }\n\n  config.resolve.extensions.push('.ts', '.tsx')\n  config.resolve.alias['@'] = path.join(__dirname, '../src')\n  config.resolve.alias['@sb'] = path.join(__dirname)\n\n  return config\n}\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n  - 'stable'\nscript:\n  - yarn lint\n  - yarn build\n  # remove agnostic tests\n  - yarn test --testPathIgnorePatterns 'components/dictionaries'\n"
  },
  {
    "path": ".vscode/locales.schema.json",
    "content": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"name\": {\n      \"$ref\": \"#/definitions/locale\"\n    },\n    \"options\": {\n      \"$ref\": \"#/definitions/locales\"\n    },\n    \"helps\": {\n      \"$ref\": \"#/definitions/locales\"\n    }\n  },\n  \"required\": [\"name\"],\n  \"additionalProperties\": false,\n\n  \"definitions\": {\n    \"locale\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"en\": { \"type\": \"string\" },\n        \"zh-CN\": { \"type\": \"string\" },\n        \"zh-TW\": { \"type\": \"string\" }\n      },\n      \"required\": [\"en\", \"zh-CN\", \"zh-TW\"],\n      \"additionalProperties\": false\n    },\n    \"locales\": {\n      \"type\": \"object\",\n      \"patternProperties\": {\n        \".+\": {\n          \"$ref\": \"#/definitions/locale\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"json.schemas\": [\n    {\n      \"fileMatch\": [\"src/components/dictionaries/**/_locales.json\"],\n      \"url\": \".vscode/locales.schema.json\"\n    }\n  ],\n  \"files.watcherExclude\": {\n    \"**/.git/objects/**\": true,\n    \"**/.git/subtree-cache/**\": true,\n    \"**/node_modules/*/**\": true,\n    \"**/build/*/**\": true,\n    \"**/assets/*/**\": true\n  },\n  \"conventionalCommits.scopes\": [\n    \"audio-control\",\n    \"background\",\n    \"components\",\n    \"config\",\n    \"dicts\",\n    \"history\",\n    \"locales\",\n    \"notebook\",\n    \"options\",\n    \"panel\",\n    \"popup\",\n    \"selecion\",\n    \"sync-services\",\n    \"word-editor\"\n  ]\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.\n\n## [7.20.0](https://github.com/crimx/ext-saladict/compare/v7.19.1...v7.20.0) (2021-10-17)\n\n\n### Features\n\n* **pdf:** inject vimium-c ([#1462](https://github.com/crimx/ext-saladict/issues/1462)) ([#1463](https://github.com/crimx/ext-saladict/issues/1463)) ([029c07a](https://github.com/crimx/ext-saladict/commit/029c07a0801177bf9d8137163fc43f70fe1e7a30))\n* **sync-services:** add eudic ([#1467](https://github.com/crimx/ext-saladict/issues/1467)) ([452bf53](https://github.com/crimx/ext-saladict/commit/452bf537a4aee7d1e3d79b7960cd5183cdff88af))\n* **wordpage:** add context cloze ([b910769](https://github.com/crimx/ext-saladict/commit/b91076941940dc82961830f43dcb9ee081596aa4))\n* add Oxford Learner's Dict ([#1458](https://github.com/crimx/ext-saladict/issues/1458)) ([aaffe00](https://github.com/crimx/ext-saladict/commit/aaffe00d760adb44676042142a5ce2af34ce5001)), closes [#1253](https://github.com/crimx/ext-saladict/issues/1253)\n\n\n### Bug Fixes\n\n* **dictpanel:** remove waveform box if option is off ([e979c1c](https://github.com/crimx/ext-saladict/commit/e979c1c62fb022f23e4e3edc600e5d0abee830c1))\n* **dicts:** fix srcset protocol ([21a0032](https://github.com/crimx/ext-saladict/commit/21a0032a3acb6b25cc5444409218b689176a9cb1)), closes [#1366](https://github.com/crimx/ext-saladict/issues/1366)\n* **hot-words:** remove daily hot words of urban dict ([#1428](https://github.com/crimx/ext-saladict/issues/1428)) ([5dc29cd](https://github.com/crimx/ext-saladict/commit/5dc29cd61fc181929804dcf4d1cd4337289a062b))\n* **notebook:** make export panel textarea background transparent ([09bddc0](https://github.com/crimx/ext-saladict/commit/09bddc0579f7844b4688aca4e6e60e409407d292))\n* **panel:** pin panel by default ([e4ee931](https://github.com/crimx/ext-saladict/commit/e4ee931efe04f13946a1883425ced8fca7f52931)), closes [#1505](https://github.com/crimx/ext-saladict/issues/1505)\n* **selecion:** check range count before getting range ([dfc46a0](https://github.com/crimx/ext-saladict/commit/dfc46a0378b3c71bbc9f0febf5d2b0058fdb3826)), closes [#1144](https://github.com/crimx/ext-saladict/issues/1144)\n\n### [7.19.1](https://github.com/crimx/ext-saladict/compare/v7.19.0...v7.19.1) (2021-07-25)\n\n\n### Bug Fixes\n\n* **no-typefield:** add support for content editable ([#1334](https://github.com/crimx/ext-saladict/issues/1334)) ([7984991](https://github.com/crimx/ext-saladict/commit/7984991e0e7adfb0b5fa16d604ffa3da6be65a40))\n* **pdf:** update pdf dir ([a8df19f](https://github.com/crimx/ext-saladict/commit/a8df19f12bcc5000eedaf40f6c0d3e0438386371))\n\n## [7.19.0](https://github.com/crimx/ext-saladict/compare/v7.18.2...v7.19.0) (2021-05-23)\n\n\n### Features\n\n* **dict-panel:** add a option to pin panel by default ([#1296](https://github.com/crimx/ext-saladict/issues/1296)) ([bbaeb76](https://github.com/crimx/ext-saladict/commit/bbaeb76c77d62e18e3d6a0af52d7bad983dbcd69))\n\n\n### Bug Fixes\n\n* **fixtures:** cnki url ([0fd0125](https://github.com/crimx/ext-saladict/commit/0fd012596c4e82e5acf7fa91469bdb78209fbbbc))\n\n### [7.18.2](https://github.com/crimx/ext-saladict/compare/v7.18.1...v7.18.2) (2021-05-23)\n\n\n### Bug Fixes\n\n* **dict-panel:** move root el to document element ([aaeae1c](https://github.com/crimx/ext-saladict/commit/aaeae1cc6f4ea9ad43e2042b8b670f8480aaefb1)), closes [#1293](https://github.com/crimx/ext-saladict/issues/1293) [#1190](https://github.com/crimx/ext-saladict/issues/1190) [#474](https://github.com/crimx/ext-saladict/issues/474) [#421](https://github.com/crimx/ext-saladict/issues/421) [#398](https://github.com/crimx/ext-saladict/issues/398) [#278](https://github.com/crimx/ext-saladict/issues/278)\n* **dict-panel:** prevent input method conflict on first input ([e4dda57](https://github.com/crimx/ext-saladict/commit/e4dda573263f77ad9bfbf10c577971b9a9631fb8)), closes [#1149](https://github.com/crimx/ext-saladict/issues/1149)\n* **dicts:** fix zdic icon ([9aea435](https://github.com/crimx/ext-saladict/commit/9aea435fc8f8141d448d2f8309fef3d9872ea75a)), closes [#1244](https://github.com/crimx/ext-saladict/issues/1244)\n\n### [7.18.1](https://github.com/crimx/ext-saladict/compare/v7.18.0...v7.18.1) (2021-01-18)\n\n\n### Bug Fixes\n\n* **fixtures:** read url of undefined ([52e209d](https://github.com/crimx/ext-saladict/commit/52e209d2ca55715bdc5f574ae9f10420ce87306b))\n\n## [7.18.0](https://github.com/crimx/ext-saladict/compare/v7.17.0...v7.18.0) (2020-11-02)\n\n\n### Features\n\n* **panel:** add shortcut for switch search history ([0a61c49](https://github.com/crimx/ext-saladict/commit/0a61c4982a0232daaecf9c9c5573858a2a2cd1a1)), closes [#1063](https://github.com/crimx/ext-saladict/issues/1063)\n* add options for browser action panel width and height ([fab67dd](https://github.com/crimx/ext-saladict/commit/fab67dd5d82a1fc13d15886d92b5e03f518c434f)), closes [#983](https://github.com/crimx/ext-saladict/issues/983)\n\n\n### Bug Fixes\n\n* **background:** query error on http url ([1922c09](https://github.com/crimx/ext-saladict/commit/1922c0914f46c306ce905d87b298b5ccd7e15e5a))\n* **dicts:** sogou access token is required Closes [#1011](https://github.com/crimx/ext-saladict/issues/1011) ([a449119](https://github.com/crimx/ext-saladict/commit/a449119ae1643b2cb19b23e68aa1a998382c6cb9))\n* **options:** hide unsupported features on Firefox ([516e030](https://github.com/crimx/ext-saladict/commit/516e03048525f426ac9145db96866fe4673ee25c)), closes [#1062](https://github.com/crimx/ext-saladict/issues/1062)\n\n## [7.17.0](https://github.com/crimx/ext-saladict/compare/v7.15.1...v7.17.0) (2020-09-08)\n\n\n### Features\n\n* add lingocloud browser shortcut ([badd839](https://github.com/crimx/ext-saladict/commit/badd839a0c40602a79337b646a0b6baa6f66f834))\n* add Lingocloud trs ([fe75c7a](https://github.com/crimx/ext-saladict/commit/fe75c7a6c9431aeeaa63138b4fd1c1eff153124f))\n\n\n### Bug Fixes\n\n* **dicts:** add google tts ([bbbe0a1](https://github.com/crimx/ext-saladict/commit/bbbe0a10a6def4608ff850e849005997abb37ad6))\n* **options:** fix dict title overflows on small screens ([54d4fe3](https://github.com/crimx/ext-saladict/commit/54d4fe30ddd0a62f0c41bc7423bf0eb3c3a9309e))\n* **options:** replace react-sortable-hoc with react-beautiful-dnd ([7e3d0c7](https://github.com/crimx/ext-saladict/commit/7e3d0c744f1d1bc18c8778460f811f2a55d029a3)), closes [#966](https://github.com/crimx/ext-saladict/issues/966)\n* **panel:** remove text loading delay on standalone panel ([42827bb](https://github.com/crimx/ext-saladict/commit/42827bb30499c7b5788be9cb10f6c9f4cf191508)), closes [#974](https://github.com/crimx/ext-saladict/issues/974)\n* **panel:** simplify summoned panel initialization ([ae322be](https://github.com/crimx/ext-saladict/commit/ae322beb688414264ce6a2a4b958e69392ab4c2c))\n* **popup:** close popup panel after menus being triggered ([931afc2](https://github.com/crimx/ext-saladict/commit/931afc279ce6f9ff1e4ad211eccf2fd86f95f330))\n\n### [7.16.1](https://github.com/crimx/ext-saladict/compare/v7.15.1...v7.16.1) (2020-09-05)\n\n\n### Features\n\n* add lingocloud browser shortcut ([badd839](https://github.com/crimx/ext-saladict/commit/badd839a0c40602a79337b646a0b6baa6f66f834))\n* add Lingocloud trs ([fe75c7a](https://github.com/crimx/ext-saladict/commit/fe75c7a6c9431aeeaa63138b4fd1c1eff153124f))\n\n\n### Bug Fixes\n\n* **dicts:** add google tts ([bbbe0a1](https://github.com/crimx/ext-saladict/commit/bbbe0a10a6def4608ff850e849005997abb37ad6))\n* **options:** fix dict title overflows on small screens ([54d4fe3](https://github.com/crimx/ext-saladict/commit/54d4fe30ddd0a62f0c41bc7423bf0eb3c3a9309e))\n* **options:** replace react-sortable-hoc with react-beautiful-dnd ([7e3d0c7](https://github.com/crimx/ext-saladict/commit/7e3d0c744f1d1bc18c8778460f811f2a55d029a3)), closes [#966](https://github.com/crimx/ext-saladict/issues/966)\n* **panel:** remove text loading delay on standalone panel ([42827bb](https://github.com/crimx/ext-saladict/commit/42827bb30499c7b5788be9cb10f6c9f4cf191508)), closes [#974](https://github.com/crimx/ext-saladict/issues/974)\n* **popup:** close popup panel after menus being triggered ([931afc2](https://github.com/crimx/ext-saladict/commit/931afc279ce6f9ff1e4ad211eccf2fd86f95f330))\n\n## [7.16.0](https://github.com/crimx/ext-saladict/compare/v7.15.1...v7.16.0) (2020-09-04)\n\n\n### Features\n\n* add lingocloud browser shortcut ([badd839](https://github.com/crimx/ext-saladict/commit/badd839a0c40602a79337b646a0b6baa6f66f834))\n* add Lingocloud trs ([fe75c7a](https://github.com/crimx/ext-saladict/commit/fe75c7a6c9431aeeaa63138b4fd1c1eff153124f))\n\n\n### Bug Fixes\n\n* **dicts:** add google tts ([bbbe0a1](https://github.com/crimx/ext-saladict/commit/bbbe0a10a6def4608ff850e849005997abb37ad6))\n* **options:** fix dict title overflows on small screens ([54d4fe3](https://github.com/crimx/ext-saladict/commit/54d4fe30ddd0a62f0c41bc7423bf0eb3c3a9309e))\n* **options:** replace react-sortable-hoc with react-beautiful-dnd ([7e3d0c7](https://github.com/crimx/ext-saladict/commit/7e3d0c744f1d1bc18c8778460f811f2a55d029a3)), closes [#966](https://github.com/crimx/ext-saladict/issues/966)\n* **panel:** remove text loading delay on standalone panel ([42827bb](https://github.com/crimx/ext-saladict/commit/42827bb30499c7b5788be9cb10f6c9f4cf191508)), closes [#974](https://github.com/crimx/ext-saladict/issues/974)\n* **popup:** close popup panel after menus being triggered ([931afc2](https://github.com/crimx/ext-saladict/commit/931afc279ce6f9ff1e4ad211eccf2fd86f95f330))\n\n### [7.15.1](https://github.com/crimx/ext-saladict/compare/v7.15.0...v7.15.1) (2020-08-07)\n\n## [7.15.0](https://github.com/crimx/ext-saladict/compare/v7.14.5...v7.15.0) (2020-08-07)\n\n\n### Features\n\n* **sync-services:** add sync server to ankiconnect ([b6a7487](https://github.com/crimx/ext-saladict/commit/b6a74873b71282805892e8172961dec1a77e13bb))\n\n\n### Bug Fixes\n\n* **background:** remove background permission on Opera ([151b0a1](https://github.com/crimx/ext-saladict/commit/151b0a16320ff7ab875ad970525b358459f733a3)), closes [#916](https://github.com/crimx/ext-saladict/issues/916)\n* **dicts:** cambridge amp-img replacement ([1aed3f4](https://github.com/crimx/ext-saladict/commit/1aed3f41f24e0b91e5bf9b20ab11a3f38967d8a7)), closes [#939](https://github.com/crimx/ext-saladict/issues/939)\n* **dicts:** cambridge idiom-only entry ([0135b3e](https://github.com/crimx/ext-saladict/commit/0135b3e84ec85af382e2e94470b5c1705e1e830e)), closes [#940](https://github.com/crimx/ext-saladict/issues/940)\n* **dicts:** replace cambridge amp-audio ([aac184c](https://github.com/crimx/ext-saladict/commit/aac184cd9d9ddb035f8331457baf8b0a160a3224)), closes [#943](https://github.com/crimx/ext-saladict/issues/943)\n* **dicts:** same url for src page ([a0696e1](https://github.com/crimx/ext-saladict/commit/a0696e1709dd700d20d182c0f277025090a14608)), closes [#935](https://github.com/crimx/ext-saladict/issues/935)\n* **locales:** typo ([8dcb8e1](https://github.com/crimx/ext-saladict/commit/8dcb8e11cf5f9e40dd424fa332525329d3385ca4))\n* **panel:** reset opacity on root container ([40abbbc](https://github.com/crimx/ext-saladict/commit/40abbbc884f6ac3fba44e9423e9d4887048e91e0)), closes [#904](https://github.com/crimx/ext-saladict/issues/904)\n* **panel:** search box on in-page panel loses focus ([c1d5984](https://github.com/crimx/ext-saladict/commit/c1d598473d02dc247c218d48e183bfa58251def2)), closes [#927](https://github.com/crimx/ext-saladict/issues/927)\n* **panel:** select background color ([2a9c144](https://github.com/crimx/ext-saladict/commit/2a9c14484d56a2815bb9fb09d98f361e41616f52))\n* **panel:** support Super Dark Mode ([6e6164e](https://github.com/crimx/ext-saladict/commit/6e6164eeb9ec1f5eeff80bad3f3b9715a3627447)), closes [#947](https://github.com/crimx/ext-saladict/issues/947)\n* **sync-services:** shanbay batch upload interrupting ([5881389](https://github.com/crimx/ext-saladict/commit/5881389b05823258383ba7448eb836e6cb59bc30)), closes [#932](https://github.com/crimx/ext-saladict/issues/932)\n* **word-editor:** correct container dimension ([0b51e6b](https://github.com/crimx/ext-saladict/commit/0b51e6bb0026d363f728cde758e299219580c791))\n\n\n### Build System\n\n* **deps-dev:** bump standard-version from 6.0.1 to 8.0.1 ([#903](https://github.com/crimx/ext-saladict/issues/903)) ([d1ac2b5](https://github.com/crimx/ext-saladict/commit/d1ac2b57d576f4eaaa94f3a8a6595b44a28b76f1))\n\n### [7.14.5](https://github.com/crimx/ext-saladict/compare/v7.14.4...v7.14.5) (2020-07-12)\n\n\n### Bug Fixes\n\n* **panel:** set missing initial config ([791cb57](https://github.com/crimx/ext-saladict/commit/791cb57cbd40264b255da03576693af3a15daf24))\n\n### [7.14.4](https://github.com/crimx/ext-saladict/compare/v7.14.3...v7.14.4) (2020-07-12)\n\n\n### Bug Fixes\n\n* **dicts:** correct machine translator rtl source text collapse fading ([42003e5](https://github.com/crimx/ext-saladict/commit/42003e51c0c3778c3ecbc0d6771142baae720ab9))\n\n\n### Build System\n\n* always strip momentjs locales ([7dbb309](https://github.com/crimx/ext-saladict/commit/7dbb309bbe3caafc70348999ca5e4526fd649618))\n\n### [7.14.3](https://github.com/crimx/ext-saladict/compare/v7.14.2...v7.14.3) (2020-07-12)\n\n\n### Bug Fixes\n\n* **panel:** fix machine source text fade color ([b36deb6](https://github.com/crimx/ext-saladict/commit/b36deb68df02254da2577cad4b9eb65722e5155a))\n\n### [7.14.2](https://github.com/crimx/ext-saladict/compare/v7.14.1...v7.14.2) (2020-07-12)\n\n\n### Bug Fixes\n\n* **panel:** move saladict-theme down under darkMode ([52d8fb2](https://github.com/crimx/ext-saladict/commit/52d8fb20dd9d53a83b0c1f807102220977aa9e0d))\n\n### [7.14.1](https://github.com/crimx/ext-saladict/compare/v7.14.0...v7.14.1) (2020-07-12)\n\n\n### Bug Fixes\n\n* **options:** update antd typings ([2879c4f](https://github.com/crimx/ext-saladict/commit/2879c4fe9e1ef6a5397ac807ce94a2d188d61d30))\n* fixed incorrect options merging ([8e062dc](https://github.com/crimx/ext-saladict/commit/8e062dc7dc87642da282bcceeabbaf86d58770f4))\n* switch default slInitial back to collapse ([524223c](https://github.com/crimx/ext-saladict/commit/524223c4a2cfafd5a0a8d0a2eb6fa494926e5099))\n\n\n### Build System\n\n* add sass globals to storybook ([f44ce5a](https://github.com/crimx/ext-saladict/commit/f44ce5a6bd54b2f3346d63934151db6aa76789d0))\n\n## [7.14.0](https://github.com/crimx/ext-saladict/compare/v7.13.4...v7.14.0) (2020-07-10)\n\n\n### Features\n\n* **background:** add background permission ([9d41e09](https://github.com/crimx/ext-saladict/commit/9d41e0980e17dffbcce2804a66b850f9e33c147e)), closes [#892](https://github.com/crimx/ext-saladict/issues/892)\n* **menus:** add saladict standlone panel ([0d4a732](https://github.com/crimx/ext-saladict/commit/0d4a73278a523405b1ab1d28ade9d514826ecd4a)), closes [#864](https://github.com/crimx/ext-saladict/issues/864)\n* **panel:** add dict item catalog ([f07ea25](https://github.com/crimx/ext-saladict/commit/f07ea25751fcb746363e09b3804da03520b35249))\n\n\n### Bug Fixes\n\n* **components:** fix select padding in firefox ([dd366d3](https://github.com/crimx/ext-saladict/commit/dd366d396e8ca63298aeb70a575dfaad858f75b7))\n* **components:** typo ([0229cc2](https://github.com/crimx/ext-saladict/commit/0229cc28124567cbe6a79c71d474c935da094756))\n* **dicts:** update tts for tencent and caiyun ([afdac41](https://github.com/crimx/ext-saladict/commit/afdac41d4099aef1efa6021c60b741e3ad6fda22))\n* **options:** update sortable list on store changes ([075aef7](https://github.com/crimx/ext-saladict/commit/075aef764d8327f78288e0d6d183ae51f9f82f48))\n* **panel:** correct panel history ([1b2781f](https://github.com/crimx/ext-saladict/commit/1b2781f48e875f07536be14a733e93a901db5958)), closes [#881](https://github.com/crimx/ext-saladict/issues/881)\n* **panel:** fix catalog scrolling ([ded090d](https://github.com/crimx/ext-saladict/commit/ded090d81b7d2bd66b0c12806a1c3fa457257328))\n\n\n### Tests\n\n* **panel:** update dict item stories ([507c638](https://github.com/crimx/ext-saladict/commit/507c638ba34a9b903cbee06b36056ede15c66a2d))\n* update stories ([49b2ad2](https://github.com/crimx/ext-saladict/commit/49b2ad2eb68e0b39117b224ad1c05264b8ed678f))\n* **dicts:** log runtime messages ([5c23327](https://github.com/crimx/ext-saladict/commit/5c23327db3f6b44f25487b3cc0866ab38741adc9))\n\n### [7.13.4](https://github.com/crimx/ext-saladict/compare/v7.13.3...v7.13.4) (2020-06-23)\n\n\n### Bug Fixes\n\n* **options:** entry incorrect initial form state ([f009670](https://github.com/crimx/ext-saladict/commit/f0096707ba14425e3c4a60a9548ddad3adf8f3e0)), closes [#865](https://github.com/crimx/ext-saladict/issues/865)\n\n### [7.13.3](https://github.com/crimx/ext-saladict/compare/v7.13.2...v7.13.3) (2020-06-20)\n\n\n### Bug Fixes\n\n* **dicts:** add macmillan american ([0c63217](https://github.com/crimx/ext-saladict/commit/0c632178733dd32cbd0a5aaab2aad31207a237b1)), closes [#837](https://github.com/crimx/ext-saladict/issues/837)\n* **dicts:** update cnki params ([952702c](https://github.com/crimx/ext-saladict/commit/952702c57946c7058291a062163511594b7c5d57)), closes [#852](https://github.com/crimx/ext-saladict/issues/852)\n\n\n### Build System\n\n* upgrade deps ([549983a](https://github.com/crimx/ext-saladict/commit/549983ac77d58de9471c8cf01a29ff95b4616e8f))\n\n### [7.13.2](https://github.com/crimx/ext-saladict/compare/v7.13.1...v7.13.2) (2020-06-04)\n\n\n### Bug Fixes\n\n* **sync-services:** fix anki returning random order of field names ([227089c](https://github.com/crimx/ext-saladict/commit/227089c8f713b32bafd512a74c5423ea0a7b5673))\n\n### [7.13.1](https://github.com/crimx/ext-saladict/compare/v7.13.0...v7.13.1) (2020-06-02)\n\n\n### Bug Fixes\n\n* **ankiconnect:** compatible with anki localization ([f1ce54c](https://github.com/crimx/ext-saladict/commit/f1ce54c732c4a3ed04b8b881c1df3d4810c65f62))\n* remove unused ([f0d203c](https://github.com/crimx/ext-saladict/commit/f0d203c84f107762d7b753593851d4ffb417c310))\n\n## [7.13.0](https://github.com/crimx/ext-saladict/compare/v7.12.1...v7.13.0) (2020-06-01)\n\n\n### Features\n\n* **panel:** add option for panel size and position memo ([bce3bfb](https://github.com/crimx/ext-saladict/commit/bce3bfbaeee386529232d261025844b187ca43e8)), closes [#812](https://github.com/crimx/ext-saladict/issues/812)\n* **sync-services:** add ankiconnect ([cd12702](https://github.com/crimx/ext-saladict/commit/cd127027a66909903f91c1f17ed7173428a0ecfd))\n\n\n### Bug Fixes\n\n* **dicts:** remove horizontal scroll ([7ebf09d](https://github.com/crimx/ext-saladict/commit/7ebf09d224e06df08750cdbde6f6cd31034875c3)), closes [#818](https://github.com/crimx/ext-saladict/issues/818)\n* **dicts:** remove lexico associated translation ([81d2cc7](https://github.com/crimx/ext-saladict/commit/81d2cc7a811a249b4ef7350fe9f31e95d1a7ce78)), closes [#818](https://github.com/crimx/ext-saladict/issues/818)\n* **i18n:** make loader singleton ([6539fc7](https://github.com/crimx/ext-saladict/commit/6539fc78debda645c4d3524ac42c146ee20e4d00))\n* **options:** add key to react component ([09a7d32](https://github.com/crimx/ext-saladict/commit/09a7d3277943aafae090e172e5046bca72181ee0))\n* **panel:** open standalone panel anyway ([88259b0](https://github.com/crimx/ext-saladict/commit/88259b0893967a0e993d1c8102043378cd27c9c7)), closes [#832](https://github.com/crimx/ext-saladict/issues/832)\n* **panel:** update fav icon after saving words ([c803998](https://github.com/crimx/ext-saladict/commit/c803998132dbc71f78d4a29730784e9446035f58))\n* **sync-services:** add version on request ([f84f359](https://github.com/crimx/ext-saladict/commit/f84f3590c577b43e67ced19382565692b6cdc67c))\n* **word-editor:** translate context when word editor shows up ([95bf129](https://github.com/crimx/ext-saladict/commit/95bf129dbfc97a9992c62e84cf7be27ce294eecc))\n* stop playing audio on panel close ([97cabf4](https://github.com/crimx/ext-saladict/commit/97cabf49e7aca7754edde247003fbcb4ea42dd59)), closes [#824](https://github.com/crimx/ext-saladict/issues/824)\n* **wordpage:** dark mode ([5921673](https://github.com/crimx/ext-saladict/commit/59216735ab2f88e9bdc9f6b8adae6e4cb4e7d93c))\n\n\n### Tests\n\n* **sync-services:** add Anki Connect ([1fb55e8](https://github.com/crimx/ext-saladict/commit/1fb55e83b58354e8449ed0b6353e591f4c47e779))\n* **sync-services:** update webdav to new architecture ([d98c16c](https://github.com/crimx/ext-saladict/commit/d98c16cdfca16a5f2c0df4a7ed78e75b4441c8cd))\n\n### [7.12.1](https://github.com/crimx/ext-saladict/compare/v7.12.0...v7.12.1) (2020-05-17)\n\n\n### Bug Fixes\n\n* **dicts:** update googledict style ([52e66df](https://github.com/crimx/ext-saladict/commit/52e66dfe282b74bc21a7bec4e91bf8a66a34f0cf))\n* **macmillan:** add styles on labels ([768ba78](https://github.com/crimx/ext-saladict/commit/768ba7851d7e22517a3d4a23dad135c103f229ab)), closes [#803](https://github.com/crimx/ext-saladict/issues/803)\n\n## [7.12.0](https://github.com/crimx/ext-saladict/compare/v7.11.2...v7.12.0) (2020-05-15)\n\n\n### Features\n\n* **command:** add shortcut for adding notebook ([524dd6c](https://github.com/crimx/ext-saladict/commit/524dd6c7a250c415a865f1587c79b09bef7cbc7c)), closes [#785](https://github.com/crimx/ext-saladict/issues/785)\n* **pdf:** open pdf viewer in standalone panel ([07f8c71](https://github.com/crimx/ext-saladict/commit/07f8c71195d1c0cf41f0592115921d226bebef07))\n* **selection:** add altKey for search modes ([fdc2ba5](https://github.com/crimx/ext-saladict/commit/fdc2ba56ad63d278668633c507a1e0c3a11070eb)), closes [#729](https://github.com/crimx/ext-saladict/issues/729)\n\n\n### Bug Fixes\n\n* respect qsFocus option ([2a9cf06](https://github.com/crimx/ext-saladict/commit/2a9cf062a29aea60aa15f473ba3b5a543f9dea49)), closes [#784](https://github.com/crimx/ext-saladict/issues/784)\n* upgrade neutrino-webextension ([5c1c48d](https://github.com/crimx/ext-saladict/commit/5c1c48d0b58a22ad172196e3106e00383b133eaa)), closes [#790](https://github.com/crimx/ext-saladict/issues/790)\n\n\n### Tests\n\n* **dicts:** update macmillan ([70150fd](https://github.com/crimx/ext-saladict/commit/70150fd41a34de85c24418932ba33d4cb8ce84d8))\n* **pdf:** update pdf tests ([6890c6d](https://github.com/crimx/ext-saladict/commit/6890c6d030415fa53adb741e7cfc0650fe43e044))\n\n### [7.11.2](https://github.com/crimx/ext-saladict/compare/v7.11.1...v7.11.2) (2020-05-06)\n\n\n### Bug Fixes\n\n* **wordeditor:** incorrect z-index ([3701084](https://github.com/crimx/ext-saladict/commit/3701084299cc3902a2e55e9e14d472fba9bcdf27)), closes [#780](https://github.com/crimx/ext-saladict/issues/780)\n* **wordpage:** refresh table on word changes ([5520feb](https://github.com/crimx/ext-saladict/commit/5520feb1b28eb73e689f5881cc07d9855c11fac9)), closes [#780](https://github.com/crimx/ext-saladict/issues/780)\n\n### [7.11.1](https://github.com/crimx/ext-saladict/compare/v7.11.0...v7.11.1) (2020-05-05)\n\n\n### Bug Fixes\n\n* **firefox:** add franc to dynamic chunks ([61580b1](https://github.com/crimx/ext-saladict/commit/61580b1217bd293885b854ad061d55b082b5be2b)), closes [#778](https://github.com/crimx/ext-saladict/issues/778)\n* **wordeditor:** fix z-index on internal page ([5c80ebb](https://github.com/crimx/ext-saladict/commit/5c80ebb186c782c0c4747fca3de97e035a334cb6))\n\n\n### Tests\n\n* update check-update ([6582938](https://github.com/crimx/ext-saladict/commit/6582938c814a92b5ab36a8ae20b4ebdf6a77cc98))\n\n## [7.11.0](https://github.com/crimx/ext-saladict/compare/v7.10.4...v7.11.0) (2020-05-01)\n\n\n### Features\n\n* **dicts:** add jikipedia ([046b850](https://github.com/crimx/ext-saladict/commit/046b850c83516c43022773bdd2ab6cacbb7696fa))\n* fix buggy axios ([9eb8172](https://github.com/crimx/ext-saladict/commit/9eb817242365961cd940bd5e54547b601678c7ce))\n* **panel:** add sticky folding ([7b2c352](https://github.com/crimx/ext-saladict/commit/7b2c3524b452925d126d6bd15770649a353e2068)), closes [#765](https://github.com/crimx/ext-saladict/issues/765)\n* **panel:** remember last standalone window position ([3d25428](https://github.com/crimx/ext-saladict/commit/3d254280e6c2a16a7bd5de99eace55090c04cc88)), closes [#766](https://github.com/crimx/ext-saladict/issues/766)\n* **profiles:** add shortcuts for top profiles ([de9ca07](https://github.com/crimx/ext-saladict/commit/de9ca077c23147859ebc648b3202faa5b25bca15))\n* added option qsFocus ([51e59f9](https://github.com/crimx/ext-saladict/commit/51e59f91fb27ed3c942d2f9c4aa88e31f78eef84)), closes [#764](https://github.com/crimx/ext-saladict/issues/764)\n\n\n### Bug Fixes\n\n* **badge:** remove badge text ([873b1c7](https://github.com/crimx/ext-saladict/commit/873b1c77d3655e4b10dcb389108aabf9c9e31b4c)), closes [#770](https://github.com/crimx/ext-saladict/issues/770)\n* **options:** prevent panel being opened accidentally ([a673c9f](https://github.com/crimx/ext-saladict/commit/a673c9f94f46d19121058b0f534a5aaa750d8453)), closes [#769](https://github.com/crimx/ext-saladict/issues/769)\n* **panel:** do not update search box text on selection ([b104405](https://github.com/crimx/ext-saladict/commit/b1044050d92ee4fb5a2f4bd594d2bd9ca44eca12))\n* **pdf:** remove 'unsafe-eval' CSP ([eaea459](https://github.com/crimx/ext-saladict/commit/eaea459ae500cf84cea3f65e59c573d6816d222d))\n\n\n### Build System\n\n* fix script arguments ([df78f19](https://github.com/crimx/ext-saladict/commit/df78f199b71fd4b018903fd57e00c39928986c1c))\n\n\n### Tests\n\n* **background:** remove update check ([1f1b5ff](https://github.com/crimx/ext-saladict/commit/1f1b5ffa7ed8fb99773f645eb20555a00b12bacd))\n* **storybook:** add path pattern ([61a883a](https://github.com/crimx/ext-saladict/commit/61a883a928aaa7c640194592ceb42a650e6d2647))\n\n### [7.10.4](https://github.com/crimx/ext-saladict/compare/v7.10.3...v7.10.4) (2020-04-27)\n\n### [7.10.3](https://github.com/crimx/ext-saladict/compare/v7.10.2...v7.10.3) (2020-04-26)\n\n### [7.10.2](https://github.com/crimx/ext-saladict/compare/v7.10.1...v7.10.2) (2020-04-26)\n\n\n### Bug Fixes\n\n* **dicts:** cnki should respect options ([c78d2d7](https://github.com/crimx/ext-saladict/commit/c78d2d7ae41dc08d1c7a8a3900613392825c3527)), closes [#752](https://github.com/crimx/ext-saladict/issues/752)\n* **options:** smooth dark/bright transition ([5433cac](https://github.com/crimx/ext-saladict/commit/5433cac8f074e00a597e91c6804cf3fc8acf3bef))\n* **options:** typo ([c66ed05](https://github.com/crimx/ext-saladict/commit/c66ed0586a1662d2a9c4611f8ef8e2c4c7099b60))\n* **selection:** cancel instant capture on keyup ([c6dbaa7](https://github.com/crimx/ext-saladict/commit/c6dbaa7ef92c29827b6a35db7bc6824c8696843f)), closes [#756](https://github.com/crimx/ext-saladict/issues/756)\n\n### [7.10.1](https://github.com/crimx/ext-saladict/compare/v7.10.0...v7.10.1) (2020-04-24)\n\n\n### Bug Fixes\n\n* **wordpage:** firefox layout ([7165fda](https://github.com/crimx/ext-saladict/commit/7165fda9101b24a229e15a64396ef39ef1c7fb85))\n\n## [7.10.0](https://github.com/crimx/ext-saladict/compare/v7.9.3...v7.10.0) (2020-04-24)\n\n\n### Features\n\n* add token settings ([df2924f](https://github.com/crimx/ext-saladict/commit/df2924f7f1a88ce50c21e6019ddf63c73f3a2ae1))\n\n\n### Bug Fixes\n\n* **context-menus:** encode selection text ([0da9e84](https://github.com/crimx/ext-saladict/commit/0da9e84fd2c5805d66af637f8977340408b2d21a))\n* **context-menus:** load locale ([e1981b1](https://github.com/crimx/ext-saladict/commit/e1981b145680a7b2fbce86be5a93e50e0266beac))\n* **googledict:** audio link ([407fa9b](https://github.com/crimx/ext-saladict/commit/407fa9b669800bde3ccc29b217e8515d810f022d))\n* **i18n:** make ready changes every time ([2a03730](https://github.com/crimx/ext-saladict/commit/2a0373045d343c77782ca327bce3d6bdf40b7c0c))\n* **i18n:** proper init language without reloading ([bca03cb](https://github.com/crimx/ext-saladict/commit/bca03cb2f3c0d3fb7258b594e69661512606877d))\n* **options:** avoid stale values ([d98b53a](https://github.com/crimx/ext-saladict/commit/d98b53a7973edca8da7274f7fc774da532e30e5e))\n* **options:** layout adjustment ([84496e6](https://github.com/crimx/ext-saladict/commit/84496e698cba7656ea0a46b8dd959fee00f1faaf))\n* **options:** make data immutable ([fc967db](https://github.com/crimx/ext-saladict/commit/fc967db6cb3eed729c9bb3dfd1b3ba9545d0c917))\n* **options:** only get values from item name ([fea31ab](https://github.com/crimx/ext-saladict/commit/fea31abd5ade22c9ff44a0a8115519a5586ff1c3))\n* **options:** reduce re-rendering of the whole form ([02f61a9](https://github.com/crimx/ext-saladict/commit/02f61a9105b0adbf87a17b4170e6522bcc5de370))\n* **options:** rerender error boundary on entry change ([7450057](https://github.com/crimx/ext-saladict/commit/7450057896ce10bd52f5c0602353e2532563d999))\n* **options:** search words on options page ([4bfc211](https://github.com/crimx/ext-saladict/commit/4bfc211d26286d71b178ffcecf9145d7ec222b15))\n* **panel:** correct standalone position on multi-screen ([f2f152f](https://github.com/crimx/ext-saladict/commit/f2f152ff604260e6e65a05b7dfea7d84f8d07e96))\n* **panel:** disable external style reset on standalone panel ([c2f26be](https://github.com/crimx/ext-saladict/commit/c2f26bec4ae533b3f22610fe5ce7ad722e15d446))\n* **panel:** hide external divs ([d584e31](https://github.com/crimx/ext-saladict/commit/d584e313e4cbf6370a8d8c397ce32f4fb9659976)), closes [#703](https://github.com/crimx/ext-saladict/issues/703)\n* **panel:** more robust dargging ([e5a876b](https://github.com/crimx/ext-saladict/commit/e5a876b5475e29538874834ed985cde21a7a5ae2)), closes [#747](https://github.com/crimx/ext-saladict/issues/747)\n* **panel:** mta font size ([0b261fd](https://github.com/crimx/ext-saladict/commit/0b261fd23d9ea9512ffc59d3adafdd14967b69c7)), closes [#721](https://github.com/crimx/ext-saladict/issues/721)\n* **panel:** profiles float box ([7b59fb3](https://github.com/crimx/ext-saladict/commit/7b59fb3246bca4c99acd8129ddb981c913affd56))\n* **pdf:** update pdf script ([f419402](https://github.com/crimx/ext-saladict/commit/f419402bfe96e9759076f865c0ea813a8dc011d1))\n* **selection:** instant selection ([85d43a0](https://github.com/crimx/ext-saladict/commit/85d43a0a03519985828362c99556719c86761f84)), closes [#742](https://github.com/crimx/ext-saladict/issues/742)\n* **sync-services:** ignore word addition from sync services ([685cd02](https://github.com/crimx/ext-saladict/commit/685cd02e09adb11bf508ef9ae3033d4d4591763c)), closes [#717](https://github.com/crimx/ext-saladict/issues/717)\n* content style origin ([230275d](https://github.com/crimx/ext-saladict/commit/230275d5db56e65de9570d27d62ceed1f0618a32))\n\n\n### Build System\n\n* better chunk naming for dict favicons ([d1f65fd](https://github.com/crimx/ext-saladict/commit/d1f65fde14b23992ce7a1969a0a3b778a4caef70))\n* control split chunks ([858ea64](https://github.com/crimx/ext-saladict/commit/858ea644d19016218b2b4481fbd63e80e8345f65))\n* fix dotenv ([07c7fc6](https://github.com/crimx/ext-saladict/commit/07c7fc6b78c1acc84607a0946775d2332f875ffb))\n\n\n### Tests\n\n* **panel:** update storybook ([657da12](https://github.com/crimx/ext-saladict/commit/657da12dd418a99a5d13f83158ed9f1d51fe3c33))\n\n### [7.9.3](https://github.com/crimx/ext-saladict/compare/v7.9.2...v7.9.3) (2020-03-19)\n\n\n### Bug Fixes\n\n* **panel:** prevent ff flash ([#691](https://github.com/crimx/ext-saladict/issues/691)) ([d18df80](https://github.com/crimx/ext-saladict/commit/d18df80956e8423d808e9a8ac64458ddc73b3b22))\n* **word-editor:** inner panel not showing up ([b8c6064](https://github.com/crimx/ext-saladict/commit/b8c606486d69fc00c1babe5d6569bcb61f26a801)), closes [#694](https://github.com/crimx/ext-saladict/issues/694)\n\n### [7.9.2](https://github.com/crimx/ext-saladict/compare/v7.9.1...v7.9.2) (2020-03-14)\n\n\n### Bug Fixes\n\n* **dicts:** wrong dict config ([551a0b3](https://github.com/crimx/ext-saladict/commit/551a0b30db29c14dd093d33f01c440d669846fc4))\n\n### [7.9.1](https://github.com/crimx/ext-saladict/compare/v7.9.0...v7.9.1) (2020-03-10)\n\n\n### Bug Fixes\n\n* **dicts:** add fallback language for machine translate ([60b10da](https://github.com/crimx/ext-saladict/commit/60b10da2dbb890270965de7b24a6672ced4ce579)), closes [#674](https://github.com/crimx/ext-saladict/issues/674)\n* **dicts:** enhance cjk detection ([8311d9e](https://github.com/crimx/ext-saladict/commit/8311d9e30d01740930725cd9a83adfa5a92bf26e))\n* **dicts:** remove caching async function ([03d7866](https://github.com/crimx/ext-saladict/commit/03d78669dacece312ba7bf2a5d8763d9b760730b))\n\n## [7.9.0](https://github.com/crimx/ext-saladict/compare/v7.8.0...v7.9.0) (2020-03-09)\n\n\n### Features\n\n* **dicts:** add lexico ([a86fc7d](https://github.com/crimx/ext-saladict/commit/a86fc7db85f8646f6326b6e1dbbd235ce930c7d6))\n* **dicts:** add renren ([b4dc38d](https://github.com/crimx/ext-saladict/commit/b4dc38da25e838f1c9869d66d6cb2b9ecfbf3fb5))\n\n\n### Bug Fixes\n\n* **dicts:** correct tts language ([76eb34d](https://github.com/crimx/ext-saladict/commit/76eb34d70fe802b3117a5c31a2e8f1d732f2f34e)), closes [#659](https://github.com/crimx/ext-saladict/issues/659)\n* **renren:** prevent detail click event being captured by panel ([921d102](https://github.com/crimx/ext-saladict/commit/921d102deac23ec19196a41512883d645c73ae13))\n* **wordpage:** keyword matching ([b9a1a3e](https://github.com/crimx/ext-saladict/commit/b9a1a3e211a405595724a9e466ffc8d9a2c7ec1d))\n\n\n### Tests\n\n* fix bing fixtures ([a7731a2](https://github.com/crimx/ext-saladict/commit/a7731a22131dc358c3036b14d59b9a3d33344a53))\n\n## [7.8.0](https://github.com/crimx/ext-saladict/compare/v7.7.6...v7.8.0) (2020-02-13)\n\n\n### Bug Fixes\n\n* remove extra clipboard search on command ([0b7166e](https://github.com/crimx/ext-saladict/commit/0b7166e)), closes [#647](https://github.com/crimx/ext-saladict/issues/647)\n* space escape ([2c31562](https://github.com/crimx/ext-saladict/commit/2c31562)), closes [#635](https://github.com/crimx/ext-saladict/issues/635)\n\n\n### Features\n\n* add standalone word editor ([24d487a](https://github.com/crimx/ext-saladict/commit/24d487a)), closes [#608](https://github.com/crimx/ext-saladict/issues/608)\n\n\n\n### [7.7.6](https://github.com/crimx/ext-saladict/compare/v7.7.5...v7.7.6) (2020-02-03)\n\n\n\n### [7.7.5](https://github.com/crimx/ext-saladict/compare/v7.7.4...v7.7.5) (2020-02-03)\n\n\n\n### [7.7.4](https://github.com/crimx/ext-saladict/compare/v7.7.3...v7.7.4) (2020-02-03)\n\n\n\n### [7.7.3](https://github.com/crimx/ext-saladict/compare/v7.7.2...v7.7.3) (2020-02-02)\n\n\n\n### [7.7.2](https://github.com/crimx/ext-saladict/compare/v7.7.1...v7.7.2) (2020-01-27)\n\n\n\n### [7.7.1](https://github.com/crimx/ext-saladict/compare/v7.7.0...v7.7.1) (2020-01-24)\n\n\n### Bug Fixes\n\n* pdf.js requires unsafe-eval csp ([533a66d](https://github.com/crimx/ext-saladict/commit/533a66d)), closes [#630](https://github.com/crimx/ext-saladict/issues/630)\n\n\n\n## [7.7.0](https://github.com/crimx/ext-saladict/compare/v7.6.2...v7.7.0) (2020-01-24)\n\n\n### Bug Fixes\n\n* **pdf:** match double quotes ([46060bd](https://github.com/crimx/ext-saladict/commit/46060bd))\n\n\n### Features\n\n* **options:** add privacy settings ([9408002](https://github.com/crimx/ext-saladict/commit/9408002))\n\n\n\n### [7.6.2](https://github.com/crimx/ext-saladict/compare/v7.6.1...v7.6.2) (2020-01-16)\n\n\n### Tests\n\n* remove fixtures ([eca13a3](https://github.com/crimx/ext-saladict/commit/eca13a3))\n\n\n\n### [7.6.1](https://github.com/crimx/ext-saladict/compare/v7.6.0...v7.6.1) (2020-01-06)\n\n\n### Bug Fixes\n\n* **background:** remove duplicated qs panel onclose response ([b9d209b](https://github.com/crimx/ext-saladict/commit/b9d209b)), closes [#618](https://github.com/crimx/ext-saladict/issues/618)\n* **selection:** respect qs panel selection settings ([4990479](https://github.com/crimx/ext-saladict/commit/4990479))\n\n\n\n## [7.6.0](https://github.com/crimx/ext-saladict/compare/v7.5.4...v7.6.0) (2019-12-29)\n\n\n### Bug Fixes\n\n* **panel:** ignore snapshot if the panel was hidden ([ae9a538](https://github.com/crimx/ext-saladict/commit/ae9a538))\n* **panel:** open word editor on wordpage ([dbb9b58](https://github.com/crimx/ext-saladict/commit/dbb9b58)), closes [#590](https://github.com/crimx/ext-saladict/issues/590)\n* **selection:** detect mouseup in panel ([989a9f6](https://github.com/crimx/ext-saladict/commit/989a9f6))\n* remove invalid window state ([c258de2](https://github.com/crimx/ext-saladict/commit/c258de2))\n* round window positions ([fa3d264](https://github.com/crimx/ext-saladict/commit/fa3d264)), closes [#607](https://github.com/crimx/ext-saladict/issues/607)\n\n\n### Features\n\n* **content:** add picker for ctx translated results ([6c0c4b8](https://github.com/crimx/ext-saladict/commit/6c0c4b8))\n* **menus:** add copu pdf url to clipboard ([cfe6d9d](https://github.com/crimx/ext-saladict/commit/cfe6d9d)), closes [#571](https://github.com/crimx/ext-saladict/issues/571)\n\n\n\n### [7.5.4](https://github.com/crimx/ext-saladict/compare/v7.5.3...v7.5.4) (2019-12-11)\n\n\n### Bug Fixes\n\n* dual screen windows management ([8196a6d](https://github.com/crimx/ext-saladict/commit/8196a6d)), closes [#587](https://github.com/crimx/ext-saladict/issues/587)\n\n\n\n### [7.5.3](https://github.com/crimx/ext-saladict/compare/v7.5.2...v7.5.3) (2019-12-10)\n\n\n### Bug Fixes\n\n* **dicts:** update moji ([fb528b1](https://github.com/crimx/ext-saladict/commit/fb528b1))\n* self messaging server init order ([8473faa](https://github.com/crimx/ext-saladict/commit/8473faa))\n* 自定义 css 对独立面板不生效 ([#579](https://github.com/crimx/ext-saladict/issues/579)) ([1db0c5a](https://github.com/crimx/ext-saladict/commit/1db0c5a))\n\n\n### Tests\n\n* remove opentranslate ([d655258](https://github.com/crimx/ext-saladict/commit/d655258))\n* update api ([34535ea](https://github.com/crimx/ext-saladict/commit/34535ea))\n* update webdav testing ([782f288](https://github.com/crimx/ext-saladict/commit/782f288))\n\n\n\n### [7.5.2](https://github.com/crimx/ext-saladict/compare/v7.5.1...v7.5.2) (2019-11-11)\n\n\n### Bug Fixes\n\n* **sync:** webdav url ending ([5dc51a8](https://github.com/crimx/ext-saladict/commit/5dc51a8)), closes [#562](https://github.com/crimx/ext-saladict/issues/562)\n\n\n\n### [7.5.1](https://github.com/crimx/ext-saladict/compare/v7.5.0...v7.5.1) (2019-11-04)\n\n\n### Bug Fixes\n\n* translate context before editing ([5c92445](https://github.com/crimx/ext-saladict/commit/5c92445)), closes [#550](https://github.com/crimx/ext-saladict/issues/550)\n\n\n\n## [7.5.0](https://github.com/crimx/ext-saladict/compare/v7.4.0...v7.5.0) (2019-11-03)\n\n\n### Bug Fixes\n\n* update homepage url ([102ff19](https://github.com/crimx/ext-saladict/commit/102ff19))\n* **config:** merge machine pronounce config ([1c37346](https://github.com/crimx/ext-saladict/commit/1c37346)), closes [#540](https://github.com/crimx/ext-saladict/issues/540)\n* **content:** prevent triggering page key events ([a74b255](https://github.com/crimx/ext-saladict/commit/a74b255))\n* **panel:** reset text align ([3b38d19](https://github.com/crimx/ext-saladict/commit/3b38d19)), closes [#537](https://github.com/crimx/ext-saladict/issues/537)\n* missing pdf locale properties ([10e9636](https://github.com/crimx/ext-saladict/commit/10e9636)), closes [#548](https://github.com/crimx/ext-saladict/issues/548)\n\n\n### Features\n\n* **content:** add bowl offset config ([3a1327f](https://github.com/crimx/ext-saladict/commit/3a1327f)), closes [#535](https://github.com/crimx/ext-saladict/issues/535)\n\n\n\n## [7.4.0](https://github.com/crimx/ext-saladict/compare/v7.3.2...v7.4.0) (2019-10-24)\n\n\n### Bug Fixes\n\n* **dicts:** fix custom tl ([1156c5e](https://github.com/crimx/ext-saladict/commit/1156c5e))\n* **menus:** update all menus before selected Closes [#533](https://github.com/crimx/ext-saladict/issues/533) ([076e072](https://github.com/crimx/ext-saladict/commit/076e072))\n* **selection:** get page info on selection ([972a9aa](https://github.com/crimx/ext-saladict/commit/972a9aa)), closes [#531](https://github.com/crimx/ext-saladict/issues/531)\n* **selection:** prevent mouseup being cancelled ([0485244](https://github.com/crimx/ext-saladict/commit/0485244))\n\n\n### Features\n\n* **dicts:** add mojidict ([2ec91ef](https://github.com/crimx/ext-saladict/commit/2ec91ef))\n\n\n\n### [7.3.2](https://github.com/crimx/ext-saladict/compare/v7.3.1...v7.3.2) (2019-10-19)\n\n\n### Bug Fixes\n\n* ignore numbers ([352a84c](https://github.com/crimx/ext-saladict/commit/352a84c))\n\n\n\n### [7.3.1](https://github.com/crimx/ext-saladict/compare/v7.3.0...v7.3.1) (2019-10-18)\n\n\n### Bug Fixes\n\n* **badges:** prevent stale values ([5f4c5d5](https://github.com/crimx/ext-saladict/commit/5f4c5d5))\n\n\n\n## [7.3.0](https://github.com/crimx/ext-saladict/compare/v7.2.2...v7.3.0) (2019-10-18)\n\n\n### Features\n\n* **wordpage:** support replacing linebreaks ([c112de5](https://github.com/crimx/ext-saladict/commit/c112de5))\n\n\n\n### [7.2.2](https://github.com/crimx/ext-saladict/compare/v7.2.1...v7.2.2) (2019-10-15)\n\n\n\n### [7.2.1](https://github.com/crimx/ext-saladict/compare/v7.2.0...v7.2.1) (2019-10-13)\n\n\n### Bug Fixes\n\n* **selection:** in-panel selection ([507581a](https://github.com/crimx/ext-saladict/commit/507581a)), closes [#513](https://github.com/crimx/ext-saladict/issues/513)\n\n\n\n## [7.2.0](https://github.com/crimx/ext-saladict/compare/v7.1.1...v7.2.0) (2019-10-12)\n\n\n### Bug Fixes\n\n* **selection:** ignore anchor and button click on panel ([904445c](https://github.com/crimx/ext-saladict/commit/904445c)), closes [#512](https://github.com/crimx/ext-saladict/issues/512)\n\n\n### Features\n\n* match all characters ([59c8183](https://github.com/crimx/ext-saladict/commit/59c8183)), closes [#434](https://github.com/crimx/ext-saladict/issues/434)\n\n\n\n### [7.1.1](https://github.com/crimx/ext-saladict/compare/v7.1.0...v7.1.1) (2019-10-08)\n\n\n### Bug Fixes\n\n* **panel:** save word without confirm ([#500](https://github.com/crimx/ext-saladict/issues/500)) ([3d77d97](https://github.com/crimx/ext-saladict/commit/3d77d97))\n* **selection:** only detect left click ([fc3797d](https://github.com/crimx/ext-saladict/commit/fc3797d)), closes [#502](https://github.com/crimx/ext-saladict/issues/502)\n* **selection:** prevent unexpected in-panel selection ([3c057ce](https://github.com/crimx/ext-saladict/commit/3c057ce)), closes [#498](https://github.com/crimx/ext-saladict/issues/498)\n\n\n\n## [7.1.0](https://github.com/crimx/ext-saladict/compare/v7.0.4...v7.1.0) (2019-10-03)\n\n\n### Bug Fixes\n\n* **panel:** always focus mta box on expand ([57840eb](https://github.com/crimx/ext-saladict/commit/57840eb))\n* **panel:** prevent page shortkeys when typing ([ba9a37a](https://github.com/crimx/ext-saladict/commit/ba9a37a)), closes [#490](https://github.com/crimx/ext-saladict/issues/490)\n\n\n### Features\n\n* **panel:** add new shortcut for searching clipboard ([e5f279d](https://github.com/crimx/ext-saladict/commit/e5f279d)), closes [#485](https://github.com/crimx/ext-saladict/issues/485)\n* **panel:** add touch mode close [#492](https://github.com/crimx/ext-saladict/issues/492) ([c86e6e8](https://github.com/crimx/ext-saladict/commit/c86e6e8))\n\n\n\n### [7.0.4](https://github.com/crimx/ext-saladict/compare/v7.0.3...v7.0.4) (2019-10-01)\n\n\n### Bug Fixes\n\n* **dicts:** hjdict switch buttons. close [#489](https://github.com/crimx/ext-saladict/issues/489) ([a2718d4](https://github.com/crimx/ext-saladict/commit/a2718d4))\n* youdao translate ([02381da](https://github.com/crimx/ext-saladict/commit/02381da))\n\n\n\n### [7.0.3](https://github.com/crimx/ext-saladict/compare/v7.0.2...v7.0.3) (2019-09-30)\n\n\n### Bug Fixes\n\n* **panel:** fix mta box init focus with clipboard content [#487](https://github.com/crimx/ext-saladict/issues/487) ([789270d](https://github.com/crimx/ext-saladict/commit/789270d))\n* **qs:** selection on quick search panel ([3ede289](https://github.com/crimx/ext-saladict/commit/3ede289)), closes [#487](https://github.com/crimx/ext-saladict/issues/487)\n\n\n\n### [7.0.2](https://github.com/crimx/ext-saladict/compare/v7.0.0...v7.0.2) (2019-09-30)\n\n\n### Bug Fixes\n\n* **config:** number merging ([cb53388](https://github.com/crimx/ext-saladict/commit/cb53388))\n* **config:** update quick search location ([70bd9be](https://github.com/crimx/ext-saladict/commit/70bd9be)), closes [#479](https://github.com/crimx/ext-saladict/issues/479)\n* **dicts:** include cambridge dphrase block ([29f7b3c](https://github.com/crimx/ext-saladict/commit/29f7b3c)), closes [#480](https://github.com/crimx/ext-saladict/issues/480)\n* **pdf:** inject panel on firefox close [#477](https://github.com/crimx/ext-saladict/issues/477) ([a7ac72b](https://github.com/crimx/ext-saladict/commit/a7ac72b))\n* **popup:** correct popup width ([170fe72](https://github.com/crimx/ext-saladict/commit/170fe72)), closes [#481](https://github.com/crimx/ext-saladict/issues/481)\n* **sync:** update mkcol authorzation close [#475](https://github.com/crimx/ext-saladict/issues/475) ([2e433e5](https://github.com/crimx/ext-saladict/commit/2e433e5))\n\n### [7.0.1](https://github.com/crimx/ext-saladict/compare/v7.0.0...v7.0.1) (2019-09-30)\n\n\n### Bug Fixes\n\n* **config:** number merging ([569f69c](https://github.com/crimx/ext-saladict/commit/569f69c))\n* **config:** update quick search location ([34a6a07](https://github.com/crimx/ext-saladict/commit/34a6a07)), closes [#479](https://github.com/crimx/ext-saladict/issues/479)\n* **dicts:** include cambridge dphrase block ([807923f](https://github.com/crimx/ext-saladict/commit/807923f)), closes [#480](https://github.com/crimx/ext-saladict/issues/480)\n* **pdf:** inject panel on firefox close [#477](https://github.com/crimx/ext-saladict/issues/477) ([745bb75](https://github.com/crimx/ext-saladict/commit/745bb75))\n* **popup:** correct popup width ([6f14ba2](https://github.com/crimx/ext-saladict/commit/6f14ba2)), closes [#481](https://github.com/crimx/ext-saladict/issues/481)\n* **sync:** update mkcol authorzation close [#475](https://github.com/crimx/ext-saladict/issues/475) ([ddd6b77](https://github.com/crimx/ext-saladict/commit/ddd6b77))\n\n## [7.0.0](https://github.com/crimx/ext-saladict/compare/v6.33.2...v7.0.0) (2019-09-29)\n\n\n### Bug Fixes\n\n* **background:** show unsupported badge on internal tabs ([fe06a06](https://github.com/crimx/ext-saladict/commit/fe06a06))\n* **content:** correct history index ([bb94e87](https://github.com/crimx/ext-saladict/commit/bb94e87))\n* **dicts:** convert chs to chz on guoyu and liangan ([5e5a058](https://github.com/crimx/ext-saladict/commit/5e5a058))\n* **dicts:** correct text color on dark mode ([7484469](https://github.com/crimx/ext-saladict/commit/7484469))\n* **dicts:** fix tencent referer ([9c7b0de](https://github.com/crimx/ext-saladict/commit/9c7b0de))\n* **dicts:** params encoding ([c689e69](https://github.com/crimx/ext-saladict/commit/c689e69))\n* **dicts:** update sogou ([c0dffa1](https://github.com/crimx/ext-saladict/commit/c0dffa1))\n* **dicts:** update sogou api ([04a1e74](https://github.com/crimx/ext-saladict/commit/04a1e74))\n* **dicts:** update sogou api ([055f24e](https://github.com/crimx/ext-saladict/commit/055f24e))\n* **dicts:** update tencent api ([f13039f](https://github.com/crimx/ext-saladict/commit/f13039f))\n* **dicts:** url params encode ([3db28ff](https://github.com/crimx/ext-saladict/commit/3db28ff))\n* **options:** disable selection outside panel on options page ([491a791](https://github.com/crimx/ext-saladict/commit/491a791))\n* **options:** increase ant modal mask z-index ([337e92a](https://github.com/crimx/ext-saladict/commit/337e92a))\n* **options:** z-index on tooltips ([c94e340](https://github.com/crimx/ext-saladict/commit/c94e340))\n* **panel:** add to notebook on standalone panel ([c7b00e5](https://github.com/crimx/ext-saladict/commit/c7b00e5))\n* **panel:** calc hight changes on expand ([f6f335e](https://github.com/crimx/ext-saladict/commit/f6f335e))\n* **panel:** correct standalone css variables ([f0e087c](https://github.com/crimx/ext-saladict/commit/f0e087c))\n* **panel:** fancy scrollbar on standalone panel ([4734ab8](https://github.com/crimx/ext-saladict/commit/4734ab8))\n* **panel:** keep panel showing on options page ([d43ac1f](https://github.com/crimx/ext-saladict/commit/d43ac1f))\n* **panel:** normal scrollbar width on firefox ([a0385a8](https://github.com/crimx/ext-saladict/commit/a0385a8))\n* **panel:** remove Firefox button inner border ([00a069f](https://github.com/crimx/ext-saladict/commit/00a069f))\n* **selection:** add page info in selection ([b64e85e](https://github.com/crimx/ext-saladict/commit/b64e85e))\n* **selection:** check mouse target when anchor node is null ([1a2487f](https://github.com/crimx/ext-saladict/commit/1a2487f))\n* **selection:** keep panel coords when pinned ([7648247](https://github.com/crimx/ext-saladict/commit/7648247))\n* **selection:** skip extra selection change on Firefox ([754db43](https://github.com/crimx/ext-saladict/commit/754db43))\n* firefox ext api ([b8efad0](https://github.com/crimx/ext-saladict/commit/b8efad0))\n* lang check ([a8bfe92](https://github.com/crimx/ext-saladict/commit/a8bfe92))\n* **selection:** text field selection ([a8628b6](https://github.com/crimx/ext-saladict/commit/a8628b6))\n* remove buttons option on filrefox ([970b921](https://github.com/crimx/ext-saladict/commit/970b921))\n* remove scrollbar color on firefox ([a00214c](https://github.com/crimx/ext-saladict/commit/a00214c))\n* skip empty src for speaker ([65ff654](https://github.com/crimx/ext-saladict/commit/65ff654))\n* sync service download ([af05e51](https://github.com/crimx/ext-saladict/commit/af05e51))\n* **components:** add appear styles for shadow portal ([b84a8e8](https://github.com/crimx/ext-saladict/commit/b84a8e8))\n* **content:** max panel height calculation ([de30946](https://github.com/crimx/ext-saladict/commit/de30946))\n* **content:** search on bowl hover ([d7e126d](https://github.com/crimx/ext-saladict/commit/d7e126d))\n* **dicts:** axios api ([dda444d](https://github.com/crimx/ext-saladict/commit/dda444d))\n* **dicts:** encode uri component ([101ae50](https://github.com/crimx/ext-saladict/commit/101ae50))\n* **dicts:** update new speaker classname ([ad19c84](https://github.com/crimx/ext-saladict/commit/ad19c84))\n* **i18n:** sync init ([3aedb2d](https://github.com/crimx/ext-saladict/commit/3aedb2d))\n* **manifest:** new assets path ([67e3421](https://github.com/crimx/ext-saladict/commit/67e3421))\n* **options:** new quick search locations ([667dc13](https://github.com/crimx/ext-saladict/commit/667dc13))\n* **panel:** add dict item key ([e85f949](https://github.com/crimx/ext-saladict/commit/e85f949))\n* **panel:** env detection ([7817f54](https://github.com/crimx/ext-saladict/commit/7817f54))\n* **panel:** firefox detect height change ([9016d81](https://github.com/crimx/ext-saladict/commit/9016d81))\n* **panel:** fix sluggish scroll on Firefox ([d054f81](https://github.com/crimx/ext-saladict/commit/d054f81))\n* **panel:** open options page when clicking icon ([6e2dc5e](https://github.com/crimx/ext-saladict/commit/6e2dc5e))\n* **panel:** prevent textarea input event propagation ([36285ff](https://github.com/crimx/ext-saladict/commit/36285ff))\n* **popup:** qrcode panel z-index ([0234943](https://github.com/crimx/ext-saladict/commit/0234943))\n* **selection:** skip extra event after instant capture ([1a01ac5](https://github.com/crimx/ext-saladict/commit/1a01ac5))\n* **storybook:** add width for panel wrapper ([276139c](https://github.com/crimx/ext-saladict/commit/276139c))\n* **wordpage:** context translation ([3f01b81](https://github.com/crimx/ext-saladict/commit/3f01b81))\n* context menus locale name ([2617939](https://github.com/crimx/ext-saladict/commit/2617939))\n* correctly made payload and meta optional ([9ac6fb3](https://github.com/crimx/ext-saladict/commit/9ac6fb3))\n* css type ([de9b809](https://github.com/crimx/ext-saladict/commit/de9b809))\n* firefox bugs ([efab253](https://github.com/crimx/ext-saladict/commit/efab253))\n* **panel:** fix menu bar shrinking ([2e0c8fc](https://github.com/crimx/ext-saladict/commit/2e0c8fc))\n* **panel:** panel opcaity transition ([673ce82](https://github.com/crimx/ext-saladict/commit/673ce82))\n* **panel:** typo ([9f7626d](https://github.com/crimx/ext-saladict/commit/9f7626d))\n* dom purify parse innerHTML ([6af3120](https://github.com/crimx/ext-saladict/commit/6af3120))\n* getFullLink supports other protocols ([6b08d5f](https://github.com/crimx/ext-saladict/commit/6b08d5f))\n* locale format ([3439005](https://github.com/crimx/ext-saladict/commit/3439005))\n* nested p tags ([fb69f55](https://github.com/crimx/ext-saladict/commit/fb69f55))\n* prevent dict panel being closed ([9c3fd0b](https://github.com/crimx/ext-saladict/commit/9c3fd0b))\n* relative url ([2a565a6](https://github.com/crimx/ext-saladict/commit/2a565a6))\n* reove style global reset ([5d89ebd](https://github.com/crimx/ext-saladict/commit/5d89ebd))\n* union hack ([d0d3cdd](https://github.com/crimx/ext-saladict/commit/d0d3cdd))\n* **storybook:** disable storybook shortcuts ([6da3254](https://github.com/crimx/ext-saladict/commit/6da3254))\n* **storybook:** prevent full rerender ([fe996dd](https://github.com/crimx/ext-saladict/commit/fe996dd))\n* **storybook:** skip wrapper components ([cd370c9](https://github.com/crimx/ext-saladict/commit/cd370c9))\n* update namespace ([9f1e253](https://github.com/crimx/ext-saladict/commit/9f1e253))\n\n\n### Build System\n\n* add shadow dom css support and storybook addons ([211986a](https://github.com/crimx/ext-saladict/commit/211986a))\n* add storybook ([5e1e88e](https://github.com/crimx/ext-saladict/commit/5e1e88e))\n* fix mjs type ([d76f6c7](https://github.com/crimx/ext-saladict/commit/d76f6c7))\n* new pack script ([78ee6aa](https://github.com/crimx/ext-saladict/commit/78ee6aa))\n* remove style loader on development ([e4bd588](https://github.com/crimx/ext-saladict/commit/e4bd588))\n* rename jsonp function ([5d4941c](https://github.com/crimx/ext-saladict/commit/5d4941c))\n* split webpack chunks ([ad90c96](https://github.com/crimx/ext-saladict/commit/ad90c96))\n* update build system to neutrino and babel-ts ([b3b05c3](https://github.com/crimx/ext-saladict/commit/b3b05c3))\n\n\n### Features\n\n* **panel:** add fancy scrollbar ([4be6ac1](https://github.com/crimx/ext-saladict/commit/4be6ac1))\n* **popup:** add options for opening standalone panel [#470](https://github.com/crimx/ext-saladict/issues/470) ([2f0be7e](https://github.com/crimx/ext-saladict/commit/2f0be7e))\n* **profile:** add nihongo profile ([285b08b](https://github.com/crimx/ext-saladict/commit/285b08b))\n* add dark mode ([a9c9407](https://github.com/crimx/ext-saladict/commit/a9c9407))\n* add shadow portal ([3d3e025](https://github.com/crimx/ext-saladict/commit/3d3e025))\n\n\n### Tests\n\n* update browser api specs ([768ce07](https://github.com/crimx/ext-saladict/commit/768ce07))\n* update mocks ([cefd766](https://github.com/crimx/ext-saladict/commit/cefd766))\n* **dicts:** add bing mock requests ([641d9db](https://github.com/crimx/ext-saladict/commit/641d9db))\n* **dicts:** remove mock text ([3d57c20](https://github.com/crimx/ext-saladict/commit/3d57c20))\n* **dicts:** udapte googledict html ([c5c6b80](https://github.com/crimx/ext-saladict/commit/c5c6b80))\n* **storybook:** update stories ([780fade](https://github.com/crimx/ext-saladict/commit/780fade))\n* added jest ([99484c7](https://github.com/crimx/ext-saladict/commit/99484c7))\n* clean old test ([074f058](https://github.com/crimx/ext-saladict/commit/074f058))\n* refactor background ([938aeea](https://github.com/crimx/ext-saladict/commit/938aeea))\n* **storybook:** add dictionaries stories ([0714aed](https://github.com/crimx/ext-saladict/commit/0714aed))\n\n\n### BREAKING CHANGES\n\n* No compatible with the old build system\n\n### [6.33.7](https://github.com/crimx/ext-saladict/compare/v6.33.6...v6.33.7) (2019-09-13)\n\n\n\n### [6.33.6](https://github.com/crimx/ext-saladict/compare/v6.33.5...v6.33.6) (2019-09-12)\n\n\n### Bug Fixes\n\n* fit the outdated typings ([f019572](https://github.com/crimx/ext-saladict/commit/f019572))\n* update dicts ([4492cd0](https://github.com/crimx/ext-saladict/commit/4492cd0))\n\n\n### [6.33.5](https://github.com/crimx/ext-saladict/compare/v6.33.4...v6.33.5) (2019-08-11)\n\n\n### Bug Fixes\n\n* change the checksums of panel.css ([e2ed394](https://github.com/crimx/ext-saladict/commit/e2ed394))\n\n\n\n### [6.33.4](https://github.com/crimx/ext-saladict/compare/v6.33.3...v6.33.4) (2019-08-09)\n\n\n### Bug Fixes\n\n* **manifest:** fix chrome 67 bug ([bca3b56](https://github.com/crimx/ext-saladict/commit/bca3b56))\n\n\n\n### [6.33.3](https://github.com/crimx/ext-saladict/compare/v6.33.2...v6.33.3) (2019-08-08)\n\n\n### Bug Fixes\n\n* **manifest:** remvoe update url ([f83a485](https://github.com/crimx/ext-saladict/commit/f83a485))\n\n\n<a name=\"6.33.2\"></a>\n## [6.33.2](https://github.com/crimx/ext-saladict/compare/v6.33.1...v6.33.2) (2019-06-27)\n\n\n\n<a name=\"6.33.1\"></a>\n## [6.33.1](https://github.com/crimx/ext-saladict/compare/v6.33.0...v6.33.1) (2019-06-15)\n\n\n### Bug Fixes\n\n* **dicts:** https audio ([d8d569f](https://github.com/crimx/ext-saladict/commit/d8d569f))\n\n\n\n<a name=\"6.33.0\"></a>\n# [6.33.0](https://github.com/crimx/ext-saladict/compare/v6.32.0...v6.33.0) (2019-06-12)\n\n\n### Bug Fixes\n\n* **dicts:** baidu options mixed with google ([239527e](https://github.com/crimx/ext-saladict/commit/239527e))\n* **selection:** context extraction ([ec7421d](https://github.com/crimx/ext-saladict/commit/ec7421d))\n\n\n### Features\n\n* **dicts:** add weblio ejje ([5b01e1e](https://github.com/crimx/ext-saladict/commit/5b01e1e))\n\n\n\n<a name=\"6.32.0\"></a>\n# [6.32.0](https://github.com/crimx/ext-saladict/compare/v6.31.1...v6.32.0) (2019-05-31)\n\n\n### Bug Fixes\n\n* same origin iframe ([39bacf9](https://github.com/crimx/ext-saladict/commit/39bacf9)), closes [#373](https://github.com/crimx/ext-saladict/issues/373)\n* **dicts:** update zdic ([ebef2ce](https://github.com/crimx/ext-saladict/commit/ebef2ce))\n* **options:** styles ([50cc464](https://github.com/crimx/ext-saladict/commit/50cc464))\n* assgin timeout ticket ([23f85c5](https://github.com/crimx/ext-saladict/commit/23f85c5))\n* correct popup page id ([45db00b](https://github.com/crimx/ext-saladict/commit/45db00b))\n* ignore esc key on standalone panel ([1970556](https://github.com/crimx/ext-saladict/commit/1970556))\n\n\n### Features\n\n* add audio control ([6df1682](https://github.com/crimx/ext-saladict/commit/6df1682))\n* add soundtouch ([ca7e7f8](https://github.com/crimx/ext-saladict/commit/ca7e7f8))\n\n\n\n<a name=\"6.31.1\"></a>\n## [6.31.1](https://github.com/crimx/ext-saladict/compare/v6.31.0...v6.31.1) (2019-05-25)\n\n\n### Bug Fixes\n\n* **dicts:** multiline result ([b17e915](https://github.com/crimx/ext-saladict/commit/b17e915))\n\n\n\n<a name=\"6.31.0\"></a>\n# [6.31.0](https://github.com/crimx/ext-saladict/compare/v6.30.0...v6.31.0) (2019-05-24)\n\n\n### Bug Fixes\n\n* **dicts:** caiyun options ([3efae72](https://github.com/crimx/ext-saladict/commit/3efae72))\n* update typings ([a3074e0](https://github.com/crimx/ext-saladict/commit/a3074e0))\n* **options:** use short title to prevent overflow ([52fb802](https://github.com/crimx/ext-saladict/commit/52fb802))\n\n\n### Features\n\n* **dicts:** add caiyun ([92ad971](https://github.com/crimx/ext-saladict/commit/92ad971))\n* **dicts:** add tencent translate ([46657ea](https://github.com/crimx/ext-saladict/commit/46657ea))\n\n\n\n<a name=\"6.30.0\"></a>\n# [6.30.0](https://github.com/crimx/ext-saladict/compare/v6.29.0...v6.30.0) (2019-05-11)\n\n\n### Bug Fixes\n\n* **sync:** replace settimeout with alarms ([05f1260](https://github.com/crimx/ext-saladict/commit/05f1260)), closes [#361](https://github.com/crimx/ext-saladict/issues/361)\n* check empty word fields ([60f8066](https://github.com/crimx/ext-saladict/commit/60f8066)), closes [#363](https://github.com/crimx/ext-saladict/issues/363)\n* typo ([8b0f3ff](https://github.com/crimx/ext-saladict/commit/8b0f3ff))\n* **dicts:** get correct lang list on consecutive searches ([4ad4dcf](https://github.com/crimx/ext-saladict/commit/4ad4dcf)), closes [#360](https://github.com/crimx/ext-saladict/issues/360)\n\n\n### Features\n\n* **dicts:** add jukuu ([a4775fd](https://github.com/crimx/ext-saladict/commit/a4775fd))\n\n\n\n<a name=\"6.29.0\"></a>\n# [6.29.0](https://github.com/crimx/ext-saladict/compare/v6.28.1...v6.29.0) (2019-05-02)\n\n\n### Bug Fixes\n\n* **panel:** correct history forward btn ([ee1d4f6](https://github.com/crimx/ext-saladict/commit/ee1d4f6)), closes [#349](https://github.com/crimx/ext-saladict/issues/349)\n* add z-index to google page translate ([f59cc57](https://github.com/crimx/ext-saladict/commit/f59cc57))\n\n\n### Features\n\n* **dicts:** add cnki ([2743cac](https://github.com/crimx/ext-saladict/commit/2743cac)), closes [#336](https://github.com/crimx/ext-saladict/issues/336)\n* add comp EntryBox ([fdd71dd](https://github.com/crimx/ext-saladict/commit/fdd71dd))\n\n\n\n<a name=\"6.28.1\"></a>\n## [6.28.1](https://github.com/crimx/ext-saladict/compare/v6.28.0...v6.28.1) (2019-04-17)\n\n\n### Bug Fixes\n\n* add z-index to google page translate elements ([38b08e5](https://github.com/crimx/ext-saladict/commit/38b08e5))\n\n\n\n<a name=\"6.28.0\"></a>\n# [6.28.0](https://github.com/crimx/ext-saladict/compare/v6.27.8...v6.28.0) (2019-04-17)\n\n\n### Features\n\n* add standalone sidebar layout ([6f4c5b8](https://github.com/crimx/ext-saladict/commit/6f4c5b8))\n\n\n\n<a name=\"6.27.8\"></a>\n## [6.27.8](https://github.com/crimx/ext-saladict/compare/v6.27.7...v6.27.8) (2019-03-31)\n\n\n### Bug Fixes\n\n* **options:** correct import and export options ([cbf2921](https://github.com/crimx/ext-saladict/commit/cbf2921))\n\n\n\n<a name=\"6.27.7\"></a>\n## [6.27.7](https://github.com/crimx/ext-saladict/compare/v6.27.6...v6.27.7) (2019-03-27)\n\n\n### Bug Fixes\n\n* **panel:** proper update dict styles ([264c731](https://github.com/crimx/ext-saladict/commit/264c731)), closes [#331](https://github.com/crimx/ext-saladict/issues/331)\n\n\n\n<a name=\"6.27.6\"></a>\n## [6.27.6](https://github.com/crimx/ext-saladict/compare/v6.27.5...v6.27.6) (2019-03-27)\n\n\n### Bug Fixes\n\n* **panel:** correct dict style update ([81a1d08](https://github.com/crimx/ext-saladict/commit/81a1d08))\n\n\n\n<a name=\"6.27.5\"></a>\n## [6.27.5](https://github.com/crimx/ext-saladict/compare/v6.27.4...v6.27.5) (2019-03-24)\n\n\n### Bug Fixes\n\n* **sync:** only sync on notebook changes ([90d4183](https://github.com/crimx/ext-saladict/commit/90d4183))\n\n\n\n<a name=\"6.27.4\"></a>\n## [6.27.4](https://github.com/crimx/ext-saladict/compare/v6.27.3...v6.27.4) (2019-03-23)\n\n\n### Bug Fixes\n\n* **panel:** frame head typos ([94a055f](https://github.com/crimx/ext-saladict/commit/94a055f))\n* **sync:** proper trun off shanbay ([af93ba0](https://github.com/crimx/ext-saladict/commit/af93ba0))\n\n\n\n<a name=\"6.27.3\"></a>\n## [6.27.3](https://github.com/crimx/ext-saladict/compare/v6.27.2...v6.27.3) (2019-03-19)\n\n\n### Bug Fixes\n\n* **dicts:** update sogou token ([570dc90](https://github.com/crimx/ext-saladict/commit/570dc90))\n\n\n\n<a name=\"6.27.2\"></a>\n## [6.27.2](https://github.com/crimx/ext-saladict/compare/v6.27.1...v6.27.2) (2019-03-18)\n\n\n### Bug Fixes\n\n* fix google translate ([d8715d1](https://github.com/crimx/ext-saladict/commit/d8715d1))\n* **dicts:** disable passive wheel events on lastest Chrome ([9e8c3f0](https://github.com/crimx/ext-saladict/commit/9e8c3f0))\n\n\n\n<a name=\"6.27.1\"></a>\n## [6.27.1](https://github.com/crimx/ext-saladict/compare/v6.27.0...v6.27.1) (2019-03-17)\n\n\n### Bug Fixes\n\n* **manifest:** firefox incognito mode ([58b946f](https://github.com/crimx/ext-saladict/commit/58b946f))\n\n\n\n<a name=\"6.27.0\"></a>\n# [6.27.0](https://github.com/crimx/ext-saladict/compare/v6.26.0...v6.27.0) (2019-03-17)\n\n\n### Bug Fixes\n\n* compress data ([3795836](https://github.com/crimx/ext-saladict/commit/3795836))\n* **dicts:** fix shanbay typing warning ([99caa99](https://github.com/crimx/ext-saladict/commit/99caa99))\n* **dicts:** prevent in-panel search ([f88b960](https://github.com/crimx/ext-saladict/commit/f88b960))\n* **dicts:** remove float elements ([143b258](https://github.com/crimx/ext-saladict/commit/143b258))\n* **dicts:** typings ([bafe61c](https://github.com/crimx/ext-saladict/commit/bafe61c))\n* **manifest:** load pdf viewer under incognito mode ([5d57b25](https://github.com/crimx/ext-saladict/commit/5d57b25))\n* **menus:** prevent items being removed in incognito mode ([a380980](https://github.com/crimx/ext-saladict/commit/a380980))\n* **panel:** disable fav icon on options page ([c616149](https://github.com/crimx/ext-saladict/commit/c616149))\n* typings ([7f382a2](https://github.com/crimx/ext-saladict/commit/7f382a2))\n* **panel:** center panel vertically when word editor shows up ([c31b5fa](https://github.com/crimx/ext-saladict/commit/c31b5fa)), closes [#315](https://github.com/crimx/ext-saladict/issues/315)\n* **panel:** max z-index for dict panel ([51b60d5](https://github.com/crimx/ext-saladict/commit/51b60d5)), closes [#316](https://github.com/crimx/ext-saladict/issues/316)\n* **sync:** duration ([4785a71](https://github.com/crimx/ext-saladict/commit/4785a71))\n\n\n### Features\n\n* **dicts:** add shanbay dictionary ([95ee0d5](https://github.com/crimx/ext-saladict/commit/95ee0d5))\n* **panel:** add custom css ([4c58886](https://github.com/crimx/ext-saladict/commit/4c58886))\n* **sync:** add shanbay ([a7389d5](https://github.com/crimx/ext-saladict/commit/a7389d5))\n\n\n### Performance Improvements\n\n* cache lang checks ([5e3034e](https://github.com/crimx/ext-saladict/commit/5e3034e))\n\n\n\n<a name=\"6.26.0\"></a>\n# [6.26.0](https://github.com/crimx/ext-saladict/compare/v6.25.1...v6.26.0) (2019-03-09)\n\n\n### Bug Fixes\n\n* **dicts:** add mp3 playing and fixing styles ([aefd2b1](https://github.com/crimx/ext-saladict/commit/aefd2b1))\n* **options:** change wording close [#314](https://github.com/crimx/ext-saladict/issues/314) ([17aa162](https://github.com/crimx/ext-saladict/commit/17aa162))\n* **selection:** match frames ([c923ce8](https://github.com/crimx/ext-saladict/commit/c923ce8))\n\n\n### Features\n\n* **menus:** google page translation ([a023074](https://github.com/crimx/ext-saladict/commit/a023074))\n\n\n### Performance Improvements\n\n* **panel:** prevent flickering when switching profiles ([c0bc97f](https://github.com/crimx/ext-saladict/commit/c0bc97f))\n\n\n\n<a name=\"6.25.1\"></a>\n## [6.25.1](https://github.com/crimx/ext-saladict/compare/v6.25.0...v6.25.1) (2019-03-04)\n\n\n### Bug Fixes\n\n* firefox style error ([8256e63](https://github.com/crimx/ext-saladict/commit/8256e63))\n* **dicts:** omit cookies ([3b6d1a8](https://github.com/crimx/ext-saladict/commit/3b6d1a8)), closes [#312](https://github.com/crimx/ext-saladict/issues/312)\n* correct lang selection ([86add32](https://github.com/crimx/ext-saladict/commit/86add32))\n\n\n\n<a name=\"6.25.0\"></a>\n# [6.25.0](https://github.com/crimx/ext-saladict/compare/v6.24.4...v6.25.0) (2019-03-02)\n\n\n### Bug Fixes\n\n* type error ([75d93d9](https://github.com/crimx/ext-saladict/commit/75d93d9))\n* **dicts:** update hjdict korean page ([66c7341](https://github.com/crimx/ext-saladict/commit/66c7341))\n* **options:** popup options ([5701525](https://github.com/crimx/ext-saladict/commit/5701525))\n* **options:** styling ([dca805f](https://github.com/crimx/ext-saladict/commit/dca805f))\n* **options:** wording ([087102a](https://github.com/crimx/ext-saladict/commit/087102a))\n* **panel:** prevent drag event losing ([05dbaec](https://github.com/crimx/ext-saladict/commit/05dbaec))\n* better korean rendering ([e13b51e](https://github.com/crimx/ext-saladict/commit/e13b51e))\n\n\n### Features\n\n* **dicts:** add dict naver ([cef45b4](https://github.com/crimx/ext-saladict/commit/cef45b4))\n\n\n### Performance Improvements\n\n* **panel:** faster style loading ([e2757af](https://github.com/crimx/ext-saladict/commit/e2757af))\n* **panel:** remove extra update for auto-pasting ([2f7182b](https://github.com/crimx/ext-saladict/commit/2f7182b))\n\n\n\n<a name=\"6.24.4\"></a>\n## [6.24.4](https://github.com/crimx/ext-saladict/compare/v6.24.3...v6.24.4) (2019-02-17)\n\n\n### Bug Fixes\n\n* **dicts:** update sogou token ([82b18cf](https://github.com/crimx/ext-saladict/commit/82b18cf))\n\n\n\n<a name=\"6.24.3\"></a>\n## [6.24.3](https://github.com/crimx/ext-saladict/compare/v6.24.2...v6.24.3) (2019-02-13)\n\n\n### Bug Fixes\n\n* csp ([7d8790d](https://github.com/crimx/ext-saladict/commit/7d8790d))\n* fix analytics ([adebbd3](https://github.com/crimx/ext-saladict/commit/adebbd3))\n\n\n\n<a name=\"6.24.2\"></a>\n## [6.24.2](https://github.com/crimx/ext-saladict/compare/v6.24.1...v6.24.2) (2019-02-13)\n\n\n### Bug Fixes\n\n* **panel:** fix changing page title ([1fe0acc](https://github.com/crimx/ext-saladict/commit/1fe0acc))\n* **panel:** fix fav icon ([5f0433f](https://github.com/crimx/ext-saladict/commit/5f0433f))\n\n\n\n<a name=\"6.24.1\"></a>\n## [6.24.1](https://github.com/crimx/ext-saladict/compare/v6.24.0...v6.24.1) (2019-02-13)\n\n\n### Bug Fixes\n\n* **background:** proper init ([9babdef](https://github.com/crimx/ext-saladict/commit/9babdef))\n\n\n\n<a name=\"6.24.0\"></a>\n# [6.24.0](https://github.com/crimx/ext-saladict/compare/v6.23.1...v6.24.0) (2019-02-12)\n\n\n### Bug Fixes\n\n* **selection:** fixed [#296](https://github.com/crimx/ext-saladict/issues/296) ([1f9b6a6](https://github.com/crimx/ext-saladict/commit/1f9b6a6))\n* fix typo ([129e863](https://github.com/crimx/ext-saladict/commit/129e863))\n* **options:** add syncConfig ([51a9e57](https://github.com/crimx/ext-saladict/commit/51a9e57))\n\n\n### Features\n\n* **config:** add baidu to ctxTrans ([a2c1fda](https://github.com/crimx/ext-saladict/commit/a2c1fda))\n* **dicts:** add baidu ([a9fecee](https://github.com/crimx/ext-saladict/commit/a9fecee))\n* add analytics ([48582bd](https://github.com/crimx/ext-saladict/commit/48582bd))\n\n\n\n<a name=\"6.23.1\"></a>\n## [6.23.1](https://github.com/crimx/ext-saladict/compare/v6.23.0...v6.23.1) (2019-01-28)\n\n\n### Bug Fixes\n\n* **options:** fix reset related errors ([55654e0](https://github.com/crimx/ext-saladict/commit/55654e0))\n* **options:** wording ([8a47354](https://github.com/crimx/ext-saladict/commit/8a47354))\n* **panel:** correct init selection ([b4a13eb](https://github.com/crimx/ext-saladict/commit/b4a13eb))\n\n\n\n<a name=\"6.23.0\"></a>\n# [6.23.0](https://github.com/crimx/ext-saladict/compare/v6.22.8...v6.23.0) (2019-01-24)\n\n\n### Bug Fixes\n\n* **panel:** open notebook on right click ([0099024](https://github.com/crimx/ext-saladict/commit/0099024))\n* close [#289](https://github.com/crimx/ext-saladict/issues/289) ([1615794](https://github.com/crimx/ext-saladict/commit/1615794))\n* **options:** add description ([deca4cb](https://github.com/crimx/ext-saladict/commit/deca4cb))\n* **options:** add valuePropName for switch ([8574a30](https://github.com/crimx/ext-saladict/commit/8574a30))\n* **options:** close modal ([b241d8b](https://github.com/crimx/ext-saladict/commit/b241d8b))\n* **options:** fix holding toggling ([5f7cdfe](https://github.com/crimx/ext-saladict/commit/5f7cdfe))\n* **options:** get profile id list on init ([114ccf0](https://github.com/crimx/ext-saladict/commit/114ccf0))\n* **options:** keep modal hide animation ([18ce805](https://github.com/crimx/ext-saladict/commit/18ce805))\n* **options:** remove unused ([0c6ea6d](https://github.com/crimx/ext-saladict/commit/0c6ea6d))\n* **popup:** fix popup flickering ([90b7d72](https://github.com/crimx/ext-saladict/commit/90b7d72))\n* **selection:** extract sentence head ([d5649e0](https://github.com/crimx/ext-saladict/commit/d5649e0)), closes [#287](https://github.com/crimx/ext-saladict/issues/287)\n* disable warning on dev ([2abc24a](https://github.com/crimx/ext-saladict/commit/2abc24a))\n* fix config typing ([d164efb](https://github.com/crimx/ext-saladict/commit/d164efb))\n* fix type error ([3db0b88](https://github.com/crimx/ext-saladict/commit/3db0b88))\n* **options:** update active profile name on init ([83cadf3](https://github.com/crimx/ext-saladict/commit/83cadf3))\n* remove activeProfileID when reset ([bbd5f01](https://github.com/crimx/ext-saladict/commit/bbd5f01))\n* **options:** replace p elements with lis ([ed42ccb](https://github.com/crimx/ext-saladict/commit/ed42ccb))\n* **profiles:** fix addActiveProfileListener ([2c67642](https://github.com/crimx/ext-saladict/commit/2c67642))\n* langcode comparison ([4dade9b](https://github.com/crimx/ext-saladict/commit/4dade9b))\n\n\n### Features\n\n* **content:** add salad bowl clicking ([e6834af](https://github.com/crimx/ext-saladict/commit/e6834af))\n* **popup:** add browser action behaviors ([6672a7a](https://github.com/crimx/ext-saladict/commit/6672a7a)), closes [#280](https://github.com/crimx/ext-saladict/issues/280)\n* add context translate engines config ([52e390b](https://github.com/crimx/ext-saladict/commit/52e390b))\n\n\n\n<a name=\"6.22.8\"></a>\n## [6.22.8](https://github.com/crimx/ext-saladict/compare/v6.22.7...v6.22.8) (2019-01-07)\n\n\n### Bug Fixes\n\n* blacklist stackedit.io ([775298d](https://github.com/crimx/ext-saladict/commit/775298d)), closes [#277](https://github.com/crimx/ext-saladict/issues/277)\n* encode uri ([6098e34](https://github.com/crimx/ext-saladict/commit/6098e34))\n* ignore &[#8203](https://github.com/crimx/ext-saladict/issues/8203); ([156275b](https://github.com/crimx/ext-saladict/commit/156275b)), closes [#274](https://github.com/crimx/ext-saladict/issues/274)\n\n\n### Performance Improvements\n\n* faster matching sentence head ([3fa2fb6](https://github.com/crimx/ext-saladict/commit/3fa2fb6)), closes [#274](https://github.com/crimx/ext-saladict/issues/274)\n\n\n\n<a name=\"6.22.7\"></a>\n## [6.22.7](https://github.com/crimx/ext-saladict/compare/v6.22.6...v6.22.7) (2018-12-31)\n\n\n### Bug Fixes\n\n* better pdf context menu ([e7ada83](https://github.com/crimx/ext-saladict/commit/e7ada83))\n* keep empty selected dicts ([acf3fb3](https://github.com/crimx/ext-saladict/commit/acf3fb3))\n\n\n\n<a name=\"6.22.6\"></a>\n## [6.22.6](https://github.com/crimx/ext-saladict/compare/v6.22.5...v6.22.6) (2018-12-23)\n\n\n### Bug Fixes\n\n* fix pdf fetching ([c79fc89](https://github.com/crimx/ext-saladict/commit/c79fc89))\n\n\n\n<a name=\"6.22.5\"></a>\n## [6.22.5](https://github.com/crimx/ext-saladict/compare/v6.22.4...v6.22.5) (2018-12-21)\n\n\n### Bug Fixes\n\n* fix google dict image src error ([682ae16](https://github.com/crimx/ext-saladict/commit/682ae16))\n\n\n\n<a name=\"6.22.4\"></a>\n## [6.22.4](https://github.com/crimx/ext-saladict/compare/v6.22.3...v6.22.4) (2018-12-21)\n\n\n\n<a name=\"6.22.3\"></a>\n## [6.22.3](https://github.com/crimx/ext-saladict/compare/v6.22.2...v6.22.3) (2018-12-20)\n\n\n### Bug Fixes\n\n* **dicts:** extract cambridge plurals ([2124c17](https://github.com/crimx/ext-saladict/commit/2124c17))\n* **dicts:** update cambridge parser Closes [#266](https://github.com/crimx/ext-saladict/issues/266) ([b996c87](https://github.com/crimx/ext-saladict/commit/b996c87))\n\n\n\n<a name=\"6.22.2\"></a>\n## [6.22.2](https://github.com/crimx/ext-saladict/compare/v6.22.1...v6.22.2) (2018-12-14)\n\n\n### Bug Fixes\n\n* **dicts:** update sogou api ([22f4d13](https://github.com/crimx/ext-saladict/commit/22f4d13))\n\n\n\n<a name=\"6.22.1\"></a>\n## [6.22.1](https://github.com/crimx/ext-saladict/compare/v6.22.0...v6.22.1) (2018-12-10)\n\n\n### Bug Fixes\n\n* **dicts:** fix hjdict cookies ([b89618f](https://github.com/crimx/ext-saladict/commit/b89618f))\n* **dicts:** use xhr to avoid Origin header ([36ffb4c](https://github.com/crimx/ext-saladict/commit/36ffb4c)), closes [#259](https://github.com/crimx/ext-saladict/issues/259)\n\n\n\n<a name=\"6.22.0\"></a>\n# [6.22.0](https://github.com/crimx/ext-saladict/compare/v6.21.2...v6.22.0) (2018-12-04)\n\n\n### Bug Fixes\n\n* generate styles for all selected dicts ([1de0599](https://github.com/crimx/ext-saladict/commit/1de0599))\n* highlight window and tab is exist ([840c085](https://github.com/crimx/ext-saladict/commit/840c085)), closes [#251](https://github.com/crimx/ext-saladict/issues/251)\n* **dicts:** add id when searching ([246c9af](https://github.com/crimx/ext-saladict/commit/246c9af))\n* inject panel to every page on install ([f108f2e](https://github.com/crimx/ext-saladict/commit/f108f2e))\n* **manifest:** make dicts styles web accessible ([2bc0dff](https://github.com/crimx/ext-saladict/commit/2bc0dff))\n\n\n### Features\n\n* **dicts:** add dict Hjdict ([76b8210](https://github.com/crimx/ext-saladict/commit/76b8210)), closes [#252](https://github.com/crimx/ext-saladict/issues/252)\n* **helpers:** add lang check ([5375f67](https://github.com/crimx/ext-saladict/commit/5375f67))\n\n\n### Performance Improvements\n\n* **dicts:** only load necessary styles ([e15fbdd](https://github.com/crimx/ext-saladict/commit/e15fbdd))\n\n\n\n<a name=\"6.21.2\"></a>\n## [6.21.2](https://github.com/crimx/ext-saladict/compare/v6.21.1...v6.21.2) (2018-11-28)\n\n\n### Bug Fixes\n\n* **dicts:** encode sogou ([cd2cbc5](https://github.com/crimx/ext-saladict/commit/cd2cbc5))\n\n\n\n<a name=\"6.21.1\"></a>\n## [6.21.1](https://github.com/crimx/ext-saladict/compare/v6.21.0...v6.21.1) (2018-11-26)\n\n\n### Bug Fixes\n\n* merge config ([791b7df](https://github.com/crimx/ext-saladict/commit/791b7df))\n\n\n\n<a name=\"6.21.0\"></a>\n# [6.21.0](https://github.com/crimx/ext-saladict/compare/v6.20.2...v6.21.0) (2018-11-25)\n\n\n### Bug Fixes\n\n* download exported wordpage files on Firefox ([aaba271](https://github.com/crimx/ext-saladict/commit/aaba271))\n* **dicts:** sogou api ([bdd0bb5](https://github.com/crimx/ext-saladict/commit/bdd0bb5))\n* **helpers:** remove Chs chars from Korean matching ([1daed3c](https://github.com/crimx/ext-saladict/commit/1daed3c)), closes [#249](https://github.com/crimx/ext-saladict/issues/249)\n* remove unused ([151bfb5](https://github.com/crimx/ext-saladict/commit/151bfb5))\n\n\n### Features\n\n* **dict:** add dict wikipedia ([603c558](https://github.com/crimx/ext-saladict/commit/603c558))\n\n\n\n<a name=\"6.20.2\"></a>\n## [6.20.2](https://github.com/crimx/ext-saladict/compare/v6.20.1...v6.20.2) (2018-11-09)\n\n\n### Bug Fixes\n\n* **content:** exit after deleting word ([af34a37](https://github.com/crimx/ext-saladict/commit/af34a37)), closes [#245](https://github.com/crimx/ext-saladict/issues/245)\n\n\n\n<a name=\"6.20.1\"></a>\n## [6.20.1](https://github.com/crimx/ext-saladict/compare/v6.20.0...v6.20.1) (2018-11-03)\n\n\n### Bug Fixes\n\n* suggests panel ([8f4e13d](https://github.com/crimx/ext-saladict/commit/8f4e13d))\n\n\n\n<a name=\"6.20.0\"></a>\n# [6.20.0](https://github.com/crimx/ext-saladict/compare/v6.19.0...v6.20.0) (2018-11-03)\n\n\n### Bug Fixes\n\n* **content:** search context when jumping from popup to wordpage ([c1703ef](https://github.com/crimx/ext-saladict/commit/c1703ef))\n* fix trans component ([0711058](https://github.com/crimx/ext-saladict/commit/0711058))\n* **dicts:** remove trimming ([44f1fc4](https://github.com/crimx/ext-saladict/commit/44f1fc4))\n* **panel:** fix suggests panel logic ([dff562d](https://github.com/crimx/ext-saladict/commit/dff562d))\n\n\n### Features\n\n* **dicts:** add options to remove linebreaks on PDF ([8b72b69](https://github.com/crimx/ext-saladict/commit/8b72b69))\n* add search suggests ([f1f458b](https://github.com/crimx/ext-saladict/commit/f1f458b))\n\n\n\n<a name=\"6.19.0\"></a>\n# [6.19.0](https://github.com/crimx/ext-saladict/compare/v6.18.1...v6.19.0) (2018-11-01)\n\n\n### Bug Fixes\n\n* **configs:** new value could be empty when deleting ([5a5fad5](https://github.com/crimx/ext-saladict/commit/5a5fad5))\n* **dicts:** trim text ([5dac1e7](https://github.com/crimx/ext-saladict/commit/5dac1e7))\n* **helpers:** ignore irrelevant events ([72aa11a](https://github.com/crimx/ext-saladict/commit/72aa11a))\n* **sync:** correct interval repeat ([0c7b607](https://github.com/crimx/ext-saladict/commit/0c7b607))\n* **wordpage:** reset selected rows on full fetch ([9f0f42e](https://github.com/crimx/ext-saladict/commit/9f0f42e))\n\n\n### Features\n\n* **background:** add badge text ([8477f65](https://github.com/crimx/ext-saladict/commit/8477f65))\n* **helpers:** add webdav sync service ([64df7c3](https://github.com/crimx/ext-saladict/commit/64df7c3))\n* **sync:** add sync options ([73e4ce6](https://github.com/crimx/ext-saladict/commit/73e4ce6))\n* add shift for instant search ([20a942a](https://github.com/crimx/ext-saladict/commit/20a942a)), closes [#232](https://github.com/crimx/ext-saladict/issues/232)\n\n\n\n<a name=\"6.18.1\"></a>\n## [6.18.1](https://github.com/crimx/ext-saladict/compare/v6.18.0...v6.18.1) (2018-10-17)\n\n\n\n<a name=\"6.18.0\"></a>\n# [6.18.0](https://github.com/crimx/ext-saladict/compare/v6.17.1...v6.18.0) (2018-10-16)\n\n\n### Bug Fixes\n\n* **content:** fix triple ctrl switch ([d1dcdeb](https://github.com/crimx/ext-saladict/commit/d1dcdeb)), closes [#222](https://github.com/crimx/ext-saladict/issues/222)\n\n\n### Features\n\n* **content:** add more keys for holding mode ([bda8c07](https://github.com/crimx/ext-saladict/commit/bda8c07)), closes [#221](https://github.com/crimx/ext-saladict/issues/221)\n\n\n\n<a name=\"6.17.1\"></a>\n## [6.17.1](https://github.com/crimx/ext-saladict/compare/v6.17.0...v6.17.1) (2018-10-12)\n\n\n### Bug Fixes\n\n* **panel:** fix panel not showing if animation is off ([a533f59](https://github.com/crimx/ext-saladict/commit/a533f59))\n* **panel:** fix search text loading on standalone panel ([828e315](https://github.com/crimx/ext-saladict/commit/828e315))\n\n\n\n<a name=\"6.17.0\"></a>\n# [6.17.0](https://github.com/crimx/ext-saladict/compare/v6.16.1...v6.17.0) (2018-10-11)\n\n\n### Bug Fixes\n\n* **background:** fix typing ([8cc0760](https://github.com/crimx/ext-saladict/commit/8cc0760))\n* **dicts:** update google tk ([c36d15d](https://github.com/crimx/ext-saladict/commit/c36d15d)), closes [#212](https://github.com/crimx/ext-saladict/issues/212)\n* **locales:** typo ([4934e7c](https://github.com/crimx/ext-saladict/commit/4934e7c))\n* **panel:** only load on top frame ([c9a8bf7](https://github.com/crimx/ext-saladict/commit/c9a8bf7)), closes [#214](https://github.com/crimx/ext-saladict/issues/214)\n* **panel:** prevent context menu on right click ([dd2b5ce](https://github.com/crimx/ext-saladict/commit/dd2b5ce))\n* **selection:** input and textarea selection on Firefox ([dfa95f7](https://github.com/crimx/ext-saladict/commit/dfa95f7))\n\n\n### Features\n\n* **command:** add command for quick search panel ([dc34810](https://github.com/crimx/ext-saladict/commit/dc34810))\n* **menu:** add google cn page translate ([f549fdc](https://github.com/crimx/ext-saladict/commit/f549fdc))\n* **panel:** add quick search standalone panel ([2ff9fa2](https://github.com/crimx/ext-saladict/commit/2ff9fa2))\n\n\n\n<a name=\"6.16.1\"></a>\n## [6.16.1](https://github.com/crimx/ext-saladict/compare/v6.16.0...v6.16.1) (2018-10-02)\n\n\n### Bug Fixes\n\n* **options:** support firefox export ([322ca3c](https://github.com/crimx/ext-saladict/commit/322ca3c)), closes [#210](https://github.com/crimx/ext-saladict/issues/210)\n\n\n\n<a name=\"6.16.0\"></a>\n# [6.16.0](https://github.com/crimx/ext-saladict/compare/v6.15.4...v6.16.0) (2018-10-01)\n\n\n### Features\n\n* **dicts:** chs to chz ([c0cf11e](https://github.com/crimx/ext-saladict/commit/c0cf11e))\n\n\n\n<a name=\"6.15.4\"></a>\n## [6.15.4](https://github.com/crimx/ext-saladict/compare/v6.15.3...v6.15.4) (2018-09-29)\n\n\n### Bug Fixes\n\n* **panel:** config not updating on init ([361c8ca](https://github.com/crimx/ext-saladict/commit/361c8ca)), closes [#209](https://github.com/crimx/ext-saladict/issues/209)\n* **panel:** not selecting when panel is called by triple ctrl ([b34b84d](https://github.com/crimx/ext-saladict/commit/b34b84d)), closes [#193](https://github.com/crimx/ext-saladict/issues/193)\n\n\n\n<a name=\"6.15.3\"></a>\n## [6.15.3](https://github.com/crimx/ext-saladict/compare/v6.15.2...v6.15.3) (2018-09-23)\n\n\n### Bug Fixes\n\n* **content:** fix salad bowl tomato on Firefox ([552f1ab](https://github.com/crimx/ext-saladict/commit/552f1ab))\n\n\n\n<a name=\"6.15.2\"></a>\n## [6.15.2](https://github.com/crimx/ext-saladict/compare/v6.15.1...v6.15.2) (2018-09-23)\n\n\n### Bug Fixes\n\n* **panel:** dict info could be undefined ([388edc0](https://github.com/crimx/ext-saladict/commit/388edc0))\n\n\n\n<a name=\"6.15.1\"></a>\n## [6.15.1](https://github.com/crimx/ext-saladict/compare/v6.15.0...v6.15.1) (2018-09-23)\n\n\n### Bug Fixes\n\n* **panel:** fix init config state mismatch ([06d7d5a](https://github.com/crimx/ext-saladict/commit/06d7d5a))\n* **selection:** fix lang check for instant capture ([90aeb1b](https://github.com/crimx/ext-saladict/commit/90aeb1b))\n\n\n\n<a name=\"6.15.0\"></a>\n# [6.15.0](https://github.com/crimx/ext-saladict/compare/v6.14.0...v6.15.0) (2018-09-16)\n\n\n### Bug Fixes\n\n* **panel:** fix line breaking in English ([4b76760](https://github.com/crimx/ext-saladict/commit/4b76760))\n* **panel:** fix profile panel shows on hover ([4b96472](https://github.com/crimx/ext-saladict/commit/4b96472))\n* **popup:** fix body width ([5212b6a](https://github.com/crimx/ext-saladict/commit/5212b6a))\n\n\n### Features\n\n* **dicts:** add lang selection for machine translations ([739e5ea](https://github.com/crimx/ext-saladict/commit/739e5ea))\n* **panel:** enable searchText on dict result ([85ec153](https://github.com/crimx/ext-saladict/commit/85ec153))\n\n\n\n<a name=\"6.14.0\"></a>\n# [6.14.0](https://github.com/crimx/ext-saladict/compare/v6.13.4...v6.14.0) (2018-09-11)\n\n\n### Bug Fixes\n\n* fix typings ([5678833](https://github.com/crimx/ext-saladict/commit/5678833))\n\n\n### Features\n\n* **components:** add dict weblio [#156](https://github.com/crimx/ext-saladict/issues/156) ([86bd514](https://github.com/crimx/ext-saladict/commit/86bd514))\n\n\n\n<a name=\"6.13.4\"></a>\n## [6.13.4](https://github.com/crimx/ext-saladict/compare/v6.13.3...v6.13.4) (2018-09-06)\n\n\n### Bug Fixes\n\n* **panel:** fix focus losing on triple ctrl ([ff7a67b](https://github.com/crimx/ext-saladict/commit/ff7a67b)), closes [#193](https://github.com/crimx/ext-saladict/issues/193)\n\n\n\n<a name=\"6.13.3\"></a>\n## [6.13.3](https://github.com/crimx/ext-saladict/compare/v6.13.2...v6.13.3) (2018-09-04)\n\n\n### Bug Fixes\n\n* **config:** fix quota bytes limit ([cfb7268](https://github.com/crimx/ext-saladict/commit/cfb7268))\n* **panel:** fix mta box toggling on update ([9f9983f](https://github.com/crimx/ext-saladict/commit/9f9983f))\n\n\n\n<a name=\"6.13.2\"></a>\n## [6.13.2](https://github.com/crimx/ext-saladict/compare/v6.13.1...v6.13.2) (2018-09-03)\n\n\n### Bug Fixes\n\n* **panel:** fix search box update on selection ([7f3a270](https://github.com/crimx/ext-saladict/commit/7f3a270)), closes [#197](https://github.com/crimx/ext-saladict/issues/197)\n\n\n\n<a name=\"6.13.1\"></a>\n## [6.13.1](https://github.com/crimx/ext-saladict/compare/v6.13.0...v6.13.1) (2018-09-02)\n\n\n### Bug Fixes\n\n* **content:** fix popup init ([fc3cc85](https://github.com/crimx/ext-saladict/commit/fc3cc85))\n\n\n\n<a name=\"6.13.0\"></a>\n# [6.13.0](https://github.com/crimx/ext-saladict/compare/v6.12.1...v6.13.0) (2018-09-02)\n\n\n### Bug Fixes\n\n* **config:** add met merge config ([74ae476](https://github.com/crimx/ext-saladict/commit/74ae476))\n* **configs:** fix config not updating on init ([7d54aa0](https://github.com/crimx/ext-saladict/commit/7d54aa0))\n* **dicts:** fix etymonline ([c5aeca2](https://github.com/crimx/ext-saladict/commit/c5aeca2))\n* **helpers:** prevent profiles blow up ([a5b7d2f](https://github.com/crimx/ext-saladict/commit/a5b7d2f))\n* **panel:** fix mta search box search text ([244a45c](https://github.com/crimx/ext-saladict/commit/244a45c))\n* **panel:** fix typings ([f312ffe](https://github.com/crimx/ext-saladict/commit/f312ffe))\n* **panel:** safety check ([23e06db](https://github.com/crimx/ext-saladict/commit/23e06db))\n\n\n### Features\n\n* **options:** add options for toggling multiline search box ([df4e241](https://github.com/crimx/ext-saladict/commit/df4e241))\n* **panel:** add multiline search box ([7370fc5](https://github.com/crimx/ext-saladict/commit/7370fc5))\n\n\n\n<a name=\"6.12.1\"></a>\n## [6.12.1](https://github.com/crimx/ext-saladict/compare/v6.12.0...v6.12.1) (2018-09-01)\n\n\n### Bug Fixes\n\n* **config:** fix quota bytes per item exceeds ([2a64195](https://github.com/crimx/ext-saladict/commit/2a64195))\n\n\n### Features\n\n* **options:** add profile adding ([22fe8e7](https://github.com/crimx/ext-saladict/commit/22fe8e7))\n\n\n\n<a name=\"6.12.0\"></a>\n# [6.12.0](https://github.com/crimx/ext-saladict/compare/v6.11.0...v6.12.0) (2018-08-31)\n\n\n### Bug Fixes\n\n* **content:** correct config and selection listener order ([876b691](https://github.com/crimx/ext-saladict/commit/876b691))\n* **content:** fix blank after new config ([ddc4953](https://github.com/crimx/ext-saladict/commit/ddc4953))\n* **content:** hide config profile panel on options page ([a60ee25](https://github.com/crimx/ext-saladict/commit/a60ee25))\n* **dicts:** fix camberidge audio ([f4c48b2](https://github.com/crimx/ext-saladict/commit/f4c48b2)), closes [#192](https://github.com/crimx/ext-saladict/issues/192)\n* **helpers:** fix listener interface ([41133da](https://github.com/crimx/ext-saladict/commit/41133da))\n* **panel:** add search box delay ([645797c](https://github.com/crimx/ext-saladict/commit/645797c))\n* **panel:** fix search box selection delay ([a3ffe05](https://github.com/crimx/ext-saladict/commit/a3ffe05))\n* **selection:** fix context matching line ending ([da80def](https://github.com/crimx/ext-saladict/commit/da80def))\n\n\n### Features\n\n* **config:** support multi-configs ([5d7660b](https://github.com/crimx/ext-saladict/commit/5d7660b))\n* **content:** add never show button for word editor ([ef77e7c](https://github.com/crimx/ext-saladict/commit/ef77e7c))\n* **content:** add UI for switching profile ([3507b45](https://github.com/crimx/ext-saladict/commit/3507b45))\n* **helpers:** add config id list stream ([398e3fd](https://github.com/crimx/ext-saladict/commit/398e3fd))\n* **options:** add config profile settings ([3825d74](https://github.com/crimx/ext-saladict/commit/3825d74))\n* **options:** add profile operations ([c7a622c](https://github.com/crimx/ext-saladict/commit/c7a622c))\n\n\n\n<a name=\"6.11.0\"></a>\n# [6.11.0](https://github.com/crimx/ext-saladict/compare/v6.10.2...v6.11.0) (2018-08-28)\n\n\n### Bug Fixes\n\n* **content:** fix typo ([ff6140a](https://github.com/crimx/ext-saladict/commit/ff6140a))\n* **dicts:** fix typings ([2ea57d3](https://github.com/crimx/ext-saladict/commit/2ea57d3))\n\n\n### Features\n\n* **content:** auto-fill translation field ([efe95d2](https://github.com/crimx/ext-saladict/commit/efe95d2))\n* **dicts:** add sogou translation ([3e11231](https://github.com/crimx/ext-saladict/commit/3e11231))\n* **options:** add word of the day for options page ([251c119](https://github.com/crimx/ext-saladict/commit/251c119))\n\n\n\n<a name=\"6.10.2\"></a>\n## [6.10.2](https://github.com/crimx/ext-saladict/compare/v6.10.1...v6.10.2) (2018-08-27)\n\n\n### Bug Fixes\n\n* **panel:** better search box focus ([fc16541](https://github.com/crimx/ext-saladict/commit/fc16541)), closes [#182](https://github.com/crimx/ext-saladict/issues/182)\n* **panel:** fix typing ([4c706a7](https://github.com/crimx/ext-saladict/commit/4c706a7))\n* **panel:** improve smoothness when panel shows up ([a3ee454](https://github.com/crimx/ext-saladict/commit/a3ee454))\n\n\n\n<a name=\"6.10.1\"></a>\n## [6.10.1](https://github.com/crimx/ext-saladict/compare/v6.10.0...v6.10.1) (2018-08-27)\n\n\n### Bug Fixes\n\n* **dicts:** fix longman styles ([4dc74c4](https://github.com/crimx/ext-saladict/commit/4dc74c4))\n* **locales:** typo ([f86f77e](https://github.com/crimx/ext-saladict/commit/f86f77e))\n\n\n\n<a name=\"6.10.0\"></a>\n# [6.10.0](https://github.com/crimx/ext-saladict/compare/v6.9.0...v6.10.0) (2018-08-27)\n\n\n### Features\n\n* **background:** add pfd black and white list ([750a745](https://github.com/crimx/ext-saladict/commit/750a745))\n* **dicts:** add google tl option ([489bdd9](https://github.com/crimx/ext-saladict/commit/489bdd9))\n* **options:** dict options support select ([dcf6357](https://github.com/crimx/ext-saladict/commit/dcf6357))\n* **panel:** add wordEditor deleteCards ([47f8f1b](https://github.com/crimx/ext-saladict/commit/47f8f1b))\n* **panel:** improve fav word process ([b632a28](https://github.com/crimx/ext-saladict/commit/b632a28))\n* **wordpage:** add page export/delete ([6ceb21a](https://github.com/crimx/ext-saladict/commit/6ceb21a))\n* **wordpage:** add word count ([6667f53](https://github.com/crimx/ext-saladict/commit/6667f53))\n\n\n### Performance Improvements\n\n* **panel:** regression. remove search delay ([c65e53c](https://github.com/crimx/ext-saladict/commit/c65e53c))\n\n\n\n<a name=\"6.9.0\"></a>\n# [6.9.0](https://github.com/crimx/ext-saladict/compare/v6.8.3...v6.9.0) (2018-08-04)\n\n\n### Bug Fixes\n\n* **panel:** patch internal panel css ([befe9ce](https://github.com/crimx/ext-saladict/commit/befe9ce))\n\n\n### Features\n\n* **dicts:** bing sentence highlight ([ed1b7c4](https://github.com/crimx/ext-saladict/commit/ed1b7c4))\n\n\n\n<a name=\"6.8.3\"></a>\n## [6.8.3](https://github.com/crimx/ext-saladict/compare/v6.8.2...v6.8.3) (2018-07-27)\n\n\n### Bug Fixes\n\n* **dicts:** fix audio link ([8b7e140](https://github.com/crimx/ext-saladict/commit/8b7e140)), closes [#175](https://github.com/crimx/ext-saladict/issues/175)\n\n\n\n<a name=\"6.8.2\"></a>\n## [6.8.2](https://github.com/crimx/ext-saladict/compare/v6.8.1...v6.8.2) (2018-07-26)\n\n\n### Bug Fixes\n\n* **panel:** triple ctrl auto search ([00a1381](https://github.com/crimx/ext-saladict/commit/00a1381)), closes [#174](https://github.com/crimx/ext-saladict/issues/174)\n\n\n\n<a name=\"6.8.1\"></a>\n## [6.8.1](https://github.com/crimx/ext-saladict/compare/v6.8.0...v6.8.1) (2018-07-26)\n\n\n### Bug Fixes\n\n* **helpers:** remove chs chars from korean [#173](https://github.com/crimx/ext-saladict/issues/173) ([38f1dce](https://github.com/crimx/ext-saladict/commit/38f1dce))\n\n\n\n<a name=\"6.8.0\"></a>\n# [6.8.0](https://github.com/crimx/ext-saladict/compare/v6.7.0...v6.8.0) (2018-07-26)\n\n\n### Bug Fixes\n\n* **selection:** correct word count ([6a28e3d](https://github.com/crimx/ext-saladict/commit/6a28e3d)), closes [#172](https://github.com/crimx/ext-saladict/issues/172)\n\n\n### Features\n\n* **panel:** open links in new tabs ([d40cf2a](https://github.com/crimx/ext-saladict/commit/d40cf2a))\n\n\n\n<a name=\"6.7.0\"></a>\n# [6.7.0](https://github.com/crimx/ext-saladict/compare/v6.6.0...v6.7.0) (2018-07-26)\n\n\n### Bug Fixes\n\n* **dicts:** encode url ([0c0cd20](https://github.com/crimx/ext-saladict/commit/0c0cd20)), closes [#170](https://github.com/crimx/ext-saladict/issues/170)\n* **options:** auto-search on options page ([39ed461](https://github.com/crimx/ext-saladict/commit/39ed461))\n* **panel:** reset internal style ([3119084](https://github.com/crimx/ext-saladict/commit/3119084))\n* **selection:** better ctrl detection ([9f0c6b6](https://github.com/crimx/ext-saladict/commit/9f0c6b6)), closes [#168](https://github.com/crimx/ext-saladict/issues/168)\n* **selection:** better language detection ([c1f24e2](https://github.com/crimx/ext-saladict/commit/c1f24e2))\n\n\n### Features\n\n* **dicts:** add google tts ([5af7145](https://github.com/crimx/ext-saladict/commit/5af7145))\n* **options:** add minor language options ([f269d0f](https://github.com/crimx/ext-saladict/commit/f269d0f))\n* **selection:** minor lang selection ([8948f3c](https://github.com/crimx/ext-saladict/commit/8948f3c))\n\n\n\n<a name=\"6.6.0\"></a>\n# [6.6.0](https://github.com/crimx/ext-saladict/compare/v6.5.1...v6.6.0) (2018-07-19)\n\n\n### Bug Fixes\n\n* **content:** fix new selection interface ([2bb8ada](https://github.com/crimx/ext-saladict/commit/2bb8ada))\n* **selection:** ignore right click [#166](https://github.com/crimx/ext-saladict/issues/166) ([ee5b794](https://github.com/crimx/ext-saladict/commit/ee5b794))\n* **selection:** ignore triple ctrl when panel is visible [#162](https://github.com/crimx/ext-saladict/issues/162) ([e0e3208](https://github.com/crimx/ext-saladict/commit/e0e3208))\n\n\n### Features\n\n* **selection:** add selection inside dict panel [#165](https://github.com/crimx/ext-saladict/issues/165) ([f8da4be](https://github.com/crimx/ext-saladict/commit/f8da4be))\n* **selection:** support selection on internal pages ([226be86](https://github.com/crimx/ext-saladict/commit/226be86))\n\n\n### Performance Improvements\n\n* **dicts:** improve google translate stability ([edd5fa9](https://github.com/crimx/ext-saladict/commit/edd5fa9))\n\n\n\n<a name=\"6.5.1\"></a>\n# [6.5.1](https://github.com/crimx/ext-saladict/compare/v6.1.0...v6.5.1) (2018-07-10)\n\n\n### Bug Fixes\n\n* **panel:** support span links ([32b4a3a](https://github.com/crimx/ext-saladict/commit/32b4a3a))\n\n\n\n<a name=\"6.5.0\"></a>\n# [6.5.0](https://github.com/crimx/ext-saladict/compare/v6.4.1...v6.5.0) (2018-07-08)\n\n\n### Bug Fixes\n\n* **options:** missing files breaks CI build ([ae35a49](https://github.com/crimx/ext-saladict/commit/ae35a49))\n* update ci ([ba3108e](https://github.com/crimx/ext-saladict/commit/ba3108e))\n* update ci ([97e5201](https://github.com/crimx/ext-saladict/commit/97e5201))\n* update ci ([7dd28a8](https://github.com/crimx/ext-saladict/commit/7dd28a8))\n* **popup:** fix preloading selection on popup page ([5912183](https://github.com/crimx/ext-saladict/commit/5912183))\n\n\n### Features\n\n* **$browser:** longman dictionary's exmaples add speaker ([8601078](https://github.com/crimx/ext-saladict/commit/8601078))\n* **$browser:** longman max level is 3 ([c9a4a80](https://github.com/crimx/ext-saladict/commit/c9a4a80))\n* **context:** add context menus saladict search [#152](https://github.com/crimx/ext-saladict/issues/152) ([6125ca8](https://github.com/crimx/ext-saladict/commit/6125ca8))\n* **dicts:** add google dict ([355740c](https://github.com/crimx/ext-saladict/commit/355740c)), closes [#145](https://github.com/crimx/ext-saladict/issues/145)\n* **options:** add balck-white list [#155](https://github.com/crimx/ext-saladict/issues/155) ([a2c8d13](https://github.com/crimx/ext-saladict/commit/a2c8d13))\n* **selection:** support Monaco editor ([edd0012](https://github.com/crimx/ext-saladict/commit/edd0012))\n\n\n\n<a name=\"6.4.1\"></a>\n## [6.4.1](https://github.com/crimx/ext-saladict/compare/v6.4.0...v6.4.1) (2018-06-28)\n\n\n### Bug Fixes\n\n* **content:** fix dynamic document.body [#150](https://github.com/crimx/ext-saladict/issues/150) ([27f2787](https://github.com/crimx/ext-saladict/commit/27f2787))\n* **manifest:** fix browser global conflict [#148](https://github.com/crimx/ext-saladict/issues/148) ([ca0d8a1](https://github.com/crimx/ext-saladict/commit/ca0d8a1))\n\n\n\n<a name=\"6.4.0\"></a>\n# [6.4.0](https://github.com/crimx/ext-saladict/compare/v6.3.2...v6.4.0) (2018-06-17)\n\n\n### Bug Fixes\n\n* **background:** regression. mistakenly added new code ([f974c61](https://github.com/crimx/ext-saladict/commit/f974c61))\n* **content:** prevent selection detection on word editor ([8cc86a8](https://github.com/crimx/ext-saladict/commit/8cc86a8))\n* **content:** regression: use position ([b5d75d8](https://github.com/crimx/ext-saladict/commit/b5d75d8))\n* **panel:** fix Firefox popup page delay bug ([c5a4d6d](https://github.com/crimx/ext-saladict/commit/c5a4d6d))\n* **panel:** iframe occasionally flickering ([e89cd03](https://github.com/crimx/ext-saladict/commit/e89cd03))\n* **selection:** range could be null ([3cc2ec2](https://github.com/crimx/ext-saladict/commit/3cc2ec2))\n* **selection:** update context matching [#144](https://github.com/crimx/ext-saladict/issues/144) ([fa20ab7](https://github.com/crimx/ext-saladict/commit/fa20ab7))\n\n\n### Features\n\n* **background:** add page translations [#146](https://github.com/crimx/ext-saladict/issues/146) ([c5d6225](https://github.com/crimx/ext-saladict/commit/c5d6225))\n* **background:** add shortcut for instant capture ([bc46a2f](https://github.com/crimx/ext-saladict/commit/bc46a2f))\n* **content:** add query panel state ([c92a7d0](https://github.com/crimx/ext-saladict/commit/c92a7d0))\n* **content:** broadcast store state ([d0a356f](https://github.com/crimx/ext-saladict/commit/d0a356f))\n* **options:** add instant capture ([71955a4](https://github.com/crimx/ext-saladict/commit/71955a4))\n* **popup:** add instant capture toggle ([32dcfdc](https://github.com/crimx/ext-saladict/commit/32dcfdc))\n* **selection:** add cursor instant capture [#14](https://github.com/crimx/ext-saladict/issues/14) ([ef37346](https://github.com/crimx/ext-saladict/commit/ef37346))\n\n\n\n<a name=\"6.3.2\"></a>\n## [6.3.2](https://github.com/crimx/ext-saladict/compare/v6.3.1...v6.3.2) (2018-06-13)\n\n\n### Bug Fixes\n\n* **popup:** qrcode hiding ([eec0d02](https://github.com/crimx/ext-saladict/commit/eec0d02))\n\n\n\n<a name=\"6.3.1\"></a>\n## [6.3.1](https://github.com/crimx/ext-saladict/compare/v6.3.0...v6.3.1) (2018-06-13)\n\n\n### Bug Fixes\n\n* **config:** increase default word count ([a8d98c4](https://github.com/crimx/ext-saladict/commit/a8d98c4))\n* **config:** lang code auto update ([cffa171](https://github.com/crimx/ext-saladict/commit/cffa171))\n* **popup:** fix id ([830386a](https://github.com/crimx/ext-saladict/commit/830386a))\n\n\n\n<a name=\"6.3.0\"></a>\n# [6.3.0](https://github.com/crimx/ext-saladict/compare/v6.2.2...v6.3.0) (2018-06-12)\n\n\n### Bug Fixes\n\n* **panel:** reset dict height ([f359205](https://github.com/crimx/ext-saladict/commit/f359205))\n\n\n### Features\n\n* **background:** add shortcuts [#141](https://github.com/crimx/ext-saladict/issues/141) ([76a35a2](https://github.com/crimx/ext-saladict/commit/76a35a2))\n* **content:** add search history incognito mode ([8168c12](https://github.com/crimx/ext-saladict/commit/8168c12))\n* **popup:** add active toggle ([3f1d115](https://github.com/crimx/ext-saladict/commit/3f1d115)), closes [#140](https://github.com/crimx/ext-saladict/issues/140)\n\n\n### Performance Improvements\n\n* **panel:** better animation ([8777470](https://github.com/crimx/ext-saladict/commit/8777470))\n\n\n\n<a name=\"6.2.2\"></a>\n## [6.2.2](https://github.com/crimx/ext-saladict/compare/v6.2.1...v6.2.2) (2018-06-08)\n\n\n### Bug Fixes\n\n* **panel:** fix missing calculation on hiding [#135](https://github.com/crimx/ext-saladict/issues/135) ([bb5823e](https://github.com/crimx/ext-saladict/commit/bb5823e))\n\n\n\n<a name=\"6.2.1\"></a>\n## [6.2.1](https://github.com/crimx/ext-saladict/compare/v6.2.0...v6.2.1) (2018-06-08)\n\n\n### Features\n\n* **config:** add sogou [#134](https://github.com/crimx/ext-saladict/issues/134) ([8833a69](https://github.com/crimx/ext-saladict/commit/8833a69))\n\n\n\n<a name=\"6.2.0\"></a>\n# [6.2.0](https://github.com/crimx/ext-saladict/compare/v6.1.3...v6.2.0) (2018-06-07)\n\n\n### Bug Fixes\n\n* **content:** animation toggle on word editor ([a1c7efd](https://github.com/crimx/ext-saladict/commit/a1c7efd))\n* **dicts:** hide sharing ([f1bd672](https://github.com/crimx/ext-saladict/commit/f1bd672))\n\n\n### Features\n\n* **dicts:** add google options ([9b4790d](https://github.com/crimx/ext-saladict/commit/9b4790d))\n* **panel:** add double click search [#115](https://github.com/crimx/ext-saladict/issues/115) ([313ff16](https://github.com/crimx/ext-saladict/commit/313ff16))\n* **panel:** selection word count [#129](https://github.com/crimx/ext-saladict/issues/129) ([a4eb1a1](https://github.com/crimx/ext-saladict/commit/a4eb1a1))\n\n\n### Performance Improvements\n\n* **content:** increase responsiveness ([b95f2ae](https://github.com/crimx/ext-saladict/commit/b95f2ae))\n\n\n\n<a name=\"6.1.3\"></a>\n## [6.1.3](https://github.com/crimx/ext-saladict/compare/v6.1.2...v6.1.3) (2018-06-06)\n\n\n### Bug Fixes\n\n* **panel:** remove panel visibility delay [#132](https://github.com/crimx/ext-saladict/issues/132) ([305de64](https://github.com/crimx/ext-saladict/commit/305de64))\n\n\n\n<a name=\"6.1.2\"></a>\n## [6.1.2](https://github.com/crimx/ext-saladict/compare/v6.1.1...v6.1.2) (2018-06-06)\n\n\n### Bug Fixes\n\n* **panel:** fix null pointer ([dc3a41e](https://github.com/crimx/ext-saladict/commit/dc3a41e)), closes [#130](https://github.com/crimx/ext-saladict/issues/130)\n\n\n\n<a name=\"6.1.1\"></a>\n## [6.1.1](https://github.com/crimx/ext-saladict/compare/v6.1.0...v6.1.1) (2018-06-05)\n\n\n### Bug Fixes\n\n* **panel:** fix right click ([d06be31](https://github.com/crimx/ext-saladict/commit/d06be31))\n\n\n\n<a name=\"6.1.0\"></a>\n# [6.1.0](https://github.com/crimx/ext-saladict/compare/v6.0.0...v6.1.0) (2018-06-05)\n\n\n### Bug Fixes\n\n* **panel:** fix auto focus [#124](https://github.com/crimx/ext-saladict/issues/124) ([4d03bda](https://github.com/crimx/ext-saladict/commit/4d03bda))\n* **panel:** fix entering in options page and popup page ([298bbb1](https://github.com/crimx/ext-saladict/commit/298bbb1))\n* **panel:** fix height calc ([148cd56](https://github.com/crimx/ext-saladict/commit/148cd56))\n* **panel:** fix iframe flickering in Chrome [#124](https://github.com/crimx/ext-saladict/issues/124) [#113](https://github.com/crimx/ext-saladict/issues/113) [#119](https://github.com/crimx/ext-saladict/issues/119) ([922d8d4](https://github.com/crimx/ext-saladict/commit/922d8d4))\n* **panel:** popup preload [#124](https://github.com/crimx/ext-saladict/issues/124) ([69aa7a8](https://github.com/crimx/ext-saladict/commit/69aa7a8))\n* **panel:** remove animation ([55ef5fa](https://github.com/crimx/ext-saladict/commit/55ef5fa)), closes [#123](https://github.com/crimx/ext-saladict/issues/123)\n* **selection:** always update last selection text ([c6aca15](https://github.com/crimx/ext-saladict/commit/c6aca15))\n* **selection:** ctrl key detection [#124](https://github.com/crimx/ext-saladict/issues/124) [#122](https://github.com/crimx/ext-saladict/issues/122) [#114](https://github.com/crimx/ext-saladict/issues/114) ([a5893d2](https://github.com/crimx/ext-saladict/commit/a5893d2))\n\n\n### Features\n\n* **components:** explain export ([c6022c3](https://github.com/crimx/ext-saladict/commit/c6022c3))\n* **config:** add toggle for word editor [#118](https://github.com/crimx/ext-saladict/issues/118) ([eb95680](https://github.com/crimx/ext-saladict/commit/eb95680))\n* **content:** explain translations ([c6d9d50](https://github.com/crimx/ext-saladict/commit/c6d9d50)), closes [#117](https://github.com/crimx/ext-saladict/issues/117)\n\n\n\n<a name=\"6.0.0\"></a>\n# [6.0.0](https://github.com/crimx/ext-saladict/compare/v5.31.7...v6.0.0) (2018-05-30)\n\n\n### Bug Fixes\n\n* **assets:** assets to static ([8dc5092](https://github.com/crimx/ext-saladict/commit/8dc5092))\n* **background:** content script cannot catch rejections from bg ([29667a5](https://github.com/crimx/ext-saladict/commit/29667a5))\n* **background:** context menus i18n init event ([25dbeef](https://github.com/crimx/ext-saladict/commit/25dbeef))\n* **background:** fix typo ([6d7e25c](https://github.com/crimx/ext-saladict/commit/6d7e25c))\n* **browser:** fix diffrenent removeListener api ([40c4721](https://github.com/crimx/ext-saladict/commit/40c4721))\n* **browser:** fix webext polyfills ([ce11f10](https://github.com/crimx/ext-saladict/commit/ce11f10))\n* **build:** fix fake env ([a1bf3ae](https://github.com/crimx/ext-saladict/commit/a1bf3ae))\n* **components:** better keys for star rates ([6b50750](https://github.com/crimx/ext-saladict/commit/6b50750))\n* **components:** fix Speaker svg dimension ([7d48654](https://github.com/crimx/ext-saladict/commit/7d48654))\n* **components:** fix StarRates style ([1f65959](https://github.com/crimx/ext-saladict/commit/1f65959))\n* **components:** stop setState when unmounted ([0d86b56](https://github.com/crimx/ext-saladict/commit/0d86b56))\n* **config:** more test friendly ([a8e194f](https://github.com/crimx/ext-saladict/commit/a8e194f))\n* **config:** replace the empty sting ([56937a9](https://github.com/crimx/ext-saladict/commit/56937a9))\n* **config:** update config ([635608b](https://github.com/crimx/ext-saladict/commit/635608b))\n* **content:** close on save & filter self ([e92a2f6](https://github.com/crimx/ext-saladict/commit/e92a2f6))\n* **content:** delay injecting css ([a12e070](https://github.com/crimx/ext-saladict/commit/a12e070))\n* **content:** fix close panel when pinned ([4771637](https://github.com/crimx/ext-saladict/commit/4771637))\n* **content:** fix dict item height restore on search ([d5fbc78](https://github.com/crimx/ext-saladict/commit/d5fbc78))\n* **content:** fix firefox !important bug ([73c162a](https://github.com/crimx/ext-saladict/commit/73c162a))\n* **content:** fix firefox svg animation ([76f17cb](https://github.com/crimx/ext-saladict/commit/76f17cb))\n* **content:** fix iframe flickering in Chrome ([9bab2af](https://github.com/crimx/ext-saladict/commit/9bab2af))\n* **content:** fix init position since now use translate ([9ffb1ef](https://github.com/crimx/ext-saladict/commit/9ffb1ef))\n* **content:** fix inject css ([4a82878](https://github.com/crimx/ext-saladict/commit/4a82878))\n* **content:** fix long press ctrl ([e2613fc](https://github.com/crimx/ext-saladict/commit/e2613fc))\n* **content:** fix mask button disabled on fold ([35769e9](https://github.com/crimx/ext-saladict/commit/35769e9))\n* **content:** fix new config refresh bug ([3f44afc](https://github.com/crimx/ext-saladict/commit/3f44afc))\n* **content:** get the first config before listening ([2a6fdf6](https://github.com/crimx/ext-saladict/commit/2a6fdf6))\n* **database:** ignore case ([72f6abe](https://github.com/crimx/ext-saladict/commit/72f6abe))\n* **dicts:** add space after basic ([8bbfb08](https://github.com/crimx/ext-saladict/commit/8bbfb08))\n* **dicts:** bypass etymonline referer checking ([a105eec](https://github.com/crimx/ext-saladict/commit/a105eec))\n* **dicts:** change url ([4772fa9](https://github.com/crimx/ext-saladict/commit/4772fa9))\n* **dicts:** fix bing audio ([b0b641f](https://github.com/crimx/ext-saladict/commit/b0b641f))\n* **dicts:** fix bing audio language detection ([ad577c3](https://github.com/crimx/ext-saladict/commit/ad577c3))\n* **dicts:** fix bing phsym key ([d248a6d](https://github.com/crimx/ext-saladict/commit/d248a6d))\n* **dicts:** fix cambridge link ([68b0a4d](https://github.com/crimx/ext-saladict/commit/68b0a4d))\n* **dicts:** fix COBUILD page link ([7a6f45e](https://github.com/crimx/ext-saladict/commit/7a6f45e))\n* **dicts:** fix img style ([130158a](https://github.com/crimx/ext-saladict/commit/130158a))\n* **dicts:** fix lang code ([b6532cd](https://github.com/crimx/ext-saladict/commit/b6532cd))\n* **dicts:** fix locales ([c39151e](https://github.com/crimx/ext-saladict/commit/c39151e))\n* **dicts:** fix macmillan style ([64aee9b](https://github.com/crimx/ext-saladict/commit/64aee9b))\n* **dicts:** fix p margin ([1561888](https://github.com/crimx/ext-saladict/commit/1561888))\n* **dicts:** get correct href ([cd887e1](https://github.com/crimx/ext-saladict/commit/cd887e1))\n* **dicts:** remove logging ([b19dab8](https://github.com/crimx/ext-saladict/commit/b19dab8))\n* **dicts:** use innerHTML ([dee2afd](https://github.com/crimx/ext-saladict/commit/dee2afd))\n* **dicts:** use lower case ([f66e99a](https://github.com/crimx/ext-saladict/commit/f66e99a))\n* **helper:** rxjs6 fromEventPattern inconsistency ([7c1594d](https://github.com/crimx/ext-saladict/commit/7c1594d))\n* **helpers:** always merge config ([49e01ff](https://github.com/crimx/ext-saladict/commit/49e01ff))\n* **helpers:** fix wrong deletion ([a55c46d](https://github.com/crimx/ext-saladict/commit/a55c46d))\n* **helpers:** get first config ([d8bb3fa](https://github.com/crimx/ext-saladict/commit/d8bb3fa))\n* **helpers:** get initial config ([a221bc7](https://github.com/crimx/ext-saladict/commit/a221bc7))\n* **helpers:** handle annoying msg errors from webext polyfill ([272f1eb](https://github.com/crimx/ext-saladict/commit/272f1eb))\n* **helpers:** props added to window should be optional ([1f12b26](https://github.com/crimx/ext-saladict/commit/1f12b26))\n* **locales:** add from Saladict ([b3b215e](https://github.com/crimx/ext-saladict/commit/b3b215e))\n* **locales:** add missing locales ([fc500ab](https://github.com/crimx/ext-saladict/commit/fc500ab))\n* **locales:** fix browser ui locale naming ([e57e61c](https://github.com/crimx/ext-saladict/commit/e57e61c))\n* **locales:** fix wording ([2d7486e](https://github.com/crimx/ext-saladict/commit/2d7486e))\n* **locales:** use standard lang code ([7a901ec](https://github.com/crimx/ext-saladict/commit/7a901ec))\n* **manifest:** declare wordeditor as web accessible resources ([f9984f5](https://github.com/crimx/ext-saladict/commit/f9984f5))\n* **manifest:** fix manifest ([685ded1](https://github.com/crimx/ext-saladict/commit/685ded1))\n* **menus:** fix rxjs path ([07fc3a9](https://github.com/crimx/ext-saladict/commit/07fc3a9))\n* **options:** add  key for v-for ([becfe1d](https://github.com/crimx/ext-saladict/commit/becfe1d))\n* **options:** add max width ([45a1532](https://github.com/crimx/ext-saladict/commit/45a1532))\n* **options:** fix auto search ([cd87b86](https://github.com/crimx/ext-saladict/commit/cd87b86))\n* **options:** fix decimal bug ([835e1ca](https://github.com/crimx/ext-saladict/commit/835e1ca))\n* **options:** fix language code ([d86b5c0](https://github.com/crimx/ext-saladict/commit/d86b5c0))\n* **options:** fix modal scroll bar flickering ([9294de4](https://github.com/crimx/ext-saladict/commit/9294de4))\n* **options:** fix style ([2417ec4](https://github.com/crimx/ext-saladict/commit/2417ec4))\n* **options:** search text when options page is opened ([c359b70](https://github.com/crimx/ext-saladict/commit/c359b70))\n* **package:** update normalize.css to version 8.0.0 ([1dbc42f](https://github.com/crimx/ext-saladict/commit/1dbc42f))\n* **panel:** blur input on drag start ([1759053](https://github.com/crimx/ext-saladict/commit/1759053))\n* **panel:** calculate margin height ([33d1a0a](https://github.com/crimx/ext-saladict/commit/33d1a0a))\n* **panel:** can't unfold a dict when the panel fisrt popup ([41e9498](https://github.com/crimx/ext-saladict/commit/41e9498))\n* **panel:** close panel and word editer on esc ([d133d6e](https://github.com/crimx/ext-saladict/commit/d133d6e))\n* **panel:** debounce animation end ([0cd7e32](https://github.com/crimx/ext-saladict/commit/0cd7e32))\n* **panel:** fix data inconsistency ([1b956aa](https://github.com/crimx/ext-saladict/commit/1b956aa))\n* **panel:** fix firame flickering ([81aebc7](https://github.com/crimx/ext-saladict/commit/81aebc7))\n* **panel:** fix icon bleeds ([9ce942a](https://github.com/crimx/ext-saladict/commit/9ce942a))\n* **panel:** fix panel init height ([799cc4c](https://github.com/crimx/ext-saladict/commit/799cc4c))\n* **panel:** fix panel init on options page ([be4815d](https://github.com/crimx/ext-saladict/commit/be4815d))\n* **panel:** fix preload text ([79732b8](https://github.com/crimx/ext-saladict/commit/79732b8))\n* **panel:** fix search box should follow history ([5cca229](https://github.com/crimx/ext-saladict/commit/5cca229))\n* **panel:** fix styles ([557fdff](https://github.com/crimx/ext-saladict/commit/557fdff))\n* **panel:** height recalculation on show full ([6f0077e](https://github.com/crimx/ext-saladict/commit/6f0077e))\n* **panel:** hide dicts when the selection lang does not match ([b099b5f](https://github.com/crimx/ext-saladict/commit/b099b5f))\n* **panel:** keep dict height unchanged when there is nothing ([47ef11b](https://github.com/crimx/ext-saladict/commit/47ef11b))\n* **panel:** only set immediate to x and y when dragging ([ce0e252](https://github.com/crimx/ext-saladict/commit/ce0e252))\n* **panel:** panel should listen to config on options page ([c751d44](https://github.com/crimx/ext-saladict/commit/c751d44))\n* **panel:** recalc body height when expanding menus ([b6a5714](https://github.com/crimx/ext-saladict/commit/b6a5714))\n* **panel:** record search text on options page ([86eec63](https://github.com/crimx/ext-saladict/commit/86eec63))\n* **panel:** remove animation on popup page ([d298201](https://github.com/crimx/ext-saladict/commit/d298201))\n* **panel:** replace same selection ([5270b8e](https://github.com/crimx/ext-saladict/commit/5270b8e))\n* **panel:** stop following cursor when pinned ([c1d04c1](https://github.com/crimx/ext-saladict/commit/c1d04c1))\n* **panel:** stop searching when selection lang doesn't match ([e506edd](https://github.com/crimx/ext-saladict/commit/e506edd))\n* **panel:** try whatever I can to stop iframe flickering ([949d03e](https://github.com/crimx/ext-saladict/commit/949d03e))\n* **panel:** tweak styles ([14437ef](https://github.com/crimx/ext-saladict/commit/14437ef))\n* **popup:** add to notebook on popup page ([f27dc2f](https://github.com/crimx/ext-saladict/commit/f27dc2f))\n* **popup:** remove white spaces ([c1f22e2](https://github.com/crimx/ext-saladict/commit/c1f22e2))\n* **sass:** add global ([73aaffa](https://github.com/crimx/ext-saladict/commit/73aaffa))\n* **selection:** compress whitespaces in selection ([d43eaa1](https://github.com/crimx/ext-saladict/commit/d43eaa1))\n* **selection:** fix className breaking on svg elements ([f932de3](https://github.com/crimx/ext-saladict/commit/f932de3))\n* **selection:** fix editor detection ([5d5c11a](https://github.com/crimx/ext-saladict/commit/5d5c11a))\n* **selection:** fix ignoring same selection rule for double click ([a587c22](https://github.com/crimx/ext-saladict/commit/a587c22))\n* **selection:** fix undefined detection ([2ad9269](https://github.com/crimx/ext-saladict/commit/2ad9269))\n* **selection:** get target on mousedown ([248578d](https://github.com/crimx/ext-saladict/commit/248578d))\n* **selection:** track same selection ([bbdbcbf](https://github.com/crimx/ext-saladict/commit/bbdbcbf))\n* **static:** fix [#99](https://github.com/crimx/ext-saladict/issues/99) fanyi.youdao bypass http request ([fe84550](https://github.com/crimx/ext-saladict/commit/fe84550))\n* **static:** use browser instead of chrome ([3709e3f](https://github.com/crimx/ext-saladict/commit/3709e3f))\n* **types:** fix typings ([f242fd3](https://github.com/crimx/ext-saladict/commit/f242fd3))\n\n\n### Features\n\n* **background:** add auto pronounce ([20a8a33](https://github.com/crimx/ext-saladict/commit/20a8a33))\n* **background:** add search result typing ([76dc07b](https://github.com/crimx/ext-saladict/commit/76dc07b))\n* **background:** context menus with i18n ([9e66f55](https://github.com/crimx/ext-saladict/commit/9e66f55))\n* **background:** timeout searching ([d2bd4d4](https://github.com/crimx/ext-saladict/commit/d2bd4d4))\n* **build:** add devbuild flag ([a5f2af0](https://github.com/crimx/ext-saladict/commit/a5f2af0))\n* **component:** add PortalFrame ([cbe2ddd](https://github.com/crimx/ext-saladict/commit/cbe2ddd))\n* **components:** add word-phrase filter for WordPage ([29cf96a](https://github.com/crimx/ext-saladict/commit/29cf96a)), closes [#103](https://github.com/crimx/ext-saladict/issues/103)\n* **components:** add WordPage ([0727db1](https://github.com/crimx/ext-saladict/commit/0727db1))\n* **components:** add wordpage search text ([f4a61fd](https://github.com/crimx/ext-saladict/commit/f4a61fd))\n* **components:** change Speaker to render nothing when no src ([5a50165](https://github.com/crimx/ext-saladict/commit/5a50165))\n* **content:** add animation switch ([8c2939e](https://github.com/crimx/ext-saladict/commit/8c2939e))\n* **content:** add component DictItem ([c600671](https://github.com/crimx/ext-saladict/commit/c600671))\n* **content:** add component SaladBowl ([4200c19](https://github.com/crimx/ext-saladict/commit/4200c19))\n* **content:** add content script entry ([a07519d](https://github.com/crimx/ext-saladict/commit/a07519d))\n* **content:** add dict panel ([9a68f2c](https://github.com/crimx/ext-saladict/commit/9a68f2c))\n* **content:** add i18n for menu bar ([b1a8c9e](https://github.com/crimx/ext-saladict/commit/b1a8c9e))\n* **content:** add menu bar ([bf6b74f](https://github.com/crimx/ext-saladict/commit/bf6b74f))\n* **content:** add menu bar search history ([5f33191](https://github.com/crimx/ext-saladict/commit/5f33191))\n* **content:** add mouseevent on bowl ([44a37bd](https://github.com/crimx/ext-saladict/commit/44a37bd))\n* **content:** add notebook ui logic ([dfd18ac](https://github.com/crimx/ext-saladict/commit/dfd18ac))\n* **content:** add onhold ([6b0844e](https://github.com/crimx/ext-saladict/commit/6b0844e))\n* **content:** add panel closing ([8a20f74](https://github.com/crimx/ext-saladict/commit/8a20f74))\n* **content:** add panel dragging ([9682afc](https://github.com/crimx/ext-saladict/commit/9682afc))\n* **content:** add redux store ([956d2af](https://github.com/crimx/ext-saladict/commit/956d2af))\n* **content:** add related words ([25c6cf7](https://github.com/crimx/ext-saladict/commit/25c6cf7))\n* **content:** add relationships between bowl and panel ([bd24060](https://github.com/crimx/ext-saladict/commit/bd24060))\n* **content:** add saladbow redux container ([6699707](https://github.com/crimx/ext-saladict/commit/6699707))\n* **content:** add search box text update ([2dc6bbc](https://github.com/crimx/ext-saladict/commit/2dc6bbc))\n* **content:** add store dictionaries ([56ce6dc](https://github.com/crimx/ext-saladict/commit/56ce6dc))\n* **content:** add store search text ([ff5c751](https://github.com/crimx/ext-saladict/commit/ff5c751))\n* **content:** add triple ctrl ([69ae1c4](https://github.com/crimx/ext-saladict/commit/69ae1c4))\n* **content:** add word editor ([9415cf0](https://github.com/crimx/ext-saladict/commit/9415cf0))\n* **content:** connect word editor to main component ([38a566c](https://github.com/crimx/ext-saladict/commit/38a566c))\n* **content:** delay mouse on bowl ([0aa1b90](https://github.com/crimx/ext-saladict/commit/0aa1b90))\n* **content:** finish word editor feature ([c77978c](https://github.com/crimx/ext-saladict/commit/c77978c))\n* **content:** intergrate content script into options page ([5c946f1](https://github.com/crimx/ext-saladict/commit/5c946f1))\n* **content:** listen to edit word event ([9188f21](https://github.com/crimx/ext-saladict/commit/9188f21))\n* **content:** move search logic together ([f7a1294](https://github.com/crimx/ext-saladict/commit/f7a1294))\n* **content:** notify parents of hight changing ([1d90c45](https://github.com/crimx/ext-saladict/commit/1d90c45))\n* **content:** setup files ([90c41c3](https://github.com/crimx/ext-saladict/commit/90c41c3))\n* **content:** support important styling ([d150e45](https://github.com/crimx/ext-saladict/commit/d150e45))\n* **content:** supprot max height ([deecdcc](https://github.com/crimx/ext-saladict/commit/deecdcc))\n* **content:** sync panel height with dict item height ([a0d215a](https://github.com/crimx/ext-saladict/commit/a0d215a))\n* **context-menus:** add youglish ([cd39f22](https://github.com/crimx/ext-saladict/commit/cd39f22))\n* **dicts:** add %h hyphen joining ([311ae79](https://github.com/crimx/ext-saladict/commit/311ae79))\n* **dicts:** add cambridge ([be329bd](https://github.com/crimx/ext-saladict/commit/be329bd))\n* **dicts:** add helper ([09f929f](https://github.com/crimx/ext-saladict/commit/09f929f))\n* **dicts:** add helpers ([999362c](https://github.com/crimx/ext-saladict/commit/999362c))\n* **dicts:** add helpers ([428cd08](https://github.com/crimx/ext-saladict/commit/428cd08))\n* **dicts:** add Longman ([90688b6](https://github.com/crimx/ext-saladict/commit/90688b6))\n* **dicts:** add oald ([65b7327](https://github.com/crimx/ext-saladict/commit/65b7327))\n* **dicts:** add webster learners dict ([6e0f1ee](https://github.com/crimx/ext-saladict/commit/6e0f1ee))\n* **dicts:** add youdao ([57ec2b3](https://github.com/crimx/ext-saladict/commit/57ec2b3))\n* **dicts:** fix longman style ([95f172e](https://github.com/crimx/ext-saladict/commit/95f172e))\n* **dicts:** more robust google engine ([701d1d4](https://github.com/crimx/ext-saladict/commit/701d1d4))\n* **helpers:** let openURL support ext based url ([e89eb54](https://github.com/crimx/ext-saladict/commit/e89eb54))\n* **history:** add history entry ([750a51a](https://github.com/crimx/ext-saladict/commit/750a51a))\n* **i18n:** loader accepts callback ([c9077b8](https://github.com/crimx/ext-saladict/commit/c9077b8))\n* **locale:** add back and next ([97d7094](https://github.com/crimx/ext-saladict/commit/97d7094))\n* **locales:** add locales ([10e9a2e](https://github.com/crimx/ext-saladict/commit/10e9a2e))\n* **locales:** update options locales ([5fbc0d1](https://github.com/crimx/ext-saladict/commit/5fbc0d1))\n* **manifest:** support dynamically generated iframes ([94ff5d4](https://github.com/crimx/ext-saladict/commit/94ff5d4)), closes [#106](https://github.com/crimx/ext-saladict/issues/106)\n* **notebook:** add notebook entry ([b24c7db](https://github.com/crimx/ext-saladict/commit/b24c7db))\n* **options:** add Acknowledgement ([578891a](https://github.com/crimx/ext-saladict/commit/578891a))\n* **options:** add animation option ([4da7b31](https://github.com/crimx/ext-saladict/commit/4da7b31))\n* **options:** add config import and export ([21d32d1](https://github.com/crimx/ext-saladict/commit/21d32d1))\n* **options:** add displaying supported languages ([6bb345e](https://github.com/crimx/ext-saladict/commit/6bb345e))\n* **options:** smart searching ([451860f](https://github.com/crimx/ext-saladict/commit/451860f))\n* **options:** update options ([817a314](https://github.com/crimx/ext-saladict/commit/817a314))\n* **options:** update options to the latest config ([6901f84](https://github.com/crimx/ext-saladict/commit/6901f84))\n* **panel:** add error boundary for dict ([56e957d](https://github.com/crimx/ext-saladict/commit/56e957d))\n* **panel:** add touch support ([2e856b4](https://github.com/crimx/ext-saladict/commit/2e856b4))\n* **panel:** disable buttons in popup page ([2b80ea4](https://github.com/crimx/ext-saladict/commit/2b80ea4))\n* **panel:** get all dict styles ([a80d0ac](https://github.com/crimx/ext-saladict/commit/a80d0ac))\n* **panel:** integrate panel with popup page ([fddbc38](https://github.com/crimx/ext-saladict/commit/fddbc38))\n* **panel:** open exteranl link ([9aaf70d](https://github.com/crimx/ext-saladict/commit/9aaf70d))\n* **panel:** open url base on lang code ([efb5187](https://github.com/crimx/ext-saladict/commit/efb5187))\n* **panel:** sticky header! pure css! ([e9490ac](https://github.com/crimx/ext-saladict/commit/e9490ac))\n* **popup:** add temporary disabling dict panel ([7710e3c](https://github.com/crimx/ext-saladict/commit/7710e3c))\n* **scss:** add scss globals ([af6e6df](https://github.com/crimx/ext-saladict/commit/af6e6df))\n* **selection:** add double click detection ([1299bec](https://github.com/crimx/ext-saladict/commit/1299bec))\n* **selection:** add get empty selection info ([9d01d9c](https://github.com/crimx/ext-saladict/commit/9d01d9c))\n* **selection:** add noTypeField ([f395f8c](https://github.com/crimx/ext-saladict/commit/f395f8c))\n* **selection:** detect esc key in all frames ([1e6ecc5](https://github.com/crimx/ext-saladict/commit/1e6ecc5))\n* **test:** add snapshot testing ([0be395c](https://github.com/crimx/ext-saladict/commit/0be395c))\n* **wordbook:** add database for words ([46c4327](https://github.com/crimx/ext-saladict/commit/46c4327))\n\n\n### Performance Improvements\n\n* **config:** refactor to get ride of cloneDeep ([d986b53](https://github.com/crimx/ext-saladict/commit/d986b53))\n* **helpers:** use DOMParser which is 6 time faster ([157a929](https://github.com/crimx/ext-saladict/commit/157a929))\n* **message:** change message type to typescript enum ([7682a1d](https://github.com/crimx/ext-saladict/commit/7682a1d))\n\n\n\n# Changelog\n\n[Unreleased]\n\n[5.31.7] - 2017-12-18\n### Added\n- 使用 webRequest 拦截 PDF 请求\n\n### Changed\n- 钉住时快速查询不移动窗口\n- 设置页面增加反馈链接\n\n### Fixed\n- 第二次打开浏览器右键菜单不显示\n- 必应词典相关单词可点击\n\n[5.30.0] - 2017-12-08\n### Changed\n- 可同时选择多个划词模式\n- 工具栏“选项”按钮改为词典目录\n\n### Fixed\n- 修复词典标题点击跳转\n\n[5.29.3] - 2017-11-29\n### Added\n- 单词记录同时保存上下文和来源\n- 可编辑单词记录，可添加翻译和注释笔记\n- 可自定义导出模板\n\n### Changed\n- 使用无限容量权限\n\n### Fixed\n- 编辑完后卡片响应\n- 查词框输入后马上点添加生词出现不匹配\n\n[5.28.1] - 2017-11-26\n### Added\n- 增加生词本\n\n### Changed\n- 可配置预加载内容（剪贴板或页面选中词）与自动开始查词，快捷查词可设置出现的位置\n\n### Fixed\n- 重构代码，减少耦合\n\n[5.27.3] - 2017-11-23\n### Added\n- 增加有道分级网页翻译2.0（支持 HTTPS）\n- 增加自动发音\n- 增加查词历史记录\n- 面板钉住时支持多种查词模式\n- 必应词典无结果时增加相关词语\n- 词带内部双击查词，点击单词链接也能直接查词\n- 对抓取页面筛选节点以增强安全性\n- 自身页面通信增加 page id 以解决冲突问题\n\n### Changed\n- 查看页面二维码移到地址栏旁的图标中\n- Chrome 最低版本支持提升为 55 以提升性能与减少大小\n- 重构代码以分散复杂度\n- 二维码生成改用 vue-qriously 更轻盈\n\n### Fixed\n- 修复打开 PDF 时弹出框查词自动粘贴失效\n- 修复 howjsay 相关词语获取\n- 修复查词滚动错误\n\n[5.19.1] - 2017-11-15\n### Added\n- 可配置双击时长\n\n### Fixed\n- 默认不显示词典以避免闪现\n\n[5.18.5] - 2017-11-13\n### Added\n- 增加汉典\n- 可配置词典只在某种语言下显示\n\n### Fixed\n- 修复繁体词典不能查简体字问题\n- 修复默认收起的词典不能隐藏\n- 更新 vuedraggable 修复拖动问题\n- 延迟音频播放避免误触\n- 每次查词滚动到顶端\n\n[5.16.1] - 2017-10-28\n### Added\n- 添加 PDF 支持\n\n### Fixed\n- 修复通知框点击\n\n[5.15.21] - 2017-10-26\n### Changed\n- 全不选时右键菜单隐藏\n\n### Fixed\n- 更新时才弹出通知\n- 重构 event page，顶层只保留监听，加快加载速度\n- 去掉 require.context，webpack 会自动生成路径\n\n[5.15.19] - 2017-10-11\n### Changed\n- 重构事件监听\n- 重构 chrome api wrap\n\n### Fixed\n- 点击发音\n- 自动恢复 dom 挂载\n- 更新 etymonline 词典\n\n[5.15.14] - 2017-09-05\n### Changed\n- 弹出查词框时自动选中所有剪贴板内容\n- 查词结构导出图片样式调整\n\n[5.15.12] - 2017-09-02\n### Fixed\n- 修复 ctrl/⌘ 模式时切换窗口的问题\n- 麦克米伦标题修复\n\n### Changed\n- 关闭自动查词\n\n[5.15.9] - 2017-08-23\n### Fixed\n- 更新必应词典\n- 修复拖动抖动问题\n- 样式修补\n\n### Changed\n- 第一次安装时打开设置页面\n\n[5.15.4] - 2017-08-10\n### Fixed\n- 词典样式\n- 麦克米伦检测问题\n\n[5.15.2] - 2017-08-06\n### Added\n- Macmillan 词典\n- 海词词频分级\n- 彩蛋\n\n### Fixed\n- 拖动问题\n- 其它小修正\n\n[5.12.8] - 2017-07-31\n### Added\n-  增加 Longman Business 词典\n\n### Changed\n- 只对剪贴板单个单词自动查词，多个单词会自动粘贴，但不开始查找，需要再按一下回车\n- 使用懒加载性能大幅度优化，提取公共模块体积减少\n- 更紧凑的架构设计，添加词典更简单\n\n### Fixed\n- 修复 Bing 发音问题\n\n## [5.11.23] - 2017-07-13\n### Added\n- 增加两岸词典与国语辞典\n- 增加点击图标弹出查词面板\n- 查词结果可以导出图片，在绿色工具栏上可以看到\n\n### Changed\n- 二维码功能移到工具栏上\n\n### Fixed\n- i18n 带 fallback\n- svg 属性迁就 html2canvas\n- 设置页面开始连查两遍的问题\n- 通过 `:root:root:root:root:root` 进一步增加元素权值\n- 改为插到 body 末尾\n\n## [5.7.20] - 2017-05-21\n### Added\n- 添加词源词典\n- 右键添加有道词典、海词词典和金山词霸\n\n### Security\n- 增强稳定性\n\n## [5.5.14] - 2017-05-15\n### Changed\n- 词典可默认不展开\n\n## [5.5.12] - 2017-05-15\n### Added\n- 增加右键谷歌网页翻译\n- 增加双语例句\n\n## [5.3.9] - 2017-05-03\n### Added\n- 添加重置按钮\n- 增加 Howjsay 发音\n\n### Fixed\n- 降低查词图标敏感度\n\n## [5.1.6] - 2017-04-06\n### Added\n- 增加双击查词\n\n### Fixed\n- 减少动画加快显示\n- 修复无法关闭\n- 修复设置时高度不更新\n\n## [5.0.0] - 2017-04-04\n### Changed\n- 全新重写，全面优化，性能大幅度提高。\n- 词典可以增删排序。\n- 新增多个词典。\n- 右键支持更多词典搜索。\n- 保留了置顶与拖动功能。\n- 更好用的配置界面。\n- 更多变化使用中发现吧。\n\n## [4.1.1] - 2015-12-27\n### Changed\n- 在必应词典和 Urban Dictionary 基础上增加 Vocabulary.com 海词统计和 Howjsay ，释义发音更详细。\n- 右键查词，选词后右键可直达牛津词典、韦氏词典、词源、谷歌翻译等等。\n- 新增三种划词模式，适合各种强迫症。\n- 连续按三次ctrl还可以直接查词，随时查词，无需再另开词典占内存啦。\n- 词典界面可以拖动，还可以固定在网页上，看论文利器啊。\n- 延迟响应时间，不容易误按，手残党福利。\n- 保留了显示当前页面二维码功能（设置界面，鼠标悬停在 “Saladict”标题上）。\n- 更多功能慢慢发现吧;D\n\n## 3.0.1\n### Changed\n- 增加了划译开关\n- 增加了 urban 词典的例子\n- 增加了必应搜索图标\n- 搜索图标右击可以变成翻译搜索\n- 修复了几处错误并加速了结果显示\n\n[Unreleased]: https://github.com/crimx/ext-saladict/compare/v5.31.7...HEAD\n[5.31.7]: https://github.com/crimx/ext-saladict/compare/v5.30.0...v5.31.7\n[5.30.0]: https://github.com/crimx/ext-saladict/compare/v5.28.3...v5.30.0\n[5.29.3]: https://github.com/crimx/ext-saladict/compare/v5.28.1...v5.29.3\n[5.28.1]: https://github.com/crimx/ext-saladict/compare/v5.27.3...v5.28.1\n[5.27.3]: https://github.com/crimx/ext-saladict/compare/v5.19.1...v5.27.3\n[5.19.1]: https://github.com/crimx/ext-saladict/compare/v5.18.5...v5.19.1\n[5.18.5]: https://github.com/crimx/ext-saladict/compare/v5.16.1...v5.18.5\n[5.16.1]: https://github.com/crimx/ext-saladict/compare/v5.15.21...v5.16.1\n[5.15.21]: https://github.com/crimx/ext-saladict/compare/v5.15.19...v5.15.21\n[5.15.19]: https://github.com/crimx/ext-saladict/compare/v5.15.14...v5.15.19\n[5.15.14]: https://github.com/crimx/ext-saladict/compare/v5.15.12...v5.15.14\n[5.15.12]: https://github.com/crimx/ext-saladict/compare/v5.15.9...v5.15.12\n[5.15.9]: https://github.com/crimx/ext-saladict/compare/v5.15.4...v5.15.9\n[5.15.4]: https://github.com/crimx/ext-saladict/compare/v5.15.2...v5.15.4\n[5.15.2]: https://github.com/crimx/ext-saladict/compare/v5.12.8...v5.15.2\n[5.12.8]: https://github.com/crimx/ext-saladict/compare/v5.11.23...v5.12.8\n[5.11.23]: https://github.com/crimx/ext-saladict/compare/v5.7.20...v5.11.23\n[5.7.20]: https://github.com/crimx/ext-saladict/compare/v5.5.14...v5.7.20\n[5.5.14]: https://github.com/crimx/ext-saladict/compare/v5.5.12...v5.5.14\n[5.5.12]: https://github.com/crimx/ext-saladict/compare/v5.3.9...v5.5.12\n[5.3.9]: https://github.com/crimx/ext-saladict/compare/v5.1.6...v5.3.9\n[5.1.6]: https://github.com/crimx/ext-saladict/compare/v5.0.0...v5.1.6\n[5.0.0]: https://github.com/crimx/ext-saladict/compare/v4.1.1...v5.0.0\n[4.1.1]: https://github.com/crimx/ext-saladict/tree/v4.1.1\n"
  },
  {
    "path": "CONTRIBUTING-zh.md",
    "content": "# 沙拉查词贡献指南\n\n:+1::tada: 首先，感谢你愿意抽时间为这个项目作贡献！ :tada::+1:\n\n## 贡献前注意\n\n:warning: 除非是小的修复，在动手前建议新开一个 WIP（施工中）issue 或 PR 阐述你要做的东西以及将要如何实现，以保证大家达成一致认识，而不白白浪费互相的时间与精力。\n\n- 先阅读 [如何开始](#如何开始).\n- 遵循[代码格式](#代码格式)以及 [commit 格式](#commit格式).\n- 提交前先本地跑[测试](#测试)以及[构建](#构建)。也可以交给 CI 处理。\n\n## 如何开始\n\n```bash\ngit clone git@github.com:crimx/ext-saladict.git\ncd ext-saladict\nyarn install\nyarn pdf\n```\n\n在项目根添加 `.env` 文件，参考 `.env.example` 格式（可留空如果你不需要这些词典）。\n\n## 修改 UI\n\n运行 `yarn fixtures` 下载测试文件（下载完成以后不必再运行）。\n\n运行 `yarn storybook` 查看所有 UI 组件。\n\n运行 `yarn start --wextentry [entry id]` 查看特定入口。项目会运行在 Webpack 开发服务器下的虚拟扩展环境中。\n\n## 测试\n\n运行 `yarn test`。支持所有 Jest [参数](https://jestjs.io/docs/en/cli)。\n\n## 构建\n\n运行 `yarn build`。\n\n参数:\n\n- `--debug`: 取消压缩代码并输出源码映射（map）文件。\n- `--analyze`: 显示打包分析图。\n\n## 如何添加词典\n\n出于安全性和可维护性，沙拉查词不提供热添加词典的功能，所有的词典添加必须向本项目提交 PR 合并。如果词典使用了未公开接口请另起项目发布到 NPM 再引用进来。\n\n1. 在 [`src/components/dictionaries/`](./src/components/dictionaries/) 下以词典 id 新建一个目录。\n   1. 可参考已有的词典如[必应](./src/components/dictionaries/bing)，复制文件到新建的目录中。\n   1. 把图标换成该词典的 LOGO。\n   1. 编辑 `config.ts` 修改词典默认设置。参见 `DictItem` 类型查看选项含义。在 [app config](./src/app-config/dicts.ts) 中注册词典让 TypeScript 生成正确的类型。词典 **必须** 遵循字母表顺序。\n   1. 更新 `_locales.json` 添加多语言的词典名字。如果词典有自定义选项请一并添加多语言的名字。\n   1. `engine.ts` **必须** `export` 至少两个函数：\n      1. `getSrcPage` 函数。当用户点击词典标题时计算出相应的链接。\n      1. `search` 函数。负责获取、解析和返回词典结果，可参考类型了解细节。\n         - 从网页中解析信息 **必须** 使用 [../helpers.ts](./components/dictionaries/helpers.ts) 中的辅助方法以保证数据干净。\n         - 如果词典支持自动发音：\n           1. 在 [`config.autopron`](https://github.com/crimx/ext-saladict/blob/a88cfed84129418b65914351ca14b86d7b1b758b/src/app-config/index.ts#L202-L223) 中注册 id。\n           2. 在返回的结果中附带 [`audio`](https://github.com/crimx/ext-saladict/blob/a88cfed84129418b65914351ca14b86d7b1b758b/src/typings/server.ts#L5-L9) 域。\n      1. 其它 `export` 的方法可以在 `View.tsx` 中通过 `'DICT_ENGINE_METHOD'` 通信通道调用。类型细节见 `src/typings/message`。也可以在项目中搜索 `DICT_ENGINE_METHOD` 查看例子。通信 **必须** 通过 `'@/_helpers/browser-api'` 的 `message` 而不是原生的 `sendMessage` 方法.\n   1. 词典结果最终会传到 `View.tsx` 中的 React 组件中。该组件 **应该** 只负责渲染结果而不带复杂逻辑。\n   1. `_style.scss` 中的选择器 **应该** 遵循类似 [ECSS](http://ecss.io/chapter5.html#anatomy-of-the-ecss-naming-convention) 的命名方式。\n\n### 热更新开发词典 UI 组件\n\n为了方便在 Storybook 中开发组件我们需要拦截词典引擎的网络请求返回本地文件。\n\n1. 新建 `fixtures.js` 在 `test/specs/components/dictionaries/[dictID]` 下。\n   - 格式可参考其它词典。\n   - 每个请求可以提供页面链接或者 axios 设置（见 `mojidict` 词典）。如果后面的请求依赖前面请求的结果，可以通过参数获得。\n1. 运行 `yarn fixtures` 下载结果。\n1. 编辑 `test/specs/components/dictionaries/[dictID]/request.mock.ts`。它会在开发时拦截词典请求并返回下载好的结果。\n1. 运行 `yarn storybook`。\n\n### 添加测试\n\n1. 添加 `[dictID]/engine.spec.ts` 测试引擎。\n\n## 代码格式\n\n本项目遵循 [Standard](https://standardjs.com) 的 TypeScript 变种格式。运行 `yarn lint` 可检查。\n\n如果使用 VSCode 等 IDE 请确保 *eslint* 和 *prettier* 插件已安装。或者构建的时候也会进行 TypeScript 完整检查。\n\n## Commit 格式\n\n本项目遵循 [conventional](https://conventionalcommits.org/) commit 格式。\n\n你可以在 commit 时运行 `yarn commit` 按指示选择。或者在 VSCode 中使用 [VSCode Conventional Commits](https://github.com/vivaxy/vscode-conventional-commits) 插件。\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Saladict\n\n:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:\n\n## How to Contribute\n\n:warning: Unless it is a small hot fix, before you write any code and get your hands dirty, please open an issue or make a WIP pull request to elaborate what you are trying to do and how you are going to implement it. Just to make sure we are on the same page and nobody's time and effort are wasted.\n\n- Read [How to get started](#how-to-get-started).\n- Follow [code style](#code-style) and [commit style](#commit-style).\n- Before submit, run [test](#testing) and [build](#building) locally. Or leave it to CI.\n\n## How to get started\n\n```bash\ngit clone git@github.com:crimx/ext-saladict.git\ncd ext-saladict\nyarn install\nyarn pdf\n```\n\nAdd a `.env` file following the `.env.example` format(leave empty if you don't use these dictionaries).\n\n## UI Tweaking\n\nRun `yarn fixtures` to download fixtures(only need to run once).\n\nRun `yarn storybook` to view all the components.\n\nRun `yarn start --wextentry [entry id]` to view a certain entry with WDS in a fake WebExtension environment.\n\n## Testing\n\nRun `yarn test` to run Jest. Supports all the Jest [options](https://jestjs.io/docs/en/cli).\n\n## Building\n\nRun `yarn build` to start a full build.\n\nToggle:\n\n- `--debug`: Remove compression and generate sourcemaps.\n- `--analyze`: Show detailed Webpack bundle analyzer.\n\n## How to add a dictionary\n\nFor safety and maintainability reason, Saladict will not support adding dictionaries on the fly. All dictionaries must be merged to this project via pull requests.\n\nIf dictionary implementation makes use of private API please move it to an independent project, release on NPM, then import it to Saladict.\n\n1. Create a directory at [`src/components/dictionaries/`](./src/components/dictionaries/), with the name of the dict ID.\n   1. Use any existing dictionary as guidance, e.g. [Bing](./src/components/dictionaries/bing). Copy files to the new directory.\n   1. Replace the favicon with a new LOGO.\n   1. Edit `config.ts` to change default options. See the `DictItem` type and explanation for more details. Register the dictionary in [app config](./src/app-config/dicts.ts) so that TypeScript generates the correct typings. Dict ID **MUST** follow alphabetical order.\n   1. Update `_locales.json` with the new dictionary name. Add locales for options, if any.\n   1. `engine.ts` **MUST** export at least two functions:\n      1. `getSrcPage` function which is responsible for generating source page url base on search text and app config. Source page url is opened when user clicks the dictionary title.\n      1. `search` function which is responsible for fetching, parsing and returning dictionary results. See the typings for more detail.\n         - Extracting information from a webpage **MUST** use helper functions in [../helpers.ts](./components/dictionaries/helpers.ts) for data cleansing.\n         - If the dictionary supports pronunciation:\n           1. Register the ID at [`config.autopron`](https://github.com/crimx/ext-saladict/blob/a88cfed84129418b65914351ca14b86d7b1b758b/src/app-config/index.ts#L202-L223).\n           1. Include an [`audio`](https://github.com/crimx/ext-saladict/blob/a88cfed84129418b65914351ca14b86d7b1b758b/src/typings/server.ts#L5-L9) field in the object which search engine returns.\n      1. Other exported functions can be called from `View.tsx` via `'DICT_ENGINE_METHOD'` message channel. See `src/typings/message` for typing details and search `DICT_ENGINE_METHOD` project-wise for examples. Messages **MUST** be sent via `message` from `'@/_helpers/browser-api'` instead of the native `sendMessage` function.\n   1. Search result will ultimately be passed to a React PureComponent in `View.tsx`, which **SHOULD** be a dumb component that renders the result accordingly.\n   1. Selectors in `_style.scss` **SHOULD** follow [ECSS](http://ecss.io/chapter5.html#anatomy-of-the-ecss-naming-convention)-ish naming convention.\n\n### Develop the dictionary UI live\n\nTo develop the component in Storybook we need to intercept http requests from dictionary engines and replace with the downloaded results.\n\n1. Add `fixtures.js` at `test/specs/components/dictionaries/[dictID]`.\n   - See other dictionaries for example.\n   - You can offer url or axios config (See `mojidict` dictionary). All results from previous requests will be passed to the next request as array.\n1. Run `yarn fixtures` to download fixtures.\n1. Edit `test/specs/components/dictionaries/[dictID]/request.mock.ts`. It will intercept requests and return the downloaded fixtures.\n1. Run `yarn storybook`.\n\n### Add Testing\n\n1. Add `[dictID]/engine.spec.ts` to test the engine.\n\n## Code Style\n\nThis project follows the TypeScript variation of [Standard](https://standardjs.com) JavaScript code style.\n\nIf you are using IDEs like VSCode, make sure *eslint* and *prettier* plugins are installed. Or you can just run [building command](#building) to perform a TypeScript full check.\n\n## Commit Style\n\nThis project follows [conventional](https://conventionalcommits.org/) commit style.\n\nYou can run `yarn commit` and follow the instructions, or use [VSCode Conventional Commits](https://github.com/vivaxy/vscode-conventional-commits) extension in VSCode.\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 CRIMX\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.\n\n"
  },
  {
    "path": "README-zh.md",
    "content": "# 沙拉查词 Saladict\n\n[![Version](https://img.shields.io/github/release/crimx/ext-saladict.svg?label=version)](https://github.com/crimx/ext-saladict/releases)\n[![Chrome Web Store](https://badgen.net/chrome-web-store/users/cdonnmffkdaoajfknoeeecmchibpmkmg?icon=chrome&color=0f9d58)](https://chrome.google.com/webstore/detail/cdonnmffkdaoajfknoeeecmchibpmkmg?hl=en)\n[![Chrome Web Store](https://badgen.net/chrome-web-store/stars/cdonnmffkdaoajfknoeeecmchibpmkmg?icon=chrome&color=0f9d58)](https://chrome.google.com/webstore/detail/cdonnmffkdaoajfknoeeecmchibpmkmg?hl=en)\n[![Mozilla Add-on](https://badgen.net/amo/users/ext-saladict?icon=firefox&color=ff9500)](https://addons.mozilla.org/firefox/addon/ext-saladict/)\n[![Mozilla Add-on](https://badgen.net/amo/stars/ext-saladict?icon=firefox&color=ff9500)](https://addons.mozilla.org/firefox/addon/ext-saladict/)\n\n[![Build Status](https://travis-ci.com/crimx/ext-saladict.svg)](https://travis-ci.com/crimx/ext-saladict)\n[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg?maxAge=2592000)](http://commitizen.github.io/cz-cli/)\n[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-brightgreen.svg?maxAge=2592000)](https://conventionalcommits.org)\n[![Standard - JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg?maxAge=2592000)](https://standardjs.com/)\n[![License](https://img.shields.io/github/license/crimx/ext-saladict.svg?colorB=44cc11?maxAge=2592000)](https://github.com/crimx/ext-saladict/blob/dev/LICENSE)\n\n[【官网】](https://www.crimx.com/ext-saladict/)Chrome/Firefox 浏览器插件，网页划词翻译。\n\n<p align=\"center\">\n  <a href=\"https://github.com/crimx/ext-saladict/releases/\" target=\"_blank\"><img src=\"https://raw.githubusercontent.com/wiki/crimx/ext-saladict/images/notebook.gif\" /></a>\n</p>\n\n沙拉查词 7 为完全重写的版本。增加了更多细腻的动效与流畅的交互，更快速更稳定更多自定义设置。\n\n## 下载\n\n见[下载页面](https://saladict.crimx.com/download.html)。\n\n## 改动日志\n\n[CHANGELOG.md](./CHANGELOG.md)\n\n## 从源码构建\n\n```bash\ngit clone git@github.com:crimx/ext-saladict.git\ncd ext-saladict\nyarn install\nyarn pdf\n```\n\n在项目根添加 `.env` 文件，参考 `.env.example` 格式（可留空如果你不需要这些词典）。\n\n```bash\nyarn build\n```\n\n在 `build/` 目录下可查看针对各个浏览器打包好的扩展包。\n\n## 开发\n\n见[项目贡献指南](./CONTRIBUTING-zh.md)。\n\n## 如何向本项目贡献代码\n\n见[项目贡献指南](./CONTRIBUTING-zh.md)。\n\n## 声明\n\n声明：沙拉查词作为自由开源的浏览器辅助插件，仅供学习交流，任何人均可免费获取产品与源码。如果认为你的合法权益收到侵犯请马上联系[作者](https://github.com/crimx)。\n\n沙拉查词项目为 [MIT](https://github.com/crimx/ext-saladict/blob/dev/LICENSE) 许可，你可以随意使用源码，但必须附带该许可与版权声明。请勿用于任何违法犯罪行为，沙拉查词强烈谴责并会尽可能配合追究责任。对于照搬源码二次发布的套壳项目沙拉查词有责任对平台和用户发出相应的举报和提醒。\n\n## 更多截图\n\n<p align=\"center\">\n  <a href=\"https://github.com/crimx/ext-saladict/releases/\" target=\"_blank\"><img src=\"https://github.com/crimx/ext-saladict/wiki/images/youdao-page.gif\" /></a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/crimx/ext-saladict/releases/\" target=\"_blank\"><img src=\"https://github.com/crimx/ext-saladict/wiki/images/screen-notebook.png\" /></a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/crimx/ext-saladict/releases/\" target=\"_blank\"><img src=\"https://github.com/crimx/ext-saladict/wiki/images/pin.gif\" /></a>\n</p>\n"
  },
  {
    "path": "README.md",
    "content": "# Saladict 沙拉查词\n\n[![Version](https://img.shields.io/github/release/crimx/ext-saladict.svg?label=version)](https://github.com/crimx/ext-saladict/releases)\n[![Chrome Web Store](https://badgen.net/chrome-web-store/users/cdonnmffkdaoajfknoeeecmchibpmkmg?icon=chrome&color=0f9d58)](https://chrome.google.com/webstore/detail/cdonnmffkdaoajfknoeeecmchibpmkmg?hl=en)\n[![Chrome Web Store](https://badgen.net/chrome-web-store/stars/cdonnmffkdaoajfknoeeecmchibpmkmg?icon=chrome&color=0f9d58)](https://chrome.google.com/webstore/detail/cdonnmffkdaoajfknoeeecmchibpmkmg?hl=en)\n[![Mozilla Add-on](https://badgen.net/amo/users/ext-saladict?icon=firefox&color=ff9500)](https://addons.mozilla.org/firefox/addon/ext-saladict/)\n[![Mozilla Add-on](https://badgen.net/amo/stars/ext-saladict?icon=firefox&color=ff9500)](https://addons.mozilla.org/firefox/addon/ext-saladict/)\n\n[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg?maxAge=2592000)](http://commitizen.github.io/cz-cli/)\n[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-brightgreen.svg?maxAge=2592000)](https://conventionalcommits.org)\n[![Standard - JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg?maxAge=2592000)](https://standardjs.com/)\n[![License](https://img.shields.io/github/license/crimx/ext-saladict.svg?colorB=44cc11?maxAge=2592000)](https://github.com/crimx/ext-saladict/blob/dev/LICENSE)\n\nChrome/Firefox WebExtension. Feature-rich inline translator with PDF support.\n\n[【中文说明】](./README-zh.md)Chrome/Firefox 浏览器插件，网页划词翻译。\n\n<p align=\"center\">\n  <a href=\"https://github.com/crimx/ext-saladict/releases/\" target=\"_blank\"><img src=\"https://raw.githubusercontent.com/wiki/crimx/ext-saladict/images/notebook.gif\" /></a>\n</p>\n\n## Downloads\n\n- [Chrome Web Store](https://chrome.google.com/webstore/detail/cdonnmffkdaoajfknoeeecmchibpmkmg?hl=en)\n- [Firefox Add-ons](https://addons.mozilla.org/firefox/addon/ext-saladict/)\n- [Microsoft Edge Addons](https://microsoftedge.microsoft.com/addons/detail/idghocbbahafpfhjnfhpbfbmpegphmmp)(Uploaded by @rumosky)\n- See [releases](https://github.com/crimx/ext-saladict/releases) for more.\n\nSaladict 7 is a complete rewrite with sophisticated interaction and buttery smooth experience. Built for speed, stability and customization.\n\n## Change Log\n\n[CHANGELOG.md](./CHANGELOG.md)\n\n## build from source\n\n```bash\ngit clone git@github.com:crimx/ext-saladict.git\ncd ext-saladict\nyarn install\nyarn pdf\n```\n\nAdd a `.env` file following the `.env.example` format(leave empty if you don't use these dictionaries).\n\n```bash\nyarn build\n```\n\nArtifacts can be found in `build/`.\n\n## Development\n\nSee the [contributing guide](./CONTRIBUTING.md).\n\n## How can I contribute?\n\n[CONTRIBUTING.md](./CONTRIBUTING.md)\n\n## Notice\n\nSaladict is a free and open-sourced project for study purpose only. Anyone can obtain a copy of Saladict free of charge. If you believe your legal rights have been violated please contact the [author](https://github.com/crimx) immediately.\n\nSaladict is licensed under [MIT](https://github.com/crimx/ext-saladict/blob/dev/LICENSE). You can use the source code freely as long as including a copy of license and copyright notice of Saladict.\n\nDO NOT use Saladict for any illegal or criminal activity. Saladict strongly condemns this behavior and will cooperate to the fullest extent possible in holding it accountable.\n\nAs for copy-and-paste clone products Saladict has the responsibility to send corresponding reports and warnings to platforms and users.\n\n## More screenshots:\n\n<p align=\"center\">\n  <a href=\"https://github.com/crimx/ext-saladict/releases/\" target=\"_blank\"><img src=\"https://github.com/crimx/ext-saladict/wiki/images/youdao-page.gif\" /></a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/crimx/ext-saladict/releases/\" target=\"_blank\"><img src=\"https://github.com/crimx/ext-saladict/wiki/images/screen-notebook.png\" /></a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/crimx/ext-saladict/releases/\" target=\"_blank\"><img src=\"https://github.com/crimx/ext-saladict/wiki/images/pin.gif\" /></a>\n</p>\n"
  },
  {
    "path": "assets/content.css",
    "content": ".saladict-div,\n.saladict-div > .saladict-external,\n.saladict-div > .saladict-panel {\n  display: block !important;\n  width: 0 !important;\n  height: 0 !important;\n  margin: 0 !important;\n  padding: 0 !important;\n  border: none !important;\n  outline: none !important;\n}\n"
  },
  {
    "path": "assets/fanyi.youdao.2.0/all-packed.css",
    "content": "html{_background:url(null) fixed;}.forbid-select{-moz-user-select:none;-khtml-user-select:none;user-select:none;}.OUTFOX_JTR_BAR{width:100%;margin:0;padding:0;border:none;position:fixed;z-index:2147483646;top:0;left:0;height:50px;_position:absolute;_top:expression((body.scrollTop+documentElement.scrollTop)+'px');}#OUTFOX_JTR_BAR_CLOSE_UPDATE_TIP{color:#429d3b;height:38px;width:200px;background:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/ydd_tip.png\") left top no-repeat;overflow:hidden;text-align:left;margin:0 auto;display:none;position:relative;top:-31px;z-index:1;}#OUTFOX_JTR_BAR_CLOSE_UPDATE_TIP div{margin-left:5px;color:#429d3b;height:38px;background:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/ydd_tip.png\") right top no-repeat;padding:3px 8px 3px 3px;}#OUTFOX_JTR_BAR_CLOSE_UPDATE_TIP .update-date{margin-left:20px;}#OUTFOX_JTR_BAR_UPDATE_SHADE{background-color:#818181;height:50px;left:0;position:absolute;top:0;width:100%;opacity:.75;filter:alpha(opacity = 75);display:none;}#OUTFOX_JTR_BAR_CLOSE_UPDATE_TIP_CONTENT_CLOSE{width:10px;height:10px;position:absolute;right:5px;top:5px;cursor:pointer;}.OUTFOX_JTR_BAR_CLOSE{display:block;background:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png\") -35px -69px no-repeat;width:17px;height:13px;position:absolute;top:1px;right:1px;cursor:pointer;}.OUTFOX_JTR_BAR_CLOSE:hover{background:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png\") -56px -69px no-repeat;}#OUTFOX_JTR_BAR_BODY{margin:0;padding:0;background:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp-repeat-x.png\") 0 -92px repeat-x;font-size:12px;height:50px;overflow:hidden;color:#6F6F6F;text-align:center;border:0;}#OUTFOX_JTR_BAR_BODY #wrapper{*width:960px;padding:0 10px;max-width:960px;margin:0 auto;text-align:left;}#OUTFOX_JTR_BAR_BODY .OUTFOX_BAR_TOTAL_NUM{font-family:Arial;font-size:18px;margin:0 5px;}#OUTFOX_JTR_BAR_BODY #headerLogo{background:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/logo.png\") no-repeat;text-indent:-999em;display:block;width:141px;height:41px;float:left;margin-top:7px;font-size:0;_background-image:none;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled = true,sizingMethod = image,src = \"http://shared.ydstatic.com/jtr/v1/images/header_logo.png\");_margin-top:17px;}#OUTFOX_JTR_BAR_BODY #sliderLabel{float:left;margin:22px 0 0 20px;}#OUTFOX_JTR_BAR_BODY #sliderWrapper{float:left;margin-top:6px;position:relative;left:15px;width:269px;}#OUTFOX_JTR_BAR_BODY #levelLabel{position:relative;height:20px;}#OUTFOX_JTR_BAR_BODY #level-0{right:10px;}#OUTFOX_JTR_BAR_BODY #level-1{left:149px;}#OUTFOX_JTR_BAR_BODY #level-2{left:83px;}#OUTFOX_JTR_BAR_BODY #level-3{left:17px;}#OUTFOX_JTR_BAR_BODY #levelLabel label{position:absolute;cursor:pointer;color:#4E86CC;}#OUTFOX_JTR_BAR_BODY.disable #levelLabel label{color:#ccc;cursor:default;}#OUTFOX_JTR_BAR_BODY #levelLabel .active{color:#707070;cursor:default;font-weight:bold;}#OUTFOX_JTR_BAR_BODY #status{float:left;margin:20px 10px 0 20px;_margin-top:22px;}#OUTFOX_JTR_BAR_BODY .statistic #status{margin-top:16px;}#OUTFOX_JTR_BAR_BODY #switchWrapper{float:right;margin-top:16px;}#OUTFOX_JTR_BAR_BODY #feedback{margin:18px 0 0 15px;float:right;text-align:right;}#OUTFOX_JTR_BAR_BODY #feedback a{color:#4E86CC;text-decoration:none;}#OUTFOX_JTR_BAR_BODY #feedback #fb{color:#FF8D3D;}#OUTFOX_JTR_BAR_BODY #feedback a:hover{text-decoration:underline;}#OUTFOX_JTR_BAR_BODY a:link{color:#4E86CC;text-decoration:none;}#OUTFOX_JTR_BAR_BODY #switch{color:#4D86CC;background-image:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/switch_button.png\");display:block;width:70px;height:23px;cursor:pointer;line-height:23px;text-align:center;text-decoration:none;}#OUTFOX_JTR_BAR_BODY #switch:hover{background-image:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/switch_button_hover.png\");}.OUTFOX_JTR_TRANSTIP_WRAPPER{z-index:2147483640;max-width:300px;*width:300px;}.OUTFOX_JTR_TRANSTIP_WRAPPER p{padding:10px;margin:0;}.OUTFOX_JTR_TRANSTIP_ADVISE_TOGGLE{display:block;cursor:pointer;}.OUTFOX_JTR_TRANSTIP_ADVISE_THANK_TIP .OUTFOX_JTR_TRANSTIP_ADVISE_TOGGLE{display:none;}.OUTFOX_JTR_TRANSTIP_ADVISE_THANK{display:none;}.OUTFOX_JTR_TRANSTIP_ADVISE_THANK_TIP .OUTFOX_JTR_TRANSTIP_ADVISE_THANK{display:inline;}.expand .OUTFOX_JTR_TRANSTIP_ADVISE_TEXT{width:250px;height:50px;margin:10px 0 0;resize:none;}.OUTFOX_JTR_NANCI_BAR{position:absolute;text-decoration:none;overflow:hidden;background:#fff;border:1px solid #AFCEF5;padding-left:3px;z-index:2147483641;-webkit-box-shadow:2px 2px 2px #ccc;-moz-box-shadow:2px 2px 2px #ccc;box-shadow:2px 2px 2px #ccc;}.OUTFOX_JTR_NANCI_BAR a{cursor:pointer;text-decoration:none;text-indent:-999em;width:15px;height:15px;display:inline-block;}.OUTFOX_JTR_NANCI_CTRL_DETAIL_BG{_width:15px;_height:15px;_zoom:1;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled = true,sizingMethod = image,src = \"http://shared.ydstatic.com/jtr/v1/images/icon_detail.png\");}.OUTFOX_JTR_NANCI_CTRL_CLOSE_BG{_width:15px;_height:15px;_zoom:1;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled = true,sizingMethod = image,src = \"http://shared.ydstatic.com/jtr/v1/images/icon_delete.png\");}.OUTFOX_JTR_NANCI_CTRL_DETAIL{_position:absolute;_margin-left:-8px;background:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png\") 0 -68px no-repeat;_background-image:none;}.OUTFOX_JTR_NANCI_CTRL_CLOSE{_position:absolute;_margin-left:-6px;background:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png\") -13px -70px no-repeat;_background-image:none;}.OUTFOX_JTR_TRANSTIP_ORIGIN .OUTFOX_JTR_TRANSTIP_ADVISE{padding-bottom:14px;}.OUTFOX_JTR_TRANSTIP_ADVISE_TEXT,.OUTFOX_JTR_TRANSTIP_ADVISE_SUBMIT{display:none;}.expand .OUTFOX_JTR_TRANSTIP_ADVISE_TEXT,.expand .OUTFOX_JTR_TRANSTIP_ADVISE_SUBMIT{display:block;}.finish .OUTFOX_JTR_TRANSTIP_ADVISE_TEXT,.finish .OUTFOX_JTR_TRANSTIP_ADVISE_SUBMIT{display:block;visibility:hidden;}div.expand a.OUTFOX_JTR_TRANSTIP_ADVISE_SUBMIT{background-image:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/trans_tip_submit_bg.png\");width:96px;height:23px;line-height:23px;text-align:center;cursor:pointer;float:right;margin-top:10px;outline:none;text-decoration:none;}div.expand a.OUTFOX_JTR_TRANSTIP_ADVISE_SUBMIT:hover{background-image:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/trans_tip_submit_bg_hover.png\");}.OUTFOX_NANCI_TIPS{white-space:nowrap;}.OUTFOX_JTR_NANCI_CTRL_WORD{color:#000;margin-right:5px;}#OUTFOX_JTR_BAR_BODY #failed{line-height:50px;}#OUTFOX_BAR_WRAPPER iframe.OUTFOX_JTR_BAR_HIDE{display:none;}*html .OUTFOX_JTR_TRANSTIP_WRAPPER .ydd-bg-bottom,*html .OUTFOX_JTR_TRANSTIP_WRAPPER .ydd-bg-top{width:300px;}*html .OUTFOX_JTR_TRANSTIP_WRAPPER{width:300px;border-width:0 1px;border-color:#d9d9d9;border-style:solid;}#yddWrapper{z-index:2147483640;max-width:280px;display:none;}.ydd-container *{padding:0;margin:0;font-size:12px;color:#2A2A2A;}.ydd-container{display:block;position:relative;width:100%;height:100%;font-size:12px;text-align:left;opacity:.95;*filter:alpha(opacity = 95);*background-color:#FFF;}.ydd-container div{display:block;float:none;}.ydd-container a{color:#4E86CC;}.ydd-container a:link{text-decoration:none;}.ydd-container a:hover{text-decoration:underline;}.ydd-body-wrapper{position:relative;padding:0 5px 0 6px;*background-color:transparent;}.ydd-body{padding:0 15px 10px;overflow:hidden;background-color:#fff;*background-color:transparent;}.ydd-lb{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAoCAYAAADdaosOAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNXG14zYAAAAWdEVYdENyZWF0aW9uIFRpbWUAMTAvMTUvMTDB0g89AAAANklEQVQoke3LsQ0AIAgF0YOEwsbZnPQPxSK6ALRWXPtyAA4EsIAt6WTmdZpKMLMa2mNgYOAbPAs0Bm/LtPJAAAAAAElFTkSuQmCC);*background-image:none;background-repeat:repeat-y;background-position:left 0;width:6px;height:100%;position:absolute;left:0;top:0;}.ydd-rb{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAoCAYAAADdaosOAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNXG14zYAAAAWdEVYdENyZWF0aW9uIFRpbWUAMTAvMTUvMTDB0g89AAAANElEQVQoke3JoREAIAwEwQuDwKDov7AvJhMaCBrzJ2/JzJJUwAE2sIA5IoKuN7TXYDB8hwuIjQgBYxgxJgAAAABJRU5ErkJggg==);*background-image:none;background-repeat:repeat-y;background-position:right 0;width:6px;height:100%;position:absolute;right:0;top:0;}.ydd-top-wrapper{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAUCAYAAAC07qxWAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAxMC8xNS8xMMHSDz0AAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzVxteM2AAAA2ElEQVQokcWQMY6DMBRE37etSAsFot8bUFPlAjlDjrIHyFFyAK7henuKPQAIEAhHeBsnIsFC0TY70sguxuOZEe8970AAE+4+kMiJBg7hwSufoIEPQL0ItsKqqk55nvfWWrfnKHVde+cczrmfYRiuZVlegFvgcqc0TeO11izLQtd1tG37XRTFEXBrsRrHkb7vmaaJJEnIsqyw1n6F/PfsKBFBRPDeM88zxhjSND0H0aOkUUptxtVaf67dAIzIpuCj6HqBqGNky/jXEdddxye8L9wp80fH/8v4C8bURyFzKYfYAAAAAElFTkSuQmCC);*background-image:none;background-repeat:no-repeat;background-position:right 0;padding-right:10px;height:20px;}.ydd-top{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAVAAAAAUCAYAAADbVmUXAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAZFJREFUeNrs2ktKw1AYhuFzSaIQLGRkRqEjR67A7Th1Irq0LqILElpDk3jS5nJ6/C0WYgn2fSAE21ILhZePpLppGtXSWiuB+CAAzNBf9ar58R8KAdXCB9IEFcDMRRMHsxEC2pwKqB9K6QCAubqZeHVKx1FE/YD2gTRdLE0QT8MKBTBj6cTr0z9q7zy8LgxoH03rnY0QUwCYm2zi5Vl3R9U9V4UR9a8Z6CCa0Wq1eiqK4jmO40djzD3fD4B/HtDePpxuYH66864bmd9uJvkL1HrxjNfr9Uue5+9pmqokSZS1lq8HwLUEVPVtdOet6+NWiqi4QNvlmWXZ62KxUFVVqc1mM7wZAFwT177bboXuwp97SgE1y+XyzcXTlmU5Pqm59Ang+rTt6yL6cfhzXKFiQKMoejDGEE0AOKxQK43JMKD7O+7uBXdtQAEAQx9PBvQIAQWAYYGq3wR0+J0nAQWA8wLKAgWAQF3XQzgJKACc6ayAcgceAMYeskAB4BILlIACAAEFgMsGlGugADD2UArolwADAA7gcFvj3gN0AAAAAElFTkSuQmCC);*background-image:none;background-repeat:no-repeat;background-position:left 0;height:20px;*width:240px;}.ydd-bottom-wrapper{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAeCAYAAAAVdY8wAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNXG14zYAAAAWdEVYdENyZWF0aW9uIFRpbWUAMTAvMTUvMTDB0g89AAAArElEQVQ4jeWUsQ3CMBBFX4AJEDukSJ2K/SgYhI3oU2SBFCnC4X8UOGCSECgoEJx0siw/ve9z4azrOmdQVVWR5/kGOAEGnFfuI26y3gclfdj4n9E/Ncz3RnvsWTA98FfgvDGEUANKrSNQEm3bHiLYw4/R7o6ZHcuy3AEhtS4kIQkzq5um2RdFsQXOEbzdMwPWcdNHhcR2j+b6bfgA9hTqQUum8ydNBiwHbze1cgH57aT/yHsyngAAAABJRU5ErkJggg==);*background-image:none;background-repeat:no-repeat;background-position:right 0;padding-right:10px;height:30px;}.ydd-bottom{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAU8AAAAeCAYAAACsaJwUAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNXG14zYAAAAWdEVYdENyZWF0aW9uIFRpbWUAMTAvMTUvMTDB0g89AAABXklEQVR4nO3dPY7TUBQF4PNshxSp0qVgB5Qp0rMMNkFHwYIowyqoUqJkJaTEpsAWJmQG6RWTjPJ90pOVn+JWR+e6eSVJk6RN0iVZ7Pf799vt9utmswkA13VP/TAMw0vOAfCqCE+AClfDcxiG9H3/0rMAvBqaJ0AF4QlQwdoOUEHzBKggPAEqWNsBKmieABU0T4AKmidABeEJUMHaDlBB8wSoIDwBKljbASpongAVpvAcxpOmaYZEeAI8p8sYmqNBeAL8X5uk5PclcCVJs16vf+x2u49N09x2MoA7VvInPKcbNN+cTqfvy+Xy7U0nA7hjl+88+yQ/z+fzl67rPmmfANeV/N0+m4z3tx+Px2+LxeJdKeWW8wHcpTJ7TuHZjqc7HA6fV6vVh7ZtrfAAM/PwvGygU4hOn8vs/wAPbf7Oc9LPvuvzb3AKUODhXQZheeLkyhPgYV0LwufCUnACJPkFXHpxDgJO4nYAAAAASUVORK5CYII=);*background-image:none;background-repeat:no-repeat;background-position:left 0;height:30px;_line-height:24px;line-height:24px;padding:0 15px;*width:210px;}.ydd-key-title{font-size:13px;font-weight:bold;}.ydd-phonetic{font-family:\"lucida sans unicode\",arial,sans-serif;color:#999;}.ydd-base-trans .ydd-tabs{display:none;}.ydd-trans-wrapper{margin:5px 0;}.ydd-trans-wrapper a{color:#999;}.ydd-tabs{background:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/swipe_hr.png\") 0 9px no-repeat;margin:5px 0;}.ydd-tab{color:#707070;background:#fff;padding-right:5px;}.ydd-voice{margin:0 5px;}.ydd-no-result{margin-top:7px;}html* .ydd-bg-top{background:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp-repeat-x.png\") 0 0 repeat-x;width:280px;height:40px;position:absolute;z-index:-1;top:0;left:0;}html* .ydd-bottom-wrapper{background:url(chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp-repeat-x.png) 0 -46px repeat-x;}html* #yddWrapper{width:280px;border-width:0 1px;border-color:#d9d9d9;border-style:solid;}html* .OUTFOX_JTR_TRANSTIP_WRAPPER #yddWrapper,html* .OUTFOX_JTR_TRANSTIP_WRAPPER .ydd-bg-bottom,html* .OUTFOX_JTR_TRANSTIP_WRAPPER .ydd-bg-top{width:300px;}html* .ydd-title{line-height:14px;}.slider-container{height:9px;width:269px;margin:0;cursor:pointer;background:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png\") 0 -56px no-repeat;_background-image:none;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled = true,sizingMethod = image,src = \"http://shared.ydstatic.com/jtr/v1/images/slider_bg.png\");}.disable .slider-container{cursor:default;}#sliderBackground{width:0;overflow:hidden;}.slider-background{height:100%;background:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png\") 0 -23px no-repeat;_background-image:none;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled = true,sizingMethod = image,src = \"http://shared.ydstatic.com/jtr/v1/images/slider_bar.png\");}.disable .slider-background{background:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png\") 0 -39px no-repeat;_background-image:none;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled = true,sizingMethod = image,src = \"http://shared.ydstatic.com/jtr/v1/images/slider_bar_gray.png\");}.slider{display:block;height:20px;width:75px;top:14px;position:absolute;background:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png\") 0 0 no-repeat;_background-image:none;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled = true,sizingMethod = image,src = \"http://shared.ydstatic.com/jtr/v1/images/slider.png\");cursor:pointer;}.slider:hover{background:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png\") -157px 0 no-repeat;_background-image:none;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled = true,sizingMethod = image,src = \"http://shared.ydstatic.com/jtr/v1/images/slider_hover.png\");}.disable .slider,.disable .slider:hover{background:url(\"chrome-extension://__MSG_@@extension_id__/assets/fanyi.youdao.2.0/bar-sp.png\") -78px 0 no-repeat;_background-image:none;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled = true,sizingMethod = image,src = \"http://shared.ydstatic.com/jtr/v1/images/slider_gray.png\");}\n"
  },
  {
    "path": "assets/fanyi.youdao.2.0/conn.html",
    "content": "\n<!DOCTYPE HTML>\n<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n<script type=\"text/javascript\" src=\"../browser-polyfill.min.js\"></script>\n<script type=\"text/javascript\" src=\"./conn.js\"></script>\n</head>\n<body></body>\n</html>\n"
  },
  {
    "path": "assets/fanyi.youdao.2.0/conn.js",
    "content": "if (!this.JSON) {\n  this.JSON = {};\n}\n/**\n* JSON 解析库\n*/\n(function() {\n  function f(n) {\n      return n < 10 ? '0' + n : n;\n  }\n\n  if (typeof Date.prototype.toJSON !== 'function') {\n      Date.prototype.toJSON = function(key) {\n          return isFinite(this.valueOf()) ? this.getUTCFullYear() + '-' +\n                  f(this.getUTCMonth() + 1) + '-' +\n                  f(this.getUTCDate()) + 'T' +\n                  f(this.getUTCHours()) + ':' +\n                  f(this.getUTCMinutes()) + ':' +\n                  f(this.getUTCSeconds()) + 'Z' : null;\n      };\n      String.prototype.toJSON = Number.prototype.toJSON = Boolean.prototype.toJSON = function(key) {\n          return this.valueOf();\n      };\n  }\n  var cx = /[\\u0000\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/g,escapable = /[\\\\\\\"\\x00-\\x1f\\x7f-\\x9f\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/g,gap,indent,meta = {'\\b':'\\\\b','\\t':'\\\\t','\\n':'\\\\n','\\f':'\\\\f','\\r':'\\\\r','\"':'\\\\\"','\\\\':'\\\\\\\\'},rep;\n\n  function quote(string) {\n      escapable.lastIndex = 0;\n      return escapable.test(string) ? '\"' + string.replace(escapable, function(a) {\n          var c = meta[a];\n          return typeof c === 'string' ? c : '\\\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);\n      }) + '\"' : '\"' + string + '\"';\n  }\n\n  function str(key, holder) {\n      var i,k,v,length,mind = gap,partial,value = holder[key];\n      if (value && typeof value === 'object' && typeof value.toJSON === 'function') {\n          value = value.toJSON(key);\n      }\n      if (typeof rep === 'function') {\n          value = rep.call(holder, key, value);\n      }\n      switch (typeof value) {case'string':return quote(value);case'number':return isFinite(value) ? String(value) : 'null';case'boolean':case'null':return String(value);case'object':if (!value) {\n          return'null';\n      }\n          gap += indent;partial = [];if (Object.prototype.toString.apply(value) === '[object Array]') {\n              length = value.length;\n              for (i = 0; i < length; i += 1) {\n                  partial[i] = str(i, value) || 'null';\n              }\n              v = partial.length === 0 ? '[]' : gap ? '[\\n' + gap +\n                      partial.join(',\\n' + gap) + '\\n' +\n                      mind + ']' : '[' + partial.join(',') + ']';\n              gap = mind;\n              return v;\n          }\n          if (rep && typeof rep === 'object') {\n              length = rep.length;\n              for (i = 0; i < length; i += 1) {\n                  k = rep[i];\n                  if (typeof k === 'string') {\n                      v = str(k, value);\n                      if (v) {\n                          partial.push(quote(k) + (gap ? ': ' : ':') + v);\n                      }\n                  }\n              }\n          } else {\n              for (k in value) {\n                  if (Object.hasOwnProperty.call(value, k)) {\n                      v = str(k, value);\n                      if (v) {\n                          partial.push(quote(k) + (gap ? ': ' : ':') + v);\n                      }\n                  }\n              }\n          }\n          v = partial.length === 0 ? '{}' : gap ? '{\\n' + gap + partial.join(',\\n' + gap) + '\\n' +\n                  mind + '}' : '{' + partial.join(',') + '}';gap = mind;return v;\n      }\n  }\n\n  if (typeof JSON.stringify !== 'function') {\n      JSON.stringify = function(value, replacer, space) {\n          var i;\n          gap = '';\n          indent = '';\n          if (typeof space === 'number') {\n              for (i = 0; i < space; i += 1) {\n                  indent += ' ';\n              }\n          } else if (typeof space === 'string') {\n              indent = space;\n          }\n          rep = replacer;\n          if (replacer && typeof replacer !== 'function' && (typeof replacer !== 'object' || typeof replacer.length !== 'number')) {\n              throw new Error('JSON.stringify');\n          }\n          return str('', {'':value});\n      };\n  }\n  if (typeof JSON.parse !== 'function') {\n      JSON.parse = function(text, reviver) {\n          var j;\n\n          function walk(holder, key) {\n              var k,v,value = holder[key];\n              if (value && typeof value === 'object') {\n                  for (k in value) {\n                      if (Object.hasOwnProperty.call(value, k)) {\n                          v = walk(value, k);\n                          if (v !== undefined) {\n                              value[k] = v;\n                          } else {\n                              delete value[k];\n                          }\n                      }\n                  }\n              }\n              return reviver.call(holder, key, value);\n          }\n\n          text = String(text);\n          cx.lastIndex = 0;\n          if (cx.test(text)) {\n              text = text.replace(cx, function(a) {\n                  return'\\\\u' +\n                          ('0000' + a.charCodeAt(0).toString(16)).slice(-4);\n              });\n          }\n          if (/^[\\],:{}\\s]*$/.test(text.replace(/\\\\(?:[\"\\\\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').replace(/\"[^\"\\\\\\n\\r]*\"|true|false|null|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?/g, ']').replace(/(?:^|:|,)(?:\\s*\\[)+/g, ''))) {\n              j = eval('(' + text + ')');\n              return typeof reviver === 'function' ? walk({'':j}, '') : j;\n          }\n          throw new SyntaxError('JSON.parse');\n      };\n  }\n}());\n\nwindow.onload = function() {\n  var BACKFLAG = 'dataBack';\n\n  /**\n   * 事件绑定\n   * @param object 要绑定事件的对象\n   * @param eventName 事件名称\n   * @param callback 事件处理函数\n   */\n  var bind = function(object, eventName, callback) {\n      if (!callback) {\n          return;\n      }\n      if (object.addEventListener) {\n          object.addEventListener(eventName, callback, false);\n      } else if (object.attachEvent) {\n          object.attachEvent('on' + eventName, callback);\n      } else {\n          object['on' + eventName] = callback;\n      }\n      return this;\n  };\n  /**\n   * 判断对象是否函数\n   * @param obj 待检查对象\n   */\n  var isFunction = function(obj) {\n      return !!(Object.prototype.toString.call(obj) === \"[object Function]\");\n  };\n  /**\n   * 跨域通信响应对象\n   */\n  var Response = {\n      /**\n       * 响应请求信息\n       * @param callback\n       */\n      onMessage:function(callback) {\n          if (!isFunction(callback)) {\n              callback = function() {\n              };\n          }\n          bind(window, 'message', function(eve) {\n              callback(eve);\n          });\n      },\n      /**\n       * 向另一个域发送请求\n       * @param responseData\n       */\n      sendMessage : function(responseData) {\n          parent.postMessage(JSON.stringify(responseData), '*');\n      }\n  };\n  /**\n   * 本地存储。 所有本地存储相关数据存储到 youdao 的域下，这样才能做到用户设置与域无关。\n   * @param key 键\n   * @param value 值\n   */\n  var storage = function(key, value) {\n      /**\n       * html5 中的本地存储方式\n       * @param key\n       * @param value\n       */\n      var html5LocalStorage = function(key, value) {\n          var store = window.localStorage;\n          if (value === undefined) {\n              return store.getItem(key);\n          }\n          if (key !== undefined && value !== undefined) {\n              store.setItem(key, value);\n              return value;\n          }\n      };\n      /**\n       * IE 本地存储方式 userData\n       * @param key\n       * @param value\n       */\n      var userdata = function(key, value) {\n          var store = document.documentElement;\n          store.addBehavior(\"#default#userData\");\n          if (value === undefined) {\n              store.load(\"fanyiweb2\");\n              return store.getAttribute(key);\n          }\n          if (key !== undefined && value !== undefined) {\n              store.setAttribute(key, value);\n              store.save(\"fanyiweb2\");\n              return value;\n          }\n      };\n      if (!!window.localStorage) {\n          return html5LocalStorage(key, value);\n      }\n      if (!!document.documentElement.addBehavior) {\n          return userdata(key, value);\n      }\n  };\n\n  /**\n   * 创建 Ajax 对象\n   */\n  function createXMLHttpObject() {\n      var XHRFactory = [\n              function () {\n                  return new XMLHttpRequest();\n              },\n              function () {\n                  return new ActiveXObject('Msxml2.XMLHTTP');\n              },\n              function () {\n                  return new ActiveXObject('Msxml3.XMLHTTP');\n              },\n              function () {\n                  return new ActiveXObject('Microsoft.XMLHTTP');\n              }\n      ];\n      var xhr = false;\n      for (var i = 0; i < XHRFactory.length; i++) {\n          try {\n              xhr = XHRFactory[i]();\n          } catch(e) {\n              continue;\n          }\n          break;\n      }\n      return xhr;\n  }\n\n  /**\n   * 处理跨域请求\n   */\n  var handleMessage = function() {\n      /**\n       * 将 request 中的 data 转为对象\n       * @param request\n       */\n      var initData = function(request) {\n          var dataArray = request.data,data = {};\n          if (typeof dataArray === 'string') {\n              dataArray = dataArray.split('&');\n          }\n\n          for (var i = 0; i < dataArray.length; i++) {\n              var d = dataArray[i].split('=');\n              data[d[0]] = d[1];\n          }\n          return data;\n      };\n      /**\n       * 所有请求的处理函数，请求的处理函数与 request.handler 属性值应保持一致\n       */\n      var handlers = {\n          /**\n           * 获取翻译的查询结果\n           * @param request 请求数据\n           */\n          translate : function(request) {\n              browser.runtime.sendMessage({ type: 'YOUDAO_TRANSLATE_AJAX', payload: request })\n                .then(response => {\n                  Response.sendMessage({\n                    ...response,\n                    'handler': BACKFLAG,\n                  })\n                })\n              // var xhr = createXMLHttpObject();\n              // xhr.onreadystatechange = function(event) {\n              //     if (xhr.readyState == 4) {\n              //         var data = xhr.status == 200 ? xhr.responseText : null;\n              //         Response.sendMessage({\n              //             'handler': BACKFLAG,\n              //             'response': data,\n              //             'index': request.index\n              //         });\n              //     }\n              // };\n              // xhr.open(request.type, request.url, true);\n\n              // if (request.type === 'POST') {\n              //     xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');\n              //     xhr.send(request.data);\n              // } else {\n              //     xhr.send(null);\n              // }\n          },\n          /**\n           * 本地存储\n           * @param request 请求信息\n           */\n          localStorage:function(request) {\n              var data = initData(request);\n              var result = decodeURIComponent(storage(data.key, data.value));\n              Response.sendMessage({\n                  'handler': BACKFLAG,\n                  'response': result,\n                  'index': request.index\n              });\n          }\n      };\n      return function(request) {\n          if (!!!handlers[request.handler]) {\n              throw new Error('类别为 ' + request.handler + ' 跨域请求处理函数不存在！');\n          }\n          handlers[request.handler](request);\n      };\n  }();\n  /**\n   * 注册消息处理机制\n   */\n  Response.onMessage(function(eve) {\n      handleMessage(JSON.parse(eve.data));\n  });\n\n  Response.sendMessage({handler:'transferStationReady'});\n}\n"
  },
  {
    "path": "assets/fanyi.youdao.2.0/main.js",
    "content": "/* eslint-disable */\n\n;(function () {\n  var JSONDAO, TR;\n  if(this.JSON&&this.JSON.stringify.toString().indexOf(\"[native code]\")!==-1){JSONDAO=this.JSON}else{JSONDAO={}}(function(){function f(n){return n<10?\"0\"+n:n}if(typeof Date.prototype.toJSON!==\"function\"){Date.prototype.toJSON=function(key){return isFinite(this.valueOf())?this.getUTCFullYear()+\"-\"+f(this.getUTCMonth()+1)+\"-\"+f(this.getUTCDate())+\"T\"+f(this.getUTCHours())+\":\"+f(this.getUTCMinutes())+\":\"+f(this.getUTCSeconds())+\"Z\":null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(key){return this.valueOf()}}var cx=/[\\u0000\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/g,escapable=/[\\\\\\\"\\x00-\\x1f\\x7f-\\x9f\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/g,gap,indent,meta={\"\\b\":\"\\\\b\",\"\\t\":\"\\\\t\",\"\\n\":\"\\\\n\",\"\\f\":\"\\\\f\",\"\\r\":\"\\\\r\",'\"':'\\\\\"',\"\\\\\":\"\\\\\\\\\"},rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'\"'+string.replace(escapable,function(a){var c=meta[a];return typeof c===\"string\"?c:\"\\\\u\"+(\"0000\"+a.charCodeAt(0).toString(16)).slice(-4)})+'\"':'\"'+string+'\"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value===\"object\"&&typeof value.toJSON===\"function\"){value=value.toJSON(key)}if(typeof rep===\"function\"){value=rep.call(holder,key,value)}switch(typeof value){case\"string\":return quote(value);case\"number\":return isFinite(value)?String(value):\"null\";case\"boolean\":case\"null\":return String(value);case\"object\":if(!value){return\"null\"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)===\"[object Array]\"){length=value.length;for(i=0;i<length;i+=1){partial[i]=str(i,value)||\"null\"}v=partial.length===0?\"[]\":gap?\"[\\n\"+gap+partial.join(\",\\n\"+gap)+\"\\n\"+mind+\"]\":\"[\"+partial.join(\",\")+\"]\";gap=mind;return v}if(rep&&typeof rep===\"object\"){length=rep.length;for(i=0;i<length;i+=1){k=rep[i];if(typeof k===\"string\"){v=str(k,value);if(v){partial.push(quote(k)+(gap?\": \":\":\")+v)}}}}else{for(k in value){if(Object.hasOwnProperty.call(value,k)){v=str(k,value);if(v){partial.push(quote(k)+(gap?\": \":\":\")+v)}}}}v=partial.length===0?\"{}\":gap?\"{\\n\"+gap+partial.join(\",\\n\"+gap)+\"\\n\"+mind+\"}\":\"{\"+partial.join(\",\")+\"}\";gap=mind;return v}}if(typeof JSONDAO.stringify!==\"function\"){JSONDAO.stringify=function(value,replacer,space){var i;gap=\"\";indent=\"\";if(typeof space===\"number\"){for(i=0;i<space;i+=1){indent+=\" \"}}else{if(typeof space===\"string\"){indent=space}}rep=replacer;if(replacer&&typeof replacer!==\"function\"&&(typeof replacer!==\"object\"||typeof replacer.length!==\"number\")){throw new Error(\"JSONDAO.stringify\")}return str(\"\",{\"\":value})}}if(typeof JSONDAO.parse!==\"function\"){JSONDAO.parse=function(text,reviver){var j;function walk(holder,key){var k,v,value=holder[key];if(value&&typeof value===\"object\"){for(k in value){if(Object.hasOwnProperty.call(value,k)){v=walk(value,k);if(v!==undefined){value[k]=v}else{delete value[k]}}}}return reviver.call(holder,key,value)}text=String(text);cx.lastIndex=0;if(cx.test(text)){text=text.replace(cx,function(a){return\"\\\\u\"+(\"0000\"+a.charCodeAt(0).toString(16)).slice(-4)})}if(/^[\\],:{}\\s]*$/.test(text.replace(/\\\\(?:[\"\\\\\\/bfnrt]|u[0-9a-fA-F]{4})/g,\"@\").replace(/\"[^\"\\\\\\n\\r]*\"|true|false|null|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?/g,\"]\").replace(/(?:^|:|,)(?:\\s*\\[)+/g,\"\"))){j=eval(\"(\"+text+\")\");return typeof reviver===\"function\"?walk({\"\":j},\"\"):j}throw new SyntaxError(\"JSONDAO.parse\")}}}());(function(){var a={browser:function(){var e={};var c=navigator.userAgent.toLowerCase();var d;(d=c.match(/msie ([\\d.]+)/))?e.msie=d[1]:(d=c.match(/firefox\\/([\\d.]+)/))?e.firefox=d[1]:(d=c.match(/chrome\\/([\\d.]+)/))?e.chrome=d[1]:(d=c.match(/opera.([\\d.]+)/))?e.opera=d[1]:(d=c.match(/version\\/([\\d.]+).*safari/))?e.safari=d[1]:0;return e}(),isDOM:function(c){return Boolean(c&&c.nodeType===1)},isArray:function(c){return Object.prototype.toString.call(c)===\"[object Array]\"},isFunction:function(c){return Object.prototype.toString.call(c)===\"[object Function]\"},each:function(d,g,f){if(d===undefined||d===null){return}if(d.length===undefined||a.isFunction(d)){for(var c in d){if(d.hasOwnProperty(c)){if(g.call(f||d[c],c,d[c])===false){break}}}}else{for(var e=0;e<d.length;e++){if(g.call(f||d[e],e,d[e])===false){break}}}return d},indexOf:function b(d,e){if(d.indexOf){return d.indexOf(e)}else{var c=-1;a.each(d,function(f){if(this===e){c=f;return false}});return c}},bind:function(d,c,e){if(!e){return}if(d.addEventListener){d.addEventListener(c,e,false)}else{if(d.attachEvent){d.attachEvent(\"on\"+c,e)}else{d[\"on\"+c]=e}}return this},unbind:function(d,c,e){if(!e){return}if(d.removeEventListener){d.removeEventListener(c,e,false)}else{if(d.detachEvent){d.detachEvent(\"on\"+c,e)}else{d[\"on\"+c]=function(){}}}return this},param:function(c){if(typeof c===\"string\"){return c}var d=[];a.each(c,function(e,f){if(f){f=encodeURIComponent(f);if(a.browser.firefox){f=encodeURIComponent(unescape(f))}d.push(encodeURIComponent(e)+\"=\"+f)}});return d.join(\"&\")},makeArray:function(c){return Array.prototype.slice.call(c,0)},getDocumentCharset:function(){a.log(\"document.characterSet || document.charset:::\"+document.characterSet||document.charset);return document.characterSet||document.charset},log:function(){},css:function(){var c=function(h,d){var i=\"\";if(d==\"float\"){document.defaultView?d=\"float\":d=\"styleFloat\"}if(h.style[d]){i=h.style[d]}else{if(h.currentStyle){i=h.currentStyle[d]}else{if(document.defaultView&&document.defaultView.getComputedStyle){d=d.replace(/([A-Z])/g,\"-$1\").toLowerCase();var f=document.defaultView.getComputedStyle(h,\"\");i=f&&f.getPropertyValue(d)}else{i=null}}}if((i==\"auto\"||i.indexOf(\"%\")!==-1)&&(\"width\"===d.toLowerCase()||\"height\"===d.toLowerCase())&&h.style.display!=\"none\"&&i.indexOf(\"%\")!==-1){i=h[\"offset\"+d.charAt(0).toUpperCase()+d.substring(1).toLowerCase()]+\"px\"}if(d==\"opacity\"){try{i=h.filters[\"DXImageTransform.Microsoft.Alpha\"].opacity;i=i/100}catch(j){try{i=h.filters(\"alpha\").opacity}catch(g){}}}return i};return function(e,d){if(typeof d===\"string\"){return c(e,d)}else{a.each(d,function(f,g){e.style[f]=g})}}}(),getPageSize:function(){var g,c;if(window.innerHeight&&window.scrollMaxY){g=document.body.scrollWidth;c=window.innerHeight+window.scrollMaxY}else{g=Math.max(document.body.scrollWidth,document.body.offsetWidth);c=Math.max(document.body.scrollHeight,document.body.offsetHeight)}var e,h;e=document.documentElement.clientWidth||document.body.clientWidth;h=document.documentElement.clientHeight||document.body.clientHeight;var f=Math.max(c,h);var d=Math.max(g,e);return{page:{width:d,height:f},window:{width:e,height:h}}},findPos:function(c){var d={x:0,y:0};if(!!document.documentElement.getBoundingClientRect()){d.x=c.getBoundingClientRect().left+a.scroll().left;d.y=c.getBoundingClientRect().top+a.scroll().top}else{while(c){d.x+=c.offsetLeft;d.y+=c.offsetTop;c=c.offsetParent}}return d},textPos:function(e,h){var i=e||window.event;var g={};var j={};var d=h.h;var f=h.v||\"bottom\";if(window.getSelection){g=window.getSelection().getRangeAt(0)}else{if(document.selection){g=document.selection.createRange()}}if(!!d){j.x=c[d]+a.scroll().left}else{if(i.pageX||i.pageY){j.x=i.pageX}else{if(i.clientX||i.clientY){j.x=i.clientX+a.scroll().left}}}if(!!g.getBoundingClientRect){var c=g.getBoundingClientRect();j.y=c[f]+a.scroll().top}else{if(i.pageX||i.pageY){j.y=i.pageY}else{if(i.clientX||i.clientY){j.y=i.clientY+a.scroll().top}}}return j},scroll:function(){return{left:document.documentElement.scrollLeft+document.body.scrollLeft,top:document.documentElement.scrollTop+document.body.scrollTop}},walkTheDOM:function walkTheDOM(e,d,c){if(c&&!c(e)){return}d(e);if(e.tagName===\"NOSCRIPT\"){return}else{if(e.tagName===\"IFRAME\"||e.tagName===\"FRAME\"){return}else{e=e.firstChild}}while(e){walkTheDOM(e,d,c);e=e.nextSibling}},getTextNodes:function(e,c){var d=[];a.walkTheDOM(e,function(f){if(f.nodeType===3&&a.trim(f.nodeValue)){d.push(f)}},c);return d},getElementsByClassName:function(e,d){if(e.getElementsByClassName){return e.getElementsByClassName(d)}else{var c=[];a.walkTheDOM(e,function(f){if(a.hasClass(f,d)){c.push(f)}});return c}},query:function(e,f){var d=new RegExp(\"(?:(?:\\\\.([^()]+))?)(?:(?:#([^()]+))?)\");var c=d.exec(e);var i=f||document;if(!c){return null}else{if(!!c[1]){var h=c[1];if(i.getElementsByClassName){return i.getElementsByClassName(h)}else{var g=[];a.walkTheDOM(i,function(j){if(a.hasClass(j,h)){g.push(j)}});return g}}if(!!c[2]){return i.getElementById(c[2])}}},trim:function(c){return c.replace(/^\\s*/,\"\").replace(/\\s*$/,\"\")},formatTemplate:function(f,g){var d=document.createElement(\"div\");for(var e in g){if(g.hasOwnProperty(e)){f=f.replace(new RegExp(\"{\"+e+\"}\",\"g\"),g[e])}}d.innerHTML=f;var c=d.firstChild;d.removeChild(c);return c},hasClass:function(g,f){if(a.isDOM(g)){if(g.className===f){return true}var e=g.className.split(\" \"),d=0,c=e.length;for(;d<c;d++){if(f===e[d]){return true}}}return false},loadCSS:function(e,d){var c=function(j){if(e&&e.createElement){var i=Date.parse(new Date()),g=e.createElement(\"link\");var f=j;g.setAttribute(\"rel\",\"stylesheet\");g.setAttribute(\"href\",f);g.setAttribute(\"type\",\"text/css\");var h=e.getElementsByTagName(\"head\")[0]||e.body;h.appendChild(g)}};if(a.isArray(d)){a.each(d,function(f,g){c(g)})}else{if(typeof d===\"string\"){c(d)}}},addClass:function(g,f){if(a.isDOM(g)){var e=g.className.split(\" \"),d=0,c=e.length;for(;d<c;d++){if(f===e[d]){return}}e[d]=f;g.className=e.join(\" \")}},removeClass:function(g,f){if(a.isDOM(g)){var e=g.className.split(\" \"),d=0,c=e.length,h=[];for(;d<c;d++){if(f!==e[d]){h.push(e[d])}}g.className=h.join(\" \")}},toggleClass:function(g,f){if(a.isDOM(g)){var e=g.className.split(\" \"),d=0,c=e.length,j=[],h=\"add\";for(;d<c;d++){if(f===e[d]){h=\"remove\"}else{j.push(e[d])}}if(h===\"add\"){e[d]=f}else{e=j}g.className=e.join(\" \")}},guid:function(){var c=function(){return(((1+Math.random())*65536)|0).toString(16).substring(1)};return function(){return(c()+c()+\"-\"+c()+\"-\"+c()+\"-\"+c()+\"-\"+c()+c()+c())}}(),protoExtend:function(e,c){var d=a.isFunction(c)?c:function(){};d.prototype=e;return new d()},stopPropagation:function(d){var c=d||window.event;if(c.stopPropagation){c.stopPropagation()}c.cancelBubble=true;return c},storage:function(d,f){var e=function(h,i){var g=window.localStorage;if(i===undefined){return g.getItem(h)}if(h!==undefined&&i!==undefined){g.setItem(h,i);return i}};var c=function(h,i){var g=document.documentElement;g.addBehavior(\"#default#userData\");if(i===undefined){g.load(\"fanyiweb2\");return g.getAttribute(h)}if(h!==undefined&&i!==undefined){g.setAttribute(h,i);g.save(\"fanyiweb2\");return i}};if(!!window.localStorage){return e(d,f)}if(!!document.documentElement.addBehavior){return c(d,f)}},cookie:function(c,f){function d(g,i){var h=30;var j=new Date();j.setTime(j.getTime()+h*24*60*60*1000);document.cookie=g+\"=\"+i+\";expire*=\"+j.toGMTString()}function e(h){var g=document.cookie.match(new RegExp(\"(^| )\"+h+\"=([^;]*)(;|$)\"));if(g!=null){return decodeURIComponent(g[2])}return null}if(!!f){d(c,f)}else{return e(c)}},parseData:function(){var c={json:function(d){try{return d=JSONDAO.parse(d)}catch(f){a.log(\"[Error]\",\"Invalid JSON data:\",d)}},xml:function(e){if(window.DOMParser){return(new DOMParser()).parseFromString(e,\"text/xml\")}else{var d=new ActiveXObject(\"Microsoft.XMLDOM\");d.async=\"false\";d.loadXML(e);return d}}};return function(d,e){if(d===undefined){return e}if(a.isFunction(d)){return d(e)}if(!!d&&!c[d]){a.log(\"[Error]\",\"Function parseData() dosen't support this type:\",d);return e}return c[d](e)}}(),once:function(c){return function(){if(a.isFunction(c)){c.apply(this,arguments)}c=function(){}}}};window.J=a})();(function(a){var b=function(e,d,c){return new b.prototype.init(e,d,c)};b.prototype={init:function(c,f,g){var d=this;var e=[];var h=function(i){d.ajax=i;while(e.length>0){i(e.pop())}};if(!!window.postMessage){this.createMessageChannel(c,f,g,h)}else{this.createJTRAssist(c,h)}this.ajax=function(i){e.push(i);return this};return this},createMessageChannel:function(d,f,h,i){var e=this;var c=(function(){if(a.isDOM(a.query(\"#\"+d))){throw new Error(\"Existed CDA iFrame element\")}if(f&&h){var j=document.createElement(\"iframe\");j.setAttribute(\"id\",d);j.className=(\"OUTFOX_JTR_CONN\");j.style.display=\"none\";j.setAttribute(\"src\",h);document.body.appendChild(j);return j.contentWindow}else{throw new Error(\"Empty domain is not allowed\")}})();var g=(function(){var k=[];var l=0;var j={transferStationReady:function(){i(function(m){m.data=a.param(m.data);m.index=l++;k[m.index]={dataType:m.dataType,callback:m.callback};delete m.callback;c.postMessage(JSONDAO.stringify(m),'*');return e})},dataBack:function(n){if(!!n&&!!k[n.index]){var m=k[n.index];if(a.isFunction(m.callback)){m.callback(a.parseData(m.dataType,n.response))}delete k[n.index]}}};return function(n){var m=JSONDAO.parse(n.data);j[m.handler](JSONDAO.parse(n.data))}})();a.bind(window,\"message\",function(j){g.call(e,j)})},createJTRAssist:function(h,e){var k=this;var d=\"http://fanyi.youdao.com/web2/JTRAssist.swf?\"+(+new Date());var j=function(){if(!!a.query(\"#\"+h)){return}var l=document.createElement(\"div\");if(a.browser.msie===\"6.0\"||a.browser.msie===\"7.0\"){l.innerHTML='<object classid=\"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000\" width=\"1\" height=\"1\" id=\"'+h+'\"><param name=\"allowScriptAccess\" value=\"always\" /><param name=\"movie\" value=\"'+d+'\" /></object>'}else{l.innerHTML='<object height=\"1\" width=\"1\" id=\"'+h+'\" data=\"'+d+'\"><param name=\"allowScriptAccess\" value=\"always\"></object>'}document.body.appendChild(l)};var c=\"outfox_jtr_fproxy_callback_\",f=0;var g=function(m){var l=c+(f++);window[l]=function(n){m.callback.call(k,a.parseData(m.dataType,decodeURI(n)))};a.query(\"#\"+h).load(m.url,m.data,m.type||\"POST\",'window[\"'+l+'\"]')};var i=function(n){var l=n.data.key,m=n.data.value;if(m===undefined){if(a.isFunction(n.callback)){n.callback(a.parseData(n.dataType,a.query(\"#\"+h).getItem(l)))}}else{a.query(\"#\"+h).setItem(l,m)}};j();window.JTRAssistIsReady=function(){e(function(l){switch(l.handler){case\"translate\":l.data=a.param(l.data);g(l);break;case\"localStorage\":i(l);break;default:throw new Error(\"Unsupported request type :\"+l.handler)}return k})}}};b.prototype.init.prototype=b.prototype;a.CDA=b})(J);(function(a){TR=function(d,b,g,c){this._manager=c;this._reqSize=b.reqSize;this._onStatusChange=b.onStatusChange||function(){};this._url=b.url;this.conn=g;this._context=d;this._request=function(i,j,k){g.ajax({url:i,handler:\"translate\",type:\"POST\",data:j,callback:k,dataType:\"json\"})};var h=new a.Page(d);var f=[];var e=h.getMainArticle();if(e){f=a.getTextNodes(e.elem,TR.isInclude)}this.mainNodeLength=f.length||null;this._nodeIndex=[];f=f.concat(a.getTextNodes(d,function(i){return(i!==(e&&e.elem))&&TR.isInclude(i)}));a.each(f,function(i,j){this._nodeIndex.push(this._manager.addNode(j))},this);this.workingThread=0;this.guid=b.guid||a.guid()};TR.prototype={doTrans:function(){var d=++this.workingThread;var h={ue:a.getDocumentCharset()||null,data:null,relatedUrl:document.location.href,guid:this.guid,mainLength:this.mainNodeLength,requestId:a.guid()};var j=[];var f=0;for(var g=0;g<this._nodeIndex.length;g++){if(this._manager.transResults[g]){if(d===this.workingThread){this._manager.replaceTrans(g)}}else{var e=null;if(j[parseInt(f/this._reqSize,10)]){e=j[parseInt(f++/this._reqSize,10)]}else{e=j[parseInt(f++/this._reqSize,10)]={}}var i=null;try{i=this._manager.nodes[g].parentNode&&this._manager.nodes[g].parentNode.tagName||null}catch(c){}e[g]={src:this._manager.originals[g],tag:i}}}if(j.length===0){this._onStatusChange({id:d,action:\"TRANS\",level:\"0\",status:\"finish\"});return}var k=this,b=function(l){k._onStatusChange({id:d,action:\"TRANS\",level:\"0\",status:\"busy\",data:[l,j.length]});h.data=JSONDAO.stringify(j[l]);k._request(k._url.textTrans,h,function(m){k._updateTrans(m,d);if(++l<j.length){b(l)}else{k._onStatusChange({id:d,action:\"TRANS\",level:\"0\",status:\"finish\"})}})};b(0)},revertTrans:function(){var c=++this.workingThread;for(var b=0;b<this._nodeIndex.length;b++){if(c===this.workingThread){this._manager.revertTrans(this._nodeIndex[b])}}},_updateTrans:function(b,c){if(!!!b){return}if(b.errorCode===40&&b.errorCode===30){a.log(\"Get Error Code:\",b.errorCode);return}a.each(b.data,function(d,e){this._manager.transResults[d]=e.tst;if(c===this.workingThread){this._manager.replaceTrans(d)}},this)},doTips:function(b){var e=++this.workingThread;var i={type:\"X\",ue:a.getDocumentCharset()||null,data:null,relatedUrl:document.location.href,mainLength:this.mainNodeLength,guid:this.guid};var k=[];var g=0;for(var h=0;h<this._nodeIndex.length;h++){if(this._manager.tipsResults[h]){if(e===this.workingThread){this._manager.replaceTips(h,b)}}else{var f=null;if(k[parseInt(g/this._reqSize,10)]){f=k[parseInt(g++/this._reqSize,10)]}else{f=k[parseInt(g++/this._reqSize,10)]={}}var j=null;try{j=this._manager.nodes[h].parentNode&&this._manager.nodes[h].parentNode.tagName||null}catch(d){}f[h]={src:this._manager.originals[h],tag:j}}}if(k.length===0){this._onStatusChange({id:e,action:\"TIPS\",level:b,status:\"finish\"});return}var l=this;var c=function(m){a.log(\"Send:\",k[m]);l._onStatusChange({id:e,action:\"TIPS\",level:b,data:[m,k.length],status:\"busy\"});i.data=JSONDAO.stringify(k[m]);l._request(l._url.tips,i,function(n){l._updateTips(n,e,b);if(++m<k.length){c(m)}else{l._onStatusChange({id:e,action:\"TIPS\",level:b,status:\"finish\"})}})};c(0)},revertTips:function(){var c=++this.workingThread;for(var b=0;b<this._nodeIndex.length;b++){if(c===this.workingThread){this._manager.revertTips(this._nodeIndex[b])}}},_updateTips:function(e,d,c){if(!!!e){return}if(e.errorCode===40&&e.errorCode===30){a.log(\"Get Error Code:\",e.errorCode);return}var b=function(g,f){return g.start>f.start};a.each(e.data,function(f,g){if(g.length>0){this._bubbleSort(g,b);this._manager.tipsResults[f]=g;if(d===this.workingThread){this._manager.replaceTips(f,c)}}else{this._manager.tipsResults[f]=[]}},this)},_bubbleSort:function(c,f){for(var d=c.length-2;d>=0;d--){for(var b=0;b<=d;b++){if(f(c[b+1],c[b])){var e=c[b];c[b]=c[b+1];c[b+1]=e}}}return c}};TR.isInclude=function(b){return !(b.tagName===\"SCRIPT\"||b.tagName===\"STYLE\"||b.tagName===\"PRE\"||(b.className&&/OUTFOX_JTR_/.test(b.className)))};a.TR=TR})(J);if(!J||!J.bind){throw new Error(\"swipe extension need J.bind support\")}(function(h){var g=\"http://fanyi.youdao.com\",f=g+\"/fsearch\",b=g+\"/translate\";var j=function(k){return'<object classid=\"clsid:d27cdb6e-ae6d-11cf-96b8-444553540000\" codebase=\"http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,0,0\" width=\"15px\" height=\"15px\" align=\"absmiddle\" id=\"speach_flash\"><param name=\"allowScriptAccess\" value=\"sameDomain\" /><param name=\"movie\" value=\"http://cidian.youdao.com/chromeplus/voice.swf\" /><param name=\"loop\" value=\"false\" /><param name=\"menu\" value=\"false\" /><param name=\"quality\" value=\"high\" /><param name=\"wmode\"  value=\"transparent\"><param name=\"FlashVars\" value=\"audio='+k+'\"><embed wmode=\"transparent\" play=\"false\" src=\"http://cidian.youdao.com/chromeplus/voice.swf\" loop=\"false\" menu=\"false\" quality=\"high\" bgcolor=\"#ffffff\" width=\"15\" height=\"15\" align=\"absmiddle\" allowScriptAccess=\"sameDomain\" FlashVars=\"audio='+k+'\" type=\"application/x-shockwave-flash\" pluginspage=\"http://www.macromedia.com/go/getflashplayer\" /></object>'};var c=function(k){return k.split&&k.split(\" \").length||0};var e={isJapanese:function(k){return !Boolean(/[^\\u0800-\\u4e00]/.test(k))},isContainJapanese:function(k){var m=0;for(var l=0;l<k.length;l++){if(this.isJapanese(k.charAt(l))){m++}}return m>2},isKoera:function(k){for(i=0;i<k.length;i++){if(((k.charCodeAt(i)>12592&&k.charCodeAt(i)<12687)||(k.charCodeAt(i)>=44032&&k.charCodeAt(i)<=55203))){return true}}return false},isContainKoera:function(k){var m=0;for(var l=0;l<k.length;l++){if(this.isKoera(k.charAt(l))){m++}}return m>0},isChinese:function(k){return !Boolean(/[^\\u4e00-\\u9fa5]/.test(k))},isContainChinese:function(k){var m=0;for(var l=0;l<k.length;l++){if(this.isChinese(k.charAt(l))){m++}}return m>5}};var a=function(k,l){return new a.fn.init(k,l)};a.fn=a.prototype={init:function(l,m){var k=this;this.wrapper=a.createFrameWrapper();this.conn=a.initConnection(m);this.context=l;h.bind(document.body,\"click\",function(o){var n=o||window.event;k.wrapper.style.display=\"none\";k.wrapper.style.position=\"absolute\";k.wrapper.innerHTML=\"\"})},enableSwipe:function(){if(!this._swipeListener){var k=this;this._swipeListener=function(l){k._onSwipe.call(k,l)};h.bind(this.context,\"mouseup\",this._swipeListener)}},disableSwipe:function(){if(this._swipeListener){h.unbind(this.context,\"mouseup\",this._swipeListener);delete this._swipeListener}},_onSwipe:function(l){var k=\"\",n=\"\";var m={};if(window.getSelection){k=window.getSelection()}else{if(document.selection){k=document.selection.createRange()}}if(k.toString){n=k.toString()}else{if(k.text){n=k.text.toString()}}var o=h.textPos(l,{});n=h.trim(n);if(!a.validateSwipeWord(n)){return}this.swipeWord(n,o.x,o.y);if(this.onSwipeCallback){this.onSwipeCallback(n)}},swipeWord:function(p,k,q,o,n){var l=this;var m=null;this.wrapper.innerHTML=\"\";if((!e.isContainChinese(p)&&c(p)>=3)||(e.isContainChinese(p)||e.isContainJapanese(p)&&p.length>4)){m=\"translate\"}else{m=\"dict\"}this.conn.request({action:m,word:p},function(r){l.wrapper.innerHTML=\"\";l._onResponse.call(l,r);a.initWrapper(l.wrapper,k,q,o,n)})},_onResponse:function(l){var m=l.firstChild,k=null;if(!m){return}else{if(m.baseName&&m.baseName==\"xml\"){m=m.nextSibling}}switch(m.tagName){case\"response\":k=a.processXmlTransData(l);break;case\"yodaodict\":k=a.processXmlDictData(l);break;default:throw new Error(\"Incorrect xml data\")}if(k){this.wrapper.appendChild(k);this.wrapper.style.display=\"block\"}}};a.createFrameWrapper=function(){var k=document.createElement(\"div\");k.id=\"yddWrapper\";h.bind(k,\"click\",function(l){h.stopPropagation(l)});h.bind(k,\"mouseup\",function(l){h.stopPropagation(l)});document.body.appendChild(k);return k};a.validateSwipeWord=function(k){return !(k===\"\"||k.length>2000)};a.initConnection=function(k){var n=null;var m=function(q){var p=null,r=null;if(q.action==\"dict\"){r={client:\"JTRHelper\",keyfrom:\"JTRHelper.bookmark\",q:q.word,pos:-1,doctype:\"xml\",xmlVersion:\"3.2\",dogVersion:\"1.0\",vendor:\"jtr\",le:\"eng\"};p=f}else{r={client:\"JTRHelper\",keyfrom:\"JTRHelper.bookmark\",i:q.word,doctype:\"xml\",xmlVersion:\"1.1\",dogVersion:\"1.0\"};p=b}return[p,r]};if(window.chrome&&window.chrome.extension&&window.chrome.extension.sendRequest){n={request:function(p,q){window.chrome.extension.sendRequest(p,function(r){if(r){q((new DOMParser()).parseFromString(r,\"text/xml\"))}})}};return n}else{if(k){n={request:function(q,r){var p=m(q);k.ajax({url:p[0],handler:\"translate\",data:p[1],callback:r,dataType:\"xml\",type:\"POST\"})}};return n}else{if(h.CDA){var l=null;try{l=h.CDA(\"_OUTFOX_JTR_SWIPE_CONN\",g,CONN_FILE_PATH)}catch(o){throw new Error(\"Unable to get cross-domain ajax file.\")}n={request:function(q,r){var p=m(q);l.ajax({url:p[0],handler:\"translate\",data:p[1],callback:r,dataType:\"xml\",type:\"POST\"})}};return n}else{throw new Error(\"Unable to initialize cross-domain connection port.\")}}}};a.initWrapper=function(p,t,s,q,D){var n=0,v=0,B=50,l=h.scroll().top,z=h.scroll().left,u=p.clientHeight,C=p.clientWidth,w=h.getPageSize().window.height,F=h.getPageSize().window.width;q=q||0;D=D||0;if(s-u>=l+B){v=s-u}else{v=s+D}if(t+C<=F+z){n=t+q}else{n=F+z-C}var r=!!(h.css(document.body,\"position\")!==\"static\");var A=h.css(document.body,\"marginLeft\");var o=h.css(document.body,\"marginRight\");if(A===\"auto\"&&o===\"auto\"){var E=h.getPageSize().page.width;var k=parseInt(h.css(document.body,\"width\"));if(E>k){A=(E-k)/2}else{A=0}}A=r?parseInt(A):0;var m=r?parseInt(h.css(document.body,\"marginTop\")):0;h.css(p,{position:\"absolute\",left:(n-A)+\"px\",top:(v-m)+\"px\"})};a.processXmlTransData=function(r){var l=(r.getElementsByTagName(\"input\")[0].childNodes[1]||r.getElementsByTagName(\"input\")[0].childNodes[0]).nodeValue,q=(r.getElementsByTagName(\"translation\")[0].childNodes[1]||r.getElementsByTagName(\"translation\")[0].childNodes[0]).nodeValue,p=r.getElementsByTagName(\"response\")[0].getAttribute(\"errorCode\")-0,n=h.trim(q),m=h.trim(l);if((e.isContainChinese(m)||e.isContainJapanese(m)||e.isContainKoera(m))&&m.length>15){m=m.substring(0,8)+\" ...\"}else{if(m.length>25){m=m.substring(0,15)+\" ...\"}}if(m==n){return null}var o=\"http://fanyi.youdao.com/translate?i=\"+encodeURIComponent(l)+\"&keyfrom=chrome\";var k='<div class=\"ydd-container\">                          <div class=\"ydd-top-wrapper\">                            <div class=\"ydd-top\">                            </div>                          </div>                          <div class=\"ydd-body-wrapper\">                            <div class=\"ydd-lb\"></div>                            <div class=\"ydd-rb\"></div>                            <div class=\"ydd-body\">                              <div class=\"ydd-titile\">                                <span>{input}</span>                                <span><a href=\"{searchURL}\" target=\"_blank\">详细&rsaquo;&rsaquo;</a></span>                              </div>                              <div class=\"ydd-middle\">                                <div class=\"ydd-trans-wrapper ydd-simple-trans\">                                  <div class=\"ydd-trans-container\">{trans}</div>                                </div>                              </div>                            </div>                          </div>                          <div class=\"ydd-bottom-wrapper\"><div class=\"ydd-bottom\"></div></div>                          <div class=\"ydd-bg-top\"></div></div>                        </div>';return h.formatTemplate(k,{searchURL:o,input:a.escapeHTML(m),trans:a.escapeHTML(q)})};a.processXmlDictData=function(D){var l=null,n=null,q=[],z=[],x=\"\",v=\"\",F=\"\",r=\"\",w=0;var t=function(H){try{return D.getElementsByTagName(H)[0].firstChild.nodeValue}catch(G){return\"\"}};x=t(\"return-phrase\");v=t(\"dictcn-speach\");F=t(\"lang\");r=t(\"phonetic-symbol\");if((n=D.getElementsByTagName(\"translation\"))&&n.length>0){for(w=0;w<n.length;w++){q.push(n[w].getElementsByTagName(\"content\")[0].firstChild.nodeValue)}}if((l=D.getElementsByTagName(\"web-translation\"))&&l.length>0){for(w=0;w<l.length-1;w++){z.push({key:l[w].getElementsByTagName(\"key\")[0].firstChild.nodeValue,value:l[w].getElementsByTagName(\"trans\")[0].getElementsByTagName(\"value\")[0].firstChild.nodeValue})}}var B=\"http://dict.youdao.com/search?q=\"+encodeURIComponent(x)+\"&keyfrom=chrome.extension\"+F,E=x,A=\"\",o=\"\",m=\"\",u=null,C=\"http://www.youdao.com/search?q={title}&keyfrom=fanyi.jtr\",s=\"\";s='<div class=\"ydd-container\">                      <div class=\"ydd-top-wrapper\">                        <div class=\"ydd-top\"></div>                      </div>                      <div class=\"ydd-body-wrapper\">                        <div class=\"ydd-lb\"></div>                        <div class=\"ydd-rb\"></div>                        <div class=\"ydd-body\">                          <div class=\"ydd-titile\">                            <span class=\"ydd-key-title\">{title}</span>                            <span class=\"ydd-phonetic\">{phonetic}</span> {speechHTML} <span class=\"ydd-detail\"><a href=\"{searchURL}\" target=\"_blank\">详细&rsaquo;&rsaquo;</a></span>                          </div>                          <div class=\"ydd-middle\">                            <div class=\"ydd-trans-wrapper ydd-base-trans\">                              <div class=\"ydd-tabs\"><span class=\"ydd-tab\">基本翻译</span></div>{baseTransHTML}</div>                              <div class=\"ydd-trans-wrapper ydd-web-trans\">                                <div class=\"ydd-tabs\"><span class=\"ydd-tab\">网络释义</span></div>{webTransHTML}</div>                              </div>                            </div>                          </div>                        <div class=\"ydd-bottom-wrapper\">                          <div class=\"ydd-bottom\"><a href=\"'+C+'\" target=\"_blank\" title=\"使用有道搜索 {title}\">搜索&nbsp;{title}</a></div>                        </div>                        <div class=\"ydd-bg-top\"></div>                      </div>                    </div>';if((e.isContainChinese(E)||e.isContainJapanese(E)||e.isContainKoera(E))&&E.length>15){E=E.substring(0,10)+\"...\"}else{if(E.length>25){E=E.substring(0,15)+\" ...\"}}if(q.length+z.length>0&&v){A='<span class=\"ydd-voice\">'+j(\"http://dict.youdao.com/speech?audio=\"+v,\"test\",\"CLICK\",\"dictcn_speech\")+\"</span>\"}for(w=0;w<q.length;w++){o+='<div class=\"ydd-trans-container\">'+q[w]+\"</div>\"}for(w=0;w<z.length;w++){var p=\"http://dict.youdao.com/search?q=\"+encodeURIComponent(z[w].key)+\"&keyfrom=chrome.extension\"+F;m+='<div class=\"ydd-trans-container\"><a href=\"'+p+'\" target=\"_blank\">'+z[w].key+\":</a> \"+z[w].value+\"</div>\"}u=h.formatTemplate(s,{phonetic:r?\"[\"+r+\"]\":\"\",title:E,searchURL:B,speechHTML:A,baseTransHTML:o,webTransHTML:m});var k=h.query(\".ydd-middle\",u)[0];n=h.query(\".ydd-base-trans\",k)[0];l=h.query(\".ydd-web-trans\",k)[0];if(q.length+z.length===0){k.innerHTML='<p class=\"ydd-no-result\">没有英汉互译结果</p>'}try{if(q.length===0){k.removeChild(n)}else{if(z.length===0){k.removeChild(l)}}}catch(y){}return u};var d={\"&\":\"&amp;\",\"<\":\"&lt;\",\">\":\"&gt;\",'\"':\"&quot;\",\"'\":\"&#39;\",\"/\":\"&#x2F;\"};a.escapeHTML=function(k){return String(k).replace(/[&<>\"'\\/]/g,function(l){return d[l]})};a.fn.init.prototype=a.fn;h.Swipe=a})(J);(function(a){var b=function(d,c,f){var e=this;if(!a.isDOM(d)){throw Error(\"Invalid slider container element\")}this.container=d;if(!a.isDOM(c)){throw Error(\"Invalid slider controller block element\")}this.controller=c;for(var g=this.container;g;g=g.parentNode){if(g.nodeType===9){this.document=g;break}}if(!g){throw Error(\"Can't find parent Document element of container, container dom node should insert to document first\")}if(a.isDOM(f.bar)){this.bar=f.bar}else{this.bar=null}this.range=Number(f.max-f.min);if(!(this.range&&this.range>0)){throw Error(\"range must greater than 0\")}if(a.isFunction(f.callback)){this.callback=function(h){f.callback.call(e,h)}}this.borderFix=Number(f.borderFix)||0;this.mousemove=function(h){e._mousemove(h)};this.mouseup=function(h){e._mouseup(h)};this.mousedown=function(h){e._mousedown(h)}};b.prototype={enable:function(){a.bind(this.container,\"mousedown\",this.mousedown)},disable:function(){a.unbind(this.container,\"mousedown\",this.mousedown)},to:function(e,d){var c=this,f=null;tempFunc=function(){if(!c.container.offsetHeight||!c.container.clientWidth){f=setTimeout(tempFunc,200);return}var g=c.container.clientWidth-c.controller.clientWidth-2*c.borderFix;pos=e/c.range*g;c._valueChange(pos/g*c.range,pos)};tempFunc()},_mousemove:function(d){var c=d||window.event;this._moveHandler(c,false)},_mouseup:function(d){var c=d||window.event;this._moveHandler(c,true);a.unbind(this.document,\"mouseup\",this.mouseup);a.unbind(this.document,\"mousemove\",this.mousemove);this.container.style.cursor=\"pointer\"},_mousedown:function(d){var c=d||window.event;this._moveHandler(c,false);a.bind(this.document,\"mouseup\",this.mouseup);a.bind(this.document,\"mousemove\",this.mousemove);if(c.preventDefault){c.preventDefault()}c.returnValue=false},_moveHandler:function(e,i){var f=e.clientX-1/2*this.controller.clientWidth-a.findPos(this.container).x-this.borderFix,h=this.container.clientWidth-this.controller.clientWidth-2*this.borderFix,g=h/this.range,c=g/2,d=f%g,j=0;if(f<0){f=0;d=0}else{if(f>h){f=h;d=0}}if(i&&d<c){j=f-d}else{if(i&&d>g-c){j=f-d+g}else{j=f}}this._valueChange(j/h*this.range,j)},_valueChange:function(c,d){this.callback(c);this.controller.style.left=d+this.borderFix+\"px\";if(this.bar){this.bar.style.width=d+this.controller.clientWidth/2+\"px\"}}};a.Slider=b})(J);(function(a){var b=function(){this.nodes=[];this.originals={};this.transResults={};this.tipsResults={}};b.prototype={addNode:function(e,c){var d=0;if(!c){for(;d<this.nodes.length;d++){if(this.nodes[d]===e){return d}}}this.nodes.push(e);this.originals[d]=e.nodeValue;return d},replaceTrans:function(d){if(this.nodes[d]&&this.transResults[d]){var f=this.nodes[d],h=document.createElement(\"font\"),c=this.transResults[d];try{if(f.nodeValue===c){return false}if(!f.parentNode){return}}catch(g){return}f.nodeValue=\"\";h.className=\"OUTFOX_JTR_TRANS_NODE\";h.id=\"OUTFOX_JTR_TRANS_NODE-\"+d;h.setAttribute(\"rel\",d);h.innerHTML=c;try{f.parentNode.insertBefore(h,f.nextSibling)}catch(e){}}},revertTrans:function(c){if(this.nodes[c]&&this.originals[c]){var e=a.query(\"#OUTFOX_JTR_TRANS_NODE-\"+c);if(e&&e.parentNode){e.parentNode.removeChild(e)}try{this.nodes[c].nodeValue=this.originals[c]}catch(d){}}},replaceTips:function(d,c){var e=c||-1;if(this.nodes[d]&&this.tipsResults[d]){a.each(this.tipsResults[d],function(q,n){if(n.level&&n.level<e){return}var k=this.nodes[d],i=n.start,m=n.end,o=n.explain;if(typeof k.parentNode===undefined){return}var h=document.createElement(\"font\");h.className=\"OUTFOX_NANCI_WRAPPER\";try{var p=k.nodeValue.substr(0,i),g=k.nodeValue.substr(i,m-i),f=k.nodeValue.substr(m);k.nodeValue=p;h.innerHTML=g;k.parentNode.insertBefore(document.createTextNode(f),k.nextSibling);k.parentNode.insertBefore(h,k.nextSibling);var j=document.createElement(\"font\");j.className=\"OUTFOX_NANCI_TIPS\";j.setAttribute(\"rel\",d);j.innerHTML=\"(\"+o+\")\";h.parentNode.insertBefore(j,h.nextSibling)}catch(l){}},this)}},revertTips:function(c){if(this.nodes[c]){var d;do{try{d=!this.revertTip(this.nodes[c].nextSibling)}catch(e){d=true}}while(!d)}},revertTip:function(d){if(a.hasClass(d,\"OUTFOX_NANCI_WRAPPER\")&&d.firstChild){var c=d.nextSibling;if(a.hasClass(c,\"OUTFOX_NANCI_TIPS\")){c.parentNode.removeChild(c)}if(d.nextSibling&&d.nextSibling.nodeType===3){d.firstChild.nodeValue+=d.nextSibling.nodeValue;d.parentNode.removeChild(d.nextSibling)}if(d.previousSibling&&d.previousSibling.nodeType===3){d.previousSibling.nodeValue+=d.firstChild.nodeValue;d.parentNode.removeChild(d)}return true}else{return false}},countTips:function(d){var c=0;a.each(this.tipsResults,function(g,e){for(var f=0;f<e.length;f++){if(e[f].level>=d){c++}}},this);return c}};a.NodeManager=b})(J);(function(a){var b=function(){this.map={};this.dataMap={}};b.prototype={getId:function(c){var d=null;a.each(this.map,function(f,e){if(e===c){d=f;return false}});if(d===null){d=a.guid();this.map[d]=c}return d},data:function(g,c,f){var h=this.getId(g);if(arguments.length===3){if(!this.dataMap[h]){this.dataMap[h]={}}this.dataMap[h][c]=f;return f}else{var e=null;try{e=this.dataMap[h][c]}catch(d){e=undefined}return e}}};a.Cache=b})(J);(function(a){var b=function(d){this.contentDocument=d;this.cache=new a.Cache()};b.prototype={IGNORE_TAGS:[\"HTML\",\"HEAD\",\"META\",\"TITLE\",\"SCRIPT\",\"STYLE\",\"LINK\",\"IMG\",\"FORM\",\"INPUT\",\"BUTTON\",\"TEXTAREA\",\"SELECT\",\"OPTION\",\"LABEL\",\"IFRAME\",\"UL\",\"OL\",\"LI\",\"DD\",\"DT\",\"A\",\"OBJECT\",\"PARAM\",\"EMBED\",\"NOSCRIPT\",\"EM\",\"B\",\"STRONG\",\"I\",\"INS\",\"BR\",\"HR\",\"PRE\",\"H1\",\"H2\",\"H3\",\"H4\",\"H5\",\"CITE\"],getMainArticle:function(){return null;var g=null,e=\"\";if(!!location){e=location.hostname}if(/\\b(google|facebook|twitter)\\b/i.test(e)){return null}var d=this._getAllArticle();if(!(d&&d.length)){return null}d.sort(function(i,h){return !!(h.weight-i.weight)});for(var f=2;f>0;f--){g=d[0];d.splice(0,1);break}return g},_getAllArticle:function(){var f=this.contentDocument.getElementsByTagName(\"*\"),e=[];for(var d=0,h=f.length>100?100:f.length;d<h;d++){var g=f[d];if(this._checkTagName(g)&&this._checkSize(g)&&this._checkVisibility(g)){e[e.length]=new c(g)}}return e},_checkTagName:function(d){return a.indexOf(this.IGNORE_TAGS,d.tagName)==-1},_checkVisibility:function(d){return !(a.css(d,\"visibility\")==\"hidden\"||a.css(d,\"display\")==\"none\"||parseInt(a.css(d,\"height\"))<=0||parseInt(a.css(d,\"width\"))<=0)},_checkSize:function(d){return d.offsetWidth>300&&d.offsetHeight>200}};var c=function(d){this.elem=d;this._texts=this._getAllTexts();this.weight=this.calcWeight()};c.prototype={IGNORE_TAGS:[\"A\",\"DD\",\"DT\",\"OL\",\"OPTION\",\"PRE\",\"SCRIPT\",\"STYLE\",\"UL\",\"IFRAME\"],MINOR_REGEXP:/comment|combx|disqus|foot|header|menu|rss|shoutbox|sidebar|sponsor/i,MAJOR_REGEXP:/article|entry|post|body|column|main|content/i,TINY_REGEXP:/comment/i,BLANK_REGEXP:/\\S/i,_getAllTexts:function(){var g=[],d=a.getTextNodes(this.elem);for(var h=0,f=d.length;h<f;h++){var j=d[h];if(this._checkTagName(j)&&this._checkLength(j)){var e=j.parentNode||{},i=e.parentNode||{};if(!(this._checkMinorContent(e)||this._checkMinorContent(i))){g.push(j)}}}return g},calcStructWeight:function(){var d=0;for(var i=0,e=this._texts.length;i<e;i++){var j=this._texts[i],g=j.nodeValue.length,h=1;if(g>20){continue}for(var f=j.parentNode;f&&f!=this.elem;f=f.parentNode){h-=0.1}d+=Math.pow(g*h,1.25)}return d},calcContentWeight:function(){var d=1;for(var e=this.elem;e;e=e.parentNode){var f=e.id+\" \"+e.className;if(this.MAJOR_REGEXP.test(f)){d+=0.4}if(this.MINOR_REGEXP.test(f)){d-=0.8}}return d},calcWeight:function(){return this.calcStructWeight()*this.calcContentWeight()},_checkTagName:function(d){return a.indexOf(this.IGNORE_TAGS,d.tagName)==-1},_checkLength:function(d){return Boolean(this.BLANK_REGEXP.test(d.nodeValue))},_checkMinorContent:function(d){return Boolean(this.TINY_REGEXP.test(d.id+\" \"+d.className))}};a.Page=b})(J);(function(a){var b={runCount:0,swipe:true,mode:\"TIPS\",level:1};var c={0:[\"TIPS\",3],1:[\"TIPS\",2],2:[\"TIPS\",1],3:[\"TRANS\",\"0\"],4:[\"NONE\",\"NONE\"]};a.TR.UI=function(e,f){var d=this;this.initLogger(f.logURL);this.log({action:\"start\"});this.guid=a.guid();this.context=e;this.conn=a.CDA(\"OUTFOX_JTR_CDA\",f.domain,f.connFilePath);this.update=f.update;this.updateTipMsg=f.updateTipMsg;this.updateDate=f.updateDate;this.manager=new a.NodeManager();this.barHeight=50;this.permissionDenied=\"由于该网页存在安全性限制, 无法加载有道网页翻译2.0\";this.translator=new a.TR(e,{reqSize:f.reqSize,onStatusChange:function(){d._trStatusChangeCallback.apply(d,arguments)},url:{textTrans:f.transURL,tips:f.tipsURL},guid:this.guid},this.conn,this.manager);this.queue={TRANS:{0:{currentThread:-1}},TIPS:{1:{currentThread:-1},2:{currentThread:-1},3:{currentThread:-1}},NONE:{NONE:{}}};this.mode=null;this.level=null;this.initFrame(f.cssURL,function(){var h=\"\";var g=this;if(location){h=location.href}this.movePage(g.barHeight);this.frame.body.innerHTML='                <div id=\"wrapper\">                    <a href=\"http://fanyi.youdao.com/web2/?keyfrom=headerLogo\" target=\"_blank\">                        <h1 id=\"headerLogo\" class=\"logo\"></h1>                    </a>                    <div id=\"sliderLabel\">翻译级别</div>                    <div id=\"sliderWrapper\" class=\"slider-wrapper\">                        <div id=\"levelLabel\">                            <label id=\"level-3\" rel=\"0\"><a>专&nbsp;&nbsp;&nbsp;家</a></label>                            <label id=\"level-2\" rel=\"1\"><a>进&nbsp;&nbsp;&nbsp;阶</a></label>                            <label id=\"level-1\" rel=\"2\"><a>入&nbsp;&nbsp;&nbsp;门</a></label>                            <label id=\"level-0\" rel=\"3\"><a>全文翻译</a></label>                        </div>                        <div id=\"sliderContainer\" class=\"slider-container\">                            <div id=\"sliderBackground\"><div class=\"slider-background\"></div></div>                            <a id=\"slider\" href=\"javascript:void(0);\" class=\"slider\"></a>                        </div>                    </div>                    <div id=\"status\"></div>                                        <div id=\"switchWrapper\"><a id=\"switch\" href=\"javascript:void(0);\"></a></div>                </div>                <a id=\"OUTFOX_JTR_BAR_CLOSE\" href=\"javascript:void(0);\" class=\"OUTFOX_JTR_BAR_CLOSE\"></a>                <div id=\"OUTFOX_JTR_BAR_UPDATE_SHADE\"></div>                <div id=\"OUTFOX_JTR_BAR_CLOSE_UPDATE_TIP\">                    <div id=\"OUTFOX_JTR_BAR_CLOSE_UPDATE_TIP_CONTENT\"></div>                </div>';this.initTipContent();this.initBarClose();this.initSwitch();this.initSlider();this.initLabel();this.initTipsCtrl();this.initTransTip();this.initSwipe();var i=function(j){g.loadSetting(j);g.enable();g.writeSettings({runCount:g.settings.runCount+1});a.each(c,function(k,l){if(g.mode===l[0]&&g.level===l[1]){g.slider.to(k)}})};this.conn.ajax({handler:\"localStorage\",data:{key:\"settings\"},dataType:\"json\",callback:function(j){i(j)}})})};a.TR.UI.prototype={positionElementInViewPort:function(k){var n=k.tip;var x=k.target;var l=!!(a.css(document.body,\"position\")!==\"static\");var r=a.css(document.body,\"marginLeft\");var h=a.css(document.body,\"marginRight\");if(r===\"auto\"&&h===\"auto\"){var w=a.getPageSize().page.width;var d=parseInt(a.css(document.body,\"width\"));if(w>d){r=(w-d)/2}else{r=0}}r=l?parseInt(r):0;var f=l?parseInt(a.css(document.body,\"marginTop\")):0;var j=a.findPos(x),p=0,g=0,e=a.scroll().top,s=a.scroll().left,t=j.x,m=j.y,i=x.offsetHeight,o=n.clientHeight,u=n.clientWidth,q=a.getPageSize().window.height,v=a.getPageSize().window.width;if(m-o>=e+this.barHeight){p=m-o}else{p=m+i}if(t+u<=v+s){g=t}else{g=v+s-u}a.css(k.tip,{position:\"absolute\",top:(p-f)+\"px\",left:(g-r)+\"px\"})},disable:function(){this.changeMode(\"NONE\",\"NONE\");this.slider.disable();this.frame.body.className=\"disable\";this.disabled=true;this.updateStatus();this.switchElem.innerHTML=\"重新翻译\"},enable:function(){this.changeMode(this.settings.mode,this.settings.level);this.slider.enable();a.removeClass(this.frame.body,\"disable\");a.addClass(this.frame.body,\"enable\");this.disabled=false;this.updateStatus();this.switchElem.innerHTML=\"取消翻译\"},_trStatusChangeCallback:function(e){if(!e.id||!e.action||!e.level){return}var d=this.queue[e.action][e.level];if(d.currentThread<=e.id){d.currentThread=e.id;d.status=e.status;d.data=e.data||null;if(e.action===this.mode&&e.level===this.level){this.updateStatus()}}},updateStatus:function(){var f=this.queue[this.mode][this.level];a.removeClass(this.statusElem.parentNode,\"statistic\");if(f.status===\"busy\"&&f.data){this.switchElem.style.visibility=\"hidden\";var d=parseInt(f.data[0]*100/f.data[1],10);if(this.mode===\"TRANS\"){this.statusElem.innerHTML=\"正在翻译&nbsp;\"+d+\"%&nbsp;...\"}else{this.statusElem.innerHTML=\"正在分析&nbsp;\"+d+\"%&nbsp;...\"}this.statusElem.className=\"busy\"}else{if(f.status===\"finish\"){this.switchElem.style.visibility=\"inherit\";if(this.mode===\"TRANS\"){this.statusElem.innerHTML=\"翻译完成\"}else{var e=this.manager.countTips(this.level);if(e!==0){a.addClass(this.statusElem.parentNode,\"statistic\");this.statusElem.innerHTML='共注释<span class=\"OUTFOX_BAR_TOTAL_NUM\">'+e+\"</span>个难词\"}else{this.statusElem.innerHTML=\"恭喜您！该网页上没有难词~\"}}this.statusElem.className=\"finish\"}else{this.switchElem.style.visibility=\"inherit\";this.statusElem.innerHTML=\"翻译助手已关闭\";this.statusElem.className=\"finish\"}}},initLogger:function(d){this.logURL=d;this._logImgCache=[]},log:function(e){/*if(this.logURL){e.relatedURL=document.location.href;e.guid=this.guid;var d=new Image();d.src=this.logURL+\"?\"+a.param(e)+\"&\"+(new Date()).getTime();this._logImgCache[this._logImgCache.length]=d}*/},initSwipe:function(){var d=this;this.swipe=a.Swipe(this.context,this.conn);this.swipe.onSwipeCallback=function(e){d.log({action:\"swipeWord\",word:e})}},movePage:function(d){if(a.browser.msie){var f=a.css(this.context,\"paddingTop\");try{f=parseInt(f)}catch(e){f=0}this.context.style.cssText+=\";padding-top:\"+(d+f)+\"px !important;\"}else{var g=a.css(this.context,\"marginTop\");try{g=parseInt(g)}catch(e){g=0}if(a.css(this.context,\"position\")===\"static\"){a.css(this.context,{position:\"relative\"})}this.context.style.cssText+=\";margin-top:\"+(d+g)+\"px !important;\"}},initFrame:function(f,i){var d=this;var h=document.createElement(\"div\");h.id=\"OUTFOX_BAR_WRAPPER\";this.context.appendChild(h);this.wrapper=h;function g(k){k.innerHTML='<iframe id=\"OUTFOX_JTR_BAR\" src=\"\" style=\"display:none;\"></iframe>';var j=a.query(\"#OUTFOX_JTR_BAR\");j.setAttribute(\"frameBorder\",0);if(a.browser.msie&&document.domain!=window.location.hostname){j.src=\"javascript:void(document.write(\\\"<script>document.domain='\"+document.domain+\"';<\\/script><body></body>\\\"))\"}if(a.browser.msie&&!(a.browser.msie===\"8.0\"&&document.compatMode===\"CSS1Compat\")){a.css(j,{width:a.getPageSize().window.width+\"px\"});a.bind(window,\"resize\",function(l){a.css(j,{width:a.getPageSize().window.width+\"px\"})})}return j}var e=g(h);this.iframe=e;setTimeout(function(){try{d.frame=e.contentDocument||e.contentWindow.document}catch(j){d.log({action:\"secRestrict\",relatedURL:window.location.href});alert(d.permissionDenied);return}a.css(e,{display:\"block\"});a.addClass(e,\"OUTFOX_JTR_BAR\");a.loadCSS(d.frame,f);d.frame.body.id=\"OUTFOX_JTR_BAR_BODY\";a.addClass(d.frame.body,\"forbid-select\");d.frame.body.onselectstart=d.frame.body.ondrag=function(){return false};i.call(d)},100)},initBarClose:function(){var d=this;var e=this.frame.getElementById(\"OUTFOX_JTR_BAR_CLOSE\");a.addClass(e,\"OUTFOX_JTR_BAR_CLOSE\");a.bind(window,\"resize\",function(f){a.css(e,{top:1+\"px\",right:1+\"px\"})});a.bind(e,\"click\",function(){d.disable();d.movePage(-d.barHeight);d.context.removeChild(d.wrapper);d.context.removeChild(a.query(\"#OUTFOX_JTR_CDA\"));d.context.removeChild(a.query(\"#yddWrapper\"));d.context.removeChild(a.query(\"#outfox_seed_js\"))})},initTipContent:function(){if(!this.update){return}var d=this;var e=function(){var i=a.query(\"#OUTFOX_JTR_BAR_CLOSE_UPDATE_TIP_CONTENT\",d.frame);var f=a.query(\"#OUTFOX_JTR_BAR_UPDATE_SHADE\",d.frame);var h=a.query(\"#OUTFOX_JTR_BAR_CLOSE_UPDATE_TIP\",d.frame);i.innerHTML=\"更新提示：<br/>\"+d.updateTipMsg+'<span class=\"update-date\">'+d.updateDate+'</span><a href=\"javascript:void(0);\" id=\"OUTFOX_JTR_BAR_CLOSE_UPDATE_TIP_CONTENT_CLOSE\"></a>';var g=a.query(\"#OUTFOX_JTR_BAR_CLOSE_UPDATE_TIP_CONTENT_CLOSE\",d.frame);a.css(f,{display:\"block\"});a.css(h,{display:\"block\"});a.bind(g,\"click\",function(){a.css(f,{display:\"none\"});a.css(h,{display:\"none\"})})};d.conn.ajax({handler:\"localStorage\",data:{key:\"date\"},callback:function(f){if(f!==d.updateDate){e();d.conn.ajax({handler:\"localStorage\",data:{key:\"date\",value:d.updateDate}})}}})},initSwitch:function(){var d=this;this.statusElem=a.query(\"#status\",this.frame);this.switchElem=a.query(\"#switch\",this.frame);a.bind(this.switchElem,\"click\",function(f){if(d.disabled){d.enable()}else{d.disable()}})},initLabel:function(){this.labels=[];var d=this;var f=function(i){var g=i||window.event,h=g.target||g.srcElement,j=h.parentNode.getAttribute(\"rel\");if(d.disabled){return}if(j){d.changeMode(c[j][0],c[j][1]);a.each(c,function(k,l){if(d.mode===l[0]&&d.level===l[1]){d.slider.to(k)}})}};for(var e=0;e<4;e++){this.labels[e]=a.query(\"#level-\"+e,this.frame);a.bind(this.labels[e],\"click\",f)}},initSlider:function(){var e=a.query(\"#sliderContainer\",this.frame);var d=a.query(\"#slider\",this.frame);var f=this,h=null,g=10;this.slider=new a.Slider(e,d,{bar:a.query(\"#sliderBackground\",this.frame),max:3,min:0,step:1,borderFix:-3,callback:function(i){clearTimeout(h);h=setTimeout(function(){f._valueChange(i)},g)}})},loadSetting:function(f){var e={};if(f===null||Object.prototype.toString.call(f)!==\"[object Object]\"){this.conn.ajax({handler:\"localStorage\",dataType:\"json\",data:{key:\"settings\",value:JSONDAO.stringify(b)}});e=b}else{for(var d in b){if(b.hasOwnProperty(d)){e[d]=f.hasOwnProperty(d)?f[d]:b[d]}}}e.mode=e.mode||b.mode;e.level=e.level||b.level;a.log(\"Load Settings:\",e);this.settings=e},_valueChange:function(f){var g=Math.round(f),e=c[g][0],d=c[g][1];if(e!==this.mode){this.changeMode(e,d)}else{if(d!==this.level){this.changeLevel(d)}}},changeMode:function(e,f){if(this.mode===\"TIPS\"){for(var d=1;d<=3;d++){this.labels[d].className=\"deactive\"}this.translator.revertTips()}else{if(this.mode===\"TRANS\"){this.labels[0].className=\"deactive\";this.translator.revertTrans()}}this.swipe.disableSwipe();this.disableTransTip();this.disableTipsCtrl();this.mode=e;this.changeLevel(f)},changeLevel:function(e){a.log(\"Change level to:\",e);if(this.level===null){this.log({action:\"view\",level:e})}else{this.log({action:\"changeLevel\",oldLevel:this.level,newLevel:e})}this.level=e;if(this.mode===\"TIPS\"){this.swipe.enableSwipe();for(var d=1;d<=3;d++){this.labels[d].className=d==e?\"active\":\"deactive\"}this.translator.revertTips();this.translator.doTips(this.level);this.enableTipsCtrl()}else{if(this.mode===\"TRANS\"){this.labels[0].className=\"active\";this.translator.doTrans();this.enableTransTip()}}if(this.mode!==\"NONE\"){this.writeSettings({mode:this.mode,level:this.level})}},writeSettings:function(d){a.each(d,function(e,f){this.settings[e]=f},this);this.conn.ajax({handler:\"localStorage\",dataType:\"json\",data:{key:\"settings\",value:JSONDAO.stringify(this.settings)}})},initTipsCtrl:function(){var d=this,f=this.initTipsCtrlElem();var e=null;this.tipsTarget=null;this.tipsCtrlHandler=function(j){var h=j||window.evt,i=h.target||h.srcElement,g=0,k=0;clearTimeout(e);if(i===d.tipsTarget||i.className&&i.className.indexOf(\"OUTFOX_JTR_NANCI_\")!==-1){return}e=setTimeout(function(){if(f.parentNode){f.parentNode.removeChild(f)}if(a.hasClass(i,\"OUTFOX_NANCI_TIPS\")){var t=a.findPos(i),l=a.query(\".OUTFOX_JTR_NANCI_CTRL_WORD\",f)[0],m=null;d.context.appendChild(f);l.innerHTML=i.innerHTML;var v=a.css(i,\"fontSize\");var r=a.css(i,\"fontFamily\");try{if(v.indexOf(\"em\")!=-1){}else{if(parseInt(v)<12){v=\"12px\"}}}catch(q){v=\"12px\"}var w=!!(a.css(document.body,\"position\")!==\"static\");var o=a.css(document.body,\"marginLeft\");var s=a.css(document.body,\"marginRight\");if(o===\"auto\"&&s===\"auto\"){var u=a.getPageSize().page.width;var p=parseInt(a.css(document.body,\"width\"));if(u>p){o=(u-p)/2}else{o=0}}o=w?parseInt(o):0;var n=w?parseInt(a.css(document.body,\"marginTop\")):0;a.css(l,{fontSize:v});a.css(l,{fontFamily:r});a.css(f,{left:(t.x-o)+\"px\",top:(t.y-n)+\"px\",position:\"absolute\"});d.tipsTarget=i}else{d.tipsTarget=null}},200)}},initTipsCtrlElem:function(){var j=document.createElement(\"span\"),g=document.createElement(\"span\"),h=document.createElement(\"a\"),i=document.createElement(\"span\"),k=document.createElement(\"a\"),d=document.createElement(\"span\"),l=this;d.className=\"OUTFOX_JTR_NANCI_CTRL_WORD\";j.appendChild(d);h.className=\"OUTFOX_JTR_NANCI_CTRL_DETAIL\";h.setAttribute(\"title\",\"查看详细解释\");h.innerHTML=\"详解\";g.appendChild(h);g.className=\"OUTFOX_JTR_NANCI_CTRL_DETAIL_BG\";j.appendChild(g);k.className=\"OUTFOX_JTR_NANCI_CTRL_CLOSE\";k.setAttribute(\"title\",\"我知道了\");k.innerHTML=\"关闭\";i.appendChild(k);i.className=\"OUTFOX_JTR_NANCI_CTRL_CLOSE_BG\";j.appendChild(i);j.className=\"OUTFOX_JTR_NANCI_BAR\";var f=function(){var m=null;try{m=l.tipsTarget.previousSibling}catch(n){}return m};var e=function(o){var m=null;if(document.createRange){var n=window.getSelection();m=document.createRange();n.removeAllRanges();m.selectNode(o);n.addRange(m)}else{if(document.body.createTextRange){m=document.body.createTextRange();m.moveToElementText(o);m.select()}}};a.bind(h,\"click\",function(o){var q=f();if(q&&l.swipe){l.log({action:\"viewDetail\",word:q.innerHTML});e(q);var p=document.createElement(\"font\");p.innerHTML=\"&nbsp\";q.appendChild(p);var n=a.findPos(p),m=p.offsetHeight;q.removeChild(p);l.swipe.swipeWord(q.firstChild.nodeValue,n.x,n.y,0,m)}j.parentNode.removeChild(j);a.removeClass(l.tipsTarget,\"on\")});a.bind(k,\"click\",function(m){var n=j.getAttribute(\"rel\"),o=f();if(o){l.log({action:\"closeTip\",word:o.innerHTML,tip:l.tipsTarget.innerHTML});l.translator._manager.revertTip(o)}j.parentNode.removeChild(j);a.removeClass(l.tipsTarget,\"on\")});return j},_findTipCtrlPosition:function(e,t){var u=e.firstChild,n=u.nodeValue,p=u.nodeValue.length,k=document.createElement(\"font\"),f=document.createTextNode(\"\"),r=[0,0],j=null,h=null,q=null,o=0,m=0,d=0,s=p,g=null;a.css(k,{border:\"none\",padding:0,margin:0});for(var l=0;l<=p;l++){f.nodeValue=u.nodeValue.substr(l);u.nodeValue=u.nodeValue.substr(0,l);e.insertBefore(f,u.nextSibling);e.insertBefore(k,u.nextSibling);q=a.findPos(k);o=q.x-t[0];m=q.y-t[1];if(m<=0&&(h===null||m>h)){g=h=m;r[0]=q.x;r[1]=q.y;j=null;d=l}if(m!==g){s=l;g=m}u.nodeValue=n;e.removeChild(f);e.removeChild(k)}k.innerHTML=u.nodeValue.substr(d,s);u.nodeValue=u.nodeValue.substr(0,d);e.insertBefore(k,u.nextSibling);r[0]+=k.offsetWidth;e.removeChild(k);u.nodeValue=n;return r},preventClose:function(){a.css(a.query(\"#OUTFOX_JTR_BAR_CLOSE\",this.frame),{display:\"none\"})},enableTipsCtrl:function(){a.bind(this.context,\"mouseover\",this.tipsCtrlHandler)},disableTipsCtrl:function(){a.unbind(this.context,\"mouseover\",this.tipsCtrlHandler)},enableTransTip:function(){a.bind(this.context,\"mouseover\",this.transTipHandler)},disableTransTip:function(){a.unbind(this.context,\"mouseover\",this.transTipHandler)},initTransTip:function(d){var f=this,g=null,e=null;this.initTransTipElem(\"\",\"\");this.transTipHandler=function(j){var h=j||window.event,i=h.target||h.srcElement;clearTimeout(e);e=setTimeout(function(){if(g===i||i.className&&(i.className.indexOf(\"OUTFOX_JTR_TRANSTIP_\")!==-1||i.className.indexOf(\"ydd-\")!==-1)){return}if(g){g.style.textDecoration=\"none\"}if(f.transTipElem.elem.parentNode){f.transTipElem.elem.parentNode.removeChild(f.transTipElem.elem)}if(a.hasClass(i,\"OUTFOX_JTR_TRANS_NODE\")){g=i;var k=i.getAttribute(\"rel\"),l=f.translator._manager;f.resetTransTipElem(l.originals[k],l.transResults[k],k);f.context.appendChild(f.transTipElem.elem);f.positionElementInViewPort({target:i,tip:f.transTipElem.elem});i.style.textDecoration=\"underline\"}else{g=null}},200)}},initTransTipElem:function(){var k=this,g=document.createElement(\"div\");g.className=\"OUTFOX_JTR_TRANSTIP_WRAPPER\";var m=document.createTextNode(\"\");var e=document.createTextNode(\"\");g.innerHTML+='<div class=\"OUTFOX_JTR_TRANSTIP_ORIGIN\">                                        <div class=\"ydd-container\">                                          <div class=\"ydd-top-wrapper\">                                            <div class=\"ydd-top\">                                            </div>                                          </div>                                          <div class=\"ydd-body-wrapper\">                                            <div class=\"ydd-lb\"></div>                                            <div class=\"ydd-rb\"></div>                                            <div class=\"ydd-body\">                                              <div class=\"ydd-title\">                                                <strong>原文：</strong>                                              </div>                                              <div class=\"ydd-middle\">                                                <div class=\"OUTFOX_JTR_TRANSTIP_ORIGIN_TEXT\"></div>                                                <div class=\"OUTFOX_JTR_TRANSTIP_ADVISE\">                                                  <textarea class=\"OUTFOX_JTR_TRANSTIP_ADVISE_TEXT\"></textarea>                                                  <a class=\"OUTFOX_JTR_TRANSTIP_ADVISE_SUBMIT\" href=\"javascript:void(0);\">提交翻译建议</a>                                                </div>                                              </div>                                            </div>                                          </div>                                          <div class=\"ydd-bottom-wrapper\">                                            <div class=\"ydd-bottom\">                                              <a class=\"OUTFOX_JTR_TRANSTIP_ADVISE_TOGGLE\">更好的翻译建议</a>                                              <span class=\"OUTFOX_JTR_TRANSTIP_ADVISE_THANK\">感谢您为有道提供建议^_^</span>                                            </div>                                          </div>                                          <div class=\"ydd-bg-top\"></div>                                        </div>                                      </div>';a.query(\".OUTFOX_JTR_TRANSTIP_ORIGIN_TEXT\",g)[0].appendChild(e);a.query(\".OUTFOX_JTR_TRANSTIP_ADVISE_TEXT\",g)[0].appendChild(m);var f=a.query(\".OUTFOX_JTR_TRANSTIP_ADVISE_TOGGLE\",g)[0],i=a.query(\".OUTFOX_JTR_TRANSTIP_ADVISE_SUBMIT\",g)[0],l=a.query(\".OUTFOX_JTR_TRANSTIP_ADVISE\",g)[0],d=a.query(\".ydd-bottom\",g)[0],h=a.query(\".OUTFOX_JTR_TRANSTIP_ADVISE_TEXT\",g)[0];i.hideFocus=true;a.bind(f,\"click\",function(){a.toggleClass(l,\"expand\");a.css(f,{display:\"none\"})});var j=function(n){n=n.replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\");return n};a.bind(i,\"click\",function(){var o=h.value;if(o===\"\"){alert(\"翻译建议不能为空，请您输入内容后再次提交\");return}o=j(o);a.addClass(l,\"finish\");a.removeClass(l,\"expand\");a.addClass(d,\"OUTFOX_JTR_TRANSTIP_ADVISE_THANK_TIP\");var n=k.transTipElem.index;if(n&&k.manager.transResults[n]){k.manager.transResults[n]=o;k.manager.revertTrans(n);k.manager.replaceTrans(n)}});this.transTipElem={elem:g,transTextContainer:h,trans:m,original:e,toggle:f,submit:i,advise:l,bottom:d,index:null}},resetTransTipElem:function(d,f,e){a.removeClass(this.transTipElem.advise,\"finish\");a.removeClass(this.transTipElem.advise,\"expand\");a.removeClass(this.transTipElem.bottom,\"OUTFOX_JTR_TRANSTIP_ADVISE_THANK_TIP\");this.transTipElem.toggle.innerHTML=\"提交翻译建议\";a.css(this.transTipElem.toggle,{display:\"\"});this.transTipElem.transTextContainer.value=f;this.transTipElem.original.nodeValue=d;this.transTipElem.index=e}}})(J);(function(d){var f=\"http://fanyi.youdao.com/web2\";var g=\"http://fanyi.youdao.com\";var h=browser.runtime.getURL(\"assets/fanyi.youdao.2.0/conn.html\");var c=f+\"/index.do\";var a=g+\"/jtr\";var e=f+\"/rl.do\";var b=browser.runtime.getURL(\"assets/fanyi.youdao.2.0/all-packed.css\");d.loadCSS(document,b);window.OUTFOX_JavascriptTranslatoR=new d.TR.UI(document.body,{domain:g,update:false,updateTipMsg:\"修复链接错误\",updateDate:\"2013-12-24\",cssURL:b,tipsURL:c,transURL:a,logURL:e,connFilePath:h,reqSize:20})})(J);\n\n  return 1; // so that chrome.tabs.executeScript knows everything is fine\n})()\n"
  },
  {
    "path": "assets/google-page-trans.js",
    "content": ";(function () {\n  const $script = document.createElement('script');\n  $script.type = 'text/javascript';\n  $script.charset = 'UTF-8';\n  $script.text = `\n;(function (){\n  if (typeof google === 'undefined') {\n    window.google = {};\n  }\n\n  if (google.translate == null) {\n    google.translate = {};\n  }\n\n  if (google.translate._const == null) {\n    google.translate._const = {};\n  }\n\n  const c = google.translate._const;\n\n  c._cest = new Date();\n  c._cl = '';\n  c._cuc = 'googleTranslateElementInit';\n  c._cac = '';\n  c._cam = '';\n  c._ctkk = '423865.2095111048';\n  c._pas = (window.location.protocol === 'http:' ? 'http' : 'https') + '://';\n  c._pah = 'translate.googleapis.com';\n  const b = c._pas + c._pah;\n  c._pbi = b + '/translate_static/img/te_bk.gif';\n  c._pci = b + '/translate_static/img/te_ctrl3.gif';\n  c._pli = b + '/translate_static/img/loading.gif';\n  c._plla = c._pah + '/translate_a/l';\n  c._pmi = b + '/translate_static/img/mini_google.png';\n  c._ps = b + '/translate_static/css/translateelement.css';\n  c._puh = 'translate.google.com';\n\n  const $trans = document.createElement('div');\n  $trans.id = 'google_translate_element';\n\n  const $css = document.createElement('link');\n  $css.type = 'text/css';\n  $css.rel = 'stylesheet';\n  $css.charset = 'UTF-8';\n  $css.href = c._ps;\n\n  const $main = document.createElement('script');\n  $main.type = 'text/javascript';\n  $main.charset = 'UTF-8';\n  $main.src = b + '/translate_static/js/element/main.js';\n  $main.onerror = function () {\n    alert(\n      '无法在此网页加载谷歌网页翻译组件，可能是网络问题或者此网站禁止加载外部脚本。\\\\n' +\n      'Unable to load google page translation script. Could be network issue or script being blocked by the website.'\n    )\n  };\n\n  function googleTranslateElementInit (){\n    new google.translate.TranslateElement({\n      pageLanguage: 'auto'\n    },'google_translate_element');\n  }\n\n  window.googleTranslateElementInit = googleTranslateElementInit;\n\n  document.body.insertBefore($trans, document.body.firstChild);\n  ;(document.head || document.body).appendChild($css);\n  ;(document.head || document.body).appendChild($main);\n}());\n  `;\n\n  const $style = document.createElement('style');\n  $style.innerHTML = `\n    #google_translate_element {\n      position: relative !important;\n    }\n    #google_translate_element,\n    .goog-te-menu-frame,\n    .goog-te-banner-frame {\n      z-index: 2147483647 !important;\n    }\n  `;\n\n  ;(document.head || document.body).appendChild($script);\n  ;(document.head || document.body).appendChild($style);\n}());\n"
  },
  {
    "path": "assets/inject-dict-panel.js",
    "content": "const manifest = browser.runtime.getManifest()\nif (manifest.content_scripts) {\n  for (const script of manifest.content_scripts) {\n    if (script.js) {\n      for (const js of script.js) {\n        const $script = document.createElement('script')\n        $script.type = 'text/javascript'\n        $script.src = /^\\/|([a-z-]+:\\/\\/)/i.test(js) ? js : `/${js}`\n        document.body.appendChild($script)\n      }\n    }\n    if (script.css) {\n      for (const css of script.css) {\n        const $link = document.createElement('link')\n        $link.rel = 'stylesheet'\n        $link.href = /^\\/|([a-z-]+:\\/\\/)/i.test(css) ? css : `/${css}`\n        document.head.appendChild($link)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "assets/vimium-c-injector.js",
    "content": "\"use strict\"; // eslint-disable-line strict\n(function () {\n    /**\n     * Vimium C 生命周期回调函数，在 https://github.com/gdh1995/vimium-c/blob/master/content/injected_end.ts 中调用\n     *\n     * @argument {number} action_code 1: \"initing\", 2: \"complete\", 3: \"destroy\"\n     */\n    const lifetimeHandler = (action_code) => {\n        if (action_code === 2) {\n            // 初始化完成\n            const api = window.VApi;\n            const oldScroll = api.$;\n            if (typeof oldScroll === \"function\") {\n                /**\n                 * 接管滚动命令，用于全屏模式下立即翻页（忽略平滑滚动）\n                 * @returns { boolean | Promise<boolean> } 是否成功滚动（版本 1.92+）\n                 */\n                api.$ = function (element, _di, amount) {\n                    if (Math.abs(amount) < 0.1) { return oldScroll.apply(this, arguments); }\n                    const Presentation = \".pdfPresentationMode\"\n                    const fullscreenElement = document.fullscreenElement\n                    const topEl = fullscreenElement || document.documentElement\n                    if ((element === topEl || fullscreenElement && !topEl.contains(element))) {\n                        element = fullscreenElement && topEl.matches(Presentation)\n                            ? topEl\n                            : topEl.querySelector(Presentation);\n                    } else {\n                        element = element.closest(Presentation);\n                    }\n                    if (element) {\n                        const oldTop = element.scrollTop, oldPage = PDFViewerApplication.page;\n                        element.dispatchEvent(\n                            new WheelEvent(\"wheel\", {\n                                bubbles: true,\n                                cancelable: true,\n                                composed: true,\n                                deltaY: Math.sign(amount) * 120,\n                            })\n                        );\n                        return oldPage !== PDFViewerApplication.page || element.scrollTop !== oldTop;\n                    }\n                    // eslint-disable-next-line prefer-rest-params\n                    return oldScroll.apply(this, arguments);\n                };\n            }\n            /**\n             * 返回 PDF 文件的 URL，用于复制网页地址等命令\n             */\n            api.u = () => {\n                const file = new URLSearchParams(location.search).get(\"file\");\n                return file || location.href;\n            };\n        } else if (action_code === 3) {\n            // 停止\n            window.removeEventListener(\"vimiumMark\", onMark, true);\n        }\n    };\n\n    /**\n     * 设置或者获取“文档滚动位置”，在 https://github.com/gdh1995/vimium-c/blob/master/content/marks.ts#L10 中调用\n     *\n     * @argument {CustomEvent} event\n     */\n    const onMark = (event) => {\n        const channelElement = event.relatedTarget;\n        const box = channelElement && document.getElementById(\"viewerContainer\");\n        event.stopImmediatePropagation();\n        if (!box) {\n            return;\n        }\n        const str = channelElement.textContent;\n        if (str) {\n            // 对应命令 Marks.activate\n            const mark = str.split(\",\");\n            const position = [~~mark[0], ~~mark[1]];\n            if (position[0] > 0 || position[1] > 0) {\n                box.scrollTo(position[0], position[1]);\n                channelElement.textContent = \"\";\n                event.preventDefault();\n            }\n        } else {\n            // 对应命令 Marks.activateCreate\n            channelElement.textContent = [box.scrollLeft, box.scrollTop];\n        }\n    };\n\n    const IDOnFirefox = \"vimium-c@gdh1995.cn\";\n    const IDOnEdge = \"aibcglbfblnogfjhbcmmpobjhnomhcdo\";\n    const IDOnChrome = \"hfjbmagddngcpeloejdejnfgbamkjaeg\";\n\n    browser.storage.sync.get(\"vimiumExtensionInjector\").then((result) => {\n        let injector = result.vimiumExtensionInjector;\n        if (injector === \"nul\" || injector === \"/dev/null\") {\n            return;\n        }\n        const IsFirefox = location.origin.startsWith(\"moz\")\n        const IsEdge = !IsFirefox && /\\sEdg\\//.test(navigator.appVersion)\n        const useFixedInjector = !!injector\n        if (!injector) {\n            injector = IsFirefox ? IDOnFirefox : IsEdge ? IDOnEdge : IDOnChrome;\n        }\n        if (injector.includes(\"://\") && injector.includes(\"/\", injector.indexOf(\"://\") + 3)) {\n            inject(injector);\n            return;\n        }\n        let expectedExtId;\n        try {\n            if (injector.includes(\"://\")) {\n                expectedExtId = new URL(injector).hostname;\n            } else {\n                expectedExtId = injector;\n            }\n        } catch (_e) {\n            return;\n        }\n        let q = browser.runtime.sendMessage(expectedExtId, { handler: \"id\" });\n        let extIdInUse = expectedExtId;\n        if (!useFixedInjector && IsEdge) {\n            q = q.catch(() => {\n                extIdInUse = IDOnChrome\n                return browser.runtime.sendMessage(extIdInUse, { handler: \"id\" })\n            })\n        }\n        q.then((response) => {\n            if (!response || !response.injector || typeof response.injector !== \"string\") {\n                if (response === false) {\n                    console.log(\"Connection to the extension named %o was refused.\", expectedExtId);\n                }\n                return;\n            }\n            console.log(\n                `Successfully connected to ${extIdInUse}: %o (version ${response.version}).`,\n                response.name\n            );\n            inject(response.injector);\n        }, () => {});\n    });\n\n    const inject = (url) => {\n        const script = document.createElement(\"script\");\n        script.src = url;\n        script.async = true;\n        script.onload = () => {\n            // 在 https://github.com/gdh1995/vimium-c/blob/master/lib/injector.ts#L87 处定义\n            const injector = window.VimiumInjector;\n            if (injector) {\n                injector.cache\n                    ? lifetimeHandler(2, \"complete\")\n                    : (injector.callback = lifetimeHandler);\n                window.addEventListener(\"vimiumMark\", onMark, true);\n            }\n        };\n        document.head.appendChild(script);\n    };\n})();\n"
  },
  {
    "path": "commitlint.config.js",
    "content": "module.exports = {\n  extends: ['@commitlint/config-conventional']\n}\n"
  },
  {
    "path": "config/jest/cssTransform.js",
    "content": "'use strict'\n\n// This is a custom Jest transformer turning style imports into empty objects.\n// http://facebook.github.io/jest/docs/tutorial-webpack.html\n\nmodule.exports = {\n  process() {\n    return 'module.exports = {};'\n  },\n  getCacheKey() {\n    // The output is always the same.\n    return 'cssTransform'\n  },\n}\n"
  },
  {
    "path": "config/jest/fileTransform.js",
    "content": "'use strict'\n\nconst path = require('path')\n\n// This is a custom Jest transformer turning file imports into filenames.\n// http://facebook.github.io/jest/docs/tutorial-webpack.html\n\nmodule.exports = {\n  process(src, filename) {\n    return `module.exports = ${JSON.stringify(path.basename(filename))};`\n  },\n}\n"
  },
  {
    "path": "config/jest/setupTests.js",
    "content": "import browser from 'sinon-chrome/extensions'\n// import Enzyme from 'enzyme'\n// import Adapter from 'enzyme-adapter-react-16'\nimport raf from 'raf'\nimport fetch from 'node-fetch'\nimport axios from 'axios'\n\n// force http in jsdom\naxios.defaults.adapter = require('axios/lib/adapters/http')\n\nwindow.browser = browser\nwindow.Request = fetch.Request\nwindow.Response = fetch.Response\nwindow.Headers = fetch.Headers\n\nif (process.env.CI) {\n  window.FormData = require('form-data')\n  window.fetch = fetch\n\n  jest.setTimeout(30000)\n}\n\n// Enzyme.configure({ adapter: new Adapter() })\n\n// In tests, polyfill requestAnimationFrame since jsdom doesn't provide it yet.\n// We don't polyfill it in the browser--this is user's responsibility.\nraf.polyfill(global)\n"
  },
  {
    "path": "jest.config.js",
    "content": "const neutrino = require('neutrino')\n\nprocess.env.NODE_ENV = process.env.NODE_ENV || 'test'\n\nmodule.exports = neutrino().jest()\n"
  },
  {
    "path": "jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \"./\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"]\n    }\n  }\n}\n"
  },
  {
    "path": "mac-app/Saladict - Pop-up Dictionary and Page Translator/Saladict - Pop-up Dictionary and Page Translator/AppDelegate.swift",
    "content": "//\n//  AppDelegate.swift\n//  Saladict - Pop-up Dictionary and Page Translator\n//\n//  Created by Jack Wong on 2021/5/31.\n//\n\nimport Cocoa\n\n@main\nclass AppDelegate: NSObject, NSApplicationDelegate {\n\n    func applicationDidFinishLaunching(_ notification: Notification) {\n        // Insert code here to initialize your application\n    }\n\n    func applicationWillTerminate(_ notification: Notification) {\n        // Insert code here to tear down your application\n    }\n\n    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {\n        return true\n    }\n\n}\n"
  },
  {
    "path": "mac-app/Saladict - Pop-up Dictionary and Page Translator/Saladict - Pop-up Dictionary and Page Translator/Assets.xcassets/AccentColor.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "mac-app/Saladict - Pop-up Dictionary and Page Translator/Saladict - Pop-up Dictionary and Page Translator/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"size\" : \"16x16\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"icon-16.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"16x16\"\n    },\n    {\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"32x32\"\n    },\n    {\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"32x32\"\n    },\n    {\n      \"size\" : \"128x128\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"icon-128.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"128x128\"\n    },\n    {\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"256x256\"\n    },\n    {\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"256x256\"\n    },\n    {\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"512x512\"\n    },\n    {\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"512x512\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "mac-app/Saladict - Pop-up Dictionary and Page Translator/Saladict - Pop-up Dictionary and Page Translator/Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "mac-app/Saladict - Pop-up Dictionary and Page Translator/Saladict - Pop-up Dictionary and Page Translator/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"16085\" targetRuntime=\"MacOSX.Cocoa\" propertyAccessControl=\"none\" useAutolayout=\"YES\" initialViewController=\"B8D-0N-5wS\">\n    <dependencies>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.CocoaPlugin\" version=\"16085\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <scenes>\n        <!--Application-->\n        <scene sceneID=\"JPo-4y-FX3\">\n            <objects>\n                <application id=\"hnw-xV-0zn\" sceneMemberID=\"viewController\">\n                    <menu key=\"mainMenu\" title=\"Main Menu\" systemMenu=\"main\" id=\"AYu-sK-qS6\">\n                        <items>\n                            <menuItem title=\"Saladict - Pop-up Dictionary and Page Translator\" id=\"1Xt-HY-uBw\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Saladict - Pop-up Dictionary and Page Translator\" systemMenu=\"apple\" id=\"uQy-DD-JDr\">\n                                    <items>\n                                        <menuItem title=\"About Saladict - Pop-up Dictionary and Page Translator\" id=\"5kV-Vb-QxS\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"orderFrontStandardAboutPanel:\" target=\"Ady-hI-5gd\" id=\"Exp-CZ-Vem\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"VOq-y0-SEH\"/>\n                                        <menuItem title=\"Hide Saladict - Pop-up Dictionary and Page Translator\" keyEquivalent=\"h\" id=\"Olw-nP-bQN\">\n                                            <connections>\n                                                <action selector=\"hide:\" target=\"Ady-hI-5gd\" id=\"PnN-Uc-m68\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Hide Others\" keyEquivalent=\"h\" id=\"Vdr-fp-XzO\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                            <connections>\n                                                <action selector=\"hideOtherApplications:\" target=\"Ady-hI-5gd\" id=\"VT4-aY-XCT\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Show All\" id=\"Kd2-mp-pUS\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"unhideAllApplications:\" target=\"Ady-hI-5gd\" id=\"Dhg-Le-xox\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"kCx-OE-vgT\"/>\n                                        <menuItem title=\"Quit Saladict - Pop-up Dictionary and Page Translator\" keyEquivalent=\"q\" id=\"4sb-4s-VLi\">\n                                            <connections>\n                                                <action selector=\"terminate:\" target=\"Ady-hI-5gd\" id=\"Te7-pn-YzF\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Help\" id=\"wpr-3q-Mcd\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Help\" systemMenu=\"help\" id=\"F2S-fz-NVQ\">\n                                    <items>\n                                        <menuItem title=\"Saladict - Pop-up Dictionary and Page Translator Help\" keyEquivalent=\"?\" id=\"FKE-Sm-Kum\">\n                                            <connections>\n                                                <action selector=\"showHelp:\" target=\"Ady-hI-5gd\" id=\"y7X-2Q-9no\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                        </items>\n                    </menu>\n                    <connections>\n                        <outlet property=\"delegate\" destination=\"Voe-Tx-rLC\" id=\"PrD-fu-P6m\"/>\n                    </connections>\n                </application>\n                <customObject id=\"Voe-Tx-rLC\" customClass=\"AppDelegate\" customModuleProvider=\"target\"/>\n                <customObject id=\"YLy-65-1bz\" customClass=\"NSFontManager\"/>\n                <customObject id=\"Ady-hI-5gd\" userLabel=\"First Responder\" customClass=\"NSResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"76\" y=\"-134\"/>\n        </scene>\n        <!--Window Controller-->\n        <scene sceneID=\"R2V-B0-nI4\">\n            <objects>\n                <windowController showSeguePresentationStyle=\"single\" id=\"B8D-0N-5wS\" sceneMemberID=\"viewController\">\n                    <window key=\"window\" title=\"Saladict - Pop-up Dictionary and Page Translator\" allowsToolTipsWhenApplicationIsInactive=\"NO\" autorecalculatesKeyViewLoop=\"NO\" restorable=\"NO\" releasedWhenClosed=\"NO\" animationBehavior=\"default\" titlebarAppearsTransparent=\"YES\" id=\"IQv-IB-iLA\">\n                        <windowStyleMask key=\"styleMask\" titled=\"YES\" closable=\"YES\"/>\n                        <windowCollectionBehavior key=\"collectionBehavior\" fullScreenNone=\"YES\"/>\n                        <rect key=\"contentRect\" x=\"196\" y=\"240\" width=\"480\" height=\"270\"/>\n                        <rect key=\"screenRect\" x=\"0.0\" y=\"0.0\" width=\"1680\" height=\"1027\"/>\n                        <connections>\n                            <outlet property=\"delegate\" destination=\"B8D-0N-5wS\" id=\"98r-iN-zZc\"/>\n                        </connections>\n                    </window>\n                    <connections>\n                        <segue destination=\"XfG-lQ-9wD\" kind=\"relationship\" relationship=\"window.shadowedContentViewController\" id=\"cq2-FE-JQM\"/>\n                    </connections>\n                </windowController>\n                <customObject id=\"Oky-zY-oP4\" userLabel=\"First Responder\" customClass=\"NSResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"75\" y=\"250\"/>\n        </scene>\n        <!--View Controller-->\n        <scene sceneID=\"hIz-AP-VOD\">\n            <objects>\n                <viewController id=\"XfG-lQ-9wD\" customClass=\"ViewController\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" id=\"m2S-Jp-Qdl\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"480\" height=\"344\"/>\n                        <autoresizingMask key=\"autoresizingMask\"/>\n                        <subviews>\n                            <stackView distribution=\"fill\" orientation=\"vertical\" alignment=\"centerX\" spacing=\"42\" horizontalStackHuggingPriority=\"249.99998474121094\" verticalStackHuggingPriority=\"249.99998474121094\" detachesHiddenViews=\"YES\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"ZLV-xE-AGT\">\n                                <rect key=\"frame\" x=\"0.0\" y=\"34\" width=\"480\" height=\"265\"/>\n                                <subviews>\n                                    <imageView horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"FWV-e2-WQh\" userLabel=\"IconView\">\n                                        <rect key=\"frame\" x=\"176\" y=\"137\" width=\"128\" height=\"128\"/>\n                                        <imageCell key=\"cell\" refusesFirstResponder=\"YES\" alignment=\"left\" image=\"AppIcon\" id=\"Hhb-TZ-Dhg\"/>\n                                    </imageView>\n                                    <textField horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"750\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"EB0-ac-UZR\">\n                                        <rect key=\"frame\" x=\"38\" y=\"63\" width=\"404\" height=\"32\"/>\n                                        <constraints>\n                                            <constraint firstAttribute=\"width\" relation=\"lessThanOrEqual\" constant=\"400\" id=\"pZE-0p-Ce8\"/>\n                                        </constraints>\n                                        <textFieldCell key=\"cell\" alignment=\"center\" title=\"App Name's extension is currently off. You can turn it on in Safari Extensions preferences.\" id=\"S7v-7o-3vW\">\n                                            <font key=\"font\" metaFont=\"system\"/>\n                                            <color key=\"textColor\" name=\"labelColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                            <color key=\"backgroundColor\" name=\"textBackgroundColor\" catalog=\"System\" colorSpace=\"catalog\"/>\n                                        </textFieldCell>\n                                    </textField>\n                                    <button verticalHuggingPriority=\"750\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"ooh-eV-eLQ\">\n                                        <rect key=\"frame\" x=\"79\" y=\"-7\" width=\"322\" height=\"32\"/>\n                                        <buttonCell key=\"cell\" type=\"push\" title=\"Quit and Open Safari Extensions Preferences…\" alternateTitle=\"Install\" bezelStyle=\"rounded\" alignment=\"center\" lineBreakMode=\"truncatingMiddle\" state=\"on\" borderStyle=\"border\" imageScaling=\"proportionallyDown\" inset=\"2\" id=\"Srx-0j-A4D\">\n                                            <behavior key=\"behavior\" pushIn=\"YES\" lightByBackground=\"YES\" lightByGray=\"YES\"/>\n                                            <font key=\"font\" metaFont=\"system\"/>\n                                            <string key=\"keyEquivalent\" base64-UTF8=\"YES\">\nDQ\n</string>\n                                            <connections>\n                                                <action selector=\"openSafariExtensionPreferences:\" target=\"XfG-lQ-9wD\" id=\"vKk-Xb-MPh\"/>\n                                            </connections>\n                                        </buttonCell>\n                                    </button>\n                                </subviews>\n                                <visibilityPriorities>\n                                    <integer value=\"1000\"/>\n                                    <integer value=\"1000\"/>\n                                    <integer value=\"1000\"/>\n                                </visibilityPriorities>\n                                <customSpacing>\n                                    <real value=\"3.4028234663852886e+38\"/>\n                                    <real value=\"3.4028234663852886e+38\"/>\n                                    <real value=\"3.4028234663852886e+38\"/>\n                                </customSpacing>\n                            </stackView>\n                        </subviews>\n                        <constraints>\n                            <constraint firstAttribute=\"trailing\" secondItem=\"ZLV-xE-AGT\" secondAttribute=\"trailing\" id=\"7aD-Ze-9ed\"/>\n                            <constraint firstItem=\"ZLV-xE-AGT\" firstAttribute=\"top\" secondItem=\"m2S-Jp-Qdl\" secondAttribute=\"top\" constant=\"45\" id=\"AJ3-sx-ZQx\"/>\n                            <constraint firstAttribute=\"bottom\" secondItem=\"ZLV-xE-AGT\" secondAttribute=\"bottom\" constant=\"34\" id=\"KVY-ss-lTJ\"/>\n                            <constraint firstItem=\"ZLV-xE-AGT\" firstAttribute=\"leading\" secondItem=\"m2S-Jp-Qdl\" secondAttribute=\"leading\" id=\"mT6-ee-vkp\"/>\n                        </constraints>\n                    </view>\n                    <connections>\n                        <outlet property=\"appNameLabel\" destination=\"EB0-ac-UZR\" id=\"SDO-j1-PQa\"/>\n                    </connections>\n                </viewController>\n                <customObject id=\"rPt-NT-nkU\" userLabel=\"First Responder\" customClass=\"NSResponder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"75\" y=\"655\"/>\n        </scene>\n    </scenes>\n    <resources>\n        <image name=\"AppIcon\" width=\"128\" height=\"128\"/>\n    </resources>\n</document>\n"
  },
  {
    "path": "mac-app/Saladict - Pop-up Dictionary and Page Translator/Saladict - Pop-up Dictionary and Page Translator/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIconFile</key>\n\t<string></string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>1.0</string>\n\t<key>CFBundleVersion</key>\n\t<string>1</string>\n\t<key>LSMinimumSystemVersion</key>\n\t<string>$(MACOSX_DEPLOYMENT_TARGET)</string>\n\t<key>NSMainStoryboardFile</key>\n\t<string>Main</string>\n\t<key>NSPrincipalClass</key>\n\t<string>NSApplication</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "mac-app/Saladict - Pop-up Dictionary and Page Translator/Saladict - Pop-up Dictionary and Page Translator/Saladict___Pop_up_Dictionary_and_Page_Translator.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>com.apple.security.app-sandbox</key>\n    <true/>\n    <key>com.apple.security.files.user-selected.read-only</key>\n    <true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "mac-app/Saladict - Pop-up Dictionary and Page Translator/Saladict - Pop-up Dictionary and Page Translator/ViewController.swift",
    "content": "//\n//  ViewController.swift\n//  Saladict - Pop-up Dictionary and Page Translator\n//\n//  Created by Jack Wong on 2021/5/31.\n//\n\nimport Cocoa\nimport SafariServices.SFSafariApplication\nimport SafariServices.SFSafariExtensionManager\n\nlet appName = \"Saladict - Pop-up Dictionary and Page Translator\"\nlet extensionBundleIdentifier = \"com.crimx.saladict.Saladict---Pop-up-Dictionary-and-Page-Translator.Extension\"\n\nclass ViewController: NSViewController {\n\n    @IBOutlet var appNameLabel: NSTextField!\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        self.appNameLabel.stringValue = appName\n        SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionBundleIdentifier) { (state, error) in\n            guard let state = state, error == nil else {\n                // Insert code to inform the user that something went wrong.\n                return\n            }\n\n            DispatchQueue.main.async {\n                if (state.isEnabled) {\n                    self.appNameLabel.stringValue = \"\\(appName)'s extension is currently on.\"\n                } else {\n                    self.appNameLabel.stringValue = \"\\(appName)'s extension is currently off. You can turn it on in Safari Extensions preferences.\"\n                }\n            }\n        }\n    }\n    \n    @IBAction func openSafariExtensionPreferences(_ sender: AnyObject?) {\n        SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionBundleIdentifier) { error in\n            guard error == nil else {\n                // Insert code to inform the user that something went wrong.\n                return\n            }\n\n            DispatchQueue.main.async {\n                NSApplication.shared.terminate(nil)\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "mac-app/Saladict - Pop-up Dictionary and Page Translator/Saladict - Pop-up Dictionary and Page Translator Extension/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleDisplayName</key>\n\t<string>Saladict - Pop-up Dictionary and Page Translator Extension</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>1.0</string>\n\t<key>CFBundleVersion</key>\n\t<string>1</string>\n\t<key>LSMinimumSystemVersion</key>\n\t<string>$(MACOSX_DEPLOYMENT_TARGET)</string>\n\t<key>NSExtension</key>\n\t<dict>\n\t\t<key>NSExtensionPointIdentifier</key>\n\t\t<string>com.apple.Safari.web-extension</string>\n\t\t<key>NSExtensionPrincipalClass</key>\n\t\t<string>$(PRODUCT_MODULE_NAME).SafariWebExtensionHandler</string>\n\t</dict>\n</dict>\n</plist>\n"
  },
  {
    "path": "mac-app/Saladict - Pop-up Dictionary and Page Translator/Saladict - Pop-up Dictionary and Page Translator Extension/SafariWebExtensionHandler.swift",
    "content": "//\n//  SafariWebExtensionHandler.swift\n//  Saladict - Pop-up Dictionary and Page Translator Extension\n//\n//  Created by Jack Wong on 2021/5/31.\n//\n\nimport SafariServices\nimport os.log\n\nlet SFExtensionMessageKey = \"message\"\n\nclass SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {\n\n\tfunc beginRequest(with context: NSExtensionContext) {\n        let item = context.inputItems[0] as! NSExtensionItem\n        let message = item.userInfo?[SFExtensionMessageKey]\n        os_log(.default, \"Received message from browser.runtime.sendNativeMessage: %@\", message as! CVarArg)\n\n        let response = NSExtensionItem()\n        response.userInfo = [ SFExtensionMessageKey: [ \"Response to\": message ] ]\n\n        context.completeRequest(returningItems: [response], completionHandler: nil)\n    }\n    \n}\n"
  },
  {
    "path": "mac-app/Saladict - Pop-up Dictionary and Page Translator/Saladict - Pop-up Dictionary and Page Translator Extension/Saladict___Pop_up_Dictionary_and_Page_Translator_Extension.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>com.apple.security.app-sandbox</key>\n    <true/>\n    <key>com.apple.security.files.user-selected.read-only</key>\n    <true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "mac-app/Saladict - Pop-up Dictionary and Page Translator/Saladict - Pop-up Dictionary and Page Translator.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 50;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t774E83BB266526B4006B5030 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774E83BA266526B4006B5030 /* AppDelegate.swift */; };\n\t\t774E83BE266526B4006B5030 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 774E83BC266526B4006B5030 /* Main.storyboard */; };\n\t\t774E83C0266526B4006B5030 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774E83BF266526B4006B5030 /* ViewController.swift */; };\n\t\t774E83C2266526B7006B5030 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 774E83C1266526B7006B5030 /* Assets.xcassets */; };\n\t\t774E83C9266526B7006B5030 /* Saladict - Pop-up Dictionary and Page Translator Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 774E83C8266526B7006B5030 /* Saladict - Pop-up Dictionary and Page Translator Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };\n\t\t774E83CE266526B7006B5030 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 774E83CD266526B7006B5030 /* Cocoa.framework */; };\n\t\t774E83D1266526B7006B5030 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774E83D0266526B7006B5030 /* SafariWebExtensionHandler.swift */; };\n\t\t774E83E8266526B8006B5030 /* popup.html in Resources */ = {isa = PBXBuildFile; fileRef = 774E83DE266526B8006B5030 /* popup.html */; };\n\t\t774E83E9266526B8006B5030 /* word-editor.html in Resources */ = {isa = PBXBuildFile; fileRef = 774E83DF266526B8006B5030 /* word-editor.html */; };\n\t\t774E83EA266526B8006B5030 /* notebook.html in Resources */ = {isa = PBXBuildFile; fileRef = 774E83E0266526B8006B5030 /* notebook.html */; };\n\t\t774E83EB266526B8006B5030 /* quick-search.html in Resources */ = {isa = PBXBuildFile; fileRef = 774E83E1266526B8006B5030 /* quick-search.html */; };\n\t\t774E83EC266526B8006B5030 /* history.html in Resources */ = {isa = PBXBuildFile; fileRef = 774E83E2266526B8006B5030 /* history.html */; };\n\t\t774E83ED266526B8006B5030 /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 774E83E3266526B8006B5030 /* manifest.json */; };\n\t\t774E83EE266526B8006B5030 /* options.html in Resources */ = {isa = PBXBuildFile; fileRef = 774E83E4266526B8006B5030 /* options.html */; };\n\t\t774E83EF266526B8006B5030 /* _locales in Resources */ = {isa = PBXBuildFile; fileRef = 774E83E5266526B8006B5030 /* _locales */; };\n\t\t774E83F0266526B8006B5030 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 774E83E6266526B8006B5030 /* assets */; };\n\t\t774E83F1266526B8006B5030 /* audio-control.html in Resources */ = {isa = PBXBuildFile; fileRef = 774E83E7266526B8006B5030 /* audio-control.html */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t774E83CA266526B7006B5030 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 774E83AE266526B4006B5030 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 774E83C7266526B7006B5030;\n\t\t\tremoteInfo = \"Saladict - Pop-up Dictionary and Page Translator Extension\";\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t774E83D9266526B7006B5030 /* Embed App Extensions */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 13;\n\t\t\tfiles = (\n\t\t\t\t774E83C9266526B7006B5030 /* Saladict - Pop-up Dictionary and Page Translator Extension.appex in Embed App Extensions */,\n\t\t\t);\n\t\t\tname = \"Embed App Extensions\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t774E83B6266526B4006B5030 /* Saladict - Pop-up Dictionary and Page Translator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = \"Saladict - Pop-up Dictionary and Page Translator.app\"; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t774E83B9266526B4006B5030 /* Saladict___Pop_up_Dictionary_and_Page_Translator.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = \"Saladict___Pop_up_Dictionary_and_Page_Translator.entitlements\"; sourceTree = \"<group>\"; };\n\t\t774E83BA266526B4006B5030 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t774E83BD266526B4006B5030 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\t774E83BF266526B4006B5030 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = \"<group>\"; };\n\t\t774E83C1266526B7006B5030 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t774E83C3266526B7006B5030 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t774E83C8266526B7006B5030 /* Saladict - Pop-up Dictionary and Page Translator Extension.appex */ = {isa = PBXFileReference; explicitFileType = \"wrapper.app-extension\"; includeInIndex = 0; path = \"Saladict - Pop-up Dictionary and Page Translator Extension.appex\"; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t774E83CD266526B7006B5030 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };\n\t\t774E83D0266526B7006B5030 /* SafariWebExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebExtensionHandler.swift; sourceTree = \"<group>\"; };\n\t\t774E83D2266526B7006B5030 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t774E83D3266526B7006B5030 /* Saladict___Pop_up_Dictionary_and_Page_Translator_Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = \"Saladict___Pop_up_Dictionary_and_Page_Translator_Extension.entitlements\"; sourceTree = \"<group>\"; };\n\t\t774E83DE266526B8006B5030 /* popup.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = popup.html; path = ../../../build/safari/popup.html; sourceTree = \"<group>\"; };\n\t\t774E83DF266526B8006B5030 /* word-editor.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = \"word-editor.html\"; path = \"../../../build/safari/word-editor.html\"; sourceTree = \"<group>\"; };\n\t\t774E83E0266526B8006B5030 /* notebook.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = notebook.html; path = ../../../build/safari/notebook.html; sourceTree = \"<group>\"; };\n\t\t774E83E1266526B8006B5030 /* quick-search.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = \"quick-search.html\"; path = \"../../../build/safari/quick-search.html\"; sourceTree = \"<group>\"; };\n\t\t774E83E2266526B8006B5030 /* history.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = history.html; path = ../../../build/safari/history.html; sourceTree = \"<group>\"; };\n\t\t774E83E3266526B8006B5030 /* manifest.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = manifest.json; path = ../../../build/safari/manifest.json; sourceTree = \"<group>\"; };\n\t\t774E83E4266526B8006B5030 /* options.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = options.html; path = ../../../build/safari/options.html; sourceTree = \"<group>\"; };\n\t\t774E83E5266526B8006B5030 /* _locales */ = {isa = PBXFileReference; lastKnownFileType = folder; name = _locales; path = ../../../build/safari/_locales; sourceTree = \"<group>\"; };\n\t\t774E83E6266526B8006B5030 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = ../../../build/safari/assets; sourceTree = \"<group>\"; };\n\t\t774E83E7266526B8006B5030 /* audio-control.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = \"audio-control.html\"; path = \"../../../build/safari/audio-control.html\"; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t774E83B3266526B4006B5030 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t774E83C5266526B7006B5030 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t774E83CE266526B7006B5030 /* Cocoa.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t774E83AD266526B4006B5030 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t774E83B8266526B4006B5030 /* Saladict - Pop-up Dictionary and Page Translator */,\n\t\t\t\t774E83CF266526B7006B5030 /* Saladict - Pop-up Dictionary and Page Translator Extension */,\n\t\t\t\t774E83CC266526B7006B5030 /* Frameworks */,\n\t\t\t\t774E83B7266526B4006B5030 /* Products */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t774E83B7266526B4006B5030 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t774E83B6266526B4006B5030 /* Saladict - Pop-up Dictionary and Page Translator.app */,\n\t\t\t\t774E83C8266526B7006B5030 /* Saladict - Pop-up Dictionary and Page Translator Extension.appex */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t774E83B8266526B4006B5030 /* Saladict - Pop-up Dictionary and Page Translator */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t774E83B9266526B4006B5030 /* Saladict___Pop_up_Dictionary_and_Page_Translator.entitlements */,\n\t\t\t\t774E83BA266526B4006B5030 /* AppDelegate.swift */,\n\t\t\t\t774E83BC266526B4006B5030 /* Main.storyboard */,\n\t\t\t\t774E83BF266526B4006B5030 /* ViewController.swift */,\n\t\t\t\t774E83C1266526B7006B5030 /* Assets.xcassets */,\n\t\t\t\t774E83C3266526B7006B5030 /* Info.plist */,\n\t\t\t);\n\t\t\tpath = \"Saladict - Pop-up Dictionary and Page Translator\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t774E83CC266526B7006B5030 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t774E83CD266526B7006B5030 /* Cocoa.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t774E83CF266526B7006B5030 /* Saladict - Pop-up Dictionary and Page Translator Extension */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t774E83DD266526B8006B5030 /* Resources */,\n\t\t\t\t774E83D0266526B7006B5030 /* SafariWebExtensionHandler.swift */,\n\t\t\t\t774E83D2266526B7006B5030 /* Info.plist */,\n\t\t\t\t774E83D3266526B7006B5030 /* Saladict___Pop_up_Dictionary_and_Page_Translator_Extension.entitlements */,\n\t\t\t);\n\t\t\tpath = \"Saladict - Pop-up Dictionary and Page Translator Extension\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t774E83DD266526B8006B5030 /* Resources */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t774E83DE266526B8006B5030 /* popup.html */,\n\t\t\t\t774E83DF266526B8006B5030 /* word-editor.html */,\n\t\t\t\t774E83E0266526B8006B5030 /* notebook.html */,\n\t\t\t\t774E83E1266526B8006B5030 /* quick-search.html */,\n\t\t\t\t774E83E2266526B8006B5030 /* history.html */,\n\t\t\t\t774E83E3266526B8006B5030 /* manifest.json */,\n\t\t\t\t774E83E4266526B8006B5030 /* options.html */,\n\t\t\t\t774E83E5266526B8006B5030 /* _locales */,\n\t\t\t\t774E83E6266526B8006B5030 /* assets */,\n\t\t\t\t774E83E7266526B8006B5030 /* audio-control.html */,\n\t\t\t);\n\t\t\tname = Resources;\n\t\t\tpath = \"Saladict - Pop-up Dictionary and Page Translator Extension\";\n\t\t\tsourceTree = SOURCE_ROOT;\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t774E83B5266526B4006B5030 /* Saladict - Pop-up Dictionary and Page Translator */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 774E83DA266526B7006B5030 /* Build configuration list for PBXNativeTarget \"Saladict - Pop-up Dictionary and Page Translator\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t774E83B2266526B4006B5030 /* Sources */,\n\t\t\t\t774E83B3266526B4006B5030 /* Frameworks */,\n\t\t\t\t774E83B4266526B4006B5030 /* Resources */,\n\t\t\t\t774E83D9266526B7006B5030 /* Embed App Extensions */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t774E83CB266526B7006B5030 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = \"Saladict - Pop-up Dictionary and Page Translator\";\n\t\t\tproductName = \"Saladict - Pop-up Dictionary and Page Translator\";\n\t\t\tproductReference = 774E83B6266526B4006B5030 /* Saladict - Pop-up Dictionary and Page Translator.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n\t\t774E83C7266526B7006B5030 /* Saladict - Pop-up Dictionary and Page Translator Extension */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 774E83D6266526B7006B5030 /* Build configuration list for PBXNativeTarget \"Saladict - Pop-up Dictionary and Page Translator Extension\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t774E83C4266526B7006B5030 /* Sources */,\n\t\t\t\t774E83C5266526B7006B5030 /* Frameworks */,\n\t\t\t\t774E83C6266526B7006B5030 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = \"Saladict - Pop-up Dictionary and Page Translator Extension\";\n\t\t\tproductName = \"Saladict - Pop-up Dictionary and Page Translator Extension\";\n\t\t\tproductReference = 774E83C8266526B7006B5030 /* Saladict - Pop-up Dictionary and Page Translator Extension.appex */;\n\t\t\tproductType = \"com.apple.product-type.app-extension\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t774E83AE266526B4006B5030 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastSwiftUpdateCheck = 1240;\n\t\t\t\tLastUpgradeCheck = 1240;\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t774E83B5266526B4006B5030 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 12.4;\n\t\t\t\t\t};\n\t\t\t\t\t774E83C7266526B7006B5030 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 12.4;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 774E83B1266526B4006B5030 /* Build configuration list for PBXProject \"Saladict - Pop-up Dictionary and Page Translator\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 774E83AD266526B4006B5030;\n\t\t\tproductRefGroup = 774E83B7266526B4006B5030 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t774E83B5266526B4006B5030 /* Saladict - Pop-up Dictionary and Page Translator */,\n\t\t\t\t774E83C7266526B7006B5030 /* Saladict - Pop-up Dictionary and Page Translator Extension */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t774E83B4266526B4006B5030 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t774E83C2266526B7006B5030 /* Assets.xcassets in Resources */,\n\t\t\t\t774E83BE266526B4006B5030 /* Main.storyboard in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t774E83C6266526B7006B5030 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t774E83EE266526B8006B5030 /* options.html in Resources */,\n\t\t\t\t774E83EB266526B8006B5030 /* quick-search.html in Resources */,\n\t\t\t\t774E83EF266526B8006B5030 /* _locales in Resources */,\n\t\t\t\t774E83F0266526B8006B5030 /* assets in Resources */,\n\t\t\t\t774E83EC266526B8006B5030 /* history.html in Resources */,\n\t\t\t\t774E83F1266526B8006B5030 /* audio-control.html in Resources */,\n\t\t\t\t774E83E8266526B8006B5030 /* popup.html in Resources */,\n\t\t\t\t774E83E9266526B8006B5030 /* word-editor.html in Resources */,\n\t\t\t\t774E83ED266526B8006B5030 /* manifest.json in Resources */,\n\t\t\t\t774E83EA266526B8006B5030 /* notebook.html in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t774E83B2266526B4006B5030 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t774E83C0266526B4006B5030 /* ViewController.swift in Sources */,\n\t\t\t\t774E83BB266526B4006B5030 /* AppDelegate.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t774E83C4266526B7006B5030 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t774E83D1266526B7006B5030 /* SafariWebExtensionHandler.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t774E83CB266526B7006B5030 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 774E83C7266526B7006B5030 /* Saladict - Pop-up Dictionary and Page Translator Extension */;\n\t\t\ttargetProxy = 774E83CA266526B7006B5030 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t774E83BC266526B4006B5030 /* Main.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t774E83BD266526B4006B5030 /* Base */,\n\t\t\t);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t774E83D4266526B7006B5030 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.15;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t774E83D5266526B7006B5030 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.15;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t774E83D7266526B7006B5030 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = \"Saladict - Pop-up Dictionary and Page Translator Extension/Saladict___Pop_up_Dictionary_and_Page_Translator_Extension.entitlements\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tINFOPLIST_FILE = \"Saladict - Pop-up Dictionary and Page Translator Extension/Info.plist\";\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t\t\"@executable_path/../../../../Frameworks\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.crimx.saladict.Saladict---Pop-up-Dictionary-and-Page-Translator.Extension\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t774E83D8266526B7006B5030 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = \"Saladict - Pop-up Dictionary and Page Translator Extension/Saladict___Pop_up_Dictionary_and_Page_Translator_Extension.entitlements\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tINFOPLIST_FILE = \"Saladict - Pop-up Dictionary and Page Translator Extension/Info.plist\";\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t\t\"@executable_path/../../../../Frameworks\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.crimx.saladict.Saladict---Pop-up-Dictionary-and-Page-Translator.Extension\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t774E83DB266526B7006B5030 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = \"Saladict - Pop-up Dictionary and Page Translator/Saladict___Pop_up_Dictionary_and_Page_Translator.entitlements\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = \"Saladict - Pop-up Dictionary and Page Translator/Info.plist\";\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.crimx.saladict.Saladict---Pop-up-Dictionary-and-Page-Translator\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t774E83DC266526B7006B5030 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = \"Saladict - Pop-up Dictionary and Page Translator/Saladict___Pop_up_Dictionary_and_Page_Translator.entitlements\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = \"Saladict - Pop-up Dictionary and Page Translator/Info.plist\";\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.crimx.saladict.Saladict---Pop-up-Dictionary-and-Page-Translator\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t774E83B1266526B4006B5030 /* Build configuration list for PBXProject \"Saladict - Pop-up Dictionary and Page Translator\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t774E83D4266526B7006B5030 /* Debug */,\n\t\t\t\t774E83D5266526B7006B5030 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t774E83D6266526B7006B5030 /* Build configuration list for PBXNativeTarget \"Saladict - Pop-up Dictionary and Page Translator Extension\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t774E83D7266526B7006B5030 /* Debug */,\n\t\t\t\t774E83D8266526B7006B5030 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t774E83DA266526B7006B5030 /* Build configuration list for PBXNativeTarget \"Saladict - Pop-up Dictionary and Page Translator\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t774E83DB266526B7006B5030 /* Debug */,\n\t\t\t\t774E83DC266526B7006B5030 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 774E83AE266526B4006B5030 /* Project object */;\n}\n"
  },
  {
    "path": "mac-app/Saladict - Pop-up Dictionary and Page Translator/Saladict - Pop-up Dictionary and Page Translator.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "mac-app/Saladict - Pop-up Dictionary and Page Translator/Saladict - Pop-up Dictionary and Page Translator.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "mac-app/Saladict - Pop-up Dictionary and Page Translator/Saladict - Pop-up Dictionary and Page Translator.xcodeproj/xcuserdata/crimx.xcuserdatad/xcschemes/xcschememanagement.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>SchemeUserState</key>\n\t<dict>\n\t\t<key>Saladict - Pop-up Dictionary and Page Translator.xcscheme_^#shared#^_</key>\n\t\t<dict>\n\t\t\t<key>orderHint</key>\n\t\t\t<integer>0</integer>\n\t\t</dict>\n\t</dict>\n</dict>\n</plist>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"saladict\",\n  \"version\": \"7.20.0\",\n  \"description\": \"Chrome extension and Firefox WebExtension; inline translator powered by multiple online dictionaries\",\n  \"private\": true,\n  \"scripts\": {\n    \"start\": \"webpack-dev-server --mode development --open --debug\",\n    \"storybook\": \"start-storybook\",\n    \"build\": \"webpack --mode production\",\n    \"postbuild\": \"node scripts/firefox-fix.js\",\n    \"devbuild\": \"webpack --mode development\",\n    \"type-check\": \"tsc --noEmit\",\n    \"test\": \"jest --testTimeout 20000\",\n    \"test:watch\": \"jest --watch\",\n    \"zip\": \"yarn neutrino-webextension-zip '!test' '!.git' '.env' 'assets/pdf' 'deps'\",\n    \"postinstall\": \"node scripts/setup-env.js\",\n    \"commit\": \"git-cz\",\n    \"release\": \"standard-version\",\n    \"fixtures\": \"node scripts/fixtures.js\",\n    \"lint\": \"eslint src/**/*.{js,ts,tsx}\",\n    \"pdf\": \"node scripts/pdf.js\"\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"commit-msg\": \"commitlint -E HUSKY_GIT_PARAMS\"\n    }\n  },\n  \"commitlint\": {\n    \"extends\": [\n      \"@commitlint/config-conventional\"\n    ]\n  },\n  \"engines\": {\n    \"node\": \">= 11.0.0\",\n    \"npm\": \">= 6.9.0\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/crimx/ext-saladict\"\n  },\n  \"homepage\": \"https://github.com/crimx/ext-saladict\",\n  \"license\": \"MIT\",\n  \"readmeFilename\": \"README.md\",\n  \"bugs\": {\n    \"url\": \"https://github.com/crimx/ext-saladict/issues\"\n  },\n  \"config\": {\n    \"commitizen\": {\n      \"path\": \"cz-conventional-changelog\"\n    }\n  },\n  \"dependencies\": {\n    \"@ant-design/icons\": \"^4.0.5\",\n    \"@opentranslate/baidu\": \"^1.4.0\",\n    \"@opentranslate/caiyun\": \"^1.4.0\",\n    \"@opentranslate/google\": \"^1.4.0\",\n    \"@opentranslate/languages\": \"^1.4.0\",\n    \"@opentranslate/sogou\": \"^1.4.0\",\n    \"@opentranslate/tencent\": \"^1.4.0\",\n    \"@opentranslate/translator\": \"^1.4.0\",\n    \"@opentranslate/youdao\": \"^1.4.0\",\n    \"@types/classnames\": \"^2.2.10\",\n    \"@types/dompurify\": \"0.0.32\",\n    \"@types/firefox-webext-browser\": \"^67.0.2\",\n    \"@types/i18next\": \"^12.1.0\",\n    \"@types/lodash\": \"^4.14.136\",\n    \"@types/react\": \"^16.8.23\",\n    \"@types/react-beautiful-dnd\": \"^13.0.0\",\n    \"@types/react-dom\": \"^16.8.4\",\n    \"@types/react-helmet\": \"^5.0.15\",\n    \"@types/react-redux\": \"^7.1.2\",\n    \"@types/react-resize-detector\": \"^4.0.2\",\n    \"@types/react-textarea-autosize\": \"^4.3.4\",\n    \"@types/react-transition-group\": \"^4.2.0\",\n    \"@types/shallowequal\": \"^1.1.1\",\n    \"@types/uuid\": \"^3.4.6\",\n    \"@types/wavesurfer.js\": \"2.0.2\",\n    \"antd\": \"^4.1.0\",\n    \"axios\": \"^0.21.1\",\n    \"classnames\": \"^2.2.6\",\n    \"dexie\": \"^2.0.4\",\n    \"dompurify\": \"^2.0.17\",\n    \"get-selection-more\": \"^1.0.2\",\n    \"i18next\": \"^17.0.6\",\n    \"lodash\": \"^4.17.21\",\n    \"md5\": \"^2.2.1\",\n    \"memoize-one\": \"^5.1.0\",\n    \"normalize-scss\": \"^7.0.1\",\n    \"observable-hooks\": \"^3.0.0\",\n    \"pako\": \"^1.0.10\",\n    \"qrcode.react\": \"^0.9.3\",\n    \"react\": \"^16.10.0\",\n    \"react-beautiful-dnd\": \"^13.0.0\",\n    \"react-dom\": \"^16.10.0\",\n    \"react-helmet\": \"^6.0.0-beta.2\",\n    \"react-hot-loader\": \"^4\",\n    \"react-number-editor\": \"^4.0.3\",\n    \"react-redux\": \"^7.1.0\",\n    \"react-resize-reporter\": \"^1.0.2\",\n    \"react-retux\": \"^0.1.0\",\n    \"react-shadow\": \"17.1.x\",\n    \"react-textarea-autosize\": \"^7.1.0\",\n    \"react-transition-group\": \"^4.2.1\",\n    \"react-use\": \"^11.3.2\",\n    \"redux\": \"^4.0.4\",\n    \"redux-observable\": \"^1.1.0\",\n    \"redux-thunk\": \"^2.3.0\",\n    \"retux\": \"^0.1.0\",\n    \"rxjs\": \"^6.5.2\",\n    \"shallowequal\": \"^1.1.0\",\n    \"soundtouchjs\": \"^0.1.24\",\n    \"utility-types\": \"^3.7.0\",\n    \"uuid\": \"^3.4.0\",\n    \"wavesurfer.js\": \"3.0.0\",\n    \"webextension-polyfill\": \"^0.6.0\"\n  },\n  \"devDependencies\": {\n    \"@babel/preset-typescript\": \"^7.8.3\",\n    \"@commitlint/cli\": \"^9.1.2\",\n    \"@commitlint/config-conventional\": \"^9.1.2\",\n    \"@neutrinojs/copy\": \"^9.1.0\",\n    \"@neutrinojs/jest\": \"^9.1.0\",\n    \"@neutrinojs/react\": \"^9.1.0\",\n    \"@storybook/addon-actions\": \"5.1.11\",\n    \"@storybook/addon-backgrounds\": \"5.1.11\",\n    \"@storybook/addon-contexts\": \"5.1.11\",\n    \"@storybook/addon-knobs\": \"5.1.11\",\n    \"@storybook/react\": \"5.1.11\",\n    \"@types/faker\": \"^4.1.5\",\n    \"@types/jest\": \"^24.0.18\",\n    \"@types/md5\": \"^2.1.33\",\n    \"@types/qs\": \"^6.9.0\",\n    \"@types/sinon-chrome\": \"^2.2.6\",\n    \"@types/storybook__react\": \"4.0.2\",\n    \"@typescript-eslint/eslint-plugin\": \"^2.20.0\",\n    \"@typescript-eslint/parser\": \"^2.20.0\",\n    \"autoprefixer\": \"^9.6.1\",\n    \"axios-mock-adapter\": \"^1.17.0\",\n    \"babel-plugin-import\": \"^1.12.1\",\n    \"clean-css-loader\": \"^2.0.0\",\n    \"cli-progress\": \"^3.9.0\",\n    \"commitizen\": \"^4.2.1\",\n    \"cz-conventional-changelog\": \"^3.3.0\",\n    \"dotenv\": \"^8.2.0\",\n    \"eslint\": \"^6.8.0\",\n    \"eslint-config-prettier\": \"^6.10.0\",\n    \"eslint-config-standard\": \"^14.1.0\",\n    \"eslint-plugin-import\": \"^2.20.1\",\n    \"eslint-plugin-jest\": \"^23.7.0\",\n    \"eslint-plugin-node\": \"^11.0.0\",\n    \"eslint-plugin-prettier\": \"^3.1.2\",\n    \"eslint-plugin-promise\": \"^4.2.1\",\n    \"eslint-plugin-react\": \"^7.18.3\",\n    \"eslint-plugin-standard\": \"^4.0.1\",\n    \"faker\": \"^4.1.0\",\n    \"fast-glob\": \"^3.1.1\",\n    \"form-data\": \"^2.5.1\",\n    \"franc-min\": \"^4.1.1\",\n    \"husky\": \"^4.3.0\",\n    \"jest\": \"^26.1.0\",\n    \"jest-fetch-mock\": \"^2.1.2\",\n    \"mini-svg-data-uri\": \"^1.1.3\",\n    \"moment-locales-webpack-plugin\": \"^1.1.0\",\n    \"neutrino\": \"^9.1.0\",\n    \"neutrino-webextension\": \"^1.2.1\",\n    \"node-fetch\": \"^3.1.1\",\n    \"postcss-loader\": \"^3.0.0\",\n    \"prettier\": \"^1.19.1\",\n    \"qs\": \"^6.9.1\",\n    \"raf\": \"^3.4.1\",\n    \"random-mua\": \"^0.5.0\",\n    \"raw-loader\": \"^3.1.0\",\n    \"react-docgen-typescript-loader\": \"^3.1.0\",\n    \"sass\": \"^1.34.0\",\n    \"sass-loader\": \"^7.1.0\",\n    \"sass-resources-loader\": \"^2.0.3\",\n    \"socks-proxy-agent\": \"^5.0.0\",\n    \"standard-version\": \"^9.0.0\",\n    \"storybook-addon-jsx\": \"7.1.15\",\n    \"storybook-addon-react-docgen\": \"1.2.32\",\n    \"to-string-loader\": \"^1.1.5\",\n    \"trsjs\": \"caiyunapp/trs.js\",\n    \"typescript\": \"^3.8.2\",\n    \"webpack\": \"^4\",\n    \"webpack-bundle-analyzer\": \"^3.5.2\",\n    \"webpack-cli\": \"^3\",\n    \"webpack-dev-server\": \"^3\",\n    \"yargs\": \"^14.0.0\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = () => ({\n  plugins: [\n    require('postcss-flexbugs-fixes'),\n    require('autoprefixer')({\n      browsers: ['Chrome >= 55', 'Firefox >= 56']\n    })\n  ]\n})\n"
  },
  {
    "path": "scripts/after-build.js",
    "content": "const fs = require('fs-extra')\nconst path = require('path')\n\nmodule.exports = class AfterBuildPlugin {\n  apply(compiler) {\n    compiler.hooks.done.tapAsync(\n      'AfterBuildPlugin',\n      (compilation, callback) => {\n        firefoxFix().then(callback)\n      }\n    )\n  }\n}\n\nasync function firefoxFix() {\n  await removeYoudaoFanyi()\n  await removeCaiyun()\n}\n\nasync function removeYoudaoFanyi() {\n  // FF policy\n  await fs.remove(\n    path.join(__dirname, '../build/firefox/assets/fanyi.youdao.2.0')\n  )\n  // Stop FF extension check errors\n  await fs.outputFile(\n    path.join(__dirname, '../build/firefox/assets/fanyi.youdao.2.0/main.js'),\n    ''\n  )\n}\n\nasync function removeCaiyun() {\n  // FF policy\n  // caiyun trs is close-sourced\n  await fs.remove(path.join(__dirname, '../build/firefox/assets/trs.js'))\n}\n"
  },
  {
    "path": "scripts/build.js",
    "content": "'use strict'\n\n// Do this as the first thing so that any code reading it knows the right env.\nprocess.env.BABEL_ENV = 'production'\nprocess.env.NODE_ENV = 'production'\nprocess.env.PUBLIC_URL = './'\n\nconst argv = require('minimist')(process.argv.slice(2))\nif (argv.debug) { process.env.DEBUG_MODE = true }\nif (argv.devbuild) { process.env.DEV_BUILD = true }\n\n// Makes the script crash on unhandled rejections instead of silently\n// ignoring them. In the future, promise rejections that are not handled will\n// terminate the Node.js process with a non-zero exit code.\nprocess.on('unhandledRejection', err => {\n  throw err\n})\n\n// Browser targets\nconst browsers = ['chrome', 'firefox']\n\n// Ensure environment variables are read.\nrequire('../config/env')\n\nconst path = require('path')\nconst chalk = require('chalk')\nconst fs = require('fs-extra')\nconst webpack = require('webpack')\nconst config = require('../config/webpack.config.prod')\nconst paths = require('../config/paths')\n// const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles')\nconst formatWebpackMessages = require('react-dev-utils/formatWebpackMessages')\nconst FileSizeReporter = require('react-dev-utils/FileSizeReporter')\nconst printBuildError = require('react-dev-utils/printBuildError')\n// const semver = require('semver')\nvar postcss = require('postcss')\nvar increaseSpecificity = require('postcss-increase-specificity')\n\nconst measureFileSizesBeforeBuild =\n  FileSizeReporter.measureFileSizesBeforeBuild\nconst printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild\n// const useYarn = fs.existsSync(paths.yarnLockFile)\n\n// These sizes are pretty large. We'll warn for bundles exceeding them.\nconst WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024\nconst WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024\n\n// First, read the current file sizes in build directory.\n// This lets us display how much they changed later.\nmeasureFileSizesBeforeBuild(paths.appBuild)\n  .then(previousFileSizes => {\n    // Remove all content but keep the directory so that\n    // if you're in it, you don't end up in Trash\n    fs.emptyDirSync(paths.appBuild)\n    // Start the webpack build\n    return build(previousFileSizes)\n  })\n  .then(\n    ({ stats, previousFileSizes, warnings }) => {\n      if (warnings.length) {\n        console.log(chalk.yellow('Compiled with warnings.\\n'))\n        console.log(warnings.join('\\n\\n'))\n        console.log(\n          '\\nSearch for the ' +\n            chalk.underline(chalk.yellow('keywords')) +\n            ' to learn more about each warning.'\n        )\n        console.log(\n          'To ignore, add ' +\n            chalk.cyan('// eslint-disable-next-line') +\n            ' to the line before.\\n'\n        )\n      } else {\n        console.log(chalk.green('Compiled successfully.\\n'))\n      }\n\n      console.log('File sizes after gzip:\\n')\n      printFileSizesAfterBuild(\n        stats,\n        previousFileSizes,\n        // only show the basename\n        '[browser]/', // paths.appBuild,\n        WARN_AFTER_BUNDLE_GZIP_SIZE,\n        WARN_AFTER_CHUNK_GZIP_SIZE\n      )\n      console.log()\n    },\n    err => {\n      console.log(chalk.red('Failed to compile.\\n'))\n      printBuildError(err)\n      process.exit(1)\n    }\n  )\n  .then(patchInternalCSS)\n  .then(generateByBrowser)\n\n// Create the production build and print the deployment instructions.\nfunction build (previousFileSizes) {\n  console.log('Creating an optimized production build...')\n\n  let compiler = webpack(config)\n  return new Promise((resolve, reject) => {\n    compiler.run((err, stats) => {\n      if (err) {\n        return reject(err)\n      }\n      const messages = formatWebpackMessages(stats.toJson({}, true))\n      if (messages.errors.length) {\n        // Only keep the first error. Others are often indicative\n        // of the same problem, but confuse the reader with noise.\n        if (messages.errors.length > 1) {\n          messages.errors.length = 1\n        }\n        return reject(new Error(messages.errors.join('\\n\\n')))\n      }\n      if (\n        process.env.CI &&\n        (typeof process.env.CI !== 'string' ||\n          process.env.CI.toLowerCase() !== 'false') &&\n        messages.warnings.length\n      ) {\n        console.log(\n          chalk.yellow(\n            '\\nTreating warnings as errors because process.env.CI = true.\\n' +\n              'Most CI servers set it automatically.\\n'\n          )\n        )\n        return reject(new Error(messages.warnings.join('\\n\\n')))\n      }\n      return resolve({\n        stats,\n        previousFileSizes,\n        warnings: messages.warnings,\n      })\n    })\n  })\n}\n\n// Generate results for all browsers\nfunction generateByBrowser () {\n  console.log('\\nGenerating files for each browser...\\n')\n\n  const commonManifest = require('../src/manifest/common.manifest.json')\n  const version = { version: require('../package.json').version }\n  const files = fs.readdirSync(paths.appBuild)\n    .map(name => ({name, path: path.join(paths.appBuild, name)}))\n\n  const localesJSON = genLocales()\n\n  return Promise.all(browsers.map(browser => {\n    const dest = path.join(paths.appBuild, browser)\n    if (!fs.existsSync(dest)){ fs.mkdirSync(dest) }\n\n    const browserManifest = require(`../src/manifest/${browser}.manifest.json`)\n\n    return Promise.all([\n      // manifest\n      fs.writeJson(\n        path.join(dest, 'manifest.json'),\n        Object.assign({}, commonManifest, browserManifest, version),\n        { spaces: 2 },\n      ),\n      // locales\n      writeLocales(path.join(dest, '_locales'), localesJSON),\n      // public assets\n      fs.copy(paths.appPublic, dest, {\n        dereference: true,\n        // ignore files or dirs start with \".\"\n        filter: file => !/[\\\\\\/]+\\./.test(file),\n      }),\n      // project files\n      ...files.map(file => fs.copy(file.path, path.join(dest, file.name), {\n        dereference: true,\n        // remove js files for css only chunks\n        filter: file => !/[\\\\\\/]+(panel(-internal)?|dicts[\\\\\\/]+[^\\\\\\/]+)\\.js(\\.map)?$/.test(file)\n      }))\n    ])\n  })).then(() => Promise.all(files.map(file =>\n    // clean up files\n    fs.remove(file.path)\n  )))\n  .then(() => {\n    console.log('Done.\\n')\n  })\n}\n\nfunction genLocales () {\n  const msgJSON = require('../src/_locales/messages.json')\n  const msgKeys = Object.keys(msgJSON)\n  const langs = Object.keys(msgJSON[msgKeys[0]].message)\n\n  return langs.reduce((json, lang) => {\n    json[lang] = {}\n    msgKeys.forEach(k => {\n      json[lang][k] = {\n        description: msgJSON[k].description,\n        message: msgJSON[k].message[lang],\n      }\n    })\n    return json\n  }, {})\n}\n\nfunction writeLocales (localesPath, localesJSON) {\n  const locales = Object.keys(localesJSON)\n  return fs.mkdir(localesPath)\n  .then(() =>\n    Promise.all(locales.map(lang => {\n      const langPath = path.join(localesPath, lang)\n      return fs.mkdir(langPath)\n      .then(() => fs.writeFile(path.join(langPath, 'messages.json'), JSON.stringify(localesJSON[lang], null, '  ')))\n    }))\n  )\n}\n\nfunction patchInternalCSS () {\n  const styles = fs.readdirSync(path.join(paths.appBuild, 'dicts'))\n    .filter(name => name.endsWith('.css'))\n    .map(name => ['dicts/' + name, 'dicts/internal/' + name])\n  styles.push(['panel-internal.css', 'panel-internal.css'])\n\n  styles.forEach(([src, out]) => {\n    const cssPath = path.join(paths.appBuild, src)\n    const panelCSS = fs.readFileSync(cssPath, 'utf8')\n    const output = postcss([\n      increaseSpecificity({\n        repeat: 1,\n        overrideIds: false,\n        stackableRoot: '.panel-StyleRoot'\n      })\n    ])\n    .process(panelCSS)\n    .css\n\n    fs.outputFileSync(path.join(paths.appBuild, out), output)\n  })\n}\n"
  },
  {
    "path": "scripts/firefox-fix.js",
    "content": "// Firefox does not support dynamic import in WebExtension\n// https://bugzilla.mozilla.org/show_bug.cgi?id=1536094\n\nconst path = require('path')\nconst fs = require('fs-extra')\nconst jsdom = require('jsdom')\nconst { JSDOM } = jsdom\n\nconst ffPath = path.join(__dirname, '../build/firefox')\nconst manifestPath = path.join(ffPath, 'manifest.json')\n\nconst manifest = require(manifestPath)\n\nmain()\n\nasync function main() {\n  const htmls = (await fs.readdir(ffPath)).filter(name =>\n    name.endsWith('.html')\n  )\n  const htmlTexts = await Promise.all(\n    htmls.map(html => fs.readFile(path.join(ffPath, html), 'utf8'))\n  )\n\n  const staticChunkIds = await getStaticChunks(htmlTexts)\n  const allChunks = await fs.readdir(path.join(ffPath, 'assets'))\n  const dynamicChunks = allChunks\n    .filter(name => {\n      const m = /^([^.]+)\\.[^.]+\\.js$/.exec(name)\n      if (m) {\n        return !staticChunkIds.has(m[1])\n      }\n      return false\n    })\n    .map(filename => `/assets/${filename}`)\n\n  const dynamicChunksWithoutAntd = dynamicChunks.filter(\n    name => !name.startsWith('/assets/antd')\n  )\n\n  manifest.content_scripts.forEach(item => {\n    if (item.js) {\n      if (item.js.some(name => name.startsWith('assets/selection'))) {\n        return\n      }\n      item.js.push(...dynamicChunksWithoutAntd)\n    }\n  })\n\n  manifest.background.scripts.push(...dynamicChunksWithoutAntd)\n\n  await fs.outputJSON(manifestPath, manifest, { spaces: 2 })\n\n  await Promise.all(\n    htmlTexts.map((text, i) => {\n      const dom = new JSDOM(text)\n\n      let chunks = dynamicChunksWithoutAntd\n\n      if (\n        htmls[i] === 'options.html' ||\n        htmls[i] === 'notebook.html' ||\n        htmls[i] === 'history.html'\n      ) {\n        chunks = dynamicChunks\n      }\n\n      chunks.forEach(name => {\n        const script = dom.window.document.createElement('script')\n        script.src = name\n        dom.window.document.head.appendChild(script)\n      })\n      return fs.outputFile(\n        path.join(ffPath, htmls[i]),\n        dom.window.document.documentElement.outerHTML\n      )\n    })\n  )\n\n  // urgh\n  // https://github.com/mozilla/addons-linter/issues/2498\n  const runtime = allChunks.find(filename => filename.startsWith('runtime.'))\n  const runtimePath = path.join(ffPath, 'assets', runtime)\n  await fs.outputFile(\n    runtimePath,\n    (await fs.readFile(runtimePath, 'utf8')).replace(\n      /import\\(/g,\n      'saladictImport('\n    )\n  )\n}\n\nasync function getStaticChunks(htmls) {\n  const staticChunks = new Set()\n\n  htmls.forEach(text => {\n    const matcher = /\"\\/assets\\/([^.]+)\\.[^.]+\\.js\"/g\n    let m\n    // eslint-disable-next-line no-cond-assign\n    while ((m = matcher.exec(text))) {\n      staticChunks.add(m[1])\n    }\n  })\n\n  manifest.content_scripts.forEach(item => {\n    if (item.js) {\n      item.js.forEach(name => {\n        const m = /assets\\/([^.]+)\\.[^.]+\\.js/.exec(name)\n        if (m) {\n          staticChunks.add(m[1])\n        }\n      })\n    }\n  })\n\n  manifest.background.scripts.forEach(name => {\n    const m = /assets\\/([^.]+)\\.[^.]+\\.js/.exec(name)\n    if (m) {\n      staticChunks.add(m[1])\n    }\n  })\n\n  staticChunks.delete('franc')\n\n  return staticChunks\n}\n"
  },
  {
    "path": "scripts/fixtures.js",
    "content": "const path = require('path')\nconst fs = require('fs-extra')\nconst axios = require('axios')\nconst SocksProxyAgent = require('socks-proxy-agent')\nconst fglob = require('fast-glob')\nconst cliProgress = require('cli-progress')\nconst randomMua = require('random-mua')\nconst argv = require('yargs').argv\nconst env = require('dotenv').config({\n  path: path.join(__dirname, '../.env')\n}).parsed\n\n// prevent hjdict tls error\n// There isn't anything sensitive of the source files so it's ok\nprocess.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'\n\n// download fixtures\n// default only download non-existed files\n// --delete remove all fixtures\n// --update remove then download all fixtures\n// --fileMatchPattern filter file path with regex\n\nmain().catch(console.log)\n\nasync function main() {\n  if (argv.delete) {\n    deletion()\n  } else {\n    if (argv.update) {\n      await deletion()\n    }\n    add()\n  }\n}\n\nasync function add() {\n  let proxyConfig = {}\n\n  if (env.PROXY_HOST) {\n    if (env.PROXY_PROTOCAL && env.PROXY_PROTOCAL.startsWith('socks')) {\n      const httpsAgent = new SocksProxyAgent(\n        `socks5://${env.PROXY_HOST}:${env.PROXY_PORT}`\n      )\n      proxyConfig = {\n        httpsAgent,\n        httpAgent: httpsAgent\n      }\n    } else {\n      proxyConfig = {\n        proxy: {\n          host: env.PROXY_HOST,\n          port: env.PROXY_PORT\n        }\n      }\n    }\n  }\n\n  if (env.PROXY_HOST) {\n    console.log(\n      `with proxy: ${env.PROXY_PROTOCAL}://${env.PROXY_HOST}:${env.PROXY_PORT}`\n    )\n  }\n\n  const progressBars = new cliProgress.MultiBar({\n    format: ' {bar} | \"{file}\" | {value}/{total} | {status}',\n    hideCursor: true,\n    barCompleteChar: '\\u2588',\n    barIncompleteChar: '\\u2591',\n    clearOnComplete: false,\n    stopOnComplete: true\n  })\n\n  const errors = []\n\n  let fixturesPath = await fglob(['**/fixtures.js'], {\n    cwd: path.join(__dirname, '../test'),\n    absolute: true,\n    onlyFiles: true\n  })\n\n  if (argv.fileMatchPattern) {\n    const matcher = new RegExp(argv.fileMatchPattern)\n    fixturesPath = fixturesPath.filter(filePath => matcher.test(filePath))\n  }\n\n  await Promise.all(fixturesPath.map(fetchDictFixtures))\n\n  if (errors.length > 0) {\n    await fs.outputFile(\n      path.join(__dirname, 'fixtures.log'),\n      errors.map(([name, error]) => name + '\\n' + error).join('\\n\\n')\n    )\n    console.log(\n      '\\nErrors:\\n\\n',\n      errors.map(([name, e, url]) => `${name}, ${e}\\n${url}\\n`).join('\\n')\n    )\n  }\n\n  async function fetchDictFixtures(fixturePath) {\n    const fixture = require(fixturePath)\n\n    const fetched = []\n\n    for (const index in fixture.files) {\n      const [filename, fetchUrl] = fixture.files[index]\n\n      const destPath = fixturePath.replace(\n        /fixtures.js$/,\n        `response/${filename}`\n      )\n      const stat = await fs.stat(destPath).catch(() => null)\n      if (stat && stat.isFile()) {\n        fetched.push(await fs.readFile(destPath, 'utf8'))\n        continue\n      }\n\n      const dictname = /[\\\\/]+([^\\\\/]+)[\\\\/]+fixtures.js$/.exec(fixturePath)[1]\n\n      const pgBar = progressBars.create(100, 0, {\n        file: `${dictname}/fixture${index * 1 + 1}`,\n        status: 'downloading'\n      })\n\n      try {\n        var customConfig =\n          typeof fetchUrl === 'string'\n            ? {\n                url: fetchUrl\n              }\n            : fetchUrl(fetched)\n      } catch (e) {\n        pgBar.update(null, { status: 'parse error' })\n        pgBar.stop()\n        continue\n      }\n\n      if (!customConfig) {\n        pgBar.update(null, { status: 'empty config' })\n        pgBar.stop()\n        continue\n      }\n\n      const { origin, host } = new URL(customConfig.url)\n      const axiosConfig = {\n        transformResponse: [data => data],\n        ...proxyConfig,\n        ...customConfig,\n        headers: {\n          'user-agent': randomMua(),\n          ...(customConfig.headers || {})\n        }\n      }\n\n      try {\n        const { data: result } = await axios(axiosConfig).catch(e =>\n          e.response && e.response.status === 404 && e.response.data\n            ? e.response\n            : axios({\n                ...axiosConfig,\n                headers: {\n                  ...axiosConfig.headers,\n                  origin,\n                  host,\n                  referer: origin\n                }\n              })\n        )\n\n        fetched.push(result)\n        await fs.outputFile(\n          fixturePath.replace(/fixtures.js$/, `response/${filename}`),\n          result\n        )\n\n        pgBar.update(100, { status: 'success' })\n        pgBar.stop()\n      } catch (e) {\n        errors.push([`${dictname}/${filename}`, e, axiosConfig.url])\n\n        pgBar.update(null, { status: 'failed' })\n        pgBar.stop()\n      }\n    }\n  }\n}\n\nasync function deletion() {\n  let fixturesPath = await fglob(['**/response'], {\n    cwd: path.join(__dirname, '../test'),\n    absolute: true,\n    onlyDirectories: true\n  })\n\n  if (argv.fileMatchPattern) {\n    const matcher = new RegExp(argv.fileMatchPattern)\n    fixturesPath = fixturesPath.filter(filePath => matcher.test(filePath))\n  }\n\n  await Promise.all(fixturesPath.map(fixturePath => fs.remove(fixturePath)))\n}\n"
  },
  {
    "path": "scripts/pdf.js",
    "content": "/** !\n * Upgrade PDF.js\n */\n\nconst shell = require('shelljs')\nconst path = require('path')\nconst fs = require('fs-extra')\n\nif (!shell.which('git')) {\n  shell.echo('Sorry, this script requires git')\n  shell.exit(1)\n}\n\nconst cacheDir = 'pdf'\nconst repoRoot = 'pdf'\nconst publicPDFRoot = path.join(__dirname, '../assets/pdf')\nconst pdfFiles = [\n  'build/pdf.js',\n  'build/pdf.worker.js',\n  'web/debugger.js',\n  'web/viewer.js',\n  'web/viewer.html',\n  'web/viewer.css'\n]\nconst pdfDirs = ['web/cmaps', 'web/images', 'web/locale']\nconst files = [...pdfFiles, ...pdfDirs]\n\nshell.cd(path.resolve(__dirname))\n\nshell.rm('-rf', cacheDir)\n\nexec(\n  `wget https://github.com/mozilla/pdf.js/releases/download/v2.16.105/pdfjs-2.16.105-dist.zip -O pdfjs.tar.gz &&\n  mkdir -p ${cacheDir} &&\n  tar -xzvf pdfjs.tar.gz -C ${cacheDir}`,\n  'Error: download failed'\n)\n\nshell.cd('./' + cacheDir)\n\nstartUpgrade()\n\nasync function startUpgrade() {\n  shell.echo('\\nChecking files.')\n  await Promise.all(files.map(p => exists(path.join(__dirname, repoRoot, p))))\n\n  shell.echo('\\nModifying files.')\n  await Promise.all([modifyViewrJS(), modifyViewerHTML()])\n\n  await fs.ensureDir(publicPDFRoot)\n\n  shell.echo('\\nCloning files.')\n  cleanInit()\n\n  await cloneFiles()\n\n  shell.echo('\\nCleaning files.')\n  shell.cd(path.resolve(__dirname))\n  shell.rm('-rf', cacheDir)\n\n  shell.echo('\\ndone.')\n}\n\nasync function modifyViewrJS() {\n  const viewerPath = path.join(__dirname, repoRoot, 'web/viewer.js')\n  let file = await fs.readFile(viewerPath, 'utf8')\n\n  file = '/* saladict */ window.__SALADICT_PDF_PAGE__ = true;\\n' + file\n\n  // change default pdf\n  const defaultPDFTester = /defaultUrl = {[\\s\\S]*?value: (['\"]\\S+?.pdf['\"]),[\\s\\S]*?kind: OptionKind\\.VIEWER/\n  if (!defaultPDFTester.test(file)) {\n    shell.echo('Could not locate default pdf in viewer.js')\n    shell.exit(1)\n  }\n  file = file.replace(defaultPDFTester, (m, p1) =>\n    m.replace(p1, \"/* saladict */'/assets/default.pdf'\")\n  )\n\n  // disable url check\n  const validateTester = /validateFileURL\\(file\\);/\n  if (!validateTester.test(file)) {\n    shell.echo('Could not locate validateFileURL in viewer.js')\n    shell.exit(1)\n  }\n  file = file.replace(validateTester, '/* saladict */')\n\n  // force dark mode\n  const viewCssTester = /\"viewerCssTheme\": 0,/\n  if (!viewCssTester.test(file)) {\n    shell.echo('Could not locate viewerCssTheme config in viewer.js')\n    shell.exit(1)\n  }\n  file = file.replace(viewCssTester, '\"viewerCssTheme\": 2, /* saladict */')\n\n  await fs.writeFile(viewerPath, file)\n}\n\nasync function modifyViewerHTML() {\n  const viewerPath = path.join(__dirname, repoRoot, 'web/viewer.html')\n  let file = await fs.readFile(viewerPath, 'utf8')\n\n  if (!file.includes(`</body>`)) {\n    shell.echo('Could not locate </body> in viewer.html')\n    shell.exit(1)\n  }\n\n  // Load Saladict dict panel\n  file = file.replace(\n    `</body>`,\n    `\n    <!-- Saladict -->\n    <script src=\"/assets/browser-polyfill.min.js\"></script>\n    <script src=\"/assets/inject-dict-panel.js\"></script>\n    <script src=\"/assets/vimium-c-injector.js\"></script>\n  </body>\n`\n  )\n\n  await fs.writeFile(viewerPath, file)\n}\n\nfunction cleanInit() {\n  pdfDirs.forEach(name => {\n    shell.rm('-rf', path.join(publicPDFRoot, name))\n  })\n}\n\nasync function exists(path) {\n  try {\n    await fs.access(path)\n  } catch (e) {\n    shell.echo(path + ' not exist')\n    shell.exit(1)\n  }\n}\n\nfunction exec(command, errorMsg) {\n  const execResult = shell.exec(command)\n\n  if (execResult.code !== 0) {\n    if (errorMsg) {\n      shell.echo(errorMsg)\n    }\n    shell.echo(execResult.stdout)\n    shell.echo(execResult.stderr)\n    shell.exit(1)\n  }\n}\n\nasync function cloneFiles() {\n  for (const pdfFile of pdfFiles) {\n    const targetPath = path.join(publicPDFRoot, pdfFile)\n    await fs.ensureFile(targetPath)\n    await fs.copy(path.join(__dirname, repoRoot, pdfFile), targetPath)\n  }\n\n  const restPdfDirs = pdfDirs.filter(name => name !== 'web/locale')\n\n  for (const pdfDir of restPdfDirs) {\n    const targetPath = path.join(publicPDFRoot, pdfDir)\n    await fs.ensureDir(targetPath)\n    await fs.copy(path.join(__dirname, repoRoot, pdfDir), targetPath)\n  }\n\n  // copy locale.properties\n  await fs.ensureDir(path.join(publicPDFRoot, 'web/locale'))\n  await fs.copy(\n    path.join(__dirname, repoRoot, 'web/locale/locale.properties'),\n    path.join(publicPDFRoot, 'web/locale/locale.properties')\n  )\n\n  const locales = (\n    await fs.readdir(path.join(__dirname, repoRoot, 'web/locale'))\n  ).filter(\n    name =>\n      name.startsWith('en') ||\n      name.startsWith('zh') ||\n      /^(ja|ko|uk)$/.test(name)\n  )\n\n  for (const locale of locales) {\n    const targetPath = path.join(publicPDFRoot, 'web/locale', locale)\n    await fs.ensureDir(targetPath)\n    await fs.copy(\n      path.join(__dirname, repoRoot, 'web/locale', locale),\n      targetPath\n    )\n  }\n}\n"
  },
  {
    "path": "scripts/setup-env.js",
    "content": "const fs = require('fs-extra')\nconst path = require('path')\n\nmain().catch(swallow)\n\n// set-up local testing env\nasync function main() {\n  fs.ensureDir(path.join(__dirname, '../build'))\n  const depsPath = path.join(__dirname, '../deps')\n  const destPath = path.join(__dirname, '../node_modules')\n  if (await isDirectory(depsPath)) {\n    const depsFiles = []\n    const rawDepsFiles = await fs.readdir(depsPath)\n    for (const name of rawDepsFiles) {\n      if (name.startsWith('@')) {\n        const nsFiles = await fs.readdir(path.join(depsPath, name))\n        for (const nsName of nsFiles) {\n          depsFiles.push(path.join(name, nsName))\n        }\n      } else {\n        depsFiles.push(name)\n      }\n    }\n    await Promise.all(\n      depsFiles.map(async name => {\n        const destPkgPath = path.join(destPath, name)\n        await fs.remove(destPkgPath).catch(swallow)\n        await fs.ensureDir(destPkgPath).catch(swallow)\n        await fs.copy(path.join(depsPath, name), destPkgPath).catch(swallow)\n      })\n    )\n  }\n}\n\nasync function isDirectory(dirPath) {\n  const dirStat = await fs.stat(dirPath).catch(swallow)\n  return Boolean(dirStat && dirStat.isDirectory())\n}\n\nfunction swallow() {\n  return null\n}\n"
  },
  {
    "path": "scripts/start.js",
    "content": "'use strict'\n\n// Do this as the first thing so that any code reading it knows the right env.\nprocess.env.BABEL_ENV = 'development'\nprocess.env.NODE_ENV = 'development'\n\nconst argv = require('minimist')(process.argv.slice(2))\nif (argv.debug) { process.env.DEBUG_MODE = true }\n\n// Makes the script crash on unhandled rejections instead of silently\n// ignoring them. In the future, promise rejections that are not handled will\n// terminate the Node.js process with a non-zero exit code.\nprocess.on('unhandledRejection', err => {\n  throw err\n})\n\n// Ensure environment variables are read.\nrequire('../config/env')\n\nconst fs = require('fs')\nconst chalk = require('chalk')\nconst webpack = require('webpack')\nconst WebpackDevServer = require('webpack-dev-server')\nconst clearConsole = require('react-dev-utils/clearConsole')\nconst checkRequiredFiles = require('react-dev-utils/checkRequiredFiles')\nconst {\n  choosePort,\n  createCompiler,\n  prepareProxy,\n  prepareUrls,\n} = require('react-dev-utils/WebpackDevServerUtils')\nconst openBrowser = require('react-dev-utils/openBrowser')\nconst paths = require('../config/paths')\nconst config = require('../config/webpack.config.dev')\nconst createDevServerConfig = require('../config/webpackDevServer.config')\n\nconst useYarn = fs.existsSync(paths.yarnLockFile)\nconst isInteractive = process.stdout.isTTY\n\n// Tools like Cloud9 rely on this.\nconst DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000\nconst HOST = process.env.HOST || '0.0.0.0'\n\n// We attempt to use the default port but if it is busy, we offer the user to\n// run on a different port. `detect()` Promise resolves to the next free port.\nchoosePort(HOST, DEFAULT_PORT)\n  .then(port => {\n    if (port == null) {\n      // We have not found a port.\n      return\n    }\n    const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'\n    const appName = require(paths.appPackageJson).name\n    const urls = prepareUrls(protocol, HOST, port)\n    // Create a webpack compiler that is configured with custom messages.\n    const compiler = createCompiler(webpack, config, appName, urls, useYarn)\n    // Load proxy config\n    const proxySetting = require(paths.appPackageJson).proxy\n    const proxyConfig = prepareProxy(proxySetting, paths.appPublic)\n    // Serve webpack assets generated by the compiler over a web sever.\n    const serverConfig = createDevServerConfig(\n      proxyConfig,\n      urls.lanUrlForConfig\n    )\n    const devServer = new WebpackDevServer(compiler, serverConfig)\n    // Launch WebpackDevServer.\n    devServer.listen(port, HOST, err => {\n      if (err) {\n        return console.log(err)\n      }\n      if (isInteractive) {\n        clearConsole()\n      }\n      console.log(chalk.cyan('Starting the development server...\\n'))\n      openBrowser(urls.localUrlForBrowser)\n    })\n\n    ;['SIGINT', 'SIGTERM'].forEach(function(sig) {\n      process.on(sig, function() {\n        devServer.close()\n        process.exit()\n      })\n    })\n  })\n  .catch(err => {\n    if (err && err.message) {\n      console.log(err.message)\n    }\n    process.exit(1)\n  })\n"
  },
  {
    "path": "scripts/style-extractor.js",
    "content": "/* eslint-disable @typescript-eslint/no-unused-vars */\nconst postcss = require('postcss')\nconst fs = require('fs')\nconst path = require('path')\n\n/**\n * Get all ids and class names under a root node.\n * Use in console.\n * @param {HTMLElement} root\n */\nfunction getIdsAndClassNames(root) {\n  const result = new Set()\n  _fn(root)\n  return Array.from(result)\n    .map(x => `'${x}',`)\n    .join('\\n')\n\n  function _fn(node) {\n    if (!node) {\n      return\n    }\n    if (typeof node.className === 'string') {\n      node.className\n        .split(/\\s+/)\n        .filter(Boolean)\n        .reduce((r, n) => r.add('.' + n), result)\n    }\n    if (node.id) {\n      result.add('#' + node.id)\n    }\n    Array.from(node.children).forEach(_fn)\n  }\n}\n\n/**\n * Get relevant styles if the selector contains the keywords..\n * @param {string[]} attrs - i.e. [\"head\", \"red\"]\n * @param {string} from - css path\n * @param {string} to - css path\n */\nfunction getStylesByAttrs(attrs, from, to) {\n  let result = ''\n\n  const lattrs = attrs.map(name => name.toLocaleLowerCase())\n\n  fs.readFile(from, (err, source) => {\n    if (err) {\n      console.error(err)\n      process.exit(1)\n    }\n    const root = postcss.parse(source, { from, to })\n    root.walkRules(rule => {\n      const selector = rule.selector.toLowerCase()\n      if (lattrs.some(attr => selector.includes(attr))) {\n        result += rule.toString() + '\\n\\n'\n        rule.remove()\n      }\n    })\n    root.walkAtRules(rule => {\n      result += rule.toString() + '\\n\\n'\n    })\n    fs.writeFile(to, result, () => true)\n  })\n}\n"
  },
  {
    "path": "scripts/test.js",
    "content": "'use strict'\n\n// Do this as the first thing so that any code reading it knows the right env.\nprocess.env.BABEL_ENV = 'test'\nprocess.env.NODE_ENV = 'test'\nprocess.env.PUBLIC_URL = ''\n\nconst rawArgv = process.argv.slice(2)\nconst argv = require('minimist')(rawArgv)\nif (argv.debug) { process.env.DEBUG_MODE = true }\n\n// Makes the script crash on unhandled rejections instead of silently\n// ignoring them. In the future, promise rejections that are not handled will\n// terminate the Node.js process with a non-zero exit code.\nprocess.on('unhandledRejection', err => {\n  throw err\n})\n\n// Ensure environment variables are read.\nrequire('../config/env')\n\nconst jest = require('jest')\n\n// Watch unless on CI or in coverage mode\n// if (!process.env.CI && !argv.coverage) {\n//   rawArgv.push('--watch')\n// }\n\nif (process.env.CI) {\n  rawArgv.push('--no-watchman', '--runInBand', '--no-cache')\n}\n\njest.run(rawArgv)\n"
  },
  {
    "path": "src/_helpers/__mocks__/browser-api.ts",
    "content": "/**\n * @file Wraps some of the extension apis\n */\n\nimport { Observable, fromEventPattern } from 'rxjs'\nimport { map } from 'rxjs/operators'\nimport { Message } from '@/typings/message'\n\n/* --------------------------------------- *\\\n * #Types\n\\* --------------------------------------- */\n\nexport type StorageArea = 'all' | 'local' | 'sync'\n\nexport type StorageListenerCb = (\n  changes: browser.storage.StorageChange,\n  areaName: string\n) => void\n\ntype onMessageEvent = (\n  message: Message,\n  sender: browser.runtime.MessageSender,\n  sendResponse: Function\n) => Promise<any> | boolean | void\n\n/* --------------------------------------- *\\\n * #Globals\n\\* --------------------------------------- */\n\nconst noop = () => {\n  /* do nothing */\n}\n\n// share the listener so that it can be manipulated manually\ndeclare global {\n  interface Window {\n    __messageListeners__: Map<\n      onMessageEvent,\n      Map<Message['type'], onMessageEvent>\n    >\n    __messageSelfListeners__: Map<\n      onMessageEvent,\n      Map<Message['type'], onMessageEvent>\n    >\n    __storageListeners__: Map<StorageListenerCb, Map<string, StorageListenerCb>>\n  }\n}\n\n/**\n * key: {function} user's callback function\n * values: {Map} listeners, key: message type, values: generated or user's callback functions\n */\nwindow.__messageListeners__ = window.__messageListeners__ || new Map()\n\n/**\n * For self page messaging\n * key: {function} user's callback function\n * values: {Map} listeners, key: message type, values: generated or user's callback functions\n */\nwindow.__messageSelfListeners__ = window.__messageSelfListeners__ || new Map()\n\n/**\n * key: {function} user's callback function\n * values: {Map} listeners, key: message type, values: generated or user's callback functions\n */\nwindow.__storageListeners__ = window.__storageListeners__ || new Map()\n\nconst messageListeners = window.__messageListeners__\nconst messageSelfListeners = window.__messageSelfListeners__\nconst storageListeners = window.__storageListeners__\n\n/* --------------------------------------- *\\\n * #Exports\n\\* --------------------------------------- */\n\nexport const storage = {\n  sync: {\n    clear: _storageClear(),\n    remove: _storageRemove(),\n    get: _storageGet(),\n    set: _storageSet(),\n    /** Only for sync area */\n    addListener: _storageAddListener('sync'),\n    /** Only for sync area */\n    removeListener: _storageRemoveListener('sync'),\n    createStream: noop,\n    dispatch: _dispatchStorageEvent('sync')\n  },\n  local: {\n    clear: _storageClear(),\n    remove: _storageRemove(),\n    get: _storageGet(),\n    set: _storageSet(),\n    /** Only for local area */\n    addListener: _storageAddListener('local'),\n    /** Only for local area */\n    removeListener: _storageRemoveListener('local'),\n    createStream: noop,\n    dispatch: _dispatchStorageEvent('local')\n  },\n  /** Clear all area */\n  clear: _storageClear(),\n  addListener: _storageAddListener('all'),\n  removeListener: _storageRemoveListener('all'),\n  createStream: noop as ReturnType<typeof _storageCreateStream>,\n  dispatch: dispatchStorageEvent\n}\n\nstorage.sync.createStream = _storageCreateStream('sync')\nstorage.local.createStream = _storageCreateStream('local')\nstorage.createStream = _storageCreateStream('all')\n\n/**\n * Wraps in-app runtime.sendMessage and tabs.sendMessage\n * Does not warp cross extension messaging!\n */\nexport const message = {\n  send: _messageSend(false),\n  addListener: _messageAddListener(false),\n  removeListener: _messageRemoveListener(false),\n  createStream: noop,\n  dispatch: _dispatchMessageEvent(false),\n\n  self: {\n    initClient: jest.fn(() => Promise.resolve()),\n    initServer: jest.fn(noop),\n    send: _messageSend(true),\n    addListener: _messageAddListener(true),\n    removeListener: _messageRemoveListener(true),\n    createStream: noop,\n    dispatch: _dispatchMessageEvent(true)\n  }\n}\n\nmessage.createStream = _messageCreateStream(false)\nmessage.self.createStream = _messageCreateStream(true)\n\n/**\n * Open a url on new tab or highlight a existing tab if already opened\n */\nexport const openUrl = jest.fn(() => Promise.resolve())\n\nexport default {\n  openUrl,\n  storage,\n  message\n}\n\n/* --------------------------------------- *\\\n * #Storage\n\\* --------------------------------------- */\nfunction _storageClear() {\n  return jest.fn(storageClear)\n\n  function storageClear(): Promise<void> {\n    return Promise.resolve()\n  }\n}\n\nfunction _storageRemove() {\n  return jest.fn(storageRemove)\n\n  function storageRemove(keys: string | string[]): Promise<void> {\n    return Promise.resolve()\n  }\n}\n\nfunction _storageGet() {\n  return jest.fn(storageGet)\n\n  function storageGet<T = any>(key?: string | string[] | null): Promise<T>\n  function storageGet<T extends Object>(key: T | any): Promise<T>\n  function storageGet<T = any>(...args): Promise<T> {\n    return Promise.resolve() as any\n  }\n}\n\nfunction _storageSet() {\n  return jest.fn(storageSet)\n\n  function storageSet(keys: any): Promise<void> {\n    return Promise.resolve() as any\n  }\n}\n\nfunction _storageAddListener(area: string) {\n  return jest.fn(storageAddListener)\n\n  function storageAddListener(cb: StorageListenerCb): void\n  function storageAddListener(key: string, cb: StorageListenerCb): void\n  function storageAddListener(...args): void {\n    let key: string\n    let cb: StorageListenerCb\n    if (typeof args[0] === 'function') {\n      key = ''\n      cb = args[0]\n    } else if (typeof args[0] === 'string' && typeof args[1] === 'function') {\n      key = args[0]\n      cb = args[1]\n    } else {\n      throw new Error('wrong arguments type')\n    }\n\n    let listeners = storageListeners.get(cb)\n    if (!listeners) {\n      listeners = new Map()\n      storageListeners.set(cb, listeners)\n    }\n    const listenerKey = area + key\n    let listener = listeners.get(listenerKey)\n    if (!listener) {\n      listener = (changes, areaName) => {\n        if ((area === 'all' || areaName === area) && (!key || changes[key])) {\n          cb(changes, areaName)\n        }\n      }\n      listeners.set(listenerKey, listener)\n    }\n  }\n}\n\nfunction _storageRemoveListener(area: string) {\n  return jest.fn(storageRemoveListener)\n\n  function storageRemoveListener(key: string, cb: StorageListenerCb): void\n  function storageRemoveListener(cb: StorageListenerCb): void\n  function storageRemoveListener(...args): void {\n    let key: string\n    let cb: StorageListenerCb\n    if (typeof args[0] === 'function') {\n      key = ''\n      cb = args[0]\n    } else if (typeof args[0] === 'string' && typeof args[1] === 'function') {\n      key = args[0]\n      cb = args[1]\n    } else {\n      throw new Error('wrong arguments type')\n    }\n\n    const listeners = storageListeners.get(cb)\n    if (listeners) {\n      if (key) {\n        // remove 'cb' listeners with 'key' under 'storageArea'\n        const listenerKey = area + key\n        const listener = listeners.get(listenerKey)\n        if (listener) {\n          listeners.delete(listenerKey)\n          if (listeners.size <= 0) {\n            storageListeners.delete(cb)\n          }\n        }\n      } else {\n        // remove all 'cb' listeners under 'storageArea'\n        storageListeners.delete(cb)\n      }\n    }\n  }\n}\n\nfunction _storageCreateStream(area: string) {\n  return jest.fn(storageCreateStream)\n\n  function storageCreateStream(key: string) {\n    const obj = area === 'all' ? storage : storage[area]\n    return fromEventPattern(\n      handler => obj.addListener(key, handler as StorageListenerCb),\n      handler => obj.removeListener(key, handler as StorageListenerCb)\n    ).pipe(map((args: any) => (Array.isArray(args) ? args[0][key] : args[key])))\n  }\n}\n\ninterface DispatchStorageEventOptions {\n  /** message key */\n  key?: string\n  newValue?: any\n  oldValue?: any\n}\n\ninterface DispatchStorageEventOptionsGeneral\n  extends DispatchStorageEventOptions {\n  area?: StorageArea | ''\n}\n\nfunction _dispatchStorageEvent(area: 'sync' | 'local') {\n  const _fn = dispatchStorageEvent\n  return function dispatchStorageEvent(options: DispatchStorageEventOptions) {\n    return _fn(Object.assign(options, { area }))\n  }\n}\n\nexport function dispatchStorageEvent(\n  options: DispatchStorageEventOptionsGeneral\n): void {\n  storageListeners.forEach(m => {\n    m.forEach((cb, key) => {\n      if (!options.key || options.key === key) {\n        if (!options.area || options.area === 'all') {\n          cb({ newValue: options.newValue, oldValue: options.oldValue }, 'sync')\n          cb(\n            { newValue: options.newValue, oldValue: options.oldValue },\n            'local'\n          )\n        } else {\n          cb(\n            { newValue: options.newValue, oldValue: options.oldValue },\n            options.area\n          )\n        }\n      }\n    })\n  })\n}\n\n/* --------------------------------------- *\\\n * #Message\n\\* --------------------------------------- */\nfunction _messageSend(self: boolean) {\n  return jest.fn(self ? messageSendSelf : messageSend)\n\n  function messageSend(tabId: number, message: Message): Promise<any>\n  function messageSend(message: Message): Promise<any>\n  function messageSend(...args): Promise<any> {\n    return Promise.resolve()\n  }\n\n  function messageSendSelf(message: Message): Promise<any> {\n    return Promise.resolve()\n  }\n}\n\nfunction _messageAddListener(self: boolean) {\n  return jest.fn<void, [Message['type'], onMessageEvent] | [onMessageEvent]>(\n    messageAddListener as any\n  )\n\n  function messageAddListener(\n    messageType: Message['type'],\n    cb: onMessageEvent\n  ): void\n  function messageAddListener(cb: onMessageEvent): void\n  function messageAddListener(...args): void {\n    const allListeners = self ? messageSelfListeners : messageListeners\n    const messageType = args.length === 1 ? undefined : args[0]\n    const cb = args.length === 1 ? args[0] : args[1]\n    let listeners = allListeners.get(cb)\n    if (!listeners) {\n      listeners = new Map()\n      allListeners.set(cb, listeners)\n    }\n    let listener = listeners.get(messageType || '__DEFAULT_MSGTYPE__')\n    if (!listener) {\n      listener = ((message, sender, sendResponse) => {\n        if (message && (self ? window.pageId === 'PAGE_INFO' : !'PAGE_INFO')) {\n          if (messageType == null || message.type === messageType) {\n            return cb(message, sender, sendResponse)\n          }\n        }\n      }) as onMessageEvent\n      listeners.set(messageType, listener)\n    }\n  }\n}\n\nfunction _messageRemoveListener(self: boolean) {\n  return jest.fn<void, [Message['type'], onMessageEvent] | [onMessageEvent]>(\n    messageRemoveListener as any\n  )\n\n  function messageRemoveListener(\n    messageType: Message['type'],\n    cb: onMessageEvent\n  ): void\n  function messageRemoveListener(cb: onMessageEvent): void\n  function messageRemoveListener(...args): void {\n    const allListeners = self ? messageSelfListeners : messageListeners\n    const messageType = args.length === 1 ? undefined : args[0]\n    const cb = args.length === 1 ? args[0] : args[1]\n    const listeners = allListeners.get(cb)\n    if (listeners) {\n      if (messageType) {\n        const listener = listeners.get(messageType)\n        if (listener) {\n          listeners.delete(messageType)\n          if (listeners.size <= 0) {\n            allListeners.delete(cb)\n          }\n        }\n      } else {\n        // delete all cb related callbacks\n        allListeners.delete(cb)\n      }\n    }\n  }\n}\n\nfunction _messageCreateStream(self: boolean) {\n  return jest.fn(messageCreateStream)\n\n  function messageCreateStream<T>(\n    messageType?: Message['type']\n  ): Observable<T> {\n    const obj = self ? message.self : message\n    const pattern$ = messageType\n      ? fromEventPattern(\n          handler => obj.addListener(messageType, handler as onMessageEvent),\n          handler => obj.removeListener(messageType, handler as onMessageEvent)\n        )\n      : fromEventPattern(\n          handler => obj.addListener(handler as onMessageEvent),\n          handler => obj.removeListener(handler as onMessageEvent)\n        )\n\n    return pattern$.pipe(map(args => (Array.isArray(args) ? args[0] : args)))\n  }\n}\n\ninterface DispatchMessageEventOptions {\n  message: Message\n  sender?: browser.runtime.MessageSender\n  sendResponse?: Function\n}\n\ninterface DispatchMessageEventOptionsGeneral\n  extends DispatchMessageEventOptions {\n  self?: boolean\n}\n\nfunction _dispatchMessageEvent(self: boolean) {\n  const _fn = dispatchMessageEvent\n  return function dispatchMessageEvent(options: DispatchMessageEventOptions) {\n    return _fn(Object.assign(options, { self }))\n  }\n}\n\nexport function dispatchMessageEvent(\n  options: DispatchMessageEventOptionsGeneral\n) {\n  const listeners = options.self ? messageSelfListeners : messageListeners\n  listeners.forEach(m => {\n    m.forEach((cb, type) => {\n      if (options.message.type === type) {\n        cb(options.message, options.sender || {}, options.sendResponse || noop)\n      }\n    })\n  })\n}\n"
  },
  {
    "path": "src/_helpers/__mocks__/config-manager.ts",
    "content": "import { AppConfig, getDefaultConfig } from '@/app-config'\n\nimport { Observable, fromEventPattern, of, concat } from 'rxjs'\nimport { map } from 'rxjs/operators'\n\nconst listeners = new Set<(changed: AppConfigChanged) => void>()\n\nexport interface AppConfigChanged {\n  newConfig: AppConfig\n  oldConfig?: AppConfig\n}\n\nexport const initConfig = jest.fn(() => Promise.resolve())\n\nexport const resetConfig = jest.fn(() => Promise.resolve())\n\nexport const getConfig = jest.fn(() => Promise.resolve(getDefaultConfig()))\n\nexport const updateConfig = jest.fn((config: AppConfig) => Promise.resolve())\n\nexport const addConfigListener = jest.fn(\n  (cb: (changed: AppConfigChanged) => void) => {\n    listeners.add(cb)\n  }\n)\n\n/**\n * Get AppConfig and create a stream listening config changing\n */\nexport const createConfigStream = jest.fn(\n  (): Observable<AppConfig> => {\n    return concat<AppConfig>(\n      of(getDefaultConfig()),\n      fromEventPattern<AppConfigChanged | [AppConfigChanged]>(handler =>\n        addConfigListener(handler)\n      ).pipe(map(args => (Array.isArray(args) ? args[0] : args).newConfig))\n    )\n  }\n)\n\nexport function dispatchConfigChangedEvent(\n  newConfig: AppConfig,\n  oldConfig?: AppConfig\n) {\n  listeners.forEach(cb => cb({ newConfig, oldConfig }))\n}\n"
  },
  {
    "path": "src/_helpers/__mocks__/selection.ts",
    "content": "export interface SelectionMock {\n  hasSelection: jest.Mock\n  getSelectionText: jest.Mock\n  getSelectionSentence: jest.Mock\n  getSelectionInfo: jest.Mock\n  getDefaultSelectionInfo: jest.Mock\n}\n\nmodule.exports = jest.genMockFromModule<SelectionMock>('../selection')\n"
  },
  {
    "path": "src/_helpers/analytics/events.ts",
    "content": "export type GAEventBase = {\n  category: string\n  action: string\n  label?: string\n  value?: string\n}\n\ntype GAEventFactory<T extends GAEventBase> = T\n\nexport type GAEvent = GAEventFactory<\n  | {\n      category: 'Page_Translate'\n      action: 'Open_Google' | 'Open_Youdao' | 'Open_Caiyun'\n      label:\n        | 'From_Browser_Action'\n        | 'From_Context_Menus'\n        | 'From_Browser_Shortcut'\n    }\n  | {\n      category: 'PDF_Viewer'\n      action: 'Open_PDF_Viewer'\n      label:\n        | 'From_Browser_Action'\n        | 'From_Context_Menus'\n        | 'From_Browser_Shortcut'\n    }\n>\n"
  },
  {
    "path": "src/_helpers/analytics/index.ts",
    "content": "import UAParser from 'ua-parser-js'\nimport axios from 'axios'\nimport uuid from 'uuid/v4'\nimport { message, storage } from '@/_helpers/browser-api'\nimport { genUniqueKey } from '@/_helpers/uniqueKey'\nimport { GAEvent, GAEventBase } from './events'\nimport { isBackgroundPage } from '../saladict'\n\nexport type GAParams = { [key: string]: string }\n\nexport async function reportPageView(page: string): Promise<void> {\n  const ua = new UAParser()\n  const browser = ua.getBrowser()\n  const os = ua.getOS()\n\n  try {\n    await requestGA({\n      t: 'pageview',\n      // required by pageview\n      dp: page,\n      // Dimensions\n      cd1: browser.name || 'None',\n      cd2: (browser.version || '0.0')\n        .split('.')\n        .slice(0, 3)\n        .join('.'),\n      cd3: os.name || 'None',\n      cd4: os.version || '0.0',\n      // Document Encoding\n      de: 'UTF-8',\n      // Document location URL\n      dl: document.location.href,\n      // Screen Colors\n      sd: screen.colorDepth + '-bit',\n      // Screen Resolution\n      sr: screen.width + 'x' + screen.height,\n      // User Language\n      ul: 'zh-cn'\n    })\n  } catch (error) {\n    if (!process.env.DEBUG) {\n      console.error('Report pageview error', error)\n    }\n  }\n}\n\nexport async function reportEvent(event: GAEvent) {\n  const params: GAParams = {\n    t: 'event',\n    ec: event.category,\n    ea: event.action\n  }\n\n  if ((event as GAEventBase).label != null) {\n    params.el = (event as GAEventBase).label!\n  }\n\n  if ((event as GAEventBase).value != null) {\n    params.ev = (event as GAEventBase).value!\n  }\n\n  try {\n    await requestGA(params)\n  } catch (error) {\n    if (!process.env.DEBUG) {\n      console.error('Report event error', error)\n    }\n  }\n}\n\nasync function requestGA(extraParams: GAParams) {\n  if (!isBackgroundPage()) {\n    return message.send({\n      type: 'REQUEST_GA',\n      payload: extraParams\n    })\n  }\n\n  if (\n    process.env.DEBUG ||\n    process.env.NODE_ENV === 'test' ||\n    process.env.NODE_ENV === 'development'\n  ) {\n    console.log('requestGA', extraParams)\n    return\n  }\n\n  let cid = (await storage.sync.get<{ gacid: string }>('gacid')).gacid\n  if (!cid) {\n    cid = uuid()\n    storage.sync.set({ gacid: cid })\n  }\n\n  return axios({\n    url: 'https://www.google-analytics.com/collect',\n    method: 'post',\n    headers: {\n      'content-type': 'text/plain;charset=UTF-8'\n    },\n    data: new URLSearchParams({\n      // required\n      v: '1',\n      tid: 'UA-49163616-4',\n      cid,\n      // Cache Buster\n      z: genUniqueKey(),\n      ...extraParams\n    })\n  })\n}\n\nexport function setupRequestGAListener() {\n  message.addListener('REQUEST_GA', ({ payload }) => {\n    requestGA(payload)\n  })\n}\n"
  },
  {
    "path": "src/_helpers/browser-api.ts",
    "content": "/**\n * @file Wraps some of the extension apis\n */\n\nimport { Observable, fromEventPattern } from 'rxjs'\nimport { map, filter } from 'rxjs/operators'\n\nimport { Message, MessageResponse, MsgType } from '@/typings/message'\nimport { Mutable } from '@/typings/helpers'\n\n/* --------------------------------------- *\\\n * #Types\n\\* --------------------------------------- */\n\nexport type StorageArea = 'all' | 'local' | 'sync'\n\nexport type StorageChange<T> = {\n  oldValue?: T\n  newValue?: T\n}\n\nexport type StorageUpdate<T> = {\n  oldValue?: T\n  newValue: T\n}\n\nexport type StorageListenerCb<T = any, K extends string = string> = (\n  changes: {\n    [field in K]: StorageChange<T>\n  },\n  areaName: string\n) => void\n\ntype onMessageEvent<T extends Message = Message> = (\n  message: T & { __pageId__?: string },\n  sender: browser.runtime.MessageSender\n) => Promise<any> | boolean | void\n\n/* --------------------------------------- *\\\n * #Globals\n\\* --------------------------------------- */\n\nconst noop = () => {\n  /* do nothing */\n}\n\n/**\n * key: {function} user's callback function\n * values: {Map} listeners, key: message type, values: generated or user's callback functions\n */\nconst messageListeners: WeakMap<\n  Function,\n  Map<MsgType | '__DEFAULT_MSGTYPE__', Function>\n> = new WeakMap()\n\n/**\n * For self page messaging\n * key: {function} user's callback function\n * values: {Map} listeners, key: message type, values: generated or user's callback functions\n */\nconst messageSelfListeners: WeakMap<\n  Function,\n  Map<MsgType | '__DEFAULT_MSGTYPE__', Function>\n> = new WeakMap()\n\n/**\n * key: {function} user's callback function\n * values: {Map} listeners, key: message type, values: generated or user's callback functions\n */\nconst storageListeners: WeakMap<\n  StorageListenerCb,\n  Map<string, StorageListenerCb>\n> = new WeakMap()\n\n/* --------------------------------------- *\\\n * #Exports\n\\* --------------------------------------- */\n\nexport const storage = {\n  sync: {\n    clear: storageClear,\n    remove: storageRemove,\n    get: storageGet,\n    set: storageSet,\n    /** Only for sync area */\n    addListener: storageAddListener,\n    /** Only for sync area */\n    removeListener: storageRemoveListener,\n    createStream: storageCreateStream,\n    get __storageArea__(): 'sync' {\n      return 'sync'\n    }\n  },\n  local: {\n    clear: storageClear,\n    remove: storageRemove,\n    get: storageGet,\n    set: storageSet,\n    /** Only for local area */\n    addListener: storageAddListener,\n    /** Only for local area */\n    removeListener: storageRemoveListener,\n    createStream: storageCreateStream,\n    get __storageArea__(): 'local' {\n      return 'local'\n    }\n  },\n  /** Clear all area */\n  clear: storageClear,\n  addListener: storageAddListener,\n  removeListener: storageRemoveListener,\n  createStream: storageCreateStream,\n  get __storageArea__(): 'all' {\n    return 'all'\n  }\n} as const\n\n/**\n * Wraps in-app runtime.sendMessage and tabs.sendMessage\n * Does not warp cross extension messaging!\n */\nexport const message = {\n  send: messageSend,\n  addListener: messageAddListener,\n  removeListener: messageRemoveListener,\n  createStream: messageCreateStream,\n  get __self__(): false {\n    return false\n  },\n\n  self: {\n    initClient,\n    initServer,\n    send: messageSendSelf,\n    addListener: messageAddListener,\n    removeListener: messageRemoveListener,\n    createStream: messageCreateStream,\n    get __self__(): true {\n      return true\n    }\n  }\n} as const\n\nexport interface OpenUrlOptions {\n  url: string\n  /** use browser.runtime.getURL? */\n  self?: boolean\n  /** focus the new tab? default true */\n  active?: boolean\n  /** ignore existing url? default true */\n  unique?: boolean\n}\n\n/**\n * Open a url on new tab or highlight a existing tab if already opened\n */\nexport async function openUrl(url: string, self?: boolean): Promise<void>\nexport async function openUrl(options: OpenUrlOptions): Promise<void>\nexport async function openUrl(\n  optionsOrUrl: string | OpenUrlOptions,\n  self?: boolean\n): Promise<void> {\n  const options: OpenUrlOptions =\n    typeof optionsOrUrl === 'string'\n      ? { url: optionsOrUrl, self }\n      : optionsOrUrl\n  const unique = options.unique !== false\n  const url = options.self ? browser.runtime.getURL(options.url) : options.url\n\n  if (unique) {\n    const tabs = await browser.tabs.query({ url }).catch(() => [])\n    if (tabs.length > 0) {\n      const { index, windowId } = tabs[0]\n      if (typeof browser.tabs['highlight'] === 'function') {\n        // Only Chrome supports tab.highlight for now\n        await browser.tabs['highlight']({ tabs: index, windowId })\n      }\n      await browser.windows.update(windowId!, { focused: true })\n      return\n    }\n  }\n\n  await browser.tabs.create({\n    url,\n    active: options.active !== false\n  })\n}\n\n/* --------------------------------------- *\\\n * #Storage\n\\* --------------------------------------- */\ntype StorageThisTwo = typeof storage.sync | typeof storage.local\ntype StorageThisThree = StorageThisTwo | typeof storage\n\nfunction storageClear(): Promise<void>\nfunction storageClear(this: StorageThisThree): Promise<void> {\n  return this.__storageArea__ === 'all'\n    ? Promise.all([\n        browser.storage.local.clear(),\n        browser.storage.sync.clear()\n      ]).then(noop)\n    : browser.storage[this.__storageArea__].clear()\n}\n\nfunction storageRemove(keys: string | string[]): Promise<void>\nfunction storageRemove(\n  this: StorageThisTwo,\n  keys: string | string[]\n): Promise<void> {\n  return browser.storage[this.__storageArea__].remove(keys)\n}\n\nfunction storageGet<T = any>(\n  key?: string | string[] | null\n): Promise<Partial<T>>\nfunction storageGet<T extends Object>(key: T | any): Promise<Partial<T>>\nfunction storageGet<T = any>(this: StorageThisTwo, ...args) {\n  return browser.storage[this.__storageArea__].get(...args) as Promise<\n    Partial<T>\n  >\n}\n\nfunction storageSet(keys: any): Promise<void>\nfunction storageSet(this: StorageThisTwo, keys: any): Promise<void> {\n  return browser.storage[this.__storageArea__].set(keys)\n}\n\nfunction storageAddListener<T = any>(cb: StorageListenerCb<T>): void\nfunction storageAddListener<T = any, K extends string = string>(\n  key: K,\n  cb: StorageListenerCb<T, K>\n): void\nfunction storageAddListener(this: StorageThisThree, ...args): void {\n  let key: string\n  let cb: StorageListenerCb\n  if (typeof args[0] === 'function') {\n    key = ''\n    cb = args[0]\n  } else if (typeof args[0] === 'string' && typeof args[1] === 'function') {\n    key = args[0]\n    cb = args[1]\n  } else {\n    throw new Error('wrong arguments type')\n  }\n\n  let listeners = storageListeners.get(cb)\n  if (!listeners) {\n    listeners = new Map()\n    storageListeners.set(cb, listeners)\n  }\n  const listenerKey = this.__storageArea__ + key\n  let listener = listeners.get(listenerKey)\n  if (!listener) {\n    listener = (changes, areaName) => {\n      if (\n        (this.__storageArea__ === 'all' || areaName === this.__storageArea__) &&\n        (!key || key in changes)\n      ) {\n        cb(changes, areaName)\n      }\n    }\n    listeners.set(listenerKey, listener)\n  }\n  return browser.storage.onChanged.addListener(listener)\n}\n\nfunction storageRemoveListener(key: string, cb: StorageListenerCb): void\nfunction storageRemoveListener(cb: StorageListenerCb): void\nfunction storageRemoveListener(this: StorageThisThree, ...args): void {\n  let key: string\n  let cb: StorageListenerCb\n  if (typeof args[0] === 'function') {\n    key = ''\n    cb = args[0]\n  } else if (typeof args[0] === 'string' && typeof args[1] === 'function') {\n    key = args[0]\n    cb = args[1]\n  } else {\n    throw new Error('wrong arguments type')\n  }\n\n  const listeners = storageListeners.get(cb)\n  if (listeners) {\n    if (key) {\n      // remove 'cb' listeners with 'key' under 'storageArea'\n      const listenerKey = this.__storageArea__ + key\n      const listener = listeners.get(listenerKey)\n      if (listener) {\n        browser.storage.onChanged.removeListener(listener)\n        listeners.delete(listenerKey)\n        if (listeners.size <= 0) {\n          storageListeners.delete(cb)\n        }\n        return\n      }\n    } else {\n      // remove all 'cb' listeners under 'storageArea'\n      listeners.forEach(listener => {\n        browser.storage.onChanged.removeListener(listener)\n      })\n      storageListeners.delete(cb)\n      return\n    }\n  }\n  browser.storage.onChanged.removeListener(cb)\n}\n\nfunction storageCreateStream<T = any>(key: string): Observable<StorageChange<T>>\nfunction storageCreateStream<T = any>(\n  this: StorageThisThree,\n  key: string\n): Observable<StorageChange<T>> {\n  if (!key) {\n    throw new Error('Missing key')\n  }\n  return fromEventPattern<StorageChange<T>>(\n    handler => this.addListener(key, handler as StorageListenerCb),\n    handler => this.removeListener(key, handler as StorageListenerCb)\n  ).pipe(\n    filter(args =>\n      Object.prototype.hasOwnProperty.call(\n        Array.isArray(args) ? args[0] : args,\n        key\n      )\n    ),\n    map(args => (Array.isArray(args) ? args[0][key] : args[key]))\n  )\n}\n\n/* --------------------------------------- *\\\n * #Message\n\\* --------------------------------------- */\ntype MessageThis = typeof message | typeof message.self\n\nfunction messageSend<T extends MsgType, R = MessageResponse<T>>(\n  message: Message<T>\n): Promise<R>\nfunction messageSend<T extends MsgType, R = MessageResponse<T>>(\n  tabId: number,\n  message: Message<T>\n): Promise<R>\nfunction messageSend<T extends MsgType>(\n  ...args: [Message<T>] | [number, Message<T>]\n): Promise<any> {\n  let callContext: Error\n  if (process.env.DEBUG) {\n    callContext = new Error('Message Call Context')\n  }\n  return (args.length === 1\n    ? browser.runtime.sendMessage(args[0])\n    : browser.tabs.sendMessage(args[0], args[1])\n  ).catch(err => {\n    if (process.env.DEBUG) {\n      console.warn(err.message, ...args, callContext)\n    }\n  })\n}\n\nasync function messageSendSelf<T extends MsgType, R = undefined>(\n  message: Message<T>\n): Promise<R extends undefined ? MessageResponse<T> : R> {\n  let callContext: Error\n  if (process.env.DEBUG) {\n    callContext = new Error('Message Call Context')\n  }\n\n  if (window.pageId === undefined) {\n    await initClient()\n  }\n  return browser.runtime\n    .sendMessage(\n      Object.assign({}, message, {\n        __pageId__: window.pageId,\n        type: `[[${message.type}]]`\n      })\n    )\n    .catch(err => {\n      if (process.env.DEBUG) {\n        console.warn(err.message, message, callContext)\n      }\n    })\n}\n\nfunction messageAddListener<T extends MsgType>(\n  messageType: T,\n  cb: onMessageEvent<Message<T>>\n): void\nfunction messageAddListener<T extends MsgType>(\n  cb: onMessageEvent<Message>\n): void\nfunction messageAddListener<T extends MsgType>(\n  this: MessageThis,\n  ...args: [T, onMessageEvent<Message<T>>] | [onMessageEvent<Message>]\n): void {\n  if (window.pageId === undefined) {\n    initClient()\n  }\n  const allListeners = this.__self__ ? messageSelfListeners : messageListeners\n  const messageType = args.length === 1 ? undefined : args[0]\n  const cb = args.length === 1 ? args[0] : args[1]\n  let listeners = allListeners.get(cb)\n  if (!listeners) {\n    listeners = new Map()\n    allListeners.set(cb, listeners)\n  }\n  let listener = listeners.get(messageType || '__DEFAULT_MSGTYPE__')\n  if (!listener) {\n    listener = ((message, sender) => {\n      if (\n        message &&\n        (this.__self__\n          ? window.pageId === message.__pageId__\n          : !message.__pageId__)\n      ) {\n        if (messageType == null || message.type === messageType) {\n          return cb(message as Message<T> & { __pageId__?: string }, sender)\n        }\n      }\n    }) as onMessageEvent\n    listeners.set(messageType || '__DEFAULT_MSGTYPE__', listener)\n  }\n  // object is handled\n  return browser.runtime.onMessage.addListener(listener as any)\n}\n\nfunction messageRemoveListener(\n  messageType: Message['type'],\n  cb: onMessageEvent\n): void\nfunction messageRemoveListener(cb: onMessageEvent): void\nfunction messageRemoveListener(\n  this: MessageThis,\n  ...args: [Message['type'], onMessageEvent] | [onMessageEvent]\n): void {\n  const allListeners = this.__self__ ? messageSelfListeners : messageListeners\n  const messageType = args.length === 1 ? undefined : args[0]\n  const cb = args.length === 1 ? args[0] : args[1]\n  const listeners = allListeners.get(cb)\n  if (listeners) {\n    if (messageType) {\n      const listener = listeners.get(messageType)\n      if (listener) {\n        // @ts-ignore\n        browser.runtime.onMessage.removeListener(listener)\n        listeners.delete(messageType)\n        if (listeners.size <= 0) {\n          allListeners.delete(cb)\n        }\n        return\n      }\n    } else {\n      // delete all cb related callbacks\n      listeners.forEach(listener =>\n        // @ts-ignore\n        browser.runtime.onMessage.removeListener(listener)\n      )\n      allListeners.delete(cb)\n      return\n    }\n  }\n  // @ts-ignore\n  browser.runtime.onMessage.removeListener(cb)\n}\n\nfunction messageCreateStream<T extends MsgType>(\n  messageType?: T\n): Observable<Message<T>>\nfunction messageCreateStream<T extends MsgType>(\n  this: MessageThis,\n  messageType?: T\n): Observable<Message<T>> {\n  const pattern$ = messageType\n    ? fromEventPattern<Message<T>>(\n        handler => this.addListener(messageType, handler),\n        handler => this.removeListener(messageType, handler)\n      )\n    : fromEventPattern<Message<T>>(\n        handler => this.addListener(handler),\n        handler => this.removeListener(handler)\n      )\n  // Arguments could be an array if there are multiple values emitted.\n  return pattern$.pipe(map(args => (Array.isArray(args) ? args[0] : args)))\n}\n\n/**\n * Deploy page script for self-messaging\n * This method is called on the first sendMessage\n */\nfunction initClient(): Promise<typeof window.pageId> {\n  if (window.pageId === undefined) {\n    return message\n      .send<'PAGE_INFO'>({ type: 'PAGE_INFO' })\n      .then(({ pageId, faviconURL, pageTitle, pageURL }) => {\n        window.pageId = pageId\n        window.faviconURL = faviconURL\n        if (pageTitle) {\n          window.pageTitle = pageTitle\n        }\n        if (pageURL) {\n          window.pageURL = pageURL\n        }\n        return pageId\n      })\n  } else {\n    return Promise.resolve(window.pageId)\n  }\n}\n\n/**\n * Deploy background proxy for self-messaging\n * This method should be invoked in background script\n */\nfunction initServer(): void {\n  window.pageId = 'background page'\n  const selfMsgTester = /^\\[\\[(.+)\\]\\]$/\n\n  browser.runtime.onMessage.addListener(\n    (message: object, sender: browser.runtime.MessageSender) => {\n      if (!message || !message['type']) {\n        return\n      }\n\n      if ((message as Message).type === 'PAGE_INFO') {\n        return Promise.resolve(_getPageInfo(sender))\n      }\n\n      const selfMsg = selfMsgTester.exec((message as Message).type)\n      if (selfMsg) {\n        ;(message as Mutable<Message>).type = selfMsg[1] as MsgType\n        const tabId = sender.tab && sender.tab.id\n        if (tabId) {\n          return messageSend(tabId, message as Message)\n        } else {\n          return messageSend(message as Message)\n        }\n      }\n    }\n  )\n}\n\nfunction _getPageInfo(sender: browser.runtime.MessageSender) {\n  const result = {\n    pageId: '' as string | number,\n    faviconURL: '',\n    pageTitle: '',\n    pageURL: ''\n  }\n  const tab = sender.tab\n  if (tab) {\n    result.pageId = tab.id || ''\n    if (tab.favIconUrl) {\n      result.faviconURL = tab.favIconUrl\n    }\n    if (tab.url) {\n      result.pageURL = tab.url\n    }\n    if (tab.title) {\n      result.pageTitle = tab.title\n    }\n  } else {\n    // FRAGILE: Assume only browser action page is tabless\n    result.pageId = 'popup'\n    if (sender.url && !sender.url.startsWith('http')) {\n      result.faviconURL = 'https://saladict.crimx.com/favicon.ico'\n    }\n  }\n  return result\n}\n"
  },
  {
    "path": "src/_helpers/check-update.ts",
    "content": "export interface ReleaseData {\n  version: string\n  data: string[]\n}\n\n/**\n * 3 major newer\n * 2 minor newer\n * 1 patch newer\n * 0 same version\n * -1 patch older\n * -2 minor older\n * -3 major older\n */\nexport type VersionDiff = number\n\nexport type ReleaseResponse = {\n  diff: VersionDiff\n  data?: ReleaseData\n}\n\nexport async function checkUpdate(\n  compareVersion?: string,\n  data?: ReleaseData\n): Promise<ReleaseResponse> {\n  if (!data) {\n    try {\n      const isZh = window.appConfig.langCode.startsWith('zh')\n      const response = await fetch(\n        `https://saladict.crimx.com/releases/${isZh ? 'chs' : 'eng'}.json`\n      )\n      data = await response.json()\n    } catch (e) {\n      console.error(e)\n    }\n  }\n\n  if (!data) {\n    return { diff: 0 }\n  }\n\n  if (!compareVersion) {\n    return { diff: 3, data }\n  }\n\n  const prev = compareVersion.split('.').map(Number)\n  const curr = data.version\n    .slice(1)\n    .split('.')\n    .map(Number)\n\n  for (let i = 0; i < 3; i++) {\n    if (curr[i] > prev[i]) {\n      return { diff: 3 - i, data }\n    }\n    if (curr[i] < prev[i]) {\n      return { diff: i - 3, data }\n    }\n  }\n\n  return { diff: 0, data }\n}\n"
  },
  {
    "path": "src/_helpers/chs-to-chz.ts",
    "content": "const charMap = new Map([\n  ['与', '與'],\n  ['丒', '囟'],\n  ['专', '專'],\n  ['丗', '卅'],\n  ['业', '業'],\n  ['丛', '叢'],\n  ['东', '東'],\n  ['丝', '絲'],\n  ['両', '兩'],\n  ['丢', '丟'],\n  ['两', '兩'],\n  ['严', '嚴'],\n  ['丧', '喪'],\n  ['个', '個'],\n  ['丬', '爿'],\n  ['丯', '丰'],\n  ['临', '臨'],\n  ['丶', '⼂'],\n  ['为', '為'],\n  ['丽', '麗'],\n  ['举', '舉'],\n  ['义', '義'],\n  ['乌', '烏'],\n  ['乐', '樂'],\n  ['乔', '喬'],\n  ['习', '習'],\n  ['乡', '鄉'],\n  ['书', '書'],\n  ['买', '買'],\n  ['乱', '亂'],\n  ['亀', '龜'],\n  ['亁', '乾'],\n  ['争', '爭'],\n  ['亏', '虧'],\n  ['亘', '亙'],\n  ['亚', '亞'],\n  ['产', '產'],\n  ['亩', '畝'],\n  ['亲', '親'],\n  ['亵', '褻'],\n  ['亸', '嚲'],\n  ['亻', '人'],\n  ['亿', '億'],\n  ['仅', '僅'],\n  ['从', '從'],\n  ['仑', '崙'],\n  ['仓', '倉'],\n  ['仪', '儀'],\n  ['们', '們'],\n  ['仮', '假'],\n  ['众', '眾'],\n  ['会', '會'],\n  ['伛', '傴'],\n  ['伞', '傘'],\n  ['伟', '偉'],\n  ['传', '傳'],\n  ['伤', '傷'],\n  ['伥', '倀'],\n  ['伦', '倫'],\n  ['伧', '傖'],\n  ['伪', '偽'],\n  ['伫', '佇'],\n  ['体', '體'],\n  ['佥', '僉'],\n  ['侠', '俠'],\n  ['侣', '侶'],\n  ['侥', '僥'],\n  ['侦', '偵'],\n  ['侧', '側'],\n  ['侨', '僑'],\n  ['侩', '儈'],\n  ['侪', '儕'],\n  ['侬', '儂'],\n  ['俣', '俁'],\n  ['俦', '儔'],\n  ['俨', '儼'],\n  ['俩', '倆'],\n  ['俪', '儷'],\n  ['俭', '儉'],\n  ['债', '債'],\n  ['倾', '傾'],\n  ['偬', '傯'],\n  ['偻', '僂'],\n  ['偾', '僨'],\n  ['偿', '償'],\n  ['傥', '儻'],\n  ['傧', '儐'],\n  ['储', '儲'],\n  ['傩', '儺'],\n  ['兎', '兔'],\n  ['兑', '兌'],\n  ['兖', '兗'],\n  ['兪', '俞'],\n  ['兰', '蘭'],\n  ['关', '關'],\n  ['兴', '興'],\n  ['兹', '茲'],\n  ['养', '養'],\n  ['兽', '獸'],\n  ['兾', '糞'],\n  ['兿', '藝'],\n  ['冁', '囅'],\n  ['内', '內'],\n  ['円', '丹'],\n  ['冈', '岡'],\n  ['册', '冊'],\n  ['写', '寫'],\n  ['军', '軍'],\n  ['农', '農'],\n  ['冝', '宜'],\n  ['冦', '寇'],\n  ['冧', '霖'],\n  ['冨', '富'],\n  ['冩', '寫'],\n  ['冮', '江'],\n  ['冯', '馮'],\n  ['冲', '沖'],\n  ['决', '決'],\n  ['况', '況'],\n  ['冸', '泮'],\n  ['冺', '泯'],\n  ['冻', '凍'],\n  ['冿', '津'],\n  ['净', '淨'],\n  ['凁', '涑'],\n  ['凂', '浼'],\n  ['凃', '涂'],\n  ['凄', '淒'],\n  ['凉', '涼'],\n  ['减', '減'],\n  ['凑', '湊'],\n  ['凒', '溰'],\n  ['凓', '溧'],\n  ['凕', '溟'],\n  ['凖', '準'],\n  ['凙', '澤'],\n  ['凛', '凜'],\n  ['凟', '瀆'],\n  ['凤', '鳳'],\n  ['凥', '尻'],\n  ['処', '處'],\n  ['凨', '云'],\n  ['凫', '鳧'],\n  ['凬', '凰'],\n  ['凭', '憑'],\n  ['凮', '鳳'],\n  ['凯', '凱'],\n  ['凴', '憑'],\n  ['击', '擊'],\n  ['凼', '窞'],\n  ['凾', '亟'],\n  ['凿', '鑿'],\n  ['刄', '刃'],\n  ['刅', '刃'],\n  ['刋', '刊'],\n  ['刍', '芻'],\n  ['刘', '劉'],\n  ['则', '則'],\n  ['刚', '剛'],\n  ['创', '創'],\n  ['删', '刪'],\n  ['刦', '劫'],\n  ['刧', '劫'],\n  ['别', '別'],\n  ['刭', '剄'],\n  ['刴', '剁'],\n  ['刹', '剎'],\n  ['刼', '劫'],\n  ['刽', '劊'],\n  ['刿', '劌'],\n  ['剀', '剴'],\n  ['剂', '劑'],\n  ['剐', '剮'],\n  ['剑', '劍'],\n  ['剥', '剝'],\n  ['剧', '劇'],\n  ['剰', '剩'],\n  ['劎', '劍'],\n  ['劒', '劍'],\n  ['劔', '劍'],\n  ['劝', '勸'],\n  ['办', '辦'],\n  ['务', '務'],\n  ['劢', '勱'],\n  ['动', '動'],\n  ['励', '勵'],\n  ['劲', '勁'],\n  ['劳', '勞'],\n  ['労', '勞'],\n  ['劵', '卷'],\n  ['効', '效'],\n  ['劽', '裂'],\n  ['势', '勢'],\n  ['勅', '敕'],\n  ['勋', '勛'],\n  ['勐', '猛'],\n  ['勚', '勩'],\n  ['勠', '戮'],\n  ['勥', '強'],\n  ['勧', '勸'],\n  ['匀', '勻'],\n  ['匦', '匭'],\n  ['匮', '匱'],\n  ['区', '區'],\n  ['医', '醫'],\n  ['华', '華'],\n  ['协', '協'],\n  ['单', '單'],\n  ['卖', '賣'],\n  ['単', '單'],\n  ['卙', '斟'],\n  ['卛', '攣'],\n  ['卟', '嚇'],\n  ['卢', '盧'],\n  ['卤', '鹵'],\n  ['卥', '囟'],\n  ['卧', '臥'],\n  ['卫', '衛'],\n  ['却', '卻'],\n  ['卺', '巹'],\n  ['厅', '廳'],\n  ['历', '歷'],\n  ['厉', '厲'],\n  ['压', '壓'],\n  ['厌', '厭'],\n  ['厕', '廁'],\n  ['厛', '廳'],\n  ['厠', '廁'],\n  ['厢', '廂'],\n  ['厣', '厴'],\n  ['厦', '廈'],\n  ['厨', '廚'],\n  ['厩', '廄'],\n  ['厮', '廝'],\n  ['厰', '廠'],\n  ['厳', '嚴'],\n  ['厶', '⼛'],\n  ['县', '縣'],\n  ['叁', '參'],\n  ['叄', '參'],\n  ['叆', '靉'],\n  ['叇', '靆'],\n  ['双', '雙'],\n  ['収', '收'],\n  ['叏', '發'],\n  ['叐', '發'],\n  ['发', '發'],\n  ['变', '變'],\n  ['叙', '敘'],\n  ['叠', '疊'],\n  ['叧', '另'],\n  ['叶', '葉'],\n  ['号', '號'],\n  ['叹', '嘆'],\n  ['叽', '嘰'],\n  ['吓', '嚇'],\n  ['吕', '呂'],\n  ['吖', '嗄'],\n  ['吗', '嗎'],\n  ['吣', '唚'],\n  ['吨', '噸'],\n  ['启', '啟'],\n  ['吴', '吳'],\n  ['吿', '告'],\n  ['呋', '咐'],\n  ['呐', '吶'],\n  ['呑', '吞'],\n  ['呒', '嘸'],\n  ['呓', '囈'],\n  ['呕', '嘔'],\n  ['呖', '嚦'],\n  ['呗', '唄'],\n  ['员', '員'],\n  ['呙', '咼'],\n  ['呛', '嗆'],\n  ['呜', '嗚'],\n  ['呪', '咒'],\n  ['咏', '詠'],\n  ['咙', '嚨'],\n  ['咛', '嚀'],\n  ['咝', '吱'],\n  ['咣', '光'],\n  ['咤', '吒'],\n  ['哌', '呱'],\n  ['响', '響'],\n  ['哐', '匡'],\n  ['哑', '啞'],\n  ['哒', '噠'],\n  ['哓', '嘵'],\n  ['哔', '嗶'],\n  ['哕', '噦'],\n  ['哗', '嘩'],\n  ['哙', '噲'],\n  ['哜', '嚌'],\n  ['哝', '噥'],\n  ['哟', '喲'],\n  ['唝', '嗊'],\n  ['唠', '嘮'],\n  ['唡', '啢'],\n  ['唢', '嗩'],\n  ['唣', '嗦'],\n  ['唤', '喚'],\n  ['唿', '呼'],\n  ['啧', '嘖'],\n  ['啬', '嗇'],\n  ['啭', '囀'],\n  ['啰', '囉'],\n  ['啴', '嘽'],\n  ['啸', '嘯'],\n  ['喷', '噴'],\n  ['喹', '奎'],\n  ['喽', '嘍'],\n  ['喾', '嚳'],\n  ['嗪', '唚'],\n  ['嗫', '囁'],\n  ['嗬', '呵'],\n  ['嗳', '噯'],\n  ['嗵', '通'],\n  ['嘘', '噓'],\n  ['嘞', '咧'],\n  ['嘠', '嘎'],\n  ['嘣', '迸'],\n  ['嘤', '嚶'],\n  ['嘨', '嘯'],\n  ['嘭', '膨'],\n  ['嘱', '囑'],\n  ['嘷', '嚎'],\n  ['噜', '嚕'],\n  ['噻', '塞'],\n  ['噼', '劈'],\n  ['嚔', '涕'],\n  ['嚢', '囊'],\n  ['嚣', '囂'],\n  ['嚯', '謔'],\n  ['团', '團'],\n  ['园', '園'],\n  ['囱', '囪'],\n  ['围', '圍'],\n  ['囵', '圇'],\n  ['国', '國'],\n  ['图', '圖'],\n  ['圆', '圓'],\n  ['圣', '聖'],\n  ['圹', '壙'],\n  ['场', '場'],\n  ['块', '塊'],\n  ['坚', '堅'],\n  ['坛', '壇'],\n  ['坜', '壢'],\n  ['坝', '壩'],\n  ['坞', '塢'],\n  ['坟', '墳'],\n  ['坠', '墜'],\n  ['垄', '壟'],\n  ['垅', '壟'],\n  ['垆', '壚'],\n  ['垒', '壘'],\n  ['垦', '墾'],\n  ['垧', '坰'],\n  ['垩', '堊'],\n  ['垫', '墊'],\n  ['垲', '塏'],\n  ['垴', '瑙'],\n  ['埘', '塒'],\n  ['埚', '堝'],\n  ['堑', '塹'],\n  ['堕', '墮'],\n  ['塡', '填'],\n  ['塬', '原'],\n  ['墙', '牆'],\n  ['壮', '壯'],\n  ['声', '聲'],\n  ['壳', '殼'],\n  ['壶', '壺'],\n  ['壸', '壼'],\n  ['夂', '⼢'],\n  ['处', '處'],\n  ['备', '備'],\n  ['夊', '⼢'],\n  ['够', '夠'],\n  ['头', '頭'],\n  ['夹', '夾'],\n  ['夺', '奪'],\n  ['奁', '奩'],\n  ['奂', '奐'],\n  ['奋', '奮'],\n  ['奖', '獎'],\n  ['奥', '奧'],\n  ['妆', '妝'],\n  ['妇', '婦'],\n  ['妈', '媽'],\n  ['妩', '嫵'],\n  ['妪', '嫗'],\n  ['妫', '媯'],\n  ['姗', '姍'],\n  ['姹', '奼'],\n  ['娄', '婁'],\n  ['娅', '婭'],\n  ['娆', '嬈'],\n  ['娇', '嬌'],\n  ['娈', '孌'],\n  ['娱', '娛'],\n  ['娲', '媧'],\n  ['娴', '嫻'],\n  ['婳', '嫿'],\n  ['婴', '嬰'],\n  ['婵', '嬋'],\n  ['婶', '嬸'],\n  ['媪', '媼'],\n  ['嫒', '嬡'],\n  ['嫔', '嬪'],\n  ['嫱', '嬙'],\n  ['嬷', '嬤'],\n  ['孙', '孫'],\n  ['学', '學'],\n  ['孪', '孿'],\n  ['孶', '孳'],\n  ['宝', '寶'],\n  ['实', '實'],\n  ['宠', '寵'],\n  ['审', '審'],\n  ['宪', '憲'],\n  ['宫', '宮'],\n  ['宽', '寬'],\n  ['宾', '賓'],\n  ['寝', '寢'],\n  ['对', '對'],\n  ['寻', '尋'],\n  ['导', '導'],\n  ['対', '對'],\n  ['寿', '壽'],\n  ['専', '專'],\n  ['尅', '剋'],\n  ['将', '將'],\n  ['尓', '爾'],\n  ['尔', '爾'],\n  ['尘', '塵'],\n  ['尝', '嘗'],\n  ['尧', '堯'],\n  ['尴', '尷'],\n  ['尽', '盡'],\n  ['层', '層'],\n  ['屃', '屭'],\n  ['屉', '屜'],\n  ['届', '屆'],\n  ['屛', '屏'],\n  ['属', '屬'],\n  ['屡', '屢'],\n  ['屦', '屨'],\n  ['屿', '嶼'],\n  ['岁', '歲'],\n  ['岂', '豈'],\n  ['岖', '嶇'],\n  ['岗', '崗'],\n  ['岘', '峴'],\n  ['岙', '嶴'],\n  ['岚', '嵐'],\n  ['岛', '島'],\n  ['岭', '嶺'],\n  ['岿', '巋'],\n  ['峄', '嶧'],\n  ['峡', '峽'],\n  ['峣', '嶢'],\n  ['峤', '嶠'],\n  ['峥', '崢'],\n  ['峦', '巒'],\n  ['峯', '峰'],\n  ['崂', '嶗'],\n  ['崃', '崍'],\n  ['崄', '嶮'],\n  ['崭', '嶄'],\n  ['崾', '要'],\n  ['嵘', '嶸'],\n  ['嵚', '嶔'],\n  ['嵝', '嶁'],\n  ['巄', '巃'],\n  ['巅', '巔'],\n  ['巌', '巖'],\n  ['巓', '巔'],\n  ['巩', '鞏'],\n  ['币', '幣'],\n  ['帅', '帥'],\n  ['师', '師'],\n  ['帏', '幃'],\n  ['帐', '帳'],\n  ['帜', '幟'],\n  ['带', '帶'],\n  ['帧', '幀'],\n  ['帮', '幫'],\n  ['帯', '帶'],\n  ['帱', '幬'],\n  ['帻', '幘'],\n  ['帼', '幗'],\n  ['幂', '冪'],\n  ['幇', '幫'],\n  ['幚', '幫'],\n  ['幞', '襆'],\n  ['幷', '并'],\n  ['广', '廣'],\n  ['庁', '廳'],\n  ['広', '麼'],\n  ['庄', '莊'],\n  ['庅', '麼'],\n  ['庆', '慶'],\n  ['庐', '廬'],\n  ['庑', '廡'],\n  ['库', '庫'],\n  ['应', '應'],\n  ['庙', '廟'],\n  ['庞', '龐'],\n  ['废', '廢'],\n  ['庼', '廎'],\n  ['廏', '廄'],\n  ['廐', '廄'],\n  ['廪', '廩'],\n  ['廴', '⼵'],\n  ['廵', '巡'],\n  ['开', '開'],\n  ['异', '異'],\n  ['弃', '棄'],\n  ['弑', '弒'],\n  ['张', '張'],\n  ['弥', '彌'],\n  ['弯', '彎'],\n  ['弹', '彈'],\n  ['强', '強'],\n  ['归', '歸'],\n  ['当', '當'],\n  ['录', '錄'],\n  ['彚', '彙'],\n  ['彛', '羿'],\n  ['彜', '羿'],\n  ['彟', '獲'],\n  ['彠', '獲'],\n  ['彡', '⼺'],\n  ['彦', '彥'],\n  ['彻', '徹'],\n  ['径', '徑'],\n  ['徕', '徠'],\n  ['徸', '德'],\n  ['忄', '心'],\n  ['忆', '憶'],\n  ['忏', '懺'],\n  ['忧', '憂'],\n  ['忾', '愾'],\n  ['怀', '懷'],\n  ['态', '態'],\n  ['怂', '慫'],\n  ['怃', '憮'],\n  ['怅', '悵'],\n  ['怆', '愴'],\n  ['怜', '憐'],\n  ['总', '總'],\n  ['怼', '懟'],\n  ['怿', '懌'],\n  ['恋', '戀'],\n  ['恒', '恆'],\n  ['恳', '懇'],\n  ['恶', '惡'],\n  ['恸', '慟'],\n  ['恹', '懨'],\n  ['恺', '愷'],\n  ['恻', '惻'],\n  ['恼', '惱'],\n  ['恽', '惲'],\n  ['悦', '悅'],\n  ['悫', '愨'],\n  ['悬', '懸'],\n  ['悭', '慳'],\n  ['悯', '憫'],\n  ['惊', '驚'],\n  ['惧', '懼'],\n  ['惨', '慘'],\n  ['惩', '懲'],\n  ['惫', '憊'],\n  ['惬', '愜'],\n  ['惭', '慚'],\n  ['惮', '憚'],\n  ['惯', '慣'],\n  ['惽', '惛'],\n  ['愠', '慍'],\n  ['愤', '憤'],\n  ['愦', '憒'],\n  ['慑', '懾'],\n  ['慭', '憖'],\n  ['懑', '懣'],\n  ['懒', '懶'],\n  ['懔', '懍'],\n  ['懴', '懺'],\n  ['戅', '戇'],\n  ['戆', '戇'],\n  ['戋', '戔'],\n  ['戏', '戲'],\n  ['戗', '戧'],\n  ['战', '戰'],\n  ['戝', '敗'],\n  ['戦', '戰'],\n  ['戬', '戩'],\n  ['戯', '戲'],\n  ['戱', '戲'],\n  ['户', '戶'],\n  ['戸', '戶'],\n  ['扌', '手'],\n  ['执', '執'],\n  ['扩', '擴'],\n  ['扪', '捫'],\n  ['扫', '掃'],\n  ['扬', '揚'],\n  ['扰', '擾'],\n  ['抅', '拘'],\n  ['抚', '撫'],\n  ['抛', '拋'],\n  ['抟', '摶'],\n  ['抠', '摳'],\n  ['抡', '掄'],\n  ['抢', '搶'],\n  ['护', '護'],\n  ['报', '報'],\n  ['担', '擔'],\n  ['拟', '擬'],\n  ['拢', '攏'],\n  ['拣', '揀'],\n  ['拥', '擁'],\n  ['拦', '攔'],\n  ['拧', '擰'],\n  ['拨', '撥'],\n  ['择', '擇'],\n  ['挚', '摯'],\n  ['挛', '攣'],\n  ['挜', '掗'],\n  ['挝', '撾'],\n  ['挞', '撻'],\n  ['挟', '挾'],\n  ['挠', '撓'],\n  ['挡', '擋'],\n  ['挢', '撟'],\n  ['挣', '掙'],\n  ['挤', '擠'],\n  ['挥', '揮'],\n  ['挦', '撏'],\n  ['捞', '撈'],\n  ['损', '損'],\n  ['捡', '撿'],\n  ['换', '換'],\n  ['捣', '搗'],\n  ['掳', '擄'],\n  ['掴', '摑'],\n  ['掷', '擲'],\n  ['掸', '撣'],\n  ['掺', '摻'],\n  ['掼', '摜'],\n  ['揸', '喳'],\n  ['揽', '攬'],\n  ['揿', '撳'],\n  ['搀', '攙'],\n  ['搁', '擱'],\n  ['搂', '摟'],\n  ['搃', '摠'],\n  ['搅', '攪'],\n  ['携', '攜'],\n  ['摄', '攝'],\n  ['摅', '攄'],\n  ['摆', '擺'],\n  ['摇', '搖'],\n  ['摈', '擯'],\n  ['摊', '攤'],\n  ['撃', '擊'],\n  ['撄', '攖'],\n  ['撑', '撐'],\n  ['撪', '攆'],\n  ['撵', '攆'],\n  ['撷', '擷'],\n  ['撹', '攪'],\n  ['撺', '攛'],\n  ['擕', '攜'],\n  ['擞', '擻'],\n  ['擡', '抬'],\n  ['擥', '掔'],\n  ['擧', '舉'],\n  ['擪', '壓'],\n  ['攒', '攢'],\n  ['攵', '又'],\n  ['敇', '敕'],\n  ['敌', '敵'],\n  ['敛', '斂'],\n  ['敮', '歃'],\n  ['数', '數'],\n  ['斉', '齊'],\n  ['斋', '齋'],\n  ['斎', '齋'],\n  ['斓', '斕'],\n  ['斩', '斬'],\n  ['断', '斷'],\n  ['旧', '舊'],\n  ['时', '時'],\n  ['旷', '曠'],\n  ['旸', '暘'],\n  ['昙', '曇'],\n  ['昼', '晝'],\n  ['昽', '曨'],\n  ['显', '顯'],\n  ['晋', '晉'],\n  ['晓', '曉'],\n  ['晔', '曄'],\n  ['晕', '暈'],\n  ['晖', '暉'],\n  ['暂', '暫'],\n  ['暧', '曖'],\n  ['术', '術'],\n  ['杀', '殺'],\n  ['杂', '雜'],\n  ['权', '權'],\n  ['条', '條'],\n  ['来', '來'],\n  ['杨', '楊'],\n  ['极', '極'],\n  ['枞', '樅'],\n  ['枢', '樞'],\n  ['枣', '棗'],\n  ['枥', '櫪'],\n  ['枧', '見'],\n  ['枨', '棖'],\n  ['枪', '槍'],\n  ['枫', '楓'],\n  ['枭', '梟'],\n  ['柠', '檸'],\n  ['柽', '檉'],\n  ['栀', '梔'],\n  ['栅', '柵'],\n  ['标', '標'],\n  ['栈', '棧'],\n  ['栉', '櫛'],\n  ['栊', '櫳'],\n  ['栋', '棟'],\n  ['栌', '櫨'],\n  ['栎', '櫟'],\n  ['栏', '欄'],\n  ['树', '樹'],\n  ['样', '樣'],\n  ['栾', '欒'],\n  ['桊', '棬'],\n  ['桠', '椏'],\n  ['桡', '橈'],\n  ['桢', '楨'],\n  ['档', '檔'],\n  ['桤', '榿'],\n  ['桥', '橋'],\n  ['桦', '樺'],\n  ['桧', '檜'],\n  ['桨', '槳'],\n  ['桩', '樁'],\n  ['梦', '夢'],\n  ['梼', '檮'],\n  ['梾', '棶'],\n  ['检', '檢'],\n  ['棂', '欞'],\n  ['椁', '槨'],\n  ['椟', '櫝'],\n  ['椠', '槧'],\n  ['椭', '橢'],\n  ['楼', '樓'],\n  ['楽', '樂'],\n  ['榄', '欖'],\n  ['榇', '櫬'],\n  ['榈', '櫚'],\n  ['榉', '櫸'],\n  ['榘', '矩'],\n  ['槚', '檟'],\n  ['槛', '檻'],\n  ['槟', '檳'],\n  ['槠', '櫧'],\n  ['横', '橫'],\n  ['樯', '檣'],\n  ['樱', '櫻'],\n  ['橥', '櫫'],\n  ['橱', '櫥'],\n  ['橹', '櫓'],\n  ['橼', '櫞'],\n  ['檪', '櫟'],\n  ['檫', '察'],\n  ['欢', '歡'],\n  ['欤', '歟'],\n  ['欧', '歐'],\n  ['歳', '歲'],\n  ['歴', '曆'],\n  ['歺', '歲'],\n  ['歼', '殲'],\n  ['殁', '歿'],\n  ['殇', '殤'],\n  ['残', '殘'],\n  ['殒', '殞'],\n  ['殓', '殮'],\n  ['殚', '殫'],\n  ['殡', '殯'],\n  ['殱', '殲'],\n  ['殴', '毆'],\n  ['毁', '毀'],\n  ['毂', '轂'],\n  ['毕', '畢'],\n  ['毙', '斃'],\n  ['毡', '氈'],\n  ['毵', '毿'],\n  ['毶', '鞠'],\n  ['気', '氣'],\n  ['氢', '氫'],\n  ['氩', '氬'],\n  ['氲', '氳'],\n  ['氵', '水'],\n  ['氽', '汆'],\n  ['汇', '匯'],\n  ['汉', '漢'],\n  ['污', '汙'],\n  ['汤', '湯'],\n  ['汹', '洶'],\n  ['沟', '溝'],\n  ['没', '沒'],\n  ['沣', '灃'],\n  ['沤', '漚'],\n  ['沥', '瀝'],\n  ['沦', '淪'],\n  ['沧', '滄'],\n  ['沨', '渢'],\n  ['沩', '溈'],\n  ['沪', '滬'],\n  ['沵', '濔'],\n  ['泞', '濘'],\n  ['泪', '淚'],\n  ['泶', '澩'],\n  ['泷', '瀧'],\n  ['泸', '瀘'],\n  ['泺', '濼'],\n  ['泻', '瀉'],\n  ['泼', '潑'],\n  ['泽', '澤'],\n  ['泾', '涇'],\n  ['洁', '潔'],\n  ['浃', '浹'],\n  ['浅', '淺'],\n  ['浆', '漿'],\n  ['浇', '澆'],\n  ['浈', '湞'],\n  ['浊', '濁'],\n  ['测', '測'],\n  ['浍', '澮'],\n  ['济', '濟'],\n  ['浏', '瀏'],\n  ['浐', '滻'],\n  ['浑', '渾'],\n  ['浒', '滸'],\n  ['浓', '濃'],\n  ['浔', '潯'],\n  ['浕', '濜'],\n  ['浜', '濱'],\n  ['涙', '淚'],\n  ['涛', '濤'],\n  ['涝', '澇'],\n  ['涞', '淶'],\n  ['涟', '漣'],\n  ['涡', '渦'],\n  ['涣', '渙'],\n  ['涤', '滌'],\n  ['润', '潤'],\n  ['涧', '澗'],\n  ['涨', '漲'],\n  ['涩', '澀'],\n  ['淀', '澱'],\n  ['渊', '淵'],\n  ['渌', '淥'],\n  ['渍', '漬'],\n  ['渎', '瀆'],\n  ['渐', '漸'],\n  ['渑', '澠'],\n  ['渔', '漁'],\n  ['渖', '瀋'],\n  ['渗', '滲'],\n  ['温', '溫'],\n  ['湼', '涅'],\n  ['湾', '灣'],\n  ['湿', '濕'],\n  ['溃', '潰'],\n  ['溅', '濺'],\n  ['溆', '漵'],\n  ['溇', '漊'],\n  ['滙', '匯'],\n  ['滚', '滾'],\n  ['滝', '瀧'],\n  ['滞', '滯'],\n  ['滟', '灩'],\n  ['滠', '灄'],\n  ['满', '滿'],\n  ['滢', '瀅'],\n  ['滤', '濾'],\n  ['滥', '濫'],\n  ['滦', '灤'],\n  ['滨', '濱'],\n  ['滩', '灘'],\n  ['滪', '澦'],\n  ['漑', '溉'],\n  ['潆', '瀠'],\n  ['潇', '瀟'],\n  ['潋', '瀲'],\n  ['潍', '濰'],\n  ['潜', '潛'],\n  ['潴', '瀦'],\n  ['澜', '瀾'],\n  ['濑', '瀨'],\n  ['濒', '瀕'],\n  ['灎', '灩'],\n  ['灏', '灝'],\n  ['灔', '灩'],\n  ['灜', '瀛'],\n  ['灧', '灩'],\n  ['灬', '火'],\n  ['灭', '滅'],\n  ['灯', '燈'],\n  ['灵', '靈'],\n  ['灾', '災'],\n  ['灿', '燦'],\n  ['炀', '煬'],\n  ['炉', '爐'],\n  ['炖', '燉'],\n  ['炜', '煒'],\n  ['炝', '熗'],\n  ['点', '點'],\n  ['炼', '煉'],\n  ['炽', '熾'],\n  ['烁', '爍'],\n  ['烂', '爛'],\n  ['烃', '烴'],\n  ['烛', '燭'],\n  ['烟', '煙'],\n  ['烦', '煩'],\n  ['烧', '燒'],\n  ['烨', '燁'],\n  ['烩', '燴'],\n  ['烫', '燙'],\n  ['烬', '燼'],\n  ['热', '熱'],\n  ['焕', '煥'],\n  ['焖', '燜'],\n  ['焘', '燾'],\n  ['煅', '煆'],\n  ['煳', '糊'],\n  ['煺', '退'],\n  ['熘', '溜'],\n  ['爱', '愛'],\n  ['爲', '為'],\n  ['爷', '爺'],\n  ['牍', '牘'],\n  ['牜', '牛'],\n  ['牦', '犛'],\n  ['牵', '牽'],\n  ['牺', '犧'],\n  ['犊', '犢'],\n  ['犟', '強'],\n  ['犭', '犬'],\n  ['状', '狀'],\n  ['犷', '獷'],\n  ['犸', '馬'],\n  ['犹', '猶'],\n  ['狈', '狽'],\n  ['狍', '包'],\n  ['狝', '獮'],\n  ['狞', '獰'],\n  ['独', '獨'],\n  ['狭', '狹'],\n  ['狮', '獅'],\n  ['狯', '獪'],\n  ['狰', '猙'],\n  ['狱', '獄'],\n  ['狲', '猻'],\n  ['猃', '獫'],\n  ['猎', '獵'],\n  ['猕', '獼'],\n  ['猡', '玀'],\n  ['猪', '豬'],\n  ['猫', '貓'],\n  ['猬', '蝟'],\n  ['献', '獻'],\n  ['獭', '獺'],\n  ['玑', '璣'],\n  ['玙', '璵'],\n  ['玚', '瑒'],\n  ['玛', '瑪'],\n  ['玮', '瑋'],\n  ['环', '環'],\n  ['现', '現'],\n  ['玱', '瑲'],\n  ['玺', '璽'],\n  ['珏', '玨'],\n  ['珐', '琺'],\n  ['珑', '瓏'],\n  ['珰', '璫'],\n  ['珱', '瓔'],\n  ['珲', '琿'],\n  ['琏', '璉'],\n  ['琐', '瑣'],\n  ['琼', '瓊'],\n  ['瑶', '瑤'],\n  ['瑷', '璦'],\n  ['璎', '瓔'],\n  ['瓒', '瓚'],\n  ['瓯', '甌'],\n  ['産', '產'],\n  ['电', '電'],\n  ['画', '畫'],\n  ['畅', '暢'],\n  ['畲', '畬'],\n  ['畳', '疊'],\n  ['畴', '疇'],\n  ['畵', '畫'],\n  ['疎', '疏'],\n  ['疖', '癤'],\n  ['疗', '療'],\n  ['疟', '瘧'],\n  ['疠', '癘'],\n  ['疡', '瘍'],\n  ['疬', '癆'],\n  ['疮', '瘡'],\n  ['疯', '瘋'],\n  ['疴', '痾'],\n  ['痈', '癰'],\n  ['痉', '痙'],\n  ['痖', '啞'],\n  ['痨', '癆'],\n  ['痩', '瘦'],\n  ['痪', '瘓'],\n  ['痫', '癇'],\n  ['痬', '瘍'],\n  ['瘅', '癉'],\n  ['瘆', '疹'],\n  ['瘗', '瘞'],\n  ['瘘', '瘺'],\n  ['瘪', '癟'],\n  ['瘫', '癱'],\n  ['瘾', '癮'],\n  ['瘿', '癭'],\n  ['癀', '廣'],\n  ['癍', '斑'],\n  ['癎', '癇'],\n  ['癞', '癩'],\n  ['癣', '癬'],\n  ['癫', '癲'],\n  ['発', '發'],\n  ['皑', '皚'],\n  ['皱', '皺'],\n  ['皲', '皸'],\n  ['盏', '盞'],\n  ['盐', '鹽'],\n  ['监', '監'],\n  ['盖', '蓋'],\n  ['盗', '盜'],\n  ['盘', '盤'],\n  ['県', '縣'],\n  ['眍', '區'],\n  ['眞', '真'],\n  ['眦', '眥'],\n  ['眬', '矓'],\n  ['着', '著'],\n  ['睁', '睜'],\n  ['睐', '睞'],\n  ['睑', '瞼'],\n  ['瞒', '瞞'],\n  ['瞩', '矚'],\n  ['矤', '病'],\n  ['矫', '矯'],\n  ['矶', '磯'],\n  ['矾', '礬'],\n  ['矿', '礦'],\n  ['砀', '碭'],\n  ['码', '碼'],\n  ['砖', '磚'],\n  ['砗', '硨'],\n  ['砚', '硯'],\n  ['砜', '風'],\n  ['砺', '礪'],\n  ['砻', '礱'],\n  ['砾', '礫'],\n  ['础', '礎'],\n  ['硁', '硜'],\n  ['硕', '碩'],\n  ['硖', '硤'],\n  ['硗', '磽'],\n  ['硙', '磑'],\n  ['硚', '礄'],\n  ['硷', '鹼'],\n  ['碍', '礙'],\n  ['碛', '磧'],\n  ['碜', '磣'],\n  ['碱', '鹼'],\n  ['碹', '宣'],\n  ['磙', '袞'],\n  ['礻', '示'],\n  ['礼', '禮'],\n  ['祎', '禕'],\n  ['祢', '禰'],\n  ['祯', '禎'],\n  ['祷', '禱'],\n  ['祸', '禍'],\n  ['禀', '稟'],\n  ['禄', '祿'],\n  ['禅', '禪'],\n  ['离', '離'],\n  ['秃', '禿'],\n  ['秆', '稈'],\n  ['积', '積'],\n  ['称', '稱'],\n  ['秽', '穢'],\n  ['秾', '穠'],\n  ['税', '稅'],\n  ['稣', '穌'],\n  ['稳', '穩'],\n  ['穑', '穡'],\n  ['穷', '窮'],\n  ['窃', '竊'],\n  ['窍', '竅'],\n  ['窑', '窯'],\n  ['窜', '竄'],\n  ['窝', '窩'],\n  ['窥', '窺'],\n  ['窦', '竇'],\n  ['窭', '窶'],\n  ['竖', '豎'],\n  ['竜', '龍'],\n  ['竞', '競'],\n  ['笃', '篤'],\n  ['笋', '筍'],\n  ['笔', '筆'],\n  ['笕', '筧'],\n  ['笺', '箋'],\n  ['笼', '籠'],\n  ['笾', '籩'],\n  ['筚', '篳'],\n  ['筛', '篩'],\n  ['筜', '簹'],\n  ['筝', '箏'],\n  ['筹', '籌'],\n  ['签', '簽'],\n  ['简', '簡'],\n  ['箓', '籙'],\n  ['箢', '宛'],\n  ['箦', '簀'],\n  ['箧', '篋'],\n  ['箨', '籜'],\n  ['箩', '籮'],\n  ['箪', '簞'],\n  ['箫', '簫'],\n  ['篑', '簣'],\n  ['篓', '簍'],\n  ['篮', '籃'],\n  ['篱', '籬'],\n  ['簖', '籪'],\n  ['籁', '籟'],\n  ['籴', '糴'],\n  ['类', '類'],\n  ['籼', '秈'],\n  ['粜', '糶'],\n  ['粝', '糲'],\n  ['粤', '粵'],\n  ['粪', '糞'],\n  ['粮', '糧'],\n  ['糁', '糝'],\n  ['糇', '餱'],\n  ['糹', '糸'],\n  ['紧', '緊'],\n  ['絵', '繪'],\n  ['絶', '絕'],\n  ['絷', '縶'],\n  ['綘', '健'],\n  ['継', '繼'],\n  ['続', '續'],\n  ['緜', '綿'],\n  ['縂', '總'],\n  ['縄', '繩'],\n  ['繋', '繫'],\n  ['繍', '繡'],\n  ['纟', '糸'],\n  ['纠', '糾'],\n  ['纡', '紆'],\n  ['红', '紅'],\n  ['纣', '紂'],\n  ['纤', '纖'],\n  ['纥', '紇'],\n  ['约', '約'],\n  ['级', '級'],\n  ['纨', '紈'],\n  ['纩', '纊'],\n  ['纪', '紀'],\n  ['纫', '紉'],\n  ['纬', '緯'],\n  ['纭', '紜'],\n  ['纮', '紘'],\n  ['纯', '純'],\n  ['纰', '紕'],\n  ['纱', '紗'],\n  ['纲', '綱'],\n  ['纳', '納'],\n  ['纴', '紝'],\n  ['纵', '縱'],\n  ['纶', '綸'],\n  ['纷', '紛'],\n  ['纸', '紙'],\n  ['纹', '紋'],\n  ['纺', '紡'],\n  ['纻', '紵'],\n  ['纼', '紖'],\n  ['纽', '紐'],\n  ['纾', '紓'],\n  ['线', '線'],\n  ['绀', '紺'],\n  ['绁', '紲'],\n  ['绂', '紱'],\n  ['练', '練'],\n  ['组', '組'],\n  ['绅', '紳'],\n  ['细', '細'],\n  ['织', '織'],\n  ['终', '終'],\n  ['绉', '縐'],\n  ['绊', '絆'],\n  ['绋', '紼'],\n  ['绌', '絀'],\n  ['绍', '紹'],\n  ['绎', '繹'],\n  ['经', '經'],\n  ['绐', '紿'],\n  ['绑', '綁'],\n  ['绒', '絨'],\n  ['结', '結'],\n  ['绔', '褲'],\n  ['绕', '繞'],\n  ['绖', '絰'],\n  ['绗', '絎'],\n  ['绘', '繪'],\n  ['给', '給'],\n  ['绚', '絢'],\n  ['绛', '絳'],\n  ['络', '絡'],\n  ['绝', '絕'],\n  ['绞', '絞'],\n  ['统', '統'],\n  ['绠', '綆'],\n  ['绡', '綃'],\n  ['绢', '絹'],\n  ['绣', '繡'],\n  ['绤', '綌'],\n  ['绥', '綏'],\n  ['绦', '絛'],\n  ['继', '繼'],\n  ['绨', '綈'],\n  ['绩', '績'],\n  ['绪', '緒'],\n  ['绫', '綾'],\n  ['续', '續'],\n  ['绮', '綺'],\n  ['绯', '緋'],\n  ['绰', '綽'],\n  ['绱', '鞜'],\n  ['绲', '緄'],\n  ['绳', '繩'],\n  ['维', '維'],\n  ['绵', '綿'],\n  ['绶', '綬'],\n  ['绷', '繃'],\n  ['绸', '綢'],\n  ['绹', '綯'],\n  ['绺', '綹'],\n  ['绻', '綣'],\n  ['综', '綜'],\n  ['绽', '綻'],\n  ['绾', '綰'],\n  ['绿', '綠'],\n  ['缀', '綴'],\n  ['缁', '緇'],\n  ['缂', '緙'],\n  ['缃', '緗'],\n  ['缄', '緘'],\n  ['缅', '緬'],\n  ['缆', '纜'],\n  ['缇', '緹'],\n  ['缈', '緲'],\n  ['缉', '緝'],\n  ['缊', '縕'],\n  ['缋', '繢'],\n  ['缌', '緦'],\n  ['缍', '綞'],\n  ['缎', '緞'],\n  ['缏', '緶'],\n  ['缐', '線'],\n  ['缑', '緱'],\n  ['缒', '縋'],\n  ['缓', '緩'],\n  ['缔', '締'],\n  ['缕', '縷'],\n  ['编', '編'],\n  ['缗', '緡'],\n  ['缘', '緣'],\n  ['缙', '縉'],\n  ['缚', '縛'],\n  ['缛', '縟'],\n  ['缜', '縝'],\n  ['缝', '縫'],\n  ['缞', '縗'],\n  ['缟', '縞'],\n  ['缠', '纏'],\n  ['缡', '縭'],\n  ['缢', '縊'],\n  ['缣', '縑'],\n  ['缤', '繽'],\n  ['缥', '縹'],\n  ['缦', '縵'],\n  ['缧', '縲'],\n  ['缨', '纓'],\n  ['缩', '縮'],\n  ['缪', '繆'],\n  ['缫', '繅'],\n  ['缬', '纈'],\n  ['缭', '繚'],\n  ['缮', '繕'],\n  ['缯', '繒'],\n  ['缰', '韁'],\n  ['缱', '繾'],\n  ['缲', '繰'],\n  ['缳', '繯'],\n  ['缴', '繳'],\n  ['缵', '纘'],\n  ['罂', '罌'],\n  ['罗', '羅'],\n  ['罚', '罰'],\n  ['罢', '罷'],\n  ['罴', '羆'],\n  ['羁', '羈'],\n  ['羗', '羌'],\n  ['羟', '羥'],\n  ['羡', '羨'],\n  ['羣', '群'],\n  ['羮', '羹'],\n  ['翘', '翹'],\n  ['翙', '翽'],\n  ['翚', '翬'],\n  ['耢', '勞'],\n  ['耥', '尚'],\n  ['耧', '耬'],\n  ['耸', '聳'],\n  ['耻', '恥'],\n  ['聂', '聶'],\n  ['聋', '聾'],\n  ['职', '職'],\n  ['聍', '聹'],\n  ['联', '聯'],\n  ['聩', '聵'],\n  ['聪', '聰'],\n  ['肀', '聿'],\n  ['肃', '肅'],\n  ['肠', '腸'],\n  ['肤', '膚'],\n  ['肷', '欠'],\n  ['肾', '腎'],\n  ['肿', '腫'],\n  ['胀', '脹'],\n  ['胁', '脅'],\n  ['胆', '膽'],\n  ['胧', '朧'],\n  ['胨', '東'],\n  ['胪', '臚'],\n  ['胫', '脛'],\n  ['胶', '膠'],\n  ['脉', '脈'],\n  ['脍', '膾'],\n  ['脏', '髒'],\n  ['脐', '臍'],\n  ['脑', '腦'],\n  ['脓', '膿'],\n  ['脔', '臠'],\n  ['脚', '腳'],\n  ['脱', '脫'],\n  ['脲', '反'],\n  ['脶', '腡'],\n  ['脸', '臉'],\n  ['腭', '齶'],\n  ['腻', '膩'],\n  ['腽', '膃'],\n  ['腾', '騰'],\n  ['膑', '臏'],\n  ['臓', '摹'],\n  ['臜', '臢'],\n  ['舆', '輿'],\n  ['舣', '艤'],\n  ['舰', '艦'],\n  ['舱', '艙'],\n  ['舻', '艫'],\n  ['艰', '艱'],\n  ['艹', '艸'],\n  ['艺', '藝'],\n  ['节', '節'],\n  ['芈', '羋'],\n  ['芗', '薌'],\n  ['芜', '蕪'],\n  ['芦', '蘆'],\n  ['苁', '蓯'],\n  ['苇', '葦'],\n  ['苋', '莧'],\n  ['苌', '萇'],\n  ['苍', '蒼'],\n  ['苎', '苧'],\n  ['苏', '蘇'],\n  ['苘', '萵'],\n  ['茎', '莖'],\n  ['茏', '蘢'],\n  ['茑', '蔦'],\n  ['茔', '塋'],\n  ['茕', '煢'],\n  ['茧', '繭'],\n  ['荆', '荊'],\n  ['荚', '莢'],\n  ['荛', '蕘'],\n  ['荜', '蓽'],\n  ['荞', '蕎'],\n  ['荟', '薈'],\n  ['荠', '薺'],\n  ['荡', '蕩'],\n  ['荣', '榮'],\n  ['荤', '葷'],\n  ['荥', '滎'],\n  ['荦', '犖'],\n  ['荧', '熒'],\n  ['荨', '蕁'],\n  ['荩', '藎'],\n  ['荪', '蓀'],\n  ['荫', '蔭'],\n  ['荬', '賣'],\n  ['荭', '葒'],\n  ['荮', '紂'],\n  ['药', '藥'],\n  ['莅', '蒞'],\n  ['莱', '萊'],\n  ['莲', '蓮'],\n  ['莳', '蒔'],\n  ['莴', '萵'],\n  ['获', '獲'],\n  ['莸', '蕕'],\n  ['莹', '瑩'],\n  ['莺', '鶯'],\n  ['莼', '蓴'],\n  ['菭', '恰'],\n  ['萚', '蘀'],\n  ['萝', '蘿'],\n  ['萤', '螢'],\n  ['营', '營'],\n  ['萦', '縈'],\n  ['萧', '蕭'],\n  ['萨', '薩'],\n  ['葱', '蔥'],\n  ['蒇', '蕆'],\n  ['蒉', '蕢'],\n  ['蒋', '蔣'],\n  ['蒌', '蔞'],\n  ['蓝', '藍'],\n  ['蓟', '薊'],\n  ['蓠', '蘺'],\n  ['蓦', '驀'],\n  ['蔷', '薔'],\n  ['蔹', '蘞'],\n  ['蔺', '藺'],\n  ['蔼', '藹'],\n  ['蕲', '蘄'],\n  ['蕴', '蘊'],\n  ['薮', '藪'],\n  ['藁', '槁'],\n  ['藓', '蘚'],\n  ['蘖', '蘗'],\n  ['虏', '虜'],\n  ['虑', '慮'],\n  ['虚', '虛'],\n  ['虬', '虯'],\n  ['虮', '蟣'],\n  ['虽', '雖'],\n  ['虾', '蝦'],\n  ['虿', '蠆'],\n  ['蚀', '蝕'],\n  ['蚁', '蟻'],\n  ['蚂', '螞'],\n  ['蚕', '蠶'],\n  ['蚬', '蜆'],\n  ['蛊', '蠱'],\n  ['蛎', '蠣'],\n  ['蛏', '蟶'],\n  ['蛮', '蠻'],\n  ['蛰', '蟄'],\n  ['蛱', '蛺'],\n  ['蛲', '蟯'],\n  ['蛳', '螄'],\n  ['蛴', '蠐'],\n  ['蜕', '蛻'],\n  ['蜖', '汀'],\n  ['蜗', '蝸'],\n  ['蝇', '蠅'],\n  ['蝈', '蟈'],\n  ['蝉', '蟬'],\n  ['蝼', '螻'],\n  ['蝾', '蠑'],\n  ['蝿', '蠅'],\n  ['螀', '螿'],\n  ['螨', '顢'],\n  ['蟏', '蠨'],\n  ['蟮', '蟺'],\n  ['蠎', '蟒'],\n  ['衅', '釁'],\n  ['衔', '銜'],\n  ['衤', '衣'],\n  ['补', '補'],\n  ['衬', '襯'],\n  ['衮', '袞'],\n  ['袄', '襖'],\n  ['袅', '裊'],\n  ['袆', '褘'],\n  ['袭', '襲'],\n  ['袯', '襏'],\n  ['袴', '褲'],\n  ['装', '裝'],\n  ['裆', '襠'],\n  ['裈', '褌'],\n  ['裢', '褳'],\n  ['裣', '襝'],\n  ['裤', '褲'],\n  ['裥', '襉'],\n  ['褛', '褸'],\n  ['褴', '襤'],\n  ['襕', '襴'],\n  ['覇', '霸'],\n  ['覚', '覺'],\n  ['覧', '覽'],\n  ['覩', '睹'],\n  ['见', '見'],\n  ['观', '觀'],\n  ['规', '規'],\n  ['觅', '覓'],\n  ['视', '視'],\n  ['觇', '覘'],\n  ['览', '覽'],\n  ['觉', '覺'],\n  ['觊', '覬'],\n  ['觋', '覡'],\n  ['觌', '覿'],\n  ['觎', '覦'],\n  ['觏', '覯'],\n  ['觐', '覲'],\n  ['觑', '覷'],\n  ['觗', '觝'],\n  ['觞', '觴'],\n  ['触', '觸'],\n  ['觯', '觶'],\n  ['訡', '吟'],\n  ['詟', '讋'],\n  ['詤', '謊'],\n  ['誀', '浴'],\n  ['誉', '譽'],\n  ['誊', '謄'],\n  ['説', '說'],\n  ['読', '讀'],\n  ['讁', '謫'],\n  ['讠', '言'],\n  ['计', '計'],\n  ['订', '訂'],\n  ['讣', '訃'],\n  ['认', '認'],\n  ['讥', '譏'],\n  ['讦', '訐'],\n  ['讧', '訌'],\n  ['讨', '討'],\n  ['让', '讓'],\n  ['讪', '訕'],\n  ['讫', '訖'],\n  ['训', '訓'],\n  ['议', '議'],\n  ['讯', '訊'],\n  ['记', '記'],\n  ['讱', '訒'],\n  ['讲', '講'],\n  ['讳', '諱'],\n  ['讴', '謳'],\n  ['讵', '詎'],\n  ['讶', '訝'],\n  ['讷', '訥'],\n  ['许', '許'],\n  ['讹', '訛'],\n  ['论', '論'],\n  ['讼', '訟'],\n  ['讽', '諷'],\n  ['设', '設'],\n  ['访', '訪'],\n  ['诀', '訣'],\n  ['证', '證'],\n  ['诂', '詁'],\n  ['诃', '訶'],\n  ['评', '評'],\n  ['诅', '詛'],\n  ['识', '識'],\n  ['诇', '詗'],\n  ['诈', '詐'],\n  ['诉', '訴'],\n  ['诊', '診'],\n  ['诋', '詆'],\n  ['诌', '謅'],\n  ['词', '詞'],\n  ['诎', '詘'],\n  ['诏', '詔'],\n  ['诐', '詖'],\n  ['译', '譯'],\n  ['诒', '詒'],\n  ['诓', '誆'],\n  ['诔', '誄'],\n  ['试', '試'],\n  ['诖', '詿'],\n  ['诗', '詩'],\n  ['诘', '詰'],\n  ['诙', '詼'],\n  ['诚', '誠'],\n  ['诛', '誅'],\n  ['诜', '詵'],\n  ['话', '話'],\n  ['诞', '誕'],\n  ['诟', '詬'],\n  ['诠', '詮'],\n  ['诡', '詭'],\n  ['询', '詢'],\n  ['诣', '詣'],\n  ['诤', '諍'],\n  ['该', '該'],\n  ['详', '詳'],\n  ['诧', '詫'],\n  ['诨', '諢'],\n  ['诩', '詡'],\n  ['诪', '譸'],\n  ['诫', '誡'],\n  ['诬', '誣'],\n  ['语', '語'],\n  ['诮', '誚'],\n  ['误', '誤'],\n  ['诰', '誥'],\n  ['诱', '誘'],\n  ['诲', '誨'],\n  ['诳', '誑'],\n  ['说', '說'],\n  ['诵', '誦'],\n  ['诶', '誒'],\n  ['请', '請'],\n  ['诸', '諸'],\n  ['诹', '諏'],\n  ['诺', '諾'],\n  ['读', '讀'],\n  ['诼', '諑'],\n  ['诽', '誹'],\n  ['课', '課'],\n  ['诿', '諉'],\n  ['谀', '諛'],\n  ['谁', '誰'],\n  ['谂', '諗'],\n  ['调', '調'],\n  ['谄', '諂'],\n  ['谅', '諒'],\n  ['谆', '諄'],\n  ['谇', '誶'],\n  ['谈', '談'],\n  ['谊', '誼'],\n  ['谋', '謀'],\n  ['谌', '諶'],\n  ['谍', '諜'],\n  ['谎', '謊'],\n  ['谏', '諫'],\n  ['谐', '諧'],\n  ['谑', '謔'],\n  ['谒', '謁'],\n  ['谓', '謂'],\n  ['谔', '諤'],\n  ['谕', '諭'],\n  ['谖', '諼'],\n  ['谗', '讒'],\n  ['谘', '諮'],\n  ['谙', '諳'],\n  ['谚', '諺'],\n  ['谛', '諦'],\n  ['谜', '謎'],\n  ['谝', '諞'],\n  ['谞', '住'],\n  ['谟', '謨'],\n  ['谠', '讜'],\n  ['谡', '謖'],\n  ['谢', '謝'],\n  ['谣', '謠'],\n  ['谤', '謗'],\n  ['谥', '謚'],\n  ['谦', '謙'],\n  ['谧', '謐'],\n  ['谨', '謹'],\n  ['谩', '謾'],\n  ['谪', '謫'],\n  ['谫', '譾'],\n  ['谬', '謬'],\n  ['谭', '譚'],\n  ['谮', '譖'],\n  ['谯', '譙'],\n  ['谰', '讕'],\n  ['谱', '譜'],\n  ['谲', '譎'],\n  ['谳', '讞'],\n  ['谴', '譴'],\n  ['谵', '譫'],\n  ['谶', '讖'],\n  ['豮', '豶'],\n  ['貭', '亍'],\n  ['貮', '貳'],\n  ['賍', '贓'],\n  ['賎', '賤'],\n  ['賖', '賒'],\n  ['賘', '髒'],\n  ['贋', '贗'],\n  ['贘', '償'],\n  ['贝', '貝'],\n  ['贞', '貞'],\n  ['负', '負'],\n  ['贡', '貢'],\n  ['财', '財'],\n  ['责', '責'],\n  ['贤', '賢'],\n  ['败', '敗'],\n  ['账', '賬'],\n  ['货', '貨'],\n  ['质', '質'],\n  ['贩', '販'],\n  ['贪', '貪'],\n  ['贫', '貧'],\n  ['贬', '貶'],\n  ['购', '購'],\n  ['贮', '貯'],\n  ['贯', '貫'],\n  ['贰', '貳'],\n  ['贱', '賤'],\n  ['贲', '賁'],\n  ['贳', '貰'],\n  ['贴', '貼'],\n  ['贵', '貴'],\n  ['贶', '貺'],\n  ['贷', '貸'],\n  ['贸', '貿'],\n  ['费', '費'],\n  ['贺', '賀'],\n  ['贻', '貽'],\n  ['贼', '賊'],\n  ['贽', '贄'],\n  ['贾', '賈'],\n  ['贿', '賄'],\n  ['赀', '貲'],\n  ['赁', '賃'],\n  ['赂', '賂'],\n  ['赃', '贓'],\n  ['资', '資'],\n  ['赅', '賅'],\n  ['赆', '贐'],\n  ['赇', '賕'],\n  ['赈', '賑'],\n  ['赉', '賚'],\n  ['赊', '賒'],\n  ['赋', '賦'],\n  ['赌', '賭'],\n  ['赍', '齎'],\n  ['赎', '贖'],\n  ['赏', '賞'],\n  ['赐', '賜'],\n  ['赑', '贔'],\n  ['赒', '賙'],\n  ['赓', '賡'],\n  ['赔', '賠'],\n  ['赖', '賴'],\n  ['赗', '賵'],\n  ['赘', '贅'],\n  ['赙', '賻'],\n  ['赚', '賺'],\n  ['赛', '賽'],\n  ['赜', '賾'],\n  ['赝', '贗'],\n  ['赞', '贊'],\n  ['赟', '贇'],\n  ['赠', '贈'],\n  ['赡', '贍'],\n  ['赢', '贏'],\n  ['赣', '贛'],\n  ['赪', '赬'],\n  ['赵', '趙'],\n  ['趋', '趨'],\n  ['趱', '趲'],\n  ['趸', '躉'],\n  ['跃', '躍'],\n  ['跄', '蹌'],\n  ['跞', '躒'],\n  ['践', '踐'],\n  ['跶', '躂'],\n  ['跷', '蹺'],\n  ['跸', '蹕'],\n  ['跹', '躚'],\n  ['跻', '躋'],\n  ['踌', '躊'],\n  ['踪', '蹤'],\n  ['踬', '躓'],\n  ['踯', '躑'],\n  ['蹑', '躡'],\n  ['蹒', '蹣'],\n  ['蹰', '躕'],\n  ['蹿', '躥'],\n  ['躏', '躪'],\n  ['躜', '躦'],\n  ['躯', '軀'],\n  ['车', '車'],\n  ['轧', '軋'],\n  ['轨', '軌'],\n  ['轩', '軒'],\n  ['轪', '軑'],\n  ['轫', '軔'],\n  ['转', '轉'],\n  ['轭', '軛'],\n  ['轮', '輪'],\n  ['软', '軟'],\n  ['轰', '轟'],\n  ['轱', '古'],\n  ['轲', '軻'],\n  ['轳', '轤'],\n  ['轴', '軸'],\n  ['轵', '軹'],\n  ['轶', '軼'],\n  ['轷', '乎'],\n  ['轸', '軫'],\n  ['轹', '轢'],\n  ['轺', '軺'],\n  ['轻', '輕'],\n  ['轼', '軾'],\n  ['载', '載'],\n  ['轾', '輊'],\n  ['轿', '轎'],\n  ['辀', '輈'],\n  ['辁', '輇'],\n  ['辂', '輅'],\n  ['较', '較'],\n  ['辄', '輒'],\n  ['辅', '輔'],\n  ['辆', '輛'],\n  ['辇', '輦'],\n  ['辈', '輩'],\n  ['辉', '輝'],\n  ['辊', '輥'],\n  ['辋', '輞'],\n  ['辌', '輬'],\n  ['辍', '輟'],\n  ['辎', '輜'],\n  ['辏', '輳'],\n  ['辐', '輻'],\n  ['辑', '輯'],\n  ['辒', '轀'],\n  ['输', '輸'],\n  ['辔', '轡'],\n  ['辕', '轅'],\n  ['辖', '轄'],\n  ['辗', '輾'],\n  ['辘', '轆'],\n  ['辙', '轍'],\n  ['辚', '轔'],\n  ['辞', '辭'],\n  ['辩', '辯'],\n  ['辫', '辮'],\n  ['辬', '辨'],\n  ['边', '邊'],\n  ['辽', '遼'],\n  ['达', '達'],\n  ['迁', '遷'],\n  ['过', '過'],\n  ['迈', '邁'],\n  ['运', '運'],\n  ['还', '還'],\n  ['这', '這'],\n  ['进', '進'],\n  ['远', '遠'],\n  ['违', '違'],\n  ['连', '連'],\n  ['迟', '遲'],\n  ['迩', '邇'],\n  ['迳', '逕'],\n  ['迹', '跡'],\n  ['选', '選'],\n  ['逊', '遜'],\n  ['递', '遞'],\n  ['逦', '邐'],\n  ['逻', '邏'],\n  ['遗', '遺'],\n  ['遥', '遙'],\n  ['邓', '鄧'],\n  ['邝', '鄺'],\n  ['邬', '鄔'],\n  ['邮', '郵'],\n  ['邹', '鄒'],\n  ['邺', '鄴'],\n  ['邻', '鄰'],\n  ['郄', '卻'],\n  ['郏', '郟'],\n  ['郐', '鄶'],\n  ['郑', '鄭'],\n  ['郓', '鄆'],\n  ['郦', '酈'],\n  ['郧', '鄖'],\n  ['郷', '鄉'],\n  ['郸', '鄲'],\n  ['鄊', '鄉'],\n  ['鄕', '鄉'],\n  ['鄷', '酆'],\n  ['酝', '醞'],\n  ['酦', '醱'],\n  ['酱', '醬'],\n  ['酽', '釅'],\n  ['酾', '釃'],\n  ['酿', '釀'],\n  ['释', '釋'],\n  ['釡', '斧'],\n  ['鉴', '鑒'],\n  ['銮', '鑾'],\n  ['錾', '鏨'],\n  ['鎻', '鎖'],\n  ['钅', '金'],\n  ['钆', '釓'],\n  ['钇', '釔'],\n  ['针', '針'],\n  ['钉', '釘'],\n  ['钊', '釗'],\n  ['钋', '釙'],\n  ['钌', '釕'],\n  ['钍', '釷'],\n  ['钏', '釧'],\n  ['钐', '釤'],\n  ['钑', '鈒'],\n  ['钒', '釩'],\n  ['钓', '釣'],\n  ['钔', '鍆'],\n  ['钕', '釹'],\n  ['钖', '鍚'],\n  ['钗', '釵'],\n  ['钘', '鈃'],\n  ['钙', '鈣'],\n  ['钚', '鈽'],\n  ['钛', '鈦'],\n  ['钜', '鉅'],\n  ['钝', '鈍'],\n  ['钞', '鈔'],\n  ['钟', '鐘'],\n  ['钠', '鈉'],\n  ['钡', '鋇'],\n  ['钢', '鋼'],\n  ['钣', '鈑'],\n  ['钤', '鈐'],\n  ['钥', '鑰'],\n  ['钦', '欽'],\n  ['钧', '鈞'],\n  ['钨', '鎢'],\n  ['钩', '鉤'],\n  ['钪', '鈧'],\n  ['钫', '鈁'],\n  ['钬', '鈥'],\n  ['钮', '鈕'],\n  ['钯', '鈀'],\n  ['钰', '鈺'],\n  ['钱', '錢'],\n  ['钲', '鉦'],\n  ['钳', '鉗'],\n  ['钴', '鈷'],\n  ['钵', '缽'],\n  ['钶', '鈳'],\n  ['钸', '鈽'],\n  ['钹', '鈸'],\n  ['钺', '鉞'],\n  ['钻', '鑽'],\n  ['钼', '鉬'],\n  ['钽', '鉭'],\n  ['钾', '鉀'],\n  ['钿', '鈿'],\n  ['铀', '鈾'],\n  ['铁', '鐵'],\n  ['铂', '鉑'],\n  ['铃', '鈴'],\n  ['铄', '鑠'],\n  ['铅', '鉛'],\n  ['铆', '鉚'],\n  ['铈', '鈰'],\n  ['铉', '鉉'],\n  ['铊', '鉈'],\n  ['铋', '鉍'],\n  ['铌', '鈮'],\n  ['铍', '鈹'],\n  ['铎', '鐸'],\n  ['铏', '鉶'],\n  ['铐', '銬'],\n  ['铑', '銠'],\n  ['铒', '鉺'],\n  ['铓', '鋩'],\n  ['铔', '錏'],\n  ['铕', '銪'],\n  ['铖', '鋮'],\n  ['铗', '鋏'],\n  ['铘', '邪'],\n  ['铙', '鐃'],\n  ['铚', '銍'],\n  ['铛', '鐺'],\n  ['铜', '銅'],\n  ['铝', '鋁'],\n  ['铞', '吊'],\n  ['铟', '銦'],\n  ['铠', '鎧'],\n  ['铡', '鍘'],\n  ['铢', '銖'],\n  ['铣', '銑'],\n  ['铤', '鋌'],\n  ['铥', '銩'],\n  ['铦', '銛'],\n  ['铧', '鏵'],\n  ['铨', '銓'],\n  ['铩', '鎩'],\n  ['铪', '鉿'],\n  ['铫', '銚'],\n  ['铬', '鉻'],\n  ['铭', '銘'],\n  ['铮', '錚'],\n  ['铯', '銫'],\n  ['铰', '鉸'],\n  ['铱', '銥'],\n  ['铲', '鏟'],\n  ['铳', '銃'],\n  ['铴', '鐋'],\n  ['铵', '銨'],\n  ['银', '銀'],\n  ['铷', '銣'],\n  ['铸', '鑄'],\n  ['铹', '鐒'],\n  ['铺', '鋪'],\n  ['铻', '鋙'],\n  ['铼', '錸'],\n  ['铽', '鋱'],\n  ['链', '鏈'],\n  ['铿', '鏗'],\n  ['销', '銷'],\n  ['锁', '鎖'],\n  ['锂', '鋰'],\n  ['锃', '呈'],\n  ['锄', '鋤'],\n  ['锅', '鍋'],\n  ['锆', '鋯'],\n  ['锇', '鋨'],\n  ['锈', '鏽'],\n  ['锉', '銼'],\n  ['锊', '鋝'],\n  ['锋', '鋒'],\n  ['锌', '鋅'],\n  ['锍', '琉'],\n  ['锎', '鉲'],\n  ['锏', '閒'],\n  ['锐', '銳'],\n  ['锑', '銻'],\n  ['锒', '鋃'],\n  ['锓', '鋟'],\n  ['锔', '鋦'],\n  ['锕', '錒'],\n  ['锖', '錆'],\n  ['锗', '鍺'],\n  ['锘', '若'],\n  ['错', '錯'],\n  ['锚', '錨'],\n  ['锛', '錛'],\n  ['锜', '錡'],\n  ['锝', '鎝'],\n  ['锞', '錁'],\n  ['锟', '錕'],\n  ['锠', '琛'],\n  ['锡', '錫'],\n  ['锢', '錮'],\n  ['锣', '鑼'],\n  ['锤', '錘'],\n  ['锥', '錐'],\n  ['锦', '錦'],\n  ['锧', '鑕'],\n  ['锨', '杴'],\n  ['锪', '忽'],\n  ['锫', '培'],\n  ['锬', '錟'],\n  ['锭', '錠'],\n  ['键', '鍵'],\n  ['锯', '鋸'],\n  ['锰', '錳'],\n  ['锱', '錙'],\n  ['锲', '鍥'],\n  ['锴', '鍇'],\n  ['锵', '鏘'],\n  ['锶', '鍶'],\n  ['锷', '鍔'],\n  ['锸', '鍤'],\n  ['锹', '鍬'],\n  ['锺', '鍾'],\n  ['锻', '鍛'],\n  ['锼', '鎪'],\n  ['锽', '鍠'],\n  ['锾', '鍰'],\n  ['锿', '鑀'],\n  ['镀', '鍍'],\n  ['镁', '鎂'],\n  ['镂', '鏤'],\n  ['镃', '鎡'],\n  ['镄', '鐨'],\n  ['镅', '鋂'],\n  ['镆', '鏌'],\n  ['镇', '鎮'],\n  ['镈', '鎛'],\n  ['镉', '鎘'],\n  ['镊', '鑷'],\n  ['镋', '钂'],\n  ['镌', '鐫'],\n  ['镍', '鎳'],\n  ['镎', '拿'],\n  ['镏', '鎦'],\n  ['镐', '鎬'],\n  ['镑', '鎊'],\n  ['镒', '鎰'],\n  ['镓', '鎵'],\n  ['镔', '鑌'],\n  ['镕', '鎔'],\n  ['镖', '鏢'],\n  ['镗', '鏜'],\n  ['镘', '鏝'],\n  ['镙', '鏍'],\n  ['镛', '鏞'],\n  ['镜', '鏡'],\n  ['镝', '鏑'],\n  ['镞', '鏃'],\n  ['镟', '鏇'],\n  ['镠', '鏐'],\n  ['镡', '鐔'],\n  ['镢', '钁'],\n  ['镣', '鐐'],\n  ['镤', '鏷'],\n  ['镥', '魯'],\n  ['镧', '鑭'],\n  ['镨', '鐠'],\n  ['镩', '串'],\n  ['镪', '鏹'],\n  ['镫', '鐙'],\n  ['镬', '鑊'],\n  ['镭', '鐳'],\n  ['镮', '鐶'],\n  ['镯', '鐲'],\n  ['镰', '鐮'],\n  ['镱', '鐿'],\n  ['镲', '察'],\n  ['镳', '鑣'],\n  ['镴', '鑞'],\n  ['镵', '鑱'],\n  ['镶', '鑲'],\n  ['长', '長'],\n  ['閲', '閱'],\n  ['门', '門'],\n  ['闩', '閂'],\n  ['闪', '閃'],\n  ['闫', '閆'],\n  ['闬', '閈'],\n  ['闭', '閉'],\n  ['问', '問'],\n  ['闯', '闖'],\n  ['闰', '閏'],\n  ['闱', '闈'],\n  ['闲', '閒'],\n  ['闳', '閎'],\n  ['间', '間'],\n  ['闵', '閔'],\n  ['闶', '閌'],\n  ['闷', '悶'],\n  ['闸', '閘'],\n  ['闹', '鬧'],\n  ['闺', '閨'],\n  ['闻', '聞'],\n  ['闼', '闥'],\n  ['闽', '閩'],\n  ['闾', '閭'],\n  ['闿', '闓'],\n  ['阀', '閥'],\n  ['阁', '閣'],\n  ['阂', '閡'],\n  ['阃', '閫'],\n  ['阄', '鬮'],\n  ['阅', '閱'],\n  ['阆', '閬'],\n  ['阇', '闍'],\n  ['阈', '閾'],\n  ['阉', '閹'],\n  ['阊', '閶'],\n  ['阋', '鬩'],\n  ['阌', '閿'],\n  ['阍', '閽'],\n  ['阎', '閻'],\n  ['阏', '閼'],\n  ['阐', '闡'],\n  ['阑', '闌'],\n  ['阒', '闃'],\n  ['阓', '闠'],\n  ['阔', '闊'],\n  ['阕', '闋'],\n  ['阖', '闔'],\n  ['阗', '闐'],\n  ['阘', '闒'],\n  ['阙', '闕'],\n  ['阚', '闞'],\n  ['阛', '闤'],\n  ['阝', '阜'],\n  ['队', '隊'],\n  ['阳', '陽'],\n  ['阴', '陰'],\n  ['阵', '陣'],\n  ['阶', '階'],\n  ['际', '際'],\n  ['陆', '陸'],\n  ['陇', '隴'],\n  ['陈', '陳'],\n  ['陉', '陘'],\n  ['陕', '陝'],\n  ['陧', '隉'],\n  ['陨', '隕'],\n  ['险', '險'],\n  ['隂', '陰'],\n  ['隌', '暗'],\n  ['随', '隨'],\n  ['隐', '隱'],\n  ['隠', '隱'],\n  ['隷', '隸'],\n  ['隽', '雋'],\n  ['难', '難'],\n  ['雏', '雛'],\n  ['雠', '讎'],\n  ['雳', '靂'],\n  ['雾', '霧'],\n  ['霁', '霽'],\n  ['霊', '靈'],\n  ['霭', '靄'],\n  ['靓', '靚'],\n  ['静', '靜'],\n  ['靥', '靨'],\n  ['鞑', '韃'],\n  ['鞒', '轎'],\n  ['鞯', '韉'],\n  ['鞲', '韝'],\n  ['鞽', '轎'],\n  ['韦', '韋'],\n  ['韧', '韌'],\n  ['韨', '韍'],\n  ['韩', '韓'],\n  ['韪', '韙'],\n  ['韫', '韞'],\n  ['韬', '韜'],\n  ['韯', '籤'],\n  ['韲', '齋'],\n  ['韵', '韻'],\n  ['顋', '腮'],\n  ['顔', '顏'],\n  ['顕', '顯'],\n  ['页', '頁'],\n  ['顶', '頂'],\n  ['顷', '頃'],\n  ['顸', '頇'],\n  ['项', '項'],\n  ['顺', '順'],\n  ['须', '須'],\n  ['顼', '頊'],\n  ['顽', '頑'],\n  ['顾', '顧'],\n  ['顿', '頓'],\n  ['颀', '頎'],\n  ['颁', '頒'],\n  ['颂', '頌'],\n  ['颃', '頏'],\n  ['预', '預'],\n  ['颅', '顱'],\n  ['领', '領'],\n  ['颇', '頗'],\n  ['颈', '頸'],\n  ['颉', '頡'],\n  ['颊', '頰'],\n  ['颋', '頲'],\n  ['颌', '頜'],\n  ['颍', '潁'],\n  ['颎', '熲'],\n  ['颏', '頦'],\n  ['颐', '頤'],\n  ['频', '頻'],\n  ['颓', '頹'],\n  ['颔', '頷'],\n  ['颕', '穎'],\n  ['颖', '穎'],\n  ['颗', '顆'],\n  ['题', '題'],\n  ['颙', '顒'],\n  ['颚', '顎'],\n  ['颛', '顓'],\n  ['颜', '顏'],\n  ['额', '額'],\n  ['颞', '顳'],\n  ['颟', '顢'],\n  ['颠', '顛'],\n  ['颡', '顙'],\n  ['颢', '顥'],\n  ['颣', '纇'],\n  ['颤', '顫'],\n  ['颥', '須'],\n  ['颦', '顰'],\n  ['颧', '顴'],\n  ['颷', '飆'],\n  ['风', '風'],\n  ['飏', '颺'],\n  ['飐', '颭'],\n  ['飑', '颮'],\n  ['飒', '颯'],\n  ['飓', '颶'],\n  ['飔', '颸'],\n  ['飕', '颼'],\n  ['飖', '颻'],\n  ['飗', '飀'],\n  ['飘', '飄'],\n  ['飙', '飆'],\n  ['飚', '飆'],\n  ['飞', '飛'],\n  ['飨', '饗'],\n  ['飬', '養'],\n  ['飮', '飲'],\n  ['飱', '餐'],\n  ['餍', '饜'],\n  ['饣', '食'],\n  ['饤', '飣'],\n  ['饥', '飢'],\n  ['饦', '飥'],\n  ['饧', '餳'],\n  ['饨', '飩'],\n  ['饩', '餼'],\n  ['饪', '飪'],\n  ['饫', '飫'],\n  ['饬', '飭'],\n  ['饭', '飯'],\n  ['饮', '飲'],\n  ['饯', '餞'],\n  ['饰', '飾'],\n  ['饱', '飽'],\n  ['饲', '飼'],\n  ['饴', '飴'],\n  ['饵', '餌'],\n  ['饶', '饒'],\n  ['饷', '餉'],\n  ['饺', '餃'],\n  ['饼', '餅'],\n  ['饽', '餑'],\n  ['饾', '餖'],\n  ['饿', '餓'],\n  ['馀', '餘'],\n  ['馁', '餒'],\n  ['馂', '餕'],\n  ['馄', '餛'],\n  ['馅', '餡'],\n  ['馆', '館'],\n  ['馇', '查'],\n  ['馈', '饋'],\n  ['馉', '稹'],\n  ['馊', '餿'],\n  ['馋', '饞'],\n  ['馌', '饁'],\n  ['馍', '饃'],\n  ['馎', '餺'],\n  ['馏', '餾'],\n  ['馐', '饈'],\n  ['馑', '饉'],\n  ['馒', '饅'],\n  ['馓', '饊'],\n  ['馔', '饌'],\n  ['馕', '囊'],\n  ['马', '馬'],\n  ['驭', '馭'],\n  ['驮', '馱'],\n  ['驯', '馴'],\n  ['驰', '馳'],\n  ['驱', '驅'],\n  ['驲', '馹'],\n  ['驳', '駁'],\n  ['驴', '驢'],\n  ['驵', '駔'],\n  ['驶', '駛'],\n  ['驷', '駟'],\n  ['驸', '駙'],\n  ['驹', '駒'],\n  ['驺', '騶'],\n  ['驻', '駐'],\n  ['驼', '駝'],\n  ['驽', '駑'],\n  ['驾', '駕'],\n  ['驿', '驛'],\n  ['骀', '駘'],\n  ['骁', '驍'],\n  ['骂', '罵'],\n  ['骃', '駰'],\n  ['骄', '驕'],\n  ['骅', '驊'],\n  ['骆', '駱'],\n  ['骇', '駭'],\n  ['骈', '駢'],\n  ['骊', '驪'],\n  ['骋', '騁'],\n  ['验', '驗'],\n  ['骍', '騂'],\n  ['骎', '駸'],\n  ['骏', '駿'],\n  ['骐', '騏'],\n  ['骑', '騎'],\n  ['骒', '騍'],\n  ['骓', '騅'],\n  ['骕', '驌'],\n  ['骖', '驂'],\n  ['骗', '騙'],\n  ['骘', '騭'],\n  ['骙', '騤'],\n  ['骚', '騷'],\n  ['骛', '騖'],\n  ['骜', '驁'],\n  ['骝', '騮'],\n  ['骞', '騫'],\n  ['骟', '騸'],\n  ['骠', '驃'],\n  ['骡', '騾'],\n  ['骢', '驄'],\n  ['骣', '驏'],\n  ['骤', '驟'],\n  ['骥', '驥'],\n  ['骦', '驦'],\n  ['骧', '驤'],\n  ['髅', '髏'],\n  ['髋', '髖'],\n  ['髌', '髕'],\n  ['鬓', '鬢'],\n  ['魇', '魘'],\n  ['魉', '魎'],\n  ['鱼', '魚'],\n  ['鱿', '魷'],\n  ['鲀', '魨'],\n  ['鲁', '魯'],\n  ['鲂', '魴'],\n  ['鲅', '鱍'],\n  ['鲆', '平'],\n  ['鲇', '占'],\n  ['鲈', '鱸'],\n  ['鲊', '鮓'],\n  ['鲋', '鮒'],\n  ['鲍', '鮑'],\n  ['鲎', '鱟'],\n  ['鲐', '鮐'],\n  ['鲑', '鮭'],\n  ['鲒', '鮚'],\n  ['鲔', '鮪'],\n  ['鲕', '鮞'],\n  ['鲖', '鮦'],\n  ['鲙', '鱠'],\n  ['鲚', '鱭'],\n  ['鲛', '鮫'],\n  ['鲜', '鮮'],\n  ['鲞', '鯗'],\n  ['鲟', '鱘'],\n  ['鲠', '鯁'],\n  ['鲡', '鱺'],\n  ['鲢', '鰱'],\n  ['鲣', '鰹'],\n  ['鲤', '鯉'],\n  ['鲥', '鰣'],\n  ['鲦', '鰷'],\n  ['鲧', '鯀'],\n  ['鲨', '鯊'],\n  ['鲩', '鯇'],\n  ['鲫', '鯽'],\n  ['鲭', '鯖'],\n  ['鲮', '鯪'],\n  ['鲰', '鯫'],\n  ['鲱', '鯡'],\n  ['鲲', '鯤'],\n  ['鲳', '鯧'],\n  ['鲴', '固'],\n  ['鲵', '鯢'],\n  ['鲶', '鯰'],\n  ['鲷', '鯛'],\n  ['鲸', '鯨'],\n  ['鲺', '虱'],\n  ['鲻', '鯔'],\n  ['鲼', '賁'],\n  ['鲽', '鰈'],\n  ['鲿', '鱨'],\n  ['鳀', '鯷'],\n  ['鳃', '鰓'],\n  ['鳄', '鱷'],\n  ['鳅', '鰍'],\n  ['鳆', '鰒'],\n  ['鳇', '鰉'],\n  ['鳊', '扁'],\n  ['鳋', '蚤'],\n  ['鳌', '鰲'],\n  ['鳍', '鰭'],\n  ['鳏', '鰥'],\n  ['鳐', '鰩'],\n  ['鳒', '鰜'],\n  ['鳔', '鰾'],\n  ['鳕', '鱈'],\n  ['鳖', '鱉'],\n  ['鳗', '鰻'],\n  ['鳘', '鱉'],\n  ['鳙', '庸'],\n  ['鳛', '鰼'],\n  ['鳜', '鱖'],\n  ['鳝', '鱔'],\n  ['鳞', '鱗'],\n  ['鳟', '鱒'],\n  ['鳡', '鰲'],\n  ['鳢', '鱧'],\n  ['鳣', '鱣'],\n  ['鸟', '鳥'],\n  ['鸠', '鳩'],\n  ['鸡', '雞'],\n  ['鸢', '鳶'],\n  ['鸣', '鳴'],\n  ['鸤', '鳲'],\n  ['鸥', '鷗'],\n  ['鸦', '鴉'],\n  ['鸧', '鶬'],\n  ['鸨', '鴇'],\n  ['鸩', '鴆'],\n  ['鸪', '鴣'],\n  ['鸬', '鸕'],\n  ['鸭', '鴨'],\n  ['鸮', '鴞'],\n  ['鸯', '鴦'],\n  ['鸰', '鴒'],\n  ['鸱', '鴟'],\n  ['鸲', '鴝'],\n  ['鸳', '鴛'],\n  ['鸵', '鴕'],\n  ['鸶', '鷥'],\n  ['鸷', '鷙'],\n  ['鸹', '鴰'],\n  ['鸺', '鵂'],\n  ['鸼', '鵃'],\n  ['鸽', '鴿'],\n  ['鸾', '鸞'],\n  ['鸿', '鴻'],\n  ['鹁', '鵓'],\n  ['鹂', '鸝'],\n  ['鹃', '鵑'],\n  ['鹄', '鵠'],\n  ['鹅', '鵝'],\n  ['鹆', '鵒'],\n  ['鹇', '鷳'],\n  ['鹈', '鵜'],\n  ['鹉', '鵡'],\n  ['鹊', '鵲'],\n  ['鹋', '苗'],\n  ['鹌', '鵪'],\n  ['鹎', '鵯'],\n  ['鹏', '鵬'],\n  ['鹑', '鶉'],\n  ['鹒', '鶊'],\n  ['鹓', '鵷'],\n  ['鹔', '鷫'],\n  ['鹕', '鶘'],\n  ['鹖', '鶡'],\n  ['鹗', '鶚'],\n  ['鹘', '鶻'],\n  ['鹙', '鶖'],\n  ['鹚', '鶿'],\n  ['鹛', '眉'],\n  ['鹜', '鶩'],\n  ['鹝', '鷊'],\n  ['鹞', '鷂'],\n  ['鹠', '鶹'],\n  ['鹡', '鶺'],\n  ['鹢', '鷁'],\n  ['鹣', '鶼'],\n  ['鹤', '鶴'],\n  ['鹥', '鷖'],\n  ['鹦', '鸚'],\n  ['鹧', '鷓'],\n  ['鹨', '鷚'],\n  ['鹩', '鷯'],\n  ['鹪', '鷦'],\n  ['鹫', '鷲'],\n  ['鹬', '鷸'],\n  ['鹭', '鷺'],\n  ['鹯', '鸇'],\n  ['鹰', '鷹'],\n  ['鹱', '獲'],\n  ['鹲', '鸏'],\n  ['鹳', '鸛'],\n  ['鹾', '鹺'],\n  ['麦', '麥'],\n  ['麸', '麩'],\n  ['麹', '麴'],\n  ['麺', '麵'],\n  ['麽', '麼'],\n  ['黄', '黃'],\n  ['黉', '黌'],\n  ['黒', '黑'],\n  ['黙', '默'],\n  ['黡', '黶'],\n  ['黩', '黷'],\n  ['黪', '黲'],\n  ['黾', '黽'],\n  ['鼋', '黿'],\n  ['鼍', '鼉'],\n  ['鼗', '鞀'],\n  ['鼹', '鼴'],\n  ['齄', '皻'],\n  ['齐', '齊'],\n  ['齑', '齏'],\n  ['齿', '齒'],\n  ['龀', '齔'],\n  ['龁', '齕'],\n  ['龂', '齗'],\n  ['龃', '齟'],\n  ['龄', '齡'],\n  ['龅', '齙'],\n  ['龆', '齠'],\n  ['龇', '齜'],\n  ['龈', '齦'],\n  ['龉', '齬'],\n  ['龊', '齪'],\n  ['龋', '齲'],\n  ['龌', '齷'],\n  ['龙', '龍'],\n  ['龚', '龔'],\n  ['龛', '龕'],\n  ['龟', '龜']\n])\n\nexport function chsToChz(text: string): string {\n  if (!text) return ''\n\n  let result = ''\n  for (let i = 0; i < text.length; i++) {\n    result += charMap.get(text[i]) || text[i]\n  }\n\n  return result\n}\n\nexport default chsToChz\n"
  },
  {
    "path": "src/_helpers/config-manager.ts",
    "content": "import pako from 'pako'\nimport { getDefaultConfig, AppConfig } from '@/app-config'\nimport { mergeConfig } from '@/app-config/merge-config'\nimport { storage } from './browser-api'\n\nimport { Observable, from, concat, fromEventPattern } from 'rxjs'\nimport { map } from 'rxjs/operators'\n\nexport interface StorageChanged<T> {\n  newValue?: T\n  oldValue?: T\n}\n\nexport interface AppConfigChanged {\n  newConfig: AppConfig\n  oldConfig?: AppConfig\n}\n\n/** Compressed config data */\ninterface AppConfigCompressed {\n  /** version */\n  v: 1\n  /** data */\n  d: string\n}\n\nfunction deflate(config: AppConfig): AppConfigCompressed {\n  return {\n    v: 1,\n    d: pako.deflate(JSON.stringify(config), { to: 'string' })\n  }\n}\n\nfunction inflate(config: AppConfig | AppConfigCompressed): AppConfig\nfunction inflate(config: undefined): undefined\nfunction inflate(\n  config?: AppConfig | AppConfigCompressed\n): AppConfig | undefined\nfunction inflate(\n  config?: AppConfig | AppConfigCompressed\n): AppConfig | undefined {\n  if (config && config['v'] === 1) {\n    return JSON.parse(\n      pako.inflate((config as AppConfigCompressed).d, { to: 'string' })\n    )\n  }\n  return config as AppConfig\n}\n\nexport async function initConfig(): Promise<AppConfig> {\n  let baseconfig = await getConfig()\n\n  baseconfig =\n    baseconfig && baseconfig.version\n      ? mergeConfig(baseconfig)\n      : getDefaultConfig()\n\n  await updateConfig(baseconfig)\n  return baseconfig\n}\n\nexport async function resetConfig() {\n  const baseconfig = getDefaultConfig()\n  await updateConfig(baseconfig)\n  return baseconfig\n}\n\nexport async function getConfig(): Promise<AppConfig> {\n  const { baseconfig } = await storage.sync.get<{\n    baseconfig: AppConfig\n  }>('baseconfig')\n  return inflate(baseconfig || getDefaultConfig())\n}\n\nexport function updateConfig(baseconfig: AppConfig): Promise<void> {\n  if (process.env.DEBUG) {\n    console.log(`Saved config`, baseconfig)\n  }\n  return storage.sync.set({ baseconfig: deflate(baseconfig) })\n}\n\n/**\n * Listen to config changes\n */\nexport async function addConfigListener(\n  cb: (changes: AppConfigChanged) => any\n) {\n  storage.sync.addListener(changes => {\n    if (changes.baseconfig) {\n      const { newValue, oldValue } = changes.baseconfig as StorageChanged<\n        AppConfigCompressed\n      >\n      if (newValue) {\n        cb({ newConfig: inflate(newValue), oldConfig: inflate(oldValue) })\n      }\n    }\n  })\n}\n\n/**\n * Get config and create a stream listening to config change\n */\nexport function createConfigStream(): Observable<AppConfig> {\n  return concat(\n    from(getConfig()),\n    fromEventPattern<[AppConfigChanged] | AppConfigChanged>(\n      addConfigListener\n    ).pipe(map(args => (Array.isArray(args) ? args[0] : args).newConfig))\n  )\n}\n"
  },
  {
    "path": "src/_helpers/dom.ts",
    "content": "/**\n * xhtml returns small case\n */\nexport function isTagName(node: Node, tagName: string): boolean {\n  return (\n    ((node as HTMLElement).tagName || '').toLowerCase() ===\n    tagName.toLowerCase()\n  )\n}\n"
  },
  {
    "path": "src/_helpers/fetch-dom.ts",
    "content": "import DOMPurify from 'dompurify'\nimport axios, { AxiosRequestConfig } from 'axios'\n\nexport function fetchDOM(\n  url: string,\n  config: AxiosRequestConfig = {}\n): Promise<DocumentFragment> {\n  return axios(url, {\n    ...config,\n    transformResponse: [data => data],\n    responseType: 'text'\n  }).then(({ data }) => DOMPurify.sanitize(data, { RETURN_DOM_FRAGMENT: true }))\n}\n\n/** about 6 time faster as it typically takes less than 5ms to parse a DOM */\nexport function fetchDirtyDOM(\n  url: string,\n  config: AxiosRequestConfig = {}\n): Promise<Document> {\n  return axios(url, {\n    withCredentials: false,\n    ...config,\n    transformResponse: [data => data],\n    responseType: 'document'\n  }).then(({ data }) =>\n    process.env.NODE_ENV !== 'production'\n      ? new DOMParser().parseFromString(data, 'text/html')\n      : data\n  )\n}\n\nexport function fetchPlainText(\n  url: string,\n  config: AxiosRequestConfig = {}\n): Promise<string> {\n  return axios(url, {\n    withCredentials: false,\n    ...config,\n    // axios bug https://github.com/axios/axios/issues/907\n    transformResponse: [data => data],\n    responseType: 'text'\n  }).then(({ data }) => data)\n}\n"
  },
  {
    "path": "src/_helpers/getSuggests.ts",
    "content": "import { first } from './promise-more'\n\ninterface Suggest {\n  entry: string\n  explain: string\n}\n\nexport function getSuggests(text: string): Promise<Suggest[]> {\n  return first([getCiba(text), getYoudao(text)]).catch(() => [])\n}\n\nfunction getCiba(text: string): Promise<Suggest[]> {\n  return fetch(\n    'http://dict-mobile.iciba.com/interface/index.php?c=word&m=getsuggest&nums=10&client=6&uid=0&is_need_mean=1&word=' +\n      encodeURIComponent(text)\n  )\n    .then(r => r.json())\n    .then(json => {\n      if (json && Array.isArray(json.message)) {\n        return json.message\n          .filter(x => x && x.key)\n          .map(x => ({\n            entry: x.key,\n            explain:\n              Array.isArray(x.means) && x.means.length > 0\n                ? x.means[0].part + ' ' + x.means[0].means.join(' ')\n                : ''\n          }))\n      }\n      if (process.env.DEBUG) {\n        console.warn('fetch suggests failed', text, json)\n      }\n      throw new Error()\n    })\n}\n\nfunction getYoudao(text: string): Promise<Suggest[]> {\n  return fetch(\n    'https://dict.youdao.com/suggest?doctype=json&le=en&ver=2.0&q=' +\n      encodeURIComponent(text)\n  )\n    .then(r => r.json())\n    .then(json => {\n      if (json && json.data && Array.isArray(json.data.entries)) {\n        return json.data.entries.filter(x => x && x.explain && x.entry)\n      }\n      if (process.env.DEBUG) {\n        console.warn('fetch suggests failed', text, json)\n      }\n      throw new Error()\n    })\n}\n"
  },
  {
    "path": "src/_helpers/hooks.ts",
    "content": "import { useRef } from 'react'\n\n/**\n * Equivalent to useCallback(fn, [])\n */\nexport function useFixedCallback<T>(fn: T): T {\n  return useRef(fn).current\n}\n\n/**\n * Equivalent to useMemo(() => value, [])\n */\nexport function useFixedMemo<T>(fn: () => T): T {\n  const initedRef = useRef(false)\n  const valueRef = useRef<T>()\n  if (!initedRef.current) {\n    initedRef.current = true\n    valueRef.current = fn()\n  }\n  return valueRef.current!\n}\n"
  },
  {
    "path": "src/_helpers/i18n.ts",
    "content": "import React, {\n  useState,\n  useLayoutEffect,\n  FC,\n  useContext,\n  useRef,\n  Fragment,\n  PropsWithChildren\n} from 'react'\nimport mapValues from 'lodash/mapValues'\nimport i18n, { TFunction } from 'i18next'\nimport { getConfig, addConfigListener } from '@/_helpers/config-manager'\nimport zip from 'lodash/zip'\n\nexport type LangCode = 'zh-CN' | 'zh-TW' | 'en'\nexport type Namespace =\n  | 'common'\n  | 'content'\n  | 'langcode'\n  | 'menus'\n  | 'options'\n  | 'popup'\n  | 'wordpage'\n  | 'dicts'\n  | 'sync'\n\nexport interface RawLocale {\n  'zh-CN': string\n  'zh-TW': string\n  en: string\n}\n\nexport interface RawLocales {\n  [message: string]: RawLocale\n}\n\nexport interface RawDictLocales {\n  name: RawLocale\n  options?: RawLocales\n  helps?: RawLocales\n}\n\nexport interface DictLocales {\n  name: string\n  options?: {\n    [message: string]: any\n  }\n  helps?: {\n    [message: string]: any\n  }\n}\n\nexport async function i18nLoader(): Promise<i18n.i18n> {\n  if (i18n.language) {\n    // singleton\n    return i18n\n  }\n\n  const { langCode } = await getConfig()\n\n  await i18n\n    .use({\n      type: 'backend',\n      init: () => {},\n      create: () => {},\n      read: async (lang: LangCode, ns: Namespace, cb: Function) => {\n        try {\n          if (ns === 'dicts') {\n            const dictLocals = extractDictLocales(lang)\n            cb(null, dictLocals)\n            return dictLocals\n          }\n\n          if (ns === 'sync') {\n            const syncLocales = extractSyncServiceLocales(lang)\n            cb(null, syncLocales)\n            return syncLocales\n          }\n\n          const { locale } = await import(\n            /* webpackInclude: /_locales\\/[^/]+\\/[^/]+\\.ts$/ */\n            /* webpackMode: \"lazy\" */\n            `@/_locales/${lang}/${ns}.ts`\n          )\n          cb(null, locale)\n          return locale\n        } catch (err) {\n          cb(err)\n        }\n      }\n    })\n    .init({\n      lng: langCode,\n      fallbackLng: false,\n      whitelist: ['en', 'zh-CN', 'zh-TW'],\n\n      debug: process.env.NODE_ENV === 'development',\n      saveMissing: false,\n      load: 'currentOnly',\n\n      ns: 'common',\n      defaultNS: 'common',\n\n      interpolation: {\n        escapeValue: false // not needed for react as it escapes by default\n      }\n    })\n\n  addConfigListener(({ newConfig }) => {\n    if (i18n.language !== newConfig.langCode) {\n      i18n.changeLanguage(newConfig.langCode)\n    }\n  })\n\n  return i18n\n}\n\nconst defaultT: i18n.TFunction = () => ''\n\nexport const I18nContext = React.createContext<string | undefined>(undefined)\nif (process.env.DEBUG) {\n  I18nContext.displayName = 'I18nContext'\n}\n\nexport const I18nContextProvider: FC = ({ children }) => {\n  const [lang, setLang] = useState<string | undefined>(undefined)\n\n  useLayoutEffect(() => {\n    const setLangCallback = () => {\n      setLang(i18n.language)\n    }\n\n    if (!i18n.language) {\n      i18nLoader().then(() => {\n        setLang(i18n.language)\n        i18n.on('languageChanged', setLangCallback)\n      })\n    }\n\n    return () => {\n      i18n.off('languageChanged', setLangCallback)\n    }\n  }, [])\n\n  return React.createElement(I18nContext.Provider, { value: lang }, children)\n}\n\nexport interface UseTranslateResult {\n  /**\n   * fixedT with the first namespace as default.\n   * It is a wrapper of the original fixedT, which\n   * keeps the same reference even after namespaces are loaded\n   */\n  t: i18n.TFunction\n  i18n: i18n.i18n\n  /**\n   * Are namespaces loaded?\n   * false not ready\n   * otherwise it is a non-zero positive number\n   * that changes everytime when new namespaces are loaded.\n   */\n  ready: false | number\n}\n\n/**\n * Tailored for this project.\n * The official `useTranslation` is too heavy.\n * @param namespaces will not monitor namespace changes.\n */\nexport function useTranslate(\n  namespaces?: Namespace | Namespace[]\n): UseTranslateResult {\n  const ticketRef = useRef(0)\n  const innerTRef = useRef<TFunction>(defaultT)\n  // keep the exposed t function always the same\n  const tRef = useRef<TFunction>((...args: Parameters<TFunction>) =>\n    innerTRef.current(...args)\n  )\n  const lang = useContext(I18nContext)\n\n  const genResult = (t: TFunction | null, ready: boolean) => {\n    if (t) {\n      innerTRef.current = t\n    }\n    if (ready) {\n      ticketRef.current = (ticketRef.current + 1) % 100000\n    }\n    const result: UseTranslateResult = {\n      t: tRef.current,\n      i18n,\n      ready: ready ? ticketRef.current : false\n    }\n    return result\n  }\n\n  const [result, setResult] = useState<UseTranslateResult>(() => {\n    if (!lang) {\n      return genResult(defaultT, false)\n    }\n\n    if (!namespaces) {\n      return genResult(i18n.t, true)\n    }\n\n    if (\n      Array.isArray(namespaces)\n        ? namespaces.every(ns => i18n.hasResourceBundle(lang, ns))\n        : i18n.hasResourceBundle(lang, namespaces)\n    ) {\n      return genResult(i18n.getFixedT(lang, namespaces), true)\n    }\n\n    return genResult(defaultT, false)\n  })\n\n  useLayoutEffect(() => {\n    let isEffectRunning = true\n\n    if (lang) {\n      if (namespaces) {\n        if (\n          Array.isArray(namespaces)\n            ? namespaces.every(ns => i18n.hasResourceBundle(lang, ns))\n            : i18n.hasResourceBundle(lang, namespaces)\n        ) {\n          setResult(genResult(i18n.getFixedT(lang, namespaces), true))\n        } else {\n          // keep the old t while marking not ready\n          setResult(genResult(null, false))\n\n          i18n.loadNamespaces(namespaces).then(() => {\n            if (isEffectRunning) {\n              setResult(genResult(i18n.getFixedT(lang, namespaces), true))\n            }\n          })\n        }\n      } else {\n        setResult(genResult(i18n.t, true))\n      }\n    }\n\n    return () => {\n      isEffectRunning = false\n    }\n  }, [lang])\n\n  return result\n}\n\n/**\n * <Trans message=\"a{b}c{d}e\">\n *   <h1>b</h1>\n *   <p>d</p>\n * </Trans>\n *  ↓\n * [\n *   \"a\",\n *   <h1>b</h1>,\n *   \"c\",\n *   <p>d</p>,\n *   \"e\"\n * ]\n */\nexport const Trans = React.memo<PropsWithChildren<{ message?: string }>>(\n  ({ message, children }) => {\n    if (!message) return null\n\n    return React.createElement(\n      Fragment,\n      null,\n      zip(\n        message.split(/{[^}]*?}/),\n        Array.isArray(children) ? children : [children]\n      )\n    )\n  }\n)\n\nfunction extractDictLocales(lang: LangCode) {\n  const req = require.context(\n    '@/components/dictionaries',\n    true,\n    /_locales\\.(json|ts)$/\n  )\n  return req.keys().reduce<{ [id: string]: DictLocales }>((o, filename) => {\n    const localeModule = req(filename)\n    const json: RawDictLocales = localeModule.locales || localeModule\n    const dictId = /([^/]+)\\/_locales\\.(json|ts)$/.exec(filename)![1]\n    o[dictId] = {\n      name: json.name[lang]\n    }\n    if (json.options) {\n      o[dictId].options = mapValues(json.options, rawLocale => rawLocale[lang])\n    }\n    if (json.helps) {\n      o[dictId].helps = mapValues(json.helps, rawLocale => rawLocale[lang])\n    }\n    return o\n  }, {})\n}\n\nfunction extractSyncServiceLocales(lang: LangCode) {\n  const req = require.context(\n    '@/background/sync-manager/services',\n    true,\n    /_locales\\/.+\\.ts$/\n  )\n  return req.keys().reduce<{ [id: string]: DictLocales }>((o, filename) => {\n    const idMatch = new RegExp(`/([^/]+)/_locales/${lang}\\\\.ts$`).exec(filename)\n    if (idMatch) {\n      const localeModule = req(filename)\n      o[idMatch[1]] = localeModule.locale || localeModule\n    }\n    return o\n  }, {})\n}\n"
  },
  {
    "path": "src/_helpers/injectSaladictInternal.ts",
    "content": "export async function injectDictPanel(tab: browser.tabs.Tab | undefined) {\n  if (tab && tab.id) {\n    const tabId = tab.id\n    const manifest = browser.runtime.getManifest()\n    if (manifest.content_scripts) {\n      for (const script of manifest.content_scripts) {\n        if (script.js) {\n          for (const js of script.js) {\n            await browser.tabs.executeScript(tabId, {\n              file: js[0] === '/' ? js : `/${js}`,\n              allFrames: script.all_frames,\n              matchAboutBlank: script.match_about_blank,\n              runAt: script.run_at\n            })\n          }\n        }\n        if (script.css) {\n          for (const css of script.css) {\n            await browser.tabs.insertCSS(tabId, {\n              file: css[0] === '/' ? css : `/${css}`,\n              allFrames: script.all_frames,\n              matchAboutBlank: script.match_about_blank,\n              runAt: script.run_at\n            })\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/_helpers/integrity.ts",
    "content": "export const isExtTainted =\n  browser.runtime.id !== atob('Y2Rvbm5tZmZrZGFvYWpma25vZWVlY21jaGlicG1rbWc=') &&\n  browser.runtime.id !== atob('c2FsYWRpY3RAY3JpbXguY29t') &&\n  browser.runtime.id !== atob('aWRnaG9jYmJhaGFmcGZoam5maHBiZmJtcGVncGhtbXA=') &&\n  /apple/i.test(navigator.vendor)\n"
  },
  {
    "path": "src/_helpers/lang-check.ts",
    "content": "import memoizeOne from 'memoize-one'\n\nconst languages = [\n  'chinese',\n  'english',\n  'japanese',\n  'korean',\n  'french',\n  'spanish',\n  'deutsch'\n] as const\n\ntype Languages = typeof languages[number]\n\nconst matchers: { [key in Languages]: RegExp } = {\n  chinese: /[\\u4e00-\\u9fa5]/,\n  english: /[a-zA-Z]/,\n  /** Hiragana & Katakana, no Chinese */\n  japanese: /[\\u3041-\\u3096\\u30A0-\\u30FF]/,\n  /** Korean Hangul, no Chinese */\n  korean: /[\\u3131-\\u4dff\\u9fa6-\\uD79D]/,\n  /** French, no English àâäèéêëîïôœùûüÿç */\n  french: /[\\u00e0\\u00e2\\u00e4\\u00e8\\u00e9\\u00ea\\u00eb\\u00ee\\u00ef\\u00f4\\u0153\\u00f9\\u00fb\\u00fc\\u00ff\\u00e7]/i,\n  /** Spanish, no English áéíóúñü¡¿ */\n  spanish: /[\\u00e1\\u00e9\\u00ed\\u00f3\\u00fa\\u00f1\\u00fc\\u00a1\\u00bf]/i,\n  /** Deutsch, no English äöüÄÖÜß */\n  deutsch: /[\\u00E4\\u00F6\\u00FC\\u00C4\\u00D6\\u00DC\\u00df]/i\n}\n\nexport const isContainChinese = (text: string): boolean =>\n  matchers.chinese.test(text)\n\nexport const isContainEnglish = (text: string): boolean =>\n  matchers.english.test(text)\n\n/** Hiragana & Katakana, no Chinese */\nexport const isContainJapanese = (text: string): boolean =>\n  matchers.japanese.test(text)\n\n/** Korean Hangul, no Chinese */\nexport const isContainKorean = (text: string): boolean =>\n  matchers.korean.test(text)\n\n/** French, no English àâäèéêëîïôœùûüÿç */\nexport const isContainFrench = (text: string): boolean =>\n  matchers.french.test(text)\n\n/** Deutsch, no English äöüÄÖÜß */\nexport const isContainDeutsch = (text: string): boolean =>\n  matchers.deutsch.test(text)\n\n/** Spanish, no English áéíóúñü¡¿ */\nexport const isContainSpanish = (text: string): boolean =>\n  matchers.spanish.test(text)\n\nconst isContain: { [key in Languages]: (text: string) => boolean } = {\n  chinese: memoizeOne(isContainChinese),\n  english: memoizeOne(isContainEnglish),\n  /** Hiragana & Katakana, no Chinese */\n  japanese: memoizeOne(isContainJapanese),\n  /** Korean Hangul, no Chinese */\n  korean: memoizeOne(isContainKorean),\n  /** French, no English àâäèéêëîïôœùûüÿç */\n  french: memoizeOne(isContainFrench),\n  /** Spanish, no English áéíóúñü¡¿ */\n  spanish: memoizeOne(isContainSpanish),\n  /** Deutsch, no English äöüÄÖÜß */\n  deutsch: memoizeOne(isContainDeutsch)\n}\n\nconst matcherPunct = /[/[\\]{}$^*+|?.\\-~!@#%&()_='\";:><,。？！，、；：“”﹃﹄「」﹁﹂‘’『』（）—［］〔〕【】…－～·‧《》〈〉﹏＿]/\nconst matchAllMeaningless = new RegExp(`^(\\\\d|\\\\s|${matcherPunct.source})+$`)\n\nconst matcherCJK = new RegExp(\n  `${matchers.chinese.source}|${matchers.japanese.source}|${matchers.korean.source}`\n)\n\nexport const countWords = memoizeOne((text: string): number => {\n  return (\n    text\n      .replace(new RegExp(matcherPunct, 'g'), ' ')\n      .replace(new RegExp(matcherCJK, 'g'), ' x ')\n      .match(/\\S+/g) || ''\n  ).length\n})\n\nexport type SupportedLangs = {\n  [key in Languages | 'others' | 'matchAll']: boolean\n}\n\nexport const supportedLangs: ReadonlyArray<keyof SupportedLangs> = [\n  ...languages,\n  'others',\n  'matchAll'\n]\n\nexport function checkSupportedLangs(\n  langs: SupportedLangs,\n  text: string\n): boolean {\n  if (!text) {\n    return false\n  }\n\n  if (langs.matchAll) {\n    if (matchAllMeaningless.test(text)) {\n      return false\n    }\n\n    if (langs.others) {\n      const checkedMatchers: RegExp[] = [/-|\\.|\\d|\\s/]\n      const uncheckedMatchers: RegExp[] = []\n\n      for (let i = languages.length - 1; i >= 0; i--) {\n        const l = languages[i]\n        if (langs[l]) {\n          checkedMatchers.push(matchers[l])\n        } else {\n          uncheckedMatchers.push(matchers[l])\n        }\n      }\n\n      for (let i = 0; i < text.length; i++) {\n        // characters of latin languages may overlap\n        if (\n          checkedMatchers.every(m => !m.test(text[i])) &&\n          uncheckedMatchers.some(m => m.test(text[i]))\n        ) {\n          return false\n        }\n      }\n      return true\n    } else {\n      const checkedMatchers = languages\n        .filter(l => langs[l])\n        .map(l => matchers[l])\n\n      checkedMatchers.push(/-|\\.|\\d|\\s/)\n\n      for (let i = text.length - 1; i >= 0; i--) {\n        if (checkedMatchers.every(m => !m.test(text[i]))) {\n          return false\n        }\n      }\n      return true\n    }\n  } /* !langs.matchAll */ else {\n    if (languages.some(l => langs[l] && isContain[l](text))) {\n      return true\n    }\n\n    if (!langs.others || matchAllMeaningless.test(text)) {\n      return false\n    }\n\n    const uncheckedMatchers = languages\n      .filter(l => !langs[l])\n      .map(l => matchers[l])\n\n    uncheckedMatchers.push(new RegExp(`${matcherPunct.source}|\\\\d|\\\\s`))\n\n    for (let i = text.length - 1; i >= 0; i--) {\n      if (uncheckedMatchers.every(m => !m.test(text[i]))) {\n        return true\n      }\n    }\n    return false\n  }\n}\n"
  },
  {
    "path": "src/_helpers/matchPatternToRegExpStr.ts",
    "content": "export function matchPatternToRegExpStr(pattern: string): string {\n  if (pattern === '') {\n    return '^(?:http|https|file|ftp|app)://'\n  }\n\n  const schemeSegment = '(\\\\*|http|https|ws|wss|file|ftp)'\n  const hostSegment = '(\\\\*|(?:\\\\*\\\\.)?(?:[^/*]+))?'\n  const pathSegment = '(.*)'\n  const matchPatternRegExp = new RegExp(\n    `^${schemeSegment}://${hostSegment}/${pathSegment}$`\n  )\n\n  const match = matchPatternRegExp.exec(pattern)\n  if (!match) {\n    return ''\n  }\n\n  let [, scheme, host, path] = match\n  if (!host) {\n    return ''\n  }\n\n  let regex = '^'\n\n  if (scheme === '*') {\n    regex += '(http|https)'\n  } else {\n    regex += scheme\n  }\n\n  regex += '://'\n\n  if (host && host === '*') {\n    regex += '[^/]+?'\n  } else if (host) {\n    if (host.match(/^\\*\\./)) {\n      regex += '[^/]*?'\n      host = host.substring(2)\n    }\n    regex += host.replace(/\\./g, '\\\\.')\n  }\n\n  if (path) {\n    if (path === '*') {\n      regex += '(/.*)?'\n    } else if (path.charAt(0) !== '/') {\n      regex += '/'\n      regex += path.replace(/\\./g, '\\\\.').replace(/\\*/g, '.*?')\n      regex += '/?'\n    }\n  }\n\n  return regex + '$'\n}\n"
  },
  {
    "path": "src/_helpers/observables.ts",
    "content": "import {\n  filter,\n  map,\n  switchMap,\n  delay,\n  debounceTime,\n  mergeMap,\n  takeUntil,\n  mapTo\n} from 'rxjs/operators'\nimport { of, Observable, OperatorFunction, from } from 'rxjs'\nimport { MouseEvent } from 'react'\n\n/**\n * Reusable Observable logics\n */\n\n/**\n * Emits true on mouse enter and false on mouse leave.\n * Delay on mouse enter.\n *\n * Shadow DOM does not send mouseenter and mouseleave\n * cross the boundary which means React synthetic\n * event handler will not collect.\n * Here mouseover and mouseout are used to simulate.\n *\n * @param event$ mouseover and mouseout events\n */\nexport function hover<N extends Node>(\n  event$: Observable<MouseEvent<N>>\n): Observable<boolean> {\n  return event$.pipe(\n    filter(\n      e =>\n        e.relatedTarget !== e.currentTarget &&\n        (!(e.relatedTarget instanceof Node) ||\n          !e.currentTarget.contains(e.relatedTarget))\n    ),\n    map(e => e.type === 'mouseover')\n  )\n}\n\n/**\n * [[hover]] with delay on enter.\n */\nexport function hoverWithDelay<N extends Node>(\n  event$: Observable<MouseEvent<N>>\n): Observable<boolean> {\n  return hover(event$).pipe(\n    // delay enter but not leave\n    switchMap(isEnter => of(isEnter).pipe(delay(isEnter ? 500 : 100)))\n  )\n}\n\n/**\n * Emits true is focus and false if blur.\n */\nexport function focusBlur(event$: Observable<{ type: string }>) {\n  return event$.pipe(\n    map(e => e.type !== 'blur'),\n    debounceTime(100)\n  )\n}\n\n/**\n *\n * SwitchMap when value on specific key changes.\n */\nexport function switchMapBy<T, R>(\n  key: keyof T,\n  mapFn: (val: T) => Observable<R> | Promise<R>\n): OperatorFunction<T, R> {\n  return input$ => {\n    return input$.pipe(\n      mergeMap(val =>\n        from(mapFn(val)).pipe(\n          takeUntil(input$.pipe(filter(input => input[key] === val[key])))\n        )\n      )\n    )\n  }\n}\n\nexport function mapToTrue<T>(input$: Observable<T>) {\n  return mapTo(true)(input$)\n}\n"
  },
  {
    "path": "src/_helpers/permission-manager.ts",
    "content": "import { AppConfig } from '@/app-config'\nimport { isFirefox, isOpera, isSafari } from './saladict'\n\nexport async function checkBackgroundPermission(\n  config: AppConfig\n): Promise<void> {\n  // Firefox, Opera and Safari does not support 'background' permission.\n  if (isFirefox || isOpera || isSafari) return\n\n  const backgroundPermissions: browser.permissions.AnyPermissions = {\n    permissions: ['background']\n  }\n  const hasBackground = await browser.permissions.contains(\n    backgroundPermissions\n  )\n  if (config.runInBg) {\n    if (!hasBackground) {\n      await browser.permissions.request(\n        backgroundPermissions as browser.permissions.Permissions\n      )\n    }\n  } else {\n    if (hasBackground) {\n      try {\n        await browser.permissions.remove(\n          backgroundPermissions as browser.permissions.Permissions\n        )\n      } catch (e) {\n        // failed silently on remove\n        console.error(e)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/_helpers/profile-manager.ts",
    "content": "/**\n * Profiles are switchable profiles\n */\nimport pako from 'pako'\nimport {\n  getDefaultProfile,\n  Profile,\n  genProfilesStorage,\n  ProfileIDList,\n  ProfileID\n} from '@/app-config/profiles'\nimport { mergeProfile } from '@/app-config/merge-profile'\nimport { storage } from './browser-api'\nimport { TFunction } from 'i18next'\n\nimport { Observable, from, concat, fromEventPattern } from 'rxjs'\nimport { map } from 'rxjs/operators'\n\nexport interface StorageChanged<T> {\n  newValue: T\n  oldValue?: T\n}\n\nexport interface ProfileChanged {\n  newProfile: Profile\n  oldProfile?: Profile\n}\n\n/** Compressed profile data */\ninterface ProfileCompressed {\n  /** version */\n  v: 1\n  /** data */\n  d: string\n}\n\nexport function deflate(profile: Profile): ProfileCompressed {\n  return {\n    v: 1,\n    d: pako.deflate(JSON.stringify(profile), { to: 'string' })\n  }\n}\n\nexport function inflate(profile: Profile | ProfileCompressed): Profile\nexport function inflate(profile: undefined): undefined\nexport function inflate(\n  profile?: Profile | ProfileCompressed\n): Profile | undefined\nexport function inflate(\n  profile?: Profile | ProfileCompressed\n): Profile | undefined {\n  if (profile && profile['v'] === 1) {\n    return JSON.parse(\n      pako.inflate((profile as ProfileCompressed).d, { to: 'string' })\n    )\n  }\n  return profile as Profile | undefined\n}\n\nexport function getProfileName(name: string, t: TFunction): string {\n  // default names\n  const match = /^%%_(\\S+)_%%$/.exec(name)\n  if (match) {\n    return t(`common:profile.${match[1]}`) || name\n  }\n  return name\n}\n\nexport async function initProfiles(): Promise<Profile> {\n  let profiles: Profile[] = []\n  let profileIDList: ProfileIDList = []\n  let activeProfileID = ''\n\n  const response = await storage.sync.get<{\n    profileIDList: ProfileIDList\n    activeProfileID: string\n  }>(['profileIDList', 'activeProfileID'])\n\n  if (response.profileIDList) {\n    profileIDList = response.profileIDList.filter(item =>\n      Boolean(\n        item && typeof item.id === 'string' && typeof item.name === 'string'\n      )\n    )\n  }\n\n  if (response.activeProfileID) {\n    activeProfileID = response.activeProfileID\n  }\n\n  if (profileIDList.length > 0) {\n    // quota bytes limit\n    for (const { id } of profileIDList) {\n      const profile = await getProfile(id)\n      profiles.push(profile ? mergeProfile(profile) : getDefaultProfile(id))\n    }\n  } else {\n    ;({ profileIDList, profiles } = genProfilesStorage())\n    activeProfileID = profileIDList[0].id\n  }\n\n  if (!activeProfileID) {\n    activeProfileID = profileIDList[0].id\n  }\n\n  let activeProfile = profiles.find(({ id }) => id === activeProfileID)\n  if (!activeProfile) {\n    activeProfile = profiles[0]\n    activeProfileID = activeProfile.id\n  }\n\n  await storage.sync.set({ profileIDList, activeProfileID })\n\n  // quota bytes per item limit\n  for (const profile of profiles) {\n    await updateProfile(profile)\n  }\n\n  return activeProfile\n}\n\nexport async function resetAllProfiles() {\n  const { profileIDList } = await storage.sync.get<{\n    profileIDList: ProfileIDList\n  }>('profileIDList')\n\n  if (profileIDList) {\n    await storage.sync.remove([\n      ...profileIDList.map(({ id }) => id),\n      'profileIDList',\n      'activeProfileID',\n      // legacy\n      'configProfileIDs',\n      'activeConfigID'\n    ])\n  }\n  return initProfiles()\n}\n\nexport async function getProfile(id: string): Promise<Profile | undefined> {\n  return inflate((await storage.sync.get(id))[id])\n}\n\n/**\n * Update profile\n */\nexport async function updateProfile(profile: Profile): Promise<void> {\n  if (process.env.DEBUG) {\n    const profileIDList = await getProfileIDList()\n    if (!profileIDList.find(item => item.id === profile.id)) {\n      console.error(`Update Profile: profile ${profile.id} does not exist`)\n    } else {\n      console.log('Savedd Profile', profile)\n    }\n  }\n  return storage.sync.set({ [profile.id]: deflate(profile) })\n}\n\nexport async function addProfile(profileID: ProfileID): Promise<void> {\n  const id = profileID.id\n  const profileIDList = await getProfileIDList()\n  if (process.env.DEBUG) {\n    if (profileIDList.find(item => item.id === id) || (await getProfile(id))) {\n      console.warn(`Add profile: profile ${id} exists`)\n    }\n  }\n\n  return storage.sync.set({\n    profileIDList: [...profileIDList, profileID],\n    [id]: deflate(getDefaultProfile(id))\n  })\n}\n\nexport async function removeProfile(id: string): Promise<void> {\n  const activeProfileID = await getActiveProfileID()\n  let profileIDList = await getProfileIDList()\n  if (process.env.DEBUG) {\n    if (\n      !profileIDList.find(item => item.id === id) ||\n      !(await getProfile(id))\n    ) {\n      console.warn(`Remove profile: profile ${id} does not exists`)\n    }\n  }\n  profileIDList = profileIDList.filter(item => item.id !== id)\n  if (activeProfileID === id) {\n    await updateActiveProfileID(profileIDList[0].id)\n  }\n  await updateProfileIDList(profileIDList)\n  return storage.sync.remove(id)\n}\n\n/**\n * Get the profile under the current mode\n */\nexport async function getActiveProfile(): Promise<Profile> {\n  const activeProfileID = await getActiveProfileID()\n  if (activeProfileID) {\n    const profile = await getProfile(activeProfileID)\n    if (profile) {\n      return profile\n    }\n  }\n  return getDefaultProfile()\n}\n\nexport async function getActiveProfileID(): Promise<string> {\n  return (await storage.sync.get('activeProfileID')).activeProfileID || ''\n}\n\nexport function updateActiveProfileID(id: string): Promise<void> {\n  return storage.sync.set({ activeProfileID: id })\n}\n\n/**\n * This is mainly for ordering\n */\nexport async function getProfileIDList(): Promise<ProfileIDList> {\n  return (await storage.sync.get('profileIDList')).profileIDList || []\n}\n\n/**\n * This is mainly for ordering\n */\nexport function updateProfileIDList(list: ProfileIDList): Promise<void> {\n  return storage.sync.set({ profileIDList: list })\n}\n\nexport function addActiveProfileIDListener(\n  cb: (changes: StorageChanged<string>) => any\n) {\n  storage.sync.addListener('activeProfileID', ({ activeProfileID }) => {\n    if (activeProfileID && activeProfileID.newValue) {\n      cb(activeProfileID as StorageChanged<string>)\n    }\n  })\n}\n\nexport function addProfileIDListListener(\n  cb: (changes: StorageChanged<ProfileIDList>) => any\n) {\n  storage.sync.addListener('profileIDList', ({ profileIDList }) => {\n    if (profileIDList && profileIDList.newValue) {\n      cb(profileIDList as StorageChanged<ProfileIDList>)\n    }\n  })\n}\n\n/**\n * Listen storage changes of the current profile\n */\nexport async function addActiveProfileListener(\n  cb: (changes: ProfileChanged) => any\n) {\n  let activeID: string | undefined = await getActiveProfileID()\n\n  storage.sync.addListener(changes => {\n    // this id changed\n    if (changes.activeProfileID) {\n      const { newValue: newID, oldValue: oldID } = (changes as {\n        activeProfileID: StorageChanged<string>\n      }).activeProfileID\n      if (newID) {\n        activeID = newID\n        if (oldID) {\n          storage.sync.get([oldID, newID]).then(obj => {\n            if (obj[newID]) {\n              cb({\n                newProfile: inflate(obj[newID]),\n                oldProfile: inflate(obj[oldID])\n              })\n              return\n            }\n          })\n        } else {\n          getProfile(newID).then(newProfile => {\n            if (newProfile) {\n              cb({ newProfile })\n              return\n            }\n          })\n        }\n      }\n    }\n\n    // the active profile itself updated\n    if (activeID && changes[activeID]) {\n      const { newValue, oldValue } = changes[activeID] as StorageChanged<\n        ProfileCompressed\n      >\n      if (newValue) {\n        cb({ newProfile: inflate(newValue), oldProfile: inflate(oldValue) })\n        return\n      }\n    }\n  })\n}\n\n/**\n * Get active profile and create a stream listening to profile changing\n */\nexport function createProfileIDListStream(): Observable<ProfileIDList> {\n  return concat(\n    from(getProfileIDList()),\n    fromEventPattern<\n      [StorageChanged<ProfileIDList>] | StorageChanged<ProfileIDList>\n    >(addProfileIDListListener as any).pipe(\n      map(args => (Array.isArray(args) ? args[0] : args).newValue)\n    )\n  )\n}\n\n/**\n * Get active profile and create a stream listening to profile changing\n */\nexport function createActiveProfileStream(): Observable<Profile> {\n  return concat(\n    from(getActiveProfile()),\n    fromEventPattern<[ProfileChanged] | ProfileChanged>(\n      addActiveProfileListener as any\n    ).pipe(map(args => (Array.isArray(args) ? args[0] : args).newProfile))\n  )\n}\n"
  },
  {
    "path": "src/_helpers/promise-more.ts",
    "content": "/* eslint-disable prettier/prettier */\n\n/**\n * Like Promise.all but always resolves.\n */\nexport function reflect<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>, T9 | PromiseLike<T9>, T10 | PromiseLike<T10>]): Promise<[T1 | null, T2 | null, T3 | null, T4 | null, T5 | null, T6 | null, T7 | null, T8 | null, T9 | null, T10 | null]>\nexport function reflect<T1, T2, T3, T4, T5, T6, T7, T8, T9>(iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>, T9 | PromiseLike<T9>]): Promise<[T1 | null, T2 | null, T3 | null, T4 | null, T5 | null, T6 | null, T7 | null, T8 | null, T9 | null]>\nexport function reflect<T1, T2, T3, T4, T5, T6, T7, T8>(iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>]): Promise<[T1 | null, T2 | null, T3 | null, T4 | null, T5 | null, T6 | null, T7 | null, T8 | null]>\nexport function reflect<T1, T2, T3, T4, T5, T6, T7>(iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>]): Promise<[T1 | null, T2 | null, T3 | null, T4 | null, T5 | null, T6 | null, T7 | null]>\nexport function reflect<T1, T2, T3, T4, T5, T6>(iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>]): Promise<[T1 | null, T2 | null, T3 | null, T4 | null, T5 | null, T6 | null]>\nexport function reflect<T1, T2, T3, T4, T5>(iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>]): Promise<[T1 | null, T2 | null, T3 | null, T4 | null, T5 | null]>\nexport function reflect<T1, T2, T3, T4>(iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>]): Promise<[T1 | null, T2 | null, T3 | null, T4 | null]>\nexport function reflect<T1, T2, T3>(iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>]): Promise<[T1 | null, T2 | null, T3 | null]>\nexport function reflect<T1, T2>(iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>]): Promise<[T1 | null, T2 | null]>\nexport function reflect<T>(iterable: ArrayLike<T | PromiseLike<T>>): Promise<(T | null)[]>\nexport function reflect(iterable: ArrayLike<any>) {\n  const arr = Array.isArray(iterable) ? iterable : Array.from(iterable)\n  return Promise.all(arr.map(p => Promise.resolve(p).catch(() => null)))\n}\n\n/**\n * Like Promise.all but only rejects when all are failed.\n */\nexport function any<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>, T9 | PromiseLike<T9>, T10 | PromiseLike<T10>]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]>\nexport function any<T1, T2, T3, T4, T5, T6, T7, T8, T9>(iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>, T9 | PromiseLike<T9>]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8, T9]>\nexport function any<T1, T2, T3, T4, T5, T6, T7, T8>(iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8]>\nexport function any<T1, T2, T3, T4, T5, T6, T7>(iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>]): Promise<[T1, T2, T3, T4, T5, T6, T7]>\nexport function any<T1, T2, T3, T4, T5, T6>(iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>]): Promise<[T1, T2, T3, T4, T5, T6]>\nexport function any<T1, T2, T3, T4, T5>(iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>]): Promise<[T1, T2, T3, T4, T5]>\nexport function any<T1, T2, T3, T4>(iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>]): Promise<[T1, T2, T3, T4]>\nexport function any<T1, T2, T3>(iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>]): Promise<[T1, T2, T3]>\nexport function any<T1, T2>(iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>]): Promise<[T1, T2]>\nexport function any<T>(iterable: ArrayLike<T | PromiseLike<T>>): Promise<T[]>\nexport function any(iterable: ArrayLike<any>) {\n  const arr = Array.isArray(iterable) ? iterable : Array.from(iterable)\n\n  let rejectCount = 0\n  const promises: Promise<any>[] = arr.map((p, i) =>\n    Promise.resolve(p).catch(e => {\n      rejectCount++\n      return null\n    })\n  )\n\n  return Promise.all(promises).then(resolutions => {\n    if (rejectCount === resolutions.length) {\n      return Promise.reject(new Error('All rejected'))\n    }\n    return Promise.resolve(resolutions)\n  })\n}\n\n/**\n * Returns the first resolved value as soon as it is resolved.\n * Fails when all are failed.\n */\nexport function first<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10> (iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>, T9 | PromiseLike<T9>, T10 | PromiseLike<T10>]): Promise<T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8 | T9 | T10>\nexport function first<T1, T2, T3, T4, T5, T6, T7, T8, T9> (iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>, T9 | PromiseLike<T9>]): Promise<T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8 | T9>\nexport function first<T1, T2, T3, T4, T5, T6, T7, T8> (iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>]): Promise<T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8>\nexport function first<T1, T2, T3, T4, T5, T6, T7> (iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>]): Promise<T1 | T2 | T3 | T4 | T5 | T6 | T7>\nexport function first<T1, T2, T3, T4, T5, T6> (iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>]): Promise<T1 | T2 | T3 | T4 | T5 | T6>\nexport function first<T1, T2, T3, T4, T5> (iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>]): Promise<T1 | T2 | T3 | T4 | T5>\nexport function first<T1, T2, T3, T4> (iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>]): Promise<T1 | T2 | T3 | T4>\nexport function first<T1, T2, T3> (iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>]): Promise<T1 | T2 | T3>\nexport function first<T1, T2> (iterable: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>]): Promise<T1 | T2>\nexport function first<T> (iterable: ArrayLike<T | PromiseLike<T>>): Promise<T>\nexport function first (iterable: ArrayLike<any>) {\n  const arr = Array.isArray(iterable) ? iterable : Array.from(iterable)\n\n  let rejectCount = 0\n  return new Promise((resolve, reject) =>\n    arr.forEach(p => {\n      Promise.resolve(p)\n        .then(resolve)\n        .catch(() => {\n          if (++rejectCount === arr.length) {\n            reject(new Error('All rejected'))\n          }\n        })\n    })\n  )\n}\n\n/**\n * Like setTimeout but returns Promise.\n */\nexport function timer(delay?: number): Promise<void>\nexport function timer<T = any>(delay: number, payload?: T): Promise<T>\nexport function timer(...args) {\n  return new Promise<any>(resolve => {\n    setTimeout(\n      () => (args.length > 1 ? resolve(args[1]) : resolve()),\n      Number(args[0]) || 0\n    )\n  })\n}\n\n/**\n * Timeouts a promise.\n * Rejects when timeout.\n */\nexport function timeout<T>(pr: PromiseLike<T>, delay = 0): Promise<T> {\n  return Promise.race([\n    pr,\n    timer(delay).then(() => Promise.reject(new Error(`timeout ${delay}ms`)))\n  ])\n}\n\nexport default {\n  reflect,\n  any,\n  first,\n  timer,\n  timeout\n}\n"
  },
  {
    "path": "src/_helpers/record-manager.ts",
    "content": "/**\n * Abstracted layer for storing large amount of word records.\n */\n\nimport { message } from '@/_helpers/browser-api'\n\nexport interface Word {\n  /** primary key, milliseconds elapsed since the UNIX epoch */\n  date: number\n  /** word text */\n  text: string\n  /** the sentence where the text string is located */\n  context: string\n  /** page title */\n  title: string\n  /** page url */\n  url: string\n  /** favicon url */\n  favicon: string\n  /** translation */\n  trans: string\n  /** custom note */\n  note: string\n}\n\nexport type DBArea = 'notebook' | 'history'\n\nexport function newWord(word?: Partial<Word>): Word {\n  return word\n    ? {\n        date: word.date || Date.now(),\n        text: word.text || '',\n        context: word.context || '',\n        title: word.title || '',\n        url: word.url || '',\n        favicon: word.favicon || '',\n        trans: word.trans || '',\n        note: word.note || ''\n      }\n    : {\n        date: Date.now(),\n        text: '',\n        context: '',\n        title: '',\n        url: '',\n        favicon: '',\n        trans: '',\n        note: ''\n      }\n}\n\nexport function isInNotebook(word: Word): Promise<boolean> {\n  return message\n    .send<'IS_IN_NOTEBOOK'>({ type: 'IS_IN_NOTEBOOK', payload: word })\n    .catch(logError(false))\n}\n\nexport async function saveWord(area: DBArea, word: Word): Promise<void> {\n  await message.send({ type: 'SAVE_WORD', payload: { area, word } })\n}\n\nexport async function deleteWords(\n  area: DBArea,\n  dates?: number[]\n): Promise<void> {\n  await message.send({ type: 'SYNC_SERVICE_DOWNLOAD' })\n  await message.send({ type: 'DELETE_WORDS', payload: { area, dates } })\n}\n\nexport function getWordsByText(\n  area: DBArea,\n  text: string\n): Promise<readonly Word[]> {\n  return message.send<'GET_WORDS_BY_TEXT'>({\n    type: 'GET_WORDS_BY_TEXT',\n    payload: {\n      area,\n      text\n    }\n  })\n}\n\nexport function getWords(\n  area: DBArea,\n  config: {\n    itemsPerPage?: number\n    pageNum?: number\n    filters: { [field: string]: (string | number)[] | null | undefined }\n    sortField?: string | number | (string | number)[]\n    sortOrder?: 'ascend' | 'descend' | false | null\n    searchText?: string\n  }\n) {\n  return message.send<'GET_WORDS'>({\n    type: 'GET_WORDS',\n    payload: {\n      area,\n      ...config\n    }\n  })\n}\n\nfunction logError<T = any>(valPassThrough: T): (x: any) => T {\n  return err => {\n    if (process.env.DEBUG) {\n      console.error(err)\n    }\n    return valPassThrough\n  }\n}\n"
  },
  {
    "path": "src/_helpers/saladict.ts",
    "content": "/** Pages with the Saladict extension domain */\nexport const isBackgroundPage = () => !!window.__SALADICT_BACKGROUND_PAGE__\n\nexport const isInternalPage = () => !!window.__SALADICT_INTERNAL_PAGE__\n\nexport const isOptionsPage = () => !!window.__SALADICT_OPTIONS_PAGE__\n\nexport const isPopupPage = () => !!window.__SALADICT_POPUP_PAGE__\n\nexport const isPDFPage = () => !!window.__SALADICT_PDF_PAGE__\n\nexport const isQuickSearchPage = () => !!window.__SALADICT_QUICK_SEARCH_PAGE__\n\n/** Dict panel is in a standalone window */\nexport const isStandalonePage = () => isPopupPage() || isQuickSearchPage()\n\n/** do not record search history on these pages */\nexport const isNoSearchHistoryPage = () =>\n  isInternalPage() && !isStandalonePage()\n\nexport const SALADICT_EXTERNAL = 'saladict-external'\n\nexport const SALADICT_PANEL = 'saladict-panel'\n\nexport const isFirefox = navigator.userAgent.includes('Firefox')\nexport const isOpera = navigator.userAgent.includes('OPR')\nexport const isSafari = /apple/i.test(navigator.vendor)\n\n/**\n * Is element in a Saladict external element\n */\nexport function isInSaladictExternal(\n  element: Element | EventTarget | null\n): boolean {\n  if (!element) {\n    return false\n  }\n\n  for (let el: Element | null = element as Element; el; el = el.parentElement) {\n    if (el.classList && el.classList.contains(SALADICT_EXTERNAL)) {\n      return true\n    }\n  }\n\n  return false\n}\n\n/**\n * Is element in Saladict Dict Panel\n */\nexport function isInDictPanel(element: Node | EventTarget | null): boolean {\n  if (!element) {\n    return false\n  }\n\n  for (let el: Element | null = element as Element; el; el = el.parentElement) {\n    if (el.classList && el.classList.contains(SALADICT_PANEL)) {\n      return true\n    }\n  }\n\n  return false\n}\n"
  },
  {
    "path": "src/_helpers/scrollbar-width.ts",
    "content": "import memoizeOne from 'memoize-one'\n\nexport const getScrollbarWidth = memoizeOne(() => {\n  if (typeof document === 'undefined') {\n    return 0\n  }\n\n  const strut = document.createElement('div')\n  const strutStyle = strut.style\n\n  strutStyle.position = 'fixed'\n  strutStyle.left = '0'\n  strutStyle.overflowY = 'scroll'\n  strutStyle.visibility = 'hidden'\n\n  document.body.appendChild(strut)\n\n  const width = strut.getBoundingClientRect().right\n\n  strut.remove()\n\n  return width\n})\n\nexport default getScrollbarWidth\n"
  },
  {
    "path": "src/_helpers/storybook.tsx",
    "content": "import React, { FC, useState, useEffect } from 'react'\nimport classNames from 'classnames'\nimport root from 'react-shadow'\nimport i18next from 'i18next'\nimport { number, boolean } from '@storybook/addon-knobs'\nimport SinonChrome from 'sinon-chrome'\nimport { Message } from '@/typings/message'\nimport { I18nContext, Namespace } from './i18n'\n\ninterface StyleWrapProps {\n  style: string\n}\n\nexport const browser: typeof SinonChrome = window.browser as any\n\nexport const StyleWrap: FC<StyleWrapProps> = props => {\n  return (\n    <div className=\"local-style-wrap\">\n      <style>{props.style}</style>\n      {props.children}\n    </div>\n  )\n}\n\n/**\n * Workaround for {@link https://github.com/storybookjs/storybook/issues/729}\n */\nexport function withLocalStyle(style: object | string) {\n  return function LocalStyle(fn) {\n    return <StyleWrap style={style.toString()}>{fn()}</StyleWrap>\n  }\n}\n\nexport const I18nNS: FC<{ story: Function }> = props => {\n  const [lang, setLang] = useState(i18next.language)\n\n  useEffect(() => {\n    const onLangChanged = (lang: string) => {\n      setLang(lang)\n    }\n\n    i18next.on('languageChanged', onLangChanged)\n\n    return () => i18next.off('languageChanged', onLangChanged)\n  }, [])\n\n  return (\n    <I18nContext.Provider value={lang}>{props.story()}</I18nContext.Provider>\n  )\n}\n\nexport function withi18nNS(ns: Namespace | Namespace[]) {\n  // eslint-disable-next-line react/display-name\n  return fn => {\n    i18next.loadNamespaces(ns)\n    return <I18nNS story={fn} />\n  }\n}\n\n/**\n * Perform side effects and clean up when switching stroies\n * @param fn performs side effects and optionally returns a clean-up function\n */\nexport function withSideEffect(fn: React.EffectCallback) {\n  const SideEffect: FC<{ story: Function }> = props => {\n    useEffect(fn, [])\n    return <>{props.story()}</>\n  }\n  // eslint-disable-next-line react/display-name\n  return story => <SideEffect story={story} />\n}\n\nexport function mockRuntimeMessage(fn: (message: Message) => Promise<any>) {\n  return () => {\n    browser.runtime.sendMessage.callsFake(fn)\n\n    return () => {\n      browser.runtime.sendMessage.callsFake(() => Promise.resolve())\n    }\n  }\n}\nexport interface WithSaladictPanelOptions {\n  /** before the story component */\n  head?: React.ReactNode\n  width?: number | string\n  height?: number | string\n  withAnimation?: boolean\n  fontSize?: number | string\n  color?: string\n  backgroundColor?: string\n}\n\n/**\n * Fake salalict panel\n */\nexport function withSaladictPanel(options: WithSaladictPanelOptions) {\n  return function SaladcitPanel(story: Function) {\n    const width =\n      options.width != null ? options.width : number('Panel Width', 450)\n\n    const height =\n      options.height != null\n        ? options.height\n        : number('Panel Height', window.innerHeight - 50)\n\n    const darkMode = boolean('Dark Mode', false)\n\n    const withAnimation =\n      options.withAnimation != null\n        ? options.withAnimation\n        : boolean('Enable Animation', true)\n\n    const fontSize =\n      options.fontSize != null\n        ? options.fontSize\n        : number('Panel Font Size', 13)\n\n    return (\n      <root.div style={{ width, margin: '10px auto' }}>\n        <div\n          className={classNames('dictPanel-Root', 'saladict-theme', {\n            isAnimate: withAnimation,\n            darkMode\n          })}\n        >\n          <style>{require('@/_sass_shared/_reset.scss').toString()}</style>\n          <style>{require('@/_sass_shared/_theme.scss').toString()}</style>\n          <div\n            className=\"dictPanel-Root saladict-theme\"\n            style={{\n              color: options.color,\n              backgroundColor: options.backgroundColor,\n              fontSize,\n              width,\n              height,\n              '--panel-font-size': fontSize + 'px',\n              '--panel-width': `${width}px`,\n              '--panel-max-height': `${number(\n                'Panel Max Hegiht',\n                window.innerHeight\n              )}px`\n            }}\n            // bug https://github.com/storybookjs/storybook/issues/6569\n            onKeyDown={e => e.stopPropagation()}\n          >\n            {options.head}\n            {story({\n              width,\n              height,\n              fontSize,\n              darkMode,\n              withAnimation\n            })}\n          </div>\n        </div>\n      </root.div>\n    )\n  }\n}\n"
  },
  {
    "path": "src/_helpers/titlebar-offset.ts",
    "content": "/**\n * Extension API is inconsistent with the window top.\n * Sometimes the titlebar height is included, sometimes not.\n */\n\nimport { storage } from './browser-api'\nimport { timer } from './promise-more'\n\nexport interface TitlebarOffset {\n  // main window title bar height\n  main: number\n  // panel window title bar height\n  panel: number\n}\n\nexport async function getTitlebarOffset(): Promise<TitlebarOffset | undefined> {\n  return (\n    await storage.local.get<{ titlebarOffset?: TitlebarOffset }>(\n      'titlebarOffset'\n    )\n  ).titlebarOffset\n}\n\nexport function setTitlebarOffset(offset: TitlebarOffset): Promise<void> {\n  return storage.local.set({ titlebarOffset: offset })\n}\n\nexport async function calibrateTitlebarOffset(): Promise<\n  TitlebarOffset | undefined\n> {\n  try {\n    const curWin = await browser.windows.getCurrent()\n    if (curWin.id == null) return\n\n    const mainWin = await browser.windows.create({ state: 'maximized' })\n    const panelWin = await browser.windows.create({\n      state: 'maximized',\n      type: 'panel'\n    })\n\n    if (mainWin?.id == null || panelWin?.id == null) return\n\n    await browser.windows.update(curWin.id, { focused: true })\n\n    await timer(0)\n\n    const main = (await browser.windows.get(mainWin.id)).top\n    const panel = (await browser.windows.get(panelWin.id)).top\n\n    browser.windows.remove(mainWin.id)\n    browser.windows.remove(panelWin.id)\n\n    if (main == null || panel == null) return\n\n    return { main, panel }\n  } catch (e) {\n    if (process.env.DEBUG) {\n      console.error(e)\n    }\n  }\n}\n"
  },
  {
    "path": "src/_helpers/translateCtx.ts",
    "content": "import { DictID, AppConfig } from '@/app-config'\nimport { MachineTranslateResult } from '@/components/dictionaries/helpers'\nimport { message } from './browser-api'\nimport { isPDFPage } from './saladict'\n\nexport type CtxTranslatorId = keyof AppConfig['ctxTrans']\n\nexport type CtxTranslateResults = {\n  [id in CtxTranslatorId]: string\n}\n\nexport interface FetchDictResultResponse {\n  id: DictID\n  result: MachineTranslateResult<DictID>\n}\n\n/**\n * translate selection context with selected machine translatior\n * @param text search text\n * @param id machine translatior id\n */\nexport async function translateCtx(\n  text: string,\n  id: CtxTranslatorId\n): Promise<string> {\n  try {\n    const response = await message.send<\n      'FETCH_DICT_RESULT',\n      FetchDictResultResponse\n    >({\n      type: 'FETCH_DICT_RESULT',\n      payload: {\n        id,\n        text,\n        payload: { isPDF: isPDFPage() }\n      }\n    })\n\n    return (\n      (response &&\n        response.result &&\n        response.result.trans &&\n        response.result.trans.paragraphs.join('\\n')) ||\n      ''\n    )\n  } catch (e) {\n    return ''\n  }\n}\n\n/**\n * translate selection context with selected machine translatiors\n * @param text search text\n * @param ctxTrans machine translatiors\n */\nexport async function translateCtxs(\n  text: string,\n  ctxTrans: AppConfig['ctxTrans']\n): Promise<CtxTranslateResults> {\n  return (\n    await Promise.all(\n      Object.keys(ctxTrans).map(async id => {\n        let content = ''\n        if (ctxTrans[id]) {\n          try {\n            content = await translateCtx(text, id as CtxTranslatorId)\n          } catch (e) {\n            console.warn(e)\n          }\n        }\n        return { id, content }\n      })\n    )\n  ).reduce((result, { id, content }) => {\n    result[id] = content\n    return result\n  }, {} as CtxTranslateResults)\n}\n\n/**\n * get translator result from text\n */\nexport function parseCtxText(text: string): CtxTranslateResults {\n  const matcher = /\\[:: (\\w+) ::\\]\\n([\\s\\S]+?)(?=(?:\\[:: \\w+ ::\\])|(?:-{15}))/g\n  let matchResult: RegExpExecArray | null\n  const result = {} as CtxTranslateResults\n  while ((matchResult = matcher.exec(text)) !== null) {\n    result[matchResult[1] as CtxTranslatorId] = matchResult[2].replace(\n      /\\n+$/g,\n      ''\n    )\n  }\n  return result\n}\n\n/**\n * Add Context translate result to text\n * @param text original text\n */\nexport function genCtxText(\n  text: string,\n  ctxTransResult: CtxTranslateResults\n): string {\n  const enginesWithResult = Object.keys(ctxTransResult).filter(\n    id => ctxTransResult[id]\n  )\n\n  if (enginesWithResult.length <= 0) {\n    return text\n  }\n\n  const ctxResults =\n    enginesWithResult\n      .map(id => `[:: ${id} ::]\\n` + ctxTransResult[id])\n      .join('\\n\\n') + `\\n${''.padEnd(15, '-')}\\n`\n\n  if (!text) {\n    return ctxResults\n  }\n\n  const matcher = /\\[:: (\\w+) ::\\]\\n([\\s\\S]+?)-{15}/\n\n  if (matcher.test(text)) {\n    return text.replace(matcher, ctxResults)\n  }\n\n  return text + '\\n\\n' + ctxResults\n}\n"
  },
  {
    "path": "src/_helpers/uniqueKey.ts",
    "content": "/**\n * Generate a unique key\n */\nexport function genUniqueKey(): string {\n  return (\n    Date.now()\n      .toString()\n      .slice(6) +\n    Math.random()\n      .toString()\n      .slice(2, 8)\n  )\n}\n\nexport function genUniqueKeyThunk() {\n  return genUniqueKey\n}\n\nexport function isGeneratedKey(key: unknown): boolean {\n  return typeof key === 'string' && /^\\d{13}$/.test(key)\n}\n"
  },
  {
    "path": "src/_helpers/wordoftheday.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport { first } from '@/_helpers/promise-more'\nimport { handleNoResult, getText } from '@/components/dictionaries/helpers'\n\nexport async function getWordOfTheDay(): Promise<string> {\n  if (!process.env.DEBUG) {\n    try {\n      return await first([\n        getWebsterWordOfTheDay(),\n        getDictionaryWordOfTheDay()\n      ])\n    } catch (e) {}\n  }\n  return 'salad'\n}\n\nexport async function getWebsterWordOfTheDay(): Promise<string> {\n  const doc = await fetchDirtyDOM(\n    'https://www.merriam-webster.com/word-of-the-day'\n  )\n  const text = getText(doc, 'title')\n  const matchResult = text.match(/Word of the Day: (.+) \\| Merriam-Webster/)\n  return (matchResult && matchResult[1]) || handleNoResult()\n}\n\nexport async function getDictionaryWordOfTheDay(): Promise<string> {\n  const doc = await fetchDirtyDOM('https://www.dictionary.com/wordoftheday/')\n  const text = getText(doc, 'title')\n  const matchResult = text.match(\n    /Get the Word of the Day - (.+) \\| Dictionary\\.com/\n  )\n  return (matchResult && matchResult[1]) || handleNoResult()\n}\n"
  },
  {
    "path": "src/_locales/en/background.ts",
    "content": "import { locale as _locale } from '../zh-CN/background'\n\nexport const locale: typeof _locale = {\n  app: {\n    off: 'Saladict disabled. (Quick Search Panel is still available)',\n    tempOff:\n      'Saladict disabled on current tab. (Quick Search Panel is still available)',\n    unsupported:\n      'Embedded Saladict panel is unsupported for current tab. Use Standalone Saladict panel instead.'\n  }\n}\n"
  },
  {
    "path": "src/_locales/en/common.ts",
    "content": "import { locale as _locale } from '../zh-CN/common'\n\nexport const locale: typeof _locale = {\n  add: 'Add',\n  delete: 'Delete',\n  save: 'Save',\n  cancel: 'Cancel',\n\n  edit: 'Edit',\n  sort: 'Sort',\n  rename: 'Rename',\n\n  confirm: 'Confirm',\n  changes_confirm: 'Changes not saved. Close anyway?',\n  delete_confirm: 'Deleted item completely?',\n\n  max: 'Max',\n  min: 'Min',\n\n  name: 'Name',\n  none: 'None',\n\n  enable: 'Enable',\n  enabled: 'Enabled',\n  disabled: 'Disabled',\n\n  blacklist: 'Blacklist',\n  whitelist: 'Whitelist',\n\n  import: 'Import',\n  export: 'Export',\n\n  lang: {\n    chinese: 'Chinese',\n    chs: 'Chinese',\n    deutsch: 'Deutsch',\n    eng: 'English',\n    english: 'English',\n    french: 'French',\n    japanese: 'Japanese',\n    korean: 'Korean',\n    matchAll: 'Match every character',\n    minor: 'Minor',\n    others: 'Others',\n    spanish: 'Spanish'\n  },\n\n  unit: {\n    mins: 'minutes',\n    ms: 'ms',\n    s: 's',\n    word: 'words'\n  },\n\n  note: {\n    word: 'Word',\n    trans: 'Translation',\n    note: 'Note',\n    context: 'Context',\n    contextCloze: 'Context Cloze',\n    date: 'Date',\n    srcTitle: 'Source Title',\n    srcLink: 'Source Link',\n    srcFavicon: 'Source Favicon'\n  },\n\n  profile: {\n    daily: 'Daily Mode',\n    sentence: 'Sentence Mode',\n    default: 'Default Mode',\n    scholar: 'Scholar Mode',\n    translation: 'Translation Mode',\n    nihongo: 'Japanese Mode'\n  }\n}\n"
  },
  {
    "path": "src/_locales/en/content.ts",
    "content": "import { locale as _locale } from '../zh-CN/content'\n\nexport const locale: typeof _locale = {\n  chooseLang: 'Choose another language',\n  standalone: 'Saladict Standalone Panel',\n  fetchLangList: 'Fetch full language list',\n  transContext: 'Retranslate',\n  neverShow: 'Stop showing',\n  fromSaladict: 'From Saladict Panel',\n  tip: {\n    historyBack: 'Previous search history',\n    historyNext: 'Next search history',\n    searchText: 'Search text',\n    openOptions: 'Open Options',\n    addToNotebook: 'Add to Notebook. Right click to open Notebook',\n    openNotebook: 'Open Notebook',\n    openHistory: 'Open History',\n    shareImg: 'Share as image',\n    pinPanel: 'Pin the panel',\n    closePanel: 'Close the panel',\n    sidebar: 'Switch to sidebar mode. Right click to right side.',\n    focusPanel: 'Panel gains focus when searching',\n    unfocusPanel: 'Panel does not gain focus when searching'\n  },\n  wordEditor: {\n    title: 'Add to Notebook',\n    wordCardsTitle: 'Other results from Notebook',\n    deleteConfirm: 'Delete from Notebook?',\n    closeConfirm: 'Changes will not be saved. Are you sure to close?',\n    chooseCtxTitle: 'Pick translated results',\n    ctxHelp:\n      'Keep the [:: xxx ::] and --------------- format if you want Saladict to handle translation selection and generate Anki table.'\n  },\n  machineTrans: {\n    switch: 'Switch Language',\n    sl: 'Source Language',\n    tl: 'Target Language',\n    auto: 'Detect language',\n    stext: 'Original',\n    showSl: 'Show Source',\n    copySrc: 'Copy Source',\n    copyTrans: 'Copy Translation',\n    login: 'Please provide {access token}.',\n    dictAccount: 'access token'\n  },\n  updateAnki: {\n    title: 'Update to Anki',\n    success: 'Successfully update word to Anki.',\n    failed: 'Failed to update word to Anki.'\n  }\n}\n"
  },
  {
    "path": "src/_locales/en/langcode.ts",
    "content": "import en from '@opentranslate/languages/locales/en.json'\nimport { locale as _locale } from '../zh-CN/langcode'\n\nexport const locale: typeof _locale = {\n  ...en,\n  default: 'Default',\n  ne_NP: 'Nepali',\n  ara: 'Arabic',\n  'bs-Latn': 'Bosnian',\n  bul: 'Bulgarian',\n  cht: 'Chinese (Traditional)',\n  dan: 'Danish',\n  est: 'Estonian',\n  fin: 'Finnish',\n  fra: 'French',\n  iw: 'Hebrew',\n  jp: 'Japanese',\n  kor: 'Korean',\n  kr: 'Korean',\n  pt_BR: 'Brazilian',\n  rom: 'Romanian',\n  slo: 'Slovenian',\n  spa: 'Spanish',\n  swe: 'Swedish',\n  tl: 'Tagalog (Filipino)',\n  vie: 'Vietnamese',\n  zh: 'Chinese (Simplified)',\n  'zh-CHS': 'Chinese (Simplified)',\n  'zh-CHT': 'Chinese (Traditional)'\n}\n"
  },
  {
    "path": "src/_locales/en/menus.ts",
    "content": "import { locale as _locale } from '../zh-CN/menus'\n\nexport const locale: typeof _locale = {\n  baidu_page_translate: 'Baidu Page Translate',\n  baidu_search: 'Baidu Search',\n  bing_dict: 'Bing Dict',\n  bing_search: 'Bing Search',\n  caiyuntrs: 'Lingocloud Page Translate',\n  cambridge: 'Cambridge',\n  copy_pdf_url: 'Copy PDF URL to Clipboard',\n  dictcn: 'Dictcn',\n  etymonline: 'Etymonline',\n  google_cn_page_translate: 'Google cn Page Translate',\n  google_page_translate: 'Google Page Translate',\n  google_search: 'Google Search',\n  google_translate: 'Google Translate',\n  google_cn_translate: 'Google.cn Translate',\n  guoyu: '國語辭典',\n  history_title: 'Search History',\n  iciba: 'iciba',\n  liangan: '兩岸詞典',\n  longman_business: 'Longman Business',\n  manual_title: 'Manual',\n  merriam_webster: 'Merriam Webster',\n  microsoft_page_translate: 'Microsoft Page Translate',\n  notebook_title: 'New Word List',\n  notification_youdao_err:\n    'Youdao Page Translate 2.0 not responding.\\nSaladict might not have permission to access this page.\\nIgnore this message if Youdao panal is shown.',\n  oxford: 'Oxford',\n  page_permission_err:\n    'Saladict \"{{name}}\" does not have permission to access this page.',\n  page_translations: 'Page Translations',\n  saladict: 'Saladict',\n  saladict_standalone: 'Saladict Standalone Panel',\n  sogou: 'Sogou Translate',\n  sogou_page_translate: 'Sogou Page Translate',\n  termonline: 'Termonline',\n  view_as_pdf: 'Open in PDF Viewer',\n  youdao: 'Youdao',\n  youdao_page_translate: 'Youdao Page Translate',\n  youglish: 'YouGlish'\n}\n"
  },
  {
    "path": "src/_locales/en/options.ts",
    "content": "import { locale as _locale } from '../zh-CN/options'\n\nexport const locale: typeof _locale = {\n  title: 'Saladict Options',\n  previewPanel: 'Preview Dict Panel',\n  shortcuts: 'Set Shortcuts',\n  msg_update_error: 'Unable to update',\n  msg_updated: 'Successfully updated',\n  msg_first_time_notice: 'First time notice',\n  msg_err_permission: 'Unable to request \"{{permission}}\" permission.',\n  unsave_confirm: 'Settings not saved. Sure to leave?',\n  nativeSearch: 'search selected text outside of browser',\n  firefox_shortcuts:\n    'Open about:addons, click the top right \"gear\" button, choose the last \"Manage extension shortcuts\".',\n  tutorial: 'Tutorial',\n  page_selection: 'Page Selection',\n\n  nav: {\n    General: 'General',\n    Notebook: 'Notebook',\n    Profiles: 'Profiles',\n    DictPanel: 'Dict Panel',\n    SearchModes: 'Search Modes',\n    Dictionaries: 'Dictionaries',\n    DictAuths: 'Access Tokens',\n    Popup: 'Popup Panel',\n    QuickSearch: 'Quick Search',\n    Pronunciation: 'Pronunciation',\n    PDF: 'PDF',\n    ContextMenus: 'Context Menus',\n    BlackWhiteList: 'Black/White List',\n    ImportExport: 'Import/Export',\n    Privacy: 'Privacy',\n    Permissions: 'Permissions'\n  },\n\n  config: {\n    active: 'Enable Inline Translator',\n    active_help:\n      '\"Quick Search\" is still available even if Inline translation is turned off.',\n    animation: 'Animation transitions',\n    animation_help: 'Switch off animation transitions to reduce runtime cost.',\n    runInBg: 'Keep in Background',\n    runInBg_help:\n      'Keep the browser running in background after close so that global shortcuts still work.',\n    darkMode: 'Dark Mode',\n    langCode: 'App Language',\n    editOnFav: 'Open WordEditor when saving',\n    editOnFav_help:\n      'When turned off, new words will be added to notebook directly.',\n    searchHistory: 'Keep search history',\n    searchHistory_help:\n      'Your browsing history could be unintentionally revealed in Search history.',\n    searchHistoryInco: 'Also in incognito mode',\n    ctxTrans: 'Context Translate Engines',\n    ctxTrans_help:\n      'Context sentence will be translated before being added to notebook.',\n    searchSuggests: 'Search suggests',\n    panelMaxHeightRatio: 'Panel max height ratio',\n    panelWidth: 'Panel width',\n    fontSize: 'Font size for search reasults',\n    bowlOffsetX: 'Saladict icon Offset X',\n    bowlOffsetY: 'Saladict icon Offset Y',\n    panelCSS: 'Custom Dict Panel Styles',\n    panelCSS_help:\n      'Custom CSS. For Dict Panel use .dictPanel-Root as root. For dictionaries use .dictRoot or .d-{id} as root',\n    noTypeField: 'No selection on editable regions',\n    noTypeField_help:\n      'If selection making in editable regions is banned, the extension will identify Input Boxes, TextAreas and other common text editors like CodeMirror, ACE and Monaco.',\n    touchMode: 'Touch Mode',\n    touchMode_help: 'Enable touch related selection',\n    language: 'Selection Languages',\n    language_help:\n      'Search when selection contains words in the chosen languages.',\n    language_extra:\n      'Note that Japanese and Korean also include Chinese. French, Deutsch and Spanish also include English. If Chinese or English is cancelled while others are selected, only the exclusive parts of those languages are tested. E.g. kana characters in Japanese.',\n    doubleClickDelay: 'Double Click Delay',\n    mode: 'Normal Selection',\n    panelMode: 'Inside Dict Panel',\n    pinMode: 'When Panel is Pinned',\n    qsPanelMode: 'When Standalone Panel is Opened',\n    bowlHover: 'Icon Mouse Hover',\n    bowlHover_help:\n      'Hover on the bowl icon to trigger searching instead of clicking.',\n    autopron: {\n      cn: {\n        dict: 'Chinese Auto-Pronounce'\n      },\n      en: {\n        dict: 'English Auto-Pronounce',\n        accent: 'Accent Preference'\n      },\n      machine: {\n        dict: 'Machine Auto-Pronounce',\n        src: 'Machine Pronounce',\n        src_help:\n          'Machine Translation Dictionary needs to be added and enabled on the list below to enable auto-pronunciation.',\n        src_search: 'Read Source Text',\n        src_trans: 'Read Translation Text'\n      }\n    },\n    pdfSniff: 'Enable PDF Sniffer',\n    pdfSniff_help: 'If turned on， PDF links will be automatically captured.',\n    pdfSniff_extra:\n      'It is recommended to {search selected text outside of browser} with your own favorite local reader.',\n    pdfStandalone: 'Standalone Panel',\n    pdfStandalone_help: 'Open PDF viewer in standalone panel.',\n    baWidth: 'Width',\n    baWidth_help:\n      'Browser Action Panel wdith. Dict Panel width will be used if a negative value is chosen.',\n    baHeight: 'Height',\n    baHeight_help: 'Browser Action Panel height.',\n    baOpen: 'Browser Action',\n    baOpen_help:\n      'When clicking the browser action icon in toolbar (next to the address bar). Items are same as Context Menus, which can be added or edited on the Context Menus config page.',\n    tripleCtrl: 'Enable Ctrl Shortkey',\n    tripleCtrl_help:\n      'Press {⌘ Command}(macOS) or {Ctrl}(Others) three times (or with browser shortkey) to summon the dictionary panel. ',\n    defaultPinned: 'Pinned when shows up',\n    qsLocation: 'Location',\n    qsFocus: 'Focus when shows up',\n    qsStandalone: 'Standalone',\n    qsStandalone_help:\n      'Render dict panel in a standalone window. You can {search selected text outside of browser}.',\n    qssaSidebar: 'Sidebar Layout',\n    qssaSidebar_help: 'Rearrange windows to sidebar-like layout.',\n    qssaHeight: 'Window Height',\n    qssaPageSel: 'Selection Response',\n    qssaPageSel_help: 'Response to page selection.',\n    qssaRectMemo: 'Remember size and position',\n    qssaRectMemo_help: 'Remember standalone panel size and position on close.',\n    updateCheck: 'Check Update',\n    updateCheck_help: 'Check update automatically.',\n    analytics: 'Enable Google Analytics',\n    analytics_help:\n      'Share anonymous device browser version information. Saladict author will offer prioritized support to popular devices and browsers.',\n\n    opt: {\n      reset: 'Reset Configs',\n      reset_confirm: 'Reset to default settings. Confirm？',\n      upload_error: 'Unable to save settings.',\n      accent: {\n        uk: 'UK',\n        us: 'US'\n      },\n      sel_blackwhitelist: 'Selection Black/White List',\n      sel_blackwhitelist_help:\n        'Saladict will not react to selection in blacklisted pages.',\n      pdf_blackwhitelist_help:\n        'Blacklisted PDF links will not jump to Saladict PDF Viewer.',\n      contextMenus_description:\n        'Each context menus item can also be customized. Youdao and Google page translate are deprecated in favor of the official extensions.',\n      contextMenus_edit: 'Edit Context Menus Items',\n      contextMenus_url_rules: 'URL with %s in place of query.',\n      baOpen: {\n        popup_panel: 'Dict Panel',\n        popup_fav: 'Add to Notebook',\n        popup_options: 'Open Saladict Options',\n        popup_standalone: 'Open Saladict Standalone Panel'\n      },\n      openQsStandalone: 'Standalone Panel Options',\n      pdfStandalone: {\n        default: 'Never',\n        always: 'Always',\n        manual: 'Manual'\n      }\n    }\n  },\n\n  matchPattern: {\n    description:\n      'Specify URL as {URL Match Pattern} or {Regular Expression}. Empty fields will be removed.',\n    url: 'URL Match Pattern',\n    url_error: 'Incorrect URL Match Pattern.',\n    regex: 'Regular Expression',\n    regex_error: 'Incorrect Regular Expression.'\n  },\n\n  searchMode: {\n    icon: 'Show Icon',\n    icon_help: 'A cute little icon pops up nearby the cursor.',\n    direct: 'Direct Search',\n    direct_help: 'Show dict panel directly.',\n    double: 'Double Click',\n    double_help: 'Show dict panel after double click selection.',\n    holding: 'Hold a key',\n    holding_help:\n      'After a selection is made, the selected key must be pressing when releasing mouse (Alt is \"⌥ Option\" on macOS. Meta key is \"⌘ Command\" on macOS and \"⊞ Windows\" for others.).',\n    instant: 'Instant Capture',\n    instant_help: 'Selection is automatically made near by the cursor.',\n    instantDirect: 'Direct',\n    instantKey: 'Key',\n    instantKey_help:\n      'If \"Direct\" is chosen it is also recommeded setting browser shortkey to toggle Instant Capture. Otherwise browser text selection could be unable to perform.',\n    instantDelay: 'Capture delay'\n  },\n\n  profiles: {\n    opt: {\n      add_name: 'Add Profile Name',\n      delete_confirm: 'Delete Profile \"{{name}}\". Confirm?',\n      edit_name: 'Change Profile Name',\n      help:\n        'Each profile represents an independent set of settings. Some of the settings (with {*} prefix) change according to profile. One may switch profiles by hovering on the menu icon on Dict Panel, or focus on the icon then hit {↓}.'\n    }\n  },\n\n  profile: {\n    mtaAutoUnfold: 'Auto unfold multiline search box',\n    waveform: 'Waveform Control',\n    waveform_help:\n      'Display a button at the bottom of the Dict Panel for expanding the Waveform Control Panel which is only loaded after expansion.',\n    stickyFold: 'Sticky Folding',\n    stickyFold_help:\n      'Remembers manual dictionary folding/unfolding states when searching. Only last on the same page.',\n\n    opt: {\n      item_extra: 'This option may change base on \"Profile\".',\n      mtaAutoUnfold: {\n        always: 'Keep Unfolding',\n        never: 'Never Unfold',\n        once: 'Unfold Once',\n        popup: 'Only On Browser Action',\n        hide: 'Hide'\n      },\n      dict_selected: 'Selected Dicts'\n    }\n  },\n\n  dict: {\n    add: 'Add dicts',\n    more_options: 'More Options',\n\n    selectionLang: 'Selection Languages',\n    selectionLang_help:\n      'Show this dictionary when selection contains words in the chosen languages.',\n    defaultUnfold: 'Default Unfold',\n    defaultUnfold_help:\n      \"If turned off, this dictionary won't start searching unless it's title bar is clicked.\",\n    selectionWC: 'Selection Word Count',\n    selectionWC_help:\n      'Show this dictionary when selection word count meets the requirements. Set 999999 for unlimited words.',\n    preferredHeight: 'Default Panel Height',\n    preferredHeight_help:\n      'Maximum height on first appearance. Contents exceeding this height will be hidden. Set 999999 for unlimited height.',\n\n    lang: {\n      de: 'De',\n      en: 'En',\n      es: 'Es',\n      fr: 'Fr',\n      ja: 'Ja',\n      kor: 'Kor',\n      zhs: 'Zhs',\n      zht: 'Zht'\n    }\n  },\n\n  syncService: {\n    description: 'Sync settings.',\n    start: 'Syncing. Do not close this page until finished.',\n    finished: 'Syncing finished',\n    success: 'Syncing success',\n    failed: 'Syncing failed',\n    close_confirm: 'Settings not saved. Close?',\n    delete_confirm: 'Delete?',\n\n    shanbay: {\n      description:\n        \"Go to shanbay.com and log in first(must stay logged in). Note that it's a one-way sync(from Saladict to Shanbay). Only the new added words are synced. Words also need to be supported by Shanbay's database.\",\n      login:\n        'Will open shanbay.com. Please log in then come back and enable again.',\n      sync_all: 'Upload all existing new words',\n      sync_all_confirm:\n        'Too many new words in notebook. Saladict will upload in batches. Note that uploading too many words in short period would cause account banning which is unrecoverable. Confirm?',\n      sync_last: 'Upload the last new word'\n    },\n\n    eudic: {\n      description:\n        'Before using Eudic to synchronize words, you must first create a default new word book on Eudic official website (my.eudic.net/home/index) (generally, it will be automatically generated and cannot be deleted after the first manual import). Pay attention not to synchronize frequently in a short time, which may cause temporary lock.',\n      token: 'Authorization information',\n      getToken: 'Get authorization',\n      verify: 'Check authorization information',\n      verified: 'Eudic authorization information checked successfully',\n      enable_help:\n        'After opening, each new word added will be automatically synchronized to the Eudic default word book (salad to Eudic word book) in one direction, and only the new word itself will be synchronized (deleted out of synchronization)',\n      token_help:\n        'Please confirm to set valid personal authorization information, otherwise the synchronization will fail. You can click the button at the bottom to check.',\n      sync_all: 'Synchronize all new words',\n      sync_help:\n        'Synchronize all existing new words in salad word book to the Eudic default word book (turn on the synchronization switch above at the same time and click save)',\n      sync_all_confirm:\n        'Note that frequent synchronization in a short time may lead to lock temporarily. Are you sure to continue?'\n    },\n\n    webdav: {\n      description:\n        'Extension settings (including this) are synced via browser. New words notebook can be synced via WebDAV through settings here.',\n      jianguo: 'See Jianguoyun for example',\n      checking: 'Connecting...',\n      exist_confirm:\n        'Saladict directory exists on server. Download it and merge with local data?',\n      upload_confirm: 'Upload local data to Server right away?',\n      verify: 'Verify server',\n      verified: 'Successfully verified WebDAV server.',\n      duration: 'Duration',\n      duration_help:\n        'Data is guaranteed to be updated before upload. If you do not need real-time syncing across browsers, set a longer polling cycle to reduce CPU and memory footprint.',\n      passwd: 'Password',\n      url: 'Server Address',\n      user: 'User Account'\n    },\n\n    ankiconnect: {\n      description:\n        'Please make sure Anki Connect plugin is installed and Anki is running. You can also update word to Anki in Word Editor.',\n      checking: 'Checking...',\n      deck_confirm:\n        'Deck \"{{deck}}\" does not exist in Anki. Generate a new deck?',\n      deck_error: 'Unable to create deck \"{{deck}}\".',\n      notetype_confirm:\n        'Note type \"{{noteType}}\" does not exist in Anki. Generate a new note type.',\n      notetype_error: 'Unable to create note type \"{{noteType}}\".',\n      upload_confirm:\n        'Sync local new words to Anki right away? Duplicated words (with same timestamp) will be skipped.',\n      add_yourself: 'Please add it youself in Anki.',\n      verify: 'Verify Anki Connect',\n      verified: 'Successfully verified Anki Connect',\n      enable_help:\n        'When enabled, each time a new word is added to Notebook it will also be ported to Anki automatically. Words that exist in Anki(with same \"Date\") can be force-updated in Word Editor.',\n      host: 'Address',\n      port: 'port',\n      key: 'Key',\n      key_help:\n        'Optional key can be added in Anki Connect config for identification.',\n      deckName: 'Deck',\n      deckName_help:\n        'If deck does not exist you can generate a default one automatically by clicking \"Verify Anki Connect\" below.',\n      noteType: 'Note Type',\n      noteType_help:\n        'Anki note type includes a set of fields and card type. If note type does not exist you can generate a default one automatically by clicking \"Verify Anki Connect\" below. DO NOT change field names when editing or adding card templates in Anki',\n      tags: 'Tags',\n      tags_help: 'Anki notes can include tags separated with commas.',\n      escapeHTML: 'Escape HTML',\n      escapeHTML_help:\n        'Escape HTML entities. Turn off if using HTML for manual layout.',\n      syncServer: 'Sync Server',\n      syncServer_help:\n        'Sync to server(e.g. AnkiWeb) after new words being added to local Anki.'\n    }\n  },\n\n  titlebarOffset: {\n    title: 'Calibrate Titlebar Height',\n    help:\n      'Different systems or browser settings may result in different titlebar height. Saladict will attempt to calibrate automatically. If you may adjust manually.',\n    main: 'Normal',\n    main_help: 'Normal windows may not have titlebar.',\n    panel: 'Panel',\n    panel_help:\n      'Saladict standalone quick search panel is a type of panel window.',\n    calibrate: 'Auto-calibrate',\n    calibrateSuccess: 'Calibration success',\n    calibrateError: 'Calibration failed'\n  },\n\n  headInfo: {\n    acknowledgement: {\n      title: 'Acknowledgement',\n      yipanhuasheng:\n        \"for adding Merriam Webster's Dict, American Heritage Dict, Oxford Learner's Dict and Eudic Notebook sync service; and updating Urban Dict and Naver Dict\",\n      naver: 'for helping add Naver dict',\n      shanbay: 'for adding Shanbay dict',\n      trans_tw: 'for traditional Chinese translation',\n      weblio: 'for helping add Weblio dict'\n    },\n    contact_author: 'Contact Author',\n    donate: 'Donate',\n    instructions: 'Instructions',\n    report_issue: 'Report Issue'\n  },\n\n  form: {\n    url_error: 'Incorrect URL.',\n    number_error: 'Incorrect number.'\n  },\n\n  preload: {\n    title: 'Preload',\n    auto: 'Auto search',\n    auto_help: 'Search automatically when panel shows up.',\n    clipboard: 'Clipboard',\n    help: 'Preload content in search box when panel shows up.',\n    selection: 'Page Selection'\n  },\n\n  locations: {\n    CENTER: 'Center',\n    TOP: 'Top',\n    RIGHT: 'Right',\n    BOTTOM: 'Bottom',\n    LEFT: 'Left',\n    TOP_LEFT: 'Top Left',\n    TOP_RIGHT: 'Top Right',\n    BOTTOM_LEFT: 'Bottom Left',\n    BOTTOM_RIGHT: 'Bottom Right'\n  },\n\n  import_export_help:\n    'Configs are auto-synced via browser. Here you can also import/export manually. Backups are exported as plain text files. Please encrypt it yourself if needed.',\n\n  import: {\n    title: 'Import Configs',\n    error: {\n      title: 'Import Error',\n      parse: 'Unable to parse backup. Incorrect format.',\n      load: 'Unable to load backup. Browser cannot obtain the local file.',\n      empty: 'No valid data found in the backup.'\n    }\n  },\n\n  export: {\n    title: 'Export Configs',\n    error: {\n      title: 'Export Error',\n      empty: 'No config to export.',\n      parse: 'Unable to parse configs.'\n    }\n  },\n\n  dictAuth: {\n    description:\n      'As the number of Saladict users grows, if you make heavily use of machine translation services it is recommended to register an account for better stability and accuracy. The account data will only be stored in the browser.',\n    dictHelp: 'See the official website of {dict}.',\n    manage: 'Manage Translator Accounts'\n  },\n\n  third_party_privacy: 'Third Party Privacy',\n  third_party_privacy_help:\n    'Saladict will not collect further information but search text and releated cookies will be sent to third party dictionary services(just like how you would search on their websites). If you do not want third party services to collect you data, remove the corresponding dictionaries at \"Dictionaries\" settings.',\n  third_party_privacy_extra:\n    'Cannot be turned off as it is the core functionality of Saladict.',\n\n  permissions: {\n    success: 'Permission requested',\n    cancel_success: 'Permission cancelled',\n    failed: 'Permission request failed',\n    cancelled: 'Permission request cancelled by user',\n    missing:\n      'Missing permission \"{{permission}}\". Either grant it or disable related functions.',\n    clipboardRead: 'Read Clipboard',\n    clipboardRead_help:\n      'This permission is needed when clipboard preload is enable for popup panel or quick search panel.',\n    clipboardWrite: 'Write Clipboard',\n    clipboardWrite_help:\n      'This permission is needed when using titlebar menus to copy source/target text from machine translator.'\n  },\n\n  unsupportedFeatures: {\n    ff: 'Feature \"{{feature}}\" is not supported in Firefox.'\n  }\n}\n"
  },
  {
    "path": "src/_locales/en/popup.ts",
    "content": "import { locale as _locale } from '../zh-CN/popup'\n\nexport const locale: typeof _locale = {\n  title: 'Saladict Browser Action Panel',\n  app_active_title: 'Enable Inline Translator',\n  app_temp_active_title: 'Temporary disabled to the page',\n  instant_capture_pinned: ' (pinned) ',\n  instant_capture_title: 'Enable Instant Capture',\n  notebook_added: 'Added',\n  notebook_empty: 'No selection found on the current page',\n  notebook_error: 'Cannot add selected text to Notebook',\n  page_no_response: 'Page no response',\n  qrcode_title: 'Qrcode of the page'\n}\n"
  },
  {
    "path": "src/_locales/en/wordpage.ts",
    "content": "import { locale as _locale } from '../zh-CN/wordpage'\n\nexport const locale: typeof _locale = {\n  title: {\n    history: 'Saladict Search History',\n    notebook: 'Saladict Notebook'\n  },\n\n  localonly: 'local only',\n\n  column: {\n    add: 'Add',\n    date: 'Date',\n    edit: 'Edit',\n    note: 'Note',\n    source: 'Source',\n    trans: 'Translation',\n    word: 'Word'\n  },\n\n  delete: {\n    title: 'Delete',\n    all: 'Delete all',\n    confirm: '. Confirm?',\n    page: 'Delete page',\n    selected: 'Delete selected'\n  },\n\n  export: {\n    title: 'Export',\n    all: 'Export all',\n    description: 'Describe the shape of each record: ',\n    explain: 'How to export to ANKI and other tools',\n    gencontent: 'Generated Content',\n    linebreak: {\n      default: 'Keep default linebreaks',\n      n: 'replace linebreaks with \\\\n',\n      br: 'replace linebreaks with <br>',\n      p: 'replace linebreaks with <p>',\n      space: 'replace linebreaks with space'\n    },\n    page: 'Export page',\n    placeholder: 'Placeholder',\n    htmlescape: {\n      title: 'Escape HTML characters in notes',\n      text: 'Escape HTML'\n    },\n    selected: 'Export selected'\n  },\n\n  filterWord: {\n    chs: 'Chinese',\n    eng: 'English',\n    word: 'Word',\n    phrase: 'Phrase'\n  },\n\n  wordCount: {\n    selected: '{{count}} item selected',\n    selected_plural: '{{count}} item selected',\n    total: '{{count}} item total',\n    total_plural: '{{count}} item total'\n  }\n}\n"
  },
  {
    "path": "src/_locales/es/background.ts",
    "content": "import { locale as _locale } from '../zh-CN/background'\n\nexport const locale: typeof _locale = {\n  app: {\n    off: 'Saladict desactivado. (El panel de búsqueda rápida sigue disponible)',\n    tempOff:\n      'Saladict desactivado en la pestaña actual. (El panel de búsqueda rápida sigue disponible)',\n    unsupported:\n      'El panel Saladict incrustado no es compatible con la pestaña actual. Utilice el panel Saladict independiente en su lugar.'\n  }\n}\n"
  },
  {
    "path": "src/_locales/es/common.ts",
    "content": "import { locale as _locale } from '../zh-CN/common'\n\nexport const locale: typeof _locale = {\n  add: 'Añadir',\n  delete: 'Eliminar',\n  save: 'Guardar',\n  cancel: 'CAncelar',\n\n  edit: 'Editar',\n  sort: 'Ordenar',\n  rename: 'Renombrar',\n\n  confirm: 'Confirmar',\n  changes_confirm: 'Cambios no guardados. ¿Cerrar de todas formas?',\n  delete_confirm: '¿Eliminar completamente el elemento?',\n\n  max: 'Max',\n  min: 'Min',\n\n  name: 'Nombre',\n  none: 'Ninguno',\n\n  enable: 'Activar',\n  enabled: 'Activado',\n  disabled: 'Desactivado',\n\n  blacklist: 'Lista negra',\n  whitelist: 'Lista blanca',\n\n  import: 'Importar',\n  export: 'Exportar',\n\n  lang: {\n    chinese: 'Chino',\n    chs: 'Chino',\n    deutsch: 'Alemán',\n    eng: 'Inglés',\n    english: 'Inglés',\n    french: 'Francés',\n    japanese: 'Japonés',\n    korean: 'Coreano',\n    matchAll: 'Coincidir con todos los caracteres',\n    minor: 'Menor',\n    others: 'Otros',\n    spanish: 'Español'\n  },\n\n  unit: {\n    mins: 'minutes',\n    ms: 'ms',\n    s: 's',\n    word: 'words'\n  },\n\n  note: {\n    word: 'Word',\n    trans: 'Translation',\n    note: 'Note',\n    context: 'Context',\n    contextCloze: 'Context Cloze',\n    date: 'Date',\n    srcTitle: 'Source Title',\n    srcLink: 'Source Link',\n    srcFavicon: 'Source Favicon'\n  },\n\n  profile: {\n    daily: 'Daily Mode',\n    sentence: 'Sentence Mode',\n    default: 'Default Mode',\n    scholar: 'Scholar Mode',\n    translation: 'Translation Mode',\n    nihongo: 'Japanese Mode'\n  }\n}\n"
  },
  {
    "path": "src/_locales/es/content.ts",
    "content": "import { locale as _locale } from '../zh-CN/content'\n\nexport const locale: typeof _locale = {\n  chooseLang: 'elegir otro idioma',\n  standalone: 'Panel de Saladict independiente',\n  fetchLangList: 'Obtener la lista completa de idiomas',\n  transContext: 'Retraducir',\n  neverShow: 'Dejar de mostrar',\n  fromSaladict: 'Desde el panel de Saladict',\n  tip: {\n    historyBack: 'Historial de búsqueda anterior',\n    historyNext: 'Siguiente historial de búsqueda',\n    searchText: 'Buscar texto',\n    openOptions: 'Abrir opciones',\n    addToNotebook: 'Agregar al cuaderno. Haga clic derecho para abrir el cuaderno',\n    openNotebook: 'Abrir cuaderno',\n    openHistory: 'Abrir historial',\n    shareImg: 'Compartir como imagen',\n    pinPanel: 'Fijar el panel',\n    closePanel: 'Cerrar el panel',\n    sidebar: 'Cambiar a modo barra lateral. Haga clic derecho para el lado derecho.',\n    focusPanel: 'El panel gana foco al buscar',\n    unfocusPanel: 'El panel no gana foco al buscar'\n  },\n  wordEditor: {\n    title: 'Agregar al cuaderno',\n    wordCardsTitle: 'Otros resultados del cuaderno',\n    deleteConfirm: '¿Eliminar del cuaderno?',\n    closeConfirm: 'Los cambios no se guardarán. ¿Estás seguro de cerrar?',\n    chooseCtxTitle: 'Elija los resultados traducidos',\n    ctxHelp:\n      'Mantenga el formato [:: xxx ::] y --------------- si desea que Saladict maneje la selección de traducción y genere una tabla de Anki.'\n  },\n  machineTrans: {\n    switch: 'Cambiar idioma',\n    sl: 'Idioma de origen',\n    tl: 'Idioma de destino',\n    auto: 'Detectar idioma',\n    stext: 'Original',\n    showSl: 'Mostrar fuente',\n    copySrc: 'Copiar fuente',\n    copyTrans: 'Copiar traducción',\n    login: 'Proporcione {access token}.',\n    dictAccount: 'access token'\n  },\n  updateAnki: {\n    title: 'Actualizar a Anki',\n    success: 'Se actualizó correctamente la palabra a Anki.',\n    failed: 'No se pudo actualizar la palabra a Anki.'\n  }\n}\n"
  },
  {
    "path": "src/_locales/es/langcode.ts",
    "content": "import en from '@opentranslate/languages/locales/en.json'\nimport { locale as _locale } from '../zh-CN/langcode'\n\nexport const locale: typeof _locale = {\n  ...en,\n  default: 'Predeterminado',\n  ara: 'Arabe',\n  'bs-Latn': 'Bosnio',\n  bul: 'Búlgaro',\n  cht: 'Chino (Tradicional)',\n  dan: 'Danés',\n  est: 'Estonio',\n  fin: 'Finlandés',\n  fra: 'Francés',\n  iw: 'Hebreo',\n  jp: 'Japonés',\n  kor: 'Coreano',\n  kr: 'Coreano',\n  pt_BR: 'Brasileño',\n  rom: 'Rumano',\n  slo: 'Esloveno',\n  spa: 'Español',\n  swe: 'Sueco',\n  tl: 'Tagalo (Filipino)',\n  vie: 'Vietnamita',\n  zh: 'Chino (Simplificado)',\n  'zh-CHS': 'Chino (Simplificado)',\n  'zh-CHT': 'Chino (Tradicional)'\n}\n"
  },
  {
    "path": "src/_locales/es/menus.ts",
    "content": "import { locale as _locale } from '../zh-CN/menus'\n\nexport const locale: typeof _locale = {\n  baidu_page_translate: 'Traductor web de baidu',\n  baidu_search: 'Buscar en baidu',\n  bing_dict: 'Bing diccionario',\n  bing_search: 'Buscar en bing',\n  caiyuntrs: 'Traductor de Lingocloud',\n  cambridge: 'Cambridge',\n  copy_pdf_url: 'Copiar URL de PDF al portapapeles',\n  dictcn: 'Dictcn',\n  etymonline: 'Etymonline',\n  google_cn_page_translate: 'Traductor web de Google.cn',\n  google_page_translate: 'Traductor de Google',\n  google_search: 'Buscar en Google',\n  google_translate: 'Traductor de Google',\n  google_cn_translate: 'Traductor de Google.cn',\n  guoyu: '國語辭典',\n  history_title: 'Historial de búsqueda',\n  iciba: 'iciba',\n  liangan: '兩岸詞典',\n  longman_business: 'Longman Business',\n  manual_title: 'Manual',\n  merriam_webster: 'Merriam Webster',\n  microsoft_page_translate: 'Traductor web de Microsoft',\n  notebook_title: 'Lista de palabras nuevas',\n  notification_youdao_err:\n    'Youdao Page Translate 2.0 no responde.\\nSaladict puede que no tenga permiso para acceder a esta página.\\nIgnora este mensaje si el panel de Youdao se muestra.',\n  oxford: 'Oxford',\n  page_permission_err:\n    'Saladict \"{{name}}\" no tiene permiso para acceder a esta página.',\n  page_translations: 'Traducciones de página',\n  saladict: 'Saladict',\n  saladict_standalone: 'Panel Saladict independiente',\n  sogou: 'Traductor de Sogou',\n  sogou_page_translate: 'Traductor web de Sogou',\n  termonline: 'Termonline',\n  view_as_pdf: 'Abrir en el visor de PDF',\n  youdao: 'Youdao',\n  youdao_page_translate: 'Traductor web de Youdao',\n  youglish: 'YouGlish'\n}\n"
  },
  {
    "path": "src/_locales/es/options.ts",
    "content": "import { locale as _locale } from '../zh-CN/options'\n\nexport const locale: typeof _locale = {\n  title: 'Saladict Opciones',\n  previewPanel: 'Panel de vista previa',\n  shortcuts: 'Atajos de teclado',\n  msg_update_error: 'Error al actualizar',\n  msg_updated: 'Actualizado',\n  msg_first_time_notice: '¡Bienvenido a Saladict!',\n  msg_err_permission: 'No se ha podido solicitar el permiso \"{{permission}}\".',\n  unsave_confirm: 'Hay cambios sin guardar. ¿Estás seguro de que quieres salir?',\n  nativeSearch: 'Buscar con el motor de búsqueda nativo',\n  firefox_shortcuts:\n    'Abra about:addons, haga clic en el botón superior derecho \"engranaje\", elija la última \"Administrar accesos directos de extensión\".',\n  tutorial: 'Tutorial',\n  page_selection: 'Selección de página',\n\n  nav: {\n    General: 'General',\n    Notebook: 'Bloc de notas',\n    Profiles: 'Perfiles',\n    DictPanel: 'Panel de diccionario',\n    SearchModes: 'Modos de búsqueda',\n    Dictionaries: 'Diccionarios',\n    DictAuths: 'Access Tokens',\n    Popup: 'Popup Panel',\n    QuickSearch: 'Busqueda rápida',\n    Pronunciation: 'Pronunciación',\n    PDF: 'PDF',\n    ContextMenus: 'Menús de contexto',\n    BlackWhiteList: 'Lista negra/Blanca',\n    ImportExport: 'Importar/Exportar',\n    Privacy: 'Privacidad',\n    Permissions: 'Permisos',\n  },\n\n    config: {\n    active: 'Activar el traductor en línea',\n    active_help:\n      'Si está desactivado, el traductor en línea no se mostrará en el panel de búsqueda rápida.',\n    animation: 'Transiciones de animación',\n    animation_help: 'Desactive las transiciones de animación para mejorar el rendimiento.',\n    runInBg: 'Ejecutar en segundo plano',\n    runInBg_help:\n      'Si está desactivado, Saladict se cerrará cuando se cierre la última ventana.',\n    darkMode: ' Modo oscuro',\n    langCode: 'Idioma de Saladict',\n    editOnFav: 'Abrir WordEditor al guardar',\n    editOnFav_help:\n      'Si está desactivado, WordEditor se abrirá cuando se agregue una palabra nueva.',\n    searchHistory: 'Historial de búsqueda',\n    searchHistory_help:\n      'Si está desactivado, el historial de búsqueda no se mostrará en el panel de búsqueda rápida.',\n    searchHistoryInco: 'Incluir búsqueda de incógnito',\n    ctxTrans: 'Traducir contexto',\n    ctxTrans_help:\n      'Si está desactivado, el contexto no se mostrará en el panel de búsqueda rápida.',\n    searchSuggests: 'Sugerencias de búsqueda',\n    panelMaxHeightRatio: 'Altura máxima del panel',\n    panelWidth: 'Ancho del panel',\n    fontSize: 'Tamaño de fuente',\n    bowlOffsetX: 'Desplazamiento X del icono de Saladict',\n    bowlOffsetY: 'Desplazamiento Y del icono de Saladict',\n    panelCSS: 'CSS personalizado',\n    panelCSS_help:\n      'CSS personalizado. Para el panel de dictado, utilice .dictPanel-Root como raíz. Para diccionarios utilice .dictRoot o .d-{id} como raíz.',\n    noTypeField: 'No hay selección en las regiones editables',\n    noTypeField_help:\n      'Si la selección en regiones editables está prohibida, la extensión identificará los cuadros de entrada, las áreas de texto y otros editores de texto comunes como CodeMirror, ACE y Monaco.',\n    touchMode: 'Modo táctil',\n    touchMode_help: 'Activar la selección táctil',\n    language: 'Selección de idiomas',\n    language_help:\n      'Buscar cuando la selección contiene palabras en los idiomas elegidos.',\n    language_extra:\n      'Tenga en cuenta que el japonés y el coreano también incluyen el chino. El francés, el alemán y el español también incluyen el inglés. Si se cancela el chino o el inglés mientras se seleccionan otros, sólo se comprueban las partes exclusivas de esos idiomas. Por ejemplo, los caracteres kana en japonés.',\n    doubleClickDelay: 'Retraso de doble clic',\n    mode: 'Seleccion normal',\n    panelMode: 'Interior del panel Dict',\n    pinMode: 'Cuando el panel está fijado',\n    qsPanelMode: 'Cuando se abre el panel independiente',\n    bowlHover: 'Icono al pasar el cursor por encima',\n    bowlHover_help:\n      'Pase el ratón sobre el icono del cuenco para activar la búsqueda en lugar de hacer clic.',\n    autopron: {\n      cn: {\n        dict: 'Autopronunciación en chino'\n      },\n      en: {\n        dict: 'Autopronunciación en inglés',\n        accent: 'Preferencia de acento'\n      },\n      machine: {\n        dict: 'Autopronunciación de la máquina',\n        src: 'Pronunciar a máquina',\n        src_help:\n          'El diccionario de traducción automática debe añadirse y activarse en la siguiente lista para activar la pronunciación automática.',\n        src_search: 'Leer texto original',\n        src_trans: 'Leer el texto de la traducción'\n      }\n    },\n    pdfSniff: 'Activar PDF Sniffer',\n    pdfSniff_help: 'Si está activada, los enlaces PDF se capturarán automáticamente.',\n    pdfSniff_extra:\n      'Se recomienda {search selected text outside of browser} con su propio lector local favorito.',\n    pdfStandalone: 'Panel independiente',\n    pdfStandalone_help: 'Abrir visor de PDF en panel independiente.',\n    baWidth: 'Anchura',\n    baWidth_help:\n      'Navegador Acción Panel con. Si se elige un valor negativo, se utilizará la anchura del panel.',\n    baHeight: 'Altura',\n    baHeight_help: 'Navegador Acción Panel altura.',\n    baOpen: 'Acción del navegador',\n    baOpen_help:\n      'Al pulsar el icono de acción del navegador en la barra de herramientas (junto a la barra de direcciones). Los elementos son los mismos que los menús contextuales, que pueden añadirse o editarse en la página de configuración de menús contextuales.',\n    tripleCtrl: 'Activar tecla corta Ctrl',\n    tripleCtrl_help:\n      'Pulse {⌘ Command}(macOS) o {Ctrl}(Otros) tres veces (o con la tecla rápida del navegador) para acceder al panel del diccionario.',\n    defaultPinned: 'Fijado cuando aparece',\n    qsLocation: 'Ubicación',\n    qsFocus: 'Concéntrese cuando aparezca',\n    qsStandalone: 'Independiente',\n    qsStandalone_help:\n      'Renderizar el panel de dictado en una ventana independiente. Puede {buscar texto seleccionado fuera del navegador}.',\n    qssaSidebar: 'Barra lateral',\n    qssaSidebar_help: 'Renderizar el panel de dictado en la barra lateral.',\n    qssaHeight: 'Ventana altura',\n    qssaPageSel: 'Selección de página',\n    qssaPageSel_help: 'Seleccionar automáticamente el texto de la página.',\n    qssaRectMemo: 'Recordar tamaño y posición',\n    qssaRectMemo_help: 'Recuerde el tamaño y la posición del panel independiente al cerrar.',\n    updateCheck: 'Comprobar actualizaciones',\n    updateCheck_help: 'Compruebe si hay actualizaciones automáticamente.',\n    analytics: 'Activar Google Analytics',\n    analytics_help:\n      'Compartir información anónima sobre la versión del navegador del dispositivo. El autor de Saladict ofrecerá soporte prioritario a los dispositivos y navegadores más populares.',\n\n    opt: {\n      reset: 'Restablecer configuración',\n      reset_confirm: '¿Estás seguro de que quieres restablecer la configuración?',\n      upload_error: 'Error al cargar la configuración',\n      accent: {\n        uk: 'UK',\n        us: 'US'\n      },\n      sel_blackwhitelist: 'Lista negra y blanca de selección',\n      sel_blackwhitelist_help:\n        'Saladict no reaccionará a la selección en páginas de la lista negra.',\n      pdf_blackwhitelist_help:\n        'Los enlaces PDF de la lista negra no saltarán a Saladict PDF Viewer.',\n      contextMenus_description:\n        'Cada elemento del menú contextual también se puede personalizar. Youdao y Google Traductor están obsoletos en favor de las extensiones oficiales.',\n      contextMenus_edit: 'Editar elementos de menús contextuales',\n      contextMenus_url_rules: 'URL con %s en lugar de query.',\n      baOpen: {\n        popup_panel: 'Panel de diccionario',\n        popup_fav: 'Añadir al bloc de notas',\n        popup_options: 'Abrir opciones de Saladict',\n        popup_standalone: 'Panel independiente Open Saladict'\n      },\n      openQsStandalone: 'Opciones de panel independiente',\n      pdfStandalone: {\n        default: 'Nunca',\n        always: 'Siempre',\n        manual: 'Manual'\n      }\n    }\n  },\n\n  matchPattern: {\n    description:\n      'Especifique URL como {URL Patrón de Coincidencia} o {Expresión Regular}. Se eliminarán los campos vacíos.',\n    url: 'URL Patrón de Coincidencia',\n    url_error: 'Patrón de coincidencia de URL incorrecto.',\n    regex: 'Expresión regular',\n    regex_error: 'Expresión regular incorrecta.'\n  },\n\n  searchMode: {\n    icon: 'Mostrar icono',\n    icon_help: 'Aparecerá un bonito icono cerca del cursor.',\n    direct: 'Busqueda directa',\n    direct_help: 'Mostrar directamente el panel dict.',\n    double: 'Doble clic',\n    double_help: 'Mostrar panel de dict después de la selección de doble clic.',\n    holding: 'Mantener pulsado',\n    holding_help:\n      'Después de realizar una selección, la tecla seleccionada debe estar pulsada al soltar el ratón (Alt es \"⌥ Opción\" en macOS. Meta es \"⌘ Comando\" en macOS y \"⊞ Windows\" para los demás).',\n    instant: 'Captura instantánea',\n    instant_help: 'La selección se realiza automáticamente cerca del cursor.',\n    instantDirect: 'Directo',\n    instantKey: 'Tecla',\n    instantKey_help:\n    'Si se elige \"Directo\", también se recomienda configurar la tecla de acceso directo del navegador para activar la Captura instantánea. De lo contrario, la selección de texto en el navegador podría ser imposible.',\n    instantDelay: 'Retraso de captura'\n  },\n\n  profiles: {\n    opt: {\n      add_name: 'Añadir nombre de perfil',\n      delete_confirm: 'Eliminar Perfil \"{{name}}\". ¿Confirmar?',\n      edit_name: 'Cambiar el nombre del perfil',\n      help:\n        'Cada perfil representa un conjunto independiente de ajustes. Algunos de los ajustes (con el prefijo {*}) cambian según el perfil. Para cambiar de perfil, sitúe el cursor sobre el icono de menú del panel de dictado, o bien sitúe el cursor sobre el icono y pulse {↓}.'\n    }\n  },\n\n  profile: {\n    mtaAutoUnfold: 'Despliegue automático del cuadro de búsqueda multilínea',\n    waveform: 'Forma de onda',\n    waveform_help:\n      'Muestra un botón en la parte inferior del panel de dictado para expandir el panel de control de forma de onda que sólo se carga después de la expansión.',\n    stickyFold: 'Plegado adhesivo',\n    stickyFold_help:\n      'Recuerda los estados de plegado/desplegado del diccionario manual al buscar. Sólo dura en la misma página.',\n\n    opt: {\n      item_extra: 'Esta opción puede cambiar en función del \"Perfil\".',\n      mtaAutoUnfold: {\n        always: 'Siempre Desplegar',\n        never: 'Nunca Desplegar',\n        once: 'Desplegar una vez',\n        popup: 'Sólo en la acción del navegador',\n        hide: 'Ocultar'\n      },\n      dict_selected: 'Seleccionar diccionarios',\n    }\n  },\n\n  dict: {\n    add: 'Añadir diccionarios',\n    more_options: 'Mas opciones',\n\n    selectionLang: 'Seleccionar idiomas',\n    selectionLang_help:\n      'Muestra este diccionario cuando la selección contiene palabras en los idiomas elegidos.',\n    defaultUnfold: 'Despliegue por defecto',\n    defaultUnfold_help:\n      \"Si está desactivado, este diccionario no iniciará la búsqueda a menos que se haga clic en su barra de título.\",\n    selectionWC: 'Selección Número de palabras',\n    selectionWC_help:\n      'Muestre este diccionario cuando el recuento de palabras de la selección cumpla los requisitos. Establezca 999999 para un número ilimitado de palabras.',\n    preferredHeight: 'Altura predeterminada del panel',\n    preferredHeight_help:\n      'Altura máxima en la primera aparición. Los contenidos que superen esta altura se ocultarán. Establezca 999999 para una altura ilimitada.',\n\n    lang: {\n      de: 'De',\n      en: 'En',\n      es: 'Es',\n      fr: 'Fr',\n      ja: 'Ja',\n      kor: 'Kor',\n      zhs: 'Zhs',\n      zht: 'Zht'\n    }\n  },\n\n  syncService: {\n    description: 'Ajustes de sincronización.',\n    start: 'Sincronizando. No cierre esta página hasta que haya terminado.',\n    finished: 'Sincronización finalizada',\n    success: 'Sincronización correcta',\n    failed: 'Sincronización fallida',\n    close_confirm: 'No se ha guardado la configuración. ¿Cerrar?',\n    delete_confirm: '¿Eliminar?',\n\n    shanbay: {\n      description:\n      \" Vaya a shanbay.com y conéctese primero(debe permanecer conectado). Tenga en cuenta que se trata de una sincronización unidireccional (de Saladict a Shanbay). Sólo se sincronizan las nuevas palabras añadidas. Las palabras también deben ser compatibles con la base de datos de Shanbay.\",\n      login:\n        'Se abrirá shanbay.com. Por favor, inicie sesión y luego volver y habilitar de nuevo.',\n      sync_all: 'Cargar todas las palabras nuevas existentes',\n      sync_all_confirm:\n        'Demasiadas palabras nuevas en el cuaderno. Saladict cargará por lotes. Ten en cuenta que si subes demasiadas palabras en un periodo corto de tiempo, tu cuenta será bloqueada y no se podrá recuperar. ¿Confirmar?',\n      sync_last: 'Cargar la última palabra nueva'\n    },\n\n    eudic: {\n      description:\n        'Antes de utilizar Eudic para sincronizar palabras, primero debe crear un nuevo libro de palabras predeterminado en el sitio web oficial de Eudic (my.eudic.net/home/index) (por lo general, se generará automáticamente y no se podrá eliminar después de la primera importación manual). Preste atención a no sincronizar con frecuencia en poco tiempo, ya que podría provocar un bloqueo temporal.',\n      token: 'Información sobre la autorización',\n      getToken: 'Obtener autorización',\n      verify: 'Comprobar la información de autorización',\n      verified: 'Información de autorización Eudic comprobada correctamente',\n      enable_help:\n        'Tras la apertura, cada nueva palabra añadida se sincronizará automáticamente con el libro de palabras predeterminado de Eudic (ensalada al libro de palabras de Eudic) en una dirección, y sólo se sincronizará la nueva palabra en sí (eliminada fuera de sincronización)',\n      token_help:\n        'Por favor, confirme que la información de autorización personal es válida, de lo contrario la sincronización fallará. Puede hacer clic en el botón de la parte inferior para comprobarlo.',\n      sync_all: 'Sincronizar todas las palabras nuevas',\n      sync_help:\n        'Sincronice todas las palabras nuevas existentes en el libro de palabras de la ensalada con el libro de palabras predeterminado de Eudic (active el interruptor de sincronización anterior al mismo tiempo y haga clic en guardar).',\n      sync_all_confirm:\n        'Tenga en cuenta que una sincronización frecuente en poco tiempo puede provocar un bloqueo temporal. ¿Está seguro de continuar?'\n    },\n\n    webdav: {\n      description:\n        'La configuración de las extensiones (incluida esta) se sincroniza a través del navegador. El cuaderno de nuevas palabras se puede sincronizar mediante WebDAV a través de la configuración aquí.',\n      jianguo: 'Véase Jianguoyun, por ejemplo',\n      checking: 'Conectando...',\n      exist_confirm:\n        'El directorio Saladict existe en el servidor. ¿Descargarlo y fusionarlo con los datos locales?',\n      upload_confirm: '¿Subir los datos locales al servidor de inmediato?',\n      verify: 'Verificar servidor',\n      verified: 'Verificado con éxito el servidor WebDAV.',\n      duration: 'Duracion',\n      duration_help:\n      'Se garantiza que los datos se actualizan antes de cargarlos. Si no necesita sincronización en tiempo real entre navegadores, establezca un ciclo de sondeo más largo para reducir el consumo de CPU y memoria.',\n      passwd: 'Contraseña',\n      url: 'Dirección del servidor',\n      user: 'Usuario',\n    },\n\n    ankiconnect: {\n      description:\n        'Por favor, asegúrate de que el plugin Anki Connect está instalado y Anki se está ejecutando. También puede actualizar la palabra a Anki en el editor de Word.',\n      checking: 'Verificando...',\n      deck_confirm:\n        'El tablero \"{{deck}}\" no existe en Anki. ¿Generar un nuevo tablero?',\n      deck_error: 'No se puede crear el tablero \"{{deck}}\".',\n      notetype_confirm:\n        'El tipo de nota \"{{noteType}}\" no existe en Anki. Genera un nuevo tipo de nota.',\n      notetype_error: 'No se puede crear el tipo de nota\"{{noteType}}\".',\n      upload_confirm:\n        '¿Sincronizar nuevas palabras locales a Anki inmediatamente? Las palabras duplicadas (con la misma marca de tiempo) se omitirán.',\n      add_yourself: 'Por favor, añádelo tú mismo en Anki.',\n      verify: 'Verificar Anki Connect',\n      verified: 'Anki Connect verificado correctamente.',\n      enable_help:\n        'Cuando está activada, cada vez que se añade una nueva palabra al Cuaderno, también se transfiere automáticamente a Anki. Las palabras que existen en Anki (con la misma \"Fecha\") pueden ser forzadas a actualizarse en el Editor de Palabras.',\n      host: 'Dirección',\n      port: 'Puerto',\n      key: 'Clave',\n      key_help:\n        'Se puede añadir una clave opcional en la configuración de Anki Connect para la identificación.',\n      deckName: 'Tablero',\n      deckName_help:\n        'Si el tablero no existe, puedes generar uno automáticamente haciendo clic en \"Verificar Anki Connect\" más abajo.',\n      noteType: 'Tipo de nota',\n      noteType_help:\n        'El tipo de nota Anki incluye un conjunto de campos y un tipo de tarjeta. Si el tipo de nota no existe puedes generar uno por defecto automáticamente haciendo clic en \"Verificar Anki Connect\" más abajo. NO cambie los nombres de los campos cuando edite o añada plantillas de tarjetas en Anki',\n      tags: 'Etiquetas',\n      tags_help: 'Anki notes can include tags separated with commas.',\n      escapeHTML: 'Escapar HTML',\n      escapeHTML_help:\n        'Escapar entidades HTML. Desactivar si se utiliza HTML para la maquetación manual.',\n      syncServer: 'Sincronizar con el servidor',\n      syncServer_help:\n        'Sincronización con el servidor (p.e. AnkiWeb) después de añadir nuevas palabras al Anki local.'\n    }\n  },\n\n  titlebarOffset: {\n    title: 'Calibración de la altura de la barra de título',\n    help:\n      'La altura de la barra de título puede variar según el sistema o la configuración del navegador. Saladict intentará calibrarla automáticamente. Si puede ajustar manualmente.',\n    main: 'Normal',\n    main_help: 'Las ventanas normales pueden no tener barra de título.',\n    panel: 'Panel',\n    panel_help:\n      'El panel de búsqueda rápida independiente de Saladict es un tipo de ventana de panel.',\n    calibrate: 'Auto-calibrate',\n    calibrateSuccess: 'Calibración correcta',\n    calibrateError: 'Error de calibración'\n  },\n\n  headInfo: {\n    acknowledgement: {\n      title: 'Reconocimiento',\n      yipanhuasheng:\n        \"por añadir los diccionarios Merriam Webster's Dict, American Heritage Dict, Oxford Learner's Dict y el servicio de sincronización Eudic Notebook; y por actualizar los diccionarios Urban Dict y Naver Dict.\",\n      naver: 'por ayudar a añadir Naver dict',\n      shanbay: 'por añadir Shanbay dict',\n      trans_tw: 'por la traducción al chino tradicional',\n      weblio: 'por ayudar a añadir Weblio dict'\n    },\n    contact_author: 'Contactar al autor',\n    donate: 'Donar',\n    instructions: 'Instrucciones',\n    report_issue: 'Informar de un problema',\n  },\n\n  form: {\n    url_error: 'URL incorrecta.',\n    number_error: 'Numero incorrecto.'\n  },\n\n  preload: {\n    title: 'Precarga',\n    auto: 'Búsqueda automática',\n    auto_help: 'Búsqueda automática cuando aparece el panel.',\n    clipboard: 'Clipboard',\n    help: 'Precarga de contenido en el cuadro de búsqueda cuando aparece el panel.',\n    selection: 'Selección de página'\n  },\n\n  locations: {\n    CENTER: 'Centrado',\n    TOP: 'Arriba',\n    RIGHT: 'Derecha',\n    BOTTOM: 'Abajo',\n    LEFT: 'Izquierda',\n    TOP_LEFT: 'Arriba a la izquierda',\n    TOP_RIGHT: 'Arriba a la derecha',\n    BOTTOM_LEFT: 'Abajo a la izquierda',\n    BOTTOM_RIGHT: 'Abajo a la derecha'\n  },\n\n  import_export_help:\n    'Las configuraciones se sincronizan automáticamente a través del navegador. Aquí también puede importar/exportar manualmente. Las copias de seguridad se exportan como archivos de texto sin formato. Por favor, codifíquelos usted mismo si es necesario.',\n\n  import: {\n    title: 'Importar Configuraciones',\n    error: {\n      title: 'Error de importación',\n      parse: 'No se ha podido analizar la copia de seguridad. Formato incorrecto.',\n      load: 'No se puede cargar la copia de seguridad. El navegador no puede obtener el archivo local.',\n      empty: 'No se han encontrado datos válidos en la copia de seguridad.'\n    }\n  },\n\n  export: {\n    title: 'Exportar Configuraciones',\n    error: {\n      title: 'Error de exportación',\n      empty: 'No hay configuración para exportar.',\n      parse: 'No se pueden analizar las configuraciones.'\n    }\n  },\n\n  dictAuth: {\n    description:\n      'A medida que crece el número de usuarios de Saladict, si hace un uso intensivo de los servicios de traducción automática se recomienda registrar una cuenta para mejorar la estabilidad y la precisión. Los datos de la cuenta sólo se almacenarán en el navegador.',\n    dictHelp: 'Consulte el sitio web oficial de {dict}.',\n    manage: 'Gestionar cuentas de traductor'\n  },\n\n  third_party_privacy: 'Privacidad de terceros',\n  third_party_privacy_help:\n    'Saladict no recopilará más información, pero el texto de la búsqueda y las cookies correspondientes se enviarán a servicios de diccionarios de terceros (igual que si buscara en sus sitios web). Si no desea que los servicios de terceros recopilen sus datos, elimine los diccionarios correspondientes en la configuración de \"Diccionarios\".',\n  third_party_privacy_extra:\n    'No se puede desactivar, ya que es la funcionalidad principal de Saladict.',\n\n  permissions: {\n    success: 'Permiso solicitado',\n    cancel_success: 'Permiso cancelado',\n    failed: 'Solitud de permiso fallida',\n    cancelled: 'Solicitud de permiso cancelada por el usuario',\n    missing:\n      'Falta el permiso \"{{permission}}\". Concederlo o desactivar las funciones relacionadas.',\n    clipboardRead: 'Leer portapapeles',\n    clipboardRead_help:\n      'Este permiso es necesario cuando la precarga del portapapeles está activada para el panel emergente o el panel de búsqueda rápida.',\n    clipboardWrite: 'Escribir en el portapapeles',\n    clipboardWrite_help:\n      'Este permiso es necesario cuando se utilizan los menús de la barra de títulos para copiar texto de origen/destino del traductor automático.'\n  },\n\n  unsupportedFeatures: {\n    ff: 'La característica \"{{feature}}\" no es compatible con Firefox.'\n  }\n}\n"
  },
  {
    "path": "src/_locales/es/popup.ts",
    "content": "import { locale as _locale } from '../zh-CN/popup'\n\nexport const locale: typeof _locale = {\n  title: 'Panel de acciones del navegador de Saladict',\n  app_active_title: 'Activar el traductor en línea',\n  app_temp_active_title: 'Desactivación temporal de la página',\n  instant_capture_pinned: ' (fijado) ',\n  instant_capture_title: 'Activar captura instantánea',\n  notebook_added: 'Añadido',\n  notebook_empty: 'No se ha encontrado ninguna selección en la página actual',\n  notebook_error: 'No se puede añadir el texto seleccionado al bloc de notas',\n  page_no_response: 'La pagina no responde',\n  qrcode_title: 'Qrcode de la página'\n}\n"
  },
  {
    "path": "src/_locales/es/wordpage.ts",
    "content": "import { locale as _locale } from '../zh-CN/wordpage'\n\nexport const locale: typeof _locale = {\n  title: {\n    history: 'Historial de búsqueda de Saladict',\n    notebook: 'Bloc de notas Saladict'\n  },\n\n  localonly: 'Solo local',\n\n  column: {\n    add: 'Añadir',\n    date: 'Fecha',\n    edit: 'Editar',\n    note: 'Nota',\n    source: 'Fuente',\n    trans: 'Traducción',\n    word: 'Palabra'\n  },\n\n  delete: {\n    title: 'Eliminar',\n    all: 'Eliminar todo',\n    confirm: '. ¿Desea eliminarlo?',\n    page: 'Eliminar página',\n    selected: 'Eliminar seleccionado'\n  },\n\n  export: {\n    title: 'Exportar',\n    all: 'Exportar todo',\n    description: 'Exportar a un archivo de texto',\n    explain: 'Cómo exportar a ANKI y otras herramientas',\n    gencontent: 'Generar contenido',\n    linebreak: {\n      default: 'Mantener los saltos de línea por defecto',\n      n: 'sustituir los saltos de línea por \\\\n',\n      br: 'sustituir los saltos de línea por <br>',\n      p: 'sustituir los saltos de línea por <p>',\n      space: 'sustituir los saltos de línea por espacios'\n    },\n    page: 'Exportar página',\n    placeholder: 'Marcador',\n    htmlescape: {\n      title: 'Caracteres HTML de escape en las notas',\n      text: 'Escape HTML'\n    },\n    selected: 'Exportar seleccionado',\n  },\n\n  filterWord: {\n    chs: 'Chino',\n    eng: 'Inglés',\n    word: 'Palabra',\n    phrase: 'Frase',\n  },\n\n  wordCount: {\n    selected: '{{count}} elemento seleccionado',\n    selected_plural: '{{count}} elemento seleccionado',\n    total: '{{count}} elemento total',\n    total_plural: '{{count}} elemento total'\n  }\n}\n"
  },
  {
    "path": "src/_locales/manifest/en/messages.json",
    "content": "{\n  \"extension_name\": {\n    \"description\": \"Extension name\",\n    \"message\": \"Saladict - Pop-up Dictionary and Page Translator\"\n  },\n  \"extension_short_name\": {\n    \"description\": \"Extension short name\",\n    \"message\": \"Saladict\"\n  },\n  \"extension_description\": {\n    \"description\": \"Description of extension\",\n    \"message\": \"Saladict is an all-in-one professional pop-up dictionary and page translator which supports multiple search modes, page translations, new word notebook and PDF selection searching.\"\n  },\n  \"command_toggle_active\": {\n    \"message\": \"Toggle inline translator\"\n  },\n  \"command_toggle_instant\": {\n    \"message\": \"Toggle instant capture\"\n  },\n  \"command_open_quick_search\": {\n    \"message\": \"Open or highlight standalone dict panel\"\n  },\n  \"command_open_google\": {\n    \"message\": \"Open Google Translate\"\n  },\n  \"command_open_youdao\": {\n    \"message\": \"Open Youdao Translate\"\n  },\n  \"command_open_caiyun\": {\n    \"message\": \"Open LingoCloud Translate\"\n  },\n  \"command_open_pdf\": {\n    \"message\": \"Open current PDF in Saladict\"\n  },\n  \"command_search_clipboard\": {\n    \"message\": \"Search clipboard content in Standalone Panel\"\n  },\n  \"command_next_history\": {\n    \"message\": \"Next Search History\"\n  },\n  \"command_prev_history\": {\n    \"message\": \"Previous Search History\"\n  },\n  \"command_next_profile\": {\n    \"message\": \"Next Profile\"\n  },\n  \"command_prev_profile\": {\n    \"message\": \"Previous Profile\"\n  },\n  \"command_profile_1\": {\n    \"message\": \"First Profile\"\n  },\n  \"command_profile_2\": {\n    \"message\": \"Second Profile\"\n  },\n  \"command_profile_3\": {\n    \"message\": \"Third Profile\"\n  },\n  \"command_profile_4\": {\n    \"message\": \"Fourth Profile\"\n  },\n  \"command_profile_5\": {\n    \"message\": \"Fifth Profile\"\n  },\n  \"command_add_notebook\": {\n    \"message\": \"Add to Notebook\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/manifest/np/messages.json",
    "content": "{\n  \"extension_name\": {\n    \"description\": \"Extension name\",\n    \"message\": \"सलाडिक्ट - पप-अप शब्दकोश र पृष्ठ अनुवादक\"\n  },\n  \"extension_short_name\": {\n    \"description\": \"Extension short name\",\n    \"message\": \"सलाडिक्ट\"\n  },\n  \"extension_description\": {\n    \"description\": \"Description of extension\",\n    \"message\": \"सलाडिक्ट एक पेशेवर पप-अप शब्दकोश र पृष्ठ अनुवादक हो जसले बहु भाषा खोज , पृष्ठ अनुवाद, नयाँ शब्द नोटबुक र PDF  खोजीलाई समर्थ छ।\"\n  },\n  \"command_toggle_active\": {\n    \"message\": \"इनलाइन अनुवादक टगल गर्नुहोस्\"\n  },\n  \"command_toggle_instant\": {\n    \"message\": \"तत्काल क्याप्चर टगल गर्नुहोस्\"\n  },\n  \"command_open_quick_search\": {\n    \"message\": \"स्ट्यान्डअलोन डिक्ट प्यानल खोल्नुहोस् वा हाइलाइट गर्नुहोस्\"\n  },\n  \"command_open_google\": {\n    \"message\": \"Google अनुवादक खोल्नुहोस्\"\n  },\n  \"command_open_youdao\": {\n    \"message\": \"Youdao अनुवाद खोल्नुहोस्\"\n  },\n  \"command_open_caiyun\": {\n    \"message\": \"LingoCloud अनुवाद खोल्नुहोस्\"\n  },\n  \"command_open_pdf\": {\n    \"message\": \"हालको PDF सलाडिक्टमा खोल्नुहोस् \"\n  },\n  \"command_search_clipboard\": {\n    \"message\": \"स्ट्यान्डअलोन प्यानलमा क्लिपबोर्ड सामग्री खोज्नुहोस्\"\n  },\n  \"command_next_history\": {\n    \"message\": \"अर्को खोज इतिहास\"\n  },\n  \"command_prev_history\": {\n    \"message\": \"अघिल्लो खोज इतिहास\"\n  },\n  \"command_next_profile\": {\n    \"message\": \"अर्को प्रोफाइल\"\n  },\n  \"command_prev_profile\": {\n    \"message\": \"अघिल्लो प्रोफाइल\"\n  },\n  \"command_profile_1\": {\n    \"message\": \"पहिलो प्रोफाइल\"\n  },\n  \"command_profile_2\": {\n    \"message\": \"दोस्रो प्रोफाइल\"\n  },\n  \"command_profile_3\": {\n    \"message\": \"तेस्रो प्रोफाइल\"\n  },\n  \"command_profile_4\": {\n    \"message\": \"चौथो प्रोफाइल\"\n  },\n  \"command_profile_5\": {\n    \"message\": \"पाँचौं प्रोफाइल\"\n  },\n  \"command_add_notebook\": {\n    \"message\": \"नोटबुकमा थप्नुहोस्\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/manifest/zh_CN/messages.json",
    "content": "{\n  \"extension_name\": {\n    \"description\": \"Extension name\",\n    \"message\": \"沙拉查词-聚合词典划词翻译\"\n  },\n  \"extension_short_name\": {\n    \"description\": \"Extension short name\",\n    \"message\": \"Saladict\"\n  },\n  \"extension_description\": {\n    \"description\": \"Description of extension\",\n    \"message\": \"Saladict 沙拉查词是一款专业划词翻译扩展，为交叉阅读而生。大量权威词典涵盖中英日韩法德西语，支持复杂的划词操作、网页翻译、生词本与 PDF 浏览。\"\n  },\n  \"command_toggle_active\": {\n    \"message\": \"鼠标划词翻译开关\"\n  },\n  \"command_toggle_instant\": {\n    \"message\": \"鼠标悬浮取词开关\"\n  },\n  \"command_open_quick_search\": {\n    \"message\": \"打开独立词典窗口\"\n  },\n  \"command_open_google\": {\n    \"message\": \"对当前页启用谷歌翻译\"\n  },\n  \"command_open_youdao\": {\n    \"message\": \"对当前页启用有道翻译\"\n  },\n  \"command_open_caiyun\": {\n    \"message\": \"对当前页启用彩云小译\"\n  },\n  \"command_open_pdf\": {\n    \"message\": \"在 Saladict 中浏览此 PDF\"\n  },\n  \"command_search_clipboard\": {\n    \"message\": \"在独立窗口中搜索剪贴板内容\"\n  },\n  \"command_next_history\": {\n    \"message\": \"下一个临时查询历史\"\n  },\n  \"command_prev_history\": {\n    \"message\": \"上一个临时查询历史\"\n  },\n  \"command_next_profile\": {\n    \"message\": \"下一个情景模式\"\n  },\n  \"command_prev_profile\": {\n    \"message\": \"上一个情景模式\"\n  },\n  \"command_profile_1\": {\n    \"message\": \"第一个情景模式\"\n  },\n  \"command_profile_2\": {\n    \"message\": \"第二个情景模式\"\n  },\n  \"command_profile_3\": {\n    \"message\": \"第三个情景模式\"\n  },\n  \"command_profile_4\": {\n    \"message\": \"第四个情景模式\"\n  },\n  \"command_profile_5\": {\n    \"message\": \"第五个情景模式\"\n  },\n  \"command_add_notebook\": {\n    \"message\": \"加入生词本\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/manifest/zh_TW/messages.json",
    "content": "{\n  \"extension_name\": {\n    \"description\": \"Extension name\",\n    \"message\": \"沙拉查詞-多字典滑鼠選字翻譯\"\n  },\n  \"extension_short_name\": {\n    \"description\": \"Extension short name\",\n    \"message\": \"Saladict\"\n  },\n  \"extension_description\": {\n    \"description\": \"Description of extension\",\n    \"message\": \"Saladict 沙拉查詞是一款專業滑鼠選字翻譯套件，為交叉閱讀而生。大量權威字典涵蓋中英日韓法德西語，支援複雜的選字操作、網頁翻譯、生字本与 PDF 瀏覽。\"\n  },\n  \"command_toggle_active\": {\n    \"message\": \"滑鼠選字翻譯開關\"\n  },\n  \"command_toggle_instant\": {\n    \"message\": \"滑鼠懸浮取詞開關\"\n  },\n  \"command_open_quick_search\": {\n    \"message\": \"開啟獨立詞典視窗\"\n  },\n  \"command_open_google\": {\n    \"message\": \"對此頁面使用 Google 翻譯\"\n  },\n  \"command_open_youdao\": {\n    \"message\": \"對此頁面使用有道翻譯\"\n  },\n  \"command_open_caiyun\": {\n    \"message\": \"對當前頁啟用彩雲小譯\"\n  },\n  \"command_open_pdf\": {\n    \"message\": \"在 Saladict 中瀏覽此 PDF\"\n  },\n  \"command_search_clipboard\": {\n    \"message\": \"在獨立視窗中搜尋剪貼簿內容\"\n  },\n  \"command_next_history\": {\n    \"message\": \"下一個臨時查詢歷史\"\n  },\n  \"command_prev_history\": {\n    \"message\": \"上一個臨時查詢歷史\"\n  },\n  \"command_next_profile\": {\n    \"message\": \"下一個情景模式\"\n  },\n  \"command_prev_profile\": {\n    \"message\": \"上一個情景模式\"\n  },\n  \"command_profile_1\": {\n    \"message\": \"第一個情景模式\"\n  },\n  \"command_profile_2\": {\n    \"message\": \"第二個情景模式\"\n  },\n  \"command_profile_3\": {\n    \"message\": \"第三個情景模式\"\n  },\n  \"command_profile_4\": {\n    \"message\": \"第四個情景模式\"\n  },\n  \"command_profile_5\": {\n    \"message\": \"第五個情景模式\"\n  },\n  \"command_add_notebook\": {\n    \"message\": \"加入生詞本\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/ne/background.ts",
    "content": "import { locale as _locale } from '../zh-CN/background'\n\nexport const locale: typeof _locale = {\n  app: {\n    off: 'सलाडिक्ट असक्षम। (द्रुत खोज प्यानल अझै उपलब्ध छ)',\n    tempOff:\n      'हालको ट्याबमा सलाडिक्ट असक्षम पारियो। (द्रुत खोज प्यानल अझै उपलब्ध छ)',\n    unsupported:\n      'हालको ट्याबका लागि एम्बेडेड सलाडिक प्यानल असमर्थित छ। यसको सट्टा स्ट्यान्डअलोन सलाडिक्ट प्यानल प्रयोग गर्नुहोस्।'\n  }\n}\n"
  },
  {
    "path": "src/_locales/ne/common.ts",
    "content": "import { locale as _locale } from '../zh-CN/common'\n\nexport const locale: typeof _locale = {\n  add: 'थप्नुहोस्',\n  delete: 'हटाउनुहोस्',\n  save: 'बचत गर्नुहोस्',\n  cancel: 'रद्द गर्नुहोस्',\n\n  edit: 'सम्पादन',\n  sort: 'क्रमबद्ध',\n  rename: 'पुनःनामकरण',\n\n  confirm: 'पुष्टि गर्नुहोस्',\n  changes_confirm: 'परिवर्तनहरू बचत गरिएका छैनन्। जे भए पनि बन्द?',\n  delete_confirm: 'वस्तु पूर्ण रूपमा मेटाउने',\n\n  max: 'अधिकतम',\n  min: 'न्यूनतम',\n\n  name: 'नाम',\n  none: 'खाली',\n\n  enable: 'सक्षम गर्नुहोस्',\n  enabled: 'सक्षम गरिएको',\n  disabled: 'असक्षम गरिएको',\n\n  blacklist: 'कालो सूची',\n  whitelist: 'सेतो सूची',\n\n  import: 'आयात गर्नुहोस्',\n  export: 'निर्यात गर्नुहोस्',\n\n  lang: {\n    chinese: 'चिनियाँ',\n    chs: 'चिनियाँ',\n    deutsch: 'डेउस्च',\n    eng: 'अंग्रेजी',\n    english: 'अंग्रेजी',\n    french: 'फ्रान्सेली',\n    japanese: 'जापानी',\n    korean: 'कोरियाली',\n    matchAll: 'सबै अक्षर मिलाउनुहोस्',\n    minor: 'झिनो',\n    others: 'अन्य',\n    spanish: 'स्पेनिस'\n  },\n\n  unit: {\n    mins: 'मिनेट',\n    ms: 'मिसे',\n    s: 'सेकेन्ड',\n    word: 'शब्द'\n  },\n\n  note: {\n    word: 'शब्द',\n    trans: 'अनुवाद',\n    note: 'टिप्पणी',\n    context: 'सन्दर्भ',\n    contextCloze: 'सन्दर्भ क्लोज',\n    date: 'मिति',\n    srcTitle: 'स्रोत शीर्षक',\n    srcLink: 'स्रोत लिंक',\n    srcFavicon: 'स्रोत फेभिकन'\n  },\n\n  profile: {\n    daily: 'दैनिक मोड',\n    sentence: 'वाक्य मोड',\n    default: 'पूर्वनिर्धारित मोड',\n    scholar: 'विद्वान मोड',\n    translation: 'अनुवाद मोड',\n    nihongo: 'जापानी मोड'\n  }\n}\n"
  },
  {
    "path": "src/_locales/ne/content.ts",
    "content": "import { locale as _locale } from '../zh-CN/content'\n\nexport const locale: typeof _locale = {\n  chooseLang: 'अर्को भाषा छान्नुहोस्',\n  standalone: 'सलाडिक्ट स्ट्यान्डअलोन प्यानल',\n  fetchLangList: 'पूर्ण भाषा सूची तान्नुहोस्',\n  transContext: 'पुनः अनुवाद गर्नुहोस्',\n  neverShow: 'देखाउन रोक्नुहोस्',\n  fromSaladict: 'सलाडिक्ट प्यानलबाट',\n  tip: {\n    historyBack: 'अघिल्लो खोज इतिहास',\n    historyNext: 'पछिल्लो खोज इतिहास',\n    searchText: 'पाठ खोज्नुहोस्',\n    openOptions: 'विकल्प खोल्नुहोस्',\n    addToNotebook: 'नोटबुकमा जोड्नुहोस्। नोटबुक खोल्न राइट क्लिक गर्नुहोस्',\n    openNotebook: 'नोटबुक खोल्नुहोस्',\n    openHistory: 'इतिहास खोल्नुहोस्',\n    shareImg: 'तस्विर रूपमा साझा गर्नुहोस्',\n    pinPanel: 'प्यानल पिन गर्नुहोस्',\n    closePanel: 'प्यानल बन्द गर्नुहोस्',\n    sidebar: 'साइडबार मोडमा स्विच गर्नुहोस्। दायाँ तर्फ दायाँ क्लिक गर्नुहोस्।',\n    focusPanel: 'खोज गर्दा प्यानलले फोकस गर्ने',\n    unfocusPanel: 'खोज गर्दा प्यानलले फोकस नगर्ने'\n  },\n  wordEditor: {\n    title: 'नोटबुकमा जोड्नुहोस्',\n    wordCardsTitle: 'नोटबुकबाट अन्य परिणामहरू',\n    deleteConfirm: 'नोटबुकबाट हटाउनुहोस्?',\n    closeConfirm: 'परिवर्तनहरू सुरक्षित हुँदैनन्। के तपाईँ बन्द गर्न चाहानुहुन्छ?',\n    chooseCtxTitle: 'अनुवाद गरिएको परिणामहरू छान्नुहोस्',\n    ctxHelp:\n      'अनुवाद छान्न र एन्की तालिका उत्पन्न गर्न सलाडिकलाई निर्देशन गर्न यदि तपाईँले [:: xxx ::] र --------------- ढाँचा राख्न चाहानुहुन्छ भने।'\n  },\n  machineTrans: {\n    switch: 'भाषा बदल्नुहोस्',\n    sl: 'स्रोत भाषा',\n    tl: 'लक्षित भाषा',\n    auto: 'भाषा पत्ता लगाउनुहोस्',\n    stext: 'सक्कल',\n    showSl: 'सोर्स देखाउनुहोस्',\n    copySrc: 'स्रोत कपि गर्नुहोस्',\n    copyTrans: 'अनुवाद कपि गर्नुहोस्',\n    login: 'कृपया {एसेस टोकन्} प्रदान गर्नुहोस्।',\n    dictAccount: 'एसेस टोकन्'\n  },\n  updateAnki: {\n    title: 'एन्कीमा अद्यावधिक गर्नुहोस्',\n    success: 'शब्द एन्कीमा सफलतापूर्वक अद्यावधिक गरियो।',\n    failed: 'शब्द एन्कीमा अद्यावधिक गर्न असफल भयो।'\n  }\n}\n"
  },
  {
    "path": "src/_locales/ne/langcode.ts",
    "content": "import en from '@opentranslate/languages/locales/en.json'\nimport { locale as _locale } from '../zh-CN/langcode'\n\nexport const locale: typeof _locale = {\n  ...en,\n  default: 'पुर्वनिर्धारित',\n  ne_NP: 'नेपाली',\n  ara: 'अरबी',\n  'bs-Latn': 'बोस्नियाली (ल्याटिन)',\n  bul: 'बुल्गेरियाली',\n  cht: 'चिनियाँ (परम्परागत)',\n  dan: 'डेनिस',\n  est: 'इस्टोनियाली',\n  fin: 'फिनिस',\n  fra: 'फ्रान्सेली',\n  iw: 'हिब्रु',\n  jp: 'जापानी',\n  kor: 'कोरियाली',\n  kr: 'कोरियाली',\n  pt_BR: 'पोर्तुगी (ब्राजिल)',\n  rom: 'रोमानियाली',\n  slo: 'स्लोभाकियाली',\n  spa: 'स्पेनिस',\n  swe: 'स्विडिस',\n  tl: 'फिलिपिनी (तागालोग)',\n  vie: 'भियतनामी',\n  zh: 'चिनियाँ (सरलिकृत)',\n  'zh-CHS': 'चिनियाँ (सरलिकृत)',\n  'zh-CHT': 'चिनियाँ (परम्परागत)',\n}\n"
  },
  {
    "path": "src/_locales/ne/menus.ts",
    "content": "import { locale as _locale } from '../zh-CN/menus'\n\nexport const locale: typeof _locale = {\n  baidu_page_translate: 'बाइडु पृष्ठ अनुवाद',\n  baidu_search: 'बाइडु खोजी',\n  bing_dict: 'बिङ शब्दकोश',\n  bing_search: 'बिङ खोजी',\n  caiyuntrs: 'Lingocloud पृष्ठ अनुवाद',\n  cambridge: 'क्याम्ब्रिज',\n  copy_pdf_url: 'पीडीएफ यूआरएल क्लिपबोर्डमा प्रतिलिपि गर्नुहोस्',\n  dictcn: 'Dictcn',\n  etymonline: 'Etymonline',\n  google_cn_page_translate: 'Google cn पृष्ठ अनुवाद',\n  google_page_translate: 'Google पृष्ठ अनुवाद',\n  google_search: 'Google खोजी',\n  google_translate: 'Google अनुवाद',\n  google_cn_translate: 'Google.cn अनुवाद',\n  guoyu: '國語辭典',\n  history_title: 'खोज इतिहास',\n  iciba: 'iciba',\n  liangan: '兩岸詞典',\n  longman_business: 'Longman Business',\n  manual_title: 'मैनुअल',\n  merriam_webster: 'Merriam Webster',\n  microsoft_page_translate: 'Microsoft पृष्ठ अनुवाद',\n  notebook_title: 'नयाँ शब्द सूची',\n  notification_youdao_err:\n    'यूडाओ पृष्ठ अनुवाद 2.0ले प्रतिक्रिया दिएन ।\\nसलाडिक्ट यस पृष्ठमा पहुँच प्राप्त गर्न सक्दैन।\\nयदि यूडाओ प्यानल देखाइएको छ भने यो सन्देश अवहेलना गर्नुहोस्।',\n  oxford: 'अक्सफोर्ड',\n  page_permission_err:\n    'सलाडिक्ट \"{{name}}\" यस पृष्ठमा पहुँच प्राप्त गर्न अनुमति छैन।',\n  page_translations: 'पृष्ठ अनुवाद',\n  saladict: 'सलाडिक्ट',\n  saladict_standalone: 'सलाडिक्ट स्ट्यान्डअलोन प्यानल',\n  sogou: 'सोगो अनुवाद',\n  sogou_page_translate: 'सोगो पृष्ठ अनुवाद',\n  termonline: 'Termonline',\n  view_as_pdf: 'पीडीएफ भिउमा खोल्नुहोस्',\n  youdao: 'यौडाओ',\n  youdao_page_translate: 'यौडाओ पृष्ठ अनुवाद',\n  youglish: 'यौग्लिश'\n}\n"
  },
  {
    "path": "src/_locales/ne/options.ts",
    "content": "import { locale as _locale } from '../zh-CN/options'\n\nexport const locale: typeof _locale = {\n  title: 'Saladict Options',\n  previewPanel: 'Preview Dict Panel',\n  shortcuts: 'Set Shortcuts',\n  msg_update_error: 'Unable to update',\n  msg_updated: 'Successfully updated',\n  msg_first_time_notice: 'First time notice',\n  msg_err_permission: 'Unable to request \"{{permission}}\" permission.',\n  unsave_confirm: 'Settings not saved. Sure to leave?',\n  nativeSearch: 'search selected text outside of browser',\n  firefox_shortcuts:\n    'Open about:addons, click the top right \"gear\" button, choose the last \"Manage extension shortcuts\".',\n  tutorial: 'Tutorial',\n  page_selection: 'Page Selection',\n\n  nav: {\n    General: 'General',\n    Notebook: 'Notebook',\n    Profiles: 'Profiles',\n    DictPanel: 'Dict Panel',\n    SearchModes: 'Search Modes',\n    Dictionaries: 'Dictionaries',\n    DictAuths: 'Access Tokens',\n    Popup: 'Popup Panel',\n    QuickSearch: 'Quick Search',\n    Pronunciation: 'Pronunciation',\n    PDF: 'PDF',\n    ContextMenus: 'Context Menus',\n    BlackWhiteList: 'Black/White List',\n    ImportExport: 'Import/Export',\n    Privacy: 'Privacy',\n    Permissions: 'Permissions'\n  },\n\n  config: {\n    active: 'Enable Inline Translator',\n    active_help:\n      '\"Quick Search\" is still available even if Inline translation is turned off.',\n    animation: 'Animation transitions',\n    animation_help: 'Switch off animation transitions to reduce runtime cost.',\n    runInBg: 'Keep in Background',\n    runInBg_help:\n      'Keep the browser running in background after close so that global shortcuts still work.',\n    darkMode: 'Dark Mode',\n    langCode: 'App Language',\n    editOnFav: 'Open WordEditor when saving',\n    editOnFav_help:\n      'When turned off, new words will be added to notebook directly.',\n    searchHistory: 'Keep search history',\n    searchHistory_help:\n      'Your browsing history could be unintentionally revealed in Search history.',\n    searchHistoryInco: 'Also in incognito mode',\n    ctxTrans: 'Context Translate Engines',\n    ctxTrans_help:\n      'Context sentence will be translated before being added to notebook.',\n    searchSuggests: 'Search suggests',\n    panelMaxHeightRatio: 'Panel max height ratio',\n    panelWidth: 'Panel width',\n    fontSize: 'Font size for search reasults',\n    bowlOffsetX: 'Saladict icon Offset X',\n    bowlOffsetY: 'Saladict icon Offset Y',\n    panelCSS: 'Custom Dict Panel Styles',\n    panelCSS_help:\n      'Custom CSS. For Dict Panel use .dictPanel-Root as root. For dictionaries use .dictRoot or .d-{id} as root',\n    noTypeField: 'No selection on editable regions',\n    noTypeField_help:\n      'If selection making in editable regions is banned, the extension will identify Input Boxes, TextAreas and other common text editors like CodeMirror, ACE and Monaco.',\n    touchMode: 'Touch Mode',\n    touchMode_help: 'Enable touch related selection',\n    language: 'Selection Languages',\n    language_help:\n      'Search when selection contains words in the chosen languages.',\n    language_extra:\n      'Note that Japanese and Korean also include Chinese. French, Deutsch and Spanish also include English. If Chinese or English is cancelled while others are selected, only the exclusive parts of those languages are tested. E.g. kana characters in Japanese.',\n    doubleClickDelay: 'Double Click Delay',\n    mode: 'Normal Selection',\n    panelMode: 'Inside Dict Panel',\n    pinMode: 'When Panel is Pinned',\n    qsPanelMode: 'When Standalone Panel is Opened',\n    bowlHover: 'Icon Mouse Hover',\n    bowlHover_help:\n      'Hover on the bowl icon to trigger searching instead of clicking.',\n    autopron: {\n      cn: {\n        dict: 'Chinese Auto-Pronounce'\n      },\n      en: {\n        dict: 'English Auto-Pronounce',\n        accent: 'Accent Preference'\n      },\n      machine: {\n        dict: 'Machine Auto-Pronounce',\n        src: 'Machine Pronounce',\n        src_help:\n          'Machine Translation Dictionary needs to be added and enabled on the list below to enable auto-pronunciation.',\n        src_search: 'Read Source Text',\n        src_trans: 'Read Translation Text'\n      }\n    },\n    pdfSniff: 'Enable PDF Sniffer',\n    pdfSniff_help: 'If turned on， PDF links will be automatically captured.',\n    pdfSniff_extra:\n      'It is recommended to {search selected text outside of browser} with your own favorite local reader.',\n    pdfStandalone: 'Standalone Panel',\n    pdfStandalone_help: 'Open PDF viewer in standalone panel.',\n    baWidth: 'Width',\n    baWidth_help:\n      'Browser Action Panel wdith. Dict Panel width will be used if a negative value is chosen.',\n    baHeight: 'Height',\n    baHeight_help: 'Browser Action Panel height.',\n    baOpen: 'Browser Action',\n    baOpen_help:\n      'When clicking the browser action icon in toolbar (next to the address bar). Items are same as Context Menus, which can be added or edited on the Context Menus config page.',\n    tripleCtrl: 'Enable Ctrl Shortkey',\n    tripleCtrl_help:\n      'Press {⌘ Command}(macOS) or {Ctrl}(Others) three times (or with browser shortkey) to summon the dictionary panel. ',\n    defaultPinned: 'Pinned when shows up',\n    qsLocation: 'Location',\n    qsFocus: 'Focus when shows up',\n    qsStandalone: 'Standalone',\n    qsStandalone_help:\n      'Render dict panel in a standalone window. You can {search selected text outside of browser}.',\n    qssaSidebar: 'Sidebar Layout',\n    qssaSidebar_help: 'Rearrange windows to sidebar-like layout.',\n    qssaHeight: 'Window Height',\n    qssaPageSel: 'Selection Response',\n    qssaPageSel_help: 'Response to page selection.',\n    qssaRectMemo: 'Remember size and position',\n    qssaRectMemo_help: 'Remember standalone panel size and position on close.',\n    updateCheck: 'Check Update',\n    updateCheck_help: 'Check update automatically.',\n    analytics: 'Enable Google Analytics',\n    analytics_help:\n      'Share anonymous device browser version information. Saladict author will offer prioritized support to popular devices and browsers.',\n\n    opt: {\n      reset: 'Reset Configs',\n      reset_confirm: 'Reset to default settings. Confirm？',\n      upload_error: 'Unable to save settings.',\n      accent: {\n        uk: 'UK',\n        us: 'US'\n      },\n      sel_blackwhitelist: 'Selection Black/White List',\n      sel_blackwhitelist_help:\n        'Saladict will not react to selection in blacklisted pages.',\n      pdf_blackwhitelist_help:\n        'Blacklisted PDF links will not jump to Saladict PDF Viewer.',\n      contextMenus_description:\n        'Each context menus item can also be customized. Youdao and Google page translate are deprecated in favor of the official extensions.',\n      contextMenus_edit: 'Edit Context Menus Items',\n      contextMenus_url_rules: 'URL with %s in place of query.',\n      baOpen: {\n        popup_panel: 'Dict Panel',\n        popup_fav: 'Add to Notebook',\n        popup_options: 'Open Saladict Options',\n        popup_standalone: 'Open Saladict Standalone Panel'\n      },\n      openQsStandalone: 'Standalone Panel Options',\n      pdfStandalone: {\n        default: 'Never',\n        always: 'Always',\n        manual: 'Manual'\n      }\n    }\n  },\n\n  matchPattern: {\n    description:\n      'Specify URL as {URL Match Pattern} or {Regular Expression}. Empty fields will be removed.',\n    url: 'URL Match Pattern',\n    url_error: 'Incorrect URL Match Pattern.',\n    regex: 'Regular Expression',\n    regex_error: 'Incorrect Regular Expression.'\n  },\n\n  searchMode: {\n    icon: 'Show Icon',\n    icon_help: 'A cute little icon pops up nearby the cursor.',\n    direct: 'Direct Search',\n    direct_help: 'Show dict panel directly.',\n    double: 'Double Click',\n    double_help: 'Show dict panel after double click selection.',\n    holding: 'Hold a key',\n    holding_help:\n      'After a selection is made, the selected key must be pressing when releasing mouse (Alt is \"⌥ Option\" on macOS. Meta key is \"⌘ Command\" on macOS and \"⊞ Windows\" for others.).',\n    instant: 'Instant Capture',\n    instant_help: 'Selection is automatically made near by the cursor.',\n    instantDirect: 'Direct',\n    instantKey: 'Key',\n    instantKey_help:\n      'If \"Direct\" is chosen it is also recommeded setting browser shortkey to toggle Instant Capture. Otherwise browser text selection could be unable to perform.',\n    instantDelay: 'Capture delay'\n  },\n\n  profiles: {\n    opt: {\n      add_name: 'Add Profile Name',\n      delete_confirm: 'Delete Profile \"{{name}}\". Confirm?',\n      edit_name: 'Change Profile Name',\n      help:\n        'Each profile represents an independent set of settings. Some of the settings (with {*} prefix) change according to profile. One may switch profiles by hovering on the menu icon on Dict Panel, or focus on the icon then hit {↓}.'\n    }\n  },\n\n  profile: {\n    mtaAutoUnfold: 'Auto unfold multiline search box',\n    waveform: 'Waveform Control',\n    waveform_help:\n      'Display a button at the bottom of the Dict Panel for expanding the Waveform Control Panel which is only loaded after expansion.',\n    stickyFold: 'Sticky Folding',\n    stickyFold_help:\n      'Remembers manual dictionary folding/unfolding states when searching. Only last on the same page.',\n\n    opt: {\n      item_extra: 'This option may change base on \"Profile\".',\n      mtaAutoUnfold: {\n        always: 'Keep Unfolding',\n        never: 'Never Unfold',\n        once: 'Unfold Once',\n        popup: 'Only On Browser Action',\n        hide: 'Hide'\n      },\n      dict_selected: 'Selected Dicts'\n    }\n  },\n\n  dict: {\n    add: 'Add dicts',\n    more_options: 'More Options',\n\n    selectionLang: 'Selection Languages',\n    selectionLang_help:\n      'Show this dictionary when selection contains words in the chosen languages.',\n    defaultUnfold: 'Default Unfold',\n    defaultUnfold_help:\n      \"If turned off, this dictionary won't start searching unless it's title bar is clicked.\",\n    selectionWC: 'Selection Word Count',\n    selectionWC_help:\n      'Show this dictionary when selection word count meets the requirements. Set 999999 for unlimited words.',\n    preferredHeight: 'Default Panel Height',\n    preferredHeight_help:\n      'Maximum height on first appearance. Contents exceeding this height will be hidden. Set 999999 for unlimited height.',\n\n    lang: {\n      de: 'De',\n      en: 'En',\n      es: 'Es',\n      fr: 'Fr',\n      ja: 'Ja',\n      kor: 'Kor',\n      zhs: 'Zhs',\n      zht: 'Zht'\n    }\n  },\n\n  syncService: {\n    description: 'Sync settings.',\n    start: 'Syncing. Do not close this page until finished.',\n    finished: 'Syncing finished',\n    success: 'Syncing success',\n    failed: 'Syncing failed',\n    close_confirm: 'Settings not saved. Close?',\n    delete_confirm: 'Delete?',\n\n    shanbay: {\n      description:\n        \"Go to shanbay.com and log in first(must stay logged in). Note that it's a one-way sync(from Saladict to Shanbay). Only the new added words are synced. Words also need to be supported by Shanbay's database.\",\n      login:\n        'Will open shanbay.com. Please log in then come back and enable again.',\n      sync_all: 'Upload all existing new words',\n      sync_all_confirm:\n        'Too many new words in notebook. Saladict will upload in batches. Note that uploading too many words in short period would cause account banning which is unrecoverable. Confirm?',\n      sync_last: 'Upload the last new word'\n    },\n\n    eudic: {\n      description:\n        'Before using Eudic to synchronize words, you must first create a default new word book on Eudic official website (my.eudic.net/home/index) (generally, it will be automatically generated and cannot be deleted after the first manual import). Pay attention not to synchronize frequently in a short time, which may cause temporary lock.',\n      token: 'Authorization information',\n      getToken: 'Get authorization',\n      verify: 'Check authorization information',\n      verified: 'Eudic authorization information checked successfully',\n      enable_help:\n        'After opening, each new word added will be automatically synchronized to the Eudic default word book (salad to Eudic word book) in one direction, and only the new word itself will be synchronized (deleted out of synchronization)',\n      token_help:\n        'Please confirm to set valid personal authorization information, otherwise the synchronization will fail. You can click the button at the bottom to check.',\n      sync_all: 'Synchronize all new words',\n      sync_help:\n        'Synchronize all existing new words in salad word book to the Eudic default word book (turn on the synchronization switch above at the same time and click save)',\n      sync_all_confirm:\n        'Note that frequent synchronization in a short time may lead to lock temporarily. Are you sure to continue?'\n    },\n\n    webdav: {\n      description:\n        'Extension settings (including this) are synced via browser. New words notebook can be synced via WebDAV through settings here.',\n      jianguo: 'See Jianguoyun for example',\n      checking: 'Connecting...',\n      exist_confirm:\n        'Saladict directory exists on server. Download it and merge with local data?',\n      upload_confirm: 'Upload local data to Server right away?',\n      verify: 'Verify server',\n      verified: 'Successfully verified WebDAV server.',\n      duration: 'Duration',\n      duration_help:\n        'Data is guaranteed to be updated before upload. If you do not need real-time syncing across browsers, set a longer polling cycle to reduce CPU and memory footprint.',\n      passwd: 'Password',\n      url: 'Server Address',\n      user: 'User Account'\n    },\n\n    ankiconnect: {\n      description:\n        'Please make sure Anki Connect plugin is installed and Anki is running. You can also update word to Anki in Word Editor.',\n      checking: 'Checking...',\n      deck_confirm:\n        'Deck \"{{deck}}\" does not exist in Anki. Generate a new deck?',\n      deck_error: 'Unable to create deck \"{{deck}}\".',\n      notetype_confirm:\n        'Note type \"{{noteType}}\" does not exist in Anki. Generate a new note type.',\n      notetype_error: 'Unable to create note type \"{{noteType}}\".',\n      upload_confirm:\n        'Sync local new words to Anki right away? Duplicated words (with same timestamp) will be skipped.',\n      add_yourself: 'Please add it youself in Anki.',\n      verify: 'Verify Anki Connect',\n      verified: 'Successfully verified Anki Connect',\n      enable_help:\n        'When enabled, each time a new word is added to Notebook it will also be ported to Anki automatically. Words that exist in Anki(with same \"Date\") can be force-updated in Word Editor.',\n      host: 'Address',\n      port: 'port',\n      key: 'Key',\n      key_help:\n        'Optional key can be added in Anki Connect config for identification.',\n      deckName: 'Deck',\n      deckName_help:\n        'If deck does not exist you can generate a default one automatically by clicking \"Verify Anki Connect\" below.',\n      noteType: 'Note Type',\n      noteType_help:\n        'Anki note type includes a set of fields and card type. If note type does not exist you can generate a default one automatically by clicking \"Verify Anki Connect\" below. DO NOT change field names when editing or adding card templates in Anki',\n      tags: 'Tags',\n      tags_help: 'Anki notes can include tags separated with commas.',\n      escapeHTML: 'Escape HTML',\n      escapeHTML_help:\n        'Escape HTML entities. Turn off if using HTML for manual layout.',\n      syncServer: 'Sync Server',\n      syncServer_help:\n        'Sync to server(e.g. AnkiWeb) after new words being added to local Anki.'\n    }\n  },\n\n  titlebarOffset: {\n    title: 'Calibrate Titlebar Height',\n    help:\n      'Different systems or browser settings may result in different titlebar height. Saladict will attempt to calibrate automatically. If you may adjust manually.',\n    main: 'Normal',\n    main_help: 'Normal windows may not have titlebar.',\n    panel: 'Panel',\n    panel_help:\n      'Saladict standalone quick search panel is a type of panel window.',\n    calibrate: 'Auto-calibrate',\n    calibrateSuccess: 'Calibration success',\n    calibrateError: 'Calibration failed'\n  },\n\n  headInfo: {\n    acknowledgement: {\n      title: 'Acknowledgement',\n      yipanhuasheng:\n        \"for adding Merriam Webster's Dict, American Heritage Dict, Oxford Learner's Dict and Eudic Notebook sync service; and updating Urban Dict and Naver Dict\",\n      naver: 'for helping add Naver dict',\n      shanbay: 'for adding Shanbay dict',\n      trans_tw: 'for traditional Chinese translation',\n      weblio: 'for helping add Weblio dict'\n    },\n    contact_author: 'Contact Author',\n    donate: 'Donate',\n    instructions: 'Instructions',\n    report_issue: 'Report Issue'\n  },\n\n  form: {\n    url_error: 'Incorrect URL.',\n    number_error: 'Incorrect number.'\n  },\n\n  preload: {\n    title: 'Preload',\n    auto: 'Auto search',\n    auto_help: 'Search automatically when panel shows up.',\n    clipboard: 'Clipboard',\n    help: 'Preload content in search box when panel shows up.',\n    selection: 'Page Selection'\n  },\n\n  locations: {\n    CENTER: 'Center',\n    TOP: 'Top',\n    RIGHT: 'Right',\n    BOTTOM: 'Bottom',\n    LEFT: 'Left',\n    TOP_LEFT: 'Top Left',\n    TOP_RIGHT: 'Top Right',\n    BOTTOM_LEFT: 'Bottom Left',\n    BOTTOM_RIGHT: 'Bottom Right'\n  },\n\n  import_export_help:\n    'Configs are auto-synced via browser. Here you can also import/export manually. Backups are exported as plain text files. Please encrypt it yourself if needed.',\n\n  import: {\n    title: 'Import Configs',\n    error: {\n      title: 'Import Error',\n      parse: 'Unable to parse backup. Incorrect format.',\n      load: 'Unable to load backup. Browser cannot obtain the local file.',\n      empty: 'No valid data found in the backup.'\n    }\n  },\n\n  export: {\n    title: 'Export Configs',\n    error: {\n      title: 'Export Error',\n      empty: 'No config to export.',\n      parse: 'Unable to parse configs.'\n    }\n  },\n\n  dictAuth: {\n    description:\n      'As the number of Saladict users grows, if you make heavily use of machine translation services it is recommended to register an account for better stability and accuracy. The account data will only be stored in the browser.',\n    dictHelp: 'See the official website of {dict}.',\n    manage: 'Manage Translator Accounts'\n  },\n\n  third_party_privacy: 'Third Party Privacy',\n  third_party_privacy_help:\n    'Saladict will not collect further information but search text and releated cookies will be sent to third party dictionary services(just like how you would search on their websites). If you do not want third party services to collect you data, remove the corresponding dictionaries at \"Dictionaries\" settings.',\n  third_party_privacy_extra:\n    'Cannot be turned off as it is the core functionality of Saladict.',\n\n  permissions: {\n    success: 'Permission requested',\n    cancel_success: 'Permission cancelled',\n    failed: 'Permission request failed',\n    cancelled: 'Permission request cancelled by user',\n    missing:\n      'Missing permission \"{{permission}}\". Either grant it or disable related functions.',\n    clipboardRead: 'Read Clipboard',\n    clipboardRead_help:\n      'This permission is needed when clipboard preload is enable for popup panel or quick search panel.',\n    clipboardWrite: 'Write Clipboard',\n    clipboardWrite_help:\n      'This permission is needed when using titlebar menus to copy source/target text from machine translator.'\n  },\n\n  unsupportedFeatures: {\n    ff: 'Feature \"{{feature}}\" is not supported in Firefox.'\n  }\n}\n"
  },
  {
    "path": "src/_locales/ne/popup.ts",
    "content": "import { locale as _locale } from '../zh-CN/popup'\n\nexport const locale: typeof _locale = {\n  title: 'सलाडिक्ट ब्राउजर एक्सन प्यानल',\n  app_active_title: 'ईनलाइन अनुवादक सक्षम गर्नुहोस्',\n  app_temp_active_title: 'पृष्ठमा अस्थायी रूपमा असक्षम गरियो',\n  instant_capture_pinned: ' (ताराङकित)',\n  instant_capture_title: 'तत्काल क्याप्चर सक्षम गर्नुहोस्',\n  notebook_added: 'थपियो',\n  notebook_empty: 'हालको पृष्ठमा कुनै चयन फेला परेन',\n  notebook_error: 'नोटबुकमा चयन गरिएको पाठ थप्न सकिएन',\n  page_no_response: 'पृष्ठको कुनै प्रतिक्रिया छैन',\n  qrcode_title: 'पृष्ठको क्युआर कोड'\n}\n"
  },
  {
    "path": "src/_locales/ne/wordpage.ts",
    "content": "import { locale as _locale } from '../zh-CN/wordpage'\n\nexport const locale: typeof _locale = {\n  title: {\n    history: 'सलाडिक्ट खोज इतिहास',\n    notebook: 'सलाडिक्ट नोटबुक',\n  },\n\n  localonly: 'स्थानीयमा मात्र',\n\n  column: {\n    add: 'थप्नुहोस्',\n    date: 'मिति',\n    edit: 'सम्पादन',\n    note: 'टिप्पणी',\n    source: 'स्रोत',\n    trans: 'अनुवाद',\n    word: 'शब्द'\n  },\n\n  delete: {\n    title: 'मेटाउनुहोस्',\n    all: 'सबै मेटाउनुहोस्',\n    confirm: '. साच्चै ?',\n    page: 'पृष्ठ मेटाउनुहोस्',\n    selected: 'चयन गरिएको मेटाउनुहोस्'\n  },\n\n  export: {\n    title: 'निर्यात',\n    all: 'सबै निर्यात गर्नुहोस्',\n    description: 'हरेक रेकर्डको आकार बताउनुहोस्:',\n    explain: 'एन्की र अन्य उपकरणमा कसरी निर्यात गर्ने',\n    gencontent: 'निर्मित सामग्री',\n    linebreak: {\n      default: 'पूर्वनिर्धारित लाइनब्रेक राख्नुहोस्',\n      n: 'लाइनब्रेकहरूलाई \\\\n संग स्थानान्तरण गर्नुहोस्',\n      br: 'लाइनब्रेकहरूलाई <br> संग स्थानान्तरण गर्नुहोस्',\n      p: 'लाइनब्रेकहरूलाई <p> संग स्थानान्तरण गर्नुहोस्',\n      space: 'लाइनब्रेकहरूलाई स्पेस संग स्थानान्तरण गर्नुहोस्'\n    },\n    page: 'पृष्ठ निर्यात गर्नुहोस्',\n    placeholder: 'प्लेसहोल्डर',\n    htmlescape: {\n      title: 'टिप्पणीहरूमा HTML वर्णहरू ऐस्केप गर्नुहोस्',\n      text: 'HTML ऐस्केप गर्नुहोस्'\n    },\n    selected: 'चयन गरिएको निर्यात गर्नुहोस्'\n  },\n\n  filterWord: {\n    chs: 'चिनियाँ',\n    eng: 'अंग्रेजी',\n    word: 'शब्द',\n    phrase: 'वाक्यांश'\n  },\n\n  wordCount: {\n    selected: '{{count}} बस्तु चयन गरिएको',\n    selected_plural: '{{count}} बस्तुहरु चयन गरिएको',\n    total: '{{count}} बस्तु जम्मा',\n    total_plural: '{{count}} बस्तुहरु जम्मा'\n  }\n}\n"
  },
  {
    "path": "src/_locales/zh-CN/background.ts",
    "content": "export const locale = {\n  app: {\n    off: '沙拉查词已关闭（快捷查词依然可用）',\n    tempOff: '沙拉查词已对当前标签关闭（快捷查词依然可用）',\n    unsupported: '内嵌查词面板不支持此类页面（独立窗口查词面板依然可用）'\n  }\n}\n"
  },
  {
    "path": "src/_locales/zh-CN/common.ts",
    "content": "export const locale = {\n  add: '添加',\n  delete: '删除',\n  save: '保存',\n  cancel: '取消',\n\n  edit: '编辑',\n  sort: '排序',\n  rename: '重命名',\n\n  confirm: '确认',\n  changes_confirm: '修改未保存。确认关闭？',\n  delete_confirm: '确定完全删除该条目？',\n\n  max: '最大',\n  min: '最小',\n\n  name: '名称',\n  none: '无',\n\n  enable: '开启',\n  enabled: '已开启',\n  disabled: '已关闭',\n\n  blacklist: '黑名单',\n  whitelist: '白名单',\n\n  import: '导入',\n  export: '导出',\n\n  lang: {\n    chinese: '中文',\n    chs: '中文',\n    deutsch: '德文',\n    eng: '英文',\n    english: '英文',\n    french: '法文',\n    japanese: '日文',\n    korean: '韩文',\n    minor: '其它语言',\n    matchAll: '所有的字符都必须匹配',\n    others: '其它字符',\n    spanish: '西班牙文'\n  },\n\n  unit: {\n    mins: '分钟',\n    ms: '毫秒',\n    s: '秒',\n    word: '个'\n  },\n\n  note: {\n    word: '单词',\n    trans: '翻译',\n    note: '笔记',\n    context: '上下文',\n    contextCloze: '上下文填空',\n    date: '日期',\n    srcTitle: '来源标题',\n    srcLink: '来源链接',\n    srcFavicon: '来源图标'\n  },\n\n  profile: {\n    daily: '日常模式',\n    sentence: '句库模式',\n    default: '默认模式',\n    scholar: '学术模式',\n    translation: '翻译模式',\n    nihongo: '日语模式'\n  }\n}\n"
  },
  {
    "path": "src/_locales/zh-CN/content.ts",
    "content": "export const locale = {\n  chooseLang: '-选择其它语言-',\n  standalone: '沙拉查词-独立查词窗口',\n  fetchLangList: '获取全部语言列表',\n  transContext: '重新翻译',\n  neverShow: '不再弹出',\n  fromSaladict: '来自沙拉查词面板',\n  tip: {\n    historyBack: '上一个查词记录',\n    historyNext: '下一个查词记录',\n    searchText: '查单词',\n    openOptions: '打开设置',\n    addToNotebook: '保存单词到生词本，右键打开生词本',\n    openNotebook: '打开生词本',\n    openHistory: '打开查词记录',\n    shareImg: '以图片方式分享查词结果',\n    pinPanel: '钉住查词面板',\n    closePanel: '关闭查词面板',\n    sidebar: '切换侧边栏模式，右键切换右侧',\n    focusPanel: '查词时面板获取焦点',\n    unfocusPanel: '查词时面板不获取焦点'\n  },\n  wordEditor: {\n    title: '保存到生词本',\n    wordCardsTitle: '生词本其它记录',\n    deleteConfirm: '从单词本中移除？',\n    closeConfirm: '记录尚未保存，确认关闭？',\n    chooseCtxTitle: '选择翻译结果',\n    ctxHelp:\n      '如需兼容选择翻译结果以及 Anki 生成表格请保持 [:: xxx ::] 和 --------------- 格式。'\n  },\n  machineTrans: {\n    switch: '更改语言',\n    sl: '来源语言',\n    tl: '目标语言',\n    auto: '自动检测',\n    stext: '原文',\n    showSl: '显示原文',\n    copySrc: '复制原文',\n    copyTrans: '复制译文',\n    login: '请登录{词典帐号}以使用。',\n    dictAccount: '词典帐号'\n  },\n  updateAnki: {\n    title: '更新到 Anki',\n    success: '更新到 Anki 成功。',\n    failed: '更新单词到 Anki 失败。'\n  }\n}\n"
  },
  {
    "path": "src/_locales/zh-CN/langcode.ts",
    "content": "import zhCN from '@opentranslate/languages/locales/zh-CN.json'\n\nexport const locale = {\n  ...zhCN,\n  default: '随扩展语言',\n  ara: '阿拉伯语',\n  'bs-Latn': '波斯尼亚语',\n  bul: '保加利亚语',\n  cht: '中文（繁体）',\n  dan: '丹麦语',\n  est: '爱沙尼亚语',\n  fin: '芬兰语',\n  fra: '法语',\n  iw: '希伯来语',\n  jp: '日语',\n  kor: '韩语',\n  kr: '韩语',\n  pt_BR: '巴西语',\n  rom: '罗马尼亚语',\n  slo: '斯洛文尼亚语',\n  spa: '西班牙语',\n  swe: '瑞典语',\n  tl: '塔加路语（菲律宾语）',\n  vie: '越南语',\n  zh: '中文（简体）',\n  'zh-CHS': '中文（简体）',\n  'zh-CHT': '中文（繁体）'\n}\n"
  },
  {
    "path": "src/_locales/zh-CN/menus.ts",
    "content": "export const locale = {\n  baidu_page_translate: '百度网页翻译',\n  baidu_search: '百度搜索',\n  bing_dict: '必应词典',\n  bing_search: '必应搜索',\n  caiyuntrs: '彩云小译网页翻译',\n  cambridge: '剑桥词典',\n  copy_pdf_url: '复制PDF链接到剪贴板',\n  dictcn: '海词词典',\n  etymonline: '培根词源',\n  google_cn_page_translate: '谷歌cn网页翻译',\n  google_page_translate: '谷歌网页翻译',\n  google_search: '谷歌搜索',\n  google_translate: '谷歌翻译',\n  google_cn_translate: '谷歌CN翻译',\n  guoyu: '国语辞典',\n  history_title: '查词历史记录',\n  iciba: '金山词霸',\n  liangan: '两岸词典',\n  longman_business: '朗文商务',\n  manual_title: '详细使用说明',\n  merriam_webster: '韦氏词典',\n  microsoft_page_translate: '微软网页翻译',\n  notebook_title: '生词本',\n  notification_youdao_err:\n    '有道网页翻译2.0 加载无响应，\\n可能扩展无权访问该页面，\\n如加载成功请忽略本消息。',\n  oxford: '牛津词典',\n  page_permission_err: '沙拉查词「{{name}}」无权访问此页面。',\n  page_translations: '网页翻译',\n  saladict: '沙拉查词',\n  saladict_standalone: '沙拉查词独立窗口',\n  sogou: '搜狗翻译',\n  sogou_page_translate: '搜狗网页翻译',\n  termonline: '术语在线',\n  view_as_pdf: '在 PDF 阅读器中打开',\n  youdao: '有道词典',\n  youdao_page_translate: '有道网页翻译',\n  youglish: 'YouGlish'\n}\n"
  },
  {
    "path": "src/_locales/zh-CN/options.ts",
    "content": "export const locale = {\n  title: '沙拉查词设置',\n  previewPanel: '预览查词面板',\n  shortcuts: '设置快捷键',\n  msg_update_error: '设置更新失败',\n  msg_updated: '设置已更新',\n  msg_first_time_notice: '初次使用注意',\n  msg_err_permission: '权限“{{permission}}”申请失败。',\n  unsave_confirm: '修改尚未保存，确定放弃？',\n  nativeSearch: '浏览器外划词',\n  firefox_shortcuts:\n    '地址栏输入 about:addons 打开，点击右上方的齿轮，选择最后一项管理扩展快捷键。',\n  tutorial: '教程',\n  page_selection: '网页划词',\n\n  nav: {\n    General: '基本选项',\n    Notebook: '单词管理',\n    Profiles: '情景模式',\n    DictPanel: '查词面板',\n    SearchModes: '查词习惯',\n    Dictionaries: '词典设置',\n    DictAuths: '词典帐号',\n    Popup: '右上弹框',\n    QuickSearch: '快捷查词',\n    Pronunciation: '发音设置',\n    PDF: 'PDF 设置',\n    ContextMenus: '右键菜单',\n    BlackWhiteList: '黑白名单',\n    ImportExport: '导入导出',\n    Privacy: '隐私设置',\n    Permissions: '权限管理'\n  },\n\n  config: {\n    active: '启用划词翻译',\n    active_help: '关闭后「快捷查词」功能依然可用。',\n    animation: '开启动画过渡',\n    animation_help: '在低性能设备上关闭过渡动画可减少渲染负担。',\n    runInBg: '后台保持运行',\n    runInBg_help:\n      '让浏览器关闭后依然保持后台运行，从而继续响应快捷键以及浏览器外划词。',\n    darkMode: '黑暗模式',\n    langCode: '界面语言',\n    editOnFav: '红心单词时弹出编辑面板',\n    editOnFav_help:\n      '关闭后，点击红心生词将自动添加到生词本，上下文翻译亦会自动获取。',\n    searchHistory: '记录查词历史',\n    searchHistory_help: '查词记录可能会泄漏您的浏览痕迹。',\n    searchHistoryInco: '在私隐模式中记录',\n    ctxTrans: '上下文翻译引擎',\n    ctxTrans_help: '单词被添加进生词本前会自动翻译上下文。',\n    searchSuggests: '输入时显示候选',\n    panelMaxHeightRatio: '查词面板最高占屏幕比例',\n    panelWidth: '查词面板宽度',\n    fontSize: '词典内容字体大小',\n    bowlOffsetX: '沙拉图标水平偏移',\n    bowlOffsetY: '沙拉图标垂直偏移',\n    panelCSS: '自定义查词面板样式',\n    panelCSS_help:\n      '为查词面板添加自定义 CSS 。词典面板使用 .dictPanel-Root 作为根，词典使用 .dictRoot 或者 .d-词典ID 作为根。',\n    noTypeField: '不在输入框划词',\n    noTypeField_help:\n      '开启后，本扩展会自动识别输入框以及常见编辑器，如 CodeMirror、ACE 和 Monaco。',\n    touchMode: '触摸模式',\n    touchMode_help: '支持触摸相关选词。',\n    language: '划词语言',\n    language_help: '当选中的文字包含相应的语言时才进行查找。',\n    language_extra:\n      '注意日语与韩语也包含了汉字。法语、德语和西语也包含了英文。若取消了中文或英语而勾选了其它语言，则只匹配那些语言独有的部分，如日语只匹配假名。',\n    doubleClickDelay: '双击间隔',\n    mode: '普通划词',\n    defaultPinned: '出现时钉住查词面板',\n    panelMode: '查词面板内部划词',\n    pinMode: '查词面板钉住后划词',\n    qsPanelMode: '独立窗口响应页面划词',\n    bowlHover: '图标悬停查词',\n    bowlHover_help: '鼠标悬停在沙拉图标上触发查词，否则需要点击。',\n    autopron: {\n      cn: {\n        dict: '中文自动发音'\n      },\n      en: {\n        dict: '英文自动发音',\n        accent: '优先口音'\n      },\n      machine: {\n        dict: '机器自动发音',\n        src: '机器发音部分',\n        src_help: '机器翻译词典需要在下方添加并启用才会自动发音。',\n        src_search: '朗读原文',\n        src_trans: '朗读翻译'\n      }\n    },\n    pdfSniff: '嗅探 PDF 链接',\n    pdfSniff_help:\n      '开启后所有 PDF 链接将自动跳转到本扩展打开（包括本地，如果在扩展管理页面勾选了允许）。',\n    pdfSniff_extra: '现在更推荐使用自己喜欢的本地阅读器搭配{浏览器外划词}。',\n    pdfStandalone: '独立窗口',\n    pdfStandalone_help:\n      '在独立窗口中打开 PDF 阅读器。独立窗口只有标题栏，占用更少空间，但不能复制链接等操作。',\n    baWidth: '弹窗宽度',\n    baWidth_help: '右上弹框面板宽度。若为负数则取查词面板的宽度。',\n    baHeight: '弹窗高度',\n    baHeight_help: '右上弹框面板高度。',\n    baOpen: '点击地址栏旁图标',\n    baOpen_help:\n      '点击地址栏旁 Saladict 图标时发生的操作。沿用了「右键菜单」的项目，可以前往该设置页面进行增加或编辑。',\n    tripleCtrl: '启用 Ctrl 快捷键',\n    tripleCtrl_help:\n      '连续按三次{⌘ Command}（Mac）或者{Ctrl}（其它键盘）（或设置浏览器快捷键）将弹出词典界面。',\n    qsLocation: '出现位置',\n    qsFocus: '出现时获取焦点',\n    qsStandalone: '独立窗口',\n    qsStandalone_help: '显示为单独的窗口，支持响应{浏览器以外划词}。',\n    qssaSidebar: '类侧边栏',\n    qssaSidebar_help: '并排显示窗口以达到类似侧边栏的布局。',\n    qssaHeight: '窗口高度',\n    qssaPageSel: '响应划词',\n    qssaPageSel_help: '响应网页划词。',\n    qssaRectMemo: '记住位置与大小',\n    qssaRectMemo_help: '独立窗口关闭时记住位置与大小。',\n    updateCheck: '检查更新',\n    updateCheck_help: '自动检查更新',\n    analytics: '启用 Google Analytics',\n    analytics_help:\n      '提供匿名设备浏览器版本信息。因精力有限，沙拉查词作者会尽可能支持用户量更多的设备和浏览器。',\n\n    opt: {\n      reset: '重置设定',\n      reset_confirm: '所有设定将还原到默认值，确定？',\n      upload_error: '设置保存失败',\n      accent: {\n        uk: '英式',\n        us: '美式'\n      },\n      sel_blackwhitelist: '划词黑白名单',\n      sel_blackwhitelist_help: '黑名单匹配的页面 Saladict 将不会响应鼠标划词。',\n      pdf_blackwhitelist_help:\n        '黑名单匹配的 PDF 链接将不会跳转到 Saladict 打开。',\n      contextMenus_description:\n        '设置右键菜单，可添加可自定义链接。网页翻译其实不需要沙拉查词，故已有的有道和谷歌网页翻译目前处于维护状态，没有计划添加新功能，请用其它官方扩展如彩云小译和谷歌翻译。',\n      contextMenus_edit: '编辑右键菜单项目',\n      contextMenus_url_rules: '链接中的 %s 会被替换为选词。',\n      baOpen: {\n        popup_panel: '显示查词面板',\n        popup_fav: '添加选词到生词本',\n        popup_options: '打开 Saladict 设置',\n        popup_standalone: '打开快捷查词独立窗口'\n      },\n      openQsStandalone: '打开独立窗口设置',\n      pdfStandalone: {\n        default: '从不',\n        always: '总是',\n        manual: '手动'\n      }\n    }\n  },\n\n  matchPattern: {\n    description: '网址支持{超链匹配}和{正则匹配}。留空保存即可清除。',\n    url: '超链匹配',\n    url_error: '不正确的超链接模式表达式。',\n    regex: '正则匹配',\n    regex_error: '不正确的正则表达式。'\n  },\n\n  searchMode: {\n    icon: '显示图标',\n    icon_help: '在鼠标附近显示一个图标，鼠标移上去后才显示词典面板。',\n    direct: '直接搜索',\n    direct_help: '直接显示词典面板。',\n    double: '双击搜索',\n    double_help: '双击选择文本之后直接显示词典面板。',\n    holding: '按住按键',\n    holding_help:\n      '在放开鼠标之前按住选择的按键才显示词典面板（Alt 为 macOS 上的 \"⌥ Option\"键。 Meta 键为 macOS 上的「⌘ Command」键以及其它键盘的「⊞ Windows」键）。',\n    instant: '鼠标悬浮取词',\n    instant_help: '自动选取鼠标附近的单词。',\n    instantDirect: '直接取词',\n    instantKey: '按键',\n    instantKey_help:\n      '因技术限制，悬浮取词通过自动选择鼠标附近单词实现，不设置按键直接取词可导致无法选词，建议配合快捷键开启关闭。',\n    instantDelay: '取词延时'\n  },\n\n  profiles: {\n    opt: {\n      add_name: '新增情景模式名称',\n      delete_confirm: '「{{name}}」将被删除，确认？',\n      edit_name: '更改情景模式名称',\n      help:\n        '每个情景模式相当于一套独立的设置，一些选项（带 {*}）会随着情景模式变化。鼠标悬浮在查词面板的菜单图标上可快速切换，或者焦点选中菜单图标然后按{↓}。'\n    }\n  },\n\n  profile: {\n    mtaAutoUnfold: '自动展开多行搜索框',\n    waveform: '波形控制按钮',\n    waveform_help:\n      '在词典面板下方显示音频控制面板展开按钮。控制面板只會在展開時才載入。',\n    stickyFold: '记忆折叠',\n    stickyFold_help:\n      '查词时记住之前手动展开与折叠词典的状态，仅在同个页面生效。',\n\n    opt: {\n      item_extra: '此选项会因「情景模式」而改变。',\n      mtaAutoUnfold: {\n        always: '保持展开',\n        never: '从不展开',\n        once: '展开一次',\n        popup: '只在右上弹框展开',\n        hide: '隐藏'\n      },\n      dict_selected: '已选词典'\n    }\n  },\n\n  dict: {\n    add: '添加词典',\n    more_options: '更多设置',\n\n    selectionLang: '划词语言',\n    selectionLang_help: '当选中的文字包含相应的语言时才显示该词典。',\n    defaultUnfold: '默认展开',\n    defaultUnfold_help:\n      '关闭后该词典将不会自动搜索，除非点击「展开」箭头。适合一些需要时再深入了解的词典，以加快初次查词速度。',\n    selectionWC: '划词字数',\n    selectionWC_help:\n      '当选中文字的字数符合条件时才显示该词典。可设置 999999 如果不希望限制字数。',\n    preferredHeight: '词典默认高度',\n    preferredHeight_help:\n      '词典初次出现的最大高度。超出此高度的内容将被隐藏并显示下箭头。可设置 999999 如果不希望限制高度。',\n\n    lang: {\n      de: '德',\n      en: '英',\n      es: '西',\n      fr: '法',\n      ja: '日',\n      kor: '韩',\n      zhs: '简',\n      zht: '繁'\n    }\n  },\n\n  syncService: {\n    description: '数据同步设置。',\n    start: '同步进行中，结束前请勿关闭此页面。',\n    finished: '同步结束',\n    success: '同步成功',\n    failed: '同步失败',\n    close_confirm: '设置未保存，关闭？',\n    delete_confirm: '清空同步设置？',\n\n    shanbay: {\n      description:\n        '先去 shanbay.com 登录扇贝（退出后将失效）。开启后每次添加生词将自动单向同步到扇贝生词本（只从沙拉查词到扇贝），只同步新增单词（删除不同步），只同步单词本身（上下文等均不能同步）。生词需要扇贝单词库支持才能被添加。',\n      login: '将打开扇贝官网，请登录再回来重新开启。',\n      sync_all: '上传现有的所有生词',\n      sync_all_confirm:\n        '生词本存在较多单词，将分批上传。注意短时间上传太多有可能会导致封号，且不可恢复，确定继续？',\n      sync_last: '上传最近的一个生词'\n    },\n\n    eudic: {\n      description:\n        '使用欧路词典同步单词前，必须先在欧路官网（my.eudic.net/home/index）创建默认生词本（一般初次手动导入会自动生成且无法删除）。注意短时间内不要频繁同步，可能会造成暂时封停。',\n      token: '授权信息',\n      getToken: '获取授权',\n      verify: '检查 授权信息',\n      verified: '成功检查 欧路授权信息',\n      enable_help:\n        '开启后每次添加生词将自动单向同步到欧路默认生词本（salad到欧路生词本），只同步新增单词本身（删除不同步）',\n      token_help:\n        '请确认设置有效的个人授权信息，否则将同步失败。可点击底部按钮检查。',\n      sync_all: '同步全部生词',\n      sync_help:\n        '将salad单词本中现有的所有生词，同步到欧路词典默认生词本中（需同时开启上方同步开关，点击保存）',\n      sync_all_confirm:\n        '注意短时间内频繁同步有可能会导致接下来一小段时间的封停，确定继续？'\n    },\n\n    webdav: {\n      description:\n        '应用设置（包括本设置）已通过浏览器自动同步。生词本可通过本设置实现 WebDAV 同步。',\n      jianguo: '参考坚果云设置',\n      checking: '连接中...',\n      exist_confirm: '服务器上已存在 Saladict 目录。是否下载合并到本地？',\n      upload_confirm: '马上上传本地数据到服务器？',\n      verify: '验证服务器',\n      verified: '成功验证服务器',\n      duration: '同步周期',\n      duration_help:\n        '添加生词后会马上上传，数据会在上传前保证同步，所以如果不需要多个浏览器实时查看更新，可将更新检测周期调大些以减少资源占用及避免服务器拒绝响应。',\n      passwd: '密码',\n      url: '服务器地址',\n      user: '账户'\n    },\n\n    ankiconnect: {\n      description: '请确保 Anki Connect 插件已安装且 Anki 在后台运行。',\n      checking: '连接中...',\n      deck_confirm: '牌组「{{deck}}」不存在 Anki 中，是否自动添加？',\n      deck_error: '无法创建牌组「{{deck}}」。',\n      notetype_confirm:\n        '笔记类型「{{noteType}}」不存在 Anki 中，是否自动添加？',\n      notetype_error: '无法创建笔记类型「{{noteType}}」。',\n      upload_confirm:\n        '马上同步本地生词到 Anki？重复的单词（相同“Date”）会被跳过。',\n      add_yourself: '请在 Anki 中自行添加。',\n      verify: '检查 Anki Connect',\n      verified: '成功检查 Anki Connect',\n      enable_help:\n        '开启后每次保存新生词都会自动同步到 Anki。Anki 上已存在的单词（以“Date”为准）可以在单词编辑器中编辑强制更新覆盖到 Anki。',\n      host: '地址',\n      port: '端口',\n      key: 'Key',\n      key_help: '可在 Anki Connect 插件中设置 key 以做简单令牌。',\n      deckName: '牌组',\n      deckName_help:\n        '如果不存在的话可以点下方「检查 Anki Connect」让本设置生成默认牌组。',\n      noteType: '笔记类型',\n      noteType_help:\n        'Anki 笔记类型包括一套字段和卡片类型。如果不存在的话可以点下方「检查 Anki Connect」让本设置生成一套默认的笔记类型。如需自行在 Anki 添加或修改卡片模板请不要更改字段名字。',\n      tags: '标签',\n      tags_help: 'Anki 笔记可以附带标签。以逗号分割。',\n      escapeHTML: '转义 HTML',\n      escapeHTML_help:\n        '对笔记内容中的 HTML 字符进行转义。如手动进行 HTML 排版请关闭选项。',\n      syncServer: '同步服务器',\n      syncServer_help: '单词添加到本地 Anki 后自动同步到服务器（如 AnkiWeb）。'\n    }\n  },\n\n  titlebarOffset: {\n    title: '校准标题栏高度',\n    help:\n      '不同的系统以及不同的浏览器设置会影响标题栏高度，沙拉查词会尝试自动校准，如弹出窗口依然出现偏移可自行调整。',\n    main: '普通窗口',\n    main_help: '普通窗口可能没有标题栏。',\n    panel: '简化窗口',\n    panel_help: '沙拉查词的独立窗口快捷查词面板为简化窗口。',\n    calibrate: '自动校准',\n    calibrateSuccess: '自动校准成功',\n    calibrateError: '自动校准失败'\n  },\n\n  headInfo: {\n    acknowledgement: {\n      title: '特别鸣谢',\n      yipanhuasheng:\n        '添加韦氏词典、美国传统词典、牛津学习词典与欧路生词同步；更新 Urban 词典与 Naver 词典',\n      naver: '协助添加 Naver 韩国语词典',\n      shanbay: '编写扇贝词典模块',\n      trans_tw: '提供部分繁体中文翻译',\n      weblio: '协助添加 Weblio 辞書'\n    },\n    contact_author: '联系作者',\n    donate: '支持项目',\n    instructions: '使用说明',\n    report_issue: '反馈问题'\n  },\n\n  form: {\n    url_error: '不正确的超链接格式。',\n    number_error: '不正确的数字'\n  },\n\n  preload: {\n    title: '预先加载',\n    auto: '自动查词',\n    auto_help: '查词面板出现时自动搜索预加载内容。',\n    clipboard: '剪贴板',\n    help: '查词面板出现时预先加载内容到搜索框。',\n    selection: '页面划词'\n  },\n\n  locations: {\n    CENTER: '居中',\n    TOP: '上方',\n    RIGHT: '右方',\n    BOTTOM: '下方',\n    LEFT: '左方',\n    TOP_LEFT: '左上',\n    TOP_RIGHT: '右上',\n    BOTTOM_LEFT: '左下',\n    BOTTOM_RIGHT: '右下'\n  },\n\n  import_export_help:\n    '设定已通过浏览器自动同步，也可以手动导入导出。备份为明文保存，对安全性有要求的请自行加密。',\n\n  import: {\n    title: '导入设定',\n    error: {\n      title: '导入失败',\n      parse: '备份解析失败，格式不正确。',\n      load: '备份加载失败，浏览器无法获得本地备份。',\n      empty: '备份中没有发现有效数据。'\n    }\n  },\n\n  export: {\n    title: '导出设定',\n    error: {\n      title: '导出失败',\n      empty: '没有设置可以导出。',\n      parse: '设置解析失败，无法导出。'\n    }\n  },\n\n  dictAuth: {\n    description:\n      '随着沙拉查词用户增多，如经常使用机器翻译，建议到官网申请帐号以获得更稳定的体验以及更准确的结果。以下帐号数据只会保留在浏览器中。',\n    dictHelp: '见{词典}官网。',\n    manage: '管理私用帐号'\n  },\n\n  third_party_privacy: '第三方隐私',\n  third_party_privacy_help:\n    '沙拉查词不会收集更多数据，但在查词时单词以及相关 cookies 数据会发送给第三方词典服务（与在该网站上查词一样），如果你不希望被该服务获取数据，请在「词典设置」中关闭相应词典。',\n  third_party_privacy_extra: '本特性为沙拉查词核心功能，无法关闭。',\n\n  permissions: {\n    success: '申请权限成功',\n    cancel_success: '取消权限成功',\n    failed: '申请权限失败',\n    cancelled: '申请权限被用户取消',\n    missing: '缺少权限「{{permission}}」。请给予权限或者关闭相关功能。',\n    clipboardRead: '读取剪贴板',\n    clipboardRead_help:\n      '快捷查词或者右上弹框设置预加载剪贴板时需要读取剪贴板权限。',\n    clipboardWrite: '写入剪贴板',\n    clipboardWrite_help:\n      '机器翻译词典标题栏菜单复制原文译文或生词本导出到剪贴板需要写入剪贴板权限。'\n  },\n\n  unsupportedFeatures: {\n    ff: '火狐尚不支持「{{feature}}」功能。'\n  }\n}\n"
  },
  {
    "path": "src/_locales/zh-CN/popup.ts",
    "content": "export const locale = {\n  title: '沙拉查词-右上弹框',\n  app_active_title: '启用划词',\n  app_temp_active_title: '对当前页暂时关闭划词',\n  instant_capture_pinned: '（钉住）',\n  instant_capture_title: '开启鼠标悬浮取词',\n  notebook_added: '已添加',\n  notebook_empty: '当前页面没有发现选词',\n  notebook_error: '无法添加选词到生词本',\n  page_no_response: '页面无响应',\n  qrcode_title: '当前页面二维码'\n}\n"
  },
  {
    "path": "src/_locales/zh-CN/wordpage.ts",
    "content": "export const locale = {\n  title: {\n    history: '沙拉查词-查词记录',\n    notebook: '沙拉查词-生词本'\n  },\n\n  localonly: '仅本地保存',\n\n  column: {\n    add: '添加',\n    date: '日期',\n    edit: '编辑',\n    note: '笔记',\n    source: '来源',\n    trans: '翻译',\n    word: '单词'\n  },\n\n  delete: {\n    title: '删除单词',\n    all: '删除所有单词',\n    confirm: '，确定？',\n    page: '删除本页单词',\n    selected: '删除选中单词'\n  },\n\n  export: {\n    title: '导出文本',\n    all: '导出所有单词',\n    description: '编写生成模板，描述每条记录生成的样子：',\n    explain: '如何配合 ANKI 等工具',\n    gencontent: '代表的内容',\n    linebreak: {\n      default: '保留换行',\n      n: '换行替换为 \\\\n',\n      br: '换行替换为 <br>',\n      p: '换行替换为 <p>',\n      space: '换行替换为空格'\n    },\n    page: '导出本页单词',\n    placeholder: '替换符',\n    htmlescape: {\n      title: '对笔记内容中的 HTML 字符进行转义',\n      text: '转义 HTML'\n    },\n    selected: '导出选中单词'\n  },\n\n  filterWord: {\n    chs: '中文',\n    eng: '英文',\n    word: '单词',\n    phrase: '词组和句子'\n  },\n\n  wordCount: {\n    selected: '已选 {{count}} 项',\n    selected_plural: '已选 {{count}} 项',\n    total: '共 {{count}} 项',\n    total_plural: '共 {{count}} 项'\n  }\n}\n"
  },
  {
    "path": "src/_locales/zh-TW/background.ts",
    "content": "import { locale as _locale } from '../zh-CN/background'\n\nexport const locale: typeof _locale = {\n  app: {\n    off: '沙拉查詞已關閉（快捷查詞依然可用）',\n    tempOff: '沙拉查詞已對當前標籤關閉（快捷查詞依然可用）',\n    unsupported: '內嵌查字介面不支援此類頁面（獨立視窗查字介面依然可用）'\n  }\n}\n"
  },
  {
    "path": "src/_locales/zh-TW/common.ts",
    "content": "import { locale as _locale } from '../zh-CN/common'\n\nexport const locale: typeof _locale = {\n  add: '新增',\n  delete: '删除',\n  save: '保存',\n  cancel: '取消',\n\n  edit: '編輯',\n  sort: '排序',\n  rename: '重新命名',\n\n  confirm: '確認',\n  changes_confirm: '變更未儲存。確定關閉？',\n  delete_confirm: '確定完全刪除該條目？',\n\n  max: '最大',\n  min: '最小',\n\n  name: '名稱',\n  none: '無',\n\n  enable: '開啟',\n  enabled: '已開啟',\n  disabled: '已關閉',\n\n  blacklist: '黑名單',\n  whitelist: '白名單',\n\n  import: '匯入',\n  export: '匯出',\n\n  lang: {\n    chinese: '漢字',\n    chs: '漢字',\n    deutsch: '德文',\n    eng: '英文',\n    english: '英文',\n    french: '法文',\n    japanese: '日文',\n    korean: '韓文',\n    minor: '其它語言',\n    matchAll: '所有的字元都必須匹配',\n    others: '其它字元',\n    spanish: '西班牙文'\n  },\n\n  unit: {\n    mins: '分鐘',\n    ms: '毫秒',\n    s: '秒',\n    word: '个'\n  },\n\n  note: {\n    word: '單字',\n    trans: '翻譯',\n    note: '筆記',\n    context: '上下文',\n    contextCloze: '上下文填空',\n    date: '日期',\n    srcTitle: '來源標題',\n    srcLink: '來源連結',\n    srcFavicon: '來源圖示'\n  },\n\n  profile: {\n    daily: '日常模式',\n    sentence: '句庫模式',\n    default: '預設模式',\n    scholar: '學術模式',\n    translation: '翻譯模式',\n    nihongo: '日語模式'\n  }\n}\n"
  },
  {
    "path": "src/_locales/zh-TW/content.ts",
    "content": "import { locale as _locale } from '../zh-CN/content'\n\nexport const locale: typeof _locale = {\n  chooseLang: '-選擇其它語言-',\n  standalone: '沙拉查詞-獨立查詞視窗',\n  fetchLangList: '取得全部語言清單',\n  transContext: '重新翻譯',\n  neverShow: '不再彈出',\n  fromSaladict: '来自沙拉查詞介面',\n  tip: {\n    historyBack: '上一個查單字記錄',\n    historyNext: '下一個查單字記錄',\n    searchText: '查單字',\n    openOptions: '開啟設定',\n    addToNotebook: '儲存單字到單字本，右点击開啟單字本',\n    openNotebook: '開啟單字本',\n    openHistory: '開啟查單字記錄',\n    shareImg: '以圖片方式分享查單字結果',\n    pinPanel: '釘選字典視窗',\n    closePanel: '關閉字典視窗',\n    sidebar: '切換側邊欄模式，右點選切換右側',\n    focusPanel: '查詞時面板獲取焦點',\n    unfocusPanel: '查詞時面板不獲取焦點'\n  },\n  wordEditor: {\n    title: '儲存到單字本',\n    wordCardsTitle: '單字本其它記錄',\n    deleteConfirm: '從單字本中移除？',\n    closeConfirm: '記錄尚未儲存，確定關閉？',\n    chooseCtxTitle: '選擇翻譯結果',\n    ctxHelp:\n      '如需相容選擇翻譯結果以及 Anki 生成表格請保持 [:: xxx ::] 和 --------------- 格式。'\n  },\n  machineTrans: {\n    switch: '變更語言',\n    sl: '來源語言',\n    tl: '目標語言',\n    auto: '偵測語言',\n    stext: '原文',\n    showSl: '顯示原文',\n    copySrc: '複製原文',\n    copyTrans: '複製譯文',\n    login: '請登入{詞典帳號}以使用。',\n    dictAccount: '詞典帳號'\n  },\n  updateAnki: {\n    title: '更新到 Anki',\n    success: '更新到 Anki 成功。',\n    failed: '更新單詞到 Anki 失敗。'\n  }\n}\n"
  },
  {
    "path": "src/_locales/zh-TW/langcode.ts",
    "content": "import zhTW from '@opentranslate/languages/locales/zh-TW.json'\nimport { locale as _locale } from '../zh-CN/langcode'\n\nexport const locale: typeof _locale = {\n  ...zhTW,\n  default: '同介面語言',\n  ara: '阿拉伯語',\n  'bs-Latn': '波斯尼亞語',\n  bul: '保加利亞語',\n  cht: '中文（繁體）',\n  dan: '丹麥語',\n  est: '愛沙尼亞語',\n  fin: '芬蘭語',\n  fra: '法語',\n  iw: '希伯來語',\n  jp: '日語',\n  kor: '韓語',\n  kr: '韓語',\n  pt_BR: '巴西語',\n  rom: '羅馬尼亞語',\n  slo: '斯洛維尼亞語',\n  spa: '西班牙語',\n  swe: '瑞典語',\n  tl: '他加祿語（菲律賓語）',\n  vie: '越南語',\n  zh: '中文（簡體）',\n  'zh-CHS': '中文（簡體）',\n  'zh-CHT': '中文（繁體）'\n}\n"
  },
  {
    "path": "src/_locales/zh-TW/menus.ts",
    "content": "import { locale as _locale } from '../zh-CN/menus'\n\nexport const locale: typeof _locale = {\n  baidu_page_translate: '百度網頁翻譯',\n  baidu_search: '百度搜尋',\n  bing_dict: 'Bing 字典',\n  bing_search: 'Bing 搜尋',\n  caiyuntrs: '彩雲小譯網頁翻譯',\n  cambridge: '劍橋字典',\n  copy_pdf_url: '複製PDF連結到剪貼簿',\n  dictcn: '海詞字典',\n  etymonline: '培根字根',\n  google_cn_page_translate: 'Google cn 網頁翻譯',\n  google_page_translate: 'Google 網頁翻譯',\n  google_search: 'Google 搜尋',\n  google_translate: 'Google 翻譯',\n  google_cn_translate: 'Google.cn 翻譯',\n  guoyu: '國語字典',\n  history_title: '查單字歷史記錄',\n  iciba: '金山詞霸',\n  liangan: '兩岸字典',\n  longman_business: '朗文商務',\n  manual_title: '詳細使用說明',\n  merriam_webster: '韋氏字典',\n  microsoft_page_translate: '微軟網頁翻譯',\n  notebook_title: '生字本',\n  notification_youdao_err:\n    '有道網頁翻譯2.0 下載後無回應，\\n可能是套件無權造訪該網站，\\n如果下載成功後，請忽略本訊息。',\n  oxford: '牛津字典',\n  page_permission_err: '沙拉查詞「{{name}}」無權訪問此頁面。',\n  page_translations: '網頁翻譯',\n  saladict: '沙拉查詞',\n  saladict_standalone: '沙拉查詞獨立視窗',\n  sogou: '搜狗翻譯',\n  sogou_page_translate: '搜狗網頁翻譯',\n  termonline: '術語在線',\n  view_as_pdf: '在 PDF 閱讀器中開啟',\n  youdao: '有道字典',\n  youdao_page_translate: '有道網頁翻譯',\n  youglish: 'YouGlish'\n}\n"
  },
  {
    "path": "src/_locales/zh-TW/options.ts",
    "content": "import { locale as _locale } from '../zh-CN/options'\n\nexport const locale: typeof _locale = {\n  title: '沙拉查詞設定',\n  previewPanel: '預覽字典介面',\n  shortcuts: '設定快速鍵',\n  msg_update_error: '設定更新失敗',\n  msg_updated: '設定已更新',\n  msg_first_time_notice: '初次使用注意',\n  msg_err_permission: '許可權“{{permission}}”申請失敗。',\n  unsave_confirm: '修改尚未儲存，確定放棄？',\n  nativeSearch: '瀏覽器外選字翻譯',\n  firefox_shortcuts:\n    '位址列跳轉到 about:addons，點選右上方的齒輪，選擇最後一項管理擴充套件快捷鍵',\n  tutorial: '教程',\n  page_selection: '網頁選字',\n\n  nav: {\n    General: '基本選項',\n    Notebook: '單字管理',\n    Profiles: '情景模式',\n    DictPanel: '字典介面',\n    SearchModes: '查字習慣',\n    Dictionaries: '字典設定',\n    DictAuths: '詞典帳號',\n    Popup: '右上彈出式視窗',\n    QuickSearch: '迅速查字',\n    Pronunciation: '朗讀設定',\n    PDF: 'PDF 設定',\n    ContextMenus: '右鍵選單',\n    BlackWhiteList: '黑白名單',\n    ImportExport: '匯入匯出',\n    Privacy: '隱私設定',\n    Permissions: '許可權管理'\n  },\n\n  config: {\n    active: '啟用滑鼠選字翻譯',\n    active_help: '關閉後「迅速查字」功能依然可用。',\n    animation: '啟用轉換動畫',\n    animation_help: '在低效能裝置上關閉過渡動畫可減少渲染負擔。',\n    runInBg: '保持瀏覽器執行',\n    runInBg_help:\n      '讓瀏覽器關閉後依然保持執行，從而繼續響應快捷鍵以及瀏覽器外劃字（見右上角官網使用說明）。',\n    darkMode: '黑暗模式',\n    langCode: '介面語言',\n    editOnFav: '紅心單字時彈出編輯介面',\n    editOnFav_help:\n      '關閉後，點選紅心生詞將自動新增到生詞本，上下文翻譯亦會自動獲取。',\n    searchHistory: '記錄查字歷史',\n    searchHistory_help: '查字典記錄可能會泄漏您的瀏覽痕跡。',\n    searchHistoryInco: '在無痕模式中記錄',\n    ctxTrans: '上下文翻譯引擎',\n    ctxTrans_help: '單字加入生字本前會自動翻譯上下文。',\n    searchSuggests: '輸入時顯示候選',\n    panelMaxHeightRatio: '字典介面最高占螢幕高度比例',\n    panelWidth: '查字典介面寬度',\n    fontSize: '字典內容字型大小',\n    bowlOffsetX: '沙拉圖示水平偏移',\n    bowlOffsetY: '沙拉圖示垂直偏移',\n    panelCSS: '自訂查字介面樣式',\n    panelCSS_help:\n      '為查詞面板新增自定義 CSS 。詞典面板使用 .dictPanel-Root 作為根，詞典使用 .dictRoot 或者 .d-詞典ID 作為根。',\n    noTypeField: '不在輸入框滑鼠滑字',\n    noTypeField_help:\n      '開啟後，本程式會自動識別輸入框以及常見編輯器，如 CodeMirror、ACE 和 Monaco。',\n    touchMode: '觸控模式',\n    touchMode_help: '支援觸控相關選字',\n    language: '選詞語言',\n    language_help: '當選取的文字包含相對應的語言時，才進行尋找。',\n    language_extra:\n      '注意日語與韓語也包含了漢字。法語、德語和西語也包含了英文。若取消了中文或英語而勾選了其它語言，則只翻譯那些語言獨有的部分，如日語只翻譯假名。',\n    doubleClickDelay: '滑鼠按兩下間隔',\n    mode: '普通選字',\n    panelMode: '字典視窗介面內部選字',\n    defaultPinned: '出現時釘住面板',\n    pinMode: '字典視窗介面釘住后選字',\n    qsPanelMode: '獨立字典視窗介面響應頁面選字',\n    bowlHover: '圖示暫留查字',\n    bowlHover_help: '滑鼠暫留在沙拉圖示上開啟字典介面，否則需要點選。',\n    autopron: {\n      cn: {\n        dict: '中文自動發音'\n      },\n      en: {\n        dict: '英文自動發音',\n        accent: '優先口音'\n      },\n      machine: {\n        dict: '機器自動發音',\n        src: '機器發音部分',\n        src_help: '機器翻譯字典需要在下方新增並啟用才會自動發音。',\n        src_search: '朗讀原文',\n        src_trans: '朗讀翻譯'\n      }\n    },\n    pdfSniff: '嗅探 PDF 連結',\n    pdfSniff_help:\n      '開啟後所有 PDF 連結將自動跳至本套件開啟（包括本機，如果在套件管理頁面勾選了允許）。',\n    pdfSniff_extra:\n      '現在更推薦使用自己喜歡的本地閱讀器搭配{瀏覽器外選字翻譯}。',\n    pdfStandalone: '獨立視窗',\n    pdfStandalone_help:\n      '在獨立視窗中開啟 PDF 閱讀器。獨立視窗只有標題欄，佔用更少空間，但不能複製連結等操作。',\n    baWidth: '彈窗寬度',\n    baWidth_help: '右上彈框面板寬度。若為負數則取查字介面的寬度。',\n    baHeight: '彈窗高度',\n    baHeight_help: '右上彈框面板高度。',\n    baOpen: '點選網址列旁圖示',\n    baOpen_help:\n      '點選網址列旁 Saladict 圖示時發生的操作。沿用了「右鍵選單」的條目，可以前往該設定頁面增加或編輯。',\n    tripleCtrl: '啟用 Ctrl 快速鍵',\n    tripleCtrl_help:\n      '連續按三次{⌘ Command}（macOS）或者{Ctrl}（其它鍵盤）（或設定瀏覽器快速鍵），將會彈出字典視窗介面。',\n    qsLocation: '出現位置',\n    qsFocus: '出現時獲取焦點',\n    qsStandalone: '獨立視窗',\n    qsStandalone_help: '顯示為獨立的視窗，支援{瀏覽器外選字翻譯}。',\n    qssaSidebar: '類側邊欄',\n    qssaSidebar_help: '並排顯示視窗以達到類似側邊欄的配置。',\n    qssaHeight: '視窗高度',\n    qssaPageSel: '響應滑字',\n    qssaPageSel_help: '對網頁滑鼠滑字作出反應。',\n    qssaRectMemo: '記住位置和大小',\n    qssaRectMemo_help: '獨立視窗關閉時記住位置和大小。',\n    updateCheck: '檢查更新',\n    updateCheck_help: '自動檢查更新',\n    analytics: '啟用 Google Analytics',\n    analytics_help:\n      '提供匿名裝置瀏覽器版本資訊。因精力有限，沙拉查詞作者會盡可能支援使用者量更多的裝置和瀏覽器。',\n\n    opt: {\n      reset: '重設設定',\n      reset_confirm: '所有設定將還原至預設值，確定？',\n      upload_error: '設定儲存失敗',\n      accent: {\n        uk: '英式',\n        us: '美式'\n      },\n      sel_blackwhitelist: '選詞黑白名單',\n      sel_blackwhitelist_help: '黑名單相符的頁面 Saladict 將不會響應滑鼠劃詞。',\n      pdf_blackwhitelist_help:\n        '黑名單相符的 PDF 連結將不會跳至 Saladict 開啟。',\n      contextMenus_description:\n        '設定右鍵選單，可新增可自定義連結。網頁翻譯其實不需要沙拉查詞，故已有的有道和谷歌網頁翻譯目前處於維護狀態，沒有計劃新增新功能，請用其它官方擴充套件如彩雲小譯和谷歌翻譯。',\n      contextMenus_edit: '編輯右鍵選單項目',\n      contextMenus_url_rules: '連結中的 %s 會被取代為選詞。',\n      baOpen: {\n        popup_panel: '開啟字典介面',\n        popup_fav: '新增選詞到生字本',\n        popup_options: '進入 Saladict 設定',\n        popup_standalone: '開啟快捷查詞獨立視窗'\n      },\n      openQsStandalone: '獨立視窗設定',\n      pdfStandalone: {\n        default: '從不',\n        always: '總是',\n        manual: '手動'\n      }\n    }\n  },\n\n  matchPattern: {\n    description: '網址支援{超鏈匹配}和{正則匹配}。留空儲存即可清除。',\n    url: '連結匹配',\n    url_error: '不正確的超連結模式匹配表示式。',\n    regex: '正則匹配',\n    regex_error: '不正確的正則表示式。'\n  },\n\n  searchMode: {\n    icon: '顯示圖示',\n    icon_help:\n      '在滑鼠附近顯示一個圖示，滑鼠移動到圖示後，會顯示出字典的視窗介面。',\n    direct: '直接搜尋',\n    direct_help: '直接顯示字典視窗介面。',\n    double: '滑鼠按兩下',\n    double_help: '滑鼠按兩下所選擇的句子或單字後，會直接顯示字典視窗介面。',\n    holding: '按住按键',\n    holding_help:\n      '在放開滑鼠之前，需按住選擇的按鍵才顯示字典視窗介面（Alt 為 macOS 上的 \"⌥ Option\"鍵。Meta 鍵為 macOS 上的「⌘ Command」鍵以及其它鍵盤的「⊞ Windows」鍵）。',\n    instant: '滑鼠懸浮取詞',\n    instant_help: '自動選取滑鼠附近的單字。',\n    instantDirect: '直接取詞',\n    instantKey: '按鍵',\n    instantKey_help:\n      '因技術限制，懸浮取詞通過自動選擇滑鼠附近單字實現，不設定按鍵直接取詞可能導致滑鼠無法選字，建議配合快速鍵開啟關閉。',\n    instantDelay: '取詞等待'\n  },\n\n  profiles: {\n    opt: {\n      add_name: '新增情景模式名稱',\n      delete_confirm: '「{{name}}」將被刪除，確認？',\n      edit_name: '變更情景模式名稱',\n      help:\n        '每個情景模式相當於一套獨立的設定，一些選項（帶有 {*}）會隨著情景模式變化。滑鼠懸浮在字典介面的選單圖示上可快速切換，或者焦點選中選單圖示然後按{↓}。'\n    }\n  },\n\n  profile: {\n    mtaAutoUnfold: '自動展開多行搜尋框',\n    waveform: '波形控制',\n    waveform_help:\n      '在字典介面下方顯示音訊控制面板展開按鈕。關閉依然可以播放音訊。',\n    stickyFold: '記憶摺疊',\n    stickyFold_help:\n      '查字時記住之前手動展開和收起字典的狀態，只在同個頁面生效。',\n\n    opt: {\n      item_extra: '此選項會因「情景模式」而改變。',\n      mtaAutoUnfold: {\n        always: '保持展開',\n        never: '永遠不展開',\n        once: '展開一次',\n        popup: '只在右上彈框展開',\n        hide: '隱藏'\n      },\n      dict_selected: '已選字典'\n    }\n  },\n\n  dict: {\n    add: '新增字典',\n    more_options: '更多設定',\n\n    selectionLang: '選詞語言',\n    selectionLang_help: '當選中的文字包含相對應的語言時才顯示這個字典。',\n    defaultUnfold: '自動展開',\n    defaultUnfold_help:\n      '關閉後此字典將不會自動搜尋，除非點選「展開」箭頭。適合一些需要時再深入瞭解的字典，以加快初次查字典速度。',\n    selectionWC: '選詞字數',\n    selectionWC_help:\n      '當選中文字的字數符合條件時才顯示該詞典。可設定 999999 如果不希望限制字數。',\n    preferredHeight: '字典預設高度',\n    preferredHeight_help:\n      '字典初次出現的最大高度。超出此高度的內容將被隱藏並顯示下箭頭。可設定 999999 如果不希望限制高度。',\n\n    lang: {\n      de: '德',\n      en: '英',\n      es: '西',\n      fr: '法',\n      ja: '日',\n      kor: '韓',\n      zhs: '简',\n      zht: '繁'\n    }\n  },\n\n  syncService: {\n    description: '資料同步設定。',\n    start: '同步進行中，結束前請勿關閉此頁面。',\n    finished: '同步結束',\n    success: '同步成功',\n    failed: '同步失敗',\n    close_confirm: '設定未儲存，關閉？',\n    delete_confirm: '清空同步設定？',\n\n    shanbay: {\n      description:\n        '先去 shanbay.com 登入扇貝（退出後將失效）。開啟後將單向同步到扇貝生詞本（只從沙拉查詞到扇貝），只同步新增單詞（刪除不同步），只同步單詞本身（上下文等均不能同步）。生詞需要扇貝單詞庫支援才能被新增。',\n      login: '將開啟扇貝官網，請登入再回來重新開啟。',\n      sync_all: '上傳現有的所有生字',\n      sync_all_confirm:\n        '生詞本存在較多單詞，將分批上傳。注意短時間上傳太多有可能會導致封號，且不可恢復，確定繼續？',\n      sync_last: '上傳最近的一個生字'\n    },\n\n    eudic: {\n      description:\n        '使用歐路詞典同步單詞前，必須先在歐路官網（my.eudic.net/home/index）創建默認生詞本（一般初次手動導入會自動生成且無法删除）。注意短時間內不要頻繁同步，可能會造成暫時封停。',\n      token: '授權資訊',\n      getToken: '獲取授權',\n      verify: '檢查 授權資訊',\n      verified: '成功檢查 歐路授權資訊',\n      enable_help:\n        '開啟後每次添加生詞將自動單向同步到歐路默認生詞本（salad到歐路生詞本），只同步新增單詞本身（删除不同步）',\n      token_help:\n        '請確認設定有效的個人授權資訊，否則將同步失敗。可點擊底部按鈕檢查。',\n      sync_all: '同步全部生詞',\n      sync_help:\n        '將salad單詞本中現有的所有生詞，同步到歐路詞典默認生詞本中（需同時開啟上方同步開關，點擊保存）',\n      sync_all_confirm:\n        '注意短時間內頻繁同步有可能會導致接下來一小段時間的封停，確定繼續？'\n    },\n\n    webdav: {\n      description:\n        '應用設定（包括本設定）已通過瀏覽器自動同步。生詞本可通過本設定實現 WebDAV 同步。',\n      jianguo: '參考堅果雲設定',\n      checking: '連線中...',\n      exist_confirm: '伺服器上已存在 Saladict 目錄。是否下載合併到本地？',\n      upload_confirm: '馬上上傳本地資料到伺服器？',\n      verify: '驗證伺服器',\n      verified: '成功驗證伺服器',\n      duration: '同步頻率',\n      duration_help:\n        '新增生字後會馬上上傳，資料會在上傳前保證同步，所以如果不需要多個瀏覽器即時檢視更新，可將更新檢查週期調大些以減少資源佔用及避免伺服器拒絕回應。',\n      passwd: '密碼',\n      url: '伺服器位址',\n      user: '帳戶'\n    },\n\n    ankiconnect: {\n      description: '請確保 Anki Connect 已安裝且 Anki 在執行。',\n      checking: '連線中...',\n      deck_confirm: '牌組「{{deck}}」不存在 Anki 中，是否自動新增？',\n      deck_error: '無法建立牌組「{{deck}}」。',\n      notetype_confirm:\n        '筆記型別「{{noteType}}」不存在 Anki 中，是否自動新增？',\n      notetype_error: '無法建立筆記型別「{{noteType}}」。',\n      upload_confirm:\n        '馬上同步本地生詞到 Anki？重複的單詞（相同“Date”）會被跳過。',\n      add_yourself: '請在 Anki 中自行新增。',\n      verify: '檢查 Anki Connect',\n      verified: '成功檢查 Anki Connect',\n      enable_help:\n        '開啟後每次儲存新單字都會自動同步到 Anki。Anki 上已存在的單字（以“Date”為準）可以在單字編輯器中編輯強制更新覆蓋到 Anki。',\n      host: '地址',\n      port: '埠',\n      key: 'Key',\n      key_help: '可在 Anki Connect 外掛中設定 key 以做簡單令牌。',\n      deckName: '牌組',\n      deckName_help:\n        '如果不存在的話可以點下方「檢查 Anki Connect」讓本設定生成預設牌組。',\n      noteType: '筆記型別',\n      noteType_help:\n        'Anki 筆記型別包括一套欄位和卡片型別。如果不存在的話可以點下方「檢查 Anki Connect」讓本設定生成一套預設的筆記型別。如需自行在 Anki 新增或修改卡片模板請不要更改欄位名字。',\n      tags: '標籤',\n      tags_help: 'Anki 筆記可以附帶標籤。以逗號分割。',\n      escapeHTML: '轉義 HTML',\n      escapeHTML_help:\n        '對筆記內容中的 HTML 字元進行轉義。如手動進行 HTML 排版請關閉選項。',\n      syncServer: '同步伺服器',\n      syncServer_help: '單詞新增到本地 Anki 後自動同步到伺服器（如 AnkiWeb）。'\n    }\n  },\n\n  titlebarOffset: {\n    title: '校準標題欄高度',\n    help:\n      '不同的系統以及不同的瀏覽器設定會影響標題欄高度，沙拉查詞會嘗試自動校準，如彈出視窗依然出現偏移可自行調整。',\n    main: '普通視窗',\n    main_help: '普通視窗可能沒有標題欄。',\n    panel: '簡化視窗',\n    panel_help: '沙拉查詞的獨立視窗快捷查詞介面為簡化視窗。',\n    calibrate: '自動校準',\n    calibrateSuccess: '自動校準成功',\n    calibrateError: '自動校準失敗'\n  },\n\n  headInfo: {\n    acknowledgement: {\n      title: '特別鳴謝',\n      yipanhuasheng:\n        '新增韋氏詞典、美國傳統詞典、牛津學習詞典與歐路生詞同步；更新 Urban 詞典與 Naver 詞典',\n      naver: '協助新增 Naver 韓國語字典',\n      shanbay: '編寫扇貝詞典模組',\n      trans_tw: '提供部分繁體中文翻譯',\n      weblio: '協助新增 Weblio 辭書'\n    },\n    contact_author: '聯絡作者',\n    donate: '支援項目',\n    instructions: '使用說明',\n    report_issue: '軟體使用疑問和建言'\n  },\n\n  form: {\n    url_error: '不正確的超連結格式。',\n    number_error: '不正確的數字'\n  },\n\n  preload: {\n    title: '預先下載',\n    auto: '自動查字',\n    auto_help: '字典介面出現時自動搜尋預先載入內容。',\n    clipboard: '剪貼簿',\n    help: '字典介面出現時預先載入內容到搜尋框。',\n    selection: '滑鼠選字'\n  },\n\n  locations: {\n    CENTER: '居中',\n    TOP: '上方',\n    RIGHT: '右方',\n    BOTTOM: '下方',\n    LEFT: '左方',\n    TOP_LEFT: '左上',\n    TOP_RIGHT: '右上',\n    BOTTOM_LEFT: '左下',\n    BOTTOM_RIGHT: '右下'\n  },\n\n  import_export_help:\n    '設定已通過瀏覽器自動同步，也可以手動匯入匯出。備份為明文儲存，對安全性有要求的請自行加密。',\n\n  import: {\n    title: '匯入設定',\n    error: {\n      title: '匯入失敗',\n      parse: '備份解析失敗，格式不正確。',\n      load: '備份載入失敗，瀏覽器無法獲得本地備份。',\n      empty: '備份中沒有發現有效資料。'\n    }\n  },\n\n  export: {\n    title: '匯出設定',\n    error: {\n      title: '匯出失敗',\n      empty: '沒有設定可以匯出。',\n      parse: '設定解析失敗，無法匯出。'\n    }\n  },\n\n  dictAuth: {\n    description:\n      '隨著沙拉查詞使用者增多，如經常使用機器翻譯，建議到官網申請帳號以獲得更穩定的體驗以及更準確的結果。以下帳號資料只會保留在瀏覽器中。',\n    dictHelp: '見{詞典}官網。',\n    manage: '管理私用帳號'\n  },\n\n  third_party_privacy: '第三方隱私',\n  third_party_privacy_help:\n    '沙拉查詞不會收集更多資料，但在查詞時單詞以及相關 cookies 資料會發送給第三方詞典服務（與在該網站上查詞一樣），如果你不希望被該服務獲取資料，請在「詞典設定」中關閉相應詞典。',\n  third_party_privacy_extra: '本特性為沙拉查詞核心功能，無法關閉。',\n\n  permissions: {\n    success: '申請許可權成功',\n    cancel_success: '取消許可權成功',\n    failed: '申請許可權失敗',\n    cancelled: '申請許可權被使用者取消',\n    missing: '缺少許可權「{{permission}}」。請給予許可權或者關閉相關功能。',\n    clipboardRead: '讀取剪貼簿',\n    clipboardRead_help:\n      '快捷查詞或者右上彈框設定預載入剪貼簿時需要讀取剪貼簿許可權。',\n    clipboardWrite: '寫入剪貼簿',\n    clipboardWrite_help:\n      '機器翻譯詞典標題欄選單複製原文譯文或生詞本匯出到剪貼簿需要寫入剪貼簿許可權。'\n  },\n\n  unsupportedFeatures: {\n    ff: '火狐尚不支援「{{feature}}」功能。'\n  }\n}\n"
  },
  {
    "path": "src/_locales/zh-TW/popup.ts",
    "content": "import { locale as _locale } from '../zh-CN/popup'\n\nexport const locale: typeof _locale = {\n  title: '沙拉查詞-右上彈框',\n  app_active_title: '啟用滑鼠選字',\n  app_temp_active_title: '對目前頁面暫時關閉滑鼠選字',\n  instant_capture_pinned: '（釘選）',\n  instant_capture_title: '啟用滑鼠懸浮取詞',\n  notebook_added: '已新增',\n  notebook_empty: '目前頁面沒有發現選詞',\n  notebook_error: '無法新增選詞到生字本',\n  page_no_response: '頁面無回應',\n  qrcode_title: '目前頁面二維條碼'\n}\n"
  },
  {
    "path": "src/_locales/zh-TW/wordpage.ts",
    "content": "import { locale as _locale } from '../zh-CN/wordpage'\n\nexport const locale: typeof _locale = {\n  title: {\n    history: '沙拉查詞-查單字紀錄',\n    notebook: '沙拉查詞-生字本'\n  },\n\n  localonly: '僅本機儲存',\n\n  column: {\n    add: '新增',\n    date: '日期',\n    edit: '編輯',\n    note: '筆記',\n    source: '來源',\n    trans: '翻譯',\n    word: '單字'\n  },\n\n  delete: {\n    title: '刪除單字',\n    all: '刪除所有單字',\n    confirm: '，確認？',\n    page: '刪除本頁單字',\n    selected: '刪除選取單字'\n  },\n\n  export: {\n    title: '匯出文字',\n    all: '匯出所有單字',\n    description: '編寫產生的範本，描述每條記錄產生的樣子：',\n    explain: '如何配合 ANKI 等工具',\n    gencontent: '代表的內容',\n    linebreak: {\n      default: '保留換行',\n      n: '換行替換為 \\\\n',\n      br: '換行替換為 <br>',\n      p: '換行替換為 <p>',\n      space: '換行替換為空格'\n    },\n    page: '輸出本頁單字',\n    placeholder: '預留位置',\n    htmlescape: {\n      title: '對筆記內容中的 HTML 字元進行轉義',\n      text: '轉義 HTML'\n    },\n    selected: '輸出選中單字'\n  },\n\n  filterWord: {\n    chs: '中文',\n    eng: '英文',\n    word: '單字',\n    phrase: '片語和句子'\n  },\n\n  wordCount: {\n    selected: '選中 {{count}} 個',\n    selected_plural: '選中 {{count}} 個',\n    total: '共 {{count}} 個',\n    total_plural: '共 {{count}} 個'\n  }\n}\n"
  },
  {
    "path": "src/_sass_shared/_fancy-scrollbar.scss",
    "content": "$scrollbar-size: 8px;\n$scrollbar-ff-width: auto; // FF-only accepts auto, thin, none\n$scrollbar-minlength: 50px; // Minimum length of scrollbar thumb\n$scrollbar-track-color: transparent;\n$scrollbar-color: rgba(138, 138, 138, 0.25);\n$scrollbar-color-hover: rgba(138, 138, 138, 0.35);\n$scrollbar-color-active: rgba(138, 138, 138, 0.6);\n\n.fancy-scrollbar {\n  overscroll-behavior: contain;\n  overflow-y: scroll;\n  -webkit-overflow-scrolling: touch;\n  -ms-overflow-style: -ms-autohiding-scrollbar;\n  scrollbar-width: $scrollbar-ff-width;\n  // Firefox has incorrect track color\n  // scrollbar-color: $scrollbar-color $scrollbar-track-color;\n\n  &::-webkit-scrollbar {\n    height: $scrollbar-size;\n    width: $scrollbar-size;\n  }\n\n  &::-webkit-scrollbar-track {\n    background-color: $scrollbar-track-color;\n  }\n\n  &::-webkit-scrollbar-thumb {\n    background-color: $scrollbar-color;\n    border-radius: math.div($scrollbar-size, 2);\n  }\n\n  &::-webkit-scrollbar-thumb:hover {\n    background-color: $scrollbar-color-hover;\n  }\n\n  &::-webkit-scrollbar-thumb:active {\n    background-color: $scrollbar-color-active;\n  }\n\n  &::-webkit-scrollbar-thumb:vertical {\n    min-height: $scrollbar-minlength;\n  }\n\n  &::-webkit-scrollbar-thumb:horizontal {\n    min-width: $scrollbar-minlength;\n  }\n}\n"
  },
  {
    "path": "src/_sass_shared/_global/_interfaces.scss",
    "content": "/*-----------------------------------------------*\\\n    Tailored for exposed components\n\\*-----------------------------------------------*/\n\n%reset-important {\n  background-attachment: scroll !important;\n  background-color: transparent !important;\n  background-image: none !important;\n  background-position: 0 0 !important;\n  background-repeat: repeat !important;\n  border-color: transparent !important;\n  border-radius: 0 !important;\n  border-style: none !important;\n  border-width: 0 !important;\n  bottom: auto !important;\n  clear: none !important;\n  clip: auto !important;\n  color: inherit !important;\n  counter-increment: none !important;\n  counter-reset: none !important;\n  cursor: auto !important;\n  direction: inherit !important;\n  display: inline !important;\n  float: none !important;\n  font-family: inherit !important;\n  font-size: inherit !important;\n  font-style: inherit !important;\n  font-variant: normal !important;\n  font-weight: inherit !important;\n  height: auto !important;\n  left: auto !important;\n  letter-spacing: normal !important;\n  line-height: inherit !important;\n  list-style-type:  inherit !important;\n  list-style-position:  outside !important;\n  list-style-image:  none !important;\n  margin: 0 !important;\n  max-height: none !important;\n  max-width: none !important;\n  min-height: 0 !important;\n  min-width: 0 !important;\n  opacity: 1 !important;\n  outline: invert none medium !important;\n  overflow: visible !important;\n  padding: 0 !important;\n  position: static !important;\n  quotes:  \"\" \"\" !important;\n  right: auto !important;\n  table-layout: auto !important;\n  text-align: inherit !important;\n  text-decoration: inherit !important;\n  text-indent: 0 !important;\n  text-transform: none !important;\n  top: auto !important;\n  unicode-bidi: normal !important;\n  vertical-align: baseline !important;\n  visibility: inherit !important;\n  white-space: normal !important;\n  width: auto !important;\n  word-spacing: normal !important;\n  z-index: auto !important;\n\n  /* CSS3 */\n  /* Including all prefixes according to http://caniuse.com/ */\n  /* CSS Animations don't cascade, so don't require resetting */\n  background-origin: padding-box !important;\n  background-clip: border-box !important;\n  background-size: auto !important;\n  border-image: none !important;\n  border-radius: 0 !important;\n  box-shadow: none !important;\n  box-sizing: content-box !important;\n  column-count: auto !important;\n  column-gap: normal !important;\n  column-rule: medium none black !important;\n  column-span: 1 !important;\n  column-width: auto !important;\n  font-feature-settings: normal !important;\n  overflow-x: visible !important;\n  overflow-y: visible !important;\n  hyphens: manual !important;\n  perspective: none !important;\n  perspective-origin: 50% 50% !important;\n  backface-visibility: visible !important;\n  text-shadow: none !important;\n  transition: all 0s ease 0s !important;\n  transform: none; // In Firefox @keyframes doesn't override !important\n  transform-origin: 50% 50% !important;\n  transform-style: flat !important;\n  word-break: normal !important;\n}\n"
  },
  {
    "path": "src/_sass_shared/_global/_mixins.scss",
    "content": "// Wrapper for @at-root.\n// Emits values with namespace selectors.\n// Usage\n// @include atRoot(.ns) { ... }\n// @include atRoot(.ns2, -enter) { ... }\n// @include atRoot(.ns3, -enter, -exit) { ... }\n@mixin atRoot($nsSelector, $modifiers...) {\n  @if length($modifiers) > 0 {\n    $selectors: ();\n    @each $modifier in $modifiers {\n      $selectors: append($selectors, #{$nsSelector}#{\" \"}#{&}#{$modifier}, comma);\n    }\n    @at-root #{$selectors} {\n      @content;\n    }\n  } @else {\n    @at-root #{$nsSelector} & {\n      @content;\n    }\n  }\n}\n\n// Usage\n// @include isAnimate { ... }\n// @include isAnimate(-enter) { ... }\n// @include isAnimate(-enter, -exit) { ... }\n@mixin isAnimate($modifiers...) {\n  @include atRoot('.isAnimate', $modifiers...) {\n    @content;\n  }\n}\n\n// Usage\n// @include isDarkMode { ... }\n@mixin isDarkMode($modifiers...) {\n  @include atRoot('.darkMode', $modifiers...) {\n    @content;\n  }\n}\n"
  },
  {
    "path": "src/_sass_shared/_global/_variables.scss",
    "content": ""
  },
  {
    "path": "src/_sass_shared/_global/_z-indices.scss",
    "content": "// Max 2^31 − 1 = 2147483647\n\n$global-zindex-dropdown-backdrop: 2147483639 !default;\n$global-zindex-navbar:            2147483640 !default;\n$global-zindex-dropdown:          2147483641 !default;\n$global-zindex-fixed:             2147483642 !default;\n$global-zindex-sticky:            2147483643 !default;\n$global-zindex-modal-backdrop:    2147483644 !default;\n$global-zindex-modal:             2147483645 !default;\n$global-zindex-popover:           2147483646 !default;\n$global-zindex-tooltip:           2147483647 !default;\n\n\n$global-zindex-dicteditor:        2147483646;\n$global-zindex-dictpanel-dragbg:  2147483646;\n$global-zindex-dictpanel:         2147483647;\n$global-zindex-bowl:              2147483647;\n"
  },
  {
    "path": "src/_sass_shared/_namespace.scss",
    "content": "@use \"sass:math\";\n"
  },
  {
    "path": "src/_sass_shared/_reset.scss",
    "content": "/*-----------------------------------------------*\\\n    Custom reset\n\\*-----------------------------------------------*/\n\n@import '~normalize-scss';\n\nh1,\nh2,\nh3,\nh4,\nul,\nli,\nbutton {\n  margin: 0;\n  padding: 0;\n}\n\nimg {\n  display: block;\n  max-width: 95%;\n}\n\np {\n  margin: 0.5em 0;\n}\n\nul li {\n  list-style-type: none;\n}\n\nbutton {\n  background: transparent;\n  border: none;\n\n  &:hover {\n    outline: none;\n  }\n}\n\na {\n  color: #f9690e;\n  text-decoration: none;\n\n  &:hover {\n    text-decoration: underline;\n  }\n}\n\nselect {\n  color: #666;\n  border: 1px solid rgba(133, 133, 133, 0.28);\n  background: transparent;\n}\n"
  },
  {
    "path": "src/_sass_shared/_theme.scss",
    "content": ".saladict-theme {\n  background-color: #fff;\n  color: #333;\n  --color-brand: #5caf9e;\n  --color-background: #fff;\n  --color-rgb-background: 255, 255, 255;\n  --color-font: #333;\n  --color-font-grey: #666;\n  --color-divider: #ddd;\n\n  @include isDarkMode {\n    background-color: #222;\n    color: #ddd;\n    --color-brand: #1e947e;\n    --color-background: #222;\n    --color-rgb-background: 34, 34, 34;\n    --color-font: #ddd;\n    --color-font-grey: #aaa;\n    --color-divider: #4d4748;\n  }\n}\n"
  },
  {
    "path": "src/app-config/auth.ts",
    "content": "import { auth as baidu } from '@/components/dictionaries/baidu/auth'\nimport { auth as caiyun } from '@/components/dictionaries/caiyun/auth'\nimport { auth as sogou } from '@/components/dictionaries/sogou/auth'\nimport { auth as tencent } from '@/components/dictionaries/tencent/auth'\nimport { auth as youdaotrans } from '@/components/dictionaries/youdaotrans/auth'\n\nexport const defaultDictAuths = {\n  baidu,\n  caiyun,\n  sogou,\n  tencent,\n  youdaotrans\n}\n\nexport type DictAuths = typeof defaultDictAuths\n\nexport const getDefaultDictAuths = (): DictAuths =>\n  JSON.parse(JSON.stringify(defaultDictAuths))\n"
  },
  {
    "path": "src/app-config/context-menus.ts",
    "content": "export interface CustomContextItem {\n  name: string\n  url: string\n}\n\nexport type ContextItem = string | CustomContextItem\n\nexport function getAllContextMenus(): { [id: string]: ContextItem } {\n  return {\n    baidu_page_translate: 'x',\n    baidu_search: 'https://www.baidu.com/s?ie=utf-8&wd=%s',\n    bing_dict: 'https://cn.bing.com/dict/?q=%s',\n    bing_search: 'https://www.bing.com/search?q=%s',\n    caiyuntrs: 'x',\n    cambridge:\n      'http://dictionary.cambridge.org/spellcheck/english-chinese-simplified/?q=%s',\n    copy_pdf_url: 'x',\n    dictcn: 'https://dict.eudic.net/dicts/en/%s',\n    etymonline: 'http://www.etymonline.com/index.php?search=%s',\n    google_cn_page_translate: 'x',\n    google_page_translate: 'x',\n    google_search: 'https://www.google.com/search?safe=off&newwindow=1&q=%s',\n    google_translate: 'https://translate.google.com/#auto/zh-CN/%s',\n    google_cn_translate: 'https://translate.google.cn/#auto/zh-CN/%s',\n    guoyu: 'https://www.moedict.tw/%s',\n    iciba: 'http://www.iciba.com/%s',\n    liangan: 'https://www.moedict.tw/~%s',\n    longman_business: 'http://www.ldoceonline.com/search/?q=%s',\n    merriam_webster: 'http://www.merriam-webster.com/dictionary/%s',\n    microsoft_page_translate: 'x',\n    oxford: 'http://www.oxforddictionaries.com/us/definition/english/%s',\n    saladict: 'x',\n    saladict_standalone: 'x',\n    sogou_page_translate: 'x',\n    sogou: 'https://fanyi.sogou.com/#auto/zh-CHS/%s',\n    termonline: 'https://www.termonline.cn/list.htm?k=%s',\n    view_as_pdf: 'x',\n    youdao_page_translate: 'x',\n    youdao: 'http://dict.youdao.com/w/%s',\n    youglish: 'https://youglish.com/search/%s'\n  }\n}\n"
  },
  {
    "path": "src/app-config/dicts.ts",
    "content": "import { SupportedLangs } from '@/_helpers/lang-check'\n\nimport baidu from '@/components/dictionaries/baidu/config'\nimport bing from '@/components/dictionaries/bing/config'\nimport ahdict from '@/components/dictionaries/ahdict/config'\nimport oaldict from '@/components/dictionaries/oaldict/config'\nimport caiyun from '@/components/dictionaries/caiyun/config'\nimport cambridge from '@/components/dictionaries/cambridge/config'\nimport cnki from '@/components/dictionaries/cnki/config'\nimport cobuild from '@/components/dictionaries/cobuild/config'\nimport etymonline from '@/components/dictionaries/etymonline/config'\nimport eudic from '@/components/dictionaries/eudic/config'\nimport google from '@/components/dictionaries/google/config'\nimport googledict from '@/components/dictionaries/googledict/config'\nimport guoyu from '@/components/dictionaries/guoyu/config'\nimport hjdict from '@/components/dictionaries/hjdict/config'\nimport jikipedia from '@/components/dictionaries/jikipedia/config'\nimport jukuu from '@/components/dictionaries/jukuu/config'\nimport lexico from '@/components/dictionaries/lexico/config'\nimport liangan from '@/components/dictionaries/liangan/config'\nimport longman from '@/components/dictionaries/longman/config'\nimport macmillan from '@/components/dictionaries/macmillan/config'\nimport mojidict from '@/components/dictionaries/mojidict/config'\nimport naver from '@/components/dictionaries/naver/config'\nimport renren from '@/components/dictionaries/renren/config'\n// import shanbay from '@/components/dictionaries/shanbay/config'\nimport sogou from '@/components/dictionaries/sogou/config'\nimport tencent from '@/components/dictionaries/tencent/config'\nimport urban from '@/components/dictionaries/urban/config'\nimport vocabulary from '@/components/dictionaries/vocabulary/config'\nimport weblio from '@/components/dictionaries/weblio/config'\nimport weblioejje from '@/components/dictionaries/weblioejje/config'\nimport merriamwebster from '@/components/dictionaries/merriamwebster/config'\nimport websterlearner from '@/components/dictionaries/websterlearner/config'\nimport wikipedia from '@/components/dictionaries/wikipedia/config'\nimport youdao from '@/components/dictionaries/youdao/config'\nimport youdaotrans from '@/components/dictionaries/youdaotrans/config'\nimport zdic from '@/components/dictionaries/zdic/config'\n\n// For TypeScript to generate typings\n// Follow alphabetical order for easy reading\nexport const defaultAllDicts = {\n  baidu: baidu(),\n  bing: bing(),\n  ahdict: ahdict(),\n  oaldict: oaldict(),\n  caiyun: caiyun(),\n  cambridge: cambridge(),\n  cnki: cnki(),\n  cobuild: cobuild(),\n  etymonline: etymonline(),\n  eudic: eudic(),\n  google: google(),\n  googledict: googledict(),\n  guoyu: guoyu(),\n  hjdict: hjdict(),\n  jikipedia: jikipedia(),\n  jukuu: jukuu(),\n  lexico: lexico(),\n  liangan: liangan(),\n  longman: longman(),\n  macmillan: macmillan(),\n  mojidict: mojidict(),\n  naver: naver(),\n  renren: renren(),\n  // shanbay: shanbay(),\n  sogou: sogou(),\n  tencent: tencent(),\n  urban: urban(),\n  vocabulary: vocabulary(),\n  weblio: weblio(),\n  weblioejje: weblioejje(),\n  merriamwebster: merriamwebster(),\n  websterlearner: websterlearner(),\n  wikipedia: wikipedia(),\n  youdao: youdao(),\n  youdaotrans: youdaotrans(),\n  zdic: zdic()\n}\n\nexport type AllDicts = typeof defaultAllDicts\n\nexport const getAllDicts = (): AllDicts =>\n  JSON.parse(JSON.stringify(defaultAllDicts))\n\ninterface DictItemBase {\n  /**\n   * Supported language: en, zh-CN, zh-TW, ja, kor, fr, de, es\n   * `1` for supported\n   */\n  lang: string\n  /** Show this dictionary when selection contains words in the chosen languages. */\n  selectionLang: SupportedLangs\n  /**\n   * If set to true, the dict start searching automatically.\n   * Otherwise it'll only start seaching when user clicks the unfold button.\n   * Default MUST be true and let user decide.\n   */\n  defaultUnfold: SupportedLangs\n  /**\n   * This is the default height when the dict first renders the result.\n   * If the content height is greater than the preferred height,\n   * the preferred height is used and a mask with a view-more button is shown.\n   * Otherwise the content height is used.\n   */\n  selectionWC: {\n    min: number\n    max: number\n  }\n  /** Word count to start searching */\n  preferredHeight: number\n}\n\n/**\n * Optional dict custom options. Can only be boolean, number or string.\n * For string, add additional `options_sel` field to list out choices.\n */\ntype DictItemWithOptions<\n  Options extends\n    | { [option: string]: number | boolean | string }\n    | undefined = undefined\n> = Options extends undefined\n  ? DictItemBase\n  : DictItemBase & { options: Options }\n\n/** Infer selectable options type */\nexport type SelectOptions<\n  Options extends\n    | { [option: string]: number | boolean | string }\n    | undefined = undefined,\n  Key extends keyof Options = Options extends undefined ? never : keyof Options\n> = {\n  [opt in Key extends any\n    ? Options[Key] extends string\n      ? Key\n      : never\n    : never]: Options[opt][]\n}\n\n/**\n * If an option is of `string` type there will be an array\n * of options in `options_sel` field.\n */\nexport type DictItem<\n  Options extends\n    | { [option: string]: number | boolean | string }\n    | undefined = undefined,\n  Key extends keyof Options = Options extends undefined ? never : keyof Options\n> = Options extends undefined\n  ? DictItemWithOptions\n  : DictItemWithOptions<Options> &\n      ((Key extends any\n      ? Options[Key] extends string\n        ? Key\n        : never\n      : never) extends never\n        ? {}\n        : {\n            options_sel: SelectOptions<Options, Key>\n          })\n"
  },
  {
    "path": "src/app-config/index.ts",
    "content": "import { DeepReadonly } from '@/typings/helpers'\nimport { SupportedLangs } from '@/_helpers/lang-check'\nimport { getAllDicts } from './dicts'\nimport { getAllContextMenus } from './context-menus'\nimport { MtaAutoUnfold as _MtaAutoUnfold } from './profiles'\nimport { getDefaultDictAuths } from './auth'\nimport { isFirefox } from '@/_helpers/saladict'\n\nexport type LangCode = 'zh-CN' | 'zh-TW' | 'en'\n\nconst langUI = browser.i18n.getUILanguage()\nconst langCode: LangCode =\n  langUI === 'zh-CN'\n    ? 'zh-CN'\n    : langUI === 'zh-TW' || langUI === 'zh-HK'\n    ? 'zh-TW'\n    : 'en'\n\nexport type DictConfigsMutable = ReturnType<typeof getAllDicts>\nexport type DictConfigs = DeepReadonly<DictConfigsMutable>\nexport type DictID = keyof DictConfigsMutable\nexport type MtaAutoUnfold = _MtaAutoUnfold\n\nexport type TCDirection =\n  | 'CENTER'\n  | 'TOP'\n  | 'RIGHT'\n  | 'BOTTOM'\n  | 'LEFT'\n  | 'TOP_LEFT'\n  | 'TOP_RIGHT'\n  | 'BOTTOM_LEFT'\n  | 'BOTTOM_RIGHT'\n\nexport type InstantSearchKey = 'direct' | 'ctrl' | 'alt' | 'shift'\n\n/** '' means no preload */\nexport type PreloadSource = '' | 'clipboard' | 'selection'\n\nexport type AllDicts = ReturnType<typeof getAllDicts>\n\nexport type AppConfigMutable = ReturnType<typeof _getDefaultConfig>\nexport type AppConfig = DeepReadonly<AppConfigMutable>\n\nexport const getDefaultConfig: () => AppConfig = _getDefaultConfig\nexport default getDefaultConfig\n\nfunction _getDefaultConfig() {\n  return {\n    version: 14,\n\n    /** activate app, won't affect triple-ctrl setting */\n    active: true,\n\n    /** Run extension in background */\n    runInBg: false,\n\n    /** enable Google analytics */\n    analytics: true,\n\n    /** enable update check */\n    updateCheck: true,\n\n    /** disable selection on type fields, like input and textarea */\n    noTypeField: false,\n\n    /** use animation for transition */\n    animation: true,\n\n    /** language code for locales */\n    langCode,\n\n    /** panel width */\n    panelWidth: 450,\n\n    /** panel max height in percentage, 0 < n < 100 */\n    panelMaxHeightRatio: 80,\n\n    bowlOffsetX: 15,\n\n    bowlOffsetY: -45,\n\n    darkMode: false,\n\n    /** custom panel css */\n    panelCSS: '',\n\n    /** panel font-size */\n    fontSize: 13,\n\n    /** sniff pdf request */\n    pdfSniff: false,\n    /**\n     * Open PDF viewer in standalone panel.\n     * 'manual': do not redirect on web requests\n     */\n    pdfStandalone: '' as '' | 'always' | 'manual',\n    /** URLs, [regexp.source, match_pattern] */\n    pdfWhitelist: [] as [string, string][],\n    /** URLs, [regexp.source, match_pattern] */\n    // tslint:disable-next-line: no-unnecessary-type-assertion\n    pdfBlacklist: [\n      ['^(http|https)://[^/]*?cnki\\\\.net(/.*)?$', '*://*.cnki.net/*'],\n      [\n        '^(http|https)://[^/]*?googleusercontent\\\\.com(/.*)?$',\n        '*://*.googleusercontent.com/*'\n      ],\n      [\n        '^(http|https)://sh-download\\\\.weiyun\\\\.com(/.*)?$',\n        '*://sh-download.weiyun.com/*'\n      ]\n    ] as [string, string][],\n\n    /** track search history */\n    searchHistory: false,\n    /** incognito mode */\n    searchHistoryInco: false,\n\n    /** open word editor when adding a word to notebook */\n    editOnFav: true,\n\n    /** Show suggestions when typing on search box */\n    searchSuggests: true,\n\n    /** Enable touch related support */\n    touchMode: false,\n\n    /** when and how to search text */\n    mode: {\n      /** show pop icon first */\n      icon: true,\n      /** how panel directly */\n      direct: false,\n      /** double click */\n      double: false,\n      /** holding a key */\n      holding: {\n        alt: false,\n        shift: false,\n        ctrl: false,\n        meta: false\n      },\n      /** cursor instant capture */\n      instant: {\n        enable: false,\n        key: 'alt' as InstantSearchKey,\n        delay: 600\n      }\n    },\n\n    /** when and how to search text if the panel is pinned */\n    pinMode: {\n      /** direct: on mouseup */\n      direct: true,\n      /** double: double click */\n      double: false,\n      /** holding a key */\n      holding: {\n        alt: false,\n        shift: false,\n        ctrl: false,\n        meta: false\n      },\n      /** cursor instant capture */\n      instant: {\n        enable: false,\n        key: 'alt' as InstantSearchKey,\n        delay: 600\n      }\n    },\n\n    /** when and how to search text inside dict panel */\n    panelMode: {\n      /** direct: on mouseup */\n      direct: false,\n      /** double: double click */\n      double: false,\n      /** holding a key */\n      holding: {\n        alt: false,\n        shift: false,\n        ctrl: false,\n        meta: false\n      },\n      /** cursor instant capture */\n      instant: {\n        enable: false,\n        key: 'alt' as InstantSearchKey,\n        delay: 600\n      }\n    },\n\n    /** when this is a quick search standalone panel running */\n    qsPanelMode: {\n      /** direct: on mouseup */\n      direct: true,\n      /** double: double click */\n      double: false,\n      /** holding a key */\n      holding: {\n        alt: false,\n        shift: false,\n        ctrl: true,\n        meta: false\n      },\n      /** cursor instant capture */\n      instant: {\n        enable: false,\n        key: 'alt' as InstantSearchKey,\n        delay: 600\n      }\n    },\n\n    /** hover instead of click */\n    bowlHover: true,\n\n    /** double click delay, in ms */\n    doubleClickDelay: 450,\n\n    /** show quick search panel when triple press ctrl */\n    tripleCtrl: true,\n\n    /** preload content on quick search panel */\n    qsPreload: 'selection' as PreloadSource,\n\n    /** auto search when quick search panel opens */\n    qsAuto: false,\n\n    /** where should the dict appears */\n    qsLocation: 'CENTER' as TCDirection,\n\n    /** focus quick search panel when shows up */\n    qsFocus: true,\n\n    /** pin panel when shows up  */\n    defaultPinned: false,\n\n    /** should panel be in a standalone window */\n    qsStandalone: true,\n\n    /** standalone panel height */\n    qssaHeight: 600,\n\n    /** resize main widnow to leave space to standalone window */\n    qssaSidebar: '' as '' | 'left' | 'right',\n\n    /** should standalone panel response to page selection */\n    qssaPageSel: true,\n\n    /** should standalone panel memo position and dimension on close */\n    qssaRectMemo: false,\n\n    /** browser action panel width defaults to as wide as possible */\n    baWidth: -1,\n\n    baHeight: 550,\n\n    /** browser action panel preload source */\n    baPreload: 'selection' as PreloadSource,\n\n    /** auto search when browser action panel shows */\n    baAuto: false,\n\n    /**\n     * browser action behavior\n     * 'popup_panel' - show dict panel\n     * 'popup_fav' - add selection to notebook\n     * 'popup_options' - opten options\n     * 'popup_standalone' - open standalone panel\n     * others are same as context menus\n     */\n    baOpen: 'popup_panel',\n\n    /** context tranlate engines */\n    ctxTrans: {\n      google: true,\n      youdaotrans: true,\n      baidu: true,\n      tencent: false,\n      caiyun: false,\n      sogou: false\n    },\n\n    /** start searching when source containing the languages */\n    language: {\n      chinese: true,\n      english: true,\n      japanese: true,\n      korean: true,\n      french: true,\n      spanish: true,\n      deutsch: true,\n      others: false,\n      matchAll: false\n    } as SupportedLangs,\n\n    /** auto pronunciation */\n    autopron: {\n      cn: {\n        dict: '' as DictID | '',\n        list: ['zdic', 'guoyu'] as DictID[]\n      },\n      en: {\n        dict: '' as DictID | '',\n        list: [\n          'bing',\n          'cambridge',\n          'cobuild',\n          'eudic',\n          'longman',\n          'macmillan',\n          'lexico',\n          'urban',\n          'websterlearner',\n          'youdao'\n        ] as DictID[],\n        accent: 'uk' as 'us' | 'uk'\n      },\n      machine: {\n        dict: '' as DictID | '',\n        list: ['google', 'sogou', 'tencent', 'baidu', 'caiyun'],\n        // play translation or source\n        src: 'trans' as 'trans' | 'searchText'\n      }\n    },\n\n    /** URLs, [regexp.source, match_pattern] */\n    whitelist: [] as [string, string][],\n    /** URLs, [regexp.source, match_pattern] */\n    // tslint:disable-next-line: no-unnecessary-type-assertion\n    blacklist: [\n      ['^https://stackedit\\\\.io(/.*)?$', 'https://stackedit.io/*'],\n      ['^https://docs\\\\.google\\\\.com(/.*)?$', 'https://docs.google.com/*'],\n      ['^https://docs\\\\.qq\\\\.com(/.*)?$', 'https://docs.qq.com/*']\n    ] as [string, string][],\n\n    contextMenus: {\n      selected:\n        isFirefox || !langCode.startsWith('zh-')\n          ? ['view_as_pdf', 'google_translate', 'saladict']\n          : ['view_as_pdf', 'caiyuntrs', 'google_translate', 'saladict'],\n      all: getAllContextMenus()\n    },\n\n    /** Open settings on first switching \"translation\" profile */\n    showedDictAuth: false,\n    dictAuth: getDefaultDictAuths()\n  }\n}\n"
  },
  {
    "path": "src/app-config/merge-config.ts",
    "content": "import { getDefaultConfig, AppConfig, AppConfigMutable } from '@/app-config'\nimport { defaultAllDicts } from './dicts'\n\nimport forEach from 'lodash/forEach'\nimport isNumber from 'lodash/isNumber'\nimport isString from 'lodash/isString'\nimport isBoolean from 'lodash/isBoolean'\nimport get from 'lodash/get'\nimport set from 'lodash/set'\n\nexport default mergeConfig\n\nexport function mergeConfig(\n  oldConfig: AppConfig,\n  baseConfig?: AppConfig\n): AppConfig {\n  const base: AppConfigMutable = baseConfig\n    ? JSON.parse(JSON.stringify(baseConfig))\n    : getDefaultConfig()\n\n  /* ----------------------------------------------- *\\\n      Pre-merge Patch Start\n  \\* ----------------------------------------------- */\n  let oldVersion = oldConfig.version\n\n  if (oldVersion < 13) {\n    ;(oldConfig as AppConfigMutable).showedDictAuth = true\n  }\n\n  if (oldVersion <= 9) {\n    oldVersion = 10\n    ;['mode', 'pinMode', 'panelMode', 'qsPanelMode'].forEach(mode => {\n      base[mode].holding.shift = false\n      base[mode].holding.ctrl = !!oldConfig[mode]['ctrl']\n      base[mode].holding.meta = !!oldConfig[mode]['ctrl']\n      delete oldConfig[mode]['ctrl']\n    })\n  }\n\n  rename('tripleCtrlPreload', 'qsPreload')\n  rename('tripleCtrlAuto', 'qsAuto')\n  rename('tripleCtrlLocation', 'qsLocation')\n  rename('tripleCtrlStandalone', 'qsStandalone')\n  rename('tripleCtrlHeight', 'qssaHeight')\n  rename('tripleCtrlSidebar', 'qssaSidebar')\n  rename('tripleCtrlPageSel', 'qssaPageSel')\n  /* ----------------------------------------------- *\\\n      Pre-merge Patch End\n  \\* ----------------------------------------------- */\n\n  Object.keys(base).forEach(key => {\n    switch (key) {\n      case 'langCode':\n        merge('langCode', val => /^(zh-CN|zh-TW|en)$/.test(val))\n        break\n      case 'pdfWhitelist':\n      case 'pdfBlacklist':\n      case 'whitelist':\n      case 'blacklist':\n        merge(key, val => Array.isArray(val))\n        break\n      case 'searhHistory':\n      case 'searchHistory':\n        base.searchHistory = oldConfig[key]\n        break\n      case 'searhHistoryInco':\n      case 'searchHistoryInco':\n        base.searchHistoryInco = oldConfig[key]\n        break\n      case 'mode':\n      case 'pinMode':\n      case 'panelMode':\n      case 'qsPanelMode':\n        if (key === 'mode') {\n          mergeBoolean('mode.icon')\n        }\n        mergeBoolean(`${key}.direct`)\n        mergeBoolean(`${key}.double`)\n        mergeBoolean(`${key}.holding.alt`)\n        mergeBoolean(`${key}.holding.shift`)\n        mergeBoolean(`${key}.holding.ctrl`)\n        mergeBoolean(`${key}.holding.meta`)\n        mergeBoolean(`${key}.instant.enable`)\n        merge(`${key}.instant.key`, val =>\n          /^(direct|ctrl|alt|shift)$/.test(val)\n        )\n        mergeNumber(`${key}.instant.delay`)\n        break\n      case 'qsPreload':\n        merge(\n          'qsPreload',\n          val => val === '' || val === 'clipboard' || val === 'selection'\n        )\n        break\n      case 'qsLocation':\n        merge(\n          'qsLocation',\n          val =>\n            val === 'CENTER' ||\n            val === 'TOP' ||\n            val === 'RIGHT' ||\n            val === 'BOTTOM' ||\n            val === 'LEFT' ||\n            val === 'TOP_LEFT' ||\n            val === 'TOP_RIGHT' ||\n            val === 'BOTTOM_LEFT' ||\n            val === 'BOTTOM_RIGHT'\n        )\n        break\n      case 'baPreload':\n        merge(\n          'baPreload',\n          val => val === '' || val === 'clipboard' || val === 'selection'\n        )\n        break\n      case 'ctxTrans':\n        forEach(base.ctxTrans, (value, key) => {\n          mergeBoolean(`ctxTrans.${key}`)\n        })\n        break\n      case 'language':\n        forEach(base.language, (value, key) => {\n          mergeBoolean(`language.${key}`)\n        })\n        break\n      case 'autopron':\n        merge('autopron.cn.dict', id => defaultAllDicts[id])\n        merge('autopron.en.dict', id => defaultAllDicts[id])\n        merge('autopron.en.accent', val => val === 'us' || val === 'uk')\n        merge('autopron.machine.dict', id => defaultAllDicts[id])\n        merge(\n          'autopron.machine.src',\n          val => val === 'trans' || val === 'searchText'\n        )\n        break\n      case 'contextMenus':\n        forEach(oldConfig.contextMenus.all, (dict, id) => {\n          if (typeof dict === 'string') {\n            // default menus\n            if (base.contextMenus.all[id]) {\n              mergeString(`contextMenus.all.${id}`)\n            }\n          } else {\n            // custom menus\n            mergeString(`contextMenus.all.${id}.name`)\n            mergeString(`contextMenus.all.${id}.url`)\n          }\n        })\n        mergeSelectedContextMenus('contextMenus')\n        break\n      case 'dictAuth':\n        merge('dictAuth', Boolean)\n        break\n      default:\n        switch (typeof base[key]) {\n          case 'string':\n            mergeString(key)\n            break\n          case 'boolean':\n            mergeBoolean(key)\n            break\n          case 'number':\n            mergeNumber(key)\n            break\n          default:\n            console.error(\n              new Error(`merge config: missing handler for '${key}'`)\n            )\n        }\n        break\n    }\n  })\n\n  /* ----------------------------------------------- *\\\n      Post-merge Patch Start\n  \\* ----------------------------------------------- */\n  oldVersion = oldConfig.version\n\n  if (oldVersion <= 10) {\n    oldVersion = 11\n    base.contextMenus.selected.unshift('view_as_pdf')\n  }\n  if (oldVersion <= 11) {\n    oldVersion = 12\n    base.blacklist.push([\n      '^https://stackedit.io(/.*)?$',\n      'https://stackedit.io/*'\n    ])\n  }\n\n  if (oldConfig.language['minor'] === false) {\n    base.language.japanese = false\n    base.language.korean = false\n    base.language.french = false\n    base.language.spanish = false\n    base.language.deutsch = false\n  }\n\n  if (base.panelMaxHeightRatio < 1) {\n    base.panelMaxHeightRatio = Math.round(base.panelMaxHeightRatio * 100)\n  }\n  /* ----------------------------------------------- *\\\n      Post-merge Patch End\n  \\* ----------------------------------------------- */\n\n  return base\n\n  function rename(oldName: string, newName: string): void {\n    if (\n      !Object.prototype.hasOwnProperty.call(oldConfig, newName) &&\n      Object.prototype.hasOwnProperty.call(oldConfig, oldName)\n    ) {\n      ;(oldConfig as AppConfigMutable)[newName] = oldConfig[oldName]\n    }\n  }\n\n  function mergeSelectedContextMenus(path: string): void {\n    const selected = get(oldConfig, [path, 'selected'])\n    if (Array.isArray(selected)) {\n      if (selected.length === 0) {\n        set(base, [path, 'selected'], [])\n      } else {\n        const allContextMenus = get(base, [path, 'all'])\n        const arr = selected.filter(id => allContextMenus[id])\n        if (arr.length > 0) {\n          set(base, [path, 'selected'], arr)\n        }\n      }\n    }\n  }\n\n  function mergeNumber(path: string): void {\n    return merge(path, isNumber)\n  }\n\n  function mergeString(path: string): void {\n    return merge(path, isString)\n  }\n\n  function mergeBoolean(path: string): void {\n    return merge(path, isBoolean)\n  }\n\n  function merge(path: string, predicate: (val) => boolean): void {\n    const val = get(oldConfig, path)\n    if (predicate(val)) {\n      set(base, path, val)\n    }\n  }\n}\n"
  },
  {
    "path": "src/app-config/merge-profile.ts",
    "content": "import { Profile, ProfileMutable, getDefaultProfile } from './profiles'\n\nimport forEach from 'lodash/forEach'\nimport isNumber from 'lodash/isNumber'\nimport isString from 'lodash/isString'\nimport isBoolean from 'lodash/isBoolean'\nimport get from 'lodash/get'\nimport set from 'lodash/set'\nimport { DictID } from '.'\n\nexport function mergeProfile(\n  oldProfile: Profile,\n  baseProfile?: Profile\n): Profile {\n  const base: ProfileMutable = baseProfile\n    ? JSON.parse(JSON.stringify(baseProfile))\n    : getDefaultProfile(oldProfile.id)\n\n  Object.keys(base).forEach(key => {\n    switch (key) {\n      case 'dicts':\n        mergeDicts()\n        break\n      default:\n        switch (typeof base[key]) {\n          case 'string':\n            mergeString(key)\n            break\n          case 'boolean':\n            mergeBoolean(key)\n            break\n          case 'number':\n            mergeNumber(key)\n            break\n          default:\n            console.error(\n              new Error(`merge profile: missing handler for '${key}'`)\n            )\n        }\n        break\n    }\n  })\n\n  function mergeDicts() {\n    mergeSelectedDicts('dicts')\n\n    forEach(base.dicts.all, (dict, id) => {\n      // legacy\n      const unfold = get(oldProfile, `dicts.all.${id}.defaultUnfold`)\n      if (isBoolean(unfold)) {\n        set(base, `dicts.all.${id}.defaultUnfold`, {\n          chinese: unfold,\n          english: unfold,\n          japanese: unfold,\n          korean: unfold,\n          french: unfold,\n          spanish: unfold,\n          deutsch: unfold,\n          others: unfold\n        })\n      } else {\n        mergeBoolean(`dicts.all.${id}.defaultUnfold.chinese`)\n        mergeBoolean(`dicts.all.${id}.defaultUnfold.english`)\n        mergeBoolean(`dicts.all.${id}.defaultUnfold.japanese`)\n        mergeBoolean(`dicts.all.${id}.defaultUnfold.korean`)\n        mergeBoolean(`dicts.all.${id}.defaultUnfold.french`)\n        mergeBoolean(`dicts.all.${id}.defaultUnfold.spanish`)\n        mergeBoolean(`dicts.all.${id}.defaultUnfold.deutsch`)\n        mergeBoolean(`dicts.all.${id}.defaultUnfold.others`)\n      }\n\n      // legacy\n      const chs = get(oldProfile, `dicts.all.${id}.selectionLang.chs`)\n      if (isBoolean(chs)) {\n        set(base, `dicts.all.${id}.selectionLang.chinese`, chs)\n      } else {\n        mergeBoolean(`dicts.all.${id}.selectionLang.chinese`)\n      }\n      const eng = get(oldProfile, `dicts.all.${id}.selectionLang.eng`)\n      if (isBoolean(eng)) {\n        set(base, `dicts.all.${id}.selectionLang.english`, eng)\n      } else {\n        mergeBoolean(`dicts.all.${id}.selectionLang.english`)\n      }\n      mergeBoolean(`dicts.all.${id}.selectionLang.japanese`)\n      mergeBoolean(`dicts.all.${id}.selectionLang.korean`)\n      mergeBoolean(`dicts.all.${id}.selectionLang.french`)\n      mergeBoolean(`dicts.all.${id}.selectionLang.spanish`)\n      mergeBoolean(`dicts.all.${id}.selectionLang.deutsch`)\n      mergeBoolean(`dicts.all.${id}.selectionLang.others`)\n\n      mergeNumber(`dicts.all.${id}.preferredHeight`)\n      mergeNumber(`dicts.all.${id}.selectionWC.min`)\n      mergeNumber(`dicts.all.${id}.selectionWC.max`)\n\n      if (dict['options']) {\n        forEach(dict['options'], (value, opt) => {\n          if (isNumber(value)) {\n            mergeNumber(`dicts.all.${id}.options.${opt}`)\n          } else if (isBoolean(value)) {\n            mergeBoolean(`dicts.all.${id}.options.${opt}`)\n          } else if (isString(value)) {\n            const choice = get(oldProfile, `dicts.all.${id}.options.${opt}`)\n            const options = get(base, `dicts.all.${id}.options_sel.${opt}`)\n            set(\n              base,\n              `dicts.all.${id}.options.${opt}`,\n              options.includes(choice) ? choice : value\n            )\n          }\n        })\n\n        // legacy bug\n        // slInitial default to collapse\n        if (!isNumber(oldProfile.version)) {\n          const machineDicts: DictID[] = [\n            'baidu',\n            'caiyun',\n            'google',\n            'sogou',\n            'tencent',\n            'youdaotrans'\n          ]\n          if (\n            machineDicts.every(\n              id => get(base, `dicts.all.${id}.options.slInitial`) === 'hide'\n            )\n          ) {\n            machineDicts.forEach(id => {\n              set(base, `dicts.all.${id}.options.slInitial`, 'collapse')\n            })\n          }\n        }\n\n        // legacy\n        const pdfNewline = get(oldProfile, `dicts.all.${id}.options.pdfNewline`)\n        if (isBoolean(pdfNewline)) {\n          set(\n            base,\n            `dicts.all.${id}.options.keepLF`,\n            pdfNewline ? 'all' : 'webpage'\n          )\n        }\n      }\n    })\n  }\n\n  /* ----------------------------------------------- *\\\n      Patch Start\n  \\* ----------------------------------------------- */\n  // hjdict changed korean location\n  if ((base.dicts.all.hjdict.options.chsas as string) === 'kor') {\n    base.dicts.all.hjdict.options.chsas = 'kr'\n  }\n  /* ----------------------------------------------- *\\\n      Patch End\n  \\* ----------------------------------------------- */\n\n  return base\n\n  function mergeSelectedDicts(path: string): void {\n    const selected = get(oldProfile, [path, 'selected'])\n    if (Array.isArray(selected)) {\n      if (selected.length === 0) {\n        set(base, [path, 'selected'], [])\n      } else {\n        const allDict = get(base, [path, 'all'])\n        const arr = selected\n          .map(id => (id === 'olad' ? 'lexico' : id))\n          .filter(id => allDict[id])\n        if (arr.length > 0) {\n          set(base, [path, 'selected'], arr)\n        }\n      }\n    }\n  }\n\n  function mergeNumber(path: string): void {\n    return merge(path, isNumber)\n  }\n\n  function mergeString(path: string): void {\n    return merge(path, isString)\n  }\n\n  function mergeBoolean(path: string): void {\n    return merge(path, isBoolean)\n  }\n\n  function merge(path: string, predicate: (val) => boolean): void {\n    const val = get(oldProfile, path)\n    if (predicate(val)) {\n      set(base, path, val)\n    }\n  }\n}\n"
  },
  {
    "path": "src/app-config/profiles.ts",
    "content": "import { DeepReadonly } from '@/typings/helpers'\nimport { genUniqueKey } from '@/_helpers/uniqueKey'\nimport { getAllDicts } from './dicts'\n\nexport type MtaAutoUnfold = '' | 'once' | 'always' | 'popup' | 'hide'\n\nexport type ProfileMutable = ReturnType<typeof _getDefaultProfile>\nexport type Profile = DeepReadonly<ProfileMutable>\n\nexport interface ProfileID {\n  id: string\n  name: string\n}\n\nexport type ProfileIDList = Array<ProfileID>\n\nexport const getDefaultProfile: (id?: string) => Profile = _getDefaultProfile\n\nexport default getDefaultProfile\n\nexport function _getDefaultProfile(id?: string) {\n  return {\n    version: 1,\n\n    id: id || genUniqueKey(),\n\n    /** auto unfold multiline textarea search box */\n    mtaAutoUnfold: '' as MtaAutoUnfold,\n\n    /** show waveform control panel */\n    waveform: true,\n\n    /** remember user manual dict folding on the same page */\n    stickyFold: false,\n\n    dicts: {\n      /** default selected dictionaries */\n      selected: [\n        'bing',\n        'cobuild',\n        'cambridge',\n        'youdao',\n        'urban',\n        'vocabulary',\n        'caiyun',\n        'youdaotrans',\n        'zdic',\n        'guoyu',\n        'liangan',\n        'googledict'\n      ] as Array<keyof ReturnType<typeof getAllDicts>>,\n      // settings of each dict will be auto-generated\n      all: getAllDicts()\n    }\n  }\n}\n\nexport function getDefaultProfileID(id?: string): ProfileID {\n  return {\n    id: id || genUniqueKey(),\n    name: '%%_default_%%'\n  }\n}\n\nexport interface ProfileStorage {\n  idItem: ProfileID\n  profile: Profile\n}\n\nexport function genProfilesStorage(): {\n  profileIDList: ProfileIDList\n  profiles: Profile[]\n} {\n  const defaultID = getDefaultProfileID()\n  const defaultProfile = getDefaultProfile(defaultID.id)\n  const sentenceStorage = sentence()\n  const translationStorage = translation()\n  const scholarStorage = scholar()\n  const nihongoStorage = nihongo()\n\n  return {\n    profileIDList: [\n      defaultID,\n      sentenceStorage.idItem,\n      translationStorage.idItem,\n      scholarStorage.idItem,\n      nihongoStorage.idItem\n    ],\n    profiles: [\n      defaultProfile,\n      sentenceStorage.profile,\n      translationStorage.profile,\n      scholarStorage.profile,\n      nihongoStorage.profile\n    ]\n  }\n}\n\nexport function sentence(): ProfileStorage {\n  const idItem = getDefaultProfileID()\n  idItem.name = '%%_sentence_%%'\n\n  const profile = getDefaultProfile(idItem.id) as ProfileMutable\n  profile.dicts.selected = [\n    'jukuu',\n    'bing',\n    'cnki',\n    'renren',\n    'eudic',\n    'cobuild',\n    'cambridge',\n    'longman',\n    'macmillan'\n  ]\n\n  const allDict = profile.dicts.all\n  allDict.bing.options.tense = false\n  allDict.bing.options.phsym = false\n  allDict.bing.options.cdef = false\n  allDict.bing.options.related = false\n  allDict.bing.options.sentence = 9999\n  allDict.cnki.options.dict = false\n  allDict.eudic.options.resultnum = 9999\n  allDict.macmillan.options.related = false\n  allDict.longman.options.wordfams = false\n  allDict.longman.options.collocations = false\n  allDict.longman.options.grammar = false\n  allDict.longman.options.thesaurus = false\n  allDict.longman.options.examples = true\n  allDict.longman.options.bussinessFirst = false\n  allDict.longman.options.related = false\n\n  return { idItem, profile }\n}\n\nexport function scholar(): ProfileStorage {\n  const idItem = getDefaultProfileID()\n  idItem.name = '%%_scholar_%%'\n\n  const profile = getDefaultProfile(idItem.id) as ProfileMutable\n  profile.dicts.selected = [\n    'googledict',\n    'cambridge',\n    'cobuild',\n    'etymonline',\n    'cnki',\n    'macmillan',\n    'lexico',\n    'websterlearner',\n    'google',\n    'youdaotrans',\n    'zdic',\n    'guoyu',\n    'liangan'\n  ]\n\n  const allDict = profile.dicts.all\n  allDict.macmillan.defaultUnfold = {\n    matchAll: false,\n    english: false,\n    chinese: false,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false\n  }\n  allDict.lexico.defaultUnfold = {\n    matchAll: false,\n    english: false,\n    chinese: false,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false\n  }\n  allDict.websterlearner.defaultUnfold = {\n    matchAll: false,\n    english: false,\n    chinese: false,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false\n  }\n  allDict.google.selectionWC.min = 5\n  allDict.youdaotrans.selectionWC.min = 5\n\n  return { idItem, profile }\n}\n\nexport function translation(): ProfileStorage {\n  const idItem = getDefaultProfileID()\n  idItem.name = '%%_translation_%%'\n\n  const profile = getDefaultProfile(idItem.id) as ProfileMutable\n  profile.dicts.selected = [\n    'google',\n    'tencent',\n    'baidu',\n    'caiyun',\n    'youdaotrans',\n    'zdic',\n    'guoyu',\n    'liangan'\n  ]\n  profile.mtaAutoUnfold = 'always'\n\n  return { idItem, profile }\n}\n\nexport function nihongo(): ProfileStorage {\n  const idItem = getDefaultProfileID()\n  idItem.name = '%%_nihongo_%%'\n\n  const profile = getDefaultProfile(idItem.id) as ProfileMutable\n  profile.dicts.selected = [\n    'mojidict',\n    'hjdict',\n    'weblioejje',\n    'weblio',\n    'google',\n    'tencent',\n    'caiyun',\n    'googledict',\n    'wikipedia'\n  ]\n  profile.dicts.all.wikipedia.options.lang = 'ja'\n  profile.waveform = false\n\n  return { idItem, profile }\n}\n"
  },
  {
    "path": "src/audio-control/audio-control.scss",
    "content": "html,\nbody {\n  height: 165px;\n  overflow: hidden;\n  margin: 0;\n  padding: 0;\n}\n\n@import '@/_sass_shared/_theme.scss';\n@import '@/components/Waveform/Waveform.scss';\n"
  },
  {
    "path": "src/audio-control/index.tsx",
    "content": "import React from 'react'\nimport ReactDOM from 'react-dom'\nimport Waveform from '@/components/Waveform/Waveform'\n\nimport './audio-control.scss'\n\nconst searchParams = new URL(document.URL).searchParams\n\nconst darkMode = Boolean(searchParams.get('darkmode'))\n\nReactDOM.render(\n  <Waveform darkMode={darkMode} />,\n  document.getElementById('root')\n)\n"
  },
  {
    "path": "src/background/__fake__/env.ts",
    "content": "import axios from 'axios'\nimport AxiosMockAdapter from 'axios-mock-adapter'\n\nbrowser.runtime.sendMessage['_sender'].callsFake(() => ({\n  tab: {\n    id: 'saladict-page'\n  }\n}))\n\n// mock dict search requests\nconst dictMock = new AxiosMockAdapter(axios)\nconst dictMockReq = require.context(\n  '../../../test/specs/components/dictionaries/',\n  true,\n  /requests\\.mock\\.ts$/\n)\ndictMockReq.keys().forEach(filename => {\n  const { mockRequest } = dictMockReq(filename)\n  mockRequest(dictMock)\n})\ndictMock.onAny().reply(config => {\n  console.warn(`Unmatch url: ${config.url}`, config)\n  return [404, {}]\n})\n"
  },
  {
    "path": "src/background/__mocks__/database.ts",
    "content": "export const db = jest.fn()\nexport const isInNotebook = jest.fn()\nexport const saveWord = jest.fn()\nexport const deleteWord = jest.fn()\nexport const getWordsByText = jest.fn()\nexport const getAllWords = jest.fn()\n"
  },
  {
    "path": "src/background/audio-manager.ts",
    "content": "import { timer } from '@/_helpers/promise-more'\n\n/**\n * To make sure only one audio plays at a time\n */\nexport class AudioManager {\n  private static instance: AudioManager\n\n  static getInstance() {\n    return AudioManager.instance || (AudioManager.instance = new AudioManager())\n  }\n\n  // singleton\n  // eslint-disable-next-line no-useless-constructor\n  private constructor() {}\n\n  private audio?: HTMLAudioElement\n\n  currentSrc?: string\n\n  reset() {\n    if (this.audio) {\n      this.audio.pause()\n      this.audio.currentTime = 0\n      this.audio.src = ''\n      this.audio.onended = null\n    }\n    this.currentSrc = ''\n  }\n\n  load(src: string): HTMLAudioElement {\n    this.reset()\n    this.currentSrc = src\n    return (this.audio = new Audio(src))\n  }\n\n  async play(src?: string): Promise<void> {\n    if (!src || src === this.currentSrc) {\n      this.reset()\n      return\n    }\n\n    const audio = this.load(src)\n\n    const onEnd = Promise.race([\n      new Promise(resolve => {\n        audio.onended = resolve\n      }),\n      timer(20000)\n    ])\n\n    await audio.play()\n    await onEnd\n\n    this.currentSrc = ''\n  }\n}\n"
  },
  {
    "path": "src/background/badge.ts",
    "content": "import { message } from '@/_helpers/browser-api'\nimport { Subject } from 'rxjs'\nimport { switchMapBy } from '@/_helpers/observables'\nimport { timer } from '@/_helpers/promise-more'\n\ninterface UpdateBadgeOptions {\n  active: boolean\n  tempDisable: boolean\n  unsupported: boolean\n}\n\nconst onUpdated$ = new Subject<{\n  delay?: boolean\n  tabId: number\n  options?: UpdateBadgeOptions\n}>()\n\nonUpdated$\n  .pipe(\n    switchMapBy('tabId', async o => {\n      if (o.options) {\n        return o as Required<typeof o>\n      }\n\n      if (o.delay) {\n        await timer(1000)\n      }\n\n      return {\n        tabId: o.tabId,\n        options: (await message\n          .send<'GET_TAB_BADGE_INFO'>(o.tabId, {\n            type: 'GET_TAB_BADGE_INFO'\n          })\n          .catch(() => {})) || {\n          active: window.appConfig.active,\n          tempDisable: false,\n          unsupported: true\n        }\n      }\n    })\n  )\n  .subscribe(({ tabId, options }) => {\n    if (!options.active) {\n      return setOff(tabId)\n    }\n\n    if (options.tempDisable) {\n      return setTempOff(tabId)\n    }\n\n    if (options.unsupported) {\n      return setUnsupported(tabId)\n    }\n\n    return setDefault(tabId)\n  })\n\nexport function initBadge() {\n  /** Sent when content script loaded */\n  message.addListener('SEND_TAB_BADGE_INFO', ({ payload }, sender) => {\n    if (sender.tab && sender.tab.id) {\n      onUpdated$.next({ tabId: sender.tab.id, options: payload })\n    }\n  })\n\n  browser.tabs.onUpdated.addListener(async (tabId, changeInfo) => {\n    if (changeInfo.status === 'complete') {\n      onUpdated$.next({ tabId, delay: true })\n    }\n  })\n}\n\nfunction setOff(tabId: number) {\n  setIcon(true, tabId)\n  // browser.browserAction.setBadgeBackgroundColor({ color: '#E74C3C', tabId })\n  // browser.browserAction.setBadgeText({ text: 'off', tabId })\n  browser.browserAction.setTitle({\n    title: require('@/_locales/' + window.appConfig.langCode + '/background')\n      .locale.app.off,\n    tabId\n  })\n}\n\nfunction setTempOff(tabId: number) {\n  setIcon(true, tabId)\n  // browser.browserAction.setBadgeBackgroundColor({ color: '#F39C12', tabId })\n  // browser.browserAction.setBadgeText({ text: 'off', tabId })\n  browser.browserAction.setTitle({\n    title: require('@/_locales/' + window.appConfig.langCode + '/background')\n      .locale.app.tempOff,\n    tabId\n  })\n}\n\nfunction setUnsupported(tabId: number) {\n  setIcon(true, tabId)\n  browser.browserAction.setTitle({\n    title: require('@/_locales/' + window.appConfig.langCode + '/background')\n      .locale.app.unsupported,\n    tabId\n  })\n}\n\nfunction setDefault(tabId: number) {\n  setIcon(false, tabId)\n  // browser.browserAction.setBadgeText({ text: '', tabId })\n  // browser.browserAction.setTitle({ title: '', tabId })\n}\n\nfunction setIcon(gray: boolean, tabId: number) {\n  browser.browserAction.setIcon({\n    tabId,\n    path: gray\n      ? {\n          16: 'assets/icon-gray-16.png',\n          19: 'assets/icon-gray-19.png',\n          24: 'assets/icon-gray-24.png',\n          38: 'assets/icon-gray-38.png',\n          48: 'assets/icon-gray-48.png',\n          128: 'assets/icon-gray-128.png'\n        }\n      : {\n          16: 'assets/icon-16.png',\n          19: 'assets/icon-19.png',\n          24: 'assets/icon-24.png',\n          38: 'assets/icon-38.png',\n          48: 'assets/icon-48.png',\n          128: 'assets/icon-128.png'\n        }\n  })\n}\n"
  },
  {
    "path": "src/background/clipboard-manager.ts",
    "content": "import { openUrl } from '@/_helpers/browser-api'\n\nexport async function copyTextToClipboard(text: string): Promise<void> {\n  if (\n    !(await browser.permissions.contains({ permissions: ['clipboardWrite'] }))\n  ) {\n    openUrl(\n      '/options.html?menuselected=Permissions&missing_permission=clipboardWrite',\n      true\n    )\n    return\n  }\n\n  const copyFrom = document.createElement('textarea')\n  copyFrom.textContent = text\n  document.body.appendChild(copyFrom)\n  copyFrom.select()\n  document.execCommand('copy')\n  copyFrom.blur()\n  document.body.removeChild(copyFrom)\n}\n\nexport async function getTextFromClipboard(): Promise<string> {\n  if (\n    !(await browser.permissions.contains({ permissions: ['clipboardRead'] }))\n  ) {\n    openUrl(\n      '/options.html?menuselected=Permissions&missing_permission=clipboardRead',\n      true\n    )\n    return ''\n  }\n\n  if (process.env.NODE_ENV === 'development') {\n    return 'clipboard content'\n  } else {\n    let el = document.getElementById(\n      'saladict-paste'\n    ) as HTMLTextAreaElement | null\n    if (!el) {\n      el = document.createElement('textarea')\n      el.id = 'saladict-paste'\n      document.body.appendChild(el)\n    }\n    el.value = ''\n    el.focus()\n    document.execCommand('paste')\n    return el.value || ''\n  }\n}\n"
  },
  {
    "path": "src/background/context-menus.ts",
    "content": "import { message, openUrl } from '@/_helpers/browser-api'\nimport { AppConfig } from '@/app-config'\nimport isEqual from 'lodash/isEqual'\nimport { createConfigStream } from '@/_helpers/config-manager'\nimport { isFirefox } from '@/_helpers/saladict'\nimport { reportEvent } from '@/_helpers/analytics'\nimport './types'\n\nimport { TFunction } from 'i18next'\nimport { I18nManager } from './i18n-manager'\n\nimport { combineLatest } from 'rxjs'\nimport { concatMap, filter, distinctUntilChanged } from 'rxjs/operators'\nimport { openPDF, extractPDFUrl } from './pdf-sniffer'\nimport { copyTextToClipboard } from './clipboard-manager'\nimport { BackgroundServer } from './server'\n\ninterface CreateMenuOptions {\n  type?: browser.contextMenus.ItemType\n  id?: string\n  parentId?: string\n  title?: string\n  contexts?: browser.contextMenus.ContextType[]\n}\n\ntype ContextMenusClickInfo = Pick<\n  browser.contextMenus.OnClickData,\n  'menuItemId' | 'selectionText' | 'linkUrl' | 'pageUrl'\n>\n\nexport class ContextMenus {\n  static async getInstance() {\n    if (!ContextMenus.instance) {\n      const instance = new ContextMenus()\n      ContextMenus.instance = instance\n\n      const i18nManager = await I18nManager.getInstance()\n\n      const contextMenusChanged$ = createConfigStream().pipe(\n        distinctUntilChanged(\n          (config1, config2) =>\n            config1 &&\n            config2 &&\n            isEqual(\n              config1.contextMenus.selected,\n              config2.contextMenus.selected\n            )\n        ),\n        filter(config => !!config)\n      )\n\n      combineLatest(contextMenusChanged$, i18nManager.getFixedT$('menus'))\n        .pipe(concatMap(instance.setContextMenus))\n        .subscribe()\n    }\n\n    return ContextMenus.instance\n  }\n\n  static init = ContextMenus.getInstance\n\n  static openGoogle() {\n    return tryExecuteScript(\n      { file: '/assets/google-page-trans.js' },\n      'google_page_translate'\n    )\n  }\n\n  static openCaiyunTrs() {\n    // FF policy\n    if (isFirefox) return\n    return tryExecuteScript({ file: '/assets/trs.js' }, 'caiyuntrs')\n  }\n\n  static async openYoudao() {\n    // FF policy\n    if (isFirefox) return\n    // inject youdao script, defaults to the active tab of the current window.\n    const result = await tryExecuteScript(\n      { file: '/assets/fanyi.youdao.2.0/main.js' },\n      'youdao_page_translate'\n    )\n    if (!result || ((result as any) !== 1 && result[0] !== 1)) {\n      await browser.notifications.create({\n        type: 'basic',\n        eventTime: Date.now() + 4000,\n        iconUrl: browser.runtime.getURL(`assets/icon-128.png`),\n        title: 'Saladict',\n        message: (await I18nManager.getInstance()).i18n.t(\n          'menus:notification_youdao_err'\n        )\n      })\n    }\n  }\n\n  static openBaiduPage() {\n    browser.tabs.query({ active: true, currentWindow: true }).then(tabs => {\n      if (tabs.length > 0 && tabs[0].url) {\n        const langCode =\n          window.appConfig.langCode === 'zh-CN'\n            ? 'zh'\n            : window.appConfig.langCode === 'zh-TW'\n            ? 'cht'\n            : 'en'\n        openUrl(\n          `https://fanyi.baidu.com/transpage?query=${encodeURIComponent(\n            tabs[0].url as string\n          )}&from=auto&to=${langCode}&source=url&render=1`\n        )\n      }\n    })\n  }\n\n  static openSogouPage() {\n    browser.tabs.query({ active: true, currentWindow: true }).then(tabs => {\n      if (tabs.length > 0 && tabs[0].url) {\n        const langCode = window.appConfig.langCode === 'zh-CN' ? 'zh-CHS' : 'en'\n        openUrl(\n          `https://translate.sogoucdn.com/pcvtsnapshot?from=auto&to=${langCode}&tfr=translatepc&url=${encodeURIComponent(\n            tabs[0].url as string\n          )}&domainType=sogou`\n        )\n      }\n    })\n  }\n\n  static openMicrosoftPage() {\n    browser.tabs.query({ active: true, currentWindow: true }).then(tabs => {\n      if (tabs.length > 0 && tabs[0].url) {\n        const langCode =\n          window.appConfig.langCode === 'zh-CN'\n            ? 'zh-Hans'\n            : window.appConfig.langCode === 'zh-TW'\n            ? 'zh-Hant'\n            : 'en'\n        openUrl(\n          `https://www.microsofttranslator.com/bv.aspx?from=auto&to=${langCode}&r=true&a=${encodeURIComponent(\n            tabs[0].url as string\n          )}`\n        )\n      }\n    })\n  }\n\n  static requestSelection() {\n    browser.tabs.query({ active: true, currentWindow: true }).then(tabs => {\n      if (tabs.length > 0 && tabs[0].id != null) {\n        message.send(tabs[0].id as number, { type: 'EMIT_SELECTION' })\n      }\n    })\n  }\n\n  private handleContextMenusClick(info: ContextMenusClickInfo) {\n    const menuItemId = String(info.menuItemId).replace(/_ba$/, '')\n    const selectionText = info.selectionText || ''\n    const linkUrl = info.linkUrl || ''\n    switch (menuItemId) {\n      case 'google_page_translate':\n        ContextMenus.openGoogle()\n        break\n      case 'caiyuntrs':\n        ContextMenus.openCaiyunTrs()\n        break\n      case 'google_cn_page_translate':\n        ContextMenus.openGoogle()\n        break\n      case 'youdao_page_translate':\n        ContextMenus.openYoudao()\n        break\n      case 'baidu_page_translate':\n        ContextMenus.openBaiduPage()\n        break\n      case 'sogou_page_translate':\n        ContextMenus.openSogouPage()\n        break\n      case 'microsoft_page_translate':\n        ContextMenus.openMicrosoftPage()\n        break\n      case 'view_as_pdf':\n        openPDF(linkUrl, info.menuItemId !== 'view_as_pdf_ba')\n        break\n      case 'copy_pdf_url': {\n        const url = extractPDFUrl(info.pageUrl)\n        if (url) {\n          copyTextToClipboard(url)\n        }\n        break\n      }\n      case 'saladict':\n        ContextMenus.requestSelection()\n        break\n      case 'saladict_standalone':\n        BackgroundServer.getInstance().searchPageSelection()\n        break\n      case 'search_history':\n        openUrl(browser.runtime.getURL('history.html'))\n        break\n      case 'notebook':\n        openUrl(browser.runtime.getURL('notebook.html'))\n        break\n      default:\n        {\n          const item = window.appConfig.contextMenus.all[menuItemId]\n          if (item) {\n            const url = typeof item === 'string' ? item : item.url\n            if (url) {\n              openUrl(url.replace('%s', encodeURIComponent(selectionText)))\n            }\n          }\n        }\n        break\n    }\n  }\n\n  private static instance: ContextMenus\n\n  // singleton\n  private constructor() {\n    browser.contextMenus.onClicked.addListener(payload => {\n      reportMenusEvent(payload.menuItemId, 'From_Context_Menus')\n      return this.handleContextMenusClick(payload)\n    })\n\n    message.addListener('CONTEXT_MENUS_CLICK', ({ payload }) => {\n      reportMenusEvent(payload.menuItemId, 'From_Browser_Action')\n      return this.handleContextMenusClick(payload)\n    })\n  }\n\n  private async setContextMenus([{ searchHistory, contextMenus }, t]: [\n    AppConfig,\n    TFunction\n  ]): Promise<void> {\n    if (!browser.extension.inIncognitoContext) {\n      // In 'split' incognito mode, this will also remove the items on normal mode windows\n      await browser.contextMenus.removeAll()\n    }\n\n    const ctx: browser.contextMenus.ContextType[] = [\n      'audio',\n      'editable',\n      'frame',\n      'image',\n      'link',\n      'selection',\n      'page',\n      'video'\n    ]\n\n    // top level context menus item\n    const containerCtx = new Set<browser.contextMenus.ContextType>([\n      'selection'\n    ])\n\n    const optionList: CreateMenuOptions[] = []\n\n    const browserActionItems: string[] = []\n\n    for (const id of contextMenus.selected) {\n      let contexts: browser.contextMenus.ContextType[]\n      switch (id) {\n        case 'caiyuntrs':\n        case 'google_page_translate':\n        case 'google_cn_page_translate':\n        case 'youdao_page_translate':\n        case 'sogou_page_translate':\n        case 'baidu_page_translate':\n        case 'microsoft_page_translate':\n          // two for browser action\n          contexts = ctx\n          browserActionItems.push(id)\n          break\n        case 'view_as_pdf':\n          containerCtx.add('link')\n          containerCtx.add('page')\n          contexts = ['link', 'page']\n          break\n        case 'copy_pdf_url':\n          containerCtx.add('page')\n          contexts = ['page']\n          break\n        default:\n          contexts = ['selection']\n          break\n      }\n      optionList.push({\n        id,\n        title: getTitle(id),\n        contexts\n      })\n    }\n\n    if (optionList.length > 1) {\n      if (browserActionItems.length > 0) {\n        ctx.forEach(type => containerCtx.add(type))\n      }\n\n      await createContextMenu({\n        id: 'saladict_container',\n        title: t('saladict'),\n        contexts: [...containerCtx]\n      })\n\n      for (const opt of optionList) {\n        opt.parentId = 'saladict_container'\n        await createContextMenu(opt)\n      }\n    } else if (optionList.length > 0) {\n      // only one item, no need for parent container\n      await createContextMenu(optionList[0])\n    }\n\n    await createContextMenu({\n      id: 'view_as_pdf_ba',\n      title: t('view_as_pdf'),\n      contexts: ['browser_action', 'page_action']\n    })\n\n    if (browserActionItems.length > 2) {\n      await createContextMenu({\n        id: 'saladict_ba_container',\n        title: t('page_translations'),\n        contexts: ['browser_action', 'page_action']\n      })\n\n      for (const id of browserActionItems) {\n        await createContextMenu({\n          id: id + '_ba',\n          parentId: 'saladict_ba_container',\n          title: getTitle(id),\n          contexts: ['browser_action', 'page_action']\n        })\n      }\n    } else if (browserActionItems.length > 0) {\n      for (const id of browserActionItems) {\n        await createContextMenu({\n          id: id + '_ba',\n          title: getTitle(id),\n          contexts: ['browser_action', 'page_action']\n        })\n      }\n    } else {\n      // Add only to browser action if not selected\n      await createContextMenu({\n        id: 'google_cn_page_translate_ba',\n        title: t('google_cn_page_translate'),\n        contexts: ['browser_action', 'page_action']\n      })\n      await createContextMenu({\n        id: 'youdao_page_translate_ba',\n        title: t('youdao_page_translate'),\n        contexts: ['browser_action', 'page_action']\n      })\n    }\n\n    await createContextMenu({\n      type: 'separator',\n      id: Date.now().toString(),\n      contexts: ['browser_action']\n    })\n\n    if (searchHistory) {\n      // search history\n      await createContextMenu({\n        id: 'search_history',\n        title: t('history_title'),\n        contexts: ['browser_action']\n      })\n    }\n\n    // Manual\n    await createContextMenu({\n      id: 'notebook',\n      title: t('notebook_title'),\n      contexts: ['browser_action']\n    })\n\n    function getTitle(id: string): string {\n      const item = contextMenus.all[id]\n      return !item || typeof item === 'string' ? t(id) : item.name\n    }\n\n    function createContextMenu(\n      createProperties: CreateMenuOptions\n    ): Promise<void> {\n      return new Promise(resolve => {\n        browser.contextMenus.create(createProperties, () => {\n          if (browser.runtime.lastError) {\n            console.error(browser.runtime.lastError)\n          }\n          resolve()\n        })\n      })\n    }\n  }\n}\n\nasync function tryExecuteScript(\n  details: browser.extensionTypes.InjectDetails,\n  nameKey: string\n) {\n  try {\n    return await browser.tabs.executeScript(details)\n  } catch (error) {\n    const { i18n } = await I18nManager.getInstance()\n    await browser.notifications.create({\n      type: 'basic',\n      eventTime: Date.now() + 4000,\n      iconUrl: browser.runtime.getURL(`assets/icon-128.png`),\n      title: 'Saladict',\n      message: i18n.t('menus:page_permission_err', {\n        name: i18n.t(`menus:${nameKey}`)\n      })\n    })\n    return error\n  }\n}\n\nfunction reportMenusEvent(\n  menuItemId: string | number,\n  label: 'From_Browser_Action' | 'From_Context_Menus'\n) {\n  menuItemId = String(menuItemId).replace(/_ba$/, '')\n  switch (menuItemId) {\n    case 'google_page_translate':\n      reportEvent({\n        category: 'Page_Translate',\n        action: 'Open_Google',\n        label\n      })\n      break\n    case 'caiyuntrs':\n      reportEvent({\n        category: 'Page_Translate',\n        action: 'Open_Caiyun',\n        label\n      })\n      break\n    case 'google_cn_page_translate':\n      reportEvent({\n        category: 'Page_Translate',\n        action: 'Open_Google',\n        label\n      })\n      break\n    case 'youdao_page_translate':\n      reportEvent({\n        category: 'Page_Translate',\n        action: 'Open_Youdao',\n        label\n      })\n      break\n    case 'view_as_pdf':\n      reportEvent({\n        category: 'PDF_Viewer',\n        action: 'Open_PDF_Viewer',\n        label\n      })\n      break\n  }\n}\n"
  },
  {
    "path": "src/background/database/core.ts",
    "content": "import Dexie from 'dexie'\nimport { Word } from '@/_helpers/record-manager'\n\nexport class SaladictDB extends Dexie {\n  // @ts-ignore\n  notebook: Dexie.Table<Word, number>\n  // @ts-ignore\n  history: Dexie.Table<Word, number>\n  // @ts-ignore\n  syncmeta: Dexie.Table<{ id: string; json: string }, string>\n\n  constructor() {\n    super('SaladictWords')\n\n    this.version(1).stores({\n      notebook: 'date,text,context,url',\n      history: 'date,text,context,url',\n      syncmeta: 'id'\n    })\n\n    // The following lines are needed if your typescript\n    // is compiled using babel instead of tsc:\n    this.notebook = this.table('notebook')\n    this.history = this.table('history')\n    this.syncmeta = this.table('syncmeta')\n  }\n}\n\nlet db: SaladictDB | undefined\n\nexport async function getDB() {\n  if (!db) {\n    db = new SaladictDB()\n  }\n\n  if (!db.isOpen()) {\n    await db.open()\n  }\n\n  return db\n}\n"
  },
  {
    "path": "src/background/database/index.ts",
    "content": "export { isInNotebook, getWordsByText, getWords } from './read'\nimport {\n  saveWord as _saveWord,\n  saveWords as _saveWords,\n  deleteWords as _deleteWords\n} from './write'\nimport { syncServiceUpload } from '../sync-manager'\n\n// prevent circular dependencies\n\nexport const saveWord: typeof _saveWord = options => {\n  if (options.area === 'notebook' && options.word) {\n    syncServiceUpload({\n      action: 'ADD',\n      words: [options.word]\n    })\n  }\n  return _saveWord(options)\n}\n\nexport const saveWords: typeof _saveWords = options => {\n  if (options.area === 'notebook' && options.words.length > 0) {\n    syncServiceUpload({\n      action: 'ADD',\n      words: options.words\n    })\n  }\n  return _saveWords(options)\n}\n\nexport const deleteWords: typeof _deleteWords = options => {\n  if (options.area === 'notebook') {\n    syncServiceUpload({\n      action: 'DELETE',\n      dates: options.dates\n    })\n  }\n  return _deleteWords(options)\n}\n"
  },
  {
    "path": "src/background/database/read.ts",
    "content": "import { isContainChinese, isContainEnglish } from '@/_helpers/lang-check'\nimport { Message, MessageResponse } from '@/typings/message'\nimport { getDB } from './core'\n\nexport async function isInNotebook(word: Message<'IS_IN_NOTEBOOK'>['payload']) {\n  const db = await getDB()\n  return db.notebook\n    .where('text')\n    .equalsIgnoreCase(word.text)\n    .count()\n    .then(count => count > 0)\n}\n\nexport async function getWordsByText({\n  area,\n  text\n}: Message<'GET_WORDS_BY_TEXT'>['payload']) {\n  const db = await getDB()\n  return db[area]\n    .where('text')\n    .equalsIgnoreCase(text)\n    .toArray()\n}\n\nexport async function getWords({\n  area,\n  itemsPerPage,\n  pageNum,\n  filters = {},\n  sortField = 'date',\n  sortOrder = 'descend',\n  searchText\n}: Message<'GET_WORDS'>['payload']): Promise<MessageResponse<'GET_WORDS'>> {\n  const db = await getDB()\n  const collection = db[area].orderBy(\n    sortField\n      ? Array.isArray(sortField)\n        ? sortField.map(str => String(str))\n        : String(sortField)\n      : 'date'\n  )\n\n  if (!sortOrder || sortOrder === 'descend') {\n    collection.reverse()\n  }\n\n  const shouldFilter = Array.isArray(filters.text) && filters.text.length > 0\n  if (shouldFilter || searchText) {\n    const validLangs = shouldFilter\n      ? (filters.text as string[]).reduce((o, l) => {\n          o[l] = true\n          return o\n        }, {})\n      : {}\n    const ls = searchText ? searchText.toLocaleLowerCase() : ''\n    collection.filter(record => {\n      const rText = shouldFilter\n        ? (validLangs['en'] && isContainEnglish(record.text)) ||\n          (validLangs['ch'] && isContainChinese(record.text)) ||\n          (validLangs['word'] && !/\\s/.test(record.text)) ||\n          (validLangs['phra'] && /\\s/.test(record.text))\n        : true\n\n      const rSearch = searchText\n        ? Object.values(record).some(\n            v =>\n              typeof v === 'string' && v.toLocaleLowerCase().indexOf(ls) !== -1\n          )\n        : true\n\n      return rText && rSearch\n    })\n  }\n\n  const total = await collection.count()\n\n  if (typeof itemsPerPage !== 'undefined' && typeof pageNum !== 'undefined') {\n    collection.offset(itemsPerPage * (pageNum - 1)).limit(itemsPerPage)\n  }\n\n  const words = await collection.toArray()\n\n  return { total, words }\n}\n"
  },
  {
    "path": "src/background/database/sync-meta.ts",
    "content": "import { getDB } from './core'\n\nexport async function getSyncMeta(serviceID: string) {\n  const db = await getDB()\n  return db.syncmeta\n    .where('id')\n    .equals(serviceID)\n    .first(record => record && record.json)\n    .catch(e => {\n      if (process.env.DEBUG) {\n        console.error(e)\n      }\n    })\n}\n\nexport async function setSyncMeta(serviceID: string, text: string) {\n  const db = await getDB()\n  return db.syncmeta.put({ id: serviceID, json: text })\n}\n\nexport async function deleteSyncMeta(serviceID: string) {\n  const db = await getDB()\n  return db.syncmeta.delete(serviceID).catch(e => {\n    if (process.env.DEBUG) {\n      console.error(e)\n    }\n  })\n}\n"
  },
  {
    "path": "src/background/database/write.ts",
    "content": "import { Word, DBArea } from '@/_helpers/record-manager'\nimport { Message } from '@/typings/message'\nimport { getDB } from './core'\n\nexport async function saveWord({\n  area,\n  word\n}: Message<'SAVE_WORD'>['payload']) {\n  const db = await getDB()\n  return db[area].put(word)\n}\n\nexport async function saveWords({\n  area,\n  words\n}: {\n  area: DBArea\n  words: Word[]\n}) {\n  if (process.env.DEBUG) {\n    if (words.length !== new Set(words.map(w => w.date)).size) {\n      console.error('save Words: duplicate records')\n    }\n  }\n  const db = await getDB()\n  return db[area].bulkPut(words)\n}\n\nexport async function deleteWords({\n  area,\n  dates\n}: Message<'DELETE_WORDS'>['payload']) {\n  const db = await getDB()\n  return Array.isArray(dates) ? db[area].bulkDelete(dates) : db[area].clear()\n}\n"
  },
  {
    "path": "src/background/env.ts",
    "content": "export {}\n\nwindow.__SALADICT_INTERNAL_PAGE__ = true\nwindow.__SALADICT_BACKGROUND_PAGE__ = true\n"
  },
  {
    "path": "src/background/i18n-manager.ts",
    "content": "import i18next, { TFunction } from 'i18next'\nimport { i18nLoader, Namespace } from '@/_helpers/i18n'\nimport { BehaviorSubject, Observable } from 'rxjs'\nimport { switchMap } from 'rxjs/operators'\n\nexport class I18nManager {\n  private static instance: I18nManager\n\n  static async getInstance() {\n    if (!I18nManager.instance) {\n      const instance = new I18nManager()\n      I18nManager.instance = instance\n\n      instance.i18n = await i18nLoader()\n      instance.i18n$$.next(instance.i18n)\n    }\n    return I18nManager.instance\n  }\n\n  i18n: i18next.i18n\n\n  readonly i18n$$: BehaviorSubject<i18next.i18n>\n\n  // singleton\n  private constructor() {\n    this.i18n = i18next\n\n    this.i18n$$ = new BehaviorSubject(this.i18n)\n\n    this.i18n.on('languageChanged', () => {\n      this.i18n$$.next(this.i18n)\n    })\n  }\n\n  getFixedT$(ns: Namespace | Namespace[]): Observable<TFunction> {\n    return this.i18n$$.pipe(\n      switchMap(async i18n => {\n        await this.i18n.loadNamespaces(ns)\n        return i18n.getFixedT(i18n.language, ns)\n      })\n    )\n  }\n}\n"
  },
  {
    "path": "src/background/index.ts",
    "content": "import './env'\nimport './initialization'\nimport { getConfig, addConfigListener } from '@/_helpers/config-manager'\nimport {\n  createActiveProfileStream,\n  createProfileIDListStream\n} from '@/_helpers/profile-manager'\nimport { message } from '@/_helpers/browser-api'\nimport { startSyncServiceInterval } from './sync-manager'\nimport { init as initPdf } from './pdf-sniffer'\nimport { ContextMenus } from './context-menus'\nimport { BackgroundServer } from './server'\nimport { initBadge } from './badge'\nimport { setupCaiyunTrsBackend } from './page-translate/caiyun'\nimport { setupRequestGAListener } from '@/_helpers/analytics'\nimport './types'\n\n// init first to recevice self messaging\nmessage.self.initServer()\n\nstartSyncServiceInterval()\n\nContextMenus.init()\nBackgroundServer.init()\n\nsetupCaiyunTrsBackend()\n\nsetupRequestGAListener()\n\ngetConfig().then(async config => {\n  window.appConfig = config\n  initPdf(config)\n  initBadge()\n\n  addConfigListener(({ newConfig }) => {\n    window.appConfig = newConfig\n  })\n})\n\ncreateActiveProfileStream().subscribe(profile => {\n  window.activeProfile = profile\n})\n\ncreateProfileIDListStream().subscribe(list => {\n  window.profileIDList = list\n})\n"
  },
  {
    "path": "src/background/initialization.ts",
    "content": "import mapValues from 'lodash/mapValues'\nimport { message, storage, openUrl } from '@/_helpers/browser-api'\nimport { isExtTainted } from '@/_helpers/integrity'\nimport { checkUpdate } from '@/_helpers/check-update'\nimport { updateConfig, initConfig } from '@/_helpers/config-manager'\nimport { initProfiles, updateActiveProfileID } from '@/_helpers/profile-manager'\nimport { injectDictPanel } from '@/_helpers/injectSaladictInternal'\nimport { isFirefox } from '@/_helpers/saladict'\nimport { timer } from '@/_helpers/promise-more'\nimport {\n  getTitlebarOffset,\n  setTitlebarOffset,\n  calibrateTitlebarOffset\n} from '@/_helpers/titlebar-offset'\nimport { reportEvent } from '@/_helpers/analytics'\nimport { ContextMenus } from './context-menus'\nimport { BackgroundServer } from './server'\nimport { openPDF } from './pdf-sniffer'\nimport './types'\n\nbrowser.runtime.onInstalled.addListener(onInstalled)\nbrowser.runtime.onStartup.addListener(onStartup)\nif (browser.notifications) {\n  browser.notifications.onClicked.addListener(\n    genClickListener('https://saladict.crimx.com/releases/')\n  )\n  if (browser.notifications.onButtonClicked) {\n    // Firefox doesn't support\n    browser.notifications.onButtonClicked.addListener(\n      genClickListener('https://saladict.crimx.com/releases/')\n    )\n  }\n}\n\nbrowser.commands.onCommand.addListener(onCommand)\n\nconst getText = decodeURI\n\nfunction onCommand(command: string) {\n  switch (command) {\n    case 'toggle-active':\n      updateConfig({\n        ...window.appConfig,\n        active: !window.appConfig.active\n      })\n      break\n    case 'toggle-instant':\n      browser.tabs.query({ active: true, currentWindow: true }).then(tabs => {\n        if (tabs.length <= 0 || tabs[0].id == null) {\n          return\n        }\n        message\n          .send<'QUERY_PIN_STATE', boolean>(tabs[0].id, {\n            type: 'QUERY_PIN_STATE'\n          })\n          .then(isPinned => {\n            const config = window.appConfig\n            const { enable } = config[isPinned ? 'pinMode' : 'mode'].instant\n\n            updateConfig({\n              ...config,\n              mode: {\n                ...config.mode,\n                instant: {\n                  ...config.mode.instant,\n                  enable: !enable\n                }\n              },\n              pinMode: {\n                ...config.pinMode,\n                instant: {\n                  ...config.pinMode.instant,\n                  enable: !enable\n                }\n              }\n            })\n          })\n      })\n      break\n    case 'open-quick-search':\n      BackgroundServer.getInstance().openQSPanel()\n      break\n    case 'open-google':\n      ContextMenus.openGoogle()\n      reportEvent({\n        category: 'Page_Translate',\n        action: 'Open_Google',\n        label: 'From_Browser_Shortcut'\n      })\n      break\n    case 'open-youdao':\n      ContextMenus.openYoudao()\n      reportEvent({\n        category: 'Page_Translate',\n        action: 'Open_Youdao',\n        label: 'From_Browser_Shortcut'\n      })\n      break\n    case 'open-caiyun':\n      ContextMenus.openCaiyunTrs()\n      reportEvent({\n        category: 'Page_Translate',\n        action: 'Open_Caiyun',\n        label: 'From_Browser_Shortcut'\n      })\n      break\n    case 'open-pdf':\n      openPDF()\n      reportEvent({\n        category: 'PDF_Viewer',\n        action: 'Open_PDF_Viewer',\n        label: 'From_Browser_Shortcut'\n      })\n      break\n    case 'search-clipboard':\n      BackgroundServer.getInstance().searchClipboard()\n      break\n    case 'next-history':\n    case 'prev-history':\n      // Send to browser action panel first\n      message\n        .send<'SWITCH_HISTORY', boolean>({\n          type: 'SWITCH_HISTORY',\n          payload: command === 'next-history' ? 'next' : 'prev'\n        })\n        .then(received => {\n          if (received) return // browser action panel is opened\n\n          return browser.tabs\n            .query({ active: true, currentWindow: true })\n            .then(tabs => {\n              if (tabs.length <= 0 || tabs[0].id == null) {\n                return\n              }\n              return message.send<'SWITCH_HISTORY', boolean>(tabs[0].id, {\n                type: 'SWITCH_HISTORY',\n                payload: command === 'next-history' ? 'next' : 'prev'\n              })\n            })\n        })\n      break\n    case 'next-profile':\n    case 'prev-profile':\n      {\n        const curID = window.activeProfile.id\n        const curIndex = window.profileIDList.findIndex(\n          ({ id }) => id === curID\n        )\n        const offset = command === 'next-profile' ? 1 : -1\n        const nextIndex =\n          curIndex < 0 ? 0 : (curIndex + offset) % window.profileIDList.length\n\n        updateActiveProfileID(window.profileIDList[nextIndex].id).then(\n          searchTextBox\n        )\n      }\n      break\n    case 'profile-1':\n    case 'profile-2':\n    case 'profile-3':\n    case 'profile-4':\n    case 'profile-5':\n      {\n        const index = +command.slice(-1)\n        if (\n          index < window.profileIDList.length &&\n          window.profileIDList[index].id !== window.activeProfile.id\n        ) {\n          updateActiveProfileID(window.profileIDList[index].id).then(\n            searchTextBox\n          )\n        }\n      }\n      break\n    case 'add-notebook':\n      addNotebook()\n      break\n  }\n}\n\nasync function onInstalled({\n  reason,\n  previousVersion\n}: {\n  reason: string\n  previousVersion?: string\n}) {\n  window.appConfig = await initConfig()\n  window.activeProfile = await initProfiles()\n\n  await storage.local.set(\n    mapValues(await storage.local.get(null), (value, key) => {\n      if (key.startsWith('dict_')) return null\n      if (key === 'lastCheckUpdate') return Date.now()\n      return value\n    })\n  )\n\n  if (reason === 'install') {\n    if (\n      !(await storage.sync.get('hasInstructionsShown')).hasInstructionsShown\n    ) {\n      openUrl('options.html?menuselected=Privacy&nopanel=true', true)\n      if (window.appConfig.langCode.startsWith('zh')) {\n        openUrl('https://saladict.crimx.com/notice.html')\n      } else {\n        openUrl('https://saladict.crimx.com/en/notice.html')\n      }\n      storage.sync.set({ hasInstructionsShown: true })\n    }\n  } else if (reason === 'update') {\n    if (!process.env.DEBUG) {\n      const curr = await checkUpdate(browser.runtime.getManifest().version)\n      // same version as server\n      if (curr.data && curr.diff === 0) {\n        const { diff, data } = await checkUpdate(previousVersion, curr.data)\n        if (data && diff >= 2) {\n          setTimeout(() => {\n            const isZh = window.appConfig.langCode.startsWith('zh')\n            const options = {\n              type: 'basic',\n              iconUrl: browser.runtime.getURL(`assets/icon-128.png`),\n              title: isZh\n                ? `沙拉查词已更新到 ${data.version}`\n                : `Saladict has updated to ${data.version}`,\n              message: data.data\n                .map((line, i) => `${i + 1}. ${line}`)\n                .join('\\n'),\n              priority: 2,\n              eventTime: Date.now() + 5000\n            } as any\n\n            if (!isFirefox) {\n              options.buttons = [{ title: isZh ? '查看更新介绍' : 'More Info' }]\n              options.silent = true\n            }\n\n            if (browser.notifications) {\n              browser.notifications.create('sd-install', options)\n            }\n          }, 5000)\n        }\n      }\n    }\n  }\n\n  loadDictPanelToAllTabs()\n\n  // firefox users may want to calibrate manually\n  if (!isFirefox && !(await getTitlebarOffset())) {\n    const offset = await calibrateTitlebarOffset()\n    if (offset) {\n      setTitlebarOffset(offset)\n    }\n  }\n}\n\nfunction onStartup(): void {\n  setTimeout(() => {\n    // wait for appConfig being loaded\n    if (!process.env.DEBUG && window.appConfig.updateCheck) {\n      storage.local\n        .get<{ lastCheckUpdate: number }>('lastCheckUpdate')\n        .then(async ({ lastCheckUpdate }) => {\n          const today = Date.now()\n          if (!lastCheckUpdate) {\n            storage.local.set({ lastCheckUpdate: today })\n          } else if (today - lastCheckUpdate > 7 * 24 * 60 * 60 * 1000) {\n            storage.local.set({ lastCheckUpdate: today })\n            const { data, diff } = await checkUpdate(\n              browser.runtime.getManifest().version\n            )\n            if (data && diff > 0) {\n              const options: browser.notifications.CreateNotificationOptions = {\n                type: 'basic',\n                iconUrl: browser.runtime.getURL(`assets/icon-128.png`),\n                title: getText('%E6%B2%99%E6%8B%89%E6%9F%A5%E8%AF%8D'),\n                message: `${getText('%E5%8F%AF%E6%9B%B4%E6%96%B0%E8%87%B3')}【${\n                  data.version\n                }】`\n              }\n              if (!isFirefox) {\n                options.buttons = [\n                  { title: getText('%E6%9F%A5%E7%9C%8B%E6%9B%B4%E6%96%B0') }\n                ]\n              }\n              if (browser.notifications) {\n                browser.notifications.create('sd-update', options)\n              }\n            }\n          }\n        })\n    }\n  }, 1000)\n\n  if (!process.env.DEBUG && isExtTainted) {\n    storage.local.get<{ swat: number }>('swat').then(({ swat }) => {\n      const today = Date.now()\n      if (!swat) {\n        storage.local.set({ swat: today })\n      } else if (today - swat > 10 * 24 * 60 * 60 * 1000) {\n        storage.local.set({ swat: today })\n        const options: browser.notifications.CreateNotificationOptions = {\n          type: 'basic',\n          iconUrl: browser.runtime.getURL(`assets/icon-128.png`),\n          title: getText('%E6%B2%99%E6%8B%89%E6%9F%A5%E8%AF%8D'),\n          message: getText(\n            '%E6%AD%A4%E3%80%8C%E6%B2%99%E6%8B%89%E6%9F%A5%E8%' +\n              'AF%8D%E3%80%8D%E6%89%A9%E5%B1%95%E5%B7%B2%E8%A2' +\n              '%AB%E4%BA%8C%E6%AC%A1%E6%89%93%E5%8C%85%EF%BC%8' +\n              'C%E8%AF%B7%E5%9C%A8%E5%AE%98%E6%96%B9%E5%BB%BA%' +\n              'E8%AE%AE%E7%9A%84%E5%B9%B3%E5%8F%B0%E5%AE%89%E8' +\n              '%A3%85%E3%80%82'\n          )\n        }\n        if (!isFirefox) {\n          options.buttons = [\n            {\n              title: getText(\n                '%E6%9F%A5%E7%9C%8B%E5%8F%AF%E9%9D%A0%E7%9A%84%E5%B9%B3%E5%8F%B0'\n              )\n            }\n          ]\n        }\n        if (browser.notifications) {\n          browser.notifications.create('sd-update', options)\n        }\n      }\n    })\n  }\n\n  // Chrome fails to inject css via manifest if the page is loaded\n  // as \"last opened tabs\" when browser opens.\n  setTimeout(() => {\n    loadDictPanelToAllTabs()\n  }, 1000)\n}\n\nfunction genClickListener(url: string) {\n  return function clickListener(notificationId: string) {\n    switch (notificationId) {\n      case 'sd-install':\n      case 'sd-update':\n        openUrl(url)\n        if (browser.notifications) {\n          browser.notifications.getAll().then(notifications => {\n            Object.keys(notifications).forEach(id =>\n              browser.notifications.clear(id)\n            )\n          })\n        }\n        break\n    }\n  }\n}\n\nasync function loadDictPanelToAllTabs() {\n  ;(await browser.tabs.query({})).forEach(async tab => {\n    if (tab.id && tab.url && tab.url.startsWith('http')) {\n      try {\n        await injectDictPanel(tab)\n      } catch (e) {\n        console.warn(e)\n      }\n    }\n  })\n}\n\n/** Search text box text on active tab */\nasync function searchTextBox() {\n  await timer(10)\n\n  if (await message.send<'SEARCH_TEXT_BOX'>({ type: 'SEARCH_TEXT_BOX' })) {\n    return // popup page received\n  }\n\n  const tabs = await browser.tabs.query({\n    active: true,\n    currentWindow: true\n  })\n  if (tabs.length <= 0 || tabs[0].id == null) {\n    return\n  }\n  message.send(tabs[0].id, { type: 'SEARCH_TEXT_BOX' })\n}\n\nasync function addNotebook() {\n  if (\n    await message.send<'ADD_NOTEBOOK'>({\n      type: 'ADD_NOTEBOOK',\n      payload: { popup: true }\n    })\n  ) {\n    return // popup page received\n  }\n\n  const tabs = await browser.tabs.query({\n    active: true,\n    currentWindow: true\n  })\n  if (tabs.length <= 0 || tabs[0].id == null) {\n    return\n  }\n  message.send(tabs[0].id, { type: 'ADD_NOTEBOOK', payload: { popup: false } })\n}\n"
  },
  {
    "path": "src/background/page-translate/caiyun.ts",
    "content": "export function setupCaiyunTrsBackend() {\n  browser.runtime.onMessage.addListener(msg => {\n    if (msg.contentScriptQuery === 'fetchUrl') {\n      const requestInit: RequestInit = {\n        method: msg.method || 'GET',\n        credentials: 'include',\n        headers: {\n          ...(msg.headers || {}),\n          'content-type': 'application/json'\n        }\n      }\n\n      if (msg.data) {\n        try {\n          requestInit.body = JSON.stringify(msg.data)\n        } catch (error) {\n          if (process.env.DEBUG) {\n            console.error('Caiyun trs message data error:', error)\n          }\n        }\n      }\n\n      return fetch(msg.url, requestInit)\n        .then(response => response.text())\n        .then(text => ({ status: 'ok', data: text }))\n        .catch(error => {\n          if (process.env.DEBUG) {\n            console.error('Caiyun trs requestAuthURL error:', error)\n          }\n          return { status: 'error', error: error }\n        })\n    }\n  })\n}\n"
  },
  {
    "path": "src/background/pdf-sniffer.ts",
    "content": "/**\n * Open pdf link directly\n */\n\nimport { AppConfig } from '@/app-config'\nimport { addConfigListener } from '@/_helpers/config-manager'\nimport { openUrl } from '@/_helpers/browser-api'\n\nexport function init(config: AppConfig) {\n  if (browser.webRequest.onBeforeRequest.hasListener(otherPdfListener)) {\n    return\n  }\n\n  if (config.pdfSniff) {\n    startListening()\n  }\n\n  addConfigListener(({ newConfig, oldConfig }) => {\n    if (newConfig) {\n      if (!oldConfig || newConfig.pdfSniff !== oldConfig.pdfSniff) {\n        if (newConfig.pdfSniff) {\n          startListening()\n        } else {\n          stopListening()\n        }\n      }\n    }\n  })\n}\n\n/**\n * @param url provide a url\n * @param force load the current tab anyway\n */\nexport async function openPDF(url?: string, force?: boolean) {\n  let pdfURL = browser.runtime.getURL('assets/pdf/web/viewer.html')\n\n  if (url) {\n    pdfURL += '?file=' + encodeURIComponent(url)\n  } else {\n    const tabs = await browser.tabs.query({ active: true, currentWindow: true })\n    if (tabs.length > 0 && tabs[0].url) {\n      const curURL = tabs[0].url\n      if (curURL.startsWith(pdfURL)) {\n        if (window.appConfig.pdfStandalone) {\n          if (tabs[0].id != null) {\n            await browser.tabs.remove(tabs[0].id)\n          }\n          pdfURL = curURL\n        } else {\n          return // ignore pdf viewer url\n        }\n      } else if (force || curURL.endsWith('pdf')) {\n        pdfURL += '?file=' + encodeURIComponent(curURL)\n      }\n    }\n  }\n\n  return window.appConfig.pdfStandalone\n    ? openPDFStandalone(pdfURL)\n    : openUrl({ url: pdfURL, unique: false })\n}\n\nexport function extractPDFUrl(fullurl?: string): string | void {\n  if (!fullurl) {\n    return\n  }\n  const searchURL = new URL(fullurl)\n  return decodeURIComponent(searchURL.searchParams.get('file') || '')\n}\n\nfunction startListening() {\n  if (!browser.webRequest.onBeforeRequest.hasListener(otherPdfListener)) {\n    browser.webRequest.onBeforeRequest.addListener(\n      otherPdfListener,\n      {\n        urls: [\n          'ftp://*/*.pdf',\n          'ftp://*/*.PDF',\n          'file://*/*.pdf',\n          'file://*/*.PDF'\n        ],\n        types: ['main_frame', 'sub_frame']\n      },\n      ['blocking']\n    )\n  }\n\n  if (!browser.webRequest.onHeadersReceived.hasListener(httpPdfListener)) {\n    browser.webRequest.onHeadersReceived.addListener(\n      httpPdfListener,\n      {\n        urls: ['https://*/*', 'https://*/*', 'http://*/*', 'http://*/*'],\n        types: ['main_frame', 'sub_frame']\n      },\n      ['blocking', 'responseHeaders']\n    )\n  }\n}\n\nfunction stopListening() {\n  browser.webRequest.onBeforeRequest.removeListener(otherPdfListener)\n  browser.webRequest.onHeadersReceived.removeListener(httpPdfListener)\n}\n\nfunction otherPdfListener({\n  tabId,\n  url\n}: Parameters<\n  Parameters<typeof browser.webRequest.onBeforeRequest.removeListener>[0]\n>[0]) {\n  const matchURL = ([r]: ReadonlyArray<string>) => new RegExp(r).test(url)\n  if (\n    window.appConfig.pdfBlacklist.some(matchURL) &&\n    !window.appConfig.pdfWhitelist.some(matchURL)\n  ) {\n    return\n  }\n\n  const redirectUrl = browser.runtime.getURL(\n    `assets/pdf/web/viewer.html?file=${encodeURIComponent(url)}`\n  )\n\n  if (tabId !== -1 && window.appConfig.pdfStandalone === 'always') {\n    browser.tabs.remove(tabId)\n    openPDFStandalone(redirectUrl)\n    return { cancel: true }\n  }\n\n  return { redirectUrl }\n}\n\nfunction httpPdfListener({\n  tabId,\n  responseHeaders,\n  url\n}: Parameters<\n  Parameters<typeof browser.webRequest.onHeadersReceived.removeListener>[0]\n>[0]) {\n  if (!responseHeaders) {\n    return\n  }\n  const matchURL = ([r]: ReadonlyArray<string>) => new RegExp(r).test(url)\n  if (\n    window.appConfig.pdfBlacklist.some(matchURL) &&\n    !window.appConfig.pdfWhitelist.some(matchURL)\n  ) {\n    return\n  }\n\n  const contentTypeHeader = responseHeaders.find(\n    ({ name }) => name.toLowerCase() === 'content-type'\n  )\n  if (contentTypeHeader && contentTypeHeader.value) {\n    const contentType = contentTypeHeader.value.toLowerCase()\n    if (\n      contentType.endsWith('pdf') ||\n      (contentType === 'application/octet-stream' && url.endsWith('.pdf'))\n    ) {\n      const redirectUrl = browser.runtime.getURL(\n        `assets/pdf/web/viewer.html?file=${encodeURIComponent(url)}`\n      )\n\n      if (tabId !== -1 && window.appConfig.pdfStandalone === 'always') {\n        browser.tabs.remove(tabId)\n        openPDFStandalone(redirectUrl)\n        return { cancel: true }\n      }\n\n      return { redirectUrl }\n    }\n  }\n}\n\nfunction openPDFStandalone(url: string) {\n  return browser.windows.create({ type: 'popup', url })\n}\n"
  },
  {
    "path": "src/background/server.ts",
    "content": "import { message, openUrl } from '@/_helpers/browser-api'\nimport { timeout, timer } from '@/_helpers/promise-more'\nimport { getSuggests } from '@/_helpers/getSuggests'\nimport { injectDictPanel } from '@/_helpers/injectSaladictInternal'\nimport { newWord, Word } from '@/_helpers/record-manager'\nimport { Message, MessageResponse } from '@/typings/message'\nimport {\n  SearchFunction,\n  DictSearchResult,\n  GetSrcPageFunction\n} from '@/components/dictionaries/helpers'\nimport {\n  isInNotebook,\n  saveWord,\n  deleteWords,\n  getWordsByText,\n  getWords\n} from './database'\nimport { AudioManager } from './audio-manager'\nimport { QsPanelManager } from './windows-manager'\nimport { getTextFromClipboard, copyTextToClipboard } from './clipboard-manager'\nimport './types'\nimport { DictID } from '@/app-config'\n\n/**\n * background script as transfer station\n */\nexport class BackgroundServer {\n  private static instance: BackgroundServer\n\n  static getInstance() {\n    return (\n      BackgroundServer.instance ||\n      (BackgroundServer.instance = new BackgroundServer())\n    )\n  }\n\n  static init = BackgroundServer.getInstance\n\n  static getDictEngine<P = {}>(\n    id: DictID\n  ): Promise<{\n    search: SearchFunction<DictSearchResult<any>, P>\n    getSrcPage: GetSrcPageFunction\n  }> {\n    return import(\n      /* webpackInclude: /engine\\.ts$/ */\n      /* webpackMode: \"lazy\" */\n      `@/components/dictionaries/${id}/engine.ts`\n    )\n  }\n\n  private qsPanelManager: QsPanelManager\n\n  // singleton\n  private constructor() {\n    this.qsPanelManager = new QsPanelManager()\n\n    message.addListener((msg, sender: browser.runtime.MessageSender) => {\n      switch (msg.type) {\n        case 'OPEN_DICT_SRC_PAGE':\n          return this.openSrcPage(msg.payload)\n        case 'OPEN_URL':\n          return openUrl(msg.payload)\n        case 'PLAY_AUDIO':\n          return AudioManager.getInstance().play(msg.payload)\n        case 'STOP_AUDIO':\n          AudioManager.getInstance().reset()\n          return\n        case 'FETCH_DICT_RESULT':\n          return this.fetchDictResult(msg.payload)\n        case 'DICT_ENGINE_METHOD':\n          return this.callDictEngineMethod(msg.payload)\n        case 'GET_CLIPBOARD':\n          return getTextFromClipboard()\n        case 'SET_CLIPBOARD':\n          return Promise.resolve(copyTextToClipboard(msg.payload))\n\n        case 'INJECT_DICTPANEL':\n          return injectDictPanel(sender.tab)\n\n        case 'QUERY_QS_PANEL':\n          return this.qsPanelManager.hasCreated()\n        case 'OPEN_QS_PANEL':\n          return this.openQSPanel()\n        case 'CLOSE_QS_PANEL':\n          AudioManager.getInstance().reset()\n          return this.qsPanelManager.destroy()\n        case 'QS_SWITCH_SIDEBAR':\n          return this.qsPanelManager.toggleSidebar(msg.payload)\n\n        case 'IS_IN_NOTEBOOK':\n          return isInNotebook(msg.payload)\n        case 'SAVE_WORD':\n          return saveWord(msg.payload).then(response => {\n            this.notifyWordSaved()\n            return response\n          })\n        case 'DELETE_WORDS':\n          return deleteWords(msg.payload).then(response => {\n            this.notifyWordSaved()\n            return response\n          })\n        case 'GET_WORDS_BY_TEXT':\n          return getWordsByText(msg.payload)\n        case 'GET_WORDS':\n          return getWords(msg.payload)\n        case 'GET_SUGGESTS':\n          return getSuggests(msg.payload)\n        case 'YOUDAO_TRANSLATE_AJAX':\n          return this.youdaoTranslateAjax(msg.payload)\n      }\n    })\n\n    browser.runtime.onConnect.addListener(port => {\n      if (port.name === 'popup') {\n        // This is a workaround for browser action page\n        // which does not fire beforeunload event\n        port.onDisconnect.addListener(() => {\n          AudioManager.getInstance().reset()\n        })\n      }\n    })\n  }\n\n  async openQSPanel(): Promise<void> {\n    if (await this.qsPanelManager.hasCreated()) {\n      await this.qsPanelManager.focus()\n      return\n    }\n    await this.qsPanelManager.create()\n  }\n\n  async searchClipboard(): Promise<void> {\n    const word = newWord({ text: await getTextFromClipboard() })\n\n    if (await this.qsPanelManager.hasCreated()) {\n      await message.send({\n        type: 'QS_PANEL_SEARCH_TEXT',\n        payload: word\n      })\n      return\n    }\n\n    await this.qsPanelManager.create(word)\n  }\n\n  async searchPageSelection(): Promise<void> {\n    const tabs = await browser.tabs.query({\n      active: true,\n      lastFocusedWindow: true\n    })\n\n    let word: Word | undefined\n\n    if (tabs.length > 0 && tabs[0].id != null) {\n      word = await message.send<'PRELOAD_SELECTION'>(tabs[0].id, {\n        type: 'PRELOAD_SELECTION'\n      })\n    }\n\n    const hasCreated = await this.qsPanelManager.hasCreated()\n\n    if (hasCreated) {\n      await this.qsPanelManager.focus()\n    } else {\n      await this.qsPanelManager.create(word)\n    }\n  }\n\n  async openSrcPage({\n    id,\n    text,\n    active\n  }: Message<'OPEN_DICT_SRC_PAGE'>['payload']): Promise<void> {\n    const engine = await BackgroundServer.getDictEngine(id)\n    return openUrl({\n      url: await engine.getSrcPage(\n        text,\n        window.appConfig,\n        window.activeProfile\n      ),\n      active\n    })\n  }\n\n  async fetchDictResult(\n    data: Message<'FETCH_DICT_RESULT'>['payload']\n  ): Promise<MessageResponse<'FETCH_DICT_RESULT'>> {\n    const payload = data.payload || {}\n\n    let response: DictSearchResult<any> | undefined\n\n    try {\n      const { search } = await BackgroundServer.getDictEngine<\n        NonNullable<typeof data['payload']>\n      >(data.id)\n\n      try {\n        response = await timeout(\n          search(data.text, window.appConfig, window.activeProfile, payload),\n          25000\n        )\n      } catch (e) {\n        if (e.message === 'NETWORK_ERROR') {\n          // retry once\n          await timer(500)\n          response = await timeout(\n            search(data.text, window.appConfig, window.activeProfile, payload),\n            25000\n          )\n        } else {\n          throw e\n        }\n      }\n    } catch (e) {\n      if (process.env.DEBUG) {\n        console.warn(data.id, e)\n      }\n    }\n\n    const result = response\n      ? { ...response, id: data.id }\n      : { result: null, id: data.id }\n\n    if (process.env.DEBUG) {\n      console.log(`Search Engine ${data.id}`, data.text, result)\n    }\n\n    return result\n  }\n\n  async callDictEngineMethod(data: Message<'DICT_ENGINE_METHOD'>['payload']) {\n    const engine = await BackgroundServer.getDictEngine(data.id)\n    return engine[data.method](...(data.args || []))\n  }\n\n  notifyWordSaved() {\n    browser.tabs.query({}).then(tabs => {\n      tabs.forEach(async tab => {\n        if (tab.id && tab.url) {\n          try {\n            await message.send(tab.id, { type: 'WORD_SAVED' })\n          } catch (e) {\n            console.warn(e)\n          }\n        }\n      })\n    })\n  }\n\n  /** Bypass http restriction */\n  youdaoTranslateAjax(request: any): Promise<any> {\n    return new Promise(resolve => {\n      const xhr = new XMLHttpRequest()\n      xhr.onreadystatechange = () => {\n        if (xhr.readyState === 4) {\n          const data = xhr.status === 200 ? xhr.responseText : null\n          resolve({\n            response: data,\n            index: request.index\n          })\n        }\n      }\n      xhr.open(request.type, request.url, true)\n\n      if (request.type === 'POST') {\n        xhr.setRequestHeader(\n          'Content-Type',\n          'application/x-www-form-urlencoded'\n        )\n        xhr.send(request.data)\n      } else {\n        xhr.send(null as any)\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "src/background/sync-manager/__mocks__/helpers.ts",
    "content": "import { EMPTY, Observable } from 'rxjs'\n\nconst emptyPromise = (): Promise<any> => Promise.resolve()\n\nexport const setSyncConfig = jest.fn(emptyPromise)\n\nexport const getSyncConfig = jest.fn(emptyPromise)\n\nexport const createSyncConfigStream = jest.fn((): Observable<any> => EMPTY)\n\nexport const setMeta = jest.fn(emptyPromise)\n\nexport const getMeta = jest.fn(emptyPromise)\n\nexport const deleteMeta = jest.fn(emptyPromise)\n\nexport const setNotebook = jest.fn(emptyPromise)\n\nexport const getNotebook = jest.fn(emptyPromise)\n"
  },
  {
    "path": "src/background/sync-manager/helpers.ts",
    "content": "import { storage } from '@/_helpers/browser-api'\nimport { Word } from '@/_helpers/record-manager'\nimport { getWords } from '@/background/database/read'\nimport { saveWords } from '@/background/database/write'\nimport {\n  getSyncMeta,\n  setSyncMeta,\n  deleteSyncMeta\n} from '@/background/database/sync-meta'\nimport { I18nManager } from '../i18n-manager'\n\nexport interface StorageSyncConfig {\n  syncConfig: { [id: string]: any }\n}\n\nexport async function setSyncConfig<T = any>(\n  serviceID: string,\n  config: T\n): Promise<void> {\n  let { syncConfig } = await storage.sync.get<StorageSyncConfig>('syncConfig')\n  if (!syncConfig) {\n    syncConfig = {}\n  }\n  syncConfig[serviceID] = config\n  await storage.sync.set({ syncConfig })\n}\n\nexport async function getSyncConfig<T>(\n  serviceID: string\n): Promise<T | undefined> {\n  const { syncConfig } = await storage.sync.get<StorageSyncConfig>('syncConfig')\n  if (syncConfig !== undefined) {\n    return syncConfig[serviceID]\n  }\n}\n\nexport async function removeSyncConfig(serviceID?: string): Promise<void> {\n  if (serviceID) {\n    await setSyncConfig(serviceID, null)\n  } else {\n    await storage.sync.remove('syncConfig')\n  }\n}\n\n/**\n * Service meta data is saved with the database\n * so that it can be shared across browser vendors.\n */\nexport async function setMeta<T = any>(\n  serviceID: string,\n  meta: T\n): Promise<void> {\n  await setSyncMeta(serviceID, JSON.stringify(meta))\n}\n\n/**\n * Service meta data is saved with the database\n * so that it can be shared across browser vendors.\n */\nexport async function getMeta<T>(serviceID: string): Promise<T | undefined> {\n  const text = await getSyncMeta(serviceID)\n  if (text) {\n    return JSON.parse(text)\n  }\n}\n\n/**\n * Service meta data is saved with the database\n * so that it can be shared across browser vendors.\n */\nexport async function deleteMeta(serviceID: string): Promise<void> {\n  await deleteSyncMeta(serviceID)\n}\n\nexport async function setNotebook(words: Word[]): Promise<void> {\n  await saveWords({ area: 'notebook', words })\n}\n\nexport async function getNotebook(): Promise<Word[]> {\n  return (await getWords({ area: 'notebook' })).words || []\n}\n\nexport async function notifyError(\n  id: string,\n  error: Error | string,\n  msgPrefix = '',\n  msgPostfix = ''\n): Promise<void> {\n  const { i18n } = await I18nManager.getInstance()\n  await i18n.loadNamespaces('sync')\n  const errorText = typeof error === 'string' ? error : error.message\n  const msgPath = `sync:${id}.error.${errorText}`\n  const msg = i18n.exists(msgPath)\n    ? i18n.t(msgPath)\n    : `Unknown error: ${errorText}`\n\n  browser.notifications.create({\n    type: 'basic',\n    iconUrl: browser.runtime.getURL(`assets/icon-128.png`),\n    title: `Saladict ${i18n.t(`sync:${id}.title`)}`,\n    message: msgPrefix + msg + msgPostfix,\n    eventTime: Date.now() + 20000,\n    priority: 2\n  })\n}\n"
  },
  {
    "path": "src/background/sync-manager/index.ts",
    "content": "import { SyncService, SyncServiceConstructor } from './interface'\nimport { concat, from } from 'rxjs'\nimport { filter, pluck, map } from 'rxjs/operators'\nimport { storage } from '@/_helpers/browser-api'\nimport { Word } from '@/_helpers/record-manager'\nimport { notifyError } from './helpers'\n\nconst reqServices = require.context('./services', true, /index\\.ts$/)\n\nconst Services = reqServices.keys().reduce((map, path) => {\n  const Servicex = reqServices(path).Service\n  return map.set(Servicex.id, Servicex)\n}, new Map<string, SyncServiceConstructor>())\n\nconst activeServices: Map<string, SyncService> = new Map()\n\nexport function startSyncServiceInterval() {\n  concat(\n    from(storage.sync.get('syncConfig')).pipe(pluck('syncConfig')),\n    storage.sync.createStream('syncConfig').pipe(pluck('newValue'))\n  )\n    .pipe(\n      filter((v): v is { [id: string]: any } => !!v),\n      map(syncConfig => {\n        // legacy fix\n        if (\n          syncConfig.webdav &&\n          !Object.prototype.hasOwnProperty.call(syncConfig.webdav, 'enable')\n        ) {\n          syncConfig.webdav.enable = !!syncConfig.webdav.url\n        }\n        return syncConfig\n      })\n    )\n    .subscribe(async syncConfig => {\n      try {\n        await Promise.all(\n          [...activeServices.values()].map(service => service.destroy())\n        )\n      } catch (e) {\n        console.error(e)\n      }\n      activeServices.clear()\n\n      if (syncConfig) {\n        Services.forEach(Service => {\n          if (syncConfig[Service.id]?.enable) {\n            const newService = new Service(syncConfig[Service.id])\n            activeServices.set(Service.id, newService)\n            newService.onStart()\n          }\n        })\n      }\n\n      if (process.env.DEBUG) {\n        console.log(`Active Sync Services:`, [...activeServices.keys()])\n      }\n    })\n}\n\nexport async function syncServiceUpload(\n  options:\n    | {\n        action: 'ADD'\n        words?: Word[]\n        force?: boolean\n      }\n    | {\n        action: 'DELETE'\n        dates?: number[]\n        force?: boolean\n      }\n) {\n  activeServices.forEach(async (service, id) => {\n    try {\n      if (options.action === 'ADD') {\n        await service.add({ words: options.words, force: options.force })\n      } else if (options.action === 'DELETE') {\n        await service.delete({ dates: options.dates, force: options.force })\n      }\n    } catch (error) {\n      notifyError(\n        id,\n        error,\n        options.action === 'ADD' && options.words?.[0]\n          ? `「${options.words?.[0].text}」`\n          : ''\n      )\n    }\n  })\n}\n"
  },
  {
    "path": "src/background/sync-manager/interface.ts",
    "content": "import { Word } from '@/_helpers/record-manager'\n\nexport interface NotebookFile {\n  timestamp: number\n  words: Word[]\n}\n\nexport interface DlResponse {\n  json: NotebookFile\n  etag: string\n}\n\nexport interface AddConfig {\n  readonly words?: ReadonlyArray<Readonly<Word>>\n  /** Do not sync before upload */\n  readonly force?: boolean\n}\n\nexport interface DeleteConfig {\n  readonly dates?: ReadonlyArray<number>\n  /** Do not sync before upload */\n  readonly force?: boolean\n}\n\nexport interface DownloadConfig<Config = any> {\n  /** Test connectivity. Do not update anything. */\n  readonly testConfig?: Readonly<Config>\n  /** ignore server 304 cache */\n  readonly noCache?: boolean\n}\n\nexport interface SyncServiceConfigBase {\n  enable: boolean\n}\n\nexport abstract class SyncService<\n  Config extends SyncServiceConfigBase = any,\n  Meta = any\n> {\n  static readonly id: string\n\n  /**\n   * Service config that is saved with browser sync storage.\n   * It is updated automatically.\n   */\n  config: Config\n  /** service data that is saved with the database */\n  meta?: Meta\n\n  static getDefaultConfig() {\n    return {}\n  }\n\n  static getDefaultMeta() {\n    return {}\n  }\n\n  constructor(config: Config) {\n    this.config = config\n  }\n\n  /** Called when user updates config. Check env, save config etc */\n  abstract init(): Promise<void>\n  /** add words */\n  abstract add(config: AddConfig): Promise<void>\n  /** delete words */\n  async delete(config: DeleteConfig): Promise<void> {}\n  /** Clean up side-effects */\n  async destroy() {}\n  /** Download code */\n  async download(config: DownloadConfig): Promise<void> {}\n  /** Called on browser start or config changes */\n  onStart() {}\n}\n\ntype SyncServiceAbstractClass = typeof SyncService\nexport interface SyncServiceConstructor extends SyncServiceAbstractClass {}\n"
  },
  {
    "path": "src/background/sync-manager/services/ankiconnect/_locales/en.ts",
    "content": "import { locale as _locale } from './zh-CN'\n\nexport const locale: typeof _locale = {\n  title: 'Anki Connect',\n  error: {\n    server: 'Cannot connect to Anki Connect. Please make sure Anki is running.',\n    deck: 'Deck not found in Anki.',\n    notetype: 'Note type not found in Anki.',\n    add: 'Failed to add word to Anki.'\n  }\n}\n"
  },
  {
    "path": "src/background/sync-manager/services/ankiconnect/_locales/zh-CN.ts",
    "content": "export const locale = {\n  title: 'Anki Connect',\n  error: {\n    server: '无法连接 Anki Connect，请确认 Anki 已在运行。',\n    deck: 'Anki 中没有找到相应牌组。',\n    notetype: 'Anki 中没有找到相应笔记类型。',\n    add: '添加单词到 Anki 失败。'\n  }\n}\n"
  },
  {
    "path": "src/background/sync-manager/services/ankiconnect/_locales/zh-TW.ts",
    "content": "import { locale as _locale } from './zh-CN'\n\nexport const locale: typeof _locale = {\n  title: 'Anki Connect',\n  error: {\n    server: '無法連線 Anki Connect，請確認 Anki 已在執行。',\n    deck: 'Anki 中沒有找到相應牌組。',\n    notetype: 'Anki 中沒有找到相應筆記型別。',\n    add: '新增單詞到 Anki 失敗。'\n  }\n}\n"
  },
  {
    "path": "src/background/sync-manager/services/ankiconnect/index.ts",
    "content": "import axios from 'axios'\nimport { Word } from '@/_helpers/record-manager'\nimport { parseCtxText } from '@/_helpers/translateCtx'\nimport { AddConfig, SyncService } from '../../interface'\nimport { getNotebook } from '../../helpers'\nimport { message } from '@/_helpers/browser-api'\nimport { Message } from '@/typings/message'\n\nexport interface SyncConfig {\n  enable: boolean\n  key: string | null\n  host: string\n  port: string\n  deckName: string\n  noteType: string\n  /** Note tags */\n  tags: string\n  escapeContext: boolean\n  escapeTrans: boolean\n  escapeNote: boolean\n  /** Sync to AnkiWeb after added */\n  syncServer: boolean\n}\n\nexport class Service extends SyncService<SyncConfig> {\n  static readonly id = 'ankiconnect'\n\n  static getDefaultConfig(): SyncConfig {\n    return {\n      enable: false,\n      host: '127.0.0.1',\n      port: '8765',\n      key: null,\n      deckName: 'Saladict',\n      noteType: 'Saladict Word',\n      tags: '',\n      escapeContext: true,\n      escapeTrans: true,\n      escapeNote: true,\n      syncServer: false\n    }\n  }\n\n  noteFileds: string[] | undefined\n\n  async init() {\n    if (!(await this.isServerUp())) {\n      throw new Error('server')\n    }\n\n    const decks = await this.request<string[]>('deckNames')\n    if (!decks?.includes(this.config.deckName)) {\n      throw new Error('deck')\n    }\n\n    const noteTypes = await this.request<string[]>('modelNames')\n    if (!noteTypes?.includes(this.config.noteType)) {\n      throw new Error('notetype')\n    }\n  }\n\n  handleMessage = (msg: Message) => {\n    switch (msg.type) {\n      case 'ANKI_CONNECT_FIND_WORD':\n        return this.findNote(msg.payload).catch(() => '')\n      case 'ANKI_CONNECT_UPDATE_WORD':\n        return this.updateWord(msg.payload.cardId, msg.payload.word).catch(e =>\n          Promise.reject(e)\n        )\n    }\n  }\n\n  onStart() {\n    message.addListener(this.handleMessage)\n  }\n\n  async destroy() {\n    message.removeListener(this.handleMessage)\n  }\n\n  async findNote(date: number): Promise<number | undefined> {\n    if (!this.noteFileds) {\n      this.noteFileds = await this.getNotefields()\n    }\n    try {\n      const notes = await this.request<number[]>('findNotes', {\n        query: `deck:${this.config.deckName} ${this.noteFileds[0]}:${date}`\n      })\n      return notes[0]\n    } catch (e) {\n      if (process.env.DEBUG) {\n        console.error(e)\n      }\n    }\n  }\n\n  async add({ words, force }: AddConfig) {\n    if (!(await this.isServerUp())) {\n      throw new Error('server')\n    }\n\n    if (force) {\n      words = await getNotebook()\n    }\n\n    if (!words || words.length <= 0) {\n      return\n    }\n\n    await Promise.all(\n      words.map(async word => {\n        if (!(await this.findNote(word.date))) {\n          try {\n            await this.addWord(word)\n          } catch (e) {\n            if (process.env.DEBUG) {\n              console.warn(e)\n            }\n            throw new Error('add')\n          }\n        }\n      })\n    )\n\n    if (this.config.syncServer) {\n      try {\n        await this.request('sync')\n      } catch (e) {\n        if (process.env.DEBUG) {\n          console.warn(e)\n        }\n      }\n    }\n  }\n\n  async addWord(word: Readonly<Word>) {\n    return this.request<number | null>('addNote', {\n      note: {\n        deckName: this.config.deckName,\n        modelName: this.config.noteType,\n        options: {\n          allowDuplicate: false,\n          duplicateScope: 'deck'\n        },\n        tags: this.extractTags(),\n        fields: await this.wordToFields(word)\n      }\n    })\n  }\n\n  async updateWord(noteId: number, word: Readonly<Word>) {\n    return this.request('updateNoteFields', {\n      note: {\n        id: noteId,\n        fields: await this.wordToFields(word)\n      }\n    })\n  }\n\n  async addDeck() {\n    return this.request('createDeck', { deck: this.config.deckName })\n  }\n\n  async addNoteType() {\n    this.noteFileds = [\n      'Date',\n      'Text',\n      'Translation',\n      'Context',\n      'ContextCloze',\n      'Note',\n      'Title',\n      'Url',\n      'Favicon',\n      'Audio'\n    ]\n\n    await this.request('createModel', {\n      modelName: this.config.noteType,\n      inOrderFields: this.noteFileds,\n      css: cardCss(),\n      cardTemplates: [\n        {\n          Name: 'Saladict Cloze',\n          Front: cardText(true, this.noteFileds),\n          Back: cardText(false, this.noteFileds)\n        }\n      ]\n    })\n\n    // Anki Connect could tranlate the field names\n    // Update again\n    this.noteFileds = await this.getNotefields()\n    await this.request('updateModelTemplates', {\n      model: {\n        name: this.config.noteType,\n        templates: {\n          'Saladict Cloze': {\n            Front: cardText(true, this.noteFileds),\n            Back: cardText(false, this.noteFileds)\n          }\n        }\n      }\n    })\n  }\n\n  async request<R = void>(action: string, params?: any): Promise<R> {\n    const { data } = await axios({\n      method: 'post',\n      url: `http://${this.config.host}:${this.config.port}`,\n      data: {\n        key: this.config.key || null,\n        version: 6,\n        action,\n        params: params || {}\n      }\n    })\n\n    if (process.env.DEBUG) {\n      console.log(`Anki Connect ${action} response`, data)\n    }\n\n    if (!data || !Object.prototype.hasOwnProperty.call(data, 'result')) {\n      throw new Error('Deprecated Anki Connect version')\n    }\n\n    if (data.error) {\n      throw new Error(data.error)\n    }\n\n    return data.result\n  }\n\n  async wordToFields(word: Readonly<Word>): Promise<{ [k: string]: string }> {\n    if (!this.noteFileds) {\n      this.noteFileds = await this.getNotefields()\n    }\n    return {\n      // Date\n      [this.noteFileds[0]]: `${word.date}`,\n      // Text\n      [this.noteFileds[1]]: word.text || '',\n      // Translation\n      [this.noteFileds[2]]: this.parseTrans(\n        word.trans,\n        this.config.escapeTrans\n      ),\n      // Context\n      [this.noteFileds[3]]: this.multiline(\n        word.context,\n        this.config.escapeContext\n      ),\n      // ContextCloze\n      [this.noteFileds[4]]:\n        this.multiline(\n          word.context.split(word.text).join(`{{c1::${word.text}}}`),\n          this.config.escapeContext\n        ) || `{{c1::${word.text}}}`,\n      // Note\n      [this.noteFileds[5]]: this.multiline(word.note, this.config.escapeNote),\n      // Title\n      [this.noteFileds[6]]: word.title || '',\n      // Url\n      [this.noteFileds[7]]: word.url || '',\n      // Favicon\n      [this.noteFileds[8]]: word.favicon || '',\n      // Audio\n      [this.noteFileds[9]]: '' // @TODO\n    }\n  }\n\n  async getNotefields(): Promise<string[]> {\n    const nf = await this.request<string[]>('modelFieldNames', {\n      modelName: this.config.noteType\n    })\n\n    // Anki connect bug\n    return nf?.includes('Date.')\n      ? [\n          'Date.',\n          'Text.',\n          'Translation.',\n          'Context.',\n          'ContextCloze.',\n          'Note.',\n          'Title.',\n          'Url.',\n          'Favicon.',\n          'Audio.'\n        ]\n      : nf?.includes('日期')\n      ? [\n          '日期',\n          '文字',\n          'Translation',\n          'Context',\n          'ContextCloze',\n          '笔记',\n          'Title',\n          'Url',\n          'Favicon',\n          'Audio'\n        ]\n      : [\n          'Date',\n          'Text',\n          'Translation',\n          'Context',\n          'ContextCloze',\n          'Note',\n          'Title',\n          'Url',\n          'Favicon',\n          'Audio'\n        ]\n  }\n\n  multiline(text: string, escape: boolean): string {\n    text = text.trim()\n    if (!text) return ''\n    if (escape) {\n      text = this.escapeHTML(text)\n    }\n    return text.trim().replace(/\\n/g, '<br/>')\n  }\n\n  parseTrans(text: string, escape: boolean): string {\n    text = text.trim()\n    if (!text) return ''\n    const ctx = parseCtxText(text)\n    const ids = Object.keys(ctx)\n    if (ids.length <= 0) {\n      return this.multiline(text, escape)\n    }\n\n    const trans = ids\n      .map(\n        id =>\n          `<span class=\"trans_title\">${id}</span><div class=\"trans_content\">${ctx[id]}</div>`\n      )\n      .join('')\n    return text\n      .split(/\\[:: \\w+ ::\\](?:[\\s\\S]+?)(?:-{15})/)\n      .map(text => this.multiline(text, escape))\n      .join(`<div class=\"trans\">${trans}</div>`)\n  }\n\n  private _div: HTMLElement | undefined\n  escapeHTML(text: string): string {\n    if (!this._div) {\n      this._div = document.createElement('div')\n      this._div.appendChild(document.createTextNode(''))\n    }\n    this._div.firstChild!.nodeValue = text\n    return this._div.innerHTML\n  }\n\n  extractTags(): string[] {\n    return this.config.tags\n      .split(/,|，/)\n      .map(t => t.trim())\n      .filter(Boolean)\n  }\n\n  async isServerUp(): Promise<boolean> {\n    try {\n      return (await this.request<number>('version')) != null\n    } catch (e) {\n      if (process.env.DEBUG) {\n        console.error(e)\n      }\n      return false\n    }\n  }\n}\n\nfunction cardText(front: boolean, nf: string[]) {\n  return `{{#${nf[4]}}}\n<section>{{cloze:${nf[4]}}}</section>\n<section>{{type:cloze:${nf[4]}}}</section>\n{{#${nf[2]}}}\n<section>{{${nf[2]}}}</section>\n{{/${nf[2]}}}\n{{/${nf[4]}}}\n\n{{^${nf[4]}}}\n<h1>{{${nf[1]}}}</h1>\n{{#${nf[2]}}}\n<section>{{${nf[2]}}}</section>\n{{/${nf[2]}}}\n{{/${nf[4]}}}\n\n{{#${nf[5]}}}\n<section>{{${(front ? 'hint:' : '') + nf[5]}}}</section>\n{{/${nf[5]}}}\n\n{{#${nf[6]}}}\n<section class=\"tsource\">\n<hr />\n{{#${nf[8]}}}\n<span class=\"favicon\" style=\"background-image:url({{${nf[8]}}})\"></span>\n{{/${nf[8]}}}\n<a href=\"{{${nf[7]}}}\">{{${nf[6]}}}</a>\n</section>\n{{/${nf[6]}}}\n`\n}\n\nfunction cardCss() {\n  return `.card {\n  font-family: arial;\n  font-size: 20px;\n  text-align: center;\n  color: #333;\n  background-color: white;\n}\n\na {\n  color: #5caf9e;\n}\n\ninput {\n  border: 1px solid #eee;\n}\n\nsection {\n  margin: 1em 0;\n}\n\n.trans {\n  border: 1px solid #eee;\n  padding: 0.5em;\n}\n\n.trans_title {\n  display: block;\n  font-size: 0.9em;\n  font-weight: bold;\n}\n\n.trans_content {\n  margin-bottom: 0.5em;\n}\n\n.cloze {\n  font-weight: bold;\n  color: #f9690e;\n}\n\n.tsource {\n  position: relative;\n  font-size: .8em;\n}\n\n.tsource img {\n  height: .7em;\n}\n\n.tsource a {\n  text-decoration: none;\n}\n\n.typeGood {\n  color: #fff;\n  background: #1EBC61;\n}\n\n.typeBad {\n  color: #fff;\n  background: #F75C4C;\n}\n\n.typeMissed {\n  color: #fff;\n  background: #7C8A99;\n}\n\n.favicon {\n  display: inline-block;\n  width: 1em;\n  height: 1em;\n  background: center/cover no-repeat;\n}\n`\n}\n"
  },
  {
    "path": "src/background/sync-manager/services/eudic/_locales/en.ts",
    "content": "import { locale as _locale } from './zh-CN'\n\nexport const locale: typeof _locale = {\n  title: 'Eudic Word Syncing',\n  open: 'Open',\n  error: {\n    network:\n      'Unable to access the new word book of Eudic, please check the network.',\n    illegal_token: 'Please set legal Eudic authorization information.',\n    no_wordbook:\n      'Unable to add to the new word book of European dictionary. Please go to the European official website to generate the default new word book first.'\n  }\n}\n"
  },
  {
    "path": "src/background/sync-manager/services/eudic/_locales/zh-CN.ts",
    "content": "export const locale = {\n  title: '欧路单词同步',\n  open: '打开',\n  error: {\n    network: '无法访问欧路词典生词本，请检查网络。',\n    illegal_token: '请设置合法的欧路词典授权信息',\n    no_wordbook: '无法添加到欧路词典生词本，请先上欧路官网生成默认生词本'\n  }\n}\n"
  },
  {
    "path": "src/background/sync-manager/services/eudic/_locales/zh-TW.ts",
    "content": "import { locale as _locale } from './zh-CN'\n\nexport const locale: typeof _locale = {\n  title: '歐路單字同步',\n  open: '開啟',\n  error: {\n    network: '無法訪問歐路詞典生詞本，請檢查網絡。',\n    illegal_token: '請設定合法的歐路詞典授權資訊',\n    no_wordbook: '無法添加到歐路詞典生詞本，請先上歐路官網生成默認生詞本'\n  }\n}\n"
  },
  {
    "path": "src/background/sync-manager/services/eudic/index.ts",
    "content": "import { AddConfig, SyncService } from '../../interface'\nimport { getNotebook } from '../../helpers'\nimport axios from 'axios'\n\nexport interface SyncConfig {\n  enable: boolean\n  token: string\n  syncAll: boolean\n}\n\ninterface Books {\n  id: string\n  language: string\n  name: string\n}\n\nexport class Service extends SyncService<SyncConfig> {\n  static readonly id = 'eudic'\n\n  static getDefaultConfig(): SyncConfig {\n    return {\n      enable: false,\n      token: '',\n      syncAll: false\n    }\n  }\n\n  async init() {\n    const wordbooks = await this.getWordbooks<Books[]>()\n\n    if (wordbooks.length === 0 || !wordbooks) {\n      throw new Error('no_wordbook')\n    }\n  }\n\n  async add(config: AddConfig) {\n    await this.addWordOrPatch(config)\n  }\n\n  /**\n   * sync a word or patch words\n   */\n  async addWordOrPatch({ words, force }: AddConfig) {\n    if (!this.config.enable) {\n      return 0\n    }\n\n    if (force) {\n      words = await getNotebook()\n    }\n\n    if (!words || words.length <= 0) {\n      return 0\n    }\n\n    const payload = force ? words.map(word => word.text) : words[0].text\n\n    await this.requestAddWords(payload)\n  }\n\n  /**\n   * get the user's wordbooks and judge the correctness of the authorization information\n   */\n  async getWordbooks<R = void>(): Promise<R> {\n    const result = await axios({\n      method: 'get',\n      url: `https://api.frdic.com/api/open/v1/studylist/category`,\n      params: {\n        language: 'en'\n      },\n      headers: {\n        Authorization: this.config.token\n      }\n    }).catch(e => {\n      if (e.response && e.response.status === 401) {\n        throw new Error('illegal_token')\n      } else {\n        throw new Error('network')\n      }\n    })\n    if (!result?.data) {\n      throw new Error('network')\n    }\n    const { data } = result\n\n    if (process.env.DEBUG) {\n      console.log(`Eudic Connect response(wordbook list)`, data)\n    }\n\n    if (!data || !Object.prototype.hasOwnProperty.call(data, 'data')) {\n      throw new Error('network')\n    }\n\n    return data.data\n  }\n\n  async requestAddWords(words: string | string[]) {\n    return await axios({\n      method: 'post',\n      url: `https://api.frdic.com/api/open/v1/studylist/words`,\n      data: {\n        id: '0', // id of default wordbook\n        language: 'en',\n        words: typeof words === 'string' ? [words] : words\n      },\n      headers: {\n        Authorization: this.config.token\n      }\n    }).catch(e => {\n      if (process.env.DEBUG) {\n        console.error(e)\n      }\n      if (e.response && e.response.status === 401) {\n        throw new Error('illegal_token')\n      } else {\n        throw new Error('network')\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "src/background/sync-manager/services/shanbay/_locales/en.ts",
    "content": "import { locale as _locale } from './zh-CN'\n\nexport const locale: typeof _locale = {\n  title: 'Shanbay Word Syncing',\n  open: 'Open',\n  error: {\n    login: 'Shanbay login failed. Click to open shanbay.com.',\n    network:\n      'Unable to access shanbay.com. Please check your network connection.',\n    word:\n      \"Unable to add to Shanbay notebook. This word is not in Shanbay's vocabulary database.\"\n  }\n}\n"
  },
  {
    "path": "src/background/sync-manager/services/shanbay/_locales/zh-CN.ts",
    "content": "export const locale = {\n  title: '扇贝单词同步',\n  open: '打开',\n  error: {\n    login: '扇贝登录已失效，请点击打开官网重新登录。',\n    network: '无法访问扇贝生词本，请检查网络。',\n    word: '无法添加到扇贝生词本，扇贝单词库没有收录此单词。'\n  }\n}\n"
  },
  {
    "path": "src/background/sync-manager/services/shanbay/_locales/zh-TW.ts",
    "content": "import { locale as _locale } from './zh-CN'\n\nexport const locale: typeof _locale = {\n  title: '扇貝單字同步',\n  open: '開啟',\n  error: {\n    login: '扇貝登入已失效，請點選開啟官網重新登入。',\n    network: '無法訪問扇貝生字本，請檢查網路。',\n    word: '無法新增到扇貝生字本，扇貝單字庫沒有收錄此單字。'\n  }\n}\n"
  },
  {
    "path": "src/background/sync-manager/services/shanbay/index.ts",
    "content": "import { AddConfig, SyncService } from '../../interface'\nimport { getNotebook, notifyError } from '../../helpers'\nimport { openUrl } from '@/_helpers/browser-api'\nimport { timer } from '@/_helpers/promise-more'\nimport { isFirefox } from '@/_helpers/saladict'\nimport { I18nManager } from '@/background/i18n-manager'\n\nexport interface SyncConfig {\n  enable: boolean\n}\n\nexport class Service extends SyncService<SyncConfig> {\n  static readonly id = 'shanbay'\n\n  static getDefaultConfig(): SyncConfig {\n    return {\n      enable: false\n    }\n  }\n\n  static openLogin() {\n    return openUrl('https://www.shanbay.com/web/account/login')\n  }\n\n  async init() {\n    if (!(await this.isLogin())) {\n      throw new Error('login')\n    }\n  }\n\n  async add(config: AddConfig) {\n    await this.addInternal(config)\n  }\n\n  /**\n   * @returns failed words\n   */\n  async addInternal({ words, force }: AddConfig): Promise<number> {\n    if (!this.config.enable) {\n      return 0\n    }\n\n    if (!(await this.isLogin())) {\n      this.notifyLogin()\n      return 0\n    }\n\n    if (force) {\n      words = await getNotebook()\n    }\n\n    if (!words || words.length <= 0) {\n      return 0\n    }\n\n    let errorCount = 0\n\n    for (let i = 0; i < words.length; i++) {\n      try {\n        await this.addWord(words[i].text)\n      } catch (error) {\n        if (error.message !== 'word') {\n          throw error\n        }\n        errorCount += 1\n        notifyError(Service.id, 'word', `「${words[i].text}」`)\n      }\n\n      if ((i + 1) % 50 === 0) {\n        await timer(15 * 60000)\n      } else {\n        await timer(500)\n      }\n    }\n\n    return errorCount\n  }\n\n  async addWord(text: string) {\n    let word: { id: string } | undefined\n    try {\n      const url =\n        'https://apiv3.shanbay.com/abc/words/senses?vocabulary_content=' +\n        encodeURIComponent(text)\n      word = await fetch(url).then(r => r.json())\n    } catch (e) {\n      throw new Error('network')\n    }\n\n    if (!word || !word.id) {\n      throw new Error('word')\n    }\n\n    let uploadResult\n\n    try {\n      uploadResult = await fetch(\n        'https://apiv3.shanbay.com/wordscollection/words',\n        {\n          method: 'POST',\n          headers: {\n            'Content-Type': 'application/json'\n          },\n          body: JSON.stringify({\n            vocab_id: word.id,\n            business_id: 6\n          })\n        }\n      ).then(r => r.json())\n    } catch (e) {\n      if (process.env.DEBUG) {\n        console.error(e)\n      }\n      throw new Error('network')\n    }\n\n    if (!uploadResult || !uploadResult.created_at) {\n      throw new Error('word')\n    }\n  }\n\n  async isLogin(): Promise<boolean> {\n    return Boolean(\n      await browser.cookies.get({\n        url: 'http://www.shanbay.com',\n        name: 'auth_token'\n      })\n    )\n  }\n\n  async notifyLogin() {\n    const { i18n } = await I18nManager.getInstance()\n    await i18n.loadNamespaces('sync')\n\n    if (browser.notifications) {\n      browser.notifications.onClicked.addListener(handleLoginNotification)\n      browser.notifications.onClosed.removeListener(removeNotificationHandler)\n      if (browser.notifications.onButtonClicked) {\n        browser.notifications.onButtonClicked.addListener(\n          handleLoginNotification\n        )\n      }\n\n      const options: browser.notifications.CreateNotificationOptions = {\n        type: 'basic',\n        iconUrl: browser.runtime.getURL(`assets/icon-128.png`),\n        title: `Saladict ${i18n.t(`sync:shanbay.title`)}`,\n        message: i18n.t('sync:shanbay.error.login'),\n        eventTime: Date.now() + 10000,\n        priority: 2\n      }\n\n      if (!isFirefox) {\n        options.buttons = [{ title: i18n.t('sync:shanbay.open') }]\n      }\n\n      browser.notifications.create('shanbay-login', options)\n    }\n  }\n}\n\nfunction handleLoginNotification(id: string) {\n  if (id === 'shanbay-login') {\n    Service.openLogin()\n    removeNotificationHandler(id)\n  }\n}\n\nfunction removeNotificationHandler(id: string) {\n  if (id === 'shanbay-login') {\n    if (browser.notifications) {\n      browser.notifications.onClicked.removeListener(handleLoginNotification)\n      browser.notifications.onClosed.removeListener(removeNotificationHandler)\n      if (browser.notifications.onButtonClicked) {\n        browser.notifications.onButtonClicked.removeListener(\n          handleLoginNotification\n        )\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/background/sync-manager/services/webdav/_locales/en.ts",
    "content": "import { locale as _locale } from './zh-CN'\n\nexport const locale: typeof _locale = {\n  title: 'WebDAV Word Syncing',\n  error: {\n    dir: 'Incorrect \"Saladict\" directory on server.',\n    download:\n      'Download failed. Unable to connect WebDAV Server. If browser proxy is enabled please adjust rules to bypass WebDAV server.',\n    internal: 'Unable to save settings.',\n    missing: 'Missing \"Saladict\" directory on server.',\n    mkcol:\n      'Cannot create \"Saladict\" directory on server. Please create the directory manualy on server.',\n    network: 'Network error. Cannot connect to server.',\n    parse: 'Incorrect response XML from server.',\n    unauthorized: 'Incorrect account or password.'\n  }\n}\n"
  },
  {
    "path": "src/background/sync-manager/services/webdav/_locales/zh-CN.ts",
    "content": "export const locale = {\n  title: 'WebDAV 单词同步',\n  error: {\n    dir: '服务器上“Saladict”目录格式不正确，请检查。',\n    download:\n      '下载失败。无法访问 WebDAV 服务器。如启用了浏览器代理请调整规则，不要代理 WebDAV 服务器。',\n    internal: '无法保存。',\n    missing: '服务器上缺少“Saladict”目录。',\n    mkcol: '无法在服务器创建“Saladict”目录。请手动在服务器上创建。',\n    network: '连接服务器失败。',\n    parse: '服务器返回 XML 格式不正确。',\n    unauthorized: '账户或密码不正确。'\n  }\n}\n"
  },
  {
    "path": "src/background/sync-manager/services/webdav/_locales/zh-TW.ts",
    "content": "import { locale as _locale } from './zh-CN'\n\nexport const locale: typeof _locale = {\n  title: 'WebDAV 單字同步',\n  error: {\n    dir: '伺服器上“Saladict”目錄格式不正確，請檢查。',\n    download:\n      '下載失敗。無法訪問 WebDAV 伺服器。如啟用了瀏覽器代理請調整規則，不要代理 WebDAV 伺服器。',\n    internal: '無法儲存。',\n    missing: '伺服器上缺少“Saladict”目錄。',\n    mkcol: '無法在伺服器建立“Saladict”目錄。請手動在伺服器上建立。',\n    network: '連線伺服器失敗。',\n    parse: '伺服器返回 XML 格式不正確。',\n    unauthorized: '帳戶或密碼不正確。'\n  }\n}\n"
  },
  {
    "path": "src/background/sync-manager/services/webdav/index.ts",
    "content": "import {\n  NotebookFile,\n  AddConfig,\n  DownloadConfig,\n  SyncService,\n  SyncServiceConfigBase\n} from '../../interface'\nimport {\n  getNotebook,\n  setNotebook,\n  setMeta,\n  getMeta,\n  notifyError\n} from '../../helpers'\n\nimport { Mutable } from '@/typings/helpers'\nimport { storage } from '@/_helpers/browser-api'\n\nexport interface SyncConfig extends SyncServiceConfigBase {\n  /** Server address. Ends with '/'. */\n  readonly url: string\n  readonly user: string\n  readonly passwd: string\n  /** In min */\n  readonly duration: number\n}\n\nexport interface SyncMeta {\n  readonly etag?: string\n  readonly timestamp?: number\n}\n\nexport class Service extends SyncService<SyncConfig, SyncMeta> {\n  static readonly id = 'webdav'\n\n  static getDefaultConfig(): SyncConfig {\n    return {\n      enable: false,\n      url: '',\n      user: '',\n      passwd: '',\n      duration: 15\n    }\n  }\n\n  meta: SyncMeta = {}\n\n  async onStart() {\n    if (process.env.DEBUG) {\n      console.log(`Sync Service WebDAV starts interval.`)\n    }\n\n    if (!this.config.enable) {\n      if (process.env.DEBUG) {\n        console.warn(`Sync Service WebDAV already started.`)\n      }\n      return\n    }\n\n    this.meta = (await getMeta(Service.id)) || this.meta\n\n    await browser.alarms.clear('webdav')\n\n    browser.alarms.onAlarm.addListener(this.handleSyncAlarm)\n\n    if (typeof this.config.url === 'string' && !this.config.url.endsWith('/')) {\n      ;(this.config as Mutable<SyncConfig>).url += '/'\n    }\n\n    if (this.config.url) {\n      const duration = +this.config.duration || 15\n      const now = Date.now()\n      let nextInterval: number = +(await storage.local.get('webdavInterval'))\n        .webdavInterval\n      if (\n        !nextInterval ||\n        nextInterval < now ||\n        now + duration * 60000 < nextInterval\n      ) {\n        nextInterval = now + 1000\n      }\n      await storage.local.set({ webdavInterval: nextInterval })\n      browser.alarms.create('webdav', {\n        when: nextInterval,\n        periodInMinutes: duration\n      })\n    } else {\n      await storage.local.set({ webdavInterval: 0 })\n    }\n  }\n\n  handleSyncAlarm = async (alarm: browser.alarms.Alarm) => {\n    if (alarm.name !== 'webdav') {\n      return\n    }\n\n    if (process.env.DEBUG) {\n      console.log('Sync Service WebDAV Interval Alarm triggered.')\n    }\n\n    try {\n      await this.download({})\n    } catch (e) {\n      console.error(e)\n      notifyError(Service.id, 'download')\n    }\n\n    const duration = this.config.duration * 60000 || 15 * 60000\n    await storage.local.set({ webdavInterval: Date.now() + duration })\n  }\n\n  async checkDir(): Promise<boolean> {\n    let text = ''\n    try {\n      const response = await fetch(this.config.url, {\n        method: 'PROPFIND',\n        headers: {\n          Authorization:\n            'Basic ' + window.btoa(`${this.config.user}:${this.config.passwd}`),\n          'Content-Type': 'application/xml; charset=\"utf-8\"',\n          Depth: '1'\n        }\n      })\n      if (!response.ok) {\n        if (response.status === 401) {\n          throw new Error('unauthorized')\n        }\n        throw new Error(`Network error: ${response.status}`)\n      }\n      text = await response.text()\n    } catch (e) {\n      throw new Error('network')\n    }\n\n    let doc: Document | undefined\n    try {\n      if (text) {\n        doc = new DOMParser().parseFromString(text, 'text/xml')\n      }\n    } catch (e) {\n      throw new Error('parse')\n    }\n\n    if (!doc) {\n      throw new Error('parse')\n    }\n\n    const $responses = Array.from(doc.querySelectorAll('response'))\n    for (const i in $responses) {\n      const href = $responses[i].querySelector('href')\n      if (href && href.textContent && href.textContent.endsWith('/Saladict/')) {\n        // is Saladict\n        if ($responses[i].querySelector('resourcetype collection')) {\n          // is collection\n          return true\n        } else {\n          throw new Error('dir')\n        }\n      }\n    }\n\n    return false\n  }\n\n  /**\n   * Check server and create a Saladict Directory if not exist.\n   */\n  async init() {\n    const dir = await this.checkDir()\n\n    if (!dir) {\n      // create directory\n      const response = await fetch(this.config.url + 'Saladict', {\n        method: 'MKCOL',\n        headers: {\n          Authorization:\n            'Basic ' + window.btoa(`${this.config.user}:${this.config.passwd}`)\n        }\n      })\n      if (!response.ok) {\n        // cannot create directory\n        throw new Error('mkcol')\n      }\n    }\n\n    if (dir) {\n      try {\n        await this.download({ testConfig: this.config, noCache: true })\n      } catch (e) {\n        // Download failed, which is desired.\n        return\n      }\n      // An old file exists on server.\n      // Let user decide whether to upload.\n      throw new Error('exist')\n    }\n  }\n\n  async add({ force }: AddConfig) {\n    if (!this.config.url) {\n      if (process.env.DEBUG) {\n        console.warn(`sync service ${Service.id} upload: empty url`)\n      }\n      return\n    }\n\n    if (!force) {\n      await this.download({})\n    }\n\n    const words = await getNotebook()\n    if (!words || words.length <= 0) {\n      return\n    }\n\n    const timestamp = Date.now()\n\n    try {\n      var body = JSON.stringify({ timestamp, words } as NotebookFile)\n    } catch (e) {\n      if (process.env.DEBUG) {\n        console.error('WebDAV: Stringify notebook failed', words)\n      }\n      throw new Error('parse')\n    }\n\n    try {\n      const response = await fetch(this.config.url + 'Saladict/notebook.json', {\n        method: 'PUT',\n        headers: {\n          Authorization:\n            'Basic ' + window.btoa(`${this.config.user}:${this.config.passwd}`)\n        },\n        body\n      })\n      if (!response.ok) {\n        throw new Error('network')\n      }\n    } catch (e) {\n      if (process.env.DEBUG) {\n        console.error('WebDAV: upload failed', e)\n      }\n      throw new Error('network')\n    }\n\n    await this.setMeta({ timestamp, etag: '' })\n  }\n\n  delete({ force }) {\n    // full sync anyway\n    return this.add({ force })\n  }\n\n  async download({ testConfig, noCache }: DownloadConfig): Promise<void> {\n    const config = testConfig || this.config\n\n    if (!config.url) {\n      if (process.env.DEBUG) {\n        console.warn(`sync service ${Service.id} download: empty url`)\n      }\n      return\n    }\n\n    const headers: { [name: string]: string } = {\n      Authorization: 'Basic ' + window.btoa(`${config.user}:${config.passwd}`)\n    }\n    if (!testConfig && !noCache && this.meta.etag != null) {\n      headers['If-None-Match'] = this.meta.etag\n      headers['If-Modified-Since'] = this.meta.etag\n    }\n\n    try {\n      var response = await fetch(\n        config.url +\n          (config.url.endsWith('/') ? '' : '/') +\n          'Saladict/notebook.json',\n        {\n          method: 'GET',\n          headers\n        }\n      )\n\n      if (response.status === 304) {\n        return\n      }\n\n      if (!response.ok) {\n        throw new Error()\n      }\n    } catch (e) {\n      if (process.env.DEBUG) {\n        console.error(e)\n      }\n      throw new Error('network')\n    }\n\n    try {\n      var json: NotebookFile = await response.json()\n    } catch (e) {\n      if (process.env.DEBUG) {\n        console.error('Fetch webdav notebook.json error', response)\n      }\n      throw new Error('parse')\n    }\n\n    if (process.env.DEBUG) {\n      if (!response.headers.get('ETag')) {\n        console.warn('webdav notebook.json no etag', response)\n      }\n    }\n\n    if (!Array.isArray(json.words) || json.words.some(w => !w.date)) {\n      if (process.env.DEBUG) {\n        console.error('Parse webdav notebook.json error: incorrect words', json)\n      }\n      throw new Error('format')\n    }\n\n    if (!json.timestamp) {\n      if (process.env.DEBUG) {\n        console.error('webdav notebook.json no timestamp', json)\n      }\n      throw new Error('timestamp')\n    }\n\n    if (testConfig) {\n      // connectivity is ok\n      return\n    }\n\n    const oldMeta = this.meta\n\n    if (!oldMeta.timestamp || json.timestamp >= oldMeta.timestamp) {\n      await this.setMeta({\n        timestamp: json.timestamp,\n        etag: response.headers.get('ETag') || oldMeta.etag || ''\n      })\n    }\n\n    if (!noCache && oldMeta.timestamp && json.timestamp <= oldMeta.timestamp) {\n      // older file\n      return\n    }\n\n    await setNotebook(json.words)\n\n    if (process.env.DEBUG) {\n      console.log('Webdav download', json)\n    }\n  }\n\n  async destroy() {\n    browser.alarms.onAlarm.removeListener(this.handleSyncAlarm)\n    await browser.alarms.clear('webdav')\n  }\n\n  setMeta(meta: SyncMeta) {\n    this.meta = meta\n    return setMeta(Service.id, meta)\n  }\n\n  async getMeta() {\n    const meta = await getMeta<SyncMeta>(Service.id)\n    this.meta = meta || ({} as SyncMeta)\n  }\n}\n"
  },
  {
    "path": "src/background/types.ts",
    "content": "import { AppConfig } from '@/app-config'\nimport { Profile, ProfileIDList } from '@/app-config/profiles'\n\ndeclare global {\n  interface Window {\n    appConfig: AppConfig\n    activeProfile: Profile\n    profileIDList: ProfileIDList\n  }\n}\n"
  },
  {
    "path": "src/background/windows-manager.ts",
    "content": "import { message, storage } from '@/_helpers/browser-api'\nimport { Word } from '@/_helpers/record-manager'\nimport { isFirefox } from '@/_helpers/saladict'\nimport { getTitlebarOffset } from '@/_helpers/titlebar-offset'\n\ninterface WinRect {\n  width: number\n  height: number\n  left: number\n  top: number\n}\n\nconst safeUpdateWindow: typeof browser.windows.update = (...args) =>\n  browser.windows.update(...args).catch(console.warn as (m: any) => undefined)\n\n/**\n * Manipulate main window\n */\nexport class MainWindowsManager {\n  /** Main window snapshot */\n  snapshot: browser.windows.Window | null = null\n\n  async correctTop(originTop?: number) {\n    if (!originTop) return originTop\n\n    const offset = await getTitlebarOffset()\n    if (!offset) return originTop\n\n    return originTop - offset.main\n  }\n\n  async focus(): Promise<void> {\n    if (this.snapshot && this.snapshot.id != null) {\n      await safeUpdateWindow(this.snapshot.id, { focused: true })\n    }\n  }\n\n  async takeSnapshot(): Promise<browser.windows.Window | null> {\n    this.snapshot = null\n\n    try {\n      const win = await browser.windows.getLastFocused({\n        windowTypes: ['normal']\n      })\n      if (win.focused && win.type === 'normal' && win.state !== 'minimized') {\n        this.snapshot = win\n      } else if (isFirefox) {\n        // Firefox does not support windowTypes in getLastFocused\n        const wins = (await browser.windows.getAll()).filter(\n          win =>\n            win.focused && win.type === 'normal' && win.state !== 'minimized'\n        )\n        if (wins.length === 1) {\n          this.snapshot = wins[0]\n        } else {\n          const focusedWins = wins.filter(win => win.focused)\n          if (focusedWins.length === 1) {\n            this.snapshot = focusedWins[0]\n          }\n        }\n      }\n    } catch (e) {\n      console.warn(e)\n    }\n\n    return this.snapshot\n  }\n\n  destroySnapshot(): void {\n    this.snapshot = null\n  }\n\n  async makeRoomForSidebar(\n    side: 'left' | 'right',\n    sidebarSnapshot: browser.windows.Window | null\n  ): Promise<void> {\n    const mainWin = this.snapshot\n\n    if (!mainWin || mainWin.id == null) {\n      return\n    }\n\n    const sidebarWidth =\n      (sidebarSnapshot && sidebarSnapshot.width) || window.appConfig.panelWidth\n\n    const updateInfo =\n      mainWin.top != null &&\n      mainWin.left != null &&\n      mainWin.width != null &&\n      mainWin.height != null\n        ? {\n            top: await this.correctTop(mainWin.top),\n            left: side === 'right' ? mainWin.left : mainWin.left + sidebarWidth,\n            width: mainWin.width - sidebarWidth,\n            height: mainWin.height,\n            state: 'normal' as 'normal'\n          }\n        : {\n            top: 0,\n            left: side === 'right' ? 0 : sidebarWidth,\n            width: window.screen.availWidth - sidebarWidth,\n            height: window.screen.availHeight,\n            state: 'normal' as 'normal'\n          }\n\n    if (side === 'right') {\n      // fix a chrome bug by moving 1 extra pixal then to 0\n      await safeUpdateWindow(mainWin.id, {\n        ...updateInfo,\n        left: updateInfo.left + 1\n      })\n    }\n\n    await safeUpdateWindow(mainWin.id, updateInfo)\n  }\n\n  async restoreSnapshot(): Promise<void> {\n    if (this.snapshot && this.snapshot.id != null) {\n      await safeUpdateWindow(\n        this.snapshot.id,\n        this.snapshot.state === 'normal'\n          ? {\n              top: await this.correctTop(this.snapshot.top),\n              left: this.snapshot.left,\n              width: this.snapshot.width,\n              height: this.snapshot.height\n            }\n          : { state: this.snapshot.state }\n      )\n    }\n  }\n}\n\n/**\n * Manipulate Standalone Quick Search Panel\n */\nexport class QsPanelManager {\n  private qsPanelId: number | null = null\n  private snapshot: browser.windows.Window | null = null\n  private isSidebar: boolean = false\n  private mainWindowsManager = new MainWindowsManager()\n\n  async correctTop(originTop?: number) {\n    if (!originTop) return originTop\n\n    const offset = await getTitlebarOffset()\n    if (!offset) return originTop\n\n    return originTop - offset.panel\n  }\n\n  /**\n   * @param preload force preload word. otherwise let the panel decide.\n   */\n  async create(preload?: Word): Promise<void> {\n    this.isSidebar = false\n\n    let wordString = ''\n    let lastTabString = ''\n\n    if (preload) {\n      try {\n        wordString = '&word=' + encodeURIComponent(JSON.stringify(preload))\n      } catch (error) {\n        if (process.env.DEBUG) {\n          console.error(error)\n        }\n      }\n    } else {\n      if (window.appConfig.qsPreload === 'selection') {\n        const tab = (\n          await browser.tabs.query({\n            active: true,\n            lastFocusedWindow: true\n          })\n        )[0]\n        if (tab && tab.id) {\n          lastTabString = '&lastTab=' + tab.id\n        }\n      }\n    }\n\n    await this.mainWindowsManager.takeSnapshot()\n\n    const qsPanelRect = window.appConfig.qssaSidebar\n      ? await this.getSidebarRect(window.appConfig.qssaSidebar)\n      : (window.appConfig.qssaRectMemo && (await this.getStorageRect())) ||\n        this.getDefaultRect()\n\n    let qsPanelWin: browser.windows.Window | undefined\n\n    try {\n      qsPanelWin = await browser.windows.create({\n        ...qsPanelRect,\n        type: 'popup',\n        url: browser.runtime.getURL(\n          `quick-search.html?sidebar=${window.appConfig.qssaSidebar}${wordString}${lastTabString}`\n        )\n      })\n    } catch (err) {\n      browser.notifications.create({\n        type: 'basic',\n        iconUrl: browser.runtime.getURL(`assets/icon-128.png`),\n        title: `Saladict`,\n        message: err.message,\n        priority: 2,\n        eventTime: Date.now() + 5000\n      })\n    }\n\n    if (qsPanelWin && qsPanelWin.id) {\n      if (isFirefox) {\n        // Firefox needs an extra push\n        safeUpdateWindow(qsPanelWin.id, qsPanelRect)\n      }\n\n      this.qsPanelId = qsPanelWin.id\n\n      if (window.appConfig.qssaSidebar) {\n        this.isSidebar = true\n        await this.mainWindowsManager.makeRoomForSidebar(\n          window.appConfig.qssaSidebar,\n          qsPanelWin\n        )\n      }\n\n      if (!window.appConfig.qsFocus) {\n        await this.mainWindowsManager.focus()\n      }\n\n      // notify all tabs\n      ;(await browser.tabs.query({})).forEach(tab => {\n        if (tab.id && tab.windowId !== this.qsPanelId) {\n          message.send(tab.id, {\n            type: 'QS_PANEL_CHANGED',\n            payload: this.qsPanelId != null\n          })\n        }\n      })\n    }\n  }\n\n  async getWin(): Promise<browser.windows.Window | null> {\n    if (!this.qsPanelId) {\n      return null\n    }\n    return browser.windows.get(this.qsPanelId).catch(() => null)\n  }\n\n  async destroy(): Promise<void> {\n    ;(await browser.tabs.query({})).forEach(tab => {\n      if (tab.id && tab.windowId !== this.qsPanelId) {\n        message.send(tab.id, {\n          type: 'QS_PANEL_CHANGED',\n          payload: false\n        })\n      }\n    })\n\n    this.qsPanelId = null\n    this.isSidebar = false\n    this.destroySnapshot()\n    await this.mainWindowsManager.restoreSnapshot()\n    this.mainWindowsManager.destroySnapshot()\n  }\n\n  isQsPanel(winId?: number): boolean {\n    return winId != null && winId === this.qsPanelId\n  }\n\n  async hasCreated(): Promise<boolean> {\n    const win = await this.getWin()\n    if (!win) {\n      this.qsPanelId = null\n    }\n    return !!win\n  }\n\n  async focus(): Promise<void> {\n    if (this.qsPanelId != null) {\n      await safeUpdateWindow(this.qsPanelId, { focused: true })\n      const [tab] = await browser.tabs.query({ windowId: this.qsPanelId })\n      if (tab && tab.id) {\n        await message.send(tab.id, { type: 'QS_PANEL_FOCUSED' })\n      }\n    }\n  }\n\n  async takeSnapshot(): Promise<void> {\n    if (this.qsPanelId != null) {\n      this.snapshot = await browser.windows\n        .get(this.qsPanelId)\n        .catch(() => null)\n    }\n  }\n\n  destroySnapshot(): void {\n    this.snapshot = null\n  }\n\n  async restoreSnapshot(): Promise<void> {\n    // restore main window first so that it will be at the bottom\n    await this.mainWindowsManager.restoreSnapshot()\n    if (this.snapshot != null && this.snapshot.id != null) {\n      await safeUpdateWindow(this.snapshot.id, {\n        top: await this.correctTop(this.snapshot.top),\n        left: this.snapshot.left,\n        width: this.snapshot.width,\n        height: this.snapshot.height\n      })\n    } else if (this.qsPanelId != null) {\n      await safeUpdateWindow(this.qsPanelId, {\n        focused: true,\n        ...this.getDefaultRect()\n      })\n    }\n    this.destroySnapshot()\n  }\n\n  async moveToSidebar(side: 'left' | 'right'): Promise<void> {\n    if (this.qsPanelId != null) {\n      await this.takeSnapshot()\n      await safeUpdateWindow(this.qsPanelId, await this.getSidebarRect(side))\n      await this.mainWindowsManager.makeRoomForSidebar(side, this.snapshot)\n    }\n  }\n\n  async toggleSidebar(side: 'left' | 'right'): Promise<void> {\n    if (!(await this.hasCreated())) {\n      return\n    }\n\n    if (this.isSidebar) {\n      await this.restoreSnapshot()\n    } else {\n      await this.moveToSidebar(side)\n    }\n\n    this.isSidebar = !this.isSidebar\n  }\n\n  getDefaultRect(): WinRect {\n    const { qsLocation, qssaHeight } = window.appConfig\n\n    let qsPanelLeft = 10\n    let qsPanelTop = 30\n    const qsPanelWidth = window.appConfig.panelWidth\n    const qsPanelHeight = window.appConfig.qssaHeight\n\n    switch (qsLocation) {\n      case 'CENTER':\n        qsPanelLeft = (window.screen.availWidth - qsPanelWidth) / 2\n        qsPanelTop = (window.screen.availHeight - qssaHeight) / 2\n        break\n      case 'TOP':\n        qsPanelLeft = (window.screen.availWidth - qsPanelWidth) / 2\n        qsPanelTop = 30\n        break\n      case 'RIGHT':\n        qsPanelLeft = window.screen.availWidth - qsPanelWidth - 30\n        qsPanelTop = (window.screen.availHeight - qssaHeight) / 2\n        break\n      case 'BOTTOM':\n        qsPanelLeft = (window.screen.availWidth - qsPanelWidth) / 2\n        qsPanelTop = window.screen.availHeight - qsPanelHeight - 10\n        break\n      case 'LEFT':\n        qsPanelLeft = 10\n        qsPanelTop = (window.screen.availHeight - qssaHeight) / 2\n        break\n      case 'TOP_LEFT':\n        qsPanelLeft = 10\n        qsPanelTop = 30\n        break\n      case 'TOP_RIGHT':\n        qsPanelLeft = window.screen.availWidth - qsPanelWidth - 30\n        qsPanelTop = 30\n        break\n      case 'BOTTOM_LEFT':\n        qsPanelLeft = 10\n        qsPanelTop = window.screen.availHeight - qsPanelHeight - 10\n        break\n      case 'BOTTOM_RIGHT':\n        qsPanelLeft = window.screen.availWidth - qsPanelWidth - 30\n        qsPanelTop = window.screen.availHeight - qsPanelHeight - 10\n        break\n    }\n\n    // coords must be integer\n    // plus offset of other screen\n    return {\n      top: Math.round(qsPanelTop + (window.screen['availTop'] || 0)),\n      left: Math.round(qsPanelLeft + (window.screen['availLeft'] || 0)),\n      width: Math.round(qsPanelWidth),\n      height: Math.round(qsPanelHeight)\n    }\n  }\n\n  /** get saved panel rect */\n  async getStorageRect(): Promise<WinRect | null> {\n    const { qssaRect } = await storage.local.get<{ qssaRect: WinRect }>(\n      'qssaRect'\n    )\n    if (!qssaRect) return null\n    return {\n      ...qssaRect,\n      top: (await this.correctTop(qssaRect.top)) || 0\n    }\n  }\n\n  async getSidebarRect(side: 'left' | 'right'): Promise<WinRect> {\n    const panelWidth =\n      (this.snapshot && this.snapshot.width) || window.appConfig.panelWidth\n    const mainWin = this.mainWindowsManager.snapshot\n    return mainWin &&\n      mainWin.state === 'normal' &&\n      mainWin.top != null &&\n      mainWin.left != null &&\n      mainWin.width != null &&\n      mainWin.height != null\n      ? // coords must be integer\n        {\n          top: Math.round(\n            (await this.mainWindowsManager.correctTop(mainWin.top)) || 0\n          ),\n          left: Math.round(\n            side === 'right'\n              ? Math.max(mainWin.width - panelWidth, panelWidth)\n              : mainWin.left\n          ),\n          width: Math.round(panelWidth),\n          height: Math.round(mainWin.height)\n        }\n      : {\n          top: 0,\n          left: Math.round(\n            side === 'right' ? window.screen.availWidth - panelWidth : 0\n          ),\n          width: Math.round(panelWidth),\n          height: Math.round(window.screen.availHeight)\n        }\n  }\n}\n"
  },
  {
    "path": "src/components/AntdRoot/AntdRootContainer.tsx",
    "content": "import React, { FC, useEffect, useMemo } from 'react'\nimport { shallowEqual } from 'react-redux'\nimport { ConfigProvider as AntdConfigProvider } from 'antd'\nimport zh_CN from 'antd/lib/locale-provider/zh_CN'\nimport zh_TW from 'antd/lib/locale-provider/zh_TW'\nimport en_US from 'antd/lib/locale-provider/en_US'\nimport { useSelector } from '@/content/redux'\nimport { reportPageView } from '@/_helpers/analytics'\n\nconst antdLocales = (saladictLocale: string) => {\n  switch (saladictLocale) {\n    case 'zh-CN':\n      return zh_CN\n    case 'zh-TW':\n      return zh_TW\n    default:\n      return en_US\n  }\n}\n\nexport interface AntdRootContainerProps {\n  /** Render Props */\n  render: () => React.ReactNode\n  /** Analytics path */\n  gaPath?: string\n}\n\n/** Inner Component so that it can access Redux store */\nexport const AntdRootContainer: FC<AntdRootContainerProps> = props => {\n  const { langCode, analytics, darkMode } = useSelector(state => {\n    const { langCode, analytics, darkMode } = state.config\n    return { langCode, analytics, darkMode }\n  }, shallowEqual)\n\n  const locale = useMemo(() => antdLocales(langCode), [langCode])\n\n  const bgStyles = useMemo(\n    () => ({ backgroundColor: darkMode ? '#000' : '#f0f2f5' }),\n    [darkMode]\n  )\n\n  useEffect(() => {\n    if (analytics && props.gaPath) {\n      reportPageView(props.gaPath)\n    }\n  }, [analytics, props.gaPath])\n\n  return (\n    <AntdConfigProvider locale={locale}>\n      <div style={bgStyles}>{props.render()}</div>\n    </AntdConfigProvider>\n  )\n}\n"
  },
  {
    "path": "src/components/AntdRoot/_style.scss",
    "content": "html {\n  background-color: #888;\n}\n\n#root {\n  &::after {\n    content: '';\n    position: fixed;\n    z-index: 2147483647;\n    top: 0;\n    left: 0;\n    bottom: 0;\n    right: 0;\n    margin: auto;\n    background-color: #888;\n    transition: background-color 0.3s;\n    opacity: 0; // for initial hardware acceleration\n    pointer-events: none;\n  }\n\n  &.saladict-theme-dark::after {\n    background-color: #000;\n    opacity: 1;\n  }\n\n  &.saladict-theme-bright::after {\n    background-color: #f0f2f5;\n    opacity: 1;\n  }\n\n  &.saladict-theme-loaded::after {\n    transition: opacity 0.4s;\n    opacity: 0;\n  }\n\n  &.saladict-theme-loading::after {\n    opacity: 1;\n  }\n}\n\n// Fix incorrect antd pagination arrow position on Firefox\n@-moz-document url-prefix() {\n  .ant-pagination-item-link > .anticon {\n    height: 100%;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n  }\n}\n"
  },
  {
    "path": "src/components/AntdRoot/index.tsx",
    "content": "import React from 'react'\nimport { Provider as ReduxProvider } from 'react-redux'\nimport ReactDOM from 'react-dom'\nimport { createStore } from '@/content/redux'\nimport SaladBowlContainer from '@/content/components/SaladBowl/SaladBowl.container'\nimport DictPanelContainer from '@/content/components/DictPanel/DictPanel.container'\nimport WordEditorContainer from '@/content/components/WordEditor/WordEditor.container'\nimport { I18nContextProvider } from '@/_helpers/i18n'\nimport { timer } from '@/_helpers/promise-more'\nimport { AntdRootContainer } from './AntdRootContainer'\n\nimport './_style.scss'\n\nexport const initAntdRoot = async (\n  render: () => React.ReactNode,\n  gaPath?: string\n): Promise<void> => {\n  const store = await createStore()\n\n  // update theme as quickly as possible\n  let { darkMode } = store.getState().config\n  await switchAntdTheme(darkMode)\n  store.subscribe(() => {\n    const { config } = store.getState()\n    if (config.darkMode !== darkMode) {\n      darkMode = config.darkMode\n      switchAntdTheme(darkMode)\n    }\n  })\n\n  ReactDOM.render(\n    <I18nContextProvider>\n      <ReduxProvider store={store}>\n        <AntdRootContainer gaPath={gaPath} render={render} />\n        <SaladBowlContainer />\n        <DictPanelContainer />\n        <WordEditorContainer />\n      </ReduxProvider>\n    </I18nContextProvider>,\n    document.getElementById('root')\n  )\n}\n\nasync function switchAntdTheme(darkMode: boolean): Promise<void> {\n  const $root = document.querySelector('#root')!\n\n  await new Promise(resolve => {\n    const filename = `antd${darkMode ? '.dark' : ''}.min.css`\n    const href =\n      process.env.NODE_ENV === 'development'\n        ? `https://cdnjs.cloudflare.com/ajax/libs/antd/4.1.0/${filename}`\n        : `/assets/${filename}`\n    let $link = document.head.querySelector<HTMLLinkElement>(\n      'link#saladict-antd-theme'\n    )\n\n    if ($link && $link.getAttribute('href') === href) {\n      resolve()\n      return\n    }\n\n    // smooth dark/bright transition\n    $root.classList.toggle('saladict-theme-dark', darkMode)\n    $root.classList.toggle('saladict-theme-bright', !darkMode)\n    $root.classList.toggle('saladict-theme-loading', true)\n\n    if ($link) {\n      $link.setAttribute('href', href)\n    } else {\n      $link = document.createElement('link')\n      $link.setAttribute('id', 'saladict-antd-theme')\n      $link.setAttribute('rel', 'stylesheet')\n      $link.setAttribute('href', href)\n      document.head.insertBefore($link, document.head.firstChild)\n    }\n\n    let loaded = false\n\n    // @ts-ignore\n    $link.onreadystatechange = function() {\n      // @ts-ignore\n      if (this.readyState === 'complete' || this.readyState === 'loaded') {\n        if (loaded === false) {\n          resolve()\n        }\n        loaded = true\n      }\n    }\n\n    $link.onload = function() {\n      if (loaded === false) {\n        resolve()\n      }\n      loaded = true\n    }\n\n    const img = document.createElement('img')\n    img.onerror = function() {\n      if (loaded === false) {\n        resolve()\n      }\n      loaded = true\n    }\n    img.src = href\n  })\n\n  await timer(500)\n\n  $root.classList.toggle('saladict-theme-loaded', true)\n  $root.classList.toggle('saladict-theme-loading', false)\n}\n"
  },
  {
    "path": "src/components/EntryBox/EntryBox.scss",
    "content": ".entryBox-Wrap {\n  padding-top: 0.8em;\n}\n\n.entryBox {\n  position: relative;\n  border: 1px solid #c76e06;\n  border-radius: 5px;\n  margin-bottom: 1em;\n  padding: 1em 0.5em 0.5em 0.5em;\n}\n\n.entryBox-Title {\n  position: absolute;\n  top: 0;\n  left: 1em;\n  transform: translateY(-50%);\n  max-width: 90%;\n  overflow: hidden;\n  padding: 0 5px;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  font-size: 1.2em;\n  background: var(--color-background);\n}\n"
  },
  {
    "path": "src/components/EntryBox/EntryBox.stories.tsx",
    "content": "import React from 'react'\nimport { storiesOf } from '@storybook/react'\nimport { withKnobs, text } from '@storybook/addon-knobs'\nimport { withSaladictPanel } from '@/_helpers/storybook'\nimport { EntryBox } from './index'\n\nstoriesOf('Content Scripts|Components', module)\n  .addParameters({\n    backgrounds: [\n      { name: 'Saladict', value: '#5caf9e', default: true },\n      { name: 'Black', value: '#000' },\n      { name: 'White', value: '#fff' }\n    ]\n  })\n  .addDecorator(withKnobs)\n  .addDecorator(\n    withSaladictPanel({\n      head: <style>{require('./EntryBox.scss').toString()}</style>\n    })\n  )\n  .add('EntryBox', () => (\n    <EntryBox title={text('Title', 'title text')}>\n      {text(\n        'Content',\n        'Lorem ipsum dolor sit amet consectetur adipisicing elit. Incidunt recusandae exercitationem minus autem repellendus soluta nulla laudantium nobis! Excepturi, dolorem. Doloremque exercitationem dolores voluptatum sint. Perspiciatis reiciendis doloribus mollitia nisi.'\n      )}\n    </EntryBox>\n  ))\n"
  },
  {
    "path": "src/components/EntryBox/index.tsx",
    "content": "import React, { FC, ComponentProps, ReactNode } from 'react'\n\nexport interface EntryBoxProps extends Omit<ComponentProps<'div'>, 'title'> {\n  title: ReactNode\n}\n\n/**\n * Box-wrapped content\n */\nexport const EntryBox: FC<EntryBoxProps> = props => {\n  const { title, className, children, ...restProps } = props\n  return (\n    <div\n      className={`entryBox-Wrap${className ? ` ${className}` : ''}`}\n      {...restProps}\n    >\n      <section className=\"entryBox\">\n        <h1 className=\"entryBox-Title\">{title}</h1>\n        <div>{children}</div>\n      </section>\n    </div>\n  )\n}\n\nexport default EntryBox\n"
  },
  {
    "path": "src/components/ErrorBoundary.tsx",
    "content": "import React, { ComponentType } from 'react'\n\ninterface ErrorBoundaryProps {\n  /** Reanders on error */\n  error?: ComponentType\n}\n\ninterface ErrorBoundaryState {\n  hasError: boolean\n}\n\nexport class ErrorBoundary extends React.PureComponent<\n  ErrorBoundaryProps,\n  ErrorBoundaryState\n> {\n  state: ErrorBoundaryState = {\n    hasError: false\n  }\n\n  static getDerivedStateFromError() {\n    return { hasError: true }\n  }\n\n  render() {\n    return this.state.hasError\n      ? this.props.error\n        ? React.createElement(this.props.error)\n        : null\n      : this.props.children\n  }\n}\n"
  },
  {
    "path": "src/components/FloatBox/FloatBox.scss",
    "content": ".floatBox-Container {\n  position: relative;\n  overflow: hidden;\n  box-sizing: border-box;\n  word-break: keep-all;\n  white-space: nowrap;\n  border-radius: 10px;\n  background: #fff;\n  box-shadow: 0px 4px 31px -8px rgba(0, 0, 0, 0.8);\n  font-size: var(--panel-font-size);\n\n  @include isDarkMode {\n    background: #2d3338;\n  }\n\n  @include isAnimate {\n    transition: width 0.4s, height 0.4s;\n  }\n}\n\n.floatBox-Measure {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: max-content; // for including <select> width\n  max-width: calc(var(--panel-width) * 0.7);\n  padding: 10px;\n}\n\n.floatBox-Item {\n  display: block;\n  width: 100%;\n  overflow: hidden;\n  padding: 5px 10px;\n  font-size: 1em;\n  text-overflow: ellipsis;\n  text-align: initial;\n  outline: none;\n  border: none;\n  border-radius: 3px;\n  color: var(--color-font);\n  background-color: transparent;\n  cursor: pointer;\n\n  &:hover,\n  &:focus {\n    background-color: rgba(215, 214, 214, 0.25);\n  }\n\n  &::-moz-focus-inner {\n    border: 0;\n  }\n\n  &:first-child {\n    border-top-left-radius: 8px;\n    border-top-right-radius: 8px;\n  }\n\n  &:last-child {\n    border-bottom-left-radius: 8px;\n    border-bottom-right-radius: 8px;\n  }\n\n  @include isDarkMode {\n    border: 1px solid transparent;\n    border-radius: 5px;\n\n    &:focus,\n    &:hover {\n      border-color: #1abc9c;\n      background-color: #29615a;\n    }\n  }\n}\n\n.floatBox-Select {\n  -moz-appearance: none;\n  -webkit-appearance: none;\n  appearance: none;\n  box-sizing: border-box;\n  margin: 0;\n  padding-right: 1.5em !important;\n  background-color: transparent;\n  background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%231abc9c%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E');\n  background-repeat: no-repeat, repeat;\n  background-position: right .5em top 50%, 0 0;\n  background-size: .65em auto, 100%;\n\n  &::-ms-expand {\n    display: none;\n  }\n\n  option {\n    font-size: 1em;\n    font-weight: normal;\n  }\n}\n\n.floatBox-Btn {\n  display: block;\n  width: 100%;\n  overflow: hidden;\n  padding: 5px 10px;\n  font-size: 1em;\n  text-overflow: ellipsis;\n  text-align: initial;\n  outline: none;\n  color: var(--color-font);\n  background: transparent;\n  border: none;\n  border-radius: 3px;\n  cursor: pointer;\n\n  &:hover,\n  &:focus {\n    background: rgba(215, 214, 214, 0.25);\n  }\n\n  &::-moz-focus-inner {\n    border: 0;\n  }\n\n  &:first-child {\n    border-top-left-radius: 8px;\n    border-top-right-radius: 8px;\n  }\n\n  &:last-child {\n    border-bottom-left-radius: 8px;\n    border-bottom-right-radius: 8px;\n  }\n}\n\n.floatBox-Entry {\n  margin-right: 1.5em;\n  color: #f9690e;\n}\n\n.floatBox-compact {\n  &.floatBox-Container {\n    font-size: 12px;\n    border-radius: 7px;\n    box-shadow: 0px 1px 15px -7px rgba(0, 0, 0, 0.8);\n  }\n\n  .floatBox-Measure {\n    padding: 5px 6px;\n  }\n\n  .floatBox-Item {\n    padding: 3px 7px;\n    border-radius: 2px;\n\n    &:first-child {\n      border-top-left-radius: 5px;\n      border-top-right-radius: 5px;\n    }\n\n    &:last-child {\n      border-bottom-left-radius: 5px;\n      border-bottom-right-radius: 5px;\n    }\n  }\n}\n\n// Firefox fix\n@-moz-document url-prefix() {\n  .floatBox-Container {\n    box-shadow: 0px 4px 20px -8px rgba(0, 0, 0, 0.8);\n\n    .floatBox-Select {\n      padding-left: 6px;\n    }\n\n    &.floatBox-compact {\n      box-shadow: 0px 1px 13px -7px rgba(0, 0, 0, 0.8);\n\n      .floatBox-Select {\n        padding-left: 3px;\n      }\n    }\n  }\n}\n\n// modify from https://loading.io/css/\n.lds-ellipsis {\n  display: block;\n  margin: 0 auto;\n  position: relative;\n  width: 64px;\n  height: 32px;\n}\n.lds-ellipsis div {\n  position: absolute;\n  top: 11px;\n  width: 11px;\n  height: 11px;\n  border-radius: 50%;\n  background: #e2e2e1;\n  animation-timing-function: cubic-bezier(0, 1, 1, 0);\n}\n.lds-ellipsis div:nth-child(1) {\n  left: 6px;\n  animation: lds-ellipsis1 0.6s infinite;\n}\n.lds-ellipsis div:nth-child(2) {\n  left: 6px;\n  animation: lds-ellipsis2 0.6s infinite;\n}\n.lds-ellipsis div:nth-child(3) {\n  left: 26px;\n  animation: lds-ellipsis2 0.6s infinite;\n}\n.lds-ellipsis div:nth-child(4) {\n  left: 45px;\n  animation: lds-ellipsis3 0.6s infinite;\n}\n@keyframes lds-ellipsis1 {\n  0% {\n    transform: scale(0);\n  }\n  100% {\n    transform: scale(1);\n  }\n}\n@keyframes lds-ellipsis3 {\n  0% {\n    transform: scale(1);\n  }\n  100% {\n    transform: scale(0);\n  }\n}\n@keyframes lds-ellipsis2 {\n  0% {\n    transform: translate(0, 0);\n  }\n  100% {\n    transform: translate(19px, 0);\n  }\n}\n"
  },
  {
    "path": "src/components/FloatBox/FloatBox.stories.tsx",
    "content": "import React from 'react'\nimport { storiesOf } from '@storybook/react'\nimport { action } from '@storybook/addon-actions'\nimport { withKnobs, boolean } from '@storybook/addon-knobs'\nimport { jsxDecorator } from 'storybook-addon-jsx'\nimport { withPropsTable } from 'storybook-addon-react-docgen'\nimport { withSaladictPanel, withi18nNS } from '@/_helpers/storybook'\nimport faker from 'faker'\nimport { FloatBox } from '.'\n\nstoriesOf('Content Scripts|Components', module)\n  .addParameters({\n    backgrounds: [\n      { name: 'Saladict', value: '#5caf9e', default: true },\n      { name: 'Black', value: '#000' }\n    ]\n  })\n  .addDecorator(withPropsTable)\n  .addDecorator(jsxDecorator)\n  .addDecorator(withKnobs)\n  .addDecorator(\n    withSaladictPanel({\n      head: (\n        <>\n          <style>{require('./FloatBox.scss').toString()}</style>\n          <style>{`.dictPanel-Root { padding: 20px; }`}</style>\n        </>\n      )\n    })\n  )\n  .addDecorator(withi18nNS('content'))\n  .add('FloatBox', () => {\n    return (\n      <FloatBox\n        list={\n          boolean('Loading', false)\n            ? undefined\n            : uniqueWordList(15).map(word => {\n                return faker.random.boolean()\n                  ? {\n                      key: word,\n                      value: word,\n                      label: word\n                    }\n                  : {\n                      key: word,\n                      value: word,\n                      options: [\n                        { value: word, label: word },\n                        ...uniqueWordList(15).map(word => {\n                          return { value: word, label: word }\n                        })\n                      ]\n                    }\n              })\n        }\n        compact={boolean('Compact', true)}\n        onSelect={action('onSelect')}\n        onFocus={() => action('onFocus')('...node')}\n        onBlur={() => action('onBlur')('...node')}\n        onMouseOver={() => action('onMouseOver')('...node')}\n        onMouseOut={() => action('onMouseOut')('...node')}\n        onArrowUpFirst={() => action('onArrowUpFirst')('...node')}\n        onArrowDownLast={() => action('onArrowDownLast')('...node')}\n        onClose={action('onClose')}\n        onHeightChanged={action('onHeightChanged')}\n      />\n    )\n  })\n\nfunction uniqueWordList(max: number): string[] {\n  return [\n    ...new Set(\n      Array(faker.random.number(max))\n        .fill(0)\n        .map(() => faker.random.word())\n    )\n  ]\n}\n"
  },
  {
    "path": "src/components/FloatBox/index.tsx",
    "content": "import React, { FC, Ref, useState, useCallback } from 'react'\nimport { ResizeReporter } from 'react-resize-reporter/scroll'\nimport classnames from 'classnames'\n\nexport type FloatBoxItem =\n  | {\n      // <button>\n      key: string\n      value: string\n      label: React.ReactNode\n      options?: undefined\n    }\n  | {\n      // <select>\n      key: string\n      value: string\n      options: Array<{\n        value: string\n        label: string\n      }>\n      title?: string\n    }\n\nexport interface FloatBoxProps {\n  list?: FloatBoxItem[]\n  /** compact layout */\n  compact?: boolean\n  /** Box container */\n  ref?: Ref<HTMLDivElement>\n  /** When a item is selected */\n  onSelect?: (key: string, value: string) => any\n  /** When a item is focused */\n  onFocus?: (e: React.FocusEvent<HTMLButtonElement | HTMLSelectElement>) => any\n  /** When a item is blur */\n  onBlur?: (e: React.FocusEvent<HTMLButtonElement | HTMLSelectElement>) => any\n  /** When mouse over on panel */\n  onMouseOver?: (e: React.MouseEvent<HTMLDivElement>) => any\n  /** When mouse out on panel */\n  onMouseOut?: (e: React.MouseEvent<HTMLDivElement>) => any\n  /** When ArrowUp key if pressed on the first item */\n  onArrowUpFirst?: (container: HTMLDivElement) => any\n  /** When ArrowDown key if pressed on the last item */\n  onArrowDownLast?: (container: HTMLDivElement) => any\n  /** When the panel is about to close */\n  onClose?: (container: HTMLDivElement) => any\n  /** When box height is changed */\n  onHeightChanged?: (height: number) => any\n}\n\n/**\n * A box that is meant to be on top of other elements\n */\nexport const FloatBox: FC<FloatBoxProps> = React.forwardRef(\n  (props: FloatBoxProps, containerRef: React.Ref<HTMLDivElement>) => {\n    const [height, _setHeight] = useState(0)\n    const [width, _setWidth] = useState(0)\n    const updateHeight = useCallback(\n      (newWidth: number, newHeight: number) => {\n        _setWidth(newWidth)\n        _setHeight(newHeight)\n        if (props.onHeightChanged && newHeight !== height) {\n          props.onHeightChanged(newHeight)\n        }\n      },\n      [props.onHeightChanged]\n    )\n\n    return (\n      <div\n        className={classnames('floatBox-Container', {\n          'floatBox-compact': props.compact\n        })}\n        style={{ width, height }}\n        onMouseOver={props.onMouseOver}\n        onMouseOut={props.onMouseOut}\n      >\n        <div className=\"floatBox-Measure\">\n          <ResizeReporter reportInit onSizeChanged={updateHeight} />\n\n          {!props.list ? (\n            <div key=\"loading\" className=\"lds-ellipsis\">\n              <div></div>\n              <div></div>\n              <div></div>\n              <div></div>\n            </div>\n          ) : (\n            <div key=\"box\" ref={containerRef} className=\"floatBox\">\n              {props.list.map(renderBoxItem)}\n            </div>\n          )}\n        </div>\n      </div>\n    )\n\n    function renderBoxItem(item: FloatBoxItem) {\n      if (item.options) {\n        return (\n          <select\n            key={item.key}\n            className=\"floatBox-Item floatBox-Select\"\n            data-key={item.key}\n            defaultValue={item.value}\n            onFocus={props.onFocus}\n            onBlur={props.onBlur}\n            onChange={onSelectItemChange}\n          >\n            {item.title && <option disabled>{item.title}</option>}\n            {item.options.map(opt => (\n              <option key={opt.value} value={opt.value}>\n                {opt.label}\n              </option>\n            ))}\n          </select>\n        )\n      }\n\n      return (\n        <button\n          key={item.key}\n          className=\"floatBox-Item floatBox-Btn\"\n          data-key={item.key}\n          data-value={item.value}\n          onFocus={props.onFocus}\n          onBlur={props.onBlur}\n          onClick={onBtnItemClick}\n          onKeyDown={onBtnItemKeyDown}\n        >\n          {item.label}\n        </button>\n      )\n    }\n\n    function onSelectItemChange(e: React.ChangeEvent<HTMLSelectElement>) {\n      if (props.onSelect) {\n        const {\n          dataset: { key },\n          value\n        } = e.currentTarget\n        props.onSelect(key!, value)\n      }\n    }\n\n    function onBtnItemClick(e: React.MouseEvent<HTMLButtonElement>) {\n      if (props.onSelect) {\n        const { key, value } = e.currentTarget.dataset\n        props.onSelect(key!, value!)\n      }\n    }\n\n    function onBtnItemKeyDown(e: React.KeyboardEvent<HTMLButtonElement>) {\n      if (e.key === 'ArrowDown') {\n        e.preventDefault()\n        e.stopPropagation()\n        const $nextLi = e.currentTarget.nextSibling\n        if ($nextLi) {\n          ;($nextLi as HTMLButtonElement).focus()\n        } else if (props.onArrowDownLast) {\n          props.onArrowDownLast(e.currentTarget.parentElement as HTMLDivElement)\n        }\n      } else if (e.key === 'ArrowUp') {\n        e.preventDefault()\n        e.stopPropagation()\n        const $prevLi = e.currentTarget.previousSibling\n        if ($prevLi) {\n          ;($prevLi as HTMLButtonElement).focus()\n        } else if (props.onArrowUpFirst) {\n          props.onArrowUpFirst(e.currentTarget.parentElement as HTMLDivElement)\n        }\n      } else if (e.key === 'Escape') {\n        // prevent the dict panel being closed\n        e.preventDefault()\n        e.stopPropagation()\n        if (props.onClose) {\n          props.onClose(e.currentTarget.parentElement as HTMLDivElement)\n        }\n      }\n    }\n  }\n)\n"
  },
  {
    "path": "src/components/HoverBox/HoverBox.scss",
    "content": "@import '@/components/FloatBox/FloatBox.scss';\n\n.hoverBox-Container {\n  display: inline-block;\n  position: relative;\n}\n\n.hoverBox-FloatBox {\n  position: absolute;\n  z-index: $global-zindex-dictpanel;\n}\n\n.csst-hoverBox {\n  @include isAnimate(-enter) {\n    opacity: 0;\n    transition: opacity 0.4s;\n  }\n\n  @include isAnimate(-enter-active, -exit) {\n    opacity: 1;\n    transition: opacity 0.4s;\n  }\n\n  @include isAnimate(-exit-active) {\n    opacity: 0;\n    transition: opacity 0.4s;\n  }\n}\n"
  },
  {
    "path": "src/components/HoverBox/index.tsx",
    "content": "import React, { FC, useContext, useRef, useState } from 'react'\nimport CSSTransition from 'react-transition-group/CSSTransition'\nimport {\n  useObservable,\n  useObservableCallback,\n  useObservableState,\n  identity\n} from 'observable-hooks'\nimport { merge } from 'rxjs'\nimport {\n  hover,\n  hoverWithDelay,\n  focusBlur,\n  mapToTrue\n} from '@/_helpers/observables'\nimport { FloatBox, FloatBoxItem } from '../FloatBox'\nimport { createPortal } from 'react-dom'\n\nexport type HoverBoxItem = FloatBoxItem\n\n/**\n * Accept a optional root element via Context which\n * will be the parent element of the float boxes.\n * This is for bypassing z-index restriction, making sure\n * the float boxes is always on top of other elements.\n */\nexport const HoverBoxContext = React.createContext<\n  React.RefObject<HTMLElement | null>\n>({ current: null })\n\nexport interface HoverBoxProps {\n  Button: React.ComponentType<React.ComponentProps<'button'>>\n  items: HoverBoxItem[]\n  /** Compact float box */\n  compact?: boolean\n  /** box top offset */\n  top?: number\n  /** box left offset */\n  left?: number\n  onSelect?: (key: string, value: string) => void\n  /** return false to prevent showing float box */\n  onBtnClick?: () => boolean\n  onHeightChanged?: (height: number) => void\n}\n\n/**\n * A button and a FloatBox that shows when hovering.\n */\nexport const HoverBox: FC<HoverBoxProps> = props => {\n  const portalRootRef = useContext(HoverBoxContext)\n  const containerRef = useRef<HTMLDivElement | null>(null)\n  const boxRef = useRef<HTMLDivElement | null>(null)\n\n  const [onHoverBtn, onHoverBtn$] = useObservableCallback<\n    boolean,\n    React.MouseEvent<Element>\n  >(hoverWithDelay)\n\n  const [onBtnClick, onBtnClick$] = useObservableCallback<boolean, void>(\n    mapToTrue\n  )\n\n  const [onHoverBox, onHoverBox$] = useObservableCallback<\n    boolean,\n    React.MouseEvent<Element>\n  >(hover)\n\n  const [onFocusBlur, focusBlur$] = useObservableCallback(focusBlur)\n\n  const [showBox, showBox$] = useObservableCallback<boolean>(identity)\n\n  const isOnBtn = useObservableState(\n    useObservable(() => merge(onHoverBtn$, onBtnClick$)),\n    false\n  )\n\n  const isOnBox = useObservableState(\n    useObservable(() => merge(onHoverBox$, focusBlur$, showBox$)),\n    false\n  )\n\n  const isShowBox = isOnBtn || isOnBox\n\n  const [floatBoxStyle, setFloatBoxStyle] = useState<React.CSSProperties>(() =>\n    props.left == null\n      ? {\n          top: props.top == null ? 40 : props.top,\n          left: '50%',\n          transform: 'translateX(-50%)'\n        }\n      : {\n          top: props.top == null ? 40 : props.top,\n          left: props.left\n        }\n  )\n\n  return (\n    <div className=\"hoverBox-Container\" ref={containerRef}>\n      <props.Button\n        onKeyDown={e => {\n          switch (e.key) {\n            case 'ArrowDown':\n              // Show float box or jump focus to the first item\n              e.preventDefault()\n              e.stopPropagation()\n              if (isShowBox) {\n                if (boxRef.current) {\n                  const firstBtn = boxRef.current.firstElementChild\n                  if (firstBtn) {\n                    ;(firstBtn as HTMLButtonElement | HTMLSelectElement).focus()\n                  }\n                }\n              } else {\n                showBox(true)\n              }\n              break\n            case 'Tab':\n              // Jump focus to the first item\n              if (!e.shiftKey && isShowBox && boxRef.current) {\n                e.preventDefault()\n                e.stopPropagation()\n                const firstBtn = boxRef.current.firstElementChild\n                if (firstBtn) {\n                  ;(firstBtn as HTMLButtonElement | HTMLSelectElement).focus()\n                }\n              }\n              break\n          }\n        }}\n        onMouseOver={onHoverBtn}\n        onMouseOut={onHoverBtn}\n        onClick={() => {\n          if (!props.onBtnClick || props.onBtnClick() !== false) {\n            onBtnClick()\n          }\n        }}\n      />\n      <CSSTransition\n        classNames=\"csst-hoverBox\"\n        in={isShowBox}\n        timeout={100}\n        mountOnEnter\n        unmountOnExit\n        onEnter={() => {\n          if (portalRootRef.current && containerRef.current) {\n            const portalRootRect = portalRootRef.current.getBoundingClientRect()\n            const containerRect = containerRef.current.getBoundingClientRect()\n            setFloatBoxStyle({\n              top:\n                containerRect.y -\n                portalRootRect.y +\n                (props.top == null ? 40 : props.top),\n              left:\n                containerRect.x -\n                portalRootRect.x +\n                (props.left == null\n                  ? -Math.floor(containerRect.width / 2)\n                  : props.left)\n            })\n          }\n        }}\n        onExited={() => props.onHeightChanged && props.onHeightChanged(0)}\n      >\n        {() => {\n          const floatBox = (\n            <div className=\"hoverBox-FloatBox\" style={floatBoxStyle}>\n              <FloatBox\n                ref={boxRef}\n                compact={props.compact}\n                list={props.items}\n                onFocus={onFocusBlur}\n                onBlur={onFocusBlur}\n                onMouseOver={onHoverBox}\n                onMouseOut={onHoverBox}\n                onArrowUpFirst={container =>\n                  (container.lastElementChild as HTMLButtonElement).focus()\n                }\n                onArrowDownLast={container =>\n                  (container.firstElementChild as HTMLButtonElement).focus()\n                }\n                onSelect={props.onSelect}\n                onHeightChanged={props.onHeightChanged}\n                onClose={() => showBox(false)}\n              />\n            </div>\n          )\n\n          return portalRootRef.current && containerRef.current\n            ? createPortal(floatBox, portalRootRef.current)\n            : floatBox\n        }}\n      </CSSTransition>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/MachineTrans/MachineTrans.scss",
    "content": ".MachineTrans-Text {\n  .saladict-Speaker {\n    position: absolute;\n    left: 0;\n    top: 2px;\n    margin: 0;\n  }\n\n  summary {\n    cursor: pointer;\n  }\n}\n\n.MachineTrans-Lines {\n  position: relative;\n  margin: 0.5em 0;\n  padding-left: 1.5em;\n\n  p {\n    margin: 0.3em 0;\n  }\n}\n\n.MachineTrans-Lines-collapse {\n  overflow: hidden;\n  position: relative;\n\n  &::after {\n    content: '';\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    margin: auto;\n    background: linear-gradient(90deg, rgba(255,255,255,0) 50%, var(--color-background) 90%);\n    pointer-events: none;\n  }\n\n  button {\n    height: 1.5em;\n    padding: 0;\n    text-align: start;\n    word-break: keep-all;\n    white-space: nowrap;\n    font-size: 1em;\n    font-weight: normal;\n    font-family: inherit;\n    color: currentColor;\n    cursor: pointer;\n  }\n}\n\n/*-----------------------------------------------*\\\n    States\n\\*-----------------------------------------------*/\n.MachineTrans-lang-ar, // Arabic\n.MachineTrans-lang-ara, // Arabic\n.MachineTrans-lang-az, // Azerbaijani\n.MachineTrans-lang-fa, // Persian\n.MachineTrans-lang-he, // Hebrew\n.MachineTrans-lang-iw, // Hebrew\n.MachineTrans-lang-ku, // Kurdish\n.MachineTrans-lang-ug, // Uighur\n.MachineTrans-lang-ur // Urdu\n{\n  direction: rtl !important;\n\n  &.MachineTrans-Lines-collapse {\n    &::after {\n      background: linear-gradient(270deg, rgba(255,255,255,0) 50%, var(--color-background) 90%);\n    }\n  }\n}\n\n.MachineTrans-has-rtl {\n  text-align: right;\n\n  .MachineTrans-Lines {\n    padding-left: 0;\n    padding-right: 1.5em;\n  }\n\n  .saladict-Speaker {\n    left: auto;\n    right: 0;\n  }\n}\n\n@font-face {\n  font-family: \"UKIJ Tuz Basma\";\n  src: url(\"https://ws-image-cdn.subat.cn/fonts/ukij-tuz-basma-bold/UKIJTuzBasma-Bold.woff2\")\n      format(\"woff2\"),\n    url(\"https://ws-image-cdn.subat.cn/fonts/ukij-tuz-basma-bold/UKIJTuzBasma-Bold.woff\")\n      format(\"woff\"),\n    url(\"https://ws-image-cdn.subat.cn/fonts/ukij-tuz-basma-bold/UKIJTuzBasma-Bold.ttf\")\n      format(\"truetype\"),\n    url(\"https://ws-image-cdn.subat.cn/fonts/ukij-tuz-basma-bold/UKIJTuzBasma-Bold.svg#UKIJTuzBasma-Bold\")\n      format(\"svg\");\n  font-style: normal;\n}\n\n.MachineTrans-lang-ug {\n  font-family: \"UKIJ Tuz Basma\" !important;\n}\n"
  },
  {
    "path": "src/components/MachineTrans/MachineTrans.stories.tsx",
    "content": "import React from 'react'\nimport { Subject } from 'rxjs'\nimport faker from 'faker'\nimport { storiesOf } from '@storybook/react'\nimport { action } from '@storybook/addon-actions'\nimport { withKnobs, boolean } from '@storybook/addon-knobs'\nimport { jsxDecorator } from 'storybook-addon-jsx'\nimport { withPropsTable } from 'storybook-addon-react-docgen'\nimport {\n  withSaladictPanel,\n  withi18nNS,\n  withSideEffect,\n  mockRuntimeMessage\n} from '@/_helpers/storybook'\nimport { DictItemHead } from '@/content/components/DictItem/DictItemHead'\nimport { MachineTrans } from './MachineTrans'\nimport { machineResult } from './engine'\n\nstoriesOf('Content Scripts|Components', module)\n  .addDecorator(withPropsTable)\n  .addDecorator(jsxDecorator)\n  .addDecorator(withKnobs)\n  .addDecorator(\n    withSideEffect(\n      mockRuntimeMessage(async message => {\n        action(message.type)(message['payload'])\n      })\n    )\n  )\n  .addDecorator(\n    withSaladictPanel({\n      head: (\n        <style>\n          {require('./MachineTrans.scss').toString()}\n          {require('@/components/Speaker/Speaker.scss').toString()}\n          {require('@/content/components/DictItem/DictItemHead.scss').toString()}\n        </style>\n      )\n    })\n  )\n  .addDecorator(withi18nNS(['content', 'langcode']))\n  .add('MachineTrans', () => {\n    const rtl = boolean('rtl', true)\n    return (\n      <MachineTrans\n        result={{\n          id: 'baidu',\n          sl: 'en',\n          tl: rtl ? 'ara' : 'zh',\n          slInitial: 'collapse',\n          searchText: {\n            paragraphs: [faker.lorem.paragraph()],\n            tts: faker.internet.url()\n          },\n          trans: {\n            paragraphs: [faker.lorem.paragraph()],\n            tts: faker.internet.url()\n          }\n        }}\n        searchText={action('Search Text')}\n        catalogSelect$={new Subject()}\n      />\n    )\n  })\n  .add('MachineTransCatalog', () => {\n    const rtl = boolean('rtl', false)\n    const noop = () => {}\n    const catalogSelect$ = new Subject<{ key: string; value: string }>()\n    const mt = machineResult(\n      {\n        result: {\n          id: 'google',\n          sl: rtl ? 'ara' : 'en',\n          tl: 'zh',\n          slInitial: 'hide',\n          searchText: {\n            paragraphs: [faker.lorem.paragraph()],\n            tts: faker.internet.url()\n          },\n          trans: {\n            paragraphs: [faker.lorem.paragraph()],\n            tts: faker.internet.url()\n          }\n        }\n      },\n      ['zh', 'cht', 'en']\n    )\n    return (\n      <>\n        <DictItemHead\n          dictID={mt.result.id}\n          isSearching={false}\n          toggleFold={noop}\n          openDictSrcPage={noop}\n          onCatalogSelect={v => catalogSelect$.next(v)}\n          catalog={mt.catalog}\n        />\n        <MachineTrans\n          result={mt.result}\n          searchText={action('Search Text')}\n          catalogSelect$={catalogSelect$}\n        />\n      </>\n    )\n  })\n"
  },
  {
    "path": "src/components/MachineTrans/MachineTrans.tsx",
    "content": "import React, {\n  FC,\n  useState,\n  useCallback,\n  useLayoutEffect,\n  useRef\n} from 'react'\nimport { useSubscription } from 'observable-hooks'\nimport Speaker from '@/components/Speaker'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { DictID } from '@/app-config'\nimport { message } from '@/_helpers/browser-api'\nimport { MachineTranslateResult } from './engine'\nimport { Trans, useTranslate } from '@/_helpers/i18n'\n\nconst rtlLangs = new Set([\n  'ar', // Arabic\n  'ara', // Arabic\n  'az', // Azerbaijani\n  'fa', // Persian\n  'he', // Hebrew\n  'iw', // Hebrew\n  'ku', // Kurdish\n  'ug', // Uighur\n  'ur' // Urdu\n])\n\nconst TSpeaker = React.memo<{\n  result: MachineTranslateResult<DictID>\n  source: 'searchText' | 'trans'\n}>(({ result, source }) => (\n  <Speaker\n    src={\n      result[source].tts === '#'\n        ? () => {\n            console.log({\n              type: 'DICT_ENGINE_METHOD',\n              payload: {\n                id: result.id,\n                method: 'getTTS',\n                args: [\n                  result[source].paragraphs.join(' '),\n                  source === 'trans' ? result.tl : result.sl\n                ]\n              }\n            })\n            return message.send<'DICT_ENGINE_METHOD', string>({\n              type: 'DICT_ENGINE_METHOD',\n              payload: {\n                id: result.id,\n                method: 'getTTS',\n                args: [\n                  result[source].paragraphs.join(' '),\n                  source === 'trans' ? result.tl : result.sl\n                ]\n              }\n            })\n          }\n        : result[source].tts\n    }\n  />\n))\n\n/** text with a speaker at the beginning */\nconst TText = React.memo<{\n  result: MachineTranslateResult<DictID>\n  source: 'searchText' | 'trans'\n  lang: string\n}>(({ result, source, lang }) => (\n  <div className={'MachineTrans-Lines'}>\n    <TSpeaker result={result} source={source} />\n    {result[source].paragraphs.map((line, i) => (\n      <p key={i} className={`MachineTrans-lang-${lang}`}>\n        {line}\n      </p>\n    ))}\n  </div>\n))\n\nconst TTextCollapsable = React.memo<{\n  result: MachineTranslateResult<DictID>\n  source: 'searchText' | 'trans'\n  lang: string\n}>(({ result, source, lang }) => {\n  const [collapse, setCollapse] = useState(false)\n  const expand = useCallback(() => setCollapse(false), [setCollapse])\n\n  const containerRef = useRef<HTMLDivElement | null>(null)\n\n  useLayoutEffect(() => {\n    if (collapse || !containerRef.current) return\n\n    // count lines\n\n    if (containerRef.current.querySelectorAll('p').length > 1) {\n      // multiple paragraphs\n      setCollapse(true)\n      return\n    }\n\n    const text = containerRef.current.querySelector('p span')\n    if (text && text.getClientRects().length > 1) {\n      // multiple lines\n      setCollapse(true)\n      return\n    }\n  }, [])\n\n  return (\n    <div ref={containerRef} className={'MachineTrans-Lines'}>\n      <TSpeaker result={result} source={source} />\n      {collapse ? (\n        <div\n          className={`MachineTrans-Lines-collapse MachineTrans-lang-${lang}`}\n        >\n          <button onClick={expand}>\n            {result[source].paragraphs.join(' ')}\n          </button>\n        </div>\n      ) : (\n        result[source].paragraphs.map((line, i) => (\n          <p key={i} className={`MachineTrans-lang-${lang}`}>\n            <span>{line}</span>\n          </p>\n        ))\n      )}\n    </div>\n  )\n})\n\nexport type MachineTransProps = ViewPorps<MachineTranslateResult<DictID>>\n\n/** Template for machine translations */\nexport const MachineTrans: FC<MachineTransProps> = props => {\n  const { tl, sl } = props.result\n  const [slState, setSlState] = useState<\n    MachineTransProps['result']['slInitial']\n  >(props.result.slInitial)\n\n  useSubscription(props.catalogSelect$, ({ key, value }) => {\n    switch (key) {\n      case 'showSl':\n        setSlState('full')\n        break\n      case 'sl':\n      case 'tl':\n        props.searchText({\n          id: props.result.id,\n          payload: {\n            sl,\n            tl,\n            [key]: value\n          }\n        })\n        break\n      case 'copySrc':\n        message.send({\n          type: 'SET_CLIPBOARD',\n          payload: props.result.searchText.paragraphs.join('\\n')\n        })\n        break\n      case 'copyTrans':\n        message.send({\n          type: 'SET_CLIPBOARD',\n          payload: props.result.trans.paragraphs.join('\\n')\n        })\n        break\n      default:\n        break\n    }\n  })\n\n  if (props.result.requireCredential) {\n    return renderCredential()\n  }\n\n  return (\n    <div\n      className={\n        rtlLangs.has(sl) || rtlLangs.has(tl)\n          ? 'MachineTrans-has-rtl'\n          : undefined\n      }\n    >\n      <div className=\"MachineTrans-Text\">\n        {slState === 'full' ? (\n          <TText result={props.result} source=\"searchText\" lang={sl} />\n        ) : slState === 'collapse' ? (\n          <TTextCollapsable\n            result={props.result}\n            source=\"searchText\"\n            lang={sl}\n          />\n        ) : null}\n        <TText result={props.result} source=\"trans\" lang={tl} />\n      </div>\n    </div>\n  )\n}\n\nfunction renderCredential() {\n  const { t } = useTranslate('content')\n  return (\n    <Trans message={t('machineTrans.login')}>\n      <a\n        href={browser.runtime.getURL('options.html?menuselected=DictAuths')}\n        target=\"_blank\"\n        rel=\"nofollow noopener noreferrer\"\n      >\n        {t('machineTrans.dictAccount')}\n      </a>\n    </Trans>\n  )\n}\n"
  },
  {
    "path": "src/components/MachineTrans/engine.ts",
    "content": "import { DictID, AppConfig } from '@/app-config'\nimport { Language } from '@opentranslate/languages'\nimport { Translator } from '@opentranslate/translator'\nimport { DictItem, SelectOptions } from '@/app-config/dicts'\nimport { isContainJapanese, isContainKorean } from '@/_helpers/lang-check'\nimport { DictSearchResult } from '../dictionaries/helpers'\n\nexport interface MachineTranslatePayload<Lang = string> {\n  sl?: Lang\n  tl?: Lang\n}\n\nexport interface MachineTranslateResult<ID extends DictID> {\n  id: ID\n  slInitial: 'hide' | 'collapse' | 'full'\n  /** Source language */\n  sl: string\n  /** Target language */\n  tl: string\n  searchText: {\n    paragraphs: string[]\n    tts?: string\n  }\n  trans: {\n    paragraphs: string[]\n    tts?: string\n  }\n  requireCredential?: boolean\n}\n\ntype DefaultMachineOptions<Lang extends Language> = {\n  /** Keep linebreaks */\n  keepLF: 'none' | 'all' | 'webpage' | 'pdf'\n  /** Source language initial state */\n  slInitial: 'hide' | 'collapse' | 'full'\n  tl: 'default' | Lang\n  tl2: 'default' | Lang\n}\n\nexport type MachineDictItem<\n  Lang extends Language,\n  Options extends { [option: string]: number | boolean | string } = {}\n> = DictItem<Options & DefaultMachineOptions<Lang>>\n\nexport type ExtractLangFromConfig<Config> = Config extends MachineDictItem<\n  infer Lang,\n  infer Options\n>\n  ? Lang\n  : never\n\nexport type ExtractOptionsFromConfig<Config> = Config extends MachineDictItem<\n  infer Lang,\n  infer Options\n>\n  ? Omit<Options, keyof DefaultMachineOptions<Lang>>\n  : never\n\n/**\n * Get Machine Translate arguments\n */\nexport async function getMTArgs(\n  translator: Translator,\n  text: string,\n  {\n    options,\n    options_sel\n  }: {\n    options: {\n      tl: 'default' | Language\n      tl2: 'default' | Language\n      keepLF: 'none' | 'all' | 'webpage' | 'pdf'\n    }\n    options_sel: {\n      tl: ReadonlyArray<'default' | Language>\n      tl2: ReadonlyArray<'default' | Language>\n    }\n  },\n  config: AppConfig,\n  payload: {\n    sl?: Language\n    tl?: Language\n    isPDF?: boolean\n  }\n): Promise<{ sl: Language; tl: Language; text: string }> {\n  if (\n    options.keepLF === 'none' ||\n    (options.keepLF === 'pdf' && !payload.isPDF) ||\n    (options.keepLF === 'webpage' && payload.isPDF)\n  ) {\n    text = text.replace(/\\n+/g, ' ')\n  }\n\n  let sl = payload.sl\n\n  if (!sl) {\n    if (isContainJapanese(text)) {\n      sl = 'ja'\n    } else if (isContainKorean(text)) {\n      sl = 'ko'\n    }\n  }\n\n  if (!sl) {\n    sl = await translator.detect(text)\n  }\n\n  let tl: Language | '' = ''\n\n  if (payload.tl) {\n    tl = payload.tl\n  } else if (options.tl === 'default') {\n    if (options_sel.tl.includes(config.langCode)) {\n      tl = config.langCode\n    }\n  } else {\n    tl = options.tl\n  }\n\n  if (!tl) {\n    tl =\n      options_sel.tl.find((lang): lang is Language => lang !== 'default') ||\n      'en'\n  }\n\n  if (sl === tl) {\n    if (!payload.tl) {\n      if (options.tl2 === 'default') {\n        if (tl !== config.langCode) {\n          tl = config.langCode\n        } else if (tl !== 'en') {\n          tl = 'en'\n        } else {\n          tl =\n            options_sel.tl.find(\n              (lang): lang is Language => lang !== 'default' && lang !== tl\n            ) || 'en'\n        }\n      } else {\n        tl = options.tl2\n      }\n    } else if (!payload.sl) {\n      sl = 'auto'\n    }\n  }\n\n  return { sl, tl, text }\n}\n\nexport function machineConfig<Config extends MachineDictItem<Language>>(\n  langs: ExtractLangFromConfig<Config>[],\n  /** overwrite configs */\n  config: Partial<Config>,\n  options: ExtractOptionsFromConfig<Config>,\n  optionsSel: SelectOptions<ExtractOptionsFromConfig<Config>>\n): Config {\n  return {\n    lang: '11111111',\n    selectionLang: {\n      english: true,\n      chinese: true,\n      japanese: true,\n      korean: true,\n      french: true,\n      spanish: true,\n      deutsch: true,\n      others: true,\n      matchAll: false\n    },\n    defaultUnfold: {\n      english: true,\n      chinese: true,\n      japanese: true,\n      korean: true,\n      french: true,\n      spanish: true,\n      deutsch: true,\n      others: true,\n      matchAll: false\n    },\n    preferredHeight: 320,\n    selectionWC: {\n      min: 1,\n      max: 999999999999999\n    },\n    ...config,\n    options: {\n      keepLF: 'webpage',\n      slInitial: 'collapse',\n      tl: 'default',\n      tl2: 'default',\n      ...(options as any)\n    },\n    options_sel: {\n      keepLF: ['none', 'all', 'webpage', 'pdf'],\n      slInitial: ['collapse', 'hide', 'full'],\n      tl: ['default', ...langs],\n      tl2: ['default', ...langs],\n      ...optionsSel\n    }\n  } as Config\n}\n\n/** Generate catalog */\nexport function machineResult<ID extends DictID>(\n  data: DictSearchResult<MachineTranslateResult<ID>>,\n  langcodes: ReadonlyArray<string>\n): DictSearchResult<MachineTranslateResult<ID>> {\n  const langCodesOptions = [\n    {\n      value: 'auto',\n      label: '%t(content:machineTrans.auto)'\n    }\n  ]\n  for (const lang of langcodes) {\n    langCodesOptions.push({\n      value: lang,\n      label: `${lang} %t(langcode:${lang})`\n    })\n  }\n\n  const catalog: DictSearchResult<MachineTranslateResult<ID>>['catalog'] = [\n    {\n      key: 'sl',\n      value: data.result.sl,\n      title: '%t(content:machineTrans.sl)',\n      options: langCodesOptions\n    },\n    {\n      key: 'tl',\n      value: data.result.tl,\n      title: '%t(content:machineTrans.tl)',\n      options: langCodesOptions\n    },\n    {\n      key: 'copySrc',\n      value: 'copySrc',\n      label: '%t(content:machineTrans.copySrc)'\n    },\n    {\n      key: 'copyTrans',\n      value: 'copyTrans',\n      label: '%t(content:machineTrans.copyTrans)'\n    }\n  ]\n\n  if (data.result.slInitial === 'hide') {\n    catalog.push({\n      key: 'showSl',\n      value: '',\n      label: '%t(content:machineTrans.showSl)'\n    })\n  }\n\n  return {\n    ...data,\n    catalog\n  }\n}\n"
  },
  {
    "path": "src/components/ShadowPortal/ShadowPortal.scss",
    "content": ".shadowPortal-appear,\n.shadowPortal-enter {\n  opacity: 0;\n}\n.shadowPortal-appear-active,\n.shadowPortal-enter-active {\n  opacity: 1;\n  transition: opacity 0.4s;\n}\n\n.shadowPortal-exit {\n  opacity: 1;\n}\n.shadowPortal-exit-active {\n  opacity: 0;\n  transition: opacity 0.1s;\n}\n"
  },
  {
    "path": "src/components/ShadowPortal/index.tsx",
    "content": "import React, { useMemo, useEffect, ReactNode } from 'react'\nimport ReactDOM from 'react-dom'\nimport CSSTransition, {\n  CSSTransitionProps\n} from 'react-transition-group/CSSTransition'\nimport root from 'react-shadow'\nimport { SALADICT_EXTERNAL } from '@/_helpers/saladict'\n\nexport const defaultTimeout = { enter: 400, exit: 100, appear: 400 }\n\nexport const defaultClassNames = 'shadowPortal'\n\n// prevent styles in shadow dom from inheriting outside rules\nconst styleResetBoundary: React.CSSProperties = { all: 'initial' }\n\nexport interface ShadowPortalOwnProps {\n  /** Unique id for the injected element */\n  id: string\n  /** Static content before the children  */\n  head?: ReactNode\n  shadowRootClassName?: string\n  innerRootClassName?: string\n  panelCSS?: string\n}\n\nexport type ShadowPortalProps = ShadowPortalOwnProps & CSSTransitionProps\n\n/**\n * Render a shadow DOM on Portal to a removable element with transition.\n * Insert the element to DOM when the Component mounts.\n * Remove the element from DOM when the Component unmounts.\n */\nexport const ShadowPortal = (props: ShadowPortalProps) => {\n  const {\n    id,\n    head,\n    shadowRootClassName,\n    innerRootClassName,\n    panelCSS,\n    onEnter,\n    onExited,\n    ...restProps\n  } = props\n\n  const $root = useMemo(() => {\n    let $root = document.getElementById(id)\n    if (!$root) {\n      $root = document.createElement('div')\n      $root.id = id\n      $root.className = `saladict-div ${shadowRootClassName ||\n        SALADICT_EXTERNAL}`\n    }\n    return $root\n  }, [shadowRootClassName])\n\n  // unmout element when React node unmounts\n  useEffect(\n    () => () => {\n      if ($root.parentNode) {\n        $root.remove()\n      }\n    },\n    []\n  )\n\n  return ReactDOM.createPortal(\n    <root.div className={shadowRootClassName || SALADICT_EXTERNAL}>\n      <div className={innerRootClassName} style={styleResetBoundary}>\n        {head}\n        {panelCSS ? <style>{panelCSS}</style> : null}\n        <CSSTransition\n          classNames={defaultClassNames}\n          mountOnEnter\n          unmountOnExit\n          appear\n          timeout={defaultTimeout}\n          {...restProps}\n          onEnter={(...args) => {\n            if (!$root.parentNode) {\n              document.documentElement.appendChild($root)\n            }\n            if (onEnter) {\n              return onEnter(...args)\n            }\n          }}\n          onExited={(...args) => {\n            if ($root.parentNode) {\n              $root.remove()\n            }\n            if (onExited) {\n              return onExited(...args)\n            }\n          }}\n        />\n      </div>\n    </root.div>,\n    $root\n  )\n}\n\nexport default ShadowPortal\n"
  },
  {
    "path": "src/components/Speaker/Speaker.scss",
    "content": "$speaker-duration: 1s !default;\n\n.saladict-Speaker {\n  display: inline-block;\n  width: 1.1em;\n  height: 1.1em;\n  text-decoration: none;\n  margin: 0 5px;\n  padding: 0;\n  line-height: 1;\n  vertical-align: text-bottom;\n  border: none;\n  background: no-repeat left / cover url('~@/assets/Speaker.svg');\n  user-select: none;\n  cursor: pointer;\n\n  &:hover {\n    outline: none;\n  }\n}\n\n.saladict-Speaker.isActive {\n  animation: saladict-Speaker-playing $speaker-duration steps(6) infinite;\n}\n\n@keyframes saladict-Speaker-playing {\n  from {\n    background-position-x: 0;\n  }\n  70% {\n    background-position-x: 100%;\n  }\n  100% {\n    background-position-x: 100%;\n  }\n}\n"
  },
  {
    "path": "src/components/Speaker/Speaker.stories.tsx",
    "content": "import React from 'react'\nimport { storiesOf } from '@storybook/react'\nimport { action } from '@storybook/addon-actions'\nimport { withKnobs, text, number } from '@storybook/addon-knobs'\nimport { jsxDecorator } from 'storybook-addon-jsx'\nimport { withPropsTable } from 'storybook-addon-react-docgen'\nimport {\n  withSaladictPanel,\n  withSideEffect,\n  mockRuntimeMessage\n} from '@/_helpers/storybook'\nimport {\n  Speaker,\n  StaticSpeakerContainer,\n  getStaticSpeakerString,\n  getStaticSpeaker\n} from './index'\nimport { timer } from '@/_helpers/promise-more'\nimport { StrElm } from '../StrElm'\n\nstoriesOf('Content Scripts|Components', module)\n  .addDecorator(withPropsTable)\n  .addDecorator(jsxDecorator)\n  .addDecorator(withKnobs)\n  .addDecorator(\n    withSideEffect(\n      mockRuntimeMessage(async message => {\n        if (message.type === 'PLAY_AUDIO') {\n          action('Play Audio')(message.payload)\n          await timer(Math.random() * 5000)\n          action('Audio End')(message.payload)\n        }\n      })\n    )\n  )\n  .addDecorator(\n    withSaladictPanel({\n      head: <style>{require('./Speaker.scss').toString()}</style>\n    })\n  )\n  .add('Speaker', () => {\n    return (\n      <Speaker\n        width={number('Icon Width', 20)}\n        height={number('Icon Height', 20)}\n        src={text('Audio URL', 'https://example.com/a.mp3')}\n      ></Speaker>\n    )\n  })\n  .add('StaticSpeakerContainer', () => {\n    const textStr = text(\n      'Audio URL for getStaticSpeakerString',\n      'https://example.com/a.mp3'\n    )\n\n    const textNode = text(\n      'Audio URL for getStaticSpeaker',\n      'https://example.com/b.mp3'\n    )\n\n    const node = getStaticSpeaker(textNode)\n\n    return (\n      <StaticSpeakerContainer\n        onPlayStart={async src => action('On Play Start')(src)}\n      >\n        <StrElm\n          html={`\n          <p>${getStaticSpeakerString(textStr)} ${textStr}</p>\n          <p>${node && node.outerHTML} ${textNode}</p>\n        `}\n        />\n      </StaticSpeakerContainer>\n    )\n  })\n"
  },
  {
    "path": "src/components/Speaker/index.tsx",
    "content": "import React, {\n  FC,\n  ComponentProps,\n  useCallback,\n  useState,\n  useContext\n} from 'react'\nimport { useUpdateEffect } from 'react-use'\nimport { timer, reflect } from '@/_helpers/promise-more'\nimport { isTagName } from '@/_helpers/dom'\n\n/** onPlayStart */\nconst StaticSpeakerContext = React.createContext<\n  (src: string) => Promise<void>\n>(async () => {})\n\nexport interface SpeakerProps {\n  /** render nothing when no src */\n  readonly src?: string | (() => Promise<string>)\n  /** @default 1.2em */\n  readonly width?: number | string\n  /** @default 1.2em */\n  readonly height?: number | string\n}\n\n/**\n * Speaker for playing audio files\n */\nexport const Speaker: FC<SpeakerProps> = props => {\n  const [src, setSrc] = useState(() =>\n    typeof props.src === 'string' ? props.src : '#'\n  )\n\n  const onPlayStart = useContext(StaticSpeakerContext)\n\n  useUpdateEffect(() => {\n    setSrc(typeof props.src === 'string' ? props.src : '#')\n  }, [props.src])\n\n  if (!props.src) return null\n\n  const width = props.width || props.height || '1.2em'\n  const height = props.height || width\n\n  return (\n    <a\n      className=\"saladict-Speaker\"\n      href={src}\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n      style={{ width, height }}\n      onClick={async e => {\n        if (src === '#' && typeof props.src === 'function') {\n          e.stopPropagation()\n          e.preventDefault()\n          const result = await props.src()\n          onPlayStart(result)\n          setSrc(result)\n        }\n      }}\n    ></a>\n  )\n}\n\nexport default React.memo(Speaker)\n\nexport interface StaticSpeakerContainerProps\n  extends Omit<ComponentProps<'div'>, 'onClick'> {\n  onPlayStart: (src: string) => Promise<void>\n}\n\n/**\n * Listens to HTML injected Speakers in childern\n */\nexport const StaticSpeakerContainer: FC<StaticSpeakerContainerProps> = props => {\n  const { onPlayStart, ...restProps } = props\n\n  const onClick = useCallback(\n    (e: React.MouseEvent<HTMLDivElement>) => {\n      if (\n        e.target &&\n        isTagName(e.target, 'a') &&\n        e.target['href'] &&\n        e.target['href'] !== '#' &&\n        e.target['classList'] &&\n        e.target['classList'].contains('saladict-Speaker')\n      ) {\n        e.preventDefault()\n        e.stopPropagation()\n\n        const target = e.target as HTMLAnchorElement\n        target.classList.add('isActive')\n\n        reflect([timer(1000), onPlayStart(target.href)]).then(() => {\n          target.classList.remove('isActive')\n        })\n      }\n    },\n    [onPlayStart]\n  )\n\n  return (\n    <StaticSpeakerContext.Provider value={onPlayStart}>\n      <div onClick={onClick} {...restProps} />\n    </StaticSpeakerContext.Provider>\n  )\n}\n\n/**\n * Returns a anchor element\n */\nexport const getStaticSpeaker = (src?: string | null) => {\n  if (!src) {\n    return ''\n  }\n\n  const $a = document.createElement('a')\n  $a.target = '_blank'\n  $a.href = src\n  $a.className = 'saladict-Speaker'\n  return $a\n}\n\n/**\n * Returns an anchor element string\n */\nexport const getStaticSpeakerString = (src?: string | null) =>\n  src\n    ? `<a href=\"${src}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"saladict-Speaker\"></a>`\n    : ''\n"
  },
  {
    "path": "src/components/StarRates/index.tsx",
    "content": "import React from 'react'\n\nexport interface StarRatesProps {\n  className?: string\n  rate?: number\n  height?: number\n  gutter?: number\n  style?: React.CSSProperties\n  max?: number\n}\n\nexport default class StarRates extends React.PureComponent<StarRatesProps> {\n  render() {\n    const className = this.props.className || 'widget-StarRates'\n    const max = this.props.max || 5\n    const rate = Number(this.props.rate) % (max + 1) || 0\n    const height = this.props.height || '1.5em'\n    const gutter = this.props.gutter || '0.3em'\n\n    const style = {\n      height: height,\n      ...(this.props.style || {})\n    }\n\n    return (\n      <div className={className} style={style}>\n        {Array.from(Array(max)).map((_, i) => (\n          <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            viewBox=\"0 0 426.67 426.67\"\n            key={i + rate}\n            width={height}\n            height={height}\n            style={{ marginRight: i === max - 1 ? '' : gutter }}\n          >\n            <path\n              fill={i < rate ? '#FAC917' : '#d1d8de'}\n              d=\"M213.33 10.44l65.92 133.58 147.42 21.42L320 269.4l25.17 146.83-131.84-69.32-131.85 69.34 25.2-146.82L0 165.45l147.4-21.42\"\n            />\n          </svg>\n        ))}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "src/components/StrElm/index.tsx",
    "content": "import React, { PropsWithChildren, useMemo, useState } from 'react'\nimport { useIsomorphicLayoutEffect } from 'react-use'\n\nexport type StrElmProps<\n  T extends keyof JSX.IntrinsicElements = keyof JSX.IntrinsicElements\n> = {\n  tag?: T\n  html: string\n} & JSX.IntrinsicElements[T]\n\nexport const StrElm = <\n  T extends keyof JSX.IntrinsicElements = keyof JSX.IntrinsicElements\n>(\n  props: PropsWithChildren<StrElmProps<T>>\n) => {\n  const { tag = 'div', html, ...restProps } = props\n  const child = useMemo<DocumentFragment | null>(() => {\n    try {\n      const fragment = document.createDocumentFragment()\n      const doc = new DOMParser().parseFromString(html, 'text/html')\n      Array.from(doc.body.childNodes).forEach(el => {\n        fragment.appendChild(el)\n      })\n      return fragment\n    } catch (e) {\n      if (process.env.DEBUG) {\n        console.error(e)\n      }\n    }\n    return null\n  }, [html])\n\n  const [node, setNode] = useState<HTMLElement | null>(null)\n\n  useIsomorphicLayoutEffect(() => {\n    if (child && node) {\n      while (node.childNodes.length > 0) {\n        node.childNodes[0].remove()\n      }\n      node.appendChild(child)\n    }\n  }, [child, node])\n\n  return React.createElement(tag, { ...restProps, ref: setNode })\n}\n"
  },
  {
    "path": "src/components/Waveform/Waveform.scss",
    "content": "#waveform-container {\n  min-height: 128px;\n  background: var(--color-background);\n}\n\n.saladict-waveformWrap {\n  overflow: hidden;\n  height: 165px;\n}\n\n.saladict-waveformCtrl {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  font-size: 16px;\n}\n\n$waveformPlayWidth: 1.3em;\n$halfWaveformPlayWidth: math.div($waveformPlayWidth, 2);\n\n%palyFGBlock {\n  content: '';\n  position: absolute;\n  top: 0;\n  width: 0;\n  height: 0;\n  border: $halfWaveformPlayWidth solid transparent;\n  border-width: $halfWaveformPlayWidth $waveformPlayWidth;\n  border-left-color: var(--color-font);\n  transition-property: border, height, right;\n  transition-duration: 0.25s;\n  transition-timing-function: ease;\n}\n\n.saladict-waveformPlay {\n  margin: 0.5em;\n  padding: 3px;\n  border: none;\n  background: none;\n  cursor: pointer;\n\n  &:hover {\n    outline: none;\n  }\n}\n\n.saladict-waveformPlay_FG {\n  position: relative;\n  width: $waveformPlayWidth;\n  height: $waveformPlayWidth;\n  overflow: hidden;\n\n  &:before {\n    @extend %palyFGBlock;\n    left: 0;\n  }\n\n  &.isPlaying:before {\n    height: $waveformPlayWidth;\n    border-width: 0 ($halfWaveformPlayWidth - 0.05em);\n  }\n\n  &:after {\n    @extend %palyFGBlock;\n    right: 0;\n  }\n\n  &.isPlaying:after {\n    right: -($halfWaveformPlayWidth - 0.05em);\n    height: $waveformPlayWidth;\n    border-width: 0 ($halfWaveformPlayWidth - 0.05em);\n  }\n}\n\n.saladict-waveformSpeed {\n  width: 3em;\n  text-align: center;\n}\n\n.saladict-waveformBtn_label {\n  display: block;\n  width: 1.3em;\n  height: 1.3em;\n  overflow: hidden;\n  margin: 0.5em;\n  cursor: pointer;\n}\n\n.saladict-waveformBtn_label > input {\n  display: inline-block;\n  position: absolute;\n  width: 0;\n  height: 0;\n  opacity: 0;\n  z-index: -20000px;\n}\n\n.saladict-waveformPitch {\n  margin: 0.5em 0;\n}\n"
  },
  {
    "path": "src/components/Waveform/Waveform.stories.tsx",
    "content": "import React from 'react'\nimport { storiesOf } from '@storybook/react'\nimport { jsxDecorator } from 'storybook-addon-jsx'\nimport { withPropsTable } from 'storybook-addon-react-docgen'\nimport { withKnobs, boolean } from '@storybook/addon-knobs'\nimport {\n  withSaladictPanel,\n  withSideEffect,\n  mockRuntimeMessage\n} from '@/_helpers/storybook'\nimport { Waveform } from './Waveform'\n\nstoriesOf('Content Scripts|Dict Panel', module)\n  .addParameters({\n    backgrounds: [\n      { name: 'Saladict', value: '#5caf9e', default: true },\n      { name: 'Black', value: '#000' }\n    ]\n  })\n  .addDecorator(withPropsTable)\n  .addDecorator(jsxDecorator)\n  .addDecorator(withKnobs)\n  .addDecorator(\n    withSaladictPanel({\n      head: <style>{require('./Waveform.scss').toString()}</style>,\n      height: 'auto'\n    })\n  )\n  .addDecorator(\n    withSideEffect(\n      mockRuntimeMessage(async message => {\n        switch (message.type as string) {\n          case 'PAGE_INFO':\n            return {\n              pageId: 'page-id'\n            }\n          case '[[LAST_PLAY_AUDIO]]':\n            return require('@sb/assets/shewalksinbeauty.mp3')\n          default:\n            break\n        }\n      })\n    )\n  )\n  .add('Waveform', () => {\n    const darkMode = boolean('Dark Mode', true)\n\n    return <Waveform darkMode={darkMode} />\n  })\n"
  },
  {
    "path": "src/components/Waveform/Waveform.tsx",
    "content": "import * as React from 'react'\nimport classNames from 'classnames'\nimport WaveSurfer from 'wavesurfer.js'\nimport RegionsPlugin from 'wavesurfer.js/dist/plugin/wavesurfer.regions.min.js'\nimport NumberEditor from 'react-number-editor'\nimport { message, storage } from '@/_helpers/browser-api'\nimport { isFirefox } from '@/_helpers/saladict'\nimport { SoundTouch, SimpleFilter, getWebAudioNode } from 'soundtouchjs'\n\ninterface AnyObject {\n  [index: string]: any\n}\n\ninterface WaveformProps {\n  darkMode: boolean\n}\n\ninterface WaveformState {\n  blob?: Blob\n  isPlaying: boolean\n  speed: number\n  loop: boolean\n  /** use pitch stretcher */\n  pitchStretch: boolean\n}\n\nexport class Waveform extends React.PureComponent<\n  WaveformProps,\n  WaveformState\n> {\n  containerRef = React.createRef<HTMLDivElement>()\n  wavesurfer: WaveSurfer | null | undefined\n  region: AnyObject | null | undefined\n  soundTouch: AnyObject | null | undefined\n  soundTouchNode: AnyObject | null | undefined\n  /** Sync Wavesurfer & SoundTouch position */\n  shouldSTSync: boolean = false\n  /** play when file is loaded */\n  playOnLoad = true\n  src?: string\n\n  state: WaveformState = {\n    isPlaying: false,\n    speed: 1,\n    loop: false,\n    pitchStretch: !isFirefox\n  }\n\n  initSoundTouch = (wavesurfer: WaveSurfer) => {\n    const buffer = wavesurfer.backend.buffer\n    const bufferLength = buffer.length\n    const lChannel = buffer.getChannelData(0)\n    const rChannel =\n      buffer.numberOfChannels > 1 ? buffer.getChannelData(1) : lChannel\n    let seekingDiff = 0\n    const source = {\n      extract: (target, numFrames, position) => {\n        if (this.shouldSTSync) {\n          // get the new diff\n          seekingDiff =\n            ~~(wavesurfer.backend.getPlayedPercents() * bufferLength) - position\n          this.shouldSTSync = false\n        }\n\n        position += seekingDiff\n\n        for (let i = 0; i < numFrames; i++) {\n          target[i * 2] = lChannel[i + position]\n          target[i * 2 + 1] = rChannel[i + position]\n        }\n\n        return Math.min(numFrames, bufferLength - position)\n      }\n    }\n\n    this.soundTouch = new SoundTouch(wavesurfer.backend.ac.sampleRate)\n    this.soundTouchNode = getWebAudioNode(\n      wavesurfer.backend.ac,\n      new SimpleFilter(source, this.soundTouch)\n    )\n    wavesurfer.backend.setFilter(this.soundTouchNode)\n  }\n\n  initWavesurfer = () => {\n    const wavesurfer = WaveSurfer.create({\n      container: this.containerRef.current!,\n      waveColor: '#f9690e',\n      progressColor: '#B71C0C',\n      plugins: [RegionsPlugin.create()]\n    })\n\n    this.wavesurfer = wavesurfer\n\n    wavesurfer.enableDragSelection({})\n\n    wavesurfer.on('region-created', region => {\n      this.removeRegion()\n      this.region = region\n    })\n    wavesurfer.on('region-update-end', this.play)\n    wavesurfer.on('region-out', this.onPlayEnd)\n\n    wavesurfer.on('seek', () => {\n      if (!this.isInRegion()) {\n        this.removeRegion()\n      }\n      this.shouldSTSync = true\n    })\n\n    wavesurfer.on('ready', this.onLoad)\n\n    wavesurfer.on('finish', this.onPlayEnd)\n  }\n\n  onLoad = () => {\n    if (this.playOnLoad) {\n      this.play()\n    }\n    // reset state\n    this.playOnLoad = true\n  }\n\n  play = () => {\n    this.setState({ isPlaying: true })\n    if (this.wavesurfer) {\n      if (\n        this.state.pitchStretch &&\n        this.soundTouchNode &&\n        this.wavesurfer.getFilters().length <= 0\n      ) {\n        this.wavesurfer.backend.setFilter(this.soundTouchNode)\n      }\n      if (this.region && !this.isInRegion()) {\n        this.wavesurfer.play(this.region.start)\n      } else {\n        this.wavesurfer.play()\n      }\n    }\n    this.shouldSTSync = true\n  }\n\n  pause = () => {\n    this.setState({ isPlaying: false })\n    if (this.soundTouchNode) {\n      this.soundTouchNode.disconnect()\n    }\n    if (this.wavesurfer) {\n      this.wavesurfer.pause()\n      this.wavesurfer.backend.disconnectFilters()\n    }\n  }\n\n  togglePlay = () => {\n    this.state.isPlaying ? this.pause() : this.play()\n  }\n\n  onPlayEnd = () => {\n    // could be region end\n    this.state.loop ? this.play() : this.pause()\n  }\n\n  updateSpeed = (speed: number) => {\n    this.setState({ speed })\n\n    if (speed < 0.1 || speed > 3) {\n      return\n    }\n\n    if (this.wavesurfer) {\n      this.wavesurfer.setPlaybackRate(speed)\n      if (speed !== 1 && this.state.pitchStretch && !this.soundTouch) {\n        this.initSoundTouch(this.wavesurfer)\n      }\n      if (this.soundTouch) {\n        this.soundTouch.tempo = speed\n      }\n    }\n\n    this.shouldSTSync = true\n  }\n\n  toggleLoop = (e: React.ChangeEvent<HTMLInputElement>) => {\n    this.setState({ loop: e.currentTarget.checked })\n    if (e.currentTarget.checked && !this.state.isPlaying) {\n      this.play()\n    }\n  }\n\n  togglePitchStretch = (e: React.ChangeEvent<HTMLInputElement>) => {\n    this.updatePitchStretch(e.currentTarget.checked)\n    storage.local.set({ waveform_pitch: e.currentTarget.checked })\n  }\n\n  updatePitchStretch = (flag: boolean) => {\n    this.setState({ pitchStretch: flag })\n\n    if (flag) {\n      if (\n        this.state.speed !== 1 &&\n        this.soundTouchNode &&\n        this.wavesurfer &&\n        this.wavesurfer.getFilters().length <= 0\n      ) {\n        this.wavesurfer.backend.setFilter(this.soundTouchNode)\n        this.shouldSTSync = true\n      }\n    } else {\n      if (this.soundTouchNode) {\n        this.soundTouchNode.disconnect()\n      }\n      if (this.wavesurfer) {\n        this.wavesurfer.backend.disconnectFilters()\n      }\n    }\n  }\n\n  isInRegion = (region = this.region): boolean => {\n    if (region && this.wavesurfer) {\n      const curTime = this.wavesurfer.getCurrentTime()\n      return curTime >= region.start && curTime <= region.end\n    }\n    return false\n  }\n\n  removeRegion = () => {\n    if (this.region) {\n      this.region.remove()\n    }\n    this.region = null\n  }\n\n  reset = () => {\n    this.removeRegion()\n    this.updateSpeed(1)\n    if (this.wavesurfer) {\n      this.wavesurfer.pause()\n      this.wavesurfer.empty()\n      this.wavesurfer.backend.disconnectFilters()\n    }\n    if (this.soundTouch) {\n      this.soundTouch.clear()\n      this.soundTouch.tempo = 1\n    }\n    if (this.soundTouchNode) {\n      this.soundTouchNode.disconnect()\n    }\n    this.soundTouch = null\n    this.soundTouchNode = null\n    this.shouldSTSync = false\n  }\n\n  load = (src: string) => {\n    if (src) {\n      if (this.wavesurfer) {\n        this.reset()\n      } else {\n        this.initWavesurfer()\n      }\n\n      if (this.wavesurfer) {\n        this.wavesurfer.load(src)\n        // https://github.com/katspaugh/wavesurfer.js/issues/1657\n        if (\n          this.wavesurfer.backend.ac.state === 'suspended' &&\n          this.playOnLoad\n        ) {\n          // fallback\n          new Audio(src).play()\n        }\n      }\n    } else {\n      this.reset()\n    }\n  }\n\n  async componentDidMount() {\n    message.self.addListener('PLAY_AUDIO', async ({ payload: src }) => {\n      this.load(src)\n    })\n\n    message.self\n      .send<'LAST_PLAY_AUDIO'>({ type: 'LAST_PLAY_AUDIO' })\n      .then(response => {\n        if (\n          response &&\n          response.src &&\n          response.timestamp - Date.now() < 10000\n        ) {\n          this.load(response.src)\n        } else {\n          this.playOnLoad = false\n          this.load(\n            // Nothing to play\n            `https://fanyi.sogou.com/reventondc/synthesis?text=Nothing%20to%20play&speed=1&lang=en&from=translateweb`\n          )\n        }\n      })\n\n    storage.local.get('waveform_pitch').then(({ waveform_pitch }) => {\n      if (waveform_pitch != null) {\n        this.updatePitchStretch(Boolean(waveform_pitch))\n      }\n    })\n  }\n\n  componentWillUnmount() {\n    this.reset()\n    if (this.wavesurfer) {\n      this.wavesurfer.destroy()\n      this.wavesurfer = null\n    }\n  }\n\n  render() {\n    return (\n      <div className={classNames({ darkMode: this.props.darkMode })}>\n        <div className=\"saladict-waveformWrap saladict-theme\">\n          <div ref={this.containerRef} />\n          <div className=\"saladict-waveformCtrl\">\n            <button\n              type=\"button\"\n              className=\"saladict-waveformPlay\"\n              title=\"Play/Pause\"\n              onClick={this.togglePlay}\n            >\n              <div\n                className={`saladict-waveformPlay_FG${\n                  this.state.isPlaying ? ' isPlaying' : ''\n                }`}\n              />\n            </button>\n            <NumberEditor\n              className=\"saladict-waveformSpeed\"\n              title=\"Speed\"\n              value={this.state.speed}\n              min={0.1} // too low could cause error\n              max={3}\n              step={0.005}\n              decimals={3}\n              onValueChange={this.updateSpeed}\n            />\n            <label className=\"saladict-waveformBtn_label\" title=\"Loop\">\n              {this.state.loop ? (\n                <svg\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  viewBox=\"0 0 512 512\"\n                  fill=\"var(--color-font)\"\n                >\n                  <path d=\"M23.242 388.417l162.59 120.596v-74.925h300.281l-.297-240.358-89.555-.239-.44 150.801H185.832l.81-75.934-163.4 120.06z\" />\n                  <path d=\"M490.884 120.747L328.294.15l.001 74.925H28.013l.297 240.358 89.555.239.44-150.801h209.99l-.81 75.934 163.4-120.06z\" />\n                </svg>\n              ) : (\n                <svg\n                  viewBox=\"0 0 512 512\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  fill=\"var(--color-divider)\"\n                >\n                  <path d=\"M 23.242 388.417 L 23.243 388.417 L 23.242 388.418 Z M 23.243 388.418 L 186.642 268.358 L 185.832 344.292 L 283.967 344.292 L 331.712 434.088 L 185.832 434.088 L 185.832 509.013 Z M 395.821 344.292 L 396.261 193.491 L 485.816 193.73 L 486.113 434.088 L 388.064 434.088 L 340.319 344.292 Z\" />\n                  <path d=\"M 490.884 120.747 L 490.883 120.746 L 490.885 120.745 Z M 490.883 120.746 L 327.485 240.805 L 328.295 164.871 L 244.267 164.871 L 196.521 75.075 L 328.295 75.075 L 328.294 0.15 Z M 118.305 164.871 L 117.865 315.672 L 28.31 315.433 L 28.013 75.075 L 141.077 75.075 L 188.823 164.871 Z\" />\n                  <rect\n                    x=\"525.825\"\n                    y=\"9.264\"\n                    width=\"45.879\"\n                    height=\"644.398\"\n                    transform=\"matrix(0.882947, -0.469472, 0.469472, 0.882947, -403.998657, 225.106232)\"\n                  />\n                </svg>\n              )}\n              <input\n                type=\"checkbox\"\n                checked={this.state.loop}\n                onChange={this.toggleLoop}\n              />\n            </label>\n            {!isFirefox && ( // @TOFIX SoundTouch bug with Firefox\n              <label\n                className=\"saladict-waveformPitch saladict-waveformBtn_label\"\n                title=\"Pitch Stretch\"\n              >\n                <svg\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  viewBox=\"0 0 467.987 467.987\"\n                  fill={\n                    this.state.pitchStretch\n                      ? 'var(--color-font)'\n                      : 'var(--color-divider)'\n                  }\n                >\n                  <path d=\"M70.01 146.717h47.924V321.27H70.01zM210.032 146.717h47.924V321.27h-47.924zM350.053 146.717h47.924V321.27h-47.924zM0 196.717h47.924v74.553H0zM280.042 196.717h47.924v74.553h-47.924zM420.063 196.717h47.924v74.553h-47.924zM140.021 96.717h47.924V371.27h-47.924z\" />\n                </svg>\n                <input\n                  type=\"checkbox\"\n                  checked={this.state.pitchStretch}\n                  onChange={this.togglePitchStretch}\n                />\n              </label>\n            )}\n          </div>\n        </div>\n      </div>\n    )\n  }\n}\n\nexport default Waveform\n"
  },
  {
    "path": "src/components/WordPage/ExportModal/Linebreak.tsx",
    "content": "import React, { FC } from 'react'\nimport { Select } from 'antd'\nimport { TFunction } from 'i18next'\n\nexport type LineBreakOption = '' | 'n' | 'p' | 'br' | 'space'\n\nexport interface LineBreakProps {\n  t: TFunction\n  value: LineBreakOption\n  onChange: (value: LineBreakOption) => void\n}\n\nexport const LineBreak: FC<LineBreakProps> = ({ t, value, onChange }) => (\n  <Select value={value} style={{ width: 210 }} onChange={onChange}>\n    <Select.Option value=\"\">{t('export.linebreak.default')}</Select.Option>\n    <Select.Option value=\"n\">{t('export.linebreak.n')}</Select.Option>\n    <Select.Option value=\"br\">{t('export.linebreak.br')}</Select.Option>\n    <Select.Option value=\"p\">{t('export.linebreak.p')}</Select.Option>\n    <Select.Option value=\"space\">{t('export.linebreak.space')}</Select.Option>\n  </Select>\n)\n\nexport const LineBreakMemo = React.memo(LineBreak)\n"
  },
  {
    "path": "src/components/WordPage/ExportModal/PlaceholderTable.tsx",
    "content": "import React, { FC } from 'react'\nimport { Table } from 'antd'\nimport { TFunction } from 'i18next'\n\nexport interface PlaceholderTableProps {\n  t: TFunction\n}\n\nexport const PlaceholderTable: FC<PlaceholderTableProps> = ({ t }) => (\n  <Table\n    rowKey=\"plcholderL\"\n    pagination={false}\n    size=\"small\"\n    bordered={true}\n    style={{ marginBottom: '1em' }}\n    dataSource={[\n      {\n        plcholderL: '%text%',\n        contentL: t('common:note.word'),\n        plcholderR: '%title%',\n        contentR: t('common:note.srcTitle')\n      },\n      {\n        plcholderL: '%context%',\n        contentL: t('common:note.context'),\n        plcholderR: '%url%',\n        contentR: t('common:note.srcLink')\n      },\n      {\n        plcholderL: '%note%',\n        contentL: t('common:note.note'),\n        plcholderR: '%favicon%',\n        contentR: t('common:note.srcFavicon')\n      },\n      {\n        plcholderL: '%trans%',\n        contentL: t('common:note.trans'),\n        plcholderR: '%date%',\n        contentR: t('common:note.date')\n      },\n      {\n        plcholderL: '%contextCloze%',\n        contentL: t('common:note.contextCloze'),\n        plcholderR: '',\n        contentR: ''\n      }\n    ]}\n    columns={[\n      {\n        title: t('export.placeholder'),\n        dataIndex: 'plcholderL',\n        key: 'plcholderL',\n        width: '25%',\n        align: 'center'\n      },\n      {\n        title: t('export.gencontent'),\n        dataIndex: 'contentL',\n        key: 'contentL',\n        width: '25%',\n        align: 'center'\n      },\n      {\n        title: t('export.placeholder'),\n        dataIndex: 'plcholderR',\n        key: 'plcholderR',\n        width: '25%',\n        align: 'center'\n      },\n      {\n        title: t('export.gencontent'),\n        dataIndex: 'contentR',\n        key: 'contentR',\n        width: '25%',\n        align: 'center'\n      }\n    ]}\n  />\n)\n\nexport const PlaceholderTableMemo = React.memo(PlaceholderTable)\n"
  },
  {
    "path": "src/components/WordPage/ExportModal/index.tsx",
    "content": "import React, { FC, useState, useEffect, useContext } from 'react'\nimport { Modal, Layout, Switch } from 'antd'\nimport escapeHTML from 'lodash/escape'\nimport { Word, newWord } from '@/_helpers/record-manager'\nimport { useTranslate, I18nContext } from '@/_helpers/i18n'\nimport { storage } from '@/_helpers/browser-api'\nimport { LineBreakMemo, LineBreakOption } from './Linebreak'\nimport { PlaceholderTableMemo } from './PlaceholderTable'\n\nconst keywordMatchStr = `%(${Object.keys(newWord()).join('|')}|contextCloze)%`\n\nexport type ExportModalTitle = 'all' | 'selected' | 'page' | ''\n\nexport interface ExportModalProps {\n  title: ExportModalTitle\n  rawWords: Word[]\n  onCancel: (e: React.MouseEvent<any>) => any\n}\n\nexport const ExportModal: FC<ExportModalProps> = props => {\n  const lang = useContext(I18nContext)\n  const { t } = useTranslate(['wordpage', 'common'])\n  const [template, setTemplate] = useState('%text%\\n%trans%\\n%context%\\n')\n  const [lineBreak, setLineBreak] = useState<LineBreakOption>('')\n  const [escape, setEscape] = useState(false)\n\n  const [output, setOutput] = useState('')\n\n  useEffect(() => {\n    setOutput(\n      props.rawWords\n        .map(word =>\n          template.replace(new RegExp(keywordMatchStr, 'g'), (match, k) => {\n            switch (k) {\n              case 'date':\n                return new Date(word.date).toLocaleDateString(lang)\n              case 'trans':\n              case 'note':\n              case 'context':\n              case 'contextCloze': {\n                let text = word[k === 'contextCloze' ? 'context' : k] || ''\n                if (escape) {\n                  text = escapeHTML(text)\n                }\n                switch (lineBreak) {\n                  case 'n':\n                    text = text.replace(/\\n|\\r\\n/g, '\\\\n')\n                    break\n                  case 'br':\n                    text = text.replace(/\\n|\\r\\n/g, '<br>')\n                    break\n                  case 'p':\n                    text = text\n                      .split(/\\n|\\r\\n/)\n                      .map(line => `<p>${line}</p>`)\n                      .join('')\n                    break\n                  case 'space':\n                    text = text.replace(/\\n|\\r\\n/g, ' ')\n                    break\n                  default:\n                    break\n                }\n                if (k === 'contextCloze' && word.text) {\n                  const matcher = new RegExp(\n                    word.text.replace(/[-/\\\\^$*+?.()|[\\]{}]/g, '\\\\$&'),\n                    'gi'\n                  )\n                  text = text.replace(\n                    matcher,\n                    ''.padStart(word.text.length, '_')\n                  )\n                }\n                return text\n              }\n              default:\n                return word[k] || ''\n            }\n          })\n        )\n        .join('\\n')\n    )\n  }, [props.rawWords, lang, template, lineBreak, escape])\n\n  useEffect(() => {\n    storage.sync\n      .get<{\n        wordpageTemplate: string\n        wordpageLineBreak: LineBreakOption\n      }>(['wordpageTemplate', 'wordpageLineBreak'])\n      .then(({ wordpageTemplate, wordpageLineBreak }) => {\n        if (wordpageTemplate != null) {\n          setTemplate(wordpageTemplate)\n        }\n        if (wordpageLineBreak != null) {\n          setLineBreak(wordpageLineBreak)\n        }\n      })\n\n    storage.local\n      .get<{\n        wordpageHTMLEscape: boolean\n      }>('wordpageHTMLEscape')\n      .then(({ wordpageHTMLEscape }) => {\n        if (wordpageHTMLEscape != null) {\n          setEscape(wordpageHTMLEscape)\n        }\n      })\n  }, [])\n\n  const exportWords = () => {\n    browser.runtime.getPlatformInfo().then(({ os }) => {\n      const content = os === 'win' ? output.replace(/\\r\\n|\\n/g, '\\r\\n') : output\n      const file = new Blob([content], { type: 'text/plain;charset=utf-8' })\n      const a = document.createElement('a')\n      a.style.display = 'none'\n      a.href = URL.createObjectURL(file)\n      a.download = `saladict-words-${Date.now()}.txt`\n      // firefox\n      a.target = '_blank'\n      document.body.appendChild(a)\n\n      a.click()\n    })\n  }\n\n  return (\n    <Modal\n      title={props.title ? t(`export.${props.title}`) : ' '}\n      visible={!!props.title}\n      destroyOnClose={true}\n      onOk={exportWords}\n      onCancel={props.onCancel}\n      okText={t('common:export')}\n      style={{ width: '90vw', maxWidth: 1200, top: 24 }}\n      width=\"90vw\"\n    >\n      <Layout\n        style={{ height: '70vh', maxHeight: 1000, background: 'transparent' }}\n      >\n        <Layout.Content style={{ display: 'flex', flexDirection: 'column' }}>\n          <p className=\"export-Description\">\n            {t('export.description')}\n            <a\n              href=\"https://saladict.crimx.com/anki.html\"\n              target=\"_blank\"\n              rel=\"nofollow noopener noreferrer\"\n            >\n              {t('export.explain')}\n            </a>\n          </p>\n          <PlaceholderTableMemo t={t} />\n          <div\n            style={{\n              display: 'flex',\n              justifyContent: 'space-between',\n              alignItems: 'center',\n              marginBottom: '1em'\n            }}\n          >\n            <LineBreakMemo\n              t={t}\n              value={lineBreak}\n              onChange={value => {\n                setLineBreak(value)\n                storage.sync.set({ wordpageLineBreak: value })\n                if (value === 'br' || value === 'p') {\n                  setEscape(true)\n                  storage.local.set({ wordpageHTMLEscape: true })\n                }\n              }}\n            />\n            <Switch\n              title={t('export.htmlescape.title')}\n              checked={escape}\n              onChange={checked => {\n                setEscape(checked)\n                storage.local.set({ wordpageHTMLEscape: checked })\n              }}\n              checkedChildren={t('export.htmlescape.text')}\n              unCheckedChildren={t('export.htmlescape.text')}\n            />\n          </div>\n          <textarea\n            style={{ flex: 1, width: '100%' }}\n            value={template}\n            onChange={({ currentTarget: { value } }) => {\n              setTemplate(value)\n              storage.sync.set({ wordpageTemplate: value })\n            }}\n          />\n        </Layout.Content>\n        <Layout.Sider\n          width=\"50%\"\n          style={{ paddingLeft: 24, background: 'transparent' }}\n        >\n          <textarea\n            style={{ width: '100%', height: '100%' }}\n            readOnly={true}\n            value={output}\n          />\n        </Layout.Sider>\n      </Layout>\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "src/components/WordPage/Header.tsx",
    "content": "import React, { FC } from 'react'\nimport { TFunction } from 'i18next'\nimport { Layout, Input, Dropdown, Menu, Button, Modal } from 'antd'\nimport { MenuProps } from 'antd/lib/menu'\nimport { DownOutlined } from '@ant-design/icons'\nimport { DBArea } from '@/_helpers/record-manager'\n\nexport interface WordPageProps {\n  t: TFunction\n  area: DBArea\n  searchText: string\n  totalCount: number\n  selectedCount: number\n  onSearchTextChanged: (text: string) => void\n  onExport: MenuProps['onClick']\n  onDelete: (key: string) => void\n}\n\nexport const Header: FC<WordPageProps> = props => {\n  const { t } = props\n\n  return (\n    <Layout.Header className=\"wordpage-Header\">\n      <div className=\"wordpage-Title\">\n        <h1 className=\"wordpage-Title_head\">\n          {t(`title.${props.area}`)}{' '}\n          <small className=\"wordpage-Title_small\">({t('localonly')})</small>\n        </h1>\n        <div style={{ whiteSpace: 'nowrap' }}>\n          {props.totalCount > 0 && (\n            <span className=\"wordpage-Wordcount\">\n              {t(`wordCount.total`, { count: props.totalCount })}\n            </span>\n          )}\n          {props.selectedCount > 0 && (\n            <span className=\"wordpage-Wordcount\">\n              {t(`wordCount.selected`, { count: props.selectedCount })}\n            </span>\n          )}\n        </div>\n      </div>\n      <div className=\"wordpage-BtnGroup\">\n        <Input\n          style={{ width: '15em' }}\n          placeholder=\"Search\"\n          onChange={e => props.onSearchTextChanged(e.currentTarget.value)}\n          value={props.searchText}\n        />\n        <Dropdown\n          overlay={\n            <Menu onClick={props.onExport}>\n              {props.selectedCount > 0 && (\n                <Menu.Item key=\"selected\">{t('export.selected')}</Menu.Item>\n              )}\n              <Menu.Item key=\"page\">{t('export.page')}</Menu.Item>\n              <Menu.Item key=\"all\">{t('export.all')}</Menu.Item>\n            </Menu>\n          }\n        >\n          <Button style={{ marginLeft: 8 }}>\n            {t('export.title')} <DownOutlined />\n          </Button>\n        </Dropdown>\n        <Dropdown\n          overlay={\n            <Menu\n              onClick={({ key }) => {\n                if (key) {\n                  Modal.confirm({\n                    title: t('delete'),\n                    content: t(`delete.${key}`) + t('delete.confirm'),\n                    okType: 'danger',\n                    onOk: () => props.onDelete(`${key}`)\n                  })\n                }\n              }}\n            >\n              {props.selectedCount > 0 && (\n                <Menu.Item key=\"selected\">{t('delete.selected')}</Menu.Item>\n              )}\n              <Menu.Item key=\"page\">{t('delete.page')}</Menu.Item>\n              <Menu.Item key=\"all\">{t('delete.all')}</Menu.Item>\n            </Menu>\n          }\n        >\n          <Button type=\"primary\" danger style={{ marginLeft: 8 }}>\n            {t('delete.title')} <DownOutlined />\n          </Button>\n        </Dropdown>\n      </div>\n    </Layout.Header>\n  )\n}\n"
  },
  {
    "path": "src/components/WordPage/WordTable.tsx",
    "content": "import React, { FC, ReactNode, useMemo } from 'react'\nimport i18next, { TFunction } from 'i18next'\nimport { Button, Tooltip } from 'antd'\nimport Table, { ColumnsType, TableProps } from 'antd/lib/table'\nimport { Word, DBArea } from '@/_helpers/record-manager'\nimport { message } from '@/_helpers/browser-api'\nimport { useTranslate } from '@/_helpers/i18n'\n\nexport const colSelectionWidth = 48\nconst colDateWidth = 150\nconst colEditWidth = 80\nconst fixedWidth = colSelectionWidth + colDateWidth + colEditWidth\nconst colTextWidth = `calc((100vw - ${fixedWidth}px) / 7)`\nconst restWidth = `calc((100vw - ${fixedWidth}px) * 2 / 7)`\n\nexport interface WordTableProps\n  extends Pick<\n    TableProps<Word>,\n    'dataSource' | 'pagination' | 'rowSelection' | 'onChange' | 'loading'\n  > {\n  area: DBArea\n}\n\nexport const WordTable: FC<WordTableProps> = props => {\n  const { t, ready } = useTranslate('wordpage')\n\n  const tableColumns = useMemo<ColumnsType<Word>>(\n    () => [\n      {\n        title: t('column.word'),\n        dataIndex: 'text',\n        key: 'text',\n        width: colTextWidth,\n        align: 'center',\n        sorter: true,\n        filters: [\n          { text: t('filterWord.chs'), value: 'ch' },\n          { text: t('filterWord.eng'), value: 'en' },\n          { text: t('filterWord.word'), value: 'word' },\n          { text: t('filterWord.phrase'), value: 'phra' }\n        ]\n      },\n      {\n        title: t('column.source'),\n        dataIndex: 'context',\n        key: 'context',\n        width: restWidth,\n        align: 'center',\n        render: renderSource\n      },\n      {\n        title: t('column.trans'),\n        dataIndex: 'trans',\n        key: 'trans',\n        width: restWidth,\n        render: renderTrans\n      },\n      {\n        title: t('column.note'),\n        dataIndex: 'note',\n        key: 'note',\n        width: restWidth,\n        render: renderNote\n      },\n      {\n        title: t('column.date'),\n        dataIndex: 'date',\n        key: 'date',\n        width: colDateWidth,\n        align: 'center',\n        sorter: true,\n        render: renderDate\n      },\n      {\n        title: t(`column.${props.area === 'notebook' ? 'edit' : 'add'}`),\n        key: 'edit',\n        align: 'center',\n        render: (_, record) => renderEdit(t, props.area, record)\n      }\n    ],\n    [ready, props.area]\n  )\n\n  return (\n    <Table\n      rowKey=\"date\"\n      columns={tableColumns}\n      bordered={true}\n      showHeader={true}\n      dataSource={props.dataSource}\n      pagination={props.pagination}\n      rowSelection={props.rowSelection}\n      onChange={props.onChange}\n      loading={props.loading}\n    />\n  )\n}\n\nfunction renderSource(_: any, record: Word): ReactNode {\n  return (\n    <React.Fragment key={record.date}>\n      {record.context && (\n        <p className=\"wordpage-Record_Context\">{record.context}</p>\n      )}\n      {record.title && (\n        <p className=\"wordpage-Source_Footer\">\n          {record.favicon && (\n            <img className=\"wordpage-Record_Favicon\" src={record.favicon} />\n          )}\n          <span className=\"wordpage-Record_Title\">{record.title}</span>\n        </p>\n      )}\n    </React.Fragment>\n  )\n}\n\nfunction renderParagraphs(text?: string): ReactNode {\n  if (!text) {\n    return ''\n  }\n  return text.split('\\n').map((line, i) => <div key={i}>{line}</div>)\n}\n\nfunction renderTrans(_: any, record: Word): ReactNode {\n  return renderParagraphs(record.trans)\n}\n\nfunction renderNote(_: any, record: Word): ReactNode {\n  return renderParagraphs(record.note)\n}\n\nfunction renderDate(datenum: number): ReactNode {\n  const date = new Date(datenum)\n  return (\n    <Tooltip\n      key={datenum}\n      placement=\"topRight\"\n      title={date.toLocaleString(i18next.language)}\n    >\n      <>{date.toLocaleDateString(i18next.language)}</>\n    </Tooltip>\n  )\n}\n\nfunction renderEdit(t: TFunction, area: DBArea, record: Word): ReactNode {\n  return (\n    <Button\n      key={record.date}\n      size=\"small\"\n      onClick={() => {\n        const word = {\n          ...record,\n          // give it a new date if it's from history\n          date: area === 'notebook' ? record.date : Date.now()\n        }\n        // wait till selection ends\n        setTimeout(() => {\n          message.self.send({\n            type: 'UPDATE_WORD_EDITOR_WORD',\n            payload: { word }\n          })\n        }, 500)\n      }}\n    >\n      {t(`column.${area === 'notebook' ? 'edit' : 'add'}`)}\n    </Button>\n  )\n}\n"
  },
  {
    "path": "src/components/WordPage/_style.scss",
    "content": "body {\n  overflow-y: scroll;\n  // Firefox table will always overflow\n  overflow-x: hidden;\n}\n\ntextarea {\n  resize: none;\n  background: transparent;\n}\n\n.ant-pagination {\n  float: none !important;\n  display: flex;\n  justify-content: center;\n}\n\n.wordpage-Container {\n  max-width: 1920px;\n  margin: 0 auto;\n  padding-top: 64px;\n}\n\n.wordpage-Header {\n  display: flex;\n  position: fixed;\n  z-index: 100;\n  top: 0;\n  left: 0;\n  width: 100%;\n\n  @media screen and (max-width: 800px) {\n    padding: 0 5px !important;\n  }\n}\n\n.wordpage-Title {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  line-height: 1;\n  color: #fff;\n}\n\n.wordpage-Title_head {\n  margin-bottom: 5px;\n  color: #fff;\n}\n\n.wordpage-Title_small {\n  font-size: 0.6em;\n  vertical-align: middle;\n\n  @media screen and (max-width: 720px) {\n    display: none !important;\n  }\n}\n\n.wordpage-Wordcount {\n  margin-left: 10px;\n  color: #fff;\n}\n\n.wordpage-BtnGroup {\n  display: flex;\n  align-items: center;\n  margin-left: auto;\n  white-space: nowrap;\n}\n\n.wordpage-Content {\n  background: #fff;\n}\n\n.wordpage-Record_Context {\n  margin: 0 0 5px 0;\n  padding: 0 0 5px 0;\n  border-bottom: 1px solid #e8e8e8;\n}\n\n.wordpage-Source_Footer {\n  margin: 0;\n}\n\n.wordpage-Record_Favicon {\n  width: 14px;\n  height: 14px;\n  margin-right: 5px;\n  vertical-align: sub;\n}\n\n.wordpage-Record_Title {\n  font-size: 12px;\n  color: #aaa;\n}\n\n.wordEditorPanel-Background {\n  z-index: 998 !important;\n}\n"
  },
  {
    "path": "src/components/WordPage/index.tsx",
    "content": "import React, { FC, useState, useEffect } from 'react'\nimport { Layout } from 'antd'\nimport { from } from 'rxjs'\nimport { switchMap, startWith, debounceTime } from 'rxjs/operators'\nimport { useObservable, useSubscription } from 'observable-hooks'\nimport { Helmet } from 'react-helmet'\nimport { DBArea, getWords, Word, deleteWords } from '@/_helpers/record-manager'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { message } from '@/_helpers/browser-api'\nimport { Header } from './Header'\nimport { WordTableProps, colSelectionWidth, WordTable } from './WordTable'\nimport { ExportModal, ExportModalTitle } from './ExportModal'\n\nimport './_style.scss'\n\nconst ITEMS_PER_PAGE = 200\n\ntype TableInfo = Pick<\n  WordTableProps,\n  'dataSource' | 'pagination' | 'rowSelection' | 'loading'\n>\n\ninterface FetchWordsConfig {\n  itemsPerPage?: number\n  pageNum?: number\n  filters: { [field: string]: (string | number)[] | null | undefined }\n  sortField?: string | number | (string | number)[]\n  sortOrder?: 'ascend' | 'descend' | false | null\n  searchText: string\n}\n\nconst initialFetchWordsConfig: Readonly<FetchWordsConfig> = {\n  searchText: '',\n  itemsPerPage: ITEMS_PER_PAGE,\n  pageNum: 1,\n  filters: {}\n}\n\nexport interface WordPageProps {\n  area: DBArea\n}\n\nexport const WordPage: FC<WordPageProps> = props => {\n  const { t } = useTranslate('wordpage')\n  const [searchText, setSearchText] = useState('')\n  const [selectedRows, setSelectedRows] = useState<Word[]>([])\n  const [tableInfo, setTableInfo] = useState<TableInfo>(() => ({\n    dataSource: [],\n    pagination: {\n      showSizeChanger: true,\n      showQuickJumper: true,\n      hideOnSinglePage: true,\n      current: 1,\n      pageSize: ITEMS_PER_PAGE,\n      defaultPageSize: ITEMS_PER_PAGE,\n      total: 0\n    },\n    rowSelection: {\n      selectedRowKeys: [],\n      columnWidth: colSelectionWidth,\n      onChange: (selectedRowKeys, selectedRows) => {\n        setTableInfo(lastInfo => ({\n          ...lastInfo,\n          rowSelection: {\n            ...lastInfo.rowSelection,\n            selectedRowKeys\n          }\n        }))\n        setSelectedRows(selectedRows)\n      }\n    },\n    loading: false\n  }))\n\n  const [exportModalTitle, setExportModalTitle] = useState<ExportModalTitle>('')\n  const [exportModalWords, setExportModalWords] = useState<Word[]>([])\n\n  const [fetchWordsConfig, setFetchWordsConfig] = useState(\n    initialFetchWordsConfig\n  )\n  const fetchWords = (config: Partial<FetchWordsConfig>) =>\n    setFetchWordsConfig(lastConfig => ({\n      ...lastConfig,\n      ...config\n    }))\n\n  const fetchWords$ = useObservable<\n    { total: number; words: Word[] } | null,\n    [FetchWordsConfig]\n  >(\n    inputs$ =>\n      inputs$.pipe(\n        debounceTime(200),\n        switchMap(([config]) =>\n          from(\n            getWords(props.area, config).catch(e => {\n              console.error(e)\n              return { total: 0, words: [] }\n            })\n          ).pipe(startWith(null))\n        )\n      ),\n    [fetchWordsConfig]\n  )\n\n  useSubscription(fetchWords$, response => {\n    setTableInfo(lastInfo => ({\n      ...lastInfo,\n      ...(response\n        ? {\n            pagination: {\n              ...lastInfo.pagination,\n              total: response.total\n            },\n            dataSource: response.words,\n            loading: false\n          }\n        : { loading: true })\n    }))\n    setSelectedRows([])\n  })\n\n  useEffect(() => {\n    const handler = (): void => {\n      fetchWords({})\n    }\n    message.addListener('WORD_SAVED', handler)\n\n    return () => message.removeListener('WORD_SAVED', handler)\n  }, [])\n\n  return (\n    <Layout className=\"wordpage-Container\">\n      <Helmet>\n        <title>{t(`title.${props.area}`)}</title>\n      </Helmet>\n      <Header\n        t={t}\n        area={props.area}\n        searchText={searchText}\n        totalCount={(tableInfo.pagination && tableInfo.pagination.total) || 0}\n        selectedCount={selectedRows.length}\n        onSearchTextChanged={text => {\n          setSearchText(text)\n          fetchWords({ searchText: text })\n        }}\n        onExport={async ({ key }) => {\n          if (key === 'all') {\n            const { total, words } = await getWords(props.area, {\n              ...fetchWordsConfig,\n              itemsPerPage: undefined,\n              pageNum: undefined\n            })\n            if (process.env.DEBUG) {\n              console.assert(words.length === total, 'get all words')\n            }\n            setExportModalTitle(key)\n            setExportModalWords(words)\n          } else if (key === 'selected') {\n            setExportModalTitle(key)\n            setExportModalWords(selectedRows)\n          } else if (key === 'page') {\n            setExportModalTitle(key)\n            setExportModalWords(tableInfo.dataSource || [])\n          } else {\n            setExportModalTitle('')\n          }\n        }}\n        onDelete={key => {\n          const keys =\n            key === 'selected'\n              ? tableInfo.rowSelection?.selectedRowKeys?.map(date =>\n                  Number(date)\n                )\n              : key === 'page'\n              ? tableInfo.dataSource?.map(({ date }) => date)\n              : undefined\n          deleteWords(props.area, keys).then(() => fetchWords({}))\n        }}\n      />\n      <Layout.Content>\n        <WordTable\n          area={props.area}\n          {...tableInfo}\n          onChange={(pagination, filters, sorter) => {\n            window.scrollTo(0, 0)\n\n            setTableInfo(lastInfo => ({\n              ...lastInfo,\n              pagination: {\n                ...lastInfo.pagination,\n                current: pagination.current || 1\n              }\n            }))\n\n            const realSorter = Array.isArray(sorter) ? sorter[0] : sorter\n\n            fetchWords({\n              itemsPerPage: pagination?.pageSize || ITEMS_PER_PAGE,\n              pageNum: pagination?.current || 1,\n              filters: filters,\n              sortField: realSorter?.field,\n              sortOrder: realSorter?.order,\n              searchText\n            })\n          }}\n        />\n      </Layout.Content>\n      <ExportModal\n        title={exportModalTitle}\n        rawWords={exportModalWords}\n        onCancel={() => {\n          setExportModalTitle('')\n        }}\n      />\n    </Layout>\n  )\n}\n"
  },
  {
    "path": "src/components/dictionaries/ahdict/View.tsx",
    "content": "import React, { FC } from 'react'\nimport Speaker from '@/components/Speaker'\nimport { AhdictResult } from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictAh: FC<ViewPorps<AhdictResult>> = ({ result }) => (\n  <div>\n    {result.map((res, resI) => {\n      return (\n        <div className=\"dictAh-WordBox\" key={resI}>\n          {/* keywords and pronunciation */}\n          <div className=\"dictAh-Title\">\n            <span>{res.title}</span>\n            {res.pron && <Speaker src={res.pron} />}\n          </div>\n\n          {/* meaning and eg */}\n          {res.meaning &&\n            res.meaning.map((m, mI) => (\n              <StrElm key={mI} className=\"dictAh-Meaning\" html={m} />\n            ))}\n\n          {/* idioms and eg */}\n          {res.idioms && !!res.idioms.length && (\n            <>\n              <div className=\"dictAh-idiomTitle\">\n                {res.idioms.length > 1 ? 'idioms' : 'idiom'}\n              </div>\n              {res.idioms.map((idiom, idiomI) => (\n                <div key={`${idiom.title}--${idiomI}`}>\n                  <div>\n                    <span className=\"dictAh-idiomWords\">{idiom.title}</span>\n                    {idiom.tips ? `(${idiom.tips})` : null}\n                  </div>\n                  <div className=\"dictAh-idiomEg\">{idiom.eg}</div>\n                </div>\n              ))}\n            </>\n          )}\n\n          {/* words origin */}\n          {res.origin && (\n            <>\n              <div className=\"dictAh-Hr\" role=\"separator\" />\n              <StrElm tag=\"p\" className=\"dictAh-origin\" html={res.origin} />\n            </>\n          )}\n\n          {/* words usage note */}\n          {res.usageNote && (\n            <StrElm tag=\"p\" className=\"dictAh-UsageNote\" html={res.usageNote} />\n          )}\n        </div>\n      )\n    })}\n  </div>\n)\n\nexport default DictAh\n"
  },
  {
    "path": "src/components/dictionaries/ahdict/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Amercian Heritage Dict\",\n    \"zh-CN\": \"美国传统词典\",\n    \"zh-TW\": \"美國傳統詞典\"\n  },\n  \"options\": {\n    \"resultnum\": {\n      \"en\": \"Show\",\n      \"zh-CN\": \"结果数量\",\n      \"zh-TW\": \"結果數量\"\n    },\n    \"resultnum_unit\": {\n      \"en\": \"results\",\n      \"zh-CN\": \"个\",\n      \"zh-TW\": \"個\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/ahdict/_style.shadow.scss",
    "content": ".dictAh-WordBox {\n  border-bottom: 1px solid var(--color-font-grey);\n\n  &:last-child {\n    border-bottom: none;\n  }\n}\n\n.dictAh-Title {\n  & > span {\n    font-size: 1.2em;\n    font-weight: bold;\n    vertical-align: -1px;\n  }\n}\n\n.dictAh-Meaning {\n  & > i {\n    font-weight: bold;\n  }\n\n  .ds-list,\n  .sds-list {\n    margin-left: 1em;\n  }\n}\n\n.dictAh-idiomTitle {\n  font-size: 1.2em;\n  font-weight: bold;\n}\n\n.dictAh-idiomWords {\n  font-style: italic;\n  font-weight: bold;\n  margin-right: 2px;\n}\n\n.dictAh-idiomEg {\n  margin-left: 1em;\n}\n\n.dictAh-Hr {\n  width: 25%;\n  display: flex;\n  clear: both;\n  background-color: --color-font-grey;\n  padding: 0;\n  margin: 2px 0;\n  height: 1px;\n}\n\n.dictAh-UsageNote {\n  border-left: 1px solid;\n  padding-left: 5px;\n  color: var(--color-font-grey);\n}\n\n.dictAh-UseNoteTitle {\n  font-size: 1.2em;\n  font-weight: bold;\n}\n\n.ds-single {\n  margin-left: 1em;\n}\n"
  },
  {
    "path": "src/components/dictionaries/ahdict/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type AhdictConfig = DictItem<{\n  resultnum: number\n}>\n\nexport default (): AhdictConfig => ({\n  lang: '10000000',\n  selectionLang: {\n    english: true,\n    chinese: false,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 240,\n  selectionWC: {\n    min: 1,\n    max: 5\n  },\n  options: {\n    resultnum: 4\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/ahdict/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  HTMLString,\n  getText,\n  getInnerHTML,\n  handleNoResult,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult\n} from '../helpers'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return `https://ahdictionary.com/word/search.html?q=${text}`\n}\n\nconst HOST = 'https://ahdictionary.com'\n\ninterface Idiom {\n  title?: string\n  eg?: string\n  tips?: string\n}\n\ninterface AhdictResultItem {\n  /** word */\n  title: string\n  /** pronunciation */\n  pron?: string\n  /** meaning and eg */\n  meaning: HTMLString[]\n  /** idiom and eg */\n  idioms: Idiom[]\n  origin?: HTMLString\n  usageNote?: string\n}\n\nexport type AhdictResult = AhdictResultItem[]\n\ntype AhdictSearchResult = DictSearchResult<AhdictResult>\n\nexport const search: SearchFunction<AhdictResult> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  const options = profile.dicts.all.ahdict.options\n\n  return fetchDirtyDOM(\n    'https://ahdictionary.com/word/search.html?q=' +\n      encodeURIComponent(text.replace(/\\s+/g, ' '))\n  )\n    .catch(handleNetWorkError)\n    .then(doc => handleDOM(doc, options))\n}\n\nfunction handleDOM(\n  doc: Document,\n  { resultnum }: { resultnum: number }\n): AhdictSearchResult | Promise<AhdictSearchResult> {\n  const result: AhdictResult = []\n\n  const tables = Array.from(doc.querySelectorAll('#results>table'))\n\n  if (tables.length <= 0) {\n    return handleNoResult()\n  }\n\n  for (let i = 0; i < tables.length && result.length < resultnum; i++) {\n    const $panel = tables[i]\n    const resultItem: AhdictResultItem = {\n      title: '',\n      meaning: [],\n      idioms: []\n    }\n\n    const $rtseg = $panel.querySelector('.rtseg') as HTMLElement\n\n    if ($rtseg) {\n      const $rtsega = $panel.querySelectorAll('.rtseg>a')\n\n      if ($rtsega[1] && $rtsega[1].getAttribute('href')) {\n        resultItem.pron = `${HOST}${$rtsega[1].getAttribute('href')}`\n      }\n\n      resultItem.title = getText($rtsega[0], 'font')\n    }\n\n    const $pseg = Array.from($panel.querySelectorAll('.pseg'))\n\n    $pseg.map(item => {\n      resultItem.meaning.push(\n        getInnerHTML(HOST, item).replace(/<\\/?(span|font)[^>]*>/g, '')\n      )\n    })\n\n    const $idmseg = Array.from($panel.querySelectorAll('.idmseg'))\n\n    if ($idmseg.length) {\n      $idmseg.map(item => {\n        const idiom = {} as Idiom\n        idiom.title = getText(item, 'b')\n        idiom.eg = getText(item, '.ds-single')\n        idiom.tips = getText(item, 'span+i')\n\n        resultItem.idioms.push(idiom)\n      })\n    }\n\n    // 获取该条信息来源\n    const $etyseg = $panel.querySelector('.etyseg') as HTMLElement\n\n    if ($etyseg) {\n      resultItem.origin = getInnerHTML(HOST, $etyseg).replace(\n        /<\\/?(span|font)[^>]*>/g,\n        ''\n      )\n    }\n\n    // 获取使用说明\n    const $usen = $panel.querySelector('.usen') as HTMLElement\n\n    if ($usen) {\n      resultItem.usageNote = getInnerHTML(HOST, $usen).replace(\n        /<\\/?(span|font|i)[^>]*>/g,\n        ''\n      )\n    }\n\n    result.push(resultItem)\n  }\n\n  if (result.length > 0) {\n    return { result }\n  } else {\n    return handleNoResult()\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/baidu/View.tsx",
    "content": "export { MachineTrans as default } from '@/components/MachineTrans/MachineTrans'\n"
  },
  {
    "path": "src/components/dictionaries/baidu/_locales.ts",
    "content": "import { getMachineLocales } from '../locales'\n\nexport const locales = getMachineLocales({\n  en: 'Baidu Translate',\n  'zh-CN': '百度翻译',\n  'zh-TW': '百度翻譯'\n})\n"
  },
  {
    "path": "src/components/dictionaries/baidu/_style.shadow.scss",
    "content": "@import '@/components/MachineTrans/MachineTrans.scss';\n"
  },
  {
    "path": "src/components/dictionaries/baidu/auth.ts",
    "content": "export const auth = {\n  appid: '',\n  key: ''\n}\n\nexport const url = 'http://api.fanyi.baidu.com/api/trans/product/prodinfo'\n"
  },
  {
    "path": "src/components/dictionaries/baidu/config.ts",
    "content": "import {\n  MachineDictItem,\n  machineConfig\n} from '@/components/MachineTrans/engine'\nimport { Language } from '@opentranslate/translator'\nimport { Subunion } from '@/typings/helpers'\n\nexport type BaiduLanguage = Subunion<\n  Language,\n  'zh-CN' | 'zh-TW' | 'en' | 'ja' | 'ko' | 'fr' | 'de' | 'es' | 'ru' | 'nl'\n>\n\nexport type BaiduConfig = MachineDictItem<BaiduLanguage>\n\nexport default (): BaiduConfig =>\n  machineConfig<BaiduConfig>(\n    ['zh-CN', 'zh-TW', 'en', 'ja', 'ko', 'fr', 'de', 'es', 'ru', 'nl'],\n    {},\n    {},\n    {}\n  )\n"
  },
  {
    "path": "src/components/dictionaries/baidu/engine.ts",
    "content": "import { SearchFunction, GetSrcPageFunction } from '../helpers'\nimport memoizeOne from 'memoize-one'\nimport { Baidu } from '@opentranslate/baidu'\nimport {\n  MachineTranslateResult,\n  MachineTranslatePayload,\n  getMTArgs,\n  machineResult\n} from '@/components/MachineTrans/engine'\nimport { BaiduLanguage } from './config'\n\nexport const getTranslator = memoizeOne(\n  () =>\n    new Baidu({\n      env: 'ext',\n      config:\n        process.env.BAIDU_APPID && process.env.BAIDU_KEY\n          ? {\n              appid: process.env.BAIDU_APPID,\n              key: process.env.BAIDU_KEY\n            }\n          : undefined\n    })\n)\n\nexport const getSrcPage: GetSrcPageFunction = (text, config, profile) => {\n  const lang =\n    profile.dicts.all.baidu.options.tl === 'default'\n      ? config.langCode === 'zh-CN'\n        ? 'zh'\n        : config.langCode === 'zh-TW'\n        ? 'cht'\n        : 'en'\n      : profile.dicts.all.baidu.options.tl\n\n  return `https://fanyi.baidu.com/#auto/${lang}/${text}`\n}\n\nexport type BaiduResult = MachineTranslateResult<'baidu'>\n\nexport const search: SearchFunction<\n  BaiduResult,\n  MachineTranslatePayload<BaiduLanguage>\n> = async (rawText, config, profile, payload) => {\n  const translator = getTranslator()\n\n  const { sl, tl, text } = await getMTArgs(\n    translator,\n    rawText,\n    profile.dicts.all.baidu,\n    config,\n    payload\n  )\n\n  const appid = config.dictAuth.baidu.appid\n  const key = config.dictAuth.baidu.key\n  const translatorConfig = appid && key ? { appid, key } : undefined\n\n  try {\n    const result = await translator.translate(text, sl, tl, translatorConfig)\n    return machineResult(\n      {\n        result: {\n          id: 'baidu',\n          slInitial: profile.dicts.all.baidu.options.slInitial,\n          sl: result.from,\n          tl: result.to,\n          searchText: result.origin,\n          trans: result.trans\n        },\n        audio: {\n          py: result.trans.tts,\n          us: result.trans.tts\n        }\n      },\n      translator.getSupportLanguages()\n    )\n  } catch (e) {\n    return machineResult(\n      {\n        result: {\n          id: 'baidu',\n          slInitial: 'hide',\n          sl,\n          tl,\n          searchText: { paragraphs: [''] },\n          trans: { paragraphs: [''] }\n        }\n      },\n      translator.getSupportLanguages()\n    )\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/bing/View.tsx",
    "content": "import React, { FC } from 'react'\nimport Speaker from '@/components/Speaker'\nimport {\n  BingResult,\n  BingResultLex,\n  BingResultMachine,\n  BingResultRelated\n} from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictBing: FC<ViewPorps<BingResult>> = ({ result }) => {\n  switch (result.type) {\n    case 'lex':\n      return renderLex(result)\n    case 'machine':\n      return renderMachine(result)\n    case 'related':\n      return renderRelated(result)\n    default:\n      return null\n  }\n}\n\nexport default DictBing\n\nfunction renderLex(result: BingResultLex) {\n  return (\n    <>\n      <h1 className=\"dictBing-Title\">{result.title}</h1>\n\n      {result.phsym && (\n        <ul className=\"dictBing-Phsym\">\n          {result.phsym.map(p => (\n            <li className=\"dictBing-PhsymItem\" key={p.lang + p.pron}>\n              {p.lang} <Speaker src={p.pron} />\n            </li>\n          ))}\n        </ul>\n      )}\n\n      {result.cdef && (\n        <ul className=\"dictBing-Cdef\">\n          {result.cdef.map(d => (\n            <li className=\"dictBing-CdefItem\" key={d.pos}>\n              <span className=\"dictBing-CdefItem_Pos\">{d.pos}</span>\n              <span className=\"dictBing-CdefItem_Def\">{d.def}</span>\n            </li>\n          ))}\n        </ul>\n      )}\n\n      {result.infs && (\n        <ul className=\"dictBing-Inf\">\n          词形：\n          {result.infs.map(inf => (\n            <li className=\"dictBing-InfItem\" key={inf}>\n              {inf}\n            </li>\n          ))}\n        </ul>\n      )}\n\n      {result.sentences && (\n        <ol className=\"dictBing-SentenceList\">\n          {result.sentences.map(sen => (\n            <li className=\"dictBing-SentenceItem\" key={sen.en}>\n              {sen.en && (\n                <p>\n                  <StrElm tag=\"span\" html={sen.en} />\n                  <Speaker src={sen.mp3}></Speaker>\n                </p>\n              )}\n              {sen.chs && <StrElm tag=\"p\" html={sen.chs} />}\n              {sen.source && (\n                <footer className=\"dictBing-SentenceSource\">\n                  {sen.source}\n                </footer>\n              )}\n            </li>\n          ))}\n        </ol>\n      )}\n    </>\n  )\n}\n\nfunction renderMachine(result: BingResultMachine) {\n  return <p>{result.mt}</p>\n}\n\nfunction renderRelated(result: BingResultRelated) {\n  return (\n    <>\n      <h1 className=\"dictBing-Related_Title\">{result.title}</h1>\n      {result.defs.map(def => (\n        <React.Fragment key={def.title}>\n          <h2 className=\"dictBing-Related_Title\">{def.title}</h2>\n          <ul>\n            {def.meanings.map(meaning => (\n              <li className=\"dictBing-Related_Meaning\" key={meaning.word}>\n                <a\n                  className=\"dictBing-Related_Meaning_Word\"\n                  href={meaning.href}\n                >\n                  {meaning.word}\n                </a>\n                <span className=\"dictBing-Related_Meaning_Def\">\n                  {meaning.def}\n                </span>\n              </li>\n            ))}\n          </ul>\n        </React.Fragment>\n      ))}\n    </>\n  )\n}\n"
  },
  {
    "path": "src/components/dictionaries/bing/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Bing Dict\",\n    \"zh-CN\": \"必应词典\",\n    \"zh-TW\": \"必應詞典\"\n  },\n  \"options\": {\n    \"tense\": {\n      \"en\": \"Show sense\",\n      \"zh-CN\": \"显示单词时态\",\n      \"zh-TW\": \"展示單詞時態\"\n    },\n    \"phsym\": {\n      \"en\": \"Show pronunciation\",\n      \"zh-CN\": \"显示单词发音\",\n      \"zh-TW\": \"展示單詞發音\"\n    },\n    \"cdef\": {\n      \"en\": \"Show definitions\",\n      \"zh-CN\": \"显示单词解释\",\n      \"zh-TW\": \"展示單詞解釋\"\n    },\n    \"related\": {\n      \"en\": \"Show related results\",\n      \"zh-CN\": \"失败时显示备选\",\n      \"zh-TW\": \"失敗時顯示備選\"\n    },\n    \"sentence\": {\n      \"en\": \"Show sentences\",\n      \"zh-CN\": \"显示例句\",\n      \"zh-TW\": \"展示例句\"\n    },\n    \"sentence_unit\": {\n      \"en\": \"results\",\n      \"zh-CN\": \"个\",\n      \"zh-TW\": \"個\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/bing/_style.shadow.scss",
    "content": ".dictBing-Title {\n  font-size: 1.5em;\n}\n\n.dictBing-Phsym {\n  display: flex;\n  margin-bottom: 5px;\n}\n\n.dictBing-PhsymItem {\n  margin-right: 1em;\n}\n\n.dictBing-Cdef {\n  margin-bottom: 5px;\n}\n\n.dictBing-CdefItem {\n  display: table;\n}\n\n.dictBing-CdefItem_Pos {\n  display: table-cell;\n  width: 3em;\n  font-weight: bold;\n  text-align: right;\n}\n\n.dictBing-CdefItem_Def {\n  display: table-cell;\n  padding: 0 12px;\n}\n\n.dictBing-Inf {\n  display: flex;\n  flex-wrap: wrap;\n  margin-bottom: 5px;\n  font-size: 12px;\n  color: #777;\n}\n\n.dictBing-InfItem {\n  margin-right: 1em;\n}\n\n.dictBing-SentenceList {\n  padding: 0 0 0 1.5em;\n}\n\n.dictBing-SentenceItem {\n  margin-bottom: 10px;\n\n  p {\n    margin: 0;\n  }\n}\n\n.dictBing-SentenceItem_HL {\n  color: #f9690e;\n}\n\n.dictBing-SentenceSource {\n  color: #999;\n}\n\n.dictBing-Related_Title {\n  font-size: 1em;\n  margin: 5px 0;\n}\n\n.dictBing-Related_DefTitle {\n  font-size: 1.2em;\n  margin: 5px 0 0 0;\n}\n\n.dictBing-Related_Meaning {\n  display: table;\n  margin-bottom: 2px;\n}\n\n.dictBing-Related_Meaning_Word {\n  display: table-cell;\n  width: 8em;\n  text-align: right;\n  color: #16a085;\n  text-decoration: none;\n  cursor: pointer;\n}\n\n.dictBing-Related_Meaning_Def {\n  display: table-cell;\n  padding: 0 12px;\n}\n"
  },
  {
    "path": "src/components/dictionaries/bing/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type BingConfig = DictItem<{\n  tense: boolean\n  phsym: boolean\n  cdef: boolean\n  related: boolean\n  sentence: number\n}>\n\nexport default (): BingConfig => ({\n  lang: '11000000',\n  selectionLang: {\n    english: true,\n    chinese: true,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 240,\n  selectionWC: {\n    min: 1,\n    max: 5\n  },\n  options: {\n    tense: true,\n    phsym: true,\n    cdef: true,\n    related: true,\n    sentence: 4\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/bing/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  handleNoResult,\n  handleNetWorkError,\n  getText,\n  getInnerHTML,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult,\n  getChsToChz\n} from '../helpers'\nimport { DictConfigs } from '@/app-config'\n\nexport const getSrcPage: GetSrcPageFunction = text =>\n  'https://cn.bing.com/dict/search?q=' +\n  encodeURIComponent(text.replace(/\\s+/g, ' '))\n\nconst HOST = 'https://cn.bing.com'\n\nconst DICT_LINK =\n  'https://cn.bing.com/dict/clientsearch?mkt=zh-CN&setLang=zh&form=BDVEHC&ClientVer=BDDTV3.5.1.4320&q='\n\n/** Lexical result */\nexport interface BingResultLex {\n  type: 'lex'\n  title: string\n  /** phonetic symbols */\n  phsym?: Array<{\n    /** Phonetic Alphabet, UK|US|PY */\n    lang: string\n    /** pronunciation */\n    pron: string\n  }>\n  /** common definitions */\n  cdef?: Array<{\n    /** part of speech */\n    pos: string\n    /** definition */\n    def: string\n  }>\n  /** infinitive */\n  infs?: string[]\n  sentences?: Array<{\n    en?: string\n    chs?: string\n    source?: string\n    mp3?: string\n  }>\n}\n\n/** Alternate machine translation result */\nexport interface BingResultMachine {\n  type: 'machine'\n  /** machine translation */\n  mt: string\n}\n\n/** Alternate result */\nexport interface BingResultRelated {\n  type: 'related'\n  title: string\n  defs: Array<{\n    title: string\n    meanings: Array<{\n      href: string\n      word: string\n      def: string\n    }>\n  }>\n}\n\nexport type BingResult = BingResultLex | BingResultMachine | BingResultRelated\n\ntype BingConfig = DictConfigs['bing']\n\ntype BingSearchResultLex = DictSearchResult<BingResultLex>\ntype BingSearchResultMachine = DictSearchResult<BingResultMachine>\ntype BingSearchResultRelated = DictSearchResult<BingResultRelated>\n\nexport const search: SearchFunction<BingResult> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  const bingConfig = profile.dicts.all.bing\n\n  return fetchDirtyDOM(\n    DICT_LINK + encodeURIComponent(text.replace(/\\s+/g, ' '))\n  )\n    .catch(handleNetWorkError)\n    .then(async doc => {\n      const transform = await getChsToChz(config.langCode)\n\n      if (doc.querySelector('.client_def_hd_hd')) {\n        return handleLexResult(doc, bingConfig.options, transform)\n      }\n\n      if (doc.querySelector('.client_trans_head')) {\n        return handleMachineResult(doc, transform)\n      }\n\n      if (bingConfig.options.related) {\n        if (doc.querySelector('.client_do_you_mean_title_bar')) {\n          return handleRelatedResult(doc, bingConfig, transform)\n        }\n      }\n\n      return handleNoResult<DictSearchResult<BingResult>>()\n    })\n}\n\nfunction handleLexResult(\n  doc: Document,\n  options: BingConfig['options'],\n  transform: null | ((text: string) => string)\n): BingSearchResultLex | Promise<BingSearchResultLex> {\n  const searchResult: DictSearchResult<BingResultLex> = {\n    result: {\n      type: 'lex',\n      title: getText(doc, '.client_def_hd_hd', transform)\n    }\n  }\n\n  // pronunciation\n  if (options.phsym) {\n    const $prons = Array.from(doc.querySelectorAll('.client_def_hd_pn_list'))\n    if ($prons.length > 0) {\n      searchResult.result.phsym = $prons.map(el => {\n        let pron = ''\n        const $audio = el.querySelector('.client_aud_o')\n        if ($audio) {\n          pron = (($audio.getAttribute('onclick') || '').match(\n            /https.*\\.mp3/\n          ) || [''])[0]\n        }\n        return {\n          lang: getText(el, '.client_def_hd_pn'),\n          pron\n        }\n      })\n\n      searchResult.audio = searchResult.result.phsym.reduce(\n        (audio, { lang, pron }) => {\n          if (/us|美/i.test(lang)) {\n            audio['us'] = pron\n          } else if (/uk|英/i.test(lang)) {\n            audio['uk'] = pron\n          }\n          return audio\n        },\n        {}\n      )\n    }\n  }\n\n  // definitions\n  if (options.cdef) {\n    const $container = doc.querySelector('.client_def_container')\n    if ($container) {\n      const $defs = Array.from($container.querySelectorAll('.client_def_bar'))\n      if ($defs.length > 0) {\n        searchResult.result.cdef = $defs.map(el => ({\n          pos: getText(el, '.client_def_title_bar', transform),\n          def: getText(el, '.client_def_list', transform)\n        }))\n      }\n    }\n  }\n\n  // tense\n  if (options.tense) {\n    const $infs = Array.from(doc.querySelectorAll('.client_word_change_word'))\n    if ($infs.length > 0) {\n      searchResult.result.infs = $infs.map(el => (el.textContent || '').trim())\n    }\n  }\n\n  if (options.sentence > 0) {\n    const $sens = doc.querySelectorAll('.client_sentence_list')\n    const sentences: typeof searchResult.result.sentences = []\n    for (\n      let i = 0;\n      i < $sens.length && sentences.length < options.sentence;\n      i++\n    ) {\n      const el = $sens[i]\n      let mp3 = ''\n      const $audio = el.querySelector('.client_aud_o')\n      if ($audio) {\n        mp3 = (($audio.getAttribute('onclick') || '').match(/https.*\\.mp3/) || [\n          ''\n        ])[0]\n      }\n      el.querySelectorAll('.client_sen_en_word').forEach($word => {\n        $word.outerHTML = getText($word)\n      })\n      el.querySelectorAll('.client_sen_cn_word').forEach($word => {\n        $word.outerHTML = getText($word, transform)\n      })\n      el.querySelectorAll('.client_sentence_search').forEach($word => {\n        $word.outerHTML = `<span class=\"dictBing-SentenceItem_HL\">${getText(\n          $word\n        )}</span>`\n      })\n      sentences.push({\n        en: getInnerHTML(HOST, el, '.client_sen_en'),\n        chs: getInnerHTML(HOST, el, {\n          selector: '.client_sen_cn',\n          transform\n        }),\n        source: getText(el, '.client_sentence_list_link'),\n        mp3\n      })\n    }\n    searchResult.result.sentences = sentences\n  }\n\n  if (Object.keys(searchResult.result).length > 2) {\n    return searchResult\n  }\n  return handleNoResult()\n}\n\nfunction handleMachineResult(\n  doc: Document,\n  transform: null | ((text: string) => string)\n): BingSearchResultMachine | Promise<BingSearchResultMachine> {\n  const mt = getText(doc, '.client_sen_cn', transform)\n\n  if (mt) {\n    return {\n      result: {\n        type: 'machine',\n        mt\n      }\n    }\n  }\n\n  return handleNoResult()\n}\n\nfunction handleRelatedResult(\n  doc: Document,\n  config: BingConfig,\n  transform: null | ((text: string) => string)\n): BingSearchResultRelated | Promise<BingSearchResultRelated> {\n  const searchResult: DictSearchResult<BingResultRelated> = {\n    result: {\n      type: 'related',\n      title: getText(doc, '.client_do_you_mean_title_bar', transform),\n      defs: []\n    }\n  }\n\n  doc.querySelectorAll('.client_do_you_mean_area').forEach($area => {\n    const $defsList = $area.querySelectorAll('.client_do_you_mean_list')\n    if ($defsList.length > 0) {\n      searchResult.result.defs.push({\n        title: getText($area, '.client_do_you_mean_title', transform),\n        meanings: Array.from($defsList).map($list => {\n          const word = getText(\n            $list,\n            '.client_do_you_mean_list_word',\n            transform\n          )\n          return {\n            href: `https://cn.bing.com/dict/search?q=${word}`,\n            word,\n            def: getText($list, '.client_do_you_mean_list_def', transform)\n          }\n        })\n      })\n    }\n  })\n\n  if (searchResult.result.defs.length > 0) {\n    return searchResult\n  }\n  return handleNoResult()\n}\n"
  },
  {
    "path": "src/components/dictionaries/caiyun/View.tsx",
    "content": "export { MachineTrans as default } from '@/components/MachineTrans/MachineTrans'\n"
  },
  {
    "path": "src/components/dictionaries/caiyun/_locales.ts",
    "content": "import { getMachineLocales } from '../locales'\n\nexport const locales = getMachineLocales({\n  en: 'LingoCloud',\n  'zh-CN': '彩云小译',\n  'zh-TW': '彩雲小譯'\n})\n"
  },
  {
    "path": "src/components/dictionaries/caiyun/_style.shadow.scss",
    "content": "@import '@/components/MachineTrans/MachineTrans.scss';\n"
  },
  {
    "path": "src/components/dictionaries/caiyun/auth.ts",
    "content": "export const auth = {\n  token: ''\n}\n\nexport const url = 'https://fanyi.caiyunapp.com/#/api'\n"
  },
  {
    "path": "src/components/dictionaries/caiyun/config.ts",
    "content": "import {\n  MachineDictItem,\n  machineConfig\n} from '@/components/MachineTrans/engine'\nimport { Language } from '@opentranslate/translator'\nimport { Subunion } from '@/typings/helpers'\n\nexport type CaiyunLanguage = Subunion<Language, 'zh-CN' | 'en' | 'ja'>\n\nexport type CaiyunConfig = MachineDictItem<CaiyunLanguage>\n\nexport default (): CaiyunConfig =>\n  machineConfig<CaiyunConfig>(\n    ['zh-CN', 'en', 'ja'],\n    {\n      lang: '11010000'\n    },\n    {},\n    {}\n  )\n"
  },
  {
    "path": "src/components/dictionaries/caiyun/engine.ts",
    "content": "import { SearchFunction, GetSrcPageFunction } from '../helpers'\nimport memoizeOne from 'memoize-one'\nimport { Caiyun } from '@opentranslate/caiyun'\nimport { TranslateResult } from '@opentranslate/translator'\nimport {\n  MachineTranslateResult,\n  MachineTranslatePayload,\n  getMTArgs,\n  machineResult\n} from '@/components/MachineTrans/engine'\nimport { getTranslator as getBaiduTranslator } from '../baidu/engine'\nimport { CaiyunLanguage } from './config'\n\nexport const getTranslator = memoizeOne(\n  () =>\n    new Caiyun({\n      env: 'ext',\n      config: process.env.CAIYUN_TOKEN\n        ? {\n            token: process.env.CAIYUN_TOKEN\n          }\n        : undefined\n    })\n)\n\nexport const getSrcPage: GetSrcPageFunction = () => {\n  return 'https://fanyi.caiyunapp.com/'\n}\n\nexport type CaiyunResult = MachineTranslateResult<'caiyun'>\n\nexport const search: SearchFunction<\n  CaiyunResult,\n  MachineTranslatePayload<CaiyunLanguage>\n> = async (rawText, config, profile, payload) => {\n  const translator = getTranslator()\n  const langcodes = translator.getSupportLanguages()\n\n  let { sl, tl, text } = await getMTArgs(\n    translator,\n    rawText,\n    profile.dicts.all.caiyun,\n    config,\n    payload\n  )\n\n  const baiduTranslator = getBaiduTranslator()\n\n  let baiduResult: TranslateResult | undefined\n\n  try {\n    // Caiyun's lang detection is broken\n    baiduResult = await baiduTranslator.translate(text, sl, tl)\n    if (langcodes.includes(baiduResult.from)) {\n      sl = baiduResult.from\n    }\n  } catch (e) {}\n\n  const caiYunToken = config.dictAuth.caiyun.token\n  const caiYunConfig = caiYunToken ? { token: caiYunToken } : undefined\n\n  try {\n    const result = await translator.translate(text, sl, tl, caiYunConfig)\n    result.origin.tts = await baiduTranslator.textToSpeech(\n      result.origin.paragraphs.join('\\n'),\n      result.from\n    )\n    result.trans.tts = await baiduTranslator.textToSpeech(\n      result.trans.paragraphs.join('\\n'),\n      result.to\n    )\n    return machineResult(\n      {\n        result: {\n          id: 'caiyun',\n          sl: result.from,\n          tl: result.to,\n          slInitial: profile.dicts.all.caiyun.options.slInitial,\n          searchText: result.origin,\n          trans: result.trans\n        },\n        audio: {\n          py: result.trans.tts,\n          us: result.trans.tts\n        }\n      },\n      langcodes\n    )\n  } catch (e) {\n    return machineResult(\n      {\n        result: {\n          id: 'caiyun',\n          sl,\n          tl,\n          slInitial: 'hide',\n          searchText: { paragraphs: [''] },\n          trans: { paragraphs: [''] }\n        }\n      },\n      translator.getSupportLanguages()\n    )\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/cambridge/View.tsx",
    "content": "import React, { FC } from 'react'\nimport { CambridgeResult } from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictCambridge: FC<ViewPorps<CambridgeResult>> = props => (\n  <>\n    {props.result.map(entry => (\n      <section\n        key={entry.id}\n        id={entry.id}\n        className=\"dictCambridge-Entry\"\n        onClick={handleEntryClick}\n      >\n        <StrElm html={entry.html} />\n      </section>\n    ))}\n  </>\n)\n\nexport default DictCambridge\n\nfunction handleEntryClick(e: React.MouseEvent<HTMLElement>) {\n  const target = e.nativeEvent.target as HTMLDivElement\n  if (target && target.classList) {\n    if (target.classList.contains('js-accord')) {\n      target.classList.toggle('open')\n    }\n\n    if (target.classList.contains('daccord_h')) {\n      target.parentElement!.classList.toggle('open')\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/cambridge/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Cambridge Dictionary\",\n    \"zh-CN\": \"剑桥词典\",\n    \"zh-TW\": \"劍橋詞典\"\n  },\n  \"options\": {\n    \"lang\": {\n      \"en\": \"Language\",\n      \"zh-CN\": \"语言\",\n      \"zh-TW\": \"語言\"\n    },\n    \"lang-default\": {\n      \"en\": \"Default\",\n      \"zh-CN\": \"随扩展\",\n      \"zh-TW\": \"未設定\"\n    },\n    \"lang-en\": {\n      \"en\": \"English\",\n      \"zh-CN\": \"English\",\n      \"zh-TW\": \"English\"\n    },\n    \"lang-en-chs\": {\n      \"en\": \"English–Chinese (Simplified)\",\n      \"zh-CN\": \"英简\",\n      \"zh-TW\": \"英简\"\n    },\n    \"lang-en-chz\": {\n      \"en\": \"English–Chinese (Traditional)\",\n      \"zh-CN\": \"英繁\",\n      \"zh-TW\": \"英繁\"\n    },\n    \"related\": {\n      \"en\": \"Show related results\",\n      \"zh-CN\": \"失败时显示备选\",\n      \"zh-TW\": \"失敗時顯示備選\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/cambridge/_style.shadow.scss",
    "content": ".dictCambridge-Header {\n  display: flex;\n  align-items: baseline;\n}\n\n.dictCambridge-Title {\n  font-size: 1.5em;\n  margin-right: 0.5em;\n}\n\n.dictCambridge-Entry {\n  margin-bottom: 1em;\n\n  a {\n    color: inherit;\n  }\n}\n\n#d-cambridge-entry-related {\n  ul {\n    padding-left: 1em;\n\n    li {\n      list-style-type: disc;\n\n      a {\n        color: #f9690e;\n      }\n    }\n  }\n}\n\n.dimg {\n  text-align: center;\n\n  img {\n    display: inline-block;\n  }\n}\n\n.inline {\n  margin-left: 0;\n  list-style: none;\n}\n\n.inline li {\n  display: inline;\n  margin: 0 15px 0 0;\n}\n\n.unstyled,\n.unstyled-nest li ul {\n  margin-left: 0;\n  padding: 0;\n  list-style: none;\n}\n\n.unstyled li {\n  margin: 0 0 5px 0;\n}\n\n.unstyled-nest li ul {\n  margin-top: 5px;\n}\n\n.link-list {\n  margin: 5px 0 10px;\n}\n\n.link-list a {\n  font-weight: 700;\n  text-decoration: none;\n}\n\n.link-list a:hover {\n  text-decoration: underline;\n}\n\n.divided li {\n  margin: 0;\n  padding: 20px 5px;\n  border-bottom: solid 1px #e3e3e8;\n}\n\n.divided li:first-child {\n  padding-top: 10px;\n}\n\n.divided li:last-child {\n  border-width: 0;\n}\n\n.checklist {\n  margin-left: 0;\n  padding-left: 25px;\n  list-style: none;\n}\n\n.checklist li:before {\n  font-weight: 300;\n  content: '\\f00c';\n  position: absolute;\n  margin-left: -25px;\n  color: #d1a14c;\n}\n\n.tiles__tile {\n  float: left;\n  width: 50%;\n  padding: 0 2px;\n}\n\n.tiles__tile label {\n  float: left;\n  width: 100%;\n  margin-bottom: 10px;\n  padding: 10px 0;\n  background: #fff;\n  text-align: center;\n  border: solid 3px #e0e0e5;\n  cursor: pointer;\n  position: relative;\n}\n\n.tiles__tile input[type='radio'],\n.tiles__tile input[type='checkbox'] {\n  display: none;\n}\n\n.tiles__tile input[type='radio']:checked + label,\n.tiles__tile input[type='checkbox']:checked + label {\n  background: #d0a44c;\n  border-color: #d0a44c;\n}\n\n.tiles__tile input[type='radio']:checked + label:before,\n.tiles__tile input[type='checkbox']:checked + label:before {\n  content: '\\f00c';\n  font-weight: 300;\n  padding-right: 7px;\n  color: #fff;\n  font-size: 0.9em;\n}\n\n.form .tiles {\n  margin-bottom: 0;\n}\n\n.with-el {\n  position: relative;\n}\n\n.with-el__el {\n  position: absolute;\n  top: 0;\n  right: 0;\n}\n\n.with-el__el--l {\n  right: auto;\n  left: 0;\n}\n\n.with-el__el--b {\n  top: auto;\n  bottom: 0;\n}\n\n.with-el__el--icons {\n  top: -5px;\n  vertical-align: 0;\n}\n\n.with-icons__content {\n  display: inline-block;\n  padding: 5px 0;\n}\n\n.with-icons__icons {\n  float: right;\n}\n\n.trend {\n  position: relative;\n  display: inline-block;\n  vertical-align: 1px;\n  color: rgba(36, 46, 78, 0.65);\n  text-transform: uppercase;\n  font-weight: 700;\n  font-size: 0.6em;\n}\n\n.trend i {\n  color: #0096ac;\n  font-size: 1.75em;\n}\n\n.trend--down i {\n  color: #e84427;\n}\n\n.prefix {\n  display: inline-block;\n  margin-right: 5px;\n  width: 40px;\n  color: #a9b3d0;\n  font-size: 2em;\n  vertical-align: -8px;\n  text-align: center;\n}\n\n.divided .prefix {\n  margin-left: -5px;\n}\n\n.prefix-float .prefix {\n  float: left;\n  display: block;\n}\n\n.prefix-float .prefix-item {\n  overflow: hidden;\n  display: block;\n}\n\n.prefix-block > * {\n  position: relative;\n  margin: 0 0 5px;\n  padding: 10px 10px 10px 50px;\n  background: #eef1f5;\n}\n\n.prefix-block .prefix {\n  position: absolute;\n  top: 0;\n  left: 0;\n  padding-top: 10px;\n  height: 100%;\n  font-size: 1em;\n  color: #fff;\n}\n\n.progress {\n  position: relative;\n  width: 95px;\n  height: 95px;\n}\n\n.progress svg {\n  position: absolute;\n  top: 0;\n  left: 0;\n}\n\n.progress__indicator {\n  position: relative;\n  border: solid 8px #ccd2e1;\n  width: 100%;\n  height: 100%;\n  -webkit-border-radius: 50%;\n  -moz-border-radius: 50%;\n  border-radius: 50%;\n}\n\n.progress__indicator__done {\n  background: #d0a44c;\n}\n\n.progress__label {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  height: 40px;\n  line-height: 40px;\n  width: 65px;\n  margin: -20px 0 0 -32.5px;\n  text-align: center;\n  font-size: 1.5em;\n  color: #848fae;\n}\n\n.cycler {\n  position: relative;\n  padding: 0 20px;\n  background: #11326f;\n  color: #fff;\n}\n\n.cycler > div {\n  overflow: hidden;\n  width: 100%;\n  padding: 0 20px 10px;\n}\n\n.cycler__nav {\n  position: absolute;\n  top: 10px;\n  left: 7px;\n  height: 40px;\n  line-height: 32px;\n  padding: 0 15px;\n  text-align: center;\n  color: #fff;\n  font-size: 2.5em;\n}\n\n.cycler__nav--next {\n  left: auto;\n  right: 7px;\n}\n\n.cycler__items {\n  overflow: hidden;\n  white-space: nowrap;\n  width: 100%;\n}\n\n.cycler__items > * {\n  white-space: normal;\n  display: inline-block;\n  width: 20%;\n  text-align: center;\n}\n\n.cycler__items.unstyled > li {\n  margin: 0;\n}\n\n.cycler__items a {\n  color: #acb7c5;\n  font-size: 2.8125em;\n  line-height: 1.3em;\n}\n\n.cycler__items a.on,\n.cycler__items .on a,\n.cycler__items a:hover {\n  color: #fff;\n}\n\n.contain--pad {\n  padding: 15px 0;\n}\n\n.nocontain {\n  margin: 0 -10px;\n}\n\nh1.hw,\n.h1.hw {\n  margin-bottom: 10px;\n  font-size: 1em;\n  font-weight: 700;\n}\n\n.definition-src {\n  margin-bottom: 40px;\n}\n\n.di {\n  margin-bottom: 10px;\n}\n\n.entry-body__el--smalltop {\n  padding-top: 10px;\n}\n\n.entry-box__el:last-child,\n.entry-body__el:last-child {\n  margin-bottom: 10px;\n}\n\n.mod.entry h3,\n.mod.entry .h3 {\n  font-size: 1em;\n}\n\n.di-head {\n  padding: 12px 20px;\n  border-left: solid 1px #e6e6eb;\n  border-right: solid 1px #e6e6eb;\n}\n\n.di-head h2,\n.di-head .h2 {\n  font-size: 1em;\n}\n\n.di-head .see-all-translations {\n  margin-left: 1px;\n  display: table;\n  line-height: 22px;\n}\n\n.di-title {\n  font-size: 1.2em;\n  font-weight: bold;\n  line-height: 1.3;\n  // border-bottom: 1px solid currentColor;\n}\n\n.normal-entry .di-title {\n  line-height: 1.3em;\n}\n\n.di-head.normal-entry {\n  position: relative;\n  background: #fff;\n  margin-bottom: 20px;\n  padding: 0;\n  border-width: 0;\n}\n\n.di .pos-header {\n  position: relative;\n  margin: 5px 0 15px;\n  color: rgba(17, 50, 111, 0.91);\n}\n\n.di .irreg-infls,\n.di .inf {\n  color: #292929;\n}\n\n.di .pos-head .pos-info {\n  margin: 0 0 20px;\n}\n\n.cdo-section-title-hw {\n  display: inline;\n  word-wrap: break-word;\n}\n\n.cdo-section-title-hw .headword,\n.di-head.normal-entry .cdo-section-title-hw {\n  display: block;\n  margin: 0 0 10px;\n  font-size: 2.5em;\n  line-height: 1.075em;\n  font-weight: 700;\n}\n\n.di-head.normal-entry .cdo-section-title-hw {\n  margin: 0;\n}\n\n.cdo-section-title-hw .posgram,\n.di-head.normal-entry .posgram,\n.pos-head .pos-info .posgram,\n.HeadwordCtn .GeographicalUsage {\n  font-style: italic;\n  // font-size: 1.25em;\n  color: #444;\n}\n\n.gcs {\n  vertical-align: -1px;\n}\n\n.pos-head .pos-info .posgram {\n  margin-right: 5px;\n}\n\n.pos-head .pos-info .pron,\n.di-head.normal-entry .pron {\n  margin-left: 5px;\n}\n\n.freq,\n.epp-xref {\n  margin-right: 3px;\n  padding: 2px 5px;\n  color: #fff;\n  font-weight: 700;\n  font-size: 0.8em;\n  min-width: 14px;\n  text-align: center;\n  background-color: #444;\n  -webkit-border-radius: 8px;\n  -moz-border-radius: 8px;\n  border-radius: 8px;\n}\n\n.freq {\n  display: none;\n}\n\n.cdo-topic {\n  font-weight: 700;\n}\n\n.extraexamps li[class='eg'] {\n  position: relative;\n  margin-left: 1.2em;\n  list-style-type: disc;\n}\n\n.runon-info .posgram .pos {\n  color: #555;\n}\n\n.i {\n  font-style: italic;\n}\n\n.lab {\n  display: inline;\n  font-variant: small-caps;\n}\n\n.lu {\n  font-weight: 700;\n}\n\n.uk,\n.us,\n.superentry .irreg-infls {\n  margin-left: 5px;\n}\n\n.uk > .region,\n.us > .region {\n  text-transform: uppercase;\n  color: #e84427;\n  font-size: 1em;\n  font-weight: 700;\n}\n\n.superentry .pron {\n  font-size: 1.063em;\n}\n\n.sense-block .hw,\n.sense-block .phrase {\n  font-weight: 700;\n  font-size: 1.1em;\n}\n\n.sense-block .pos {\n  font-style: italic;\n}\n\n.gram {\n  color: #555;\n  margin-right: 3px;\n\n  a {\n    color: inherit;\n  }\n}\n\n.sense-block .guideword {\n  margin-left: 8px;\n}\n\n.sense-block .guideword span {\n  vertical-align: -1px;\n}\n\n.emphasized .gram {\n  font-style: normal;\n}\n\n.pos-head .pos-info .fcdo {\n  font-size: 14px;\n  vertical-align: -2px;\n}\n\n.fav-entry {\n  width: 22px;\n  height: 22px;\n}\n\n.fav-entry .fcdo {\n  line-height: 22px;\n}\n\n.def-block {\n  position: relative;\n}\n\n.def-block .fav-entry {\n  position: absolute;\n  top: 2px;\n  left: 0;\n}\n\n.phrase-block .def-block .fav-entry {\n  top: 4px;\n}\n\n.entry-divide {\n  position: relative;\n  border-top: solid 1px #e6e6eb;\n  border-bottom: solid 1px #e6e6eb;\n  height: 20px;\n}\n\n.results.link-list a {\n  font-weight: 400;\n}\n\n.results.link-list em {\n  font-style: italic;\n  color: #242b4e;\n}\n\n.results.link-list a:hover {\n  text-decoration: none;\n}\n\n.results.link-list a:hover b {\n  text-decoration: underline;\n}\n\n.feature-w,\n.feature-w-big {\n  font-size: 1.25em;\n  line-height: 1em;\n  font-weight: 400;\n}\n\n.feature-w-big {\n  font-size: 2.2em;\n  line-height: 0.9em;\n}\n\n.resp {\n  display: none;\n}\n\n.resp.open {\n  display: block;\n}\n\n.oflow-hide {\n  overflow: hidden;\n}\n\n.center {\n  text-align: center;\n}\n\n.left {\n  text-align: left;\n}\n\n.right {\n  text-align: right;\n}\n\n.lower {\n  text-transform: lowercase;\n}\n\n.upper {\n  text-transform: uppercase;\n}\n\n.clr {\n  clear: both;\n}\n\n.clr-left {\n  clear: left;\n}\n\n.clr-right {\n  clear: right;\n}\n\n.f-left {\n  float: left;\n}\n\n.f-right {\n  float: right;\n}\n\n.title {\n  padding-bottom: 5px;\n  border-bottom: solid 1px #e6e6eb;\n}\n\n.fade {\n  opacity: 0.7;\n}\n\n.hide {\n  display: none;\n}\n\n.hide.open {\n  display: block;\n}\n\n.hide-txt {\n  text-indent: 100%;\n  white-space: nowrap;\n  overflow: hidden;\n}\n\n.hidden {\n  visibility: hidden;\n}\n\n.flush,\ndiv.flush {\n  margin-bottom: 0;\n}\n\n.tight,\n.semi-flush {\n  margin-bottom: 5px;\n}\n\n.nudge-top {\n  margin-top: 2px;\n}\n\n.normal-top {\n  margin-top: 15px;\n}\n\n.normal-base {\n  margin-bottom: 15px;\n}\n\n.space-top {\n  margin-top: 5px;\n}\n\n.space-base {\n  margin-bottom: 10px;\n}\n\n.space-both {\n  margin-top: 5px;\n  margin-bottom: 10px;\n}\n\n.spaced {\n  margin-bottom: 20px;\n}\n\n.spaced-top {\n  margin-top: 20px;\n}\n\n.spaced-out {\n  margin: 5px 0 25px;\n}\n\n.spaced-big {\n  margin-bottom: 30px;\n}\n\n.spaced-big-top {\n  margin-top: 30px;\n}\n\n.pad {\n  padding: 0 5px;\n}\n\n.pad-indent {\n  padding-left: 1em;\n}\n\n.pad-indent-both {\n  padding-left: 1em;\n  padding-right: 1em;\n}\n\n.pad-all {\n  padding: 1em;\n}\n\n.pad-all-sml {\n  padding: 0.5em;\n}\n\n.pad-extra {\n  padding: 0 0.5em;\n}\n\n.pad-l-flush {\n  padding-left: 0;\n}\n\n.pad-l-sml {\n  padding-left: 5px;\n}\n\n.pad-l {\n  padding-left: 10px;\n}\n\n.pad-l-lrg {\n  padding-left: 15px;\n}\n\n.pad-r-flush {\n  padding-right: 0;\n}\n\n.pad-r-sml {\n  padding-right: 5px;\n}\n\n.pad-r {\n  padding-right: 10px;\n}\n\n.pad-r-lrg {\n  padding-right: 15px;\n}\n\n.pad-t-flush {\n  padding-top: 0;\n}\n\n.pad-t-sml {\n  padding-top: 5px;\n}\n\n.pad-t {\n  padding-top: 10px;\n}\n\n.pad-t-lrg {\n  padding-top: 15px;\n}\n\n.pad-b-flush {\n  padding-bottom: 0;\n}\n\n.pad-b-sml {\n  padding-bottom: 5px;\n}\n\n.pad-b {\n  padding-bottom: 10px;\n}\n\n.pad-b-lrg {\n  padding-bottom: 15px;\n}\n\n.pad-sides {\n  padding-left: 10px;\n  padding-right: 10px;\n}\n\n.pad-sides-sml {\n  padding-left: 5px;\n  padding-right: 5px;\n}\n\n.underline {\n  text-decoration: underline;\n}\n\n.fig-frame {\n  width: 100%;\n  text-align: center;\n}\n\n.fig-frame img {\n  border: solid 6px #fff;\n}\n\n.leader {\n  font-size: 1.25em;\n  line-height: 1.4em;\n}\n\n.meta {\n  color: #686868;\n}\n\n.standout {\n  color: #242e4e;\n}\n\n.pointer {\n  cursor: pointer;\n}\n\n.small {\n  font-size: 0.875em;\n}\n\n.smaller {\n  font-size: 0.8em;\n}\n\n.bigger {\n  font-size: 1.125em;\n}\n\n.light {\n  color: #888;\n}\n\n.bg-h:after {\n  content: ' ';\n  position: absolute;\n  top: 0;\n  left: 0;\n  height: 100%;\n  width: 0;\n  z-index: 1;\n}\n\n.accessibility {\n  overflow: hidden;\n  position: absolute;\n  top: -9999px;\n  left: -9999px;\n  float: none;\n  width: auto;\n  margin: 0;\n  padding: 0;\n}\n\n.contain:before,\n.clrd:before,\n.tabs:before,\n.cdo-search:before,\n.stacks:before,\n.tiles:before,\n.tabs__content > .block-wrap:before {\n  content: ' ';\n  display: table;\n}\n\n.contain:after,\n.clrd:after,\n.tabs:after,\n.cdo-search:after,\n.stacks:after,\n.tiles:after,\n.tabs__content > .block-wrap:after {\n  content: ' ';\n  display: table;\n  clear: both;\n}\n\n.dropdown__box {\n  z-index: 9000;\n}\n\n.site-msg {\n  z-index: 9999;\n}\n\n.english-french .pad-indent .runon.pad-indent,\n.french-english .pad-indent .runon.pad-indent {\n  margin-left: -30px;\n}\n\n.english-french .runon-body.pad-indent,\n.french-english .runon-body.pad-indent {\n  margin-left: -20px;\n}\n\n.relativDiv {\n  position: relative;\n}\n\n.divBlock {\n  display: block;\n}\n\n.img-thumb {\n  margin-bottom: 10px;\n}\n\na.a--b {\n  font-weight: bold !important;\n}\n\na.a--rev,\na.a--none {\n  text-decoration: none !important;\n}\n\na.a--rev:hover {\n  text-decoration: underline !important;\n}\n\nlabel,\n.label {\n  display: block;\n  margin: 0;\n  padding: 11px 0;\n  font-weight: bold;\n}\n\ninput[type='text'],\ninput[type='email'],\ninput[type='password'],\ninput.text,\ntextarea,\nselect {\n  padding: 11px;\n  width: 100%;\n  border: 1px solid #ddd;\n  background: #f1f1f1;\n  box-sizing: border-box;\n  border-radius: 2px;\n  box-shadow: inset 1px 1px 2px 0 rgba(0, 0, 0, 0.1);\n  color: #444;\n}\n\ninput[type='text'],\ninput[type='email'],\ninput[type='password'],\ninput.text {\n  height: 44px;\n}\n\n.input-wrap {\n  display: inline-block;\n  width: 100%;\n  vertical-align: bottom;\n}\n\n.form div em {\n  display: block;\n  margin: 5px 0 20px;\n  color: var(--color-font-grey);\n}\n\n.form > div {\n  clear: both;\n  margin-bottom: 10px;\n}\n\n.form input.btn {\n  margin: 20px 0 10px;\n}\n\n.form__inline label {\n  float: none;\n  display: inline;\n  padding: 0 10px 0 0;\n  font-weight: normal;\n}\n\n.form__inline input {\n  float: left;\n  clear: left;\n  margin: 3px 5px 5px 0;\n  width: auto;\n  border: 0 none;\n}\n\n.form__list {\n  display: inline-block;\n  line-height: 1.3em;\n  padding-top: 11px;\n}\n\n.form--restrict input[type='text'],\n.form--restrict input[type='email'],\n.form--restrict input[type='password'],\n.form--restrict input.text,\n.form--restrict textarea,\n.form--restrict select {\n  max-width: 450px;\n}\n\n.text--ico-key input.text,\n.text--ico-key textarea,\n.text--ico-key select {\n  padding-right: 40px;\n}\n\n.csstransforms3d .point {\n  display: block !important;\n  position: absolute;\n  top: 0;\n  left: 0;\n  height: 10px;\n  width: 10px;\n  background: #fff;\n  -moz-transform: rotate(45deg);\n  -webkit-transform: rotate(45deg);\n  transform: rotate(45deg);\n}\n\n.csstransforms3d .tiles--pointer input[type='radio']:checked + label:after {\n  content: '';\n  position: absolute;\n  bottom: -8px;\n  left: 50%;\n  height: 16px;\n  width: 16px;\n  margin-left: -8px;\n  background: #d0a44c;\n  -moz-transform: rotate(45deg);\n  -webkit-transform: rotate(45deg);\n  transform: rotate(45deg);\n}\n\n.btn {\n  display: inline-block;\n  padding: 10px 12px;\n  text-align: center;\n  color: #fff;\n  text-decoration: none;\n  line-height: 1em;\n  cursor: pointer;\n  -webkit-border-radius: 2px;\n  -moz-border-radius: 2px;\n  border-radius: 2px;\n}\n\n.btn--impact {\n  background: #caa54c;\n  border-color: #caa54c;\n  color: #111;\n  font-weight: bold;\n}\n\n.btn--impact:hover {\n  background: #b79441;\n}\n\n.btn--impact2 {\n  padding: 11px 13px;\n  border-width: 0;\n  background: rgba(0, 0, 0, 0.34);\n  font-weight: bold;\n}\n\n.btn--impact2:hover {\n  background: rgba(0, 0, 0, 0.45);\n}\n\n.btn--alt {\n  background: #dde2f0;\n  border-color: #dde2f0;\n}\n\n.btn--alt:hover {\n  background: #c7cee2;\n}\n\n.btn--white {\n  background: #fff;\n  border-color: #fff;\n}\n\n.btn--white:hover {\n  background: #f4f4f4;\n}\n\n.btn--white,\n.btn--alt,\n.btn--alt2 {\n  color: #292929;\n}\n\n.btn--lrg {\n  font-size: 1.1em;\n  font-weight: bold;\n  padding: 12px 20px;\n}\n\n.btn--small {\n  font-size: 0.875em;\n}\n\n.btn--bold,\ninput.btn {\n  font-weight: bold;\n}\n\n.btn--dropdown {\n  padding-right: 30px;\n}\n\n.btn--dropdown-pad {\n  padding-right: 40px;\n}\n\n.btn--dropdown:after {\n  position: absolute;\n  content: '\\f078';\n  top: 50%;\n  right: 10px;\n  margin-top: -8px;\n  font-size: 16px;\n  line-height: 1em;\n  color: #d0a44c;\n  font-weight: 300;\n}\n\n.btn--dropdown.on:after {\n  content: '\\f077';\n}\n\n.btn--options {\n  font-size: 0.8em;\n  padding: 10px 30px 10px 10px;\n  background: #dde2f0;\n  color: #292929;\n  border: 0;\n  border-radius: 0;\n}\n\n.btn--options:hover {\n  background: #dde2f0;\n}\n\n.btn--options:after {\n  font-size: 0.9em;\n  margin-top: -5px;\n}\n\n.btn--plus.on {\n  border-radius: 2px 2px 0 0;\n}\n\n.btn--plus.on .fcdo:before {\n  content: '\\f068';\n}\n\n.btn--input {\n  height: 44px;\n  padding: 12px 16px;\n  border-radius: 0 2px 2px 0;\n}\n\n.btn--input--nudge {\n  padding-bottom: 13px;\n}\n\n.btn--translate:before {\n  content: ' ';\n  position: absolute;\n}\n\n.btn--translate {\n  position: relative;\n  padding: 12px 12px 12px 43px;\n}\n\n.btn--translate:before {\n  width: 31px;\n  height: 31px;\n  top: 4px;\n  left: 10px;\n  background-position: -550px 0;\n}\n\n.btn--social {\n  display: block;\n  border: 0;\n  padding: 12px;\n  margin: 0 auto 10px;\n  font-weight: bold;\n  max-width: 300px;\n}\n\n.btn--social .fcdo {\n  font-size: 1.2em;\n  padding-right: 5px;\n}\n\n.btn .fcdo {\n  color: inherit;\n}\n\n.btn--alt .fcdo {\n  color: #354da5;\n}\n\n.btn--small .fcdo {\n  font-size: 1.14286em;\n}\n\n.btn--ico-l {\n  position: relative;\n  padding-left: 33px;\n}\n\n.btn--ico-l--extra-pad {\n  padding-left: 40px;\n}\n\n.btn--ico-l .fcdo {\n  position: absolute;\n  top: 50%;\n  left: 10px;\n  height: 100%;\n  margin-top: -12px;\n  line-height: 100%;\n  font-size: 22px;\n}\n\n.btn--ico-l .fcdo-quiz {\n  left: 10px;\n  margin-top: -13px;\n  font-size: 26px;\n}\n\n.cols,\n.cols__col {\n  box-sizing: border-box;\n}\n\n.cols .cols__col:first-child {\n  margin-left: 0;\n}\n\n.cols--icons .cols__col {\n  padding: 70px 0 20px;\n}\n\n.txt-block {\n  display: block;\n  font-weight: normal;\n  box-sizing: border-box;\n  text-decoration: none;\n  border-bottom: 1px solid rgba(199, 110, 6, 0.5);\n}\n\n.txt-block--shallow {\n  padding: 4px 20px;\n}\n\n.txt-block--padder {\n  padding: 15px 20px;\n}\n\n.txt-block--alt3 {\n  background: #e84427;\n  color: #fff;\n}\n\n.txt-block--impact {\n  background: #d0a44c;\n  color: #111;\n}\n\n.txt-block--padl {\n  padding-left: 70px;\n}\n\n.txt-block--padr {\n  padding-right: 70px;\n}\n\n.txt-block--alt h2,\n.txt-block--alt h3,\n.txt-block--alt h4,\n.txt-block--alt h5,\n.txt-block--alt2 h2,\n.txt-block--alt2 h3,\n.txt-block--alt2 h4,\n.txt-block--alt2 h5,\n.txt-block--alt .h2,\n.txt-block--alt .h3,\n.txt-block--alt .h4,\n.txt-block--alt .h5,\n.txt-block--alt2 .h2,\n.txt-block--alt2 .h3,\n.txt-block--alt2 .h4,\n.txt-block--alt2 .h5 {\n  color: inherit;\n}\n\n.txt-block--alt h3 span,\n.txt-block--alt .h3 span {\n  color: #a7b5c9;\n}\n\na.txt-block:hover span {\n  text-decoration: underline;\n}\n\na.txt-block {\n  font-weight: bold;\n}\n\na.txt-block--impact .fcdo {\n  color: #303076;\n}\n\na.txt-block:hover {\n  opacity: 0.9;\n}\n\n.txt-block .with-el__el {\n  top: 13px;\n  right: 20px;\n}\n\n.txt-block .with-el__el--icons {\n  top: 8px;\n}\n\n.txt-block.with-icons {\n  padding: 8px 20px;\n}\n\n.txt-block.item-tag {\n  position: relative;\n}\n\n.txt-block.item-tag h2,\n.txt-block.item-tag h3,\n.txt-block.item-tag h4,\n.txt-block.item-tag h5,\n.txt-block.item-tag .h2,\n.txt-block.item-tag .h3,\n.txt-block.item-tag .h4,\n.txt-block.item-tag .h5,\n.txt-block.item-tag p {\n  margin-bottom: 0;\n}\n\n.txt-block .item-tag__tag--clear {\n  background: transparent;\n}\n\n.cols__col--product {\n  position: relative;\n}\n\n.cols__col--product-img {\n  height: 100px;\n  margin: 0 0 15px;\n  position: absolute;\n  top: 0;\n  left: 0;\n}\n\n.cols__col--product .cols__col--product-img {\n  float: left;\n  position: initial;\n  margin-right: 1em;\n}\n\n.section .smaller,\nul.accord > li > ul li a span.alt {\n  position: relative;\n  top: -0.1em;\n}\n\n.spr--ico-key-translation:before {\n  background-position: -114px -239px;\n  width: 47px;\n  height: 47px;\n}\n\n.trends--egt .title {\n  padding-bottom: 0;\n  border-bottom: 0;\n}\n\n.circle.bg--more.open .fcdo-minus,\n.circle.bg--more .bg--more {\n  display: inline-block;\n}\n\n.bg--white {\n  background: #fff;\n}\n\n.bg--def,\n.bg--more {\n  background: #e84427;\n}\n\n.bg--di {\n  background: #0091ff;\n}\n\n.bg--fb {\n  background: #3b5998;\n}\n\n.bg--gp {\n  background: #dc4e41;\n}\n\n.bg--re {\n  background: #ff4500;\n}\n\n.bg--su {\n  background: #eb4924;\n}\n\n.bg--tu {\n  background: #36465d;\n}\n\n.bg--tw {\n  background: #55acee;\n}\n\n.helper {\n  background: #bfcdea;\n  padding: 20px;\n  margin-top: 15px;\n  color: #111;\n  -webkit-border-radius: 3px;\n  -moz-border-radius: 3px;\n  border-radius: 3px;\n  font-size: 0.875em;\n}\n\n.helper .point {\n  display: none;\n  background: #bfcdea;\n  top: 0;\n  left: 50%;\n  margin: -5px 0 0 -5px;\n}\n\na.helper {\n  display: block;\n  padding: 11px 30px 11px 20px;\n  text-decoration: none;\n  font-weight: bold;\n  cursor: pointer;\n}\n\na.helper p {\n  overflow: hidden;\n  margin: 0;\n  white-space: nowrap;\n  line-height: 1em;\n  text-overflow: ellipsis;\n}\n\na.helper:hover,\na.helper:hover .point {\n  background: #b2c0de;\n}\n\n.circle.bg--more .fcdo-minus,\n.circle.bg--more.open .fcdo-plus,\n.translator_layout .content2,\n.translator_layout .content1 {\n  display: none;\n}\n\n.translator_layout .translate-tool .virtualKeyboard {\n  z-index: 5;\n}\n\n.kernerman-copyright-img {\n  height: 20px;\n  margin-right: 5px;\n}\n\n.translator_layout .dropdown[data-selectbox-id='languageTo'] .dropdown__box {\n  right: 0;\n}\n\n.translator_layout .cdo-tpl-alt {\n  margin-top: 5px;\n}\n\n.translator_layout .with-el {\n  border-top: 0;\n}\n\n.translator_layout .translate-tool__from__input {\n  border: 0;\n  resize: none;\n  margin-bottom: 30px;\n  outline: 0;\n}\n\n.translator_layout .translate-tool__from__keyboard-trig {\n  background: transparent;\n}\n\n.translator_layout .translate-tool__from__keyboard-trig {\n  bottom: 0;\n}\n\n.cdo-search__button,\n.cdo-search__dataset {\n  float: right;\n}\n\n.cdo-hdr__blocks--home .cdo-search.cdo-search-centered {\n  float: none;\n  width: 100%;\n}\n\n.cdo-hdr__profile a.hdr-btn .fcdo {\n  opacity: 0.5;\n  color: inherit;\n}\n\n.cdo-hdr__profile a.hdr-btn:hover .fcdo,\n.cdo-hdr__profile a.hdr-btn.on .fcdo {\n  opacity: 1;\n  color: inherit;\n}\n\n.mod-browser .pos {\n  color: gray;\n  font-style: italic;\n}\n\n.sense-block .cdo-cloud-content .pos {\n  color: inherit;\n}\n\n.dropdown--options .dropdown__box .btn {\n  text-align: inherit;\n  font-weight: inherit;\n}\n\n.dropdown--options .dropdown__box a {\n  margin-bottom: 5px;\n}\n\n.modal-confirm {\n  max-width: 400px;\n  min-height: 0;\n}\n\na {\n  cursor: pointer;\n}\n\n.mod-quiz .incorrect {\n  color: #f00;\n}\n\n.mod-quiz .correct {\n  color: #2e8b57;\n}\n\n.mod-quiz .virtualKeyboard {\n  margin-top: 1em;\n}\n\n.mod-quiz .fcdo {\n  color: inherit;\n  vertical-align: top;\n  font-size: 1.33333333em;\n  line-height: 0.75em;\n}\n\n.mod-quiz .circle .fcdo {\n  vertical-align: middle;\n  font-size: inherit;\n  line-height: inherit;\n}\n\n#informational-content .circle-btn--sml {\n  line-height: 22px;\n}\n\n#informational-content .txt-block {\n  padding: 11px 20px;\n}\n\n#informational-content .txt-block--alt4 {\n  background: #bfcce9;\n  padding: 5px 20px;\n}\n\n#informational-content pre.linktous {\n  white-space: normal;\n  word-break: break-word;\n  overflow: hidden;\n  background-color: #f0f0f0;\n  font-size: 1.2em;\n  padding: 0.5em;\n  margin: 0.25em 1px;\n}\n\n.mod-quiz .questionary-resume-item {\n  padding: 10px 20px;\n  margin-bottom: 1em;\n}\n\n.mod-quiz .questionary-resume {\n  text-align: left;\n}\n\n.di.english-chinese-simplified .trans,\n.di.english-chinese-traditional .trans {\n  font-weight: normal;\n}\n\ndiv.entry_title .results .pos {\n  font-style: italic;\n  font-size: 95%;\n}\n\n.mod-define > a:before {\n  content: '\\e903';\n}\n\n.pronunciation-english.entry-body__el {\n  min-height: 0;\n}\n\n.pronunciation-english .pronunciation-item {\n  margin-bottom: 10px;\n}\n\n.pronunciation-english .sound {\n  display: inline-block;\n  background: #e84427;\n  color: #fff;\n  border-radius: 3px;\n  padding: 2px 10px 0;\n  text-transform: uppercase;\n  font-weight: bold;\n  margin-right: 5px;\n}\n\n.pronunciation-english .sound .fcdo {\n  font-size: 1.5em;\n  display: inline-block;\n  margin: 0 5px 3px 0;\n}\n\n.lab {\n  font-size: 1.1em;\n}\n\n.lab .region {\n  text-transform: lowercase;\n  font-style: normal;\n}\n\n.pronVideos {\n  max-width: 560px;\n  margin: 0 auto 20px auto;\n}\n\n.pronVideo {\n  width: 100%;\n  height: 315px;\n  margin-bottom: 10px;\n}\n\n#browseGroups div {\n  display: inline;\n}\n\n.caro__el img {\n  min-width: 100%;\n}\n\n#mobEntryDictName {\n  text-transform: capitalize;\n}\n\nbody {\n  padding-top: 111px;\n}\n\n.default_layout .cdo-search,\n.translator_layout .cdo-search {\n  padding: 7px 20px 6px;\n}\n\n.wordlist-popup * {\n  box-sizing: border-box;\n}\n\n.wordlist-popup {\n  position: absolute;\n  bottom: 25px;\n  right: -10px;\n  width: 240px;\n  margin: 10px 0;\n  z-index: 6;\n  background: #fff;\n  border: 1px solid #c5c5c5;\n  box-shadow: 0 0 15px rgba(0, 0, 0, 0.18);\n  font-size: 14px;\n  text-align: left;\n}\n\n.wordlist-popup:before {\n  content: '';\n  display: inline-block;\n  position: absolute;\n  right: 11px;\n  width: 0;\n  height: 0;\n  border-style: solid;\n  bottom: -10px;\n  border-width: 10px 10px 0 10px;\n  border-color: #c5c5c5 transparent transparent transparent;\n}\n\n.wordlist-popup.under {\n  top: 25px;\n  bottom: auto;\n}\n\n.wordlist-popup.right {\n  right: auto;\n  left: 0;\n}\n\n.wordlist-popup.right:before {\n  right: auto;\n  left: 0;\n}\n\n.wordlist-popup.under:before {\n  bottom: auto;\n  top: -10px;\n  border-width: 0 10px 10px 10px;\n  border-color: transparent transparent #c5c5c5 transparent;\n}\n\n.wordlist-popup ul {\n  max-height: 200px;\n  overflow-y: auto;\n  overflow-x: hidden;\n  margin: 0;\n}\n\n.wordlist-popup li {\n  margin: 0;\n  clear: both;\n}\n\n.wordlist-popup .title,\n.wordlist-popup .login-button {\n  text-decoration: underline;\n}\n\n.wordlist-popup .title {\n  font-weight: bold;\n  padding: 8px;\n  border-bottom: 1px solid #c5c5c5;\n  margin: 0;\n  display: block;\n}\n\n.wordlist-popup .spinner {\n  display: table;\n  margin: 30px auto;\n}\n\n.wordlist-popup .name {\n  float: left;\n  display: block;\n  padding: 8px;\n  text-overflow: ellipsis;\n  width: 180px;\n  overflow: hidden;\n  white-space: nowrap;\n}\n\n.wordlist-popup .name:hover {\n  background: #d8d8ff;\n  text-decoration: underline;\n}\n\n.wordlist-popup .add {\n  margin: 3px 5px;\n  float: right;\n}\n\n.wordlist-popup .error {\n  padding: 8px;\n  background: #a22f1b;\n  color: #fff;\n  font-size: 14px;\n}\n\n.wordlist-popup .info {\n  padding: 8px;\n  background: #bfcce9;\n  color: #11326f;\n  text-overflow: ellipsis;\n  width: 240px;\n  overflow: hidden;\n  font-size: 14px;\n}\n\n.wordlist-popup .circle i {\n  font-size: 1.2em;\n  line-height: 1.9rem;\n}\n\n.wordlist-popup form {\n  position: relative;\n  border-top: solid 1px #c5c5c5;\n  padding: 2px 0;\n}\n\n.wordlist-popup form input {\n  border: 0;\n}\n\n.wordlist-popup form input[type='submit'] {\n  float: right;\n  line-height: 24px;\n}\n\n.wordlist-popup form input[type='text'] {\n  width: 100%;\n  margin: 3px;\n  margin-right: 0;\n  outline: 0;\n  padding: 8px;\n  background: 0;\n  box-shadow: none;\n  margin: 0;\n}\n\n.word-list {\n  word-wrap: break-word;\n  line-break: after-white-space;\n}\n\n.cdo-search__input[type='text'] {\n  outline: 0;\n}\n\n.tiles__tile {\n  text-transform: capitalize;\n}\n\n.ipa {\n  display: inline-block;\n  padding: 0 2px 0 1px;\n}\n\n.cycler__items a {\n  white-space: nowrap;\n}\n\n.i {\n  font-style: italic;\n}\n\n.gl {\n  font-style: normal;\n}\n\n.lex {\n  text-transform: none;\n  font-style: italic;\n}\n\n.b {\n  font-weight: bold;\n}\n\n.sp,\n.nu {\n  font-size: 66%;\n  position: relative;\n  bottom: 0.5em;\n}\n\n.sb,\n.dn {\n  font-size: 66%;\n  position: relative;\n  top: 0.3em;\n}\n\n.cle-xeg,\n.xeg {\n  text-decoration: line-through;\n}\n\n.u {\n  text-decoration: underline;\n}\n\n.email-form {\n  text-align: center;\n}\n\n.tiles__tile label {\n  padding: 20px 0;\n}\n\n.tiles__tile input[type='radio']:checked + label:before,\n.tiles__tile input[type='checkbox']:checked + label:before {\n  padding: 1px 3px 2px 2px;\n  position: absolute;\n  top: 0;\n  left: 0;\n  color: #000;\n  background: rgba(255, 255, 255, 0.8);\n  border-radius: 0 0 8px 0;\n}\n\n.wordlist-panel h1 {\n  font-size: 1.5em;\n}\n\n.wordlist-panel h1.breadcrumb {\n  font-size: 1em;\n}\n\n.wordlist-panel h1 a {\n  text-decoration: none;\n}\n\n.wordlist-panel h1 a:hover {\n  text-decoration: underline;\n}\n\n.fav-entry .fcdo.fcdo-plus {\n  line-height: 31px;\n}\n\n.sect.sect--bg h2 {\n  margin-bottom: 15px;\n  font-size: 1.35em;\n  font-weight: normal;\n}\n\nh1.cdo-hero__title span {\n  color: #d0a44c;\n}\n\n.margin-bottom {\n  margin-bottom: 50px;\n}\n\n#browseResults .title {\n  padding: 0;\n  border: 0 none;\n}\n\n.padding-15 {\n  padding: 15px 0;\n}\n\n.margin-top-15 {\n  margin-top: 15px;\n}\n\n.txt-block--alt a:hover,\n.txt-block--alt2 a:hover {\n  text-decoration: underline;\n}\n\n.trans[lang='ar'] {\n  font-size: 2em;\n  font-weight: normal;\n  unicode-bidi: -webkit-plaintext;\n}\n\n.rv + .rv:before {\n  content: ' / ';\n}\n\n.img-thumb ~ .extraexamps,\n.img-thumb ~ .smartt {\n  clear: both;\n}\n\n.sense-body:after {\n  content: '';\n  display: table;\n  clear: both;\n}\n\n.img-thumb {\n  position: relative;\n  border: 1px solid #bfcce9;\n  display: inline-block;\n}\n\n.img-thumb .img-copyright {\n  border-radius: 0;\n  top: auto;\n  left: auto;\n  right: 0;\n  bottom: 0;\n  color: #fff;\n  background: rgba(0, 0, 0, 0.2);\n  z-index: 2;\n}\n\n.img-thumb .img-copyright span {\n  word-break: normal;\n}\n\n.entry-body.british-grammar .section {\n  margin-top: 20px;\n}\n\n.entry-body.british-grammar .section ~ .section {\n  margin-top: 40px;\n}\n\n.entry-body.british-grammar .section_anchor {\n  height: 0;\n}\n\n.entry-body.british-grammar .panel {\n  margin: 20px 0;\n}\n\n.entry-body.british-grammar blockquote {\n  font-size: inherit;\n  font-style: inherit;\n  color: var(--color-font-grey);\n}\n\n.entry-body.british-grammar blockquote .utterance {\n  clear: both;\n  padding-left: 2em;\n}\n\n.entry-body.british-grammar blockquote .speaker {\n  float: left;\n  font-weight: bold;\n  font-style: normal;\n  margin-left: -2em;\n  margin-top: 2px;\n}\n\n.entry-body.british-grammar .nav p,\n.entry-body.british-grammar td p {\n  margin: 0;\n}\n\n.entry-body.british-grammar .nav > p {\n  margin-top: 20px;\n  line-height: 1.5em;\n  font-weight: 700;\n}\n\n.entry-body.british-grammar .nav ul {\n  margin-left: 30px;\n  list-style-type: none;\n}\n\n.entry-body.british-grammar .nav a {\n  font-weight: 700;\n  text-decoration: none;\n}\n\n.entry-body.british-grammar .nav a:hover {\n  text-decoration: underline;\n}\n\n.entry-body.british-grammar td {\n  background: #eef1f5;\n}\n\n.entry-body.british-grammar blockquote::before {\n  content: '';\n}\n\n.ruby {\n  display: inline-block;\n  text-align: text-bottom;\n}\n\n.rt {\n  display: block;\n  font-size: 80%;\n  text-align: center;\n  font-style: normal;\n}\n\n.rb {\n  display: block;\n}\n\n.intonation-arrow {\n  display: inline-block;\n  height: 2.25em;\n  vertical-align: bottom;\n  width: 0;\n  font-weight: normal;\n}\n\n.entry-body.british-grammar h1 {\n  margin: 0 0 5px;\n}\n\n.entry-body.british-grammar .header {\n  margin-bottom: 20px;\n}\n\n.cloud {\n  margin-bottom: 15px;\n}\n\n.cloud.txt-block {\n  padding-right: 0;\n  background: #eff1f6;\n}\n\n.cloud ul {\n  margin-bottom: 10px;\n  text-align: center;\n  line-height: 1.8em;\n}\n\n.cloud li {\n  display: inline-block;\n  margin-right: 25px;\n}\n\n.cloud li a {\n  color: #16a085;\n}\n\n.cloud li a i {\n  font-style: normal;\n}\n\n.cloud li a .pos {\n  font-style: italic;\n}\n\n.cloud li a.odd {\n  color: #16a085;\n}\n\n.cloud .topic_0 {\n  font-size: 0.9em;\n}\n\n.cloud .topic_2 {\n  font-size: 1.15em;\n}\n\n.cloud .topic_3 {\n  font-size: 1.5em;\n}\n\n.cloud .topic_4 {\n  font-size: 1.8em;\n}\n\n.def-body {\n  display: list;\n\n  .trans {\n    display: block;\n    margin: 0 0 5px 0;\n    font-style: normal;\n    color: var(--color-font-grey);\n\n    &:first-child {\n      font-weight: bold;\n    }\n  }\n\n  .examp {\n    margin-left: 1.3em;\n    display: list-item;\n  }\n}\n\n.phrase-body.pad-indent {\n  padding: 0;\n}\n\n.cols {\n  .item {\n    display: list-item;\n    margin-left: 2.2em;\n  }\n\n  a {\n    color: inherit;\n  }\n}\n\n.js-accord {\n  margin-left: 1em;\n  display: inline-block;\n  padding: 0 8px;\n  border-radius: 5px;\n  color: #fff;\n  background: #797979;\n  cursor: pointer;\n  user-select: none;\n\n  &::before {\n    content: '+ ';\n  }\n\n  + * {\n    display: none;\n\n    a[title^='Synonyms and related'] {\n      color: inherit;\n    }\n  }\n\n  &.open + * {\n    display: block;\n  }\n}\n\n.see_also {\n  a {\n    margin-left: 1em;\n    color: #16a085;\n  }\n}\n\n.def-head {\n  margin-bottom: 0;\n\n  a {\n    color: inherit;\n  }\n}\n\n.share {\n  display: none !important;\n}\n\nul.daccord_b {\n  padding-left: 1.3em;\n}\n\nli.dexamp {\n  list-style-type: disc;\n}\n\n.amp-accordion {\n  > .daccord_h {\n    font-weight: bold;\n    cursor: pointer;\n  }\n\n  & > :last-child {\n    display: none;\n    padding: 0 1em 1em;\n\n    ul {\n      padding-left: 1em;\n    }\n\n    li {\n      list-style-type: disc;\n    }\n  }\n\n  .i-plus:before {\n    content: '+';\n  }\n\n  &.open {\n    .i-plus:before {\n      content: '-';\n    }\n\n    & > :last-child {\n      display: block;\n    }\n  }\n}\n\n.dphrase-block {\n  padding: 5px;\n}\n\n.dwl {\n  position: relative;\n  margin-top: 2px;\n  border-top: solid thin #c76e06;\n}\n"
  },
  {
    "path": "src/components/dictionaries/cambridge/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type CambridgeConfig = DictItem<{\n  lang: 'default' | 'en' | 'en-chs' | 'en-chz'\n  related: boolean\n}>\n\nexport default (): CambridgeConfig => ({\n  lang: '11100000',\n  selectionLang: {\n    english: true,\n    chinese: false,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 265,\n  selectionWC: {\n    min: 1,\n    max: 5\n  },\n  options: {\n    lang: 'default',\n    related: true\n  },\n  options_sel: {\n    lang: ['default', 'en', 'en-chs', 'en-chz']\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/cambridge/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport { getStaticSpeaker } from '@/components/Speaker'\nimport { DictConfigs } from '@/app-config'\nimport {\n  HTMLString,\n  getInnerHTML,\n  handleNoResult,\n  getText,\n  removeChild,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult,\n  getFullLink,\n  externalLink,\n  getChsToChz\n} from '../helpers'\n\nexport const getSrcPage: GetSrcPageFunction = async (text, config, profile) => {\n  let { lang } = profile.dicts.all.cambridge.options\n\n  if (lang === 'default') {\n    switch (config.langCode) {\n      case 'zh-CN':\n        lang = 'en-chs'\n        break\n      case 'zh-TW':\n        lang = 'en-chz'\n        break\n      default:\n        lang = 'en'\n        break\n    }\n  }\n\n  switch (lang) {\n    case 'en':\n      return (\n        'https://dictionary.cambridge.org/search/direct/?datasetsearch=english&q=' +\n        encodeURIComponent(\n          text\n            .trim()\n            .split(/\\s+/)\n            .join('-')\n        )\n      )\n    case 'en-chs':\n      return (\n        'https://dictionary.cambridge.org/zhs/%E6%90%9C%E7%B4%A2/direct/?datasetsearch=english-chinese-simplified&q=' +\n        encodeURIComponent(text)\n      )\n    case 'en-chz': {\n      const chsToChz = await getChsToChz()\n      return (\n        'https://dictionary.cambridge.org/zht/%E6%90%9C%E7%B4%A2/direct/?datasetsearch=english-chinese-traditional&q=' +\n        encodeURIComponent(chsToChz(text))\n      )\n    }\n  }\n}\n\nconst HOST = 'https://dictionary.cambridge.org'\n\ntype CambridgeResultItem = {\n  id: string\n  html: HTMLString\n}\n\nexport type CambridgeResult = CambridgeResultItem[]\n\ntype CambridgeSearchResult = DictSearchResult<CambridgeResult>\n\nexport const search: SearchFunction<CambridgeResult> = async (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  return fetchDirtyDOM(await getSrcPage(text, config, profile))\n    .catch(handleNetWorkError)\n    .then(doc => handleDOM(doc, profile.dicts.all.cambridge.options))\n}\n\nfunction handleDOM(\n  doc: Document,\n  options: DictConfigs['cambridge']['options']\n): CambridgeSearchResult | Promise<CambridgeSearchResult> {\n  const result: CambridgeResult = []\n  const catalog: NonNullable<CambridgeSearchResult['catalog']> = []\n  const audio: { us?: string; uk?: string } = {}\n\n  doc.querySelectorAll('.entry-body__el').forEach(($entry, i) => {\n    if (!getText($entry, '.headword')) {\n      return\n    }\n\n    const $posHeader = $entry.querySelector('.pos-header')\n    if ($posHeader) {\n      $posHeader.querySelectorAll('.dpron-i').forEach($pron => {\n        const $daud = $pron.querySelector<HTMLSpanElement>('.daud')\n        if (!$daud) return\n        const $source = $daud.querySelector<HTMLSourceElement>(\n          'source[type=\"audio/mpeg\"]'\n        )\n        if (!$source) return\n\n        const src = getFullLink(HOST, $source, 'src')\n\n        if (src) {\n          $daud.replaceWith(getStaticSpeaker(src))\n\n          if (!audio.uk && $pron.classList.contains('uk')) {\n            audio.uk = src\n          }\n\n          if (!audio.us && $pron.classList.contains('us')) {\n            audio.us = src\n          }\n        }\n      })\n      removeChild($posHeader, '.share')\n    }\n\n    sanitizeEntry($entry)\n\n    const entryId = `d-cambridge-entry${i}`\n\n    result.push({\n      id: entryId,\n      html: getInnerHTML(HOST, $entry)\n    })\n\n    catalog.push({\n      key: `#${i}`,\n      value: entryId,\n      label:\n        '#' + getText($entry, '.di-title') + ' ' + getText($entry, '.posgram')\n    })\n  })\n\n  if (result.length <= 0) {\n    // check idiom\n    const $idiom = doc.querySelector('.idiom-block')\n    if ($idiom) {\n      removeChild($idiom, '.bb.hax')\n\n      sanitizeEntry($idiom)\n\n      result.push({\n        id: 'd-cambridge-entry-idiom',\n        html: getInnerHTML(HOST, $idiom)\n      })\n    }\n  }\n\n  if (result.length <= 0 && options.related) {\n    const $link = doc.querySelector('link[rel=canonical]')\n    if (\n      $link &&\n      /dictionary\\.cambridge\\.org\\/([^/]+\\/)?spellcheck\\//.test(\n        $link.getAttribute('href') || ''\n      )\n    ) {\n      const $related = doc.querySelector('.hfl-s.lt2b.lmt-10.lmb-25.lp-s_r-20')\n      if ($related) {\n        result.push({\n          id: 'd-cambridge-entry-related',\n          html: getInnerHTML(HOST, $related)\n        })\n      }\n    }\n  }\n\n  if (result.length > 0) {\n    return { result, audio, catalog }\n  }\n\n  return handleNoResult()\n}\n\nfunction sanitizeEntry<E extends Element>($entry: E): E {\n  // expand button\n  $entry.querySelectorAll('.daccord_h').forEach($btn => {\n    $btn.parentElement!.classList.add('amp-accordion')\n  })\n\n  // replace amp-img\n  $entry.querySelectorAll('amp-img').forEach($ampImg => {\n    const $img = document.createElement('img')\n\n    $img.setAttribute('src', getFullLink(HOST, $ampImg, 'src'))\n\n    const attrs = ['width', 'height', 'title']\n    for (const attr of attrs) {\n      const val = $ampImg.getAttribute(attr)\n      if (val) {\n        $img.setAttribute(attr, val)\n      }\n    }\n\n    $ampImg.replaceWith($img)\n  })\n\n  // replace amp-audio\n  $entry.querySelectorAll('amp-audio').forEach($ampAudio => {\n    const $source = $ampAudio.querySelector('source')\n    if ($source) {\n      const src = getFullLink(HOST, $source, 'src')\n      if (src) {\n        $ampAudio.replaceWith(getStaticSpeaker(src))\n        return\n      }\n    }\n    $ampAudio.remove()\n  })\n\n  // See more results\n  $entry.querySelectorAll<HTMLAnchorElement>('a.had').forEach(externalLink)\n\n  return $entry\n}\n"
  },
  {
    "path": "src/components/dictionaries/cnki/View.tsx",
    "content": "import React, { FC } from 'react'\nimport { CNKIResult } from './engine'\nimport EntryBox from '@/components/EntryBox'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictCambridge: FC<ViewPorps<CNKIResult>> = ({ result }) => (\n  <div className=\"dictCNKI\">\n    {result.dict.length > 0 && (\n      <EntryBox title=\"英汉汉英词典\">\n        {result.dict.map(({ word, href }, i) => (\n          <a\n            key={i}\n            className=\"dictCNKI-DictLink\"\n            href={href}\n            target=\"_blank\"\n            rel=\"nofollow noopener noreferrer\"\n          >\n            {word}\n          </a>\n        ))}\n      </EntryBox>\n    )}\n    {result.senbi.length > 0 && (\n      <EntryBox title=\"双语例句\" className=\"dictCNKI-Sensbi\">\n        {result.senbi.map(({ title, more, sens }, i) => (\n          <React.Fragment key={i}>\n            <h2 className=\"dictCNKI-SensTitle\">{title}</h2>\n            {sens.map((sen, i) => (\n              <StrElm tag=\"p\" key={i} html={sen} />\n            ))}\n            <div className=\"dictCNKI-SensMore\">\n              <a href={more} target=\"_blank\" rel=\"nofollow noopener noreferrer\">\n                更多\n              </a>\n            </div>\n          </React.Fragment>\n        ))}\n      </EntryBox>\n    )}\n    {result.seneng.length > 0 && (\n      <EntryBox title=\"英文例句\" className=\"dictCNKI-Senseng\">\n        {result.seneng.map(({ title, more, sens }, i) => (\n          <React.Fragment key={i}>\n            <h2 className=\"dictCNKI-SensTitle\">{title}</h2>\n            {sens.map((sen, i) => (\n              <StrElm tag=\"p\" key={i} html={sen} />\n            ))}\n            <div className=\"dictCNKI-SensMore\">\n              <a href={more} target=\"_blank\" rel=\"nofollow noopener noreferrer\">\n                更多\n              </a>\n            </div>\n          </React.Fragment>\n        ))}\n      </EntryBox>\n    )}\n  </div>\n)\n\nexport default DictCambridge\n"
  },
  {
    "path": "src/components/dictionaries/cnki/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"CNKI Dict\",\n    \"zh-CN\": \"CNKI翻译助手\",\n    \"zh-TW\": \"CNKI翻譯助手\"\n  },\n  \"options\": {\n    \"dict\": {\n      \"en\": \"Show dict result\",\n      \"zh-CN\": \"显示词典结果\",\n      \"zh-TW\": \"展示字典結果\"\n    },\n    \"senbi\": {\n      \"en\": \"Show bilingual sentences\",\n      \"zh-CN\": \"显示双语例句\",\n      \"zh-TW\": \"展示雙語例句\"\n    },\n    \"seneng\": {\n      \"en\": \"Show English sentences\",\n      \"zh-CN\": \"显示英文例句\",\n      \"zh-TW\": \"展示英文例句\"\n    },\n    \"digests\": {\n      \"en\": \"Show relevant digests\",\n      \"zh-CN\": \"显示相关文摘\",\n      \"zh-TW\": \"展示相關文摘\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/cnki/_style.shadow.scss",
    "content": ".dictCNKI-DictLink {\n  display: inline-block; // keep in one line\n  margin-right: 1em;\n  line-height: 1.8;\n}\n\n.dictCNKI-Sensbi {\n  p {\n    position: relative;\n    padding-left: 2em;\n  }\n\n  p:nth-of-type(2n + 1) {\n    margin-bottom: 0;\n\n    & + p {\n      margin-top: 0;\n    }\n\n    &::before {\n      content: '•';\n      display: block;\n      position: absolute;\n      left: 0.8em;\n    }\n  }\n\n  font {\n    color: #f9690e;\n  }\n}\n\n.dictCNKI-Senseng {\n  p {\n    position: relative;\n    padding-left: 2em;\n  }\n\n  p::before {\n    content: '•';\n    display: block;\n    position: absolute;\n    left: 0.8em;\n  }\n\n  font {\n    color: #f9690e;\n  }\n}\n\n.dictCNKI-SensTitle {\n  margin-left: 5px;\n  font-size: 1.3em;\n}\n\n.dictCNKI-SensMore {\n  text-align: right;\n  padding: 0 1em;\n}\n"
  },
  {
    "path": "src/components/dictionaries/cnki/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type CnkiConfig = DictItem<{\n  dict: boolean\n  senbi: boolean\n  seneng: boolean\n}>\n\nexport default (): CnkiConfig => ({\n  lang: '11000000',\n  selectionLang: {\n    english: true,\n    chinese: true,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 300,\n  selectionWC: {\n    min: 1,\n    max: 100\n  },\n  options: {\n    dict: true,\n    senbi: true,\n    seneng: true\n    // digests: true,\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/cnki/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  HTMLString,\n  getInnerHTML,\n  getFullLink,\n  handleNoResult,\n  getText,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult\n} from '../helpers'\nimport { DictConfigs } from '@/app-config'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return (\n    'http://dict.cnki.net/old/dict_result.aspx?scw=' + encodeURIComponent(text)\n  )\n}\n\nconst HOST = 'http://dict.cnki.net/old'\n\ninterface CNKIDictItem {\n  word: string\n  href: string\n}\n\ninterface CNKISensItem {\n  title: string\n  more: string\n  sens: HTMLString[]\n}\n\nexport interface CNKIResult {\n  dict: CNKIDictItem[]\n  senbi: CNKISensItem[]\n  seneng: CNKISensItem[]\n  // digests?: {\n  //   more: string\n  //   content: HTMLString\n  // }\n}\n\ntype CNKISearchResult = DictSearchResult<CNKIResult>\n\nexport const search: SearchFunction<CNKIResult> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  return fetchDirtyDOM(\n    'http://dict.cnki.net/old/dict_result.aspx?scw=' + encodeURIComponent(text),\n    { withCredentials: false }\n  )\n    .catch(handleNetWorkError)\n    .then(doc => handleDOM(doc, profile.dicts.all.cnki.options))\n}\n\nfunction handleDOM(\n  doc: Document,\n  options: DictConfigs['cnki']['options']\n): CNKISearchResult | Promise<CNKISearchResult> {\n  const $entries = [...doc.querySelectorAll('.main-table')]\n\n  const result: CNKIResult = {\n    dict: [],\n    senbi: [],\n    seneng: []\n  }\n\n  if (options.dict) {\n    const $dict = $entries.find($e =>\n      Boolean($e.querySelector('img[src=\"images/02.gif\"]'))\n    )\n    if ($dict) {\n      result.dict = [...$dict.querySelectorAll('.zztj li')]\n        .map($li => {\n          const word = ($li.textContent || '').trim()\n          if (word) {\n            const $a = $li.querySelector('a:nth-of-type(2)')\n            if ($a) {\n              const href = getFullLink(HOST, $a, 'href')\n              if (href) {\n                return { word, href }\n              }\n            }\n          }\n        })\n        .filter((x): x is CNKIDictItem => Boolean(x))\n    }\n  }\n\n  if (options.senbi) {\n    result.senbi = extractSens(\n      $entries,\n      'img[src=\"images/word.jpg\"]',\n      'showjd_'\n    )\n  }\n\n  if (options.seneng) {\n    result.seneng = extractSens(\n      $entries,\n      'img[src=\"images/dian_ywlj.gif\"]',\n      'showlj_'\n    )\n  }\n  // if (options.digests) {\n  //   const $digests = $entries.find($e => Boolean($e.querySelector('img[src=\"images/04.gif\"]')))\n  //   if ($digests) {\n  //     let more = ''\n\n  //     $digests.querySelectorAll('td[align=right]').forEach($td => {\n  //       if (($td.textContent || '').trim().endsWith('更多相关文摘')) {\n  //         const $a = $td.querySelector('a')\n  //         if ($a) {\n  //           more = getFullLink($a, 'href')\n  //         }\n  //       }\n  //       $td.remove()\n  //     })\n\n  //     result.digests = {\n  //       more,\n  //       content: [...$digests.querySelectorAll('p')]\n  //         .map($p => getOuterHTML($p).replace(/&nbsp;/g, ''))\n  //         .join('')\n  //     }\n  //   }\n  // }\n\n  if (\n    // result.digests ||\n    result.dict.length > 0 ||\n    result.senbi.length > 0 ||\n    result.seneng.length > 0\n  ) {\n    return { result }\n  }\n\n  return handleNoResult()\n}\n\nfunction extractSens(\n  $entries: Element[],\n  selector: string,\n  sensid: string\n): CNKISensItem[] {\n  const $sens = $entries.find($e => Boolean($e.querySelector(selector)))\n  if (!$sens) {\n    return []\n  }\n\n  return [...$sens.querySelectorAll(`[id^=${sensid}]`)].map($sens => {\n    let more = ''\n\n    $sens.querySelectorAll('td[align=right]').forEach($td => {\n      if (($td.textContent || '').trim() === '更多') {\n        const $a = $td.querySelector('a')\n        if ($a) {\n          more = getFullLink(HOST, $a, 'href')\n        }\n      }\n      $td.remove()\n    })\n\n    return {\n      title: getText($sens.previousElementSibling!).trim(),\n      more,\n      sens: [...$sens.querySelectorAll('td')].map($td =>\n        getInnerHTML(HOST, $td).replace(/&nbsp;/g, '')\n      )\n    }\n  })\n}\n"
  },
  {
    "path": "src/components/dictionaries/cobuild/View.tsx",
    "content": "import React, { FC, useState } from 'react'\nimport { Speaker } from '@/components/Speaker'\nimport StarRates from '@/components/StarRates'\nimport { COBUILDResult, COBUILDCibaResult, COBUILDColResult } from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictCOBUILD: FC<ViewPorps<COBUILDResult>> = ({ result }) => {\n  switch (result.type) {\n    case 'ciba':\n      return renderCiba(result)\n    case 'collins':\n      return renderCol(result)\n  }\n  return null\n}\n\nexport default DictCOBUILD\n\nfunction renderCiba(result: COBUILDCibaResult) {\n  return (\n    <>\n      <h1 className=\"dictCOBUILD-Title\">{result.title}</h1>\n      {result.prons && (\n        <ul className=\"dictCOBUILD-Pron\">\n          {result.prons.map(p => (\n            <li key={p.phsym} className=\"dictCOBUILD-PronItem\">\n              {p.phsym}\n              <Speaker src={p.audio} />\n            </li>\n          ))}\n        </ul>\n      )}\n      <div className=\"dictCOBUILD-Rate\">\n        {(result.star as number) >= 0 && <StarRates rate={result.star} />}\n        {result.level && (\n          <span className=\"dictCOBUILD-Level\">{result.level}</span>\n        )}\n      </div>\n      {result.defs && (\n        <ol className=\"dictCOBUILD-Defs\">\n          {result.defs.map((def, i) => (\n            <StrElm tag=\"li\" className=\"dictCOBUILD-Def\" key={i} html={def} />\n          ))}\n        </ol>\n      )}\n    </>\n  )\n}\n\nfunction renderCol(result: COBUILDColResult) {\n  const [iSec, setiSec] = useState('0')\n  const curSection = result.sections[iSec]\n\n  return (\n    <div className=\"dictCOBUILD-ColEntry\">\n      {result.sections.length > 0 && (\n        <select value={iSec} onChange={e => setiSec(e.currentTarget.value)}>\n          {result.sections.map((section, i) => (\n            <option key={section.id} value={i}>\n              {section.type}\n              {section.title ? ` :${section.title}` : ''}\n              {section.num ? ` ${section.num}` : ''}\n            </option>\n          ))}\n        </select>\n      )}\n      <div className=\"dictionary\">\n        <div className=\"dc\">\n          <div className=\"he\">\n            <div className=\"page\">\n              <div className=\"dictionary\">\n                <div className=\"dictentry\">\n                  <div className=\"dictlink\">\n                    <StrElm\n                      key={curSection}\n                      className={curSection.className}\n                      html={curSection.content}\n                    />\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/dictionaries/cobuild/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"COBUILD\",\n    \"zh-CN\": \"柯林斯高阶\",\n    \"zh-TW\": \"柯林斯高階\"\n  },\n  \"options\": {\n    \"cibaFirst\": {\n      \"en\": \"Prefer bilingual dictionary\",\n      \"zh-CN\": \"优先查双语词典\",\n      \"zh-TW\": \"優先查雙語詞典\"\n    }\n  },\n  \"helps\": {\n    \"cibaFirst\": {\n      \"en\": \"Bilingual server is less stable\",\n      \"zh-CN\": \"双语词典服务器不稳定，解释没全英丰富\",\n      \"zh-TW\": \"雙語詞典伺服器不穩定，解釋沒全英豐富\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/cobuild/_style.shadow.scss",
    "content": ".dictCOBUILD-Title {\n  font-size: 1.5em;\n  font-weight: bold;\n}\n\n.dictCOBUILD-Pron {\n  display: flex;\n  margin-bottom: 5px;\n}\n\n.dictCOBUILD-PronItem {\n  margin-right: 1em;\n\n  .icon-Speaker {\n    margin-left: 2px;\n  }\n}\n\n.dictCOBUILD-Rate {\n  display: flex;\n  margin-bottom: 5px;\n}\n\n.dictCOBUILD-Level {\n  margin-left: 10px;\n  color: #aaa;\n}\n\n.dictCOBUILD-Defs {\n  margin: 0;\n  padding-left: 15px;\n}\n\n.dictCOBUILD-Def {\n  p {\n    margin: 0;\n  }\n\n  b {\n    color: #f9690e;\n  }\n\n  .prep-en {\n    display: block;\n    margin-bottom: 5px;\n  }\n\n  .text-sentence {\n    padding-left: 10px;\n    padding-bottom: 5px;\n    color: var(--color-font-grey);\n    border-left: var(--color-font-grey) solid 1px;\n\n    &:last-child {\n      padding-bottom: 0;\n      margin-bottom: 10px;\n    }\n  }\n}\n\n.dictCOBUILD-ColEntry {\n  * {\n    word-wrap: break-word;\n    margin: 0;\n    padding: 0;\n    border: 0;\n    box-sizing: border-box;\n  }\n\n  .padLeft {\n    padding-left: 20px;\n  }\n\n  .textCenter {\n    text-align: center;\n  }\n\n  ol,\n  ul {\n    list-style-type: none;\n  }\n\n  q {\n    quotes: none;\n  }\n\n  input,\n  button,\n  select {\n    font-size: inherit;\n    color: inherit;\n    padding: 0.5em;\n    border: solid 1px #c3c3c3;\n  }\n\n  label {\n    display: block;\n  }\n\n  label.inline {\n    display: inline;\n  }\n\n  button {\n    cursor: pointer;\n  }\n\n  h1 {\n    font-size: 2em;\n    line-height: 1.4em;\n  }\n\n  h2 {\n    font-size: 1.2em;\n  }\n\n  .center {\n    display: table;\n    margin: 0 auto;\n  }\n\n  .clear:before,\n  .clear:after {\n    content: ' ';\n    display: block;\n    height: 0;\n    overflow: hidden;\n  }\n\n  .clear:after {\n    clear: both;\n  }\n\n  .floatRight {\n    float: right;\n  }\n\n  .discrete {\n    font-size: 0.85em;\n    color: #888;\n  }\n\n  .text-right {\n    text-align: right;\n  }\n\n  .columns-block {\n    -webkit-column-break-inside: avoid;\n    page-break-inside: avoid;\n    break-inside: avoid;\n    display: table;\n    width: 100%;\n  }\n\n  .white .topslot_container {\n    margin-left: auto;\n    margin-right: auto;\n  }\n\n  .topslot_container,\n  #ad_rightslot,\n  #ad_rightslot2 {\n    margin-bottom: 20px;\n  }\n\n  .ac_leftslot_a {\n    min-height: 600px;\n  }\n\n  .btmslot_a-container {\n    width: 100%;\n  }\n\n  .content-box {\n    padding: 0;\n    margin-bottom: 1em;\n    -webkit-column-break-inside: avoid;\n    page-break-inside: avoid;\n    break-inside: avoid;\n  }\n\n  .content-box h2,\n  .content-box .h2 {\n    margin-bottom: 0.5em;\n  }\n\n  .content-box .view_more a {\n    opacity: 0.7;\n    text-decoration: none;\n    margin-top: 1em;\n    display: block;\n    font-size: 0.8em;\n  }\n\n  .lsw {\n    font-weight: bold;\n  }\n\n  .lsw a {\n    text-decoration: none;\n  }\n\n  .lsw[data-type='trends'] a {\n    margin-left: 0.5em;\n  }\n\n  .lsw[data-type='trends'] .percentVariation .icon-fiber_new {\n    font-size: 2em;\n  }\n\n  .lsw[data-type='trends'] .percentVariation {\n    display: inline-block;\n    min-width: 3em;\n    text-align: right;\n  }\n\n  .lsw i {\n    font-size: 1.3em;\n    vertical-align: sub;\n    margin-right: 5px;\n  }\n\n  .lsw .lsw_title {\n    font-size: 1.5em;\n    margin-bottom: 1em;\n  }\n\n  .lsw[data-type='trends'] .lsw_title {\n    margin-bottom: 0.3em;\n  }\n\n  .lsw .lsw_title i {\n    font-size: 3em;\n    color: #7e7b87;\n    margin-right: 10px;\n  }\n\n  .lsw .view_more {\n    border: solid 1px #1c4b8b;\n    padding: 20px;\n    display: inline-block;\n    background: #e8e8e8;\n    margin: 1em 20px 0;\n    font-size: 1.1em;\n    padding: 15px 25px;\n  }\n\n  .lsw .lsw_list {\n    margin-left: 1em;\n  }\n\n  .lsw[data-type='trends'] .lsw_list {\n    line-height: 1.2em;\n  }\n\n  .lsw .lsw_list li {\n    margin-bottom: 5px;\n  }\n\n  .lsw .lsw_list span {\n    font-size: 0.7em;\n    color: grey;\n  }\n\n  .lsw i.green {\n    color: #008000;\n  }\n\n  .lsw i.red {\n    color: #e05555;\n  }\n\n  .lgg {\n    background: 0;\n    font-weight: bold;\n    border: 0;\n    box-shadow: none;\n    font-size: 1.2em;\n    padding: 0;\n  }\n\n  .lgg h2,\n  .lgg .margin {\n    color: var(--color-font-grey);\n    text-align: center;\n    padding: 21px 5px;\n    margin-bottom: 5px;\n  }\n\n  .lgg a {\n    text-decoration: none;\n    margin-bottom: 5px;\n    text-align: center;\n    padding: 10px;\n    border: 1px solid rgba(216, 216, 216, 0.7);\n    border-radius: 5px;\n    background-color: #e5ebf3;\n    display: block;\n  }\n\n  .lgg a:hover {\n    border: 1px solid rgba(144, 144, 144, 0.7);\n    background-color: rgba(187, 183, 183, 0.37);\n  }\n\n  .def-dict {\n    text-decoration: none;\n    display: block;\n    box-shadow: 0 0 3px rgba(0, 0, 0, 0.2);\n    background: #e5ebf3;\n    color: #194885;\n  }\n\n  .def-dict-title {\n    font-size: 1.6em;\n    margin: 0.5em 0 1em 0;\n    font-weight: bold;\n  }\n\n  .def-dict-footer {\n    margin-top: 1em;\n    font-weight: bold;\n    color: #1c4b8b;\n  }\n\n  .def-dict-footer i {\n    font-size: 1.5em;\n    vertical-align: text-top;\n    margin-right: 10px;\n  }\n\n  .toc {\n    padding: 0 10px;\n    text-align: center;\n    margin-bottom: 0.5em;\n  }\n\n  .toc-group {\n    display: inline-block;\n    margin-right: 1em;\n  }\n\n  .related {\n    -webkit-column-break-inside: avoid;\n    page-break-inside: avoid;\n    break-inside: avoid;\n  }\n\n  .related-title {\n    font-weight: bold;\n    text-decoration: none;\n    border-bottom: 1px dotted;\n  }\n\n  .related-definition {\n    font-style: italic;\n  }\n\n  .promoBoxGrammar {\n    overflow: hidden;\n  }\n\n  .promoBoxGrammar .entry_container ul {\n    overflow: hidden;\n  }\n\n  .promoBoxGrammar .entry_container {\n    box-shadow: none;\n    background-color: white;\n    overflow-y: auto;\n    max-height: 400px;\n  }\n\n  .browse-block {\n    -webkit-column-break-inside: avoid;\n    page-break-inside: avoid;\n    break-inside: avoid;\n  }\n\n  .dc .cobuild-logo {\n    display: none;\n    width: 150px;\n    height: 20px;\n    // background: url(https://www.collinsdictionary.com/external/images/cobuild-logo.png?version=3.1.211);\n    background-size: contain;\n    background-repeat: no-repeat;\n    background-position: center center;\n    float: right;\n    text-decoration: none;\n    border: 0;\n    margin-top: 5px;\n  }\n\n  .dc .cobuild-logo.partner {\n    width: 200px;\n    background-image: url(https://www.collinsdictionary.com/external/images/cobuild-word-partner.png?version=3.1.211);\n    margin-right: 0.5em;\n    margin-top: 10px;\n  }\n\n  .beta {\n    background-image: url(https://www.collinsdictionary.com/external/images/beta.png?version=3.1.211);\n    background-repeat: no-repeat;\n    background-position: 50% 50%;\n    background-size: 75%;\n    display: inline-block;\n    width: 80px;\n  }\n\n  .dc .cobuild-partner {\n    float: right;\n  }\n\n  .dc .expandable-list {\n    padding: 0.1em;\n  }\n\n  .dc .content-box {\n    padding-top: 0;\n  }\n\n  .dc .content-box-thesaurus {\n    margin-top: 20px;\n  }\n\n  .dc .content-box-header:after {\n    content: ' ';\n    display: block;\n    clear: both;\n  }\n\n  .dc .content-box-header {\n    background: #e5ebf3;\n    padding: 0.5em 15px;\n  }\n\n  .cdet .dc .content-box .content-box-header,\n  .dc .content .content-box-header {\n    background: 0;\n    padding: 0;\n    margin: 0;\n  }\n\n  .dc .content-box-definition.cobuild .content-box-header,\n  .dc .content-box-collocation.cobuild .content-box-header,\n  .dc .content-box-learners .content-box-header {\n    background: #ccedf0;\n  }\n\n  .dc .content-box-examples .content-box-header {\n    background: #deefe3;\n  }\n\n  .dc .content-box-translations .content-box-header {\n    background: #fff6dd;\n  }\n\n  .dc .content-box-nearby-words .content-box-header {\n    background: #ffefeb;\n  }\n\n  .dc .content-box-origin .content-box-header {\n    background: #eff7ff;\n  }\n\n  .dc .content-box-comments .content-box-header {\n    background: #dbf7dc;\n  }\n\n  .dc .content-box-usage .content-box-header {\n    background: #e9ffe3;\n  }\n\n  .dc .content-box-images .content-box-header {\n    background: #f4ffd4;\n  }\n\n  .dc .content-box-videos .content-box-header {\n    background: #ffebeb;\n  }\n\n  .dc .content-box-wordlists .content-box-header {\n    background: #fff1e6;\n  }\n\n  .dc .content-box-quotations .content-box-header {\n    background: #fff1e6;\n  }\n\n  .dc .content-box-related-terms .content-box-header {\n    background: #fffeec;\n  }\n\n  .dc .content-box-header .h2_entry {\n    margin-bottom: 0;\n  }\n\n  .dc .content-box-videos .entryVideo {\n    width: 100%;\n    max-width: 640px;\n    height: 320px;\n  }\n\n  .dc .h2_entry {\n    display: none;\n    font-size: 1.4em;\n    margin-bottom: 0.5em;\n  }\n\n  .dc .content-box-definition h2.h2_entry {\n    font-size: 1.8em;\n    line-height: 1;\n  }\n\n  .dc .content-box-examples h2.h2_entry {\n    display: inline-block;\n  }\n\n  .dc .definitions .thes {\n    margin-top: 0.8em;\n  }\n\n  .dc .example_box,\n  .dc ol li,\n  .dc .thesaurus_synonyms,\n  .verbtable_content .headword_link,\n  .dc .link-right.verbtable {\n    margin-bottom: 1em;\n  }\n\n  .dc .h3_entry {\n    font-size: 1.3em;\n    margin: 0.5em 0;\n  }\n\n  .dc .sense_list .scbold {\n    display: block;\n    font-style: italic;\n    font-family: 'Times New Roman', Times, serif;\n    border-bottom: 1px dotted #c5c5c5;\n  }\n\n  .dc strong,\n  .dc .content-box-synonyms .firstSyn,\n  .dc .content-box-nearby-words .current,\n  .dc .cit-type-example .orth,\n  .dc .mini_h2 .orth,\n  .dc .example_box .author,\n  .dc .thesaurus_synonyms .synonym:first-of-type,\n  .dc .content-box-translation .phr,\n  .dc .blackbold {\n    font-weight: bold;\n  }\n\n  .dc .blackbold {\n    color: black;\n  }\n\n  .dc .school .def {\n    display: inline;\n  }\n\n  .dc .def {\n    font-size: 1.15em;\n    margin-bottom: 0.5em;\n    line-height: 1.5em;\n  }\n\n  .dc .sensenumdef .def {\n    font-size: 1.1em;\n    margin: 0.5em 0;\n  }\n\n  .dc .hom ol ol {\n    list-style-type: lower-alpha;\n  }\n\n  .dc #synonyms_content:first-of-type {\n    border: 0;\n  }\n\n  .dc #synonyms_content {\n    border-top: 1px dotted #c5c5c5;\n    padding-top: 12px;\n  }\n\n  .dc .lbl.type-register,\n  .dc .lbl.misc,\n  .dc .colloc,\n  .dc #synonyms_content .thesaurus_synonyms .lbl,\n  .dc #synonyms_content .thesaurus_synonyms .misc {\n    font-style: italic;\n  }\n\n  .dc .lbl.type-curric {\n    font-variant: small-caps;\n  }\n\n  .school .form.type-drv .orth,\n  .school .form.type-phrasalverb .orth {\n    font-weight: bold;\n    font-size: 1.1em;\n  }\n\n  .school .fraction {\n    display: inline-block;\n    position: relative;\n    vertical-align: middle;\n    letter-spacing: 0.001em;\n    text-align: center;\n    padding-left: 3px;\n    padding-right: 3px;\n    font-size: 0.7em;\n    line-height: 1.1em;\n  }\n\n  .school .fraction > span {\n    display: none;\n    padding: 0 0.2em;\n  }\n\n  .school .fraction > .denominator {\n    border-top: thin solid black;\n  }\n\n  .school .fraction > .numerator,\n  .school .fraction > .denominator {\n    display: block;\n  }\n\n  .dc .lbl.type-syntax {\n    font-size: 0.8em;\n    color: var(--color-font-grey);\n  }\n\n  .dc ol {\n    list-style-position: outside;\n    list-style-type: decimal;\n    margin-left: 2em;\n  }\n\n  .dc ol.single,\n  .dc ol ol.single {\n    list-style-type: none;\n  }\n\n  .dc .content-box-synonyms .h2_entry {\n    display: inline-block;\n  }\n\n  .dc .content-box-synonyms .extra-link,\n  .dc .content-box-collocation .explore-collocation {\n    display: inline-block;\n    margin-left: 1em;\n  }\n\n  .dc .thesaurus_synonyms .firstSyn {\n    font-weight: bold;\n    display: inline-block;\n    border-width: 1px;\n    border-style: solid;\n    border-color: initial;\n    border-image: initial;\n    padding: 5px 11px 2px;\n    margin: 0 5px 0 0;\n  }\n\n  .dc .thesaurus_synonyms .firstSyn > a {\n    text-decoration: none;\n  }\n\n  .dc .ref.type-thesaurus,\n  .dc .content-box-synonyms .thesaurus-link-plus,\n  .dc .link-right.verbtable,\n  .dc .extra-link,\n  .dc .content-box-examples .button,\n  .verbtable_content .headword_link {\n    background: rgba(218, 218, 218, 0.5);\n    display: inline-block;\n    padding: 2px 10px;\n    border: 0;\n    text-decoration: none;\n    display: inline-block;\n    font-weight: bold;\n    font-size: 0.9em;\n    margin-top: 2px;\n  }\n\n  .dc .h2_entry .dictname,\n  .dc .h2_entry .lbl.type-misc {\n    font-size: 16px;\n  }\n\n  .dc .translation .lang_EN-GB {\n    margin-bottom: 10px;\n  }\n\n  .dc .translation .def {\n    font-weight: bold;\n    font-size: inherit;\n  }\n\n  .dc .translation .example {\n    display: block;\n    font-style: italic;\n    margin-bottom: 1em;\n  }\n\n  .dc .translation_list {\n    margin: 1em 0;\n  }\n\n  .dc .translation_list .gramGrp {\n    text-transform: lowercase;\n  }\n\n  .dc .audio_play_button,\n  .audio_play_button {\n    color: #ec2615;\n    vertical-align: middle;\n    -webkit-transition: transform 0.2s, text-shadow 0.2s;\n    transition: transform 0.2s, text-shadow 0.2s;\n    border: 0;\n  }\n\n  .dc .h1_entry {\n    font-size: 1.8em;\n    line-height: 1.75em;\n  }\n\n  .dc .entry_title {\n    font-size: 1.5em;\n    line-height: 1.4em;\n    text-align: left;\n    color: #4d4e51;\n    font-weight: bold;\n  }\n\n  .dc .gotodict {\n    float: right;\n    line-height: 3.9em;\n  }\n\n  .dc .h2_entry .homnum {\n    background-color: #1c4b8b;\n    color: #fff;\n    font-size: 12px;\n    font-weight: bold;\n    padding: 2px 5px;\n    vertical-align: super;\n  }\n\n  .context-english-thesaurus .quote {\n    display: block;\n    margin-top: 10px;\n  }\n\n  .dc .dictionary .quote {\n    color: var(--color-font-grey);\n  }\n\n  .dc .dictionary .cit.type-translation .quote {\n    font-weight: bold;\n  }\n\n  .dc .dictionary .cit.type-example .quote .emph {\n    color: var(--color-brand);\n  }\n\n  .context-english-thesaurus .scbold br {\n    display: none;\n  }\n\n  .context-english-thesaurus .dc .sense_list .scbold {\n    border-bottom: 0;\n  }\n\n  .context-english-thesaurus .scbold {\n    display: block;\n    font-style: italic;\n    font-family: 'Times New Roman', Times, serif;\n    border-bottom: 0;\n    margin-top: 15px;\n    margin-bottom: 5px;\n    font-weight: bold;\n  }\n\n  .dc .sup {\n    vertical-align: super;\n    font-size: smaller;\n  }\n\n  .dc .content-box:after,\n  .dc .dictionary .content:after {\n    content: '';\n    clear: both;\n    display: table;\n  }\n\n  .dc .cit.type-quotation .quote,\n  .dc .content-box-quotations .quote,\n  .dc .content-box-examples .quote,\n  .dc .content-box-thesaurus .quote {\n    display: block;\n    margin-top: 1em;\n  }\n\n  .dc .cit.type-quotation .author,\n  .dc .content-box-quotations .author,\n  .dc .content-box-examples .author,\n  .dc .content-box-thesaurus .author {\n    font-weight: bold;\n    font-style: italic;\n    font-size: 0.8em;\n  }\n\n  .dc .cit.type-quotation .title,\n  .dc .content-box-quotations .title,\n  .dc .content-box-examples .title,\n  .dc .content-box-thesaurus .title {\n    display: inline;\n    font-variant: small-caps;\n    font-style: italic;\n    font-size: 0.8em;\n  }\n\n  .dc .cit.type-quotation .year,\n  .dc .content-box-quotations .year,\n  .dc .content-box-examples .year,\n  .dc .content-box-thesaurus .year {\n    font-size: 0.8em;\n    font-style: italic;\n  }\n\n  .dc .content-box-syn-of-syns div.type-syn_of_syn_head .orth,\n  .dc .thesbase .key,\n  .context-dataset-english-thesaurus .author,\n  .dc .rend-b,\n  .headwordSense {\n    color: var(--color-brand);\n  }\n\n  .dc .minimalunit {\n    font-weight: bold;\n  }\n\n  .dc .image {\n    background: #fafafa;\n    border: solid 1px #eee;\n    display: inline-block;\n  }\n\n  .dc .image .imageImg {\n    max-width: 100%;\n    max-height: 250px;\n    vertical-align: middle;\n  }\n\n  .dc .image .imageDescription {\n    font-style: italic;\n    font-size: 0.8em;\n    padding: 0 5px;\n  }\n\n  .dc .example-info i {\n    color: red;\n    font-size: 21px;\n    vertical-align: text-top;\n    border-bottom: 0;\n  }\n\n  .dc .example-info {\n    font-style: italic;\n    font-size: 0.9em;\n  }\n\n  .page {\n    font-size: 1em;\n  }\n\n  .page .dictname {\n    font-size: 0.7em;\n  }\n\n  .page .dictionary .copyright .i {\n    color: gray;\n  }\n\n  .page .copyright {\n    text-align: center;\n    color: gray;\n    font-size: small;\n    margin-top: 10px;\n  }\n\n  .page .metadata {\n    display: none;\n  }\n\n  .page .infls,\n  .page .description,\n  .page .title,\n  .page .url,\n  .page .summary,\n  .page .og,\n  .page .infls,\n  .page .description,\n  .page .title,\n  .page .url,\n  .page .summary,\n  .page .og {\n    display: block;\n  }\n\n  .page .assetref {\n    display: block;\n  }\n\n  .page .assettype {\n    font-weight: bold;\n    color: blue;\n  }\n\n  .page .dictentry,\n  .page .colloList {\n    margin-bottom: 20px;\n  }\n\n  .page .assets_intro,\n  .page .asset_intro {\n    color: green;\n    display: block;\n    font-weight: bold;\n    font-variant: small-caps;\n  }\n\n  .page .dictionary .re .hom {\n    display: inline;\n  }\n\n  .page .dictionary .re {\n    display: block;\n  }\n\n  .page .cobuild .hom {\n    display: block;\n    margin-left: 1.5em;\n    margin-bottom: 1em;\n  }\n\n  .page .cobuild .sense {\n    margin-left: 0;\n    margin-bottom: 0;\n    margin-top: 0.25em;\n  }\n\n  .page .dictionary .sense {\n    display: block;\n    margin-left: 1.5em;\n    margin-bottom: 0.5em;\n    margin-top: 0.5em;\n  }\n\n  .page .dictionary .sense.inline {\n    display: inline;\n    margin-left: 0;\n  }\n\n  .page .dictionary .inline {\n    display: inline;\n  }\n\n  .page .dictionary .newline {\n    display: block;\n  }\n\n  .page .cobuild br {\n    display: none;\n  }\n\n  .page .dictionary .subc,\n  .page .dictionary .colloc {\n    font-style: italic;\n    font-weight: normal;\n  }\n\n  .page .dictionary .re .pos {\n    font-style: italic;\n    color: black;\n  }\n\n  .page .dictionary .b,\n  .page .dictionary .form.type-infl .orth,\n  .page .dictionary .form.type-drv .orth,\n  .page .dictionary .hi.rend-b,\n  .page .dictionary .emph {\n    font-weight: bold;\n  }\n\n  .page .dictionary .form.type-inflected {\n    display: none;\n  }\n\n  .page .dictionary .hi.rend-b,\n  .page .dictionary .emph {\n    font-weight: bold;\n  }\n\n  .page .dictionary .hi.rend-sc {\n    font-variant: small-caps;\n  }\n\n  .page .dictionary .hi.rend-u {\n    text-decoration: underline;\n    font-size: inherit;\n  }\n\n  .page .dictionary .hi.rend-r {\n    font-weight: normal;\n    font-style: normal;\n  }\n\n  .page .dictionary .sup,\n  .page .dictionary .hi.rend-sup {\n    vertical-align: super;\n    font-size: smaller;\n  }\n\n  .page .dictionary .sub,\n  .page .dictionary .hi.rend-sub {\n    vertical-align: sub;\n    font-size: smaller;\n  }\n\n  .page .dictionary .hi.rend-i {\n    font-style: italic;\n  }\n\n  .page .dictionary .i {\n    font-weight: normal;\n    font-style: italic;\n    color: black;\n  }\n\n  .page .dictionary .note {\n    color: black;\n    line-height: 1.4em;\n    font-style: normal;\n    background-color: #e9eef4;\n    margin: 6px 0;\n    padding: 6px 4px 6px 18px;\n    font-weight: normal;\n    display: block;\n  }\n\n  .page .dictionary .posp {\n    font-size: 80%;\n    text-transform: uppercase;\n  }\n\n  .page .dictionary .r {\n    font-style: normal;\n  }\n\n  .page .dictionary .u {\n    text-decoration: underline;\n  }\n\n  .page .dictionary .block {\n    display: block;\n    margin-top: 3px;\n  }\n\n  .page .hin .block {\n    display: block;\n    margin-top: 15px;\n    margin-bottom: 7.5px;\n  }\n\n  .page .dictionary .bolditalic {\n    font-weight: bold;\n    font-style: italic;\n  }\n\n  .page span.bluebold {\n    font-weight: bold;\n    color: #1c4b8b;\n  }\n\n  .page span.italics,\n  .page span.ital {\n    font-style: italic;\n  }\n\n  .page span.sensenum {\n    margin-left: -1.3em;\n    float: left;\n    font-weight: bold;\n    font-size: 1.1em;\n  }\n\n  .page .dictionary .cit.type-translation .quote {\n    font-style: normal;\n    color: var(--color-brand);\n  }\n\n  .page span.bold,\n  .page .dictionary .cit.type-translation .pos,\n  .page .dictionary .var {\n    font-style: bold;\n  }\n\n  .dc a,\n  .openerBig {\n    cursor: pointer;\n    color: inherit;\n    text-decoration: none;\n  }\n\n  label.openerBig {\n    display: inline;\n  }\n\n  .page .dictionary a:hover {\n    color: #f9690e;\n  }\n\n  .page .dictionary .power {\n    float: right;\n  }\n\n  .page .dictionary .power .i {\n    color: #1c4b8b;\n    font-size: inherit;\n  }\n\n  .page .dictionary .hom_subsec {\n    display: block;\n  }\n\n  .page .dictionary .definitions,\n  .page .dictionary .derivs,\n  .page .dictionary .etyms {\n    margin-bottom: 1em;\n  }\n\n  .page .dictionary .inflected_forms {\n    display: block;\n    padding-bottom: 1.25em;\n  }\n\n  .page .dictionary .scbold {\n    font-weight: bold;\n    text-transform: uppercase;\n    font-size: 0.8em;\n  }\n\n  .page .dictionary .note .scbold {\n    display: block;\n  }\n\n  .page .dictionary .pron .ptr {\n    color: red;\n  }\n\n  .page .dictionary .list,\n  .page .dictionary .relwordgrp {\n    display: block;\n    margin-left: 20px;\n  }\n\n  .page .dictionary .listitem,\n  .page .dictionary .relwordunit {\n    display: list-item;\n  }\n\n  .page .dictionary .type-syngrp,\n  .page .dictionary .type-antgrp {\n    display: block;\n  }\n\n  .page .asset.Corpus_Examples_EN .quote {\n    font-style: italic;\n  }\n\n  .page .cobuild .sense {\n    margin-left: 0;\n  }\n\n  .page .cit.type-example .content {\n    background-color: white;\n    margin-bottom: 20px;\n    padding: 20px;\n  }\n\n  .page .cit.type-example .author {\n    font-weight: bold;\n    font-style: italic;\n  }\n\n  .page .cit.type-example .title {\n    display: inline;\n    font-variant: small-caps;\n    font-style: italic;\n  }\n\n  .page .cit.type-example .ref.type-def {\n    text-decoration: none;\n    color: inherit;\n  }\n\n  .page .biling .lbl {\n    font-style: italic;\n    font-variant: initial;\n    font-weight: initial;\n  }\n\n  .page .biling .lbl.type-subj {\n    font-variant: small-caps;\n  }\n\n  .page .biling .lbl.type-tm {\n    font-style: normal;\n  }\n\n  .page .biling .lbl.type-tm_hw {\n    font-size: 0.78em;\n  }\n\n  .page .biling .lbl.type-infl span,\n  .page .biling .lbl.type-infl {\n    font-style: normal;\n    color: #1c4b8b;\n    font-weight: normal;\n  }\n\n  .page .biling br {\n    display: none;\n  }\n\n  .page .biling .phrasals .re .orth {\n    font-size: 1.25em;\n  }\n\n  .page .biling .sense .re {\n    font-size: 100%;\n    margin-left: 0;\n  }\n\n  .page .hin .form.type-syn .orth,\n  .page .hin .form.type-ant .orth,\n  .page .hin .form.type-phr .orth {\n    font-weight: normal;\n    font-size: 100%;\n  }\n\n  .page .biling .re {\n    display: block;\n    margin-left: 1em;\n  }\n\n  .page .thesbase .synunit .cit {\n    display: inline;\n  }\n\n  .page .thesbase .xr.type-theslink {\n    display: inline-block;\n    margin-left: 20px;\n  }\n\n  .page .thesbase .relwgrp {\n    display: block;\n    margin-left: 1em;\n  }\n\n  .page .thesbase .caption {\n    display: block;\n    font-weight: bold;\n    margin-top: 10px;\n    font-size: larger;\n  }\n\n  .page .thesbase .table,\n  .page .thesbase .bibl,\n  .page .thesbase .cit.type-proverb {\n    display: block;\n  }\n\n  .page .thesbase .bibl .title {\n    display: inline;\n  }\n\n  .page .thesbase .tr {\n    display: table-row;\n  }\n\n  .page .thesbase .td {\n    display: table-cell;\n    padding: 3px;\n  }\n\n  .page .thesbase .th {\n    display: table-cell;\n    font-weight: bold;\n  }\n\n  .page .thesbase .cit.type-example .quote {\n    padding-left: 10px;\n  }\n\n  .page .thesbase .note {\n    background-color: transparent;\n    padding: 0;\n    margin-top: 10px;\n    overflow: hidden;\n  }\n\n  .page .thesbase .note .tr {\n    display: block;\n    margin-bottom: 20px;\n  }\n\n  .page .thesbase .tr .td:first-child {\n    background-color: #e9eef4;\n    font-weight: bold;\n    color: #1c4b8b;\n    padding: 5px 15px;\n  }\n\n  .page .thesbase .note .td {\n    padding: 8px 15px;\n    display: block;\n  }\n\n  .page .thesbase .note .th {\n    display: none;\n  }\n\n  .page .thesbase .link {\n    text-decoration: underline;\n    font-family: 'Open Sans', sans-serif;\n    background: #e5ebf3;\n    color: #1c4b8b;\n    padding: 0.3em 0.8em;\n    margin: 5px 0;\n    display: inline-block;\n  }\n\n  .page .thesbase .sense {\n    margin-bottom: 2em;\n  }\n\n  .page .thesbase .author {\n    font-weight: bold;\n    font-style: italic;\n  }\n\n  .cdet .content-box-origin {\n    padding-top: 0;\n    padding-bottom: 0;\n  }\n\n  .page .thesbase .sensehead > .sensenum {\n    float: none;\n  }\n\n  .page .thesbase .scbold {\n    background: #efefef;\n    padding: 0.5em 22px;\n    margin: 2em 0 1em 0;\n    font-weight: bold;\n    font-size: 80%;\n    text-transform: uppercase;\n    display: block;\n  }\n\n  .page .content-box-syn-of-syns div.type-syn_of_syn_head {\n    display: inline-block;\n  }\n\n  .page .content-box-syn-of-syns div.type-syn_of_syn_head .orth,\n  .page .thesbase .key {\n    font-weight: bold;\n    margin-right: 0;\n    display: inline-block;\n    margin-left: 0;\n    padding: 0.3em 0.3em;\n    border: 0;\n    font-size: 1.1em;\n    padding-left: 0;\n    padding-bottom: 0;\n  }\n\n  .headwordSense {\n    font-weight: bold;\n    display: inline-block;\n    font-size: 1.1em;\n    cursor: pointer;\n  }\n\n  .page .thesbase .key {\n    padding-right: 0;\n  }\n\n  .page .thesbase h2.first-sense {\n    display: inline;\n    font-size: inherit;\n  }\n\n  .thesbase ol.square {\n    margin-left: 1.6em;\n  }\n\n  .page .thesbase .firstSyn {\n    color: black;\n    font-size: 0.9em;\n  }\n\n  .page .content-box-syn-of-syns .syns_head {\n    margin-top: 2.2em;\n  }\n\n  .page .content-box-syn-of-syns .syns_example {\n    line-height: 2.5em;\n  }\n\n  .page .type-ant.columns3,\n  .page .content-box-syn-of-syns .columns3 {\n    -webkit-column-count: 3;\n    -moz-column-count: 3;\n    column-count: 3;\n  }\n\n  .page .content-box-syn-of-syns .syns_items {\n    display: block;\n  }\n\n  .pagination {\n    text-align: center;\n    margin: 1em;\n  }\n\n  .pagination a.prev,\n  .pagination a.next,\n  .pagination a.page,\n  .pagination span.page,\n  .pagination p,\n  .pagination p a {\n    padding: 0.3em 0.8em;\n    display: inline-block;\n    text-decoration: none;\n    font-weight: bold;\n    border: 0;\n  }\n\n  .pagination a.prev,\n  .pagination a.next,\n  .pagination a.page {\n    background: #e5ebf3;\n    color: #194885;\n  }\n\n  .pagination span.page,\n  .pagination p,\n  .pagination p a {\n    background: #194885;\n    color: #e5ebf3;\n  }\n\n  .page .content-box-syn-of-syns .lbl,\n  .page .content-box-syn-of-syns .lbl span,\n  .page .dictionary.thesbase .lbl,\n  .page .dictionary.thesbase .lbl span {\n    font-style: italic;\n    color: green;\n  }\n\n  .page .dictionary.thesbase .sensebody {\n    display: block;\n    margin: 0.5em 0 0.5em 6px;\n  }\n\n  .page .thesbase span.bold {\n    font-weight: bold;\n  }\n\n  .page .thesbase span.kerntouch {\n    letter-spacing: -0.18em;\n  }\n\n  .page .thesbase span.kern60 {\n    letter-spacing: -0.6em;\n  }\n\n  .page .thesbase span.manualdiacritic {\n    vertical-align: 25%;\n    letter-spacing: -1em;\n  }\n\n  .page .thesbase span.numerator {\n    vertical-align: 35%;\n    font-size: smaller;\n  }\n\n  .page .thesbase span.numerator_back {\n    position: absolute;\n    vertical-align: 35%;\n    letter-spacing: -1em;\n    font-size: smaller;\n  }\n\n  .page .thesbase span.denominator {\n    vertical-align: -35%;\n    font-size: smaller;\n  }\n\n  .page .thesbase span.italics {\n    font-weight: normal;\n    font-style: italic;\n    color: black;\n  }\n\n  .page .thesbase span.homnum {\n    font-weight: bold;\n    color: #fff;\n    vertical-align: super;\n    font-size: 50%;\n  }\n\n  .page .thesbase span.sensenum {\n    font-weight: bold;\n    display: inline-block;\n    min-width: 1em;\n  }\n\n  .cdet .toc-group a {\n    color: var(--color-brand);\n    border-bottom: dashed 1px var(--color-brand);\n  }\n\n  .cdet .toc {\n    text-align: left;\n    padding: 0 1em;\n    margin-top: 1em;\n  }\n\n  .cdet .toc-group .pos {\n    font-style: italic;\n  }\n\n  .cdet .toc-group .orth {\n    color: var(--color-brand);\n  }\n\n  .page .thesbase span.QA {\n    font-style: italic;\n    color: red;\n    font-size: 90%;\n  }\n\n  .page .thesbase hr {\n    width: 50%;\n    text-align: left;\n    border: 3px inset #555;\n    height: 6px;\n    margin: 10px auto 5px 0;\n  }\n\n  .page .thesbase .cit.type-quotation {\n    display: block;\n  }\n\n  .page .thesbase .cit.type-quotation > .quote,\n  .page .thesbase .cit.type-proverb > .quote,\n  .page .thesbase .cit.type-quotation > .bibl {\n    display: block;\n    margin-left: 1em;\n    padding-left: 0;\n  }\n\n  .page .thesbase > .re.type-phr .xr {\n    font-weight: bold;\n  }\n\n  .page .thesbase .div .xr {\n    display: block;\n    margin-left: 1em;\n  }\n\n  .cdet .content-box {\n    padding: 0;\n    border-left: none;\n  }\n\n  .cdet .toggleExample {\n    position: absolute;\n    right: 0;\n    top: -1em;\n    padding: 0.2em;\n    padding-right: 1em;\n    background-color: rgba(164, 189, 212, 0.53);\n  }\n\n  .cdet .blockSyn {\n    position: relative;\n  }\n\n  .cdet div[data-type-block] .sense .sensenum {\n    margin-left: 0;\n  }\n\n  .cdet .page .dictionary .sense,\n  .cdet .sense.moreSyn {\n    margin-left: 0;\n    margin-bottom: 0.5em;\n    padding-bottom: 0.5em;\n    position: relative;\n  }\n\n  .cdet .sense .sensehead .xr {\n    display: none;\n  }\n\n  .cdet .h1Word {\n    color: #e9573f;\n  }\n\n  .cdet .content-box-syn-of-syns .syns_container {\n    padding-left: 1.9em;\n  }\n\n  .cdet .dictionary.thesbase .sensebody,\n  .cdet div[data-type-block] .sense .sensebody {\n    margin: 0;\n    display: block;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    word-wrap: normal;\n    padding-right: 1.9em;\n    padding-left: 1.9em;\n    line-height: 1.5em;\n    font-size: 0.9em;\n  }\n\n  .cdet .content-box-syn-of-syns div[data-type-block] .sense .def,\n  .cdet .content-box-syn-of-syns div[data-type-block] .sense .syns_example {\n    margin: 0;\n    display: block;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    word-wrap: normal;\n    line-height: 1.5em;\n    font-size: 0.9em;\n    padding-right: 0;\n    padding-left: 0;\n  }\n\n  .cdet .sensehead > .sensenum {\n    min-width: 1.9em;\n    display: inline-block;\n    text-align: center;\n    font-size: 0.9em;\n    color: #4d4e51;\n  }\n\n  .cdet .dictionary .sense.opened,\n  .cdet div[data-type-block] .sense.opened {\n    margin-left: 0;\n    margin-bottom: 1em;\n    padding-bottom: 0.5em;\n    cursor: auto;\n    position: relative;\n  }\n\n  .cdet .dictionary.thesbase .sense.opened .sensebody,\n  .cdet div[data-type-block] .sense.opened .sensebody {\n    overflow: auto;\n    text-overflow: inherit;\n    white-space: inherit;\n  }\n\n  .cdet .toggleExample,\n  .cdet .iconContainer {\n    display: none;\n  }\n\n  .cdet .iconContainer {\n    position: absolute;\n    right: 0;\n  }\n\n  .cdet .sense.opened .iconContainer {\n    display: block;\n  }\n\n  .cdet .syns_container {\n    padding-left: 0;\n  }\n\n  .cdet .content-box-syn-of-syns .sense.moreSyn {\n    margin-bottom: 0.5em;\n  }\n\n  .cdet .form.type-syn .orth,\n  .cdet .type-ant .orth,\n  .cdet .syns_container .form.type-syn .orth {\n    background-color: transparent;\n    border: 0;\n    font-size: 0.97em;\n    font-weight: bold;\n    text-decoration: none;\n    color: #4d4e51;\n    margin: 0;\n    display: inline-block;\n  }\n\n  .cdet .dictionary .content,\n  .cdet .form.type-syn {\n    position: relative;\n  }\n\n  .cdet .titleTypeContainer {\n    font-size: 0.9em;\n  }\n\n  .cdet .titleTypeContainer .titleType {\n    font-size: 1.1em;\n    color: #4d4e51;\n    display: inline-block;\n    margin-top: 0.5em;\n    font-weight: bold;\n  }\n\n  body.context-language-THESAURUS {\n    background-color: white;\n  }\n\n  .cdet .sense .form *[class*='type'] {\n    font-size: 0.9em;\n  }\n\n  .cdet .titleTypeSubContainer {\n    margin-top: 0.5em;\n    font-weight: bold;\n    font-size: 0.9em;\n  }\n\n  .cdet .form.type-syn .titleTypeSubContainer {\n    display: block;\n  }\n\n  .cdet .sense.moreAnt .type-ant div,\n  .cdet .form.type-syn .titleTypeSubContainer,\n  .cdet .form.type-ant .titleTypeSubContainer,\n  .cdet .form.type-ant,\n  .cdet .blockAnt,\n  .cdet .blockSyn,\n  .cdet .blockAnt div {\n    display: inline;\n  }\n\n  .entry.dictionary.thesbase.content-box.content-box-thesbase {\n    position: relative;\n  }\n\n  .cdet .form.type-syn .titleTypeSubContainer:after,\n  .cdet .form.type-ant .titleTypeSubContainer:after {\n    content: ': ';\n    display: inline;\n  }\n\n  .cdet .titleTypeSubContainer .titleType {\n    display: inline;\n    padding: 0;\n    font-size: 1.3em;\n    font-variant: all-small-caps;\n  }\n\n  .cdet .blockSyn {\n    margin-bottom: 1em;\n  }\n\n  .headerSensePos {\n    font-size: 0.9em;\n    color: #888;\n    font-style: italic;\n  }\n\n  .blockAnt .type-ant > div:before {\n    content: ', ';\n    display: inline;\n  }\n\n  .cdet .blockAnt .type-ant > div:first-child:before {\n    content: '';\n    display: inline;\n  }\n\n  .cdet .type-ant .orth {\n    font-weight: normal;\n  }\n\n  .cdet .type-ant,\n  .cdet .content-box-syn-of-syns .syns_container {\n    padding-left: 0;\n    margin-left: 0;\n  }\n\n  .cdet .content-box-comments {\n    background-color: white;\n    margin-bottom: 20px;\n    padding: 20px;\n    position: relative;\n  }\n\n  .cdet .content-box-comments,\n  .cdet .content-box-origin,\n  .cdet .content-box-nearby-words,\n  .cdet .dictionary .content,\n  .cdet .content-box-syn-of-syns {\n    border-left: none;\n    box-shadow: none;\n  }\n\n  .cdet .re.type-phr .xr,\n  .cdet .content-box-nearby-words li {\n    margin-left: 0;\n    padding-left: 0.85em;\n    margin-bottom: 0.3em;\n    padding-bottom: 0.3em;\n    display: block;\n  }\n\n  .cdet .content-box-syn-of-syns div.type-syn_of_syn_head {\n    display: block;\n  }\n\n  .cdet .content-box-syn-of-syns .sense .def {\n    padding-left: 1em;\n  }\n\n  .cdet .content-box-syn-of-syns .sense .def,\n  .cdet .content-box-syn-of-syns .sense .syns_example {\n    margin: 0;\n    display: block;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    word-wrap: normal;\n    line-height: 1.5em;\n    font-size: 0.9em;\n    color: #4d4e51;\n  }\n\n  .cdet .content-box-syn-of-syns .quote {\n    font-style: italic;\n  }\n\n  .cdet div[data-type-block] .sense.opened .sensebody,\n  .cdet .content-box-syn-of-syns .sense.opened .def,\n  .cdet .content-box-syn-of-syns .sense.opened .syns_example {\n    overflow: auto;\n    white-space: normal;\n  }\n\n  .cdet .content-box-syn-of-syns .syns_head {\n    margin-top: 0;\n  }\n\n  .cdet .cit.type-quotation > .bibl {\n    font-size: 0.85em;\n  }\n\n  .cdet .cit.type-quotation {\n    margin-left: 0;\n    margin-bottom: 0.3em;\n    padding-bottom: 0.3em;\n  }\n\n  .cdet .re.type-phr > .titleTypeContainer,\n  .cdet .cit.type-quotation > .titleTypeContainer {\n    margin-bottom: 1em;\n  }\n\n  .cdet .cit.type-quotation > .quote {\n    line-height: 1.3em;\n    margin-bottom: 0.3em;\n  }\n\n  .cdet .syns_example .cit.type-example {\n    overflow: inherit;\n    text-overflow: inherit;\n    white-space: inherit;\n  }\n\n  .cdet .cit.type-quotation .title,\n  .cdet .cit.type-quotation .author {\n    font-size: inherit;\n  }\n\n  .cdet span.sensenum {\n    margin-left: 0;\n  }\n\n  .cdet .dictionary .quote {\n    color: #4d4e51;\n    font-size: 0.9em;\n  }\n\n  .cdet span.quote:before {\n    content: '';\n    margin: 2px 6px;\n    display: inline-block;\n    background-color: black;\n    padding: 3px;\n  }\n\n  .cdet .headerSense .sensenum {\n    margin-left: -20px;\n  }\n\n  .cdet .synonymBlock,\n  .cdet .containerBlock {\n    margin-left: 20px;\n  }\n\n  .cdet .headerThes .entry_title + div a {\n    color: var(--color-brand);\n    font-size: 0.9em;\n    margin-bottom: 1em;\n    display: inline-block;\n  }\n\n  .cdet .form {\n    display: inline;\n  }\n\n  .cdet .cit.type-example,\n  .selectorOpernerBig:checked + .sense.opened .sep,\n  .selectorOpernerBig:checked + .sense.opened .openerBig,\n  .selectorOpernerBig:checked ~ label[for='default'],\n  .selectorOpernerBig {\n    display: none;\n  }\n\n  .selectorOpernerBig + .shadow_layer {\n    position: fixed;\n    width: 100%;\n    height: 100%;\n    top: 0;\n    left: 0;\n    background-color: rgba(128, 128, 128, 0.78);\n    z-index: 2;\n  }\n\n  .selectorOpernerBig:checked + .sense.opened .cit.type-example,\n  .selectorOpernerBig:checked + .sense.opened div > .form {\n    display: block;\n  }\n\n  .selectorOpernerBig:checked + .sense.opened:before {\n    content: '';\n    display: block;\n    height: 2em;\n    position: fixed;\n    top: 0;\n    z-index: 1;\n    background-color: white;\n    width: 70%;\n    left: 15%;\n  }\n\n  .selectorOpernerBig:checked + .sense.sense.opened {\n    position: fixed;\n    top: 0;\n    left: 15%;\n    height: calc(100% - 2em);\n    width: 70%;\n    background-color: white;\n    z-index: 3;\n    margin: 0;\n    padding: 1em;\n    overflow-y: auto;\n    margin-top: 2em;\n  }\n\n  .selectorOpernerBig ~ label.menuPanelCloseButton {\n    position: fixed;\n    display: block;\n    top: 0;\n    border-bottom: 0;\n    z-index: 4;\n    right: calc(15% - -10px);\n  }\n\n  .he .grammar .page {\n    display: block;\n    border: solid 1px;\n    font-family: arial, helvetica, sans-serif;\n    margin-bottom: 20px;\n    padding: 15px;\n    padding-bottom: 40px;\n  }\n\n  .he .grammar a.previous,\n  .he .grammar a.next {\n    background: #e5ebf3;\n    color: #194885;\n    padding: 0.5em 1em;\n    font-weight: bolder;\n    border-bottom: 0;\n    float: left;\n    margin-top: 1em;\n  }\n\n  .he .grammar a.next {\n    float: right;\n  }\n\n  .he .grammar a.previous:hover,\n  .he .grammar a.next:hover {\n    color: #194885;\n  }\n\n  .he .grammar a.previous i,\n  .he .grammar a.next i {\n    font-size: 1.3em;\n    vertical-align: middle;\n    padding-right: 8px;\n    padding-left: 8px;\n    display: inline-block;\n  }\n\n  .he .grammar .exmplblk ul {\n    padding-left: 0;\n  }\n\n  .he .grammar .exmplblk {\n    padding: 0.5em;\n  }\n\n  .he .grammar .exmplgrp ul {\n    padding-left: 20px;\n    padding-bottom: 10px;\n  }\n\n  .he .grammar .intro.suppressed {\n    display: none;\n  }\n\n  .he .grammar h2 {\n    font-size: 16pt;\n    line-height: 2em;\n    text-decoration: underline;\n  }\n\n  .he .grammar h3 {\n    font-size: 14pt;\n  }\n\n  .he .grammar h4 {\n    font-size: 12pt;\n    font-weight: bold;\n    margin-bottom: 1em;\n  }\n\n  .he .grammar u {\n    text-decoration: underline;\n  }\n\n  .he .grammar .lemma {\n    font-weight: bold;\n  }\n\n  .he .grammar .caption {\n    font-weight: bold;\n    margin-top: 1.5em;\n  }\n\n  .he .grammar .p {\n    display: block;\n    margin-top: 5px;\n    margin-bottom: 5px;\n  }\n\n  .he .grammar .group {\n    display: block;\n    margin-top: 2em;\n    margin-bottom: 2em;\n  }\n\n  .he .grammar .exmpl {\n    font-weight: normal;\n    font-style: italic;\n  }\n\n  .he .grammar .i,\n  .he .grammar .post {\n    font-style: italic;\n  }\n\n  .he .grammar .posp {\n    font-weight: bold;\n    font-style: normal;\n  }\n\n  .he .grammar .pattern {\n    font-family: sans-serif;\n  }\n\n  .he .grammar .ul {\n    margin-top: 5px;\n    list-style-type: none;\n    padding-left: 15px;\n  }\n\n  .he .grammar ul.arrow {\n    list-style-type: square;\n  }\n\n  .he .grammar ul.star {\n    list-style-type: disc;\n  }\n\n  .he .grammar ul.alpha {\n    list-style-type: lower-alpha;\n  }\n\n  .he .grammar ol {\n    margin-top: 5px;\n    list-style-type: decimal;\n  }\n\n  .he .grammar .li.exmpl {\n    font-style: italic;\n  }\n\n  .he .grammar .lemmalist .li {\n    margin-top: 10px;\n  }\n\n  .he .grammar .lemmalist {\n    border-color: #ccc;\n    border-style: solid;\n    border-width: 1px;\n    margin: 4px;\n    margin-top: 2em;\n    padding: 1em;\n    -webkit-column-count: 4;\n    -moz-column-count: 4;\n    column-count: 4;\n  }\n\n  .he .grammar div.greyborder2 {\n    border-color: #ccc;\n    border-style: solid;\n    border-width: 1px;\n    margin: 4px;\n    -webkit-column-count: 4;\n    -moz-column-count: 4;\n    column-count: 4;\n  }\n\n  .he .grammar th,\n  .he .grammar td {\n    border-color: #000;\n    border-style: solid;\n    border-width: 1px;\n    padding: 0.5em 1.4em;\n  }\n\n  .he .grammar th {\n    background-color: #ddd;\n    font-weight: bold;\n    font-size: 0.9em;\n  }\n\n  .he .grammar table {\n    border-collapse: collapse;\n    border-color: #000;\n    border-style: solid;\n    border-width: 1px;\n    margin-top: 1.5em;\n    margin-bottom: 1em;\n  }\n\n  .linksTool + .linksTool {\n    margin-top: 0.5em;\n  }\n\n  .linksTool {\n    margin: 1.5em 1em 1.5em 1em;\n    font-style: italic;\n    font-size: 0.9em;\n  }\n\n  .he .grammar a.block {\n    display: block;\n  }\n\n  .he .grammar i.icon-chevron-thin-right {\n    display: inline-block;\n    font-weight: bold;\n    width: 2em;\n    text-align: center;\n    font-size: 0.6em;\n  }\n\n  .he .grammar .group a:before,\n  .he .grammar .section a:before,\n  .he .grammar .posGr a:before,\n  .he .grammar .subpattern a:before,\n  .he .grammar .pattern a:before,\n  .he .grammar .chapter a:before {\n    display: block;\n    content: '';\n  }\n\n  .entry_container {\n    color: inherit;\n    display: block;\n    background: #fff;\n    box-shadow: 0 0 3px rgba(0, 0, 0, 0.2);\n    text-decoration: none;\n    margin-bottom: 20px;\n    padding: 20px;\n  }\n\n  .he .grammar .breadcrumb {\n    margin-bottom: 2em;\n  }\n\n  .synonymBlock {\n    border: 1px solid transparent;\n  }\n\n  .synonymBlock:after {\n    content: '';\n    display: block;\n    clear: both;\n  }\n\n  .grammar .bold {\n    display: inline;\n    font-weight: bold;\n  }\n\n  .grammar .italic {\n    display: inline;\n    font-style: italic;\n  }\n\n  .grammar .bolditalic {\n    display: inline;\n    font-weight: bold;\n    font-style: italic;\n  }\n\n  .grammar .roman {\n    font-style: normal;\n  }\n\n  .grammar .color {\n    color: #0058a9;\n  }\n\n  .grammar .color1 {\n    color: #0058a9;\n    font-style: normal;\n  }\n\n  .grammar .chaptitle {\n    font-size: x-large;\n    font-weight: bold;\n    color: #0058a9;\n  }\n\n  .grammar .parttitle {\n    font-size: xx-large;\n    font-weight: bold;\n    color: #0058a9;\n  }\n\n  .grammar .head1 {\n    font-weight: bold;\n  }\n\n  .grammar .head {\n    font-size: medium;\n    font-weight: bold;\n    color: #0058a9;\n    margin-top: 2em;\n  }\n\n  .grammar .p {\n    font-size: medium;\n    font-style: normal;\n    text-indent: 0;\n  }\n\n  .grammar .ind {\n    font-size: medium;\n    font-weight: normal;\n    font-style: normal;\n    text-indent: 0;\n    margin-top: 0.3em;\n    margin-bottom: 0.3em;\n    margin-left: 0.8em;\n    text-indent: -0.8em;\n  }\n\n  .grammar .ul.cll0 {\n    padding-left: 0;\n    list-style-type: disc;\n  }\n\n  .grammar .ul.cll0 .li {\n    margin-left: 1em;\n  }\n\n  .grammar .ul.cll1 {\n    padding-left: 0;\n    list-style-type: none;\n  }\n\n  .grammar .ul.cll1 > .li:before {\n    content: '– ';\n  }\n\n  .grammar .ul.cll1 .li,\n  .grammar .ul.cll2 .li,\n  .grammar .ol.cll4 .li {\n    margin-left: 0.8em;\n    text-indent: -0.8em;\n  }\n\n  .grammar .ul.cll2a,\n  .grammar .ul.cll2 {\n    list-style-type: none;\n    margin-top: 1em;\n    margin-bottom: 1em;\n  }\n\n  .grammar .ul.cll2a {\n    padding-left: 0.75em;\n  }\n\n  .grammar .ul.cll2a .li {\n    margin-left: 0;\n    text-indent: -0.8em;\n  }\n\n  .grammar .ul.cll3 {\n    padding-left: 1.2em;\n    color: #0058a9;\n  }\n\n  .grammar .ul.cll3 > .li > span {\n    color: black;\n  }\n\n  .grammar .ol.cll4 {\n    list-style-type: none;\n    margin-top: 1em;\n    margin-bottom: 1em;\n    padding-left: 1.5em;\n  }\n\n  .grammar span.label {\n    width: 0.8em;\n    display: inline-block;\n    color: #0058a9;\n    font-weight: bold;\n  }\n\n  .grammar span.label1 {\n    width: 0.8em;\n    display: inline-block;\n  }\n\n  .grammar .block {\n    font-size: 0.83em;\n    font-style: italic;\n    font-weight: normal;\n    text-align: justify;\n    text-indent: 0;\n    margin: 0.3em 1.3em;\n  }\n\n  .grammar div.box {\n    border: 1px solid #0058a9;\n    margin-top: 2em;\n    margin-bottom: 2em;\n    padding: 0 2.5em;\n    background-color: #e1e4ee;\n    border-radius: 15px;\n  }\n\n  .grammar .toc {\n    margin-top: 0.25em;\n    margin-bottom: 0.25em;\n  }\n\n  .grammar .center {\n    text-align: center;\n  }\n\n  .grammar .right {\n    text-align: right;\n  }\n\n  .grammar .small {\n    font-size: 78%;\n  }\n\n  .grammar td {\n    vertical-align: top;\n  }\n\n  .grammar td.filet_b,\n  .grammar td.filet_t,\n  .grammar td.filet_l,\n  .grammar td.filet_r {\n    border-right: 1px solid black;\n  }\n\n  .grammar .tab1 {\n    margin-left: 5em;\n  }\n\n  .grammar .strike {\n    text-decoration: line-through;\n  }\n\n  .navigation .tab.current .ref > .pos {\n    color: white;\n  }\n\n  .cdet .ref > .pos {\n    font-style: italic;\n    font-size: 0.9em;\n    color: #777772;\n  }\n\n  .openTootip,\n  .openTootip ~ .def {\n    display: none;\n  }\n\n  .openTootip:checked ~ .def {\n    display: block;\n  }\n\n  .linkDef {\n    display: inline;\n  }\n\n  .selectorOpernerBig:checked + .sense.sense.opened .cit .quote:before {\n    display: none;\n  }\n\n  @media screen and (min-width: 321px) {\n    .cdet .page .dictionary .sense,\n    .cdet .sense.moreSyn,\n    .cdet .dictionary .hom,\n    .cdet .dictionary .syn_of_syns {\n      overflow: hidden;\n    }\n  }\n\n  @media screen and (max-width: 761px) {\n    .cdet .navigation .nav {\n      display: none;\n    }\n\n    .cdet .more {\n      margin: 10px auto 10px auto;\n      width: 50%;\n    }\n\n    .selectorOpernerBig:checked + .sense.sense.opened:before,\n    .selectorOpernerBig:checked + .sense.sense.opened {\n      width: 100%;\n      left: 0;\n    }\n\n    .selectorOpernerBig ~ label.menuPanelCloseButton {\n      right: 0;\n    }\n  }\n\n  .navigation {\n    position: relative;\n    width: 100%;\n  }\n\n  .navigation:before {\n    content: '\\a0';\n    position: absolute;\n    width: 100%;\n    height: 100%;\n    top: 0;\n    background-color: inherit;\n  }\n\n  .navigation a.tab,\n  .navigation span.tab > a {\n    font-size: 0.9em;\n    display: inline-block;\n    line-height: 26px;\n    padding: 4px 8px;\n    color: #333;\n    background-color: #e5ebf3;\n    margin: 6px 2px 10px 2px;\n    border: 0;\n    border-radius: 2px;\n  }\n\n  .navigation .expo {\n    position: relative;\n    top: -4px;\n    font-size: 0.8em;\n    margin-left: 2px;\n  }\n\n  .navigation[data-position='fixed'] {\n    position: fixed;\n    top: 50px;\n    z-index: 2;\n  }\n\n  .navigation a.nav:hover {\n    background-color: #1c4b8b;\n    color: white;\n  }\n\n  .navigation .tab .ref:active,\n  .navigation a.tab.current,\n  .navigation span.tab.current > a {\n    background-color: #1c4b8b;\n    color: white;\n    position: relative;\n  }\n\n  .cdet .ref:active > .pos {\n    color: white;\n  }\n\n  .navigation .tabsNavigation {\n    overflow: hidden;\n    white-space: nowrap;\n    position: relative;\n    word-wrap: normal;\n  }\n\n  .navigation .tabsNavigation i {\n    font-size: 0.85em;\n  }\n\n  .navigation .tab.current::before {\n    content: '';\n    display: inline-block;\n    position: absolute;\n    left: 50%;\n    left: 1.2em;\n    bottom: -5px;\n    width: 0;\n    height: 0;\n    border-style: solid;\n    border-width: 0 5px 5px 5px;\n    border-color: transparent transparent #1c4b8b transparent;\n    transform: rotate(180deg);\n    -webkit-transform: rotate(180deg);\n    -moz-transform: rotate(180deg);\n    -ms-transform: rotate(180deg);\n  }\n\n  .navigation a.nav {\n    background: #cdd4de;\n    position: absolute;\n    top: 0;\n    font-size: 20px;\n    overflow: hidden;\n    display: inline-block;\n    color: #4d4e51;\n    margin-left: 0;\n    margin-right: 0;\n    padding-left: 15px;\n    padding-right: 15px;\n  }\n\n  .navigation .left {\n    left: 0;\n  }\n\n  .navigation .right {\n    right: 0;\n  }\n\n  .cdet .navigation {\n    background-color: white;\n  }\n\n  .navigation .tab a {\n    border-bottom: 0;\n  }\n\n  .cdet .tab span + span {\n    display: inline-block;\n    padding-left: 0.3em;\n  }\n\n  .cdet .tab a {\n    display: inline-block;\n  }\n\n  .dc .frenquency-title {\n    float: right;\n  }\n\n  .dc .word-frequency-img {\n    margin-left: 0.5em;\n  }\n\n  .dc .word-frequency-container .level {\n    border-radius: 50%;\n    display: inline-block;\n  }\n\n  .dc .word-frequency-container .level.roundRed {\n    background-color: #f9690e;\n  }\n\n  .dc .word-frequency-container .level1 {\n    width: 14px;\n    height: 14px;\n  }\n\n  .dc .word-frequency-container .level2 {\n    width: 15px;\n    height: 15px;\n  }\n\n  .dc .word-frequency-container .level3 {\n    width: 16px;\n    height: 16px;\n  }\n\n  .dc .word-frequency-container .level4 {\n    width: 17px;\n    height: 17px;\n  }\n\n  .dc .word-frequency-container .level5 {\n    width: 18px;\n    height: 18px;\n  }\n\n  .dc .word-frequency-container .round {\n    width: 100%;\n    height: 100%;\n  }\n\n  .dc .word-frequency-container.relevance .level {\n    border-radius: 0;\n    width: 15px;\n    vertical-align: bottom;\n  }\n\n  .dc .word-frequency-container.relevance .level1 {\n    background: #f6b26b;\n    height: 10px;\n  }\n\n  .dc .word-frequency-container.relevance .level2 {\n    background: #f6b26b;\n    height: 13px;\n  }\n\n  .dc .word-frequency-container.relevance .level3 {\n    background: #ffd966;\n    height: 16px;\n  }\n\n  .dc .word-frequency-container.relevance .level4 {\n    background: #ffd966;\n    height: 19px;\n  }\n\n  .dc .word-frequency-container.relevance .level5 {\n    background: #b6d7a8;\n    height: 22px;\n  }\n\n  .dc .word-frequency-container.relevance .level6 {\n    background: #b6d7a8;\n    height: 25px;\n  }\n\n  .lightboxLink {\n    cursor: pointer;\n  }\n\n  .lightboxOverlay {\n    background: rgba(0, 0, 0, 0.8);\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    z-index: 10;\n    padding: 20px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    text-align: center;\n  }\n\n  .lightboxContainer {\n    display: inline-block;\n    position: relative;\n    padding-bottom: 30px;\n  }\n\n  .lightboxImage {\n    min-height: 200px;\n    min-width: 200px;\n    vertical-align: middle;\n    border: solid 5px #fff;\n    border-radius: 3px;\n    background: #fff;\n  }\n\n  .lightboxClose {\n    position: absolute;\n    right: 0;\n    bottom: 0;\n    color: #fff;\n    font-size: 2em;\n  }\n\n  .lightboxCopyright {\n    position: absolute;\n    bottom: 0;\n    color: #fff;\n    left: 0;\n  }\n\n  .tagline {\n    font-size: 15px;\n    margin-left: 4px;\n  }\n\n  .home_menu {\n    margin: 1em 0;\n    text-align: center;\n  }\n\n  .home_logo_link {\n    display: inline-block;\n  }\n\n  .home_logo {\n    width: 100%;\n    max-width: 320px;\n  }\n\n  .home_menu li {\n    display: inline-block;\n  }\n\n  .home_menu a.current,\n  .home_menu a:hover {\n    background: rgba(255, 255, 255, 0.2);\n  }\n\n  .home_menu a {\n    padding: 10px 25px;\n    color: inherit;\n    display: inline-block;\n    -webkit-transition: background-color 0.4s ease, color 0.4s ease;\n    -moz-transition: background-color 0.4s ease, color 0.4s ease;\n    -o-transition: background-color 0.4s ease, color 0.4s ease;\n    transition: background-color 0.4s ease, color 0.4s ease;\n  }\n\n  .home_search_container {\n    margin: 1em auto 0 auto;\n  }\n\n  .dataset-description {\n    font-size: 1em;\n    opacity: 0.8;\n    margin-top: 1em;\n    text-align: center;\n  }\n\n  h1 {\n    padding-top: 0.5em;\n    font-size: 2em;\n    text-align: center;\n  }\n\n  .search-desktop {\n    width: 900px;\n  }\n\n  .word-content li {\n    display: inline-block;\n  }\n\n  .blue {\n    background: #e8e8e8;\n    color: #000;\n  }\n\n  .blue .main-content {\n    padding: 0;\n  }\n\n  .main-content {\n    padding: 20px 0;\n    padding-top: 0;\n    margin: 0 auto;\n  }\n\n  .word-content {\n    text-align: center;\n  }\n\n  .word-content .home-link.current,\n  .word-content .home-link:hover {\n    background: #ddd;\n  }\n\n  .word-content .home-link.current {\n    font-weight: bold;\n  }\n\n  .word-content .home-link {\n    display: inline-block;\n    color: inherit;\n    padding: 1em 1.15em;\n    position: relative;\n    text-decoration: none;\n  }\n\n  .word-content .home-link.current::before {\n    content: '';\n    display: inline-block;\n    position: absolute;\n    left: 50%;\n    margin-left: -10px;\n    top: -10px;\n    width: 0;\n    height: 0;\n    border-style: solid;\n    border-width: 0 10px 10px 10px;\n    border-color: transparent transparent #e5ebf3 transparent;\n  }\n\n  .white {\n    padding-top: 20px;\n  }\n\n  @media screen and (max-width: 761px) {\n    header .main-content {\n      padding-top: 10px;\n    }\n\n    header .main-content {\n      padding-top: 0;\n      background-position: calc(100% - 10px) calc(10px);\n      background-size: 20%;\n      padding-bottom: 25px;\n    }\n\n    .search-desktop {\n      width: 100%;\n    }\n\n    .home_search_container {\n      margin: 0 10px;\n      display: block;\n    }\n\n    .word-content .home-link.current::before {\n      display: none;\n    }\n\n    .word-content .home-link {\n      padding: 0.5em;\n    }\n\n    .home_logo_link {\n      display: block;\n      text-align: center;\n      margin-top: 1em;\n      margin-bottom: 2em;\n    }\n\n    .home_logo {\n      max-width: none;\n      width: 80%;\n    }\n\n    .white {\n      padding-top: 10px;\n    }\n  }\n\n  @media screen and (min-width: 762px) and (max-width: 948px) {\n    .tagline {\n      font-size: 10px;\n      margin-left: 4px;\n    }\n\n    header .main-content {\n      background-position: calc(100% - 5px) calc(5px);\n      background-size: 25%;\n    }\n\n    .search-desktop {\n      width: 700px;\n      margin: 0 auto;\n    }\n\n    .home_logo {\n      max-width: 210px;\n    }\n  }\n\n  .columns .columns_item {\n    display: inline-block;\n    width: 24.5%;\n    padding-right: 20px;\n    vertical-align: top;\n    min-height: 360px;\n  }\n\n  @media screen and (max-width: 761px) {\n    .columns .columns_item {\n      display: block;\n      width: auto;\n      padding-right: 0;\n      min-height: 0;\n    }\n  }\n\n  @media screen and (min-width: 762px) {\n    .columns .columns_item {\n      width: 49.5%;\n    }\n  }\n\n  [class^='icon-'],\n  [class*=' icon-'] {\n    font-family: 'icomoon';\n    font-style: normal;\n    font-weight: normal;\n    font-variant: normal;\n    text-transform: none;\n    line-height: 1;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n  }\n\n  .icon-fw {\n    display: inline-block;\n    text-align: center;\n    width: 1.55em;\n  }\n\n  .icon-2x {\n    font-size: 2em;\n  }\n\n  .icon-chat:before {\n    content: '\\e900';\n  }\n\n  .icon-community:before {\n    content: '\\e901';\n  }\n\n  .icon-keyboard:before {\n    content: '\\e955';\n  }\n\n  .icon-search:before {\n    content: '\\f002';\n  }\n\n  .icon-user:before {\n    content: '\\f007';\n  }\n\n  .icon-times:before {\n    content: '\\f00d';\n  }\n\n  .icon-volume-up:before {\n    content: '\\f028';\n  }\n\n  .icon-twitter-square:before {\n    content: '\\f081';\n  }\n\n  .icon-twitter:before {\n    content: '\\f099';\n  }\n\n  .icon-facebook:before {\n    content: '\\f09a';\n  }\n\n  .icon-globe:before {\n    content: '\\f0ac';\n  }\n\n  .icon-bars:before {\n    content: '\\f0c9';\n  }\n\n  .icon-caret-down:before {\n    content: '\\f0d7';\n  }\n\n  .icon-caret-up:before {\n    content: '\\f0d8';\n  }\n\n  .icon-caret-left:before {\n    content: '\\f0d9';\n  }\n\n  .icon-caret-right:before {\n    content: '\\f0da';\n  }\n\n  .icon-copyright:before {\n    content: '\\f1f9';\n  }\n\n  .icon-facebook-official:before {\n    content: '\\f230';\n  }\n\n  .icon-warning:before {\n    content: '\\e907';\n  }\n\n  .icon-fiber_new:before {\n    content: '\\e05e';\n  }\n\n  .icon-trending_down:before {\n    content: '\\e8e3';\n  }\n\n  .icon-trending_flat:before {\n    content: '\\e8e4';\n  }\n\n  .icon-trending_up:before {\n    content: '\\e8e5';\n  }\n\n  .icon-chevron-thin-left:before {\n    content: '\\e905';\n  }\n\n  .icon-chevron-thin-right:before {\n    content: '\\e906';\n  }\n\n  .icon-books:before {\n    content: '\\e920';\n  }\n\n  .icon-eye-plus:before {\n    content: '\\e9cf';\n  }\n\n  .icon-eye-minus:before {\n    content: '\\e9d0';\n  }\n\n  .icon-share2:before {\n    content: '\\ea82';\n  }\n\n  .icon-read:before {\n    content: '\\e902';\n  }\n\n  .icon-copy:before {\n    content: '\\e908';\n  }\n\n  .icon-exchange:before {\n    content: '\\e909';\n  }\n\n  .icon-sort:before {\n    content: '\\f0dc';\n  }\n\n  [class*='res_cell'] {\n    float: left;\n    display: block;\n    width: 100%;\n  }\n\n  .res_cell_left {\n    width: 160px;\n    min-height: 1px;\n  }\n\n  .res_cell_right {\n    width: 300px;\n  }\n\n  .res_cell_2_3 {\n    width: 66%;\n  }\n\n  .res_cell_2_3_content {\n    padding: 0 20px 0 0;\n  }\n\n  .res_cell_1_3 {\n    width: 34%;\n  }\n\n  .mpuslot_b-container {\n    min-width: 320px;\n    text-align: center;\n    padding: 0;\n  }\n\n  @media screen and (max-width: 320px) {\n    .mpuslot_b-container {\n      margin: 0 0 0 -38px;\n    }\n\n    .cdet .mpuslot_b-container {\n      margin: 0 0 0 -10px;\n    }\n\n    .content-box-examples .mpuslot_b-container {\n      margin: 0 0 0 -14px;\n    }\n\n    .mpuslot_b-container-amp {\n      margin: 0 0 0 -26px;\n    }\n\n    .thesbase .mpuslot_b-container-amp {\n      margin: 0;\n    }\n\n    .content-box-examples .mpuslot_b-container-amp {\n      margin: 0 0 0 -4px;\n    }\n  }\n\n  @media screen and (min-width: 321px) and (max-width: 761px) {\n    .mpuslot_b-container {\n      margin: 0 0 0 -24px;\n    }\n\n    .cdet .mpuslot_b-container {\n      margin: 0;\n    }\n\n    .content-box-examples .mpuslot_b-container {\n      margin: 0 0 0 -14px;\n    }\n  }\n\n  @media screen and (max-width: 761px) {\n    main > .dictionary,\n    main > .browse_wrapper,\n    main > .spellcheck_wrapper,\n    main > .content_wrapper,\n    main > .submit_new_word_wrapper,\n    main > .word_submitted_wrapper,\n    main > .suggested_word_wrapper {\n      width: 100%;\n      float: none;\n    }\n\n    [class*='res_cell'] {\n      clear: both;\n      width: 100%;\n      margin: 0 0 10px 0;\n    }\n\n    .res_cell_center_content,\n    .res_cell_2_3_content {\n      padding: 0;\n    }\n\n    .res_hos {\n      display: none;\n    }\n\n    .homepage header .left {\n      float: none;\n    }\n\n    main,\n    .main-content {\n      width: 100%;\n    }\n\n    .page .type-ant.columns3,\n    .page .content-box-syn-of-syns .columns3 {\n      -webkit-column-count: 1;\n      -moz-column-count: 1;\n      column-count: 1;\n    }\n\n    .dc .entry_title {\n      font-size: 1.5em;\n      padding-left: 0.5em;\n    }\n\n    .topslot_container {\n      margin-bottom: 10px;\n    }\n\n    .mpuslot_b {\n      width: 320px;\n      margin: 0 auto;\n    }\n\n    .columns .columns_item {\n      display: block;\n      width: auto;\n      padding-right: 0;\n      min-height: 0;\n    }\n\n    .page .dictionary .power {\n      display: none;\n    }\n\n    .content-box,\n    .page .Corpus_Examples_EN .content,\n    .dc .content-box,\n    .wotd-txt-block,\n    .promoBox-content {\n      padding: 10px;\n    }\n\n    .dc .content-box {\n      padding-top: 0;\n    }\n\n    .promoBox,\n    .promoBox {\n      min-height: 0;\n    }\n\n    .search-desktop {\n      display: none;\n    }\n\n    .search-desktop {\n      display: block;\n    }\n\n    #searchPanelButton:checked ~ .search-desktop {\n      display: block;\n      width: auto;\n      margin: 4px;\n      clear: both;\n    }\n\n    .search-desktop .custom-select {\n      display: block;\n      position: absolute;\n      left: 0;\n      opacity: 0.001;\n      width: 50px;\n      height: 100%;\n      font-size: medium;\n    }\n\n    .search-desktop .custom-select-menu {\n      -webkit-column-count: 1;\n      -moz-column-count: 1;\n      column-count: 1;\n    }\n\n    .dc .h1_entry {\n      line-height: 1.2em;\n    }\n\n    .cdet .titleTypeContainer .titleType {\n      margin: 0;\n    }\n\n    .dc .content-box.cobuild.br .content-box-header::after {\n      content: none;\n    }\n\n    .dc .content-box-videos .entryVideo {\n      max-width: 320px;\n      height: 160px;\n    }\n\n    .cdet .dc .entry_title,\n    .cdet .headerThes .entry_title + div {\n      padding-left: 10px;\n    }\n  }\n\n  @media screen and (min-width: 762px) and (max-width: 948px) {\n    main > .dictionary,\n    main > .browse_wrapper,\n    main > .spellcheck_wrapper,\n    main > .content_wrapper,\n    main > .submit_new_word_wrapper,\n    main > .word_submitted_wrapper,\n    main > .suggested_word_wrapper {\n      width: 100%;\n      float: none;\n    }\n\n    .res_cell_right {\n      width: 230px;\n    }\n\n    .res_cell_center_content {\n      padding-left: 0;\n    }\n  }\n\n  i[class^='icon'],\n  .extra-link,\n  .socialButtons {\n    display: none !important;\n  }\n\n  .saladict-StaticSpeaker {\n    margin: 0 1px;\n  }\n}\n\n.share-overlay,\n.popup-overlay,\n.share-button {\n  display: none !important;\n}\n"
  },
  {
    "path": "src/components/dictionaries/cobuild/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type CobuildConfig = DictItem<{\n  cibaFirst: boolean\n}>\n\nexport default (): CobuildConfig => ({\n  lang: '10000000',\n  selectionLang: {\n    english: true,\n    chinese: false,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 300,\n  selectionWC: {\n    min: 1,\n    max: 5\n  },\n  options: {\n    cibaFirst: (browser.i18n.getUILanguage() || 'en').startsWith('zh-')\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/cobuild/engine.ts",
    "content": "import { AppConfig } from '@/app-config'\nimport { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  HTMLString,\n  getInnerHTML,\n  handleNoResult,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  externalLink,\n  DictSearchResult,\n  getChsToChz\n} from '../helpers'\nimport { getStaticSpeaker } from '@/components/Speaker'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return (\n    `https://www.collinsdictionary.com/dictionary/english/` +\n    encodeURIComponent(text.replace(/\\s+/g, '-'))\n  )\n}\n\nexport interface COBUILDCibaResult {\n  type: 'ciba'\n  title: string\n  defs: HTMLString[]\n  level?: string\n  star?: number\n  prons?: Array<{\n    phsym: string\n    audio: string\n  }>\n}\n\nexport interface COBUILDColResult {\n  type: 'collins'\n  sections: Array<{\n    id: string\n    className: string\n    type: string\n    title: string\n    num: string\n    content: HTMLString\n  }>\n}\n\nexport type COBUILDResult = COBUILDCibaResult | COBUILDColResult\n\nexport const search: SearchFunction<COBUILDResult> = async (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  text = encodeURIComponent(text.replace(/\\s+/g, '-'))\n  const { options } = profile.dicts.all.cobuild\n  const sources: string[] = [\n    'https://www.collinsdictionary.com/dictionary/english/',\n    'https://www.collinsdictionary.com/zh/dictionary/english/'\n  ]\n\n  if (options.cibaFirst) {\n    sources.reverse()\n  }\n\n  try {\n    return handleDOM(await fetchDirtyDOM(sources[0] + text), config)\n  } catch (e) {\n    let doc: Document\n    try {\n      doc = await fetchDirtyDOM(sources[1] + text)\n    } catch (e) {\n      return handleNetWorkError()\n    }\n    return handleDOM(doc, config)\n  }\n}\n\nasync function handleDOM(\n  doc: Document,\n  config: AppConfig\n): Promise<DictSearchResult<COBUILDColResult>> {\n  const transform = await getChsToChz(config.langCode)\n\n  const result: COBUILDColResult = {\n    type: 'collins',\n    sections: []\n  }\n  const audio: { uk?: string; us?: string } = {}\n\n  result.sections = [\n    ...doc.querySelectorAll<HTMLDivElement>(`[data-type-block]`)\n  ]\n    .filter($section => {\n      const type = $section.dataset.typeBlock || ''\n      return (\n        type &&\n        type !== 'Video' &&\n        type !== 'Trends' &&\n        type !== '英语词汇表' &&\n        type !== '趋势'\n      )\n    })\n    .map($section => {\n      const type = $section.dataset.typeBlock || ''\n      const title = $section.dataset.titleBlock || ''\n      const num = $section.dataset.numBlock || ''\n      const id = type + title + num\n      const className = $section.className || ''\n\n      if (type === 'Learner') {\n        //   const $frequency = $section.querySelector<HTMLSpanElement>('.word-frequency-img')\n        //   if ($frequency) {\n        //     const star = Number($frequency.dataset.band)\n        //     if (star) {\n        //       result.star = star\n        //     }\n        //   }\n        if (!audio.uk) {\n          const mp3 = getAudio($section)\n          if (mp3) {\n            audio.uk = mp3\n          }\n        }\n      } else if (type === 'English') {\n        audio.uk = getAudio($section)\n      } else if (type === 'American') {\n        audio.us = getAudio($section)\n      }\n\n      const $video = $section.querySelector<HTMLDivElement>('#videos .video')\n      if ($video) {\n        const $youtubeVideo = $video.querySelector<HTMLDivElement>(\n          '.youtube-video'\n        )\n        if ($youtubeVideo && $youtubeVideo.dataset.embed) {\n          const width = config.panelWidth - 25\n          const height = (width / 560) * 315\n          return {\n            id,\n            className,\n            type,\n            title,\n            num,\n            content: `<iframe width=\"${width}\" height=\"${height}\" src=\"https://www.youtube-nocookie.com/embed/${$youtubeVideo.dataset.embed}\" frameborder=\"0\" allow=\"accelerometer; encrypted-media\"></iframe>`\n          }\n        }\n      }\n\n      $section\n        .querySelectorAll<HTMLAnchorElement>('.audio_play_button')\n        .forEach($speaker => {\n          $speaker.replaceWith(getStaticSpeaker($speaker.dataset.srcMp3))\n        })\n\n      // so that clicking won't trigger in-panel search\n      $section\n        .querySelectorAll<HTMLAnchorElement>('a.type-thesaurus')\n        .forEach(externalLink)\n\n      return {\n        id,\n        className,\n        type,\n        title,\n        num,\n        content: getInnerHTML('https://www.collinsdictionary.com', $section, {\n          transform\n        })\n      }\n    })\n\n  if (result.sections.length > 0) {\n    return { result, audio }\n  }\n\n  return handleNoResult()\n}\n\nfunction getAudio($section: HTMLElement): string | undefined {\n  const $audio = $section.querySelector<HTMLAnchorElement>(\n    '.pron .audio_play_button'\n  )\n  if ($audio) {\n    const src = $audio.dataset.srcMp3\n    if (src) {\n      return src\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/dictionaries.stories.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport axios from 'axios'\nimport AxiosMockAdapter from 'axios-mock-adapter'\nimport { storiesOf } from '@storybook/react'\nimport { action } from '@storybook/addon-actions'\nimport { withKnobs, select, number, boolean } from '@storybook/addon-knobs'\nimport {\n  withSaladictPanel,\n  withSideEffect,\n  mockRuntimeMessage,\n  withi18nNS\n} from '@/_helpers/storybook'\nimport { DictItem } from '@/content/components/DictItem/DictItem'\nimport { getDefaultConfig, DictID } from '@/app-config'\nimport { getDefaultProfile } from '@/app-config/profiles'\nimport { SearchFunction, MockRequest } from './helpers'\nimport { getAllDicts } from '@/app-config/dicts'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { timer } from '@/_helpers/promise-more'\n\nconst stories = storiesOf('Content Scripts|Dictionaries', module)\n  .addParameters({\n    backgrounds: [\n      { name: 'Saladict', value: '#5caf9e', default: true },\n      { name: 'Black', value: '#000' }\n    ]\n  })\n  .addDecorator(\n    withSideEffect(\n      mockRuntimeMessage(async message => {\n        if (message.type === 'DICT_ENGINE_METHOD') {\n          action('Calling DICT_ENGINE_METHOD')(message.payload)\n\n          await timer(Math.random() * 2000)\n\n          const method = require('@/components/dictionaries/' +\n            message.payload.id +\n            '/engine.ts')[message.payload.method]\n\n          return method(...(message.payload.args || []))\n        } else {\n          action(message.type)(message['payload'])\n        }\n      })\n    )\n  )\n  .addDecorator(\n    withSaladictPanel({\n      head: (\n        <style>\n          {require('@/content/components/DictItem/DictItem.scss').toString()}\n        </style>\n      ),\n      height: 'auto'\n    })\n  )\n  .addDecorator(withi18nNS(['content', 'dicts']))\n  .addDecorator(withKnobs)\n\nObject.keys(getAllDicts())\n  .filter(\n    // opentranslate\n    id =>\n      id !== 'baidu' &&\n      id !== 'caiyun' &&\n      id !== 'google' &&\n      id !== 'sogou' &&\n      id !== 'tencent' &&\n      id !== 'youdaotrans'\n  )\n  .forEach(id => {\n    // @ts-ignore: wrong storybook typing\n    stories.add(id, ({ fontSize, withAnimation, darkMode }) => (\n      <Dict\n        key={id}\n        dictID={id as DictID}\n        fontSize={fontSize}\n        darkMode={darkMode}\n        withAnimation={withAnimation}\n      />\n    ))\n  })\n\nfunction Dict(props: {\n  dictID: DictID\n  fontSize: number\n  darkMode: boolean\n  withAnimation: boolean\n}) {\n  const { i18n } = useTranslate()\n\n  const {\n    mockSearchTexts,\n    mockRequest\n  } = require('../../../test/specs/components/dictionaries/' +\n    props.dictID +\n    '/requests.mock.ts') as {\n    mockSearchTexts: string[]\n    mockRequest: MockRequest\n  }\n\n  const localesModule = require('@/components/dictionaries/' +\n    props.dictID +\n    '/_locales')\n\n  const locales = localesModule.locales || localesModule\n\n  const { search } = require('@/components/dictionaries/' +\n    props.dictID +\n    '/engine.ts') as { search: SearchFunction<any> }\n\n  const searchText =\n    mockSearchTexts.length > 1\n      ? select(\n          'Search Text',\n          mockSearchTexts.reduce((o, t) => {\n            o[t] = t\n            return o\n          }, {}),\n          mockSearchTexts[0]\n        )\n      : mockSearchTexts[0]\n\n  const [status, setStatus] = useState<'IDLE' | 'SEARCHING' | 'FINISH'>('IDLE')\n  const [result, setResult] = useState<any>(null)\n  const [catalog, setCatalog] = useState<any>()\n\n  const [profiles, updateProfiles] = useState(() => getDefaultProfile())\n  // custom dict options\n  const options = profiles.dicts.all[props.dictID]['options']\n  const optKeys = options ? Object.keys(options) : []\n  const optValues = optKeys.map(key => {\n    const name = locales.options[key][i18n.language]\n    switch (typeof options[key]) {\n      case 'boolean':\n        return boolean(name, options[key])\n      case 'number':\n        return number(name, options[key])\n      case 'string': {\n        const values: string[] =\n          profiles.dicts.all[props.dictID]['options_sel'][key]\n        return select(\n          name,\n          values.reduce((o, k) => {\n            o[locales.options[`${key}-${k}`][i18n.language]] = k\n            return o\n          }, {}),\n          options[key]\n        )\n      }\n      default:\n        return options[key]\n    }\n  })\n\n  useEffect(() => {\n    const newProfiles = getDefaultProfile()\n    const newOptions = newProfiles.dicts.all[props.dictID]['options']\n    optKeys.forEach((key, i) => {\n      newOptions[key] = optValues[i]\n    })\n    updateProfiles(newProfiles)\n  }, optValues)\n\n  useEffect(() => {\n    // mock requests\n    const mock = new AxiosMockAdapter(axios)\n    mockRequest(mock)\n    mock.onAny().reply(config => {\n      console.warn(`Unmatch url: ${config.url}`, config)\n      return [404, {}]\n    })\n    return () => mock.restore()\n  }, [])\n\n  useEffect(() => {\n    setStatus('SEARCHING')\n    search(searchText, getDefaultConfig(), profiles, {\n      isPDF: false\n    }).then(async ({ result, catalog }) => {\n      setStatus('FINISH')\n      setResult(result)\n      setCatalog(catalog)\n    })\n  }, [searchText, profiles])\n\n  return (\n    <DictItem\n      dictID={props.dictID}\n      catalog={catalog}\n      darkMode={props.darkMode}\n      withAnimation={props.withAnimation}\n      panelCSS={''}\n      preferredHeight={number('Preferred Height', 256)}\n      searchStatus={status}\n      searchResult={result}\n      searchText={action('Search Text')}\n      openDictSrcPage={action('Open Dict Source Page')}\n      onHeightChanged={action('Height Changed')}\n      onUserFold={action('User Fold')}\n      onSpeakerPlay={src => {\n        action('Speaker Play')(src)\n        return Promise.resolve()\n      }}\n      onInPanelSelect={() => action('Inpanel Select')()}\n    />\n  )\n}\n"
  },
  {
    "path": "src/components/dictionaries/etymonline/View.tsx",
    "content": "import React, { FC } from 'react'\nimport { EtymonlineResult } from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictEtymonline: FC<ViewPorps<EtymonlineResult>> = ({ result }) => (\n  <ul className=\"dictEtymonline-List\">\n    {result.map(item => (\n      <li key={item.title} className=\"dictEtymonline-Item\">\n        <h2 id={item.id} className=\"dictEtymonline-Title\">\n          {item.href ? (\n            <a\n              href={item.href}\n              target=\"_blank\"\n              rel=\"nofollow noopener noreferrer\"\n            >\n              {item.title}\n            </a>\n          ) : (\n            item.title\n          )}\n        </h2>\n        <StrElm tag=\"p\" className=\"dictEtymonline-Def\" html={item.def} />\n        {item.chart ? (\n          <img src={item.chart} alt={'Origin of ' + item.title} />\n        ) : null}\n      </li>\n    ))}\n  </ul>\n)\n\nexport default DictEtymonline\n"
  },
  {
    "path": "src/components/dictionaries/etymonline/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Etymonline\",\n    \"zh-CN\": \"Etymonline\",\n    \"zh-TW\": \"Etymonline\"\n  },\n  \"options\": {\n    \"resultnum\": {\n      \"en\": \"Show\",\n      \"zh-CN\": \"结果数量\",\n      \"zh-TW\": \"結果數量\"\n    },\n    \"resultnum_unit\": {\n      \"en\": \"results\",\n      \"zh-CN\": \"个\",\n      \"zh-TW\": \"個\"\n    },\n    \"chart\": {\n      \"en\": \"Show Chart\",\n      \"zh-CN\": \"显示图表\",\n      \"zh-TW\": \"顯示圖表\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/etymonline/_style.shadow.scss",
    "content": ".dictEtymonline-List {\n  p {\n    margin: 0 0 5px 0;\n  }\n\n  blockquote {\n    margin: 0.5em 0;\n    padding: 0 1em;\n    font-style: italic;\n    border-left: 2px solid #f9690e;\n  }\n\n  .foreign {\n    font-style: italic;\n  }\n\n  .line-break {\n    margin-bottom: 5px;\n  }\n}\n\n.dictEtymonline-Item {\n  margin: 10px 0;\n}\n\n.dictEtymonline-Title {\n  margin: 0;\n  font-size: 1em;\n}\n\n.dictEtymonline-Def {\n  margin: 0;\n}\n"
  },
  {
    "path": "src/components/dictionaries/etymonline/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type EtymonlineConfig = DictItem<{\n  resultnum: number\n  chart: boolean\n}>\n\nexport default (): EtymonlineConfig => ({\n  lang: '10000000',\n  selectionLang: {\n    english: true,\n    chinese: false,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 265,\n  selectionWC: {\n    min: 1,\n    max: 5\n  },\n  options: {\n    resultnum: 4,\n    chart: true\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/etymonline/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport { DictConfigs } from '@/app-config'\nimport {\n  getText,\n  getInnerHTML,\n  getFullLink,\n  handleNoResult,\n  HTMLString,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult\n} from '../helpers'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return `http://www.etymonline.com/search?q=${text}`\n}\n\nconst HOST = 'https://www.etymonline.com'\n\ntype EtymonlineResultItem = {\n  id: string\n  title: string\n  def: HTMLString\n  href?: string\n  chart?: string\n}\n\nexport type EtymonlineResult = EtymonlineResultItem[]\n\ntype EtymonlineSearchResult = DictSearchResult<EtymonlineResult>\n\nexport const search: SearchFunction<EtymonlineResult> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  const options = profile.dicts.all.etymonline.options\n  text = encodeURIComponent(text.replace(/\\s+/g, ' '))\n\n  // http to bypass the referer checking\n  return fetchDirtyDOM('https://www.etymonline.com/word/' + text)\n    .catch(() => fetchDirtyDOM('https://www.etymonline.com/search?q=' + text))\n    .catch(handleNetWorkError)\n    .then(doc => handleDOM(doc, options))\n}\n\nfunction handleDOM(\n  doc: Document,\n  options: DictConfigs['etymonline']['options']\n): EtymonlineSearchResult | Promise<EtymonlineSearchResult> {\n  const result: EtymonlineResult = []\n  const catalog: NonNullable<EtymonlineSearchResult['catalog']> = []\n  const $items = Array.from(doc.querySelectorAll('[class*=\"word--\"]'))\n\n  for (let i = 0; i < $items.length && result.length < options.resultnum; i++) {\n    const $item = $items[i]\n\n    const title = getText($item, '[class*=\"word__name--\"]')\n    if (!title) {\n      continue\n    }\n\n    let def = ''\n    const $def = $item.querySelector('[class*=\"word__defination--\"]>*')\n    if ($def) {\n      $def.querySelectorAll('.crossreference').forEach($cf => {\n        const word = getText($cf)\n\n        const $a = document.createElement('a')\n        $a.target = '_blank'\n        $a.href = `https://www.etymonline.com/word/${word}`\n        $a.textContent = word\n\n        $cf.replaceWith($a)\n      })\n      def = getInnerHTML(HOST, $def)\n    }\n    if (!def) {\n      continue\n    }\n\n    const href = getFullLink(HOST, $item, 'href')\n\n    let chart = ''\n    if (options.chart) {\n      const $chart = $item.querySelector<HTMLImageElement>(\n        '[class*=\"chart--\"] img'\n      )\n      if ($chart) {\n        chart = getFullLink(HOST, $chart, 'src')\n      }\n    }\n\n    const id = `d-etymonline-entry${i}`\n\n    result.push({ id, href, title, def, chart })\n\n    catalog.push({\n      key: `#${i}`,\n      value: id,\n      label: `#${title}`\n    })\n  }\n\n  if (result.length > 0) {\n    return { result, catalog }\n  }\n\n  return handleNoResult()\n}\n"
  },
  {
    "path": "src/components/dictionaries/eudic/View.tsx",
    "content": "import React, { FC } from 'react'\nimport { EudicResult } from './engine'\nimport Speaker from '@/components/Speaker'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\n\nexport const DictEudic: FC<ViewPorps<EudicResult>> = ({ result }) => (\n  <ul className=\"dictEudic-List\">\n    {result.map(item => (\n      <li key={item.chs} className=\"dictEudic-Item\">\n        <p>\n          {item.eng} <Speaker src={item.mp3} />\n        </p>\n        <p>{item.chs}</p>\n        <footer>\n          {item.channel && <p className=\"dictEudic-Channel\">{item.channel}</p>}\n        </footer>\n      </li>\n    ))}\n  </ul>\n)\n\nexport default DictEudic\n"
  },
  {
    "path": "src/components/dictionaries/eudic/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Eudic\",\n    \"zh-CN\": \"双语例句\",\n    \"zh-TW\": \"雙語例句\"\n  },\n  \"options\": {\n    \"resultnum\": {\n      \"en\": \"Show\",\n      \"zh-CN\": \"结果数量\",\n      \"zh-TW\": \"結果數量\"\n    },\n    \"resultnum_unit\": {\n      \"en\": \"results\",\n      \"zh-CN\": \"个\",\n      \"zh-TW\": \"個\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/eudic/_style.shadow.scss",
    "content": ".dictEudic-Item {\n  margin-bottom: 10px;\n\n  p {\n    margin: 0;\n  }\n}\n\n.dictEudic-Channel {\n  color: #999;\n\n  &::before {\n    content: '《';\n  }\n\n  &::after {\n    content: '》';\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/eudic/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type EudicConfig = DictItem<{\n  resultnum: number\n}>\n\nexport default (): EudicConfig => ({\n  lang: '11000000',\n  selectionLang: {\n    english: true,\n    chinese: true,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 240,\n  selectionWC: {\n    min: 1,\n    max: 5\n  },\n  options: {\n    resultnum: 10\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/eudic/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  getText,\n  handleNoResult,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult\n} from '../helpers'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return `https://dict.eudic.net/dicts/en/${text}`\n}\n\ninterface EudicResultItem {\n  chs: string\n  eng: string\n  mp3?: string\n  channel?: string\n}\n\nexport type EudicResult = EudicResultItem[]\n\ntype EudicSearchResult = DictSearchResult<EudicResult>\n\nexport const search: SearchFunction<EudicResult> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  text = encodeURIComponent(\n    text\n      .split(/\\s+/)\n      .slice(0, 2)\n      .join(' ')\n  )\n  const options = profile.dicts.all.eudic.options\n\n  return fetchDirtyDOM('https://dict.eudic.net/dicts/en/' + text, {\n    withCredentials: false\n  })\n    .catch(handleNetWorkError)\n    .then(validator)\n    .then(doc => handleDOM(doc, options))\n}\n\nfunction handleDOM(\n  doc: Document,\n  { resultnum }: { resultnum: number }\n): EudicSearchResult | Promise<EudicSearchResult> {\n  const result: EudicResult = []\n  const audio: { uk?: string; us?: string } = {}\n\n  const $items = Array.from(doc.querySelectorAll('#lj_ting .lj_item'))\n  for (let i = 0; i < $items.length && result.length < resultnum; i++) {\n    const $item = $items[i]\n    const item: EudicResultItem = { chs: '', eng: '' }\n\n    item.chs = getText($item, '.exp')\n    if (!item.chs) {\n      continue\n    }\n\n    item.eng = getText($item, '.line')\n    if (!item.eng) {\n      continue\n    }\n\n    item.channel = getText($item, '.channel_title')\n\n    const audioID = $item.getAttribute('source')\n    if (audioID) {\n      const mp3 =\n        'https://fs-gateway.eudic.net/store_main/sentencemp3/' +\n        audioID +\n        '.mp3'\n      item.mp3 = mp3\n      if (!audio.us) {\n        audio.us = mp3\n        audio.uk = mp3\n      }\n    }\n\n    result.push(item)\n  }\n\n  if (result.length > 0) {\n    return { result, audio }\n  }\n\n  return handleNoResult()\n}\n\nfunction validator(doc: Document): Document | Promise<Document> {\n  if (doc.querySelector('#TingLiju')) {\n    return doc\n  }\n\n  const status = doc.querySelector('#page-status') as HTMLInputElement\n  if (!status || !status.value) {\n    return handleNoResult()\n  }\n\n  const formData = new FormData()\n  formData.append('status', status.value)\n\n  return fetchDirtyDOM('https://dict.eudic.net/Dicts/en/tab-detail/-12', {\n    method: 'POST',\n    data: formData,\n    withCredentials: false\n  })\n}\n"
  },
  {
    "path": "src/components/dictionaries/google/View.tsx",
    "content": "export { MachineTrans as default } from '@/components/MachineTrans/MachineTrans'\n"
  },
  {
    "path": "src/components/dictionaries/google/_locales.ts",
    "content": "import { getMachineLocales } from '../locales'\n\nexport const locales = getMachineLocales(\n  {\n    en: 'Google Translation',\n    'zh-CN': '谷歌翻译',\n    'zh-TW': '谷歌翻譯'\n  },\n  {\n    cnfirst: {\n      en: 'Use google.cn first',\n      'zh-CN': '优先使用 google.cn',\n      'zh-TW': '優先使用 google.cn'\n    },\n    concurrent: {\n      en: 'Search concurrently',\n      'zh-CN': '并行查询',\n      'zh-TW': '並行查詢'\n    }\n  },\n  {\n    concurrent: {\n      en: 'Search .com and .cn concurrently',\n      'zh-CN': '同时搜索 .com 和 .cn 取最先返回的结果',\n      'zh-TW': '同時搜尋 .com 和 .cn 取最先返回的結果'\n    }\n  }\n)\n"
  },
  {
    "path": "src/components/dictionaries/google/_style.shadow.scss",
    "content": "@import '@/components/MachineTrans/MachineTrans.scss';\n"
  },
  {
    "path": "src/components/dictionaries/google/config.ts",
    "content": "import {\n  MachineDictItem,\n  machineConfig\n} from '@/components/MachineTrans/engine'\nimport { Language } from '@opentranslate/translator'\nimport { Subunion } from '@/typings/helpers'\n\nexport type GoogleLanguage = Subunion<\n  Language,\n  'zh-CN' | 'zh-TW' | 'en' | 'ja' | 'ko' | 'fr' | 'de' | 'es' | 'ru' | 'nl'\n>\n\nexport type GoogleConfig = MachineDictItem<\n  GoogleLanguage,\n  {\n    concurrent: boolean\n  }\n>\n\nexport default (): GoogleConfig =>\n  machineConfig<GoogleConfig>(\n    ['zh-CN', 'zh-TW', 'en', 'ja', 'ko', 'fr', 'de', 'es', 'ru', 'nl'],\n    {},\n    {\n      concurrent: false\n    },\n    {}\n  )\n"
  },
  {
    "path": "src/components/dictionaries/google/engine.ts",
    "content": "import { SearchFunction, GetSrcPageFunction } from '../helpers'\nimport memoizeOne from 'memoize-one'\nimport { Google } from '@opentranslate/google'\nimport {\n  MachineTranslateResult,\n  MachineTranslatePayload,\n  getMTArgs,\n  machineResult\n} from '@/components/MachineTrans/engine'\nimport { GoogleLanguage } from './config'\nimport { Language } from '@opentranslate/languages'\n\nexport const getTranslator = memoizeOne(() => new Google({ env: 'ext' }))\n\nexport const getSrcPage: GetSrcPageFunction = (text, config, profile) => {\n  const domain = 'com'\n  const lang =\n    profile.dicts.all.google.options.tl === 'default'\n      ? config.langCode\n      : profile.dicts.all.google.options.tl\n\n  return `https://translate.google.${domain}/#auto/${lang}/${text}`\n}\n\nexport type GoogleResult = MachineTranslateResult<'google'>\n\nexport const search: SearchFunction<\n  GoogleResult,\n  MachineTranslatePayload<GoogleLanguage>\n> = async (rawText, config, profile, payload) => {\n  const options = profile.dicts.all.google.options\n\n  const translator = getTranslator()\n\n  const { sl, tl, text } = await getMTArgs(\n    translator,\n    rawText,\n    profile.dicts.all.google,\n    config,\n    payload\n  )\n\n  try {\n    const result = await translator.translate(text, sl, tl, {\n      token: process.env.GOOGLE_TOKEN || '',\n      concurrent: options.concurrent,\n      apiAsFallback: true\n    })\n    return machineResult(\n      {\n        result: {\n          id: 'google',\n          sl: result.from,\n          tl: result.to,\n          slInitial: profile.dicts.all.google.options.slInitial,\n          searchText: result.origin,\n          trans: result.trans\n        },\n        audio: {\n          py: result.trans.tts,\n          us: result.trans.tts\n        }\n      },\n      translator.getSupportLanguages()\n    )\n  } catch (e) {\n    return machineResult(\n      {\n        result: {\n          id: 'google',\n          sl,\n          tl,\n          slInitial: 'hide',\n          searchText: { paragraphs: [''] },\n          trans: { paragraphs: [''] }\n        }\n      },\n      translator.getSupportLanguages()\n    )\n  }\n}\n\nexport async function getTTS(text: string, lang: Language): Promise<string> {\n  return (await getTranslator().textToSpeech(text, lang)) || ''\n}\n"
  },
  {
    "path": "src/components/dictionaries/googledict/View.tsx",
    "content": "import React, { FC } from 'react'\nimport { GoogleDictResult } from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictGoogleDict: FC<ViewPorps<GoogleDictResult>> = ({ result }) => (\n  <div>\n    {result.styles.map((style, i) => (\n      <style key={i}>{style}</style>\n    ))}\n    <StrElm onClick={onEntryClick} className=\"xpdopen\" html={result.entry} />\n  </div>\n)\n\nfunction onEntryClick(e: React.MouseEvent) {\n  for (\n    let isMoreBtn: boolean | null = null, node = e.target as Element | null;\n    node;\n    node = node.parentElement\n  ) {\n    if (node.getAttribute('jsname') === 'Stv3Z') {\n      isMoreBtn = true\n    } else if (node.getAttribute('jsname') === 'hj0qK') {\n      isMoreBtn = false\n    }\n    if (node.classList) {\n      if (node.classList.contains('P2Dfkf')) {\n        if (isMoreBtn === null) {\n          continue\n        }\n        if (isMoreBtn) {\n          node.classList.replace('SkSOXb', 'KAwqid')\n        } else {\n          node.classList.replace('KAwqid', 'SkSOXb')\n        }\n        break\n      }\n    }\n  }\n}\n\nexport default DictGoogleDict\n"
  },
  {
    "path": "src/components/dictionaries/googledict/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Google Dictionary\",\n    \"zh-CN\": \"谷歌词典\",\n    \"zh-TW\": \"谷歌詞典\"\n  },\n  \"options\": {\n    \"enresult\": {\n      \"en\": \"English Result\",\n      \"zh-CN\": \"强制显示英文结果\",\n      \"zh-TW\": \"強制顯示英文結果\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/googledict/_style.shadow.scss",
    "content": "ol li {\n  list-style: none;\n}\n\nol,\nul,\nli {\n  border: 0;\n  margin: 0;\n  padding: 0;\n}\n\nimg {\n  max-width: 99%;\n}\n\n.saladict-Speaker {\n  float: left;\n  margin-top: 10px;\n}\n\n.hide-focus-ring {\n  outline: 0;\n}\n\n// hide more buttons on expand\n.KAwqid {\n  [jsname=Stv3Z] {\n    display: none !important;\n  }\n\n  [jsname=hj0qK] {\n    display: block !important;\n  }\n}\n\n// clean button text\n[role=listitem] > div > a {\n  color: inherit;\n  text-decoration: none;\n}\n\n// hide g-audio button\n.brWULd {\n  width: initial !important;\n  height: initial !important;\n  margin: initial !important;\n  padding: initial !important;\n\n  & > .saladict-Speaker ~ * {\n    display: none !important;\n  }\n}\n\n.gycwpf {\n  margin: 0 !important;\n}\n\n.GgmXif {\n  font-size: 1.8em !important;\n}\n\n.DgZBFd {\n  line-height: 1 !important;\n}\n\n.jY7QFf {\n  min-height: 1em !important;\n}\n\n.pgRvse {\n  padding-top: 0 !important;\n}\n\n.SDZsVb {\n  color: #f9690e !important;\n}\n\n// @TODO hide \"See definitions in:\" for now\n[jsname=p0q1Sd] {\n  display: none !important;\n}\n\n// tags button color\n.MR2UAc {\n  background: var(--color-background) !important;\n  border: 1px solid var(--color-divider) !important;\n}\n\n.jEdCLc, .D1MTm {\n  color: var(--color-font-grey) !important;\n}\n\n.g-img {\n  height: unset !important;\n}\n"
  },
  {
    "path": "src/components/dictionaries/googledict/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type GoogledictConfig = DictItem<{\n  enresult: boolean\n}>\n\nexport default (): GoogledictConfig => ({\n  lang: '11110000',\n  selectionLang: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 240,\n  selectionWC: {\n    min: 1,\n    max: 5\n  },\n  options: {\n    enresult: true\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/googledict/engine.ts",
    "content": "import {\n  HTMLString,\n  handleNoResult,\n  getInnerHTML,\n  removeChildren,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult,\n  getFullLink,\n  getText,\n  removeChild\n} from '../helpers'\nimport { getStaticSpeaker } from '@/components/Speaker'\nimport { fetchPlainText } from '@/_helpers/fetch-dom'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return (\n    'https://www.google.com.hk/search?hl=en&safe=off&q=meaning:' +\n    encodeURIComponent(text.toLowerCase().replace(/\\s+/g, '+'))\n  )\n}\n\nexport interface GoogleDictResult {\n  entry: HTMLString\n  styles: string[]\n}\n\ntype GoogleDictSearchResult = DictSearchResult<GoogleDictResult>\n\nexport const search: SearchFunction<GoogleDictResult> = async (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  const isen = profile.dicts.all.googledict.options.enresult\n    ? 'hl=en&gl=en&'\n    : ''\n\n  const encodedText = encodeURIComponent(\n    text.toLowerCase().replace(/\\s+/g, '+')\n  )\n\n  try {\n    return await fetchPlainText(\n      `https://www.google.com/search?hl=en&safe=off&${isen}q=meaning:${encodedText}`\n    )\n      .catch(handleNetWorkError)\n      .then(handleDOM)\n  } catch (e) {\n    return await fetchPlainText(\n      `https://www.google.com/search?hl=en&safe=off&${isen}q=define:${encodedText}`\n    )\n      .catch(handleNetWorkError)\n      .then(handleDOM)\n  }\n\n  function handleDOM(\n    bodyText: string\n  ): GoogleDictSearchResult | Promise<GoogleDictSearchResult> {\n    const doc = new DOMParser().parseFromString(bodyText, 'text/html')\n\n    // mend fragments\n    extFragements(bodyText).forEach(({ id, innerHTML }) => {\n      try {\n        const el = doc.querySelector(`#${id}`)\n        if (el) {\n          el.innerHTML = innerHTML\n        }\n      } catch (e) {\n        // ignore\n      }\n    })\n\n    const $obcontainer = doc.querySelector('.lr_container')\n    if ($obcontainer) {\n      $obcontainer\n        .querySelectorAll<HTMLDivElement>('.vkc_np')\n        .forEach($block => {\n          if (\n            $block.querySelector('.zbA8Me') || // Dictionary title\n            $block.querySelector('#dw-siw') || // Search box\n            $block.querySelector('#tl_select') // Translate to\n          ) {\n            $block.remove()\n          }\n        })\n\n      removeChildren($obcontainer, '.lr_dct_trns_h') // other Translate to blocks\n      removeChildren($obcontainer, '.S5TwIf') // Learn to pronounce\n      removeChildren($obcontainer, '.VZVCid') // From Oxford\n      removeChildren($obcontainer, '.u7XA4b') // footer\n      removeChild($obcontainer, '[jsname=L4Nn5e]') // remove translate to\n\n      // tts\n      $obcontainer.querySelectorAll('audio').forEach($audio => {\n        const $source = $audio.querySelector('source')\n\n        let src =\n          $source && getFullLink('https://ssl.gstatic.com', $source, 'src')\n\n        if (!src) {\n          src =\n            'https://www.google.com/speech-api/v1/synthesize?enc=mpeg&lang=zh-cn&speed=0.4&client=lr-language-tts&use_google_only_voices=1&text=' +\n            encodeURIComponent(text)\n        }\n\n        $audio.replaceWith(getStaticSpeaker(src))\n      })\n\n      $obcontainer\n        .querySelectorAll('[role=listitem] > [jsname=F457ec]')\n        .forEach($word => {\n          // let saladict jump into the words\n          const $a = document.createElement('a')\n          $a.textContent = getText($word)\n          Array.from($word.childNodes).forEach($child => {\n            $child.remove()\n          })\n          $word.appendChild($a)\n          // always appeared available\n          $word.removeAttribute('style')\n          $word.classList.add('MR2UAc')\n          $word.classList.add('I6a0ee')\n          $word.classList.remove('cO53qb')\n        })\n\n      $obcontainer.querySelectorAll('g-img > img').forEach($img => {\n        const src = $img.getAttribute('title')\n        if (src) {\n          $img.setAttribute('src', src)\n        }\n      })\n\n      extractImg(bodyText).forEach(({ id, src }) => {\n        try {\n          const el = $obcontainer.querySelector(`#${id}`)\n          if (el) {\n            el.setAttribute('src', src)\n          }\n        } catch (e) {\n          // ignore\n        }\n      })\n\n      const cleanText = getInnerHTML('https://www.google.com', $obcontainer, {\n        config: {\n          ADD_TAGS: ['g-img'],\n          ADD_ATTR: ['jsname', 'jsaction']\n        }\n      })\n        .replace(/synonyms:/g, 'syn:')\n        .replace(/antonyms:/g, 'ant:')\n\n      const styles: string[] = []\n      doc.querySelectorAll('style').forEach($style => {\n        const textContent = getText($style)\n        if (textContent && /\\.xpdxpnd|\\.lr_container/.test(textContent)) {\n          styles.push(textContent)\n        }\n      })\n\n      return { result: { entry: cleanText, styles } }\n    }\n\n    return handleNoResult<GoogleDictSearchResult>()\n  }\n}\n\nfunction extFragements(text: string): Array<{ id: string; innerHTML: string }> {\n  const result: Array<{ id: string; innerHTML: string }> = []\n  const matcher = /\\(function\\(\\)\\{window\\.jsl\\.dh\\('([^']+)','([^']+)'\\);\\}\\)\\(\\);/g\n  let match: RegExpExecArray | null | undefined\n  while ((match = matcher.exec(text))) {\n    result.push({\n      id: match[1],\n      innerHTML: match[2]\n        // escape \\x\n        .replace(/\\\\x([\\da-f]{2})/gi, decodeHex)\n        // escape \\u\n        .replace(/\\\\[u]([\\da-f]{4})/gi, decodeHex)\n    })\n  }\n  return result\n}\n\nfunction extractImg(text: string): Array<{ id: string; src: string }> {\n  const kvPairMatch = /google.ldi={([^}]+)}/.exec(text)\n  if (kvPairMatch) {\n    try {\n      const json = JSON.parse(`{${kvPairMatch[1]}}`)\n      return Object.keys(json).map(key => ({ id: key, src: json[key] }))\n    } catch (e) {\n      // ignore\n    }\n  }\n  return []\n}\n\nfunction decodeHex(m: string, code: string): string {\n  return String.fromCharCode(parseInt(code, 16))\n}\n"
  },
  {
    "path": "src/components/dictionaries/guoyu/View.tsx",
    "content": "import React, { FC } from 'react'\nimport { GuoYuResult } from './engine'\nimport Speaker from '@/components/Speaker'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\n\nexport const DictGuoyu: FC<ViewPorps<GuoYuResult>> = ({ result }) => (\n  <>\n    {result.h &&\n      result.h.map(h => (\n        <div className=\"dictMoe-H\" key={h.p}>\n          <h1 className=\"dictMoe-Title\">{replaceLink(result.t)}</h1>\n          <span className=\"dictMoe-Pinyin\">{h.p || ''}</span>\n          <Speaker src={h['=']}></Speaker>\n          {h.d && (\n            <ol className=\"dictMoe-Defs\">\n              {h.d.map(defs => (\n                <li key={defs.f}>\n                  <p className=\"dictMoe-Defs_F\">{replaceLink(defs.f)}</p>\n                  {defs.e &&\n                    defs.e.map(e => (\n                      <p key={e} className=\"dictMoe-Defs_E\">\n                        {replaceLink(e)}\n                      </p>\n                    ))}\n                </li>\n              ))}\n            </ol>\n          )}\n        </div>\n      ))}\n\n    {result.translation && (\n      <>\n        {result.translation.English && (\n          <div className=\"dictMoe-Trans\">\n            <span className=\"dictMoe-Trans_Pos\">英.</span>\n            <span className=\"dictMoe-Trans_Def\">\n              {result.translation.English.join(', ')}\n            </span>\n          </div>\n        )}\n        {result.translation.francais && (\n          <div className=\"dictMoe-Trans\">\n            <span className=\"dictMoe-Trans_Pos\">法.</span>\n            <span className=\"dictMoe-Trans_Def\">\n              {result.translation.francais.join(', ')}\n            </span>\n          </div>\n        )}\n        {result.translation.Deutsch && (\n          <div className=\"dictMoe-Trans\">\n            <span className=\"dictMoe-Trans_Pos\">德.</span>\n            <span className=\"dictMoe-Trans_Def\">\n              {result.translation.Deutsch.join(', ')}\n            </span>\n          </div>\n        )}\n      </>\n    )}\n  </>\n)\n\nexport default DictGuoyu\n\nfunction replaceLink(text: string) {\n  if (!text) {\n    return ''\n  }\n  return text.split(/`(.*?)~/g).map((word, i) =>\n    i % 2 === 0 ? (\n      word.replace('例⃝', '')\n    ) : (\n      <a\n        key={i}\n        className=\"dictMoe-Link\"\n        href={`https://www.moedict.tw/${word}`}\n      >\n        {word}\n      </a>\n    )\n  )\n}\n"
  },
  {
    "path": "src/components/dictionaries/guoyu/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"國語辭典\",\n    \"zh-CN\": \"国语辞典\",\n    \"zh-TW\": \"國語辭典\"\n  },\n  \"options\": {\n    \"trans\": {\n      \"en\": \"Show Translation\",\n      \"zh-CN\": \"显示翻译\",\n      \"zh-TW\": \"顯示翻譯\"\n    }\n  },\n  \"helps\": {\n    \"trans\": {\n      \"en\": \"Show translation of other languages.\",\n      \"zh-CN\": \"显示其它语言的翻译。\",\n      \"zh-TW\": \"顯示其它語言的翻譯。\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/guoyu/_style.shadow.scss",
    "content": ".dictMoe-H {\n  margin-bottom: 10px;\n}\n\n.dictMoe-Title {\n  display: inline;\n  font-size: 1.6em;\n  font-weight: normal;\n  margin: 0 0.2em;\n}\n\n.dictMoe-Defs {\n  margin: 0 0 10px;\n  padding-left: 1.5em;\n}\n\n.dictMoe-Defs_F {\n  margin: 0;\n}\n\n.dictMoe-Defs_E {\n  margin: 0;\n  color: var(--color-font-grey);\n}\n\n.dictMoe-Trans {\n  display: table;\n}\n\n.dictMoe-Trans_Pos {\n  display: table-cell;\n  width: 2em;\n  font-weight: bold;\n  text-align: right;\n}\n\n.dictMoe-Trans_Def {\n  display: table-cell;\n  padding: 0 12px;\n}\n\n.dictMoe-Link {\n  &:link,\n  &:visited,\n  &:hover,\n  &:active {\n    color: inherit;\n    text-decoration: none;\n  }\n\n  &:focus,\n  &:hover {\n    background: #16a085;\n    outline: 3px solid #16a085;\n    color: #fff;\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/guoyu/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type GuoyuConfig = DictItem<{\n  /** show translation */\n  trans: boolean\n}>\n\nexport default (): GuoyuConfig => ({\n  lang: '00100000',\n  selectionLang: {\n    english: false,\n    chinese: true,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 265,\n  selectionWC: {\n    min: 1,\n    max: 5\n  },\n  options: {\n    trans: true\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/guoyu/engine.ts",
    "content": "import {\n  handleNoResult,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult,\n  getChsToChz\n} from '../helpers'\nimport { AppConfig } from '@/app-config'\nimport axios from 'axios'\nimport { Profile } from '@/app-config/profiles'\n\nexport const getSrcPage: GetSrcPageFunction = async text => {\n  const transform = await getChsToChz()\n  return `https://www.moedict.tw/${transform(text)}`\n}\n\n/** @see https://github.com/audreyt/moedict-webkit#4-國語-a */\nexport interface GuoYuResult {\n  n: number\n  /** Title */\n  t: string\n  r: string\n  c: number\n  h?: Array<{\n    /** Definitions */\n    d: Array<{\n      /** Title */\n      type: string\n      /** Meaning */\n      f: string\n      /** Homophones */\n      l?: string[]\n      /** Examples */\n      e?: string[]\n      /** Quotes */\n      q?: string[]\n    }>\n    /** Pinyin */\n    p: string\n    /** Audio ID */\n    '='?: string\n  }>\n  translation?: {\n    francais?: string[]\n    Deutsch?: string[]\n    English?: string[]\n  }\n}\n\nexport const search: SearchFunction<GuoYuResult> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  return moedictSearch<GuoYuResult>(\n    'a',\n    text,\n    config,\n    profile.dicts.all.guoyu.options\n  )\n}\n\nexport async function moedictSearch<R extends GuoYuResult>(\n  moedictID: string,\n  text: string,\n  config: AppConfig,\n  options: Profile['dicts']['all']['guoyu']['options']\n): Promise<DictSearchResult<R>> {\n  const chsToChz = await getChsToChz()\n  const { data } = await axios\n    .get<R>(\n      `https://www.moedict.tw/${moedictID}/${encodeURIComponent(\n        chsToChz(text.replace(/\\s+/g, ''))\n      )}.json`\n    )\n    .catch(handleNetWorkError)\n\n  if (!data || !data.h) {\n    return handleNoResult()\n  }\n\n  if (!options.trans) {\n    data.translation = undefined\n  }\n\n  const result: DictSearchResult<R> = { result: data }\n\n  for (const h of data.h) {\n    if (h['=']) {\n      h[\n        '='\n      ] = `https://203146b5091e8f0aafda-15d41c68795720c6e932125f5ace0c70.ssl.cf1.rackcdn.com/${h['=']}.ogg`\n    }\n    if (!result.audio) {\n      result.audio = {\n        py: h['=']\n      }\n    }\n  }\n\n  return result\n}\n"
  },
  {
    "path": "src/components/dictionaries/helpers.ts",
    "content": "import DOMPurify from 'dompurify'\nimport { useEffect, useRef } from 'react'\nimport { useSubscription, useObservableCallback } from 'observable-hooks'\nimport { debounceTime, map, tap } from 'rxjs/operators'\nimport { Observable } from 'rxjs'\nimport AxiosMockAdapter from 'axios-mock-adapter'\nimport { DictID, AppConfig } from '@/app-config'\nimport { Profile } from '@/app-config/profiles'\nimport { Word } from '@/_helpers/record-manager'\nimport { isTagName } from '@/_helpers/dom'\nimport { isInternalPage } from '@/_helpers/saladict'\n\n/** Fetch and parse dictionary search result */\nexport interface SearchFunction<Result, Payload = {}> {\n  (\n    text: string,\n    config: AppConfig,\n    profile: Profile,\n    payload: Readonly<Payload & { isPDF: boolean }>\n  ): Promise<DictSearchResult<Result>>\n}\n\nexport interface DictSearchResult<Result> {\n  /** search result */\n  result: Result\n  /** auto play sound */\n  audio?: {\n    uk?: string\n    us?: string\n    py?: string\n  }\n  /** generate menus on dict titlebars */\n  catalog?: Array<\n    | {\n        // <button>\n        key: string\n        value: string\n        label: string\n        options?: undefined\n      }\n    | {\n        // <select>\n        key: string\n        value: string\n        options: Array<{\n          value: string\n          label: string\n        }>\n        title?: string\n      }\n  >\n}\n\n/** Return a dictionary source page url for the dictionary header */\nexport interface GetSrcPageFunction {\n  (text: string, config: AppConfig, profile: Profile): string | Promise<string>\n}\n\n/**\n * For testing and storybook.\n *\n * Mock all the requests and returns all searchable texts.\n */\nexport interface MockRequest {\n  (mock: AxiosMockAdapter): void\n}\n\nexport type HTMLString = string\n\nexport interface ViewPorps<T> {\n  result: T\n  searchText: <P = { [index: string]: any }>(arg?: {\n    id?: DictID\n    word?: Word\n    payload?: P\n  }) => any\n  /** Emit catalog key and value when selected */\n  catalogSelect$: Observable<{ key: string; value: string }>\n}\n\nexport type SearchErrorType = 'NO_RESULT' | 'NETWORK_ERROR'\n\nexport function handleNoResult<T = any>(): Promise<T> {\n  return Promise.reject(new Error('NO_RESULT'))\n}\n\nexport function handleNetWorkError(): Promise<never> {\n  return Promise.reject(new Error('NETWORK_ERROR'))\n}\n\n/**\n * Get chs-chz transform function on-demand.\n * The dict object is huge.\n * @param langCode\n */\nexport async function getChsToChz(): Promise<(text: string) => string>\nexport async function getChsToChz(\n  langCode: string\n): Promise<null | ((text: string) => string)>\nexport async function getChsToChz(\n  langCode?: string\n): Promise<null | ((text: string) => string)> {\n  return langCode == null || /zh-TW|zh-HK/i.test(langCode)\n    ? (await import('@/_helpers/chs-to-chz')).chsToChz\n    : null\n}\n\n/**\n * Get the textContent of a node or its child.\n */\nexport function getText(\n  parent: ParentNode | null,\n  selector?: string,\n  transform?: null | ((text: string) => string)\n): string\nexport function getText(\n  parent: ParentNode | null,\n  transform?: null | ((text: string) => string),\n  selector?: string\n): string\nexport function getText(\n  parent: ParentNode | null,\n  ...args:\n    | [string?, (null | ((text: string) => string))?]\n    | [(null | ((text: string) => string))?, string?]\n): string {\n  if (!parent) {\n    return ''\n  }\n\n  let selector = ''\n  let transform: null | ((text: string) => string) = null\n  for (let i = args.length - 1; i >= 0; i--) {\n    if (typeof args[i] === 'string') {\n      selector = args[i] as string\n    } else if (typeof args[i] === 'function') {\n      transform = args[i] as (text: string) => string\n    }\n  }\n\n  const child = selector\n    ? parent.querySelector(selector)\n    : (parent as HTMLElement)\n  if (!child) {\n    return ''\n  }\n\n  const textContent = child.textContent || ''\n  return transform ? transform(textContent) : textContent\n}\n\nexport interface GetHTMLConfig {\n  /** innerHTML or outerHTML */\n  mode?: 'innerHTML' | 'outerHTML'\n  /** Select child node */\n  selector?: string\n  /** transform text */\n  transform?: null | ((text: string) => string)\n  /** Give url and src a host */\n  host?: string\n  /** DOM Purify config */\n  config?: DOMPurify.Config\n}\n\nconst defaultDOMPurifyConfig: DOMPurify.Config = {\n  FORBID_TAGS: ['style'],\n  FORBID_ATTR: ['style']\n}\n\nexport function getHTML(\n  parent: ParentNode,\n  {\n    mode = 'innerHTML',\n    selector,\n    transform,\n    host,\n    config = defaultDOMPurifyConfig\n  }: GetHTMLConfig = {}\n): string {\n  const node = selector\n    ? parent.querySelector<HTMLElement>(selector)\n    : (parent as HTMLElement)\n  if (!node) {\n    return ''\n  }\n\n  if (host) {\n    const fillLink = (el: HTMLElement) => {\n      if (el.getAttribute('href')) {\n        el.setAttribute('href', getFullLink(host!, el, 'href'))\n      }\n      if (el.getAttribute('src')) {\n        el.setAttribute('src', getFullLink(host!, el, 'src'))\n      }\n      if (isInternalPage() && el.getAttribute('srcset')) {\n        el.setAttribute(\n          'srcset',\n          el\n            .getAttribute('srcset')!\n            .replace(/(,| |^)\\/\\//g, (_, head) => head + 'https://')\n        )\n      }\n    }\n\n    if (isTagName(node, 'a') || isTagName(node, 'img')) {\n      fillLink(node)\n    }\n    node.querySelectorAll('a').forEach(fillLink)\n    node.querySelectorAll('img').forEach(fillLink)\n  }\n\n  const fragment = DOMPurify.sanitize(node, {\n    ...config,\n    RETURN_DOM_FRAGMENT: true\n  })\n\n  const content = fragment.firstChild ? fragment.firstChild[mode] : ''\n\n  return transform ? transform(content) : content\n}\n\nexport function getInnerHTML(\n  host: string,\n  parent: ParentNode,\n  selectorOrConfig: string | Omit<GetHTMLConfig, 'mode' | 'host'> = {}\n) {\n  return getHTML(\n    parent,\n    typeof selectorOrConfig === 'string'\n      ? { selector: selectorOrConfig, host, mode: 'innerHTML' }\n      : { ...selectorOrConfig, host, mode: 'innerHTML' }\n  )\n}\n\nexport function getOuterHTML(\n  host: string,\n  parent: ParentNode,\n  selectorOrConfig: string | Omit<GetHTMLConfig, 'mode' | 'host'> = {}\n) {\n  return getHTML(\n    parent,\n    typeof selectorOrConfig === 'string'\n      ? { selector: selectorOrConfig, host, mode: 'outerHTML' }\n      : { ...selectorOrConfig, host, mode: 'outerHTML' }\n  )\n}\n\n/**\n * Remove a child node from a parent node\n */\nexport function removeChild(parent: ParentNode, selector: string) {\n  const child = parent.querySelector(selector)\n  if (child) {\n    child.remove()\n  }\n}\n\n/**\n * Remove all the matching child nodes from a parent node\n */\nexport function removeChildren(parent: ParentNode, selector: string) {\n  parent.querySelectorAll(selector).forEach(el => el.remove())\n}\n\n/**\n * HEX string to normal string\n */\nexport function decodeHEX(text: string): string {\n  return text.replace(/\\\\x([0-9A-Fa-f]{2})/g, (m, p1) =>\n    String.fromCharCode(parseInt(p1, 16))\n  )\n}\n\n/**\n * Will jump to the website instead of searching\n * when clicking on the dict panel\n */\nexport function externalLink($a: HTMLElement) {\n  $a.setAttribute('target', '_blank')\n  $a.setAttribute('rel', 'nofollow noopener noreferrer')\n}\n\nexport function getFullLink(host: string, el: Element, attr: string): string {\n  if (host.endsWith('/')) {\n    host = host.slice(0, -1)\n  }\n\n  const protocol = host.startsWith('https') ? 'https:' : 'http:'\n\n  const link = el.getAttribute(attr)\n  if (!link) {\n    return ''\n  }\n\n  if (/^[a-zA-Z0-9]+:/.test(link)) {\n    return link\n  }\n\n  if (link.startsWith('//')) {\n    return protocol + link\n  }\n\n  if (/^.?\\/+/.test(link)) {\n    return host + '/' + link.replace(/^.?\\/+/, '')\n  }\n\n  return host + '/' + link\n}\n\n/**\n * Horizontally scroll a list of items\n * React event listener doesn't support passive arguemnt.\n */\nexport const useHorizontalScroll = <T extends HTMLElement>() => {\n  const [onWheel, onWHeel$] = useObservableCallback(_useHorizontalScrollOnWheel)\n  useSubscription(onWHeel$)\n\n  const tabsRef = useRef<T>(null)\n  useEffect(() => {\n    if (tabsRef.current) {\n      // take the node out for cleaning up\n      const node = tabsRef.current\n      node.addEventListener('wheel', onWheel, { passive: false })\n      return () => {\n        node.removeEventListener('wheel', onWheel)\n      }\n    }\n  }, [tabsRef.current])\n\n  return tabsRef\n}\nfunction _useHorizontalScrollOnWheel(event$: Observable<WheelEvent>) {\n  return event$.pipe(\n    map(e => {\n      e.stopPropagation()\n      e.preventDefault()\n      return [e.currentTarget, e.deltaY] as [HTMLElement, number]\n    }),\n    debounceTime(80),\n    tap(([node, deltaY]) => {\n      node.scrollBy({\n        left: deltaY > 0 ? 250 : -250,\n        behavior: 'smooth'\n      })\n    })\n  )\n}\n"
  },
  {
    "path": "src/components/dictionaries/hjdict/View.tsx",
    "content": "import React, { FC } from 'react'\nimport { HjdictResult, HjdictResultLex, HjdictResultRelated } from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictHjDict: FC<ViewPorps<HjdictResult>> = props =>\n  props.result.type === 'lex' ? (\n    <Lex {...props} />\n  ) : props.result.type === 'related' ? (\n    <Related {...props} />\n  ) : null\n\nexport default DictHjDict\n\nfunction Lex(props: ViewPorps<HjdictResult>) {\n  const { header, entries } = props.result as HjdictResultLex\n  return (\n    <div className=\"dictHjdict-Entry\" onClick={handleClick}>\n      <LangSelect {...props} />\n      {header && (\n        <StrElm tag=\"header\" className=\"word-details-header\" html={header} />\n      )}\n      {entries.map((entry, i) => (\n        <StrElm key={i} html={entry} />\n      ))}\n    </div>\n  )\n}\n\nfunction Related(props: ViewPorps<HjdictResult>) {\n  const { content } = props.result as HjdictResultRelated\n  return (\n    <div>\n      <LangSelect {...props} />\n      <StrElm className=\"dictHjdict-Entry\" html={content} />\n    </div>\n  )\n}\n\nconst langSelectList = ['w', 'jp/cj', 'jp/jc', 'kr', 'fr', 'de', 'es']\n\nfunction LangSelect(props: ViewPorps<HjdictResult>) {\n  const { langCode } = props.result\n  const { t } = useTranslate('dicts')\n\n  return (\n    <select\n      value={langCode}\n      onChange={e =>\n        props.searchText({\n          id: 'hjdict',\n          payload: { langCode: e.target.value }\n        })\n      }\n    >\n      {langSelectList.map(lang => (\n        <option key={lang} value={lang}>\n          {t(`hjdict.options.chsas-${lang}`)}\n        </option>\n      ))}\n    </select>\n  )\n}\n\nfunction handleClick(e: React.MouseEvent<HTMLElement>): void {\n  const $tab = getWordDetailsTab(e.target)\n  if ($tab) {\n    if ($tab.classList.contains('word-details-tab-active')) {\n      return\n    }\n    const container = e.currentTarget\n    if (container) {\n      const index = +($tab.dataset.categories || '0')\n\n      const $panes = container.querySelectorAll('.word-details-pane')\n\n      container.querySelectorAll('.word-details-tab').forEach(($tab, i) => {\n        if (i === index) {\n          $tab.classList.add('word-details-tab-active')\n          $panes[i].classList.add('word-details-pane-active')\n        } else {\n          $tab.classList.remove('word-details-tab-active')\n          $panes[i].classList.remove('word-details-pane-active')\n        }\n      })\n    }\n  }\n}\n\nfunction getWordDetailsTab(target: any): HTMLElement | null {\n  for (let el = target; el; el = el.parentElement) {\n    if (el.classList && el.classList.contains('word-details-tab')) {\n      return el\n    }\n  }\n  return null\n}\n"
  },
  {
    "path": "src/components/dictionaries/hjdict/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Hu Jiang\",\n    \"zh-CN\": \"沪江小D\",\n    \"zh-TW\": \"沪江小D\"\n  },\n  \"options\": {\n    \"engas\": {\n      \"en\": \"Treat English as\",\n      \"zh-CN\": \"对全英文使用词典\",\n      \"zh-TW\": \"對全英文使用字典\"\n    },\n    \"chsas\": {\n      \"en\": \"Treat Chinese as\",\n      \"zh-CN\": \"对全中文使用词典\",\n      \"zh-TW\": \"對全中文使用字典\"\n    },\n    \"uas\": {\n      \"en\": \"Treat 'ü' as\",\n      \"zh-CN\": \"将 'ü' 字符当作\",\n      \"zh-TW\": \"把 'ü' 字元當成\"\n    },\n    \"aas\": {\n      \"en\": \"Treat 'ä' as\",\n      \"zh-CN\": \"将 'ä' 字符当作\",\n      \"zh-TW\": \"把 'ä' 字元當成\"\n    },\n    \"eas\": {\n      \"en\": \"Treat 'é' as\",\n      \"zh-CN\": \"将 'é' 字符当作\",\n      \"zh-TW\": \"把 'é' 字元當成\"\n    },\n    \"related\": {\n      \"en\": \"Show related results\",\n      \"zh-CN\": \"失败时显示备选\",\n      \"zh-TW\": \"失敗時顯示備選\"\n    },\n    \"chsas-jp/cj\": {\n      \"en\": \"中 → 日\",\n      \"zh-CN\": \"中 → 日\",\n      \"zh-TW\": \"中 → 日\"\n    },\n    \"chsas-jp/jc\": {\n      \"en\": \"日 → 中\",\n      \"zh-CN\": \"日 → 中\",\n      \"zh-TW\": \"日 → 中\"\n    },\n    \"chsas-kr\": {\n      \"en\": \"Korean\",\n      \"zh-CN\": \"韩文\",\n      \"zh-TW\": \"韓文\"\n    },\n    \"chsas-w\": {\n      \"en\": \"English\",\n      \"zh-CN\": \"英文\",\n      \"zh-TW\": \"英文\"\n    },\n    \"chsas-fr\": {\n      \"en\": \"French\",\n      \"zh-CN\": \"法文\",\n      \"zh-TW\": \"法文\"\n    },\n    \"chsas-de\": {\n      \"en\": \"Deutsch\",\n      \"zh-CN\": \"德文\",\n      \"zh-TW\": \"德文\"\n    },\n    \"chsas-es\": {\n      \"en\": \"Spanish\",\n      \"zh-CN\": \"西班牙文\",\n      \"zh-TW\": \"西班牙文\"\n    },\n    \"engas-w\": {\n      \"en\": \"English\",\n      \"zh-CN\": \"英文\",\n      \"zh-TW\": \"英文\"\n    },\n    \"engas-fr\": {\n      \"en\": \"French\",\n      \"zh-CN\": \"法文\",\n      \"zh-TW\": \"法文\"\n    },\n    \"engas-de\": {\n      \"en\": \"Deutsch\",\n      \"zh-CN\": \"德文\",\n      \"zh-TW\": \"德文\"\n    },\n    \"engas-es\": {\n      \"en\": \"Spanish\",\n      \"zh-CN\": \"西班牙文\",\n      \"zh-TW\": \"西班牙文\"\n    },\n    \"uas-fr\": {\n      \"en\": \"French\",\n      \"zh-CN\": \"法文\",\n      \"zh-TW\": \"法文\"\n    },\n    \"uas-de\": {\n      \"en\": \"Deutsch\",\n      \"zh-CN\": \"德文\",\n      \"zh-TW\": \"德文\"\n    },\n    \"uas-es\": {\n      \"en\": \"Spanish\",\n      \"zh-CN\": \"西班牙文\",\n      \"zh-TW\": \"西班牙文\"\n    },\n    \"aas-fr\": {\n      \"en\": \"French\",\n      \"zh-CN\": \"法文\",\n      \"zh-TW\": \"法文\"\n    },\n    \"aas-de\": {\n      \"en\": \"Deutsch\",\n      \"zh-CN\": \"德文\",\n      \"zh-TW\": \"德文\"\n    },\n    \"eas-fr\": {\n      \"en\": \"French\",\n      \"zh-CN\": \"法文\",\n      \"zh-TW\": \"法文\"\n    },\n    \"eas-es\": {\n      \"en\": \"Spanish\",\n      \"zh-CN\": \"西班牙文\",\n      \"zh-TW\": \"西班牙文\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/hjdict/_style.shadow.scss",
    "content": ".ui-helper-hidden {\n  display: none;\n}\n\n.ui-helper-hidden-accessible {\n  border: 0;\n  clip: rect(0 0 0 0);\n  height: 1px;\n  margin: -1px;\n  overflow: hidden;\n  padding: 0;\n  position: absolute;\n  width: 1px;\n}\n\n.ui-helper-reset {\n  margin: 0;\n  padding: 0;\n  border: 0;\n  outline: 0;\n  line-height: 1.3;\n  text-decoration: none;\n  font-size: 100%;\n  list-style: none;\n}\n\n.ui-helper-clearfix:after,\n.ui-helper-clearfix:before {\n  content: '';\n  display: table;\n  border-collapse: collapse;\n}\n\n.ui-helper-clearfix:after {\n  clear: both;\n}\n\n.ui-helper-zfix {\n  width: 100%;\n  height: 100%;\n  top: 0;\n  left: 0;\n  position: absolute;\n  opacity: 0;\n  filter: Alpha(Opacity=0);\n}\n\n.ui-front {\n  z-index: 100;\n}\n\n.ui-state-disabled {\n  cursor: default !important;\n  pointer-events: none;\n}\n\n.ui-icon {\n  display: inline-block;\n  vertical-align: middle;\n  margin-top: -0.25em;\n  position: relative;\n  text-indent: -99999px;\n  overflow: hidden;\n  background-repeat: no-repeat;\n}\n\n.ui-widget-icon-block {\n  left: 50%;\n  margin-left: -8px;\n  display: block;\n}\n\n.ui-widget-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n}\n\n/*!\n   * jQuery UI Button 1.12.1\n   * http://jqueryui.com\n   *\n   * Copyright jQuery Foundation and other contributors\n   * Released under the MIT license.\n   * http://jquery.org/license\n   *\n   * http://api.jqueryui.com/button/#theming\n   */\n.ui-button {\n  padding: 0.4em 1em;\n  display: inline-block;\n  position: relative;\n  line-height: normal;\n  margin-right: 0.1em;\n  cursor: pointer;\n  vertical-align: middle;\n  text-align: center;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n  overflow: visible;\n}\n\n.ui-button,\n.ui-button:active,\n.ui-button:hover,\n.ui-button:link,\n.ui-button:visited {\n  text-decoration: none;\n}\n\n.ui-button-icon-only {\n  width: 2em;\n  -webkit-box-sizing: border-box;\n  box-sizing: border-box;\n  text-indent: -9999px;\n  white-space: nowrap;\n}\n\ninput.ui-button.ui-button-icon-only {\n  text-indent: 0;\n}\n\n.ui-button-icon-only .ui-icon {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  margin-top: -8px;\n  margin-left: -8px;\n}\n\n.ui-button.ui-icon-notext .ui-icon {\n  padding: 0;\n  width: 2.1em;\n  height: 2.1em;\n  text-indent: -9999px;\n  white-space: nowrap;\n}\n\ninput.ui-button.ui-icon-notext .ui-icon {\n  width: auto;\n  height: auto;\n  text-indent: 0;\n  white-space: normal;\n  padding: 0.4em 1em;\n}\n\nbutton.ui-button::-moz-focus-inner,\ninput.ui-button::-moz-focus-inner {\n  border: 0;\n  padding: 0;\n}\n\n/*!\n   * jQuery UI CSS Framework 1.12.1\n   * http://jqueryui.com\n   *\n   * Copyright jQuery Foundation and other contributors\n   * Released under the MIT license.\n   * http://jquery.org/license\n   *\n   * http://api.jqueryui.com/category/theming/\n   *\n   * To view and modify this theme, visit http://jqueryui.com/themeroller/\n   */\n.ui-widget {\n  font-family: Arial, Helvetica, sans-serif;\n}\n\n.ui-widget,\n.ui-widget .ui-widget {\n  font-size: 1em;\n}\n\n.ui-widget button,\n.ui-widget input,\n.ui-widget select,\n.ui-widget textarea {\n  font-family: Arial, Helvetica, sans-serif;\n  font-size: 1em;\n}\n\n.ui-widget.ui-widget-content {\n  border: 1px solid #c5c5c5;\n}\n\n.ui-widget-content {\n  border: 1px solid #ddd;\n  background: #fff;\n}\n\n.ui-widget-header {\n  border: 1px solid #ddd;\n  background: #e9e9e9;\n  font-weight: 700;\n}\n\n.ui-button,\n.ui-state-default,\n.ui-widget-content .ui-state-default,\n.ui-widget-header .ui-state-default,\nhtml .ui-button.ui-state-disabled:active,\nhtml .ui-button.ui-state-disabled:hover {\n  border: 1px solid #c5c5c5;\n  background: #f6f6f6;\n  font-weight: 400;\n  color: #454545;\n}\n\n.ui-button,\n.ui-state-default a,\n.ui-state-default a:link,\n.ui-state-default a:visited,\na.ui-button,\na:link.ui-button,\na:visited.ui-button {\n  color: #454545;\n  text-decoration: none;\n}\n\n.ui-button:focus,\n.ui-button:hover,\n.ui-state-focus,\n.ui-state-hover,\n.ui-widget-content .ui-state-focus,\n.ui-widget-content .ui-state-hover,\n.ui-widget-header .ui-state-focus,\n.ui-widget-header .ui-state-hover {\n  border: 1px solid #ccc;\n  background: #ededed;\n  font-weight: 400;\n  color: #2b2b2b;\n}\n\n.ui-state-focus a,\n.ui-state-focus a:hover,\n.ui-state-focus a:link,\n.ui-state-focus a:visited,\n.ui-state-hover a,\n.ui-state-hover a:hover,\n.ui-state-hover a:link,\n.ui-state-hover a:visited,\na.ui-button:focus,\na.ui-button:hover {\n  color: #2b2b2b;\n  text-decoration: none;\n}\n\n.ui-visual-focus {\n  -webkit-box-shadow: 0 0 3px 1px #5e9ed6;\n  box-shadow: 0 0 3px 1px #5e9ed6;\n}\n\n.ui-button.ui-state-active:hover,\n.ui-button:active,\n.ui-state-active,\n.ui-widget-content .ui-state-active,\n.ui-widget-header .ui-state-active,\na.ui-button:active {\n  border: 1px solid #003eff;\n  background: #007fff;\n  font-weight: 400;\n  color: #fff;\n}\n\n.ui-icon-background,\n.ui-state-active .ui-icon-background {\n  border: #003eff;\n  background-color: #fff;\n}\n\n.ui-state-active a,\n.ui-state-active a:link,\n.ui-state-active a:visited {\n  color: #fff;\n  text-decoration: none;\n}\n\n.ui-state-highlight,\n.ui-widget-content .ui-state-highlight,\n.ui-widget-header .ui-state-highlight {\n  border: 1px solid #dad55e;\n  background: #fffa90;\n  color: #777620;\n}\n\n.ui-state-checked {\n  border: 1px solid #dad55e;\n  background: #fffa90;\n}\n\n.ui-state-highlight a,\n.ui-widget-content .ui-state-highlight a,\n.ui-widget-header .ui-state-highlight a {\n  color: #777620;\n}\n\n.ui-state-error,\n.ui-widget-content .ui-state-error,\n.ui-widget-header .ui-state-error {\n  border: 1px solid #f1a899;\n  background: #fddfdf;\n  color: #5f3f3f;\n}\n\n.ui-state-error-text,\n.ui-state-error a,\n.ui-widget-content .ui-state-error-text,\n.ui-widget-content .ui-state-error a,\n.ui-widget-header .ui-state-error-text,\n.ui-widget-header .ui-state-error a {\n  color: #5f3f3f;\n}\n\n.ui-priority-primary,\n.ui-widget-content .ui-priority-primary,\n.ui-widget-header .ui-priority-primary {\n  font-weight: 700;\n}\n\n.ui-priority-secondary,\n.ui-widget-content .ui-priority-secondary,\n.ui-widget-header .ui-priority-secondary {\n  opacity: 0.7;\n  filter: Alpha(Opacity=70);\n  font-weight: 400;\n}\n\n.ui-state-disabled,\n.ui-widget-content .ui-state-disabled,\n.ui-widget-header .ui-state-disabled {\n  opacity: 0.35;\n  filter: Alpha(Opacity=35);\n  background-image: none;\n}\n\n.ui-state-disabled .ui-icon {\n  filter: Alpha(Opacity=35);\n}\n\n.ui-icon {\n  width: 16px;\n  height: 16px;\n}\n\n.ui-icon,\n.ui-widget-content .ui-icon {\n  background-image: url(https://www.hjdict.com/img/ui-icons_444444_256x240-a4c73.png);\n}\n\n.ui-widget-header .ui-icon {\n  background-image: url(https://www.hjdict.com/img/ui-icons_444444_256x240-a4c73.png);\n}\n\n.ui-button:focus .ui-icon,\n.ui-button:hover .ui-icon,\n.ui-state-focus .ui-icon,\n.ui-state-hover .ui-icon {\n  background-image: url(https://www.hjdict.com/img/ui-icons_555555_256x240-97136.png);\n}\n\n.ui-button:active .ui-icon,\n.ui-state-active .ui-icon {\n  background-image: url(https://www.hjdict.com/img/ui-icons_ffffff_256x240-bf272.png);\n}\n\n.ui-button .ui-state-highlight.ui-icon,\n.ui-state-highlight .ui-icon {\n  background-image: url(https://www.hjdict.com/img/ui-icons_777620_256x240-208a2.png);\n}\n\n.ui-state-error-text .ui-icon,\n.ui-state-error .ui-icon {\n  background-image: url(https://www.hjdict.com/img/ui-icons_cc0000_256x240-0de3b.png);\n}\n\n.ui-button .ui-icon {\n  background-image: url(https://www.hjdict.com/img/ui-icons_777777_256x240-73a1f.png);\n}\n\n.ui-icon-blank {\n  background-position: 16px 16px;\n}\n\n.ui-icon-caret-1-n {\n  background-position: 0 0;\n}\n\n.ui-icon-caret-1-ne {\n  background-position: -16px 0;\n}\n\n.ui-icon-caret-1-e {\n  background-position: -32px 0;\n}\n\n.ui-icon-caret-1-se {\n  background-position: -48px 0;\n}\n\n.ui-icon-caret-1-s {\n  background-position: -65px 0;\n}\n\n.ui-icon-caret-1-sw {\n  background-position: -80px 0;\n}\n\n.ui-icon-caret-1-w {\n  background-position: -96px 0;\n}\n\n.ui-icon-caret-1-nw {\n  background-position: -112px 0;\n}\n\n.ui-icon-caret-2-n-s {\n  background-position: -128px 0;\n}\n\n.ui-icon-caret-2-e-w {\n  background-position: -144px 0;\n}\n\n.ui-icon-triangle-1-n {\n  background-position: 0 -16px;\n}\n\n.ui-icon-triangle-1-ne {\n  background-position: -16px -16px;\n}\n\n.ui-icon-triangle-1-e {\n  background-position: -32px -16px;\n}\n\n.ui-icon-triangle-1-se {\n  background-position: -48px -16px;\n}\n\n.ui-icon-triangle-1-s {\n  background-position: -65px -16px;\n}\n\n.ui-icon-triangle-1-sw {\n  background-position: -80px -16px;\n}\n\n.ui-icon-triangle-1-w {\n  background-position: -96px -16px;\n}\n\n.ui-icon-triangle-1-nw {\n  background-position: -112px -16px;\n}\n\n.ui-icon-triangle-2-n-s {\n  background-position: -128px -16px;\n}\n\n.ui-icon-triangle-2-e-w {\n  background-position: -144px -16px;\n}\n\n.ui-icon-arrow-1-n {\n  background-position: 0 -32px;\n}\n\n.ui-icon-arrow-1-ne {\n  background-position: -16px -32px;\n}\n\n.ui-icon-arrow-1-e {\n  background-position: -32px -32px;\n}\n\n.ui-icon-arrow-1-se {\n  background-position: -48px -32px;\n}\n\n.ui-icon-arrow-1-s {\n  background-position: -65px -32px;\n}\n\n.ui-icon-arrow-1-sw {\n  background-position: -80px -32px;\n}\n\n.ui-icon-arrow-1-w {\n  background-position: -96px -32px;\n}\n\n.ui-icon-arrow-1-nw {\n  background-position: -112px -32px;\n}\n\n.ui-icon-arrow-2-n-s {\n  background-position: -128px -32px;\n}\n\n.ui-icon-arrow-2-ne-sw {\n  background-position: -144px -32px;\n}\n\n.ui-icon-arrow-2-e-w {\n  background-position: -160px -32px;\n}\n\n.ui-icon-arrow-2-se-nw {\n  background-position: -176px -32px;\n}\n\n.ui-icon-arrowstop-1-n {\n  background-position: -192px -32px;\n}\n\n.ui-icon-arrowstop-1-e {\n  background-position: -208px -32px;\n}\n\n.ui-icon-arrowstop-1-s {\n  background-position: -224px -32px;\n}\n\n.ui-icon-arrowstop-1-w {\n  background-position: -240px -32px;\n}\n\n.ui-icon-arrowthick-1-n {\n  background-position: 1px -48px;\n}\n\n.ui-icon-arrowthick-1-ne {\n  background-position: -16px -48px;\n}\n\n.ui-icon-arrowthick-1-e {\n  background-position: -32px -48px;\n}\n\n.ui-icon-arrowthick-1-se {\n  background-position: -48px -48px;\n}\n\n.ui-icon-arrowthick-1-s {\n  background-position: -64px -48px;\n}\n\n.ui-icon-arrowthick-1-sw {\n  background-position: -80px -48px;\n}\n\n.ui-icon-arrowthick-1-w {\n  background-position: -96px -48px;\n}\n\n.ui-icon-arrowthick-1-nw {\n  background-position: -112px -48px;\n}\n\n.ui-icon-arrowthick-2-n-s {\n  background-position: -128px -48px;\n}\n\n.ui-icon-arrowthick-2-ne-sw {\n  background-position: -144px -48px;\n}\n\n.ui-icon-arrowthick-2-e-w {\n  background-position: -160px -48px;\n}\n\n.ui-icon-arrowthick-2-se-nw {\n  background-position: -176px -48px;\n}\n\n.ui-icon-arrowthickstop-1-n {\n  background-position: -192px -48px;\n}\n\n.ui-icon-arrowthickstop-1-e {\n  background-position: -208px -48px;\n}\n\n.ui-icon-arrowthickstop-1-s {\n  background-position: -224px -48px;\n}\n\n.ui-icon-arrowthickstop-1-w {\n  background-position: -240px -48px;\n}\n\n.ui-icon-arrowreturnthick-1-w {\n  background-position: 0 -64px;\n}\n\n.ui-icon-arrowreturnthick-1-n {\n  background-position: -16px -64px;\n}\n\n.ui-icon-arrowreturnthick-1-e {\n  background-position: -32px -64px;\n}\n\n.ui-icon-arrowreturnthick-1-s {\n  background-position: -48px -64px;\n}\n\n.ui-icon-arrowreturn-1-w {\n  background-position: -64px -64px;\n}\n\n.ui-icon-arrowreturn-1-n {\n  background-position: -80px -64px;\n}\n\n.ui-icon-arrowreturn-1-e {\n  background-position: -96px -64px;\n}\n\n.ui-icon-arrowreturn-1-s {\n  background-position: -112px -64px;\n}\n\n.ui-icon-arrowrefresh-1-w {\n  background-position: -128px -64px;\n}\n\n.ui-icon-arrowrefresh-1-n {\n  background-position: -144px -64px;\n}\n\n.ui-icon-arrowrefresh-1-e {\n  background-position: -160px -64px;\n}\n\n.ui-icon-arrowrefresh-1-s {\n  background-position: -176px -64px;\n}\n\n.ui-icon-arrow-4 {\n  background-position: 0 -80px;\n}\n\n.ui-icon-arrow-4-diag {\n  background-position: -16px -80px;\n}\n\n.ui-icon-extlink {\n  background-position: -32px -80px;\n}\n\n.ui-icon-newwin {\n  background-position: -48px -80px;\n}\n\n.ui-icon-refresh {\n  background-position: -64px -80px;\n}\n\n.ui-icon-shuffle {\n  background-position: -80px -80px;\n}\n\n.ui-icon-transfer-e-w {\n  background-position: -96px -80px;\n}\n\n.ui-icon-transferthick-e-w {\n  background-position: -112px -80px;\n}\n\n.ui-icon-folder-collapsed {\n  background-position: 0 -96px;\n}\n\n.ui-icon-folder-open {\n  background-position: -16px -96px;\n}\n\n.ui-icon-document {\n  background-position: -32px -96px;\n}\n\n.ui-icon-document-b {\n  background-position: -48px -96px;\n}\n\n.ui-icon-note {\n  background-position: -64px -96px;\n}\n\n.ui-icon-mail-closed {\n  background-position: -80px -96px;\n}\n\n.ui-icon-mail-open {\n  background-position: -96px -96px;\n}\n\n.ui-icon-suitcase {\n  background-position: -112px -96px;\n}\n\n.ui-icon-comment {\n  background-position: -128px -96px;\n}\n\n.ui-icon-person {\n  background-position: -144px -96px;\n}\n\n.ui-icon-print {\n  background-position: -160px -96px;\n}\n\n.ui-icon-trash {\n  background-position: -176px -96px;\n}\n\n.ui-icon-locked {\n  background-position: -192px -96px;\n}\n\n.ui-icon-unlocked {\n  background-position: -208px -96px;\n}\n\n.ui-icon-bookmark {\n  background-position: -224px -96px;\n}\n\n.ui-icon-tag {\n  background-position: -240px -96px;\n}\n\n.ui-icon-home {\n  background-position: 0 -112px;\n}\n\n.ui-icon-flag {\n  background-position: -16px -112px;\n}\n\n.ui-icon-calendar {\n  background-position: -32px -112px;\n}\n\n.ui-icon-cart {\n  background-position: -48px -112px;\n}\n\n.ui-icon-pencil {\n  background-position: -64px -112px;\n}\n\n.ui-icon-clock {\n  background-position: -80px -112px;\n}\n\n.ui-icon-disk {\n  background-position: -96px -112px;\n}\n\n.ui-icon-calculator {\n  background-position: -112px -112px;\n}\n\n.ui-icon-zoomin {\n  background-position: -128px -112px;\n}\n\n.ui-icon-zoomout {\n  background-position: -144px -112px;\n}\n\n.ui-icon-search {\n  background-position: -160px -112px;\n}\n\n.ui-icon-wrench {\n  background-position: -176px -112px;\n}\n\n.ui-icon-gear {\n  background-position: -192px -112px;\n}\n\n.ui-icon-heart {\n  background-position: -208px -112px;\n}\n\n.ui-icon-star {\n  background-position: -224px -112px;\n}\n\n.ui-icon-link {\n  background-position: -240px -112px;\n}\n\n.ui-icon-cancel {\n  background-position: 0 -128px;\n}\n\n.ui-icon-plus {\n  background-position: -16px -128px;\n}\n\n.ui-icon-plusthick {\n  background-position: -32px -128px;\n}\n\n.ui-icon-minus {\n  background-position: -48px -128px;\n}\n\n.ui-icon-minusthick {\n  background-position: -64px -128px;\n}\n\n.ui-icon-close {\n  background-position: -80px -128px;\n}\n\n.ui-icon-closethick {\n  background-position: -96px -128px;\n}\n\n.ui-icon-key {\n  background-position: -112px -128px;\n}\n\n.ui-icon-lightbulb {\n  background-position: -128px -128px;\n}\n\n.ui-icon-scissors {\n  background-position: -144px -128px;\n}\n\n.ui-icon-clipboard {\n  background-position: -160px -128px;\n}\n\n.ui-icon-copy {\n  background-position: -176px -128px;\n}\n\n.ui-icon-contact {\n  background-position: -192px -128px;\n}\n\n.ui-icon-image {\n  background-position: -208px -128px;\n}\n\n.ui-icon-video {\n  background-position: -224px -128px;\n}\n\n.ui-icon-script {\n  background-position: -240px -128px;\n}\n\n.ui-icon-alert {\n  background-position: 0 -144px;\n}\n\n.ui-icon-info {\n  background-position: -16px -144px;\n}\n\n.ui-icon-notice {\n  background-position: -32px -144px;\n}\n\n.ui-icon-help {\n  background-position: -48px -144px;\n}\n\n.ui-icon-check {\n  background-position: -64px -144px;\n}\n\n.ui-icon-bullet {\n  background-position: -80px -144px;\n}\n\n.ui-icon-radio-on {\n  background-position: -96px -144px;\n}\n\n.ui-icon-radio-off {\n  background-position: -112px -144px;\n}\n\n.ui-icon-pin-w {\n  background-position: -128px -144px;\n}\n\n.ui-icon-pin-s {\n  background-position: -144px -144px;\n}\n\n.ui-icon-play {\n  background-position: 0 -160px;\n}\n\n.ui-icon-pause {\n  background-position: -16px -160px;\n}\n\n.ui-icon-seek-next {\n  background-position: -32px -160px;\n}\n\n.ui-icon-seek-prev {\n  background-position: -48px -160px;\n}\n\n.ui-icon-seek-end {\n  background-position: -64px -160px;\n}\n\n.ui-icon-seek-first,\n.ui-icon-seek-start {\n  background-position: -80px -160px;\n}\n\n.ui-icon-stop {\n  background-position: -96px -160px;\n}\n\n.ui-icon-eject {\n  background-position: -112px -160px;\n}\n\n.ui-icon-volume-off {\n  background-position: -128px -160px;\n}\n\n.ui-icon-volume-on {\n  background-position: -144px -160px;\n}\n\n.ui-icon-power {\n  background-position: 0 -176px;\n}\n\n.ui-icon-signal-diag {\n  background-position: -16px -176px;\n}\n\n.ui-icon-signal {\n  background-position: -32px -176px;\n}\n\n.ui-icon-battery-0 {\n  background-position: -48px -176px;\n}\n\n.ui-icon-battery-1 {\n  background-position: -64px -176px;\n}\n\n.ui-icon-battery-2 {\n  background-position: -80px -176px;\n}\n\n.ui-icon-battery-3 {\n  background-position: -96px -176px;\n}\n\n.ui-icon-circle-plus {\n  background-position: 0 -192px;\n}\n\n.ui-icon-circle-minus {\n  background-position: -16px -192px;\n}\n\n.ui-icon-circle-close {\n  background-position: -32px -192px;\n}\n\n.ui-icon-circle-triangle-e {\n  background-position: -48px -192px;\n}\n\n.ui-icon-circle-triangle-s {\n  background-position: -64px -192px;\n}\n\n.ui-icon-circle-triangle-w {\n  background-position: -80px -192px;\n}\n\n.ui-icon-circle-triangle-n {\n  background-position: -96px -192px;\n}\n\n.ui-icon-circle-arrow-e {\n  background-position: -112px -192px;\n}\n\n.ui-icon-circle-arrow-s {\n  background-position: -128px -192px;\n}\n\n.ui-icon-circle-arrow-w {\n  background-position: -144px -192px;\n}\n\n.ui-icon-circle-arrow-n {\n  background-position: -160px -192px;\n}\n\n.ui-icon-circle-zoomin {\n  background-position: -176px -192px;\n}\n\n.ui-icon-circle-zoomout {\n  background-position: -192px -192px;\n}\n\n.ui-icon-circle-check {\n  background-position: -208px -192px;\n}\n\n.ui-icon-circlesmall-plus {\n  background-position: 0 -208px;\n}\n\n.ui-icon-circlesmall-minus {\n  background-position: -16px -208px;\n}\n\n.ui-icon-circlesmall-close {\n  background-position: -32px -208px;\n}\n\n.ui-icon-squaresmall-plus {\n  background-position: -48px -208px;\n}\n\n.ui-icon-squaresmall-minus {\n  background-position: -64px -208px;\n}\n\n.ui-icon-squaresmall-close {\n  background-position: -80px -208px;\n}\n\n.ui-icon-grip-dotted-vertical {\n  background-position: 0 -224px;\n}\n\n.ui-icon-grip-dotted-horizontal {\n  background-position: -16px -224px;\n}\n\n.ui-icon-grip-solid-vertical {\n  background-position: -32px -224px;\n}\n\n.ui-icon-grip-solid-horizontal {\n  background-position: -48px -224px;\n}\n\n.ui-icon-gripsmall-diagonal-se {\n  background-position: -64px -224px;\n}\n\n.ui-icon-grip-diagonal-se {\n  background-position: -80px -224px;\n}\n\n.ui-corner-all,\n.ui-corner-left,\n.ui-corner-tl,\n.ui-corner-top {\n  border-top-left-radius: 3px;\n}\n\n.ui-corner-all,\n.ui-corner-right,\n.ui-corner-top,\n.ui-corner-tr {\n  border-top-right-radius: 3px;\n}\n\n.ui-corner-all,\n.ui-corner-bl,\n.ui-corner-bottom,\n.ui-corner-left {\n  border-bottom-left-radius: 3px;\n}\n\n.ui-corner-all,\n.ui-corner-bottom,\n.ui-corner-br,\n.ui-corner-right {\n  border-bottom-right-radius: 3px;\n}\n\n.ui-widget-overlay {\n  background: #aaa;\n  opacity: 0.3;\n  filter: Alpha(Opacity=30);\n}\n\n.ui-widget-shadow {\n  -webkit-box-shadow: 0 0 5px #666;\n  box-shadow: 0 0 5px #666;\n}\n\n.ui-dialog {\n  display: none !important;\n}\n\n.ui-widget-overlay {\n  display: none;\n}\n\n.ui-checkboxradio-label {\n  display: none;\n}\n\n.ui-checkboxradio-radio-label {\n  display: none;\n}\n\n.ui-checkboxradio-disabled {\n  pointer-events: none;\n}\n\n.word-feedback {\n  display: none;\n}\n\n.pronounces {\n  font-size: 1em;\n  line-height: 1.2;\n}\n\n.pronounces .word-audio {\n  vertical-align: top;\n  margin-left: 2px;\n}\n\n.pronounces .word-audio-en {\n  margin-right: 30px;\n}\n\n.pronounces .word-audio-kr {\n  margin-right: 50px;\n}\n\n.pronounces .pronounce-value-en,\n.pronounces .pronounce-value-us {\n  font-family: Lucida Sans Unicode;\n}\n\n.pronounces .pronounce-value-jp {\n  font-family: ms gothic, arial, sans-serif;\n}\n\n.simple {\n  margin-top: 0.5em;\n}\n\n.simple > p {\n  margin: 0.5em 0;\n  line-height: 1.2;\n}\n\n.simple > h2 {\n  font-size: 1em;\n  font-weight: 400;\n  margin: 0.5em 0 0.5em -7px；;\n}\n\n.simple > ul > li {\n  margin-bottom: 0;\n}\n\n.simple > ul > li:last-child {\n  margin: 0;\n}\n\n.simple > ul > li > span {\n  display: inline-block;\n  text-align: left;\n  width: 14px;\n  line-height: 1.2;\n  margin-right: 8px;\n}\n\n.word-details .simple a {\n  display: inline-block;\n  margin-right: 16px;\n  color: #fff;\n}\n\n.word-details .simple a:hover {\n  text-decoration: underline;\n}\n\n.phrase-items {\n  counter-reset: eq;\n  margin: 0;\n  padding: 0 0 0 1em;\n}\n\n.phrase-items li {\n  line-height: 1.2em;\n  margin-bottom: 0.5em;\n  list-style-type: none;\n}\n\n.phrase-items li:before {\n  counter-increment: eq;\n  content: counter(eq) '.';\n  width: 22px;\n  float: left;\n}\n\n.phrase-items li:first-of-type:last-of-type:before {\n  display: none;\n}\n\n.phrase-def {\n  color: var(--color-font-grey);\n  margin-left: 10px;\n}\n\n.enen-groups dl {\n  counter-reset: eq;\n}\n\n.enen-groups dt {\n  margin-bottom: 0.5em;\n}\n\n.enen-groups dd {\n  line-height: 1.2;\n  margin: 0 0 0.5em 1em;\n}\n\n.enen-groups dd:before {\n  counter-increment: eq;\n  content: counter(eq) '.';\n  width: 22px;\n  float: left;\n}\n\n.enen-groups dd:first-of-type:last-of-type:before {\n  display: none;\n}\n\n.detail-source {\n  font-size: 1em;\n  line-height: 1.2;\n  margin-bottom: 0.5em;\n}\n\n.detail-source span {\n  font-size: 0.8em;\n  color: #fff;\n  line-height: 1.2;\n  display: inline-block;\n  border-radius: 2px;\n  text-align: center;\n  white-space: nowrap;\n  word-break: keep-all;\n  margin-left: 7px;\n  padding: 1px 3px;\n}\n\n.detail-source .collins-icon {\n  background: #c94444;\n}\n\n.detail-source .wys-icon {\n  background: #414585;\n}\n\n.detail-source .wjs-icon {\n  background: #287ae4;\n}\n\n.detail-source .sflep-icon {\n  color: #fff;\n  background: #1f9b88;\n}\n\n.detail-source .krcr-icon {\n  color: #fff;\n  width: 34px;\n  background: #c94444;\n}\n\n.detail-tags-en {\n  overflow: hidden;\n}\n\n.detail-tags-en li {\n  height: 16px;\n  line-height: 1.2;\n  background-color: #f0f0f0;\n  border-radius: 2px;\n  text-align: center;\n  color: var(--color-font-grey);\n  float: left;\n  font-size: 1em;\n  padding: 0 4px;\n  margin-right: 8px;\n}\n\n.detail .tags-word {\n  font-weight: 700;\n  font-size: 1.2em;\n  line-height: 1.2;\n  padding-right: 4px;\n}\n\n.detail-tags-jp {\n  margin: 0 0 12px;\n}\n\n.detail-tags-jp i {\n  color: var(--color-font-grey);\n  padding: 0 4px;\n  font-style: normal;\n}\n\n.detail-groups {\n  margin-top: 1em;\n}\n\n.detail-groups dl {\n  counter-reset: eq;\n  margin-bottom: 1em;\n}\n\n.detail-groups dt {\n  margin-bottom: 0.8em;\n  font-weight: 700;\n  line-height: 1.2;\n}\n\n.detail-groups dd {\n  margin: 0 0 1em 1em;\n}\n\n.detail-groups dd h3 {\n  font-size: 1em;\n  font-weight: 400;\n  line-height: 1.2;\n  margin: 0 0 0.5em;\n}\n\n.detail-groups dd h3:before {\n  counter-increment: eq;\n  content: counter(eq) '.';\n  display: block;\n  width: 22px;\n  float: left;\n}\n\n.detail-groups dd h3 p {\n  margin: 0 0 0 22px;\n}\n\n.detail-groups dd h3 p:first-child {\n  margin-bottom: 6px;\n}\n\n.detail-groups dd p {\n  margin-left: 22px;\n}\n\n.detail-groups dd:first-of-type:last-of-type h3:before {\n  display: none;\n}\n\n.detail-groups dd:first-of-type:last-of-type h3 p {\n  margin: 0;\n}\n\n.detail-groups dd:first-of-type:last-of-type ul {\n  margin-left: 0;\n}\n\n.detail-groups ul {\n  margin-left: 22px;\n}\n\n.detail-groups ul li {\n  color: var(--color-font-grey);\n  margin-bottom: 0.5em;\n}\n\n.detail-groups ul li p {\n  margin: 0 0 5px;\n  line-height: 1.2;\n}\n\n.detail-pron {\n  margin-left: 6px;\n  font-family: Lucida Sans Unicode;\n}\n\n.detail .def-sentence-from .def-sentence-to span {\n  vertical-align: top;\n  margin: 1px 0 2px 2px;\n  display: inline-block;\n}\n\n.detail .def-tags span {\n  border: 1px solid #333;\n  color: var(--color-font-grey);\n  border-radius: 2px;\n  display: inline-block;\n  line-height: 1.2em;\n  font-size: 1em;\n  width: 36px;\n  text-align: center;\n  margin-right: 8px;\n}\n\n.analyzes-title {\n  line-height: 1.2;\n  margin: 1.1em 0 1em;\n}\n\n.analyzes-title:first-of-type {\n  margin-top: 0;\n}\n\n.analyzes-items {\n  counter-reset: eq;\n}\n\n.analyzes-items li {\n  margin: 0 0 0.8em 1em;\n  line-height: 1.2;\n}\n\n.analyzes-items li p {\n  color: var(--color-font-grey);\n  margin: 2px 0 0 22px;\n}\n\n.analyzes-items li:before {\n  counter-increment: eq;\n  content: counter(eq) '.';\n  width: 22px;\n  float: left;\n}\n\n.inflections li {\n  line-height: 1.2em;\n  margin-bottom: 0.8em;\n}\n\n.inflections-item-attr {\n  display: inline-block;\n  margin-right: 10px;\n}\n\n.inflections-value {\n  color: var(--color-font-grey);\n}\n\n.sentences-items {\n  counter-reset: eq;\n}\n\n.sentences li {\n  margin-bottom: 14px;\n}\n\n.sentences li p {\n  line-height: 1.2;\n  margin: 0 0 6px;\n}\n\n.sentences-item-from:before {\n  counter-increment: eq;\n  content: counter(eq) '.';\n  width: 22px;\n  float: left;\n}\n\n.sentences-item-from span {\n  line-height: 1.2;\n  margin: 1px 0 2px 2px;\n}\n\n.sentences-item-to {\n  color: var(--color-font-grey);\n  padding-left: 22px;\n}\n\n.ant > p,\n.syn > p {\n  color: var(--color-font-grey);\n  margin: 0 0 6px;\n}\n\n.ant table,\n.syn table {\n  margin-bottom: 14px;\n}\n\n.ant td,\n.syn td {\n  line-height: 1.2;\n  padding-right: 50px;\n}\n\n.ant-single,\n.syn-single {\n  display: inline-block;\n  margin: 0 50px 14px 0;\n}\n\n.ant-single > span,\n.syn-single > span {\n  color: var(--color-font-grey);\n}\n\n.synant-content {\n  margin-top: 20px;\n}\n\n.synant p {\n  color: var(--color-font-grey);\n}\n\n.word-notfound,\n.word-suggestions {\n  min-height: 800px;\n  padding: 50px 30px;\n}\n\n.word-notfound h2,\n.word-suggestions h2 {\n  font-size: 2em;\n  font-weight: 400;\n}\n\n.word-suggestions h2 {\n  margin-bottom: 20px;\n}\n\n.word-suggestions ul {\n  font-size: 1.16em;\n}\n\n.word-suggestions ul li {\n  margin-bottom: 14px;\n}\n\n.word-suggestions a {\n  color: #2e94f7;\n}\n\n.word-suggestions a:hover {\n  text-decoration: underline;\n}\n\n.word-notfound {\n  text-align: center;\n  vertical-align: bottom;\n}\n\n.word-notfound:before {\n  content: '';\n  display: inline-block;\n  width: 54px;\n  height: 54px;\n  margin-right: 32px;\n  background: url(https://www.hjdict.com/img/icon-notfound@2x-bbd46.png)\n    no-repeat 0/100%;\n}\n\n.word-notfound-inner {\n  display: inline-block;\n  text-align: left;\n}\n\n.word-notfound h2 {\n  margin: 0 0 5px;\n}\n\n.word-notfound p {\n  margin: 0;\n  color: #a9a9a9;\n}\n\n.word-notfound p em {\n  font-style: normal;\n  font-weight: 700;\n}\n\n.word-details {\n  position: relative;\n}\n\n.word-details-header {\n  padding-top: 5px;\n}\n\n.word-details-header > p {\n  line-height: 1.2;\n  margin: 0 0 1em;\n}\n\n.word-details-header > p > span {\n  color: #2e94f7;\n}\n\n.word-details .redirection {\n  color: #fff;\n  line-height: 1.2;\n  margin: 0 0 20px;\n  opacity: 0.8;\n}\n\n.word-details-tab {\n  cursor: pointer;\n  display: inline-block;\n  margin: 0 10px 10px 0;\n  border-radius: 5px;\n  padding: 8px 10px;\n  white-space: nowrap;\n  word-break: keep-all;\n  color: #333;\n  background: #f5f8ff;\n}\n\n.word-details-tab h2 {\n  font-size: 1.5em;\n  line-height: 1.2;\n  margin: 0;\n  font-weight: 400;\n  display: inline-block;\n}\n\n.word-details-tab-active {\n  color: #fff;\n  background-color: #2e94f7;\n}\n\n.word-details-tab .pronounces {\n  display: inline-block;\n  line-height: 1.2;\n  vertical-align: top;\n  margin-left: 5px;\n}\n\n.word-details-pane {\n  display: none;\n}\n\n.word-details-pane-header {\n  margin-bottom: 1em;\n}\n\n.word-details-pane-header .word-text {\n  margin: 5px 0 0.1em;\n}\n\n.word-details-pane-header .word-text .add-to-scb-loading {\n  background: url(https://www.hjdict.com/img/loading-289f3.png) no-repeat 0 0 /\n    cover;\n  -webkit-animation: xd-loading 0.6s steps(8) infinite both;\n  animation: xd-loading 0.6s steps(8) infinite both;\n}\n\n.word-details-pane-header .word-text .add-to-scb-success {\n  background: url(data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAYAAAAehFoBAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTQ1N0M1MTRDMDRCMTFFNzlGNEY5NDYwNkM0QUE2NjciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTQ1N0M1MTVDMDRCMTFFNzlGNEY5NDYwNkM0QUE2NjciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxNDU3QzUxMkMwNEIxMUU3OUY0Rjk0NjA2QzRBQTY2NyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxNDU3QzUxM0MwNEIxMUU3OUY0Rjk0NjA2QzRBQTY2NyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pvx0vOAAAAJLSURBVHja7JkxSMNAFIbT1LGDYnEIRRBdrIOjk0uELkJFHDo5OEpQWlAcXHXTVSld3NwEB7t1ciw4VaRDxUGLIB1sESy0xv/ghvS8xGuauyDmwUfJJe/d3+Tl3V1O02C2bcfBPqiCri3furQv0mdcG8ao2IodnlWGEa2DAjC18MykGoQF57TwLTdMSnwyj2hStjr0kWT6/BT1jZGrBxpgKm6p33517Y9ZJDgSHAlWbGN+y9CwFlS5jFIiEuw3hyUM2fe+3oWw5hJRDv+bOuyVUgiRws8aWAWzwKCnmqABbsA1Qjx7CnGa6HVu5uJrgBLoCYTo0WuNUASjKQvaPhanxCerVDAOd0F/hBU18d3xJdjHEijrIrYG8iANEpQ0bau5iM5KFUxzts35eGIB3cNPp9d0OelhyBRc4og1SXhBTI7okhTBpHRxqoElKHQclMES9WGrR0qGYIuTs7qA2ClwR31aYJGT05YMwWUmZJ4KOgRHLmJJzj8wfq/ggGkryxBcZ0KSClBwHJ+AmEPsDHjkPP5N6uu0ugzBHSbkBvhi2s6o6HnwwnlB1+mfSTDnOioET4BLTn29Am9M2wfIOO6+EsG8lCDfoC9+GdXewTKT2z9SQsb0ssEcZ0AfbIFzF58WWAG3HN/B2IrLGsnbU+Z8EyxwKoeuqqyJDBzHtP0JzLmUOteBg/2gnVQ0NG+DaT9DczWgzZWi6OTHY7QTmvzsBS1Y9vQyqG2vIic1gp/AO0SPurFY9JjIB7dEUrQBE9giNKZY+MjL/G8BBgDu7CBuz18G6wAAAABJRU5ErkJggg==)\n    0 0 / contain;\n}\n\n.word-details-pane-header .word-text .word-info {\n  margin-bottom: 20px;\n}\n\n.word-details-pane-header .word-text h2 {\n  font-size: 1.5em;\n  line-height: 1.2;\n  margin: 0;\n  font-weight: 400;\n  display: inline-block;\n}\n\n.word-details-pane-header .word-text a,\n.word-details-pane-header .word-text button {\n  display: inline-block;\n  width: 20px;\n  height: 20px;\n  line-height: 1.2;\n  margin-left: 30px;\n  background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAYAAAAehFoBAAAABGdBTUEAALGPC/xhBQAAAp9JREFUWAntWbFOAkEQBbHESipCbOj8DAobEyz9ATqi0UQ/xsBPWNCY+AW2WtIZPkApMFHxvePOm5u7PQbclZAwyYSZ2zcz7/aG2727SgUyn89r0FvoE/QDGlpYg7VYs0YOZmEA9BG6KWFtM+k9nNk1tGM+Q/9A1iYHk5DwuQkZFmTngMsxU71wGJZb9J9pqJoza80qAyW4CpF+KHvdumyJrZId4dCXazfDuxlWM7CvfKerb0NOoGPA1+1y18OOCfZ2eOtm2NzDvnpQTPWLsM3mxvYSZoYKuHUtsXWEzT1svQ+X9TpytHCFz6Cn0Da0CaVMoGPoCHqPHK/4LRYSkVKMijbdEua0i+IBbkIH0E9nYDpADLHJyWRTpriFlR1NPY1z+WnEwgKuC31z4UuOM6ar8/ERPyM5QHwgAypxZDxgl9CvEviyIcZeyJxmwpkgg4NCnNkiss84fgU9htZjpc1jHNPCHOlM61EDl6UQ5GTP6jbgy5M+1Hln4liM0S9zmGvR0zAyspSNAYCE/NNIIYGODJWDtNVYB4c06UGEKQuUSaw28rWg+m7Q1/HL6mKcV0MKc7a89zCS6kLsy1wbSCa0C06I7aF7up9LpAPX8LkoSBliIfiWByx2HDNU2FPzSqcCy1yuYFIe6BTNogTp8XjFjGIFru19t4bC7yhQF0UOUHyqCYnxQpOEEcM8zJfINERLJMmD/IYgzI2MlCPprGjr2EkIwtx1STmhw0ssRQJKxqNYgR2HIMwtopQeenHlOnFMTyaCPQpxHw6+cOgX2g11Viu7mJ2gSzO/5viQu+TMkCzo5ufGB1vk+CVM4vCDbS99ffbKEI5J+9/Ax4l9fFjMERYzrffGlota/IjEpKEF7Lw9hP7LF6NkQkD8z4/5P0eRZrlVgPOgAAAAAElFTkSuQmCC)\n    0 0 / contain;\n  border: none;\n  outline: none;\n  cursor: pointer;\n}\n\n.word-details-pane-header .word-text .add-scb {\n  display: none;\n}\n\n.word-details-pane-content {\n  padding: 0;\n}\n\n.word-details-pane mark.highlight {\n  color: #2e94f7;\n  background: none;\n}\n\n.word-details-item {\n  margin-bottom: 1em;\n}\n\n.word-details-item:last-of-type {\n  margin-bottom: 0;\n}\n\n.word-details-item > h2 {\n  font-size: 1.5em;\n  line-height: 1.2;\n  margin: 0 0 10px;\n  padding-bottom: 5px;\n  font-weight: 400;\n  border-bottom: 1px solid #666;\n}\n\n.word-details-button-expand {\n  cursor: pointer;\n}\n\n.word-details-button-feedback {\n  padding: 10px 25px;\n  font-size: 1.16em;\n  border-radius: 20px;\n  background-color: #fff;\n  border: 1px solid #2e94f7;\n  color: #2e94f7;\n}\n\n.word-details a {\n  color: #2e94f7;\n}\n\n.word-details a:hover {\n  text-decoration: underline;\n}\n\n.word-details-pane-active {\n  display: block !important;\n}\n"
  },
  {
    "path": "src/components/dictionaries/hjdict/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type HjdictConfig = DictItem<{\n  related: boolean\n  chsas: 'jp/cj' | 'jp/jc' | 'kr' | 'w' | 'fr' | 'de' | 'es'\n  engas: 'w' | 'fr' | 'de' | 'es'\n  uas: 'fr' | 'de' | 'es'\n  aas: 'fr' | 'de'\n  eas: 'fr' | 'es'\n}>\n\nexport default (): HjdictConfig => ({\n  lang: '10011111',\n  selectionLang: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 265,\n  selectionWC: {\n    min: 1,\n    max: 10\n  },\n  options: {\n    related: true,\n    chsas: 'jp/jc',\n    engas: 'w',\n    uas: 'fr',\n    aas: 'fr',\n    eas: 'fr'\n  },\n  options_sel: {\n    chsas: ['jp/cj', 'jp/jc', 'kr', 'w', 'fr', 'de', 'es'],\n    engas: ['w', 'fr', 'de', 'es'],\n    uas: ['fr', 'de', 'es'],\n    aas: ['fr', 'de'],\n    eas: ['fr', 'es']\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/hjdict/engine.ts",
    "content": "import {\n  isContainChinese,\n  isContainEnglish,\n  isContainJapanese,\n  isContainKorean,\n  isContainFrench,\n  isContainDeutsch,\n  isContainSpanish\n} from '@/_helpers/lang-check'\nimport {\n  HTMLString,\n  getInnerHTML,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult\n} from '../helpers'\nimport { DictConfigs } from '@/app-config'\nimport { Profile } from '@/app-config/profiles'\nimport { getStaticSpeaker } from '@/components/Speaker'\nimport { fetchDirtyDOM } from '@/_helpers/fetch-dom'\n\nexport const getSrcPage: GetSrcPageFunction = (text, config, profile) => {\n  return `https://www.hjdict.com/${getLangCode(\n    text,\n    profile\n  )}/${encodeURIComponent(text)}`\n}\n\nconst HOST = 'https://www.hjdict.com'\n\nexport interface HjdictResultLex {\n  type: 'lex'\n  langCode: string\n  header?: HTMLString\n  entries: HTMLString[]\n}\n\nexport interface HjdictResultRelated {\n  type: 'related'\n  langCode: string\n  content: HTMLString\n}\n\nexport type HjdictResult = HjdictResultLex | HjdictResultRelated\n\ntype HjdictSearchResult = DictSearchResult<HjdictResult>\n\ninterface HjdictPayload {\n  langCode?: string\n}\n\nexport const search: SearchFunction<HjdictResult, HjdictPayload> = async (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  const cookies = {\n    HJ_SITEID: 3,\n    HJ_UID: getUUID(),\n    HJ_SID: getUUID(),\n    HJ_SSID: getUUID(),\n    HJID: 0,\n    HJ_VT: 2,\n    HJ_SST: 1,\n    HJ_CSST: 1,\n    HJ_ST: 1,\n    HJ_CST: 1,\n    HJ_T: +new Date(),\n    _: getUUID(16)\n  }\n\n  await Promise.all(\n    Object.keys(cookies).map(name =>\n      browser.cookies.set({\n        url: 'https://www.hjdict.com',\n        domain: 'hjdict.com',\n        name,\n        value: String(cookies[name])\n      })\n    )\n  )\n\n  const langCode = payload.langCode || getLangCode(text, profile)\n\n  return fetchDirtyDOM(\n    `https://www.hjdict.com/${langCode}/${encodeURIComponent(text)}`,\n    {\n      withCredentials: true\n    }\n  )\n    .catch(handleNetWorkError)\n    .then(doc => handleDOM(doc, profile.dicts.all.hjdict.options, langCode))\n}\n\nfunction handleDOM(\n  doc: Document,\n  options: DictConfigs['hjdict']['options'],\n  langCode: string\n): HjdictSearchResult | Promise<HjdictSearchResult> {\n  if (doc.querySelector('.word-notfound')) {\n    return wrapNoResult(langCode)\n  }\n\n  const $suggests = doc.querySelector('.word-suggestions')\n  if ($suggests) {\n    if (options.related) {\n      return {\n        result: {\n          type: 'related',\n          langCode,\n          content: getInnerHTML(HOST, $suggests)\n        }\n      }\n    }\n    return wrapNoResult(langCode)\n  }\n\n  let header = ''\n  const $header = doc.querySelector('.word-details-multi .word-details-header')\n  if ($header) {\n    $header\n      .querySelectorAll<HTMLLIElement>('.word-details-tab')\n      .forEach(($tab, i) => {\n        $tab.dataset.categories = String(i)\n      })\n    header = getInnerHTML(HOST, $header)\n  }\n\n  doc.querySelectorAll<HTMLSpanElement>('.word-audio').forEach($audio => {\n    $audio.replaceWith(getStaticSpeaker($audio.dataset.src))\n  })\n\n  const entries: HTMLString[] = [\n    ...doc.querySelectorAll('.word-details-pane')\n  ].map(\n    ($pane, i) => `\n      <div class=\"word-details-pane${\n        i === 0 ? ' word-details-pane-active' : ''\n      }\">\n        <div class=\"word-details-pane-header\">\n          ${getInnerHTML(HOST, $pane, '.word-details-pane-header')}\n        </div>\n        <div class=\"word-details-pane-content\">\n          ${getInnerHTML(HOST, $pane, '.word-details-pane-content')}\n        </div>\n      </div>\n    `\n  )\n\n  return entries.length > 0\n    ? { result: { type: 'lex', header, entries, langCode } }\n    : wrapNoResult(langCode)\n}\n\nfunction wrapNoResult(langCode: string): DictSearchResult<HjdictResultRelated> {\n  return {\n    result: {\n      type: 'related',\n      langCode,\n      content: '<p style=\"text-align:center;\">No Result</p>'\n    }\n  }\n}\n\n/**\n * Firefox adds 'Origin' field with `fetch` which would be rejected by the server.\n */\n// function xhrDirtyDOM (url: string): Promise<Document> {\n//   return new Promise((resolve, reject) => {\n//     const xhr = new XMLHttpRequest()\n//     xhr.open('GET', url, true)\n//     xhr.responseType = 'document'\n//     xhr.withCredentials = true\n//     xhr.onload = () => {\n//       if (xhr.readyState === xhr.DONE && xhr.status >= 200 && xhr.status < 300) {\n//         if (xhr.responseXML) {\n//           resolve(xhr.responseXML)\n//         } else {\n//           reject(xhr)\n//         }\n//       }\n//     }\n//     xhr.onerror = err => reject(err)\n//     xhr.send(null)\n//   })\n// }\n\nfunction getLangCode(text: string, profile: Profile): string {\n  // ü\n  if (/\\u00fc/i.test(text)) {\n    return profile.dicts.all.hjdict.options.uas\n  }\n\n  // ä\n  if (/\\u00e4/i.test(text)) {\n    return profile.dicts.all.hjdict.options.aas\n  }\n\n  // é\n  if (/\\u00e9/i.test(text)) {\n    return profile.dicts.all.hjdict.options.eas\n  }\n\n  if (isContainFrench(text)) {\n    return 'fr'\n  }\n\n  if (isContainDeutsch(text)) {\n    return 'de'\n  }\n\n  if (isContainSpanish(text)) {\n    return 'es'\n  }\n\n  if (isContainEnglish(text)) {\n    return profile.dicts.all.hjdict.options.engas\n  }\n\n  if (isContainJapanese(text)) {\n    return 'jp/jc'\n  }\n\n  if (isContainKorean(text)) {\n    return 'kr'\n  }\n\n  if (isContainChinese(text)) {\n    return profile.dicts.all.hjdict.options.chsas\n  }\n\n  return 'w'\n}\n\nfunction getUUID(e?: number): string {\n  let t = arguments.length > 1 && undefined !== arguments[1] ? arguments[1] : 16\n  let n = ''\n  if ('number' === typeof e) {\n    for (let i = 0; i < e; i++) {\n      const r = Math.floor(10 * Math.random())\n      n += r % 2 === 0 ? 'x' : 'y'\n    }\n  } else {\n    n = e || 'xxxxxxxx-xyxx-yxxx-xxxy-xxyxxxxxxxxx'\n  }\n  return (\n    ('number' !== typeof t || t < 2 || t > 36) && (t = 16),\n    n.replace(/[xy]/g, function(e) {\n      const n = (Math.random() * t) | 0\n      return ('x' === e ? n : (3 & n) | 8).toString(t)\n    })\n  )\n}\n"
  },
  {
    "path": "src/components/dictionaries/jikipedia/View.tsx",
    "content": "import React, { FC } from 'react'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { JikipediaResult } from './engine'\nimport { StrElm } from '@/components/StrElm'\n\nexport const Jikipedia: FC<ViewPorps<JikipediaResult>> = ({ result }) => (\n  <ul className=\"dictJikipedia-List\">\n    {result.map(item => (\n      <li key={item.title + item.url} className=\"dictJikipedia-Item\">\n        <h2 className=\"dictJikipedia-Title\">\n          <a href={item.url} target=\"_blank\" rel=\"nofollow noopener noreferrer\">\n            {item.title}\n          </a>\n        </h2>\n        {item.content && (\n          <StrElm className=\"dictJikipedia-Content\" html={item.content} />\n        )}\n        <footer className=\"dictJikipedia-Footer\">\n          {item.author && (\n            <a\n              className=\"dictJikipedia-Author\"\n              href={item.author.url}\n              target=\"_blank\"\n              rel=\"nofollow noopener noreferrer\"\n            >\n              {item.author.name}\n            </a>\n          )}\n          {item.likes > 0 && (\n            <span className=\"dictJikipedia-Thumbs\">\n              <svg\n                className=\"dictJikipedia-IconThumbsUp\"\n                width=\"0.9em\"\n                height=\"0.9em\"\n                fill=\"#666\"\n                viewBox=\"0 0 561 561\"\n              >\n                <path d=\"M0 535.5h102v-306H0v306zM561 255c0-28.05-22.95-51-51-51H349.35l25.5-117.3v-7.65c0-10.2-5.1-20.4-10.2-28.05L336.6 25.5 168.3 193.8c-10.2 7.65-15.3 20.4-15.3 35.7v255c0 28.05 22.95 51 51 51h229.5c20.4 0 38.25-12.75 45.9-30.6l76.5-181.05c2.55-5.1 2.55-12.75 2.55-17.85v-51H561c0 2.55 0 0 0 0z\" />\n              </svg>\n              {item.likes}\n            </span>\n          )}\n        </footer>\n      </li>\n    ))}\n  </ul>\n)\n\nexport default Jikipedia\n"
  },
  {
    "path": "src/components/dictionaries/jikipedia/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Jikipedia\",\n    \"zh-CN\": \"小鸡词典\",\n    \"zh-TW\": \"小雞詞典\"\n  },\n  \"options\": {\n    \"resultnum\": {\n      \"en\": \"Show\",\n      \"zh-CN\": \"结果数量\",\n      \"zh-TW\": \"結果數量\"\n    },\n    \"resultnum_unit\": {\n      \"en\": \"results\",\n      \"zh-CN\": \"个\",\n      \"zh-TW\": \"個\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/jikipedia/_style.shadow.scss",
    "content": ".brax-render {\n  display: block\n}\n\n.brax-node {\n  word-wrap: break-word\n}\n\n.brax-bold,.braxParse-b {\n  font-weight: 700\n}\n\n.brax-strikethrough {\n  text-decoration: line-through\n}\n\n.brax-redact {\n  color: #000;\n  background-color: #000;\n  transition: .2s\n}\n\n.brax-redact:active,.brax-redact:hover {\n  color: auto;\n  background-color: transparent\n}\n\n.brax-italic,.braxParse-i {\n  font-style: italic\n}\n\n.brax-underline,.braxParse-u {\n  text-decoration: underline\n}\n\n.brax-underline-through {\n  text-decoration: underline line-through\n}\n\n.dictJikipedia-Title {\n  font-size: 1.2em;\n\n  a {\n    color: currentColor;\n  }\n}\n\n.dictJikipedia-Item {\n  margin-bottom: 10px;\n}\n\n.dictJikipedia-Content {\n  margin: 0 0 3px 0;\n}\n\n.dictJikipedia-Image {\n  text-align: center;\n\n  > img {\n    max-width: 80%;\n    max-height: 200px;\n  }\n}\n\n.dictJikipedia-Footer {\n  color: var(--color-font-grey);\n}\n\n.dictJikipedia-Author {\n  color: #5caf9e;\n  margin-right: 1em;\n}\n\n.dictJikipedia-Thumbs {\n  margin-right: 5px;\n}\n\n.dictJikipedia-IconThumbsUp {\n  width: 0.9em;\n  height: 0.9em;\n  fill: #666;\n  margin-right: 2px;\n}\n"
  },
  {
    "path": "src/components/dictionaries/jikipedia/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type UrbanConfig = DictItem<{\n  resultnum: number\n}>\n\nexport default (): UrbanConfig => ({\n  lang: '01000000',\n  selectionLang: {\n    english: true,\n    chinese: true,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: true,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 380,\n  selectionWC: {\n    min: 1,\n    max: 5\n  },\n  options: {\n    resultnum: 4\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/jikipedia/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  HTMLString,\n  getText,\n  getInnerHTML,\n  handleNoResult,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult,\n  getFullLink\n} from '../helpers'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return `https://jikipedia.com/search?phrase=${encodeURIComponent(text)}`\n}\n\nconst HOST = 'https://jikipedia.com'\n\ninterface JikipediaResultItem {\n  title: string\n  content: HTMLString\n  likes: number\n  url?: string\n  author?: {\n    name: string\n    url: string\n  }\n}\n\nexport type JikipediaResult = JikipediaResultItem[]\n\ntype JikipediaSearchResult = DictSearchResult<JikipediaResult>\n\nexport const search: SearchFunction<JikipediaResult> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  const options = profile.dicts.all.jikipedia.options\n\n  return fetchDirtyDOM(\n    `https://jikipedia.com/search?phrase=${encodeURIComponent(text)}`\n  )\n    .catch(handleNetWorkError)\n    .then(doc => handleDOM(doc, options))\n}\n\nfunction handleDOM(\n  doc: Document,\n  { resultnum }: { resultnum: number }\n): JikipediaSearchResult | Promise<JikipediaSearchResult> {\n  const $cards = doc.querySelectorAll('.lite-card')\n  if ($cards.length < 1) {\n    return handleNoResult()\n  }\n\n  doc.querySelectorAll('.ad-card').forEach(el => el.remove())\n\n  const result: JikipediaResult = []\n\n  for (const $card of $cards) {\n    if (result.length >= resultnum) {\n      break\n    }\n\n    const item: JikipediaResultItem = {\n      title: getText($card, '.title'),\n      content: getInnerHTML(HOST, $card, '.content'),\n      likes: Number(getText($card, '.like-count')) || 0\n    }\n\n    if (!item.content) {\n      continue\n    }\n\n    const $a = $card.querySelector('a.card-content')\n    if ($a) {\n      item.url = getFullLink(HOST, $a, 'href')\n    }\n\n    const $author = $card.querySelector('.author a')\n    if ($author) {\n      item.author = {\n        name: getText($author),\n        url: getFullLink(HOST, $author, 'href')\n      }\n    }\n\n    result.push(item)\n  }\n\n  return result.length > 0 ? { result } : handleNoResult()\n}\n"
  },
  {
    "path": "src/components/dictionaries/jukuu/View.tsx",
    "content": "import React, { FC } from 'react'\nimport { JukuuResult, JukuuPayload, JukuuLang } from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictJukuu: FC<ViewPorps<JukuuResult>> = props => {\n  const { result, searchText } = props\n  const { t } = useTranslate('dicts')\n  return (\n    <>\n      <select\n        onChange={e => {\n          if (e.target.value) {\n            searchText<JukuuPayload>({\n              id: 'jukuu',\n              payload: {\n                lang: e.target.value as JukuuLang\n              }\n            })\n          }\n        }}\n      >\n        <option value=\"zheng\" selected={result.lang === 'zheng'}>\n          {t('jukuu.options.lang-zheng')}\n        </option>\n        <option value=\"engjp\" selected={result.lang === 'engjp'}>\n          {t('jukuu.options.lang-engjp')}\n        </option>\n        <option value=\"zhjp\" selected={result.lang === 'zhjp'}>\n          {t('jukuu.options.lang-zhjp')}\n        </option>\n      </select>\n      <ul className=\"dictJukuu-Sens\">\n        {result.sens.map((sen, i) => (\n          <li key={i} className=\"dictJukuu-Sen\">\n            <StrElm tag=\"p\" html={sen.trans} />\n            <p className=\"dictJukuu-Ori\">{sen.original}</p>\n            <p className=\"dictJukuu-Src\">{sen.src}</p>\n          </li>\n        ))}\n      </ul>\n    </>\n  )\n}\n\nexport default DictJukuu\n"
  },
  {
    "path": "src/components/dictionaries/jukuu/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Jukuu\",\n    \"zh-CN\": \"句酷\",\n    \"zh-TW\": \"句酷\"\n  },\n  \"options\": {\n    \"lang\": {\n      \"en\": \"Language\",\n      \"zh-CN\": \"语言\",\n      \"zh-TW\": \"語言\"\n    },\n    \"lang-zheng\": {\n      \"en\": \"Chinese-English\",\n      \"zh-CN\": \"中英\",\n      \"zh-TW\": \"中英\"\n    },\n    \"lang-engjp\": {\n      \"en\": \"English-Japanese\",\n      \"zh-CN\": \"英日\",\n      \"zh-TW\": \"英日\"\n    },\n    \"lang-zhjp\": {\n      \"en\": \"Chinese-Japanese\",\n      \"zh-CN\": \"中日\",\n      \"zh-TW\": \"中日\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/jukuu/_style.shadow.scss",
    "content": ".dictJukuu-Sens {\n  padding-left: 20px;\n\n  b {\n    color: #f9690e;\n  }\n\n  p {\n    margin: 0;\n  }\n}\n\n.dictJukuu-Sen {\n  list-style-type: disc;\n  margin: 0.5em 0;\n}\n\n.dictJukuu-Ori {\n  color: olive;\n}\n\n.dictJukuu-Src {\n  font-size: 0.9em;\n  text-align: right;\n  color: #777;\n}\n"
  },
  {
    "path": "src/components/dictionaries/jukuu/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type JukuuConfig = DictItem<{\n  lang: 'zheng' | 'engjp' | 'zhjp'\n}>\n\nexport default (): JukuuConfig => ({\n  lang: '11010000',\n  selectionLang: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 300,\n  selectionWC: {\n    min: 1,\n    max: 99999\n  },\n  options: {\n    lang: 'zheng'\n  },\n  options_sel: {\n    lang: ['zheng', 'engjp', 'zhjp']\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/jukuu/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  HTMLString,\n  getText,\n  getInnerHTML,\n  handleNoResult,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  removeChildren,\n  DictSearchResult\n} from '../helpers'\n\nexport type JukuuLang = 'engjp' | 'zhjp' | 'zheng'\n\nfunction getUrl(text: string, lang: JukuuLang) {\n  text = encodeURIComponent(text.replace(/\\s+/g, '+'))\n\n  switch (lang) {\n    case 'engjp':\n      return 'http://www.jukuu.com/jsearch.php?q=' + text\n    case 'zhjp':\n      return 'http://www.jukuu.com/jcsearch.php?q=' + text\n    // case 'zheng':\n    default:\n      return 'http://www.jukuu.com/search.php?q=' + text\n  }\n}\n\nexport const getSrcPage: GetSrcPageFunction = (text, config, profile) => {\n  return getUrl(text, profile.dicts.all.jukuu.options.lang)\n}\n\ninterface JukuuTransItem {\n  trans: HTMLString\n  original: string\n  src: string\n}\n\nexport interface JukuuResult {\n  lang: JukuuLang\n  sens: JukuuTransItem[]\n}\n\nexport interface JukuuPayload {\n  lang?: JukuuLang\n}\n\ntype JukuuSearchResult = DictSearchResult<JukuuResult>\n\nexport const search: SearchFunction<JukuuResult, JukuuPayload> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  const lang = payload.lang || profile.dicts.all.jukuu.options.lang\n  return fetchDirtyDOM(getUrl(text, lang))\n    .catch(handleNetWorkError)\n    .then(handleDOM)\n    .then(sens =>\n      sens.length > 0 ? { result: { lang, sens } } : handleNoResult()\n    )\n}\n\nfunction handleDOM(doc: Document): JukuuTransItem[] {\n  return [...doc.querySelectorAll('tr.e')]\n    .map($e => {\n      const $trans = $e.lastElementChild\n      if (!$trans) {\n        return\n      }\n      removeChildren($trans, 'img')\n\n      const $original = $e.nextElementSibling\n      if (!$original || !$original.classList.contains('c')) {\n        return\n      }\n\n      const $src = $original.nextElementSibling\n\n      return {\n        trans: getInnerHTML('http://www.jukuu.com', $trans),\n        original: getText($original),\n        src:\n          $src && $src.classList.contains('s')\n            ? getText($src).replace(/^[\\s-]*/, '')\n            : ''\n      }\n    })\n    .filter((item): item is JukuuTransItem => Boolean(item && item.trans))\n}\n"
  },
  {
    "path": "src/components/dictionaries/lexico/View.tsx",
    "content": "import React, { FC } from 'react'\nimport { LexicoResult, LexicoResultLex, LexicoResultRelated } from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictLexico: FC<ViewPorps<LexicoResult>> = ({ result }) => {\n  switch (result.type) {\n    case 'lex':\n      return renderLex(result)\n    case 'related':\n      return renderRelated(result)\n    default:\n      return null\n  }\n}\n\nfunction renderLex(result: LexicoResultLex) {\n  return (\n    <StrElm\n      className=\"dictLexico-Lex\"\n      onClick={onLexClick}\n      html={result.entry}\n    />\n  )\n}\n\nfunction renderRelated(result: LexicoResultRelated) {\n  return (\n    <>\n      <p>Did you mean:</p>\n      <ul className=\"dictLexico-Related\">\n        {result.list.map((item, i) => (\n          <li key={i}>\n            <a\n              rel=\"nofollow noopener noreferrer\"\n              target=\"_blank\"\n              href={item.href}\n            >\n              {item.text}\n            </a>\n          </li>\n        ))}\n      </ul>\n    </>\n  )\n}\n\nexport default DictLexico\n\nfunction onLexClick(e: React.MouseEvent): void {\n  const $target = e.target as Element\n  const $info = $target.classList?.contains('moreInfo')\n    ? $target\n    : $target.parentElement?.classList?.contains('moreInfo')\n    ? $target.parentElement\n    : null\n  if ($info) {\n    $info.classList.toggle('active')\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/lexico/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Lexico\",\n    \"zh-CN\": \"Lexico\",\n    \"zh-TW\": \"Lexico\"\n  },\n  \"options\": {\n    \"related\": {\n      \"en\": \"Show related results\",\n      \"zh-CN\": \"失败时显示备选\",\n      \"zh-TW\": \"失敗時顯示備選\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/lexico/_style.shadow.scss",
    "content": "button {\n  cursor: pointer;\n  outline: none;\n}\n\n.layout:after,\n.topHeader:after,\n.headerContent:after,\n.headerContent .searchField .searchMain form:after,\n.news section a:after,\n.posts section a:after,\n.tests form fieldset .steps:after,\n.categories section a:after {\n  content: '';\n  display: block;\n  clear: both;\n}\n\n.boxSizing,\n.container,\n.topHeader .nav > ul .drop ul,\n.headerContent .searchField .searchMain form,\n.headerContent .searchField .searchMain form .sbSelector,\n.headerContent .searchField .searchMain form fieldset input,\n.headerContent .searchField .searchMain form fieldset .ui-autocomplete,\n.headerContent .searchField .searchMain form fieldset .keyboardBox,\n.mainHeader.fixed .headerContent .searchField,\n.mainHeader.fixed .burger span,\n.banbox-mini .banner,\n.news section,\n.news section.mainNews h1,\n.words > div,\n.trend section .popular ul,\n.contentPanel section .popular ul,\n.posts section,\n.posts section a h4 span,\n.quizzes > div,\n.quizzes > div section article,\n.quotes > div,\n.quotes .blogbox article,\n.videos ul li,\n.socials ul li a,\n.socials ul li .more,\n.socials ul li .more i,\n.tests form fieldset ul li label span:before,\n.textBlock blockquote,\n.textBlock .large,\n.headwordAudio ul .close,\n.gramb .semb > p .cnt .pop-label {\n  -webkit-box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  box-sizing: border-box;\n}\n\n.inlineBlock,\n.topHeader .nav > ul > li,\n.topHeader .signIn,\n.topHeader .signIn a i,\n.topHeader .signIn a span,\n.topHeader .sharing,\n.topHeader .sharing ul li,\n.headerContent .searchField .searchMain form fieldset .keyboard svg,\n.headerContent\n  .searchField\n  .searchMain\n  form\n  fieldset\n  .keyboardBox\n  .kBoxContent\n  ul\n  li,\n.mainHeader.fixed .burger svg,\n.mainHeader.fixed .burger img,\n.mainHeader.fixed .burger span,\n.footerNav nav.sharing ul li a i,\n.footerNav nav.sharing ul li a span,\n.trend section .popular p span,\n.trend section .popular p i,\n.contentPanel section .popular p span,\n.contentPanel section .popular p i,\n.dayword section .daywordmain strong span,\n.dayword section .daywordmain strong a,\n.faces > div,\n.breadcrumbs p span,\n.socials ul li,\n.hwg .hw,\n.headwordAudio,\n.headwordAudio ul li a span,\n.headwordAudio ul li a button,\n.trg .tr > span,\n.trg .tr > a,\n.lang {\n  display: inline-block;\n  vertical-align: middle;\n}\n\n.transition,\na,\n.topHeader .sharing ul li a svg {\n  -webkit-transition: all 0.3s;\n  -moz-transition: all 0.3s;\n  -ms-transition: all 0.3s;\n  -o-transition: all 0.3s;\n  transition: all 0.3s;\n}\n\n.posts section {\n  width: 100% !important;\n}\n\n.posts section .box-img img.lazy {\n  height: auto !important;\n}\n\n.contentPanel section.boxSizing,\n.contentPanel section.container,\n.contentPanel .headerContent .searchField .searchMain form section.sbSelector,\n.headerContent .searchField .searchMain form .contentPanel section.sbSelector,\n.contentPanel\n  .headerContent\n  .searchField\n  .searchMain\n  form\n  fieldset\n  section.ui-autocomplete,\n.headerContent\n  .searchField\n  .searchMain\n  form\n  fieldset\n  .contentPanel\n  section.ui-autocomplete,\n.contentPanel\n  .headerContent\n  .searchField\n  .searchMain\n  form\n  fieldset\n  section.keyboardBox,\n.headerContent\n  .searchField\n  .searchMain\n  form\n  fieldset\n  .contentPanel\n  section.keyboardBox,\n.contentPanel .mainHeader.fixed .headerContent section.searchField,\n.mainHeader.fixed .headerContent .contentPanel section.searchField,\n.contentPanel .banbox-mini section.banner,\n.banbox-mini .contentPanel section.banner,\n.contentPanel .news section,\n.news .contentPanel section,\n.contentPanel .posts section,\n.posts .contentPanel section,\n.contentPanel .socials ul li section.more,\n.socials ul li .contentPanel section.more,\n.contentPanel .textBlock section.large,\n.textBlock .contentPanel section.large,\n.contentPanel .headwordAudio ul section.close,\n.headwordAudio ul .contentPanel section.close,\n.contentPanel .gramb .semb > p .cnt section.pop-label,\n.gramb .semb > p .cnt .contentPanel section.pop-label {\n  padding: 0px;\n}\n\n.posts {\n  font-size: 0;\n}\n\n.posts section {\n  display: inline-block;\n  vertical-align: top;\n  width: 25%;\n  padding: 0 20px 35px;\n  margin-bottom: 32px;\n  font-size: 1em;\n  position: relative;\n}\n\n.posts section a {\n  display: block;\n}\n\n.posts section a .box-img {\n  height: 168px;\n  width: 100%;\n}\n\n.posts section a h4 {\n  text-align: center;\n  font: 13px/5px 'Open Sans', Helvetica, Arial, sans-serif;\n  font-weight: 600;\n  color: #3c9ae3;\n  text-transform: uppercase;\n  letter-spacing: 2px;\n  -webkit-transform: translate(0, -100%);\n  -moz-transform: translate(0, -100%);\n  -ms-transform: translate(0, -100%);\n  -o-transform: translate(0, -100%);\n  transform: translate(0, -100%);\n}\n\n.posts section a h4 span {\n  display: inline-block;\n  background-color: #fff;\n  padding: 14px 15px 0;\n  min-height: 20px;\n}\n\n.posts section a h4 span {\n  padding: 7px 15px 6px;\n  margin-bottom: -1px;\n  font-size: 1em;\n}\n\n.posts section a h4 span:empty {\n  padding: 0;\n}\n\n.posts section a h2 {\n  color: #333;\n  font-size: 1em;\n  font-family: 'Open Sans', Helvetica, Arial, sans-serif;\n}\n\n.posts section:nth-child(4),\n.posts section:nth-child(5) {\n  margin-bottom: 26px;\n}\n\n.posts section:nth-child(4):before,\n.posts section:nth-child(5):before {\n  content: '';\n  position: absolute;\n  top: -31px;\n  right: 20px;\n  left: 20px;\n  height: 0;\n  border-top: 1px dotted #ccc;\n}\n\n.posts section.mainPost {\n  float: left;\n  width: 50%;\n  margin-bottom: 26px;\n}\n\n.posts section.mainPost a {\n  position: relative;\n  border: none;\n}\n\n.posts section.mainPost a .box-img {\n  width: 100%;\n  height: 360px;\n}\n\n.posts section.mainPost h2 {\n  color: #333;\n  font-size: 1em;\n  line-height: 1.2;\n  font-family: 'Open Sans', Helvetica, Arial, sans-serif;\n  font-family: inherit;\n  padding-right: 20px;\n  margin-bottom: 12px;\n}\n\n.posts section.mainPost h2 {\n  font-size: 1em;\n}\n\n.posts section.mainPost h2 {\n  line-height: normal;\n}\n\n.posts section.mainPost p {\n  color: #555;\n  line-height: 1.2;\n  font-family: 'Open Sans', Helvetica, Arial, sans-serif;\n  padding-right: 20px;\n  font-size: 1em;\n}\n\n.posts section {\n  padding: 0 15px 35px;\n}\n\n.posts section a .box-img {\n  height: 132px;\n}\n\n.posts section.mainPost {\n  margin-bottom: 20px;\n}\n\n.posts section.mainPost a .box-img {\n  height: 283px;\n}\n\n.posts section {\n  width: 50%;\n  padding: 0 10px 20px;\n  margin-bottom: 44px;\n}\n\n.posts section:nth-child(4),\n.posts section:nth-child(5) {\n  margin-bottom: 31px;\n}\n\n.posts section:nth-child(4):before,\n.posts section:nth-child(5):before {\n  left: 10px;\n  right: 10px;\n}\n\n.posts section a .box-img {\n  height: 210px;\n}\n\n.posts section a h4 {\n  line-height: 1.2;\n}\n\n.posts section.mainPost {\n  width: 100%;\n}\n\n.posts section.mainPost h4 {\n  font-size: 1em;\n  line-height: 1em;\n  margin-bottom: 6px;\n}\n\n.posts section.mainPost p {\n  font-size: 1em;\n  line-height: 1.2;\n}\n\n.posts section.mainPost a .box-img {\n  height: 311px;\n}\n\n.posts section {\n  padding: 0 5px 20px;\n  margin-bottom: 15px;\n}\n\n.posts section a .box-img {\n  height: 85px;\n}\n\n.posts section a h2 {\n  margin-bottom: -5px;\n}\n\n.posts section a h4 {\n  font-size: 1em;\n  line-height: 1.2;\n}\n\n.posts section.mainPost {\n  margin-bottom: 2px;\n}\n\n.posts section.mainPost h4 {\n  font-size: 1em;\n  line-height: 1.2;\n  margin-bottom: 6px;\n  padding: 0;\n}\n\n.posts section.mainPost p {\n  font-size: 1em;\n  line-height: 1.2;\n  padding-right: 0;\n}\n\n.posts section.mainPost a .box-img {\n  height: 178px;\n}\n\n.posts section:nth-child(4),\n.posts section:nth-child(5) {\n  margin-bottom: 6px;\n}\n\n.posts section:nth-child(4):before,\n.posts section:nth-child(5):before {\n  top: -18px;\n  left: 5px;\n  right: 5px;\n}\n\n.dictLexico-Lex > .banbox {\n  padding: 0px;\n}\n\n.dictLexico-Lex > .banbox .container .banner {\n  padding: 15px 0px;\n}\n\n.posts.layout article h2,\n.posts.topHeader article h2,\n.posts.headerContent article h2,\n.headerContent .searchField .searchMain form.posts article h2,\n.news section a.posts article h2,\n.posts section a.posts article h2,\n.tests form fieldset .posts.steps article h2,\n.categories section a.posts article h2 {\n  margin-top: -25px !important;\n}\n\n.lex-filling > div.dictLexico-Lex {\n  margin-bottom: 0;\n  padding-bottom: 0;\n}\n\n.dictLexico-Lex > section {\n  margin: 0 0 1em;\n  border-top: 1px solid var(--color-divider);\n  padding-top: 4px;\n}\n\n.dictLexico-Lex section h3 {\n  font-size: 1em;\n  font-family: 'Merriweather', 'Cambria', Georgia, 'Times New Roman', Times,\n    serif;\n  font-family: inherit;\n}\n\n.dictLexico-Lex section.relatedSection {\n  margin: 0;\n  border-top: 1px solid #d8eaee;\n  border-bottom: 1px solid #d8eaee;\n  background-color: #f8fafe;\n  padding: 21px 20px 7px;\n}\n\n.dictLexico-Lex .breadcrumbs p {\n  padding: 22px 15px 13px 20px;\n}\n\n.dictLexico-Lex .breadcrumbs p a {\n  font-size: 1em;\n}\n\n.dictLexico-Lex > section {\n  margin: 0 0 1em;\n}\n\n.dictLexico-Lex .breadcrumbs p {\n  padding: 24px 20px 13px;\n}\n\n.lex-filling > div > .breadcrumbs.layout,\n.lex-filling > div > .breadcrumbs.topHeader,\n.lex-filling > div > .breadcrumbs.headerContent,\n.headerContent .searchField .searchMain .lex-filling > div > form.breadcrumbs,\n.news section .lex-filling > div > a.breadcrumbs,\n.posts section .lex-filling > div > a.breadcrumbs,\n.tests form fieldset .lex-filling > div > .breadcrumbs.steps,\n.categories section .lex-filling > div > a.breadcrumbs {\n  display: inline-block;\n}\n\n.entryHead h1 {\n  font: 16px 'Open Sans', Helvetica, Arial, sans-serif;\n  font-family: 'Open Sans', Helvetica, Arial, sans-serif;\n  margin-bottom: 22px;\n}\n\n.entryHead h1 em {\n  font-style: normal;\n}\n\n.entryHead h2 em {\n  font-style: normal;\n}\n\n.entryHead .headwordAudio {\n  height: 23px;\n  width: 27px;\n}\n\n.entryHead .headwordAudio button {\n  -webkit-background-size: 27px 23px;\n  background-size: 27px 23px;\n}\n\n.entryHead .headwordAudio ul li a button {\n  -webkit-background-size: 14px 12px;\n  background-size: 14px 12px;\n}\n\n.entryHead h1 {\n  margin-bottom: 10px;\n}\n\n.socials {\n  float: right;\n  margin-top: 4px;\n  position: relative;\n}\n\n.socials .social-drop {\n  right: -2px;\n}\n\n.socials ul li {\n  width: 32px;\n  height: 32px;\n  padding-left: 0;\n}\n\n.socials ul.adds li {\n  width: auto;\n  height: auto;\n}\n\n.socials ul.adds li a i {\n  width: 32px;\n  height: 32px;\n}\n\n.non-lexical-socials .socials {\n  margin-top: 10px;\n  padding-right: 20px;\n  z-index: 2;\n}\n\n.socials-mobile {\n  display: none;\n}\n\n.socials-mobile .socials {\n  text-align: center;\n  width: 100%;\n  padding-right: 0;\n}\n\n.socials-mobile.non-lexical-socials,\n.socials-mobile .lexical-socials {\n  float: none;\n}\n\n.socials {\n  display: none;\n}\n\n.non-lexical-socials.socials-mobile,\n.lexical-socials.socials-mobile {\n  display: block;\n}\n\n.non-lexical-socials.socials-mobile .socials,\n.lexical-socials.socials-mobile .socials {\n  display: block;\n  float: none;\n  margin-bottom: 50px;\n}\n\n.non-lexical-socials.socials-mobile .socials .social-drop,\n.lexical-socials.socials-mobile .socials .social-drop {\n  position: relative;\n  width: 60px;\n  margin: auto;\n  margin-top: 13px;\n  left: 41px;\n}\n\n.non-lexical-socials.socials-mobile .socials .social-drop ul,\n.lexical-socials.socials-mobile .socials .social-drop ul {\n  margin-bottom: 0;\n}\n\n.non-lexical-socials.socials-mobile .socials .social-drop ul:first-child,\n.lexical-socials.socials-mobile .socials .social-drop ul:first-child {\n  border-bottom: none;\n}\n\n.hwg {\n  width: 80%;\n}\n\n.hwg .hw {\n  font-size: 1em;\n  font-weight: 700;\n  margin-right: 6px;\n  -ms-word-break: break-all;\n  word-break: break-word;\n}\n\n.hwg a.hw {\n  color: #3c9ae3;\n}\n\n.gramb .ps {\n  font: 16px 'Open Sans', Helvetica, Arial, sans-serif;\n  font-family: 'Open Sans', Helvetica, Arial, sans-serif;\n  color: #f15a24;\n  text-transform: uppercase;\n  letter-spacing: 0.8px;\n}\n\n.gramb .ps .pos {\n  font-weight: 700;\n}\n\n.gramb .ps .qualifiers {\n  font-weight: 400;\n  color: #787878;\n  text-transform: none;\n  letter-spacing: normal;\n}\n\n.gramb .ps .qualifiers .plural {\n  font-weight: 700;\n  color: black;\n}\n\n.gramb .ps .listSeparator {\n  color: grey;\n}\n\n.gramb .ps .pos-inflections {\n  font-weight: normal;\n  text-transform: lowercase;\n  letter-spacing: initial;\n}\n\n.gramb .ps .pos-inflections .inflection-text {\n  font-weight: bold;\n}\n\n.gramb .ps .pos-inflections .languageGroup {\n  text-transform: uppercase;\n}\n\n.gramb .ps .pos-inflections .inflection-type {\n  color: var(--color-font-grey)666;\n}\n\n.gramb .ps .pos-inflections:before {\n  content: ' (';\n}\n\n.gramb .ps .pos-inflections:after {\n  content: ') ';\n}\n\n.gramb .semb > p {\n  font-family: 'Open Sans', Helvetica, Arial, sans-serif;\n  margin-bottom: 17px;\n  position: relative;\n  padding-left: 20px;\n  line-height: 1.2;\n}\n\n.gramb .semb > p .iteration {\n  font-weight: 700;\n  font-size: 1em;\n  line-height: 1.2;\n  position: absolute;\n  top: 1px;\n  left: 0;\n}\n\n.gramb .semb > p .cnt {\n  text-transform: uppercase;\n  font-size: 0.9em;\n  color: var(--color-brand);\n  line-height: 1.2;\n  position: relative;\n  cursor: default;\n}\n\n.gramb .semb > p .cnt .pop-label {\n  position: absolute;\n  bottom: 100%;\n  left: 50%;\n  color: #fff;\n  text-transform: none;\n  width: 202px;\n  padding: 7px 12px;\n  margin-bottom: 9px;\n  line-height: 1.2;\n  background-color: #333;\n  border-radius: 5px;\n  display: none;\n  -webkit-transform: translate(-50%, 0);\n  -moz-transform: translate(-50%, 0);\n  -ms-transform: translate(-50%, 0);\n  -o-transform: translate(-50%, 0);\n  transform: translate(-50%, 0);\n}\n\n.gramb .semb > p .cnt .pop-label:after {\n  content: '';\n  position: absolute;\n  top: 100%;\n  left: 0;\n  right: 0;\n  width: 0;\n  height: 0;\n  border-style: solid solid none solid;\n  border-width: 7px;\n  border-color: #333 transparent transparent transparent;\n  margin: auto;\n}\n\n.gramb .semb .semb {\n  padding-left: 20px;\n}\n\n.gramb .semb .semb > li {\n  padding-left: 24px;\n}\n\n.gramb .semb .semb > li .trg .iteration {\n  left: -24px;\n}\n\n.gramb .semb .semb .trg {\n  margin-bottom: 7px;\n}\n\n.gramb .semb .semb .trg.spanish_label {\n  margin-bottom: 0;\n}\n\n.gramb .semb .semb .trg .tr > span {\n  vertical-align: inherit;\n}\n\n.gramb .semb .sembsub > li {\n  padding-left: 33px;\n}\n\n.gramb .semb .sembsub > li .trg .iteration {\n  left: -33px;\n}\n\n.gramb .semb.uncount .cnt {\n  cursor: pointer;\n}\n\n.gramb .semb.uncount > p {\n  margin-bottom: 5px;\n}\n\n.gramb ul.semb > li {\n  padding-left: 1em;\n  margin-bottom: 1em;\n}\n\n.gramb ul.semb > li .trg {\n  margin-bottom: 8px;\n}\n\n.gramb ul.semb > li .trg .iteration {\n  position: absolute;\n  top: 1px;\n  left: -1em;\n}\n\n.gramb ul.semb > li ul.semb {\n  padding-left: 0;\n}\n\n.gramb ul.semb > li p + ul.semb {\n  margin-top: 15px;\n}\n\n.gramb ul.semb .exg > ul > li .headwordAudio {\n  margin-right: 4px;\n}\n\n.gramb .semb > p .cnt .pop-label {\n  position: fixed;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  margin: 0;\n  top: auto;\n  -webkit-transform: translate(0, 0);\n  -moz-transform: translate(0, 0);\n  -ms-transform: translate(0, 0);\n  -o-transform: translate(0, 0);\n  transform: translate(0, 0);\n  border-radius: 0;\n  z-index: 1;\n  width: auto;\n}\n\n.trg > p {\n  font-family: 'Open Sans', Helvetica, Arial, sans-serif;\n  position: relative;\n  line-height: 1.2;\n}\n\n.trg > p .iteration {\n  font-weight: 700;\n  font-size: 1em;\n  margin-right: 12px;\n}\n\n.trg > p .cs,\n.trg > p .ind {\n  font-size: 1em;\n  // color: var(--color-brand);\n}\n\n.trg .tr > span + .headwordAudio,\n.trg .tr > a + .headwordAudio {\n  margin-top: 2px;\n}\n\n.trg .tr > span .tgr {\n  font-weight: 400;\n  color: #898989;\n}\n\n.trg .tr > span .let-or {\n  color: #898989;\n  font-weight: 400;\n}\n\n.trg .tr > span + .lang {\n  margin-left: 3px;\n}\n\n.trg .tr > a {\n  font: 700 18px/27px 'Open Sans', Helvetica, Arial, sans-serif;\n  font-family: 'Open Sans', Helvetica, Arial, sans-serif;\n  color: #3c9ae3;\n}\n\n.trg .tr > a .tgr {\n  font-weight: 400;\n  color: #898989;\n}\n\n.trg .tr > a + .lang {\n  margin-left: 3px;\n}\n\n.trg .tr .lang {\n  font: 12px 'Open Sans', Helvetica, Arial, sans-serif;\n  font-family: 'Open Sans', Helvetica, Arial, sans-serif;\n  color: #898989;\n}\n\n.trg .tr .headwordAudio {\n  margin-right: 4px;\n}\n\n.trg .tr > span {\n  display: inline;\n}\n\n.exg .ex span + .lang {\n  margin-left: 10px;\n}\n\n.exg .ex span + .headwordAudio {\n  margin-bottom: 2px;\n}\n\n.exg .ex span.lev {\n  font-size: 1em;\n}\n\n.exg .ex .u {\n  color: #898989;\n}\n\n.exg .ex .reg {\n  color: #2aa850;\n  font-family: 'Open Sans', Helvetica, Arial, sans-serif;\n}\n\n.exg.list-ex {\n  margin: 12px 0;\n}\n\n.exg.list-ex .ex {\n  font-size: 1em;\n  margin-bottom: 3px;\n}\n\n.exg.list-ex ol {\n  list-style: none;\n  counter-reset: listEx;\n}\n\n.exg.list-ex ol li {\n  position: relative;\n}\n\n.exg.list-ex ol li.ex {\n  margin-bottom: 0;\n  padding-left: 16px;\n}\n\n.exg.list-ex ol li:before {\n  content: counter(listEx, decimal) '. ';\n  counter-increment: listEx;\n  font: italic 700 14px/14px 'Open Sans', Helvetica, Arial, sans-serif;\n  font-family: 'Open Sans', Helvetica, Arial, sans-serif;\n  position: absolute;\n  top: 7px;\n  margin: auto;\n  height: 14px;\n  left: 0;\n}\n\n.moreInfo {\n  margin: 7px 0;\n}\n\n.moreInfo button {\n  border: 2px solid var(--color-divider);\n  color: var(--color-font-grey)666;\n  padding: 2px 8px 3px;\n  -webkit-background-size: 8px 8px;\n  background-size: 8px 8px;\n  border-radius: 13px;\n  margin: 3px 0 5px;\n}\n\n.moreInfo + .xrg,\n.moreInfo + .exg {\n  display: none;\n}\n\n.moreInfo.active + .xrg,\n.moreInfo.active + .exg {\n  display: block;\n}\n\n.moreInfo + .xrg .ex,\n.moreInfo + .exg .ex {\n  font-size: 1em;\n  line-height: 1.2;\n  padding: 0 0 9px;\n  margin-bottom: 6px;\n  border-bottom: 1px solid #ddd;\n}\n\n.moreInfo + .xrg .ex:last-child,\n.moreInfo + .exg .ex:last-child {\n  border: none;\n}\n\n.moreInfo.active button {\n  background-color: var(--color-font-grey)666;\n  border-color: var(--color-font-grey)666;\n  color: #fff;\n}\n\n.english-ex {\n  margin-left: 5px;\n}\n\n.trg .tr > span.lev,\n.lev {\n  color: #2aa850;\n}\n\n.sg .posg {\n  color: #f15a24;\n  letter-spacing: 0.8px;\n  margin-bottom: 16px;\n  text-transform: uppercase;\n}\n\n.sg .posg .fg {\n  text-transform: none;\n  color: #000;\n  margin: 0 4px;\n  letter-spacing: 0;\n}\n\n.sg .posg .listSeparator {\n  color: var(--color-font-grey);\n}\n\n.sg .posg .pos-inflections {\n  font-weight: normal;\n  text-transform: lowercase;\n  letter-spacing: initial;\n}\n\n.sg .posg .pos-inflections .inflection-text {\n  font-weight: bold;\n}\n\n.sg .posg .pos-inflections .inflection-type {\n  color: var(--color-font-grey)666;\n}\n\n.sg .posg .pos-inflections:before {\n  content: ' (';\n}\n\n.sg .posg .pos-inflections:after {\n  content: ') ';\n}\n\n.sg ul li .iteration {\n  position: absolute;\n  top: 3px;\n  left: 0;\n}\n\n.etym h2 {\n  padding: 4px 0;\n  margin-bottom: 8px;\n}\n\n.pronSection {\n  font-size: 1em;\n}\n\n.pronSection span {\n  margin-left: 7px;\n}\n\n.pronSection .pron h2 {\n  display: inline;\n}\n\n.pronSection .pronWord {\n  font-weight: bold;\n}\n\n.pronSection .phoneticspelling {\n  font-weight: normal;\n}\n\n.pronSection .ps {\n  color: #f15a24;\n  text-transform: uppercase;\n  letter-spacing: 0.8px;\n}\n\n.pron.inline {\n  margin-top: -10px;\n  margin-bottom: 20px;\n  font-size: 1em;\n}\n\n.desktop .posts section a:hover h4,\n.desktop .news section a:hover h4,\n.desktop .quotes .blogbox a:hover article h4,\n.desktop .videos ul li a:hover p,\n.desktop .signup p a:hover,\n.desktop .trend section .popular ul li:hover,\n.desktop .breadcrumbs a:hover,\n.desktop .seemore:hover,\n.desktop .categories section a:hover h2,\n.desktop .socials ul.adds li a:hover span,\n.desktop .relatedSection .relatedBox ul li a:hover,\n.desktop .msDict .xrg a:hover,\n.desktop .dayword section .daywordmain strong .linkword:hover,\n.desktop .footerNav nav.sharing ul li a:hover span {\n  text-decoration: underline;\n}\n\n.desktop .moreInfo button:hover {\n  background-color: #dbdee2;\n  border-color: #dbdee2;\n}\n\n.desktop .moreInfo.active button:hover {\n  background-color: var(--color-font-grey)666;\n  border-color: var(--color-font-grey)666;\n}\n\n.desktop .gramb .uncount > p .cnt:hover {\n  color: #00791c;\n}\n\n.desktop .lang:hover span,\n.desktop .exg .ex .lang:hover,\n.desktop .trg .tr .lang:hover {\n  color: #565656;\n}\n\n.subSenses {\n  padding-left: 0;\n  list-style: none;\n  font-size: 1em;\n  line-height: 1.2;\n  font-family: 'Open Sans', sans-serif;\n}\n\n.subSenses .subSense {\n  padding-left: 3em;\n}\n\n.subSenses .subsenseIteration {\n  display: inline-block;\n  font-weight: 700;\n  font-size: 1em;\n  position: relative;\n  right: 2em;\n  width: 0px;\n}\n\n.controller__thesaurus .exampleGroup {\n  font-family: 'Merriweather', serif;\n  font-size: 1em;\n}\n\n.controller__thesaurus .senseInnerWrapper > p,\n.controller__thesaurus .phraseInnerWrapper > p {\n  display: block;\n  margin: 0 0 16px;\n}\n\n.controller__thesaurus .iteration {\n  font-weight: 700;\n  padding-right: 10px;\n}\n\n.controller__thesaurus a.hw.core {\n  font-weight: 700;\n}\n\n.controller__thesaurus .phrasesGroupSections {\n  border-top: 2px solid #888;\n}\n\n.controller__thesaurus .phrasesGroupTitle {\n  font-family: 'Merriweather', serif;\n  font-size: 1.1em;\n  font-weight: 600;\n  margin: 12px 0;\n  margin-bottom: 18px;\n}\n\n.controller__thesaurus .phrases-title {\n  font-size: 1.1em;\n}\n\n.controller__thesaurus .phrase {\n  font-family: 'Merriweather', serif;\n  font-size: 1.1em;\n  margin-bottom: 15px;\n  font-weight: 600;\n}\n\n.entry .entryHead h2 .pos {\n  font-size: 0.5em !important;\n}\n\n.pronunciations {\n  margin: 10px 0;\n}\n\n.phoneticSpelling {\n  margin: 0 5px;\n}\n\n.entryHead a.headwordAudio {\n  margin: 0 10px;\n}\n\n.pronunciations {\n  font-family: 'Open Sans', 'Arial', sans-serif;\n  font-size: 1em;\n  line-height: 1.2;\n  font-weight: 400;\n  margin-bottom: 18px;\n}\n\n.pronunciations a.headwordAudio {\n  margin: -2px 10px;\n}\n\n.phrase_sense {\n  margin-left: 0px;\n  padding-left: 0px !important;\n}\n\n.phrase_sense.numbered {\n  margin-left: 24px;\n}\n\n.phrase {\n  font-family: 'Merriweather', serif;\n  font-size: 1em;\n}\n\n#content.entry-ad-code-speech-mpu .dictLexico-Lex .banbox {\n  padding-top: 0;\n}\n\n#content.entry-ad-code-speech-mpu .dictLexico-Lex .banbox {\n  height: 250px !important;\n}\n\n#content.entry-ad-code-speech-mpu\n  .dictLexico-Lex\n  .banbox\n  .container.mpu\n  .banner {\n  height: 250px !important;\n}\n\n.homographs {\n  border: 2px solid #dfeaf0;\n  padding: 3px 5px;\n  background-color: #f8fafe;\n  font-weight: 600;\n  font-size: 1em;\n}\n\n.homographs a {\n  margin-right: 8px;\n  color: #3c9ae3;\n}\n\n.homographs a:visited {\n  color: #3c9ae3;\n}\n\n.homographs em {\n  font-weight: bold;\n}\n\n.homographs h1,\n.homographs h2 {\n  margin-bottom: 1.5em !important;\n  font-weight: normal !important;\n  font-family: Open sans, Helvetica, Arial, sans-serif !important;\n}\n\n.homographs h1,\n.homographs h2 {\n  display: inline !important;\n  max-width: 100%;\n  font-size: 1em !important;\n}\n\n.grammatical_note {\n  color: var(--color-brand);\n  font-weight: normal;\n  font-size: 1em;\n}\n\n.grammatical_note:before {\n  content: '[';\n}\n\n.grammatical_note:after {\n  content: ']';\n}\n\n.synonyms,\n.examples {\n  display: inline;\n}\n\n.synonyms .moreInfo,\n.examples .moreInfo {\n  display: inline;\n  margin-right: 8px;\n}\n\n.exs {\n  font-size: 1em;\n  line-height: 1.2;\n  margin-bottom: 8px;\n  font-weight: normal;\n  font-family: 'Open Sans', Helvetica, Arial, sans-serif;\n}\n\n.exs strong {\n  font-weight: bold;\n}\n\n.sense-regions {\n  margin-right: 8px;\n}\n\n.gramb p.note {\n  font-size: 1em;\n  position: relative;\n  line-height: 1.2;\n  margin-top: 7px;\n  margin-bottom: 9px;\n  padding: 0 10px;\n  border-left: 3px solid #ccc;\n  padding-left: 20px;\n}\n\n.gramb p.note + p.note {\n  margin-top: -10px;\n  padding-top: 12px;\n}\n\n.gramb .semb p.note {\n  font-size: 1em;\n  position: relative;\n  line-height: 1.2;\n  margin-top: 7px;\n  margin-bottom: 9px;\n  padding: 0 10px;\n  border-left: 3px solid #ccc;\n  padding-left: 20px;\n}\n\n.gramb .semb p.note + p.note {\n  margin-top: -10px;\n  padding-top: 12px;\n}\n\nspan.transitivity {\n  font-weight: bold;\n  margin-bottom: 1px;\n  display: block;\n  margin-right: 3.6px;\n  font-size: 1em;\n  text-transform: uppercase;\n}\n\n.trg span.transitivity {\n  font-weight: normal;\n  display: inline;\n}\n\n.sense-regions,\n.sense-registers {\n  color: #f15a24;\n  font-size: 1em;\n}\n\n.domain_labels {\n  color: var(--color-brand);\n  font-size: 1em;\n}\n\n.lex-filling > div.dictLexico-Lex {\n  padding-bottom: 10px;\n  min-height: 590px;\n}\n\n.dictLexico-Lex .searchHeading {\n  font-size: 2em;\n  padding-left: 20px;\n}\n\n.dictLexico-Lex .search-results li a {\n  padding-left: 20px;\n  font-family: 'Open Sans', Helvetica, Arial, sans-serif;\n  font-size: 1.1em;\n  color: #3c9ae3;\n}\n\n.dictLexico-Lex .search-results li a:hover {\n  text-decoration: underline;\n}\n\na.ipaLink {\n  display: none;\n}\n\n.indicators {\n  color: var(--color-brand);\n  line-height: 1.2;\n  font-style: normal;\n  font-size: 1em;\n  font-family: 'Open Sans', sans-serif;\n  position: relative;\n  line-height: 1.2;\n  font-style: italic !important;\n}\n\n.spanish_label {\n  margin-top: -7px;\n  margin-right: 0px;\n  font-weight: normal !important;\n  font-size: 16px !important;\n  line-height: 16px !important;\n}\n\nspan.hw.head-translation {\n  color: #787878;\n}\n\n.collocations,\n.indicator_tags {\n  font-style: normal;\n  color: var(--color-brand) !important;\n  font-size: 16px !important;\n  font-weight: 400 !important;\n  font-family: 'Open Sans', sans-serif;\n  position: relative;\n}\n\n.phrases ul li {\n  padding-left: 0px;\n}\n\n.subsense_definitions {\n  font-size: 1em;\n}\n\nsection.etymology.etym.usage {\n  border-top: 2px solid #888;\n  padding-top: 4px;\n}\n\n.synonyms .exg > div {\n  margin-bottom: 6px;\n}\n\n.socialSignin {\n  padding-top: 1em;\n}\n\n.socialSigninButtons a {\n  width: 220px;\n  display: inline-block;\n  text-transform: none;\n  color: #fff;\n  background-repeat: no-repeat;\n  background-position: 14px center;\n  font-size: 1em;\n  padding: 0.9em 1em;\n  line-height: 1em;\n  border-radius: 5px;\n  margin-right: 1em;\n  margin-bottom: 1em;\n}\n\n.socialSigninButtons a:hover {\n  color: #fff;\n}\n\n.socialSigninButtons a i {\n  width: 20px;\n}\n\n.socialSigninButtons a.facebookSignin {\n  background-color: #005ab1;\n}\n\n.socialSigninButtons a.twitterSignin {\n  background-color: #55acee;\n}\n\n.socialSigninButtons a.googleSignin {\n  background-color: #d93b2b;\n}\n\n.dictionary__es .controller__bilingual_words .exg > .ex em:last-child {\n  font-family: 'Merriweather', sans-serif !important;\n}\n\n.dictionary__es .controller__bilingual_words .ex em {\n  font-family: 'Open Sans', sans-serif !important;\n}\n\n.dictionary__es .controller__bilingual_words .ex em:first-child {\n  font-style: normal;\n}\n\n.dictionary__es .controller__bilingual_words .english-ex {\n  font-family: 'Open Sans', Helvetica, Arial, sans-serif;\n}\n\n.dictionary__es .tablet.portrait .dictionary__es .entryHead .socials,\n.dictionary__es .tablet.landscape .dictionary__es .entryHead .socials {\n  position: relative;\n  float: right;\n  margin: 70px 0 7px 15px;\n}\n\n.dictionary__es .rsbtn.initd,\n.dictionary__es .headwordAudio.initd {\n  display: inline-block;\n}\n\n.dictionary__es .exg > .ex em {\n  font-family: 'Merriweather', sans-serif;\n}\n\n.dictionary__en .exg > .ex em {\n  font-family: 'Merriweather', sans-serif;\n}\n\n.controller__thesaurus .homographs h1 {\n  font-size: 100%;\n  display: inline;\n}\n\n.controller__thesaurus .homographs {\n  border: 2px solid #dfeaf0;\n  padding: 8px;\n  padding-bottom: 5px;\n  background-color: #f8fafe;\n  font-weight: 600;\n  margin-bottom: 0.95em;\n  line-height: 1.6em;\n  font-size: 1em;\n}\n\n.controller__thesaurus .homographs a {\n  margin-right: 8px;\n  color: #3c9ae3;\n}\n\n.controller__thesaurus .homographs a:visited {\n  color: #3c9ae3;\n}\n\n.controller__thesaurus .homographs em {\n  font-weight: bold;\n}\n\n.controller__thesaurus .homographs h1,\n.controller__thesaurus .homographs h2 {\n  margin-bottom: 1.5em !important;\n  font-weight: normal !important;\n  font-family: Open sans, Helvetica, Arial, sans-serif !important;\n}\n\n.controller__thesaurus .homographs h1,\n.controller__thesaurus .homographs h2 {\n  display: inline !important;\n  max-width: 100%;\n  font-size: 16px !important;\n}\n\n.controller__thesaurus .grammatical_note {\n  font-weight: normal;\n  font-size: 1em;\n}\n\n.controller__thesaurus .grammatical_note:before {\n  content: '[';\n}\n\n.controller__thesaurus .grammatical_note:after {\n  content: ']';\n}\n\n.controller__thesaurus .synonyms,\n.controller__thesaurus .examples {\n  display: inline;\n}\n\n.controller__thesaurus .synonyms .moreInfo,\n.controller__thesaurus .examples .moreInfo {\n  display: inline;\n  margin-right: 8px;\n}\n\n.controller__thesaurus .exs {\n  font-size: 1em;\n  line-height: 1.2;\n  margin-bottom: 8px;\n  font-weight: normal;\n  font-family: 'Open Sans', Helvetica, Arial, sans-serif;\n}\n\n.controller__thesaurus .exs strong {\n  font-weight: bold;\n}\n\n.controller__thesaurus .sense-regions {\n  margin-right: 8px;\n}\n\n.controller__thesaurus .gramb ol.subSenses {\n  margin-top: 19px;\n}\n\n.controller__thesaurus .gramb ol.subSenses li {\n  margin-bottom: 19px;\n}\n\n.controller__thesaurus .gramb p.note {\n  font-size: 1em;\n  position: relative;\n  line-height: 1.2;\n  margin-top: 7px;\n  margin-bottom: 9px;\n  padding: 0 10px;\n  border-left: 3px solid #ccc;\n  padding-left: 20px;\n}\n\n.controller__thesaurus .gramb p.note + p.note {\n  margin-top: -10px;\n  padding-top: 12px;\n}\n\n.controller__thesaurus .gramb .semb p.note {\n  font-size: 1em;\n  position: relative;\n  line-height: 1.2;\n  margin-top: 7px;\n  margin-bottom: 9px;\n  padding: 0 10px;\n  border-left: 3px solid #ccc;\n  padding-left: 20px;\n}\n\n.controller__thesaurus .gramb .semb p.note + p.note {\n  margin-top: -10px;\n  padding-top: 12px;\n}\n\n.word_type.pos {\n  text-transform: uppercase;\n  margin-bottom: 5px;\n}\n\n.form-groups {\n  margin-right: 0.5em;\n}\n\na[data-value='view synonyms'] {\n  display: inline-block;\n  margin-bottom: 0.5em;\n}\n\n.exg {\n  padding-left: 0.5em;\n  border-left: 1px solid var(--color-font-grey);\n}\n\n@media screen and (max-width: 1290px) {\n  .container {\n    padding: 0 20px;\n  }\n  .container.full {\n    padding: 0 5px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .container {\n    padding: 0 20px;\n  }\n  .container.full {\n    padding: 0 10px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .container {\n    padding: 0 10px;\n  }\n  .container.full {\n    padding: 0 5px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .topHeader {\n    display: none;\n  }\n}\n\n@media screen and (max-width: 900px) {\n  .topHeader .nav > ul > li.mob {\n    display: none;\n  }\n}\n\n@media screen and (max-width: 900px) {\n  .topHeader .nav > ul > li.more {\n    display: inline-block;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .topHeader .nav > ul .drop ul {\n    width: 190px;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .topHeader .nav > ul .drop ul li a {\n    padding: 5px 14px 3px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .topHeader .signIn {\n    margin-right: 18px;\n  }\n  .topHeader .signIn a span {\n    padding: 0;\n  }\n}\n\n@keyframes dropIn {\n  from {\n    transform: translate(0, -100%);\n  }\n  to {\n    transform: translate(0, 0);\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .headerContent .logo {\n    height: 65px;\n    margin: 44px 20px 33px 152px;\n  }\n  .headerContent .logo img,\n  .headerContent .logo svg {\n    height: 65px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .headerContent .logo {\n    height: 45px;\n    margin: 17px 0 14px 106px;\n  }\n  .headerContent .logo img,\n  .headerContent .logo svg {\n    height: 45px;\n    width: 110px;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .headerContent .searchField {\n    margin-left: 344px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .headerContent .searchField {\n    margin-left: 245px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .headerContent .searchField .beforeSearch {\n    display: none;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .headerContent .searchField .powered {\n    display: none;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .headerContent .searchField {\n    float: left;\n    margin: 0;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .headerContent .searchField .searchMain {\n    padding: 15px 0 0;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .headerContent .searchField .searchMain {\n    padding: 0;\n    position: relative;\n    padding-top: 45px;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .headerContent .searchField .searchMain form .sbOptions a {\n    padding: 14px 15px 10px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .headerContent .searchField .searchMain form .sbHolder {\n    height: 45px;\n    width: 166px;\n  }\n  .headerContent .searchField .searchMain form .sbHolder .sbToggle {\n    height: 6px;\n    width: 13px;\n    right: 13px;\n  }\n  .headerContent .searchField .searchMain form .sbHolder .sbSelector {\n    height: 45px;\n    line-height: 1.2;\n  }\n  .headerContent .searchField .searchMain form .sbHolder .sbOptions {\n    margin-top: 2px;\n    width: 166px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .headerContent .searchField .searchMain form .sbHolder {\n    display: none;\n    border-radius: 5px;\n    float: none;\n    height: 35px;\n    width: 100%;\n    margin-bottom: 10px;\n  }\n  .headerContent .searchField .searchMain form .sbHolder .sbSelector {\n    height: 35px;\n    line-height: 1.2;\n    border-radius: 5px;\n  }\n  .headerContent .searchField .searchMain form .sbHolder .sbOptions {\n    width: 100%;\n  }\n  .headerContent .searchField .searchMain form .sbToggleOpen + .sbSelector {\n    border-radius: 5px 5px 0 0;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .headerContent .searchField .searchMain form fieldset input {\n    border-radius: 5px;\n    padding: 0 44px 0 11px;\n    height: 35px;\n    border-style: solid;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .headerContent .searchField .searchMain form fieldset.ac-active input {\n    border-radius: 5px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .headerContent .searchField .searchMain form fieldset .keyboard {\n    display: none;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .headerContent .searchField .searchMain form fieldset button[type='submit'] {\n    top: 0;\n    right: 0;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .headerContent .searchField .searchMain form fieldset .autocompleteBox {\n    left: 9px;\n    right: 41px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .headerContent .searchField .searchMain form fieldset .ui-autocomplete li {\n    font-size: 1em;\n    padding: 3px 15px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .headerContent .searchField .searchMain form fieldset {\n    margin-left: 166px;\n  }\n  .headerContent .searchField .searchMain form fieldset input {\n    height: 45px;\n    font-size: 1em;\n    padding: 0 100px 0 14px;\n  }\n  .headerContent .searchField .searchMain form fieldset .keyboard {\n    right: 49px;\n  }\n  .headerContent .searchField .searchMain form fieldset button[type='submit'] {\n    height: 40px;\n    width: 40px;\n  }\n  .headerContent\n    .searchField\n    .searchMain\n    form\n    fieldset\n    button[type='submit']\n    svg {\n    height: 22px;\n    width: 22px;\n  }\n  .headerContent .searchField .searchMain form .sbSelector {\n    font-size: 1em;\n    padding: 2px 0 0 12px;\n  }\n  .headerContent .searchField .searchMain form .sbOptions a {\n    font-size: 1em;\n    padding: 14px 15px 13px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .headerContent .searchField .searchMain form fieldset {\n    margin: 0 0 10px;\n    left: 0;\n    position: absolute;\n    right: 0;\n    top: 0;\n  }\n  .headerContent .searchField .searchMain form fieldset input {\n    border-radius: 5px;\n    padding: 0 44px 0 11px;\n    height: 35px;\n    border-style: solid;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .mainHeader {\n    position: relative !important;\n    animation: none !important;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .mainHeader.fixed {\n    display: block;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .mainHeader.fixed .headerContent .logo {\n    width: 55px;\n    height: 55px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .mainHeader.fixed .headerContent .logo {\n    width: 45px;\n    height: 45px;\n    margin: 15px 0;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .mainHeader.fixed .headerContent .logo {\n    width: 100%;\n    height: 42px;\n    margin: 9px 0;\n    padding: 0;\n    display: block;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .mainHeader.fixed .headerContent .logo img,\n  .mainHeader.fixed .headerContent .logo svg {\n    width: 55px;\n    height: 55px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .mainHeader.fixed .headerContent .logo img,\n  .mainHeader.fixed .headerContent .logo svg {\n    height: 45px;\n    width: 45px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .mainHeader.fixed .headerContent .logo img,\n  .mainHeader.fixed .headerContent .logo svg {\n    display: none;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .mainHeader.fixed .headerContent .logo img,\n  .mainHeader.fixed .headerContent .logo svg {\n    display: block;\n    height: 42px;\n    width: 110px;\n    margin: 0 auto;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .mainHeader.fixed .headerContent .searchField {\n    margin-right: 132px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .mainHeader.fixed .headerContent .searchField {\n    padding: 0;\n    margin-left: 0;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .mainHeader.fixed .headerContent .searchField {\n    margin: 0;\n    width: 100%;\n    height: auto;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .mainHeader.fixed .burger {\n    right: 0;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .mainHeader.fixed .burger {\n    background-color: transparent;\n    bottom: auto;\n    height: 22px;\n    line-height: 1.2;\n    padding: 0;\n    right: 6px;\n    top: 16px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .mainHeader.fixed .burger span {\n    display: none;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .mainHeader.fixed {\n    -webkit-transition: none;\n    -moz-transition: none;\n    -ms-transition: none;\n    -o-transition: none;\n    transition: none;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  #footer .footerWrap {\n    padding-bottom: 86px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  #footer .copyright {\n    padding: 0 9px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .footerNav nav.sharing {\n    margin-right: 51px;\n  }\n  .footerNav nav.sharing:lang(ur) {\n    margin-right: 0px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .footerNav nav {\n    margin-right: 45px;\n  }\n  .footerNav nav .sharing {\n    margin-right: 50px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .footerNav {\n    padding: 28px 0 11px;\n    margin: 0 11px 13px;\n  }\n  .footerNav nav {\n    display: block;\n    margin: 0 0 27px;\n  }\n  .footerNav .brand_logo {\n    display: block;\n    padding: 5px 0;\n    position: static;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .mobile_banner {\n    background-color: #f5f5f5;\n    border-top: 1px solid #a2a2a2;\n    bottom: 0px;\n    z-index: 99;\n    position: fixed;\n    width: 100%;\n    -webkit-box-shadow: 0px 15px 20px 4px black;\n    -moz-box-shadow: 0px 15px 20px 4px black;\n    box-shadow: 0px 15px 20px 4px black;\n  }\n  .banbox-mini {\n    display: none !important;\n  }\n}\n\n@media screen and (max-width: 670px) {\n}\n\n@media screen and (max-width: 1290px) {\n  .banbox .banner {\n    max-width: 100%;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .banbox {\n    padding: 8px 0;\n  }\n  .banbox .container {\n    padding: 0;\n  }\n  .banbox .banner {\n    width: 320px;\n    height: 50px !important;\n  }\n  .banbox.violbox .content-ban .banner {\n    height: 250px !important;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .banbox-mini .banner {\n    padding: 0;\n    background-color: transparent;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .banbox-mini .banner {\n    padding-top: 30px;\n    background-color: #fff;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .banbox-mini .banner {\n    padding: 0;\n    background-color: transparent;\n  }\n}\n\n@media screen and (min-width: 1290px) {\n  .banbox-mini .banner.marked-ad > div > div::before {\n    top: -30px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .video.video-md .box-img:before {\n    width: 46px;\n    height: 46px;\n    -webkit-background-size: 46px 46px;\n    background-size: 46px 46px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .further_reading,\n  h3.title {\n    padding: 39px 0 24px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .further_reading,\n  h3.title {\n    font-size: 1.1em;\n    padding: 24px 10px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .h-box h3.title {\n    padding: 31px 0 29px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .h-box h3.title {\n    padding: 31px 0 23px;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .news {\n    padding: 31px 0 17px;\n  }\n  .news section {\n    padding: 9px 15px;\n  }\n  .news section a h2 {\n    margin-bottom: -6px;\n  }\n  .news section a .box-img {\n    width: 116px;\n    height: 68px;\n  }\n  .news section.mainNews h1 {\n    font-size: 1em;\n    line-height: 1.2;\n    padding: 0 27px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .news {\n    padding: 19px 0 0;\n  }\n  .news section {\n    width: 50%;\n    padding: 9px 10px;\n  }\n  .news section a {\n    padding-bottom: 18px;\n  }\n  .news section a h2 {\n    line-height: 1.2;\n  }\n  .news section.mainNews {\n    width: 50%;\n  }\n  .news section.mainNews a {\n    height: auto;\n  }\n  .news section.mainNews a .box-img {\n    height: 211px;\n  }\n  .news section.mainNews a .box-img:after {\n    display: none;\n  }\n  .news section.mainNews h1 {\n    position: relative;\n    bottom: auto;\n    float: left;\n    width: 100%;\n    padding-bottom: 10px;\n    font: 24px 'Open Sans', Helvetica, Arial, sans-serif;\n    font-family: inherit;\n    color: #333;\n    padding: 10px 12px;\n    border: 1px dotted #ccc;\n    border-top: none;\n  }\n  .news section.mainNews h1 i {\n    font-family: 'Open Sans', Helvetica, Arial, sans-serif;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .news {\n    padding: 14px 0 0;\n  }\n  .news section {\n    padding: 5px;\n    width: 100%;\n  }\n  .news section a {\n    margin-bottom: 5px;\n    padding-bottom: 17px;\n  }\n  .news section a h4 {\n    line-height: 1.2;\n  }\n  .news section.mainNews {\n    width: 100%;\n  }\n  .news section.mainNews a {\n    margin-bottom: 11px;\n    padding: 0;\n  }\n  .news section.mainNews a .box-img {\n    height: 177px;\n  }\n  .news section.mainNews h1 {\n    font-size: 1em;\n    padding-bottom: 11px;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .words.newWords {\n    padding-top: 13px;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .words {\n    padding-top: 25px;\n  }\n  .words > div {\n    padding: 15px;\n    height: 375px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .words {\n    padding: 13px 0;\n    font-size: 0;\n    text-align: center;\n  }\n  .words > div {\n    display: inline-block;\n    width: 50%;\n    height: 363px;\n    padding: 10px;\n    font-size: 1em;\n    vertical-align: top;\n  }\n  .words > div.banbox-mini {\n    height: 329px;\n    min-height: initial;\n    float: none;\n    width: 380px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .words {\n    padding: 15px 0 10px;\n  }\n  .words {\n    display: flex;\n    flex-wrap: wrap-reverse;\n  }\n  .words > div {\n    padding: 5px;\n    width: 100%;\n    height: auto;\n    margin-bottom: 10px;\n  }\n  .words > div.banbox-mini {\n    width: 310px;\n    height: 260px;\n    width: 310px;\n    margin: 0px auto 10px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .centerableWrapper {\n    margin: auto;\n    float: none !important;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .trend section .popular p,\n  .contentPanel section .popular p {\n    padding-bottom: 3px;\n  }\n  .trend section ol li a,\n  .contentPanel section ol li a {\n    line-height: 1.2;\n    display: block;\n  }\n  .trend section ol li:before,\n  .contentPanel section ol li:before {\n    top: 2px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .trend section ol li:before,\n  .contentPanel section ol li:before {\n    top: 3px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .trend section,\n  .contentPanel section {\n    padding: 27px 23px 17px;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .dayword section .daywordmain p {\n    padding: 0 28px 2px;\n  }\n  .dayword section form fieldset label {\n    width: 67%;\n  }\n  .dayword section form fieldset button {\n    width: 30%;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .dayword section .daywordmain p {\n    padding: 0 40px;\n  }\n  .dayword section form {\n    padding: 13px;\n  }\n  .dayword section form fieldset label {\n    width: 68%;\n  }\n  .dayword section form fieldset button {\n    width: 29%;\n    letter-spacing: 0.8px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .dayword section h3 {\n    margin-bottom: 28px;\n  }\n  .dayword section .daywordmain {\n    height: auto;\n  }\n  .dayword section .daywordmain > div {\n    height: auto;\n  }\n  .dayword section .daywordmain strong {\n    margin-bottom: 10px;\n  }\n  .dayword section .daywordmain p {\n    padding: 0 30px;\n  }\n  .dayword section form {\n    padding: 14px 13px;\n  }\n  .dayword section form fieldset label {\n    width: -webkit-calc(100% - 50px);\n    width: -moz-calc(100% - 50px);\n    width: -ms-calc(100% - 50px);\n    width: calc(100% - 50px);\n    margin-right: 8px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n}\n\n@media (max-width: 768px) {\n}\n\n@media (max-width: 550px) {\n}\n\n@media screen and (max-width: 1290px) {\n}\n\n@media screen and (max-width: 1023px) {\n}\n\n@media screen and (max-width: 767px) {\n}\n\n@media screen and (max-width: 767px) {\n  .quizzes > div section article a.tertiary-link {\n    font-size: 1em;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .quizzes > div {\n    padding: 0 15px;\n  }\n  .quizzes > div section article {\n    margin: -19% 11px 0;\n    padding: 20px 25px 25px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .quizzes > div {\n    padding: 0 10px;\n    width: 50%;\n    margin-bottom: 21px;\n  }\n  .quizzes > div section .box-img {\n    height: 210px;\n  }\n  .quizzes > div section article {\n    margin: -19% 11px 0;\n    padding: 20px 25px 25px;\n  }\n  .quizzes .banbox-mini {\n    height: 308px;\n    margin-bottom: 45px;\n    width: 380px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .quizzes > div {\n    padding: 0 5px;\n  }\n  .quizzes > div section .box-img {\n    height: 172px;\n  }\n  .quizzes > div section article h4 {\n    font-size: 1em;\n  }\n  .quizzes > div section article a {\n    padding: 3px 21px 0 0;\n  }\n  .quizzes .banbox-mini {\n    height: 250px;\n    width: 310px;\n    margin-bottom: 32px;\n  }\n}\n\n@media screen and (max-width: 550px) {\n  .quizzes > div {\n    width: 100%;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .faces section {\n    padding: 0 55px;\n  }\n  .faces section h4 {\n    margin-bottom: 20px;\n  }\n  .faces section p {\n    line-height: 1.2;\n  }\n  .faces .faces-pic {\n    background-size: cover;\n    height: 486px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .faces {\n    position: relative;\n    padding-bottom: 162px;\n  }\n  .faces > div {\n    width: 100%;\n  }\n  .faces .faces-pic {\n    background-position: -2px 0;\n    background-size: cover;\n    height: 162px;\n    position: absolute;\n    left: 0;\n    right: 0;\n    bottom: 0;\n  }\n  .faces section {\n    padding: 27px 14px 20px;\n  }\n  .faces section h4 {\n    font-size: 1em;\n    margin-bottom: 10px;\n    line-height: 1.2;\n  }\n  .faces section p {\n    line-height: 1.2;\n    margin-bottom: 17px;\n    font-size: 1em;\n  }\n  .faces section a {\n    padding: 11px 16px;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .quotes {\n    padding: 44px 0 45px;\n  }\n  .quotes > div {\n    padding: 15px;\n  }\n  .quotes .blogbox article {\n    margin: -75px 13px 0;\n    padding: 20px 20px 50px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .quotes {\n    padding: 33px 0 32px;\n  }\n  .quotes > div {\n    padding: 10px;\n  }\n  .quotes .blogbox article {\n    margin: -42px 11px 0;\n    padding: 20px 20px 29px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .quotes {\n    padding: 13px 0 15px;\n  }\n  .quotes > div {\n    width: 100%;\n  }\n  .quotes .blogbox .box-img {\n    height: 172px;\n  }\n  .quotes .blogbox article {\n    margin: -63px 8px 0;\n    padding: 17px 16px 19px;\n  }\n  .quotes .blogbox article h2 {\n    margin-bottom: 5px;\n  }\n  .quotes .blogbox article h4 {\n    font-size: 1em;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .quotewrap .auth {\n    font-size: 1em;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .quotewrap {\n    padding: 36px 30px 30px;\n  }\n  .quotewrap .quotemain {\n    height: auto;\n    padding: 22px 0;\n  }\n  .quotewrap .quotemain p {\n    font-size: 1.1em;\n    line-height: 1.25;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .videos ul li a h2 {\n    font-size: 1em;\n    margin-top: -10px;\n    line-height: 1.2;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .videos ul li {\n    padding: 5px 15px;\n    margin-bottom: 65px;\n  }\n  .videos ul li a .box-img {\n    height: 132px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .videos ul li {\n    padding: 0 10px;\n    width: 50%;\n    margin-bottom: 50px;\n    position: relative;\n  }\n  .videos ul li:nth-child(3):before,\n  .videos ul li:nth-child(4):before {\n    border-top: 1px dotted #cccccc;\n    content: '';\n    height: 0;\n    left: 10px;\n    position: absolute;\n    right: 10px;\n    top: -27px;\n  }\n  .videos ul li a .box-img {\n    height: 208px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .videos ul li {\n    padding: 0 5px;\n    margin-bottom: 38px;\n  }\n  .videos ul li a .box-img {\n    height: 82px;\n  }\n  .videos ul li:nth-child(3):before,\n  .videos ul li:nth-child(4):before {\n    top: -19px;\n    left: 5px;\n    right: 5px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .violbox {\n    margin-bottom: -20px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  h3.title {\n    font-size: 28px !important;\n  }\n  .quotes .blogbox article h4 {\n    font-size: 16px !important;\n  }\n  .quotes .blogbox article h2 {\n    font-size: 20px !important;\n  }\n  .faces section h2 {\n    font: 28px 'Open Sans', Helvetica, Arial, sans-serif !important;\n  }\n  .faces section a {\n    padding: 7px 12px !important;\n    font: 14px 'Open Sans', Helvetica, Arial, sans-serif !important;\n  }\n  .quizzes > div section article h2 {\n    font: 18px 'Open Sans', Helvetica, Arial, sans-serif !important;\n  }\n  .quizzes > div section > a > article {\n    min-height: 0px !important;\n    padding-top: 0;\n  }\n  .quizzes > div section > article {\n    min-height: 0px !important;\n  }\n}\n\n@media screen and (max-width: 767px) {\n}\n\n@media screen and (max-width: 1290px) {\n  .sidebar {\n    margin-top: 0;\n  }\n  .sidebar .news section a article {\n    padding: 7px 8px 12px;\n  }\n}\n\n@media screen and (max-width: 549px) {\n  .sidebar .sidebar-content .contribute {\n    margin-left: 0px !important;\n    margin-right: 0px !important;\n    margin-top: 24px !important;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .sidebar {\n    width: 100%;\n    margin: 0;\n  }\n  .sidebar .sidebar-content {\n    margin: 0 -10px;\n    font-size: 0;\n    text-align: center;\n  }\n  .sidebar .sidebar-content .contribute {\n    padding-bottom: 0px;\n    padding-top: 29px;\n    margin-top: 0px;\n    margin-bottom: 23px;\n    margin-left: 10px;\n    margin-right: 10px;\n  }\n  .sidebar .sidebar-content .contribute h4 {\n    font-size: 1.1em;\n    line-height: 1.25em;\n    color: #fff;\n    margin-bottom: 0.4em;\n  }\n  .sidebar .sidebar-content .contribute a {\n    font-size: 1em;\n  }\n  .sidebar .dayword {\n    display: inline-block;\n    width: 100%;\n    vertical-align: top;\n    padding: 0 10px;\n    -webkit-box-sizing: border-box;\n    -moz-box-sizing: border-box;\n    box-sizing: border-box;\n    margin-bottom: 25px;\n    margin-top: 20px;\n  }\n  .sidebar .dayword section .daywordmain {\n    margin-top: -34px;\n    height: 196px;\n  }\n  .sidebar .dayword section .daywordmain > div {\n    height: 123px;\n  }\n  .sidebar .dayword section .daywordmain > div > div strong {\n    margin-bottom: 0;\n  }\n  .sidebar .banbox-mini {\n    float: left;\n    width: 100%;\n    vertical-align: top;\n    margin: -15px 0 15px;\n  }\n  .sidebar .banbox-mini.mobile-ver {\n    display: inline-block;\n  }\n  .sidebar .banbox-mini .banner {\n    margin: 0 auto 15px;\n  }\n  .sidebar .banbox-mini + .signup-wrap {\n    clear: both;\n    margin-top: 0;\n  }\n  .sidebar .news {\n    padding: 0;\n  }\n  .sidebar .news section {\n    display: inline-block;\n    width: 50%;\n    vertical-align: top;\n    padding: 0 10px;\n    background-color: transparent;\n    border: none;\n    text-align: left;\n  }\n  .sidebar .news section.no-visible-mob {\n    display: none;\n  }\n  .sidebar .news section.no-visible-desk {\n    display: inline-block;\n  }\n  .sidebar .news section:nth-last-child(1):nth-of-type(odd) {\n    float: none;\n    clear: both;\n    display: block;\n    margin: 0 auto 22px;\n  }\n  .sidebar .news section a {\n    background-color: #ffffff;\n    border: 1px solid #d5d5d5;\n  }\n  .sidebar .news section a article {\n    min-height: 42px;\n  }\n  .sidebar .news .sideTestWrap {\n    width: 50%;\n  }\n  .sidebar .news .banbox-mini {\n    margin-top: 10px;\n  }\n  .sidebar .sideTestWrap,\n  .sidebar .sideTrendWrap {\n    display: inline-block;\n    width: 50%;\n    vertical-align: top;\n    padding: 0 10px;\n    -webkit-box-sizing: border-box;\n    -moz-box-sizing: border-box;\n    box-sizing: border-box;\n  }\n  .sidebar .signup-wrap {\n    display: inline-block;\n    width: 100%;\n    padding: 0 10px;\n    margin-bottom: 37px;\n    font-size: 1em;\n  }\n  .sidebar .signup-wrap .signup {\n    -webkit-box-sizing: border-box;\n    -moz-box-sizing: border-box;\n    box-sizing: border-box;\n  }\n  .sidebar .tests {\n    -webkit-box-sizing: border-box;\n    -moz-box-sizing: border-box;\n    box-sizing: border-box;\n  }\n}\n\n@media screen and (max-width: 900px) {\n  .sidebar .news section a article {\n    min-height: 60px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .sidebar .sidebar-content {\n    margin: 20px 0;\n    padding: 0 10px;\n    text-align: center;\n  }\n  .sidebar .dayword section h4 {\n    margin-bottom: 12px;\n  }\n  .sidebar .dayword section .daywordmain {\n    height: auto;\n  }\n  .sidebar .dayword section .daywordmain > div {\n    height: auto;\n  }\n  .sidebar .dayword section .daywordmain > div > div strong {\n    margin-bottom: 16px;\n  }\n  .sidebar .dayword section form {\n    height: auto;\n  }\n  .sidebar .news section a article {\n    min-height: 60px;\n    text-align: left;\n  }\n  .sidebar .banbox-mini {\n    margin-bottom: 22px;\n    width: 100%;\n  }\n  .sidebar .banbox-mini.mobile-ver {\n    display: block;\n    margin-left: auto;\n    margin-right: auto;\n  }\n  .sidebar .signup-wrap {\n    margin: 0 auto 28px;\n  }\n}\n\n@media screen and (max-width: 550px) {\n  .sidebar .dayword {\n    width: auto;\n    display: block;\n    padding: 0;\n    margin-bottom: 10px;\n  }\n  .sidebar .news section {\n    display: block;\n    width: 100%;\n    padding: 0;\n    margin-bottom: 10px;\n  }\n  .sidebar .news section:nth-last-child(1):nth-of-type(odd) {\n    float: left;\n  }\n  .sidebar .news section a {\n    margin: 0;\n  }\n  .sidebar .news section a article {\n    min-height: 0;\n  }\n  .sidebar .news .sideTestWrap {\n    width: 100%;\n  }\n  .sidebar .sideTestWrap,\n  .sidebar .sideTrendWrap {\n    display: block;\n    width: auto;\n    padding: 0;\n    -webkit-box-sizing: border-box;\n    -moz-box-sizing: border-box;\n    box-sizing: border-box;\n  }\n  .sidebar .sideTestWrap > div,\n  .sidebar .sideTrendWrap > div {\n    min-height: 0;\n    height: auto;\n    margin-bottom: 10px;\n  }\n  .sidebar .sideTestWrap {\n    float: left;\n    width: 100%;\n  }\n  .sidebar .banbox-mini {\n    margin-bottom: 10px;\n    margin-top: 5px;\n    width: 100%;\n  }\n  .sidebar .banbox-mini.mobile-ver {\n    display: block;\n  }\n  .sidebar .signup-wrap {\n    width: auto;\n    display: block;\n    width: auto;\n    padding: 0;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .lex-container .banbox {\n    height: 50px !important;\n    background-color: #f5f5f5;\n    border-top: 1px solid #a2a2a2;\n    -webkit-box-shadow: 0px 15px 20px 4px black;\n    -moz-box-shadow: 0px 15px 20px 4px black;\n    box-shadow: 0px 15px 20px 4px black;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .lex-container .banbox .content-ban,\n  .lex-container .banbox .category-ban {\n    padding: 0 20px 0 195px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .lex-container .banbox .content-ban,\n  .lex-container .banbox .category-ban {\n    padding: 0 20px;\n  }\n  .lex-container .banbox .content-ban .banner,\n  .lex-container .banbox .category-ban .banner {\n    margin: 0 auto;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .lex-container .banbox .content-ban .banner {\n    margin: 0 auto 0;\n  }\n  .lex-container .banbox .container {\n    padding: 0;\n  }\n  .lex-container .main-content .container {\n    padding: 0;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .mono-lingual-entry-ad-code-top-mpu .lex-container .banbox .content-ban,\n  .entry-ad-code-top-mpu .lex-container .banbox .content-ban {\n    padding: 0;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .mono-lingual-entry-ad-code-top-mpu .lex-container .banbox .content-ban,\n  .entry-ad-code-top-mpu .lex-container .banbox .content-ban {\n    padding: 0;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .non-lexical-ad-code-top-mpu .lex-container .banbox .content-ban {\n    padding: 0;\n  }\n  .non-lexical-ad-code-top-mpu .lex-container .banbox .content-ban .banner {\n    margin: 0 auto;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .lex-content .lex-filling {\n    padding-right: 0;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .lex-content .lex-filling {\n    padding-left: 0;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .lex-category .lex-filling {\n    padding-right: 0;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .lex-category .lex-filling {\n    padding-left: 0 !important;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .lex-filling .box-img {\n    height: 141px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .lex-filling .box-img {\n    height: 159px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .lex-filling {\n    padding-left: 0;\n  }\n  .lex-filling > div {\n    padding-bottom: 0;\n  }\n  .lex-filling > div.comments {\n    padding-bottom: 32px;\n  }\n}\n\n@media screen and (min-width: 1540px) {\n  .sideBanner {\n    width: 300px !important;\n    margin-left: -111% !important;\n  }\n  .sideBanner .sideBannerWrap {\n    width: 300px !important;\n  }\n  .sideBanner .sideBannerWrap .banner {\n    width: 300px !important;\n  }\n  .sideBanner .sideBannerWrap .adUnit {\n    width: 300px !important;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .sideBanner .sideBannerWrap {\n    margin-top: 0;\n  }\n  .sideBanner .sideBannerWrap .banner.abs {\n    margin-top: -39px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .sideBanner {\n    display: none;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .lex-category .lex-filling {\n    padding-right: 0;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .categories .banbox {\n    display: none;\n  }\n  .categories .banbox-mini {\n    display: block;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .categories .banbox-mini .banner {\n    background-color: transparent;\n    padding: 0;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .categories section a .box-img {\n    min-height: 0;\n    width: 130px;\n    margin-right: 7px;\n  }\n  .categories section a article {\n    padding: 0 10px 0 130px;\n  }\n  .categories section a h2 {\n    font: 18px/22px 'Open Sans', Helvetica, Arial, sans-serif;\n    margin-bottom: 0;\n    padding-left: 7px;\n  }\n  .categories section a h4 {\n    display: none;\n    padding-left: 7px;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .textBlock {\n    padding: 25px 20px;\n  }\n  .textBlock:before {\n    left: 21px;\n    right: 21px;\n  }\n  .textBlock .large {\n    float: none;\n    font-size: 1.1em;\n    line-height: 1.2;\n    margin-bottom: 23px;\n    padding: 37px 37px 0 31px;\n    width: auto;\n  }\n  .textBlock .large:before {\n    height: 7px;\n    left: 32px;\n    top: 19px;\n    width: 80px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .textBlock .large {\n    padding: 34px 37px 0 27px;\n    margin-bottom: 23px;\n  }\n  .textBlock .large:before {\n    left: 26px;\n    top: 14px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .textBlock {\n    padding: 12px 10px 16px;\n  }\n  .textBlock h1 {\n    font: 32px 'Open Sans', Helvetica, Arial, sans-serif;\n    font-family: inherit;\n    text-align: left;\n    margin-bottom: 4px;\n  }\n  .textBlock h1 span {\n    border: none;\n    margin-bottom: 16px;\n    padding-bottom: 0;\n  }\n  .textBlock h2 {\n    font-size: 1em;\n    margin-bottom: 11px;\n    margin-top: 26px;\n  }\n  .textBlock h3 {\n    font-size: 1.1em;\n    margin-bottom: 8px;\n    margin-top: 21px;\n  }\n  .textBlock p {\n    margin-bottom: 14px;\n  }\n  .textBlock p.centered {\n    text-align: left;\n    max-width: none;\n  }\n  .textBlock .large {\n    padding: 38px 25px 0 15px;\n  }\n  .textBlock .large:before {\n    left: 15px;\n    top: 18px;\n  }\n  .textBlock ul li {\n    padding-left: 38px;\n  }\n  .textBlock ul li:before {\n    left: 19px;\n  }\n  .textBlock dl dd {\n    line-height: 1.2;\n    margin: 2px 0 15px 18px;\n    padding: 0 0 3px 8px;\n  }\n  .textBlock ol li {\n    padding-left: 39px;\n  }\n  .textBlock ol li:before {\n    left: 17px;\n  }\n  .textBlock ol.no-num li {\n    padding-left: 20px;\n  }\n  .textBlock:before {\n    left: 12px;\n    right: 12px;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .comments {\n    padding: 31px 21px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .comments {\n    padding: 32px 12px;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .lex .quizzes {\n    padding: 0 10px;\n  }\n  .lex .quizzes > div {\n    margin-bottom: 41px;\n  }\n  .lex .quizzes > div section .box-img {\n    height: 179px;\n  }\n  .lex .quizzes > div section article h4 {\n    font-size: 1em;\n  }\n  .lex .quizzes .banbox-mini {\n    height: auto;\n  }\n  .lex .quizzes .banbox-mini .banner {\n    width: 300px;\n    margin: 0 auto;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .lex.h-box h3.title {\n    padding: 51px 0 39px;\n  }\n  .lex .quizzes {\n    padding: 0 5px;\n  }\n  .lex .quizzes .banbox-mini {\n    width: 50%;\n  }\n  .lex .quizzes .banbox-mini .banner {\n    height: 300px;\n    margin: 0 auto;\n    padding-top: 25px;\n    width: 340px;\n  }\n  .lex .quizzes > div {\n    padding: 0 15px;\n    margin-bottom: 41px;\n  }\n  .lex .quizzes > div section article p {\n    margin-bottom: 9px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .lex.h-box h3.title {\n    font-size: 1em;\n    padding: 51px 0 39px;\n  }\n  .lex .quizzes > div {\n    margin-bottom: 32px;\n  }\n  .lex .quizzes .banbox-mini {\n    padding: 0;\n  }\n  .lex .quizzes .banbox-mini .banner {\n    height: 100%;\n    padding: 0;\n    width: 300px;\n  }\n}\n\n@media screen and (max-width: 620px) {\n  .lex .quizzes > div:first-child {\n    width: -webkit-calc(100% - 300px);\n    width: -moz-calc(100% - 300px);\n    width: -ms-calc(100% - 300px);\n    width: calc(100% - 300px);\n  }\n  .lex .quizzes .banbox-mini {\n    width: 300px;\n  }\n}\n\n@media screen and (max-width: 550px) {\n  .lex .quizzes > div {\n    padding: 0px;\n    margin-bottom: 30px;\n  }\n  .lex .quizzes > div:first-child {\n    width: 100%;\n  }\n  .lex .quizzes .banbox-mini {\n    width: auto;\n    padding: 0;\n  }\n}\n\n@media screen and (max-width: 1100px) {\n  .lex-category .banbox .content-ban .banner {\n    width: 300px;\n    height: 250px;\n    margin-bottom: 10px;\n  }\n  .lex-category .banbox .content-ban .banner div {\n    height: 100%;\n  }\n}\n\n@media screen and (max-width: 767px) {\n}\n\n@media screen and (max-width: 767px) {\n}\n\n@media screen and (max-width: 767px) {\n}\n\n@media (min-width: 768px) {\n}\n\n@media screen and (max-width: 767px) {\n}\n\n@media screen and (max-width: 767px) {\n}\n\n@media screen and (max-width: 767px) {\n}\n\n@media screen and (max-width: 767px) {\n  .sg ul li p {\n    line-height: 1.2;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .topHeader .container.full {\n    padding: 0 23px 0 3px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .topHeader .container.full {\n    padding: 0 20px 0 7px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  #main {\n    padding-top: 0px;\n  }\n  .bluebox {\n    margin: 0 10px 27px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .sidebar .trending-panel-odo,\n  .sidebar .quiz-panel-odo {\n    -webkit-box-sizing: border-box;\n    -moz-box-sizing: border-box;\n    box-sizing: border-box;\n    display: inline-block;\n    width: 99%;\n    vertical-align: top;\n    padding: 0 10px;\n    margin-bottom: 14px;\n  }\n  .quiz-panel {\n    min-height: 270px;\n  }\n}\n\n@media screen and (max-width: 550px) {\n  .quiz-panel {\n    min-height: 100px;\n  }\n}\n\n@media screen and (max-width: 550px) {\n  .sidebar .trending-panel-odo,\n  .sidebar .quiz-panel-odo {\n    width: auto;\n    display: block;\n    width: auto;\n    padding: 0;\n  }\n}\n\n@media (max-width: 550px) {\n  .media,\n  .media-body {\n    margin-bottom: 25px;\n  }\n  .media-left {\n    display: block !important;\n  }\n  .media-object {\n    width: 100%;\n    clear: both;\n    height: auto;\n    min-height: auto;\n    min-width: 375px;\n  }\n  .media-heading {\n    margin-bottom: 5px;\n    line-height: 1em;\n    font-size: 0.8em;\n  }\n}\n\n@media all and (min-width: 768px) {\n  .cookie-container {\n    padding: 0 20px;\n  }\n}\n\n@media all and (min-width: 768px) {\n  #homePage .cookies-eu {\n    padding-bottom: 15px;\n  }\n}\n\n@media (max-width: 549px) {\n  .controller__thesaurus .sidebar,\n  .controller__thesaurus .sidebar__item,\n  .controller__thesaurus .heading__item,\n  .controller__thesaurus .heading__item--fixed {\n    width: 100%;\n  }\n  .controller__thesaurus .heading__item--fixed .banner {\n    padding: 0;\n  }\n  .controller__thesaurus .sidebar {\n    padding: 10px 0;\n  }\n  .controller__thesaurus .main-content {\n    padding: 0;\n  }\n  .controller__thesaurus .banner--left {\n    display: none;\n  }\n  .controller__thesaurus .banner--top {\n    height: auto;\n    margin-bottom: 10px;\n    padding-left: 0;\n  }\n  .controller__thesaurus .heading__item {\n    padding: 10px;\n  }\n  .controller__thesaurus .content {\n    width: 100%;\n    margin-left: 0;\n  }\n  .controller__thesaurus .footer__container {\n    padding: 30px 20px;\n  }\n  .controller__thesaurus .copyright-con {\n    padding: 0 10px;\n  }\n}\n\n@media (max-width: 730px) {\n  .controller__thesaurus .footer__container .in-b {\n    display: block;\n    width: 100%;\n  }\n  .controller__thesaurus .footer__item {\n    display: block;\n    width: 100%;\n    padding-right: 10px;\n    padding-bottom: 20px;\n  }\n}\n\n@media (max-width: 767px) {\n  .controller__thesaurus .banner--top {\n    text-align: center;\n  }\n  .controller__thesaurus .banner--top img {\n    display: inline-block;\n    width: 320px;\n    height: 50px;\n  }\n  .controller__thesaurus .entryPage + .fl-r .social__links {\n    display: none;\n  }\n  .controller__thesaurus .content {\n    padding: 20px 10px;\n  }\n}\n\n@media (min-width: 768px) {\n  .controller__thesaurus .content {\n    padding: 20px;\n  }\n}\n\n@media (min-width: 550px) and (max-width: 767px) {\n  .controller__thesaurus .sidebar {\n    padding: 20px;\n  }\n  .controller__thesaurus .main-content {\n    padding: 0;\n  }\n  .controller__thesaurus .banner--top {\n    height: auto;\n    margin-bottom: 10px;\n    padding-left: 0;\n  }\n  .controller__thesaurus .banner--left {\n    display: none;\n  }\n  .controller__thesaurus .content {\n    width: 100%;\n    margin-left: 0;\n  }\n  .controller__thesaurus .heading__item {\n    padding: 10px;\n  }\n  .controller__thesaurus .heading__item__content {\n    padding-right: 15px;\n    padding-left: 15px;\n  }\n}\n\n@media (min-width: 768px) and (max-width: 1023px) {\n  .controller__thesaurus .content {\n    width: -webkit-calc(100% - 175px);\n    width: calc(100% - 175px);\n    margin-right: 0;\n  }\n  .controller__thesaurus .banner--left {\n    margin-top: 0;\n  }\n  .controller__thesaurus .banner--top {\n    padding-left: 0;\n  }\n}\n\n@media (max-width: 960px) {\n  .controller__thesaurus .content {\n    margin-right: 0;\n  }\n}\n\n@media (max-width: 1264px) {\n  .controller__thesaurus .sidebar {\n    margin-top: 0;\n  }\n  .controller__thesaurus .banner-3 .in-b:first-of-type {\n    display: none;\n  }\n}\n\n@media (min-width: 1024px) {\n  .controller__thesaurus .content {\n    width: -webkit-calc(100% - 495px);\n    width: calc(100% - 495px);\n  }\n  .controller__thesaurus .heading__item__content h4 {\n    font-size: 1em;\n  }\n}\n\n@media (min-width: 550px) and (max-width: 1023px) {\n  .controller__thesaurus .heading__item,\n  .controller__thesaurus .heading__item--fixed {\n    min-width: 50%;\n  }\n  .controller__thesaurus .sidebar {\n    font-size: 0;\n    width: 100%;\n  }\n  .controller__thesaurus .sidebar__item {\n    font-size: 1rem;\n    display: inline-block;\n    width: -webkit-calc(50% - 10px);\n    width: calc(50% - 10px);\n    margin: 10px;\n    vertical-align: top;\n  }\n  .controller__thesaurus .sidebar__item:nth-child(even) {\n    margin-right: 0;\n  }\n  .controller__thesaurus .sidebar__item:nth-child(odd) {\n    margin-left: 0;\n  }\n  .controller__thesaurus .sidebar .sidebar__item:first-child {\n    margin-top: 10px;\n  }\n}\n\n@media (max-width: 1289px) {\n  .controller__thesaurus .hide1290 {\n    display: none !important;\n  }\n  .controller__thesaurus .banner-in-c .hide-desc img {\n    display: inline-block;\n    width: 300px;\n    height: auto;\n  }\n}\n\n@media (min-width: 1290px) {\n  .controller__thesaurus .hide-desc {\n    display: none !important;\n  }\n}\n\n@media all and (max-width: 767px) {\n  .homeSearch {\n    display: block;\n    background-color: #00b9fe;\n  }\n  .homeSearch .homeSearchContent {\n    width: 100%;\n  }\n}\n\n@media all and (max-width: 767px) {\n  .visibleMobile {\n    display: block;\n  }\n  .visibleDesktop {\n    display: none;\n  }\n  .panel {\n    min-height: 194px;\n  }\n}\n\n@media all and (min-width: 767px) {\n  .visibleMobile {\n    display: none;\n  }\n  .visibleDesktop {\n    display: block;\n  }\n}\n\n@media all and (min-width: 767px) {\n  .lex-category .browse-menu-outer ul.horizontal-list li a {\n    width: 55px;\n    height: 55px;\n    line-height: 1.2;\n  }\n  .lex-category .browse-menu-outer ul.horizontal-list li::before {\n    left: 55px !important;\n  }\n}\n\n@-webkit-keyframes load8 {\n  0% {\n    -webkit-transform: rotate(0deg);\n    transform: rotate(0deg);\n  }\n  100% {\n    -webkit-transform: rotate(360deg);\n    transform: rotate(360deg);\n  }\n}\n\n@keyframes load8 {\n  0% {\n    -webkit-transform: rotate(0deg);\n    transform: rotate(0deg);\n  }\n  100% {\n    -webkit-transform: rotate(360deg);\n    transform: rotate(360deg);\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  .news section a span.article-title {\n    margin-bottom: -6px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .quizzes > div section article span.further_reading_article_title {\n    font-size: 1em;\n  }\n}\n\n@media screen and (max-width: 767px) {\n}\n\n@media screen and (max-width: 1290px) {\n  #content.bi-lingual-enes-ad-code-top-mpu .lex-container .banbox .content-ban,\n  #content.us-english-entry-ad-code-top-mpu\n    .lex-container\n    .banbox\n    .content-ban {\n    padding: 0;\n    margin: auto;\n  }\n}\n\n@media screen and (min-width: 1024px) {\n  #content.thesaurus-ad-code-top-mpu .lex-container .banbox .content-ban {\n    padding: 0 315px 0 0px;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  #content.thesaurus-ad-code-top-mpu .lex-container .banbox .content-ban {\n    padding: 0;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  #content.us-english-entry-ad-code-speech-mpu .banbox .mpu .banner {\n    width: 320px;\n    height: 250px !important;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  #content.bi-lingual-esen-ad-code-default\n    .lex-container\n    .banbox\n    .content-ban\n    .banner {\n    margin: auto;\n  }\n}\n\n@media screen and (max-width: 1290px) {\n  #content.bi-lingual-esen-ad-code-top-mpu .lex-container .banbox .content-ban {\n    padding: 0px;\n  }\n}\n\n@media screen and (min-width: 1024px) {\n  .sidebar .banbox-mini .banner {\n    float: left;\n  }\n  .sidebar .banbox-mini + .signup-wrap {\n    margin-top: 0;\n    float: left;\n  }\n}\n\n@media screen and (max-width: 769px) {\n  .container .banbox .banner {\n    display: block;\n    margin: 0 auto;\n    width: 320px;\n    overflow: hidden;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .mediaContentSection p i {\n    float: left;\n    height: 30px;\n  }\n}\n\n@media screen and (min-width: 1023px) {\n  .banner.high_impact {\n    max-width: 970px;\n    max-height: 250px;\n  }\n  .banbox-mini .banner.high_impact {\n    max-width: 300px;\n    max-height: 600px;\n  }\n  .hub-hero {\n    width: calc(100% - 20px);\n    float: left;\n    min-height: 1px;\n    margin-bottom: 20px;\n    margin-left: 10px;\n    border: 1px solid #e0eef2;\n    background: #f8faff;\n    display: block;\n  }\n  .hub-hero .box-img {\n    width: 50%;\n    float: left;\n    min-height: 280px;\n  }\n  .hub-hero .left-side {\n    float: left;\n    height: 260px;\n    width: 50%;\n  }\n  .hub-hero .left-side div {\n    padding-left: 20px;\n    padding-bottom: 20px;\n  }\n  .left-side h1,\n  .left-side h2 {\n    color: #333333;\n    font-size: 1em;\n    line-height: 1.2;\n    font-family: 'Open Sans';\n    padding-right: 20px;\n    margin-bottom: 12px;\n  }\n  .left-side p {\n    color: #555555;\n    line-height: 1.2;\n    font-family: 'Open Sans', sans-serif;\n    padding-right: 20px;\n    font-size: 1em;\n  }\n  .hub-hero:after {\n    clear: both;\n  }\n  .hub-tile {\n    margin-left: 10px;\n    margin-right: 10px;\n    width: calc(50% - 20px);\n    float: left;\n    position: relative;\n    min-height: 1px;\n    padding-right: 15px;\n    padding-left: 15px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .anchor {\n    height: 148px;\n    margin-top: -148px;\n    position: absolute;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  #homePage .words > .banbox-mini {\n    height: 329px;\n    min-height: 0;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  #homePage .words > .banbox-mini {\n    height: 260px;\n    margin-bottom: 15px;\n  }\n}\n\n@media all and (min-width: 768px) {\n  .formPage .formContent {\n    max-width: 685px;\n    margin: 1em auto 0 auto;\n  }\n}\n\n@media all and (min-width: 550px) {\n  .formContent {\n    padding: 45px;\n  }\n}\n\n@media screen and (max-width: 1023px) {\n  .dictionary__es .sbOptions,\n  .dictionary__es .sbHolder {\n    width: 186px !important;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .dictionary__es .sbOptions,\n  .dictionary__es .sbHolder {\n    width: 100% !important;\n  }\n}\n\n@media (max-width: 767px) {\n  .controller__thesaurus .entryPage + .fl-r .social__links {\n    display: initial;\n  }\n}\n\n@media screen and (min-width: 767px) {\n  .error-page {\n    margin-bottom: 280px;\n  }\n}\n\n@media (min-width: 768px) {\n  #feedbackTab {\n    bottom: 140px;\n    right: -23px;\n  }\n  #feedbackTab:hover {\n    -webkit-transition: right 0.3s;\n    -ms-transition: right 0.3s;\n    transition: right 0.3s;\n    right: -17px;\n  }\n}\n\n@media (max-width: 767px) {\n  #feedbackTab {\n    padding: 5px 8px 13px 8px;\n    font-size: 1em;\n    line-height: 1.2;\n    bottom: 166px;\n    right: -20px;\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/lexico/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type LexicoConfig = DictItem<{\n  related: boolean\n}>\n\nexport default (): LexicoConfig => ({\n  lang: '10000000',\n  selectionLang: {\n    english: true,\n    chinese: false,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 265,\n  selectionWC: {\n    min: 1,\n    max: 5\n  },\n  options: {\n    related: true\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/lexico/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  HTMLString,\n  handleNoResult,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult,\n  getText,\n  getFullLink,\n  removeChild,\n  removeChildren,\n  getInnerHTML,\n  externalLink\n} from '../helpers'\nimport { getStaticSpeaker } from '@/components/Speaker'\n\nconst getSrc = (text: string) =>\n  `https://www.lexico.com/definition/${text.trim().replace(/\\s+/g, '_')}`\n\nexport const getSrcPage: GetSrcPageFunction = getSrc\n\nconst HOST = 'https://www.lexico.com'\n\nexport interface LexicoResultLex {\n  type: 'lex'\n  entry: HTMLString\n}\n\nexport interface LexicoResultRelated {\n  type: 'related'\n  list: ReadonlyArray<{\n    href: string\n    text: string\n  }>\n}\n\nexport type LexicoResult = LexicoResultLex | LexicoResultRelated\n\nexport const search: SearchFunction<LexicoResult> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  const { options } = profile.dicts.all.lexico\n\n  return fetchDirtyDOM(getSrc(text))\n    .catch(handleNetWorkError)\n    .then(doc => {\n      const $noResult = doc.querySelector('.no-exact-matches')\n      if ($noResult) {\n        if (options.related) {\n          const $similar = $noResult.querySelectorAll<HTMLAnchorElement>(\n            '.similar-results .search-results li a'\n          )\n          if ($similar.length > 0) {\n            const result: LexicoResultRelated = {\n              type: 'related',\n              list: [...$similar].map($a => ({\n                href: getFullLink(HOST, $a, 'href'),\n                text: getText($a)\n              }))\n            }\n            return { result }\n          }\n        }\n        return handleNoResult()\n      }\n      return handleDOM(doc)\n    })\n}\n\nfunction handleDOM(\n  doc: Document\n):\n  | Promise<DictSearchResult<LexicoResultLex>>\n  | DictSearchResult<LexicoResultLex> {\n  const $entry = doc.querySelector('.entryWrapper')\n\n  if ($entry) {\n    removeChild($entry, '.breadcrumbs')\n    removeChildren($entry, '.socials')\n    removeChildren($entry, '.homographs')\n    removeChildren($entry, '.associatedTranslation')\n\n    $entry.querySelectorAll('.entryHead header > h1').forEach($h1 => {\n      if ($h1.textContent?.trim().startsWith('Meaning of')) {\n        $h1.remove()\n      }\n    })\n\n    let mp3: string | undefined\n\n    $entry\n      .querySelectorAll<HTMLAnchorElement>('a[data-value=\"view synonyms\"]')\n      .forEach($a => externalLink($a))\n    ;[\n      ...$entry.querySelectorAll<HTMLAnchorElement>('.headwordAudio'),\n      ...$entry.querySelectorAll<HTMLAnchorElement>('.speaker')\n    ].forEach($speaker => {\n      const $audio = $speaker.querySelector<HTMLAudioElement>('audio')\n      const src = $audio && getFullLink(HOST, $audio, 'src')\n      $speaker.replaceWith(getStaticSpeaker(src))\n      if (src && !mp3) {\n        mp3 = src\n      }\n    })\n\n    return {\n      result: {\n        type: 'lex',\n        entry: getInnerHTML(HOST, $entry)\n      },\n      audio: mp3 ? { uk: mp3 } : undefined\n    }\n  }\n\n  return handleNoResult()\n}\n"
  },
  {
    "path": "src/components/dictionaries/liangan/View.tsx",
    "content": "import { DictGuoyu } from '../guoyu/View'\n\nexport const DictLiangAn = DictGuoyu\n\nDictLiangAn.displayName = 'LiangAn'\n\nexport default DictLiangAn\n"
  },
  {
    "path": "src/components/dictionaries/liangan/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"兩岸詞典\",\n    \"zh-CN\": \"两岸词典\",\n    \"zh-TW\": \"兩岸詞典\"\n  },\n  \"options\": {\n    \"trans\": {\n      \"en\": \"Show Translation\",\n      \"zh-CN\": \"显示翻译\",\n      \"zh-TW\": \"顯示翻譯\"\n    }\n  },\n  \"helps\": {\n    \"trans\": {\n      \"en\": \"Show translation of other languages.\",\n      \"zh-CN\": \"显示其它语言的翻译。\",\n      \"zh-TW\": \"顯示其它語言的翻譯。\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/liangan/_style.shadow.scss",
    "content": "@import '../guoyu/_style.shadow.scss';\n"
  },
  {
    "path": "src/components/dictionaries/liangan/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type LianganConfig = DictItem<{\n  trans: boolean\n}>\n\nexport default (): LianganConfig => ({\n  lang: '00100000',\n  selectionLang: {\n    english: false,\n    chinese: true,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 265,\n  selectionWC: {\n    min: 1,\n    max: 5\n  },\n  options: {\n    trans: false\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/liangan/engine.ts",
    "content": "import { SearchFunction, GetSrcPageFunction, getChsToChz } from '../helpers'\nimport { moedictSearch, GuoYuResult } from '../guoyu/engine'\n\nexport const getSrcPage: GetSrcPageFunction = async text => {\n  const chsToChz = await getChsToChz()\n  return `https://www.moedict.tw/~${chsToChz(text)}`\n}\n\nexport type LiangAnResult = GuoYuResult\n\nexport const search: SearchFunction<LiangAnResult> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  return moedictSearch<LiangAnResult>(\n    'c',\n    text,\n    config,\n    profile.dicts.all.liangan.options\n  ).then(result => {\n    if (result.result.h) {\n      result.result.h.forEach(h => {\n        if (h.p) {\n          h.p = h.p.replace('<br>陸⃝', ' [大陆]: ')\n        }\n      })\n    }\n    return result\n  })\n}\n"
  },
  {
    "path": "src/components/dictionaries/locales.ts",
    "content": "interface LocaleItem {\n  en: string\n  'zh-CN': string\n  'zh-TW': string\n}\n\ninterface LocaleObject {\n  [name: string]: LocaleItem\n}\n\nexport function getMachineLocales(\n  name: LocaleItem,\n  options: LocaleObject = {},\n  helps: LocaleObject = {}\n): {\n  name: LocaleItem\n  options: LocaleObject\n  helps: LocaleObject\n} {\n  return {\n    name,\n    options: {\n      keepLF: {\n        en: 'Keep linebreaks',\n        'zh-CN': '保留换行',\n        'zh-TW': '保留換行'\n      },\n      'keepLF-none': {\n        en: 'None',\n        'zh-CN': '不保留',\n        'zh-TW': '不保留'\n      },\n      'keepLF-all': {\n        en: 'All',\n        'zh-CN': '全保留',\n        'zh-TW': '全保留'\n      },\n      'keepLF-pdf': {\n        en: 'PDF',\n        'zh-CN': '保留 PDF 换行',\n        'zh-TW': '保留 PDF 換行'\n      },\n      'keepLF-webpage': {\n        en: 'Webpage',\n        'zh-CN': '保留网页换行',\n        'zh-TW': '保留網頁換行'\n      },\n      slInitial: {\n        en: 'Source Language',\n        'zh-CN': '原文显示',\n        'zh-TW': '原文顯示'\n      },\n      'slInitial-hide': {\n        en: 'Hide',\n        'zh-CN': '隐藏',\n        'zh-TW': '隱藏'\n      },\n      'slInitial-collapse': {\n        en: 'Collapse',\n        'zh-CN': '收起',\n        'zh-TW': '收起'\n      },\n      'slInitial-full': {\n        en: 'Full',\n        'zh-CN': '完整显示',\n        'zh-TW': '完整顯示'\n      },\n      tl: {\n        en: 'Target language',\n        'zh-CN': '目标语言',\n        'zh-TW': '目標語言'\n      },\n      tl2: {\n        en: 'Fallback target language',\n        'zh-CN': '第二目标语言',\n        'zh-TW': '第二目標語言'\n      },\n      ...options\n    },\n    helps: {\n      slInitial: {\n        en:\n          'Source language initial state. If hided can be reopened via dictionary titlebar menu.',\n        'zh-CN': '原文初始显示状态。隐藏后可通过字典标题栏菜单打开。',\n        'zh-TW': '原文初始顯示狀態。隱藏後可通過字典標題欄選單開啟。'\n      },\n      tl2: {\n        en:\n          'Fallback when detected languange and target language are identical.',\n        'zh-CN': '如果检测的源语言与目标语言相同将自动切换第二目标语言。',\n        'zh-TW': '如果檢測的源語言與目標語言相同將自動切換第二目標語言。'\n      },\n      ...helps\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/longman/View.tsx",
    "content": "import React, { FC } from 'react'\nimport Speaker from '@/components/Speaker'\nimport StarRates from '@/components/StarRates'\nimport {\n  LongmanResult,\n  LongmanResultLex,\n  LongmanResultRelated,\n  LongmanResultEntry\n} from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictLongman: FC<ViewPorps<LongmanResult>> = ({ result }) =>\n  result.type === 'lex'\n    ? renderLex(result)\n    : result.type === 'related'\n    ? renderRelated(result)\n    : null\n\nexport default DictLongman\n\nfunction renderEntry(entry: LongmanResultEntry) {\n  return (\n    <section\n      key={entry.title.HWD + entry.title.HOMNUM}\n      className=\"dictLongman-Entry\"\n    >\n      <header>\n        <div className=\"dictLongman-HeaderContainer\">\n          <h1 className=\"dictLongman-Title\">\n            <span className=\"dictLongman-Title_HWD\">{entry.title.HWD}</span>\n            <span className=\"dictLongman-Title_HYPHENATION\">\n              {entry.title.HYPHENATION}\n            </span>\n            <span className=\"dictLongman-Title_HOMNUM\">\n              {entry.title.HOMNUM}\n            </span>\n          </h1>\n          {entry.level ? (\n            <span title={entry.level.title} className=\"dictLongman-Level\">\n              <StarRates\n                max={3}\n                rate={entry.level.rate}\n                className=\"dictLongman-Level_Rate\"\n              />\n            </span>\n          ) : null}\n          {entry.freq &&\n            entry.freq.map(freq => (\n              <span\n                key={freq.rank}\n                className=\"dictLongman-FREQ\"\n                title={freq.title}\n              >\n                {freq.rank}\n              </span>\n            ))}\n        </div>\n        <div className=\"dictLongman-HeaderContainer\">\n          <span className=\"dictLongman-Pos\">{entry.pos}</span>\n          <span className=\"dictLongman-Phsym\">{entry.phsym}</span>\n          {entry.prons &&\n            entry.prons.map(pron => (\n              <React.Fragment key={pron.pron}>\n                {pron.lang.toUpperCase()}: <Speaker src={pron.pron} />\n              </React.Fragment>\n            ))}\n          {entry.topic && (\n            <>\n              Topic:{' '}\n              <a href={entry.topic.href} rel=\"nofollow noopener noreferrer\">\n                {entry.topic.title}\n              </a>\n            </>\n          )}\n        </div>\n      </header>\n\n      {entry.senses.map(sen => (\n        <StrElm key={sen} className=\"dictLongman-Sense\" html={sen} />\n      ))}\n\n      {entry.collocations && (\n        <StrElm className=\"dictLongman-Box\" html={entry.collocations} />\n      )}\n\n      {entry.grammar && (\n        <StrElm className=\"dictLongman-Box\" html={entry.grammar} />\n      )}\n\n      {entry.thesaurus && (\n        <StrElm className=\"dictLongman-Box\" html={entry.thesaurus} />\n      )}\n\n      {entry.examples && entry.examples.length > 0 && (\n        <>\n          <h2 className=\"dictLongman-Examples_Title\">\n            Examples from the Corpus\n          </h2>\n          {entry.examples.map(exa => (\n            <StrElm key={exa} className=\"dictLongman-Examples\" html={exa} />\n          ))}\n        </>\n      )}\n    </section>\n  )\n}\n\nfunction renderLex(result: LongmanResultLex) {\n  type Dicts = ['bussiness', 'contemporary'] | ['contemporary', 'bussiness']\n  // const dictTitle = {\n  //   contemporary: 'Longman Dictionary of Contemporary English',\n  //   bussiness: 'Longman Business Dictionary',\n  // }\n  const dicts: Dicts = result.bussinessFirst\n    ? ['bussiness', 'contemporary']\n    : ['contemporary', 'bussiness']\n\n  return (\n    <>\n      {result.wordfams && (\n        <StrElm className=\"dictLongman-Wordfams\" html={result.wordfams} />\n      )}\n\n      {dicts.map((dict, index) =>\n        result[dict].length > 0 ? (\n          <div className=\"dictLongman-Dict\" key={dict + index}>\n            {/* <h1 className='dictLongman-DictTitle'>\n              <span>- {dictTitle[dict]} -</span>\n            </h1> */}\n            {result[dict].map(renderEntry)}\n          </div>\n        ) : null\n      )}\n    </>\n  )\n}\n\nfunction renderRelated(result: LongmanResultRelated) {\n  return (\n    <>\n      <p>Did you mean:</p>\n      <StrElm tag=\"ul\" className=\"dictLongman-Related\" html={result.list} />\n    </>\n  )\n}\n"
  },
  {
    "path": "src/components/dictionaries/longman/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Longman Dictionary\",\n    \"zh-CN\": \"朗文词典\",\n    \"zh-TW\": \"朗文詞典\"\n  },\n  \"options\": {\n    \"wordfams\": {\n      \"en\": \"Show word family\",\n      \"zh-CN\": \"显示单词家族\",\n      \"zh-TW\": \"展示單字家族\"\n    },\n    \"collocations\": {\n      \"en\": \"Show collocations\",\n      \"zh-CN\": \"显示惯用搭配\",\n      \"zh-TW\": \"展示慣用搭配\"\n    },\n    \"grammar\": {\n      \"en\": \"Show grammar\",\n      \"zh-CN\": \"显示语法解释\",\n      \"zh-TW\": \"展示文法講解\"\n    },\n    \"thesaurus\": {\n      \"en\": \"Show thesaurus\",\n      \"zh-CN\": \"显示同义词\",\n      \"zh-TW\": \"展示同義詞\"\n    },\n    \"examples\": {\n      \"en\": \"Show examples\",\n      \"zh-CN\": \"显示例句\",\n      \"zh-TW\": \"展示例句\"\n    },\n    \"bussinessFirst\": {\n      \"en\": \"Show bussiness result first\",\n      \"zh-CN\": \"优先显示商务词典\",\n      \"zh-TW\": \"優先展示商務字典\"\n    },\n    \"related\": {\n      \"en\": \"Show related results\",\n      \"zh-CN\": \"失败时显示备选\",\n      \"zh-TW\": \"失敗時顯示備選\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/longman/_style.shadow.scss",
    "content": ".dictLongman-Wordfams {\n  .asset_intro {\n    display: block;\n    font-weight: bold;\n  }\n\n  .pos {\n    color: #16a085;\n    font-weight: bold;\n\n    &::before {\n      content: '';\n      display: block;\n      border-left: 1px solid red;\n    }\n\n    & + * {\n      margin-left: 0.5em;\n    }\n  }\n}\n\n.dictLongman-Dict {\n  margin-bottom: 1.2em;\n}\n\n.dictLongman-DictTitle {\n  font-size: 14px;\n  font-weight: normal;\n  text-align: center;\n\n  > span {\n    padding: 5px 10px;\n    color: #fff;\n    background: #b8b8b8;\n    border-radius: 4px;\n  }\n}\n\n.dictLongman-HeaderContainer {\n  display: flex;\n  align-items: center;\n  flex-wrap: wrap;\n}\n\n.dictLongman-Title,\n.dictLongman-Level {\n  font-size: 1em;\n  margin-right: 0.6em;\n}\n\n.dictLongman-Title_HYPHENATION {\n  font-size: 1.5em;\n}\n\n.dictLongman-Title_HWD {\n  display: none;\n}\n\n.dictLongman-Title_HOMNUM {\n  font-size: 0.5em;\n  vertical-align: super;\n}\n\n.dictLongman-FREQ {\n  margin-right: 0.6em;\n  padding: 1px 0.5em 0;\n  color: #f9690e;\n  line-height: 1.5;\n  vertical-align: middle;\n  border: 1px solid #f9690e;\n  border-radius: 3px;\n  user-select: none;\n}\n\n.dictLongman-Phsym {\n  margin: 0 0.5em;\n}\n\n.dictLongman-Sense {\n  position: relative;\n  padding-left: 1.4em;\n}\n\n.dictLongman-Entry {\n  font-family: arial, helvetica, sans-serif;\n\n  .SubEntry,\n  .EXAMPLE,\n  .Sense,\n  .newline,\n  .ColloExa,\n  .GramExa {\n    display: block;\n  }\n\n  .ACTIV,\n  .HWD,\n  .FIELD,\n  .FIELDXX {\n    display: none;\n  }\n\n  .HYPHENATION {\n    font-size: 160%;\n  }\n\n  .POS,\n  .SYN,\n  .sensenum,\n  .GRAM,\n  .EXPR,\n  .LEXUNIT,\n  .COLLOINEXA,\n  .HYPHENATION,\n  .PROPFORMPREP {\n    font-weight: bold;\n  }\n\n  .GEO {\n    font-weight: normal;\n  }\n\n  .COLLOINEXA,\n  .GEO {\n    font-style: italic;\n  }\n\n  .GRAM {\n    margin-right: 0.25em;\n    color: #16a085;\n  }\n\n  .GEO,\n  .GLOSS {\n    color: #999;\n  }\n\n  .sensenum {\n    position: absolute;\n    left: 0;\n    text-align: right;\n    width: 1em;\n  }\n\n  .Subsense {\n    display: block;\n    padding-left: 1.4em;\n    position: relative;\n  }\n\n  .EXAMPLE {\n    margin-bottom: 0.4em;\n    padding-left: 0.8em;\n    color: var(--color-font-grey);\n    position: relative;\n\n    &:before {\n      content: '•';\n      margin-right: 0.5em;\n      user-select: none;\n    }\n\n    &.withSpeaker:before {\n      display: none;\n    }\n  }\n\n  .SYN .synopp,\n  .SIGNPOST {\n    color: #fff;\n    font-size: 80%;\n    font-weight: bold;\n    background: #ff9552;\n    padding: 2px 0.4em 1px;\n    border-radius: 3px;\n    text-transform: uppercase;\n  }\n\n  .OPP {\n    font-weight: bold;\n\n    .synopp {\n      color: #fff;\n      border-color: #f1d600;\n      background-color: #f1d600;\n      padding: 0 0.4em;\n      border-radius: 3px;\n      text-transform: uppercase;\n    }\n  }\n\n  a:link,\n  a:visited,\n  a:hover,\n  a:active {\n    text-decoration: none;\n    color: #f9690e;\n  }\n\n  .SubEntry {\n    color: #999;\n    margin-left: 0.8em;\n\n    &:last-child {\n      margin-bottom: 0.4em;\n    }\n\n    a:link,\n    a:visited,\n    a:hover,\n    a:active {\n      text-decoration: none;\n      color: #999;\n    }\n  }\n\n  .ldoceEntry .synopp,\n  .ldoceEntry .FREQ,\n  .ldoceEntry .AC {\n    display: inline-block;\n    font-style: normal;\n    font-weight: bold;\n    text-transform: uppercase;\n    border-radius: 5px;\n    border: solid 1px;\n    padding-left: 4px;\n    padding-right: 4px;\n  }\n\n  .COLLO {\n    font-weight: bold;\n    margin-left: 10px;\n  }\n\n  .neutral {\n    color: #333;\n    font-style: normal;\n    font-weight: normal;\n    font-variant: normal;\n    background: none;\n  }\n}\n\n.dictLongman-Box {\n  position: relative;\n  margin: 1.4em 0.8em 0.6em 0.4em;\n  padding: 0.8em 0.5em 0.5em;\n  border: 1px solid #f9690e;\n  border-radius: 3px;\n\n  .heading {\n    position: absolute;\n    top: 0;\n    left: 0.8em;\n    transform: translateY(-50%);\n    padding: 0 0.4em;\n    font-size: 1.3em;\n    background: var(--color-background);\n  }\n\n  .CROSS,\n  .dont_say,\n  .BADEXA {\n    color: #f9690e;\n  }\n\n  .CROSS {\n    padding-left: 0.4em;\n\n    .neutral {\n      color: #f9690e;\n    }\n  }\n\n  .BADEXA {\n    padding-right: 0.4em;\n  }\n}\n\n.dictLongman-Examples_Title {\n  font-weight: normal;\n  font-size: 1.3em;\n}\n\n.dictLongman-Examples {\n  margin-bottom: 0.6em;\n\n  .title {\n    display: block;\n    font-weight: bold;\n    font-size: 1.1em;\n  }\n\n  .exa {\n    display: block;\n    position: relative;\n    margin-left: 0.8em;\n    padding-left: 0.8em;\n  }\n\n  .neutral {\n    position: absolute;\n    left: 0;\n  }\n}\n\n.dictLongman-Related {\n  a {\n    margin-left: 2em;\n    color: #16a085;\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/longman/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type LongmanConfig = DictItem<{\n  wordfams: boolean\n  collocations: boolean\n  grammar: boolean\n  thesaurus: boolean\n  examples: boolean\n  bussinessFirst: boolean\n  related: boolean\n}>\n\nexport default (): LongmanConfig => ({\n  lang: '10000000',\n  selectionLang: {\n    english: true,\n    chinese: false,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 265,\n  selectionWC: {\n    min: 1,\n    max: 5\n  },\n  options: {\n    wordfams: false,\n    collocations: true,\n    grammar: true,\n    thesaurus: true,\n    examples: true,\n    bussinessFirst: true,\n    related: true\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/longman/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  HTMLString,\n  getText,\n  getInnerHTML,\n  handleNoResult,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult,\n  getFullLink\n} from '../helpers'\nimport { DictConfigs } from '@/app-config'\nimport { getStaticSpeaker } from '@/components/Speaker'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return `https://www.ldoceonline.com/dictionary/${text\n    .trim()\n    .split(/\\s+/)\n    .join('-')}`\n}\n\nconst HOST = 'https://www.ldoceonline.com'\n\nexport interface LongmanResultEntry {\n  title: {\n    HWD: string\n    HYPHENATION: string\n    HOMNUM: string\n  }\n  senses: HTMLString[]\n  prons: Array<{\n    lang: string\n    pron: string\n  }>\n  topic?: {\n    title: string\n    href: string\n  }\n  phsym?: string\n  level?: {\n    rate: number\n    title: string\n  }\n  freq?: Array<{\n    title: string\n    rank: string\n  }>\n  pos?: string\n  collocations?: HTMLString\n  grammar?: HTMLString\n  thesaurus?: HTMLString\n  examples?: HTMLString[]\n}\n\nexport interface LongmanResultLex {\n  type: 'lex'\n  bussinessFirst: boolean\n  contemporary: LongmanResultEntry[]\n  bussiness: LongmanResultEntry[]\n  wordfams?: HTMLString\n}\n\nexport interface LongmanResultRelated {\n  type: 'related'\n  list: HTMLString\n}\n\nexport type LongmanResult = LongmanResultLex | LongmanResultRelated\n\ntype LongmanSearchResult = DictSearchResult<LongmanResult>\ntype LongmanSearchResultLex = DictSearchResult<LongmanResultLex>\ntype LongmanSearchResultRelated = DictSearchResult<LongmanResultRelated>\n\nexport const search: SearchFunction<LongmanResult> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  const options = profile.dicts.all.longman.options\n  return fetchDirtyDOM(\n    'http://www.ldoceonline.com/dictionary/' +\n      text.toLocaleLowerCase().replace(/[^A-Za-z0-9]+/g, '-')\n  )\n    .catch(handleNetWorkError)\n    .then(doc => handleDOM(doc, options))\n}\n\nfunction handleDOM(\n  doc: Document,\n  options: DictConfigs['longman']['options']\n): LongmanSearchResult | Promise<LongmanSearchResult> {\n  if (doc.querySelector('.dictentry')) {\n    return handleDOMLex(doc, options)\n  } else if (options.related) {\n    return handleDOMRelated(doc)\n  }\n  return handleNoResult()\n}\n\nfunction handleDOMLex(\n  doc: Document,\n  options: DictConfigs['longman']['options']\n): LongmanSearchResultLex | Promise<LongmanSearchResultLex> {\n  const result: LongmanResultLex = {\n    type: 'lex',\n    bussinessFirst: options.bussinessFirst,\n    contemporary: [],\n    bussiness: []\n  }\n\n  const audio: { uk?: string; us?: string } = {}\n\n  doc\n    .querySelectorAll<HTMLSpanElement>('.speaker.exafile')\n    .forEach($speaker => {\n      const mp3 = $speaker.dataset.srcMp3\n      if (mp3) {\n        const parent = $speaker.parentElement\n        $speaker.replaceWith(getStaticSpeaker(mp3))\n        if (parent && parent.classList.contains('EXAMPLE')) {\n          parent.classList.add('withSpeaker')\n        }\n      }\n    })\n\n  if (options.wordfams) {\n    result.wordfams = getInnerHTML(HOST, doc, '.wordfams')\n  }\n\n  const $dictentries = doc.querySelectorAll('.dictentry')\n  let currentDict: 'contemporary' | 'bussiness' | '' = ''\n  for (let i = 0; i < $dictentries.length; i++) {\n    const $entry = $dictentries[i]\n    const $intro = $entry.querySelector('.dictionary_intro')\n    if ($intro) {\n      const dict = $intro.textContent || ''\n      if (dict.includes('Contemporary')) {\n        currentDict = 'contemporary'\n      } else if (dict.includes('Business')) {\n        currentDict = 'bussiness'\n      } else {\n        currentDict = ''\n      }\n    }\n\n    if (!currentDict) {\n      continue\n    }\n\n    const entry: LongmanResultEntry = {\n      title: {\n        HWD: '',\n        HYPHENATION: '',\n        HOMNUM: ''\n      },\n      prons: [],\n      senses: []\n    }\n\n    const $topic = $entry.querySelector<HTMLAnchorElement>('a.topic')\n    if ($topic) {\n      entry.topic = {\n        title: $topic.textContent || '',\n        href: getFullLink(HOST, $topic, 'href')\n      }\n    }\n\n    const $head = $entry.querySelector('.Head')\n    if (!$head) {\n      continue\n    }\n\n    entry.title.HWD = getText($head, '.HWD')\n    entry.title.HYPHENATION = getText($head, '.HYPHENATION')\n    entry.title.HOMNUM = getText($head, '.HOMNUM')\n\n    entry.phsym = getText($head, '.PronCodes')\n\n    const $level = $head.querySelector('.LEVEL') as HTMLSpanElement\n    if ($level) {\n      const level = {\n        rate: 0,\n        title: ''\n      }\n\n      level.rate = (($level.textContent || '').match(/●/g) || []).length\n      level.title = $level.title\n\n      entry.level = level\n    }\n\n    entry.freq = Array.from(\n      $head.querySelectorAll<HTMLSpanElement>('.FREQ')\n    ).map($el => ({\n      title: $el.title,\n      rank: $el.textContent || ''\n    }))\n\n    entry.pos = getText($head, '.POS')\n\n    $head.querySelectorAll<HTMLSpanElement>('.speaker').forEach($pron => {\n      let lang = 'us'\n      const title = $pron.title\n      if (title.includes('British')) {\n        lang = 'uk'\n      }\n      const pron = $pron.getAttribute('data-src-mp3') || ''\n\n      audio[lang] = pron\n      entry.prons.push({ lang, pron })\n    })\n\n    entry.senses = Array.from($entry.querySelectorAll('.Sense')).map($sen =>\n      getInnerHTML(HOST, $sen)\n    )\n\n    if (options.collocations) {\n      entry.collocations = getInnerHTML(HOST, $entry, '.ColloBox')\n    }\n\n    if (options.grammar) {\n      entry.grammar = getInnerHTML(HOST, $entry, '.GramBox')\n    }\n\n    if (options.thesaurus) {\n      entry.thesaurus = getInnerHTML(HOST, $entry, '.ThesBox')\n    }\n\n    if (options.examples) {\n      entry.examples = Array.from(\n        $entry.querySelectorAll('.exaGroup')\n      ).map($exa => getInnerHTML(HOST, $exa))\n    }\n\n    result[currentDict].push(entry)\n  }\n\n  if (result.contemporary.length <= 0 && result.bussiness.length <= 0) {\n    return handleNoResult()\n  }\n\n  return { result, audio }\n}\n\nfunction handleDOMRelated(\n  doc: Document\n): LongmanSearchResultRelated | Promise<LongmanSearchResultRelated> {\n  const $didyoumean = doc.querySelector('.didyoumean')\n  if ($didyoumean) {\n    return {\n      result: {\n        type: 'related',\n        list: getInnerHTML(HOST, $didyoumean)\n      }\n    }\n  }\n  return handleNoResult()\n}\n"
  },
  {
    "path": "src/components/dictionaries/macmillan/View.tsx",
    "content": "import React, { FC, ReactNode } from 'react'\nimport Speaker from '@/components/Speaker'\nimport StarRates from '@/components/StarRates'\nimport {\n  MacmillanResult,\n  MacmillanResultLex,\n  MacmillanResultRelated\n} from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictMacmillan: FC<ViewPorps<MacmillanResult>> = ({\n  result,\n  searchText\n}) => {\n  switch (result.type) {\n    case 'lex':\n      return renderLex(result, renderSelect(result, searchText))\n    case 'related':\n      return renderRelated(result)\n    default:\n      return null\n  }\n}\n\nexport default DictMacmillan\n\nfunction renderSelect(\n  result: MacmillanResultLex,\n  searchText: ViewPorps<MacmillanResultLex>['searchText']\n) {\n  return result.relatedEntries.length > 0 ? (\n    <select\n      value={''}\n      onChange={e => {\n        if (e.currentTarget.value) {\n          searchText({\n            id: 'macmillan',\n            payload: { href: e.currentTarget.value }\n          })\n        }\n      }}\n    >\n      <option value=\"\">\n        {result.title + (result.pos ? ` ${result.pos.toUpperCase()}` : '')}\n      </option>\n      {result.relatedEntries.map(({ title, href }) => (\n        <option key={href} value={href}>\n          {title}\n        </option>\n      ))}\n    </select>\n  ) : null\n}\n\nfunction renderLex(result: MacmillanResultLex, select: ReactNode) {\n  return (\n    <section onClick={onEntryClick}>\n      {select}\n      <header className=\"dictMacmillan-Header\">\n        {result.ratting! > 0 && <StarRates rate={result.ratting} />}\n        <span className=\"dictMacmillan-Header_Info\">\n          {result.phsym} <Speaker src={result.pron} /> {result.pos} {result.sc}\n        </span>\n      </header>\n      <StrElm tag=\"ol\" className=\"dictMacmillan-Sences\" html={result.senses} />\n      {result.toggleables.map((toggleable, i) => (\n        <StrElm key={i} html={toggleable} />\n      ))}\n    </section>\n  )\n}\n\nfunction renderRelated(result: MacmillanResultRelated) {\n  return (\n    <>\n      <p>Did you mean:</p>\n      <ul className=\"dictMacmillan-Related\">\n        {result.list.map(({ title, href }) => (\n          <li key={href}>\n            <a href={href}>{title}</a>\n          </li>\n        ))}\n      </ul>\n    </>\n  )\n}\n\nfunction onEntryClick(event: React.MouseEvent<HTMLElement>) {\n  if (!event.target['classList']) {\n    return\n  }\n  const target = event.target as Element\n\n  let isToggleHead =\n    target.classList.contains('toggle-open') ||\n    target.classList.contains('toggle-close')\n\n  if (!isToggleHead) {\n    for (let el: Element | null = target; el; el = el.parentElement) {\n      if (el.classList.contains('toggle-toggle')) {\n        isToggleHead = true\n        break\n      }\n    }\n  }\n\n  if (!isToggleHead) {\n    return\n  }\n\n  for (let el: Element | null = target; el; el = el.parentElement) {\n    if (el.classList.contains('toggleable')) {\n      el.classList.toggle('closed')\n      event.preventDefault()\n      event.stopPropagation()\n      break\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/macmillan/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Macmillan\",\n    \"zh-CN\": \"麦克米伦\",\n    \"zh-TW\": \"麥克米倫\"\n  },\n  \"options\": {\n    \"related\": {\n      \"en\": \"Show related results\",\n      \"zh-CN\": \"失败时显示备选\",\n      \"zh-TW\": \"失敗時顯示備選\"\n    },\n    \"locale\": {\n      \"en\": \"Locale\",\n      \"zh-CN\": \"方言\",\n      \"zh-TW\": \"方言\"\n    },\n    \"locale-uk\": {\n      \"en\": \"UK\",\n      \"zh-CN\": \"英式\",\n      \"zh-TW\": \"英式\"\n    },\n    \"locale-us\": {\n      \"en\": \"US\",\n      \"zh-CN\": \"美式\",\n      \"zh-TW\": \"美式\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/macmillan/_style.shadow.scss",
    "content": ".dictMacmillan-Header {\n  display: flex;\n  flex-wrap: wrap;\n  align-items: center;\n  line-height: 1;\n  margin-bottom: 5px;\n}\n\n.dictMacmillan-Header_Info {\n  margin-left: 10px;\n  color: var(--color-font-grey);\n}\n\n.dictMacmillan-Title {\n  font-size: 1.5em;\n  font-weight: bold;\n}\n\n.dictMacmillan-Sences {\n  list-style-type: none;\n  padding-left: 0;\n  margin: 0;\n\n  ol {\n    list-style-type: none;\n    padding-left: 0;\n  }\n\n  > li {\n    margin-bottom: 15px;\n  }\n\n  p {\n    margin: 0;\n  }\n\n  a:link,\n  a:visited {\n    color: currentColor;\n    text-decoration: none;\n  }\n\n  a:hover,\n  a:active {\n    color: #16a085;\n    border-bottom: thin dotted #16a085;\n  }\n}\n\n.dictMacmillan-Related {\n  a {\n    margin-left: 2em;\n    color: #16a085;\n  }\n}\n\n.flex-extend {\n  flex-grow: 2;\n}\n\n.dflex {\n  display: flex;\n  flex: 1 1 auto;\n  flex-wrap: nowrap;\n}\n\n.between-xs {\n  justify-content: space-between;\n}\n\n.middle-xs {\n  align-items: center;\n}\n\n.toggleable {\n  margin-bottom: 5px;\n  padding: 1em 1.5em;\n  background: rgba(191, 191, 191, 0.08);\n\n  &:not(.closed) .visible-closed,\n  &.closed .hidden-closed {\n    display: none;\n  }\n}\n\n.toggle-toggle, .toggle-open, .toggle-close {\n  cursor: pointer;\n}\n\n.toggle-open, .toggle-close {\n  font-family: Martel, serif;\n  line-height: 1em;\n  font-size: 1.2em;\n  font-weight: bold;\n  color: currentColor;\n}\n\n.thes-color {\n  float: right;\n}\n\n.SENSE-VARIANT,\nh3.SENSE-ENTRY,\nh2.MULTIWORD,\nh2.PHRASE-VARIANT {\n  display: inline;\n}\n\n.foldimage {\n  display: none;\n}\n\n.gray-divider.mini {\n  width: 5em;\n  margin-top: 0;\n  margin-left: 0;\n  margin-right: 100%;\n}\n\n.EXAMPLES {\n  margin-bottom: 5px;\n  padding-left: 10px;\n  color: var(--color-font-grey);\n  border-left: var(--color-divider) solid 1px;\n  font-style: italic;\n\n  strong {\n    font-style: normal;\n  }\n\n  a:link,\n  a:visited {\n    color: var(--color-font-grey);\n    text-decoration: none;\n  }\n\n  a:hover,\n  a:active {\n    color: #16a085;\n    border-bottom: thin dotted #16a085;\n  }\n}\n\n.EXAMPLES + .EXAMPLES {\n  margin-top: -5px;\n  padding-top: 3px;\n}\n\n.DEFINITION + .EXAMPLES {\n  margin-top: 5px;\n}\n\n.THES {\n  margin-bottom: 5px;\n}\n\n.openSense .DEFINITION,\n.openDef {\n  font-weight: bold;\n  word-break: break-word;\n}\n\n.SENSE-NUM {\n  font-weight: bold;\n  height: 1.2em;\n  padding: .2em;\n  margin-right: .2em;\n  display: inline-block;\n  line-height: 1.2em;\n  text-align: center;\n\n  &::after {\n    content: '.';\n  }\n}\n\n.PATTERNS-COLLOCATIONS, .VOCAB-XREF, .BOLD, .KEY-REF {\n  font-weight: bold;\n}\n\n.icon_thesaurus_small_bullet {\n  font-weight: bold;\n  color: #f9690e;\n}\n\n.thessnippet {\n  margin-left: 10px;\n}\n\n.entry-labels *,\n.DIALECT,\n.RESTRICTION-CLASS,\n.STYLE-LEVEL,\n.SUBJECT-AREA,\n.SYNTAX-CODING {\n  margin-right: .4em;\n  text-transform: uppercase;\n  font-size: .8em;\n  color: var(--color-font-grey);\n}\n\n.h2 {\n  margin-right: 3px;\n  font-weight: bold;\n}\n\n.centred {\n  &::before {\n    content: '>';\n    color: var(--color-font-grey);\n  }\n}\n\n.moreButton {\n  &:link,\n  &:visited {\n    color: var(--color-font-grey);\n  }\n}\n\n.ONEBOX-HEAD {\n  font-weight: bold;\n  color: #f9690e;\n}\n\n.sideboxbody {\n  margin-left: 10px;\n}\n\n.SUB-SENSES {\n  padding-left: 16px;\n}\n\n.sound,\n.audio_play_button {\n  width: 16px;\n  vertical-align: text-bottom;\n  cursor: pointer;\n}\n\n.openEntry {\n  margin: 1em;\n  padding: 0 1em;\n  border: 1px #ababab solid;\n}\n\n.entry-od-sense {\n  display: flex;\n  flex: 1 0 auto;\n\n  .openEntry {\n    margin: 0;\n    margin-left: .2em;\n  }\n}\n\n.openDETAIL {\n  font-style: italic;\n  text-align: right;\n  text-align: end;\n  display: block;\n  font-size: .9em;\n  margin: 1.5em -1rem 1em;\n  padding-right: 1em;\n  color: var(--color-font-grey);\n}\n\n.open-footer-content {\n  font-size: .7em;\n  font-weight: bold;\n  text-transform: uppercase;\n  text-align: center;\n}\n\n.open-footer-logo {\n  display: inline-block;\n  height: .8em;\n  vertical-align: middle;\n  margin-bottom: .2em;\n  margin-right: .3em;\n}\n\n.entry-bold {\n  text-transform: uppercase;\n  font-weight: bold;\n  font-size: .9em;\n}\n\n.line-box-content {\n  ul {\n    padding-left: 2em;\n  }\n\n  li {\n    list-style-type: disc;\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/macmillan/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type MacmillanConfig = DictItem<{\n  related: boolean\n  locale: 'uk' | 'us'\n}>\n\nexport default (): MacmillanConfig => ({\n  lang: '10000000',\n  selectionLang: {\n    english: true,\n    chinese: false,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 465,\n  selectionWC: {\n    min: 1,\n    max: 5\n  },\n  options: {\n    related: true,\n    locale: 'uk'\n  },\n  options_sel: {\n    locale: ['uk', 'us']\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/macmillan/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  HTMLString,\n  getInnerHTML,\n  handleNoResult,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  externalLink,\n  DictSearchResult,\n  removeChildren,\n  getText,\n  removeChild,\n  getFullLink,\n  getOuterHTML\n} from '../helpers'\nimport { DictConfigs } from '@/app-config'\n\nexport const getSrcPage: GetSrcPageFunction = (text, config, profile) => {\n  const lang =\n    profile.dicts.all.macmillan.options.locale === 'us' ? 'american' : 'british'\n  return (\n    `http://www.macmillandictionary.com/dictionary/${lang}/` +\n    encodeURIComponent(text.toLocaleLowerCase().replace(/[^A-Za-z0-9]+/g, '-'))\n  )\n}\n\nconst HOST = 'http://www.macmillandictionary.com'\n\nexport interface MacmillanResultLex {\n  type: 'lex'\n  title: string\n  senses: HTMLString\n  /** part of speech */\n  pos?: string\n  /** syntax coding */\n  sc?: string\n  phsym?: string\n  pron?: string\n  ratting?: number\n  toggleables: HTMLString[]\n  relatedEntries: Array<{\n    title: string\n    href: string\n  }>\n}\n\nexport interface MacmillanResultRelated {\n  type: 'related'\n  list: Array<{\n    title: string\n    href: string\n  }>\n}\n\nexport type MacmillanResult = MacmillanResultLex | MacmillanResultRelated\n\ntype MacmillanSearchResult = DictSearchResult<MacmillanResult>\n\nexport interface MacmillanPayload {\n  href?: string\n}\n\nexport const search: SearchFunction<MacmillanResult, MacmillanPayload> = async (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  const options = profile.dicts.all.macmillan.options\n\n  return fetchMacmillanDom(\n    payload.href || (await getSrcPage(text, config, profile))\n  )\n    .catch(handleNetWorkError)\n    .then(doc => checkResult(doc, options))\n}\n\nasync function checkResult(\n  doc: Document,\n  options: DictConfigs['macmillan']['options']\n): Promise<MacmillanSearchResult> {\n  if (doc.querySelector('.senses')) {\n    return handleDOM(doc)\n  } else if (options.related) {\n    const alternatives = [\n      ...doc.querySelectorAll<HTMLAnchorElement>('.display-list li a')\n    ].map($a => ({\n      title: getText($a),\n      href: getFullLink(HOST, $a, 'href')\n    }))\n    if (alternatives.length > 0) {\n      return {\n        result: {\n          type: 'related',\n          list: alternatives\n        }\n      }\n    }\n  }\n  return handleNoResult()\n}\n\nfunction handleDOM(\n  doc: Document\n): MacmillanSearchResult | Promise<MacmillanSearchResult> {\n  const $entry = doc.querySelector('#entryContent .left-content')\n  if (!$entry) {\n    return handleNoResult()\n  }\n\n  const result: MacmillanResultLex = {\n    type: 'lex',\n    title: getText($entry, '.big-title .BASE'),\n    senses: '',\n    toggleables: [],\n    relatedEntries: []\n  }\n\n  if (!result.title) {\n    return handleNoResult()\n  }\n\n  $entry\n    .querySelectorAll<HTMLAnchorElement>('a.moreButton')\n    .forEach(externalLink)\n\n  result.senses = getInnerHTML(HOST, $entry, '.senses')\n\n  if (!result.senses) {\n    return handleNoResult()\n  }\n\n  removeChild($entry, '.senses')\n\n  result.pos = getText($entry, '.entry-pron-head .PART-OF-SPEECH')\n  result.sc = getText($entry, '.entry-pron-head .SYNTAX-CODING')\n  result.phsym = getText($entry, '.entry-pron-head .PRON')\n  result.ratting = $entry.querySelectorAll('.entry-red-star').length\n\n  $entry.querySelectorAll('.toggleable').forEach($toggleable => {\n    result.toggleables.push(getOuterHTML(HOST, $toggleable))\n  })\n\n  doc\n    .querySelectorAll<HTMLAnchorElement>('.related-entries-item a')\n    .forEach($a => {\n      const $pos = $a.querySelector('.PART-OF-SPEECH')\n      if ($pos) {\n        $pos.textContent = getText($pos).toUpperCase()\n      }\n\n      result.relatedEntries.push({\n        title: getText($a),\n        href: getFullLink(HOST, $a, 'href')\n      })\n    })\n\n  const audio: { uk?: string } = {}\n\n  const $sound = $entry.querySelector<HTMLDivElement>(\n    '.entry-pron-head .PRONS .sound'\n  )\n  if ($sound && $sound.dataset.srcMp3) {\n    result.pron = $sound.dataset.srcMp3\n    audio.uk = result.pron\n  }\n\n  return { result, audio }\n}\n\nasync function fetchMacmillanDom(url: string): Promise<Document> {\n  const doc = await fetchDirtyDOM(url)\n  removeChildren(doc, '.visible-xs')\n  return doc\n}\n"
  },
  {
    "path": "src/components/dictionaries/merriamwebster/View.tsx",
    "content": "import React, { FC } from 'react'\nimport Speaker from '@/components/Speaker'\nimport { MerriamWebsterResultV2 } from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\n\nexport const DictMerriamWebster: FC<ViewPorps<MerriamWebsterResultV2>> = ({\n  result\n}) => (\n  // <ul className=\"mw-list\">\n  <ul>\n    {result.groups.map((g, i) => (\n      <li key={`${`mw-g`}-${i}`} className=\"mw-item\">\n        <div className=\"mw-top-container\">\n          <div className=\"mw-title-area\">\n            {/* <sup className=\"mw-Sup\">{i + 1}</sup> */}\n            <span className=\"mw-title\">{g.title}</span>\n            <span className=\"mw-pos\">({g.pos})</span>\n          </div>\n          <div className=\"mw-prs\">\n            {g.pr?.syllable && (\n              <span className=\"mw-syllable\">{g.pr?.syllable}</span>\n            )}\n            {g.pr?.phonetics &&\n              g.pr?.phonetics.map((v, j) => (\n                <div\n                  key={'mw-pt-' + j}\n                  className={v.audio ? 'mw-pt' : 'mw-pt-text'}\n                >\n                  {v.symbol}\n                  {v.audio && <Speaker src={v.audio} />}\n                </div>\n              ))}\n          </div>\n        </div>\n\n        {g.sections.map((s, n) => (\n          <div key={'mw-section-' + n} className=\"mw-section\">\n            {s.title && <div className=\"mw-section-title\">{s.title}</div>}\n            <div>\n              {s.meaningGroups.map((means, o) => (\n                <div key={'mw-mg-' + o} className=\"mw-mg-area\">\n                  <div className=\"mw-mg-left\">\n                    <div className=\"mw-mg-sign\"> {o + 1}</div>\n                    <div className=\"mw-mg-line\"></div>\n                  </div>\n\n                  <div className=\"mw-mg-right\">\n                    {means.map((mean, k) => (\n                      <div key={'mw-meaning-' + k} className=\"mw-mean-area\">\n                        {(mean.examples || mean.explaining) &&\n                          means.length > 1 && (\n                            <span className=\"mw-mean-sign\">\n                              {String.fromCharCode(k + 97)}\n                            </span>\n                          )}\n\n                        {mean.explaining && (\n                          <div className=\"mw-mean-text\">{mean.explaining}</div>\n                        )}\n\n                        {mean.examples && (\n                          <div className=\"mw-mean-ex-area\">\n                            {mean.examples?.map((ex, m) => (\n                              <div\n                                key={'mw-example-' + m}\n                                className=\"mw-mean-ex-item\"\n                              >\n                                {ex}\n                              </div>\n                            ))}\n                          </div>\n                        )}\n                      </div>\n                    ))}\n                  </div>\n                </div>\n              ))}\n            </div>\n          </div>\n        ))}\n      </li>\n    ))}\n\n    {result.etymology && (\n      <li>\n        <div className=\"mw-extra mw-title-area\">\n          <div className=\"mw-extra-title mw-title\">Etymology</div>\n          {result.etymology?.map((v, i) => (\n            <div key={'mw-etymolog' + i}>\n              {v[0] && <div className=\"mw-section-title\">{v[0]}</div>}\n              <div>{v[1]}</div>\n            </div>\n          ))}\n        </div>\n      </li>\n    )}\n\n    {result.synonyms && (\n      <li>\n        <div className=\"mw-extra mw-title-area\">\n          <div className=\"mw-extra-title mw-title\">Synonyms</div>\n          {result.synonyms?.map((v, i) => (\n            <div key={'mw-etymolog' + i}>\n              {v[0] && <div className=\"mw-section-title\">{v[0]}</div>}\n              <div>{v[1].join('; ')}</div>\n            </div>\n          ))}\n        </div>\n      </li>\n    )}\n  </ul>\n)\n\nexport default DictMerriamWebster\n"
  },
  {
    "path": "src/components/dictionaries/merriamwebster/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Merriam-Webster's Dictionary\",\n    \"zh-CN\": \"韦氏词典\",\n    \"zh-TW\": \"韋氏字典\"\n  },\n  \"options\": {\n    \"resultnum\": {\n      \"en\": \"Show\",\n      \"zh-CN\": \"结果数量\",\n      \"zh-TW\": \"結果數量\"\n    },\n    \"resultnum_unit\": {\n      \"en\": \"results\",\n      \"zh-CN\": \"个\",\n      \"zh-TW\": \"個\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/merriamwebster/_style.shadow.scss",
    "content": ".mw-title-area {\n  margin-bottom: 2px;\n}\n\n.mw-title {\n  font-size: 1.5em;\n  font-weight: 700;\n  line-height: 1.6;\n}\n\n.mw-pos {\n  font-style: italic;\n  margin-left: 4px;\n  margin-right: 3px;\n}\n\n.mw-prs {\n  margin-bottom: 3px;\n  border-width: thin;\n}\n\n.mw-syllable {\n  font-weight: bold;\n  margin-right: 2px;\n}\n\n.mw-pt-text {\n  display: inline-block;\n  margin: 2px;\n}\n\n.mw-pt {\n  color: rgb(15, 56, 80);\n  border: 1px solid rgb(15, 56, 80);\n  border-radius: 5px;\n  margin: 2px;\n  padding-left: 10px;\n  display: inline-block;\n\n  @include isDarkMode {\n    color: #e7f0f4;\n    border: 1px solid #e7f0f4;\n  }\n}\n\n.mw-seciton {\n  margin-bottom: 10px;\n}\n\n.mw-section-title {\n  color: #8f8f8f;\n  border-bottom: 1px solid;\n  margin-bottom: 2px;\n}\n\n\n.mw-mg-area {\n  display: flex;\n  flex-direction: row;\n}\n\n.mw-mg-left {\n  display: flex;\n  flex-direction: column;\n  width: auto;\n  margin-right: 5px;\n  align-items: center;\n}\n\n.mw-mg-sign {\n  font-size: larger;\n  font-weight: bold;\n}\n\n.mw-mg-line {\n  flex: auto;\n  background: linear-gradient(180deg, #bbd5e0, #deeaef 17.8%, #deeaef 81.87%, #bbd5e0);\n  border-radius: 2px;\n  width: 1px;\n}\n\n.mw-mg-right {\n  margin-top: 3px;\n}\n\n.mw-mean-area {\n  display: block;\n}\n\n.mw-mean-text {\n  display: inline-block;\n}\n\n.mw-mean-sign {\n  font-weight: bold;\n  display: inline-block;\n  margin-right: 1px;\n}\n\n.mw-mean-ex-area {\n  padding-left: 1px;\n}\n\n.mw-mean-ex-item {\n  color: #777;\n  border-left: 1.5px solid #777;\n  padding-left: 5px;\n  line-height: 1em;\n  margin-top: 5px;\n  margin-bottom: 5px;\n}\n\n.mw-extra {\n  margin-top: 10px;\n}\n\n.mw-extra-title {\n  padding-left: 5px;\n  padding-top: 1px;\n  padding-bottom: 1px;\n  background-color: #777;\n  border-left: 10px solid #575757;\n}"
  },
  {
    "path": "src/components/dictionaries/merriamwebster/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type MerriamWebsterConfig = DictItem<{\n  resultnum: number\n}>\n\nexport default (): MerriamWebsterConfig => ({\n  lang: '10000000',\n  selectionLang: {\n    english: true,\n    chinese: false,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 180,\n  selectionWC: {\n    min: 1,\n    max: 5\n  },\n  options: {\n    resultnum: 4\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/merriamwebster/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction\n} from '../helpers'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return `https://www.merriam-webster.com/dictionary/${text}`\n}\n\nexport interface Meaning {\n  explaining?: string\n  examples?: string[]\n}\n\nexport type MeaningGroup = Meaning[]\n\nexport interface Section {\n  // could be transitive or intranstive if pos was verb\n  title?: string\n  meaningGroups: MeaningGroup[]\n}\n\nexport interface Phonetic {\n  symbol?: string\n  audio?: string\n}\n\nexport interface Pronunciation {\n  syllable?: string\n  phonetics: Phonetic[]\n}\n\nexport interface Group {\n  title?: string\n  pos?: string\n  pr?: Pronunciation\n  conjugation?: string\n  sections: Section[]\n  forms?: string[]\n}\n\nexport interface MerriamWebsterResultV2 {\n  groups: Group[]\n  synonyms?: Array<[string, string[]]>\n  etymology?: Array<[string, string]>\n}\n\nexport const search: SearchFunction<MerriamWebsterResultV2> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  // const options = profile.dicts.all.merriamwebster.options\n\n  return fetchDirtyDOM(\n    'https://www.merriam-webster.com/dictionary/' +\n      encodeURIComponent(text.replace(/\\s+/g, ' '))\n  )\n    .catch(handleNetWorkError)\n    .then(doc => {\n      return { result: getResult(doc) }\n      // return handleDOM(doc, options)\n    })\n}\n\nexport function _getContentEle(doc: Document): Element {\n  const content = doc.querySelector('#left-content') as HTMLElement\n\n  if (!content || !content.querySelector('div[id^=dictionary-entry]')) {\n    throw new Error('NO_RESULT')\n  }\n  return content\n}\n\nexport function _getGroupsEles(content: Element): Element[] {\n  return new Array(\n    ...content\n      .querySelectorAll(\n        'div.entry-word-section-container[id^=dictionary-entry]'\n      )\n      .values()\n  )\n}\n\nexport function _getSynonyms(\n  content: Element\n): Array<[string, string[]]> | undefined {\n  const synonymsEle = content\n    .querySelector('#synonyms')\n    ?.querySelector('.content-section-body')\n\n  if (!synonymsEle) return undefined\n\n  const functions = [\n    ...synonymsEle?.querySelectorAll('.function-label').values()\n  ]\n  const lists = [\n    ...synonymsEle?.querySelectorAll('ul.synonyms-antonyms-grid-list')?.values()\n  ]\n\n  if (!lists) return undefined\n\n  const words = [...lists].map(l =>\n    [...l.querySelectorAll('a[lang]').values()].map(v => v.textContent)\n  )\n\n  if (functions.length === 0 || words.length === 0) return undefined\n\n  return functions.map((v, i) => [v.textContent, [...words[i]]]) as any\n}\n\nexport function _getEtymology(\n  content: Element\n): Array<[string, string]> | undefined {\n  const eles = content\n    .querySelector('#word-history')\n    ?.querySelector('.etymology-content-section')\n\n  if (!eles) return undefined\n\n  const functions = [...eles.querySelectorAll('p.function-label').values()].map(\n    v => v.textContent\n  )\n  const paragraphs = [...eles.querySelectorAll('p.et')].map(v =>\n    v.textContent?.trim()\n  )\n  if (paragraphs.length === 0) return undefined\n\n  return (functions.length > 0\n    ? functions.map((v, i) => [v, paragraphs[i]])\n    : paragraphs.map(v => ['', v])) as any\n}\n\nexport function _getTitle(group: Element): string | undefined {\n  return (\n    group.querySelector('div.entry-header-content')?.querySelector('.hword')\n      ?.textContent || undefined\n  )\n}\n\nexport function _getPartOfSpeech(group: Element): string | undefined {\n  return (\n    group\n      .querySelector('h2.parts-of-speech')\n      ?.querySelector('a.important-blue-link')?.textContent || undefined\n  )\n}\n\nexport function _getPrEle(group: Element): Element | undefined {\n  return (\n    group.querySelector('div.word-syllables-prons-header-content') || undefined\n  )\n}\n\nexport function _getSyllable(pr: Element): string | undefined {\n  return pr.querySelector('span.word-syllables-entry')?.textContent || undefined\n}\n\nexport function _getPhoneticEles(pr: Element): Element[] | undefined {\n  const pEles = pr\n    .querySelector('span.prons-entries-list-inline')\n    ?.querySelectorAll('.prons-entry-list-item')\n    ?.values()\n\n  return pEles ? [...pEles] : undefined\n}\n\nexport function _getPhoneticSymbol(pt: Element): string | undefined {\n  if (pt.tagName === 'a') {\n    return pt.textContent?.trim() || undefined\n  }\n  return pt.childNodes.item(0).textContent?.trim() || undefined\n}\n\nexport function _getPhoneticAudio(pt: Element): string | undefined {\n  const dir = pt.getAttribute('data-dir')\n  const fileName = pt.getAttribute('data-file')\n  const lang = pt.getAttribute('data-lang')\n  return dir && fileName && lang\n    ? `https://media.merriam-webster.com/audio/prons/${lang.replace(\n        '_',\n        '/'\n      )}/mp3/${dir}/${fileName}.mp3`\n    : undefined\n}\n\nexport function _getConjugation(group: Element): string | undefined {\n  const conjEles = group\n    .querySelector('.row.headword-row.header-ins')\n    ?.querySelectorAll('span.vg-ins')\n    ?.values()\n\n  return conjEles\n    ? new Array(...conjEles)\n        .map(e => e.textContent)\n        .join()\n        .trim()\n    : undefined\n}\n\nexport function _getSectionsEles(group: Element): Element[] | undefined {\n  const sEles = group.querySelectorAll('div.vg').values()\n  return sEles ? [...sEles] : undefined\n}\n\nexport function _getForms(group: Element): string[] {\n  return ['']\n}\n\nexport function _getSectionTitle(section: Element): string | undefined {\n  return (\n    section.querySelector('p.vd')?.querySelector('a.important-blue-link')\n      ?.textContent || undefined\n  )\n}\n\nexport function _getMeaningGroupEles(section: Element): Element[] | undefined {\n  const mgEles = section.querySelectorAll('div.vg-sseq-entry-item').values()\n  return mgEles ? [...mgEles] : undefined\n}\n\nexport function _getMeaningEles(mg: Element): Element[] | undefined {\n  const meanEles = mg.querySelectorAll('div.sb-entry').values()\n  return meanEles ? [...meanEles] : undefined\n}\n\nexport function _getExpaining(meaning: Element): string | undefined {\n  return (\n    meaning.querySelector('span.dtText')?.textContent?.trim() ||\n    meaning.querySelector('span.unText')?.textContent?.trim()\n  )\n}\n\nexport function _getExamples(meaning: Element): string[] | undefined {\n  const result = [] as string[]\n  const eles = meaning.querySelectorAll('span.ex-sent.sents').values()\n  if (!eles) return undefined\n  for (const e of eles) {\n    if (e.textContent) result.push(e.textContent)\n  }\n  return result.length > 0 ? result : undefined\n}\n\nexport function getResult(dom: Document): MerriamWebsterResultV2 {\n  const eleContent = _getContentEle(dom)\n\n  const groups: Group[] = []\n  const synonyms = _getSynonyms(eleContent)\n  const etymology = _getEtymology(eleContent)\n\n  for (const g of _getGroupsEles(eleContent)) {\n    const title = _getTitle(g)\n    const pos = _getPartOfSpeech(g)\n    const conjugations = _getConjugation(g)\n    const pr = {} as Pronunciation\n    const sections: Section[] = []\n    const forms = _getForms(g)\n\n    const prEle = _getPrEle(g)\n    if (prEle) {\n      pr.syllable = _getSyllable(prEle)\n      pr.phonetics = []\n      const ptEles = _getPhoneticEles(prEle)\n      if (ptEles)\n        for (const p of ptEles) {\n          if (p) {\n            const symbol = _getPhoneticSymbol(p)\n            const audio = _getPhoneticAudio(p)\n            pr.phonetics.push({ symbol, audio })\n          }\n        }\n    }\n\n    const sEles = _getSectionsEles(g)\n    if (sEles)\n      for (const s of sEles) {\n        const title = _getSectionTitle(s)\n        const meaningGroups: MeaningGroup[] = []\n        const mgEles = _getMeaningGroupEles(s)\n\n        if (mgEles)\n          for (const mg of mgEles) {\n            const meanings: Meaning[] = []\n            const mEles = _getMeaningEles(mg)\n\n            if (mEles)\n              for (const m of mEles) {\n                const explaining = _getExpaining(m)\n                const examples = _getExamples(m)\n                meanings.push({ explaining, examples })\n              }\n\n            meaningGroups.push(meanings)\n          }\n\n        sections.push({ title, meaningGroups })\n      }\n\n    groups.push({ title, pos, pr, conjugation: conjugations, sections, forms })\n  }\n\n  return { groups, synonyms, etymology }\n}\n"
  },
  {
    "path": "src/components/dictionaries/mojidict/View.tsx",
    "content": "import React, { FC } from 'react'\nimport { PromiseType } from 'utility-types'\nimport Speaker from '@/components/Speaker'\nimport EntryBox from '@/components/EntryBox'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { message } from '@/_helpers/browser-api'\nimport { MojidictResult, GetTTS } from './engine'\n\nexport const DictMojidict: FC<ViewPorps<MojidictResult>> = ({ result }) => (\n  <>\n    {result.word && (\n      <div>\n        <h1>{result.word.spell}</h1>\n        <span>{result.word.pron}</span>{' '}\n        <Speaker\n          src={\n            result.word.tts ||\n            (() =>\n              message.send<\n                'DICT_ENGINE_METHOD',\n                PromiseType<ReturnType<GetTTS>>\n              >({\n                type: 'DICT_ENGINE_METHOD',\n                payload: {\n                  id: 'mojidict',\n                  method: 'getTTS',\n                  args: [result.word?.tarId, 102]\n                }\n              }))\n          }\n        />\n      </div>\n    )}\n    {result.details &&\n      result.details.map(detail => (\n        <EntryBox key={detail.title} title={detail.title}>\n          {detail.subdetails && (\n            <ul className=\"dictMojidict-List\">\n              {detail.subdetails.map(subdetail => (\n                <li\n                  key={subdetail.title}\n                  className=\"dictMojidict-ListItem_Disc\"\n                >\n                  <p>{subdetail.title}</p>\n                  {subdetail.examples && (\n                    <ul className=\"dictMojidict-Sublist\">\n                      {subdetail.examples.map(example => (\n                        <li key={example.title}>\n                          <p className=\"dictMojidict-Word_Title\">\n                            {example.title}\n                            <Speaker\n                              src={() =>\n                                message.send<\n                                  'DICT_ENGINE_METHOD',\n                                  PromiseType<ReturnType<GetTTS>>\n                                >({\n                                  type: 'DICT_ENGINE_METHOD',\n                                  payload: {\n                                    id: 'mojidict',\n                                    method: 'getTTS',\n                                    args: [example.objectId, 103]\n                                  }\n                                })\n                              }\n                            />\n                          </p>\n                          <p className=\"dictMojidict-Word_Trans\">\n                            {example.trans}\n                          </p>\n                        </li>\n                      ))}\n                    </ul>\n                  )}\n                </li>\n              ))}\n            </ul>\n          )}\n        </EntryBox>\n      ))}\n    {result.releated && (\n      <EntryBox title=\"関連用語\">\n        <ul className=\"dictMojidict-List\">\n          {result.releated.map(word => (\n            <li key={word.title}>\n              <p className=\"dictMojidict-Word_Title\">{word.title}</p>\n              <p className=\"dictMojidict-Word_Trans\">{word.excerpt}</p>\n            </li>\n          ))}\n        </ul>\n      </EntryBox>\n    )}\n  </>\n)\n\nexport default DictMojidict\n"
  },
  {
    "path": "src/components/dictionaries/mojidict/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"MOJi辞書\",\n    \"zh-CN\": \"MOJi辞書\",\n    \"zh-TW\": \"MOJi辞書\"\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/mojidict/_style.shadow.scss",
    "content": ".dictMojidict-Word_Title {\n  margin-bottom: 0;\n}\n\n.dictMojidict-Word_Trans {\n  margin-top: 0;\n  padding-left: 0.5em;\n  font-size: 0.9em;\n  color: #999;\n}\n\n.dictMojidict-List {\n  padding-left: 1em;\n}\n\n.dictMojidict-Sublist {\n  padding-left: 0.5em;\n}\n\n.dictMojidict-ListItem_Disc {\n  list-style-type: disc;\n}\n"
  },
  {
    "path": "src/components/dictionaries/mojidict/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type LianganConfig = DictItem\n\nexport default (): LianganConfig => ({\n  lang: '10010000',\n  selectionLang: {\n    english: false,\n    chinese: true,\n    japanese: true,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 265,\n  selectionWC: {\n    min: 1,\n    max: 5\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/mojidict/engine.ts",
    "content": "import {\n  SearchFunction,\n  GetSrcPageFunction,\n  handleNoResult,\n  handleNetWorkError\n} from '../helpers'\nimport axios, { AxiosResponse } from 'axios'\n\nexport const getSrcPage: GetSrcPageFunction = async text => {\n  const suggests = await getSuggests(text).catch(() => null)\n  if (suggests) {\n    const tarId =\n      suggests.searchResults &&\n      suggests.searchResults[0] &&\n      suggests.searchResults[0].tarId\n    if (tarId) {\n      return `https://www.mojidict.com/details/${tarId}`\n    }\n  }\n  return 'https://www.mojidict.com'\n}\n\ninterface FetchWordResult {\n  details?: Array<{\n    objectId: string\n    title: string\n    wordId: string\n  }>\n  examples?: Array<{\n    objectId: string\n    subdetailsId: string\n    title: string\n    trans: string\n    wordId: string\n  }>\n  subdetails?: Array<{\n    detailsId: string\n    objectId: string\n    title: string\n    wordId: string\n  }>\n  word?: {\n    accent: string\n    objectId: string\n    pron: string\n    spell: string\n    tts: string\n  }\n}\n\ninterface SuggestsResult {\n  originalSearchText: string\n  searchResults?: Array<{\n    objectId: string\n    searchText: string\n    tarId: string\n  }>\n  words?: Array<{\n    accent: string\n    excerpt: string\n    objectId: string\n    pron: string\n    romaji: string\n    spell: string\n  }>\n}\n\ninterface FetchTtsResult {\n  result: {\n    code: number\n    result?: {\n      text: string\n      url: string\n      identity: string\n      existed: boolean\n      msg: string\n    }\n  }\n}\n\nexport interface MojidictResult {\n  word?: {\n    tarId: string\n    spell: string\n    pron: string\n    tts?: string\n  }\n  details?: Array<{\n    objectId: string\n    title: string\n    subdetails?: Array<{\n      objectId: string\n      title: string\n      examples?: Array<{\n        objectId: string\n        title: string\n        trans: string\n      }>\n    }>\n  }>\n  releated?: Array<{\n    title: string\n    excerpt: string\n  }>\n}\n\nexport const search: SearchFunction<MojidictResult> = async (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  const suggests = await getSuggests(text)\n\n  const tarId = suggests.searchResults?.[0]?.tarId\n  if (!tarId) {\n    return handleNoResult()\n  }\n\n  const {\n    data: { result: wordResult }\n  }: AxiosResponse<{ result: FetchWordResult }> = await axios({\n    method: 'post',\n    url: 'https://api.mojidict.com/parse/functions/fetchWord_v2',\n    headers: {\n      'content-type': 'text/plain'\n    },\n    data: requestPayload({ wordId: tarId })\n  })\n\n  const result: MojidictResult = {}\n\n  if (wordResult && (wordResult.details || wordResult.word)) {\n    if (wordResult.word) {\n      result.word = {\n        tarId,\n        spell: wordResult.word.spell,\n        pron: `${wordResult.word.pron || ''} ${wordResult.word.accent || ''}`\n      }\n    }\n\n    if (wordResult.details) {\n      result.details = wordResult.details.map(detail => ({\n        objectId: detail.objectId,\n        title: detail.title,\n        subdetails: wordResult?.subdetails\n          ?.filter(subdetail => subdetail.detailsId === detail.objectId)\n          .map(subdetail => ({\n            objectId: subdetail.objectId,\n            title: subdetail.title,\n            examples: wordResult?.examples?.filter(\n              example => example.subdetailsId === subdetail.objectId\n            )\n          }))\n      }))\n    }\n\n    if (suggests.words && suggests?.words.length > 1) {\n      result.releated = suggests.words\n        .map(word => ({\n          title: `${word.spell} | ${word.pron || ''} ${word.accent || ''}`,\n          excerpt: word.excerpt\n        }))\n        .slice(1)\n    }\n\n    if (result.word && config.autopron.cn.dict === 'mojidict') {\n      result.word.tts = await getTTS(tarId, 102)\n      return { result, audio: { py: result.word.tts } }\n    }\n\n    return { result }\n  }\n\n  return handleNoResult()\n}\n\nasync function getSuggests(text: string): Promise<SuggestsResult> {\n  try {\n    const {\n      data: { result }\n    }: AxiosResponse<{ result?: SuggestsResult }> = await axios({\n      method: 'post',\n      url: 'https://api.mojidict.com/parse/functions/search_v3',\n      headers: {\n        'content-type': 'text/plain'\n      },\n      data: requestPayload({\n        langEnv: 'zh-CN_ja',\n        needWords: true,\n        searchText: text\n      })\n    })\n\n    return result || handleNoResult()\n  } catch (e) {\n    return handleNetWorkError()\n  }\n}\n\n/**\n * @param tarId word id\n * @param tarType 102 word, 103 sentence\n */\nexport async function getTTS(\n  tarId: string,\n  tarType: 102 | 103\n): Promise<string> {\n  try {\n    const { data }: AxiosResponse<FetchTtsResult> = await axios({\n      method: 'post',\n      url: 'https://api.mojidict.com/parse/functions/fetchTts_v2',\n      headers: {\n        'content-type': 'text/plain'\n      },\n      data: requestPayload({ tarId, tarType })\n    })\n\n    return data.result?.result?.url || ''\n  } catch (e) {\n    if (process.env.DEBUG) {\n      console.error(e)\n    }\n  }\n  return ''\n}\n\nexport type GetTTS = typeof getTTS\n\nfunction requestPayload(data: object) {\n  return JSON.stringify({\n    _ApplicationId: process.env.MOJI_ID,\n    _ClientVersion: 'js2.12.0',\n    _InstallationId: getInstallationId(),\n    ...data\n  })\n}\n\nfunction getInstallationId() {\n  return s() + s() + '-' + s() + '-' + s() + '-' + s() + '-' + s() + s() + s()\n}\n\nfunction s() {\n  return Math.floor(65536 * (1 + Math.random()))\n    .toString(16)\n    .substring(1)\n}\n"
  },
  {
    "path": "src/components/dictionaries/naver/View.tsx",
    "content": "import React, { FC } from 'react'\nimport Speaker from '@/components/Speaker'\nimport { NaverResult } from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictNaver: FC<ViewPorps<NaverResult>> = props => {\n  const ListMap = props.result.entry\n\n  return (\n    <>\n      <select\n        onChange={e =>\n          props.searchText({\n            id: 'naver',\n            payload: { lang: e.target.value }\n          })\n        }\n        value={props.result.lang}\n      >\n        <option key=\"zh\" value=\"zh\">\n          中韩\n        </option>\n        <option key=\"ja\" value=\"ja\">\n          日韓\n        </option>\n      </select>\n\n      {/* entry */}\n      {!!ListMap?.WORD?.items?.length && (\n        <div className={'dictNaver-EntryBox'}>\n          <span className={'dictNaver-EntryBoxTitle'}>单词</span>\n\n          {ListMap.WORD.items.map((word, wordI) => {\n            return (\n              <div className={'dictNaver-Entry'} key={wordI}>\n                <StrElm\n                  tag=\"h3\"\n                  className={'dictNaver-EntryTitle'}\n                  html={word.expEntry}\n                />\n                {word.expEntrySuperscript && (\n                  <sup className={'dictNaver-EntrySup'}>\n                    {word.expEntrySuperscript}\n                  </sup>\n                )}\n                {word.expKanji && (\n                  <>\n                    [\n                    <StrElm\n                      tag=\"span\"\n                      className={'dictNaver-EntryKanji'}\n                      html={word.expKanji}\n                    />\n                    ]\n                  </>\n                )}\n\n                <div className={'dictNaver-EntryPron'}>\n                  {!!word?.expAliasGeneralAlwaysList?.length && (\n                    <span className={'dictNaver-EntryPronVal'}>\n                      {word?.expAliasGeneralAlwaysList[0].originLanguageValue}\n                    </span>\n                  )}\n                  {!!word?.searchPhoneticSymbolList?.length && (\n                    <>\n                      {word.searchPhoneticSymbolList[0].phoneticSymbol && (\n                        <StrElm\n                          tag=\"span\"\n                          html={`[${word.searchPhoneticSymbolList[0].phoneticSymbol}]`}\n                        />\n                      )}\n                      {word?.searchPhoneticSymbolList[0]\n                        ?.phoneticSymbolPath && (\n                        <Speaker\n                          src={\n                            word.searchPhoneticSymbolList[0]?.phoneticSymbolPath?.split(\n                              '|'\n                            ).length > 1\n                              ? word.searchPhoneticSymbolList[0]?.phoneticSymbolPath?.split(\n                                  '|'\n                                )[0]\n                              : word.searchPhoneticSymbolList[0]\n                                  .phoneticSymbolPath\n                          }\n                        />\n                      )}\n                    </>\n                  )}\n\n                  {word?.frequencyAdd?.split('^').map(wordF => (\n                    <span key={wordF} className={'dictNaver-EntryPronFa'}>\n                      {wordF}\n                    </span>\n                  ))}\n                </div>\n\n                <div className={'dictNaver-EntryExp'}>\n                  {word?.meansCollector?.map((wordMc, wordMcI) => {\n                    return (\n                      <ul key={wordMcI}>\n                        {wordMc.means.map((m, mI) => (\n                          <li key={mI}>\n                            {m.order && <span>{m.order}.</span>}\n                            {wordMc.partOfSpeech2 && (\n                              <span className={'dictNaver-EntryExpPos'}>\n                                {wordMc.partOfSpeech2}\n                              </span>\n                            )}\n                            {m.subjectGroup && <span>{m.subjectGroup}</span>}\n                            <StrElm tag=\"span\" html={m.value} />\n                          </li>\n                        ))}\n                      </ul>\n                    )\n                  })}\n                </div>\n\n                <a\n                  className={'dictNaver-EntrySource'}\n                  href={word.sourceDictnameLink}\n                >\n                  {word.sourceDictnameOri}\n                </a>\n              </div>\n            )\n          })}\n        </div>\n      )}\n\n      {/* mean */}\n      {!!ListMap?.MEANING?.items?.length && (\n        <div className={'dictNaver-MeanBox'}>\n          <span className={'dictNaver-MeanBoxTitle'}>释义</span>\n\n          {ListMap.MEANING.items.map((meaning, meaningI) => {\n            return (\n              <div className={'dictNaver-Mean'} key={meaningI}>\n                <StrElm\n                  tag=\"h3\"\n                  className={'dictNaver-MeanTitle'}\n                  html={meaning.expEntry}\n                />\n                {meaning.expEntrySuperscript && (\n                  <sup className={'dictNaver-MeanSup'}>\n                    {meaning.expEntrySuperscript}\n                  </sup>\n                )}\n\n                {!!meaning?.expAliasGeneralAlwaysList?.length && (\n                  <StrElm\n                    tag=\"span\"\n                    className={'dictNaver-MeanAlias'}\n                    html={\n                      meaning.expAliasGeneralAlwaysList[0].originLanguageValue\n                    }\n                  />\n                )}\n\n                <div className={'dictNaver-MeanPron'}>\n                  {!!meaning?.searchPhoneticSymbolList?.length && (\n                    <>\n                      <span>\n                        [{meaning.searchPhoneticSymbolList[0].phoneticSymbol}]\n                      </span>\n                      <Speaker\n                        src={\n                          meaning.searchPhoneticSymbolList[0].phoneticSymbolPath\n                        }\n                      />\n                    </>\n                  )}\n                </div>\n\n                <div className={'dictNaver-MeanExp'}>\n                  {meaning?.meansCollector?.map((meaningMc, meaningMcI) => {\n                    return (\n                      <ul key={meaningMcI}>\n                        {meaningMc?.means.map((m, mI) => (\n                          <li key={mI}>\n                            {m.order && <span>{m.order}.</span>}\n                            {meaningMc.partOfSpeech2 && (\n                              <span className={'dictNaver-MeanExpPos'}>\n                                {meaningMc.partOfSpeech2}\n                              </span>\n                            )}\n                            {m.subjectGroup && <span>{m.subjectGroup}</span>}\n                            {m.languageGroup && (\n                              <span className={'dictNaver-MeanExpLg'}>\n                                {m.languageGroup}\n                              </span>\n                            )}\n                            <StrElm tag=\"span\" html={m.value} />\n                          </li>\n                        ))}\n                      </ul>\n                    )\n                  })}\n                </div>\n\n                <a\n                  className={'dictNaver-MeanSource'}\n                  href={meaning.sourceDictnameLink}\n                >\n                  {meaning.sourceDictnameOri}\n                </a>\n              </div>\n            )\n          })}\n        </div>\n      )}\n\n      {/* example */}\n      {!!ListMap?.EXAMPLE?.items?.length && (\n        <div className={'dictNaver-ExampleBox'}>\n          <span className={'dictNaver-ExampleBoxTitle'}>例句</span>\n\n          {ListMap.EXAMPLE.items.map((example, exampleI) => {\n            return (\n              <div className={'dictNaver-Example'} key={exampleI}>\n                <StrElm\n                  tag=\"h3\"\n                  className={'dictNaver-ExampleTitle'}\n                  html={example.expExample1}\n                />\n\n                <div className={'dictNaver-ExamplePron'}>\n                  <Speaker\n                    src={\n                      props.result.lang === 'ja'\n                        ? `https://ja.dict.naver.com/api/nvoice?speaker=yuri&service=dictionary&speech_fmt=mp3&text=${example.exampleEncode}`\n                        : `https://zh.dict.naver.com/tts?service=zhkodict&from=pc&speaker=zh_cn&text=${example.exampleEncode}`\n                    }\n                  />\n                </div>\n\n                <div className={'dictNaver-ExamplePronun'}>\n                  {example.expExample1Pronun}\n                </div>\n\n                <StrElm\n                  className={'dictNaver-ExampleExtra'}\n                  html={example.expExample2}\n                />\n\n                <div>\n                  <a\n                    className={'dictNaver-ExampleSource'}\n                    href={example.sourceDictnameURL}\n                  >\n                    {example.sourceDictnameOri}\n                  </a>\n                </div>\n              </div>\n            )\n          })}\n        </div>\n      )}\n    </>\n  )\n}\n\nexport default DictNaver\n"
  },
  {
    "path": "src/components/dictionaries/naver/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Naver韩国语词典\",\n    \"zh-CN\": \"Naver韩国语词典\",\n    \"zh-TW\": \"Naver韩国语词典\"\n  },\n  \"options\": {\n    \"hanAsJa\": {\n      \"en\": \"Search Chinese with Japanese Dictionary\",\n      \"zh-CN\": \"用日语词典查中文\",\n      \"zh-TW\": \"用日語詞典查漢字\"\n    },\n    \"korAsJa\": {\n      \"en\": \"Search Korean with Japanese Dictionary\",\n      \"zh-CN\": \"用日语词典查韩语\",\n      \"zh-TW\": \"用日语词典查韓語\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/naver/_style.shadow.scss",
    "content": ".dictNaver-EntryBox {\n  border: 1px solid #eee;\n  padding: 1em 0.5em 0.5em;\n  margin-top: 1.4em;\n  position: relative;\n\n  .dictNaver-Entry {\n    margin-bottom: 4px;\n  }\n\n  .dictNaver-EntryBoxTitle {\n    position: absolute;\n    top: 0;\n    left: 0.8em;\n    transform: translateY(-50%);\n    padding: 0 0.4em;\n    font-size: 1.3em;\n    font-weight: bold;\n    background: var(--color-background);\n  }\n\n  .dictNaver-EntryTitle {\n    display: inline-block;\n    margin-right: 4px;\n    font-size: 1.2em;\n  }\n\n  .dictNaver-EntrySup {\n    margin: 0 4px;\n  }\n\n  .dictNaver-EntryKanji {\n    font-size: 1.1em;\n    font-weight: bold;\n  }\n\n  .dictNaver-EntryPron {\n    display: inline-block;\n\n    .dictNaver-EntryPronVal {\n      margin-right: 4px;\n      color: #666;\n    }\n\n    .dictNaver-EntryPronFa {\n      display: inline-block;\n      height: 14px;\n      line-height: 14px;\n      margin-right: 4px;\n      border: 1px solid #e0e0e0;\n      padding: 0 2px;\n      font-size: 12px;\n      color: #999;\n    }\n  }\n\n  .dictNaver-EntryExp {\n    .dictNaver-EntryExpPos {\n      color: #71829f;\n      margin: 0 4px;\n    }\n  }\n\n  .dictNaver-EntrySource {\n    color: #969696;\n    display: inline-block;\n    font-size: 12px;\n\n    &:hover {\n      text-decoration: none;\n    }\n  }\n}\n\n.dictNaver-MeanBox {\n  border: 1px solid #eee;\n  padding: 1em 0.5em 0.5em;\n  margin-top: 1.4em;\n  position: relative;\n\n  .dictNaver-MeanBoxTitle {\n    position: absolute;\n    top: 0;\n    left: 0.8em;\n    transform: translateY(-50%);\n    padding: 0 0.4em;\n    font-size: 1.3em;\n    font-weight: bold;\n    background: var(--color-background);\n  }\n\n  .dictNaver-MeanTitle {\n    display: inline-block;\n    margin-right: 4px;\n    font-size: 1.2em;\n  }\n\n  .dictNaver-MeanAlias {\n    color: #5b5b5b;\n    margin: 0 4px;\n  }\n\n  .dictNaver-MeanPron {\n    display: inline-block;\n  }\n\n  .dictNaver-MeanExp {\n    .dictNaver-MeanExpPos {\n      color: #71829f;\n      margin: 0 4px;\n    }\n\n    .dictNaver-MeanExpLg {\n      color: #858585;\n      margin-right: 4px;\n    }\n  }\n\n  .dictNaver-MeanSource {\n    color: #969696;\n    display: inline-block;\n    margin-top: 2px;\n    font-size: 12px;\n\n    &:hover {\n      text-decoration: none;\n    }\n  }\n}\n\n.dictNaver-ExampleBox {\n  border: 1px solid #eee;\n  padding: 1em 0.5em 0.5em;\n  margin-top: 1.4em;\n  position: relative;\n\n  .dictNaver-ExampleBoxTitle {\n    position: absolute;\n    top: 0;\n    left: 0.8em;\n    transform: translateY(-50%);\n    padding: 0 0.4em;\n    font-size: 1.3em;\n    font-weight: bold;\n    background: var(--color-background);\n  }\n\n  .dictNaver-ExampleTitle {\n    display: inline-block;\n    margin-right: 4px;\n    font-size: 1.2em;\n  }\n\n  .dictNaver-ExamplePron {\n    display: inline-block;\n  }\n\n  .dictNaver-ExampleSource {\n    color: #969696;\n    display: inline-block;\n    margin-top: 2px;\n    font-size: 12px;\n\n    &:hover {\n      text-decoration: none;\n    }\n  }\n}\n\nruby {\n  margin-right: 4px;\n\n  & rt {\n    color: #fb5b63;\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/naver/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type NaverConfig = DictItem<{\n  hanAsJa: boolean\n  korAsJa: boolean\n}>\n\nexport default (): NaverConfig => ({\n  lang: '01011000',\n  selectionLang: {\n    english: false,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 465,\n  selectionWC: {\n    min: 1,\n    max: 10\n  },\n  options: {\n    hanAsJa: false,\n    korAsJa: false\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/naver/engine.ts",
    "content": "import {\n  handleNoResult,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult\n} from '../helpers'\nimport { isContainJapanese, isContainKorean } from '@/_helpers/lang-check'\nimport axios from 'axios'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return isContainJapanese(text)\n    ? `https://ja.dict.naver.com/#/search?query=${encodeURIComponent(text)}`\n    : `https://zh.dict.naver.com/#/search?query=${encodeURIComponent(text)}`\n}\n\n/** Alternate machine translation result */\nexport interface NaverResult {\n  lang: 'zh' | 'ja'\n  entry: {\n    WORD: { items: any[] }\n    MEANING: { items: any[] }\n    EXAMPLE: { items: any[] }\n  }\n}\n\ninterface NaverPayload {\n  lang?: 'zh' | 'ja'\n}\n\ntype NaverSearchResult = DictSearchResult<NaverResult>\n\nexport const search: SearchFunction<NaverResult, NaverPayload> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  const { options } = profile.dicts.all.naver\n\n  if (\n    payload.lang === 'ja' ||\n    options.hanAsJa ||\n    isContainJapanese(text) ||\n    (options.korAsJa && isContainKorean(text))\n  ) {\n    return jaDict(text)\n  }\n\n  return zhDict(text)\n}\n\nasync function zhDict(text: string): Promise<NaverSearchResult> {\n  const { data } = await axios\n    .get(\n      `https://zh.dict.naver.com/api3/zhko/search?query=${encodeURIComponent(\n        text\n      )}&lang=zh_CN`\n    )\n    .catch(handleNetWorkError)\n\n  const ListMap = data?.searchResultMap?.searchResultListMap\n\n  if (!ListMap) {\n    return handleNoResult()\n  }\n\n  return {\n    result: {\n      lang: 'zh',\n      entry: ListMap\n    }\n  }\n}\n\nasync function jaDict(text: string): Promise<NaverSearchResult> {\n  const { data } = await axios\n    .get(\n      `https://ja.dict.naver.com/api3/jako/search?query=${encodeURIComponent(\n        text\n      )}`\n    )\n    .catch(handleNetWorkError)\n\n  const ListMap = data?.searchResultMap?.searchResultListMap\n\n  if (!ListMap) {\n    return handleNoResult()\n  }\n\n  return {\n    result: {\n      lang: 'ja',\n      entry: ListMap\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/oaldict/View.tsx",
    "content": "import React, { FC } from 'react'\nimport Speaker from '@/components/Speaker'\nimport { OaldictResult } from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictOal: FC<ViewPorps<OaldictResult>> = ({ result }) => (\n  <div>\n    <div className={'dictOal-TopContainer'}>\n      {/* title */}\n      <div className={'dictOal-TitleBox'}>\n        <span className={'dictOal-TitleBoxT'}>{result.title}</span>\n        {result.pos && (\n          <span className={'dictOal-TitleBoxPos'}>({result.pos})</span>\n        )}\n        {result.symbol && (\n          <span className={'dictOal-TitleBoxSymbol'}>{result.symbol}</span>\n        )}\n      </div>\n\n      {/* pron */}\n      <div className={'dictOal-Phonetics'}>\n        {result.pron.uk.sound && (\n          <div className={'dictOal-PhonsUK'}>\n            <span className={'dictOal-PhonsCountry'}>UK</span>\n            <Speaker src={result.pron.uk.sound} />\n            <span>{result.pron.uk.phon}</span>\n          </div>\n        )}\n        {result.pron.us.sound && (\n          <div className={'dictOal-PhonsUS'}>\n            <span className={'dictOal-PhonsCountry'}>US</span>\n            <Speaker src={result.pron.us.sound} />\n            <span>{result.pron.us.phon}</span>\n          </div>\n        )}\n      </div>\n    </div>\n\n    {/* senses */}\n    {!!result.senses.length && (\n      <ol className={'dictOal-SensesMultiple'}>\n        {result.senses.map((sense, senseI) => {\n          return (\n            <div key={senseI}>\n              <div>\n                {sense.title &&\n                  (!result.isPhrasal ? (\n                    <div className={'dictOal-SensesTitle'}>{sense.title}</div>\n                  ) : (\n                    <StrElm\n                      tag=\"span\"\n                      className=\"dictOal-SensesTitle\"\n                      html={sense.title || ''}\n                    />\n                  ))}\n\n                {sense.symbol && (\n                  <span className={'dictOal-TitleBoxSymbol'}>\n                    {sense.symbol}\n                  </span>\n                )}\n\n                {sense.variants && (\n                  <StrElm\n                    tag=\"div\"\n                    className=\"dictOal-SensesVariants\"\n                    style={{ marginLeft: '-20px' }}\n                    html={sense.variants}\n                  />\n                )}\n              </div>\n              {sense.means.map((mean, meanI) => {\n                return (\n                  <li key={meanI}>\n                    {mean.symbols && (\n                      <span className={'dictOal-SensesSymbols'}>\n                        {mean.symbols}\n                      </span>\n                    )}\n                    {mean.variants && !mean.variantsIsBlock && (\n                      <StrElm\n                        tag=\"span\"\n                        className=\"dictOal-SensesVariants\"\n                        html={mean.variants || ''}\n                      />\n                    )}\n                    {mean.grammar && (\n                      <span className={'dictOal-SensesGrammar'}>\n                        {mean.grammar}\n                      </span>\n                    )}\n                    {mean.labels && (\n                      <span className={'dictOal-SensesLabels'}>\n                        {mean.labels}\n                      </span>\n                    )}\n                    {mean.variants && mean.variantsIsBlock && (\n                      <StrElm\n                        className=\"dictOal-SensesVariants\"\n                        html={mean.variants || ''}\n                      />\n                    )}\n                    {mean.use && <div>{mean.use}</div>}\n\n                    {mean.cf && (\n                      <span className={'dictOal-SensesCf'}>{mean.cf}</span>\n                    )}\n                    <span>{mean.def}</span>\n                    <StrElm\n                      tag=\"p\"\n                      className=\"dictOal-MeanExamples\"\n                      html={mean.examples || ''}\n                    />\n                  </li>\n                )\n              })}\n            </div>\n          )\n        })}\n      </ol>\n    )}\n\n    {/* origin */}\n    {result.origin && (\n      <div className={'dictOal-Origin'}>\n        <span className={'dictOal-OriginTitle'}>Origin</span>\n        <StrElm\n          tag=\"p\"\n          className=\"dictOal-OriginMean\"\n          html={result.origin || ''}\n        />\n      </div>\n    )}\n\n    {/* idioms */}\n    {!!result.idioms.length && (\n      <div className={'dictOal-Idioms'}>\n        <div className={'dictOal-IdiomsT'}>Idioms</div>\n        {result.idioms.map((idiom, idiomI) => (\n          <div key={idiomI}>\n            <div>\n              <StrElm\n                className=\"dictOal-IdiomsTitle\"\n                html={idiom.title || ''}\n              />\n            </div>\n            <div>\n              {idiom.labels && (\n                <span className={'dictOal-IdiomsLabels'}>{idiom.labels}</span>\n              )}\n              {idiom.def}\n            </div>\n            <StrElm\n              className=\"dictOal-IdiomsExamples\"\n              html={idiom.examples || ''}\n            />\n          </div>\n        ))}\n      </div>\n    )}\n  </div>\n)\n\nexport default DictOal\n"
  },
  {
    "path": "src/components/dictionaries/oaldict/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Oxford Learner's Dict\",\n    \"zh-CN\": \"牛津高阶词典\",\n    \"zh-TW\": \"牛津高階詞典\"\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/oaldict/_style.shadow.scss",
    "content": ".dictOal-TitleBoxSymbol {\n  padding: 2px 5px;\n  color: #fff;\n  font-weight: 700;\n  font-size: 0.8em;\n  min-width: 14px;\n  text-align: center;\n  background-color: #444;\n  border-radius: 8px;\n}\n\n.dictOal-TitleBox {\n  .dictOal-TitleBoxT {\n    font-size: 1.5em;\n    font-weight: 700;\n    line-height: 1.6;\n  }\n\n  .dictOal-TitleBoxPos {\n    font-style: italic;\n    margin-left: 4px;\n    margin-right: 3px;\n  }\n}\n\n.dictOal-Phonetics {\n  display: flex;\n\n  .dictOal-PhonsCountry {\n    text-transform: uppercase;\n    color: #e84427;\n    font-size: 1em;\n    font-weight: 700;\n  }\n\n  .dictOal-PhonsUS {\n    margin-left: 4px;\n  }\n}\n\n.dictOal-SensesMultiple {\n  padding-left: 20px;\n  margin-top: 4px;\n\n  & li::marker {\n    font-weight: 700;\n  }\n\n  .dictOal-SensesTitle {\n    font-size: 1.2em;\n    font-weight: 700;\n    border-bottom: 1px solid var(--color-font-grey);\n    margin: auto 4px 4px -20px;\n\n    .pvarr:before {\n      content: '<->';\n      display: inline-block;\n    }\n  }\n\n  .dictOal-SensesLabels {\n    color: #6f6f6f;\n    font-style: italic;\n    font-weight: 400;\n    margin-right: 3px;\n  }\n\n  .dictOal-SensesVariants {\n    font-style: italic;\n    color: #6f6f6f;\n    margin-right: 3px;\n\n    & .v {\n      font-weight: 600;\n      font-style: normal;\n      color: #1a3561;\n    }\n  }\n\n  .dictOal-SensesSymbols {\n    margin-right: 3px;\n    padding: 2px 5px;\n    color: #fff;\n    font-weight: 700;\n    font-size: 0.8em;\n    min-width: 14px;\n    text-align: center;\n    background-color: #444;\n    border-radius: 8px;\n  }\n\n  .dictOal-SensesGrammar {\n    color: #6f6f6f;\n    font-weight: 400;\n    margin-right: 3px;\n  }\n\n  .dictOal-SensesCf {\n    font-weight: 600;\n    font-style: normal;\n    color: #333333;\n    margin-right: 0.25rem;\n\n    @include isDarkMode {\n      color: #9d9d9d;\n    }\n  }\n\n  .dictOal-MeanExamples {\n    .examples > li {\n      &::before {\n        content: '•';\n        padding-right: 8px;\n        margin-left: -4px;\n        color: rgb(111, 111, 111);\n      }\n\n      .x {\n        font-style: italic;\n      }\n\n      .cl {\n        font-weight: 600;\n        font-style: italic;\n        color: #333333;\n\n        @include isDarkMode {\n          color: #6f6f6f;\n        }\n      }\n    }\n  }\n}\n\n.dictOal-Origin {\n  .dictOal-OriginT {\n    font-size: 1.3em;\n    font-weight: 700;\n    line-height: 1.6;\n    color: #27a1b0;\n  }\n\n  .dictOal-OriginTitle {\n    font-size: 1em;\n    font-weight: 700;\n    line-height: 1.3;\n    color: #27a1b0;\n  }\n\n  .dictOal-OriginMean {\n    padding-left: 20px;\n  }\n}\n\n.dictOal-Idioms {\n  padding: 8px;\n  border: 1px solid #27a1b0;\n  border-radius: 8px 8px 0 0;\n\n  .dictOal-IdiomsT {\n    font-size: 1.3em;\n    font-weight: 700;\n    line-height: 1.6;\n    color: #27a1b0;\n  }\n\n  .dictOal-IdiomsTitle {\n    font-size: 1em;\n    font-weight: 700;\n    line-height: 1.3;\n  }\n\n  .dictOal-IdiomsLabels {\n    color: #6f6f6f;\n    font-weight: 400;\n    margin-right: 3px;\n  }\n\n  .dictOal-IdiomsExamples {\n    .examples > li {\n      padding-left: 8px;\n\n      &::before {\n        content: '•';\n        padding-right: 8px;\n        margin-left: -4px;\n        color: rgb(111, 111, 111);\n      }\n\n      .x {\n        font-style: italic;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/oaldict/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type OalDictConfig = DictItem\n\nexport default (): OalDictConfig => ({\n  lang: '10000000',\n  selectionLang: {\n    english: true,\n    chinese: false,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 240,\n  selectionWC: {\n    min: 1,\n    max: 5\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/oaldict/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  HTMLString,\n  getText,\n  getInnerHTML,\n  getOuterHTML,\n  handleNoResult,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult\n} from '../helpers'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return `https://www.oxfordlearnersdictionaries.com/search/english/direct/?q=${text}`\n}\n\nconst HOST = 'https://www.oxfordlearnersdictionaries.com'\n\ninterface Idiom {\n  title?: string\n  labels?: string\n  def?: string\n  examples?: HTMLString\n}\n\ninterface Mean {\n  symbols?: string\n  grammar?: string\n  labels?: string\n  variants?: HTMLString\n  variantsIsBlock?: boolean\n  use?: string\n  cf?: string\n  def?: string\n  examples?: HTMLString\n}\n\ninterface Sense {\n  title?: string\n  symbol?: string\n  variants?: string\n  means: Mean[]\n}\n\ninterface Ipron {\n  uk: {\n    sound?: string\n    phon?: string\n  }\n  us: {\n    sound?: string\n    phon?: string\n  }\n}\n\ninterface OaldictResultItem {\n  /** word */\n  title: string\n  pos?: string\n  symbol?: string\n  /** pronunciation */\n  pron: Ipron\n  /** sense and eg */\n  senses: Sense[]\n  origin?: HTMLString\n  /** idiom and eg */\n  idioms: Idiom[]\n  /** phrasal template */\n  isPhrasal?: boolean\n}\n\nexport type OaldictResult = OaldictResultItem\n\ntype OaldictSearchResult = DictSearchResult<OaldictResult>\n\nexport const search: SearchFunction<OaldictResult> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  return fetchDirtyDOM(\n    'https://www.oxfordlearnersdictionaries.com/search/english/direct/?q=' +\n      encodeURIComponent(text.replace(/\\s+/g, ' '))\n  )\n    .catch(handleNetWorkError)\n    .then(doc => handleDOM(doc))\n}\n\nfunction handleDOM(\n  doc: Document\n): OaldictSearchResult | Promise<OaldictSearchResult> {\n  const result: OaldictResultItem = {\n    title: '',\n    idioms: [],\n    senses: [],\n    pron: { uk: {}, us: {} }\n  }\n\n  const main = doc.querySelector('#entryContent') as HTMLElement\n  if (!main) {\n    return handleNoResult()\n  }\n\n  const $symbol = main.querySelector('.symbols>a') as HTMLElement\n\n  if ($symbol) {\n    result.symbol = ($symbol.getAttribute('href') || '').split('level=')[1]\n  }\n\n  const $title = main.querySelector('.top-container .webtop') as HTMLElement\n\n  if ($title) {\n    result.title = getText($title, '.headword')\n    result.pos = getText($title, '.pos')\n\n    const $pron = Array.from($title.querySelectorAll(':scope>.phonetics>div'))\n\n    $pron.map((pr, prI) => {\n      const pronKey = prI ? 'us' : 'uk'\n      const $sound = pr.querySelector('.sound') as HTMLElement\n      let sound\n      if ($sound) {\n        sound = $sound.getAttribute('data-src-mp3')\n      }\n      const phon = getText(pr, '.phon')\n\n      result.pron[pronKey].sound = sound\n      result.pron[pronKey].phon = phon\n    })\n  }\n\n  // senses_multiple senses_single and senses_phrasal\n  const $senses_li = Array.from(main.querySelectorAll('.entry>ol>.sense'))\n\n  const $senses = Array.from(main.querySelectorAll('.senses_multiple .shcut-g'))\n\n  const $senses_phrasal = Array.from(main.querySelectorAll('.entry>.pv-g'))\n\n  function handleGetMeans($mean, sense) {\n    const mean: Mean = {}\n\n    const $symbols = $mean.querySelector('.sensetop .symbols>a') as HTMLElement\n\n    if ($symbols) {\n      mean.symbols = ($symbols.getAttribute('href') || '').split('level=')[1]\n    }\n\n    mean.grammar = getText($mean.querySelector('.grammar'))\n\n    mean.labels = getText($mean.querySelector('.labels'))\n\n    const $variantsType = $mean.querySelector('.variants') as HTMLElement\n\n    if ($variantsType) {\n      mean.variantsIsBlock = $variantsType.getAttribute('type') === 'vf'\n\n      mean.variants = getInnerHTML(HOST, $mean, '.variants')\n    }\n\n    mean.use = getText($mean.querySelector('.use'))\n\n    mean.cf = getText(\n      $mean.querySelector(':scope>.cf') || $mean.querySelector('.sensetop>.cf')\n    )\n\n    mean.def = getText($mean.querySelector('.def'))\n\n    mean.examples = getOuterHTML(HOST, $mean, '.examples')\n\n    sense.means.push(mean)\n  }\n\n  // Judge whether it is wrapped by span label\n  if ($senses_phrasal.length) {\n    result.isPhrasal = true\n\n    $senses_phrasal.map($mean => {\n      const sense: Sense = {\n        title: '',\n        means: []\n      }\n\n      const $title = $mean.querySelector('.top-container .pv') as HTMLElement\n\n      if ($title) {\n        sense.symbol = $title.getAttribute('cefr') || ''\n        sense.title = getOuterHTML(HOST, $title)\n      }\n\n      const $variantsType = $mean.querySelector('.variants') as HTMLElement\n\n      if ($variantsType) {\n        sense.variants = getInnerHTML(HOST, $mean, '.variants')\n      }\n\n      const $senses_mul_p = Array.from(\n        $mean.querySelectorAll('.senses_multiple .sense')\n      )\n      const $senses_single_p = Array.from(\n        $mean.querySelectorAll('.sense_single .sense')\n      )\n      ;(\n        ($senses_mul_p.length && $senses_mul_p) ||\n        ($senses_single_p.length && $senses_single_p) ||\n        []\n      ).map($m => handleGetMeans($m, sense))\n\n      result.senses.push(sense)\n    })\n  } else if ($senses_li.length) {\n    const sense: Sense = {\n      title: '',\n      means: []\n    }\n    $senses_li.map($mean => handleGetMeans($mean, sense))\n    result.senses.push(sense)\n  } else {\n    $senses.map($sense => {\n      const sense: Sense = {\n        title: '',\n        means: []\n      }\n      sense.title = getText($sense.querySelector('.shcut'))\n\n      const $means = Array.from($sense.querySelectorAll('.sense'))\n\n      $means.map($mean => handleGetMeans($mean, sense))\n\n      result.senses.push(sense)\n    })\n  }\n\n  const $origin = main.querySelector(\n    '.senses_multiple>.collapse>span'\n  ) as HTMLElement\n\n  if ($origin && $origin.getAttribute('unbox') === 'wordorigin') {\n    result.origin = getInnerHTML(HOST, $origin, '.body')\n  }\n\n  const $idioms = Array.from(main.querySelectorAll('.idioms .idm-g'))\n\n  $idioms.map($idiom => {\n    const idiom: Idiom = {}\n\n    const $topC = $idiom.querySelector('.top-container')\n\n    if ($topC) {\n      idiom.title = getInnerHTML(HOST, $topC, '.webtop')\n    }\n\n    idiom.labels = getText($idiom.querySelector('.labels'))\n\n    idiom.def = getText($idiom.querySelector('.def'))\n\n    idiom.examples = getOuterHTML(HOST, $idiom, '.examples')\n\n    result.idioms.push(idiom)\n  })\n\n  if (result.title || result.senses.length > 0 || result.idioms.length > 0) {\n    return { result }\n  } else {\n    return handleNoResult()\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/renren/View.tsx",
    "content": "import React, { FC, useState, useEffect } from 'react'\nimport Speaker from '@/components/Speaker'\nimport { RenrenResult, RenrenSlide } from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { message } from '@/_helpers/browser-api'\nimport { StrElm } from '@/components/StrElm'\n\ninterface RenrenSlideProps {\n  slide: RenrenSlide\n}\n\nconst Slide: FC<RenrenSlideProps> = ({ slide }) => {\n  const [imgHeight, setImgHeight] = useState(240)\n  const [isImgLoaded, setImgLoaded] = useState(false)\n  useEffect(() => {\n    setImgLoaded(false)\n  }, [slide.cover])\n\n  return (\n    <div className=\"dictRenren-Slide\">\n      <div className=\"dictRenren-Slide_Speaker\">\n        <Speaker src={slide.mp3} width={20} />\n      </div>\n      <figure style={{ height: imgHeight }}>\n        <img\n          src={slide.cover}\n          alt={slide.en}\n          className={`dictRenren-Slide_Cover${isImgLoaded ? ' isLoaded' : ''}`}\n          onLoad={e => {\n            setImgHeight(e.currentTarget.height)\n            setImgLoaded(true)\n          }}\n        />\n        <figcaption>\n          <StrElm tag=\"p\" html={slide.en} className=\"dictRenren-Slide_En\" />\n          <p className=\"dictRenren-Slide_Chs\">{slide.chs}</p>\n        </figcaption>\n      </figure>\n    </div>\n  )\n}\n\nexport const DictRenren: FC<ViewPorps<RenrenResult>> = ({ result }) => {\n  const [slide, setSlide] = useState(0)\n  const [details, setDetails] = useState<{ [k: string]: RenrenSlide[] }>({})\n\n  useEffect(() => {\n    setSlide(0)\n  }, [result])\n\n  const handleDetailClick = async (e: React.MouseEvent) => {\n    e.preventDefault()\n    e.stopPropagation()\n\n    const selectedSlide = result[slide]\n    const detail = await message.send<'DICT_ENGINE_METHOD', RenrenSlide[]>({\n      type: 'DICT_ENGINE_METHOD',\n      payload: {\n        id: 'renren',\n        method: 'getDetail',\n        args: [selectedSlide.detail]\n      }\n    })\n\n    if (detail && detail.length > 0) {\n      setDetails(details => ({\n        ...details,\n        [selectedSlide.key]: detail\n      }))\n    }\n  }\n\n  return (\n    <>\n      <select\n        onChange={e => setSlide(Number(e.currentTarget.value) || 0)}\n        value={slide}\n      >\n        {result.map((item, i) => (\n          <option key={item.key} value={i}>\n            {item.title}\n          </option>\n        ))}\n      </select>\n      {details[result[slide].key] ? (\n        details[result[slide].key].map(slide => (\n          <Slide key={slide.cover + slide.mp3} slide={slide} />\n        ))\n      ) : (\n        <>\n          <Slide slide={result[slide].slide} />\n          <a\n            className=\"dictRenren-Detail\"\n            href={result[slide].detail}\n            onClick={handleDetailClick}\n          >\n            ⤋查看详情\n          </a>\n          {result[slide].context.map(ctx => (\n            <div key={ctx.title} className=\"dictRenren-Ctx\">\n              <p className=\"dictRenren-Ctx_Title\">{ctx.title}</p>\n              <div className=\"dictRenren-Ctx_Subtitles\">\n                {ctx.content.map(subtitle => (\n                  <p key={subtitle}>{subtitle}</p>\n                ))}\n              </div>\n            </div>\n          ))}\n        </>\n      )}\n    </>\n  )\n}\n\nexport default DictRenren\n"
  },
  {
    "path": "src/components/dictionaries/renren/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"91dict\",\n    \"zh-CN\": \"人人词典\",\n    \"zh-TW\": \"人人詞典\"\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/renren/_style.shadow.scss",
    "content": ".dictRenren-Slide {\n  overflow: hidden;\n  position: relative;\n  margin-bottom: 5px;\n  background: var(--color-divider);\n\n  figure {\n    width: 100%;\n    min-height: 200px;\n    margin: 0;\n    padding: 0;\n    transition: height 0.4s;\n\n    @include isAnimate {\n      transition: height 0.4s;\n    }\n  }\n\n  figcaption {\n    position: absolute;\n    z-index: 2;\n    bottom: 0;\n    left: 0;\n    width: 100%;\n\n    p {\n      text-align: center;\n      padding: 0 0.3em;\n      line-height: 1.2;\n      color: #fff;\n    }\n  }\n}\n\n.dictRenren-Slide_Cover {\n  position: absolute;\n  top: 0;\n  left: 0;\n  max-width: 100%;\n  opacity: 0;\n\n  &.isLoaded {\n    opacity: 1;\n  }\n\n  @include isAnimate {\n    transition: opacity 0.4s;\n  }\n}\n\n.dictRenren-Slide_Speaker {\n  position: absolute;\n  z-index: 2;\n  top: 5px;\n  left: 5px;\n  padding: 3px;\n  border-radius: 2px;\n  font-size: 0;\n  background-color: rgba(255, 255, 255, 0.1);\n\n  .saladict-Speaker {\n    margin: 0;\n  }\n}\n\n.dictRenren-Slide_En {\n  text-shadow: #005177 1px 0 0, #005177 0 1px 0, #005177 -1px 0 0,\n    #005177 0 -1px 0;\n\n  em {\n    color: #ffa800;\n  }\n}\n\n.dictRenren-Slide_Chs {\n  text-shadow: #00080a 1px 0 0, #00080a 0 1px 0, #00080a -1px 0 0,\n    #00080a 0 -1px 0;\n}\n\n.dictRenren-Detail {\n  display: block;\n  border: none;\n  width: 100%;\n  text-align: center;\n  font-size: 0.95em;\n  color: #5caf9e;\n  text-decoration: none !important;\n}\n\n.dictRenren-Ctx {\n  display: table;\n}\n\n.dictRenren-Ctx_Title {\n  display: table-cell;\n  width: 3em;\n  font-weight: 700;\n  text-align: right;\n}\n\n.dictRenren-Ctx_Subtitles {\n  display: table-cell;\n  padding-left: 1em;\n}\n"
  },
  {
    "path": "src/components/dictionaries/renren/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type RenrenConfig = DictItem\n\nexport default (): RenrenConfig => ({\n  lang: '11000000',\n  selectionLang: {\n    english: true,\n    chinese: true,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 400,\n  selectionWC: {\n    min: 1,\n    max: 999\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/renren/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  HTMLString,\n  getText,\n  getInnerHTML,\n  handleNoResult,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult,\n  getFullLink\n} from '../helpers'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return `https://www.91dict.com/words?w=${encodeURIComponent(\n    text.replace(/\\s+/g, '+')\n  )}`\n}\n\nconst HOST = 'https://www.91dict.com'\n\nexport interface RenrenSlide {\n  cover: string\n  mp3: string\n  en: HTMLString\n  chs: string\n}\n\ninterface RenrenResultItem {\n  key: string\n  title: string\n  detail: string\n  slide: RenrenSlide\n  context: Array<{\n    title: string\n    content: string[]\n  }>\n}\n\nexport type RenrenResult = RenrenResultItem[]\n\ntype RenrenSearchResult = DictSearchResult<RenrenResult>\n\nexport const search: SearchFunction<RenrenResult> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  return fetchDirtyDOM(\n    `https://www.91dict.com/words?w=${encodeURIComponent(\n      text.replace(/\\s+/g, '+')\n    )}`\n  )\n    .catch(handleNetWorkError)\n    .then(handleDOM)\n}\n\nfunction handleDOM(\n  doc: Document\n): RenrenSearchResult | Promise<RenrenSearchResult> {\n  const result: RenrenResult = []\n\n  const $slides = doc.querySelectorAll('.tmInfo .slides > li')\n\n  if ($slides.length <= 0) {\n    return handleNoResult()\n  }\n\n  $slides.forEach($li => {\n    const title = getText($li.querySelector('.mTop')).trim()\n    if (!title) return\n\n    const slide = extractSlide($li)\n    if (!slide) return\n\n    const item: RenrenResultItem = {\n      key: '',\n      title,\n      detail: '',\n      slide,\n      context: []\n    }\n\n    const $detail = $li.querySelector('.viewdetail')\n    if ($detail) {\n      item.detail = getFullLink(HOST, $detail, 'href')\n    }\n\n    $li.querySelectorAll('.mTextend .box').forEach($box => {\n      const title = getText($box, '.sty1')\n      if (!title) return\n\n      item.context.push({\n        title,\n        content: [...$box.querySelectorAll('.sty2 > *')].map($p => getText($p))\n      })\n    })\n\n    item.key = title + slide.cover\n\n    result.push(item)\n  })\n\n  if (result.length > 0) {\n    return { result }\n  } else {\n    return handleNoResult()\n  }\n}\n\nexport async function getDetail(src: string): Promise<RenrenSlide[]> {\n  const result: RenrenSlide[] = []\n\n  try {\n    const doc = await fetchDirtyDOM(src)\n    doc.querySelectorAll('.item li').forEach($li => {\n      const slide = extractSlide($li)\n      if (slide) {\n        result.push(slide)\n      }\n    })\n  } catch (e) {\n    console.warn(e)\n  }\n\n  return result\n}\n\nfunction extractSlide($li: Element): RenrenSlide | null {\n  const slide: RenrenSlide = {\n    cover: '',\n    mp3: '',\n    en: '',\n    chs: ''\n  }\n\n  const $cover = $li.querySelector('img')\n  if (!$cover) return null\n  const cover = getFullLink(HOST, $cover, 'src')\n  if (!cover) return null\n  slide.cover = cover\n\n  const $audio = $li.querySelector('.mTop audio')\n  if ($audio) {\n    slide.mp3 = getFullLink(HOST, $audio, 'src')\n  }\n\n  slide.en = getInnerHTML(HOST, $li, '.mBottom')\n  slide.chs = getText($li, '.mFoot').trim()\n\n  return slide\n}\n"
  },
  {
    "path": "src/components/dictionaries/shanbay/View.tsx",
    "content": "import React, { FC } from 'react'\nimport Speaker from '@/components/Speaker'\nimport { ShanbayResult } from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictShanbay: FC<ViewPorps<ShanbayResult>> = ({ result }) => (\n  <>\n    {result.title && (\n      <div className=\"dictShanbay-HeaderContainer\">\n        <h1 className=\"dictShanbay-Title\">{result.title}</h1>\n        <span className=\"dictShanbay-Pattern\">{result.pattern}</span>\n      </div>\n    )}\n    {result.prons.length > 0 && (\n      <div className=\"dictShanBay-HeaderContainer\">\n        {result.prons.map(({ phsym, url }) => (\n          <React.Fragment key={url}>\n            {phsym} <Speaker src={url} />\n          </React.Fragment>\n        ))}\n      </div>\n    )}\n    {result.basic && (\n      <StrElm className=\"dictShanbay-Basic\" html={result.basic} />\n    )}\n    {result.sentences && (\n      <div>\n        <h1 className=\"dictShanbay-SecTitle\">权威例句</h1>\n        <ol className=\"dictShanbay-Sentence\">\n          {result.sentences.map(sentence => (\n            <li key={sentence.annotation}>\n              <StrElm tag=\"p\" html={sentence.annotation} />\n              <p>{sentence.translation}</p>\n            </li>\n          ))}\n        </ol>\n      </div>\n    )}\n  </>\n)\n\nexport default DictShanbay\n"
  },
  {
    "path": "src/components/dictionaries/shanbay/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Shanbay Dictionary\",\n    \"zh-CN\": \"扇贝词典\",\n    \"zh-TW\": \"扇貝詞典\"\n  },\n  \"options\": {\n    \"basic\": {\n      \"en\": \"Show basic meaning\",\n      \"zh-CN\": \"显示简单释义\",\n      \"zh-TW\": \"顯示簡單解釋\"\n    },\n    \"sentence\": {\n      \"en\": \"Show sentences\",\n      \"zh-CN\": \"显示权威例句\",\n      \"zh-TW\": \"顯示權威例句\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/shanbay/_style.shadow.scss",
    "content": ".dictShanbay-HeaderContainer {\n  display: flex;\n  align-items: center;\n  flex-wrap: wrap;\n}\n\n.dictShanbay-Title {\n  font-size: 1.5em;\n  margin-right: 8px;\n}\n\n.dictShanbay-Pattern {\n  margin-top: 0.5em;\n}\n\n.dictShanbay-SecTitle {\n  font-size: 1.1em;\n  margin-top: 1em;\n  border-bottom: 1px solid rgba(199, 110, 6, 0.5);\n}\n\n.dictShanbay-Basic {\n  margin: 0.5em 0;\n\n  .definition-pos {\n    padding-right: 0.5em;\n  }\n}\n\n.dictShanbay-Sentence {\n  margin: 0;\n  padding: 0 0 0 1.5em;\n}\n"
  },
  {
    "path": "src/components/dictionaries/shanbay/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type ShanbayConfig = DictItem<{\n  basic: boolean\n  sentence: boolean\n}>\n\nexport default (): ShanbayConfig => ({\n  lang: '10000000',\n  selectionLang: {\n    english: true,\n    chinese: false,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 150,\n  selectionWC: {\n    min: 1,\n    max: 30\n  },\n  options: {\n    basic: true,\n    sentence: true\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/shanbay/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  getText,\n  getInnerHTML,\n  handleNoResult,\n  HTMLString,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult\n} from '../helpers'\nimport { DictConfigs } from '@/app-config'\nimport axios from 'axios'\nimport DOMPurify from 'dompurify'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return `https://www.shanbay.com/bdc/mobile/preview/word?word=${text}`\n}\n\nconst HOST = 'http://www.shanbay.com'\n\nexport interface ShanbayResultLex {\n  type: 'lex'\n  title: string\n  pattern: string\n  prons: Array<{\n    phsym: string\n    url: string\n  }>\n  basic?: HTMLString\n  wordId?: string | null\n  sentences: Array<{\n    annotation: string\n    translation: string\n  }>\n  translation?: HTMLString\n  id: 'shanbay'\n}\n\nexport type ShanbayResult = ShanbayResultLex\n\ntype ShanbaySearchResult = DictSearchResult<ShanbayResult>\n\nexport const search: SearchFunction<ShanbayResult> = (\n  text,\n  config,\n  profile\n) => {\n  const options = profile.dicts.all.shanbay.options\n  return fetchDirtyDOM(\n    'https://www.shanbay.com/bdc/mobile/preview/word?word=' +\n      encodeURIComponent(text.replace(/\\s+/g, ' '))\n  )\n    .catch(handleNetWorkError)\n    .then(doc => checkResult(doc, options))\n}\n\nfunction checkResult(\n  doc: Document,\n  options: DictConfigs['shanbay']['options']\n): ShanbaySearchResult | Promise<ShanbaySearchResult> {\n  const $typo = doc.querySelector('.error-typo')\n  if (!$typo) {\n    return handleDOM(doc, options)\n  }\n  return handleNoResult()\n}\n\nfunction loadSentences(id: string) {\n  return axios\n    .get(\n      `https://www.shanbay.com/api/v1/bdc/example/?vocabulary_id=${id}&type=sys`\n    )\n    .then(({ data: { data } }) => {\n      if (Array.isArray(data)) {\n        return data.map(\n          (sentence: { annotation: string; translation: string }) => {\n            return {\n              annotation: DOMPurify.sanitize(sentence.annotation),\n              translation: DOMPurify.sanitize(sentence.translation)\n            }\n          }\n        )\n      }\n      return []\n    })\n}\n\nasync function handleDOM(\n  doc: Document,\n  options: DictConfigs['shanbay']['options']\n): Promise<ShanbaySearchResult> {\n  const word = doc.querySelector('.word-spell')\n  const result: ShanbayResult = {\n    id: 'shanbay',\n    type: 'lex',\n    title: getText(doc, '.word-spell'),\n    pattern: getText(doc, '.pattern'),\n    prons: [],\n    sentences: []\n  }\n\n  const audio: { uk: string; us: string } = {\n    uk: 'http://media.shanbay.com/audio/uk/' + result.title + '.mp3',\n    us: 'http://media.shanbay.com/audio/us/' + result.title + '.mp3'\n  }\n\n  result.prons.push({\n    phsym: getText(doc, '.word-announace'),\n    url: audio.us\n  })\n\n  if (options.basic) {\n    result.basic = getInnerHTML(HOST, doc, '.definition-cn')\n  }\n\n  result.wordId = word && word.getAttribute('data-id')\n  if (options.sentence && result.wordId) {\n    result.sentences = await loadSentences(result.wordId)\n  }\n\n  if (result.title) {\n    return { result, audio }\n  }\n\n  return handleNoResult()\n}\n"
  },
  {
    "path": "src/components/dictionaries/sogou/View.tsx",
    "content": "export { MachineTrans as default } from '@/components/MachineTrans/MachineTrans'\n"
  },
  {
    "path": "src/components/dictionaries/sogou/_locales.ts",
    "content": "import { getMachineLocales } from '../locales'\n\nexport const locales = getMachineLocales({\n  en: 'Sogou Translation',\n  'zh-CN': '搜狗翻译',\n  'zh-TW': '搜狗翻譯'\n})\n"
  },
  {
    "path": "src/components/dictionaries/sogou/_style.shadow.scss",
    "content": "@import '@/components/MachineTrans/MachineTrans.scss';\n"
  },
  {
    "path": "src/components/dictionaries/sogou/auth.ts",
    "content": "export const auth = {\n  pid: '',\n  key: ''\n}\n\nexport const url = 'https://deepi.sogou.com/?from=translatepc'\n"
  },
  {
    "path": "src/components/dictionaries/sogou/config.ts",
    "content": "import {\n  MachineDictItem,\n  machineConfig\n} from '@/components/MachineTrans/engine'\nimport { Language } from '@opentranslate/translator'\nimport { Subunion } from '@/typings/helpers'\n\nexport type SogouLanguage = Subunion<\n  Language,\n  'zh-CN' | 'zh-TW' | 'en' | 'ja' | 'ko' | 'fr' | 'de' | 'es' | 'ru'\n>\n\nexport type SogouConfig = MachineDictItem<SogouLanguage>\n\nexport default (): SogouConfig =>\n  machineConfig<SogouConfig>(\n    ['zh-CN', 'zh-TW', 'en', 'ja', 'ko', 'fr', 'de', 'es', 'ru'],\n    {},\n    {},\n    {}\n  )\n"
  },
  {
    "path": "src/components/dictionaries/sogou/engine.ts",
    "content": "import { SearchFunction, GetSrcPageFunction } from '../helpers'\nimport memoizeOne from 'memoize-one'\nimport { Sogou } from '@opentranslate/sogou'\nimport {\n  MachineTranslateResult,\n  MachineTranslatePayload,\n  getMTArgs,\n  machineResult\n} from '@/components/MachineTrans/engine'\nimport { SogouLanguage } from './config'\n\nexport const getTranslator = memoizeOne(\n  () =>\n    new Sogou({\n      env: 'ext',\n      config:\n        process.env.SOGOU_PID && process.env.SOGOU_KEY\n          ? {\n              pid: process.env.SOGOU_PID,\n              key: process.env.SOGOU_KEY\n            }\n          : undefined\n    })\n)\n\nexport const getSrcPage: GetSrcPageFunction = (text, config, profile) => {\n  const lang =\n    profile.dicts.all.sogou.options.tl === 'default'\n      ? config.langCode === 'zh-CN'\n        ? 'zh-CHS'\n        : config.langCode === 'zh-TW'\n        ? 'zh-CHT'\n        : 'en'\n      : profile.dicts.all.sogou.options.tl\n\n  return `https://fanyi.sogou.com/#auto/${lang}/${text}`\n}\n\nexport type SogouResult = MachineTranslateResult<'sogou'>\n\nexport const search: SearchFunction<\n  SogouResult,\n  MachineTranslatePayload<SogouLanguage>\n> = async (rawText, config, profile, payload) => {\n  if (!config.dictAuth.sogou.pid) {\n    return machineResult(\n      {\n        result: {\n          requireCredential: true,\n          id: 'sogou',\n          sl: 'auto',\n          tl: 'auto',\n          slInitial: 'hide',\n          searchText: { paragraphs: [''] },\n          trans: { paragraphs: [''] }\n        }\n      },\n      []\n    )\n  }\n\n  const translator = getTranslator()\n\n  const { sl, tl, text } = await getMTArgs(\n    translator,\n    rawText,\n    profile.dicts.all.sogou,\n    config,\n    payload\n  )\n\n  const translatorConfig = {\n    pid: config.dictAuth.sogou.pid,\n    key: config.dictAuth.sogou.key\n  }\n\n  try {\n    const result = await translator.translate(text, sl, tl, translatorConfig)\n    return machineResult(\n      {\n        result: {\n          id: 'sogou',\n          sl: result.from,\n          tl: result.to,\n          slInitial: profile.dicts.all.sogou.options.slInitial,\n          searchText: result.origin,\n          trans: result.trans\n        },\n        audio: {\n          py: result.trans.tts,\n          us: result.trans.tts\n        }\n      },\n      translator.getSupportLanguages()\n    )\n  } catch (e) {\n    return machineResult(\n      {\n        result: {\n          id: 'sogou',\n          sl,\n          tl,\n          slInitial: 'hide',\n          searchText: { paragraphs: [''] },\n          trans: { paragraphs: [''] }\n        }\n      },\n      translator.getSupportLanguages()\n    )\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/tencent/View.tsx",
    "content": "export { MachineTrans as default } from '@/components/MachineTrans/MachineTrans'\n"
  },
  {
    "path": "src/components/dictionaries/tencent/_locales.ts",
    "content": "import { getMachineLocales } from '../locales'\n\nexport const locales = getMachineLocales({\n  en: 'Tencent Translate',\n  'zh-CN': '腾讯翻译君',\n  'zh-TW': '騰訊翻譯君'\n})\n"
  },
  {
    "path": "src/components/dictionaries/tencent/_style.shadow.scss",
    "content": "@import '@/components/MachineTrans/MachineTrans.scss';\n"
  },
  {
    "path": "src/components/dictionaries/tencent/auth.ts",
    "content": "export const auth = {\n  secretId: '',\n  secretKey: ''\n}\n\nexport const url = 'https://curl.qcloud.com/imsowZzT'\n"
  },
  {
    "path": "src/components/dictionaries/tencent/config.ts",
    "content": "import {\n  MachineDictItem,\n  machineConfig\n} from '@/components/MachineTrans/engine'\nimport { Language } from '@opentranslate/translator'\nimport { Subunion } from '@/typings/helpers'\n\nexport type TencentLanguage = Subunion<\n  Language,\n  'zh-CN' | 'en' | 'ja' | 'ko' | 'fr' | 'de' | 'es' | 'ru'\n>\n\nexport type TencentConfig = MachineDictItem<TencentLanguage>\n\nexport default (): TencentConfig =>\n  machineConfig<TencentConfig>(\n    ['zh-CN', 'en', 'ja', 'ko', 'fr', 'de', 'es', 'ru'],\n    {\n      lang: '11011111'\n    },\n    {},\n    {}\n  )\n"
  },
  {
    "path": "src/components/dictionaries/tencent/engine.ts",
    "content": "import { SearchFunction, GetSrcPageFunction } from '../helpers'\nimport memoizeOne from 'memoize-one'\nimport { Tencent } from '@opentranslate/tencent'\nimport {\n  MachineTranslateResult,\n  MachineTranslatePayload,\n  getMTArgs,\n  machineResult\n} from '@/components/MachineTrans/engine'\nimport { getTranslator as getBaiduTranslator } from '../baidu/engine'\nimport { TencentLanguage } from './config'\n\nexport const getTranslator = memoizeOne(\n  () =>\n    new Tencent({\n      env: 'ext',\n      config:\n        process.env.TENCENT_SECRETID && process.env.TENCENT_SECRETKEY\n          ? {\n              secretId: process.env.TENCENT_SECRETID,\n              secretKey: process.env.TENCENT_SECRETKEY\n            }\n          : undefined\n    })\n)\n\nexport const getSrcPage: GetSrcPageFunction = (text, config, profile) => {\n  const lang =\n    profile.dicts.all.tencent.options.tl === 'default'\n      ? config.langCode === 'zh-CN'\n        ? 'zh-CHS'\n        : config.langCode === 'zh-TW'\n        ? 'zh-CHT'\n        : 'en'\n      : profile.dicts.all.tencent.options.tl\n\n  return `https://fanyi.qq.com/#auto/${lang}/${text}`\n}\n\nexport type TencentResult = MachineTranslateResult<'tencent'>\n\nexport const search: SearchFunction<\n  TencentResult,\n  MachineTranslatePayload<TencentLanguage>\n> = async (rawText, config, profile, payload) => {\n  const translator = getTranslator()\n\n  const { sl, tl, text } = await getMTArgs(\n    translator,\n    rawText,\n    profile.dicts.all.tencent,\n    config,\n    payload\n  )\n\n  const secretId = config.dictAuth.tencent.secretId\n  const secretKey = config.dictAuth.tencent.secretKey\n  const translatorConfig =\n    secretId && secretKey ? { secretId, secretKey } : undefined\n\n  if (!translatorConfig) {\n    return machineResult(\n      {\n        result: {\n          requireCredential: true,\n          id: 'tencent',\n          sl: 'auto',\n          tl: 'auto',\n          slInitial: 'hide',\n          searchText: { paragraphs: [''] },\n          trans: { paragraphs: [''] }\n        }\n      },\n      []\n    )\n  }\n\n  try {\n    const result = await translator.translate(text, sl, tl, translatorConfig)\n    // Tencent needs extra api credits for TTS which does\n    // not fit in the current Saladict architecture.\n    // Use Baidu instead.\n    const baidu = getBaiduTranslator()\n    result.origin.tts = await baidu.textToSpeech(\n      result.origin.paragraphs.join('\\n'),\n      result.from\n    )\n    result.trans.tts = await baidu.textToSpeech(\n      result.trans.paragraphs.join('\\n'),\n      result.to\n    )\n\n    return machineResult(\n      {\n        result: {\n          id: 'tencent',\n          sl: result.from,\n          tl: result.to,\n          slInitial: profile.dicts.all.tencent.options.slInitial,\n          searchText: result.origin,\n          trans: result.trans\n        },\n        audio: {\n          py: result.trans.tts,\n          us: result.trans.tts\n        }\n      },\n      translator.getSupportLanguages()\n    )\n  } catch (e) {\n    return machineResult(\n      {\n        result: {\n          id: 'tencent',\n          sl,\n          tl,\n          slInitial: 'hide',\n          searchText: { paragraphs: [''] },\n          trans: { paragraphs: [''] }\n        }\n      },\n      translator.getSupportLanguages()\n    )\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/urban/View.tsx",
    "content": "import React, { FC } from 'react'\nimport Speaker from '@/components/Speaker'\nimport { UrbanResult } from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictUrban: FC<ViewPorps<UrbanResult>> = ({ result }) => (\n  <ul className=\"dictUrban-List\">\n    {result.map(def => (\n      <li key={def.meaning} className=\"dictUrban-Item\">\n        <h2 className=\"dictUrban-Title\">\n          {def.title} <Speaker src={def.pron} />\n        </h2>\n        {def.meaning && (\n          <StrElm tag=\"p\" className=\"dictUrban-Meaning\" html={def.meaning} />\n        )}\n        {def.example && (\n          <StrElm tag=\"p\" className=\"dictUrban-Example\" html={def.example} />\n        )}\n        {def.gif && (\n          <figure className=\"dictUrban-Gif\">\n            <img src={def.gif.src} alt={def.gif.attr} />\n            <figcaption>{def.gif.attr}</figcaption>\n          </figure>\n        )}\n        {def.tags && (\n          <ul className=\"dictUrban-Tags\">\n            {def.tags.map(tag => (\n              <a\n                key={tag}\n                className=\"dictUrban-TagItem\"\n                href={`https://www.urbandictionary.com/tags.php?tag=${tag}`}\n                rel=\"nofollow noopener noreferrer\"\n              >\n                #{tag}{' '}\n              </a>\n            ))}\n          </ul>\n        )}\n        <footer className=\"dictUrban-Footer\">\n          {def.contributor && (\n            <span className=\"dictUrban-Contributor\">{def.contributor}</span>\n          )}\n          {typeof def.thumbsUp === 'number' && (\n            <span className=\"dictUrban-Thumbs\">\n              <svg\n                className=\"dictUrban-IconThumbsUp\"\n                width=\"0.9em\"\n                height=\"0.9em\"\n                fill=\"#666\"\n                viewBox=\"0 0 561 561\"\n              >\n                <path d=\"M0 535.5h102v-306H0v306zM561 255c0-28.05-22.95-51-51-51H349.35l25.5-117.3v-7.65c0-10.2-5.1-20.4-10.2-28.05L336.6 25.5 168.3 193.8c-10.2 7.65-15.3 20.4-15.3 35.7v255c0 28.05 22.95 51 51 51h229.5c20.4 0 38.25-12.75 45.9-30.6l76.5-181.05c2.55-5.1 2.55-12.75 2.55-17.85v-51H561c0 2.55 0 0 0 0z\" />\n              </svg>\n              {def.thumbsUp}\n            </span>\n          )}\n          {typeof def.thumbsDown === 'number' && (\n            <span className=\"dictUrban-Thumbs\">\n              <svg\n                className=\"dictUrban-IconThumbsDown\"\n                width=\"0.95em\"\n                height=\"0.95em\"\n                fill=\"#666\"\n                viewBox=\"0 0 561 561\"\n              >\n                <path d=\"M357 25.5H127.5c-20.4 0-38.25 12.75-45.9 30.6L5.1 237.15C2.55 242.25 0 247.35 0 255v51c0 28.05 22.95 51 51 51h160.65l-25.5 117.3v7.65c0 10.2 5.1 20.4 10.2 28.05l28.05 25.5 168.3-168.3c10.2-10.2 15.3-22.95 15.3-35.7v-255c0-28.05-22.95-51-51-51zm102 0v306h102v-306H459z\" />\n              </svg>\n              {def.thumbsDown}\n            </span>\n          )}\n        </footer>\n      </li>\n    ))}\n  </ul>\n)\n\nexport default DictUrban\n"
  },
  {
    "path": "src/components/dictionaries/urban/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Urban\",\n    \"zh-CN\": \"Urban\",\n    \"zh-TW\": \"Urban\"\n  },\n  \"options\": {\n    \"resultnum\": {\n      \"en\": \"Show\",\n      \"zh-CN\": \"结果数量\",\n      \"zh-TW\": \"結果數量\"\n    },\n    \"resultnum_unit\": {\n      \"en\": \"results\",\n      \"zh-CN\": \"个\",\n      \"zh-TW\": \"個\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/urban/_style.shadow.scss",
    "content": ".dictUrban-Title {\n  font-size: 1.2em;\n}\n\n.dictUrban-Item {\n  margin-bottom: 10px;\n}\n\n.dictUrban-Meaning {\n  margin: 0 0 8px 0;\n}\n\n.dictUrban-Example {\n  margin: 0 0 8px 0.5em;\n  padding-left: 5px;\n  color: var(--color-font-grey);\n  border-left: 1px solid #666;\n}\n\n.dictUrban-Gif {\n  text-align: center;\n\n  > img {\n    max-width: 80%;\n    max-height: 200px;\n  }\n}\n\n.dictUrban-TagItem {\n  margin-right: 0.5em;\n  text-decoration: none;\n  color: #16a085;\n}\n\n.dictUrban-Footer {\n  color: var(--color-font-grey);\n}\n\n.dictUrban-Contributor {\n  margin-right: 1em;\n}\n\n.dictUrban-Thumbs {\n  margin-right: 5px;\n}\n\n.dictUrban-IconThumbsUp {\n  width: 0.9em;\n  height: 0.9em;\n  fill: #666;\n  margin-right: 2px;\n}\n\n.dictUrban-IconThumbsDown {\n  width: 0.95em;\n  height: 0.95em;\n  fill: #666;\n  vertical-align: middle;\n  margin-right: 2px;\n}\n"
  },
  {
    "path": "src/components/dictionaries/urban/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type UrbanConfig = DictItem<{\n  resultnum: number\n}>\n\nexport default (): UrbanConfig => ({\n  lang: '10000000',\n  selectionLang: {\n    english: true,\n    chinese: false,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 180,\n  selectionWC: {\n    min: 1,\n    max: 5\n  },\n  options: {\n    resultnum: 4\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/urban/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  HTMLString,\n  getText,\n  getInnerHTML,\n  handleNoResult,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult\n} from '../helpers'\n\nimport axios from 'axios'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return `http://www.urbandictionary.com/define.php?term=${text}`\n}\n\nconst HOST = 'https://www.urbandictionary.com'\n\ninterface UrbanResultItem {\n  /** keyword */\n  title: string\n  /** pronunciation */\n  pron?: string\n  meaning?: HTMLString\n  example?: HTMLString\n  gif?: {\n    src: string\n    attr: string\n  }\n  tags?: string[]\n  /** who write this explanation */\n  contributor?: string\n  /** numbers of thumbs up */\n  thumbsUp?: string\n  /** numbers of thumbs down */\n  thumbsDown?: string\n}\n\ninterface thumbItem {\n  current: string\n  defid: number\n  down: number\n  up: number\n}\n\ninterface thumbRes {\n  thumbs: thumbItem[]\n}\n\ninterface thumbMapItem {\n  up: string\n  down: string\n}\n\ninterface thumbMap {\n  [defid: string]: thumbMapItem\n}\n\nexport type UrbanResult = UrbanResultItem[]\n\ntype UrbanSearchResult = DictSearchResult<UrbanResult>\n\nexport const search: SearchFunction<UrbanResult> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  const options = profile.dicts.all.urban.options\n\n  return fetchDirtyDOM(\n    'http://www.urbandictionary.com/define.php?term=' +\n      encodeURIComponent(text.replace(/\\s+/g, ' '))\n  )\n    .catch(handleNetWorkError)\n    .then(doc => handleDOM(doc, options))\n}\n\n/** get thumbs-up and thumbs-down nums  */\nasync function getThumbsNums(ids: string): Promise<thumbMap | null> {\n  const thumbsMap = {}\n\n  const result = await axios\n    .get<thumbRes>(`https://api.urbandictionary.com/v0/uncacheable`, {\n      params: {\n        ids\n      }\n    })\n    .catch(handleNetWorkError)\n\n  if (!result?.data) {\n    return null\n  }\n\n  result?.data?.thumbs?.map(t => {\n    thumbsMap[t.defid] = {\n      up: t.up,\n      down: t.down\n    }\n  })\n  return thumbsMap\n}\n\nasync function handleDOM(\n  doc: Document,\n  { resultnum }: { resultnum: number }\n): Promise<UrbanSearchResult> {\n  const result: UrbanResult = []\n  const audio: { us?: string } = {}\n\n  const defPanels = Array.from(doc.querySelectorAll('.def-panel'))\n\n  if (defPanels.length <= 0) {\n    return handleNoResult()\n  }\n\n  const defIds: string[] = []\n\n  for (let i = 0; i < defPanels.length && result.length < resultnum; i++) {\n    const defId = defPanels[i]?.getAttribute('data-defid')\n\n    defId && defIds.push(defId)\n  }\n  const thumbsMap = await getThumbsNums(defIds.join(','))\n\n  for (let i = 0; i < defPanels.length && result.length < resultnum; i++) {\n    const $panel = defPanels[i]\n    const defId = defPanels[i]?.getAttribute('data-defid') || ''\n\n    const resultItem: UrbanResultItem = { title: '' }\n\n    resultItem.title = getText($panel, '.word')\n    if (!resultItem.title) {\n      continue\n    }\n\n    const $pron = $panel.querySelector('.play-sound') as HTMLElement\n    if ($pron && $pron.dataset.urls) {\n      try {\n        const pron = JSON.parse($pron.dataset.urls)[0]\n        if (pron) {\n          resultItem.pron = pron\n          audio.us = pron\n        }\n      } catch (error) {\n        /* ignore */\n      }\n    }\n\n    resultItem.meaning = getInnerHTML(HOST, $panel, '.meaning')\n    if (/There aren't any definitions for/i.test(resultItem.meaning || '')) {\n      continue\n    }\n\n    resultItem.example = getInnerHTML(HOST, $panel, '.example')\n\n    const $gif = $panel.querySelector('.gif > img') as HTMLImageElement\n    if ($gif) {\n      const $attr = $gif.nextElementSibling\n      resultItem.gif = {\n        src: $gif.src,\n        attr: getText($attr)\n      }\n    }\n\n    const $tags = Array.from($panel.querySelectorAll('.tags a'))\n    if ($tags && $tags.length > 0) {\n      resultItem.tags = $tags.map($tag => ($tag.textContent || ' ').slice(1))\n    }\n\n    resultItem.contributor = getText($panel, '.contributor')\n    resultItem.thumbsUp = thumbsMap?.[defId]?.up\n    resultItem.thumbsDown = thumbsMap?.[defId]?.down\n\n    result.push(resultItem)\n  }\n\n  if (result.length > 0) {\n    return { result, audio }\n  } else {\n    return handleNoResult()\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/vocabulary/View.tsx",
    "content": "import React, { FC } from 'react'\nimport { VocabularyResult } from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\n\nexport const DictVocabulary: FC<ViewPorps<VocabularyResult>> = ({ result }) => (\n  <>\n    <p className=\"dictVocabulary-Short\">{result.short}</p>\n    <p className=\"dictVocabulary-Long\">{result.long}</p>\n  </>\n)\n\nexport default DictVocabulary\n"
  },
  {
    "path": "src/components/dictionaries/vocabulary/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Vocabulary.com\",\n    \"zh-CN\": \"Vocabulary.com\",\n    \"zh-TW\": \"Vocabulary.com\"\n  }\n}"
  },
  {
    "path": "src/components/dictionaries/vocabulary/_style.shadow.scss",
    "content": ".dictVocabulary-Long {\n  padding-left: 5px;\n  border-left: 1px solid #666;\n}\n"
  },
  {
    "path": "src/components/dictionaries/vocabulary/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type VocabularyConfig = DictItem\n\nexport default (): VocabularyConfig => ({\n  lang: '10000000',\n  selectionLang: {\n    english: true,\n    chinese: false,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 180,\n  selectionWC: {\n    min: 1,\n    max: 5\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/vocabulary/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  getText,\n  handleNoResult,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult\n} from '../helpers'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return `https://www.vocabulary.com/dictionary/${text}`\n}\n\nexport interface VocabularyResult {\n  short: string\n  long: string\n}\n\ntype VocabularySearchResult = DictSearchResult<VocabularyResult>\n\nexport const search: SearchFunction<VocabularyResult> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  return fetchDirtyDOM(\n    'https://www.vocabulary.com/dictionary/' +\n      encodeURIComponent(text.replace(/\\s+/g, ' '))\n  )\n    .catch(handleNetWorkError)\n    .then(handleDOM)\n}\n\nfunction handleDOM(\n  doc: Document\n): VocabularySearchResult | Promise<VocabularySearchResult> {\n  const short = getText(doc, '.short')\n  if (!short) {\n    return handleNoResult()\n  }\n\n  const long = getText(doc, '.long')\n  if (!long) {\n    return handleNoResult()\n  }\n\n  return { result: { long, short } }\n}\n"
  },
  {
    "path": "src/components/dictionaries/weblio/View.tsx",
    "content": "import React, { FC } from 'react'\nimport { WeblioResult } from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport EntryBox from '@/components/EntryBox'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictWeblio: FC<ViewPorps<WeblioResult>> = ({ result }) => (\n  <div className=\"dictWeblio-Container\">\n    {result.map(({ title, def }) => (\n      <EntryBox\n        key={title}\n        className=\"dictWeblio-Entry\"\n        title={<StrElm tag=\"span\" html={title} />}\n      >\n        <StrElm html={def} />\n      </EntryBox>\n    ))}\n  </div>\n)\n\nexport default DictWeblio\n"
  },
  {
    "path": "src/components/dictionaries/weblio/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Weblio\",\n    \"zh-CN\": \"Weblio 辞書\",\n    \"zh-TW\": \"Weblio 辞書\"\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/weblio/_style.shadow.scss",
    "content": "@import '@/_sass_shared/_reset.scss';\n@import '@/components/EntryBox/EntryBox.scss';\n\n.dictWeblio-Entry > .entryBox {\n  position: relative;\n\n  &::after {\n    content: '';\n    display: table;\n    clear: both;\n  }\n}\n\n.Wpppl table {\n  border-collapse: collapse;\n  float: left;\n  margin: 10px 0 10px 0;\n  padding: 10px 0 10px 0;\n  width: 100%;\n}\n\n.Wpmov table {\n  width: 100%;\n  padding: 10px 0 10px 0;\n  margin: 10px 0 10px 0;\n  border-collapse: collapse;\n}\n\ntable.Wpmov tr.WpmovW td {\n  padding-left: 20px;\n}\n\ntable.Wpmov tr.WpmovW td {\n  padding-left: 20px;\n}\n\n.Jmayh dl {\n  margin: 0;\n}\n\n.Jmayh .data {\n  background-color: #9ba8ca;\n  border-collapse: collapse;\n  border-spacing: -2px;\n}\n\n.Jmayh .data th {\n  background-color: #556ca5;\n  border: #9ba8ca solid 2px;\n  color: #fff;\n  font-weight: normal;\n  padding: 6px;\n}\n\n.Jmayh .data td {\n  background-color: #fff;\n  border: #9ba8ca solid 2px;\n  padding: 6px;\n}\n\n.Fkkck div {\n  padding-top: 5px;\n}\n\nh2.midashigo sub {\n  font-size: smaller;\n}\n\n.Ktiau table td {\n  padding: 5px;\n}\n\n.Ktiau table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n}\n\n.Ktiau td {\n  border: currentColor solid 1px;\n}\n\n.Ktiau th {\n  border: currentColor solid 1px;\n  text-align: left;\n}\n\n.Ktiau tr {\n  border: currentColor solid 1px;\n}\n\n.Ktsbm table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n  width: 600px;\n}\n\n.Ktsbm table td {\n  border: currentColor solid 1px;\n  padding-left: 7px;\n  text-align: left;\n}\n\n.Ktsbm table th {\n  border: currentColor solid 1px;\n  font-weight: normal;\n  padding-right: 7px;\n  text-align: left;\n}\n\n.Ktsbm table .head {\n  background: #f2f2f2;\n  font-weight: bold;\n}\n\n.Ktsbm .ktsbmC {\n  font-size: .9em;\n  width: 600px;\n}\n\n.Ktsbm .ktsbmImg {\n  margin: 0 auto;\n  width: 400px;\n}\n\n.Ktsbm .KtsbmImgL {\n  float: left;\n}\n\n.Ktsbm .KtsbmImgR {\n  float: right;\n}\n\n.Ktsbm .ktsbmI {\n  margin: 0 auto;\n  width: 600px;\n}\n\n.Ktsbm .ktsbmI ul {\n  list-style: none;\n}\n\n.Ktsbm .ktsbmI ul li {\n  float: left;\n  text-align: center;\n  width: 120px;\n}\n\n.Ktdcm br .CF {\n  clear: both;\n}\n\n.Ktdcm .KtdcmImg {\n  text-align: center;\n}\n\n.Ktdcm .KtdcmImgLeft {\n  float: left;\n  width: 48%;\n}\n\n.Ktdcm .KtdcmImgRight {\n  float: right;\n  width: 48%;\n}\n\n.Ktdcm .KtdcmImage0 {\n  margin: 0 0 15px 0;\n  text-align: center;\n}\n\n.Ktdcm .KtdcmImage1 {\n  margin: 0 0 15px 0;\n  text-align: center;\n}\n\n.Ktdcm table.border td {\n  padding: 5px;\n}\n\n.Ktdcm .bgwhite {\n  background: #fff;\n}\n\n.Ktdcm .brownDark {\n  background: #f0c200;\n}\n\n.Ktdcm .brownNormal {\n  background: #ead88c;\n}\n\n.Ktdcm .brownLight {\n  background: #f5edc6;\n}\n\n.Ktdcm .grayDark {\n  background: #cfcfcf;\n}\n\n.Ktdcm .grayNormal {\n  background: #e5e5e5;\n}\n\n.Ktdcm .grayLight {\n  background: #f2f2f2;\n}\n\n.Ktdcm ul.notice,\n.Ktdcm ul.notice li,\n.Ktdcm ul.notice ul {\n  list-style-type: none;\n  margin: 0;\n  padding: 0;\n}\n\n.Ktdcm img {\n  border: none;\n}\n\n.Ktdcm img.border {\n  border: #ccc solid 1px;\n}\n\n.Ktdcm .notice img.icon {\n  margin-left: 0;\n}\n\n.Ktdcm img.icon {\n  margin: 0 5px;\n  vertical-align: middle;\n}\n\n.Ktdcm .maincol {\n  margin-left: 15px;\n  text-align: left;\n}\n\n.Ktdcm li.non {\n  list-style-type: none;\n  padding-left: 0;\n}\n\n.Ktdcm li.full {\n  width: 100%;\n}\n\n.Ktdcm ul.fright li.full {\n  text-align: right;\n}\n\n.Ktdcm .maincol .boxArea {\n  margin-bottom: 16px;\n  padding-top: 7px;\n}\n\n.Ktdcm .maincol .boxArea .wrap {\n  padding-bottom: 8px;\n}\n\n.Ktdcm .maincol .boxArea .section {\n  padding: 0 7px;\n}\n\n.Ktdcm table.cellpt01 {\n  border-bottom: #999 solid 1px;\n  border-right: #999 solid 1px;\n  margin-bottom: 8px;\n}\n\n.Ktdcm table.cellpt01 td {\n  border-left: #999 solid 1px;\n  border-top: #999 solid 1px;\n  padding: 3px 3px;\n}\n\n.Ktdcm table.cellpt01 td.theader {\n  padding: 5px;\n}\n\n.Ktdcm table.cellpt02 {\n  border-bottom: #a1a1a1 solid 1px;\n  margin-bottom: 8px;\n}\n\n.Ktdcm table.cellpt02 td {\n  border-top: #a1a1a1 solid 1px;\n  padding: 3px 5px;\n}\n\n.Ktdcm table.layout td {\n  vertical-align: top;\n}\n\n.Ktdcm table.cell2 {\n  width: 560px;\n}\n\n.Ktdcm table.cell2 .right {\n  padding-left: 16px;\n}\n\n.Ktdcm table.cell2 .section {\n  width: 272px;\n}\n\n.Ktdcm table.cell2 table {\n  width: 263px;\n}\n\n.Ktdcm table.cell2 table .section {\n  width: 147px;\n}\n\n.Ktdcm ul.notice li {\n  margin-bottom: 5px;\n  padding-left: 16px;\n  text-indent: -12px;\n}\n\n.Sngsj .gaiji {\n  height: 1.0em;\n  vertical-align: text-bottom;\n  width: 1.0em;\n}\n\n.Sngsj td.kana {\n  color: var(--color-font-grey);\n  font-size: 90%;\n}\n\n.Sngsj td.status {\n  color: var(--color-font-grey);\n}\n\n.Sngsj td.body {\n  line-height: 1.3em;\n  padding-bottom: 10px;\n  padding-top: 20px;\n}\n\n.Sngsj p.notice {\n  color: #333080;\n  font-size: 95%;\n  padding: 10px 20px 10px 30px;\n}\n\n.Otnet .OtnetBGImgDiv {\n  background-repeat: no-repeat;\n}\n\n.Otnet .OtnetRed {\n  border-bottom: #ccc solid 1px;\n  border-left: #f00 solid 10px;\n  border-right: #ccc solid 0;\n  margin: 12px;\n  padding: 1px 5px;\n}\n\n.Otnet .OtnetBlue {\n  border-bottom: #ccc solid 1px;\n  border-left: #00f solid 10px;\n  border-right: #ccc solid 0;\n  margin: 12px;\n  padding: 1px 5px;\n}\n\n.Fkkyr div.box-photo {\n  float: left;\n  width: 380px;\n}\n\n.Fkkyr dl.data1 {\n  float: left;\n  width: 250px;\n}\n\n.Fkkyr br.cr {\n  clear: both;\n}\n\n.Fkkyr div.box-model {\n  float: left;\n  width: 380px;\n}\n\n.Fkkyr .box-data {\n  float: left;\n}\n\n.Fkkyr dl.data2 {\n  width: 250px;\n}\n\n.Fkkyr #sitemap {\n  clear: both;\n}\n\n.Fkkyr .left_column {\n  padding: 15px 0 0 0;\n}\n\n.Shkli {\n  font-size: 12px;\n  border-collapse: collapse;\n  margin: 0 0 20px 0;\n  width: 100%;\n}\n\n.Shkli td {\n  border: 1px solid #696969;\n  text-align: center;\n}\n\n.Shkli tr.ShkliHV td {\n  font-size: 12px;\n  background-color: #d9d9f3;\n  font-family: \"ＭＳ ゴシック\";\n  text-align: center;\n  padding: 3px 3px 0 3px;\n  height: 100px;\n  vertical-align: top;\n}\n\n.Shkli tr.ShkliHV td.ShkliHL {\n  text-align: left;\n  vertical-align: middle;\n  background-color: #fff;\n}\n\n.Sunos table {\n  line-height: 1.5;\n}\n\ndiv.Zndzk {\n  width: 100%;\n}\n\n.Otnee {\n  text-align: center;\n}\n\n.Otnee table {\n  line-height: 1.5;\n}\n\n.Chgth {\n  text-align: center;\n  padding-top: 10px;\n  padding-bottom: 20px;\n}\n\n.Chgth table {\n  margin: 0 auto;\n}\n\n.Ednpl {\n  padding-top: 10px;\n  padding-bottom: 20px;\n}\n\n.Ednyr {\n  padding-top: 10px;\n  padding-bottom: 20px;\n}\n\n.Dowcp table {\n  border-collapse: collapse;\n}\n\n.Dowcp td {\n  border-bottom: 1px #cdcdcd dotted;\n  padding: 5px;\n}\n\n.Fjshi {\n  border-collapse: collapse;\n  width: 95%;\n}\n\n.Fjshi td {\n  border-style: dashed;\n  border-width: 0 0 1px 0;\n  border-color: #cdcdcd;\n  padding: 8px 0 8px 0;\n}\n\n.Sunco table {\n  width: 95%;\n}\n\n.Sunco td.white_txt {\n  width: 100%;\n}\n\n.Ysztk {\n  border-collapse: collapse;\n}\n\n.Ysztk td {\n  padding: 0 0 5px 0;\n}\n\n.Ysztk table {\n  border-collapse: collapse;\n}\n\n.Ysztk table td {\n  padding: 0;\n}\n\n.Suncy {\n  padding: 10px 5px 20px 0;\n}\n\n.Chkgc table table table {\n  border-collapse: collapse;\n  border: 1px currentColor solid;\n}\n\n.Chkgc table table table td {\n  padding: 5px;\n  border: 1px currentColor solid;\n}\n\n.Grnry .grnryInfo {\n  background-color: #f5f5f5;\n  border: #808080 solid 1px;\n  margin-top: 20px;\n  padding: 10px;\n}\n\n.Smkbj {\n  border-collapse: collapse;\n  border: 1px #696969 solid;\n  width: 90%;\n}\n\n.Smkbj td {\n  border: 1px #696969 solid;\n  padding: 3px;\n}\n\nh2.midashigo sub {\n  font-size: smaller;\n}\n\nh2.midashigo sub {\n  font-size: smaller;\n}\n\nh2.midashigo sub {\n  font-size: smaller;\n}\n\n.Glfyg div {\n  padding: 0 0 15px 15px;\n}\n\n.Glfyg img {\n  float: right;\n  vertical-align: top;\n}\n\n.Glfyg img.GlfygIc {\n  float: none;\n  vertical-align: baseline;\n}\n\n.Uodbj {\n  border-collapse: collapse;\n  border: 1px #696969 solid;\n  width: 90%;\n}\n\n.Uodbj td {\n  border: 1px #696969 solid;\n  padding: 5px;\n}\n\n.Mjkbr {\n  border-collapse: collapse;\n  border: 1px #696969 solid;\n}\n\n.Mjkbr td {\n  border: 1px #696969 solid;\n  padding: 5px;\n}\n\n.Damjt .damjtInfo {\n  background-color: #f5f5f5;\n  border: #808080 solid 1px;\n  margin-top: 20px;\n  padding: 10px;\n}\n\n.Shkgz .ShkgzBT {\n  display: block;\n}\n\n.Shkgz .ShkgzBB {\n  display: block;\n  margin-top: 1.1em;\n}\n\n.Knkyy sub {\n  font-size: smaller;\n}\n\n.Skiis td {\n  vertical-align: top;\n}\n\n.Skiis .skiisP {\n  margin-left: 13px;\n}\n\n.hkdhj div {\n  padding-top: 5px;\n}\n\n.hkdhj div b {\n  color: #f30;\n}\n\n.Tkmhg {\n  border-collapse: collapse;\n  width: 90%;\n}\n\n.Tkmhg td {\n  border: 1px #696969 solid;\n  padding: 3px;\n}\n\ntd.midashigo {\n  color: #4f519b;\n  font-weight: bold;\n  padding: 10px 5px 30px 2px;\n}\n\n.Kmkrz table {\n  border-collapse: collapse;\n  border-color: currentColor;\n  border-width: 1px;\n  border-style: solid;\n  margin: 0 auto;\n}\n\n.Kmkrz table td {\n  border-color: currentColor;\n  border-width: 1px;\n  border-style: solid;\n  padding: 5px;\n}\n\n.Kmkrz .img-shrink {\n  width: 100%;\n}\n\n.Tkzkn table td {\n  padding: 8px;\n}\n\n.Tkzkn table table {\n  border-width: 0;\n}\n\n.Dkijt .path {\n  font-size: .75em;\n  color: #555;\n  text-align: left;\n  margin-left: 0;\n  margin-right: auto;\n  margin-bottom: 0;\n}\n\n.Dkijt .path-a {\n  font-size: .75em;\n  color: #555;\n  text-align: left;\n  margin-left: auto;\n  margin-right: auto;\n  margin-bottom: 0;\n}\n\n.Dkijt .sp {\n  width: 7em;\n}\n\n.Dkijt p.head {\n  text-align: left;\n  margin: 0;\n  font-size: .625em;\n  font-family: verdana;\n  font-weight: bold;\n  color: #5e8eab;\n  background-color: WhiteSmoke;\n  padding-bottom: 5px;\n  padding-top: 5px;\n  padding-left: 1em;\n  width: 768px;\n  border-bottom: solid 4px #c5e1ed;\n}\n\n.Dkijt p.head img {\n  margin-right: 1em;\n  vertical-align: middle;\n  border: 0;\n}\n\n.Dkijt .kao_pic {\n  float: right;\n  margin-left: 1em;\n  font-size: .85em;\n  color: #5e8eab;\n  width: 150px;\n  text-align: center;\n  line-height: 1em;\n  margin-top: -10px;\n}\n\n.Dkijt .pict_r {\n  float: left;\n  margin-left: 0;\n  margin-right: 1em;\n  font-size: .85em;\n  color: #5e8eab;\n  width: 150px;\n  text-align: center;\n  line-height: 1em;\n}\n\n.Dkijt .pict_r_s {\n  float: left;\n  margin-left: 0;\n  margin-right: 1em;\n  font-size: .85em;\n  color: #5e8eab;\n  width: 100px;\n  text-align: center;\n  line-height: 1em;\n}\n\n.Dkijt .pict_c {\n  text-align: center;\n  margin-top: .2em;\n  margin-bottom: .2em;\n  font-size: .9em;\n  color: #5e8eab;\n  line-height: 1em;\n}\n\n.Dkijt h2 {\n  font-size: 1em;\n  text-align: left;\n  border-left: solid 18px #5e8eab;\n  padding-left: 1em;\n  margin-bottom: 1em;\n  margin-left: .5em;\n}\n\n.Dkijt h3 {\n  font-size: 1em;\n  text-align: left;\n  border-left: solid 18px #5e8eab;\n  padding-left: 1em;\n  margin-top: 1.5em;\n  margin-bottom: 1em;\n  margin-left: .5em;\n}\n\n.Dkijt .mae {\n  margin-bottom: 1em;\n  margin-left: 2em;\n  margin-right: 1em;\n}\n\n.Dkijt p.maegaki {\n  letter-spacing: .1em;\n  color: #323232;\n  line-height: .3em;\n}\n\n.Dkijt p.a {\n  text-indent: .875em;\n  letter-spacing: .09em;\n}\n\n.Dkijt p.honbun {\n  margin-bottom: .75em;\n  letter-spacing: .09em;\n}\n\n.Dkijt .section {\n  text-align: left;\n  font-size: .875em;\n  line-height: 1.5em;\n  color: #555;\n  margin-top: 0;\n  margin-bottom: 1em;\n  margin-left: .7em;\n  margin-right: .7em;\n}\n\n.Dkijt .section a {\n  color: blue;\n  font-weight: bold;\n}\n\n.Dkijt .contents {\n  width: 488px;\n  margin-left: 0;\n  margin-right: 0;\n  position: absolute;\n  left: 145px;\n  top: 70px;\n  border-left: solid 1px #5e8eab;\n  border-right: solid 1px #5e8eab;\n}\n\n.Dkijt ul em {\n  color: #555;\n  font-style: normal;\n  font-weight: bold;\n  margin-left: .2em;\n  margin-right: .2em;\n}\n\n.Dkijt .sidebar {\n  position: absolute;\n  left: 640px;\n  top: 80px;\n  display: block;\n  width: 136px;\n  border-left: solid 1px #5e8eab;\n  border-right: solid 1px #5e8eab;\n  border-bottom: solid 1px #5e8eab;\n  background-color: WhiteSmoke;\n  text-align: left;\n}\n\n.Dkijt p.midasi {\n  line-height: 2em;\n  background-color: Gainsboro;\n  font-size: .85em;\n  text-align: left;\n  text-indent: .6em;\n  display: block;\n  width: 136px;\n  border-top: solid 1px #5e8eab;\n  border-bottom: solid 1px #5e8eab;\n  margin: 0;\n  color: #555;\n}\n\n.Dkijt .kanren {\n  text-indent: .75em;\n}\n\n.Dkijt .sidebar a {\n  display: block;\n  white-space: nowrap;\n  line-height: 1.5em;\n  font-size: .75em;\n  text-decoration: none;\n}\n\n.Dkijt .sidebar a:hover {\n  color: Red;\n  background-color: Thistle;\n}\n\n.Dkijt .sidebar span {\n  display: none;\n}\n\n.Dkijt p.side {\n  font-size: .85em;\n  line-height: 1.5em;\n}\n\n.Kkgnj p {\n  margin: 0 0 1.33em 0;\n}\n\nh2.midashigo rt {\n  font-size: .5em;\n}\n\nh2.midashigo rp {\n  font-size: .5em;\n}\n\n.Mntyg p {\n  margin: 0 0 1.33em 0;\n}\n\n.Dchkm p {\n  margin: 0 0 1.33em 0;\n}\n\n.Igokh p {\n  margin: 0 0 1.33em 0;\n}\n\n.Krdjh p {\n  margin: 0 0 1.33em 0;\n}\n\n.Kaigo p {\n  margin: 0 0 1.33em 0;\n}\n\n.Bdygs table {\n  width: 90%;\n}\n\n.Hktbn table {\n  border-collapse: collapse;\n  width: 90%;\n}\n\n.Hktbn table td {\n  border: 1px solid #696969;\n}\n\n.Mngtr p {\n  margin: 0 0 1.33em 0;\n}\n\n.Hnddb table {\n  border: 1px solid #696969;\n}\n\n.Nhkns p {\n  margin: 0 0 1.33em 0;\n}\n\n.Nhkns img {\n  margin: auto 0;\n}\n\n.Ykich td {\n  border: 1px #696969 solid;\n  padding: 3px;\n}\n\n.Recju .recjuL {\n  float: left;\n  margin: 6px;\n  padding: 0;\n  width: 400px;\n}\n\n.Recju .recjuR {\n  float: right;\n  margin: 6px;\n  padding: 0;\n  width: 220px;\n}\n\n.Rkdsr table {\n  border-collapse: collapse;\n  width: 45%;\n}\n\n.Bshjj {\n  text-align: left;\n}\n\n.Bshjj p {\n  margin: 0 0 1.33em 0;\n}\n\n.Bshjj h3 {\n  margin: 1.33em 0;\n}\n\n.Mtsbs .bkspecgray {\n  background-color: #d2d2d2;\n}\n\n.Mtsbs .bkspecgray2 {\n  background-color: #e6e6e6;\n}\n\n.Mtsbs .bkspecgray3 {\n  background-color: var(--color-font-grey);\n}\n\n.Mtsbs .bkspecpink {\n  background-color: #fbdee7;\n}\n\n.Mtsbs .bkwhite {\n  background-color: #fff;\n}\n\n.Mtsbs .notes_mainArea {\n  margin-top: 10px;\n}\n\n.Mtsbs table.spec {\n  border-right: #666 1px solid;\n  border-bottom: #666 1px solid;\n  border-collapse: collapse;\n  clear: both;\n  padding: 0;\n  width: 100%;\n}\n\n.Mtsbs table.spec .spline th {\n  padding: 0 5px;\n}\n\n.Mtsbs table.spec .spline td {\n  padding: 0 5px;\n}\n\n.Mtsbs table.spec th {\n  border-bottom: #666 1px solid;\n  border-left: #666 1px solid;\n  border-top: #666 1px solid;\n  font-weight: normal;\n  line-height: normal;\n  padding: 5px;\n  text-align: center;\n}\n\n.Mtsbs table.spec td {\n  border: solid 1px #666;\n  font-weight: normal;\n  line-height: normal;\n  padding: 5px;\n  text-align: center;\n}\n\n.Mtsbs table.spec tr.mainheader th {\n  background-color: #d2d2d2;\n  font-weight: bold;\n}\n\n.Mtsbs table.spec tr.mainheader th.basic {\n  background-color: #E8F6D9;\n}\n\n.Mtsbs table.spec table td {\n  padding: 0;\n  border: none;\n}\n\n.Mtsbs table.spec .tdleft {\n  text-align: left;\n}\n\n.Mtsbs table.spec .tdright {\n  text-align: right;\n}\n\n.Mtsbs table.spec td.tdleft_nb {\n  text-align: left;\n  border-right-style: none;\n}\n\n.Mtsbs table.spec td.tdright_nb {\n  text-align: right;\n  border-left-style: none;\n}\n\n.Mtsbs .carmain_font80 {\n  font-size: .8em;\n}\n\n.Mtsbs .carmain_font70 {\n  font-size: .7em;\n}\n\n.Mtsbs td.tdleft {\n  text-align: left;\n}\n\n.Mtsbs td.bkspecRE {\n  background-color: #deebde;\n}\n\n.Mtsbs th.tdleft {\n  text-align: left;\n}\n\n.Koeki table {\n  border-collapse: collapse;\n  width: 90%;\n  border: 1px solid #696969;\n}\n\n.Koeki td {\n  border: 1px solid #696969;\n  padding: 3px;\n}\n\n.Koeki table table {\n  border: 0;\n}\n\n.Koeki table table td {\n  border: 0;\n}\n\n.Koeki .koekicellL {\n  background-color: #ddd;\n}\n\n.Koeki .koekicellR {\n  padding-left: 5px;\n}\n\n.Phpyg p {\n  margin: 0 0 1.33em 0;\n}\n\n.Phpyg h3 {\n  margin: 1.33em 0 0 0;\n}\n\n.Phpyg ul {\n  margin: 0 0 0 20px;\n  padding: 0 0 0 20px;\n}\n\n.Tnshk p {\n  margin: 0 0 1.33em 0;\n}\n\n.Azttu table {\n  border-collapse: collapse;\n  border: 1px solid #696969;\n  width: 90%;\n}\n\n.Azttu td {\n  border: 1px solid #696969;\n}\n\n.Cesih table {\n  border-collapse: collapse;\n  border: 1px solid #696969;\n  width: 90%;\n}\n\n.Cesih th {\n  border: 1px solid #696969;\n  padding: 3px;\n}\n\n.Cesih td {\n  border: 1px solid #696969;\n  padding: 3px;\n}\n\n.Cesih .Cesihdc {\n  width: 600px;\n}\n\n.Cesih .Cesihdc img {\n  float: right;\n  margin: 0 0 10px 15px;\n}\n\n.Nomen img {\n  float: left;\n  vertical-align: top;\n  margin-right: 10px;\n}\n\n.Gnshk b {\n  font-size: 12px;\n  font-weight: normal;\n}\n\n.Gnshk .ChartTitle {\n  margin: 0 10px 0 0;\n}\n\n.Gnshk .deep {\n  color: #fff;\n  padding: 0 0 0 3px;\n  width: 500px;\n}\n\n.Gnshk .deep div {\n  color: #fff;\n}\n\n.Gnshk .pale {\n  color: currentColor;\n  padding: 0 0 0 3px;\n  width: 500px;\n}\n\n.Gnshk .pale div {\n  color: currentColor;\n}\n\n.Gnshk a.colorChart {\n  text-decoration: none;\n}\n\n.Gnshk a.colorChart:hover {\n  border-left: #fff solid 2px;\n  border-right: #fff solid 2px;\n}\n\n.Ssndh table {\n  vertical-align: top;\n}\n\n.Btnkb td {\n  vertical-align: top;\n  padding: 2px;\n}\n\n.Rkjsh table {\n  vertical-align: top;\n  width: 100%;\n}\n\n.Rkjsh table .rkjshI {\n  vertical-align: top;\n}\n\n.Rkjsh table .rkjshI,\n.Rkjsh table .rkjshI a {\n  white-space: nowrap;\n}\n\n.Kkjsh table {\n  border-top: 1px solid #999;\n  border-left: 1px solid #999;\n}\n\n.Kkjsh table tr td {\n  padding: 2px 4px;\n  border-bottom: 1px solid #999;\n  border-right: 1px solid #999;\n}\n\n.Kkjsh table tr td.center {\n  text-align: center;\n}\n\n.Kkjsh table tr td.right {\n  text-align: right;\n}\n\n.Kkjsh table tr td.cream {\n  background-color: #FDFCEC;\n}\n\n.Kkjsh table tr td.gray {\n  background-color: #EDEDED;\n}\n\n.Kkjsh table tr th {\n  padding: 2px 4px;\n  border-bottom: 1px solid #999;\n  border-right: 1px solid #999;\n  background-color: #F2F2F2;\n  color: currentColor;\n  text-align: center;\n}\n\n.Tfnsr table {\n  font-size: 90%;\n}\n\n.Tfnsr div {\n  color: #f93;\n  font-size: 110%;\n  font-weight: bold;\n}\n\n.Gndhh td {\n  font-size: 100%;\n}\n\n.Mzdmt spec table {\n  border-top: #a1a1a1 solid 1px;\n  text-align: center;\n  width: 542px;\n}\n\n.Mzdmt spec table td {\n  vertical-align: middle;\n}\n\n.Mzdmt .bg01 {\n  background-color: #f1f1f1;\n}\n\n.Mzdmt .bg02 {\n  background-color: #fff;\n}\n\n.Mzdmt .cell_center {\n  border: #a1a1a1 solid;\n  border-width: 0 1px 1px 0;\n  text-align: center;\n}\n\n.Mzdmt .cell_center_no {\n  border-bottom: #a1a1a1 solid 1px;\n  height: 22px;\n  text-align: center;\n}\n\n.Mzdmt .cell_center_left {\n  border-right: #a1a1a1 solid 1px;\n  text-align: center;\n}\n\n.Mzdmt .cell_left_no {\n  border-bottom: #a1a1a1 solid 1px;\n  height: 22px;\n  text-align: left;\n}\n\n.Mzdmt .cell_left {\n  border: #a1a1a1 solid;\n  border-width: 0 1px 1px 0;\n  height: 22px;\n  text-align: left;\n}\n\n.Mzdmt .cell_w2 {\n  width: 80px;\n}\n\n.Mzdmt .caution_list dt {\n  float: left;\n}\n\n.Mzdmt .caution_list dd {\n  margin: 0;\n  padding: 0 0 0 13px;\n  vertical-align: top;\n}\n\n.Mzdmt .cp1 {\n  color: currentColor;\n  font-size: 77%;\n  margin: 0;\n}\n\n.Mzdmt td.cp1 {\n  text-align: left;\n}\n\n.Mzdmt .cp2 {\n  color: #0655d8;\n  font-size: 77%;\n  margin: 0;\n}\n\n.Mzdmt td.cp2 {\n  vertical-align: top;\n}\n\n.Mzdmt .cp3 {\n  color: #c00;\n  font-size: 77%;\n  margin: 0;\n}\n\n.Mzdmt .cp4 {\n  color: #444;\n  font-size: 77%;\n}\n\n.Mzdmt .bd5 {\n  color: #109d0d;\n}\n\n.Mzdmt td.bd5 {\n  vertical-align: top;\n}\n\n.Kaike p {\n  margin: 0 0 1.33em 0;\n}\n\n.Dhtsu .DhtsuT {\n  border: #696969 solid 1px;\n  border-collapse: collapse;\n}\n\n.Dhtsu .DhtsuT td {\n  border: #696969 solid 1px;\n}\n\n.Dhtsu .DhtsuT td table td {\n  border: 0;\n}\n\n.Hyazi img {\n  border: 0;\n}\n\n.Tkgyg pre {\n  font-family: \"ＭＳ ゴシック\";\n}\n\n.Nnkyk img {\n  border: 0;\n}\n\n.Nnkyk a {\n  text-decoration: none;\n}\n\n.Npohd h3 {\n  border-left: #696969 solid 5px;\n  margin: 10px 0 0 0;\n  padding: 0 10px;\n}\n\n.Npohd table.npohdW {\n  max-width: 625px;\n  width: 100%;\n}\n\n.Npohd .npohdW * {\n  margin: 0;\n  padding: 0;\n}\n\n.Npohd .npohdW pre {\n  white-space: pre-wrap;\n  word-wrap: break-word;\n}\n\n.Npohd .npohdW {\n  border: 1px solid #999;\n  border-collapse: collapse;\n  table-layout: fixed;\n}\n\n.Npohd .npohdW td {\n  border: 1px solid #999;\n}\n\n.Npohd .npohdW table.npoHdSubTB td {\n  border: medium none;\n}\n\n.Npohd .npohdW td.npohdBs {\n  border-style: none;\n}\n\n.Npohd .npohdW td.npohdLS {\n  width: 21%;\n}\n\n.Npohd .npohdW td.npohdRS {\n  width: 79%;\n}\n\n.Npohd .npohdW td {\n  letter-spacing: -2px;\n}\n\n.Npohd table.npoHdMainTB {\n  width: 600px;\n}\n\n.Msdnc div.border {\n  border: #999 solid;\n  border-width: 1px 0 0 0;\n}\n\n.Msdnc p {\n  margin: 0 0 10px 0;\n  padding: 0;\n}\n\n.Msdnc div.section {\n  padding-left: 20px;\n}\n\n.Msdnc div.code {\n  background-color: #DDD;\n  border-bottom: #fff solid 10px;\n  margin: 0;\n  padding: 0;\n}\n\n.Msdnc table {\n  border-collapse: collapse;\n  margin: 10px 0 10px 0;\n  width: 100%;\n}\n\n.Msdnc table p {\n  margin: 0;\n  padding: 0;\n}\n\n.Msdnc table th,\n.Msdnc table td {\n  font-size: 3mm;\n  padding: 5px;\n  text-align: left;\n}\n\n.Msdnc table th {\n  background: #ccc;\n  vertical-align: bottom;\n}\n\n.Msdnc .code {\n  display: block;\n  margin: 0 10px 0 0;\n  max-width: 100%;\n  padding: 5px 5px 5px 5px;\n}\n\n.Msdnc pre {\n  background: #ddd;\n  margin: 0 5px 0 0;\n  padding-top: 0;\n  padding-bottom: 0;\n  word-break: break-all;\n  word-wrap: break-word;\n}\n\n.Msdnc ul {\n  margin: 0 0 0 20px;\n  padding: 0;\n}\n\n.Msdnc ul ul {\n  padding: 0;\n  margin-top: 4px;\n}\n\n.Msdnc ul ul li {\n  line-height: 1.2em;\n}\n\n.Msdnc ul li ul {\n  margin-bottom: 5px;\n}\n\n.Msdnc ul li ul li {\n  margin-bottom: 5px;\n  line-height: 140%;\n}\n\n.Msdnc li p {\n  margin: 0;\n  padding: 0;\n}\n\n.Msdnc li ul {\n  margin-left: -40px;\n  padding: 0;\n}\n\n.Msdnc li ul li p {\n  margin-left: 0;\n}\n\n.Msdnc li ul li {\n  line-height: inherit;\n  margin-left: 40px;\n  padding-left: 10px;\n}\n\n.Msdnc ol {\n  margin: 0;\n  padding: 0;\n}\n\n.Msdnc ol li {\n  margin: 0 0 5 40;\n  line-height: 140%;\n}\n\n.Msdnc table,\n.Msdnc td,\n.Msdnc th {\n  border: #DDD solid 1px;\n}\n\n.Sdkys dd {\n  margin: 0;\n}\n\n.Otrks h4 {\n  border-bottom: #9da8b0 solid 1px;\n  margin-bottom: 4px;\n  margin-top: 0;\n  padding-bottom: 0;\n}\n\n.Otrks #jiten-honbun {\n  float: left;\n  line-height: 1.4em;\n  margin-left: 3px;\n  margin-right: 20px;\n  width: 400px;\n}\n\n.Otrks #jiten-access {\n  margin-bottom: 20px;\n}\n\n.Otrks #jiten-media {\n  float: left;\n  margin-bottom: 10px;\n  width: 216px;\n}\n\n.Otrks #jiten-movieplayer {\n  border-top: #6c93b0 solid 1px;\n  clear: both;\n  padding-top: 10px;\n  text-align: right;\n  width: 660px;\n}\n\n.Otrks .jiten-photo {\n  background-color: #fff;\n  border: #999 solid 0;\n  padding: 8px;\n  text-align: center;\n  width: 200px;\n}\n\n.Otrks .jiten-hosoku {\n  margin-bottom: 8px;\n  margin-top: 6px;\n}\n\n.Otrks .jiten-movie-waku a {\n  background: url(https://weblio.hs.llnwd.net/e7/img/OtsuRekishiYogojitenImg/btn_moviestart.png) no-repeat;\n  display: block;\n  height: 30px;\n  margin-left: 9px;\n  text-indent: -10000px;\n  width: 120px;\n}\n\n.Triph .data table {\n  color: currentColor;\n  width: 100%;\n}\n\n.Triph .data caption {\n  background: #94b7df;\n  border-right: #fff solid 1px;\n  border-top: #fff solid 1px;\n  color: #fff;\n  font-weight: bold;\n  padding: 2px 17px;\n}\n\n.Triph .data th,\n.data td {\n  border-right: #fff solid 1px;\n  border-top: #fff solid 1px;\n  padding: 2px 17px;\n  background: #eff4fa;\n}\n\n.Triph .data th {\n  background: #dfe9f5;\n  font-weight: normal;\n  vertical-align: top;\n  width: 106px;\n}\n\n.Triph #colourChoices {\n  background: #f1f1f1;\n  padding: 2px 5px;\n  width: 50%;\n}\n\n.Triph #rollOver {\n  color: #999;\n}\n\n.Triph #colourChanger img {\n  border: #b8b8b8 solid 1px;\n}\n\n.Triph #colourChanger span {\n  display: none;\n}\n\n.Sndib table {\n  border: #696969 solid 1px;\n  border-collapse: collapse;\n  width: 90%;\n}\n\n.Sndib table th {\n  background-color: #f5f5f5;\n  border: #696969 solid 1px;\n  font-weight: bold;\n  padding: 3px;\n}\n\n.Sndib table td {\n  border: #696969 solid 1px;\n  padding: 3px;\n}\n\n.Aprla table {\n  border-collapse: collapse;\n}\n\n.Kaiso td {\n  padding: 2px;\n}\n\n.Cntkj table th {\n  text-align: left;\n  white-space: nowrap;\n}\n\n.Cntkj iframe {\n  border: #b6b6b6 solid 1px;\n  height: 250px;\n  margin-top: 15px;\n  width: 100%;\n}\n\n.Cntkj .com_prof {\n  float: left;\n  width: 55%;\n}\n\n.Cntkj .description {\n  float: right;\n  width: 43%;\n}\n\n.Nnkdt img {\n  margin-top: 10px;\n  width: 400px;\n}\n\n.Snntd table {\n  border: #696969 solid 1px;\n  border-collapse: collapse;\n}\n\n.Snntd td {\n  border: #696969 solid 1px;\n}\n\n.Snntd img {\n  margin-bottom: 15px;\n}\n\n.Ezndt .EzndtTd {\n  width: 134px;\n}\n\n.Ezndt img {\n  margin: 10px 0 10px 0;\n}\n\n.Nyugy p {\n  margin: 0 0 1.33em 0;\n}\n\n.Srjtn table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n}\n\n.Srjtn table td {\n  border: currentColor solid 1px;\n  padding: 1px 6px 1px 6px;\n}\n\n.Yesrs table {\n  width: 100%;\n}\n\n.Yesrs table .YesrsTd {\n  color: #fff;\n}\n\n.Hyndi table {\n  width: 100%;\n}\n\n.Hyndi .spec-table {\n  border: #b8b9d2 solid;\n  border-width: 1px 0 0 1px;\n}\n\n.Hyndi .spec-table th,\n.spec-table td {\n  border: #b8b9d2 solid;\n  border-width: 0 1px 1px 0;\n  padding: 10px;\n}\n\n.Hyndi .spec-table th.row {\n  text-align: center;\n}\n\n.Hyndi .spec-table th {\n  background-color: #e5e5ef;\n  font-weight: normal;\n  text-align: left;\n}\n\n.Hyndi .spec-table th.violet {\n  background-color: #7f7fb2;\n  color: #fff;\n  font-weight: bold;\n}\n\n.Hyndi .spec-table th.center {\n  text-align: center;\n}\n\n.Hyndi .spec-table td {\n  text-align: center;\n  vertical-align: middle;\n}\n\n.Hyndi .spec-table td.left {\n  text-align: left;\n}\n\n.Hyndi .spec-table td.remarks {\n  text-align: left;\n  vertical-align: text-top;\n}\n\n.Hyndi .spec-table td.remarks ul {\n  line-height: 1.4em;\n  list-style: square;\n  margin: 0 0 0 1.0em;\n  padding: 0;\n}\n\n.Hyndi .spec-table-foot .r-mark {\n  color: #f00;\n}\n\n.Rnult td {\n  color: currentColor;\n  font-size: .9em;\n  line-height: 1.2em;\n}\n\n.Rnult .RnultT {\n  border: #696969 solid 1px;\n  border-collapse: collapse;\n}\n\n.Rnult .RnultT td {\n  border: #696969 solid 1px;\n}\n\n.Ukybz table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n}\n\n.Ukybz table td {\n  border: currentColor solid 1px;\n  padding: 3px;\n}\n\n.Abrms table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n}\n\n.Abrms table td {\n  border: currentColor solid 1px;\n  padding: 3px;\n}\n\n.Osaka table {\n  border: #696969 solid 1px;\n  border-collapse: collapse;\n  width: 90%;\n}\n\n.Osaka table th {\n  background-color: #f5f5f5;\n  border: #696969 solid 1px;\n  font-weight: bold;\n  padding: 3px;\n}\n\n.Osaka table th.OsakaL {\n  width: 20%;\n}\n\n.Osaka table th.OsakaC {\n  width: 30%;\n}\n\n.Osaka table th.OsakaR {\n  width: 50%;\n}\n\n.Osaka table td {\n  border: #696969 solid 1px;\n  padding: 3px;\n}\n\n.Osaka table td.OsakaL {\n  width: 20%;\n}\n\n.Osaka table td.OsakaC {\n  width: 30%;\n}\n\n.Osaka table td.OsakaR {\n  width: 50%;\n}\n\n.Hndmr .autoline-up-table-grade {\n  border: currentColor solid;\n  border-width: 1px 1px 1px 0;\n  font-size: .9em;\n}\n\n.Hndmr .autoline-up-table-grade td {\n  border-left: currentColor solid 1px;\n  border-top: #505050 solid 1px;\n}\n\n.Hndmr .autoline-up-table-grade td.element {\n  border-left: none;\n  line-height: 1.2em;\n  padding: 3px 0 3px 2px;\n}\n\n.Hndmr .autoline-up-table-grade td.category {\n  border-left: none;\n  font-size: .9em;\n  padding: 0 3px 0 3px;\n  text-align: center;\n}\n\n.Hndmr .autoline-up-table-grade td.std {\n  padding: 3px 0 3px 0;\n  text-align: center;\n}\n\n.Hndmr .autoline-up-table-grade td.fuel {\n  background-color: #fddaec;\n  padding: 3px 0 3px 0;\n  text-align: center;\n}\n\n.Hndmr .autoline-up-table-grade td.division-rt {\n  border-left: currentColor solid 1px;\n}\n\n.Hndmr .autoline-up-table-grade td .star {\n  color: #008837;\n}\n\n.Hndmr .autoline-up-table-grade td.layout-bottom-border {\n  border-bottom: currentColor solid 1px;\n}\n\n.Hndmr table#webcatalogue-table td {\n  color: var(--color-font-grey);\n}\n\n.Hndmr table#webcatalogue-table img {\n  border: none;\n}\n\n.Hndmr table#webcatalogue-table p {\n  margin: 0;\n  padding: 0;\n}\n\n.Hndmr table#webcatalogue-table p.leadcopy {\n  color: currentColor;\n  font-size: .9em;\n  font-weight: bold;\n  line-height: 18px;\n}\n\n.Hndmr table#webcatalogue-table p.leadcopy2 {\n  font-size: .9em;\n  font-weight: bold;\n  line-height: 21px;\n}\n\n.Hndmr table#webcatalogue-table p.text {\n  font-size: .9em;\n  line-height: 16px;\n}\n\n.Hndmr table#webcatalogue-table p.caution {\n  color: #888;\n  font-size: .9em;\n  line-height: 12px;\n  margin-top: 3px;\n}\n\n.Hndmr table#webcatalogue-table span.typebetsu {\n  font-size: .9em;\n  font-weight: normal;\n}\n\n.Hndmr table#webcatalogue-table p.concepttext {\n  color: #fff;\n  line-height: 18px;\n  margin: 0 15px 10px 15px;\n}\n\n.Hndmr table#webcatalogue-table strong.v6 {\n  color: #003f98;\n}\n\n.Hndmr table#webcatalogue-table p.safe-midashi {\n  background-color: currentColor;\n  color: #fff;\n  font-size: .9em;\n  font-weight: bold;\n  line-height: 18px;\n  padding: 3px 5px 3px 5px;\n}\n\n.Hndmr table#webcatalogue-table p.realworldtext {\n  color: #51318f;\n}\n\n.Hndmr table#webcatalogue-table span.co2 {\n  font-size: .9em;\n}\n\n.Hndmr table#webcatalogue-table p.texthyoujimark {\n  font-size: .9em;\n  line-height: 16px;\n}\n\n.Hndmr table#webcatalogue-table #env-data {\n  font-size: .9em;\n}\n\n.Hndmr table#webcatalogue-table #env-data td.tabletext {\n  padding: 2px;\n}\n\n.Hndmr table#webcatalogue-table strong.price {\n  font-size: .9em;\n}\n\n.Hndmr table#webcatalogue-table p.caution_vg {\n  margin-top: 7px;\n}\n\n.Hndmr table#webcatalogue-table p.navi-midashi {\n  background-color: #1c1f7a;\n  color: #fff;\n  font-size: .9em;\n  font-weight: bold;\n  line-height: 18px;\n  padding: 3px 5px 3px 5px;\n}\n\n.Hndmr table#webcatalogue-table .note {\n  color: #1c1f7a;\n}\n\n.Hndmr table#webcatalogue-table span.komidashi {\n  color: #006965;\n}\n\n.Hndmr .autoline-up-table-eqp {\n  border: currentColor solid;\n  border-width: 0 1px 1px 0;\n  font-size: .9em;\n}\n\n.Hndmr .autoline-up-table-eqp td {\n  border-left: currentColor solid 1px;\n  border-top: #505050 solid 1px;\n}\n\n.Hndmr .autoline-up-table-eqp td.top {\n  border-top: none;\n}\n\n.Hndmr .autoline-up-table-eqp td.none {\n  border-left: none;\n  border-top: none;\n}\n\n.Hndmr .autoline-up-table-eqp td.element {\n  border-left: none;\n  line-height: 130%;\n  padding: 6px 0 3px 2px;\n}\n\n.Hndmr .autoline-up-table-eqp td.category {\n  border-left: none;\n  font-size: .9em;\n  padding: 0 3px 0 3px;\n}\n\n.Hndmr .autoline-up-table-eqp td.std {\n  background-color: #cbc9e2;\n  padding: 6px 0 3px 0;\n  text-align: center;\n}\n\n.Hndmr .autoline-up-table-eqp td.maker {\n  background-color: #b3d0c5;\n  padding: 6px 0 3px 0;\n  text-align: center;\n}\n\n.Hndmr .autoline-up-table-eqp td.muji {\n  padding: 6px 0 3px 0;\n  text-align: center;\n}\n\n.Hndmr .autoline-up-table-eqp td.division-rt {\n  border-left: currentColor solid 1px;\n}\n\n.Hndmr .autoline-up-table-eqp td.layout-bottom-border {\n  border-bottom: currentColor solid 1px;\n}\n\n.Hndmr #auto-line-up-eqp-hosoku {\n  text-align: left;\n  width: 900px;\n}\n\n.Hndmr #auto-line-up-eqp-hosoku h3 {\n  font-size: .9em;\n  margin: 35px 0 6px 0;\n  padding: 0;\n}\n\n.Hndmr #auto-line-up-eqp-hosoku .all-type {\n  border: #202020 solid 1px;\n  padding: 5px 25px 5px 25px;\n}\n\n.Hndmr #auto-line-up-eqp-hosoku .all-type h4 {\n  border-bottom: #808080 solid 1px;\n  font-size: .9em;\n  margin: 0;\n  padding: 10px 0 4px 0;\n}\n\n.Hndmr #auto-line-up-eqp-hosoku .all-type p {\n  font-size: .9em;\n  margin: 0;\n  padding: 6px 0 8px 0;\n}\n\n.Hndmr #auto-line-up-eqp-hosoku p.caution-table {\n  font-size: .9em;\n  line-height: 150%;\n}\n\n.Hndmr #auto-line-up-eqp-hosoku p.caution_maker {\n  background-color: #bdd7d9;\n  font-size: .9em;\n  line-height: 120%;\n  margin: 10px 0 10px 0;\n  padding: 6px 10px 5px 10px;\n}\n\n.Hndmr #auto-line-up-eqp-hosoku p.caution {\n  font-size: .9em;\n  line-height: 150%;\n}\n\n.Hndmr .autoline-up-table-eqp {\n  border: currentColor solid;\n  border-width: 0 1px 1px 0;\n  font-size: .9em;\n}\n\n.Hndmr .autoline-up-table-eqp td {\n  border-left: currentColor solid 1px;\n  border-top: #505050 solid 1px;\n}\n\n.Hndmr .autoline-up-table-eqp td.top {\n  border-top: none;\n}\n\n.Hndmr .fuel {\n  background-color: #fbe6ef;\n}\n\n.Hndmr .autoline-up-table-eqp .fuel {\n  background-color: #fbe6ef;\n}\n\n.Hndmr p.fuel2 {\n  background-color: #fbe6ef;\n}\n\n.Hndmr span.akamaru {\n  color: #D90000;\n}\n\n.Hndmr .star {\n  color: #008837;\n}\n\n.Hndmr .autoline-up-table-eqp td.none {\n  border-left: none;\n  border-top: none;\n}\n\n.Hndmr .autoline-up-table-eqp td.element {\n  border-left: none;\n  line-height: 130%;\n  padding: 6px 0 3px 2px;\n}\n\n.Hndmr .autoline-up-table-eqp td.category {\n  border-left: none;\n  font-size: .9em;\n  padding: 0 3px 0 3px;\n}\n\n.Hndmr .autoline-up-table-eqp td.std {\n  background-color: #cbc9e2;\n  padding: 6px 0 3px 0;\n  text-align: center;\n}\n\n.Hndmr .autoline-up-table-eqp td.maker {\n  background-color: #b3d0c5;\n  padding: 6px 0 3px 0;\n  text-align: center;\n}\n\n.Hndmr .autoline-up-table-eqp td.muji {\n  padding: 6px 0 3px 0;\n  text-align: center;\n}\n\n.Hndmr .autoline-up-table-eqp td.division-rt {\n  border-left: currentColor solid 1px;\n}\n\n.Hndmr .autoline-up-table-eqp td.layout-bottom-border {\n  border-bottom: currentColor solid 1px;\n}\n\n.Hndmr #auto-line-up-eqp-hosoku {\n  text-align: left;\n  width: 600px;\n}\n\n.Hndmr #auto-line-up-eqp-hosoku h3 {\n  font-size: .9em;\n  margin: 35px 0 6px 0;\n  padding: 0;\n}\n\n.Hndmr #auto-line-up-eqp-hosoku .all-type {\n  border: #202020 solid 1px;\n  padding: 5px 25px 5px 25px;\n}\n\n.Hndmr #auto-line-up-eqp-hosoku .all-type h4 {\n  border-bottom: #808080 solid 1px;\n  font-size: .9em;\n  margin: 0;\n  padding: 10px 0 4px 0;\n}\n\n.Hndmr #auto-line-up-eqp-hosoku .all-type p {\n  font-size: .9em;\n  margin: 0;\n  padding: 6px 0 8px 0;\n}\n\n.Hndmr #auto-line-up-eqp-hosoku p.caution-table {\n  font-size: .9em;\n  line-height: 150%;\n}\n\n.Hndmr #auto-line-up-eqp-hosoku p.caution_maker {\n  background-color: #bdd7d9;\n  font-size: .9em;\n  line-height: 120%;\n  margin: 10px 0 10px 0;\n  padding: 6px 10px 5px 10px;\n}\n\n.Hndmr #auto-line-up-eqp-hosoku p.caution {\n  font-size: .9em;\n  line-height: 150%;\n}\n\n.Hndmr table.spec-table {\n  border-top: 1px solid #2c2c2c;\n}\n\n.Hndmr table.spec-table td {\n  border-right: 1px solid #2c2c2c;\n  border-bottom: 1px solid #2c2c2c;\n  text-align: center;\n}\n\n.Hndmr table.spec-table td.bd-left {\n  border-left: 1px solid #2c2c2c;\n}\n\n.Hndmr table#spec td.data {\n  text-align: center;\n  border-left: 1px solid currentColor;\n}\n\n.Hndmr table#spec td.type-name {\n  text-align: center;\n  font-weight: bold;\n  color: #FFF;\n  background-color: #415863;\n}\n\n.Hndmr table#spec td.sikiri-top {\n  border-top: 2px solid currentColor;\n}\n\n.Hndmr table#spec td.sikiri-bottom {\n  border-bottom: 2px solid currentColor;\n}\n\n.Hndmr p.inspire-spec {\n  font-weight: bold;\n}\n\n.Hndmr #spec-table td {\n  font-size: .9em;\n}\n\n.Hndmr #spec-table td.suuchi {\n  border-left: 1px solid #2c2c2c;\n  text-align: center;\n}\n\n.Hndmr #spec-table td.nenpi {\n  border-left: 1px solid #2c2c2c;\n  text-align: center;\n}\n\n.Hndmr #spec-table td p {\n  margin: 0;\n  padding: .2em .1em .2em .1em;\n}\n\n.Hndmr #spec-table td.tekiyougawa {\n  text-align: center;\n}\n\n.Hndmr #spec-table td.makeroption {\n  background-color: #a0c3d0;\n}\n\n.Hndmr #spec-table td.nakasen {\n  background-color: currentColor;\n}\n\n.Hndmr #spec-table td.nenpi {\n  background-color: #fddaec;\n}\n\n.Hndmr #spec-table td {\n  border-bottom: 1px solid #666;\n}\n\n.Hndmr #spec-table td.bdtop {\n  border-top: 1px solid #666;\n}\n\n.Hndmr .block_color {\n  background-color: #7DCDF4;\n}\n\n.Hndmr .block_color2 {\n  background-color: #00A0E9;\n  color: #FFF;\n}\n\n.Hndmr .block_color3 {\n  background-color: #BFBFBF;\n}\n\n.Hndmr .block_color4 {\n  background-color: #F9C270;\n}\n\n.Hndmr .block_line {\n  border-bottom: solid 1px currentColor;\n  border-left: solid 1px currentColor;\n}\n\n.Hndmr .block_line_top {\n  border-top: solid 1px currentColor;\n}\n\n.Hndmr .block_line_right {\n  border-right: solid 1px currentColor;\n}\n\n.Hndmr .position {\n  text-align: center;\n}\n\n.Hndmr p.nenpi {\n  margin: .3em 0;\n  padding: .3em .2em;\n  background-color: #FADCE9;\n}\n\n.Hndmr table#type-midashi {\n  border: 1px solid currentColor;\n  border-bottom: 2px solid currentColor;\n  border-collapse: collapse;\n}\n\n.Hndmr table#type-midashi td {\n  font-size: 11px;\n  line-height: 13px;\n  text-align: center;\n}\n\n.Hndmr table#type-midashi span {\n  font-size: 10px;\n}\n\n.Hndmr table#type-midashi strong {\n  font-size: 12px;\n}\n\n.Hndmr #type-midashi td.migi {\n  border-right: 1px solid currentColor;\n}\n\n.Hndmr #type-midashi td.sita {\n  border-bottom: 1px solid currentColor;\n}\n\n.Hndmr table#spec th {\n  border-top: 1px solid currentColor;\n}\n\n.Hndmr table#spec td {\n  border-top: 1px solid currentColor;\n}\n\n.Hndmr table#spec .spec {\n  border-top: 1px solid currentColor;\n}\n\n.Hndmr table#spec td.spec-name {\n  border-right: 1px solid currentColor;\n  border-bottom: 1px solid currentColor;\n  padding: 2px 0 2px 2px;\n}\n\n.Hndmr table#spec td.atai {\n  text-align: center;\n  border-right: 1px solid currentColor;\n  border-bottom: 1px solid currentColor;\n}\n\n.Hndmr table#spec td.atai2 {\n  text-align: center;\n  border-right: 1px solid currentColor;\n  border-bottom: 1px solid currentColor;\n  background-color: #FBE6EF;\n}\n\n.Hndmr table#spec td.category-botom {\n  border-bottom: 2px solid currentColor;\n}\n\n.Hndmr table#spec td.category-name {\n  font-weight: bold;\n  border-left: 1px solid currentColor;\n  border-bottom: 2px solid currentColor;\n}\n\n.Hndmr table.caution {\n  width: 640px;\n}\n\n.Hndmr table.caution td {\n  font-size: 10px;\n  line-height: 1.3em;\n}\n\n.Hndmr #spec-table td.line-migisita {\n  border-right-width: 1px;\n  border-bottom-width: 1px;\n  border-right-style: solid;\n  border-bottom-style: solid;\n  border-right-color: currentColor;\n  border-bottom-color: currentColor;\n}\n\n.Hndmr #spec-table td.line-migi {\n  border-right-width: 1px;\n  border-right-style: solid;\n  border-right-color: currentColor;\n}\n\n.Hndmr #spec-table td.line-sita {\n  border-bottom-width: 2px;\n  border-bottom-style: solid;\n  border-bottom-color: currentColor;\n}\n\n.Hndmr #spec-table td.speccontents {\n  text-align: center;\n}\n\n.Hndmr #spec-table td.speccontents-katasiki {\n  text-align: center;\n}\n\n.Hndmr p.zentype-category {\n  font-weight: bold;\n}\n\n.Hndmr #webcata_footer {\n  clear: both;\n  padding: 30px 128px 15px 20px;\n  text-align: right;\n}\n\n.Hndmr #listtable {\n  font-size: 80%;\n  font-style: normal;\n  font-weight: normal;\n  border: currentColor solid;\n  border-width: 2px 0 0 2px;\n}\n\n.Hndmr #listtable td {\n  border: currentColor solid;\n  border-width: 0 1px 1px 0;\n  padding: 2px;\n  text-align: center;\n}\n\n.Hndmr #listtable td.midashi1 {\n  border-width: 0 0 2px 2px;\n  text-align: center;\n  vertical-align: middle;\n}\n\n.Hndmr #listtable td.midashi1b {\n  border-width: 0 2px 0 2px;\n  text-align: center;\n  vertical-align: middle;\n}\n\n.Hndmr #listtable td.midashi2 {\n  border-right-width: 2px;\n  text-align: left;\n  vertical-align: top;\n}\n\n.Hndmr #listtable td.midashi2b {\n  border-right-width: 0;\n  text-align: left;\n  vertical-align: top;\n}\n\n.Hndmr #listtable td.midashi2c {\n  border-width: 0 2px 2px 0;\n  text-align: left;\n  vertical-align: top;\n}\n\n.Hndmr #listtable td.midashi2d {\n  border-width: 0 0 2px 0;\n  text-align: left;\n  vertical-align: top;\n}\n\n.Hndmr #listtable .cell-border {\n  border-bottom-width: 2px;\n}\n\n.Hndmr #listtable .cell-normal {\n  background-color: #f1f9fb;\n}\n\n.Hndmr #listtable .cell-makeroption {\n  background-color: #f1f8ed;\n}\n\n.Hndmr #listtable .cell-normal-border {\n  background-color: #f1f9fb;\n  border-bottom-width: 2px;\n}\n\n.Hndmr #listtable .cell-makeroption-border {\n  background-color: #f1f8ed;\n  border-bottom-width: 2px;\n}\n\n.Hndmr #listtable .cell-border2 {\n  border-right-width: 2px;\n}\n\n.Hndmr #listtable .cell-normal2 {\n  background-color: #f1f9fb;\n  border-right-width: 2px;\n}\n\n.Hndmr #listtable .cell-makeroption2 {\n  background-color: #f1f8ed;\n  border-right-width: 2px;\n}\n\n.Hndmr #listtable .cell-normal-border2 {\n  background-color: #f1f9fb;\n  border-width: 0 2px 2px 0;\n}\n\n.Hndmr #listtable .cell-makeroption-border2 {\n  background-color: #f1f8ed;\n  border-width: 0 2px 2px 0;\n}\n\n.Hndmr #listtable .cell-border3 {\n  border-width: 0 2px 2px 0;\n}\n\n.Hndmr #listtable .cell-border3b {\n  border-width: 0 2px 2px 0;\n  text-align: left;\n  vertical-align: top;\n}\n\n.Hndmr #listtable .cell-border4 {\n  border-width: 0 0 2px 2px;\n}\n\n.Hndmr #listtable .cell-border5 {\n  border-right-width: 0;\n}\n\n.Hndmr #listtable .cell-border6 {\n  border-width: 0 2px 0 2px;\n}\n\n.Hndmr #listtable .cell-noborder {\n  border-width: 0;\n}\n\n.Hndmr #listtable .type {\n  font-size: small;\n}\n\n.Hndmr #listtable .noborder {\n  border-width: 0;\n  padding: 0;\n}\n\n.Hndmr #listtable .noborder2 {\n  border-width: 0 2px 0 0;\n  padding: 0;\n}\n\n.Hndmr p.footnote-r {\n  font-size: x-small;\n  margin-bottom: 10px;\n  text-align: right;\n}\n\n.Hndmr p.footnote-l {\n  font-size: x-small;\n  line-height: 130%;\n  text-align: left;\n}\n\n.Hndmr #specifications {\n  font-size: x-small;\n  line-height: 130%;\n  text-align: center;\n  margin-bottom: 5px;\n  border-width: 2px 1px 0 2px;\n  border-style: solid;\n  border-color: currentColor;\n}\n\n.Hndmr #specifications th,\n#specifications td {\n  padding: 2px;\n}\n\n.Hndmr #specifications th.midashi {\n  font-weight: normal;\n  text-align: left;\n  border-width: 0 1px 1px 0;\n  border-style: solid;\n  border-color: currentColor;\n}\n\n.Hndmr #specifications th.midashi2 {\n  font-weight: normal;\n  text-align: left;\n  border-width: 0 1px 2px 0;\n  border-style: solid;\n  border-color: currentColor;\n}\n\n.Hndmr #specifications th.midashi3 {\n  border-width: 0 0 2px 0;\n  border-style: solid;\n  border-color: currentColor;\n}\n\n.Hndmr #specifications th.car {\n  font-size: medium;\n  font-weight: bold;\n  background-color: #d9d9d9;\n  border-width: 0 1px 1px 0;\n  border-style: solid;\n  border-color: currentColor;\n  line-height: 130%;\n}\n\n.Hndmr #specifications th.car .small {\n  font-size: x-small;\n  line-height: 120%;\n}\n\n.Hndmr #specifications th.ff-4wd {\n  font-weight: bold;\n  border-width: 0 1px 2px 0;\n  border-style: solid;\n  border-color: currentColor;\n}\n\n.Hndmr #specifications td.midashi {\n  border-width: 0 1px 1px 0;\n  border-style: solid;\n  border-color: currentColor;\n  text-align: left;\n  vertical-align: top;\n}\n\n.Hndmr #specifications td.midashi2 {\n  border-width: 0 1px 2px 0;\n  border-style: solid;\n  border-color: currentColor;\n  text-align: left;\n  vertical-align: top;\n}\n\n.Hndmr #specifications td.cell {\n  border-width: 0 1px 1px 0;\n  border-style: solid;\n  border-color: currentColor;\n  text-align: center;\n}\n\n.Hndmr #specifications td.cell2 {\n  border-width: 0 1px 2px 0;\n  border-style: solid;\n  border-color: currentColor;\n  text-align: center;\n}\n\n.Hndmr #specifications td.cell-fuel {\n  border-width: 0 1px 1px 0;\n  border-style: solid;\n  border-color: currentColor;\n  background-color: #fadce9;\n  text-align: center;\n}\n\n.Hndmr #specifications td.midashi-fuel {\n  border-width: 0 1px 1px 0;\n  border-style: solid;\n  border-color: currentColor;\n  text-align: left;\n  vertical-align: top;\n  background-color: #fadce9;\n}\n\n.Hndmr .green {\n  color: #094;\n}\n\n.Hndmr .fuel {\n  background-color: #fadce9;\n}\n\n.Hndmr #spec {\n  border: currentColor solid;\n  border-width: 0 0 1px 0;\n  font-size: x-small;\n  line-height: 130%;\n  margin-bottom: 5px;\n}\n\n.Hndmr #spec th,\n#spec td {\n  border: currentColor solid;\n  border-width: 1px 0 0 0;\n  vertical-align: top;\n  padding: 2px 5px 2px 5px;\n}\n\n.Hndmr #spec th {\n  background-color: #e6e6e6;\n  font-weight: normal;\n  text-align: left;\n}\n\n.Hndmr #spec .spec {\n  border: currentColor solid;\n  border-width: 0 0 0 1px;\n  text-align: center;\n}\n\n.Hndmr #spec .fuel {\n  background-color: #bccde9;\n}\n\n.Hndmr #spec .specfuel {\n  background-color: #bccde9;\n  border: currentColor solid;\n  border-width: 0 0 0 1px;\n  text-align: center;\n}\n\n.Hndmr .footnote {\n  font-size: x-small;\n  line-height: 130%;\n}\n\n.Hndmr .footnote .spec {\n  color: #003f98;\n}\n\n.Hndmr div#web-catalog-contents {\n  margin: 24px;\n  width: 461px;\n}\n\n.Hndmr div#web-catalog-contents h4 {\n  background: #325958;\n  color: #fff;\n  font-size: .9em;\n  margin-bottom: 14px;\n  padding: 4px 0 4px 10px;\n  width: 451px;\n}\n\n.Hndmr div#web-catalog-contents table.model-navi {\n  margin: 0 0 5px 0;\n}\n\n.Hndmr div#web-catalog-contents table.model-navi td {\n  padding: 0 25px 0 10px;\n}\n\n.Hndmr div#web-catalog-contents * {\n  margin: 0;\n  padding: 0;\n}\n\n.Hndmr div#web-catalog-contents h4 span {\n  font-size: .9em;\n  font-weight: normal;\n}\n\n.Hndmr div#web-catalog-contents p.caution,\np.caution {\n  font-size: .9em;\n  line-height: 120%;\n}\n\n.Hndmr #eq_spec_list {\n  background-color: #fff;\n  border: #808080 solid;\n  border-width: 1px 0 0 1px;\n  font-size: .9em;\n  margin-bottom: 15px;\n}\n\n.Hndmr #eq_spec_list td {\n  border: #808080 solid;\n  border-width: 0 1px 1px 0;\n  padding: 2px;\n}\n\n.Hndmr #eq_spec_list td.right_non_border {\n  border-right: none;\n}\n\n.Hndmr #eq_spec_list td p {\n  width: 10px;\n}\n\n.Hndmr #eq_spec_list td.list_top {\n  background-color: #03494a;\n  color: #fff;\n  font-weight: bold;\n}\n\n.Hndmr #eq_spec_list td.txt_center {\n  text-align: center;\n}\n\n.Hndmr .card_index img {\n  border: 0;\n  float: right;\n}\n\n.Hndmr .card_index br {\n  clear: both;\n}\n\n.Tgrgj table {\n  border: #696969 solid 1px;\n  border-collapse: collapse;\n  width: 90%;\n}\n\n.Tgrgj table th {\n  background-color: #f5f5f5;\n  border: #696969 solid 1px;\n  font-weight: bold;\n  padding: 3px;\n}\n\n.Tgrgj table td {\n  border: #696969 solid 1px;\n  padding: 3px;\n}\n\n.Smsbj table {\n  border: #696969 solid 1px;\n  border-collapse: collapse;\n  width: 90%;\n}\n\n.Smsbj table th {\n  background-color: #f5f5f5;\n  border: #696969 solid 1px;\n  font-weight: bold;\n  padding: 3px;\n}\n\n.Smsbj table td {\n  border: #696969 solid 1px;\n  padding: 3px;\n}\n\n.Volvo table.specTable {\n  background: #e9e6e1;\n  border: none;\n  border-bottom: #fff solid 1px;\n  border-collapse: collapse;\n  font-size: 10pt;\n}\n\n.Volvo table.specTable th,\ntable.specTable td {\n  padding: 7px;\n}\n\n.Volvo table.specTable thead th {\n  background: #999;\n  color: #fff;\n  border: solid 1px;\n  border-color: #ece9d8 #aaa #889294 #aaa;\n}\n\n.Volvo table.specTable tbody th {\n  background: #c1d2d9;\n  border: #fff solid 1px;\n  border-color: #879294 #fff #ece9d8 #c7d0d7;\n  color: #60686b;\n  text-align: left;\n}\n\n.Volvo table.specTable tbody td {\n  border: #fff solid 1px;\n  border-bottom: none;\n  border-color: #ece9d8 #fff;\n}\n\n.Volvo table.specTable tbody tr.b td {\n  background: #dedbd2;\n}\n\n.Htmlr table {\n  width: 100%;\n}\n\n.Htmlr table td {\n  padding: 3px;\n}\n\n.Htmlr .nmp,\n.Htmlr .nmg {\n  margin: 0;\n}\n\n.Htmlr .exp {\n  background-color: #eee;\n}\n\n.Htmlr .tbl {\n  background-color: #ccc;\n  border: #696969 solid;\n  border-width: 1px 0 1px 0;\n}\n\n.Htmlr .gray td {\n  background-color: #ddd;\n  border: #696969 solid;\n  border-width: 0 0 1px 0;\n}\n\n.Htmlr .wht td {\n  border: #696969 solid;\n  border-width: 0 0 1px 0;\n}\n\n.Htmlr td.gray {\n  background-color: #ddd;\n  border: #696969 solid;\n  border-width: 0 0 1px 0;\n}\n\n.Htmlr td.wht {\n  border: #696969 solid;\n  border-width: 0 0 1px 0;\n}\n\n.Htmlr .exsamp {\n  font-size: .8em;\n  margin: 0;\n  padding: 0;\n}\n\n.Frdmr div.content {\n  font-size: .9em;\n}\n\n.Frdmr div.content_wide {\n  font-size: .9em;\n}\n\n.Frdmr h3 {\n  font-size: .9em;\n}\n\n.Frdmr table.Fordtable {\n  border: #84a2c6 solid 1px;\n  border-collapse: collapse;\n  font-size: .9em;\n  width: 100%;\n}\n\n.Frdmr td {\n  border-right: #84a2c6 solid 1px;\n  text-align: center;\n  width: 200px;\n}\n\n.Frdmr td.Fordannotd {\n  border-right: #fff solid 1px;\n  text-align: left;\n  width: 20%;\n}\n\n.Frdmr td.Forddeltd {\n  border-right: #fff solid 1px;\n  text-align: left;\n}\n\n.Frdmr td.Fordlabel {\n  text-align: left;\n  width: 30%;\n}\n\n.Frdmr td.Fordtdest {\n  text-align: center;\n  width: 30%;\n}\n\n.Frdmr td.Fordtd {\n  text-align: center;\n}\n\n.Frdmr th {\n  border-right: #84a2c6 solid 1px;\n}\n\n.Frdmr th.Fordmondeo {\n  vertical-align: top;\n}\n\n.Frdmr th.label {\n  text-align: left;\n}\n\n.Frdmr tr.alt {\n  background-color: #e7eff7;\n}\n\n.Frdmr tr.head {\n  background-color: #f7f3e7;\n}\n\n.Omtsd table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n  text-align: center;\n  width: 590px;\n}\n\n.Omtsd table td {\n  border: currentColor solid 1px;\n}\n\n.Omtsd table th {\n  border: currentColor solid 1px;\n}\n\n.Tnskj img {\n  float: left;\n  margin: 0 10px 0 10px;\n}\n\n.Ydkrz {\n  text-align: center;\n}\n\n.Ydkrz p {\n  margin: 20px 50px 20px 50px;\n  text-align: left;\n}\n\n.Lndrv table.LndrvFL {\n  border: #ccc solid 1px;\n  border-collapse: collapse;\n  text-align: center;\n  width: 100%;\n}\n\n.Lndrv table.LndrvFL td {\n  background-color: #f4f7fb;\n  border: #ccc solid 1px;\n}\n\n.Spchk table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n  width: 100%;\n}\n\n.Spchk table th {\n  background-color: #eee;\n  border: currentColor solid 1px;\n}\n\n.Spchk table td {\n  border: currentColor solid 1px;\n}\n\n.Ktmbd table {\n  border: #666 solid 1px;\n  border-collapse: collapse;\n  width: 100%;\n}\n\n.Ktmbd table td {\n  border: #666 solid 1px;\n}\n\n.Ycezj .YcezjL {\n  float: left;\n  text-align: center;\n  width: 400px;\n}\n\n.Ycezj .YcezjR {\n  float: right;\n  text-align: left;\n  width: 240px;\n}\n\n.Ycezj .YcezjR .YcezjReal {\n  float: left;\n  text-align: center;\n  width: 120px;\n}\n\n.Ycezj .YcezjR .YcezjWinm {\n  float: right;\n  text-align: center;\n  width: 120px;\n}\n\n.Szkdb table {\n  border: #999 solid 1px;\n  border-collapse: collapse;\n  width: 80%;\n}\n\n.Szkdb table td {\n  border: #999 solid 1px;\n}\n\n.Opsyg .OpsygK {\n  border: #cdcdcd solid;\n  border-width: 0 0 1px 0;\n  font-weight: bold;\n  margin: 0;\n  width: 550px;\n}\n\n.Buell table {\n  border: #696969 solid 1px;\n  border-collapse: collapse;\n}\n\n.Buell table td {\n  border: #696969 solid 1px;\n}\n\n.Dctbd table {\n  border: #999 solid 1px;\n  border-collapse: collapse;\n  width: 80%;\n}\n\n.Dctbd table td {\n  border: #999 solid 1px;\n}\n\n.Bmwag table {\n  border-collapse: collapse;\n  width: 600px;\n}\n\n.Bmwag table td {\n  border: #999 solid;\n  border-width: 1px 0 1px 0;\n}\n\n.Bmwag table th {\n  text-align: left;\n}\n\n.Bmwag .BmwagR {\n  text-align: right;\n}\n\n.Szkmt .e9 {\n  font-size: 9px;\n}\n\n.Szkmt .e10 {\n  font-size: 10px;\n}\n\n.Szkmt .e12 {\n  font-size: 12px;\n}\n\n.Szkmt .j9 {\n  font-size: 10px;\n}\n\n.Szkmt .j10 {\n  font-size: 11px;\n}\n\n.Szkmt .j12 {\n  font-size: 12px;\n}\n\n.Szkmt .j14 {\n  font-size: 14px;\n}\n\n.Szkmt .e9l {\n  font-size: 9px;\n}\n\n.Szkmt .e10l {\n  font-size: 10px;\n}\n\n.Szkmt .e12l {\n  font-size: 12px;\n}\n\n.Szkmt .j9l {\n  font-size: 10px;\n}\n\n.Szkmt .j10l {\n  font-size: 11px;\n}\n\n.Szkmt .j12l {\n  font-size: 12px;\n}\n\n.Szkmt .j14l {\n  font-size: 14px;\n}\n\n.Szkmt .e9h {\n  font-size: 9px;\n}\n\n.Szkmt .e10h {\n  font-size: 10px;\n}\n\n.Szkmt .e12h {\n  font-size: 12px;\n}\n\n.Szkmt .j9h {\n  font-size: 10px;\n}\n\n.Szkmt .j10h {\n  font-size: 11px;\n}\n\n.Szkmt .j12h {\n  font-size: 12px;\n}\n\n.Szkmt .j14h {\n  font-size: 14px;\n}\n\n.Szkmt .style1 {\n  color: #f00;\n}\n\n.Wpgjn .Wpgjntable {\n  border: #ff9a9c solid 1px;\n  border-collapse: collapse;\n}\n\n.Ngkyg p {\n  margin: 0 0 1.33em 0;\n}\n\n.Esttd .EsttdFloatL {\n  float: left;\n  width: 280px;\n}\n\n.Esttd .EsttdSpecBox {\n  float: right;\n  margin: 0;\n  width: 310px;\n}\n\n.Mokat .MokatL {\n  float: left;\n  width: 170px;\n}\n\n.Mokat .MokatR {\n  float: right;\n  width: 520px;\n}\n\n.Osksk .OskskL {\n  float: left;\n  width: 310px;\n}\n\n.Osksk .OskskR {\n  float: right;\n  width: 300px;\n}\n\n.Osksk table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n}\n\n.Osksk table td {\n  border: currentColor solid 1px;\n  font-size: .9em;\n}\n\n.Keiod .KeiodL {\n  float: left;\n  width: 545px;\n}\n\n.Keiod .KeiodR {\n  float: right;\n  margin-left: 10px;\n  width: 145px;\n}\n\n.Kmbkz .KmbkzL {\n  float: left;\n  width: 530px;\n}\n\n.Kmbkz .KmbkzR {\n  float: right;\n  margin-left: 10px;\n  width: 150px;\n}\n\n.Ibrtd table {\n  border-collapse: collapse;\n}\n\n.Ktskk a img {\n  border: 0;\n}\n\n.Ktskk .KtskkM {\n  border: currentColor solid;\n  border-width: 0 0 1px 5px;\n  font-size: 1.0em;\n  font-weight: bold;\n  width: 50%;\n}\n\n.Jagar table {\n  color: #808080;\n}\n\n.Jagar table#JAGUAR_XF {\n  border: #696969 solid 1px;\n  border-collapse: collapse;\n  color: currentColor;\n  text-align: center;\n}\n\n.Tndhs .TndhsC table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n}\n\n.Tndhs .TndhsC table td {\n  border: currentColor solid 1px;\n}\n\n.Snbkk table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n  text-align: center;\n}\n\n.Snbkk td {\n  border: currentColor solid 1px;\n  text-align: center;\n}\n\n.Snbkk th {\n  border: currentColor solid 1px;\n  text-align: center;\n}\n\n.Acgty .AcgtyL {\n  float: left;\n  width: 410px;\n}\n\n.Acgty .AcgtyR {\n  float: right;\n  margin-left: 10px;\n  text-align: center;\n  width: 270px;\n}\n\n.Oranf table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n  text-align: center;\n}\n\n.Oranf td {\n  border: currentColor solid 1px;\n  text-align: center;\n}\n\n.Oranf th {\n  border: currentColor solid 1px;\n  text-align: center;\n}\n\n.Oranf .OranfL {\n  float: left;\n  margin-left: 5px;\n  text-align: center;\n}\n\n.Oranf .OranfR {\n  float: right;\n  margin-right: 5px;\n  text-align: center;\n}\n\n.Oranf .OranfC {\n  margin-left: 265px;\n  text-align: center;\n  width: 185px;\n}\n\n.Oranf p {\n  margin: 10px 0 0 0;\n  padding: 0;\n}\n\n.Oranf span {\n  display: block;\n}\n\n.Hgnhf .HgnhfBox {\n  background-color: #ddd;\n  font-weight: bold;\n  margin: 3px 0 3px 0;\n  padding: 0 0 0 3px;\n}\n\n.Hgnhf .HgnhfBrd {\n  border: #ccc solid 1px;\n  font-weight: bold;\n  margin: 3px 0 3px 0;\n  padding: 0 0 0 3px;\n}\n\n.Hgnhf .ship_data {\n  float: right;\n  height: 185px;\n  margin-bottom: 10px;\n  width: 225px;\n}\n\n.Hgnhf .data_img {\n  float: left;\n}\n\n.Hgnhf .ship_data th {\n  background: #c0c0c0;\n  border-bottom: #fff solid 1px;\n  font-weight: normal;\n  padding: 3px;\n  text-align: left;\n}\n\n.Hgnhf .ship_data td {\n  background: #ddd;\n  border-bottom: #fff solid 1px;\n  padding: 3px;\n}\n\n.Hgnhf * {\n  margin: 0;\n  padding: 0;\n}\n\n.Hknks .HknksT table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n  text-align: center;\n}\n\n.Hknks .HknksT td {\n  border: currentColor solid 1px;\n  text-align: center;\n}\n\n.Okisf .OkisfT table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n  text-align: center;\n}\n\n.Okisf .OkisfT td {\n  border: currentColor solid 1px;\n  padding: 0 5px 0 5px;\n  text-align: center;\n}\n\n.Hankf table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n  text-align: center;\n}\n\n.Hankf td {\n  border: currentColor solid 1px;\n  padding: 0 5px 0 5px;\n  text-align: center;\n}\n\n.Myzkf table table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n  text-align: center;\n}\n\n.Myzkf table table td {\n  border: currentColor solid 1px;\n  padding: 0 5px 0 5px;\n  text-align: center;\n}\n\n.Ipmks .IpmksImg img {\n  margin: 3px 0 3px 0;\n}\n\n.SSMTF .SsmtfL {\n  float: left;\n  width: 35%;\n}\n\n.SSMTF .SsmtfR {\n  float: right;\n  width: 65%;\n}\n\n.Ssimk .SsimkBlackLine {\n  background-color: currentColor;\n}\n\n.Tkwfr .TkwfrTable td {\n  border: #eee solid 1px;\n  border-left: #ddd solid 5px;\n  position: relative;\n}\n\n.Tkwfr .TkwfrTable th {\n  background-color: #eee;\n  border-bottom: #fff solid 1px;\n  border-left: #fff solid 1px;\n  width: 35%;\n}\n\n.Tkwfr .TkwfrTable {\n  border: #eee solid 1px;\n  border-collapse: collapse;\n  margin-bottom: 10px;\n  width: 284px;\n}\n\n.Tkwfr .TkwfrTable td ul {\n  margin-top: 10px;\n}\n\n.Tkwfr .TkwfrTable th,\n.TkwfrTable td {\n  padding-left: 10px;\n  padding-top: 4px;\n  text-align: left;\n  vertical-align: top;\n}\n\n.Gicns .ga_contents {\n  margin: 0;\n  padding: 0;\n  width: 595px;\n}\n\n.Gicns .ga_small {\n  font-size: .9em;\n}\n\n.Gicns .ga_name_d {\n  background-color: #f5f5f5;\n  border: #ddd solid 1px;\n  line-height: 1.2em;\n  margin: 0 0 10px;\n  padding: 6px 2px 2px 12px;\n  width: 579px;\n}\n\n.Gicns .ga_name_g {\n  background-color: #f5f5f5;\n  border: #ddd solid 1px;\n  font-weight: normal;\n  margin: 0 0 10px;\n  padding: 6px 2px 2px 12px;\n  width: 579px;\n}\n\n.Gicns .ga_photogroup {\n  float: left;\n  margin: 0 10px 10px 0;\n  padding: 0;\n  width: 220px;\n}\n\n.Gicns .ga_frame_d {\n  background-color: #fff;\n  border: #c0c0c0 solid 1px;\n  margin: 0 0 10px;\n  padding: 0;\n  width: 208px;\n}\n\n.Gicns .ga_frame_d2 {\n  background-color: #fff;\n  border: #ddd solid 1px;\n  margin: 0 0 10px;\n  padding: 0;\n  width: 208px;\n}\n\n.Gicns .ga_ftitle_d {\n  background-color: #c0c0c0;\n  border-bottom: #eee solid 1px;\n  font-size: .9em;\n  height: 22px;\n  margin: 0;\n  padding: 2px;\n  text-align: center;\n  width: 204px;\n}\n\n.Gicns .ga_ftitle_d2 {\n  background-color: #f5f5f5;\n  border-bottom: #ddd solid 1px;\n  font-size: .9em;\n  height: 22px;\n  margin: 0;\n  padding: 2px;\n  text-align: center;\n  width: 204px;\n}\n\n.Gicns .ga_fexp {\n  margin: 10px;\n  padding: 0;\n}\n\n.Gicns .ga_fexp ul {\n  margin: 4px 0;\n  padding: 0;\n}\n\n.Gicns .ga_fexp li {\n  line-height: 1.2em;\n  margin: 6px 0 6px 14px;\n  padding: 0;\n}\n\n.Gicns .ga_exp {\n  line-height: 1.4em;\n  margin: 0 0 20px 10px;\n  padding: 0;\n}\n\n.Gicns .ga_frame {\n  background-color: #fff;\n  border: #c0c0c0 solid 1px;\n  margin: 0 0 10px;\n  padding: 0;\n  width: 148px;\n}\n\n.Gicns .ga_ftitle {\n  background-color: #c0c0c0;\n  font-size: .9em;\n  height: 22px;\n  margin: 0;\n  padding: 2px;\n  text-align: center;\n  width: 144px;\n}\n\n.Gicns .ga_frame2 {\n  background-color: #fff;\n  border: #ddd solid 1px;\n  margin: 0 0 10px;\n  padding: 0;\n  width: 148px;\n}\n\n.Gicns .ga_ftitle2 {\n  background-color: #f5f5f5;\n  font-size: .9em;\n  height: 22px;\n  margin: 0;\n  padding: 2px;\n  text-align: center;\n  width: 144px;\n}\n\n.Nchcz .nchczB {\n  border: #808080 solid 1px;\n}\n\n.Nchcz .nchczT {\n  border: #808080 solid 1px;\n  border-collapse: collapse;\n}\n\n.Nchcz .nchczT td {\n  border: #808080 solid 1px;\n}\n\n.Smzkn table {\n  border: #808080 solid 1px;\n  border-collapse: collapse;\n}\n\n.Sfnyg img {\n  float: left;\n  margin-right: 5px;\n  vertical-align: top;\n}\n\n.Ksyhs .KsyhsThumImg {\n  float: left;\n  margin: 0;\n  padding: 0;\n  width: 158px;\n}\n\n.Bsgys div.boxinner {\n  padding: 5px 5px 5px 10px;\n}\n\n.Bsgys div.boxbottom {\n  height: 5px;\n}\n\n.Bsgys span.Bsgysfn {\n  color: #436976;\n  font-size: .8em;\n  font-weight: bold;\n  vertical-align: super;\n}\n\n.Bsgys h1,\n.Bsgys h2,\n.Bsgys h3,\n.Bsgys h4,\n.Bsgys h5 {\n  background-color: inherit;\n  border-bottom: currentColor 1px solid;\n  clear: left;\n  color: currentColor;\n  font-size: 100%;\n  font-weight: bold;\n  margin: 0 0 .5em 0;\n  padding: 1em 0 0 0;\n  z-index: 0;\n}\n\n.Bsgys div.BsgysImg {\n  float: left;\n  margin: 13px 0 9px 10px;\n  width: 88px;\n}\n\n.Bsgys .BsgysLicense {\n  margin-top: 10px;\n  width: 100%;\n}\n\n.Bsgys div.BsgysLicense {\n  border: #808080 solid 1px;\n}\n\n.Bsgys div.BsgysText {\n  float: left;\n  font-size: .8em;\n  margin: 7px 0 10px 10px;\n  width: 520px;\n}\n\n.Bsgys table {\n  border-collapse: collapse;\n}\n\n.Bsgys table.inline td {\n  border: #8cacbb 1px solid;\n  padding: 3px;\n}\n\n.Bsgys table.inline th {\n  background-color: #dee7ec;\n  border: #8cacbb 1px solid;\n  padding: 3px;\n}\n\n.Knnsz .KnnszL {\n  text-align: center;\n  float: left;\n  width: 260px;\n}\n\n.Knnsz .KnnszR {\n  float: right;\n  width: 365px;\n}\n\n.Knnsz dd {\n  margin-left: 24px;\n  padding: 0;\n}\n\n.Knnsz ol {\n  margin: 0;\n  padding: 0;\n}\n\n.Esksk Img {\n  margin: 3px 0;\n}\n\n.Esksk .eskskC {\n  text-align: center;\n  vertical-align: middle;\n  width: 300px;\n  height: 300px;\n}\n\n.Zkknm .ZkknmL {\n  float: left;\n  width: 350px;\n}\n\n.Zkknm .ZkknmR {\n  float: right;\n  width: 290px;\n}\n\n.Kkszi .KksziL {\n  float: left;\n  width: 450px;\n}\n\n.Kkszi .KksziR {\n  float: right;\n  width: 175px;\n}\n\n.Gzicr .GzicrL {\n  float: left;\n  width: 450px;\n}\n\n.Gzicr .GzicrR {\n  float: right;\n  width: 175px;\n}\n\n.Nsrsk dt {\n  border-left: currentColor solid 6px;\n  text-indent: 1ex;\n}\n\n.Nsrsk .NsrskMaintxt {\n  float: left;\n  text-align: left;\n}\n\n.Nsrsk .NsrskRightph {\n  border: #ccc solid 1px;\n  float: right;\n  text-align: center;\n}\n\n.Hknac .table03 table {\n  margin: 6px 0 0;\n}\n\n.Hknac .table03 td {\n  border: #fff solid 1px;\n  vertical-align: top;\n}\n\n.Tltdb div.TltdbLeft {\n  float: left;\n  text-align: center;\n}\n\n.Tltdb img {\n  display: block;\n}\n\n.Tltdb strong {\n  color: #808080;\n  font-size: .9em;\n  font-weight: normal;\n}\n\n.Tltdb table {\n  border-collapse: collapse;\n  float: right;\n  width: 400px;\n}\n\n.Tltdb table td.Tltdbtitle {\n  border-top: #ccc 1px dotted;\n  font-weight: bold;\n  text-align: left;\n  width: 120px;\n}\n\n.Tltdb table td {\n  border-top: #ccc 1px dotted;\n}\n\n.Mntey .intro {\n  background-color: #f5f5f5;\n  border: #808080 solid 1px;\n  margin-top: 25px;\n  padding: 10px;\n}\n\n.Ssmnf .imgbox {\n  text-align: right;\n  width: 605px;\n}\n\n.Ssmnf .imgbox img {\n  margin-bottom: 5px;\n}\n\n.Ssmnf .imgbox .imgboxspan {\n  border: black solid 1px;\n  padding: 3px;\n}\n\n.Tytmt .specTbl2 {\n  width: 100%;\n}\n\n.Tytmt .specTbl {\n  width: 100%;\n}\n\n.Tytmt table {\n  border-collapse: collapse;\n}\n\n.Tytmt table td {\n  border: currentColor solid 1px;\n  padding: 3px;\n}\n\n.Tytmt table tr {\n  border: currentColor solid 1px;\n  padding: 3px;\n}\n\n.Tytmt table table td {\n  border: 0;\n}\n\n.Tytmt table table tr {\n  border: 0;\n}\n\n.Tytmt .smallMText {\n  font-size: .7em;\n}\n\n.Jlgci .Jlgciclub-box {\n  border: #ccc solid;\n  border-width: 1px 1px 0 1px;\n}\n\n.Jlgci .Jlgciclub-box th {\n  background-color: #eee;\n  border-bottom: #ccc solid 1px;\n  border-right: #aaa solid 1px;\n  vertical-align: middle;\n  width: 30%;\n}\n\n.Jlgci td {\n  border-bottom: #ccc solid 1px;\n  background-color: #fff;\n  padding-left: 5px;\n}\n\n.Lxsmt dt {\n  float: left;\n}\n\n.Lxsmt table {\n  border-collapse: collapse;\n  width: 100%;\n}\n\n.Lxsmt table td {\n  border: currentColor solid 1px;\n  padding: 3px;\n}\n\n.Lxsmt table tr {\n  border: currentColor solid 1px;\n  padding: 3px;\n}\n\n.Lxsmt table table td {\n  border: 0;\n}\n\n.Lxsmt table table tr {\n  border: 0;\n}\n\n.Lxsmt td {\n  vertical-align: top;\n}\n\n.Lxsmt th {\n  vertical-align: top;\n}\n\n.Ktiem table.KtiemBorder {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n  width: 100%;\n}\n\n.Ktiem table.KtiemBorder td {\n  border: currentColor solid 1px;\n}\n\n.Ktiem table.KtiemBorder th {\n  border: currentColor solid 1px;\n}\n\n.Ktiem table.KtiemBSpec {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n  width: 100%;\n}\n\n.Ktiem table.KtiemBSpec td {\n  border: currentColor solid 1px;\n}\n\n.Nkski .NkskiData {\n  float: right;\n  width: 60%;\n}\n\n.Nkski .NkskiPh {\n  float: left;\n  text-align: center;\n  width: 35%;\n}\n\n.Nkski .NkskiTitle {\n  font-weight: bold;\n  line-height: 1.2em;\n}\n\n.Nkski table {\n  border: #ccc solid 1px;\n  border-collapse: collapse;\n}\n\n.Nkski td {\n  border: #ccc solid 1px;\n  text-align: center;\n}\n\n.Nkski th {\n  background-color: #dedfde;\n  border: #ccc solid 1px;\n}\n\n.Ktkei .KtkeiImg {\n  float: left;\n  width: 40%;\n}\n\ndiv.Kkirn {\n  margin: 0 auto;\n  width: 250px;\n}\n\n.Kkirn table {\n  border: #999 solid 1px;\n  border-collapse: collapse;\n  width: 250px;\n}\n\n.Kkirn table th {\n  background-color: #f7fff0;\n  padding: 4px 6px;\n  border: #999 solid 1px;\n}\n\n.Kkirn table td {\n  border: #999 solid 1px;\n  padding: 4px 6px;\n}\n\n.Tpkys div.paintBox p.txt {\n  float: left;\n  width: 400px;\n}\n\n.Tpkys div.paintBox p.img {\n  float: right;\n  width: 172px;\n}\n\n.Tpkys div.paintBox p.img span {\n  display: block;\n  padding-top: 2px;\n}\n\n.Tpkys div.stroke p.txt {\n  float: left;\n  width: 330px;\n}\n\n.Tpkys div.stroke p.img {\n  float: right;\n  width: 122px;\n}\n\n.Tpkys div.stroke div.imgArea {\n  float: right;\n  width: 249px;\n}\n\n.Tpkys div.stroke div.imgArea p {\n  float: left;\n  margin-right: 5px;\n  width: 122px;\n}\n\n.Tpkys div.stroke div.imgArea p.rightImg {\n  margin-right: 0;\n}\n\n.Tpkys div.sputtering p.img {\n  float: right;\n}\n\n.Tpkys .howtoBoxEnd {\n  float: left;\n}\n\n.Tpkys dl,\n.Tpkys dt,\n.Tpkys dd {\n  margin: 0;\n  padding: 0;\n}\n\n.Tpkys .closeBoxIn div {\n  border: #ccc dotted;\n  border-width: 1px 0 1px 1px;\n  float: left;\n  height: 350px;\n  padding: 10px;\n  width: 135px;\n}\n\n.Tpkys .closeBoxIn div.makeRight {\n  border: #ccc dotted 1px;\n  float: left;\n  height: 350px;\n  padding: 10px;\n  width: 135px;\n}\n\n.Hgksi table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n}\n\n.Srsbz .SrsbzLeft {\n  float: left;\n  width: 290px;\n}\n\n.Srsbz .SrsbzRight {\n  float: right;\n  width: 320px;\n}\n\n.Nrksm .NrksmMargin {\n  margin-top: 10px;\n}\n\n.Nrksm .NrksmSI .NrksmSIOne {\n  float: left;\n  margin: 0 0 0 10px;\n  text-align: center;\n}\n\n.Nrksm .NrksmT1 td {\n  padding: 2px 10px 2px 5px;\n  vertical-align: top;\n}\n\n.Nrksm .NrksmT1 th {\n  font-weight: bold;\n  padding: 2px 10px 2px 5px;\n  text-align: left;\n  vertical-align: top;\n  width: 100px;\n}\n\n.Nrksm .NrksmT2 caption {\n  font-weight: bold;\n  text-align: left;\n}\n\n.Nrksm .NrksmT2 table {\n  border-collapse: collapse;\n}\n\n.Nrksm .NrksmT2 td {\n  border: 1px solid #ccc;\n  padding: 2px 4px;\n}\n\n.Nrksm .NrksmT2 th {\n  background-color: #f5f5f5;\n  border: 1px solid #ccc;\n  font-weight: normal;\n  padding-top: 2px 4px;\n  text-align: center;\n}\n\n.Nrksm .NrksmT3 {\n  float: left;\n  margin: 5px;\n  width: 350px;\n}\n\n.Nimky .sew_toolBox .toolBoxIn p {\n  float: left;\n  width: 200px;\n}\n\n.Nkifr .NkifrImgTd {\n  vertical-align: top;\n}\n\n.Trhnt table {\n  border-collapse: collapse;\n  border: 1px solid #696969;\n  margin-top: 10px;\n  width: 300px;\n}\n\n.Trhnt .TrhntLeft {\n  float: left;\n  width: 260px;\n}\n\n.Trhnt .TrhntRight {\n  float: right;\n  width: 370px;\n}\n\n.Njsgs .hrLong {\n  border: none;\n  border-top: #696969 dotted 1px;\n  font-size: 1px;\n  height: 1px;\n  margin: 0 auto 0 0;\n  padding: none;\n  text-align: left;\n  width: 620px;\n}\n\n.Njsgs .hrShort {\n  border: none;\n  border-top: #696969 dotted 1px;\n  font-size: 1px;\n  height: 1px;\n  margin: 0 auto 0 0;\n  padding: none;\n  text-align: left;\n  width: 510px;\n}\n\n.Cryle table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n  text-align: center;\n}\n\n.Cryle td {\n  border: currentColor solid 1px;\n  text-align: center;\n}\n\n.Cryle .header {\n  border-left: #fff solid 1px;\n  border-right: #fff solid 1px;\n  border-top: #fff solid 1px;\n  font-weight: bold;\n  text-align: left;\n}\n\n.Jeepm table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n  text-align: center;\n}\n\n.Jeepm td {\n  border: currentColor solid 1px;\n  text-align: center;\n}\n\n.Jeepm .header {\n  border-left: #fff solid 1px;\n  border-right: #fff solid 1px;\n  border-top: #fff solid 1px;\n  font-weight: bold;\n  text-align: left;\n}\n\n.Dodge table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n  text-align: center;\n}\n\n.Dodge td {\n  border: currentColor solid 1px;\n  text-align: center;\n}\n\n.Dprss .tableBorder {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n  text-align: center;\n}\n\n.Dprss .tableBorder th {\n  background-color: #e6e6e6;\n  border: currentColor solid 1px;\n  text-align: center;\n}\n\n.Dprss .tableBorder td {\n  border: currentColor solid 1px;\n  text-align: center;\n}\n\n.Nrtdi dd {\n  border-right: #ccc solid 1px;\n  border-top: #ccc solid 1px;\n  margin-left: 13.3em;\n  padding: 4px;\n}\n\n.Nrtdi dl {\n  border-bottom: #ccc solid 1px;\n  border-left: #ccc 1px solid;\n  float: left;\n  margin: 0 0 10px;\n  width: 340px;\n}\n\n.Nrtdi dt {\n  border-right: #ccc solid 1px;\n  border-top: #ccc 1px solid;\n  float: left;\n  padding: 4px;\n  width: 12.7em;\n}\n\n.Nrtdi img {\n  margin-right: 10px;\n}\n\n.Hgnsh .clearfix {\n  width: 550px;\n}\n\n.Hgnsh .syosaiLeft {\n  float: left;\n  margin: 0 0 0 5px;\n  width: 290px;\n}\n\n.Hgnsh .syosaiLeftBox {\n  line-height: 1.4em;\n  margin-top: 15px;\n  text-align: justify;\n  text-justify: inter-ideograph;\n  width: 290px;\n}\n\n.Hgnsh .syosaiRight {\n  float: right;\n  margin-top: 5px;\n  width: 200px;\n}\n\n.Hgnsh .syosaiRightBox {\n  text-align: center;\n  width: 200px;\n}\n\n.Hgnsh .syosaiRightBox img {\n  margin-bottom: 0;\n  padding: 15px 0 15px;\n}\n\n.Hskks #syosai {\n  width: 550px;\n}\n\n.Hskks .syosaiLeft {\n  float: left;\n  margin: 0 0 0 5px;\n  width: 300px;\n}\n\n.Hskks .syosaiLeftBox {\n  line-height: 1.4em;\n  margin-top: 15px;\n  text-align: justify;\n  text-justify: inter-ideograph;\n  width: 300px;\n}\n\n.Hskks .syosaiRight {\n  float: right;\n  margin-top: 5px;\n  width: 200px;\n}\n\n.Hskks .syosaiRightBox {\n  text-align: center;\n  width: 200px;\n}\n\n.Hskks .syosaiRightBox img {\n  margin-bottom: 0;\n  padding: 15px 0 15px;\n}\n\n.Fkokk .FkokkTMargin {\n  margin: 0 0 10px;\n}\n\n.Fkokk .FkokkDMargin {\n  margin: 0 0 10px;\n}\n\n.Hinom .HinomTd {\n  text-align: left;\n}\n\n.Hinom .HinomTdBr2 {\n  text-align: right;\n}\n\n.Hinom .HinomTdDL {\n  border-bottom: #fff solid 1px;\n  text-align: left;\n}\n\n.Hinom .HinoTdJ {\n  border-top: #fff solid 1px;\n  text-align: left;\n  vertical-align: top;\n}\n\n.Hinom table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n  width: 100%;\n}\n\n.Hinom td {\n  border: currentColor solid 1px;\n  text-align: center;\n}\n\n.Grikt .GriktDiv {\n  border-top: #ccc solid 1px;\n  font-weight: bold;\n}\n\n.Grikt .GriktNavi-hanrei {\n  text-align: right;\n}\n\n.Grikt rt.GriktTg {\n  font-weight: bold;\n}\n\n.Grikt span.GriktTg {\n  text-decoration: underline;\n}\n\n.Gztmn .GztmnImgR {\n  float: right;\n}\n\n.Gztmn .GztmnTh {\n  border: currentColor solid 1px;\n}\n\n.Gztmn table {\n  border: currentColor solid 1px;\n  border-collapse: collapse;\n}\n\n.Gztmn td {\n  border: currentColor solid 1px;\n}\n\n.Nhnkz a.NhnkzAnchor {\n  background-image: url(https://weblio.hs.llnwd.net/e7/img/link_out.png);\n  background-position: right top;\n  background-repeat: no-repeat;\n  padding: 0 14px 0 0;\n}\n\n.Nhnkz .NhnkzData2 {\n  background-color: #eee;\n  border-collapse: collapse;\n}\n\n.Nhnkz .NhnkzData2 td {\n  background-color: #fff;\n  border: #ccc solid 2px;\n  padding: 3px;\n}\n\n.Nhnkz .NhnkzData2 th {\n  background-color: #eee;\n  border: #ccc solid 2px;\n  color: currentColor;\n  font-weight: normal;\n  padding: 3px;\n}\n\n.Nhnkz h2.NhnkzULine {\n  border-bottom: #ccc solid 1px;\n  font-size: 100%;\n  font-weight: bold;\n  margin-bottom: 5px;\n  margin-top: 5px;\n  padding: 3px 0 0 0;\n}\n\n.Mnjtn .MnjtnFont {\n  font-size: 10px;\n}\n\n.Fjtrs .textcenter {\n  text-align: center;\n}\n\n.Ksbdb .Ksbdbcell {\n  padding: 3px;\n  vertical-align: top;\n}\n\n.Ntwky .ntwkyRH {\n  font-weight: bold;\n  margin-bottom: 0;\n}\n\n.Ntwky .ntwkyRL {\n  margin-top: 0;\n}\n\n.Kkykc .kkykcL {\n  vertical-align: top;\n  padding-right: 16px;\n}\n\n.Kkykc .kkykcR {\n  vertical-align: top;\n}\n\n.Kcnys .kcnysPHC {\n  text-align: center;\n}\n\n.Kcnys .kcnysPH {\n  margin: 0 auto;\n  padding: 10px;\n  width: 500px;\n}\n\n.Kkkys .kkkysLi li {\n  list-style: none;\n}\n\n.Kkkys .kkkysLi span {\n  margin-left: -29px;\n}\n\n.Kkkys .kkkysLis li {\n  list-style: none;\n}\n\n.Kkkys .kkkysLis span {\n  margin-left: -21px;\n}\n\n.Sngjy .sngjyPD {\n  text-align: right;\n}\n\n.Sngjy .sngjyPgh {\n  font-size: .4em;\n}\n\n.Sngjy .sngjyBQ {\n  font-style: italic;\n  margin-top: 0;\n  margin-bottom: 0;\n}\n\n.Sngjy table {\n  font-size: .9em;\n}\n\n.Skazy table {\n  border-collapse: collapse;\n}\n\n.Skazy table td {\n  border: 1px currentColor solid;\n  padding: 4px;\n  vertical-align: top;\n}\n\n.Knjjn table {\n  margin: 5px 0 30px 0;\n}\n\n.Jmnep {\n  border-collapse: collapse;\n}\n\n.Jmnep table {\n  border-collapse: collapse;\n  border: 1px #696969 solid;\n  width: 40%;\n}\n\n.Jmnep table th {\n  background-color: #f5f5f5;\n  border: 1px #696969 solid;\n  font-weight: bold;\n  padding: 3px;\n  white-space: nowrap;\n}\n\n.Jmnep table td {\n  border: 1px #696969 solid;\n  padding: 5px;\n  line-height: 1.3em;\n}\n\n.Jmnep .JmnepL {\n  font-weight: bold;\n}\n\n.Jmnep .jmnepR {\n  width: 50%;\n}\n\n.Tkkyy p {\n  margin: 0 0 8px 0;\n}\n\n.Tkkyy .tkkyyH {\n  font-weight: bold;\n  margin: 0;\n}\n\n.Sgkkk p {\n  margin: 0 0 8px 0;\n}\n\n.Sgkkk img {\n  margin: 8px;\n}\n\n.Jdhky .jdhkyT {\n  border-top: 1px #ddd solid;\n  border-left: 1px #ddd solid;\n  margin-bottom: 1.5em;\n}\n\n.Jdhky .jdhkyT th,\n.Jdhky .jdhkyT td {\n  border-right: 1px #ddd solid;\n  border-bottom: 1px #ddd solid;\n  padding: 8px 10px;\n  vertical-align: middle;\n}\n\n.Jdhky .jdhkyT th {\n  background: #f6f6f6;\n  color: #006;\n  font-weight: bold;\n}\n\n.Jdhky .jdhkyT tr.jdhkyH th {\n  background: #608cc9;\n  color: #fff;\n  vertical-align: middle;\n}\n\n.Jdhky .jdhkyC {\n  text-align: center;\n}\n\n.Jdhky .jdhkyB {\n  margin: 0;\n  padding: 0;\n}\n\n.Jdhky .jdhkyBL {\n  float: left;\n  margin: 0;\n  padding: 10px 0 10px 0;\n  width: 400px;\n}\n\n.Jdhky .jdhkyBR {\n  float: right;\n  margin: 0;\n  padding: 10px 0 10px 0;\n  width: 220px;\n}\n\n.Jdhky .jdhkyInfo {\n  background-color: #f5f5f5;\n  border: #808080 solid 1px;\n  margin: 30px 0;\n  padding: 10px;\n}\n\n.Nhsgb table {\n  border-collapse: collapse;\n  width: 90%;\n  border: 1px solid #696969;\n}\n\n.Nhsgb td {\n  border: 1px solid #696969;\n  padding: 3px;\n}\n\n.Nhsgb .nhsgbL {\n  background-color: #ddd;\n  text-align: center;\n  width: 40px;\n}\n\n.Nhsgb .nhsgbR {\n  padding-left: 5px;\n}\n\n.Nhskb table {\n  border-collapse: collapse;\n  width: 90%;\n  border: 1px solid #696969;\n}\n\n.Nhskb td {\n  border: 1px solid #696969;\n  padding: 3px;\n}\n\n.Nhskb .nhskbL {\n  background-color: #ddd;\n  text-align: center;\n  width: 40px;\n}\n\n.Nhskb .nhskbR {\n  padding-left: 5px;\n}\n\n.Nkbjw td {\n  margin: 0;\n  padding: 0;\n  vertical-align: top;\n}\n\n.Nkbjw .nkbjwL {\n  float: left;\n  width: 440px;\n}\n\n.Nkbjw .nkbjwTL {\n  white-space: nowrap;\n  width: 100px;\n}\n\n.Nkbjw .nkbjwTL a {\n  white-space: nowrap;\n}\n\n.Nkbjw .nkbjwR {\n  float: right;\n  width: 185px;\n}\n\n.Nkbjw .nkbjwI {\n  border: currentColor solid 1px;\n}\n\n.Nkbjw .nkbjwD {\n  background-image: url(https://weblio.hs.llnwd.net/e7/img/link_out.png);\n  background-position: right center;\n  background-repeat: no-repeat;\n  margin-top: 0;\n  padding: 0 14px 0 0;\n  text-align: right;\n}\n\n.Pnkkj {\n  border-collapse: collapse;\n}\n\n.Pnkkj .pnkkjS {\n  font-weight: bold;\n  margin: 0;\n  padding: 15px 15px 20px 0;\n}\n\n.Pnkkj .pnkkjN {\n  font-weight: bold;\n}\n\n.Pnkkj .pnkkjA {\n  text-align: right;\n}\n\n.Pnkkj .pnkkjT {\n  border-collapse: collapse;\n  border: 1px #696969 solid;\n  width: 97%;\n}\n\n.Pnkkj .pnkkjT th {\n  background-color: #f5f5f5;\n  border: 1px #696969 solid;\n  font-weight: bold;\n  padding: 3px;\n  text-align: left;\n  white-space: nowrap;\n}\n\n.Pnkkj .pnkkjT td {\n  border: 1px #696969 solid;\n  padding: 5px;\n  line-height: 1.3em;\n}\n\n.Pnkkj .pnkkjOT {\n  border-collapse: collapse;\n  border: 1px #f5f5f5 solid;\n  width: 97%;\n}\n\n.Pnkkj .pnkkjOT th {\n  background-color: #f5f5f5;\n  border: 1px #f5f5f5 solid;\n  font-weight: normal;\n  padding: 3px;\n  text-align: left;\n  white-space: nowrap;\n}\n\n.Pnkkj .pnkkjOT td {\n  border: 1px #f5f5f5 solid;\n  padding: 5px;\n}\n\n.Pnkkj .pnkkjT .pnkkjTN {\n  font-weight: bold;\n}\n\n.Pnskj .pnskjA {\n  text-align: right;\n}\n\n.Pnskj .pnskjPS {\n  background-color: #eee;\n  border: dotted;\n  border-color: #ABC9A2;\n  border-width: 1px;\n  padding: 2px;\n}\n\n.Pnskj .pnskjPSH {\n  border: 1px #a9a9a9 dashed;\n  background: #eee;\n  font-size: small;\n  font-weight: bold;\n  margin: 0 8px 0 8px;\n  padding: 2px 10px;\n}\n\n.Pnskj .pnskjPSB {\n  margin: 0 10px 0 10px;\n}\n\n.Ykysb .ykysbTL {\n  width: 15%;\n}\n\n.Yznhg h2 {\n  margin: 1.5em 0 .8em;\n  padding: 1px 5px 1px 5px;\n  font-size: 130%;\n  background: url(https://weblio.hs.llnwd.net/e7/img/dict/yznhg/shared/templates/free/images/contents/h2_bg.gif) left bottom repeat-x;\n  border-top: 1px solid #9FB7D4;\n  border-right: 1px solid #9FB7D4;\n  border-bottom: 1px solid #9FB7D4;\n  border-left: 4px solid #05155C;\n}\n\n.Yznhg h3 {\n  margin: 1.5em 0 .8em;\n  padding: 0 5px 0 22px;\n  font-size: 120%;\n  background: url(https://weblio.hs.llnwd.net/e7/img/dict/yznhg/shared/templates/free/images/contents/h3_bg.gif) left center no-repeat;\n  border-bottom: 1px solid #9FB7D4;\n}\n\n.Tkdkb .tkdkbR {\n  margin: 20px 0 0 0;\n}\n\n.Tkdkb ul {\n  margin-top: 0;\n}\n\n.Jajcw h3,\n.Jajcw h4,\n.Jajcw h5 {\n  margin: 0 auto;\n  padding: 0;\n}\n\n.Jajcw h3 {\n  font-size: 1.0em;\n}\n\n.Jajcw .jwSubTtlH {\n  display: block;\n  font-size: 1.2em;\n  margin: 3px 0 1px 0;\n}\n\n.Jajcw .jwSubTtlH span {\n  border-left: #7f7f7f solid 5px;\n  line-height: 1.2em;\n  margin-left: 2px;\n  padding: 0 2px 0 5px;\n  text-decoration: none;\n}\n\n.Jajcw .jwjHdC {\n  // background-color: #eee;\n  border: #666 solid 1px;\n  // color: #363636;\n  font-size: .9em;\n  font-weight: normal;\n  padding: 1px;\n}\n\n.Jajcw p,\n.Jajcw dl {\n  margin: 1px 0 1px 5px;\n  padding: 0;\n}\n\n.Jajcw ol,\n.Jajcw ul {\n  margin: 0 auto;\n  padding: 0 auto;\n}\n\n.Jajcw .jajcwLastMd {\n  font-size: .8em;\n  text-align: right;\n  margin: 0;\n  padding: 0;\n}\n\n.Jajcw sup {\n  font-size: .8em;\n}\n\n.Jajcw .navFrmHd {\n  padding: 2px 10px 2px 0;\n  text-align: left;\n  width: 100%;\n}\n\n.Jajcw .navFrmHdB {\n  background-color: #efefef;\n  padding: 0 10px;\n}\n\n.Nhgkt .nhgktL {\n  background-color: #eee;\n  border: #999 solid 1px;\n  color: currentColor;\n  float: left;\n  font-size: .9em;\n  line-height: 1.0em;\n  margin: 8px;\n  padding: 1px;\n}\n\n.Nhgkt .nhgktR {\n  float: left;\n  font-size: .9em;\n  line-height: 1.0em;\n  margin: 4px;\n  padding: 1px;\n  width: 500px;\n}\n\n.Nhgkt .nhgktInfo {\n  border: #b5b6b5 solid 1px;\n  font-size: .8em;\n  line-height: 1.32em;\n  margin: 50px 0 0 0;\n  padding: 10px;\n  width: 500px;\n}\n\n.Nhgkt .nhgktD {\n  font-size: 6px;\n}\n\n.Zkksb table {\n  border: 1px #696969 solid;\n  border-collapse: collapse;\n  width: 97%;\n}\n\n.Zkksb table td {\n  border: 1px #696969 solid;\n  line-height: 1.3em;\n  padding: 5px;\n}\n\n.Zkksb .zkksbL {\n  background-color: #f5f5f5;\n  padding: 3px;\n  white-space: nowrap;\n  width: 100px;\n}\n\n.Szggj div#szggjJS {\n  margin: 0 0 16px;\n}\n\n.Szggj .medusa_cell {\n  padding: 6px;\n  width: 280px;\n}\n\n.Szggj .medusa_img_area {\n  float: left;\n  width: 50px;\n}\n\n.Szggj .medusa_text_area {\n  float: left;\n  width: 200px;\n}\n\n.Szggj .medusa_text_area p {\n  font-size: .8em;\n  line-height: .8em;\n  margin: 0;\n  padding: 0;\n}\n\n.Szggj div#medusa_cell1 {\n  float: left;\n}\n\n.Szggj div#medusa_cell2 {\n  float: right;\n}\n\n.Kkgys h2.kkgysS {\n  border-bottom: #ccc solid 1px;\n}\n\n.Kkgys .kkgysInfo {\n  background-color: #f5f5f5;\n  border: #808080 solid 1px;\n  margin: 30px 0;\n  padding: 10px;\n}\n\n.Kkgys table {\n  background-color: #ccc;\n  margin-left: 10px;\n}\n\n.Kkgys th {\n  background-color: #f5f2dc;\n  color: #353535;\n  padding: 8px;\n  text-align: left;\n}\n\n.Kkgys td {\n  background-color: #fff;\n  color: #353535;\n  padding: 8px;\n  text-align: left;\n}\n\n.Ingdj ol {\n  margin: 0;\n  padding: 0;\n}\n\n.Ingdj li {\n  margin-left: 22px;\n}\n\n.Ingdj .ingdjL {\n  // background-color: #f0f0f0;\n  border: #666 solid 1px;\n  // color: #363636;\n  font-size: .9em;\n  padding: 1px;\n}\n\n.Ingdj .ingdjInfo {\n  border: #b5b6b5 solid 1px;\n  font-size: .8em;\n  line-height: 1.32em;\n  margin: 10px 0 0 0;\n  padding: 10px;\n  width: 500px;\n}\n\n.Tnhgj .tnhgjInfo {\n  border: #b5b6b5 solid 1px;\n  font-size: .8em;\n  line-height: 1.32em;\n  margin: 50px 0 0 0;\n  padding: 10px;\n  width: 500px;\n}\n\n.Bngkt .bngktL {\n  background-color: #eee;\n  border: #999 solid 1px;\n  color: currentColor;\n  float: left;\n  font-size: .9em;\n  line-height: 1.0em;\n  margin: 8px;\n  padding: 1px;\n}\n\n.Bngkt .bngktR {\n  float: left;\n  font-size: .9em;\n  line-height: 1.0em;\n  margin: 4px;\n  padding: 1px;\n  width: 500px;\n}\n\n.Bngkt .bngktInfo {\n  border: #b5b6b5 solid 1px;\n  font-size: .8em;\n  line-height: 1.32em;\n  margin: 50px 0 0 0;\n  padding: 10px;\n  width: 500px;\n}\n\n.Bngkt .bngktD {\n  font-size: 6px;\n}\n\n.Sptjn .sptjnR {\n  float: right;\n}\n\n.Sptjn .sptjnL {\n  float: left;\n}\n\n.Efref .efrefTO {\n  background-color: #fff;\n  border-bottom: 1px solid #ccc;\n  color: #454545;\n  height: 40px;\n  padding: 0;\n  vertical-align: top;\n}\n\n.Efref .efrefTE {\n  background-color: #f3f3f3;\n  border-bottom: 1px solid #ccc;\n  border-top: 1px solid #ccc;\n  color: #454545;\n  height: 40px;\n  padding: 0;\n  vertical-align: top;\n}\n\n.Efref .efrefC {\n  border-collapse: collapse;\n  font-size: 12px;\n  margin-bottom: 20px;\n}\n\n.Efref .efrefC th {\n  background: none repeat scroll 0 0 #d8d8d8!important;\n  border-bottom: 1px solid #A4A4A4;\n  border-top: 1px solid #A4A4A4;\n  color: currentColor;\n  padding: 3px 10px 3px 5px;\n  text-align: left;\n}\n\n.Efref ul {\n  padding: 0;\n}\n\n.Efref ul li {\n  list-style: square;\n  list-style-position: inside;\n  padding: 0;\n  text-align: left;\n}\n\n.Efref h2 {\n  border-bottom: 1px solid #4d4d4d;\n}\n\n.Efref .efrefA {\n  color: #999;\n  font-size: 14px;\n  margin: 0 0 12px;\n}\n\n.Efref pre {\n  background-color: #eee;\n  padding: 8px;\n}\n\n.Efref .efrefCN {\n  background: none repeat scroll 0 0 #f9f9f9;\n  border: #eaeaea solid none;\n  border-width: 1px;\n  color: #454545;\n  font-size: 12px;\n  font-weight: normal;\n  margin: 20px 0 5px;\n  padding: 1px 2px 1px 1px;\n}\n\n.Efref .efrefCI36 {\n  margin-left: 44px;\n}\n\n.Wkpkm .wkpkmT {\n  margin: 0;\n  text-align: right;\n}\n\n.Wkpkm .wkpkmT {\n  margin-top: 16px;\n}\n\n.Wkpkm .wkpkmN {\n  margin-top: 16px;\n}\n\n.Wkpkm .wkpkmInfo {\n  border: #b5b6b5 solid 1px;\n  font-size: .8em;\n  line-height: 1.32em;\n  margin: 50px 0 0 0;\n  padding: 10px;\n  width: 500px;\n}\n\nh2.midashigo .cgkgjSm,\n.Cgkgj .cgkgjSm {\n  font-size: .6em;\n  margin-left: .4em;\n}\n\n.Hlddb * {\n  padding: 0;\n  margin: 0;\n}\n\n.Hlddb .hlddbC {\n  font-size: 18px;\n  font-weight: bold;\n  width: auto;\n}\n\n.Hlddb div.hlddbT {\n  border-collapse: collapse;\n  border-spacing: 0;\n  empty-cells: show;\n  margin: 0 0 20px 0;\n}\n\n.Hlddb div.hlddbT table {\n  font-size: 13px;\n  font-family: Arial;\n  width: 500px;\n}\n\n.Hlddb div.hlddbT table th {\n  text-align: left;\n  font-weight: normal;\n  padding: 0 0 0 5px;\n  margin: 0;\n}\n\n.Hlddb div.hlddbT table td {\n  margin: 0;\n  padding: 2px 0;\n}\n\n.Hlddb div.hlddbT table tr.hlddbO {\n  background-color: #EAEAEA;\n}\n\n.Hlddb ul,\n.Hlddb ol {\n  list-style-type: none;\n}\n\n.Gkjyj span {\n  // background-color: #f0f0f0;\n  border: #666 solid 1px;\n  // color: #363636;\n  font-size: .9em;\n  line-height: 1.0em;\n  margin-right: 5px;\n  padding: 1px;\n}\n\n.Tssmj p {\n  margin: 0 0 15px 0;\n}\n\n.Tssmj span {\n  // background-color: #f0f0f0;\n  border: #666 solid 1px;\n  // color: #363636;\n  font-size: .9em;\n  line-height: 1.0em;\n  margin-right: 5px;\n  padding: 1px;\n}\n\n.Dshar p {\n  word-break: normal;\n  word-wrap: break-word;\n}\n\n.Dshar .DsharC {\n  // background-color: #f0f0f0;\n  border: #666 solid 1px;\n  // color: #363636;\n  font-size: .9em;\n  line-height: 1.0em;\n  padding: 1px;\n}\n\n.Dshar .dsharInfo {\n  background-color: #f5f5f5;\n  border: #808080 solid 1px;\n  margin-top: 20px;\n  padding: 10px;\n}\n\n.Pdqgy .pdqgyInfo {\n  background-color: #ffd;\n  border: #b5b6b5 solid 1px;\n  font-size: .8em;\n  line-height: 1.5em;\n  margin: 30px 0 0 0;\n  padding: 10px;\n}\n\ntable.shosn {\n  width: 100%;\n}\n\ntable.shosn td {\n  vertical-align: top;\n}\n\n.Qqqdb .qqqdbInfo {\n  background-color: #f5f5f5;\n  border: #808080 solid 1px;\n  margin-top: 20px;\n  padding: 10px;\n}\n\n.wrpEx {\n  height: 25px;\n  margin: 0;\n}\n\n.wrpEx p {\n  color: #525152;\n  font-size: 1.0em;\n  height: 20px;\n  margin: 0 .5em 0 0;\n  padding: 3px 0 0 5px;\n}\n\n.wrpEx p a:link,\n.wrpEx p a:visited,\n.wrpEx p a:active {\n  color: #525152;\n  font-size: 1.0em;\n  text-decoration: none;\n}\n\n.wrpEx p span {\n  color: #9c9a9c;\n  font-size: .7em;\n}\n\n.wrpEx p.wrpExFL {\n  color: #525152;\n  font-size: .8em;\n  height: 20px;\n  margin: 0 .5em 0 0;\n  padding: 3px 0 0 5px;\n}\n\n.clr {\n  clear: both;\n  font-size: 0;\n  line-height: 0;\n  overflow: hidden;\n}\n\n.clrBc {\n  clear: both;\n  display: block;\n  font-size: 0;\n  line-height: 0;\n  overflow: hidden;\n}\n\n.contFtB {\n  line-height: 1.8em;\n  margin: 0;\n  padding: 10px 0 0 10px;\n}\n\n.contFtB li {\n  background-image: url(https://weblio.hs.llnwd.net/e7/img/icons/wRenew/iconPntBk.png);\n  background-repeat: no-repeat;\n  list-style: none;\n  padding: 0 0 0 8px;\n}\n\n.fndAnc b,\n.fwlAnc b {\n  display: none;\n}\n\n.contFtB a,\n.linkTl {\n  position: relative;\n  top: -8px;\n}\n\n#linkTag .linkTagR {\n  font-size: .8em;\n  font-weight: normal;\n  text-align: right;\n}\n\n.phraseWrp * {\n  line-height: 1.2em;\n}\n\n.phraseCtWrp {\n  margin-top: 0;\n}\n\n.phraseCtWrp p {\n  display: inline;\n}\n\n.phraseCtWrp table {\n  border: 0;\n  margin: 0;\n}\n\n.phraseCtWrp b {\n  font-size: .8em;\n}\n\n.phraseCtTtl {\n  background-color: #ebebeb;\n  font-size: .8em;\n  padding: 4px 2px 2px 6px;\n  text-align: left;\n  vertical-align: top;\n  width: 180px;\n}\n\n.phraseCtDes {\n  background-color: #f7f7f7;\n  font-size: .8em;\n  padding: 2px 5px;\n  text-align: left;\n  vertical-align: top;\n  width: 74%;\n}\n\n.phraseCtLink {\n  font-size: 1.0em;\n  margin: 2px 2px 0 0;\n  text-align: right;\n}\n\n.phraseCtLink a {\n  color: #848284;\n}\n\n.mainLeftAdWrp {\n  margin-bottom: 10px;\n  padding-left: 13px;\n}\n\n.mainLeftAdWrpL {\n  float: left;\n  margin-bottom: 10px;\n  margin-right: 40px;\n}\n\n.mainLeftAdWrpR {\n  margin-bottom: 10px;\n  float: left;\n}\n\n.trnsBtn {\n  background-color: transparent;\n  border: 0;\n  color: #fff;\n  cursor: hand;\n  cursor: pointer;\n  font-size: 13px;\n  font-weight: bold;\n  height: 20px;\n  left: 5px;\n  line-height: 22px;\n  margin: 0;\n  padding: 0;\n  position: relative;\n  top: 5px;\n  text-align: center;\n  width: 101px;\n  z-index: 20;\n}\n\n.trnsBtnWrp {\n  border: #a21a06 solid 1px;\n  display: block;\n  height: 20px;\n  left: 5px;\n  margin: 0 0 -10px 0;\n  position: relative;\n  top: -15px;\n  width: 100px;\n}\n\n.trnsBtnH {\n  background-color: #ca2109;\n  display: block;\n  font-size: 0;\n  height: 10px;\n  position: relative;\n  top: 0;\n  width: 100px;\n  z-index: 1;\n}\n\n.trnsBtnB {\n  background-color: #b81e07;\n  display: block;\n  font-size: 0;\n  height: 10px;\n  position: relative;\n  top: 0;\n  width: 100px;\n  z-index: 1;\n}\n\n.trnsMdlBxWrp {\n  background-color: #f5f5f5;\n  border: #dfdfdf solid 1px;\n  margin: 10px 0;\n  padding: 2px;\n  text-align: left;\n  width: 99%;\n}\n\n.trnsMdlBxB {\n  background-color: #e2e2e2;\n  padding: 0 8px 0 8px;\n}\n\n.trnsMdlBxTtlTbl {\n  font-size: 14px;\n  margin-bottom: 2px;\n  width: 100%;\n}\n\n.trnsMdlBxTtlL a {\n  color: currentColor;\n  font-weight: bold;\n}\n\n.trnsMdlBxTtlR {\n  text-align: right;\n}\n\n.trnsMdlBxDsc {\n  background-color: #e2e2e2;\n  width: 100%;\n}\n\n.trnsMdlBxTx {\n  font-size: 13px;\n  margin: 0 auto;\n  overflow: auto;\n  width: 100%;\n}\n\n.trnsMdlBxBtn {\n  vertical-align: middle;\n}\n\n.trnsMdlBxBtnTbl {\n  border-collapse: collapse;\n  font-size: 12px;\n  margin: 0 auto;\n  padding: 0;\n  width: 100%;\n}\n\n.trnsMdlBxBtnTblLB {\n  position: relative;\n  top: -4px;\n}\n\n.trnsMdlBxBtnTblL {\n  width: 80%;\n}\n\n:root *>.trnsMdlBxBtnTblL {\n  width: 83%;\n}\n\n.trnsMdlBxBtnTblL input {\n  position: relative;\n  top: 3px;\n}\n\n.copyRtWrp .lgDict {\n  float: left;\n}\n\n.pbarT {\n  border-bottom: #2b2b2b solid 6px;\n  border-collapse: collapse;\n  margin: 0;\n  padding: 0;\n  position: relative;\n  width: 100%;\n}\n\n.pbarTLW {\n  bottom: 0;\n  display: inline;\n  position: absolute;\n}\n\n.kijiHdCt {\n  display: inline;\n  font-size: 12px;\n  position: relative;\n  bottom: 6px;\n  z-index: 1;\n}\n\n.pbarTR {\n  text-align: right;\n}\n\n.wList {\n  margin: 0 10px 0 0;\n  padding: 0;\n}\n\n.kiji {\n  color: #111;\n  font-size: 1.0em;\n  line-height: 1.7em;\n  margin-bottom: 5px;\n}\n\n.kiji * {\n  font-size: 100%;\n  line-height: 1.7em;\n  white-space: normal;\n  word-break: break-all;\n}\n\n.kiji pre {\n  white-space: pre;\n}\n\n.kiji h2 {\n  font-size: 1.1em;\n}\n\nh2.midashigo {\n  font-size: 1.1em;\n  color: currentColor;\n}\n\n.hrDot {\n  border-style: dotted none none none;\n  border-top-width: 2px;\n  border-top-color: #c0c0c0;\n  margin: 0 0 15px 0;\n  padding: 0;\n}\n\n.SsdSml,\n.SsdSmlEx {\n  font-size: 12px;\n  padding-right: 10px;\n}\n\n.SsdSmlIE .SsdSml {\n  height: 0;\n}\n\n.SsdSmlL {\n  font-weight: bold;\n  float: left;\n}\n\n.SsdSmlR {\n  float: right;\n  text-align: right;\n  margin-top: 5px;\n\n  br {\n    display: none;\n  }\n}\n\n.SsdSmlCt {\n  background-color: #f7f7f7;\n  clear: both;\n  margin: 25px 0 0 8px;\n  padding: 2px 8px;\n}\n\n.SsdSmlRK {\n  color: var(--color-font-grey);\n  font-size: 12px;\n  padding: 3px 15px;\n}\n\n.fndAnc {\n  font-size: 13px;\n  margin: 0 0 -5px 0;\n  padding: 0;\n}\n\n.fndAnc b {\n  font-weight: normal;\n}\n\n.fwlAnc {\n  font-size: 13px;\n  margin: 0 0 -5px 0;\n  padding: 0;\n}\n\n.fwlAnc b {\n  font-weight: normal;\n}\n\n.content-foot-dict-ranking {\n  margin-bottom: 1em;\n\n  .dict-ranking-title {\n    margin-bottom: 5px;\n  }\n\n  .ranking-item {\n    display:flex;\n    flex-wrap:wrap;\n    margin-bottom: 5px;\n  }\n\n  .item{\n    padding-right:2.8em;\n\n    span{\n      padding-right:0.4em;\n    }\n\n    a:hover{\n      text-decoration:underline;\n    }\n  }\n}\n\n.dict-foot-guide{\n  position:relative;\n  // padding:20px 25px;\n  margin-top:1em;\n}\n\n.dict-foot-guide>div{\n  margin-bottom:10px;\n}\n\n.dict-foot-guide>div:last-child{\n  margin-bottom:0;\n}\n\n.dict-foot-guide .title{\n  font-size:1.2em;\n  font-weight:bold;\n  line-height:1.875;\n}\n\n.dict-foot-guide .word-list{\n  display:flex;\n  flex-wrap:wrap;\n  line-height:2.2;\n}\n\n.dict-foot-guide .word-list span{\n  margin-right:1.4em;\n}\n\n.dict-foot-guide .label{\n  line-height:2.14286;\n}\n\n.dict-foot-guide .label a{\n  color: var(--color-font-grey);\n  text-decoration:underline;\n}\n\n.dict-foot-guide .label a:hover{\n  text-decoration:none;\n}\n\n.linker-list>.lists>span.wList,\n.dict-foot-guide .fndAnc{\n  position:relative;\n}\n\n.linker-list>.lists>span.wList>a,\n.dict-foot-guide .fndAnc a{\n  padding-left:0.8em;\n}\n\n.linker-list>.lists>span.wList>a::before,\n.dict-foot-guide .fndAnc a::before{\n  content:'';\n  width:0;\n  height:0;\n  border-top:0.4em solid transparent;\n  border-bottom:0.4em solid transparent;\n  border-left:0.6em solid var(--color-font-grey);\n  position:absolute;\n  left:0;\n  top:0.2em;\n  color:var(--color-font-grey);\n}\n\n.dict-foot-guide .word-list a:hover,\n.dict-foot-guide .fndAnc a:hover{\n  text-decoration:underline;\n}\n"
  },
  {
    "path": "src/components/dictionaries/weblio/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type WeblioConfig = DictItem\n\nexport default (): WeblioConfig => ({\n  lang: '00010000',\n  selectionLang: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 265,\n  selectionWC: {\n    min: 1,\n    max: 20\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/weblio/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  HTMLString,\n  getInnerHTML,\n  handleNoResult,\n  handleNetWorkError,\n  getOuterHTML,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult,\n  getText,\n  removeChild\n} from '../helpers'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return `https://www.weblio.jp/content/${text}`\n}\n\nconst HOST = 'https://www.weblio.jp'\n\nexport type WeblioResult = Array<{\n  title: HTMLString\n  def: HTMLString\n}>\n\ntype WeblioSearchResult = DictSearchResult<WeblioResult>\n\nexport const search: SearchFunction<WeblioResult> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  return fetchDirtyDOM(\n    'https://www.weblio.jp/content/' +\n      encodeURIComponent(text.replace(/\\s+/g, ' '))\n  )\n    .catch(handleNetWorkError)\n    .then(handleDOM)\n}\n\nfunction handleDOM(\n  doc: Document\n): WeblioSearchResult | Promise<WeblioSearchResult> {\n  const result: WeblioResult = []\n  const $titles = doc.querySelectorAll<HTMLAnchorElement>(\n    '#cont>.pbarT .pbarTL>a'\n  )\n  doc\n    .querySelectorAll<HTMLDivElement>('#cont>.kijiWrp>.kiji')\n    .forEach(($dict, i) => {\n      const $title = $titles[i]\n      if (!$title) {\n        if (process.env.DEBUG) {\n          console.error(`Dict Weblio: missing title`)\n        }\n        return\n      }\n\n      if ($title.title === '百科事典') {\n        // too long\n        return\n      }\n\n      result.push({\n        title: getOuterHTML(HOST, $title, { config: {} }),\n        def: getInnerHTML(HOST, $dict, { config: {} })\n      })\n    })\n\n  if (result.length <= 0) {\n    doc.querySelectorAll('.section-card .basic-card').forEach($card => {\n      const title = getText($card, '.pbarT h2')\n      if (title) {\n        removeChild($card, '.pbarT')\n        result.push({\n          title,\n          def: getInnerHTML(HOST, $card, { config: {} })\n        })\n      }\n    })\n  }\n\n  return result.length > 0 ? { result } : handleNoResult()\n}\n"
  },
  {
    "path": "src/components/dictionaries/weblioejje/View.tsx",
    "content": "import React, { FC } from 'react'\nimport { WeblioejjeResult } from './engine'\nimport EntryBox from '@/components/EntryBox'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictWeblioejje: FC<ViewPorps<WeblioejjeResult>> = ({ result }) => (\n  <div>\n    {result.map((entry, i) =>\n      entry.title ? (\n        <EntryBox key={entry.title + i} title={entry.title}>\n          <StrElm html={entry.content} />\n        </EntryBox>\n      ) : (\n        <StrElm key={i} className=\"dictWeblioejje-Box\" html={entry.content} />\n      )\n    )}\n  </div>\n)\n\nexport default DictWeblioejje\n"
  },
  {
    "path": "src/components/dictionaries/weblioejje/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Weblio ejje\",\n    \"zh-CN\": \"Weblio 英和和英\",\n    \"zh-TW\": \"Weblio 英和和英\"\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/weblioejje/_style.shadow.scss",
    "content": ".Liscj .liscjWC {\n  font-style: italic;\n}\n\n.Jmdct .jmdctYm,\n.Jmdct .jmdctDm {\n  margin: 4px 0 4px 0;\n}\n\n.Jmdct .jmdctGlsL {\n  float: left;\n  margin: 0;\n  vertical-align: top;\n  width: 20px;\n}\n\n.Jmdct .jmdctGlsR {\n  float: left;\n  margin: 0;\n  vertical-align: top;\n}\n\n.Jmdct .jmdctL {\n  background-color: #f0f0f0;\n  border: #666 solid 1px;\n  color: #363636;\n  font-size: 0.9em;\n  font-weight: normal;\n  line-height: 1em;\n  margin-right: 8px;\n  padding: 1px;\n}\n\n.Jmdct table td.jmdctT {\n  text-align: right;\n  vertical-align: top;\n  width: 70px;\n}\n\n.Jmned .jmnedYm,\n.Jmned .jmnedDm,\n.Jmned .jmnedGls {\n  margin: 0;\n  padding: 0;\n}\n\n.Jmned {\n  border-collapse: collapse;\n}\n\n.Jmned table {\n  border-collapse: collapse;\n  border: 1px #696969 solid;\n  width: 100%;\n}\n\n.Jmned table th {\n  background-color: #f5f5f5;\n  border: 1px #696969 solid;\n  font-weight: bold;\n  padding: 3px;\n  white-space: nowrap;\n}\n\n.Jmned table td {\n  border: 1px #696969 solid;\n  padding: 5px;\n  line-height: 1.3em;\n}\n\n.Jmned .jmnedL {\n  font-weight: bold;\n}\n\n.Jmned .jmnedC {\n  width: 20%;\n}\n\n.Jmned .jmnedR {\n  width: 60%;\n}\n\n.Jmned .jmnedInfo {\n  background-color: #f5f5f5;\n  border: #b5b6b5 solid 1px;\n  font-size: 0.9em;\n  line-height: 1.62em;\n  margin: 1em 0 0 0;\n  padding: 10px;\n}\n\n.Stwdj .stwdjS {\n  border-left: #815733 solid 6px;\n  font-size: 1.3em;\n  font-weight: bold;\n  line-height: 1em;\n  margin: 10px 0 5px 0;\n  padding-left: 3px;\n}\n\n.Stwdj .stwdjC,\n.Stwdj .stwdjC a {\n  line-height: 1em;\n}\n\n.Stwdj .stwdjHdC {\n  background-color: #f0f0f0;\n  border: #666 solid 1px;\n  color: #363636;\n  font-size: 0.9em;\n  padding: 1px;\n}\n\n.Stwdj .stwdjYr {\n  margin-top: 10px;\n  padding: 5px;\n}\n\n.Stwdj .stwdjYr a {\n  color: #000;\n}\n\n.Stwdj .stwdjYr a:hover {\n  color: #000;\n}\n\n.Stwdj .stwdjYr a:active {\n  color: #000;\n}\n\n.Stwdj .stwdjYr a:visited {\n  color: #000;\n}\n\n.Stwdj .stwdjYr div {\n  line-height: 1em;\n  margin-left: 10px;\n}\n\n.Stwdj .stwdjYr sup {\n  line-height: 1em;\n  margin: 0;\n  padding: 0;\n}\n\n.Stwdj .stwdjYrHd i,\n.Stwdj .stwdjYrHdFld i {\n  margin-bottom: -2px;\n  margin-right: 3px;\n}\n\n.Stwdj .stwdjYrHd:before,\n.Stwdj .stwdjYrHdFld:before {\n  margin-right: 3px;\n}\n\n.Stwdj .stwdjYrHdFld img,\n.Stwdj .stwdjYrHdFld span {\n  cursor: pointer;\n}\n\n.Stwdj .stwdjYrHd .stwdjHdC {\n  border: #666 solid 1px;\n  color: #363636;\n  font-size: 0.9em;\n  margin-left: 16px;\n  padding: 1px;\n}\n\n.Stwdj .stwdjYrHdFld .stwdjHdC {\n  border: #666 solid 1px;\n  color: #363636;\n  font-size: 0.9em;\n  padding: 1px;\n}\n\n.Stwdj .stwdjR,\n.Stwdj .stwdjRF {\n  background-image: url(https://weblio.hs.llnwd.net/e7/img/iconCclBlS.png);\n  background-position: 2px 8px;\n  background-repeat: no-repeat;\n  font-family: Arial;\n  font-weight: normal;\n  margin: 2px 0 0 24px;\n  padding: 0 0 0 14px;\n}\n\n.Stwdj .stwdjRF {\n  display: none;\n}\n\n.Stwdj .stwdjBld {\n  font-weight: bold;\n  font-weight: bold;\n}\n\n.Stwdj .stwdjNH,\n.Stwdj .stwdjAH,\n.Stwdj .stwdjNB {\n  float: left;\n  margin: 0;\n  padding: 0;\n  vertical-align: bottom;\n}\n\n.Stwdj .stwdjNB {\n  margin: 0.3em 0 0 0;\n  padding: 0.1em 0 0 0;\n}\n\n.Edrnt .edrntC {\n  background-color: #f0f0f0;\n  border: #666 solid 1px;\n  color: #363636;\n  font-size: 0.9em;\n  padding: 1px;\n}\n\n.Jjabc ruby {\n  font-size: 120%;\n}\n\n.infobox.sisterproject {\n  font-size: 90%;\n  width: 20em;\n}\n\ntd.sub_yomi {\n  background-image: url(https://weblio.hs.llnwd.net/e7/img/jinmei_haikei1.png);\n  background-repeat: repeat-y;\n  border-color: #b5d2e2;\n  border-width: 1px 1px 0 0;\n  border-style: solid;\n  color: #4f519b;\n  font-weight: bold;\n  padding: 5px 5px 5px 15px;\n  width: 80px;\n}\n\ntd.item_yomi {\n  border-top: 1px dashed #c0c0c0;\n  padding: 5px 5px 5px 10px;\n}\n\n.Dkijt .kanren {\n  text-indent: 0.75em;\n}\n\n.Jeepm table {\n  border: #000 solid 1px;\n  border-collapse: collapse;\n  text-align: center;\n}\n\n.Jeepm td {\n  border: #000 solid 1px;\n  text-align: center;\n}\n\n.Jeepm .header {\n  border-left: #fff solid 1px;\n  border-right: #fff solid 1px;\n  border-top: #fff solid 1px;\n  font-weight: bold;\n  text-align: left;\n}\n\n.LiscjYr .kanren {\n  font-family: Arial;\n  margin: 5px 0 0 24px;\n}\n\n.Wkgje .wkgjeL {\n  float: left;\n  margin: 0 0 0 4px;\n  padding: 0;\n  width: 80px;\n}\n\n.Wkgje .wkgjeR {\n  float: left;\n  margin: 0 0 0 4px;\n  padding: 0;\n  width: 500px;\n}\n\n.Jawik .level1Block {\n  display: block;\n  font-size: 1.2em;\n  font-weight: normal;\n  margin: 2px 0 2px 10px;\n}\n\n.Jawik .level1Block b {\n  font-weight: bold;\n}\n\n.Jawik .level1,\n.Jawik .level2,\n.Jawik .level1Ex {\n  display: inline;\n}\n\n.Jawik .level1 b,\n.Jawik .level2 b,\n.Jawik .level2Block b {\n  background-color: #eee;\n  border: #999 solid 1px;\n  color: currentColor;\n  font-weight: normal;\n  padding: 0 2px;\n  margin: 0 5px 0 2px;\n}\n\n.Wehgj .wehgjT span,\n.Wehgj .wehgjE span,\n.Wehgj .wehgjR span {\n  border: #666 solid 1px;\n  font-size: 0.9em;\n  line-height: 1em;\n  padding: 1px;\n}\n\n.Wehgj .wehgjE,\n.Wehgj .wehgjR {\n  padding: 0 0 0 16px;\n}\n\n.Jfwik .level1Block {\n  display: block;\n  font-size: 1.2em;\n  font-weight: normal;\n  margin: 2px 0 2px 10px;\n}\n\n.Jfwik .level1Block b {\n  font-weight: bold;\n}\n\n.Jfwik .level1,\n.Jfwik .level2,\n.Jfwik .level1Ex {\n  display: inline;\n}\n\n.Jfwik .level1 b,\n.Jfwik .level2 b,\n.Jfwik .level2Block b {\n  background-color: #eee;\n  border: #999 solid 1px;\n  color: currentColor;\n  font-weight: normal;\n  padding: 0 2px;\n  margin: 0 5px 0 2px;\n}\n\n.wrpCmp {\n  border: #06c solid;\n  border-width: 0 0 1px 0;\n  height: 25px;\n  margin: 15px 0 5px 0;\n}\n\n.wrpCmp p {\n  border: #06c solid;\n  border-width: 0 0 0 5px;\n  font-size: 1.3em;\n  height: 20px;\n  margin: 0;\n  padding: 0 0 3px 5px;\n}\n\n.wrpCmp p a {\n  color: #000;\n  font-weight: bolder;\n}\n\n.wrpCmpCom {\n  border: #de7d29 solid;\n  border-width: 0 0 1px 0;\n  height: 25px;\n  margin: 15px 0 5px 0;\n}\n\n.wrpCmpCom p {\n  border: #de7d29 solid;\n  border-width: 0 0 0 5px;\n  font-size: 1.3em;\n  height: 20px;\n  margin: 0;\n  padding: 0 0 3px 5px;\n}\n\n.wrpCmpCom p a {\n  color: #000;\n  font-weight: bolder;\n}\n\n.wrpEx {\n  height: 25px;\n  margin: 0;\n}\n\n.wrpEx p {\n  color: #525152;\n  font-size: 1em;\n  height: 20px;\n  margin: 0 0.5em 0 0;\n  padding: 3px 0 0 5px;\n}\n\n.wrpEx p a:link,\n.wrpEx p a:visited,\n.wrpEx p a:active {\n  color: #525152;\n  font-size: 1em;\n  text-decoration: none;\n}\n\n.wrpEx p span {\n  color: #9c9a9c;\n  font-size: 0.7em;\n}\n\n.wrpEx p.wrpExFL {\n  color: #525152;\n  font-size: 0.8em;\n  height: 20px;\n  margin: 0 0.5em 0 0;\n  padding: 3px 0 0 5px;\n}\n\n.kijiEx {\n  color: #000;\n  font-size: 1em;\n  line-height: 1.8em;\n  margin-bottom: 15px;\n}\n\n.kijiEx * {\n  font-size: 100%;\n  line-height: 1.8em;\n}\n\n.wrpExTxt {\n  margin: 0 0 -8px 0;\n}\n\n.wrpExTxt p {\n  font-size: 1em;\n  margin-right: 0.5em;\n  padding: 0 0 0 5px;\n}\n\n.kijiWrpTxt p.adDes {\n  font-size: 95%;\n}\n\n.wrpExTxt p > a {\n  text-decoration: none;\n}\n\n.wrpExTxt p a:hover {\n  color: #f00;\n}\n\n.wrpExTxt p a:link,\n.wrpExTxt p a:visited {\n  color: #525152;\n}\n\n.kijiWrpTxt {\n  font-size: 90%;\n  margin: 0 0 15px 0;\n  padding: 0;\n}\n\n.wrpAdFTxt p {\n  color: #525152;\n  font-size: 0.9em;\n  line-height: 1.8em;\n  margin: 14px 0 14px 6px;\n}\n\n.wrpIMCmp {\n  border: #06c solid;\n  border-width: 0 0 1px 0;\n  height: 25px;\n  margin: 5px 0 5px 0;\n}\n\n.wrpIMCmp p {\n  border: #06c solid;\n  border-width: 0 0 0 5px;\n  font-size: 1.1em;\n  font-weight: bolder;\n  height: 18px;\n  margin: 0;\n  padding: 2px 0 5px 5px;\n}\n\n.wrpIMCmp p a {\n  color: #000;\n  font-size: 0.8em;\n  font-weight: normal;\n}\n\n.wrpIMCmpCom {\n  border: #de7d29 solid;\n  border-width: 0 0 1px 0;\n  height: 25px;\n  margin: 5px 0 5px 0;\n}\n\n.wrpIMCmpCom p {\n  border: #de7d29 solid;\n  border-width: 0 0 0 5px;\n  font-size: 1.1em;\n  font-weight: bolder;\n  height: 18px;\n  margin: 0;\n  padding: 2px 0 5px 5px;\n}\n\n.wrpIMCmpCom p a {\n  color: #000;\n  font-size: 0.8em;\n  font-weight: normal;\n}\n\n#main .spoBoxHEYO {\n  background-color: #feaa15;\n  border: #e38e00 solid;\n  border-width: 0 0 1px 0;\n  color: #fff;\n  font-weight: bold;\n  margin: 0 4px 0 20px;\n  padding: 5px 3px 5px 7px;\n  text-align: left;\n}\n\n#main .spoBoxBEYO {\n  border: #ccc solid;\n  border-width: 0 1px 1px 1px;\n  margin: 0 4px 10px 20px;\n}\n\n#main .spoBoxHEYOT {\n  background-color: #9097a2;\n  border: #868d99 solid;\n  border-width: 0 0 1px 0;\n  color: #fff;\n  font-weight: bold;\n  margin: 0 4px 0 0;\n  padding: 5px 3px 5px 7px;\n  text-align: left;\n}\n\n#main .spoBoxBEYOT {\n  border: #ccc solid;\n  border-width: 0 1px 1px 1px;\n  margin: 0 4px 10px 0;\n}\n\n.ejjeScCnt {\n  margin: 0 0 10px 0;\n  width: 99%;\n  _width: 100%;\n}\n\n.ejjeScCnt .chsShwcsH {\n  background-color: #feaa15;\n  border: #feaa15 solid 1px;\n  border-bottom: #e38e00 solid 1px;\n  color: #fff;\n  font-size: 1em;\n  font-weight: bold;\n  margin: 0;\n  padding: 4px 0;\n  width: 100%;\n}\n\n.ejjeScCnt .chsShwcsHT {\n  padding: 0 5px;\n}\n\n.ejjeScCnt .chsShwcsC {\n  border: #ccc solid;\n  border-width: 0 1px 1px 1px;\n  margin: 0;\n  padding: 2px 0 3px 0;\n  width: 100%;\n}\n\n.ejjeScCnt .chsShwcsT {\n  border-collapse: collapse;\n  margin: 2px 0 2px 0;\n  padding: 0;\n  width: 100%;\n}\n\n.ejjeScCnt .chsShwcsTD {\n  border-collapse: collapse;\n  margin: 0;\n  padding: 2px 0 2px 6px;\n  _padding: 2px 0 2px 2px;\n  vertical-align: top;\n  width: 33%;\n}\n\n.ejjeScCnt .chsShwcsTD a {\n  font-size: 1em;\n}\n\n.ejjeScCnt .chsShwcsTD a:active,\n.ejjeScCnt .chsShwcsTD a:link,\n.ejjeScCnt .chsShwcsTD a:visited {\n  color: #06c;\n}\n\n.ejjeScCnt .chsShwcsTD a:hover {\n  color: #d50000;\n}\n\n.ejjeScCnt .chsShwcsTD span {\n  font-size: 0.85em;\n  font-weight: normal;\n}\n\n.treeBoxC .adIFLeftE {\n  color: #06c;\n  font-size: 0.7em;\n  text-align: right;\n}\n\n.JWAdsR .highlight {\n  background-color: transparent;\n}\n\n.AdR .highlight {\n  background-color: transparent;\n}\n\n.clrBc {\n  clear: both;\n  display: block;\n  font-size: 0;\n  line-height: 0;\n  overflow: hidden;\n}\n\n.highlight-menu {\n  background-color: #f8f9ff;\n  display: none;\n  top: 0;\n  height: auto;\n  position: absolute;\n  left: 0;\n  z-index: 10;\n}\n\n.highlight-menu table {\n  background-color: #f6f6f6;\n  border: #b5b5b5 solid 1px;\n  border-collapse: separate;\n  border-spacing: 4px;\n}\n\n.highlight-menu table td {\n  cursor: pointer;\n  height: 24px;\n  margin: 0;\n  text-align: center;\n  padding: 0;\n  width: 24px;\n}\n\n.highlight-menu img {\n  width: 24px;\n}\n\n.highlight-menu #pick-del img {\n  width: 22px;\n}\n\n.red {\n  background-color: #fcc;\n}\n\n.highlight-menu-sub {\n  position: relative;\n}\n\n.highlight-menu table.hl-tbl-hz td {\n  padding-right: 5px;\n}\n\n.highlight-menu table.hl-tbl-hz td:last-child {\n  padding-right: 0;\n}\n\n.highlight-dfcl .hl-pick-dfcolor-table {\n  margin: 37px 0 15px;\n  table-layout: fixed;\n  width: 100%;\n}\n\n.highlight-dfcl .hl-pick-dfcolor-table td {\n  padding: 0;\n  text-align: left;\n}\n\n.highlight-dfcl .dfclsq {\n  box-sizing: border-box;\n  cursor: pointer;\n  display: inline-block;\n  height: 40px;\n  line-height: 40px;\n  text-align: center;\n  vertical-align: middle;\n  width: 40px;\n}\n\n.highlight-dfcl .dfclsq > span {\n  background-color: #585858;\n  display: none;\n  height: 8px;\n  margin-top: 16px;\n  width: 8px;\n}\n\n.highlight-dfcl #dfslsq-yellow {\n  background-color: #ffe467;\n}\n\n.highlight-dfcl #dfslsq-blue {\n  background-color: #75baff;\n}\n\n.highlight-dfcl #dfslsq-green {\n  background-color: #82d131;\n}\n\n.highlight-dfcl #dfslsq-red {\n  background-color: #ffa4a4;\n}\n\n.highlight-dfcl .dfclmsg-yellow,\n.highlight-dfcl .dfclmsg-blue,\n.highlight-dfcl .dfclmsg-green,\n.highlight-dfcl .dfclmsg-red {\n  font-size: 0.78em;\n}\n\n.highlight-dfcl .hl-pick-dfcolor-table td.dfclmsg-td {\n  padding-left: 11px;\n}\n\n.highlight-htu .sect {\n  line-height: 1.7em;\n  margin-bottom: 23px;\n}\n\n.highlight-htu .sect ol {\n  margin: 0;\n  padding-left: 16px;\n}\n\n.highlight-htu .semi-heading {\n  font-size: 1.07em;\n  font-weight: bold;\n  margin-bottom: 3px;\n}\n\n.highlight-htu .hl-page-heading {\n  margin-bottom: 23px;\n}\n\n.highlight-lib .formBlk {\n  overflow: hidden;\n}\n\n.highlight-lib .hl-sortWrp {\n  float: left;\n}\n\n.highlight-lib .hl-searchWrp {\n  float: right;\n}\n\n.highlight-lib .searchHlBtn {\n  background-color: var(--color-font-grey);\n  box-shadow: 0 2px 0 0 #000;\n  font-size: 0.92em;\n  height: 25px;\n  line-height: 25px;\n  margin: 0;\n  padding: 0 10px;\n}\n\n.highlight-lib input[name='q'] {\n  border: 0;\n  border: solid 1px #ccc;\n  border-radius: 2px;\n  font-family: Arial, sans-serif;\n  height: 27px;\n  margin: 0 3px 0 0;\n  padding-left: 3px;\n}\n\n.highlight-lib .hl-content-wrap {\n  margin-top: 18px;\n}\n\n.highlight-lib .hl-content {\n  margin-bottom: 39px;\n}\n\n.highlight-lib .hl-title {\n  margin-bottom: 12px;\n  font-weight: bold;\n}\n\n.highlight-lib .hl-item {\n  background-color: #f9f9f9;\n  margin-bottom: 5px;\n  padding: 7px 10px;\n  position: relative;\n}\n\n.highlight-lib .hl-item p {\n  border-style: solid;\n  border-width: 0 0 0 4px;\n  float: left;\n  padding-left: 11px;\n  width: 93%;\n}\n\n.red-left-border {\n  border-color: #ffa4a4;\n}\n\n.highlight-lib .hl-item-edit {\n  cursor: pointer;\n  display: none;\n  float: right;\n}\n\n.highlight-lib .hl-more-less,\n.highlight-lib .hl-title a {\n  border-bottom: #000 dotted 1px;\n  font-size: 0.85em;\n  padding-bottom: 1px;\n  text-decoration: none;\n}\n\n.highlight-lib .highlight-edit {\n  display: none;\n  right: -38px;\n  top: 0;\n  position: absolute;\n  z-index: 1;\n}\n\n.highlight-lib .highlight-edit table {\n  background-color: #f6f6f6;\n  border: #b5b5b5 solid 1px;\n  border-collapse: separate;\n  border-spacing: 4px;\n}\n\n.highlight-lib .highlight-edit table td {\n  cursor: pointer;\n  height: 24px;\n  margin: 0;\n  text-align: center;\n  padding: 0;\n  width: 24px;\n}\n\n.highlight-lib .highlight-edit table div {\n  height: 100%;\n  line-height: 24px;\n}\n\n.highlight-lib .highlight-edit .edit-del img {\n  width: 24px;\n}\n\n.highlight-lib .hlpaginationWrp {\n  text-align: center;\n}\n\n.highlight-lib .hl-pagination {\n  border-radius: 4px;\n  display: inline-block;\n  margin: 20px 0;\n  padding-left: 0;\n}\n\n.highlight-lib .hl-pagination > li {\n  display: inline;\n}\n\n.highlight-lib .hl-pagination > li:first-child > a {\n  border-top-left-radius: 4px;\n  border-bottom-left-radius: 4px;\n  margin-left: 0;\n}\n\n.highlight-lib .hl-pagination > li:last-child > a {\n  border-top-right-radius: 4px;\n  border-bottom-right-radius: 4px;\n}\n\n.highlight-lib .hl-pagination > li > a {\n  background-color: #fff;\n  border: 1px solid #ddd;\n  color: #337ab7;\n  float: left;\n  line-height: 1.42857143;\n  margin-left: -1px;\n  position: relative;\n  padding: 2px 10px;\n  text-decoration: none;\n}\n\n.highlight-lib .hl-pagination > li i {\n  line-height: 1.42857143;\n}\n\n.highlight-lib .hl-pagination a:link,\n.highlight-lib .hl-pagination a:visited,\n.highlight-lib .hl-pagination a:hover,\n.highlight-lib .hl-pagination a:active {\n  color: currentColor !important;\n  display: inline-block;\n  text-decoration: none;\n}\n\n.highlight-lib .hl-pagination a:hover {\n  background-color: #aac9e9;\n}\n\n.highlight-lib .hl-pagination .active a {\n  background-color: #e4e4e4;\n  cursor: default;\n}\n\n#hl-sticky-menu-library .fa {\n  color: currentColor;\n  font-size: 1.64em;\n}\n\n.stickyMenuSampleWrp .smClPk .fa {\n  display: inline-block;\n  font-size: 1.92em;\n  text-align: center;\n  width: 24px;\n}\n\n.introjs-arrow.right {\n  right: -10px;\n  top: 10px;\n  border-top-color: transparent;\n  border-right-color: transparent;\n  border-bottom-color: transparent;\n  border-left-color: white;\n}\n\n.introjs-arrow.right-bottom {\n  bottom: 10px;\n  right: -10px;\n  border-top-color: transparent;\n  border-right-color: transparent;\n  border-bottom-color: transparent;\n  border-left-color: white;\n}\n\n.introjs-arrow.left {\n  left: -10px;\n  top: 10px;\n  border-top-color: transparent;\n  border-right-color: white;\n  border-bottom-color: transparent;\n  border-left-color: transparent;\n}\n\n.introjs-arrow.left-bottom {\n  left: -10px;\n  bottom: 10px;\n  border-top-color: transparent;\n  border-right-color: white;\n  border-bottom-color: transparent;\n  border-left-color: transparent;\n}\n\n.pinned-content-header .fa-thumb-tack,\n.pinned-content-header span .fa-square {\n  color: currentColor;\n}\n\n.fa-rotate-315 {\n  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3.5);\n  -webkit-transform: rotate(315deg);\n  -ms-transform: rotate(315deg);\n  -moz-transform: rotate(315deg);\n  -o-transform: rotate(315deg);\n  transform: rotate(315deg);\n}\n\n.pinned-content-button-wrapper > .error {\n  font-size: 0.71em;\n  background-color: #f8ddde;\n  text-align: center;\n  margin-bottom: 5px;\n  display: none;\n}\n\n.pinned-content-button-wrapper > .error {\n  letter-spacing: 1.4;\n  padding: 2px 0;\n  width: 80%;\n  margin: 0 auto 10px;\n}\n\n.pinned-content-button-wrapper > .error a {\n  font-weight: bold;\n  color: #000;\n}\n\n.pinned-content-button-wrapper.show > .error {\n  display: block;\n}\n\n.pinned-content-button-wrapper .add-word-list-button .fa {\n  margin-right: 5px;\n}\n\n.modal-wrapper .loading .fa {\n  color: #fff;\n  font-size: 6.42em;\n  left: 38%;\n  position: absolute;\n  top: 40%;\n}\n\n.modal-wrapper.for-already-exists .modal-close-wrapper .fa-stack {\n  position: absolute;\n  right: 0;\n  top: 0;\n  transform: scale(0.5);\n  cursor: pointer;\n}\n\n.modal-wrapper.for-already-exists .modal-close-wrapper .fa-stack:hover {\n  opacity: 0.7;\n}\n\n.free-member-features-modal .modal-message:not(.registered):not(.login) {\n  float: left;\n  margin: 10px 0 20px 30px;\n}\n\n.free-member-features-modal .modal-message div {\n  font-weight: normal;\n  margin: 0 !important;\n}\n\n.free-member-features-modal .modal-message div:first-child {\n  margin: 0 0 15px 0 !important;\n}\n\n.free-member-features-modal .modal-message div:first-child span {\n  font-size: 2em;\n}\n\n.free-member-features-modal .modal-message div:first-child span:first-child {\n  font-weight: bold;\n}\n\n.free-member-features-modal .modal-message div:not(:first-child) {\n  font-size: 1.28em;\n}\n\n.free-member-features-modal .modal-message div.sentence-list-features {\n  line-height: 2em;\n}\n\n.free-member-features-modal .modal-message:not(.registered) .modal-login-link {\n  margin: 0;\n}\n\n.free-member-features-modal\n  .modal-message:not(.registered)\n  .modal-login-link:hover {\n  color: currentColor;\n  filter: alpha(opacity=70);\n  opacity: 0.7;\n}\n\n.free-member-features-modal .modal-example {\n  float: none;\n  margin: 5px 0 0;\n}\n\n.free-member-features-modal .modal-example img {\n  box-shadow: none;\n\n  -webkit-box-shadow: none;\n}\n\n.free-member-features-modal .free-register-wrap:not(.login):not(.registered) {\n  border-collapse: separate;\n  border-spacing: 15px 0;\n  display: table;\n  margin-left: 15px;\n}\n\n.free-member-features-modal .free-member-features {\n  border: #aaa solid 1px;\n  display: table-cell;\n  vertical-align: top;\n}\n\n.free-member-features-modal .free-member-features > div:first-child {\n  background-color: #ea9035;\n  color: #fff;\n  font-size: 1.42em;\n  padding: 7px 0;\n}\n\n.free-member-features-modal .free-member-features .free-member-feature-content {\n  display: inline-block;\n  margin: 20px 10px;\n}\n\n.free-member-features-modal\n  .free-member-features\n  .free-member-feature-content\n  img {\n  margin: 10px 0;\n}\n\n.free-member-features-modal\n  .free-member-features\n  .free-member-feature-content\n  div {\n  text-align: left;\n}\n\n.free-member-features-modal .free-member-features > div:last-child {\n  margin-bottom: 10px;\n}\n\n.free-member-features-modal .free-member-features > div:last-child span {\n  font-weight: bold;\n}\n\n.free-member-features-modal\n  .free-register-wrap:not(.login):not(.registered)\n  .free-register {\n  border: #48a267 solid 1px;\n  display: table-cell;\n  position: relative;\n}\n\n.free-member-features-modal .free-register > div:first-child {\n  left: 3px;\n  position: absolute;\n  top: -30px;\n}\n\n.free-member-features-modal\n  .free-register-wrap.login\n  .free-register\n  > div:first-child,\n.free-member-features-modal\n  .free-register-wrap.registered\n  .free-register\n  > div:first-child {\n  display: none;\n}\n\n.free-member-features-modal\n  .free-register-wrap:not(.login)\n  .free-register\n  .modal-error-message {\n  margin-top: 15px;\n}\n\n.free-member-features-modal .free-register .footer span {\n  line-height: 14px;\n}\n\n.free-member-features-modal .free-register .modal-register-button {\n  font-weight: normal;\n  margin: 10px auto;\n}\n\n.free-member-features-modal .free-register .free-register-link {\n  display: inline-block;\n  margin-bottom: 10px;\n}\n\n.free-member-features-modal .free-register .free-register-link:hover {\n  color: currentColor;\n  filter: alpha(opacity=70);\n  opacity: 0.7;\n}\n\n.free-member-features-modal.hidden .free-member-features {\n  display: none;\n}\n\n.userInfo .right-cell {\n  padding-left: 5px;\n}\n\n.userInfo .right-cell .free-description {\n  font-size: 1em;\n  margin-left: 6px;\n  margin-bottom: 8px;\n  letter-spacing: 3px;\n}\n\n.userInfo .right-cell .free-description span {\n  letter-spacing: 0;\n  padding-right: 3px;\n}\n\n.userInfo .right-cell .free-description span span {\n  font-size: 0.85em;\n}\n\n.userInfo .right-cell .free-button {\n  display: block;\n  height: 35px;\n\n  margin: 6px 0 0;\n}\n\n.userInfo .right-cell .free-button:hover {\n  opacity: 0.7;\n}\n\n.userInfo .right-cell .free-button:active {\n  box-shadow: none !important;\n  position: relative;\n  top: 2px;\n}\n\n.userInfo .right-cell .merits {\n  margin: 4px 0;\n}\n\n.userInfo .right-cell .merits p {\n  font-size: 0.78em;\n  font-weight: bold;\n  margin-bottom: 4px;\n}\n\n.userInfo .right-cell .merits p img {\n  width: 12px;\n  padding: 0 8px;\n}\n\n.userInfo .left-cell {\n  text-align: center;\n}\n\n.userInfo .left-cell .member-state-label {\n  color: #fff;\n  display: block;\n  width: 80px;\n  text-align: center;\n  height: 30px;\n  box-sizing: border-box;\n  padding: 5px 0;\n  font-size: 90%;\n}\n\n.userInfo .left-cell .member-state-label.free {\n  background-color: #48a267;\n}\n\n.userInfo .left-cell .member-state-label.premium {\n  background-color: #ea9034;\n  font-size: 70%;\n  padding: 6.3px 0;\n}\n\n#sideBHPAEjje > .error {\n  background-color: #f8ddde;\n  display: none;\n  font-size: 0.71em;\n  letter-spacing: 1.4;\n  margin-bottom: 5px;\n  padding: 0 5px;\n  text-align: center;\n}\n\n#sideBHPAEjje > .error a {\n  color: #000;\n  font-weight: bold;\n}\n\n#sideBHPAEjje.show > .error {\n  display: block;\n}\n\n.footer_banner .title .small {\n  font-size: 0.92em;\n}\n\n.footer_banner ul.features {\n  font-size: 0.85em;\n  list-style-type: none;\n  color: #534a41;\n  margin: 0;\n  padding: 10px 0;\n  line-height: 17px;\n}\n\n.footer_banner ul.features li {\n  margin-left: 70px;\n  padding: 5px 0;\n  position: relative;\n}\n\n.footer_banner ul.features li:before {\n  content: '';\n  width: 17px;\n  height: 17px;\n  background-position: center center;\n  background-repeat: no-repeat;\n  position: absolute;\n  top: 5px;\n  left: -25px;\n}\n\n.footer_banner ul.features li.search-history:before {\n  background-image: url(https://weblio.hs.llnwd.net/e7/img/banner/history.png);\n}\n\n.footer_banner ul.features li.vocab-test:before {\n  background-image: url(https://weblio.hs.llnwd.net/e7/img/banner/vocab.png);\n}\n\n.footer_banner ul.features li.folders:before {\n  background-image: url(https://weblio.hs.llnwd.net/e7/img/banner/folders.png);\n}\n\n.footer_banner ul.features li.ads:before {\n  background-image: url(https://weblio.hs.llnwd.net/e7/img/banner/ad.png);\n}\n\n#free-house-ad-on-load-modal-container .header-wrap .modal-close i.fa-times,\n#premium-house-ad-on-load-modal-container .header-wrap .modal-close i.fa-times {\n  color: #595858;\n  font-size: 1.71em;\n  height: 35px;\n  line-height: 35px;\n}\n\n#free-house-ad-on-load-modal-container .features-wrapper {\n  display: flex;\n  flex-wrap: wrap;\n  margin: 25px 5px 10px 5px;\n}\n\n#free-house-ad-on-load-modal-container .features-wrapper .feature {\n  width: calc(33.3333% - 12px);\n  padding: 20px 0 5px;\n  font-size: 1em;\n  text-align: center;\n  vertical-align: top;\n  border: 1px solid #d1d1d1;\n  position: relative;\n  margin: 10px 5px;\n}\n\n#free-house-ad-on-load-modal-container\n  .features-wrapper\n  .feature.other-features {\n  border: none;\n}\n\n#free-house-ad-on-load-modal-container\n  .features-wrapper\n  .feature.other-features\n  img {\n  width: 75%;\n}\n\n#free-house-ad-on-load-modal-container\n  .features-wrapper\n  .feature.other-features:before {\n  display: none;\n}\n\n#free-house-ad-on-load-modal-container .features-wrapper .feature:before {\n  background-color: #e99034;\n  border-radius: 50%;\n  content: '0' counter(feature-number);\n  counter-increment: feature-number;\n  color: #fff;\n  display: inline-block;\n  font-size: 1em;\n  height: 30px;\n  left: 60px;\n  line-height: 30px;\n  position: absolute;\n  top: -15px;\n  width: 30px;\n}\n\n#free-house-ad-on-load-modal-container .features-wrapper .feature span {\n  font-size: 1.28em;\n  font-weight: bold;\n  color: #554c45;\n}\n\n#free-house-ad-on-load-modal-container .features-wrapper .feature div.img-wrap {\n  display: table-cell;\n  height: 65px;\n  text-align: center;\n  vertical-align: middle;\n}\n\n#free-house-ad-on-load-modal-container\n  .features-wrapper\n  .feature\n  div.img-wrap\n  img {\n  width: 40px;\n}\n\n#free-house-ad-on-load-modal-container\n  .features-wrapper\n  .feature\n  div.description {\n  line-height: 18px;\n}\n\n#free-house-ad-on-load-modal-container td div.description,\n#premium-house-ad-on-load-modal-container td div.description {\n  line-height: 18px;\n}\n\n#free-house-ad-on-load-modal-container\n  .features-wrapper\n  .feature\n  .img-wrap\n  img.large {\n  width: 50px;\n}\n\n.clr {\n  clear: both;\n  font-size: 0;\n  line-height: 0;\n  overflow: hidden;\n}\n\n.clrBc {\n  clear: both;\n  display: block;\n  font-size: 0.71em;\n  line-height: 0;\n  overflow: hidden;\n}\n\n#main {\n  float: left;\n  text-align: left;\n  padding: 0 0 0 8px;\n}\n\n.topic {\n  font-size: 75%;\n  height: 44px;\n  line-height: 1.3em;\n  margin: 0 0 2px 0;\n  overflow: hidden;\n  padding-top: 5px;\n  width: 100%;\n  word-break: break-all;\n}\n\n.topic table {\n  height: 30px;\n  width: 100%;\n}\n\n.topicL {\n  font-size: 0.78em;\n  float: left;\n  vertical-align: middle;\n}\n\n.topicLB {\n  font-size: 0.71em;\n}\n\n.topicR {\n  float: right;\n  text-align: right;\n}\n\n.topicR table {\n  border-collapse: collapse;\n  font-size: 1.1em;\n}\n\n.wrp {\n  width: 100%;\n  border-collapse: collapse;\n}\n\n.wrp img.lgDictLg {\n  max-height: 16px !important;\n  width: auto !important;\n}\n\n.wrp img.lgDictSp {\n  height: 18px !important;\n  margin-top: -17px !important;\n}\n\n.left {\n  vertical-align: bottom;\n}\n\n.right {\n  float: right;\n  font-size: 0.9em;\n  margin: 0 5px 0 0;\n  text-align: right;\n  vertical-align: top;\n}\n\n.kijiWrp {\n  padding-left: 1px;\n}\n\n.kijiWrp .lgDict {\n  float: right;\n  margin: 1px 1px 0 0;\n}\n\n.kiji {\n  color: currentColor;\n  font-size: 1em;\n  line-height: 1.8em;\n}\n\n.kiji * {\n  font-size: 100%;\n  line-height: 1.8em;\n}\n\n.midashigo:before {\n  content: '\\25b6';\n}\n\n.midashigo {\n  font-size: 115%;\n  letter-spacing: 0.02em;\n}\n\ncrosslink:hover,\n.crosslink:link,\n.crosslink:visited,\n.crosslink:active {\n  color: currentColor;\n  text-decoration: underline;\n}\n\n.kijiFoot {\n  margin-top: 10px;\n  text-align: right;\n}\n\n.linkTagML {\n  width: 60%;\n}\n\n.linkTagML input {\n  border: #ccc solid 1px;\n  height: 20px;\n}\n\n.linkTagMR {\n  text-align: right;\n  vertical-align: bottom;\n  width: 38%;\n}\n\n.linkTagMR table {\n  border-collapse: collapse;\n  float: right;\n  font-size: 80%;\n}\n\n.linkTagMR table td {\n  vertical-align: bottom;\n}\n\n.linkOut {\n  height: 12px;\n  margin-left: 2px;\n  width: 13px;\n}\n\n.intrstR > table {\n  border-spacing: 0 2px;\n}\n\nspan.pofsp,\n.KnenjSub {\n  background-color: var(--color-font-grey);\n  border: none !important;\n  color: #fff;\n  display: inline;\n  font-size: 1.07em !important;\n  font-weight: normal !important;\n  line-height: 30px !important;\n  padding: 3px 5px !important;\n}\n\n.clrPhrBc {\n  clear: both;\n  display: block;\n  font-size: 0.71em;\n  line-height: 0;\n  margin-top: 1em;\n  overflow: hidden;\n}\n\n.wrpExE {\n  height: 25px;\n  margin-top: 18px;\n}\n\n.wrpExE p {\n  color: #525152;\n  font-size: 1em;\n  height: 20px;\n  margin: 0;\n  padding-left: 2px;\n}\n\n.wrpExE p a:link,\n.wrpExE p a:visited,\n.wrpExE p a:active {\n  color: #525152;\n  font-size: 1em;\n  text-decoration: none;\n}\n\n.intrst {\n  width: 100%;\n  border-top: 1px solid #ccc;\n  border-collapse: collapse;\n}\n\n#summary table:last-of-type.intrst {\n  border-bottom: 1px solid #ccc;\n  margin-bottom: 1em;\n}\n\n.intrst .intrstL {\n  background-color: rgba(245, 239, 230, 0.13);\n  padding-left: 5px;\n  vertical-align: middle;\n  width: 80px;\n}\n\n.intrst .intrstL h2 {\n  font-size: 0.92em;\n  font-weight: normal;\n}\n\n.intrst .intrstR {\n  padding-left: 4px;\n  vertical-align: middle;\n}\n\n.linkTagRR span {\n  font-size: 0;\n  display: block;\n  height: 23px;\n  line-height: 0;\n  width: 61px;\n}\n\nimg.weblioMascot {\n  margin: 15px 0 15px;\n}\n\n#main {\n  float: left;\n  margin: 0 15px 0 auto;\n}\n\n#main a:link,\n#main a:visited,\n#main a:active {\n  color: currentColor;\n}\n\n#main a:hover {\n  color: #4f7bb9;\n}\n\n.clrBc {\n  clear: both;\n  content: '';\n  display: block;\n  font-size: 0;\n  line-height: 0;\n  overflow: hidden;\n}\n\n.treeBoxC a:link,\n.treeBoxC a:visited,\n.treeBoxC a:active {\n  font-family: 'ＭＳ ゴシック', '平成角ゴシック', monospace;\n  line-height: 1.5em;\n  padding: 8px;\n}\n\n.treeBoxC h3 {\n  text-align: left;\n}\n\n.treeBoxC h3 a:link,\n.treeBoxC h3 a:visited,\n.treeBoxC h3 a:active {\n  font-size: 0.85em;\n  font-weight: normal;\n  left: -1px;\n  line-height: 1.6em;\n  padding: 0;\n  position: relative;\n}\n\n.treeBoxC h3 span {\n  color: #1c9000;\n  display: list-item;\n  margin: 0 0 0 14px;\n  padding: 0;\n  position: relative;\n}\n\n.treeBoxC hr {\n  border: #c0c0c0 solid;\n  border-width: 1px 0 0 0;\n  height: 1px;\n  margin: 5px 5px 5px 5px;\n}\n\n.treeBoxC p {\n  font-size: 1.42em;\n  margin: 0;\n  padding: 0;\n  text-align: left;\n}\n\n.treeBoxCFoldLi .pl {\n  border: #000 solid 1px;\n}\n\n#summary {\n  padding: 2px 2px 10px 2px;\n}\n\n.summaryR > .error,\n#sideBHPAEjje > .error {\n  font-size: 0.71em;\n  background-color: #f8ddde;\n  text-align: center;\n  margin-bottom: 5px;\n  display: none;\n}\n\n#sideBHPAEjje > .error {\n  letter-spacing: 1.4;\n  padding: 0 5px;\n}\n\n#sideBHPAEjje > .error a {\n  font-weight: bold;\n  color: #000;\n}\n\n.summaryR.show > .error,\n#sideBHPAEjje.show > .error {\n  display: block;\n}\n\n#summaryMargin {\n  margin-top: 150px;\n}\n\n.summaryM .description {\n  background-color: var(--color-font-grey);\n  color: #fff;\n  font-size: 1.07em;\n  font-weight: normal;\n  margin: 0 5px 0 3px;\n  padding: 3px 5px !important;\n}\n\n.linkTagRClrAd a:active,\n.linkTagRClrAd a:hover,\n.linkTagRClrAd a:link,\n.linkTagRClrAd a:visited {\n  text-decoration: none;\n}\n\nb.highlight {\n  font-weight: normal;\n}\n\n.agglutination {\n  vertical-align: top;\n}\n\n.agglutination agglutinationT {\n  font-size: 1.42em;\n}\n\n.agglutination li {\n  font-size: 1em;\n}\n\n.agglutination ul {\n  margin: 10px 0 0 0;\n  padding: 0 0 0 10px;\n}\n\n.relatedwords relatedwordsT {\n  font-size: 1.42em;\n}\n\n.EGateCoreDataWrp b,\n.descriptionWrp b {\n  display: block;\n  text-align: center;\n}\n\n.EGateCoreDataWrp table td:first-child,\n.descriptionWrp table td:first-child {\n  width: 80px;\n}\n\n.descriptionWrp table td:first-child {\n  vertical-align: top;\n  padding: 16px 0 10px 0;\n}\n\n.EGateCoreDataWrp table td,\n.descriptionWrp table td {\n  font-size: 90%;\n  font-weight: bold;\n}\n\n#searchSettingsWrp .reibun-sample .fa {\n  font-size: 1.28em;\n}\n\n.pin-icon-cell {\n  text-align: center;\n  width: 45px;\n}\n\n.pin-icon-cell td {\n  text-align: center;\n  white-space: nowrap;\n}\n\n.pin-icon-cell span {\n  color: var(--color-font-grey);\n  font-size: 0.71em;\n}\n\n.pin-icon-cell .fa {\n  cursor: pointer;\n  font-size: 2.42em;\n}\n\n.pin-icon-cell .fa:hover {\n  filter: alpha(opacity=70);\n  opacity: 0.7;\n}\n\n#learning-level-table-wrap {\n  display: table;\n  width: 100%;\n}\n\n#learning-level-table {\n  display: table-cell;\n}\n\n#learning-level-table div {\n  display: table;\n}\n\n.learning-level-row {\n  display: table-row;\n}\n\n.learning-level-row span {\n  display: table-cell;\n  padding: 2px 0;\n}\n\n.learning-level-label {\n  text-align: right;\n}\n\n.learning-level-content {\n  padding-right: 15px !important;\n}\n\n#side .addLmFd .premium,\n#main .addLmFd .premium {\n  background-color: #ff8022;\n  color: #fff;\n  display: block;\n  font-size: 70%;\n  position: relative;\n  width: 100%;\n  height: 35px;\n}\n\n#side .addLmFd .premium:hover,\n#main .addLmFd .premium:hover {\n  opacity: 0.7;\n}\n\n#summary.non-member .intrstR #leadBtnWrp,\n#summary.non-member .intrstR #learning-level-table {\n  display: block;\n  width: 100%;\n}\n\n#summary.non-member .intrstR #learning-level-table div,\n#summary.non-member .intrstR #learning-level-table div * {\n  display: inline;\n}\n\n#summary.non-member .intrstR #leadToVocabIndexBtn,\n#summary.non-member .intrstR #leadToSpeakingTestIndexBtn {\n  display: table-cell;\n\n  vertical-align: middle;\n  position: relative;\n  box-sizing: border-box;\n}\n\n#summary.non-member .intrstR #leadBtnWrp .insideLlTable {\n  display: inline-block;\n  margin: 10px 5px;\n  box-sizing: border-box;\n}\n\n.free-member-features {\n  padding: 0 0 10px 0;\n}\n\n.free-member-features .features-title {\n  background-color: #554c45;\n  color: white;\n  font-size: 1.28em;\n  font-weight: bold;\n  text-align: center;\n  padding: 10px 0;\n}\n\n.free-member-features .features-subtitle {\n  font-size: 1em;\n  font-weight: bold;\n  text-align: center;\n  padding: 8px 0 12px 0;\n}\n\n.free-member-features .features-subtitle .red {\n  background-color: transparent;\n  color: #e04a12;\n  font-size: 1.28em;\n  padding: 0 5px;\n}\n\n.free-member-features ul.features {\n  margin: 0;\n  padding: 0;\n  text-align: center;\n}\n\n.free-member-features ul.features li {\n  width: 96px;\n  height: 121px;\n  display: inline-block;\n  border: 1px solid #cdcdcd;\n  text-align: center;\n  margin-right: 15px;\n  vertical-align: top;\n}\n\n.free-member-features ul.features li:first-child {\n  margin-left: 38px;\n}\n\n.free-member-features ul.features li .feature-name {\n  font-size: 0.85em;\n  font-weight: bold;\n  margin: 11px 0 10px 0;\n}\n\n.free-member-features ul.features li img {\n  height: 36px;\n  width: auto;\n}\n\n.free-member-features ul.features li .feature-desc {\n  font-size: 0.85em;\n  margin-top: 5px;\n}\n\n.free-member-features ul.features li.more-features {\n  margin-right: 0;\n  position: relative;\n  border: none;\n}\n\n.free-member-features ul.features li.more-features img {\n  width: 86px;\n  height: auto;\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n}\n\n.free-member-features a.free-register-button {\n  display: block;\n  background-color: #48a267;\n  color: white;\n  text-decoration: none;\n  text-align: center;\n  border-radius: 15px;\n\n  margin: 10px auto 0 auto;\n  font-size: 1.14em;\n  font-weight: bold;\n  padding: 15px 0;\n  color: white;\n  position: relative;\n  line-height: 21px;\n}\n\n.free-member-features a.free-register-button:after {\n  content: '\\f0da';\n  font-family: FontAwesome;\n  position: absolute;\n  height: 21px;\n  width: 30px;\n  font-size: 1.14em;\n  line-height: 21px;\n  top: 50%;\n  right: 30px;\n  transform: translateY(-50%);\n}\n\n#main .free-member-features a.free-register-button:link,\n#main .free-member-features a.free-register-button:visited,\n#main .free-member-features a.free-register-button:active {\n  color: white;\n}\n\n.free-member-features a.free-register-button .small {\n  font-size: 1em;\n}\n\n.descriptionWrp table td.content-explanation {\n  font-size: 1.12em;\n  line-height: 2;\n  padding: 10px 0 5px 0;\n}\n\n.descriptionWrp table td.content-explanation.ej {\n  letter-spacing: 1.5;\n}\n\n#main .KnenjSub a:link,\n#main .KnenjSub a:visited,\n#main .KnenjSub a:active {\n  color: #fff;\n}\n\nh2.midashigo rt {\n  font-size: 0.5em;\n}\n\nh2.midashigo rp {\n  font-size: 0.5em;\n}\n\n.infobox caption {\n  font-size: larger;\n  margin-left: inherit;\n}\n\n.infobox.bordered {\n  border-collapse: collapse;\n}\n\n.infobox.bordered td,\n.infobox.bordered th {\n  border: #aaa solid 1px;\n}\n\n.infobox.bordered .borderless td,\n.infobox.bordered .borderless th {\n  border: 0;\n}\n\n.infobox.standard-talk.bordered td,\n.infobox.standard-talk.bordered th {\n  border: #c0c090 solid 1px;\n}\n\n.infobox.bordered .mergedtoprow td,\n.infobox.bordered .mergedtoprow th {\n  border: 0;\n  border-right: #aaa solid 1px;\n  border-top: #aaa solid 1px;\n}\n\n.infobox.bordered .mergedrow td,\n.infobox.bordered .mergedrow th {\n  border: 0;\n  border-right: #aaa solid 1px;\n}\n\n.wikitable caption,\n.prettytable caption {\n  font-weight: bold;\n  margin-left: inherit;\n  margin-right: inherit;\n}\n\ndl#infoboxCountry dt.infoboxCountryNameJa {\n  font-size: 1.36em;\n  margin: 0 0 0.13em;\n  text-align: center;\n}\n\n.dablink {\n  border-bottom: #aaa solid 1px;\n  font-size: 90%;\n  margin: 0.5em 0 0.5em 0;\n  padding: 3px 2em 3px 2em;\n}\n\n.midashigo sub {\n  font-size: 0.6em;\n}\n\n.midashigo sup {\n  font-size: 0.6em;\n}\n\ntd.movie_staff_left {\n  text-align: right;\n}\n\n.ad02_left_box {\n  width: 7px;\n  height: 30px;\n  margin: 0 2px 2px 0;\n  padding: 0;\n  background-color: #d0d0d0;\n  vertical-align: middle;\n  font-size: 10pt;\n}\n\n.ad02_center-left_box {\n  height: 30px;\n  margin: 0 2px 2px 0;\n  padding: 4px;\n  background-color: #f0f0f0;\n  vertical-align: middle;\n  font-size: 10pt;\n}\n\n.ad02_center-right_box {\n  height: 30px;\n  margin: 0 2px 2px 0;\n  padding: 4px;\n  background-color: #e0e0e0;\n  vertical-align: middle;\n  font-size: 10pt;\n}\n\n.ad02_right_box {\n  height: 30px;\n  margin: 0 2px 2px 0;\n  padding: 4px;\n  background-color: #e0e0e0;\n  vertical-align: middle;\n  font-size: 10pt;\n}\n\n.main3noh {\n  font-size: 1em;\n  color: #fff;\n}\n\n.mainb {\n  font-size: 1em;\n  color: #000;\n  font-weight: bold;\n  line-height: 18px;\n}\n\nh2.midashigo sub {\n  font-size: smaller;\n}\n\nul.linkListStrong li {\n  list-style-type: none;\n}\n\n.Ktdcm .KtdcmImgLeft {\n  float: left;\n  width: 48%;\n}\n\n.Ktdcm .KtdcmImgRight {\n  float: right;\n  width: 48%;\n}\n\n.Ktdcm .maincol {\n  margin-left: 15px;\n  text-align: left;\n}\n\n.Ktdcm ul.fright li.full {\n  text-align: right;\n}\n\n.Ktdcm .maincol .boxArea {\n  margin-bottom: 16px;\n  padding-top: 7px;\n}\n\n.Ktdcm .maincol .boxArea .wrap {\n  padding-bottom: 8px;\n}\n\n.Ktdcm .maincol .boxArea .section {\n  padding: 0 7px;\n}\n\n.Ktdcm table.cell2 .right {\n  padding-left: 16px;\n}\n\n.Sngsj .gaiji {\n  height: 1em;\n  vertical-align: text-bottom;\n  width: 1em;\n}\n\n.Otnet .OtnetRed {\n  border-bottom: #ccc solid 1px;\n  border-left: #f00 solid 10px;\n  border-right: #ccc solid 0;\n  margin: 12px;\n  padding: 1px 5px;\n}\n\n.Fkkyr .left_column {\n  padding: 15px 0 0 0;\n}\n\ntd.midashigo {\n  color: #4f519b;\n  font-weight: bold;\n  padding: 10px 5px 30px 2px;\n}\n\n.Mtsbs .notes_mainArea {\n  margin-top: 10px;\n}\n\n.Mtsbs table.spec tr.mainheader th {\n  background-color: #d2d2d2;\n  font-weight: bold;\n}\n\n.Mtsbs table.spec tr.mainheader th.basic {\n  background-color: #e8f6d9;\n}\n\n.Mtsbs table.spec .tdleft {\n  text-align: left;\n}\n\n.Mtsbs table.spec .tdright {\n  text-align: right;\n}\n\n.Mtsbs table.spec td.tdleft_nb {\n  text-align: left;\n  border-right-style: none;\n}\n\n.Mtsbs table.spec td.tdright_nb {\n  text-align: right;\n  border-left-style: none;\n}\n\n.Mtsbs .carmain_font80 {\n  font-size: 0.8em;\n}\n\n.Mtsbs .carmain_font70 {\n  font-size: 0.7em;\n}\n\n.Mtsbs td.tdleft {\n  text-align: left;\n}\n\n.Mtsbs th.tdleft {\n  text-align: left;\n}\n\n.Kkjsh table tr td.right {\n  text-align: right;\n}\n\n.Mzdmt .cell_center_left {\n  border-right: #a1a1a1 solid 1px;\n  text-align: center;\n}\n\n.Mzdmt .cell_left_no {\n  border-bottom: #a1a1a1 solid 1px;\n  height: 22px;\n  text-align: left;\n}\n\n.Mzdmt .cell_left {\n  border: #a1a1a1 solid;\n  border-width: 0 1px 1px 0;\n  height: 22px;\n  text-align: left;\n}\n\n.Triph .data caption {\n  background: #94b7df;\n  border-right: #fff solid 1px;\n  border-top: #fff solid 1px;\n  color: #fff;\n  font-weight: bold;\n  padding: 2px 17px;\n}\n\n.Cntkj .description {\n  float: right;\n  width: 43%;\n}\n\n.Hyndi .spec-table td.left {\n  text-align: left;\n}\n\n.Hndmr table#webcatalogue-table td {\n  color: var(--color-font-grey);\n}\n\n.Hndmr table#webcatalogue-table img {\n  border: none;\n}\n\n.Hndmr table#webcatalogue-table p {\n  margin: 0;\n  padding: 0;\n}\n\n.Hndmr table#webcatalogue-table p.leadcopy {\n  font-size: 0.9em;\n  font-weight: bold;\n  line-height: 18px;\n}\n\n.Hndmr table#webcatalogue-table p.leadcopy2 {\n  font-size: 0.9em;\n  font-weight: bold;\n  line-height: 21px;\n}\n\n.Hndmr table#webcatalogue-table p.text {\n  font-size: 0.9em;\n  line-height: 16px;\n}\n\n.Hndmr table#webcatalogue-table p.caution {\n  color: #888;\n  font-size: 0.9em;\n  line-height: 12px;\n  margin-top: 3px;\n}\n\n.Hndmr table#webcatalogue-table span.typebetsu {\n  font-size: 0.9em;\n  font-weight: normal;\n}\n\n.Hndmr table#webcatalogue-table p.concepttext {\n  color: #fff;\n  line-height: 18px;\n  margin: 0 15px 10px 15px;\n}\n\n.Hndmr table#webcatalogue-table strong.v6 {\n  color: #003f98;\n}\n\n.Hndmr table#webcatalogue-table p.safe-midashi {\n  // background-color: currentColor;\n  color: #fff;\n  font-size: 0.9em;\n  font-weight: bold;\n  line-height: 18px;\n  padding: 3px 5px 3px 5px;\n}\n\n.Hndmr table#webcatalogue-table p.realworldtext {\n  color: #51318f;\n}\n\n.Hndmr table#webcatalogue-table span.co2 {\n  font-size: 0.9em;\n}\n\n.Hndmr table#webcatalogue-table p.texthyoujimark {\n  font-size: 0.9em;\n  line-height: 16px;\n}\n\n.Hndmr table#webcatalogue-table #env-data {\n  font-size: 0.9em;\n}\n\n.Hndmr table#webcatalogue-table #env-data td.tabletext {\n  padding: 2px;\n}\n\n.Hndmr table#webcatalogue-table strong.price {\n  font-size: 0.9em;\n}\n\n.Hndmr table#webcatalogue-table p.caution_vg {\n  margin-top: 7px;\n}\n\n.Hndmr table#webcatalogue-table p.navi-midashi {\n  background-color: #1c1f7a;\n  color: #fff;\n  font-size: 0.9em;\n  font-weight: bold;\n  line-height: 18px;\n  padding: 3px 5px 3px 5px;\n}\n\n.Hndmr table#webcatalogue-table .note {\n  color: #1c1f7a;\n}\n\n.Hndmr table#webcatalogue-table span.komidashi {\n  color: #006965;\n}\n\n.Hndmr table.spec-table td.bd-left {\n  border-left: 1px solid #2c2c2c;\n}\n\n.Hndmr .block_line_right {\n  border-right: solid 1px #000;\n}\n\n.Hndmr #webcata_footer {\n  clear: both;\n  padding: 30px 128px 15px 20px;\n  text-align: right;\n}\n\n.Hndmr #specifications th.car .small {\n  font-size: x-small;\n  line-height: 120%;\n}\n\n.Hndmr div#web-catalog-contents {\n  margin: 24px;\n}\n\n.Hndmr div#web-catalog-contents h4 {\n  background: #325958;\n  color: #fff;\n  font-size: 0.9em;\n  margin-bottom: 14px;\n  padding: 4px 0 4px 10px;\n}\n\n.Hndmr div#web-catalog-contents table.model-navi {\n  margin: 0 0 5px 0;\n}\n\n.Hndmr div#web-catalog-contents table.model-navi td {\n  padding: 0 25px 0 10px;\n}\n\n.Hndmr div#web-catalog-contents * {\n  margin: 0;\n  padding: 0;\n}\n\n.Hndmr div#web-catalog-contents h4 span {\n  font-size: 0.9em;\n  font-weight: normal;\n}\n\n.Hndmr div#web-catalog-contents p.caution,\np.caution {\n  font-size: 0.9em;\n  line-height: 120%;\n}\n\n.Hndmr #eq_spec_list td.right_non_border {\n  border-right: none;\n}\n\n.Kejje dd {\n  margin-left: 20px;\n}\n\n.Kejje .gaiji {\n  border: 0;\n  margin-bottom: -3px;\n}\n\n.Kejje .level1 {\n  margin-left: 1em;\n}\n\n.Kejje .level2 {\n  margin-left: 1.5em;\n}\n\n.Kejje .backlink {\n  margin-top: 10px;\n}\n\n.Kejje .backlink img {\n  margin-bottom: -3px;\n  margin-right: 5px;\n}\n\n.Kejje .onsei {\n  margin-bottom: -8px;\n}\n\n.Kejje .playSd {\n  cursor: pointer;\n}\n\n.youreilink {\n  border-bottom: #080 solid 1px;\n  color: #080;\n  font-weight: bold;\n  text-decoration: none;\n}\n\n.KejjeYrL,\n.KejjeYrLS,\n.KejjeYrM,\n.KejjeYrMS,\n.KejjeYrR {\n  background-color: rgba(247, 247, 247, 0.14);\n  font-size: 0.9em;\n  vertical-align: top;\n}\n\n.KejjeYrL,\n.KejjeYrLS {\n  color: #363636;\n  font-size: 0.9em;\n  padding: 0 4px 0 4px;\n  width: 40px;\n}\n\n.KejjeYrM,\n.KejjeYrMS {\n  width: 13px;\n}\n\n.KejjeYrR {\n  padding: 0 4px 0 4px;\n}\n\n.KejjeYr {\n  border: 0;\n  border-collapse: collapse;\n  margin: 0 0 3px 25px;\n  padding: 0;\n}\n\n.KejjeYrMS i {\n  margin: 2px 0 0 2px;\n}\n\n.KejjeYrLS {\n  cursor: pointer;\n}\n\n.KejjeYrC {\n  border: #666 solid 1px;\n  font-size: 0.9em;\n  padding: 1px;\n}\n\n.KejjeYrHd {\n  padding: 0 0.5em 0 0;\n}\n\n.KejjeYrTxt {\n  display: none;\n  margin: 0;\n  padding: 0 0.5em 0 0;\n}\n\n.KejjeYrHd a,\n.KejjeYrTxt a {\n  color: black;\n}\n\n.KejjeYrHd a:active,\n.KejjeYrTxt a:active {\n  color: black;\n}\n\n.KejjeYrHd a:hover,\n.KejjeYrTxt a:hover {\n  color: black;\n}\n\n.KejjeYrHd a:visited,\n.KejjeYrTxt a:visited {\n  color: black;\n}\n\n.KejjeYrLn {\n  background-image: url(https://weblio.hs.llnwd.net/e7/img/iconCclBlS.png);\n  background-position: left 5px;\n  background-repeat: no-repeat;\n  line-height: 1.2em;\n  margin: 0;\n  padding: 0 0 0 16px;\n}\n\n.KejjeYrLn sup {\n  line-height: 1em;\n  margin: 0;\n  padding: 0;\n}\n\n.KejjeYrLn span {\n  line-height: 1.6em;\n  margin: 0;\n  padding: 0;\n}\n\n.KejjeYrLn .KejjeYrEn {\n  font-family: Arial;\n  font-weight: bold;\n}\n\n.KejjeYrKwrd {\n  line-height: 1em;\n}\n\n.KejjeSm {\n  font-size: 0.8em;\n  font-weight: normal;\n  line-height: 1em;\n}\n\n.KejjeSm a {\n  line-height: 1em;\n}\n\n.Kejje .kenjeEnE {\n  border: 0;\n  display: list-item;\n  list-style-position: inside;\n  margin: 0;\n  padding: 0 0 0 10px;\n}\n\n.Kejje .onseiSwf {\n  display: inline-block;\n  position: relative;\n  top: 8px;\n  margin-left: 5px;\n}\n\n.KejjeYrMS img {\n  cursor: pointer;\n  margin-top: 3px;\n}\n\n.KejjeSm {\n  font-size: 0.8em;\n  font-weight: normal;\n  line-height: 1em;\n  padding-left: 4px;\n}\n\n.Kejje .lvlNH,\n.Kejje .lvlAH,\n.Kejje .lvlB {\n  float: left;\n  margin: 0;\n  padding: 0;\n  vertical-align: bottom;\n}\n\n.Kejje .lvlNH {\n  font-size: 1.3em;\n  font-weight: bold;\n  height: 1.1em;\n  width: 28px;\n}\n\n.Kejje .lvlAH {\n  font-size: 1.4em;\n  font-weight: bold;\n  line-height: 1.6em;\n  width: 16px;\n}\n\n.Kejje .lvlUAH {\n  font-size: 1.3em;\n  font-weight: bold;\n  height: 1.1em;\n}\n\n.Kejje .lvlUAB {\n  font-weight: bold;\n}\n\n.Kejje .lvlB {\n  padding: 0.1em 0 0 0;\n  max-width: 85%;\n}\n\n.Kejje .lvlNHje {\n  float: left;\n  font-weight: bold;\n  margin: 0;\n  padding: 0;\n  vertical-align: bottom;\n  width: 16px;\n}\n\n.Kejje .lvlNBje {\n  float: left;\n  font-weight: bold;\n  margin: 0 0 12px 0;\n  padding: 0;\n}\n\n.Kejje .lvlNBje td {\n  line-height: 1.6em;\n  margin: 0;\n  padding: 0 20px 0 0;\n  vertical-align: top;\n}\n\n.Kejje .lvlNBjeT {\n  float: left;\n  margin: 0;\n  padding: 0;\n}\n\n.Kejje .lvlNBjeT sup {\n  line-height: 1em;\n  margin: 0;\n  padding: 0;\n}\n\n.Kejje .lvlNBjeL {\n  white-space: nowrap;\n}\n\n.Kejje .lvlBje {\n  font-weight: bold;\n  margin: 0;\n  padding: 0;\n}\n\n.Kejje .lvlB {\n  margin: 0.3em 0 0 0;\n}\n\n.Kejcy .gaiji {\n  border: 0;\n  margin-bottom: -3px;\n}\n\n.Kejcy .level0 {\n  margin: 1em 0 0 0;\n}\n\n.Kejcy .level1 {\n  margin: 0 1.5em 0 1.5em;\n}\n\n.Kejcy .level2 {\n  font-size: 0.9em;\n  margin: 0 3em 0.5em 3em;\n  padding: 0.2em 0.5em 0 0.5em;\n}\n\n.Gicns .ga_small {\n  font-size: 0.9em;\n}\n\n.Nsrsk .NsrskMaintxt {\n  float: left;\n  text-align: left;\n}\n\n.Nsrsk .NsrskRightph {\n  border: #ccc solid 1px;\n  float: right;\n  text-align: center;\n}\n\n.Tltdb div.TltdbLeft {\n  float: left;\n  text-align: center;\n}\n\n.Tytmt .smallMText {\n  font-size: 0.7em;\n}\n\n.Tpkys div.stroke div.imgArea p.rightImg {\n  margin-right: 0;\n}\n\n.Tpkys .closeBoxIn div.makeRight {\n  border: #ccc dotted 1px;\n  float: left;\n  height: 350px;\n  padding: 10px;\n}\n\n.Srsbz .SrsbzLeft {\n  float: left;\n}\n\n.Srsbz .SrsbzRight {\n  float: right;\n}\n\n.Nrksm .NrksmT2 caption {\n  font-weight: bold;\n  text-align: left;\n}\n\n.Trhnt .TrhntLeft {\n  float: left;\n}\n\n.Trhnt .TrhntRight {\n  float: right;\n}\n\n.rmvDots {\n  background: none !important;\n  padding: 0 !important;\n}\n\n.syosaiLeft {\n  float: left;\n  margin: 0 0 20px 25px;\n}\n\n.Hgnsh .syosaiLeft {\n  float: left;\n  margin: 0 0 0 5px;\n}\n\n.Hgnsh .syosaiLeftBox {\n  line-height: 1.4em;\n  margin-top: 15px;\n  text-align: justify;\n  text-justify: inter-ideograph;\n}\n\n.Hgnsh .syosaiRight {\n  float: right;\n  margin-top: 5px;\n}\n\n.Hgnsh .syosaiRightBox {\n  text-align: center;\n}\n\n.Hgnsh .syosaiRightBox img {\n  margin-bottom: 0;\n  padding: 15px 0 15px;\n}\n\n.Hskks .syosaiLeft {\n  float: left;\n  margin: 0 0 0 5px;\n}\n\n.Hskks .syosaiLeftBox {\n  line-height: 1.4em;\n  margin-top: 15px;\n  text-align: justify;\n  text-justify: inter-ideograph;\n}\n\n.Hskks .syosaiRight {\n  float: right;\n  margin-top: 5px;\n}\n\n.Hskks .syosaiRightBox {\n  text-align: center;\n}\n\n.Hskks .syosaiRightBox img {\n  margin-bottom: 0;\n  padding: 15px 0 15px;\n}\n\n.LiscjYr .synonym1 {\n  float: left;\n  margin: 0;\n  width: 45%;\n}\n\n.LiscjYr .synonym2 {\n  float: right;\n  margin: 0;\n  width: 45%;\n}\n\n.LiscjYr .clear_column {\n  clear: both;\n}\n\n.Liscj .caption p,\n.Liscj .meaning p {\n  margin: 0;\n}\n\n.Wejty .wejtyT span,\n.Wejty .wejtyE span,\n.Wejty .wejtyR span {\n  background-color: #f0f0f0;\n  border: #666 solid 1px;\n  color: #363636;\n  font-size: 0.9em;\n  line-height: 1em;\n  padding: 1px;\n}\n\n.Wejty .wejtyT {\n  padding: 0;\n}\n\n.Wejty .wejtyE,\n.Wejty .wejtyR {\n  padding: 0 0 0 16px;\n}\n\n.Wejty .wejtyL {\n  background-color: #f0f0f0;\n  border: #666 solid 1px;\n  color: #363636;\n  font-size: 0.9em;\n  font-weight: normal;\n  line-height: 1em;\n  margin-right: 8px;\n  padding: 1px;\n}\n\n.Wejty .wejtyInfo {\n  border: #b5b6b5 solid 1px;\n  font-size: 0.8em;\n  line-height: 1.32em;\n  margin: 1em 0 0 0;\n  padding: 10px;\n}\n\n.Wwsej .wwsejInfo {\n  border: #b5b6b5 solid 1px;\n  font-size: 0.8em;\n  line-height: 1.32em;\n  margin: 20px 0 0 0;\n  padding: 10px;\n}\n\n.Nwnej .nwnejP {\n  border-left: #815733 solid 6px;\n  font-size: 1em;\n  font-weight: bold;\n  line-height: 1em;\n  margin: 12px 0 2px 0;\n  padding-left: 3px;\n}\n\n.Nwnej .nwnejP a {\n  line-height: 1em;\n}\n\n.Nwnej .nwnejSEnL,\n.Nwnej .nwnejSEnR {\n  max-width: 90%;\n  float: left;\n  margin: 12px 0 0 0;\n}\n\n.Nwnej .nwnejSEnL {\n  line-height: 1.4em;\n  width: 16px;\n}\n\n.Nwnej .nwnejSJp {\n  margin: 0 0 0 1.5em;\n  padding: 0 0 12px 0;\n}\n\n.Nwnej .nwnejS p {\n  line-height: 1.4em;\n  margin: 0;\n  padding: 0;\n}\n\n.Nwnej .nwnejS a {\n  line-height: 1em;\n}\n\n.Nwnej .nwnejYr {\n  margin: 0 0 0 1.5em;\n  padding: 0;\n}\n\n.Nwnej .nwnejYrL,\n.Nwnej .nwnejYrLS,\n.Nwnej .nwnejYrR {\n  font-size: 0.9em;\n  vertical-align: top;\n}\n\n.Nwnej .nwnejYrL,\n.Nwnej .nwnejYrLS {\n  margin: 0;\n  padding: 0;\n  width: 13px;\n}\n\n.Nwnej .nwnejYrR {\n  padding: 0 4px 0 4px;\n}\n\n.Nwnej .nwnejYrLS i {\n  margin: 4px 0 0 3px;\n}\n\n.Nwnej .nwnejYrHd {\n  padding: 0 0.5em 0 0;\n}\n\n.Nwnej .nwnejYrTxt {\n  display: none;\n  margin: 0;\n  padding: 0 0.5em 0 0;\n}\n\n.Nwnej .nwnejYrLn {\n  background-image: url(https://weblio.hs.llnwd.net/e7/img/iconCclBlS.png);\n  background-position: left 5px;\n  background-repeat: no-repeat;\n  line-height: 1.2em;\n  padding: 0 0 0 12px;\n}\n\n.Nwnej .nwnejYrE,\n.Nwnej .nwnejYrJ {\n  font-size: 0.9em;\n  margin: 0;\n}\n\n.Nwnej .nwnejYrE {\n  font-family: Arial;\n  font-weight: bold;\n  padding-right: 8px;\n}\n\n.Nwnej .nwnejThL a:active,\n.Nwnej .nwnejThL a:hover,\n.Nwnej .nwnejThL a:link,\n.Nwnej .nwnejThL a:visited {\n  border-bottom: currentColor dotted 1px;\n  display: inline-block;\n  font-size: 1em;\n  line-height: 10px;\n  text-decoration: none;\n}\n\n.Wejhs .wejhsSub {\n  border-left: #815733 solid 6px;\n  font-size: 1.3em;\n  font-weight: bold;\n  line-height: 1em;\n  margin: 10px 0 5px 0;\n  padding-left: 3px;\n}\n\n.Wejhs .wejhsSub a {\n  line-height: 1em;\n}\n\n.Wejhs .wejhsD {\n  margin-left: 20px;\n  margin-top: 10px;\n  padding: 5px;\n}\n\n.Wejhs .wejhsL {\n  float: left;\n  padding: 2px 0 2px 0;\n}\n\n.Wejhs .wejhsL span {\n  border: #666 solid 1px;\n  color: #363636;\n  font-size: 0.9em;\n  margin: 0 16px 0 12px;\n  padding: 1px;\n}\n\n.Wejhs .wejhsR {\n  float: left;\n}\n\n.Wejhs .wejhsInfo {\n  border: #b5b6b5 solid 1px;\n  font-size: 0.8em;\n  line-height: 1.32em;\n  margin: 50px 0 0 0;\n  padding: 10px;\n}\n\n.Wkpen .wkpenWrp {\n  line-height: 1.3em;\n  margin-top: 5px;\n  padding: 9px 5px 9px 15px;\n  width: 95%;\n}\n\n.Jawik .level0,\n.Jawik .level0Head {\n  font-size: 1.4em;\n  font-weight: bold;\n  margin: 10px 0 2px 0;\n  padding-left: 5px;\n}\n\n.Jawik .level0Head {\n  margin-top: 0;\n}\n\n.Jawik .level0 span,\n.Jawik .level0Head span {\n  border: #b81e07 solid;\n  border-width: 0 0 0 8px;\n  padding: 3px 2px 2px 0;\n}\n\n.Hypej .level0 {\n  margin: 1em 0 0 0;\n  padding: 0;\n}\n\n.Hypej .HypejSub {\n  border-left: #815733 solid 6px;\n  font-size: 1.3em;\n  font-weight: bold;\n  line-height: 1em;\n  margin: 10px 0 5px 0;\n  padding-left: 3px;\n}\n\n.Hypej .HypejSm {\n  font-size: 0.8em;\n  font-weight: normal;\n  line-height: 1em;\n  padding-left: 3px;\n}\n\n.Hypej .lvlNH {\n  float: left;\n  font-size: 1.3em;\n  font-weight: bold;\n  height: 1.1em;\n  margin: 0;\n  padding: 0;\n  vertical-align: bottom;\n  width: 28px;\n}\n\n.Hypej .lvlB {\n  float: left;\n  margin: 0;\n  padding: 0.3em 0;\n}\n\n.HypejYrL,\n.HypejYrR {\n  font-size: 0.9em;\n  vertical-align: top;\n}\n\n.Hypej .HypejYr {\n  border: 0;\n  border-collapse: collapse;\n  margin: 5px 0 1em 1em;\n  padding: 0;\n}\n\n.Hypej .HypejYrL {\n  padding: 0 4px 0 4px;\n  width: 40px;\n}\n\n.Hypej .HypejYrC {\n  border: #666 solid 1px;\n  font-size: 0.9em;\n  padding: 1px;\n}\n\n.Hypej .HypejYrLn {\n  background-image: url(https://weblio.hs.llnwd.net/e7/img/iconCclBlS.png);\n  background-position: left 5px;\n  background-repeat: no-repeat;\n  line-height: 1.2em;\n  margin: 0;\n  padding: 0 0 0 16px;\n}\n\n.Hypej .HypejYrEn {\n  font-family: Arial;\n  font-weight: bold;\n}\n\n.Hypej .HypejLb {\n  background-color: #eee;\n  border: #999 solid 1px;\n  color: currentColor;\n  font-size: 1em;\n  font-weight: normal;\n  margin: 0 2px 0 3px;\n  padding: 1px;\n}\n\n.Hypej .HypejB {\n  border-collapse: collapse;\n  width: 80%;\n}\n\n.Hypej .HypejB tr th {\n  border: #808080 solid 1px;\n  padding: 3px;\n}\n\n.Hypej .HypejB tr td {\n  border: #808080 solid 1px;\n  padding: 3px;\n}\n\n.Hypej .onseiSwf {\n  padding: 10px 0 0 2px;\n  vertical-align: middle;\n}\n\n.Gkzkj .level0 {\n  margin: 0;\n  padding: 0;\n}\n\n.Efref .efrefA {\n  color: #999;\n  font-size: 1em;\n  margin: 0 0 12px;\n}\n\n.Nkjtn .nkjtnI caption {\n  font-weight: bold;\n}\n\nh2.midashigo .cgkgjSm,\n.Cgkgj .cgkgjSm {\n  font-size: 0.6em;\n  margin-left: 0.4em;\n}\n\n.Cgkgj .level0 {\n  margin: 0;\n  padding: 0;\n}\n\n.EgtejLb {\n  border: #666 solid 1px;\n  font-size: 1em;\n  font-weight: bold;\n  margin: 2px 0;\n  margin-right: 8px;\n  padding: 0 3px !important;\n  white-space: nowrap;\n}\n\n.EgtejRSpc {\n  margin-right: 10px;\n}\n\n.EgtejSub,\n.EgtejSubS {\n  background-color: var(--color-font-grey);\n  border: none !important;\n  border-radius: 3px;\n  color: #fff;\n  display: inline;\n  font-size: 1em;\n  font-weight: normal !important;\n  line-height: 30px !important;\n  margin-right: 5px;\n  padding: 0 5px 3px 5px !important;\n}\n\n.EgtejSubS {\n  font-size: 1em;\n}\n\n.EgtejSub a,\n.EgtejSubS a,\n.EgtejCcl a {\n  color: #fff !important;\n  text-decoration: none !important;\n}\n\n.EgtejCcl a {\n  line-height: 18px;\n}\n\n.EgtejBld {\n  font-weight: bold;\n}\n\n.EgtejYrAr,\n.EgtejIdxAr {\n  margin-top: 3px;\n  background-color: rgba(173, 173, 173, 0.16);\n  padding: 5px;\n}\n\n.EgtejYrTb {\n  border-collapse: collapse;\n  width: 100%;\n}\n\n.EgtejYrTb td {\n  vertical-align: top;\n}\n\n.EgtejYrImg img {\n  width: 100%;\n}\n\n.EgtejYrTb th {\n  vertical-align: top;\n  width: 40px;\n}\n\n.EgtejWrp {\n  margin-left: 2px;\n}\n\n.EgtejCrTb {\n  margin-bottom: 10px;\n}\n\n.EgtejCrTb td {\n  padding-left: 30px;\n  vertical-align: top;\n}\n\n.EgtejCrTb td:first-child {\n  padding: 0;\n  width: 40%;\n}\n\n.EgtejCrTb td img {\n  border: #ccc solid 1px;\n  padding: 10px;\n  width: 100%;\n}\n\n.EgtejSpc {\n  display: inline-block;\n  width: 1em;\n}\n\n.EgtejClmTb {\n  width: 100%;\n}\n\n.EgtejClmTb td {\n  line-height: 1.2em;\n  vertical-align: top;\n}\n\n.EgtejClmImg img {\n  width: 100%;\n}\n\n.EgtejIdxCld {\n  margin-left: 40px;\n  margin-right: 10px;\n}\n\n.EgtejSign {\n  display: inline-block;\n  font-size: 1em;\n  line-height: 1.2em;\n  margin-top: 10px;\n}\n\n.EgtejCcl {\n  background-color: var(--color-font-grey);\n  border-radius: 50%;\n  color: #fff;\n  display: inline-block;\n  font-size: 1em;\n  font-weight: bold;\n  height: 18px;\n  line-height: 18px;\n  text-align: center;\n  width: 18px;\n}\n\n.EgtejRmIdx {\n  font-size: 1em;\n}\n\n.EgtejAncTgt {\n  display: none;\n}\n\n.EgtejAncTgt + .br {\n  margin-bottom: 1em;\n}\n\n.EgtejAncLnk {\n  text-decoration: none;\n}\n\n.CtbdsWrp {\n  margin: 0 15px 25px 4px;\n}\n\n.CtbdsWrpNt {\n  margin-top: 36px;\n  margin-bottom: 36px;\n}\n\n.Jfwik .level0,\n.Jfwik .level0Head {\n  font-size: 1.4em;\n  font-weight: bold;\n  margin: 10px 0 2px 0;\n  padding-left: 5px;\n}\n\n.Jfwik .level0Head {\n  margin-top: 0;\n}\n\n.Jfwik .level0 span,\n.Jfwik .level0Head span {\n  border: #95adce solid;\n  border-width: 0 0 0 8px;\n  padding: 3px 2px 2px 0;\n}\n\n.flex-rectangle-ads-frame {\n  display: flex;\n  justify-content: space-around;\n  align-items: center;\n}\n\n.intrstR .conjugateRowL {\n  width: 14%;\n  vertical-align: top;\n}\n\n.summaryM {\n  margin-bottom: 5px;\n}\n\n.summaryHead {\n  display: flex;\n  align-items: center;\n\n  h1 {\n    font-size: 1.3rem;\n  }\n\n  .h1keywords {\n    display: none;\n  }\n}\n\nimg {\n  display: inline;\n}\n\n.qotC {\n  margin: 0.5em 0;\n}\n\n.qotC p {\n  margin: 0;\n}\n\n.qotCJE .squareCircle,\n.qotCE .squareCircle {\n  display: none;\n}\n\n.CtbdsLb {\n  background-color: var(--color-font-grey);\n  color: #fff;\n  font-size: 15px;\n  font-weight: normal;\n  margin: 0 6px 0 4px;\n  padding: 3px 5px !important;\n  white-space: nowrap;\n}\n\n.CtbdsLbNt {\n  font-size: 20px;\n}\n\n.CtbdsTdCore,\n.CtbdsTdPoint {\n  width: auto;\n}\n\n.CtbdsWrp {\n  margin: 0 15px 25px 4px;\n}\n\n.CtbdsWrpNt {\n  margin-top: 36px;\n  margin-bottom: 36px;\n}\n\n.CtbdsCat,\n.CtbdsPv {\n  margin: 10px 0;\n}\n\n.CtbdsPv {\n  font-weight: bold;\n}\n\n.CtbdsSemSpaced {\n  margin-bottom: 10px;\n}\n\n.CtbdsExAr {\n  background-color: rgba(173, 173, 173, 0.16);\n  height: auto;\n  margin: 0 5px 5px 5px;\n  padding: 5px;\n}\n\n.CtbdsEx {\n  padding-left: 20px;\n  margin: 0;\n}\n\n.CtbdsEx li {\n  list-style-type: disc;\n}\n\n.CtbdsMetaTb {\n  border-spacing: 0;\n  margin: 0 0 15px 0;\n}\n\n.CtbdsMetaTb td {\n  vertical-align: top;\n}\n\n.jmnedGls {\n  margin: 0;\n}\n\n.br {\n  margin-bottom: 5px;\n}\n\n.level0 .br {\n  margin: 0;\n}\n\n.Nekym table {\n  width: 100%;\n  table-layout: fixed;\n  border-collapse: collapse;\n  border: 1px #696969 solid;\n}\n\n.Nekym table th {\n  background-color: #f5f5f5;\n  border: 1px #696969 solid;\n  font-weight: bold;\n  padding: 3px;\n  white-space: nowrap;\n}\n\n.Nekym table td {\n  border: 1px #696969 solid;\n  padding: 5px;\n  line-height: 1.3em;\n}\n\n.Nekym .nekymS {\n  border-left: #815733 solid 6px;\n  font-size: 1.3em;\n  font-weight: bold;\n  line-height: 1em;\n  margin: 10px 0 5px 0;\n  padding-left: 3px;\n}\n\n.KejjeIdH {\n  border-left: #666 solid 6px;\n  font-size: 1.2em;\n  line-height: 1em;\n  margin: 10px 0 5px 0;\n  padding-left: 3px;\n}\n\n.phraseEjjeT td {\n  vertical-align: top;\n}\n\n.nwnejThL {\n  margin-top: 1em;\n}\n\n.qotHS {\n  border: #666 solid 1px;\n  padding: 0 2px;\n}\n\n.qotCE {\n  font-weight: bold;\n  line-height: 1.2;\n  margin: 2px 0 2px 0;\n  padding: 0;\n}\n\n.qotCJ {\n  color: var(--color-font-grey);\n  line-height: 1.2em;\n  margin: -1px 0 13px 0;\n  padding: 0;\n}\n\n.phraseEjjeT {\n  width: 100%;\n}\n"
  },
  {
    "path": "src/components/dictionaries/weblioejje/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type VocabularyConfig = DictItem\n\nexport default (): VocabularyConfig => ({\n  lang: '10010000',\n  selectionLang: {\n    english: true,\n    chinese: false,\n    japanese: true,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 400,\n  selectionWC: {\n    min: 1,\n    max: 999\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/weblioejje/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  handleNoResult,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  getOuterHTML,\n  HTMLString,\n  externalLink,\n  getText,\n  removeChildren,\n  DictSearchResult\n} from '../helpers'\nimport { getStaticSpeakerString, getStaticSpeaker } from '@/components/Speaker'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return `https://ejje.weblio.jp/content/${encodeURIComponent(\n    text.replace(/\\s+/g, '+')\n  )}`\n}\n\nconst HOST = 'https://ejje.weblio.jp'\n\nexport type WeblioejjeResult = Array<{\n  title?: string\n  content: HTMLString\n}>\n\ntype WeblioejjeSearchResult = DictSearchResult<WeblioejjeResult>\n\nexport const search: SearchFunction<WeblioejjeResult> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  return fetchDirtyDOM(getSrcPage(text, config, profile))\n    .catch(handleNetWorkError)\n    .then(handleDOM)\n}\n\nfunction handleDOM(\n  doc: Document\n): WeblioejjeSearchResult | Promise<WeblioejjeSearchResult> {\n  const result: WeblioejjeResult = []\n\n  doc.querySelectorAll<HTMLDivElement>('.mainBlock').forEach($entry => {\n    if ($entry.id === 'summary') {\n      let head = ''\n\n      const $summaryTbl = $entry.querySelector('.summaryTbl')\n      if ($summaryTbl) {\n        head += getOuterHTML(HOST, $summaryTbl, '.summaryL h1')\n\n        const $audio = $summaryTbl.querySelector('.summaryC audio source')\n        if ($audio) {\n          head += getStaticSpeakerString($audio.getAttribute('src'))\n        }\n\n        $summaryTbl.outerHTML = `<div class=\"summaryHead\">${head}</div>`\n      }\n\n      removeChildren($entry, '#leadBtnWrp')\n      removeChildren($entry, '.addLmFdWr')\n      removeChildren($entry, '.flex-rectangle-ads-frame')\n      removeChildren($entry, '.outsideLlTable')\n\n      result.push({ content: getOuterHTML(HOST, $entry) })\n      return\n    }\n\n    if (\n      !$entry.className.includes('hlt_') ||\n      $entry.classList.contains('hlt_CPRHT') ||\n      $entry.classList.contains('hlt_RLTED')\n    ) {\n      return\n    }\n\n    let title = ''\n    let $title = $entry.querySelector('.wrp')\n    if ($title) {\n      title = getText($title, '.dictNm')\n      if (title.includes('Wiktionary')) {\n        return\n      }\n      $title.remove()\n    } else {\n      $title = $entry.querySelector('.qotH')\n      if ($title) {\n        title = getText($title, '.qotHT')\n        $title.remove()\n      }\n    }\n\n    removeChildren($entry, '.hideDictWrp')\n    removeChildren($entry, '.kijiFoot')\n    removeChildren($entry, '.addToSlBtnCntner')\n\n    $entry.querySelectorAll('.fa-volume-up').forEach($audio => {\n      const $source = $audio.querySelector('source')\n      if ($source) {\n        $audio.replaceWith(getStaticSpeaker($source.getAttribute('src')))\n      }\n    })\n\n    $entry.querySelectorAll('br').forEach($br => {\n      $br.classList.add('br')\n      $br.outerHTML = `<div class=\"${$br.className}\"></div>`\n    })\n\n    $entry.querySelectorAll('a').forEach($a => {\n      if (!$a.classList.contains('crosslink')) {\n        externalLink($a)\n      }\n    })\n\n    result.push({ title, content: getOuterHTML(HOST, $entry) })\n  })\n\n  return result.length > 0 ? { result } : handleNoResult()\n}\n"
  },
  {
    "path": "src/components/dictionaries/websterlearner/View.tsx",
    "content": "import React, { FC } from 'react'\nimport Speaker from '@/components/Speaker'\nimport {\n  WebsterLearnerResult,\n  WebsterLearnerResultLex,\n  WebsterLearnerResultRelated\n} from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictWebsterLearner: FC<ViewPorps<WebsterLearnerResult>> = ({\n  result\n}) => {\n  switch (result.type) {\n    case 'lex':\n      return renderLex(result)\n    case 'related':\n      return renderRelated(result)\n    default:\n      return null\n  }\n}\n\nfunction renderLex(result: WebsterLearnerResultLex) {\n  return (\n    <>\n      {result.items.map(entry => (\n        <section key={entry.title} className=\"dictWebsterLearner-Entry\">\n          <header className=\"dictWebsterLearner-Header\">\n            <StrElm tag=\"span\" className=\"hw_d hw_0\" html={entry.title} />\n            <Speaker src={entry.pron} />\n          </header>\n          {entry.infs && (\n            <div className=\"dictWebsterLearner-Header\">\n              <StrElm tag=\"span\" className=\"hw_infs_d\" html={entry.infs} />\n              <Speaker src={entry.infsPron} />\n            </div>\n          )}\n          {entry.labels && <StrElm className=\"labels\" html={entry.labels} />}\n          {entry.senses && <StrElm className=\"sblocks\" html={entry.senses} />}\n          {entry.arts &&\n            entry.arts.length > 0 &&\n            entry.arts.map(src => <img key={src} src={src} />)}\n          {entry.phrases && <StrElm className=\"dros\" html={entry.phrases} />}\n          {entry.derived && <StrElm className=\"uros\" html={entry.derived} />}\n        </section>\n      ))}\n    </>\n  )\n}\n\nfunction renderRelated(result: WebsterLearnerResultRelated) {\n  return (\n    <>\n      <p>Did you mean:</p>\n      <StrElm\n        tag=\"ul\"\n        className=\"dictWebsterLearner-Related\"\n        html={result.list}\n      />\n    </>\n  )\n}\n\nexport default DictWebsterLearner\n"
  },
  {
    "path": "src/components/dictionaries/websterlearner/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Merriam-Webster's Learner's Dictionary\",\n    \"zh-CN\": \"韦氏学习词典\",\n    \"zh-TW\": \"韋氏學習字典\"\n  },\n  \"options\": {\n    \"defs\": {\n      \"en\": \"Show definitions\",\n      \"zh-CN\": \"显示释义\",\n      \"zh-TW\": \"顯示解釋\"\n    },\n    \"phrase\": {\n      \"en\": \"Show phrases\",\n      \"zh-CN\": \"显示词组\",\n      \"zh-TW\": \"顯示片語\"\n    },\n    \"derived\": {\n      \"en\": \"Show derived words\",\n      \"zh-CN\": \"显示派生词\",\n      \"zh-TW\": \"顯示衍生字\"\n    },\n    \"arts\": {\n      \"en\": \"Show pictures\",\n      \"zh-CN\": \"显示图片释义\",\n      \"zh-TW\": \"顯示圖片解釋\"\n    },\n    \"related\": {\n      \"en\": \"Show related results\",\n      \"zh-CN\": \"失败时显示备选\",\n      \"zh-TW\": \"失敗時顯示備選\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/websterlearner/_style.shadow.scss",
    "content": ".dictWebsterLearner-Header {\n  .hw_txt {\n    font-size: 1.5em;\n    font-weight: bold;\n  }\n}\n\n.dictWebsterLearner-Related {\n  a {\n    margin-left: 2em;\n    color: #16a085;\n  }\n}\n\n.dictWebsterLearner-Entry {\n  & > .labels > .lb {\n    font-style: italic;\n  }\n\n  & > .labels > .gram > .gram_internal {\n    color: #757575;\n  }\n\n  & > .labels > .sl {\n    font-style: italic;\n  }\n\n  /*view:  dros*/\n  & > .dros {\n    margin-bottom: 1em;\n  }\n\n  & > .dros > .dro {\n    margin-bottom: 0.8em;\n    padding-left: 0.8em;\n  }\n\n  & > .dros > .dro:last-child {\n    margin-bottom: 0;\n  }\n\n  & > .dros > .dro > .dro_line {\n    margin-left: -0.8em;\n  }\n\n  & > .dros > .dro > .dro_line > * {\n    vertical-align: middle;\n  }\n\n  & > .dros > .dro > .dro_line > .dre {\n    display: inline;\n    font-weight: bold;\n    padding: 0;\n    margin: 0;\n    font-size: inherit;\n  }\n\n  & > .dros > .dro > .dro_line > .gram > .gram_internal {\n    color: #757575;\n  }\n\n  & > .dros > .dro > .dro_line > .sl {\n    font-style: italic;\n  }\n\n  & > .dros > .dro > .dro_line > .rsl {\n    font-style: italic;\n  }\n\n  & > .dros > .dro > .dxs {\n    margin-top: 0.6em;\n    display: block;\n  }\n\n  /*view:  uros*/\n  & > .uros {\n    margin-bottom: 1em;\n  }\n\n  & > .uros > .uro {\n    margin-bottom: 0.8em;\n    padding-left: 0.8em;\n  }\n\n  & > .uros > .uro:last-child {\n    margin-bottom: 0;\n  }\n\n  & > .uros > .uro > .uro_line {\n    margin-left: -0.8em;\n  }\n\n  & > .uros > .uro > .uro_line > * {\n    vertical-align: middle;\n  }\n\n  & > .uros > .uro > .uro_line > .ure {\n    display: inline;\n    font-weight: bold;\n    padding: 0;\n    margin: 0;\n    font-size: inherit;\n    margin-right: 0.5em;\n  }\n\n  & > .uros > .uro > .uro_line > .gram > .gram_internal {\n    color: #757575;\n  }\n\n  & > .uros > .uro > .uro_line > .lb {\n    font-style: italic;\n  }\n\n  & > .uros > .uro > .uro_line > .sl {\n    font-style: italic;\n  }\n\n  & > .uros > .uro > .uro_line > .fl {\n    color: #717274;\n    font-style: italic;\n    font-weight: bold;\n  }\n\n  & > .uros > .uro > .uro_def {\n    margin: 0.5em 0 0 0;\n  }\n\n  & > .uros > .uro > .uro_def:first-child {\n    margin-top: 0;\n  }\n\n  & > .uros > .uro > .uro_def > *:first-child {\n    margin-top: 0;\n  }\n\n  /*view:  cognate cross entries*/\n  & > .cxs {\n    margin-top: 1.2em;\n    margin-bottom: 1.2em;\n  }\n\n  & > .cxs .cx_link {\n    font-variant: small-caps;\n    font-size: 1.1em;\n    line-height: 1;\n  }\n\n  & > .cxs .cx_link sup {\n    font-size: 50%;\n  }\n\n  & > .cxs .cl {\n    font-style: italic;\n  }\n}\n\n/*utils*/\n.smark {\n  font-size: 117%;\n  font-weight: bold;\n  padding-left: 1px;\n}\n\n.boxy {\n  -moz-box-sizing: border-box;\n  -webkit-box-sizing: border-box;\n  box-sizing: border-box;\n}\n\n.comma {\n  margin: 0;\n  padding: 0;\n  font-weight: normal;\n  color: #000;\n  font-style: normal;\n}\n\n.semicolon {\n  margin: 0;\n  padding: 0;\n  font-weight: normal;\n  color: #000;\n  font-style: normal;\n}\n\n.bc {\n  font-weight: bold;\n}\n\n/*mw markup overrides*/\n.mw_spm_aq {\n  color: #000;\n  margin: 0;\n  padding: 0;\n  padding: 0;\n  border: none;\n}\n\n/*Dotted line*/\n.dline {\n  background: #ffffff\n    url(data:image/gif;base64,R0lGODlhAwAOAJEAALKysv///////wAAACH5BAEHAAIALAAAAAADAA4AAAIGlI+poN0FADs=)\n    repeat-x left center;\n  margin-bottom: 1.3em;\n}\n\n.dline span {\n  background-color: #fff;\n  color: #3692a4;\n  line-height: 1.1;\n  font-weight: bold;\n  display: inline-block;\n  margin: 0;\n  padding-right: 0.8em;\n}\n\n/*WOD only*/\n.wod_img_tit {\n  color: #757575;\n  font-style: italic;\n  margin-bottom: 15px;\n}\n\n.wod_img_act {\n  padding: 0;\n  line-height: 0;\n}\n\n/*non-HW prons*/\n.pron_w {\n  color: #717274;\n  font-weight: normal;\n  font-size: 109%;\n}\n\n.pron_i {\n  color: #d40218;\n  font-size: 140%;\n}\n\n.pron_i:hover {\n  color: #ff0000;\n  text-decoration: none;\n}\n\n/*non-HW variations*/\n.v_label {\n  font-style: italic;\n  color: #757575;\n}\n\n.v_text {\n  font-weight: bold;\n}\n\n/*non-HW inflections*/\n.i_label {\n  font-style: italic;\n  color: #757575;\n}\n\n.i_text {\n  font-weight: bold;\n}\n\n/*view:  phrasal verbs*/\n.pva {\n  font-weight: bold;\n}\n\n/*view:  synonym paragraphs*/\n.synpar {\n  border: 1px solid #ccc;\n  padding: 4px 10px 6px 10px;\n  margin-bottom: 1em;\n}\n\n.synpar > .synpar_part {\n  margin-bottom: 10px;\n}\n\n.synpar > .synpar_part:last-child {\n  margin-bottom: 0;\n}\n\n.synpar > .synpar_part > .synpar_w {\n  font-variant: small-caps;\n  font-size: 1.1em;\n  line-height: 1;\n}\n\n.synpar > .synpar_part > *:last-child {\n  margin-bottom: 0;\n}\n\n/*view:  supplementary notes*/\n.snotes {\n  margin-bottom: 1em;\n  overflow: hidden;\n}\n\n.snotes > * {\n  margin-bottom: 0.8em;\n}\n\n.snotes > *:last-child {\n  margin-bottom: 0;\n}\n\n.snotes > .snote_text > .snote_link {\n  font-variant: small-caps;\n  font-size: 1.1em;\n  line-height: 1;\n}\n\n.snotes > .snote_text > .snote_link sup {\n  font-size: 50%;\n}\n\n/*view:  supplementary noteboxes*/\n.snotebox {\n  border: 1px solid #ccc;\n  padding: 0.8em;\n  margin-bottom: 1em;\n  overflow: hidden;\n}\n\n.snotebox > .snotebox_text {\n  text-align: justify;\n}\n\n.snotebox > .snotebox_text > .snote_link {\n  font-variant: small-caps;\n  font-size: 1.1em;\n  line-height: 1;\n}\n\n.snotebox > .snotebox_text > .snote_link sup {\n  font-size: 50%;\n}\n\n/*view:  sense blocks*/\n.sblocks {\n  margin-bottom: 1em;\n}\n\n.sblocks .sense {\n  margin-bottom: 0.5em;\n}\n\n.sblocks > .sblock {\n  width: 100%;\n  margin: 6px 0px 0px 0px;\n}\n\n.sblocks > .sblock > .sblock_c {\n  margin-bottom: 10px;\n}\n\n.sblocks > .sblock > .sblock_c:last-child {\n  margin-bottom: 0;\n}\n\n.sblocks > .sblock > .sblock_c > .sn_block_num {\n  float: left;\n  font-weight: bold;\n}\n\n.sblocks > .sblock > .sblock_c > .scnt > .sblock_labels > .slb {\n  font-style: italic;\n}\n\n.sblocks > .sblock > .sblock_c > .scnt > .sblock_labels > .ssla {\n  font-style: italic;\n}\n\n.sblocks\n  > .sblock\n  > .sblock_c\n  > .scnt\n  > .sblock_labels\n  > .sgram\n  > .sgram_internal {\n  color: #757575;\n}\n\n.sblocks > .sblock > .sblock_c > .scnt > .sblock_labels > .bnote {\n  font-weight: bold;\n  font-style: italic;\n}\n\n.sblocks > .sblock > .sblock_c > .scnt {\n  margin-bottom: 10px;\n  overflow: auto;\n}\n\n.sblocks > .sblock > .sblock_c > .scnt:last-child {\n  margin-bottom: 0;\n}\n\n.sblocks > .sblock > .sblock_c > .scnt > .sense > *:last-child {\n  margin-bottom: 0;\n  padding-bottom: 0;\n}\n\n.sblocks > .sblock > .sblock_c > .scnt > .sense > .sn_letter {\n  font-weight: bold;\n  float: left;\n}\n\n.sblocks > .sblock > .sblock_c > .scnt > .sense > .sd {\n  font-style: italic;\n}\n\n.sblocks > .sblock > .sblock_c > .scnt > .sense > .bnote {\n  font-weight: bold;\n  font-style: italic;\n  color: #000;\n}\n\n.sblocks > .sblock > .sblock_c > .scnt > .sense > .slb {\n  font-style: italic;\n}\n\n.sblocks > .sblock > .sblock_c > .scnt > .sense > .sgram > .sgram_internal {\n  color: #757575;\n}\n\n.sblocks > .sblock > .sblock_c > .scnt > .sense > .ssla {\n  font-style: italic;\n}\n\n.sblocks > .sblock > .sblock_c > .scnt > .sense > .def_text {\n  margin-bottom: 6px;\n}\n\n.sblocks > .sblock > .sblock_c > .scnt > .sense > .def_text > .bc {\n  font-weight: bold;\n}\n\n.sblocks > .sblock > .sblock_c > .scnt > .sense > .def_labels {\n  margin-top: 10px;\n  padding-left: 14px;\n}\n\n.sblocks\n  > .sblock\n  > .sblock_c\n  > .scnt\n  > .sense\n  > .def_labels\n  > .wsgram\n  > .wsgram_internal {\n  color: #757575;\n}\n\n.sblocks > .sblock > .sblock_c > .scnt > .sense > .def_labels > .sl {\n  font-style: italic;\n}\n\n/*view:  verbal illustration*/\n.vis_w {\n  margin-bottom: 0.3em;\n}\n\n.vis_w > .vis {\n  padding-left: 1.2em;\n}\n\n.vis_w > .vis > .vi {\n  padding: 0.08em 0;\n  list-style-type: square;\n  color: #5fb68c;\n}\n\n.vis_w > .vis > .vi > .vi_content {\n  color: var(--color-font-grey);\n  margin-left: -2px;\n  line-height: 1.3;\n}\n\n.vis_w > .vis > .vi:first-child {\n  padding-top: 0;\n}\n\n.vis_w > .vis > .vi:last-child {\n  padding-bottom: 0;\n}\n\n.vis_w > .vi_more {\n  display: none;\n}\n\n/*view:  inline synonyms*/\n.isyns > .bc {\n  font-weight: bold;\n}\n\n.isyns > .isyn_link {\n  font-variant: small-caps;\n  font-size: 1.1em;\n  line-height: 1;\n}\n\n.isyns > .isyn_link sup {\n  font-size: 50%;\n}\n\n/*view:  directional cross entries*/\n.dxs.dxs_nl {\n  margin-bottom: 1em;\n}\n\n.dxs .dx .dx_link {\n  font-variant: small-caps;\n  font-size: 1.1em;\n  line-height: 1;\n}\n\n.dxs .dx .dx_link sup {\n  font-size: 50%;\n}\n\n.dxs .dx .dx_span {\n  font-variant: small-caps;\n  font-size: 1.1em;\n  line-height: 1;\n}\n\n.dxs .dx .dx_span sup {\n  font-size: 50%;\n}\n\n.dxs .dx .dx_ab {\n  font-style: italic;\n}\n\n.dxs .dx .dx_tag {\n  font-style: italic;\n}\n\n/*view:  cas*/\n.cas {\n  margin-top: 14px;\n}\n\n.cas > .ca_prefix {\n  font-style: italic;\n}\n\n.cas > .ca_text {\n  font-style: italic;\n}\n\n/*view:  usage paragraphs*/\n.usage_par {\n  padding: 0.3em 0.7em;\n  border: 1px solid #ccc;\n  margin-bottom: 1em;\n}\n\n.usage_par > .usage_par_h {\n  font-weight: bold;\n}\n\n.usage_par > .ud_text {\n  text-align: justify;\n}\n\n.usage_par > * {\n  margin-bottom: 0.5em;\n}\n\n.usage_par > *:last-child {\n  margin-bottom: 0;\n}\n\n/*view:  synref*/\n.synref_h1 {\n  font-weight: bold;\n}\n\n.synref_link {\n  font-variant: small-caps;\n  font-size: 1.1em;\n  line-height: 1;\n}\n\n.synref_link sup {\n  font-size: 50%;\n}\n\n/*view:  usageref*/\n.usageref_block > .usageref_h1 {\n  font-weight: bold;\n}\n\n.usageref_block > .usageref_link {\n  font-variant: small-caps;\n  font-size: 1.1em;\n  line-height: 1;\n}\n\n.usageref_block > .usageref_link sup {\n  font-size: 50%;\n}\n"
  },
  {
    "path": "src/components/dictionaries/websterlearner/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type WebsterlearnerConfig = DictItem<{\n  defs: boolean\n  phrase: boolean\n  derived: boolean\n  arts: boolean\n  related: boolean\n}>\n\nexport default (): WebsterlearnerConfig => ({\n  lang: '10000000',\n  selectionLang: {\n    english: true,\n    chinese: false,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 265,\n  selectionWC: {\n    min: 1,\n    max: 5\n  },\n  options: {\n    defs: true,\n    phrase: true,\n    derived: true,\n    arts: true,\n    related: true\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/websterlearner/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  HTMLString,\n  getInnerHTML,\n  handleNoResult,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult\n} from '../helpers'\nimport { DictConfigs } from '@/app-config'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return `http://www.learnersdictionary.com/definition/${text\n    .trim()\n    .split(/\\s+/)\n    .join('-')}`\n}\n\nconst HOST = 'http://www.learnersdictionary.com'\n\ninterface WebsterLearnerResultItem {\n  title: HTMLString\n  pron?: string\n\n  infs?: HTMLString\n  infsPron?: string\n\n  labels?: HTMLString\n  senses?: HTMLString\n  phrases?: HTMLString\n  derived?: HTMLString\n  arts?: string[]\n}\n\nexport interface WebsterLearnerResultLex {\n  type: 'lex'\n  items: WebsterLearnerResultItem[]\n}\n\nexport interface WebsterLearnerResultRelated {\n  type: 'related'\n  list: HTMLString\n}\n\nexport type WebsterLearnerResult =\n  | WebsterLearnerResultLex\n  | WebsterLearnerResultRelated\n\ntype WebsterLearnerSearchResult = DictSearchResult<WebsterLearnerResult>\ntype WebsterLearnerSearchResultLex = DictSearchResult<WebsterLearnerResultLex>\n\nexport const search: SearchFunction<WebsterLearnerResult> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  const options = profile.dicts.all.websterlearner.options\n\n  return fetchDirtyDOM(\n    'http://www.learnersdictionary.com/definition/' +\n      text.toLocaleLowerCase().replace(/[^A-Za-z0-9]+/g, '-')\n  )\n    .catch(handleNetWorkError)\n    .then(doc => checkResult(doc, options))\n}\n\nfunction checkResult(\n  doc: Document,\n  options: DictConfigs['websterlearner']['options']\n): WebsterLearnerSearchResult | Promise<WebsterLearnerSearchResult> {\n  const $alternative = doc.querySelector<HTMLAnchorElement>(\n    '[id^=\"spelling\"] .links'\n  )\n  if (!$alternative) {\n    return handleDOM(doc, options)\n  } else if (options.related) {\n    return {\n      result: {\n        type: 'related',\n        list: getInnerHTML(HOST, $alternative)\n      }\n    }\n  }\n  return handleNoResult()\n}\n\nfunction handleDOM(\n  doc: Document,\n  options: DictConfigs['websterlearner']['options']\n): WebsterLearnerSearchResultLex | Promise<WebsterLearnerSearchResultLex> {\n  doc.querySelectorAll('.d_hidden').forEach(el => el.remove())\n\n  const result: WebsterLearnerResultLex = {\n    type: 'lex',\n    items: []\n  }\n  const audio: { us?: string } = {}\n\n  doc.querySelectorAll('.entry').forEach($entry => {\n    const entry: WebsterLearnerResultItem = {\n      title: ''\n    }\n\n    const $headword = $entry.querySelector('.hw_d')\n    if (!$headword) {\n      return\n    }\n    const $pron = $headword.querySelector<HTMLAnchorElement>('.play_pron')\n    if ($pron) {\n      const path = ($pron.dataset.lang || '').replace('_', '/')\n      const dir = $pron.dataset.dir || ''\n      const file = $pron.dataset.file || ''\n      entry.pron = `http://media.merriam-webster.com/audio/prons/${path}/mp3/${dir}/${file}.mp3`\n      audio.us = entry.pron\n      $pron.remove()\n    }\n    entry.title = getInnerHTML(HOST, $headword)\n\n    const $headwordInfs = $entry.querySelector('.hw_infs_d')\n    if ($headwordInfs) {\n      const $pron = $headwordInfs.querySelector<HTMLAnchorElement>('.play_pron')\n      if ($pron) {\n        const path = ($pron.dataset.lang || '').replace('_', '/')\n        const dir = $pron.dataset.dir || ''\n        const file = $pron.dataset.file || ''\n        entry.infsPron = `http://media.merriam-webster.com/audio/prons/${path}/mp3/${dir}/${file}.mp3`\n        $pron.remove()\n      }\n      entry.infs = getInnerHTML(HOST, $headwordInfs)\n    }\n\n    entry.labels = getInnerHTML(HOST, $entry, '.labels')\n\n    if (options.defs) {\n      entry.senses = getInnerHTML(HOST, $entry, '.sblocks')\n    }\n\n    if (options.phrase) {\n      entry.phrases = getInnerHTML(HOST, $entry, '.dros')\n    }\n\n    if (options.derived) {\n      entry.derived = getInnerHTML(HOST, $entry, '.uros')\n    }\n\n    if (options.arts) {\n      entry.arts = Array.from(\n        $entry.querySelectorAll<HTMLImageElement>('.arts img')\n      ).map($img => $img.src)\n    }\n\n    if (\n      entry.senses ||\n      entry.phrases ||\n      entry.derived ||\n      (entry.arts && entry.arts.length > 0)\n    ) {\n      result.items.push(entry)\n    }\n  })\n\n  if (result.items.length > 0) {\n    return { result, audio }\n  }\n\n  return handleNoResult()\n}\n"
  },
  {
    "path": "src/components/dictionaries/wikipedia/View.tsx",
    "content": "import React, { FC, useState, ReactNode, useEffect } from 'react'\nimport {\n  WikipediaResult,\n  WikipediaPayload,\n  fetchLangList,\n  LangList\n} from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { message } from '@/_helpers/browser-api'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictWikipedia: FC<ViewPorps<WikipediaResult>> = ({\n  result,\n  searchText\n}) => {\n  const [langList, setLangList] = useState<LangList>()\n  const { t } = useTranslate('content')\n\n  useEffect(() => {\n    setLangList([])\n  }, [result.langSelector])\n\n  const handleSelectChanged = (e: React.ChangeEvent<HTMLSelectElement>) => {\n    if (e.target.value) {\n      searchText<WikipediaPayload>({\n        id: 'wikipedia',\n        payload: {\n          url: e.target.value\n        }\n      })\n    }\n  }\n\n  let langSelector: ReactNode = null\n  if (langList && langList.length > 0) {\n    langSelector = (\n      <select onChange={handleSelectChanged} defaultValue={''}>\n        <option key=\"\" value=\"\">\n          {t('chooseLang')}\n        </option>\n        {langList.map(item => (\n          <option key={item.url} value={item.url}>\n            {item.title}\n          </option>\n        ))}\n      </select>\n    )\n  } else if (result.langSelector) {\n    langSelector = (\n      <button\n        className=\"dictWikipedia-LangSelectorBtn\"\n        onClick={async () => {\n          setLangList(\n            await message.send<\n              'DICT_ENGINE_METHOD',\n              ReturnType<typeof fetchLangList>\n            >({\n              type: 'DICT_ENGINE_METHOD',\n              payload: {\n                id: 'wikipedia',\n                method: 'fetchLangList',\n                args: [result.langSelector]\n              }\n            })\n          )\n        }}\n      >\n        {t('fetchLangList')}\n      </button>\n    )\n  }\n\n  return (\n    <>\n      <h1 className=\"dictWikipedia-Title\">{result.title}</h1>\n      {langSelector}\n      <div className=\"dictWikipedia-Content\" onClick={handleEntryClick}>\n        <StrElm className=\"client-js\" html={result.content} />\n      </div>\n    </>\n  )\n}\n\nfunction handleEntryClick(e: React.MouseEvent<HTMLDivElement>) {\n  if (!e.target['classList']) {\n    return\n  }\n\n  let $header = e.target as HTMLElement\n  if (!$header.classList.contains('section-heading')) {\n    $header = $header.parentElement as HTMLElement\n    if (!$header || !$header.classList.contains('section-heading')) {\n      return\n    }\n  }\n\n  e.stopPropagation()\n  e.preventDefault()\n\n  // Toggle titles\n\n  $header.classList.toggle('open-block')\n\n  const $content = $header.nextElementSibling\n  if ($content) {\n    const pressed = $header.classList.contains('open-block').toString()\n    $content.classList.toggle('open-block')\n    $content.setAttribute('aria-pressed', pressed)\n    $content.setAttribute('aria-expanded', pressed)\n  }\n\n  const $arrow = $header.querySelector('.mw-ui-icon-mf-arrow')\n  if ($arrow) {\n    $arrow.classList.toggle('mf-mw-ui-icon-rotate-flip')\n  }\n}\n\nexport default DictWikipedia\n"
  },
  {
    "path": "src/components/dictionaries/wikipedia/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Wikipedia\",\n    \"zh-CN\": \"维基百科\",\n    \"zh-TW\": \"維基百科\"\n  },\n  \"options\": {\n    \"lang\": {\n      \"en\": \"Language\",\n      \"zh-CN\": \"百科语言\",\n      \"zh-TW\": \"百科語言\"\n    },\n    \"lang-auto\": {\n      \"en\": \"Auto (respect Wiki settings)\",\n      \"zh-CN\": \"自动（遵循维基账户设定）\",\n      \"zh-TW\": \"自动（遵循維基賬戶設定）\"\n    },\n    \"lang-zh\": {\n      \"en\": \"中文（简繁由维基账户设定）\",\n      \"zh-CN\": \"中文（简繁由维基账户设定）\",\n      \"zh-TW\": \"中文（簡繁由維基賬戶設定）\"\n    },\n    \"lang-zh-cn\": {\n      \"en\": \"强制简体中文子页面\",\n      \"zh-CN\": \"强制简体中文子页面\",\n      \"zh-TW\": \"强制简体中文子页面\"\n    },\n    \"lang-zh-tw\": {\n      \"en\": \"強制台灣正體子頁面\",\n      \"zh-CN\": \"強制台灣正體子頁面\",\n      \"zh-TW\": \"強制台灣正體子頁面\"\n    },\n    \"lang-zh-hk\": {\n      \"en\": \"強制香港繁體子頁面\",\n      \"zh-CN\": \"強制香港繁體子頁面\",\n      \"zh-TW\": \"強制香港繁體子頁面\"\n    },\n    \"lang-en\": {\n      \"en\": \"English\",\n      \"zh-CN\": \"English\",\n      \"zh-TW\": \"English\"\n    },\n    \"lang-ja\": {\n      \"en\": \"日本語\",\n      \"zh-CN\": \"日本語\",\n      \"zh-TW\": \"日本語\"\n    },\n    \"lang-fr\": {\n      \"en\": \"Français\",\n      \"zh-CN\": \"Français\",\n      \"zh-TW\": \"Français\"\n    },\n    \"lang-de\": {\n      \"en\": \"Deutschsprachige\",\n      \"zh-CN\": \"Deutschsprachige\",\n      \"zh-TW\": \"Deutschsprachige\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/wikipedia/_style.shadow.scss",
    "content": "@import '@/_sass_shared/_reset.scss';\n\n.dictWikipedia-LangSelectorBtn {\n  margin: 0.2em 0 1em;\n  padding: 0.1em 0.2em;\n  color: currentColor;\n  border: 1px solid #666;\n  border-radius: 2px;\n  cursor: pointer;\n}\n\n.dictWikipedia-Title {\n  font-size: 1.5em;\n}\n\n.dictWikipedia-Content {\n  img {\n    display: inline;\n  }\n\n  ul li {\n    list-style-type: unset;\n  }\n\n  div,\n  span,\n  h1,\n  h2,\n  h3,\n  h4,\n  h5,\n  h6,\n  p,\n  blockquote,\n  pre,\n  a,\n  abbr,\n  acronym,\n  address,\n  big,\n  cite,\n  code,\n  del,\n  ins,\n  em,\n  img,\n  small,\n  strike,\n  strong,\n  sub,\n  sup,\n  tt,\n  b,\n  u,\n  i,\n  center,\n  dl,\n  dt,\n  dd,\n  ol,\n  ul,\n  li,\n  fieldset,\n  form,\n  label,\n  legend,\n  input,\n  button,\n  select,\n  audio,\n  video {\n    margin: 0;\n    padding: 0;\n    border: 0;\n    font: inherit;\n    font-size: 100%;\n    vertical-align: baseline;\n    background: none;\n  }\n  table,\n  caption,\n  tbody,\n  tfoot,\n  thead,\n  tr,\n  th,\n  td {\n    font-size: 100%;\n  }\n  caption {\n    font-weight: bold;\n  }\n  button {\n    border: 0;\n    background-color: transparent;\n    cursor: pointer;\n  }\n  input {\n    line-height: normal;\n  }\n  ol,\n  ul {\n    list-style: none;\n  }\n  table {\n    border-collapse: collapse;\n  }\n}\n\n.mw-cite-backlink,\n.cite-accessibility-label {\n  -moz-user-select: none;\n  -webkit-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n.mw-references-columns {\n  -webkit-column-width: 30em;\n  -moz-column-width: 30em;\n  column-width: 30em;\n}\n.mw-references-columns li {\n  -webkit-column-break-inside: avoid;\n  page-break-inside: avoid;\n  break-inside: avoid-column;\n}\nsup.reference {\n  unicode-bidi: -moz-isolate;\n  unicode-bidi: -webkit-isolate;\n  unicode-bidi: isolate;\n  white-space: nowrap;\n  font-weight: normal;\n  font-style: normal;\n}\nol.references li:target,\nsup.reference:target {\n  background-color: #eaf3ff;\n}\n.mw-ext-cite-error {\n  font-weight: bold;\n  unicode-bidi: embed;\n}\n.mw-cite-dir-ltr .reference-text {\n  direction: ltr;\n  unicode-bidi: embed;\n}\n.mw-cite-dir-rtl .reference-text {\n  direction: rtl;\n  unicode-bidi: embed;\n}\n.mw-references-columns {\n  -webkit-column-width: 25em;\n  -moz-column-width: 25em;\n  column-width: 25em;\n}\n.mwe-math-mathml-inline {\n  display: inline !important;\n}\n.mwe-math-mathml-display {\n  display: block !important;\n  margin-left: auto;\n  margin-right: auto;\n}\n.mwe-math-mathml-a11y {\n  clip: rect(1px, 1px, 1px, 1px);\n  overflow: hidden;\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  opacity: 0;\n}\n.mwe-math-fallback-image-inline {\n  display: inline-block;\n  vertical-align: middle;\n}\n.mwe-math-fallback-image-display {\n  display: block;\n  margin-left: auto !important;\n  margin-right: auto !important;\n}\n.mwe-math-fallback-source-inline {\n  display: inline;\n  vertical-align: middle;\n}\n.mwe-math-fallback-source-display {\n  display: block;\n  margin-left: auto;\n  margin-right: auto;\n}\nimg.tex {\n  vertical-align: middle;\n}\ndiv.mwe-math-element {\n  overflow-x: auto;\n  max-width: 100%;\n}\n.hlist dl,\n.hlist ol,\n.hlist ul {\n  margin: 0;\n  padding: 0;\n}\n.hlist dl dl,\n.hlist ol dl,\n.hlist ul dl,\n.hlist dl ol,\n.hlist ol ol,\n.hlist ul ol,\n.hlist dl ul,\n.hlist ol ul,\n.hlist ul ul {\n  display: inline;\n}\n.hlist dd,\n.hlist dt,\n.hlist li {\n  margin: 0;\n  display: inline;\n}\n.hlist > ul li,\n.hlist > dl li,\nul.hlist li {\n  display: inline-block;\n  margin-right: 8px;\n}\n.hlist-separated li:after {\n  content: '•' !important;\n  padding-left: 8px;\n  font-size: 1em;\n  line-height: 1;\n}\n.hlist-separated :last-child:after {\n  content: none !important;\n}\n.mw-ui-button {\n  font-family: inherit;\n  font-size: 1em;\n  display: inline-block;\n  min-width: 4em;\n  max-width: 28.75em;\n  padding: 0.546875em 1em;\n  line-height: 1.286;\n  margin: 0;\n  border-radius: 2px;\n  -webkit-box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  box-sizing: border-box;\n  -webkit-appearance: none;\n  *display: inline;\n  zoom: 1;\n  vertical-align: middle;\n  background-color: #f8f9fa;\n  color: #222222;\n  border: 1px solid #a2a9b1;\n  text-align: center;\n  font-weight: bold;\n  cursor: pointer;\n}\n.mw-ui-button:visited {\n  color: #222222;\n}\n.mw-ui-button:hover {\n  background-color: #ffffff;\n  color: #444444;\n  border-color: #a2a9b1;\n}\n.mw-ui-button:focus {\n  background-color: #ffffff;\n  color: #222222;\n  border-color: #3366cc;\n  box-shadow: inset 0 0 0 1px #3366cc, inset 0 0 0 2px #ffffff;\n}\n.mw-ui-button:active,\n.mw-ui-button.is-on,\n.mw-ui-button.mw-ui-checked {\n  background-color: #d9d9d9;\n  color: #000000;\n  border-color: #72777d;\n  box-shadow: none;\n}\n.mw-ui-button:disabled {\n  background-color: #c8ccd1;\n  color: #fff;\n  border-color: #c8ccd1;\n}\n.mw-ui-button:disabled:hover,\n.mw-ui-button:disabled:active {\n  background-color: #c8ccd1;\n  color: #fff;\n  box-shadow: none;\n  border-color: #c8ccd1;\n}\n.mw-ui-button:focus {\n  outline-width: 0;\n}\n.mw-ui-button:focus::-moz-focus-inner {\n  border-color: transparent;\n  padding: 0;\n}\n.mw-ui-button:not(:disabled) {\n  -webkit-transition: background-color 100ms, color 100ms, border-color 100ms,\n    box-shadow 100ms;\n  -moz-transition: background-color 100ms, color 100ms, border-color 100ms,\n    box-shadow 100ms;\n  transition: background-color 100ms, color 100ms, border-color 100ms,\n    box-shadow 100ms;\n}\n.mw-ui-button:disabled {\n  text-shadow: none;\n  cursor: default;\n}\n.mw-ui-button.mw-ui-big {\n  font-size: 1.3em;\n}\n.mw-ui-button.mw-ui-block {\n  display: block;\n  width: 100%;\n  margin-left: auto;\n  margin-right: auto;\n}\n.mw-ui-button.mw-ui-progressive {\n  background-color: #3366cc;\n  color: #fff;\n  border: 1px solid #3366cc;\n}\n.mw-ui-button.mw-ui-progressive:hover {\n  background-color: #447ff5;\n  border-color: #447ff5;\n}\n.mw-ui-button.mw-ui-progressive:focus {\n  box-shadow: inset 0 0 0 1px #3366cc, inset 0 0 0 2px #ffffff;\n}\n.mw-ui-button.mw-ui-progressive:active,\n.mw-ui-button.mw-ui-progressive.is-on,\n.mw-ui-button.mw-ui-progressive.mw-ui-checked {\n  background-color: #2a4b8d;\n  border-color: #2a4b8d;\n  box-shadow: none;\n}\n.mw-ui-button.mw-ui-progressive:disabled {\n  background-color: #c8ccd1;\n  color: #fff;\n  border-color: #c8ccd1;\n}\n.mw-ui-button.mw-ui-progressive:disabled:hover,\n.mw-ui-button.mw-ui-progressive:disabled:active,\n.mw-ui-button.mw-ui-progressive:disabled.mw-ui-checked {\n  background-color: #c8ccd1;\n  color: #fff;\n  border-color: #c8ccd1;\n  box-shadow: none;\n}\n.mw-ui-button.mw-ui-progressive.mw-ui-quiet {\n  color: #222222;\n}\n.mw-ui-button.mw-ui-progressive.mw-ui-quiet:hover {\n  background-color: transparent;\n  color: #447ff5;\n}\n.mw-ui-button.mw-ui-progressive.mw-ui-quiet:active,\n.mw-ui-button.mw-ui-progressive.mw-ui-quiet.mw-ui-checked {\n  color: #2a4b8d;\n}\n.mw-ui-button.mw-ui-progressive.mw-ui-quiet:focus {\n  background-color: transparent;\n  color: #3366cc;\n}\n.mw-ui-button.mw-ui-progressive.mw-ui-quiet:disabled {\n  color: #c8ccd1;\n}\n.mw-ui-button.mw-ui-destructive {\n  background-color: #dd3333;\n  color: #fff;\n  border: 1px solid #dd3333;\n}\n.mw-ui-button.mw-ui-destructive:hover {\n  background-color: #ff4242;\n  border-color: #ff4242;\n}\n.mw-ui-button.mw-ui-destructive:focus {\n  box-shadow: inset 0 0 0 1px #dd3333, inset 0 0 0 2px #ffffff;\n}\n.mw-ui-button.mw-ui-destructive:active,\n.mw-ui-button.mw-ui-destructive.is-on,\n.mw-ui-button.mw-ui-destructive.mw-ui-checked {\n  background-color: #b32424;\n  border-color: #b32424;\n  box-shadow: none;\n}\n.mw-ui-button.mw-ui-destructive:disabled {\n  background-color: #c8ccd1;\n  color: #fff;\n  border-color: #c8ccd1;\n}\n.mw-ui-button.mw-ui-destructive:disabled:hover,\n.mw-ui-button.mw-ui-destructive:disabled:active,\n.mw-ui-button.mw-ui-destructive:disabled.mw-ui-checked {\n  background-color: #c8ccd1;\n  color: #fff;\n  border-color: #c8ccd1;\n  box-shadow: none;\n}\n.mw-ui-button.mw-ui-destructive.mw-ui-quiet {\n  color: #222222;\n}\n.mw-ui-button.mw-ui-destructive.mw-ui-quiet:hover {\n  background-color: transparent;\n  color: #ff4242;\n}\n.mw-ui-button.mw-ui-destructive.mw-ui-quiet:active,\n.mw-ui-button.mw-ui-destructive.mw-ui-quiet.mw-ui-checked {\n  color: #b32424;\n}\n.mw-ui-button.mw-ui-destructive.mw-ui-quiet:focus {\n  background-color: transparent;\n  color: #dd3333;\n}\n.mw-ui-button.mw-ui-destructive.mw-ui-quiet:disabled {\n  color: #c8ccd1;\n}\n.mw-ui-button.mw-ui-quiet {\n  background: transparent;\n  border: 0;\n  text-shadow: none;\n  color: #222222;\n}\n.mw-ui-button.mw-ui-quiet:hover {\n  background-color: transparent;\n  color: #444444;\n}\n.mw-ui-button.mw-ui-quiet:active,\n.mw-ui-button.mw-ui-quiet.mw-ui-checked {\n  color: #000000;\n}\n.mw-ui-button.mw-ui-quiet:focus {\n  background-color: transparent;\n  color: #222222;\n}\n.mw-ui-button.mw-ui-quiet:disabled {\n  color: #c8ccd1;\n}\n.mw-ui-button.mw-ui-quiet:hover,\n.mw-ui-button.mw-ui-quiet:focus {\n  box-shadow: none;\n}\n.mw-ui-button.mw-ui-quiet:active,\n.mw-ui-button.mw-ui-quiet:disabled {\n  background: transparent;\n}\ninput.mw-ui-button::-moz-focus-inner,\nbutton.mw-ui-button::-moz-focus-inner {\n  margin-top: -1px;\n  margin-bottom: -1px;\n}\na.mw-ui-button {\n  text-decoration: none;\n}\na.mw-ui-button:hover,\na.mw-ui-button:focus {\n  text-decoration: none;\n}\n.mw-ui-button-group > * {\n  min-width: 48px;\n  border-radius: 0;\n  float: left;\n}\n.mw-ui-button-group > *:first-child {\n  border-top-left-radius: 2px;\n  border-bottom-left-radius: 2px;\n}\n.mw-ui-button-group > *:not(:first-child) {\n  border-left: 0;\n}\n.mw-ui-button-group > *:last-child {\n  border-top-right-radius: 2px;\n  border-bottom-right-radius: 2px;\n}\n.mw-ui-button-group .is-on .button {\n  cursor: default;\n}\n.mw-ui-icon {\n  position: relative;\n  line-height: 1.5em;\n  min-height: 1.5em;\n  min-width: 1.5em;\n}\nspan.mw-ui-icon {\n  display: inline-block;\n}\n.mw-ui-icon.mw-ui-icon-element {\n  text-indent: -999px;\n  overflow: hidden;\n  width: 3.5em;\n  min-width: 3.5em;\n  max-width: 3.5em;\n}\n.mw-ui-icon.mw-ui-icon-element:before {\n  left: 0;\n  right: 0;\n  position: absolute;\n  margin: 0 1em;\n}\n.mw-ui-icon.mw-ui-icon-element.mw-ui-icon-large {\n  width: 4.625em;\n  min-width: 4.625em;\n  max-width: 4.625em;\n  line-height: 4.625em;\n  min-height: 4.625em;\n}\n.mw-ui-icon.mw-ui-icon-element.mw-ui-icon-large:before {\n  min-height: 4.625em;\n}\n.mw-ui-icon.mw-ui-icon-before:before,\n.mw-ui-icon.mw-ui-icon-element:before {\n  background-position: 50% 50%;\n  background-repeat: no-repeat;\n  background-size: 100% auto;\n  float: left;\n  display: block;\n  min-height: 1.5em;\n  content: '';\n}\n.mw-ui-icon.mw-ui-icon-before:before {\n  position: relative;\n  width: 1.5em;\n  margin-right: 1em;\n}\n.mw-ui-icon.mw-ui-icon-small:before {\n  background-size: 66.67% auto;\n}\n\n#content {\n  border-top: 1px solid transparent;\n  padding-bottom: 32px;\n}\n.header-container {\n  border-bottom: 1px solid #666;\n}\n.header-container.header-chrome {\n  border: 0;\n  box-shadow: inset 0 -1px 3px rgba(0, 0, 0, 0.08);\n}\n#footer-info-lastmod {\n  display: none;\n}\n.last-modified-bar a,\n.last-modified-bar a:visited {\n  color: #54595d;\n}\n.last-modified-bar a:nth-child(2),\n.last-modified-bar a:visited:nth-child(2) {\n  font-weight: bold;\n}\n.header {\n  display: table;\n  width: 100%;\n  border-spacing: 0;\n  border-collapse: collapse;\n  height: 3.35em;\n  white-space: nowrap;\n  border-top: 1px solid #c8ccd1;\n  margin-top: -1px;\n}\n.header > div {\n  width: 3.35em;\n  position: relative;\n  vertical-align: middle;\n  display: table-cell;\n}\n.header > div a {\n  display: block;\n}\n.header .branding-box {\n  width: auto;\n}\n.header .branding-box h1,\n.header .branding-box a {\n  margin-left: 5px;\n  font-size: 1em;\n  text-decoration: none;\n  color: #222222;\n}\n.header .branding-box h1 span,\n.header .branding-box a span {\n  line-height: 1;\n  font-size: inherit;\n}\n.header .branding-box h1 img,\n.header .branding-box a img {\n  vertical-align: middle;\n}\n.header .branding-box h1 > *,\n.header .branding-box a > * {\n  float: left;\n}\n.header .branding-box h1 sup,\n.header .branding-box a sup {\n  color: #54595d;\n  display: none;\n}\n.beta .header .branding-box h1 sup,\n.beta .header .branding-box a sup {\n  display: initial;\n}\n.header > .header-title {\n  vertical-align: middle;\n}\n#searchInput {\n  cursor: text;\n}\n.search-box,\n.header .search-box {\n  display: none;\n  width: auto;\n}\n.search-box .search {\n  outline: 0;\n  width: 100%;\n  background-color: #fff !important;\n  -webkit-appearance: none;\n  padding: 0.5em 0 0.5em 32px;\n  background-position: left 6px center;\n  background-repeat: no-repeat;\n  -webkit-background-size: 20px 20px;\n  background-size: 20px 20px;\n  border-radius: 2px;\n  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n  margin-top: 0;\n}\ninput.search::-webkit-search-decoration,\ninput.search::-webkit-search-cancel-button,\ninput.search::-webkit-search-results-button,\ninput.search::-webkit-search-results-decoration {\n  display: none;\n}\n.content h1 .edit-page {\n  font-size: 0.58823529em;\n}\n.content h2 {\n  clear: left;\n}\n.content h2 .edit-page {\n  font-size: 0.66666667em;\n}\n.content h3 .edit-page {\n  font-size: 0.83333333em;\n}\n.content .edit-page {\n  display: inline-block;\n}\n.content .collapsible-heading .edit-page {\n  visibility: hidden;\n}\n.content .collapsible-heading.open-block .edit-page {\n  visibility: visible;\n}\n.content .mw-parser-output > h2,\n.content .section-heading {\n  border-bottom: 1px solid #eaecf0;\n  margin-bottom: 0.5em;\n}\n.content .mw-parser-output > h2 .indicator,\n.content .section-heading .indicator {\n  font-size: 0.4em;\n  margin-left: -1em;\n}\n.content .mw-parser-output > h1,\n.content .mw-parser-output > h2,\n.content .mw-parser-output > h3,\n.content .mw-parser-output > h4,\n.content .mw-parser-output > h5,\n.content .section-heading,\n.content .in-block {\n  display: table;\n}\n.content .mw-parser-output > h1 .mw-headline,\n.content .mw-parser-output > h2 .mw-headline,\n.content .mw-parser-output > h3 .mw-headline,\n.content .mw-parser-output > h4 .mw-headline,\n.content .mw-parser-output > h5 .mw-headline,\n.content .section-heading .mw-headline,\n.content .in-block .mw-headline {\n  width: 100%;\n}\n.content .mw-parser-output > h1 > span,\n.content .mw-parser-output > h2 > span,\n.content .mw-parser-output > h3 > span,\n.content .mw-parser-output > h4 > span,\n.content .mw-parser-output > h5 > span,\n.content .section-heading > span,\n.content .in-block > span {\n  display: table-cell;\n  vertical-align: middle;\n}\n.client-nojs .section-heading .indicator {\n  display: none;\n}\n#page-secondary-actions {\n  clear: both;\n}\n#page-secondary-actions a {\n  margin: 10px 2px 2px 0;\n}\n.transparent-shield,\n.navigation-drawer {\n  position: absolute;\n  z-index: 0;\n  visibility: hidden;\n}\n#bodyContent .panel .content,\n.overlay .content-header,\n.overlay .panel,\n.page-list.side-list .list-thumb,\n.page-list li,\n.topic-title-list li,\n.site-link-list li,\n.previewnote p,\n.pointer-overlay,\n.drawer,\n.successbox,\n.errorbox,\n.list-header,\n.warningbox,\n.mw-revision {\n  padding-left: 16px;\n  padding-right: 16px;\n}\n.talk-overlay .comment .comment-content,\n.backtotop,\n.image-list,\n.pre-content,\n#mw-content-text > form > .oo-ui-fieldLayout > .oo-ui-fieldLayout-body,\n#mw-content-text > form > .oo-ui-widget,\n.content,\n.post-content {\n  margin: 0 16px;\n}\n@media all and (min-width: 720px) {\n  .page-summary-list,\n  .topic-title-list,\n  .site-link-list,\n  .overlay .panel,\n  .list-header {\n    padding-left: 3.35em;\n    padding-right: 3.35em;\n  }\n}\n.heading-holder {\n  padding: 20px 0 3.6em;\n  position: relative;\n}\n.heading-holder .tagline {\n  color: #54595d;\n  font-size: 0.85em;\n  margin: 2px 0 0;\n}\n.heading-holder .tagline:first-letter {\n  text-transform: capitalize;\n}\n#section_0 {\n  padding-top: 0;\n  padding-bottom: 0;\n  border-bottom: 0;\n}\n#page-actions {\n  font-size: 1.1em;\n  float: none;\n  border: 0;\n  overflow: hidden;\n  position: absolute;\n  bottom: 0;\n  width: 100%;\n  border-top: 1px solid #eaecf0;\n  border-bottom: 1px solid #c8ccd1;\n  padding: 0.5em 0;\n}\n#page-actions li {\n  display: inline-block;\n  position: relative;\n  cursor: pointer;\n  margin-right: 0;\n  margin-bottom: 0;\n}\n#page-actions li a {\n  position: absolute;\n  display: block;\n  width: 100%;\n  height: 100%;\n  margin: 0 0 8px;\n  cursor: pointer;\n}\n#page-actions li:first-child {\n  margin-top: 0;\n}\n#page-actions .language-selector {\n  float: left;\n  margin-left: -1em;\n}\n#page-actions .language-selector.disabled {\n  cursor: default;\n  opacity: 0.25;\n}\n#page-actions #ca-edit {\n  margin-right: -1em;\n}\n.client-nojs .watch-this-article {\n  visibility: hidden;\n}\n.client-nojs .is-authenticated .watch-this-article {\n  visibility: visible;\n}\n@media all and (max-width: 320px - 1) {\n  .client-nojs #page-actions {\n    display: none;\n  }\n  .client-nojs #section_0 {\n    border: 0;\n  }\n}\n.view-border-box *,\n.view-border-box {\n  -webkit-box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  box-sizing: border-box;\n}\n.toc-mobile,\n.toc,\n.client-nojs .toc-mobile,\n.client-js .mw-redirectedfrom,\n.printfooter,\n.jsonly {\n  display: none;\n}\n.client-js .jsonly {\n  display: inherit;\n}\n.hidden {\n  display: none !important;\n}\n#mw-mf-viewport {\n  position: relative;\n  height: 100%;\n}\n#mw-mf-page-center {\n  width: 100%;\n  position: relative;\n  z-index: 0;\n}\n.minerva-footer {\n  border-top: solid 1px #c8ccd1;\n  overflow: auto;\n  padding-bottom: 6px;\n}\nfooter .hlist li:after {\n  color: #3366cc;\n}\nfooter .last-modified-bar {\n  border-bottom: solid 1px #c8ccd1;\n  display: block;\n  color: #54595d;\n  line-height: 1.5em;\n  transition: background-color 0.2s ease, color 0.2s ease;\n}\nfooter .last-modifier-tagline {\n  display: block;\n  width: 100%;\n  font-size: 0.9em;\n  padding: 7px 2em 7px 0;\n}\nfooter .indicator {\n  position: absolute;\n  right: -1em;\n}\n.client-nojs footer .indicator {\n  display: none;\n}\nfooter > .post-content {\n  overflow: auto;\n  margin-top: 42px;\n}\nfooter > .post-content > * {\n  margin-bottom: 9px;\n}\nfooter > .post-content > h2 {\n  border-bottom: solid 1px #c8ccd1;\n  padding-bottom: 10px;\n  margin-top: 42px;\n  font-size: 1em;\n  font-weight: bold;\n}\nfooter > .post-content > h2:first-child {\n  margin-top: 0;\n}\nfooter > .post-content .hlist,\nfooter > .post-content .license {\n  font-size: 0.875em;\n}\n@media (min-width: 720px) {\n  footer .last-modified-bar {\n    padding-left: 0;\n    padding-right: 0;\n    font-size: 1em;\n  }\n}\n\n.content {\n  line-height: 1.65;\n  word-wrap: break-word;\n}\n.content .center {\n  width: 100%;\n  text-align: center;\n}\n.content .center > *,\n.content .center .thumb {\n  margin-left: auto;\n  margin-right: auto;\n}\n.client-js .collapsible-block {\n  display: none;\n}\n.client-js .collapsible-block.open-block {\n  display: block;\n}\n.nomobile {\n  display: none !important;\n}\n@media all and (min-width: 720px) {\n  .client-js [onclick] + .collapsible-block {\n    display: block;\n  }\n}\n.content figure,\n.content .thumb {\n  margin: 0.6em 0;\n}\n.content figure .thumbinner,\n.content .thumb .thumbinner {\n  margin: 0 auto;\n  max-width: 100% !important;\n}\n.content figcaption,\n.content .thumbcaption {\n  margin: 0.5em 0 0;\n  font-size: 0.8em;\n  line-height: 1.5;\n  padding: 0 !important;\n  color: #54595d;\n}\n.content .thumbcaption {\n  width: auto !important;\n}\n.content .thumbborder {\n  border: 1px solid #c8ccd1;\n}\n.content .magnify {\n  display: none;\n}\n.content img {\n  vertical-align: middle;\n}\n.content .floatright {\n  clear: right;\n  float: right;\n  margin: 0 0 0.6em 0.6em;\n}\n.content .floatleft {\n  clear: left;\n  float: left;\n  margin: 0 0.6em 0.6em 0;\n}\n.content a > img,\n.content a > .lazy-image-placeholder,\n.content noscript > img {\n  max-width: 100% !important;\n}\n.content noscript > img,\n.content a > img {\n  height: auto !important;\n}\n.content .noresize {\n  max-width: 100%;\n  overflow-x: auto;\n}\n.content .noresize a > img {\n  max-width: none !important;\n}\nh1 {\n  font-size: 1.7em;\n}\nh2 {\n  font-size: 1.5em;\n}\nh3 {\n  font-size: 1.2em;\n  font-weight: bold;\n}\nh4 {\n  font-weight: bold;\n}\n.pre-content h1,\n.content h1,\n.content h2 {\n  font-family: 'Linux Libertine', 'Georgia', 'Times', serif;\n}\nh3,\nh4,\nh5,\nh6 {\n  font-family: 'Helvetica Neue', 'Helvetica', 'Nimbus Sans L', 'Arial',\n    'Liberation Sans', sans-serif;\n}\n.pre-content h1,\n.content h1,\n.content h2,\nh3,\nh4,\nh5,\nh6 {\n  line-height: 1.3;\n  word-wrap: break-word;\n  word-break: break-word;\n}\n.content h2,\n.content h3,\n.content h4,\n.content h5,\n.content h6 {\n  padding: 0.5em 0;\n}\n.content ul {\n  list-style: square outside;\n  padding-left: 1em;\n}\n.content ul > li > ul {\n  list-style-type: disc;\n}\n.content ul > li > ul > li > ul {\n  list-style-type: circle;\n}\n.content ol {\n  list-style: decimal inside;\n}\n.content ol ol,\n.content ul ol,\n.content ol ul,\n.content ul ul {\n  margin-left: 1em;\n}\n.content ol li,\n.content ul li {\n  margin-bottom: 10px;\n}\n.content ol li:last-child,\n.content ul li:last-child {\n  margin-bottom: inherit;\n}\ndl {\n  margin-left: 1em;\n}\ndl dt {\n  font-weight: bold;\n}\ndl dd {\n  display: block;\n  overflow: auto;\n}\na:not([href]) {\n  color: #222222;\n}\na {\n  text-decoration: none;\n  color: #3366cc;\n}\na:visited {\n  color: #5a3696;\n}\na:active {\n  color: #faa700;\n}\na:hover {\n  text-decoration: underline;\n}\na.new,\na.new:visited,\na.new:hover {\n  color: #dd3333;\n}\na.new > *,\na.new:visited > *,\na.new:hover > * {\n  color: #dd3333;\n}\na.external {\n  -webkit-background-size: 10px 10px;\n  background-size: 10px 10px;\n  background-repeat: no-repeat;\n  background-position: center right;\n  padding-right: 13px;\n}\n.return-link {\n  display: block;\n  font-size: 0.9em;\n  margin-top: 1.5em;\n}\n.plainlinks a {\n  background: none !important;\n  padding: 0 !important;\n}\n.content p {\n  margin: 0.5em 0 1em 0;\n}\n.content kbd,\n.content samp,\n.content code,\n.content pre {\n  font-family: monospace, monospace;\n  border: solid 1px #c8ccd1;\n  white-space: pre-wrap;\n}\n.content code {\n  padding: 0.2em 0.5em;\n}\n.content pre {\n  padding: 1em;\n}\nb,\nstrong {\n  font-weight: bold;\n}\nblockquote {\n  border-left: 3px solid #c8ccd1;\n  font-family: 'Linux Libertine', 'Georgia', 'Times', serif;\n  font-size: 1.1em;\n  padding: 1em 25px 1em 30px;\n  position: relative;\n  overflow: hidden;\n}\nem,\ni {\n  font-style: italic;\n}\nsup {\n  vertical-align: super;\n}\nsub {\n  vertical-align: sub;\n}\nsub,\nsup {\n  font-size: 0.75em;\n  line-height: 1;\n}\n@media all and (max-width: 720px) {\n  .content table {\n    display: block;\n    width: 100% !important;\n  }\n  .content caption {\n    display: block;\n  }\n}\n.content table {\n  margin: 1em 0;\n  overflow: auto;\n  overflow-y: hidden;\n  overflow-x: auto;\n}\n.content table caption {\n  text-align: left;\n}\ntable.wikitable {\n  border: 1px solid rgba(133, 133, 133, 0.28);\n}\ntable.wikitable > tr > th,\ntable.wikitable > tr > td,\ntable.wikitable > * > tr > th,\ntable.wikitable > * > tr > td {\n  border: 1px solid rgba(133, 133, 133, 0.28);\n  padding: 0.2em;\n}\n.ambox,\ntable.ambox {\n  display: none;\n  margin: 0;\n}\n.issues-group-B .ambox {\n  display: block;\n}\n.client-js .ambox {\n  cursor: pointer;\n  font-size: 0.8em;\n  width: auto;\n  background: #f8f9fa;\n  color: #54595d;\n  margin-bottom: 1px;\n}\n.client-js .ambox tbody {\n  display: table;\n  width: 100%;\n}\n.client-js .ambox .mbox-text-span {\n  max-height: 3.3em;\n  height: 3.3em;\n  overflow: hidden;\n}\n.client-js .ambox div {\n  margin: 0 !important;\n  padding: 0 !important;\n}\n.client-js .ambox td {\n  position: relative;\n  padding: 8px 8px 8px 32px;\n}\n.client-js .ambox b {\n  font-weight: inherit;\n}\n.client-js .ambox a {\n  color: inherit !important;\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important;\n}\n.client-js .ambox a:hover,\n.client-js .ambox a:focus {\n  text-decoration: none;\n  outline: inherit;\n}\n.client-js .ambox small,\n.client-js .ambox .hide-when-compact,\n.client-js .ambox .mw-collapsible-content,\n.client-js .ambox .mbox-empty-cell,\n.client-js .ambox hr,\n.client-js .ambox .verbose,\n.client-js .ambox table,\n.client-js .ambox .mbox-image {\n  display: none;\n}\n.client-js .ambox .mw-ui-icon {\n  position: absolute;\n  left: -8px;\n}\n.client-js .ambox .mw-ui-icon:before {\n  -webkit-background-size: 75% auto;\n  background-size: 75% auto;\n}\n.client-js .ambox .ambox-learn-more {\n  color: #3366cc;\n  position: absolute;\n  right: 8px;\n  bottom: 8px;\n  z-index: 1;\n  line-height: 1.65;\n}\n.client-js .ambox .ambox-learn-more:before {\n  content: '';\n  position: absolute;\n  z-index: -1;\n  bottom: 0;\n  right: 0;\n  width: 100%;\n  height: 100%;\n  box-sizing: content-box;\n  padding-left: 4em;\n  background: -webkit-linear-gradient(\n    left,\n    rgba(248, 249, 250, 0) 0,\n    #f8f9fa 3em\n  );\n  background: -moz-gradient(left, rgba(248, 249, 250, 0) 0, #f8f9fa 3em);\n  background: linear-gradient(to right, rgba(248, 249, 250, 0) 0, #f8f9fa 3em);\n}\n@media screen and (min-width: 720px) {\n  .client-js .ambox .mbox-text-span {\n    height: auto;\n    margin-bottom: 24px !important;\n  }\n  .client-js .ambox .ambox-learn-more {\n    left: 32px;\n    right: 0;\n    background: none;\n  }\n  .client-js .ambox .ambox-learn-more:before {\n    top: -1.65em;\n    width: 10px;\n  }\n}\n.collapsible td {\n  width: auto !important;\n}\n.content .vertical-navbox,\n.content .navbox {\n  display: none;\n}\n.content .action-edit .fmbox,\n.content .tmbox,\n.content #coordinates,\n.content .topicon {\n  display: none !important;\n}\n.content table {\n  float: none !important;\n  margin-left: 0 !important;\n  margin-right: 0 !important;\n}\n.content table.infobox {\n  font-size: 90%;\n  position: relative;\n  // border: 1px solid #eaecf0;\n  margin-bottom: 2em;\n  // background-color: #f8f9fa;\n  display: flex;\n  flex: 1 1 100%;\n  flex-flow: column nowrap;\n}\n.content table.infobox th,\n.content table.infobox td {\n  vertical-align: top;\n  border: 0;\n  border-bottom: 1px solid rgba(186, 186, 186, 0.44);\n  padding: 7px 10px;\n}\n.content table.infobox tbody > tr > td,\n.content table.infobox tbody > tr > th {\n  flex: 1 0;\n}\n.content table.infobox td:only-child,\n.content table.infobox th:only-child {\n  width: 100%;\n}\n.content table.infobox tr:last-child th,\n.content table.infobox tr:last-child td {\n  border: 0;\n}\n.content table.infobox > tbody,\n.content table.infobox > caption {\n  display: flex;\n  flex-flow: column nowrap;\n}\n.content table.infobox > tbody > tr {\n  min-width: 100%;\n  display: flex;\n  flex-flow: row nowrap;\n}\n.content .mw-content-ltr table.infobox {\n  text-align: left;\n}\n.content .mw-content-rtl table.infobox {\n  text-align: right;\n}\n#filetoc {\n  display: none;\n}\n.references-column-count,\n.column-count {\n  -moz-column-width: 35em;\n  -webkit-column-width: 35em;\n  column-width: 35em;\n}\n.references li:target {\n  background-color: #def;\n}\n.hatnote,\n.dablink,\n.rellink {\n  padding: 5px 7px;\n  color: #54595d;\n  font-size: 0.8em;\n  background-color: #f8f9fa;\n  margin-bottom: 1px;\n  overflow: hidden;\n}\n.hatnote a,\n.dablink a,\n.rellink a {\n  color: #3366cc;\n}\n@media all and (min-width: 720px) {\n  .content .vertical-navbox,\n  .content .navbox {\n    display: inherit;\n  }\n}\n@media all and (max-width: 720px) {\n  .content table.multicol > tr > td,\n  .content table.multicol > tbody > tr > td {\n    display: block !important;\n    width: auto !important;\n  }\n  .content .thumb .thumbinner {\n    display: -webkit-flex;\n    display: -moz-flex;\n    display: -ms-flexbox;\n    display: flex;\n    justify-content: center;\n    flex-wrap: wrap;\n    align-content: flex-start;\n    flex-direction: column;\n  }\n  .content .thumb .thumbinner > .thumbcaption {\n    -webkit-box-pack: justify;\n    -moz-box-pack: justify;\n    -ms-flex-pack: justify;\n    justify-content: space-between;\n    -webkit-box-flex: 1;\n    -moz-box-flex: 1;\n    width: 100%;\n    -ms-flex: 1 0 100%;\n    flex: 1 0 100%;\n    -webkit-box-ordinal-group: 1;\n    -moz-box-ordinal-group: 1;\n    -ms-flex-order: 1;\n    order: 1;\n    display: block;\n  }\n}\ninput.search,\n.mw-ui-icon-minerva-magnifying-glass:before {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.content.styles.images&image=input.search%2C.mw-ui-icon-minerva-magnifying-glass%3Abefore&format=rasterized&lang=zh-hans&skin=minerva);\n  background-image: linear-gradient(transparent, transparent),\n    url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.content.styles.images&image=input.search%2C.mw-ui-icon-minerva-magnifying-glass%3Abefore&format=original&lang=zh-hans&skin=minerva);\n}\na.external {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.content.styles.images&image=a.external&format=rasterized&lang=zh-hans&skin=minerva);\n  background-image: linear-gradient(transparent, transparent),\n    url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.content.styles.images&image=a.external&format=original&lang=zh-hans&skin=minerva);\n}\n.mw-ui-icon-minerva-watch:before {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.icons.images&image=watch&format=rasterized&lang=zh-hans&skin=minerva);\n  background-image: linear-gradient(transparent, transparent),\n    url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.icons.images&image=watch&format=original&lang=zh-hans&skin=minerva);\n}\n.mw-ui-icon-minerva-watched:before {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.icons.images&image=watched&format=rasterized&lang=zh-hans&skin=minerva);\n  background-image: linear-gradient(transparent, transparent),\n    url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.icons.images&image=watched&format=original&lang=zh-hans&skin=minerva);\n}\n.mw-ui-icon-minerva-warning:before {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.icons.images&image=warning&format=rasterized&lang=zh-hans&skin=minerva);\n  background-image: linear-gradient(transparent, transparent),\n    url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.icons.images&image=warning&format=original&lang=zh-hans&skin=minerva);\n}\n.mw-ui-icon-minerva-mainmenu:before {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.icons.images&image=mainmenu&format=rasterized&lang=zh-hans&skin=minerva);\n  background-image: linear-gradient(transparent, transparent),\n    url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.icons.images&image=mainmenu&format=original&lang=zh-hans&skin=minerva);\n}\n.mw-ui-icon-minerva-edit:before {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.icons.images&image=edit&format=rasterized&lang=zh-hans&skin=minerva);\n  background-image: linear-gradient(transparent, transparent),\n    url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.icons.images&image=edit&format=original&lang=zh-hans&skin=minerva);\n}\n.mw-ui-icon-minerva-edit-enabled:before {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.icons.images&image=edit-enabled&format=rasterized&lang=zh-hans&skin=minerva);\n  background-image: linear-gradient(transparent, transparent),\n    url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.icons.images&image=edit-enabled&format=original&lang=zh-hans&skin=minerva);\n}\n.mw-ui-icon-minerva-language-switcher:before {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.icons.images&image=language-switcher&format=rasterized&lang=zh-hans&skin=minerva);\n  background-image: linear-gradient(transparent, transparent),\n    url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.icons.images&image=language-switcher&format=original&lang=zh-hans&skin=minerva);\n}\n.mw-ui-icon-minerva-notifications:before {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.icons.loggedin&image=notifications&format=rasterized&lang=zh-hans&skin=minerva);\n  background-image: linear-gradient(transparent, transparent),\n    url(https://en.wikipedia.org/w/load.php?modules=skins.minerva.icons.loggedin&image=notifications&format=original&lang=zh-hans&skin=minerva);\n}\n.user-button {\n  position: relative;\n}\n.user-button .label {\n  visibility: hidden;\n}\n.user-button.loading span {\n  display: none;\n}\n.notification-count {\n  margin: auto;\n  height: 24px;\n  color: #54595d;\n  cursor: pointer;\n}\n.notification-count .circle {\n  border-radius: 50%;\n  border: 2px solid #54595d;\n  margin: auto;\n  height: 22px;\n  width: 22px;\n  display: block;\n  text-align: center;\n  display: -webkit-box;\n  display: flex;\n  -webkit-box-align: center;\n  align-items: center;\n  -webkit-box-pack: center;\n  justify-content: center;\n}\n.notification-count .circle span {\n  font-weight: bold;\n  font-size: 13px;\n  line-height: 1;\n  letter-spacing: -0.5px;\n}\n.notification-count.notification-unseen {\n  color: #ffffff;\n}\n.notification-count.notification-unseen .circle {\n  background: #dd3333;\n  border-color: #dd3333;\n}\n.notification-count.max {\n  right: 0.2em;\n  width: 2em;\n  height: 2em;\n  line-height: 2em;\n  font-size: 0.7em;\n}\n.notification-count:hover {\n  text-decoration: none;\n}\n@media screen and (min-width: 720px) {\n  .client-js #searchIcon {\n    display: none;\n  }\n  .header .branding-box {\n    width: 11.0625em;\n  }\n  .header .search-box {\n    display: table-cell;\n    width: auto;\n  }\n  .header .search-box .search {\n    width: 23.4375em;\n  }\n  .toc-mobile {\n    display: table;\n    visibility: visible;\n    position: relative;\n    font-size: 1.3em;\n    margin: 1em 0;\n    border: solid 1px transparent;\n  }\n  .toc-mobile > h2 {\n    visibility: hidden;\n    font-family: 'Helvetica Neue', 'Helvetica', 'Nimbus Sans L', 'Arial',\n      'Liberation Sans', sans-serif;\n    font-size: 0.8em;\n    font-weight: bold;\n    border-bottom: 0;\n    padding: 0.7em 0;\n  }\n  .pre-content,\n  #mw-content-text > form,\n  .post-content {\n    max-width: 993.3px;\n    margin: 0 3.35em;\n  }\n  .content figure,\n  .content .thumb {\n    max-width: 704px;\n  }\n  .content figure.mw-halign-left,\n  .content .thumb.mw-halign-left,\n  .content figure.tleft,\n  .content .thumb.tleft {\n    float: left;\n    clear: left;\n    margin-right: 1.4em;\n  }\n  .content figure.mw-default-size,\n  .content .thumb.mw-default-size,\n  .content figure.mw-halign-right,\n  .content .thumb.mw-halign-right,\n  .content figure.tright,\n  .content .thumb.tright {\n    float: right;\n    clear: right;\n    margin-left: 1.4em;\n  }\n  .content table caption {\n    background: inherit;\n  }\n  .content table tbody {\n    display: table-row-group;\n  }\n  .last-modified-bar {\n    background-color: transparent;\n    padding-left: 0;\n    padding-right: 0;\n    font-size: 1em;\n  }\n}\n@media screen and (min-width: 1000px) {\n  .banner-container,\n  .header,\n  .page-header-bar,\n  .content-header,\n  .overlay-header,\n  .content,\n  .overlay-content,\n  .content-unstyled,\n  .pre-content,\n  .post-content,\n  #mw-content-text > form,\n  #mw-mf-page-center .pointer-overlay {\n    margin-left: auto;\n    margin-right: auto;\n    width: 90%;\n    max-width: 993.3px;\n  }\n  .header {\n    max-width: 995.3px;\n  }\n}\n@media all and (min-width: 720px) {\n  table.infobox {\n    margin: 0.5em 0 1em 35px !important;\n    max-width: 320px;\n    width: auto !important;\n    float: right !important;\n    clear: right !important;\n  }\n}\n\n////// style tags ///////////\n.mw-ui-anchor.mw-ui-progressive {\n  color: #3366cc;\n}\n.mw-ui-anchor.mw-ui-progressive:hover {\n  color: #6a8fda;\n}\n.mw-ui-anchor.mw-ui-progressive:focus,\n.mw-ui-anchor.mw-ui-progressive:active {\n  color: #254a95;\n  outline: 0;\n}\n.mw-ui-anchor.mw-ui-progressive.mw-ui-quiet {\n  color: #54595d;\n  text-decoration: none;\n}\n.mw-ui-anchor.mw-ui-progressive.mw-ui-quiet:hover {\n  color: #3366cc;\n}\n.mw-ui-anchor.mw-ui-progressive.mw-ui-quiet:focus,\n.mw-ui-anchor.mw-ui-progressive.mw-ui-quiet:active {\n  color: #254a95;\n}\n.mw-ui-anchor.mw-ui-destructive {\n  color: #dd3333;\n}\n.mw-ui-anchor.mw-ui-destructive:hover {\n  color: #e76e6e;\n}\n.mw-ui-anchor.mw-ui-destructive:focus,\n.mw-ui-anchor.mw-ui-destructive:active {\n  color: #ae1d1d;\n  outline: 0;\n}\n.mw-ui-anchor.mw-ui-destructive.mw-ui-quiet {\n  color: #54595d;\n  text-decoration: none;\n}\n.mw-ui-anchor.mw-ui-destructive.mw-ui-quiet:hover {\n  color: #dd3333;\n}\n.mw-ui-anchor.mw-ui-destructive.mw-ui-quiet:focus,\n.mw-ui-anchor.mw-ui-destructive.mw-ui-quiet:active {\n  color: #ae1d1d;\n}\n.page-list,\n.topic-title-list,\n.site-link-list {\n  overflow: hidden;\n}\n.page-list li,\n.topic-title-list li,\n.site-link-list li {\n  color: #54595d;\n  position: relative;\n  padding-top: 0.8em;\n  padding-bottom: 0.8em;\n  margin: -1px 0 0;\n  line-height: 1;\n}\n.page-list li .watch-this-article,\n.topic-title-list li .watch-this-article,\n.site-link-list li .watch-this-article {\n  position: absolute;\n  right: 0;\n  top: 0.8em;\n  margin-top: 1px;\n}\n.page-list li .watch-this-article button,\n.topic-title-list li .watch-this-article button,\n.site-link-list li .watch-this-article button {\n  position: absolute;\n  text-indent: inherit;\n  outline: 0;\n}\n.page-list li > a,\n.topic-title-list li > a,\n.site-link-list li > a {\n  display: block;\n  color: #54595d;\n}\n.page-list li > a:active,\n.topic-title-list li > a:active,\n.site-link-list li > a:active,\n.page-list li > a:hover,\n.topic-title-list li > a:hover,\n.site-link-list li > a:hover,\n.page-list li > a:visited,\n.topic-title-list li > a:visited,\n.site-link-list li > a:visited {\n  text-decoration: none;\n  color: #54595d;\n}\n.page-list.thumbs li,\n.topic-title-list.thumbs li,\n.site-link-list.thumbs li,\n.page-list.side-list li,\n.topic-title-list.side-list li,\n.site-link-list.side-list li {\n  padding-left: 85px;\n}\n.page-list .info,\n.topic-title-list .info,\n.site-link-list .info {\n  font-size: 0.7em;\n  text-transform: uppercase;\n}\n.page-list .info,\n.topic-title-list .info,\n.site-link-list .info,\n.page-list .component,\n.topic-title-list .component,\n.site-link-list .component {\n  color: #72777d;\n  line-height: 1.2;\n}\n.page-list .title h3,\n.topic-title-list .title h3,\n.site-link-list .title h3,\n.page-list .title .mw-mf-user,\n.topic-title-list .title .mw-mf-user,\n.site-link-list .title .mw-mf-user,\n.page-list .title .component,\n.topic-title-list .title .component,\n.site-link-list .title .component,\n.page-list .title .info,\n.topic-title-list .title .info,\n.site-link-list .title .info {\n  margin: 0.5em 0;\n}\n.page-list.thumbs .title,\n.topic-title-list.thumbs .title,\n.site-link-list.thumbs .title {\n  padding-right: 24px;\n}\n.page-list .list-thumb,\n.topic-title-list .list-thumb,\n.site-link-list .list-thumb {\n  position: absolute;\n  width: 70px;\n  height: 100%;\n  top: 0;\n  left: 0;\n}\n.page-list p,\n.topic-title-list p,\n.site-link-list p {\n  font-size: 0.9em;\n  line-height: normal;\n}\n.page-list.side-list .list-thumb,\n.topic-title-list.side-list .list-thumb,\n.site-link-list.side-list .list-thumb {\n  padding-top: 0.8em;\n  padding-bottom: 0.8em;\n  color: #222222;\n}\n.page-list.side-list .list-thumb p,\n.topic-title-list.side-list .list-thumb p,\n.site-link-list.side-list .list-thumb p {\n  line-height: 1.2;\n  margin: 0.5em 0;\n}\n.page-list.side-list .list-thumb .timestamp,\n.topic-title-list.side-list .list-thumb .timestamp,\n.site-link-list.side-list .list-thumb .timestamp {\n  margin-bottom: 0.65em;\n}\n@media all and (min-width: 720px) {\n  .page-summary-list,\n  .topic-title-list,\n  .site-link-list {\n    padding-top: 0;\n    padding-bottom: 0;\n  }\n}\n.page-summary h2,\n.page-summary h3 {\n  font: inherit;\n  font-weight: bold;\n  color: #54595d;\n}\n.page-summary h2 a,\n.page-summary h3 a {\n  color: inherit;\n}\n.page-summary h2 strong,\n.page-summary h3 strong {\n  text-decoration: underline;\n}\n.list-header {\n  font-weight: bold;\n  font-size: 0.85em;\n  padding-top: 0.5em;\n  padding-bottom: 0.4em;\n  color: #72777d;\n}\n.list-thumb {\n  background-repeat: no-repeat;\n  background-position: center center;\n}\n.list-thumb.list-thumb-none {\n  background-image: url(https://en.wikipedia.org/w/extensions/MobileFrontend/resources/mobile.pagesummary.styles/noimage.png?537bb);\n  background-image: linear-gradient(transparent, transparent),\n    url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 56 56%22%3E%3Cpath fill=%22%23eaecf0%22 d=%22M0 0h56v56H0z%22/%3E%3Cpath fill=%22%2372777d%22 d=%22M36.4 13.5H17.8v24.9c0 1.4.9 2.3 2.3 2.3h18.7v-25c.1-1.4-1-2.2-2.4-2.2zM30.2 17h5.1v6.4h-5.1V17zm-8.8 0h6v1.8h-6V17zm0 4.6h6v1.8h-6v-1.8zm0 15.5v-1.8h13.8v1.8H21.4zm13.8-4.5H21.4v-1.8h13.8v1.8zm0-4.7H21.4v-1.8h13.8v1.8z%22/%3E%3C/svg%3E');\n}\n.list-thumb.list-thumb-x {\n  -webkit-background-size: 100% auto;\n  background-size: 100% auto;\n}\n.list-thumb.list-thumb-y {\n  -webkit-background-size: auto 100%;\n  background-size: auto 100%;\n}\n.mw-ui-icon-mf-alert:before {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=mobile.startup.images.variants&image=alert&format=rasterized&lang=zh-hans&skin=minerva&version=0rl97mj);\n  background-image: linear-gradient(transparent, transparent),\n    url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2220%22 height=%2220%22 viewBox=%220 0 20 20%22%3E%3Ctitle%3Ealert%3C/title%3E%3Cpath d=%22M19.64 16.36L11.53 2.3A1.85 1.85 0 0 0 10 1.21 1.85 1.85 0 0 0 8.48 2.3L.36 16.36C-.48 17.81.21 19 1.88 19h16.24c1.67 0 2.36-1.19 1.52-2.64zM11 16H9v-2h2zm0-4H9V6h2z%22/%3E%3C/svg%3E');\n}\n.mw-ui-icon-mf-alert-gray:before {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=mobile.startup.images.variants&image=alert&variant=gray&format=rasterized&lang=zh-hans&skin=minerva&version=0rl97mj);\n  background-image: linear-gradient(transparent, transparent),\n    url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2220%22 height=%2220%22 viewBox=%220 0 20 20%22%3E%3Cg fill=%22%23a2a9b1%22%3E%3Ctitle xmlns:default=%22http://www.w3.org/2000/svg%22%3Ealert%3C/title%3E%3Cpath xmlns:default=%22http://www.w3.org/2000/svg%22 d=%22M19.64 16.36L11.53 2.3A1.85 1.85 0 0 0 10 1.21 1.85 1.85 0 0 0 8.48 2.3L.36 16.36C-.48 17.81.21 19 1.88 19h16.24c1.67 0 2.36-1.19 1.52-2.64zM11 16H9v-2h2zm0-4H9V6h2z%22/%3E%3C/g%3E%3C/svg%3E');\n}\n.mw-ui-icon-mf-alert-invert:before {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=mobile.startup.images.variants&image=alert&variant=invert&format=rasterized&lang=zh-hans&skin=minerva&version=0rl97mj);\n  background-image: linear-gradient(transparent, transparent),\n    url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2220%22 height=%2220%22 viewBox=%220 0 20 20%22%3E%3Cg fill=%22%23fff%22%3E%3Ctitle xmlns:default=%22http://www.w3.org/2000/svg%22%3Ealert%3C/title%3E%3Cpath xmlns:default=%22http://www.w3.org/2000/svg%22 d=%22M19.64 16.36L11.53 2.3A1.85 1.85 0 0 0 10 1.21 1.85 1.85 0 0 0 8.48 2.3L.36 16.36C-.48 17.81.21 19 1.88 19h16.24c1.67 0 2.36-1.19 1.52-2.64zM11 16H9v-2h2zm0-4H9V6h2z%22/%3E%3C/g%3E%3C/svg%3E');\n}\n.mw-ui-icon-mf-arrow:before {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=mobile.startup.images.variants&image=arrow&format=rasterized&lang=zh-hans&skin=minerva&version=0rl97mj);\n  background-image: linear-gradient(transparent, transparent),\n    url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 -407 24 24%22%3E%3Cpath d=%22M21.348-401.268q.94 0 1.61.668l.92.922-11.858 11.86-11.822-11.842.922-.94q.65-.686 1.59-.686.94 0 1.61.668l7.718 7.7 7.7-7.682q.67-.668 1.61-.668z%22/%3E%3C/svg%3E');\n}\n.mw-ui-icon-mf-arrow-gray:before {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=mobile.startup.images.variants&image=arrow&variant=gray&format=rasterized&lang=zh-hans&skin=minerva&version=0rl97mj);\n  background-image: linear-gradient(transparent, transparent),\n    url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 -407 24 24%22%3E%3Cg fill=%22%23a2a9b1%22%3E%3Cpath xmlns:default=%22http://www.w3.org/2000/svg%22 d=%22M21.348-401.268q.94 0 1.61.668l.92.922-11.858 11.86-11.822-11.842.922-.94q.65-.686 1.59-.686.94 0 1.61.668l7.718 7.7 7.7-7.682q.67-.668 1.61-.668z%22/%3E%3C/g%3E%3C/svg%3E');\n}\n.mw-ui-icon-mf-arrow-invert:before {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=mobile.startup.images.variants&image=arrow&variant=invert&format=rasterized&lang=zh-hans&skin=minerva&version=0rl97mj);\n  background-image: linear-gradient(transparent, transparent),\n    url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 -407 24 24%22%3E%3Cg fill=%22%23fff%22%3E%3Cpath xmlns:default=%22http://www.w3.org/2000/svg%22 d=%22M21.348-401.268q.94 0 1.61.668l.92.922-11.858 11.86-11.822-11.842.922-.94q.65-.686 1.59-.686.94 0 1.61.668l7.718 7.7 7.7-7.682q.67-.668 1.61-.668z%22/%3E%3C/g%3E%3C/svg%3E');\n}\n.mw-ui-icon-mf-back:before {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=mobile.startup.images.variants&image=back&format=rasterized&lang=zh-hans&skin=minerva&version=0rl97mj);\n  background-image: linear-gradient(transparent, transparent),\n    url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22%3E%3Cpath fill=%22%2354595d%22 d=%22M24 10.667H5.107l4.78-4.78L8 4l-8 8 8 8 1.887-1.887-4.78-4.78H24z%22/%3E%3C/svg%3E');\n}\n.mw-ui-icon-mf-back-gray:before {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=mobile.startup.images.variants&image=back&variant=gray&format=rasterized&lang=zh-hans&skin=minerva&version=0rl97mj);\n  background-image: linear-gradient(transparent, transparent),\n    url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22%3E%3Cg fill=%22%23a2a9b1%22%3E%3Cpath xmlns:default=%22http://www.w3.org/2000/svg%22 fill=%22%2354595d%22 d=%22M24 10.667H5.107l4.78-4.78L8 4l-8 8 8 8 1.887-1.887-4.78-4.78H24z%22/%3E%3C/g%3E%3C/svg%3E');\n}\n.mw-ui-icon-mf-back-invert:before {\n  background-image: url(https://en.wikipedia.org/w/load.php?modules=mobile.startup.images.variants&image=back&variant=invert&format=rasterized&lang=zh-hans&skin=minerva&version=0rl97mj);\n}\n.mf-mw-ui-icon-rotate-anti-clockwise:before {\n  -webkit-transform: rotate(-90deg);\n  -moz-transform: rotate(-90deg);\n  transform: rotate(-90deg);\n}\n.mf-mw-ui-icon-rotate-clockwise:before {\n  -webkit-transform: rotate(90deg);\n  -moz-transform: rotate(90deg);\n  transform: rotate(90deg);\n}\n.mf-mw-ui-icon-rotate-flip:before {\n  -webkit-transform: scaleY(-1);\n  -moz-transform: scaleY(-1);\n  transform: scaleY(-1);\n}\n.rtl .mf-mw-ui-icon-rotate-anti-clockwise:before {\n  -webkit-transform: rotate(90deg);\n  -moz-transform: rotate(90deg);\n  transform: rotate(90deg);\n}\n.rtl .mf-mw-ui-icon-rotate-clockwise:before {\n  -webkit-transform: rotate(-90deg);\n  -moz-transform: rotate(-90deg);\n  transform: rotate(-90deg);\n}\n#bodyContent .panel {\n  clear: both;\n  margin-top: 1em;\n  text-align: center;\n}\n#bodyContent .panel .content {\n  padding-top: 1em;\n  padding-bottom: 1em;\n  margin: 0;\n}\n.overlay-enabled #mw-mf-page-center {\n  overflow: hidden;\n  display: block;\n}\n.overlay-enabled .overlay,\n.overlay-enabled #mw-mf-page-center {\n  height: 100%;\n}\n.overlay {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  background: #fff;\n  z-index: 1;\n  display: none;\n}\n.overlay.visible {\n  display: block;\n}\n.overlay input,\n.overlay textarea {\n  padding: 0.5em;\n}\n.overlay .captcha-word,\n.overlay .summary {\n  margin: 0 0 0.7em;\n  width: 100%;\n}\n.overlay .wikitext-editor {\n  min-height: 50%;\n  line-height: 1.5;\n  border: 0;\n}\n.overlay .panel {\n  padding-top: 12px;\n  padding-bottom: 12px;\n  border-bottom: 1px solid #eaecf0;\n}\n.overlay .content .cancel {\n  display: block;\n  margin: 1em auto;\n}\n.overlay .content-header {\n  background-color: #f8f9fa;\n  border-bottom: 1px solid #eaecf0;\n  padding-top: 20px;\n  padding-bottom: 20px;\n  line-height: inherit;\n}\n.overlay .slider-button {\n  position: absolute;\n  top: 0;\n  min-width: 60px;\n  width: 3.75em;\n  bottom: 0;\n  z-index: 1;\n}\n.overlay .slider-button:before {\n  top: 0;\n}\n.overlay .slider-button.prev {\n  left: 0;\n}\n.overlay .slider-button.next {\n  right: 0;\n}\n.overlay .slider-button > * {\n  position: absolute;\n  top: 50%;\n  margin-top: -50%;\n  left: 50%;\n  margin-left: -1.75em;\n  -webkit-filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.8));\n  filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.8));\n}\n.overlay-header .overlay-title {\n  width: 100%;\n}\n.overlay-header .header-action a,\n.overlay-header .header-action button {\n  display: table-cell;\n  vertical-align: middle;\n  width: auto;\n  padding: 0 1.2em;\n  font-weight: bold;\n  white-space: nowrap;\n  text-decoration: none;\n  border-radius: 0;\n  cursor: pointer;\n  position: relative;\n}\n.overlay-header .header-action a:before,\n.overlay-header .header-action button:before {\n  top: 0;\n}\n.overlay-header .header-action a[disabled],\n.overlay-header .header-action button[disabled] {\n  opacity: 0.5;\n}\n.overlay-header .header-action a:focus,\n.overlay-header .header-action button:focus {\n  outline: 0;\n}\n.overlay-header button {\n  cursor: pointer;\n}\n.overlay-header .continue {\n  background-color: #3366cc;\n  color: #fff;\n}\n.overlay-header .submit {\n  background-color: #3366cc;\n  color: #fff;\n}\n.overlay-header h2 {\n  display: table;\n  width: 100%;\n  font-size: 1em;\n}\n.overlay-header h2 > * {\n  width: 1em;\n  display: table-cell;\n  padding-right: 0.4em;\n}\n.overlay-header h2 span {\n  width: auto;\n  max-width: 1em;\n}\n.overlay-header > ul,\n.overlay-header > div {\n  display: table-cell;\n  vertical-align: middle;\n}\n.overlay-header > ul li {\n  display: block;\n}\n.overlay-header-container,\n.overlay-footer-container {\n  width: 100%;\n  background: #fff;\n  z-index: 2;\n}\n.overlay-header-container.position-fixed,\n.overlay-footer-container.position-fixed {\n  left: 0;\n  right: 0;\n}\n.overlay-header-container {\n  top: 0;\n}\n.overlay-footer-container {\n  background-color: #fff;\n  bottom: 0;\n  border-top: 1px solid #c8ccd1;\n}\n.overlay-footer-container a {\n  display: block;\n  padding: 1em 1em 1em 10px;\n  text-align: center;\n}\n.overlay-bottom {\n  border-top: 1px solid #c8ccd1;\n  top: auto;\n  bottom: 0;\n  height: auto !important;\n  background: #f8f9fa;\n}\n.overlay-bottom .overlay-header-container {\n  background: #f8f9fa;\n}\n.overlay-ios .overlay-header-container {\n  position: absolute !important;\n  top: 1px;\n}\n.overlay-ios .overlay-footer-container {\n  position: absolute !important;\n}\n.overlay-ios .overlay-content {\n  overflow-y: scroll;\n  -webkit-overflow-scrolling: touch;\n}\n@media all and (min-width: 720px) {\n  .overlay .panel {\n    padding-top: 12px;\n    padding-bottom: 12px;\n  }\n}\n.overlay.overlay-loading {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  opacity: 0.5;\n}\n.overlay.overlay-loading .header {\n  display: none;\n}\n.overlay.overlay-loading .overlay-content {\n  overflow: hidden;\n}\n.drawer {\n  text-align: center;\n  padding-top: 0;\n  padding-bottom: 1em;\n  max-width: 500px;\n  margin: 0 auto;\n}\n.drawer.text {\n  line-height: 1;\n  font-size: 0.9em;\n  text-align: left;\n  padding-top: 0.5em;\n}\n.drawer p {\n  line-height: 1.4;\n  margin-top: 0.5em;\n}\n.drawer p,\n.drawer a:not(:last-child),\n.drawer .mw-ui-button {\n  margin-bottom: 1em;\n}\n.drawer .cancel {\n  display: block;\n  margin: 0.5em auto 0.625em auto;\n}\n.has-drawer {\n  background-color: #000000;\n}\n.has-drawer > *:not(.drawer) {\n  opacity: 1;\n  -webkit-transition: opacity 0.25s linear;\n  -moz-transition: opacity 0.25s linear;\n  transition: opacity 0.25s linear;\n}\n.has-drawer.drawer-visible > *:not(.drawer) {\n  opacity: 0.6;\n}\n.overlay-enabled .drawer {\n  display: none !important;\n}\n.cloaked-element {\n  opacity: 0;\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n}\n.position-fixed {\n  position: fixed !important;\n}\n.touch-events :focus {\n  outline: 0;\n}\n.drawer {\n  background-color: #f8f9fa;\n  position: absolute;\n  width: 100%;\n}\n.mw-notification,\n.toast,\n.drawer {\n  -webkit-box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  box-sizing: border-box;\n  position: fixed;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  box-shadow: 0 -1px 8px 0 rgba(0, 0, 0, 0.35);\n  word-wrap: break-word;\n  z-index: 2;\n  display: none;\n}\n.mw-notification.visible,\n.toast.visible,\n.drawer.visible,\n.mw-notification.mw-notification-visible,\n.toast.mw-notification-visible,\n.drawer.mw-notification-visible {\n  display: block;\n}\n.mw-notification,\n.toast {\n  font-size: 0.9em;\n  padding: 0.9em 1em;\n  background-color: #222222;\n  color: #fff;\n  margin: 0 10% 20px;\n  width: 80%;\n  text-align: center;\n  border-radius: 2px;\n}\n.mw-notification.mw-notification-type-error,\n.toast.mw-notification-type-error {\n  background-image: url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22%3E%3Cpath fill=%22%23d33%22 fill-rule=%22evenodd%22 d=%22M11.643 22.364c1.234 0 2.235-.956 2.235-2.136h-4.47c0 1.18 1 2.136 2.234 2.136zm7.25-12.284v3.998l1.77 3.603v1.1H2.623v-1.1l1.77-3.602V10.08c0-2.894 1.822-5.41 4.475-6.47.26-1.283 1.415-2.227 2.773-2.227s2.51.944 2.776 2.227c2.653 1.06 4.477 3.576 4.477 6.47zM12.92 4.974h-2.554c-2.438.553-4.255 2.64-4.255 5.14v4.474l-1.7 2.44h14.47l-1.702-2.44v-4.474c.024-4.076-3.616-5.09-4.255-5.14z%22/%3E%3C/svg%3E');\n  -webkit-background-size: 24px 24px;\n  background-size: 24px 24px;\n  background-position: 16px 50%;\n  background-repeat: no-repeat;\n  padding-left: 5%;\n  width: 75%;\n  border: 0;\n}\n.mw-notification a,\n.toast a {\n  color: #3366cc;\n}\n.mw-notification a.new,\n.toast a.new {\n  color: #dd3333;\n}\n.mw-notification-area {\n  z-index: 2;\n  position: fixed;\n  bottom: 0;\n  width: 100%;\n}\n.animations .mw-notification,\n.animations .drawer {\n  display: block;\n  visibility: hidden;\n  -webkit-transform: translate(0, 100px);\n  -moz-transform: translate(0, 100px);\n  transform: translate(0, 100px);\n  bottom: 100px;\n  opacity: 0;\n}\n.animations .mw-notification.animated,\n.animations .drawer.animated,\n.animations .mw-notification.mw-notification-tag-toast,\n.animations .drawer.mw-notification-tag-toast {\n  backface-visibility: hidden;\n  -webkit-transition: -webkit-transform 0.25s, opacity 0.25s,\n    visibility 0s 0.25s, bottom 0s 0.25s;\n  -moz-transition: -moz-transform 0.25s, opacity 0.25s, visibility 0s 0.25s,\n    bottom 0s 0.25s;\n  transition: transform 0.25s, opacity 0.25s, visibility 0s 0.25s,\n    bottom 0s 0.25s;\n}\n.animations .mw-notification.visible,\n.animations .drawer.visible,\n.animations .mw-notification.mw-notification-visible,\n.animations .drawer.mw-notification-visible {\n  bottom: 0;\n  backface-visibility: hidden;\n  -webkit-transition: -webkit-transform 0.25s, opacity 0.25s;\n  -moz-transition: -moz-transform 0.25s, opacity 0.25s;\n  transition: transform 0.25s, opacity 0.25s;\n  visibility: visible;\n  opacity: 1;\n  -webkit-transform: translate(0, 0);\n  -moz-transform: translate(0, 0);\n  transform: translate(0, 0);\n}\n.overlay {\n  padding-top: 3.35em;\n}\n.overlay textarea {\n  -webkit-box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  box-sizing: border-box;\n  width: 100%;\n  padding: 10px 16px 10px 16px;\n  resize: none;\n}\n.overlay > ul,\n.overlay button {\n  width: 3.35em;\n}\n.overlay .license {\n  font-size: 0.9em;\n  color: #72777d;\n  margin-top: 0.5em;\n  line-height: 1.4;\n}\n.overlay .header-action a,\n.overlay .header-action button {\n  height: 3.35em;\n}\n.overlay .slider-button {\n  top: 3.35em;\n}\n.overlay-header .overlay-title {\n  padding: 0.15em 0;\n}\n.overlay-header .overlay-title:last-child {\n  padding-right: 1em;\n}\n@media all and (min-width: 1000px) {\n  .overlay-header {\n    max-width: 995.3px;\n  }\n}\n.client-js .collapsible-heading,\n.client-js .collapsible-block {\n  clear: left;\n}\n.client-js .collapsible-heading {\n  cursor: pointer;\n  position: relative;\n}\n.client-js .collapsible-heading .indicator {\n  float: left;\n  margin-top: 0.3em;\n  font-size: 0.4em;\n}\n.animations .watch-this-article {\n  backface-visibility: hidden;\n  -webkit-transition: -webkit-transform 0.5s;\n  -moz-transition: -moz-transform 0.5s;\n  transition: transform 0.5s;\n}\n.animations .watch-this-article.watched {\n  -webkit-transform: rotate(72deg);\n  -moz-transform: rotate(72deg);\n  transform: rotate(72deg);\n}\n.panel-inline {\n  display: none;\n}\n.panel-inline.visible {\n  display: block;\n}\n.mf-font-size-small #content p,\n.mf-font-size-small .content p {\n  font-size: 90%;\n}\n.mf-font-size-large #content p,\n.mf-font-size-large .content p {\n  font-size: 120%;\n}\n.mf-font-size-x-large .content p,\n.mf-font-size-x-large #content p {\n  font-size: 140%;\n}\n.drawer-enabled {\n  overflow: hidden;\n}\n.drawer.references {\n  background-color: #000000;\n  color: #c8ccd1;\n  max-height: 400px;\n  overflow-y: auto;\n  padding: 20px;\n}\n.drawer.references.text {\n  font-size: 1em;\n}\n.drawer.references a {\n  color: #3366cc;\n}\n.drawer.references .cite {\n  padding-bottom: 20px;\n}\n.drawer.references .cite:after {\n  content: '';\n  display: table;\n  clear: both;\n}\n.drawer.references .cite .text {\n  color: #72777d;\n  cursor: default;\n  letter-spacing: 0.2em;\n  float: left;\n  font-size: 0.75em;\n  padding-top: 0.25em;\n  text-transform: uppercase;\n}\n.drawer.references .cite .cancel {\n  cursor: pointer;\n  font-size: 0.8em;\n  margin: -1em -1em -1em 0;\n  padding: 1em 0;\n  position: absolute;\n  right: 20px;\n  top: 20px;\n}\n.drawer.references .mw-cite-backlink {\n  display: none;\n}\n.drawer.references .reference-text {\n  line-height: 1.4;\n}\n.search-overlay {\n  background: #fff;\n}\n.search-overlay a.mw-ui-icon {\n  display: inline-block;\n}\n.search-overlay .spinner-container {\n  background-color: #ffffff;\n  bottom: 0;\n  display: none;\n  left: 0;\n  opacity: 0.7;\n  right: 0;\n  z-index: 2;\n}\n.search-overlay .spinner-container .spinner {\n  display: block;\n  left: 50%;\n  margin-left: -1.75em;\n  position: absolute;\n  top: 10%;\n}\n.search-overlay .search-box {\n  display: block;\n}\n.search-overlay .results,\n.search-overlay .search-feedback {\n  background-color: #fff;\n}\n.search-overlay .overlay-header {\n  background-color: transparent;\n}\n.search-overlay .overlay-title {\n  position: relative;\n  padding-left: 15px;\n}\n.search-overlay .header input {\n  border: 0;\n  padding-right: 3em;\n}\n.search-overlay .header input::-ms-clear {\n  display: none;\n}\n.search-overlay .overlay-content {\n  position: relative;\n  height: 100%;\n  width: 100%;\n}\n.search-overlay .search-content {\n  border-bottom: 1px solid #c8ccd1;\n  cursor: pointer;\n}\n.search-overlay .search-content .caption {\n  padding: 1em 0;\n}\n.search-overlay .search-content.overlay-header {\n  padding: 0;\n}\n.search-overlay .results {\n  box-shadow: 0 3px 3px 0 rgba(117, 117, 117, 0.3);\n}\n.search-overlay .results li:last-child {\n  border-bottom: 0;\n}\n.search-overlay .results h2 {\n  font: inherit;\n}\n.search-overlay li.page-summary {\n  display: table;\n  height: 70px;\n  width: 100%;\n}\n.search-overlay li.page-summary .title {\n  display: table-cell;\n  vertical-align: middle;\n}\n.search-overlay li.page-summary h3 {\n  margin: 0;\n  font-weight: normal;\n}\n.search-overlay li.page-summary h3 strong {\n  text-decoration: none;\n}\n.search-overlay li.page-summary .wikidata-description {\n  font-size: 0.8em;\n  margin-top: 0.5em;\n}\n.search-overlay .search-feedback {\n  box-shadow: 0 3px 3px 0 rgba(117, 117, 117, 0.3);\n  border-top: 1px solid #c8ccd1;\n  font-size: 0.8em;\n  padding: 0.5em 1em;\n}\n.search-overlay.no-results .search-feedback {\n  border-top: 0;\n}\n@-webkit-keyframes fadeIn {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n@-webkit-keyframes fadeOut {\n  from {\n    opacity: 1;\n  }\n  to {\n    opacity: 0;\n  }\n}\n@keyframes fadeOut {\n  from {\n    opacity: 1;\n  }\n  to {\n    opacity: 0;\n  }\n}\n.animations .search-overlay.visible {\n  -webkit-animation: fadeIn 0.5s;\n  -moz-animation: fadeIn 0.5s;\n  animation: fadeIn 0.5s;\n}\n.animations .search-overlay.fade-out {\n  -webkit-animation: fadeOut 0.5s;\n  -moz-animation: fadeOut 0.5s;\n  animation: fadeOut 0.5s;\n}\n.animations .search-overlay.overlay-ios {\n  -webkit-animation: none;\n  -moz-animation: none;\n  animation: none;\n}\n.search-overlay .spinner-container {\n  top: 3.35em;\n}\n.search-overlay .clear {\n  position: absolute;\n  top: 0.925em;\n  right: 0;\n  margin-top: -1px;\n}\n@media all and (min-width: 720px) {\n  .search-overlay .search-box {\n    display: table-cell;\n  }\n  .search-overlay .spinner-container,\n  .search-overlay .search-content,\n  .search-overlay .results {\n    width: 23.4375em;\n    margin-left: 14.5625em;\n  }\n  .search-overlay .overlay-title {\n    width: 23.4375em;\n    padding-left: 14.5625em;\n  }\n  .search-overlay .spinner-container {\n    left: auto;\n    right: auto;\n  }\n  .search-overlay ul {\n    width: auto;\n  }\n}\n@media all and (min-width: 1000px) {\n  .search-overlay .overlay-content {\n    max-width: 995.3px;\n  }\n}\n.last-modified-bar.active {\n  background-color: #00af89;\n  color: #fff;\n}\n.last-modified-bar.active a {\n  color: #fff;\n}\n.truncated-text {\n  white-space: nowrap;\n  overflow: hidden;\n  -webkit-text-overflow: ellipsis;\n  text-overflow: ellipsis;\n}\n@-webkit-keyframes fadeInImage {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n@keyframes fadeInImage {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\nimg.image-lazy-loaded {\n  -webkit-animation: fadeInImage 0.3s ease-in;\n  -moz-animation: fadeInImage 0.3s ease-in;\n  animation: fadeInImage 0.3s ease-in;\n}\n.mw-mf-cleanup {\n  display: block;\n  margin: 0;\n  padding: 0;\n  font-size: 0.8em;\n  color: #72777d;\n}\n.overlay-issues .cleanup > li {\n  border-bottom: solid 1px #c8ccd1;\n}\n.overlay-issues .cleanup > li .issue-notice {\n  padding: 24px 24px 24px 0;\n}\n.overlay-issues .cleanup > li .issue-notice .mw-ui-icon {\n  float: left;\n}\n.overlay-issues .cleanup > li small,\n.overlay-issues .cleanup > li .hide-when-compact {\n  font-size: 0.8em;\n}\n.overlay-issues .cleanup > li .hide-when-compact {\n  display: block;\n  margin: 8px 0;\n}\n.overlay-issues .issue-details {\n  padding-left: 3.5em;\n}\n.overlay-issues .issue-details > :first-line {\n  line-height: 1;\n}\n.overlay-issues .issue-details small i {\n  color: #72777d;\n}\n.ra-read-more h2 {\n  color: #54595d;\n  border-bottom: 0;\n  padding-bottom: 0.5em;\n  font-size: 0.8em;\n  font-weight: normal;\n  letter-spacing: 1px;\n  text-transform: uppercase;\n}\n.ext-related-articles-card-list {\n  display: -webkit-flex;\n  display: -moz-flex;\n  display: -ms-flexbox;\n  display: flex;\n  flex-flow: row wrap;\n  font-size: 1em;\n  list-style: none;\n  overflow: hidden;\n  position: relative;\n}\n.ext-related-articles-card-list .ext-related-articles-card {\n  background-color: #fff;\n  box-sizing: border-box;\n  margin: 0;\n  height: 80px;\n  position: relative;\n  width: 100%;\n  border: 1px solid rgba(0, 0, 0, 0.2);\n}\n.ext-related-articles-card-list\n  .ext-related-articles-card\n  + .ext-related-articles-card {\n  border-top: 0;\n}\n.ext-related-articles-card-list .ext-related-articles-card:first-child {\n  border-radius: 2px 2px 0 0;\n}\n.ext-related-articles-card-list .ext-related-articles-card:last-child {\n  border-radius: 0 0 2px 2px;\n}\n.ext-related-articles-card-list .ext-related-articles-card > a {\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 1;\n}\n.ext-related-articles-card-list .ext-related-articles-card > a:hover {\n  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);\n}\n.ext-related-articles-card-list h3 {\n  font-family: inherit;\n  font-size: 1em;\n  max-height: 2.6em;\n  line-height: 1.3;\n  margin: 0;\n  overflow: hidden;\n  padding: 0;\n  position: relative;\n  font-weight: 500;\n}\n.ext-related-articles-card-list h3 a {\n  color: #000;\n}\n.ext-related-articles-card-list h3:after {\n  content: ' ';\n  position: absolute;\n  right: 0;\n  bottom: 0;\n  width: 25%;\n  height: 1.3em;\n  background-color: transparent;\n  background-image: -webkit-linear-gradient(\n    right,\n    rgba(255, 255, 255, 0),\n    #ffffff 50%\n  );\n  background-image: -moz-linear-gradient(\n    right,\n    rgba(255, 255, 255, 0),\n    #ffffff 50%\n  );\n  background-image: -o-linear-gradient(\n    right,\n    rgba(255, 255, 255, 0),\n    #ffffff 50%\n  );\n  background-image: linear-gradient(\n    to right,\n    rgba(255, 255, 255, 0),\n    #ffffff 50%\n  );\n}\n.ext-related-articles-card-list .ext-related-articles-card-detail {\n  position: relative;\n  top: 50%;\n  -webkit-transform: translateY(-50%);\n  -ms-transform: translateY(-50%);\n  transform: translateY(-50%);\n}\n.ext-related-articles-card-list .ext-related-articles-card-extract {\n  color: #72777d;\n  font-size: 0.8em;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  margin-top: 2px;\n}\n.ext-related-articles-card-list .ext-related-articles-card-thumb {\n  background-image: url(https://en.wikipedia.org/w/extensions/RelatedArticles/resources/ext.relatedArticles.cards/noimage.png?9e3d8);\n  background-image: linear-gradient(transparent, transparent),\n    url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 56 56%22%3E %3Cpath fill=%22%23eaecf0%22 d=%22M0 0h56v56h-56%22/%3E %3Cpath fill=%22%2372777d%22 d=%22M36.4 13.5h-18.6v24.9c0 1.4.9 2.3 2.3 2.3h18.7v-25c.1-1.4-1-2.2-2.4-2.2zm-6.2 3.5h5.1v6.4h-5.1v-6.4zm-8.8 0h6v1.8h-6v-1.8zm0 4.6h6v1.8h-6v-1.8zm0 15.5v-1.8h13.8v1.8h-13.8zm13.8-4.5h-13.8v-1.8h13.8v1.8zm0-4.7h-13.8v-1.8h13.8v1.8z%22/%3E %3C/svg%3E');\n  background-repeat: no-repeat;\n  background-position: top center;\n  -webkit-background-size: 100% 100%;\n  background-size: 100% 100%;\n  background-size: cover;\n  float: left;\n  height: 100%;\n  width: 80px;\n  margin-right: 10px;\n}\n@media all and (min-width: 720px) {\n  .ext-related-articles-card-list {\n    border-top: 0;\n  }\n  .ext-related-articles-card-list .ext-related-articles-card {\n    border: 1px solid rgba(0, 0, 0, 0.2);\n    margin-right: 1%;\n    margin-bottom: 10px;\n    width: 32.66666667%;\n  }\n  .ext-related-articles-card-list .ext-related-articles-card,\n  .ext-related-articles-card-list .ext-related-articles-card:first-child,\n  .ext-related-articles-card-list .ext-related-articles-card:last-child {\n    border-radius: 2px;\n  }\n  .ext-related-articles-card-list .ext-related-articles-card:last-child {\n    margin-right: 0;\n  }\n  .ext-related-articles-card-list\n    .ext-related-articles-card\n    + .ext-related-articles-card {\n    border: 1px solid rgba(0, 0, 0, 0.2);\n  }\n  .ext-related-articles-card-list .ext-related-articles-card:nth-child(3n + 3) {\n    margin-right: 0;\n  }\n}\n.backtotop {\n  visibility: hidden;\n  opacity: 0;\n  position: fixed;\n  width: 2.5em;\n  height: 2.5em;\n  border-radius: 100%;\n  box-shadow: 0.1em 0.2em 0.3em #c8ccd1;\n  bottom: 20px;\n  right: 0;\n  cursor: pointer;\n  z-index: 1;\n  background-color: #3366cc;\n  transition: opacity 0.5s 0s;\n}\n.backtotop.visible {\n  opacity: 0.8;\n}\n.backtotop.visible:hover {\n  opacity: 1;\n}\n.backtotop > .arrow-up {\n  width: 0;\n  height: 0;\n  border-left: 7px solid transparent;\n  border-right: 7px solid transparent;\n  border-bottom: 7px solid #fff;\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n}\n.rtl > .backtotop .arrow-up {\n  transform: translate(50%, -50%);\n}\n\n//// patch ////\n.lazy-image-placeholder {\n  display: none;\n}\n\n.mw-editsection {\n  display: none !important;\n}\n"
  },
  {
    "path": "src/components/dictionaries/wikipedia/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type WikipediaConfig = DictItem<{\n  lang: 'auto' | 'zh' | 'zh-cn' | 'zh-tw' | 'zh-hk' | 'en' | 'ja' | 'fr' | 'de'\n}>\n\nexport default (): WikipediaConfig => ({\n  lang: '11110000',\n  selectionLang: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 420,\n  selectionWC: {\n    min: 1,\n    max: 999999999999999\n  },\n  options: {\n    lang: 'auto'\n  },\n  options_sel: {\n    lang: ['auto', 'zh', 'zh-cn', 'zh-tw', 'zh-hk', 'en', 'ja', 'fr', 'de']\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/wikipedia/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport { isContainJapanese, isContainChinese } from '@/_helpers/lang-check'\nimport {\n  handleNoResult,\n  handleNetWorkError,\n  getOuterHTML,\n  SearchFunction,\n  HTMLString,\n  GetSrcPageFunction,\n  getText,\n  DictSearchResult,\n  getFullLink\n} from '../helpers'\nimport { AllDicts } from '@/app-config'\n\nexport const getSrcPage: GetSrcPageFunction = (text, config, profile) => {\n  const { lang } = profile.dicts.all.wikipedia.options\n  const subdomain = getSubdomain(text, lang)\n  const path = lang.startsWith('zh-') ? lang : 'wiki'\n  return `https://${subdomain}.wikipedia.org/${path}/${encodeURIComponent(\n    text\n  )}`\n}\n\nexport type LangListItem = {\n  title: string\n  url: string\n}\n\nexport type LangList = LangListItem[]\n\nexport interface WikipediaResult {\n  title: string\n  content: HTMLString\n  langSelector: string\n}\n\ntype WikipediaSearchResult = DictSearchResult<WikipediaResult>\n\nexport type WikipediaPayload = {\n  /** Search a specific url */\n  url?: string\n}\n\nexport const search: SearchFunction<WikipediaResult, WikipediaPayload> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  const { lang } = profile.dicts.all.wikipedia.options\n  let subdomain = getSubdomain(text, lang)\n\n  let url = payload.url\n  if (url) {\n    const matchSubdomain = url.match(/([^/.]+)\\.m\\.wikipedia\\.org/)\n    if (matchSubdomain) {\n      subdomain = matchSubdomain[1]\n    } else {\n      url = url.replace(/^\\//, `https://${subdomain}.m.wikipedia.org/`)\n    }\n  } else {\n    const path = lang.startsWith('zh-') ? lang : 'wiki'\n    url = `https://${subdomain}.m.wikipedia.org/${path}/${encodeURIComponent(\n      text\n    )}`\n  }\n\n  return fetchDirtyDOM(url)\n    .catch(handleNetWorkError)\n    .then(doc => handleDOM(doc, subdomain))\n}\n\nexport function fetchLangList(langSelector: string) {\n  return fetchDirtyDOM(langSelector)\n    .then(getLangList)\n    .catch(e => {\n      console.error('dict wikipedia: fetch langlist failed', e)\n      return [] as LangList\n    })\n}\n\nfunction handleDOM(\n  doc: Document,\n  subdomain: string\n): WikipediaSearchResult | Promise<WikipediaSearchResult> {\n  const $bs = [...doc.querySelectorAll('#mf-section-0 b')]\n  if (\n    $bs.some($b => {\n      const textContent = $b.textContent\n      return (\n        textContent === `The article that you're looking for doesn't exist.` ||\n        textContent === `维基百科目前还没有与上述标题相同的条目。`\n      )\n    })\n  ) {\n    return handleNoResult<WikipediaSearchResult>()\n  }\n\n  const title = getText(doc, '#section_0')\n  if (!title) {\n    return handleNoResult<WikipediaSearchResult>()\n  }\n\n  doc.querySelectorAll('#bodyContent .section-heading').forEach($header => {\n    $header.classList.add('collapsible-heading')\n    $header.setAttribute(\"role\", \"button\")\n    const $icon = $header.querySelector('.mw-ui-icon')\n    if ($icon) {\n      $icon.classList.add('mw-ui-icon-mf-arrow')\n      $icon.classList.remove('mf-mw-ui-icon-rotate-flip')\n    }\n  })\n\n  const content = getOuterHTML(`https://${subdomain}.wikipedia.org/`, doc, {\n    selector: '#bodyContent',\n    config: {}\n  })\n  if (!content) {\n    return handleNoResult<WikipediaSearchResult>()\n  }\n\n  let langSelector = ''\n  let $langSelector = doc.querySelector('a.language-selector')\n  if (!$langSelector) {\n    $langSelector = doc.querySelector('.language-selector a')\n  }\n  if ($langSelector) {\n    langSelector = getFullLink(\n      `https://${subdomain}.m.wikipedia.org/`,\n      $langSelector,\n      'href'\n    )\n  }\n\n  return { result: { title, content, langSelector } }\n}\n\nfunction getSubdomain(\n  text: string,\n  lang: AllDicts['wikipedia']['options']['lang']\n): string {\n  if (lang.startsWith('zh-')) {\n    return 'zh'\n  }\n\n  if (lang === 'auto') {\n    return isContainJapanese(text) ? 'ja' : isContainChinese(text) ? 'zh' : 'en'\n  }\n\n  return lang\n}\n\nfunction getLangList(doc: Document): LangList {\n  return [...doc.querySelectorAll('#mw-content-text li a')]\n    .map<LangListItem | undefined>($a => {\n      const url = $a.getAttribute('href')\n      const title = $a.getAttribute('title')\n      if (url && title) {\n        return { url, title }\n      }\n    })\n    .filter((x): x is LangListItem => !!x)\n}\n"
  },
  {
    "path": "src/components/dictionaries/youdao/View.tsx",
    "content": "import React, { FC, useState } from 'react'\nimport Speaker from '@/components/Speaker'\nimport StarRates from '@/components/StarRates'\nimport { YoudaoResult } from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport EntryBox from '@/components/EntryBox'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictYoudao: FC<ViewPorps<YoudaoResult>> = ({ result }) => {\n  const [collinsEntry, setCollinsEntry] = useState<string | number>(0)\n\n  if (result.type === 'related') {\n    return <StrElm className=\"dictYoudao-Related\" html={result.list} />\n  }\n\n  return (\n    <>\n      {result.title && (\n        <div className=\"dictYoudao-HeaderContainer\">\n          <h1 className=\"dictYoudao-Title\">{result.title}</h1>\n          <span className=\"dictYoudao-Pattern\">{result.pattern}</span>\n        </div>\n      )}\n      {(result.stars > 0 || result.prons.length > 0) && (\n        <div className=\"dictYoudao-HeaderContainer\">\n          {result.stars > 0 && (\n            <StarRates className=\"dictYoudao-Stars\" rate={result.stars} />\n          )}\n          {result.prons.map(({ phsym, url }) => (\n            <React.Fragment key={url}>\n              {phsym} <Speaker src={url} />\n            </React.Fragment>\n          ))}\n          <span className=\"dictYoudao-Rank\">{result.rank}</span>\n        </div>\n      )}\n      {result.basic && (\n        <StrElm className=\"dictYoudao-Basic\" html={result.basic} />\n      )}\n      {result.collins.length > 0 && (\n        <EntryBox title=\"柯林斯英汉双解\">\n          {result.collins.length > 1 && (\n            <select\n              value={collinsEntry}\n              onChange={e => setCollinsEntry(e.currentTarget.value)}\n            >\n              {result.collins.map((col, i) => (\n                <option key={i} value={i}>\n                  {col.title}\n                </option>\n              ))}\n            </select>\n          )}\n          <StrElm\n            className=\"dictYoudao-Collins\"\n            html={result.collins[collinsEntry].content}\n          />\n        </EntryBox>\n      )}\n      {result.discrimination && (\n        <div className=\"dictYoudao-Discrimination\">\n          <h1 className=\"dictYoudao-Discrimination_Title\">词义辨析</h1>\n          <StrElm html={result.discrimination} />\n        </div>\n      )}\n      {result.sentence && (\n        <EntryBox title=\"权威例句\">\n          <StrElm\n            tag=\"ol\"\n            className=\"dictYoudao-Sentence\"\n            html={result.sentence}\n          />\n        </EntryBox>\n      )}\n      {result.translation && (\n        <EntryBox title=\"机器翻译\">\n          <StrElm\n            className=\"dictYoudao-Translation\"\n            html={result.translation}\n          />\n        </EntryBox>\n      )}\n    </>\n  )\n}\n\nexport default DictYoudao\n"
  },
  {
    "path": "src/components/dictionaries/youdao/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"Youdao Dictionary\",\n    \"zh-CN\": \"有道词典\",\n    \"zh-TW\": \"有道詞典\"\n  },\n  \"options\": {\n    \"basic\": {\n      \"en\": \"Show basic meaning\",\n      \"zh-CN\": \"显示简单释义\",\n      \"zh-TW\": \"顯示簡單解釋\"\n    },\n    \"collins\": {\n      \"en\": \"Show Collins result\",\n      \"zh-CN\": \"显示柯林斯双解\",\n      \"zh-TW\": \"顯示柯林斯雙解\"\n    },\n    \"discrimination\": {\n      \"en\": \"Show discrimination\",\n      \"zh-CN\": \"显示词语辨析\",\n      \"zh-TW\": \"顯示詞語辨析\"\n    },\n    \"sentence\": {\n      \"en\": \"Show sentences\",\n      \"zh-CN\": \"显示权威例句\",\n      \"zh-TW\": \"顯示權威例句\"\n    },\n    \"translation\": {\n      \"en\": \"Show translation\",\n      \"zh-CN\": \"显示有道翻译\",\n      \"zh-TW\": \"顯示有道翻譯\"\n    },\n    \"related\": {\n      \"en\": \"Show related results\",\n      \"zh-CN\": \"失败时显示备选\",\n      \"zh-TW\": \"失敗時顯示備選\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/youdao/_style.shadow.scss",
    "content": ".dictYoudao-HeaderContainer {\n  display: flex;\n  align-items: center;\n  flex-wrap: wrap;\n}\n\n.dictYoudao-Title {\n  font-size: 1.5em;\n  margin-right: 8px;\n}\n\n.dictYoudao-Pattern {\n  margin-top: 0.5em;\n}\n\n.dictYoudao-Stars {\n  margin-right: 8px;\n}\n\n.dictYoudao-Basic {\n  margin: 0.5em 0;\n}\n\n.dictYoudao-Collins {\n  position: relative;\n\n  ul,\n  ol {\n    padding-left: 1.5em;\n  }\n\n  h4 {\n    padding: 0 0.5em;\n\n    > * {\n      margin-right: 5px;\n    }\n\n    > .title {\n        color: #f9690e;\n      }\n  }\n\n  .additional {\n    font-weight: normal;\n    color: var(--color-font-grey);\n  }\n\n  .collinsOrder {\n    position: absolute;\n    left: 0;\n    width: 1.5em;\n    text-align: right;\n  }\n\n  .exampleLists {\n    position: relative;\n    padding-left: 2.5em;\n    color: var(--color-font-grey);\n    border-left: 1px solid #666;\n\n    .collinsOrder {\n      left: 0.5em;\n    }\n  }\n}\n\n.dictYoudao-Discrimination {\n  position: relative;\n\n  .do-detail {\n    display: none;\n  }\n\n  .title {\n    font-size: 1.2em;\n    font-weight: bold;\n  }\n\n  .wordGroup,\n  .via {\n    padding-left: 1.2em;\n  }\n\n  .via {\n    color: #888;\n  }\n}\n\n.dictYoudao-Discrimination_Title {\n  font-size: 1.2em;\n}\n\n.dictYoudao-Sentence {\n  margin: 0;\n  padding: 0 0 0 1.5em;\n}\n\n.dictYoudao-Translation > p:last-child {\n  display: none;\n}\n"
  },
  {
    "path": "src/components/dictionaries/youdao/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type YoudaoConfig = DictItem<{\n  basic: boolean\n  collins: boolean\n  discrimination: boolean\n  sentence: boolean\n  translation: boolean\n  related: boolean\n}>\n\nexport default (): YoudaoConfig => ({\n  lang: '11000000',\n  selectionLang: {\n    english: true,\n    chinese: true,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 265,\n  selectionWC: {\n    min: 1,\n    max: 999999999999999\n  },\n  options: {\n    basic: true,\n    collins: true,\n    discrimination: true,\n    sentence: true,\n    translation: false,\n    related: true\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/youdao/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  getText,\n  getInnerHTML,\n  handleNoResult,\n  HTMLString,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult,\n  getChsToChz,\n  removeChild\n} from '../helpers'\nimport { DictConfigs } from '@/app-config'\n\nexport const getSrcPage: GetSrcPageFunction = text =>\n  'https://dict.youdao.com/w/' + encodeURIComponent(text.replace(/\\s+/g, ' '))\n\nconst HOST = 'http://www.youdao.com'\n\nexport interface YoudaoResultLex {\n  type: 'lex'\n  title: string\n  stars: number\n  rank: string\n  pattern: string\n  prons: Array<{\n    phsym: string\n    url: string\n  }>\n  basic?: HTMLString\n  collins: Array<{\n    title: string\n    content: HTMLString\n  }>\n  discrimination?: HTMLString\n  sentence?: HTMLString\n  translation?: HTMLString\n}\n\nexport interface YoudaoResultRelated {\n  type: 'related'\n  list: HTMLString\n}\n\nexport type YoudaoResult = YoudaoResultLex | YoudaoResultRelated\n\ntype YoudaoSearchResult = DictSearchResult<YoudaoResult>\n\nexport const search: SearchFunction<YoudaoResult> = async (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  const options = profile.dicts.all.youdao.options\n  const transform = await getChsToChz(config.langCode)\n\n  return fetchDirtyDOM(\n    'https://dict.youdao.com/w/' + encodeURIComponent(text.replace(/\\s+/g, ' '))\n  )\n    .catch(handleNetWorkError)\n    .then(doc => checkResult(doc, options, transform))\n}\n\nfunction checkResult(\n  doc: Document,\n  options: DictConfigs['youdao']['options'],\n  transform: null | ((text: string) => string)\n): YoudaoSearchResult | Promise<YoudaoSearchResult> {\n  const $typo = doc.querySelector('.error-typo')\n  if (!$typo) {\n    return handleDOM(doc, options, transform)\n  } else if (options.related) {\n    return {\n      result: {\n        type: 'related',\n        list: getInnerHTML(HOST, $typo, { transform })\n      }\n    }\n  }\n  return handleNoResult()\n}\n\nfunction handleDOM(\n  doc: Document,\n  options: DictConfigs['youdao']['options'],\n  transform: null | ((text: string) => string)\n): YoudaoSearchResult | Promise<YoudaoSearchResult> {\n  const result: YoudaoResult = {\n    type: 'lex',\n    title: getText(doc, '.keyword', transform),\n    stars: 0,\n    rank: getText(doc, '.rank'),\n    pattern: getText(doc, '.pattern', transform),\n    prons: [],\n    collins: []\n  }\n\n  const audio: { uk?: string; us?: string } = {}\n\n  const $star = doc.querySelector('.star')\n  if ($star) {\n    result.stars = Number(($star.className.match(/\\d+/) || [0])[0])\n  }\n\n  doc.querySelectorAll('.baav .pronounce').forEach($pron => {\n    const phsym = $pron.textContent || ''\n    const $voice = $pron.querySelector<HTMLAnchorElement>('.dictvoice')\n    if ($voice && $voice.dataset.rel) {\n      const url =\n        'https://dict.youdao.com/dictvoice?audio=' + $voice.dataset.rel\n\n      result.prons.push({ phsym, url })\n\n      if (phsym.includes('英')) {\n        audio.uk = url\n      } else if (phsym.includes('美')) {\n        audio.us = url\n      }\n    }\n  })\n\n  if (options.basic) {\n    result.basic = getInnerHTML(HOST, doc, {\n      selector: '#phrsListTab .trans-container',\n      transform\n    })\n  }\n\n  if (options.collins) {\n    doc.querySelectorAll('#collinsResult .wt-container').forEach($container => {\n      const item = { title: '', content: '' }\n\n      const $title = $container.querySelector(':scope > .title.trans-tip')\n      if ($title) {\n        removeChild($title, '.do-detail')\n        item.title = getText($title)\n        $title.remove()\n      }\n\n      const $star = $container.querySelector('.star')\n      if ($star) {\n        const starMatch = /star(\\d+)/.exec(String($star.className))\n        if (starMatch) {\n          const rate = +starMatch[1]\n          let stars = ''\n          for (let i = 0; i < 5; i++) {\n            stars += `<svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              viewBox=\"0 0 426.67 426.67\"\n              width=\"1em\"\n              height=\"1em\"\n              style=\"${i === 4 ? '' : 'margin-right: 1px'}\"\n            >\n              <path\n                fill=${i < rate ? '#FAC917' : '#d1d8de'}\n                d=\"M213.33 10.44l65.92 133.58 147.42 21.42L320 269.4l25.17 146.83-131.84-69.32-131.85 69.34 25.2-146.82L0 165.45l147.4-21.42\"\n              />\n            </svg>`\n          }\n          $star.innerHTML = stars\n        }\n      }\n\n      item.content = getInnerHTML(HOST, $container, { transform })\n      if (item.content) {\n        result.collins.push(item)\n      }\n    })\n  }\n\n  if (options.discrimination) {\n    result.discrimination = getInnerHTML(HOST, doc, {\n      selector: '#discriminate',\n      transform\n    })\n  }\n\n  if (options.sentence) {\n    result.sentence = getInnerHTML(HOST, doc, {\n      selector: '#authority .ol',\n      transform\n    })\n  }\n\n  if (options.translation) {\n    result.translation = getInnerHTML(HOST, doc, {\n      selector: '#fanyiToggle .trans-container',\n      transform\n    })\n  }\n\n  if (result.title || result.translation) {\n    return { result, audio }\n  }\n  return handleNoResult()\n}\n"
  },
  {
    "path": "src/components/dictionaries/youdaotrans/View.tsx",
    "content": "export { MachineTrans as default } from '@/components/MachineTrans/MachineTrans'\n"
  },
  {
    "path": "src/components/dictionaries/youdaotrans/_locales.ts",
    "content": "import { getMachineLocales } from '../locales'\n\nexport const locales = getMachineLocales({\n  en: 'Youdao Translate',\n  'zh-CN': '有道翻译',\n  'zh-TW': '有道翻譯'\n})\n"
  },
  {
    "path": "src/components/dictionaries/youdaotrans/_style.shadow.scss",
    "content": "@import '@/components/MachineTrans/MachineTrans.scss';\n"
  },
  {
    "path": "src/components/dictionaries/youdaotrans/auth.ts",
    "content": "export const auth = {\n  appKey: '',\n  key: ''\n}\n\nexport const url = 'http://ai.youdao.com/gw.s'\n"
  },
  {
    "path": "src/components/dictionaries/youdaotrans/config.ts",
    "content": "import {\n  MachineDictItem,\n  machineConfig\n} from '@/components/MachineTrans/engine'\nimport { Language } from '@opentranslate/translator'\nimport { Subunion } from '@/typings/helpers'\n\nexport type YoudaotransLanguage = Subunion<\n  Language,\n  'zh-CN' | 'en' | 'pt' | 'es' | 'ja' | 'ko' | 'fr' | 'ru'\n>\n\nexport type YoudaotransConfig = MachineDictItem<YoudaotransLanguage>\n\nexport default (): YoudaotransConfig =>\n  machineConfig<YoudaotransConfig>(\n    ['zh-CN', 'en', 'pt', 'es', 'ja', 'ko', 'fr', 'ru'],\n    {\n      lang: '11011111'\n    },\n    {},\n    {}\n  )\n"
  },
  {
    "path": "src/components/dictionaries/youdaotrans/engine.ts",
    "content": "import { SearchFunction, GetSrcPageFunction } from '../helpers'\nimport memoizeOne from 'memoize-one'\nimport { Youdao } from '@opentranslate/youdao'\nimport {\n  MachineTranslateResult,\n  MachineTranslatePayload,\n  getMTArgs,\n  machineResult\n} from '@/components/MachineTrans/engine'\nimport { YoudaotransLanguage } from './config'\n\nexport const getTranslator = memoizeOne(\n  () =>\n    new Youdao({\n      env: 'ext',\n      config:\n        process.env.YOUDAO_APPKEY && process.env.YOUDAO_KEY\n          ? {\n              appKey: process.env.YOUDAO_APPKEY,\n              key: process.env.YOUDAO_KEY\n            }\n          : undefined\n    })\n)\n\nexport const getSrcPage: GetSrcPageFunction = (text, config, profile) => {\n  return `http://fanyi.youdao.com`\n}\n\nexport type YoudaotransResult = MachineTranslateResult<'youdaotrans'>\n\nexport const search: SearchFunction<\n  YoudaotransResult,\n  MachineTranslatePayload<YoudaotransLanguage>\n> = async (rawText, config, profile, payload) => {\n  const translator = getTranslator()\n\n  const { sl, tl, text } = await getMTArgs(\n    translator,\n    rawText,\n    profile.dicts.all.youdaotrans,\n    config,\n    payload\n  )\n\n  const appKey = config.dictAuth.youdaotrans.appKey\n  const key = config.dictAuth.youdaotrans.key\n  const translatorConfig = appKey && key ? { appKey, key } : undefined\n\n  try {\n    const result = await translator.translate(text, sl, tl, translatorConfig)\n    return machineResult(\n      {\n        result: {\n          id: 'youdaotrans',\n          sl: result.from,\n          tl: result.to,\n          slInitial: profile.dicts.all.youdaotrans.options.slInitial,\n          searchText: result.origin,\n          trans: result.trans\n        },\n        audio: {\n          py: result.trans.tts,\n          us: result.trans.tts\n        }\n      },\n      translator.getSupportLanguages()\n    )\n  } catch (e) {\n    return machineResult(\n      {\n        result: {\n          id: 'youdaotrans',\n          sl,\n          tl,\n          slInitial: 'hide',\n          searchText: { paragraphs: [''] },\n          trans: { paragraphs: [''] }\n        }\n      },\n      translator.getSupportLanguages()\n    )\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/zdic/View.tsx",
    "content": "import React, { FC } from 'react'\nimport { ZdicResult } from './engine'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport EntryBox from '@/components/EntryBox'\nimport { StrElm } from '@/components/StrElm'\n\nexport const DictZdic: FC<ViewPorps<ZdicResult>> = ({ result }) => (\n  <div>\n    {result.map(entry => (\n      <EntryBox title={entry.title} key={entry.title}>\n        <StrElm html={entry.content} />\n      </EntryBox>\n    ))}\n  </div>\n)\n\nexport default DictZdic\n"
  },
  {
    "path": "src/components/dictionaries/zdic/_locales.json",
    "content": "{\n  \"name\": {\n    \"en\": \"汉典\",\n    \"zh-CN\": \"汉典\",\n    \"zh-TW\": \"漢典\"\n  },\n  \"options\": {\n    \"audio\": {\n      \"en\": \"Enable audio\",\n      \"zh-CN\": \"开启发音\",\n      \"zh-TW\": \"啟用發音\"\n    }\n  },\n  \"helps\": {\n    \"audio\": {\n      \"en\": \"Referer modification is required, which may slightly impact performance.\",\n      \"zh-CN\": \"突破外链限制需要改写 Referer，可能会轻微影响性能。\",\n      \"zh-TW\": \"突破外鏈限制需要改寫 Referer，可能會輕微影響效能。\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/dictionaries/zdic/_style.shadow.scss",
    "content": "#gg_bslot_a,\n#gg_bslot_b {\n  text-align: center;\n}\n\n.btmslot_a-container {\n  width: 100%;\n}\n\n#gg_bslot_a {\n  margin-bottom: 20px;\n}\n\n.contentslot,\n.mpuslot_b {\n  margin-top: 20px;\n  text-align: center;\n}\n\n.nr-box {\n  border: 1px solid #af9a87;\n  padding: 15px;\n  background: #fff;\n  margin-bottom: 20px;\n  -webkit-column-break-inside: avoid;\n  page-break-inside: avoid;\n  break-inside: avoid;\n}\n\n.nr-box h2,\n.nr-box .h2 {\n  margin-bottom: 0.5em;\n}\n\n.nr-box .view_more a {\n  opacity: 0.7;\n  text-decoration: none;\n  margin-top: 1em;\n  display: block;\n  font-size: 0.8em;\n}\n\n.login_wrapper .login_social .facebook i {\n  color: #3e5a98;\n}\n\n.hc_user_form .button,\n.profile_wrapper .button,\n.comment .button,\n.recent_word_suggestions_search .button {\n  background-color: #194480;\n  color: white;\n  padding: 0.5em 1em;\n  border: 0;\n  text-decoration: none;\n  display: inline-block;\n}\n\n.hc_user_form .button i,\n.profile_wrapper .button i,\n.comment .button i,\n.recent_word_suggestions_search .button i {\n  vertical-align: -2px;\n  margin-left: 0.5em;\n}\n\n.register_content .register_social .facebook i {\n  color: #3e5a98;\n}\n\n.search-desktop .specialchar div.button-special {\n  background-color: #fff;\n  border: 1px solid rgba(0, 0, 0, 0.31);\n  border-radius: 4px;\n  display: inline-block;\n  line-height: 46px;\n  margin: 1px;\n  text-align: center;\n  width: 46px;\n  cursor: pointer;\n}\n\n.suggested_word_wrapper h1.h2_entry {\n  font-size: 1.8em;\n}\n\n.suggested_word_wrapper .content .nr-box-header:after {\n  display: initial;\n}\n\n.verbtable_content .type,\n.verbtable_content .conjugation {\n  margin-left: 12px;\n  border-bottom: 1px dotted #c5c5c5;\n  -webkit-column-break-inside: avoid;\n  page-break-inside: avoid;\n  break-inside: avoid;\n}\n\n.verbtable_content .type {\n  min-height: 60px;\n}\n\n.verbtable_content .type h3,\n.verbtable_content .conjugation h3 {\n  font-size: 1.2em;\n  color: #0069b3;\n}\n\n.shiyi_content .nr-box {\n  padding-top: 0;\n}\n\n.shiyi_content .nr-box-origin {\n  border-left: solid 4px #669eff;\n}\n\n.shiyi_content .nr-box-wordlists,\n.shiyi_content .nr-box-synonyms,\n.shiyi_content .nr-box-quotations {\n  border-left: solid 4px #d49882;\n}\n\n.shiyi_content .nr-box-images {\n  border-left: solid 4px #bb1;\n}\n\n.shiyi_content .nr-box-translation {\n  border-right: solid 4px #194885;\n}\n\n.shiyi_content .nr-box-shiyi.jbjs {\n  border-right: solid 4px #c99464;\n}\n\n.shiyi_content .nr-box-shiyi.xxjs {\n  border-right: solid 4px #ea8a61;\n}\n\n.shiyi_content .nr-box-shiyi.gyjs {\n  border-right: solid 4px #6a0000;\n}\n\n.shiyi_content .nr-box-shiyi.cyjs {\n  border-right: solid 4px #e8ad62;\n}\n\n.shiyi_content .nr-box-shiyi.kxzd {\n  border-right: solid 4px #e8ad62;\n}\n\n.shiyi_content .nr-box-shiyi.swjz {\n  border-right: solid 4px #aea4a4;\n}\n\n.shiyi_content .nr-box-shiyi.yyfy {\n  border-right: solid 4px #8899bd;\n}\n\n.shiyi_content .nr-box-shiyi.zyzx {\n  border-right: solid 4px #c69f7b;\n}\n\n.shiyi_content .nr-box-shiyi.wytl {\n  border-right: solid 4px #88abc3;\n}\n\n.shiyi_content .nr-box-shiyi.zyyy {\n  border-right: solid 4px #9c7474;\n}\n\n.shiyi_content .nr-box-derived {\n  border-right: solid 4px #5f167d;\n}\n\n.shiyi_content .nr-box-usage {\n  border-right: solid 4px #78ad79;\n}\n\n.shiyi_content .nr-box-translations {\n  border-right: solid 4px #ff8c4d;\n}\n\n.shiyi_content .nr-box-examples {\n  border-right: solid 4px #1c7744;\n}\n\n.shiyi_content .nr-box-comments {\n  border-right: solid 4px #3b8e8d;\n}\n\n.shiyi_content .nr-box-learners {\n  border-right: solid 4px #467f7f;\n}\n\n.shiyi_content .nr-box-nearby-words {\n  border-right: solid 4px #bb5454;\n}\n\n.shiyi_content .nr-box-header:after {\n  content: ' ';\n  display: block;\n  clear: both;\n}\n\n.shiyi_content .nr-box-header {\n  background: #f3e8df;\n  padding: 10px 15px;\n  margin: 0 -15px 1em -15px;\n  font-size: 12px;\n}\n\n.cdet .shiyi_content .nr-box .nr-box-header,\n.shiyi_content .content .nr-box-header {\n  background: 0;\n  padding: 0;\n  margin: 0;\n}\n\n.shiyi_content .nr-box-shiyi.jbjs .nr-box-header,\n.shiyi_content .nr-box-learners .nr-box-header {\n  border-bottom: solid 1px #af9a87;\n}\n\n.shiyi_content .nr-box-shiyi.xxjs .nr-box-header {\n  border-bottom: solid 1px #af9a87;\n}\n\n.shiyi_content .nr-box-shiyi.kxzd .nr-box-header {\n  border-bottom: solid 1px #af9a87;\n}\n\n.shiyi_content .nr-box-shiyi.swjz .nr-box-header {\n  border-bottom: solid 1px #af9a87;\n}\n\n.shiyi_content .nr-box-shiyi.yyfy .nr-box-header {\n  border-bottom: solid 1px #af9a87;\n}\n\n.shiyi_content .nr-box-shiyi.zyzx .nr-box-header {\n  border-bottom: solid 1px #af9a87;\n}\n\n.shiyi_content .nr-box-shiyi.wytl .nr-box-header {\n  border-bottom: solid 1px #af9a87;\n}\n\n.shiyi_content .nr-box-examples .nr-box-header {\n  background: #deefe3;\n}\n\n.shiyi_content .nr-box-translations .nr-box-header {\n  background: #fff6dd;\n}\n\n.shiyi_content .nr-box-nearby-words .nr-box-header {\n  background: #ffefeb;\n}\n\n.shiyi_content .nr-box-origin .nr-box-header {\n  background: #eff7ff;\n}\n\n.shiyi_content .nr-box-comments .nr-box-header {\n  background: #dbf7dc;\n}\n\n.shiyi_content .nr-box-usage .nr-box-header {\n  background: #e9ffe3;\n}\n\n.shiyi_content .nr-box-images .nr-box-header {\n  background: #f4ffd4;\n}\n\n.shiyi_content .nr-box-videos .nr-box-header {\n  background: #ffebeb;\n}\n\n.shiyi_content .nr-box-wordlists .nr-box-header {\n  background: #fff1e6;\n}\n\n.shiyi_content .nr-box-quotations .nr-box-header {\n  background: #fff1e6;\n}\n\n.shiyi_content .nr-box-header .h2_entry {\n  margin-bottom: 0;\n}\n\n.shiyi_content .nr-box-videos .entryVideo {\n  width: 100%;\n  max-width: 640px;\n  height: 320px;\n}\n\n.shiyi_content .h2_entry {\n  font-size: 1.4em;\n  margin-bottom: 0.5em;\n}\n\n.shiyi_content .nr-box-shiyi h2.h2_entry {\n  font-size: 1.8em;\n}\n\n.shiyi_content .definitions .thes {\n  margin-top: 0.8em;\n}\n\n.shiyi_content .xxjscz_box {\n  margin-right: 10px;\n}\n\n.shiyi_content .xxjscz_box,\n.shiyi_content ol li,\n.shiyi_content .thesaurus_synonyms,\n.verbtable_content .headword_link,\n.shiyi_content .link-right.verbtable {\n  margin-bottom: 1em;\n}\n\n.shiyi_content strong,\n.shiyi_content .nr-box-synonyms .firstSyn,\n.shiyi_content .nr-box-nearby-words .current,\n.shiyi_content .cit-type-xxjs .orth,\n.shiyi_content .xxjscz_box .author,\n.shiyi_content .thesaurus_synonyms .synonym:first-of-type,\n.shiyi_content .nr-box-translation .phr {\n  font-weight: bold;\n}\n\n.shiyi_content .lbl.type-register,\n.shiyi_content .lbl.misc,\n.shiyi_content .colloc,\n.shiyi_content #synonyms_content .thesaurus_synonyms .lbl,\n.shiyi_content #synonyms_content .thesaurus_synonyms .misc {\n  font-style: italic;\n}\n\n.shiyi_content .lbl.type-syntax {\n  font-size: 0.8em;\n  color: var(--color-font-grey);\n}\n\n.shiyi_content .nr-box-synonyms .h2_entry {\n  display: inline-block;\n}\n\n.shiyi_content .nr-box-synonyms .extra-link {\n  display: inline-block;\n  margin-left: 1em;\n}\n\n.shiyi_content .ref.type-thesaurus,\n.shiyi_content .nr-box-synonyms .thesaurus-link-plus,\n.shiyi_content .link-right.verbtable,\n.shiyi_content .extra-link,\n.shiyi_content .nr-box-examples .button,\n.verbtable_content .headword_link {\n  background: #e5ebf3;\n  display: inline-block;\n  padding: 2px 10px;\n  border: 0;\n  text-decoration: none;\n  display: inline-block;\n  font-weight: bold;\n  font-size: 0.9em;\n  margin-top: 2px;\n}\n\n.shiyi_content .h2_entry .dictname,\n.shiyi_content .h2_entry .lbl.type-misc {\n  font-size: 16px;\n}\n\n.shiyi_content .audio_play_button,\n.audio_play_button {\n  color: #ec2615;\n  vertical-align: middle;\n  -webkit-transition: transform 0.2s, text-shadow 0.2s;\n  transition: transform 0.2s, text-shadow 0.2s;\n  border: 0;\n}\n\n.shiyi_content .h2_entry .homnum {\n  background-color: #1c4b8b;\n  color: #fff;\n  font-size: 12px;\n  font-weight: bold;\n  padding: 2px 5px;\n  vertical-align: super;\n}\n\n.shiyi_content .zdict .quote {\n  color: #555;\n  font-style: italic;\n}\n\n.shiyi_content .zdict .biling .quote {\n  font-style: normal;\n}\n\n.shiyi_content .zdict .cit.type-translation .quote {\n  font-weight: bold;\n}\n\n.shiyi_content .nr-box:after,\n.shiyi_content .zdict .content:after {\n  content: '';\n  clear: both;\n  display: table;\n}\n\n.shiyi_content .cit.type-quotation .quote,\n.shiyi_content .nr-box-quotations .quote,\n.shiyi_content .nr-box-examples .quote,\n.shiyi_content .nr-box-thesaurus .quote {\n  display: block;\n  margin-top: 1em;\n}\n\n.shiyi_content .cit.type-quotation .author,\n.shiyi_content .nr-box-quotations .author,\n.shiyi_content .nr-box-examples .author,\n.shiyi_content .nr-box-thesaurus .author {\n  font-weight: bold;\n  font-style: italic;\n  font-size: 0.8em;\n}\n\n.shiyi_content .cit.type-quotation .title,\n.shiyi_content .nr-box-quotations .title,\n.shiyi_content .nr-box-examples .title,\n.shiyi_content .nr-box-thesaurus .title {\n  display: inline;\n  font-variant: small-caps;\n  font-style: italic;\n  font-size: 0.8em;\n}\n\n.shiyi_content .cit.type-quotation .year,\n.shiyi_content .nr-box-quotations .year,\n.shiyi_content .nr-box-examples .year,\n.shiyi_content .nr-box-thesaurus .year {\n  font-size: 0.8em;\n  font-style: italic;\n}\n\n.shiyi_content .nr-box-syn-of-syns div.type-syn_of_syn_head .orth,\n.shiyi_content .thesbase .key,\n.context-dataset-english-thesaurus .author,\n.shiyi_content .rend-b {\n  color: #1683be;\n}\n\n.page {\n  font-size: 16px;\n}\n\n.page .dictname {\n  font-size: 0.7em;\n}\n\n.page .zdict .copyright .i {\n  color: gray;\n}\n\n.copyright {\n  display: none;\n  color: gray;\n  font-size: small;\n  margin-top: 10px;\n}\n\n.page .metadata {\n  display: none;\n}\n\n.page .infls,\n.page .description,\n.page .title,\n.page .url,\n.page .summary,\n.page .og,\n.page .infls,\n.page .description,\n.page .title,\n.page .url,\n.page .summary,\n.page .og {\n  display: block;\n}\n\n.page .assetref {\n  display: block;\n}\n\n.page .assettype {\n  font-weight: bold;\n  color: blue;\n}\n\n.page .dictentry {\n  margin-bottom: 20px;\n}\n\n.page .assets_intro,\n.page .asset_intro {\n  color: green;\n  display: block;\n  font-weight: bold;\n  font-variant: small-caps;\n}\n\n.page .zdict .re .hom {\n  display: inline;\n}\n\n.page .zdict .re {\n  display: block;\n}\n\n.page .jbjs .hom {\n  display: block;\n  margin-left: 1.5em;\n  margin-bottom: 1em;\n}\n\n.page .jbjs .sense {\n  margin-left: 0;\n  margin-bottom: 0;\n  margin-top: 0.25em;\n}\n\n.page .zdict .sense {\n  display: block;\n  margin-left: 1.5em;\n  margin-bottom: 0.5em;\n  margin-top: 0.5em;\n}\n\n.page .zdict .sense.inline {\n  display: inline;\n  margin-left: 0;\n  margin-bottom: 0.5em;\n  margin-top: 0.5em;\n}\n\n.page .zdict .inline {\n  display: inline;\n}\n\n.page .zdict .newline {\n  display: block;\n}\n\n.page .jbjs br {\n  display: none;\n}\n\n.page .zdict .subc,\n.page .zdict .colloc {\n  font-style: italic;\n  font-weight: normal;\n}\n\n.page .zdict .re .pos {\n  font-style: italic;\n}\n\n.page .zdict .b {\n  font-weight: bold;\n}\n\n.page .zdict .form.type-infl .orth {\n  font-weight: bold;\n}\n\n.page .zdict .form.type-drv .orth {\n  font-weight: bold;\n}\n\n.page .zdict .form.type-inflected {\n  display: none;\n}\n\n.page .zdict .hi.rend-b {\n  font-weight: bold;\n}\n\n.page .zdict .hi.rend-sc {\n  font-variant: small-caps;\n}\n\n.page .zdict .hi.rend-u {\n  text-decoration: underline;\n  font-size: inherit;\n}\n\n.page .zdict .hi.rend-r {\n  font-weight: normal;\n  font-style: normal;\n}\n\n.page .zdict .hi.rend-sup {\n  vertical-align: super;\n  font-size: smaller;\n}\n\n.page .zdict .hi.rend-sub {\n  vertical-align: sub;\n  font-size: smaller;\n}\n\n.page .zdict .hi.rend-i {\n  font-style: italic;\n}\n\n.page .zdict .i {\n  font-weight: normal;\n  font-style: italic;\n  color: black;\n}\n\n.page .zdict .note {\n  color: black;\n  line-height: 1.4em;\n  font-style: normal;\n  background-color: #e9eef4;\n  margin: 6px 0;\n  padding: 6px 4px 6px 18px;\n  font-weight: normal;\n  display: block;\n}\n\n.page .zdict .posp {\n  font-size: 80%;\n  text-transform: uppercase;\n}\n\n.page .zdict .r {\n  font-style: normal;\n}\n\n.page .zdict .sub {\n  vertical-align: sub;\n  font-size: smaller;\n}\n\n.page .zdict .sup {\n  vertical-align: super;\n  font-size: smaller;\n}\n\n.page .zdict .u {\n  text-decoration: underline;\n}\n\n.page .zdict .block {\n  display: block;\n  margin-top: 3px;\n}\n\n.page .hin .block {\n  display: block;\n  margin-top: 15px;\n  margin-bottom: 7.5px;\n}\n\n.page .zdict .bolditalic {\n  font-weight: bold;\n  font-style: italic;\n}\n\n.page span.bold {\n  font-weight: bold;\n}\n\n.page span.bluebold {\n  font-weight: bold;\n  color: #1c4b8b;\n}\n\n.page span.italics,\n.page span.ital {\n  font-style: italic;\n}\n\n.page span.sensenum {\n  margin-left: -1.3em;\n  float: left;\n  font-weight: bold;\n  font-size: 1.1em;\n}\n\n.page .zdict .cit.type-translation .quote {\n  font-style: normal;\n  color: #1683be;\n}\n\n.page .zdict .cit.type-translation .pos {\n  font-style: bold;\n}\n\n.page .zdict a:hover {\n  color: #f9690e;\n}\n\n.page .zdict .var {\n  font-weight: bold;\n}\n\n.page .zdict .power {\n  float: right;\n}\n\n.page .zdict .power .i {\n  color: #1c4b8b;\n  font-size: inherit;\n}\n\n.page .zdict .hom_subsec {\n  display: block;\n}\n\n.page .zdict .definitions,\n.page .zdict .derivs,\n.page .zdict .etyms {\n  margin-bottom: 1em;\n}\n\n.page .zdict .inflected_forms {\n  display: block;\n  padding-bottom: 1.25em;\n}\n\n.page .zdict .scbold {\n  font-weight: bold;\n  text-transform: uppercase;\n  font-size: 0.8em;\n}\n\n.page .zdict .note .scbold {\n  display: block;\n}\n\n.page .zdict .pron .ptr {\n  color: red;\n}\n\n.page .zdict .list,\n.page .zdict .relwordgrp {\n  display: block;\n  margin-left: 20px;\n}\n\n.page .zdict .listitem,\n.page .zdict .relwordunit {\n  display: list-item;\n}\n\n.page .zdict .type-syngrp,\n.page .zdict .type-antgrp {\n  display: block;\n}\n\n.page .asset.Corpus_Examples_EN .quote {\n  font-style: italic;\n}\n\n.page .jbjs .sense {\n  margin-left: 0;\n}\n\n.page .cit.type-xxjs .content {\n  background-color: white;\n  margin-bottom: 20px;\n  padding: 20px;\n}\n\n.page .cit.type-xxjs .author {\n  font-weight: bold;\n  font-style: italic;\n}\n\n.page .cit.type-xxjs .title {\n  display: inline;\n  font-variant: small-caps;\n  font-style: italic;\n}\n\n.page .cit.type-xxjs .ref.type-def {\n  text-decoration: none;\n  color: inherit;\n}\n\n.page .biling .lbl {\n  font-style: italic;\n  color: #555;\n}\n\n.page .biling .lbl.type-subj {\n  font-variant: small-caps;\n}\n\n.page .biling .lbl.type-subj .lbl {\n  font-variant: normal;\n}\n\n.page .biling .lbl.type-tm {\n  font-style: normal;\n}\n\n.page .biling .lbl.type-tm_hw {\n  font-size: 0.78em;\n}\n\n.page .biling .lbl.type-infl span,\n.page .biling .lbl.type-infl {\n  font-style: normal;\n  color: #1c4b8b;\n  font-weight: normal;\n}\n\n.page .biling br {\n  display: none;\n}\n\n.page .biling .phrasals .re .orth {\n  font-size: 1.25em;\n}\n\n.page .biling .sense .re {\n  font-size: 100%;\n  margin-left: 0;\n}\n\n.page .hin .form.type-syn .orth,\n.page .hin .form.type-ant .orth,\n.page .hin .form.type-phr .orth {\n  font-weight: normal;\n  font-size: 100%;\n}\n\n.page .biling .re {\n  display: block;\n  margin-left: 1em;\n}\n\n.page .thesbase .form.type-syn {\n  margin-left: 0.5em;\n}\n\n.page .thesbase .synunit .cit {\n  display: inline;\n}\n\n.page .thesbase .def {\n  display: block;\n}\n\n.page .thesbase .xr.type-theslink {\n  display: inline-block;\n  margin-left: 20px;\n}\n\n.page .thesbase .relwgrp {\n  display: block;\n  margin-left: 1em;\n}\n\n.page .thesbase .table {\n  display: block;\n}\n\n.page .thesbase .caption {\n  display: block;\n  font-weight: bold;\n  margin-top: 10px;\n  font-size: larger;\n}\n\n.page .thesbase .bibl {\n  display: block;\n}\n\n.page .thesbase .bibl .title {\n  display: inline;\n}\n\n.page .thesbase .cit.type-proverb {\n  display: block;\n}\n\n.page .thesbase .tr {\n  display: table-row;\n}\n\n.page .thesbase .td {\n  display: table-cell;\n  padding: 3px;\n}\n\n.page .thesbase .th {\n  display: table-cell;\n  font-weight: bold;\n}\n\n.page .thesbase .cit.type-xxjs,\n.page .thesbase .cit.type-xxjs .crefe {\n  display: inline;\n  padding-left: 0.25em;\n}\n\n.page .thesbase .note {\n  background-color: transparent;\n  padding: 0;\n  margin-top: 10px;\n  overflow: hidden;\n}\n\n.page .thesbase .note .tr {\n  display: block;\n  margin-bottom: 20px;\n}\n\n.page .thesbase .tr .td:first-child {\n  background-color: #e9eef4;\n  font-weight: bold;\n  color: #1c4b8b;\n  padding: 5px 15px;\n}\n\n.page .thesbase .note .td {\n  padding: 8px 15px;\n  display: block;\n}\n\n.page .thesbase .note .th {\n  display: none;\n}\n\n.page .nr-box-syn-of-syns .syns_container .form.type-syn .orth {\n  font-weight: bold;\n}\n\n.page .thesbase .link {\n  text-decoration: underline;\n  font: 14px/1 'Microsoft Yahei', sans-serif, Arial, Verdana;\n  background: #e5ebf3;\n  color: #1c4b8b;\n  padding: 0.3em 0.8em;\n  margin: 5px 0;\n  display: inline-block;\n}\n\n.page .thesbase .sense {\n  margin-bottom: 2em;\n}\n\n.page .thesbase .author {\n  font-weight: bold;\n  font-style: italic;\n}\n\n.cdet .nr-box-origin {\n  padding-top: 0;\n  padding-bottom: 0;\n}\n\n.page .thesbase .sensehead > .sensenum {\n  float: none;\n}\n\n.page .thesbase .scbold {\n  background: #efefef;\n  padding: 0.5em 22px;\n  margin: 2em 0 1em 0;\n  font-weight: bold;\n  font-size: 80%;\n  text-transform: uppercase;\n  display: block;\n}\n\n.page .nr-box-syn-of-syns div.type-syn_of_syn_head {\n  display: inline-block;\n}\n\n.page .nr-box-syn-of-syns div.type-syn_of_syn_head .orth,\n.page .thesbase .key {\n  font-weight: bold;\n  margin-right: 0;\n  display: inline-block;\n  margin-left: 0;\n  padding: 0.3em 0.8em;\n  border: 0;\n  font-size: 1.1em;\n  padding-left: 0;\n  padding-bottom: 0;\n}\n\n.page .thesbase .key {\n  padding-right: 0;\n}\n\n.page .thesbase .firstSyn {\n  color: black;\n  font-size: 0.9em;\n}\n\n.page .nr-box-syn-of-syns .syns_head {\n  margin-top: 2.2em;\n}\n\n.page .nr-box-syn-of-syns .syns_example {\n  line-height: 2.5em;\n}\n\n.page .type-ant.columns3,\n.page .nr-box-syn-of-syns .columns3 {\n  -webkit-column-count: 3;\n  -moz-column-count: 3;\n  column-count: 3;\n}\n\n.page .nr-box-syn-of-syns .syns_items {\n  display: block;\n}\n\n.pagination a.prev,\n.pagination a.next,\n.pagination a.page,\n.pagination span.page,\n.pagination p,\n.pagination p a {\n  padding: 0.3em 0.8em;\n  display: inline-block;\n  text-decoration: none;\n  font-weight: bold;\n  border: 0;\n}\n\n.pagination a.prev,\n.pagination a.next,\n.pagination a.page {\n  background: #e5ebf3;\n  color: #194885;\n}\n\n.pagination span.page,\n.pagination p,\n.pagination p a {\n  background: #194885;\n  color: #e5ebf3;\n}\n\n.page .nr-box-syn-of-syns .lbl,\n.page .nr-box-syn-of-syns .lbl span,\n.page .zdict.thesbase .lbl,\n.page .zdict.thesbase .lbl span {\n  font-style: italic;\n  color: green;\n}\n\n.page .zdict.thesbase .sensebody {\n  display: block;\n  margin: 0.5em 0 0.5em 6px;\n}\n\n.page .thesbase span.bold {\n  font-weight: bold;\n}\n\n.page .thesbase span.kerntouch {\n  letter-spacing: -0.18em;\n}\n\n.page .thesbase span.kern60 {\n  letter-spacing: -0.6em;\n}\n\n.page .thesbase span.manualdiacritic {\n  vertical-align: 25%;\n  letter-spacing: -1em;\n}\n\n.page .thesbase span.numerator {\n  vertical-align: 35%;\n  font-size: smaller;\n}\n\n.page .thesbase span.numerator_back {\n  position: absolute;\n  vertical-align: 35%;\n  letter-spacing: -1em;\n  font-size: smaller;\n}\n\n.page .thesbase span.denominator {\n  vertical-align: -35%;\n  font-size: smaller;\n}\n\n.page .thesbase span.italics {\n  font-weight: normal;\n  font-style: italic;\n  color: black;\n}\n\n.page .thesbase span.homnum {\n  font-weight: bold;\n  color: #fff;\n  vertical-align: super;\n  font-size: 50%;\n}\n\n.page .thesbase span.sensenum {\n  font-weight: bold;\n}\n\n.page .thesbase span.QA {\n  font-style: italic;\n  color: red;\n  font-size: 90%;\n}\n\n.page .thesbase hr {\n  width: 50%;\n  text-align: left;\n  border: 3px inset #777;\n  height: 6px;\n  margin: 10px auto 5px 0;\n}\n\n.page .thesbase .cit.type-quotation {\n  display: block;\n}\n\n.page .thesbase .cit.type-quotation > .quote,\n.page .thesbase .cit.type-proverb > .quote,\n.page .thesbase .cit.type-quotation > .bibl {\n  display: block;\n  margin-left: 1em;\n  padding-left: 0;\n}\n\n.page .thesbase > .re.type-phr .xr {\n  margin-left: 1em;\n  font-weight: bold;\n}\n\n.page .thesbase .div .xr {\n  display: block;\n  margin-left: 1em;\n}\n\n.cdet .sense .type-ant div.invisibleElements,\n.cdet .sense .form.type-syn.invisibleElements {\n  display: none;\n}\n\n.cdet .sense.moreAnt .type-ant div,\n.cdet .sense.moreSyn .form.type-syn.head {\n  display: block;\n}\n\n.cdet .page .zdict .sense,\n.cdet .sense.moreSyn {\n  margin-left: 0;\n  margin-bottom: 0.5em;\n  padding-bottom: 0.5em;\n  border-bottom: 1px solid #dbdada;\n  position: relative;\n  overflow: hidden;\n}\n\n.cdet .nr-box-syn-of-syns .syns_container {\n  padding-left: 1.9em;\n}\n\n.cdet .zdict.thesbase .sensebody,\n.cdet div[data-type-block] .sense .sensebody {\n  margin: 0;\n  display: block;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  word-wrap: normal;\n  padding-right: 1.9em;\n  padding-left: 1.9em;\n  line-height: 1.5em;\n  font-size: 0.9em;\n}\n\n.cdet .nr-box-syn-of-syns div[data-type-block] .sense .def,\n.cdet .nr-box-syn-of-syns div[data-type-block] .sense .syns_example {\n  margin: 0;\n  display: block;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  word-wrap: normal;\n  line-height: 1.5em;\n  font-size: 0.9em;\n  padding-right: 0;\n  padding-left: 0;\n}\n\n.cdet .zdict .sense .synonymBlock {\n  cursor: pointer;\n}\n\n.cdet .zdict .sense.opened,\n.cdet div[data-type-block] .sense.opened {\n  margin-left: 0;\n  margin-bottom: 0.5em;\n  padding-bottom: 0.5em;\n  border-bottom: 1px solid #dbdada;\n  cursor: auto;\n  position: relative;\n}\n\n.cdet .zdict.thesbase .sense.opened .sensebody,\n.cdet div[data-type-block] .sense.opened .sensebody {\n  overflow: auto;\n  text-overflow: inherit;\n  white-space: inherit;\n}\n\n.cdet .syns_container .form.type-syn,\n.cdet .nr-box-syn-of-syns .form.type-syn,\n.cdet .form.type-syn,\n.cdet .type-ant div {\n  margin-left: 0.5em;\n}\n\n.cdet .nr-box-syn-of-syns .sense.moreSyn {\n  margin-bottom: 0.5em;\n}\n\n.cdet .form.type-syn .orth,\n.cdet .type-ant .orth,\n.cdet .syns_container .form.type-syn .orth {\n  background-color: transparent;\n  border: 0;\n  font-size: 0.9em;\n  font-weight: bold;\n  text-decoration: none;\n  color: #4d4e51;\n  margin: 0;\n  padding: 0 0.2em;\n}\n\n.cdet .zdict .content,\n.cdet .form.type-syn {\n  position: relative;\n}\n\n.cdet .zdict .hom,\n.cdet .zdict .syn_of_syns {\n  overflow: hidden;\n}\n\n.cdet .sense.moreinfo .form .orth:after {\n  display: block;\n}\n\n.cdet .type-ant,\n.cdet .nr-box-syn-of-syns .syns_container {\n  padding-left: 0;\n  margin-left: 0;\n}\n\n.cdet .nr-box-comments {\n  background-color: white;\n  margin-bottom: 20px;\n  padding: 20px;\n  position: relative;\n}\n\n.cdet .nr-box-comments,\n.cdet .nr-box-origin,\n.cdet .nr-box-nearby-words,\n.cdet .zdict .content,\n.cdet .nr-box-syn-of-syns {\n  border-left: none;\n  box-shadow: none;\n}\n\n.cdet .re.type-phr .xr,\n.cdet .nr-box-nearby-words li {\n  margin-left: 0;\n  padding-left: 0.85em;\n  margin-bottom: 0.3em;\n  padding-bottom: 0.3em;\n  display: block;\n}\n\n.cdet .nr-box-syn-of-syns div.type-syn_of_syn_head {\n  display: block;\n}\n\n.cdet .nr-box-syn-of-syns .sense .def {\n  padding-left: 1em;\n}\n\n.cdet .nr-box-syn-of-syns .sense .def,\n.cdet .nr-box-syn-of-syns .sense .syns_example {\n  margin: 0;\n  display: block;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  word-wrap: normal;\n  line-height: 1.5em;\n  font-size: 0.9em;\n  color: #4d4e51;\n}\n\n.cdet .nr-box-syn-of-syns .quote {\n  font-style: italic;\n}\n\n.cdet div[data-type-block] .sense.opened .sensebody,\n.cdet .nr-box-syn-of-syns .sense.opened .def,\n.cdet .nr-box-syn-of-syns .sense.opened .syns_example {\n  overflow: auto;\n  white-space: normal;\n}\n\n.cdet .nr-box-syn-of-syns .syns_head {\n  margin-top: 0;\n}\n\n.cdet .form.type-syn .titleTypeSubContainer {\n  display: block;\n}\n\n.cdet .cit.type-quotation > .bibl {\n  font-size: 0.85em;\n}\n\n.cdet .cit.type-quotation {\n  margin-left: 0;\n  margin-bottom: 0.3em;\n  padding-bottom: 0.3em;\n}\n\n.cdet .re.type-phr > .titleTypeContainer,\n.cdet .cit.type-quotation > .titleTypeContainer {\n  margin-bottom: 1em;\n}\n\n.cdet .cit.type-quotation > .quote {\n  line-height: 1.3em;\n  margin-bottom: 0.3em;\n}\n\n.cdet .cit.type-quotation .title,\n.cdet .cit.type-quotation .author {\n  font-size: inherit;\n}\n\n.cdet .zdict .quote {\n  color: #4d4e51;\n  font-style: italic;\n  display: block;\n  font-size: 0.9em;\n}\n\n.homograph-entry .grammar .page {\n  display: block;\n  border: solid 1px;\n  font-family: arial, helvetica, sans-serif;\n  margin-bottom: 20px;\n  padding: 15px;\n  padding-bottom: 40px;\n}\n\n.homograph-entry .grammar a.previous,\n.homograph-entry .grammar a.next {\n  background: #e5ebf3;\n  color: #194885;\n  padding: 0.5em 1em;\n  font-weight: bolder;\n  border-bottom: 0;\n  float: left;\n  margin-top: 1em;\n}\n\n.homograph-entry .grammar a.next {\n  float: right;\n}\n\n.homograph-entry .grammar a.previous:hover,\n.homograph-entry .grammar a.next:hover {\n  color: #194885;\n}\n\n.homograph-entry .grammar a.previous i,\n.homograph-entry .grammar a.next i {\n  font-size: 1.3em;\n  vertical-align: middle;\n  padding-right: 8px;\n  padding-left: 8px;\n  display: inline-block;\n}\n\n.homograph-entry .grammar .exmplblk ul {\n  padding-left: 0;\n}\n\n.homograph-entry .grammar .exmplblk {\n  padding: 0.5em;\n}\n\n.homograph-entry .grammar .exmplgrp ul {\n  padding-left: 20px;\n  padding-bottom: 10px;\n}\n\n.homograph-entry .grammar .intro.suppressed {\n  display: none;\n}\n\n.homograph-entry .grammar h2 {\n  font-size: 16pt;\n  line-height: 2em;\n  text-decoration: underline;\n}\n\n.homograph-entry .grammar h3 {\n  font-size: 14pt;\n}\n\n.homograph-entry .grammar h4 {\n  font-size: 12pt;\n  font-weight: bold;\n  margin-bottom: 1em;\n}\n\n.homograph-entry .grammar u {\n  text-decoration: underline;\n}\n\n.homograph-entry .grammar .lemma {\n  font-weight: bold;\n}\n\n.homograph-entry .grammar .caption {\n  font-weight: bold;\n  margin-top: 1.5em;\n}\n\n.homograph-entry .grammar .p {\n  display: block;\n  margin-top: 5px;\n  margin-bottom: 5px;\n}\n\n.homograph-entry .grammar .group {\n  display: block;\n  margin-top: 2em;\n  margin-bottom: 2em;\n}\n\n.homograph-entry .grammar .exmpl {\n  font-weight: normal;\n  font-style: italic;\n}\n\n.homograph-entry .grammar .i {\n  font-style: italic;\n}\n\n.homograph-entry .grammar .post {\n  font-style: italic;\n}\n\n.homograph-entry .grammar .posp {\n  font-weight: bold;\n  font-style: normal;\n}\n\n.homograph-entry .grammar .pattern {\n  font-family: sans-serif;\n}\n\n.homograph-entry .grammar .ul {\n  margin-top: 5px;\n  list-style-type: none;\n  padding-left: 15px;\n}\n\n.homograph-entry .grammar ul.arrow {\n  list-style-type: square;\n}\n\n.homograph-entry .grammar ul.star {\n  list-style-type: disc;\n}\n\n.homograph-entry .grammar ul.alpha {\n  list-style-type: lower-alpha;\n}\n\n.homograph-entry .grammar ol {\n  margin-top: 5px;\n  list-style-type: decimal;\n}\n\n.homograph-entry .grammar .li.exmpl {\n  font-style: italic;\n}\n\n.homograph-entry .grammar .lemmalist .li {\n  margin-top: 10px;\n}\n\n.homograph-entry .grammar .lemmalist {\n  border-color: #ccc;\n  border-style: solid;\n  border-width: 1px;\n  margin: 4px;\n  margin-top: 2em;\n  padding: 1em;\n  -webkit-column-count: 4;\n  -moz-column-count: 4;\n  column-count: 4;\n}\n\n.homograph-entry .grammar div.greyborder2 {\n  border-color: #ccc;\n  border-style: solid;\n  border-width: 1px;\n  margin: 4px;\n  -webkit-column-count: 4;\n  -moz-column-count: 4;\n  column-count: 4;\n}\n\n.homograph-entry .grammar th,\n.homograph-entry .grammar td {\n  border-color: #000;\n  border-style: solid;\n  border-width: 1px;\n  padding: 0.5em 1.4em;\n}\n\n.homograph-entry .grammar th {\n  background-color: #ddd;\n  font-weight: bold;\n  font-size: 0.9em;\n}\n\n.homograph-entry .grammar table {\n  border-collapse: collapse;\n  border-color: #000;\n  border-style: solid;\n  border-width: 1px;\n  margin-top: 1.5em;\n  margin-bottom: 1em;\n}\n\n.homograph-entry .grammar a.block {\n  display: block;\n}\n\n.homograph-entry .grammar i.i_chevron-thin-right {\n  display: inline-block;\n  font-weight: bold;\n  width: 2em;\n  text-align: center;\n  font-size: 0.6em;\n}\n\n.homograph-entry .grammar .group a:before,\n.homograph-entry .grammar .section a:before,\n.homograph-entry .grammar .posGr a:before,\n.homograph-entry .grammar .subpattern a:before,\n.homograph-entry .grammar .pattern a:before,\n.homograph-entry .grammar .chapter a:before {\n  display: block;\n  content: '';\n}\n\n.homograph-entry .grammar .breadcrumb {\n  margin-bottom: 2em;\n}\n\n.zyyy span.label {\n  width: 0.8em;\n  display: inline-block;\n  color: #0058a9;\n  font-weight: bold;\n}\n\n.zyyy span.label1 {\n  width: 0.8em;\n  display: inline-block;\n}\n\n.shiyi_content .zib-title {\n  float: right;\n  font-size: 16px;\n}\n\n.shiyi_content .zi-b-img,\n.shiyi_content .zib-title .label {\n  display: inline-block;\n}\n\n.shiyi_content .zi-b-container .level {\n  border: 1px solid rgba(0, 0, 0, 0.5);\n  border-radius: 50%;\n  display: inline-block;\n  background-color: #f2928e;\n}\n\n.shiyi_content .zi-b-container .level.roundRed {\n  background-color: #e52920;\n}\n\n.shiyi_content .zi-b-container .level1 {\n  width: 14px;\n  height: 14px;\n}\n\n.shiyi_content .zi-b-container .level2 {\n  width: 15px;\n  height: 15px;\n}\n\n.shiyi_content .zi-b-container .level3 {\n  width: 16px;\n  height: 16px;\n}\n\n.shiyi_content .zi-b-container .level4 {\n  width: 17px;\n  height: 17px;\n}\n\n.shiyi_content .zi-b-container .level5 {\n  width: 18px;\n  height: 18px;\n}\n\n.shiyi_content .zi-b-container .round {\n  width: 100%;\n  height: 100%;\n}\n\n.shiyi_content .zi-b-container.relevance .level {\n  border-radius: 0;\n  width: 15px;\n  vertical-align: bottom;\n}\n\n.shiyi_content .zi-b-container.relevance .level1 {\n  background: #f6b26b;\n  height: 10px;\n}\n\n.shiyi_content .zi-b-container.relevance .level2 {\n  background: #f6b26b;\n  height: 13px;\n}\n\n.shiyi_content .zi-b-container.relevance .level3 {\n  background: #ffd966;\n  height: 16px;\n}\n\n.shiyi_content .zi-b-container.relevance .level4 {\n  background: #ffd966;\n  height: 19px;\n}\n\n.shiyi_content .zi-b-container.relevance .level5 {\n  background: #b6d7a8;\n  height: 22px;\n}\n\n.shiyi_content .zi-b-container.relevance .level6 {\n  background: #b6d7a8;\n  height: 25px;\n}\n\n.i_volume-up:before {\n  content: '\\f028';\n}\n\n.favadd:before {\n  content: '\\2729';\n}\n\n.favdel:before {\n  content: '\\2605';\n}\n\nmain > .zdict,\nmain > .browse_wrapper,\nmain > .spellcheck_wrapper,\nmain > .content_wrapper,\nmain > .submit_new_word_wrapper,\nmain > .word_submitted_wrapper,\nmain > .suggested_word_wrapper {\n  width: calc(100% - 160px);\n  float: left;\n}\n\nmain > .zdict,\nmain > .browse_wrapper,\nmain > .spellcheck_wrapper,\nmain > .content_wrapper,\nmain > .submit_new_word_wrapper,\nmain > .word_submitted_wrapper,\nmain > .suggested_word_wrapper {\n  width: 100%;\n  float: none;\n}\n\n.res_s {\n  display: none;\n}\n\n.page .type-ant.columns3,\n.page .nr-box-syn-of-syns .columns3 {\n  -webkit-column-count: 1;\n  -moz-column-count: 1;\n  column-count: 1;\n}\n\n.mpuslot_b-container {\n  min-width: 320px;\n  width: 100%;\n  margin: 0 0 0 -15px;\n  padding: 0;\n  z-index: 10000;\n}\n\n.page .zdict .power {\n  display: none;\n}\n\n.nr-box,\n.page .Corpus_Examples_EN .content,\n.shiyi_content .nr-box,\n.wotd-txt-block,\n.yczsl-content {\n  padding: 10px;\n}\n\n.shiyi_content .nr-box {\n  padding-top: 0;\n}\n\n.shiyi_content .nr-box-header {\n  margin: 0 -10px 1em -10px;\n}\n\n.shiyi_content .nr-box.jbjs.br .nr-box-header::after {\n  content: none;\n}\n\nmain > .zdict,\nmain > .browse_wrapper,\nmain > .spellcheck_wrapper,\nmain > .content_wrapper,\nmain > .submit_new_word_wrapper,\nmain > .word_submitted_wrapper,\nmain > .suggested_word_wrapper {\n  width: 100%;\n  float: none;\n}\n\n.res_t {\n  display: none;\n}\n\n.res_d {\n  display: none;\n}\n\n.pl-zdic a {\n  border-bottom: none;\n}\n\n.zib-title .icon-star {\n  background: #9c7474;\n  border-radius: 4px;\n  width: 25px;\n  height: 25px;\n  padding-top: 3px;\n  display: inline-block;\n  color: #fff;\n  font-size: 18px;\n  text-align: center;\n}\n\n.shiyi_content .h2_entry .spanr {\n  display: inline-block;\n  float: right;\n}\n\n.zib-title .icon-star {\n  padding-top: 4px;\n}\n\n.dicpy {\n  font-family: Arial;\n  color: #f9690e;\n  font-weight: normal;\n}\n\n.jnr li {\n  list-style-type: none;\n}\n\n.jnr,\n.bknr,\n.wknr {\n  margin: 0.5em;\n  line-height: 1.5em;\n}\n\n.shiyi_content .jnr ul {\n  list-style: none;\n  counter-reset: li;\n}\n\n.shiyi_content .jnr ul li::before {\n  content: counter(li);\n  color: red;\n  display: inline-block;\n  width: 2em;\n  margin-left: -1.5em;\n  margin-right: 0.5em;\n  text-align: right;\n  direction: rtl;\n}\n\n.shiyi_content .jnr li {\n  counter-increment: li;\n}\n\n.shiyi_content .nr-box-shiyi.cyjs {\n  border-right: solid 4px #e8ad62;\n}\n\n.shiyi_content .nr-box-shiyi.wljs {\n  border-right: solid 4px #aea4a4;\n}\n\n.shiyi_content .nr-box-shiyi.cyjs .nr-box-header {\n  border-bottom: solid 1px #af9a87;\n}\n\n.shiyi_content .nr-box-shiyi.wljs .nr-box-header {\n  border-bottom: solid 1px #af9a87;\n}\n\nspan.z_ts2,\nspan.z_ts3,\nspan.z_ts4 {\n  height: 18px;\n  line-height: 18px;\n  color: #f9690e;\n  font-size: 12px;\n  text-align: center;\n  display: inline-block;\n  font-weight: normal;\n  margin-right: 5px;\n  padding: 0;\n}\n\nspan.z_ts2 {\n  background-position: 0 -78px;\n  width: 33px;\n}\n\nspan.z_ts3 {\n  background-position: -33px -78px;\n  width: 47px;\n}\n\nspan.z_ts4 {\n  background-position: 0 -114px;\n  width: 55px;\n}\n\n.dicpy {\n  font-size: 1.3em;\n  font-family: Arial;\n  color: #f9690e;\n}\n\n.diczx1 {\n  color: #f9690e;\n}\n\n.cino {\n  font-weight: bold;\n}\n\n.encs {\n  color: #339900;\n}\n\n.smcs {\n  font-weight: bold;\n  color: #f9690e;\n}\n\n.yhcs {\n  color: #f9690e;\n  font-weight: bold;\n}\n\n.crefe {\n  border-bottom: dashed 1px rgba(0, 0, 0, 0.6);\n  margin-right: 1em;\n}\n\n.enbox {\n  padding: 10px 0 5px 0;\n  border-top: 1px solid rgba(133, 133, 133, 0.28);\n  margin: 5px;\n  background: rgba(254, 249, 249, 0.2);\n}\n\n@media screen and (max-width: 761px) {\n  .cdet .navigation .nav {\n    display: none;\n  }\n\n  .cdet .more {\n    margin: 10px auto 10px auto;\n    width: 50%;\n  }\n}\n\n@media screen and (max-width: 761px) {\n  .navigation .tabsNavigation {\n    overflow-x: auto;\n  }\n}\n\n@media screen and (max-width: 761px) {\n  [class*='res_c'] {\n    clear: both;\n    width: 100%;\n    margin: 0 0 10px 0;\n  }\n\n  .res_c_center_content,\n  .res_c_2_3_content {\n    padding: 0;\n  }\n\n  header .left,\n  header .right,\n  header .center {\n    padding: 0.3em;\n  }\n\n  header .logo {\n    width: 130px;\n    margin-left: 5px;\n    line-height: inherit;\n  }\n\n  .homepage header .left {\n    float: none;\n  }\n\n  .zmenu .drop {\n    box-shadow: none;\n    margin-left: 15px;\n  }\n\n  .zmenu .drop,\n  .zmenu .drop:after {\n    display: none;\n  }\n\n  .zmenu .drop .this_page {\n    padding-left: 0;\n    padding-right: 0;\n  }\n\n  .zmenu .drop .zmenu_i {\n    padding: 0;\n  }\n\n  .shiyi_content .entry_title {\n    padding-left: 0.2em;\n    padding-right: 0.2em;\n  }\n\n  .login_wrapper .login_or_signup,\n  .login_wrapper .sign_up,\n  .profile_wrapper .profile_block,\n  .profile_wrapper .profile_body {\n    float: none;\n    margin: 0;\n    width: auto;\n  }\n\n  #gg_tslot {\n    width: 320px;\n    min-height: 50px;\n    display: table;\n    margin: 0 auto;\n  }\n\n  .topslot_container {\n    margin-bottom: 10px;\n  }\n\n  .mpuslot_b {\n    width: 320px;\n    margin: 0 auto;\n  }\n\n  .lies .lie_x {\n    display: block;\n    width: auto;\n    padding-right: 0;\n    min-height: 0;\n  }\n\n  .yczsl,\n  .yczsl {\n    min-height: 0;\n  }\n\n  .search-desktop {\n    display: none;\n  }\n\n  .searchPanelOpen .search-desktop {\n    display: block;\n    width: auto;\n    margin: 4px;\n    clear: both;\n    margin-bottom: 16px;\n  }\n\n  .search-desktop .custom-select {\n    display: block;\n    position: absolute;\n    left: 0;\n    opacity: 0.001;\n    width: 50px;\n    height: 100%;\n  }\n\n  .search-desktop .cs_m {\n    -webkit-column-count: 1;\n    -moz-column-count: 1;\n    column-count: 1;\n  }\n\n  .cdet .sense.moreinfo .form *[class*='type'] {\n    display: block;\n  }\n\n  .shiyi_content .h1_entry {\n    line-height: 1.2em;\n  }\n\n  .cdet .titleTypeContainer .titleType {\n    margin: 0;\n  }\n}\n\np {\n  padding-left: 1em;\n}\n\n.cino {\n  margin-left: -1em;\n}\n\na:hover {\n  color: #f9690e;\n}\n\na {\n  cursor: pointer;\n  color: inherit;\n  text-decoration: none;\n  border-bottom: dashed 1px rgba(0, 0, 0, 0.6);\n}\n\n.gnr .pz {\n  border-bottom: 1px dashed rgba(133, 133, 133, 0.28);\n}\n.gnr .pz ruby {\n  margin-left: 20px;\n}\n.gnr .pz ruby rbc {\n  font-size: 20px;\n  line-height: 30px;\n  font-weight: bold;\n}\n.gnr .pz ruby rtc {\n  font-size: 12px;\n  color: #8f6652;\n  line-height: 30px;\n}\n.gnr .def {\n  margin: 5px 0 5px;\n  line-height: 1.6em;\n}\n.pz {\n  border-bottom: 1px dashed rgba(133, 133, 133, 0.28);\n  padding-bottom: 5px;\n}\n.pz ruby {\n  margin-left: 20px;\n  font-size: 1.5em;\n}\n.pz ruby rbc {\n  font-size: 20px;\n  line-height: 30px;\n  font-weight: bold;\n}\n.pz ruby rtc {\n  font-size: 12px;\n  color: #8f6652;\n  line-height: 30px;\n}\n\nruby {\n  display: inline-table;\n  text-align: center;\n  white-space: nowrap;\n  text-indent: 0;\n  margin: 0;\n  vertical-align: -20%;\n}\n\nruby > rb,\nruby > rbc {\n  display: table-row-group;\n  line-height: 90%;\n}\n\nruby > rt,\nruby > rbc + rtc {\n  display: table-header-group;\n  font-size: 60%;\n  line-height: 40%;\n  letter-spacing: 0;\n}\n\nruby > rbc + rtc + rtc {\n  display: table-footer-group;\n  font-size: 60%;\n  line-height: 40%;\n  letter-spacing: 0;\n}\n\nrbc > rb,\nrtc > rt {\n  display: table-cell;\n  letter-spacing: 0;\n}\n\n/* rt[rbspan] should be transformed into td[colspan] but that requires xslt */\nrtc > rt[rbspan] {\n  display: table-caption;\n}\nrp {\n  display: none;\n}\n\n.dichr {\n  border-color: rgba(133, 133, 133, 0.28);\n}\n\n// #1244\n.jbjs_ico {\n  width: 1.3em;\n  height: 1.3em;\n  display: inline-block;\n  margin-right: 0.2em;\n  vertical-align: text-bottom;\n}\n"
  },
  {
    "path": "src/components/dictionaries/zdic/config.ts",
    "content": "import { DictItem } from '@/app-config/dicts'\n\nexport type ZdicConfig = DictItem<{\n  audio: boolean\n}>\n\nexport default (): ZdicConfig => ({\n  lang: '01000000',\n  selectionLang: {\n    english: false,\n    chinese: true,\n    japanese: false,\n    korean: false,\n    french: false,\n    spanish: false,\n    deutsch: false,\n    others: false,\n    matchAll: false\n  },\n  defaultUnfold: {\n    english: true,\n    chinese: true,\n    japanese: true,\n    korean: true,\n    french: true,\n    spanish: true,\n    deutsch: true,\n    others: true,\n    matchAll: false\n  },\n  preferredHeight: 400,\n  selectionWC: {\n    min: 1,\n    max: 5\n  },\n  options: {\n    audio: false\n  }\n})\n"
  },
  {
    "path": "src/components/dictionaries/zdic/engine.ts",
    "content": "import { fetchDirtyDOM } from '@/_helpers/fetch-dom'\nimport {\n  HTMLString,\n  getInnerHTML,\n  handleNoResult,\n  handleNetWorkError,\n  SearchFunction,\n  GetSrcPageFunction,\n  DictSearchResult\n} from '../helpers'\nimport { getStaticSpeaker } from '@/components/Speaker'\n\nexport const getSrcPage: GetSrcPageFunction = text => {\n  return `https://www.zdic.net/hans/${text}`\n}\n\nconst HOST = 'https://www.zdic.net'\n\nexport type ZdicResult = Array<{\n  title: string\n  content: HTMLString\n}>\n\ntype ZdicSearchResult = DictSearchResult<ZdicResult>\n\nlet isRefererModified = false\n\nexport const search: SearchFunction<ZdicResult> = (\n  text,\n  config,\n  profile,\n  payload\n) => {\n  const isAudio = profile.dicts.all.zdic.options.audio\n  if (!isRefererModified && isAudio) {\n    isRefererModified = true\n    modifyReferer()\n  }\n\n  return fetchDirtyDOM(\n    'https://www.zdic.net/hans/' + encodeURIComponent(text.replace(/\\s+/g, ' '))\n  )\n    .catch(handleNetWorkError)\n    .then(doc => handleDOM(doc, isAudio))\n}\n\nfunction handleDOM(\n  doc: Document,\n  isAudio: boolean\n): ZdicSearchResult | Promise<ZdicSearchResult> {\n  const response: ZdicSearchResult = {\n    result: []\n  }\n\n  for (const $entry of doc.querySelectorAll<HTMLDivElement>(\n    '[data-type-block]'\n  )) {\n    const title = $entry.dataset.typeBlock || ''\n    if (!/基本解释|词语解释|详细解释/.test(title)) {\n      continue\n    }\n\n    for (const $a of $entry.querySelectorAll<HTMLAnchorElement>(\n      '[data-src-mp3]'\n    )) {\n      if (isAudio) {\n        if (!response.audio) {\n          response.audio = {\n            py: $a.dataset.srcMp3\n          }\n        }\n        $a.replaceWith(getStaticSpeaker($a.dataset.srcMp3))\n      } else {\n        $a.remove()\n      }\n    }\n\n    response.result.push({\n      title,\n      content: getInnerHTML(HOST, $entry, '.content')\n    })\n  }\n\n  return response.result.length > 0 ? response : handleNoResult()\n}\n\nfunction modifyReferer() {\n  const extraInfoSpec = ['blocking', 'requestHeaders']\n  // https://developer.chrome.com/extensions/webRequest#life_cycle_footnote\n  if (\n    browser.webRequest['OnBeforeSendHeadersOptions'] &&\n    Object.prototype.hasOwnProperty.call(\n      browser.webRequest['OnBeforeSendHeadersOptions'],\n      'EXTRA_HEADERS'\n    )\n  ) {\n    extraInfoSpec.push('extraHeaders')\n  }\n\n  browser.webRequest.onBeforeSendHeaders.addListener(\n    details => {\n      if (details && details.requestHeaders) {\n        for (var i = 0; i < details.requestHeaders.length; ++i) {\n          if (details.requestHeaders[i].name === 'Referer') {\n            details.requestHeaders[i].value = 'https://www.zdic.net'\n            break\n          }\n        }\n        if (i === details.requestHeaders.length) {\n          details.requestHeaders.push({\n            name: 'Referer',\n            value: 'https://www.zdic.net'\n          })\n        }\n      }\n      return { requestHeaders: details.requestHeaders }\n    },\n    { urls: ['https://img.zdic.net/audio/*'] },\n    /** WebExt type is missing Chrome support */\n    extraInfoSpec as any\n  )\n}\n"
  },
  {
    "path": "src/content/__fake__/env-instant-capture.ts",
    "content": "import { createIntantCaptureStream } from '@/selection/instant-capture'\nimport getDefaultConfig, { AppConfigMutable } from '@/app-config'\n\nconst config = getDefaultConfig() as AppConfigMutable\nconfig.mode.instant.enable = true\nconfig.mode.instant.key = 'ctrl'\n\ncreateIntantCaptureStream(config).subscribe(console.log)\n"
  },
  {
    "path": "src/content/__fake__/env-select-text.ts",
    "content": "import { createSelectTextStream } from '@/selection/select-text'\nimport getDefaultConfig from '@/app-config'\n\nconst config = getDefaultConfig()\n\ncreateSelectTextStream(config).subscribe(console.log)\n"
  },
  {
    "path": "src/content/__fake__/env.ts",
    "content": "import faker from 'faker'\nimport '@/selection'\nimport { initConfig, updateConfig } from '@/_helpers/config-manager'\nimport { initProfiles, updateProfile } from '@/_helpers/profile-manager'\nimport { ProfileMutable } from '@/app-config/profiles'\nimport { AppConfigMutable } from '@/app-config'\n\nbrowser.runtime.sendMessage['_sender'].callsFake(() => ({\n  tab: {\n    id: 'saladict-page'\n  }\n}))\n\ninitConfig().then(_config => {\n  const config = _config as AppConfigMutable\n  config.mode.instant.enable = true\n  config.panelMode.direct = true\n  updateConfig(config)\n})\ninitProfiles().then(profile => {\n  ;(profile as ProfileMutable).dicts.selected = ['bing']\n  updateProfile(profile)\n})\n\nfor (let i = 0; i < 10; i++) {\n  const $p = document.createElement('p')\n  $p.innerHTML = 'love ' + faker.lorem.paragraph()\n  document.body.appendChild($p)\n}\n"
  },
  {
    "path": "src/content/_style.scss",
    "content": ".saladict-div {\n  @extend %reset-important;\n}\n"
  },
  {
    "path": "src/content/components/DictItem/DictItem.scss",
    "content": "@import './DictItemHead.scss';\n\n.dictItem {\n  position: relative;\n  background: var(--color-background);\n}\n\n.dictItem-Body {\n  overflow: hidden;\n  position: relative;\n  padding: 0 10px;\n  font-size: 13px;\n  line-height: 1.6;\n  color: var(--color-font);\n\n  @include isAnimate {\n    transition: height 0.4s, opacity 0.4s;\n  }\n}\n\n.dictItem-BodyMesure {\n  // clear margin collapsing which seems to causing flickering\n  // when changing the parent height\n  overflow: hidden;\n  opacity: 0;\n  // react-resize-reporter\n  position: relative;\n\n  > *:first-child {\n    margin-top: 10px !important;\n  }\n\n  > *:last-child {\n    margin-bottom: 10px !important;\n  }\n\n  @include isAnimate {\n    transition: opacity 0.4s;\n  }\n\n  @include atRoot(\".isUnfold\") {\n    opacity: 1;\n  }\n}\n\n.dictItem-Loader {\n  align-self: center;\n  width: 120px;\n  height: 10px;\n  user-select: none;\n}\n\n.dictItem-Loader_Ball {\n  width: 10px;\n  height: 10px;\n  fill: orange;\n\n  &:nth-child(2) {\n    transform: translateX(15px);\n  }\n  &:nth-child(3) {\n    transform: translateX(30px);\n  }\n  &:nth-child(4) {\n    transform: translateX(45px);\n  }\n  &:nth-child(5) {\n    transform: translateX(60px);\n  }\n\n  @include isAnimate {\n    animation: dictItem-Loader-shift 2s linear infinite;\n\n    &:nth-child(2) {\n      animation-delay: -0.4s;\n    }\n    &:nth-child(3) {\n      animation-delay: -0.8s;\n    }\n    &:nth-child(4) {\n      animation-delay: -1.2s;\n    }\n    &:nth-child(5) {\n      animation-delay: -1.6s;\n    }\n  }\n}\n\n.dictItem-FoldMask {\n  position: absolute;\n  left: 0;\n  bottom: 0;\n  width: 100%;\n  height: 50px;\n  padding: 0;\n  border: none;\n  // safari doesn't support transparent\n  background: linear-gradient(\n    rgba(var(--color-rgb-background), 0) 40%,\n    rgba(var(--color-rgb-background), 0.5) 60%,\n    var(--color-background) 100%\n  );\n  opacity: 0.6;\n  cursor: pointer;\n  user-select: none;\n\n  &:hover {\n    outline: none;\n\n    opacity: 1;\n  }\n\n  @include isAnimate {\n    transition: opacity 400ms;\n  }\n}\n\n.dictItem-FoldMaskArrow {\n  position: absolute;\n  z-index: 10;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  margin: 0 auto;\n  width: 15px;\n  height: 15px;\n  fill: var(--color-font);\n}\n\n/*-----------------------------------------------*\\\n    States\n\\*-----------------------------------------------*/\n\n.noHeightTransition {\n  .dictItem-Body {\n    @include isAnimate {\n      transition: height 0s;\n    }\n  }\n}\n\n@keyframes dictItem-Loader-shift {\n  0% {\n    transform: translateX(0);\n    opacity: 0;\n  }\n  10% {\n    transform: translateX(30px);\n    opacity: 1;\n  }\n  90% {\n    transform: translateX(80px);\n    opacity: 1;\n  }\n  100% {\n    transform: translateX(110px);\n    opacity: 0;\n  }\n}\n"
  },
  {
    "path": "src/content/components/DictItem/DictItem.stories.tsx",
    "content": "import React from 'react'\nimport faker from 'faker'\nimport { storiesOf } from '@storybook/react'\nimport { jsxDecorator } from 'storybook-addon-jsx'\nimport { withPropsTable } from 'storybook-addon-react-docgen'\nimport { action } from '@storybook/addon-actions'\nimport { withKnobs, select, number } from '@storybook/addon-knobs'\nimport { withi18nNS, withSaladictPanel } from '@/_helpers/storybook'\nimport { DictItem } from './DictItem'\n\nstoriesOf('Content Scripts|Dict Panel', module)\n  .addParameters({\n    backgrounds: [\n      { name: 'Saladict', value: '#5caf9e', default: true },\n      { name: 'Black', value: '#000' }\n    ]\n  })\n  .addDecorator(withPropsTable)\n  .addDecorator(jsxDecorator)\n  .addDecorator(withKnobs)\n  .addDecorator(withi18nNS('content'))\n  .addDecorator(\n    withSaladictPanel({\n      head: <style>{require('./DictItem.scss').toString()}</style>,\n      height: 'auto'\n    })\n  )\n  // @ts-ignore: wrong storybook typing\n  .add('DictItem', ({ withAnimation, darkMode }) => {\n    return (\n      <DictItem\n        dictID=\"baidu\"\n        darkMode={darkMode}\n        withAnimation={withAnimation}\n        panelCSS={''}\n        preferredHeight={number('Preferred Height', 256)}\n        searchStatus={select(\n          'Search Status',\n          { IDLE: 'IDLE', SEARCHING: 'SEARCHING', FINISH: 'FINISH' },\n          'FINISH'\n        )}\n        searchResult={{\n          count: number('Paragraphs', 5)\n        }}\n        TestComp={({ result }: { result: { count: number } }) => (\n          <>\n            {[...Array(result.count)].map((line, i) => (\n              <p key={i}>{faker.lorem.paragraph()}</p>\n            ))}\n          </>\n        )}\n        searchText={action('Search Text')}\n        openDictSrcPage={action('Open Dict Source Page')}\n        onHeightChanged={action('Height Changed')}\n        onUserFold={action('User fold')}\n        onInPanelSelect={action('In-panel Selection')}\n        onSpeakerPlay={async src => action('Speaker Play')(src)}\n      />\n    )\n  })\n"
  },
  {
    "path": "src/content/components/DictItem/DictItem.tsx",
    "content": "import React, {\n  ComponentType,\n  FC,\n  useState,\n  useEffect,\n  useCallback,\n  useRef,\n  useMemo\n} from 'react'\nimport { useObservableCallback, identity } from 'observable-hooks'\nimport classnames from 'classnames'\nimport { ResizeReporter } from 'react-resize-reporter/scroll'\nimport { DictID } from '@/app-config'\nimport { message } from '@/_helpers/browser-api'\nimport { newWord } from '@/_helpers/record-manager'\nimport { timer } from '@/_helpers/promise-more'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { DictItemHead, DictItemHeadProps } from './DictItemHead'\nimport { DictItemBody, DictItemBodyProps } from './DictItemBody'\nimport { isTagName } from '@/_helpers/dom'\n\nconst DICT_ITEM_HEAD_HEIGHT = 20\n\nexport interface DictItemProps\n  extends Omit<DictItemBodyProps, 'catalogSelect$' | 'dictRootRef'> {\n  /** default height when search result is received */\n  preferredHeight: number\n  withAnimation: boolean\n  /** Inject dict component. Mainly for testing */\n  TestComp?: ComponentType<ViewPorps<any>>\n\n  catalog?: DictItemHeadProps['catalog']\n  openDictSrcPage: DictItemHeadProps['openDictSrcPage']\n\n  onHeightChanged: (id: DictID, height: number) => void\n\n  /** User manually folds or unfolds */\n  onUserFold: (id: DictID, fold: boolean) => void\n}\n\nexport const DictItem: FC<DictItemProps> = props => {\n  const [onCatalogSelect, catalogSelect$] = useObservableCallback<{\n    key: string\n    value: string\n  }>(identity)\n\n  /** Expand/collapse transition */\n  const [noHeightTransition, setNoHeightTransition] = useState(false)\n\n  const [foldState, setFoldState] = useState<'COLLAPSE' | 'HALF' | 'FULL'>(\n    'COLLAPSE'\n  )\n  /** Rendered height */\n  const [offsetHeight, setOffsetHeight] = useState(10)\n\n  const visibleHeight = useMemo(\n    () =>\n      Math.max(\n        10,\n        foldState === 'COLLAPSE'\n          ? 10\n          : foldState === 'FULL'\n          ? offsetHeight\n          : Math.min(offsetHeight, props.preferredHeight)\n      ),\n    [foldState, offsetHeight, props.preferredHeight]\n  )\n\n  useEffect(() => {\n    if (props.searchStatus === 'FINISH') {\n      setFoldState('HALF')\n    } else {\n      setFoldState('COLLAPSE')\n    }\n  }, [props.searchStatus])\n\n  useEffect(() => {\n    props.onHeightChanged(props.dictID, visibleHeight + DICT_ITEM_HEAD_HEIGHT)\n  }, [visibleHeight])\n\n  const dictItemRef = useRef<HTMLDivElement | null>(null)\n  // container element in shadow dom\n  const dictRootRef = useRef<HTMLDivElement | null>(null)\n\n  const preCatalogSelect = useCallback(\n    async (item: { key: string; value: string }) => {\n      if (item.key[0] !== '#') return onCatalogSelect(item)\n\n      // handle anchor jump\n      if (!dictRootRef.current) return\n\n      const anchor = dictRootRef.current.querySelector<HTMLElement>(\n        `#${item.value}`\n      )\n      if (!anchor) return\n\n      if (foldState !== 'FULL') {\n        setNoHeightTransition(true)\n        setFoldState('FULL')\n        await timer(0)\n        setNoHeightTransition(false)\n      }\n\n      if (dictItemRef.current) {\n        const rootNode = dictItemRef.current.getRootNode() as HTMLDivElement\n        if (rootNode.querySelector) {\n          const scrollParent = rootNode.querySelector('.dictPanel-Body')\n          if (scrollParent) {\n            scrollParent.scrollTo({\n              top:\n                anchor.getBoundingClientRect().y -\n                scrollParent.firstElementChild!.getBoundingClientRect().y -\n                30, // plus the sticky title bar\n              behavior: props.withAnimation ? 'smooth' : 'auto'\n            })\n            return\n          }\n        }\n      }\n\n      // Fallback to scrollIntoView\n      // The topmost area may scroll beyond dict header due to sticky layout\n      anchor.scrollIntoView({\n        behavior: props.withAnimation ? 'smooth' : 'auto'\n      })\n    },\n    [foldState, props.withAnimation]\n  )\n\n  return (\n    <section\n      ref={dictItemRef}\n      className={classnames('dictItem', {\n        isUnfold: foldState !== 'COLLAPSE',\n        noHeightTransition\n      })}\n    >\n      <DictItemHead\n        dictID={props.dictID}\n        catalog={props.catalog}\n        isSearching={props.searchStatus === 'SEARCHING'}\n        toggleFold={toggleFold}\n        openDictSrcPage={props.openDictSrcPage}\n        onCatalogSelect={preCatalogSelect}\n      />\n      <div\n        className=\"dictItem-Body\"\n        key={props.dictID}\n        style={{ height: visibleHeight }}\n        onClick={searchLinkText}\n      >\n        <article className=\"dictItem-BodyMesure\">\n          <ResizeReporter reportInit onHeightChanged={setOffsetHeight} />\n          {props.TestComp ? (\n            props.searchStatus === 'FINISH' &&\n            props.searchResult &&\n            React.createElement(props.TestComp, {\n              result: props.searchResult,\n              searchText: props.searchText,\n              catalogSelect$: catalogSelect$\n            })\n          ) : (\n            <DictItemBody\n              {...props}\n              catalogSelect$={catalogSelect$}\n              dictRootRef={dictRootRef}\n            />\n          )}\n        </article>\n        {foldState === 'HALF' &&\n          visibleHeight < offsetHeight &&\n          props.searchResult && (\n            <button\n              className=\"dictItem-FoldMask\"\n              onClick={() => setFoldState('FULL')}\n            >\n              <svg\n                className=\"dictItem-FoldMaskArrow\"\n                width=\"15\"\n                height=\"15\"\n                viewBox=\"0 0 59.414 59.414\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n              >\n                <path d=\"M58 14.146L29.707 42.44 1.414 14.145 0 15.56 29.707 45.27 59.414 15.56\" />\n              </svg>\n            </button>\n          )}\n      </div>\n    </section>\n  )\n\n  /** Search the content of an <a> instead of jumping unless it's external */\n  function searchLinkText(e: React.MouseEvent<HTMLElement>) {\n    if (e.ctrlKey || e.metaKey || e.altKey) {\n      // ignore if extra key is pressed\n      return\n    }\n\n    if (!(e.target as HTMLElement).tagName) {\n      return\n    }\n\n    const $dictItemRoot = e.currentTarget\n    for (\n      let el: HTMLElement | null = e.target as HTMLElement;\n      el && el !== $dictItemRoot;\n      el = el.parentElement\n    ) {\n      if (isTagName(el, 'a') || el.getAttribute('role') === 'link') {\n        e.preventDefault()\n        e.stopPropagation()\n\n        const $a = el as HTMLAnchorElement\n        if (/nofollow|noopener|noreferrer/.test($a.rel)) {\n          message.send({\n            type: 'OPEN_URL',\n            payload: {\n              url: $a.href\n            }\n          })\n        } else {\n          props.searchText({\n            word: newWord({\n              text: $a.textContent || '',\n              title: 'Saladict',\n              favicon: 'https://saladict.crimx.com/favicon.ico'\n            })\n          })\n        }\n\n        return\n      }\n    }\n  }\n\n  function toggleFold() {\n    if (props.searchStatus === 'SEARCHING') {\n      return\n    }\n\n    if (foldState !== 'COLLAPSE') {\n      setFoldState('COLLAPSE')\n      props.onUserFold(props.dictID, true)\n      return\n    }\n\n    props.onUserFold(props.dictID, false)\n\n    if (props.searchResult) {\n      setFoldState('HALF')\n    } else {\n      props.searchText({ id: props.dictID })\n    }\n  }\n}\n"
  },
  {
    "path": "src/content/components/DictItem/DictItemBody.tsx",
    "content": "import React, { ComponentType, FC, useMemo, Suspense } from 'react'\nimport classNames from 'classnames'\nimport root from 'react-shadow'\nimport { Observable } from 'rxjs'\nimport { DictID } from '@/app-config'\nimport { Word } from '@/_helpers/record-manager'\nimport { SALADICT_PANEL } from '@/_helpers/saladict'\nimport { ViewPorps } from '@/components/dictionaries/helpers'\nimport { ErrorBoundary } from '@/components/ErrorBoundary'\nimport { StaticSpeakerContainer } from '@/components/Speaker'\n\nconst dictContentStyles = require('./DictItemContent.shadow.scss').toString()\n\nexport interface DictItemBodyProps {\n  dictID: DictID\n\n  darkMode: boolean\n  withAnimation: boolean\n\n  panelCSS: string\n\n  searchStatus: 'IDLE' | 'SEARCHING' | 'FINISH'\n  searchResult?: object | null\n\n  catalogSelect$: Observable<{ key: string; value: string }>\n\n  dictRootRef: React.MutableRefObject<HTMLDivElement | null>\n\n  searchText: (arg?: {\n    id?: DictID\n    word?: Word\n    payload?: { [index: string]: any }\n  }) => any\n\n  onSpeakerPlay: (src: string) => Promise<void>\n\n  onInPanelSelect: (e: React.MouseEvent<HTMLElement>) => void\n}\n\nexport const DictItemBody: FC<DictItemBodyProps> = props => {\n  const Dict = useMemo(\n    () =>\n      React.lazy<ComponentType<ViewPorps<any>>>(() =>\n        import(\n          /* webpackInclude: /View\\.tsx$/ */\n          /* webpackMode: \"lazy\" */\n          `@/components/dictionaries/${props.dictID}/View.tsx`\n        )\n      ),\n    [props.dictID]\n  )\n\n  const DictStyle = useMemo(\n    () =>\n      React.lazy(async () => {\n        const styleModule = await import(\n          /* webpackInclude: /_style\\.shadow\\.scss$/ */\n          /* webpackMode: \"lazy\" */\n          `@/components/dictionaries/${props.dictID}/_style.shadow.scss`\n        )\n        return {\n          default: () => (\n            <style>{(styleModule.default || styleModule).toString()}</style>\n          )\n        }\n      }),\n    [props.dictID]\n  )\n\n  return (\n    <ErrorBoundary error={DictRenderError}>\n      <Suspense fallback={null}>\n        {props.searchStatus === 'FINISH' && props.searchResult && (\n          <root.div>\n            <div\n              ref={props.dictRootRef}\n              className={classNames({ darkMode: props.darkMode })}\n            >\n              <style>{dictContentStyles}</style>\n              <DictStyle />\n              {props.panelCSS ? <style>{props.panelCSS}</style> : null}\n              <StaticSpeakerContainer\n                className={classNames(\n                  `d-${props.dictID}`,\n                  'dictRoot',\n                  SALADICT_PANEL,\n                  { isAnimate: props.withAnimation }\n                )}\n                onPlayStart={props.onSpeakerPlay}\n                onMouseUp={props.onInPanelSelect}\n              >\n                <Dict\n                  result={props.searchResult}\n                  searchText={props.searchText}\n                  catalogSelect$={props.catalogSelect$}\n                />\n              </StaticSpeakerContainer>\n            </div>\n          </root.div>\n        )}\n      </Suspense>\n    </ErrorBoundary>\n  )\n}\n\nfunction DictRenderError() {\n  return (\n    <p style={{ textAlign: 'center' }}>\n      Render error. Please{' '}\n      <a\n        href=\"https://github.com/crimx/ext-saladict/issues\"\n        target=\"_blank\"\n        rel=\"nofollow noopener noreferrer\"\n      >\n        report issue\n      </a>\n      .\n    </p>\n  )\n}\n"
  },
  {
    "path": "src/content/components/DictItem/DictItemContent.shadow.scss",
    "content": "@import '@/_sass_shared/_reset.scss';\n@import '@/components/Speaker/Speaker.scss';\n@import '@/components/EntryBox/EntryBox.scss';\n\n.dictRoot {\n  font-size: var(--panel-font-size);\n  -webkit-font-smoothing: antialiased;\n  text-rendering: optimizelegibility;\n  font-family: \"Helvetica Neue\", Helvetica, Arial, \"Hiragino Sans GB\", \"Hiragino Sans GB W3\", \"Microsoft YaHei UI\", \"Microsoft YaHei\", sans-serif;\n\n  select {\n    display: block;\n    box-sizing: border-box;\n    width: 100%;\n    margin: 0 0 0.5em 0;\n    padding: .6em 1.4em .5em .8em;\n    border-radius: .5em;\n    font-size: var(--panel-font-size);\n    font-weight: 700;\n    line-height: 1.3;\n    -moz-appearance: none;\n    -webkit-appearance: none;\n    appearance: none;\n    -moz-appearance: none;\n    border: 1px solid rgba(133, 133, 133, 0.28);\n    box-shadow: 0 1px 0 1px rgba(0,0,0,.04);\n    color: var(--color-font-grey);\n    background-color: var(--color-background);\n    background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%235caf9e%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E');\n    background-repeat: no-repeat, repeat;\n    background-position: right .7em top 50%, 0 0;\n    background-size: .65em auto, 100%;\n\n    &::-ms-expand {\n      display: none;\n    }\n\n    &:hover {\n      border-color: rgba(133, 133, 133, 0.48);\n    }\n\n    &:focus {\n      border-color: rgba(59, 153, 252, .7);;\n      outline: none;\n    }\n\n    option {\n      font-weight: normal;\n    }\n  }\n}\n"
  },
  {
    "path": "src/content/components/DictItem/DictItemHead.scss",
    "content": "@import '@/components/HoverBox/HoverBox.scss';\n\n.dictItemHead {\n  display: flex;\n  box-sizing: border-box;\n  align-items: flex-start;\n  position: sticky;\n  top: 0;\n  z-index: 100;\n  height: 20px;\n  padding-right: 2px;\n  border-top: 1px var(--color-divider) dashed;\n  // sticky header to cover content\n  background: var(--color-background);\n}\n\n.dictItemHead-Logo {\n  width: 20px;\n  height: 20px;\n  margin-top: -1px;\n  user-select: none;\n}\n\n.dictItemHead-Title {\n  margin: 0;\n  padding: 0 3px;\n  line-height: 20px;\n  font-size: 12px;\n  font-weight: normal;\n\n  a {\n    color: var(--color-font);\n    text-decoration: none;\n  }\n}\n\n.dictItemHead-Menus_Btn {\n  width: 18px;\n  height: 18px;\n  margin: 1px 0 0;\n  padding: 0;\n  font-size: 0;\n  border: none;\n  border-radius: 3px;\n  outline: none;\n  background-color: transparent;\n  opacity: 0.7;\n  transition: background-color 0.4s;\n  cursor: pointer;\n\n  svg {\n    fill: var(--color-font);\n  }\n\n  &:hover,\n  &:focus,\n  &:active {\n    background-color: var(--color-divider);\n  }\n\n  @include isDarkMode {\n    &:hover,\n    &:focus,\n    &:active {\n      background-color: #2d3338;\n    }\n  }\n}\n\n.dictItemHead-EmptyArea {\n  flex: 1;\n  align-self: stretch;\n}\n\n.dictItemHead-FoldArrowBtn {\n  width: 20px - 1px;\n  height: 20px - 1px;\n  overflow: hidden;\n  background: none;\n  border: none;\n  padding: 0;\n  cursor: pointer;\n  user-select: none;\n\n  &:hover {\n    outline: none;\n  }\n}\n\n.dictItemHead-FoldArrow {\n  box-sizing: border-box;\n  fill: var(--color-font);\n  width: 18px;\n  height: 18px;\n  padding: 3px;\n  transform-origin: center;\n\n  @include isAnimate {\n    transition: transform 400ms;\n  }\n\n  @include atRoot(\".isUnfold\") {\n    transform: rotate(-90deg);\n  }\n}\n\n.dictItemHead-Loader {\n  display: flex;\n  align-items: center;\n  width: 54px;\n  height: 20px;\n}\n\n.dictItemHead-Loader > div {\n  width: 8px;\n  height: 8px;\n  margin: 2px;\n  background: #f9690e;\n  border-radius: 100%;\n\n  @include isAnimate {\n    animation: dictItemHead-Loader 1.5s infinite ease-in-out;\n\n    $dictItemHead-LoaderNum: 5;\n    @for $i from 1 through $dictItemHead-LoaderNum {\n      &:nth-child(#{$dictItemHead-LoaderNum + 1 - $i}) {\n        animation-delay: -0.1s * ($i - 1);\n      }\n    }\n  }\n}\n\n@keyframes dictItemHead-Loader {\n  0%,\n  30%,\n  70%,\n  100% {\n    transform: scale(0);\n  }\n\n  50% {\n    transform: scale(1);\n  }\n}\n"
  },
  {
    "path": "src/content/components/DictItem/DictItemHead.tsx",
    "content": "import React, { FC, useState, useEffect, useMemo } from 'react'\nimport classnames from 'classnames'\nimport { DictID } from '@/app-config'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { message } from '@/_helpers/browser-api'\nimport { HoverBox, HoverBoxItem } from '@/components/HoverBox'\n\nexport interface DictItemHeadProps {\n  dictID: DictID\n  isSearching: boolean\n  toggleFold: () => void\n  openDictSrcPage: (id: DictID, ctrlKey: boolean) => void\n  onCatalogSelect: (item: { key: string; value: string }) => void\n  catalog?: Array<\n    | {\n        // <button>\n        key: string\n        value: string\n        label: string\n        options?: undefined\n      }\n    | {\n        // <select>\n        key: string\n        value: string\n        options: Array<{\n          value: string\n          label: string\n        }>\n        title?: string\n      }\n  >\n}\n\nexport const DictItemHead: FC<DictItemHeadProps> = props => {\n  const { t, ready } = useTranslate(['dicts', 'content', 'langcode'])\n\n  const [showLoader, setShowLoader] = useState(false)\n  useEffect(() => {\n    // small time offset to add a little organic feeling\n    const ticket = setTimeout(\n      () => setShowLoader(props.isSearching),\n      Math.random() * 1500\n    )\n    return () => {\n      clearTimeout(ticket)\n    }\n  }, [props.isSearching])\n\n  const icon = useMemo(\n    () =>\n      browser.runtime.getURL(\n        require('@/components/dictionaries/' + props.dictID + '/favicon.png')\n      ),\n    [props.dictID]\n  )\n\n  const menuItems = useMemo(() => {\n    const menuItems: HoverBoxItem[] = []\n    const localedLabel = (text: string) =>\n      text.replace(/%t\\((\\S+)\\)/g, (m, s1) => t(s1))\n\n    if (props.catalog) {\n      for (const item of props.catalog) {\n        if (item.options) {\n          menuItems.push({\n            key: item.key,\n            value: item.value,\n            title: item.title && localedLabel(item.title),\n            options: item.options.map(opt => ({\n              value: opt.value,\n              label: localedLabel(opt.label)\n            }))\n          })\n        } else {\n          menuItems.push({\n            key: item.key,\n            value: item.value,\n            label: localedLabel(item.label)\n          })\n        }\n      }\n    }\n\n    menuItems.push({\n      key: '_options',\n      value: '_options',\n      label: t('content:tip.openOptions')\n    })\n\n    return menuItems\n  }, [props.catalog, ready])\n\n  return (\n    <header\n      className={classnames('dictItemHead', {\n        isSearching: props.isSearching\n      })}\n    >\n      <img className=\"dictItemHead-Logo\" src={icon} alt=\"dict logo\" />\n      <h1 className=\"dictItemHead-Title\">\n        <a\n          href=\"#\"\n          onClick={(e: React.MouseEvent<HTMLElement>) => {\n            e.stopPropagation()\n            e.preventDefault()\n            props.openDictSrcPage(props.dictID, e.ctrlKey)\n          }}\n        >\n          {t(`${props.dictID}.name`)}\n        </a>\n      </h1>\n      <HoverBox\n        compact\n        Button={MenusBtn}\n        items={menuItems}\n        top={25}\n        onSelect={(key, value) => {\n          if (key === '_options') {\n            message.send({\n              type: 'OPEN_URL',\n              payload: {\n                url: 'options.html?menuselected=Dictionaries',\n                self: true\n              }\n            })\n          } else {\n            props.onCatalogSelect({ key, value })\n          }\n        }}\n      />\n      {showLoader && (\n        <div className=\"dictItemHead-Loader\">\n          <div />\n          <div />\n          <div />\n          <div />\n          <div />\n        </div>\n      )}\n      <div className=\"dictItemHead-EmptyArea\" onClick={props.toggleFold} />\n      <button\n        className=\"dictItemHead-FoldArrowBtn\"\n        onMouseOut={e => e.currentTarget.blur()}\n        onClick={props.toggleFold}\n      >\n        <svg\n          className=\"dictItemHead-FoldArrow\"\n          width=\"18\"\n          height=\"18\"\n          viewBox=\"0 0 59.414 59.414\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n        >\n          <path\n            className=\"dictItemHead-FoldArrowPath\"\n            d=\"M43.854 59.414L14.146 29.707 43.854 0l1.414 1.414-28.293 28.293L45.268 58\"\n          />\n        </svg>\n      </button>\n    </header>\n  )\n}\n\nfunction MenusBtn(props: React.ComponentProps<'button'>) {\n  return (\n    <button className=\"dictItemHead-Menus_Btn\" {...props}>\n      <svg width=\"16\" height=\"16\" viewBox=\"0 0 512 512\">\n        <path d=\"M301.256 394.29A45.256 45.256 0 01256 439.546a45.256 45.256 0 01-45.256-45.256A45.256 45.256 0 01256 349.034a45.256 45.256 0 0145.256 45.256zM301.256 257.48A45.256 45.256 0 01256 302.736a45.256 45.256 0 01-45.256-45.256A45.256 45.256 0 01256 212.224a45.256 45.256 0 0145.256 45.256zM301.256 117.71A45.256 45.256 0 01256 162.964a45.256 45.256 0 01-45.256-45.256A45.256 45.256 0 01256 72.453a45.256 45.256 0 0145.256 45.256z\" />\n      </svg>\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/content/components/DictList/DictList.container.tsx",
    "content": "import { connect } from 'react-redux'\nimport {\n  ExtractDispatchers,\n  MapStateToProps,\n  MapDispatchToPropsFunction\n} from 'react-retux'\nimport memoizeOne from 'memoize-one'\nimport { StoreState, StoreDispatch } from '@/content/redux/modules'\nimport { message } from '@/_helpers/browser-api'\nimport { DictList, DictListProps } from './DictList'\n\nconst memoizedDicts = memoizeOne(\n  (\n    renderedDicts: StoreState['renderedDicts'],\n    allDict: StoreState['activeProfile']['dicts']['all']\n  ) =>\n    renderedDicts.map(dict => ({\n      dictID: dict.id,\n      searchStatus: dict.searchStatus,\n      searchResult: dict.searchResult,\n      catalog: dict.catalog,\n      preferredHeight: allDict[dict.id].preferredHeight\n    }))\n)\n\ntype Dispatchers = ExtractDispatchers<\n  DictListProps,\n  | 'searchText'\n  | 'openDictSrcPage'\n  | 'onHeightChanged'\n  | 'onUserFold'\n  | 'onSpeakerPlay'\n  | 'newSelection'\n>\n\nconst mapStateToProps: MapStateToProps<\n  StoreState,\n  DictListProps,\n  Dispatchers\n> = state => {\n  const { config } = state\n  return {\n    darkMode: config.darkMode,\n    withAnimation: config.animation,\n    panelCSS: config.panelCSS,\n    touchMode: config.touchMode,\n    language: config.language,\n    doubleClickDelay: config.doubleClickDelay,\n    dicts: memoizedDicts(state.renderedDicts, state.activeProfile.dicts.all)\n  }\n}\n\nconst mapDispatchToProps: MapDispatchToPropsFunction<\n  StoreDispatch,\n  DictListProps,\n  Dispatchers\n> = dispatch => ({\n  searchText: payload => {\n    dispatch({ type: 'SEARCH_START', payload })\n  },\n  openDictSrcPage: (id, ctrlKey) => {\n    dispatch((dispatch, getState) => {\n      const { searchHistory } = getState()\n      const word = searchHistory[searchHistory.length - 1]\n      message.send({\n        type: 'OPEN_DICT_SRC_PAGE',\n        payload: {\n          id,\n          text: word && word.text ? word.text : '',\n          active: !ctrlKey\n        }\n      })\n    })\n  },\n  onHeightChanged: height => {\n    dispatch({\n      type: 'UPDATE_PANEL_HEIGHT',\n      payload: { area: 'dictlist', height }\n    })\n  },\n  onUserFold: (id, fold) => {\n    dispatch({ type: 'USER_FOLD_DICT', payload: { id, fold } })\n  },\n  onSpeakerPlay: src => {\n    return new Promise(resolve => {\n      dispatch((dispatch, getState) => {\n        if (getState().isExpandWaveformBox) {\n          message.self.send({ type: 'PLAY_AUDIO', payload: src }).then(resolve)\n        } else {\n          message.send({ type: 'PLAY_AUDIO', payload: src }).then(resolve)\n        }\n        dispatch({\n          type: 'PLAY_AUDIO',\n          payload: { src, timestamp: Date.now() }\n        })\n      })\n    })\n  },\n  newSelection: (payload: StoreState['selection']) => {\n    dispatch({ type: 'NEW_SELECTION', payload })\n  }\n})\n\nexport const DictListContainer = connect(\n  mapStateToProps,\n  mapDispatchToProps\n)(DictList)\n\nexport default DictListContainer\n"
  },
  {
    "path": "src/content/components/DictList/DictList.scss",
    "content": "@import '../DictItem/DictItem.scss';\n\n.dictList > .dictItem:first-child > .dictItemHead {\n  border-top-color: transparent;\n}\n"
  },
  {
    "path": "src/content/components/DictList/DictList.stories.tsx",
    "content": "import React from 'react'\nimport faker from 'faker'\nimport { storiesOf } from '@storybook/react'\nimport { action } from '@storybook/addon-actions'\nimport { jsxDecorator } from 'storybook-addon-jsx'\nimport { withPropsTable } from 'storybook-addon-react-docgen'\nimport { withKnobs, number, boolean, object } from '@storybook/addon-knobs'\nimport { withSaladictPanel, withi18nNS } from '@/_helpers/storybook'\nimport { getAllDicts } from '@/app-config/dicts'\nimport { getDefaultConfig, DictID } from '@/app-config'\nimport { HoverBoxContext } from '@/components/HoverBox'\nimport { DictList } from './DictList'\n\nconst defaultLanguage = getDefaultConfig().language\n\nconst allDicts = getAllDicts()\nconst dicts = Object.keys(allDicts).map(id => ({\n  dictID: id as DictID,\n  preferredHeight: allDicts[id].preferredHeight\n}))\nconst searchStatus = ['IDLE', 'SEARCHING', 'FINISH'] as [\n  'IDLE',\n  'SEARCHING',\n  'FINISH'\n]\n\nstoriesOf('Content Scripts|Dict Panel', module)\n  .addParameters({\n    backgrounds: [\n      { name: 'Saladict', value: '#5caf9e', default: true },\n      { name: 'Black', value: '#000' }\n    ]\n  })\n  .addDecorator(withPropsTable)\n  .addDecorator(jsxDecorator)\n  .addDecorator(withKnobs)\n  .addDecorator(story => {\n    const rootRef: React.MutableRefObject<HTMLDivElement | null> = {\n      current: null\n    }\n    return (\n      <HoverBoxContext.Provider value={rootRef}>\n        <div ref={rootRef} style={{ position: 'relative' }}>\n          {story()}\n        </div>\n      </HoverBoxContext.Provider>\n    )\n  })\n  .addDecorator(\n    withSaladictPanel({\n      head: <style>{require('./DictList.scss').toString()}</style>,\n      height: 'auto'\n    })\n  )\n  .addDecorator(withi18nNS(['content', 'dicts']))\n  // @ts-ignore: wrong storybook typing\n  .add('DictList', ({ darkMode }) => (\n    <DictList\n      darkMode={darkMode}\n      touchMode={boolean('Touch Mode', false)}\n      language={object('Language', defaultLanguage)}\n      doubleClickDelay={number('Double Click Delay', 200)}\n      newSelection={action('New Selection')}\n      withAnimation={boolean('Enable Animation', true)}\n      panelCSS={''}\n      dicts={[...Array(faker.random.number({ min: 3, max: 10 }))].map(() => ({\n        ...faker.random.arrayElement(dicts),\n        searchStatus: faker.random.arrayElement(searchStatus),\n        searchResult: {\n          paragraphs: faker.lorem.paragraphs(\n            faker.random.number({ min: 1, max: 4 })\n          )\n        },\n        TestComp\n      }))}\n      searchText={action('Search Text')}\n      openDictSrcPage={action('Open Dict Source Page')}\n      onHeightChanged={action('Height Changed')}\n      onUserFold={action('User fold')}\n      onSpeakerPlay={async src => action('Speaker Play')(src)}\n    />\n  ))\n\nfunction TestComp({ result }: { result: { paragraphs: string } }) {\n  return (\n    <>\n      {result.paragraphs.split('\\n').map((line, i) => (\n        <p key={i}>{line}</p>\n      ))}\n    </>\n  )\n}\n"
  },
  {
    "path": "src/content/components/DictList/DictList.tsx",
    "content": "import React, { FC, useEffect, useRef, useMemo } from 'react'\nimport { DictItem, DictItemProps } from '../DictItem/DictItem'\nimport { DictID, AppConfig } from '@/app-config'\nimport { useObservableCallback, useSubscription } from 'observable-hooks'\nimport { debounceTime } from 'rxjs/operators'\nimport { useInPanelSelect } from '@/selection/select-text'\nimport { Word } from '@/_helpers/record-manager'\n\nconst MemoDictItem = React.memo(DictItem)\n\ntype DictListItemKeys =\n  | 'dictID'\n  | 'preferredHeight'\n  | 'searchStatus'\n  | 'searchResult'\n  | 'TestComp'\n\nexport interface DictListProps\n  extends Omit<\n    DictItemProps,\n    DictListItemKeys | 'onHeightChanged' | 'onInPanelSelect'\n  > {\n  dicts: Pick<DictItemProps, DictListItemKeys>[]\n  onHeightChanged: (height: number) => void\n\n  touchMode: AppConfig['touchMode']\n  language: AppConfig['language']\n  doubleClickDelay: AppConfig['doubleClickDelay']\n  newSelection: (payload: {\n    word: Word | null\n    mouseX: number\n    mouseY: number\n    dbClick: boolean\n    altKey: boolean\n    shiftKey: boolean\n    ctrlKey: boolean\n    metaKey: boolean\n    self: boolean\n    instant: boolean\n    force: boolean\n  }) => void\n}\n\ntype Height = {\n  dicts: { [key in DictID]?: number }\n  sum: number\n}\n\nexport const DictList: FC<DictListProps> = props => {\n  const {\n    dicts,\n    onHeightChanged,\n    touchMode,\n    language,\n    doubleClickDelay,\n    newSelection,\n    ...restProps\n  } = props\n\n  const heightRef = useRef<Height>({ dicts: {}, sum: 0 })\n\n  const [updateHeight, iupdateHeight$] = useObservableCallback<number>(event$ =>\n    debounceTime<number>(10)(event$)\n  )\n\n  useSubscription(iupdateHeight$, onHeightChanged)\n\n  const onItemHeightChanged = useRef((id: DictID, height: number) => {\n    heightRef.current.sum =\n      heightRef.current.sum - (heightRef.current.dicts[id] || 0) + height\n    heightRef.current.dicts[id] = height\n    updateHeight(heightRef.current.sum)\n  }).current\n\n  const dictIds = useMemo(\n    () => dicts.reduce((idStr, { dictID }) => idStr + dictID + ',', ''),\n    [dicts]\n  )\n\n  useEffect(() => {\n    const oldHeight = heightRef.current\n    heightRef.current = dicts.reduce(\n      (height, { dictID }) => {\n        height.dicts[dictID] = oldHeight.dicts[dictID] || 30\n        height.sum += height.dicts[dictID] || 30\n        return height\n      },\n      { dicts: {}, sum: 0 } as Height\n    )\n    updateHeight(heightRef.current.sum)\n  }, [dictIds])\n\n  const onInPanelSelect = useInPanelSelect(\n    touchMode,\n    language,\n    doubleClickDelay,\n    newSelection\n  )\n\n  return (\n    <div className=\"dictList\">\n      {dicts.map(data => (\n        <MemoDictItem\n          key={data.dictID}\n          {...restProps}\n          {...data}\n          onInPanelSelect={onInPanelSelect}\n          onHeightChanged={onItemHeightChanged}\n        />\n      ))}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/content/components/DictPanel/DictPanel.container.tsx",
    "content": "import React from 'react'\nimport { connect } from 'react-redux'\nimport {\n  ExtractDispatchers,\n  MapStateToProps,\n  MapDispatchToProps\n} from 'react-retux'\nimport { StoreState, StoreAction } from '@/content/redux/modules'\nimport { DictPanelPortal, DictPanelPortalProps } from './DictPanel.portal'\nimport { MenuBarContainer } from '../MenuBar/MenuBar.container'\nimport { MtaBoxContainer } from '../MtaBox/MtaBox.container'\nimport { DictListContainer } from '../DictList/DictList.container'\nimport { WaveformBoxContainer } from '../WaveformBox/WaveformBox.container'\n\nconst menuBar = <MenuBarContainer />\nconst dictList = <DictListContainer />\nconst waveformBox = <WaveformBoxContainer />\n\ntype Dispatchers = ExtractDispatchers<DictPanelPortalProps, 'onDragEnd'>\n\nconst mapStateToProps: MapStateToProps<\n  StoreState,\n  DictPanelPortalProps,\n  Dispatchers\n> = state => ({\n  show: state.isShowDictPanel,\n  coord: state.dictPanelCoord,\n  takeCoordSnapshot: state.wordEditor.isShow,\n  width: state.config.panelWidth,\n  height: state.panelHeight,\n  maxHeight: state.panelMaxHeight,\n  fontSize: state.config.fontSize,\n  withAnimation: state.config.animation,\n  panelCSS: state.config.panelCSS,\n  darkMode: state.config.darkMode,\n  menuBar,\n  mtaBox: state.isShowMtaBox ? <MtaBoxContainer /> : null,\n  dictList,\n  waveformBox: state.activeProfile.waveform ? waveformBox : null,\n  dragStartCoord: state.dragStartCoord\n})\n\nconst mapDispatchToProps: MapDispatchToProps<\n  StoreAction,\n  DictPanelPortalProps,\n  Dispatchers\n> = dispatch => ({\n  onDragEnd: () => {\n    dispatch({ type: 'DRAG_START_COORD', payload: null })\n  }\n})\n\nexport const DictPanelPortalContainer = connect(\n  mapStateToProps,\n  mapDispatchToProps\n)(DictPanelPortal)\n\nexport default DictPanelPortalContainer\n"
  },
  {
    "path": "src/content/components/DictPanel/DictPanel.portal.tsx",
    "content": "import React, { FC, useRef, useState } from 'react'\nimport { useUpdateEffect } from 'react-use'\nimport classnames from 'classnames'\nimport { useRefFn } from 'observable-hooks'\nimport { SALADICT_PANEL } from '@/_helpers/saladict'\nimport { ShadowPortal, defaultTimeout } from '@/components/ShadowPortal'\nimport { DictPanel, DictPanelProps } from './DictPanel'\n\nexport interface DictPanelPortalProps extends DictPanelProps {\n  show: boolean\n  withAnimation: boolean\n  darkMode: boolean\n  panelCSS: string\n}\n\nexport const DictPanelPortal: FC<DictPanelPortalProps> = props => {\n  const {\n    show: showProps,\n    panelCSS,\n    withAnimation,\n    darkMode,\n    ...restProps\n  } = props\n  const showRef = useRef(showProps)\n  const [show, setShow] = useState(showProps)\n\n  const panelStyle = useRefFn(() => (\n    <style>{require('./DictPanel.shadow.scss').toString()}</style>\n  )).current\n\n  useUpdateEffect(() => {\n    setShow(showProps)\n  }, [showProps])\n\n  // Restore if panel was hidden before snapshot,\n  // otherwise ignore.\n  useUpdateEffect(() => {\n    if (props.takeCoordSnapshot) {\n      showRef.current = show\n    } else if (!showRef.current) {\n      setShow(false)\n    }\n  }, [props.takeCoordSnapshot])\n\n  return (\n    <ShadowPortal\n      id=\"saladict-dictpanel-root\"\n      head={panelStyle}\n      shadowRootClassName={SALADICT_PANEL}\n      innerRootClassName={classnames({ isAnimate: withAnimation, darkMode })}\n      panelCSS={panelCSS}\n      in={show}\n      timeout={props.withAnimation ? defaultTimeout : 0}\n    >\n      {() => <DictPanel {...restProps} />}\n    </ShadowPortal>\n  )\n}\n\nexport default DictPanelPortal\n"
  },
  {
    "path": "src/content/components/DictPanel/DictPanel.scss",
    "content": "@import '@/_sass_shared/_theme.scss';\n\n.dictPanel-FloatBox-Container {\n  position: relative;\n}\n\n.dictPanel-Root {\n  display: flex;\n  flex-direction: column;\n  box-sizing: border-box;\n  position: fixed;\n  z-index: $global-zindex-dictpanel;\n  top: 0;\n  left: 0;\n  overflow: hidden;\n  text-align: initial;\n  border-radius: 6px;\n  background-color: inherit;\n  box-shadow: rgba(0, 0, 0, 0.8) 0px 4px 23px -6px;\n}\n\n.dictPanel-Head {\n  flex-shrink: 0;\n}\n\n.dictPanel-Body {\n  flex: 1;\n  overflow-x: hidden;\n  overflow-y: scroll;\n  // font-size: 0; // https://bugzilla.mozilla.org/show_bug.cgi?id=1573030\n  -webkit-overflow-scrolling: touch;\n}\n\n@import '@/_sass_shared/_fancy-scrollbar.scss';\n\n@import '../MenuBar/MenuBar.scss';\n@import '../MtaBox/MtaBox.scss';\n@import '../DictList/DictList.scss';\n@import '../WaveformBox/WaveformBox.scss';\n"
  },
  {
    "path": "src/content/components/DictPanel/DictPanel.shadow.scss",
    "content": "@import '@/components/ShadowPortal/ShadowPortal.scss';\n@import './DictPanel.scss';\n\n.dictPanel-DragMask {\n  position: fixed;\n  z-index: $global-zindex-dictpanel;\n  top: 0;\n  left: 0;\n  bottom: 0;\n  right: 0;\n  margin: auto;\n  background: rgba(225, 225, 225, 0.01);\n  cursor: grabbing;\n  cursor: -moz-grabbing;\n  cursor: -webkit-grabbing;\n}\n\n.dictPanel-Root {\n  @include isAnimate {\n    transition: width 0.4s, height 0.4s, opacity 0.4s,\n      top 0.4s cubic-bezier(0.55, 0.82, 0.63, 0.95),\n      left 0.4s cubic-bezier(0.4, 0.9, 0.71, 1.02);\n  }\n\n  &.isDragging {\n    @include isAnimate {\n      transition: width 0.4s, height 0.4s, opacity 0.4s;\n    }\n  }\n}\n"
  },
  {
    "path": "src/content/components/DictPanel/DictPanel.stories.tsx",
    "content": "import React, { useState, useMemo } from 'react'\nimport { storiesOf } from '@storybook/react'\nimport { action } from '@storybook/addon-actions'\nimport { jsxDecorator } from 'storybook-addon-jsx'\nimport { withPropsTable } from 'storybook-addon-react-docgen'\nimport {\n  withKnobs,\n  number,\n  boolean,\n  select,\n  text\n} from '@storybook/addon-knobs'\nimport {\n  withLocalStyle,\n  withSideEffect,\n  mockRuntimeMessage,\n  withi18nNS\n} from '@/_helpers/storybook'\nimport faker from 'faker'\nimport { DictPanel } from './DictPanel'\nimport { DictPanelPortal, DictPanelPortalProps } from './DictPanel.portal'\nimport { newWord } from '@/_helpers/record-manager'\nimport { getAllDicts } from '@/app-config/dicts'\nimport { getDefaultConfig, DictID } from '@/app-config'\nimport { MenuBar } from '../MenuBar/MenuBar'\nimport { MtaBox } from '../MtaBox/MtaBox'\nimport { DictList } from '../DictList/DictList'\nimport { WaveformBox } from '../WaveformBox/WaveformBox'\nimport { timer } from '@/_helpers/promise-more'\nimport { SuggestItem } from '../MenuBar/Suggest'\n\nconst allDicts = getAllDicts()\nconst dicts = Object.keys(allDicts).map(id => ({\n  dictID: id as DictID,\n  preferredHeight: allDicts[id].preferredHeight\n}))\nconst searchStatus = ['IDLE', 'SEARCHING', 'FINISH'] as [\n  'IDLE',\n  'SEARCHING',\n  'FINISH'\n]\n\nstoriesOf('Content Scripts|Dict Panel', module)\n  .addParameters({\n    backgrounds: [\n      { name: 'Saladict', value: '#5caf9e', default: true },\n      { name: 'Black', value: '#000' }\n    ]\n  })\n  .addDecorator(withPropsTable)\n  .addDecorator(jsxDecorator)\n  .addDecorator(withKnobs)\n  .addDecorator(\n    withSideEffect(\n      mockRuntimeMessage(async message => {\n        if (message.type === 'GET_SUGGESTS') {\n          await timer(Math.random() * 1500)\n          return fakeSuggest(message.payload)\n        }\n        console.log(message.type)\n      })\n    )\n  )\n  .addDecorator(withLocalStyle(require('./DictPanel.shadow.scss').toString()))\n  .addDecorator(withi18nNS(['content', 'dicts']))\n  // @ts-ignore\n  .addDecorator(Story => <Story />)\n  .add('DictPanel', () => <DictPanel {...useDictPanelProps()} />)\n  .add('DictPanelPortal', () => <DictPanelPortal {...useDictPanelProps()} />)\n\nfunction useDictPanelProps(): DictPanelPortalProps {\n  const [searchText, setText] = useState('saladict')\n  const [expandMta, setExpandMta] = useState(false)\n  const [expandWavform, setExpandWavform] = useState(false)\n  const withAnimation = boolean('Enable Animation', true)\n  const [dragStartCoord, setDragStartCoord] = useState<null | {\n    x: number\n    y: number\n  }>(null)\n\n  const config = getDefaultConfig()\n\n  const dictsNum = number(\n    'Dict Item Count',\n    faker.random.number({ min: 3, max: 10 })\n  )\n\n  const randomDicts = useMemo(() => {\n    const shuffledDicts = faker.helpers.shuffle(\n      dicts.map(config => ({\n        ...config,\n        searchStatus: faker.random.arrayElement(searchStatus),\n        searchResult: {\n          paragraphs: faker.lorem.paragraphs(\n            faker.random.number({ min: 1, max: 4 })\n          )\n        },\n        TestComp: TestComp\n      }))\n    )\n    return shuffledDicts.slice(\n      0,\n      faker.random.number({ max: shuffledDicts.length })\n    )\n  }, [dictsNum])\n\n  const histories = Array.from(Array(5)).map((_, i) =>\n    newWord({\n      date: Date.now(),\n      text: 'text' + (i + 1)\n    })\n  )\n\n  const profiles = Array.from(Array(5)).map((_, i) => ({\n    id: `profile${i + 1}`,\n    name: `Profile${i + 1}`\n  }))\n\n  const profilesOption = profiles.reduce((o, p) => {\n    o[p.name] = p.id\n    return o\n  }, {})\n\n  const darkMode = boolean('Dark Mode', false)\n\n  return {\n    show: boolean('Show', true),\n    panelCSS: text('Panel CSS', ''),\n    coord: {\n      x: number('x', (window.innerWidth - 450) / 2),\n      y: number('y', 10)\n    },\n    takeCoordSnapshot: boolean('Take Coord Snapshot', false),\n    width: number('Width', 450),\n    height: number('Height', window.innerHeight - 20),\n    maxHeight: number('Max Height', window.innerHeight - 40),\n    fontSize: number('Font Size', 13),\n    withAnimation: withAnimation,\n    darkMode,\n    menuBar: (\n      <MenuBar\n        text={searchText}\n        isTrackHistory={false}\n        updateText={text => {\n          action('Update Text')(text)\n          setText(text)\n        }}\n        searchText={action('Search Text')}\n        isInNotebook={boolean('Is In Notebook', false)}\n        addToNoteBook={action('Add to Notebook')}\n        shouldFocus={!expandMta}\n        enableSuggest={boolean('Enable Suggest', true)}\n        histories={histories}\n        historyIndex={number('History Index', 0)}\n        switchHistory={action('Switch History')}\n        isPinned={boolean('Is Pinned', false)}\n        togglePin={action('Toggle Pin')}\n        isQSFocus={boolean('Is Quick Search Panel Focus', false)}\n        toggleQSFocus={action('Toggle Quick Search Panel Focus')}\n        onClose={action('Close Panel')}\n        profiles={profiles}\n        activeProfileId={select(\n          'Active Profile',\n          profilesOption,\n          profiles[0].id\n        )}\n        onSelectProfile={action('Select Profile')}\n        onHeightChanged={newHeight => {\n          action('MenuBar Height Changed')(newHeight)\n        }}\n        onDragAreaMouseDown={e =>\n          setDragStartCoord({ x: e.clientX, y: e.clientY })\n        }\n        onDragAreaTouchStart={e =>\n          setDragStartCoord({\n            x: e.changedTouches[0].clientX,\n            y: e.changedTouches[0].clientY\n          })\n        }\n        onSwitchSidebar={action('onSwitchSidebar')}\n      />\n    ),\n    mtaBox: (\n      <MtaBox\n        text={searchText}\n        expand={expandMta}\n        searchText={action('Search Text')}\n        onInput={text => {\n          action('Input')(text)\n          setText(text)\n        }}\n        onDrawerToggle={() => {\n          action('Drawer Toggle')()\n          setExpandMta(!expandMta)\n        }}\n        onHeightChanged={action('Dict Mta Box Height Changed')}\n        shouldFocus={boolean('Should Focus', true)}\n      />\n    ),\n    dictList: (\n      <DictList\n        darkMode={config.darkMode}\n        touchMode={config.touchMode}\n        language={config.language}\n        doubleClickDelay={config.doubleClickDelay}\n        withAnimation={withAnimation}\n        panelCSS={''}\n        dicts={randomDicts}\n        searchText={action('Search Text')}\n        openDictSrcPage={action('Open Source Page')}\n        onSpeakerPlay={async src => action('Open Source Page')(src)}\n        onHeightChanged={action('Dict List Height Changed')}\n        onUserFold={action('User Fold')}\n        newSelection={action('New Selection')}\n      />\n    ),\n    waveformBox: (\n      <WaveformBox\n        isExpand={expandWavform}\n        darkMode={darkMode}\n        toggleExpand={() => setExpandWavform(flag => !flag)}\n        onHeightChanged={action('Dict Waveform Box Height Changed')}\n      />\n    ),\n    dragStartCoord,\n    onDragEnd: () => setDragStartCoord(null)\n  }\n}\n\nfunction TestComp({ result }: { result: { paragraphs: string } }) {\n  return (\n    <>\n      {result.paragraphs.split('\\n').map((line, i) => (\n        <p key={i}>{line}</p>\n      ))}\n    </>\n  )\n}\n\nfunction fakeSuggest(text: string): SuggestItem[] {\n  return Array.from(Array(10)).map((v, i) => ({\n    explain: `单词 ${text} 的各种相近的建议#${i}`,\n    entry: `Word ${text}#${i}`\n  }))\n}\n"
  },
  {
    "path": "src/content/components/DictPanel/DictPanel.tsx",
    "content": "import React, {\n  FC,\n  ReactNode,\n  useRef,\n  useState,\n  useMemo,\n  useEffect\n} from 'react'\nimport classnames from 'classnames'\nimport { useUpdateEffect } from 'react-use'\nimport { getScrollbarWidth } from '@/_helpers/scrollbar-width'\nimport { SALADICT_PANEL, isInternalPage } from '@/_helpers/saladict'\nimport { HoverBoxContext } from '@/components/HoverBox'\n\nexport interface DictPanelProps {\n  /** Update position command from uptream */\n  coord: {\n    x: number\n    y: number\n  }\n  /** Take or restore position snaphot when this value changes */\n  takeCoordSnapshot: boolean\n\n  width: number\n  height: number\n  maxHeight: number\n  fontSize: number\n\n  menuBar: ReactNode\n  mtaBox: ReactNode\n  dictList: ReactNode\n  waveformBox: ReactNode\n\n  dragStartCoord: null | { x: number; y: number }\n  onDragEnd: () => void\n}\n\nexport const DictPanel: FC<DictPanelProps> = props => {\n  const rootElRef = useRef<HTMLDivElement | null>(null)\n\n  const [x, setX] = useState(() => reconcileX(props.width, props.coord.x))\n  const [y, setY] = useState(() => reconcileY(props.height, props.coord.y))\n\n  const userDraggedRef = useRef(false)\n\n  const coordSnapshotRef = useRef<{ x: number; y: number }>()\n\n  useUpdateEffect(() => {\n    if (props.takeCoordSnapshot) {\n      coordSnapshotRef.current = { x, y }\n    } else {\n      if (coordSnapshotRef.current) {\n        setX(reconcileX(props.width, coordSnapshotRef.current.x))\n        setY(reconcileY(props.height, coordSnapshotRef.current.y))\n      }\n    }\n  }, [props.takeCoordSnapshot])\n\n  useUpdateEffect(() => {\n    setX(reconcileX(props.width, props.coord.x))\n    setY(reconcileY(props.height, props.coord.y))\n  }, [props.coord])\n\n  useUpdateEffect(() => {\n    // only reconcile if never been dragged\n    if (!userDraggedRef.current) {\n      setX(x => reconcileX(props.width, x))\n      setY(y => reconcileY(props.height, y))\n    }\n  }, [props.width, props.height])\n\n  useEffect(() => {\n    if (props.dragStartCoord) {\n      userDraggedRef.current = true\n    }\n  }, [props.dragStartCoord])\n\n  const dragStartPanelCoord = useMemo(\n    () => (props.dragStartCoord ? { x, y } : null),\n    [props.dragStartCoord]\n  )\n\n  return (\n    // an extra layer for float box\n    <div\n      ref={rootElRef}\n      className=\"dictPanel-FloatBox-Container saladict-theme\"\n    >\n      <div\n        className={classnames('dictPanel-Root', SALADICT_PANEL, {\n          isDragging: props.dragStartCoord\n        })}\n        style={{\n          left: x,\n          top: y,\n          zIndex: isInternalPage() ? 999 : 2147483647, // for popups on options page\n          width: props.width,\n          height: props.height,\n          '--panel-width': props.width + 'px',\n          '--panel-max-height': props.maxHeight + 'px',\n          '--panel-font-size': props.fontSize + 'px'\n        }}\n      >\n        <div className=\"dictPanel-Head\">{props.menuBar}</div>\n        <HoverBoxContext.Provider value={rootElRef}>\n          <div\n            className={`dictPanel-Body${\n              getScrollbarWidth() > 0 ? ' fancy-scrollbar' : ''\n            }`}\n          >\n            {props.mtaBox}\n            {props.dictList}\n          </div>\n        </HoverBoxContext.Provider>\n        {props.waveformBox}\n        {props.dragStartCoord && (\n          <div\n            className=\"dictPanel-DragMask\"\n            onMouseMove={e => {\n              if (dragStartPanelCoord && props.dragStartCoord) {\n                e.stopPropagation()\n                e.preventDefault()\n                setX(e.clientX - props.dragStartCoord.x + dragStartPanelCoord.x)\n                setY(e.clientY - props.dragStartCoord.y + dragStartPanelCoord.y)\n              }\n            }}\n            onTouchMove={e => {\n              if (dragStartPanelCoord && props.dragStartCoord) {\n                e.stopPropagation()\n                e.preventDefault()\n                setX(\n                  e.changedTouches[0].clientX -\n                    props.dragStartCoord.x +\n                    dragStartPanelCoord.x\n                )\n                setY(\n                  e.changedTouches[0].clientY -\n                    props.dragStartCoord.y +\n                    dragStartPanelCoord.y\n                )\n              }\n            }}\n            onMouseOut={e => {\n              if (!e.relatedTarget) {\n                props.onDragEnd()\n              }\n            }}\n            onMouseUp={props.onDragEnd}\n            onTouchCancel={props.onDragEnd}\n            onTouchEnd={props.onDragEnd}\n          />\n        )}\n      </div>\n    </div>\n  )\n}\n\nfunction reconcileX(width: number, x: number): number {\n  const winWidth = window.innerWidth\n\n  // also counted scrollbar width\n  if (x + width + 25 > winWidth) {\n    x = winWidth - 25 - width\n  }\n\n  if (x < 10) {\n    x = 10\n  }\n\n  return x\n}\n\nfunction reconcileY(height: number, y: number): number {\n  const winHeight = window.innerHeight\n\n  if (y + height + 15 > winHeight) {\n    y = winHeight - 15 - height\n  }\n\n  if (y < 15) {\n    y = 15\n  }\n\n  return y\n}\n"
  },
  {
    "path": "src/content/components/DictPanel/DictPanelStandalone.container.tsx",
    "content": "import React from 'react'\nimport { connect } from 'react-redux'\nimport {\n  DictPanelStandalone,\n  DictPanelStandaloneProps\n} from './DictPanelStandalone'\nimport { StoreState } from '@/content/redux/modules'\nimport { MenuBarContainer } from '../MenuBar/MenuBar.container'\nimport { MtaBoxContainer } from '../MtaBox/MtaBox.container'\nimport { DictListContainer } from '../DictList/DictList.container'\nimport { WaveformBoxContainer } from '../WaveformBox/WaveformBox.container'\n\nconst menuBar = <MenuBarContainer />\nconst dictList = <DictListContainer />\nconst waveformBox = <WaveformBoxContainer />\n\ntype OwnProps = 'height' | 'width'\n\nconst mapStateToProps = (\n  state: StoreState,\n  ownProps: Pick<DictPanelStandaloneProps, OwnProps>\n): DictPanelStandaloneProps => {\n  return {\n    withAnimation: state.config.animation,\n    darkMode: state.config.darkMode,\n    panelCSS: state.config.panelCSS,\n    fontSize: state.config.fontSize,\n    menuBar,\n    mtaBox: state.isShowMtaBox ? <MtaBoxContainer /> : null,\n    dictList,\n    waveformBox: state.activeProfile.waveform ? waveformBox : null,\n    width: ownProps.width,\n    height: ownProps.height\n  }\n}\n\nexport const DictPanelStandaloneContainer = connect(mapStateToProps)(\n  DictPanelStandalone\n)\n\nexport default DictPanelStandaloneContainer\n"
  },
  {
    "path": "src/content/components/DictPanel/DictPanelStandalone.scss",
    "content": "@import './DictPanel.scss';\n\n.dictPanel-FloatBox-Container {\n  position: absolute;\n  top: 0;\n  left: 0;\n}\n\n.dictPanel-Root {\n  position: relative !important;\n  top: 0 !important;\n  left: 0 !important;\n  width: 450px;\n  height: 500px;\n  --panel-width: 450px;\n  --panel-max-height: 500px;\n  border-radius: 0;\n  box-shadow: rgba(0, 0, 0, 0.8) 0px 5px 20px -12px;\n\n  @include isAnimate {\n    transition: height 0.4s;\n  }\n}\n"
  },
  {
    "path": "src/content/components/DictPanel/DictPanelStandalone.tsx",
    "content": "import React, { FC, ReactNode, useRef } from 'react'\nimport classnames from 'classnames'\nimport { SALADICT_PANEL } from '@/_helpers/saladict'\nimport { HoverBoxContext } from '@/components/HoverBox'\n\nexport interface DictPanelStandaloneProps {\n  width: string\n  height: string\n  fontSize: number\n\n  withAnimation: boolean\n  darkMode: boolean\n  panelCSS?: string\n\n  menuBar: ReactNode\n  mtaBox: ReactNode\n  dictList: ReactNode\n  waveformBox: ReactNode\n}\n\nexport const DictPanelStandalone: FC<DictPanelStandaloneProps> = props => {\n  const rootElRef = useRef<HTMLDivElement | null>(null)\n\n  return (\n    // an extra layer as float box offest parent\n    <div\n      className={classnames('dictPanel-FloatBox-Container', {\n        isAnimate: props.withAnimation,\n        darkMode: props.darkMode\n      })}\n    >\n      <div ref={rootElRef} className=\"saladict-theme\">\n        {props.panelCSS ? <style>{props.panelCSS}</style> : null}\n        <div\n          className={`dictPanel-Root ${SALADICT_PANEL}`}\n          style={{\n            width: props.width,\n            height: props.height,\n            '--panel-width': props.width,\n            '--panel-max-height': props.height,\n            '--panel-font-size': props.fontSize + 'px'\n          }}\n        >\n          <div className=\"dictPanel-Head\">{props.menuBar}</div>\n          <HoverBoxContext.Provider value={rootElRef}>\n            <div className=\"dictPanel-Body fancy-scrollbar\">\n              {props.mtaBox}\n              {props.dictList}\n            </div>\n          </HoverBoxContext.Provider>\n          {props.waveformBox}\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport default DictPanelStandalone\n"
  },
  {
    "path": "src/content/components/MenuBar/MenuBar.container.tsx",
    "content": "import { connect } from 'react-redux'\nimport {\n  ExtractDispatchers,\n  MapStateToProps,\n  MapDispatchToPropsFunction\n} from 'react-retux'\nimport { StoreState, StoreDispatch } from '@/content/redux/modules'\nimport { updateActiveProfileID } from '@/_helpers/profile-manager'\nimport {\n  isStandalonePage,\n  isPopupPage,\n  isQuickSearchPage\n} from '@/_helpers/saladict'\nimport { newWord } from '@/_helpers/record-manager'\nimport { message } from '@/_helpers/browser-api'\nimport { MenuBar, MenuBarProps } from './MenuBar'\nimport { updateConfig } from '@/_helpers/config-manager'\nimport { timer } from '@/_helpers/promise-more'\nimport { objectKeys } from '@/typings/helpers'\n\ntype Dispatchers = ExtractDispatchers<\n  MenuBarProps,\n  | 'searchText'\n  | 'updateText'\n  | 'addToNoteBook'\n  | 'switchHistory'\n  | 'togglePin'\n  | 'toggleQSFocus'\n  | 'onClose'\n  | 'onSwitchSidebar'\n  | 'onSelectProfile'\n  | 'onDragAreaMouseDown'\n  | 'onDragAreaTouchStart'\n  | 'onHeightChanged'\n>\n\nconst mapStateToProps: MapStateToProps<\n  StoreState,\n  MenuBarProps,\n  Dispatchers\n> = state => ({\n  text: state.text,\n  isInNotebook: state.isFav,\n  shouldFocus:\n    !state.isExpandMtaBox && // multiline search box must be folded\n    (((state.isQSPanel || isQuickSearchPage()) && // is quick search panel\n      state.config.qsFocus) ||\n      isPopupPage()), // or popup page\n  enableSuggest: state.config.searchSuggests,\n  isTrackHistory: state.config.searchHistory,\n  histories: state.searchHistory,\n  historyIndex: state.historyIndex,\n  showedDictAuth: state.config.showedDictAuth,\n  profiles: state.profiles,\n  activeProfileId: state.activeProfile.id,\n  isPinned: state.isPinned,\n  isQSFocus: state.isQSFocus\n})\n\nconst mapDispatchToProps: MapDispatchToPropsFunction<\n  StoreDispatch,\n  MenuBarProps,\n  Dispatchers\n> = dispatch => ({\n  searchText: text => {\n    dispatch({\n      type: 'SEARCH_START',\n      payload: {\n        word: newWord({\n          text,\n          title: 'Saladict',\n          favicon: 'https://saladict.crimx.com/favicon.ico'\n        })\n      }\n    })\n  },\n  updateText: text => {\n    dispatch({ type: 'UPDATE_TEXT', payload: text })\n  },\n  addToNoteBook: () => {\n    dispatch({ type: 'ADD_TO_NOTEBOOK' })\n  },\n  switchHistory: direction => {\n    dispatch({ type: 'SWITCH_HISTORY', payload: direction })\n  },\n  togglePin: () => {\n    dispatch({ type: 'TOGGLE_PIN' })\n  },\n  toggleQSFocus: () => {\n    dispatch({ type: 'TOGGLE_QS_FOCUS' })\n  },\n  onClose: () => {\n    if (isStandalonePage()) {\n      window.close()\n    } else {\n      dispatch({ type: 'CLOSE_PANEL' })\n    }\n  },\n  onSwitchSidebar: (side: 'left' | 'right') => {\n    message.send({ type: 'QS_SWITCH_SIDEBAR', payload: side })\n  },\n  onHeightChanged: (height: number) => {\n    dispatch({\n      type: 'UPDATE_PANEL_HEIGHT',\n      payload: {\n        area: 'menubar',\n        height: 30,\n        floatHeight: height\n      }\n    })\n  },\n  onDragAreaMouseDown: event => {\n    dispatch({\n      type: 'DRAG_START_COORD',\n      payload: {\n        x: event.clientX,\n        y: event.clientY\n      }\n    })\n  },\n  onDragAreaTouchStart: event => {\n    dispatch({\n      type: 'DRAG_START_COORD',\n      payload: {\n        x: event.changedTouches[0].clientX,\n        y: event.changedTouches[0].clientY\n      }\n    })\n  },\n  onSelectProfile: id => {\n    dispatch(async (dispatch, getState) => {\n      const state = getState()\n      const { showedDictAuth, dictAuth } = state.config\n\n      // no jumping on popup page which breaks user flow\n      if (!showedDictAuth && !isPopupPage()) {\n        await updateConfig({\n          ...state.config,\n          showedDictAuth: true\n        })\n\n        if (\n          objectKeys(dictAuth).every(id =>\n            objectKeys(dictAuth[id]).every(k => !dictAuth[id]?.[k])\n          )\n        ) {\n          message.send({\n            type: 'OPEN_URL',\n            payload: {\n              url: 'options.html?menuselected=DictAuths',\n              self: true\n            }\n          })\n          return\n        }\n      }\n\n      await updateActiveProfileID(id)\n      await timer(10)\n      dispatch({\n        type: 'SEARCH_START',\n        payload: {\n          word:\n            state.searchHistory[state.historyIndex]?.text === state.text\n              ? state.searchHistory[state.historyIndex]\n              : newWord({\n                  text: state.text,\n                  title: 'Saladict',\n                  favicon: 'https://saladict.crimx.com/favicon.ico'\n                })\n        }\n      })\n    })\n  }\n})\n\nexport const MenuBarContainer = connect(\n  mapStateToProps,\n  mapDispatchToProps\n)(MenuBar)\n\nexport default MenuBarContainer\n"
  },
  {
    "path": "src/content/components/MenuBar/MenuBar.scss",
    "content": "@import './MenubarBtns.scss';\n@import './SearchBox.scss';\n@import './Profiles.scss';\n\n.menuBar {\n  display: flex;\n  align-items: center;\n  position: relative;\n  height: 30px;\n  padding: 0 3px;\n  font-size: 14px;\n  background-color: var(--color-brand);\n}\n\n.menuBar-DragArea {\n  flex: 3;\n  align-self: stretch;\n  user-select: none;\n  // prevent scrolling\n  touch-action: none;\n  cursor: move;\n}\n"
  },
  {
    "path": "src/content/components/MenuBar/MenuBar.stories.tsx",
    "content": "import React from 'react'\nimport { storiesOf } from '@storybook/react'\nimport { jsxDecorator } from 'storybook-addon-jsx'\nimport { withPropsTable } from 'storybook-addon-react-docgen'\nimport { action } from '@storybook/addon-actions'\nimport {\n  withKnobs,\n  select,\n  text,\n  number,\n  boolean\n} from '@storybook/addon-knobs'\nimport {\n  withSaladictPanel,\n  withSideEffect,\n  mockRuntimeMessage,\n  withi18nNS\n} from '@/_helpers/storybook'\nimport { newWord } from '@/_helpers/record-manager'\nimport { MenuBar } from './MenuBar'\nimport { timer } from '@/_helpers/promise-more'\nimport { SuggestItem } from './Suggest'\n\nstoriesOf('Content Scripts|Dict Panel/Menubar', module)\n  .addDecorator(withPropsTable)\n  .addDecorator(jsxDecorator)\n  .addDecorator(withKnobs)\n  .addDecorator(\n    withSaladictPanel({\n      head: <style>{require('./MenuBar.scss').toString()}</style>,\n      backgroundColor: 'transparent'\n    })\n  )\n  .addDecorator(\n    withSideEffect(\n      mockRuntimeMessage(async message => {\n        if (message.type === 'GET_SUGGESTS') {\n          await timer(Math.random() * 1500)\n          return fakeSuggest(message.payload)\n        }\n      })\n    )\n  )\n  .addDecorator(withi18nNS(['common', 'content']))\n  .add('MenuBar', () => {\n    const histories = Array.from(Array(5)).map((_, i) =>\n      newWord({\n        date: Date.now(),\n        text: 'text' + (i + 1)\n      })\n    )\n\n    const profiles = Array.from(Array(5)).map((_, i) => ({\n      id: `profile${i + 1}`,\n      name: `Profile${i + 1}`\n    }))\n\n    const profilesOption = profiles.reduce((o, p) => {\n      o[p.name] = p.id\n      return o\n    }, {})\n\n    return (\n      <MenuBar\n        text={text('Search Text', 'text')}\n        updateText={action('Update Text')}\n        searchText={action('Search Text')}\n        isInNotebook={boolean('Is In Notebook', false)}\n        addToNoteBook={action('Add to Notebook')}\n        shouldFocus={true}\n        enableSuggest={boolean('Enable Suggest', true)}\n        isTrackHistory={boolean('Track History', true)}\n        histories={histories}\n        historyIndex={number('History Index', 0)}\n        switchHistory={action('Switch History')}\n        isPinned={boolean('Is Pinned', false)}\n        togglePin={action('Toggle Pin')}\n        isQSFocus={boolean('Is Quick Search Focus', false)}\n        toggleQSFocus={action('Toggle Quick Search Focus')}\n        onClose={action('Close Panel')}\n        onSwitchSidebar={action('Switch Sidebar')}\n        profiles={profiles}\n        activeProfileId={select(\n          'Active Profile',\n          profilesOption,\n          profiles[0].id\n        )}\n        onSelectProfile={action('Select Profile')}\n        onDragAreaMouseDown={action('Darg Area Mousedown')}\n        onDragAreaTouchStart={action('Darg Area Touchstart')}\n        onHeightChanged={action('Height Changed')}\n      />\n    )\n  })\n\nfunction fakeSuggest(text: string): SuggestItem[] {\n  return Array.from(Array(10)).map((v, i) => ({\n    explain: `单词 ${text} 的各种相近的建议#${i}`,\n    entry: `Word ${text}#${i}`\n  }))\n}\n"
  },
  {
    "path": "src/content/components/MenuBar/MenuBar.tsx",
    "content": "import React, { FC } from 'react'\nimport {\n  useObservableCallback,\n  useObservable,\n  useSubscription\n} from 'observable-hooks'\nimport { Observable, combineLatest } from 'rxjs'\nimport { startWith, debounceTime, map } from 'rxjs/operators'\nimport { Word } from '@/_helpers/record-manager'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { message } from '@/_helpers/browser-api'\nimport {\n  isStandalonePage,\n  isOptionsPage,\n  isPopupPage,\n  isQuickSearchPage\n} from '@/_helpers/saladict'\nimport {\n  HistoryBackBtn,\n  HistoryNextBtn,\n  FavBtn,\n  HistoryBtn,\n  NotebookBtn,\n  PinBtn,\n  CloseBtn,\n  SidebarBtn,\n  FocusBtn\n} from './MenubarBtns'\nimport { SearchBox, SearchBoxProps } from './SearchBox'\nimport { Profiles, ProfilesProps } from './Profiles'\n\nexport interface MenuBarProps {\n  text: string\n  updateText: SearchBoxProps['onInput']\n  searchText: (text: string) => any\n\n  /** is in Notebook */\n  isInNotebook: boolean\n  addToNoteBook: () => any\n\n  shouldFocus: boolean\n  enableSuggest: boolean\n\n  isTrackHistory: boolean\n  histories: Word[]\n  historyIndex: number\n  switchHistory: (direction: 'prev' | 'next') => void\n\n  onSelectProfile: (id: string) => void\n  profiles: ProfilesProps['profiles']\n  activeProfileId: ProfilesProps['activeProfileId']\n\n  isPinned: boolean\n  togglePin: () => any\n\n  isQSFocus: boolean\n  toggleQSFocus: () => any\n\n  onClose: () => any\n  onSwitchSidebar: (side: 'left' | 'right') => any\n\n  onHeightChanged: (height: number) => void\n\n  onDragAreaMouseDown: (e: React.MouseEvent<HTMLDivElement>) => any\n  onDragAreaTouchStart: (e: React.TouchEvent<HTMLDivElement>) => any\n}\n\nexport const MenuBar: FC<MenuBarProps> = props => {\n  const { t } = useTranslate(['content', 'common'])\n\n  const [updateProfileHeight, profileHeight$] = useObservableCallback<number>(\n    heightChangeTransform\n  )\n\n  const [updateSBHeight, searchBoxHeight$] = useObservableCallback<number>(\n    heightChangeTransform\n  )\n\n  // update panel min height\n  useSubscription(\n    useObservable(() =>\n      combineLatest(profileHeight$, searchBoxHeight$).pipe(\n        // a little delay for organic feeling\n        debounceTime(100),\n        map(heights => {\n          const max = Math.max(...heights)\n          return max > 0 ? max + 72 : 0\n        })\n      )\n    ),\n    props.onHeightChanged\n  )\n\n  return (\n    <header className=\"menuBar\">\n      <HistoryBackBtn\n        t={t}\n        disabled={props.historyIndex <= 0}\n        onClick={() => props.switchHistory('prev')}\n      />\n      <HistoryNextBtn\n        t={t}\n        disabled={props.historyIndex >= props.histories.length - 1}\n        onClick={() => props.switchHistory('next')}\n      />\n      <SearchBox\n        key=\"searchbox\"\n        t={t}\n        text={props.text}\n        shouldFocus={props.shouldFocus}\n        enableSuggest={props.enableSuggest}\n        onInput={props.updateText}\n        onSearch={props.searchText}\n        onHeightChanged={updateSBHeight}\n      />\n      {isStandalonePage() || (\n        <div\n          className=\"menuBar-DragArea\"\n          onMouseDown={props.onDragAreaMouseDown}\n          onTouchStart={props.onDragAreaTouchStart}\n        />\n      )}\n      <Profiles\n        t={t}\n        profiles={props.profiles}\n        activeProfileId={props.activeProfileId}\n        onSelectProfile={props.onSelectProfile}\n        onHeightChanged={updateProfileHeight}\n      />\n      <FavBtn\n        t={t}\n        isFav={props.isInNotebook}\n        onClick={props.addToNoteBook}\n        onMouseDown={e => {\n          if (e.button === 2) {\n            e.preventDefault()\n            e.stopPropagation()\n            e.currentTarget.blur()\n            message.send({\n              type: 'OPEN_URL',\n              payload: {\n                url: 'notebook.html',\n                self: true\n              }\n            })\n          }\n        }}\n      />\n      {props.isTrackHistory ? (\n        <HistoryBtn\n          t={t}\n          onClick={() =>\n            message.send({\n              type: 'OPEN_URL',\n              payload: { url: 'history.html', self: true }\n            })\n          }\n        />\n      ) : (\n        <NotebookBtn\n          t={t}\n          onClick={() =>\n            message.send({\n              type: 'OPEN_URL',\n              payload: { url: 'notebook.html', self: true }\n            })\n          }\n        />\n      )}\n\n      {isQuickSearchPage() ? (\n        <>\n          <FocusBtn\n            t={t}\n            isFocus={props.isQSFocus}\n            onClick={props.toggleQSFocus}\n            disabled={isOptionsPage() || isPopupPage()}\n          />\n          <SidebarBtn\n            t={t}\n            onMouseDown={e => {\n              e.preventDefault()\n              props.onSwitchSidebar(e.button === 0 ? 'left' : 'right')\n            }}\n          />\n        </>\n      ) : isPopupPage() ? null : (\n        <>\n          <PinBtn\n            t={t}\n            isPinned={props.isPinned}\n            onClick={props.togglePin}\n            disabled={isOptionsPage() || isPopupPage()}\n          />\n          <CloseBtn t={t} onClick={props.onClose} />\n        </>\n      )}\n    </header>\n  )\n}\n\nfunction heightChangeTransform(\n  height$: Observable<number>\n): Observable<number> {\n  return startWith<number>(0)(height$)\n}\n"
  },
  {
    "path": "src/content/components/MenuBar/MenubarBtns.scss",
    "content": ".menuBar-Btn {\n  width: 30px;\n  height: 30px;\n  overflow: hidden;\n  padding: 8px;\n  font-size: 0;\n  border: none;\n  background: transparent;\n  cursor: pointer;\n  user-select: none;\n\n  &:focus,\n  &:hover {\n    outline: none;\n    background: rgba(0, 0, 0, 0.05);\n  }\n\n  &:disabled {\n    svg {\n      fill: #9accc1;\n    }\n\n    .menuBar-Btn_Icon-fav {\n      fill: none;\n      stroke: #9accc1;\n    }\n\n    &:hover {\n      cursor: unset;\n      background: transparent;\n    }\n  }\n\n  @include isAnimate {\n    transition: background 0.2s;\n  }\n}\n\n// history back and next\n.menuBar-Btn-dir {\n  @extend .menuBar-Btn;\n  width: 22px;\n  padding: 4px;\n}\n\n.menuBar-Btn_Icon {\n  width: 100%;\n  height: 100%;\n  fill: #fff;\n}\n\n.menuBar-Btn_Icon-history {\n  @extend .menuBar-Btn_Icon;\n  fill-opacity: 0.8;\n}\n\n.menuBar-Btn_Icon-fav {\n  @extend .menuBar-Btn_Icon;\n  fill: none;\n  stroke: #fff;\n  stroke-width: 2;\n\n  &.isActive {\n    fill: #dd4b39;\n    stroke-width: 0;\n  }\n}\n\n.menuBar-Btn_Icon-pin {\n  transform-origin: center;\n\n  &.isActive {\n    transform: rotate(45deg);\n  }\n\n  @include isAnimate {\n    transition: transform 0.4s;\n  }\n}\n"
  },
  {
    "path": "src/content/components/MenuBar/MenubarBtns.stories.tsx",
    "content": "import React from 'react'\nimport i18next from 'i18next'\nimport { storiesOf } from '@storybook/react'\nimport { action } from '@storybook/addon-actions'\nimport { jsxDecorator } from 'storybook-addon-jsx'\nimport { withPropsTable } from 'storybook-addon-react-docgen'\nimport { withKnobs, boolean } from '@storybook/addon-knobs'\nimport { withi18nNS, withSaladictPanel } from '@/_helpers/storybook'\nimport {\n  HistoryBackBtn,\n  HistoryNextBtn,\n  SearchBtn,\n  OptionsBtn,\n  FavBtn,\n  HistoryBtn,\n  NotebookBtn,\n  PinBtn,\n  FocusBtn,\n  CloseBtn,\n  SidebarBtn\n} from './MenubarBtns'\nimport { useTranslate } from '@/_helpers/i18n'\n\nstoriesOf('Content Scripts|Dict Panel/Menubar', module)\n  .addParameters({\n    backgrounds: [\n      { name: 'Saladict', value: '#5caf9e', default: true },\n      { name: 'Black', value: '#000' }\n    ]\n  })\n  .addDecorator(withPropsTable)\n  .addDecorator(jsxDecorator)\n  .addDecorator(withKnobs)\n  .addDecorator(story => <BtnsParent story={story} />)\n  .addDecorator(\n    withSaladictPanel({\n      head: <style>{require('./MenubarBtns.scss').toString()}</style>,\n      backgroundColor: 'transparent'\n    })\n  )\n  .addDecorator(withi18nNS('content'))\n  .add('HistoryBackBtn', () => {\n    return (\n      <HistoryBackBtn\n        t={i18next.getFixedT(i18next.language, 'content')}\n        disabled={boolean('Disabled', false)}\n        onClick={action('onClick')}\n      />\n    )\n  })\n  .add('HistoryNextBtn', () => {\n    return (\n      <HistoryNextBtn\n        t={i18next.getFixedT(i18next.language, 'content')}\n        disabled={boolean('Disabled', false)}\n        onClick={action('onClick')}\n      />\n    )\n  })\n  .add('SearchBtn', () => {\n    return (\n      <SearchBtn\n        t={i18next.getFixedT(i18next.language, 'content')}\n        disabled={boolean('Disabled', false)}\n        onClick={action('onClick')}\n      />\n    )\n  })\n  .add('OptionsBtn', () => {\n    return (\n      <OptionsBtn\n        t={i18next.getFixedT(i18next.language, 'content')}\n        disabled={boolean('Disabled', false)}\n        onClick={action('onClick')}\n        onKeyDown={action('onKeyDown')}\n        onMouseOver={action('onMouseOver')}\n        onMouseOut={action('onMouseOut')}\n      />\n    )\n  })\n  .add('FavBtn', () => {\n    return (\n      <FavBtn\n        t={i18next.getFixedT(i18next.language, 'content')}\n        disabled={boolean('Disabled', false)}\n        isFav={boolean('Is in Notebook', true)}\n        onClick={action('onClick')}\n        onMouseDown={action('onMouseDown')}\n      />\n    )\n  })\n  .add('HistoryBtn', () => {\n    return (\n      <HistoryBtn\n        t={i18next.getFixedT(i18next.language, 'content')}\n        disabled={boolean('Disabled', false)}\n        onClick={action('onClick')}\n      />\n    )\n  })\n  .add('NotebookBtn', () => {\n    return (\n      <NotebookBtn\n        t={i18next.getFixedT(i18next.language, 'content')}\n        disabled={boolean('Disabled', false)}\n        onClick={action('onClick')}\n      />\n    )\n  })\n  .add('PinBtn', () => {\n    return (\n      <PinBtn\n        t={i18next.getFixedT(i18next.language, 'content')}\n        disabled={boolean('Disabled', false)}\n        isPinned={boolean('Is pinned', false)}\n        onClick={action('onClick')}\n      />\n    )\n  })\n  .add('FocusBtn', () => {\n    return (\n      <FocusBtn\n        t={i18next.getFixedT(i18next.language, 'content')}\n        disabled={boolean('Disabled', false)}\n        isFocus={boolean('Is pinned', false)}\n        onClick={action('onClick')}\n      />\n    )\n  })\n  .add('CloseBtn', () => {\n    return (\n      <CloseBtn\n        t={i18next.getFixedT(i18next.language, 'content')}\n        disabled={boolean('Disabled', false)}\n        onClick={action('onClick')}\n      />\n    )\n  })\n  .add('SidebarBtn', () => {\n    return (\n      <SidebarBtn\n        t={i18next.getFixedT(i18next.language, 'content')}\n        disabled={boolean('Disabled', false)}\n        onClick={action('onClick')}\n      />\n    )\n  })\n\nfunction BtnsParent(props: { story: any }) {\n  const { t } = useTranslate('content')\n  return <>{props.story(t)}</>\n}\n"
  },
  {
    "path": "src/content/components/MenuBar/MenubarBtns.tsx",
    "content": "import React, { FC } from 'react'\nimport { TFunction } from 'i18next'\n\nexport interface MenubarBtnProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  t: TFunction\n}\n\n/**\n * History Back Button\n */\nexport const HistoryBackBtn: FC<MenubarBtnProps> = props => {\n  const { t, ...restProps } = props\n  return (\n    <button\n      className=\"menuBar-Btn-dir\"\n      title={t('tip.historyBack')}\n      {...restProps}\n    >\n      <svg\n        className=\"menuBar-Btn_Icon\"\n        width=\"30\"\n        height=\"30\"\n        viewBox=\"0 0 32 32\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <path d=\"M 7.191 15.999 L 21.643 1.548 C 21.998 1.192 21.998 0.622 21.643 0.267 C 21.288 -0.089 20.718 -0.089 20.362 0.267 L 5.267 15.362 C 4.911 15.718 4.911 16.288 5.267 16.643 L 20.362 31.732 C 20.537 31.906 20.771 32 20.999 32 C 21.227 32 21.462 31.913 21.636 31.732 C 21.992 31.377 21.992 30.807 21.636 30.451 L 7.191 15.999 Z\" />\n      </svg>\n    </button>\n  )\n}\n\n/**\n * History Back Button\n */\nexport const HistoryNextBtn: FC<MenubarBtnProps> = props => {\n  const { t, ...restProps } = props\n  return (\n    <button\n      className=\"menuBar-Btn-dir\"\n      title={props.t('tip.historyNext')}\n      {...restProps}\n    >\n      <svg\n        className=\"menuBar-Btn_Icon\"\n        width=\"30\"\n        height=\"30\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 32 32\"\n      >\n        <path d=\"M 25.643 15.362 L 10.547 0.267 C 10.192 -0.089 9.622 -0.089 9.267 0.267 C 8.911 0.622 8.911 1.192 9.267 1.547 L 23.718 15.999 L 9.267 30.451 C 8.911 30.806 8.911 31.376 9.267 31.732 C 9.441 31.906 9.676 32 9.904 32 C 10.132 32 10.366 31.913 10.541 31.732 L 25.636 16.636 C 25.992 16.288 25.992 15.711 25.643 15.362 Z\" />\n      </svg>\n    </button>\n  )\n}\n\n/**\n * Search Button\n */\nexport const SearchBtn: FC<MenubarBtnProps> = props => {\n  const { t, ...restProps } = props\n  return (\n    <button className=\"menuBar-Btn\" title={t('tip.searchText')} {...restProps}>\n      <svg\n        className=\"menuBar-Btn_Icon\"\n        width=\"30\"\n        height=\"30\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 52.966 52.966\"\n      >\n        <path d=\"M51.704 51.273L36.844 35.82c3.79-3.8 6.14-9.04 6.14-14.82 0-11.58-9.42-21-21-21s-21 9.42-21 21 9.42 21 21 21c5.082 0 9.747-1.817 13.383-4.832l14.895 15.49c.196.206.458.308.72.308.25 0 .5-.093.694-.28.398-.382.41-1.015.028-1.413zM21.984 40c-10.478 0-19-8.523-19-19s8.522-19 19-19 19 8.523 19 19-8.525 19-19 19z\" />\n      </svg>\n    </button>\n  )\n}\n\n/**\n * Options Button\n */\nexport const OptionsBtn: FC<MenubarBtnProps> = props => {\n  const { t, ...restProps } = props\n  return (\n    <button className=\"menuBar-Btn\" title={t('tip.openOptions')} {...restProps}>\n      <svg\n        className=\"menuBar-Btn_Icon\"\n        width=\"30\"\n        height=\"30\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 612 612\"\n      >\n        <path d=\"M0 97.92v24.48h612V97.92H0zm0 220.32h612v-24.48H0v24.48zm0 195.84h612V489.6H0v24.48z\" />\n      </svg>\n    </button>\n  )\n}\n\nexport interface FavBtnProps extends MenubarBtnProps {\n  /** Current text is in Notebook */\n  isFav: boolean\n}\n\n/**\n * Add to Notebook\n */\nexport const FavBtn: FC<FavBtnProps> = props => {\n  const { t, isFav, ...restProps } = props\n  return (\n    <button\n      className=\"menuBar-Btn\"\n      title={t('tip.addToNotebook')}\n      {...restProps}\n    >\n      <svg\n        className={`menuBar-Btn_Icon-fav${isFav ? ' isActive' : ''}`}\n        width=\"30\"\n        height=\"30\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 32 32\"\n      >\n        <path d=\"M 23.363 2 C 20.105 2 17.3 4.65 16.001 7.42 C 14.701 4.65 11.896 2 8.637 2 C 4.145 2 0.5 5.646 0.5 10.138 C 0.5 19.278 9.719 21.673 16.001 30.708 C 21.939 21.729 31.5 18.986 31.5 10.138 C 31.5 5.646 27.855 2 23.363 2 Z\" />\n      </svg>\n    </button>\n  )\n}\n\n/**\n * Open History page\n */\nexport const HistoryBtn: FC<MenubarBtnProps> = props => {\n  const { t, ...restProps } = props\n  return (\n    <button className=\"menuBar-Btn\" title={t('tip.openHistory')} {...restProps}>\n      <svg\n        className=\"menuBar-Btn_Icon-history\"\n        width=\"30\"\n        height=\"30\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 64 64\"\n      >\n        <path d=\"M34.688 3.315c-15.887 0-28.812 12.924-28.81 28.73-.012.25-.157 4.434 1.034 8.94l-3.88-2.262c-.965-.56-2.193-.235-2.76.727-.557.96-.233 2.195.728 2.755l9.095 5.302c.02.01.038.013.056.022.1.05.2.09.31.12.07.02.14.05.21.07.09.02.176.02.265.03.06.003.124.022.186.022.036 0 .07-.01.105-.015.034 0 .063.007.097.004.05-.003.097-.024.146-.032.097-.017.19-.038.28-.068.08-.028.157-.06.23-.095.086-.04.165-.085.24-.137.074-.046.14-.096.206-.15.07-.06.135-.125.198-.195.06-.067.11-.135.16-.207.026-.04.062-.07.086-.11.017-.03.017-.067.032-.1.03-.053.07-.1.096-.16l3.62-8.96c.417-1.03-.08-2.205-1.112-2.622-1.033-.413-2.207.083-2.624 1.115l-1.86 4.6c-1.24-4.145-1.1-8.406-1.093-8.523C9.92 18.455 21.04 7.34 34.7 7.34c13.663 0 24.78 11.116 24.78 24.78S48.357 56.9 34.694 56.9c-1.114 0-2.016.902-2.016 2.015s.9 2.02 2.012 2.02c15.89 0 28.81-12.925 28.81-28.81 0-15.89-12.923-28.814-28.81-28.814z\" />\n        <path d=\"M33.916 36.002c.203.084.417.114.634.13.045.002.09.026.134.026.236 0 .465-.054.684-.134.06-.022.118-.054.177-.083.167-.08.32-.18.463-.3.03-.023.072-.033.103-.07L48.7 22.98c.788-.79.788-2.064 0-2.852-.787-.788-2.062-.788-2.85 0l-11.633 11.63-10.44-4.37c-1.032-.432-2.208.052-2.64 1.08-.43 1.027.056 2.208 1.08 2.638L33.907 36c.002 0 .006 0 .01.002z\" />\n      </svg>\n    </button>\n  )\n}\n\nexport const NotebookBtn: FC<MenubarBtnProps> = props => {\n  const { t, ...restProps } = props\n  return (\n    <button\n      className=\"menuBar-Btn\"\n      title={t('tip.openNotebook')}\n      {...restProps}\n    >\n      <svg\n        className=\"menuBar-Btn_Icon\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        width=\"30\"\n        height=\"30\"\n        viewBox=\"0 0 64 64\"\n      >\n        <path d=\"M 57.389 0.966 L 6.612 0.966 C 5.699 0.966 4.957 1.525 4.957 2.217 L 4.957 61.783 C 4.955 62.282 5.342 62.734 5.949 62.933 C 6.16 63.001 6.385 63.036 6.612 63.034 C 7.023 63.032 7.417 62.917 7.723 62.709 L 32.003 46.006 L 56.282 62.709 C 57.227 63.354 58.742 62.983 59.008 62.041 C 59.033 61.956 59.044 61.871 59.044 61.783 L 59.044 2.217 C 59.044 1.525 58.306 0.966 57.389 0.966 Z M 33.111 43.392 C 32.478 42.954 31.508 42.954 30.875 43.392 L 8.266 58.955 L 8.266 3.469 L 55.735 3.469 L 55.735 58.955 L 33.111 43.392 Z\" />\n        <path d=\"M 47.508 17.756 C 47.287 17.526 46.994 17.375 46.677 17.33 L 37.446 15.988 L 33.262 7.693 C 32.767 6.7 31.382 6.614 30.77 7.541 C 30.737 7.59 30.708 7.641 30.68 7.693 L 26.555 16.06 L 17.325 17.401 C 16.225 17.56 15.712 18.849 16.399 19.722 C 16.44 19.774 16.484 19.822 16.532 19.867 L 23.252 26.3 L 21.723 35.561 C 21.541 36.655 22.613 37.537 23.653 37.146 C 23.708 37.127 23.763 37.102 23.816 37.076 L 32.066 32.748 L 40.316 37.076 C 41.3 37.59 42.472 36.845 42.425 35.737 C 42.423 35.679 42.417 35.619 42.407 35.561 L 40.792 26.3 L 47.472 19.794 C 48.045 19.243 48.061 18.33 47.508 17.756 Z M 38.238 24.857 C 37.899 25.186 37.744 25.661 37.821 26.127 L 39.031 33.165 L 32.687 29.835 C 32.265 29.613 31.765 29.613 31.344 29.835 L 25.013 33.165 L 26.223 26.113 C 26.3 25.646 26.145 25.174 25.806 24.844 L 20.685 19.853 L 27.768 18.743 C 28.236 18.672 28.641 18.376 28.849 17.95 L 32.023 11.531 L 35.196 17.95 C 35.403 18.376 35.808 18.672 36.277 18.743 L 43.36 19.766 L 38.238 24.857 Z\" />\n      </svg>\n    </button>\n  )\n}\n\nexport interface PinBtnProps extends MenubarBtnProps {\n  /** Dict panel is pinned */\n  isPinned: boolean\n}\n\n/**\n * Pin the dict panel.\n *\n * - Normal in-page dict panel will stay visible.\n * - Standalone dict panel will stay on top of other windows.\n */\nexport const PinBtn: FC<PinBtnProps> = props => {\n  const { t, isPinned, ...restProps } = props\n  return (\n    <button className=\"menuBar-Btn\" title={t('tip.pinPanel')} {...restProps}>\n      <svg\n        className=\"menuBar-Btn_Icon\"\n        width=\"30\"\n        height=\"30\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 53.011 53.011\"\n      >\n        <path\n          className={`menuBar-Btn_Icon-pin${isPinned ? ' isActive' : ''}`}\n          d=\"M52.963 21.297c-.068-.33-.297-.603-.61-.727-8.573-3.416-16.172-.665-18.36.288L19.113 8.2C19.634 3.632 17.17.508 17.06.372c-.18-.22-.442-.356-.725-.372-.282-.006-.56.09-.76.292L.32 15.546c-.202.2-.308.48-.29.765.015.285.152.55.375.727 2.775 2.202 6.35 2.167 7.726 2.055l12.722 14.953c-.868 2.23-3.52 10.27-.307 18.337.124.313.397.54.727.61.067.013.135.02.202.02.263 0 .518-.104.707-.293l14.57-14.57 13.57 13.57c.196.194.452.292.708.292s.512-.098.707-.293c.39-.392.39-1.024 0-1.415l-13.57-13.57 14.527-14.528c.237-.238.34-.58.27-.91zm-17.65 15.458L21.89 50.18c-2.437-8.005.993-15.827 1.03-15.91.158-.352.1-.764-.15-1.058L9.31 17.39c-.19-.225-.473-.352-.764-.352-.05 0-.103.004-.154.013-.036.007-3.173.473-5.794-.954l13.5-13.5c.604 1.156 1.39 3.26.964 5.848-.058.346.07.697.338.924l15.785 13.43c.31.262.748.31 1.105.128.077-.04 7.378-3.695 15.87-1.017L35.313 36.754z\"\n        />\n      </svg>\n    </button>\n  )\n}\n\nexport interface FocusBtnProps extends MenubarBtnProps {\n  /** Dict panel focus */\n  isFocus: boolean\n}\n\n/**\n * Focus standalone panel when searching\n */\nexport const FocusBtn: FC<FocusBtnProps> = props => {\n  const { t, isFocus, ...restProps } = props\n  return (\n    <button\n      className=\"menuBar-Btn\"\n      title={t(`tip.${isFocus ? 'focusPanel' : 'unfocusPanel'}`)}\n      {...restProps}\n    >\n      <svg\n        className=\"menuBar-Btn_Icon\"\n        width=\"30\"\n        height=\"30\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 53 53\"\n      >\n        {isFocus ? (\n          <>\n            <path d=\"M 36.414 35 L 46 35 C 46.553 35 47 34.552 47 34 C 47 33.448 46.553 33 46 33 L 34 33 C 33.87 33 33.74 33.027 33.618 33.077 C 33.374 33.178 33.179 33.373 33.077 33.618 C 33.026 33.74 33 33.869 33 34 L 33 46 C 33 46.552 33.448 47 34 47 C 34.552 47 35 46.552 35 46 L 35 36.414 L 51.293 52.707 C 51.488 52.902 51.744 53 52 53 C 52.256 53 52.512 52.902 52.707 52.707 C 53.098 52.316 53.098 51.684 52.707 51.293 L 36.414 35 Z\" />\n            <path d=\"M 16.584 17.999 L 6.999 17.999 C 6.447 17.999 5.999 18.447 5.999 18.999 C 5.999 19.551 6.447 19.999 6.999 19.999 L 18.999 19.999 C 19.129 19.999 19.259 19.972 19.381 19.922 C 19.625 19.821 19.82 19.626 19.922 19.381 C 19.973 19.259 19.999 19.129 19.999 18.999 L 19.999 6.999 C 19.999 6.447 19.551 5.999 18.999 5.999 C 18.447 5.999 17.999 6.447 17.999 6.999 L 17.999 16.585 L 1.707 0.293 C 1.316 -0.098 0.684 -0.098 0.293 0.293 C -0.098 0.684 -0.098 1.316 0.293 1.707 L 16.584 17.999 Z\" />\n            <path d=\"M 19.382 33.077 C 19.26 33.027 19.13 33 19 33 L 7 33 C 6.448 33 6 33.448 6 34 C 6 34.552 6.448 35 7 35 L 16.586 35 L 0.293 51.293 C -0.098 51.684 -0.098 52.316 0.293 52.707 C 0.488 52.902 0.744 53 1 53 C 1.256 53 1.512 52.902 1.707 52.707 L 18 36.414 L 18 46 C 18 46.552 18.448 47 19 47 C 19.552 47 20 46.552 20 46 L 20 34 C 20 33.87 19.973 33.74 19.923 33.618 C 19.821 33.373 19.627 33.179 19.382 33.077 Z\" />\n            <path d=\"M 33.618 19.923 C 33.74 19.973 33.87 20 34 20 L 46 20 C 46.553 20 47 19.552 47 19 C 47 18.448 46.553 18 46 18 L 36.414 18 L 52.707 1.707 C 53.098 1.316 53.098 0.684 52.707 0.293 C 52.316 -0.098 51.684 -0.098 51.293 0.293 L 35 16.586 L 35 7 C 35 6.448 34.552 6 34 6 C 33.448 6 33 6.448 33 7 L 33 19 C 33 19.13 33.027 19.26 33.077 19.382 C 33.179 19.627 33.373 19.821 33.618 19.923 Z\" />\n            <path d=\"M 26.5 19 C 22.364 19 19 22.364 19 26.5 C 19 30.636 22.364 34 26.5 34 C 30.636 34 34 30.636 34 26.5 C 34 22.364 30.636 19 26.5 19 Z M 26.5 32 C 23.467 32 21 29.533 21 26.5 C 21 23.467 23.467 21 26.5 21 C 29.533 21 32 23.467 32 26.5 C 32 29.533 29.533 32 26.5 32 Z\" />\n          </>\n        ) : (\n          <>\n            <path d=\"M 34 20 C 33.744 20 33.488 19.902 33.293 19.707 C 32.902 19.316 32.902 18.684 33.293 18.293 L 49.586 2 L 40 2 C 39.448 2 39 1.552 39 1 C 39 0.448 39.448 0 40 0 L 51.978 0 C 52.241 -0.006 52.506 0.092 52.707 0.293 C 52.908 0.494 53.006 0.759 53 1.022 L 53 13 C 53 13.552 52.552 14 52 14 C 51.448 14 51 13.552 51 13 L 51 3.414 L 34.707 19.707 C 34.512 19.902 34.256 20 34 20 Z\" />\n            <path d=\"M 0.293 52.707 C 0.092 52.506 -0.006 52.241 0 51.978 L 0 40 C 0 39.448 0.448 39 1 39 C 1.552 39 2 39.448 2 40 L 2 49.586 L 18.293 33.293 C 18.684 32.902 19.316 32.902 19.707 33.293 C 20.098 33.684 20.098 34.316 19.707 34.707 L 3.414 51 L 13 51 C 13.552 51 14 51.448 14 52 C 14 52.552 13.552 53 13 53 L 1 53 C 0.879 53 0.764 52.979 0.658 52.94 C 0.602 52.919 0.548 52.894 0.498 52.865 C 0.424 52.822 0.355 52.769 0.293 52.707 Z\" />\n            <path d=\"M 33.293 34.707 C 32.902 34.316 32.902 33.684 33.293 33.293 C 33.684 32.902 34.316 32.902 34.707 33.293 L 51 49.586 L 51 40 C 51 39.448 51.448 39 52 39 C 52.552 39 53 39.448 53 40 L 53 51.978 C 53.006 52.241 52.908 52.506 52.707 52.707 C 52.645 52.769 52.576 52.822 52.502 52.865 C 52.452 52.894 52.398 52.919 52.342 52.94 C 52.236 52.979 52.121 53 52 53 L 40 53 C 39.448 53 39 52.552 39 52 C 39 51.448 39.448 51 40 51 L 49.586 51 Z\" />\n            <path d=\"M 18.999 19.999 C 18.743 19.999 18.487 19.901 18.292 19.706 L 2 3.414 L 2 13 C 2 13.552 1.552 14 1 14 C 0.448 14 0 13.552 0 13 L 0 1.022 C -0.006 0.759 0.092 0.494 0.293 0.293 C 0.494 0.092 0.759 -0.006 1.022 0 L 13 0 C 13.552 0 14 0.448 14 1 C 14 1.552 13.552 2 13 2 L 3.414 2 L 19.706 18.292 C 20.097 18.683 20.097 19.315 19.706 19.706 C 19.51 19.901 19.254 19.999 18.999 19.999 Z\" />\n          </>\n        )}\n      </svg>\n    </button>\n  )\n}\n\n/**\n * Close dict panel\n */\nexport const CloseBtn: FC<MenubarBtnProps> = props => {\n  const { t, ...restProps } = props\n  return (\n    <button className=\"menuBar-Btn\" title={t('tip.closePanel')} {...restProps}>\n      <svg\n        className=\"menuBar-Btn_Icon\"\n        width=\"30\"\n        height=\"30\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 31.112 31.112\"\n      >\n        <path d=\"M31.112 1.414L29.698 0 15.556 14.142 1.414 0 0 1.414l14.142 14.142L0 29.698l1.414 1.414L15.556 16.97l14.142 14.142 1.414-1.414L16.97 15.556\" />\n      </svg>\n    </button>\n  )\n}\n\nexport const SidebarBtn: FC<MenubarBtnProps> = props => {\n  const { t, ...restProps } = props\n  return (\n    <button className=\"menuBar-Btn\" title={t('tip.sidebar')} {...restProps}>\n      <svg\n        className=\"menuBar-Btn_Icon\"\n        width=\"30\"\n        height=\"30\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 30 30\"\n      >\n        <path d=\"M 29.318 0 L 0.682 0 C 0.305 0 0 0.305 0 0.682 L 0 29.318 C 0 29.695 0.305 30 0.682 30 L 29.318 30 C 29.695 30 30 29.695 30 29.318 L 30 0.682 C 30 0.305 29.695 0 29.318 0 Z M 9.545 28.636 L 1.364 28.636 L 1.364 1.364 L 9.545 1.364 L 9.545 28.636 Z M 28.636 28.636 L 10.909 28.636 L 10.909 1.364 L 28.636 1.364 L 28.636 28.636 Z\" />\n      </svg>\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/content/components/MenuBar/Profiles.scss",
    "content": "@import '@/components/HoverBox/HoverBox.scss';\n@import './MenubarBtns.scss';\n\n.menuBar-ProfileItem {\n  position: relative;\n  padding-left: 10px;\n  color: var(--color-font);\n\n  &.isActive::before {\n    content: '';\n    position: absolute;\n    top: 50%;\n    transform: translateY(-55%);\n    left: -5px;\n    width: 0;\n    height: 0;\n    border-left: 10px solid currentColor;\n    border-top: 5px solid transparent;\n    border-bottom: 5px solid transparent;\n  }\n}\n"
  },
  {
    "path": "src/content/components/MenuBar/Profiles.stories.tsx",
    "content": "import React from 'react'\nimport i18next from 'i18next'\nimport { storiesOf } from '@storybook/react'\nimport { jsxDecorator } from 'storybook-addon-jsx'\nimport { withPropsTable } from 'storybook-addon-react-docgen'\nimport { withKnobs, select } from '@storybook/addon-knobs'\nimport { withi18nNS, withSaladictPanel } from '@/_helpers/storybook'\nimport { Profiles } from './Profiles'\nimport { action } from '@storybook/addon-actions'\n\nstoriesOf('Content Scripts|Dict Panel/Menubar', module)\n  .addParameters({\n    backgrounds: [\n      { name: 'Saladict', value: '#5caf9e', default: true },\n      { name: 'Black', value: '#000' }\n    ]\n  })\n  .addDecorator(withPropsTable)\n  .addDecorator(jsxDecorator)\n  .addDecorator(withKnobs)\n  .addDecorator(\n    withSaladictPanel({\n      head: <style>{require('./Profiles.scss').toString()}</style>,\n      backgroundColor: 'transparent'\n    })\n  )\n  .addDecorator(stroy => <div style={{ marginLeft: 50 }}>{stroy()}</div>)\n  .addDecorator(withi18nNS('content'))\n  .add('Profiles', () => {\n    const profiles = Array.from(Array(5)).map((_, i) => ({\n      id: `profile${i + 1}`,\n      name: `Profile${i + 1}`\n    }))\n\n    const profilesOption = profiles.reduce((o, p) => {\n      o[p.name] = p.id\n      return o\n    }, {})\n    return (\n      <Profiles\n        t={i18next.getFixedT(i18next.language, ['content', 'common'])}\n        profiles={profiles}\n        activeProfileId={select(\n          'Active Profile',\n          profilesOption,\n          profiles[0].id\n        )}\n        onHeightChanged={action('Height Changed')}\n        onSelectProfile={action('Profile Selected')}\n      />\n    )\n  })\n"
  },
  {
    "path": "src/content/components/MenuBar/Profiles.tsx",
    "content": "import React, { FC } from 'react'\nimport { TFunction } from 'i18next'\nimport { getProfileName } from '@/_helpers/profile-manager'\nimport { message } from '@/_helpers/browser-api'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { isOptionsPage } from '@/_helpers/saladict'\nimport { HoverBox, HoverBoxItem } from '@/components/HoverBox'\nimport { OptionsBtn } from './MenubarBtns'\n\nexport interface ProfilesProps {\n  t: TFunction\n  profiles: Array<{ id: string; name: string }>\n  activeProfileId: string\n  onSelectProfile: (id: string) => void\n  onHeightChanged: (height: number) => void\n}\n\n/**\n * Pick and choose profiles\n */\nexport const Profiles: FC<ProfilesProps> = props => {\n  const { t } = useTranslate(['common'])\n\n  const listItems: HoverBoxItem[] = props.profiles.map(p => {\n    return {\n      key: p.id,\n      value: p.id,\n      label: (\n        <span\n          className={`menuBar-ProfileItem${\n            p.id === props.activeProfileId ? ' isActive' : ''\n          }`}\n        >\n          {getProfileName(p.name, t)}\n        </span>\n      )\n    }\n  })\n\n  return (\n    <HoverBox\n      Button={ProfilesBtn}\n      items={listItems}\n      onBtnClick={() => {\n        message.send({\n          type: 'OPEN_URL',\n          payload: { url: 'options.html', self: true }\n        })\n        return false\n      }}\n      onSelect={props.onSelectProfile}\n      onHeightChanged={props.onHeightChanged}\n    />\n  )\n}\n\nfunction ProfilesBtn(props: React.ComponentProps<'button'>) {\n  const { t } = useTranslate(['content'])\n  return <OptionsBtn {...props} t={t} disabled={isOptionsPage()} />\n}\n"
  },
  {
    "path": "src/content/components/MenuBar/SearchBox.scss",
    "content": "@import './Suggest.scss';\n@import './MenubarBtns.scss';\n\n.menuBar-SearchBox_Wrap {\n  flex: 2;\n  position: relative;\n\n  &.isExpand {\n    flex: 7;\n  }\n\n  @include isAnimate {\n    transition: flex 0.6s;\n  }\n}\n\n.menuBar-SearchBox {\n  width: 100%;\n  box-sizing: border-box;\n  padding: 0 5px;\n  border: 0 none;\n  outline: 0 none;\n  color: #fff;\n  background-color: rgba(225, 225, 225, 0.1);\n}\n\n.menuBar-SearchBox_Suggests {\n  position: absolute;\n  left: 0;\n  top: 30px;\n  z-index: 1000;\n}\n\n.csst-menuBar-SearchBox_Suggests {\n  @include isAnimate(-enter) {\n    opacity: 0;\n    transition: opacity 0.4s;\n  }\n\n  @include isAnimate(-enter-active, -exit) {\n    opacity: 1;\n    transition: opacity 0.4s;\n  }\n\n  @include isAnimate(-exit-active) {\n    opacity: 0;\n    transition: opacity 0.4s;\n  }\n}\n"
  },
  {
    "path": "src/content/components/MenuBar/SearchBox.stories.tsx",
    "content": "import React, { useState } from 'react'\nimport i18next from 'i18next'\nimport { storiesOf } from '@storybook/react'\nimport { action } from '@storybook/addon-actions'\nimport { jsxDecorator } from 'storybook-addon-jsx'\nimport { withPropsTable } from 'storybook-addon-react-docgen'\nimport { withKnobs, boolean } from '@storybook/addon-knobs'\nimport {\n  withi18nNS,\n  withSideEffect,\n  withSaladictPanel,\n  mockRuntimeMessage\n} from '@/_helpers/storybook'\nimport { SuggestItem } from './Suggest'\nimport { SearchBox } from './SearchBox'\nimport { timer } from '@/_helpers/promise-more'\nimport { useTranslate } from '@/_helpers/i18n'\n\nstoriesOf('Content Scripts|Dict Panel/Menubar', module)\n  .addParameters({\n    backgrounds: [\n      { name: 'Saladict', value: '#5caf9e', default: true },\n      { name: 'Black', value: '#000' }\n    ]\n  })\n  .addDecorator(withPropsTable)\n  .addDecorator(jsxDecorator)\n  .addDecorator(withKnobs)\n  .addDecorator(story => <BtnsParent story={story} />)\n  .addDecorator(\n    withSaladictPanel({\n      head: <style>{require('./SearchBox.scss').toString()}</style>,\n      backgroundColor: 'transparent'\n    })\n  )\n  .addDecorator(withi18nNS('content'))\n  .addDecorator(\n    withSideEffect(\n      mockRuntimeMessage(async message => {\n        if (message.type === 'GET_SUGGESTS') {\n          await timer(Math.random() * 1500)\n          return fakeSuggest(message.payload)\n        }\n      })\n    )\n  )\n  // @ts-ignore\n  .addDecorator(Story => <Story />)\n  .add('SearchBox', () => {\n    const [text, setText] = useState('text')\n    return (\n      <SearchBox\n        t={i18next.getFixedT(i18next.language, 'content')}\n        text={text}\n        shouldFocus={boolean('Focus On Mount', true)}\n        enableSuggest={boolean('Enable Suggest', true)}\n        onInput={text => {\n          setText(text)\n          action('Input')(text)\n        }}\n        onSearch={text => {\n          setText(text)\n          action('Search')(text)\n        }}\n        onHeightChanged={action('Height Changed')}\n      />\n    )\n  })\n\nfunction fakeSuggest(text: string): SuggestItem[] {\n  return Array.from(Array(10)).map((v, i) => ({\n    explain: `单词 ${text} 的各种相近的建议#${i}`,\n    entry: `Word ${text}#${i}`\n  }))\n}\n\nfunction BtnsParent(props: { story: any }) {\n  const { t } = useTranslate('content')\n  return <>{props.story(t)}</>\n}\n"
  },
  {
    "path": "src/content/components/MenuBar/SearchBox.tsx",
    "content": "import React, { FC, useRef, useLayoutEffect, useMemo, useEffect } from 'react'\nimport CSSTransition from 'react-transition-group/CSSTransition'\nimport { TFunction } from 'i18next'\nimport {\n  useObservableCallback,\n  useObservableState,\n  useObservable,\n  identity\n} from 'observable-hooks'\nimport { merge, combineLatest } from 'rxjs'\nimport { filter, map, distinctUntilChanged, mapTo, delay } from 'rxjs/operators'\nimport { focusBlur } from '@/_helpers/observables'\nimport { message } from '@/_helpers/browser-api'\nimport { Suggest } from './Suggest'\nimport { SearchBtn } from './MenubarBtns'\n\nexport interface SearchBoxProps {\n  t: TFunction\n  /** Search box text */\n  text: string\n  /** Focus search box */\n  shouldFocus: boolean\n  /** Show suggest panel when typing */\n  enableSuggest: boolean\n  onInput: (text: string) => any\n  /** Start searching */\n  onSearch: (text: string) => any\n\n  onHeightChanged: (height: number) => void\n}\n\n/**\n * Search box\n */\nexport const SearchBox: FC<SearchBoxProps> = props => {\n  // Textarea also shares the text so only replace here\n  const text = useMemo(() => props.text.replace(/\\s+/g, ' '), [props.text])\n\n  const [onSearchBoxFocusBlur, searchBoxFocusBlur$] = useObservableCallback(\n    focusBlur\n  )\n\n  const [onSuggestFocusBlur, suggestFocusBlur$] = useObservableCallback(\n    focusBlur\n  )\n\n  const [onShowSuggest, onShowSuggest$] = useObservableCallback<boolean>(\n    identity\n  )\n\n  const isShowSuggest = useObservableState(\n    useObservable(\n      inputs$ =>\n        combineLatest([\n          inputs$,\n          merge(\n            // only show suggest when start typing\n            searchBoxFocusBlur$.pipe(filter(isFocus => !isFocus)),\n            suggestFocusBlur$,\n            onShowSuggest$.pipe(delay(0)), // Prevent input method conflict on first input #1149\n            message.createStream('SEARCH_TEXT_BOX').pipe(mapTo(false))\n          )\n        ]).pipe(\n          map(([[enableSuggest, text], shouldShowSuggest]) =>\n            Boolean(enableSuggest && text && shouldShowSuggest)\n          ),\n          distinctUntilChanged()\n        ),\n      [props.enableSuggest, props.text]\n    ),\n    false\n  )\n\n  const isExpand = useObservableState(searchBoxFocusBlur$)\n\n  const hasTypedRef = useRef(false)\n\n  const inputRef = useRef<HTMLInputElement>(null)\n  const suggestRef = useRef<HTMLDivElement>(null)\n\n  const focusInput = useRef(() => {\n    if (inputRef.current) {\n      inputRef.current.focus()\n      // Although search box is selected on focus event\n      // this is needed as the box may be focused initially.\n      inputRef.current.select()\n    }\n  }).current\n\n  const searchText = (text: unknown) => {\n    hasTypedRef.current = false\n    onShowSuggest(false)\n    props.onSearch(typeof text === 'string' ? text : props.text)\n    focusInput()\n  }\n\n  const checkFocus = () => {\n    if (props.shouldFocus && !hasTypedRef.current && !isShowSuggest) {\n      focusInput()\n    }\n  }\n\n  // useEffect is not quick enough on popup panel.\n  useLayoutEffect(checkFocus, [])\n  // On in-page panel, layout effect only works on the first time.\n  useEffect(checkFocus, [props.text])\n\n  return (\n    <>\n      <div className={`menuBar-SearchBox_Wrap${isExpand ? ' isExpand' : ''}`}>\n        <input\n          type=\"text\"\n          className=\"menuBar-SearchBox\"\n          key=\"search-box\"\n          ref={inputRef}\n          onChange={e => {\n            props.onInput(e.currentTarget.value)\n            onShowSuggest(true)\n          }}\n          onKeyDown={e => {\n            // prevent page hot keys\n            e.nativeEvent.stopPropagation()\n\n            hasTypedRef.current = true\n            if (e.key === 'ArrowDown') {\n              const firstSuggestBtn =\n                suggestRef.current && suggestRef.current.querySelector('button')\n              if (firstSuggestBtn) {\n                firstSuggestBtn.focus()\n              } else {\n                onShowSuggest(true)\n              }\n              e.preventDefault()\n              e.stopPropagation()\n            } else if (e.key === 'Enter') {\n              searchText(props.text)\n            }\n          }}\n          onFocus={event => {\n            event.currentTarget.select()\n            onSearchBoxFocusBlur(event)\n          }}\n          onBlur={onSearchBoxFocusBlur}\n          value={text}\n        />\n\n        <CSSTransition\n          classNames=\"csst-menuBar-SearchBox_Suggests\"\n          in={isShowSuggest}\n          timeout={100}\n          mountOnEnter={true}\n          unmountOnExit={true}\n          onExited={() => props.onHeightChanged(0)}\n        >\n          {() => (\n            <div className=\"menuBar-SearchBox_Suggests\">\n              <Suggest\n                ref={suggestRef}\n                text={text}\n                onSelect={searchText}\n                onFocus={onSuggestFocusBlur}\n                onBlur={onSuggestFocusBlur}\n                onArrowUpFirst={focusInput}\n                onClose={focusInput}\n                onHeightChanged={props.onHeightChanged}\n              />\n            </div>\n          )}\n        </CSSTransition>\n      </div>\n      <SearchBtn t={props.t} onClick={searchText} />\n    </>\n  )\n}\n"
  },
  {
    "path": "src/content/components/MenuBar/Suggest.scss",
    "content": "@import '@/components/FloatBox/FloatBox.scss';\n\n.menuBar-SuggestsEntry {\n  margin-right: 1.5em;\n  color: #f9690e;\n}\n\n.menuBar-SuggestsExplain {\n  color: var(--color-font);\n}\n"
  },
  {
    "path": "src/content/components/MenuBar/Suggest.stories.tsx",
    "content": "import React from 'react'\nimport { storiesOf } from '@storybook/react'\nimport { action } from '@storybook/addon-actions'\nimport { jsxDecorator } from 'storybook-addon-jsx'\nimport { withPropsTable } from 'storybook-addon-react-docgen'\nimport { withKnobs, text } from '@storybook/addon-knobs'\nimport {\n  withi18nNS,\n  withSideEffect,\n  withSaladictPanel,\n  mockRuntimeMessage\n} from '@/_helpers/storybook'\nimport { Suggest, SuggestItem } from './Suggest'\nimport { timer } from '@/_helpers/promise-more'\n\nstoriesOf('Content Scripts|Dict Panel/Menubar', module)\n  .addParameters({\n    backgrounds: [\n      { name: 'Saladict', value: '#5caf9e', default: true },\n      { name: 'Dark', value: '#222' },\n      { name: 'Black', value: '#000' }\n    ]\n  })\n  .addDecorator(withPropsTable)\n  .addDecorator(jsxDecorator)\n  .addDecorator(withKnobs)\n  .addDecorator(\n    withSaladictPanel({\n      head: <style>{require('./Suggest.scss').toString()}</style>,\n      height: 'auto',\n      backgroundColor: 'transparent'\n    })\n  )\n  .addDecorator(withi18nNS('content'))\n  .addDecorator(\n    withSideEffect(\n      mockRuntimeMessage(async message => {\n        if (message.type === 'GET_SUGGESTS') {\n          await timer(Math.random() * 1500)\n          return fakeSuggest(message.payload)\n        }\n      })\n    )\n  )\n  .add('Suggest', () => {\n    return (\n      <Suggest\n        text={text('Search text', 'text')}\n        onSelect={action('Select')}\n        onFocus={action('Focus')}\n        onBlur={action('Blur')}\n      />\n    )\n  })\n\nfunction fakeSuggest(text: string): SuggestItem[] {\n  return Array.from(Array(10)).map((v, i) => ({\n    explain: `单词 ${text} 的各种相近的建议#${i}`,\n    entry: `Word ${text}#${i}`\n  }))\n}\n"
  },
  {
    "path": "src/content/components/MenuBar/Suggest.tsx",
    "content": "import React, { FC } from 'react'\nimport { useObservable, useObservableState } from 'observable-hooks'\nimport { from } from 'rxjs'\nimport {\n  map,\n  filter,\n  distinctUntilChanged,\n  switchMap,\n  debounceTime,\n  startWith\n} from 'rxjs/operators'\nimport { message } from '@/_helpers/browser-api'\nimport { FloatBox, FloatBoxProps } from '@/components/FloatBox'\n\nexport interface SuggestItem {\n  explain: string\n  entry: string\n}\n\nexport type SuggestProps = {\n  /** Search box text */\n  text: string\n} & Pick<\n  FloatBoxProps,\n  | 'ref'\n  | 'onFocus'\n  | 'onBlur'\n  | 'onSelect'\n  | 'onArrowUpFirst'\n  | 'onClose'\n  | 'onHeightChanged'\n>\n\n/**\n * Suggest panel offering similar words.\n */\nexport const Suggest: FC<SuggestProps> = React.forwardRef(\n  (props: SuggestProps, ref: React.Ref<HTMLDivElement>) => {\n    return useObservableState(\n      useObservable(\n        inputs$ =>\n          inputs$.pipe(\n            map(([text]) => text),\n            filter<string>(Boolean),\n            distinctUntilChanged(),\n            debounceTime(750),\n            switchMap(text =>\n              from(\n                message\n                  .send<'GET_SUGGESTS'>({\n                    type: 'GET_SUGGESTS',\n                    payload: text\n                  })\n                  .catch(() => [] as readonly SuggestItem[])\n              ).pipe(\n                map(suggests => (\n                  <FloatBox\n                    ref={ref}\n                    list={suggests.map(s => ({\n                      key: s.entry,\n                      value: s.entry,\n                      label: (\n                        <>\n                          <span className=\"menuBar-SuggestsEntry\">\n                            {s.entry}\n                          </span>\n                          <span className=\"menuBar-SuggestsExplain\">\n                            {s.explain}\n                          </span>\n                        </>\n                      )\n                    }))}\n                    {...props}\n                  />\n                )),\n                startWith(<FloatBox {...props} />)\n              )\n            )\n          ),\n        [props.text]\n      ),\n      () => <FloatBox {...props} />\n    )\n  }\n)\n"
  },
  {
    "path": "src/content/components/MtaBox/MtaBox.container.tsx",
    "content": "import { connect } from 'react-redux'\nimport {\n  ExtractDispatchers,\n  MapStateToProps,\n  MapDispatchToProps\n} from 'react-retux'\nimport { StoreState, StoreAction } from '@/content/redux/modules'\nimport { newWord } from '@/_helpers/record-manager'\nimport { isQuickSearchPage, isPopupPage } from '@/_helpers/saladict'\nimport { MtaBox, MtaBoxProps } from './MtaBox'\n\ntype Dispatchers = ExtractDispatchers<\n  MtaBoxProps,\n  'searchText' | 'onInput' | 'onDrawerToggle' | 'onHeightChanged'\n>\n\nconst mapStateToProps: MapStateToProps<\n  StoreState,\n  MtaBoxProps,\n  Dispatchers\n> = state => ({\n  expand: state.isExpandMtaBox,\n  text: state.text,\n  shouldFocus:\n    !state.activeProfile.mtaAutoUnfold ||\n    state.activeProfile.mtaAutoUnfold !== 'hide' ||\n    ((state.isQSPanel || isQuickSearchPage()) && state.config.qsFocus) ||\n    isPopupPage()\n})\n\nconst mapDispatchToProps: MapDispatchToProps<\n  StoreAction,\n  MtaBoxProps,\n  Dispatchers\n> = dispatch => ({\n  searchText: text => {\n    dispatch({ type: 'SEARCH_START', payload: { word: newWord({ text }) } })\n  },\n  onInput: text => {\n    dispatch({ type: 'UPDATE_TEXT', payload: text })\n  },\n  onDrawerToggle: () => {\n    dispatch({ type: 'TOGGLE_MTA_BOX' })\n  },\n  onHeightChanged: height => {\n    dispatch({\n      type: 'UPDATE_PANEL_HEIGHT',\n      payload: { area: 'mtabox', height }\n    })\n  }\n})\n\nexport const MtaBoxContainer = connect(\n  mapStateToProps,\n  mapDispatchToProps\n)(MtaBox)\n\nexport default MtaBoxContainer\n"
  },
  {
    "path": "src/content/components/MtaBox/MtaBox.scss",
    "content": ".mtaBox-TextArea-Wrap {\n  max-height: var(--panel-max-height);\n  position: relative;\n  overflow: hidden;\n  height: 0; // init height transition\n\n  @include isAnimate {\n    transition: height 0.4s;\n\n    &.isTyping {\n      transition: height 0s;\n    }\n  }\n}\n\n.mtaBox-TextArea {\n  display: block;\n  width: 100%;\n  max-height: var(--panel-max-height);\n  box-sizing: border-box;\n  padding: 5px 5px 10px;\n  border: none;\n  color: var(--color-font);\n  font-size: var(--panel-font-size);\n  background: transparent;\n}\n\n.mtaBox-TextArea-Wrap-appear,\n.mtaBox-TextArea-Wrap-appear-active,\n.mtaBox-TextArea-Wrap-enter,\n.mtaBox-TextArea-Wrap-enter-active,\n.mtaBox-TextArea-Wrap-exit,\n.mtaBox-TextArea-Wrap-exit-active {\n  .mtaBox-TextArea {\n    position: absolute;\n    bottom: 0;\n  }\n}\n\n.mtaBox-DrawerBtn {\n  display: block;\n  width: 100%;\n  height: 12px;\n  overflow: hidden;\n  padding: 0; // remove padding in firefox\n  border: none;\n  border-top: 1px solid var(--color-divider);\n  border-bottom: 1px dashed var(--color-divider);\n  font-size: 0;\n  background-color: var(--color-background);\n  cursor: pointer;\n\n  &:focus {\n    outline: none;\n    border: 1px solid var(--color-font) !important;\n  }\n}\n\n.mtaBox-DrawerBtn_Arrow {\n  fill: var(--color-font);\n\n  &.isExpand {\n    transform: rotate(180deg);\n  }\n}\n"
  },
  {
    "path": "src/content/components/MtaBox/MtaBox.stories.tsx",
    "content": "import React, { useState } from 'react'\nimport { storiesOf } from '@storybook/react'\nimport { action } from '@storybook/addon-actions'\nimport { jsxDecorator } from 'storybook-addon-jsx'\nimport { withPropsTable } from 'storybook-addon-react-docgen'\nimport { withKnobs } from '@storybook/addon-knobs'\nimport { withSaladictPanel } from '@/_helpers/storybook'\nimport faker from 'faker'\nimport { MtaBox } from './MtaBox'\n\nstoriesOf('Content Scripts|Dict Panel', module)\n  .addParameters({\n    backgrounds: [\n      { name: 'Saladict', value: '#5caf9e', default: true },\n      { name: 'Black', value: '#000' }\n    ]\n  })\n  .addDecorator(withPropsTable)\n  .addDecorator(jsxDecorator)\n  .addDecorator(withKnobs)\n  .addDecorator(\n    withSaladictPanel({\n      head: <style>{require('./MtaBox.scss').toString()}</style>\n    })\n  )\n  // @ts-ignore\n  .addDecorator(Story => <Story />)\n  .add('MtaBox', () => {\n    const [expand, setExpand] = useState(true)\n    const [text, setText] = useState(() => faker.lorem.paragraph(2))\n\n    return (\n      <MtaBox\n        expand={expand}\n        text={text}\n        shouldFocus={true}\n        searchText={action('Search Text')}\n        onInput={text => {\n          action('Input')(text)\n          setText(text)\n        }}\n        onDrawerToggle={() => {\n          action('Drawer Toggle')()\n          setExpand(!expand)\n        }}\n        onHeightChanged={action('Height Changed')}\n      />\n    )\n  })\n"
  },
  {
    "path": "src/content/components/MtaBox/MtaBox.tsx",
    "content": "import React, { FC, useRef, useState, useEffect } from 'react'\nimport classNames from 'classnames'\nimport CSSTransition from 'react-transition-group/CSSTransition'\nimport AutosizeTextarea from 'react-textarea-autosize'\nimport { useObservableState } from 'observable-hooks'\nimport { switchMap, mapTo, startWith } from 'rxjs/operators'\nimport { timer, Observable } from 'rxjs'\n\nexport interface MtaBoxProps {\n  expand: boolean\n  text: string\n  shouldFocus: boolean\n  searchText: (text: string) => any\n  onInput: (text: string) => void\n  /** Expand or Shrink */\n  onDrawerToggle: () => void\n  onHeightChanged: (height: number) => void\n}\n\n/**\n * Multiline Textarea Drawer. With animation on Expanding and Shrinking.\n */\nexport const MtaBox: FC<MtaBoxProps> = props => {\n  const isTypedRef = useRef(false)\n  const textareaRef = useRef<HTMLTextAreaElement>(null)\n  const [height, setHeight] = useState(0)\n\n  const [isTyping, onKeyDown] = useObservableState(transformTyping, false)\n\n  const firstExpandRef = useRef(true)\n  useEffect(() => {\n    if (props.expand) {\n      if (!firstExpandRef.current || props.shouldFocus) {\n        if (textareaRef.current) {\n          textareaRef.current.focus()\n          textareaRef.current.select()\n        }\n      }\n      firstExpandRef.current = false\n    }\n  }, [props.expand])\n\n  useEffect(() => {\n    // could be from clipboard with delay\n    if (\n      props.shouldFocus &&\n      !isTypedRef.current &&\n      props.expand &&\n      textareaRef.current\n    ) {\n      textareaRef.current.focus()\n      textareaRef.current.select()\n    }\n  }, [props.text])\n\n  useEffect(() => {\n    props.onHeightChanged((props.expand ? height : 0) + 12)\n  }, [height, props.expand])\n\n  return (\n    <div>\n      <div\n        className={classNames('mtaBox-TextArea-Wrap', { isTyping })}\n        style={{ height: props.expand ? height : 0 }}\n      >\n        <CSSTransition\n          in={props.expand}\n          timeout={400}\n          classNames=\"mtaBox-TextArea-Wrap\"\n          appear\n          mountOnEnter\n          unmountOnExit\n        >\n          {() => (\n            <AutosizeTextarea\n              autoFocus\n              inputRef={textareaRef}\n              className=\"mtaBox-TextArea\"\n              value={props.text}\n              onChange={e => {\n                isTypedRef.current = true\n                props.onInput(e.currentTarget.value)\n              }}\n              onKeyDown={e => {\n                // prevent page shortkeys\n                e.nativeEvent.stopPropagation()\n\n                if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {\n                  props.searchText(props.text)\n                }\n\n                onKeyDown(e)\n              }}\n              minRows={2}\n              onHeightChange={height => setHeight(height)}\n            />\n          )}\n        </CSSTransition>\n      </div>\n      <button className=\"mtaBox-DrawerBtn\" onClick={props.onDrawerToggle}>\n        <svg\n          width=\"10\"\n          height=\"10\"\n          viewBox=\"0 0 59.414 59.414\"\n          className={classNames('mtaBox-DrawerBtn_Arrow', {\n            isExpand: props.expand\n          })}\n        >\n          <path d=\"M58 14.146L29.707 42.44 1.414 14.145 0 15.56 29.707 45.27 59.414 15.56\" />\n        </svg>\n      </button>\n    </div>\n  )\n}\n\nexport default MtaBox\n\nfunction transformTyping(event$: Observable<React.KeyboardEvent>) {\n  return event$.pipe(\n    switchMap(event => {\n      event.stopPropagation()\n      return timer(1000).pipe(mapTo(false), startWith(true))\n    })\n  )\n}\n"
  },
  {
    "path": "src/content/components/SaladBowl/SaladBowl.container.tsx",
    "content": "import { connect } from 'react-redux'\nimport {\n  ExtractDispatchers,\n  MapStateToProps,\n  MapDispatchToProps\n} from 'react-retux'\nimport { StoreState, StoreAction } from '@/content/redux/modules'\nimport { SaladBowlPortal, SaladBowlPortalProps } from './SaladBowl.portal'\n\ntype Dispatchers = ExtractDispatchers<SaladBowlPortalProps, 'onActive'>\n\nconst mapStateToProps: MapStateToProps<\n  StoreState,\n  SaladBowlPortalProps,\n  Dispatchers\n> = state => ({\n  show: state.isShowBowl,\n  panelCSS: state.config.panelCSS,\n  x: state.bowlCoord.x,\n  y: state.bowlCoord.y,\n  withAnimation: state.config.animation,\n  enableHover: state.config.bowlHover\n})\n\nconst mapDispatchToProps: MapDispatchToProps<\n  StoreAction,\n  SaladBowlPortalProps,\n  Dispatchers\n> = dispatch => ({\n  onActive: () => {\n    dispatch({ type: 'BOWL_ACTIVATED' })\n  }\n})\n\nexport const SaladBowlContainer = connect(\n  mapStateToProps,\n  mapDispatchToProps\n)(SaladBowlPortal)\n\nexport default SaladBowlContainer\n"
  },
  {
    "path": "src/content/components/SaladBowl/SaladBowl.portal.tsx",
    "content": "import React, { FC, useState } from 'react'\nimport { useRefFn } from 'observable-hooks'\nimport ShadowPortal from '@/components/ShadowPortal'\nimport { SaladBowl, SaladBowlProps } from './SaladBowl'\n\nconst animationTimeout = { enter: 1000, exit: 100, appear: 1000 }\n\nexport interface SaladBowlPortalProps extends Omit<SaladBowlProps, 'onHover'> {\n  show: boolean\n  panelCSS: string\n  withAnimation: boolean\n}\n\n/**\n * React portal wrapped SaladBowlShadow.\n * Detach from DOM when not visible.\n */\nexport const SaladBowlPortal: FC<SaladBowlPortalProps> = props => {\n  const { show, panelCSS, withAnimation, ...restProps } = props\n  const [isHover, setHover] = useState(false)\n  const bowlStyles = useRefFn(() => (\n    <style>{require('./SaladBowl.shadow.scss').toString()}</style>\n  )).current\n\n  return (\n    <ShadowPortal\n      id=\"saladict-saladbowl-root\"\n      head={bowlStyles}\n      classNames=\"saladbowl\"\n      innerRootClassName={withAnimation ? 'isAnimate' : ''}\n      panelCSS={panelCSS}\n      in={show || isHover}\n      timeout={withAnimation ? animationTimeout : 0}\n    >\n      {() => <SaladBowl {...restProps} onHover={setHover} />}\n    </ShadowPortal>\n  )\n}\n"
  },
  {
    "path": "src/content/components/SaladBowl/SaladBowl.shadow.scss",
    "content": "$bowl-width: 30px;\n\n.saladbowl {\n  position: fixed;\n  z-index: $global-zindex-bowl;\n  top: 0;\n  left: 0;\n  width: $bowl-width;\n  height: $bowl-width;\n  user-select: none;\n  cursor: pointer;\n\n  @include isAnimate {\n    will-change: transform;\n    transition: transform 0.3s ease-out;\n\n    &.enableHover:hover {\n      .saladbowl-leaf {\n        animation: saladbowl-leaf-shake 0.7s infinite linear;\n      }\n      .saladbowl-orange {\n        transform-origin: 301.8px 187.4px;\n        animation: saladbowl-orange-spin 0.7s infinite linear;\n      }\n      .saladbowl-tomato {\n        transform-origin: 297.8px 126.4px;\n        animation: saladbowl-tomato-shake 0.7s infinite linear;\n      }\n    }\n  }\n\n  &,\n  & > svg {\n    // #947 fix Super Dark Mode\n    background-color: transparent !important;\n  }\n}\n\n.saladbowl {\n  @include isAnimate(-enter-active) {\n    & > svg {\n      animation: saladbowl-jelly 1000ms linear;\n    }\n  }\n\n  @include isAnimate(-exit) {\n    opacity: 1;\n  }\n\n  @include isAnimate(-exit-active) {\n    opacity: 0;\n    transition: opacity 0.1s;\n  }\n\n  &-exit-done {\n    display: none;\n  }\n}\n\n// prettier-ignore\n@keyframes saladbowl-jelly {\n      0% { transform: matrix3d(0.500, 0, 0, 0, 0, 0.500, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n   3.40% { transform: matrix3d(0.658, 0, 0, 0, 0, 0.703, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n   4.70% { transform: matrix3d(0.725, 0, 0, 0, 0, 0.800, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n   6.81% { transform: matrix3d(0.830, 0, 0, 0, 0, 0.946, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n   9.41% { transform: matrix3d(0.942, 0, 0, 0, 0, 1.084, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  10.21% { transform: matrix3d(0.971, 0, 0, 0, 0, 1.113, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  13.61% { transform: matrix3d(1.062, 0, 0, 0, 0, 1.166, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  14.11% { transform: matrix3d(1.070, 0, 0, 0, 0, 1.165, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  17.52% { transform: matrix3d(1.104, 0, 0, 0, 0, 1.120, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  18.72% { transform: matrix3d(1.106, 0, 0, 0, 0, 1.094, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  21.32% { transform: matrix3d(1.098, 0, 0, 0, 0, 1.035, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  24.32% { transform: matrix3d(1.075, 0, 0, 0, 0, 0.980, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  25.23% { transform: matrix3d(1.067, 0, 0, 0, 0, 0.969, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  29.03% { transform: matrix3d(1.031, 0, 0, 0, 0, 0.948, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  29.93% { transform: matrix3d(1.024, 0, 0, 0, 0, 0.949, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  35.54% { transform: matrix3d(0.990, 0, 0, 0, 0, 0.981, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  36.74% { transform: matrix3d(0.986, 0, 0, 0, 0, 0.989, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  41.04% { transform: matrix3d(0.980, 0, 0, 0, 0, 1.011, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  44.44% { transform: matrix3d(0.983, 0, 0, 0, 0, 1.016, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  52.15% { transform: matrix3d(0.996, 0, 0, 0, 0, 1.003, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  59.86% { transform: matrix3d(1.003, 0, 0, 0, 0, 0.995, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  63.26% { transform: matrix3d(1.004, 0, 0, 0, 0, 0.996, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  75.28% { transform: matrix3d(1.001, 0, 0, 0, 0, 1.002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  85.49% { transform: matrix3d(0.999, 0, 0, 0, 0, 1.000, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n  90.69% { transform: matrix3d(1.000, 0, 0, 0, 0, 1.000, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n    100% { transform: matrix3d(1.000, 0, 0, 0, 0, 1.000, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }\n}\n\n// prettier-ignore\n@keyframes saladbowl-leaf-shake {\n    0% { transform: translate( 2px,  1px) rotate(0deg); }\n   10% { transform: translate(-1px, -2px) rotate(1deg); }\n   20% { transform: translate(-2px,  0  ) rotate(1deg); }\n   30% { transform: translate( 0  ,  2px) rotate(0deg); }\n   40% { transform: translate( 1px, -1px) rotate(1deg); }\n   50% { transform: translate(-1px,  2px) rotate(1deg); }\n   60% { transform: translate(-2px,  1px) rotate(0deg); }\n   70% { transform: translate( 2px,  1px) rotate(1deg); }\n   80% { transform: translate(-1px, -1px) rotate(1deg); }\n   90% { transform: translate( 2px,  2px) rotate(0deg); }\n  100% { transform: translate( 1px, -2px) rotate(1deg); }\n}\n\n// prettier-ignore\n@keyframes saladbowl-orange-spin {\n  from { transform: rotate(  0deg); }\n    to { transform: rotate(360deg); }\n}\n\n// prettier-ignore\n@keyframes saladbowl-tomato-shake {\n    0% { transform: rotate(10deg); }\n   30% { transform: rotate( 0deg); }\n   60% { transform: rotate(10deg); }\n   90% { transform: rotate( 0deg); }\n  100% { transform: rotate( 5deg); }\n}\n"
  },
  {
    "path": "src/content/components/SaladBowl/SaladBowl.stories.tsx",
    "content": "import React, { useState } from 'react'\nimport { storiesOf } from '@storybook/react'\nimport { action } from '@storybook/addon-actions'\nimport { jsxDecorator } from 'storybook-addon-jsx'\nimport { withPropsTable } from 'storybook-addon-react-docgen'\nimport { withKnobs, boolean, number, text } from '@storybook/addon-knobs'\nimport { SaladBowl } from './SaladBowl'\nimport { SaladBowlPortal } from './SaladBowl.portal'\nimport { withLocalStyle } from '@/_helpers/storybook'\n\nstoriesOf('Content Scripts|SaladBowl', module)\n  .addDecorator(withPropsTable)\n  .addDecorator(jsxDecorator)\n  .addDecorator(withKnobs)\n  .add(\n    'SaladBowl',\n    () => (\n      <SaladBowl\n        x={number('mouseX', 30)}\n        y={number('mouseY', 30)}\n        enableHover={boolean('Enable hover', true)}\n        onActive={action('onActive')}\n        onHover={action('onActive')}\n      />\n    ),\n    {\n      decorators: [withLocalStyle(require('./SaladBowl.shadow.scss'))],\n      jsx: { skip: 1 }\n    }\n  )\n  .add('SaladBowlPortal', () => (\n    <SaladBowlPortal\n      show={boolean('Show', true)}\n      panelCSS={text('Panel CSS', '')}\n      x={number('mouseX', 30)}\n      y={number('mouseY', 30)}\n      withAnimation={boolean('Animation', true)}\n      enableHover={boolean('Enable hover', true)}\n      onActive={action('onActive')}\n    />\n  ))\n  .add('Bowl Playground', () => React.createElement(BowlBackground))\n\nfunction BowlBackground() {\n  const [{ x, y }, setCoord] = useState({ x: 0, y: 0 })\n\n  const iconWidth = 30\n  const iconGap = 15\n  const scrollbarWidth = 10\n\n  return (\n    <div\n      style={{\n        width: '100vw',\n        height: '100vh',\n        background: '#5caf9e',\n        overflow: 'hidden'\n      }}\n      onClick={e =>\n        setCoord({\n          x:\n            e.clientX + iconGap + iconWidth > window.innerWidth - scrollbarWidth // right overflow\n              ? e.clientX - iconGap - iconWidth // switch to left\n              : e.clientX + iconGap,\n          y:\n            e.clientY < iconWidth + iconGap // top overflow\n              ? e.clientY + iconGap // switch to bottom\n              : e.clientY - iconWidth - iconGap\n        })\n      }\n    >\n      <p style={{ textAlign: 'center', color: '#fff', userSelect: 'none' }}>\n        CLICK AROUND AND SEE THE BOWL FOLLOWS\n      </p>\n      <SaladBowlPortal\n        show\n        panelCSS={text('Panel CSS', '')}\n        x={x}\n        y={y}\n        withAnimation={boolean('Animation', true)}\n        enableHover={boolean('Enable hover', true)}\n        onActive={action('onActive')}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/content/components/SaladBowl/SaladBowl.tsx",
    "content": "import React, { FC } from 'react'\nimport classnames from 'classnames'\nimport { useSubscription, useObservableCallback } from 'observable-hooks'\nimport { hoverWithDelay } from '@/_helpers/observables'\nimport { SALADICT_EXTERNAL } from '@/_helpers/saladict'\n\nexport interface SaladBowlProps {\n  /** Viewport based coordinate. */\n  readonly x: number\n  /** Viewport based coordinate. */\n  readonly y: number\n  /** React on hover. */\n  readonly enableHover: boolean\n  /** When bowl is activated via mouse. */\n  readonly onActive: () => void\n  readonly onHover: (isHover: boolean) => void\n}\n\n/**\n * Cute little icon that pops up near the selection.\n */\nexport const SaladBowl: FC<SaladBowlProps> = props => {\n  const [onMouseOverOut, mouseOverOut$] = useObservableCallback<\n    boolean,\n    React.MouseEvent<HTMLDivElement>\n  >(hoverWithDelay)\n\n  useSubscription(mouseOverOut$, active => {\n    props.onHover(active)\n    if (active) {\n      props.onActive()\n    }\n  })\n\n  return (\n    <div\n      role=\"img\"\n      className={classnames('saladbowl', SALADICT_EXTERNAL, {\n        enableHover: props.enableHover\n      })}\n      style={{ transform: `translate(${props.x}px, ${props.y}px)` }}\n      onMouseOver={props.enableHover ? onMouseOverOut : undefined}\n      onMouseOut={onMouseOverOut}\n      onClick={() => props.onActive()}\n    >\n      {/* prettier-ignore */}\n      <svg viewBox='0 0 612 612' width='30' height='30'>\n          <g className='saladbowl-leaf'>\n            <path fill='#6bbc57' d='M 577.557 184.258 C 560.417 140.85 519.54 59.214 519.54 59.214 L 519.543 59.204 C 519.543 59.204 436.903 97.626 396.441 120.878 C 366.171 138.274 354.981 169.755 352.221 177.621 C 349.001 186.851 339.891 228.811 358.341 268.481 C 382.271 319.921 409.201 374.521 409.201 374.521 L 409.201 374.531 C 409.201 374.531 464.511 348.701 515.291 323.401 C 554.451 303.891 573.591 265.441 576.821 256.221 C 579.571 248.356 590.398 216.746 577.574 184.271 Z'/>\n            <path fill='#bde9b7' d='M 501.052 102.162 L 507.518 104.425 L 426.69 335.38 L 420.224 333.117 Z'/>\n          </g>\n          <g className='saladbowl-orange'>\n            <circle fill='#ffb30d' cx='299.756' cy='198.246' r='178.613'/>\n            <circle fill='#fce29c' cx='299.756' cy='198.246' r='155.24'/>\n            <path fill='#fcc329' d='M 299.756 189.873 L 341.269 113.475 C 349.169 82.543 324.349 58.588 299.749 57.891 C 275.149 57.201 248.229 82.781 256.489 113.481 L 299.749 189.881 Z M 307.026 194.757 L 393.974 194.757 C 424.928 187.083 434.124 153.681 422.994 131.737 C 411.864 109.795 376.534 98.357 353.5 120.27 L 307.025 194.757 Z M 308.79 203.444 L 354.885 277.168 C 377.925 299.268 410.995 289.438 423.701 268.368 C 436.411 247.298 427.381 211.276 396.591 203.362 L 308.801 203.442 Z M 300.208 206.618 L 259.628 283.516 C 252.098 314.543 277.214 338.193 301.815 338.591 C 326.415 338.991 353.022 313.081 344.392 282.491 L 300.208 206.631 Z M 292.058 203.3 L 205.108 203.415 C 174.163 211.277 165.014 244.54 176.172 266.468 C 187.33 288.396 226.052 300.541 249.056 278.598 L 292.056 203.301 Z M 292.465 194.83 L 246.497 121.024 C 223.494 98.884 190.409 108.658 177.667 129.706 C 164.925 150.753 173.893 186.791 204.669 194.756 L 292.459 194.829 Z'/>\n          </g>\n          <g className='saladbowl-tomato'>\n            <path fill='#a63131' d='M 71.014 337.344 C 147.291 422.594 278.234 429.866 363.482 353.589 L 87.258 44.87 C 2.01 121.15 -5.262 252.092 71.014 337.342 Z'/>\n            <path fill='#bc5757' d='M 101.447 310.115 C 162.685 378.555 267.811 384.393 336.251 323.155 L 114.49 75.31 C 46.047 136.55 40.21 241.674 101.447 310.115 Z'/>\n            <path fill='#f1d4af' d='M 186.412 237.54 L 151.659 245.444 C 139.989 251.384 139.339 265.51 145.779 273.27 C 152.219 281.028 167.379 282.39 174.599 271.538 L 186.399 237.54 Z M 242.062 269.832 L 223.366 300.175 C 219.439 312.658 229.066 323.018 239.116 323.85 C 249.168 324.685 260.756 314.815 258.061 302.065 L 242.061 269.832 Z M 160.202 178.317 L 130.357 158.837 C 117.98 154.585 107.375 163.939 106.277 173.965 C 105.183 183.99 114.747 195.833 127.563 193.471 L 160.203 178.321 Z'/>\n          </g>\n          <g className='saladbowl-bowl'>\n            <path fill='#2d97b7' d='M 30.857 311.46 C 30.857 429.87 105.371 530.8 209.867 569.52 L 209.867 589.2 L 400.987 589.2 L 400.987 568.9 C 503.595 530.114 576.887 431.202 578.31 314.907 L 589.196 295.97 L 22.804 295.97 L 30.867 309.998 C 30.865 310.488 30.857 310.971 30.857 311.458 Z'/>\n            <path fill='#fff' d='M 540.565 321.42 C 540.585 322.587 540.595 323.755 540.595 324.927 C 540.595 405.941 497.513 476.884 433.015 516.122 L 437.178 523.317 C 504.152 482.64 548.895 409.009 548.895 324.927 C 548.895 323.755 548.885 322.587 548.865 321.419 Z M 399.885 532.68 C 388.298 537.31 376.237 541.002 363.793 543.654 L 363.793 544.45 L 364.971 551.893 C 378.481 549.049 391.551 545.018 404.081 539.935 Z'/>\n          </g>\n        </svg>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/content/components/WaveformBox/WaveformBox.container.tsx",
    "content": "import { connect } from 'react-redux'\nimport {\n  ExtractDispatchers,\n  MapStateToProps,\n  MapDispatchToProps\n} from 'react-retux'\nimport { StoreAction, StoreState } from '@/content/redux/modules'\nimport { WaveformBox, WaveformBoxProps } from './WaveformBox'\n\ntype Dispatchers = ExtractDispatchers<\n  WaveformBoxProps,\n  'onHeightChanged' | 'toggleExpand'\n>\n\nconst mapStateToProps: MapStateToProps<\n  StoreState,\n  WaveformBoxProps,\n  Dispatchers\n> = state => {\n  return {\n    darkMode: state.config.darkMode,\n    isExpand: state.isExpandWaveformBox\n  }\n}\n\nconst mapDispatchToProps: MapDispatchToProps<\n  StoreAction,\n  WaveformBoxProps,\n  Dispatchers\n> = dispatch => ({\n  onHeightChanged: height => {\n    dispatch({\n      type: 'UPDATE_PANEL_HEIGHT',\n      payload: { area: 'waveformbox', height }\n    })\n  },\n  toggleExpand: () => {\n    dispatch({ type: 'TOGGLE_WAVEFORM_BOX' })\n  }\n})\n\nexport const WaveformBoxContainer = connect(\n  mapStateToProps,\n  mapDispatchToProps\n)(WaveformBox)\n\nexport default WaveformBoxContainer\n"
  },
  {
    "path": "src/content/components/WaveformBox/WaveformBox.scss",
    "content": "$waveform-height: 165px;\n\n.waveformBox-DrawerBtn {\n  display: block;\n  width: 100%;\n  height: 12px;\n  overflow: hidden;\n  border: none;\n  border-top: 1px solid var(--color-divider);\n  font-size: 0;\n  background-color: var(--color-background);\n  cursor: pointer;\n\n  &:focus {\n    outline: none;\n    border: 1px solid var(--color-font) !important;\n  }\n}\n\n.waveformBox-FrameWrap {\n  overflow: hidden;\n  height: 0;\n  font-size: 0;\n  transition: height 0.4s;\n}\n\n.waveformBox-DrawerBtn_Arrow {\n  fill: var(--color-font);\n}\n\n.waveformBox.isExpand {\n  .waveformBox-FrameWrap {\n    height: $waveform-height;\n  }\n\n  .waveformBox-DrawerBtn {\n    border-bottom: 1px solid var(--color-divider);\n  }\n\n  .waveformBox-DrawerBtn_Arrow {\n    transform: rotate(180deg);\n  }\n}\n\n.waveformBox-Frame {\n  width: 100%;\n  height: $waveform-height;\n  border: none;\n  transition: opacity 0.4s;\n}\n\n.waveformBox-Frame-enter {\n  opacity: 0;\n}\n\n.waveformBox-Frame-enter-active,\n.waveformBox-Frame-exit {\n  opacity: 1;\n}\n\n.waveformBox-Frame-exit-active {\n  opacity: 0;\n}\n"
  },
  {
    "path": "src/content/components/WaveformBox/WaveformBox.stories.tsx",
    "content": "import React from 'react'\nimport { storiesOf } from '@storybook/react'\nimport { jsxDecorator } from 'storybook-addon-jsx'\nimport { withPropsTable } from 'storybook-addon-react-docgen'\nimport { withKnobs, boolean } from '@storybook/addon-knobs'\nimport { withSaladictPanel } from '@/_helpers/storybook'\nimport { WaveformBox } from './WaveformBox'\nimport { action } from '@storybook/addon-actions'\n\nstoriesOf('Content Scripts|Dict Panel', module)\n  .addParameters({\n    backgrounds: [\n      { name: 'Saladict', value: '#5caf9e', default: true },\n      { name: 'Black', value: '#000' }\n    ]\n  })\n  .addDecorator(withPropsTable)\n  .addDecorator(jsxDecorator)\n  .addDecorator(withKnobs)\n  .addDecorator(story => (\n    <div\n      style={{\n        height: '100%',\n        display: 'flex',\n        flexDirection: 'column-reverse'\n      }}\n    >\n      {story()}\n    </div>\n  ))\n  .addDecorator(\n    withSaladictPanel({\n      head: <style>{require('./WaveformBox.scss').toString()}</style>\n    })\n  )\n  .add('WaveformBox', () => (\n    <WaveformBox\n      darkMode={boolean('Dark Mode', false)}\n      isExpand={boolean('Expand', true)}\n      toggleExpand={action('Toggle Expand')}\n      onHeightChanged={action('Height Changed')}\n    />\n  ))\n"
  },
  {
    "path": "src/content/components/WaveformBox/WaveformBox.tsx",
    "content": "import React, { useEffect, FC } from 'react'\nimport CSSTransition from 'react-transition-group/CSSTransition'\nimport { SALADICT_EXTERNAL } from '@/_helpers/saladict'\n\nexport interface WaveformBoxProps {\n  darkMode: boolean\n  isExpand: boolean\n  toggleExpand: () => void\n  onHeightChanged: (height: number) => void\n}\n\nexport const WaveformBox: FC<WaveformBoxProps> = props => {\n  useEffect(() => {\n    props.onHeightChanged((props.isExpand ? 165 : 0) + 12)\n  }, [props.isExpand])\n\n  return (\n    <div\n      className={`waveformBox ${SALADICT_EXTERNAL}${\n        props.isExpand ? ' isExpand' : ''\n      }`}\n    >\n      <button className=\"waveformBox-DrawerBtn\" onClick={props.toggleExpand}>\n        <svg\n          width=\"10\"\n          height=\"10\"\n          viewBox=\"0 0 59.414 59.414\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          className=\"waveformBox-DrawerBtn_Arrow\"\n        >\n          <path d=\"M 58 45.269 L 29.707 16.975 L 1.414 45.27 L 0 43.855 L 29.707 14.145 L 59.414 43.855\" />\n        </svg>\n      </button>\n      <div className=\"waveformBox-FrameWrap\">\n        <CSSTransition\n          timeout={400}\n          in={props.isExpand}\n          classNames=\"waveformBox-Frame\"\n          mountOnEnter\n          unmountOnExit\n        >\n          {() => (\n            <iframe\n              className=\"waveformBox-Frame\"\n              src={`${browser.runtime.getURL('/audio-control.html')}${\n                props.darkMode ? '?darkmode=true' : ''\n              }`}\n              sandbox=\"allow-same-origin allow-scripts\"\n            />\n          )}\n        </CSSTransition>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/content/components/WordEditor/CtxTransList.scss",
    "content": ".ctxTransList {\n  margin-top: 0;\n}\n\n.ctxTransItem {\n  list-style-type: none;\n  border-top: 1px #ccc dashed;\n\n  &:first-of-type {\n    border-top: none;\n  }\n}\n\n.ctxTransItem-Head {\n  display: flex;\n  align-items: center;\n}\n\n.ctxTrans-Title {\n  position: relative;\n  margin: 5px 5px 5px 0;\n  font-size: 16px;\n\n  input {\n    position: absolute;\n    top: 3px;\n    left: -25px;\n  }\n}\n\n.ctxTrans-Content {\n  margin: 0 0 5px 0;\n}\n\n.ctxTrans-Loader {\n  display: flex;\n  align-items: center;\n  width: 54px;\n  height: 20px;\n\n  & > div {\n    width: 8px;\n    height: 8px;\n    margin: 2px;\n    background: #f9690e;\n    border-radius: 100%;\n\n    animation: ctxTrans-Loader 1.5s infinite ease-in-out;\n\n    $ctxTrans-LoaderNum: 5;\n    @for $i from 1 through $ctxTrans-LoaderNum {\n      &:nth-child(#{$ctxTrans-LoaderNum + 1 - $i}) {\n        animation-delay: -0.1s * ($i - 1);\n      }\n    }\n  }\n}\n\n@keyframes ctxTrans-Loader {\n  0%,\n  30%,\n  70%,\n  100% {\n    transform: scale(0);\n  }\n\n  50% {\n    transform: scale(1);\n  }\n}\n"
  },
  {
    "path": "src/content/components/WordEditor/CtxTransList.stories.tsx",
    "content": "import React from 'react'\nimport { storiesOf } from '@storybook/react'\nimport { action } from '@storybook/addon-actions'\nimport { jsxDecorator } from 'storybook-addon-jsx'\nimport { withPropsTable } from 'storybook-addon-react-docgen'\nimport { withKnobs } from '@storybook/addon-knobs'\nimport {\n  withLocalStyle,\n  withSideEffect,\n  mockRuntimeMessage\n} from '@/_helpers/storybook'\nimport faker from 'faker'\nimport getDefaultConfig from '@/app-config'\nimport { CtxTransList } from './CtxTransList'\nimport { CtxTranslateResults } from '@/_helpers/translateCtx'\nimport { newWord } from '@/_helpers/record-manager'\n\nstoriesOf('Content Scripts|WordEditor', module)\n  .addDecorator(withPropsTable)\n  .addDecorator(jsxDecorator)\n  .addDecorator(withKnobs)\n  .addDecorator(\n    withSideEffect(\n      mockRuntimeMessage(async message => {\n        action(message.type)(message['payload'])\n      })\n    )\n  )\n  .add(\n    'CtxTransList',\n    () => {\n      const config = getDefaultConfig()\n\n      return (\n        <CtxTransList\n          word={newWord({\n            date: faker.date.past().valueOf(),\n            text: faker.random.word(),\n            context: faker.lorem.sentence(),\n            title: faker.random.word(),\n            url: faker.internet.url(),\n            favicon: faker.image.imageUrl(),\n            trans: faker.lorem.sentence(),\n            note: faker.lorem.sentences()\n          })}\n          ctxTransConfig={config.ctxTrans}\n          ctxTransResult={Object.keys(config.ctxTrans).reduce((result, id) => {\n            result[id] = faker.random.boolean() ? faker.lorem.paragraphs() : ''\n            return result\n          }, {} as CtxTranslateResults)}\n          onNewCtxTransConfig={action('onNewCtxTransConfig')}\n          onNewCtxTransResult={action('onNewCtxTransResult')}\n        />\n      )\n    },\n    {\n      decorators: [withLocalStyle(require('./CtxTransList.scss'))],\n      jsx: { skip: 1 }\n    }\n  )\n"
  },
  {
    "path": "src/content/components/WordEditor/CtxTransList.tsx",
    "content": "import React, { FC, useState } from 'react'\nimport { AppConfig } from '@/app-config'\nimport {\n  CtxTranslatorId,\n  CtxTranslateResults,\n  translateCtx\n} from '@/_helpers/translateCtx'\nimport { Word } from '@/_helpers/record-manager'\n\nexport interface CtxTransListProps {\n  word: Word\n  ctxTransConfig: AppConfig['ctxTrans']\n  ctxTransResult: CtxTranslateResults\n  onNewCtxTransConfig: (id: CtxTranslatorId, enabled: boolean) => void\n  onNewCtxTransResult: (id: CtxTranslatorId, content: string) => void\n}\n\nexport const CtxTransList: FC<CtxTransListProps> = props => {\n  const [isLoading, setIsLoading] = useState(() =>\n    Object.keys(props.ctxTransConfig).reduce((result, id) => {\n      result[id] = false\n      return result\n    }, {} as { [id in keyof AppConfig['ctxTrans']]: boolean })\n  )\n\n  const onTicked = async (evt: React.ChangeEvent<HTMLInputElement>) => {\n    const { name } = evt.currentTarget.dataset\n    if (\n      name &&\n      Object.prototype.hasOwnProperty.call(props.ctxTransConfig, name)\n    ) {\n      props.onNewCtxTransConfig(\n        name as CtxTranslatorId,\n        evt.currentTarget.checked\n      )\n\n      const text = props.word.context || props.word.text\n      if (evt.currentTarget.checked && text) {\n        setIsLoading(isLoading => ({\n          ...isLoading,\n          [name]: true\n        }))\n\n        const result = await translateCtx(text, name as CtxTranslatorId)\n\n        setIsLoading(isLoading => ({\n          ...isLoading,\n          [name]: false\n        }))\n\n        props.onNewCtxTransResult(name as CtxTranslatorId, result)\n      } else {\n        props.onNewCtxTransResult(name as CtxTranslatorId, '')\n      }\n    }\n  }\n\n  return (\n    <ul className=\"ctxTransList\">\n      {Object.keys(props.ctxTransResult).map(name => (\n        <li key={name} className=\"ctxTransItem\">\n          <div className=\"ctxTransItem-Head\">\n            <h1 className=\"ctxTrans-Title\">\n              <input\n                type=\"checkbox\"\n                checked={props.ctxTransConfig[name]}\n                id={'ctx-' + name}\n                data-name={name}\n                onChange={onTicked}\n              />\n              <label htmlFor={'ctx-' + name}>{name}</label>\n            </h1>\n            {isLoading[name] && (\n              <div className=\"ctxTrans-Loader\">\n                <div />\n                <div />\n                <div />\n                <div />\n                <div />\n              </div>\n            )}\n          </div>\n          <p className=\"ctxTrans-Content\">{props.ctxTransResult[name]}</p>\n        </li>\n      ))}\n    </ul>\n  )\n}\n"
  },
  {
    "path": "src/content/components/WordEditor/Notes.scss",
    "content": "@import './WordEditorPanel.scss';\n@import './WordCards.scss';\n@import './CtxTransList.scss';\n\n.wordEditorNote-Container {\n  display: flex;\n}\n\n.wordEditorNote {\n  flex: 3;\n  padding: 15px;\n\n  a {\n    text-decoration: none;\n    color: #1890ff;\n  }\n\n  label {\n    display: block;\n    margin-bottom: 5px;\n    font-weight: bold;\n  }\n\n  input,\n  textarea {\n    box-sizing: border-box;\n    display: block;\n    resize: vertical;\n    width: 100%;\n    margin-bottom: 15px;\n    padding: 6px 12px;\n    font-size: 14px;\n    line-height: 1.42857;\n    color: var(--color-font);\n    background: var(--color-background);\n    background-image: none;\n    border: 1px solid #ccc;\n    border-radius: 4px;\n    box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n    transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;\n\n    &:focus {\n      border-color: #66afe9;\n      outline: 0;\n      box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),\n        0 0 8px rgba(102, 175, 233, 0.6);\n    }\n  }\n\n  @include isDarkMode {\n    input,\n    textarea {\n      border-color: #8b8b8b;\n    }\n  }\n}\n\n.wordEditorNote_Help {\n  margin-top: -15px;\n  color: var(--color-font-grey);\n}\n\n.wordEditorNote_SrcFavicon {\n  height: 16px;\n  margin-left: 5px;\n  vertical-align: text-bottom;\n}\n\n.wordEditorNote_LabelWithBtn {\n  & > label {\n    display: inline-block;\n    margin-right: 5px;\n  }\n\n  & > button {\n    position: relative;\n    top: -1px;\n    padding: 1px 5px;\n    background: transparent;\n    border: 1px solid #ccc;\n    border-radius: 3px;\n    font-size: 90%;\n    color: var(--color-font);\n    cursor: pointer;\n  }\n}\n\n.notes-fade-appear,\n.notes-fade-enter {\n  opacity: 0;\n}\n.notes-fade-appear-active,\n.notes-fade-enter-active {\n  opacity: 1;\n  transition: opacity 0.4s;\n}\n\n.notes-fade-exit {\n  opacity: 1;\n}\n.notes-fade-exit-active {\n  opacity: 0;\n  transition: opacity 0.1s;\n}\n"
  },
  {
    "path": "src/content/components/WordEditor/Notes.tsx",
    "content": "import React, { FC, useState, useEffect } from 'react'\nimport { useUpdateEffect } from 'react-use'\nimport {\n  useObservable,\n  useObservableState,\n  useObservableCallback,\n  useSubscription,\n  pluckFirst\n} from 'observable-hooks'\nimport { of } from 'rxjs'\nimport {\n  withLatestFrom,\n  switchMap,\n  debounceTime,\n  startWith\n} from 'rxjs/operators'\n\nimport {\n  Word,\n  getWordsByText,\n  deleteWords,\n  saveWord\n} from '@/_helpers/record-manager'\nimport { AppConfig } from '@/app-config'\nimport {\n  translateCtxs,\n  genCtxText,\n  CtxTranslateResults\n} from '@/_helpers/translateCtx'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { message, storage } from '@/_helpers/browser-api'\nimport { isOptionsPage } from '@/_helpers/saladict'\n\nimport { WordCards } from './WordCards'\nimport {\n  WordEditorPanel,\n  WordEditorPanelProps,\n  WordEditorPanelBtns\n} from './WordEditorPanel'\nimport { CSSTransition } from 'react-transition-group'\nimport { CtxTransList } from './CtxTransList'\nimport { StorageSyncConfig } from '@/background/sync-manager/helpers'\n\nexport interface NotesProps\n  extends Pick<WordEditorPanelProps, 'containerWidth'> {\n  wordEditor: {\n    word: Word\n    translateCtx: boolean\n  }\n  /** dicts to translate context */\n  ctxTrans: AppConfig['ctxTrans']\n\n  onClose: () => void\n}\n\nconst notesFadeTimeout = { enter: 400, exit: 100, appear: 400 }\n\nexport const Notes: FC<NotesProps> = props => {\n  const { t } = useTranslate(['common', 'content'])\n  const [isDirty, setDirty] = useState(false)\n  const [isShowCtxTransList, setShowCtxTransList] = useState(false)\n\n  const [word, setWord] = useState(props.wordEditor.word)\n  const word$ = useObservable(pluckFirst, [word])\n\n  const [ctxTransConfig, setCtxTransConfig] = useState(props.ctxTrans)\n  useUpdateEffect(() => {\n    setCtxTransConfig(props.ctxTrans)\n  }, [props.ctxTrans])\n\n  const [ctxTransResult, setCtxTransResult] = useState(() =>\n    Object.keys(props.ctxTrans).reduce((result, id) => {\n      result[id] = ''\n      return result\n    }, {} as CtxTranslateResults)\n  )\n\n  const [getRelatedWords, relatedWords$] = useObservableCallback<\n    Word[],\n    never,\n    []\n  >(event$ =>\n    event$.pipe(\n      debounceTime(200),\n      withLatestFrom(word$),\n      switchMap(([, word]) => {\n        if (!word.text) {\n          return of([])\n        }\n\n        return getWordsByText('notebook', word.text)\n          .then(words => words.filter(({ date }) => date !== word.date))\n          .catch(() => [])\n      }),\n      startWith([])\n    )\n  )\n\n  const relatedWords = useObservableState(relatedWords$)\n\n  const [onTranslateCtx, translateCtx$] = useObservableCallback<\n    CtxTranslateResults,\n    typeof ctxTransConfig\n  >(event$ =>\n    event$.pipe(\n      withLatestFrom(word$),\n      switchMap(([ctxTransConfig, word]) => {\n        return translateCtxs(word.context || word.text, ctxTransConfig)\n      })\n    )\n  )\n  useSubscription(translateCtx$, setCtxTransResult)\n\n  useEffect(() => {\n    if (props.wordEditor.translateCtx) {\n      onTranslateCtx(ctxTransConfig)\n    }\n  }, [])\n\n  useEffect(getRelatedWords, [word.text, word.context])\n\n  useUpdateEffect(() => {\n    setWord({\n      ...word,\n      trans: genCtxText(word.trans, ctxTransResult)\n    })\n  }, [ctxTransResult])\n\n  const closeEditor = () => {\n    if (!isDirty || confirm(t('content:wordEditor.closeConfirm'))) {\n      props.onClose()\n    }\n  }\n\n  const formChanged = ({\n    currentTarget\n  }: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {\n    setDirty(true)\n    setWord({\n      ...word,\n      [currentTarget.name]: currentTarget.value\n    })\n  }\n\n  const panelBtns: WordEditorPanelBtns = [\n    {\n      type: 'normal',\n      title: t('content:transContext'),\n      onClick: () => onTranslateCtx(ctxTransConfig)\n    },\n    {\n      type: 'normal',\n      title: t('content:neverShow'),\n      onClick: () => {\n        if (!isOptionsPage()) {\n          message.send({\n            type: 'OPEN_URL',\n            payload: {\n              url: 'options.html?menuselected=Notebook',\n              self: true\n            }\n          })\n        }\n      }\n    },\n    {\n      type: 'normal',\n      title: t('cancel'),\n      onClick: closeEditor\n    },\n    {\n      type: 'primary',\n      title: t('save'),\n      onClick: () => {\n        saveWord('notebook', word)\n          .then(props.onClose)\n          .catch(console.error)\n      }\n    }\n  ]\n\n  const [ankiCardId, setAnkiCardId] = useState<number | undefined>()\n\n  useEffect(() => {\n    let isRunning = true\n    storage.sync\n      .get<StorageSyncConfig>('syncConfig')\n      .then(async ({ syncConfig }) => {\n        if (syncConfig?.ankiconnect?.enable) {\n          const cardId = await message.send<'ANKI_CONNECT_FIND_WORD'>({\n            type: 'ANKI_CONNECT_FIND_WORD',\n            payload: word.date\n          })\n          if (isRunning) {\n            setAnkiCardId(cardId)\n          }\n        }\n      })\n    return () => {\n      isRunning = false\n    }\n  }, [])\n\n  if (ankiCardId) {\n    panelBtns.unshift({\n      type: 'normal',\n      title: t('content:updateAnki.title'),\n      onClick: async () => {\n        let status = 'content:updateAnki.success'\n        try {\n          await message.send<'ANKI_CONNECT_UPDATE_WORD'>({\n            type: 'ANKI_CONNECT_UPDATE_WORD',\n            payload: { cardId: ankiCardId, word }\n          })\n        } catch (e) {\n          if (process.env.DEBUG) {\n            console.error(e)\n          }\n          status = 'content:updateAnki.failed'\n        }\n        browser.notifications.create({\n          type: 'basic',\n          eventTime: Date.now() + 2000,\n          iconUrl: browser.runtime.getURL(`assets/icon-128.png`),\n          title: 'Saladict',\n          message: t(status)\n        })\n      }\n    })\n  }\n\n  return (\n    <>\n      <WordEditorPanel\n        containerWidth={props.containerWidth}\n        title={t('content:wordEditor.title')}\n        btns={panelBtns}\n        onClose={closeEditor}\n      >\n        <div className=\"wordEditorNote-Container\">\n          <div className=\"wordEditorNote\">\n            <label htmlFor=\"wordEditorNote_Word\">{t('note.word')}</label>\n            <input\n              type=\"text\"\n              name=\"text\"\n              id=\"wordEditorNote_Word\"\n              value={word.text}\n              onChange={formChanged}\n              onKeyDown={stopPropagation}\n            />\n            <label htmlFor=\"wordEditorNote_Context\">{t('note.context')}</label>\n            <textarea\n              rows={3}\n              name=\"context\"\n              id=\"wordEditorNote_Context\"\n              value={word.context}\n              onChange={formChanged}\n              onKeyDown={stopPropagation}\n            />\n            <div className=\"wordEditorNote_LabelWithBtn\">\n              <label htmlFor=\"wordEditorNote_Trans\">\n                {t('note.trans')}\n                <a\n                  href=\"https://saladict.crimx.com/q&a.html#%E9%97%AE%EF%BC%9A%E6%B7%BB%E5%8A%A0%E7%94%9F%E8%AF%8D%E5%8F%AF%E4%B8%8D%E5%8F%AF%E4%BB%A5%E5%8A%A0%E5%85%A5%E5%8D%95%E8%AF%8D%E7%BF%BB%E8%AF%91%EF%BC%88%E8%80%8C%E4%B8%8D%E6%98%AF%E7%BF%BB%E8%AF%91%E6%95%B4%E5%8F%A5%E4%B8%8A%E4%B8%8B%E6%96%87%EF%BC%89%E3%80%82\"\n                  target=\"_blank\"\n                  rel=\"nofollow noopener noreferrer\"\n                >\n                  {' '}\n                  Why?\n                </a>\n              </label>\n              <button onClick={() => setShowCtxTransList(true)}>\n                {t('content:wordEditor.chooseCtxTitle')}\n              </button>\n            </div>\n            <textarea\n              rows={10}\n              name=\"trans\"\n              id=\"wordEditorNote_Trans\"\n              value={word.trans}\n              onChange={formChanged}\n              onKeyDown={stopPropagation}\n            />\n            <p className=\"wordEditorNote_Help\">\n              {t('content:wordEditor.ctxHelp')}\n            </p>\n            <label htmlFor=\"wordEditorNote_Note\">{t('note.note')}</label>\n            <textarea\n              rows={5}\n              name=\"note\"\n              id=\"wordEditorNote_Note\"\n              value={word.note}\n              onChange={formChanged}\n              onKeyDown={stopPropagation}\n            />\n            <label htmlFor=\"wordEditorNote_SrcTitle\">\n              {t('note.srcTitle')}\n            </label>\n            <input\n              type=\"text\"\n              name=\"title\"\n              id=\"wordEditorNote_SrcTitle\"\n              value={word.title}\n              onChange={formChanged}\n              onKeyDown={stopPropagation}\n            />\n            <label htmlFor=\"wordEditorNote_SrcLink\">{t('note.srcLink')}</label>\n            <input\n              type=\"text\"\n              name=\"url\"\n              id=\"wordEditorNote_SrcLink\"\n              value={word.url}\n              onChange={formChanged}\n              onKeyDown={stopPropagation}\n            />\n            <label htmlFor=\"wordEditorNote_SrcFavicon\">\n              {t('note.srcFavicon')}\n              {word.favicon ? (\n                <img\n                  className=\"wordEditorNote_SrcFavicon\"\n                  src={word.favicon}\n                  alt={t('note.srcTitle')}\n                />\n              ) : null}\n            </label>\n            <input\n              type=\"text\"\n              name=\"favicon\"\n              id=\"wordEditorNote_SrcFavicon\"\n              value={word.favicon}\n              onChange={formChanged}\n              onKeyDown={stopPropagation}\n            />\n          </div>\n          {relatedWords && relatedWords.length > 0 && (\n            <WordCards\n              words={relatedWords}\n              onCardDelete={word => {\n                if (window.confirm(t('content:wordEditor.deleteConfirm'))) {\n                  deleteWords('notebook', [word.date]).then(getRelatedWords)\n                }\n              }}\n            />\n          )}\n        </div>\n      </WordEditorPanel>\n\n      <CSSTransition\n        classNames=\"notes-fade\"\n        mountOnEnter\n        unmountOnExit\n        timeout={notesFadeTimeout}\n        in={isShowCtxTransList}\n      >\n        {() => (\n          <WordEditorPanel\n            containerWidth={props.containerWidth - 100}\n            title={t('content:wordEditor.chooseCtxTitle')}\n            onClose={() => setShowCtxTransList(false)}\n            btns={[\n              {\n                type: 'normal',\n                title: t('content:transContext'),\n                onClick: () => onTranslateCtx(ctxTransConfig)\n              }\n            ]}\n          >\n            <CtxTransList\n              word={word}\n              ctxTransConfig={ctxTransConfig}\n              ctxTransResult={ctxTransResult}\n              onNewCtxTransConfig={(id, enabled) => {\n                setCtxTransConfig(ctxTransConfig => ({\n                  ...ctxTransConfig,\n                  [id]: enabled\n                }))\n              }}\n              onNewCtxTransResult={(id, content) => {\n                setCtxTransResult(ctxTransResult => ({\n                  ...ctxTransResult,\n                  [id]: content\n                }))\n              }}\n            />\n          </WordEditorPanel>\n        )}\n      </CSSTransition>\n    </>\n  )\n}\n\nfunction stopPropagation(e: React.KeyboardEvent<HTMLElement>) {\n  e.stopPropagation()\n  e.nativeEvent.stopPropagation()\n}\n"
  },
  {
    "path": "src/content/components/WordEditor/WordCards.scss",
    "content": ".wordCards {\n  flex: 2;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n\n.wordCards-Title {\n  margin: 0;\n  padding: 3px 0;\n  text-align: center;\n  font-size: 1em;\n  font-weight: normal;\n  border-bottom: 1px solid #e5e5e5;\n}\n\n.wordCards-CardList {\n  flex: 1;\n  margin: 0;\n  padding: 10px;\n  overflow-x: hidden;\n  overflow-y: auto;\n}\n\n.wordCards-Card {\n  list-style-type: none;\n  position: relative;\n  display: block;\n  overflow: hidden;\n  margin: 0 0 10px 0;\n  padding: 10px 10px 10px;\n  word-wrap: break-word;\n  border: 1px #faebcc solid;\n  border-radius: 20px;\n  color: #8a6d3b;\n  background: #fcf8e3;\n}\n\n.wordCards-CardClose {\n  position: absolute;\n  top: 5px;\n  right: 5px;\n  border: none;\n  font-size: 20px;\n  font-weight: bold;\n  color: #8a6d3b;\n  background: transparent;\n  cursor: pointer;\n}\n\n.wordCards-CardTitle {\n  margin: 0 0 0.5em;\n  text-align: center;\n}\n\n.wordCards-CardItem {\n  position: relative;\n  overflow: visible;\n  margin-bottom: 0.5em;\n  padding: 0 0 0 25px;\n}\n\n.wordCards-CardItem_Cont {\n  p {\n    margin: 0;\n  }\n}\n\n.wordCards-CardItem_Icon {\n  position: absolute;\n  top: -5px;\n  left: 0;\n  width: 18px;\n  height: 18px;\n  fill: #8a6d3b;\n  user-select: none;\n}\n\n.wordCards-CardFooter {\n  position: relative;\n  border-top: 1px solid #faebcc;\n  padding: 8px 0;\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n}\n\n.wordCards-Favicon {\n  position: absolute;\n  top: 8px;\n  left: 3px;\n  width: 14px;\n  height: 14px;\n}\n\n.wordCards-URL {\n  padding: 0 0 0 25px;\n  color: inherit;\n  font-size: 12px;\n}\n"
  },
  {
    "path": "src/content/components/WordEditor/WordCards.tsx",
    "content": "import React, { FC } from 'react'\nimport { Word } from '@/_helpers/record-manager'\nimport { useTranslate } from '@/_helpers/i18n'\n\nexport interface WordCardsProps {\n  words: Word[]\n  onCardDelete: (word: Word) => any\n}\n\nexport const WordCards: FC<WordCardsProps> = ({ words, onCardDelete }) => {\n  const { t } = useTranslate(['common', 'content'])\n\n  return (\n    <aside className=\"wordCards\">\n      <header>\n        <h1 className=\"wordCards-Title\">\n          {t('content:wordEditor.wordCardsTitle')}\n        </h1>\n      </header>\n      <ul className=\"wordCards-CardList\">\n        {words.map(word => (\n          <li className=\"wordCards-Card\" key={word.date}>\n            <button\n              type=\"button\"\n              className=\"wordCards-CardClose\"\n              onClick={() => onCardDelete(word)}\n            >\n              &times;\n            </button>\n            <h2 className=\"wordCards-CardTitle\">{word.text}</h2>\n            {word.context && (\n              <div className=\"wordCards-CardItem\">\n                <svg\n                  className=\"wordCards-CardItem_Icon\"\n                  width=\"18\"\n                  height=\"18\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  viewBox=\"0 0 95.333 95.332\"\n                >\n                  <title>{t('note.context')}</title>\n                  <path d=\"M 36.587 45.263 C 35.07 44.825 33.553 44.605 32.078 44.605 C 29.799 44.605 27.898 45.125 26.423 45.763 C 27.844 40.559 31.259 31.582 38.061 30.57 C 38.69 30.476 39.207 30.021 39.379 29.408 L 40.864 24.09 C 40.99 23.641 40.916 23.16 40.66 22.77 C 40.403 22.38 39.991 22.119 39.529 22.056 C 39.027 21.987 38.515 21.952 38.009 21.952 C 29.844 21.952 21.759 30.474 18.347 42.675 C 16.344 49.833 15.757 60.595 20.686 67.369 C 23.445 71.16 27.472 73.183 32.657 73.385 L 32.717 73.386 C 39.114 73.386 44.783 69.079 46.508 62.915 C 47.538 59.229 47.073 55.364 45.196 52.029 C 43.338 48.731 40.28 46.327 36.581 45.263 Z M 76.615 52.029 C 74.758 48.731 71.699 46.327 68.002 45.263 C 66.484 44.823 64.968 44.604 63.492 44.604 C 61.214 44.604 59.311 45.121 57.838 45.76 C 59.259 40.553 62.673 31.579 69.475 30.564 C 70.102 30.47 70.619 30.016 70.793 29.402 L 72.28 24.085 C 72.403 23.635 72.332 23.155 72.073 22.764 C 71.814 22.373 71.401 22.113 70.942 22.049 C 70.438 21.981 69.928 21.946 69.417 21.946 C 61.253 21.946 53.169 30.467 49.755 42.669 C 47.752 49.827 47.166 60.59 52.101 67.364 C 54.858 71.153 58.887 73.178 64.069 73.379 C 64.091 73.38 64.111 73.381 64.134 73.381 C 70.527 73.381 76.198 69.074 77.923 62.908 C 78.953 59.224 78.485 55.358 76.609 52.022 Z\" />\n                </svg>\n                <div className=\"wordCards-CardItem_Cont\">\n                  {genParagraphs(word.context)}\n                </div>\n              </div>\n            )}\n            {word.trans && (\n              <div className=\"wordCards-CardItem\">\n                <svg\n                  className=\"wordCards-CardItem_Icon\"\n                  width=\"18\"\n                  height=\"18\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  viewBox=\"0 0 469.333 469.333\"\n                >\n                  <title>{t('note.trans')}</title>\n                  <path d=\"M253.227 300.267L199.04 246.72l.64-.64c37.12-41.387 63.573-88.96 79.147-139.307h62.507V64H192V21.333h-42.667V64H0v42.453h238.293c-14.4 41.173-36.907 80.213-67.627 114.347-19.84-22.08-36.267-46.08-49.28-71.467H78.72c15.573 34.773 36.907 67.627 63.573 97.28l-108.48 107.2L64 384l106.667-106.667 66.347 66.347 16.213-43.413zM373.333 192h-42.667l-96 256h42.667l24-64h101.333l24 64h42.667l-96-256zm-56 149.333L352 248.853l34.667 92.48h-69.334z\" />\n                </svg>\n                <div className=\"wordCards-CardItem_Cont\">\n                  {genParagraphs(word.trans)}\n                </div>\n              </div>\n            )}\n            {word.note && (\n              <div className=\"wordCards-CardItem\">\n                <svg\n                  className=\"wordCards-CardItem_Icon\"\n                  width=\"18\"\n                  height=\"18\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  viewBox=\"0 0 35.738 35.738\"\n                >\n                  <title>{t('note.note')}</title>\n                  <path d=\"M0 35.667S11.596-1.403 35.738.117c0 0-2.994 4.85-10.55 6.416 0 0 3.517.43 6.368-.522 0 0-1.71 5.517-11.025 6.275 0 0 5.135 1.33 7.416.57 0 0-.62 4.11-10.102 6.154-.562.12-4.347 1.066-1.306 1.447 0 0 4.37.763 5.514.38 0 0-3.743 5.608-12.927 5.133-.903-.048-1.332 0-1.332 0L0 35.666z\" />\n                </svg>\n                <div className=\"wordCards-CardItem_Cont\">\n                  {genParagraphs(word.note)}\n                </div>\n              </div>\n            )}\n            <div className=\"wordCards-CardFooter\">\n              {word.favicon && (\n                <img className=\"wordCards-Favicon\" src={word.favicon} />\n              )}\n              <a\n                className=\"wordCards-URL\"\n                href={word.url}\n                target=\"_blank\"\n                rel=\"nofollow noopener noreferrer\"\n                title={word.title}\n              >\n                {word.title}\n              </a>\n            </div>\n          </li>\n        ))}\n      </ul>\n    </aside>\n  )\n}\n\nexport default WordCards\n\nfunction genParagraphs(text: string): React.ReactNode {\n  return text.split('\\n').map((line, i) => <p key={i}>{line}</p>)\n}\n"
  },
  {
    "path": "src/content/components/WordEditor/WordEditor.container.tsx",
    "content": "import { connect } from 'react-redux'\nimport {\n  ExtractDispatchers,\n  MapStateToProps,\n  MapDispatchToProps\n} from 'react-retux'\nimport { StoreState, StoreAction } from '@/content/redux/modules'\nimport { WordEditorPortal, WordEditorPortalProps } from './WordEditor.portal'\n\ntype Dispatchers = ExtractDispatchers<WordEditorPortalProps, 'onClose'>\n\nconst mapStateToProps: MapStateToProps<\n  StoreState,\n  WordEditorPortalProps,\n  Dispatchers\n> = state => ({\n  show: state.wordEditor.isShow,\n  darkMode: state.config.darkMode,\n  withAnimation: state.config.animation,\n  containerWidth: window.innerWidth - state.config.panelWidth - 100,\n  ctxTrans: state.config.ctxTrans,\n  wordEditor: state.wordEditor\n})\n\nconst mapDispatchToProps: MapDispatchToProps<\n  StoreAction,\n  WordEditorPortalProps,\n  Dispatchers\n> = dispatch => ({\n  onClose: () => {\n    dispatch({ type: 'WORD_EDITOR_STATUS', payload: { word: null } })\n  }\n})\n\nexport const WordEditorContainer = connect(\n  mapStateToProps,\n  mapDispatchToProps\n)(WordEditorPortal)\n\nexport default WordEditorContainer\n"
  },
  {
    "path": "src/content/components/WordEditor/WordEditor.portal.tsx",
    "content": "import React, { FC } from 'react'\nimport classnames from 'classnames'\nimport { useRefFn } from 'observable-hooks'\nimport { ShadowPortal, defaultTimeout } from '@/components/ShadowPortal'\nimport { WordEditor, WordEditorProps } from './WordEditor'\n\nexport interface WordEditorPortalProps extends WordEditorProps {\n  show: boolean\n  withAnimation: boolean\n  darkMode: boolean\n}\n\nexport const WordEditorPortal: FC<WordEditorPortalProps> = props => {\n  const { show, withAnimation, darkMode, ...restProps } = props\n  const editorStyles = useRefFn(() => (\n    <style>{require('./WordEditor.shadow.scss').toString()}</style>\n  )).current\n  return (\n    <ShadowPortal\n      id=\"saladict-wordeditor-root\"\n      head={editorStyles}\n      in={show}\n      innerRootClassName={classnames({ isAnimate: withAnimation, darkMode })}\n      timeout={withAnimation ? defaultTimeout : 0}\n    >\n      {() => <WordEditor {...restProps} />}\n    </ShadowPortal>\n  )\n}\n\nexport default WordEditorPortal\n"
  },
  {
    "path": "src/content/components/WordEditor/WordEditor.scss",
    "content": "/*-----------------------------------------------*\\\n    Variables\n\\*-----------------------------------------------*/\n@import '@/_sass_shared/_theme.scss';\n\n/*-----------------------------------------------*\\\nLibs\n\\*-----------------------------------------------*/\n@import '~normalize-scss';\n\n/*-----------------------------------------------*\\\n    Components\n\\*-----------------------------------------------*/\n@import './Notes.scss';\n"
  },
  {
    "path": "src/content/components/WordEditor/WordEditor.shadow.scss",
    "content": "@import './WordEditor.scss';\n@import '@/components/ShadowPortal/ShadowPortal.scss';\n"
  },
  {
    "path": "src/content/components/WordEditor/WordEditor.stories.tsx",
    "content": "import React from 'react'\nimport { storiesOf } from '@storybook/react'\nimport { action } from '@storybook/addon-actions'\nimport { jsxDecorator } from 'storybook-addon-jsx'\nimport { withPropsTable } from 'storybook-addon-react-docgen'\nimport { withKnobs, boolean, number } from '@storybook/addon-knobs'\nimport { WordEditor } from './WordEditor'\nimport {\n  withLocalStyle,\n  withSideEffect,\n  mockRuntimeMessage,\n  withi18nNS\n} from '@/_helpers/storybook'\nimport faker from 'faker'\nimport { newWord } from '@/_helpers/record-manager'\nimport getDefaultConfig from '@/app-config'\nimport WordEditorPortal from './WordEditor.portal'\n\nstoriesOf('Content Scripts|WordEditor', module)\n  .addDecorator(withPropsTable)\n  .addDecorator(jsxDecorator)\n  .addDecorator(withKnobs)\n  .addDecorator(\n    withSideEffect(\n      mockRuntimeMessage(async message => {\n        action(message.type)(message['payload'])\n        switch (message.type) {\n          case 'GET_WORDS_BY_TEXT':\n            return faker.random.boolean()\n              ? [\n                  newWord({\n                    date: faker.date.past().valueOf(),\n                    text: message.payload.text,\n                    context: faker.lorem.sentence(),\n                    title: faker.random.word(),\n                    url: faker.internet.url(),\n                    favicon: faker.image.imageUrl(),\n                    trans: faker.lorem.sentence(),\n                    note: faker.lorem.sentences()\n                  })\n                ]\n              : []\n        }\n      })\n    )\n  )\n  .addDecorator(withi18nNS(['common', 'content']))\n  .add(\n    'WordEditor',\n    () => {\n      const config = getDefaultConfig()\n      const darkMode = boolean('Dark Mode', false)\n\n      return (\n        <WordEditor\n          containerWidth={number('Panel X', 450 + 100)}\n          darkMode={darkMode}\n          wordEditor={{\n            word: newWord({\n              date: faker.date.past().valueOf(),\n              text: faker.random.word(),\n              context: faker.lorem.sentence(),\n              title: faker.random.word(),\n              url: faker.internet.url(),\n              favicon: faker.image.imageUrl(),\n              trans: faker.lorem.sentence(),\n              note: faker.lorem.sentences()\n            }),\n            translateCtx: false\n          }}\n          ctxTrans={config.ctxTrans}\n          onClose={action('Close')}\n        />\n      )\n    },\n    {\n      decorators: [withLocalStyle(require('./WordEditor.scss'))],\n      jsx: { skip: 1 }\n    }\n  )\n  .add('WordEditorPortal', () => {\n    const config = getDefaultConfig()\n    const darkMode = boolean('Dark Mode', false)\n\n    return (\n      <WordEditorPortal\n        show={boolean('Show', true)}\n        darkMode={darkMode}\n        withAnimation={boolean('With Animation', true)}\n        containerWidth={number('Panel X', 450 + 100)}\n        wordEditor={{\n          word: newWord({\n            date: faker.date.past().valueOf(),\n            text: faker.random.word(),\n            context: faker.lorem.sentence(),\n            title: faker.random.word(),\n            url: faker.internet.url(),\n            favicon: faker.image.imageUrl(),\n            trans: faker.lorem.sentence(),\n            note: faker.lorem.sentences()\n          }),\n          translateCtx: false\n        }}\n        ctxTrans={config.ctxTrans}\n        onClose={action('Close')}\n      />\n    )\n  })\n"
  },
  {
    "path": "src/content/components/WordEditor/WordEditor.tsx",
    "content": "import React, { FC } from 'react'\nimport { Notes, NotesProps } from './Notes'\nimport { SALADICT_EXTERNAL } from '@/_helpers/saladict'\n\nexport interface WordEditorProps extends NotesProps {}\n\nexport const WordEditor: FC<WordEditorProps> = props => {\n  return (\n    <div className={`${SALADICT_EXTERNAL} saladict-theme`}>\n      <Notes {...props} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/content/components/WordEditor/WordEditorPanel.scss",
    "content": "@import '@/_sass_shared/_fancy-scrollbar.scss';\n\n.wordEditorPanel-Background {\n  position: fixed;\n  z-index: $global-zindex-dicteditor;\n  top: 0;\n  left: 0;\n  width: 100vw;\n  height: 100vh;\n  display: flex;\n  align-items: center;\n  text-align: initial;\n  background: rgba(0, 0, 0, 0.4);\n}\n\n.wordEditorPanel-Container {\n  display: flex;\n  align-items: center;\n  height: 100%;\n  min-width: 440px;\n  margin-left: auto;\n}\n\n.wordEditorPanel {\n  display: flex;\n  flex-direction: column;\n  width: 80%;\n  max-width: 800px;\n  min-width: 400px;\n  max-height: 90vh;\n  border-radius: 6px;\n  color: var(--color-font);\n  background-color: var(--color-background);\n  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n  font-size: 13px;\n  font-family: 'Helvetica Neue', Helvetica, Arial, 'Hiragino Sans GB',\n    'Hiragino Sans GB W3', 'Microsoft YaHei UI', 'Microsoft YaHei', sans-serif;\n}\n\n.wordEditorPanel-Header {\n  display: flex;\n  border-bottom: 1px solid #ccc;\n}\n\n.wordEditorPanel-Title {\n  margin: 0;\n  font-size: 18px;\n  padding: 15px;\n}\n\n.wordEditorPanel-BtnClose {\n  text-decoration: none;\n  margin-left: auto;\n  padding: 15px;\n  font-size: 21px;\n  font-weight: bold;\n  line-height: 1;\n  border: none;\n  background: transparent;\n  color: var(--color-font);\n  text-shadow: 0 1px 0 var(--color-background);\n  opacity: 0.5;\n  cursor: pointer;\n\n  &:hover {\n    outline: none;\n  }\n}\n\n.wordEditorPanel-Main {\n  flex: 1;\n  overflow-x: hidden;\n  overflow-y: scroll;\n  overscroll-behavior: contain;\n}\n\n.wordEditorPanel-Footer {\n  padding: 15px;\n  text-align: right;\n  border-top: 1px solid #ccc;\n}\n\n%wordEditorPanel-Btn {\n  display: inline-block;\n  margin-bottom: 0;\n  font-weight: normal;\n  text-align: center;\n  vertical-align: middle;\n  touch-action: manipulation;\n  cursor: pointer;\n  background-image: none;\n  border: 1px solid transparent;\n  white-space: nowrap;\n  padding: 6px 12px;\n  font-size: 14px;\n  line-height: 1.42857;\n  border-radius: 4px;\n  user-select: none;\n}\n\n.wordEditorPanel-Btn {\n  @extend %wordEditorPanel-Btn;\n  margin-right: 10px;\n  color: #333;\n  background-color: #fff;\n  border-color: #ccc;\n\n  &:focus {\n    background-color: #e6e6e6;\n    border-color: #8c8c8c;\n  }\n\n  &:hover {\n    outline: 0;\n    background-color: #e6e6e6;\n    border-color: #adadad;\n  }\n\n  &:active {\n    outline: 0;\n    background-color: #e6e6e6;\n    border-color: #8c8c8c;\n    box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  }\n}\n\n.wordEditorPanel-Btn_normal {\n  @extend .wordEditorPanel-Btn;\n}\n\n.wordEditorPanel-Btn_primary {\n  @extend %wordEditorPanel-Btn;\n  color: #fff;\n  background-color: #337ab7;\n  border-color: #2e6da4;\n\n  &:focus {\n    background-color: #286090;\n    border-color: #122b40;\n  }\n\n  &:hover {\n    outline: 0;\n    background-color: #286090;\n    border-color: #204d74;\n  }\n\n  &:active {\n    outline: 0;\n    background-color: #204d74;\n    border-color: #122b40;\n    box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  }\n}\n\n.darkMode {\n  .wordEditorPanel-Header,\n  .wordEditorPanel-Footer {\n    border-color: #8b8b8b;\n  }\n\n  .wordEditorPanel-Btn {\n    @extend %wordEditorPanel-Btn;\n    margin-right: 10px;\n    color: #333;\n    background-color: #ddd;\n    border-color: #ccc;\n\n    &:focus {\n      background-color: #bfbfbf;\n      border-color: #8c8c8c;\n    }\n\n    &:hover {\n      outline: 0;\n      background-color: #bfbfbf;\n      border-color: #adadad;\n    }\n\n    &:active {\n      outline: 0;\n      background-color: #bfbfbf;\n      border-color: #8c8c8c;\n      box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n    }\n  }\n}\n"
  },
  {
    "path": "src/content/components/WordEditor/WordEditorPanel.stories.tsx",
    "content": "import React from 'react'\nimport classNames from 'classnames'\nimport { storiesOf } from '@storybook/react'\nimport { action } from '@storybook/addon-actions'\nimport { jsxDecorator } from 'storybook-addon-jsx'\nimport { withPropsTable } from 'storybook-addon-react-docgen'\nimport { withKnobs, boolean, number, text } from '@storybook/addon-knobs'\nimport { WordEditorPanel } from './WordEditorPanel'\nimport {\n  withLocalStyle,\n  withSideEffect,\n  mockRuntimeMessage,\n  withi18nNS\n} from '@/_helpers/storybook'\nimport faker from 'faker'\n\nstoriesOf('Content Scripts|WordEditor', module)\n  .addDecorator(withPropsTable)\n  .addDecorator(jsxDecorator)\n  .addDecorator(withKnobs)\n  .addDecorator(\n    withSideEffect(\n      mockRuntimeMessage(async message => {\n        action(message.type)(message['payload'])\n      })\n    )\n  )\n  .addDecorator(withi18nNS(['common', 'content']))\n  .add(\n    'WordEditorPanel',\n    () => {\n      const darkMode = boolean('Dark Mode', false)\n\n      return (\n        <div className={classNames({ darkMode })}>\n          <div\n            className=\"saladict-theme\"\n            style={{\n              display: 'flex',\n              justifyContent: 'center',\n              padding: '20px 0'\n            }}\n          >\n            <WordEditorPanel\n              containerWidth={number('Panel X', 450 + 100)}\n              btns={\n                boolean('With Buttons', true)\n                  ? [\n                      {\n                        type: 'normal',\n                        title: 'Normal Button',\n                        onClick: action('Normal button clicked')\n                      },\n                      {\n                        type: 'primary',\n                        title: 'Primary Button',\n                        onClick: action('Primary button clicked')\n                      }\n                    ]\n                  : undefined\n              }\n              title={text('Title', faker.random.word())}\n              onClose={action('Close')}\n            >\n              <div style={{ padding: 10 }}>\n                {text('Content', faker.lorem.paragraphs())\n                  .split('\\n')\n                  .map((paragraph, index) => (\n                    <p key={index}>{paragraph}</p>\n                  ))}\n              </div>\n            </WordEditorPanel>\n          </div>\n        </div>\n      )\n    },\n    {\n      decorators: [\n        withLocalStyle(require('./WordEditorPanel.scss')),\n        withLocalStyle(require('@/_sass_shared/_theme.scss'))\n      ],\n      jsx: { skip: 1 }\n    }\n  )\n"
  },
  {
    "path": "src/content/components/WordEditor/WordEditorPanel.tsx",
    "content": "import React, { FC } from 'react'\nimport { isInternalPage } from '@/_helpers/saladict'\n\nexport type WordEditorPanelBtns = Array<{\n  type?: 'normal' | 'primary'\n  title: React.ReactNode\n  onClick: () => void\n}>\n\nexport interface WordEditorPanelProps {\n  containerWidth: number\n  title: React.ReactNode\n  btns?: WordEditorPanelBtns\n  onClose: () => void\n}\n\nexport const WordEditorPanel: FC<WordEditorPanelProps> = props => {\n  return (\n    <div\n      className=\"wordEditorPanel-Background\"\n      style={{\n        zIndex: isInternalPage() ? 998 : 2147483646 // for popups on options page\n      }}\n    >\n      <div\n        className=\"wordEditorPanel-Container\"\n        style={{ width: props.containerWidth }}\n      >\n        <div className=\"wordEditorPanel\">\n          <header className=\"wordEditorPanel-Header\">\n            <h1 className=\"wordEditorPanel-Title\">{props.title}</h1>\n            <button\n              type=\"button\"\n              className=\"wordEditorPanel-BtnClose\"\n              onClick={props.onClose}\n            >\n              ×\n            </button>\n          </header>\n          <div className=\"wordEditorPanel-Main fancy-scrollbar\">\n            {props.children}\n          </div>\n          {props.btns && props.btns.length > 0 && (\n            <footer className=\"wordEditorPanel-Footer\">\n              {props.btns.map((btn, index) => (\n                <button\n                  key={index}\n                  type=\"button\"\n                  className={\n                    btn.type\n                      ? `wordEditorPanel-Btn_${btn.type}`\n                      : 'wordEditorPanel-Btn'\n                  }\n                  onClick={btn.onClick}\n                >\n                  {btn.title}\n                </button>\n              ))}\n            </footer>\n          )}\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport default WordEditorPanel\n"
  },
  {
    "path": "src/content/components/WordEditor/WordEditorStandalone.container.tsx",
    "content": "import { connect } from 'react-redux'\nimport { MapStateToProps } from 'react-retux'\nimport { StoreState } from '@/content/redux/modules'\nimport { WordEditor, WordEditorProps } from './WordEditor'\n\nconst onClose = () => {\n  window.close()\n}\n\nconst mapStateToProps: MapStateToProps<\n  StoreState,\n  WordEditorProps\n> = state => ({\n  darkMode: state.config.darkMode,\n  containerWidth: window.innerWidth,\n  ctxTrans: state.config.ctxTrans,\n  wordEditor: state.wordEditor,\n  onClose\n})\n\nexport const WordEditorStandaloneContainer = connect(mapStateToProps)(\n  WordEditor\n)\n\nexport default WordEditorStandaloneContainer\n"
  },
  {
    "path": "src/content/index.tsx",
    "content": "import React from 'react'\nimport ReactDOM from 'react-dom'\nimport { Provider as ProviderRedux } from 'react-redux'\nimport SaladBowlContainer from './components/SaladBowl/SaladBowl.container'\nimport DictPanelContainer from './components/DictPanel/DictPanel.container'\nimport WordEditorContainer from './components/WordEditor/WordEditor.container'\nimport { createStore } from './redux'\n\nimport { I18nContextProvider } from '@/_helpers/i18n'\n\nimport './_style.scss'\n\n// Only load on top frame\nif (window.parent === window && !window.__SALADICT_PANEL_LOADED__) {\n  window.__SALADICT_PANEL_LOADED__ = true\n\n  main()\n}\n\nasync function main() {\n  const store = await createStore()\n  const App = () => (\n    <ProviderRedux store={store}>\n      <I18nContextProvider>\n        <SaladBowlContainer />\n        <DictPanelContainer />\n        <WordEditorContainer />\n      </I18nContextProvider>\n    </ProviderRedux>\n  )\n\n  ReactDOM.render(<App />, document.createElement('div'))\n}\n"
  },
  {
    "path": "src/content/redux/epics/index.ts",
    "content": "import { combineEpics } from 'redux-observable'\nimport { from, of, EMPTY } from 'rxjs'\nimport { map, mapTo, mergeMap, filter, pairwise } from 'rxjs/operators'\n\nimport { isPopupPage, isStandalonePage } from '@/_helpers/saladict'\nimport { saveWord } from '@/_helpers/record-manager'\n\nimport { StoreAction, StoreState } from '../modules'\nimport { ofType } from './utils'\n\nimport searchStartEpic from './searchStart.epic'\nimport newSelectionEpic from './newSelection.epic'\nimport { translateCtxs, genCtxText } from '@/_helpers/translateCtx'\nimport { message } from '@/_helpers/browser-api'\n\nexport const epics = combineEpics<StoreAction, StoreAction, StoreState>(\n  /** Start searching text. This will also send to Redux. */\n  (action$, state$) =>\n    action$.pipe(\n      ofType('BOWL_ACTIVATED'),\n      map(\n        () =>\n          state$.value.selection.word\n            ? {\n                type: 'SEARCH_START',\n                payload: { word: state$.value.selection.word }\n              }\n            : { type: 'SEARCH_START' } // this should never be reached\n      )\n    ),\n  (action$, state$) =>\n    action$.pipe(\n      ofType('SWITCH_HISTORY'),\n      mapTo({ type: 'SEARCH_START', payload: { noHistory: true } })\n    ),\n  (action$, state$) =>\n    state$.pipe(\n      map(state => state.isShowDictPanel),\n      pairwise(),\n      mergeMap(([oldShow, newShow]) => {\n        if (oldShow && !newShow) {\n          message.send({ type: 'STOP_AUDIO' })\n        }\n        return EMPTY\n      })\n    ),\n  (action$, state$) =>\n    action$.pipe(\n      ofType('ADD_TO_NOTEBOOK'),\n      mergeMap(() => {\n        if (state$.value.config.editOnFav) {\n          const word = state$.value.searchHistory[state$.value.historyIndex]\n\n          if (isPopupPage() || isStandalonePage()) {\n            const { width: screenWidth, height: screenHeight } = window.screen\n            const width = Math.round(Math.min(Math.max(screenWidth, 440), 640))\n            const height = Math.round(Math.min(screenHeight - 150, 800))\n\n            let wordString = ''\n            try {\n              wordString = encodeURIComponent(JSON.stringify(word))\n            } catch (e) {\n              console.warn(e)\n            }\n\n            browser.windows\n              .create({\n                type: 'popup',\n                url: browser.runtime.getURL(\n                  `word-editor.html?word=${wordString}`\n                ),\n                top: Math.round((screenHeight - height) / 2),\n                left: Math.round((screenWidth - width) / 2),\n                width,\n                height\n              })\n              .catch(e => {\n                console.warn(e)\n              })\n\n            return EMPTY\n          }\n\n          return of({\n            type: 'WORD_EDITOR_STATUS',\n            payload: { word, translateCtx: true }\n          } as const)\n        }\n\n        return from(\n          (async () => {\n            const word =\n              state$.value.searchHistory[state$.value.searchHistory.length - 1]\n            if (word) {\n              try {\n                word.trans = genCtxText(\n                  word.trans,\n                  await translateCtxs(\n                    word.context || word.text,\n                    state$.value.config.ctxTrans\n                  )\n                )\n                await saveWord('notebook', word)\n                return true\n              } catch (e) {\n                console.warn(e)\n                return false\n              }\n            }\n            return false\n          })()\n        ).pipe(\n          // dim icon if failed\n          filter(isSuccess => !isSuccess),\n          mapTo({ type: 'WORD_IN_NOTEBOOK', payload: false } as const)\n        )\n      })\n    ),\n  newSelectionEpic,\n  searchStartEpic\n)\n\nexport default epics\n"
  },
  {
    "path": "src/content/redux/epics/newSelection.epic.ts",
    "content": "import { switchMap } from 'rxjs/operators'\nimport { EMPTY, of } from 'rxjs'\nimport { StoreAction, StoreState } from '../modules'\nimport { Epic, ofType } from './utils'\nimport { message } from '@/_helpers/browser-api'\nimport { isStandalonePage, isOptionsPage } from '@/_helpers/saladict'\n\nexport const newSelectionEpic: Epic = (action$, state$) =>\n  action$.pipe(\n    ofType('NEW_SELECTION'),\n    // Selection may be skipped in state, use payload instead.\n    switchMap(({ payload: selection }) => {\n      const { config, withQssaPanel, isShowDictPanel, isPinned } = state$.value\n\n      if (selection.self) {\n        // Selection inside dict panel.\n        return selectionInsideDictPanel(config, selection)\n      }\n\n      if (isOptionsPage()) {\n        return EMPTY\n      }\n\n      if (withQssaPanel && config.qssaPageSel) {\n        // standalone panel takes control\n        return selectionToQSPanel(config, selection)\n      }\n\n      if (isStandalonePage()) {\n        return EMPTY\n      }\n\n      const { pinMode } = config\n\n      if (\n        isShowDictPanel &&\n        selection.word &&\n        selection.word.text &&\n        (!isPinned ||\n          pinMode.direct ||\n          (pinMode.double && selection.dbClick) ||\n          (pinMode.holding.alt && selection.altKey) ||\n          (pinMode.holding.shift && selection.shiftKey) ||\n          (pinMode.holding.ctrl && selection.ctrlKey) ||\n          (pinMode.holding.meta && selection.metaKey))\n      ) {\n        // continue searching\n        return of<StoreAction>({\n          type: 'SEARCH_START',\n          payload: { word: selection.word }\n        })\n      }\n\n      return EMPTY\n    })\n  )\n\nexport default newSelectionEpic\n\nfunction selectionInsideDictPanel(\n  config: StoreState['config'],\n  selection: StoreState['selection']\n): ReturnType<Epic> {\n  // inside dict panel\n  const { direct, double, holding } = config.panelMode\n  if (\n    selection.word &&\n    selection.word.text &&\n    (selection.instant ||\n      direct ||\n      (double && selection.dbClick) ||\n      (holding.alt && selection.altKey) ||\n      (holding.shift && selection.shiftKey) ||\n      (holding.ctrl && selection.ctrlKey) ||\n      (holding.meta && selection.metaKey))\n  ) {\n    return of<StoreAction>({\n      type: 'SEARCH_START',\n      payload: {\n        word: {\n          ...selection.word,\n          title: 'Saladict Panel',\n          favicon: 'https://saladict.crimx.com/favicon.ico'\n        }\n      }\n    })\n  }\n  return EMPTY\n}\n\nfunction selectionToQSPanel(\n  config: StoreState['config'],\n  selection: StoreState['selection']\n): ReturnType<Epic> {\n  // standalone panel takes control\n  const { direct, double, holding } = config.qsPanelMode\n  if (\n    selection.word &&\n    selection.word.text &&\n    (selection.instant ||\n      direct ||\n      (double && selection.dbClick) ||\n      (holding.alt && selection.altKey) ||\n      (holding.shift && selection.shiftKey) ||\n      (holding.ctrl && selection.ctrlKey) ||\n      (holding.meta && selection.metaKey))\n  ) {\n    message.send({\n      type: 'QS_PANEL_SEARCH_TEXT',\n      payload: selection.word\n    })\n  }\n  return EMPTY\n}\n"
  },
  {
    "path": "src/content/redux/epics/searchStart.epic.ts",
    "content": "import {\n  switchMap,\n  map,\n  share,\n  take,\n  filter,\n  tap,\n  switchMapTo\n} from 'rxjs/operators'\nimport { merge, from, EMPTY } from 'rxjs'\nimport { StoreAction } from '../modules'\nimport { Epic, ofType } from './utils'\nimport { isInNotebook, saveWord } from '@/_helpers/record-manager'\nimport { message } from '@/_helpers/browser-api'\nimport {\n  isPDFPage,\n  isInternalPage,\n  isStandalonePage\n} from '@/_helpers/saladict'\nimport { DictID } from '@/app-config'\nimport { MachineTranslateResult } from '@/components/MachineTrans/engine'\nimport { MessageResponse } from '@/typings/message'\n\nexport const searchStartEpic: Epic = (action$, state$) =>\n  action$.pipe(\n    ofType('SEARCH_START'),\n    switchMap(({ payload }) => {\n      const {\n        config,\n        searchHistory,\n        historyIndex,\n        renderedDicts\n      } = state$.value\n      const word = searchHistory[historyIndex]\n\n      if (\n        config.searchHistory &&\n        (!isInternalPage() || isStandalonePage()) &&\n        (!browser.extension.inIncognitoContext || config.searchHistoryInco) &&\n        (historyIndex <= 0 ||\n          searchHistory[historyIndex - 1].text !== word.text ||\n          searchHistory[historyIndex - 1].context !== word.context)\n      ) {\n        saveWord('history', word)\n      }\n\n      const toStart = new Set<DictID>()\n      for (const d of renderedDicts) {\n        if (d.searchStatus === 'SEARCHING') {\n          toStart.add(d.id)\n        }\n      }\n\n      const { cn, en, machine } = config.autopron\n      if (cn.dict) toStart.add(cn.dict)\n      if (en.dict) toStart.add(en.dict)\n      if (machine.dict) toStart.add(machine.dict)\n\n      const searchResults$$ = merge(\n        ...[...toStart].map(\n          (id): Promise<MessageResponse<'FETCH_DICT_RESULT'>> =>\n            message\n              .send<'FETCH_DICT_RESULT'>({\n                type: 'FETCH_DICT_RESULT',\n                payload: {\n                  id,\n                  text: word.text,\n                  payload:\n                    payload && payload.payload\n                      ? { isPDF: isPDFPage(), ...payload.payload }\n                      : { isPDF: isPDFPage() }\n                }\n              })\n              .catch(() => ({ id, result: null }))\n        )\n      ).pipe(share())\n\n      const playAudio$ =\n        payload && payload.id\n          ? EMPTY\n          : searchResults$$.pipe(\n              filter(({ id, audio, result }) => {\n                if (!audio) return false\n                if (id === cn.dict && audio.py) return true\n                if (id === en.dict && (audio.uk || audio.us)) return true\n                return (\n                  id === machine.dict &&\n                  !!(result as MachineTranslateResult<DictID>)[machine.src].tts\n                )\n              }),\n              take(1),\n              tap(({ id, audio, result }) => {\n                if (id === cn.dict) {\n                  return message.send({\n                    type: 'PLAY_AUDIO',\n                    payload: audio!.py!\n                  })\n                }\n\n                if (id === en.dict) {\n                  const src =\n                    en.accent === 'us'\n                      ? audio!.us || audio!.uk\n                      : audio!.uk || audio!.us\n                  return message.send({ type: 'PLAY_AUDIO', payload: src! })\n                }\n\n                message.send({\n                  type: 'PLAY_AUDIO',\n                  payload: (result as MachineTranslateResult<DictID>)[\n                    machine.src\n                  ].tts!\n                })\n              }),\n              // never pass to down stream\n              switchMapTo(EMPTY)\n            )\n\n      return merge(\n        from(isInNotebook(word).catch(() => false)).pipe(\n          map(\n            (isInNotebook): StoreAction => ({\n              type: 'WORD_IN_NOTEBOOK',\n              payload: isInNotebook\n            })\n          )\n        ),\n        searchResults$$.pipe(\n          map(\n            ({ id, result, catalog }): StoreAction => ({\n              type: 'SEARCH_END',\n              payload: { id, result, catalog }\n            })\n          )\n        ),\n        playAudio$\n      )\n    })\n  )\n\nexport default searchStartEpic\n"
  },
  {
    "path": "src/content/redux/epics/utils.ts",
    "content": "import { Observable } from 'rxjs'\nimport { Epic as RawEpic, ofType as rawOfType } from 'redux-observable'\nimport { StoreAction, StoreActionType, StoreState } from '../modules'\n\n/** Tailored `Epic` for the store. */\nexport type Epic<\n  TOutType extends StoreActionType = StoreActionType,\n  TDeps = any\n> = RawEpic<StoreAction, StoreAction<TOutType>, StoreState, TDeps>\n\n/**\n * Tailored `ofType` for the store.\n * Now you can use `ofType` directly without the need to\n * manually offer types each time.\n */\nexport const ofType = rawOfType as <\n  TInAction extends StoreAction,\n  TTypes extends StoreActionType[] = StoreActionType[],\n  TOutAction extends StoreAction = StoreAction<TTypes[number]>\n>(\n  ...types: TTypes\n) => (source: Observable<TInAction>) => Observable<TOutAction>\n"
  },
  {
    "path": "src/content/redux/index.ts",
    "content": "import {\n  createStore as createReduxStore,\n  applyMiddleware,\n  compose,\n  Store\n} from 'redux'\nimport thunkMiddleware from 'redux-thunk'\nimport {\n  useStore as _useStore,\n  useSelector as _useSelector,\n  useDispatch as _useDispatch\n} from 'react-redux'\nimport { createEpicMiddleware } from 'redux-observable'\nimport { Observable } from 'rxjs'\nimport { map, distinctUntilChanged, startWith } from 'rxjs/operators'\nimport { message } from '@/_helpers/browser-api'\nimport { reportPageView } from '@/_helpers/analytics'\nimport { isPDFPage, isPopupPage, isStandalonePage } from '@/_helpers/saladict'\n\nimport {\n  getRootReducer,\n  StoreState,\n  StoreAction,\n  StoreDispatch\n} from './modules'\nimport { init } from './init'\nimport { epics } from './epics'\n\nconst epicMiddleware = createEpicMiddleware<\n  StoreAction,\n  StoreAction,\n  StoreState\n>()\n\nexport const useStore: () => Store<StoreState, StoreAction> = _useStore\n\nexport const useSelector: <TSelected = unknown>(\n  selector: (state: StoreState) => TSelected,\n  equalityFn?: (left: TSelected, right: TSelected) => boolean\n) => TSelected = _useSelector\n\nexport const useDispatch: () => StoreDispatch = _useDispatch\n\nexport const createStore = async () => {\n  const composeEnhancers: typeof compose =\n    window['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'] || compose\n\n  const store = createReduxStore(\n    await getRootReducer(),\n    composeEnhancers(applyMiddleware(thunkMiddleware, epicMiddleware))\n  )\n\n  epicMiddleware.run(epics)\n\n  init(store.dispatch, store.getState)\n\n  // sync state\n  const storeState$ = new Observable<StoreState>(observer => {\n    store.subscribe(() => observer.next(store.getState()))\n  })\n\n  storeState$\n    .pipe(\n      map(state => state.isPinned),\n      distinctUntilChanged()\n    )\n    .subscribe(isPinned => {\n      message.self.send({\n        type: 'PIN_STATE',\n        payload: isPinned\n      })\n    })\n\n  storeState$\n    .pipe(\n      map(state => state.isShowDictPanel),\n      startWith(false),\n      distinctUntilChanged()\n    )\n    .subscribe(isShowDictPanel => {\n      if (isShowDictPanel) {\n        if (isPopupPage()) {\n          reportPageView('/popup')\n        } else if (isPDFPage()) {\n          reportPageView('/pdf-dictpanel')\n        } else if (isStandalonePage()) {\n          reportPageView('/standalone')\n        } else {\n          reportPageView('/dictpanel')\n        }\n      }\n    })\n\n  message.addListener('QUERY_PIN_STATE', queryStoreState)\n  message.self.addListener('QUERY_PIN_STATE', queryStoreState)\n\n  async function queryStoreState() {\n    return store.getState().isPinned\n  }\n\n  return store\n}\n"
  },
  {
    "path": "src/content/redux/init.ts",
    "content": "import { addConfigListener } from '@/_helpers/config-manager'\nimport {\n  addActiveProfileListener,\n  createProfileIDListStream\n} from '@/_helpers/profile-manager'\nimport {\n  isPopupPage,\n  isQuickSearchPage,\n  isOptionsPage,\n  isStandalonePage\n} from '@/_helpers/saladict'\nimport { message } from '@/_helpers/browser-api'\nimport { Word, newWord } from '@/_helpers/record-manager'\nimport { timer } from '@/_helpers/promise-more'\nimport { MessageResponse } from '@/typings/message'\nimport { StoreDispatch, StoreState } from './modules'\nimport { isTagName } from '@/_helpers/dom'\n\nexport const init = (dispatch: StoreDispatch, getState: () => StoreState) => {\n  window.addEventListener('resize', () => {\n    dispatch({ type: 'WINDOW_RESIZE' })\n  })\n\n  addConfigListener(({ newConfig }) => {\n    if (newConfig.active !== getState().config.active) {\n      message.send({\n        type: 'SEND_TAB_BADGE_INFO',\n        payload: {\n          active: newConfig.active,\n          tempDisable: getState().isTempDisabled,\n          unsupported: isStandalonePage()\n            ? false\n            : !isTagName(document.body, 'body')\n        }\n      })\n    }\n\n    dispatch({ type: 'NEW_CONFIG', payload: newConfig })\n  })\n\n  addActiveProfileListener(({ newProfile }) => {\n    dispatch({ type: 'NEW_ACTIVE_PROFILE', payload: newProfile })\n  })\n\n  createProfileIDListStream().subscribe(idlist => {\n    dispatch({ type: 'NEW_PROFILES', payload: idlist })\n  })\n\n  message.addListener(msg => {\n    switch (msg.type) {\n      case 'TEMP_DISABLED_STATE':\n        if (msg.payload.op === 'set') {\n          dispatch({ type: 'TEMP_DISABLED_STATE', payload: msg.payload.value })\n          setTimeout(() => {\n            const state = getState()\n            message.send({\n              type: 'SEND_TAB_BADGE_INFO',\n              payload: {\n                active: state.config.active,\n                tempDisable: state.isTempDisabled,\n                unsupported: isStandalonePage()\n                  ? false\n                  : !isTagName(document.body, 'body')\n              }\n            })\n          }, 0)\n          return Promise.resolve(true)\n        } else {\n          return Promise.resolve(getState().isTempDisabled)\n        }\n\n      case 'SEARCH_TEXT_BOX': {\n        const { searchHistory, historyIndex, text } = getState()\n        dispatch({\n          type: 'SEARCH_START',\n          payload: {\n            word:\n              searchHistory[historyIndex]?.text === text\n                ? searchHistory[historyIndex]\n                : newWord({\n                    text,\n                    title: 'Saladict',\n                    favicon: 'https://saladict.crimx.com/favicon.ico'\n                  })\n          }\n        })\n        return isPopupPage() ? Promise.resolve(true) : Promise.resolve()\n      }\n\n      case 'WORD_SAVED': {\n        const { text } = getState()\n        if (text) {\n          message\n            .send<'IS_IN_NOTEBOOK'>({\n              type: 'IS_IN_NOTEBOOK',\n              payload: newWord({ text })\n            })\n            .then(isInNotebook => {\n              dispatch({ type: 'WORD_IN_NOTEBOOK', payload: isInNotebook })\n            })\n        }\n        break\n      }\n\n      case 'ADD_NOTEBOOK': {\n        if (msg.payload.popup === isPopupPage()) {\n          dispatch({ type: 'ADD_TO_NOTEBOOK' })\n          return Promise.resolve(true)\n        }\n        return\n      }\n\n      case 'SWITCH_HISTORY': {\n        dispatch({ type: 'SWITCH_HISTORY', payload: msg.payload })\n        return Promise.resolve(true)\n      }\n\n      case 'QS_PANEL_SEARCH_TEXT':\n        if (isQuickSearchPage()) {\n          // request searching text, from other tabs\n          dispatch({ type: 'SEARCH_START', payload: { word: msg.payload } })\n\n          if (getState().isQSFocus) {\n            // focus standalone panel\n            message.send({ type: 'OPEN_QS_PANEL' })\n          }\n        }\n        return Promise.resolve()\n\n      case 'QS_PANEL_CHANGED':\n        if (!isQuickSearchPage()) {\n          dispatch({ type: 'QS_PANEL_CHANGED', payload: msg.payload })\n        }\n        return Promise.resolve()\n\n      case 'QS_PANEL_FOCUSED':\n        if (isQuickSearchPage()) {\n          const input = document.querySelector<\n            HTMLTextAreaElement | HTMLInputElement\n          >(\n            getState().isExpandMtaBox\n              ? '.mtaBox-TextArea'\n              : '.menuBar-SearchBox'\n          )\n          if (input) {\n            input.focus()\n            input.select()\n          }\n        }\n        return Promise.resolve()\n\n      case 'GET_TAB_BADGE_INFO': {\n        const state = getState()\n        return Promise.resolve<MessageResponse<'GET_TAB_BADGE_INFO'>>({\n          active: state.config.active,\n          tempDisable: state.isTempDisabled,\n          unsupported: isStandalonePage()\n            ? false\n            : !isTagName(document.body, 'body')\n        })\n      }\n    }\n  })\n\n  /**\n   * Date of last instant capture.\n   * To skip extra event after resuming selection\n   */\n  let lastInstantDate = 0\n\n  message.self.addListener(msg => {\n    switch (msg.type) {\n      case 'SELECTION':\n        if (msg.payload.instant) {\n          lastInstantDate = Date.now()\n        } else if (Date.now() - lastInstantDate < 500) {\n          return Promise.resolve()\n        }\n        dispatch({ type: 'NEW_SELECTION', payload: msg.payload })\n        return Promise.resolve()\n\n      case 'ESCAPE_KEY':\n        dispatch({ type: 'CLOSE_PANEL' })\n        return Promise.resolve()\n\n      case 'SEARCH_TEXT':\n        dispatch({ type: 'SEARCH_START', payload: { word: msg.payload } })\n        return Promise.resolve()\n\n      case 'CLOSE_PANEL':\n        dispatch({ type: 'CLOSE_PANEL' })\n        return Promise.resolve()\n\n      case 'TRIPLE_CTRL':\n        if (!isPopupPage() && !isOptionsPage()) {\n          const state = getState()\n          if (state.config.tripleCtrl) {\n            if (state.config.qsStandalone) {\n              // focus if the standalone panel is already opened\n              message.send({ type: 'OPEN_QS_PANEL' })\n            } else if (!state.isShowDictPanel) {\n              dispatch({ type: 'OPEN_QS_PANEL' })\n              initTripleCtrl(dispatch, state)\n            }\n          }\n        }\n        return Promise.resolve()\n\n      case 'UPDATE_WORD_EDITOR_WORD':\n        dispatch({ type: 'WORD_EDITOR_STATUS', payload: msg.payload })\n        return timer(100).then(() => {\n          // wait till snapshot is taken\n          dispatch({\n            type: 'SEARCH_START',\n            payload: { word: msg.payload.word }\n          })\n        })\n\n      case 'LAST_PLAY_AUDIO':\n        return Promise.resolve(getState().lastPlayAudio)\n    }\n  })\n\n  if (isPopupPage()) {\n    initPopup(dispatch, getState())\n  } else if (isQuickSearchPage()) {\n    initStandaloneQuickSearch(dispatch, getState())\n  } else {\n    message\n      .send<'QUERY_QS_PANEL'>({ type: 'QUERY_QS_PANEL' })\n      .then(response =>\n        dispatch({ type: 'QS_PANEL_CHANGED', payload: response })\n      )\n  }\n}\n\nasync function initStandaloneQuickSearch(\n  dispatch: StoreDispatch,\n  state: StoreState\n) {\n  let word: Word | null = null\n\n  const { searchParams } = new URL(document.URL)\n\n  if (!searchParams.get('sidebar')) {\n    // pin panel if not sidebar mode\n    dispatch({ type: 'TOGGLE_PIN' })\n  }\n\n  const wordString = searchParams.get('word')\n  if (wordString) {\n    try {\n      word = JSON.parse(decodeURIComponent(wordString))\n    } catch (error) {\n      if (process.env.DEBUG) {\n        console.warn(error)\n      }\n      word = null\n    }\n  }\n\n  if (!word) {\n    if (state.config.qsPreload === 'selection') {\n      const lastTab = Number(searchParams.get('lastTab'))\n      if (lastTab) {\n        word = await message.send<'PRELOAD_SELECTION'>(lastTab, {\n          type: 'PRELOAD_SELECTION'\n        })\n      }\n    } else if (state.config.qsPreload === 'clipboard') {\n      word = newWord({\n        text: await message.send<'GET_CLIPBOARD'>({ type: 'GET_CLIPBOARD' }),\n        title: 'From Clipboard'\n      })\n    }\n  }\n\n  if (word) {\n    if (word.text && (state.config.qsAuto || wordString)) {\n      dispatch({ type: 'SEARCH_START', payload: { word } })\n    } else {\n      dispatch({ type: 'SUMMONED_PANEL_INIT', payload: word.text })\n    }\n  }\n}\n\nasync function initPopup(dispatch: StoreDispatch, state: StoreState) {\n  let word: Word | null = null\n\n  if (state.config.baPreload === 'selection') {\n    const tab = (\n      await browser.tabs.query({\n        active: true,\n        currentWindow: true\n      })\n    )[0]\n    if (tab && tab.id != null) {\n      word = await message.send<'PRELOAD_SELECTION'>(tab.id, {\n        type: 'PRELOAD_SELECTION'\n      })\n    }\n  } else if (state.config.baPreload === 'clipboard') {\n    word = newWord({\n      text: await message.send<'GET_CLIPBOARD'>({ type: 'GET_CLIPBOARD' }),\n      title: 'From Clipboard'\n    })\n  }\n\n  if (word) {\n    if (word.text && state.config.baAuto) {\n      dispatch({ type: 'SEARCH_START', payload: { word } })\n    } else {\n      dispatch({ type: 'SUMMONED_PANEL_INIT', payload: word.text })\n    }\n  }\n}\n\nasync function initTripleCtrl(dispatch: StoreDispatch, state: StoreState) {\n  let word: Word | null = null\n\n  if (state.config.qsPreload === 'selection') {\n    if (state.selection.word) {\n      word = state.selection.word\n    }\n  } else if (state.config.qsPreload === 'clipboard') {\n    word = newWord({\n      text: await message.send<'GET_CLIPBOARD'>({ type: 'GET_CLIPBOARD' }),\n      title: 'From Clipboard'\n    })\n  }\n\n  if (word) {\n    if (word.text && state.config.qsAuto) {\n      dispatch({ type: 'SEARCH_START', payload: { word } })\n    } else {\n      dispatch({ type: 'SUMMONED_PANEL_INIT', payload: word.text })\n    }\n  }\n}\n"
  },
  {
    "path": "src/content/redux/modules/action-catalog.ts",
    "content": "import { CreateActionCatalog } from 'retux'\nimport { AppConfig, DictID } from '@/app-config'\nimport { Profile, ProfileIDList } from '@/app-config/profiles'\nimport { Message } from '@/typings/message'\nimport { Word } from '@/_helpers/record-manager'\nimport { DictSearchResult } from '@/components/dictionaries/helpers'\n\nexport type ActionCatalog = CreateActionCatalog<{\n  NEW_CONFIG: {\n    payload: AppConfig\n  }\n\n  NEW_PROFILES: {\n    payload: ProfileIDList\n  }\n\n  NEW_ACTIVE_PROFILE: {\n    payload: Profile\n  }\n\n  NEW_SELECTION: {\n    payload: Message<'SELECTION'>['payload']\n  }\n\n  WINDOW_RESIZE: {}\n\n  /** Is App temporary disabled */\n  TEMP_DISABLED_STATE: {\n    payload: boolean\n  }\n\n  /** Click or hover on salad bowl */\n  BOWL_ACTIVATED: {}\n\n  /* ------------------------------------------------ *\\\n     Dict Panel\n  \\* ------------------------------------------------ */\n\n  UPDATE_TEXT: {\n    payload: string\n  }\n\n  TOGGLE_MTA_BOX: {}\n\n  TOGGLE_WAVEFORM_BOX: {}\n\n  TOGGLE_PIN: {}\n\n  /** Focus button on quick search panel */\n  TOGGLE_QS_FOCUS: {}\n\n  OPEN_PANEL: {\n    payload: {\n      x: number\n      y: number\n    }\n  }\n\n  CLOSE_PANEL: {}\n\n  SWITCH_HISTORY: {\n    payload: 'prev' | 'next'\n  }\n\n  /** Is current word in Notebook */\n  WORD_IN_NOTEBOOK: {\n    payload: boolean\n  }\n\n  // Add the latest history item to Notebook\n  ADD_TO_NOTEBOOK: {}\n\n  SEARCH_START: {\n    payload?: {\n      /** Search with specific dict */\n      id?: DictID\n      /** Search specific word */\n      word?: Word\n      /** Additional payload passed to search engine */\n      payload?: any\n      /** Do not update search history */\n      noHistory?: boolean\n    }\n  }\n\n  SEARCH_END: {\n    payload: {\n      id: DictID\n      result: any\n      catalog?: DictSearchResult<DictID>['catalog']\n    }\n  }\n\n  UPDATE_PANEL_HEIGHT: {\n    payload: {\n      area: 'menubar' | 'mtabox' | 'dictlist' | 'waveformbox'\n      height: number\n      /** independent layer */\n      floatHeight?: number\n    }\n  }\n\n  /** User manually folds or unfolds dict item */\n  USER_FOLD_DICT: {\n    payload: {\n      id: DictID\n      fold: boolean\n    }\n  }\n\n  DRAG_START_COORD: {\n    payload: null | {\n      x: number\n      y: number\n    }\n  }\n\n  /* ------------------------------------------------ *\\\n    Quick Search Dict Panel\n  \\* ------------------------------------------------ */\n\n  SUMMONED_PANEL_INIT: {\n    /** search text */\n    payload: string\n  }\n\n  QS_PANEL_CHANGED: {\n    payload: boolean\n  }\n\n  OPEN_QS_PANEL: {}\n\n  /* ------------------------------------------------ *\\\n     Word Editor Panel\n  \\* ------------------------------------------------ */\n\n  WORD_EDITOR_STATUS: {\n    payload: {\n      word: Word | null\n      /** translate context when word editor shows */\n      translateCtx?: boolean\n    }\n  }\n\n  /* ------------------------------------------------ *\\\n     Others\n  \\* ------------------------------------------------ */\n\n  PLAY_AUDIO: {\n    payload: {\n      src: string\n      timestamp: number\n    }\n  }\n}>\n"
  },
  {
    "path": "src/content/redux/modules/action-handlers/index.ts",
    "content": "import { ActionHandlers } from 'retux'\nimport {\n  isStandalonePage,\n  isPopupPage,\n  isQuickSearchPage,\n  isOptionsPage\n} from '@/_helpers/saladict'\nimport { State } from '../state'\nimport { ActionCatalog } from '../action-catalog'\nimport { searchStart } from './search-start'\nimport { newSelection } from './new-selection'\nimport { openQSPanel } from './open-qs-panel'\n\nexport const actionHandlers: ActionHandlers<State, ActionCatalog> = {\n  NEW_CONFIG: (state, { payload }) => {\n    const url = window.location.href\n    const panelMaxHeight =\n      (window.innerHeight * payload.panelMaxHeightRatio) / 100\n\n    return {\n      ...state,\n      config: payload,\n      panelHeight: Math.min(state.panelHeight, panelMaxHeight),\n      panelMaxHeight,\n      isQSFocus: payload.qsFocus,\n      isTempDisabled:\n        payload.blacklist.some(([r]) => new RegExp(r).test(url)) &&\n        payload.whitelist.every(([r]) => !new RegExp(r).test(url))\n    }\n  },\n\n  NEW_PROFILES: (state, { payload }) => ({\n    ...state,\n    profiles: payload\n  }),\n\n  NEW_ACTIVE_PROFILE: (state, { payload }) => {\n    const isShowMtaBox = payload.mtaAutoUnfold !== 'hide'\n    return {\n      ...state,\n      activeProfile: payload,\n      isShowMtaBox,\n      isExpandMtaBox:\n        isShowMtaBox &&\n        (payload.mtaAutoUnfold === 'once' ||\n          payload.mtaAutoUnfold === 'always' ||\n          (payload.mtaAutoUnfold === 'popup' && isPopupPage())),\n      renderedDicts: state.renderedDicts.filter(({ id }) =>\n        payload.dicts.selected.includes(id)\n      )\n    }\n  },\n\n  NEW_SELECTION: newSelection,\n\n  WINDOW_RESIZE: state => ({\n    ...state,\n    panelMaxHeight:\n      (window.innerHeight * state.config.panelMaxHeightRatio) / 100\n  }),\n\n  TEMP_DISABLED_STATE: (state, { payload }) =>\n    payload\n      ? {\n          ...state,\n          isTempDisabled: true,\n          isPinned: false,\n          // keep showing if it's standalone page\n          isShowDictPanel: isStandalonePage(),\n          isShowBowl: false,\n          // also reset quick search panel state\n          isQSPanel: isQuickSearchPage()\n        }\n      : {\n          ...state,\n          isTempDisabled: false\n        },\n\n  BOWL_ACTIVATED: state => ({\n    ...state,\n    isShowBowl: false,\n    isShowDictPanel: true,\n    isPinned: state.config.defaultPinned\n  }),\n\n  UPDATE_TEXT: (state, { payload }) => ({\n    ...state,\n    text: payload\n  }),\n\n  TOGGLE_MTA_BOX: state => ({\n    ...state,\n    isExpandMtaBox: !state.isExpandMtaBox\n  }),\n\n  TOGGLE_PIN: state => ({\n    ...state,\n    isPinned: !state.isPinned\n  }),\n\n  TOGGLE_QS_FOCUS: state => ({\n    ...state,\n    isQSFocus: !state.isQSFocus\n  }),\n\n  TOGGLE_WAVEFORM_BOX: state => ({\n    ...state,\n    isExpandWaveformBox: !state.isExpandWaveformBox\n  }),\n\n  OPEN_PANEL: (state, { payload }) =>\n    isStandalonePage()\n      ? state\n      : {\n          ...state,\n          isPinned: state.config.defaultPinned,\n          isShowDictPanel: true,\n          dictPanelCoord: {\n            x: payload.x,\n            y: payload.y\n          }\n        },\n\n  CLOSE_PANEL: state =>\n    isStandalonePage()\n      ? state\n      : {\n          ...state,\n          isPinned: false,\n          isShowBowl: false,\n          isShowDictPanel: false,\n          isQSPanel: isQuickSearchPage()\n        },\n\n  SWITCH_HISTORY: (state, { payload }) => {\n    const historyIndex = Math.min(\n      Math.max(0, state.historyIndex + (payload === 'prev' ? -1 : 1)),\n      state.searchHistory.length - 1\n    )\n\n    return {\n      ...state,\n      historyIndex,\n      text: state.searchHistory[historyIndex]\n        ? state.searchHistory[historyIndex].text\n        : state.text\n    }\n  },\n\n  WORD_IN_NOTEBOOK: (state, { payload }) => ({\n    ...state,\n    isFav: payload\n  }),\n\n  ADD_TO_NOTEBOOK: state =>\n    state.config.editOnFav && !isStandalonePage()\n      ? state\n      : {\n          ...state,\n          // epic will set this back to false if transation failed\n          isFav: true\n        },\n\n  SEARCH_START: searchStart,\n\n  SEARCH_END: (state, { payload }) => {\n    if (state.renderedDicts.every(({ id }) => id !== payload.id)) {\n      // this dict is for auto-pronunciation only\n      return state\n    }\n\n    return {\n      ...state,\n      renderedDicts: state.renderedDicts.map(d =>\n        d.id === payload.id\n          ? {\n              id: d.id,\n              searchStatus: 'FINISH',\n              searchResult: payload.result,\n              catalog: payload.catalog\n            }\n          : d\n      )\n    }\n  },\n\n  UPDATE_PANEL_HEIGHT: (state, { payload }) => {\n    const { _panelHeightCache } = state\n    const sum =\n      _panelHeightCache.sum - _panelHeightCache[payload.area] + payload.height\n    const floatHeight =\n      payload.floatHeight == null\n        ? _panelHeightCache.floatHeight\n        : payload.floatHeight\n\n    return {\n      ...state,\n      panelHeight: Math.min(Math.max(sum, floatHeight), state.panelMaxHeight),\n      _panelHeightCache: {\n        ..._panelHeightCache,\n        [payload.area]: payload.height,\n        sum,\n        floatHeight\n      }\n    }\n  },\n\n  USER_FOLD_DICT: (state, { payload }) => ({\n    ...state,\n    userFoldedDicts: {\n      ...state.userFoldedDicts,\n      [payload.id]: payload.fold\n    }\n  }),\n\n  DRAG_START_COORD: (state, { payload }) => ({\n    ...state,\n    dragStartCoord: payload\n  }),\n\n  SUMMONED_PANEL_INIT: (state, { payload }) => ({\n    ...state,\n    text: payload,\n    historyIndex: 0,\n    isPinned: state.config.defaultPinned,\n    isShowDictPanel: true,\n    isShowBowl: false\n  }),\n\n  QS_PANEL_CHANGED: (state, { payload }) => {\n    if (state.withQssaPanel === payload) {\n      return state\n    }\n\n    // hide panel on other pages and leave just quick search panel\n    return payload && state.config.qssaPageSel\n      ? {\n          ...state,\n          withQssaPanel: payload,\n          isPinned: false,\n          // no hiding if it's browser action page\n          isShowDictPanel:\n            isPopupPage() || (isOptionsPage() ? state.isShowDictPanel : false),\n          isShowBowl: false,\n          isQSPanel: false\n        }\n      : {\n          ...state,\n          withQssaPanel: payload,\n          isQSPanel: isQuickSearchPage()\n        }\n  },\n\n  OPEN_QS_PANEL: openQSPanel,\n\n  WORD_EDITOR_STATUS: (state, { payload: { word, translateCtx } }) =>\n    word\n      ? {\n          ...state,\n          wordEditor: {\n            isShow: true,\n            word,\n            translateCtx: !!translateCtx\n          },\n          dictPanelCoord: {\n            x: 50,\n            y: window.innerHeight * 0.2\n          }\n        }\n      : {\n          ...state,\n          wordEditor: {\n            isShow: false,\n            word: state.wordEditor.word,\n            translateCtx: false\n          }\n        },\n\n  PLAY_AUDIO: (state, { payload }) => ({\n    ...state,\n    lastPlayAudio: payload\n  })\n}\n"
  },
  {
    "path": "src/content/redux/modules/action-handlers/new-selection.ts",
    "content": "import { ActionHandler } from 'retux'\nimport { isStandalonePage, isOptionsPage } from '@/_helpers/saladict'\nimport { Mutable } from '@/typings/helpers'\nimport { State } from '../state'\nimport { ActionCatalog } from '../action-catalog'\n\nexport const newSelection: ActionHandler<\n  State,\n  ActionCatalog,\n  'NEW_SELECTION'\n> = (state, { payload: selection }) => {\n  // Skip selection inside panel\n  if (selection.self) return state\n\n  const { config } = state\n\n  const newState: Mutable<typeof state> = {\n    ...state,\n    selection\n  }\n\n  if (isOptionsPage()) {\n    return newState\n  }\n\n  if (selection.word) {\n    if (selection.force) {\n      newState.dictPanelCoord = {\n        x: selection.mouseX,\n        y: selection.mouseY\n      }\n    } else if (!state.isPinned) {\n      // icon position       10px  panel position\n      //           +-------+      +------------------------+\n      //           |       |      |                        |\n      //           |       | 30px |                        |\n      //      50px +-------+      |                        |\n      //           |  30px        |                        |\n      //     20px  |              |                        |\n      //     +-----+              |                        |\n      // cursor\n      const iconWidth = 30\n      const scrollbarWidth = 10\n\n      newState.bowlCoord = {\n        x: selection.mouseX + config.bowlOffsetX,\n        y: selection.mouseY + config.bowlOffsetY\n      }\n\n      if (newState.bowlCoord.x < 30) {\n        newState.bowlCoord.x = 30\n      } else if (\n        newState.bowlCoord.x + iconWidth + 30 + scrollbarWidth >\n        window.innerWidth\n      ) {\n        newState.bowlCoord.x =\n          window.innerWidth - iconWidth - scrollbarWidth - 30\n      }\n\n      if (newState.bowlCoord.y < 30) {\n        newState.bowlCoord.y = 30\n      } else if (newState.bowlCoord.y + iconWidth + 30 > window.innerHeight) {\n        newState.bowlCoord.y = window.innerHeight - iconWidth - 30\n      }\n\n      newState.dictPanelCoord = {\n        x: newState.bowlCoord.x + iconWidth + 10,\n        y: newState.bowlCoord.y\n      }\n\n      if (\n        newState.dictPanelCoord.x + newState.config.panelWidth + 20 >\n        window.innerWidth\n      ) {\n        // right overflow\n        newState.dictPanelCoord.x =\n          newState.bowlCoord.x - 10 - newState.config.panelWidth\n      }\n    }\n  }\n\n  if ((state.withQssaPanel && config.qssaPageSel) || isStandalonePage()) {\n    return newState\n  }\n\n  const isActive = config.active && !state.isTempDisabled\n\n  const { direct, holding, double, icon } = config.mode\n\n  newState.isShowDictPanel = Boolean(\n    state.isPinned ||\n      (isActive &&\n        selection.word &&\n        selection.word.text &&\n        (state.isShowDictPanel ||\n          direct ||\n          (double && selection.dbClick) ||\n          (holding.alt && selection.altKey) ||\n          (holding.shift && selection.shiftKey) ||\n          (holding.ctrl && selection.ctrlKey) ||\n          (holding.meta && selection.metaKey) ||\n          selection.instant)) ||\n      isStandalonePage()\n  )\n\n  newState.isShowBowl = Boolean(\n    isActive &&\n      selection.word &&\n      selection.word.text &&\n      icon &&\n      !newState.isShowDictPanel &&\n      !direct &&\n      !(double && selection.dbClick) &&\n      !(holding.alt && selection.altKey) &&\n      !(holding.shift && selection.shiftKey) &&\n      !(holding.ctrl && selection.ctrlKey) &&\n      !(holding.meta && selection.metaKey) &&\n      !selection.instant &&\n      !isStandalonePage()\n  )\n\n  return newState\n}\n\nexport default newSelection\n"
  },
  {
    "path": "src/content/redux/modules/action-handlers/open-qs-panel.ts",
    "content": "import { ActionHandler } from 'retux'\nimport { State } from '../state'\nimport { ActionCatalog } from '../action-catalog'\n\nexport const openQSPanel: ActionHandler<\n  State,\n  ActionCatalog,\n  'OPEN_QS_PANEL'\n> = state => {\n  const { panelWidth, tripleCtrl, qsLocation } = state.config\n\n  if (!tripleCtrl || state.isShowDictPanel) {\n    return state\n  }\n\n  let x = 10\n  let y = 10\n\n  switch (qsLocation) {\n    case 'CENTER':\n      x = (window.innerWidth - panelWidth) / 2\n      y = window.innerHeight * 0.3\n      break\n    case 'TOP':\n      x = (window.innerWidth - panelWidth) / 2\n      y = 10\n      break\n    case 'RIGHT':\n      x = window.innerWidth - panelWidth - 30\n      y = window.innerHeight * 0.3\n      break\n    case 'BOTTOM':\n      x = (window.innerWidth - panelWidth) / 2\n      y = window.innerHeight - 10\n      break\n    case 'LEFT':\n      x = 10\n      y = window.innerHeight * 0.3\n      break\n    case 'TOP_LEFT':\n      x = 10\n      y = 10\n      break\n    case 'TOP_RIGHT':\n      x = window.innerWidth - panelWidth - 30\n      y = 10\n      break\n    case 'BOTTOM_LEFT':\n      x = 10\n      y = window.innerHeight - 10\n      break\n    case 'BOTTOM_RIGHT':\n      x = window.innerWidth - panelWidth - 30\n      y = window.innerHeight - 10\n      break\n  }\n\n  return {\n    ...state,\n    isQSPanel: true,\n    isShowDictPanel: true,\n    dictPanelCoord: { x, y }\n  }\n}\n\nexport default openQSPanel\n"
  },
  {
    "path": "src/content/redux/modules/action-handlers/search-start.ts",
    "content": "import { ActionHandler } from 'retux'\nimport { checkSupportedLangs, countWords } from '@/_helpers/lang-check'\nimport { isPopupPage } from '@/_helpers/saladict'\nimport { Word } from '@/_helpers/record-manager'\nimport { State } from '../state'\nimport { ActionCatalog } from '../action-catalog'\n\nexport const searchStart: ActionHandler<\n  State,\n  ActionCatalog,\n  'SEARCH_START'\n> = (state, { payload }) => {\n  const { activeProfile, searchHistory, historyIndex } = state\n\n  let word: Word\n  const newSearchHistory: Word[] =\n    payload && payload.noHistory\n      ? searchHistory\n      : searchHistory.slice(0, historyIndex + 1)\n  let newHistoryIndex = historyIndex\n\n  if (payload && payload.word) {\n    word = payload.word\n    const lastWord = searchHistory[historyIndex]\n\n    if (!payload.noHistory && (!lastWord || lastWord.text !== word.text)) {\n      newSearchHistory.push(word)\n      newHistoryIndex = newSearchHistory.length - 1\n    }\n  } else {\n    word = searchHistory[historyIndex]\n  }\n\n  if (!word) {\n    if (process.env.DEBUG) {\n      console.warn(`SEARCH_START: Empty word on first search`, payload)\n    }\n    return state\n  }\n\n  return {\n    ...state,\n    text: word.text,\n    isShowDictPanel: true,\n    isExpandMtaBox:\n      activeProfile.mtaAutoUnfold === 'always' ||\n      (activeProfile.mtaAutoUnfold === 'popup' && isPopupPage()),\n    searchHistory: newSearchHistory,\n    historyIndex: newHistoryIndex,\n    renderedDicts:\n      payload && payload.id\n        ? // expand an folded dict item\n          state.renderedDicts.map(d =>\n            d.id === payload.id\n              ? {\n                  id: d.id,\n                  searchStatus: 'SEARCHING',\n                  searchResult: null\n                }\n              : d\n          )\n        : activeProfile.dicts.selected\n            .filter(id => {\n              // dicts that should be rendered\n              const dict = activeProfile.dicts.all[id]\n              if (checkSupportedLangs(dict.selectionLang, word.text)) {\n                const wordCount = countWords(word.text)\n                const { min, max } = dict.selectionWC\n                return wordCount >= min && wordCount <= max\n              }\n              return false\n            })\n            .map(id => {\n              // fold or unfold\n              return {\n                id,\n                searchStatus:\n                  checkSupportedLangs(\n                    activeProfile.dicts.all[id].defaultUnfold,\n                    word.text\n                  ) &&\n                  (!state.activeProfile.stickyFold ||\n                    !state.userFoldedDicts[id])\n                    ? 'SEARCHING'\n                    : 'IDLE',\n                searchResult: null\n              }\n            })\n  }\n}\n\nexport default searchStart\n"
  },
  {
    "path": "src/content/redux/modules/index.ts",
    "content": "import { Action, ActionType, createReducer } from 'retux'\nimport {\n  ThunkAction as CreateThunkAction,\n  ThunkDispatch as CreateThunkDispatch\n} from 'redux-thunk'\nimport { initState, State } from './state'\nimport { ActionCatalog } from './action-catalog'\nimport { actionHandlers } from './action-handlers'\n\nexport type StoreState = State\n\nexport type StoreActionCatalog = ActionCatalog\n\nexport type StoreActionType = ActionType<StoreActionCatalog>\n\nexport type StoreAction<T extends StoreActionType = StoreActionType> = Action<\n  StoreActionCatalog,\n  T\n>\n\nexport type ThunkAction<\n  Type extends StoreActionType = StoreActionType,\n  Result = void\n> = CreateThunkAction<\n  Result,\n  StoreState,\n  never,\n  Action<StoreActionCatalog, Type>\n>\n\nexport type StoreDispatch<\n  Type extends StoreActionType = StoreActionType\n> = CreateThunkDispatch<StoreState, never, StoreAction<Type>>\n\nexport const getRootReducer = async () => {\n  return createReducer(await initState(), actionHandlers)\n}\n"
  },
  {
    "path": "src/content/redux/modules/state.ts",
    "content": "import { PromiseType } from 'utility-types'\nimport { newWord, Word } from '@/_helpers/record-manager'\nimport { DictID } from '@/app-config'\nimport { getConfig } from '@/_helpers/config-manager'\nimport { getProfileIDList, getActiveProfile } from '@/_helpers/profile-manager'\nimport {\n  isQuickSearchPage,\n  isStandalonePage,\n  isOptionsPage,\n  isPopupPage\n} from '@/_helpers/saladict'\nimport { DictSearchResult } from '@/components/dictionaries/helpers'\n\nexport const initState = async () => {\n  const pConfig = getConfig()\n  const pProfiles = getProfileIDList()\n  const pActiveProfile = getActiveProfile()\n\n  const config = await pConfig\n  const profiles = await pProfiles\n  const activeProfile = await pActiveProfile\n\n  const url = window.location.href\n\n  const isShowMtaBox = activeProfile.mtaAutoUnfold !== 'hide'\n\n  return {\n    config,\n    profiles,\n    activeProfile,\n    selection: {\n      word: newWord() as Word | null,\n      mouseX: 0,\n      mouseY: 0,\n      self: false,\n      dbClick: false,\n      altKey: false,\n      shiftKey: false,\n      ctrlKey: false,\n      metaKey: false,\n      instant: false,\n      force: false\n    },\n    /** Temporary disable Saladict */\n    isTempDisabled:\n      config.blacklist.some(([r]) => new RegExp(r).test(url)) &&\n      config.whitelist.every(([r]) => !new RegExp(r).test(url)),\n    /**\n     * Is current panel a Quick Search Panel,\n     * which could be in a standalone window or in-page element.\n     */\n    isQSPanel: isQuickSearchPage(),\n    isQSFocus: config.qsFocus,\n    /** is a standalone quick search panel running */\n    withQssaPanel: false,\n    wordEditor: {\n      isShow: false,\n      word: newWord(),\n      // translate context on start\n      translateCtx: false\n    },\n    isShowBowl: false,\n    isShowDictPanel: isStandalonePage(),\n    isShowMtaBox,\n    isExpandMtaBox:\n      isShowMtaBox &&\n      (activeProfile.mtaAutoUnfold === 'once' ||\n        activeProfile.mtaAutoUnfold === 'always' ||\n        (activeProfile.mtaAutoUnfold === 'popup' && isPopupPage())),\n    isExpandWaveformBox: false,\n    isPinned: false,\n    /** Is current word in Notebook */\n    isFav: false,\n    bowlCoord: { x: 0, y: 0 },\n    /** The actual coord of dict panel might be different */\n    dictPanelCoord: isOptionsPage()\n      ? { x: window.innerWidth - config.panelWidth - 20, y: 80 }\n      : { x: 0, y: 0 },\n    panelHeight: 30,\n    _panelHeightCache: {\n      menubar: 30,\n      mtabox: 0,\n      dictlist: 0,\n      waveformbox: 0,\n      sum: 30,\n      /** independent layer */\n      floatHeight: 0\n    },\n    panelMaxHeight: (window.innerHeight * config.panelMaxHeightRatio) / 100,\n    /** Dicts that will be rendered to dict panel */\n    renderedDicts: [] as {\n      readonly id: DictID\n      readonly searchStatus: 'IDLE' | 'SEARCHING' | 'FINISH'\n      readonly searchResult: any\n      readonly catalog?: DictSearchResult<DictID>['catalog']\n    }[],\n    /** User manually folded or unfolded */\n    userFoldedDicts: {} as { [id in DictID]?: boolean },\n    /** Search text */\n    text: '',\n    /** 0 is the oldest */\n    searchHistory: [] as Word[],\n    /** User can view back search history */\n    historyIndex: -1,\n    /** Record init coordinate on dragstart */\n    dragStartCoord: null as null | { x: number; y: number },\n    lastPlayAudio: null as null | { src: string; timestamp: number }\n  }\n}\n\nexport type State = PromiseType<ReturnType<typeof initState>>\n\nexport default initState\n"
  },
  {
    "path": "src/history/env.ts",
    "content": "export {}\n\nwindow.__SALADICT_INTERNAL_PAGE__ = true\n"
  },
  {
    "path": "src/history/index.tsx",
    "content": "import './env'\nimport '@/selection'\n\nimport React from 'react'\nimport { WordPage } from '@/components/WordPage'\nimport { initAntdRoot } from '@/components/AntdRoot'\n\ndocument.title = 'Saladict History'\n\ninitAntdRoot(() => <WordPage area=\"history\" />, '/wordpage/history')\n"
  },
  {
    "path": "src/manifest/chrome.manifest.json",
    "content": "{\n  \"background\": {\n    \"persistent\": true\n  },\n  \"options_ui\": {\n    \"chrome_style\": false\n  },\n  \"optional_permissions\": [\n    \"background\"\n  ],\n  \"content_security_policy\": \"script-src 'self' chrome-extension://hfjbmagddngcpeloejdejnfgbamkjaeg/ chrome-extension://aibcglbfblnogfjhbcmmpobjhnomhcdo/; object-src 'self'\",\n  \"incognito\": \"split\",\n  \"update_url\": \"https://clients2.google.com/service/update2/crx\",\n  \"minimum_chrome_version\": \"63\"\n}\n"
  },
  {
    "path": "src/manifest/common.manifest.js",
    "content": "module.exports = {\n  manifest_version: 2,\n\n  homepage_url: 'https://saladict.crimx.com/',\n\n  minimum_chrome_version: '55',\n\n  name: '__MSG_extension_name__',\n  short_name: '__MSG_extension_short_name__',\n  description: '__MSG_extension_description__',\n\n  default_locale: 'zh_CN',\n\n  icons: {\n    '16': 'assets/icon-16.png',\n    '48': 'assets/icon-48.png',\n    '128': 'assets/icon-128.png'\n  },\n\n  commands: {\n    'toggle-active': {\n      description: '__MSG_command_toggle_active__'\n    },\n    'toggle-instant': {\n      description: '__MSG_command_toggle_instant__'\n    },\n    'search-clipboard': {\n      description: '__MSG_command_search_clipboard__'\n    },\n    'open-pdf': {\n      description: '__MSG_command_open_pdf__'\n    },\n    'open-quick-search': {\n      description: '__MSG_command_open_quick_search__'\n    },\n    'open-youdao': {\n      description: '__MSG_command_open_youdao__'\n    },\n    'open-google': {\n      description: '__MSG_command_open_google__'\n    },\n    'open-caiyun': {\n      description: '__MSG_command_open_caiyun__'\n    },\n    'next-history': {\n      description: '__MSG_command_next_history__'\n    },\n    'prev-history': {\n      description: '__MSG_command_prev_history__'\n    },\n    'next-profile': {\n      description: '__MSG_command_next_profile__'\n    },\n    'prev-profile': {\n      description: '__MSG_command_prev_profile__'\n    },\n    'profile-1': {\n      description: '__MSG_command_profile_1__'\n    },\n    'profile-2': {\n      description: '__MSG_command_profile_2__'\n    },\n    'profile-3': {\n      description: '__MSG_command_profile_3__'\n    },\n    'profile-4': {\n      description: '__MSG_command_profile_4__'\n    },\n    'profile-5': {\n      description: '__MSG_command_profile_5__'\n    },\n    'add-notebook': {\n      description: '__MSG_command_add_notebook__'\n    }\n  },\n\n  web_accessible_resources: [\n    'assets/*',\n    'audio-control.html',\n    'quick-search.html'\n  ],\n\n  permissions: [\n    '<all_urls>',\n    'alarms',\n    'contextMenus',\n    'cookies',\n    'notifications',\n    'storage',\n    'tabs',\n    'unlimitedStorage',\n    'webRequest',\n    'webRequestBlocking'\n  ],\n\n  optional_permissions: ['clipboardRead', 'clipboardWrite'],\n\n  content_security_policy: \"script-src 'self'; object-src 'self'\"\n}\n"
  },
  {
    "path": "src/manifest/edge.manifest.json",
    "content": "{\n  \"background\": {\n    \"persistent\": true\n  },\n  \"options_ui\": {\n    \"chrome_style\": false\n  },\n  \"optional_permissions\": [\n    \"background\"\n  ],\n  \"content_security_policy\": \"script-src 'self' chrome-extension://hfjbmagddngcpeloejdejnfgbamkjaeg/ chrome-extension://aibcglbfblnogfjhbcmmpobjhnomhcdo/; object-src 'self'\",\n  \"incognito\": \"split\",\n  \"update_url\": \"https://edge.microsoft.com/extensionwebstorebase/v1/crx\"\n}\n"
  },
  {
    "path": "src/manifest/firefox.manifest.json",
    "content": "{\n  \"options_ui\": {\n    \"browser_style\": false\n  },\n  \"applications\": {\n    \"gecko\": {\n      \"id\": \"saladict@crimx.com\",\n      \"strict_min_version\": \"67.0\"\n    }\n  },\n  \"content_security_policy\": \"script-src 'self'; object-src 'self'\"\n}\n"
  },
  {
    "path": "src/manifest/safari.manifest.json",
    "content": "{\n  \"background\": {\n    \"persistent\": true\n  },\n  \"options_ui\": {\n    \"chrome_style\": false\n  },\n  \"optional_permissions\": [\n    \"background\"\n  ],\n  \"incognito\": \"split\"\n}\n"
  },
  {
    "path": "src/notebook/env.ts",
    "content": "export {}\n\nwindow.__SALADICT_INTERNAL_PAGE__ = true\n"
  },
  {
    "path": "src/notebook/index.tsx",
    "content": "import './env'\nimport '@/selection'\n\nimport React from 'react'\nimport { WordPage } from '@/components/WordPage'\nimport { initAntdRoot } from '@/components/AntdRoot'\n\ndocument.title = 'Saladict Notebook'\n\ninitAntdRoot(() => <WordPage area=\"notebook\" />, '/wordpage/notebook')\n"
  },
  {
    "path": "src/options/__fake__/env.ts",
    "content": "import { initConfig } from '@/_helpers/config-manager'\nimport { initProfiles } from '@/_helpers/profile-manager'\nimport { browser } from '../../../test/helper'\nimport packagejson from '../../../package.json'\n\nbrowser.runtime.getManifest.callsFake(() => ({\n  version: packagejson.version\n}))\n\nbrowser.permissions.contains.callsFake(() => Promise.resolve(true))\nbrowser.permissions.request.callsFake(() => Promise.resolve(false))\n\ninitConfig()\ninitProfiles()\n"
  },
  {
    "path": "src/options/_style.scss",
    "content": "/* ======================================= *\\\n * Antd Patch\n\\* ======================================= */\n\nbody {\n  // prevent layout shaking when switching entries\n  overflow-y: scroll;\n}\n\n.ant-form-item.form-item-inline {\n  display: inline-block;\n  margin: 0 10px 0 0;\n\n  &:last-of-type {\n    margin-right: 0;\n  }\n}\n\n.ant-form-item-label {\n  white-space: normal !important;\n\n  > label {\n    margin-top: 3px !important;\n    align-items: flex-start !important;\n    height: auto !important;\n\n    &.ant-form-item-required::before {\n      display: none !important;\n      line-height: unset !important;\n    }\n  }\n}\n\n\n.sortable-list-item {\n  width: 100%;\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n\n.ant-radio-group.sortable-list-radio-group {\n  display: block;\n  margin-bottom: 10;\n}\n\n.ant-btn.sortable-list-item-btn {\n  margin-left: 10px;\n  padding: 0;\n  line-height: 1;\n  border: none;\n}\n\n/* ======================================= *\\\n * 快捷键 <kbd>\n\\* ======================================= */\n\nkbd {\n  position: relative;\n  top: -0.3em;\n  display: inline-block;\n  padding: 0.25em 0.5em 0.2em;\n  margin-left: 0.25em;\n  margin-right: 0.25em;\n  font: 75%/1 monaco, menlo, consolas, 'courier new', courier, monospace;\n  border: solid 1px #ccc;\n  border-bottom-color: #bbb;\n  border-radius: 3px;\n  white-space: nowrap;\n  word-wrap: normal;\n  text-transform: capitalize; // 首字母大写\n\n  color: #555;\n  background-color: #fefefe;\n  background-image: linear-gradient(\n    to bottom,\n    rgba(0, 0, 0, 0.05),\n    rgba(0, 0, 0, 0)\n  );\n  box-shadow: 0 2px 0 #ccc, 0 3px 1px #999, inset 0 1px 1px #fff;\n}\n\n.dark-mode kbd,\n.dark kbd,\nkbd.dark {\n  color: #fdfdfd;\n  text-shadow: 0 -1px 0 #000;\n  border-color: #000;\n  background-color: #4d4c4c;\n  background-image: linear-gradient(\n    rgba(0, 0, 0, 0.5),\n    rgba(0, 0, 0, 0) 80%,\n    rgba(0, 0, 0, 0)\n  );\n  box-shadow: 0 2px 0 #000, 0 3px 1px #999, inset 0 1px 1px #aaa,\n    inset 0 -1px 3px #272727;\n}\n\nkbd kbd {\n  padding: 0;\n  font-size: 100%;\n  font-weight: bold;\n  box-shadow: none;\n}\n\n// Mac 修饰键符号\n// https://support.apple.com/kb/PH10564?locale=zh_CN\n// [Mac——如何输入⌘、⌥、⇧、⌃、⎋等特殊字符](http://softu.cn/447)\n\nkbd[data-key]:after {\n  font-family: 'Myriad Set Pro', 'Helvetica Neue', 'Helvetica', 'Arial',\n    'Verdana', 'sans-serif';\n}\n\nkbd[data-key='command']:after {\n  content: ' ⌘';\n}\n\nkbd[data-key='cmd']:after {\n  content: ' ⌘';\n}\n\nkbd[data-key='shift']:after {\n  content: ' ⇧';\n}\n\nkbd[data-key='control']:after {\n  content: ' ⌃';\n}\n\nkbd[data-key='option']:after {\n  content: ' ⌥';\n}\n\nkbd[data-key='capslock']:after {\n  content: ' ⇪';\n}\n\nkbd[data-key='caps lock']:after {\n  content: ' ⇪';\n}\n\nkbd[data-key='escape']:after {\n  content: ' ⎋';\n}\n\nkbd[data-key='esc']:after {\n  content: ' ⎋';\n}\n\nkbd[data-key='return']:after {\n  content: ' ↩';\n}\n\nkbd[data-key='enter']:after {\n  content: ' ↩';\n}\n\nkbd[data-key='delete']:after {\n  content: ' ⌫';\n}\n\nkbd[data-key='eject']:after {\n  content: ' ⏏';\n}\n\n/* ======================================= *\\\n * Base\n\\* ======================================= */\n\ncode {\n  border: 1px solid #ddd;\n  color: #f14668;\n  background: #f6f6f6;\n  padding: 3px 5px;\n  border-radius: 3px;\n  font-size: 1em;\n}\n\n.saladict-theme-dark {\n  code {\n    border-color: #333;\n    background: #181818;\n  }\n}\n\n/* ======================================= *\\\n * Components\n\\* ======================================= */\n\n.main-entry {\n  --opt-color: rgba(0, 0, 0, 0.65);\n  --opt-background-color: #fff;\n\n  &.dark-mode {\n    --opt-color: hsla(0, 0%, 100%, 0.65);\n    --opt-background-color: #141414;\n  }\n}\n\n.saladict-form-profile-title {\n  cursor: help;\n\n  .anticon {\n    color: #f5222d;\n    margin-right: 0.5em;\n  }\n}\n"
  },
  {
    "path": "src/options/acknowledgement.ts",
    "content": "export type Acknowledgement = Array<{\n  name: string\n  href: string\n  locale: string\n}>\n\nexport const acknowledgement: Acknowledgement = [\n  {\n    name: 'yipanhuasheng',\n    href: 'https://github.com/crimx/ext-saladict/commits?author=yipanhuasheng',\n    locale: 'yipanhuasheng'\n  },\n  {\n    name: 'zhtw2013',\n    href: 'https://github.com/crimx/ext-saladict/commits?author=zhtw2013',\n    locale: 'trans_tw'\n  },\n  {\n    name: 'lwdgit',\n    href: 'https://github.com/crimx/ext-saladict/commits?author=lwdgit',\n    locale: 'shanbay'\n  },\n  {\n    name: 'Wekey',\n    href: 'https://weibo.com/925515171?is_hot=1',\n    locale: 'naver'\n  },\n  {\n    name: 'caerlie',\n    href: 'https://github.com/caerlie',\n    locale: 'weblio'\n  },\n  {\n    name: 'stockyman',\n    href: 'https://github.com/stockyman',\n    locale: 'trans_tw'\n  }\n]\n\nexport default acknowledgement\n"
  },
  {
    "path": "src/options/components/BtnPreview/PreviewIcon.tsx",
    "content": "import React, { FC, ComponentProps } from 'react'\nimport Icon from '@ant-design/icons/lib/components/Icon'\n\nexport const PreviewIcon: FC<ComponentProps<typeof Icon>> = props => {\n  const SVGIcon = () => (\n    <svg width={21} height={21} viewBox=\"0 0 21 21\" fill=\"currentColor\">\n      <g fillRule=\"evenodd\">\n        <g fillRule=\"nonzero\">\n          <path d=\"M7.02 3.635l12.518 12.518a1.863 1.863 0 010 2.635l-1.317 1.318a1.863 1.863 0 01-2.635 0L3.068 7.588A2.795 2.795 0 117.02 3.635zm2.09 14.428a.932.932 0 110 1.864.932.932 0 010-1.864zm-.043-9.747L7.75 9.635l9.154 9.153 1.318-1.317-9.154-9.155zM3.52 12.473c.514 0 .931.417.931.931v.932h.932a.932.932 0 110 1.864h-.932v.931a.932.932 0 01-1.863 0l-.001-.931h-.93a.932.932 0 010-1.864h.93v-.932c0-.514.418-.931.933-.931zm15.374-3.727a1.398 1.398 0 110 2.795 1.398 1.398 0 010-2.795zM4.385 4.953a.932.932 0 000 1.317l2.046 2.047L7.75 7 5.703 4.953a.932.932 0 00-1.318 0zM14.701.36a.932.932 0 01.931.932v.931h.932a.932.932 0 010 1.864h-.933l.001.932a.932.932 0 11-1.863 0l-.001-.932h-.93a.932.932 0 110-1.864h.93v-.931a.932.932 0 01.933-.932z\" />\n        </g>\n      </g>\n    </svg>\n  )\n  return <Icon component={SVGIcon} {...props} />\n}\n"
  },
  {
    "path": "src/options/components/BtnPreview/_style.scss",
    "content": ".btn-preview {\n  position: fixed !important;\n  right: 30px;\n  bottom: 60px;\n  border: none;\n  color: #000 !important;\n  background: #fff !important;\n  box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),\n    0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);\n}\n\n.btn-preview-fade-enter {\n  opacity: 0;\n  transition: opacity 0.5s;\n}\n\n.btn-preview-fade-enter-active,\n.btn-preview-fade-exit {\n  opacity: 1;\n  transition: opacity 0.5s;\n}\n\n.btn-preview-fade-exit-active {\n  opacity: 0;\n  transition: opacity 0.5s;\n}\n"
  },
  {
    "path": "src/options/components/BtnPreview/index.tsx",
    "content": "import React, { FC } from 'react'\nimport { Dispatch } from 'redux'\nimport { useDispatch } from 'react-redux'\nimport { CSSTransition } from 'react-transition-group'\nimport { Button } from 'antd'\nimport { StoreAction } from '@/content/redux/modules'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { newWord } from '@/_helpers/record-manager'\nimport { getWordOfTheDay } from '@/_helpers/wordoftheday'\nimport { useIsShowDictPanel } from '@/options/helpers/panel-store'\nimport { PreviewIcon } from './PreviewIcon'\n\nimport './_style.scss'\n\n// pre-fetch the word\nconst pWordOfTheDay = getWordOfTheDay()\n\nexport const BtnPreview: FC = () => {\n  const { t } = useTranslate('options')\n  const show = !useIsShowDictPanel()\n  const dispatch = useDispatch<Dispatch<StoreAction>>()\n\n  return (\n    <CSSTransition\n      classNames=\"btn-preview-fade\"\n      mountOnEnter\n      unmountOnExit\n      appear\n      in={show}\n      timeout={500}\n    >\n      <div>\n        <Button\n          className=\"btn-preview\"\n          title={t('previewPanel')}\n          shape=\"circle\"\n          size=\"large\"\n          icon={<PreviewIcon />}\n          onClick={async e => {\n            const { x, width } = e.currentTarget.getBoundingClientRect()\n            // panel will adjust the position itself\n            dispatch({ type: 'OPEN_PANEL', payload: { x: x + width, y: 80 } })\n            dispatch({\n              type: 'SEARCH_START',\n              payload: {\n                word: newWord({ text: await pWordOfTheDay })\n              }\n            })\n          }}\n        />\n      </div>\n    </CSSTransition>\n  )\n}\n\nexport const BtnPreviewMemo = React.memo(BtnPreview)\n"
  },
  {
    "path": "src/options/components/Entries/BlackWhiteList.tsx",
    "content": "import React, { FC, useState } from 'react'\nimport { Button } from 'antd'\nimport { SaladictForm } from '@/options/components/SaladictForm'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { MatchPatternModal } from '../MatchPatternModal'\n\nexport const BlackWhiteList: FC = () => {\n  const { t } = useTranslate(['options', 'common'])\n  const [editingArea, setEditingArea] = useState<\n    'pdfWhitelist' | 'pdfBlacklist' | 'whitelist' | 'blacklist' | null\n  >(null)\n\n  return (\n    <>\n      <SaladictForm\n        hideFooter\n        items={[\n          {\n            key: 'BlackWhiteList',\n            label: t('config.opt.sel_blackwhitelist'),\n            help: t('config.opt.sel_blackwhitelist_help'),\n            children: (\n              <>\n                <Button\n                  style={{ marginRight: 10 }}\n                  onClick={() => setEditingArea('blacklist')}\n                >\n                  {t('common:blacklist')}\n                </Button>\n                <Button onClick={() => setEditingArea('whitelist')}>\n                  {t('common:whitelist')}\n                </Button>\n              </>\n            )\n          },\n          {\n            key: 'PDFBlackWhiteList',\n            label: 'PDF ' + t('nav.BlackWhiteList'),\n            help: t('config.opt.pdf_blackwhitelist_help'),\n            children: (\n              <>\n                <Button\n                  style={{ marginRight: 10 }}\n                  onClick={() => setEditingArea('pdfBlacklist')}\n                >\n                  PDF {t('common:blacklist')}\n                </Button>\n                <Button onClick={() => setEditingArea('pdfWhitelist')}>\n                  PDF {t('common:whitelist')}\n                </Button>\n              </>\n            )\n          }\n        ]}\n      />\n      <MatchPatternModal\n        area={editingArea}\n        onClose={() => setEditingArea(null)}\n      />\n    </>\n  )\n}\n"
  },
  {
    "path": "src/options/components/Entries/ContextMenus/AddModal.tsx",
    "content": "import React, { FC, useMemo } from 'react'\nimport { List, Modal, Button, Tooltip } from 'antd'\nimport { CheckOutlined, CloseOutlined, EditOutlined } from '@ant-design/icons'\nimport omit from 'lodash/omit'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { isFirefox } from '@/_helpers/saladict'\nimport { genUniqueKey } from '@/_helpers/uniqueKey'\nimport { useSelector } from '@/content/redux'\nimport { getConfigPath } from '@/options/helpers/path-joiner'\nimport { useUpload } from '@/options/helpers/upload'\n\n/**\n * key: menu id\n * value: reason\n */\nconst unsupportedFeatures: Readonly<{ [id: string]: 'ff' | '' }> = {\n  caiyuntrs: isFirefox ? 'ff' : '',\n  youdao_page_translate: isFirefox ? 'ff' : ''\n}\n\nexport interface AddModalProps {\n  show: boolean\n  onEdit: (menuID: string) => void\n  onClose: () => void\n}\n\nexport const AddModal: FC<AddModalProps> = ({ show, onEdit, onClose }) => {\n  const { t } = useTranslate(['common', 'menus', 'options'])\n  const contextMenus = useSelector(state => state.config.contextMenus)\n  const unselected = useMemo(() => {\n    if (!contextMenus) {\n      return []\n    }\n\n    const selectedSet = new Set(contextMenus.selected as string[])\n    return Object.keys(contextMenus.all).filter(id => !selectedSet.has(id))\n  }, [contextMenus])\n  const upload = useUpload()\n\n  return (\n    <Modal\n      visible={show}\n      title={t('common:add')}\n      destroyOnClose\n      onOk={onClose}\n      onCancel={onClose}\n      footer={null}\n    >\n      <Button type=\"dashed\" block onClick={addItem}>\n        {t('common:add')}\n      </Button>\n      <List dataSource={unselected} renderItem={renderListItem} />\n      <Button type=\"dashed\" block onClick={addItem}>\n        {t('common:add')}\n      </Button>\n    </Modal>\n  )\n\n  function renderListItem(menuID: string) {\n    if (!contextMenus) return null\n\n    const item = contextMenus.all[menuID]\n    const itemName = typeof item === 'string' ? t(`menus:${menuID}`) : item.name\n    return (\n      <List.Item>\n        <div className=\"sortable-list-item\">\n          {itemName}\n          <div>\n            <div>\n              <Tooltip\n                title={\n                  unsupportedFeatures[menuID]\n                    ? t(\n                        `options:unsupportedFeatures.${unsupportedFeatures[menuID]}`,\n                        { feature: itemName }\n                      )\n                    : ''\n                }\n              >\n                <Button\n                  title={t('common:add')}\n                  className=\"sortable-list-item-btn\"\n                  shape=\"circle\"\n                  size=\"small\"\n                  icon={<CheckOutlined />}\n                  disabled={!!unsupportedFeatures[menuID]}\n                  onClick={selectItem}\n                />\n              </Tooltip>\n              <Button\n                title={t('common:edit')}\n                className=\"sortable-list-item-btn\"\n                shape=\"circle\"\n                size=\"small\"\n                icon={<EditOutlined />}\n                disabled={item === 'x' /** internal options */}\n                onClick={() => onEdit(menuID)}\n              />\n              <Button\n                title={t('common:delete')}\n                disabled={item === 'x' /** internal options */}\n                className=\"sortable-list-item-btn\"\n                shape=\"circle\"\n                size=\"small\"\n                icon={<CloseOutlined />}\n                onClick={deleteItem}\n              />\n            </div>\n          </div>\n        </div>\n      </List.Item>\n    )\n\n    function selectItem() {\n      if (!contextMenus) return\n\n      upload({\n        [getConfigPath('contextMenus', 'selected')]: [\n          ...contextMenus.selected,\n          menuID\n        ]\n      })\n    }\n\n    function deleteItem() {\n      if (!contextMenus) return\n\n      Modal.confirm({\n        title: t('common:delete_confirm'),\n        okType: 'danger',\n        onOk: () => {\n          if (contextMenus.all[menuID] !== 'x') {\n            upload({\n              [getConfigPath('contextMenus')]: {\n                ...contextMenus,\n                selected: contextMenus.selected.filter(id => id !== menuID),\n                all: omit(contextMenus.all, [menuID])\n              }\n            })\n          }\n        }\n      })\n    }\n  }\n\n  function addItem() {\n    onEdit(`c_${genUniqueKey()}`)\n  }\n}\n"
  },
  {
    "path": "src/options/components/Entries/ContextMenus/EditeModal.tsx",
    "content": "import React, { FC, useMemo, useRef } from 'react'\nimport { useUpdateEffect } from 'react-use'\nimport { useObservableState } from 'observable-hooks'\nimport { Input, Modal, Form } from 'antd'\nimport { ExclamationCircleOutlined } from '@ant-design/icons'\nimport { FormInstance } from 'antd/lib/form/Form'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { useSelector } from '@/content/redux'\nimport { useUpload, uploadStatus$ } from '@/options/helpers/upload'\n\nexport interface EditModalProps {\n  menuID?: string | null\n  onClose: () => void\n}\n\nexport const EditModal: FC<EditModalProps> = ({ menuID, onClose }) => {\n  const { t } = useTranslate(['options', 'dicts', 'common', 'langcode'])\n  const formRef = useRef<FormInstance>(null)\n  const allMenus = useSelector(state => state.config.contextMenus.all)\n  const uploadStatus = useObservableState(uploadStatus$, 'idle')\n  const upload = useUpload()\n\n  const namePath = `config.contextMenus.all.${menuID}.name`\n  const urlPath = `config.contextMenus.all.${menuID}.url`\n\n  const initialValues = useMemo(() => {\n    if (allMenus && menuID) {\n      const item = allMenus[menuID]\n      if (typeof item === 'string') {\n        return {\n          [namePath]: t(`menus:${menuID}`),\n          [urlPath]: item\n        }\n      }\n      if (item) {\n        return {\n          [namePath]: item.name,\n          [urlPath]: item.url\n        }\n      }\n    }\n    return {\n      [namePath]: '',\n      [urlPath]: ''\n    }\n  }, [allMenus, menuID])\n\n  useUpdateEffect(() => {\n    if (menuID && uploadStatus === 'idle') {\n      onClose()\n    }\n  }, [uploadStatus])\n\n  return (\n    <Modal\n      visible={!!menuID}\n      zIndex={1001}\n      title={t(`config.opt.contextMenus_edit`)}\n      destroyOnClose\n      onOk={submitForm}\n      onCancel={closeModal}\n    >\n      <Form ref={formRef} initialValues={initialValues} onFinish={upload}>\n        <Form.Item name={namePath} label={t('common:name')}>\n          <Input />\n        </Form.Item>\n        <Form.Item name={urlPath} label=\"URL\">\n          <Input />\n        </Form.Item>\n      </Form>\n    </Modal>\n  )\n\n  function submitForm() {\n    if (formRef.current) {\n      formRef.current.submit()\n    }\n  }\n\n  function closeModal() {\n    if (formRef.current && formRef.current.isFieldsTouched()) {\n      Modal.confirm({\n        zIndex: 1002,\n        title: t('syncService.close_confirm'),\n        icon: <ExclamationCircleOutlined />,\n        okType: 'danger',\n        onOk: onClose\n      })\n    } else {\n      onClose()\n    }\n  }\n}\n"
  },
  {
    "path": "src/options/components/Entries/ContextMenus/index.tsx",
    "content": "import React, { FC, useState, useLayoutEffect } from 'react'\nimport { Row, Col } from 'antd'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { useSelector } from '@/content/redux'\nimport { SortableList, reorder } from '@/options/components/SortableList'\nimport { getConfigPath } from '@/options/helpers/path-joiner'\nimport { useListLayout } from '@/options/helpers/layout'\nimport { useUpload } from '@/options/helpers/upload'\nimport { AddModal } from './AddModal'\nimport { EditModal } from './EditeModal'\n\nexport const ContextMenus: FC = () => {\n  const { t } = useTranslate(['options', 'common', 'menus'])\n  const upload = useUpload()\n  const [showAddModal, setShowAddModal] = useState(false)\n  const [editingMenu, setEditingMenu] = useState<string | null>(null)\n  const listLayout = useListLayout()\n  const contextMenus = useSelector(state => state.config.contextMenus)\n  // make a local copy to avoid flickering on drag end\n  const [selectedMenus, setSelectedMenus] = useState<ReadonlyArray<string>>(\n    contextMenus.selected\n  )\n  useLayoutEffect(() => {\n    setSelectedMenus(contextMenus.selected)\n  }, [contextMenus.selected])\n\n  return (\n    <Row>\n      <Col {...listLayout}>\n        <SortableList\n          title={t('nav.ContextMenus')}\n          description={<p>{t('config.opt.contextMenus_description')}</p>}\n          list={selectedMenus.map(id => {\n            const item = contextMenus.all[id]\n            return {\n              value: id,\n              title: typeof item === 'string' ? t(`menus:${id}`) : item.name\n            }\n          })}\n          disableEdit={(index, item) => contextMenus.all[item.value] === 'x'}\n          onAdd={() => setShowAddModal(true)}\n          onEdit={index => {\n            setEditingMenu(selectedMenus[index])\n          }}\n          onDelete={index => {\n            const newList = selectedMenus.slice()\n            newList.splice(index, 1)\n            upload({\n              [getConfigPath('contextMenus', 'selected')]: newList\n            })\n            setSelectedMenus(newList)\n          }}\n          onOrderChanged={(oldIndex, newIndex) => {\n            const newList = reorder(selectedMenus, oldIndex, newIndex)\n            upload({\n              [getConfigPath('contextMenus', 'selected')]: newList\n            })\n            setSelectedMenus(newList)\n          }}\n        />\n      </Col>\n      <AddModal\n        show={showAddModal}\n        onEdit={setEditingMenu}\n        onClose={() => setShowAddModal(false)}\n      />\n      <EditModal menuID={editingMenu} onClose={() => setEditingMenu(null)} />\n    </Row>\n  )\n}\n"
  },
  {
    "path": "src/options/components/Entries/DictAuths.tsx",
    "content": "import React, { FC } from 'react'\nimport { Input } from 'antd'\nimport { useSelector } from '@/content/redux'\nimport { getConfigPath } from '@/options/helpers/path-joiner'\nimport {\n  SaladictForm,\n  SaladictFormItem\n} from '@/options/components/SaladictForm'\nimport { useTranslate, Trans } from '@/_helpers/i18n'\nimport { objectKeys } from '@/typings/helpers'\n\nexport const DictAuths: FC = () => {\n  const { t } = useTranslate(['options', 'dicts'])\n  const dictAuths = useSelector(state => state.config.dictAuth)\n\n  if (dictAuths === null) return null\n\n  const formItems: SaladictFormItem[] = [\n    {\n      key: 'dictauthstitle',\n      label: t('nav.DictAuths'),\n      children: (\n        <span className=\"ant-form-text\">{t('dictAuth.description')}</span>\n      )\n    }\n  ]\n\n  objectKeys(dictAuths).forEach(dictID => {\n    const auth = dictAuths[dictID]!\n    const configPath = getConfigPath('dictAuth', dictID)\n    const title = t(`dicts:${dictID}.name`)\n\n    objectKeys(auth).forEach((key, i, keys) => {\n      const isLast = i + 1 === keys.length\n      formItems.push({\n        name: configPath + '.' + key,\n        label: (\n          <span>\n            {i === 0 ? title + ' ' : ''}\n            <code>{key}</code>\n          </span>\n        ),\n        help: isLast ? (\n          <Trans message={t('dictAuth.dictHelp')}>\n            <a\n              href={require(`@/components/dictionaries/${dictID}/auth.ts`).url}\n              target=\"_blank\"\n              rel=\"nofollow noopener noreferrer\"\n            >\n              {title}\n            </a>\n          </Trans>\n        ) : null,\n        style: { marginBottom: isLast ? 10 : 5 },\n        children: <Input autoComplete=\"off\" />\n      })\n    })\n  })\n\n  return <SaladictForm items={formItems} />\n}\n"
  },
  {
    "path": "src/options/components/Entries/DictPanel.tsx",
    "content": "import React, { FC } from 'react'\nimport { Select, Switch, Input, Slider } from 'antd'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { getProfilePath, getConfigPath } from '@/options/helpers/path-joiner'\nimport {\n  SaladictForm,\n  percentageSlideFormatter,\n  pixelSlideFormatter\n} from '@/options/components/SaladictForm'\n\nexport const DictPanel: FC = () => {\n  const { t } = useTranslate('options')\n  const { availWidth } = window.screen\n  return (\n    <SaladictForm\n      items={[\n        {\n          name: getProfilePath('mtaAutoUnfold'),\n          children: (\n            <Select>\n              <Select.Option value=\"\">\n                {t('profile.opt.mtaAutoUnfold.never')}\n              </Select.Option>\n              <Select.Option value=\"once\">\n                {t('profile.opt.mtaAutoUnfold.once')}\n              </Select.Option>\n              <Select.Option value=\"always\">\n                {t('profile.opt.mtaAutoUnfold.always')}\n              </Select.Option>\n              <Select.Option value=\"popup\">\n                {t('profile.opt.mtaAutoUnfold.popup')}\n              </Select.Option>\n              <Select.Option value=\"hide\">\n                {t('profile.opt.mtaAutoUnfold.hide')}\n              </Select.Option>\n            </Select>\n          )\n        },\n        {\n          name: getProfilePath('waveform'),\n          valuePropName: 'checked',\n          children: <Switch />\n        },\n        {\n          name: getConfigPath('searchSuggests'),\n          valuePropName: 'checked',\n          children: <Switch />\n        },\n        {\n          name: getConfigPath('defaultPinned'),\n          valuePropName: 'checked',\n          children: <Switch />\n        },\n        {\n          name: getConfigPath('animation'),\n          valuePropName: 'checked',\n          children: <Switch />\n        },\n        {\n          name: getConfigPath('darkMode'),\n          valuePropName: 'checked',\n          children: <Switch />\n        },\n        {\n          name: getConfigPath('panelMaxHeightRatio'),\n          children: (\n            <Slider\n              tipFormatter={percentageSlideFormatter}\n              min={0}\n              max={100}\n              marks={{ 0: '0%', 80: '80%', 100: '100%' }}\n            />\n          )\n        },\n        {\n          name: getConfigPath('panelWidth'),\n          children: (\n            <Slider\n              tipFormatter={pixelSlideFormatter}\n              min={250}\n              max={availWidth}\n              marks={{\n                250: '250px',\n                450: '450px',\n                [availWidth]: `${availWidth}px`\n              }}\n            />\n          )\n        },\n        {\n          name: getConfigPath('fontSize'),\n          children: (\n            <Slider\n              tipFormatter={pixelSlideFormatter}\n              min={8}\n              max={30}\n              marks={{ 8: '8px', 30: '30px' }}\n            />\n          )\n        },\n        {\n          name: getConfigPath('bowlOffsetX'),\n          children: (\n            <Slider\n              tipFormatter={pixelSlideFormatter}\n              min={-100}\n              max={100}\n              marks={{ '-100': '-100px', 0: '0px', 100: '100px' }}\n            />\n          )\n        },\n        {\n          name: getConfigPath('bowlOffsetY'),\n          children: (\n            <Slider\n              tipFormatter={pixelSlideFormatter}\n              min={-100}\n              max={100}\n              marks={{ '-100': '-100px', 0: '0px', 100: '100px' }}\n            />\n          )\n        },\n        {\n          name: getConfigPath('panelCSS'),\n          extra: (\n            <a\n              href=\"https://github.com/crimx/ext-saladict/wiki/PanelCSS#wiki-content\"\n              target=\"_blank\"\n              rel=\"nofollow noopener noreferrer\"\n            >\n              Examples\n            </a>\n          ),\n          children: (\n            <Input.TextArea\n              placeholder=\".dictPanel-Root { }\"\n              autoSize={{ minRows: 4, maxRows: 15 }}\n            />\n          )\n        }\n      ]}\n    />\n  )\n}\n"
  },
  {
    "path": "src/options/components/Entries/Dictionaries/AllDicts.tsx",
    "content": "import React, { FC, useMemo } from 'react'\nimport { Card, List, Switch } from 'antd'\nimport { DictID } from '@/app-config'\nimport { useSelector } from '@/content/redux'\nimport { objectKeys } from '@/typings/helpers'\nimport { DictTitle } from './DictTitle'\n\nexport interface AllDictsProps {\n  value?: DictID[]\n  onChange?: (list: DictID[]) => void\n}\n\n/**\n * Antd form item compatible list\n */\nexport const AllDicts: FC<AllDictsProps> = props => {\n  const allDicts = useSelector(state => state.activeProfile.dicts.all)\n  const allDictIds = useMemo(() => objectKeys(allDicts), [allDicts])\n  const selected = useMemo(() => new Set(props.value || []), [props.value])\n\n  return (\n    <Card>\n      <List\n        size=\"large\"\n        dataSource={allDictIds}\n        renderItem={dictID => (\n          <List.Item>\n            <div className=\"sortable-list-item\">\n              <DictTitle dictID={dictID} dictLangs={allDicts[dictID].lang} />\n              <Switch\n                checked={selected.has(dictID)}\n                onChange={checked => {\n                  if (props.onChange && props.value) {\n                    props.onChange(\n                      checked\n                        ? [...props.value, dictID]\n                        : props.value.filter(id => id !== dictID)\n                    )\n                  }\n                }}\n              />\n            </div>\n          </List.Item>\n        )}\n      />\n    </Card>\n  )\n}\n"
  },
  {
    "path": "src/options/components/Entries/Dictionaries/DictTitle/_style.scss",
    "content": ".saladict-dict-title {\n  word-break: break-all;\n\n  & > * {\n    word-break: keep-all;\n    white-space: nowrap;\n  }\n}\n\n.saladict-dict-title-icon {\n  width: 1.3em;\n  height: 1.3em;\n  margin-right: 5px;\n  vertical-align: text-bottom;\n}\n\n.saladict-dict-title-link {\n  color: currentColor;\n}\n\n.saladict-dict-langs-char {\n  margin-left: 5px;\n  padding: 0 2px;\n  font-size: 0.92em;\n  color: #777;\n  border: 1px solid #777;\n  border-radius: 2px;\n}\n"
  },
  {
    "path": "src/options/components/Entries/Dictionaries/DictTitle/index.tsx",
    "content": "import React, { FC } from 'react'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { message } from '@/_helpers/browser-api'\nimport { DictID } from '@/app-config'\n\nimport './_style.scss'\n\nexport interface DictTitleProps {\n  dictID: DictID\n  /** Supported languages */\n  dictLangs: string\n}\n\nconst langCodes = ['en', 'zhs', 'zht', 'ja', 'kor', 'fr', 'de', 'es'] as const\n\nexport const DictTitle: FC<DictTitleProps> = ({ dictID, dictLangs }) => {\n  const { t } = useTranslate(['options', 'dicts'])\n  const title = t(`dicts:${dictID}.name`)\n\n  return (\n    <span className=\"saladict-dict-title\">\n      <span>\n        <img\n          className=\"saladict-dict-title-icon\"\n          src={require('@/components/dictionaries/' + dictID + '/favicon.png')}\n          alt={`logo ${title}`}\n        />\n        <a\n          className=\"saladict-dict-title-link\"\n          href=\"#\"\n          onClick={e => {\n            e.stopPropagation()\n            e.preventDefault()\n            openDictSrcPage(dictID, dictLangs)\n          }}\n        >\n          {title}\n        </a>\n      </span>\n      <span>\n        {dictLangs.split('').map((c, i) =>\n          +c ? (\n            <span className=\"saladict-dict-langs-char\" key={langCodes[i]}>\n              {t(`dict.lang.${langCodes[i]}`)}\n            </span>\n          ) : null\n        )}\n      </span>\n    </span>\n  )\n}\n\nexport const DictTitleMemo = React.memo(DictTitle)\n\nfunction openDictSrcPage(dictID: DictID, dictLangs: string) {\n  const text = +dictLangs[0]\n    ? 'salad'\n    : +dictLangs[1] || +dictLangs[2]\n    ? '沙拉'\n    : +dictLangs[3]\n    ? 'サラダ'\n    : +dictLangs[4]\n    ? '샐러드'\n    : 'salad'\n\n  message.send({\n    type: 'OPEN_DICT_SRC_PAGE',\n    payload: {\n      id: dictID,\n      text\n    }\n  })\n}\n"
  },
  {
    "path": "src/options/components/Entries/Dictionaries/EditModal.tsx",
    "content": "import React, { FC, useContext } from 'react'\nimport { shallowEqual } from 'react-redux'\nimport { Translator } from '@opentranslate/translator'\nimport { Switch, Select, Checkbox, Button, Modal } from 'antd'\nimport { ExclamationCircleOutlined } from '@ant-design/icons'\nimport { Rule } from 'antd/lib/form'\nimport { DictID } from '@/app-config'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { supportedLangs } from '@/_helpers/lang-check'\nimport { useSelector } from '@/content/redux'\nimport { getProfilePath } from '@/options/helpers/path-joiner'\nimport { SaladictFormItem } from '@/options/components/SaladictForm'\nimport { InputNumberGroup } from '@/options/components/InputNumberGroup'\nimport { SaladictModalForm } from '@/options/components/SaladictModalForm'\nimport { ChangeEntryContext } from '@/options/helpers/change-entry'\nimport { useFormDirty, setFormDirty } from '@/options/helpers/use-form-dirty'\n\nexport interface EditModalProps {\n  dictID?: DictID | null\n  onClose: () => void\n}\n\nexport const EditModal: FC<EditModalProps> = ({ dictID, onClose }) => {\n  const { t, i18n } = useTranslate(['options', 'dicts', 'common', 'langcode'])\n  const changeEntry = useContext(ChangeEntryContext)\n  const formDirtyRef = useFormDirty()\n  const { dictAuth, allDicts } = useSelector(\n    state => ({\n      dictAuth: state.config.dictAuth,\n      allDicts: state.activeProfile.dicts.all\n    }),\n    shallowEqual\n  )\n  const formItems: SaladictFormItem[] = []\n\n  const NUMBER_RULES: Rule[] = [\n    { type: 'number', message: t('form.number_error'), required: true }\n  ]\n\n  if (dictID) {\n    formItems.push(\n      {\n        key: getProfilePath('dicts', 'all', dictID, 'selectionLang'),\n        label: t('dict.selectionLang'),\n        help: t('dict.selectionLang_help'),\n        extra: t('config.language_extra'),\n        className: 'saladict-form-danger-extra',\n        items: supportedLangs.map(lang => ({\n          name: getProfilePath('dicts', 'all', dictID, 'selectionLang', lang),\n          className: 'form-item-inline',\n          valuePropName: 'checked',\n          children: <Checkbox>{t(`common:lang.${lang}`)}</Checkbox>\n        }))\n      },\n      {\n        key: getProfilePath('dicts', 'all', dictID, 'defaultUnfold'),\n        label: t('dict.defaultUnfold'),\n        help: t('dict.defaultUnfold_help'),\n        extra: t('config.language_extra'),\n        className: 'saladict-form-danger-extra',\n        items: supportedLangs.map(lang => ({\n          name: getProfilePath('dicts', 'all', dictID, 'defaultUnfold', lang),\n          className: 'form-item-inline',\n          valuePropName: 'checked',\n          children: <Checkbox>{t(`common:lang.${lang}`)}</Checkbox>\n        }))\n      },\n      {\n        key: getProfilePath('dicts', 'all', dictID, 'selectionWC'),\n        label: t('dict.selectionWC'),\n        help: t('dict.selectionWC_help'),\n        items: [\n          {\n            name: getProfilePath('dicts', 'all', dictID, 'selectionWC', 'min'),\n            label: null,\n            style: { marginBottom: 5 },\n            rules: NUMBER_RULES,\n            children: <InputNumberGroup suffix={t('common:min')} />\n          },\n          {\n            name: getProfilePath('dicts', 'all', dictID, 'selectionWC', 'max'),\n            label: null,\n            style: { marginBottom: 5 },\n            rules: NUMBER_RULES,\n            children: <InputNumberGroup suffix={t('common:max')} />\n          }\n        ]\n      },\n      {\n        name: getProfilePath('dicts', 'all', dictID, 'preferredHeight'),\n        label: t('dict.preferredHeight'),\n        help: t('dict.preferredHeight_help'),\n        rules: NUMBER_RULES,\n        children: <InputNumberGroup suffix={t('common:max')} />\n      }\n    )\n\n    // Dict Auth for Machine Translators\n    if (dictAuth[dictID]) {\n      formItems.push({\n        key: dictID + '_auth',\n        label: t('nav.DictAuths'),\n        children: (\n          <Button\n            href=\"./?menuselected=DictAuths\"\n            onClick={e => {\n              e.preventDefault()\n              e.stopPropagation()\n              if (formDirtyRef.value) {\n                Modal.confirm({\n                  title: t('unsave_confirm'),\n                  icon: <ExclamationCircleOutlined />,\n                  okType: 'danger',\n                  onOk: () => {\n                    setFormDirty(false)\n                    changeEntry('DictAuths')\n                  }\n                })\n              } else {\n                changeEntry('DictAuths')\n              }\n            }}\n          >\n            {t('dictAuth.manage')}\n          </Button>\n        )\n      })\n    }\n\n    // custom options\n    const options = allDicts[dictID]['options']\n    if (options) {\n      formItems.push(\n        ...Object.keys(options).map(optKey => {\n          // can be number | boolean | string(select)\n          const value = options[optKey]\n\n          const item: SaladictFormItem = {\n            name: `profile.dicts.all.${dictID}.options.${optKey}`,\n            label: t(`dicts:${dictID}.options.${optKey}`),\n            help: i18n.exists(`dicts:${dictID}.helps.${optKey}`)\n              ? t(`dicts:${dictID}.helps.${optKey}`)\n              : null\n          }\n\n          switch (typeof value) {\n            case 'number':\n              item.rules = NUMBER_RULES\n              item.children = (\n                <InputNumberGroup\n                  suffix={t(`dicts:${dictID}.options.${optKey}_unit`)}\n                />\n              )\n              break\n            case 'string':\n              if (optKey === 'tl' || optKey === 'tl2') {\n                const getTranslator:\n                  | undefined\n                  | (() => Translator) = require(`@/components/dictionaries/${dictID}/engine`)\n                  .getTranslator\n\n                const langs = getTranslator\n                  ? getTranslator()\n                      .getSupportLanguages()\n                      .map(lang => (lang === 'auto' ? 'default' : lang))\n                  : allDicts[dictID]['options_sel'][optKey]\n\n                item.children = (\n                  <Select>\n                    {langs.map((option: string) => (\n                      <Select.Option value={option} key={option}>\n                        {option === 'default' ? '' : option + ' '}\n                        {t(`langcode:${option}`)}\n                      </Select.Option>\n                    ))}\n                  </Select>\n                )\n              } else {\n                item.children = (\n                  <Select>\n                    {allDicts[dictID]['options_sel'][optKey].map(\n                      (option: string) => (\n                        <Select.Option value={option} key={option}>\n                          {t(`dicts:${dictID}.options.${optKey}-${option}`)}\n                        </Select.Option>\n                      )\n                    )}\n                  </Select>\n                )\n              }\n              break\n            default:\n              item.valuePropName = 'checked'\n              item.children = <Switch />\n          }\n          return item\n        })\n      )\n    }\n  }\n\n  return (\n    <SaladictModalForm\n      visible={!!dictID}\n      title={t(`dicts:${dictID}.name`)}\n      items={formItems}\n      onClose={onClose}\n    />\n  )\n}\n"
  },
  {
    "path": "src/options/components/Entries/Dictionaries/index.tsx",
    "content": "import React, { FC, useState, useLayoutEffect } from 'react'\nimport { Tooltip, Row, Col } from 'antd'\nimport { BlockOutlined } from '@ant-design/icons'\nimport { DictID } from '@/app-config'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { useSelector } from '@/content/redux'\nimport { SortableList, reorder } from '@/options/components/SortableList'\nimport { SaladictModalForm } from '@/options/components/SaladictModalForm'\nimport { getProfilePath } from '@/options/helpers/path-joiner'\nimport { useCheckDictAuth } from '@/options/helpers/use-check-dict-auth'\nimport { useListLayout } from '@/options/helpers/layout'\nimport { useUpload } from '@/options/helpers/upload'\nimport { DictTitleMemo } from './DictTitle'\nimport { EditModal } from './EditModal'\nimport { AllDicts } from './AllDicts'\n\nexport const Dictionaries: FC = () => {\n  const { t } = useTranslate(['options', 'common', 'dicts'])\n  const checkDictAuth = useCheckDictAuth()\n  const [editingDict, setEditingDict] = useState<DictID | null>(null)\n  const [showAddModal, setShowAddModal] = useState(false)\n  const listLayout = useListLayout()\n  const dicts = useSelector(state => state.activeProfile.dicts)\n  const upload = useUpload()\n\n  // make a local copy to avoid flickering on drag end\n  const [selectedDicts, setSelectedDicts] = useState<ReadonlyArray<DictID>>(\n    dicts.selected\n  )\n  useLayoutEffect(() => {\n    setSelectedDicts(dicts.selected)\n  }, [dicts.selected])\n\n  return (\n    <Row>\n      <Col {...listLayout}>\n        <SortableList\n          title={\n            <Tooltip\n              title={t('profile.opt.item_extra')}\n              className=\"saladict-form-profile-title\"\n            >\n              <span>\n                <BlockOutlined />\n                {t('profile.opt.dict_selected')}\n              </span>\n            </Tooltip>\n          }\n          list={selectedDicts.map(id => ({\n            value: id,\n            title: <DictTitleMemo dictID={id} dictLangs={dicts.all[id].lang} />\n          }))}\n          onAdd={async () => {\n            if (await checkDictAuth()) {\n              setShowAddModal(true)\n            }\n          }}\n          onEdit={index => {\n            setEditingDict(selectedDicts[index])\n          }}\n          onDelete={index => {\n            const newList = selectedDicts.slice()\n            newList.splice(index, 1)\n            upload({\n              [getProfilePath('dicts', 'selected')]: newList\n            })\n            setSelectedDicts(newList)\n          }}\n          onOrderChanged={(oldIndex, newIndex) => {\n            const newList = reorder(selectedDicts, oldIndex, newIndex)\n            upload({\n              [getProfilePath('dicts', 'selected')]: newList\n            })\n            setSelectedDicts(newList)\n          }}\n        />\n      </Col>\n      <SaladictModalForm\n        visible={showAddModal}\n        title={t('dict.add')}\n        onClose={() => setShowAddModal(false)}\n        wrapperCol={{ span: 24 }}\n        items={[\n          {\n            name: getProfilePath('dicts', 'selected'),\n            label: null,\n            help: null,\n            extra: null,\n            children: <AllDicts />\n          }\n        ]}\n      />\n      <EditModal dictID={editingDict} onClose={() => setEditingDict(null)} />\n    </Row>\n  )\n}\n"
  },
  {
    "path": "src/options/components/Entries/General.tsx",
    "content": "import React, { FC } from 'react'\nimport { Switch, Select } from 'antd'\nimport { getConfigPath } from '@/options/helpers/path-joiner'\nimport {\n  SaladictForm,\n  SaladictFormItem\n} from '@/options/components/SaladictForm'\nimport { isFirefox, isOpera } from '@/_helpers/saladict'\n\nexport const General: FC = () => {\n  const formItems: SaladictFormItem[] = [\n    {\n      name: getConfigPath('active'),\n      valuePropName: 'checked',\n      children: <Switch />\n    },\n    {\n      name: getConfigPath('animation'),\n      valuePropName: 'checked',\n      children: <Switch />\n    }\n  ]\n\n  if (!(isFirefox || isOpera)) {\n    formItems.push({\n      name: getConfigPath('runInBg'),\n      valuePropName: 'checked',\n      children: <Switch />\n    })\n  }\n\n  formItems.push(\n    {\n      name: getConfigPath('darkMode'),\n      valuePropName: 'checked',\n      children: <Switch />\n    },\n    {\n      name: getConfigPath('langCode'),\n      children: (\n        <Select>\n          <Select.Option value=\"zh-CN\">简体中文</Select.Option>\n          <Select.Option value=\"zh-TW\">繁體中文</Select.Option>\n          <Select.Option value=\"en\">English</Select.Option>\n        </Select>\n      )\n    }\n  )\n\n  return <SaladictForm items={formItems} />\n}\n"
  },
  {
    "path": "src/options/components/Entries/ImportExport.tsx",
    "content": "import React, { FC } from 'react'\nimport { TFunction } from 'i18next'\nimport { Row, Col, Upload, notification } from 'antd'\nimport { RcFile } from 'antd/lib/upload'\nimport { DownloadOutlined, UploadOutlined } from '@ant-design/icons'\nimport { AppConfig } from '@/app-config'\nimport mergeConfig from '@/app-config/merge-config'\nimport { ProfileIDList, Profile } from '@/app-config/profiles'\nimport { mergeProfile } from '@/app-config/merge-profile'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { storage } from '@/_helpers/browser-api'\nimport { updateConfig, getConfig } from '@/_helpers/config-manager'\nimport { updateProfile, getProfile } from '@/_helpers/profile-manager'\nimport { useListLayout } from '@/options/helpers/layout'\n\nexport type ConfigStorage = {\n  baseconfig: AppConfig\n  activeProfileID: string\n  hasInstructionsShown: boolean\n  profileIDList: ProfileIDList\n} & {\n  [id: string]: Profile\n}\n\nexport const ImportExport: FC = () => {\n  const { t } = useTranslate('options')\n  const layout = useListLayout()\n\n  return (\n    <Row>\n      <Col {...layout}>\n        <Row gutter={10}>\n          <Col span={12}>\n            <Upload.Dragger\n              showUploadList={false}\n              beforeUpload={file => {\n                importConfig(file, t)\n                return false\n              }}\n            >\n              <p className=\"ant-upload-drag-icon\">\n                <DownloadOutlined />\n              </p>\n              <p className=\"ant-upload-text\">{t('import.title')}</p>\n            </Upload.Dragger>\n          </Col>\n          <Col span={12}>\n            <button\n              className=\"ant-upload ant-upload-drag\"\n              onClick={() => exportConfig(t)}\n            >\n              <div className=\"ant-upload ant-upload-btn\">\n                <p className=\"ant-upload-drag-icon\">\n                  <UploadOutlined />\n                </p>\n                <p className=\"ant-upload-text\">{t('export.title')}</p>\n              </div>\n            </button>\n          </Col>\n        </Row>\n        <Row justify=\"center\">\n          <p style={{ margin: '1em 0' }}>{t('import_export_help')}</p>\n        </Row>\n      </Col>\n    </Row>\n  )\n}\n\nasync function importConfig(file: RcFile, t: TFunction) {\n  const result = await new Promise<Partial<ConfigStorage> | null>(resolve => {\n    const fr = new FileReader()\n    fr.onload = () => {\n      try {\n        const json = JSON.parse(fr.result as string)\n        resolve(json)\n      } catch (err) {\n        notification.error({\n          message: t('import.error.title'),\n          description: t('import.error.parse')\n        })\n      }\n      resolve()\n    }\n    fr.onerror = () => {\n      notification.error({\n        message: t('import.error.title'),\n        description: t('import.error.parse')\n      })\n      resolve()\n    }\n    fr.readAsText(file)\n  })\n\n  if (!result) {\n    return\n  }\n\n  let {\n    baseconfig,\n    activeProfileID,\n    hasInstructionsShown,\n    profileIDList,\n    syncConfig\n  } = result\n\n  if (\n    !baseconfig &&\n    !activeProfileID &&\n    !profileIDList &&\n    hasInstructionsShown == null\n  ) {\n    notification.error({\n      message: t('import.error.title'),\n      description: t('import.error.empty')\n    })\n    return\n  }\n\n  await storage.sync.clear()\n\n  if (baseconfig) {\n    await updateConfig(mergeConfig(baseconfig))\n  }\n\n  if (syncConfig) {\n    await storage.sync.set({ syncConfig })\n  }\n\n  if (hasInstructionsShown != null) {\n    await storage.sync.set({ hasInstructionsShown })\n  }\n\n  if (profileIDList) {\n    profileIDList = profileIDList.filter(({ id }) => result[id])\n    if (profileIDList.length > 0) {\n      for (const { id } of profileIDList) {\n        await updateProfile(mergeProfile(result[id] as Profile))\n      }\n      if (\n        !activeProfileID ||\n        profileIDList.every(({ id }) => id !== activeProfileID)\n      ) {\n        // use first item instead\n        activeProfileID = profileIDList[0].id\n      }\n      await storage.sync.set({ activeProfileID, profileIDList })\n    }\n  }\n}\n\nasync function exportConfig(t: TFunction) {\n  const result = await storage.sync.get([\n    'activeProfileID',\n    'hasInstructionsShown',\n    'profileIDList',\n    'syncConfig'\n  ])\n\n  result.baseconfig = await getConfig()\n\n  if (!result.baseconfig || !result.activeProfileID || !result.profileIDList) {\n    notification.error({\n      message: t('export.error.title'),\n      description: t('export.error.empty')\n    })\n    return\n  }\n\n  for (const { id } of result.profileIDList) {\n    result[id] = await getProfile(id)\n  }\n\n  try {\n    let text = JSON.stringify(result)\n    const { os } = await browser.runtime.getPlatformInfo()\n    if (os === 'win') {\n      text = text.replace(/\\r\\n|\\n/g, '\\r\\n')\n    }\n    const file = new Blob([text], { type: 'text/plain;charset=utf-8' })\n    const a = document.createElement('a')\n    a.href = URL.createObjectURL(file)\n    a.download = `config-${Date.now()}.saladict`\n\n    // firefox\n    a.target = '_blank'\n    document.body.appendChild(a)\n\n    a.click()\n  } catch (err) {\n    notification.error({\n      message: t('export.error.title'),\n      description: t('export.error.parse')\n    })\n  }\n}\n"
  },
  {
    "path": "src/options/components/Entries/Notebook/index.tsx",
    "content": "import React, { FC, useState } from 'react'\nimport { Switch, Checkbox, Button } from 'antd'\nimport { concat, from } from 'rxjs'\nimport { pluck, map } from 'rxjs/operators'\nimport { useObservableState, useObservable, useRefFn } from 'observable-hooks'\nimport { objectKeys } from '@/typings/helpers'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { storage } from '@/_helpers/browser-api'\nimport { useSelector } from '@/content/redux'\nimport { getConfigPath } from '@/options/helpers/path-joiner'\nimport {\n  SaladictForm,\n  SaladictFormItem\n} from '@/options/components/SaladictForm'\n\nconst reqSyncService = require.context('./sync-services', false, /\\.tsx$/)\n\nexport const Notebook: FC = () => {\n  const { t } = useTranslate(['options', 'dicts', 'common', 'sync'])\n  const ctxTrans = useSelector(state => state.config.ctxTrans)\n  const syncServiceIds = useRefFn(() =>\n    reqSyncService.keys().map(path => /([^/]+)\\.tsx$/.exec(path)![1])\n  ).current\n  const [showSyncServices, setShowSyncServices] = useState<{\n    [id: string]: boolean\n  }>({})\n  const syncConfigs = useObservableState(\n    useObservable(() =>\n      concat(\n        from(storage.sync.get('syncConfig')).pipe(pluck('syncConfig')),\n        storage.sync.createStream('syncConfig').pipe(pluck('newValue'))\n      ).pipe(\n        map(syncConfig => {\n          // legacy fix\n          if (\n            syncConfig?.webdav &&\n            !Object.prototype.hasOwnProperty.call(syncConfig.webdav, 'enable')\n          ) {\n            syncConfig.webdav.enable = !!syncConfig.webdav.url\n          }\n          return syncConfig\n        })\n      )\n    )\n  )\n\n  const formItems: SaladictFormItem[] = [\n    {\n      name: getConfigPath('editOnFav'),\n      valuePropName: 'checked',\n      children: <Switch />\n    },\n    {\n      name: getConfigPath('searchHistory'),\n      valuePropName: 'checked',\n      children: <Switch />\n    },\n    {\n      name: getConfigPath('searchHistoryInco'),\n      hide: values => !values[getConfigPath('searchHistory')],\n      valuePropName: 'checked',\n      children: <Switch />\n    },\n    {\n      key: getConfigPath('ctxTrans'),\n      style: { marginBottom: 10 },\n      items: objectKeys(ctxTrans).map(id => ({\n        name: getConfigPath('ctxTrans', id),\n        valuePropName: 'checked',\n        style: { marginBottom: 0 },\n        children: <Checkbox>{t(`dicts:${id}.name`)}</Checkbox>\n      }))\n    }\n  ]\n\n  syncServiceIds.forEach(id => {\n    const key = `syncService.btn.${id}`\n    const title = t(`sync:${id}.title`)\n    formItems.push({\n      key,\n      label: title,\n      children: (\n        <Button\n          onClick={() =>\n            setShowSyncServices(showSyncServices => ({\n              ...showSyncServices,\n              [id]: true\n            }))\n          }\n        >{`${title} (${t(\n          syncConfigs?.[id]?.enable ? 'common:enabled' : 'common:disabled'\n        )})`}</Button>\n      )\n    })\n  })\n\n  return (\n    <>\n      <SaladictForm items={formItems} />\n      {syncServiceIds.map(id =>\n        React.createElement(reqSyncService(`./${id}.tsx`).default, {\n          key: id,\n          syncConfig: syncConfigs?.[id],\n          show: showSyncServices[id],\n          onClose: () =>\n            setShowSyncServices(showSyncServices => ({\n              ...showSyncServices,\n              [id]: false\n            }))\n        })\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "src/options/components/Entries/Notebook/sync-services/ankiconnect.tsx",
    "content": "/* eslint-disable no-throw-literal */\nimport React, { FC, useState, useRef } from 'react'\nimport {\n  Form,\n  Input,\n  Modal,\n  Button,\n  notification,\n  Switch,\n  Checkbox\n} from 'antd'\nimport { FormInstance } from 'antd/lib/form'\nimport { ExclamationCircleOutlined } from '@ant-design/icons'\nimport {\n  Service,\n  SyncConfig\n} from '@/background/sync-manager/services/ankiconnect'\nimport { setSyncConfig } from '@/background/sync-manager/helpers'\nimport { useTranslate } from '@/_helpers/i18n'\n\nexport interface AnkiConnectModalProps {\n  syncConfig?: SyncConfig\n  show: boolean\n  onClose: () => void\n}\n\nexport const AnkiConnectModal: FC<AnkiConnectModalProps> = props => {\n  const { t, i18n } = useTranslate(['options', 'common', 'sync'])\n  const [serviceChecking, setServiceChecking] = useState(false)\n  const formRef = useRef<FormInstance>(null)\n\n  return (\n    <Modal\n      visible={props.show}\n      title={t('sync:ankiconnect.title')}\n      onOk={submitForm}\n      onCancel={closeModal}\n      destroyOnClose\n      footer={[\n        <Button\n          key=\"verify\"\n          disabled={serviceChecking}\n          loading={serviceChecking}\n          onClick={verifyService}\n        >\n          {t('syncService.ankiconnect.verify')}\n        </Button>,\n        <Button\n          key=\"save\"\n          type=\"primary\"\n          disabled={serviceChecking}\n          onClick={submitForm}\n        >\n          {t('common:save')}\n        </Button>,\n        <Button key=\"cancel\" onClick={closeModal}>\n          {t('common:cancel')}\n        </Button>\n      ]}\n    >\n      <Form\n        ref={formRef}\n        initialValues={props.syncConfig || Service.getDefaultConfig()}\n        labelCol={{ span: 5 }}\n        wrapperCol={{ span: 18 }}\n        onFinish={saveService}\n      >\n        <p>\n          {t('syncService.ankiconnect.description')}\n          <a\n            href=\"https://saladict.crimx.com/anki.html\"\n            target=\"_blank\"\n            rel=\"nofollow noopener noreferrer\"\n          >\n            {t('tutorial')}\n          </a>\n        </p>\n        <Form.Item\n          name=\"enable\"\n          label={t('common:enable')}\n          help={t('syncService.ankiconnect.enable_help')}\n          valuePropName=\"checked\"\n        >\n          <Switch />\n        </Form.Item>\n        <Form.Item\n          name=\"host\"\n          label={t('syncService.ankiconnect.host')}\n          hasFeedback\n          rules={[{ required: true }]}\n        >\n          <Input />\n        </Form.Item>\n        <Form.Item\n          name=\"port\"\n          label={t('syncService.ankiconnect.port')}\n          hasFeedback\n          rules={[{ required: true }]}\n        >\n          <Input />\n        </Form.Item>\n        <Form.Item\n          name=\"key\"\n          label={t('syncService.ankiconnect.key')}\n          help={t('syncService.ankiconnect.key_help')}\n        >\n          <Input />\n        </Form.Item>\n        <Form.Item\n          name=\"deckName\"\n          label={t('syncService.ankiconnect.deckName')}\n          help={t('syncService.ankiconnect.deckName_help')}\n          hasFeedback\n          rules={[{ required: true }]}\n        >\n          <Input />\n        </Form.Item>\n        <Form.Item\n          name=\"noteType\"\n          label={t('syncService.ankiconnect.noteType')}\n          help={t('syncService.ankiconnect.noteType_help')}\n          hasFeedback\n          rules={[{ required: true }]}\n        >\n          <Input />\n        </Form.Item>\n        <Form.Item\n          name=\"tags\"\n          label={t('syncService.ankiconnect.tags')}\n          help={t('syncService.ankiconnect.tags_help')}\n        >\n          <Input />\n        </Form.Item>\n        <Form.Item\n          label={t('syncService.ankiconnect.escapeHTML')}\n          help={t('syncService.ankiconnect.escapeHTML_help')}\n        >\n          <Form.Item\n            name=\"escapeContext\"\n            className=\"form-item-inline\"\n            valuePropName=\"checked\"\n          >\n            <Checkbox>{t('common:note.context')}</Checkbox>\n          </Form.Item>\n          <Form.Item\n            name=\"escapeTrans\"\n            className=\"form-item-inline\"\n            valuePropName=\"checked\"\n          >\n            <Checkbox>{t('common:note.trans')}</Checkbox>\n          </Form.Item>\n          <Form.Item\n            name=\"escapeNote\"\n            className=\"form-item-inline\"\n            valuePropName=\"checked\"\n          >\n            <Checkbox>{t('common:note.note')}</Checkbox>\n          </Form.Item>\n        </Form.Item>\n        <Form.Item\n          name=\"syncServer\"\n          label={t('syncService.ankiconnect.syncServer')}\n          help={t('syncService.ankiconnect.syncServer_help')}\n          valuePropName=\"checked\"\n        >\n          <Switch />\n        </Form.Item>\n      </Form>\n    </Modal>\n  )\n\n  function submitForm() {\n    if (formRef.current) {\n      formRef.current.submit()\n    }\n  }\n\n  function closeModal() {\n    if (formRef.current && formRef.current.isFieldsTouched()) {\n      Modal.confirm({\n        title: t('syncService.close_confirm'),\n        icon: <ExclamationCircleOutlined />,\n        okType: 'danger',\n        onOk: props.onClose\n      })\n    } else {\n      props.onClose()\n    }\n  }\n\n  async function verify(service: Service) {\n    try {\n      await service.init()\n      notification.success({ message: t('syncService.ankiconnect.verified') })\n    } catch (error) {\n      const errorText = typeof error === 'string' ? error : error.message\n      switch (errorText) {\n        case 'deck':\n          if (\n            confirm(\n              t('syncService.ankiconnect.deck_confirm', {\n                deck: service.config.deckName\n              })\n            )\n          ) {\n            try {\n              await service.addDeck()\n              return verify(service)\n            } catch (e) {\n              notification.error({\n                message: 'Error',\n                description: t('syncService.ankiconnect.deck_error', {\n                  deck: service.config.deckName\n                })\n              })\n              return\n            }\n          } else {\n            confirm(t('syncService.ankiconnect.add_yourself'))\n          }\n          break\n        case 'notetype':\n          if (\n            confirm(\n              t('syncService.ankiconnect.notetype_confirm', {\n                noteType: service.config.noteType\n              })\n            )\n          ) {\n            try {\n              await service.addNoteType()\n              return verify(service)\n            } catch (e) {\n              notification.error({\n                message: 'Error',\n                description: t('syncService.ankiconnect.notetype_error', {\n                  noteType: service.config.noteType\n                })\n              })\n              return\n            }\n          } else {\n            confirm(t('syncService.ankiconnect.add_yourself'))\n          }\n          break\n        default:\n          throw error\n      }\n    }\n  }\n\n  async function verifyService() {\n    const config = extractConfigFromForm()\n    if (!config) return\n\n    setServiceChecking(true)\n\n    const service = new Service(config)\n    try {\n      await verify(service)\n      if (confirm(t('syncService.ankiconnect.upload_confirm'))) {\n        await service.add({ force: true })\n      }\n    } catch (e) {\n      notifyError(e)\n    }\n\n    setServiceChecking(false)\n  }\n\n  async function saveService() {\n    const config = extractConfigFromForm()\n    if (!config) return\n\n    if (config.enable) {\n      setServiceChecking(true)\n\n      const service = new Service(config)\n\n      try {\n        await verify(service)\n      } catch (e) {\n        setServiceChecking(false)\n        return notifyError(e)\n      }\n\n      setServiceChecking(false)\n    }\n\n    try {\n      await setSyncConfig(Service.id, config)\n      props.onClose()\n    } catch (error) {\n      notifyError(error)\n    }\n  }\n\n  function extractConfigFromForm(): SyncConfig | undefined {\n    if (!formRef.current) {\n      if (process.env.DEBUG) {\n        console.error(new Error('Missing form ref when saving service'))\n      }\n      notification.error({\n        message: 'Error',\n        description: t('sync:ankiconnect.error.internal')\n      })\n      return\n    }\n\n    return formRef.current.getFieldsValue() as SyncConfig\n  }\n\n  function notifyError(error: Error | string) {\n    const errorText = typeof error === 'string' ? error : error.message\n    const msgPath = 'sync:ankiconnect.error.' + errorText\n    const description = i18n.exists(msgPath) ? t(msgPath) : errorText\n    notification.error({ message: 'Error', description })\n  }\n}\n\nexport default AnkiConnectModal\n"
  },
  {
    "path": "src/options/components/Entries/Notebook/sync-services/eudic.tsx",
    "content": "import React, { FC, useState, useRef } from 'react'\nimport {\n  Modal,\n  Button,\n  Form,\n  Input,\n  Switch,\n  message as AntdMsg,\n  notification\n} from 'antd'\nimport { FormInstance } from 'antd/lib/form'\nimport { ExclamationCircleOutlined } from '@ant-design/icons'\nimport { Service, SyncConfig } from '@/background/sync-manager/services/eudic'\nimport { setSyncConfig } from '@/background/sync-manager/helpers'\nimport { getWords } from '@/_helpers/record-manager'\nimport { useTranslate } from '@/_helpers/i18n'\n\nexport interface EudicModalProps {\n  syncConfig?: SyncConfig\n  show: boolean\n  onClose: () => void\n}\n\nexport const EuDicModal: FC<EudicModalProps> = props => {\n  const { t, i18n } = useTranslate(['options', 'common', 'sync'])\n  const [serviceChecking, setServiceChecking] = useState(false)\n  const formRef = useRef<FormInstance>(null)\n\n  return (\n    <Modal\n      visible={props.show}\n      title={t('sync:eudic.title')}\n      destroyOnClose\n      onOk={submitForm}\n      onCancel={closeModal}\n      footer={[\n        <Button\n          key=\"verify\"\n          disabled={serviceChecking}\n          loading={serviceChecking}\n          onClick={verifyService}\n        >\n          {t('syncService.eudic.verify')}\n        </Button>,\n        <Button\n          key=\"save\"\n          type=\"primary\"\n          disabled={serviceChecking}\n          onClick={submitForm}\n        >\n          {t('common:save')}\n        </Button>,\n        <Button key=\"cancel\" onClick={closeModal}>\n          {t('common:cancel')}\n        </Button>\n      ]}\n    >\n      <p>\n        {t('syncService.eudic.description')}\n        {t('syncService.eudic.token_help')}\n        <a\n          href=\"https://my.eudic.net/OpenAPI/Authorization\"\n          target=\"_blank\"\n          rel=\"nofollow noopener noreferrer\"\n        >\n          {t('syncService.eudic.getToken')}\n        </a>\n      </p>\n      <Form\n        ref={formRef}\n        initialValues={props.syncConfig || Service.getDefaultConfig()}\n        labelCol={{ span: 5 }}\n        wrapperCol={{ span: 18 }}\n        onFinish={saveService}\n      >\n        <Form.Item\n          name=\"enable\"\n          label={t('common:enable')}\n          help={t('syncService.eudic.enable_help')}\n          valuePropName=\"checked\"\n        >\n          <Switch />\n        </Form.Item>\n        <Form.Item\n          name=\"token\"\n          label={t('syncService.eudic.token')}\n          hasFeedback\n          rules={[{ required: true }]}\n        >\n          <Input />\n        </Form.Item>\n        <Form.Item\n          name=\"syncAll\"\n          label={t('syncService.eudic.sync_all')}\n          help={t('syncService.eudic.sync_help')}\n          valuePropName=\"checked\"\n        >\n          <Switch />\n        </Form.Item>\n      </Form>\n    </Modal>\n  )\n\n  function submitForm() {\n    if (formRef.current) {\n      formRef.current.submit()\n    }\n  }\n\n  function closeModal() {\n    if (formRef.current && formRef.current.isFieldsTouched()) {\n      Modal.confirm({\n        title: t('syncService.close_confirm'),\n        icon: <ExclamationCircleOutlined />,\n        okType: 'danger',\n        onOk: props.onClose\n      })\n    } else {\n      props.onClose()\n    }\n  }\n\n  async function verify(service: Service): Promise<Boolean> {\n    let isError = false\n    try {\n      await service.init()\n      notification.success({ message: t('syncService.eudic.verified') })\n      isError = false\n    } catch (error) {\n      isError = true\n      notifyError(error)\n    }\n    return isError\n  }\n\n  async function verifyService() {\n    const config = extractConfigFromForm()\n    if (!config) return\n\n    setServiceChecking(true)\n\n    const service = new Service(config)\n\n    await verify(service)\n\n    setServiceChecking(false)\n  }\n\n  async function saveService() {\n    const config = extractConfigFromForm()\n    if (!config) return\n\n    if (config.enable) {\n      setServiceChecking(true)\n\n      const service = new Service(config)\n\n      const isError = await verify(service)\n\n      setServiceChecking(false)\n\n      if (isError) {\n        return\n      }\n\n      if (config.syncAll) {\n        await onSyncAll(service)\n      }\n    }\n\n    try {\n      await setSyncConfig(Service.id, config)\n      props.onClose()\n    } catch (error) {\n      notifyError(error)\n    }\n  }\n\n  function extractConfigFromForm(): SyncConfig | undefined {\n    if (!formRef.current) {\n      if (process.env.DEBUG) {\n        console.error(new Error('Missing form ref when saving service'))\n      }\n      notification.error({\n        message: 'Error',\n        description: t('sync:eudic.error.internal')\n      })\n      return\n    }\n\n    return formRef.current.getFieldsValue() as SyncConfig\n  }\n\n  async function onSyncAll(service: Service) {\n    const { total } = await getWords('notebook', {\n      itemsPerPage: 1,\n      filters: {}\n    })\n    if (total > 50 && !confirm(t('syncService.eudic.sync_all_confirm'))) {\n      return\n    }\n\n    await syncWords(service)\n  }\n\n  async function syncWords(service: Service) {\n    AntdMsg.destroy()\n    AntdMsg.success(t('syncService.start'), 0)\n\n    try {\n      await service.addWordOrPatch({\n        words: [],\n        force: true\n      })\n      AntdMsg.destroy()\n      AntdMsg.success(t('syncService.success'))\n    } catch (error) {\n      notifyError(error, t('syncService.failed'))\n    }\n  }\n\n  function notifyError(error: Error | string, message: string = 'Error') {\n    const errorText = typeof error === 'string' ? error : error.message\n    const msgPath = 'sync:eudic.error.' + errorText\n    const description = i18n.exists(msgPath) ? t(msgPath) : errorText\n    notification.error({ message, description })\n  }\n}\n\nexport default EuDicModal\n"
  },
  {
    "path": "src/options/components/Entries/Notebook/sync-services/shanbay.tsx",
    "content": "import React, { FC, useState, useEffect } from 'react'\nimport { Modal, Button, Switch, message as AntdMsg, notification } from 'antd'\nimport { Service, SyncConfig } from '@/background/sync-manager/services/shanbay'\nimport { setSyncConfig as uploadSyncConfig } from '@/background/sync-manager/helpers'\nimport { getWords, Word } from '@/_helpers/record-manager'\nimport { useTranslate } from '@/_helpers/i18n'\n\nexport interface WebdavModalProps {\n  syncConfig?: SyncConfig\n  show: boolean\n  onClose: () => void\n}\n\nexport const ShanbayModal: FC<WebdavModalProps> = props => {\n  const { t, i18n } = useTranslate(['options', 'common', 'sync'])\n  const [syncConfig, setSyncConfig] = useState<SyncConfig>(\n    () => props.syncConfig || Service.getDefaultConfig()\n  )\n  useEffect(() => {\n    if (props.syncConfig) {\n      setSyncConfig(props.syncConfig)\n    }\n  }, [props.syncConfig])\n\n  return (\n    <Modal\n      visible={props.show}\n      title={t('sync:shanbay.title')}\n      destroyOnClose\n      onCancel={props.onClose}\n      footer={null}\n    >\n      <p>{t('syncService.shanbay.description')}</p>\n      <div style={{ textAlign: 'center', marginBottom: 20 }}>\n        {t('common:enable')}：\n        <Switch checked={syncConfig.enable} onChange={onToggleEnable} />\n      </div>\n      {syncConfig.enable && (\n        <div style={{ textAlign: 'center' }}>\n          <Button onClick={onSyncAll} style={{ marginRight: 10 }}>\n            {t('syncService.shanbay.sync_all')}\n          </Button>\n          <Button onClick={onSyncLast}>\n            {t('syncService.shanbay.sync_last')}\n          </Button>\n        </div>\n      )}\n    </Modal>\n  )\n\n  async function onToggleEnable(enable: boolean) {\n    const newConfig = {\n      ...syncConfig,\n      enable\n    }\n    if (enable) {\n      const service = new Service(newConfig)\n\n      try {\n        await service.init()\n      } catch (e) {\n        Modal.confirm({\n          title: t('syncService.shanbay.login'),\n          onOk: () => {\n            Service.openLogin()\n          }\n        })\n        return\n      }\n    }\n\n    try {\n      await uploadSyncConfig('shanbay', newConfig)\n      setSyncConfig(newConfig)\n      AntdMsg.destroy()\n      AntdMsg.success(t('msg_updated'))\n    } catch (e) {\n      notification.error({\n        message: t('config.opt.upload_error'),\n        description: `${e}`\n      })\n    }\n  }\n\n  async function onSyncAll() {\n    const { total } = await getWords('notebook', {\n      itemsPerPage: 1,\n      filters: {}\n    })\n    if (total > 50 && !confirm(t('syncService.shanbay.sync_all_confirm'))) {\n      return\n    }\n\n    await syncWords()\n  }\n\n  async function onSyncLast() {\n    const { words } = await getWords('notebook', {\n      itemsPerPage: 1,\n      filters: {}\n    })\n    if (!words || words.length <= 0) {\n      return\n    }\n\n    await syncWords(words)\n  }\n\n  async function syncWords(words?: Word[]) {\n    AntdMsg.destroy()\n    AntdMsg.success(t('syncService.start'), 0)\n\n    const service = new Service(syncConfig)\n\n    try {\n      const errorCount = await service.addInternal({ words, force: true })\n      AntdMsg.destroy()\n      if (errorCount > 0) {\n        AntdMsg.info(t('syncService.finished'))\n      } else {\n        AntdMsg.success(t('syncService.success'))\n      }\n    } catch (error) {\n      if (error === 'words') return\n      const msgPath = `sync:shanbay.error.${error}`\n      notification.error({\n        message: t('syncService.failed'),\n        description: i18n.exists(msgPath) ? t(msgPath) : `${error}`\n      })\n    }\n  }\n}\n\nexport default ShanbayModal\n"
  },
  {
    "path": "src/options/components/Entries/Notebook/sync-services/webdav.tsx",
    "content": "/* eslint-disable no-throw-literal */\nimport React, { FC, useState, useRef } from 'react'\nimport {\n  Form,\n  Input,\n  Modal,\n  Button,\n  message as antdMsg,\n  notification,\n  Switch\n} from 'antd'\nimport { FormInstance } from 'antd/lib/form'\nimport { ExclamationCircleOutlined } from '@ant-design/icons'\nimport { Service, SyncConfig } from '@/background/sync-manager/services/webdav'\nimport {\n  removeSyncConfig,\n  setSyncConfig\n} from '@/background/sync-manager/helpers'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { InputNumberGroup } from '@/options/components/InputNumberGroup'\n\nexport interface WebdavModalProps {\n  syncConfig?: SyncConfig\n  show: boolean\n  onClose: () => void\n}\n\nexport const WebdavModal: FC<WebdavModalProps> = props => {\n  const { t, i18n } = useTranslate(['options', 'common', 'sync'])\n  const [serviceChecking, setServiceChecking] = useState(false)\n  const formRef = useRef<FormInstance>(null)\n\n  return (\n    <Modal\n      visible={props.show}\n      title={t('sync:webdav.title')}\n      onOk={submitForm}\n      onCancel={closeModal}\n      destroyOnClose\n      footer={[\n        <Button key=\"delete\" type=\"primary\" danger onClick={deleteService}>\n          {t('common:delete')}\n        </Button>,\n        <Button\n          key=\"verify\"\n          disabled={serviceChecking}\n          loading={serviceChecking}\n          onClick={verifyService}\n        >\n          {t('syncService.webdav.verify')}\n        </Button>,\n        <Button\n          key=\"save\"\n          type=\"primary\"\n          disabled={serviceChecking}\n          onClick={submitForm}\n        >\n          {t('common:save')}\n        </Button>,\n        <Button key=\"cancel\" onClick={closeModal}>\n          {t('common:cancel')}\n        </Button>\n      ]}\n    >\n      <Form\n        ref={formRef}\n        initialValues={props.syncConfig || Service.getDefaultConfig()}\n        labelCol={{ span: 5 }}\n        wrapperCol={{ span: 18 }}\n        onFinish={saveService}\n      >\n        <p>\n          {t('syncService.webdav.description')}\n          <a\n            href=\"http://help.jianguoyun.com/?p=2064\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            {t('syncService.webdav.jianguo')}\n          </a>\n        </p>\n        <Form.Item\n          name=\"enable\"\n          label={t('common:enable')}\n          valuePropName=\"checked\"\n        >\n          <Switch />\n        </Form.Item>\n        <Form.Item\n          name=\"url\"\n          label={t('syncService.webdav.url')}\n          hasFeedback\n          rules={[\n            { type: 'url', message: t('form.url_error'), required: true }\n          ]}\n        >\n          <Input />\n        </Form.Item>\n        <Form.Item name=\"user\" label={t('syncService.webdav.user')}>\n          <Input />\n        </Form.Item>\n        <Form.Item name=\"passwd\" label={t('syncService.webdav.passwd')}>\n          <Input type=\"password\" />\n        </Form.Item>\n        <Form.Item\n          name=\"duration\"\n          label={t('syncService.webdav.duration')}\n          extra={t('syncService.webdav.duration_help')}\n          rules={[\n            { type: 'number', message: t('form.number_error'), required: true }\n          ]}\n        >\n          <InputNumberGroup suffix={t('common:unit.mins')} />\n        </Form.Item>\n      </Form>\n    </Modal>\n  )\n\n  function submitForm() {\n    if (formRef.current) {\n      formRef.current.submit()\n    }\n  }\n\n  function closeModal() {\n    if (formRef.current && formRef.current.isFieldsTouched()) {\n      Modal.confirm({\n        title: t('syncService.close_confirm'),\n        icon: <ExclamationCircleOutlined />,\n        okType: 'danger',\n        onOk: props.onClose\n      })\n    } else {\n      props.onClose()\n    }\n  }\n\n  function deleteService() {\n    Modal.confirm({\n      title: t('syncService.delete_confirm'),\n      onOk: () =>\n        tryTo(async () => {\n          await removeSyncConfig(Service.id)\n          props.onClose()\n        })\n    })\n  }\n\n  async function verifyService() {\n    const config = extractConfigFromForm()\n    if (!config) return\n\n    setServiceChecking(true)\n\n    const service = new Service(config)\n\n    try {\n      if (!config.url) {\n        throw new Error('network')\n      }\n      try {\n        await service.init()\n      } catch (error) {\n        const errorText = typeof error === 'string' ? error : error.message\n        if (errorText !== 'exist') {\n          throw error\n        }\n        if (confirm(t('syncService.webdav.exist_confirm'))) {\n          await service.download({ noCache: true })\n        }\n      }\n      if (confirm(t('syncService.webdav.upload_confirm'))) {\n        await service.add({ force: true })\n      }\n      notification.success({ message: t('syncService.webdav.verified') })\n    } catch (error) {\n      notifyError(error)\n    }\n\n    setServiceChecking(false)\n  }\n\n  async function saveService() {\n    const config = extractConfigFromForm()\n    if (!config) return\n\n    if (config.enable) {\n      if (!config.url) {\n        return notifyError('network')\n      }\n\n      setServiceChecking(true)\n\n      const service = new Service(config)\n\n      try {\n        const dir = await service.checkDir()\n        if (!dir) {\n          throw new Error('missing')\n        }\n      } catch (e) {\n        setServiceChecking(false)\n        return notifyError(e)\n      }\n\n      service.setMeta({})\n      setServiceChecking(false)\n    }\n\n    try {\n      await setSyncConfig(Service.id, config)\n      props.onClose()\n    } catch (error) {\n      notifyError(error)\n    }\n  }\n\n  function extractConfigFromForm(): SyncConfig | undefined {\n    if (!formRef.current) {\n      if (process.env.DEBUG) {\n        console.error(new Error('Missing form ref when saving service'))\n      }\n      notification.error({\n        message: 'Error',\n        description: t('sync:webdav.error.internal')\n      })\n      return\n    }\n\n    const values = formRef.current.getFieldsValue()\n\n    return {\n      ...values,\n      url:\n        values.url && !values.url.endsWith('/')\n          ? (values.url += '/')\n          : values.url\n    } as SyncConfig\n  }\n\n  async function tryTo(action: () => any): Promise<void> {\n    try {\n      await action()\n      antdMsg.destroy()\n      antdMsg.success(t('msg_updated'))\n    } catch (error) {\n      notification.error({\n        message: 'Error',\n        description: error.message\n      })\n    }\n  }\n\n  function notifyError(error: Error | string) {\n    const errorText = typeof error === 'string' ? error : error.message\n    const msgPath = 'sync:webdav.error.' + errorText\n    const description = i18n.exists(msgPath) ? t(msgPath) : errorText\n    notification.error({ message: 'Error', description })\n  }\n}\n\nexport default WebdavModal\n"
  },
  {
    "path": "src/options/components/Entries/PDF.tsx",
    "content": "import React, { FC, useState } from 'react'\nimport { Switch, Button, Select } from 'antd'\nimport { getConfigPath } from '@/options/helpers/path-joiner'\nimport { SaladictForm } from '@/options/components/SaladictForm'\nimport { useTranslate, Trans } from '@/_helpers/i18n'\nimport { MatchPatternModal } from '../MatchPatternModal'\n\nexport const PDF: FC = () => {\n  const { t } = useTranslate(['options', 'common'])\n  const [editingArea, setEditingArea] = useState<\n    'pdfBlacklist' | 'pdfWhitelist' | null\n  >(null)\n\n  return (\n    <>\n      <SaladictForm\n        items={[\n          {\n            name: getConfigPath('pdfSniff'),\n            valuePropName: 'checked',\n            extra: (\n              <Trans message={t(getConfigPath('pdfSniff') + '_extra')}>\n                <a\n                  href=\"https://saladict.crimx.com/native.html\"\n                  target=\"_blank\"\n                  rel=\"nofollow noopener noreferrer\"\n                >\n                  {t('nativeSearch')}\n                </a>\n              </Trans>\n            ),\n            children: <Switch />\n          },\n          {\n            name: getConfigPath('pdfStandalone'),\n            children: (\n              <Select>\n                <Select.Option value=\"\">\n                  {t('config.opt.pdfStandalone.default')}\n                </Select.Option>\n                <Select.Option value=\"always\">\n                  {t('config.opt.pdfStandalone.always')}\n                </Select.Option>\n                <Select.Option value=\"manual\">\n                  {t('config.opt.pdfStandalone.manual')}\n                </Select.Option>\n              </Select>\n            )\n          },\n          {\n            key: 'BlackWhiteList',\n            label: t('nav.BlackWhiteList'),\n            help: t('config.opt.pdf_blackwhitelist_help'),\n            children: (\n              <>\n                <Button\n                  style={{ marginRight: 10 }}\n                  onClick={() => setEditingArea('pdfBlacklist')}\n                >\n                  PDF {t('common:blacklist')}\n                </Button>\n                <Button onClick={() => setEditingArea('pdfWhitelist')}>\n                  PDF {t('common:whitelist')}\n                </Button>\n              </>\n            )\n          }\n        ]}\n      />\n      <MatchPatternModal\n        area={editingArea}\n        onClose={() => setEditingArea(null)}\n      />\n    </>\n  )\n}\n"
  },
  {
    "path": "src/options/components/Entries/Permissions.tsx",
    "content": "import React, { FC, useState, useEffect } from 'react'\nimport { Form, Switch, message as antdMsg } from 'antd'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { useFormItemLayout } from '@/options/helpers/layout'\n\nconst permissions = ['clipboardRead', 'clipboardWrite'] as const\n\nexport const Permissions: FC = () => {\n  const formItemLayout = useFormItemLayout()\n  const { t } = useTranslate(['options', 'common'])\n\n  const [status, setStatus] = useState(() =>\n    permissions.reduce((status, permission) => {\n      status[permission] = false\n      return status\n    }, {} as { [p in typeof permissions[number]]: boolean })\n  )\n\n  useEffect(() => {\n    Promise.all(\n      permissions.map(async permission => {\n        try {\n          return await browser.permissions.contains({\n            permissions: [permission]\n          })\n        } catch (e) {\n          console.error(e)\n        }\n        return false\n      })\n    ).then(contains => {\n      setStatus(\n        permissions.reduce((status, permission, i) => {\n          status[permission] = contains[i]\n          return status\n        }, {} as { [p in typeof permissions[number]]: boolean })\n      )\n    })\n  }, [])\n\n  return (\n    <Form {...formItemLayout}>\n      {permissions.map(permission => (\n        <Form.Item\n          key={permission}\n          label={t(`permissions.${permission}`)}\n          help={t(`permissions.${permission}_help`)}\n        >\n          <Switch\n            checked={status[permission]}\n            onChange={async checked => {\n              if (checked) {\n                try {\n                  if (\n                    !(await browser.permissions.request({\n                      permissions: [permission]\n                    }))\n                  ) {\n                    antdMsg.warn(t('permissions.cancelled'))\n                    return\n                  }\n                  antdMsg.success(t('permissions.success'))\n                  setStatus(status => ({\n                    ...status,\n                    [permission]: true\n                  }))\n                } catch (e) {\n                  console.error(e)\n                  antdMsg.error(t('permissions.failed'))\n                }\n              } else {\n                await browser.permissions.remove({\n                  permissions: [permission]\n                })\n                antdMsg.success(t('permissions.cancel_success'))\n                setStatus(status => ({\n                  ...status,\n                  [permission]: false\n                }))\n              }\n            }}\n          />\n        </Form.Item>\n      ))}\n    </Form>\n  )\n}\n"
  },
  {
    "path": "src/options/components/Entries/Popup.tsx",
    "content": "import React, { FC } from 'react'\nimport { Switch, Select, Slider } from 'antd'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { isFirefox } from '@/_helpers/saladict'\nimport { useSelector } from '@/content/redux'\nimport { getConfigPath } from '@/options/helpers/path-joiner'\nimport {\n  SaladictForm,\n  pixelSlideFormatter\n} from '@/options/components/SaladictForm'\n\nexport const Popup: FC = () => {\n  const { t } = useTranslate(['options', 'menus'])\n  const menusIds = useSelector(state => {\n    const ids = Object.keys(state.config.contextMenus.all)\n    if (isFirefox) {\n      return ids.filter(id => {\n        switch (id) {\n          case 'youdao_page_translate':\n          case 'caiyuntrs':\n            return false\n        }\n        return true\n      })\n    }\n    return ids\n  })\n  const { availWidth } = window.screen\n\n  return (\n    <SaladictForm\n      items={[\n        {\n          name: getConfigPath('baOpen'),\n          children: (\n            <Select>\n              <Select.Option value=\"popup_panel\">\n                {t('config.opt.baOpen.popup_panel')}\n              </Select.Option>\n              <Select.Option value=\"popup_fav\">\n                {t('config.opt.baOpen.popup_fav')}\n              </Select.Option>\n              <Select.Option value=\"popup_options\">\n                {t('config.opt.baOpen.popup_options')}\n              </Select.Option>\n              <Select.Option value=\"popup_standalone\">\n                {t('config.opt.baOpen.popup_standalone')}\n              </Select.Option>\n              {menusIds.map(id => (\n                <Select.Option key={id} value={id}>\n                  {t(`menus:${id}`)}\n                </Select.Option>\n              ))}\n            </Select>\n          )\n        },\n        {\n          name: getConfigPath('baWidth'),\n          hide: values => values[getConfigPath('baOpen')] !== 'popup_panel',\n          children: (\n            <Slider\n              tipFormatter={pixelSlideFormatter}\n              min={-1}\n              max={availWidth}\n              marks={{\n                '-1': '-1',\n                450: '450px',\n                [availWidth]: `${availWidth}px`\n              }}\n            />\n          )\n        },\n        {\n          name: getConfigPath('baHeight'),\n          hide: values => values[getConfigPath('baOpen')] !== 'popup_panel',\n          children: (\n            <Slider\n              tipFormatter={pixelSlideFormatter}\n              min={250}\n              max={availWidth}\n              marks={{\n                250: '250px',\n                550: '550px',\n                [availWidth]: `${availWidth}px`\n              }}\n            />\n          )\n        },\n        {\n          name: getConfigPath('baPreload'),\n          label: t('preload.title'),\n          help: t('preload.help'),\n          hide: values => values[getConfigPath('baOpen')] !== 'popup_panel',\n          children: (\n            <Select>\n              <Select.Option value=\"\">{t('common:none')}</Select.Option>\n              <Select.Option value=\"clipboard\">\n                {t('preload.clipboard')}\n              </Select.Option>\n              <Select.Option value=\"selection\">\n                {t('preload.selection')}\n              </Select.Option>\n            </Select>\n          )\n        },\n        {\n          name: getConfigPath('baAuto'),\n          label: t('preload.auto'),\n          help: t('preload.auto_help'),\n          hide: values =>\n            values[getConfigPath('baOpen')] !== 'popup_panel' ||\n            !values[getConfigPath('baPreload')],\n          valuePropName: 'checked',\n          children: <Switch />\n        }\n      ]}\n    />\n  )\n}\n"
  },
  {
    "path": "src/options/components/Entries/Privacy.tsx",
    "content": "import React, { FC } from 'react'\nimport { Switch } from 'antd'\nimport { getConfigPath } from '@/options/helpers/path-joiner'\nimport { SaladictForm } from '@/options/components/SaladictForm'\n\nexport const Privacy: FC = () => {\n  return (\n    <SaladictForm\n      items={[\n        {\n          name: getConfigPath('updateCheck'),\n          valuePropName: 'checked',\n          children: <Switch />\n        },\n        {\n          name: getConfigPath('analytics'),\n          valuePropName: 'checked',\n          children: <Switch />\n        },\n        {\n          name: getConfigPath('searchHistory'),\n          valuePropName: 'checked',\n          children: <Switch />\n        },\n        {\n          name: getConfigPath('searchHistoryInco'),\n          hide: values => !values[getConfigPath('searchHistory')],\n          valuePropName: 'checked',\n          children: <Switch />\n        },\n        {\n          key: 'third_party_privacy',\n          children: <Switch disabled checked />\n        }\n      ]}\n    />\n  )\n}\n"
  },
  {
    "path": "src/options/components/Entries/Profiles/EditNameModal.tsx",
    "content": "import React, { FC, useRef } from 'react'\nimport { Input, Modal } from 'antd'\nimport { ProfileID } from '@/app-config/profiles'\n\nexport interface EditNameModalProps {\n  title: string\n  show: boolean\n  profileID: ProfileID | null\n  onClose: (newProfileID?: ProfileID) => void\n}\n\nexport const EditNameModal: FC<EditNameModalProps> = props => {\n  const inputRef = useRef<Input | null>(null)\n\n  return (\n    <Modal\n      visible={props.show}\n      title={props.title}\n      destroyOnClose\n      onOk={() => {\n        const name = (inputRef.current?.input.value || '').trim()\n        if (name && props.profileID) {\n          props.onClose({\n            ...props.profileID,\n            name\n          })\n        } else {\n          props.onClose()\n        }\n      }}\n      onCancel={() => props.onClose()}\n    >\n      <Input ref={inputRef} autoFocus defaultValue={props.profileID?.name} />\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "src/options/components/Entries/Profiles/index.tsx",
    "content": "import React, { FC, useState, useLayoutEffect } from 'react'\nimport { Row, Col, Modal, notification, message as antdMsg } from 'antd'\nimport { BlockOutlined } from '@ant-design/icons'\nimport { useTranslate, Trans } from '@/_helpers/i18n'\nimport {\n  ProfileID,\n  ProfileIDList,\n  getDefaultProfileID\n} from '@/app-config/profiles'\nimport {\n  getProfileName,\n  updateActiveProfileID,\n  removeProfile,\n  updateProfileIDList,\n  addProfile\n} from '@/_helpers/profile-manager'\nimport { useSelector } from '@/content/redux'\nimport { SortableList, reorder } from '@/options/components/SortableList'\nimport { useListLayout } from '@/options/helpers/layout'\nimport { useCheckDictAuth } from '@/options/helpers/use-check-dict-auth'\nimport { EditNameModal } from './EditNameModal'\n\nexport const Profiles: FC = () => {\n  const { t } = useTranslate('options')\n  const checkDictAuth = useCheckDictAuth()\n  const activeProfileID = useSelector(state => state.activeProfile.id)\n  const [showAddProfileModal, setShowAddProfileModal] = useState(false)\n  const [showEditNameModal, setShowEditNameModal] = useState(false)\n  const [editingProfileID, setEditingProfileID] = useState<ProfileID | null>(\n    null\n  )\n  const listLayout = useListLayout()\n\n  const storeProfileIDList = useSelector(state => state.profiles)\n  // make a local copy to avoid flickering on drag end\n  const [profileIDList, setProfileIDList] = useState<ProfileIDList>(\n    storeProfileIDList\n  )\n  useLayoutEffect(() => {\n    setProfileIDList(storeProfileIDList)\n  }, [storeProfileIDList])\n\n  const tryTo = async (action: () => any): Promise<void> => {\n    try {\n      await action()\n      antdMsg.destroy()\n      antdMsg.success(t('msg_updated'))\n    } catch (error) {\n      notification.error({\n        message: 'Error',\n        description: error.message\n      })\n    }\n  }\n\n  const onEditNameModalClose = (profileID?: ProfileID) => {\n    setShowAddProfileModal(false)\n    setShowEditNameModal(false)\n\n    if (profileID) {\n      tryTo(async () => {\n        if (profileIDList.find(({ id }) => id === profileID.id)) {\n          const newList = profileIDList.map(p =>\n            p.id === profileID.id ? profileID : p\n          )\n          await updateProfileIDList(newList)\n        } else {\n          await addProfile(profileID)\n        }\n      })\n    }\n  }\n\n  return (\n    <Row>\n      <Col {...listLayout}>\n        <SortableList\n          title={t('nav.Profiles')}\n          description={\n            <Trans message={t('profiles.opt.help')}>\n              <BlockOutlined style={{ color: '#f5222d' }} />\n              <kbd>↓</kbd>\n            </Trans>\n          }\n          selected={activeProfileID}\n          list={profileIDList.map(({ id, name }) => ({\n            value: id,\n            title: getProfileName(name, t)\n          }))}\n          onSelect={async ({ target: { value } }) => {\n            if (await checkDictAuth()) {\n              tryTo(() => updateActiveProfileID(value))\n            }\n          }}\n          onAdd={() => {\n            setEditingProfileID({\n              ...getDefaultProfileID(),\n              name: ''\n            })\n            setShowAddProfileModal(true)\n          }}\n          onEdit={index => {\n            setEditingProfileID(\n              profileIDList[index]\n                ? {\n                    ...profileIDList[index],\n                    name: getProfileName(profileIDList[index].name, t)\n                  }\n                : getDefaultProfileID()\n            )\n            setShowEditNameModal(true)\n          }}\n          onDelete={index => {\n            const { id, name } = profileIDList[index]\n            Modal.confirm({\n              title: t('profiles.opt.delete_confirm', {\n                name: getProfileName(name, t)\n              }),\n              onOk: () => tryTo(() => removeProfile(id))\n            })\n          }}\n          onOrderChanged={(oldIndex, newIndex) => {\n            const newList = reorder(profileIDList, oldIndex, newIndex)\n            setProfileIDList(newList)\n            tryTo(() => updateProfileIDList(newList))\n          }}\n        />\n        <EditNameModal\n          title={t('profiles.opt.add_name')}\n          show={showAddProfileModal}\n          profileID={editingProfileID}\n          onClose={onEditNameModalClose}\n        />\n        <EditNameModal\n          title={t('profiles.opt.edit_name')}\n          show={showEditNameModal}\n          profileID={editingProfileID}\n          onClose={onEditNameModalClose}\n        />\n      </Col>\n    </Row>\n  )\n}\n"
  },
  {
    "path": "src/options/components/Entries/Pronunciation.tsx",
    "content": "import React, { FC } from 'react'\nimport { Switch, Select } from 'antd'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { useSelector } from '@/content/redux'\nimport { getConfigPath, getProfilePath } from '@/options/helpers/path-joiner'\nimport { SaladictForm } from '@/options/components/SaladictForm'\n\nexport const Pronunciation: FC = () => {\n  const { t } = useTranslate(['options', 'common', 'dicts'])\n  const autopronLists = useSelector(state => ({\n    cn: state.activeProfile.dicts.all.zdic.options.audio\n      ? state.config.autopron.cn.list\n      : state.config.autopron.cn.list.filter(id => id !== 'zdic'),\n    en: state.config.autopron.en.list,\n    machine: state.config.autopron.machine.list\n  }))\n\n  return (\n    <SaladictForm\n      items={[\n        {\n          name: getConfigPath('autopron', 'cn', 'dict'),\n          children: (\n            <Select>\n              <Select.Option value=\"\">{t('common:none')}</Select.Option>\n              {autopronLists.cn.map(id => (\n                <Select.Option key={id} value={id}>\n                  {t(`dicts:${id}.name`)}\n                </Select.Option>\n              ))}\n            </Select>\n          )\n        },\n        {\n          name: getConfigPath('autopron', 'en', 'dict'),\n          children: (\n            <Select>\n              <Select.Option value=\"\">{t('common:none')}</Select.Option>\n              {autopronLists.en.map(id => (\n                <Select.Option key={id} value={id}>\n                  {t(`dicts:${id}.name`)}\n                </Select.Option>\n              ))}\n            </Select>\n          )\n        },\n        {\n          name: getConfigPath('autopron', 'en', 'accent'),\n          hide: values => !values[getConfigPath('autopron', 'en', 'dict')],\n          children: (\n            <Select>\n              <Select.Option value=\"uk\">\n                {t('config.opt.accent.uk')}\n              </Select.Option>\n              <Select.Option value=\"us\">\n                {t('config.opt.accent.us')}\n              </Select.Option>\n            </Select>\n          )\n        },\n        {\n          name: getConfigPath('autopron', 'machine', 'dict'),\n          children: (\n            <Select>\n              <Select.Option value=\"\">{t('common:none')}</Select.Option>\n              {autopronLists.machine.map(id => (\n                <Select.Option key={id} value={id}>\n                  {t(`dicts:${id}.name`)}\n                </Select.Option>\n              ))}\n            </Select>\n          )\n        },\n        {\n          name: getConfigPath('autopron', 'machine', 'src'),\n          hide: values => !values[getConfigPath('autopron', 'machine', 'dict')],\n          children: (\n            <Select>\n              <Select.Option value=\"trans\">\n                {t('config.autopron.machine.src_trans')}\n              </Select.Option>\n              <Select.Option value=\"searchText\">\n                {t('config.autopron.machine.src_search')}\n              </Select.Option>\n            </Select>\n          )\n        },\n        {\n          name: getProfilePath('waveform'),\n          valuePropName: 'checked',\n          children: <Switch />\n        }\n      ]}\n    />\n  )\n}\n"
  },
  {
    "path": "src/options/components/Entries/QuickSearch/StandaloneModal.tsx",
    "content": "import React, { FC, useState } from 'react'\nimport { Select, Slider, Switch, Button } from 'antd'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { getConfigPath } from '@/options/helpers/path-joiner'\nimport { SaladictModalForm } from '@/options/components/SaladictModalForm'\nimport { pixelSlideFormatter } from '@/options/components/SaladictForm'\nimport { searchMode } from '../SearchModes/searchMode'\nimport { TitlebarOffsetModal } from './TitlebarOffsetModal'\n\nexport interface StandaloneModalProps {\n  show: boolean\n  onClose: () => void\n}\n\nexport const StandaloneModal: FC<StandaloneModalProps> = ({\n  show,\n  onClose\n}) => {\n  const { t } = useTranslate(['options', 'common'])\n  const { availHeight } = window.screen\n  const [showTitlebarOffsetModal, setTitlebarOffsetModal] = useState(false)\n\n  return (\n    <>\n      <SaladictModalForm\n        title={t(getConfigPath('qsStandalone'))}\n        visible={show}\n        onClose={onClose}\n        items={[\n          {\n            name: getConfigPath('qssaSidebar'),\n            children: (\n              <Select>\n                <Select.Option value=\"\">{t('common:none')}</Select.Option>\n                <Select.Option value=\"left\">\n                  {t('locations.LEFT')}\n                </Select.Option>\n                <Select.Option value=\"right\">\n                  {t('locations.RIGHT')}\n                </Select.Option>\n              </Select>\n            )\n          },\n          {\n            name: getConfigPath('qssaHeight'),\n            hide: values => values[getConfigPath('qssaSidebar')],\n            children: (\n              <Slider\n                tipFormatter={pixelSlideFormatter}\n                min={250}\n                max={availHeight}\n                marks={{ 250: '250px', [availHeight]: `${availHeight}px` }}\n              />\n            )\n          },\n          {\n            name: getConfigPath('qssaRectMemo'),\n            valuePropName: 'checked',\n            children: <Switch />\n          },\n          {\n            key: 'titlebar-offset',\n            label: t('titlebarOffset.title'),\n            help: t('titlebarOffset.help'),\n            children: (\n              <Button onClick={() => setTitlebarOffsetModal(true)}>\n                {t('titlebarOffset.title')}\n              </Button>\n            )\n          },\n          {\n            name: getConfigPath('qssaPageSel'),\n            valuePropName: 'checked',\n            children: <Switch />\n          },\n          {\n            ...searchMode('qsPanelMode', t),\n            label: t('page_selection')\n          }\n        ]}\n      />\n      <TitlebarOffsetModal\n        show={showTitlebarOffsetModal}\n        onClose={() => setTitlebarOffsetModal(false)}\n      />\n    </>\n  )\n}\n"
  },
  {
    "path": "src/options/components/Entries/QuickSearch/TitlebarOffsetModal.tsx",
    "content": "import React, { FC, useState, useEffect, useRef } from 'react'\nimport { Modal, Form, Slider, Button, message as antdMsg } from 'antd'\nimport { FormInstance } from 'antd/lib/form'\nimport { ExclamationCircleOutlined } from '@ant-design/icons'\nimport {\n  setTitlebarOffset,\n  TitlebarOffset,\n  getTitlebarOffset,\n  calibrateTitlebarOffset\n} from '@/_helpers/titlebar-offset'\nimport { formItemModalLayout } from '@/options/helpers/layout'\nimport { pixelSlideFormatter } from '@/options/components/SaladictForm'\nimport { useTranslate } from '@/_helpers/i18n'\n\nexport interface TitlebarOffsetModalProps {\n  show: boolean\n  onClose: () => void\n}\n\nexport const TitlebarOffsetModal: FC<TitlebarOffsetModalProps> = props => {\n  const { t } = useTranslate(['options', 'common'])\n  const [offset, setOffset] = useState<TitlebarOffset>()\n  const formRef = useRef<FormInstance>(null)\n\n  useEffect(() => {\n    if (props.show) return\n\n    let stale = false\n\n    getTitlebarOffset().then(titlebarOffset => {\n      if (!stale) {\n        setOffset(\n          titlebarOffset || {\n            main: 0,\n            panel: 0\n          }\n        )\n      }\n    })\n\n    return () => {\n      stale = true\n    }\n  }, [props.show])\n\n  const onSubmit = () => {\n    if (formRef.current) {\n      formRef.current.submit()\n    }\n  }\n\n  const onCancel = () => {\n    if (formRef.current && formRef.current.isFieldsTouched()) {\n      Modal.confirm({\n        title: t('unsave_confirm'),\n        icon: <ExclamationCircleOutlined />,\n        okType: 'danger',\n        onOk: props.onClose\n      })\n    } else {\n      props.onClose()\n    }\n  }\n\n  const onFormFinish = async (values: any) => {\n    if (process.env.DEBUG) {\n      console.log(values)\n    }\n    await setTitlebarOffset(values as TitlebarOffset)\n    antdMsg.destroy()\n    antdMsg.success(t('msg_updated'))\n    props.onClose()\n  }\n\n  const onCalibrate = async () => {\n    const offset = await calibrateTitlebarOffset()\n\n    if (offset && formRef.current) {\n      formRef.current.setFieldsValue(offset)\n      antdMsg.destroy()\n      antdMsg.success(t('titlebarOffset.calibrateSuccess'))\n    } else {\n      antdMsg.destroy()\n      antdMsg.error(t('titlebarOffset.calibrateError'))\n    }\n  }\n\n  return (\n    <Modal\n      title={t('titlebarOffset.title')}\n      visible={props.show && !!offset}\n      onOk={onSubmit}\n      onCancel={onCancel}\n      footer={[\n        <Button key=\"calibration\" onClick={onCalibrate}>\n          {t('titlebarOffset.calibrate')}\n        </Button>,\n        <Button key=\"cancel\" onClick={onCancel}>\n          {t('common:cancel')}\n        </Button>,\n        <Button key=\"submit\" type=\"primary\" onClick={onSubmit}>\n          {t('common:save')}\n        </Button>\n      ]}\n    >\n      <p>{t('titlebarOffset.help')}</p>\n      <Form\n        {...formItemModalLayout}\n        initialValues={offset}\n        ref={formRef}\n        onFinish={onFormFinish}\n      >\n        <Form.Item\n          name=\"main\"\n          label={t('titlebarOffset.main')}\n          help={t('titlebarOffset.main_help')}\n        >\n          <Slider\n            tipFormatter={pixelSlideFormatter}\n            min={0}\n            max={100}\n            marks={{ 0: '0px', 100: '100px' }}\n          />\n        </Form.Item>\n        <Form.Item\n          name=\"panel\"\n          label={t('titlebarOffset.panel')}\n          help={t('titlebarOffset.panel_help')}\n        >\n          <Slider\n            tipFormatter={pixelSlideFormatter}\n            min={0}\n            max={100}\n            marks={{ 0: '0px', 100: '100px' }}\n          />\n        </Form.Item>\n      </Form>\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "src/options/components/Entries/QuickSearch/index.tsx",
    "content": "import React, { FC, useState } from 'react'\nimport { Switch, Select, Button } from 'antd'\nimport { TCDirection } from '@/app-config'\nimport { useTranslate, Trans } from '@/_helpers/i18n'\nimport { getConfigPath } from '@/options/helpers/path-joiner'\nimport { SaladictForm } from '@/options/components/SaladictForm'\nimport { StandaloneModal } from './StandaloneModal'\n\nconst locLocale: Readonly<TCDirection[]> = [\n  'CENTER',\n  'TOP',\n  'RIGHT',\n  'BOTTOM',\n  'LEFT',\n  'TOP_LEFT',\n  'TOP_RIGHT',\n  'BOTTOM_LEFT',\n  'BOTTOM_RIGHT'\n]\n\nexport const QuickSearch: FC = () => {\n  const { t } = useTranslate(['options', 'menus'])\n  const [showStandaloneModal, setShowStandaloneModal] = useState(false)\n\n  return (\n    <>\n      <SaladictForm\n        items={[\n          {\n            name: getConfigPath('tripleCtrl'),\n            help: (\n              <Trans message={t(getConfigPath('tripleCtrl') + '_help')}>\n                <kbd>⌘ Command</kbd>\n                <kbd>Ctrl</kbd>\n              </Trans>\n            ),\n            valuePropName: 'checked',\n            children: <Switch />\n          },\n          {\n            name: getConfigPath('qsLocation'),\n            children: (\n              <Select>\n                {locLocale.map(locale => (\n                  <Select.Option key={locale} value={locale}>\n                    {t(`locations.${locale}`)}\n                  </Select.Option>\n                ))}\n              </Select>\n            )\n          },\n          {\n            name: getConfigPath('qsPreload'),\n            label: t('preload.title'),\n            help: t('preload.help'),\n            children: (\n              <Select>\n                <Select.Option value=\"\">{t('common:none')}</Select.Option>\n                <Select.Option value=\"clipboard\">\n                  {t('preload.clipboard')}\n                </Select.Option>\n                <Select.Option value=\"selection\">\n                  {t('preload.selection')}\n                </Select.Option>\n              </Select>\n            )\n          },\n          {\n            name: getConfigPath('qsAuto'),\n            label: t('preload.auto'),\n            help: t('preload.auto_help'),\n            hide: values => !values[getConfigPath('qsPreload')],\n            valuePropName: 'checked',\n            children: <Switch />\n          },\n          {\n            name: getConfigPath('qsFocus'),\n            valuePropName: 'checked',\n            children: <Switch />\n          },\n          {\n            name: getConfigPath('qsStandalone'),\n            help: (\n              <Trans message={t(getConfigPath('qsStandalone') + '_help')}>\n                <a\n                  href=\"https://saladict.crimx.com/native.html\"\n                  target=\"_blank\"\n                  rel=\"nofollow noopener noreferrer\"\n                >\n                  {t('nativeSearch')}\n                </a>\n              </Trans>\n            ),\n            valuePropName: 'checked',\n            children: <Switch />\n          },\n          {\n            key: 'config.opt.openQsStandalone',\n            children: (\n              <Button onClick={() => setShowStandaloneModal(true)}>\n                {t('config.opt.openQsStandalone')}\n              </Button>\n            )\n          }\n        ]}\n      />\n      <StandaloneModal\n        show={showStandaloneModal}\n        onClose={() => setShowStandaloneModal(false)}\n      />\n    </>\n  )\n}\n"
  },
  {
    "path": "src/options/components/Entries/SearchModes/index.tsx",
    "content": "import React, { FC } from 'react'\nimport { Switch, Checkbox, Slider } from 'antd'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { getConfigPath, getProfilePath } from '@/options/helpers/path-joiner'\nimport { SaladictForm } from '@/options/components/SaladictForm'\nimport { supportedLangs } from '@/_helpers/lang-check'\nimport { searchMode } from './searchMode'\n\nexport const SearchModes: FC = () => {\n  const { t } = useTranslate(['options', 'common'])\n  return (\n    <SaladictForm\n      items={[\n        {\n          name: getConfigPath('noTypeField'),\n          valuePropName: 'checked',\n          children: <Switch />\n        },\n        {\n          name: getConfigPath('touchMode'),\n          valuePropName: 'checked',\n          children: <Switch />\n        },\n        {\n          key: getConfigPath('language'),\n          className: 'saladict-form-danger-extra',\n          items: supportedLangs.map(lang => ({\n            name: getConfigPath('language', lang),\n            className: 'form-item-inline',\n            valuePropName: 'checked',\n            children: <Checkbox>{t(`common:lang.${lang}`)}</Checkbox>\n          }))\n        },\n        {\n          name: getProfilePath('stickyFold'),\n          valuePropName: 'checked',\n          children: <Switch />\n        },\n        {\n          name: getConfigPath('doubleClickDelay'),\n          children: (\n            <Slider\n              tipFormatter={v => v + t('common:unit.ms')}\n              min={100}\n              max={2000}\n              marks={{\n                100: '0.1' + t('common:unit.s'),\n                2000: '2' + t('common:unit.s')\n              }}\n            />\n          )\n        },\n        searchMode('mode', t),\n        searchMode('pinMode', t),\n        searchMode('panelMode', t),\n        searchMode('qsPanelMode', t)\n      ]}\n    />\n  )\n}\n"
  },
  {
    "path": "src/options/components/Entries/SearchModes/searchMode.tsx",
    "content": "import React from 'react'\nimport { Checkbox, Slider, Select } from 'antd'\nimport { TFunction } from 'i18next'\nimport {\n  SaladictFormItem,\n  pixelSlideFormatter\n} from '@/options/components/SaladictForm'\nimport { getConfigPath } from '@/options/helpers/path-joiner'\n\ntype Mode = 'mode' | 'pinMode' | 'panelMode' | 'qsPanelMode'\n\nexport const searchMode = (mode: Mode, t: TFunction): SaladictFormItem => {\n  const items: SaladictFormItem[] = []\n\n  if (mode === 'mode') {\n    items.push(\n      {\n        name: getConfigPath('mode', 'icon'),\n        label: null,\n        help: t('searchMode.icon_help'),\n        valuePropName: 'checked',\n        children: <Checkbox>{t('searchMode.icon')}</Checkbox>\n      },\n      {\n        name: getConfigPath('bowlHover'),\n        label: null,\n        hide: values => !values[getConfigPath('mode', 'icon')],\n        valuePropName: 'checked',\n        children: <Checkbox>{t(getConfigPath('bowlHover'))}</Checkbox>\n      },\n      {\n        name: getConfigPath('bowlOffsetX'),\n        hide: values => !values[getConfigPath('mode', 'icon')],\n        children: (\n          <Slider\n            tipFormatter={pixelSlideFormatter}\n            min={-100}\n            max={100}\n            marks={{ '-100': '-100px', 0: '0px', 100: '100px' }}\n            style={{ marginBottom: 0 }}\n          />\n        )\n      },\n      {\n        name: getConfigPath('bowlOffsetY'),\n        hide: values => !values[getConfigPath('mode', 'icon')],\n        children: (\n          <Slider\n            tipFormatter={pixelSlideFormatter}\n            min={-100}\n            max={100}\n            marks={{ '-100': '-100px', 0: '0px', 100: '100px' }}\n            style={{ marginBottom: 0 }}\n          />\n        )\n      }\n    )\n  }\n\n  items.push(\n    {\n      name: getConfigPath(mode, 'direct'),\n      label: null,\n      help: t('searchMode.direct_help'),\n      valuePropName: 'checked',\n      children: <Checkbox>{t('searchMode.direct')}</Checkbox>\n    },\n    {\n      name: getConfigPath(mode, 'double'),\n      label: null,\n      help: t('searchMode.double_help'),\n      valuePropName: 'checked',\n      children: <Checkbox>{t('searchMode.double')}</Checkbox>\n    },\n    {\n      key: getConfigPath(mode, 'holding'),\n      label: null,\n      help: t('searchMode.holding_help'),\n      items: [\n        {\n          name: getConfigPath(mode, 'holding', 'ctrl'),\n          label: null,\n          className: 'form-item-inline',\n          valuePropName: 'checked',\n          children: (\n            <Checkbox>\n              <kbd>Ctrl</kbd>\n            </Checkbox>\n          )\n        },\n        {\n          name: getConfigPath(mode, 'holding', 'alt'),\n          label: null,\n          className: 'form-item-inline',\n          valuePropName: 'checked',\n          children: (\n            <Checkbox>\n              <kbd>Alt</kbd>\n            </Checkbox>\n          )\n        },\n        {\n          name: getConfigPath(mode, 'holding', 'shift'),\n          label: null,\n          className: 'form-item-inline',\n          valuePropName: 'checked',\n          children: (\n            <Checkbox>\n              <kbd>Shift</kbd>\n            </Checkbox>\n          )\n        },\n        {\n          name: getConfigPath(mode, 'holding', 'meta'),\n          label: null,\n          className: 'form-item-inline',\n          valuePropName: 'checked',\n          children: (\n            <Checkbox>\n              <kbd>Meta</kbd>\n            </Checkbox>\n          )\n        }\n      ]\n    },\n    {\n      name: getConfigPath(mode, 'instant', 'enable'),\n      label: null,\n      help: t('searchMode.instant_help'),\n      valuePropName: 'checked',\n      children: <Checkbox>{t('searchMode.instant')}</Checkbox>\n    },\n    {\n      name: getConfigPath(mode, 'instant', 'key'),\n      label: t('searchMode.instantKey'),\n      help: t('searchMode.instantKey_help'),\n      hide: values => !values[getConfigPath(mode, 'instant', 'enable')],\n      children: (\n        <Select style={{ width: 100 }}>\n          <Select.Option value=\"direct\">\n            {t('searchMode.instantDirect')}\n          </Select.Option>\n          <Select.Option value=\"ctrl\">Ctrl/⌘</Select.Option>\n          <Select.Option value=\"alt\">Alt</Select.Option>\n          <Select.Option value=\"shift\">Shift</Select.Option>\n        </Select>\n      )\n    },\n    {\n      name: getConfigPath(mode, 'instant', 'delay'),\n      label: t('searchMode.instantDelay'),\n      hide: values => !values[getConfigPath(mode, 'instant', 'enable')],\n      children: (\n        <Slider\n          tipFormatter={v => v + t('common:unit.ms')}\n          min={100}\n          max={2000}\n          marks={{\n            100: '0.1' + t('common:unit.s'),\n            2000: '2' + t('common:unit.s')\n          }}\n        />\n      )\n    }\n  )\n\n  return {\n    key: getConfigPath(mode),\n    items\n  }\n}\n"
  },
  {
    "path": "src/options/components/EntryError.tsx",
    "content": "import React, { FC, useEffect } from 'react'\nimport { FrownOutlined } from '@ant-design/icons'\nimport { message } from '@/_helpers/browser-api'\n\nexport const EntryError: FC = () => {\n  useEffect(() => {\n    message.self.send({ type: 'CLOSE_PANEL' })\n  }, [])\n\n  return (\n    <div\n      style={{\n        height: 'calc(100vh - 160px)',\n        display: 'flex',\n        flexDirection: 'column',\n        justifyContent: 'center',\n        alignItems: 'center'\n      }}\n    >\n      <FrownOutlined\n        style={{ fontSize: 80, color: '#eb2f96', marginBottom: 10 }}\n      />\n      <h1>Entry Not Found</h1>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/options/components/EntrySideBar/_style.scss",
    "content": "@import '@/_sass_shared/_fancy-scrollbar.scss';\n\n.entry-sidebar {\n  height: calc(100vh - 64px);\n  background: transparent;\n\n  &.isAffixed {\n    height: 100vh;\n  }\n\n  @media (hover: hover) {\n    overflow-y: hidden;\n\n    &:hover {\n      overflow-y: auto;\n    }\n  }\n}\n"
  },
  {
    "path": "src/options/components/EntrySideBar/index.tsx",
    "content": "import React, { FC, useState } from 'react'\nimport { Layout, Menu, Affix, Modal } from 'antd'\nimport {\n  SettingOutlined,\n  TagsOutlined,\n  DashboardOutlined,\n  ProfileOutlined,\n  SelectOutlined,\n  BookOutlined,\n  SoundOutlined,\n  FilePdfOutlined,\n  DatabaseOutlined,\n  LayoutOutlined,\n  FlagOutlined,\n  ExceptionOutlined,\n  SwapOutlined,\n  LockOutlined,\n  ExclamationCircleOutlined,\n  SafetyCertificateOutlined,\n  KeyOutlined\n} from '@ant-design/icons'\nimport { useObservableState } from 'observable-hooks'\nimport classnames from 'classnames'\nimport { debounceTime, scan, distinctUntilChanged } from 'rxjs/operators'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { setFormDirty, useFormDirty } from '@/options/helpers/use-form-dirty'\n\nimport './_style.scss'\n\nexport interface EntrySideBarProps {\n  entry: string\n  onChange: (entry: string) => void\n}\n\nexport const EntrySideBar: FC<EntrySideBarProps> = props => {\n  const { t } = useTranslate('options')\n  const formDirtyRef = useFormDirty()\n  // trigger affix rerendering on collapse state changes to update width\n  const [affixKey, onCollapse] = useObservableState<number, boolean>(event$ =>\n    event$.pipe(\n      distinctUntilChanged(), // onCollapse will be triggered on initial collapsed state\n      debounceTime(500), // wait for transition\n      scan(id => (id + 1) % 10000, 0) // unique id\n    )\n  )\n  const [affixed, setAffixed] = useState<boolean>()\n\n  return (\n    <Affix key={affixKey} onChange={setAffixed}>\n      <Layout>\n        <Layout.Sider\n          className={classnames('entry-sidebar', 'fancy-scrollbar', {\n            isAffixed: affixed\n          })}\n          width={180}\n          breakpoint=\"lg\"\n          collapsible\n          trigger={null}\n          onCollapse={onCollapse}\n        >\n          <Menu\n            mode=\"inline\"\n            selectedKeys={[props.entry]}\n            onSelect={({ key }) => {\n              const switchTab = () => {\n                props.onChange(`${key}`)\n                setFormDirty(false)\n              }\n              if (formDirtyRef.value) {\n                Modal.confirm({\n                  title: t('unsave_confirm'),\n                  icon: <ExclamationCircleOutlined />,\n                  okType: 'danger',\n                  onOk: switchTab\n                })\n              } else {\n                switchTab()\n              }\n            }}\n          >\n            <Menu.Item key=\"General\">\n              <SettingOutlined />\n              <span>{t('nav.General')}</span>\n            </Menu.Item>\n            <Menu.Item key=\"Notebook\">\n              <TagsOutlined />\n              <span>{t('nav.Notebook')}</span>\n            </Menu.Item>\n            <Menu.Item key=\"Profiles\">\n              <DashboardOutlined />\n              <span>{t('nav.Profiles')}</span>\n            </Menu.Item>\n            <Menu.Item key=\"DictPanel\">\n              <ProfileOutlined />\n              <span>{t('nav.DictPanel')}</span>\n            </Menu.Item>\n            <Menu.Item key=\"SearchModes\">\n              <SelectOutlined />\n              <span>{t('nav.SearchModes')}</span>\n            </Menu.Item>\n            <Menu.Item key=\"Dictionaries\">\n              <BookOutlined />\n              <span>{t('nav.Dictionaries')}</span>\n            </Menu.Item>\n            <Menu.Item key=\"DictAuths\">\n              <KeyOutlined />\n              <span>{t('nav.DictAuths')}</span>\n            </Menu.Item>\n            <Menu.Item key=\"Popup\">\n              <LayoutOutlined />\n              <span>{t('nav.Popup')}</span>\n            </Menu.Item>\n            <Menu.Item key=\"QuickSearch\">\n              <FlagOutlined />\n              <span>{t('nav.QuickSearch')}</span>\n            </Menu.Item>\n            <Menu.Item key=\"Pronunciation\">\n              <SoundOutlined />\n              <span>{t('nav.Pronunciation')}</span>\n            </Menu.Item>\n            <Menu.Item key=\"PDF\">\n              <FilePdfOutlined />\n              <span>{t('nav.PDF')}</span>\n            </Menu.Item>\n            <Menu.Item key=\"ContextMenus\">\n              <DatabaseOutlined />\n              <span>{t('nav.ContextMenus')}</span>\n            </Menu.Item>\n            <Menu.Item key=\"BlackWhiteList\">\n              <ExceptionOutlined />\n              <span>{t('nav.BlackWhiteList')}</span>\n            </Menu.Item>\n            <Menu.Item key=\"ImportExport\">\n              <SwapOutlined />\n              <span>{t('nav.ImportExport')}</span>\n            </Menu.Item>\n            <Menu.Item key=\"Privacy\">\n              <SafetyCertificateOutlined />\n              <span>{t('nav.Privacy')}</span>\n            </Menu.Item>\n            <Menu.Item key=\"Permissions\">\n              <LockOutlined />\n              <span>{t('nav.Permissions')}</span>\n            </Menu.Item>\n          </Menu>\n        </Layout.Sider>\n      </Layout>\n    </Affix>\n  )\n}\n\nexport const EntrySideBarMemo = React.memo(EntrySideBar)\n"
  },
  {
    "path": "src/options/components/Header/HeadInfo/AckList.tsx",
    "content": "import React from 'react'\nimport { List } from 'antd'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { acknowledgement } from '@/options/acknowledgement'\n\nexport const AckList = React.memo(() => {\n  const { t } = useTranslate('options')\n  return (\n    <List\n      dataSource={acknowledgement.map((ack, i) => (\n        <div key={i}>\n          <a href={ack.href} rel=\"nofollow noopener noreferrer\" target=\"_blank\">\n            {ack.name}\n          </a>{' '}\n          {t(`headInfo.acknowledgement.${ack.locale}`)}\n        </div>\n      ))}\n      renderItem={item => <List.Item>{item}</List.Item>}\n    />\n  )\n})\n"
  },
  {
    "path": "src/options/components/Header/HeadInfo/_style.scss",
    "content": ".head-info {\n  align-self: flex-end;\n  display: flex;\n  align-items: flex-end;\n  margin: 0 0 10px 0;\n  padding: 0;\n\n  & > li {\n    margin: 0 0 0 8px;\n    list-style-type: none;\n\n    & > a {\n      color: #fff;\n      opacity: 0.65;\n      transition: opacity 0.4s;\n\n      &:hover,\n      &:active,\n      &:focus {\n        opacity: 1;\n      }\n    }\n  }\n}\n\n.head-info-bubble-wrap {\n  position: relative;\n}\n\n.head-info-bubble {\n  position: absolute;\n  z-index: $global-zindex-tooltip;\n  top: 80%;\n  right: 0;\n  padding: 8px 8px;\n  line-height: 1.6;\n  color: var(--opt-color);\n  background-color: var(--opt-background-color);\n  border-radius: 15px;\n  box-shadow: 3px 4px 31px -8px rgba(0, 0, 0, 0.8);\n\n  & > ol {\n    margin: 0;\n\n    & > li {\n      white-space: nowrap;\n      line-height: 2;\n      padding-right: 20px;\n    }\n  }\n}\n\n.head-info-donate {\n  display: flex;\n  white-space: nowrap;\n  margin-bottom: 1em;\n\n  img {\n    width: 200px;\n    height: 200px;\n    margin-right: 10px;\n\n    &:last-child {\n      margin-right: 0;\n    }\n  }\n}\n\n.head-info-fade-enter {\n  opacity: 0;\n  transition: opacity 0.5s;\n}\n\n.head-info-fade-enter-active,\n.head-info-fade-exit {\n  opacity: 1;\n  transition: opacity 0.5s;\n}\n\n.head-info-fade-exit-active {\n  opacity: 0;\n  transition: opacity 0.5s;\n}\n\n.head-info-unin {\n  @media screen and (max-width: 800px) {\n    display: none !important;\n  }\n}\n"
  },
  {
    "path": "src/options/components/Header/HeadInfo/index.tsx",
    "content": "import React, { FC } from 'react'\nimport { Tooltip, Popover } from 'antd'\nimport { WarningOutlined } from '@ant-design/icons'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { AckList } from './AckList'\n\nimport './_style.scss'\n\nexport const HeadInfo: FC = () => {\n  const { t } = useTranslate('options')\n  return (\n    <ul className=\"head-info\">\n      {process.env.DEBUG || process.env.SDAPP_VETTED ? null : (\n        <li className=\"head-info-bubble-wrap\">\n          <Tooltip\n            placement=\"bottom\"\n            title={decodeURI(\n              '%E6%AD%A4%E6%89%A9%E5%B1%95%E5%B7%B2%E8%A2%AB%E5%86%8D%E6%AC%A1%E6%89%93%E5%8C%85%EF%BC%8C%E5%8F%AF%E8%83%BD%E5%B7%B2%E8%A2%AB%E5%8A%A0%E5%85%A5%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81%EF%BC%8C%E8%AF%B7%E5%89%8D%E5%BE%80%E3%80%8C%E6%B2%99%E6%8B%89%E6%9F%A5%E8%AF%8D%E3%80%8D%E5%AE%98%E6%96%B9%E5%BB%BA%E8%AE%AE%E7%9A%84%E5%B9%B3%E5%8F%B0%E5%AE%89%E8%A3%85'\n            )}\n          >\n            <span style={{ color: '#fff' }}>\n              <WarningOutlined />{' '}\n              {decodeURI('%E6%BD%9C%E5%9C%A8%E5%A8%81%E8%83%81')}\n            </span>\n          </Tooltip>\n        </li>\n      )}\n      <li className=\"head-info-bubble-wrap head-info-unin\">\n        <Popover placement=\"bottomRight\" content={<AckList />}>\n          <a\n            href=\"https://github.com/crimx/ext-saladict/wiki#acknowledgement\"\n            onClick={preventDefault}\n          >\n            {t('headInfo.acknowledgement.title')}\n          </a>\n        </Popover>\n      </li>\n      <li>\n        <a\n          href=\"https://saladict.crimx.com/manual.html\"\n          target=\"_blank\"\n          rel=\"nofollow noopener noreferrer\"\n        >\n          {t('headInfo.instructions')}\n        </a>\n      </li>\n      <li>\n        <a\n          href=\"https://saladict.crimx.com/support.html\"\n          target=\"_blank\"\n          rel=\"nofollow noopener noreferrer\"\n        >\n          💪{t('headInfo.donate')}\n        </a>\n      </li>\n      <li>\n        <a\n          href=\"https://github.com/crimx/ext-saladict/issues\"\n          target=\"_blank\"\n          rel=\"nofollow noopener noreferrer\"\n        >\n          {t('headInfo.report_issue')}\n        </a>\n      </li>\n    </ul>\n  )\n}\n\nexport const HeadInfoMemo = React.memo(HeadInfo)\n\nfunction preventDefault(e: React.MouseEvent<HTMLElement>) {\n  e.preventDefault()\n}\n"
  },
  {
    "path": "src/options/components/Header/_style.scss",
    "content": ".options-header {\n  max-width: 1440px;\n  height: 100%;\n  margin: 0 auto;\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  line-height: 1.2;\n\n  @media screen and (max-width: 800px) {\n    margin: 0 -20px !important;\n  }\n\n  @media screen and (max-width: 600px) {\n    margin: 0 -40px !important;\n  }\n\n  > * {\n    color: #fff;\n  }\n}\n\n.options-header-title {\n  text-align: right;\n\n  h1 {\n    font-size: 1.5em;\n    color: #fff;\n    margin: 0;\n  }\n\n  span {\n    opacity: 0.65;\n  }\n}\n"
  },
  {
    "path": "src/options/components/Header/index.tsx",
    "content": "import React, { FC, useMemo } from 'react'\nimport { shallowEqual } from 'react-redux'\nimport { Layout } from 'antd'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { getProfileName } from '@/_helpers/profile-manager'\nimport { useSelector } from '@/content/redux'\nimport { HeadInfoMemo } from './HeadInfo'\n\nimport './_style.scss'\n\nexport interface HeaderProps {\n  openProfilesTab: (entry: 'Profiles') => void\n}\n\nexport const Header: FC<HeaderProps> = props => {\n  const { t, ready } = useTranslate(['options', 'common'])\n  const { profileId, profileIDList } = useSelector(\n    state => ({\n      profileId: state.activeProfile.id,\n      profileIDList: state.profiles\n    }),\n    shallowEqual\n  )\n\n  const version = useMemo(() => 'v' + browser.runtime.getManifest().version, [])\n\n  const profileName = useMemo(\n    () =>\n      ready\n        ? `「 ${getProfileName(\n            profileIDList.find(({ id }) => id === profileId)?.name || '',\n            t\n          )} 」`\n        : '',\n    [profileId, profileIDList, ready]\n  )\n\n  return (\n    <Layout.Header>\n      <div className=\"options-header\">\n        <div className=\"options-header-title\">\n          <h1>{t('title')}</h1>\n          <span>{version}</span>\n        </div>\n        <a\n          href=\"/?menuselected=Profiles\"\n          onClick={e => {\n            e.preventDefault()\n            e.stopPropagation()\n            props.openProfilesTab('Profiles')\n          }}\n        >\n          {profileName}\n        </a>\n        <HeadInfoMemo />\n      </div>\n    </Layout.Header>\n  )\n}\n\nexport const HeaderMemo = React.memo(Header)\n"
  },
  {
    "path": "src/options/components/InputNumberGroup/_style.scss",
    "content": ".input-number-group-wrapper {\n  display: inline-block;\n  vertical-align: top;\n  width: auto;\n}\n\nform .input-number-group-wrapper {\n  display: inline-block;\n  vertical-align: middle;\n  width: auto;\n  position: relative;\n  top: -1px;\n}\n\n.input-number-group {\n  font-size: 14px;\n  font-variant: tabular-nums;\n  line-height: 1.5;\n  color: rgba(0,0,0,0.65);\n  margin: 0;\n  padding: 0;\n  list-style: none;\n  position: relative;\n  display: table;\n  border-collapse: separate;\n  border-spacing: 0;\n  width: 100%;\n\n  &>.ant-input-number {\n    display: table-cell;\n    float: left;\n    width: auto;\n    margin-bottom: 0;\n  }\n\n  &>.ant-input-number:first-child {\n    border-bottom-right-radius: 0;\n    border-top-right-radius: 0;\n  }\n}\n\n.input-number-group-addon {\n  display: table-cell;\n  width: 1px;\n  white-space: nowrap;\n  vertical-align: middle;\n  padding: 0 11px;\n  font-size: 14px;\n  font-weight: normal;\n  line-height: 1;\n  color: rgba(0,0,0,0.65);\n  text-align: center;\n  background-color: #fafafa;\n  border: 1px solid #d9d9d9;\n  border-radius: 4px;\n  position: relative;\n  transition: all .3s;\n\n  &:last-child {\n    border-left: 0;\n    border-bottom-left-radius: 0;\n    border-top-left-radius: 0;\n  }\n}\n"
  },
  {
    "path": "src/options/components/InputNumberGroup/index.tsx",
    "content": "import React, { FC } from 'react'\nimport { InputNumber } from 'antd'\nimport { InputNumberProps } from 'antd/lib/input-number'\n\nimport './_style.scss'\n\nexport interface InputNumberGroupProps extends InputNumberProps {\n  suffix?: React.ReactNode\n}\n\nexport const InputNumberGroup: FC<InputNumberGroupProps> = props => {\n  const { suffix, ...restProps } = props\n  return (\n    <span className=\"input-number-group-wrapper\">\n      <span className=\"input-number-group\">\n        <InputNumber {...restProps} />\n        {suffix && <span className=\"input-number-group-addon\">{suffix}</span>}\n      </span>\n    </span>\n  )\n}\n"
  },
  {
    "path": "src/options/components/MainEntry.tsx",
    "content": "import React, { FC, useState, useEffect, useContext, useRef } from 'react'\nimport { Helmet } from 'react-helmet'\nimport { shallowEqual } from 'react-redux'\nimport { Layout, Row, Col, message as antMsg } from 'antd'\nimport { useSelector } from '@/content/redux'\nimport { reportPageView } from '@/_helpers/analytics'\nimport { ErrorBoundary } from '@/components/ErrorBoundary'\nimport { useTranslate, I18nContext } from '@/_helpers/i18n'\nimport { ChangeEntryContext } from '../helpers/change-entry'\nimport { useFormDirty } from '../helpers/use-form-dirty'\nimport { EntrySideBarMemo } from './EntrySideBar'\nimport { HeaderMemo } from './Header'\nimport { EntryError } from './EntryError'\nimport { BtnPreviewMemo } from './BtnPreview'\n\nconst EntryComponent = React.memo(({ entry }: { entry: string }) =>\n  React.createElement(require(`./Entries/${entry}`)[entry])\n)\n\nexport const MainEntry: FC = () => {\n  const lang = useContext(I18nContext)\n  const { t, ready } = useTranslate('options')\n  const [entry, setEntry] = useState(getEntry)\n  const formDirtyRef = useFormDirty()\n  const warnedMissingPermissionRef = useRef(false)\n  const { analytics, darkMode } = useSelector(\n    state => ({\n      analytics: state.config.analytics,\n      darkMode: state.config.darkMode\n    }),\n    shallowEqual\n  )\n\n  useEffect(() => {\n    if (getEntry() !== entry) {\n      const { protocol, host, pathname } = window.location\n      const newurl = `${protocol}//${host}${pathname}?menuselected=${entry}`\n      window.history.pushState({ key: entry }, '', newurl)\n    }\n    if (analytics) {\n      reportPageView(`/options/${entry}`)\n    }\n  }, [entry, analytics])\n\n  useEffect(() => {\n    // Warn about unsaved settings before closing window\n    window.addEventListener('beforeunload', e => {\n      if (formDirtyRef.value) {\n        e.preventDefault()\n        e.returnValue = t('unsave_confirm')\n      }\n    })\n  }, [])\n\n  useEffect(() => {\n    if (ready && !warnedMissingPermissionRef.current) {\n      warnedMissingPermissionRef.current = true\n      const permission = new URL(document.URL).searchParams.get(\n        'missing_permission'\n      )\n      if (permission) {\n        antMsg.warn(\n          t('permissions.missing', {\n            permission: t(`permissions.${permission}`)\n          }),\n          20\n        )\n      }\n    }\n  }, [Boolean(ready)])\n\n  return (\n    <>\n      <Helmet>\n        {ready && <title>{`${t('title')} - ${t('nav.' + entry)}`}</title>}\n      </Helmet>\n      <HeaderMemo openProfilesTab={setEntry} />\n      <Layout\n        style={{ maxWidth: 1400, margin: '0 auto' }}\n        className={`main-entry${darkMode ? ' dark-mode' : ''}`}\n      >\n        <Row>\n          <Col>\n            <EntrySideBarMemo entry={entry} onChange={setEntry} />\n          </Col>\n          <Col style={{ flex: '1' }}>\n            <Layout style={{ padding: 24 }}>\n              <Layout.Content\n                data-option-content={entry} // for utools hiding unused options\n                style={{\n                  padding: 24,\n                  backgroundColor: 'var(--opt-background-color)'\n                }}\n              >\n                <ChangeEntryContext.Provider value={setEntry}>\n                  <ErrorBoundary key={entry + lang} error={EntryError}>\n                    {ready && <EntryComponent entry={entry} />}\n                  </ErrorBoundary>\n                </ChangeEntryContext.Provider>\n              </Layout.Content>\n            </Layout>\n          </Col>\n        </Row>\n        <BtnPreviewMemo />\n      </Layout>\n    </>\n  )\n}\n\nfunction getEntry(): string {\n  return new URL(document.URL).searchParams.get('menuselected') || 'General'\n}\n"
  },
  {
    "path": "src/options/components/MatchPatternModal/ PatternItem.tsx",
    "content": "import React, { FC, useState } from 'react'\nimport { Input, Select } from 'antd'\nimport { useTranslate } from '@/_helpers/i18n'\n\nexport interface PatternItemProps {\n  value?: [string, string]\n  onChange?: (value: [string, string]) => void\n}\n\nexport const PatternItem: FC<PatternItemProps> = ({ value, onChange }) => {\n  const { t } = useTranslate('options')\n  const [patternType, setPatternType] = useState<'0' | '1'>(\n    value?.[1] ? '1' : '0'\n  )\n\n  return (\n    <Input\n      addonBefore={\n        <Select\n          value={patternType}\n          onChange={setPatternType}\n          className=\"select-before\"\n        >\n          <Select.Option value=\"0\">{t('matchPattern.regex')}</Select.Option>\n          <Select.Option value=\"1\">{t('matchPattern.url')}</Select.Option>\n        </Select>\n      }\n      value={value?.[patternType]}\n      onChange={e => {\n        if (patternType === '0') {\n          // regex\n          onChange && onChange([e.currentTarget.value, ''])\n        } else {\n          // url\n          onChange && onChange(['', e.currentTarget.value])\n        }\n      }}\n    />\n  )\n}\n"
  },
  {
    "path": "src/options/components/MatchPatternModal/index.tsx",
    "content": "import React, { FC, useRef } from 'react'\nimport { shallowEqual } from 'react-redux'\nimport { useUpdateEffect } from 'react-use'\nimport { useObservableState } from 'observable-hooks'\nimport { Form, Modal, Button } from 'antd'\nimport { FormInstance, Rule } from 'antd/lib/form'\nimport { ExclamationCircleOutlined, PlusOutlined } from '@ant-design/icons'\nimport { useTranslate, Trans } from '@/_helpers/i18n'\nimport { matchPatternToRegExpStr } from '@/_helpers/matchPatternToRegExpStr'\nimport { useSelector } from '@/content/redux'\nimport { getConfigPath } from '@/options/helpers/path-joiner'\nimport { useUpload, uploadStatus$ } from '@/options/helpers/upload'\nimport { PatternItem } from './ PatternItem'\n\nexport interface MatchPatternModalProps {\n  area: null | 'pdfWhitelist' | 'pdfBlacklist' | 'whitelist' | 'blacklist'\n  onClose: () => void\n}\n\nexport const MatchPatternModal: FC<MatchPatternModalProps> = ({\n  area,\n  onClose\n}) => {\n  const { t } = useTranslate(['options', 'common'])\n  const formRef = useRef<FormInstance>(null)\n  const uploadStatus = useObservableState(uploadStatus$, 'idle')\n  const patterns = useSelector(\n    state => ({\n      pdfWhitelist: state.config.pdfWhitelist,\n      pdfBlacklist: state.config.pdfBlacklist,\n      whitelist: state.config.whitelist,\n      blacklist: state.config.blacklist\n    }),\n    shallowEqual\n  )\n  const upload = useUpload()\n\n  useUpdateEffect(() => {\n    if (area && uploadStatus === 'idle') {\n      onClose()\n    }\n  }, [uploadStatus])\n\n  const title = area\n    ? (area.startsWith('pdf') ? 'PDF ' : '') +\n      t(area.endsWith('hitelist') ? 'common:whitelist' : 'common:blacklist')\n    : t('nav.BlackWhiteList')\n\n  async function validatePatterns(rule: Rule, value: [string, string]) {\n    if (value[1]) {\n      // url\n      value[0] = matchPatternToRegExpStr(value[1])\n      if (!value[0]) {\n        throw new Error(t('matchPattern.url_error'))\n      }\n    } else if (value[0]) {\n      // regex\n      try {\n        RegExp(value[0])\n      } catch (e) {\n        throw new Error(t('matchPattern.regex_error'))\n      }\n    }\n  }\n\n  return (\n    <Modal\n      visible={!!area}\n      title={title}\n      destroyOnClose\n      onOk={() => {\n        if (formRef.current) {\n          formRef.current.submit()\n        }\n      }}\n      onCancel={() => {\n        if (formRef.current && formRef.current.isFieldsTouched()) {\n          Modal.confirm({\n            title: t('unsave_confirm'),\n            icon: <ExclamationCircleOutlined />,\n            okType: 'danger',\n            onOk: onClose\n          })\n        } else {\n          onClose()\n        }\n      }}\n    >\n      <p>\n        <Trans message={t('matchPattern.description')}>\n          <a\n            href=\"https://developer.mozilla.org/zh-CN/Add-ons/WebExtensions/Match_patterns#范例\"\n            target=\"_blank\"\n            rel=\"nofollow noopener noreferrer\"\n          >\n            {t('matchPattern.url')}\n          </a>\n          <a\n            href=\"https://deerchao.cn/tutorials/regex/regex.htm\"\n            target=\"_blank\"\n            rel=\"nofollow noopener noreferrer\"\n          >\n            {t('matchPattern.regex')}\n          </a>\n        </Trans>\n      </p>\n      <Form\n        ref={formRef}\n        wrapperCol={{ span: 24 }}\n        initialValues={area ? { patterns: patterns[area] } : {}}\n        onFinish={values => {\n          if (area) {\n            const patterns: [string, string][] | undefined = values.patterns\n            upload({\n              [getConfigPath(area)]: (patterns || []).filter(p => p[0])\n            })\n          }\n        }}\n      >\n        <Form.List name=\"patterns\">\n          {(fields, { add }) => (\n            <div>\n              {fields.map(field => (\n                <Form.Item\n                  // @ts-ignore\n                  key={field.key}\n                  {...field}\n                  validateTrigger={['onChange', 'onBlur']}\n                  hasFeedback\n                  rules={[{ validator: validatePatterns }]}\n                >\n                  <PatternItem />\n                </Form.Item>\n              ))}\n              <Form.Item>\n                <Button type=\"dashed\" block onClick={() => add(['', ''])}>\n                  <PlusOutlined /> {t('common:add')}\n                </Button>\n              </Form.Item>\n            </div>\n          )}\n        </Form.List>\n      </Form>\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "src/options/components/SaladictForm/SaveBtn.tsx",
    "content": "import React, { FC } from 'react'\nimport { Button } from 'antd'\nimport { useObservableState } from 'observable-hooks'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { uploadStatus$ } from '@/options/helpers/upload'\n\n/**\n * Move the button out as independent component to reduce\n * re-rendering of the whole component.\n */\nexport const SaveBtn: FC = () => {\n  const { t } = useTranslate('common')\n  const uploadStatus = useObservableState(uploadStatus$, 'idle')\n\n  return (\n    <Button\n      type=\"primary\"\n      htmlType=\"submit\"\n      disabled={uploadStatus === 'uploading'}\n    >\n      {t('common:save')}\n    </Button>\n  )\n}\n"
  },
  {
    "path": "src/options/components/SaladictForm/_style.scss",
    "content": ".saladict-form-btns {\n  margin-top: 50px;\n\n  button {\n    margin-right: 8px;\n    margin-bottom: 12px;\n  }\n}\n\n.saladict-hide {\n  display: none !important;\n}\n\n.saladict-form-danger-extra .ant-form-item-extra {\n  color: #c0392b;\n}\n"
  },
  {
    "path": "src/options/components/SaladictForm/index.tsx",
    "content": "import React, { ReactNode, useMemo, Ref } from 'react'\nimport { Form, Button, Modal, Tooltip } from 'antd'\nimport { FormItemProps, Rule, FormProps, FormInstance } from 'antd/lib/form'\nimport { ExclamationCircleOutlined, BlockOutlined } from '@ant-design/icons'\nimport { map, distinctUntilChanged } from 'rxjs/operators'\nimport get from 'lodash/get'\nimport mapValues from 'lodash/mapValues'\nimport shallowEqual from 'shallowequal'\nimport { useObservableState } from 'observable-hooks'\nimport { resetConfig } from '@/_helpers/config-manager'\nimport { resetAllProfiles } from '@/_helpers/profile-manager'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { isFirefox } from '@/_helpers/saladict'\nimport { openUrl } from '@/_helpers/browser-api'\nimport { useSelector } from '@/content/redux'\nimport {\n  useFormItemLayout,\n  formItemFooterLayout\n} from '@/options/helpers/layout'\nimport { useUpload } from '@/options/helpers/upload'\nimport { setFormDirty } from '@/options/helpers/use-form-dirty'\nimport { SaveBtn } from './SaveBtn'\n\nimport './_style.scss'\n\ninterface FieldValues {\n  [name: string]: any\n}\n\ninterface FieldShow {\n  [name: string]: boolean\n}\n\nexport interface SaladictFormItem\n  extends Omit<FormItemProps, 'name' | 'children'> {\n  /** Must set name or key. Set name if the item has value. */\n  name?: string\n  /** Must set name or key. Set key if the item does not carry value. */\n  key?: string\n  /** Hide item based on other fields */\n  hide?: (values: FieldValues) => boolean\n  /** Nested items. Must set items or children. */\n  items?: SaladictFormItem[]\n  /** Must set items or children. */\n  children?: ReactNode\n}\n\nexport interface SaladictFormProps\n  extends Omit<FormProps, 'initialValues' | 'onFinish'> {\n  items: SaladictFormItem[]\n  hideFooter?: boolean\n}\n\nexport const SaladictForm = React.forwardRef(\n  (props: SaladictFormProps, ref: Ref<FormInstance>) => {\n    const { items, hideFooter, ...restProps } = props\n    const formItemLayout = useFormItemLayout()\n    const { t, i18n, ready } = useTranslate(['options', 'common'])\n    const data = useSelector(\n      state => ({\n        config: state.config,\n        profile: state.activeProfile\n      }),\n      shallowEqual\n    )\n    const upload = useUpload()\n\n    function extractInitial(\n      items: SaladictFormItem[],\n      result: {\n        initialValues: { [index: string]: any }\n        hideFieldFns: { [index: string]: (values: FieldValues) => boolean }\n      } = { initialValues: {}, hideFieldFns: {} }\n    ): { [index: string]: any } {\n      for (const item of items) {\n        if (item.items) {\n          extractInitial(item.items, result)\n        } else {\n          if (item.hide) {\n            result.hideFieldFns[(item.key || item.name)!] = item.hide\n          }\n\n          if (item.name) {\n            const value = get(data, item.name, data)\n            if (value !== data) {\n              result.initialValues[item.name] = value\n            } else if (process.env.DEBUG) {\n              console.warn(\n                new Error('Missing value for form key: ' + item.name)\n              )\n            }\n          }\n        }\n      }\n      return result\n    }\n\n    const { initialValues, hideFieldFns } = useMemo(\n      () => extractInitial(items),\n      [items]\n    )\n\n    const [hideFields, setHideFields] = useObservableState<\n      FieldShow,\n      FieldValues\n    >(\n      input$ =>\n        input$.pipe(\n          map(values => mapValues(hideFieldFns, hide => hide(values))),\n          distinctUntilChanged(shallowEqual)\n        ),\n      () => mapValues(hideFieldFns, hide => hide(initialValues))\n    )\n\n    function genFormItems(items: SaladictFormItem[]) {\n      return items.map(item => {\n        const name = (item.key || item.name)!\n        const isProfile = name.startsWith('profile.')\n\n        if (\n          item.label === undefined &&\n          ready &&\n          i18n.exists(`options:${name}`)\n        ) {\n          item.label = isProfile ? (\n            <Tooltip\n              title={t('profile.opt.item_extra')}\n              className=\"saladict-form-profile-title\"\n            >\n              <span>\n                <BlockOutlined />\n                {t(name)}\n              </span>\n            </Tooltip>\n          ) : (\n            t(name)\n          )\n        }\n\n        if (item.help === undefined) {\n          const help = `options:${name}_help`\n          if (ready && i18n.exists(help)) {\n            item.help = t(help)\n          }\n        }\n\n        if (item.extra === undefined) {\n          const extra = `options:${name}_extra`\n          if (ready && i18n.exists(extra)) {\n            item.extra = t(extra)\n          }\n        }\n\n        let { className, hide, children, items: subItems, ...itemProps } = item\n        if (hideFields[name]) {\n          className = className ? className + ' saladict-hide' : 'saladict-hide'\n        }\n\n        return (\n          <Form.Item key={name} {...itemProps} className={className}>\n            {subItems ? genFormItems(subItems) : children!}\n          </Form.Item>\n        )\n      })\n    }\n\n    const formItems = useMemo(() => genFormItems(items), [\n      ready,\n      i18n.language,\n      hideFields,\n      items\n    ])\n\n    return (\n      <Form\n        {...formItemLayout}\n        {...restProps}\n        initialValues={initialValues}\n        onFinish={upload}\n        onValuesChange={(_, values) => {\n          setFormDirty(true)\n          setHideFields(values)\n          if (props.onValuesChange) {\n            props.onValuesChange(_, values)\n          }\n        }}\n        ref={ref}\n      >\n        {formItems}\n        {!hideFooter && (\n          <Form.Item {...formItemFooterLayout} className=\"saladict-form-btns\">\n            <SaveBtn />\n            <Button\n              onClick={() => {\n                if (isFirefox) {\n                  Modal.info({ content: t('firefox_shortcuts') })\n                } else {\n                  openUrl('chrome://extensions/shortcuts')\n                }\n              }}\n            >\n              {t('shortcuts')}\n            </Button>\n            <Button\n              type=\"primary\"\n              danger\n              onClick={() => {\n                Modal.confirm({\n                  title: t('config.opt.reset_confirm'),\n                  icon: <ExclamationCircleOutlined />,\n                  okType: 'danger',\n                  onOk: async () => {\n                    await resetConfig()\n                    await resetAllProfiles()\n                    setFormDirty(false)\n                  }\n                })\n              }}\n            >\n              {t('config.opt.reset')}\n            </Button>\n          </Form.Item>\n        )}\n      </Form>\n    )\n  }\n)\n\nexport const NUMBER_RULES: Rule[] = [\n  { type: 'number', whitespace: true, required: true }\n]\n\nexport const percentageSlideFormatter = (v?: number) => `${v || 0}%`\n\nexport const pixelSlideFormatter = (v?: number) => `${v || 0}px`\n"
  },
  {
    "path": "src/options/components/SaladictModalForm.tsx",
    "content": "import React, { FC, useRef, ReactNode } from 'react'\nimport { useUpdateEffect } from 'react-use'\nimport { useObservableState } from 'observable-hooks'\nimport { Modal } from 'antd'\nimport { FormInstance } from 'antd/lib/form'\nimport { ExclamationCircleOutlined } from '@ant-design/icons'\nimport { useTranslate } from '@/_helpers/i18n'\nimport {\n  SaladictForm,\n  SaladictFormItem,\n  SaladictFormProps\n} from '@/options/components/SaladictForm'\nimport { formItemModalLayout } from '@/options/helpers/layout'\nimport { useFormDirty, setFormDirty } from '@/options/helpers/use-form-dirty'\nimport { uploadStatus$ } from '@/options/helpers/upload'\n\nexport interface SaladictModalFormProps\n  extends Omit<SaladictFormProps, 'title'> {\n  visible: boolean\n  title: ReactNode\n  zIndex?: number\n  items: SaladictFormItem[]\n  onClose: () => void\n}\n\nexport const SaladictModalForm: FC<SaladictModalFormProps> = props => {\n  const { visible, title, zIndex, onClose, ...restProps } = props\n  const { t } = useTranslate('options')\n  const uploadStatus = useObservableState(uploadStatus$, 'idle')\n  const formDirtyRef = useFormDirty()\n  const formRef = useRef<FormInstance>(null)\n\n  useUpdateEffect(() => {\n    if (visible && uploadStatus === 'idle') {\n      onClose()\n    }\n  }, [uploadStatus])\n\n  return (\n    <Modal\n      visible={visible}\n      title={title}\n      zIndex={zIndex}\n      width={600}\n      destroyOnClose\n      onOk={() => {\n        if (formRef.current) {\n          formRef.current.submit()\n        }\n      }}\n      onCancel={() => {\n        if (formDirtyRef.value) {\n          Modal.confirm({\n            title: t('unsave_confirm'),\n            icon: <ExclamationCircleOutlined />,\n            okType: 'danger',\n            onOk: () => {\n              setFormDirty(false)\n              onClose()\n            }\n          })\n        } else {\n          onClose()\n        }\n      }}\n    >\n      <SaladictForm\n        {...formItemModalLayout}\n        hideFooter\n        {...restProps}\n        ref={formRef}\n      />\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "src/options/components/SortableList/_style.scss",
    "content": ".sortable-list-item {\n  button,\n  .ant-radio-inner,\n  .ant-radio-inner::after {\n    transition: none !important;\n  }\n\n  .ant-btn-icon-only.ant-btn-sm {\n    > * {\n      font-size: 16px;\n    }\n\n    .anticon svg {\n      display: block;\n    }\n  }\n}\n\n.sortable-list-item-btns {\n  min-width: fit-content;\n}\n"
  },
  {
    "path": "src/options/components/SortableList/index.tsx",
    "content": "import React from 'react'\nimport { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'\nimport { List, Radio, Button, Card } from 'antd'\nimport {\n  PlusOutlined,\n  SwapOutlined,\n  EditOutlined,\n  CloseOutlined\n} from '@ant-design/icons'\nimport { RadioChangeEvent } from 'antd/lib/radio'\nimport { Omit } from '@/typings/helpers'\nimport { useTranslate } from '@/_helpers/i18n'\n\nimport './_style.scss'\n\nexport { reorder } from './reorder'\n\nexport type SortableListItem = { value: string; title: React.ReactNode }\n\nexport interface SortableListItemProps {\n  indexCopy: number\n  selected?: string\n  item: SortableListItem\n  disableEdit?: (index: number, item: SortableListItem) => boolean\n  onEdit?: (index: number, item: SortableListItem) => void\n  onDelete?: (index: number, item: SortableListItem) => void\n}\n\nexport interface SortableListProps\n  extends Omit<SortableListItemProps, 'item' | 'indexCopy'> {\n  /** List title */\n  title: React.ReactNode\n  description?: React.ReactNode\n  list: SortableListItem[]\n  /** List Item can be selected */\n  selected?: string\n  /** show add button */\n  isShowAdd?: boolean\n  onAdd?: () => void\n  /** Title being selected */\n  onSelect?: (e: RadioChangeEvent) => void\n  onOrderChanged?: (oldIndex: number, newIndex: number) => void\n}\n\nexport function SortableList(props: SortableListProps) {\n  const { t } = useTranslate('common')\n\n  return (\n    <Card\n      title={props.title}\n      extra={\n        <Button type=\"dashed\" size=\"small\" onClick={props.onAdd}>\n          <PlusOutlined />\n          {t('add')}\n        </Button>\n      }\n    >\n      {props.description}\n      <Radio.Group\n        className=\"sortable-list-radio-group\"\n        value={props.selected}\n        onChange={props.onSelect}\n      >\n        <DragDropContext\n          onDragEnd={result => {\n            if (\n              props.onOrderChanged &&\n              result.destination &&\n              result.source.index !== result.destination.index\n            ) {\n              props.onOrderChanged(\n                result.source.index,\n                result.destination.index\n              )\n            }\n          }}\n        >\n          <Droppable droppableId=\"droppable\">\n            {({ innerRef: droppableRef, droppableProps, placeholder }) => (\n              <div ref={droppableRef} {...droppableProps}>\n                <List size=\"large\">\n                  {props.list.map((item, index) => (\n                    <Draggable\n                      key={item.value}\n                      draggableId={item.value}\n                      index={index}\n                      disableInteractiveElementBlocking\n                    >\n                      {({\n                        innerRef: draggableRef,\n                        draggableProps,\n                        dragHandleProps\n                      }) => (\n                        <div ref={draggableRef} {...draggableProps}>\n                          <List.Item key={item.value}>\n                            <div className=\"sortable-list-item\">\n                              {props.selected == null ? (\n                                item.title\n                              ) : (\n                                <Radio value={item.value}>{item.title}</Radio>\n                              )}\n                              <div className=\"sortable-list-item-btns\">\n                                <SwapOutlined\n                                  rotate={90}\n                                  title={t('sort')}\n                                  style={{ cursor: 'move' }}\n                                  {...dragHandleProps}\n                                />\n                                <Button\n                                  className=\"sortable-list-item-btn\"\n                                  title={t('edit')}\n                                  shape=\"circle\"\n                                  size=\"small\"\n                                  icon={<EditOutlined />}\n                                  disabled={\n                                    props.disableEdit != null &&\n                                    props.disableEdit(index, item)\n                                  }\n                                  onClick={() =>\n                                    props.onEdit && props.onEdit(index, item)\n                                  }\n                                />\n                                <Button\n                                  title={t('delete')}\n                                  className=\"sortable-list-item-btn\"\n                                  shape=\"circle\"\n                                  size=\"small\"\n                                  icon={<CloseOutlined />}\n                                  disabled={\n                                    props.selected != null &&\n                                    item.value === props.selected\n                                  }\n                                  onClick={() =>\n                                    props.onDelete &&\n                                    props.onDelete(index, item)\n                                  }\n                                />\n                              </div>\n                            </div>\n                          </List.Item>\n                        </div>\n                      )}\n                    </Draggable>\n                  ))}\n                  {placeholder}\n                </List>\n              </div>\n            )}\n          </Droppable>\n        </DragDropContext>\n      </Radio.Group>\n      {(props.isShowAdd == null || props.isShowAdd) && (\n        <Button type=\"dashed\" style={{ width: '100%' }} onClick={props.onAdd}>\n          <PlusOutlined /> {t('add')}\n        </Button>\n      )}\n    </Card>\n  )\n}\n"
  },
  {
    "path": "src/options/components/SortableList/reorder.ts",
    "content": "export function reorder<T extends readonly any[]>(\n  list: T,\n  startIndex: number,\n  endIndex: number\n) {\n  const result = Array.from(list)\n  const [removed] = result.splice(startIndex, 1)\n  result.splice(endIndex, 0, removed)\n  return result\n}\n"
  },
  {
    "path": "src/options/env.ts",
    "content": "export {}\n\nwindow.__SALADICT_INTERNAL_PAGE__ = true\nwindow.__SALADICT_OPTIONS_PAGE__ = true\nwindow.__SALADICT_LAST_SEARCH__ = ''\n"
  },
  {
    "path": "src/options/helpers/change-entry.ts",
    "content": "import React from 'react'\n\nexport const ChangeEntryContext = React.createContext<(entry: string) => void>(\n  null as any\n)\n"
  },
  {
    "path": "src/options/helpers/layout.ts",
    "content": "import { useIsShowDictPanel } from './panel-store'\n\nexport const formItemFooterLayout = {\n  wrapperCol: { offset: 6, span: 18 }\n} as const\n\nexport const formItemModalLayout = {\n  labelCol: { span: 6 },\n  wrapperCol: { span: 18 }\n} as const\n\nconst formItemLayoutWithDictPanel = {\n  labelCol: {\n    xs: { span: 9 },\n    sm: { span: 9 },\n    lg: { span: 5 }\n  },\n  wrapperCol: {\n    xs: { span: 15 },\n    sm: { span: 15 },\n    lg: { span: 8 }\n  }\n} as const\n\nconst formItemLayoutWithoutDictPanel = {\n  labelCol: {\n    xs: { span: 9 },\n    sm: { span: 9 },\n    lg: { span: 7 }\n  },\n  wrapperCol: {\n    xs: { span: 15 },\n    sm: { span: 15 },\n    lg: { span: 9 }\n  }\n} as const\n\nexport const useFormItemLayout = () =>\n  useIsShowDictPanel() && window.innerWidth < 1920\n    ? formItemLayoutWithDictPanel\n    : formItemLayoutWithoutDictPanel\n\nconst listLayoutWithPanel = {\n  xs: { span: 24 },\n  sm: { span: 24 },\n  lg: { span: 12 }\n} as const\n\nconst listLayoutWithoutPanel = {\n  xs: { span: 24 },\n  sm: { span: 24 },\n  lg: { span: 14, offset: 2 }\n} as const\n\nexport const useListLayout = () =>\n  useIsShowDictPanel() && window.innerWidth < 1920\n    ? listLayoutWithPanel\n    : listLayoutWithoutPanel\n"
  },
  {
    "path": "src/options/helpers/panel-store.ts",
    "content": "import { useSelector } from 'react-redux'\nimport { StoreState } from '@/content/redux/modules'\n\nconst pickIsShowDictPanel = (state: StoreState): boolean =>\n  state.isShowDictPanel\n\nexport const useIsShowDictPanel = () => useSelector(pickIsShowDictPanel)\n"
  },
  {
    "path": "src/options/helpers/path-joiner.ts",
    "content": "import { useRefFn } from 'observable-hooks'\nimport { AppConfig } from '@/app-config'\nimport { Profile } from '@/app-config/profiles'\n\nexport function getConfigPath<A extends keyof AppConfig>(pA: A): string\nexport function getConfigPath<\n  A extends keyof AppConfig,\n  B extends keyof AppConfig[A]\n>(pA: A, pB: B): string\nexport function getConfigPath<\n  A extends keyof AppConfig,\n  B extends keyof AppConfig[A],\n  C extends keyof AppConfig[A][B]\n>(pA: A, pB: B, pC: C): string\nexport function getConfigPath<\n  A extends keyof AppConfig,\n  B extends keyof AppConfig[A],\n  C extends keyof AppConfig[A][B],\n  D extends keyof AppConfig[A][B][C]\n>(pA: A, pB: B, pC: C, pD: D): string\nexport function getConfigPath<\n  A extends keyof AppConfig,\n  B extends keyof AppConfig[A],\n  C extends keyof AppConfig[A][B],\n  D extends keyof AppConfig[A][B][C],\n  E extends keyof AppConfig[A][B][C][D]\n>(pA: A, pB: B, pC: C, pD: D, pE: E): string\nexport function getConfigPath<\n  A extends keyof AppConfig,\n  B extends keyof AppConfig[A],\n  C extends keyof AppConfig[A][B],\n  D extends keyof AppConfig[A][B][C],\n  E extends keyof AppConfig[A][B][C][D],\n  F extends keyof AppConfig[A][B][C][D][E]\n>(pA: A, pB: B, pC: C, pD: D, pE: E, pF: F): string\nexport function getConfigPath<\n  A extends keyof AppConfig,\n  B extends keyof AppConfig[A],\n  C extends keyof AppConfig[A][B],\n  D extends keyof AppConfig[A][B][C],\n  E extends keyof AppConfig[A][B][C][D],\n  F extends keyof AppConfig[A][B][C][D][E],\n  G extends keyof AppConfig[A][B][C][D][E][F]\n>(pA: A, pB: B, pC: C, pD: D, pE: E, pF: F, pG: G): string\nexport function getConfigPath<\n  A extends keyof AppConfig,\n  B extends keyof AppConfig[A],\n  C extends keyof AppConfig[A][B],\n  D extends keyof AppConfig[A][B][C],\n  E extends keyof AppConfig[A][B][C][D],\n  F extends keyof AppConfig[A][B][C][D][E],\n  G extends keyof AppConfig[A][B][C][D][E][F],\n  H extends keyof AppConfig[A][B][C][D][E][F][G]\n>(pA: A, pB: B, pC: C, pD: D, pE: E, pF: F, pG: G, pH: H): string\nexport function getConfigPath<\n  A extends keyof AppConfig,\n  B extends keyof AppConfig[A],\n  C extends keyof AppConfig[A][B],\n  D extends keyof AppConfig[A][B][C],\n  E extends keyof AppConfig[A][B][C][D],\n  F extends keyof AppConfig[A][B][C][D][E],\n  G extends keyof AppConfig[A][B][C][D][E][F],\n  H extends keyof AppConfig[A][B][C][D][E][F][G],\n  I extends keyof AppConfig[A][B][C][D][E][F][G][H]\n>(pA: A, pB: B, pC: C, pD: D, pE: E, pF: F, pG: G, pH: H, pI: I): string\nexport function getConfigPath<\n  A extends keyof AppConfig,\n  B extends keyof AppConfig[A],\n  C extends keyof AppConfig[A][B],\n  D extends keyof AppConfig[A][B][C],\n  E extends keyof AppConfig[A][B][C][D],\n  F extends keyof AppConfig[A][B][C][D][E],\n  G extends keyof AppConfig[A][B][C][D][E][F],\n  H extends keyof AppConfig[A][B][C][D][E][F][G],\n  I extends keyof AppConfig[A][B][C][D][E][F][G][H],\n  J extends keyof AppConfig[A][B][C][D][E][F][G][H][I]\n>(pA: A, pB: B, pC: C, pD: D, pE: E, pF: F, pG: G, pH: H, pI: I, pJ: J): string\nexport function getConfigPath<\n  A extends keyof AppConfig,\n  B extends keyof AppConfig[A],\n  C extends keyof AppConfig[A][B],\n  D extends keyof AppConfig[A][B][C],\n  E extends keyof AppConfig[A][B][C][D],\n  F extends keyof AppConfig[A][B][C][D][E],\n  G extends keyof AppConfig[A][B][C][D][E][F],\n  H extends keyof AppConfig[A][B][C][D][E][F][G],\n  I extends keyof AppConfig[A][B][C][D][E][F][G][H],\n  J extends keyof AppConfig[A][B][C][D][E][F][G][H][I],\n  K extends keyof AppConfig[A][B][C][D][E][F][G][H][I][J]\n>(\n  pA: A,\n  pB: B,\n  pC: C,\n  pD: D,\n  pE: E,\n  pF: F,\n  pG: G,\n  pH: H,\n  pI: I,\n  pJ: J,\n  pK: K\n): string\nexport function getConfigPath(...args: string[]): string {\n  return 'config.' + args.join('.')\n}\n\nexport const useConfigPath: typeof getConfigPath = (\n  ...args: string[]\n): string => {\n  return useRefFn(() => 'config.' + args.join('.')).current\n}\n\nexport function getProfilePath<A extends keyof Profile>(pA: A): string\nexport function getProfilePath<\n  A extends keyof Profile,\n  B extends keyof Profile[A]\n>(pA: A, pB: B): string\nexport function getProfilePath<\n  A extends keyof Profile,\n  B extends keyof Profile[A],\n  C extends keyof Profile[A][B]\n>(pA: A, pB: B, pC: C): string\nexport function getProfilePath<\n  A extends keyof Profile,\n  B extends keyof Profile[A],\n  C extends keyof Profile[A][B],\n  D extends keyof Profile[A][B][C]\n>(pA: A, pB: B, pC: C, pD: D): string\nexport function getProfilePath<\n  A extends keyof Profile,\n  B extends keyof Profile[A],\n  C extends keyof Profile[A][B],\n  D extends keyof Profile[A][B][C],\n  E extends keyof Profile[A][B][C][D]\n>(pA: A, pB: B, pC: C, pD: D, pE: E): string\nexport function getProfilePath<\n  A extends keyof Profile,\n  B extends keyof Profile[A],\n  C extends keyof Profile[A][B],\n  D extends keyof Profile[A][B][C],\n  E extends keyof Profile[A][B][C][D],\n  F extends keyof Profile[A][B][C][D][E]\n>(pA: A, pB: B, pC: C, pD: D, pE: E, pF: F): string\nexport function getProfilePath<\n  A extends keyof Profile,\n  B extends keyof Profile[A],\n  C extends keyof Profile[A][B],\n  D extends keyof Profile[A][B][C],\n  E extends keyof Profile[A][B][C][D],\n  F extends keyof Profile[A][B][C][D][E],\n  G extends keyof Profile[A][B][C][D][E][F]\n>(pA: A, pB: B, pC: C, pD: D, pE: E, pF: F, pG: G): string\nexport function getProfilePath<\n  A extends keyof Profile,\n  B extends keyof Profile[A],\n  C extends keyof Profile[A][B],\n  D extends keyof Profile[A][B][C],\n  E extends keyof Profile[A][B][C][D],\n  F extends keyof Profile[A][B][C][D][E],\n  G extends keyof Profile[A][B][C][D][E][F],\n  H extends keyof Profile[A][B][C][D][E][F][G]\n>(pA: A, pB: B, pC: C, pD: D, pE: E, pF: F, pG: G, pH: H): string\nexport function getProfilePath<\n  A extends keyof Profile,\n  B extends keyof Profile[A],\n  C extends keyof Profile[A][B],\n  D extends keyof Profile[A][B][C],\n  E extends keyof Profile[A][B][C][D],\n  F extends keyof Profile[A][B][C][D][E],\n  G extends keyof Profile[A][B][C][D][E][F],\n  H extends keyof Profile[A][B][C][D][E][F][G],\n  I extends keyof Profile[A][B][C][D][E][F][G][H]\n>(pA: A, pB: B, pC: C, pD: D, pE: E, pF: F, pG: G, pH: H, pI: I): string\nexport function getProfilePath<\n  A extends keyof Profile,\n  B extends keyof Profile[A],\n  C extends keyof Profile[A][B],\n  D extends keyof Profile[A][B][C],\n  E extends keyof Profile[A][B][C][D],\n  F extends keyof Profile[A][B][C][D][E],\n  G extends keyof Profile[A][B][C][D][E][F],\n  H extends keyof Profile[A][B][C][D][E][F][G],\n  I extends keyof Profile[A][B][C][D][E][F][G][H],\n  J extends keyof Profile[A][B][C][D][E][F][G][H][I]\n>(pA: A, pB: B, pC: C, pD: D, pE: E, pF: F, pG: G, pH: H, pI: I, pJ: J): string\nexport function getProfilePath<\n  A extends keyof Profile,\n  B extends keyof Profile[A],\n  C extends keyof Profile[A][B],\n  D extends keyof Profile[A][B][C],\n  E extends keyof Profile[A][B][C][D],\n  F extends keyof Profile[A][B][C][D][E],\n  G extends keyof Profile[A][B][C][D][E][F],\n  H extends keyof Profile[A][B][C][D][E][F][G],\n  I extends keyof Profile[A][B][C][D][E][F][G][H],\n  J extends keyof Profile[A][B][C][D][E][F][G][H][I],\n  K extends keyof Profile[A][B][C][D][E][F][G][H][I][J]\n>(\n  pA: A,\n  pB: B,\n  pC: C,\n  pD: D,\n  pE: E,\n  pF: F,\n  pG: G,\n  pH: H,\n  pI: I,\n  pJ: J,\n  pK: K\n): string\nexport function getProfilePath(...args: string[]): string {\n  return 'profile.' + args.join('.')\n}\n\nexport const useProfilePath: typeof getProfilePath = (\n  ...args: string[]\n): string => {\n  return useRefFn(() => 'profile.' + args.join('.')).current\n}\n"
  },
  {
    "path": "src/options/helpers/upload.ts",
    "content": "import { BehaviorSubject } from 'rxjs'\nimport { notification, message as antMsg } from 'antd'\nimport { TFunction } from 'i18next'\nimport set from 'lodash/set'\nimport { AppConfig } from '@/app-config'\nimport { Profile } from '@/app-config/profiles'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { updateConfig } from '@/_helpers/config-manager'\nimport { updateProfile } from '@/_helpers/profile-manager'\nimport { checkBackgroundPermission } from '@/_helpers/permission-manager'\nimport { useDispatch } from '@/content/redux'\nimport { setFormDirty } from './use-form-dirty'\n\nexport const uploadStatus$ = new BehaviorSubject<\n  'idle' | 'uploading' | 'error'\n>('idle')\n\nexport const useUpload = () => {\n  const { t } = useTranslate('options')\n  const dispatch = useDispatch()\n\n  return (values: { [stateObjectPaths: string]: any }) =>\n    dispatch(async (dispatch, getState) => {\n      uploadStatus$.next('uploading')\n\n      const data: { config?: AppConfig; profile?: Profile } = {}\n      const paths = Object.keys(values)\n\n      if (process.env.DEBUG) {\n        if (paths.length <= 0) {\n          console.warn('Saving empty fields.', values)\n        }\n      }\n\n      for (const path of paths) {\n        if (path.startsWith('config.')) {\n          if (!data.config) {\n            data.config = JSON.parse(JSON.stringify(getState().config))\n          }\n          set(data, path, values[path])\n        } else if (path.startsWith('profile.')) {\n          if (!data.profile) {\n            data.profile = JSON.parse(JSON.stringify(getState().activeProfile))\n          }\n          set(data, path, values[path])\n        } else {\n          console.error(new Error(`Saving unknown path: ${path}`))\n        }\n      }\n\n      const requests: Promise<void>[] = []\n\n      if (data.config) {\n        if (!(await checkOptionalPermissions(data.config, t))) {\n          return\n        }\n        requests.push(updateConfig(data.config))\n      }\n\n      if (data.profile) {\n        requests.push(updateProfile(data.profile))\n      }\n\n      try {\n        await Promise.all(requests)\n        setFormDirty(false)\n        antMsg.destroy()\n        antMsg.success(t('msg_updated'))\n        uploadStatus$.next('idle')\n      } catch (e) {\n        notification.error({\n          message: t('config.opt.upload_error'),\n          description: e.message\n        })\n        uploadStatus$.next('error')\n      }\n\n      if (process.env.DEBUG) {\n        console.log('saved setting', data)\n      }\n    })\n}\n\nasync function checkOptionalPermissions(\n  config: AppConfig,\n  t: TFunction\n): Promise<boolean> {\n  try {\n    await checkBackgroundPermission(config)\n  } catch (e) {\n    console.error(e)\n    antMsg.destroy()\n    antMsg.error(t('msg_err_permission', { permission: 'background' }))\n    return false\n  }\n\n  return true\n}\n"
  },
  {
    "path": "src/options/helpers/use-check-dict-auth.ts",
    "content": "import { useContext } from 'react'\nimport { message } from 'antd'\nimport { objectKeys } from '@/typings/helpers'\nimport { updateConfig } from '@/_helpers/config-manager'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { useStore } from '@/content/redux'\nimport { ChangeEntryContext } from './change-entry'\n\nexport const useCheckDictAuth = () => {\n  const { t } = useTranslate('options')\n  const changeEntry = useContext(ChangeEntryContext)\n  const store = useStore()\n\n  return async () => {\n    const { config } = store.getState()\n\n    if (!config.showedDictAuth) {\n      // opens on Profiles\n      await updateConfig({\n        ...config,\n        showedDictAuth: true\n      })\n\n      if (\n        objectKeys(config.dictAuth).every(id =>\n          objectKeys(config.dictAuth[id]).every(k => !config.dictAuth[id]?.[k])\n        )\n      ) {\n        message.warning(t('msg_first_time_notice'), 10)\n        changeEntry('DictAuths')\n        return false\n      }\n    }\n\n    return true\n  }\n}\n"
  },
  {
    "path": "src/options/helpers/use-form-dirty.ts",
    "content": "const formDirty = {\n  value: false\n}\n\nexport const setFormDirty = (value: boolean) => {\n  formDirty.value = value\n}\n\nexport const useFormDirty = (): Readonly<typeof formDirty> => formDirty\n"
  },
  {
    "path": "src/options/index.tsx",
    "content": "import './env'\nimport '@/selection'\n\nimport React from 'react'\n\nimport { initAntdRoot } from '@/components/AntdRoot'\nimport { MainEntry } from './components/MainEntry'\n\nimport './_style.scss'\n\ndocument.title = 'Saladict Options'\n\ninitAntdRoot(() => <MainEntry />)\n"
  },
  {
    "path": "src/popup/Notebook.tsx",
    "content": "import React, { FC, useEffect } from 'react'\nimport { Word } from '@/_helpers/record-manager'\nimport { TFunction } from 'i18next'\nimport { useTranslate } from '@/_helpers/i18n'\n\ninterface NotebookProps {\n  word?: Word\n  hasError: boolean\n}\n\nconst wrapperStyle: React.CSSProperties = {\n  textAlign: 'center',\n  margin: '10px 20px',\n  fontSize: 14\n}\n\nexport const Notebook: FC<NotebookProps> = ({ word, hasError }) => {\n  useEffect(() => {\n    setTimeout(() => {\n      window.close()\n    }, 3000)\n  }, [])\n\n  const { t } = useTranslate('popup')\n\n  if (hasError) {\n    return renderError(t)\n  }\n  if (word && word.text) {\n    return renderSuccess(t, word)\n  } else {\n    return renderEmpty(t)\n  }\n}\n\nfunction renderError(t: TFunction) {\n  return (\n    <div style={wrapperStyle}>\n      <p>\n        <svg\n          width=\"100\"\n          height=\"100\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          viewBox=\"0 0 426.667 426.667\"\n        >\n          <path\n            fill=\"#f05228\"\n            d=\"M213.333 0C95.514 0 0 95.514 0 213.333s95.514 213.333 213.333 213.333 213.333-95.514 213.333-213.333S331.153 0 213.333 0zm117.662 276.689l-54.302 54.306-63.36-63.356-63.36 63.36-54.302-54.31 63.356-63.356-63.356-63.36 54.302-54.302 63.36 63.356 63.36-63.356 54.302 54.302-63.356 63.36 63.356 63.356z\"\n          />\n        </svg>\n      </p>\n      <p>{t('notebook_error')}</p>\n    </div>\n  )\n}\n\nfunction renderEmpty(t: TFunction) {\n  return (\n    <div style={wrapperStyle}>\n      <p>\n        <svg\n          width=\"100\"\n          height=\"100\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          viewBox=\"0 0 426.667 426.667\"\n        >\n          <g fill=\"#fac917\">\n            <path d=\"M213.338 0C95.509 0 0 95.497 0 213.325c0 117.854 95.509 213.342 213.338 213.342 117.82 0 213.329-95.488 213.329-213.342C426.667 95.497 331.157 0 213.338 0zm-.005 99.49c14.793 0 26.786 11.994 26.786 26.786s-11.998 26.782-26.786 26.782-26.786-11.994-26.786-26.782c0-14.792 11.994-26.786 26.786-26.786zm46.874 227.691H166.46v-40.183h20.087V206.64H166.46v-40.18h73.664v120.537h20.087v40.183h-.004z\" />\n            <path d=\"M325.935 394.449l93.615 25.08-25.084-93.611z\" />\n          </g>\n        </svg>\n      </p>\n      <p>{t('notebook_empty')}</p>\n    </div>\n  )\n}\n\nfunction renderSuccess(t: TFunction, word: Word) {\n  return (\n    <div style={wrapperStyle}>\n      <p>\n        <svg\n          width=\"100\"\n          height=\"100\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          viewBox=\"0 0 426.667 426.667\"\n        >\n          <path\n            fill=\"#6ac259\"\n            d=\"M213.333 0C95.518 0 0 95.514 0 213.333s95.518 213.333 213.333 213.333c117.828 0 213.333-95.514 213.333-213.333S331.157 0 213.333 0zm-39.134 322.918l-93.935-93.931 31.309-31.309 62.626 62.622 140.894-140.898 31.309 31.309-172.203 172.207z\"\n          />\n        </svg>\n      </p>\n      <p>\n        {t('notebook_added')} 「{word.text}」\n      </p>\n    </div>\n  )\n}\n\nexport default Notebook\n"
  },
  {
    "path": "src/popup/Popup.tsx",
    "content": "import React, {\n  FC,\n  useState,\n  useEffect,\n  useCallback,\n  useLayoutEffect\n} from 'react'\nimport classNames from 'classnames'\nimport QRCode from 'qrcode.react'\nimport CSSTransition from 'react-transition-group/CSSTransition'\nimport { AppConfig } from '@/app-config'\nimport { updateConfig, addConfigListener } from '@/_helpers/config-manager'\nimport { message } from '@/_helpers/browser-api'\nimport { useTranslate } from '@/_helpers/i18n'\nimport { DictPanelStandaloneContainer } from '@/content/components/DictPanel/DictPanelStandalone.container'\n\ninterface PopupProps {\n  config: AppConfig\n}\n\nexport const Popup: FC<PopupProps> = props => {\n  const { t } = useTranslate('popup')\n\n  const [config, setConfig] = useState(props.config)\n\n  /** URL box with QR code */\n  const [isShowUrlBox, setIsShowUrlBox] = useState(false)\n  const [currentTabUrl, setCurrentTabUrl] = useState('')\n\n  const [dictPanelHeight, setDictPanelHeight] = useState(30)\n  const expandDictPanel = useCallback(\n    () => setDictPanelHeight(config.baHeight - 51),\n    [config.baHeight]\n  )\n  const shrinkDictPanel = useCallback(\n    () => setDictPanelHeight(config.baHeight - 151),\n    [config.baHeight]\n  )\n\n  /** Instant Capture Mode */\n  const [insCapMode, setInsCapMode] = useState<'mode' | 'pinMode'>('mode')\n\n  const [isTempOff, setTempOff] = useState(false)\n\n  const [isShowPageNoResponse, setShowPageNoResponse] = useState(false)\n\n  useLayoutEffect(() => {\n    document.body.style.width =\n      (config.baWidth >= 0 ? config.baWidth : config.panelWidth) + 'px'\n  }, [config])\n\n  useEffect(() => {\n    expandDictPanel()\n\n    addConfigListener(({ newConfig }) => {\n      setConfig(newConfig)\n    })\n\n    browser.tabs\n      .query({ active: true, currentWindow: true })\n      .then(tabs => {\n        if (tabs.length > 0 && tabs[0].id != null) {\n          message\n            .send<'TEMP_DISABLED_STATE'>(tabs[0].id, {\n              type: 'TEMP_DISABLED_STATE',\n              payload: { op: 'get' }\n            })\n            .then(flag => {\n              setTempOff(flag)\n            })\n\n          message\n            .send<'QUERY_PIN_STATE', boolean>(tabs[0].id, {\n              type: 'QUERY_PIN_STATE'\n            })\n            .then(isPinned => {\n              setInsCapMode(isPinned ? 'pinMode' : 'mode')\n            })\n        }\n      })\n      .catch(err =>\n        console.warn('Error when receiving MsgTempDisabled response', err)\n      )\n  }, [])\n\n  return (\n    <div\n      className={classNames('popup-root', { 'dark-mode': config.darkMode })}\n      style={{ height: config.baHeight }}\n    >\n      <DictPanelStandaloneContainer\n        width=\"100vw\"\n        height={dictPanelHeight + 'px'}\n      />\n      <div\n        className=\"switch-container\"\n        onMouseEnter={shrinkDictPanel}\n        onMouseLeave={expandDictPanel}\n      >\n        <div className=\"active-switch\">\n          <span className=\"switch-title\">{t('app_temp_active_title')}</span>\n          <input\n            type=\"checkbox\"\n            id=\"opt-temp-active\"\n            className=\"btn-switch\"\n            checked={isTempOff}\n            onChange={toggleTempOff}\n            onFocus={shrinkDictPanel}\n          />\n          <label htmlFor=\"opt-temp-active\"></label>\n        </div>\n        <div className=\"active-switch\">\n          <span className=\"switch-title\">\n            {t('instant_capture_title') +\n              (insCapMode === 'pinMode' ? t('instant_capture_pinned') : '')}\n          </span>\n          <input\n            type=\"checkbox\"\n            id=\"opt-instant-capture\"\n            className=\"btn-switch\"\n            checked={config[insCapMode].instant.enable}\n            onChange={toggleInsCap}\n            onFocus={shrinkDictPanel}\n          />\n          <label htmlFor=\"opt-instant-capture\"></label>\n        </div>\n        <div className=\"active-switch\">\n          <svg\n            className=\"icon-qrcode\"\n            onMouseEnter={showQRcode}\n            xmlns=\"http://www.w3.org/2000/svg\"\n            viewBox=\"0 0 612 612\"\n          >\n            <path d=\"M0 225v25h250v-25H0zM0 25h250V0H0v25z\" />\n            <path d=\"M0 250h25V0H0v250zm225 0h25V0h-25v250zM87.5 162.5h75v-75h-75v75zM362 587v25h80v-25h-80zm0-200h80v-25h-80v25z\" />\n            <path d=\"M362 612h25V362h-25v250zm190-250v25h60v-25h-60zm-77.5 87.5v25h50v-25h-50z\" />\n            <path d=\"M432 497.958v-25h-70v25h70zM474.5 387h50v-25h-50v25zM362 225v25h250v-25H362zm0-200h250V0H362v25z\" />\n            <path d=\"M362 250h25V0h-25v250zm225 0h25V0h-25v250zm-137.5-87.5h75v-75h-75v75zM0 587v25h250v-25H0zm0-200h250v-25H0v25z\" />\n            <path d=\"M0 612h25V362H0v250zm225 0h25V362h-25v250zM87.5 524.5h75v-75h-75v75zM587 612h25V441h-25v171zM474.5 499.5v25h50v-25h-50z\" />\n            <path d=\"M474.5 449.5v75h25v-75h-25zM562 587v25h50v-25h-50z\" />\n          </svg>\n          <span className=\"switch-title\">{t('app_active_title')}</span>\n          <input\n            type=\"checkbox\"\n            id=\"opt-active\"\n            className=\"btn-switch\"\n            checked={config.active}\n            onChange={toggleAppActive}\n            onFocus={shrinkDictPanel}\n          />\n          <label htmlFor=\"opt-active\"></label>\n        </div>\n        <CSSTransition\n          classNames=\"fade\"\n          in={!!currentTabUrl}\n          timeout={500}\n          exit={false}\n          mountOnEnter\n          unmountOnExit\n        >\n          {() => (\n            <div\n              className=\"qrcode-panel\"\n              onMouseLeave={() => setCurrentTabUrl('')}\n            >\n              <QRCode\n                value={currentTabUrl}\n                size={250}\n                bgColor={config.darkMode ? '#ddd' : '#fff'}\n                fgColor=\"#222\"\n              />\n              <p className=\"qrcode-panel-title\">\n                {isShowUrlBox ? (\n                  <input\n                    type=\"text\"\n                    autoFocus\n                    readOnly\n                    value={currentTabUrl}\n                    onFocus={e => e.currentTarget.select()}\n                  />\n                ) : (\n                  <span>{t('qrcode_title')}</span>\n                )}\n              </p>\n              {isShowPageNoResponse && (\n                <div className=\"page-no-response-panel\">\n                  <p className=\"page-no-response-title\">\n                    {t('page_no_response')}\n                  </p>\n                </div>\n              )}\n            </div>\n          )}\n        </CSSTransition>\n      </div>\n    </div>\n  )\n\n  function toggleTempOff() {\n    const newTempOff = !isTempOff\n\n    setTempOff(newTempOff)\n\n    browser.tabs\n      .query({ active: true, currentWindow: true })\n      .then(tabs => {\n        if (tabs.length > 0 && tabs[0].id != null) {\n          return message.send<'TEMP_DISABLED_STATE'>(tabs[0].id, {\n            type: 'TEMP_DISABLED_STATE',\n            payload: {\n              op: 'set',\n              value: newTempOff\n            }\n          })\n        }\n        return false\n      })\n      .then(isSuccess => {\n        if (!isSuccess) {\n          setTempOff(!newTempOff)\n          throw new Error('Set tempOff failed')\n        }\n      })\n      .catch(() => setShowPageNoResponse(true))\n  }\n\n  function toggleInsCap() {\n    updateConfig({\n      ...config,\n      [insCapMode]: {\n        ...config[insCapMode],\n        instant: {\n          ...config[insCapMode].instant,\n          enable: !config[insCapMode].instant.enable\n        }\n      }\n    })\n  }\n\n  function toggleAppActive() {\n    updateConfig({\n      ...config,\n      active: !config.active\n    })\n  }\n\n  async function showQRcode() {\n    const tabs = await browser.tabs.query({ active: true, currentWindow: true })\n    if (tabs.length > 0) {\n      const url = tabs[0].url\n      if (url) {\n        if (!url.startsWith('http')) {\n          const match = /pdf\\/web\\/viewer\\.html\\?file=(.*)$/.exec(url)\n          if (match) {\n            setIsShowUrlBox(true)\n            setCurrentTabUrl(decodeURIComponent(match[1]))\n            return\n          }\n        }\n        setIsShowUrlBox(false)\n        setCurrentTabUrl(url)\n      }\n    }\n  }\n}\n\nexport default Popup\n"
  },
  {
    "path": "src/popup/__fake__/_style.scss",
    "content": "body {\n  background: #ddd;\n}\n"
  },
  {
    "path": "src/popup/__fake__/env.ts",
    "content": "import '../index'\nimport './_style.scss'\nimport { initConfig, updateConfig } from '@/_helpers/config-manager'\n\nasync function main() {\n  const config = await initConfig()\n  await updateConfig({\n    ...config,\n    darkMode: true\n  })\n}\n\nmain()\n"
  },
  {
    "path": "src/popup/_style.scss",
    "content": "@import '@/_sass_shared/_theme.scss';\n\n/*------------------------------------*\\\n   Base\n\\*------------------------------------*/\nhtml {\n  margin: 0;\n  padding: 0;\n  overflow: hidden;\n}\n\nbody {\n  position: relative;\n  margin: 0;\n  padding: 0;\n  overflow: hidden;\n  font-family: 'Helvetica Neue', Helvetica, Arial, 'Hiragino Sans GB',\n    'Hiragino Sans GB W3', 'Microsoft YaHei UI', 'Microsoft YaHei', sans-serif;\n}\n\n/*------------------------------------*\\\n   Components\n\\*------------------------------------*/\n\n@import '@/content/components/DictPanel/DictPanelStandalone.scss';\n\n.popup-root {\n  overflow: hidden;\n  display: flex;\n  flex-direction: column-reverse;\n  width: 100vw;\n  height: 550px;\n  font-size: 14px;\n}\n\n.qrcode-panel {\n  position: fixed;\n  z-index: $global-zindex-dictpanel;\n  bottom: 10px;\n  left: 10px;\n  padding: 20px;\n  background: #fff;\n  border-radius: 10px;\n  box-shadow: rgba(0, 0, 0, 0.8) 0px 4px 23px -6px;\n}\n\n.qrcode-panel-title {\n  text-align: center;\n  margin: 5px 0 0 0;\n\n  input {\n    width: 100%;\n  }\n}\n\n.page-no-response-panel {\n  position: fixed;\n  z-index: 100;\n  bottom: 60px;\n  right: 25px;\n  padding: 0 10px;\n  background: #fff;\n  border-radius: 10px;\n  box-shadow: rgba(0, 0, 0, 0.8) 0px 4px 23px -6px;\n}\n\n.switch-container {\n  overflow: hidden;\n  background: #f9f9f9;\n}\n\n.active-switch {\n  display: flex;\n  align-items: center;\n  position: relative;\n  height: 49px;\n  border-bottom: 1px solid #d8d8d8;\n  padding: 0 20px;\n  user-select: none;\n\n  &:last-child {\n    border-bottom-color: transparent;\n  }\n}\n\n.icon-qrcode {\n  width: 20px;\n  margin-top: 3px;\n}\n\n.switch-title {\n  flex: 1;\n  font-size: 1.2em;\n  padding: 0 15px;\n  text-align: left;\n  color: #333;\n}\n\n$switch-button-width: 55px;\n$switch-button-height: 35px;\n.btn-switch {\n  // hide input\n  position: absolute;\n  z-index: -200000;\n  opacity: 0;\n\n  & + label {\n    display: inline-block;\n    width: $switch-button-width;\n    height: $switch-button-height;\n    position: relative;\n    margin: auto;\n    background-color: #ddd;\n    border-radius: $switch-button-height;\n    cursor: pointer;\n    outline: 0;\n    user-select: none;\n  }\n\n  & + label:before {\n    content: '';\n    display: block;\n    position: absolute;\n    top: 1px;\n    left: 1px;\n    bottom: 1px;\n    right: 1px;\n    background-color: #f1f1f1;\n    border-radius: $switch-button-height;\n    transition: background 0.4s;\n  }\n\n  & + label:after {\n    content: '';\n    display: block;\n    position: absolute;\n    height: $switch-button-height - 2px;\n    width: $switch-button-height - 2px;\n    background-color: #fff;\n    border-radius: 100%;\n    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);\n    transition: margin 0.4s;\n  }\n\n  &:checked + label:before {\n    background-color: #8ce196;\n  }\n\n  &:checked + label:after {\n    margin-left: $switch-button-width - $switch-button-height + 2px;\n  }\n\n  &:active + label,\n  &:focus + label {\n    outline: 5px auto rgb(59, 153, 252);\n    outline: 5px auto -webkit-focus-ring-color;\n  }\n\n  & + label:hover {\n    outline: none !important;\n  }\n}\n\n.fade-enter {\n  opacity: 0;\n  transition: opacity 0.5s;\n}\n\n.fade-enter-active {\n  opacity: 1;\n  transition: opacity 0.5s;\n}\n\n.dark-mode {\n  background: #222;\n\n  .switch-container {\n    background: #414141;\n  }\n\n  .active-switch {\n    border-bottom-color: #666;\n  }\n\n  .switch-title {\n    color: #ddd;\n  }\n\n  .icon-qrcode {\n    fill: #ddd;\n  }\n\n  .btn-switch + label:before {\n    background-color: #666;\n  }\n\n  .btn-switch:checked + label:before {\n    background-color: #8ce196;\n  }\n\n  .btn-switch + label:after {\n    background-color: #ddd;\n  }\n\n  .qrcode-panel {\n    background-color: #ddd;\n  }\n}\n"
  },
  {
    "path": "src/popup/env.ts",
    "content": "export {}\n\nwindow.__SALADICT_INTERNAL_PAGE__ = true\nwindow.__SALADICT_POPUP_PAGE__ = true\n"
  },
  {
    "path": "src/popup/index.tsx",
    "content": "import './env'\nimport '@/selection'\n\nimport React, { FC } from 'react'\nimport ReactDOM from 'react-dom'\nimport { Helmet } from 'react-helmet'\nimport { AppConfig } from '@/app-config'\nimport { getConfig } from '@/_helpers/config-manager'\nimport { message, openUrl } from '@/_helpers/browser-api'\nimport { saveWord, Word } from '@/_helpers/record-manager'\nimport { translateCtxs, genCtxText } from '@/_helpers/translateCtx'\nimport { Message } from '@/typings/message'\n\nimport { Provider as ProviderRedux } from 'react-redux'\nimport { createStore } from '@/content/redux'\n\nimport { I18nContextProvider, useTranslate } from '@/_helpers/i18n'\n\nimport Popup from './Popup'\nimport Notebook from './Notebook'\nimport './_style.scss'\n\n// This is a workaround for browser action page\n// which does not fire beforeunload event\nbrowser.runtime.connect({ name: 'popup' } as any) // wrong typing\n\nconst Title: FC = () => {\n  const { t } = useTranslate('popup')\n  return (\n    <Helmet>\n      <title>{t('title')}</title>\n    </Helmet>\n  )\n}\n\ngetConfig().then(config => {\n  document.body.style.width =\n    config.baOpen === 'popup_panel'\n      ? (config.baWidth >= 0 ? config.baWidth : config.panelWidth) + 'px'\n      : '450px'\n\n  switch (config.baOpen) {\n    case 'popup_panel':\n      showPanel(config)\n      break\n    case 'popup_fav':\n      addNotebook()\n      break\n    case 'popup_options':\n      openOptions()\n      break\n    case 'popup_standalone':\n      message.send({ type: 'OPEN_QS_PANEL' })\n      break\n    default:\n      sendContextMenusClick(config.baOpen).then(() => {\n        window.close()\n      })\n      break\n  }\n})\n\nasync function showPanel(config: AppConfig) {\n  if (config.analytics) {\n  }\n\n  const store = await createStore()\n\n  ReactDOM.render(\n    <I18nContextProvider>\n      <Title />\n      <ProviderRedux store={store}>\n        <Popup config={config} />\n      </ProviderRedux>\n    </I18nContextProvider>,\n    document.getElementById('root')\n  )\n}\n\nasync function addNotebook() {\n  let hasError = false\n  let word: Word | undefined\n\n  const tabs = await browser.tabs.query({ active: true, currentWindow: true })\n  const tab = tabs[0]\n  if (tab && tab.id) {\n    try {\n      word = await message.send<'PRELOAD_SELECTION'>(tab.id, {\n        type: 'PRELOAD_SELECTION'\n      })\n    } catch (err) {\n      hasError = true\n    }\n\n    if (word && word.text) {\n      try {\n        await saveWord('notebook', word)\n      } catch (err) {\n        hasError = true\n      }\n    }\n  } else {\n    hasError = true\n  }\n\n  ReactDOM.render(\n    <I18nContextProvider>\n      <Notebook word={word} hasError={hasError} />\n    </I18nContextProvider>,\n    document.getElementById('root')\n  )\n\n  // async get translations\n  if (word && word.context) {\n    const config = await getConfig()\n    word.trans = genCtxText(\n      word.trans,\n      await translateCtxs(word.context || word.title, config.ctxTrans)\n    )\n    try {\n      await saveWord('notebook', word)\n    } catch (err) {\n      /* */\n    }\n  }\n}\n\nfunction openOptions() {\n  openUrl('options.html', true)\n}\n\nasync function sendContextMenusClick(menuItemId: string) {\n  const payload: Message<'CONTEXT_MENUS_CLICK'>['payload'] = {\n    menuItemId\n  }\n\n  const tabs = await browser.tabs\n    .query({ active: true, currentWindow: true })\n    .catch((): browser.tabs.Tab[] => [])\n\n  const tab = tabs[0]\n\n  if (tab && tab.url) {\n    payload.linkUrl = tab.url\n    if (tab.id) {\n      try {\n        const word = await message.send<'PRELOAD_SELECTION'>(tab.id, {\n          type: 'PRELOAD_SELECTION'\n        })\n        if (word && word.text) {\n          payload.selectionText = word.text\n        }\n      } catch (e) {\n        console.error(e)\n      }\n    }\n  }\n\n  await message.send({\n    type: 'CONTEXT_MENUS_CLICK',\n    payload\n  })\n}\n"
  },
  {
    "path": "src/quick-search/env.ts",
    "content": "export {}\n\nwindow.__SALADICT_INTERNAL_PAGE__ = true\nwindow.__SALADICT_QUICK_SEARCH_PAGE__ = true\n"
  },
  {
    "path": "src/quick-search/index.tsx",
    "content": "import './env'\nimport '@/selection'\n\nimport React, { FC } from 'react'\nimport ReactDOM from 'react-dom'\nimport { Helmet } from 'react-helmet'\nimport { message, storage } from '@/_helpers/browser-api'\n\nimport { Provider as ProviderRedux } from 'react-redux'\nimport { createStore } from '@/content/redux'\n\nimport { I18nContextProvider, useTranslate } from '@/_helpers/i18n'\n\nimport { DictPanelStandaloneContainer } from '@/content/components/DictPanel/DictPanelStandalone.container'\n\nimport './quick-search.scss'\n\ndocument.title = 'Saladict Standalone Panel'\n\nconst Title: FC = () => {\n  const { t } = useTranslate('content')\n  return (\n    <Helmet>\n      <title>{t('standalone')}</title>\n    </Helmet>\n  )\n}\n\ncreateStore().then(store => {\n  ReactDOM.render(\n    <I18nContextProvider>\n      <Title />\n      <ProviderRedux store={store}>\n        <DictPanelStandaloneContainer width=\"100vw\" height=\"100vh\" />\n      </ProviderRedux>\n    </I18nContextProvider>,\n    document.getElementById('root')\n  )\n\n  // Firefox cannot fire 'unload' event.\n  window.addEventListener('beforeunload', () => {\n    message.send({ type: 'CLOSE_QS_PANEL' })\n\n    if (!store.getState().config.qssaSidebar) {\n      storage.local.set({\n        qssaRect: {\n          top: window.screenY,\n          left: window.screenX,\n          width: window.outerWidth,\n          height: window.outerHeight\n        }\n      })\n    }\n  })\n})\n"
  },
  {
    "path": "src/quick-search/quick-search.scss",
    "content": "@import '@/content/components/DictPanel/DictPanelStandalone.scss';\n\nhtml,\nbody,\n#root {\n  position: static;\n  height: 100%;\n  margin: 0;\n  padding: 0;\n  // hide white spaces\n  font-size: 0;\n}\n\n#root {\n  overflow: hidden;\n}\n\n.popup-root {\n  overflow: hidden;\n  display: flex;\n  flex-direction: column-reverse;\n  width: 450px;\n  height: 550px;\n  font-size: 14px;\n}\n"
  },
  {
    "path": "src/selection/helper.ts",
    "content": "import { AppConfig } from '@/app-config'\nimport { Observable, fromEvent, merge, of } from 'rxjs'\nimport { map, mapTo, filter, distinctUntilChanged } from 'rxjs/operators'\nimport { newWord, Word } from '@/_helpers/record-manager'\nimport { message } from '@/_helpers/browser-api'\nimport { isTagName } from '@/_helpers/dom'\n\nconst isMac = /mac/i.test(navigator.platform)\n\n/**\n * Is quick search key pressed(command on mac, ctrl on others)\n */\nexport function isQSKey(evt: KeyboardEvent): boolean {\n  return isMac ? evt.key === 'Meta' : evt.key === 'Control'\n}\n\n/**\n * Is esc button pressed\n */\nexport function isEscapeKey(evt: KeyboardEvent): boolean {\n  return evt.key === 'Escape'\n}\n\nexport function whenKeyPressed(\n  keySelectior: (e: KeyboardEvent) => boolean\n): Observable<true> {\n  return merge(\n    map(keySelectior)(\n      fromEvent<KeyboardEvent>(window, 'keydown', { capture: true })\n    ),\n    mapTo(false)(fromEvent(window, 'keyup', { capture: true })),\n    mapTo(false)(fromEvent(window, 'blur', { capture: true })),\n    of(false)\n  ).pipe(\n    distinctUntilChanged(), // ignore long press\n    filter((x): x is true => x)\n  )\n}\n\n// common editors\nconst editorTester = /CodeMirror|ace_editor|monaco-editor/\n\nexport function isTypeField(element: Node | EventTarget | null): boolean {\n  if (!element || !element['tagName']) {\n    return false\n  }\n\n  for (\n    let el: HTMLElement | null = element as HTMLElement;\n    el;\n    el = el.parentElement\n  ) {\n    if (\n      isTagName(el, 'input') ||\n      isTagName(el, 'textarea') ||\n      el.isContentEditable\n    ) {\n      return true\n    }\n\n    // With CodeMirror the `pre.CodeMirror-line` somehow got detached when the event\n    // triggerd. So el will never reach the root `.CodeMirror`.\n    if (editorTester.test(String(el.className))) {\n      return true\n    }\n  }\n\n  return false\n}\n\nexport function isBlacklisted(config: AppConfig): boolean {\n  const url = window.pageURL || document.URL || ''\n  if (!url) {\n    return false\n  }\n  return (\n    config.blacklist.some(([r]) => new RegExp(r).test(url)) &&\n    config.whitelist.every(([r]) => !new RegExp(r).test(url))\n  )\n}\n\nexport async function newSelectionWord(\n  word: Partial<Word> = {}\n): Promise<Word> {\n  const info = await message.send<'PAGE_INFO'>({ type: 'PAGE_INFO' })\n  window.faviconURL = info.faviconURL\n  if (info.pageTitle) {\n    window.pageTitle = info.pageTitle\n  }\n  if (info.pageURL) {\n    window.pageURL = info.pageURL\n  }\n  return newWord({\n    title: info.pageTitle || document.title || '',\n    url: info.pageURL || document.URL || '',\n    favicon: info.faviconURL || '',\n    ...word\n  })\n}\n"
  },
  {
    "path": "src/selection/index.ts",
    "content": "import {\n  getText,\n  getSentence,\n  getTextFromSelection,\n  getSentenceFromSelection\n} from 'get-selection-more'\nimport { message } from '@/_helpers/browser-api'\nimport { createConfigStream } from '@/_helpers/config-manager'\nimport { isInDictPanel } from '@/_helpers/saladict'\n\nimport { share, map, switchMap } from 'rxjs/operators'\n\nimport { postMessageHandler, sendMessage, sendEmptyMessage } from './message'\nimport {\n  isEscapeKey,\n  whenKeyPressed,\n  isBlacklisted,\n  newSelectionWord\n} from './helper'\nimport { createIntantCaptureStream } from './instant-capture'\nimport { createQuickSearchStream } from './quick-search'\nimport { createSelectTextStream } from './select-text'\n\n// Firefox somehow loads it two times\nif (!window.__SALADICT_SELECTION_LOADED__) {\n  window.__SALADICT_SELECTION_LOADED__ = true\n\n  const config$$ = createConfigStream().pipe(\n    map(config => (isBlacklisted(config) ? null : config)),\n    share()\n  )\n\n  /**\n   * Send selection to standalone page\n   * Beware that this is run on every frame.\n   */\n  message.addListener('PRELOAD_SELECTION', () => {\n    const text = getText()\n    if (text) {\n      return newSelectionWord({\n        text,\n        context: getSentence()\n      })\n    }\n  })\n\n  /**\n   * Manually emit selection\n   * Beware that this is run on every frame.\n   */\n  message.createStream('EMIT_SELECTION').subscribe(async () => {\n    const selection = window.getSelection()\n    if (selection && selection.rangeCount > 0) {\n      const text = getTextFromSelection(selection)\n      const rect = selection.getRangeAt(0).getBoundingClientRect()\n      if (text) {\n        sendMessage({\n          mouseX: rect.right,\n          mouseY: rect.top,\n          instant: true,\n          self: isInDictPanel(selection.anchorNode),\n          word: await newSelectionWord({\n            text,\n            context: getSentenceFromSelection(selection)\n          }),\n          dbClick: false,\n          altKey: false,\n          shiftKey: false,\n          ctrlKey: false,\n          metaKey: false,\n          force: false\n        })\n      }\n    }\n  })\n\n  /** Pass through message from iframes */\n  window.addEventListener('message', postMessageHandler)\n\n  /**\n   * Escape key pressed\n   */\n  whenKeyPressed(isEscapeKey).subscribe(() =>\n    message.self.send({ type: 'ESCAPE_KEY' })\n  )\n\n  config$$.pipe(switchMap(createQuickSearchStream)).subscribe(() => {\n    message.self.send({ type: 'TRIPLE_CTRL' })\n  })\n\n  config$$.pipe(switchMap(createSelectTextStream)).subscribe(async result => {\n    if (result.word) {\n      sendMessage({\n        dbClick: false,\n        altKey: false,\n        shiftKey: false,\n        ctrlKey: false,\n        metaKey: false,\n        self: false,\n        instant: false,\n        force: false,\n        ...result,\n        word: await newSelectionWord(result.word)\n      })\n    } else {\n      sendEmptyMessage(result.self)\n    }\n  })\n\n  config$$\n    .pipe(switchMap(createIntantCaptureStream))\n    .subscribe(async ({ word, event, self }) => {\n      sendMessage({\n        word: await newSelectionWord(word),\n        altKey: event.altKey,\n        shiftKey: event.shiftKey,\n        ctrlKey: event.ctrlKey,\n        metaKey: event.metaKey,\n        dbClick: false,\n        force: false,\n        instant: true,\n        mouseX: event.clientX,\n        mouseY: event.clientY,\n        self\n      })\n    })\n}\n"
  },
  {
    "path": "src/selection/instant-capture.ts",
    "content": "import {\n  getTextFromSelection,\n  getSentenceFromSelection\n} from 'get-selection-more'\nimport { AppConfig } from '@/app-config'\nimport { isStandalonePage, isInDictPanel } from '@/_helpers/saladict'\nimport { checkSupportedLangs } from '@/_helpers/lang-check'\nimport { message } from '@/_helpers/browser-api'\n\nimport { fromEvent, merge, of, timer, combineLatest, EMPTY, from } from 'rxjs'\nimport {\n  map,\n  mapTo,\n  filter,\n  switchMap,\n  distinctUntilChanged,\n  debounce,\n  pluck,\n  startWith\n} from 'rxjs/operators'\n\n/**\n * Create an instant capture Observable\n */\nexport function createIntantCaptureStream(config: AppConfig | null) {\n  if (!config) return EMPTY\n\n  const isPinned$ = message.self\n    .createStream('PIN_STATE')\n    .pipe(pluck('payload'), startWith(false))\n\n  const responseToQSPanel$ = merge(\n    // When Quick Search Panel show and hide\n    from(\n      message.send<'QUERY_QS_PANEL'>({ type: 'QUERY_QS_PANEL' })\n    ),\n    message.createStream('QS_PANEL_CHANGED').pipe(pluck('payload'))\n  ).pipe(\n    map(withQssaPanel => withQssaPanel && config.qssaPageSel),\n    startWith(false)\n  )\n\n  return combineLatest(isPinned$, responseToQSPanel$).pipe(\n    switchMap(([isPinned, responseToQSPanel]) => {\n      const { instant: panelInstant } = config.panelMode\n      const { instant: otherInstant } = config[\n        responseToQSPanel ? 'qsPanelMode' : isPinned ? 'pinMode' : 'mode'\n      ]\n\n      if (!panelInstant.enable && !otherInstant.enable) {\n        return of(null)\n      }\n\n      // Reduce GC\n      // Only the latest result is used so it's safe to reuse the object\n      const reuseObj = ({} as unknown) as {\n        event: MouseEvent\n        self: boolean\n      }\n\n      return merge(\n        mapTo(null)(fromEvent(window, 'mouseup', { capture: true })),\n        mapTo(null)(fromEvent(window, 'mouseout', { capture: true })),\n        mapTo(null)(fromEvent(window, 'keyup', { capture: true })),\n        fromEvent<MouseEvent>(window, 'mousemove', { capture: true }).pipe(\n          map(event => {\n            const self = isInDictPanel(event.target)\n            const instant =\n              self || isStandalonePage() ? panelInstant : otherInstant\n            if (instant.enable) {\n              if (\n                (instant.key === 'alt' && event.altKey) ||\n                (instant.key === 'shift' && event.shiftKey) ||\n                (instant.key === 'ctrl' && (event.ctrlKey || event.metaKey)) ||\n                (instant.key === 'direct' &&\n                  !(event.ctrlKey || event.metaKey || event.altKey))\n              ) {\n                reuseObj.event = event\n                reuseObj.self = self\n                return reuseObj\n              }\n            }\n            return null\n          })\n        )\n      ).pipe(\n        // distinctUntilChanged((oldObj, newObj) =>\n        //   Boolean(\n        //     oldObj &&\n        //       newObj &&\n        //       Math.abs(oldObj.event.clientX - newObj.event.clientX) <= 1 &&\n        //       Math.abs(oldObj.event.clientY - newObj.event.clientY) <= 1\n        //   )\n        // ),\n        debounce(obj =>\n          obj ? timer(obj.self ? panelInstant.delay : otherInstant.delay) : of()\n        )\n      )\n    }),\n    map(obj => obj && { word: getCursorWord(obj.event), ...obj }),\n    distinctUntilChanged((oldObj, newObj) => {\n      if (!oldObj || !newObj) return false\n      const { word: oldWord, event: oldEvent } = oldObj\n      const { word: newWord, event: newEvent } = newObj\n      return (\n        oldWord?.text === newWord?.text &&\n        oldWord?.context === newWord?.context &&\n        oldEvent.shiftKey === newEvent.shiftKey &&\n        oldEvent.ctrlKey === newEvent.ctrlKey &&\n        oldEvent.metaKey === newEvent.metaKey &&\n        oldEvent.altKey === newEvent.altKey\n      )\n    }),\n    filter((obj): obj is {\n      word: { text: string; context: string }\n      event: MouseEvent\n      config: AppConfig\n      self: boolean\n    } =>\n      Boolean(\n        obj && obj.word && checkSupportedLangs(config.language, obj.word.text)\n      )\n    )\n  )\n}\n\nfunction getCursorWord(\n  event: MouseEvent\n): { text: string; context: string } | null {\n  const x = event.clientX\n  const y = event.clientY\n\n  let offsetNode: Node\n  let offset: number\n  let originRange: Range | undefined\n\n  const sel = window.getSelection()\n  if (!sel) return null\n  if (sel.rangeCount > 0) {\n    originRange = sel.getRangeAt(0)\n    sel.removeAllRanges()\n  }\n\n  if (document.caretPositionFromPoint) {\n    const pos = document.caretPositionFromPoint(x, y)\n    if (!pos) return null\n    offsetNode = pos.offsetNode\n    offset = pos.offset\n  } else if (document.caretRangeFromPoint) {\n    const pos = document.caretRangeFromPoint(x, y)\n    if (!pos) return null\n    offsetNode = pos.startContainer\n    offset = pos.startOffset\n  } else {\n    return null\n  }\n\n  if (offsetNode.nodeType === Node.TEXT_NODE) {\n    const textNode = offsetNode as Text\n    const content = textNode.data\n    const head = (content.slice(0, offset).match(/[-_a-z]+$/i) || [''])[0]\n    const tail = (content\n      .slice(offset)\n      .match(/^([-_a-z]+|[\\u4e00-\\u9fa5])/i) || [''])[0]\n    if (head.length <= 0 && tail.length <= 0) {\n      return null\n    }\n\n    const range = document.createRange()\n    range.setStart(textNode, offset - head.length)\n    range.setEnd(textNode, offset + tail.length)\n    const rangeRect = range.getBoundingClientRect()\n\n    // When cursor is pointing at the blank space of\n    // the last line of a paragraph,\n    // caretPositionFromPoint would select the nearest\n    // ending text.\n    // This will make sure the text is truly under cursor.\n    if (\n      rangeRect.left <= x &&\n      rangeRect.right >= x &&\n      rangeRect.top <= y &&\n      rangeRect.bottom >= y\n    ) {\n      sel.removeAllRanges()\n      sel.addRange(range)\n      // select the whole word(CJK)\n      if (sel['modify']) {\n        sel['modify']('move', 'backward', 'word')\n        sel.collapseToStart()\n        sel['modify']('extend', 'forward', 'word')\n      }\n    }\n\n    const text = getTextFromSelection(sel)\n    const context = getSentenceFromSelection(sel)\n\n    sel.removeAllRanges()\n    if (originRange) {\n      sel.addRange(originRange)\n    }\n    range.detach()\n\n    return text ? { text, context } : null\n  }\n\n  return null\n}\n"
  },
  {
    "path": "src/selection/message.ts",
    "content": "import { Message } from '@/typings/message'\nimport { message } from '@/_helpers/browser-api'\n\ninterface PostMessageEvent extends MessageEvent {\n  data: {\n    type: 'SALADICT_SELECTION'\n    payload: Message<'SELECTION'>['payload']\n  }\n}\n\nexport function postMessageHandler({ data, source }: PostMessageEvent) {\n  if (!data || data.type !== 'SALADICT_SELECTION') {\n    return\n  }\n\n  // get the souce iframe\n  const matchSrc = ({ contentWindow }: HTMLIFrameElement | HTMLFrameElement) =>\n    contentWindow === source\n\n  const frame =\n    Array.from(document.querySelectorAll('iframe')).find(matchSrc) ||\n    Array.from(document.querySelectorAll('frame')).find(matchSrc)\n\n  if (!frame) {\n    return\n  }\n\n  const { left, top } = frame.getBoundingClientRect()\n  data.payload.mouseX = data.payload.mouseX + left\n  data.payload.mouseY = data.payload.mouseY + top\n  sendMessage(data.payload)\n}\n\n/**\n * Send to upper frame for calculating offset.\n * Finally send to dict panel.\n */\nexport function sendMessage(payload: Message<'SELECTION'>['payload']) {\n  if (window.parent === window) {\n    // top\n    if (process.env.DEBUG) {\n      console.log('New selection', payload)\n    }\n\n    message.self.send({\n      type: 'SELECTION',\n      payload\n    })\n  } else {\n    // post to upper frames/window\n    window.parent.postMessage(\n      {\n        type: 'SALADICT_SELECTION',\n        payload\n      },\n      '*'\n    )\n  }\n}\n\n/**\n * Send a\n */\nexport function sendEmptyMessage(isDictPanel: boolean) {\n  // empty message\n  const msg: Message<'SELECTION'> = {\n    type: 'SELECTION',\n    payload: {\n      word: null,\n      self: isDictPanel,\n      mouseX: 0,\n      mouseY: 0,\n      dbClick: false,\n      altKey: false,\n      shiftKey: false,\n      ctrlKey: false,\n      metaKey: false,\n      instant: false,\n      force: false\n    }\n  }\n\n  if (process.env.DEBUG) {\n    console.log('New selection', msg.payload)\n  }\n\n  return message.self.send(msg)\n}\n"
  },
  {
    "path": "src/selection/quick-search.ts",
    "content": "import { EMPTY, merge } from 'rxjs'\nimport { share, buffer, debounceTime, filter } from 'rxjs/operators'\nimport { AppConfig } from '@/app-config'\nimport { isStandalonePage, isOptionsPage } from '@/_helpers/saladict'\nimport { whenKeyPressed, isQSKey } from './helper'\n\n/**\n * Listen to triple-ctrl shortcut which opens quick search panel.\n * Pressing ctrl/command key more than three times within 500ms\n * trigers triple-ctrl.\n */\nexport function createQuickSearchStream(config: AppConfig | null) {\n  if (!config || !config.tripleCtrl || isStandalonePage() || isOptionsPage()) {\n    return EMPTY\n  }\n\n  const qsKeyPressed$$ = share<true>()(whenKeyPressed(isQSKey))\n\n  return qsKeyPressed$$.pipe(\n    buffer(\n      merge(\n        debounceTime(500)(qsKeyPressed$$), // collect after 0.5s\n        whenKeyPressed(e => !isQSKey(e)) // other key pressed\n      )\n    ),\n    filter(group => group.length >= 3)\n  )\n}\n"
  },
  {
    "path": "src/selection/select-text.ts",
    "content": "import { EMPTY, fromEvent, merge, timer, Observable } from 'rxjs'\nimport {\n  withLatestFrom,\n  filter,\n  map,\n  mapTo,\n  debounce,\n  switchMap,\n  scan,\n  startWith,\n  throttle,\n  delay,\n  distinctUntilChanged\n} from 'rxjs/operators'\nimport {\n  useObservable,\n  useObservableCallback,\n  identity,\n  useSubscription\n} from 'observable-hooks'\nimport { AppConfig } from '@/app-config'\nimport {\n  isInDictPanel,\n  isInSaladictExternal,\n  isFirefox\n} from '@/_helpers/saladict'\nimport {\n  getTextFromSelection,\n  getSentenceFromSelection\n} from 'get-selection-more'\nimport { checkSupportedLangs } from '@/_helpers/lang-check'\nimport { Message } from '@/typings/message'\nimport { isTypeField, newSelectionWord } from './helper'\nimport { isTagName } from '@/_helpers/dom'\n\nexport function createSelectTextStream(config: AppConfig | null) {\n  if (!config) {\n    return EMPTY\n  }\n\n  return config.touchMode ? withTouchMode(config) : withoutTouchMode(config)\n}\n\nfunction withTouchMode(config: AppConfig) {\n  const mousedown$ = merge(\n    fromEvent<MouseEvent>(window, 'mousedown', { capture: true }).pipe(\n      filter(e => e.button === 0)\n    ),\n    fromEvent<TouchEvent>(window, 'touchstart', { capture: true }).pipe(\n      map(e => e.changedTouches[0])\n    )\n  )\n\n  const mouseup$ = merge(\n    fromEvent<MouseEvent>(window, 'mouseup', { capture: true }).pipe(\n      filter(e => e.button === 0)\n    ),\n    fromEvent<TouchEvent>(window, 'touchend', { capture: true }).pipe(\n      map(e => e.changedTouches[0])\n    )\n  )\n\n  const clickPeriodCount$ = clickPeriodCountStream(\n    mouseup$,\n    config.doubleClickDelay\n  )\n\n  const isMouseDown$ = merge(\n    mapTo(true)(mousedown$),\n    mapTo(false)(mouseup$),\n    mapTo(false)(fromEvent(window, 'mouseout', { capture: true })),\n    mapTo(false)(fromEvent(window, 'blur', { capture: true }))\n  )\n\n  return fromEvent(document, 'selectionchange').pipe(\n    withLatestFrom(isMouseDown$),\n    debounce(([, isWithMouse]) => (isWithMouse ? mouseup$ : timer(400))),\n    map(([, isWithMouse]) => [window.getSelection(), isWithMouse] as const),\n    filter(\n      (args): args is [Selection, boolean] =>\n        !!args[0] && !isInSaladictExternal(args[0].anchorNode)\n    ),\n    withLatestFrom(mouseup$, mousedown$, clickPeriodCount$),\n    map(([[selection, isWithMouse], mouseup, mousedown, clickPeriodCount]) => {\n      const self = isInDictPanel(selection.anchorNode || mousedown.target)\n\n      if (\n        config.noTypeField &&\n        isTypeField(isWithMouse ? mousedown.target : selection.anchorNode)\n      ) {\n        return { self }\n      }\n\n      const text = getTextFromSelection(selection)\n\n      if (!checkSupportedLangs(config.language, text)) {\n        return { self }\n      }\n\n      if (isWithMouse) {\n        return {\n          word: {\n            text,\n            context: getSentenceFromSelection(selection)\n          },\n          self,\n          dbClick: clickPeriodCount >= 2,\n          mouseX: mouseup.clientX,\n          mouseY: mouseup.clientY,\n          altKey: !!mouseup['altKey'],\n          shiftKey: !!mouseup['shiftKey'],\n          ctrlKey: !!mouseup['ctrlKey'],\n          metaKey: !!mouseup['metaKey']\n        }\n      }\n\n      if (selection.rangeCount <= 0) {\n        return { self }\n      }\n\n      const rect = selection.getRangeAt(0).getBoundingClientRect()\n\n      if (\n        rect.top === 0 &&\n        rect.left === 0 &&\n        rect.width === 0 &&\n        rect.height === 0\n      ) {\n        // Selection is made inside textarea with keyboard. Ignore.\n        return { self }\n      }\n\n      return {\n        word: {\n          text,\n          context: getSentenceFromSelection(selection)\n        },\n        self,\n        dbClick: clickPeriodCount >= 2,\n        mouseX: rect.right,\n        mouseY: rect.top\n      }\n    }),\n    throttle(result => {\n      // Firefox will fire an extra selectionchange event\n      // when selection is made inside dict panel and\n      // continue search is triggered.\n      // Need to skip this event otherwise the panel is\n      // closed unexpectedly.\n      if (isFirefox && result.self && result.word && result.word.text) {\n        const { direct, double, holding } = config.panelMode\n        if (\n          direct ||\n          (double && result.dbClick) ||\n          (holding.alt && result.altKey) ||\n          (holding.shift && result.shiftKey) ||\n          (holding.ctrl && result.ctrlKey) ||\n          (holding.meta && result.metaKey)\n        ) {\n          return timer(500)\n        }\n      }\n      return timer(0)\n    })\n  )\n}\n\nfunction withoutTouchMode(config: AppConfig) {\n  const mousedown$ = fromEvent<MouseEvent>(window, 'mousedown', {\n    capture: true\n  }).pipe(filter(e => e.button === 0))\n\n  const mouseup$ = fromEvent<MouseEvent>(window, 'mouseup', {\n    capture: true\n  }).pipe(filter(e => e.button === 0))\n\n  const clickPeriodCount$ = clickPeriodCountStream(\n    mouseup$,\n    config.doubleClickDelay\n  )\n\n  return mouseup$.pipe(\n    filter(e => !isInSaladictExternal(e.target)),\n    // if user click on a selected text,\n    // getSelection would reture the text before the highlight disappears\n    // delay to wait for selection get cleared\n    delay(10),\n    withLatestFrom(mousedown$, clickPeriodCount$),\n    // handle in-panel search separately\n    // due to tricky shadow dom event retarget\n    filter(\n      ([mouseup, mousedown]) =>\n        !isInDictPanel(mouseup.target) && !isInDictPanel(mousedown.target)\n    ),\n    map(([mouseup, mousedown, clickPeriodCount]) => {\n      if (config.noTypeField && isTypeField(mousedown.target)) {\n        return { self: false }\n      }\n\n      const selection = window.getSelection()\n      const text = getTextFromSelection(selection)\n\n      if (!checkSupportedLangs(config.language, text)) {\n        return { self: false }\n      }\n\n      return {\n        word: {\n          text,\n          context: getSentenceFromSelection(selection)\n        },\n        self: false,\n        dbClick: clickPeriodCount >= 2,\n        mouseX: mouseup.clientX,\n        mouseY: mouseup.clientY,\n        altKey: mouseup.altKey,\n        shiftKey: mouseup.shiftKey,\n        ctrlKey: mouseup.ctrlKey,\n        metaKey: mouseup.metaKey\n      }\n    }),\n    distinctUntilChanged((oldVal, newVal) => {\n      // (Ignore this rule if it is a double click.)\n      // Same selection. This could be caused by other widget on the page\n      // that uses preventDefault which stops selection being cleared when clicked.\n      // Ignore it so that the panel won't follow.\n      return (\n        !newVal.dbClick &&\n        !!oldVal.word &&\n        !!newVal.word &&\n        oldVal.word.text === newVal.word.text &&\n        oldVal.word.context === newVal.word.context\n      )\n    })\n  )\n}\n\nexport function useInPanelSelect(\n  touchMode: AppConfig['touchMode'],\n  language: AppConfig['language'],\n  doubleClickDelay: AppConfig['doubleClickDelay'],\n  newSelection: (payload: Message<'SELECTION'>['payload']) => void\n) {\n  const [onMouseUp, mouseUp$] = useObservableCallback<React.MouseEvent>(\n    event$ => event$.pipe(filter(e => e.button === 0))\n  )\n\n  const config$ = useObservable(identity, [touchMode, language] as const)\n\n  const clickPeriodCount$ = useObservable(\n    inputs$ =>\n      inputs$.pipe(\n        switchMap(([doubleClickDelay]) =>\n          clickPeriodCountStream(mouseUp$, doubleClickDelay)\n        )\n      ),\n    [doubleClickDelay] as const\n  )\n\n  const output$ = useObservable(() =>\n    mouseUp$.pipe(\n      withLatestFrom(config$),\n      filter(([mouseup, [touchMode]]) => {\n        if (touchMode) {\n          return false\n        }\n\n        for (\n          let el = mouseup.target as HTMLElement | null;\n          el;\n          el = el.parentElement\n        ) {\n          if (isTagName(el, 'a') || isTagName(el, 'button')) {\n            return false\n          }\n        }\n\n        return true\n      }),\n      map(([mouseup, [, language]]) => ({\n        mouseup: mouseup.nativeEvent,\n        language\n      })),\n      delay(10),\n      withLatestFrom(clickPeriodCount$),\n      map(([{ mouseup, language }, clickPeriodCount]) => {\n        const selection = window.getSelection()\n        const text = getTextFromSelection(selection)\n\n        return checkSupportedLangs(language, text)\n          ? {\n              word: {\n                text,\n                context: getSentenceFromSelection(selection)\n              },\n              dbClick: clickPeriodCount >= 2,\n              mouseX: mouseup.clientX,\n              mouseY: mouseup.clientY,\n              altKey: mouseup.altKey,\n              shiftKey: mouseup.shiftKey,\n              ctrlKey: mouseup.ctrlKey,\n              metaKey: mouseup.metaKey,\n              self: true,\n              instant: false,\n              force: false\n            }\n          : {\n              word: null,\n              self: true,\n              mouseX: 0,\n              mouseY: 0,\n              dbClick: false,\n              altKey: false,\n              shiftKey: false,\n              ctrlKey: false,\n              metaKey: false,\n              instant: false,\n              force: false\n            }\n      }),\n      distinctUntilChanged((oldVal, newVal) => {\n        return (\n          !newVal.dbClick &&\n          !!oldVal.word &&\n          !!newVal.word &&\n          oldVal.word.text === newVal.word.text &&\n          oldVal.word.context === newVal.word.context\n        )\n      })\n    )\n  )\n\n  useSubscription(output$, async result => {\n    if (result.word) {\n      result.word = await newSelectionWord(result.word)\n    }\n    newSelection(result as Message<'SELECTION'>['payload'])\n  })\n\n  return onMouseUp\n}\n\nfunction clickPeriodCountStream(\n  mouseup$: Observable<MouseEvent | Touch | React.MouseEvent>,\n  doubleClickDelay: number\n) {\n  return mouseup$.pipe(\n    switchMap(() =>\n      timer(doubleClickDelay).pipe(mapTo(false), startWith(true))\n    ),\n    scan((sum: number, flag: boolean) => (flag ? sum + 1 : 0), 0)\n  )\n}\n"
  },
  {
    "path": "src/typings/css.d.ts",
    "content": "// eslint-disable-next-line\nimport * as CSS from 'csstype'\n\ndeclare module 'csstype' {\n  interface Properties {\n    '--panel-width'?: string\n    '--panel-max-height'?: string\n    '--panel-font-size'?: string\n    '--color-brand'?: string\n    '--color-font'?: string\n    '--color-background'?: string\n    '--color-rgb-background'?: string\n    '--color-divider'?: string\n  }\n}\n"
  },
  {
    "path": "src/typings/global.d.ts",
    "content": "interface Window {\n  browser: typeof browser\n\n  __SALADICT_PANEL_LOADED__?: boolean\n  __SALADICT_SELECTION_LOADED__?: boolean\n\n  // For self page messaging\n  pageId?: number | string\n  faviconURL?: string\n  pageTitle?: string\n  pageURL?: string\n\n  __SALADICT_BACKGROUND_PAGE__?: boolean\n  __SALADICT_INTERNAL_PAGE__?: boolean\n  __SALADICT_OPTIONS_PAGE__?: boolean\n  __SALADICT_POPUP_PAGE__?: boolean\n  __SALADICT_QUICK_SEARCH_PAGE__?: boolean\n  __SALADICT_PDF_PAGE__?: boolean\n\n  // Options page\n  __SALADICT_LAST_SEARCH__?: string\n\n  // eslint-disable-next-line\n  __webpack_public_path__?: string\n}\n"
  },
  {
    "path": "src/typings/helpers.ts",
    "content": "interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}\n\ntype DeepReadonlyObject<T> = {\n  readonly [P in keyof T]: DeepReadonly<T[P]>\n}\n\nexport type DeepReadonly<T> = T extends (infer R)[]\n  ? DeepReadonlyArray<R>\n  : T extends Function\n  ? T\n  : T extends object\n  ? DeepReadonlyObject<T>\n  : T\n\nexport type Diff<T, K> = Exclude<T, K>\n\nexport type Omit<T, K extends keyof T> = Pick<T, Diff<keyof T, K>>\n\nexport type Mutable<T> = { -readonly [P in keyof T]: T[P] }\n\nexport type UnionKeys<T> = T extends any ? keyof T : never\nexport type UnionPick<T, K extends UnionKeys<T>> = T extends any\n  ? Pick<T, Extract<K, keyof T>>\n  : never\n\nexport type Subunion<T, U extends T> = U\n\nexport const objectKeys = Object.keys as <T>(o: T) => Extract<keyof T, string>[]\n"
  },
  {
    "path": "src/typings/message.ts",
    "content": "import { Word, DBArea } from '@/_helpers/record-manager'\nimport { DictID } from '@/app-config'\nimport { DictSearchResult } from '@/components/dictionaries/helpers'\nimport { OpenUrlOptions } from '@/_helpers/browser-api'\n\ntype MessageConfigType<\n  T extends {\n    [type in string]: { [key in 'payload' | 'response']?: any }\n  }\n> = T\n\nexport type MessageConfig = MessageConfigType<{\n  /* ------------------------------------------------ *\\\n     Backend - From other pages to background script\n  \\* ------------------------------------------------ */\n\n  /** Open url in new tab or update existing tab */\n  OPEN_URL: {\n    payload: OpenUrlOptions\n  }\n\n  /** Open the source page of a dictionary */\n  OPEN_DICT_SRC_PAGE: {\n    payload: {\n      id: DictID\n      text: string\n      /** Focus on the new page? */\n      active?: boolean\n    }\n  }\n\n  /** Get clipboard content */\n  GET_CLIPBOARD: {\n    response: string\n  }\n\n  SET_CLIPBOARD: {\n    payload: string\n  }\n\n  /** Request backend for page info */\n  PAGE_INFO: {\n    response: {\n      pageId: string | number\n      faviconURL?: string\n      pageTitle?: string\n      pageURL?: string\n    }\n  }\n\n  /** Request backend to fetch suggest */\n  GET_SUGGESTS: {\n    /** Search text */\n    payload: string\n    /** Response with suggest items */\n    response: Array<{\n      explain: string\n      entry: string\n    }>\n  }\n\n  FETCH_DICT_RESULT: {\n    payload: {\n      id: DictID\n      text: string\n      /** engine search function payload */\n      payload: {\n        isPDF: boolean\n        [index: string]: any\n      }\n    }\n    response: {\n      id: DictID\n      result: any\n      catalog?: DictSearchResult<DictID>['catalog']\n      audio?: DictSearchResult<DictID>['audio']\n    }\n  }\n\n  /** call any method exported from the engine */\n  DICT_ENGINE_METHOD: {\n    payload: {\n      id: DictID\n      method: string\n      args?: any[]\n    }\n    response: any\n  }\n\n  /** Inject dict panel to any page */\n  INJECT_DICTPANEL: {}\n\n  /* ------------------------------------------------ *\\\n     Backend IndexedDB: Notebook or History\n  \\* ------------------------------------------------ */\n\n  /** Is a word in Notebook */\n  IS_IN_NOTEBOOK: {\n    payload: Word\n    response: boolean\n  }\n\n  /** Save a word to Notebook or History */\n  SAVE_WORD: {\n    payload: {\n      area: DBArea\n      word: Word\n    }\n  }\n\n  WORD_SAVED: {}\n\n  DELETE_WORDS: {\n    payload: {\n      area: DBArea\n      dates?: number[]\n    }\n  }\n\n  GET_WORDS_BY_TEXT: {\n    payload: {\n      area: DBArea\n      text: string\n    }\n    response: Word[]\n  }\n\n  GET_WORDS: {\n    payload: {\n      area: DBArea\n      itemsPerPage?: number\n      pageNum?: number\n      filters?: { [field: string]: (string | number)[] | null | undefined }\n      sortField?: string | number | (string | number)[]\n      sortOrder?: 'ascend' | 'descend' | false | null\n      searchText?: string\n    }\n    response: {\n      total: number\n      words: Word[]\n    }\n  }\n\n  /* ------------------------------------------------ *\\\n     Audio Playing\n  \\* ------------------------------------------------ */\n\n  PLAY_AUDIO: {\n    /** url: to backend */\n    payload: string\n  }\n\n  STOP_AUDIO: {}\n\n  LAST_PLAY_AUDIO: {\n    response?: null | { src: string; timestamp: number }\n  }\n\n  /* ------------------------------------------------ *\\\n     Text Selection\n  \\* ------------------------------------------------ */\n\n  /** To dict panel */\n  SELECTION: {\n    payload: {\n      word: Word | null\n      mouseX: number\n      mouseY: number\n      dbClick: boolean\n      altKey: boolean\n      shiftKey: boolean\n      ctrlKey: boolean\n      metaKey: boolean\n      /** inside panel? */\n      self: boolean\n      /** skip salad bowl and show panel directly */\n      instant: boolean\n      /** force panel to skip reconciling position */\n      force: boolean\n    }\n  }\n\n  /** From backend to active panel */\n  ADD_NOTEBOOK: {\n    payload: {\n      /** to browser action page */\n      popup: boolean\n    }\n    /** is received */\n    response?: boolean\n  }\n\n  /** send to the current active tab for selection */\n  PRELOAD_SELECTION: {\n    response: Word\n  }\n\n  /** Manually emit selection */\n  EMIT_SELECTION: {}\n\n  ESCAPE_KEY: {}\n\n  /** Ctrl/Command has been hit 3 times */\n  TRIPLE_CTRL: {}\n\n  /* ------------------------------------------------ *\\\n     Dict Panel\n  \\* ------------------------------------------------ */\n\n  /** From dict panel when it is pinned or unpinned */\n  PIN_STATE: {\n    payload: boolean\n  }\n\n  /** switch to the next or previous history */\n  SWITCH_HISTORY: {\n    payload: 'prev' | 'next'\n    /** received? */\n    response: boolean\n  }\n\n  /** From other pages or frames query for active panel pin state */\n  QUERY_PIN_STATE: {\n    response: boolean\n  }\n\n  /** request searching */\n  SEARCH_TEXT: {\n    payload: Word\n  }\n\n  /** request searching text box text from other pages */\n  SEARCH_TEXT_BOX: {\n    /** is popup received */\n    response?: boolean\n  }\n\n  /** request closing panel */\n  CLOSE_PANEL: {}\n\n  TEMP_DISABLED_STATE: {\n    payload:\n      | {\n          op: 'get'\n        }\n      | {\n          op: 'set'\n          value: boolean\n        }\n    response: boolean\n  }\n\n  /** Info for brwoser action badge. From background to content. */\n  GET_TAB_BADGE_INFO: {\n    response: {\n      active: boolean\n      tempDisable: boolean\n      unsupported: boolean\n    }\n  }\n\n  /** Info for brwoser action badge. From content to background. */\n  SEND_TAB_BADGE_INFO: {\n    payload: {\n      active: boolean\n      tempDisable: boolean\n      unsupported: boolean\n    }\n  }\n\n  /* ------------------------------------------------ *\\\n    Quick Search Dict Panel\n  \\* ------------------------------------------------ */\n\n  /** Send new words to standalone panel */\n  QS_PANEL_SEARCH_TEXT: {\n    payload: Word\n  }\n\n  /** Open or update Quick Search Panel */\n  OPEN_QS_PANEL: {}\n\n  CLOSE_QS_PANEL: {}\n\n  /** query backend for standalone panel appearance */\n  QUERY_QS_PANEL: {\n    response: boolean\n  }\n\n  /** Fired from backend when standalone panel show or hide */\n  QS_PANEL_CHANGED: {\n    payload: boolean\n  }\n\n  /** Focus standalone quick search panel */\n  QS_PANEL_FOCUSED: {}\n\n  /** Switch to Sidebar */\n  QS_SWITCH_SIDEBAR: {\n    payload: 'left' | 'right'\n  }\n\n  /* ------------------------------------------------ *\\\n     Word Editor\n  \\* ------------------------------------------------ */\n\n  UPDATE_WORD_EDITOR_WORD: {\n    payload: {\n      word: Word\n      translateCtx?: boolean\n    }\n  }\n\n  /* ------------------------------------------------ *\\\n     Context Menus\n  \\* ------------------------------------------------ */\n\n  /** Manually trigger context menus click */\n  CONTEXT_MENUS_CLICK: {\n    payload: {\n      menuItemId: string\n      selectionText?: string\n      linkUrl?: string\n    }\n  }\n\n  /* ------------------------------------------------ *\\\n     Sync Services\n  \\* ------------------------------------------------ */\n\n  ANKI_CONNECT_FIND_WORD: {\n    /** Word Date */\n    payload: number\n    /** Card ID */\n    response: number | undefined\n  }\n\n  ANKI_CONNECT_UPDATE_WORD: {\n    payload: {\n      cardId: number\n      word: Word\n    }\n  }\n\n  /* ------------------------------------------------ *\\\n    GA\n  \\* ------------------------------------------------ */\n\n  /** Send new words to standalone panel */\n  REQUEST_GA: {\n    payload: { [key: string]: string }\n  }\n\n  /* ------------------------------------------------ *\\\n     Third-party Scripts\n  \\* ------------------------------------------------ */\n\n  YOUDAO_TRANSLATE_AJAX: {\n    payload: any\n    response: any\n  }\n}>\n\nexport type MsgType = keyof MessageConfig\n\n// 'extends' hack to generate union\n// https://www.typescriptlang.org/docs/handbook/advanced-types.html#distributive-conditional-types\nexport type Message<T extends MsgType = MsgType> = T extends any\n  ? Readonly<\n      {\n        type: T\n      } & ('payload' extends keyof MessageConfig[T]\n        ? Pick<MessageConfig[T], Extract<'payload', keyof MessageConfig[T]>>\n        : { payload?: null })\n    >\n  : never\n\nexport type MessageResponse<T extends MsgType> = Readonly<\n  'response' extends keyof MessageConfig[T]\n    ? MessageConfig[T][Extract<'response', keyof MessageConfig[T]>]\n    : void\n>\n"
  },
  {
    "path": "src/word-editor/env.ts",
    "content": "export {}\n\nwindow.__SALADICT_INTERNAL_PAGE__ = true\n"
  },
  {
    "path": "src/word-editor/index.tsx",
    "content": "import './env'\n\nimport React from 'react'\nimport ReactDOM from 'react-dom'\n\nimport { Provider as ProviderRedux } from 'react-redux'\nimport { createStore } from '@/content/redux'\n\nimport { I18nContextProvider } from '@/_helpers/i18n'\n\nimport { WordEditorStandaloneContainer } from '@/content/components/WordEditor/WordEditorStandalone.container'\n\nimport './word-editor.scss'\n\ndocument.title = 'Saladict Word Editor'\n\ncreateStore().then(store => {\n  const searchParams = new URL(document.URL).searchParams\n\n  const wordString = searchParams.get('word')\n  if (wordString) {\n    try {\n      const word = JSON.parse(decodeURIComponent(wordString))\n      if (word) {\n        store.dispatch({\n          type: 'WORD_EDITOR_STATUS',\n          payload: { word, translateCtx: true }\n        })\n      }\n    } catch (e) {\n      console.warn(e)\n    }\n  }\n\n  ReactDOM.render(\n    <I18nContextProvider>\n      <ProviderRedux store={store}>\n        <WordEditorStandaloneContainer />\n      </ProviderRedux>\n    </I18nContextProvider>,\n    document.getElementById('root')\n  )\n})\n"
  },
  {
    "path": "src/word-editor/word-editor.scss",
    "content": "@import '@/content/components/WordEditor/WordEditor.scss';\n\nhtml,\nbody,\n#root {\n  position: static;\n  height: 100%;\n  margin: 0;\n  padding: 0;\n  // hide white spaces\n  font-size: 0;\n}\n\n#root {\n  overflow: hidden;\n}\n\n.wordEditorPanel-Container {\n  width: 100% !important;\n}\n\n.wordEditorPanel {\n  width: 100% !important;\n  height: 100vh !important;\n  max-width: unset;\n  max-height: unset;\n  border-radius: 0;\n  box-shadow: none;\n}\n"
  },
  {
    "path": "test/helper.ts",
    "content": "import * as SinonChrome from 'sinon-chrome'\n\nexport const browser = (window.browser as unknown) as typeof SinonChrome\n"
  },
  {
    "path": "test/specs/_helpers/browser-api.spec.ts",
    "content": "import { message, storage, openUrl } from '@/_helpers/browser-api'\nimport { take } from 'rxjs/operators'\nimport sinon from 'sinon'\nimport { browser } from '../../helper'\nimport { Message } from '@/typings/message'\n\ndescribe('Browser API Wapper', () => {\n  beforeEach(() => {\n    browser.flush()\n    delete window.pageId\n    delete window.faviconURL\n    delete window.pageTitle\n    delete window.pageURL\n    browser.runtime.sendMessage.callsFake(() => Promise.resolve({}))\n    browser.tabs.sendMessage.callsFake(() => Promise.resolve({}))\n  })\n\n  describe('Storage', () => {\n    const storageArea: ['sync', 'local'] = ['sync', 'local']\n    storageArea.forEach(area => {\n      it(`storage.${area}.clear`, () => {\n        storage[area].clear()\n        expect(browser.storage[area].clear.calledOnce).toBeTruthy()\n      })\n      it(`storage.${area}.remove`, () => {\n        const key = `key-${area}`\n        storage[area].remove(key)\n        expect(browser.storage[area].remove.calledWith(key)).toBeTruthy()\n      })\n      it(`storage.${area}.get`, () => {\n        const key = `key-${area}`\n        storage[area].get(key)\n        expect(browser.storage[area].get.calledWith(key)).toBeTruthy()\n      })\n      it(`storage.${area}.set`, () => {\n        const key = { key: area }\n        storage[area].set(key)\n        expect(browser.storage[area].set.calledWith(key)).toBeTruthy()\n      })\n      describe(`storage.${area}.addListener`, () => {\n        const changes = {\n          key: { newValue: 'new value', oldValue: 'old value' }\n        }\n        const otherArea = storageArea.find(x => x !== area)\n\n        it('with cb', () => {\n          const cb = jest.fn()\n          let cbCall = 0\n          storage[area].addListener(cb)\n          expect(browser.storage.onChanged.addListener.calledOnce).toBeTruthy()\n\n          browser.storage.onChanged.dispatch(changes, otherArea)\n          expect(cb).toHaveBeenCalledTimes(cbCall)\n\n          browser.storage.onChanged.dispatch(changes, area)\n          expect(cb).toHaveBeenCalledTimes(++cbCall)\n          expect(cb).toBeCalledWith(changes, area)\n        })\n        it('with key and cb', () => {\n          const cb = jest.fn()\n          let cbCall = 0\n          storage[area].addListener('key', cb)\n          expect(browser.storage.onChanged.addListener.calledOnce).toBeTruthy()\n\n          browser.storage.onChanged.dispatch(changes, otherArea)\n          expect(cb).toHaveBeenCalledTimes(cbCall)\n\n          browser.storage.onChanged.dispatch({ badKey: 'value' }, area)\n          expect(cb).toHaveBeenCalledTimes(cbCall)\n\n          browser.storage.onChanged.dispatch(changes, area)\n          expect(cb).toHaveBeenCalledTimes(++cbCall)\n          expect(cb).toBeCalledWith(changes, area)\n        })\n      })\n      describe(`storage.${area}.removeListener`, () => {\n        const changes = {\n          key: { newValue: 'new value', oldValue: 'old value' }\n        }\n\n        it('with cb remove addListener with cb', () => {\n          const cb = jest.fn()\n          let cbCall = 0\n          storage[area].addListener(cb)\n\n          // won't affect cb\n          storage[area].removeListener('key', cb)\n          expect(\n            browser.storage.onChanged.removeListener.calledOnce\n          ).toBeTruthy()\n\n          browser.storage.onChanged.dispatch(changes, area)\n          expect(cb).toHaveBeenCalledTimes(++cbCall)\n          expect(cb).toBeCalledWith(changes, area)\n\n          storage[area].removeListener(cb)\n          expect(\n            browser.storage.onChanged.removeListener.calledTwice\n          ).toBeTruthy()\n\n          browser.storage.onChanged.dispatch(changes, area)\n          expect(cb).toHaveBeenCalledTimes(cbCall)\n\n          browser.storage.onChanged.dispatch({ badKey: 'value' }, area)\n          expect(cb).toHaveBeenCalledTimes(cbCall)\n        })\n\n        it('with cb, remove addListener with key and cb', () => {\n          const cb = jest.fn()\n          let cbCall = 0\n          storage[area].addListener('key', cb)\n\n          browser.storage.onChanged.dispatch(changes, area)\n          expect(cb).toHaveBeenCalledTimes(++cbCall)\n          expect(cb).toBeCalledWith(changes, area)\n\n          storage[area].removeListener(cb)\n          expect(\n            browser.storage.onChanged.removeListener.calledOnce\n          ).toBeTruthy()\n\n          browser.storage.onChanged.dispatch(changes, area)\n          expect(cb).toHaveBeenCalledTimes(cbCall)\n\n          browser.storage.onChanged.dispatch({ badKey: 'value' }, area)\n          expect(cb).toHaveBeenCalledTimes(cbCall)\n        })\n\n        it('with key and cb', () => {\n          const cb = jest.fn()\n          let cbCall = 0\n          storage[area].addListener('key', cb)\n\n          // won't affect key + cb\n          storage[area].removeListener('badkey', cb)\n          expect(\n            browser.storage.onChanged.removeListener.calledOnce\n          ).toBeTruthy()\n\n          browser.storage.onChanged.dispatch(changes, area)\n          expect(cb).toHaveBeenCalledTimes(++cbCall)\n          expect(cb).toBeCalledWith(changes, area)\n\n          storage[area].removeListener('key', cb)\n          expect(\n            browser.storage.onChanged.removeListener.calledTwice\n          ).toBeTruthy()\n\n          browser.storage.onChanged.dispatch(changes, area)\n          expect(cb).toHaveBeenCalledTimes(cbCall)\n\n          browser.storage.onChanged.dispatch({ badKey: 'value' }, area)\n          expect(cb).toHaveBeenCalledTimes(cbCall)\n        })\n      })\n      describe(`storage.${area}.createStream`, () => {\n        const changes = {\n          key: { newValue: 'new value', oldValue: 'old value' }\n        }\n        const otherArea = storageArea.find(x => x !== area)\n\n        it('with key', () => {\n          const nextStub = jest.fn()\n          const errorStub = jest.fn()\n          const completeStub = jest.fn()\n          storage[area]\n            .createStream<typeof changes.key>('key')\n            .pipe(take(1))\n            .subscribe(nextStub, errorStub, completeStub)\n          expect(browser.storage.onChanged.addListener.calledOnce).toBeTruthy()\n          expect(nextStub).toHaveBeenCalledTimes(0)\n          expect(errorStub).toHaveBeenCalledTimes(0)\n          expect(completeStub).toHaveBeenCalledTimes(0)\n\n          browser.storage.onChanged.dispatch(changes, otherArea)\n          expect(nextStub).toHaveBeenCalledTimes(0)\n          expect(errorStub).toHaveBeenCalledTimes(0)\n          expect(completeStub).toHaveBeenCalledTimes(0)\n\n          browser.storage.onChanged.dispatch(changes, area)\n          expect(nextStub).toHaveBeenCalledTimes(1)\n          expect(errorStub).toHaveBeenCalledTimes(0)\n          expect(completeStub).toHaveBeenCalledTimes(1)\n          expect(nextStub).toBeCalledWith(changes.key)\n        })\n      })\n    })\n\n    it('storage.clear', () => {\n      storage.clear()\n      expect(browser.storage.sync.clear.calledOnce).toBeTruthy()\n      expect(browser.storage.local.clear.calledOnce).toBeTruthy()\n    })\n    describe('storage.addListener', () => {\n      const changes = { key: { newValue: 'new value', oldValue: 'old value' } }\n      const otherChanges = { otherKey: 'other value' }\n\n      it('with cb', () => {\n        const cb = jest.fn()\n        let cbCall = 0\n        storage.addListener(cb)\n        expect(browser.storage.onChanged.addListener.calledOnce).toBeTruthy()\n        expect(cb).toHaveBeenCalledTimes(cbCall)\n\n        storageArea.forEach(area => {\n          browser.storage.onChanged.dispatch(changes, area)\n          expect(cb).toHaveBeenCalledTimes(++cbCall)\n          browser.storage.onChanged.dispatch(otherChanges, area)\n          expect(cb).toHaveBeenCalledTimes(++cbCall)\n        })\n      })\n      it('with key and cb', () => {\n        const cb = jest.fn()\n        let cbCall = 0\n        storage.addListener('key', cb)\n        expect(browser.storage.onChanged.addListener.calledOnce).toBeTruthy()\n        expect(cb).toHaveBeenCalledTimes(cbCall)\n\n        const changes = {\n          key: { newValue: 'new value', oldValue: 'old value' }\n        }\n        const otherChanges = { otherKey: 'other value' }\n        storageArea.forEach(area => {\n          browser.storage.onChanged.dispatch(changes, area)\n          expect(cb).toHaveBeenCalledTimes(++cbCall)\n          browser.storage.onChanged.dispatch(otherChanges, area)\n          expect(cb).toHaveBeenCalledTimes(cbCall)\n        })\n      })\n    })\n    describe('storage.removeListener', () => {\n      const changes = { key: { newValue: 'new value', oldValue: 'old value' } }\n      const otherChanges = { otherKey: 'other value' }\n\n      it('with cb', () => {\n        const cb = jest.fn()\n        let cbCall = 0\n        storage.addListener(cb)\n\n        storageArea.forEach(area => {\n          browser.storage.onChanged.dispatch(changes, area)\n          expect(cb).toHaveBeenCalledTimes(++cbCall)\n          browser.storage.onChanged.dispatch(otherChanges, area)\n          expect(cb).toHaveBeenCalledTimes(++cbCall)\n        })\n\n        storage.removeListener(cb)\n        expect(browser.storage.onChanged.removeListener.calledOnce).toBeTruthy()\n\n        storageArea.forEach(area => {\n          browser.storage.onChanged.dispatch(changes, area)\n          expect(cb).toHaveBeenCalledTimes(cbCall)\n          browser.storage.onChanged.dispatch(otherChanges, area)\n          expect(cb).toHaveBeenCalledTimes(cbCall)\n        })\n      })\n\n      it('with key and cb', () => {\n        const cb = jest.fn()\n        let cbCall = 0\n        storage.addListener('key', cb)\n\n        storageArea.forEach(area => {\n          browser.storage.onChanged.dispatch(changes, area)\n          expect(cb).toHaveBeenCalledTimes(++cbCall)\n          browser.storage.onChanged.dispatch(otherChanges, area)\n          expect(cb).toHaveBeenCalledTimes(cbCall)\n        })\n\n        storage.removeListener('otherKey', cb)\n        expect(browser.storage.onChanged.removeListener.calledOnce).toBeTruthy()\n\n        storageArea.forEach(area => {\n          browser.storage.onChanged.dispatch(changes, area)\n          expect(cb).toHaveBeenCalledTimes(++cbCall)\n          browser.storage.onChanged.dispatch(otherChanges, area)\n          expect(cb).toHaveBeenCalledTimes(cbCall)\n        })\n\n        storage.removeListener('key', cb)\n        expect(\n          browser.storage.onChanged.removeListener.calledTwice\n        ).toBeTruthy()\n\n        storageArea.forEach(area => {\n          browser.storage.onChanged.dispatch(changes, area)\n          expect(cb).toHaveBeenCalledTimes(cbCall)\n          browser.storage.onChanged.dispatch(otherChanges, area)\n          expect(cb).toHaveBeenCalledTimes(cbCall)\n        })\n      })\n    })\n    describe(`storage.createStream`, () => {\n      const changes1 = {\n        key1: { newValue: 'new value', oldValue: 'old value' }\n      }\n      const changes2 = {\n        key2: { newValue: 'new value', oldValue: 'old value' }\n      }\n\n      it('with key', () => {\n        const nextStub = jest.fn()\n        const errorStub = jest.fn()\n        const completeStub = jest.fn()\n        storage\n          .createStream<typeof changes2.key2>('key2')\n          .pipe(take(1))\n          .subscribe(nextStub, errorStub, completeStub)\n        expect(browser.storage.onChanged.addListener.calledOnce).toBeTruthy()\n        expect(nextStub).toHaveBeenCalledTimes(0)\n        expect(errorStub).toHaveBeenCalledTimes(0)\n        expect(completeStub).toHaveBeenCalledTimes(0)\n\n        browser.storage.onChanged.dispatch(changes1, 'local')\n        expect(nextStub).toHaveBeenCalledTimes(0)\n        expect(errorStub).toHaveBeenCalledTimes(0)\n        expect(completeStub).toHaveBeenCalledTimes(0)\n\n        browser.storage.onChanged.dispatch(changes2, 'sync')\n        expect(nextStub).toHaveBeenCalledTimes(1)\n        expect(errorStub).toHaveBeenCalledTimes(0)\n        expect(completeStub).toHaveBeenCalledTimes(1)\n        expect(nextStub).toBeCalledWith(changes2.key2)\n      })\n    })\n  })\n\n  describe('Message', () => {\n    it('message.send', () => {\n      const tabId = 1\n      const msg: Message = { type: 'OPEN_QS_PANEL' }\n\n      message.send(msg)\n      expect(browser.runtime.sendMessage.calledWith(msg)).toBeTruthy()\n      expect(browser.tabs.sendMessage.notCalled).toBeTruthy()\n\n      browser.runtime.sendMessage.flush()\n      browser.tabs.sendMessage.flush()\n      browser.runtime.sendMessage.callsFake(() => Promise.resolve({}))\n      browser.tabs.sendMessage.callsFake(() => Promise.resolve({}))\n\n      message.send(tabId, msg)\n      expect(browser.tabs.sendMessage.calledWith(tabId, msg)).toBeTruthy()\n      expect(browser.runtime.sendMessage.notCalled).toBeTruthy()\n    })\n    it('message.addListener', () => {\n      const cb1 = jest.fn()\n      const cb2 = jest.fn()\n      let cb1Call = 0\n      let cb2Call = 0\n      message.addListener(cb1)\n      message.addListener('OPEN_QS_PANEL', cb2)\n      expect(browser.runtime.onMessage.addListener.calledTwice).toBeTruthy()\n      expect(cb1).toHaveBeenCalledTimes(cb1Call)\n      expect(cb2).toHaveBeenCalledTimes(cb2Call)\n\n      browser.runtime.onMessage.dispatch({ type: 'CLOSE_QS_PANEL' })\n      expect(cb1).toHaveBeenCalledTimes(++cb1Call)\n      expect(cb2).toHaveBeenCalledTimes(cb2Call)\n\n      browser.runtime.onMessage.dispatch({ type: 'OPEN_QS_PANEL' })\n      expect(cb1).toHaveBeenCalledTimes(++cb1Call)\n      expect(cb2).toHaveBeenCalledTimes(++cb2Call)\n    })\n    it('message.removeListener', () => {\n      const cb1 = jest.fn()\n      const cb2 = jest.fn()\n      let cb1Call = 0\n      const cb2Call = 0\n      message.addListener('OPEN_QS_PANEL', cb1)\n      message.addListener('CLOSE_QS_PANEL', cb2)\n      browser.runtime.onMessage.dispatch({ type: 'OPEN_QS_PANEL' })\n      expect(cb1).toHaveBeenCalledTimes(++cb1Call)\n      expect(cb2).toHaveBeenCalledTimes(cb2Call)\n\n      message.removeListener('QUERY_QS_PANEL', cb1)\n      message.removeListener(cb2)\n      expect(browser.runtime.onMessage.removeListener.calledTwice).toBeTruthy()\n\n      browser.runtime.onMessage.dispatch({ type: 'OPEN_QS_PANEL' })\n      browser.runtime.onMessage.dispatch({ type: 'CLOSE_QS_PANEL' })\n      expect(cb1).toHaveBeenCalledTimes(++cb1Call)\n      expect(cb2).toHaveBeenCalledTimes(cb2Call)\n\n      message.removeListener('OPEN_QS_PANEL', cb1)\n      browser.runtime.onMessage.dispatch({ type: 'OPEN_QS_PANEL' })\n      expect(cb1).toHaveBeenCalledTimes(cb1Call)\n    })\n    describe('message.createStream', () => {\n      it('without argument', () => {\n        const nextStub = jest.fn()\n        const errorStub = jest.fn()\n        const completeStub = jest.fn()\n        message\n          .createStream()\n          .pipe(take(2))\n          .subscribe(nextStub, errorStub, completeStub)\n        expect(browser.runtime.onMessage.addListener.calledOnce).toBeTruthy()\n        expect(nextStub).toHaveBeenCalledTimes(0)\n        expect(errorStub).toHaveBeenCalledTimes(0)\n        expect(completeStub).toHaveBeenCalledTimes(0)\n\n        browser.runtime.onMessage.dispatch({ type: 1 })\n        expect(nextStub).toHaveBeenCalledTimes(1)\n        expect(errorStub).toHaveBeenCalledTimes(0)\n        expect(completeStub).toHaveBeenCalledTimes(0)\n        expect(nextStub).toBeCalledWith({ type: 1 })\n\n        browser.runtime.onMessage.dispatch({ type: 2 })\n        expect(nextStub).toHaveBeenCalledTimes(2)\n        expect(errorStub).toHaveBeenCalledTimes(0)\n        expect(completeStub).toHaveBeenCalledTimes(1)\n        expect(nextStub).toBeCalledWith({ type: 2 })\n      })\n\n      it('with message type', () => {\n        const nextStub = jest.fn()\n        const errorStub = jest.fn()\n        const completeStub = jest.fn()\n        message\n          .createStream('OPEN_QS_PANEL')\n          .pipe(take(1))\n          .subscribe(nextStub, errorStub, completeStub)\n        expect(browser.runtime.onMessage.addListener.calledOnce).toBeTruthy()\n        expect(nextStub).toHaveBeenCalledTimes(0)\n        expect(errorStub).toHaveBeenCalledTimes(0)\n        expect(completeStub).toHaveBeenCalledTimes(0)\n\n        browser.runtime.onMessage.dispatch({ type: 'CLOSE_QS_PANEL' })\n        expect(nextStub).toHaveBeenCalledTimes(0)\n        expect(errorStub).toHaveBeenCalledTimes(0)\n        expect(completeStub).toHaveBeenCalledTimes(0)\n\n        browser.runtime.onMessage.dispatch({ type: 'OPEN_QS_PANEL' })\n        expect(nextStub).toHaveBeenCalledTimes(1)\n        expect(errorStub).toHaveBeenCalledTimes(0)\n        expect(completeStub).toHaveBeenCalledTimes(1)\n        expect(nextStub).toBeCalledWith({ type: 'OPEN_QS_PANEL' })\n      })\n    })\n\n    it('message.self.initClient', () => {\n      browser.runtime.sendMessage.withArgs({ type: 'PAGE_INFO' }).returns(\n        Promise.resolve({\n          pageId: 'pageId',\n          faviconURL: 'faviconURL',\n          pageTitle: 'pageTitle',\n          pageURL: 'pageURL'\n        })\n      )\n      return message.self.initClient().then(() => {\n        expect(\n          browser.runtime.sendMessage.calledWith({ type: 'PAGE_INFO' })\n        ).toBeTruthy()\n        expect(window.pageId).toBe('pageId')\n        expect(window.faviconURL).toBe('faviconURL')\n        expect(window.pageTitle).toBe('pageTitle')\n        expect(window.pageURL).toBe('pageURL')\n      })\n    })\n    describe('message.self.initServer', () => {\n      const tab = {\n        id: 1,\n        favIconUrl: 'https://example.com/favIconUrl',\n        url: 'https://example.com/url',\n        title: 'title'\n      }\n\n      it('From tab', done => {\n        message.self.initServer()\n        expect(browser.runtime.onMessage.addListener.calledOnce).toBeTruthy()\n\n        browser.runtime.onMessage['_listeners']\n          [0]({ type: 'PAGE_INFO' }, { tab })\n          .then(response => {\n            expect(response).toEqual({\n              pageId: tab.id,\n              faviconURL: tab.favIconUrl,\n              pageTitle: tab.title,\n              pageURL: tab.url\n            })\n            done()\n          })\n      })\n\n      it('From browser action page', done => {\n        message.self.initServer()\n        expect(browser.runtime.onMessage.addListener.calledOnce).toBeTruthy()\n\n        const sendResponse = jest.fn()\n        browser.runtime.onMessage['_listeners']\n          [0]({ type: 'PAGE_INFO' }, {}, sendResponse)\n          .then(response => {\n            expect(response).toHaveProperty('pageId', 'popup')\n            done()\n          })\n      })\n\n      it('Self page message transmission', () => {\n        message.self.initServer()\n        expect(browser.runtime.onMessage.addListener.calledOnce).toBeTruthy()\n\n        browser.runtime.onMessage.dispatch({ type: '[[1]]', __pageId__: 1 }, {})\n        expect(\n          browser.runtime.sendMessage.calledWith({ type: '1', __pageId__: 1 })\n        ).toBeTruthy()\n        browser.runtime.sendMessage.resetHistory()\n\n        browser.runtime.onMessage.dispatch(\n          { type: '[[1]]', __pageId__: 1 },\n          { tab }\n        )\n        expect(\n          browser.tabs.sendMessage.calledWith(tab.id, {\n            type: '1',\n            __pageId__: 1\n          })\n        ).toBeTruthy()\n      })\n    })\n    it('message.self.send', () => {\n      window.pageId = 1\n      message.self.send({\n        type: 'QUERY_PANEL_STATE',\n        payload: 'value'\n      })\n      expect(\n        browser.runtime.sendMessage.calledWith({\n          type: '[[QUERY_PANEL_STATE]]',\n          __pageId__: window.pageId,\n          payload: 'value'\n        })\n      ).toBeTruthy()\n    })\n    it('message.self.addListener', () => {\n      window.pageId = 1\n      const cb1 = jest.fn()\n      const cb2 = jest.fn()\n      let cb1Call = 0\n      let cb2Call = 0\n      message.self.addListener(cb1)\n      message.addListener(cb2)\n      expect(browser.runtime.onMessage.addListener.calledTwice).toBeTruthy()\n      expect(cb1).toHaveBeenCalledTimes(cb1Call)\n      expect(cb2).toHaveBeenCalledTimes(cb2Call)\n\n      browser.runtime.onMessage.dispatch({ type: 1, __pageId__: window.pageId })\n      expect(cb1).toHaveBeenCalledTimes(++cb1Call)\n      expect(cb2).toHaveBeenCalledTimes(cb2Call)\n\n      browser.runtime.onMessage.dispatch({ type: 1 })\n      expect(cb1).toHaveBeenCalledTimes(cb1Call)\n      expect(cb2).toHaveBeenCalledTimes(++cb2Call)\n\n      browser.runtime.onMessage.dispatch({\n        type: 1,\n        __pageId__: window.pageId + 2\n      })\n      expect(cb1).toHaveBeenCalledTimes(cb1Call)\n      expect(cb2).toHaveBeenCalledTimes(cb2Call)\n    })\n    it('message.self.removeListener', () => {\n      window.pageId = 1\n      const cb1 = jest.fn()\n      let cb1Call = 0\n      message.self.addListener(cb1)\n      browser.runtime.onMessage.dispatch({ __pageId__: window.pageId })\n      expect(cb1).toHaveBeenCalledTimes(++cb1Call)\n\n      message.self.removeListener(cb1)\n      expect(browser.runtime.onMessage.removeListener.calledOnce).toBeTruthy()\n\n      browser.runtime.onMessage.dispatch({ __pageId__: window.pageId })\n      expect(cb1).toHaveBeenCalledTimes(cb1Call)\n    })\n    describe('message.self.createStream', () => {\n      it('without argument', () => {\n        window.pageId = 1\n        const nextStub = jest.fn()\n        const errorStub = jest.fn()\n        const completeStub = jest.fn()\n        message.self\n          .createStream()\n          .pipe(take(2))\n          .subscribe(nextStub, errorStub, completeStub)\n        expect(browser.runtime.onMessage.addListener.calledOnce).toBeTruthy()\n        expect(nextStub).toHaveBeenCalledTimes(0)\n        expect(errorStub).toHaveBeenCalledTimes(0)\n        expect(completeStub).toHaveBeenCalledTimes(0)\n\n        browser.runtime.onMessage.dispatch({\n          type: 1,\n          __pageId__: window.pageId\n        })\n        expect(nextStub).toHaveBeenCalledTimes(1)\n        expect(errorStub).toHaveBeenCalledTimes(0)\n        expect(completeStub).toHaveBeenCalledTimes(0)\n        expect(nextStub).toBeCalledWith({ type: 1, __pageId__: window.pageId })\n\n        browser.runtime.onMessage.dispatch({\n          type: 2,\n          __pageId__: window.pageId\n        })\n        expect(nextStub).toHaveBeenCalledTimes(2)\n        expect(errorStub).toHaveBeenCalledTimes(0)\n        expect(completeStub).toHaveBeenCalledTimes(1)\n        expect(nextStub).toBeCalledWith({ type: 2, __pageId__: window.pageId })\n      })\n\n      it('with message type', () => {\n        window.pageId = 1\n        const nextStub = jest.fn()\n        const errorStub = jest.fn()\n        const completeStub = jest.fn()\n        message.self\n          .createStream('OPEN_QS_PANEL')\n          .pipe(take(1))\n          .subscribe(nextStub, errorStub, completeStub)\n        expect(browser.runtime.onMessage.addListener.calledOnce).toBeTruthy()\n        expect(nextStub).toHaveBeenCalledTimes(0)\n        expect(errorStub).toHaveBeenCalledTimes(0)\n        expect(completeStub).toHaveBeenCalledTimes(0)\n\n        browser.runtime.onMessage.dispatch({\n          type: 'CLOSE_QS_PANEL',\n          __pageId__: window.pageId\n        })\n        expect(nextStub).toHaveBeenCalledTimes(0)\n        expect(errorStub).toHaveBeenCalledTimes(0)\n        expect(completeStub).toHaveBeenCalledTimes(0)\n\n        browser.runtime.onMessage.dispatch({\n          type: 'OPEN_QS_PANEL',\n          __pageId__: window.pageId\n        })\n        expect(nextStub).toHaveBeenCalledTimes(1)\n        expect(errorStub).toHaveBeenCalledTimes(0)\n        expect(completeStub).toHaveBeenCalledTimes(1)\n        expect(nextStub.mock.calls[0][0]).toEqual({\n          type: 'OPEN_QS_PANEL',\n          __pageId__: window.pageId\n        })\n      })\n    })\n  })\n  describe('openUrl', () => {\n    const url = 'https://example.com'\n\n    it('Existing tab', () => {\n      browser.tabs.query.returns(\n        Promise.resolve([\n          {\n            windowId: 10,\n            index: 1\n          }\n        ])\n      )\n      return openUrl(url).then(() => {\n        expect(browser.tabs.query.calledWith({ url })).toBeTruthy()\n        expect(\n          browser.tabs.highlight.calledWith({ tabs: 1, windowId: 10 })\n        ).toBeTruthy()\n        expect(browser.tabs.create.notCalled).toBeTruthy()\n      })\n    })\n    it('New tab', () => {\n      browser.tabs.query.returns(Promise.resolve([]))\n      return openUrl(url).then(() => {\n        expect(browser.tabs.query.calledWith({ url })).toBeTruthy()\n        expect(browser.tabs.highlight.notCalled).toBeTruthy()\n        expect(\n          browser.tabs.create.calledWith(sinon.match({ url }))\n        ).toBeTruthy()\n      })\n    })\n    it('Concat extension base url', () => {\n      browser.tabs.query.returns(Promise.resolve([]))\n      browser.runtime.getURL.returns('test')\n      return openUrl(url, true).then(() => {\n        expect(browser.runtime.getURL.calledWith(url)).toBeTruthy()\n        expect(\n          browser.tabs.create.calledWith(sinon.match({ url: 'test' }))\n        ).toBeTruthy()\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "test/specs/_helpers/check-update.spec.ts",
    "content": "import { checkUpdate } from '@/_helpers/check-update'\nimport _fetchMock, { FetchMock } from 'jest-fetch-mock'\nimport getDefaultConfig from '@/app-config'\n\nconst fetchMock = _fetchMock as FetchMock\n\ndescribe('Check Update', () => {\n  beforeAll(() => {\n    window.fetch = fetchMock\n  })\n\n  beforeEach(() => {\n    fetchMock.resetMocks()\n    window.appConfig = getDefaultConfig()\n  })\n\n  const tests = [\n    ['Same', 'v1.1.1', 'v1.1.1', 0],\n    ['Newer Patch', 'v1.1.2', 'v1.1.0', 1],\n    ['Newer Minor', 'v1.2.1', 'v1.0.1', 2],\n    ['Newer Major', 'v2.1.1', 'v0.1.1', 3],\n    ['Older Patch', 'v1.1.0', 'v1.1.2', -1],\n    ['Older Minor', 'v1.0.1', 'v1.2.1', -2],\n    ['Older Major', 'v0.1.1', 'v2.1.1', -3]\n  ] as const\n\n  tests.forEach(([title, newerVersion, olderVersion, diff]) => {\n    it(title, async () => {\n      const responseObj = { version: newerVersion }\n      const resolveSpy = jest.fn()\n      const rejectSpy = jest.fn()\n      const catchSpy = jest.fn()\n\n      fetchMock.mockResponseOnce(JSON.stringify(responseObj))\n\n      await checkUpdate(olderVersion.slice(1))\n        .then(resolveSpy, rejectSpy)\n        .catch(catchSpy)\n\n      expect(fetchMock).toHaveBeenCalledTimes(1)\n      expect(resolveSpy).toHaveBeenCalledTimes(1)\n      expect(rejectSpy).toHaveBeenCalledTimes(0)\n      expect(catchSpy).toHaveBeenCalledTimes(0)\n      expect(resolveSpy).toBeCalledWith({\n        diff,\n        data: responseObj\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "test/specs/_helpers/chs-to-chz.spec.ts",
    "content": "import chsToChz from '@/_helpers/chs-to-chz'\n\ndescribe('Chs to Chz', () => {\n  it('should convert chs to chz', () => {\n    expect(chsToChz('龙龟')).toBe('龍龜')\n  })\n})\n"
  },
  {
    "path": "test/specs/_helpers/lang-check.spec.ts",
    "content": "import {\n  isContainChinese,\n  isContainEnglish,\n  checkSupportedLangs,\n  SupportedLangs\n} from '@/_helpers/lang-check'\n\ndescribe('Language Check', () => {\n  it('isContainChinese should return ture if text contains Chinese', () => {\n    expect(isContainChinese('lo你ve.')).toBeTruthy()\n  })\n  it('isContainChinese should return false if text does not contain Chinese', () => {\n    expect(isContainChinese('love.')).toBeFalsy()\n  })\n  it('isContainEnglish should return ture if text contains English', () => {\n    expect(isContainEnglish('lo你ve.')).toBeTruthy()\n  })\n  it('isContainEnglish should return ture if text does not contain English', () => {\n    expect(isContainEnglish('你.')).toBeFalsy()\n  })\n\n  describe('Check supported languages', () => {\n    function tlHelper(matchAll: boolean) {\n      return function tl(\n        text: string,\n        ...args: Array<Exclude<keyof SupportedLangs, 'matchAll'>>\n      ) {\n        const langs = args.reduce(\n          (result, lang) => {\n            result[lang] = true\n            return result\n          },\n          {\n            chinese: false,\n            english: false,\n            japanese: false,\n            korean: false,\n            french: false,\n            spanish: false,\n            deutsch: false,\n            others: false,\n            matchAll\n          }\n        )\n        return checkSupportedLangs(langs, text)\n      }\n    }\n\n    describe('with matchAll on', () => {\n      const tl = tlHelper(true)\n\n      it('should return false with all meaningless characters', () => {\n        expect(tl('。「')).toBe(false)\n        expect(tl('。「', 'chinese')).toBe(false)\n        expect(tl('。「', 'english')).toBe(false)\n        expect(tl('。「', 'others')).toBe(false)\n        expect(tl('1234')).toBe(false)\n        expect(tl('1234', 'chinese')).toBe(false)\n        expect(tl('1234', 'others')).toBe(false)\n      })\n\n      it('should work with CJK', () => {\n        expect(tl('你好', 'chinese')).toBe(true)\n        expect(tl('コイル', 'japanese')).toBe(true)\n        expect(tl('你好', 'japanese')).toBe(false)\n\n        expect(tl('你好電脳コイル', 'chinese')).toBe(false)\n        expect(tl('你好電脳コイル', 'japanese')).toBe(false)\n        expect(tl('你好電脳コイル', 'chinese', 'japanese')).toBe(true)\n\n        expect(tl('你好 電脳コイル。', 'chinese', 'japanese')).toBe(false)\n        expect(tl('你好電脳コイル。', 'chinese', 'japanese', 'others')).toBe(\n          true\n        )\n\n        expect(tl('你好 電脳コイル❤️', 'chinese', 'japanese')).toBe(false)\n        expect(tl('你好電脳コイル❤️', 'chinese', 'japanese', 'others')).toBe(\n          true\n        )\n      })\n\n      it('should work with Latins', () => {\n        expect(tl('Love you', 'english')).toBe(true)\n        expect(tl('é', 'french')).toBe(true)\n        expect(tl('Love you', 'french')).toBe(false)\n\n        expect(tl('bon appétit', 'english')).toBe(false)\n        expect(tl('bon appétit', 'french')).toBe(false)\n        expect(tl('bon appétit', 'english', 'french')).toBe(true)\n\n        expect(tl('bon appétit?', 'english', 'french')).toBe(false)\n        expect(tl('bon appétit?', 'english', 'french', 'others')).toBe(true)\n\n        expect(tl('bon appétit❤️', 'english', 'french')).toBe(false)\n        expect(tl('bon appétit❤️', 'english', 'french', 'others')).toBe(true)\n      })\n    })\n\n    describe('with matchAll off', () => {\n      const tl = tlHelper(false)\n\n      it('should return false with all meaningless characters', () => {\n        expect(tl('。「')).toBe(false)\n        expect(tl('。「', 'chinese')).toBe(false)\n        expect(tl('。「', 'english')).toBe(false)\n        expect(tl('。「', 'others')).toBe(false)\n        expect(tl('1')).toBe(false)\n        expect(tl('1', 'english')).toBe(false)\n        expect(tl('1', 'others')).toBe(false)\n      })\n\n      it('should work with CJK', () => {\n        expect(tl('你好', 'chinese')).toBe(true)\n        expect(tl('コイル', 'japanese')).toBe(true)\n        expect(tl('你好', 'japanese')).toBe(false)\n\n        expect(tl('你好電脳コイル', 'chinese')).toBe(true)\n        expect(tl('你好電脳コイル', 'japanese')).toBe(true)\n        expect(tl('你好電脳コイル', 'chinese', 'japanese')).toBe(true)\n\n        expect(tl('你好 電脳コイル。', 'chinese', 'japanese')).toBe(true)\n        expect(tl('你好電脳コイル。', 'chinese', 'japanese', 'others')).toBe(\n          true\n        )\n\n        expect(tl('你好 電脳コイル❤️。', 'chinese', 'japanese')).toBe(true)\n        expect(tl('你好電脳コイル❤️。', 'chinese', 'japanese', 'others')).toBe(\n          true\n        )\n        expect(tl('❤️', 'others')).toBe(true)\n        expect(tl('你好電脳コイル❤️。', 'others')).toBe(true)\n        expect(tl('你好電脳コイル。', 'others')).toBe(false)\n      })\n\n      it('should work with Latins', () => {\n        expect(tl('Love you', 'english')).toBe(true)\n        expect(tl('é', 'french')).toBe(true)\n        expect(tl('Love you', 'french')).toBe(false)\n\n        expect(tl('bon appétit', 'english')).toBe(true)\n        expect(tl('bon appétit', 'french')).toBe(true)\n        expect(tl('bon appétit', 'english', 'french')).toBe(true)\n\n        expect(tl('bon appétit?', 'english', 'french')).toBe(true)\n        expect(tl('bon appétit?', 'english', 'french', 'others')).toBe(true)\n\n        expect(tl('❤️', 'english', 'french')).toBe(false)\n        expect(tl('❤️', 'others')).toBe(true)\n        expect(tl('bon appétit❤️', 'english', 'french')).toBe(true)\n        expect(tl('bon appétit❤️', 'english', 'french', 'others')).toBe(true)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "test/specs/_helpers/profile-manager.spec.ts",
    "content": "import * as profileManagerOrigin from '@/_helpers/profile-manager'\nimport {\n  getDefaultProfile,\n  Profile,\n  getDefaultProfileID\n} from '@/app-config/profiles'\nimport sinon from 'sinon'\nimport { timer } from '@/_helpers/promise-more'\nimport { pick } from 'lodash'\nimport { browser } from '../../helper'\n\nfunction fakeStorageGet(store) {\n  browser.storage.sync.get.callsFake(keys => {\n    return Promise.resolve(\n      keys ? pick(store, Array.isArray(keys) ? keys : [keys]) : store\n    )\n  })\n}\n\nlet profileManager: typeof profileManagerOrigin\n\ndescribe('Profile Manager', () => {\n  beforeEach(() => {\n    browser.flush()\n    browser.storage.sync.set.callsFake(() => Promise.resolve())\n    browser.storage.sync.remove.callsFake(() => Promise.resolve())\n    jest.resetModules()\n    profileManager = require('@/_helpers/profile-manager')\n  })\n\n  it('should init with default profile the first time', async () => {\n    fakeStorageGet({})\n\n    const profile = await profileManager.initProfiles()\n    expect(typeof profile).toBe('object')\n    expect(\n      browser.storage.sync.set.calledWith(\n        sinon.match({\n          profileIDList: sinon.match.array,\n          activeProfileID: sinon.match.string\n        })\n      )\n    ).toBeTruthy()\n  })\n\n  it('should keep existing profiles when init', async () => {\n    const id1 = getDefaultProfileID()\n    const id2 = getDefaultProfileID()\n    const profile1 = getDefaultProfile(id1.id)\n    const profile2 = getDefaultProfile(id2.id)\n    const deflatedProfile1 = profileManager.deflate(profile1)\n    const deflatedProfile2 = profileManager.deflate(profile2)\n    fakeStorageGet({\n      profileIDList: [id1, id2],\n      activeProfileID: profile2.id,\n      [profile1.id]: deflatedProfile1,\n      [profile2.id]: deflatedProfile2\n    })\n\n    const profile = await profileManager.initProfiles()\n    expect(profile).toEqual(profile2)\n    expect(\n      browser.storage.sync.set.calledWith(\n        sinon.match({\n          profileIDList: [id1, id2],\n          activeProfileID: profile2.id\n        })\n      )\n    ).toBeTruthy()\n    expect(\n      browser.storage.sync.set.calledWith(\n        sinon.match({\n          [profile1.id]: deflatedProfile1\n        })\n      )\n    ).toBeTruthy()\n    expect(\n      browser.storage.sync.set.calledWith(\n        sinon.match({\n          [profile2.id]: deflatedProfile2\n        })\n      )\n    ).toBeTruthy()\n  })\n\n  it('should remove detached keys when init', async () => {\n    const id1 = getDefaultProfileID()\n    const id2 = getDefaultProfileID()\n    const profile1 = getDefaultProfile(id1.id)\n    const profile2 = getDefaultProfile(id2.id)\n    const detached1 = getDefaultProfile()\n    const detached2 = getDefaultProfile()\n    fakeStorageGet({\n      profileIDList: [id1, id2],\n      activeProfileID: profile2.id,\n      [profile1.id]: profileManager.deflate(profile1),\n      [profile2.id]: profileManager.deflate(profile2),\n      [detached1.id]: detached1,\n      [detached2.id]: detached2\n    })\n\n    const profile = await profileManager.initProfiles()\n    expect(profile).toEqual(profile2)\n    expect(\n      browser.storage.sync.set.calledWith(\n        sinon.match({\n          profileIDList: [id1, id2],\n          activeProfileID: profile2.id\n        })\n      )\n    ).toBeTruthy()\n  })\n\n  it('should reset to default profile', async () => {\n    const id1 = getDefaultProfileID()\n    const id2 = getDefaultProfileID()\n    const profile1 = getDefaultProfile(id1.id)\n    const profile2 = getDefaultProfile(id2.id)\n    fakeStorageGet({\n      profileIDList: [id1, id2],\n      activeProfileID: profile2.id,\n      [profile1.id]: profileManager.deflate(profile1),\n      [profile2.id]: profileManager.deflate(profile2)\n    })\n\n    await profileManager.resetAllProfiles()\n    expect(\n      browser.storage.sync.remove.calledWith(\n        sinon.match([\n          profile1.id,\n          profile2.id,\n          'profileIDList',\n          'activeProfileID',\n          'configProfileIDs',\n          'activeConfigID'\n        ])\n      )\n    ).toBeTruthy()\n    expect(\n      browser.storage.sync.set.calledWith(\n        sinon.match({\n          profileIDList: sinon.match.array,\n          activeProfileID: sinon.match.string\n        })\n      )\n    ).toBeTruthy()\n  })\n\n  it('should add profile', async () => {\n    const id1 = getDefaultProfileID()\n    const id2 = getDefaultProfileID()\n    const profile1 = getDefaultProfile(id1.id)\n    const profile2 = getDefaultProfile(id2.id)\n    fakeStorageGet({\n      profileIDList: [id1, id2],\n      activeProfileID: profile2.id,\n      [profile1.id]: profileManager.deflate(profile1),\n      [profile2.id]: profileManager.deflate(profile2)\n    })\n\n    const id3 = getDefaultProfileID()\n    await profileManager.addProfile(id3)\n    expect(\n      browser.storage.sync.set.calledWith({\n        profileIDList: [id1, id2, id3],\n        [id3.id]: sinon.match.object\n      })\n    ).toBeTruthy()\n  })\n\n  it('should remove profile', async () => {\n    const id1 = getDefaultProfileID()\n    const id2 = getDefaultProfileID()\n    const profile1 = getDefaultProfile(id1.id)\n    const profile2 = getDefaultProfile(id2.id)\n    fakeStorageGet({\n      profileIDList: [id1, id2],\n      activeProfileID: profile2.id,\n      [profile1.id]: profileManager.deflate(profile1),\n      [profile2.id]: profileManager.deflate(profile2)\n    })\n\n    await profileManager.removeProfile(profile1.id)\n    expect(browser.storage.sync.remove.calledWith(profile1.id)).toBeTruthy()\n    expect(\n      browser.storage.sync.set.calledWith({\n        profileIDList: [id2]\n      })\n    ).toBeTruthy()\n  })\n\n  it('should get active profile', async () => {\n    const id1 = getDefaultProfileID()\n    const id2 = getDefaultProfileID()\n    const profile1 = getDefaultProfile(id1.id)\n    const profile2 = getDefaultProfile(id2.id)\n    fakeStorageGet({\n      profileIDList: [id1, id2],\n      activeProfileID: profile2.id,\n      [profile1.id]: profileManager.deflate(profile1),\n      [profile2.id]: profileManager.deflate(profile2)\n    })\n\n    expect((await profileManager.getActiveProfile()).id).toBe(profile2.id)\n  })\n\n  it('should update profile ID list', async () => {\n    const id1 = getDefaultProfileID()\n    const id2 = getDefaultProfileID()\n    await profileManager.updateProfileIDList([id2, id1])\n    expect(\n      browser.storage.sync.set.calledWith({\n        profileIDList: [id2, id1]\n      })\n    ).toBeTruthy()\n  })\n\n  it('should update active profile ID', async () => {\n    const id1 = getDefaultProfileID()\n    await profileManager.updateActiveProfileID(id1.id)\n    expect(\n      browser.storage.sync.set.calledWith({\n        activeProfileID: id1.id\n      })\n    ).toBeTruthy()\n  })\n\n  it('should update active profile', async () => {\n    const profile = getDefaultProfile()\n    await profileManager.updateProfile(profile)\n    expect(\n      browser.storage.sync.set.calledWith({\n        [profile.id]: sinon.match(profileManager.deflate(profile))\n      })\n    ).toBeTruthy()\n  })\n\n  describe('add active profile listener', () => {\n    let profile1: Profile\n    let profile2: Profile\n    let callback: jest.Mock\n\n    beforeEach(async () => {\n      const id1 = getDefaultProfileID()\n      const id2 = getDefaultProfileID()\n      profile1 = getDefaultProfile(id1.id)\n      profile2 = getDefaultProfile(id2.id)\n      fakeStorageGet({\n        profileIDList: [id1, id2],\n        activeProfileID: profile2.id,\n        [profile1.id]: profileManager.deflate(profile1),\n        [profile2.id]: profileManager.deflate(profile2)\n      })\n      callback = jest.fn()\n      await profileManager.addActiveProfileListener(callback)\n    })\n\n    it('should add storage event listener', () => {\n      expect(browser.storage.onChanged.addListener.calledOnce).toBeTruthy()\n    })\n\n    it('should fire if active profile has changed', async () => {\n      const newProfile2: Profile = {\n        ...profile2,\n        mtaAutoUnfold: 'popup'\n      }\n      browser.storage.onChanged.dispatch(\n        {\n          [profile2.id]: {\n            newValue: newProfile2,\n            oldValue: profile2\n          }\n        },\n        'sync'\n      )\n      await timer(0)\n      expect(callback).toBeCalledWith({\n        newProfile: newProfile2,\n        oldProfile: profile2\n      })\n    })\n\n    it('should not fire if active profile has not changed', async () => {\n      browser.storage.onChanged.dispatch(\n        {\n          [profile1.id]: {\n            newValue: {\n              ...profile1,\n              mtaAutoUnfold: 'popup'\n            },\n            oldValue: profile1\n          }\n        },\n        'sync'\n      )\n      await timer(0)\n      expect(callback).toHaveBeenCalledTimes(0)\n    })\n\n    it('should fire if active profile ID has changed', async () => {\n      browser.storage.onChanged.dispatch(\n        {\n          activeProfileID: {\n            newValue: profile1.id\n          }\n        },\n        'sync'\n      )\n      await timer(0)\n      expect(callback).toBeCalledWith({\n        newProfile: profile1\n      })\n    })\n\n    it('should fire if active profile ID has changed (with last ID)', async () => {\n      browser.storage.onChanged.dispatch(\n        {\n          activeProfileID: {\n            newValue: profile1.id,\n            oldValue: profile2.id\n          }\n        },\n        'sync'\n      )\n      await timer(0)\n      expect(callback).toBeCalledWith({\n        newProfile: profile1,\n        oldProfile: profile2\n      })\n    })\n  })\n\n  it('should create active profile stream', async () => {\n    const id1 = getDefaultProfileID()\n    const id2 = getDefaultProfileID()\n    const profile1 = getDefaultProfile(id1.id)\n    const profile2 = getDefaultProfile(id2.id)\n    fakeStorageGet({\n      profileIDList: [id1, id2],\n      activeProfileID: profile2.id,\n      [profile1.id]: profileManager.deflate(profile1),\n      [profile2.id]: profileManager.deflate(profile2)\n    })\n    const subscriber = jest.fn()\n\n    profileManager.createActiveProfileStream().subscribe(subscriber)\n    await timer(0)\n    expect(subscriber).toBeCalledWith(profile2)\n\n    browser.storage.onChanged.dispatch(\n      {\n        activeProfileID: {\n          newValue: profile1.id,\n          oldValue: profile2.id\n        }\n      },\n      'sync'\n    )\n    await timer(0)\n    expect(subscriber).toBeCalledWith(profile1)\n  })\n})\n"
  },
  {
    "path": "test/specs/_helpers/promise-more.spec.ts",
    "content": "import * as pm from '@/_helpers/promise-more'\n\ndescribe('Promise More', () => {\n  beforeAll(() => {\n    jest.useFakeTimers()\n  })\n  afterAll(() => {\n    jest.useRealTimers()\n  })\n\n  describe('reflect', () => {\n    it('All resolved', () => {\n      const resolveSpy = jest.fn()\n      const rejectSpy = jest.fn()\n      const catchSpy = jest.fn()\n      return pm\n        .reflect([1, 2, 3])\n        .then(resolveSpy, rejectSpy)\n        .catch(catchSpy)\n        .then(() => {\n          expect(resolveSpy).toBeCalledWith([1, 2, 3])\n          expect(rejectSpy).not.toBeCalled()\n          expect(catchSpy).not.toBeCalled()\n        })\n    })\n    it('Partly rejected', () => {\n      const resolveSpy = jest.fn()\n      const rejectSpy = jest.fn()\n      const catchSpy = jest.fn()\n      return pm\n        .reflect([1, 2, Promise.reject(null)])\n        .then(resolveSpy, rejectSpy)\n        .catch(catchSpy)\n        .then(() => {\n          expect(resolveSpy).toBeCalledWith([1, 2, null])\n          expect(rejectSpy).not.toBeCalled()\n          expect(catchSpy).not.toBeCalled()\n        })\n    })\n    it('All rejected', () => {\n      const resolveSpy = jest.fn()\n      const rejectSpy = jest.fn()\n      const catchSpy = jest.fn()\n      return pm\n        .reflect([\n          Promise.reject(null),\n          Promise.reject(null),\n          Promise.reject(null)\n        ])\n        .then(resolveSpy, rejectSpy)\n        .catch(catchSpy)\n        .then(() => {\n          expect(resolveSpy).toBeCalledWith([null, null, null])\n          expect(rejectSpy).not.toBeCalled()\n          expect(catchSpy).not.toBeCalled()\n        })\n    })\n  })\n\n  describe('any', () => {\n    it('All resolved', () => {\n      const resolveSpy = jest.fn()\n      const rejectSpy = jest.fn()\n      const catchSpy = jest.fn()\n      return pm\n        .any([1, 2, 3])\n        .then(resolveSpy, rejectSpy)\n        .catch(catchSpy)\n        .then(() => {\n          expect(resolveSpy).toBeCalledWith([1, 2, 3])\n          expect(rejectSpy).not.toBeCalled()\n          expect(catchSpy).not.toBeCalled()\n        })\n    })\n    it('Partly rejected', () => {\n      const resolveSpy = jest.fn()\n      const rejectSpy = jest.fn()\n      const catchSpy = jest.fn()\n      return pm\n        .any([1, 2, Promise.reject(null)])\n        .then(resolveSpy, rejectSpy)\n        .catch(catchSpy)\n        .then(() => {\n          expect(resolveSpy).toBeCalledWith([1, 2, null])\n          expect(rejectSpy).not.toBeCalled()\n          expect(catchSpy).not.toBeCalled()\n        })\n    })\n    it('All rejected', () => {\n      const resolveSpy = jest.fn()\n      const rejectSpy = jest.fn()\n      const catchSpy = jest.fn()\n      return pm\n        .any([Promise.reject(null), Promise.reject(null), Promise.reject(null)])\n        .then(resolveSpy, rejectSpy)\n        .catch(catchSpy)\n        .then(() => {\n          expect(resolveSpy).not.toBeCalled()\n          expect(rejectSpy).toBeCalledWith(expect.any(Error))\n          expect(catchSpy).not.toBeCalled()\n        })\n    })\n  })\n\n  describe('first', () => {\n    it('All resolved', () => {\n      const resolveSpy = jest.fn()\n      const rejectSpy = jest.fn()\n      const catchSpy = jest.fn()\n      return pm\n        .first([1, 2, 3])\n        .then(resolveSpy, rejectSpy)\n        .catch(catchSpy)\n        .then(() => {\n          expect(resolveSpy).toBeCalledWith(1)\n          expect(rejectSpy).not.toBeCalled()\n          expect(catchSpy).not.toBeCalled()\n        })\n    })\n    it('Partly rejected', () => {\n      const resolveSpy = jest.fn()\n      const rejectSpy = jest.fn()\n      const catchSpy = jest.fn()\n      return pm\n        .first([Promise.reject(null), 2, 3])\n        .then(resolveSpy, rejectSpy)\n        .catch(catchSpy)\n        .then(() => {\n          expect(resolveSpy).toBeCalledWith(2)\n          expect(rejectSpy).not.toBeCalled()\n          expect(catchSpy).not.toBeCalled()\n        })\n    })\n    it('All rejected', () => {\n      const resolveSpy = jest.fn()\n      const rejectSpy = jest.fn()\n      const catchSpy = jest.fn()\n      return pm\n        .first([\n          Promise.reject(null),\n          Promise.reject(null),\n          Promise.reject(null)\n        ])\n        .then(resolveSpy, rejectSpy)\n        .catch(catchSpy)\n        .then(() => {\n          expect(resolveSpy).not.toBeCalled()\n          expect(rejectSpy).toBeCalledWith(expect.any(Error))\n          expect(catchSpy).not.toBeCalled()\n        })\n    })\n  })\n\n  it('timer', () => {\n    const resolveSpy = jest.fn()\n    const rejectSpy = jest.fn()\n    const catchSpy = jest.fn()\n\n    const p = pm\n      .timer(10)\n      .then(resolveSpy, rejectSpy)\n      .catch(catchSpy)\n\n    expect(setTimeout).toBeCalledWith(expect.any(Function), 10)\n    jest.runAllTimers()\n    return p.then(() => {\n      expect(resolveSpy).toHaveBeenCalledTimes(1)\n      expect(rejectSpy).not.toBeCalled()\n      expect(catchSpy).not.toBeCalled()\n    })\n  })\n\n  describe('timeout', () => {\n    it('Finish before Timeout', () => {\n      const resolveSpy = jest.fn()\n      const rejectSpy = jest.fn()\n      const catchSpy = jest.fn()\n\n      const job = new Promise((resolve, reject) => {\n        setTimeout(() => resolve('job'), 10)\n      })\n\n      const p = pm\n        .timeout(job, 100)\n        .then(resolveSpy, rejectSpy)\n        .catch(catchSpy)\n\n      expect(setTimeout).toBeCalledWith(expect.any(Function), 100)\n      jest.runAllTimers()\n      return p.then(() => {\n        expect(resolveSpy).toHaveBeenCalledTimes(1)\n        expect(resolveSpy).toBeCalledWith('job')\n        expect(rejectSpy).not.toBeCalled()\n        expect(catchSpy).not.toBeCalled()\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "test/specs/background/audio-manager.spec.ts",
    "content": "import { AudioManager } from '@/background/audio-manager'\n\nconst audioManager = AudioManager.getInstance()\n\ndescribe('Audio Manager', () => {\n  const bakAudio = (window as any).Audio\n  const mockAudioInstances: any[] = []\n  const mockAudio = jest.fn(() => {\n    const instance = {\n      play: jest.fn(() => Promise.resolve()),\n      pause: jest.fn(),\n      addEventListener: jest.fn()\n    }\n    mockAudioInstances.push(instance)\n    return instance\n  })\n  beforeAll(() => {\n    ;(window as any).Audio = mockAudio\n  })\n  afterAll(() => {\n    ;(window as any).Audio = bakAudio\n  })\n  beforeEach(() => {\n    audioManager.reset()\n    mockAudio.mockClear()\n    mockAudioInstances.length = 0\n  })\n\n  it('load', () => {\n    const url = 'https://e.a/load.mp3'\n    expect(audioManager.load(url)).toBe(mockAudioInstances[0])\n    expect(mockAudio).toBeCalledWith(url)\n  })\n\n  it('play', () => {\n    const url = 'https://e.b/play.mp3'\n    expect(audioManager.play(url)).toBeInstanceOf(Promise)\n    expect(mockAudio).toBeCalledWith(url)\n    expect(mockAudioInstances.length).toBe(1)\n    expect(mockAudioInstances[0].play).toHaveBeenCalledTimes(1)\n  })\n\n  it('play x 2 interrupted', () => {\n    const url1 = 'https://e.b/play1.mp3'\n    const url2 = 'https://e.b/play2.mp3'\n    expect(audioManager.load(url1)).toBe(mockAudioInstances[0])\n    expect(mockAudio).toBeCalledWith(url1)\n    expect(audioManager.play(url2)).toBeInstanceOf(Promise)\n    expect(mockAudio).toBeCalledWith(url2)\n    expect(mockAudioInstances.length).toBe(2)\n    expect(mockAudioInstances[0].play).toHaveBeenCalledTimes(0)\n    expect(mockAudioInstances[0].pause).toHaveBeenCalledTimes(1)\n    expect(mockAudioInstances[1].play).toHaveBeenCalledTimes(1)\n    expect(mockAudioInstances[1].pause).toHaveBeenCalledTimes(0)\n  })\n})\n"
  },
  {
    "path": "test/specs/background/context-menus.spec.ts",
    "content": "import { getDefaultConfig, AppConfig, AppConfigMutable } from '@/app-config'\nimport sinon from 'sinon'\nimport { take } from 'rxjs/operators'\nimport '@/background/types'\nimport { timer } from '@/_helpers/promise-more'\nimport * as configManagerMock from '@/_helpers/__mocks__/config-manager'\nimport { openUrl as openUrlMock } from '@/_helpers/__mocks__/browser-api'\nimport { browser } from '../../helper'\n\nwindow.appConfig = getDefaultConfig()\n\njest.mock('@/_helpers/config-manager')\njest.mock('@/_helpers/browser-api')\n\nlet configManager: typeof configManagerMock\nlet openUrl: typeof openUrlMock\n\nfunction specialConfig() {\n  const config = getDefaultConfig() as AppConfigMutable\n  config.contextMenus.selected = ['youdao', 'dictcn']\n  return config\n}\n\ndescribe.skip('Context Menus', () => {\n  beforeAll(() => {\n    // Order matters. Do not change.\n    browser.flush()\n    browser.i18n.getUILanguage.returns('en')\n    jest.resetModules()\n    require('@/background/context-menus')\n    configManager = require('@/_helpers/config-manager')\n    openUrl = require('@/_helpers/browser-api').openUrl\n  })\n  afterAll(() => browser.flush())\n\n  describe('Context Menus Click', () => {\n    beforeEach(() => {\n      openUrl.mockClear()\n      browser.tabs.query.flush()\n      browser.runtime.getURL.callsFake(s => s)\n      browser.tabs.query\n        .onFirstCall()\n        .returns(Promise.resolve([{ url: 'test-url' }]))\n        .onSecondCall()\n        .returns(Promise.resolve([]))\n    })\n\n    it('init', () => {\n      expect(browser.contextMenus.onClicked.addListener.calledOnce).toBeTruthy()\n    })\n\n    it('google_page_translate', async () => {\n      browser.tabs.executeScript.flush()\n      browser.tabs.executeScript.callsFake(() => Promise.resolve())\n      browser.contextMenus.onClicked.dispatch({\n        menuItemId: 'google_page_translate'\n      })\n      expect(browser.tabs.executeScript.calledOnce).toBeTruthy()\n    })\n    it('youdao_page_translate', () => {\n      browser.tabs.executeScript.flush()\n      browser.tabs.executeScript.callsFake(() => Promise.resolve())\n      browser.contextMenus.onClicked.dispatch({\n        menuItemId: 'youdao_page_translate'\n      })\n      expect(\n        browser.tabs.executeScript.calledWith({ file: sinon.match('youdao') })\n      ).toBeTruthy()\n    })\n    it('view_as_pdf', async () => {\n      browser.tabs.query.onFirstCall().returns(Promise.resolve([]))\n      browser.contextMenus.onClicked.dispatch({ menuItemId: 'view_as_pdf' })\n      await timer(0)\n      expect(openUrl).toHaveBeenCalledTimes(1)\n    })\n    it('search_history', async () => {\n      browser.tabs.query.onFirstCall().returns(Promise.resolve([]))\n      browser.contextMenus.onClicked.dispatch({ menuItemId: 'search_history' })\n      await timer(0)\n      expect(openUrl).toHaveBeenCalledTimes(1)\n      expect(openUrl).toBeCalledWith(expect.stringContaining('history'))\n    })\n    it('notebook', async () => {\n      browser.tabs.query.onFirstCall().returns(Promise.resolve([]))\n      browser.contextMenus.onClicked.dispatch({ menuItemId: 'notebook' })\n      await timer(0)\n      expect(openUrl).toHaveBeenCalledTimes(1)\n      expect(openUrl).toBeCalledWith(expect.stringContaining('notebook'))\n    })\n    it('default', async () => {\n      browser.tabs.query.onFirstCall().returns(Promise.resolve([]))\n      browser.contextMenus.onClicked.dispatch({ menuItemId: 'bing_dict' })\n      await timer(0)\n      expect(openUrl).toHaveBeenCalledTimes(1)\n      expect(openUrl).toBeCalledWith(expect.stringContaining('bing'))\n    })\n  })\n\n  describe('initListener', () => {\n    let config: AppConfig\n\n    beforeEach(() => {\n      // Order matters. Do not change.\n      browser.flush()\n      browser.i18n.getUILanguage.returns('en')\n      config = specialConfig()\n      browser.contextMenus.removeAll.callsFake(() => Promise.resolve())\n      browser.contextMenus.create.callsFake((_, cb) => cb())\n      jest.resetModules()\n      configManager = require('@/_helpers/config-manager')\n    })\n\n    it('should set menus on init', done => {\n      const { init } = require('@/background/context-menus')\n      take(1)(init(config.contextMenus)).subscribe(() => {\n        expect(browser.contextMenus.removeAll.calledOnce).toBeTruthy()\n        expect(\n          browser.contextMenus.create.calledWithMatch(\n            { id: 'youdao' },\n            sinon.match.func\n          )\n        ).toBeTruthy()\n        expect(\n          browser.contextMenus.create.calledWithMatch(\n            { id: 'dictcn' },\n            sinon.match.func\n          )\n        ).toBeTruthy()\n        done()\n      })\n    })\n\n    it('should not init setup when called multiple times', done => {\n      const { init } = require('@/background/context-menus')\n      take(1)(init(config.contextMenus)).subscribe(() => {\n        expect(browser.contextMenus.removeAll.calledOnce).toBeTruthy()\n\n        const setMenus2$$ = init(config.contextMenus)\n        const setMenus3$$ = init(config.contextMenus)\n\n        expect(browser.contextMenus.removeAll.calledOnce).toBeTruthy()\n        expect(setMenus2$$).toBe(setMenus3$$)\n\n        done()\n      })\n    })\n\n    it(\"should do nothing when contex menus config didn't change\", done => {\n      const newConfig = specialConfig()\n      newConfig.active = !newConfig.active\n\n      const { init } = require('@/background/context-menus')\n      take(1)(init(config.contextMenus)).subscribe(() => {\n        expect(browser.contextMenus.removeAll.calledOnce).toBeTruthy()\n        configManager.dispatchConfigChangedEvent(newConfig, config)\n        setTimeout(() => {\n          expect(browser.contextMenus.removeAll.calledOnce).toBeTruthy()\n          done()\n        }, 0)\n      })\n    })\n\n    it('should set menus at first time change', done => {\n      const newConfig = specialConfig()\n      newConfig.contextMenus.selected.pop()\n\n      const { init } = require('@/background/context-menus')\n      take(1)(init(config.contextMenus)).subscribe(() => {\n        expect(browser.contextMenus.removeAll.calledOnce).toBeTruthy()\n        configManager.dispatchConfigChangedEvent(newConfig)\n        setTimeout(() => {\n          expect(browser.contextMenus.removeAll.calledTwice).toBeTruthy()\n          done()\n        }, 0)\n      })\n    })\n\n    it('should set menus when contex menus config changed', done => {\n      const newConfig = specialConfig()\n      newConfig.contextMenus.selected.pop()\n\n      const { init } = require('@/background/context-menus')\n      take(1)(init(config.contextMenus)).subscribe(() => {\n        expect(browser.contextMenus.removeAll.calledOnce).toBeTruthy()\n        configManager.dispatchConfigChangedEvent(newConfig, config)\n        setTimeout(() => {\n          expect(browser.contextMenus.removeAll.calledTwice).toBeTruthy()\n          done()\n        }, 0)\n      })\n    })\n\n    it('should only set twice if source emits values during the first setting', done => {\n      const { init } = require('@/background/context-menus')\n      take(1)(init(config.contextMenus)).subscribe(() => {\n        expect(browser.contextMenus.removeAll.calledOnce).toBeTruthy()\n\n        const newConfig1 = specialConfig()\n        newConfig1.contextMenus.selected = ['bing_dict']\n\n        const newConfig2 = specialConfig()\n        newConfig2.contextMenus.selected = ['iciba']\n\n        const newConfig3 = specialConfig()\n        newConfig3.contextMenus.selected = ['oxford']\n\n        const newConfig4 = specialConfig()\n        newConfig4.contextMenus.selected = ['youdao']\n\n        configManager.dispatchConfigChangedEvent(newConfig1, config)\n        configManager.dispatchConfigChangedEvent(newConfig2, newConfig1)\n        configManager.dispatchConfigChangedEvent(newConfig3, newConfig2)\n        configManager.dispatchConfigChangedEvent(newConfig4, newConfig3)\n\n        setTimeout(() => {\n          expect(browser.contextMenus.removeAll.calledThrice).toBeTruthy()\n          expect(\n            browser.contextMenus.create.calledWithMatch(\n              { id: 'bing_dict' },\n              sinon.match.func\n            )\n          ).toBeTruthy()\n          expect(\n            browser.contextMenus.create.calledWithMatch(\n              { id: 'iciba' },\n              sinon.match.func\n            )\n          ).toBeFalsy()\n          expect(\n            browser.contextMenus.create.calledWithMatch(\n              { id: 'oxford' },\n              sinon.match.func\n            )\n          ).toBeFalsy()\n          expect(\n            browser.contextMenus.create.calledWithMatch(\n              { id: 'youdao' },\n              sinon.match.func\n            )\n          ).toBeTruthy()\n          done()\n        }, 0)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "test/specs/background/initialization.spec.ts",
    "content": "import { message, storage } from '@/_helpers/browser-api'\nimport { getDefaultConfig } from '@/app-config'\nimport getDefaultProfile from '@/app-config/profiles'\nimport { timer } from '@/_helpers/promise-more'\nimport '@/background/types'\nimport { browser } from '../../helper'\n\nwindow.appConfig = getDefaultConfig()\nwindow.activeProfile = getDefaultProfile()\n\nwindow.fetch = jest.fn(() =>\n  Promise.resolve({\n    ok: true,\n    json: () => ''\n  })\n) as any\n\njest.mock('@/app-config/merge-config', () => {\n  const { getDefaultConfig } = require('@/app-config')\n  return {\n    mergeConfig: jest.fn(config =>\n      Promise.resolve(config || getDefaultConfig())\n    )\n  }\n})\n\njest.mock('@/app-config/merge-profile', () => {\n  const { getDefaultProfile } = require('@/app-config/profiles')\n  return {\n    mergeProfile: jest.fn(profile =>\n      Promise.resolve(profile || getDefaultProfile())\n    )\n  }\n})\n\njest.mock('@/background/context-menus', () => {\n  return { init: jest.fn(() => Promise.resolve()) }\n})\n\njest.mock('@/background/pdf-sniffer', () => {\n  return { init: jest.fn() }\n})\n\njest.mock('@/background/sync-manager', () => {\n  return { startSyncServiceInterval: jest.fn() }\n})\n\njest.mock('@/background/server', () => {\n  return { openQSPanel: jest.fn() }\n})\n\njest.mock('@/_helpers/check-update', () => {\n  return jest.fn().mockReturnValue(Promise.resolve())\n})\n\njest.doMock('@/_helpers/browser-api', () => {\n  return {\n    message,\n    storage,\n    openUrl: jest.fn(() => Promise.resolve())\n  }\n})\n\ndescribe('Initialization', () => {\n  let initMenus: jest.Mock\n  let initPdf: jest.Mock\n\n  beforeAll(() => {\n    browser.runtime.sendMessage.callsFake(() => Promise.resolve({}))\n    browser.tabs.sendMessage.callsFake(() => Promise.resolve({}))\n  })\n\n  beforeEach(() => {\n    browser.flush()\n    jest.resetModules()\n\n    const contextMenus = require('@/background/context-menus')\n    const pdfSniffer = require('@/background/pdf-sniffer')\n    initMenus = contextMenus.init\n    initPdf = pdfSniffer.init\n\n    browser.storage.sync.get.callsFake(() => Promise.resolve({}))\n    browser.storage.sync.set.callsFake(() => Promise.resolve())\n    browser.storage.sync.clear.callsFake(() => Promise.resolve())\n    browser.storage.local.get.callsFake(() => Promise.resolve({}))\n    browser.storage.local.set.callsFake(() => Promise.resolve())\n    browser.storage.local.clear.callsFake(() => Promise.resolve())\n\n    require('@/background/initialization')\n  })\n\n  it('should properly set up', async () => {\n    await timer(0)\n    expect(browser.runtime.onInstalled.addListener.calledOnce).toBeTruthy()\n    expect(browser.runtime.onStartup.addListener.calledOnce).toBeTruthy()\n    expect(browser.notifications.onClicked.addListener.calledOnce).toBeTruthy()\n    expect(\n      browser.notifications.onButtonClicked.addListener.calledOnce\n    ).toBeTruthy()\n    expect(initMenus).toHaveBeenCalledTimes(0)\n    expect(initPdf).toHaveBeenCalledTimes(0)\n  })\n\n  describe('onStartup', () => {\n    let checkUpdate: jest.Mock\n    beforeEach(() => {\n      checkUpdate = require('@/_helpers/check-update')\n    })\n\n    it('should not check update if last check was just now', async () => {\n      browser.storage.local.get.onFirstCall().returns(\n        Promise.resolve({\n          lastCheckUpdate: Date.now()\n        })\n      )\n      browser.runtime.onStartup.dispatch()\n\n      await timer(0)\n      expect(checkUpdate).toHaveBeenCalledTimes(0)\n    })\n  })\n})\n"
  },
  {
    "path": "test/specs/background/pdf-sniffer.spec.ts",
    "content": "import { getDefaultConfig, AppConfigMutable, AppConfig } from '@/app-config'\nimport { matchPatternToRegExpStr } from '@/_helpers/matchPatternToRegExpStr'\nimport { init as initPdfOrigin } from '@/background/pdf-sniffer'\nimport { timer } from '@/_helpers/promise-more'\nimport * as configManagerMock from '@/_helpers/__mocks__/config-manager'\nimport { browser } from '../../helper'\n\njest.mock('@/_helpers/config-manager')\n\nlet configManager: typeof configManagerMock\n\nfunction hasListenerPatch(fn) {\n  // @ts-ignore\n  if (this._listeners) {\n    // @ts-ignore\n    return this._listeners.some(x => x === fn)\n  }\n  return false\n}\n\nfunction changeConfig(newConfig: AppConfig, oldConfig: AppConfig) {\n  window.appConfig = newConfig\n  configManager.dispatchConfigChangedEvent(newConfig, oldConfig)\n}\n\nlet initPdf: typeof initPdfOrigin\n\ndescribe('PDF Sniffer', () => {\n  beforeEach(() => {\n    browser.flush()\n    browser.runtime.getURL.callsFake(s => s)\n    jest.resetModules()\n    initPdf = require('@/background/pdf-sniffer').init\n    configManager = require('@/_helpers/config-manager')\n    // @ts-ignore\n    browser.webRequest.onBeforeRequest.hasListener = hasListenerPatch\n    // @ts-ignore\n    browser.webRequest.onHeadersReceived.hasListener = hasListenerPatch\n    window.appConfig = getDefaultConfig()\n  })\n\n  const urlPdf = 'https://test.com/c.pdf'\n  const urlPdfEncoded = encodeURIComponent(urlPdf)\n  const urlTxt = 'https://test.com/c.txt'\n  const urlTxtEncoded = encodeURIComponent(urlTxt)\n\n  it('should not start sniffing if sniff config is off', async () => {\n    ;(window.appConfig as AppConfigMutable).pdfSniff = false\n    initPdf(window.appConfig)\n    await timer(0)\n    expect(\n      browser.webRequest.onBeforeRequest.addListener.notCalled\n    ).toBeTruthy()\n    expect(\n      browser.webRequest.onHeadersReceived.addListener.notCalled\n    ).toBeTruthy()\n    expect(configManager.addConfigListener).toHaveBeenCalledTimes(1)\n  })\n\n  it('should start snifffing if sniff config is on', async () => {\n    ;(window.appConfig as AppConfigMutable).pdfSniff = true\n    initPdf(window.appConfig)\n    await timer(0)\n    expect(\n      browser.webRequest.onBeforeRequest.addListener.calledOnce\n    ).toBeTruthy()\n    expect(\n      browser.webRequest.onHeadersReceived.addListener.calledOnce\n    ).toBeTruthy()\n    expect(configManager.addConfigListener).toHaveBeenCalledTimes(1)\n  })\n\n  it('should stop sniffing if sniff config is turned off', async () => {\n    ;(window.appConfig as AppConfigMutable).pdfSniff = true\n    initPdf(window.appConfig)\n    await timer(0)\n    changeConfig(\n      { ...window.appConfig, pdfSniff: false },\n      { ...window.appConfig, pdfSniff: true }\n    )\n    await timer(0)\n    expect(\n      browser.webRequest.onBeforeRequest.addListener.calledOnce\n    ).toBeTruthy()\n    expect(\n      browser.webRequest.onHeadersReceived.addListener.calledOnce\n    ).toBeTruthy()\n    expect(\n      browser.webRequest.onBeforeRequest.removeListener.calledOnce\n    ).toBeTruthy()\n    expect(\n      browser.webRequest.onHeadersReceived.removeListener.calledOnce\n    ).toBeTruthy()\n    expect(configManager.addConfigListener).toHaveBeenCalledTimes(1)\n  })\n\n  it('should start snifffing only once if init multiple times', async () => {\n    ;(window.appConfig as AppConfigMutable).pdfSniff = true\n    initPdf(window.appConfig)\n    initPdf(window.appConfig)\n    initPdf(window.appConfig)\n    initPdf(window.appConfig)\n    await timer(0)\n    expect(\n      browser.webRequest.onBeforeRequest.addListener.calledOnce\n    ).toBeTruthy()\n    expect(\n      browser.webRequest.onHeadersReceived.addListener.calledOnce\n    ).toBeTruthy()\n    expect(configManager.addConfigListener).toHaveBeenCalledTimes(1)\n  })\n\n  it('should start snifffing only once if being turned on multiple times', async () => {\n    ;(window.appConfig as AppConfigMutable).pdfSniff = false\n    initPdf(window.appConfig)\n    await timer(0)\n    changeConfig(\n      { ...window.appConfig, pdfSniff: true },\n      { ...window.appConfig, pdfSniff: false }\n    )\n    changeConfig(\n      { ...window.appConfig, pdfSniff: true },\n      { ...window.appConfig, pdfSniff: false }\n    )\n    await timer(0)\n    expect(\n      browser.webRequest.onBeforeRequest.addListener.calledOnce\n    ).toBeTruthy()\n    expect(\n      browser.webRequest.onHeadersReceived.addListener.calledOnce\n    ).toBeTruthy()\n    expect(configManager.addConfigListener).toHaveBeenCalledTimes(1)\n  })\n\n  it('should intercept ftp/file pdf request and redirect to pdf.js', async () => {\n    ;(window.appConfig as AppConfigMutable).pdfSniff = true\n    initPdf(window.appConfig)\n    await timer(0)\n    const handler = browser.webRequest.onBeforeRequest['_listeners'][0]\n    expect(handler({ url: urlPdf })).toEqual({\n      redirectUrl: expect.stringMatching(urlPdfEncoded)\n    })\n    expect(handler({ url: urlTxt })).toEqual({\n      redirectUrl: expect.stringMatching(urlTxtEncoded)\n    })\n  })\n\n  it('should not intercept ftp/file pdf request if the url matches blacklist', async () => {\n    ;(window.appConfig as AppConfigMutable).pdfSniff = true\n    ;(window.appConfig as AppConfigMutable).pdfBlacklist = [\n      [matchPatternToRegExpStr(urlPdf), urlPdf]\n    ]\n    initPdf(window.appConfig)\n    await timer(0)\n    const handler = browser.webRequest.onBeforeRequest['_listeners'][0]\n    expect(handler({ url: urlPdf })).toBeUndefined()\n    expect(handler({ url: urlTxt })).toEqual({\n      redirectUrl: expect.stringMatching(urlTxtEncoded)\n    })\n  })\n\n  it('should intercept ftp/file pdf request if the url matches whitelist', async () => {\n    ;(window.appConfig as AppConfigMutable).pdfSniff = true\n    ;(window.appConfig as AppConfigMutable).pdfWhitelist = [\n      [matchPatternToRegExpStr(urlPdf), urlPdf]\n    ]\n    initPdf(window.appConfig)\n    await timer(0)\n    const handler = browser.webRequest.onBeforeRequest['_listeners'][0]\n    expect(handler({ url: urlPdf })).toEqual({\n      redirectUrl: expect.stringMatching(urlPdfEncoded)\n    })\n    expect(handler({ url: urlTxt })).toEqual({\n      redirectUrl: expect.stringMatching(urlTxtEncoded)\n    })\n  })\n\n  it('should intercept ftp/file pdf request if the url matches both blacklist and whitelist', async () => {\n    ;(window.appConfig as AppConfigMutable).pdfSniff = true\n    ;(window.appConfig as AppConfigMutable).pdfBlacklist = [\n      [matchPatternToRegExpStr(urlPdf), urlPdf]\n    ]\n    ;(window.appConfig as AppConfigMutable).pdfWhitelist = [\n      [matchPatternToRegExpStr(urlPdf), urlPdf]\n    ]\n    initPdf(window.appConfig)\n    await timer(0)\n    const handler = browser.webRequest.onBeforeRequest['_listeners'][0]\n    expect(handler({ url: urlPdf })).toEqual({\n      redirectUrl: expect.stringMatching(urlPdfEncoded)\n    })\n    expect(handler({ url: urlTxt })).toEqual({\n      redirectUrl: expect.stringMatching(urlTxtEncoded)\n    })\n  })\n\n  describe('intercept http/https pdf request and redirect to pdf.js', () => {\n    it('No PDF Content', async () => {\n      ;(window.appConfig as AppConfigMutable).pdfSniff = true\n      initPdf(window.appConfig)\n      await timer(0)\n      const handler = browser.webRequest.onHeadersReceived['_listeners'][0]\n      expect(handler({ resposeHeaders: [], url: urlPdf })).toBeUndefined()\n\n      const otherResponseHeaders = [{ name: 'content-type', value: 'other' }]\n      expect(\n        handler({ responseHeaders: otherResponseHeaders, url: urlPdf })\n      ).toBeUndefined()\n    })\n\n    it('With PDF Content Type', async () => {\n      ;(window.appConfig as AppConfigMutable).pdfSniff = true\n      initPdf(window.appConfig)\n      await timer(0)\n      const handler = browser.webRequest.onHeadersReceived['_listeners'][0]\n      const responseHeaders = [\n        { name: 'content-type', value: 'application/pdf' }\n      ]\n      expect(handler({ responseHeaders, url: urlPdf })).toEqual({\n        redirectUrl: expect.stringMatching(urlPdfEncoded)\n      })\n      expect(handler({ responseHeaders, url: urlTxt })).toEqual({\n        redirectUrl: expect.stringMatching(urlTxtEncoded)\n      })\n    })\n\n    it('PDF url with octet-stream Content Type', async () => {\n      ;(window.appConfig as AppConfigMutable).pdfSniff = true\n      initPdf(window.appConfig)\n      await timer(0)\n      const handler = browser.webRequest.onHeadersReceived['_listeners'][0]\n      const responseHeaders = [\n        { name: 'content-type', value: 'application/octet-stream' }\n      ]\n      expect(handler({ responseHeaders, url: urlPdf })).toEqual({\n        redirectUrl: expect.stringMatching(urlPdfEncoded)\n      })\n      expect(handler({ responseHeaders, url: urlTxt })).toBeUndefined()\n    })\n\n    it('should not intercept if the url matches blacklist', () => {\n      ;(window.appConfig as AppConfigMutable).pdfSniff = true\n      ;(window.appConfig as AppConfigMutable).pdfBlacklist = [\n        [matchPatternToRegExpStr(urlPdf), urlPdf]\n      ]\n      initPdf(window.appConfig)\n      const handler = browser.webRequest.onHeadersReceived['_listeners'][0]\n      const responseHeaders = [\n        { name: 'content-type', value: 'application/pdf' }\n      ]\n      expect(handler({ responseHeaders, url: urlPdf })).toBeUndefined()\n      expect(handler({ responseHeaders, url: urlTxt })).toEqual({\n        redirectUrl: expect.stringMatching(urlTxtEncoded)\n      })\n    })\n\n    it('should intercept if the url matches whitelist', async () => {\n      ;(window.appConfig as AppConfigMutable).pdfSniff = true\n      ;(window.appConfig as AppConfigMutable).pdfWhitelist = [\n        [matchPatternToRegExpStr(urlPdf), urlPdf]\n      ]\n      initPdf(window.appConfig)\n      await timer(0)\n      const handler = browser.webRequest.onHeadersReceived['_listeners'][0]\n      const responseHeaders = [\n        { name: 'content-type', value: 'application/pdf' }\n      ]\n      expect(handler({ responseHeaders, url: urlPdf })).toEqual({\n        redirectUrl: expect.stringMatching(urlPdfEncoded)\n      })\n      expect(handler({ responseHeaders, url: urlTxt })).toEqual({\n        redirectUrl: expect.stringMatching(urlTxtEncoded)\n      })\n    })\n\n    it('should intercept if the url matches both blacklist and whitelist', async () => {\n      ;(window.appConfig as AppConfigMutable).pdfSniff = true\n      ;(window.appConfig as AppConfigMutable).pdfBlacklist = [\n        [matchPatternToRegExpStr(urlPdf), urlPdf]\n      ]\n      ;(window.appConfig as AppConfigMutable).pdfWhitelist = [\n        [matchPatternToRegExpStr(urlPdf), urlPdf]\n      ]\n      initPdf(window.appConfig)\n      await timer(0)\n      const handler = browser.webRequest.onHeadersReceived['_listeners'][0]\n      const responseHeaders = [\n        { name: 'content-type', value: 'application/pdf' }\n      ]\n      expect(handler({ responseHeaders, url: urlPdf })).toEqual({\n        redirectUrl: expect.stringMatching(urlPdfEncoded)\n      })\n      expect(handler({ responseHeaders, url: urlTxt })).toEqual({\n        redirectUrl: expect.stringMatching(urlTxtEncoded)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "test/specs/background/sync-manager/services/ankiconnect.spec.ts",
    "content": "import axios from 'axios'\nimport AxiosMockAdapter from 'axios-mock-adapter'\n// import * as helpersMock from '@/background/sync-manager/__mocks__/helpers'\n// import { NotebookFile } from '@/background/sync-manager/interface'\nimport {\n  Service\n  // SyncConfig\n} from '@/background/sync-manager/services/ankiconnect'\n// import { Word, newWord } from '@/_helpers/record-manager'\n\njest.mock('@/background/sync-manager/helpers')\n\n// const helpers: typeof helpersMock = require('@/background/sync-manager/helpers')\n\ndescribe('Sync service Anki Connect', () => {\n  const axiosMock = new AxiosMockAdapter(axios)\n\n  const mockRequest = (handler: (data: any) => any[]) =>\n    axiosMock.onPost().reply(config => {\n      try {\n        return handler(JSON.parse(config.data))\n      } catch (e) {}\n      return [404]\n    })\n\n  afterAll(() => {\n    axiosMock.restore()\n  })\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n    axiosMock.reset()\n    axiosMock.onAny().reply(404)\n  })\n\n  describe('init', () => {\n    it('should warn if Anki Connect is not running.', async () => {\n      const config = Service.getDefaultConfig()\n\n      const service = new Service(config)\n      service.addWord = jest.fn(async () => null)\n\n      let error: Error | undefined\n      try {\n        await service.init()\n      } catch (e) {\n        error = e\n      }\n\n      expect(service.addWord).toHaveBeenCalledTimes(0)\n      expect(error?.message).toBe('server')\n    })\n\n    it('should warn if deck does not exist in Anki.', async () => {\n      const config = Service.getDefaultConfig()\n\n      mockRequest(data => {\n        switch (data.action) {\n          case 'version':\n            return [200, { result: 6, error: null }]\n          case 'deckNames':\n            return [200, { result: [], error: null }]\n          default:\n            return [404]\n        }\n      })\n\n      const service = new Service(config)\n      service.addWord = jest.fn(async () => null)\n\n      let error: Error | undefined\n      try {\n        await service.init()\n      } catch (e) {\n        error = e\n      }\n\n      expect(service.addWord).toHaveBeenCalledTimes(0)\n      expect(error?.message).toBe('deck')\n    })\n\n    it('should warn if note type does not exist in Anki.', async () => {\n      const config = Service.getDefaultConfig()\n\n      mockRequest(data => {\n        switch (data.action) {\n          case 'version':\n            return [200, { result: 6, error: null }]\n          case 'deckNames':\n            return [200, { result: [config.deckName], error: null }]\n          case 'modelNames':\n            return [200, { result: [], error: null }]\n          default:\n            return [404]\n        }\n      })\n\n      const service = new Service(config)\n      service.addWord = jest.fn(async () => null)\n\n      let error: Error | undefined\n      try {\n        await service.init()\n      } catch (e) {\n        error = e\n      }\n\n      expect(service.addWord).toHaveBeenCalledTimes(0)\n      expect(error?.message).toBe('notetype')\n    })\n\n    it('should init successfully', async () => {\n      const config = Service.getDefaultConfig()\n\n      mockRequest(data => {\n        switch (data.action) {\n          case 'version':\n            return [200, { result: 6, error: null }]\n          case 'deckNames':\n            return [200, { result: [config.deckName], error: null }]\n          case 'modelNames':\n            return [200, { result: [config.noteType], error: null }]\n          default:\n            return [404]\n        }\n      })\n\n      const service = new Service(config)\n      service.addWord = jest.fn(async () => null)\n\n      let error: Error | undefined\n      try {\n        await service.init()\n      } catch (e) {\n        error = e\n      }\n\n      expect(service.addWord).toHaveBeenCalledTimes(0)\n      expect(error).toBeUndefined()\n    })\n  })\n})\n"
  },
  {
    "path": "test/specs/background/sync-manager/services/webdav.spec.ts",
    "content": "import * as helpersMock from '@/background/sync-manager/__mocks__/helpers'\nimport { NotebookFile } from '@/background/sync-manager/interface'\nimport {\n  Service,\n  SyncConfig,\n  SyncMeta\n} from '@/background/sync-manager/services/webdav'\nimport { Word, newWord } from '@/_helpers/record-manager'\n\njest.mock('@/background/sync-manager/helpers')\n\nconst helpers: typeof helpersMock = require('@/background/sync-manager/helpers')\n\nconst fetchArgs = {\n  checkServer(config: SyncConfig) {\n    return [\n      config.url,\n      {\n        method: 'PROPFIND',\n        headers: {\n          Authorization:\n            'Basic ' + window.btoa(`${config.user}:${config.passwd}`),\n          'Content-Type': 'application/xml; charset=\"utf-8\"',\n          Depth: '1'\n        }\n      }\n    ]\n  },\n\n  createDir(config: SyncConfig) {\n    return [\n      config.url + 'Saladict',\n      {\n        method: 'MKCOL',\n        headers: {\n          Authorization:\n            'Basic ' + window.btoa(`${config.user}:${config.passwd}`)\n        }\n      }\n    ]\n  },\n\n  upload(config: SyncConfig, body: any = '') {\n    return [\n      config.url + 'Saladict/notebook.json',\n      {\n        method: 'PUT',\n        headers: {\n          Authorization:\n            'Basic ' + window.btoa(`${config.user}:${config.passwd}`)\n        },\n        body\n      }\n    ]\n  },\n\n  download(config: SyncConfig, headers: { [index: string]: string } = {}) {\n    return [\n      config.url + 'Saladict/notebook.json',\n      {\n        method: 'GET',\n        headers: {\n          Authorization:\n            'Basic ' + window.btoa(`${config.user}:${config.passwd}`),\n          ...headers\n        }\n      }\n    ]\n  }\n}\n\nfunction mockFetch(\n  config: SyncConfig,\n  route: Partial<\n    {\n      [k in keyof typeof fetchArgs]: (\n        url: string,\n        rqInit?: RequestInit\n      ) => Response\n    }\n  >\n) {\n  const urltokey: { [key: string]: keyof typeof fetchArgs } = Object.keys(\n    fetchArgs\n  ).reduce((o, k) => {\n    const args = fetchArgs[k](config)\n    o[args[0] + ((args[1] && args[1].method) || '')] = k\n    return o\n  }, {})\n\n  window.fetch = jest.fn(\n    (url: string, init?: RequestInit): Promise<Response> => {\n      const key = urltokey[url + ((init && init.method) || '')]\n      const handler = key && route[key]\n      if (handler) {\n        return Promise.resolve(handler(url, init))\n      }\n      return Promise.resolve(new Response())\n    }\n  ) as any\n}\n\ndescribe('Sync service WebDAV', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n    window.fetch = null as any\n  })\n\n  it('upload: should success', async () => {\n    const config: SyncConfig = {\n      enable: true,\n      url: 'https://example.com/dav/',\n      user: 'user',\n      passwd: 'passwd',\n      duration: 0\n    }\n\n    const fetchInit = {\n      upload: jest.fn(() => new Response())\n    }\n    mockFetch(config, fetchInit)\n\n    const words = [getWord(), getWord({ text: 'word' })]\n    helpers.getNotebook.mockImplementationOnce(() => Promise.resolve(words))\n\n    const service = new Service(config)\n\n    await service.add({ force: true })\n\n    expect(fetchInit.upload).toHaveBeenCalledTimes(1)\n    expect(fetchInit.upload).lastCalledWith(\n      ...fetchArgs.upload(\n        config,\n        expect.stringContaining(JSON.stringify(words))\n      )\n    )\n  })\n\n  describe('download', () => {\n    it('should save file on first download', async () => {\n      const config: SyncConfig = {\n        enable: true,\n        url: 'https://example.com/dav/',\n        user: 'user',\n        passwd: 'passwd',\n        duration: 0\n      }\n\n      const words = [\n        getWord({\n          ...newWord({ text: 'test' }),\n          date: Date.now()\n        })\n      ]\n      const timestamp = Date.now()\n      const file: NotebookFile = { timestamp, words }\n\n      const etag = 'etag222'\n\n      const fetchInit = {\n        download: jest.fn(\n          () =>\n            new Response(JSON.stringify(file), {\n              headers: {\n                etag\n              }\n            })\n        )\n      }\n\n      mockFetch(config, fetchInit)\n\n      const service = new Service(config)\n\n      await service.download({})\n\n      expect(helpers.setNotebook).lastCalledWith(words)\n      expect(helpers.setMeta).lastCalledWith('webdav', { timestamp, etag })\n      expect(fetchInit.download).toHaveBeenCalledTimes(1)\n      expect(fetchInit.download).lastCalledWith(...fetchArgs.download(config))\n    })\n\n    it('should save file if etag changed', async () => {\n      const config: SyncConfig = {\n        enable: true,\n        url: 'https://example.com/dav/',\n        user: 'user',\n        passwd: 'passwd',\n        duration: 0\n      }\n\n      const words = [\n        getWord({\n          ...newWord({ text: 'test' }),\n          date: Date.now()\n        })\n      ]\n      const timestamp = Date.now()\n      const file: NotebookFile = { timestamp, words }\n\n      const etagOrigin = 'etag12345'\n      const etag = 'etag222'\n\n      const fetchInit = {\n        download: jest.fn(\n          () =>\n            new Response(JSON.stringify(file), {\n              headers: {\n                etag\n              }\n            })\n        )\n      }\n\n      mockFetch(config, fetchInit)\n\n      const service = new Service(config)\n      service.meta = { etag: etagOrigin }\n\n      await service.download({})\n\n      expect(helpers.setNotebook).lastCalledWith(words)\n      expect(helpers.setMeta).lastCalledWith('webdav', { timestamp, etag })\n      expect(fetchInit.download).toHaveBeenCalledTimes(1)\n      expect(fetchInit.download).lastCalledWith(\n        ...fetchArgs.download(config, {\n          'If-None-Match': etagOrigin,\n          'If-Modified-Since': etagOrigin\n        })\n      )\n    })\n\n    it('should do nothing if 304 (same etag)', async () => {\n      const config: SyncConfig = {\n        enable: true,\n        url: 'https://example.com/dav/',\n        user: 'user',\n        passwd: 'passwd',\n        duration: 0\n      }\n\n      const etag = 'etag222'\n\n      const fetchInit = {\n        download: jest.fn(\n          () =>\n            new Response(null, {\n              status: 304,\n              headers: {\n                etag\n              }\n            })\n        )\n      }\n\n      mockFetch(config, fetchInit)\n\n      const service = new Service(config)\n      service.meta = { etag }\n\n      await service.download({})\n\n      expect(helpers.setNotebook).toHaveBeenCalledTimes(0)\n      expect(helpers.setMeta).toHaveBeenCalledTimes(0)\n      expect(fetchInit.download).toHaveBeenCalledTimes(1)\n      expect(fetchInit.download).lastCalledWith(\n        ...fetchArgs.download(config, {\n          'If-None-Match': etag,\n          'If-Modified-Since': etag\n        })\n      )\n    })\n\n    it('should do nothing if etags are different but timestamps are identical', async () => {\n      const config: SyncConfig = {\n        enable: true,\n        url: 'https://example.com/dav/',\n        user: 'user',\n        passwd: 'passwd',\n        duration: 0\n      }\n\n      const file: NotebookFile = {\n        timestamp: Date.now(),\n        words: [\n          {\n            ...newWord({ text: 'test' }),\n            date: Date.now()\n          }\n        ]\n      }\n\n      const etagOrigin = 'etag12345'\n      const etag = 'etag222'\n\n      const fetchInit = {\n        download: jest.fn(\n          () =>\n            new Response(JSON.stringify(file), {\n              headers: {\n                etag\n              }\n            })\n        )\n      }\n\n      mockFetch(config, fetchInit)\n\n      const service = new Service(config)\n      service.meta = {\n        etag: etagOrigin,\n        timestamp: file.timestamp\n      }\n\n      await service.download({})\n\n      expect(helpers.setNotebook).toHaveBeenCalledTimes(0)\n      expect(helpers.setMeta).toHaveBeenCalledTimes(1)\n      expect(fetchInit.download).toHaveBeenCalledTimes(1)\n      expect(fetchInit.download).lastCalledWith(\n        ...fetchArgs.download(config, {\n          'If-None-Match': etagOrigin,\n          'If-Modified-Since': etagOrigin\n        })\n      )\n    })\n\n    it('should do nothing if etags are different but timestamps are identical', async () => {\n      const config: SyncConfig = {\n        enable: true,\n        url: 'https://example.com/dav/',\n        user: 'user',\n        passwd: 'passwd',\n        duration: 0\n      }\n\n      const file: NotebookFile = {\n        timestamp: Date.now(),\n        words: [\n          getWord({\n            ...newWord({ text: 'test' }),\n            date: Date.now()\n          })\n        ]\n      }\n\n      const etagOrigin = 'etag12345'\n      const etag = 'etag222'\n\n      const fetchInit = {\n        download: jest.fn(\n          () =>\n            new Response(JSON.stringify(file), {\n              headers: {\n                etag\n              }\n            })\n        )\n      }\n\n      mockFetch(config, fetchInit)\n\n      const service = new Service(config)\n      service.meta = {\n        etag: etagOrigin,\n        timestamp: file.timestamp\n      }\n\n      await service.download({})\n\n      expect(helpers.setNotebook).toHaveBeenCalledTimes(0)\n      expect(helpers.setMeta).toHaveBeenCalledTimes(1)\n      expect(fetchInit.download).toHaveBeenCalledTimes(1)\n      expect(fetchInit.download).lastCalledWith(\n        ...fetchArgs.download(config, {\n          'If-None-Match': etagOrigin,\n          'If-Modified-Since': etagOrigin\n        })\n      )\n    })\n\n    it('should do nothing if words are corrupted', async () => {\n      const config: SyncConfig = {\n        enable: true,\n        url: 'https://example.com/dav/',\n        user: 'user',\n        passwd: 'passwd',\n        duration: 0\n      }\n\n      const file: NotebookFile = {\n        timestamp: Date.now(),\n        words: ['corrupted format'] as any\n      }\n\n      const etag = 'etag222'\n\n      const fetchInit = {\n        download: jest.fn(\n          () =>\n            new Response(JSON.stringify(file), {\n              headers: {\n                etag\n              }\n            })\n        )\n      }\n\n      mockFetch(config, fetchInit)\n\n      const service = new Service(config)\n\n      try {\n        await service.download({})\n      } catch (e) {\n        expect(e.message).toBe('format')\n      }\n\n      expect(helpers.setNotebook).toHaveBeenCalledTimes(0)\n      expect(helpers.setMeta).toHaveBeenCalledTimes(0)\n      expect(fetchInit.download).toHaveBeenCalledTimes(1)\n      expect(fetchInit.download).lastCalledWith(...fetchArgs.download(config))\n    })\n\n    it('should do nothing if network failed', async () => {\n      const config: SyncConfig = {\n        enable: true,\n        url: 'https://example.com/dav/',\n        user: 'user',\n        passwd: 'passwd',\n        duration: 0\n      }\n\n      const fetchInit = {\n        download: jest.fn(\n          () =>\n            new Response(null, {\n              status: 404\n            })\n        )\n      }\n\n      mockFetch(config, fetchInit)\n\n      const service = new Service(config)\n\n      try {\n        await service.download({})\n      } catch (e) {\n        expect(e.message).toBe('network')\n      }\n\n      expect(helpers.setNotebook).toHaveBeenCalledTimes(0)\n      expect(helpers.setMeta).toHaveBeenCalledTimes(0)\n      expect(fetchInit.download).toHaveBeenCalledTimes(1)\n      expect(fetchInit.download).lastCalledWith(...fetchArgs.download(config))\n    })\n  })\n\n  describe('initServer', () => {\n    it('should create dir and upload files on first init', async () => {\n      const config: SyncConfig = {\n        enable: true,\n        url: 'https://example.com/dav/',\n        user: 'user',\n        passwd: 'passwd',\n        duration: 0\n      }\n\n      const file: NotebookFile = {\n        timestamp: Date.now(),\n        words: [\n          {\n            ...newWord({ text: 'test' }),\n            date: Date.now()\n          }\n        ]\n      }\n      const fileText = JSON.stringify(file)\n\n      const etag = 'etag222'\n\n      const fetchInit = {\n        checkServer: jest.fn(() => new Response(genXML())),\n        upload: jest.fn(() => new Response()),\n        download: jest.fn(\n          () =>\n            new Response(fileText, {\n              headers: {\n                etag\n              }\n            })\n        ),\n        createDir: jest.fn(() => new Response())\n      }\n\n      mockFetch(config, fetchInit)\n\n      const service = new Service(config)\n      service.download = jest.fn(() => Promise.resolve())\n\n      await service.init()\n\n      expect(service.download).toHaveBeenCalledTimes(0)\n      expect(fetchInit.checkServer).toHaveBeenCalledTimes(1)\n      expect(fetchInit.checkServer).lastCalledWith(\n        ...fetchArgs.checkServer(config)\n      )\n      expect(fetchInit.createDir).toHaveBeenCalledTimes(1)\n      expect(fetchInit.createDir).lastCalledWith(...fetchArgs.createDir(config))\n      expect(fetchInit.upload).toHaveBeenCalledTimes(0)\n      expect(fetchInit.download).toHaveBeenCalledTimes(0)\n      expect(helpers.setMeta).toHaveBeenCalledTimes(0)\n      expect(helpers.setNotebook).toHaveBeenCalledTimes(0)\n    })\n\n    it('should do nothing if local files are older', async () => {\n      const config: SyncConfig = {\n        enable: true,\n        url: 'https://example.com/dav/',\n        user: 'user',\n        passwd: 'passwd',\n        duration: 0\n      }\n\n      const file: NotebookFile = {\n        timestamp: Date.now(),\n        words: [\n          {\n            ...newWord({ text: 'test' }),\n            date: Date.now()\n          }\n        ]\n      }\n      const fileText = JSON.stringify(file)\n\n      const etagLocal = 'etag12345'\n      const etag = 'etag222'\n\n      const fetchInit = {\n        checkServer: jest.fn(() => new Response(genXML(true))),\n        upload: jest.fn(() => new Response()),\n        download: jest.fn(\n          () =>\n            new Response(fileText, {\n              headers: {\n                etag\n              }\n            })\n        ),\n        createDir: jest.fn(() => new Response())\n      }\n\n      helpers.getMeta.mockImplementationOnce(\n        (): Promise<SyncMeta> =>\n          Promise.resolve({\n            timestamp: file.timestamp - 100,\n            etag: etagLocal\n          })\n      )\n      mockFetch(config, fetchInit)\n\n      const service = new Service(config)\n      service.download = jest.fn(() => Promise.resolve())\n\n      await service.init()\n\n      expect(service.download).toHaveBeenCalledTimes(0)\n      expect(fetchInit.checkServer).toHaveBeenCalledTimes(1)\n      expect(fetchInit.checkServer).lastCalledWith(\n        ...fetchArgs.checkServer(config)\n      )\n      // @upstream JSDOM missing namespace selector support\n      // expect(fetchInit.createDir).toHaveBeenCalledTimes(0)\n      expect(fetchInit.upload).toHaveBeenCalledTimes(0)\n      expect(fetchInit.download).toHaveBeenCalledTimes(0)\n      expect(helpers.setMeta).toHaveBeenCalledTimes(0)\n      expect(helpers.setNotebook).toHaveBeenCalledTimes(0)\n    })\n\n    it('should reject with \"network\" if netword errored', async () => {\n      const config: SyncConfig = {\n        enable: true,\n        url: 'https://example.com/dav/',\n        user: 'user',\n        passwd: 'passwd',\n        duration: 0\n      }\n\n      const fetchInit = {\n        checkServer: jest.fn(() => new Response(null, { status: 404 })),\n        upload: jest.fn(() => new Response()),\n        download: jest.fn(() => new Response()),\n        createDir: jest.fn(() => new Response())\n      }\n\n      mockFetch(config, fetchInit)\n\n      const service = new Service(config)\n      service.download = jest.fn(() => Promise.resolve())\n\n      try {\n        await service.init()\n      } catch (e) {\n        expect(e.message).toBe('network')\n      }\n\n      expect(service.download).toHaveBeenCalledTimes(0)\n      expect(fetchInit.checkServer).toHaveBeenCalledTimes(1)\n      expect(fetchInit.checkServer).lastCalledWith(\n        ...fetchArgs.checkServer(config)\n      )\n      // @upstream JSDOM missing namespace selector support\n      // expect(fetchInit.createDir).toHaveBeenCalledTimes(0)\n      expect(fetchInit.upload).toHaveBeenCalledTimes(0)\n      expect(fetchInit.download).toHaveBeenCalledTimes(0)\n      expect(helpers.setMeta).toHaveBeenCalledTimes(0)\n      expect(helpers.setNotebook).toHaveBeenCalledTimes(0)\n    })\n\n    it('should reject with \"mkcol\" if cannot create dir', async () => {\n      const config: SyncConfig = {\n        enable: true,\n        url: 'https://example.com/dav/',\n        user: 'user',\n        passwd: 'passwd',\n        duration: 0\n      }\n\n      const fetchInit = {\n        checkServer: jest.fn(() => new Response(genXML(true))),\n        upload: jest.fn(() => new Response()),\n        download: jest.fn(() => new Response()),\n        createDir: jest.fn(() => new Response(null, { status: 504 }))\n      }\n\n      mockFetch(config, fetchInit)\n\n      const service = new Service(config)\n      service.download = jest.fn(() => Promise.resolve())\n\n      try {\n        await service.init()\n      } catch (e) {\n        expect(e.message).toBe('mkcol')\n      }\n\n      expect(service.download).toHaveBeenCalledTimes(0)\n      expect(fetchInit.checkServer).toHaveBeenCalledTimes(1)\n      expect(fetchInit.checkServer).lastCalledWith(\n        ...fetchArgs.checkServer(config)\n      )\n      expect(fetchInit.createDir).toHaveBeenCalledTimes(1)\n      expect(fetchInit.createDir).lastCalledWith(...fetchArgs.createDir(config))\n      expect(fetchInit.upload).toHaveBeenCalledTimes(0)\n      expect(fetchInit.download).toHaveBeenCalledTimes(0)\n      expect(helpers.setMeta).toHaveBeenCalledTimes(0)\n      expect(helpers.setNotebook).toHaveBeenCalledTimes(0)\n    })\n\n    // @upstream JSDOM missing namespace selector support\n    // it('should reject with \"exist\" if local has a newer file', async () => {\n    //   const config: SyncConfig = {\n    //     enable: true,\n    //     url: 'https://example.com/dav/',\n    //     user: 'user',\n    //     passwd: 'passwd',\n    //     duration: 0,\n    //   }\n\n    //   const file: NotebookFile = {\n    //     timestamp: Date.now(),\n    //     words: [\n    //       {\n    //         ...newWord({ text: 'test' }),\n    //         date: Date.now(),\n    //       }\n    //     ],\n    //   }\n    //   const fileText = JSON.stringify(file)\n\n    //   const etagLocal = 'etag12345'\n    //   const etag = 'etag222'\n\n    //   const fetchInit = {\n    //     checkServer: jest.fn(() => new Response(genXML(true))),\n    //     upload: jest.fn(() => new Response()),\n    //     download: jest.fn(() => new Response(\n    //       fileText,\n    //       {\n    //         headers: {\n    //           etag,\n    //         }\n    //       }\n    //     )),\n    //     createDir: jest.fn(() => new Response())\n    //   }\n\n    //   helpers.getMeta.mockImplementationOnce((): Promise<Meta> => Promise.resolve({\n    //     timestamp: file.timestamp + 100,\n    //     etag: etagLocal\n    //   }))\n    //   mockFetch(config, fetchInit)\n\n    //   c{ onsor t} err = await initServer(config)\n    //   exerrort(err).toBe('exist')\n    //   expect(fetchInit.checkServer).toHaveBeenCalledTimes(1)\n    //   expect(fetchInit.checkServer).lastCalledWith(...fetchArgs.checkServer(config))\n    //   // @upstream JSDOM missing namespace selector support\n    //   // expect(fetchInit.createDir).toHaveBeenCalledTimes(0)\n    //   expect(fetchInit.upload).toHaveBeenCalledTimes(0)\n    //   expect(fetchInit.download).toHaveBeenCalledTimes(0)\n    //   expect(helpers.setMeta).toHaveBeenCalledTimes(0)\n    //   expect(helpers.setNotebook).toHaveBeenCalledTimes(0)\n    // })\n  })\n})\n\nfunction genXML(withDir?: boolean): string {\n  const dir = `<d:response>\n    <d:href>/dav/Saladict/</d:href>\n    <d:propstat>\n      <d:prop>\n        <d:getlastmodified>Mon, 31 Oct 2018 07:31:21 GMT</d:getlastmodified>\n        <d:getcontentlength>0</d:getcontentlength>\n        <d:owner>example@somemail.com</d:owner>\n        <d:current-user-privilege-set>\n          <d:privilege>\n            <d:read />\n          </d:privilege>\n          <d:privilege>\n            <d:write />\n          </d:privilege>\n          <d:privilege>\n            <d:all />\n          </d:privilege>\n          <d:privilege>\n            <d:read_acl />\n          </d:privilege>\n          <d:privilege>\n            <d:write_acl />\n          </d:privilege>\n        </d:current-user-privilege-set>\n        <d:getcontenttype>httpd/unix-directory</d:getcontenttype>\n        <d:displayname>Saladict</d:displayname>\n        <d:resourcetype>\n          <d:collection />\n        </d:resourcetype>\n      </d:prop>\n      <d:status>HTTP/1.1 200 OK</d:status>\n    </d:propstat>\n  </d:response>`\n\n  return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n  <d:multistatus xmlns:d=\"DAV:\" xmlns:s=\"http://ns.example.com\">\n    <d:response>\n      <d:href>/dav/</d:href>\n      <d:propstat>\n        <d:prop>\n          <d:getlastmodified>Mon, 31 Oct 2018 07:31:21 GMT</d:getlastmodified>\n          <d:getcontentlength>0</d:getcontentlength>\n          <d:owner>example@somemail.com</d:owner>\n          <d:current-user-privilege-set>\n            <d:privilege>\n              <d:read />\n            </d:privilege>\n          </d:current-user-privilege-set>\n          <d:getcontenttype>httpd/unix-directory</d:getcontenttype>\n          <d:displayname>dav</d:displayname>\n          <d:resourcetype>\n            <d:collection />\n          </d:resourcetype>\n        </d:prop>\n        <d:status>HTTP/1.1 200 OK</d:status>\n      </d:propstat>\n    </d:response>\n    ${withDir ? dir : ''}\n  </d:multistatus>`\n}\n\nfunction getWord(word: Partial<Word> = {}): Word {\n  return {\n    date: Date.now(),\n    text: '',\n    context: '',\n    title: '',\n    url: '',\n    favicon: '',\n    trans: '',\n    note: '',\n    ...word\n  }\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/ahdict/fixtures.js",
    "content": "module.exports = {\n  files: [\n    ['comment.html', 'https://ahdictionary.com/word/search.html?q=comment'],\n    ['love.html', 'https://ahdictionary.com/word/search.html?q=love'],\n    ['salad.html', 'https://ahdictionary.com/word/search.html?q=salad']\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/ahdict/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['comment', 'love', 'salad']\n\nexport const mockRequest: MockRequest = mock => {\n  mock\n    .onGet(/ahdictionary.+comment$/)\n    .reply(200, require('!raw-loader!./response/comment.html').default)\n\n  mock\n    .onGet(/ahdictionary.+love$/)\n    .reply(200, require('!raw-loader!./response/love.html').default)\n\n  mock\n    .onGet(/ahdictionary.+salad$/)\n    .reply(200, require('!raw-loader!./response/salad.html').default)\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/bing/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport {\n  search,\n  BingResultLex,\n  BingResultMachine,\n  BingResultRelated\n} from '@/components/dictionaries/bing/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile, ProfileMutable } from '@/app-config/profiles'\n\ndescribe('Dict/Bing/engine', () => {\n  it('should parse lex result correctly', () => {\n    const profile = getDefaultProfile() as ProfileMutable\n    profile.dicts.all.bing.options = {\n      tense: true,\n      phsym: true,\n      cdef: true,\n      related: true,\n      sentence: 4\n    }\n    return retry(() =>\n      search('love', getDefaultConfig(), profile, { isPDF: false }).then(\n        searchResult => {\n          expect(searchResult.audio).toHaveProperty(\n            'us',\n            expect.stringContaining('mp3')\n          )\n          expect(searchResult.audio).toHaveProperty(\n            'uk',\n            expect.stringContaining('mp3')\n          )\n\n          const result = searchResult.result as BingResultLex\n          expect(result.type).toBe('lex')\n          expect((result.phsym as any).length).toBeGreaterThan(0)\n          expect((result.cdef as any).length).toBeGreaterThan(0)\n          expect((result.infs as any).length).toBeGreaterThan(0)\n          expect(result.sentences).toHaveLength(4)\n        }\n      )\n    )\n  })\n\n  it('should parse machine result correctly', () => {\n    return retry(() =>\n      search(\n        'lose yourself in the dark',\n        getDefaultConfig(),\n        getDefaultProfile(),\n        { isPDF: false }\n      ).then(searchResult => {\n        expect(searchResult.audio).toBeUndefined()\n\n        const result = searchResult.result as BingResultMachine\n        expect(result.type).toBe('machine')\n        expect(typeof result.mt).toBe('string')\n        expect(result.mt.length).toBeGreaterThan(0)\n      })\n    )\n  })\n\n  it('should parse related result correctly', () => {\n    return retry(() =>\n      search('lovxx', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(searchResult => {\n        expect(searchResult.audio).toBeUndefined()\n\n        const result = searchResult.result as BingResultRelated\n        expect(result.type).toBe('related')\n        expect(result.defs.length).toBeGreaterThan(0)\n      })\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/bing/fixtures.js",
    "content": "module.exports = {\n  files: [\n    [\n      'lex.html',\n      'https://cn.bing.com/dict/clientsearch?mkt=zh-CN&setLang=zh&form=BDVEHC&ClientVer=BDDTV3.5.1.4320&q=love'\n    ],\n    [\n      'machine.html',\n      `https://cn.bing.com/dict/clientsearch?mkt=zh-CN&setLang=zh&form=BDVEHC&ClientVer=BDDTV3.5.1.4320&q=${encodeURIComponent(\n        'lose yourself in the dark'\n      )}`\n    ],\n    [\n      'related.html',\n      'https://cn.bing.com/dict/clientsearch?mkt=zh-CN&setLang=zh&form=BDVEHC&ClientVer=BDDTV3.5.1.4320&q=lovxx'\n    ]\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/bing/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['love', 'machine', 'related']\n\nexport const mockRequest: MockRequest = mock => {\n  mock\n    .onGet(/bing\\.com.+love$/)\n    .reply(200, require('!raw-loader!./response/lex.html').default)\n\n  mock\n    .onGet(/bing\\.com.+machine$/)\n    .reply(200, require('!raw-loader!./response/machine.html').default)\n\n  mock\n    .onGet(/bing\\.com.+related$/)\n    .reply(200, require('!raw-loader!./response/related.html').default)\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/cambridge/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport { search } from '@/components/dictionaries/cambridge/engine'\nimport { getDefaultConfig, AppConfigMutable } from '@/app-config'\nimport getDefaultProfile from '@/app-config/profiles'\n\nconst fetchbak = window.fetch\n\ndescribe('Dict/Cambridge/engine', () => {\n  afterAll(() => {\n    window.fetch = fetchbak\n  })\n\n  it('should parse result (en) correctly', () => {\n    return retry(() =>\n      search('love', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(({ result, audio }) => {\n        expect(audio && typeof audio.uk).toBe('string')\n        expect(audio && typeof audio.us).toBe('string')\n\n        expect(result.length).toBeGreaterThanOrEqual(1)\n\n        expect(result.every(x => typeof x === 'string')).toBeGreaterThanOrEqual(\n          1\n        )\n      })\n    )\n  })\n\n  it('should parse result (zhs) correctly', () => {\n    return retry(() =>\n      search('house', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(({ result, audio }) => {\n        expect(audio && typeof audio.uk).toBe('string')\n        expect(audio && typeof audio.us).toBe('string')\n\n        expect(result.length).toBeGreaterThanOrEqual(1)\n\n        expect(result.every(x => typeof x === 'string')).toBeGreaterThanOrEqual(\n          1\n        )\n      })\n    )\n  })\n\n  it('should parse result (zht) correctly', () => {\n    const config = getDefaultConfig() as AppConfigMutable\n    config.langCode = 'zh-TW'\n    return retry(() =>\n      search('catch', config, getDefaultProfile(), { isPDF: false }).then(\n        ({ result, audio }) => {\n          expect(audio && typeof audio.uk).toBe('string')\n          expect(audio && typeof audio.us).toBe('string')\n\n          expect(result.length).toBeGreaterThanOrEqual(1)\n\n          expect(\n            result.every(x => typeof x === 'string')\n          ).toBeGreaterThanOrEqual(1)\n        }\n      )\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/cambridge/fixtures.js",
    "content": "module.exports = {\n  files: [\n    [\n      'catch-zht.html',\n      'https://dictionary.cambridge.org/zht/%E6%90%9C%E7%B4%A2/direct/?datasetsearch=english-chinese-traditional&q=catch'\n    ],\n    [\n      'house-zhs.html',\n      'https://dictionary.cambridge.org/zhs/%E6%90%9C%E7%B4%A2/direct/?datasetsearch=english-chinese-simplified&q=house'\n    ],\n    [\n      'love.html',\n      'https://dictionary.cambridge.org/search/direct/?datasetsearch=english&q=love'\n    ],\n    [\n      'jumblish.html',\n      'https://dictionary.cambridge.org/search/direct/?datasetsearch=english&q=jumblish'\n    ]\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/cambridge/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['jumblish', 'catch-zht', 'house-zhs', 'love']\n\nexport const mockRequest: MockRequest = mock => {\n  mock.onGet(/cambridge/).reply(info => {\n    return [\n      200,\n      require('!raw-loader!./response/' +\n        new URL(info.url!).searchParams.get('q') +\n        '.html').default\n    ]\n  })\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/cnki/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport { search } from '@/components/dictionaries/cnki/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile } from '@/app-config/profiles'\n\ndescribe('Dict/CNKI/engine', () => {\n  it('should parse result correctly', () => {\n    return retry(() =>\n      search('love', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(({ result, audio }) => {\n        expect(audio).toBeUndefined()\n        expect(result.dict.length).toBeGreaterThan(0)\n        expect(result.senbi.length).toBeGreaterThan(0)\n        expect(result.seneng.length).toBeGreaterThan(0)\n      })\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/cnki/fixtures.js",
    "content": "module.exports = {\n  files: [\n    ['love.html', 'http://dict.cnki.net/old/dict_result.aspx?scw=love']\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/cnki/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['love']\n\nexport const mockRequest: MockRequest = mock => {\n  mock.onGet(/cnki/).reply(info => {\n    return [\n      200,\n      require('!raw-loader!./response/' +\n        new URL(info.url!).searchParams.get('searchword') +\n        '.html').default\n    ]\n  })\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/cobuild/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport { search } from '@/components/dictionaries/cobuild/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile, ProfileMutable } from '@/app-config/profiles'\n\ndescribe('Dict/COBUILD/engine', () => {\n  it('should parse result correctly', () => {\n    const profile = getDefaultProfile() as ProfileMutable\n    return retry(() =>\n      search('love', getDefaultConfig(), profile, { isPDF: false }).then(\n        searchResult => {\n          expect(searchResult.result).toBeTruthy()\n        }\n      )\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/cobuild/fixtures.js",
    "content": "module.exports = {\n  files: [\n    ['how.html', 'https://www.collinsdictionary.com/dictionary/english/how'],\n    [\n      'love.html',\n      'https://www.collinsdictionary.com/zh/dictionary/english/love'\n    ]\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/cobuild/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['test']\n\nexport const mockRequest: MockRequest = mock => {\n  mock\n    .onGet(/collinsdictionary\\.com\\/zh/)\n    .reply(200, require('!raw-loader!./response/love.html').default)\n\n  mock\n    .onGet(/collinsdictionary/)\n    .reply(200, require('!raw-loader!./response/how.html').default)\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/etymonline/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport { search } from '@/components/dictionaries/etymonline/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile, ProfileMutable } from '@/app-config/profiles'\n\ndescribe('Dict/Etymonline/engine', () => {\n  it('should parse result correctly', () => {\n    const profile = getDefaultProfile() as ProfileMutable\n    profile.dicts.all.etymonline.options = {\n      chart: true,\n      resultnum: 4\n    }\n    return retry(() =>\n      search('love', getDefaultConfig(), profile, { isPDF: false }).then(\n        searchResult => {\n          expect(searchResult.audio).toBeUndefined()\n\n          const result = searchResult.result\n          expect(result.length).toBeGreaterThanOrEqual(1)\n          expect(typeof result[0].title).toBe('string')\n          expect(typeof result[0].href).toBe('string')\n          expect(typeof result[0].def).toBe('string')\n        }\n      )\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/etymonline/fixtures.js",
    "content": "module.exports = {\n  files: [\n    ['love-word.html', 'https://www.etymonline.com/word/love'],\n    ['love.html', 'http://www.etymonline.com/search?q=love']\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/etymonline/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['love-word', 'love']\n\nexport const mockRequest: MockRequest = mock => {\n  mock.onGet(/etymonline.+\\/word\\//).reply(info => {\n    return /love-word/.test(info.url || '')\n      ? [200, require('!raw-loader!./response/love-word.html').default]\n      : [404]\n  })\n\n  mock.onGet(/etymonline.+\\/search\\?/).reply(info => {\n    return /love-word/.test(info.url || '')\n      ? [404]\n      : [200, require('!raw-loader!./response/love-word.html').default]\n  })\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/eudic/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport { search } from '@/components/dictionaries/eudic/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile } from '@/app-config/profiles'\n\ndescribe('Dict/Eudic/engine', () => {\n  it('should parse result correctly', async () => {\n    return retry(() =>\n      search('love', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(searchResult => {\n        expect(searchResult.audio && typeof searchResult.audio.us).toBe(\n          'string'\n        )\n        expect(searchResult.result).toHaveLength(10)\n        const item = searchResult.result[0]\n        expect(typeof item.chs).toBe('string')\n        expect(typeof item.eng).toBe('string')\n        expect(typeof item.mp3).toBe('string')\n        expect(typeof item.channel).toBe('string')\n      })\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/eudic/fixtures.js",
    "content": "module.exports = {\n  files: [\n    ['love.html', 'https://dict.eudic.net/dicts/en/love'],\n    [\n      'sentences.html',\n      ([page]) => {\n        const statusMatch = /id=\"page-status\" value=\"([^\"]+)\"/.exec(page)\n        if (statusMatch) {\n          return {\n            url: 'https://dict.eudic.net/Dicts/en/tab-detail/-12',\n            method: 'post',\n            transformResponse: [data => data],\n            headers: {\n              'Content-Type': 'application/x-www-form-urlencoded'\n            },\n            responseType: 'text/plain',\n            data: `status=${statusMatch[1]}`\n          }\n        }\n      }\n    ]\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/eudic/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['love']\n\nexport const mockRequest: MockRequest = mock => {\n  mock.onAny(/eudic/).reply(info => {\n    const file = /tab-detail/.test(info.url || '') ? 'sentences' : 'love'\n    return [200, require(`raw-loader!./response/${file}.html`).default]\n  })\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/googledict/fixtures.js",
    "content": "module.exports = {\n  files: [\n    [\n      'chs-mouse.html',\n      'https://www.google.com/search?hl=en&safe=off&q=meaning:mouse'\n    ],\n    [\n      'chs-爱.html',\n      'https://www.google.com/search?hl=en&safe=off&q=meaning:' +\n        encodeURIComponent('爱')\n    ],\n    [\n      'en-love.html',\n      'https://www.google.com/search?hl=en&safe=off&q=meaning:love'\n    ],\n    [\n      'en-salad.html',\n      'https://www.google.com/search?hl=en&safe=off&q=meaning:salad'\n    ]\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/googledict/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['salad', 'mouse', '爱', 'love']\n\nexport const mockRequest: MockRequest = mock => {\n  mock\n    .onGet(/google.+(define|meaning).+mouse/)\n    .reply(200, require('!raw-loader!./response/chs-mouse.html').default)\n\n  mock\n    .onGet(new RegExp(encodeURIComponent('爱')))\n    .reply(200, require('!raw-loader!./response/chs-爱.html').default)\n\n  mock\n    .onGet(/google.+(define|meaning).+love/)\n    .reply(200, require('!raw-loader!./response/en-love.html').default)\n\n  mock\n    .onGet(/google.+(define|meaning).+salad/)\n    .reply(200, require('!raw-loader!./response/en-salad.html').default)\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/guoyu/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport { search } from '@/components/dictionaries/guoyu/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile } from '@/app-config/profiles'\n\ndescribe('Dict/GuoYu/engine', () => {\n  it('should parse result correctly', () => {\n    return retry(() =>\n      search('愛', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(searchResult => {\n        expect(searchResult.audio && typeof searchResult.audio.py).toBe(\n          'string'\n        )\n        expect(typeof searchResult.result.t).toBe('string')\n        expect(Array.isArray(searchResult.result.h)).toBeTruthy()\n        expect(searchResult.result.translation).toBeTruthy()\n      })\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/guoyu/fixtures.js",
    "content": "module.exports = {\n  files: [\n    ['愛.json', `https://www.moedict.tw/a/${encodeURIComponent('愛')}.json`]\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/guoyu/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['愛']\n\nexport const mockRequest: MockRequest = mock => {\n  mock.onGet(/moedict/).reply(200, require('./response/愛.json'))\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/helpers.ts",
    "content": "import { timer } from '@/_helpers/promise-more'\n\nexport async function retry(executor: () => Promise<any>, retryTimes = 1) {\n  let times = retryTimes + 1\n  while (times--) {\n    try {\n      return await executor()\n    } catch (e) {\n      await timer(1000)\n    }\n  }\n  console.error('>>>>> timeout')\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/hjdict/fixtures.js",
    "content": "module.exports = {\n  files: [\n    ['henr.html', 'https://www.hjdict.com/w/henr'],\n    ['love.html', 'https://www.hjdict.com/w/love'],\n    ['爱.html', 'https://www.hjdict.com/jp/jc/%E7%88%B1']\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/hjdict/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['爱', 'love', 'henr']\n\nexport const mockRequest: MockRequest = mock => {\n  mock.onGet(/hjdict/).reply(info => {\n    const wordMatch = /[^/]+$/.exec(info.url || '')\n    return wordMatch\n      ? [\n          200,\n          require(`raw-loader!./response/${decodeURIComponent(\n            wordMatch[0]\n          )}.html`).default\n        ]\n      : [404]\n  })\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/jikipedia/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport { search } from '@/components/dictionaries/jikipedia/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile } from '@/app-config/profiles'\n\ndescribe('Dict/Jikipedia/engine', () => {\n  it('should parse result correctly', () => {\n    return retry(() =>\n      search('xswl', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(searchResult => {\n        expect(typeof searchResult.result.length).toBeGreaterThan(0)\n        expect(searchResult.result[0].title).toBe('string')\n        expect(searchResult.result[0].content).toBe('string')\n      })\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/jikipedia/fixtures.js",
    "content": "module.exports = {\n  files: [['xswl.html', 'https://jikipedia.com/search?phrase=xswl']]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/jikipedia/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['xswl']\n\nexport const mockRequest: MockRequest = mock => {\n  mock\n    .onGet(/jikipedia/)\n    .reply(200, require(`raw-loader!./response/xswl.html`).default)\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/jukuu/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport { search } from '@/components/dictionaries/jukuu/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile } from '@/app-config/profiles'\n\ndescribe('Dict/Jukuu/engine', () => {\n  it('should parse result correctly', () => {\n    return retry(() =>\n      search('love', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(searchResult => {\n        expect(typeof searchResult.result.lang).toBe('string')\n        expect(searchResult.result.sens.length).toBeGreaterThan(0)\n        expect(typeof searchResult.result.sens[0].trans).toBe('string')\n        expect(searchResult.result.sens[0].trans.length).toBeGreaterThan(0)\n      })\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/jukuu/fixtures.js",
    "content": "module.exports = {\n  files: [['love.html', 'http://www.jukuu.com/search.php?q=love']]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/jukuu/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['love']\n\nexport const mockRequest: MockRequest = mock => {\n  mock\n    .onGet(/jukuu/)\n    .reply(200, require(`raw-loader!./response/love.html`).default)\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/lexico/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport {\n  search,\n  LexicoResultLex,\n  LexicoResultRelated\n} from '@/components/dictionaries/lexico/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile } from '@/app-config/profiles'\n\ndescribe('Dict/Lexico/engine', () => {\n  it('should parse lex result correctly', () => {\n    return retry(() =>\n      search('love', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(searchResult => {\n        expect(searchResult.audio && typeof searchResult.audio.uk).toBe(\n          'string'\n        )\n        expect(searchResult.audio && typeof searchResult.audio.us).toBe(\n          'string'\n        )\n\n        const result = searchResult.result as LexicoResultLex\n        expect(result.type).toBe('lex')\n        expect(typeof result.entry).toBe('string')\n      })\n    )\n  })\n\n  it('should parse related result correctly', () => {\n    return retry(() =>\n      search('jumblish', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(searchResult => {\n        expect(searchResult.audio).toBeUndefined()\n\n        const result = searchResult.result as LexicoResultRelated\n        expect(result.type).toBe('related')\n        expect(result.list.length).toBeGreaterThan(0)\n      })\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/lexico/fixtures.js",
    "content": "module.exports = {\n  files: [\n    ['jumblish.html', 'https://www.lexico.com/definition/jumblish'],\n    ['love.html', 'https://www.lexico.com/definition/love'],\n    ['how.html', 'https://www.lexico.com/definition/how']\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/lexico/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['how', 'love', 'jumblish']\n\nexport const mockRequest: MockRequest = mock => {\n  mock.onGet(/lexico/).reply(info => {\n    const wordMatch = /[^/]+$/.exec(info.url || '')\n    return wordMatch\n      ? [\n          200,\n          require(`raw-loader!./response/${decodeURIComponent(\n            wordMatch[0]\n          )}.html`).default\n        ]\n      : [404]\n  })\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/liangan/fixtures.js",
    "content": "module.exports = {\n  files: [\n    ['愛.json', `https://www.moedict.tw/c/${encodeURIComponent('愛')}.json`]\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/liangan/requests.mock.ts",
    "content": "export { mockSearchTexts, mockRequest } from '../guoyu/requests.mock'\n"
  },
  {
    "path": "test/specs/components/dictionaries/longman/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport {\n  search,\n  LongmanResultLex,\n  LongmanResultRelated\n} from '@/components/dictionaries/longman/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile, ProfileMutable } from '@/app-config/profiles'\n\ndescribe('Dict/Longman/engine', () => {\n  it('should parse lex result (love) correctly', () => {\n    const profile = getDefaultProfile() as ProfileMutable\n    profile.dicts.all.longman.options = {\n      wordfams: false,\n      collocations: true,\n      grammar: true,\n      thesaurus: true,\n      examples: true,\n      bussinessFirst: true,\n      related: true\n    }\n\n    return retry(() =>\n      search('love', getDefaultConfig(), profile, { isPDF: false }).then(\n        searchResult => {\n          expect(searchResult.audio && typeof searchResult.audio.uk).toBe(\n            'string'\n          )\n          expect(searchResult.audio && typeof searchResult.audio.us).toBe(\n            'string'\n          )\n\n          const result = searchResult.result as LongmanResultLex\n          expect(result.type).toBe('lex')\n\n          expect(result.bussinessFirst).toBe(true)\n          expect(result.wordfams).toBeUndefined()\n          expect(result.contemporary).toHaveLength(2)\n          expect(result.bussiness).toHaveLength(0)\n\n          result.contemporary.forEach(entry => {\n            expect(entry.title.HWD.length).toBeGreaterThan(0)\n            expect(entry.title.HYPHENATION.length).toBeGreaterThan(0)\n            expect(entry.title.HOMNUM.length).toBeGreaterThan(0)\n            expect(entry.senses.length).toBeGreaterThan(0)\n            expect(typeof entry.phsym).toBe('string')\n            expect(typeof entry.pos).toBe('string')\n            expect(entry.freq).toHaveLength(2)\n            expect(entry.level).toBeDefined()\n            expect((entry.level as any).rate).toBe(3)\n            expect(entry.prons).toHaveLength(2)\n          })\n\n          expect(typeof result.contemporary[0].grammar).toBe('string')\n          expect(typeof result.contemporary[0].thesaurus).toBe('string')\n          expect(result.contemporary[0].examples).toHaveLength(3)\n          expect(result.contemporary[0].topic).toBeUndefined()\n\n          expect(typeof result.contemporary[0].collocations).toBe('string')\n          expect(typeof result.contemporary[0].thesaurus).toBe('string')\n          expect(result.contemporary[1].examples).toHaveLength(4)\n        }\n      )\n    )\n  })\n\n  it('should parse lex result (profit) correctly', () => {\n    const profile = getDefaultProfile() as ProfileMutable\n    profile.dicts.all.longman.options = {\n      wordfams: true,\n      collocations: true,\n      grammar: true,\n      thesaurus: true,\n      examples: true,\n      bussinessFirst: false,\n      related: true\n    }\n\n    return retry(() =>\n      search('profit', getDefaultConfig(), profile, { isPDF: false }).then(\n        searchResult => {\n          expect(searchResult.audio && typeof searchResult.audio.uk).toBe(\n            'string'\n          )\n          expect(searchResult.audio && typeof searchResult.audio.us).toBe(\n            'string'\n          )\n\n          const result = searchResult.result as LongmanResultLex\n          expect(result.type).toBe('lex')\n\n          expect(result.bussinessFirst).toBe(false)\n          expect((result.wordfams as string).length).toBeGreaterThan(0)\n          expect(result.contemporary).toHaveLength(2)\n          expect(result.bussiness).toHaveLength(2)\n\n          result.contemporary.forEach(entry => {\n            expect(entry.title.HWD.length).toBeGreaterThan(0)\n            expect(entry.title.HYPHENATION.length).toBeGreaterThan(0)\n            expect(entry.title.HOMNUM.length).toBeGreaterThan(0)\n            expect(entry.senses.length).toBeGreaterThan(0)\n            expect(typeof entry.phsym).toBe('string')\n            expect(typeof entry.pos).toBe('string')\n            expect(entry.prons).toHaveLength(2)\n          })\n\n          result.bussiness.forEach(entry => {\n            expect(entry.title.HWD.length).toBeGreaterThan(0)\n            expect(entry.title.HYPHENATION.length).toBeGreaterThan(0)\n            expect(entry.title.HOMNUM.length).toBeGreaterThan(0)\n            expect(entry.senses.length).toBeGreaterThan(0)\n            expect(typeof entry.phsym).toBe('string')\n            expect(typeof entry.pos).toBe('string')\n            expect(entry.freq).toHaveLength(0)\n            expect(entry.level).toBeUndefined()\n            expect(entry.prons).toHaveLength(0)\n          })\n\n          expect(result.contemporary[0].level).toBeDefined()\n          expect((result.contemporary[0].level as any).rate).toBe(3)\n          expect(typeof result.contemporary[0].collocations).toBe('string')\n          expect(typeof result.contemporary[0].thesaurus).toBe('string')\n          expect(result.contemporary[0].freq).toHaveLength(2)\n          expect(result.contemporary[0].examples).toHaveLength(2)\n          expect(typeof result.contemporary[0].topic).toBeTruthy()\n\n          expect(result.contemporary[1].freq).toHaveLength(0)\n          expect(result.contemporary[1].examples).toHaveLength(3)\n          expect(result.contemporary[1].level).toBeDefined()\n          expect((result.contemporary[1].level as any).rate).toBe(1)\n        }\n      )\n    )\n  })\n\n  it('should parse related result correctly', () => {\n    return retry(() =>\n      search('jumblish', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(searchResult => {\n        expect(searchResult.audio).toBeUndefined()\n\n        const result = searchResult.result as LongmanResultRelated\n        expect(result.type).toBe('related')\n        expect(typeof result.list).toBe('string')\n      })\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/longman/fixtures.js",
    "content": "module.exports = {\n  files: [\n    ['jumblish.html', 'http://www.ldoceonline.com/dictionary/jumblish'],\n    ['love.html', 'http://www.ldoceonline.com/dictionary/love'],\n    ['profit.html', 'http://www.ldoceonline.com/dictionary/profit']\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/longman/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['love', 'profit', 'jumblish']\n\nexport const mockRequest: MockRequest = mock => {\n  mock.onGet(/ldoceonline/).reply(info => {\n    const wordMatch = /[^/]+$/.exec(info.url || '')\n    return wordMatch\n      ? [\n          200,\n          require(`raw-loader!./response/${decodeURIComponent(\n            wordMatch[0]\n          )}.html`).default\n        ]\n      : [404]\n  })\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/macmillan/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport {\n  search,\n  MacmillanResultLex,\n  MacmillanResultRelated\n} from '@/components/dictionaries/macmillan/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile } from '@/app-config/profiles'\n\ndescribe('Dict/Macmillan/engine', () => {\n  it('should parse lex result correctly', () => {\n    return retry(() =>\n      search('love', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(searchResult => {\n        expect(searchResult.audio && typeof searchResult.audio.uk).toBe(\n          'string'\n        )\n\n        const result = searchResult.result as MacmillanResultLex\n        expect(result.type).toBe('lex')\n        expect(typeof result.title).toBe('string')\n        expect(typeof result.senses).toBe('string')\n        expect(typeof result.pos).toBe('string')\n        expect(typeof result.sc).toBe('string')\n        expect(typeof result.phsym).toBe('string')\n        expect(typeof result.pron).toBe('string')\n        expect(typeof result.ratting).toBe('number')\n      })\n    )\n  })\n\n  it('should parse related result correctly', () => {\n    return retry(() =>\n      search('jumblish', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(searchResult => {\n        expect(searchResult.audio).toBeUndefined()\n\n        const result = searchResult.result as MacmillanResultRelated\n        expect(result.type).toBe('related')\n        expect(typeof result.list).toBe('string')\n      })\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/macmillan/fixtures.js",
    "content": "module.exports = {\n  files: [\n    [\n      'jumblish.html',\n      'http://www.macmillandictionary.com/dictionary/british/jumblish'\n    ],\n    ['love.html', 'http://www.macmillandictionary.com/dictionary/british/love'],\n    [\n      'love_2.html',\n      'http://www.macmillandictionary.com/dictionary/british/love_2'\n    ],\n    [\n      'viral.html',\n      'http://www.macmillandictionary.com/dictionary/british/viral'\n    ]\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/macmillan/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['love', 'viral', 'jumblish']\n\nexport const mockRequest: MockRequest = mock => {\n  mock.onGet(/macmillan/).reply(info => {\n    const wordMatch = /[^/]+$/.exec(info.url || '')\n    return wordMatch\n      ? [\n          200,\n          require(`raw-loader!./response/${decodeURIComponent(\n            wordMatch[0]\n          )}.html`).default\n        ]\n      : [404]\n  })\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/merriamwebster/engine.spec.ts",
    "content": "import {\n  _getConjugation,\n  _getContentEle,\n  _getEtymology,\n  _getExamples,\n  _getExpaining,\n  _getGroupsEles,\n  _getMeaningEles,\n  _getMeaningGroupEles,\n  _getPartOfSpeech,\n  _getPhoneticAudio,\n  _getPhoneticEles,\n  _getPhoneticSymbol,\n  _getPrEle,\n  _getSectionTitle,\n  _getSectionsEles,\n  _getSyllable,\n  _getSynonyms,\n  _getTitle\n} from '@/components/dictionaries/merriamwebster/engine'\nimport { cases } from './testCases'\n// import getDefaultProfile from '@/app-config/profiles'\n\ndescribe('Dict/MerriamWebster/engine', () => {\n  // const profile = getDefaultProfile()\n  // const options = profile.dicts.all.merriamwebster.options\n\n  let multiGroup: Element\n  let multiSyllable: Element\n\n  beforeAll(() => {\n    multiGroup = _getContentEle(cases.multiGroup.dom())\n    multiSyllable = _getContentEle(cases.multiSyllable.dom())\n  })\n\n  it('should return right number of groups', () => {\n    const groupEles1 = _getGroupsEles(multiGroup)\n    expect(groupEles1.length).toBe(2)\n\n    const groupEles2 = _getGroupsEles(multiSyllable)\n    expect(groupEles2.length).toBe(1)\n  })\n\n  it('should returns correct synonyms', () => {\n    const synonyms1 = _getSynonyms(multiGroup)\n    expect(synonyms1).toStrictEqual(cases.multiGroup.expect.synonyms)\n\n    const synonyms2 = _getSynonyms(multiSyllable)\n    expect(synonyms2).toStrictEqual(cases.multiSyllable.expect.synonyms)\n  })\n\n  it('should returns correct etymology', () => {\n    const etymology1 = _getEtymology(multiGroup)\n    expect(etymology1).toStrictEqual(cases.multiGroup.expect.etymology)\n\n    const etymology2 = _getEtymology(multiSyllable)\n    expect(etymology2).toStrictEqual(cases.multiSyllable.expect.etymology)\n  })\n\n  it('should returns correct group title', () => {\n    const groupEles1 = _getGroupsEles(multiGroup)\n    const gexp1 = cases.multiGroup.expect\n\n    groupEles1.forEach((v, i) => {\n      const title = _getTitle(v)\n      expect(title).toEqual(gexp1.groups[i].title)\n    })\n\n    const groupEles2 = _getGroupsEles(multiSyllable)\n    const gexp2 = cases.multiSyllable.expect\n\n    groupEles2.forEach((v, i) => {\n      const title = _getTitle(v)\n      expect(title).toEqual(gexp2.groups[i].title)\n    })\n  })\n\n  it('should returns correct part of speech', () => {\n    const groupEles1 = _getGroupsEles(multiGroup)\n    const gexp1 = cases.multiGroup.expect\n\n    groupEles1.forEach((v, i) => {\n      const pos = _getPartOfSpeech(v)\n      expect(pos).toEqual(gexp1.groups[i].pos)\n    })\n\n    const groupEles2 = _getGroupsEles(multiSyllable)\n    const gexp2 = cases.multiSyllable.expect\n\n    groupEles2.forEach((v, i) => {\n      const pos = _getPartOfSpeech(v)\n      expect(pos).toEqual(gexp2.groups[i].pos)\n    })\n  })\n\n  it('should returns correct conjugation', () => {\n    const groupEles1 = _getGroupsEles(multiGroup)\n    const gexp1 = cases.multiGroup.expect\n\n    groupEles1.forEach((v, i) => {\n      const conj = _getConjugation(v)\n      expect(conj).toEqual(gexp1.groups[i].conjugation)\n    })\n\n    const groupEles2 = _getGroupsEles(multiSyllable)\n    const gexp2 = cases.multiSyllable.expect\n\n    groupEles2.forEach((v, i) => {\n      const conj = _getConjugation(v)\n      expect(conj).toEqual(gexp2.groups[i].conjugation)\n    })\n  })\n\n  it.todo('should returns correct forms')\n\n  it('should returns correct syllable', () => {\n    const groupEles1 = _getGroupsEles(multiGroup)\n    const gexp1 = cases.multiGroup.expect\n\n    groupEles1.forEach((v, i) => {\n      const pEles = _getPrEle(v)\n      if (pEles) {\n        const syllable = _getSyllable(pEles)\n        expect(syllable).toEqual(gexp1.groups[i].pr?.syllable)\n      }\n    })\n\n    const groupEles2 = _getGroupsEles(multiSyllable)\n    const gexp2 = cases.multiSyllable.expect\n\n    groupEles2.forEach((v, i) => {\n      const pEles = _getPrEle(v)\n      if (pEles) {\n        const syllable = _getSyllable(pEles)\n        expect(syllable).toEqual(gexp2.groups[i].pr?.syllable)\n      }\n    })\n  })\n\n  it('should returns right number of phonetics', () => {\n    const groupEles1 = _getGroupsEles(multiGroup)\n    const gexp1 = cases.multiGroup.expect\n\n    groupEles1.forEach((v, i) => {\n      const pEles = _getPrEle(v)\n      if (pEles) {\n        const pts = _getPhoneticEles(pEles)\n        if (pts) {\n          expect(pts.length).toEqual(gexp1.groups[i].pr?.phonetics.length)\n        }\n      }\n    })\n\n    const groupEles2 = _getGroupsEles(multiSyllable)\n    const gexp2 = cases.multiSyllable.expect\n\n    groupEles2.forEach((v, i) => {\n      const pEles = _getPrEle(v)\n      if (pEles) {\n        const pts = _getPhoneticEles(pEles)\n        if (pts) {\n          expect(pts.length).toEqual(gexp2.groups[i].pr?.phonetics.length)\n        }\n      }\n    })\n  })\n\n  it('should returns correct phonetic symbol', () => {\n    const groupEles1 = _getGroupsEles(multiGroup)\n    const gexp1 = cases.multiGroup.expect\n\n    groupEles1.forEach((v, i) => {\n      const pEles = _getPrEle(v)\n      if (pEles) {\n        const pts = _getPhoneticEles(pEles)\n        if (pts) {\n          pts.forEach((v, j) => {\n            expect(_getPhoneticSymbol(v)).toEqual(\n              gexp1.groups[i].pr?.phonetics[j].symbol\n            )\n          })\n        }\n      }\n    })\n\n    const groupEles2 = _getGroupsEles(multiSyllable)\n    const gexp2 = cases.multiSyllable.expect\n\n    groupEles2.forEach((v, i) => {\n      const pEles = _getPrEle(v)\n      if (pEles) {\n        const pts = _getPhoneticEles(pEles)\n        if (pts) {\n          pts.forEach((v, j) => {\n            expect(_getPhoneticSymbol(v)).toEqual(\n              gexp2.groups[i].pr?.phonetics[j].symbol\n            )\n          })\n        }\n      }\n    })\n  })\n\n  it('should returns correct phonetic audio', () => {\n    const groupEles1 = _getGroupsEles(multiGroup)\n    const gexp1 = cases.multiGroup.expect\n\n    groupEles1.forEach((v, i) => {\n      const pEles = _getPrEle(v)\n      if (pEles) {\n        const pts = _getPhoneticEles(pEles)\n        if (pts) {\n          pts.forEach((v, j) => {\n            expect(_getPhoneticAudio(v)).toEqual(\n              gexp1.groups[i].pr?.phonetics[j].audio\n            )\n          })\n        }\n      }\n    })\n\n    const groupEles2 = _getGroupsEles(multiSyllable)\n    const gexp2 = cases.multiSyllable.expect\n\n    groupEles2.forEach((v, i) => {\n      const pEles = _getPrEle(v)\n      if (pEles) {\n        const pts = _getPhoneticEles(pEles)\n        if (pts) {\n          pts.forEach((v, j) => {\n            expect(_getPhoneticAudio(v)).toEqual(\n              gexp2.groups[i].pr?.phonetics[j].audio\n            )\n          })\n        }\n      }\n    })\n  })\n\n  it('should returns right number of sections', () => {\n    const groupEles1 = _getGroupsEles(multiGroup)\n    const gexp1 = cases.multiGroup.expect\n\n    groupEles1.forEach((v, i) => {\n      const sEles = _getSectionsEles(v)\n      if (sEles) {\n        expect(sEles.length).toEqual(gexp1.groups[i].sections?.length)\n      }\n    })\n\n    const groupEles2 = _getGroupsEles(multiSyllable)\n    const gexp2 = cases.multiSyllable.expect\n\n    groupEles2.forEach((v, i) => {\n      const sEles = _getSectionsEles(v)\n      if (sEles) {\n        expect(sEles.length).toEqual(gexp2.groups[i].sections?.length)\n      }\n    })\n  })\n\n  it('should returns correct section title', () => {\n    const groupEles1 = _getGroupsEles(multiGroup)\n    const gexp1 = cases.multiGroup.expect\n\n    groupEles1.forEach((v1, i) => {\n      const sEles = _getSectionsEles(v1)\n      if (sEles) {\n        sEles.forEach((v2, j) => {\n          expect(_getSectionTitle(v2)).toEqual(\n            gexp1.groups[i].sections[j].title\n          )\n        })\n      }\n    })\n\n    const groupEles2 = _getGroupsEles(multiSyllable)\n    const gexp2 = cases.multiSyllable.expect\n\n    groupEles2.forEach((v1, i) => {\n      const sEles = _getSectionsEles(v1)\n      if (sEles) {\n        sEles.forEach((v2, j) => {\n          expect(_getSectionTitle(v2)).toEqual(\n            gexp2.groups[i].sections[j].title\n          )\n        })\n      }\n    })\n  })\n\n  it('should returns right number of meaning group elements', () => {\n    const groupEles1 = _getGroupsEles(multiGroup)\n    const gexp1 = cases.multiGroup.expect\n\n    groupEles1.forEach((v1, i) => {\n      const sEles = _getSectionsEles(v1)\n      if (sEles) {\n        sEles.forEach((v2, j) => {\n          expect(_getMeaningGroupEles(v2)?.length).toEqual(\n            gexp1.groups[i].sections[j].meaningGroups.length\n          )\n        })\n      }\n    })\n\n    const groupEles2 = _getGroupsEles(multiSyllable)\n    const gexp2 = cases.multiSyllable.expect\n\n    groupEles2.forEach((v1, i) => {\n      const sEles = _getSectionsEles(v1)\n      if (sEles) {\n        sEles.forEach((v2, j) => {\n          expect(_getMeaningGroupEles(v2)?.length).toEqual(\n            gexp2.groups[i].sections[j].meaningGroups.length\n          )\n        })\n      }\n    })\n  })\n\n  it('should returns right number of meaning elements', () => {\n    const groupEles1 = _getGroupsEles(multiGroup)\n    const gexp1 = cases.multiGroup.expect\n\n    groupEles1.forEach((v1, i) => {\n      const sEles = _getSectionsEles(v1)\n      if (sEles) {\n        sEles.forEach((v2, j) => {\n          const mg = _getMeaningGroupEles(v2)\n          if (mg)\n            mg.forEach((v3, n) => {\n              expect(_getMeaningEles(v3)?.length).toEqual(\n                gexp1.groups[i].sections[j].meaningGroups[n].length\n              )\n            })\n        })\n      }\n    })\n\n    const groupEles2 = _getGroupsEles(multiSyllable)\n    const gexp2 = cases.multiSyllable.expect\n\n    groupEles2.forEach((v1, i) => {\n      const sEles = _getSectionsEles(v1)\n      if (sEles) {\n        sEles.forEach((v2, j) => {\n          const mg = _getMeaningGroupEles(v2)\n          if (mg)\n            mg.forEach((v3, n) => {\n              expect(_getMeaningEles(v3)?.length).toEqual(\n                gexp2.groups[i].sections[j].meaningGroups[n].length\n              )\n            })\n        })\n      }\n    })\n  })\n\n  it('should returns correct explaining', () => {\n    const groupEles1 = _getGroupsEles(multiGroup)\n    const gexp1 = cases.multiGroup.expect\n\n    groupEles1.forEach((v1, i) => {\n      const sEles = _getSectionsEles(v1)\n      if (sEles) {\n        sEles.forEach((v2, j) => {\n          const mg = _getMeaningGroupEles(v2)\n          if (mg)\n            mg.forEach((v3, n) => {\n              const ms = _getMeaningEles(v3)\n              if (ms)\n                ms.forEach((v4, o) => {\n                  expect(_getExpaining(v4)).toEqual(\n                    gexp1.groups[i].sections[j].meaningGroups[n][o].explaining\n                  )\n                })\n            })\n        })\n      }\n    })\n\n    const groupEles2 = _getGroupsEles(multiSyllable)\n    const gexp2 = cases.multiSyllable.expect\n\n    groupEles2.forEach((v1, i) => {\n      const sEles = _getSectionsEles(v1)\n      if (sEles) {\n        sEles.forEach((v2, j) => {\n          const mg = _getMeaningGroupEles(v2)\n          if (mg)\n            mg.forEach((v3, n) => {\n              const ms = _getMeaningEles(v3)\n              if (ms)\n                ms.forEach((v4, o) => {\n                  expect(_getExpaining(v4)).toEqual(\n                    gexp2.groups[i].sections[j].meaningGroups[n][o].explaining\n                  )\n                })\n            })\n        })\n      }\n    })\n  })\n\n  it('should returns correct explaining', () => {\n    const groupEles1 = _getGroupsEles(multiGroup)\n    const gexp1 = cases.multiGroup.expect\n\n    groupEles1.forEach((v1, i) => {\n      const sEles = _getSectionsEles(v1)\n      if (sEles) {\n        sEles.forEach((v2, j) => {\n          const mg = _getMeaningGroupEles(v2)\n          if (mg)\n            mg.forEach((v3, n) => {\n              const ms = _getMeaningEles(v3)\n              if (ms)\n                ms.forEach((v4, o) => {\n                  console.log(`i ${i} j ${j} n ${n} o ${o}`)\n                  expect(_getExamples(v4)).toStrictEqual(\n                    gexp1.groups[i].sections[j].meaningGroups[n][o].examples\n                  )\n                })\n            })\n        })\n      }\n    })\n\n    const groupEles2 = _getGroupsEles(multiSyllable)\n    const gexp2 = cases.multiSyllable.expect\n\n    groupEles2.forEach((v1, i) => {\n      const sEles = _getSectionsEles(v1)\n      if (sEles) {\n        sEles.forEach((v2, j) => {\n          const mg = _getMeaningGroupEles(v2)\n          if (mg)\n            mg.forEach((v3, n) => {\n              const ms = _getMeaningEles(v3)\n              if (ms)\n                ms.forEach((v4, o) => {\n                  expect(_getExamples(v4)).toStrictEqual(\n                    gexp2.groups[i].sections[j].meaningGroups[n][o].examples\n                  )\n                })\n            })\n        })\n      }\n    })\n  })\n\n  // it('should return correct result', () => {\n  //   expect(getResult(cases.multiGroup.dom)).toStrictEqual(\n  //     cases.multiGroup.expect\n  //   )\n  //   expect(getResult(cases.multiSyllable.dom)).toStrictEqual(\n  //     cases.multiSyllable.expect\n  //   )\n  // })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/merriamwebster/fixtures.js",
    "content": "module.exports = {\n  files: [\n    ['love.html', 'https://www.merriam-webster.com/dictionary/love'],\n    ['text.html', 'https://www.merriam-webster.com/dictionary/text'],\n    ['salad.html', 'https://www.merriam-webster.com/dictionary/salad'],\n    ['add.html', 'https://www.merriam-webster.com/dictionary/add'],\n    ['transitive.html', 'https://www.merriam-webster.com/dictionary/transitive']\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/merriamwebster/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['love', 'text', 'salad']\n\nexport const mockRequest: MockRequest = mock => {\n  mock\n    .onGet(/merriam-webster.+love$/)\n    .reply(200, require(`raw-loader!./response/love.html`).default)\n\n  mock\n    .onGet(/merriam-webster.+text$/)\n    .reply(200, require(`raw-loader!./response/text.html`).default)\n\n  mock\n    .onGet(/merriam-webster.+salad$/)\n    .reply(200, require(`raw-loader!./response/salad.html`).default)\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/merriamwebster/testCases.ts",
    "content": "import { MerriamWebsterResultV2 } from '@/components/dictionaries/merriamwebster/engine'\nimport fs from 'fs'\nimport path from 'path'\n\nexport const cases = {\n  multiGroup: {\n    dom: () => {\n      const data = fs.readFileSync(path.join(__dirname, '/response/add.html'), {\n        encoding: 'utf-8'\n      })\n      return new DOMParser().parseFromString(data, 'text/html')\n    },\n    expect: {\n      groups: [\n        {\n          title: 'add',\n          pos: 'verb',\n          pr: {\n            phonetics: [\n              {\n                symbol: 'ˈad',\n                audio:\n                  'https://media.merriam-webster.com/audio/prons/en/us/mp3/a/add00001.mp3'\n              }\n            ]\n          },\n          conjugation: 'added; adding; adds',\n          sections: [\n            {\n              title: 'transitive verb',\n              meaningGroups: [\n                [\n                  {\n                    explaining:\n                      ': to join or unite so as to bring about an increase or improvement',\n                    examples: [\n                      'adds 60 acres to his land',\n                      'wine adds a creative touch to cooking'\n                    ]\n                  }\n                ],\n                [\n                  {\n                    explaining: ': to say further : append',\n                    examples: [\n                      'Do you have anything else to add to the discussion?'\n                    ]\n                  }\n                ],\n                [\n                  {\n                    explaining:\n                      ': to combine (numbers) into an equivalent simple quantity or number'\n                  }\n                ],\n                [\n                  {\n                    explaining: ': to include as a member of a group',\n                    examples: [`Don't forget to add me in.`]\n                  }\n                ]\n              ]\n            },\n            {\n              title: 'intransitive verb',\n              meaningGroups: [\n                [\n                  {\n                    explaining: ': to perform addition'\n                  },\n                  {\n                    explaining: ': to come together or unite by addition',\n                    examples: [\n                      'The facts added together to support his theory.'\n                    ]\n                  }\n                ],\n                [\n                  {\n                    explaining: ': to serve as an addition',\n                    examples: ['The movie will add to his fame.']\n                  },\n                  {\n                    explaining: ': to make an addition',\n                    examples: ['added to her savings']\n                  }\n                ]\n              ]\n            }\n          ],\n          forms: ['addable[adjective]']\n        },\n        {\n          title: 'ADD',\n          pos: 'abbreviation',\n          sections: [\n            {\n              meaningGroups: [\n                [\n                  {\n                    explaining: 'American Dialect Dictionary'\n                  }\n                ],\n                [\n                  {\n                    explaining: 'attention deficit disorder'\n                  }\n                ]\n              ]\n            }\n          ]\n        }\n      ],\n      synonyms: [\n        ['Verb', ['adjoin', 'annex', 'append', 'subjoin', 'tack (on)']]\n      ],\n      etymology: [\n        [\n          'Verb',\n          `Middle English adden, borrowed from Anglo-French adder, borrowed from Latin addere, from ad- ad- + -dere \\\"to put, place,\\\" going back to a reduced ablaut grade of Indo-European *dheh1-  — more at do entry 1`\n        ]\n      ]\n    } as MerriamWebsterResultV2\n  },\n  multiSyllable: {\n    dom: () => {\n      const data = fs.readFileSync(\n        path.join(__dirname, '/response/transitive.html'),\n        {\n          encoding: 'utf-8'\n        }\n      )\n      return new DOMParser().parseFromString(data, 'text/html')\n    },\n    expect: {\n      groups: [\n        {\n          title: 'transitive',\n          pos: 'adjective',\n          pr: {\n            syllable: 'tran·​si·​tive',\n            phonetics: [\n              {\n                symbol: 'ˈtran(t)-sə-tiv',\n                audio:\n                  'https://media.merriam-webster.com/audio/prons/en/us/mp3/t/transi17.mp3'\n              },\n              {\n                symbol: 'ˈtran-zə-'\n              },\n              {\n                symbol: 'ˈtran(t)s-tiv'\n              }\n            ]\n          },\n          sections: [\n            {\n              meaningGroups: [\n                [\n                  {\n                    explaining:\n                      ': characterized by having or containing a direct object',\n                    examples: ['a transitive verb']\n                  }\n                ],\n                [\n                  {\n                    explaining:\n                      ': being or relating to a relation with the property that if the relation holds between a first element and a second and between the second element and a third, it holds between the first and third elements',\n                    examples: ['equality is a transitive relation']\n                  }\n                ],\n                [\n                  {\n                    explaining:\n                      ': of, relating to, or characterized by transition'\n                  }\n                ]\n              ]\n            }\n          ],\n          forms: [\n            'transitively[adverb]',\n            'transitiveness[noun]',\n            'transitivity[noun]'\n          ]\n        }\n      ],\n      etymology: [\n        [\n          '',\n          'Late Latin transitivus, from Latin transitus, past participle of transire'\n        ]\n      ]\n    } as MerriamWebsterResultV2\n  }\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/mojidict/fixtures.js",
    "content": "const path = require('path')\nconst env = require('dotenv').config({\n  path: path.join(__dirname, '../../../../../.env')\n}).parsed\n\nmodule.exports = {\n  files: [\n    [\n      '心/search.json',\n      () => ({\n        method: 'post',\n        url: 'https://api.mojidict.com/parse/functions/search_v3',\n        headers: {\n          'content-type': 'text/plain'\n        },\n        data: JSON.stringify({\n          langEnv: 'zh-CN_ja',\n          needWords: true,\n          searchText: '心',\n          _ApplicationId: env.MOJI_ID,\n          _ClientVersion: 'js2.7.1',\n          _InstallationId: getInstallationId()\n        })\n      })\n    ],\n    [\n      '心/fetchWord.json',\n      ([searchResult]) => ({\n        method: 'post',\n        url: 'https://api.mojidict.com/parse/functions/fetchWord_v2',\n        headers: {\n          'content-type': 'text/plain'\n        },\n        data: JSON.stringify({\n          wordId: JSON.parse(searchResult).result.searchResults[0].tarId,\n          _ApplicationId: env.MOJI_ID,\n          _ClientVersion: 'js2.7.1',\n          _InstallationId: getInstallationId()\n        })\n      })\n    ],\n    [\n      '心/fetchTts.json',\n      ([searchResult, fetchWordResult]) => {\n        const word = JSON.parse(fetchWordResult).result.word\n        return {\n          method: 'post',\n          url: 'https://api.mojidict.com/parse/functions/fetchTts',\n          headers: {\n            'content-type': 'text/plain'\n          },\n          data: JSON.stringify({\n            identity: word.objectId,\n            text: word.spell,\n            _ApplicationId: env.MOJI_ID,\n            _ClientVersion: 'js2.7.1',\n            _InstallationId: getInstallationId()\n          })\n        }\n      }\n    ]\n  ]\n}\n\nfunction getInstallationId() {\n  return s() + s() + '-' + s() + '-' + s() + '-' + s() + '-' + s() + s() + s()\n}\n\nfunction s() {\n  return Math.floor(65536 * (1 + Math.random()))\n    .toString(16)\n    .substring(1)\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/mojidict/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['心']\n\nexport const mockRequest: MockRequest = mock => {\n  mock\n    .onPost(/mojidict.*fetchWord/)\n    .reply(200, require(`./response/心/fetchWord.json`))\n    .onPost(/mojidict.*search/)\n    .reply(200, require(`./response/心/search.json`))\n    .onPost(/mojidict.*fetchTts/)\n    .reply(200, require(`./response/心/fetchTts.json`))\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/naver/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport { search } from '@/components/dictionaries/naver/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile, ProfileMutable } from '@/app-config/profiles'\n\ndescribe('Dict/Naver/engine', () => {\n  it('should search zh dict', () => {\n    return retry(() =>\n      search('爱', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(searchResult => {\n        expect(searchResult.result.lang).toBe('zh')\n        expect(typeof searchResult.result.entry).toBe('object')\n      })\n    )\n  })\n\n  it('should search ja dict', () => {\n    const profile = getDefaultProfile() as ProfileMutable\n    profile.dicts.all.naver.options.hanAsJa = true\n    return retry(() =>\n      search('愛', getDefaultConfig(), profile, { isPDF: false }).then(\n        searchResult => {\n          expect(searchResult.result.lang).toBe('ja')\n          expect(typeof searchResult.result.entry).toBe('object')\n        }\n      )\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/naver/fixtures.js",
    "content": "module.exports = {\n  files: [\n    [\n      '愛.json',\n      'https://ja.dict.naver.com/api3/jako/search?query=' +\n        encodeURIComponent('愛')\n    ],\n    [\n      '爱.json',\n      `https://zh.dict.naver.com/api3/zhko/search?query=${encodeURIComponent(\n        '爱'\n      )}&lang=zh_CN`\n    ]\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/naver/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['爱', '愛']\n\nexport const mockRequest: MockRequest = mock => {\n  mock\n    .onGet(new RegExp('naver.+' + encodeURIComponent('爱')))\n    .reply(200, require(`./response/爱.json`))\n\n  mock\n    .onGet(new RegExp('naver.+' + encodeURIComponent('愛')))\n    .reply(200, require(`./response/愛.json`))\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/oaldict/fixtures.js",
    "content": "module.exports = {\n  files: [\n    [\n      'comment.html',\n      'https://www.oxfordlearnersdictionaries.com/search/english/direct/?q=comment'\n    ],\n    [\n      'love.html',\n      'https://www.oxfordlearnersdictionaries.com/search/english/direct/?q=love'\n    ],\n    [\n      'salad.html',\n      'https://www.oxfordlearnersdictionaries.com/search/english/direct/?q=salad'\n    ]\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/oaldict/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['comment', 'love', 'salad']\n\nexport const mockRequest: MockRequest = mock => {\n  mock\n    .onGet(/oxfordlearnersdictionaries.+comment$/)\n    .reply(200, require('!raw-loader!./response/comment.html').default)\n\n  mock\n    .onGet(/oxfordlearnersdictionaries.+love$/)\n    .reply(200, require('!raw-loader!./response/love.html').default)\n\n  mock\n    .onGet(/oxfordlearnersdictionaries.+salad$/)\n    .reply(200, require('!raw-loader!./response/salad.html').default)\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/renren/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport { search } from '@/components/dictionaries/renren/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile } from '@/app-config/profiles'\n\ndescribe('Dict/Renren/engine', () => {\n  it('should parse result correctly', () => {\n    return retry(() =>\n      search('love', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(searchResult => {})\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/renren/fixtures.js",
    "content": "module.exports = {\n  files: [\n    ['love.html', 'https://www.91dict.com/words?w=love'],\n    [\n      'detail.html',\n      ([page]) => {\n        const detailMatch = /\\/r_subs\\?sub_id=[^\"']+/.exec(page)\n        if (detailMatch) {\n          return {\n            url: 'https://www.91dict.com' + detailMatch\n          }\n        }\n      }\n    ]\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/renren/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['love']\n\nexport const mockRequest: MockRequest = mock => {\n  mock\n    .onGet(/91dict.+r_subs/)\n    .reply(200, require(`raw-loader!./response/detail.html`).default)\n\n  mock\n    .onGet(/91dict/)\n    .reply(200, require(`raw-loader!./response/love.html`).default)\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/shanbay/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport { search } from '@/components/dictionaries/shanbay/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile } from '@/app-config/profiles'\nimport { isContainChinese } from '@/_helpers/lang-check'\nimport { browser } from '../../../../helper'\n\ndescribe('Dict/Shanbay/engine', () => {\n  beforeEach(() => {\n    browser.storage.local.get.callsFake(() => Promise.resolve({}))\n    browser.storage.local.set.callsFake(() => Promise.resolve())\n  })\n\n  it('should parse result correctly', () => {\n    return retry(() =>\n      search('hello', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(searchResult => {\n        expect(searchResult.result.title).toBe('hello')\n        expect(typeof (searchResult.audio || {}).us).toBe('string')\n        expect(searchResult.result.id).toBe('shanbay')\n        expect(isContainChinese(searchResult.result.basic || '')).toBeTruthy()\n        expect(searchResult.result.sentences.length).toBeGreaterThanOrEqual(1)\n        expect(searchResult.result.prons.length).toBeGreaterThanOrEqual(1)\n        expect(searchResult.result.prons[0].url).toEqual(\n          (searchResult.audio || {}).us\n        )\n      })\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/shanbay/fixtures.js",
    "content": "module.exports = {\n  files: [\n    ['love.html', 'https://www.shanbay.com/bdc/mobile/preview/word?word=love'],\n    [\n      'love.json',\n      ([page]) => {\n        const wordIdMatch = /\"word-spell\" data-id=\"([^\"]+)\"/.exec(page)\n        if (wordIdMatch) {\n          return {\n            url: `https://www.shanbay.com/api/v1/bdc/example/?vocabulary_id=${\n              wordIdMatch[1]\n            }&type=sys`\n          }\n        }\n      }\n    ]\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/shanbay/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['love']\n\nexport const mockRequest: MockRequest = mock => {\n  mock.onGet(/shanbay/).reply(info => {\n    return /mobile/.test(info.url || '')\n      ? [200, require(`raw-loader!./response/love.html`).default]\n      : [200, require(`./response/love.json`)]\n  })\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/urban/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport { search } from '@/components/dictionaries/urban/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile } from '@/app-config/profiles'\n\ndescribe('Dict/Urban/engine', () => {\n  it('should parse result correctly', () => {\n    return retry(() =>\n      search('love', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(searchResult => {\n        expect(searchResult.audio && typeof searchResult.audio.us).toBe(\n          'string'\n        )\n        expect(searchResult.result.length).toBeGreaterThan(0)\n        const item = searchResult.result[0]\n        expect(typeof item.title).toBe('string')\n        expect(typeof item.pron).toBe('string')\n        expect(typeof item.meaning).toBe('string')\n        expect(typeof item.example).toBe('string')\n        expect(typeof item.contributor).toBe('string')\n        expect(typeof item.thumbsUp).toBe('string')\n        expect(typeof item.thumbsDown).toBe('string')\n      })\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/urban/fixtures.js",
    "content": "module.exports = {\n  files: [['love.html', 'http://www.urbandictionary.com/define.php?term=love']]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/urban/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['love']\n\nexport const mockRequest: MockRequest = mock => {\n  mock\n    .onGet(/urbandictionary/)\n    .reply(200, require(`raw-loader!./response/love.html`).default)\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/vocabulary/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport { search } from '@/components/dictionaries/vocabulary/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile } from '@/app-config/profiles'\n\ndescribe('Dict/Vocabulary/engine', () => {\n  it('should parse result correctly', () => {\n    return retry(() =>\n      search('love', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(({ result, audio }) => {\n        expect(audio).toBeUndefined()\n        expect(typeof result.long).toBe('string')\n        expect(typeof result.short).toBe('string')\n      })\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/vocabulary/fixtures.js",
    "content": "module.exports = {\n  files: [['love.html', 'https://www.vocabulary.com/dictionary/love']]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/vocabulary/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['love']\n\nexport const mockRequest: MockRequest = mock => {\n  mock\n    .onGet(/vocabulary/)\n    .reply(200, require(`raw-loader!./response/love.html`).default)\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/weblio/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport { search } from '@/components/dictionaries/weblio/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile } from '@/app-config/profiles'\n\ndescribe('Dict/Weblio/engine', () => {\n  ;['love', '吐く', '当たる'].forEach(text => {\n    it(`should parse result ${text} correctly`, () => {\n      return retry(() =>\n        search(text, getDefaultConfig(), getDefaultProfile(), {\n          isPDF: false\n        }).then(({ result }) => {\n          expect(result.length).toBeGreaterThanOrEqual(1)\n          expect(typeof result[0].title).toBe('string')\n          expect(typeof result[0].def).toBe('string')\n        })\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/weblio/fixtures.js",
    "content": "module.exports = {\n  files: [\n    [\n      '主催.html',\n      'https://www.weblio.jp/content/' + encodeURIComponent('主催')\n    ],\n    ['love.html', 'https://www.weblio.jp/content/love'],\n    [\n      '吐く.html',\n      'https://www.weblio.jp/content/' + encodeURIComponent('吐く')\n    ],\n    [\n      '当たる.html',\n      'https://www.weblio.jp/content/' + encodeURIComponent('当たる')\n    ]\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/weblio/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['主催', 'love', '吐く', '当たる']\n\nexport const mockRequest: MockRequest = mock => {\n  mock\n    .onGet(/www\\.weblio\\.jp.+love/)\n    .reply(200, require(`raw-loader!./response/love.html`).default)\n\n  mock\n    .onGet(new RegExp('www\\\\.weblio\\\\.jp.+' + encodeURIComponent('吐く')))\n    .reply(200, require(`raw-loader!./response/吐く.html`).default)\n\n  mock\n    .onGet(new RegExp('www\\\\.weblio\\\\.jp.+' + encodeURIComponent('当たる')))\n    .reply(200, require(`raw-loader!./response/当たる.html`).default)\n\n  mock\n    .onGet(new RegExp('www\\\\.weblio\\\\.jp.+' + encodeURIComponent('主催')))\n    .reply(200, require(`raw-loader!./response/主催.html`).default)\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/weblioejje/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport { search } from '@/components/dictionaries/weblioejje/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile } from '@/app-config/profiles'\n\ndescribe('Dict/Weblioejje/engine', () => {\n  ;['love', '愛'].forEach(text => {\n    it(`should parse result ${text} correctly`, () => {\n      return retry(() =>\n        search(text, getDefaultConfig(), getDefaultProfile(), {\n          isPDF: false\n        }).then(({ result }) => {\n          expect(result.length).toBeGreaterThanOrEqual(1)\n          for (const { content } of result) {\n            expect(typeof content).toBe('string')\n            expect(content.length).toBeGreaterThan(1)\n          }\n        })\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/weblioejje/fixtures.js",
    "content": "module.exports = {\n  files: [\n    ['love.html', 'https://ejje.weblio.jp/content/love'],\n    ['愛.html', 'https://ejje.weblio.jp/content/' + encodeURIComponent('愛')]\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/weblioejje/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['love', '愛']\n\nexport const mockRequest: MockRequest = mock => {\n  mock\n    .onGet(/ejje\\.weblio\\.jp.+love/)\n    .reply(200, require(`raw-loader!./response/love.html`).default)\n\n  mock\n    .onGet(new RegExp('ejje\\\\.weblio\\\\.jp.+' + encodeURIComponent('愛')))\n    .reply(200, require(`raw-loader!./response/愛.html`).default)\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/websterlearner/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport {\n  search,\n  WebsterLearnerResultLex\n} from '@/components/dictionaries/websterlearner/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile } from '@/app-config/profiles'\n\ndescribe('Dict/WebsterLearner/engine', () => {\n  it('should parse lex result correctly', () => {\n    return retry(() =>\n      search('house', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(searchResult => {\n        expect(searchResult.audio && typeof searchResult.audio.us).toBe(\n          'string'\n        )\n\n        const result = searchResult.result as WebsterLearnerResultLex\n        expect(result.type).toBe('lex')\n        expect(result.items).toHaveLength(2)\n\n        expect(typeof result.items[0].title).toBe('string')\n        expect(result.items[0].title).toBeTruthy()\n\n        expect(typeof result.items[0].pron).toBe('string')\n        expect(result.items[0].pron).toBeTruthy()\n\n        expect(typeof result.items[0].infs).toBe('string')\n        expect(result.items[0].infs).toBeTruthy()\n\n        expect(typeof result.items[0].infsPron).toBe('string')\n        expect(result.items[0].infsPron).toBeTruthy()\n\n        expect(result.items[0].labels).toBeFalsy()\n\n        expect(typeof result.items[0].senses).toBe('string')\n        expect(result.items[0].senses).toBeTruthy()\n\n        expect(result.items[0].arts).toHaveLength(1)\n\n        expect(typeof result.items[0].phrases).toBe('string')\n        expect(result.items[0].phrases).toBeTruthy()\n\n        expect(typeof result.items[0].derived).toBe('string')\n        expect(result.items[0].derived).toBeTruthy()\n\n        // 2\n        expect(typeof result.items[1].title).toBe('string')\n        expect(result.items[1].title).toBeTruthy()\n\n        expect(typeof result.items[1].pron).toBe('string')\n        expect(result.items[1].pron).toBeTruthy()\n\n        expect(typeof result.items[1].infs).toBe('string')\n        expect(result.items[1].infs).toBeTruthy()\n\n        expect(result.items[1].infsPron).toBeFalsy()\n\n        expect(typeof result.items[1].labels).toBe('string')\n        expect(result.items[1].labels).toBeTruthy()\n\n        expect(typeof result.items[1].senses).toBe('string')\n        expect(result.items[1].senses).toBeTruthy()\n\n        expect(result.items[1].arts).toHaveLength(0)\n\n        expect(result.items[1].phrases).toBeFalsy()\n\n        expect(result.items[1].derived).toBeFalsy()\n      })\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/websterlearner/fixtures.js",
    "content": "module.exports = {\n  files: [\n    ['door.html', 'http://www.learnersdictionary.com/definition/door'],\n    ['house.html', 'http://www.learnersdictionary.com/definition/house'],\n    ['jumblish.html', 'http://www.learnersdictionary.com/definition/jumblish']\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/websterlearner/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['door', 'house', 'jumblish']\n\nexport const mockRequest: MockRequest = mock => {\n  mock.onGet(/learnersdictionary/).reply(info => {\n    const wordMatch = /[^/]+$/.exec(info.url || '')\n    return wordMatch\n      ? [\n          200,\n          require(`raw-loader!./response/${decodeURIComponent(\n            wordMatch[0]\n          )}.html`).default\n        ]\n      : [404]\n  })\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/wikipedia/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport { search } from '@/components/dictionaries/wikipedia/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile } from '@/app-config/profiles'\n\ndescribe('Dict/Wikipedia/engine', () => {\n  it('should parse result correctly', () => {\n    return retry(() =>\n      search('数字', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(({ result }) => {\n        expect(typeof result.title).toBe('string')\n        expect(typeof result.content).toBe('string')\n      })\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/wikipedia/fixtures.js",
    "content": "module.exports = {\n  files: [\n    [\n      'langlist.html',\n      'https://zh.m.wikipedia.org/wiki/Special:%E7%A7%BB%E5%8A%A8%E7%89%88%E8%AF%AD%E8%A8%80/%E6%95%B8%E5%AD%97'\n    ],\n    ['数字.html', 'https://zh.m.wikipedia.org/wiki/%E6%95%B0%E5%AD%97']\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/wikipedia/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['数字']\n\nexport const mockRequest: MockRequest = mock => {\n  mock\n    .onGet(/wikipedia.+Special/)\n    .reply(200, require(`raw-loader!./response/langlist.html`).default)\n\n  mock\n    .onGet(/m\\.wikipedia/)\n    .reply(200, require(`raw-loader!./response/数字.html`).default)\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/youdao/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport {\n  search,\n  YoudaoResultLex,\n  YoudaoResultRelated\n} from '@/components/dictionaries/youdao/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport { getDefaultProfile, ProfileMutable } from '@/app-config/profiles'\n\ndescribe('Dict/Youdao/engine', () => {\n  it('should parse lex result correctly', () => {\n    return retry(() =>\n      search('love', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(searchResult => {\n        expect(searchResult.audio && typeof searchResult.audio.uk).toBe(\n          'string'\n        )\n        expect(searchResult.audio && typeof searchResult.audio.us).toBe(\n          'string'\n        )\n\n        const result = searchResult.result as YoudaoResultLex\n        expect(result.type).toBe('lex')\n        expect(result.stars).toBe(5)\n        expect(result.prons).toHaveLength(2)\n        expect(typeof result.title).toBe('string')\n        expect(result.title).toBeTruthy()\n        expect(typeof result.rank).toBe('string')\n        expect(result.rank).toBeTruthy()\n        expect(typeof result.pattern).toBe('string')\n        expect(result.pattern).toBeTruthy()\n        expect(typeof result.basic).toBe('string')\n        expect(result.basic).toBeTruthy()\n        expect(typeof result.collins).toBe('string')\n        expect(result.collins).toBeTruthy()\n        expect(typeof result.discrimination).toBe('string')\n        expect(result.discrimination).toBeTruthy()\n        expect(typeof result.sentence).toBe('string')\n        expect(result.sentence).toBeTruthy()\n        expect(result.translation).toBeFalsy()\n      })\n    )\n  })\n\n  it('should parse lex result correctly when options are changed', () => {\n    const profile = getDefaultProfile() as ProfileMutable\n    profile.dicts.all.youdao.options = {\n      basic: false,\n      collins: false,\n      discrimination: false,\n      sentence: false,\n      translation: false,\n      related: false\n    }\n    return retry(() =>\n      search('love', getDefaultConfig(), profile, { isPDF: false }).then(\n        searchResult => {\n          expect(searchResult.audio && typeof searchResult.audio.uk).toBe(\n            'string'\n          )\n          expect(searchResult.audio && typeof searchResult.audio.us).toBe(\n            'string'\n          )\n\n          const result = searchResult.result as YoudaoResultLex\n          expect(result.type).toBe('lex')\n          expect(result.stars).toBe(5)\n          expect(result.prons).toHaveLength(2)\n          expect(typeof result.title).toBe('string')\n          expect(result.title).toBeTruthy()\n          expect(typeof result.rank).toBe('string')\n          expect(result.rank).toBeTruthy()\n          expect(typeof result.pattern).toBe('string')\n          expect(result.pattern).toBeTruthy()\n          expect(result.basic).toBeFalsy()\n          expect(result.collins).toBeFalsy()\n          expect(result.discrimination).toBeFalsy()\n          expect(result.sentence).toBeFalsy()\n          expect(result.translation).toBeFalsy()\n        }\n      )\n    )\n  })\n\n  it('should parse translation result correctly', () => {\n    const text =\n      'She walks in beauty, like the night Of cloudless climes and starry skies.'\n\n    return retry(() =>\n      search(text, getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(searchResult => {\n        expect(!searchResult.audio || !searchResult.audio.uk).toBeTruthy()\n        expect(!searchResult.audio || !searchResult.audio.us).toBeTruthy()\n\n        const result = searchResult.result as YoudaoResultLex\n        expect(result.type).toBe('lex')\n        expect(result.stars).toBeFalsy()\n        expect(result.prons).toHaveLength(0)\n        expect(result.title).toBeFalsy()\n        expect(result.rank).toBeFalsy()\n        expect(result.pattern).toBeFalsy()\n        expect(result.basic).toBeFalsy()\n        expect(result.collins).toBeFalsy()\n        expect(result.discrimination).toBeFalsy()\n        expect(result.sentence).toBeFalsy()\n        expect(result.translation).toBeTruthy()\n        expect(typeof result.translation).toBe('string')\n      })\n    )\n  })\n\n  it('should parse related result correctly', () => {\n    return retry(() =>\n      search('jumblish', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(searchResult => {\n        expect(searchResult.audio).toBeUndefined()\n\n        const result = searchResult.result as YoudaoResultRelated\n        expect(result.type).toBe('related')\n        expect(typeof result.list).toBe('string')\n      })\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/youdao/fixtures.js",
    "content": "module.exports = {\n  files: [\n    ['jumblish.html', 'https://dict.youdao.com/w/jumblish'],\n    ['love.html', 'https://dict.youdao.com/w/love'],\n    ['make.html', 'https://dict.youdao.com/w/make'], // collins\n    [\n      'translation.html',\n      'https://dict.youdao.com/w/' +\n        encodeURIComponent(\n          `She walks in beauty, like the night Of cloudless climes and starry skies.`\n        )\n    ]\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/youdao/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['make', 'love', 'translation', 'jumblish']\n\nexport const mockRequest: MockRequest = mock => {\n  mock.onGet(/youdao/).reply(info => {\n    const wordMatch = /[^/]+$/.exec(info.url || '')\n    return wordMatch\n      ? [\n          200,\n          require(`raw-loader!./response/${decodeURIComponent(\n            wordMatch[0]\n          )}.html`).default\n        ]\n      : [404]\n  })\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/zdic/engine.spec.ts",
    "content": "import { retry } from '../helpers'\nimport { search } from '@/components/dictionaries/zdic/engine'\nimport { getDefaultConfig } from '@/app-config'\nimport getDefaultProfile, { ProfileMutable } from '@/app-config/profiles'\n\ndescribe('Dict/Zdic/engine', () => {\n  it('should parse word result correctly', () => {\n    return retry(() =>\n      search('爱', getDefaultConfig(), getDefaultProfile(), {\n        isPDF: false\n      }).then(({ result, audio }) => {\n        expect(audio && typeof audio.py).toBeUndefined()\n        expect(result.length).toBeGreaterThan(0)\n      })\n    )\n  })\n\n  it('should parse phrase result correctly', () => {\n    const profile = getDefaultProfile() as ProfileMutable\n    profile.dicts.all.zdic.options.audio = true\n    return retry(() =>\n      search('沙拉', getDefaultConfig(), profile, { isPDF: false }).then(\n        ({ result, audio }) => {\n          expect(audio && typeof audio.py).toBe('string')\n          expect(result.length).toBeGreaterThan(0)\n        }\n      )\n    )\n  })\n})\n"
  },
  {
    "path": "test/specs/components/dictionaries/zdic/fixtures.js",
    "content": "module.exports = {\n  files: [\n    ['沙拉.html', 'https://www.zdic.net/hans/' + encodeURIComponent('沙拉')],\n    ['爱.html', 'https://www.zdic.net/hans/' + encodeURIComponent('爱')]\n  ]\n}\n"
  },
  {
    "path": "test/specs/components/dictionaries/zdic/requests.mock.ts",
    "content": "import { MockRequest } from '@/components/dictionaries/helpers'\n\nexport const mockSearchTexts = ['沙拉', '爱']\n\nexport const mockRequest: MockRequest = mock => {\n  mock.onGet(/zdic/).reply(info => {\n    const wordMatch = /[^/]+$/.exec(info.url || '')\n    return wordMatch\n      ? [\n          200,\n          require(`raw-loader!./response/${decodeURIComponent(\n            wordMatch[0]\n          )}.html`).default\n        ]\n      : [404]\n  })\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    // Target latest version of ECMAScript.\n    \"target\": \"esnext\",\n    // Search under node_modules for non-relative imports.\n    \"moduleResolution\": \"node\",\n    // Process & infer types from .js files.\n    \"allowJs\": true,\n    // Don't emit; allow Babel to transform files.\n    \"noEmit\": true,\n    // Enable strictest settings like strictNullChecks & noImplicitAny.\n    \"strict\": true,\n    // Disallow features that require cross-file information for emit.\n    \"isolatedModules\": true,\n    // Import non-ES modules as default imports.\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"resolveJsonModule\": true,\n    \"noImplicitAny\": false,\n    \"jsx\": \"react\",\n    \"baseUrl\": \"./\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"]\n    },\n    \"typeRoots\": [\"node_modules/@types\", \"src/typings\"],\n    \"lib\": [\"esnext\", \"dom\", \"dom.iterable\"]\n  },\n  \"include\": [\"src\", \"test\", \".storybook\"]\n}\n"
  },
  {
    "path": "webpack.config.js",
    "content": "module.exports = require('neutrino')().webpack()\n"
  }
]