Hanzipopup - Popup Dictionary for Chinese Language

A port of Zhongwen Chinese-English Pop-Up Dictionary as UserScript for Safari

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name            Hanzipopup - Popup Dictionary for Chinese Language
// @namespace       https://krmanik.github.io/hanzipopup/
// @version         0.0.1
// @description     A port of Zhongwen Chinese-English Pop-Up Dictionary as UserScript for Safari
// @homepageURL     https://krmanik.github.io/hanzipopup/
// @supportURL      https://github.com/krmanik/hanzipopup
// @icon            https://krmanik.github.io/hanzipopup/icon.png
// @match           *://*/*
// @exclude-match   *://*/*/wordlist.*
// @exclude-match   *://*/*/tts.*
// @inject-into     content
// @grant           GM.addStyle
// @grant           GM.getValue
// @grant           GM.setValue
// @grant           GM.xmlHttpRequest
// @license         GPL-2.0
// ==/UserScript==

/*
 Hanzipopup - A Chinese-English Pop-Up Dictionary UserScript
 Copyright (C) 2024 krmanik
 https://github.com/krmanik/hanzipopup
 
 ---
 
 Zhongwen - A Chinese-English Pop-Up Dictionary
 Copyright (C) 2010-2023 Christian Schiller
 https://chrome.google.com/extensions/detail/kkmlkkjojmombglmlpbpapmhcaljjkde

 ---

 Originally based on Rikaikun 0.8
 Copyright (C) 2010 Erek Speed
 http://code.google.com/p/rikaikun/

 ---

 Originally based on Rikaichan 1.07
 by Jonathan Zarate
 http://www.polarcloud.com/

 ---

 Originally based on RikaiXUL 0.4 by Todd Rudick
 http://www.rikai.com/
 http://rikaixul.mozdev.org/

 ---

 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

 ---

 Please do not change or remove any of the copyrights or links to web pages
 when modifying any of the files.
 */

 'use strict';

// https://github.com/cschiller/zhongwen
// https://github.com/krmanik/hanzipopup
let style = `
.unselectable {
  -khtml-user-select: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
}
#custom-popup-container {
  display: none;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background-color: #fff;
  box-shadow: rgba(0, 0, 0, 0.8) 0px 0px 200px 28px;
  z-index: 99999999999;
  width: 90%;
  height: 400px;
}
#custom-title-container {
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 1px solid #ccc;
  padding: 8px 8px 8px 18px;
}
.custom-popup-label {
  display: flex;
  margin-bottom: 2px;
  font-weight: bold;
  font-size: 14px;
  border-bottom: 1px solid #ccc;
  width: 95%;
  padding-bottom: 4px;
  place-items: center;
  align-items: center;
}
#custom-popup-container a {
  text-decoration: none;
  color: #2196F3;
}
#custom-popup-container input[type=checkbox],
input[type=radio] {
  accent-color: #2196F3;
}
#zhongwenPopupContainer {
  height: 348px;
  overflow-y: scroll;
  margin-left: 20px;
  margin-bottom: 60px;
}
#zhongwenPopupContainer::-webkit-scrollbar {
  width: 6px;
}
#zhongwenPopupContainer::-webkit-scrollbar-track {
  background: #fff;
}
#zhongwenPopupContainer::-webkit-scrollbar-thumb {
  background: #4CAF50;
  border-radius: 20px;
}
#zhongwenPopupContainer::-webkit-scrollbar-thumb:hover {
  background: #555;
}
@media (prefers-color-scheme: dark) {
  #custom-popup-container {
      background-color: #2f2f2f !important;
      color: #fff;
  }
  #zhongwenPopupContainer {
      select {
        color: #fff;
        background-color: #2f2f2f !important;
        margin: 0;
      }
  }
  #zhongwenPopupContainer::-webkit-scrollbar-track {
    background: #2f2f2f;
  }
}
/* Generic Styles */
#zhongwen-window,
#zhongwen-window * {
  width: auto;
  height: auto;
  background: transparent;
  border: none !important;
  margin: 0px;
  padding: 0px;
  letter-spacing: normal;
  text-align: left;
  text-decoration: none;
  text-indent: 0px;
  text-transform: none;
  white-space: normal;
  word-spacing: normal;
  font-weight: normal;
  font-size: 12px;
  font-family: Tahoma, Geneva, sans-serif;
  visibility: visible;
  line-height: initial;
}
#zhongwen-window {
  position: absolute;
  z-index: 99999999;
  border: 1px solid #d0d0d0 !important;
  padding: 4px;
  background: #e6f4ff;
  top: 5px;
  left: 5px;
  min-width: 100px;
  border-radius: 5px;
  box-shadow: 10px 10px 5px -5px #999999;
}
#zhongwen-window .w-hanzi {
  font-size: 32px;
  margin-right: 0.7em;
}
#zhongwen-window .w-pinyin {
  font-size: 18px;
}
#zhongwen-window .w-def {
  font-size: 16px;
}
#zhongwen-window .w-zhuyin {
  font-family: PMingLiU, 'Apple LiGothic', sans-serif;
  font-size: 16px;
}
#zhongwen-window .w-hanzi-small {
  font-size: 18px;
  margin-right: 0.7em;
}
#zhongwen-window .w-pinyin-small {
  font-size: 16px;
}
#zhongwen-window .w-def-small {
  font-size: 12px;
}
#zhongwen-window .w-zhuyin-small {
  font-family: PMingLiU, 'Apple LiGothic', sans-serif;
  font-size: 12px;
}
#zhongwen-window .grammar {
  font-weight: bold;
}
#zhongwen-window .vocab {
  font-weight: bold;
}
/* Yellow Background */
#zhongwen-window.background-yellow,
#zhongwen-window.background-yellow * {
  color: #000000;
  background: #ffffbf;
}
#zhongwen-window.background-yellow .w-hanzi,
#zhongwen-window.background-yellow .w-hanzi-small {
  color: #7070e0;
}
#zhongwen-window.background-yellow .grammar {
  color: #00008b;
}
#zhongwen-window.background-yellow .vocab {
  color: #00008b;
}
/* Lightblue Background */
#zhongwen-window.background-lightblue,
#zhongwen-window.background-lightblue * {
  color: #000000;
  background: #e6f4ff;
}
#zhongwen-window.background-lightblue .w-hanzi,
#zhongwen-window.background-lightblue .w-hanzi-small {
  color: #3082bf;
}
#zhongwen-window.background-lightblue .w-pinyin,
#zhongwen-window.background-lightblue .w-pinyin-small {
  color: #00b366;
}
#zhongwen-window.background-lightblue .grammar {
  color: #00008b;
}
#zhongwen-window.background-lightblue .vocab {
  color: #00008b;
}
/* Blue Background */
#zhongwen-window.background-blue,
#zhongwen-window.background-blue * {
  color: #ffffff;
  background: #5c73b8;
}
#zhongwen-window.background-blue .w-hanzi,
#zhongwen-window.background-blue .w-hanzi-small {
  color: #b7e7ff;
}
#zhongwen-window.background-blue .w-pinyin,
#zhongwen-window.background-blue .w-pinyin-small {
  color: #c0ffc0;
}
#zhongwen-window.background-blue .grammar {
  color: #add8e6;
}
#zhongwen-window.background-blue .vocab {
  color: #add8e6;
}
/* Black Background */
#zhongwen-window.background-black,
#zhongwen-window.background-black * {
  color: #ffffff;
  background: #000000;
}
#zhongwen-window.background-black .w-hanzi,
#zhongwen-window.background-black .w-hanzi-small {
  color: #7070e0;
}
#zhongwen-window.background-black .w-pinyin,
#zhongwen-window.background-black .w-pinyin-small {
  color: #20a020;
}
#zhongwen-window.background-black .grammar {
  color: #add8e6;
}
#zhongwen-window.background-black .vocab {
  color: #add8e6;
}
/* Standard Tone Colors */
#zhongwen-window.tonecolor-standard .tone1 {
  color: #ee363e;
}
#zhongwen-window.tonecolor-standard .tone2 {
  color: #f47c36;
}
#zhongwen-window.tonecolor-standard .tone3 {
  color: #73bb4f;
}
#zhongwen-window.tonecolor-standard .tone4 {
  color: #649cd3;
}
#zhongwen-window.tonecolor-standard .tone5 {
  color: #a0a0a0;
}
/* Pleco Tone Colors */
#zhongwen-window.tonecolor-pleco .tone1 {
  color: #e30000;
}
#zhongwen-window.tonecolor-pleco .tone2 {
  color: #02b31c;
}
#zhongwen-window.tonecolor-pleco .tone3 {
  color: #1510f0;
}
#zhongwen-window.tonecolor-pleco .tone4 {
  color: #8900bf;
}
#zhongwen-window.tonecolor-pleco .tone5 {
  color: #777777;
}
/* Hanping Tone Colors */
#zhongwen-window.tonecolor-hanping .tone1 {
  color: #64b4ff;
}
#zhongwen-window.tonecolor-hanping .tone2 {
  color: #30b030;
}
#zhongwen-window.tonecolor-hanping .tone3 {
  color: #f08000;
}
#zhongwen-window.tonecolor-hanping .tone4 {
  color: #d00020;
}
#zhongwen-window.tonecolor-hanping .tone5 {
  color: #a0a0a0;
}
#hanzipopup-fab-container,
#custom-popup-container {
  .info-button {
    background: #828282;
    padding: 8px;
    margin: 1px;
    border-radius: 8px;
    color: white;
    text-align: center;
    cursor: pointer;
    border: none;
  }
  #zhongwen-info-buttons {
    display: flex;
    flex-direction: column;
    position: fixed;
    bottom: 64px;
    padding: 4px;
    z-index: 9999999999;
  }
  .option-div {
    margin: 4px 0px 4px 2px;
  }
  .h-icon-btn {
    width: 36px;
    height: 36px;
    position: relative;
    border: none;
    border-radius: 10%;
    cursor: pointer;
    outline: none;
    top: unset;
    left: unset;
    right: unset;
    bottom: unset;
  }
  .h-icon-btn:before {
    content: "";
    position: absolute;
    left: 6px;
    top: 0;
    bottom: 0;
    width: 24px;
  }
  .init {
    border-radius: 2px;
    display: inline-flex;
    width: 24px !important;
    height: 24px !important;
    cursor: pointer;
  }
  .init:before {
    left: 0px !important;
    position: absolute;
  }
  .hero:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='m476-80 182-480h84L924-80h-84l-43-122H603L560-80h-84ZM160-200l-56-56 202-202q-35-35-63.5-80T190-640h84q20 39 40 68t48 58q33-33 68.5-92.5T484-720H40v-80h280v-80h80v80h280v80H564q-21 72-63 148t-83 116l96 98-30 82-122-125-202 201Zm468-72h144l-72-204-72 204Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .enable:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='m476-80 182-480h84L924-80h-84l-43-122H603L560-80h-84ZM160-200l-56-56 202-202q-35-35-63.5-80T190-640h84q20 39 40 68t48 58q33-33 68.5-92.5T484-720H40v-80h280v-80h80v80h280v80H564q-21 72-63 148t-83 116l96 98-30 82-122-125-202 201Zm468-72h144l-72-204-72 204Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .disable:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='m476-80 182-480h84L924-80h-84l-43-122H603L560-80h-84ZM160-200l-56-56 202-202q-35-35-63.5-80T190-640h84q20 39 40 68t48 58q33-33 68.5-92.5T484-720H40v-80h280v-80h80v80h280v80H564q-21 72-63 148t-83 116l96 98-30 82-122-125-202 201Zm468-72h144l-72-204-72 204Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .option:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .save:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='M840-680v480q0 33-23.5 56.5T760-120H200q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h480l160 160Zm-80 34L646-760H200v560h560v-446ZM480-240q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35ZM240-560h360v-160H240v160Zm-40-86v446-560 114Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .view:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='M360-240h440v-107H360v107ZM160-613h120v-107H160v107Zm0 187h120v-107H160v107Zm0 186h120v-107H160v107Zm200-186h440v-107H360v107Zm0-187h440v-107H360v107ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .show:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='M500-640v320l160-160-160-160ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm120-80v-560H200v560h120Zm80 0h360v-560H400v560Zm-80 0H200h120Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .hide:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='M460-320v-320L300-480l160 160ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm440-80h120v-560H640v560Zm-80 0v-560H200v560h360Zm80 0h120-120Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .tts:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='M160-80q-33 0-56.5-23.5T80-160v-640q0-33 23.5-56.5T160-880h360l-80 80H160v640h440v-120h80v120q0 33-23.5 56.5T600-80H160Zm80-160v-80h280v80H240Zm0-120v-80h200v80H240Zm360 0L440-520H320v-200h120l160-160v520Zm80-122v-276q36 21 58 57t22 81q0 45-22 81t-58 57Zm0 172v-84q70-25 115-86.5T840-620q0-78-45-139.5T680-846v-84q104 27 172 112.5T920-620q0 112-68 197.5T680-310Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .prev:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='M440-760v-80h80v80h-80Zm0 640v-80h80v80h-80ZM280-760v-80h80v80h-80Zm0 640v-80h80v80h-80ZM120-760v-80h80v80h-80Zm0 640v-80h80v80h-80Zm480 0v-80h80v-560h-80v-80h240v80h-80v560h80v80H600ZM280-320 120-480l160-160 56 56-63 64h287v80H273l63 64-56 56Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .next:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='M440-120v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80v80h-80Zm0-640v-80h80v80h-80ZM120-120v-80h80v-560h-80v-80h240v80h-80v560h80v80H120Zm560-200-56-56 63-64H400v-80h287l-63-64 56-56 160 160-160 160Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .up:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='M240-400v-480h480v480H240Zm80-80h320v-320H320v320Zm320 240v-80h80v80h-80Zm-400 0v-80h80v80h-80ZM640-80v-80h80v80h-80Zm-200 0v-80h80v80h-80Zm-200 0v-80h80v80h-80Zm240-560Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .down:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='M240-80v-480h480v480H240Zm80-80h320v-320H320v320Zm-80-480v-80h80v80h-80Zm400 0v-80h80v80h-80ZM240-800v-80h80v80h-80Zm200 0v-80h80v80h-80Zm200 0v-80h80v80h-80ZM480-320Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .copy:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='M120-220v-80h80v80h-80Zm0-140v-80h80v80h-80Zm0-140v-80h80v80h-80ZM260-80v-80h80v80h-80Zm100-160q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360Zm0-80h360v-480H360v480Zm40 240v-80h80v80h-80Zm-200 0q-33 0-56.5-23.5T120-160h80v80Zm340 0v-80h80q0 33-23.5 56.5T540-80ZM120-640q0-33 23.5-56.5T200-720v80h-80Zm420 80Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .alt:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-480H160v480Zm0 0v-480 480Zm280-40h320v-240H440v240Zm80-80v-80h160v80H520Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .more-info:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='m357-384 123-123 123 123 57-56-180-180-180 180 57 56ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .popup:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-480H200v480Zm80-280v-80h400v80H280Zm0 160v-80h240v80H280Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .pinyin:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='M340-468h56v-82h-56v82ZM101-260l-17-45h39q5 0 9.5-3.5t4.5-8.5v-101l-52 17-11-44 63-19v-90H81v-42h56v-92h44v92h45v42h-46v77l40-13 6 42-46 16v125q0 18-10.5 32.5T142-260h-41Zm139 6-29-32q35-23 57.5-59t23.5-79h-68v-45h72v-81h-54v-41h253v44h-57v83h70l-2 40h-68v165h-44v-165h-58q-2 52-27.5 96.5T240-254Zm193-327-41-10 22-48q11-24 19-49l45 16q-10 23-21.5 45.5T433-581Zm-121-2q-10-23-21-45t-25-42l40-17q14 20 25 42t21 45l-40 17Zm390 249q28 0 54.5-13t48.5-37v-106q-23 3-42.5 7t-36.5 9q-45 14-67.5 35T636-390q0 26 18 41t48 15Zm-23 68q-57 0-90-32.5T556-387q0-52 33-85t106-53q23-6 50.5-11t59.5-9q-2-47-22-68.5T721-635q-26 0-51.5 9.5T604-592l-32-56q33-25 77.5-40.5T740-704q71 0 108 44t37 128v257h-67l-6-45q-28 25-61.5 39.5T679-266Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .font:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='M560-160v-520H360v-120h520v120H680v520H560Zm-360 0v-320H80v-120h360v120H320v320H200Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .translate:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='m476-80 182-480h84L924-80h-84l-43-122H603L560-80h-84ZM160-200l-56-56 202-202q-35-35-63.5-80T190-640h84q20 39 40 68t48 58q33-33 68.5-92.5T484-720H40v-80h280v-80h80v80h280v80H564q-21 72-63 148t-83 116l96 98-30 82-122-125-202 201Zm468-72h144l-72-204-72 204Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .book:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='M160-391h45l23-66h104l24 66h44l-97-258h-46l-97 258Zm81-103 38-107h2l38 107h-78Zm319-70v-68q33-14 67.5-21t72.5-7q26 0 51 4t49 10v64q-24-9-48.5-13.5T700-600q-38 0-73 9.5T560-564Zm0 220v-68q33-14 67.5-21t72.5-7q26 0 51 4t49 10v64q-24-9-48.5-13.5T700-380q-38 0-73 9t-67 27Zm0-110v-68q33-14 67.5-21t72.5-7q26 0 51 4t49 10v64q-24-9-48.5-13.5T700-490q-38 0-73 9.5T560-454ZM260-320q47 0 91.5 10.5T440-278v-394q-41-24-87-36t-93-12q-36 0-71.5 7T120-692v396q35-12 69.5-18t70.5-6Zm260 42q44-21 88.5-31.5T700-320q36 0 70.5 6t69.5 18v-396q-33-14-68.5-21t-71.5-7q-47 0-93 12t-87 36v394Zm-40 118q-48-38-104-59t-116-21q-42 0-82.5 11T100-198q-21 11-40.5-1T40-234v-482q0-11 5.5-21T62-752q46-24 96-36t102-12q58 0 113.5 15T480-740q51-30 106.5-45T700-800q52 0 102 12t96 36q11 5 16.5 15t5.5 21v482q0 23-19.5 35t-40.5 1q-37-20-77.5-31T700-240q-60 0-116 21t-104 59ZM280-499Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .message:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='M320-520q17 0 28.5-11.5T360-560q0-17-11.5-28.5T320-600q-17 0-28.5 11.5T280-560q0 17 11.5 28.5T320-520Zm160 0q17 0 28.5-11.5T520-560q0-17-11.5-28.5T480-600q-17 0-28.5 11.5T440-560q0 17 11.5 28.5T480-520Zm160 0q17 0 28.5-11.5T680-560q0-17-11.5-28.5T640-600q-17 0-28.5 11.5T600-560q0 17 11.5 28.5T640-520ZM80-80v-720q0-33 23.5-56.5T160-880h640q33 0 56.5 23.5T880-800v480q0 33-23.5 56.5T800-240H240L80-80Zm126-240h594v-480H160v525l46-45Zm-46 0v-480 480Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .zi:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='M560-480h80v-120h-80v120ZM180-160l-20-80h60q8 0 14-6t6-14v-125q-17 7-33 13.5T176-360l-16-78q19-4 39.5-11t40.5-15v-136h-60v-80h60v-120h80v120h60v80h-60v96q15-9 29-18t27-18v80q-12 10-26 19.5T320-423v203q0 23-18.5 41.5T260-160h-80Zm222 0-44-66q42-28 72.5-75t42.5-99h-73v-80h80v-120h-60v-80h360v80h-60v120h80v80h-80v240h-80v-240h-86q-14 71-54.5 136.5T402-160Zm288-490-69-30q16-27 35.5-59t31.5-61l74 27q-15 29-35 62t-37 61Zm-189-8q-17-25-39-55t-42-53l72-34q18 23 38.5 52t37.5 53l-67 37Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
  .close:before {
    position: absolute;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='25' height='25' viewBox='0 -960 960 960' fill='white'%3E%3Cpath d='m336-280 144-144 144 144 56-56-144-144 144-144-56-56-144 144-144-144-56 56 144 144-144 144 56 56ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z'%3E%3C/path%3E%3C/svg%3E") center / contain no-repeat;
  }
}
@keyframes tts-loading {
  to {
    transform: rotate(360deg);
  }
}
.tts-loading:before {
  content: '' !important;
  box-sizing: border-box !important;
  position: absolute !important;
  top: 50% !important;
  left: 50% !important;
  width: 20px !important;
  height: 20px !important;
  margin-top: -10px !important;
  margin-left: -10px !important;
  border-radius: 50% !important;
  border: 2px solid #fff !important;
  border-top-color: #ec440e !important;
  animation: tts-loading .6s linear infinite !important;
  -webkit-animation: tts-loading .6s linear infinite !important;
}
#hanzipopup-fab-container {
  position: fixed;
  transform: translate(-50%, -50%);
  bottom: 0px;
  right: 0px;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  z-index: 9999999999;
  touch-action: none;
  .fab-btn {
      position: absolute;
      top: 0;
      left: 0;
      display: flex;
      justify-content: center;
      align-items: center;
      width: 36px;
      height: 36px;
      border-radius: 10%;
      background-color: #585e88;
      color: white;
      z-index: 1000;
      box-shadow: 0px 2px 18px -1px rgba(0, 0, 0, 0.3);
      outline: none;
      border: none;
      cursor: pointer;
  }
  ul {
    li {
      position: absolute;
      top: 0;
      left: 0;
      width: 36px;
      height: 36px;
      display: flex;
      align-items: center;
      justify-content: center;
      list-style-type: none;
      transition: .5s;
      border-radius: 10%;
      cursor: pointer;
      margin: 0 auto;
    }
  }
  &.fab-active {
      &.fab-active {
        :nth-child(1) { --nth-child: 1 }
        :nth-child(2) { --nth-child: 2 }
        :nth-child(3) { --nth-child: 3 }
        :nth-child(4) { --nth-child: 4 }
        :nth-child(5) { --nth-child: 5 }
        :nth-child(6) { --nth-child: 6 }
        :nth-child(7) { --nth-child: 7 }
        :nth-child(8) { --nth-child: 8 }
        li {
          top: 0;
        }
        &.left {
          li:nth-child(n+1) {
            left: calc(var(--nth-child) * 40px);
            transition-delay: calc(var(--nth-child) * 0.01s);
          }
        }
        &.right {
          li:nth-child(n+1) {
            left: calc(var(--nth-child) * -40px);
            transition-delay: calc(var(--nth-child) * 0.01s);
          }
        }
      }
    }
}
`;

