├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ └── release.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── esbuild.config.mjs ├── manifest.json ├── package-lock.json ├── package.json ├── public ├── changingword.png ├── newreading.png ├── newsetting.png ├── reading.png ├── recordword.png ├── relgraph.png ├── tutorial.pdf └── wordfile.png ├── publish.mjs ├── shims.d.ts ├── src ├── api │ └── server.ts ├── component │ └── WordMore.vue ├── constant.ts ├── db │ ├── base.ts │ ├── file_db.ts │ ├── idb.ts │ ├── interface.ts │ ├── local_db.ts │ └── web_db.ts ├── dictionary │ ├── cambridge │ │ ├── View.vue │ │ └── engine.ts │ ├── deepl │ │ ├── View.vue │ │ └── engine.ts │ ├── helpers.ts │ ├── hjdict │ │ ├── View.vue │ │ └── engine.ts │ ├── jukuu │ │ ├── View.vue │ │ └── engine.ts │ ├── list.ts │ ├── uses.ts │ └── youdao │ │ ├── View.vue │ │ ├── YDCollins.vue │ │ └── engine.ts ├── lang │ ├── helper.ts │ └── locale │ │ ├── en.ts │ │ ├── zh-TW.ts │ │ └── zh.ts ├── main.css ├── modals.ts ├── plugin.ts ├── settings.ts ├── stalin.css ├── store.ts ├── utils │ ├── frontmatter.ts │ ├── helpers.ts │ ├── style.ts │ └── use.ts └── views │ ├── CountBar.vue │ ├── DataPanel.vue │ ├── DataPanelView.ts │ ├── DictItem.vue │ ├── Global.vue │ ├── LearnPanel.vue │ ├── LearnPanelView.ts │ ├── PDFView.ts │ ├── PopupSearch.vue │ ├── ReadingArea.vue │ ├── ReadingView.ts │ ├── SearchPanel.vue │ ├── SearchPanelView.ts │ ├── Stat.vue │ ├── StatView.ts │ └── parser.ts ├── styles.css ├── tsconfig.json ├── version-bump.mjs └── versions.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | tab_width = 4 10 | line_width = 120 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | custom: ['https://www.buymeacoffee.com/thtree'] 3 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build obsidian plugin 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - "*" # Push events to matching any tag format, i.e. 1.0, 20.15.10 8 | 9 | env: 10 | PLUGIN_NAME: obsidian-language-learner # Change this to the name of your plugin-id folder 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: "16.x" # You might need to adjust this value to your own version 22 | - name: Build 23 | id: build 24 | run: | 25 | npm install 26 | npm run build 27 | mkdir ${{ env.PLUGIN_NAME }} 28 | cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }} 29 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} 30 | ls 31 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" 32 | - name: Create Release 33 | id: create_release 34 | uses: actions/create-release@v1 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | VERSION: ${{ github.ref }} 38 | with: 39 | tag_name: ${{ github.ref }} 40 | release_name: ${{ github.ref }} 41 | draft: false 42 | prerelease: false 43 | - name: Upload zip file 44 | id: upload-zip 45 | uses: actions/upload-release-asset@v1 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | with: 49 | upload_url: ${{ steps.create_release.outputs.upload_url }} 50 | asset_path: ./${{ env.PLUGIN_NAME }}.zip 51 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip 52 | asset_content_type: application/zip 53 | - name: Upload main.js 54 | id: upload-main 55 | uses: actions/upload-release-asset@v1 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | with: 59 | upload_url: ${{ steps.create_release.outputs.upload_url }} 60 | asset_path: ./main.js 61 | asset_name: main.js 62 | asset_content_type: text/javascript 63 | - name: Upload manifest.json 64 | id: upload-manifest 65 | uses: actions/upload-release-asset@v1 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | with: 69 | upload_url: ${{ steps.create_release.outputs.upload_url }} 70 | asset_path: ./manifest.json 71 | asset_name: manifest.json 72 | asset_content_type: application/json 73 | - name: Upload styles.css 74 | id: upload-css 75 | uses: actions/upload-release-asset@v1 76 | env: 77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | with: 79 | upload_url: ${{ steps.create_release.outputs.upload_url }} 80 | asset_path: ./styles.css 81 | asset_name: styles.css 82 | asset_content_type: text/css -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | /main.css 15 | 16 | # Exclude sourcemaps 17 | *.map 18 | 19 | # obsidian 20 | data.json 21 | 22 | # Exclude macOS Finder (System Explorer) View States 23 | .DS_Store 24 | 25 | # pdf 26 | pdf -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 the_tree 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # 前言 3 | 4 | 语言学习插件 language-learner 在0.2.5版本中开发者已经实现了查词、阅读模式、添加笔记等功能,基础操作可参见[B站视频](https://www.bilibili.com/video/BV1N24y1k7SL/?vd_source=595ecb634d520a0458c323613451f9a6)和[原项目地址](https://github.com/guopenghui/obsidian-language-learner)。通过二次开发,本项目将继续为爱发电,一起用obsidian高效地学习语言吧。 5 | 6 | 0.2.5版本主要面板: 7 | 8 | 9 | ![主要面板](public/reading.png) 10 | 11 | # 新增功能 12 | 13 | ## Markdown渲染 14 | 15 | 相比于0.2.5版本,现阅读模式文本已支持 md 渲染(如多级标题、粗斜体、本地图片或网络图片等的渲染) 16 | 17 | 18 | 19 | 20 | ![0.3.1版本阅读模式](public/newreading.png) 21 | 22 | ## 变形单词识别 23 | 24 | 在学习面板中填写单词的变形形式,每个变形单词用","隔开,可以添加名词复数、动词时态语态变化形式等(小提示:可以直接复制词典中的变形单词到变形栏中,不用一个一个打) 25 | 26 | ![记录单词](public/recordword.png) 27 | 28 | 提交后可以看到have的各种形式都识别到了,学习状态与have相同: 29 | 30 | ![变形单词识别](public/changingword.png) 31 | 32 | ## 单词文件库 33 | 34 | 想要更直观的看到自己的单词数据?想要自由操作自己的单词数据?想要使用dataview展示单词信息? 35 | 36 | 可以在设置里开启单词文件库,选择一个存放单词文件的文件夹后,每当退出阅读模式,将自动生成单词的md文件。如果你已经使用 0.2.5 版本一段时间了,indexDB中有了一些单词数据,你可以点击“更新单词文件库”,它会自动把indexDB中的非无视状态(新学、眼熟等状态)的单词写入单词文件库路径文件夹 37 | 38 | 39 | ![新增设置项](public/newsetting.png) 40 | 41 | 生成的单词文件如下: 42 | 43 | 44 | ![单词文件](public/wordfile.png) 45 | 46 | 单词信息存放在每个md文件的frontmatter中,你可以自由修改单词信息,修改后点击“更新indexDB数据库”,indexDB数据库的数据就会更新同步,更新过程中无视状态的单词不会受到影响。(你也可以删除某个单词文件,indexDB数据库中的该单词数据也会删除) 47 | 48 | “更新indexDB数据库”的另一种用法是多设备的同步,你可以Obsidian文件同步方法来同步单词文件,这样每当一个设备的单词文件变化时,文件同步后在另一个设备点击“更新indexDB数据库”即可实现多设备中indexDB的同步 49 | 50 | 如果你觉得同时使用indexDB数据库和单词文件库太繁琐了,你可以打开“仅使用单词文件库”,这样插件的运行将只与单词文件库交互(无视状态的单词还是会写入indexDB),多端的同步也更方便。需要注意的是,开启此功能会删除indexDB中的非无视单词的信息,而打开后会自动根据最新的单词文件库把非无视单词的信息写入indexDB 51 | 52 | 得益于obsidian的双链,打开关系图谱会发现单词和所在的文件连在了一起。 53 | 54 | ![关系图谱](public/relgraph.png) 55 | 56 | 关系图谱配色推荐: 57 | 58 | ["status":新学]:#ff9800 59 | 60 | ["status":眼熟]:#ffeb3c 61 | 62 | ["status":了解]:#9eda58 63 | 64 | ["status":掌握]:#4cb051 65 | 66 | path:文章路径 :#9c9c9c 67 | 68 | ## 安装 69 | 70 | 视频教程:[0.3.1版本教程]( https://www.bilibili.com/video/BV1rkWSefEYQ/?share_source=copy_web&vd_source=9be45cda2ce3f5c9a05bf519a7555757) 71 | 72 | - 从realease下载最新插件压缩包 73 | - 解压到obsidian库的`.obsidian/plugins/`文件夹下 74 | - 重启obsidian,在插件中启用本插件`Language Learner`. 75 | - 配置见[使用指南](public/tutorial.pdf) 76 | 77 | ## 自行构建 78 | 79 | 下载源码到本地 80 | 81 | ``` 82 | git clone https://github.com/asa-world/obsidian-language-learner.git 83 | ``` 84 | 85 | 进入文件夹,运行 86 | 87 | ``` 88 | cd obsidian-language-learner 89 | # 安装依赖 90 | npm install 91 | # 构建 会自动压缩代码体积 92 | npm run build 93 | ``` 94 | 95 | ## 问题或建议 96 | 97 | 欢迎大家提交issue: 98 | 99 | - bug反馈 100 | - 对新功能的想法 101 | - 对已有功能的优化 102 | 103 | 可能有时作者暂时比较忙,或是对于提出的功能需求暂时没想到好的实现方法而没有一一回复。 104 | 105 | 但是只要提了issue都会看的,所以大家有想法或反馈直接发到issue就行。 -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from 'builtin-modules'; 4 | import vue from "@the_tree/esbuild-plugin-vue3"; 5 | 6 | const banner = 7 | `/* 8 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 9 | if you want to view the source, please visit the github repository of this plugin 10 | */ 11 | `; 12 | 13 | const prod = (process.argv[2] === 'production'); 14 | 15 | await esbuild.build({ 16 | banner: { 17 | js: banner, 18 | }, 19 | plugins: [ 20 | vue({ isProd: true }), 21 | ], 22 | entryPoints: ['./src/plugin.ts'], 23 | bundle: true, 24 | external: [ 25 | 'obsidian', 26 | 'electron', 27 | '@codemirror/autocomplete', 28 | '@codemirror/closebrackets', 29 | '@codemirror/collab', 30 | '@codemirror/commands', 31 | '@codemirror/comment', 32 | '@codemirror/fold', 33 | '@codemirror/gutter', 34 | '@codemirror/highlight', 35 | '@codemirror/history', 36 | '@codemirror/language', 37 | '@codemirror/lint', 38 | '@codemirror/matchbrackets', 39 | '@codemirror/panel', 40 | '@codemirror/rangeset', 41 | '@codemirror/rectangular-selection', 42 | '@codemirror/search', 43 | '@codemirror/state', 44 | '@codemirror/stream-parser', 45 | '@codemirror/text', 46 | '@codemirror/tooltip', 47 | '@codemirror/view', 48 | ...builtins], 49 | format: 'cjs', 50 | watch: !prod, 51 | target: 'es2016', 52 | logLevel: "info", 53 | sourcemap: prod ? false : 'inline', 54 | minify: prod ? true : false, 55 | treeShaking: true, 56 | outfile: 'main.js', 57 | }).catch(() => process.exit(1)); 58 | 59 | await esbuild.build({ 60 | entryPoints: ["./src/main.css"], 61 | outfile: "styles.css", 62 | watch: !prod, 63 | bundle: true, 64 | allowOverwrite: true, 65 | minify: false, 66 | }); 67 | 68 | // if (!prod) { 69 | // fs.rm("./main.css", () => { 70 | // console.log("Build completed successfully.") 71 | // }) 72 | // } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-language-learner", 3 | "name": "Language Learner", 4 | "version": "0.3.4", 5 | "minAppVersion": "0.12.17", 6 | "description": "阅读、查词、复习、使用、统计全部合一的语言学习插件", 7 | "author": "the_tree, Asa", 8 | "authorUrl": "asa-world.cn", 9 | "isDesktopOnly": false 10 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-sample-plugin", 3 | "version": "1.0.1", 4 | "description": "This is a sample plugin for Obsidian (https://obsidian.md)", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "pub": "node publish.mjs", 10 | "version": "node version-bump.mjs && git add manifest.json versions.json" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "MIT", 15 | "dependencies": { 16 | "@vueuse/core": "^9.6.0", 17 | "ac-auto": "^2.0.0", 18 | "dexie": "^3.2.2", 19 | "dexie-export-import": "^1.0.3", 20 | "downloadjs": "^1.4.7", 21 | "echarts": "^5.3.2", 22 | "monkey-around": "^2.3.0", 23 | "nlcst-to-string": "^3.1.0", 24 | "parse-english": "^5.0.0", 25 | "retext-english": "^4.1.0", 26 | "unified": "^10.1.2", 27 | "unist-util-modify-children": "^3.0.0", 28 | "unist-util-visit": "^4.1.0", 29 | "vue": "^3.2.31" 30 | }, 31 | "devDependencies": { 32 | "@the_tree/esbuild-plugin-vue3": "^0.3.1", 33 | "@types/downloadjs": "^1.4.3", 34 | "@types/node": "^16.11.6", 35 | "@typescript-eslint/eslint-plugin": "^5.2.0", 36 | "@typescript-eslint/parser": "^5.2.0", 37 | "builtin-modules": "^3.2.0", 38 | "esbuild": "0.13.12", 39 | "hash-sum": "^2.0.0", 40 | "naive-ui": "^2.33.0", 41 | "obsidian": "latest", 42 | "sass": "^1.56.1", 43 | "tslib": "2.3.1", 44 | "typescript": "4.4.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /public/changingword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asa-world/obsidian-language-learner/e39ae1a2354841e11ab66d13ba8ca91f6a76297b/public/changingword.png -------------------------------------------------------------------------------- /public/newreading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asa-world/obsidian-language-learner/e39ae1a2354841e11ab66d13ba8ca91f6a76297b/public/newreading.png -------------------------------------------------------------------------------- /public/newsetting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asa-world/obsidian-language-learner/e39ae1a2354841e11ab66d13ba8ca91f6a76297b/public/newsetting.png -------------------------------------------------------------------------------- /public/reading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asa-world/obsidian-language-learner/e39ae1a2354841e11ab66d13ba8ca91f6a76297b/public/reading.png -------------------------------------------------------------------------------- /public/recordword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asa-world/obsidian-language-learner/e39ae1a2354841e11ab66d13ba8ca91f6a76297b/public/recordword.png -------------------------------------------------------------------------------- /public/relgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asa-world/obsidian-language-learner/e39ae1a2354841e11ab66d13ba8ca91f6a76297b/public/relgraph.png -------------------------------------------------------------------------------- /public/tutorial.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asa-world/obsidian-language-learner/e39ae1a2354841e11ab66d13ba8ca91f6a76297b/public/tutorial.pdf -------------------------------------------------------------------------------- /public/wordfile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asa-world/obsidian-language-learner/e39ae1a2354841e11ab66d13ba8ca91f6a76297b/public/wordfile.png -------------------------------------------------------------------------------- /publish.mjs: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import process from "process"; 3 | import child from "child_process"; 4 | import path from "path"; 5 | 6 | const version = process.argv[2]; 7 | const root = process.cwd(); 8 | 9 | // write version 10 | let manifest = JSON.parse(fs.readFileSync(path.join(root, "manifest.json"), "utf8")); 11 | if (manifest.version !== version) { 12 | manifest.version = version; 13 | fs.writeFileSync(path.join(root, "manifest.json"), JSON.stringify(manifest, null, 4)); 14 | // message must use " instead of ' on windows 15 | child.execSync('git commit -am "update manifest"'); 16 | } 17 | 18 | child.execSync("git push"); 19 | child.execSync(`git tag ${version}`); 20 | child.execSync("git push --tags"); 21 | 22 | console.log("> Publish succeeded."); 23 | -------------------------------------------------------------------------------- /shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { ComponentOptions, DefineComponent } from 'vue'; 3 | const componentOptions: ComponentOptions; 4 | export default componentOptions; 5 | // const defineComponent: DefineComponent; 6 | // export default defineComponent; 7 | } -------------------------------------------------------------------------------- /src/api/server.ts: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import url from "url"; 3 | import LanguageLearner from "@/plugin"; 4 | import { dict } from "@/constant"; 5 | 6 | const ALLOWED_HEADERS = 7 | 'Access-Control-Allow-Headers, Origin, Authorization,Accept,x-client-id, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers, hypothesis-client-version'; 8 | 9 | const mimeType = { 10 | '.ico': 'image/x-icon', 11 | '.html': 'text/html', 12 | '.js': 'text/javascript', 13 | '.json': 'application/json', 14 | '.css': 'text/css', 15 | '.png': 'image/png', 16 | '.jpg': 'image/jpeg', 17 | '.wav': 'audio/wav', 18 | '.mp3': 'audio/mpeg', 19 | '.svg': 'image/svg+xml', 20 | '.pdf': 'application/pdf', 21 | '.zip': 'application/zip', 22 | '.doc': 'application/msword', 23 | '.eot': 'application/vnd.ms-fontobject', 24 | '.ttf': 'application/x-font-ttf' 25 | }; 26 | 27 | type RequestType = "LOAD" | "STORE" | "TAG" | "ECHO" | "OTHER"; 28 | 29 | 30 | export default class Server { 31 | plugin: LanguageLearner; 32 | _server: http.Server; 33 | port: number; 34 | 35 | constructor(plugin: LanguageLearner, port: number) { 36 | this.plugin = plugin; 37 | this.port = port; 38 | } 39 | 40 | async _startListen(port: number): Promise { 41 | return new Promise((resolve, reject) => { 42 | this._server.listen(port, () => { 43 | resolve(); 44 | }); 45 | }); 46 | } 47 | 48 | async start() { 49 | const server = http.createServer(); 50 | this._server = server; 51 | server.on("request", this.process); 52 | await this._startListen(this.port); 53 | console.log(`${dict["NAME"]}: Server established on port ${this.port}`); 54 | } 55 | 56 | async _closeServer() { 57 | return new Promise((resolve, reject) => { 58 | this._server.close(() => { 59 | resolve(); 60 | }); 61 | }); 62 | } 63 | 64 | async close() { 65 | await this._closeServer(); 66 | this._server = null; 67 | console.log(`${dict["NAME"]}: Server on port ${this.port} has closed`); 68 | } 69 | 70 | process = async (req: http.IncomingMessage, res: http.ServerResponse) => { 71 | res.setHeader('Access-Control-Allow-Origin', '*'); 72 | res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE'); 73 | res.setHeader('Access-Control-Allow-Headers', ALLOWED_HEADERS); 74 | res.setHeader('Access-Control-Allow-Credentials', 'true'); 75 | if (req.method === "OPTIONS") { 76 | res.end(); 77 | return; 78 | } 79 | 80 | let type = this.parseUrl(req.url); 81 | switch (type) { 82 | case "ECHO": { 83 | // console.log("hello from chrome") 84 | res.setHeader("Keep-Alive", "timeout=0"); 85 | res.statusCode = 200; 86 | res.end("hi"); 87 | break; 88 | } 89 | case "LOAD": { 90 | let data = await this.parseData(req, res); 91 | let expr = await this.plugin.db.getExpression(data); 92 | res.setHeader('Content-type', mimeType[".json"]); 93 | res.statusCode = 200; 94 | res.end(JSON.stringify(expr)); 95 | break; 96 | } 97 | case "STORE": { 98 | let data = await this.parseData(req, res); 99 | await this.plugin.db.postExpression(data); 100 | res.statusCode = 200; 101 | res.end(); 102 | if (this.plugin.settings.auto_refresh_db) { 103 | this.plugin.refreshTextDB(); 104 | } 105 | break; 106 | } 107 | case "TAG": { 108 | let tags = await this.plugin.db.getTags(); 109 | res.setHeader('Content-type', mimeType[".json"]); 110 | res.statusCode = 200; 111 | res.end(JSON.stringify(tags)); 112 | break; 113 | } 114 | default: { 115 | res.statusCode = 400; 116 | res.end("Bad Request"); 117 | } 118 | } 119 | }; 120 | 121 | async parseData(req: http.IncomingMessage, res: http.ServerResponse): Promise { 122 | return new Promise((resolve, reject) => { 123 | let _data: number[] = []; 124 | req.on("data", chunk => { 125 | _data.push(...chunk); 126 | }); 127 | req.on("end", async () => { 128 | let rawtext = new TextDecoder().decode(new Uint8Array(_data)); 129 | if (!rawtext) rawtext = '"hello"'; 130 | let data = JSON.parse(rawtext); 131 | resolve(data); 132 | }); 133 | }); 134 | 135 | } 136 | 137 | parseUrl(_url: string): RequestType { 138 | let urlObj = url.parse(_url, true); 139 | if (urlObj.path === "/word") return "LOAD"; 140 | if (urlObj.path === "/update") return "STORE"; 141 | if (urlObj.path === "/tags") return "TAG"; 142 | if (urlObj.path === "/echo") return "ECHO"; 143 | 144 | return "OTHER"; 145 | } 146 | } 147 | 148 | 149 | -------------------------------------------------------------------------------- /src/component/WordMore.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 43 | 44 | -------------------------------------------------------------------------------- /src/constant.ts: -------------------------------------------------------------------------------- 1 | import { t } from "./lang/helper"; 2 | 3 | const dict = { 4 | NAME: "Language Learner" 5 | }; 6 | 7 | type Position = { 8 | x: number; 9 | y: number; 10 | }; 11 | 12 | //继承 GlobalEventHandlersEventMap:EventMap 继承了 GlobalEventHandlersEventMap, 13 | //这意味着 EventMap 将包含 GlobalEventHandlersEventMap 中的所有事件,同时添加自定义事件。 14 | interface EventMap extends GlobalEventHandlersEventMap { 15 | "obsidian-langr-search": CustomEvent<{ 16 | selection: string, 17 | target?: HTMLElement, 18 | evtPosition?: Position, 19 | }>; 20 | "obsidian-langr-refresh": CustomEvent<{ 21 | expression: string, 22 | type: string, 23 | status: number, 24 | meaning: string, 25 | aliases: string[], 26 | }>; 27 | "obsidian-langr-refresh-stat": CustomEvent<{}>; 28 | } 29 | 30 | 31 | 32 | export { dict }; 33 | export type { EventMap, Position } 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/db/base.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArticleWords, Word, Phrase, WordsPhrase, Sentence, 3 | ExpressionInfo, ExpressionInfoSimple, CountInfo, WordCount, Span 4 | } from "./interface"; 5 | 6 | 7 | abstract class DbProvider { 8 | abstract open(): Promise; 9 | abstract close(): void; 10 | // 在文章中寻找之前记录过的单词和词组 11 | abstract getStoredWords(payload: ArticleWords): Promise; 12 | // 查询单个单词/词组的全部信息 13 | abstract getExpression(expression: string): Promise; 14 | //获取一批单词的简略信息 15 | abstract getExpressionsSimple(expressions: string[]): Promise; 16 | //获取一批单词的全部信息,包括句子 17 | abstract getExprall(expressions: string[]): Promise; 18 | // 某一时间之后添加的全部单词 19 | abstract getExpressionAfter(time: string): Promise; 20 | // 获取全部单词的简略信息 21 | abstract getAllExpressionSimple(ignores?: boolean): Promise; 22 | // 发送单词信息到数据库保存 23 | abstract postExpression(payload: ExpressionInfo): Promise; 24 | // 获取所有tag 25 | abstract getTags(): Promise; 26 | // 批量发送单词,全部标记为ignore 27 | abstract postIgnoreWords(payload: string[]): Promise; 28 | // 查询一个例句是否已经记录过 29 | abstract tryGetSen(text: string): Promise; 30 | // 获取各类单词的个数 31 | abstract getCount(): Promise; 32 | // 获取7天内的统计信息 33 | abstract countSeven(): Promise; 34 | // 销毁数据库 35 | abstract destroyAll(): Promise; 36 | // 导入数据库 37 | abstract importDB(data: any): Promise; 38 | // 导出数据库 39 | abstract exportDB(): Promise; 40 | } 41 | 42 | 43 | export default DbProvider; -------------------------------------------------------------------------------- /src/db/idb.ts: -------------------------------------------------------------------------------- 1 | import Dexie from "dexie"; 2 | import Plugin from "@/plugin"; 3 | 4 | export class WordDB extends Dexie { 5 | expressions: Dexie.Table; 6 | sentences: Dexie.Table; 7 | plugin: Plugin; 8 | dbName: string; 9 | constructor(plugin: Plugin) { 10 | super(plugin.settings.db_name); 11 | this.plugin = plugin; 12 | this.dbName = plugin.settings.db_name; 13 | this.version(1).stores({ 14 | expressions: "++id, &expression, status, t, date, *tags", 15 | sentences: "++id, &text" 16 | }); 17 | } 18 | } 19 | 20 | export interface Expression { 21 | id?: number, 22 | expression: string, 23 | meaning: string, 24 | status: number, 25 | t: string, 26 | date: number, 27 | notes: string[], 28 | tags: Set, 29 | sentences: Set, 30 | aliases: string[], 31 | } 32 | interface Sentence { 33 | id?: number; 34 | text: string, 35 | trans: string, 36 | origin: string, 37 | } 38 | -------------------------------------------------------------------------------- /src/db/interface.ts: -------------------------------------------------------------------------------- 1 | interface ArticleWords { 2 | article: string; 3 | words: string[]; 4 | } 5 | 6 | interface Word { 7 | text: string; 8 | status: number; 9 | } 10 | 11 | interface Phrase { 12 | text: string; 13 | status: number; 14 | offset: number; 15 | } 16 | 17 | interface WordsPhrase { 18 | words: Word[]; 19 | phrases: Phrase[]; 20 | } 21 | 22 | interface Sentence { 23 | text: string; 24 | trans: string; 25 | origin: string; 26 | } 27 | 28 | interface ExpressionInfo { 29 | expression: string; 30 | meaning: string; 31 | status: number; 32 | t: string; 33 | tags: string[]; 34 | notes: string[]; 35 | sentences: Sentence[]; 36 | aliases:string[]; 37 | date: number; 38 | } 39 | 40 | interface ExpressionInfoSimple { 41 | expression: string; 42 | meaning: string; 43 | status: number; 44 | t: string; 45 | tags: string[]; 46 | note_num: number; 47 | sen_num: number; 48 | date: number; 49 | aliases:string[]; 50 | } 51 | 52 | interface CountInfo { 53 | word_count: number[]; 54 | phrase_count: number[]; 55 | } 56 | 57 | 58 | interface Span { 59 | from: number; 60 | to: number; 61 | } 62 | 63 | interface WordCount { 64 | today: number[]; 65 | accumulated: number[]; 66 | } 67 | 68 | interface link { 69 | link: {[key:string]: string[]}; 70 | } 71 | 72 | 73 | export type { 74 | ArticleWords, Word, Phrase, WordsPhrase, Sentence, 75 | ExpressionInfo, ExpressionInfoSimple, CountInfo, WordCount, Span 76 | }; -------------------------------------------------------------------------------- /src/db/web_db.ts: -------------------------------------------------------------------------------- 1 | import { requestUrl, RequestUrlParam, moment } from "obsidian"; 2 | import { 3 | ArticleWords, Word, Phrase, WordsPhrase, Sentence, 4 | ExpressionInfo, ExpressionInfoSimple, CountInfo, WordCount, Span 5 | } from "./interface"; 6 | 7 | import DbProvider from "./base"; 8 | 9 | 10 | export class WebDb extends DbProvider { 11 | port: number; 12 | constructor(port: number) { 13 | super(); 14 | this.port = port; 15 | } 16 | async open() { } 17 | 18 | close() { } 19 | 20 | // 寻找页面中已经记录过的单词和词组 21 | async getStoredWords( 22 | payload: ArticleWords 23 | ): Promise { 24 | let request: RequestUrlParam = { 25 | url: `http://localhost:${this.port}/word_phrase`, 26 | method: "POST", 27 | body: JSON.stringify(payload), 28 | contentType: "application/json", 29 | }; 30 | 31 | try { 32 | let response = await requestUrl(request); 33 | let data: WordsPhrase = response.json; 34 | return data; 35 | } catch (e) { 36 | console.warn("Error when getting parse info from server:" + e); 37 | } 38 | } 39 | 40 | 41 | 42 | // 获取单词/词组的详细信息 43 | async getExpression( 44 | expression: string 45 | ): Promise { 46 | let request: RequestUrlParam = { 47 | url: `http://localhost:${this.port}/word`, 48 | method: "POST", 49 | body: JSON.stringify(expression.toLowerCase()), 50 | contentType: "application/json", 51 | }; 52 | 53 | try { 54 | let response = await requestUrl(request); 55 | return response.json; 56 | } catch (e) { 57 | console.warn("Error while getting data from server." + e); 58 | } 59 | } 60 | 61 | async getExpressionsSimple(expressions: string[]): Promise { 62 | expressions = expressions.map(v => v.toLowerCase()); 63 | let request: RequestUrlParam = { 64 | url: `http://localhost:${this.port}/words_simple`, 65 | method: "POST", 66 | body: JSON.stringify(expressions), 67 | contentType: "application/json", 68 | }; 69 | 70 | try { 71 | let response = await requestUrl(request); 72 | return response.json; 73 | } catch (e) { 74 | console.error("Error getting simple data from server: " + e); 75 | } 76 | } 77 | 78 | 79 | // 获取某一时间之后的所有单词的详细信息 80 | async getExpressionAfter(time: string): Promise { 81 | let unixStamp = moment.utc(time).unix(); 82 | let request: RequestUrlParam = { 83 | url: `http://localhost:${this.port}/words/after`, 84 | method: "POST", 85 | body: JSON.stringify(unixStamp), 86 | contentType: "application/json", 87 | }; 88 | try { 89 | let response = await requestUrl(request); 90 | return response.json; 91 | } catch (e) { 92 | console.warn("Error getting exprs after time from server" + e); 93 | } 94 | } 95 | 96 | 97 | // 通过status查询单词/词组,获取简略信息 98 | async getAllExpressionSimple( 99 | ignores?: boolean 100 | ): Promise { 101 | let mode = ignores ? "all" : "no_ignore"; 102 | 103 | let request: RequestUrlParam = { 104 | url: `http://localhost:${this.port}/words_simple/${mode}`, 105 | method: "GET", 106 | }; 107 | 108 | try { 109 | let response = await requestUrl(request); 110 | 111 | return response.json; 112 | } catch (e) { 113 | console.warn("Error while getting all simple data from server." + e); 114 | } 115 | } 116 | 117 | // 添加或更新单词/词组的信息 118 | async postExpression(payload: ExpressionInfo): Promise { 119 | let request: RequestUrlParam = { 120 | url: `http://localhost:${this.port}/update`, 121 | method: "POST", 122 | body: JSON.stringify(payload), 123 | contentType: "application/json", 124 | }; 125 | try { 126 | let response = await requestUrl(request); 127 | return response.status; 128 | } catch (e) { 129 | console.warn("Error while saving data to server." + e); 130 | } 131 | } 132 | 133 | // 获取所有的tag 134 | async getTags(): Promise { 135 | let request: RequestUrlParam = { 136 | url: `http://localhost:${this.port}/tags`, 137 | method: "GET", 138 | }; 139 | 140 | try { 141 | let response = await requestUrl(request); 142 | return response.json; 143 | } catch (e) { 144 | console.warn("Error getting tags from server." + e); 145 | } 146 | } 147 | 148 | // 发送所有忽略的新词 149 | async postIgnoreWords(payload: string[]) { 150 | let request: RequestUrlParam = { 151 | url: `http://localhost:${this.port}/ignores`, 152 | method: "POST", 153 | body: JSON.stringify(payload), 154 | contentType: "application/json", 155 | }; 156 | 157 | try { 158 | await requestUrl(request); 159 | } catch (e) { 160 | console.warn("Error sending ignore words" + e); 161 | } 162 | } 163 | 164 | // 尝试查询已存在的例句 165 | async tryGetSen(text: string): Promise { 166 | let request: RequestUrlParam = { 167 | url: `http://localhost:${this.port}/sentence`, 168 | method: "POST", 169 | body: JSON.stringify(text), 170 | contentType: "application/json", 171 | }; 172 | 173 | try { 174 | let res = await requestUrl(request); 175 | return res.json; 176 | } catch (e) { 177 | console.warn("Error trying to get sentence" + e); 178 | } 179 | } 180 | 181 | // 统计部分 182 | // 获取各种类型的单词/词组类型 183 | async getCount(): Promise { 184 | let request: RequestUrlParam = { 185 | url: `http://localhost:${this.port}/count_all`, 186 | method: "GET", 187 | }; 188 | try { 189 | let response = await requestUrl(request); 190 | let wordsCount: CountInfo = response.json; 191 | return wordsCount; 192 | } catch (e) { 193 | console.warn("Error getting words count" + e); 194 | } 195 | } 196 | 197 | //未编写 198 | async getExprall(expressions: string[]): Promise { 199 | expressions = expressions.map(expression => expression.toLowerCase()); 200 | // 组装结果 201 | let expressionInfos: ExpressionInfo[] = []; 202 | return expressionInfos; 203 | } 204 | 205 | // 获取包括今天在内的7天内每一天的新单词量和累计单词量 206 | async countSeven(): Promise { 207 | let spans: Span[] = []; 208 | 209 | spans = [0, 1, 2, 3, 4, 5, 6].map((i) => { 210 | let start = moment().subtract(6, "days").startOf("day"); 211 | let from = start.add(i, "days"); 212 | return { 213 | from: from.unix(), 214 | to: from.endOf("day").unix(), 215 | }; 216 | }); 217 | 218 | let request: RequestUrlParam = { 219 | url: `http://localhost:${this.port}/count_time`, 220 | method: "POST", 221 | body: JSON.stringify(spans), 222 | contentType: "application/json", 223 | }; 224 | 225 | try { 226 | let res = await requestUrl(request); 227 | return res.json; 228 | } catch (e) { } 229 | } 230 | 231 | async importDB() { } 232 | 233 | async exportDB() { } 234 | 235 | async destroyAll() { 236 | // 什么也没有发生 237 | } 238 | 239 | } -------------------------------------------------------------------------------- /src/dictionary/cambridge/View.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 54 | 55 | -------------------------------------------------------------------------------- /src/dictionary/cambridge/engine.ts: -------------------------------------------------------------------------------- 1 | import { fetchDirtyDOM } from '../helpers'; 2 | // import { getStaticSpeaker } from '@/components/Speaker' 3 | import { 4 | HTMLString, 5 | getInnerHTML, 6 | handleNoResult, 7 | getText, 8 | removeChild, 9 | handleNetWorkError, 10 | SearchFunction, 11 | GetSrcPageFunction, 12 | DictSearchResult, 13 | getFullLink, 14 | getStaticSpeaker, 15 | externalLink, 16 | // getChsToChz 17 | } from '../helpers'; 18 | 19 | export const getSrcPage: GetSrcPageFunction = (text) => { 20 | // let { lang } = profile.dicts.all.cambridge.options 21 | const langDict: { [K in string]: string } = { 22 | "en": "en", 23 | "zh": "en-chs", 24 | "zh-TW": "en-chz", 25 | }; 26 | let language = window.localStorage.getItem("language"); 27 | let lang = langDict[language] || "en"; 28 | // if (lang === 'default') { 29 | // switch (config.langCode) { 30 | // case 'zh-CN': 31 | // lang = 'en-chs' 32 | // break 33 | // case 'zh-TW': 34 | // lang = 'en-chz' 35 | // break 36 | // default: 37 | // lang = 'en' 38 | // break 39 | // } 40 | // } 41 | 42 | switch (lang) { 43 | case 'en': 44 | return ( 45 | 'https://dictionary.cambridge.org/search/direct/?datasetsearch=english&q=' + 46 | encodeURIComponent( 47 | text 48 | .trim() 49 | .split(/\s+/) 50 | .join('-') 51 | ) 52 | ); 53 | case 'en-chs': 54 | return ( 55 | 'https://dictionary.cambridge.org/zhs/%E6%90%9C%E7%B4%A2/direct/?datasetsearch=english-chinese-simplified&q=' + 56 | encodeURIComponent(text) 57 | ); 58 | case 'en-chz': { 59 | // const chsToChz = await getChsToChz() 60 | return ( 61 | 'https://dictionary.cambridge.org/zht/%E6%90%9C%E7%B4%A2/direct/?datasetsearch=english-chinese-traditional&q=' + 62 | encodeURIComponent(text) 63 | // encodeURIComponent(chsToChz(text)) 64 | ); 65 | } 66 | } 67 | }; 68 | 69 | const HOST = 'https://dictionary.cambridge.org'; 70 | 71 | type CambridgeResultItem = { 72 | id: string; 73 | html: HTMLString; 74 | }; 75 | 76 | export type CambridgeResult = CambridgeResultItem[]; 77 | 78 | type CambridgeSearchResult = DictSearchResult; 79 | 80 | export const search: SearchFunction = async ( 81 | text, 82 | ) => { 83 | return fetchDirtyDOM(await getSrcPage(text)) 84 | .catch(handleNetWorkError) 85 | .then(doc => handleDOM(doc)) 86 | .catch(handleNoResult); 87 | }; 88 | 89 | function prune(doc: DocumentFragment): void { 90 | doc.querySelectorAll(".smartt, .grammar, .bb, .dimg, .xref").forEach(el => el.remove()); 91 | // doc.querySelectorAll(".grammar").forEach(el => el.remove()) 92 | // doc.querySelectorAll(".bb").forEach(el => el.remove) 93 | } 94 | 95 | function handleDOM( 96 | doc: DocumentFragment, 97 | // options: DictConfigs['cambridge']['options'] 98 | ): CambridgeSearchResult | Promise { 99 | const result: CambridgeResult = []; 100 | const catalog: NonNullable = []; 101 | const audio: { us?: string; uk?: string; } = {}; 102 | 103 | prune(doc); 104 | 105 | doc.querySelectorAll('.entry-body__el').forEach(($entry, i) => { 106 | if (!getText($entry, '.headword')) { 107 | return; 108 | } 109 | 110 | const $posHeader = $entry.querySelector('.pos-header'); 111 | if ($posHeader) { 112 | $posHeader.querySelectorAll('.dpron-i').forEach($pron => { 113 | const $daud = $pron.querySelector('.daud'); 114 | if (!$daud) return; 115 | const $source = $daud.querySelector( 116 | 'source[type="audio/mpeg"]' 117 | ); 118 | if (!$source) return; 119 | 120 | const src = getFullLink(HOST, $source, 'src'); 121 | 122 | if (src) { 123 | $daud.replaceWith(getStaticSpeaker(src)); 124 | 125 | if (!audio.uk && $pron.classList.contains('uk')) { 126 | audio.uk = src; 127 | } 128 | 129 | if (!audio.us && $pron.classList.contains('us')) { 130 | audio.us = src; 131 | } 132 | } 133 | }); 134 | removeChild($posHeader, '.share'); 135 | } 136 | 137 | sanitizeEntry($entry); 138 | 139 | const entryId = `d-cambridge-entry${i}`; 140 | 141 | result.push({ 142 | id: entryId, 143 | html: getInnerHTML(HOST, $entry) 144 | }); 145 | 146 | catalog.push({ 147 | key: `#${i}`, 148 | value: entryId, 149 | label: 150 | '#' + getText($entry, '.di-title') + ' ' + getText($entry, '.posgram') 151 | }); 152 | }); 153 | 154 | if (result.length <= 0) { 155 | // check idiom 156 | const $idiom = doc.querySelector('.idiom-block'); 157 | if ($idiom) { 158 | removeChild($idiom, '.bb.hax'); 159 | 160 | sanitizeEntry($idiom); 161 | 162 | result.push({ 163 | id: 'd-cambridge-entry-idiom', 164 | html: getInnerHTML(HOST, $idiom) 165 | }); 166 | } 167 | } 168 | 169 | // if (result.length <= 0 && options.related) { 170 | if (result.length <= 0 && true) { 171 | const $link = doc.querySelector('link[rel=canonical]'); 172 | if ( 173 | $link && 174 | /dictionary\.cambridge\.org\/([^/]+\/)?spellcheck\//.test( 175 | $link.getAttribute('href') || '' 176 | ) 177 | ) { 178 | const $related = doc.querySelector('.hfl-s.lt2b.lmt-10.lmb-25.lp-s_r-20'); 179 | if ($related) { 180 | result.push({ 181 | id: 'd-cambridge-entry-related', 182 | html: getInnerHTML(HOST, $related) 183 | }); 184 | } 185 | } 186 | } 187 | 188 | if (result.length > 0) { 189 | return { result, audio, catalog }; 190 | } 191 | 192 | return handleNoResult(); 193 | } 194 | 195 | function sanitizeEntry($entry: E): E { 196 | // expand button 197 | $entry.querySelectorAll('.daccord_h').forEach($btn => { 198 | $btn.parentElement!.classList.add('amp-accordion'); 199 | }); 200 | 201 | // replace amp-img 202 | $entry.querySelectorAll('amp-img').forEach($ampImg => { 203 | const $img = document.createElement('img'); 204 | 205 | $img.setAttribute('src', getFullLink(HOST, $ampImg, 'src')); 206 | 207 | const attrs = ['width', 'height', 'title']; 208 | for (const attr of attrs) { 209 | const val = $ampImg.getAttribute(attr); 210 | if (val) { 211 | $img.setAttribute(attr, val); 212 | } 213 | } 214 | 215 | $ampImg.replaceWith($img); 216 | }); 217 | 218 | // replace amp-audio 219 | $entry.querySelectorAll('amp-audio').forEach($ampAudio => { 220 | const $source = $ampAudio.querySelector('source'); 221 | if ($source) { 222 | const src = getFullLink(HOST, $source, 'src'); 223 | if (src) { 224 | $ampAudio.replaceWith(getStaticSpeaker(src)); 225 | return; 226 | } 227 | } 228 | $ampAudio.remove(); 229 | }); 230 | 231 | // See more results 232 | $entry.querySelectorAll('a.had').forEach(externalLink); 233 | 234 | return $entry; 235 | } 236 | -------------------------------------------------------------------------------- /src/dictionary/deepl/View.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 42 | 43 | -------------------------------------------------------------------------------- /src/dictionary/deepl/engine.ts: -------------------------------------------------------------------------------- 1 | import { requestUrl, RequestUrlParam } from "obsidian" 2 | 3 | const langMap: Record = { 4 | zh: "ZH", 5 | en: "EN", 6 | jp: "JA", 7 | fr: "FR", 8 | de: "DE", 9 | es: "ES", 10 | }; 11 | 12 | export async function search(text: string, lang: string = ""): Promise { 13 | let target = (/[\u4e00-\u9fa5]/.test(text) && !/[\u0800-\u4e00]/.test(text)) // chinese 14 | ? langMap[lang] || "ZH" 15 | : "ZH"; 16 | const payload = { 17 | text, 18 | source_lang: "auto", 19 | target_lang: target, 20 | }; 21 | 22 | const data: RequestUrlParam = { 23 | url: "https://deeplx.vercel.app/translate", 24 | method: "POST", 25 | body: JSON.stringify(payload), 26 | contentType: "application/json" 27 | }; 28 | 29 | try { 30 | let res = (await requestUrl(data)).json; 31 | if (res.code !== 200) throw new Error("Deeplx api source error."); 32 | 33 | return res.data; 34 | } catch (err) { 35 | console.error(err.message) 36 | } 37 | } -------------------------------------------------------------------------------- /src/dictionary/helpers.ts: -------------------------------------------------------------------------------- 1 | // from ext-saladict: https://github.com/crimx/ext-saladict 2 | import { 3 | request, 4 | RequestUrlParam, 5 | sanitizeHTMLToDom, 6 | } from "obsidian"; 7 | 8 | 9 | export interface GetSrcPageFunction { 10 | (text: string): string; 11 | } 12 | 13 | export interface SearchFunction { 14 | (text: string, config?: any): Promise>; 15 | } 16 | 17 | export async function fetchDirtyDOM(url: string, config?: any): Promise { 18 | const param: RequestUrlParam = { 19 | url, 20 | method: "GET", 21 | }; 22 | if (config) { 23 | let cookie = Object.keys(config.cookies) 24 | .map(name => `${name}=${config.cookies[name]}`) 25 | .join("; "); 26 | let headers = { 27 | "cookie": cookie, 28 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.0.3 Chrome/100.0.4896.160 Electron/18.3.5 Safari/537.36" 29 | }; 30 | param.headers = headers; 31 | } 32 | let response = await request(param); 33 | response = response.replace(//g, ""); 34 | return sanitizeHTMLToDom(response); 35 | } 36 | 37 | 38 | export function isTagName(node: Node, tagName: string): boolean { 39 | return ( 40 | ((node as HTMLElement).tagName || '').toLowerCase() === 41 | tagName.toLowerCase() 42 | ); 43 | } 44 | 45 | // 不知道是干啥的,瞎写的 46 | const isInternalPage = () => false; 47 | 48 | export interface DictSearchResult { 49 | result: Result; 50 | audio?: { 51 | uk?: string; 52 | us?: string; 53 | py?: string; 54 | }; 55 | /** generate menus on dict titlebars */ 56 | catalog?: Array< 57 | | { 58 | //