Compare commits
59 Commits
dependabot
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed129f2a45 | ||
|
|
2a9125aa99 | ||
|
|
5d4dde170a | ||
|
|
2d10915c7d | ||
|
|
18fac41190 | ||
|
|
f653f8a11a | ||
|
|
28cf7c86de | ||
|
|
6edfda0388 | ||
|
|
6056c4208f | ||
|
|
5cc74c4986 | ||
|
|
a26f5fd0a9 | ||
|
|
eec0d47a96 | ||
|
|
df847e59be | ||
|
|
50956fefff | ||
|
|
f93d22d29b | ||
|
|
a9d13a20c6 | ||
|
|
3c9695fa71 | ||
|
|
735da8a43a | ||
|
|
b56669ae8f | ||
|
|
66c0460513 | ||
|
|
f52eb9f100 | ||
|
|
0075dc8a43 | ||
|
|
cd14a26c11 | ||
|
|
fdd4d20a95 | ||
|
|
2cc3ac7622 | ||
|
|
0663e5b0b1 | ||
|
|
7738410680 | ||
|
|
d1f6367825 | ||
|
|
2839b3ac53 | ||
|
|
7f6b7c54b2 | ||
|
|
fc8a4ac9d9 | ||
|
|
f5f2fd3297 | ||
|
|
be0b82f23d | ||
|
|
6f3a6d112e | ||
|
|
3d9f8a8a0d | ||
|
|
032aa74f6e | ||
|
|
57028c7ba8 | ||
|
|
4eb662ea2f | ||
|
|
56c427b26a | ||
|
|
133dde03c7 | ||
|
|
3c8f1b3db3 | ||
|
|
fb9109d87d | ||
|
|
e2885ec563 | ||
|
|
ade49628ff | ||
|
|
1208ad54a9 | ||
|
|
a56e31fa0d | ||
|
|
12e7cb9641 | ||
|
|
77e43bf378 | ||
|
|
57319f498f | ||
|
|
df7bde4757 | ||
|
|
1364288293 | ||
|
|
1572a57f22 | ||
|
|
ddb2bc6898 | ||
|
|
23ffc5fdf8 | ||
|
|
25990fd552 | ||
|
|
10e57afadb | ||
|
|
6d4e9628a5 | ||
|
|
0d67969ad8 | ||
|
|
526a18cc37 |
|
|
@ -1,41 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"root": true,
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"overrides": [],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"rules": {
|
||||
"@typescript-eslint/ban-ts-comment": [
|
||||
"warn",
|
||||
{
|
||||
"ts-expect-error": "allow-with-description",
|
||||
"ts-ignore": "allow-with-description",
|
||||
"ts-nocheck": "allow-with-description",
|
||||
"ts-check": "allow-with-description"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": ["off", { "ignoreRestArgs": true }],
|
||||
"@typescript-eslint/no-non-null-assertion": "off"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"**/build/**",
|
||||
"**/dist/**",
|
||||
"**/node_modules/**",
|
||||
"**/scripts/**",
|
||||
"**/*.js",
|
||||
"**/*.bak"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":semanticPrefixChore",
|
||||
":prHourlyLimitNone",
|
||||
":prConcurrentLimitNone",
|
||||
":enableVulnerabilityAlerts",
|
||||
":dependencyDashboard",
|
||||
"schedule:weekends"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackageNames": ["zotero-plugin-toolkit", "zotero-types"],
|
||||
"automerge": true
|
||||
}
|
||||
],
|
||||
"git-submodules": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- V**
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GitHub_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install deps
|
||||
run: npm install
|
||||
|
||||
- name: Release to GitHub
|
||||
run: |
|
||||
npm run release -- --no-increment --no-git --github.release --ci --VV
|
||||
sleep 1s
|
||||
|
||||
- name: Notify release
|
||||
uses: apexskier/github-release-commenter@v1
|
||||
continue-on-error: true
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
comment-template: |
|
||||
:rocket: _This ticket has been resolved in {release_tag}. See {release_link} for release notes._
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
**/build
|
||||
build
|
||||
logs
|
||||
node_modules
|
||||
package-lock.json
|
||||
zotero-cmd.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
zotero-cmd.json
|
||||
.DS_Store
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
build
|
||||
logs
|
||||
node_modules
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
# zotero-cmd.json
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"tabWidth": 2
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"npm": {
|
||||
"publish": false
|
||||
},
|
||||
"github": {
|
||||
"release": true,
|
||||
"assets": ["build/*.xpi"]
|
||||
},
|
||||
"hooks": {
|
||||
"before:init": "npm run lint",
|
||||
"after:bump": "npm run build",
|
||||
"after:release": "echo Successfully released ${name} v${version} to ${repo.repository}."
|
||||
}
|
||||
}
|
||||
|
|
@ -7,23 +7,16 @@
|
|||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "StartDev",
|
||||
"name": "Start",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "start-watch"]
|
||||
"runtimeArgs": ["run", "start"]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Restart",
|
||||
"name": "Build",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "restart"]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Restart in Prod Mode",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "restart-prod"]
|
||||
"runtimeArgs": ["run", "build"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@
|
|||
"editor.formatOnType": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@
|
|||
"\tremoveIfExists: ${13:true},",
|
||||
"\tcustomCheck: (doc: Document, options: ElementOptions) => ${14:true},",
|
||||
"\tchildren: [$15]",
|
||||
"}, ${16:container});"
|
||||
]
|
||||
"}, ${16:container});",
|
||||
],
|
||||
},
|
||||
"appendElement - minimum": {
|
||||
"scope": "javascript,typescript",
|
||||
"prefix": "appendElement",
|
||||
"body": "appendElement({ tag: '$1' }, $2);"
|
||||
"body": "appendElement({ tag: '$1' }, $2);",
|
||||
},
|
||||
"register Notifier": {
|
||||
"scope": "javascript,typescript",
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
"\t) => {",
|
||||
"\t\t$0",
|
||||
"\t}",
|
||||
"});"
|
||||
]
|
||||
}
|
||||
"});",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ async function startup({ id, version, resourceURI, rootURI }, reason) {
|
|||
`${rootURI}/chrome/content/scripts/__addonRef__.js`,
|
||||
ctx,
|
||||
);
|
||||
Zotero.__addonInstance__.hooks.onStartup();
|
||||
}
|
||||
|
||||
async function onMainWindowLoad({ window }, reason) {
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -0,0 +1,4 @@
|
|||
itemPaneSection-header =
|
||||
.label = TLDR
|
||||
itemPaneSection-sidenav =
|
||||
.tooltiptext = TLDR
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
itemPaneSection-header =
|
||||
.label = TLDR
|
||||
itemPaneSection-sidenav =
|
||||
.tooltiptext = TLDR
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
"applications": {
|
||||
"zotero": {
|
||||
"id": "__addonID__",
|
||||
"update_url": "__updaterdf__",
|
||||
"update_url": "__updateURL__",
|
||||
"strict_min_version": "6.999",
|
||||
"strict_max_version": "7.0.*"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,420 @@
|
|||
# Zotero Plugin Template
|
||||
|
||||
[](https://www.zotero.org)
|
||||
[](https://github.com/windingwind/zotero-plugin-template)
|
||||
|
||||
这是 [Zotero](https://www.zotero.org/) 的插件模板.
|
||||
|
||||
[English](../README.md) | [简体中文](./README-zhCN.md)
|
||||
|
||||
📖 [插件开发文档](https://zotero.yuque.com/books/share/8d230829-6004-4934-b4c6-685a7001bfa0/vec88d) (中文版,已过时)
|
||||
|
||||
[📖 Zotero 7 插件开发文档](https://www.zotero.org/support/dev/zotero_7_for_developers)
|
||||
|
||||
🛠️ [Zotero 插件工具包](https://github.com/windingwind/zotero-plugin-toolkit) | [API 文档](https://github.com/windingwind/zotero-plugin-toolkit/blob/master/docs/zotero-plugin-toolkit.md)
|
||||
|
||||
ℹ️ [Zotero 类型定义](https://github.com/windingwind/zotero-types)
|
||||
|
||||
📜 [Zotero 源代码](https://github.com/zotero/zotero)
|
||||
|
||||
📌 [Zotero 插件模板](https://github.com/windingwind/zotero-plugin-template) (即本仓库)
|
||||
|
||||
> [!tip]
|
||||
> 👁 Watch 本仓库,以及时收到修复或更新的通知.
|
||||
|
||||
## 使用此模板构建的插件
|
||||
|
||||
[](https://github.com/windingwind/zotero-better-notes)
|
||||
[](https://github.com/windingwind/zotero-pdf-preview)
|
||||
[](https://github.com/windingwind/zotero-pdf-translate)
|
||||
[](https://github.com/windingwind/zotero-tag)
|
||||
[](https://github.com/iShareStuff/ZoteroTheme)
|
||||
[](https://github.com/MuiseDestiny/zotero-reference)
|
||||
[](https://github.com/MuiseDestiny/zotero-citation)
|
||||
[](https://github.com/MuiseDestiny/ZoteroStyle)
|
||||
[](https://github.com/volatile-static/Chartero)
|
||||
[](https://github.com/l0o0/tara)
|
||||
[](https://github.com/redleafnew/delitemwithatt)
|
||||
[](https://github.com/redleafnew/zotero-updateifsE)
|
||||
[](https://github.com/northword/zotero-format-metadata)
|
||||
[](https://github.com/inciteful-xyz/inciteful-zotero-plugin)
|
||||
[](https://github.com/MuiseDestiny/zotero-gpt)
|
||||
[](https://github.com/zoushucai/zotero-journalabbr)
|
||||
[](https://github.com/MuiseDestiny/zotero-figure)
|
||||
[](https://github.com/l0o0/jasminum)
|
||||
[](https://github.com/lifan0127/ai-research-assistant)
|
||||
|
||||
[](https://github.com/daeh/zotero-markdb-connect)
|
||||
|
||||
如果你正在使用此库,我建议你将这个标志 ([](https://github.com/windingwind/zotero-plugin-template)) 放在 README 文件中:
|
||||
|
||||
```md
|
||||
[](https://github.com/windingwind/zotero-plugin-template)
|
||||
```
|
||||
|
||||
## Features 特性
|
||||
|
||||
- 事件驱动、函数式编程的可扩展框架;
|
||||
- 简单易用,开箱即用;
|
||||
- ⭐[新特性!]自动热重载!每当修改源码时,都会自动编译并重新加载插件;[详情请跳转→](#自动热重载)
|
||||
- `src/modules/examples.ts` 中有丰富的示例,涵盖了插件中常用的大部分API (使用的插件工具包 zotero-plugin-toolkit,仓库地址 https://github.com/windingwind/zotero-plugin-toolkit);
|
||||
- TypeScript 支持:
|
||||
- 为使用 JavaScript 编写的Zotero源码提供全面的类型定义支持 (使用类型定义包 zotero-types,仓库地址 https://github.com/windingwind/zotero-types);
|
||||
- 全局变量和环境设置;
|
||||
- 插件开发/构建/发布工作流:
|
||||
- 自动生成/更新插件id和版本、更新配置和设置环境变量 (`development`/`production`);
|
||||
- 自动在 Zotero 中构建和重新加载代码;
|
||||
- 自动发布到GitHub (使用[release-it](https://github.com/release-it/release-it));
|
||||
- 集成Prettier和ES Lint;
|
||||
|
||||
> [!warning]
|
||||
> Zotero本地化已升级(`dtd` 已弃用,我们将不再使用 `.properties`). 主分支将只支持 Zotero 7.0.0-beta.12 或更高版本. 如果需要支持 Zotero 6,你可能需要同时使用`dtd`、`properties` 和`ftl`. 请参考此库的 `zotero6-bootstrap` 分支.
|
||||
|
||||
## Examples 示例
|
||||
|
||||
此库提供了 [zotero-plugin-toolkit](https://github.com/windingwind/zotero-plugin-toolkit) 中API的示例.
|
||||
|
||||
在 `src/examples.ts` 中搜索`@example` 查看示例. 这些示例在 `src/hooks.ts` 中调用演示.
|
||||
|
||||
### 基本示例(Basic Examples)
|
||||
|
||||
- registerNotifier
|
||||
- registerPrefs, unregisterPrefs
|
||||
|
||||
### 快捷键示例(Shortcut Keys Examples)
|
||||
|
||||
- registerShortcuts
|
||||
- exampleShortcutLargerCallback
|
||||
- exampleShortcutSmallerCallback
|
||||
- exampleShortcutConflictionCallback
|
||||
|
||||
### UI示例(UI Examples)
|
||||
|
||||

|
||||
|
||||
- registerStyleSheet(the official make-it-red example)
|
||||
- registerRightClickMenuItem
|
||||
- registerRightClickMenuPopup
|
||||
- registerWindowMenuWithSeprator
|
||||
- registerExtraColumn
|
||||
- registerExtraColumnWithCustomCell
|
||||
- registerCustomItemBoxRow
|
||||
- registerLibraryTabPanel
|
||||
- registerReaderTabPanel
|
||||
|
||||
### 首选项面板示例(Preference Pane Examples)
|
||||
|
||||

|
||||
|
||||
- Preferences bindings
|
||||
- UI Events
|
||||
- Table
|
||||
- Locale
|
||||
|
||||
详情参见 [`src/modules/preferenceScript.ts`](./src/modules/preferenceScript.ts)
|
||||
|
||||
### 帮助示例(HelperExamples)
|
||||
|
||||

|
||||
|
||||
- dialogExample
|
||||
- clipboardExample
|
||||
- filePickerExample
|
||||
- progressWindowExample
|
||||
- vtableExample(See Preference Pane Examples)
|
||||
|
||||
### 指令行示例(PromptExamples)
|
||||
|
||||
Obsidian风格的指令输入模块,它通过接受文本来运行插件,并在弹出窗口中显示可选项.
|
||||
|
||||
使用 `Shift+P` 激活.
|
||||
|
||||

|
||||
|
||||
- registerAlertPromptExample
|
||||
|
||||
## Quick Start Guide 快速入门指南
|
||||
|
||||
### 0 前置要求(Requirement)
|
||||
|
||||
1. 安装测试版 Zotero:https://www.zotero.org/support/beta_builds
|
||||
2. 安装 Node.js(https://nodejs.org/en/)和 Git(https://git-scm.com/)
|
||||
|
||||
> [!note]
|
||||
> 本指南假定你已经对 Zotero 插件的基本结构和工作原理有初步的了解. 如果你还不了解,请先参考官方文档(https://www.zotero.org/support/dev/zotero_7_for_developers)和官方插件样例 Make It Red(仓库地址 https://github.com/zotero/make-it-red).
|
||||
|
||||
### 1 创建你的仓库(Create Your Repo)
|
||||
|
||||
1. 点击 `Use this template`;
|
||||
2. 使用 `git clone` 克隆上一步生成的仓库;
|
||||
<details >
|
||||
<summary>💡 从 GitHub Codespace 开始</summary>
|
||||
|
||||
_GitHub CodeSpace_ 使你可以直接开始开发而无需在本地下载代码/IDE/依赖.
|
||||
|
||||
重复下列步骤,仅需三十秒即可开始构建你的第一个插件!
|
||||
|
||||
- 去 [homepage](https://github.com/windingwind/zotero-plugin-template)顶部,点击绿色按钮`Use this template`,点击 `Open in codespace`, 你需要登录你的GitHub账号.
|
||||
- 等待 codespace 加载.
|
||||
|
||||
</details>
|
||||
|
||||
3. 进入项目文件夹;
|
||||
|
||||
### 2 配置模板和开发环境(Config Template Settings and Enviroment)
|
||||
|
||||
1. 修改 `./package.json` 中的设置,包括:
|
||||
|
||||
```json5
|
||||
{
|
||||
version: "", // to 0.0.0
|
||||
author: "",
|
||||
description: "",
|
||||
homepage: "",
|
||||
config: {
|
||||
addonName: "", // name to be displayed in the plugin manager
|
||||
addonID: "", // ID to avoid conflict. IMPORTANT!
|
||||
addonRef: "", // e.g. Element ID prefix
|
||||
addonInstance: "", // the plugin's root instance: Zotero.${addonInstance}
|
||||
prefsPrefix: "extensions.zotero.${addonRef}", // the prefix of prefs
|
||||
releasePage: "", // URL to releases
|
||||
updateJSON: "", // URL to update.json
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
> [!warning]
|
||||
> 注意设置 addonID 和 addonRef 以避免冲突.
|
||||
|
||||
如果你需要在GitHub以外的地方托管你的 XPI 包,请删除 `releasePage` 并添加 `updateLink`,并将值设置为你的 XPI 下载地址.
|
||||
|
||||
2. 复制 Zotero 启动配置,填入 Zotero 可执行文件路径和 profile 路径.
|
||||
|
||||
> (可选项) 此操作仅需执行一次: 使用 `/path/to/zotero -p` 启动 Zotero,创建一个新的配置文件并用作开发配置文件.
|
||||
> 将配置文件的路径 `profilePath` 放入 `zotero-cmd.json` 中,以指定要使用的配置文件.
|
||||
|
||||
```sh
|
||||
cp ./scripts/zotero-cmd-template.json ./scripts/zotero-cmd.json
|
||||
vim ./scripts/zotero-cmd.json
|
||||
```
|
||||
|
||||
3. 运行 `npm install` 以安装相关依赖
|
||||
|
||||
> 如果你使用 `pnpm` 作为包管理器,你需要添加 `public-hoist-pattern[]=*@types/bluebird*` 到`.npmrc`, 详情请查看 zotero-types(https://github.com/windingwind/zotero-types?tab=readme-ov-file#usage)的文档.
|
||||
|
||||
### 3 开始开发(Coding)
|
||||
|
||||
使用 `npm start` 启动开发服务器,它将:
|
||||
|
||||
- 在开发模式下预构建插件
|
||||
- 启动 Zotero ,并让其从 `build/` 中加载插件
|
||||
- 打开开发者工具(devtool)
|
||||
- 监听 `src/**` 和 `addon/**`.
|
||||
- 如果 `src/**` 修改了,运行 esbuild 并且重新加载
|
||||
- 如果 `addon/**` 修改了,(在开发模式下)重新构建插件并且重新加载
|
||||
|
||||
#### 自动热重载
|
||||
|
||||
厌倦了无休止的重启吗?忘掉它,拥抱热加载!
|
||||
|
||||
1. 运行 `npm start`.
|
||||
2. 编码. (是的,就这么简单)
|
||||
|
||||
当检测到 `src` 或 `addon` 中的文件修改时,插件将自动编译并重新加载.
|
||||
|
||||
<details style="text-indent: 2em">
|
||||
<summary>💡 将此功能添加到现有插件的步骤</summary>
|
||||
|
||||
1. 复制 `scripts/**.mjs`
|
||||
2. 复制 `server` 、`build` 和 `stop` 命令到 `package.json`
|
||||
3. 运行 `npm install --save-dev chokidar`
|
||||
4. 结束.
|
||||
|
||||
</details>
|
||||
|
||||
#### 在 Zotero 中 Debug
|
||||
|
||||
你还可以:
|
||||
|
||||
- 在 Tools->Developer->Run Javascript 中测试代码片段;
|
||||
|
||||
- 使用 `Zotero.debug()` 调试输出. 在 Help->Debug Output Logging->View Output 查看输出;
|
||||
|
||||
- 调试 UI. Zotero 建立在 Firefox XUL 框架之上. 使用 [XUL Explorer](https://udn.realityripple.com/docs/Archive/Mozilla/XUL_Explorer) 等软件调试 XUL UI.
|
||||
|
||||
> XUL 文档: <http://www.devdoc.net/web/developer.mozilla.org/en-US/docs/XUL.html>
|
||||
|
||||
### 4 构建(Build)
|
||||
|
||||
运行 `npm run build` 在生产模式下构建插件,构建的结果位于 `build/` 目录中.
|
||||
|
||||
`scripts/build.mjs` 的运行步骤:
|
||||
|
||||
- 创建/清空 `build/`
|
||||
- 复制 `addon/**` 到 `build/addon/**`
|
||||
- 替换占位符:使用 `replace-in-file` 去替换在 `package.json` 中定义的关键字和配置 (`xhtml`、`.flt` 等)
|
||||
- 准备本地化文件以避免冲突,查看官方文档了解更多(https://www.zotero.org/support/dev/zotero_7_for_developers#avoiding_localization_conflicts)
|
||||
- 重命名`**/*.flt` 为 `**/${addonRef}-*.flt`
|
||||
- 在每个消息前加上 `addonRef-`
|
||||
- 使用 Esbuild 来将 `.ts` 源码构建为 `.js`,从 `src/index.ts` 构建到`./build/addon/chrome/content/scripts`
|
||||
- (仅在生产模式下工作) 压缩 `./build/addon` 目录为 `./build/*.xpi`
|
||||
- (仅在生产模式下工作) 准备 `update.json` 或 `update-beta.json`
|
||||
|
||||
> [!note]
|
||||
>
|
||||
> **Dev & prod 两者有什么区别?**
|
||||
>
|
||||
> - 此环境变量存储在 `Zotero.${addonInstance}.data.env` 中,控制台输出在生产模式下被禁用.
|
||||
> - 你可以根据此变量决定用户无法查看/使用的内容.
|
||||
> - 在生产模式下,构建脚本将自动打包插件并更新 `update.json`.
|
||||
|
||||
### 5 发布(Release)
|
||||
|
||||
如果要构建和发布插件,运行如下指令:
|
||||
|
||||
```shell
|
||||
# A release-it command: version increase, npm run build, git push, and GitHub release
|
||||
# release-it: https://github.com/release-it/release-it
|
||||
npm run release
|
||||
```
|
||||
|
||||
> [!note]
|
||||
> 在此模板中,release-it 被配置为在本地升级版本、构建、推送提交和 git 标签,随后GitHub Action 将重新构建插件并将 XPI 发布到 GitHub Release.
|
||||
>
|
||||
> 如果你需要发布一个本地构建的 XPI,将 `package.json` 中的 `release-it.github.release` 设置为 `true`,然后移除 `.github/workflows/release.yml`. 此外,你还需要设置环境变量 `GITHUB_TOKEN`,获取 GitHub Token(https://github.com/settings/tokens).
|
||||
|
||||
#### 关于预发布
|
||||
|
||||
该模板将 `prerelease` 定义为插件的测试版,当你在 release-it 中选择 `prerelease` 版本 (版本号中带有 `-` ),构建脚本将创建一个 `update-beta.json` 给预发布版本使用,这将确保常规版本的用户不会自动更新到测试版,只有手动下载并安装了测试版的用户才能自动更新到下一个测试版. 当下一个正式版本更新时,脚本将同步更新 `update.json` 和 `update-beta.json`,这将使正式版和测试版用户都可以更新到最新的正式版.
|
||||
|
||||
> [!warning]
|
||||
> 严格来说,区分 Zotero 6 和 Zotero 7 兼容的插件版本应该通过 `update.json` 的 `addons.__addonID__.updates[]` 中分别配置 `applications.zotero.strict_min_version`,这样 Zotero 才能正确识别,详情在 Zotero 7 开发文档(https://www.zotero.org/support/dev/zotero_7_for_developers#updaterdf_updatesjson)获取.
|
||||
|
||||
## Details 更多细节
|
||||
|
||||
### 关于Hooks(About Hooks)
|
||||
|
||||
> 可以在 [`src/hooks.ts`](https://github.com/windingwind/zotero-plugin-template/blob/main/src/hooks.ts) 中查看更多
|
||||
|
||||
1. 当在 Zotero 中触发安装/启用/启动时,`bootstrap.js` > `startup` 被调用
|
||||
- 等待 Zotero 就绪
|
||||
- 加载 `index.js` (插件代码的主入口,从 `index.ts` 中构建)
|
||||
- 如果是 Zotero 7 以上的版本则注册资源
|
||||
2. 主入口 `index.js` 中,插件对象被注入到 `Zotero` ,并且 `hooks.ts` > `onStartup` 被调用.
|
||||
- 初始化插件需要的资源,包括通知监听器、首选项面板和UI元素.
|
||||
3. 当在 Zotero 中触发卸载/禁用时,`bootstrap.js` > `shutdown` 被调用.
|
||||
- `events.ts` > `onShutdown` 被调用. 移除 UI 元素、首选项面板或插件创建的任何内容.
|
||||
- 移除脚本并释放资源.
|
||||
|
||||
### 关于全局变量(About Global Variables)
|
||||
|
||||
> 可以在 [`src/index.ts`](https://github.com/windingwind/zotero-plugin-template/blob/main/src/index.ts)中查看更多
|
||||
|
||||
bootstrap插件在沙盒中运行,但沙盒中没有默认的全局变量,例如 `Zotero` 或 `window` 等我们曾在overlay插件环境中使用的变量.
|
||||
|
||||
此模板将以下变量注册到全局范围:
|
||||
|
||||
```ts
|
||||
Zotero, ZoteroPane, Zotero_Tabs, window, document, rootURI, ztoolkit, addon;
|
||||
```
|
||||
|
||||
### 创建元素 API(Create Elements API)
|
||||
|
||||
插件模板为 bootstrap 插件提供了一些新的API. 我们有两个原因使用这些 API,而不是使用 `createElement/createElementNS`:
|
||||
|
||||
- 在 bootstrap 模式下,插件必须在推出(禁用或卸载)时清理所有 UI 元素,这非常麻烦. 使用 `createElement`,插件模板将维护这些元素. 仅仅在退出时 `unregisterAll` .
|
||||
- Zotero 7 需要 createElement()/createElementNS() → createXULElement() 来表示其他的 XUL 元素,而 Zotero 6 并不支持 `createXULElement`. 类似于 React.createElement 的API `createElement` 检测 namespace(xul/html/svg) 并且自动创建元素,返回元素为对应的 TypeScript 元素类型.
|
||||
|
||||
```ts
|
||||
createElement(document, "div"); // returns HTMLDivElement
|
||||
createElement(document, "hbox"); // returns XUL.Box
|
||||
createElement(document, "button", { namespace: "xul" }); // manually set namespace. returns XUL.Button
|
||||
```
|
||||
|
||||
### 关于 Zotero API(About Zotero API)
|
||||
|
||||
Zotero 文档已过时且不完整,克隆 https://github.com/zotero/zotero 并全局搜索关键字.
|
||||
|
||||
> ⭐[zotero-types](https://github.com/windingwind/zotero-types) 提供了最常用的 Zotero API,在默认情况下它被包含在此模板中. 你的 IDE 将为大多数的 API 提供提醒.
|
||||
|
||||
猜你需要:查找所需 API的技巧
|
||||
|
||||
在 `.xhtml`/`.flt` 文件中搜索 UI 标签,然后在 locale 文件中找到对应的键. ,然后在 `.js`/`.jsx` 文件中搜索此键.
|
||||
|
||||
### 目录结构(Directory Structure)
|
||||
|
||||
本部分展示了模板的目录结构.
|
||||
|
||||
- 所有的 `.js/.ts` 代码都在 `./src`;
|
||||
- 插件配置文件:`./addon/manifest.json`;
|
||||
- UI 文件: `./addon/chrome/content/*.xhtml`.
|
||||
- 区域设置文件: `./addon/locale/**/*.flt`;
|
||||
- 首选项文件: `./addon/prefs.js`;
|
||||
> 不要在 `prefs.js` 中换行
|
||||
|
||||
```shell
|
||||
.
|
||||
|-- .eslintrc.json # eslint conf
|
||||
|-- .gitattributes # git conf
|
||||
|-- .github/ # github conf
|
||||
|-- .gitignore # git conf
|
||||
|-- .prettierrc # prettier conf
|
||||
|-- .release-it.json # release-it conf
|
||||
|-- .vscode # vs code conf
|
||||
| |-- extensions.json
|
||||
| |-- launch.json
|
||||
| |-- setting.json
|
||||
| `-- toolkit.code-snippets
|
||||
|-- package-lock.json # npm conf
|
||||
|-- package.json # npm conf
|
||||
|-- LICENSE
|
||||
|-- README.md
|
||||
|-- addon
|
||||
| |-- bootstrap.js # addon load/unload script, like a main.c
|
||||
| |-- chrome
|
||||
| | `-- content
|
||||
| | |-- icons/
|
||||
| | |-- preferences.xhtml # preference panel
|
||||
| | `-- zoteroPane.css
|
||||
| |-- locale # locale
|
||||
| | |-- en-US
|
||||
| | | |-- addon.ftl
|
||||
| | | `-- preferences.ftl
|
||||
| | `-- zh-CN
|
||||
| | |-- addon.ftl
|
||||
| | `-- preferences.ftl
|
||||
| |-- manifest.json # addon config
|
||||
| `-- prefs.js
|
||||
|-- build/ # build dir
|
||||
|-- scripts # scripts for dev
|
||||
| |-- build.mjs # script to build plugin
|
||||
| |-- scripts.mjs # scripts send to Zotero, such as reload, openDevTool, etc
|
||||
| |-- server.mjs # script to start a development server
|
||||
| |-- start.mjs # script to start Zotero process
|
||||
| |-- stop.mjs # script to kill Zotero process
|
||||
| |-- utils.mjs # utils functions for dev scripts
|
||||
| |-- update-template.json # template of `update.json`
|
||||
| `-- zotero-cmd-template.json # template of local env
|
||||
|-- src # source code
|
||||
| |-- addon.ts # base class
|
||||
| |-- hooks.ts # lifecycle hooks
|
||||
| |-- index.ts # main entry
|
||||
| |-- modules # sub modules
|
||||
| | |-- examples.ts
|
||||
| | `-- preferenceScript.ts
|
||||
| `-- utils # utilities
|
||||
| |-- locale.ts
|
||||
| |-- prefs.ts
|
||||
| |-- wait.ts
|
||||
| `-- window.ts
|
||||
|-- tsconfig.json # https://code.visualstudio.com/docs/languages/jsconfig
|
||||
|-- typings # ts typings
|
||||
| `-- global.d.ts
|
||||
`-- update.json
|
||||
```
|
||||
|
||||
## Disclaimer 免责声明
|
||||
|
||||
在 AGPL 下使用此代码. 不提供任何保证. 遵守你所在地区的法律!
|
||||
|
||||
如果你想更改许可,请通过 <wyzlshx@foxmail.com> 与我联系.
|
||||
133
package.json
133
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "zotero-tldr",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.7",
|
||||
"description": "TLDR(too long; didn't read) from sematic scholar",
|
||||
"config": {
|
||||
"addonName": "Zotero TLDR",
|
||||
|
|
@ -8,26 +8,17 @@
|
|||
"addonRef": "zoterotldr",
|
||||
"addonInstance": "ZoteroTLDR",
|
||||
"prefsPrefix": "extensions.zotero.zoterotldr",
|
||||
"releasepage": "https://github.com/syt2/zotero-tldr/releases/latest/download/zotero-tldr.xpi",
|
||||
"updaterdf": "https://raw.githubusercontent.com/syt2/zotero-tldr/main/update.json"
|
||||
"releasepage": "https://github.com/syt2/zotero-tldr/releases",
|
||||
"updateJSON": "https://raw.githubusercontent.com/syt2/zotero-tldr/main/update.json"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"build-dev": "cross-env NODE_ENV=development node scripts/build.mjs",
|
||||
"build-prod": "cross-env NODE_ENV=production node scripts/build.mjs",
|
||||
"build": "concurrently -c auto npm:build-prod npm:tsc",
|
||||
"tsc": "tsc --noEmit",
|
||||
"start": "node scripts/start.mjs",
|
||||
"start-watch": "npm run build-dev && concurrently -c auto npm:start npm:watch",
|
||||
"start": "node scripts/server.mjs",
|
||||
"build": "tsc --noEmit && node scripts/build.mjs production",
|
||||
"stop": "node scripts/stop.mjs",
|
||||
"restart-dev": "npm run build-dev && npm run stop && npm run start",
|
||||
"restart-prod": "npm run build-prod && npm run stop && npm run start",
|
||||
"restart": "npm run restart-dev",
|
||||
"reload": "npm run build-dev && node scripts/reload.mjs",
|
||||
"watch": "chokidar \"src/**\" \"addon/**\" -c \"npm run reload\"",
|
||||
"release": "release-it",
|
||||
"lint": "prettier --write . && eslint . --ext .ts --fix",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"release": "release-it --only-version --preReleaseId=beta",
|
||||
"update-deps": "npm update --save"
|
||||
},
|
||||
"repository": {
|
||||
|
|
@ -41,23 +32,103 @@
|
|||
},
|
||||
"homepage": "https://github.com/syt2/zotero-tldr#readme",
|
||||
"dependencies": {
|
||||
"zotero-plugin-toolkit": "^2.2.2"
|
||||
"zotero-plugin-toolkit": "^2.3.29"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.4.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"chokidar-cli": "^3.0.0",
|
||||
"compressing": "^1.9.0",
|
||||
"concurrently": "^8.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.18.12",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"prettier": "^3.0.0",
|
||||
"release-it": "^16.1.0",
|
||||
"replace-in-file": "^7.0.1",
|
||||
"typescript": "^5.1.6",
|
||||
"zotero-types": "^1.0.16"
|
||||
"@types/node": "^20.10.4",
|
||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
||||
"@typescript-eslint/parser": "^7.1.1",
|
||||
"chokidar": "^3.5.3",
|
||||
"compressing": "^1.10.0",
|
||||
"esbuild": "^0.20.1",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"prettier": "^3.1.1",
|
||||
"release-it": "^17.0.1",
|
||||
"replace-in-file": "^7.0.2",
|
||||
"typescript": "^5.3.3",
|
||||
"zotero-types": "^1.3.10"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"root": true,
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"overrides": [],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/ban-ts-comment": [
|
||||
"warn",
|
||||
{
|
||||
"ts-expect-error": "allow-with-description",
|
||||
"ts-ignore": "allow-with-description",
|
||||
"ts-nocheck": "allow-with-description",
|
||||
"ts-check": "allow-with-description"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": [
|
||||
"off",
|
||||
{
|
||||
"ignoreRestArgs": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-non-null-assertion": "off"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"**/build/**",
|
||||
"**/logs/**",
|
||||
"**/dist/**",
|
||||
"**/node_modules/**",
|
||||
"**/scripts/**",
|
||||
"**/*.js",
|
||||
"**/*.bak"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"endOfLine": "lf",
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.xhtml"
|
||||
],
|
||||
"options": {
|
||||
"htmlWhitespaceSensitivity": "css"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"release-it": {
|
||||
"git": {
|
||||
"tagName": "V${version}"
|
||||
},
|
||||
"npm": {
|
||||
"publish": false
|
||||
},
|
||||
"github": {
|
||||
"release": false,
|
||||
"assets": [
|
||||
"build/*.xpi"
|
||||
]
|
||||
},
|
||||
"hooks": {
|
||||
"before:init": "npm run lint",
|
||||
"after:bump": "npm run build"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,121 +1,29 @@
|
|||
import { build } from "esbuild";
|
||||
import { zip } from "compressing";
|
||||
import path from "path";
|
||||
import details from "../package.json" assert { type: "json" };
|
||||
import {
|
||||
existsSync,
|
||||
lstatSync,
|
||||
writeFileSync,
|
||||
readFileSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
renameSync,
|
||||
} from "fs";
|
||||
Logger,
|
||||
clearFolder,
|
||||
copyFileSync,
|
||||
copyFolderRecursiveSync,
|
||||
dateFormat,
|
||||
} from "./utils.mjs";
|
||||
import { zip } from "compressing";
|
||||
import { build } from "esbuild";
|
||||
import { existsSync, readdirSync, renameSync } from "fs";
|
||||
import path from "path";
|
||||
import { env, exit } from "process";
|
||||
import replaceInFile from "replace-in-file";
|
||||
|
||||
const { replaceInFileSync } = replaceInFile;
|
||||
import details from "../package.json" assert { type: "json" };
|
||||
|
||||
const { name, author, description, homepage, version, config } = details;
|
||||
process.env.NODE_ENV =
|
||||
process.argv[2] === "production" ? "production" : "development";
|
||||
|
||||
const t = new Date();
|
||||
const buildTime = dateFormat("YYYY-mm-dd HH:MM:SS", new Date());
|
||||
const buildDir = "build";
|
||||
|
||||
const { name, author, description, homepage, version, config } = details;
|
||||
const isPreRelease = version.includes("-");
|
||||
|
||||
function copyFileSync(source, target) {
|
||||
var targetFile = target;
|
||||
|
||||
// If target is a directory, a new file with the same name will be created
|
||||
if (existsSync(target)) {
|
||||
if (lstatSync(target).isDirectory()) {
|
||||
targetFile = path.join(target, path.basename(source));
|
||||
}
|
||||
}
|
||||
|
||||
writeFileSync(targetFile, readFileSync(source));
|
||||
}
|
||||
|
||||
function copyFolderRecursiveSync(source, target) {
|
||||
var files = [];
|
||||
|
||||
// Check if folder needs to be created or integrated
|
||||
var targetFolder = path.join(target, path.basename(source));
|
||||
if (!existsSync(targetFolder)) {
|
||||
mkdirSync(targetFolder);
|
||||
}
|
||||
|
||||
// Copy
|
||||
if (lstatSync(source).isDirectory()) {
|
||||
files = readdirSync(source);
|
||||
files.forEach(function (file) {
|
||||
var curSource = path.join(source, file);
|
||||
if (lstatSync(curSource).isDirectory()) {
|
||||
copyFolderRecursiveSync(curSource, targetFolder);
|
||||
} else {
|
||||
copyFileSync(curSource, targetFolder);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function clearFolder(target) {
|
||||
if (existsSync(target)) {
|
||||
rmSync(target, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
mkdirSync(target, { recursive: true });
|
||||
}
|
||||
|
||||
function dateFormat(fmt, date) {
|
||||
let ret;
|
||||
const opt = {
|
||||
"Y+": date.getFullYear().toString(),
|
||||
"m+": (date.getMonth() + 1).toString(),
|
||||
"d+": date.getDate().toString(),
|
||||
"H+": date.getHours().toString(),
|
||||
"M+": date.getMinutes().toString(),
|
||||
"S+": date.getSeconds().toString(),
|
||||
};
|
||||
for (let k in opt) {
|
||||
ret = new RegExp("(" + k + ")").exec(fmt);
|
||||
if (ret) {
|
||||
fmt = fmt.replace(
|
||||
ret[1],
|
||||
ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, "0"),
|
||||
);
|
||||
}
|
||||
}
|
||||
return fmt;
|
||||
}
|
||||
|
||||
function renameLocaleFiles() {
|
||||
const localeDir = path.join(buildDir, "addon/locale");
|
||||
const localeFolders = readdirSync(localeDir, { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name);
|
||||
|
||||
for (const localeSubFolder of localeFolders) {
|
||||
const localeSubDir = path.join(localeDir, localeSubFolder);
|
||||
const localeSubFiles = readdirSync(localeSubDir, {
|
||||
withFileTypes: true,
|
||||
})
|
||||
.filter((dirent) => dirent.isFile())
|
||||
.map((dirent) => dirent.name);
|
||||
|
||||
for (const localeSubFile of localeSubFiles) {
|
||||
if (localeSubFile.endsWith(".ftl")) {
|
||||
renameSync(
|
||||
path.join(localeSubDir, localeSubFile),
|
||||
path.join(localeSubDir, `${config.addonRef}-${localeSubFile}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function replaceString() {
|
||||
function replaceString(buildTime) {
|
||||
const replaceFrom = [
|
||||
/__author__/g,
|
||||
/__description__/g,
|
||||
|
|
@ -125,15 +33,20 @@ function replaceString() {
|
|||
];
|
||||
const replaceTo = [author, description, homepage, version, buildTime];
|
||||
|
||||
config.updateURL = isPreRelease
|
||||
? config.updateJSON.replace("update.json", "update-beta.json")
|
||||
: config.updateJSON;
|
||||
|
||||
replaceFrom.push(
|
||||
...Object.keys(config).map((k) => new RegExp(`__${k}__`, "g")),
|
||||
);
|
||||
replaceTo.push(...Object.values(config));
|
||||
|
||||
const optionsAddon = {
|
||||
const replaceResult = replaceInFileSync({
|
||||
files: [
|
||||
`${buildDir}/addon/**/*.xhtml`,
|
||||
`${buildDir}/addon/**/*.html`,
|
||||
`${buildDir}/addon/**/*.css`,
|
||||
`${buildDir}/addon/**/*.json`,
|
||||
`${buildDir}/addon/prefs.js`,
|
||||
`${buildDir}/addon/manifest.json`,
|
||||
|
|
@ -142,137 +55,189 @@ function replaceString() {
|
|||
from: replaceFrom,
|
||||
to: replaceTo,
|
||||
countMatches: true,
|
||||
};
|
||||
|
||||
if (!isPreRelease) {
|
||||
optionsAddon.files.push("update.json");
|
||||
}
|
||||
|
||||
const replaceResult = replaceInFileSync(optionsAddon);
|
||||
|
||||
const localeMessage = new Set();
|
||||
const localeMessageMiss = new Set();
|
||||
|
||||
const replaceResultFlt = replaceInFileSync({
|
||||
files: [`${buildDir}/addon/locale/**/*.ftl`],
|
||||
processor: (fltContent) => {
|
||||
const lines = fltContent.split("\n");
|
||||
const prefixedLines = lines.map((line) => {
|
||||
// https://regex101.com/r/lQ9x5p/1
|
||||
const match = line.match(
|
||||
/^(?<message>[a-zA-Z]\S*)([ ]*=[ ]*)(?<pattern>.*)$/m,
|
||||
);
|
||||
if (match) {
|
||||
localeMessage.add(match.groups.message);
|
||||
return `${config.addonRef}-${line}`;
|
||||
} else {
|
||||
return line;
|
||||
}
|
||||
});
|
||||
return prefixedLines.join("\n");
|
||||
},
|
||||
});
|
||||
|
||||
const replaceResultXhtml = replaceInFileSync({
|
||||
files: [`${buildDir}/addon/**/*.xhtml`],
|
||||
// Logger.debug(
|
||||
// "[Build] Run replace in ",
|
||||
// replaceResult.filter((f) => f.hasChanged).map((f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`),
|
||||
// );
|
||||
}
|
||||
|
||||
function prepareLocaleFiles() {
|
||||
// Prefix Fluent messages in xhtml
|
||||
const MessagesInHTML = new Set();
|
||||
replaceInFileSync({
|
||||
files: [`${buildDir}/addon/**/*.xhtml`, `${buildDir}/addon/**/*.html`],
|
||||
processor: (input) => {
|
||||
const matchs = [...input.matchAll(/(data-l10n-id)="(\S*)"/g)];
|
||||
matchs.map((match) => {
|
||||
if (localeMessage.has(match[2])) {
|
||||
input = input.replace(
|
||||
match[0],
|
||||
`${match[1]}="${config.addonRef}-${match[2]}"`,
|
||||
);
|
||||
} else {
|
||||
localeMessageMiss.add(match[2]);
|
||||
}
|
||||
input = input.replace(
|
||||
match[0],
|
||||
`${match[1]}="${config.addonRef}-${match[2]}"`,
|
||||
);
|
||||
MessagesInHTML.add(match[2]);
|
||||
});
|
||||
return input;
|
||||
},
|
||||
});
|
||||
|
||||
console.log(
|
||||
"[Build] Run replace in ",
|
||||
replaceResult
|
||||
.filter((f) => f.hasChanged)
|
||||
.map((f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`),
|
||||
replaceResultFlt.filter((f) => f.hasChanged).map((f) => `${f.file} : OK`),
|
||||
replaceResultXhtml.filter((f) => f.hasChanged).map((f) => `${f.file} : OK`),
|
||||
);
|
||||
// Walk the sub folders of `build/addon/locale`
|
||||
const localesPath = path.join(buildDir, "addon/locale"),
|
||||
localeNames = readdirSync(localesPath, { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name);
|
||||
|
||||
if (localeMessageMiss.size !== 0) {
|
||||
console.warn(
|
||||
`[Build] [Warn] Fluent message [${new Array(
|
||||
...localeMessageMiss,
|
||||
)}] do not exsit in addon's locale files.`,
|
||||
);
|
||||
for (const localeName of localeNames) {
|
||||
const localePath = path.join(localesPath, localeName);
|
||||
const ftlFiles = readdirSync(localePath, {
|
||||
withFileTypes: true,
|
||||
})
|
||||
.filter((dirent) => dirent.isFile())
|
||||
.map((dirent) => dirent.name);
|
||||
|
||||
// rename *.ftl to addonRef-*.ftl
|
||||
for (const ftlFile of ftlFiles) {
|
||||
if (ftlFile.endsWith(".ftl")) {
|
||||
renameSync(
|
||||
path.join(localePath, ftlFile),
|
||||
path.join(localePath, `${config.addonRef}-${ftlFile}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix Fluent messages in each ftl
|
||||
const MessageInThisLang = new Set();
|
||||
replaceInFileSync({
|
||||
files: [`${buildDir}/addon/locale/${localeName}/*.ftl`],
|
||||
processor: (fltContent) => {
|
||||
const lines = fltContent.split("\n");
|
||||
const prefixedLines = lines.map((line) => {
|
||||
// https://regex101.com/r/lQ9x5p/1
|
||||
const match = line.match(
|
||||
/^(?<message>[a-zA-Z]\S*)([ ]*=[ ]*)(?<pattern>.*)$/m,
|
||||
);
|
||||
if (match) {
|
||||
MessageInThisLang.add(match.groups.message);
|
||||
return `${config.addonRef}-${line}`;
|
||||
} else {
|
||||
return line;
|
||||
}
|
||||
});
|
||||
return prefixedLines.join("\n");
|
||||
},
|
||||
});
|
||||
|
||||
// If a message in xhtml but not in ftl of current language, log it
|
||||
MessagesInHTML.forEach((message) => {
|
||||
if (!MessageInThisLang.has(message)) {
|
||||
Logger.error(`[Build] ${message} don't exist in ${localeName}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function esbuild() {
|
||||
await build({
|
||||
entryPoints: ["src/index.ts"],
|
||||
define: {
|
||||
__env__: `"${env.NODE_ENV}"`,
|
||||
},
|
||||
bundle: true,
|
||||
target: "firefox102",
|
||||
outfile: path.join(
|
||||
buildDir,
|
||||
`addon/chrome/content/scripts/${config.addonRef}.js`,
|
||||
),
|
||||
// Don't turn minify on
|
||||
// minify: true,
|
||||
}).catch(() => exit(1));
|
||||
function prepareUpdateJson() {
|
||||
// If it is a pre-release, use update-beta.json
|
||||
if (!isPreRelease) {
|
||||
copyFileSync("scripts/update-template.json", "update.json");
|
||||
}
|
||||
if (existsSync("update-beta.json") || isPreRelease) {
|
||||
copyFileSync("scripts/update-template.json", "update-beta.json");
|
||||
}
|
||||
|
||||
const updateLink =
|
||||
config.updateLink ?? isPreRelease
|
||||
? `${config.releasePage}/download/v${version}/${name}.xpi`
|
||||
: `${config.releasePage}/latest/download/${name}.xpi`;
|
||||
|
||||
const replaceResult = replaceInFileSync({
|
||||
files: [
|
||||
"update-beta.json",
|
||||
isPreRelease ? "pass" : "update.json",
|
||||
`${buildDir}/addon/manifest.json`,
|
||||
],
|
||||
from: [
|
||||
/__addonID__/g,
|
||||
/__buildVersion__/g,
|
||||
/__updateLink__/g,
|
||||
/__updateURL__/g,
|
||||
],
|
||||
to: [config.addonID, version, updateLink, config.updateURL],
|
||||
countMatches: true,
|
||||
});
|
||||
|
||||
Logger.debug(
|
||||
`[Build] Prepare Update.json for ${
|
||||
isPreRelease
|
||||
? "\u001b[31m Prerelease \u001b[0m"
|
||||
: "\u001b[32m Release \u001b[0m"
|
||||
}`,
|
||||
replaceResult
|
||||
.filter((f) => f.hasChanged)
|
||||
.map((f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`),
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(
|
||||
export const esbuildOptions = {
|
||||
entryPoints: ["src/index.ts"],
|
||||
define: {
|
||||
__env__: `"${env.NODE_ENV}"`,
|
||||
},
|
||||
bundle: true,
|
||||
target: "firefox102",
|
||||
outfile: path.join(
|
||||
buildDir,
|
||||
`addon/chrome/content/scripts/${config.addonRef}.js`,
|
||||
),
|
||||
// Don't turn minify on
|
||||
minify: env.NODE_ENV === "production",
|
||||
};
|
||||
|
||||
export async function main() {
|
||||
const t = new Date();
|
||||
const buildTime = dateFormat("YYYY-mm-dd HH:MM:SS", new Date());
|
||||
|
||||
Logger.info(
|
||||
`[Build] BUILD_DIR=${buildDir}, VERSION=${version}, BUILD_TIME=${buildTime}, ENV=${[
|
||||
env.NODE_ENV,
|
||||
]}`,
|
||||
);
|
||||
|
||||
clearFolder(buildDir);
|
||||
|
||||
copyFolderRecursiveSync("addon", buildDir);
|
||||
|
||||
if (isPreRelease) {
|
||||
console.log(
|
||||
"[Build] [Warn] Running in pre-release mode. update.json will not be replaced.",
|
||||
Logger.debug("[Build] Replacing");
|
||||
replaceString(buildTime);
|
||||
|
||||
Logger.debug("[Build] Preparing locale files");
|
||||
prepareLocaleFiles();
|
||||
|
||||
Logger.debug("[Build] Running esbuild");
|
||||
await build(esbuildOptions);
|
||||
|
||||
Logger.debug("[Build] Addon prepare OK");
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
Logger.debug("[Build] Packing Addon");
|
||||
await zip.compressDir(
|
||||
path.join(buildDir, "addon"),
|
||||
path.join(buildDir, `${name}.xpi`),
|
||||
{
|
||||
ignoreBase: true,
|
||||
},
|
||||
);
|
||||
|
||||
prepareUpdateJson();
|
||||
|
||||
Logger.debug(
|
||||
`[Build] Finished in ${(new Date().getTime() - t.getTime()) / 1000} s.`,
|
||||
);
|
||||
} else {
|
||||
copyFileSync("update-template.json", "update.json");
|
||||
}
|
||||
|
||||
await esbuild();
|
||||
|
||||
console.log("[Build] Run esbuild OK");
|
||||
|
||||
replaceString();
|
||||
|
||||
console.log("[Build] Replace OK");
|
||||
|
||||
// Walk the builds/addon/locale folder's sub folders and rename *.ftl to addonRef-*.ftl
|
||||
renameLocaleFiles();
|
||||
|
||||
console.log("[Build] Addon prepare OK");
|
||||
|
||||
await zip.compressDir(
|
||||
path.join(buildDir, "addon"),
|
||||
path.join(buildDir, `${name}.xpi`),
|
||||
{
|
||||
ignoreBase: true,
|
||||
},
|
||||
);
|
||||
|
||||
console.log("[Build] Addon pack OK");
|
||||
console.log(
|
||||
`[Build] Finished in ${(new Date().getTime() - t.getTime()) / 1000} s.`,
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.log(err);
|
||||
exit(1);
|
||||
});
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
main().catch((err) => {
|
||||
Logger.error(err);
|
||||
exit(1);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
import { exit } from "process";
|
||||
import { execSync } from "child_process";
|
||||
import details from "../package.json" assert { type: "json" };
|
||||
import cmd from "./zotero-cmd.json" assert { type: "json" };
|
||||
|
||||
const { addonID, addonName } = details.config;
|
||||
const { version } = details;
|
||||
const { zoteroBinPath, profilePath } = cmd.exec;
|
||||
|
||||
const startZotero = `"${zoteroBinPath}" --debugger --purgecaches -profile "${profilePath}"`;
|
||||
|
||||
const script = `
|
||||
(async () => {
|
||||
Services.obs.notifyObservers(null, "startupcache-invalidate", null);
|
||||
const { AddonManager } = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
|
||||
const addon = await AddonManager.getAddonByID("${addonID}");
|
||||
await addon.reload();
|
||||
const progressWindow = new Zotero.ProgressWindow({ closeOnClick: true });
|
||||
progressWindow.changeHeadline("${addonName} Hot Reload");
|
||||
progressWindow.progress = new progressWindow.ItemProgress(
|
||||
"chrome://zotero/skin/tick.png",
|
||||
"VERSION=${version}, BUILD=${new Date().toLocaleString()}. By zotero-plugin-toolkit"
|
||||
);
|
||||
progressWindow.progress.setProgress(100);
|
||||
progressWindow.show();
|
||||
progressWindow.startCloseTimer(5000);
|
||||
})()`;
|
||||
|
||||
const url = `zotero://ztoolkit-debug/?run=${encodeURIComponent(script)}`;
|
||||
|
||||
const command = `${startZotero} -url "${url}"`;
|
||||
|
||||
execSync(command);
|
||||
exit(0);
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import details from "../package.json" assert { type: "json" };
|
||||
|
||||
const { addonID, addonName } = details.config;
|
||||
const { version } = details;
|
||||
|
||||
export const reloadScript = `
|
||||
(async () => {
|
||||
Services.obs.notifyObservers(null, "startupcache-invalidate", null);
|
||||
const { AddonManager } = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
|
||||
const addon = await AddonManager.getAddonByID("${addonID}");
|
||||
await addon.reload();
|
||||
const progressWindow = new Zotero.ProgressWindow({ closeOnClick: true });
|
||||
progressWindow.changeHeadline("${addonName} Hot Reload");
|
||||
progressWindow.progress = new progressWindow.ItemProgress(
|
||||
"chrome://zotero/skin/tick.png",
|
||||
"VERSION=${version}, BUILD=${new Date().toLocaleString()}. By zotero-plugin-toolkit"
|
||||
);
|
||||
progressWindow.progress.setProgress(100);
|
||||
progressWindow.show();
|
||||
progressWindow.startCloseTimer(5000);
|
||||
})()`;
|
||||
|
||||
export const openDevToolScript = `
|
||||
(async () => {
|
||||
|
||||
// const { BrowserToolboxLauncher } = ChromeUtils.import(
|
||||
// "resource://devtools/client/framework/browser-toolbox/Launcher.jsm",
|
||||
// );
|
||||
// BrowserToolboxLauncher.init();
|
||||
// TODO: Use the above code to open the devtool after https://github.com/zotero/zotero/pull/3387
|
||||
|
||||
Zotero.Prefs.set("devtools.debugger.remote-enabled", true, true);
|
||||
Zotero.Prefs.set("devtools.debugger.remote-port", 6100, true);
|
||||
Zotero.Prefs.set("devtools.debugger.prompt-connection", false, true);
|
||||
Zotero.Prefs.set("devtools.debugger.chrome-debugging-websocket", false, true);
|
||||
|
||||
env =
|
||||
Services.env ||
|
||||
Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
|
||||
|
||||
env.set("MOZ_BROWSER_TOOLBOX_PORT", 6100);
|
||||
Zotero.openInViewer(
|
||||
"chrome://devtools/content/framework/browser-toolbox/window.html",
|
||||
{
|
||||
onLoad: (doc) => {
|
||||
doc.querySelector("#status-message-container").style.visibility =
|
||||
"collapse";
|
||||
let toolboxBody;
|
||||
waitUntil(
|
||||
() => {
|
||||
toolboxBody = doc
|
||||
.querySelector(".devtools-toolbox-browsertoolbox-iframe")
|
||||
?.contentDocument?.querySelector(".theme-body");
|
||||
return toolboxBody;
|
||||
},
|
||||
() => {
|
||||
toolboxBody.style = "pointer-events: all !important";
|
||||
}
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function waitUntil(condition, callback, interval = 100, timeout = 10000) {
|
||||
const start = Date.now();
|
||||
const intervalId = setInterval(() => {
|
||||
if (condition()) {
|
||||
clearInterval(intervalId);
|
||||
callback();
|
||||
} else if (Date.now() - start > timeout) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
}, interval);
|
||||
}
|
||||
})()`;
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import { main as build, esbuildOptions } from "./build.mjs";
|
||||
import { openDevToolScript, reloadScript } from "./scripts.mjs";
|
||||
import { main as startZotero } from "./start.mjs";
|
||||
import { Logger } from "./utils.mjs";
|
||||
import cmd from "./zotero-cmd.json" assert { type: "json" };
|
||||
import { execSync } from "child_process";
|
||||
import chokidar from "chokidar";
|
||||
import { context } from "esbuild";
|
||||
import { exit } from "process";
|
||||
|
||||
process.env.NODE_ENV = "development";
|
||||
|
||||
const { zoteroBinPath, profilePath } = cmd.exec;
|
||||
|
||||
const startZoteroCmd = `"${zoteroBinPath}" --debugger --purgecaches -profile "${profilePath}"`;
|
||||
|
||||
async function watch() {
|
||||
const watcher = chokidar.watch(["src/**", "addon/**"], {
|
||||
ignored: /(^|[\/\\])\../, // ignore dotfiles
|
||||
persistent: true,
|
||||
});
|
||||
|
||||
let esbuildCTX = await context(esbuildOptions);
|
||||
|
||||
watcher
|
||||
.on("ready", () => {
|
||||
Logger.info("Server Ready! \n");
|
||||
})
|
||||
.on("change", async (path) => {
|
||||
Logger.info(`${path} changed.`);
|
||||
if (path.startsWith("src")) {
|
||||
await esbuildCTX.rebuild();
|
||||
} else if (path.startsWith("addon")) {
|
||||
await build()
|
||||
// Do not abort the watcher when errors occur in builds triggered by the watcher.
|
||||
.catch((err) => {
|
||||
Logger.error(err);
|
||||
});
|
||||
}
|
||||
// reload
|
||||
reload();
|
||||
})
|
||||
.on("error", (err) => {
|
||||
Logger.error("Server start failed!", err);
|
||||
});
|
||||
}
|
||||
|
||||
function reload() {
|
||||
Logger.debug("Reloading...");
|
||||
const url = `zotero://ztoolkit-debug/?run=${encodeURIComponent(
|
||||
reloadScript,
|
||||
)}`;
|
||||
const command = `${startZoteroCmd} -url "${url}"`;
|
||||
execSync(command);
|
||||
}
|
||||
|
||||
function openDevTool() {
|
||||
Logger.debug("Open dev tools...");
|
||||
const url = `zotero://ztoolkit-debug/?run=${encodeURIComponent(
|
||||
openDevToolScript,
|
||||
)}`;
|
||||
const command = `${startZoteroCmd} -url "${url}"`;
|
||||
execSync(command);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// build
|
||||
await build();
|
||||
|
||||
// start Zotero
|
||||
startZotero(openDevTool);
|
||||
|
||||
// watch
|
||||
await watch();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
Logger.error(err);
|
||||
// execSync("node scripts/stop.mjs");
|
||||
exit(1);
|
||||
});
|
||||
|
||||
process.on("SIGINT", (code) => {
|
||||
execSync("node scripts/stop.mjs");
|
||||
Logger.info(`Server terminated with signal ${code}.`);
|
||||
exit(0);
|
||||
});
|
||||
|
|
@ -1,33 +1,39 @@
|
|||
import { execSync } from "child_process";
|
||||
import { exit } from "process";
|
||||
import { existsSync, writeFileSync, readFileSync, mkdirSync } from "fs";
|
||||
import path from "path";
|
||||
import details from "../package.json" assert { type: "json" };
|
||||
import { Logger } from "./utils.mjs";
|
||||
import cmd from "./zotero-cmd.json" assert { type: "json" };
|
||||
import { spawn } from "child_process";
|
||||
import { existsSync, readFileSync, writeFileSync, rmSync } from "fs";
|
||||
import { clearFolder } from "./utils.mjs";
|
||||
import path from "path";
|
||||
import { exit } from "process";
|
||||
|
||||
const { addonID } = details.config;
|
||||
const { zoteroBinPath, profilePath, dataDir } = cmd.exec;
|
||||
|
||||
// Keep in sync with the addon's onStartup
|
||||
const loadDevToolWhen = `Plugin ${addonID} startup`;
|
||||
|
||||
const logPath = "logs";
|
||||
const logFilePath = path.join(logPath, "zotero.log");
|
||||
|
||||
if (!existsSync(zoteroBinPath)) {
|
||||
throw new Error("Zotero binary does not exist.");
|
||||
}
|
||||
|
||||
if (existsSync(profilePath)) {
|
||||
if (!existsSync(profilePath)) {
|
||||
throw new Error("The given Zotero profile does not exist.");
|
||||
}
|
||||
|
||||
function prepareDevEnv() {
|
||||
const addonProxyFilePath = path.join(profilePath, `extensions/${addonID}`);
|
||||
const buildPath = path.resolve("build/addon");
|
||||
|
||||
if (!existsSync(path.join(buildPath, "./manifest.json"))) {
|
||||
throw new Error(
|
||||
`The built file does not exist, maybe you need to build the addon first.`,
|
||||
);
|
||||
}
|
||||
|
||||
function writeAddonProxyFile() {
|
||||
writeFileSync(addonProxyFilePath, buildPath);
|
||||
console.log(
|
||||
`[info] Addon proxy file has been updated.
|
||||
File path: ${addonProxyFilePath}
|
||||
Addon path: ${buildPath} `,
|
||||
Logger.debug(
|
||||
`Addon proxy file has been updated.
|
||||
File path: ${addonProxyFilePath}
|
||||
Addon path: ${buildPath} `,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -36,15 +42,14 @@ if (existsSync(profilePath)) {
|
|||
writeAddonProxyFile();
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
existsSync(profilePath) &&
|
||||
!existsSync(path.join(profilePath, "extensions"))
|
||||
) {
|
||||
mkdirSync(path.join(profilePath, "extensions"));
|
||||
}
|
||||
writeAddonProxyFile();
|
||||
}
|
||||
|
||||
const addonXpiFilePath = path.join(profilePath, `extensions/${addonID}.xpi`);
|
||||
if (existsSync(addonXpiFilePath)) {
|
||||
rmSync(addonXpiFilePath);
|
||||
}
|
||||
|
||||
const prefsPath = path.join(profilePath, "prefs.js");
|
||||
if (existsSync(prefsPath)) {
|
||||
const PrefsLines = readFileSync(prefsPath, "utf-8").split("\n");
|
||||
|
|
@ -56,19 +61,59 @@ if (existsSync(profilePath)) {
|
|||
return;
|
||||
}
|
||||
if (line.includes("extensions.zotero.dataDir") && dataDir !== "") {
|
||||
return `user_pref("extensions.zotero.dataDir", "${dataDir}");`;
|
||||
return `user_pref("extensions.zotero.dataDir", "${dataDir.replace(/\\\\?/g, "\\\\")}");`;
|
||||
}
|
||||
return line;
|
||||
});
|
||||
const updatedPrefs = filteredLines.join("\n");
|
||||
writeFileSync(prefsPath, updatedPrefs, "utf-8");
|
||||
console.log("[info] The <profile>/prefs.js has been modified.");
|
||||
Logger.debug("The <profile>/prefs.js has been modified.");
|
||||
}
|
||||
} else {
|
||||
throw new Error("The given Zotero profile does not exist.");
|
||||
}
|
||||
|
||||
const startZotero = `"${zoteroBinPath}" --debugger --purgecaches -profile "${profilePath}"`;
|
||||
function prepareLog() {
|
||||
clearFolder(logPath);
|
||||
writeFileSync(logFilePath, "");
|
||||
}
|
||||
|
||||
execSync(startZotero);
|
||||
exit(0);
|
||||
export function main(callback) {
|
||||
let isZoteroReady = false;
|
||||
|
||||
prepareDevEnv();
|
||||
|
||||
prepareLog();
|
||||
|
||||
const zoteroProcess = spawn(zoteroBinPath, [
|
||||
"--debugger",
|
||||
"--purgecaches",
|
||||
"-profile",
|
||||
profilePath,
|
||||
]);
|
||||
|
||||
zoteroProcess.stdout.on("data", (data) => {
|
||||
if (!isZoteroReady && data.toString().includes(loadDevToolWhen)) {
|
||||
isZoteroReady = true;
|
||||
callback();
|
||||
}
|
||||
writeFileSync(logFilePath, data, {
|
||||
flag: "a",
|
||||
});
|
||||
});
|
||||
|
||||
zoteroProcess.stderr.on("data", (data) => {
|
||||
writeFileSync(logFilePath, data, {
|
||||
flag: "a",
|
||||
});
|
||||
});
|
||||
|
||||
zoteroProcess.on("close", (code) => {
|
||||
Logger.info(`Zotero terminated with code ${code}.`);
|
||||
exit(0);
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
// Handle interrupt signal (Ctrl+C) to gracefully terminate Zotero process
|
||||
zoteroProcess.kill();
|
||||
exit();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,26 @@
|
|||
import process from "process";
|
||||
import { execSync } from "child_process";
|
||||
import { Logger, isRunning } from "./utils.mjs";
|
||||
import cmd from "./zotero-cmd.json" assert { type: "json" };
|
||||
import { execSync } from "child_process";
|
||||
import process from "process";
|
||||
|
||||
const { killZoteroWindows, killZoteroUnix } = cmd;
|
||||
|
||||
try {
|
||||
if (process.platform === "win32") {
|
||||
execSync(killZoteroWindows);
|
||||
isRunning("zotero", (status) => {
|
||||
if (status) {
|
||||
killZotero();
|
||||
} else {
|
||||
execSync(killZoteroUnix);
|
||||
Logger.warn("No Zotero running.");
|
||||
}
|
||||
});
|
||||
|
||||
function killZotero() {
|
||||
try {
|
||||
if (process.platform === "win32") {
|
||||
execSync(killZoteroWindows);
|
||||
} else {
|
||||
execSync(killZoteroUnix);
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error(e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"updates": [
|
||||
{
|
||||
"version": "__buildVersion__",
|
||||
"update_link": "__releasepage__",
|
||||
"update_link": "__updateLink__",
|
||||
"applications": {
|
||||
"zotero": {
|
||||
"strict_min_version": "6.999"
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
import { exec } from "child_process";
|
||||
import {
|
||||
existsSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "fs";
|
||||
import path from "path";
|
||||
|
||||
export function copyFileSync(source, target) {
|
||||
var targetFile = target;
|
||||
|
||||
// If target is a directory, a new file with the same name will be created
|
||||
if (existsSync(target)) {
|
||||
if (lstatSync(target).isDirectory()) {
|
||||
targetFile = path.join(target, path.basename(source));
|
||||
}
|
||||
}
|
||||
|
||||
writeFileSync(targetFile, readFileSync(source));
|
||||
}
|
||||
|
||||
export function copyFolderRecursiveSync(source, target) {
|
||||
var files = [];
|
||||
|
||||
// Check if folder needs to be created or integrated
|
||||
var targetFolder = path.join(target, path.basename(source));
|
||||
if (!existsSync(targetFolder)) {
|
||||
mkdirSync(targetFolder);
|
||||
}
|
||||
|
||||
// Copy
|
||||
if (lstatSync(source).isDirectory()) {
|
||||
files = readdirSync(source);
|
||||
files.forEach(function (file) {
|
||||
var curSource = path.join(source, file);
|
||||
if (lstatSync(curSource).isDirectory()) {
|
||||
copyFolderRecursiveSync(curSource, targetFolder);
|
||||
} else {
|
||||
copyFileSync(curSource, targetFolder);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function clearFolder(target) {
|
||||
if (existsSync(target)) {
|
||||
rmSync(target, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
mkdirSync(target, { recursive: true });
|
||||
}
|
||||
|
||||
export function dateFormat(fmt, date) {
|
||||
let ret;
|
||||
const opt = {
|
||||
"Y+": date.getFullYear().toString(),
|
||||
"m+": (date.getMonth() + 1).toString(),
|
||||
"d+": date.getDate().toString(),
|
||||
"H+": date.getHours().toString(),
|
||||
"M+": date.getMinutes().toString(),
|
||||
"S+": date.getSeconds().toString(),
|
||||
};
|
||||
for (let k in opt) {
|
||||
ret = new RegExp("(" + k + ")").exec(fmt);
|
||||
if (ret) {
|
||||
fmt = fmt.replace(
|
||||
ret[1],
|
||||
ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, "0"),
|
||||
);
|
||||
}
|
||||
}
|
||||
return fmt;
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
static log(...args) {
|
||||
console.log(...args);
|
||||
}
|
||||
|
||||
// red
|
||||
static error(...args) {
|
||||
console.error("\u001b[31m [ERROR]", ...args, "\u001b[0m");
|
||||
}
|
||||
|
||||
// yellow
|
||||
static warn(...args) {
|
||||
console.warn("\u001b[33m [WARN]", ...args, "\u001b[0m");
|
||||
}
|
||||
|
||||
// blue
|
||||
static debug(...args) {
|
||||
console.log("\u001b[34m [DEBUG]\u001b[0m", ...args);
|
||||
}
|
||||
|
||||
// green
|
||||
static info(...args) {
|
||||
console.log("\u001b[32m [INFO]", ...args, "\u001b[0m");
|
||||
}
|
||||
|
||||
// cyan
|
||||
static trace(...args) {
|
||||
console.log("\u001b[36m [TRACE]\u001b[0m", ...args);
|
||||
}
|
||||
}
|
||||
|
||||
export function isRunning(query, cb) {
|
||||
let platform = process.platform;
|
||||
let cmd = "";
|
||||
switch (platform) {
|
||||
case "win32":
|
||||
cmd = `tasklist`;
|
||||
break;
|
||||
case "darwin":
|
||||
cmd = `ps -ax | grep ${query}`;
|
||||
break;
|
||||
case "linux":
|
||||
cmd = `ps -A`;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
exec(cmd, (err, stdout, stderr) => {
|
||||
cb(stdout.toLowerCase().indexOf(query.toLowerCase()) > -1);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"usage": "Copy and rename this file to zotero-cmd.json. Edit the cmd.",
|
||||
"killZoteroWindows": "taskkill /f /im zotero.exe",
|
||||
"killZoteroUnix": "kill -9 $(ps -x | grep zotero)",
|
||||
"killZoteroUnix": "kill -9 $(ps -x | grep '[z]otero' | awk '{print $1}')",
|
||||
"exec": {
|
||||
"@comment-zoteroBinPath": "Please input the path of the Zotero binary file in `zoteroBinPath`.",
|
||||
"@comment-zoteroBinPath-tip": "The path delimiter should be escaped as `\\` for win32. The path is `*/Zotero.app/Contents/MacOS/zotero` for MacOS.",
|
||||
25
src/hooks.ts
25
src/hooks.ts
|
|
@ -12,12 +12,18 @@ async function onStartup() {
|
|||
Zotero.unlockPromise,
|
||||
Zotero.uiReadyPromise,
|
||||
]);
|
||||
|
||||
// TODO: Remove this after zotero#3387 is merged
|
||||
if (__env__ === "development") {
|
||||
// Keep in sync with the scripts/startup.mjs
|
||||
const loadDevToolWhen = `Plugin ${config.addonID} startup`;
|
||||
ztoolkit.log(loadDevToolWhen);
|
||||
}
|
||||
|
||||
initLocale();
|
||||
|
||||
await tldrs.getAsync();
|
||||
|
||||
RegisterFactory.registerPrefs();
|
||||
|
||||
RegisterFactory.registerNotifier();
|
||||
|
||||
await onMainWindowLoad(window);
|
||||
|
|
@ -27,13 +33,15 @@ async function onMainWindowLoad(win: Window): Promise<void> {
|
|||
// Create ztoolkit for every window
|
||||
addon.data.ztoolkit = createZToolkit();
|
||||
|
||||
await Zotero.Promise.delay(1000);
|
||||
(win as any).MozXULElement.insertFTLIfNeeded(
|
||||
`${config.addonRef}-mainWindow.ftl`,
|
||||
);
|
||||
|
||||
UIFactory.registerRightClickMenuItem();
|
||||
|
||||
UIFactory.registerRightClickCollectionMenuItem();
|
||||
|
||||
await UIFactory.registerTLDRItemBoxRow();
|
||||
UIFactory.registerTLDRItemBoxRow();
|
||||
|
||||
onLoad();
|
||||
}
|
||||
|
|
@ -91,7 +99,7 @@ function onLoad() {
|
|||
for (const lib of Zotero.Libraries.getAll()) {
|
||||
needFetchItems = needFetchItems.concat(
|
||||
(await Zotero.Items.getAll(lib.id)).filter((item: Zotero.Item) => {
|
||||
return item.isRegularItem() && !item.isCollection();
|
||||
return item.isRegularItem();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
@ -127,8 +135,8 @@ function onUpdateItems(items: Zotero.Item[], forceFetch: boolean = false) {
|
|||
if (!item.getField("title")) {
|
||||
return false;
|
||||
}
|
||||
if (!forceFetch) {
|
||||
return tldrs.get()[item.id] === undefined;
|
||||
if (!forceFetch && item.key in tldrs.get()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
|
@ -157,7 +165,6 @@ function onUpdateItems(items: Zotero.Item[], forceFetch: boolean = false) {
|
|||
? succeedItems.push(item)
|
||||
: failedItems.push(item);
|
||||
await Zotero.Promise.delay(50);
|
||||
ztoolkit.ItemBox.refresh();
|
||||
popupWin.changeLine({
|
||||
progress: (index * 100) / count,
|
||||
text: `${getString("popWindow-waiting")}: ${
|
||||
|
|
@ -184,7 +191,7 @@ function onUpdateItems(items: Zotero.Item[], forceFetch: boolean = false) {
|
|||
|
||||
// Add your hooks here. For element click, etc.
|
||||
// Keep in mind hooks only do dispatch. Don't add code that does real jobs in hooks.
|
||||
// Otherwise the code would be hard to read and maintian.
|
||||
// Otherwise the code would be hard to read and maintain.
|
||||
|
||||
export default {
|
||||
onStartup,
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ import { config } from "../package.json";
|
|||
const basicTool = new BasicTool();
|
||||
|
||||
if (!basicTool.getGlobal("Zotero")[config.addonInstance]) {
|
||||
// Set global variables
|
||||
_globalThis.Zotero = basicTool.getGlobal("Zotero");
|
||||
defineGlobal("window");
|
||||
defineGlobal("document");
|
||||
defineGlobal("ZoteroPane");
|
||||
|
|
@ -16,8 +14,6 @@ if (!basicTool.getGlobal("Zotero")[config.addonInstance]) {
|
|||
return _globalThis.addon.data.ztoolkit;
|
||||
});
|
||||
Zotero[config.addonInstance] = addon;
|
||||
// Trigger addon hook for initialization
|
||||
addon.hooks.onStartup();
|
||||
}
|
||||
|
||||
function defineGlobal(name: Parameters<BasicTool["getGlobal"]>[0]): void;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { config } from "../../package.json";
|
||||
import { getString } from "../utils/locale";
|
||||
import { tldrs, TLDRItemNotFound, TLDRUnrelated } from "./dataStorage";
|
||||
import { tldrs } from "./dataStorage";
|
||||
|
||||
export class RegisterFactory {
|
||||
// 注册zotero的通知
|
||||
|
|
@ -36,25 +36,12 @@ export class RegisterFactory {
|
|||
private static unregisterNotifier(notifierID: string) {
|
||||
Zotero.Notifier.unregisterObserver(notifierID);
|
||||
}
|
||||
|
||||
// 注册首选项配置
|
||||
static registerPrefs() {
|
||||
// const prefOptions = {
|
||||
// pluginID: config.addonID,
|
||||
// src: rootURI + "chrome/content/preferences.xhtml",
|
||||
// label: getString("prefs.title"),
|
||||
// image: `chrome://${config.addonRef}/content/icons/favicon.png`,
|
||||
// extraDTD: [`chrome://${config.addonRef}/locale/overlay.dtd`],
|
||||
// defaultXUL: true,
|
||||
// };
|
||||
// ztoolkit.PreferencePane.register(prefOptions);
|
||||
}
|
||||
}
|
||||
|
||||
export class UIFactory {
|
||||
// item右键菜单
|
||||
static registerRightClickMenuItem() {
|
||||
const menuIcon = `chrome://${config.addonRef}/content/icons/favicon@0.5x.png`;
|
||||
const menuIcon = `chrome://${config.addonRef}/content/icons/favicon.png`;
|
||||
// item menuitem with icon
|
||||
ztoolkit.Menu.register("item", {
|
||||
tag: "menuitem",
|
||||
|
|
@ -70,7 +57,7 @@ export class UIFactory {
|
|||
|
||||
// collection右键菜单
|
||||
static registerRightClickCollectionMenuItem() {
|
||||
const menuIcon = `chrome://${config.addonRef}/content/icons/favicon@0.5x.png`;
|
||||
const menuIcon = `chrome://${config.addonRef}/content/icons/favicon.png`;
|
||||
ztoolkit.Menu.register("collection", {
|
||||
tag: "menuitem",
|
||||
id: "zotero-collectionmenu-tldr",
|
||||
|
|
@ -86,36 +73,45 @@ export class UIFactory {
|
|||
|
||||
// tldr行
|
||||
static async registerTLDRItemBoxRow() {
|
||||
await ztoolkit.ItemBox.register(
|
||||
"TLDR",
|
||||
getString("itembox-tldrlabel"),
|
||||
(field, unformatted, includeBaseMapped, item, original) => {
|
||||
const tldrInfo = tldrs.get()[item.id];
|
||||
if (tldrInfo === TLDRUnrelated) {
|
||||
return getString(TLDRUnrelated);
|
||||
} else if (tldrInfo === TLDRItemNotFound) {
|
||||
return getString(TLDRItemNotFound);
|
||||
} else if (tldrInfo) {
|
||||
return tldrInfo;
|
||||
} else {
|
||||
return "";
|
||||
const itemTLDR = (item: Zotero.Item) => {
|
||||
const noteKey = tldrs.get()[item.key];
|
||||
if (noteKey) {
|
||||
const obj = Zotero.Items.getByLibraryAndKey(item.libraryID, noteKey);
|
||||
if (
|
||||
obj &&
|
||||
obj instanceof Zotero.Item &&
|
||||
item.getNotes().includes(obj.id)
|
||||
) {
|
||||
let str = obj.getNote();
|
||||
if (str.startsWith("<p>TL;DR</p>\n<p>")) {
|
||||
str = str.slice("<p>TL;DR</p>\n<p>".length);
|
||||
}
|
||||
if (str.endsWith("</p>")) {
|
||||
str = str.slice(0, -4);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
Zotero.ItemPaneManager.registerSection({
|
||||
paneID: config.addonRef,
|
||||
pluginID: config.addonID,
|
||||
header: {
|
||||
l10nID: `${config.addonRef}-itemPaneSection-header`,
|
||||
icon: `chrome://${config.addonRef}/content/icons/favicon@16.png`,
|
||||
},
|
||||
{
|
||||
editable: true,
|
||||
setFieldHook: (field, value, loadIn, item, original) => {
|
||||
(async () => {
|
||||
await tldrs.modify((data: any) => {
|
||||
data[item.id] = value;
|
||||
return data;
|
||||
});
|
||||
ztoolkit.ItemBox.refresh();
|
||||
})();
|
||||
return true;
|
||||
},
|
||||
index: 2,
|
||||
multiline: true,
|
||||
sidenav: {
|
||||
l10nID: `${config.addonRef}-itemPaneSection-sidenav`,
|
||||
icon: `chrome://${config.addonRef}/content/icons/favicon@20.png`,
|
||||
},
|
||||
);
|
||||
onRender: ({ body, item }: any) => {
|
||||
let tldr = itemTLDR(item);
|
||||
if (tldr.length <= 0 && item.parentItem) {
|
||||
tldr = itemTLDR(item.parentItem);
|
||||
}
|
||||
body.textContent = tldr;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,6 +119,6 @@ export class DataStorage {
|
|||
}
|
||||
}
|
||||
|
||||
export const TLDRUnrelated = "tldr-unrelated"; // semantic scholar 找到了该item,但是该item没有tldr
|
||||
export const TLDRItemNotFound = "tldr-itemnotfound"; // semantic scholar 找不到该item
|
||||
export const tldrs = DataStorage.instance<string | number, string>("TLDR.json");
|
||||
export const tldrs = DataStorage.instance<string, string | false>(
|
||||
"fetchedItems.json",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { tldrs, TLDRUnrelated, TLDRItemNotFound } from "./dataStorage";
|
||||
import { tldrs } from "./dataStorage";
|
||||
|
||||
type SemanticScholarItemInfo = {
|
||||
title?: string;
|
||||
|
|
@ -13,7 +13,7 @@ export class TLDRFetcher {
|
|||
|
||||
constructor(item: Zotero.Item) {
|
||||
this.zoteroItem = item;
|
||||
if (item.isRegularItem() && !item.isCollection()) {
|
||||
if (item.isRegularItem()) {
|
||||
this.title = item.getField("title") as string;
|
||||
this.abstract = item.getField("abstractNote") as string;
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ export class TLDRFetcher {
|
|||
if (!this.title || this.title.length <= 0) {
|
||||
return false;
|
||||
}
|
||||
const noteKey = (await tldrs.getAsync())[this.zoteroItem.key];
|
||||
try {
|
||||
const infos = await this.fetchRelevanceItemInfos(this.title);
|
||||
for (const info of infos) {
|
||||
|
|
@ -36,23 +37,37 @@ export class TLDRFetcher {
|
|||
) {
|
||||
match = true;
|
||||
}
|
||||
if (match) {
|
||||
const result = info.tldr ?? TLDRUnrelated;
|
||||
tldrs.modify((data: any) => {
|
||||
data[this.zoteroItem.id] = result;
|
||||
if (match && info.tldr) {
|
||||
let note = new Zotero.Item("note");
|
||||
if (noteKey) {
|
||||
const obj = Zotero.Items.getByLibraryAndKey(
|
||||
this.zoteroItem.libraryID,
|
||||
noteKey,
|
||||
);
|
||||
if (
|
||||
obj &&
|
||||
obj instanceof Zotero.Item &&
|
||||
this.zoteroItem.getNotes().includes(obj.id)
|
||||
) {
|
||||
note = obj;
|
||||
}
|
||||
}
|
||||
note.setNote(`<p>TL;DR</p>\n<p>${info.tldr}</p>`);
|
||||
note.parentID = this.zoteroItem.id;
|
||||
await note.saveTx();
|
||||
await tldrs.modify((data: any) => {
|
||||
data[this.zoteroItem.key] = note.key;
|
||||
return data;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
tldrs.modify((data: any) => {
|
||||
data[this.zoteroItem.id] = TLDRItemNotFound;
|
||||
await tldrs.modify((data: any) => {
|
||||
data[this.zoteroItem.key] = false;
|
||||
return data;
|
||||
});
|
||||
return false;
|
||||
} catch (error) {
|
||||
Zotero.log(`post semantic scholar request error: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
"target": "ES2016",
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true
|
||||
"strict": true,
|
||||
},
|
||||
"include": ["src", "typings", "node_modules/zotero-types"],
|
||||
"exclude": ["build", "addon"]
|
||||
"exclude": ["build", "addon"],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
"zoterotldr@syt.com": {
|
||||
"updates": [
|
||||
{
|
||||
"version": "1.0.3",
|
||||
"update_link": "https://github.com/syt2/zotero-tldr/releases/latest/download/zotero-tldr.xpi",
|
||||
"version": "1.0.7",
|
||||
"update_link": "undefined/latest/download/zotero-tldr.xpi",
|
||||
"applications": {
|
||||
"zotero": {
|
||||
"strict_min_version": "6.999"
|
||||
|
|
|
|||
Loading…
Reference in New Issue