GM.addStyle(style);

// https://github.com/krmanik/hanzipopup
let defaultConfig = {
    popupColor: 'yellow',
    fontSize: 'small',
    zhuyin: false,
    grammar: true,
    vocab: true,
    simpTrad: 'classic',
    toneColorScheme: 'standard',
    enable: false,
    tts: false,
    prev: false,
    next: false,
    more: false,
    cnTtsEngine: 'browser',
    ttsvoice: "zh-CN-XiaoxiaoNeural",
}

async function setConfig(key, value) {
    let config = await GM.getValue('config', JSON.stringify(defaultConfig));
    config = JSON.parse(config);
    config[key] = value;
    let jsonStr = JSON.stringify(config);
    return await GM.setValue('config', jsonStr);
}

async function getConfig() {
    let config = await GM.getValue('config', null);
    if (config === null) {
        config = JSON.stringify(defaultConfig);
        await GM.setValue('config', config);
        return defaultConfig;
    }
    config = JSON.parse(config);
    return config
}

// https://github.com/cschiller/zhongwen
function altViewInfo() {
    altView = (altView + 1) % 3;
    triggerSearch();
}

function copyToClip() {
    copyToClipboard(getTextForClipboard());
}

function copyToClipboard(data) {
    let txt = document.createElement('textarea');
    txt.style.position = "absolute";
    txt.style.left = "-100%";
    txt.value = data;
    document.body.appendChild(txt);
    txt.select();
    document.execCommand('copy');
    document.body.removeChild(txt);

    showPopup('Copied to clipboard', null, -1, -1);
}

function wordPrev() {
    let offset = selStartDelta;
    selStartDelta = --offset;
    let ret = triggerSearch();
    if (ret === 0) {
        return
    } else if (ret === 2) {
        savedRangeNode = findPreviousTextNode(savedRangeNode.parentNode, savedRangeNode);
        savedRangeOffset = 0;
        offset = savedRangeNode.data.length;
    }
}

function wordNext() {
    selStartDelta += selStartIncrement;
    let ret = triggerSearch();
    if (ret === 0) {
        return;
    } else if (ret === 2) {
        savedRangeNode = findNextTextNode(savedRangeNode.parentNode, savedRangeNode);
        savedRangeOffset = 0;
        selStartDelta = 0;
        selStartIncrement = 0;
    }
}

async function grammarInfo() {
    let config = await getConfig();
    if (config['grammar'] && savedSearchResults.grammar) {
        let sel = encodeURIComponent(window.getSelection().toString());

        // https://resources.allsetlearning.com/chinese/grammar/%E4%B8%AA
        let allset = 'https://resources.allsetlearning.com/chinese/grammar/' + sel;
        infoWindowOpen(allset);
    }
}

async function vocabInfo() {
    let config = await getConfig();
    if (config['vocab'] && savedSearchResults.vocab) {
        let sel = encodeURIComponent(window.getSelection().toString());

        // https://resources.allsetlearning.com/chinese/vocabulary/%E4%B8%AA
        let allset = 'https://resources.allsetlearning.com/chinese/vocabulary/' + sel;
        infoWindowOpen(allset);
    }
}

function tatoebaInfo() {
    let sel = encodeURIComponent(window.getSelection().toString());

    // https://tatoeba.org/eng/sentences/search?from=cmn&to=eng&query=%E8%BF%9B%E8%A1%8C
    let tatoeba = 'https://tatoeba.org/eng/sentences/search?from=cmn&to=eng&query=' + sel;
    infoWindowOpen(tatoeba);
}

function movePopupUp() {
    altView = 0;
    popY -= 20;
    triggerSearch();
}

function movePopupDown() {
    altView = 0;
    popY += 20;
    triggerSearch();
}

function lineDictInfo() {
    // use the simplified character for linedict lookup
    let simp = savedSearchResults[0][0];

    // https://english.dict.naver.com/english-chinese-dictionary/#/search?query=%E8%AF%8D%E5%85%B8
    let linedict = 'https://english.dict.naver.com/english-chinese-dictionary/#/search?query=' +
        encodeURIComponent(simp);
    infoWindowOpen(linedict);
}

function forvoInfo() {
    let sel = encodeURIComponent(window.getSelection().toString());

    // https://forvo.com/search/%E4%B8%AD%E6%96%87/zh/
    var forvo = 'https://forvo.com/search/' + sel + '/zh/';
    infoWindowOpen(forvo);

}

function dictInfo() {
    let sel = encodeURIComponent(window.getSelection().toString());

    // https://dict.cn/%E7%BF%BB%E8%AF%91
    let dictcn = 'https://dict.cn/' + sel;
    infoWindowOpen(dictcn);

}

function icibaInfo() {
    let sel = encodeURIComponent(window.getSelection().toString());

    // https://www.iciba.com/%E4%B8%AD%E9%A4%90
    let iciba = 'https://www.iciba.com/' + sel;
    infoWindowOpen(iciba);

}

function mdbgInfo() {
    let sel = encodeURIComponent(window.getSelection().toString());

    // https://www.mdbg.net/chinese/dictionary?page=worddict&wdrst=0&wdqb=%E4%B8%AD%E6%96%87
    let mdbg = 'https://www.mdbg.net/chinese/dictionary?page=worddict&wdrst=0&wdqb=' + sel;
    infoWindowOpen(mdbg);
}

function reversoInfo() {
    let sel = encodeURIComponent(
        window.getSelection().toString());

    let reverso = 'https://context.reverso.net/translation/chinese-english/' + sel;
    infoWindowOpen(reverso);
}

function moedictInfo() {
    // use the traditional character for moedict lookup
    let trad = savedSearchResults[0][1];

    // https://www.moedict.tw/~%E4%B8%AD%E6%96%87
    let moedict = 'https://www.moedict.tw/~' + encodeURIComponent(trad);
    infoWindowOpen(moedict);
}

let infoButtons = [
    { id: 'grammar_info', label: 'Grammar', func: grammarInfo },
    { id: 'vocab_info', label: 'Vocabulary', func: vocabInfo },
    { id: 'tatoeba_info', label: 'Tatoeba', func: tatoebaInfo },
    { id: 'line_dict_info', label: 'Line Dict', func: lineDictInfo },
    { id: 'forvo_info', label: 'Forvo', func: forvoInfo },
    { id: 'dict_info', label: 'Dict.cn', func: dictInfo },
    { id: 'iciba_info', label: 'Iciba', func: icibaInfo },
    { id: 'mdbg_info', label: 'MDBG', func: mdbgInfo },
    { id: 'reverso_info', label: 'Reverso', func: reversoInfo },
    { id: 'moedict_info', label: 'Moedict', func: moedictInfo }
];

const infoButtonContainer = document.createElement('div');
infoButtonContainer.id = 'zhongwen-info-buttons';
infoButtonContainer.style.display = 'none';

async function setupInfoButtons() {
    let config = await getConfig();

    infoButtons.forEach(function (button) {
        let infoButton = createLabelButton(button.label, button.func);
        infoButton.classList.add('info-button');
        infoButton.id = button.id;

        if (!config[button.id]) {
            infoButton.style.display = 'none';
            config[button.id] = false;
        } else {
            infoButton.style.display = 'inline-block';
            config[button.id] = true;
        }
        infoButtonContainer.appendChild(infoButton);
    });
}

async function updateInfoButtons(id) {
    id = id.replace("hanzipopup-", "");

    let config = await getConfig();
    let infoButton = document.querySelector(`#${id}`);

    if (!config[id]) {
        infoButton.style.display = 'none';
    } else {
        infoButton.style.display = 'inline-block';
    }
}

setupInfoButtons();
document.body.appendChild(infoButtonContainer);

function onKeyDown(keyDown) {
    let sel = window.getSelection().toString();
    if (sel.length === 0) {
        return;
    }

    if (keyDown.ctrlKey || keyDown.metaKey) {
        return;
    }

    if (keyDown.keyCode === 27) {
        // esc key pressed
        hidePopup();
        return;
    }

    if (keyDown.altKey && keyDown.keyCode === 87) {
        // Alt + w
        viewWordList();
        return;
    }

    switch (keyDown.keyCode) {
        case 65: // 'a'
            altViewInfo();
            break;
        case 66: // 'b'
            wordPrev();
            break;
        case 67: // 'c'
            copyToClip();
            break;
        case 68: // 'd'
            disableTab();
            break;
        case 71: // 'g'
            grammarInfo();
            break;
        case 77: // 'm'
            selStartIncrement = 1;
        // falls through
        case 78: // 'n'
            wordNext();
            break;
        case 80: // 'p'
            ttsPlay();
            break;
        case 82: // 'r'
            saveWordList();
            break;
        case 84: // 't'
            tatoebaInfo();
            break;
        case 86: // 'v'
            vocabInfo();
            break;
        case 88: // 'x'
            movePopupUp();
            break;
        case 89: // 'y'
            movePopupDown();
            break;
        case 49: // '1'
            lineDictInfo();
            break;
        case 50: // '2'
            forvoInfo();
            break;
        case 51: // '3'
            dictInfo();
            break;
        case 52: // '4'
            icibaInfo();
            break;
        case 53: // '5'
            mdbgInfo();
            break;
        case 54: // '6'
            reversoInfo();
            break;
        case 55: // '7'
            moedictInfo();
            break;
        default:
            return;
    }
}

// https://gist.github.com/likev/c36fcc8a08ba1a2c5d08f9c7d806a0ad
// JS port of https://github.com/Migushthe2nd/MsEdgeTTS

let socket = null;
let ttsText = null;
let ttsWindow = null;
let ttsError = false;
let ttsAudio = new Audio("");

