// ==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 ‌
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 ‌
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> ';
} else {
hanziClass = 'w-hanzi';
if (config.fontSize === 'small') {
hanziClass += '-small';
}
html += '<span class="' + hanziClass + '">' + entry[2] + '</span> ';
if (entry[1] !== entry[2]) {
html += '<span class="' + hanziClass + '">' + entry[1] + '</span> ';
}
}
// 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 += '…<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: '̄',
2: '́',
3: '̌',
4: '̀',
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:/, 'ü');
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 += ' ';
text += ' ';
zhuyin += ' ';
}
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();