├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── README_zh_CN.md ├── assets ├── bibitem.png ├── commandpanel.png ├── database.png ├── preview.jpg ├── protyleslash.png ├── searchpanel.png ├── setcommand.png ├── sponsor_alipay.jpg ├── sponsor_weixin.jpg ├── titleIconMenu.jpg └── zoteroIntegration.png ├── citeIcon.ico ├── icon.png ├── icon_edge.svg ├── icon_reverse.png ├── icon_reverse.svg ├── package.json ├── plugin.json ├── pnpm-lock.yaml ├── preview.png ├── sample-data ├── sample.bib └── sample.json ├── scripts └── citation.lua ├── src ├── api │ ├── base-api.ts │ ├── kernel-api.ts │ └── networkManagers.ts ├── database │ ├── database.ts │ ├── filesLibrary.ts │ ├── modal.ts │ └── zoteroLibrary.ts ├── events │ ├── customEventBus.ts │ └── eventTrigger.ts ├── export │ └── exportManager.ts ├── frontEnd │ ├── interaction.ts │ ├── misc │ │ ├── BlockIcon.svelte │ │ ├── Svg.svelte │ │ ├── SvgArrow.svelte │ │ ├── index.ts │ │ └── tooltips.ts │ ├── searchDialog │ │ ├── dialogComponent.svelte │ │ └── searchDialog.ts │ └── settingTab │ │ ├── Example.svelte │ │ ├── event.ts │ │ ├── item │ │ ├── Card.svelte │ │ ├── Group.svelte │ │ ├── Input.svelte │ │ ├── Item.svelte │ │ ├── MiniItem.svelte │ │ └── item.ts │ │ ├── panel │ │ ├── Panel.svelte │ │ └── Panels.svelte │ │ ├── settingTab.ts │ │ ├── settingTabComponent.svelte │ │ ├── specified │ │ └── Shortcut.svelte │ │ ├── tab.ts │ │ └── tab │ │ ├── Tab.svelte │ │ └── Tabs.svelte ├── i18n │ ├── en_US.json │ └── zh_CN.json ├── index.scss ├── index.ts ├── references │ ├── cite.ts │ ├── literatureNote.ts │ ├── noteProcessor.ts │ ├── pool.ts │ └── reference.ts └── utils │ ├── constants.ts │ ├── notes.ts │ ├── noticer.ts │ ├── shortcut │ ├── index.ts │ └── match.ts │ ├── simple-logger.ts │ ├── templates.ts │ ├── types.ts │ ├── updates.ts │ └── util.ts ├── tsconfig.json ├── webpack.config.js └── zoteroJS ├── addTagsToItem.ts ├── checkItemKeyExist.ts ├── checkRunning.ts ├── citeWithZoteroDialog.ts ├── getAllItems.ts ├── getAttachmentByItemKey.ts ├── getItemByItemKey.ts ├── getItemKeyByCiteKey.ts ├── getMarkdownNotes.ts ├── getNotesByItemKey.ts ├── getSelectedItems.ts └── updateURLToItem.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | index.js 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: {node: true, browser: true, es6: true}, 4 | parser: "@typescript-eslint/parser", 5 | plugins: [ 6 | "@typescript-eslint", 7 | ], 8 | extends: [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | ], 12 | rules: { 13 | semi: [2, "always"], 14 | quotes: [2, "double", {"avoidEscape": true}], 15 | "no-async-promise-executor": "off", 16 | "no-prototype-builtins": "off", 17 | "no-useless-escape": "off", 18 | "no-irregular-whitespace": "off", 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "@typescript-eslint/no-var-requires": "off", 21 | "@typescript-eslint/explicit-function-return-type": "off", 22 | "@typescript-eslint/explicit-module-boundary-types": "off", 23 | "@typescript-eslint/no-explicit-any": "off", 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | node_modules 4 | .DS_Store 5 | .eslintcache 6 | dist 7 | pnpm-lock.yaml 8 | package.zip 9 | index.css 10 | index.js 11 | i18n -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 SiYuan 思源笔记 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 | -------------------------------------------------------------------------------- /assets/bibitem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WingDr/siyuan-plugin-citation/87d66b2c31340a591506d4ac5e5296be775473d5/assets/bibitem.png -------------------------------------------------------------------------------- /assets/commandpanel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WingDr/siyuan-plugin-citation/87d66b2c31340a591506d4ac5e5296be775473d5/assets/commandpanel.png -------------------------------------------------------------------------------- /assets/database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WingDr/siyuan-plugin-citation/87d66b2c31340a591506d4ac5e5296be775473d5/assets/database.png -------------------------------------------------------------------------------- /assets/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WingDr/siyuan-plugin-citation/87d66b2c31340a591506d4ac5e5296be775473d5/assets/preview.jpg -------------------------------------------------------------------------------- /assets/protyleslash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WingDr/siyuan-plugin-citation/87d66b2c31340a591506d4ac5e5296be775473d5/assets/protyleslash.png -------------------------------------------------------------------------------- /assets/searchpanel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WingDr/siyuan-plugin-citation/87d66b2c31340a591506d4ac5e5296be775473d5/assets/searchpanel.png -------------------------------------------------------------------------------- /assets/setcommand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WingDr/siyuan-plugin-citation/87d66b2c31340a591506d4ac5e5296be775473d5/assets/setcommand.png -------------------------------------------------------------------------------- /assets/sponsor_alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WingDr/siyuan-plugin-citation/87d66b2c31340a591506d4ac5e5296be775473d5/assets/sponsor_alipay.jpg -------------------------------------------------------------------------------- /assets/sponsor_weixin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WingDr/siyuan-plugin-citation/87d66b2c31340a591506d4ac5e5296be775473d5/assets/sponsor_weixin.jpg -------------------------------------------------------------------------------- /assets/titleIconMenu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WingDr/siyuan-plugin-citation/87d66b2c31340a591506d4ac5e5296be775473d5/assets/titleIconMenu.jpg -------------------------------------------------------------------------------- /assets/zoteroIntegration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WingDr/siyuan-plugin-citation/87d66b2c31340a591506d4ac5e5296be775473d5/assets/zoteroIntegration.png -------------------------------------------------------------------------------- /citeIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WingDr/siyuan-plugin-citation/87d66b2c31340a591506d4ac5e5296be775473d5/citeIcon.ico -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WingDr/siyuan-plugin-citation/87d66b2c31340a591506d4ac5e5296be775473d5/icon.png -------------------------------------------------------------------------------- /icon_edge.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.10, written by Peter Selinger 2001-2011 9 | 10 | 12 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /icon_reverse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WingDr/siyuan-plugin-citation/87d66b2c31340a591506d4ac5e5296be775473d5/icon_reverse.png -------------------------------------------------------------------------------- /icon_reverse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "siyuan-plugin-citation", 3 | "version": "0.0.3", 4 | "description": "This is a citation plugin (https://github.com/WingDr/siyuan-plugin-citation) for Siyuan (https://b3log.org/siyuan)", 5 | "main": ".src/index.js", 6 | "scripts": { 7 | "lint": "eslint . --fix --cache", 8 | "dev": "webpack --mode development", 9 | "build": "webpack --mode production" 10 | }, 11 | "keywords": [], 12 | "author": "WingDr", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@tsconfig/svelte": "5.0.4", 16 | "@types/node": "^20.17.9", 17 | "@typescript-eslint/eslint-plugin": "8.13.0", 18 | "@typescript-eslint/parser": "8.13.0", 19 | "copy-webpack-plugin": "^11.0.0", 20 | "css-loader": "^6.11.0", 21 | "esbuild-loader": "^3.2.0", 22 | "eslint": "^9.16.0", 23 | "mini-css-extract-plugin": "2.3.0", 24 | "sass": "^1.82.0", 25 | "sass-loader": "^12.6.0", 26 | "siyuan": "^1.0.6", 27 | "svelte": "^5.6.2", 28 | "svelte-loader": "3.2.4", 29 | "svelte-preprocess": "6.0.3", 30 | "ts-node": "^10.9.2", 31 | "tslib": "2.4.0", 32 | "typescript": "^5.7.2", 33 | "webpack": "^5.97.0", 34 | "webpack-cli": "^5.1.4", 35 | "zip-webpack-plugin": "^4.0.2" 36 | }, 37 | "dependencies": { 38 | "@retorquere/bibtex-parser": "^9.0.17", 39 | "axios": "^1.7.9", 40 | "fuse.js": "^7.0.0", 41 | "moment": "^2.30.1", 42 | "template_js": "^3.1.4", 43 | "zotero-types": "^3.1.0", 44 | "form-data": "4.0.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "siyuan-plugin-citation", 3 | "author": "WingDr", 4 | "url": "https://github.com/WingDr/siyuan-plugin-citation", 5 | "version": "0.4.1", 6 | "minAppVersion": "3.0.4", 7 | "backends": ["windows", "linux", "darwin", "docker"], 8 | "frontends": ["all"], 9 | "i18n": ["en_US", "zh_CN"], 10 | "displayName": { 11 | "default": "Citation for SiYuan", 12 | "zh_CN": "文献引用" 13 | }, 14 | "description": { 15 | "default": "Insert literature citations to your notes", 16 | "zh_CN": "在笔记中插入文献引用" 17 | }, 18 | "readme": { 19 | "default": "README.md", 20 | "zh_CN": "README_zh_CN.md" 21 | }, 22 | "funding": { 23 | "openCollective": "", 24 | "patreon": "", 25 | "github": "", 26 | "custom": [] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WingDr/siyuan-plugin-citation/87d66b2c31340a591506d4ac5e5296be775473d5/preview.png -------------------------------------------------------------------------------- /sample-data/sample.bib: -------------------------------------------------------------------------------- 1 | @article{zuoNewClassFinitetime2014, 2 | title = {A New Class of Finite-Time Nonlinear Consensus Protocols for Multi-Agent Systems}, 3 | author = {Zuo, Zongyu and Tie, Lin}, 4 | date = {2014-02}, 5 | journaltitle = {International Journal of Control}, 6 | shortjournal = {International Journal of Control}, 7 | volume = {87}, 8 | number = {2}, 9 | pages = {363--370}, 10 | issn = {0020-7179, 1366-5820}, 11 | doi = {10.1080/00207179.2013.834484}, 12 | url = {http://www.tandfonline.com/doi/abs/10.1080/00207179.2013.834484}, 13 | urldate = {2022-03-02}, 14 | abstract = {This paper is devoted to investigating the finite-time consensus problem for a multi-agent system in networks with undirected topology. A new class of global continuous time-invariant consensus protocols is constructed for each single-integrator agent dynamics with the aid of Lyapunov functions. In particular, it is shown that the settling time of the proposed new class of finite-time consensus protocols is upper bounded for arbitrary initial conditions. This makes it possible for network consensus problems that the convergence time is designed and estimated offline for a given undirected information flow and a group volume of agents. Finally, a numerical simulation example is presented as a proof of concept.}, 15 | langid = {english} 16 | } -------------------------------------------------------------------------------- /sample-data/sample.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"id":"zuoRobustFixedTimeStabilization2022","abstract":"This article addresses the robust fixed-time stabilization control problem for generic linear systems with both matched and mismatched disturbances. A new observer-based fixed-time control technique is proposed to solve this robust stabilization problem, provided that the system matrix pair (A, B) is controllable. The ultimate boundedness of the closed-loop system in the presence of mismatched disturbances is proven. An upper bound of the convergence time is provided, which is irrelevant to initial conditions. Finally, a simulation example is presented to show the efficiency of the proposed control design method.","accessed":{"date-parts":[[2022,11,9]]},"author":[{"family":"Zuo","given":"Zongyu"},{"family":"Song","given":"Jiawei"},{"family":"Tian","given":"Bailing"},{"family":"Basin","given":"Michael"}],"citation-key":"zuoRobustFixedTimeStabilization2022","container-title":"IEEE Transactions on Systems, Man, and Cybernetics: Systems","container-title-short":"IEEE Trans. Syst. Man Cybern, Syst.","DOI":"10.1109/TSMC.2020.3010221","ISSN":"2168-2216, 2168-2232","issue":"2","issued":{"date-parts":[[2022,2]]},"language":"en","page":"759-768","source":"DOI.org (Crossref)","title":"Robust Fixed-Time Stabilization Control of Generic Linear Systems With Mismatched Disturbances","type":"article-journal","URL":"https://ieeexplore.ieee.org/document/9159695/","volume":"52"} 3 | ] 4 | -------------------------------------------------------------------------------- /scripts/citation.lua: -------------------------------------------------------------------------------- 1 | function Citation(el) 2 | local cite_pattern = "@siyuan_cite{([^}]+)}" 3 | local name_pattern = "@siyuan_name{([^}]+)}" 4 | if el.text:match(cite_pattern) then 5 | if el.text:match(name_pattern) then 6 | print(el.text) 7 | local cite_match = el.text:match(cite_pattern) 8 | -- 用逗号分割id,添加上前后缀,然后再用逗号拼接 9 | local ids = {} 10 | for id in string.gmatch(cite_match, "[^,]+") do 11 | table.insert(ids, '{"id":' .. id .. '}') 12 | end 13 | local cite_result = table.concat(ids, ",") 14 | print(cite_result) 15 | local name_match = el.text:match(name_pattern) 16 | return pandoc.RawInline("openxml", 'ADDIN ZOTERO_ITEM CSL_CITATION {"citationItems":['.. cite_result .. ']}' .. name_match .. '') 17 | end 18 | end 19 | end 20 | 21 | return { 22 | { Str = Citation } 23 | } -------------------------------------------------------------------------------- /src/api/base-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Terwer . All rights reserved. 3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 | * 5 | * This code is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU General Public License version 2 only, as 7 | * published by the Free Software Foundation. Terwer designates this 8 | * particular file as subject to the "Classpath" exception as provided 9 | * by Terwer in the LICENSE file that accompanied this code. 10 | * 11 | * This code is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 | * version 2 for more details (a copy is included in the LICENSE file that 15 | * accompanied this code). 16 | * 17 | * You should have received a copy of the GNU General Public License version 18 | * 2 along with this work; if not, write to the Free Software Foundation, 19 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 | * 21 | * Please contact Terwer, Shenzhen, Guangdong, China, youweics@163.com 22 | * or visit www.terwer.space if you need additional information or have any 23 | * questions. 24 | */ 25 | 26 | import { createLogger } from "../utils/simple-logger"; 27 | import { isDev, showRequest, siyuanApiToken, siyuanApiUrl } from "../utils/constants"; 28 | 29 | /** 30 | * 思源 API 返回类型 31 | */ 32 | export interface SiyuanData { 33 | /** 34 | * 非 0 为异常情况 35 | */ 36 | code: number 37 | 38 | /** 39 | * 正常情况下是空字符串,异常情况下会返回错误文案 40 | */ 41 | msg: string 42 | 43 | /** 44 | * 可能为 \{\}、[] 或者 NULL,根据不同接口而不同 45 | */ 46 | data: any[] | object | null | undefined 47 | } 48 | 49 | export class BaseApi { 50 | private logger; 51 | 52 | constructor() { 53 | this.logger = createLogger("base-api"); 54 | } 55 | 56 | /** 57 | * 向思源请求数据 58 | * 59 | * @param url - url 60 | * @param data - 数据 61 | */ 62 | public async siyuanRequest(url: string, data: object): Promise { 63 | const reqUrl = `${siyuanApiUrl}${url}`; 64 | 65 | const fetchOps = { 66 | body: JSON.stringify(data), 67 | method: "POST", 68 | }; 69 | if (siyuanApiToken !== "") { 70 | Object.assign(fetchOps, { 71 | headers: { 72 | Authorization: `Token ${siyuanApiToken}`, 73 | }, 74 | }); 75 | } 76 | 77 | if (isDev && showRequest) { 78 | this.logger.info("开始向思源请求数据,reqUrl=>", reqUrl); 79 | this.logger.info("开始向思源请求数据,fetchOps=>", fetchOps); 80 | } 81 | 82 | const response = await fetch(reqUrl, fetchOps); 83 | const resJson = (await response.json() as any) as SiyuanData; 84 | if (isDev && showRequest) { 85 | this.logger.info("思源请求数据返回,resJson=>", resJson); 86 | } 87 | 88 | if (resJson.code === -1) { 89 | throw new Error(resJson.msg); 90 | } 91 | return resJson; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/api/kernel-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Terwer . All rights reserved. 3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 | * 5 | * This code is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU General Public License version 2 only, as 7 | * published by the Free Software Foundation. Terwer designates this 8 | * particular file as subject to the "Classpath" exception as provided 9 | * by Terwer in the LICENSE file that accompanied this code. 10 | * 11 | * This code is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 | * version 2 for more details (a copy is included in the LICENSE file that 15 | * accompanied this code). 16 | * 17 | * You should have received a copy of the GNU General Public License version 18 | * 2 along with this work; if not, write to the Free Software Foundation, 19 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 | * 21 | * Please contact Terwer, Shenzhen, Guangdong, China, youweics@163.com 22 | * or visit www.terwer.space if you need additional information or have any 23 | * questions. 24 | */ 25 | 26 | import { BaseApi, type SiyuanData } from "./base-api"; 27 | import { siyuanApiToken, siyuanApiUrl } from "../utils/constants"; 28 | import { fetchPost } from "siyuan"; 29 | /** 30 | * 思源笔记服务端API v2.8.8 31 | * 32 | * @see {@link https://github.com/siyuan-note/siyuan/blob/master/API_zh_CN.md API} 33 | * 34 | * @author terwer 35 | * @version 1.0.0 36 | * @since 1.0.0 37 | */ 38 | class KernelApi extends BaseApi { 39 | /** 40 | * 列出笔记本 41 | */ 42 | public async lsNotebooks(): Promise { 43 | return await this.siyuanRequest("/api/notebook/lsNotebooks", {}); 44 | } 45 | 46 | /** 47 | * 打开笔记本 48 | * 49 | * @param notebookId - 笔记本ID 50 | */ 51 | public async openNotebook(notebookId: string): Promise { 52 | return await this.siyuanRequest("/api/notebook/openNotebook", { 53 | notebook: notebookId, 54 | }); 55 | } 56 | 57 | /** 58 | * 列出文件 59 | * 60 | * @param path - 路径 61 | */ 62 | public async readDir(path: string): Promise { 63 | return await this.siyuanRequest("/api/file/readDir", { 64 | path: path, 65 | }); 66 | } 67 | 68 | /** 69 | * 写入文件 70 | * 71 | * @param path - 文件路径,例如:/data/20210808180117-6v0mkxr/20200923234011-ieuun1p.sy 72 | * @param isDir - 是否是文件夹,如果为true则只创建文件夹,忽略文件 73 | * @param file - 上传的文件 74 | */ 75 | public putFile(path: string, isDir: boolean, file: any): Promise { 76 | const formData = new FormData(); 77 | formData.append("path", path); 78 | formData.append("isDir", String(isDir)); 79 | formData.append("modTime", Math.floor(Date.now() / 1000).toString()); 80 | formData.append("file", file); 81 | 82 | return new Promise((resolve, reject) => { 83 | fetchPost("/api/file/putFile", formData, (data) => { 84 | if (data.code === 0) { 85 | resolve(data); 86 | } else { 87 | reject(data); 88 | } 89 | }); 90 | }); 91 | } 92 | 93 | public async saveTextData(fileName: string, data: any) { 94 | return new Promise((resolve) => { 95 | const pathString = `/temp/convert/pandoc/${fileName}`; 96 | const file = new File([new Blob([data])], pathString.split("/").pop()!); 97 | const formData = new FormData(); 98 | formData.append("path", pathString); 99 | formData.append("file", file); 100 | formData.append("isDir", "false"); 101 | fetchPost("/api/file/putFile", formData, (response) => { 102 | resolve(response); 103 | }); 104 | }); 105 | } 106 | 107 | /** 108 | * 读取文件 109 | * 110 | * @param path - 文件路径,例如:/data/20210808180117-6v0mkxr/20200923234011-ieuun1p.sy 111 | * @param type - 类型 112 | */ 113 | public async getFile(path: string, type: "text" | "json" | "any") { 114 | const response = await fetch(`${siyuanApiUrl}/api/file/getFile`, { 115 | method: "POST", 116 | headers: { 117 | Authorization: `Token ${siyuanApiToken}`, 118 | }, 119 | body: JSON.stringify({ 120 | path: path, 121 | }), 122 | }); 123 | if (response.status === 200) { 124 | if (type === "text") { 125 | return await response.text(); 126 | } else if (type === "json") { 127 | return await response.json(); 128 | } 129 | else if (type === "any") { 130 | return await response; 131 | } 132 | } else if (response.status === 202) { 133 | const data = await response.json() as any; 134 | switch (data.code) { 135 | case -1 : { console.error("参数解析错误", {msg: data.msg}); break; } 136 | case 403 : { console.error("无访问权限 (文件不在工作空间下)", {msg: data.msg}); break; } 137 | case 404 : { console.error("未找到 (文件不存在)", {msg: data.msg}); break; } 138 | case 405 : { console.error("方法不被允许 (这是一个目录)", {msg: data.msg}); break; } 139 | case 500 : { console.error("服务器错误 (文件查询失败 / 文件读取失败)", {msg: data.msg}); break; } 140 | } 141 | }else { 142 | console.error(response); 143 | } 144 | return null; 145 | } 146 | 147 | /** 148 | * 移动工作空间外的文件 149 | * 150 | * @param srcs - 要移动的源文件 151 | * @param destDir - 工作空间中的目标路径 152 | */ 153 | public async globalCopyFiles(srcs: string[], destDir: string): Promise { 154 | const params = { 155 | srcs, 156 | destDir 157 | }; 158 | return await this.siyuanRequest("/api/file/globalCopyFiles", params); 159 | } 160 | 161 | /** 162 | * 移动工作空间外的文件 163 | * 164 | * @param path - 要移动的源文件 165 | * @param newPath - 工作空间中的目标路径 166 | */ 167 | public async renameFile(path: string, newPath: string): Promise { 168 | const params = { 169 | path, 170 | newPath 171 | }; 172 | return await this.siyuanRequest("/api/file/renameFile", params); 173 | } 174 | 175 | /** 176 | * 删除文件 177 | * 178 | * @param path - 路径 179 | */ 180 | public async removeFile(path: string): Promise { 181 | const params = { 182 | path: path, 183 | }; 184 | return await this.siyuanRequest("/api/file/removeFile", params); 185 | } 186 | 187 | /** 188 | * 通过 Markdown 创建文档 189 | * 190 | * @param notebook - 笔记本 191 | * @param path - 路径 192 | * @param md - md 193 | */ 194 | public async createDocWithMd(notebook: string, path: string, md: string): Promise { 195 | const params = { 196 | notebook: notebook, 197 | path: path, 198 | markdown: md, 199 | }; 200 | return await this.siyuanRequest("/api/filetree/createDocWithMd", params); 201 | } 202 | 203 | /** 204 | * 导入 Markdown 文件 205 | * 206 | * @param localPath - 本地 MD 文档绝对路径 207 | * @param notebook - 笔记本 208 | * @param path - 路径 209 | */ 210 | public async importStdMd(localPath: string, notebook: string, path: string): Promise { 211 | const params = { 212 | // Users/terwer/Documents/mydocs/SiYuanWorkspace/public/temp/convert/pandoc/西蒙学习法:如何在短时间内快速学会新知识-友荣方略.md 213 | localPath: localPath, 214 | notebook: notebook, 215 | toPath: path, 216 | }; 217 | return await this.siyuanRequest("/api/import/importStdMd", params); 218 | } 219 | 220 | public async deleteBlock(blockId: string) { 221 | const params = { 222 | "id": blockId 223 | }; 224 | return await this.siyuanRequest("/api/block/deleteBlock", params); 225 | } 226 | 227 | public async searchFileInSpecificPath(notebook: string, hpath: string): Promise { 228 | const params = { 229 | "stmt": `SELECT * FROM blocks WHERE box like '${notebook}' and hpath like '${hpath}' and type like 'd'` 230 | }; 231 | return await this.siyuanRequest("/api/query/sql", params); 232 | } 233 | 234 | public async searchFileWithKey(notebook: string, hpath: string, key: string): Promise { 235 | const params = { 236 | "stmt": `SELECT * FROM blocks WHERE box like '${notebook}' and hpath like '${hpath}%' and ial like "%custom-literature-key=_${key}%" and type like 'd'` 237 | }; 238 | return await this.siyuanRequest("/api/query/sql", params); 239 | } 240 | 241 | public async getLiteratureDocInPath(notebook: string, dir_hpath: string, offset: number, limit: number): Promise { 242 | const params = { 243 | "stmt": `SELECT 244 | b.id, b.root_id, b.box, b."path", b.hpath, b.name, b.content, a.value as literature_key 245 | FROM blocks b 246 | left outer join ( 247 | select * FROM "attributes" WHERE name = "custom-literature-key" 248 | ) as a on b.id = a.block_id 249 | WHERE 250 | b.box like '${notebook}' and 251 | b.hpath like '${dir_hpath}%' and 252 | b.type like 'd' limit ${limit}, ${offset}` 253 | }; 254 | return await this.siyuanRequest("/api/query/sql", params); 255 | } 256 | 257 | public async getLiteratureUserData(literatureId: string) { 258 | const params = { 259 | "stmt": `select a.block_id from attributes a where 260 | name = "custom-literature-block-type" and 261 | value = "user data" and 262 | root_id = "${literatureId}"` 263 | }; 264 | return await this.siyuanRequest("/api/query/sql", params); 265 | } 266 | 267 | public async getBlockContent(blockId: string) { 268 | const params = { 269 | "id": blockId 270 | }; 271 | return await this.siyuanRequest("/api/block/getBlockKramdown", params); 272 | } 273 | 274 | public async getChidBlocks(blockId: string) { 275 | const params = { 276 | "id": blockId 277 | }; 278 | return await this.siyuanRequest("/api/block/getChildBlocks", params); 279 | } 280 | 281 | public async updateBlockContent(blockId: string, dataType: "markdown" | "dom", data: string) { 282 | const params = { 283 | "dataType": dataType, 284 | "data": data, 285 | "id": blockId 286 | }; 287 | return await this.siyuanRequest("/api/block/updateBlock", params); 288 | } 289 | 290 | public async getCitedBlocks(fileId: string) { 291 | const params = { 292 | "stmt": `SELECT * FROM blocks WHERE root_id like '${fileId}' and type like 'p' and markdown like '%((%))%'` 293 | }; 294 | return await this.siyuanRequest("/api/query/sql", params); 295 | } 296 | 297 | public async getCitedSpans(blockId: string) { 298 | const params = { 299 | "stmt": `SELECT * FROM spans WHERE block_id like '${blockId}' and type like 'textmark block-ref'` 300 | }; 301 | return await this.siyuanRequest("/api/query/sql", params); 302 | } 303 | 304 | public async prependBlock(blockId: string, type: "markdown" | "dom", data: string) { 305 | const params = { 306 | "data": data, 307 | "dataType": type, 308 | "parentID": blockId 309 | }; 310 | return await this.siyuanRequest("/api/block/prependBlock", params); 311 | } 312 | 313 | public async appendBlock(blockId: string, type: "markdown" | "dom", data: string) { 314 | const params = { 315 | "data": data, 316 | "dataType": type, 317 | "parentID": blockId 318 | }; 319 | return await this.siyuanRequest("/api/block/appendBlock", params); 320 | } 321 | 322 | public async updateCitedBlock(blockId: string, md: string): Promise { 323 | const updateParams = { 324 | "dataType": "markdown", 325 | "data": md, 326 | "id": blockId 327 | }; 328 | return await this.siyuanRequest("/api/block/updateBlock", updateParams); 329 | } 330 | 331 | public async getBlock(blockId: string): Promise { 332 | const params = { 333 | "stmt": `SELECT * FROM blocks WHERE id like '${blockId}'` 334 | }; 335 | return await this.siyuanRequest("/api/query/sql", params); 336 | } 337 | 338 | public async getBlockAttrs(blockId: string): Promise { 339 | const params = { 340 | "id": blockId 341 | }; 342 | return await this.siyuanRequest("/api/attr/getBlockAttrs", params); 343 | } 344 | 345 | public async renameDoc(notebook: string, path: string, title: string) { 346 | const params = { 347 | "notebook": notebook, 348 | "path": path, 349 | "title": title 350 | }; 351 | return await this.siyuanRequest("/api/filetree/renameDoc", params); 352 | } 353 | 354 | public async setBlockKey(blockId: string, key: string): Promise { 355 | const attrParams = { 356 | "id": blockId, 357 | "attrs": { 358 | "custom-literature-key": key 359 | } 360 | }; 361 | return await this.siyuanRequest("/api/attr/setBlockAttrs", attrParams); 362 | } 363 | 364 | public async setNameOfBlock(blockId: string, name:string){ 365 | const attrParams = { 366 | "id": blockId, 367 | "attrs": { 368 | "name": name 369 | } 370 | }; 371 | return await this.siyuanRequest("/api/attr/setBlockAttrs", attrParams); 372 | } 373 | 374 | public async setBlockAttr(blockId: string, attrs: {[key: string]: string}): Promise { 375 | const attrParams = { 376 | "id": blockId, 377 | "attrs": attrs 378 | }; 379 | return await this.siyuanRequest("/api/attr/setBlockAttrs", attrParams); 380 | } 381 | 382 | public async setBlockEntry(blockId: string, entryData: string): Promise { 383 | const attrParams = { 384 | "id": blockId, 385 | "attrs": { 386 | "custom-entry-data": entryData 387 | } 388 | }; 389 | return await this.siyuanRequest("/api/attr/setBlockAttrs", attrParams); 390 | } 391 | 392 | public async getBlocksWithContent(notebookId: string, fileId: string, content: string) { 393 | const params = { 394 | "stmt": `SELECT * FROM blocks WHERE box like '${notebookId}' and root_id like '${fileId}' and type like 'p' and markdown like '${content}'` 395 | }; 396 | return await this.siyuanRequest("/api/query/sql", params); 397 | } 398 | 399 | public async getAttributeView(avID: string) { 400 | return await this.siyuanRequest("/api/av/getAttributeView", {id: avID}); 401 | } 402 | 403 | public async getAttributeViewKeys(id: string) { 404 | return await this.siyuanRequest("api/av/getAttributeViewKeys", {id}); 405 | } 406 | 407 | public async addAttributeViewBlocks(avID: string, srcs: { 408 | id: string, 409 | isDetached: boolean 410 | }[]) { 411 | return await this.siyuanRequest("/api/av/addAttributeViewBlocks", { 412 | avID, 413 | srcs 414 | }) 415 | } 416 | 417 | public async setAttributeViewBlockAttr(avID: string, keyID: string, blockID: string,value:any ) { 418 | return await this.siyuanRequest("/api/av/setAttributeViewBlockAttr", { 419 | avID, 420 | keyID, 421 | rowID: blockID, 422 | value 423 | }) 424 | } 425 | 426 | public async setExport(options: object) { 427 | return await this.siyuanRequest("/api/setting/setExport", options); 428 | 429 | } 430 | 431 | public async exportMDContent(blockID: string) { 432 | const params = { 433 | "id": blockID 434 | }; 435 | return await this.siyuanRequest("/api/export/exportMdContent", params); 436 | } 437 | 438 | public async exportResources(paths: string[], name: string) { 439 | const params = { 440 | paths, 441 | name 442 | }; 443 | return await this.siyuanRequest("/api/export/exportResources", params); 444 | } 445 | 446 | public async pandoc(dir: string, args: string[]) { 447 | const params = {dir, args}; 448 | return await this.siyuanRequest("/api/convert/pandoc", params); 449 | } 450 | 451 | public async renderTemplate(id: string, absPath: string) { 452 | const params = { 453 | id, path: absPath 454 | } 455 | return await this.siyuanRequest("/api/template/render", params); 456 | } 457 | } 458 | 459 | export default KernelApi; 460 | -------------------------------------------------------------------------------- /src/api/networkManagers.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "../utils/util"; 2 | import type SiYuanPluginCitation from "../index"; 3 | import axios from "axios"; 4 | 5 | 6 | export class NetworkMananger { 7 | 8 | private reqNum; 9 | 10 | constructor(private plugin: SiYuanPluginCitation, private reqLimit: number) { 11 | this.reqNum = 0; 12 | } 13 | 14 | public async sendRequest(reqOpt: {method: string, url: string, headers: any, data: string}) { 15 | while (this.reqNum >= this.reqLimit) { 16 | await sleep(500); 17 | } 18 | this.reqNum += 1; 19 | const res = await axios(reqOpt); 20 | this.reqNum -= 1; 21 | return res; 22 | } 23 | 24 | public async sendNetworkMission(params: any[], missionFn:any) { 25 | while (this.reqNum >= this.reqLimit) { 26 | await sleep(500); 27 | } 28 | this.reqNum += 1; 29 | const res = await missionFn(...params); 30 | this.reqNum -= 1; 31 | return res; 32 | } 33 | } -------------------------------------------------------------------------------- /src/database/database.ts: -------------------------------------------------------------------------------- 1 | import { Protyle, Menu } from "siyuan"; 2 | import SiYuanPluginCitation from "../index"; 3 | import{ 4 | loadLocalRef, 5 | sleep 6 | } from "../utils/util"; 7 | import { isDev, databaseType, STORAGE_NAME } from "../utils/constants"; 8 | import { createLogger, type ILogger } from "../utils/simple-logger"; 9 | import { 10 | DataModal, 11 | FilesModal, 12 | ZoteroModal, 13 | ZoteroDBModal 14 | } from "./modal"; 15 | 16 | export type DatabaseType = typeof databaseType[number]; 17 | 18 | export class Database { 19 | private logger: ILogger; 20 | public type: DatabaseType | null; 21 | private dataModal!: DataModal; 22 | private refStartNode: HTMLElement | null; 23 | private refEndNode: HTMLElement | null; 24 | 25 | private protyle!: Protyle; 26 | 27 | constructor(private plugin: SiYuanPluginCitation) { 28 | this.logger = createLogger("database"); 29 | this.type = null; 30 | this.refStartNode = null; 31 | this.refEndNode = null; 32 | } 33 | 34 | public async buildDatabase(type: DatabaseType) { 35 | // 如果数据库类型没变化就不需要再构建 36 | if (type === this.type) { 37 | if (isDev) this.logger.info("数据库无变化,不需要重建"); 38 | return null; 39 | } 40 | this.type = type; 41 | 42 | // 如果已经存在就删除原先的 43 | // if (this.dataModal) delete this.dataModal; 44 | 45 | if (isDev) this.logger.info("建立数据库类型=>", {type}); 46 | switch (type) { 47 | case "BibTex and CSL-JSON": { 48 | this.dataModal = new FilesModal(this.plugin); 49 | break; 50 | } 51 | case "Zotero (better-bibtex)": { 52 | this.dataModal = new ZoteroModal(this.plugin, "Zotero"); 53 | break; 54 | } 55 | case "Juris-M (better-bibtex)": { 56 | this.dataModal = new ZoteroModal(this.plugin, "Juris-M"); 57 | break; 58 | } 59 | case "Zotero (debug-bridge)": { 60 | this.dataModal = new ZoteroDBModal(this.plugin, "Zotero", this.plugin.data[STORAGE_NAME].useItemKey); 61 | break; 62 | } 63 | case "Juris-M (debug-bridge)": { 64 | this.dataModal = new ZoteroDBModal(this.plugin, "Juris-M", this.plugin.data[STORAGE_NAME].useItemKey); 65 | break; 66 | } 67 | } 68 | await this.dataModal.buildModal(); 69 | if (isDev) this.logger.info("载入引用"); 70 | loadLocalRef(this.plugin); 71 | } 72 | 73 | 74 | // Group: 功能接口 75 | 76 | public async insertCiteLink(protyle: Protyle) { 77 | this.protyle = protyle; 78 | if (await this.checkSettings()) return this.dataModal.showSearching(protyle, this.insertCiteLinkBySelection.bind(this)); 79 | else protyle.insert("", false, true); 80 | } 81 | 82 | public insertNotes(protyle:Protyle) { 83 | this.protyle = protyle; 84 | this.dataModal.showSearching(protyle, this.insertNotesBySelection.bind(this)); 85 | } 86 | 87 | public async insertSelectedCiteLink(protyle: Protyle) { 88 | this.protyle = protyle; 89 | if (await this.checkSettings()) { 90 | const keys = await this.dataModal.getSelectedItems(); 91 | if (isDev) this.logger.info("获得到Zotero中选中的条目, keys=>", keys); 92 | if (!keys.length) this.plugin.noticer.info((this.plugin.i18n.notices as any).noSelectedItem); 93 | else this.insertCiteLinkBySelection(keys); 94 | } else { 95 | protyle.insert("", false, true); 96 | } 97 | } 98 | 99 | public async copyCiteLink() { 100 | if (await this.checkSettings()) return this.dataModal.showSearching(null, this.copyCiteLinkBySelection.bind(this)); 101 | } 102 | 103 | public copyNotes() { 104 | this.dataModal.showSearching(null, this.copyNotesBySelection.bind(this)); 105 | } 106 | 107 | public async copySelectedCiteLink() { 108 | if (await this.checkSettings()) { 109 | const keys = await this.dataModal.getSelectedItems(); 110 | if (isDev) this.logger.info("获得到Zotero中选中的条目, keys=>", keys); 111 | if (!keys.length) this.plugin.noticer.info((this.plugin.i18n.notices as any).noSelectedItem); 112 | else this.copyCiteLinkBySelection(keys); 113 | } 114 | } 115 | 116 | 117 | // Group: 间接通用接口 118 | 119 | public setSelected(keys: string[]) { 120 | if (!keys.length) { 121 | // 确保可能导致选择问题的全部为空 122 | this.setRefNode(null, null); 123 | this.plugin.reference.setEmptySelection(); 124 | } 125 | this.dataModal.selectedList = keys; 126 | } 127 | 128 | public setRefNode(refStartNode: HTMLElement | null, refEndNode: HTMLElement | null) { 129 | this.refStartNode = refStartNode; 130 | this.refEndNode = refEndNode; 131 | } 132 | 133 | public async getContentByKey(key: string, shortAuthorLimit: number = 2) { 134 | const content = await this.dataModal.getContentFromKey(key, shortAuthorLimit); 135 | return content; 136 | } 137 | 138 | public async getAttachmentByItemKey(itemKey: string) { 139 | return await this.dataModal.getAttachmentByItemKey(itemKey); 140 | } 141 | 142 | public getTotalKeys() { 143 | return this.dataModal.getTotalKeys(); 144 | } 145 | 146 | public async updateDataSourceItem(key: string, content: {[attr: string]: any}) { 147 | return this.dataModal.updateDataSourceItem(key, content); 148 | } 149 | 150 | private async checkSettings(): Promise { 151 | await this.plugin.reference.checkRefDirExist(); 152 | if (this.plugin.data[STORAGE_NAME].referenceNotebook === "") { 153 | this.plugin.noticer.error((this.plugin.i18n.errors as any).notebookUnselected); 154 | if (isDev) this.logger.error("未选择笔记本!"); 155 | } else if (!this.plugin.isRefPathExist) { 156 | this.plugin.noticer.error((this.plugin.i18n.errors as any).refPathInvalid); 157 | if (isDev) this.logger.error("文献库路径不存在!"); 158 | } else { 159 | return true; 160 | } 161 | return false; 162 | } 163 | 164 | private async insertCiteLinkBySelection(keys: string[]) { 165 | const fileId = (this.protyle as any).protyle.block.rootID; 166 | await this.plugin.reference.checkRefDirExist(); 167 | if (this.plugin.isRefPathExist) { 168 | const menu = new Menu("cite-type-selection"); 169 | const linkTempGroup = this.plugin.data[STORAGE_NAME].linkTemplatesGroup; 170 | const useDefaultCiteType = this.plugin.data[STORAGE_NAME].useDefaultCiteType; 171 | if (!useDefaultCiteType) { 172 | linkTempGroup.map((tmp: { name: string | undefined; }) => { 173 | menu.addItem({ 174 | label: tmp.name, 175 | icon: "iconRef", 176 | click: async () => { 177 | const content = await this.plugin.reference.processReferenceContents(keys, fileId, tmp.name); 178 | this.plugin.reference.insertContent(this.protyle, content.join(""), this.refStartNode, this.refEndNode); 179 | } 180 | }); 181 | }); 182 | if (isDev) this.logger.info("展示引用类型选择菜单", menu); 183 | const rect = this.protyle.protyle.toolbar!.range.getBoundingClientRect(); 184 | await sleep(500); 185 | menu.open({ 186 | x: rect.left, 187 | y: rect.top 188 | }); 189 | } else { 190 | const content = await this.plugin.reference.processReferenceContents(keys, fileId, linkTempGroup[0].name); 191 | this.plugin.reference.insertContent(this.protyle, content.join(""), this.refStartNode, this.refEndNode); 192 | } 193 | } 194 | } 195 | 196 | private async insertNotesBySelection(keys: string[]) { 197 | const insertContent = keys.map(async key => { 198 | return await this.dataModal.getCollectedNotesFromKey(key); 199 | }); 200 | const content = await Promise.all(insertContent); 201 | this.plugin.reference.insertContent(this.protyle, content.join("")); 202 | } 203 | 204 | private async copyCiteLinkBySelection(keys: string[]) { 205 | await this.plugin.reference.checkRefDirExist(); 206 | if (this.plugin.isRefPathExist) { 207 | const content = await this.plugin.reference.processReferenceContents(keys); 208 | this.plugin.reference.copyContent(content.join(""), this.plugin.i18n.citeLink); 209 | } 210 | } 211 | 212 | private async copyNotesBySelection(keys: string[]) { 213 | const insertContent = keys.map(async key => { 214 | return await this.dataModal.getCollectedNotesFromKey(key); 215 | }); 216 | const content = await Promise.all(insertContent); 217 | this.plugin.reference.copyContent(content.join(""), this.plugin.i18n.notes); 218 | } 219 | } -------------------------------------------------------------------------------- /src/database/zoteroLibrary.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import { Entry, type Author, type IIndexable, type File, type SingleNote } from "./filesLibrary"; 3 | import { htmlNotesProcess } from "../utils/notes"; 4 | import { filePathProcess, fileNameProcess } from "../utils/util"; 5 | 6 | interface Creator { 7 | firstName?: string, 8 | lastName?: string, 9 | prefix?: string, 10 | suffix?: string, 11 | name?: string, 12 | creatorType: string 13 | } 14 | 15 | interface Annotation { 16 | parentKey: string, 17 | parentTitle: string, 18 | details: AnnotationDetail[] 19 | } 20 | 21 | interface AnnotationDetail { 22 | key: string, 23 | annotationText: string, 24 | annotationType: string, 25 | annotationPosition: { 26 | pageIndex: number 27 | }, 28 | annotationComment?: string 29 | } 30 | 31 | export interface EntryDataZotero { 32 | abstractNote?: string; 33 | accessDate?: string; 34 | attachments?: any[]; 35 | annotations?: Annotation[]; 36 | citekey?: string; 37 | citationKey?: string; 38 | conferenceName?: string; 39 | creators?: Creator[]; 40 | thesisType?: string; 41 | date?: string; 42 | dateAdded?: string; 43 | dateModified?: string; 44 | DOI?: string; 45 | edition?: string; 46 | eprint?: string; 47 | eprinttype?: string; 48 | ISBN?: string; 49 | ISSN?: string; 50 | itemID?: number; 51 | itemKey: string; 52 | itemType?: string; 53 | language?: string; 54 | libraryID: number; 55 | journalAbbreviation?: string; 56 | notes?: any[]; 57 | numPages?: string; 58 | pages?: string; 59 | place?: string; 60 | primaryClass?: string; 61 | proceedingsTitle?: string; 62 | publisher?: string; 63 | publicationTitle?: string; 64 | relations?: any[]; 65 | tags?: any[]; 66 | title?: string; 67 | university?: string; 68 | url?: string; 69 | volume?: string; 70 | } 71 | 72 | const ZOTERO_PROPERTY_MAPPING: Record = { 73 | booktitle: "_containerTitle", 74 | date: "issued", 75 | DOI: "DOI", 76 | eprint: "eprint", 77 | eprinttype: "eprinttype", 78 | eventtitle: "event", 79 | journal: "_containerTitle", 80 | journaltitle: "_containerTitle", 81 | location: "publisherPlace", 82 | pages: "page", 83 | title: "title", 84 | venue: "eventPlace", 85 | year: "_year", 86 | publisher: "publisher", 87 | notes: "_note", 88 | abstractNote: "abstract", 89 | accessDate: "accessDate", 90 | attachments: "_attachments", 91 | conferenceName: "_containerTitle", 92 | thesisType: "thesis", 93 | dateAdded: "dateAdded", 94 | dateModified: "dateModified", 95 | edition: "edition", 96 | ISBN: "ISBN", 97 | ISSN: "ISSN", 98 | itemID: "itemID", 99 | itemKey: "itemKey", 100 | language: "lang", 101 | journalAbbreviation: "containerTitleShort", 102 | shortjournal: "containerTitleShort", 103 | numPages: "numPages", 104 | place: "publisherPlace", 105 | primaryClass: "primaryclass", 106 | proceedingsTitle: "_containerTitle", 107 | publicationTitle: "_containerTitle", 108 | relations: "_relations", 109 | shortTitle: "titleShort", 110 | tags: "_tags", 111 | university: "publisher", 112 | url: "URL", 113 | volume: "volume" 114 | }; 115 | 116 | export class EntryZoteroAdapter extends Entry { 117 | abstract?: string; 118 | _containerTitle?: string; 119 | containerTitleShort?: string; 120 | DOI?: string; 121 | eprint?: string; 122 | eprinttype?: string; 123 | event?: string; 124 | eventPlace?: string; 125 | issued?: string; 126 | itemKey!: string; 127 | page?: string; 128 | primaryclass?: string; 129 | publisher?: string; 130 | publisherPlace?: string; 131 | title?: string; 132 | titleShort?: string; 133 | thesis?: string; 134 | URL?: string; 135 | useItemKey: boolean; 136 | shortAuthorLimit: number; 137 | declare _year?: string; 138 | declare _note?: any[]; 139 | declare _tags?: any[]; 140 | 141 | constructor(private data: EntryDataZotero, useItemKey = false, shortAuthorLimit = 2) { 142 | super(); 143 | 144 | this.useItemKey = useItemKey; 145 | this.shortAuthorLimit = shortAuthorLimit; 146 | 147 | Object.entries(ZOTERO_PROPERTY_MAPPING).forEach( 148 | (map: [string, string]) => { 149 | const [src, tgt] = map; 150 | if (Object.keys(this.data).indexOf(src) != -1) { 151 | const val = (this.data as Record)[src]; 152 | 153 | (this as IIndexable)[tgt] = val; 154 | } 155 | }, 156 | ); 157 | } 158 | 159 | get id() { 160 | return (this.data.citekey || this.data.citationKey)!; 161 | } 162 | 163 | get key() { 164 | if (this.useItemKey || !this.id.length) return this.data.libraryID + "_" + this.itemKey; 165 | else return this.data.libraryID + "_" + this.id; 166 | } 167 | 168 | get type() { 169 | return this.data.itemType!; 170 | } 171 | 172 | get files(): string[] { 173 | const attachments = this.data.attachments ?? []; 174 | return [...attachments.reduce((acc, attach) => { 175 | const fileName = attach.title; 176 | if (attach.path) { 177 | const res = (attach.path as string).split("."); 178 | const fileType = res[res.length - 1]; 179 | if (fileType === "pdf") { 180 | const res = (attach.select as string).split("/"); 181 | const itemID = res[res.length - 1]; 182 | return [...acc, `[[Open]](zotero://open-pdf/library/items/${itemID})\t|\t[${fileName}](file://${filePathProcess(attach.path)})`]; 183 | } 184 | return [...acc, `[[Locate]](${attach.select})\t|\t[${fileName}](file://${filePathProcess(attach.path)})`]; 185 | } else return acc; 186 | }, [])]; 187 | } 188 | 189 | get fileList(): File[] { 190 | const attachments = this.data.attachments ?? []; 191 | return [...attachments.reduce((acc, attach) => { 192 | const fileName = attach.title; 193 | if (attach.path) { 194 | const res = (attach.path as string).split("."); 195 | const fileType = res[res.length - 1]; 196 | let zoteroOpenURI = ""; 197 | if (fileType === "pdf") { 198 | const res = (attach.select as string).split("/"); 199 | const itemID = res[res.length - 1]; 200 | zoteroOpenURI = `zotero://open-pdf/library/items/${itemID}`; 201 | } 202 | return [...acc, { 203 | fileName, 204 | type: fileType, 205 | path: "file://" + filePathProcess(attach.path), 206 | zoteroOpenURI, 207 | zoteroSelectURI: attach.select 208 | } as File]; 209 | } else return acc; 210 | }, [])]; 211 | } 212 | 213 | get authorString() { 214 | const authors = this.data.creators?.filter(c => c.creatorType === "author"); 215 | if (authors) { 216 | const names = authors.map((name) => { 217 | if (name.name) { 218 | return name.name; 219 | } else { 220 | const parts = [name.firstName, name.prefix, name.lastName, name.suffix]; 221 | // Drop any null parts and join 222 | return parts.filter((x) => x).join(" "); 223 | } 224 | }); 225 | return names.join(", "); 226 | } else { 227 | return undefined; 228 | } 229 | } 230 | 231 | get creators() { 232 | return this.data.creators; 233 | } 234 | 235 | get firstCreator() { 236 | return this.data.creators ? this.data.creators[0] : null; 237 | } 238 | 239 | get annotations(): any[] { 240 | const annotations = this.data.annotations ?? []; 241 | return annotations.map((anno, index) => { 242 | const title = `\n\n---\n\n###### Annotation in ${anno.parentTitle}\n\n`; 243 | const content = anno.details.map((detail, idx) => { 244 | const openURI = `zotero://open-pdf/library/items/${anno.parentKey}?page=${detail.annotationPosition.pageIndex}&annotation=${detail.key}`; 245 | return { 246 | index: idx, 247 | detail, 248 | openURI 249 | }; 250 | }); 251 | return { 252 | title, 253 | index, 254 | content 255 | }; 256 | }); 257 | } 258 | 259 | get annotationList() { 260 | const annotations = this.data.annotations ?? []; 261 | return annotations.map(anno => { 262 | return { 263 | ...anno, 264 | details: anno.details.map(detail => { 265 | return { 266 | zoteroOpenURI: `zotero://open-pdf/library/items/${anno.parentKey}?page=${detail.annotationPosition.pageIndex}&annotation=${detail.key}`, 267 | ...detail 268 | }; 269 | }) 270 | }; 271 | }); 272 | } 273 | 274 | get shortAuthor(): string { 275 | const limit = this.shortAuthorLimit; 276 | let shortAuthor = ""; 277 | const author = this.data.creators?.filter(c => c.creatorType === "author"); 278 | if (!author || author.length == 0) { 279 | return ""; 280 | } 281 | for (let i = 0; i < limit && i < author.length; i++) { 282 | const name = author[i].lastName ? author[i].lastName: author[i].name; 283 | if (i == 0) { 284 | shortAuthor += name ?? ""; 285 | if (limit < author.length) { 286 | shortAuthor += shortAuthor.length ? " et al." : ""; 287 | } 288 | } else if (i == limit - 1) { 289 | shortAuthor += name ? " and " + name : ""; 290 | if (limit < author.length) { 291 | shortAuthor += shortAuthor.length ? " et al." : ""; 292 | } 293 | } else if (author.length < limit && i == author.length - 1) { 294 | shortAuthor += name ? " and " + name : ""; 295 | } else { 296 | shortAuthor += name ? ", " + name : ""; 297 | } 298 | } 299 | return shortAuthor; 300 | } 301 | 302 | get containerTitle() { 303 | if (this._containerTitle) { 304 | return this._containerTitle; 305 | } else if (this.eprint) { 306 | const prefix = this.eprinttype 307 | ? `${this.eprinttype}:` 308 | : ""; 309 | const suffix = this.primaryclass 310 | ? ` [${this.primaryclass}]` 311 | : ""; 312 | return `${prefix}${this.eprint}${suffix}`; 313 | } else if (this.type === "thesis") { 314 | return `${this.publisher} ${this.thesis}`; 315 | } else { 316 | return undefined; 317 | } 318 | } 319 | 320 | get issuedDate() { 321 | if (this.issued) { 322 | const splits = ["-", "/", "\\", "=", " "]; 323 | for (const s of splits) { 324 | const conditions = [ 325 | `YYYY${s}MM${s}DD`, `DD${s}MM${s}YY`, `MM${s}YYYY`, `YYYY${s}MM` 326 | ]; 327 | if (this.issued.split(s).length > 1) { 328 | return moment(this.issued, conditions).toDate(); 329 | } 330 | } 331 | return moment(this.issued, "YYYY").toDate(); 332 | } else return undefined; 333 | } 334 | 335 | get author(): Author[] { 336 | const authors = this.data.creators?.filter(c => c.creatorType === "author"); 337 | if (authors) { 338 | return authors.map((a) => { 339 | if (a.name) { 340 | return { 341 | given: "", 342 | family: a.name 343 | }; 344 | } else { 345 | return { 346 | given: a.firstName, 347 | family: a.lastName, 348 | }; 349 | } 350 | }); 351 | } else { 352 | return []; 353 | } 354 | } 355 | 356 | get note(): SingleNote[] { 357 | return this._note ? this._note.map((singleNote, index) => { 358 | return { 359 | index: index, 360 | prefix: `\n\n---\n\n###### Note No.${index+1}\t[[Locate]](zotero://select/items/0_${singleNote.key}/)\n\n\n\n`, 361 | content: `\n${singleNote.note.replace(/\\(.?)/g, (_m: any, p1: any) => p1)}\n` 362 | }; 363 | }) : []; 364 | } 365 | 366 | get tags(): string | undefined { 367 | return this._tags?.map(tag => tag.tag).join(", "); 368 | } 369 | 370 | public get zoteroSelectURI(): string { 371 | return `zotero://select/library/items/${this.itemKey}`; 372 | } 373 | } 374 | 375 | /** 376 | * For the given citekey, find the corresponding `Entry` and return a 377 | * collection of template variable assignments. 378 | */ 379 | export function getTemplateVariablesForZoteroEntry(entry: EntryZoteroAdapter): Record { 380 | const shortcuts = { 381 | key: entry.key, 382 | citekey: entry.id?.length ? entry.id : entry.itemKey, 383 | 384 | abstract: entry.abstract, 385 | author: entry.author, 386 | authorString: entry.authorString, 387 | annotations: entry.annotations, 388 | annotationList: entry.annotationList, 389 | containerTitle: entry.containerTitle, 390 | containerTitleShort: entry.containerTitleShort, 391 | creators: entry.creators, 392 | DOI: entry.DOI, 393 | eprint: entry.eprint, 394 | eprinttype: entry.eprinttype, 395 | eventPlace: entry.eventPlace, 396 | files: entry.files, 397 | fileList: entry.fileList, 398 | firstCreator: entry.firstCreator, 399 | getNow: moment(), 400 | note: entry.note, 401 | page: entry.page, 402 | publisher: entry.publisher, 403 | publisherPlace: entry.publisherPlace, 404 | tags: entry.tags, 405 | title: entry.title, 406 | titleShort: entry.titleShort, 407 | type: entry.type, 408 | shortAuthor: entry.shortAuthor, 409 | URL: entry.URL, 410 | year: entry.year?.toString(), 411 | itemKey: entry.itemKey, 412 | zoteroSelectURI: entry.zoteroSelectURI, 413 | }; 414 | 415 | return { entry: entry.toJSON(), ...shortcuts }; 416 | } 417 | -------------------------------------------------------------------------------- /src/events/customEventBus.ts: -------------------------------------------------------------------------------- 1 | import type { TEventBus } from "siyuan"; 2 | import { createLogger, type ILogger } from "../utils/simple-logger"; 3 | import type SiYuanPluginCitation from "../index"; 4 | import { isDev, STORAGE_NAME } from "../utils/constants"; 5 | import { loadLocalRef } from "../utils/util"; 6 | 7 | const ruleType = ["GetFromPool", "Refresh"] as const; 8 | type TRuleType = typeof ruleType[number]; 9 | 10 | interface CustomEventDetail { 11 | type: string, 12 | triggerFn: (params: any) => any 13 | } 14 | 15 | interface RefreshEventDetail extends CustomEventDetail { 16 | type: "database" | "literature note", 17 | docIDs?: string[] 18 | keys?: string[] 19 | refreshAll?: boolean 20 | confirmUserData?:boolean 21 | } 22 | 23 | interface GetEventDetail extends CustomEventDetail { 24 | keyorid: string 25 | triggerFn: (idorkey: string) => any 26 | } 27 | 28 | export class CustomEventBus { 29 | 30 | private logger: ILogger; 31 | 32 | constructor(private plugin: SiYuanPluginCitation) { 33 | this.logger = createLogger("Custom EventBus"); 34 | ruleType.forEach(rule => { 35 | this.plugin.eventBus.on(rule as TEventBus, this.manageRules.bind(this)); 36 | }); 37 | } 38 | 39 | private manageRules(e: CustomEvent) { 40 | if (isDev) this.logger.info("EventBus触发,event=>", e); 41 | switch (e.type as TRuleType) { 42 | case "GetFromPool": this.getFromPool(e.detail as GetEventDetail); break; 43 | case "Refresh": this.refresh(e.detail as RefreshEventDetail); break; 44 | } 45 | } 46 | 47 | private getFromPool(detail: GetEventDetail) { 48 | detail.triggerFn(this.plugin.literaturePool.get(detail.keyorid)); 49 | } 50 | 51 | private async refresh(detail: RefreshEventDetail) { 52 | if (detail.type == "database") { 53 | await this.plugin.reference.checkRefDirExist(); 54 | return this.plugin.database.buildDatabase(this.plugin.data[STORAGE_NAME].database); 55 | } else if (detail.type == "literature note") { 56 | if (detail.refreshAll) { 57 | this.plugin.reference.refreshLiteratureNoteContents(detail.confirmUserData); 58 | } else { 59 | await loadLocalRef(this.plugin); 60 | if (detail.docIDs) { 61 | detail.docIDs.forEach(id => { 62 | this.plugin.reference.refreshSingleLiteratureNote(id, false, detail.confirmUserData); 63 | }); 64 | } 65 | if (detail.keys) { 66 | detail.keys.forEach(key => { 67 | const id = this.plugin.literaturePool.get(key); 68 | this.plugin.reference.refreshSingleLiteratureNote(id, false, detail.confirmUserData); 69 | }); 70 | } 71 | } 72 | } 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /src/events/eventTrigger.ts: -------------------------------------------------------------------------------- 1 | import type { TEventBus } from "siyuan"; 2 | import type SiYuanPluginCitation from "../index"; 3 | import { isDev } from "../utils/constants"; 4 | import { createLogger, type ILogger } from "../utils/simple-logger"; 5 | import { CustomEventBus } from "./customEventBus"; 6 | 7 | class QueueT { 8 | private data: Array; 9 | constructor() {this.data = new Array;} 10 | push = (item: T) => this.data.push(item); 11 | pop = (): T|undefined => { 12 | if (!this.data.length) return undefined; 13 | else { 14 | const result = this.data[0]; 15 | this.data = this.data.slice(1); 16 | return result; 17 | } 18 | }; 19 | } 20 | 21 | interface EventQueue { 22 | type: "repeated" | "once"; 23 | params: Record; 24 | queue?: QueueT; 25 | triggerFns?: ((params: any) => any)[]; 26 | } 27 | 28 | export interface TriggeredEvent { 29 | triggerFn: (params: any) => any; 30 | params: Record; 31 | type: "repeated" | "once" 32 | } 33 | 34 | // 管理特殊的事件触发 35 | export class EventTrigger { 36 | private eventQueue: Record; 37 | private logger: ILogger; 38 | private customEvent: CustomEventBus; 39 | private onProcessing: boolean; 40 | 41 | 42 | constructor(private plugin: SiYuanPluginCitation){ 43 | this.eventQueue = {}; 44 | this.onProcessing = false; 45 | this.logger = createLogger("event trigger"); 46 | this.plugin.eventBus.on("ws-main", this.wsMainTrigger.bind(this)); 47 | this.customEvent = new CustomEventBus(this.plugin); 48 | } 49 | 50 | private wsMainTrigger(event: CustomEvent) { 51 | if (this.onProcessing) return; 52 | // if (isDev) this.logger.info("触发器运行中"); 53 | if ( Object.keys(this.eventQueue).indexOf(event.detail.cmd) != -1 ) { 54 | // 在这里输出不方便debug 55 | // if (isDev) this.logger.info("事件触发,event =>", {type: "ws-main", cmd: event.detail.cmd}); 56 | this.execEvent(event.detail.cmd, event); 57 | } 58 | } 59 | 60 | public addEventBusEvent(type: TEventBus, event: (e: CustomEvent) => void) { 61 | this.plugin.eventBus.on(type, event); 62 | } 63 | 64 | public addSQLIndexEvent(event: TriggeredEvent) { 65 | this.addEvent("databaseIndexCommit", event); 66 | } 67 | 68 | private async execEvent(name: string, event: CustomEvent) { 69 | this.onProcessing = true; 70 | if (this.eventQueue[name].type == "repeated") { 71 | const triggerEvent = this.eventQueue[name]; 72 | // 对于重复执行的任务,就不需要触发输出了,不然太麻烦 73 | // if (isDev) this.logger.info("事件执行,event=>", {...triggerEvent, name}); 74 | if (!this.eventQueue[name].triggerFns) return; 75 | const pList = this.eventQueue[name].triggerFns.map(async (triggerFn) => { 76 | await triggerFn({...triggerEvent.params, event}); 77 | }); 78 | await Promise.all(pList); 79 | } else if (this.eventQueue[name].type == "once") { 80 | const triggerEvents = []; 81 | let triggerEvent = this.withdrawEvent(name); 82 | while (triggerEvent) { 83 | triggerEvents.push(triggerEvent); 84 | triggerEvent = this.withdrawEvent(name); 85 | } 86 | const pList = triggerEvents.map( (tre:TriggeredEvent) => { 87 | if (isDev) this.logger.info("事件执行,event=>", {...tre, name}); 88 | return new Promise( async (resolve, ) => { 89 | resolve(await tre.triggerFn({...tre.params, event})); 90 | }); 91 | }); 92 | await Promise.all(pList); 93 | } 94 | // if (isDev) this.logger.info("事件执行完毕"); 95 | this.onProcessing = false; 96 | } 97 | 98 | public addEvent(name: string, event: TriggeredEvent) { 99 | if (isDev) this.logger.info(`向触发队列${name}中添加事件, queue=>`, this.eventQueue[name]); 100 | if (!this.eventQueue[name]) { 101 | if (isDev) this.logger.info(`新建触发队列${name}`); 102 | this.eventQueue[name] = { 103 | type: event.type, 104 | queue: new QueueT, 105 | triggerFns: [], 106 | params: event.params 107 | }; 108 | } 109 | if (event.type == "once") this.eventQueue[name].queue!.push(event); 110 | else if (event.type == "repeated") this.eventQueue[name].triggerFns!.push(event.triggerFn); 111 | } 112 | 113 | private withdrawEvent(name: string): undefined | TriggeredEvent { 114 | return this.eventQueue[name]?.queue?.pop(); 115 | } 116 | } -------------------------------------------------------------------------------- /src/export/exportManager.ts: -------------------------------------------------------------------------------- 1 | import { dataDir, isDev, workspaceDir } from "../utils/constants"; 2 | import SiYuanPluginCitation from "../index"; 3 | import { createLogger, type ILogger } from "../utils/simple-logger"; 4 | 5 | export const exportTypes = ["markdown", "word", "latex"] as const; 6 | export type ExportType = typeof exportTypes[number]; 7 | interface ExportOption { 8 | "paragraphBeginningSpace"?: boolean, 9 | "addTitle"?: boolean, 10 | "markdownYFM"?: boolean, 11 | "blockRefMode"?: number, 12 | "blockEmbedMode"?: number, 13 | "fileAnnotationRefMode"?: number, 14 | "pdfFooter"?: string, 15 | "blockRefTextLeft"?: string, 16 | "blockRefTextRight"?: string, 17 | "tagOpenMarker"?: string, 18 | "tagCloseMarker"?: string, 19 | "pandocBin"?: string 20 | } 21 | 22 | export class ExportManager { 23 | private userConfig!: {[key: string]: string}; 24 | private logger: ILogger; 25 | 26 | constructor (private plugin: SiYuanPluginCitation) { 27 | this.logger = createLogger("export manager"); 28 | } 29 | 30 | public async export(exportID: string, exportType: ExportType ) { 31 | switch (exportType) { 32 | case "markdown": { 33 | return await this.exportMarkdown(exportID); 34 | } 35 | case "word": { 36 | return await this.exportWord(exportID); 37 | } 38 | case "latex": { 39 | return await this.exportLatex(exportID); 40 | } 41 | } 42 | 43 | } 44 | 45 | private async exportMarkdown(exportID: string) { 46 | // 导出带有citekey的markdown文档 47 | await this.setExportConfig({ 48 | blockRefMode: 2, 49 | blockEmbedMode: 0, 50 | blockRefTextLeft: "\\exportRef{", 51 | blockRefTextRight: "\}" 52 | } as ExportOption); 53 | 54 | try { 55 | const refReg = /\[\\exportRef\{(.*?)\}\]\(siyuan:\/\/blocks\/(.*?)\)/; 56 | const citeBlockIDs = this.plugin.literaturePool.ids; 57 | let res = await this.plugin.kernelApi.getBlock(exportID); 58 | const fileTitle = (res.data as any)[0].content; 59 | res = await this.plugin.kernelApi.exportMDContent(exportID); 60 | let content = (res.data as any).content as string; 61 | if (isDev) this.logger.info("获得导出内容,content=>", {content}); 62 | let iter = 0; 63 | while (content.match(refReg)) { 64 | const match = content.match(refReg); 65 | let replaceContent = ""; 66 | if (citeBlockIDs.indexOf(match![2]) != -1) { 67 | const key = this.plugin.literaturePool.get(match![2]); 68 | const entry = await this.plugin.database.getContentByKey(key); 69 | if (entry.citekey) { 70 | replaceContent = `[@${entry.citekey}]`; 71 | } else { 72 | replaceContent = `[@${match![1]}](zotero://select/library/items/${entry.itemKey})` 73 | } 74 | } else { 75 | replaceContent = match![1]; 76 | } 77 | content = content.replace(refReg, replaceContent); 78 | iter = iter + 1; 79 | if (iter > 1000) break; 80 | } 81 | if (isDev) this.logger.info("获得处理后的导出内容, contents=>", {content}); 82 | // 下载文件 83 | const file = new Blob([content]); 84 | const url = window.URL.createObjectURL(file); 85 | const a = document.createElement('a'); 86 | a.href = url; 87 | a.download = `${fileTitle}.md`; 88 | a.click(); 89 | window.URL.revokeObjectURL(url); 90 | } finally { 91 | await this.resetExportConfig(); 92 | } 93 | } 94 | 95 | private async exportWord(exportID: string) { 96 | // 导出带有citekey的markdown文档 97 | await this.setExportConfig({ 98 | blockRefMode: 2, 99 | blockEmbedMode: 0, 100 | blockRefTextLeft: "\\exportRef{", 101 | blockRefTextRight: "\}" 102 | } as ExportOption); 103 | try { 104 | const refReg = /\[\\exportRef\{(.*?)\}\]\(siyuan:\/\/blocks\/(.*?)\)/; 105 | const citeBlockIDs = this.plugin.literaturePool.ids; 106 | let res = await this.plugin.kernelApi.getBlock(exportID); 107 | const fileTitle = (res.data as any)[0].content; 108 | res = await this.plugin.kernelApi.exportMDContent(exportID); 109 | let content = (res.data as any).content as string; 110 | if (isDev) this.logger.info("获得导出内容,content=>", {content}); 111 | let iter = 0; 112 | while (content.match(refReg)) { 113 | let replaceContent = ""; 114 | let totalStr = "" 115 | const match = content.match(refReg); 116 | if (citeBlockIDs.indexOf(match![2])==-1) { 117 | totalStr = match![0]; 118 | replaceContent = match![1]; 119 | } 120 | else { 121 | const following = this.getNeighborCites(content.slice(match!.index! + match![0].length), citeBlockIDs) 122 | totalStr = following ? match![0] + following.totalStr : match![0]; 123 | const keys = following ? [match![2], ...following.keys] : [match![2]]; 124 | const links = following ? [match![1], ...following.links] : [match![1]]; 125 | console.log({totalStr, keys, links}) 126 | if (keys.length == 1) { 127 | if (citeBlockIDs.indexOf(keys[0]) != -1) { 128 | const key = this.plugin.literaturePool.get(match![2]); 129 | const entry = await this.plugin.database.getContentByKey(key); 130 | replaceContent = `[@siyuan_cite{${entry.entry.data.id}}@siyuan_name{zotero_refresh_to_update}}]`; 131 | } else { 132 | replaceContent = match![1]; 133 | } 134 | } else { 135 | const ids = []; 136 | for (let key of keys) { 137 | const itemKey = this.plugin.literaturePool.get(key); 138 | const entry = await this.plugin.database.getContentByKey(itemKey); 139 | ids.push(entry.entry.data.id); 140 | } 141 | replaceContent = `[@siyuan_cite{${ids.join(",")}}@siyuan_name{zotero_refresh_to_update}}]`; 142 | } 143 | } 144 | content = content.replace(totalStr, replaceContent); 145 | iter = iter + 1; 146 | if (iter > 1000) break; 147 | } 148 | if (isDev) this.logger.info("获得处理后的导出内容, contents=>", {content}); 149 | await this.plugin.kernelApi.putFile("/temp/convert/pandoc/citation/exportTemp.md", false, new Blob([content])); 150 | await this.plugin.kernelApi.pandoc("citation", [ 151 | "./exportTemp.md", 152 | "-o", "exportTemp.docx", 153 | "--lua-filter", dataDir + "/plugins/siyuan-plugin-citation/scripts/citation.lua", 154 | "--reference-doc", this.userConfig.docxTemplate 155 | ]) 156 | res = await this.plugin.kernelApi.getFile("/temp/convert/pandoc/citation/exportTemp.docx", "any") as any; 157 | const file = await (new Response(((res as any).body as ReadableStream))).blob() 158 | // 下载 159 | const url = window.URL.createObjectURL(file); 160 | const a = document.createElement('a'); 161 | a.href = url; 162 | a.download = `${fileTitle}.docx`; 163 | a.click(); 164 | window.URL.revokeObjectURL(url); 165 | } finally { 166 | await this.resetExportConfig(); 167 | } 168 | } 169 | 170 | private async exportLatex(exportID: string) { 171 | // 导出带有citekey的markdown文档 172 | await this.setExportConfig({ 173 | blockRefMode: 2, 174 | blockEmbedMode: 0, 175 | blockRefTextLeft: "\\exportRef{", 176 | blockRefTextRight: "\}" 177 | } as ExportOption); 178 | try { 179 | const refReg = /\[\\exportRef\{(.*?)\}\]\(siyuan:\/\/blocks\/(.*?)\)/; 180 | const citeBlockIDs = this.plugin.literaturePool.ids; 181 | let res = await this.plugin.kernelApi.getBlock(exportID); 182 | const fileTitle = (res.data as any)[0].content; 183 | res = await this.plugin.kernelApi.exportMDContent(exportID); 184 | let content = (res.data as any).content as string; 185 | if (isDev) this.logger.info("获得导出内容,content=>", {content}); 186 | let iter = 0; 187 | while (content.match(refReg)) { 188 | let replaceContent = ""; 189 | let totalStr = "" 190 | const match = content.match(refReg); 191 | if (citeBlockIDs.indexOf(match![2])==-1) { 192 | totalStr = match![0]; 193 | replaceContent = match![1]; 194 | } 195 | else { 196 | const following = this.getNeighborCites(content.slice(match!.index! + match![0].length), citeBlockIDs) 197 | totalStr = following ? match![0] + following.totalStr : match![0]; 198 | const keys = following ? [match![2], ...following.keys] : [match![2]]; 199 | const links = following ? [match![1], ...following.links] : [match![1]]; 200 | console.log({totalStr, keys, links}) 201 | if (keys.length == 1) { 202 | if (citeBlockIDs.indexOf(keys[0]) != -1) { 203 | const key = this.plugin.literaturePool.get(match![2]); 204 | const entry = await this.plugin.database.getContentByKey(key); 205 | replaceContent = `\\cite{${entry.citekey}}`; 206 | } else { 207 | replaceContent = match![1]; 208 | } 209 | } else { 210 | const ids = []; 211 | for (let key of keys) { 212 | const itemKey = this.plugin.literaturePool.get(key); 213 | const entry = await this.plugin.database.getContentByKey(itemKey); 214 | ids.push(entry.citekey); 215 | } 216 | replaceContent = `\\cite{${ids.join(",")}}`; 217 | } 218 | } 219 | content = content.replace(totalStr, replaceContent); 220 | iter = iter + 1; 221 | if (iter > 1000) break; 222 | } 223 | if (isDev) this.logger.info("获得处理后的导出内容, contents=>", {content}); 224 | await this.plugin.kernelApi.putFile("/temp/convert/pandoc/citation/exportTemp.md", false, new Blob([content])); 225 | await this.plugin.kernelApi.pandoc("citation", [ 226 | "./exportTemp.md", 227 | "-o", "exportTemp.tex", 228 | "--wrap=none" 229 | ]) 230 | res = await this.plugin.kernelApi.getFile("/temp/convert/pandoc/citation/exportTemp.tex", "any") as any; 231 | const file = await (new Response(((res as any).body as ReadableStream))).blob() 232 | // 下载 233 | const url = window.URL.createObjectURL(file); 234 | const a = document.createElement('a'); 235 | a.href = url; 236 | a.download = `${fileTitle}.tex`; 237 | a.click(); 238 | window.URL.revokeObjectURL(url); 239 | } finally { 240 | await this.resetExportConfig(); 241 | } 242 | } 243 | 244 | private async setExportConfig(changedOptions: ExportOption) { 245 | this.userConfig = (window as unknown as {siyuan: any}).siyuan.config.export; 246 | if (isDev) this.logger.info("读取到用户设置数据, config=>", this.userConfig); 247 | // 拷贝设置然后修改 248 | const setConfig = Object.assign({}, this.userConfig); 249 | Object.keys(changedOptions).forEach(key => { 250 | setConfig[key] = (changedOptions as Record)[key]; 251 | }); 252 | await this.plugin.kernelApi.setExport(setConfig); 253 | } 254 | 255 | private async resetExportConfig() { 256 | await this.plugin.kernelApi.setExport(this.userConfig); 257 | if (isDev) this.logger.info("成功还原用户设置数据"); 258 | } 259 | 260 | private getNeighborCites(content: string, citeBlockIDs: string[]): null | {totalStr: string, links: string[], keys: string[]} { 261 | const refReg = /\[\\exportRef\{(.*?)\}\]\(siyuan:\/\/blocks\/(.*?)\)/; 262 | const match = content.match(refReg); 263 | if (!match) { return null; } 264 | else if (citeBlockIDs.indexOf(match[2]) != -1 && (match.index == 1 || match.index == 0)) { 265 | const following = this.getNeighborCites(content.slice(match.index + match[0].length), citeBlockIDs); 266 | if (!following) return { 267 | totalStr: content.slice(0, match.index + match[0].length), 268 | links: [match[1]], 269 | keys: [match[2]] 270 | } 271 | else return { 272 | totalStr: content.slice(0, match.index + match[0].length) + following.totalStr, 273 | links: [match[1], ...following.links], 274 | keys: [match[2], ...following.keys] 275 | } 276 | } else { return null; } 277 | } 278 | } -------------------------------------------------------------------------------- /src/frontEnd/misc/BlockIcon.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 77 | 78 | 82 | onClick(e, element, props)} 88 | data-type={type} 89 | aria-label={ariaLabel} 90 | class:fn__none={none} 91 | class:block__icon--show={show} 92 | class:toolbar__item--active={active} 93 | class:toolbar__item--disabled={disabled} 94 | class:b3-tooltips={tooltipsDirection !== TooltipsDirection.none} 95 | class="block__icon fn__flex-center {tooltipsDirection}" 96 | > 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/frontEnd/misc/Svg.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 33 | 34 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/frontEnd/misc/SvgArrow.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | 26 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/frontEnd/misc/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2023 Zuoqiu Yingyi 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | import type { Writable } from "svelte/store"; 19 | import type { TooltipsDirection } from "./tooltips"; 20 | 21 | /* 状态变量 */ 22 | export interface IBlockIconStatus { 23 | icon: string; // svg 图标引用 ID 24 | tag?: string; // 元素 HTML 标签名称 25 | none?: boolean; // 是否隐藏 .fn__none (display: none) 26 | show?: boolean; // 是否显示 .block__icon--show (opacity: 1) 27 | active?: boolean; // 是否激活 .toolbar__item--active 28 | disabled?: boolean; // 是否禁用 .toolbar__item--disabled 29 | type?: string; // data-type 30 | ariaLabel?: string; // 提示标签内容 aria-label 31 | tooltipsDirection?: TooltipsDirection; // 提示标签方向 32 | } 33 | 34 | /* 响应式状态变量 */ 35 | export type IBlockIconStores = { 36 | [P in keyof IBlockIconStatus]?: Writable 37 | } 38 | 39 | /* 组件属性 */ 40 | export interface IBlockIconProps extends IBlockIconStatus { 41 | onClick?: ( 42 | e: MouseEvent, 43 | element: HTMLElement, 44 | props: IBlockIconStores, 45 | ) => any; // 按钮点击回调函数 46 | } 47 | -------------------------------------------------------------------------------- /src/frontEnd/misc/tooltips.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2023 Zuoqiu Yingyi 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | export enum TooltipsDirection { 19 | none = "", 20 | 21 | n = "b3-tooltips__n", 22 | ne = "b3-tooltips__ne", 23 | e = "b3-tooltips__e", 24 | se = "b3-tooltips__se", 25 | s = "b3-tooltips__s", 26 | sw = "b3-tooltips__sw", 27 | w = "b3-tooltips__w", 28 | nw = "b3-tooltips__nw", 29 | } 30 | -------------------------------------------------------------------------------- /src/frontEnd/searchDialog/dialogComponent.svelte: -------------------------------------------------------------------------------- 1 | 121 | 122 | 175 | 176 | 177 | 178 | 179 | 180 | 189 | 190 | 191 | {#each selectedList as sItem, sIndex} 192 | 193 | {sItem.author} 194 | {sItem.year} 195 | × 196 | 197 | {/each} 198 | 199 | 200 | 201 | 202 | {#each highlightedRes as resItem, index} 203 | 204 | 205 | 206 | 209 | {@html resItem.title} 210 | {@html resItem.year + "\t | \t" + resItem.authorString} 211 | 212 | 213 | {/each} 214 | 215 | -------------------------------------------------------------------------------- /src/frontEnd/searchDialog/searchDialog.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog 3 | } from "siyuan"; 4 | import SiYuanPluginCitation from "../../index"; 5 | import { type ILogger, createLogger } from "../../utils/simple-logger"; 6 | import { isDev } from "../../utils/constants"; 7 | import SearchDialogComponent from "./dialogComponent.svelte"; 8 | import { mount, unmount } from "svelte"; 9 | 10 | export interface SearchRes { 11 | item: { 12 | key: string 13 | }, 14 | itemContent: { 15 | title: string, 16 | year: string, 17 | authorString: string 18 | }, 19 | matches: Match[] 20 | } 21 | 22 | export interface Match { 23 | value: string, 24 | indices: number[][] 25 | } 26 | 27 | 28 | export class SearchDialog { 29 | private searchDialog!: Dialog; 30 | private logger: ILogger; 31 | 32 | constructor (public plugin: SiYuanPluginCitation) { 33 | this.logger = createLogger("search dialog"); 34 | } 35 | 36 | public showSearching( 37 | search: (pattern: string) => any, 38 | onSelection: (keys: string[]) => void, 39 | selectedList: {key: string, author: string, year: string}[] = [] 40 | ) { 41 | if (isDev) this.logger.info("打开搜索界面"); 42 | 43 | const id = `dialog-search-${Date.now()}`; 44 | this.searchDialog = new Dialog({ 45 | content: ``, 46 | width: this.plugin.isMobile ? "92vw" : "640px", 47 | height: "40vh", 48 | // destroyCallback(){if (component) unmount(component)} 49 | }); 50 | 51 | const component = mount(SearchDialogComponent, { 52 | target: this.searchDialog.element.querySelector(`#${id}`)!, 53 | props: { 54 | onSelection, 55 | search, 56 | selectedList, 57 | refresh: () => { 58 | const table = this.searchDialog.element.getElementsByTagName("ul").item(0)!; 59 | table.scrollTop = 0; 60 | }, 61 | confirm: () => { 62 | if (isDev) this.logger.info("关闭搜索界面"); 63 | return this.searchDialog.destroy(); 64 | }, 65 | select: (selector: number) => { 66 | const focusElement = this.searchDialog.element.getElementsByTagName("li").item(selector)!; 67 | focusElement.scrollIntoView({behavior: "smooth", block: "center"}); 68 | } 69 | } 70 | }) 71 | 72 | // const component = new SearchDialogComponent({ 73 | // target: this.searchDialog.element.querySelector(`#${id}`), 74 | // props: { 75 | // onSelection, 76 | // search, 77 | // selectedList 78 | // } 79 | // }); 80 | 81 | this.searchDialog.element.querySelector(".b3-dialog__header")!.className = "resize__move b3-dialog__header fn__none-custom"; 82 | const input = this.searchDialog.element.querySelector("#pattern-input") as HTMLInputElement; 83 | input.focus(); 84 | 85 | // component.$on("refresh", () => { 86 | // const table = this.searchDialog.element.getElementsByTagName("ul").item(0); 87 | // table.scrollTop = 0; 88 | // }); 89 | 90 | // component.$on("select", e => { 91 | // const selector = e.detail.selector; 92 | // const focusElement = this.searchDialog.element.getElementsByTagName("li").item(selector); 93 | // focusElement.scrollIntoView({behavior: "smooth", block: "center"}); 94 | // }); 95 | 96 | // component.$on("confirm", ()=> { 97 | // if (isDev) this.logger.info("关闭搜索界面"); 98 | // return this.searchDialog.destroy(); 99 | // }); 100 | } 101 | } -------------------------------------------------------------------------------- /src/frontEnd/settingTab/event.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2023 Zuoqiu Yingyi 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | import type { IMouseStatus } from "../../utils/shortcut"; 19 | import type { TabKey } from "./tab"; 20 | 21 | export interface ITabEvent { 22 | changed: { 23 | key: TabKey; 24 | }; 25 | } 26 | 27 | export interface IPanelsEvent { 28 | changed: { 29 | key: TabKey; 30 | }; 31 | "search-changed": { 32 | value: string; 33 | }; 34 | } 35 | 36 | export interface IShortcutEvent { 37 | changed: { 38 | shortcut: IMouseStatus; 39 | }; 40 | } 41 | 42 | export interface IInputEvent { 43 | clicked: { 44 | event: MouseEvent; 45 | }; 46 | changed: { 47 | key: string; 48 | value: any; 49 | event: Event; 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/frontEnd/settingTab/item/Card.svelte: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 23 | 24 | 25 | 28 | {@html title} 29 | 30 | {@html text} 31 | 32 | 33 | 37 | 41 | 42 | -------------------------------------------------------------------------------- /src/frontEnd/settingTab/item/Group.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 28 | 29 | 30 | {@html title} 31 | 32 | {@render children?.()} 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/frontEnd/settingTab/item/Input.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 75 | 76 | {#if type === ItemType.checkbox} 77 | 78 | 87 | {:else if type === ItemType.text} 88 | 89 | 99 | {:else if type === ItemType.number} 100 | 101 | 115 | {:else if type === ItemType.slider} 116 | 117 | 129 | {:else if type === ItemType.button} 130 | 131 | 139 | {settingValue} 140 | 141 | {:else if type === ItemType.select} 142 | 143 | 152 | {#each options as option (option.key)} 153 | {option.text} 154 | {/each} 155 | 156 | {:else if type === ItemType.textarea} 157 | 158 | 0 ? `${height}px` : undefined} 164 | {placeholder} 165 | style:resize={block ? "vertical": "auto"} 166 | rows={rows > 0 ? rows : undefined} 167 | bind:value={settingValue} 168 | onchange={changed} 169 | > 170 | {/if} 171 | 172 | 184 | -------------------------------------------------------------------------------- /src/frontEnd/settingTab/item/Item.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 22 | 23 | 38 | 39 | 40 | 44 | {@render titleSlot?.()} 45 | {@html title} 46 | 47 | {@render textSlot?.()} 48 | {@html text} 49 | 50 | 51 | {#if block} 52 | 53 | {@render input?.()} 54 | {/if} 55 | 56 | 57 | {#if !block} 58 | 59 | {@render input?.()} 60 | {/if} 61 | 62 | 63 | 68 | -------------------------------------------------------------------------------- /src/frontEnd/settingTab/item/MiniItem.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 41 | 42 | 47 | {@render icon?.()} 48 | 49 | 50 | 51 | 52 | {@render title?.()} 53 | 54 | 55 | 56 | 57 | {@render input?.()} 58 | 59 | 60 | 72 | -------------------------------------------------------------------------------- /src/frontEnd/settingTab/item/item.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2023 Zuoqiu Yingyi 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | export enum ItemType { 19 | checkbox, 20 | text, 21 | number, 22 | slider, 23 | button, 24 | select, 25 | textarea, 26 | } 27 | 28 | export interface ILimits { 29 | min: number; 30 | max: number; 31 | step: number; 32 | } 33 | 34 | export interface IOption { 35 | key: string | number, 36 | text: string, 37 | } 38 | 39 | export type IOptions = IOption[]; 40 | -------------------------------------------------------------------------------- /src/frontEnd/settingTab/panel/Panel.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 35 | 36 | 42 | {@render children?.()} 43 | 44 | -------------------------------------------------------------------------------- /src/frontEnd/settingTab/panel/Panels.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 56 | 57 | 58 | 59 | 60 | {#if searchEnable} 61 | 62 | 63 | 67 | 73 | 74 | {/if} 75 | 76 | {#each panels as panel (panel.key)} 77 | 78 | changed(panel.key)} 81 | onkeyup={() => changed(panel.key)} 82 | data-name={panel.name} 83 | class:b3-list-item--focus={panel.key === focus} 84 | class="b3-list-item" 85 | > 86 | 90 | 91 | {@html panel.text} 92 | 93 | 94 | {/each} 95 | 96 | 97 | 98 | 99 | {#if children}{@render children({ focus, })}{:else}Container{/if} 100 | 101 | 102 | 103 | 108 | -------------------------------------------------------------------------------- /src/frontEnd/settingTab/settingTab.ts: -------------------------------------------------------------------------------- 1 | import { Dialog } from "siyuan"; 2 | import { mount, unmount } from "svelte"; 3 | 4 | import { createLogger, type ILogger } from "../../utils/simple-logger"; 5 | import type SiYuanPluginCitation from "../../index"; 6 | import SettingTabComponent from "./settingTabComponent.svelte"; 7 | import { isDev } from "../../utils/constants"; 8 | import { type DatabaseType } from "../../database/database"; 9 | 10 | export class SettingTab { 11 | private logger: ILogger; 12 | 13 | constructor(private plugin: SiYuanPluginCitation) { 14 | this.logger = createLogger("setting tab"); 15 | } 16 | 17 | public openSetting() { 18 | const id = `dialog-setting-${Date.now()}`; 19 | const settingTab = new Dialog({ 20 | content: ``, 21 | width: this.plugin.isMobile ? "92vw" : "850px", 22 | height: "70vh", 23 | destroyCallback: () => { if (component) unmount(component) } 24 | }); 25 | 26 | const props = { 27 | plugin: this.plugin, 28 | logger: this.logger, 29 | reloadDatabase: async (database: string) => { 30 | if (isDev) this.logger.info("reload database"); 31 | await this.plugin.database.buildDatabase(database as DatabaseType); 32 | return await this.plugin.reference.checkRefDirExist(); 33 | }, 34 | refreshLiteratureNoteTitle: async (titleTemplate: string) => { 35 | if (isDev) this.logger.info("refresh literature note title"); 36 | return this.plugin.reference.refreshLiteratureNoteTitles(titleTemplate); 37 | } 38 | }; 39 | const component = mount(SettingTabComponent, { 40 | target: settingTab.element.querySelector(`#${id}`)!, 41 | props, 42 | }) 43 | } 44 | } -------------------------------------------------------------------------------- /src/frontEnd/settingTab/specified/Shortcut.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 108 | 109 | 110 | {#if displayCtrlKey} 111 | 115 | 119 | {#snippet title()} 120 | Ctrl 121 | {/snippet} 122 | {#snippet input()} 123 | 132 | {/snippet} 133 | 134 | {/if} 135 | {#if displayShiftKey} 136 | 140 | 144 | {#snippet title()} 145 | Shift 146 | {/snippet} 147 | {#snippet input()} 148 | 157 | {/snippet} 158 | 159 | {/if} 160 | {#if displayAltKey} 161 | 165 | 169 | {#snippet title()} 170 | Alt 171 | {/snippet} 172 | {#snippet input()} 173 | 182 | {/snippet} 183 | 184 | {/if} 185 | {#if displayMetaKey} 186 | 190 | 194 | {#snippet title()} 195 | Meta 196 | {/snippet} 197 | {#snippet input()} 198 | 207 | {/snippet} 208 | 209 | {/if} 210 | {#if displayMouseButton} 211 | 215 | 219 | {#snippet title()} 220 | {mouseButtonTitle} 221 | {/snippet} 222 | {#snippet input()} 223 | 233 | {/snippet} 234 | 235 | {/if} 236 | {#if displayMouseEvent} 237 | 241 | 245 | {#snippet title()} 246 | {mouseEventTitle} 247 | {/snippet} 248 | {#snippet input()} 249 | 259 | {/snippet} 260 | 261 | {/if} 262 | 263 | -------------------------------------------------------------------------------- /src/frontEnd/settingTab/tab.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2023 Zuoqiu Yingyi 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | import type { DatabaseType } from "../../database/database"; 19 | 20 | export type TabKey = string | number; 21 | 22 | export interface ITab { 23 | key: TabKey; 24 | text: string; 25 | name?: string; 26 | icon?: string; 27 | } 28 | 29 | export interface IPanel { 30 | key: TabKey; 31 | text: string; 32 | name?: string; 33 | icon?: string; 34 | supportDatabase?: DatabaseType[]; 35 | } 36 | -------------------------------------------------------------------------------- /src/frontEnd/settingTab/tab/Tab.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 48 | 49 | 50 | 51 | 58 | 59 | 60 | {#if icon} 61 | 62 | {@render icon?.()} 63 | 64 | {/if} 65 | 66 | 67 | {#if text}{@render text()}{:else}text{/if} 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/frontEnd/settingTab/tab/Tabs.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 38 | 39 | 43 | 44 | 45 | {#each tabs as tab (tab.key)} 46 | 47 | 53 | {#snippet icon()} 54 | 55 | {#if tab.icon.startsWith("#")} 56 | 57 | {:else} 58 | {@html tab.icon} 59 | {/if} 60 | 61 | {/snippet} 62 | {#snippet text()} 63 | 64 | {@html tab.text} 65 | 66 | {/snippet} 67 | 68 | {/each} 69 | 70 | 71 | 72 | 73 | 74 | {#if children}{@render children({ focus, })}{:else}Container{/if} 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/i18n/en_US.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginName": "Citation", 3 | "cancel": "Cancel", 4 | "save": "Save", 5 | "byeMenu": "Bye, Menu!", 6 | "helloPlugin": "Hello, Plugin!", 7 | "byePlugin": "Bye, Plugin!", 8 | "showDialog": "Show dialog", 9 | "removedData": "Data deleted", 10 | "prefix": "Ciation: ", 11 | "citeLink": "citation link", 12 | "notes": "notes", 13 | "addCitation": "Add citation", 14 | "addNotes": "Add notes of literature", 15 | "reloadDatabase": "Reload the database", 16 | "refreshLiteratureNotesTitle": "Refresh all literature note document titles", 17 | "copyCiteLink": "Copy citation", 18 | "copyNotes": "Copy notes", 19 | "addSelectedItems": "Cite Selected Literature in Zotero", 20 | "refreshLiteratureNotesContents": "Refresh all literature notes", 21 | "refreshSingleLiteratureNote": "Refresh literature note", 22 | "test": "test", 23 | "settingTab": { 24 | "settingTabTitle": "Plugin Version: ${version}", 25 | "settingTabDescription": "If you have any good ideas or find bugs, please feel free to submit an issue on GitHub or email me at my email.", 26 | "basic": { 27 | "title": "Basic Settings", 28 | "notebookSelectorTitle": "Notebook", 29 | "notebookSelectorDescription": "Select the notebook where the literature library is placed.", 30 | "referencePathInputTitle": "Literature Library Path", 31 | "referencePathInputDescription": "Set the path for storing literature content in the library, starting with '/' and separating subdirectories, for example, '/Assets/References'.", 32 | "databaseSelectorTitle": "Database Type", 33 | "databaseSelectorDescription": "Select the type of database to use.", 34 | "UseItemKeySwitchTitle": "Use ItemKey as Index", 35 | "UseItemKeySwitchDescription": "Use the unique key of literature in Zotero as the index. When enabled, it can work without relying on the better bibtex plugin.Only available when using the debug-bridge plugin.", 36 | "AutoReplaceSwitchTitle": "Automatic Replacement of Zotero Links with Citation Links", 37 | "AutoReplaceSwitchDescription": "If a block of text contains links pointing to Zotero items (including annotations copied from Zotero's PDF viewer or selected areas dragged into SiYuan), these links will be automatically replaced with citations of the respective items. This feature is intended for users who predominantly use this plugin.", 38 | "DeleteUserDataWithoutConfirmSwitchTitle": "Disable User Data Safety Prompt", 39 | "DeleteUserDataWithoutConfirmSwitchDescription": "When enabled, if user data would be deleted while updating literature content, the confirmation dialog will not appear.❗❗❗Do not enable this unless you have specific requirements", 40 | "reloadBtnTitle": "Reload Database", 41 | "reloadBtnDescription": "Reload the database, including checking if the literature library path exists.", 42 | "reloadBtnText": "Reload", 43 | "deleteDataBtnTitle": "Delete Data", 44 | "deleteDataBtnDescription": "Delete all setting data of this plugin (only including data displayed on the setting panel).", 45 | "confirmRemove": "Confirm to delete the setting data of ${name}?", 46 | "deleteDataBtnText": "Delete" 47 | }, 48 | "templates": { 49 | "title": "Template Settings", 50 | "citeLink": { 51 | "title": "Citation Links", 52 | "useDefaultCiteTypeTitle": "默认使用第一种引用类型", 53 | "useDefaultCiteTypeDescription": "在插入引用时直接使用第一种引用类型,不单独弹窗选择引用类型(在引用的右键菜单中可以手动切换引用类型)", 54 | "citeTypeCardTitle": "Citation Type List", 55 | "citeTypeCardSet": "Settings", 56 | "citeTypeCardDelete": "Delete", 57 | "CustomCiteTextSwitchTitle": "Custom Cite", 58 | "CustomCiteTextSwitchDescription": "Fully customize the format of citations. The content generated by the citation link template will no longer be anchor text, but will be directly inserted into the document.⚠️Note: After enabling this switch, please make sure your template includes \"...(( {{citeFileID}} \"...\"))...\" or \"...(( {{citeFileID}} '...'))...\". Otherwise, the generated links will not be able to reference properly.The regular expression for finding citation links is /\\(\\((.*?)\\\"(.*?)\\\"\\)\\)/g and /\\(\\((.*?)\\'(.*?)\\'\\)\\)/g. Please use this to handle your template format.", 59 | "linkTempInputTitle": "Citation Template", 60 | "linkTempInputDescription": "Set the template for adding citation links to the document.", 61 | "useDynamicRefLinkSwitchTitle": "Use Dynamic Anchor Text for Citation Links", 62 | "useDynamicRefLinkSwitchDescription": "Enable citation links with dynamic anchor text. These links won't have a prefixed anchor text. When inserted, they will also refresh the literature note document names based on the citation link template. (Not recommended, dynamic anchor text can be less stable)", 63 | "citeNameTitle": "Citation Type Name", 64 | "citeNameDescription": "Add the type of citation to distinguish between different templates.", 65 | "nameTempInputTitle": "Literature Note Document Naming Template", 66 | "nameTempInputDescription": "Set the template for updating the names of literature note documents when inserting or refreshing citations. Only effective when both 'Custom Citation' and 'Use Dynamic Anchor Text for Citation Links' are enabled.", 67 | "shortAuthorLimitTitle": "Short Author Limit", 68 | "shortAuthorLimitDescription": "Limit the maximum number of authors displayed in the shortAuthor variable (use 'et al.' for additional authors).", 69 | "multiCiteTitle": "Multiple Citation Settings", 70 | "multiCiteDescription": "Prefix, connector, and suffix for referencing multiple literature sources at the same time." 71 | }, 72 | "literatureNote": { 73 | "title": "Literature Content", 74 | "titleTemplateInputTitle": "Literature Note Document Title Template", 75 | "titleTemplateInputDescription": "Set the title template for generating literature content documents (user modifications will not be refreshed, titles can be regenerated using \"Refresh All Literature Note Titles\").", 76 | "refreshLiteratureNoteBtnTitle": "Refresh All Literature Note Titles", 77 | "refreshLiteratureNoteBtnDesciption": "Refresh the titles of all documents in the literature library. It will also update the document names (after switching index variables or updating versions).", 78 | "refreshLiteratureNoteBtnText": "Refresh", 79 | "moveImgToAssetsTitle": "Move Images to Siyuan Workspace", 80 | "moveImgToAssetsDescription": "When importing image-type annotations, move the images to the Assets directory under the Siyuan workspace, otherwise the images cannot be displayed.", 81 | "noteTempTexareaTitle": "Literature Note Template", 82 | "noteTempTexareaDescription": "Set the content template for storing literature documents in the literature library." 83 | }, 84 | "userData": { 85 | "title": "User Data", 86 | "titleUserDataInput": "Custom 'User Data' headers", 87 | "titleUserDataInputDescription": "The default is 'User Data', which can be customized", 88 | "userDataTemplatePathTitle": "用户数据模板路径", 89 | "userDataTemplatePathDescription": "使用思源自带的模板自动生成用户数据区域,在输入框中输入该模板的路径,以/data/开头。", 90 | "useWholeDocAsUserDataTitle": "使用整个文档作为用户数据区域", 91 | "useWholeDocAsUserDataDescription": "将整个文档作为用户数据区域使用,新建之后不再更新,建议配合用户数据模板和数据库功能使用。", 92 | "attrViewBlockInput": "Database block id", 93 | "attrViewBlockDescription": "The id of the database that needed to be update during citation", 94 | "attrViewTemplateInput": "Database attribute template", 95 | "attrViewTemplateDescription": "Add the content document of the literature to the specified database during the citation, and adjust the database properties according to this template. The following will dynamically display the corresponding relationship between the database block ID and column names:" 96 | } 97 | }, 98 | "debug_bridge": { 99 | "title": "debug-bridge Plugin", 100 | "zotero": { 101 | "title": "Zotero Content Template", 102 | "notAbleTitle": "Cannot Insert Content into Zotero", 103 | "notAbleDescription": "The database type used does not support inserting content into Zotero.", 104 | "zoteroLinkTitleTemplateTitle": "Insert Zotero Backlink Title Template", 105 | "zoteroLinkTitleTemplateDescription": "Set the title template for inserting SiYuan backlinks to Zotero corresponding items. Leave it blank if you do not want to add backlinks. The variable \"{{siyuanLink}}\" represents the link itself.", 106 | "zoteroTagTemplateTitle": "Insert Zotero Tag Template", 107 | "zoteroTagTemplateDescription": "Set the template for adding tags to the corresponding item when inserting literature. Leave it blank for no default tags, use commas to insert multiple tags." 108 | }, 109 | "plugin": { 110 | "title": "Plugin Settings", 111 | "dbPasswordInputTitle": "Debug-Bridge Plugin Password", 112 | "dbPasswordInputDescription": "The password set according to the tutorial when setting up the debug-bridge plugin. The default password is \"CTT\" in the tutorial, and this plugin also defaults to the same password.", 113 | "searchDialogSelectorTitle": "Search Panel for Inserting Literature", 114 | "searchDialogSelectorDescription": "Select the search panel used when inserting literature: \"SiYuan\" is based on SiYuan style and has faster and more stable searching speed; \"Zotero\" is the classic view of Zotero, which can be set in the Zotero software." 115 | } 116 | } 117 | }, 118 | "menuItems": { 119 | "refreshCitation": "Refresh citation links", 120 | "refreshSingleLiteratureNote": "Refresh literature note", 121 | "turnTo": "Turn to", 122 | "export": "Export" 123 | }, 124 | "errors": { 125 | "notebookUnselected": "Notebook Not Selected!", 126 | "refPathInvalid": "Literature Library Path Does Not Exist! Please create the corresponding path in the document tree.", 127 | "hotKeyUsage": "Please use the shortcut key to execute this command!", 128 | "loadRefFailed": "Failed to Import Literature References", 129 | "zoteroNotRunning": "${type} is not running", 130 | "getLiteratureFailed": "Failed to Get Cited Literature", 131 | "loadLibraryFailed": "Failed to Import Literature Library", 132 | "bbtDisabled": "No 'citekey' field detected. Please check if the better-bibtex plugin is running or switch to using 'itemKey' as the index.", 133 | "wrongDBPassword": "Debug-Bridge Plugin Password Incorrect. Please set the correct password in the settings interface.", 134 | "userDataTemplatePathRenderError": "用户数据模板路径错误,无法渲染模板" 135 | }, 136 | "confirms": { 137 | "updateWithoutUserData": "The \"User Data (# User Data)\" area of literature note document \"${title}\" is not detected (missing citations or citations are invalid). Are you sure you want to update the literature content?" 138 | }, 139 | "notices": { 140 | "loadLibrarySuccess": "Successfully Imported ${size} Literature Data", 141 | "loadRefSuccess": "Successfully Imported ${size} Literature References", 142 | "refreshTitleSuccess": "Successfully Refreshed ${size} Literature Note Document Titles", 143 | "copyContentSuccess": "${type} copied to clipboard", 144 | "changeKey": "Switched Index Successfully. Literature index has been changed to ${keyType}.", 145 | "noSelectedItem": "No literature is selected.", 146 | "refreshLiteratureNoteContents": "Successfully Refreshed ${size} Literature Note.", 147 | "refreshSingleLiteratureNoteSucess": "Successfully Refreshed Note of Literature ${key}." 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/i18n/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginName": "文献引用", 3 | "cancel": "取消", 4 | "save": "保存", 5 | "byeMenu": "再见,菜单!", 6 | "helloPlugin": "你好,插件!", 7 | "byePlugin": "再见,插件!", 8 | "showDialog": "弹出一个对话框", 9 | "removedData": "数据已删除", 10 | "prefix": "文献引用:", 11 | "citeLink": "引用链接", 12 | "notes": "笔记", 13 | "addCitation": "插入文献引用", 14 | "addNotes": "插入文献笔记", 15 | "reloadDatabase": "重新载入数据库", 16 | "refreshLiteratureNotesTitle": "刷新所有文献内容文档标题", 17 | "copyCiteLink": "复制文献引用", 18 | "copyNotes": "复制文献笔记", 19 | "addSelectedItems": "引用Zotero中选中的文献", 20 | "refreshSingleLiteratureNote": "刷新文献内容", 21 | "refreshLiteratureNotesContents": "刷新所有文献内容", 22 | "test": "测试", 23 | "settingTab": { 24 | "settingTabTitle": "插件版本:${version}", 25 | "settingTabDescription": "如果有好的想法或者发现了bug,欢迎到github上提issue或者发邮件到我的邮箱", 26 | "basic": { 27 | "title": "基础设置", 28 | "notebookSelectorTitle": "笔记本", 29 | "notebookSelectorDescription": "选择文献库放置的笔记本", 30 | "referencePathInputTitle": "文献库路径", 31 | "referencePathInputDescription": "放置存放文献内容的文献库文档路径,以'/'开头和分隔子目录,例如'/Assets/References'", 32 | "databaseSelectorTitle": "数据库类型", 33 | "databaseSelectorDescription": "选择使用的数据库类型", 34 | "UseItemKeySwitchTitle": "使用itemKey作为索引", 35 | "UseItemKeySwitchDescription": "使用文献在Zotero中的唯一key作为索引,开启后可以不依赖better bibtex插件仅使用debug-bridge插件时可以开启", 36 | "AutoReplaceSwitchTitle": "自动替换指向Zotero链接为引用链接", 37 | "AutoReplaceSwitchDescription": "如果编辑包含指向Zotero条目的链接的块(包括从Zotero的pdf浏览器复制批注/拖动选区到思源),自动将链接替换为该条目的引用。适用于主要使用本插件的用户。", 38 | "DeleteUserDataWithoutConfirmSwitchTitle": "关闭用户数据安全提示", 39 | "DeleteUserDataWithoutConfirmSwitchDescription": "若开启,在更新文献内容时如果会删除用户数据,则不会弹出确认框。❗❗❗如果没有特殊需求请勿开启", 40 | "reloadBtnTitle": "重新载入数据库", 41 | "reloadBtnDescription": "重新载入数据库,包括检查文献库路径是否存在", 42 | "reloadBtnText": "重载", 43 | "deleteDataBtnTitle": "删除数据", 44 | "deleteDataBtnDescription": "删除本插件的所有设置数据(仅包括设置面板上显示的数据)", 45 | "confirmRemove": "确认删除 ${name} 的设置数据?", 46 | "deleteDataBtnText": "删除" 47 | }, 48 | "templates": { 49 | "title": "模板设置", 50 | "citeLink": { 51 | "title": "引用链接", 52 | "useDefaultCiteTypeTitle": "默认使用第一种引用类型", 53 | "useDefaultCiteTypeDescription": "在插入引用时直接使用第一种引用类型,不单独弹窗选择引用类型(在引用的右键菜单中可以手动切换引用类型)", 54 | "citeTypeCardTitle": "引用类型列表", 55 | "citeTypeCardSet": "设置", 56 | "citeTypeCardDelete": "删除", 57 | "CustomCiteTextSwitchTitle": "自定义引用", 58 | "CustomCiteTextSwitchDescription": "完全自定义引用的格式,引用链接模板生成的内容不再为锚文本,而是将直接插入文档⚠️注意:开关开启后请确保你的模板中包含\"...(( {{citeFileID}} \"...\"))...\"或者\"...(( {{citeFileID}} '...'))...\"否则生成的链接将无法引用查找引用链接的正则表达式为/\\(\\((.*?)\\\"(.*?)\\\"\\)\\)/g和/\\(\\((.*?)\\'(.*?)\\'\\)\\)/g,请据此把握好您的模板格式", 59 | "linkTempInputTitle": "引用模板", 60 | "linkTempInputDescription": "设置添加在文档中的引用链接的模板", 61 | "useDynamicRefLinkSwitchTitle": "引用链接使用动态锚文本", 62 | "useDynamicRefLinkSwitchDescription": "使用动态锚文本的引用链接,不会前置锚文本,插入的同时会根据引用链接模板刷新文献内容文档命名。(不建议开启,动态锚文本较为不稳定)", 63 | "citeNameTitle": "引用类型名称", 64 | "citeNameDescription": "添加引用的类型,在不同的模板之间进行区分", 65 | "nameTempInputTitle": "文献内容文档命名模板", 66 | "nameTempInputDescription": "设置插入/刷新引用时更新文献内容文档命名的模板,仅在同时开启“自定义引用”和“引用链接中使用动态锚文本”的时候有效", 67 | "shortAuthorLimitTitle": "shortAuthor变量限制", 68 | "shortAuthorLimitDescription": "限制在shortAuthor变量中最多展示的作者数(多的用et al.表示)", 69 | "multiCiteTitle": "多文献引用设置", 70 | "multiCiteDescription": "同时引用多个文献时,整体的前缀-连接符-后缀" 71 | }, 72 | "literatureNote": { 73 | "title": "文献内容", 74 | "titleTemplateInputTitle": "文献内容文档标题模板", 75 | "titleTemplateInputDescription": "设置文献内容文档的生成标题(用户修改后不会刷新,可以通过“刷新所有文献内容文档标题”重新生成标题)", 76 | "refreshLiteratureNoteBtnTitle": "刷新所有文献内容文档标题", 77 | "refreshLiteratureNoteBtnDesciption": "刷新文献库中所有文档的标题,同时也会更新文档的命名(切换索引变量或者更新版本之后)", 78 | "refreshLiteratureNoteBtnText": "刷新", 79 | "moveImgToAssetsTitle": "移动图片到思源工作空间", 80 | "moveImgToAssetsDescription": "在导入图片类型的批注时,将图片移动到思源工作空间的Assets目录下,否则无法显示图片", 81 | "noteTempTexareaTitle": "文献内容模板", 82 | "noteTempTexareaDescription": "设置在文献库中储存的文献文档的内容模板" 83 | }, 84 | "userData": { 85 | "title": "数据", 86 | "titleUserDataInput": "自定义用户数据标题", 87 | "titleUserDataInputDescription": "默认是 User Data,可以自定义", 88 | "userDataTemplatePathTitle": "用户数据模板路径", 89 | "userDataTemplatePathDescription": "使用思源自带的模板自动生成用户数据区域,在输入框中输入该模板的路径,以/data/开头。", 90 | "useWholeDocAsUserDataTitle": "使用整个文档作为用户数据区域", 91 | "useWholeDocAsUserDataDescription": "将整个文档作为用户数据区域使用,新建之后不再更新,建议配合用户数据模板和数据库功能使用。", 92 | "attrViewBlockInput": "数据库块id", 93 | "attrViewBlockDescription": "需要更新的数据库所在块id", 94 | "attrViewTemplateInput": "数据库属性模板", 95 | "attrViewTemplateDescription": "在引用时将文献内容文档添加进指定数据库,并按照该模板对数据库属性进行幅值,以下将动态展示数据库块的id和列名称的对应关系:" 96 | } 97 | }, 98 | "debug_bridge": { 99 | "title": "debug-bridge插件", 100 | "zotero": { 101 | "title": "Zotero内容模板", 102 | "notAbleTitle": "无法向Zotero中插入内容", 103 | "notAbleDescription": "当前使用的数据库类型不支持向Zotero中插入内容", 104 | "zoteroLinkTitleTemplateTitle": "插入Zotero反向链接标题模板", 105 | "zoteroLinkTitleTemplateDescription": "设置当插入文献的同时插入到Zotero对应条目下的思源反链的标题模板,置空则不会添加反链,变量\"{{siyuanLink}}\"代表链接本身", 106 | "zoteroTagTemplateTitle": "插入Zotero标签模板", 107 | "zoteroTagTemplateDescription": "设置当插入文献的同时添加到对应条目的标签的模板,置空则默认不添加标签,插入多个标签用逗号隔开" 108 | }, 109 | "plugin": { 110 | "title": "插件设置", 111 | "dbPasswordInputTitle": "debug-bridge插件密码", 112 | "dbPasswordInputDescription": "根据教程对debug-bridge插件进行设置时设定的密码,教程中默认为\"CTT\",本插件也默认为此密码", 113 | "searchDialogSelectorTitle": "插入文献时使用的搜索面板", 114 | "searchDialogSelectorDescription": "选择插入文献时使用的搜索面板:\"SiYuan\" 为基于思源样式类型的搜索面板,搜索速度更快并且更稳定;\"Zotero\"为Zotero自带的搜索面板,可以在Zotero软件中设置是否使用经典视图" 115 | } 116 | } 117 | }, 118 | "menuItems": { 119 | "refreshCitation": "刷新引用", 120 | "refreshSingleLiteratureNote": "刷新文献内容", 121 | "turnTo": "转换为", 122 | "export": "导出" 123 | }, 124 | "errors": { 125 | "notebookUnselected": "未选择笔记本!", 126 | "refPathInvalid": "文献库路径不存在!请在文档树中新建对应路径的文档。", 127 | "hotKeyUsage": "请使用快捷键执行此命令!", 128 | "loadRefFailed": "文献引用导入失败", 129 | "zoteroNotRunning": "${type}没有在运行", 130 | "getLiteratureFailed": "获得引用文献失败", 131 | "loadLibraryFailed": "文献库导入失败", 132 | "bbtDisabled": "未检索到citekey字段,请检查better-bibtex插件是否运行,或者切换到使用itemKey进行索引", 133 | "wrongDBPassword": "debug-bridge插件密码错误,请在设置界面中设置正确密码", 134 | "userDataTemplatePathRenderError": "用户数据模板路径错误,无法渲染模板" 135 | }, 136 | "confirms": { 137 | "updateWithoutUserData": "文献内容文档\"${title}\"未检测到“用户数据(# User Data)”区域(缺少引用或引用失效),确定要更新文献内容吗?" 138 | }, 139 | "notices": { 140 | "loadLibrarySuccess": "成功导入${size}个文献数据", 141 | "loadRefSuccess": "成功导入${size}个文献引用", 142 | "refreshTitleSuccess": "成功刷新${size}篇文献内容文档标题", 143 | "copyContentSuccess": "${type}已在剪贴板", 144 | "changeKey": "切换索引成功,文献索引已经切换到${keyType}。", 145 | "noSelectedItem": "没有选中的文献", 146 | "refreshLiteratureNoteContentsSuccess": "成功刷新${size}篇文献内容", 147 | "refreshSingleLiteratureNoteSuccess": "成功刷新文献${key}内容" 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | .plugin-citation { 2 | &__custom-tab { 3 | background-color: var(--b3-theme-background); 4 | height: 100%; 5 | width: 100%; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | } 10 | 11 | &__custom-dock { 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | } 16 | } 17 | 18 | .b3-dialog__container { 19 | .b3-dialog { 20 | //弹窗标题 21 | &__header { 22 | // 自定义弹窗去掉无意义标题 23 | &.fn__none-custom { 24 | display: none; 25 | } 26 | } 27 | } 28 | } 29 | .protyle div[data-type="NodeSuperBlock"][custom-annotation-color] { 30 | border: dashed 0.1em gray; 31 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Plugin, 3 | getFrontend, 4 | getBackend, 5 | Protyle, 6 | } from "siyuan"; 7 | 8 | import KernelApi from "./api/kernel-api"; 9 | import { Database, type DatabaseType } from "./database/database"; 10 | import { Reference } from "./references/reference"; 11 | import { InteractionManager } from "./frontEnd/interaction"; 12 | import { ExportManager } from "./export/exportManager"; 13 | import { 14 | isDev, 15 | STORAGE_NAME, 16 | defaultSettingData 17 | } from "./utils/constants"; 18 | import { 19 | createLogger, 20 | type ILogger 21 | } from "./utils/simple-logger"; 22 | 23 | import "./index.scss"; 24 | import { createNoticer, type INoticer } from "./utils/noticer"; 25 | import { changeUpdate } from "./utils/updates"; 26 | import { LiteraturePool } from "./references/pool"; 27 | import type { EventTrigger } from "./events/eventTrigger"; 28 | import { SettingTab } from "./frontEnd/settingTab/settingTab"; 29 | import { NetworkMananger } from "./api/networkManagers"; 30 | 31 | export default class SiYuanPluginCitation extends Plugin { 32 | 33 | public isMobile!: boolean; 34 | public isRefPathExist!: boolean; 35 | 36 | public literaturePool!: LiteraturePool; 37 | 38 | public database!: Database; 39 | public reference!: Reference; 40 | public interactionManager!: InteractionManager; 41 | public exportManager!: ExportManager; 42 | public kernelApi!: KernelApi; 43 | public eventTrigger!: EventTrigger; 44 | public settingTab!: SettingTab; 45 | public networkManager!: NetworkMananger; 46 | 47 | public noticer!: INoticer; 48 | private logger!: ILogger; 49 | 50 | async onload() { 51 | this.logger = createLogger("index"); 52 | this.noticer = createNoticer(); 53 | this.literaturePool = new LiteraturePool(); 54 | this.networkManager = new NetworkMananger(this, 64); 55 | 56 | if (isDev) this.logger.info("插件载入"); 57 | 58 | this.data[STORAGE_NAME] = defaultSettingData; 59 | 60 | const frontEnd = getFrontend(); 61 | this.isMobile = frontEnd === "mobile" || frontEnd === "browser-mobile"; 62 | if (isDev) this.logger.info(`前端: ${getFrontend()}; 后端: ${getBackend()}`); 63 | 64 | if (isDev) this.logger.info("读取本地数据"); 65 | await this.loadData(STORAGE_NAME); 66 | 67 | if (isDev) this.logger.info("获取到储存数据=>", this.data[STORAGE_NAME]); 68 | 69 | await changeUpdate(this); 70 | this.kernelApi = new KernelApi(); 71 | 72 | this.interactionManager = new InteractionManager(this); 73 | await this.interactionManager.customSettingTab().then(setting => { 74 | this.settingTab = setting; 75 | }); 76 | await this.interactionManager.customCommand(); 77 | (await this.interactionManager.customProtyleSlash()).forEach((slash: { filter: string[]; html: string; id: string; callback(protyle: Protyle): void; }) => { 78 | this.protyleSlash.push(slash); 79 | }); 80 | this.interactionManager.eventBusReaction(); 81 | 82 | this.exportManager = new ExportManager(this); 83 | this.database = new Database(this); 84 | return this.database.buildDatabase(this.data[STORAGE_NAME].database as DatabaseType) 85 | .then(() => { 86 | this.reference = new Reference(this); 87 | }); 88 | } 89 | 90 | async onLayoutReady() { 91 | // // @ts-ignore 92 | // this.eventBus.emit("Refresh", {type: "literature note", refreshAll: true, confirmUserData: false}); 93 | } 94 | 95 | openSetting(): void { 96 | this.settingTab.openSetting(); 97 | } 98 | 99 | onunload() { 100 | if (isDev) this.logger.info("插件卸载,plugin=>", this); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/references/cite.ts: -------------------------------------------------------------------------------- 1 | import { type ILogger, createLogger } from "../utils/simple-logger"; 2 | import SiYuanPluginCitation from "../index"; 3 | import { 4 | STORAGE_NAME, 5 | citeLinkDynamic, 6 | citeLinkStatic, 7 | isDev, 8 | refRegDynamic, 9 | refRegStatic } from "../utils/constants"; 10 | import { generateFromTemplate } from "../utils/templates"; 11 | 12 | export class Cite { 13 | plugin: SiYuanPluginCitation; 14 | private logger: ILogger; 15 | 16 | constructor(plugin: SiYuanPluginCitation) { 17 | this.plugin = plugin; 18 | this.logger = createLogger("cite"); 19 | 20 | } 21 | 22 | public async generateCiteLink(key: string, index: number, typeSetting: any, onlyLink=false) { 23 | const linkTemplate = typeSetting.linkTemplate as string; 24 | const useDynamicRefLink = typeSetting.useDynamicRefLink as boolean; 25 | const shortAuthorLimit = typeSetting.shortAuthorLimit as number; 26 | let template = ""; 27 | if (onlyLink) { 28 | const linkReg = useDynamicRefLink ? refRegDynamic : refRegStatic; 29 | const matchRes = linkTemplate.matchAll(linkReg); 30 | let modifiedTemplate = ""; 31 | for (const match of matchRes) { 32 | if (match[1].indexOf("{{citeFileID}}") != -1) { 33 | modifiedTemplate = match[2]; 34 | } 35 | } 36 | if (isDev) this.logger.info("仅包含链接的模板 =>", modifiedTemplate); 37 | template = modifiedTemplate; 38 | } else { 39 | template = linkTemplate; 40 | } 41 | const entry = await this.plugin.database.getContentByKey(key, shortAuthorLimit); 42 | if (!entry) { 43 | if (isDev) this.logger.error("找不到文献数据", {key, blockID: this.plugin.literaturePool.get(key)}); 44 | this.plugin.noticer.error((this.plugin.i18n.errors as any).getLiteratureFailed); 45 | return ""; 46 | } 47 | if (isDev) this.logger.info("仅包含链接的模板 =>", {index, id: this.plugin.literaturePool.get(key)}); 48 | return generateFromTemplate(template, { 49 | index, 50 | citeFileID: this.plugin.literaturePool.get(key), 51 | ...entry 52 | }); 53 | } 54 | 55 | public async generateLiteratureName(key: string, typeSetting: any) { 56 | const nameTemplate = typeSetting.nameTemplate; 57 | const customCiteText = typeSetting.customCiteText; 58 | const useDynamicRefLink = typeSetting.useDynamicRefLink; 59 | // 如果本身不同时使用自定义链接和动态链接,就不需要生成文档的命名 60 | if (!(customCiteText && useDynamicRefLink)) return ""; 61 | const entry = await this.plugin.database.getContentByKey(key); 62 | if (!entry) { 63 | if (isDev) this.logger.error("找不到文献数据", {key, blockID: this.plugin.literaturePool.get(key)}); 64 | this.plugin.noticer.error((this.plugin.i18n.errors as any).getLiteratureFailed); 65 | return ""; 66 | } 67 | return generateFromTemplate(nameTemplate, { 68 | citeFileID: this.plugin.literaturePool.get(key), 69 | ...entry 70 | }); 71 | } 72 | 73 | public async generateCiteRef(citeFileId: string, link: string, name: string, typeSetting: any) { 74 | const customCiteText = typeSetting.customCiteText; 75 | const useDynamicRefLink = typeSetting.useDynamicRefLink; 76 | if (customCiteText) { 77 | if (useDynamicRefLink) await this.plugin.kernelApi.setNameOfBlock(citeFileId, name); 78 | return link; 79 | } else if (useDynamicRefLink) { 80 | await this.plugin.kernelApi.setNameOfBlock(citeFileId, link); 81 | return citeLinkDynamic.replace("${id}", citeFileId).replace("${cite_type}", typeSetting.name); 82 | } else { 83 | return citeLinkStatic.replace("${id}", citeFileId).replace("${link}", link).replace("${cite_type}", typeSetting.name); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/references/noteProcessor.ts: -------------------------------------------------------------------------------- 1 | import type SiYuanPluginCitation from "../index"; 2 | import { createLogger, type ILogger } from "../utils/simple-logger"; 3 | 4 | export class NoteProcessor { 5 | private logger: ILogger; 6 | 7 | constructor(private plugin: SiYuanPluginCitation) { 8 | this.logger = createLogger("note processor") 9 | } 10 | 11 | public async processNote(noteContent: string): Promise { 12 | let resContent = noteContent; 13 | const imgReg = //; 14 | let iter = 0; 15 | while (resContent.match(imgReg)) { 16 | const match = resContent.match(imgReg); 17 | const itemKey = match![1]; 18 | const detail = await this.plugin.database.getAttachmentByItemKey(itemKey); 19 | detail.annotationType = detail.itemType; 20 | const link = await this.plugin.reference.moveImgToAssets(detail.path, detail, "html"); 21 | resContent = resContent.replace(match![0], link); 22 | iter = iter + 1; 23 | if (iter == 20) break; 24 | } 25 | return resContent; 26 | } 27 | } -------------------------------------------------------------------------------- /src/references/pool.ts: -------------------------------------------------------------------------------- 1 | // 只能容纳key和value一对一存在的池子 2 | export class LiteraturePool { 3 | private key2idDict: {[key: string]: string}; 4 | private id2keyDict: {[id: string]: string}; 5 | 6 | constructor() { 7 | this.id2keyDict = {}; 8 | this.key2idDict = {}; 9 | } 10 | 11 | public set(pair: {id: string, key: string}) { 12 | const id = pair.id; 13 | const key = pair.key; 14 | if (this.id2keyDict[id]) { 15 | this.id2keyDict[id] = key; 16 | this.key2idDict = this.reverse(this.id2keyDict); 17 | } else if (this.key2idDict[key]) { 18 | this.key2idDict[key] = id; 19 | this.id2keyDict = this.reverse(this.key2idDict); 20 | } else { 21 | // 都没有就直接新建,不需要再重构目前的 22 | this.id2keyDict[id] = key; 23 | this.key2idDict[key] = id; 24 | } 25 | } 26 | 27 | public get(key: string) { 28 | return this.key2idDict[key] || this.id2keyDict[key]; 29 | } 30 | 31 | public get size() { 32 | return Object.keys(this.id2keyDict).length; 33 | } 34 | 35 | public get keys() { 36 | return Object.keys(this.key2idDict); 37 | } 38 | 39 | public get ids() { 40 | return Object.keys(this.id2keyDict); 41 | } 42 | 43 | public get content() { 44 | return Object.keys(this.id2keyDict).map(id => { 45 | return { 46 | id, 47 | key: this.id2keyDict[id] 48 | }; 49 | }); 50 | } 51 | 52 | public delete(key: string) { 53 | if (this.id2keyDict[key]) { 54 | delete this.id2keyDict[key]; 55 | this.key2idDict = this.reverse(this.id2keyDict); 56 | return true; 57 | } else if (this.key2idDict[key]) { 58 | delete this.key2idDict[key]; 59 | this.id2keyDict = this.reverse(this.key2idDict); 60 | return true; 61 | } else { 62 | return false; 63 | } 64 | } 65 | 66 | public empty() { 67 | this.id2keyDict = {}; 68 | this.key2idDict = {}; 69 | } 70 | 71 | private reverse(dict: {[key: string]: string}) { 72 | return Object.keys(dict).reduce( 73 | (acc, cur) => ({ 74 | ...acc, 75 | [dict[cur]]: cur, 76 | }), 77 | {} 78 | ); 79 | } 80 | } -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const workspaceDir = `${(window as any).siyuan.config.system.workspaceDir}`; 2 | export const dataDir = `${(window as any).siyuan.config.system.dataDir}`; 3 | export const mediaDir = "./assets"; 4 | export const isDev = process.env.NODE_ENV === "development"; 5 | // export const isDev = true; 6 | export const showRequest = false; 7 | export const siyuanApiUrl = ""; 8 | export const siyuanApiToken = ""; 9 | 10 | export const pluginIconSVG = ''; 11 | 12 | export const citeLinkStatic = "${link}"; 13 | export const citeLinkDynamic = "${link}"; 14 | export const DISALLOWED_FILENAME_CHARACTERS_RE = /[*"\\/<>:|?\/]/g; 15 | export const refReg = /\(\((.*?)\)\)/; 16 | export const refRegStatic = /\(\((.*?)\"(.*?)\"\)\)/g; 17 | export const refRegDynamic = /\(\((.*?)\'(.*?)\'\)\)/g; 18 | 19 | export const databaseType = ["BibTex and CSL-JSON", "Zotero (better-bibtex)", "Juris-M (better-bibtex)", "Zotero (debug-bridge)", "Juris-M (debug-bridge)"] as const; 20 | export const dbSearchDialogTypes = ["SiYuan", "Zotero"]; 21 | export const REF_DIR_PATH = "/references/"; 22 | // 用户指南不应该作为可以写入的笔记本 23 | export const hiddenNotebook: Set = new Set(["思源笔记用户指南", "SiYuan User Guide"]); 24 | export const STORAGE_NAME = "menu-config"; 25 | export const defaultReferencePath = "/References"; 26 | export const defaultTitleTemplate = "{{citekey}}"; 27 | export const defaultNoteTemplate = ` 28 | --- 29 | 30 | **Title**:\t{{title}} 31 | 32 | **Author**:\t{{authorString}} 33 | 34 | **Year**:\t{{year}} 35 | 36 | --- 37 | 38 | # Abstract 39 | 40 | {{abstract}} 41 | 42 | # Select on Zotero 43 | 44 | [zotero]({{zoteroSelectURI}}) 45 | 46 | # Files 47 | 48 | {{files}} 49 | 50 | # Note 51 | 52 | {{note}} 53 | `; 54 | export const defaultLinkTemplate = "({{shortAuthor}} {{year}})"; 55 | export const defaultDBPassword = "CTT"; 56 | export const defaultUserDataTile = "User Data"; 57 | export const defaultAttrViewBlock = ""; 58 | export const defaultAttrViewTemplate = ""; 59 | export const defaultUserDataTemplatePath = ""; 60 | export const defaultUseDefaultCiteType = false; 61 | export const defaultUseWholeDocAsUserData = false; 62 | 63 | export const defaultSettingData = { 64 | referenceNotebook: "", 65 | referencePath: defaultReferencePath, 66 | database: databaseType[0], 67 | titleTemplate: defaultTitleTemplate, 68 | userDataTitle: defaultUserDataTile, 69 | noteTemplate: defaultNoteTemplate, 70 | moveImgToAssets: true, 71 | linkTemplate: defaultLinkTemplate, 72 | nameTemplate: "", 73 | customCiteText: false, 74 | useItemKey: false, 75 | autoReplace: false, 76 | deleteUserDataWithoutConfirm: false, 77 | useDynamicRefLink: false, 78 | zoteroLinkTitleTemplate: "", 79 | zoteroTagTemplate: "", 80 | dbPassword: defaultDBPassword, 81 | dbSearchDialogType: dbSearchDialogTypes[0], 82 | shortAuthorLimit: 2, 83 | multiCitePrefix: "", 84 | multiCiteConnector: "", 85 | multiCiteSuffix: "", 86 | linkTemplatesGroup: [], 87 | attrViewBlock: defaultAttrViewBlock, 88 | attrViewTemplate: defaultAttrViewTemplate, 89 | userDataTemplatePath: defaultUserDataTemplatePath, 90 | useWholeDocAsUserData: defaultUseWholeDocAsUserData, 91 | useDefaultCiteType: defaultUseDefaultCiteType 92 | }; 93 | -------------------------------------------------------------------------------- /src/utils/notes.ts: -------------------------------------------------------------------------------- 1 | const blockLabels = [ 2 | "h1", "h2", "h3", "h4", "h5", "h6", 3 | "blockquote", 4 | "table", "ol", "ul", 5 | "p" 6 | ]; 7 | 8 | const requireEnterLabels = [ 9 | "blockquote" 10 | ]; 11 | 12 | interface htmlBlock { 13 | content: string, 14 | isSeparated: boolean 15 | } 16 | 17 | export function htmlNotesProcess(note: string) { 18 | return processBlocks([{ 19 | content: removeWrapDiv(note).replace(/\n+/g, "\n"), 20 | isSeparated: false 21 | }] as htmlBlock[], 0).map(b => "\n"+b.content+"\n").join("\n\n"); 22 | } 23 | 24 | function removeWrapDiv(content: string): string { 25 | const trimContent = content.trim(); 26 | if (trimContent.search(//) == -1 || trimContent.search(//) != 0) { 27 | return trimContent; 28 | } else { 29 | const noDivContent = trimContent.replace(//, ""); 30 | return noDivContent.slice(0, noDivContent.length - "".length); 31 | } 32 | } 33 | 34 | function processBlocks(blockList: htmlBlock[], labelIndex: number) { 35 | if (labelIndex == blockLabels.length) { 36 | return blockList.filter(block => { 37 | return !(block.content == "" || block.content.length == 0); 38 | }); 39 | } 40 | const newList:htmlBlock[] = []; 41 | const label = blockLabels[labelIndex]; 42 | const regExpString = "<" + label + ".*?>[\\s\\S]*?" + label + ">"; 43 | const reg = new RegExp(regExpString); 44 | blockList.forEach(block => { 45 | if (block.isSeparated) { 46 | newList.push(block); 47 | } else { 48 | const blocks = separateBlocks(block.content, reg); 49 | if (requireEnterLabels.indexOf(label) != -1) { 50 | const startLabelExp = new RegExp("<" + label + ".*?>"); 51 | const endLabel= "" + label + ">"; 52 | blocks.forEach(block => { 53 | if (block.isSeparated == true) { 54 | const content = block.content; 55 | const match = content.match(startLabelExp); 56 | block.content = match![0] + "\n" + content.slice(match![0].length, content.length - endLabel.length).trim() + "\n" + endLabel; 57 | } 58 | }); 59 | } 60 | newList.push(...blocks); 61 | } 62 | }); 63 | return processBlocks(newList, labelIndex + 1); 64 | } 65 | 66 | function separateBlocks(content: string, reg: RegExp): htmlBlock[] { 67 | const trimContent = content.trim(); 68 | if (trimContent.search(reg) == -1) { 69 | return [{ 70 | content: trimContent, 71 | isSeparated: false 72 | }]; 73 | } else if (trimContent.search(reg) == 0) { 74 | const match = trimContent.match(reg); 75 | return [{ 76 | content: match![0], 77 | isSeparated: true 78 | }, ...separateBlocks(trimContent.slice(match![0].length), reg)]; 79 | } else { 80 | return [{ 81 | content: trimContent.slice(0, trimContent.search(reg)), 82 | isSeparated: false 83 | }, ...separateBlocks(trimContent.slice(trimContent.search(reg)), reg)]; 84 | } 85 | } -------------------------------------------------------------------------------- /src/utils/noticer.ts: -------------------------------------------------------------------------------- 1 | import { showMessage } from "siyuan"; 2 | 3 | export interface INoticer { 4 | info: (msg: string, obj?: any) => void 5 | error: (msg: string | Error, obj?: any) => void 6 | } 7 | 8 | export const createNoticer = (): INoticer => { 9 | const sign = "siyuan-citation-plugin"; 10 | 11 | const formatDate = (date: Date) => { 12 | const year = date.getFullYear(); 13 | const month = String(date.getMonth() + 1).padStart(2, "0"); 14 | const day = String(date.getDate()).padStart(2, "0"); 15 | const hours = String(date.getHours()).padStart(2, "0"); 16 | const minutes = String(date.getMinutes()).padStart(2, "0"); 17 | const seconds = String(date.getSeconds()).padStart(2, "0"); 18 | 19 | return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; 20 | }; 21 | 22 | const log = (level: "info" | "error", msg: any, timeout: number, obj?: any) => { 23 | const time = formatDate(new Date()); 24 | if (obj) { 25 | let finalMsg = msg as string; 26 | Object.keys(obj).forEach(key => { 27 | finalMsg = finalMsg.replaceAll(`$\{${key}\}`, obj[key]); 28 | }); 29 | showMessage(`[${sign}] [${time}] ${finalMsg}`, timeout, level); 30 | } else { 31 | showMessage(`[${sign}] [${time}] ${msg}`, timeout, level); 32 | } 33 | }; 34 | 35 | return { 36 | info: (msg: string, obj?: any) => log("info", msg, 2000, obj), 37 | error: (msg: string | Error, obj?: any) => { 38 | if (typeof msg == "string") { 39 | log("error", msg, 0, obj); 40 | } else { 41 | showMessage(`[${sign}] [${formatDate(new Date())}] error occurred\n` + JSON.stringify(msg), 0, "error"); 42 | } 43 | }, 44 | }; 45 | }; -------------------------------------------------------------------------------- /src/utils/shortcut/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2023 Zuoqiu Yingyi 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | /** 19 | * 鼠标按钮定义 20 | * REF: [MouseEvent.button - Web API 接口参考 | MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/MouseEvent/button) 21 | */ 22 | export enum MouseButton { 23 | Left = 0, 24 | Middle = 1, 25 | Right = 2, 26 | Back = 3, 27 | Forward = 4, 28 | } 29 | 30 | export enum MouseEvent { 31 | click = "click", 32 | dblclick = "dblclick", 33 | 34 | contextmenu = "contextmenu", 35 | 36 | mousedown = "mousedown", 37 | mouseup = "mouseup", 38 | 39 | mouseenter = "mouseenter", 40 | mouseleave = "mouseleave", 41 | 42 | mousewheel = "mousewheel", 43 | mouseover = "mouseover", 44 | mousemove = "mousemove", 45 | mouseout = "mouseout", 46 | } 47 | 48 | /** 49 | * 功能键状态 50 | * REF: [Event - Web API 接口参考 | MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Event) 51 | */ 52 | export interface IFunctionKeysStatus { 53 | altKey: boolean; 54 | ctrlKey: boolean; 55 | metaKey: boolean; 56 | shiftKey: boolean; 57 | } 58 | 59 | /* 事件类型状态 */ 60 | export interface ITypeStatus extends IFunctionKeysStatus { 61 | // REF [event.type - Web API 接口参考 | MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Event/type) 62 | type: string; 63 | } 64 | 65 | /** 66 | * 键盘状态 67 | * REF: [KeyboardEvent - Web API 接口参考 | MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/KeyboardEvent) 68 | */ 69 | export interface IKeyboardStatus extends ITypeStatus { 70 | // REF [KeyboardEvent.key - Web API 接口参考 | MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/KeyboardEvent/key) 71 | key: string; 72 | } 73 | 74 | /** 75 | * 鼠标状态 76 | * REF: [鼠标事件 - Web API 接口参考 | MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/MouseEvent) 77 | */ 78 | export interface IMouseStatus extends ITypeStatus { 79 | // REF [MouseEvent.button - Web API 接口参考 | MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/MouseEvent/button) 80 | button: MouseButton; 81 | } 82 | -------------------------------------------------------------------------------- /src/utils/shortcut/match.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2023 Zuoqiu Yingyi 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | import type { 19 | IKeyboardStatus, 20 | IMouseStatus, 21 | ITypeStatus, 22 | } from "."; 23 | 24 | /* 判断是否为预期的键盘事件 */ 25 | export function isMatchedKeyboardEvent(e: KeyboardEvent, status: IKeyboardStatus) { 26 | return e.key === status.key 27 | && e.type === status.type 28 | && e.altKey === status.altKey 29 | && e.ctrlKey === status.ctrlKey 30 | && e.metaKey === status.metaKey 31 | && e.shiftKey === status.shiftKey; 32 | } 33 | 34 | /* 判断是否为预期的鼠标事件 */ 35 | export function isMatchedMouseEvent(e: MouseEvent, status: IMouseStatus) { 36 | return e.button === status.button 37 | && e.type === status.type 38 | && e.altKey === status.altKey 39 | && e.ctrlKey === status.ctrlKey 40 | && e.metaKey === status.metaKey 41 | && e.shiftKey === status.shiftKey; 42 | } 43 | 44 | /* 判断是否为预期的类型事件 */ 45 | export function isMatchedTypeEvent(e: KeyboardEvent | MouseEvent, status: ITypeStatus) { 46 | return e.type === status.type 47 | && e.altKey === status.altKey 48 | && e.ctrlKey === status.ctrlKey 49 | && e.metaKey === status.metaKey 50 | && e.shiftKey === status.shiftKey; 51 | } 52 | -------------------------------------------------------------------------------- /src/utils/simple-logger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Terwer . All rights reserved. 3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 | * 5 | * This code is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU General Public License version 2 only, as 7 | * published by the Free Software Foundation. Terwer designates this 8 | * particular file as subject to the "Classpath" exception as provided 9 | * by Terwer in the LICENSE file that accompanied this code. 10 | * 11 | * This code is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 | * version 2 for more details (a copy is included in the LICENSE file that 15 | * accompanied this code). 16 | * 17 | * You should have received a copy of the GNU General Public License version 18 | * 2 along with this work; if not, write to the Free Software Foundation, 19 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 | * 21 | * Please contact Terwer, Shenzhen, Guangdong, China, youweics@163.com 22 | * or visit www.terwer.space if you need additional information or have any 23 | * questions. 24 | */ 25 | 26 | /** 27 | * 简单的日志接口 28 | */ 29 | export interface ILogger { 30 | debug: (msg: string, obj?: any) => void 31 | info: (msg: string, obj?: any) => void 32 | warn: (msg: string, obj?: any) => void 33 | error: (msg: string | Error, obj?: any) => void 34 | } 35 | 36 | /** 37 | * 一个简单轻量级的日志记录器 38 | * 39 | * @author terwer 40 | * @version 1.0.0 41 | * @since 1.0.0 42 | */ 43 | export const createLogger = (name: string): ILogger => { 44 | const sign = "citation"; 45 | 46 | const formatDate = (date: Date) => { 47 | const year = date.getFullYear(); 48 | const month = String(date.getMonth() + 1).padStart(2, "0"); 49 | const day = String(date.getDate()).padStart(2, "0"); 50 | const hours = String(date.getHours()).padStart(2, "0"); 51 | const minutes = String(date.getMinutes()).padStart(2, "0"); 52 | const seconds = String(date.getSeconds()).padStart(2, "0"); 53 | 54 | return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; 55 | }; 56 | 57 | const log = (level: string, msg: any, obj?: any) => { 58 | const time = formatDate(new Date()); 59 | if (obj) { 60 | console.log(`[${sign}] [${time}] [${level}] [${name}] ${msg}`, obj); 61 | } else { 62 | console.log(`[${sign}] [${time}] [${level}] [${name}] ${msg}`); 63 | } 64 | }; 65 | 66 | return { 67 | debug: (msg: string, obj?: any) => log("DEBUG", msg, obj), 68 | info: (msg: string, obj?: any) => log("INFO", msg, obj), 69 | warn: (msg: string, obj?: any) => { 70 | const time = formatDate(new Date()); 71 | if (obj) { 72 | console.warn(`[${sign}] [${time}] [WARN] ${msg}`, obj); 73 | } else { 74 | console.warn(`[${sign}] [${time}] [WARN] ${msg}`); 75 | } 76 | }, 77 | error: (msg: string | Error, obj?: any) => { 78 | if (typeof msg == "string") { 79 | log("ERROR", msg, obj); 80 | } else { 81 | console.error(`[${sign}] [${formatDate(new Date())}] [ERROR] [${name}] error occurred`, msg); 82 | } 83 | }, 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /src/utils/templates.ts: -------------------------------------------------------------------------------- 1 | import template from "template_js"; 2 | 3 | export function generateFromTemplate(contentTemplate: string, params: object) { 4 | const reg = /\{\{(.*?)\}\}/g; 5 | template.config({escape: false}); 6 | return template(contentTemplate.replace(reg, (match, pname) => { 7 | return `<%= ${pname} %>`; 8 | }), params); 9 | } -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export interface ISpanItem { 2 | block_id: string; 3 | box: string; 4 | content: string; 5 | ial: string; 6 | id: string; 7 | markdown: string; 8 | path: string; 9 | root_id: string; 10 | type: string; 11 | } 12 | 13 | export interface IBlockItem { 14 | alias: string; 15 | box: string; 16 | content: string; 17 | created: string; 18 | fcontent: string; 19 | hash: string; 20 | hpath: string; 21 | ial: string; 22 | id: string; 23 | length: number; 24 | markdown: string; 25 | memo: string; 26 | name: string; 27 | parent_id: string; 28 | path: string; 29 | root_id: string; 30 | sort: number; 31 | subtype: string; 32 | tag: string; 33 | type: string; 34 | updated: string; 35 | } -------------------------------------------------------------------------------- /src/utils/updates.ts: -------------------------------------------------------------------------------- 1 | import SiYuanPluginCitation from "../index"; 2 | import { STORAGE_NAME, defaultUserDataTile } from "./constants"; 3 | 4 | // 专门负责处理更新事务的函数 5 | export async function changeUpdate(plugin: SiYuanPluginCitation) { 6 | await updateDatabaseType(plugin); 7 | await updateUserDataTitle(plugin); 8 | await updateLinkTemplatesGroup(plugin); 9 | } 10 | 11 | async function updateDatabaseType(plugin: SiYuanPluginCitation) { 12 | let data_change = false; 13 | if (plugin.data[STORAGE_NAME].databaseType == "Zotero") { 14 | plugin.data[STORAGE_NAME].databaseType = "Zotero (better-bibtex)"; 15 | data_change = true; 16 | } else if (plugin.data[STORAGE_NAME].databaseType == "Juris-M") { 17 | plugin.data[STORAGE_NAME].databaseType = "Juris-M (better-bibtex)"; 18 | data_change = true; 19 | } 20 | if (data_change) await plugin.saveData(STORAGE_NAME, plugin.data[STORAGE_NAME]); 21 | } 22 | 23 | async function updateUserDataTitle(plugin: SiYuanPluginCitation) { 24 | let data_change = false; 25 | if (!plugin.data[STORAGE_NAME].userDataTitle) { 26 | plugin.data[STORAGE_NAME].userDataTitle = defaultUserDataTile; 27 | data_change = true; 28 | } 29 | if (data_change) await plugin.saveData(STORAGE_NAME, plugin.data[STORAGE_NAME]); 30 | } 31 | 32 | async function updateLinkTemplatesGroup(plugin:SiYuanPluginCitation) { 33 | let data_change = false; 34 | if (!plugin.data[STORAGE_NAME].linkTemplatesGroup?.length) { 35 | plugin.data[STORAGE_NAME].linkTemplatesGroup = [{ 36 | name: "default", 37 | ...plugin.data[STORAGE_NAME] 38 | }]; 39 | data_change = true; 40 | } 41 | if (data_change) await plugin.saveData(STORAGE_NAME, plugin.data[STORAGE_NAME]); 42 | } -------------------------------------------------------------------------------- /src/utils/util.ts: -------------------------------------------------------------------------------- 1 | import SiYuanPluginCitation from "../index"; 2 | import { 3 | isDev, 4 | STORAGE_NAME 5 | } from "./constants"; 6 | import { createLogger } from "./simple-logger"; 7 | import { type INoticer } from "./noticer"; 8 | 9 | interface ReadDirRes { 10 | isDir: boolean; 11 | isSymlink: boolean; 12 | name: string; 13 | updated: string; 14 | } 15 | 16 | /** 17 | * Reading the given directory and 18 | * search all the .json and .bib file 19 | */ 20 | export async function fileSearch(plugin: SiYuanPluginCitation, dirPath: string, noticer: INoticer): Promise { 21 | const absStoragePath = "/data/storage/petal/siyuan-plugin-citation"; 22 | const absDirPath = absStoragePath + dirPath; 23 | // 读取文件夹 24 | let files = (await plugin.kernelApi.readDir(absDirPath)).data as ReadDirRes[]; 25 | // 如果没有这个文件夹就新建 26 | if (!files) { plugin.kernelApi.putFile(absDirPath, true, ""); files = []; } 27 | const pList = files.map( async file => { 28 | if (file.isDir) { 29 | // 如果是文件夹就迭代搜索 30 | const subdirPath = dirPath + file.name + "/"; 31 | const files = await fileSearch(plugin, subdirPath, noticer); 32 | return files; 33 | } else { 34 | // 如果不是文件夹就获取文件内容 35 | const typePos = file.name.split(".").length - 1; 36 | if (file.name.split(".")[typePos] == "json" || file.name.split(".")[typePos] == "bib") { 37 | return [absDirPath + file.name]; 38 | } else { 39 | return []; 40 | } 41 | } 42 | }); 43 | return await Promise.all(pList).then(finalRes => { 44 | const filePaths: any[] | PromiseLike = []; 45 | finalRes.forEach(fileList => { 46 | filePaths.push(...fileList); 47 | }); 48 | return filePaths; 49 | }); 50 | } 51 | 52 | export async function loadLocalRef(plugin: SiYuanPluginCitation): Promise { 53 | const logger = createLogger("load references"); 54 | const notebookId = plugin.data[STORAGE_NAME].referenceNotebook as string; 55 | const refPath = plugin.data[STORAGE_NAME].referencePath as string; 56 | plugin.literaturePool.empty(); 57 | const limit = 64; 58 | let offset = 0; 59 | let cont = true; 60 | let promiseList: any[] = []; 61 | while (cont) { 62 | /** 63 | * 通过读取文献库中的文献构建文献池,用于快速索引和对应key与文档id 64 | * 文献的索引位置经过两次更新,因此目前要考虑将更新前的情况进行转化: 65 | * 1. key在文档标题位置 66 | * 2. key在文档的命名中 67 | * 3. key在文档的自定义字段“custom-literature-key”中 68 | */ 69 | const literatureDocs = (await plugin.kernelApi.getLiteratureDocInPath(notebookId, refPath + "/", offset, limit)).data as any[]; 70 | if (literatureDocs.length < limit) { 71 | // 已经提取到所有了 72 | cont = false; 73 | } 74 | const pList = literatureDocs.map(async file => { 75 | let key = ""; 76 | const literatureKey = file.literature_key; 77 | if (!literatureKey) { 78 | // 如果没有这个自定义属性,就在标题和命名里找一下 79 | if (file.name === "") { 80 | // 如果命名为空就从标题里拿 81 | await plugin.kernelApi.setBlockKey(file.id, file.content); 82 | key = file.content; 83 | } else { 84 | // 命名不为空那就在命名里 85 | await plugin.kernelApi.setBlockKey(file.id, file.name); 86 | key = file.name; 87 | } 88 | } else key = literatureKey; 89 | plugin.literaturePool.set({id: file.id, key}); 90 | }); 91 | promiseList = [...promiseList, ...pList]; 92 | offset += limit; 93 | } 94 | await Promise.all(promiseList); 95 | if (isDev) logger.info("成功载入引用,content=>", plugin.literaturePool.content); 96 | plugin.noticer.info((plugin.i18n.notices as any).loadRefSuccess, {size: plugin.literaturePool.size}); 97 | } 98 | 99 | export function generateFileLinks(files: string[]) { 100 | return files ? files.map(file => { 101 | const fileName = file.split("\\").slice(-1)[0]; 102 | return `[${fileName}]` + "\(file://" + file.replace(/\\(.?)/g, (m, p1) => p1) + "\)"; 103 | }).join("\n") : files; 104 | } 105 | 106 | export function filePathProcess(path: string) { 107 | const illegalSigns = /([\(\)])/g; 108 | return path.replace(illegalSigns, (m, p1) => `\\${p1}`); 109 | } 110 | 111 | export function fileNameProcess(path: string) { 112 | const illegalSigns = /([\(\)\[\]])/g; 113 | return path.replace(illegalSigns, (m, p1) => `\\${p1}`); 114 | } 115 | 116 | export async function sleep(time: number) { 117 | return new Promise(resolve => { 118 | setTimeout(() => { 119 | resolve(true); 120 | }, time); 121 | }); 122 | } 123 | 124 | export function cleanEmptyKey(obj: any) { 125 | const cleanedObj = Object.keys(obj).reduce((prv: any, key) => { 126 | if (obj[key] == null || obj[key] == undefined || obj[key] == "") return prv; 127 | else if (typeof obj[key] == "object") { 128 | if (Object.prototype.toString.call(obj[key]) === "[object Array]") { 129 | if (obj[key].length > 0) prv[key] = obj[key].map((o: any) => { 130 | if (typeof o == "object") return cleanEmptyKey(o); 131 | else return o; 132 | }); 133 | } else prv[key] = cleanEmptyKey(obj[key]); 134 | } else prv[key] = obj[key]; 135 | return prv; 136 | }, {}); 137 | return cleanedObj; 138 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | // "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "target": "ESNext", 7 | "useDefineForClassFields": true, 8 | "module": "ESNext", 9 | "lib": [ 10 | "ES2023", 11 | "DOM", 12 | "DOM.Iterable", 13 | "ES6", 14 | "ScriptHost" 15 | ], 16 | "allowJs": true, 17 | /* Bundler mode */ 18 | "moduleResolution": "Node", 19 | "importHelpers": true, 20 | "allowImportingTsExtensions": true, 21 | "allowSyntheticDefaultImports": true, 22 | "resolveJsonModule": true, 23 | "noEmit": true, 24 | "types": [ 25 | "node", 26 | "svelte" 27 | ] 28 | }, 29 | "include": [ 30 | "src/**/*.ts", 31 | "src/**/*.svelte", 32 | "typing", 33 | "node_modules/zotero-types", 34 | "zoteroJS" 35 | ], 36 | "root": ".", 37 | "extends": "@tsconfig/svelte/tsconfig.json" 38 | } 39 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const webpack = require("webpack"); 4 | const {EsbuildPlugin} = require("esbuild-loader"); 5 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 6 | const CopyPlugin = require("copy-webpack-plugin"); 7 | const ZipPlugin = require("zip-webpack-plugin"); 8 | const sveltePreprocess = require("svelte-preprocess"); 9 | 10 | module.exports = (env, argv) => { 11 | const targetDir = "D:/Documents/SiYuan/data/plugins/siyuan-plugin-citation/"; 12 | const isPro = argv.mode === "production"; 13 | const plugins = [ 14 | new MiniCssExtractPlugin({ 15 | filename: isPro ? "dist/index.css" : "index.css", 16 | }) 17 | ]; 18 | let entry = { 19 | "index": "./src/index.ts", 20 | }; 21 | if (isPro) { 22 | entry = { 23 | "dist/index": "./src/index.ts", 24 | }; 25 | plugins.push(new webpack.BannerPlugin({ 26 | banner: () => { 27 | return fs.readFileSync("LICENSE").toString(); 28 | }, 29 | })); 30 | plugins.push(new CopyPlugin({ 31 | patterns: [ 32 | {from: "preview.png", to: "./dist/"}, 33 | {from: "icon.png", to: "./dist/"}, 34 | {from: "citeIcon.ico", to: "./dist/"}, 35 | {from: "README*.md", to: "./dist/"}, 36 | {from: "plugin.json", to: "./dist/"}, 37 | {from: "src/i18n/", to: "./dist/i18n/"}, 38 | {from: "assets/", to: "./dist/assets/"}, 39 | {from: "sample-data/sample.bib", to: "./dist/sample-data/"}, 40 | {from: "sample-data/sample.json", to: "./dist/sample-data/"}, 41 | {from: "zoteroJS/", to: "./dist/zoteroJS/"}, 42 | {from: "scripts/", to: "./dist/scripts/"} 43 | ], 44 | })); 45 | plugins.push(new ZipPlugin({ 46 | filename: "package.zip", 47 | algorithm: "gzip", 48 | include: [/dist/], 49 | pathMapper: (assetPath) => { 50 | return assetPath.replace("dist/", ""); 51 | }, 52 | })); 53 | } else { 54 | 55 | plugins.push(new CopyPlugin({ 56 | patterns: [ 57 | {from: "src/i18n/", to: "./i18n/"}, 58 | {from: "preview.png", to: "./"}, 59 | {from: "icon.png", to: "./"}, 60 | {from: "citeIcon.ico", to: "./"}, 61 | {from: "README*.md", to: "./"}, 62 | {from: "plugin.json", to: "./"}, 63 | {from: "assets/", to: "./assets/"}, 64 | {from: "sample-data/", to: "./sample-data/"}, 65 | {from: "zoteroJS/", to: "./zoteroJS/"}, 66 | {from: "scripts/", to: "./scripts/"} 67 | ] 68 | })); 69 | } 70 | return { 71 | mode: argv.mode || "development", 72 | watch: !isPro, 73 | devtool: isPro ? false : "eval", 74 | output: { 75 | filename: "[name].js", 76 | // path: path.resolve(__dirname), 77 | path: targetDir, 78 | libraryTarget: "commonjs2", 79 | library: { 80 | type: "commonjs2", 81 | }, 82 | }, 83 | externals: { 84 | siyuan: "siyuan", 85 | }, 86 | entry, 87 | optimization: { 88 | minimize: true, 89 | minimizer: [ 90 | new EsbuildPlugin(), 91 | ], 92 | }, 93 | resolve: { 94 | extensions: [".ts", ".scss", ".js", ".json", ".mjs", ".svelte"] 95 | }, 96 | module: { 97 | rules: [ 98 | { 99 | test: /\.ts(x?)$/, 100 | include: [path.resolve(__dirname, "src")], 101 | use: [ 102 | { 103 | loader: "esbuild-loader", 104 | options: { 105 | target: "es6", 106 | } 107 | }, 108 | ], 109 | }, 110 | { 111 | test: /\.scss$/, 112 | include: [path.resolve(__dirname, "src")], 113 | use: [ 114 | MiniCssExtractPlugin.loader, 115 | { 116 | loader: "css-loader", // translates CSS into CommonJS 117 | }, 118 | { 119 | loader: "sass-loader", // compiles Sass to CSS 120 | }, 121 | ], 122 | }, 123 | { 124 | test: /\.(html|svelte)$/, 125 | use: { 126 | loader: "svelte-loader", 127 | options: { 128 | preprocess: sveltePreprocess() 129 | } 130 | } 131 | }, 132 | { 133 | // required to prevent errors from Svelte on Webpack 5+, omit on Webpack 4 134 | test: /node_modules\/svelte\/.*\.mjs$/, 135 | resolve: { 136 | fullySpecified: false 137 | } 138 | } 139 | ], 140 | }, 141 | plugins, 142 | }; 143 | }; 144 | -------------------------------------------------------------------------------- /zoteroJS/addTagsToItem.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /* eslint-disable */ 3 | var targetItem = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key); 4 | var existTags = targetItem.getTags(); 5 | 6 | var setList = tags.split(",").reduce((acc, tag) => { 7 | if (existTags.indexOf(tag) == -1) { 8 | return [...acc, { 9 | tag, 10 | type: 0 11 | }]; 12 | } else { 13 | return acc; 14 | } 15 | }, existTags); 16 | 17 | await targetItem.setTags(setList); 18 | return JSON.stringify({result: await targetItem.saveTx()}); -------------------------------------------------------------------------------- /zoteroJS/checkItemKeyExist.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /* eslint-disable */ 3 | var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key); 4 | 5 | var notItemType = ["attachment", "annotation"] 6 | 7 | if (!item || (notItemType.indexOf(item.itemType) != -1)) return JSON.stringify({ itemKey: key, itemKeyExist: false }) 8 | else return JSON.stringify({ itemKey: key, itemKeyExist: true }) -------------------------------------------------------------------------------- /zoteroJS/checkRunning.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /* eslint-disable */ 3 | return JSON.stringify({ 4 | ready: true 5 | }); -------------------------------------------------------------------------------- /zoteroJS/citeWithZoteroDialog.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | /* eslint-disable max-classes-per-file */ 3 | var is7 = (typeof location !== 'undefined' && location.search) ? ((new URLSearchParams(location.search)).get('is7') === 'true') : Zotero.platformMajorVersion >= 102; 4 | 5 | class FieldEnumerator { 6 | // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match 7 | 8 | constructor(doc) { 9 | this.doc = doc; 10 | this.idx = 0; 11 | this.QueryInterface = (is7 ? ChromeUtils : XPCOMUtils).generateQI([Components.interfaces.nsISupports, Components.interfaces.nsISimpleEnumerator]); 12 | } 13 | 14 | hasMoreElements() { return this.idx < this.doc.fields.length; } 15 | getNext() { return this.doc.fields[this.idx++]; } 16 | 17 | } 18 | 19 | /** 20 | * The Field class corresponds to a field containing an individual citation 21 | * or bibliography 22 | */ 23 | class Field { 24 | 25 | constructor(doc) { 26 | this.doc = doc; 27 | this.code = ""; 28 | this.text = "{Placeholder}"; 29 | this.wrappedJSObject = this; 30 | this.noteIndex = 0; 31 | } 32 | 33 | delete() { this.doc.fields.filter(field => field !== this); } 34 | 35 | removeCode() { this.code = ""; } 36 | 37 | select() { return 0; } 38 | 39 | setText(text, isRich) { 40 | this.text = text; 41 | this.isRich = isRich; 42 | } 43 | 44 | getText() { return this.text; } 45 | 46 | setCode(code) { this.code = code; } 47 | 48 | getCode() { return this.code; } 49 | 50 | equals(field) { return this === field; } 51 | 52 | getNoteIndex() { return 0; } 53 | } 54 | 55 | /** 56 | * The Document class corresponds to a single word processing document. 57 | */ 58 | class Document { 59 | 60 | constructor(docId, options) { 61 | this.id = docId; 62 | this.fields = []; 63 | 64 | options.style = options.style || "apa"; 65 | var style = Zotero.Styles.get(`http://www.zotero.org/styles/${options.style}`) || Zotero.Styles.get(`http://juris-m.github.io/styles/${options.style}`) || Zotero.Styles.get(options.style); 66 | options.style = style ? style.url : "http://www.zotero.org/styles/apa"; 67 | 68 | var data = new Zotero.Integration.DocumentData(); 69 | data.prefs = { 70 | noteType: 0, 71 | fieldType: "Field", 72 | automaticJournalAbbreviations: true, 73 | }; 74 | data.style = {styleID: options.style, locale: "en-US", hasBibliography: true, bibliographyStyleHasBeenSet: true}; 75 | data.sessionID = Zotero.Utilities.randomString(10); // eslint-disable-line no-magic-numbers 76 | this.data = data.serialize(); 77 | } 78 | 79 | displayAlert(_dialogText, _icon, _buttons) { return 0; } 80 | 81 | activate() { return 0; } 82 | 83 | canInsertField(_fieldType) { return true; } 84 | 85 | cursorInField(_fieldType) { return false; } 86 | 87 | 88 | getDocumentData() { return this.data; } 89 | 90 | setDocumentData(data) { this.data = data; } 91 | 92 | insertField(fieldType, noteType) { 93 | if (typeof noteType !== "number") { 94 | throw new Error("noteType must be an integer"); 95 | } 96 | var field = new Field(this); 97 | this.fields.push(field); 98 | return field; 99 | } 100 | 101 | getFields(_fieldType) { return new FieldEnumerator(this); } 102 | 103 | getFieldsAsync(fieldType, observer) { 104 | observer.observe(this.getFields(fieldType), "fields-available", null); 105 | } 106 | 107 | setBibliographyStyle(_firstLineIndent, _bodyIndent, _lineSpacing, _entrySpacing, _tabStops, _tabStopsCount) { return 0; } 108 | 109 | convert(_fields, _toFieldType, _toNoteType, _count) { return 0; } 110 | 111 | cleanup() { return 0; } 112 | 113 | complete() { return 0; } 114 | 115 | citation() { 116 | if (!this.fields[0] || !this.fields[0].code || !this.fields[0].code.startsWith("ITEM CSL_CITATION ")) return []; 117 | 118 | var citationItems = JSON.parse(this.fields[0].code.replace(/ITEM CSL_CITATION /, "")).citationItems; 119 | 120 | var items = (citationItems.map(item => { 121 | var zoteroItem = Zotero.Items.get(item.id); 122 | return { 123 | id: item.id, 124 | key: zoteroItem.key, 125 | libraryID: zoteroItem.libraryID, 126 | locator: item.locator || "", 127 | suppressAuthor: !!item["suppress-author"], 128 | prefix: item.prefix || "", 129 | suffix: item.suffix || "", 130 | label: item.locator ? (item.label || "page") : "", 131 | citekey: Zotero.BetterBibTeX?.KeyManager.get(item.id).citekey, 132 | 133 | uri: Array.isArray(item.uri) ? item.uri[0] : undefined, 134 | itemType: item.itemData ? item.itemData.type : undefined, 135 | title: item.itemData ? item.itemData.title : undefined, 136 | }})); 137 | return items; 138 | } 139 | } 140 | 141 | // export singleton: https://k94n.com/es6-modules-single-instance-pattern 142 | var application = new class { 143 | constructor() { 144 | this.primaryFieldType = "Field"; 145 | this.secondaryFieldType = "Bookmark"; 146 | this.fields = []; 147 | this.docs = {}; 148 | } 149 | 150 | getActiveDocument() { return this.docs[this.active]; } 151 | 152 | async getDocument(id) { 153 | return this.docs[id]; 154 | } 155 | 156 | QueryInterface() { return this; } 157 | 158 | createDocument(options) { 159 | this.active = `citation-for-siyuan-cayw-${Zotero.Utilities.generateObjectKey()}`; 160 | this.docs[this.active] = new Document(this.active, options); 161 | return this.docs[this.active]; 162 | } 163 | 164 | closeDocument(doc) { 165 | delete this.docs[doc.id]; 166 | } 167 | }; 168 | 169 | var document, session, documentImported; 170 | var agent = "Citation for SiYuan"; 171 | var command = "addEditCitation"; 172 | var options = { 173 | format: "translate", 174 | translator: "36a3b0b5-bad0-4a04-b79b-441c7cef77db", 175 | exportNotes: true 176 | }; 177 | var doc = application.createDocument(options); 178 | var docId = doc.id; 179 | 180 | Zotero.Integration.currentDoc = true; 181 | 182 | var startTime = (new Date()).getTime(); 183 | 184 | try { 185 | Zotero.debug(`Integration: ${agent}-${command}${docId ? `:'${docId}'` : ""} invoked`); 186 | var documentPromise = (application.getDocument && docId ? application.getDocument(docId) : application.getActiveDocument()); 187 | Zotero.Integration.currentDoc = document = await documentPromise; 188 | 189 | [session, documentImported] = await Zotero.Integration.getSession(application, document, agent, false); 190 | Zotero.Integration.currentSession = session; 191 | if (!documentImported) { 192 | await (new Zotero.Integration.Interface(application, document, session))[command](); 193 | } 194 | await document.setDocumentData(session.data.serialize()); 195 | } 196 | catch (e) { 197 | if (!(e instanceof Zotero.Exception.UserCancelled)) { 198 | await Zotero.Integration._handleCommandError(document, session, e); 199 | } 200 | else { 201 | if (session) { 202 | // If user cancels we should still write the currently assigned session ID 203 | try { 204 | await document.setDocumentData(session.data.serialize()); 205 | // And any citations marked for processing (like retraction warning ignore flag changes) 206 | if (Object.keys(session.processIndices).length) { 207 | session.updateDocument(FORCE_CITATIONS_FALSE, false, false); 208 | } 209 | // Since user cancelled we can ignore if processor fails here. 210 | } catch(e) {} 211 | } 212 | } 213 | } 214 | finally { 215 | var diff = ((new Date()).getTime() - startTime)/1000; 216 | Zotero.debug(`Integration: ${agent}-${command}${docId ? `:'${docId}'` : ""} complete in ${diff}s`); 217 | if (document) { 218 | try { 219 | await document.cleanup(); 220 | await document.activate(); 221 | 222 | // Call complete function if one exists 223 | if (document.wrappedJSObject && document.wrappedJSObject.complete) { 224 | document.wrappedJSObject.complete(); 225 | } else if (document.complete) { 226 | await document.complete(); 227 | } 228 | } catch(e) { 229 | Zotero.logError(e); 230 | } 231 | } 232 | 233 | if(Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) { 234 | var oldWindow = Zotero.Integration.currentWindow; 235 | Zotero.Promise.delay(100).then(function() { 236 | oldWindow.close(); 237 | }); 238 | } 239 | 240 | if (Zotero.Integration.currentSession && Zotero.Integration.currentSession.progressBar) { 241 | Zotero.Promise.delay(5).then(function() { 242 | Zotero.Integration.currentSession.progressBar.hide(); 243 | }); 244 | } 245 | Zotero.Integration.currentDoc = Zotero.Integration.currentWindow = false; 246 | } 247 | 248 | var picked = doc.citation(); 249 | application.closeDocument(doc); 250 | return JSON.stringify(picked); 251 | -------------------------------------------------------------------------------- /zoteroJS/getAllItems.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /* eslint-disable */ 3 | let Result = []; 4 | var s = new Zotero.Search(); 5 | s.libraryID = Zotero.Libraries.userLibraryID; 6 | s.addCondition('noChildren', 'true'); 7 | s.addCondition('recursive', 'true'); 8 | s.addCondition('joinMode', 'any'); 9 | s.addCondition("itemType", "isNot", "note", true); 10 | s.addCondition("itemType", "isNot", "attachment", true); 11 | s.addCondition("itemType", "isNot", "annotation", true); 12 | var ids = await s.search(); 13 | 14 | for (let id of ids) { 15 | var item = Zotero.Items.get(id) 16 | Result.push({ 17 | libraryID: item.libraryID, 18 | itemKey: item.key, 19 | citationKey: item.getField("citationKey"), 20 | creators: item.getCreatorsJSON(), 21 | year: item.getField("year"), 22 | title: item.getField("title"), 23 | }); 24 | } 25 | 26 | return JSON.stringify(Result); -------------------------------------------------------------------------------- /zoteroJS/getAttachmentByItemKey.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /* eslint-disable */ 3 | var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key); 4 | 5 | function getAllFields(item) { 6 | // 获得fields 7 | let fieldDetail = {}; 8 | let fields = Object.keys(item); 9 | const fieldIgnored = [ 10 | "_ObjectsClass", "_ObjectType", "_objectTypePlural", "_ObjectTypePlural", 11 | "_synced", "_deleted", "_inCache", "_loaded", "_skipDataTypeLoad", "_changed", 12 | "_previousData", "_changedData", "_dataTypesToReload", "_disabled", "_creators", 13 | "_itemData", "_annotationPosition", "_attachments" 14 | ] 15 | for (let fieldName of fields) { 16 | if (fieldIgnored.indexOf(fieldName) == -1) fieldDetail[fieldName.slice(1)] = item[fieldName]; 17 | } 18 | fields = item.getUsedFields(true); 19 | for (let fieldName of fields) { 20 | fieldDetail[fieldName] = item.getField(fieldName); 21 | } 22 | return fieldDetail; 23 | } 24 | 25 | // 获得attachment 26 | let res = { 27 | key: item.key, 28 | itemType: item.itemType, 29 | ...getAllFields(item), 30 | path: await item.getFilePathAsync(), 31 | select: 'zotero://select/library/items/' + item.key, 32 | } 33 | 34 | // 输出结果 35 | return JSON.stringify(res); -------------------------------------------------------------------------------- /zoteroJS/getItemByItemKey.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /* eslint-disable */ 3 | var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key); 4 | 5 | var notItemType = ["attachment", "annotation"] 6 | 7 | if (!item || (notItemType.indexOf(item.itemType) != -1)) return { 8 | itemExist: false 9 | } 10 | 11 | function getAllFields(item) { 12 | // 获得fields 13 | let fieldDetail = {}; 14 | let fields = Object.keys(item); 15 | const fieldIgnored = [ 16 | "_ObjectsClass", "_ObjectType", "_objectTypePlural", "_ObjectTypePlural", 17 | "_synced", "_deleted", "_inCache", "_loaded", "_skipDataTypeLoad", "_changed", 18 | "_previousData", "_changedData", "_dataTypesToReload", "_disabled", "_creators", 19 | "_itemData", "_annotationPosition", "_attachments" 20 | ] 21 | for (let fieldName of fields) { 22 | if (fieldIgnored.indexOf(fieldName) == -1) fieldDetail[fieldName.slice(1)] = item[fieldName]; 23 | } 24 | fields = item.getUsedFields(true); 25 | for (let fieldName of fields) { 26 | fieldDetail[fieldName] = item.getField(fieldName); 27 | } 28 | return fieldDetail; 29 | } 30 | Zotero.Item.getUsedFields 31 | // 获得fields 32 | var itemFields = getAllFields(item); 33 | 34 | // 获得notes 35 | var noteIDs = item.getNotes(); 36 | let notes = []; 37 | for (let noteID of noteIDs) { 38 | var noteItem = Zotero.Items.get(noteID); 39 | notes.push({ 40 | note: noteItem.getNote(), 41 | ...getAllFields(noteItem), 42 | key: noteItem.key, 43 | itemType: noteItem.itemType 44 | }); 45 | } 46 | 47 | // 获得attachments和annotations 48 | var attachmentIDs = item.getAttachments(); 49 | let attachments = []; 50 | var annotationFileType = ["pdf", "equb"]; 51 | let annotations = []; 52 | for (let attachmentID of attachmentIDs) { 53 | var attachment = Zotero.Items.get(attachmentID); 54 | var attachDetail = { 55 | path: await attachment.getFilePathAsync(), 56 | key: attachment.key, 57 | title: attachment.getField("title"), 58 | select: 'zotero://select/library/items/' + attachment.key, 59 | ...getAllFields(attachment) 60 | } 61 | attachments.push(attachDetail); 62 | if (attachDetail.path 63 | && (attachDetail.path.split(".").slice(-1) == "pdf" 64 | || attachDetail.path.split(".").slice(-1) == "equb")) { 65 | var annoItems = attachment.getAnnotations(); 66 | if (annoItems.length) { 67 | var annoDetail = { 68 | parentKey: attachDetail.key, 69 | parentTitle: attachDetail.title, 70 | details: [] 71 | } 72 | for (let annoItem of annoItems) { 73 | annoDetail.details.push({ 74 | key: annoItem.key, 75 | annotationText: annoItem.annotationText, 76 | annotationPosition: JSON.parse(annoItem.annotationPosition), 77 | annotationComment: annoItem.annotationComment, 78 | imagePath: Zotero.Annotations.getCacheImagePath(annoItem), 79 | ...getAllFields(annoItem) 80 | }) 81 | } 82 | annotations.push(annoDetail); 83 | } 84 | } 85 | } 86 | 87 | // 输出结果 88 | return JSON.stringify({ 89 | ...itemFields, 90 | notes, 91 | annotations, 92 | attachments, 93 | itemKey: item.key, 94 | itemType: item.itemType, 95 | creators: item.getCreatorsJSON(), 96 | tags: item.getTags(), 97 | citationKey: item.getField("citationKey"), 98 | year: item.getField("year"), 99 | itemExist: true 100 | }); -------------------------------------------------------------------------------- /zoteroJS/getItemKeyByCiteKey.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /* eslint-disable */ 3 | var s = new Zotero.Search(); 4 | s.libraryID = libraryID; 5 | s.addCondition("citationKey", "is", citekey); 6 | var ids = await s.search(); 7 | let resKey = ""; 8 | if (ids.length) { 9 | var item = Zotero.Items.get(ids[0]); 10 | 11 | // 输出结果 12 | resKey = libraryID + "_" + item.key; 13 | } 14 | return JSON.stringify({ 15 | itemKey: resKey 16 | }) 17 | -------------------------------------------------------------------------------- /zoteroJS/getMarkdownNotes.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /* eslint-disable */ 3 | var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key); 4 | 5 | function _translate(items, format, callback) { 6 | let translation = new Zotero.Translate.Export(); 7 | translation.setItems(items.slice()); 8 | translation.setTranslator(format.id); 9 | if (format.options) { 10 | translation.setDisplayOptions(format.options); 11 | } 12 | translation.setHandler("done", callback); 13 | translation.translate(); 14 | } 15 | 16 | let markdownFormat = { mode: 'export', id: Zotero.Translators.TRANSLATOR_ID_NOTE_MARKDOWN, options: {exportNotes: true} }; 17 | 18 | return await new Promise((resolve, reject) => { 19 | _translate([item], markdownFormat, (obj, worked) => { 20 | resolve(obj.string.replace(/\r\n/g, '\n')) 21 | }) 22 | }) -------------------------------------------------------------------------------- /zoteroJS/getNotesByItemKey.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /* eslint-disable */ 3 | var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key); 4 | 5 | function getAllFields(item) { 6 | // 获得fields 7 | let fieldDetail = {}; 8 | let fields = Object.keys(item); 9 | const fieldIgnored = [ 10 | "_ObjectsClass", "_ObjectType", "_objectTypePlural", "_ObjectTypePlural", 11 | "_synced", "_deleted", "_inCache", "_loaded", "_skipDataTypeLoad", "_changed", 12 | "_previousData", "_changedData", "_dataTypesToReload", "_disabled", "_creators", 13 | "_itemData", "_annotationPosition", "_attachments" 14 | ] 15 | for (let fieldName of fields) { 16 | if (fieldIgnored.indexOf(fieldName) == -1) fieldDetail[fieldName.slice(1)] = item[fieldName]; 17 | } 18 | fields = item.getUsedFields(true); 19 | for (let fieldName of fields) { 20 | fieldDetail[fieldName] = item.getField(fieldName); 21 | } 22 | return fieldDetail; 23 | } 24 | 25 | // 获得notes 26 | var noteIDs = item.getNotes(); 27 | let notes = []; 28 | for (let noteID of noteIDs) { 29 | var noteItem = Zotero.Items.get(noteID); 30 | notes.push({ 31 | note: noteItem.getNote(), 32 | ...getAllFields(noteItem), 33 | key: noteItem.key, 34 | itemType: noteItem.itemType 35 | }); 36 | } 37 | 38 | // 输出结果 39 | return JSON.stringify(notes); -------------------------------------------------------------------------------- /zoteroJS/getSelectedItems.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /* eslint-disable */ 3 | var selectedItems = Zotero.getActiveZoteroPane().getSelectedItems(); 4 | var resItems = []; 5 | selectedItems.forEach(item => { 6 | var item = Zotero.Items.get(item.id); 7 | resItems.push(item.libraryID + "_" + item.key); 8 | }) 9 | return JSON.stringify(resItems); -------------------------------------------------------------------------------- /zoteroJS/updateURLToItem.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /* eslint-disable */ 3 | var targetItem = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key); 4 | 5 | if (!targetItem) return { 6 | itemExist: false 7 | } 8 | 9 | // 获得attachments和annotations 10 | var attachmentIDs = targetItem.getAttachments(); 11 | let existAttachID = 0; 12 | for (let attachmentID of attachmentIDs) { 13 | var attachment = Zotero.Items.get(attachmentID); 14 | var attachURL = attachment.getField("url"); 15 | if (attachURL === url) { 16 | existAttachID = attachmentID; 17 | } 18 | } 19 | 20 | if (existAttachID) { 21 | var attachment = Zotero.Items.get(existAttachID); 22 | attachment.setField("title", title); 23 | return JSON.stringify({result: await attachment.saveTx()}); 24 | } else { 25 | var itemData = { 26 | url, 27 | parentItemID: targetItem.id, 28 | title, 29 | }; 30 | return JSON.stringify({result: await Zotero.Attachments.linkFromURL(itemData)}); 31 | } 32 | --------------------------------------------------------------------------------