let langList = [{
    "Name": "Microsoft Server Speech Text to Speech Voice (zh-CN, XiaoxiaoNeural)",
    "ShortName": "zh-CN-XiaoxiaoNeural",
    "Gender": "Female",
    "Locale": "zh-CN",
    "SuggestedCodec": "audio-24khz-48kbitrate-mono-mp3",
    "FriendlyName": "Microsoft Xiaoxiao Online (Natural) - Chinese (Mainland)",
    "Status": "GA",
    "VoiceTag": { "ContentCategories": ["News", "Novel"], "VoicePersonalities": ["Warm"] }
},
{
    "Name": "Microsoft Server Speech Text to Speech Voice (zh-CN, XiaoyiNeural)",
    "ShortName": "zh-CN-XiaoyiNeural",
    "Gender": "Female",
    "Locale": "zh-CN",
    "SuggestedCodec": "audio-24khz-48kbitrate-mono-mp3",
    "FriendlyName": "Microsoft Xiaoyi Online (Natural) - Chinese (Mainland)",
    "Status": "GA",
    "VoiceTag": { "ContentCategories": ["Cartoon", "Novel"], "VoicePersonalities": ["Lively"] }
},
{
    "Name": "Microsoft Server Speech Text to Speech Voice (zh-CN, YunjianNeural)",
    "ShortName": "zh-CN-YunjianNeural",
    "Gender": "Male",
    "Locale": "zh-CN",
    "SuggestedCodec": "audio-24khz-48kbitrate-mono-mp3",
    "FriendlyName": "Microsoft Yunjian Online (Natural) - Chinese (Mainland)",
    "Status": "GA",
    "VoiceTag": { "ContentCategories": ["Sports", " Novel"], "VoicePersonalities": ["Passion"] }
},
{
    "Name": "Microsoft Server Speech Text to Speech Voice (zh-CN, YunxiNeural)",
    "ShortName": "zh-CN-YunxiNeural",
    "Gender": "Male",
    "Locale": "zh-CN",
    "SuggestedCodec": "audio-24khz-48kbitrate-mono-mp3",
    "FriendlyName": "Microsoft Yunxi Online (Natural) - Chinese (Mainland)",
    "Status": "GA",
    "VoiceTag": { "ContentCategories": ["Novel"], "VoicePersonalities": ["Lively", "Sunshine"] }
},
{
    "Name": "Microsoft Server Speech Text to Speech Voice (zh-CN, YunxiaNeural)",
    "ShortName": "zh-CN-YunxiaNeural",
    "Gender": "Male",
    "Locale": "zh-CN",
    "SuggestedCodec": "audio-24khz-48kbitrate-mono-mp3",
    "FriendlyName": "Microsoft Yunxia Online (Natural) - Chinese (Mainland)",
    "Status": "GA",
    "VoiceTag": { "ContentCategories": ["Cartoon", "Novel"], "VoicePersonalities": ["Cute"] }
},
{
    "Name": "Microsoft Server Speech Text to Speech Voice (zh-CN, YunyangNeural)",
    "ShortName": "zh-CN-YunyangNeural",
    "Gender": "Male",
    "Locale": "zh-CN",
    "SuggestedCodec": "audio-24khz-48kbitrate-mono-mp3",
    "FriendlyName": "Microsoft Yunyang Online (Natural) - Chinese (Mainland)",
    "Status": "GA",
    "VoiceTag": { "ContentCategories": ["News"], "VoicePersonalities": ["Professional", "Reliable"] }
},
{
    "Name": "Microsoft Server Speech Text to Speech Voice (zh-CN-liaoning, XiaobeiNeural)",
    "ShortName": "zh-CN-liaoning-XiaobeiNeural",
    "Gender": "Female",
    "Locale": "zh-CN-liaoning",
    "SuggestedCodec": "audio-24khz-48kbitrate-mono-mp3",
    "FriendlyName": "Microsoft Xiaobei Online (Natural) - Chinese (Northeastern Mandarin)",
    "Status": "GA",
    "VoiceTag": { "ContentCategories": ["Dialect"], "VoicePersonalities": ["Humorous"] }
},
{
    "Name": "Microsoft Server Speech Text to Speech Voice (zh-CN-shaanxi, XiaoniNeural)",
    "ShortName": "zh-CN-shaanxi-XiaoniNeural",
    "Gender": "Female",
    "Locale": "zh-CN-shaanxi",
    "SuggestedCodec": "audio-24khz-48kbitrate-mono-mp3",
    "FriendlyName": "Microsoft Xiaoni Online (Natural) - Chinese (Zhongyuan Mandarin Shaanxi)",
    "Status": "GA",
    "VoiceTag": { "ContentCategories": ["Dialect"], "VoicePersonalities": ["Bright"] }
}];

function create_edge_TTS({ voice = "zh-CN-XiaoxiaoNeural", timeout = 10, auto_reconnect = true } = {}) {
    const TRUSTED_CLIENT_TOKEN = "6A5AA1D4EAFF4E9FB37E23D68491D6F4";
    // const VOICES_URL = `https://speech.platform.bing.com/consumer/speech/synthesize/readaloud/voices/list?trustedclienttoken=${TRUSTED_CLIENT_TOKEN}`;
    const SYNTH_URL = `wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1?TrustedClientToken=${TRUSTED_CLIENT_TOKEN}`;
    const BINARY_DELIM = "Path:audio\r\n";
    const VOICE_LANG_REGEX = /\w{2}-\w{2}/;

    let _outputFormat = "audio-24khz-48kbitrate-mono-mp3";
    let _voiceLocale = 'zh-CN';
    let _voice = voice;
    const _queue = { message: [], url_resolve: {}, url_reject: {} };
    let ready = false;

    function _SSMLTemplate(input) {
        return `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="${_voiceLocale}">
                  <voice name="${_voice}">
                      ${input}
                  </voice>
              </speak>`;
    }

    function uuidv4() {
        return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
            (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
        );
    }

    create_new_ws();

    function setFormat(format) {
        if (format) {
            _outputFormat = format;
        }
        socket.send(`Content-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n
                      {
                          "context": {
                              "synthesis": {
                                  "audio": {
                                      "metadataoptions": {
                                          "sentenceBoundaryEnabled": "false",
                                          "wordBoundaryEnabled": "false"
                                      },
                                      "outputFormat": "${_outputFormat}" 
                                  }
                              }
                          }
                      }
                  `);
    }

    async function createURL(requestId) {
        let index_message = 0;
        for (let message of _queue.message) {
            const isbinary = message instanceof Blob;

            if (!isbinary) {
                continue;
            }

            const data = await message.text();
            const Id = /X-RequestId:(.*?)\r\n/gm.exec(data)[1];

            if (Id !== requestId) {
                continue;
            }

            if (data.charCodeAt(0) === 0x00 && data.charCodeAt(1) === 0x67 && data.charCodeAt(2) === 0x58) {
                // Last (empty) audio fragment
                const blob = new Blob(_queue[requestId], { 'type': 'audio/mp3' });
                _queue[requestId] = null;
                const url = URL.createObjectURL(blob);
                _queue.url_resolve[requestId](url);
            } else {
                const index = data.indexOf(BINARY_DELIM) + BINARY_DELIM.length;
                const audioData = message.slice(index);
                _queue[requestId].push(audioData);
                _queue.message[index_message] = null;
            }
            ++index_message;
        }
    }

    function onopen(event) {
        setFormat();
        ready = true;
    }

    async function onmessage(event) {
        const isbinary = event.data instanceof Blob;
        _queue.message.push(event.data)
        if (!isbinary) {
            const requestId = /X-RequestId:(.*?)\r\n/gm.exec(event.data)[1];
            if (event.data.includes("Path:turn.end")) {
                createURL(requestId);
                addLoading(false);
            }
        }
    }

    function onerror(event) {
        ready = false;
        addLoading(false);
    }

    function onclose(event) {
        ready = false;
        addLoading(false);
    }

    function addSocketListeners() {
        socket.addEventListener('open', onopen);
        socket.addEventListener('message', onmessage);
        socket.addEventListener('error', onerror);
        socket.addEventListener('close', onclose);
    }

    function create_new_ws() {
        try {
            if (ttsError) {
                addLoading(false);
                return;
            }

            socket = new WebSocket(SYNTH_URL);

            socket.onerror = function (event) {
                ttsError = true;
                ttsPostMessage();
                addLoading(false);
            }

            addSocketListeners();
        } catch (e) {
            console.log(e);
        }
    }

    let toStream = function (input) {
        let requestSSML = _SSMLTemplate(input);
        const requestId = uuidv4().replaceAll('-', '');
        const request = `X-RequestId:${requestId}\r\nContent-Type:application/ssml+xml\r\nPath:ssml\r\n\r\n` + requestSSML.trim();

        _queue[requestId] = [];

        return new Promise((resolve, reject) => {
            _queue.url_resolve[requestId] = resolve, _queue.url_reject[requestId] = reject;

            if (!ready) {
                if (auto_reconnect) {
                    create_new_ws();
                    socket.addEventListener('open', _ => socket.send(request));

                    setTimeout(_ => { if (!ready) reject('reconnect timeout') }, timeout * 1000);
                }
                else reject('socket error or timeout');
            } else {
                socket.send(request)
            }
        });
    }

    async function play(input) {
        const url = await toStream(input);
        let play_resolve = function () { };
        ttsAudio.src = url;
        ttsAudio.onended = (e) => {
            addLoading(false);
            play_resolve(true);
        }
        await ttsAudio.play();
        return new Promise((resolve, reject) => {
            play_resolve = resolve
        });
    }

    return new Promise((resolve, reject) => {
        setTimeout(_ => reject('socket open timeout'), timeout * 1000);
        // Connection opened
        socket.addEventListener('open', function (event) {
            resolve({
                play,
                toStream,
                setVoice: (voice, locale) => {
                    _voice = voice;
                    if (!locale) {
                        const voiceLangMatch = VOICE_LANG_REGEX.exec(_voice);
                        if (!voiceLangMatch) {
                            throw new Error("Could not infer voiceLocale from voiceName!");
                        }
                        _voiceLocale = voiceLangMatch[0];
                    } else {
                        _voiceLocale = locale;
                    }
                },
                setFormat,
                isReady: _ => ready
            })
        });
    });
}

function addLoading(add) {
    let speakBtn = document.querySelector("#hanzi-popup-tts-btn > button");
    if (speakBtn) {
        if (add) {
            speakBtn.classList.remove("tts");
            speakBtn.classList.add("tts-loading");
        } else {
            speakBtn.classList.add("tts");
            speakBtn.classList.remove("tts-loading");
        }
    }
}

function ttsPostMessage(ttsText, voice) {
    if (!ttsWindow || ttsWindow.closed) {
        ttsWindow = window.open(`${host}/tts.html`);
    } else {
        ttsWindow.postMessage({ message: "ttsPlay", text: ttsText, voice: voice }, "*");
    }
}

async function edgeTtsPlay(text, voice = "zh-CN-XiaoxiaoNeural") {
    if (text === undefined || text === null || text === '') {
        return;
    }

    addLoading(true);

    if (ttsError) {
        ttsPostMessage(text, voice);
        addLoading(false);
        return;
    }

    ttsText = text;
    const tts = await create_edge_TTS({ voice });

    try {
        await tts.play(text);
    } catch (e) {
        ttsError = true;
        console.log(e);
        addLoading(false);
        // again 
        edgeTtsPlay(text);
    }
}

window.addEventListener("message", function (event) {
    if (event.data.message === "ttsResult") {
        if (!event.data.result) {
            if (confirm("TTS failed. Open Hanzipopup TTS page again?")) {
                if (ttsWindow) {
                    ttsWindow.focus();
                }
            }
        }
    }
});

function enableAutoTTS() {
    if (typeof window === 'undefined') {
        return;
    }
    const isiOS = navigator.userAgent.match(/ipad|iphone/i);
    if (!isiOS) {
        return;
    }
    const simulateSpeech = () => {
        const lecture = new SpeechSynthesisUtterance('hello');
        lecture.volume = 0;
        speechSynthesis.speak(lecture);
        document.removeEventListener('click', simulateSpeech);
    };

    document.addEventListener('click', simulateSpeech);
}

enableAutoTTS();

let savedTarget;
let savedRangeNode;
let savedRangeOffset;
let savedTtsStr;
let selText;
let clientX;
let clientY;
let selStartDelta;
let selStartIncrement;
let popX = 0;
let popY = 0;
let timer;
let altView = 0;
let savedSearchResults = [];
let savedSelStartOffset = 0;
let savedSelEndList = [];
let dict;
let enable = false;
let host = "https://krmanik.github.io/hanzipopup";
let wordListWindow;
let infoWindow;
let clickedTarget;

function createLabel(labelText, icon) {
    let labelIcon = document.createElement('div');
    labelIcon.classList.add("h-icon-btn");
    labelIcon.classList.add("init");
    labelIcon.classList.add(icon.name);
    labelIcon.style.background = icon.color;

    let labelDiv = document.createElement('div');
    labelDiv.style.marginLeft = '4px';
    labelDiv.innerHTML = labelText;
    let label = document.createElement('label');
    label.classList.add("custom-popup-label");
    label.appendChild(labelIcon);
    label.appendChild(labelDiv);
    return label;
}

function createRadioFormGroup(labelText, groupName, options, icon) {
    let formGroup = document.createElement('div');
    formGroup.id = groupName;
    formGroup.style.marginBottom = '20px';
    formGroup.style.textAlign = "left";

    let label = createLabel(labelText, icon);
    formGroup.appendChild(label);

    options.forEach(option => {
        let radioDiv = document.createElement('div');
        radioDiv.classList.add("option-div");

        let radioInput = document.createElement('input');
        radioInput.type = 'radio';
        radioInput.id = option.id;
        radioInput.name = groupName;
        radioInput.value = option.value;
        radioInput.addEventListener('change', () => {
            setConfig(groupName, option.value);
        });

        let radioLabel = document.createElement('label');
        radioLabel.style.marginLeft = '4px';
        radioLabel.style.fontSize = '16px';
        radioLabel.htmlFor = option.id;
        radioLabel.innerHTML = option.label;

        radioDiv.appendChild(radioInput);
        radioDiv.appendChild(radioLabel);
        formGroup.appendChild(radioDiv);
    });
    return formGroup;
}

function createCheckboxFormGroup(id, labelText, options, svg) {
    let formGroup = document.createElement('div');
    formGroup.id = id;
    formGroup.style.marginBottom = '20px';
    formGroup.style.textAlign = "left";

    let label = createLabel(labelText, svg);
    formGroup.appendChild(label);

    options.forEach(option => {
        let checkboxDiv = document.createElement('div');
        checkboxDiv.classList.add("option-div");

        let checkboxInput = document.createElement('input');
        checkboxInput.type = 'checkbox';
        checkboxInput.id = `hanzipopup-${option.id}`;
        checkboxInput.name = `hanzipopup-${option.id}`;
        checkboxInput.addEventListener('click', async (e) => {
            setTimeout(async () => {
                await setConfig(option.id, checkboxInput.checked);
            }, 50);
            setTimeout(async () => {
                if (checkboxInput.id === "hanzipopup-more") {
                    let config = await getConfig();
                    let moreInfo = document.querySelector("#hanzipopup-more-info");
                    if (config['more']) {
                        moreInfo.style.display = "block";
                        moreInfo.scrollIntoView();
                    } else {
                        moreInfo.style.display = "none";
                        moreInfo.scrollIntoView();
                    }
                } else if (checkboxInput.id.includes("_info")) {
                    await updateInfoButtons(checkboxInput.id);
                }
                await setupToolbar();
            }, 100);
        });

        let checkboxLabelElem = document.createElement('label');
        checkboxLabelElem.style.marginLeft = "4px";
        checkboxLabelElem.style.fontSize = '16px';
        checkboxLabelElem.htmlFor = option.id;
        checkboxLabelElem.innerHTML = option.label;

        checkboxDiv.appendChild(checkboxInput);
        checkboxDiv.appendChild(checkboxLabelElem);
        formGroup.appendChild(checkboxDiv);
    });
    return formGroup;
}

