├── .codeclimate.yml ├── .github └── workflows │ ├── build-safari.yml │ ├── build.yml │ ├── license.yml │ ├── submit.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── __test__ ├── background │ ├── queue.test.ts │ └── unique.test.ts ├── main │ ├── chrome.js │ ├── core │ │ ├── config.test.ts │ │ ├── entries.test.ts │ │ ├── entry.test.ts │ │ ├── generator.test.js │ │ ├── phrase.test.ts │ │ ├── transform.test.ts │ │ └── view.test.js │ └── lib │ │ ├── decoy.test.ts │ │ ├── dom.test.ts │ │ ├── shortcache.test.ts │ │ ├── storage.test.ts │ │ ├── text.test.ts │ │ └── utils.test.ts ├── options │ └── logic │ │ ├── data.test.ts │ │ ├── dictparser.test.ts │ │ ├── encoding.test.ts │ │ ├── linereader.test.ts │ │ └── resource.test.ts ├── rule.dummy.json └── testdata.ts ├── biome.json ├── codecov.yml ├── data ├── dict │ ├── a.json5 │ ├── b.json5 │ ├── c.json5 │ ├── d.json5 │ ├── e.json5 │ ├── f.json5 │ ├── g.json5 │ ├── h.json5 │ ├── i.json5 │ ├── j.json5 │ ├── k.json5 │ ├── l.json5 │ ├── m.json5 │ ├── n.json5 │ ├── o.json5 │ ├── p.json5 │ ├── q.json5 │ ├── r.json5 │ ├── s.json5 │ ├── t.json5 │ ├── u.json5 │ ├── v.json5 │ ├── w.json5 │ ├── x.json5 │ ├── y.json5 │ └── z.json5 ├── manifest │ ├── manifest-chrome.json │ ├── manifest-firefox-debug.json │ ├── manifest-firefox.json │ └── manifest-safari.json └── rule │ ├── README.md │ ├── letters.json5 │ ├── noun.json5 │ ├── phrase.json5 │ ├── pronoun.json5 │ ├── spelling.json5 │ ├── trailing.json5 │ └── verb.json5 ├── package-lock.json ├── package.json ├── src ├── README.md ├── background │ ├── background.js │ ├── queue.js │ └── unique.js ├── main │ ├── core │ │ ├── config.js │ │ ├── entry.js │ │ ├── entry │ │ │ ├── default.js │ │ │ ├── en.js │ │ │ └── ja.js │ │ ├── events.js │ │ ├── generator.js │ │ ├── launch.js │ │ ├── lookuper.js │ │ ├── pdf.js │ │ ├── resource.js │ │ ├── rule.js │ │ ├── rule │ │ │ ├── base.js │ │ │ ├── phrase.js │ │ │ ├── pronoun.js │ │ │ └── spelling.js │ │ └── view.js │ ├── env.js │ ├── lib │ │ ├── decoy.js │ │ ├── dom.js │ │ ├── draggable.js │ │ ├── edge.js │ │ ├── ponyfill │ │ │ ├── chrome.js │ │ │ ├── firefox.js │ │ │ ├── ponyfill.js │ │ │ └── safari.js │ │ ├── ribbon.js │ │ ├── shortcache.js │ │ ├── snap.js │ │ ├── sound.js │ │ ├── storage.js │ │ ├── template.js │ │ ├── text.js │ │ ├── traverser.js │ │ └── utils.js │ ├── settings.js │ └── start.js └── options │ ├── app.tsx │ ├── component │ ├── atom │ │ ├── Button.tsx │ │ ├── DataUsage.tsx │ │ ├── EditableSpan.tsx │ │ ├── ExternalLink.tsx │ │ ├── HighlightEditor.tsx │ │ ├── Launch.tsx │ │ ├── Overlay.tsx │ │ ├── Panel.tsx │ │ ├── Select.tsx │ │ ├── Switch.tsx │ │ ├── Toggle.tsx │ │ └── index.ts │ └── organism │ │ ├── AdvancedSettings.tsx │ │ ├── BasicSettings.tsx │ │ ├── LoadDictionary.tsx │ │ ├── OperationPanel.tsx │ │ ├── ReplaceRuleEditor.tsx │ │ ├── Tips.tsx │ │ ├── WholeSettings.tsx │ │ └── index.ts │ ├── extern │ ├── config.ts │ ├── env.ts │ ├── index.ts │ ├── settings.ts │ └── storage.ts │ ├── logic │ ├── data.ts │ ├── debounce.ts │ ├── dict.ts │ ├── dictparser │ │ ├── dictparser.ts │ │ ├── eijiroparser.ts │ │ ├── index.ts │ │ ├── jsondictparser.ts │ │ └── simpledictparser.ts │ ├── encoding.ts │ ├── index.ts │ ├── linereader.ts │ ├── message.ts │ ├── preview.ts │ └── resource.ts │ ├── page │ └── Main.tsx │ ├── resource │ ├── en.ts │ ├── index.ts │ ├── ja.ts │ ├── links.ts │ └── types.ts │ └── types.ts ├── static ├── base │ ├── _locales │ │ ├── en │ │ │ └── messages.json │ │ └── ja │ │ │ └── messages.json │ ├── icons │ │ ├── icon128.png │ │ ├── icon16.png │ │ ├── icon19.png │ │ └── icon48.png │ └── options │ │ ├── img │ │ ├── external.png │ │ ├── hint.png │ │ ├── loading.gif │ │ ├── logo.png │ │ ├── pdf.png │ │ ├── settings1.png │ │ └── settings2.png │ │ ├── options.css │ │ └── options.html ├── overwrite │ └── .keep └── pdf │ ├── README.md │ └── options │ └── pdf │ └── LICENSE ├── tools ├── archive.js ├── build.js ├── build.json ├── bundle_json.js ├── make_dict.js ├── make_license_html.js └── make_manifest.js ├── tsconfig.json ├── vite.config.ts └── vitest.setup.ts /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | checks: 3 | method-lines: 4 | enabled: true 5 | config: 6 | threshold: 150 7 | method-complexity: 8 | enabled: true 9 | config: 10 | threshold: 25 11 | file-lines: 12 | config: 13 | threshold: 2000 14 | return-statements: 15 | config: 16 | threshold: 10 17 | plugins: 18 | eslint: 19 | enabled: true 20 | csslint: 21 | enabled: true 22 | duplication: 23 | enabled: true 24 | config: 25 | languages: 26 | javascript: 27 | count_threshold: 3 28 | patterns: 29 | - "*.js" 30 | exclude_patterns: 31 | - "__test__/" 32 | - "tools/" 33 | - "**/*.css" 34 | -------------------------------------------------------------------------------- /.github/workflows/build-safari.yml: -------------------------------------------------------------------------------- 1 | name: Build for Safari 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | runs-on: macos-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [22.x, 23.x] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - name: Install xcrun 23 | run: sudo xcode-select -s /Library/Developer/CommandLineTools 24 | - name: Install Safari Web Extension Converter 25 | run: sudo xcode-select -s /Applications/Xcode.app 26 | - name: Install Packages 27 | run: npm install 28 | - name: Build 29 | run: npm run release-safari 30 | - name: Upload Build as Artifact 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: dist-safari-${{ matrix.node-version }} 34 | path: dist-safari 35 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | platform: [chrome, firefox] 15 | node-version: [22.x, 23.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: Install Packages 24 | run: npm install 25 | - name: Build 26 | run: npm run release-${{ matrix.platform }} 27 | - name: Upload Build as Artifact 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: dist-${{ matrix.platform }}-${{ matrix.node-version }} 31 | path: dist-${{ matrix.platform }} 32 | -------------------------------------------------------------------------------- /.github/workflows/license.yml: -------------------------------------------------------------------------------- 1 | name: License 2 | 3 | permissions: 4 | contents: write 5 | pull-requests: write 6 | 7 | on: 8 | push: 9 | tags: 10 | - "v*.*.*" 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [23.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - name: Install NPM License Checker 27 | run: npm install -g license-checker 28 | - name: Install Packages 29 | run: npm install 30 | - name: License 31 | run: license-checker --json --out license.json 32 | - name: Make HTML 33 | run: node tools/make_license_html.js license.json 34 | - name: Upload 35 | uses: softprops/action-gh-release@v2 36 | with: 37 | files: license.html 38 | -------------------------------------------------------------------------------- /.github/workflows/submit.yml: -------------------------------------------------------------------------------- 1 | name: Submit Chrome and Firefox 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | market: [chrome, firefox] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: "20.x" 22 | - name: Install Packages 23 | run: npm install 24 | - name: Build 25 | run: | 26 | npm run package-${{ matrix.market }} 27 | - name: Submit artifact 28 | uses: PlasmoHQ/bpp@v2 29 | with: 30 | artifact: mouse-dictionary-${{ matrix.market }}-{version}.zip 31 | keys: ${{ secrets.SUBMIT_KEYS }} 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [22.x, 23.x] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - name: Install codecov 23 | run: npm install -g codecov 24 | - name: Install Packages 25 | run: npm install 26 | - name: Lint 27 | run: npm run lint 28 | - name: Run Unit Tests 29 | run: npm run test-cov 30 | - name: Upload coverage to Codecov 31 | uses: codecov/codecov-action@v5 32 | with: 33 | fail_ci_if_error: true 34 | files: ./coverage/coverage-final.json 35 | flags: unittests 36 | name: codecov-coverage 37 | token: ${{ secrets.CODECOV_TOKEN }} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.tgz 3 | .*/ 4 | !.github 5 | dist/ 6 | dist-*/ 7 | *.zip 8 | coverage/ 9 | static/gen/ 10 | static/gen-*/ 11 | static/overwrite/ 12 | static/pdf/ 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present wtetsu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__test__/background/queue.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import ExpiringQueue from "../../src/background/queue"; 3 | 4 | test("", async () => { 5 | const queue = new ExpiringQueue(5); 6 | 7 | expect(queue.get("")).toEqual(null); 8 | 9 | queue.push("XXX", "XXX!"); 10 | queue.push("ZZZ", "ZZZ!"); 11 | expect(queue.shiftId()).toEqual("XXX"); 12 | expect(queue.shiftId()).toEqual("ZZZ"); 13 | expect(queue.shiftId()).toEqual(null); 14 | expect(queue.shiftId()).toEqual(null); 15 | 16 | queue.push("01", "ABC01"); 17 | queue.push("02", "ABC02"); 18 | queue.push("03", "ABC03"); 19 | 20 | expect(queue.get("01")).toEqual("ABC01"); 21 | expect(queue.get("02")).toEqual("ABC02"); 22 | expect(queue.get("03")).toEqual("ABC03"); 23 | expect(queue.get("04")).toEqual(null); 24 | 25 | expect(queue.shiftId()).toEqual("01"); 26 | expect(queue.shiftId()).toEqual("02"); 27 | 28 | expect(queue.get("01")).toEqual("ABC01"); 29 | expect(queue.get("02")).toEqual("ABC02"); 30 | expect(queue.get("03")).toEqual("ABC03"); 31 | expect(queue.get("04")).toEqual(null); 32 | 33 | await sleep(10); 34 | 35 | expect(queue.get("01")).toEqual(null); 36 | expect(queue.get("02")).toEqual(null); 37 | expect(queue.get("03")).toEqual(null); 38 | expect(queue.get("04")).toEqual(null); 39 | }); 40 | 41 | const sleep = (time: number) => { 42 | return new Promise((done) => { 43 | setTimeout(() => { 44 | done(); 45 | }, time); 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /__test__/background/unique.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import unique from "../../src/background/unique"; 3 | 4 | test("", () => { 5 | const generatedIds = new Set(); 6 | for (let i = 0; i < 1000; i++) { 7 | const newId = unique(); 8 | expect(generatedIds.has(newId)).toBe(false); 9 | generatedIds.add(newId); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /__test__/main/chrome.js: -------------------------------------------------------------------------------- 1 | class Storage { 2 | constructor() { 3 | this.data = {}; 4 | } 5 | 6 | get(keys, callback) { 7 | const result = {}; 8 | for (const key of keys) { 9 | result[key] = this.data[key]; 10 | } 11 | callback(result); 12 | } 13 | 14 | set(items, callback) { 15 | Object.assign(this.data, items); 16 | callback(); 17 | } 18 | } 19 | 20 | class Chrome { 21 | constructor() { 22 | this.runtime = { 23 | lastError: null, 24 | }; 25 | this.storage = { 26 | local: new Storage(), 27 | sync: new Storage(), 28 | }; 29 | } 30 | } 31 | 32 | export default Chrome; 33 | -------------------------------------------------------------------------------- /__test__/main/core/config.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, expect, test } from "vitest"; 2 | import config from "../../../src/main/core/config"; 3 | import env from "../../../src/main/env"; 4 | import Chrome from "../chrome"; 5 | 6 | beforeEach(() => { 7 | global.chrome = new Chrome() as any; 8 | }); 9 | 10 | afterEach(() => { 11 | env.enableUserSettings = true; 12 | env.enableWindowStatusSave = true; 13 | }); 14 | 15 | test("loadAll should return consistent settings and position", async () => { 16 | const { settings, position } = await config.loadAll(); 17 | expect(await config.loadAll()).toEqual({ settings, position }); 18 | expect(await config.loadAll()).toEqual({ settings: await config.loadSettings(), position }); 19 | 20 | env.enableUserSettings = false; 21 | expect(await config.loadAll()).toEqual({ settings, position }); 22 | expect(await config.loadAll()).toEqual({ settings: await config.loadSettings(), position }); 23 | }); 24 | 25 | test("parseSettings should handle various input cases", async () => { 26 | expect(config.parseSettings({})).toEqual({}); 27 | 28 | expect( 29 | config.parseSettings({ 30 | shortWordLength: 2, 31 | null: null, 32 | empty: "", 33 | zero: 0, 34 | normalDialogStyles: null, 35 | movingDialogStyles: "", 36 | hiddenDialogStyles: "{", 37 | }), 38 | ).toEqual({ 39 | shortWordLength: 2, 40 | empty: "", 41 | zero: 0, 42 | movingDialogStyles: null, 43 | hiddenDialogStyles: null, 44 | }); 45 | }); 46 | 47 | test("parseSettings should handle initialPosition and window status save", async () => { 48 | expect(config.parseSettings({})).toEqual({}); 49 | 50 | expect(config.parseSettings({ initialPosition: "right" })).toEqual({ initialPosition: "right" }); 51 | expect(config.parseSettings({ initialPosition: "keep" })).toEqual({ initialPosition: "keep" }); 52 | env.enableWindowStatusSave = false; 53 | expect(config.parseSettings({ initialPosition: "right" })).toEqual({ initialPosition: "right" }); 54 | expect(config.parseSettings({ initialPosition: "keep" })).toEqual({ initialPosition: "right" }); 55 | }); 56 | 57 | test("savePosition should save and load position correctly", async () => { 58 | global.chrome = chrome as any; 59 | 60 | expect((await config.loadAll()).position).toEqual({}); 61 | 62 | await config.savePosition({ x: 123, y: 345 }); 63 | expect((await config.loadAll()).position).toEqual({ x: 123, y: 345 }); 64 | 65 | env.enableUserSettings = false; 66 | await config.savePosition({ x: 999, y: 999 }); 67 | env.enableUserSettings = true; 68 | expect((await config.loadAll()).position).toEqual({ x: 123, y: 345 }); 69 | }); 70 | -------------------------------------------------------------------------------- /__test__/main/core/entry.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import entry from "../../../src/main/core/entry"; 3 | 4 | test("should detect and generate Japanese text", () => { 5 | const detector = (text: string) => { 6 | if (text[0] === "e") { 7 | return "en"; 8 | } 9 | if (text[0] === "j") { 10 | return "ja"; 11 | } 12 | if (text[0] === "z") { 13 | return "zh"; 14 | } 15 | return "default"; 16 | }; 17 | const generators = { 18 | ja: (text: string) => { 19 | return text + "(ja)"; 20 | }, 21 | en: (text: string) => { 22 | return text + "(en)"; 23 | }, 24 | zh: (text: string) => { 25 | return text + "(zh)"; 26 | }, 27 | default: (text: string) => { 28 | return text + "(default)"; 29 | }, 30 | }; 31 | 32 | const build = entry.build(detector, generators); 33 | 34 | expect({ entries: "jaaaaaaaaaaaaaaaaaaaaaaa(ja)", lang: "ja" }).toEqual(build("jaaaaaaaaaaaaaaaaaaaaaaa")); 35 | expect({ entries: "eaaaaaaaaaaaaaaaaaaaaaaa(en)", lang: "en" }).toEqual(build("eaaaaaaaaaaaaaaaaaaaaaaa")); 36 | expect({ entries: "zaaaaaaaaaaaaaaaaaaaaaaa(zh)", lang: "zh" }).toEqual(build("zaaaaaaaaaaaaaaaaaaaaaaa")); 37 | expect({ entries: "Test(default)", lang: "default" }).toEqual(build("Test")); 38 | }); 39 | -------------------------------------------------------------------------------- /__test__/main/core/generator.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import Generator from "../../../src/main/core/generator"; 3 | import defaultSettings from "../../../src/main/settings"; 4 | 5 | test("Generator should return empty HTML when no words are provided", () => { 6 | const generator = new Generator(defaultSettings); 7 | 8 | expect(generator.generate([], {}, false)).toEqual({ 9 | hitCount: 0, 10 | html: `
11 |
`, 12 | }); 13 | 14 | expect(generator.generate(["hasOwnProperty"], {}, false)).toEqual({ 15 | hitCount: 0, 16 | html: `
17 |
`, 18 | }); 19 | 20 | expect(generator.generate(["test"], { test: "テスト" }, false)).toEqual({ 21 | hitCount: 1, 22 | html: `
23 | 24 | test 25 | 26 | 🔊 27 |
28 | 29 | テスト 30 | 31 |
`, 32 | }); 33 | 34 | expect(generator.generate(["test"], { test: "テスト ■TEST" }, false)).toEqual({ 35 | hitCount: 1, 36 | html: `
37 | 38 | test 39 | 40 | 🔊 41 |
42 | 43 | テスト ■TEST 44 | 45 |
`, 46 | }); 47 | }); 48 | 49 | test("Generator should handle null search in replaceRules without error", () => { 50 | const settings = { 51 | ...defaultSettings, 52 | replaceRules: [{ search: null, replace: "xxx" }], 53 | }; 54 | new Generator(settings); // No error 55 | }); 56 | 57 | test("Generator should fail to compile regexp with invalid search pattern", () => { 58 | const settings = { 59 | ...defaultSettings, 60 | replaceRules: [{ search: "\\", replace: "xxx" }], 61 | }; 62 | 63 | // Fail to compile regexp 64 | new Generator(settings); 65 | }); 66 | -------------------------------------------------------------------------------- /__test__/main/core/phrase.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, expect, test } from "vitest"; 2 | import rule from "../../../src/main/core/rule"; 3 | import testdata from "../../testdata"; 4 | 5 | beforeAll(() => { 6 | testdata.load(); 7 | }); 8 | 9 | // expect(generateEntries("abc.")).toEqual(expect.arrayContaining(["abc"])); 10 | 11 | test("should handle simple three-word phrases", () => { 12 | expect(rule.doPhrase(["a", "b", "c"])).toEqual( 13 | expect.arrayContaining([ 14 | ["a", "~", "c"], // 15 | ["a", "__", "c"], 16 | ["a", "b", "~"], 17 | ["a", "b", "__"], 18 | ["a", "c"], 19 | ]), 20 | ); 21 | expect(rule.doPhrase(["power", "of", "100"])).toEqual( 22 | expect.arrayContaining([ 23 | ["power", "of", "__"], // 24 | ]), 25 | ); 26 | expect(rule.doPhrase(["after", "two", "weeks"])).toEqual( 27 | expect.arrayContaining([ 28 | ["after", "__", "weeks"], // 29 | ]), 30 | ); 31 | expect(rule.doPhrase(["after", "a", "lot", "of", "weeks"])).toEqual( 32 | expect.arrayContaining([ 33 | ["after", "__", "weeks"], // 34 | ]), 35 | ); 36 | }); 37 | 38 | test("should handle four-word phrases", () => { 39 | expect(rule.doPhrase(["a", "b", "c", "d"])).toEqual( 40 | expect.arrayContaining([ 41 | ["a", "~", "c", "d"], // 42 | ["a", "b", "~", "d"], 43 | ["a", "~", "d"], 44 | ["a", "__", "c", "d"], // 45 | ["a", "b", "__", "d"], 46 | ["a", "b", "c", "~"], 47 | ["a", "b", "c", "__"], 48 | ["a", "__", "d"], 49 | ["a", "A", "c", "B"], 50 | ["a", "d"], 51 | ]), 52 | ); 53 | }); 54 | 55 | test("should handle five-word phrases", () => { 56 | expect(rule.doPhrase(["a", "b", "c", "d", "e"])).toEqual( 57 | expect.arrayContaining([ 58 | ["a", "~", "c", "d", "e"], 59 | ["a", "b", "~", "d", "e"], 60 | ["a", "b", "c", "~", "e"], 61 | ["a", "~", "d", "e"], 62 | ["a", "~", "e"], 63 | ["a", "__", "e"], 64 | ["a", "b", "~", "d", "e"], 65 | ["a", "b", "~", "e"], 66 | ["a", "b", "c", "~", "e"], 67 | ["a", "A", "d", "B"], 68 | ["a", "A", "c", "B", "e"], 69 | ["a", "A", "c", "d", "B"], 70 | ["a", "b", "A", "d", "B"], 71 | ["a", "b", "c", "d", "__"], 72 | ]), 73 | ); 74 | }); 75 | 76 | test("should handle long phrases with no modifications", () => { 77 | expect( 78 | rule.doPhrase(["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t"]), 79 | ).toEqual([]); 80 | }); 81 | 82 | test("should handle specific phrase modifications", () => { 83 | expect(rule.doPhrase(["make", "some", "modifications"])).toEqual( 84 | expect.arrayContaining([ 85 | ["make", "a", "modifications"], // 86 | ]), 87 | ); 88 | expect(rule.doPhrase(["make", "thousands", "of", "modifications"])).toEqual( 89 | expect.arrayContaining([["make", "a", "modifications"]]), 90 | ); 91 | expect(rule.doPhrase(["make", "a", "lot", "of", "modifications"])).toEqual( 92 | expect.arrayContaining([["make", "a", "modifications"]]), 93 | ); 94 | expect(rule.doPhrase(["make", "some", "careful", "selections"])).toEqual( 95 | expect.arrayContaining([["make", "a", "careful", "selections"]]), 96 | ); 97 | expect(rule.doPhrase(["make", "thousands", "of", "careful", "selections"])).toEqual( 98 | expect.arrayContaining([["make", "a", "careful", "selections"]]), 99 | ); 100 | expect(rule.doPhrase(["make", "a", "lot", "of", "careful", "selections"])).toEqual( 101 | expect.arrayContaining([["make", "a", "careful", "selections"]]), 102 | ); 103 | 104 | expect(rule.doPhrase(["make", "some", "announcement"])).toEqual( 105 | expect.arrayContaining([ 106 | ["make", "an", "announcement"], // 107 | ]), 108 | ); 109 | expect(rule.doPhrase(["make", "thousands", "of", "announcement"])).toEqual( 110 | expect.arrayContaining([["make", "an", "announcement"]]), 111 | ); 112 | expect(rule.doPhrase(["make", "a", "lot", "of", "announcement"])).toEqual( 113 | expect.arrayContaining([["make", "an", "announcement"]]), 114 | ); 115 | }); 116 | -------------------------------------------------------------------------------- /__test__/main/core/transform.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, expect, test } from "vitest"; 2 | import rule from "../../../src/main/core/rule"; 3 | import testdata from "../../testdata"; 4 | 5 | beforeAll(() => { 6 | testdata.load(); 7 | }); 8 | 9 | test("should return base forms for English words", () => { 10 | expect(rule.doBase("word")).toEqual([]); 11 | expect(rule.doBase("deal")).toEqual([]); 12 | expect(rule.doBase("deals")).toEqual(["deal"]); 13 | expect(rule.doBase("dealt")).toEqual(["deal"]); 14 | expect(rule.doBase("dealing")).toEqual(["deal", "deale"]); 15 | 16 | expect(rule.doBase("run")).toEqual([]); 17 | expect(rule.doBase("runs")).toEqual(["run"]); 18 | expect(rule.doBase("ran")).toEqual(["run"]); 19 | expect(rule.doBase("running")).toEqual(["run", "runne", "runn"]); 20 | }); 21 | 22 | test("should return base forms for Japanese words", () => { 23 | expect(rule.doJa("死んだ")).toEqual(expect.arrayContaining(["死ぬ"])); 24 | expect(rule.doJa("殺った")).toEqual(expect.arrayContaining(["殺る"])); 25 | }); 26 | -------------------------------------------------------------------------------- /__test__/main/core/view.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import view from "../../../src/main/core/view"; 3 | 4 | test("should create a dialog element with the correct tag name", () => { 5 | const r = view.create({ 6 | dialogTemplate: "
", 7 | contentWrapperTemplate: "

", 8 | }); 9 | expect(r.dialog.tagName).toEqual("DIV"); 10 | expect(r.content.tagName).toEqual("P"); 11 | }); 12 | -------------------------------------------------------------------------------- /__test__/main/lib/decoy.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import decoy from "../../../src/main/lib/decoy"; 3 | import dom from "../../../src/main/lib/dom"; 4 | 5 | test("should handle input element", () => { 6 | const d = decoy.create("div"); 7 | 8 | const lines: string[] = []; 9 | lines.push(''); 10 | 11 | const elem = dom.create(lines.map((a) => a.trim()).join("")); 12 | 13 | expect(d.decoy).toEqual(null); 14 | d.deactivate(); 15 | expect(d.decoy).toEqual(null); 16 | d.activate(elem); 17 | expect(d.decoy).not.toEqual(null); 18 | expect(d.decoy.innerText).toEqual("this is text"); 19 | d.deactivate(); 20 | expect(d.decoy).toEqual(null); 21 | d.deactivate(); 22 | expect(d.decoy).toEqual(null); 23 | }); 24 | 25 | test("should handle textarea element", () => { 26 | const d = decoy.create("div"); 27 | 28 | const lines: string[] = []; 29 | lines.push(""); 30 | 31 | const elem = dom.create(lines.map((a) => a.trim()).join("")); 32 | 33 | expect(d.decoy).toEqual(null); 34 | d.deactivate(); 35 | expect(d.decoy).toEqual(null); 36 | d.activate(elem); 37 | expect(d.decoy).not.toEqual(null); 38 | expect(d.decoy.innerText).toEqual("this is text"); 39 | d.deactivate(); 40 | expect(d.decoy).toEqual(null); 41 | d.deactivate(); 42 | expect(d.decoy).toEqual(null); 43 | }); 44 | 45 | test("should handle select element", () => { 46 | const d = decoy.create("div"); 47 | 48 | const lines: string[] = []; 49 | lines.push(""); 50 | 51 | const elem = dom.create(lines.map((a) => a.trim()).join("")); 52 | 53 | expect(d.decoy).toEqual(null); 54 | d.deactivate(); 55 | expect(d.decoy).toEqual(null); 56 | d.activate(elem); 57 | expect(d.decoy).not.toEqual(null); 58 | expect(d.decoy.innerText).toEqual("this is text"); 59 | d.deactivate(); 60 | expect(d.decoy).toEqual(null); 61 | d.deactivate(); 62 | expect(d.decoy).toEqual(null); 63 | }); 64 | 65 | test("should handle div element", () => { 66 | const d = decoy.create("div"); 67 | 68 | const lines: string[] = []; 69 | lines.push("
this is text
"); 70 | 71 | const elem = dom.create(lines.map((a) => a.trim()).join("")); 72 | 73 | expect(d.decoy).toEqual(null); 74 | d.deactivate(); 75 | expect(d.decoy).toEqual(null); 76 | d.activate(elem); 77 | expect(d.decoy).toEqual(null); 78 | d.deactivate(); 79 | expect(d.decoy).toEqual(null); 80 | d.deactivate(); 81 | expect(d.decoy).toEqual(null); 82 | }); 83 | 84 | test("should handle null element", () => { 85 | const d = decoy.create(null); 86 | 87 | const lines: string[] = []; 88 | lines.push(''); 89 | 90 | const elem = dom.create(lines.map((a) => a.trim()).join("")); 91 | 92 | expect(d.decoy).toEqual(null); 93 | d.deactivate(); 94 | expect(d.decoy).toEqual(null); 95 | d.activate(elem); 96 | expect(d.decoy).toEqual(null); 97 | d.deactivate(); 98 | expect(d.decoy).toEqual(null); 99 | d.deactivate(); 100 | expect(d.decoy).toEqual(null); 101 | }); 102 | -------------------------------------------------------------------------------- /__test__/main/lib/shortcache.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import ShortCache from "../../../src/main/lib/shortcache"; 3 | 4 | test("should return null for non-existent key", () => { 5 | const cache = new ShortCache(3); 6 | 7 | expect(cache.get("key00")).toEqual(null); 8 | 9 | cache.put("key00", "val00"); 10 | cache.put("key01", "val01"); 11 | cache.put("key02", "val02"); 12 | 13 | expect(cache.get("key00")).toEqual("val00"); 14 | expect(cache.get("key01")).toEqual("val01"); 15 | expect(cache.get("key02")).toEqual("val02"); 16 | 17 | cache.put("key03", "val03"); 18 | 19 | expect(cache.get("key00")).toEqual(null); 20 | expect(cache.get("key01")).toEqual("val01"); 21 | expect(cache.get("key02")).toEqual("val02"); 22 | expect(cache.get("key03")).toEqual("val03"); 23 | 24 | cache.put("key01", "val01a"); 25 | cache.put("key02", "val02a"); 26 | 27 | expect(cache.get("key00")).toEqual(null); 28 | expect(cache.get("key01")).toEqual("val01"); 29 | expect(cache.get("key02")).toEqual("val02"); 30 | expect(cache.get("key03")).toEqual("val03"); 31 | 32 | cache.put("key03", "val03b"); 33 | cache.put("key04", "val04b"); 34 | 35 | expect(cache.get("key00")).toEqual(null); 36 | expect(cache.get("key01")).toEqual(null); 37 | expect(cache.get("key02")).toEqual("val02"); 38 | expect(cache.get("key03")).toEqual("val03"); 39 | expect(cache.get("key04")).toEqual("val04b"); 40 | 41 | cache.put("key05", "val05c"); 42 | cache.put("key06", "val06c"); 43 | 44 | expect(cache.get("key00")).toEqual(null); 45 | expect(cache.get("key01")).toEqual(null); 46 | expect(cache.get("key02")).toEqual(null); 47 | expect(cache.get("key03")).toEqual(null); 48 | expect(cache.get("key04")).toEqual("val04b"); 49 | expect(cache.get("key05")).toEqual("val05c"); 50 | expect(cache.get("key06")).toEqual("val06c"); 51 | 52 | expect(cache.get(undefined)).toEqual(null); 53 | }); 54 | -------------------------------------------------------------------------------- /__test__/main/lib/storage.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, expect, test } from "vitest"; 2 | import storage from "../../../src/main/lib/storage"; 3 | import Chrome from "../chrome"; 4 | 5 | beforeEach(() => { 6 | global.chrome = new Chrome() as any; 7 | }); 8 | 9 | afterEach(() => {}); 10 | 11 | test("should handle local storage get and set operations correctly", async () => { 12 | expect(await storage.local.get([])).toEqual({}); 13 | expect(await storage.local.get(["key01"])).toEqual({}); 14 | expect(await storage.local.pick("key01")).toEqual(undefined); 15 | 16 | await storage.local.set({}); 17 | expect(await storage.local.get(["key01"])).toEqual({}); 18 | 19 | await storage.local.set({ key01: "value01", key02: "value02" }); 20 | expect(await storage.local.get(["key01"])).toEqual({ key01: "value01" }); 21 | expect(await storage.local.get(["key01", "key02"])).toEqual({ key01: "value01", key02: "value02" }); 22 | expect(await storage.local.pick("key01")).toEqual("value01"); 23 | 24 | await storage.local.set({ key01: "value01!", key02: "value02!" }); 25 | expect(await storage.local.get(["key01"])).toEqual({ key01: "value01!" }); 26 | expect(await storage.local.get(["key01", "key02"])).toEqual({ key01: "value01!", key02: "value02!" }); 27 | expect(await storage.local.pick("key01")).toEqual("value01!"); 28 | }); 29 | 30 | test("should handle sync storage get and set operations correctly", async () => { 31 | expect(await storage.sync.get([])).toEqual({}); 32 | expect(await storage.sync.get(["key01"])).toEqual({}); 33 | expect(await storage.sync.pick("key01")).toEqual(undefined); 34 | 35 | await storage.sync.set({}); 36 | expect(await storage.sync.get(["key01"])).toEqual({}); 37 | 38 | await storage.sync.set({ key01: "value01", key02: "value02" }); 39 | expect(await storage.sync.get(["key01"])).toEqual({ key01: "value01" }); 40 | expect(await storage.sync.get(["key01", "key02"])).toEqual({ key01: "value01", key02: "value02" }); 41 | expect(await storage.sync.pick("key01")).toEqual("value01"); 42 | 43 | await storage.sync.set({ key01: "value01!", key02: "value02!" }); 44 | expect(await storage.sync.get(["key01"])).toEqual({ key01: "value01!" }); 45 | expect(await storage.sync.get(["key01", "key02"])).toEqual({ key01: "value01!", key02: "value02!" }); 46 | expect(await storage.sync.pick("key01")).toEqual("value01!"); 47 | }); 48 | 49 | test("should throw an error when local storage get operation fails", async () => { 50 | expect.hasAssertions(); 51 | global.chrome.runtime.lastError = { message: "error!" }; 52 | try { 53 | await storage.local.get([]); 54 | } catch (e) { 55 | expect(e.message).toBe("error!"); 56 | } 57 | }); 58 | 59 | test("should throw an error when sync storage get operation fails", async () => { 60 | expect.hasAssertions(); 61 | global.chrome.runtime.lastError = { message: "error!" }; 62 | try { 63 | await storage.sync.get([]); 64 | } catch (e) { 65 | expect(e.message).toBe("error!"); 66 | } 67 | }); 68 | -------------------------------------------------------------------------------- /__test__/options/logic/data.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import defaultsettings from "../../../src/main/settings"; 3 | import * as data from "../../../src/options/logic/data"; 4 | import type { MouseDictionarySettings } from "../../../src/options/types"; 5 | 6 | test("preProcessSettings and postProcessSettings should correctly process settings", () => { 7 | const d1 = defaultsettings as MouseDictionarySettings; 8 | const d2 = data.preProcessSettings(d1); 9 | const d3 = data.postProcessSettings(d1); 10 | expect(false).toEqual(d1 === d2); 11 | expect(false).toEqual(d2 === d3); 12 | 13 | expect(0).toEqual(d1.replaceRules.filter((r) => r.key).length); 14 | expect(d1.replaceRules.length).toEqual(d1.replaceRules.length); 15 | expect(d2.replaceRules.length).toEqual(d2.replaceRules.filter((r) => r.key).length); 16 | expect(0).toEqual(d3.replaceRules.filter((r) => r.key).length); 17 | 18 | expect(JSON.stringify(d1)).toEqual(JSON.stringify(d3)); 19 | }); 20 | -------------------------------------------------------------------------------- /__test__/options/logic/dictparser.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { EijiroParser, JsonDictParser, SimpleDictParser } from "../../../src/options/logic/dictparser"; 3 | 4 | test("", () => { 5 | const parser = new EijiroParser(); 6 | 7 | let hd: any; 8 | hd = parser.addLine("■when {代名} : **"); 9 | expect(hd).toEqual(undefined); 10 | hd = parser.addLine("■when {名} : 〔*******〕*****"); 11 | expect(hd).toEqual(undefined); 12 | hd = parser.addLine("■when : 【レベル】**********"); 13 | expect(hd).toEqual(undefined); 14 | 15 | hd = parser.addLine("■アイウエオ {人名} : あいうえお〔火〕"); 16 | expect(hd.head).toEqual("when"); 17 | expect(hd.desc).toEqual("{代名} : **\n{名} : 〔*******〕*****\n【レベル】**********"); 18 | 19 | hd = parser.addLine("■がぎぐげご〔動物が〕 {形} : ざじずぜぞ"); 20 | expect(hd.head).toEqual("アイウエオ"); 21 | expect(hd.desc).toEqual("{人名} : あいうえお〔火〕"); 22 | 23 | hd = parser.addLine("■tile {自動} : 《コ》〔******〕********"); 24 | expect(hd.head).toEqual("がぎぐげご"); 25 | expect(hd.desc).toEqual("〔動物が〕 {形} : ざじずぜぞ"); 26 | 27 | hd = parser.addLine("■tile {他動-1} : ****"); 28 | expect(hd).toEqual(undefined); 29 | 30 | hd = parser.addLine("# invalid line"); 31 | expect(hd).toEqual(undefined); 32 | 33 | hd = parser.addLine("■ invalid line"); 34 | expect(hd).toEqual(undefined); 35 | 36 | hd = parser.flush(); 37 | expect(hd).toEqual({ tile: "{自動} : 《コ》〔******〕********\n{他動-1} : ****" }); 38 | }); 39 | 40 | test("", () => { 41 | const parser = new SimpleDictParser(" /// "); 42 | 43 | let hd: any; 44 | hd = parser.addLine("aaa///bbb"); 45 | expect(hd).toEqual(undefined); 46 | 47 | hd = parser.addLine("aaa /// bbb"); 48 | expect(hd.head).toEqual("aaa"); 49 | expect(hd.desc).toEqual("bbb"); 50 | 51 | hd = parser.addLine("bbb///ccc"); 52 | expect(hd).toEqual(undefined); 53 | 54 | hd = parser.addLine("bbb /// ccc"); 55 | expect(hd.head).toEqual("bbb"); 56 | expect(hd.desc).toEqual("ccc"); 57 | 58 | expect(parser.flush()).toEqual(undefined); 59 | }); 60 | 61 | test("", () => { 62 | const parser = new JsonDictParser(); 63 | 64 | parser.addLine("{"); 65 | parser.addLine('"key1":"val1",'); 66 | parser.addLine('"key2":"val2",'); 67 | parser.addLine('"key3":"val3"'); 68 | parser.addLine("}"); 69 | 70 | const r = parser.flush(); 71 | expect(r.key1).toEqual("val1"); 72 | expect(r.key2).toEqual("val2"); 73 | expect(r.key3).toEqual("val3"); 74 | }); 75 | -------------------------------------------------------------------------------- /__test__/options/logic/linereader.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { LineReader } from "../../../src/options/logic/linereader"; 3 | 4 | test("", () => { 5 | const reader = new LineReader(""); 6 | 7 | expect(reader.getLine).toThrowError(); 8 | expect(reader.next()).toEqual(true); 9 | expect(reader.getLine()).toEqual(""); 10 | expect(reader.next()).toEqual(false); 11 | expect(reader.getLine).toThrowError(); 12 | expect(reader.next()).toEqual(false); 13 | expect(reader.getLine).toThrowError(); 14 | expect(reader.next()).toEqual(false); 15 | expect(reader.getLine).toThrowError(); 16 | }); 17 | 18 | test("", () => { 19 | const reader = new LineReader("aaa\nbbb\nccc"); 20 | 21 | expect(reader.getLine).toThrowError(); 22 | expect(reader.next()).toEqual(true); 23 | expect(reader.getLine()).toEqual("aaa"); 24 | expect(reader.next()).toEqual(true); 25 | expect(reader.getLine()).toEqual("bbb"); 26 | expect(reader.next()).toEqual(true); 27 | expect(reader.getLine()).toEqual("ccc"); 28 | expect(reader.next()).toEqual(false); 29 | expect(reader.getLine).toThrowError(); 30 | expect(reader.next()).toEqual(false); 31 | expect(reader.getLine).toThrowError(); 32 | expect(reader.next()).toEqual(false); 33 | expect(reader.getLine).toThrowError(); 34 | }); 35 | 36 | test("", () => { 37 | const reader = new LineReader("aaa\nbbb\nccc\n"); 38 | 39 | expect(reader.getLine).toThrowError(); 40 | expect(reader.next()).toEqual(true); 41 | expect(reader.getLine()).toEqual("aaa"); 42 | expect(reader.next()).toEqual(true); 43 | expect(reader.getLine()).toEqual("bbb"); 44 | expect(reader.next()).toEqual(true); 45 | expect(reader.getLine()).toEqual("ccc"); 46 | expect(reader.next()).toEqual(false); 47 | expect(reader.getLine).toThrowError(); 48 | expect(reader.next()).toEqual(false); 49 | expect(reader.getLine).toThrowError(); 50 | expect(reader.next()).toEqual(false); 51 | expect(reader.getLine).toThrowError(); 52 | }); 53 | 54 | test("", () => { 55 | const reader = new LineReader("aaa\nbbb\nccc\n\n"); 56 | 57 | expect(reader.getLine).toThrowError(); 58 | expect(reader.next()).toEqual(true); 59 | expect(reader.getLine()).toEqual("aaa"); 60 | expect(reader.next()).toEqual(true); 61 | expect(reader.getLine()).toEqual("bbb"); 62 | expect(reader.next()).toEqual(true); 63 | expect(reader.getLine()).toEqual("ccc"); 64 | expect(reader.next()).toEqual(true); 65 | expect(reader.getLine()).toEqual(""); 66 | expect(reader.next()).toEqual(false); 67 | expect(reader.getLine).toThrowError(); 68 | expect(reader.next()).toEqual(false); 69 | expect(reader.getLine).toThrowError(); 70 | expect(reader.next()).toEqual(false); 71 | expect(reader.getLine).toThrowError(); 72 | }); 73 | 74 | test("", () => { 75 | const reader = new LineReader("aaa\r\nbbb\r\nccc"); 76 | 77 | expect(reader.getLine).toThrowError(); 78 | expect(reader.next()).toEqual(true); 79 | expect(reader.getLine()).toEqual("aaa"); 80 | expect(reader.next()).toEqual(true); 81 | expect(reader.getLine()).toEqual("bbb"); 82 | expect(reader.next()).toEqual(true); 83 | expect(reader.getLine()).toEqual("ccc"); 84 | expect(reader.next()).toEqual(false); 85 | expect(reader.getLine).toThrowError(); 86 | expect(reader.next()).toEqual(false); 87 | expect(reader.getLine).toThrowError(); 88 | expect(reader.next()).toEqual(false); 89 | expect(reader.getLine).toThrowError(); 90 | }); 91 | 92 | test("", () => { 93 | const reader = new LineReader("aaabbbccc"); 94 | 95 | expect(reader.getLine).toThrowError(); 96 | expect(reader.next()).toEqual(true); 97 | expect(reader.getLine()).toEqual("aaabbbccc"); 98 | expect(reader.next()).toEqual(false); 99 | expect(reader.getLine).toThrowError(); 100 | expect(reader.next()).toEqual(false); 101 | expect(reader.getLine).toThrowError(); 102 | expect(reader.next()).toEqual(false); 103 | expect(reader.getLine).toThrowError(); 104 | }); 105 | 106 | test("", () => { 107 | const reader = new LineReader("aaabbbccc\n"); 108 | 109 | expect(reader.getLine).toThrowError(); 110 | expect(reader.next()).toEqual(true); 111 | expect(reader.getLine()).toEqual("aaabbbccc"); 112 | expect(reader.next()).toEqual(false); 113 | expect(reader.getLine).toThrowError(); 114 | expect(reader.next()).toEqual(false); 115 | expect(reader.getLine).toThrowError(); 116 | expect(reader.next()).toEqual(false); 117 | expect(reader.getLine).toThrowError(); 118 | }); 119 | -------------------------------------------------------------------------------- /__test__/options/logic/resource.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import * as res from "../../../src/options/logic/resource"; 3 | import type { TextResourceKeys } from "../../../src/options/resource"; 4 | 5 | test("", () => { 6 | expect(res.decideInitialLanguage([])).toEqual("en"); 7 | expect(res.decideInitialLanguage(["en", "ja"])).toEqual("en"); 8 | expect(res.decideInitialLanguage(["ja", "en"])).toEqual("ja"); 9 | expect(res.decideInitialLanguage(["fr", "ja", "en"])).toEqual("ja"); 10 | expect(res.decideInitialLanguage(["en-US", "ja"])).toEqual("en"); 11 | expect(res.decideInitialLanguage(["en-UK", "ja"])).toEqual("en"); 12 | expect(res.decideInitialLanguage(["ja-JP", "en"])).toEqual("ja"); 13 | 14 | res.setLang("ja"); 15 | expect(res.get("selectDictFile")).toEqual("辞書ファイルを選択してください。"); 16 | expect(res.get("finishRegister", { count: 999 })).toEqual("登録完了(999語)"); 17 | expect(res.get("progressRegister", { count: 999, progress: "hello" })).toEqual("999語登録(hello)"); 18 | expect(res.get("invalidKey" as TextResourceKeys)).toEqual("invalidKey"); 19 | 20 | res.setLang("en"); 21 | expect(res.get("selectDictFile")).toEqual("Select dictionary data"); 22 | expect(res.get("finishRegister", { count: 999 })).toEqual("Loading has finished(999 words)"); 23 | expect(res.get("progressRegister", { count: 999, progress: "hello" })).toEqual( 24 | "999 words have been registered(hello)", 25 | ); 26 | expect(res.get("invalidKey" as TextResourceKeys)).toEqual("invalidKey"); 27 | 28 | res.setLang("invalid_language"); 29 | expect(res.get("selectDictFile")).toEqual("selectDictFile"); 30 | expect(res.get("finishRegister", { count: 999 })).toEqual("finishRegister"); 31 | expect(res.get("progressRegister", { count: 999, progress: "hello" })).toEqual("progressRegister"); 32 | expect(res.get("invalidKey" as TextResourceKeys)).toEqual("invalidKey"); 33 | }); 34 | -------------------------------------------------------------------------------- /__test__/rule.dummy.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /__test__/testdata.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import json5 from "json5"; 3 | const jaRule = require("deinja/src/data"); 4 | import rule from "../src/main/core/rule"; 5 | 6 | const load = () => { 7 | rule.registerRuleData({ 8 | letters: readJson("letters.json5"), 9 | noun: readJson("noun.json5"), 10 | phrase: readJson("phrase.json5"), 11 | pronoun: readJson("pronoun.json5"), 12 | spelling: readJson("spelling.json5"), 13 | trailing: readJson("trailing.json5"), 14 | verb: readJson("verb.json5"), 15 | ja: jaRule, 16 | }); 17 | }; 18 | 19 | const readJson = (fileName: string) => { 20 | const json = fs.readFileSync(`data/rule/${fileName}`, "utf8"); 21 | return json5.parse(json); 22 | }; 23 | 24 | export default { load }; 25 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "organizeImports": { 3 | "enabled": true 4 | }, 5 | "formatter": { 6 | "enabled": true, 7 | "indentStyle": "space", 8 | "lineWidth": 120 9 | }, 10 | "linter": { 11 | "enabled": true, 12 | "rules": { 13 | "style": { 14 | "useTemplate": "off" 15 | }, 16 | "correctness": { 17 | "useExhaustiveDependencies": "off" 18 | }, 19 | "a11y": { 20 | "noLabelWithoutControl": "off", 21 | "useKeyWithClickEvents": "off", 22 | "useAltText": "off", 23 | "useValidAnchor": "off", 24 | "noAutofocus": "off" 25 | }, 26 | "suspicious": { 27 | "noExplicitAny": "off", 28 | "noArrayIndexKey": "off" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | threshold: 80% 6 | patch: 7 | default: 8 | enabled: false 9 | -------------------------------------------------------------------------------- /data/dict/x.json5: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | * 6 | * This data is based on ejdict-hand 7 | * https://github.com/kujirahand/EJDict 8 | */ 9 | { 10 | "x": "x-axis\n〈C〉英語アルファベットの第24字 / 〈C〉X字形[のもの] / 〈U〉ローマ数字の10:IX=9,XV=15,LX=60,XC=90 / 〈U〉〈C〉(数学で)第1未知数(量) / 〈C〉未知(未定)の人(物・要素など) / 〈C〉X印(読み書きのできない人が署名の代りに用いたり,または地図・図表上の特定の地点を示したりするのに用いる符号) / 〈C〉(手紙などの終わりに書く)キスの印 / 〈C〉(18歳未満は禁止の)成人映画", 11 | "x-axis": "(グラフの)x軸,横軸", 12 | "x-radiation": "X線放射(照射)", 13 | "x-ray tube": "X線管", 14 | "xebec": "3本マストの小型帆船(昔,地中海で海賊が使用した)", 15 | "xenon": "キセノン(希ガス元素;化学記号はXe)", 16 | "xenophobia": "外国[人]ぎらい", 17 | "xenophobic": "外国[人]ぎらいの", 18 | "xeric": "乾燥した,湿度の低い", 19 | "xi": "クシー(ギリシア語アルファベットの第14字Ξ,ξ;英語のX,xに相当)", 20 | "xylem": "(植物の維管束の)木[質]部", 21 | "xylophone": "『木琴』,シロフォン", 22 | "X": "Christ / Christian\n〈C〉英語アルファベットの第24字 / 〈C〉X字形[のもの] / 〈U〉ローマ数字の10:IX=9,XV=15,LX=60,XC=90 / 〈U〉〈C〉(数学で)第1未知数(量) / 〈C〉未知(未定)の人(物・要素など) / 〈C〉X印(読み書きのできない人が署名の代りに用いたり,または地図・図表上の特定の地点を示したりするのに用いる符号) / 〈C〉(手紙などの終わりに書く)キスの印 / 〈C〉(18歳未満は禁止の)成人映画", 23 | "X ray": "〈C〉〈U〉《通例複数形で》『X線』,レントゲン線 / 〈C〉X線(レントゲン)写真,X線検査 / 『X線の』,レントゲンの / …‘を'X線で調べる(治療をする);…‘の'X線写真を撮る", 24 | "X-chromosome": "X染色体", 25 | "X-rate": "〈映画・本など〉‘を'成人向きに指定する", 26 | "X-rated": "(映画が)成人向けの", 27 | "X-ray": "〈C〉〈U〉《通例複数形で》『X線』,レントゲン線 / 〈C〉X線(レントゲン)写真,X線検査 / 『X線の』,レントゲンの / …‘を'X線で調べる(治療をする);…‘の'X線写真を撮る", 28 | "X-ray technician": "レントゲン[写真]技師(《英》radiographer)", 29 | "XL": "extra large", 30 | "Xanthippe": "クサンティッペ(Socratesの妻;悪妻の典型とされる) / 〈C〉口やかましい女,悪妻", 31 | "Xavier": "ザビエル(St. Francisco Xavier;1506‐52:スペイン人の宣教師で,1549年に初めて日本にキリスト教を伝えた)", 32 | "Xe": "xenonの化学記号", 33 | "Xenophon": "クセノフォン(434?‐355?B.C.;ギリシアの歴史家)", 34 | "Xerox": "ゼロックス(複写印刷および複写機の商標名) / …‘を'ゼロックスで複写する", 35 | "Xmas": "『クリスマス』", 36 | } 37 | -------------------------------------------------------------------------------- /data/manifest/manifest-chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mouse Dictionary", 3 | "manifest_version": 3, 4 | "version": "", 5 | "description": "__MSG_appDesc__", 6 | "default_locale": "en", 7 | "author": "wtetsu", 8 | "options_ui": { 9 | "page": "options/options.html", 10 | "open_in_tab": true 11 | }, 12 | "permissions": ["storage", "unlimitedStorage", "activeTab", "scripting"], 13 | "background": { 14 | "service_worker": "background.js" 15 | }, 16 | "action": { 17 | "default_icon": "icons/icon19.png", 18 | "default_title": "Mouse Dictionary" 19 | }, 20 | "commands": { 21 | "_execute_action": { 22 | "description": "Activate the extension" 23 | }, 24 | "scroll_down": { 25 | "description": "__MSG_scrollDown__" 26 | }, 27 | "scroll_up": { 28 | "description": "__MSG_scrollUp__" 29 | } 30 | }, 31 | "icons": { 32 | "16": "icons/icon16.png", 33 | "48": "icons/icon48.png", 34 | "128": "icons/icon128.png" 35 | }, 36 | "web_accessible_resources": [ 37 | { 38 | "resources": ["data/rule.json", "data/dict*.json"], 39 | "matches": [""] 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /data/manifest/manifest-firefox-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "browser_specific_settings": { 3 | "gecko": { 4 | "id": "dummy@example.com" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /data/manifest/manifest-firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Mouse Dictionary", 4 | "version": "", 5 | "description": "__MSG_appDesc__", 6 | "default_locale": "en", 7 | "author": "wtetsu", 8 | "options_ui": { 9 | "page": "options/options.html", 10 | "open_in_tab": true 11 | }, 12 | "permissions": ["storage", "unlimitedStorage", "activeTab"], 13 | "background": { 14 | "scripts": ["background.js"] 15 | }, 16 | "browser_action": { 17 | "default_icon": "icons/icon19.png", 18 | "default_title": "Mouse Dictionary" 19 | }, 20 | "commands": { 21 | "_execute_browser_action": { 22 | "description": "Activate the extension" 23 | }, 24 | "scroll_down": { 25 | "description": "__MSG_scrollDown__" 26 | }, 27 | "scroll_up": { 28 | "description": "__MSG_scrollUp__" 29 | } 30 | }, 31 | "icons": { 32 | "16": "icons/icon16.png", 33 | "48": "icons/icon48.png", 34 | "128": "icons/icon128.png" 35 | }, 36 | "web_accessible_resources": ["data/*.json"] 37 | } 38 | -------------------------------------------------------------------------------- /data/manifest/manifest-safari.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Mouse Dictionary", 4 | "version": "", 5 | "description": "__MSG_appDesc__", 6 | "default_locale": "en", 7 | "author": "wtetsu", 8 | "options_ui": { 9 | "page": "options/options.html", 10 | "open_in_tab": true 11 | }, 12 | "permissions": ["storage", "unlimitedStorage", "activeTab"], 13 | "background": { 14 | "scripts": ["background.js"], 15 | "persistent": false 16 | }, 17 | "browser_action": { 18 | "default_icon": "icons/icon19.png", 19 | "default_title": "Mouse Dictionary" 20 | }, 21 | "commands": { 22 | "_execute_browser_action": { 23 | "description": "Activate the extension" 24 | } 25 | }, 26 | "icons": { 27 | "16": "icons/icon16.png", 28 | "48": "icons/icon48.png", 29 | "128": "icons/icon128.png" 30 | }, 31 | "web_accessible_resources": ["data/*.json"] 32 | } 33 | -------------------------------------------------------------------------------- /data/rule/README.md: -------------------------------------------------------------------------------- 1 | Note: 2 | 3 | These JSON5 files are transformed into one "data/rule.json" file in build time. And Mouse Dictionary loads this rule.js when its start-up process. 4 | 5 | Advantages of this mechanism: 6 | * For better UX. The load process is asynchronous. Mouse Dictionary can show its window before finishing to load and process rule.json 7 | * [JSON can be parsed more efficiently than JavaScript.](https://v8.dev/blog/cost-of-javascript-2019) 8 | * Can decouple settings and processes. -------------------------------------------------------------------------------- /data/rule/noun.json5: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | // This data is based on ejdic-hand. 7 | [ 8 | ["abaci", "abacus"], 9 | ["addenda", "addendum"], 10 | ["alumni", "alumnus"], 11 | ["apices", "apex"], 12 | ["appendices", "appendix"], 13 | ["automata", "automaton"], 14 | ["axes", "axis"], 15 | ["barbeque", "barbecue"], 16 | ["beaux", "beau"], 17 | ["beeves", "beef"], 18 | ["bronchi", "bronchus"], 19 | ["bureaux", "bureau"], 20 | ["busses", "bus"], 21 | ["cacti", "cactus"], 22 | ["calves", "calf"], 23 | ["calyces", "calyx"], 24 | ["cantharides", "cantharis"], 25 | ["cherubim", "cherub"], 26 | ["children", "child"], 27 | ["cola", "colon"], 28 | ["concerti", "concerto"], 29 | ["corpora", "corpus"], 30 | ["criteria", "criterion"], 31 | ["dicta", "dictum"], 32 | ["elves", "elf"], 33 | ["errata", "erratum"], 34 | ["feet", "foot"], 35 | ["flagella", "flagellum"], 36 | ["foci", "focus"], 37 | ["fora", "forum"], 38 | ["frolick", "frolic"], 39 | ["frusta", "frustum"], 40 | ["fungi", "fungus"], 41 | ["geese", "goose"], 42 | ["genera", "genus"], 43 | ["genii", "genie"], 44 | ["halfpence", "halfpenny"], 45 | ["halves", "half"], 46 | ["hooves", "hoof"], 47 | ["indices", "index"], 48 | ["jinn", "jinni"], 49 | ["knives", "knife"], 50 | ["leaves", "leaf"], 51 | ["lei", "leu"], 52 | ["lice", "louse"], 53 | ["lives", "life"], 54 | ["loaves", "loaf"], 55 | ["loci", "locus"], 56 | ["maria", "mare"], 57 | ["matrices", "matrix"], 58 | ["maxima", "maximum"], 59 | ["memoranda", "memorandum"], 60 | ["men", "man"], 61 | ["mesdames", "madame"], 62 | ["mesdemoiselles", "mademoiselle"], 63 | ["messieurs", "monsieur"], 64 | ["mice", "mouse"], 65 | ["micra", "micron"], 66 | ["minima", "minimum"], 67 | ["millennial", "millenarian"], 68 | ["nimbi", "nimbus"], 69 | ["nuclei", "nucleus"], 70 | ["octopi", "octopus"], 71 | ["ova", "ovum"], 72 | ["oxen", "ox"], 73 | ["pease", "pea"], 74 | ["pelves", "pelvis"], 75 | ["pence", "penny"], 76 | ["phenomena", "phenomenon"], 77 | ["pix", "pic"], 78 | ["plena", "plenum"], 79 | ["quanta", "quantum"], 80 | ["radii", "radius"], 81 | ["sancta", "sanctum"], 82 | ["selves", "self"], 83 | ["sera", "serum"], 84 | ["seraphim", "seraph"], 85 | ["sheaves", "sheaf"], 86 | ["shelves", "shelf"], 87 | ["sox", "sock"], 88 | ["spectra", "spectrum"], 89 | ["staves", "staff"], 90 | ["stomata", "stoma"], 91 | ["stimuli", "stimulus"], 92 | ["strata", "stratum"], 93 | ["strati", "stratus"], 94 | ["syllabi", "syllabus"], 95 | ["tableaux", "tableau"], 96 | ["teeth", "tooth"], 97 | ["termini", "terminus"], 98 | ["testes", "testis"], 99 | ["thieves", "thief"], 100 | ["tympana", "tympanum"], 101 | ["ultimata", "ultimatum"], 102 | ["vertices", "vertex"], 103 | ["vortices", "vortex"], 104 | ["wharves", "wharf"], 105 | ["wives", "wife"], 106 | ["wolves", "wolf"], 107 | ["women", "woman"], 108 | ["yourselves", "yourself"], 109 | ["Messeigneurs", "Monseigneur"], 110 | ] 111 | -------------------------------------------------------------------------------- /data/rule/pronoun.json5: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | [ 7 | [ 8 | ["my", "one's"], 9 | ["your", "one's"], 10 | ["his", "one's"], 11 | ["her", "one's"], 12 | ["its", "one's"], 13 | ["our", "one's"], 14 | ["their", "one's"], 15 | ["'s", "one's"], 16 | ["one's", "one's"], 17 | ["someone's", "someone's"], 18 | ["myself", "oneself"], 19 | ["yourself", "oneself"], 20 | ["himself", "oneself"], 21 | ["herself", "oneself"], 22 | ["ourselves", "oneself"], 23 | ["themselves", "oneself"], 24 | ["him", "someone"], 25 | ["them", "someone"], 26 | ["us", "someone"], 27 | ["isn't", "not"], 28 | ["aren't", "not"], 29 | ["wasn't", "not"], 30 | ["weren't", "not"], 31 | ], 32 | [ 33 | ["my", "someone's"], 34 | ["your", "someone's"], 35 | ["his", "someone's"], 36 | ["her", "someone's"], 37 | ["its", "someone's"], 38 | ["our", "someone's"], 39 | ["their", "someone's"], 40 | ["'s", "someone's"], 41 | ["one's", "one's"], 42 | ["someone's", "someone's"], 43 | ["myself", "oneself"], 44 | ["yourself", "oneself"], 45 | ["himself", "oneself"], 46 | ["herself", "oneself"], 47 | ["ourselves", "oneself"], 48 | ["themselves", "oneself"], 49 | ["him", "someone"], 50 | ["them", "someone"], 51 | ["us", "someone"], 52 | ["isn't", "not"], 53 | ["aren't", "not"], 54 | ["wasn't", "not"], 55 | ["weren't", "not"], 56 | ], 57 | [ 58 | ["my", "someone's"], 59 | ["your", "someone's"], 60 | ["his", "someone's"], 61 | ["her", "someone"], 62 | ["its", "someone's"], 63 | ["our", "someone's"], 64 | ["their", "someone's"], 65 | ["'s", "someone's"], 66 | ["one's", "one's"], 67 | ["someone's", "someone's"], 68 | ["myself", "oneself"], 69 | ["yourself", "oneself"], 70 | ["himself", "oneself"], 71 | ["herself", "oneself"], 72 | ["ourselves", "oneself"], 73 | ["themselves", "oneself"], 74 | ["him", "someone"], 75 | ["them", "someone"], 76 | ["us", "someone"], 77 | ["isn't", "not"], 78 | ["aren't", "not"], 79 | ["wasn't", "not"], 80 | ["weren't", "not"], 81 | ], 82 | ] 83 | -------------------------------------------------------------------------------- /data/rule/trailing.json5: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | [ 7 | [ 8 | { search: "nned", new: "n" }, 9 | { search: "tted", new: "t" }, 10 | { search: "dded", new: "d" }, 11 | { search: "gged", new: "g" }, 12 | { search: "pped", new: "p" }, 13 | { search: "mmed", new: "m" }, 14 | { search: "bbed", new: "b" }, 15 | { search: "rred", new: "r" }, 16 | { search: "zzed", new: "z" }, 17 | { search: "ied", new: "y" }, 18 | ], 19 | [ 20 | { search: "nning", new: "n" }, 21 | { search: "tting", new: "t" }, 22 | { search: "dding", new: "d" }, 23 | { search: "gging", new: "g" }, 24 | { search: "pping", new: "p" }, 25 | { search: "mming", new: "m" }, 26 | { search: "bbing", new: "b" }, 27 | { search: "rring", new: "r" }, 28 | { search: "lling", new: "l" }, 29 | { search: "zzing", new: "z" }, 30 | { search: "ing", new: "" }, 31 | ], 32 | [ 33 | { search: "er", new: "e" }, 34 | { search: "est", new: "e" }, 35 | ], 36 | [ 37 | { search: "tter", new: "t" }, 38 | { search: "ttest", new: "t" }, 39 | { search: "dder", new: "d" }, 40 | { search: "ddest", new: "d" }, 41 | { search: "gger", new: "g" }, 42 | { search: "ggest", new: "g" }, 43 | { search: "ppier", new: "ppy" }, 44 | { search: "ppiest", new: "ppy" }, 45 | { search: "nner", new: "n" }, 46 | { search: "nnest", new: "n" }, 47 | { search: "ier", new: "y" }, 48 | { search: "iest", new: "y" }, 49 | { search: "er", new: "" }, 50 | { search: "est", new: "" }, 51 | ], 52 | [ 53 | { search: "ying", new: "ie" }, 54 | { search: "ing", new: "e" }, 55 | ], 56 | [{ search: "ing", new: "" }], 57 | [{ search: "ed", new: "" }], 58 | [{ search: "ed", new: "e" }], 59 | [{ search: "ies", new: "y" }], 60 | [{ search: "ier", new: "y" }], 61 | [{ search: "ves", new: "fe" }], 62 | [{ search: "ves", new: "f" }], 63 | [{ search: "zzes", new: "z" }], 64 | [{ search: "es", new: "" }], 65 | [{ search: "s", new: "" }], 66 | [{ search: "men", new: "man" }], 67 | [{ search: "ae", new: "a" }], 68 | [{ search: "li", new: "us" }], 69 | [{ search: "ia", new: "ium" }], 70 | [{ search: "gi", new: "gus" }], 71 | [{ search: "ses", new: "sis" }], 72 | ] 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mouse-dictionary", 3 | "version": "1.7.1", 4 | "repository": "https://github.com/wtetsu/mouse-dictionary.git", 5 | "author": "wtetsu", 6 | "license": "MIT", 7 | "scripts": { 8 | "res": "node tools/bundle_json.js && node tools/make_dict.js ", 9 | "res-force": "node tools/bundle_json.js --force && node tools/make_dict.js --force", 10 | "build": "npm run build-chrome", 11 | "watch": "npm run watch-chrome", 12 | "release": "npm run release-chrome", 13 | "package": "npm run package-chrome", 14 | "lint": "biome check ./src ./__test__", 15 | "format": "biome format --write ./src ./__test__", 16 | "test": "vitest run", 17 | "test-watch": "vitest watch", 18 | "test-cov": "vitest run --coverage", 19 | "test-cov-watch": "vitest watch --coverage", 20 | "build-chrome": "node tools/make_manifest.js chrome development && npm run res && node tools/build.js chrome development", 21 | "watch-chrome": "node tools/make_manifest.js chrome development && npm run res && node tools/build.js chrome development watch", 22 | "release-chrome": "node tools/make_manifest.js chrome production && npm run res-force && node tools/build.js chrome production", 23 | "package-chrome": "npm run release-chrome && node tools/archive.js chrome", 24 | "build-vivaldi": "node tools/make_manifest.js chrome development activate_extension && npm run res && node tools/build.js chrome development", 25 | "watch-vivaldi": "node tools/make_manifest.js chrome development activate_extension && npm run res && node tools/build.js chrome development watch", 26 | "release-vivaldi": "node tools/make_manifest.js chrome production activate_extension && npm run res-force && node tools/build.js chrome production", 27 | "package-vivaldi": "npm run release-vivaldi && node tools/archive.js chrome", 28 | "build-firefox": "node tools/make_manifest.js firefox development && npm run res && node tools/build.js firefox development", 29 | "watch-firefox": "node tools/make_manifest.js firefox development && npm run res && node tools/build.js firefox development watch", 30 | "release-firefox": "node tools/make_manifest.js firefox production && npm run res-force && node tools/build.js firefox production", 31 | "package-firefox": "npm run release-firefox && node tools/archive.js firefox", 32 | "build-safari": "node tools/make_manifest.js safari development && npm run res && node tools/build.js safari development", 33 | "watch-safari": "node tools/make_manifest.js safari development && npm run res && node tools/build.js safari development watch", 34 | "release-safari": "node tools/make_manifest.js safari production && npm run res-force && node tools/build.js safari production && yes | xcrun safari-web-extension-converter --force --no-open dist-safari --project-location dist-safari", 35 | "package-safari": "npm run release-safari && node tools/archive.js safari" 36 | }, 37 | "dependencies": { 38 | "deinja": "0.0.3", 39 | "immer": "^10.1.1", 40 | "mustache": "^4.2.0", 41 | "react": "^19.1.0", 42 | "react-ace": "^14.0.1", 43 | "react-dom": "^19.1.0", 44 | "sweetalert": "^2.1.2", 45 | "uniqlist": "^1.0.4" 46 | }, 47 | "devDependencies": { 48 | "@biomejs/biome": "^1.9.4", 49 | "@types/chrome": "^0.0.326", 50 | "@types/mustache": "^4.2.6", 51 | "@types/react": "^19.1.6", 52 | "@types/react-dom": "^19.1.5", 53 | "@vitest/coverage-v8": "^3.1.4", 54 | "adm-zip": "^0.5.16", 55 | "esbuild": "^0.25.5", 56 | "fast-glob": "^3.3.3", 57 | "fs-extra": "^11.3.0", 58 | "jsdom": "^26.1.0", 59 | "json5": "^2.2.3", 60 | "milligram": "^1.4.1", 61 | "typescript": "^5.8.3", 62 | "vitest": "^3.1.4" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Source code 2 | 3 | ## Outline 4 | 5 | | | main | options | 6 | | -------------- | ------------------- | ------------------ | 7 | | Responsibility | Core features | Options screen | 8 | | Priority | Speed and lightness | Functionality | 9 | | Implementation | Pure JavaScript | TypeScript + React | 10 | | Dependency | No dependency (\*) | Many libraries | 11 | | Module | default | named | 12 | 13 | (\*) mustache.js is the only exception. 14 | 15 | ## Dependency 16 | 17 | ``` 18 | main <- (extern) <- options 19 | ``` 20 | 21 | Always go through `extern` in order to access `main`. 22 | -------------------------------------------------------------------------------- /src/background/background.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import ExpiringQueue from "./queue"; 8 | import generateUniqueId from "./unique"; 9 | 10 | if (BROWSER === "chrome") { 11 | chrome.action.onClicked.addListener((tab) => { 12 | chrome.scripting.executeScript({ 13 | target: { tabId: tab.id }, 14 | files: ["main.js"], 15 | }); 16 | }); 17 | } else { 18 | chrome.browserAction.onClicked.addListener(() => { 19 | chrome.tabs.executeScript({ 20 | file: "./main.js", 21 | }); 22 | }); 23 | } 24 | 25 | // cross-extension messaging 26 | chrome.runtime.onMessageExternal.addListener((message) => { 27 | sendToActiveTab((tabId) => { 28 | chrome.tabs.sendMessage(tabId, { message: message }); 29 | }); 30 | }); 31 | 32 | // Shortcut key handling 33 | chrome.commands.onCommand.addListener((command) => { 34 | switch (command) { 35 | case "scroll_up": 36 | sendToActiveTab((tabId) => chrome.tabs.sendMessage(tabId, { message: { type: "scroll_up" } })); 37 | break; 38 | case "scroll_down": 39 | sendToActiveTab((tabId) => chrome.tabs.sendMessage(tabId, { message: { type: "scroll_down" } })); 40 | break; 41 | case "activate_extension": 42 | // Workaround for Vivaldi (see #84) 43 | sendToActiveTab((tabId) => 44 | chrome.scripting.executeScript({ 45 | target: { tabId }, 46 | files: ["main.js"], 47 | }), 48 | ); 49 | break; 50 | } 51 | }); 52 | 53 | // PDF handling 54 | const queue = new ExpiringQueue(1000 * 30); 55 | chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => { 56 | switch (request?.type) { 57 | case "open_pdf": { 58 | const id = generateUniqueId(); 59 | queue.push(id, request.payload); 60 | chrome.runtime.sendMessage({ type: "prepare_pdf" }); 61 | chrome.runtime.openOptionsPage(() => { 62 | sendResponse(); 63 | }); 64 | break; 65 | } 66 | case "shift_pdf_id": { 67 | const frontId = queue.shiftId(); 68 | sendResponse(frontId); 69 | break; 70 | } 71 | case "get_pdf_data": { 72 | const pdfData = queue.get(request.id); 73 | sendResponse(pdfData); 74 | break; 75 | } 76 | } 77 | }); 78 | 79 | const sendToActiveTab = (callback) => { 80 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 81 | for (let i = 0; i < tabs.length; i++) { 82 | callback(tabs[i].id); 83 | } 84 | }); 85 | }; 86 | -------------------------------------------------------------------------------- /src/background/queue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | // A Queue with expiring feature 8 | export default class ExpiringQueue { 9 | constructor(ttl) { 10 | this.ttl = ttl; 11 | this.pdfIdQueue = new Set(); 12 | this.pdfData = new Map(); 13 | } 14 | 15 | push(id, data) { 16 | this.pdfData.set(id, data); 17 | this.pdfIdQueue.add(id); 18 | 19 | setTimeout(() => { 20 | this.pdfIdQueue.delete(id); 21 | this.pdfData.delete(id); 22 | }, this.ttl); 23 | } 24 | 25 | shiftId() { 26 | let frontId = null; 27 | if (this.pdfIdQueue.size >= 1) { 28 | frontId = this.pdfIdQueue.values().next().value; 29 | this.pdfIdQueue.delete(frontId); 30 | } 31 | return frontId; 32 | } 33 | 34 | get(id) { 35 | return this.pdfData.get(id) ?? null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/background/unique.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | const generateUniqueId = () => crypto.randomUUID(); 7 | 8 | export default generateUniqueId; 9 | -------------------------------------------------------------------------------- /src/main/core/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import env from "../env"; 8 | import storage from "../lib/storage"; 9 | import defaultSettings from "../settings"; 10 | 11 | const KEY_USER_CONFIG = "**** config ****"; 12 | const KEY_LAST_POSITION = "**** last_position ****"; 13 | const KEY_LOADED = "**** loaded ****"; 14 | 15 | const JSON_FIELDS = new Set(["normalDialogStyles", "movingDialogStyles", "hiddenDialogStyles"]); 16 | 17 | const loadAll = async () => { 18 | if (!env.enableUserSettings) { 19 | return { settings: parseSettings(defaultSettings), position: {} }; 20 | } 21 | const data = await getStoredData([KEY_USER_CONFIG, KEY_LAST_POSITION]); 22 | const mergedSettings = { ...defaultSettings, ...data[KEY_USER_CONFIG] }; 23 | const settings = parseSettings(mergedSettings); 24 | 25 | const position = data[KEY_LAST_POSITION]; 26 | return { settings, position }; 27 | }; 28 | 29 | const loadSettings = async () => { 30 | const rawSettings = await loadRawSettings(); 31 | return parseSettings(rawSettings); 32 | }; 33 | 34 | const loadRawSettings = async () => { 35 | if (!env.enableUserSettings) { 36 | return { ...defaultSettings }; 37 | } 38 | 39 | const data = await getStoredData([KEY_USER_CONFIG]); 40 | const userSettings = data[KEY_USER_CONFIG]; 41 | return { ...defaultSettings, ...userSettings }; 42 | }; 43 | 44 | const parseSettings = (settings) => { 45 | const result = {}; 46 | const keys = Object.keys(settings); 47 | for (let i = 0; i < keys.length; i++) { 48 | const field = keys[i]; 49 | const value = settings[field]; 50 | if (value === null || value === undefined) { 51 | continue; 52 | } 53 | result[field] = JSON_FIELDS.has(field) ? parseJson(value) : value; 54 | } 55 | if (!env.enableWindowStatusSave && settings.initialPosition === "keep") { 56 | result.initialPosition = "right"; 57 | } 58 | return result; 59 | }; 60 | 61 | const parseJson = (json) => { 62 | if (!json) { 63 | return null; 64 | } 65 | let result; 66 | try { 67 | result = JSON.parse(json); 68 | } catch (e) { 69 | result = null; 70 | console.error("Failed to parse json:" + json); 71 | console.error(e); 72 | } 73 | return result; 74 | }; 75 | 76 | const savePosition = async (e) => { 77 | if (!env.enableUserSettings || !env.enableWindowStatusSave) { 78 | return; 79 | } 80 | return storage.sync.set({ 81 | [KEY_LAST_POSITION]: JSON.stringify(e), 82 | }); 83 | }; 84 | 85 | const getStoredData = async (keys) => { 86 | const result = {}; 87 | const storedData = await storage.sync.get(keys); 88 | 89 | for (let i = 0; i < keys.length; i++) { 90 | const key = keys[i]; 91 | const json = storedData[key] ?? "{}"; 92 | result[key] = parseJson(json) ?? {}; 93 | } 94 | 95 | return result; 96 | }; 97 | 98 | const isDataReady = () => storage.local.pick(KEY_LOADED); 99 | 100 | export default { 101 | loadAll, 102 | loadSettings, 103 | loadRawSettings, 104 | parseSettings, 105 | savePosition, 106 | isDataReady, 107 | }; 108 | -------------------------------------------------------------------------------- /src/main/core/entry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | const build = (languageDetector, generators) => { 8 | return (text, withCapitalized, mustIncludeOriginalText) => { 9 | const lang = languageDetector(text); 10 | const generator = generators[lang] ?? generators.default; 11 | const entries = generator(text, withCapitalized, mustIncludeOriginalText); 12 | return { entries, lang }; 13 | }; 14 | }; 15 | 16 | export default { build }; 17 | -------------------------------------------------------------------------------- /src/main/core/entry/default.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import entry from "../entry"; 8 | import entryGeneratorEn from "./en"; 9 | import entryGeneratorJa from "./ja"; 10 | 11 | // Can add other languages here 12 | const generators = { 13 | en: entryGeneratorEn, 14 | ja: entryGeneratorJa, 15 | default: entryGeneratorEn, 16 | }; 17 | 18 | const languageDetector = (text) => (isEnglishText(text) ? "en" : "ja"); 19 | 20 | const isEnglishText = (str) => { 21 | let result = true; 22 | for (let i = 0; i < str.length; i++) { 23 | const code = str.charCodeAt(i); 24 | const isEnglishLike = (0x20 <= code && code <= 0x7e) || code === 0x2011 || code === 0x200c; 25 | if (!isEnglishLike) { 26 | result = false; 27 | break; 28 | } 29 | } 30 | return result; 31 | }; 32 | 33 | const build = () => { 34 | return entry.build(languageDetector, generators); 35 | }; 36 | 37 | export default build; 38 | -------------------------------------------------------------------------------- /src/main/core/entry/ja.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import UniqList from "uniqlist"; 8 | import rule from "../rule"; 9 | const RE_ALPHABETS_NUMBERS = /[A-Za-z0-9]/g; 10 | const FULLWIDTH_OFFSET = 0xfee0; 11 | 12 | const createLookupWordsJa = (sourceStr) => { 13 | const str = sourceStr 14 | .substring(0, 40) 15 | .replaceAll("\u200c", "") // ZERO WIDTH NON-JOINER 16 | .replace(RE_ALPHABETS_NUMBERS, (s) => String.fromCharCode(s.charCodeAt(0) + FULLWIDTH_OFFSET)); 17 | 18 | const result = new UniqList(); 19 | 20 | result.push(sourceStr); // Add the original word 21 | 22 | for (let i = str.length; i >= 1; i--) { 23 | const part = str.substring(0, i); 24 | result.push(part); 25 | 26 | if (i >= 2) { 27 | const deinedWords = rule.doJa(part); 28 | result.merge(deinedWords); 29 | } 30 | } 31 | return result.toArray(); 32 | }; 33 | 34 | export default createLookupWordsJa; 35 | -------------------------------------------------------------------------------- /src/main/core/generator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import template from "../lib/template"; 8 | 9 | export default class Generator { 10 | constructor(settings) { 11 | this.shortWordLength = settings.shortWordLength; 12 | this.cutShortWordDescription = settings.cutShortWordDescription; 13 | 14 | // cssReset is deprecated. It is kept for backward compatibility with older settings. 15 | const cssReset = "margin:0;padding:0;border:0;vertical-align:baseline;line-height:normal;text-shadow:none;"; 16 | 17 | this.baseParameters = { 18 | headFontColor: settings.headFontColor, 19 | descFontColor: settings.descFontColor, 20 | headFontSize: settings.headFontSize, 21 | descFontSize: settings.descFontSize, 22 | cssReset, 23 | }; 24 | 25 | this.scroll = settings.scroll; 26 | 27 | this.compiledReplaceRules = compileReplaceRules(settings.replaceRules, { 28 | cssReset, 29 | }); 30 | 31 | this.contentTemplate = settings.contentTemplate; 32 | 33 | // Pre-parse and cache template 34 | template.parse(settings.contentTemplate); 35 | } 36 | 37 | generate(words, descriptions, enableShortWordLength = true) { 38 | const html = this.#createContentHtml(words, descriptions, enableShortWordLength); 39 | const hitCount = Object.keys(descriptions).length; 40 | return { html, hitCount }; 41 | } 42 | 43 | #createContentHtml(words, descriptions, enableShortWordLength) { 44 | const parameters = { 45 | ...this.baseParameters, 46 | words: this.#createWordsParameter(words, descriptions, enableShortWordLength), 47 | }; 48 | return template.render(this.contentTemplate, parameters); 49 | } 50 | 51 | #createDescriptionHtml(sourceText) { 52 | let result = sourceText; 53 | for (let i = 0; i < this.compiledReplaceRules.length; i++) { 54 | const rule = this.compiledReplaceRules[i]; 55 | result = result.replace(rule.search, rule.replace); 56 | } 57 | return result; 58 | } 59 | 60 | #createWordsParameter(words, descriptions, enableShortWordLength) { 61 | const data = []; 62 | const shortWordLength = enableShortWordLength ? this.shortWordLength : 0; 63 | for (let i = 0; i < words.length; i++) { 64 | const word = words[i]; 65 | const desc = descriptions[word]; 66 | if (typeof desc !== "string") { 67 | continue; 68 | } 69 | const isShort = word.length <= shortWordLength; 70 | const isShortWord = word.length <= this.shortWordLength; 71 | data.push({ 72 | head: escapeHtml(word), 73 | desc: this.#createDescriptionHtml(desc), 74 | isShort, 75 | isShortWord, 76 | shortDesc: desc.substring(0, this.cutShortWordDescription), 77 | isFirst: false, 78 | isLast: false, 79 | }); 80 | } 81 | if (data.length >= 1) { 82 | data[0].isFirst = true; 83 | data[0].isShort = false; 84 | data[data.length - 1].isLast = true; 85 | } 86 | return data; 87 | } 88 | } 89 | 90 | const compileReplaceRules = (replaceRules, renderParameters) => { 91 | const compiledReplaceRules = []; 92 | for (let i = 0; i < replaceRules.length; i++) { 93 | const compiledRule = compileReplaceRule(replaceRules[i], renderParameters); 94 | if (compiledRule) { 95 | compiledReplaceRules.push(compiledRule); 96 | } 97 | } 98 | return compiledReplaceRules; 99 | }; 100 | 101 | const compileReplaceRule = (rule, renderParameters) => { 102 | if (!rule.search) { 103 | return null; 104 | } 105 | let re = null; 106 | try { 107 | re = new RegExp(rule.search, "g"); 108 | } catch (error) { 109 | console.error(error); 110 | } 111 | if (!re) { 112 | return null; 113 | } 114 | 115 | const replace = template.render(rule.replace, renderParameters); 116 | 117 | return { 118 | search: re, 119 | replace, 120 | }; 121 | }; 122 | 123 | const mapForEscapeHtml = { 124 | "&": "&", 125 | "<": "<", 126 | ">": ">", 127 | '"': """, 128 | }; 129 | 130 | const reForEscapeHtml = /&|<|>|"/g; 131 | 132 | const escapeHtml = (str) => { 133 | return str.replace(reForEscapeHtml, (ch) => mapForEscapeHtml[ch]); 134 | }; 135 | -------------------------------------------------------------------------------- /src/main/core/pdf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import ribbon from "../lib/ribbon"; 8 | import res from "./resource"; 9 | 10 | const invoke = async () => { 11 | const [updateRibbon, closeRibbon] = ribbon.create(); 12 | 13 | updateRibbon(res("downloadingPdf")); 14 | 15 | let response; 16 | try { 17 | response = await fetch(location.href); 18 | } catch (e) { 19 | if (location.href.startsWith("file://")) { 20 | updateRibbon(res("cannotFetchLocalPdf"), [""]); 21 | } else { 22 | updateRibbon(e.message, [""]); 23 | } 24 | return; 25 | } 26 | 27 | if (response.status !== 200) { 28 | updateRibbon(await response.text(), [""]); 29 | return; 30 | } 31 | 32 | updateRibbon(res("preparingPdf")); 33 | 34 | const arrayBuffer = await response.arrayBuffer(); 35 | 36 | if (!isPdf(arrayBuffer)) { 37 | updateRibbon(res("nonPdf"), [""]); 38 | return; 39 | } 40 | 41 | const payload = convertToBase64(arrayBuffer); 42 | sendMessage({ type: "open_pdf", payload }); 43 | 44 | closeRibbon(); 45 | }; 46 | 47 | const isPdf = (arrayBuffer) => { 48 | const first4 = new Uint8Array(arrayBuffer.slice(0, 4)); 49 | return first4[0] === 37 && first4[1] === 80 && first4[2] === 68 && first4[3] === 70; 50 | }; 51 | 52 | const convertToBase64 = (arrayBuffer) => { 53 | let result = ""; 54 | const byteArray = new Uint8Array(arrayBuffer); 55 | 56 | for (let i = 0; ; i++) { 57 | if (i * 1023 >= byteArray.length) { 58 | break; 59 | } 60 | const start = i * 1023; 61 | const end = (i + 1) * 1023; 62 | 63 | const slice = byteArray.slice(start, end); 64 | const base64slice = btoa(String.fromCharCode(...slice)); 65 | 66 | result += base64slice; 67 | } 68 | return result; 69 | }; 70 | 71 | const sendMessage = async (message) => { 72 | return new Promise((done) => { 73 | chrome.runtime.sendMessage(message, (response) => { 74 | done(response); 75 | }); 76 | }); 77 | }; 78 | 79 | export default { invoke }; 80 | -------------------------------------------------------------------------------- /src/main/core/resource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | // This resource file is separated from the file of the options UI screen, 8 | // in order to make the main feature lighter and faster. 9 | 10 | const resources = { 11 | ja: { 12 | continueProcessingPdf: 13 | "このPDFファイルをダウンロードし、Mouse Dictionaryの内部ビューアで表示します。よろしいですか?\n(設定画面で、この確認ダイアログ表示をオフにすることもできます)", 14 | doesntSupportFrame: "Mouse Dictionaryは、フレームのあるページで使用することはできません。", 15 | downloadingPdf: "📘ダウンロード中...", 16 | preparingPdf: "📘PDFビューア準備中...", 17 | nonPdf: "PDFファイルではないようです。処理を中断しました。", 18 | cannotFetchLocalPdf: 19 | "⛔Mouse DictionaryはローカルにあるPDFファイル上では起動できません。オプション画面から開けるPDFビューアをご利用ください。", 20 | }, 21 | en: { 22 | continueProcessingPdf: 23 | "Are you sure you want to download this PDF file and view it using Mouse Dictionary's internal viewer?\n(You can disable this confirmation by changing settings)", 24 | doesntSupportFrame: "Mouse Dictionary doesn't support frame pages.", 25 | downloadingPdf: "📘Downloading...", 26 | preparingPdf: "📘Preparing PDF viewer...", 27 | nonPdf: "This is not a PDF document.", 28 | cannotFetchLocalPdf: "⛔Mouse Dictionary can't launch on local PDFs. Use the PDF viewer from the options screen.", 29 | }, 30 | }; 31 | 32 | // Build process removes unrelated messages 33 | if (BROWSER === "chrome") { 34 | resources.ja.needToPrepareDict = "辞書データをロードしてください(拡張のアイコンを右クリック→「オプション」)"; 35 | resources.en.needToPrepareDict = 36 | 'Please load dictionary data first. Right click on the extension icon and select "Options"'; 37 | } 38 | 39 | if (BROWSER === "firefox") { 40 | resources.ja.needToPrepareDict = 41 | "辞書データをロードしてください(拡張のアイコンを右クリック→「拡張機能を管理」→「...」をクリック→「オプション」)"; 42 | resources.en.needToPrepareDict = 43 | 'Please load dictionary data first. Right click on the extension icon, select "Manage Extension", click "…", and select "Options"'; 44 | } 45 | 46 | if (BROWSER === "safari") { 47 | resources.ja.needToPrepareDict = "辞書データをロードしてください(拡張のアイコンを右クリック→「拡張機能」→「設定」)"; 48 | resources.en.needToPrepareDict = 49 | 'Please load dictionary data first. Right click on the extension icon, select "Extensions" tab, and select "Preferences"'; 50 | } 51 | 52 | const decideLanguage = () => { 53 | let result = "en"; 54 | const languages = navigator.languages; 55 | if (!languages) { 56 | return result; 57 | } 58 | const validLanguages = Object.keys(resources); 59 | for (let i = 0; i < languages.length; i++) { 60 | const lang = languages[i].toLowerCase().split("-")[0]; 61 | if (validLanguages.includes(lang)) { 62 | result = lang; 63 | break; 64 | } 65 | } 66 | return result; 67 | }; 68 | 69 | export default (key) => { 70 | const lang = decideLanguage(); 71 | const res = resources[lang]; 72 | return res[key] ?? null; 73 | }; 74 | -------------------------------------------------------------------------------- /src/main/core/rule.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import utils from "../lib/utils"; 8 | 9 | import buildDeinja from "deinja/build"; 10 | import base from "./rule/base"; 11 | import phrase from "./rule/phrase"; 12 | import pronoun from "./rule/pronoun"; 13 | import spelling from "./rule/spelling"; 14 | 15 | // Lazy load 16 | const nounRule = new Map(); 17 | const phraseRule = []; 18 | const pronounRule = []; 19 | const spellingRule = new Map(); 20 | const trailingRule = []; 21 | const verbRule = new Map(); 22 | const lettersRule = new Map(); 23 | 24 | let deinjaConvert = () => {}; 25 | const registerLetters = (data) => utils.updateMap(lettersRule, data); 26 | const registerNoun = (data) => utils.updateMap(nounRule, data); 27 | const registerPhrase = (data) => Object.assign(phraseRule, data); 28 | const registerPronoun = (data) => 29 | Object.assign( 30 | pronounRule, 31 | data.map((datum) => new Map(datum)), 32 | ); 33 | const registerSpelling = (data) => utils.updateMap(spellingRule, data); 34 | const registerTrailing = (data) => Object.assign(trailingRule, data); 35 | const registerVerb = (data) => utils.updateMap(verbRule, data); 36 | const registerJa = (data) => { 37 | deinjaConvert = buildDeinja(data); 38 | }; 39 | 40 | const DEFAULT_RULE_FILE = "data/rule.json"; 41 | 42 | // Note: Parsing JSON is faster than long Object literals. 43 | // https://v8.dev/blog/cost-of-javascript-2019 44 | const readAndLoadRuleFiles = async (ruleFile) => { 45 | DEBUG && console.time("rule"); 46 | 47 | const rulePromise = utils.loadJson(ruleFile); 48 | 49 | // Redefine in order not to be executed twice 50 | loadBody = () => rulePromise; 51 | 52 | const loadedRuleData = await rulePromise; 53 | registerRuleData(loadedRuleData); 54 | 55 | DEBUG && console.timeEnd("rule"); 56 | 57 | return loadedRuleData; 58 | }; 59 | 60 | const registerRuleData = (ruleData) => { 61 | const processes = [ 62 | { field: "letters", register: registerLetters }, 63 | { field: "noun", register: registerNoun }, 64 | { field: "phrase", register: registerPhrase }, 65 | { field: "pronoun", register: registerPronoun }, 66 | { field: "spelling", register: registerSpelling }, 67 | { field: "trailing", register: registerTrailing }, 68 | { field: "verb", register: registerVerb }, 69 | { field: "ja", register: registerJa }, 70 | ]; 71 | 72 | for (let i = 0; i < processes.length; i++) { 73 | const proc = processes[i]; 74 | const data = ruleData[proc.field]; 75 | if (data) { 76 | proc.register(data); 77 | } 78 | } 79 | }; 80 | 81 | let loadBody = readAndLoadRuleFiles; 82 | 83 | const load = async (ruleFile = DEFAULT_RULE_FILE) => { 84 | return loadBody(ruleFile); 85 | }; 86 | 87 | export default { 88 | load, 89 | registerRuleData, 90 | doBase: (word) => base({ noun: nounRule, trailing: trailingRule, verb: verbRule }, word), 91 | doLetters: (ch) => lettersRule.get(ch), 92 | doPhrase: (words) => phrase(phraseRule, words), 93 | doPronoun: (words) => pronoun(pronounRule, words), 94 | doSpelling: (words) => spelling(spellingRule, words), 95 | doJa: (word) => deinjaConvert(word), 96 | }; 97 | -------------------------------------------------------------------------------- /src/main/core/rule/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import UniqList from "uniqlist"; 8 | import text from "../../lib/text"; 9 | 10 | export default (rule, word) => { 11 | const list = new UniqList(); 12 | const v = rule.verb.get(word); 13 | if (v) { 14 | list.push(v); 15 | } 16 | const n = rule.noun.get(word); 17 | if (n) { 18 | list.push(n); 19 | } 20 | const otherForms = text.tryToReplaceTrailingStrings(word, rule.trailing); 21 | list.merge(otherForms); 22 | return list.toArray(); 23 | }; 24 | -------------------------------------------------------------------------------- /src/main/core/rule/phrase.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | export default (allRules, words) => { 8 | const ruleDataList = allRules[words.length]; 9 | if (!ruleDataList) { 10 | return []; 11 | } 12 | const result = []; 13 | for (let i = 0; i < ruleDataList.length; i++) { 14 | const ruleData = ruleDataList[i]; 15 | const newWordsList = normalizeByRule(words, ruleData); 16 | result.push(...newWordsList); 17 | } 18 | return result; 19 | }; 20 | 21 | const VOWELS = new Set(["a", "e", "i", "o", "u", "A", "E", "I", "O", "U"]); 22 | 23 | // ["provide", "him", "with", "money"], [0, 1, 0, 1] 24 | // -> [["provide", "A", "with", "B"]] 25 | // 26 | // ["pick", "her", "up"], [0, -1, 0] 27 | // -> [["pick", "up]] 28 | const normalizeByRule = (words, ruleData) => { 29 | const processedWords = []; 30 | let wordIndex = 0; 31 | const replaceIndices = []; 32 | let lastIsA = false; 33 | for (let i = 0; i < ruleData.length; i++) { 34 | if (wordIndex >= words.length) { 35 | break; 36 | } 37 | const ruleCode = ruleData[i]; 38 | const { newWord, indexPlus } = processRuleCode(ruleCode, words[wordIndex]); 39 | 40 | wordIndex += indexPlus; 41 | if (ruleCode > 0 && ruleCode < 100) { 42 | replaceIndices.push(processedWords.length); 43 | } 44 | if (newWord !== undefined) { 45 | if (lastIsA && VOWELS.has(newWord?.[0])) { 46 | processedWords[processedWords.length - 1] = "an"; 47 | } 48 | processedWords.push(newWord); 49 | lastIsA = newWord === "a"; 50 | } 51 | } 52 | if (replaceIndices.length === 0) { 53 | return [processedWords]; 54 | } 55 | 56 | return completePhraseProcess(processedWords, replaceIndices); 57 | }; 58 | 59 | const processRuleCode = (ruleCode, word) => { 60 | if (ruleCode === 0) { 61 | return { newWord: word, indexPlus: 1 }; 62 | } 63 | if (ruleCode === 102) { 64 | return { newWord: "a", indexPlus: 1 }; 65 | } 66 | if (ruleCode === 103) { 67 | return { newWord: "a", indexPlus: 0 }; 68 | } 69 | if (ruleCode === 104 && word === "/") { 70 | return { newWord: "and", indexPlus: 1 }; 71 | } 72 | if (ruleCode === 105 && word === "/") { 73 | return { newWord: "or", indexPlus: 1 }; 74 | } 75 | if (ruleCode > 0 && ruleCode < 100) { 76 | return { newWord: null, indexPlus: ruleCode }; 77 | } 78 | if (ruleCode < 0) { 79 | return { indexPlus: -ruleCode }; 80 | } 81 | return { indexPlus: 0 }; 82 | }; 83 | 84 | const completePhraseProcess = (processedWords, replaceIndices) => { 85 | if (replaceIndices.length === 1) { 86 | const processedWords2 = [...processedWords]; 87 | 88 | processedWords[replaceIndices[0]] = "~"; 89 | processedWords2[replaceIndices[0]] = "__"; 90 | return [processedWords, processedWords2]; 91 | } 92 | 93 | for (let i = 0; i < replaceIndices.length; i++) { 94 | const index = replaceIndices[i]; 95 | const charCode = 65 + i; 96 | processedWords[index] = String.fromCharCode(charCode); 97 | } 98 | return [processedWords]; 99 | }; 100 | -------------------------------------------------------------------------------- /src/main/core/rule/pronoun.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | /** 8 | * ["on", "my", "own"] -> [["on", "one's", "own"], ["on", "someone's", "own"]] 9 | */ 10 | export default (pronounRule, words) => { 11 | const result = []; 12 | let changed = false; 13 | for (let i = 0; i < pronounRule.length; i++) { 14 | const convertedWords = [...words]; 15 | for (let j = 0; j < convertedWords.length; j++) { 16 | const w = doConvert(convertedWords[j], pronounRule[i]); 17 | if (w) { 18 | convertedWords[j] = w; 19 | changed = true; 20 | } 21 | } 22 | if (changed) { 23 | result.push(convertedWords); 24 | } 25 | } 26 | return result; 27 | }; 28 | 29 | const doConvert = (word, pronouns) => { 30 | let result = null; 31 | const w = pronouns.get(word); 32 | if (w) { 33 | result = w; 34 | } else { 35 | const firstCode = word.charCodeAt(0); 36 | if (firstCode >= 65 && firstCode <= 90 && (word.endsWith("'s") || word.endsWith("s'"))) { 37 | result = pronouns.get("'s"); 38 | } 39 | } 40 | return result; 41 | }; 42 | -------------------------------------------------------------------------------- /src/main/core/rule/spelling.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | export default (spellingRule, words) => { 8 | let converted = false; 9 | const convertedWords = []; 10 | for (let j = 0; j < words.length; j++) { 11 | const word = words[j]; 12 | const convertedWord = spellingRule.get(word); 13 | if (convertedWord) { 14 | converted = true; 15 | convertedWords.push(convertedWord); 16 | } else { 17 | convertedWords.push(word); 18 | } 19 | } 20 | if (!converted) { 21 | return null; 22 | } 23 | return convertedWords; 24 | }; 25 | -------------------------------------------------------------------------------- /src/main/core/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import dom from "../lib/dom"; 8 | import template from "../lib/template"; 9 | 10 | const createDialogElement = (settings) => { 11 | const html = template.render(settings.dialogTemplate, { 12 | backgroundColor: settings.backgroundColor, 13 | width: settings.width, 14 | height: settings.height, 15 | scroll: "scroll", // For backward compatibility 16 | }); 17 | const dialog = dom.create(html); 18 | dom.applyStyles(dialog, settings.normalDialogStyles); 19 | return dialog; 20 | }; 21 | 22 | const create = (settings) => { 23 | const dialog = createDialogElement(settings); 24 | const content = dom.create(settings.contentWrapperTemplate); 25 | dialog.appendChild(content); 26 | return { dialog, content }; 27 | }; 28 | 29 | export default { create }; 30 | -------------------------------------------------------------------------------- /src/main/env.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | const env = { 8 | enableWindowStatusSave: true, 9 | enableUserSettings: true, 10 | }; 11 | 12 | export default env; 13 | -------------------------------------------------------------------------------- /src/main/lib/decoy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import dom from "./dom"; 8 | 9 | const create = (tag) => { 10 | return new Decoy(tag); 11 | }; 12 | 13 | const INPUT_TAGS = new Set(["INPUT", "TEXTAREA", "SELECT", "OPTION"]); 14 | 15 | const DEFAULT_STYLES = { 16 | position: "absolute", 17 | zIndex: 2147483647, 18 | opacity: 0, 19 | }; 20 | 21 | const INPUT_STYLES = { 22 | INPUT: { overflow: "hidden", whiteSpace: "nowrap" }, 23 | TEXTAREA: { overflow: "hidden" }, 24 | SELECT: { overflow: "hidden", whiteSpace: "nowrap" }, 25 | OPTION: { overflow: "hidden", whiteSpace: "nowrap" }, 26 | }; 27 | 28 | const COPY_STYLE_PROPERTIES = [ 29 | "fontSize", 30 | "fontWeight", 31 | "fontFamily", 32 | "lineHeight", 33 | "paddingTop", 34 | "paddingRight", 35 | "paddingBottom", 36 | "paddingLeft", 37 | ]; 38 | 39 | class Decoy { 40 | constructor(tag) { 41 | this.elementCache = createElement(tag); 42 | this.decoy = null; 43 | } 44 | 45 | activate(underlay) { 46 | if (!this.elementCache) { 47 | return; 48 | } 49 | if (!INPUT_TAGS.has(underlay.tagName)) { 50 | return; 51 | } 52 | const decoy = prepare(dom.clone(underlay, this.elementCache), underlay); 53 | 54 | document.body.appendChild(decoy); 55 | this.decoy = decoy; 56 | 57 | // These values are required to be set after appendChild. 58 | decoy.scrollTop = underlay.scrollTop; 59 | decoy.scrollLeft = underlay.scrollLeft; 60 | const correctionWidth = underlay.clientWidth - decoy.clientWidth; 61 | const correctionHeight = underlay.clientHeight - decoy.clientHeight; 62 | 63 | const computedStyle = getComputedStyle(underlay); 64 | const decoyAdditionStyles = {}; 65 | for (const prop of COPY_STYLE_PROPERTIES) { 66 | decoyAdditionStyles[prop] = computedStyle[prop]; 67 | } 68 | decoyAdditionStyles.width = `${underlay.clientWidth + correctionWidth}px`; 69 | decoyAdditionStyles.height = `${underlay.clientHeight + correctionHeight}px`; 70 | 71 | dom.applyStyles(decoy, decoyAdditionStyles); 72 | } 73 | 74 | deactivate() { 75 | if (!this.elementCache) { 76 | return; 77 | } 78 | const decoy = this.decoy; 79 | this.decoy = null; 80 | if (decoy) { 81 | document.body.removeChild(decoy); 82 | } 83 | } 84 | } 85 | 86 | const createElement = (tag) => { 87 | if (!tag) { 88 | return null; 89 | } 90 | return document.createElement(tag); 91 | }; 92 | 93 | const prepare = (decoy, underlay) => { 94 | decoy.innerText = getElementText(underlay); 95 | 96 | const style = createDecoyStyle(decoy, underlay); 97 | 98 | // Specify only absolute size 99 | style.width = `${underlay.clientWidth}px`; 100 | style.height = `${underlay.clientHeight}px`; 101 | dom.applyStyles(decoy, style); 102 | for (const p of ["min-width", "min-height", "max-width", "max-height"]) { 103 | decoy.style.removeProperty(p); 104 | } 105 | return decoy; 106 | }; 107 | 108 | const getElementText = (element) => { 109 | if (element.tagName === "SELECT") { 110 | return getSelectText(element); 111 | } 112 | return element.text ?? element.value; 113 | }; 114 | 115 | function getSelectText(element) { 116 | const index = element.selectedIndex; 117 | return element.options[index]?.text; 118 | } 119 | 120 | const createDecoyStyle = (decoy, underlay) => { 121 | const offset = getOffset(underlay); 122 | const top = offset.top - dom.pxToFloat(decoy.style.marginTop); 123 | const left = offset.left - dom.pxToFloat(decoy.style.marginLeft); 124 | 125 | const dynamicStyles = { 126 | top: `${top}px`, 127 | left: `${left}px`, 128 | }; 129 | 130 | return { 131 | ...dynamicStyles, 132 | ...DEFAULT_STYLES, 133 | ...INPUT_STYLES[underlay.tagName], 134 | }; 135 | }; 136 | 137 | const getOffset = (element) => { 138 | const rect = element.getBoundingClientRect(); 139 | const doc = document.documentElement; 140 | 141 | return { 142 | top: rect.top + window.scrollY - doc.clientTop, 143 | left: rect.left + window.scrollX - doc.clientLeft, 144 | }; 145 | }; 146 | 147 | export default { create }; 148 | -------------------------------------------------------------------------------- /src/main/lib/edge.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import utils from "./utils"; 8 | 9 | const TOP = 1; 10 | const RIGHT = 2; 11 | const BOTTOM = 4; 12 | const LEFT = 8; 13 | const EDGE = TOP | RIGHT | BOTTOM | LEFT; 14 | const INSIDE = 16; 15 | const DOWNWARDS = { near: TOP, far: BOTTOM }; 16 | const RIGHTWARDS = { near: LEFT, far: RIGHT }; 17 | 18 | // Create a "packed" array 19 | // https://v8.dev/blog/elements-kinds 20 | const CURSOR_STYLES = [ 21 | "move", 22 | "ns-resize", // TOP 23 | "ew-resize", // RIGHT 24 | "nesw-resize", // TOP | RIGHT 25 | "ns-resize", // BOTTOM 26 | "move", 27 | "nwse-resize", // BOTTOM | RIGHT 28 | "move", 29 | "ew-resize", // LEFT 30 | "nwse-resize", // TOP | LEFT 31 | "move", 32 | "move", 33 | "nesw-resize", // BOTTOM | LEFT 34 | "move", 35 | "move", 36 | "move", 37 | "move", 38 | ]; 39 | 40 | class Edge { 41 | constructor(options) { 42 | this.gripWidth = options.gripWidth; 43 | } 44 | 45 | getEdgeState(rect, x, y) { 46 | if (Number.isNaN(x) || Number.isNaN(y)) { 47 | return 0; 48 | } 49 | let edge = 0; 50 | if (inRange(rect.left, x, rect.left + this.gripWidth)) { 51 | edge |= LEFT; 52 | } else if (inRange(rect.left + rect.width - this.gripWidth, x, rect.left + rect.width)) { 53 | edge |= RIGHT; 54 | } 55 | if (inRange(rect.top, y, rect.top + this.gripWidth)) { 56 | edge |= TOP; 57 | } else if (inRange(rect.top + rect.height - this.gripWidth, y, rect.top + rect.height)) { 58 | edge |= BOTTOM; 59 | } 60 | if (edge !== 0 || utils.isInsideRange(rect, { x, y })) { 61 | edge |= INSIDE; 62 | } 63 | return edge; 64 | } 65 | 66 | getCursorStyle(edgeState) { 67 | return CURSOR_STYLES[edgeState & EDGE]; 68 | } 69 | } 70 | 71 | const build = (options) => { 72 | return new Edge(options); 73 | }; 74 | 75 | const inRange = (low, value, high) => { 76 | return low <= value && value <= high; 77 | }; 78 | 79 | class Square { 80 | constructor(onsetSquare, edgeState, minimumLength) { 81 | this.square = onsetSquare; 82 | this.edgeState = edgeState; 83 | this.minimumLength = minimumLength; 84 | } 85 | 86 | move(movedX, movedY) { 87 | return { 88 | left: this.square.left + movedX, 89 | top: this.square.top + movedY, 90 | }; 91 | } 92 | 93 | resize(movedX, movedY) { 94 | const resizedSquare = { left: null, top: null, width: null, height: null }; 95 | 96 | const downwardsPosition = this.calculate1dPosition(DOWNWARDS, this.square.top, this.square.height, movedY); 97 | resizedSquare.top = downwardsPosition.position; 98 | resizedSquare.height = downwardsPosition.length; 99 | 100 | const rightwardsPosition = this.calculate1dPosition(RIGHTWARDS, this.square.left, this.square.width, movedX); 101 | resizedSquare.left = rightwardsPosition.position; 102 | resizedSquare.width = rightwardsPosition.length; 103 | 104 | return resizedSquare; 105 | } 106 | 107 | calculate1dPosition(edges, startPosition, startLength, movedLength) { 108 | if (this.edgeState & edges.near) { 109 | const length = Math.max(startLength - movedLength, this.minimumLength); 110 | const position = startPosition + startLength - length; 111 | return { position, length }; 112 | } 113 | if (this.edgeState & edges.far) { 114 | const length = Math.max(startLength + movedLength, this.minimumLength); 115 | return { position: null, length }; 116 | } 117 | return {}; 118 | } 119 | } 120 | 121 | const createSquare = (square, edgeState, minimumLength) => { 122 | return new Square(square, edgeState, minimumLength); 123 | }; 124 | 125 | export default { 126 | build, 127 | createSquare, 128 | TOP, 129 | RIGHT, 130 | BOTTOM, 131 | LEFT, 132 | EDGE, 133 | INSIDE, 134 | }; 135 | -------------------------------------------------------------------------------- /src/main/lib/ponyfill/chrome.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | const getComputedCssText = (params) => { 8 | const computedStyle = window.getComputedStyle(params); 9 | return computedStyle.cssText; 10 | }; 11 | 12 | const getCaretNodeAndOffsetFromPoint = (ownerDocument, pointX, pointY) => { 13 | const range = ownerDocument.caretRangeFromPoint(pointX, pointY); 14 | if (!range) { 15 | return null; 16 | } 17 | return { 18 | node: range.startContainer, 19 | offset: range.startOffset, 20 | }; 21 | }; 22 | 23 | export default { getComputedCssText, getCaretNodeAndOffsetFromPoint }; 24 | -------------------------------------------------------------------------------- /src/main/lib/ponyfill/firefox.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu, suiheilibe 4 | * Licensed under MIT 5 | */ 6 | 7 | const getComputedCssText = (params) => { 8 | const computedStyle = window.getComputedStyle(params); 9 | 10 | const styles = []; 11 | for (const key in computedStyle) { 12 | if (!isNumberString(key)) { 13 | const value = computedStyle[key]; 14 | styles.push(`${key}:${value}`); 15 | } 16 | } 17 | return styles.join(";"); 18 | }; 19 | 20 | const isNumberString = (str) => { 21 | if (!str) { 22 | return false; 23 | } 24 | let isNumberStr = true; 25 | for (let i = 0; i < str.length; i++) { 26 | const code = str.charCodeAt(i); 27 | const isNumberChar = code >= 48 && code <= 57; 28 | if (!isNumberChar) { 29 | isNumberStr = false; 30 | break; 31 | } 32 | } 33 | return isNumberStr; 34 | }; 35 | 36 | const getCaretNodeAndOffsetFromPoint = (ownerDocument, pointX, pointY) => { 37 | const position = ownerDocument.caretPositionFromPoint(pointX, pointY); 38 | if (!position) { 39 | return null; 40 | } 41 | return { 42 | node: position.offsetNode, 43 | offset: position.offset, 44 | }; 45 | }; 46 | 47 | export default { getComputedCssText, getCaretNodeAndOffsetFromPoint }; 48 | -------------------------------------------------------------------------------- /src/main/lib/ponyfill/ponyfill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import chrome from "./chrome"; 8 | import firefox from "./firefox"; 9 | import safari from "./safari"; 10 | 11 | let ponyfill; 12 | if (BROWSER === "chrome") { 13 | ponyfill = chrome; 14 | } 15 | if (BROWSER === "firefox") { 16 | ponyfill = firefox; 17 | } 18 | if (BROWSER === "safari") { 19 | ponyfill = safari; 20 | } 21 | export default ponyfill; 22 | -------------------------------------------------------------------------------- /src/main/lib/ponyfill/safari.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import chrome from "./chrome"; 8 | 9 | export default { 10 | getComputedCssText: chrome.getComputedCssText, 11 | getCaretNodeAndOffsetFromPoint: chrome.getCaretNodeAndOffsetFromPoint, 12 | }; 13 | -------------------------------------------------------------------------------- /src/main/lib/ribbon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import dom from "./dom"; 8 | 9 | const INDICATORS = ["⠿", "⠿", "⠿", "⠷", "⠯", "⠟", "⠻", "⠽", "⠾"]; 10 | 11 | const DEFAULT_STYLE = 12 | "position:absolute;width:100%;bottom:0;background-color:black;opacity:0.90;text-align:center;font-size:x-large;color:#FFFFFF"; 13 | 14 | const create = (style = "") => { 15 | const line = dom.create(`
`); 16 | 17 | const progress = dom.create(''); 18 | const indicator = dom.create(''); 19 | 20 | let indicators = [...INDICATORS]; 21 | 22 | let indicatorCount = 0; 23 | const intervalId = setInterval(() => { 24 | indicator.textContent = indicators[indicatorCount % indicators.length]; 25 | indicatorCount++; 26 | }, 150); 27 | 28 | line.appendChild(progress); 29 | line.appendChild(indicator); 30 | document.body.appendChild(line); 31 | 32 | const doUpdate = (text, newIndicators) => { 33 | progress.textContent = text; 34 | if (newIndicators) { 35 | indicators = newIndicators; 36 | } 37 | }; 38 | const doClose = () => { 39 | line.parentNode.removeChild(line); 40 | clearInterval(intervalId); 41 | }; 42 | return [doUpdate, doClose]; 43 | }; 44 | 45 | export default { create }; 46 | -------------------------------------------------------------------------------- /src/main/lib/shortcache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | class ShortCache { 8 | constructor(size) { 9 | this.size = size; 10 | this.list = createArray(size, ""); 11 | this.dict = new Map(); 12 | this.index = 0; 13 | } 14 | 15 | put(key, value) { 16 | if (this.get(key)) { 17 | return; 18 | } 19 | 20 | let currentData = this.list[this.index]; 21 | if (currentData) { 22 | const oldKey = currentData.key; 23 | this.dict.delete(oldKey); 24 | } else { 25 | currentData = {}; 26 | this.list[this.index] = currentData; 27 | } 28 | currentData.key = key; 29 | currentData.value = value; 30 | 31 | this.dict.set(key, this.index); 32 | 33 | this.index = (this.index + 1) % this.size; 34 | } 35 | 36 | get(key) { 37 | if (!key) { 38 | return null; 39 | } 40 | const index = this.dict.get(key); 41 | if (!Number.isFinite(index)) { 42 | return null; 43 | } 44 | return this.list[index].value; 45 | } 46 | } 47 | 48 | /** 49 | * Create a "packed" array. 50 | * See also: https://v8.dev/blog/elements-kinds 51 | */ 52 | const createArray = (length, initialValue) => { 53 | const newArray = []; 54 | for (let i = 0; i < length; i++) { 55 | newArray.push(initialValue); 56 | } 57 | return newArray; 58 | }; 59 | 60 | export default ShortCache; 61 | -------------------------------------------------------------------------------- /src/main/lib/snap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu, suiheilibe 4 | * Licensed under MIT 5 | */ 6 | 7 | import dom from "./dom"; 8 | 9 | const DEFAULT_STYLE = "position:fixed;background-color:#4169e1;opacity:0.20;z-index:2147483646;"; 10 | 11 | class Snap { 12 | constructor() { 13 | this.initialized = false; 14 | this.activated = false; 15 | this.minArea = 300 * 300; 16 | this.minSide = 90; 17 | this.isElementPresent = false; 18 | } 19 | 20 | initialize() { 21 | if (this.initialized) { 22 | return; 23 | } 24 | this.snapElement = dom.create(`
`); 25 | this.initialized = true; 26 | } 27 | 28 | appendElement() { 29 | if (this.isElementPresent) { 30 | return; 31 | } 32 | document.body.appendChild(this.snapElement); 33 | this.isElementPresent = true; 34 | } 35 | removeElement() { 36 | if (!this.isElementPresent) { 37 | return; 38 | } 39 | this.snapElement.remove(); 40 | this.isElementPresent = false; 41 | } 42 | 43 | update(pointX, pointY, square, clientWidth) { 44 | const range = this.fetchSnapRange(pointX, pointY, square, clientWidth); 45 | if (!range) { 46 | return; 47 | } 48 | this.lastRange = range; 49 | 50 | if (this.initialized) { 51 | this.transform(range); 52 | } 53 | } 54 | 55 | transform(range) { 56 | const style = { 57 | left: `${range.left}px`, 58 | top: `${range.top}px`, 59 | width: `${range.width}px`, 60 | height: `${range.height}px`, 61 | }; 62 | dom.applyStyles(this.snapElement, style); 63 | } 64 | 65 | activate() { 66 | this.initialize(); 67 | this.appendElement(); 68 | this.activated = true; 69 | } 70 | 71 | deactivate() { 72 | this.removeElement(); 73 | this.activated = false; 74 | } 75 | 76 | getRange() { 77 | if (!this.lastRange) { 78 | return null; 79 | } 80 | return { ...this.lastRange }; 81 | } 82 | 83 | fetchSnapRange(pointX, pointY, square, clientWidth) { 84 | if (square.left < 0) { 85 | const width = Math.min(clientWidth, window.innerWidth / 2); 86 | const height = window.innerHeight - 6; 87 | return { left: 0, top: 0, width, height }; 88 | } 89 | 90 | if (square.left + square.width > window.innerWidth) { 91 | const width = Math.min(clientWidth, window.innerWidth / 2); 92 | const height = window.innerHeight - 6; 93 | const left = document.documentElement.clientWidth - width; 94 | return { left, top: 0, width, height }; 95 | } 96 | 97 | const candidates = document.elementsFromPoint(pointX, pointY); 98 | return this.selectSnapElementRange(candidates); 99 | } 100 | 101 | selectSnapElementRange(elements) { 102 | for (let i = 1; i < elements.length; i++) { 103 | const e = elements[i]; 104 | if (this.isExceptionalElement(e)) { 105 | continue; 106 | } 107 | const range = adjustRange(e.getBoundingClientRect()); 108 | if (this.isEligibleElement(range)) { 109 | return range; 110 | } 111 | } 112 | return null; 113 | } 114 | 115 | isExceptionalElement(element) { 116 | if (element === this.snapElement) { 117 | return true; 118 | } 119 | const tagName = element.tagName; 120 | if (tagName === "BODY" || tagName === "HTML") { 121 | return true; 122 | } 123 | return false; 124 | } 125 | 126 | isEligibleElement(range) { 127 | if (range.width < this.minSide || range.height < this.minSide) { 128 | return false; 129 | } 130 | const rangeArea = range.width * range.height; 131 | const windowArea = window.innerWidth * window.innerHeight; 132 | if (rangeArea < this.minArea || rangeArea > windowArea / 2) { 133 | return false; 134 | } 135 | return true; 136 | } 137 | 138 | isActivated() { 139 | return this.activated; 140 | } 141 | } 142 | 143 | const adjustRange = (range) => { 144 | return { 145 | left: range.left, 146 | top: range.top, 147 | width: Math.min(range.width, window.innerWidth), 148 | height: Math.min(range.height, window.innerHeight), 149 | }; 150 | }; 151 | 152 | const build = () => { 153 | return new Snap(); 154 | }; 155 | 156 | export default { 157 | build, 158 | }; 159 | -------------------------------------------------------------------------------- /src/main/lib/sound.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu, suiheilibe 4 | * Licensed under MIT 5 | */ 6 | 7 | const pronounce = (text) => { 8 | if (!text) { 9 | return; 10 | } 11 | const ssu = new SpeechSynthesisUtterance(text); 12 | if (isEnglishLikeCharacter(text.charCodeAt(0))) { 13 | ssu.lang = "en-US"; 14 | } else { 15 | ssu.lang = "ja-JP"; 16 | } 17 | speechSynthesis.speak(ssu); 18 | }; 19 | 20 | // Temporary; 21 | const isEnglishLikeCharacter = (code) => 0x20 <= code && code <= 0x7e; 22 | 23 | export default { pronounce }; 24 | -------------------------------------------------------------------------------- /src/main/lib/storage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | const locals = { 8 | get: (...args) => chrome.storage.local.get(...args), 9 | set: (...args) => chrome.storage.local.set(...args), 10 | }; 11 | 12 | const syncs = { 13 | get: (...args) => chrome.storage.sync.get(...args), 14 | set: (...args) => chrome.storage.sync.set(...args), 15 | }; 16 | 17 | const sync = { 18 | get: async (args) => doAsync(syncs.get, args), 19 | set: async (args) => doAsync(syncs.set, args), 20 | async pick(key) { 21 | const data = await sync.get([key]); 22 | return data?.[key]; 23 | }, 24 | }; 25 | 26 | const local = { 27 | get: async (args) => doAsync(locals.get, args), 28 | set: async (args) => doAsync(locals.set, args), 29 | async pick(key) { 30 | const data = await local.get([key]); 31 | return data?.[key]; 32 | }, 33 | }; 34 | 35 | const doAsync = async (fn, params) => { 36 | if (!fn) { 37 | return null; 38 | } 39 | return new Promise((resolve, reject) => { 40 | try { 41 | const callBack = (data) => { 42 | if (chrome.runtime.lastError) { 43 | reject(chrome.runtime.lastError); 44 | } else { 45 | resolve(data); 46 | } 47 | }; 48 | if (params) { 49 | fn(params, callBack); 50 | } else { 51 | fn(callBack); 52 | } 53 | } catch (e) { 54 | reject(e); 55 | } 56 | }); 57 | }; 58 | 59 | export default { local, sync, doAsync }; 60 | -------------------------------------------------------------------------------- /src/main/lib/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import Mustache from "mustache"; 8 | 9 | const parse = (template) => Mustache.parse(template); 10 | 11 | const render = (template, view) => Mustache.render(template, view); 12 | 13 | export default { parse, render }; 14 | -------------------------------------------------------------------------------- /src/main/lib/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | const loadJson = async (fname) => { 8 | const url = chrome.runtime.getURL(fname); 9 | return fetch(url).then((r) => r.json()); 10 | }; 11 | 12 | const updateMap = (map, data) => { 13 | for (let i = 0; i < data.length; i++) { 14 | const arr = data[i]; 15 | map.set(arr[0], arr[1]); 16 | } 17 | }; 18 | 19 | /** 20 | * omap({ a: 1, b: 2, c: 3 }, v => v * 2, ["b", "c"]); 21 | * -> { a: 1, b: 4, c: 6 } 22 | */ 23 | const omap = (object, func, specifiedProps) => { 24 | const result = {}; 25 | const props = specifiedProps ?? Object.keys(object); 26 | for (let i = 0; i < props.length; i++) { 27 | const prop = props[i]; 28 | result[prop] = func ? func(object[prop]) : null; 29 | } 30 | return result; 31 | }; 32 | 33 | const areSame = (a, b) => { 34 | // On the assumption that both have the same properties 35 | const props = Object.keys(b); 36 | let same = true; 37 | for (let i = 0; i < props.length; i++) { 38 | const prop = props[i]; 39 | if (a[prop] !== b[prop]) { 40 | same = false; 41 | break; 42 | } 43 | } 44 | return same; 45 | }; 46 | 47 | const isInsideRange = (range, position) => { 48 | return ( 49 | position.x >= range.left && 50 | position.x <= range.left + range.width && 51 | position.y >= range.top && 52 | position.y <= range.top + range.height 53 | ); 54 | }; 55 | 56 | const convertToInt = (str) => { 57 | let r; 58 | if (str === null || str === undefined || str === "") { 59 | r = 0; 60 | } else { 61 | r = Number.parseInt(str, 10); 62 | if (Number.isNaN(r)) { 63 | r = 0; 64 | } 65 | } 66 | return r; 67 | }; 68 | 69 | const convertToStyles = (position) => { 70 | const styles = {}; 71 | const keys = Object.keys(position); 72 | for (let i = 0; i < keys.length; i++) { 73 | const key = keys[i]; 74 | const n = position[key]; 75 | if (Number.isFinite(n)) { 76 | styles[key] = `${n}px`; 77 | } 78 | } 79 | return styles; 80 | }; 81 | 82 | const optimizeInitialPosition = (position, minWindowSize = 50, edgeSpace = 5) => { 83 | const windowWidth = window.innerWidth; 84 | const windowHeight = window.innerHeight; 85 | 86 | return { 87 | left: clamp(position.left, edgeSpace, windowWidth - position.width - edgeSpace), 88 | top: clamp(position.top, edgeSpace, windowHeight - position.height - edgeSpace), 89 | width: clamp(position.width, minWindowSize, windowWidth - edgeSpace * 2), 90 | height: clamp(position.height, minWindowSize, windowHeight - edgeSpace * 2), 91 | }; 92 | }; 93 | 94 | const clamp = (value, minValue, maxValue) => { 95 | let r = value; 96 | r = min(r, maxValue); 97 | r = max(r, minValue); 98 | return r; 99 | }; 100 | 101 | const max = (a, b) => { 102 | if (Number.isFinite(a)) { 103 | return Math.max(a, b); 104 | } 105 | return null; 106 | }; 107 | 108 | const min = (a, b) => { 109 | if (Number.isFinite(a)) { 110 | return Math.min(a, b); 111 | } 112 | return null; 113 | }; 114 | 115 | const getSelection = () => { 116 | const selection = window.getSelection(); 117 | return selection.toString().replace("\r", " ").replace("\n", " ").trim(); 118 | }; 119 | 120 | export default { 121 | loadJson, 122 | updateMap, 123 | omap, 124 | areSame, 125 | isInsideRange, 126 | convertToInt, 127 | convertToStyles, 128 | optimizeInitialPosition, 129 | getSelection, 130 | }; 131 | -------------------------------------------------------------------------------- /src/main/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | export default { 8 | shortWordLength: 2, 9 | cutShortWordDescription: 30, 10 | parseWordsLimit: 8, 11 | lookupWithCapitalized: false, 12 | initialPosition: "right", 13 | skipPdfConfirmation: false, 14 | pdfUrl: "", 15 | backgroundColor: "#ffffff", 16 | headFontColor: "#000088", 17 | descFontColor: "#101010", 18 | headFontSize: "x-large", 19 | descFontSize: "small", 20 | 21 | width: 350, 22 | height: 500, 23 | 24 | replaceRules: [ 25 | { 26 | search: "(■.+|◆.+)", 27 | replace: '$1', 28 | }, 29 | { 30 | search: "({.+?}|\\[.+?\\]|\\(.+?\\))", 31 | replace: '$1', 32 | }, 33 | { 34 | search: "(【.+?】|《.+?》|〈.+?〉|〔.+?〕)", 35 | replace: '$1', 36 | }, 37 | { 38 | search: "\\n|\\\\n", 39 | replace: "
", 40 | }, 41 | ], 42 | 43 | normalDialogStyles: `{ 44 | "opacity": 0.95, 45 | "zIndex": 2147483647 46 | }`, 47 | 48 | movingDialogStyles: `{ 49 | "opacity": 0.6 50 | }`, 51 | 52 | hiddenDialogStyles: `{ 53 | "opacity": 0.0, 54 | "zIndex": -1 55 | }`, 56 | 57 | contentWrapperTemplate: `
58 |
`, 59 | 60 | dialogTemplate: `
72 |
`, 73 | 74 | contentTemplate: `
75 | {{#words}} 76 | {{^isShort}} 77 | 78 | {{head}} 79 | 80 | 🔊 81 |
82 | 83 | {{{desc}}} 84 | 85 | {{/isShort}} 86 | {{#isShort}} 87 | 88 | {{head}} 89 | 90 | 91 | {{shortDesc}} 92 | 93 | {{/isShort}} 94 | {{^isLast}} 95 |

96 | {{/isLast}} 97 | {{/words}} 98 |
`, 99 | domType: "shadow", 100 | }; 101 | -------------------------------------------------------------------------------- /src/main/start.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import launch from "./core/launch"; 8 | 9 | const main = async () => { 10 | DEBUG && console.time("launch"); 11 | await launch(); 12 | DEBUG && console.timeEnd("launch"); 13 | }; 14 | 15 | main(); 16 | -------------------------------------------------------------------------------- /src/options/app.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import { useEffect, useState } from "react"; 8 | import { createRoot } from "react-dom/client"; 9 | import swal from "sweetalert"; 10 | import rule from "../main/core/rule"; 11 | import { res } from "./logic"; 12 | import { Main } from "./page/Main"; 13 | 14 | import ace from "ace-builds/src-noconflict/ace"; 15 | import "ace-builds/src-noconflict/mode-html"; 16 | import "ace-builds/src-noconflict/mode-json"; 17 | import "ace-builds/src-noconflict/theme-xcode"; 18 | import "ace-builds/src-noconflict/theme-tomorrow"; 19 | import "ace-builds/src-noconflict/theme-solarized_light"; 20 | ace.config.set("basePath", "/options"); 21 | 22 | res.setLang(res.decideInitialLanguage([...navigator.languages])); 23 | 24 | window.onerror = (msg) => { 25 | swal({ 26 | text: msg.toString(), 27 | icon: "error", 28 | }); 29 | }; 30 | 31 | const sendMessage = async (message: any) => { 32 | return new Promise((done) => { 33 | chrome.runtime.sendMessage(message, (response) => { 34 | done(response); 35 | }); 36 | }); 37 | }; 38 | 39 | const App = () => { 40 | const [mode, setMode] = useState<"loading" | "options" | "pdf">("loading"); 41 | 42 | const showPdfViewer = (id: string) => { 43 | setMode("pdf"); 44 | location.href = `pdf/web/viewer.html?id=${id}`; 45 | }; 46 | 47 | useEffect(() => { 48 | const init = async (): Promise => { 49 | const id = (await sendMessage({ type: "shift_pdf_id" })) as string; 50 | if (id) { 51 | showPdfViewer(id); 52 | } else { 53 | setMode("options"); 54 | } 55 | }; 56 | init(); 57 | 58 | chrome.runtime.onMessage.addListener(async (request) => { 59 | switch (request?.type) { 60 | case "prepare_pdf": { 61 | const id = (await sendMessage({ type: "shift_pdf_id" })) as string; 62 | if (id) { 63 | showPdfViewer(id); 64 | } 65 | break; 66 | } 67 | } 68 | }); 69 | }, []); 70 | 71 | switch (mode) { 72 | case "loading": 73 | return
; 74 | case "options": 75 | return
; 76 | case "pdf": 77 | return
; 78 | } 79 | }; 80 | 81 | const root = createRoot(document.getElementById("app") as HTMLElement); 82 | 83 | root.render(); 84 | 85 | // Lazy load 86 | rule.load(); 87 | -------------------------------------------------------------------------------- /src/options/component/atom/Button.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import type { MouseEventHandler } from "react"; 8 | 9 | type Props = { 10 | text: string; 11 | onClick: MouseEventHandler; 12 | type: "primary" | "cancel" | "revert" | "json"; 13 | disabled?: boolean; 14 | }; 15 | 16 | const CLASS = { 17 | primary: "", 18 | cancel: "button-outline button-black", 19 | revert: "button-outline button-small", 20 | json: "button-black", 21 | }; 22 | 23 | export const Button: React.FC = (props) => { 24 | return ( 25 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/options/component/atom/DataUsage.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import { useEffect } from "react"; 8 | import { config, storage } from "../../extern"; 9 | import { res } from "../../logic"; 10 | 11 | type Props = { 12 | byteSize: number | undefined; 13 | onUpdate: (byteSize: number) => void; 14 | }; 15 | 16 | export const DataUsage: React.FC = (props) => { 17 | useEffect(() => { 18 | const updateSize = async () => { 19 | if (props.byteSize === undefined) { 20 | const newSize = (await config.getBytesInUse()) ?? -1; 21 | props.onUpdate(newSize); 22 | } 23 | if (props.byteSize === -1) { 24 | const pSize = storage.local.getBytesInUse(); 25 | // Waits at least 500 ms in order to show off doing something :-) 26 | await Promise.all([pSize, wait(500)]); 27 | const newSize = (await pSize) ?? 0; 28 | config.setBytesInUse(newSize); 29 | props.onUpdate(newSize); 30 | } 31 | }; 32 | updateSize(); 33 | }, [props.byteSize]); 34 | 35 | if (props.byteSize === undefined || props.byteSize === -1) { 36 | return ( 37 |
38 | 39 |
40 | ); 41 | } 42 | 43 | const sizeString = Math.floor(props.byteSize / 1024).toLocaleString(); 44 | const sizeInfo = res.get("dictDataUsage", { size: sizeString }); 45 | return ( 46 |
props.onUpdate(-1)}> 47 | {sizeInfo} 48 |
49 | ); 50 | }; 51 | 52 | const wait = (time: number): Promise => 53 | new Promise((done) => { 54 | setTimeout(() => { 55 | done(); 56 | }, time); 57 | }); 58 | -------------------------------------------------------------------------------- /src/options/component/atom/EditableSpan.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import { useState } from "react"; 8 | 9 | type Props = { 10 | value: string; 11 | style: React.CSSProperties; 12 | onChange: (e: React.ChangeEvent) => void; 13 | }; 14 | 15 | export const EditableSpan: React.FC = (props) => { 16 | const [editable, setEditable] = useState(props.value === ""); 17 | 18 | if (!editable) { 19 | return ( 20 | setEditable(true)}> 21 | {props.value} 22 | 23 | ); 24 | } 25 | 26 | return ( 27 | props.onChange(e)} 32 | onKeyDown={(e) => { 33 | if (e.key === "Enter") { 34 | setEditable(false); 35 | } 36 | }} 37 | /> 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/options/component/atom/ExternalLink.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | type Props = { 8 | href: string; 9 | icon?: boolean; 10 | style?: React.CSSProperties; 11 | children?: React.ReactNode; 12 | }; 13 | 14 | export const ExternalLink: React.FC = (props) => { 15 | return ( 16 | 22 | {props.children} 23 | {props.icon && } 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/options/component/atom/HighlightEditor.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import AceEditor from "react-ace"; 8 | 9 | type Props = { 10 | value: string; 11 | mode: string; 12 | theme: string; 13 | style: React.CSSProperties; 14 | onChange?: (value: string, event?: any) => void; 15 | }; 16 | 17 | const DEFAULT_STYLE = { 18 | width: 800, 19 | border: "1px solid #d1d1d1", 20 | borderRadius: "3px", 21 | fontSize: 13, 22 | marginBottom: 20, 23 | }; 24 | 25 | export const HighlightEditor: React.FC = (props) => { 26 | return ( 27 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/options/component/atom/Launch.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import { useState } from "react"; 8 | 9 | type Props = { 10 | text: string; 11 | href: string; 12 | image: string; 13 | style?: React.CSSProperties; 14 | }; 15 | 16 | const STYLE_OUTER: React.CSSProperties = { 17 | width: 180, 18 | }; 19 | 20 | const STYLE_IMAGE: React.CSSProperties = { 21 | verticalAlign: "middle", 22 | }; 23 | 24 | const STYLE1: React.CSSProperties = { 25 | backgroundColor: "#ffffff", 26 | boxShadow: "2px 2px 2px 2px #b0b0b0", 27 | borderRadius: 10, 28 | paddingLeft: 6, 29 | paddingTop: 10, 30 | paddingBottom: 10, 31 | marginTop: 10, 32 | width: 180, 33 | textAlign: "center", 34 | verticalAlign: "middle", 35 | }; 36 | 37 | const STYLE2: React.CSSProperties = { 38 | ...STYLE1, 39 | backgroundColor: "#fff4ff", 40 | }; 41 | 42 | export const Launch: React.FC = (props) => { 43 | const [hover, setHover] = useState(false); 44 | 45 | const enter = () => { 46 | setHover(true); 47 | }; 48 | const leave = () => { 49 | setHover(false); 50 | }; 51 | 52 | return ( 53 | 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /src/options/component/atom/Overlay.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | type Props = { 8 | active: boolean; 9 | children?: React.ReactNode; 10 | }; 11 | 12 | export const Overlay: React.FC = (props) => { 13 | if (!props.active) { 14 | return <>; 15 | } 16 | return ( 17 |
29 | {props.children} 30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/options/component/atom/Panel.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | type Props = { 8 | active: boolean; 9 | children?: React.ReactNode; 10 | }; 11 | 12 | export const Panel: React.FC = (props) => { 13 | if (!props.active) { 14 | return <>; 15 | } 16 | return <>{props.children}; 17 | }; 18 | -------------------------------------------------------------------------------- /src/options/component/atom/Select.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | type Props = { 8 | value: string; 9 | options: { value: string; name: string }[]; 10 | onChange: (value: string) => void; 11 | style?: React.CSSProperties; 12 | ref?: React.Ref; 13 | }; 14 | 15 | export const Select: React.FC = (props) => { 16 | return ( 17 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/options/component/atom/Switch.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | type Props = { 8 | visible: boolean; 9 | children?: React.ReactNode; 10 | }; 11 | 12 | export const Switch: React.FC = (props) => { 13 | if (!props.visible) { 14 | return <>; 15 | } 16 | return <>{props.children}; 17 | }; 18 | -------------------------------------------------------------------------------- /src/options/component/atom/Toggle.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import type { MouseEventHandler } from "react"; 8 | 9 | type Props = { 10 | switch: boolean; 11 | image: string; 12 | text1: string; 13 | text2: string; 14 | onClick: MouseEventHandler; 15 | }; 16 | 17 | const style = { verticalAlign: "bottom", marginRight: 2, transition: "0.5s" }; 18 | const style1 = { ...style, transform: "rotateZ(0deg)" }; 19 | const style2 = { ...style, transform: "rotateZ(-250deg)" }; 20 | 21 | export const Toggle: React.FC = (props) => { 22 | return ( 23 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/options/component/atom/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | export * from "./Button"; 8 | export * from "./DataUsage"; 9 | export * from "./EditableSpan"; 10 | export * from "./ExternalLink"; 11 | export * from "./HighlightEditor"; 12 | export * from "./Launch"; 13 | export * from "./Overlay"; 14 | export * from "./Panel"; 15 | export * from "./Select"; 16 | export * from "./Switch"; 17 | export * from "./Toggle"; 18 | -------------------------------------------------------------------------------- /src/options/component/organism/LoadDictionary.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import { useEffect, useRef, useState } from "react"; 8 | import { res } from "../../logic"; 9 | import { detectFileEncoding } from "../../logic/encoding"; 10 | import type { DictionaryFileEncoding, DictionaryFileFormat } from "../../types"; 11 | import { Button } from "../atom/Button"; 12 | import { Select } from "../atom/Select"; 13 | 14 | type Props = { 15 | defaultEncoding?: DictionaryFileEncoding; 16 | defaultFormat?: DictionaryFileFormat; 17 | busy: boolean; 18 | trigger: (e: TriggerEvent) => void; 19 | }; 20 | 21 | type TriggerEvent = { 22 | type: "load"; 23 | payload: { 24 | file: File | undefined; 25 | encoding: DictionaryFileEncoding; 26 | format: DictionaryFileFormat; 27 | }; 28 | }; 29 | 30 | export const LoadDictionary: React.FC = (props) => { 31 | const [encoding, setEncoding] = useState("Shift_JIS"); 32 | const [format, setFormat] = useState("EIJIRO"); 33 | const [file, setFile] = useState(undefined); 34 | const selectRef = useRef(null); 35 | 36 | const ENCODINGS = [ 37 | { value: "Shift_JIS", name: "Shift_JIS" }, 38 | { value: "UTF-8", name: "UTF-8" }, 39 | { value: "UTF-16", name: "UTF-16" }, 40 | ]; 41 | 42 | const FORMATS = [ 43 | { value: "EIJIRO", name: res.get("formatEijiroText") }, 44 | { value: "TSV", name: res.get("formatTsv") }, 45 | { value: "PDIC_LINE", name: res.get("formatPdicOneLine") }, 46 | { value: "JSON", name: res.get("formatJson") }, 47 | ]; 48 | 49 | useEffect(() => { 50 | if (!file) { 51 | return; 52 | } 53 | const load = async () => { 54 | const detectedEncoding = await detectFileEncoding(file); 55 | if (detectedEncoding === "Unknown") { 56 | return; 57 | } 58 | const detectedEncodingName = ( 59 | detectedEncoding === "ASCII" ? "UTF-8" : detectedEncoding 60 | ) as DictionaryFileEncoding; 61 | setEncoding(detectedEncodingName); 62 | 63 | if (selectRef.current) { 64 | selectRef.current.style.transition = "background-color 0.5s ease"; 65 | selectRef.current.style.backgroundColor = "lightyellow"; 66 | setTimeout(() => { 67 | if (selectRef.current) { 68 | selectRef.current.style.backgroundColor = ""; 69 | } 70 | }, 1500); 71 | } 72 | }; 73 | load(); 74 | }, [file]); 75 | 76 | return ( 77 |
78 | 79 | setFormat(value as DictionaryFileFormat)} /> 87 | 88 | setFile(e.target.files?.[0])} /> 89 |
90 |
111 | ); 112 | }; 113 | -------------------------------------------------------------------------------- /src/options/component/organism/OperationPanel.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Mouse Dictionary (https://github.com/wtetsu/mouse-dictionary/) 3 | * Copyright 2018-present wtetsu 4 | * Licensed under MIT 5 | */ 6 | 7 | import { useState } from "react"; 8 | import { res } from "../../logic"; 9 | import { Button } from "../atom/Button"; 10 | 11 | type Props = { 12 | disable: boolean; 13 | trigger: (type: "save" | "factoryReset") => void; 14 | }; 15 | 16 | const style = { 17 | backgroundColor: "#FFFFFF", 18 | color: "#FF4500", 19 | transition: "0.5s", 20 | }; 21 | const style1 = { ...style, opacity: 0.0 }; 22 | const style2 = { ...style, opacity: 1.0 }; 23 | 24 | export const OperationPanel: React.FC = (props) => { 25 | const [mode, setMode] = useState<1 | 2>(1); 26 | 27 | return ( 28 | <> 29 |
30 |
44 | 92 | 100 | 107 | update({ 108 | type: "change", 109 | payload: { index: i, target: "search", value: e.target.value }, 110 | }) 111 | } 112 | /> 113 | {res.get("replaceRule1")} 114 | 121 | update({ 122 | type: "change", 123 | payload: { index: i, target: "replace", value: e.target.value }, 124 | }) 125 | } 126 | /> 127 | {res.get("replaceRule2")} 128 | 129 | 137 |
138 | ))} 139 |