function createLabelButton(text, clickCallback) {
    const button = document.createElement('button');
    button.innerHTML = text;
    button.className = "unselectable";
    button.addEventListener('click', clickCallback);
    return button;
}

async function createLangList(langList) {
    let config = await getConfig();

    let container = document.createElement("div");
    container.style.margin = "8px 0px 8px 20px";
    container.style.width = "80%";
    container.style.display = "inline-block";
    container.id = "tts-voice-select";

    if (!config["tts"]) {
        container.style.display = "none";
    }

    let labelElement = document.createElement("label");
    labelElement.setAttribute("for", "voiceSelection");
    labelElement.textContent = "Select voice";

    let selectElement = document.createElement("select");
    selectElement.id = "voiceSelection";
    selectElement.name = "voiceSelection";
    selectElement.style.width = "90%";
    selectElement.addEventListener("change", async (e) => {
        selectElement.value = e.target.value;
        config['ttsvoice'] = selectElement.value;
        await setConfig('ttsvoice', config['ttsvoice']);
    });

    for (let i = 0; i < langList.length; i++) {
        let option = document.createElement("option");
        option.value = langList[i].ShortName;
        option.textContent = langList[i].FriendlyName + " (" + langList[i].Gender + ")";
        selectElement.add(option);
    }

    container.appendChild(labelElement);
    container.appendChild(selectElement);
    document.querySelector("#cnTtsEngineEdge").parentElement.appendChild(container);
}

async function ttsPlay() {
    let hanzi = savedSearchResults[0][0];
    let config = await getConfig();
    let cnTtsEngine = config['cnTtsEngine'] || 'browser';

    if (cnTtsEngine === 'browser') {
        let utterance = new SpeechSynthesisUtterance(hanzi);
        utterance.lang = "zh-CN";
        speechSynthesis.speak(utterance);
    } else {
        edgeTtsPlay(hanzi, config['ttsvoice']);
    }
}

function toggleFeature() {
    if (enable) {
        disableTab();
        document.querySelector("#hanzi-popup-enable-btn > button").style.background = "#a1a1a1";
        document.getElementById("hanzipopup-fab-btn").style.borderBottom = "unset";
    } else {
        enableTab();
        document.querySelector("#hanzi-popup-enable-btn > button").style.background = "#33b249";
        document.getElementById("hanzipopup-fab-btn").style.borderBottom = "4px solid #31eb3f";
    }
    enable = !enable;
    setConfig('enable', enable);
}

let buttonList = {
    'option': { func: toggleOption, color: "#424769" },
    'view': { func: viewWordList, color: "#3E64FF" },
    'save': { func: saveWordList, color: "#3E64FF" },
    'more-info': { func: toggleMore, color: "#5C6BC0" },
    'next': { func: wordNext, color: "#5C6BC0" },
    'prev': { func: wordPrev, color: "#5C6BC0" },
    'tts': { func: ttsPlay, color: "#5C6BC0" },
    'enable': { func: toggleFeature, color: "#a1a1a1" }
};

const fabContainer = document.createElement("div");
fabContainer.id = "hanzipopup-fab-container";
fabContainer.classList.add('right');
const fabElement = document.createElement('button');
fabElement.id = "hanzipopup-fab-btn";
fabElement.classList.add("h-icon-btn");
fabElement.classList.add("pinyin");
fabElement.classList.add("fab-btn");

async function setupToolbar() {
    let config = await getConfig();

    let elementsToRemove = [];

    if (!config['tts']) {
        elementsToRemove.push('tts');
        document.querySelector("#cnTtsEngine").style.display = "none";
    } else {
        document.querySelector("#cnTtsEngine").style.display = "unset";
        document.querySelector("#cnTtsEngine").value = config['cnTtsEngine'];
    }

    if (!config['prev']) {
        elementsToRemove.push('prev');
    }
    if (!config['next']) {
        elementsToRemove.push('next');
    }
    if (!config['more']) {
        elementsToRemove.push('more-info');
    }

    let filteredButtonList = Object.keys(buttonList).reduce((result, key) => {
        if (!elementsToRemove.includes(key)) {
            result[key] = buttonList[key];
        }
        return result;
    }, {});

    let windowWidth = window.innerWidth;
    fabContainer.style.left = windowWidth - 38 + "px";
    fabContainer.style.top = window.innerHeight - 38 + "px";

    let toolList = document.querySelector("#hanzipopup-tools");
    if (toolList) {
        toolList.innerHTML = "";
    } else {
        toolList = document.createElement("ul");
        toolList.id = "hanzipopup-tools";
    }

    fabContainer.appendChild(toolList);

    for (let key in filteredButtonList) {
        const liElem = document.createElement("li");
        liElem.onclick = filteredButtonList[key].func;
        liElem.id = `hanzi-popup-${key}-btn`

        let icon = document.createElement("button");
        icon.classList.add("h-icon-btn");
        icon.classList.add(key);
        icon.style.background = filteredButtonList[key].color;

        liElem.appendChild(icon)
        toolList.appendChild(liElem);
    }

    if (config['enable']) {
        document.querySelector("#hanzi-popup-enable-btn > button").style.background = "#33b249";
        document.getElementById("hanzipopup-fab-btn").style.borderBottom = "4px solid #31eb3f";
    }

    let oldPositionX = fabContainer.style.left;
    let oldPositionY = fabContainer.style.top;

    const move = (e) => {
        if (e.type === "touchmove") {
            fabContainer.style.top = e.touches[0].clientY + "px";
            fabContainer.style.left = e.touches[0].clientX + "px";
        } else {
            fabContainer.style.top = e.clientY + "px";
            fabContainer.style.left = e.clientX + "px";
        }
    };

    const mouseDown = (e) => {
        oldPositionY = fabContainer.style.top;
        oldPositionX = fabContainer.style.left;
        if (e.type === "mousedown") {
            window.addEventListener("mousemove", move);
        } else {
            window.addEventListener("touchmove", move);
        }

        fabContainer.style.transition = "none";
    };

    const mouseUp = (e) => {
        if (e.type === "mouseup") {
            window.removeEventListener("mousemove", move);
        } else {
            window.removeEventListener("touchmove", move);
        }
        snapToSide(e);
        fabContainer.style.transition = "0.3s ease-in-out left";
    };

    const snapToSide = (e) => {
        windowWidth = window.innerWidth;
        let currPositionX, currPositionY;

        if (e.type === "touchend") {
            currPositionX = e.changedTouches[0].clientX;
            currPositionY = e.changedTouches[0].clientY;
        } else {
            currPositionX = e.clientX;
            currPositionY = e.clientY;
        }
        if (currPositionY < 38) {
            fabContainer.style.top = 38 + "px";
        }
        if (currPositionX < windowWidth / 2) {
            if (!isFabButtonClicked(e)) {
                return;
            }
            fabContainer.style.left = 30 + "px";
            fabContainer.classList.remove('right');
            fabContainer.classList.add('left');
        } else {
            if (!isFabButtonClicked(e)) {
                return;
            }
            fabContainer.style.left = windowWidth - 38 + "px";
            fabContainer.classList.remove('left');
            fabContainer.classList.add('right');
        }
    };

    fabContainer.addEventListener("mousedown", mouseDown);
    fabContainer.addEventListener("mouseup", mouseUp);
    fabContainer.addEventListener("touchstart", mouseDown);
    fabContainer.addEventListener("touchend", mouseUp);
    fabContainer.addEventListener("click", (e) => {
        // console.log({ oldPositionX, oldPositionY, currentX: fabContainer.style.left, currentY: fabContainer.style.top, clientX: e.clientX, clientY: e.clientY })
        if (oldPositionY === fabContainer.style.top && oldPositionX === fabContainer.style.left) {
            if (isFabButtonClicked(e)) {
                fabContainer.classList.toggle("fab-active");
                return;
            }
        }
    });
    function isFabButtonClicked(e) {
        if (e.target.id === "hanzipopup-fab-btn") {
            return true;
        }
        return false;
    }
}

function toggleMore(e) {
    infoButtonContainer.style.left = e.clientX - 40 + "px";
    if (e.clientY < 400) {
        infoButtonContainer.style.top = e.clientY + 20 + "px";
    } else {
        infoButtonContainer.style.bottom = window.innerHeight - e.clientY + 28 + "px";
    }
    infoButtonContainer.style.display = infoButtonContainer.style.display === 'none' ? 'flex' : 'none';
}

// Background color of the pop-up window
const formGroupColors = createRadioFormGroup('Popup Background Color', 'popupColor',
    [{ id: 'popupColorYellow', value: 'yellow', label: 'Yellow pop-up background' },
    { id: 'popupColorBlue', value: 'blue', label: 'Blue pop-up background' },
    { id: 'popupColorLightBlue', value: 'lightblue', label: 'Light blue pop-up background' },
    { id: 'popupColorBlack', value: 'black', label: 'Black pop-up background' }],
    { name: 'popup', color: "#de4e4e" }
);
formGroupColors.style.marginTop = '8px';

// Coloring pinyin syllables based on the tone of the character
const formGroupToneColors = createRadioFormGroup(
    'Pinyin Color',
    'toneColors',
    [{ id: 'toneColorsStandard', value: 'standard', label: 'Use the default color scheme.' },
    { id: 'toneColorsPleco', value: 'pleco', label: 'Use the <a href="https://pleco.com" target="_blank">Pleco</a> color scheme.' },
    { id: 'toneColorsHanping', value: 'hanping', label: 'Use the <a href="https://hanpingchinese.com/" target="_blank">Hanping</a> color scheme.' },
    { id: 'toneColorsNone', value: 'none', label: 'Don\'t show any tone colors.' }],
    { name: 'pinyin', color: "#34d941" }
);

// Font size of the characters in the pop-up window
const formGroupFontSize = createRadioFormGroup('Font Size', 'fontSize',
    [{ id: 'fontSizeSmall', value: 'small', label: 'Display characters in a smaller font size.' },
    { id: 'fontSizeLarge', value: 'large', label: 'Display characters in a larger font size.' }],
    { name: 'font', color: "#6272cf" }
);

// Simplified and traditional characters
const formGroupSimpTrad = createRadioFormGroup('Simplified & Traditional Characters', 'simpTrad',
    [{ id: 'simpTradClassic', value: 'classic', label: 'Display both simplified and traditional characters.' },
    { id: 'simpTradAuto', value: 'auto', label: 'Use the character style automatically detected on the page (either simplified or traditional).' }],
    { name: 'zi', color: "#ff9800" }
);

// Saving entries to the Built-in Word List
const formGroupSaveToWordList = createRadioFormGroup('Save Words', 'saveToWordList',
    [{ id: 'saveToWordListAllEntries', value: 'allEntries', label: 'Save all entries.' },
    { id: 'saveToWordListFirstEntryOnly', value: 'firstEntryOnly', label: 'Save only the first entry.' }],
    { name: 'save', color: "#3E64FF" }
);

// Saving entries to the Built-in Word List
const formGroupTtsEngine = createRadioFormGroup('Text to Speech Engine', 'cnTtsEngine',
    [{ id: 'cnTtsEngineBrowser', value: 'browser', label: 'Use Browser Speech Synthesis' },
    { id: 'cnTtsEngineEdge', value: 'msedge', label: 'Use MS Edge TTS' }],
    { name: 'tts', color: "#3E64FF" }
);

// Additional Chinese transliterations
const formGroupAdditional = createCheckboxFormGroup('optional-feature', 'Optional Feature',
    [{ id: 'zhuyin', label: 'Show <a href="https://wikipedia.org/wiki/Bopomofo" target="_blank">Zhuyin (Bopomofo)</a> phonetic symbols.' },
    { id: 'tts', label: 'Text to Speech for selected Chinese characters' },
    { id: 'prev', label: 'Select previous Chinese character' },
    { id: 'next', label: 'Select next Chinese character' },
    { id: 'more', label: 'Show more button to display additional information' }],
    { name: 'translate', color: "#ff7043" }
);

// Grammar and usage notes
const formGroupGrammarNotes = createCheckboxFormGroup('grammar-vocab', 'Grammar & Vocabulary Notes', [{ id: 'grammar', label: 'Show grammar and usage notes' }, { id: 'vocab', label: 'Show vocabulary notes' }], { name: 'book', color: "#efa22f" });

const grammarNotesInfo = document.createElement('div');
grammarNotesInfo.style.marginLeft = '4px';
grammarNotesInfo.innerHTML = 'Grammar and usage notes from the <a href="https://resources.allsetlearning.com/chinese/grammar" target="_blank">Chinese Grammar Wiki</a>.';
grammarNotesInfo.innerHTML += '<br>';
grammarNotesInfo.innerHTML += 'Vocabulary notes from the <a href="https://resources.allsetlearning.com/chinese/vocabulary" target="_blank">Chinese Vocabulary Wiki</a>.';
formGroupGrammarNotes.appendChild(grammarNotesInfo);

// More information list button
const formGroupMoreInfo = createCheckboxFormGroup('hanzipopup-more-info', 'More Info', infoButtons, { name: 'message', color: "#4caf50" });

// create header title
const headerTitle = document.createElement('div');
headerTitle.textContent = 'Options';
headerTitle.style.fontSize = '20px';
headerTitle.style.fontWeight = 'bold';

const titleContainer = document.createElement('div');
titleContainer.id = 'custom-title-container';

const closeBtn = document.createElement("button");
closeBtn.classList.add("h-icon-btn");
closeBtn.classList.add("init");
closeBtn.classList.add("close");
closeBtn.style.background = "red";
closeBtn.onclick = toggleOption;

titleContainer.appendChild(headerTitle);
titleContainer.appendChild(closeBtn);

// Create popup container element
const popupContainer = document.createElement('div');
popupContainer.id = 'custom-popup-container';
popupContainer.style.display = 'none';

// Create zhongwen config container element
const zhongwenPopupContainer = document.createElement('div');
zhongwenPopupContainer.id = 'zhongwenPopupContainer';

zhongwenPopupContainer.appendChild(formGroupColors);
zhongwenPopupContainer.appendChild(formGroupToneColors);
zhongwenPopupContainer.appendChild(formGroupFontSize);
zhongwenPopupContainer.appendChild(formGroupSimpTrad);
zhongwenPopupContainer.appendChild(formGroupSaveToWordList);
zhongwenPopupContainer.appendChild(formGroupGrammarNotes);
zhongwenPopupContainer.appendChild(formGroupAdditional);
zhongwenPopupContainer.appendChild(formGroupTtsEngine);
zhongwenPopupContainer.appendChild(formGroupMoreInfo);

popupContainer.appendChild(titleContainer);
popupContainer.appendChild(zhongwenPopupContainer);
document.body.appendChild(popupContainer);
createLangList(langList);
setupToolbar();
fabContainer.append(fabElement);
document.body.appendChild(fabContainer);

// https://github.com/cschiller/zhongwen
class ZhongwenDictionary {

    constructor(wordDict, wordIndex, grammarKeywords, vocabKeywords) {
        this.wordDict = wordDict;
        this.wordIndex = wordIndex;
        this.grammarKeywords = grammarKeywords;
        this.vocabKeywords = vocabKeywords;
        this.cache = {};
    }

    static find(needle, haystack) {

        let beg = 0;
        let end = haystack.length - 1;

        while (beg < end) {
            let mi = Math.floor((beg + end) / 2);
            let i = haystack.lastIndexOf('\n', mi) + 1;

            let mis = haystack.substr(i, needle.length);
            if (needle < mis) {
                end = i - 1;
            } else if (needle > mis) {
                beg = haystack.indexOf('\n', mi + 1) + 1;
            } else {
                return haystack.substring(i, haystack.indexOf('\n', mi + 1));
            }
        }

        return null;
    }

    hasGrammarKeyword(keyword) {
        return this.grammarKeywords[keyword];
    }

    hasVocabKeyword(keyword) {
        return this.vocabKeywords[keyword];
    }

    wordSearch(word, max) {

        let entry = { data: [] };

        let dict = this.wordDict;
        let index = this.wordIndex;

        let maxTrim = max || 7;

        let count = 0;
        let maxLen = 0;

        WHILE:
        while (word.length > 0) {

            let ix = this.cache[word];
            if (!ix) {
                ix = ZhongwenDictionary.find(word + ',', index);
                if (!ix) {
                    this.cache[word] = [];
                    continue;
                }
                ix = ix.split(',');
                this.cache[word] = ix;
            }

            for (let j = 1; j < ix.length; ++j) {
                let offset = ix[j];

                let dentry = dict.substring(offset, dict.indexOf('\n', offset));

                if (count >= maxTrim) {
                    entry.more = 1;
                    break WHILE;
                }

                ++count;
                if (maxLen === 0) {
                    maxLen = word.length;
                }

                entry.data.push([dentry, word]);
            }

            word = word.substr(0, word.length - 1);
        }

        if (entry.data.length === 0) {
            return null;
        }

        entry.matchLen = maxLen;
        return entry;
    }
}

function makeGetRequest(url, responseType = 'text') {
    return new Promise((resolve, reject) => {
        GM.xmlHttpRequest({
            method: 'GET',
            url: url,
            responseType: responseType,
            onload: function (response) {
                if (response.status === 200) {
                    resolve(response.response);
                } else {
                    reject(new Error(`Request failed with status: ${response.status}`));
                }
            },
            onerror: function (error) {
                reject(new Error(`Request failed with error: ${error}`));
            }
        });
    });
}

async function getCedict(url) {
    let arraybuffer = await makeGetRequest(url, 'arraybuffer');
    const { entries } = await unzipit.unzip(new Uint8Array(arraybuffer));
    const arrayBuffer = await entries['cedict_ts.u8'].arrayBuffer();
    const string = new TextDecoder().decode(arrayBuffer);
    return string;
}

async function loadDictData() {
    let wordDict = getCedict(`${host}/data/cedict_ts.zip`);

    // let wordDict = makeGetRequest(`${host}/data/cedict_ts.u8`);
    let wordIndex = makeGetRequest(`${host}/data/cedict.idx`);
    let grammarKeywords = makeGetRequest(`${host}/data/grammarKeywordsMin.json`, 'json');
    let vocabKeywords = makeGetRequest(`${host}/data/vocabularyKeywordsMin.json`, 'json');

    return Promise.all([wordDict, wordIndex, grammarKeywords, vocabKeywords]);
}

async function loadDictionary() {
    const [wordDict, wordIndex, grammarKeywords, vocabKeywords] = await loadDictData();
    return new ZhongwenDictionary(wordDict, wordIndex, grammarKeywords, vocabKeywords);
}

async function loadDict() {
    try {
        if (dict) {
            return;
        }
        dict = await loadDictionary().then(r => dict = r);
        console.log("Dictionary Loaded...");
    } catch (e) {
        disableTab();
        console.log(e);
    }
}

// regular expression for zero-width non-joiner U+200C &zwnj;
let zwnj = /\u200c/g;

function search(text) {

    if (!dict) {
        return;
    }

    let entry = dict.wordSearch(text);

    if (entry) {
        for (let i = 0; i < entry.data.length; i++) {
            let word = entry.data[i][1];
            if (dict.hasGrammarKeyword(word) && (entry.matchLen === word.length)) {
                // the final index should be the last one with the maximum length
                entry.grammar = { keyword: word, index: i };
            }
            if (dict.hasVocabKeyword(word) && (entry.matchLen === word.length)) {
                // the final index should be the last one with the maximum length
                entry.vocab = { keyword: word, index: i };
            }
        }
    }

    return entry;
}

// https://github.com/cschiller/zhongwen
function onMouseMove(mouseMove) {
    clickedTarget = mouseMove.target;

    if(!shouldHidePopup()) {
        return;
    }

    if (clientX && clientY) {
        if (mouseMove.clientX === clientX && mouseMove.clientY === clientY) {
            return;
        }
    }

    clientX = mouseMove.clientX;
    clientY = mouseMove.clientY;

    let range;
    let rangeNode;
    let rangeOffset;

    let moveClientX = mouseMove.clientX;
    let moveClientY = mouseMove.clientY;

    // Handle Chrome and Firefox
    if (document.caretRangeFromPoint) {
        range = document.caretRangeFromPoint(moveClientX, moveClientY);
        if (range === null) {
            return;
        }
        rangeNode = range.startContainer;
        rangeOffset = range.startOffset;
    } else if (document.caretPositionFromPoint) {
        range = document.caretPositionFromPoint(moveClientX, moveClientY);
        if (range === null) {
            return;
        }
        rangeNode = range.offsetNode;
        rangeOffset = range.offset;
    }

    if (mouseMove.target === savedTarget) {
        if (rangeNode === savedRangeNode && rangeOffset === savedRangeOffset) {
            return;
        }
    }

    if (timer) {
        clearTimeout(timer);
        timer = null;
    }

    if (rangeNode.data && rangeOffset === rangeNode.data.length) {
        rangeNode = findNextTextNode(rangeNode.parentNode, rangeNode);
        rangeOffset = 0;
    }

    if (!rangeNode || rangeNode.parentNode !== mouseMove.target) {
        rangeNode = null;
        rangeOffset = -1;
    }

    savedTarget = mouseMove.target;
    savedRangeNode = rangeNode;
    savedRangeOffset = rangeOffset;

    selStartDelta = 0;
    selStartIncrement = 1;

    if (rangeNode && rangeNode.data && rangeOffset < rangeNode.data.length) {
        popX = mouseMove.clientX;
        popY = mouseMove.clientY;
        timer = setTimeout(() => triggerSearch(), 50);
        return;
    }

    // Don't close just because we moved from a valid pop-up slightly over to a place with nothing.
    let dx = popX - mouseMove.clientX;
    let dy = popY - mouseMove.clientY;
    let distance = Math.sqrt(dx * dx + dy * dy);
    if (distance > 4) {
        clearHighlight();
        hidePopup();
    }
}

async function triggerSearch() {

    let rangeNode = savedRangeNode;
    let selStartOffset = savedRangeOffset + selStartDelta;

    selStartIncrement = 1;

    if (!rangeNode) {
        // clearHighlight();
        hidePopup();
        return 1;
    }

    if (selStartOffset < 0 || rangeNode.data.length <= selStartOffset) {
        // clearHighlight();
        hidePopup();
        return 2;
    }

    let u = rangeNode.data.charCodeAt(selStartOffset);

    let isChineseCharacter = !isNaN(u) && (
        u === 0x25CB ||
        (0x3400 <= u && u <= 0x9FFF) ||
        (0xF900 <= u && u <= 0xFAFF) ||
        (0xFF21 <= u && u <= 0xFF3A) ||
        (0xFF41 <= u && u <= 0xFF5A) ||
        (0xD800 <= u && u <= 0xDFFF)
    );

    if (!isChineseCharacter) {
        clearHighlight();
        hidePopup();
        return 3;
    }

    let selEndList = [];
    let originalText = getText(rangeNode, selStartOffset, selEndList, 30 /*maxlength*/);

    // Workaround for Google Docs: remove zero-width non-joiner &zwnj;
    let text = originalText.replace(zwnj, '');

    savedSelStartOffset = selStartOffset;
    savedSelEndList = selEndList;

    let result = search(text);
    result.originalText = originalText;

    processSearchResult(result);

    return 0;
}

async function processSearchResult(result) {

    let selStartOffset = savedSelStartOffset;
    let selEndList = savedSelEndList;

    if (!result) {
        hidePopup();
        clearHighlight();
        return;
    }

    let highlightLength;
    let index = 0;
    for (let i = 0; i < result.matchLen; i++) {
        // Google Docs workaround: determine the correct highlight length
        while (result.originalText[index] === '\u200c') {
            index++;
        }
        index++;
    }
    highlightLength = index;

    selStartIncrement = result.matchLen;
    selStartDelta = (selStartOffset - savedRangeOffset);

    let rangeNode = savedRangeNode;
    // don't try to highlight form elements
    if (!('form' in savedTarget)) {
        let doc = rangeNode.ownerDocument;
        if (!doc) {
            clearHighlight();
            hidePopup();
            return;
        }
        highlightMatch(doc, rangeNode, selStartOffset, highlightLength, selEndList);
    }

    let config = await getConfig();
    showPopup(await makeHtml(result, config.toneColorScheme), savedTarget, popX, popY, false);
}

// modifies selEndList as a side-effect
function getText(startNode, offset, selEndList, maxLength) {
    let text = '';
    let endIndex;

    if (startNode.nodeType !== Node.TEXT_NODE) {
        return '';
    }

    endIndex = Math.min(startNode.data.length, offset + maxLength);
    text += startNode.data.substring(offset, endIndex);
    selEndList.push({
        node: startNode,
        offset: endIndex
    });

    let nextNode = startNode;
    while ((text.length < maxLength) && ((nextNode = findNextTextNode(nextNode.parentNode, nextNode)) !== null)) {
        text += getTextFromSingleNode(nextNode, selEndList, maxLength - text.length);
    }

    return text;
}

// modifies selEndList as a side-effect
function getTextFromSingleNode(node, selEndList, maxLength) {
    let endIndex;

    if (node.nodeName === '#text') {
        endIndex = Math.min(maxLength, node.data.length);
        selEndList.push({
            node: node,
            offset: endIndex
        });
        return node.data.substring(0, endIndex);
    } else {
        return '';
    }
}

function highlightMatch(doc, rangeStartNode, rangeStartOffset, matchLen, selEndList) {
    if (!selEndList || selEndList.length === 0) return;

    let selEnd;
    let offset = rangeStartOffset + matchLen;

    for (let i = 0, len = selEndList.length; i < len; i++) {
        selEnd = selEndList[i];
        if (offset <= selEnd.offset) {
            break;
        }
        offset -= selEnd.offset;
    }

    let range = doc.createRange();
    range.setStart(rangeStartNode, rangeStartOffset);
    range.setEnd(selEnd.node, offset);

    let sel = window.getSelection();
    if (!sel.isCollapsed && selText !== sel.toString())
        return;
    sel.empty();
    sel.addRange(range);
    selText = sel.toString();
    return selText;
}

function getTextForClipboard() {
    let result = '';
    for (let i = 0; i < savedSearchResults.length; i++) {
        result += savedSearchResults[i].slice(0, -1).join('\t');
        result += '\n';
    }
    return result;
}

function clearHighlight() {

    if (selText === null) {
        return;
    }

    let selection = window.getSelection();
    if (selection.isCollapsed || selText === selection.toString()) {
        selection.empty();
    }
    selText = null;
}

function findNextTextNode(root, previous) {
    if (root === null) {
        return null;
    }
    let nodeIterator = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, null);
    let node = nodeIterator.nextNode();
    while (node !== previous) {
        node = nodeIterator.nextNode();
        if (node === null) {
            return findNextTextNode(root.parentNode, previous);
        }
    }
    let result = nodeIterator.nextNode();
    if (result !== null) {
        return result;
    } else {
        return findNextTextNode(root.parentNode, previous);
    }
}

function findPreviousTextNode(root, previous) {
    if (root === null) {
        return null;
    }
    let nodeIterator = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, null);
    let node = nodeIterator.nextNode();
    while (node !== previous) {
        node = nodeIterator.nextNode();
        if (node === null) {
            return findPreviousTextNode(root.parentNode, previous);
        }
    }
    nodeIterator.previousNode();
    let result = nodeIterator.previousNode();
    if (result !== null) {
        return result;
    } else {
        return findPreviousTextNode(root.parentNode, previous);
    }
}

// https://github.com/cschiller/zhongwen
function enableTab() {
    document.addEventListener('mousedown', onMouseMove);
    document.addEventListener('touchstart', onMouseMove);
    document.addEventListener('keydown', onKeyDown);
}

function disableTab() {
    document.removeEventListener('mousedown', onMouseMove);
    document.removeEventListener('touchstart', onMouseMove);
    document.removeEventListener('keydown', onKeyDown);

    let popup = document.getElementById('zhongwen-window');
    if (popup) {
        popup.parentNode.removeChild(popup);
    }

    clearHighlight();
}

function saveWordList() {
    let entries = [];
    for (let j = 0; j < savedSearchResults.length; j++) {
        let entry = {
            simplified: savedSearchResults[j][0],
            traditional: savedSearchResults[j][1],
            pinyin: savedSearchResults[j][2],
            definition: savedSearchResults[j][3]
        };
        entries.push(entry);
    }

    if (wordListWindow && !wordListWindow.closed) {
        wordListWindow.postMessage({ message: "requestResult", entries: entries }, "*");
    } else {
        wordListWindow = window.open(`${host}/wordlist.html`);
    }

    let interval = setInterval(function () {
        try {
            wordListWindow.postMessage({ message: "requestResult", entries: entries }, "*");
        } catch (e) {
            if (wordListWindow.closed) {
                clearInterval(interval);
                return;
            }
        }
    }, 500);

    window.addEventListener("message", function (event) {
        if (event.data.message === "deliverResult") {
            clearInterval(interval);
            wordListWindow.postMessage({ message: "refreshPage", refresh: true }, "*");
        }
    }, false);

    showPopup(`Added to word list.<p>Press View button to open word list.`, null, -1, -1);
}

function viewWordList() {
    // Alt + w
    if (!wordListWindow) {
        wordListWindow = window.open(`${host}/wordlist.html`);
    } else {
        wordListWindow.focus();
        wordListWindow.location.href = `${host}/wordlist.html`;
    }
}

function infoWindowOpen(url) {
    if (!infoWindow || infoWindow.closed) {
        infoWindow = window.open(url);
    } else {
        infoWindow.location.href = url;
        infoWindow.focus();
    }
}

function toggleOption() {
    popupContainer.style.display = (popupContainer.style.display === 'none' || popupContainer.style.display === '') ? 'block' : 'none';
}

async function loadVals() {
    let config = await getConfig();

    const popupColor = config['popupColor'] || 'yellow';
    document.querySelector(`input[name="popupColor"][value="${popupColor}"]`).checked = true;

    const toneColorScheme = config['toneColorScheme'] || 'standard';
    if (toneColorScheme === 'none') {
        document.querySelector('#toneColorsNone').checked = true;
    } else {
        document.querySelector(`input[name="toneColors"][value="${toneColorScheme}"]`).checked = true;
    }

    const fontSize = config['fontSize'] || 'small';
    document.querySelector(`input[name="fontSize"][value="${fontSize}"]`).checked = true;

    const simpTrad = config['simpTrad'] || 'classic';
    document.querySelector(`input[name="simpTrad"][value="${simpTrad}"]`).checked = true;

    const saveToWordList = config['saveToWordList'] || 'allEntries';
    document.querySelector(`input[name="saveToWordList"][value="${saveToWordList}"]`).checked = true;

    const cnTtsEngine = config['cnTtsEngine'] || 'browser';
    document.querySelector(`input[name="cnTtsEngine"][value="${cnTtsEngine}"]`).checked = true;

    document.querySelector('#hanzipopup-grammar').checked = config['grammar'];
    document.querySelector('#hanzipopup-vocab').checked = config['vocab'];
    document.querySelector('#hanzipopup-zhuyin').checked = config['zhuyin'];
    document.querySelector('#hanzipopup-tts').checked = config['tts'];
    document.querySelector('#hanzipopup-prev').checked = config['prev'];
    document.querySelector('#hanzipopup-next').checked = config['next'];
    document.querySelector('#hanzipopup-more').checked = config['more'];

    infoButtons.forEach(function (button) {
        document.querySelector(`#hanzipopup-${button.id}`).checked = config[button.id];
    });

    if (config['ttsvoice']) {
        document.querySelector("#voiceSelection").value = config['ttsvoice'];
    } else {
        config['ttsvoice'] = "zh-CN-XiaoxiaoNeural";
        document.querySelector("#voiceSelection").value = "zh-CN-XiaoxiaoNeural";
        await setConfig('ttsvoice', config['ttsvoice']);
    }

    if (config['more']) {
        document.querySelector("#hanzipopup-more-info").style.display = "block";
    } else {
        document.querySelector("#hanzipopup-more-info").style.display = "none";
    }
}

function shouldHidePopup() {
    if (clickedTarget.tagName === "BUTTON") {
        return false;
    }
    return true;
}

// https://github.com/cschiller/zhongwen
async function showPopup(html, elem, x, y, looseWidth) {
    let config = await getConfig();

    if (!x || !y) {
        x = y = 0;
    }

    let popup = document.getElementById('zhongwen-window');

    if (!popup) {
        popup = document.createElement('div');
        popup.setAttribute('id', 'zhongwen-window');
        document.body.appendChild(popup);
    }

    popup.style.width = 'auto';
    popup.style.height = 'auto';
    popup.style.maxWidth = (looseWidth ? '' : '600px');
    popup.className = `background-${config.popupColor} tonecolor-${config.toneColorScheme}`;

    popup.innerHTML = html;

    if (elem) {
        popup.style.top = '-1000px';
        popup.style.left = '0px';
        popup.style.display = '';

        let pW = popup.offsetWidth;
        let pH = popup.offsetHeight;

        if (pW <= 0) {
            pW = 200;
        }
        if (pH <= 0) {
            pH = 0;
            let j = 0;
            while ((j = html.indexOf('<br/>', j)) !== -1) {
                j += 5;
                pH += 22;
            }
            pH += 25;
        }

        if (altView === 1) {
            x = window.scrollX;
            y = window.scrollY;
        } else if (altView === 2) {
            x = (window.innerWidth - (pW + 20)) + window.scrollX;
            y = (window.innerHeight - (pH + 20)) + window.scrollY;
        } else if (elem instanceof window.HTMLOptionElement) {

            x = 0;
            y = 0;

            let p = elem;
            while (p) {
                x += p.offsetLeft;
                y += p.offsetTop;
                p = p.offsetParent;
            }

            if (elem.offsetTop > elem.parentNode.clientHeight) {
                y -= elem.offsetTop;
            }

            if (x + popup.offsetWidth > window.innerWidth) {
                // too much to the right, go left
                x -= popup.offsetWidth + 5;
                if (x < 0) {
                    x = 0;
                }
            } else {
                // use SELECT's width
                x += elem.parentNode.offsetWidth + 5;
            }
        } else {
            // go left if necessary
            if (x + pW > window.innerWidth - 20) {
                x = (window.innerWidth - pW) - 20;
                if (x < 0) {
                    x = 0;
                }
            }

            // below the mouse
            let v = 25;

            // go up if necessary
            if (y + v + pH > window.innerHeight) {
                let t = y - pH - 30;
                if (t >= 0) {
                    y = t;
                }
            } else {
                y += v;
            }

            x += window.scrollX;
            y += window.scrollY;
        }
    } else {
        x += window.scrollX;
        y += window.scrollY;
    }

    // (-1, -1) indicates: leave position unchanged
    if (x !== -1 && y !== -1) {
        popup.style.left = x + 'px';
        popup.style.top = y + 'px';
        popup.style.display = '';
    }
}

async function makeHtml(result, showToneColors) {
    let config = await getConfig();

    let entry;
    let html = '';
    let texts = [];
    let hanziClass;

    if (result === null) return '';

    for (let i = 0; i < result.data.length; ++i) {
        entry = result.data[i][0].match(/^([^\s]+?)\s+([^\s]+?)\s+\[(.*?)\]?\s*\/(.+)\//);

        if (!entry) continue;

        // Hanzi

        if (config.simpTrad === 'auto') {

            let word = result.data[i][1];

            hanziClass = 'w-hanzi';
            if (config.fontSize === 'small') {
                hanziClass += '-small';
            }
            html += '<span class="' + hanziClass + '">' + word + '</span>&nbsp;';

        } else {

            hanziClass = 'w-hanzi';
            if (config.fontSize === 'small') {
                hanziClass += '-small';
            }
            html += '<span class="' + hanziClass + '">' + entry[2] + '</span>&nbsp;';
            if (entry[1] !== entry[2]) {
                html += '<span class="' + hanziClass + '">' + entry[1] + '</span>&nbsp;';
            }

        }

        // Pinyin

        let pinyinClass = 'w-pinyin';
        if (config.fontSize === 'small') {
            pinyinClass += '-small';
        }
        let p = await pinyinAndZhuyin(entry[3], showToneColors, pinyinClass);
        html += p[0];

        // Zhuyin

        if (config.zhuyin) {
            html += '<br>' + p[2];
        }

        // Definition

        let defClass = 'w-def';
        if (config.fontSize === 'small') {
            defClass += '-small';
        }
        let translation = entry[4].replace(/\//g, ' ◆ ');
        html += '<br><span class="' + defClass + '">' + translation + '</span><br>';

        let addFinalBr = false;

        // Grammar
        if (config['grammar'] && result.grammar && result.grammar.index === i) {
            html += '<br><span class="grammar">Press "g" for grammar and usage notes.</span><br>';
            addFinalBr = true;
        }

        // Vocab
        if (config['vocab'] && result.vocab && result.vocab.index === i) {
            html += '<br><span class="vocab">Press "v" for vocabulary notes.</span><br>';
            addFinalBr = true;
        }

        if (addFinalBr) {
            html += '<br>';
        }

        texts[i] = [entry[2], entry[1], p[1], translation, entry[3]];
    }
    if (result.more) {
        html += '&hellip;<br/>';
    }

    savedSearchResults = texts;
    savedSearchResults.grammar = result.grammar;
    savedSearchResults.vocab = result.vocab;

    return html;
}

function hidePopup() {
    if(!shouldHidePopup()) {
        return;
    }

    let popup = document.getElementById('zhongwen-window');
    if (popup) {
        popup.style.display = 'none';
        popup.textContent = '';
    }
}

// https://github.com/cschiller/zhongwen
let tones = {
    1: '&#772;',
    2: '&#769;',
    3: '&#780;',
    4: '&#768;',
    5: ''
};

let utones = {
    1: '\u0304',
    2: '\u0301',
    3: '\u030C',
    4: '\u0300',
    5: ''
};

function parse(s) {
    return s.match(/([^AEIOU:aeiou]*)([AEIOUaeiou:]+)([^aeiou:]*)([1-5])/);
}

function tonify(vowels, tone) {
    let html = '';
    let text = '';

    if (vowels === 'ou') {
        html = 'o' + tones[tone] + 'u';
        text = 'o' + utones[tone] + 'u';
    } else {
        let tonified = false;
        for (let i = 0; i < vowels.length; i++) {
            let c = vowels.charAt(i);
            html += c;
            text += c;
            if (c === 'a' || c === 'e') {
                html += tones[tone];
                text += utones[tone];
                tonified = true;
            } else if (i === vowels.length - 1 && !tonified) {
                html += tones[tone];
                text += utones[tone];
                tonified = true;
            }
        }
        html = html.replace(/u:/, '&uuml;');
        text = text.replace(/u:/, '\u00FC');
    }

    return [html, text];
}

async function pinyinAndZhuyin(syllables, showToneColors, pinyinClass) {
    let config = await getConfig();

    let text = '';
    let html = '';
    let zhuyin = '';
    let a = syllables.split(/[\s·]+/);
    for (let i = 0; i < a.length; i++) {
        let syllable = a[i];

        // ',' in pinyin
        if (syllable === ',') {
            html += ' ,';
            text += ' ,';
            continue;
        }

        if (i > 0) {
            html += '&nbsp;';
            text += ' ';
            zhuyin += '&nbsp;';
        }
        if (syllable === 'r5') {
            if (showToneColors) {
                html += '<span class="' + pinyinClass + ' tone5">r</span>';
            } else {
                html += '<span class="' + pinyinClass + '">r</span>';
            }
            text += 'r';
            continue;
        }
        if (syllable === 'xx5') {
            if (showToneColors) {
                html += '<span class="' + pinyinClass + ' tone5">??</span>';
            } else {
                html += '<span class="' + pinyinClass + '">??</span>';
            }
            text += '??';
            continue;
        }
        let m = parse(syllable);
        if (showToneColors) {
            html += '<span class="' + pinyinClass + ' tone' + m[4] + '">';
        } else {
            html += '<span class="' + pinyinClass + '">';
        }
        let t = tonify(m[2], m[4]);
        html += m[1] + t[0] + m[3];
        html += '</span>';
        text += m[1] + t[1] + m[3];

        let zhuyinClass = 'w-zhuyin';
        if (config.fontSize === 'small') {
            zhuyinClass += '-small';
        }

        zhuyin += '<span class="tone' + m[4] + ' ' + zhuyinClass + '">'
            + numericPinyin2Zhuyin(syllable) + '</span>';
    }
    return [html, text, zhuyin];
}

const zhuyinTones = ['?', '', '\u02CA', '\u02C7', '\u02CB', '\u30FB'];

const pinyinTones = {
    1: '\u0304',
    2: '\u0301',
    3: '\u030C',
    4: '\u0300',
    5: ''
};

const zhuyinMap = {
    'a': '\u311a',
    'ai': '\u311e',
    'an': '\u3122',
    'ang': '\u3124',
    'ao': '\u3120',
    'ba': '\u3105\u311a',
    'bai': '\u3105\u311e',
    'ban': '\u3105\u3122',
    'bang': '\u3105\u3124',
    'bao': '\u3105\u3120',
    'bei': '\u3105\u311f',
    'ben': '\u3105\u3123',
    'beng': '\u3105\u3125',
    'bi': '\u3105\u3127',
    'bian': '\u3105\u3127\u3122',
    'biao': '\u3105\u3127\u3120',
    'bie': '\u3105\u3127\u311d',
    'bin': '\u3105\u3127\u3123',
    'bing': '\u3105\u3127\u3125',
    'bo': '\u3105\u311b',
    'bu': '\u3105\u3128',
    'ca': '\u3118\u311a',
    'cai': '\u3118\u311e',
    'can': '\u3118\u3122',
    'cang': '\u3118\u3124',
    'cao': '\u3118\u3120',
    'ce': '\u3118\u311c',
    'cen': '\u3118\u3123',
    'ceng': '\u3118\u3125',
    'cha': '\u3114\u311a',
    'chai': '\u3114\u311e',
    'chan': '\u3114\u3122',
    'chang': '\u3114\u3124',
    'chao': '\u3114\u3120',
    'che': '\u3114\u311c',
    'chen': '\u3114\u3123',
    'cheng': '\u3114\u3125',
    'chi': '\u3114',
    'chong': '\u3114\u3128\u3125',
    'chou': '\u3114\u3121',
    'chu': '\u3114\u3128',
    'chua': '\u3114\u3128\u311a',
    'chuai': '\u3114\u3128\u311e',
    'chuan': '\u3114\u3128\u3122',
    'chuang': '\u3114\u3128\u3124',
    'chui': '\u3114\u3128\u311f',
    'chun': '\u3114\u3128\u3123',
    'chuo': '\u3114\u3128\u311b',
    'ci': '\u3118',
    'cong': '\u3118\u3128\u3125',
    'cou': '\u3118\u3121',
    'cu': '\u3118\u3128',
    'cuan': '\u3118\u3128\u3122',
    'cui': '\u3118\u3128\u311f',
    'cun': '\u3118\u3128\u3123',
    'cuo': '\u3118\u3128\u311b',
    'da': '\u3109\u311a',
    'dai': '\u3109\u311e',
    'dan': '\u3109\u3122',
    'dang': '\u3109\u3124',
    'dao': '\u3109\u3120',
    'de': '\u3109\u311c',
    'dei': '\u3109\u311f',
    'den': '\u3109\u3123',
    'deng': '\u3109\u3125',
    'di': '\u3109\u3127',
    'dian': '\u3109\u3127\u3122',
    'diang': '\u3109\u3127\u3124',
    'diao': '\u3109\u3127\u3120',
    'die': '\u3109\u3127\u311d',
    'ding': '\u3109\u3127\u3125',
    'diu': '\u3109\u3127\u3121',
    'dong': '\u3109\u3128\u3125',
    'dou': '\u3109\u3121',
    'du': '\u3109\u3128',
    'duan': '\u3109\u3128\u3122',
    'dui': '\u3109\u3128\u311f',
    'dun': '\u3109\u3128\u3123',
    'duo': '\u3109\u3128\u311b',
    'e': '\u311c',
    'ei': '\u311f',
    'en': '\u3123',
    'er': '\u3126',
    'fa': '\u3108\u311a',
    'fan': '\u3108\u3122',
    'fang': '\u3108\u3124',
    'fei': '\u3108\u311f',
    'fen': '\u3108\u3123',
    'feng': '\u3108\u3125',
    'fo': '\u3108\u311b',
    'fou': '\u3108\u3121',
    'fu': '\u3108\u3128',
    'ga': '\u310d\u311a',
    'gai': '\u310d\u311e',
    'gan': '\u310d\u3122',
    'gang': '\u310d\u3124',
    'gao': '\u310d\u3120',
    'ge': '\u310d\u311c',
    'gei': '\u310d\u311f',
    'gen': '\u310d\u3123',
    'geng': '\u310d\u3125',
    'gong': '\u310d\u3128\u3125',
    'gou': '\u310d\u3121',
    'gu': '\u310d\u3128',
    'gua': '\u310d\u3128\u311a',
    'guai': '\u310d\u3128\u311e',
    'guan': '\u310d\u3128\u3122',
    'guang': '\u310d\u3128\u3124',
    'gui': '\u310d\u3128\u311f',
    'gun': '\u310d\u3128\u3123',
    'guo': '\u310d\u3128\u311b',
    'ha': '\u310f\u311a',
    'hai': '\u310f\u311e',
    'han': '\u310f\u3122',
    'hang': '\u310f\u3124',
    'hao': '\u310f\u3120',
    'he': '\u310f\u311c',
    'hei': '\u310f\u311f',
    'hen': '\u310f\u3123',
    'heng': '\u310f\u3125',
    'hong': '\u310f\u3128\u3125',
    'hou': '\u310f\u3121',
    'hu': '\u310f\u3128',
    'hua': '\u310f\u3128\u311a',
    'huai': '\u310f\u3128\u311e',
    'huan': '\u310f\u3128\u3122',
    'huang': '\u310f\u3128\u3124',
    'hui': '\u310f\u3128\u311f',
    'hun': '\u310f\u3128\u3123',
    'huo': '\u310f\u3128\u311b',
    'ji': '\u3110\u3127',
    'jia': '\u3110\u3127\u311a',
    'jian': '\u3110\u3127\u3122',
    'jiang': '\u3110\u3127\u3124',
    'jiao': '\u3110\u3127\u3120',
    'jie': '\u3110\u3127\u311d',
    'jin': '\u3110\u3127\u3123',
    'jing': '\u3110\u3127\u3125',
    'jiong': '\u3110\u3129\u3125',
    'jiu': '\u3110\u3127\u3121',
    'ju': '\u3110\u3129',
    'juan': '\u3110\u3129\u3122',
    'jue': '\u3110\u3129\u311d',
    'jun': '\u3110\u3129\u3123',
    'ka': '\u310e\u311a',
    'kai': '\u310e\u311e',
    'kan': '\u310e\u3122',
    'kang': '\u310e\u3124',
    'kao': '\u310e\u3120',
    'ke': '\u310e\u311c',
    'ken': '\u310e\u3123',
    'keng': '\u310e\u3125',
    'kong': '\u310e\u3128\u3125',
    'kou': '\u310e\u3121',
    'ku': '\u310e\u3128',
    'kua': '\u310e\u3128\u311a',
    'kuai': '\u310e\u3128\u311e',
    'kuan': '\u310e\u3128\u3122',
    'kuang': '\u310e\u3128\u3124',
    'kui': '\u310e\u3128\u311f',
    'kun': '\u310e\u3128\u3123',
    'kuo': '\u310e\u3128\u311b',
    'la': '\u310c\u311a',
    'lai': '\u310c\u311e',
    'lan': '\u310c\u3122',
    'lang': '\u310c\u3124',
    'lao': '\u310c\u3120',
    'le': '\u310c\u311c',
    'lei': '\u310c\u311f',
    'leng': '\u310c\u3125',
    'li': '\u310c\u3127',
    'lia': '\u310c\u3127\u311a',
    'lian': '\u310c\u3127\u3122',
    'liang': '\u310c\u3127\u3124',
    'liao': '\u310c\u3127\u3120',
    'lie': '\u310c\u3127\u311d',
    'lin': '\u310c\u3127\u3123',
    'ling': '\u310c\u3127\u3125',
    'liu': '\u310c\u3127\u3121',
    'lo': '\u310c\u311b',
    'long': '\u310c\u3128\u3125',
    'lou': '\u310c\u3121',
    'lu': '\u310c\u3128',
    'lu:': '\u310c\u3129',
    'luan': '\u310c\u3128\u3123',
    'lu:e': '\u310c\u3129\u311d',
    'lun': '\u310c\u3129',
    'lu:n': '\u310c\u3129\u3123',
    'luo': '\u310c\u3129\u3123',
    'ma': '\u3107\u311a',
    'mai': '\u3107\u311e',
    'man': '\u3107\u3122',
    'mang': '\u3107\u3124',
    'mao': '\u3107\u3120',
    'me': '\u3107\u311c',
    'mei': '\u3107\u311f',
    'men': '\u3107\u3123',
    'meng': '\u3107\u3125',
    'mi': '\u3107\u3127',
    'mian': '\u3107\u3127\u3122',
    'miao': '\u3107\u3127\u3120',
    'mie': '\u3107\u3127\u311d',
    'min': '\u3107\u3127\u3123',
    'ming': '\u3107\u3127\u3125',
    'miu': '\u3107\u3127\u3121',
    'mo': '\u3107\u311b',
    'mou': '\u3107\u3121',
    'mu': '\u3107\u3128',
    'na': '\u310b\u311a',
    'nai': '\u310b\u311e',
    'nan': '\u310b\u3122',
    'nang': '\u310b\u3124',
    'nao': '\u310b\u3120',
    'ne': '\u310b\u311c',
    'nei': '\u310b\u311f',
    'nen': '\u310b\u3123',
    'neng': '\u310b\u3125',
    'ni': '\u310b\u3127',
    'nia': '\u310b\u3127\u311a',
    'nian': '\u310b\u3127\u3122',
    'niang': '\u310b\u3127\u3124',
    'niao': '\u310b\u3127\u3120',
    'nie': '\u310b\u3127\u311d',
    'nin': '\u310b\u3127\u3123',
    'ning': '\u310b\u3127\u3125',
    'niu': '\u310b\u3127\u3121',
    'nong': '\u310b\u3128\u3125',
    'nou': '\u310b\u3121',
    'nu': '\u310b\u3128',
    'nu:': '\u310b\u3129',
    'nuan': '\u310b\u3128\u3123',
    'nu:e': '\u310b\u3129\u311d',
    'nun': '\u310b\u3129',
    'nuo': '\u310b\u3129\u311d',
    'ou': '\u3121',
    'pa': '\u3106\u311a',
    'pai': '\u3106\u311e',
    'pan': '\u3106\u3122',
    'pang': '\u3106\u3124',
    'pao': '\u3106\u3120',
    'pei': '\u3106\u311f',
    'pen': '\u3106\u3123',
    'peng': '\u3106\u3125',
    'pi': '\u3106\u3127',
    'pian': '\u3106\u3127\u3122',
    'piao': '\u3106\u3127\u3120',
    'pie': '\u3106\u3127\u311d',
    'pin': '\u3106\u3127\u3123',
    'ping': '\u3106\u3127\u3125',
    'po': '\u3106\u311b',
    'pou': '\u3106\u3121',
    'pu': '\u3106\u3128',
    'qi': '\u3111\u3127',
    'qia': '\u3111\u3127\u311a',
    'qian': '\u3111\u3127\u3122',
    'qiang': '\u3111\u3127\u3124',
    'qiao': '\u3111\u3127\u3120',
    'qie': '\u3111\u3127\u311d',
    'qin': '\u3111\u3127\u3123',
    'qing': '\u3111\u3127\u3125',
    'qiong': '\u3111\u3129\u3125',
    'qiu': '\u3111\u3127\u3121',
    'qu': '\u3111\u3129',
    'quan': '\u3111\u3129\u3122',
    'que': '\u3111\u3129\u311d',
    'qun': '\u3111\u3129\u3123',
    'ran': '\u3116\u3122',
    'rang': '\u3116\u3124',
    'rao': '\u3116\u3120',
    're': '\u3116\u311c',
    'ren': '\u3116\u3123',
    'reng': '\u3116\u3125',
    'ri': '\u3116',
    'rong': '\u3116\u3128\u3125',
    'rou': '\u3116\u3121',
    'ru': '\u3116\u3128',
    'ruan': '\u3116\u3128\u3122',
    'rui': '\u3116\u3128\u311f',
    'run': '\u3116\u3128\u3123',
    'ruo': '\u3116\u3128\u311b',
    'sa': '\u3119\u311a',
    'sai': '\u3119\u311e',
    'san': '\u3119\u3122',
    'sang': '\u3119\u3124',
    'sao': '\u3119\u3120',
    'se': '\u3119\u311c',
    'sei': '\u3119\u311f',
    'sen': '\u3119\u3123',
    'seng': '\u3119\u3125',
    'sha': '\u3115\u311a',
    'shai': '\u3115\u311e',
    'shan': '\u3115\u3122',
    'shang': '\u3115\u3124',
    'shao': '\u3115\u3120',
    'she': '\u3115\u311c',
    'shei': '\u3115\u311f',
    'shen': '\u3115\u3123',
    'sheng': '\u3115\u3125',
    'shi': '\u3115',
    'shong': '\u3115\u3128\u3125',
    'shou': '\u3115\u3121',
    'shu': '\u3115\u3128',
    'shua': '\u3115\u3128\u311a',
    'shuai': '\u3115\u3128\u311e',
    'shuan': '\u3115\u3128\u3122',
    'shuang': '\u3115\u3128\u3124',
    'shui': '\u3115\u3128\u311f',
    'shun': '\u3115\u3128\u3123',
    'shuo': '\u3115\u3128\u311b',
    'si': '\u3119',
    'song': '\u3119\u3128\u3125',
    'sou': '\u3119\u3121',
    'su': '\u3119\u3128',
    'suan': '\u3119\u3128\u3122',
    'sui': '\u3119\u3128\u311f',
    'sun': '\u3119\u3128\u3123',
    'suo': '\u3119\u3128\u311b',
    'ta': '\u310a\u311a',
    'tai': '\u310a\u311e',
    'tan': '\u310a\u3122',
    'tang': '\u310a\u3124',
    'tao': '\u310a\u3120',
    'te': '\u310a\u311c',
    'teng': '\u310a\u3125',
    'ti': '\u310a\u3127',
    'tian': '\u310a\u3127\u3122',
    'tiao': '\u310a\u3127\u3120',
    'tie': '\u310a\u3127\u311d',
    'ting': '\u310a\u3127\u3125',
    'tong': '\u310a\u3128\u3125',
    'tou': '\u310a\u3121',
    'tu': '\u310a\u3128',
    'tuan': '\u310a\u3128\u3122',
    'tui': '\u310a\u3128\u311f',
    'tun': '\u310a\u3128\u3123',
    'tuo': '\u310a\u3128\u311b',
    'wa': '\u3128\u311a',
    'wai': '\u3128\u311e',
    'wan': '\u3128\u3122',
    'wang': '\u3128\u3124',
    'wei': '\u3128\u311f',
    'wen': '\u3128\u3123',
    'weng': '\u3128\u3125',
    'wo': '\u3128\u311b',
    'wu': '\u3128',
    'xi': '\u3112\u3127',
    'xia': '\u3112\u3127\u311a',
    'xian': '\u3112\u3127\u3122',
    'xiang': '\u3112\u3127\u3124',
    'xiao': '\u3112\u3127\u3120',
    'xie': '\u3112\u3127\u311d',
    'xin': '\u3112\u3127\u3123',
    'xing': '\u3112\u3127\u3125',
    'xiong': '\u3112\u3129\u3125',
    'xiu': '\u3112\u3127\u3121',
    'xu': '\u3112\u3129',
    'xuan': '\u3112\u3129\u3122',
    'xue': '\u3112\u3129\u311d',
    'xun': '\u3112\u3129\u3123',
    'ya': '\u3127\u311a',
    'yan': '\u3127\u3122',
    'yang': '\u3127\u3124',
    'yao': '\u3127\u3120',
    'ye': '\u3127\u311d',
    'yi': '\u3127',
    'yin': '\u3127\u3123',
    'ying': '\u3127\u3125',
    'yong': '\u3129\u3125',
    'you': '\u3127\u3121',
    'yu': '\u3129',
    'yuan': '\u3129\u3122',
    'yue': '\u3129\u311d',
    'yun': '\u3129\u3123',
    'za': '\u3117\u311a',
    'zai': '\u3117\u311e',
    'zan': '\u3117\u3122',
    'zang': '\u3117\u3124',
    'zao': '\u3117\u3120',
    'ze': '\u3117\u311c',
    'zei': '\u3117\u311f',
    'zen': '\u3117\u3123',
    'zeng': '\u3117\u3125',
    'zha': '\u3113\u311a',
    'zhai': '\u3113\u311e',
    'zhan': '\u3113\u3122',
    'zhang': '\u3113\u3124',
    'zhao': '\u3113\u3120',
    'zhe': '\u3113\u311c',
    'zhei': '\u3113\u311f',
    'zhen': '\u3113\u3123',
    'zheng': '\u3113\u3125',
    'zhi': '\u3113',
    'zhong': '\u3113\u3128\u3125',
    'zhou': '\u3113\u3121',
    'zhu': '\u3113\u3128',
    'zhua': '\u3113\u3128\u311a',
    'zhuai': '\u3113\u3128\u311e',
    'zhuan': '\u3113\u3128\u3122',
    'zhuang': '\u3113\u3128\u3124',
    'zhui': '\u3113\u3128\u311f',
    'zhun': '\u3113\u3128\u3123',
    'zhuo': '\u3113\u3128\u311b',
    'zi': '\u3117',
    'zong': '\u3117\u3128\u3125',
    'zou': '\u3117\u3121',
    'zu': '\u3117\u3128',
    'zuan': '\u3117\u3128\u3122',
    'zui': '\u3117\u3128\u311f',
    'zun': '\u3117\u3128\u3123',
    'zuo': '\u3117\u3128\u311b'
};

globalThis.numericPinyin2Zhuyin = function (syllable) {
    return zhuyinMap[syllable.substring(0, syllable.length - 1).toLowerCase()]
        + zhuyinTones[syllable[syllable.length - 1]] + '</span>';

};

globalThis.accentedPinyin2Zhuyin = function (syllable) {
    let lowerCased = syllable.toLowerCase();
    let key = lowerCased;
    let tone = 5;
    for (let i = 1; i <= 4; i++) {
        let idx = lowerCased.indexOf(pinyinTones[i]);
        if (idx > 0) {
            key = lowerCased.substring(0, idx);
            if (idx < lowerCased.length - 1) {
                key += lowerCased.substring(idx + 1);
            }
            tone = i;
            break;
        }
    }
    return zhuyinMap[key] + zhuyinTones[tone];
};

/* [email protected], license MIT */
'use strict';(function(z,G){"object"===typeof exports&&"undefined"!==typeof module?G(exports):"function"===typeof define&&define.amd?define(["exports"],G):(z="undefined"!==typeof globalThis?globalThis:z||self,G(z.unzipit={}))})(this,function(z){function G(a){return a.arrayBuffer?a.arrayBuffer():new Promise((b,c)=>{const e=new FileReader;e.addEventListener("loadend",()=>{b(e.result)});e.addEventListener("error",c);e.readAsArrayBuffer(a)})}async function na(a){a=await G(a);return new Uint8Array(a)}
function aa(a){return"undefined"!==typeof Blob&&a instanceof Blob}function I(a){return"undefined"!==typeof SharedArrayBuffer&&a instanceof SharedArrayBuffer}function R(a,b){var c=a.length;if(b<=c)return a;b=new Uint8Array(Math.max(c<<1,b));b.set(a,0);return b}function oa(a,b,c,e,d,h){for(var k=ba,f=ca,l=0;l<c;){var n=a[f(e,d)&b];d+=n&15;var u=n>>>4;if(15>=u)h[l]=u,l++;else{var x=n=0;16==u?(x=3+k(e,d,2),d+=2,n=h[l-1]):17==u?(x=3+k(e,d,3),d+=3):18==u&&(x=11+k(e,d,7),d+=7);for(u=l+x;l<u;)h[l]=n,l++}}return d}
function da(a,b,c,e){for(var d=0,h=0,k=e.length>>>1;h<c;){var f=a[h+b];e[h<<1]=0;e[(h<<1)+1]=f;f>d&&(d=f);h++}for(;h<k;)e[h<<1]=0,e[(h<<1)+1]=0,h++;return d}function J(a,b){var c=a.length,e,d;var h=g.bl_count;for(d=0;d<=b;d++)h[d]=0;for(d=1;d<c;d+=2)h[a[d]]++;d=g.next_code;var k=0;h[0]=0;for(e=1;e<=b;e++)k=k+h[e-1]<<1,d[e]=k;for(b=0;b<c;b+=2)h=a[b+1],0!=h&&(a[b]=d[h],d[h]++)}function K(a,b,c){for(var e=a.length,d=g.rev15,h=0;h<e;h+=2)if(0!=a[h+1]){var k=a[h+1],f=h>>1<<4|k,l=b-k;k=a[h]<<l;for(l=k+
(1<<l);k!=l;)c[d[k]>>>15-b]=f,k++}}function ea(a,b){for(var c=g.rev15,e=15-b,d=0;d<a.length;d+=2)a[d]=c[a[d]<<b-a[d+1]]>>>e}function ba(a,b,c){return(a[b>>>3]|a[(b>>>3)+1]<<8)>>>(b&7)&(1<<c)-1}function pa(a,b,c){return(a[b>>>3]|a[(b>>>3)+1]<<8|a[(b>>>3)+2]<<16)>>>(b&7)&(1<<c)-1}function ca(a,b){return(a[b>>>3]|a[(b>>>3)+1]<<8|a[(b>>>3)+2]<<16)>>>(b&7)}function qa(a){D.push(a.target);S();const {id:b,error:c,data:e}=a.data;a=N.get(b);N.delete(b);c?a.reject(c):a.resolve(e)}function T(a){return new Promise((b,
c)=>{const e=new Worker(a);e.onmessage=d=>{"start"===d.data?(e.onerror=void 0,e.onmessage=void 0,b(e)):c(Error(`unexpected message: ${d.data}`))};e.onerror=c})}async function ra(){if(0===D.length&&U<y.numWorkers){++U;try{const a=await V.createWorker(y.workerURL);O.push(a);D.push(a);V.addEventListener(a,qa)}catch(a){P=!1}}return D.pop()}async function S(){if(0!==B.length){if(y.useWorkers&&P){var a=await ra();if(P){if(a){if(0===B.length){D.push(a);S();return}const {id:E,src:W,uncompressedSize:X,type:Y,
resolve:L,reject:sa}=B.shift();N.set(E,{id:E,resolve:L,reject:sa});a.postMessage({type:"inflate",data:{id:E,type:Y,src:W,uncompressedSize:X}},[])}return}}for(;B.length;){const {src:E,uncompressedSize:W,type:X,resolve:Y}=B.shift();a=E;aa(E)&&(a=await na(E));{var b=a;a=X;var c=Y;const L=new Uint8Array(W);var e=void 0,d=void 0,h,k=L,f=Uint8Array;if(3==b[0]&&0==b[1])k||new f(0);else{var l=pa,n=ba,u=oa,x=ca,A=null==k;A&&(k=new f(b.length>>>2<<3));for(var C=0,t=0,r=h=0,m=0;0==C;){C=l(b,m,1);var q=l(b,m+
1,2);m+=3;if(0==q)0!=(m&7)&&(m+=8-(m&7)),m=(m>>>3)+4,q=b[m-4]|b[m-3]<<8,A&&(k=R(k,r+q)),k.set(new f(b.buffer,b.byteOffset+m,q),r),m=m+q<<3,r+=q;else{A&&(k=R(k,r+131072));1==q&&(d=g.flmap,e=g.fdmap,t=511,h=31);if(2==q){q=n(b,m,5)+257;h=n(b,m+5,5)+1;e=n(b,m+10,4)+4;m+=14;for(d=0;38>d;d+=2)g.itree[d]=0,g.itree[d+1]=0;t=1;for(d=0;d<e;d++){var p=n(b,m+3*d,3);g.itree[(g.ordr[d]<<1)+1]=p;p>t&&(t=p)}m+=3*e;J(g.itree,t);K(g.itree,t,g.imap);d=g.lmap;e=g.dmap;m=u(g.imap,(1<<t)-1,q+h,b,m,g.ttree);p=da(g.ttree,
0,q,g.ltree);t=(1<<p)-1;q=da(g.ttree,q,h,g.dtree);h=(1<<q)-1;J(g.ltree,p);K(g.ltree,p,d);J(g.dtree,q);K(g.dtree,q,e)}for(;;)if(q=d[x(b,m)&t],m+=q&15,p=q>>>4,0==p>>>8)k[r++]=p;else if(256==p)break;else{q=r+p-254;264<p&&(p=g.ldef[p-257],q=r+(p>>>3)+n(b,m,p&7),m+=p&7);p=e[x(b,m)&h];m+=p&15;p=g.ddef[p>>>4];var Q=(p>>>4)+l(b,m,p&15);m+=p&15;for(A&&(k=R(k,r+131072));r<q;)k[r]=k[r++-Q],k[r]=k[r++-Q],k[r]=k[r++-Q],k[r]=k[r++-Q];r=q}}}k.length==r||k.slice(0,r)}c(a?new Blob([L],{type:a}):L.buffer)}}}}function fa(a,
b,c){return new Promise((e,d)=>{B.push({src:a,uncompressedSize:b,type:c,resolve:e,reject:d,id:ta++});S()})}async function ua(){for(const a of O)await V.terminate(a);O.splice(0,O.length);D.splice(0,D.length);B.splice(0,B.length);N.clear();U=0;P=!0}async function H(a,b,c){return await a.read(b,c)}async function Z(a,b,c,e){return a.sliceAsBlob?await a.sliceAsBlob(b,c,e):await a.read(b,c)}function v(a,b){return a[b]+256*a[b+1]}function w(a,b){return a[b]+256*a[b+1]+65536*a[b+2]+16777216*a[b+3]}function F(a,
b){return w(a,b)+4294967296*w(a,b+4)}function M(a,b){I(a.buffer)&&(a=new Uint8Array(a));return va.decode(a)}async function wa(a,b){var c=Math.min(65557,b);b-=c;var e=await H(a,b,c);for(c-=22;0<=c;--c){if(101010256!==w(e,c))continue;var d=new Uint8Array(e.buffer,e.byteOffset+c,e.byteLength-c);e=v(d,4);if(0!==e)throw Error(`multi-volume zip files are not supported. This is volume: ${e}`);e=v(d,10);const k=w(d,12),f=w(d,16);var h=v(d,20);const l=d.length-22;if(h!==l)throw Error(`invalid comment length. expected: ${l}, actual: ${h}`);
d=new Uint8Array(d.buffer,d.byteOffset+22,h);h=M(d);return 65535===e||4294967295===f?await xa(a,b+c,h,d):await ha(a,f,k,e,h,d)}throw Error("could not find end of central directory. maybe not zip file");}async function xa(a,b,c,e){b=await H(a,b-20,20);if(117853008!==w(b,0))throw Error("invalid zip64 end of central directory locator signature");b=F(b,8);var d=await H(a,b,56);if(101075792!==w(d,0))throw Error("invalid zip64 end of central directory record signature");b=F(d,32);const h=F(d,40);d=F(d,
48);return ha(a,d,h,b,c,e)}async function ha(a,b,c,e,d,h){let k=0;b=await H(a,b,c);c=[];for(let A=0;A<e;++A){var f=b.subarray(k,k+46),l=w(f,0);if(33639248!==l)throw Error(`invalid central directory file header signature: 0x${l.toString(16)}`);f={versionMadeBy:v(f,4),versionNeededToExtract:v(f,6),generalPurposeBitFlag:v(f,8),compressionMethod:v(f,10),lastModFileTime:v(f,12),lastModFileDate:v(f,14),crc32:w(f,16),compressedSize:w(f,20),uncompressedSize:w(f,24),fileNameLength:v(f,28),extraFieldLength:v(f,
30),fileCommentLength:v(f,32),internalFileAttributes:v(f,36),externalFileAttributes:w(f,38),relativeOffsetOfLocalHeader:w(f,42)};if(f.generalPurposeBitFlag&64)throw Error("strong encryption is not supported");k+=46;l=b.subarray(k,k+f.fileNameLength+f.extraFieldLength+f.fileCommentLength);f.nameBytes=l.slice(0,f.fileNameLength);f.name=M(f.nameBytes);var n=f.fileNameLength+f.extraFieldLength;const C=l.slice(f.fileNameLength,n);f.extraFields=[];for(var u=0;u<C.length-3;){const t=v(C,u+0);var x=v(C,u+
2);u+=4;x=u+x;if(x>C.length)throw Error("extra field length exceeds extra field buffer size");f.extraFields.push({id:t,data:C.slice(u,x)});u=x}f.commentBytes=l.slice(n,n+f.fileCommentLength);f.comment=M(f.commentBytes);k+=l.length;if(4294967295===f.uncompressedSize||4294967295===f.compressedSize||4294967295===f.relativeOffsetOfLocalHeader){l=f.extraFields.find(t=>1===t.id);if(!l)throw Error("expected zip64 extended information extra field");l=l.data;n=0;if(4294967295===f.uncompressedSize){if(n+8>
l.length)throw Error("zip64 extended information extra field does not include uncompressed size");f.uncompressedSize=F(l,n);n+=8}if(4294967295===f.compressedSize){if(n+8>l.length)throw Error("zip64 extended information extra field does not include compressed size");f.compressedSize=F(l,n);n+=8}if(4294967295===f.relativeOffsetOfLocalHeader){if(n+8>l.length)throw Error("zip64 extended information extra field does not include relative header offset");f.relativeOffsetOfLocalHeader=F(l,n);n+=8}}if(l=f.extraFields.find(t=>
28789===t.id&&6<=t.data.length&&1===t.data[0]&&w(t.data,1),ya.unsigned(f.nameBytes)))f.fileName=M(l.data.slice(5));if(0===f.compressionMethod&&(l=f.uncompressedSize,0!==(f.generalPurposeBitFlag&1)&&(l+=12),f.compressedSize!==l))throw Error(`compressed size mismatch for stored file: ${f.compressedSize} != ${l}`);c.push(f)}return{zip:{comment:d,commentBytes:h},entries:c.map(A=>new za(a,A))}}async function ia(a,b){if(b.generalPurposeBitFlag&1)throw Error("encrypted entries not supported");var c=await H(a,
b.relativeOffsetOfLocalHeader,30);a=await a.getLength();var e=w(c,0);if(67324752!==e)throw Error(`invalid local file header signature: 0x${e.toString(16)}`);e=v(c,26);var d=v(c,28);c=b.relativeOffsetOfLocalHeader+c.length+e+d;if(0===b.compressionMethod)e=!1;else if(8===b.compressionMethod)e=!0;else throw Error(`unsupported compression method: ${b.compressionMethod}`);d=c+b.compressedSize;if(0!==b.compressedSize&&d>a)throw Error(`file data overflows file bounds: ${c} +  ${b.compressedSize}  > ${a}`);
return{decompress:e,fileDataStart:c}}async function Aa(a,b){const {decompress:c,fileDataStart:e}=await ia(a,b);if(!c)return b=await H(a,e,b.compressedSize),0===b.byteOffset&&b.byteLength===b.buffer.byteLength?b.buffer:b.slice().buffer;a=await Z(a,e,b.compressedSize);return await fa(a,b.uncompressedSize)}async function Ba(a,b,c){const {decompress:e,fileDataStart:d}=await ia(a,b);if(!e)return b=await Z(a,d,b.compressedSize,c),aa(b)?b:new Blob([I(b.buffer)?new Uint8Array(b):b],{type:c});a=await Z(a,
d,b.compressedSize);return await fa(a,b.uncompressedSize,c)}async function ja(a){if("undefined"!==typeof Blob&&a instanceof Blob)a=new ka(a);else if(a instanceof ArrayBuffer||a&&a.buffer&&a.buffer instanceof ArrayBuffer)a=new la(a);else if(I(a)||I(a.buffer))a=new la(a);else if("string"===typeof a){var b=await fetch(a);if(!b.ok)throw Error(`failed http request ${a}, status: ${b.status}: ${b.statusText}`);a=await b.blob();a=new ka(a)}else if("function"!==typeof a.getLength||"function"!==typeof a.read)throw Error("unsupported source type");
b=await a.getLength();if(b>Number.MAX_SAFE_INTEGER)throw Error(`file too large. size: ${b}. Only file sizes up 4503599627370496 bytes are supported`);return await wa(a,b)}const Ca="undefined"!==typeof process&&process.versions&&"undefined"!==typeof process.versions.node&&"undefined"===typeof process.versions.electron;class la{constructor(a){this.typedArray=a instanceof ArrayBuffer||I(a)?new Uint8Array(a):new Uint8Array(a.buffer,a.byteOffset,a.byteLength)}async getLength(){return this.typedArray.byteLength}async read(a,
b){return new Uint8Array(this.typedArray.buffer,this.typedArray.byteOffset+a,b)}}class ka{constructor(a){this.blob=a}async getLength(){return this.blob.size}async read(a,b){a=this.blob.slice(a,a+b);a=await G(a);return new Uint8Array(a)}async sliceAsBlob(a,b,c=""){return this.blob.slice(a,a+b,c)}}class Da{constructor(a){this.url=a}async getLength(){if(void 0===this.length){const a=await fetch(this.url,{method:"HEAD"});if(!a.ok)throw Error(`failed http request ${this.url}, status: ${a.status}: ${a.statusText}`);
this.length=parseInt(a.headers.get("content-length"));if(Number.isNaN(this.length))throw Error("could not get length");}return this.length}async read(a,b){if(0===b)return new Uint8Array(0);const c=await fetch(this.url,{headers:{Range:`bytes=${a}-${a+b-1}`}});if(!c.ok)throw Error(`failed http request ${this.url}, status: ${c.status} offset: ${a} size: ${b}: ${c.statusText}`);a=await c.arrayBuffer();return new Uint8Array(a)}}const g=function(){var a=Uint16Array,b=Uint32Array;return{next_code:new a(16),
bl_count:new a(16),ordr:[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],of0:[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999],exb:[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0],ldef:new a(32),df0:[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,65535,65535],dxb:[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0],ddef:new b(32),flmap:new a(512),
fltree:[],fdmap:new a(32),fdtree:[],lmap:new a(32768),ltree:[],ttree:[],dmap:new a(32768),dtree:[],imap:new a(512),itree:[],rev15:new a(32768),lhst:new b(286),dhst:new b(30),ihst:new b(19),lits:new b(15E3),strt:new a(65536),prev:new a(32768)}}();(function(){function a(e,d,h){for(;0!=d--;)e.push(0,h)}for(var b=0;32768>b;b++){var c=b;c=(c&2863311530)>>>1|(c&1431655765)<<1;c=(c&3435973836)>>>2|(c&858993459)<<2;c=(c&4042322160)>>>4|(c&252645135)<<4;c=(c&4278255360)>>>8|(c&16711935)<<8;g.rev15[b]=(c>>>
16|c<<16)>>>17}for(b=0;32>b;b++)g.ldef[b]=g.of0[b]<<3|g.exb[b],g.ddef[b]=g.df0[b]<<4|g.dxb[b];a(g.fltree,144,8);a(g.fltree,112,9);a(g.fltree,24,7);a(g.fltree,8,8);J(g.fltree,9);K(g.fltree,9,g.flmap);ea(g.fltree,9);a(g.fdtree,32,5);J(g.fdtree,5);K(g.fdtree,5,g.fdmap);ea(g.fdtree,5);a(g.itree,19,0);a(g.ltree,286,0);a(g.dtree,30,0);a(g.ttree,320,0)})();const ma={table:function(){for(var a=new Uint32Array(256),b=0;256>b;b++){for(var c=b,e=0;8>e;e++)c=c&1?3988292384^c>>>1:c>>>1;a[b]=c}return a}(),update:function(a,
b,c,e){for(var d=0;d<e;d++)a=ma.table[(a^b[c+d])&255]^a>>>8;return a},crc:function(a,b,c){return ma.update(4294967295,a,b,c)^4294967295}},y={numWorkers:1,workerURL:"",useWorkers:!1};let ta=0,U=0,P=!0;const O=[],D=[],B=[],N=new Map,V=function(){if(Ca){const {Worker:a}=module.require?module.require("worker_threads"):{};return{async createWorker(b){return new a(b)},addEventListener(b,c){b.on("message",e=>{c({target:b,data:e})})},async terminate(b){await b.terminate()}}}return{async createWorker(a){try{return await T(a)}catch(c){console.warn("could not load worker:",
a)}let b;try{const c=await fetch(a,{mode:"cors"});if(!c.ok)throw Error(`could not load: ${a}`);b=await c.text();a=URL.createObjectURL(new Blob([b],{type:"application/javascript"}));const e=await T(a);y.workerURL=a;return e}catch(c){console.warn("could not load worker via fetch:",a)}if(void 0!==b)try{a=`data:application/javascript;base64,${btoa(b)}`;const c=await T(a);y.workerURL=a;return c}catch(c){console.warn("could not load worker via dataURI")}console.warn("workers will not be used");throw Error("can not start workers");
},addEventListener(a,b){a.addEventListener("message",b)},async terminate(a){a.terminate()}}}();class za{constructor(a,b){this._reader=a;this._rawEntry=b;this.name=b.name;this.nameBytes=b.nameBytes;this.size=b.uncompressedSize;this.compressedSize=b.compressedSize;this.comment=b.comment;this.commentBytes=b.commentBytes;this.compressionMethod=b.compressionMethod;a=b.lastModFileDate;var c=b.lastModFileTime;this.lastModDate=new Date((a>>9&127)+1980,(a>>5&15)-1,a&31,c>>11&31,c>>5&63,2*(c&31),0);this.isDirectory=
0===b.uncompressedSize&&b.name.endsWith("/");this.encrypted=!!(b.generalPurposeBitFlag&1);this.externalFileAttributes=b.externalFileAttributes;this.versionMadeBy=b.versionMadeBy}async blob(a="application/octet-stream"){return await Ba(this._reader,this._rawEntry,a)}async arrayBuffer(){return await Aa(this._reader,this._rawEntry)}async text(){const a=await this.arrayBuffer();return M(new Uint8Array(a))}async json(){const a=await this.text();return JSON.parse(a)}}const ya={unsigned(){return 0}},va=
new TextDecoder;z.HTTPRangeReader=Da;z.cleanup=function(){ua()};z.setOptions=function(a){y.workerURL=a.workerURL||y.workerURL;a.workerURL&&(y.useWorkers=!0);y.useWorkers=void 0!==a.useWorkers?a.useWorkers:y.useWorkers;y.numWorkers=a.numWorkers||y.numWorkers};z.unzip=async function(a){const {zip:b,entries:c}=await ja(a);return{zip:b,entries:Object.fromEntries(c.map(e=>[e.name,e]))}};z.unzipRaw=ja;Object.defineProperty(z,"__esModule",{value:!0})});
/* [email protected], license MIT */

/*
 Hanzipopup - A Chinese-English Pop-Up Dictionary UserScript
 Copyright (C) 2024 krmanik
 https://github.com/krmanik/hanzipopup
*/

async function setupHanzipopup() {
    await loadVals();
    await loadDict();

    let config = await getConfig();
    if (config["enable"]) {
        enable = true;
        document.querySelector("#hanzi-popup-enable-btn > button").style.background = "#33b249";
        enableTab();
    }
}

setupHanzipopup();