Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
568e460189 | ||
|
|
5851b95ff1 | ||
|
|
719be35dca | ||
|
|
bf46815ddb | ||
|
|
476b569555 | ||
|
|
7ec71c9a10 | ||
|
|
83151daf7f | ||
|
|
6259a9a2a6 | ||
|
|
79f7c81886 | ||
|
|
270d7fcb7f | ||
|
|
cfedb94159 | ||
|
|
07e624c67d | ||
|
|
b1fe4cc735 | ||
|
|
4726e7ca76 | ||
|
|
4fae5a422f | ||
|
|
63f8ce3a58 | ||
|
|
7dd98cbd87 | ||
|
|
a138848c4f | ||
|
|
2b0ef07fee | ||
|
|
0e42f90549 | ||
|
|
a144a74356 | ||
|
|
e776bb9b22 | ||
|
|
fb3d218f9f | ||
|
|
2b4745e417 | ||
|
|
303f13639c | ||
|
|
a88a582d71 | ||
|
|
a59647688e | ||
|
|
80fcb350ef | ||
|
|
f95f2c87c5 | ||
|
|
f93a43fdb3 | ||
|
|
b8924c7e1f | ||
|
|
62cdaf9c4d | ||
|
|
e5e149e6f2 | ||
|
|
6bfae7376c | ||
|
|
95a0f4916d | ||
|
|
6c8b8dc988 | ||
|
|
a3b5ac8050 | ||
|
|
c6d4d93c5d | ||
|
|
a667f8ac72 | ||
|
|
8f9092e787 | ||
|
|
3cf2379380 | ||
|
|
78eeff91a3 | ||
|
|
43a74a013b | ||
|
|
33330f0bda | ||
|
|
65c220f292 | ||
|
|
c4a88df09f | ||
|
|
6dc9259518 | ||
|
|
d17d5dc025 | ||
|
|
cd7edb0f63 | ||
|
|
a326c71a2c | ||
|
|
88e831d395 | ||
|
|
67ffda4830 | ||
|
|
60d0f30836 | ||
|
|
4cf1de6fbc | ||
|
|
f216b93771 | ||
|
|
e15e0552f6 | ||
|
|
47b65c7d97 | ||
|
|
802bb3fc1c | ||
|
|
6590c94fd7 |
|
|
@ -43,7 +43,6 @@ body:
|
|||
- Item
|
||||
- Text
|
||||
- QuickInsert
|
||||
- QuickBackLink
|
||||
- QuickImport
|
||||
- QuickNote
|
||||
- ExportMDFileName
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
name: Release
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
|
@ -11,8 +14,30 @@ permissions:
|
|||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
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 -f
|
||||
|
||||
- name: Test
|
||||
run: npm test
|
||||
|
||||
# If it's triggered by a tag and the test job is successful, release the package
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
if: github.event_name == 'push' && needs.test.result == 'success'
|
||||
env:
|
||||
# Allow triggering other workflows
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
|
|
@ -5,4 +5,5 @@ pnpm-lock.yaml
|
|||
yarn.lock
|
||||
zotero-cmd.json
|
||||
.DS_Store
|
||||
.env
|
||||
.env
|
||||
.scaffold
|
||||
|
|
@ -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}",
|
||||
"});"
|
||||
]
|
||||
}
|
||||
"});",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
|
|||
48
README.md
48
README.md
|
|
@ -90,7 +90,7 @@ and:
|
|||
|
||||
- Download the plugin (.xpi file) from below.
|
||||
|
||||
- [Latest Version: 2.1.2](https://github.com/windingwind/zotero-better-notes/releases/download/v2.1.2/better-notes-for-zotero.xpi)
|
||||
- [Latest Version: 2.2.5](https://github.com/windingwind/zotero-better-notes/releases/download/v2.2.5/better-notes-for-zotero.xpi)
|
||||
- [Latest Stable](https://github.com/windingwind/zotero-better-notes/releases/latest)
|
||||
- [All Releases](https://github.com/windingwind/zotero-better-notes/releases)
|
||||
|
||||
|
|
@ -136,6 +136,22 @@ To open a note in a tab, double-click/press `Enter` on the item in the library,
|
|||
|
||||
<div align=center><img src="https://github.com/user-attachments/assets/ec2ba415-8ec9-450d-9d73-d24d177ccd13" width="400px"></img></div>
|
||||
|
||||
#### Magic Key
|
||||
|
||||
BN enhances the note editor with a _Magic Key_ command palette, which can be opened by typing `/` in the editor. You can type or use the arrow keys to navigate the commands, and press `Enter` to execute.
|
||||
|
||||
<div align=center><img src="https://github.com/user-attachments/assets/bdbe244e-c120-4d9b-aa28-5285a25a723a" width="800px"></img></div>
|
||||
|
||||
#### Note Link Preview
|
||||
|
||||
BN enhances the note editor with link preview. Hover+Ctrl/Cmd or click the link to preview the linked note without leaving the current note.
|
||||
|
||||
<div align=center><img width="400px" alt="image" src="https://github.com/user-attachments/assets/b29769a1-3436-42bd-8481-eee1c0b4896a"></div>
|
||||
|
||||
#### Direct Markdown Paste
|
||||
|
||||
BN supports direct markdown paste. You can paste markdown content into the note editor, and it will be converted to the rich text format automatically.
|
||||
|
||||
### Note Link
|
||||
|
||||
To create a _note link_ between the note you are editing and another note, click the <img src="addon/chrome/content/icons/favicon.png" width="20px"></img> button in the title bar of the note editor.
|
||||
|
|
@ -224,6 +240,17 @@ BN provides APIs for other plugin developers in `Zotero.BetterNotes.api.${API_MO
|
|||
- `$export`: Export note
|
||||
- `$import`: Import note
|
||||
- `editor`: Note editor APIs. Give your script full control of contents in the note editor.
|
||||
- `note`: Note APIs. Parse and manipulate note content.
|
||||
- `relation`: Note relation APIs. Get and set note relations.
|
||||
- `utils`: Utility functions.
|
||||
|
||||
### Concepts about Note-Related APIs
|
||||
|
||||
In Zotero, the content of a note is stored as rich text, while when a note is opened in the note editor, it is rendered by ProseMirror as HTML.
|
||||
|
||||
Most of the time, it is recommended to use the `editor` API to interact with the content of the note, as it supports undo/redo and other features provided by editor. The `editor` API provides a set of powerful functions to analyze and manipulate the content in the note editor. Most of them needs an `editor` instance as the input, you can get the instance by calling `Zotero.BetterNotes.api.editor.getEditorInstance(noteId)`.
|
||||
|
||||
However, if note is not opened in the editor, you cannot get the `editor` instance. In this case, you can use the `note` API to interact with the content of the note.
|
||||
|
||||
## 🔧 Development
|
||||
|
||||
|
|
@ -240,14 +267,31 @@ npm run build
|
|||
|
||||
The plugin is built to `./builds/*.xpi`.
|
||||
|
||||
To debug, run
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
This will open a new Zotero instance with the plugin installed.
|
||||
|
||||
To test the plugin, run
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
This will run the tests in the `./test` directory.
|
||||
|
||||
## 🔔 Disclaimer
|
||||
|
||||
Use this code under AGPL. No warranties are provided. Keep the laws of your locality in mind!
|
||||
|
||||
## 🔎 My Zotero Plugins
|
||||
|
||||
- [Translate for Zotero](https://github.com/windingwind/zotero-pdf-translate): PDF translation for Zotero
|
||||
- [Translate for Zotero](https://github.com/windingwind/zotero-pdf-translate): Translate PDF, EPub, webpage, metadata, annotations, notes to the target language.
|
||||
- [Actions & Tags for Zotero](https://github.com/windingwind/zotero-tag): Customize your Zotero workflow.
|
||||
- [Bionic for Zotero](https://github.com/windingwind/bionic-for-zotero): Bionic reading experience with Zotero.
|
||||
|
||||
## 🙌 Sponsors
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ async function startup({ id, version, resourceURI, rootURI }, reason) {
|
|||
`${rootURI}/chrome/content/scripts/__addonRef__.js`,
|
||||
ctx,
|
||||
);
|
||||
Zotero.__addonInstance__.hooks.onStartup();
|
||||
await Zotero.__addonInstance__.hooks.onStartup();
|
||||
}
|
||||
|
||||
function onMainWindowLoad({ window: win }) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
var { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -21,9 +21,6 @@
|
|||
</style>
|
||||
<script>
|
||||
var browser;
|
||||
var { Services } = ChromeUtils.import(
|
||||
"resource://gre/modules/Services.jsm",
|
||||
);
|
||||
var { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -44,10 +44,6 @@
|
|||
</xul:keyset>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", (ev) => {
|
||||
const { Services } = ChromeUtils.import(
|
||||
"resource://gre/modules/Services.jsm",
|
||||
);
|
||||
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://zotero/content/include.js",
|
||||
this,
|
||||
|
|
|
|||
|
|
@ -45,10 +45,6 @@
|
|||
</xul:keyset>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", (ev) => {
|
||||
const { Services } = ChromeUtils.import(
|
||||
"resource://gre/modules/Services.jsm",
|
||||
);
|
||||
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://zotero/content/include.js",
|
||||
this,
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ alert-notValidParentItemError = No valid parent item.
|
|||
alert-syncImportedNotes = Keep imported notes in sync with MarkDown files?
|
||||
alert-linkCreator-emptyNote = Cannot create link from/to an empty note.
|
||||
alert-templateEditor-shouldImport = Seems like you are trying to directly save a note template share code. Do you want to import it as a template?
|
||||
alert-templateEditor-unsaved = You have unsaved changes in the template editor. Do you want to save them?
|
||||
|
||||
userGuide-start-title = Welcome to Better Notes!
|
||||
userGuide-start-desc = Better Notes is a powerful note-taking tool that helps you organize your thoughts and ideas while reading papers. This guide will help you get started with Better Notes and show you how to make the most of its features.
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ alert-notValidParentItemError = Nessun elemento genitore valido.
|
|||
alert-syncImportedNotes = Si desidera sincronizzare le note importate con i file markdown?
|
||||
alert-linkCreator-emptyNote = Non è possibile creare un link da/a una nota vuota.
|
||||
alert-templateEditor-shouldImport = Sembra che tu stia cercando di salvare direttamente un codice di condivisione di un template di nota. Vuoi importarlo come template?
|
||||
alert-templateEditor-unsaved = You have unsaved changes in the template editor. Do you want to save them?
|
||||
|
||||
userGuide-start-title = Questo è Better Notes!
|
||||
userGuide-start-desc = Better Notes è un potente strumento di gestione delle note che ti può aiutare a organizzare pensieri e idee durante la lettura di articoli scientifici. Questa guida ti aiuterà a prendere confidenza con Better Notes e ti mostrerà come usare al meglio le sue funzioni.
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ alert-notValidParentItemError=Нет валидного родительског
|
|||
alert-syncImportedNotes = Синхронизировать импортированные заметки с файлами MarkDown?
|
||||
alert-linkCreator-emptyNote = Cannot create link from/to an empty note.
|
||||
alert-templateEditor-shouldImport = Вы пытаетесь сохранить код шаблона заметки. Хотите импортировать его как шаблон?
|
||||
alert-templateEditor-unsaved = You have unsaved changes in the template editor. Do you want to save them?
|
||||
|
||||
userGuide-start-title = Welcome to Better Notes!
|
||||
userGuide-start-desc = Better Notes is a powerful note-taking tool that helps you organize your thoughts and ideas while reading papers. This guide will help you get started with Better Notes and show you how to make the most of its features.
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ alert-notValidParentItemError = Geçerli ana eser yok.
|
|||
alert-syncImportedNotes = İçe aktarılmış notları Markdown dosyalarıyla eşitlemede tutmak ister misiniz?
|
||||
alert-linkCreator-emptyNote = Boş bir nota/nottan bağlantı oluşturulamaz. note.
|
||||
alert-templateEditor-shouldImport = Şablon kodunu doğrudan kaydetmeye çalışıyorsunuz gibi görünüyor. Şablon olarak içe aktarmak ister misiniz?
|
||||
alert-templateEditor-unsaved = You have unsaved changes in the template editor. Do you want to save them?
|
||||
|
||||
userGuide-start-title = Better Notes'a hoş geldiniz!
|
||||
userGuide-start-desc = Better Notes düşüncelerinizi ve tasarılarınızı düzenlemenize yardımcı olan güçlü bir not alma aracıdır. Kullanımı kolay ve yalın biçimde tasarlanmış olsa da karmaşık not alma işlerini görebilecek kadar da esnektir. Bu rehber size Better Notes'u kullanmaya başlamanıza ve ileri düzey işlevleri verimli kullanmanıza yardımcı olacaktır.
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ alert-notValidParentItemError=无效的父条目。
|
|||
alert-syncImportedNotes = 保持导入的笔记与 MarkDown 文件同步?
|
||||
alert-linkCreator-emptyNote = 无法从/向空笔记创建链接。
|
||||
alert-templateEditor-shouldImport = 似乎您正在尝试直接保存一个笔记模板分享代码。您想要将其导入为模板吗?
|
||||
alert-templateEditor-unsaved = 您在模板编辑器中有未保存的更改。您想要保存它们吗?
|
||||
|
||||
userGuide-start-title = 欢迎使用Better Notes!
|
||||
userGuide-start-desc = Better Notes是一个强大的笔记工具,帮助您组织阅读论文时的概念和想法。本指南将帮助您开始使用Better Notes,并向您展示如何充分利用其功能。
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
"id": "__addonID__",
|
||||
"update_url": "__updateURL__",
|
||||
"strict_min_version": "7.0.0-beta.70",
|
||||
"strict_max_version": "7.0.*"
|
||||
"strict_max_version": "7.1.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ One-click to import.
|
|||
```yaml
|
||||
# This template is specifically for importing/sharing, using better
|
||||
# notes 'import from clipboard': copy the content and
|
||||
# goto Zotero menu bar, click Edit->New Template from Clipboard.
|
||||
# goto Zotero menu bar, click Tools->New Template from Clipboard.
|
||||
# Do not copy-paste this to better notes template editor directly.
|
||||
name: "[Text] Current Time"
|
||||
content: |-
|
||||
|
|
@ -32,7 +32,7 @@ content: |-
|
|||
|
||||
</details>
|
||||
|
||||
2. Goto Zotero menubar, click `Edit`->`New Template from Clipboard`.
|
||||
2. Goto Zotero menubar, click `Tools`->`New Template from Clipboard`.
|
||||
3. Click OK.
|
||||
|
||||
Now you can open a note/the workspace and in editor toolbar, click `Insert Template to cursor line`. Select the template, it is inserted to the note.
|
||||
|
|
@ -88,7 +88,9 @@ Let the compiler know you are using markdown. Otherwise the template will be pro
|
|||
Allow the generated content to be updated using the `Update content from templates` in the note editor.
|
||||
The generated content will be wrapped in separators with a YAML metadata section for update.
|
||||
|
||||
> The template with this pragma should not contain any separator (`---` or `<hr>`) in the content.
|
||||
The template with this pragma should not contain any separator (`---` or `<hr>`) in the content.
|
||||
|
||||
> Since the first line of the content is a separator, the note generated from a template with this pragma will have a blank note title. See the solution [here](https://github.com/windingwind/zotero-better-notes/issues/1247#issuecomment-2573739339).
|
||||
|
||||
### `// @author`
|
||||
|
||||
|
|
@ -148,7 +150,7 @@ If no stage pragma is given, the whole template will be processed on the default
|
|||
```yaml
|
||||
# This template is specifically for importing/sharing, using better
|
||||
# notes 'import from clipboard': copy the content and
|
||||
# goto Zotero menu bar, click Edit->New Template from Clipboard.
|
||||
# goto Zotero menu bar, click Tools->New Template from Clipboard.
|
||||
# Do not copy-paste this to better notes template editor directly.
|
||||
name: "[Item] Example Item Template"
|
||||
content: |-
|
||||
|
|
@ -214,7 +216,6 @@ The name of builtin templates are not allowed to be modified.
|
|||
| Name | Description | Variables |
|
||||
| ------------------- | -------------------------------------------------------- | ------------------------------------- |
|
||||
| QuickInsert | For forward link. | link, linkText, subNoteItem, noteItem |
|
||||
| QuickBackLink | For back link. | link, linkText, subNoteItem, noteItem |
|
||||
| QuickImport | For importing note link content. | link, noteItem |
|
||||
| QuickNote | For generating note from annotation. | annotationItem, topItem, noteItem |
|
||||
| ExportMDFileName | For generating Markdown file name when exporting. | noteItem |
|
||||
|
|
@ -304,10 +305,10 @@ ${{
|
|||
}
|
||||
key = att.key;
|
||||
if (att.libraryID === 1) {
|
||||
return `zotero://open-pdf/library/items/${key}`;
|
||||
return `zotero://open/library/items/${key}`;
|
||||
} else {
|
||||
groupID = Zotero.Libraries.get(att.libraryID).id;
|
||||
return `zotero://open-pdf/groups/${groupID}/items/${key}`;
|
||||
return `zotero://open/groups/${groupID}/items/${key}`;
|
||||
}
|
||||
}
|
||||
sharedObj.getPDFLink = getPDFLink;
|
||||
|
|
@ -408,7 +409,7 @@ A template snippet should be in YAML format (YAML has better support for multi-l
|
|||
```yaml
|
||||
# This template is specifically for importing/sharing, using better
|
||||
# notes 'import from clipboard': copy the content and
|
||||
# goto Zotero menu bar, click Edit->New Template from Clipboard.
|
||||
# goto Zotero menu bar, click Tools->New Template from Clipboard.
|
||||
# Do not copy-paste this to better notes template editor directly.
|
||||
name: "[TYPE] TEMPLATE NAME"
|
||||
content: |-
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
52
package.json
52
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "zotero-better-notes",
|
||||
"version": "2.1.2",
|
||||
"version": "2.2.5",
|
||||
"description": "Everything about note management. All in Zotero.",
|
||||
"config": {
|
||||
"addonName": "Better Notes for Zotero",
|
||||
|
|
@ -14,9 +14,11 @@
|
|||
"scripts": {
|
||||
"start": "zotero-plugin serve",
|
||||
"build": "tsc --noEmit && zotero-plugin build",
|
||||
"build-dev": "tsc --noEmit && zotero-plugin build --dev && cd build/addon && zip -r ../zotero-better-notes-dev.xpi .",
|
||||
"release": "zotero-plugin release",
|
||||
"lint": "prettier --write . && eslint . --ext .ts --fix",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"test": "zotero-plugin test --abort-on-fail --exit-on-finish",
|
||||
"test-dev": "zotero-plugin test --abort-on-fail",
|
||||
"update-deps": "npm update --save"
|
||||
},
|
||||
"repository": {
|
||||
|
|
@ -30,16 +32,16 @@
|
|||
},
|
||||
"homepage": "https://github.com/windingwind/zotero-better-notes#readme",
|
||||
"dependencies": {
|
||||
"asciidoctor": "^3.0.2",
|
||||
"dexie": "^4.0.4",
|
||||
"diff": "^5.1.0",
|
||||
"hast-util-to-html": "^9.0.0",
|
||||
"asciidoctor": "^3.0.4",
|
||||
"dexie": "^4.0.11",
|
||||
"diff": "^5.2.0",
|
||||
"hast-util-to-html": "^9.0.4",
|
||||
"hast-util-to-mdast": "^8.4.1",
|
||||
"hast-util-to-text": "^4.0.0",
|
||||
"hast-util-to-text": "^4.0.2",
|
||||
"hastscript": "^8.0.0",
|
||||
"html-docx-js": "^0.3.1",
|
||||
"html-docx-js-typescript": "^0.1.5",
|
||||
"katex": "^0.16.9",
|
||||
"katex": "^0.16.21",
|
||||
"path-browserify": "^1.0.1",
|
||||
"rehype-format": "^4.0.1",
|
||||
"rehype-parse": "^8.0.5",
|
||||
|
|
@ -56,36 +58,38 @@
|
|||
"unist-util-visit": "^5.0.0",
|
||||
"unist-util-visit-parents": "^6.0.1",
|
||||
"yamljs": "^0.3.0",
|
||||
"zotero-plugin-toolkit": "^4.0.9"
|
||||
"zotero-plugin-toolkit": "^4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
||||
"@prettier/plugin-xml": "^3.2.2",
|
||||
"@prettier/plugin-xml": "^3.4.1",
|
||||
"@types/browser-or-node": "^1.3.2",
|
||||
"@types/diff": "^5.0.9",
|
||||
"@types/chai": "^5.0.1",
|
||||
"@types/diff": "^5.2.3",
|
||||
"@types/html-docx-js": "^0.3.4",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/node": "^20.10.4",
|
||||
"@types/path-browserify": "^1.0.2",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^20.17.14",
|
||||
"@types/path-browserify": "^1.0.3",
|
||||
"@types/seedrandom": "^3.0.8",
|
||||
"@types/yamljs": "^0.2.34",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"chokidar-cli": "^3.0.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"prettier": "^3.1.1",
|
||||
"prosemirror-model": "^1.19.4",
|
||||
"prettier": "^3.4.2",
|
||||
"prosemirror-model": "^1.24.1",
|
||||
"prosemirror-state": "^1.4.3",
|
||||
"prosemirror-transform": "^1.8.0",
|
||||
"prosemirror-view": "^1.32.6",
|
||||
"prosemirror-transform": "^1.10.2",
|
||||
"prosemirror-view": "^1.37.1",
|
||||
"replace-in-file": "^7.2.0",
|
||||
"typescript": "^5.3.3",
|
||||
"xslt3": "^2.6.0",
|
||||
"zotero-plugin-scaffold": "^0.1.6",
|
||||
"zotero-types": "^3.0.0"
|
||||
"typescript": "^5.7.3",
|
||||
"xslt3": "^2.7.0",
|
||||
"zotero-plugin-scaffold": "^0.2.0-beta.20",
|
||||
"zotero-types": "^3.1.6"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"env": {
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ class Addon {
|
|||
public data: {
|
||||
uid: string;
|
||||
alive: boolean;
|
||||
// Env type, see build.js
|
||||
env: "development" | "production";
|
||||
env: "development" | "production" | "test";
|
||||
initialized?: boolean;
|
||||
ztoolkit: ZToolkit;
|
||||
// ztoolkit: ZoteroToolkit;
|
||||
locale?: {
|
||||
|
|
@ -24,7 +24,7 @@ class Addon {
|
|||
window: Window;
|
||||
};
|
||||
export: {
|
||||
pdf: { promise?: _ZoteroTypes.PromiseObject };
|
||||
pdf: { promise?: _ZoteroTypes.Promise.PromiseObject };
|
||||
};
|
||||
sync: {
|
||||
data?: LargePrefHelper;
|
||||
|
|
@ -66,7 +66,7 @@ class Addon {
|
|||
templates: string[];
|
||||
};
|
||||
picker: {
|
||||
mode: "insert" | "create" | "export";
|
||||
mode: "insert" | "create" | "export" | "pick";
|
||||
data: Record<string, any>;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
runTemplate,
|
||||
runTextTemplate,
|
||||
runItemTemplate,
|
||||
runQuickInsertTemplate,
|
||||
} from "./modules/template/api";
|
||||
import {
|
||||
getTemplateKeys,
|
||||
|
|
@ -124,6 +125,7 @@ const template = {
|
|||
runTemplate,
|
||||
runTextTemplate,
|
||||
runItemTemplate,
|
||||
runQuickInsertTemplate,
|
||||
getTemplateKeys,
|
||||
getTemplateText,
|
||||
setTemplate,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export class PluginCEBase extends XULElementBase {
|
|||
useShadowRoot = false;
|
||||
|
||||
connectedCallback(): void {
|
||||
// @ts-ignore - plugin instance
|
||||
this._addon = Zotero[config.addonInstance];
|
||||
Zotero.UIProperties.registerRoot(this);
|
||||
if (!this.useShadowRoot) {
|
||||
|
|
|
|||
|
|
@ -66,11 +66,10 @@ export class InboundCreator extends PluginCEBase {
|
|||
|
||||
async accept(io: any) {
|
||||
if (!this.targetNote) return;
|
||||
const content = await this.getContentToInsert();
|
||||
this.notePicker.saveRecentNotes();
|
||||
|
||||
io.targetNoteID = this.targetNote.id;
|
||||
io.content = content;
|
||||
io.sourceNoteIDs = [this.currentNote!.id];
|
||||
io.lineIndex = this.getIndexToInsert();
|
||||
}
|
||||
|
||||
|
|
@ -216,21 +215,11 @@ export class InboundCreator extends PluginCEBase {
|
|||
|
||||
async getContentToInsert() {
|
||||
if (!this.currentNote || !this.targetNote) return "";
|
||||
const forwardLink = this._addon.api.convert.note2link(this.currentNote, {});
|
||||
const content = await this._addon.api.template.runTemplate(
|
||||
"[QuickInsertV2]",
|
||||
"link, linkText, subNoteItem, noteItem",
|
||||
[
|
||||
forwardLink,
|
||||
this.currentNote.getNoteTitle().trim() || forwardLink,
|
||||
this.currentNote,
|
||||
this.targetNote,
|
||||
],
|
||||
{
|
||||
dryRun: true,
|
||||
},
|
||||
return await this._addon.api.template.runQuickInsertTemplate(
|
||||
this.currentNote,
|
||||
this.targetNote,
|
||||
{ dryRun: true },
|
||||
);
|
||||
return content;
|
||||
}
|
||||
|
||||
getIndexToInsert() {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ export class NotePicker extends PluginCEBase {
|
|||
|
||||
_cachedLibraryIDs: number[] = [];
|
||||
|
||||
_cachedSelectedNoteIDs: number[] = [];
|
||||
|
||||
_disableSelectionChange = false;
|
||||
|
||||
get content() {
|
||||
return MozXULElement.parseXULToFragment(`
|
||||
<linkset>
|
||||
|
|
@ -121,8 +125,8 @@ export class NotePicker extends PluginCEBase {
|
|||
}
|
||||
|
||||
destroy(): void {
|
||||
this.collectionsView.unregister();
|
||||
if (this.itemsView) this.itemsView.unregister();
|
||||
this.collectionsView?.unregister();
|
||||
this.itemsView?.unregister();
|
||||
unregisterPrefObserver(this._prefObserverID);
|
||||
}
|
||||
|
||||
|
|
@ -395,6 +399,9 @@ export class NotePicker extends PluginCEBase {
|
|||
}
|
||||
|
||||
onItemSelected() {
|
||||
if (this._disableSelectionChange) {
|
||||
return;
|
||||
}
|
||||
this.activeSelectionType = "library";
|
||||
const selectedIDs = this.itemsView.getSelectedItems(true) as number[];
|
||||
// Compare the selected IDs with the cached IDs
|
||||
|
|
@ -402,19 +409,48 @@ export class NotePicker extends PluginCEBase {
|
|||
if (arraysEqual(this._cachedLibraryIDs, selectedIDs)) {
|
||||
return;
|
||||
}
|
||||
this.deselectOtherPanes();
|
||||
this.dispatchSelectionChange();
|
||||
}
|
||||
|
||||
onOpenedNoteSelected(selection: { selected: Set<number> }) {
|
||||
if (this._disableSelectionChange) {
|
||||
return;
|
||||
}
|
||||
this.activeSelectionType = "tabs";
|
||||
this.deselectOtherPanes();
|
||||
this.dispatchSelectionChange(selection);
|
||||
}
|
||||
|
||||
onRecentNoteSelected(selection: { selected: Set<number> }) {
|
||||
if (this._disableSelectionChange) {
|
||||
return;
|
||||
}
|
||||
this.activeSelectionType = "recent";
|
||||
this.deselectOtherPanes();
|
||||
this.dispatchSelectionChange(selection);
|
||||
}
|
||||
|
||||
deselectItemsPane() {
|
||||
this.itemsView?.selection?.clearSelection();
|
||||
}
|
||||
|
||||
deselectOpenedNotePane() {
|
||||
this.openedNotesView?.treeInstance?.selection?.clearSelection();
|
||||
}
|
||||
|
||||
deselectRecentNotePane() {
|
||||
this.recentNotesView?.treeInstance?.selection?.clearSelection();
|
||||
}
|
||||
|
||||
deselectOtherPanes() {
|
||||
this._disableSelectionChange = true;
|
||||
if (this.activeSelectionType !== "library") this.deselectItemsPane();
|
||||
if (this.activeSelectionType !== "tabs") this.deselectOpenedNotePane();
|
||||
if (this.activeSelectionType !== "recent") this.deselectRecentNotePane();
|
||||
this._disableSelectionChange = false;
|
||||
}
|
||||
|
||||
getRecentNotes() {
|
||||
return ((getPref("linkCreator.recentNotes") as string) || "")
|
||||
.split(",")
|
||||
|
|
@ -438,13 +474,23 @@ export class NotePicker extends PluginCEBase {
|
|||
}
|
||||
|
||||
dispatchSelectionChange(selection?: { selected: Set<number> }) {
|
||||
if (this._disableSelectionChange) {
|
||||
return false;
|
||||
}
|
||||
const selectedNotes = this.getSelectedNotes(selection);
|
||||
const selectedNoteIDs = selectedNotes.map((n) => n.id);
|
||||
if (arraysEqual(this._cachedSelectedNoteIDs, selectedNoteIDs)) {
|
||||
return false;
|
||||
}
|
||||
this._cachedSelectedNoteIDs = selectedNoteIDs;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("selectionchange", {
|
||||
detail: {
|
||||
selectedNotes: this.getSelectedNotes(selection),
|
||||
selectedNotes,
|
||||
},
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
getSelectedNotes(selection?: { selected: Set<number> }): Zotero.Item[] {
|
||||
|
|
|
|||
|
|
@ -69,10 +69,10 @@ export class OutboundCreator extends PluginCEBase {
|
|||
|
||||
async accept(io: any) {
|
||||
if (!this.targetNotes) return;
|
||||
const content = await this.getContentToInsert();
|
||||
this.notePicker.saveRecentNotes();
|
||||
|
||||
io.targetNoteID = this.currentNote!.id;
|
||||
io.sourceNoteIDs = this.targetNotes.map((item) => item.id).filter(Boolean);
|
||||
io.content = content;
|
||||
io.lineIndex = this.getIndexToInsert();
|
||||
}
|
||||
|
|
@ -240,19 +240,10 @@ export class OutboundCreator extends PluginCEBase {
|
|||
if (!this.currentNote || !this.targetNotes?.length) return "";
|
||||
let content = "";
|
||||
for (const note of this.targetNotes) {
|
||||
const forwardLink = this._addon.api.convert.note2link(note, {});
|
||||
content += await this._addon.api.template.runTemplate(
|
||||
"[QuickInsertV2]",
|
||||
"link, linkText, subNoteItem, noteItem",
|
||||
[
|
||||
forwardLink,
|
||||
note.getNoteTitle().trim() || forwardLink,
|
||||
note,
|
||||
this.currentNote,
|
||||
],
|
||||
{
|
||||
dryRun: true,
|
||||
},
|
||||
content += await this._addon.api.template.runQuickInsertTemplate(
|
||||
note,
|
||||
this.currentNote,
|
||||
{ dryRun: true },
|
||||
);
|
||||
content += "\n";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ const editorStrings = {
|
|||
"en-US": "Copy Line Link",
|
||||
"zh-CN": "复制行链接",
|
||||
},
|
||||
refreshTemplates: {
|
||||
"en-US": "Update content from templates",
|
||||
"zh-CN": "更新模板生成内容",
|
||||
},
|
||||
heading1: {
|
||||
"en-US": "Heading 1",
|
||||
"zh-CN": "一级标题",
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ class PluginState {
|
|||
height: auto;
|
||||
}
|
||||
.link-preview .primary-editor li {
|
||||
white-space: nowrap;
|
||||
white-space: normal;
|
||||
}
|
||||
</style>`),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ declare const _currentEditorInstance: {
|
|||
|
||||
interface MagicKeyOptions {
|
||||
insertTemplate?: () => void;
|
||||
refreshTemplates?: () => void;
|
||||
insertLink?: (type: "inbound" | "outbound") => void;
|
||||
copyLink?: (mode: "section" | "line") => void;
|
||||
openAttachment?: () => void;
|
||||
|
|
@ -20,6 +21,7 @@ interface MagicKeyOptions {
|
|||
|
||||
interface MagicCommand {
|
||||
messageId?: string;
|
||||
searchParts?: string[];
|
||||
title?: string;
|
||||
icon?: string;
|
||||
command: (state: EditorState) => void | Transaction;
|
||||
|
|
@ -34,30 +36,35 @@ class PluginState {
|
|||
_commands: MagicCommand[] = [
|
||||
{
|
||||
messageId: "insertTemplate",
|
||||
searchParts: ["it", "insertTemplate"],
|
||||
command: (state) => {
|
||||
this.options.insertTemplate?.();
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: "outboundLink",
|
||||
searchParts: ["ol", "obl", "outboundLink"],
|
||||
command: (state) => {
|
||||
this.options.insertLink?.("outbound");
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: "inboundLink",
|
||||
searchParts: ["il", "ibl", "inboundLink"],
|
||||
command: (state) => {
|
||||
this.options.insertLink?.("inbound");
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: "insertCitation",
|
||||
searchParts: ["ic", "insertCitation"],
|
||||
command: (state) => {
|
||||
getPlugin("citation")?.insertCitation();
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: "openAttachment",
|
||||
searchParts: ["oa", "openAttachment"],
|
||||
command: (state) => {
|
||||
this.options.openAttachment?.();
|
||||
},
|
||||
|
|
@ -67,18 +74,28 @@ class PluginState {
|
|||
},
|
||||
{
|
||||
messageId: "copySectionLink",
|
||||
searchParts: ["csl", "copySectionLink"],
|
||||
command: (state) => {
|
||||
this.options.copyLink?.("section");
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: "copyLineLink",
|
||||
searchParts: ["cll", "copyLineLink"],
|
||||
command: (state) => {
|
||||
this.options.copyLink?.("line");
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: "refreshTemplates",
|
||||
searchParts: ["rt", "refreshTemplates"],
|
||||
command: (state) => {
|
||||
this.options.refreshTemplates?.();
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: "table",
|
||||
searchParts: ["t", "tb", "table"],
|
||||
command: (state) => {
|
||||
const input = prompt(
|
||||
"Enter the number of rows and columns, separated by a comma (e.g., 3,3)",
|
||||
|
|
@ -118,60 +135,70 @@ class PluginState {
|
|||
},
|
||||
{
|
||||
messageId: "heading1",
|
||||
searchParts: ["h1", "heading1"],
|
||||
command: (state) => {
|
||||
getPlugin()?.heading1.run();
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: "heading2",
|
||||
searchParts: ["h2", "heading2"],
|
||||
command: (state) => {
|
||||
getPlugin()?.heading2.run();
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: "heading3",
|
||||
searchParts: ["h3", "heading3"],
|
||||
command: (state) => {
|
||||
getPlugin()?.heading3.run();
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: "paragraph",
|
||||
searchParts: ["p", "paragraph"],
|
||||
command: (state) => {
|
||||
getPlugin()?.paragraph.run();
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: "monospaced",
|
||||
searchParts: ["m", "monospaced"],
|
||||
command: (state) => {
|
||||
getPlugin()?.codeBlock.run();
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: "bulletList",
|
||||
searchParts: ["ul", "bulletList", "unorderedList"],
|
||||
command: (state) => {
|
||||
getPlugin()?.bulletList.run();
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: "orderedList",
|
||||
searchParts: ["ol", "orderedList"],
|
||||
command: (state) => {
|
||||
getPlugin()?.orderedList.run();
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: "blockquote",
|
||||
searchParts: ["bq", "blockquote"],
|
||||
command: (state) => {
|
||||
getPlugin()?.blockquote.run();
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: "mathBlock",
|
||||
searchParts: ["mb", "mathBlock"],
|
||||
command: (state) => {
|
||||
getPlugin()?.math_display.run();
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: "clearFormatting",
|
||||
searchParts: ["cf", "clearFormatting"],
|
||||
command: (state) => {
|
||||
getPlugin()?.clearFormatting.run();
|
||||
},
|
||||
|
|
@ -221,9 +248,20 @@ class PluginState {
|
|||
update(state: EditorState, prevState?: EditorState) {
|
||||
this.state = state;
|
||||
|
||||
if (!prevState || prevState.doc.eq(state.doc)) {
|
||||
if (!prevState) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the selection has changed, then try to close the popup
|
||||
if (!prevState.selection.eq(state.selection)) {
|
||||
this._closePopup();
|
||||
}
|
||||
|
||||
// If the document hasn't changed, we don't need to do anything
|
||||
if (prevState.doc.eq(state.doc)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When `/` is pressed, we should open the command palette
|
||||
const selectionText = state.doc.textBetween(
|
||||
state.selection.from,
|
||||
|
|
@ -313,7 +351,7 @@ class PluginState {
|
|||
</style>
|
||||
<div class="popup-content">
|
||||
<input type="text" class="popup-input" placeholder="Search commands" />
|
||||
<div class="popup-list">
|
||||
<div class="popup-list" tabindex="-1">
|
||||
${Object.entries(this.commands)
|
||||
.map(
|
||||
([id, command]) => `
|
||||
|
|
@ -343,9 +381,24 @@ class PluginState {
|
|||
const item = this.popup!.container.querySelector(
|
||||
`.popup-item[data-command-id="${id}"]`,
|
||||
) as HTMLElement;
|
||||
if (!value) {
|
||||
item.hidden = false;
|
||||
continue;
|
||||
}
|
||||
const matchedIndex = command
|
||||
.title!.toLowerCase()
|
||||
.indexOf(value.toLowerCase());
|
||||
if (
|
||||
matchedIndex < 0 &&
|
||||
// Try to match the search parts
|
||||
!command.searchParts?.some((part) =>
|
||||
part.toLowerCase().includes(value.toLowerCase()),
|
||||
)
|
||||
) {
|
||||
item.hidden = true;
|
||||
} else {
|
||||
item.hidden = false;
|
||||
}
|
||||
if (matchedIndex >= 0) {
|
||||
// Change the matched part to bold
|
||||
const title = command.title!;
|
||||
|
|
@ -353,21 +406,11 @@ class PluginState {
|
|||
title.slice(0, matchedIndex) +
|
||||
`<b>${title.slice(matchedIndex, matchedIndex + value.length)}</b>` +
|
||||
title.slice(matchedIndex + value.length);
|
||||
item.hidden = false;
|
||||
} else {
|
||||
item.hidden = true;
|
||||
}
|
||||
}
|
||||
this._selectCommand();
|
||||
});
|
||||
|
||||
input.addEventListener("blur", () => {
|
||||
if (__env__ === "development") {
|
||||
return;
|
||||
}
|
||||
this._closePopup();
|
||||
});
|
||||
|
||||
input.addEventListener("keydown", (event) => {
|
||||
if (event.key === "ArrowUp") {
|
||||
this._selectCommand(this.selectedCommandIndex - 1, "up");
|
||||
|
|
@ -375,6 +418,38 @@ class PluginState {
|
|||
} else if (event.key === "ArrowDown") {
|
||||
this._selectCommand(this.selectedCommandIndex + 1, "down");
|
||||
event.preventDefault();
|
||||
} else if (event.key === "ArrowLeft") {
|
||||
// Select the first command
|
||||
this._selectCommand(this.commands.length, "up");
|
||||
event.preventDefault();
|
||||
} else if (event.key === "ArrowRight") {
|
||||
// Select the last command
|
||||
this._selectCommand(-1, "down");
|
||||
event.preventDefault();
|
||||
} else if (event.key === "Tab") {
|
||||
// If has input, autocomplete the selected command to the first space
|
||||
const command = this.commands[this.selectedCommandIndex];
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
if (!input.value) {
|
||||
return;
|
||||
}
|
||||
const title = command.title!;
|
||||
// Compute after the matched part
|
||||
const matchedIndex = title
|
||||
.toLowerCase()
|
||||
.indexOf(input.value.toLowerCase());
|
||||
const spaceIndex = title.indexOf(
|
||||
" ",
|
||||
matchedIndex + input.value.length,
|
||||
);
|
||||
if (spaceIndex >= 0) {
|
||||
input.value = title.slice(0, spaceIndex);
|
||||
} else {
|
||||
input.value = title;
|
||||
}
|
||||
event.preventDefault();
|
||||
} else if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
const command = this.commands[this.selectedCommandIndex];
|
||||
|
|
@ -384,6 +459,7 @@ class PluginState {
|
|||
}
|
||||
this._executeCommand(this.selectedCommandIndex, state);
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
this._closePopup();
|
||||
} else if (event.key === "z" && (event.ctrlKey || event.metaKey)) {
|
||||
this._closePopup();
|
||||
|
|
@ -396,11 +472,11 @@ class PluginState {
|
|||
event.stopPropagation();
|
||||
const target = event.target as HTMLElement;
|
||||
// Find the command
|
||||
const item = target.closest(".popup-item");
|
||||
const item = target.closest(".popup-item") as HTMLElement;
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const index = Array.from(item.parentElement!.children).indexOf(item);
|
||||
const index = parseInt(item.dataset.commandId || "-1", 10);
|
||||
|
||||
this._executeCommand(index, state);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ let io: {
|
|||
};
|
||||
accepted: boolean;
|
||||
useBuiltInExport: boolean;
|
||||
deferred: _ZoteroTypes.DeferredPromise<void>;
|
||||
deferred: _ZoteroTypes.Promise.DeferredPromise<void>;
|
||||
embedLink: boolean;
|
||||
standaloneLink: boolean;
|
||||
exportNote: boolean;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ let io: {
|
|||
currentNoteID: number;
|
||||
currentLineIndex?: number;
|
||||
openedNoteIDs?: number[];
|
||||
deferred: _ZoteroTypes.DeferredPromise<void>;
|
||||
deferred: _ZoteroTypes.Promise.DeferredPromise<void>;
|
||||
|
||||
targetNoteID?: number;
|
||||
content?: string;
|
||||
|
|
@ -40,7 +40,7 @@ window.onunload = function () {
|
|||
function init() {
|
||||
// Set font size from pref
|
||||
const sbc = document.getElementById("top-container");
|
||||
Zotero.UIProperties.registerRoot(sbc);
|
||||
Zotero.UIProperties.registerRoot(sbc!);
|
||||
|
||||
setTimeout(() => {
|
||||
const size = ((getPref("windows.linkCreator.size") as string) || "").split(
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ let args = window.arguments[0] as any;
|
|||
if (!args._initPromise) {
|
||||
args = args.wrappedJSObject;
|
||||
}
|
||||
const templateData = args.templates;
|
||||
const templateData = (args.templates as string[]) || [];
|
||||
templateData.sort();
|
||||
const multiSelect = args.multiSelect;
|
||||
let tableHelper: VirtualizedTableHelper;
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ function accept() {
|
|||
);
|
||||
}
|
||||
|
||||
// @ts-ignore - plugin instance
|
||||
const getString = (Zotero[config.addonRef] as typeof addon).api.utils.getString;
|
||||
|
||||
function initTable() {
|
||||
|
|
|
|||
20
src/hooks.ts
20
src/hooks.ts
|
|
@ -48,6 +48,7 @@ import { showUserGuide } from "./modules/userGuide";
|
|||
import { refreshTemplatesInNote } from "./modules/template/refresh";
|
||||
import { closeParsingServer } from "./utils/parsing";
|
||||
import { patchExportItems } from "./modules/exportItems";
|
||||
import { patchOpenTabMenu } from "./modules/openTabMenu";
|
||||
|
||||
async function onStartup() {
|
||||
await Promise.all([
|
||||
|
|
@ -79,7 +80,10 @@ async function onStartup() {
|
|||
|
||||
setSyncing();
|
||||
|
||||
await onMainWindowLoad(Zotero.getMainWindow());
|
||||
await Promise.all(Zotero.getMainWindows().map(onMainWindowLoad));
|
||||
|
||||
// For testing
|
||||
addon.data.initialized = true;
|
||||
}
|
||||
|
||||
async function onMainWindowLoad(win: _ZoteroTypes.MainWindow): Promise<void> {
|
||||
|
|
@ -102,7 +106,10 @@ async function onMainWindowLoad(win: _ZoteroTypes.MainWindow): Promise<void> {
|
|||
|
||||
patchExportItems(win);
|
||||
|
||||
restoreNoteTabs();
|
||||
// TEMP: This doesn't work, maybe better to wait for the support from Zotero
|
||||
// patchOpenTabMenu(win);
|
||||
|
||||
await restoreNoteTabs();
|
||||
|
||||
showUserGuide(win);
|
||||
}
|
||||
|
|
@ -117,6 +124,7 @@ function onShutdown(): void {
|
|||
ztoolkit.unregisterAll();
|
||||
// Remove addon object
|
||||
addon.data.alive = false;
|
||||
// @ts-ignore plugin instance
|
||||
delete Zotero[config.addonInstance];
|
||||
}
|
||||
|
||||
|
|
@ -133,6 +141,13 @@ async function onNotify(
|
|||
if (extraData?.skipBN) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
["add", "close"].includes(event) &&
|
||||
type === "tab" &&
|
||||
extraData[ids[0]]?.type === "note"
|
||||
) {
|
||||
Zotero.Session.debounceSave();
|
||||
}
|
||||
if (event === "select" && type === "tab") {
|
||||
onTabSelect(extraData[ids[0]].type);
|
||||
}
|
||||
|
|
@ -297,6 +312,5 @@ export default {
|
|||
onCreateNoteFromTemplate,
|
||||
onCreateNoteFromMD,
|
||||
onCreateNote,
|
||||
restoreNoteTabs,
|
||||
onShowUserGuide,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { config } from "../package.json";
|
|||
|
||||
const basicTool = new BasicTool();
|
||||
|
||||
// @ts-ignore - plugin instance
|
||||
if (!basicTool.getGlobal("Zotero")[config.addonInstance]) {
|
||||
// Set global variables
|
||||
defineGlobal("window");
|
||||
|
|
@ -16,6 +17,7 @@ if (!basicTool.getGlobal("Zotero")[config.addonInstance]) {
|
|||
return _globalThis.addon.data.ztoolkit;
|
||||
},
|
||||
});
|
||||
// @ts-ignore - plugin instance
|
||||
Zotero[config.addonInstance] = addon;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,48 +12,101 @@ function registerReaderAnnotationButton() {
|
|||
(event) => {
|
||||
const { doc, append, params, reader } = event;
|
||||
const annotationData = params.annotation;
|
||||
append(
|
||||
ztoolkit.UI.createElement(doc, "div", {
|
||||
tag: "div",
|
||||
classList: ["icon"],
|
||||
properties: {
|
||||
innerHTML: ICONS.readerQuickNote,
|
||||
const button = ztoolkit.UI.createElement(doc, "div", {
|
||||
classList: ["icon"],
|
||||
properties: {
|
||||
innerHTML: getAnnotationNoteButtonInnerHTML(false),
|
||||
title: getAnnotationNoteButtonTitle(false),
|
||||
},
|
||||
listeners: [
|
||||
{
|
||||
type: "click",
|
||||
listener: (e) => {
|
||||
const button = e.currentTarget as HTMLElement;
|
||||
createNoteFromAnnotation(
|
||||
reader._item.libraryID,
|
||||
annotationData.id,
|
||||
(e as MouseEvent).shiftKey ? "window" : "builtin",
|
||||
);
|
||||
button.innerHTML = getAnnotationNoteButtonInnerHTML(true);
|
||||
e.preventDefault();
|
||||
},
|
||||
},
|
||||
listeners: [
|
||||
{
|
||||
type: "click",
|
||||
listener: (e) => {
|
||||
createNoteFromAnnotation(
|
||||
reader._item.libraryID,
|
||||
annotationData.id,
|
||||
(e as MouseEvent).shiftKey ? "window" : "builtin",
|
||||
);
|
||||
e.preventDefault();
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "mouseover",
|
||||
listener: (e) => {
|
||||
(e.target as HTMLElement).style.backgroundColor = "#F0F0F0";
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "mouseout",
|
||||
listener: (e) => {
|
||||
(e.target as HTMLElement).style.removeProperty(
|
||||
"background-color",
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
enableElementRecord: false,
|
||||
}),
|
||||
],
|
||||
enableElementRecord: false,
|
||||
});
|
||||
updateAnnotationNoteButton(
|
||||
button,
|
||||
reader._item.libraryID,
|
||||
annotationData.id,
|
||||
);
|
||||
append(button);
|
||||
},
|
||||
config.addonID,
|
||||
);
|
||||
}
|
||||
|
||||
function getAnnotationNoteButtonInnerHTML(hasNote: boolean) {
|
||||
return `${hasNote ? ICONS.openInNewWindow : ICONS.readerQuickNote}
|
||||
<style>
|
||||
.icon {
|
||||
border-radius: 4px;
|
||||
color: #ffd400;
|
||||
}
|
||||
.icon:hover {
|
||||
background-color: var(--fill-quinary);
|
||||
outline: 2px solid var(--fill-quinary);
|
||||
}
|
||||
.icon:active {
|
||||
background-color: var(--fill-quarternary);
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
function getAnnotationNoteButtonTitle(hasNote: boolean) {
|
||||
return hasNote ? "Open note" : "Create note from annotation";
|
||||
}
|
||||
|
||||
function updateAnnotationNoteButton(
|
||||
button: HTMLElement,
|
||||
libraryID: number,
|
||||
itemKey: string,
|
||||
) {
|
||||
hasNoteFromAnnotation(libraryID, itemKey).then((hasNote) => {
|
||||
button.innerHTML = getAnnotationNoteButtonInnerHTML(hasNote);
|
||||
button.title = getAnnotationNoteButtonTitle(hasNote);
|
||||
});
|
||||
}
|
||||
|
||||
async function hasNoteFromAnnotation(
|
||||
libraryID: number,
|
||||
itemKey: string,
|
||||
): Promise<boolean> {
|
||||
const annotationItem = Zotero.Items.getByLibraryAndKey(
|
||||
libraryID,
|
||||
itemKey,
|
||||
) as Zotero.Item;
|
||||
if (!annotationItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const linkTarget = await addon.api.relation.getLinkTargetByAnnotation(
|
||||
annotationItem.libraryID,
|
||||
annotationItem.key,
|
||||
);
|
||||
if (linkTarget) {
|
||||
const targetItem = Zotero.Items.getByLibraryAndKey(
|
||||
linkTarget.toLibID,
|
||||
linkTarget.toKey,
|
||||
);
|
||||
if (targetItem) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function createNoteFromAnnotation(
|
||||
libraryID: number,
|
||||
itemKey: string,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export function patchExportItems(win: _ZoteroTypes.MainWindow) {
|
|||
const Zotero_File_Interface = win.Zotero_File_Interface;
|
||||
new PatchHelper().setData({
|
||||
target: Zotero_File_Interface,
|
||||
funcSign: "exportItems",
|
||||
funcSign: "exportItems" as any,
|
||||
patcher: (origin) =>
|
||||
function () {
|
||||
if (!getPref("exportNotes.takeover")) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
export function patchOpenTabMenu(win: _ZoteroTypes.MainWindow) {
|
||||
const Zotero_Tabs = win.Zotero_Tabs;
|
||||
const popupset = win.document.querySelector("popupset")!;
|
||||
const observer = new win.MutationObserver(async () => {
|
||||
await new Promise((resolve) =>
|
||||
requestIdleCallback(resolve, { timeout: 100 }),
|
||||
);
|
||||
// Find menupopup menuitem[label="${Zotero.getString('general.showInLibrary')}"]
|
||||
const menupopup = popupset.querySelector(
|
||||
`menupopup menuitem[label="${Zotero.getString("tabs.move")}"]`,
|
||||
)?.parentNode as XULPopupElement;
|
||||
ztoolkit.log("openTabMenu observer", popupset.children);
|
||||
if (menupopup && !menupopup.dataset.patched) {
|
||||
menupopup.dataset.patched = "true";
|
||||
const menuitem = menupopup.querySelector(
|
||||
`menuitem[label="${Zotero.getString("general.showInLibrary")}"]`,
|
||||
);
|
||||
ztoolkit.log("openTabMenu observer");
|
||||
|
||||
menuitem?.addEventListener("command", () => {
|
||||
const { tab } = Zotero_Tabs._getTab(Zotero_Tabs.selectedID);
|
||||
if (tab && tab.type === "note") {
|
||||
let itemID = tab.data.itemID;
|
||||
const item = Zotero.Items.get(itemID);
|
||||
if (item && item.parentItemID) {
|
||||
itemID = item.parentItemID;
|
||||
}
|
||||
win.ZoteroPane.selectItem(itemID);
|
||||
}
|
||||
});
|
||||
// observer.disconnect();
|
||||
}
|
||||
});
|
||||
observer.observe(popupset, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
function openNoteTabMenu(
|
||||
x: number,
|
||||
y: number,
|
||||
id: string,
|
||||
win: _ZoteroTypes.MainWindow,
|
||||
) {
|
||||
const Zotero_Tabs = win.Zotero_Tabs;
|
||||
const doc = win.document;
|
||||
const { tab, tabIndex } = Zotero_Tabs._getTab(id);
|
||||
let menuitem;
|
||||
const popup = doc.createXULElement("menupopup") as XULPopupElement;
|
||||
doc.querySelector("popupset")!.appendChild(popup);
|
||||
popup.addEventListener("popuphidden", function (event) {
|
||||
if (event.target === popup) {
|
||||
popup.remove();
|
||||
}
|
||||
});
|
||||
if (id !== "zotero-pane") {
|
||||
// Show in library
|
||||
menuitem = doc.createXULElement("menuitem");
|
||||
menuitem.setAttribute("label", Zotero.getString("general.showInLibrary"));
|
||||
menuitem.addEventListener("command", () => {
|
||||
if (tab) {
|
||||
let itemID = tab.data.itemID;
|
||||
const item = Zotero.Items.get(itemID);
|
||||
if (item && item.parentItemID) {
|
||||
itemID = item.parentItemID;
|
||||
}
|
||||
ZoteroPane_Local.selectItem(itemID);
|
||||
}
|
||||
});
|
||||
popup.appendChild(menuitem);
|
||||
// Move tab
|
||||
const menu = doc.createXULElement("menu");
|
||||
menu.setAttribute("label", Zotero.getString("tabs.move"));
|
||||
const menupopup = doc.createXULElement("menupopup");
|
||||
menu.append(menupopup);
|
||||
popup.appendChild(menu);
|
||||
// Move to start
|
||||
menuitem = doc.createXULElement("menuitem");
|
||||
menuitem.setAttribute("label", Zotero.getString("tabs.moveToStart"));
|
||||
menuitem.setAttribute("disabled", tabIndex == 1 ? "true" : "false");
|
||||
menuitem.addEventListener("command", () => {
|
||||
Zotero_Tabs.move(id, 1);
|
||||
});
|
||||
menupopup.appendChild(menuitem);
|
||||
// Move to end
|
||||
menuitem = doc.createXULElement("menuitem");
|
||||
menuitem.setAttribute("label", Zotero.getString("tabs.moveToEnd"));
|
||||
menuitem.setAttribute(
|
||||
"disabled",
|
||||
tabIndex == Zotero_Tabs._tabs.length - 1 ? "true" : "false",
|
||||
);
|
||||
menuitem.addEventListener("command", () => {
|
||||
Zotero_Tabs.move(id, Zotero_Tabs._tabs.length);
|
||||
});
|
||||
menupopup.appendChild(menuitem);
|
||||
// Move to new window
|
||||
menuitem = doc.createXULElement("menuitem");
|
||||
menuitem.setAttribute("label", Zotero.getString("tabs.moveToWindow"));
|
||||
menuitem.setAttribute("disabled", "false");
|
||||
menuitem.addEventListener("command", () => {
|
||||
const { tab } = Zotero_Tabs._getTab(id);
|
||||
Zotero_Tabs.close(id);
|
||||
const { itemID } = tab.data;
|
||||
addon.hooks.onOpenNote(itemID, "window", { forceTakeover: true });
|
||||
});
|
||||
menupopup.appendChild(menuitem);
|
||||
// Duplicate tab
|
||||
// menuitem = doc.createXULElement("menuitem");
|
||||
// menuitem.setAttribute("label", Zotero.getString("tabs.duplicate"));
|
||||
// menuitem.addEventListener("command", () => {
|
||||
// if (tab.data.itemID) {
|
||||
// const { secondViewState } = tab.data;
|
||||
// Zotero.Reader.open(tab.data.itemID, null, {
|
||||
// tabIndex: tabIndex + 1,
|
||||
// allowDuplicate: true,
|
||||
// secondViewState,
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// popup.appendChild(menuitem);
|
||||
// Separator
|
||||
popup.appendChild(doc.createXULElement("menuseparator"));
|
||||
}
|
||||
// Close
|
||||
if (id != "zotero-pane") {
|
||||
menuitem = doc.createXULElement("menuitem");
|
||||
menuitem.setAttribute("label", Zotero.getString("general.close"));
|
||||
menuitem.addEventListener("command", () => {
|
||||
Zotero_Tabs.close(id);
|
||||
});
|
||||
popup.appendChild(menuitem);
|
||||
}
|
||||
// Close other tabs
|
||||
if (!(Zotero_Tabs._tabs.length == 2 && id != "zotero-pane")) {
|
||||
menuitem = doc.createXULElement("menuitem");
|
||||
menuitem.setAttribute("label", Zotero.getString("tabs.closeOther"));
|
||||
menuitem.addEventListener("command", () => {
|
||||
Zotero_Tabs.close(
|
||||
Zotero_Tabs._tabs
|
||||
.slice(1)
|
||||
.filter((x) => x.id != id)
|
||||
.map((x) => x.id),
|
||||
);
|
||||
});
|
||||
popup.appendChild(menuitem);
|
||||
}
|
||||
// Undo close
|
||||
menuitem = doc.createXULElement("menuitem");
|
||||
menuitem.setAttribute(
|
||||
"label",
|
||||
Zotero.getString(
|
||||
"tabs.undoClose",
|
||||
[],
|
||||
// If not disabled, show proper plural for tabs to reopen
|
||||
// @ts-ignore
|
||||
Zotero_Tabs._history.length
|
||||
? // @ts-ignore
|
||||
Zotero_Tabs._history[Zotero_Tabs._history.length - 1].length
|
||||
: 1,
|
||||
),
|
||||
);
|
||||
menuitem.setAttribute(
|
||||
"disabled",
|
||||
// @ts-ignore
|
||||
!Zotero_Tabs._history.length ? "true" : "false",
|
||||
);
|
||||
menuitem.addEventListener("command", () => {
|
||||
Zotero_Tabs.undoClose();
|
||||
});
|
||||
popup.appendChild(menuitem);
|
||||
popup.openPopupAtScreen(x, y, true);
|
||||
}
|
||||
|
|
@ -3,7 +3,12 @@ import { itemPicker } from "../../utils/itemPicker";
|
|||
import { getString } from "../../utils/locale";
|
||||
import { fill, slice } from "../../utils/str";
|
||||
|
||||
export { runTemplate, runTextTemplate, runItemTemplate };
|
||||
export {
|
||||
runTemplate,
|
||||
runTextTemplate,
|
||||
runItemTemplate,
|
||||
runQuickInsertTemplate,
|
||||
};
|
||||
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
||||
|
||||
|
|
@ -237,6 +242,26 @@ async function runItemTemplate(
|
|||
return renderedString;
|
||||
}
|
||||
|
||||
async function runQuickInsertTemplate(
|
||||
noteItem: Zotero.Item,
|
||||
targetNoteItem: Zotero.Item,
|
||||
options: {
|
||||
dryRun?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
if (!noteItem || !targetNoteItem) return "";
|
||||
const link = addon.api.convert.note2link(noteItem, {});
|
||||
const content = await runTemplate(
|
||||
"[QuickInsertV2]",
|
||||
"link, linkText, subNoteItem, noteItem",
|
||||
[link, noteItem.getNoteTitle().trim() || link, noteItem, targetNoteItem],
|
||||
{
|
||||
dryRun: options.dryRun,
|
||||
},
|
||||
);
|
||||
return content;
|
||||
}
|
||||
|
||||
async function getItemTemplateData() {
|
||||
// If topItems are pre-defined, use it without asking
|
||||
if (addon.data.template.picker.data.topItemIds?.length > 0) {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,12 @@ function removeTemplate(keyName: string | undefined): void {
|
|||
addon.data.template.data?.deleteKey(keyName);
|
||||
}
|
||||
|
||||
function importTemplateFromClipboard(text?: string) {
|
||||
function importTemplateFromClipboard(
|
||||
text?: string,
|
||||
options: {
|
||||
quiet?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
if (!text) {
|
||||
text = Zotero.Utilities.Internal.getClipboard("text/plain") || "";
|
||||
}
|
||||
|
|
@ -83,9 +88,16 @@ function importTemplateFromClipboard(text?: string) {
|
|||
showHint("The copied template is invalid");
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(`Import template "${template.name}"?`)) {
|
||||
if (
|
||||
!options.quiet &&
|
||||
!window.confirm(`Import template "${template.name}"?`)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setTemplate({ name: template.name, text: template.content });
|
||||
showHint(`Template ${template.name} saved.`);
|
||||
if (addon.data.template.editor.window) {
|
||||
addon.data.template.editor.window.refresh();
|
||||
}
|
||||
return template.name;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export async function showTemplateEditor() {
|
|||
(Zotero.isMac && event.key == "Backspace")
|
||||
) {
|
||||
addon.api.template.removeTemplate(getSelectedTemplateName());
|
||||
refresh();
|
||||
refresh(true);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -193,12 +193,15 @@ export async function showTemplateEditor() {
|
|||
?.addEventListener("command", (ev) => {
|
||||
updateSnippets((ev.target as XULMenuListElement)?.value);
|
||||
});
|
||||
// An ugly hack to make the editor refresh exposed
|
||||
_window.refresh = refresh;
|
||||
addon.data.template.editor.window?.focus();
|
||||
const editorWin = (_window.document.querySelector("#editor") as any)
|
||||
.contentWindow;
|
||||
await waitUtilAsync(() => editorWin?.loadMonaco);
|
||||
const isDark = editorWin?.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches;
|
||||
const isDark = editorWin?.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
).matches;
|
||||
const { monaco, editor } = await editorWin.loadMonaco({
|
||||
language: "javascript",
|
||||
theme: "vs-" + (isDark ? "dark" : "light"),
|
||||
|
|
@ -209,7 +212,18 @@ export async function showTemplateEditor() {
|
|||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
async function refresh(force = false) {
|
||||
const win = addon.data.template.editor.window;
|
||||
if (!win) {
|
||||
return;
|
||||
}
|
||||
if (!force && isTemplateNotSaved()) {
|
||||
const save = win.confirm(getString("alert-templateEditor-unsaved"));
|
||||
if (save) {
|
||||
saveSelectedTemplate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
updateData();
|
||||
updateTable();
|
||||
updateEditor();
|
||||
|
|
@ -259,6 +273,28 @@ function getRowLabelColor(type: string) {
|
|||
}
|
||||
}
|
||||
|
||||
function isTemplateNotSaved() {
|
||||
const name = getSelectedTemplateName();
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
const text = addon.data.template.editor.editor?.getValue() as string;
|
||||
const savedText = addon.api.template.getTemplateText(name);
|
||||
if (text !== savedText) {
|
||||
return true;
|
||||
}
|
||||
const { type, name: displayName } = getRowData(getSelectedIndex());
|
||||
const templateType =
|
||||
addon.data.template.editor.window?.document.querySelector(
|
||||
"#editor-type",
|
||||
) as XULMenuListElement;
|
||||
const templateName =
|
||||
addon.data.template.editor.window?.document.querySelector(
|
||||
"#editor-name",
|
||||
) as HTMLInputElement;
|
||||
return type !== templateType.value || displayName !== templateName.value;
|
||||
}
|
||||
|
||||
function updateData() {
|
||||
addon.data.template.editor.templates = addon.api.template.getTemplateKeys();
|
||||
}
|
||||
|
|
@ -582,7 +618,7 @@ function saveSelectedTemplate() {
|
|||
);
|
||||
if (useImport) {
|
||||
addon.hooks.onImportTemplateFromClipboard(template.text);
|
||||
refresh();
|
||||
refresh(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -596,7 +632,7 @@ function saveSelectedTemplate() {
|
|||
addon.data.template.editor.tableHelper?.treeInstance.selection.selected
|
||||
.values()
|
||||
.next().value;
|
||||
refresh().then(() => updateTable(selectedId));
|
||||
refresh(true).then(() => updateTable(selectedId));
|
||||
}
|
||||
|
||||
function deleteSelectedTemplate() {
|
||||
|
|
@ -608,7 +644,7 @@ function deleteSelectedTemplate() {
|
|||
return;
|
||||
}
|
||||
addon.api.template.removeTemplate(name);
|
||||
refresh();
|
||||
refresh(true);
|
||||
}
|
||||
|
||||
function resetSelectedTemplate() {
|
||||
|
|
@ -698,7 +734,7 @@ async function restoreTemplates(win: Window) {
|
|||
}
|
||||
addon.api.template.setTemplate(t);
|
||||
}
|
||||
await refresh();
|
||||
await refresh(true);
|
||||
}
|
||||
|
||||
const formatStore = [
|
||||
|
|
|
|||
|
|
@ -20,14 +20,19 @@ async function showTemplatePicker(
|
|||
mode: "export",
|
||||
data?: Record<string, never>,
|
||||
): Promise<void>;
|
||||
async function showTemplatePicker(): Promise<void>;
|
||||
async function showTemplatePicker(mode: "pick"): Promise<string[]>;
|
||||
async function showTemplatePicker(): Promise<any>;
|
||||
async function showTemplatePicker(
|
||||
mode: typeof addon.data.template.picker.mode = "insert",
|
||||
data: Record<string, any> = {},
|
||||
) {
|
||||
): Promise<unknown> {
|
||||
addon.data.template.picker.mode = mode;
|
||||
addon.data.template.picker.data = data;
|
||||
const selected = await openTemplatePicker();
|
||||
// For pick mode, return selected templates
|
||||
if (mode === "pick") {
|
||||
return selected;
|
||||
}
|
||||
if (!selected.length) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -68,11 +73,12 @@ async function insertTemplateCallback(name: string) {
|
|||
targetNoteId: targetNoteItem.id,
|
||||
});
|
||||
}
|
||||
await addLineToNote(
|
||||
targetNoteItem,
|
||||
html,
|
||||
addon.data.template.picker.data.lineIndex,
|
||||
);
|
||||
let lineIndex = addon.data.template.picker.data.lineIndex;
|
||||
// Insert to the end of the line
|
||||
if (lineIndex >= 0) {
|
||||
lineIndex += 1;
|
||||
}
|
||||
await addLineToNote(targetNoteItem, html, lineIndex);
|
||||
}
|
||||
|
||||
async function createTemplateNoteCallback(name: string) {
|
||||
|
|
|
|||
|
|
@ -93,25 +93,6 @@ async function renderTemplatePreview(
|
|||
},
|
||||
);
|
||||
}
|
||||
} else if (templateName.includes("QuickBackLink")) {
|
||||
// link, linkText, subNoteItem, noteItem
|
||||
const data = inputItems?.find((item) => item.isNote());
|
||||
if (!data) {
|
||||
html = messages.noNoteItem;
|
||||
} else {
|
||||
const link = getNoteLink(data);
|
||||
const noteItem = new Zotero.Item("note");
|
||||
const linkText = noteItem.getNoteTitle().trim() || "Workspace Note";
|
||||
const subNoteItem = data;
|
||||
html = await addon.api.template.runTemplate(
|
||||
templateName,
|
||||
"link, linkText, subNoteItem, noteItem",
|
||||
[link, linkText, subNoteItem, noteItem],
|
||||
{
|
||||
dryRun: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
} else if (templateName.includes("QuickImport")) {
|
||||
// link, noteItem
|
||||
const data = inputItems?.find((item) => item.isNote());
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ async function renderSection(
|
|||
outbound: addon.api.relation.getNoteLinkOutboundRelation,
|
||||
};
|
||||
const inLinks = await api[type](item.id);
|
||||
let count = 0;
|
||||
for (const linkData of inLinks) {
|
||||
const targetItem = (await Zotero.Items.getByLibraryAndKeyAsync(
|
||||
linkData[
|
||||
|
|
@ -108,7 +109,12 @@ async function renderSection(
|
|||
linkData[
|
||||
{ inbound: "fromKey", outbound: "toKey" }[type] as "fromKey" | "toKey"
|
||||
],
|
||||
)) as Zotero.Item;
|
||||
)) as Zotero.Item | false;
|
||||
|
||||
if (!targetItem) {
|
||||
continue;
|
||||
}
|
||||
count++;
|
||||
|
||||
const linkParams = {
|
||||
workspaceUID: (body.closest("bn-workspace") as Workspace)?.dataset.uid,
|
||||
|
|
@ -160,7 +166,6 @@ async function renderSection(
|
|||
body.append(row);
|
||||
}
|
||||
|
||||
const count = inLinks.length;
|
||||
setCount(count);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export function openNotePreview(
|
|||
l10nID: `${config.addonRef}-note-preview-open`,
|
||||
onClick: ({ event }) => {
|
||||
const position = (event as MouseEvent).shiftKey ? "window" : "tab";
|
||||
// @ts-ignore - plugin instance
|
||||
Zotero[config.addonRef].hooks.onOpenNote(noteItem.id, position);
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -60,16 +60,17 @@ export function onTabSelect(tabType: string) {
|
|||
ZoteroContextPane.update();
|
||||
}
|
||||
|
||||
export function restoreNoteTabs() {
|
||||
export async function restoreNoteTabs() {
|
||||
const tabsCache: _ZoteroTypes.TabInstance[] =
|
||||
Zotero.Session.state.windows.find((x: any) => x.type == "pane")?.tabs;
|
||||
for (const i in tabsCache) {
|
||||
const tab = tabsCache[i];
|
||||
if (tab.type !== TAB_TYPE) continue;
|
||||
openWorkspaceTab(Zotero.Items.get(tab.data.itemID), {
|
||||
openWorkspaceTab(await Zotero.Items.getAsync(tab.data.itemID), {
|
||||
select: !!tab.selected,
|
||||
});
|
||||
}
|
||||
Zotero.Session.debounceSave();
|
||||
}
|
||||
|
||||
export function onUpdateNoteTabsTitle(noteItems: Zotero.Item[]) {
|
||||
|
|
|
|||
|
|
@ -161,11 +161,11 @@ function serializeAnnotations(
|
|||
}
|
||||
|
||||
let template: string = "";
|
||||
if (annotation.type === "highlight") {
|
||||
if (["highlight", "underline"].includes(annotation.type)) {
|
||||
template = Zotero.Prefs.get(
|
||||
"annotations.noteTemplates.highlight",
|
||||
) as string;
|
||||
} else if (annotation.type === "note") {
|
||||
} else if (["note", "text"].includes(annotation.type)) {
|
||||
template = Zotero.Prefs.get("annotations.noteTemplates.note") as string;
|
||||
} else if (annotation.type === "image") {
|
||||
template = "<p>{{image}}<br/>{{citation}} {{comment}}</p>";
|
||||
|
|
|
|||
|
|
@ -262,18 +262,23 @@ function annotations2html(
|
|||
|
||||
async function note2html(
|
||||
noteItems: Zotero.Item | Zotero.Item[],
|
||||
options: { targetNoteItem?: Zotero.Item; html?: string } = {},
|
||||
options: {
|
||||
targetNoteItem?: Zotero.Item;
|
||||
html?: string;
|
||||
dryRun?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
if (!Array.isArray(noteItems)) {
|
||||
noteItems = [noteItems];
|
||||
}
|
||||
const { targetNoteItem } = options;
|
||||
const { targetNoteItem, dryRun } = options;
|
||||
let html = options.html;
|
||||
if (!html) {
|
||||
html = noteItems.map((item) => item.getNote()).join("\n");
|
||||
}
|
||||
if (targetNoteItem?.isNote()) {
|
||||
return await copyEmbeddedImagesInHTML(html, targetNoteItem, noteItems);
|
||||
if (!dryRun && targetNoteItem?.isNote()) {
|
||||
const str = await copyEmbeddedImagesInHTML(html, targetNoteItem, noteItems);
|
||||
return str;
|
||||
}
|
||||
return await renderNoteHTML(html, noteItems);
|
||||
}
|
||||
|
|
@ -417,24 +422,35 @@ async function rehype2remark(rehype: HRoot) {
|
|||
* ```
|
||||
*/
|
||||
li: (h, node) => {
|
||||
const mnode = defaultHandlers.li(h, node) as ListContent;
|
||||
const mNode = defaultHandlers.li(h, node) as ListContent;
|
||||
// If no more than 1 children, skip
|
||||
if (!mnode || mnode.children.length < 2) {
|
||||
return mnode;
|
||||
if (!mNode || mNode.children.length < 2) {
|
||||
return mNode;
|
||||
}
|
||||
const children: any[] = [];
|
||||
const paragraphNodes = ["list", "code", "math", "table"];
|
||||
// Merge none-list nodes inside li into the previous paragraph node to avoid line break
|
||||
while (mnode.children.length > 0) {
|
||||
const current = mnode.children.shift();
|
||||
while (mNode.children.length > 0) {
|
||||
const current = mNode.children.shift();
|
||||
const cached = children[children.length - 1];
|
||||
if (cached?.type === "paragraph" && current?.type !== "list") {
|
||||
cached.children.push(current);
|
||||
if (current?.type && !paragraphNodes.includes(current?.type)) {
|
||||
if (cached?.type === "paragraph") {
|
||||
cached.children.push(current);
|
||||
} else {
|
||||
// https://github.com/windingwind/zotero-better-notes/issues/1207
|
||||
// Create a new paragraph node
|
||||
const paragraph = {
|
||||
type: "paragraph",
|
||||
children: [current],
|
||||
};
|
||||
children.push(paragraph);
|
||||
}
|
||||
} else {
|
||||
children.push(current);
|
||||
}
|
||||
}
|
||||
mnode.children.push(...children);
|
||||
return mnode;
|
||||
mNode.children.push(...children);
|
||||
return mNode;
|
||||
},
|
||||
wrapper: (h, node) => {
|
||||
return h(node, "wrapper", toText(node));
|
||||
|
|
@ -803,12 +819,12 @@ function processN2MRehypeHighlightNodes(
|
|||
const libraryType = uriParts[3];
|
||||
const key = uriParts[uriParts.length - 1];
|
||||
if (libraryType === "users") {
|
||||
openURI = "zotero://open-pdf/library/items/" + key;
|
||||
openURI = "zotero://open/library/items/" + key;
|
||||
}
|
||||
// groups
|
||||
else {
|
||||
const groupID = uriParts[4];
|
||||
openURI = "zotero://open-pdf/groups/" + groupID + "/items/" + key;
|
||||
openURI = "zotero://open/groups/" + groupID + "/items/" + key;
|
||||
}
|
||||
|
||||
openURI +=
|
||||
|
|
|
|||
|
|
@ -247,6 +247,13 @@ function getPositionAtLine(
|
|||
type: "start" | "end" = "end",
|
||||
): number {
|
||||
const core = getEditorCore(editor);
|
||||
const lineCount = getLineCount(editor);
|
||||
if (lineIndex < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (lineIndex >= lineCount) {
|
||||
return core.view.state.doc.content.size;
|
||||
}
|
||||
const lineNodeDesc =
|
||||
core.view.docView.children[
|
||||
Math.max(0, Math.min(core.view.docView.children.length - 1, lineIndex))
|
||||
|
|
@ -521,6 +528,9 @@ function initEditorPlugins(editor: Zotero.EditorInstance) {
|
|||
lineIndex: getLineAtCursor(editor),
|
||||
});
|
||||
},
|
||||
refreshTemplates: () => {
|
||||
addon.hooks.onRefreshTemplatesInNote(editor);
|
||||
},
|
||||
insertLink: (mode: "inbound" | "outbound") => {
|
||||
openLinkCreator(editor._item, {
|
||||
lineIndex: getLineAtCursor(editor),
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@ export function getNoteLinkParams(link: string) {
|
|||
if (id === "u") {
|
||||
libraryID = Zotero.Libraries.userLibraryID;
|
||||
} else {
|
||||
libraryID = Zotero.Groups.getLibraryIDFromGroupID(id);
|
||||
const libID = Zotero.Groups.getLibraryIDFromGroupID(Number(id));
|
||||
if (!libID) {
|
||||
throw new Error("Invalid group ID");
|
||||
}
|
||||
libraryID = libID;
|
||||
}
|
||||
const line = url.searchParams.get("line");
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -41,10 +41,20 @@ async function openLinkCreator(
|
|||
await io.deferred.promise;
|
||||
|
||||
const targetNote = Zotero.Items.get(io.targetNoteID);
|
||||
const content = io.content;
|
||||
const lineIndex = io.lineIndex;
|
||||
|
||||
if (!targetNote || !content) return;
|
||||
if (!targetNote) return;
|
||||
|
||||
const sourceNotes = Zotero.Items.get(io.sourceNoteIDs as number[]);
|
||||
let content = "";
|
||||
for (const note of sourceNotes) {
|
||||
content += await addon.api.template.runQuickInsertTemplate(
|
||||
note,
|
||||
targetNote,
|
||||
{ dryRun: false },
|
||||
);
|
||||
content += "\n";
|
||||
}
|
||||
const lineIndex = io.lineIndex;
|
||||
|
||||
await addLineToNote(targetNote, content, lineIndex);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,8 @@ async function addLineToNote(
|
|||
return;
|
||||
}
|
||||
const noteLines = await getLinesInNote(note);
|
||||
if (lineIndex < 0 || lineIndex >= noteLines.length) {
|
||||
// No need to handle the case when lineIndex is out of range, as it will always be inserted at the very end
|
||||
if (lineIndex < 0) {
|
||||
lineIndex = noteLines.length;
|
||||
}
|
||||
ztoolkit.log(`insert to ${lineIndex}, it used to be ${noteLines[lineIndex]}`);
|
||||
|
|
@ -86,7 +87,12 @@ async function addLineToNote(
|
|||
const editor = getEditorInstance(note.id);
|
||||
if (editor && !forceMetadata) {
|
||||
// The note is opened. Add line via note editor
|
||||
const pos = getPositionAtLine(editor, lineIndex, "start");
|
||||
// If the lineIndex is out of range, the line will be inserted at the end (after the last line)
|
||||
const pos = getPositionAtLine(
|
||||
editor,
|
||||
lineIndex,
|
||||
lineIndex >= noteLines.length ? "end" : "start",
|
||||
);
|
||||
ztoolkit.log("Add note line via note editor", pos);
|
||||
insert(editor, html, pos);
|
||||
// The selection is automatically moved to the next line
|
||||
|
|
@ -322,6 +328,10 @@ async function copyEmbeddedImagesFromNote(
|
|||
) {
|
||||
await Zotero.DB.executeTransaction(async () => {
|
||||
for (const fromNote of sourceNotes) {
|
||||
// Do not copy to itself, otherwise the note may break the DB
|
||||
if (!fromNote.id || !targetNote.id || fromNote.id === targetNote.id) {
|
||||
continue;
|
||||
}
|
||||
await Zotero.Notes.copyEmbeddedImages(fromNote, targetNote);
|
||||
}
|
||||
});
|
||||
|
|
@ -361,18 +371,27 @@ async function copyEmbeddedImagesInHTML(
|
|||
doc.querySelectorAll(`img[data-attachment-key="${attachment.key}"]`),
|
||||
) as HTMLImageElement[];
|
||||
if (nodes.length) {
|
||||
let copiedAttachment: Zotero.Item;
|
||||
let copiedAttachment: Zotero.Item | undefined;
|
||||
await Zotero.DB.executeTransaction(async () => {
|
||||
Zotero.DB.requireTransaction();
|
||||
// Do not copy to itself, otherwise the note may break the DB
|
||||
if (
|
||||
!attachment.parentID ||
|
||||
!targetNote.id ||
|
||||
attachment.parentID === targetNote.id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
copiedAttachment = await Zotero.Attachments.copyEmbeddedImage({
|
||||
attachment,
|
||||
note: targetNote,
|
||||
});
|
||||
});
|
||||
nodes.forEach(
|
||||
(node) =>
|
||||
node?.setAttribute("data-attachment-key", copiedAttachment.key),
|
||||
Node,
|
||||
if (!copiedAttachment) {
|
||||
continue;
|
||||
}
|
||||
nodes.forEach((node) =>
|
||||
node?.setAttribute("data-attachment-key", copiedAttachment!.key),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,38 @@
|
|||
import { config } from "../../package.json";
|
||||
|
||||
export function getPref(key: string) {
|
||||
return Zotero.Prefs.get(`${config.prefsPrefix}.${key}`, true);
|
||||
export {
|
||||
getPref,
|
||||
setPref,
|
||||
clearPref,
|
||||
getPrefJSON,
|
||||
registerPrefObserver,
|
||||
unregisterPrefObserver,
|
||||
};
|
||||
|
||||
type _PluginPrefsMap = _ZoteroTypes.Prefs["PluginPrefsMap"];
|
||||
|
||||
function getPref<K extends keyof _PluginPrefsMap>(key: K): _PluginPrefsMap[K];
|
||||
function getPref(key: string): string | number | boolean;
|
||||
function getPref(key: string): string | number | boolean {
|
||||
return Zotero.Prefs.get(`${config.prefsPrefix}.${key}`, true) as any;
|
||||
}
|
||||
|
||||
export function setPref(key: string, value: string | number | boolean) {
|
||||
function setPref<K extends keyof _PluginPrefsMap>(
|
||||
key: K,
|
||||
value: _PluginPrefsMap[K],
|
||||
): void;
|
||||
function setPref(key: string, value: string | number | boolean): void;
|
||||
function setPref(key: string, value: string | number | boolean) {
|
||||
return Zotero.Prefs.set(`${config.prefsPrefix}.${key}`, value, true);
|
||||
}
|
||||
|
||||
export function clearPref(key: string) {
|
||||
function clearPref<K extends keyof _PluginPrefsMap>(key: K): void;
|
||||
function clearPref(key: string): void;
|
||||
function clearPref(key: string) {
|
||||
return Zotero.Prefs.clear(`${config.prefsPrefix}.${key}`, true);
|
||||
}
|
||||
|
||||
export function getPrefJSON(key: string) {
|
||||
function getPrefJSON(key: string) {
|
||||
try {
|
||||
return JSON.parse(String(getPref(key) || "{}"));
|
||||
} catch (e) {
|
||||
|
|
@ -21,10 +41,15 @@ export function getPrefJSON(key: string) {
|
|||
return {};
|
||||
}
|
||||
|
||||
export function registerPrefObserver(
|
||||
function registerPrefObserver<K extends keyof _PluginPrefsMap>(
|
||||
key: K,
|
||||
callback: (value: _PluginPrefsMap[K]) => void,
|
||||
): symbol;
|
||||
function registerPrefObserver(
|
||||
key: string,
|
||||
callback: (value: any) => void,
|
||||
) {
|
||||
): symbol;
|
||||
function registerPrefObserver(key: string, callback: (value: any) => void) {
|
||||
return Zotero.Prefs.registerObserver(
|
||||
`${config.prefsPrefix}.${key}`,
|
||||
callback,
|
||||
|
|
@ -32,6 +57,6 @@ export function registerPrefObserver(
|
|||
);
|
||||
}
|
||||
|
||||
export function unregisterPrefObserver(observerID: symbol) {
|
||||
function unregisterPrefObserver(observerID: symbol) {
|
||||
return Zotero.Prefs.unregisterObserver(observerID);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ function closeRelationServer() {
|
|||
}
|
||||
}
|
||||
|
||||
async function getRelationServer() {
|
||||
async function getRelationServer(): Promise<MessageHelper<typeof handlers>> {
|
||||
if (!addon.data.relation.server) {
|
||||
const worker = new Worker(
|
||||
`chrome://${config.addonRef}/content/scripts/relationWorker.js`,
|
||||
|
|
@ -95,7 +95,9 @@ async function updateNoteLinkRelation(noteID: number) {
|
|||
);
|
||||
}
|
||||
|
||||
async function getNoteLinkOutboundRelation(noteID: number) {
|
||||
async function getNoteLinkOutboundRelation(
|
||||
noteID: number,
|
||||
): Promise<LinkModel[]> {
|
||||
const note = Zotero.Items.get(noteID);
|
||||
const fromLibID = note.libraryID;
|
||||
const fromKey = note.key;
|
||||
|
|
@ -104,7 +106,9 @@ async function getNoteLinkOutboundRelation(noteID: number) {
|
|||
).proxy.getOutboundLinks(fromLibID, fromKey);
|
||||
}
|
||||
|
||||
async function getNoteLinkInboundRelation(noteID: number) {
|
||||
async function getNoteLinkInboundRelation(
|
||||
noteID: number,
|
||||
): Promise<LinkModel[]> {
|
||||
const note = Zotero.Items.get(noteID);
|
||||
const toLibID = note.libraryID;
|
||||
const toKey = note.key;
|
||||
|
|
@ -135,13 +139,19 @@ async function linkAnnotationToTarget(model: AnnotationModel) {
|
|||
return await (await getRelationServer()).proxy.linkAnnotationToTarget(model);
|
||||
}
|
||||
|
||||
async function getLinkTargetByAnnotation(fromLibID: number, fromKey: string) {
|
||||
async function getLinkTargetByAnnotation(
|
||||
fromLibID: number,
|
||||
fromKey: string,
|
||||
): Promise<AnnotationModel | undefined> {
|
||||
return await (
|
||||
await getRelationServer()
|
||||
).proxy.getLinkTargetByAnnotation(fromLibID, fromKey);
|
||||
}
|
||||
|
||||
async function getAnnotationByLinkTarget(toLibID: number, toKey: string) {
|
||||
async function getAnnotationByLinkTarget(
|
||||
toLibID: number,
|
||||
toKey: string,
|
||||
): Promise<AnnotationModel | undefined> {
|
||||
return await (
|
||||
await getRelationServer()
|
||||
).proxy.getAnnotationByLinkTarget(toLibID, toKey);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,255 @@
|
|||
/* eslint-disable no-irregular-whitespace */
|
||||
/* eslint-disable no-useless-escape */
|
||||
import { ClipboardHelper } from "zotero-plugin-toolkit";
|
||||
import { getAddon } from "../utils/global";
|
||||
import { resetAll } from "../utils/status";
|
||||
import { getNoteContent, parseTemplateString } from "../utils/note";
|
||||
import { getTempDirectory } from "../utils/io";
|
||||
|
||||
describe("Export", function () {
|
||||
const addon = getAddon();
|
||||
this.beforeAll(async function () {
|
||||
await resetAll();
|
||||
});
|
||||
|
||||
this.afterEach(async function () {});
|
||||
|
||||
it("api.$export.saveMD", async function () {
|
||||
const note = new Zotero.Item("note");
|
||||
note.setNote(getNoteContent());
|
||||
await note.saveTx();
|
||||
|
||||
const tempDir = await getTempDirectory();
|
||||
|
||||
const filePath = PathUtils.join(tempDir, "test.md");
|
||||
|
||||
await getAddon().api.$export.saveMD(filePath, note.id, {
|
||||
keepNoteLink: true,
|
||||
withYAMLHeader: false,
|
||||
});
|
||||
|
||||
debug("Note saved to", filePath);
|
||||
|
||||
const content = await Zotero.File.getContentsAsync(filePath);
|
||||
|
||||
const expected = `# Markdown Test Document
|
||||
|
||||
## Headers
|
||||
|
||||
# H1 Header
|
||||
|
||||
## H2 Header
|
||||
|
||||
### H3 Header
|
||||
|
||||
#### H4 Header
|
||||
|
||||
##### H5 Header
|
||||
|
||||
###### H6 Header
|
||||
|
||||
## Emphasis
|
||||
|
||||
*This text is italicized.* *This text is also italicized.*
|
||||
|
||||
**This text is bold.** **This text is also bold.**
|
||||
|
||||
***This text is bold and italicized.*** ***This text is also bold and italicized.***
|
||||
|
||||
## Links
|
||||
|
||||
[Link with title](https://example.com "Title") [Link without title](https://example.com)
|
||||
|
||||
## Images
|
||||
|
||||
## Blockquotes
|
||||
|
||||
> This is a blockquote.
|
||||
>
|
||||
> > Nested blockquote.
|
||||
>
|
||||
> Back to the outer blockquote.
|
||||
|
||||
## Lists
|
||||
|
||||
### Unordered List
|
||||
|
||||
* Item 1
|
||||
|
||||
* Subitem 1.1
|
||||
|
||||
* Subitem 1.1.1
|
||||
|
||||
* Item 2
|
||||
|
||||
### Ordered List
|
||||
|
||||
1. First item
|
||||
|
||||
1. Subitem 1.1
|
||||
|
||||
1. Subitem 1.1.1
|
||||
|
||||
2. Second item
|
||||
|
||||
## Code
|
||||
|
||||
### Inline Code
|
||||
|
||||
Here is some \`inline code\`.
|
||||
|
||||
### Code Block
|
||||
|
||||
\`\`\`
|
||||
def hello_world():
|
||||
print("Hello, world!")
|
||||
|
||||
\`\`\`
|
||||
|
||||
## Horizontal Rules
|
||||
|
||||
***
|
||||
|
||||
This is text between horizontal rules
|
||||
|
||||
***
|
||||
|
||||
## Tables
|
||||
|
||||
| Header 1 | Header 2 | Header 3 |
|
||||
| -------- | -------- | -------- |
|
||||
| Row 1 | Data 1.2 | Data 1.3 |
|
||||
| Row 2 | Data 2.2 | Data 2.3 |
|
||||
|
||||
## Math
|
||||
|
||||
### Inline Math
|
||||
|
||||
This is an inline math equation: \$E = mc^2\$.
|
||||
|
||||
### Block Math
|
||||
|
||||
Below is a block math equation:
|
||||
|
||||
\$\$
|
||||
\\\\int_a^b f(x) \\\\, dx = F(b) - F(a)
|
||||
\$\$
|
||||
|
||||
### Complex Math
|
||||
|
||||
Solve the quadratic equation:
|
||||
|
||||
\$\$
|
||||
x = \\\\frac\{-b \\\\pm \\\\sqrt\{b^2 - 4ac\}\}\{2a\}
|
||||
\$\$
|
||||
|
||||
## Nested Elements
|
||||
|
||||
### Nested Code and Lists
|
||||
|
||||
1. Ordered list item
|
||||
|
||||
* Unordered subitem
|
||||
|
||||
\`\`\`
|
||||
console.log("Nested code block");
|
||||
|
||||
\`\`\`
|
||||
|
||||
2. This is a nested math
|
||||
|
||||
\$\$
|
||||
y=x^2
|
||||
\$\$
|
||||
|
||||
3. This is a inline math\$123\$
|
||||
|
||||
4. This is a line table
|
||||
|
||||
| | | |
|
||||
| - | - | - |
|
||||
| 1 | 2 | 3 |
|
||||
| 4 | 5 | 6 |
|
||||
| 7 | 8 | 9 |
|
||||
|
||||
## Special Characters
|
||||
|
||||
Escape sequences for special characters: \\* \\_ \\\\\\\` \\[ ] ( ) # + - .
|
||||
|
||||
## HTML in Markdown
|
||||
|
||||
This is a HTML block inside Markdown.
|
||||
|
||||
## Highlight Text
|
||||
|
||||
Highlight <span style="background-color: rgba(255, 102, 102, 0.5)">text</span> is here
|
||||
|
||||
## Colored Text
|
||||
|
||||
Colored <span style="color: rgb(255, 32, 32)">text</span> is here
|
||||
|
||||
## Task Lists
|
||||
|
||||
* Completed item
|
||||
* Incomplete item
|
||||
|
||||
## Strikethrough
|
||||
|
||||
~~This text is strikethrough.~~
|
||||
|
||||
## Recursive Elements
|
||||
|
||||
### Recursive Links and Emphasis
|
||||
|
||||
**[Bold link](https://example.com)**
|
||||
|
||||
### Recursive Emphasis
|
||||
|
||||
***Bold and nested italic within bold.***
|
||||
|
||||
## Image
|
||||
|
||||
IMAGE\\_PLACEHOLDER
|
||||
|
||||
## Citation
|
||||
|
||||
CITATION\\_PLACEHOLDER
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### Empty Link
|
||||
|
||||
[Link](https://)
|
||||
|
||||
### Zotero Link
|
||||
|
||||
[Zotero Link](zotero://note/u/123456)
|
||||
|
||||
### Lone Asterisk
|
||||
|
||||
* This should not be italic.
|
||||
|
||||
### Broken Lists
|
||||
|
||||
* Item 1
|
||||
|
||||
* Item 2Continuation of item 2 without proper indentation.
|
||||
|
||||
### Long Text Wrapping
|
||||
|
||||
This is a very long paragraph that does not have any line breaks and is intended to test how the Markdown engine handles text wrapping when there are no explicit line breaks within the text.
|
||||
|
||||
***
|
||||
|
||||
## Conclusion
|
||||
|
||||
This document contains a wide range of Markdown elements, including headers, lists, blockquotes, inline and block code, tables, images, links, math, and special characters. It also tests recursive and edge cases to ensure the Markdown engine is robust.
|
||||
`;
|
||||
|
||||
// new ClipboardHelper()
|
||||
// .addText(parseTemplateString(content as string))
|
||||
// .copy();
|
||||
|
||||
assert.equal(content, expected);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { getAddon } from "../utils/global";
|
||||
|
||||
describe("Startup", function () {
|
||||
it("should have plugin instance defined", function () {
|
||||
assert.isNotEmpty(getAddon());
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,389 @@
|
|||
/* eslint-disable no-useless-escape */
|
||||
/* eslint-disable no-irregular-whitespace */
|
||||
import { ClipboardHelper } from "zotero-plugin-toolkit";
|
||||
import { getAddon } from "../utils/global";
|
||||
import { resetAll } from "../utils/status";
|
||||
import { getNoteContent, parseTemplateString } from "../utils/note";
|
||||
|
||||
describe("Template", function () {
|
||||
const addon = getAddon();
|
||||
this.beforeAll(async function () {
|
||||
await resetAll();
|
||||
});
|
||||
|
||||
this.afterEach(async function () {});
|
||||
|
||||
it("hooks.onImportTemplateFromClipboard", async function () {
|
||||
const key = importTemplate();
|
||||
assert.isNotEmpty(key);
|
||||
addon.api.template.removeTemplate(key);
|
||||
});
|
||||
|
||||
it("api.template.getTemplateText", async function () {
|
||||
const key = importTemplate();
|
||||
assert.isNotEmpty(addon.api.template.getTemplateText(key!));
|
||||
addon.api.template.removeTemplate(key);
|
||||
});
|
||||
|
||||
it("api.template.getTemplateText", async function () {
|
||||
const key = importTemplate();
|
||||
assert.isTrue(addon.api.template.getTemplateKeys().includes(key!));
|
||||
addon.api.template.removeTemplate(key);
|
||||
});
|
||||
|
||||
it("api.template.removeTemplate", async function () {
|
||||
const key = importTemplate();
|
||||
assert.isNotEmpty(key);
|
||||
addon.api.template.removeTemplate(key!);
|
||||
assert.isFalse(addon.api.template.getTemplateKeys().includes(key!));
|
||||
});
|
||||
|
||||
it("api.template.renderTemplatePreview", async function () {
|
||||
const key = importTemplate();
|
||||
const preview = await addon.api.template.renderTemplatePreview(key!);
|
||||
const expected = `<div data-schema-version="9"><h1>Markdown Test Document</h1>
|
||||
<h2>Headers</h2>
|
||||
<h1>H1 Header</h1>
|
||||
<h2>H2 Header</h2>
|
||||
<h3>H3 Header</h3>
|
||||
<h4>H4 Header</h4>
|
||||
<h5>H5 Header</h5>
|
||||
<h6>H6 Header</h6>
|
||||
<h2>Emphasis</h2>
|
||||
<p><em>This text is italicized.</em> <em>This text is also italicized.</em></p>
|
||||
<p><strong>This text is bold.</strong> <strong>This text is also bold.</strong></p>
|
||||
<p><strong><em>This text is bold and italicized.</em></strong> <strong><em>This text is also bold and italicized.</em></strong></p>
|
||||
<h2>Links</h2>
|
||||
<p><a href="https://example.com" title="Title" rel="noopener noreferrer nofollow">Link with title</a> <a href="https://example.com" rel="noopener noreferrer nofollow">Link without title</a></p>
|
||||
<h2>Images</h2>
|
||||
<p></p>
|
||||
<h2>Blockquotes</h2>
|
||||
<blockquote>
|
||||
<p>This is a blockquote.</p>
|
||||
<blockquote>
|
||||
<p>Nested blockquote.</p>
|
||||
</blockquote>
|
||||
<p>Back to the outer blockquote.</p>
|
||||
</blockquote>
|
||||
<h2>Lists</h2>
|
||||
<h3>Unordered List</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Item 1</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Subitem 1.1</p>
|
||||
<ul>
|
||||
<li>
|
||||
Subitem 1.1.1
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
Item 2
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Ordered List</h3>
|
||||
<ol>
|
||||
<li>
|
||||
<p>First item</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Subitem 1.1</p>
|
||||
<ol>
|
||||
<li>
|
||||
Subitem 1.1.1
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>
|
||||
Second item
|
||||
</li>
|
||||
</ol>
|
||||
<h2>Code</h2>
|
||||
<h3>Inline Code</h3>
|
||||
<p>Here is some <code>inline code</code>.</p>
|
||||
<h3>Code Block</h3>
|
||||
<pre>def hello_world():
|
||||
print("Hello, world!")
|
||||
</pre>
|
||||
<h2>Horizontal Rules</h2>
|
||||
<hr>
|
||||
<p>This is text between horizontal rules</p>
|
||||
<hr>
|
||||
<h2>Tables</h2>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
<p>Header 1</p>
|
||||
</th>
|
||||
<th>
|
||||
<p>Header 2</p>
|
||||
</th>
|
||||
<th>
|
||||
<p>Header 3</p>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>Row 1</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>Data 1.2</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>Data 1.3</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>Row 2</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>Data 2.2</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>Data 2.3</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>Math</h2>
|
||||
<h3>Inline Math</h3>
|
||||
<p>This is an inline math equation: <span class="math"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E = mc^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8141em;"></span><span class="mord mathnormal">m</span><span class="mord"><span class="mord mathnormal">c</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span>.</p>
|
||||
<h3>Block Math</h3>
|
||||
<p>Below is a block math equation:</p>
|
||||
<pre class="math"><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msubsup><mo>∫</mo><mi>a</mi><mi>b</mi></msubsup><mi>f</mi><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo><mtext> </mtext><mi>d</mi><mi>x</mi><mo>=</mo><mi>F</mi><mo stretchy="false">(</mo><mi>b</mi><mo stretchy="false">)</mo><mo>−</mo><mi>F</mi><mo stretchy="false">(</mo><mi>a</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">\\int_a^b f(x) \\, dx = F(b) - F(a)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:2.511em;vertical-align:-0.9119em;"></span><span class="mop"><span class="mop op-symbol large-op" style="margin-right:0.44445em;position:relative;top:-0.0011em;">∫</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.599em;"><span style="top:-1.7881em;margin-left:-0.4445em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">a</span></span></span><span style="top:-3.8129em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">b</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.9119em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">d</span><span class="mord mathnormal">x</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.13889em;">F</span><span class="mopen">(</span><span class="mord mathnormal">b</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.13889em;">F</span><span class="mopen">(</span><span class="mord mathnormal">a</span><span class="mclose">)</span></span></span></span></span></pre>
|
||||
<h3>Complex Math</h3>
|
||||
<p>Solve the quadratic equation:</p>
|
||||
<pre class="math"><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>x</mi><mo>=</mo><mfrac><mrow><mo>−</mo><mi>b</mi><mo>±</mo><msqrt><mrow><msup><mi>b</mi><mn>2</mn></msup><mo>−</mo><mn>4</mn><mi>a</mi><mi>c</mi></mrow></msqrt></mrow><mrow><mn>2</mn><mi>a</mi></mrow></mfrac></mrow><annotation encoding="application/x-tex">x = \\frac\{-b \\pm \\sqrt\{b^2 - 4ac\}\}\{2a\}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">x</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.2764em;vertical-align:-0.686em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.5904em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord">2</span><span class="mord mathnormal">a</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord">−</span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">±</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord sqrt"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.9134em;"><span class="svg-align" style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord" style="padding-left:0.833em;"><span class="mord"><span class="mord mathnormal">b</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7401em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord">4</span><span class="mord mathnormal">a</span><span class="mord mathnormal">c</span></span></span><span style="top:-2.8734em;"><span class="pstrut" style="height:3em;"></span><span class="hide-tail" style="min-width:0.853em;height:1.08em;"><svg xmlns="http://www.w3.org/2000/svg" width="400em" height="1.08em" viewBox="0 0 400000 1080" preserveAspectRatio="xMinYMin slice"><path d="M95,702
|
||||
c-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14
|
||||
c0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54
|
||||
c44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10
|
||||
s173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429
|
||||
c69,-144,104.5,-217.7,106.5,-221
|
||||
l0 -0
|
||||
c5.3,-9.3,12,-14,20,-14
|
||||
H400000v40H845.2724
|
||||
s-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7
|
||||
c-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z
|
||||
M834 80h400000v40h-400000z"></path></svg></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.1266em;"><span></span></span></span></span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.686em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></pre>
|
||||
<h2>Nested Elements</h2>
|
||||
<h3>Nested Code and Lists</h3>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Ordered list item</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Unordered subitem</p>
|
||||
<pre>console.log("Nested code block");
|
||||
</pre>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>This is a nested math</p>
|
||||
<pre class="math"><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>y</mi><mo>=</mo><msup><mi>x</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">y=x^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8641em;"></span><span class="mord"><span class="mord mathnormal">x</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span></pre>
|
||||
</li>
|
||||
<li>
|
||||
This is a inline math<span class="math"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>123</mn></mrow><annotation encoding="application/x-tex">123</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6444em;"></span><span class="mord">123</span></span></span></span></span>
|
||||
</li>
|
||||
<li>
|
||||
<p>This is a line table</p>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<p>1</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>2</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>3</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>4</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>5</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>6</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>7</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>8</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>9</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</li>
|
||||
</ol>
|
||||
<h2>Special Characters</h2>
|
||||
<p>Escape sequences for special characters: * _ \` [ ] ( ) # + - .</p>
|
||||
<h2>HTML in Markdown</h2>
|
||||
<p>This is a HTML block inside Markdown.</p>
|
||||
<h2>Highlight Text</h2>
|
||||
<p>Highlight <span style="background-color: rgb(255, 102, 102);">text</span> is here</p>
|
||||
<h2>Colored Text</h2>
|
||||
<p>Colored <span style="color: rgb(255, 32, 32)">text</span> is here</p>
|
||||
<h2>Task Lists</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Completed item
|
||||
</li>
|
||||
<li>
|
||||
Incomplete item
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Strikethrough</h2>
|
||||
<p><span style="text-decoration: line-through">This text is strikethrough.</span></p>
|
||||
<h2>Recursive Elements</h2>
|
||||
<h3>Recursive Links and Emphasis</h3>
|
||||
<p><strong><a href="https://example.com" rel="noopener noreferrer nofollow">Bold link</a></strong></p>
|
||||
<h3>Recursive Emphasis</h3>
|
||||
<p><strong><em>Bold and nested italic within bold.</em></strong></p>
|
||||
<h2>Image</h2>
|
||||
<p>IMAGE_PLACEHOLDER</p>
|
||||
<h2>Citation</h2>
|
||||
<p>CITATION_PLACEHOLDER</p>
|
||||
<h2>Edge Cases</h2>
|
||||
<h3>Empty Link</h3>
|
||||
<p><a href="https://" rel="noopener noreferrer nofollow">Link</a></p>
|
||||
<h3>Zotero Link</h3>
|
||||
<p><a href="zotero://note/u/123456" rel="noopener noreferrer nofollow">Zotero Link</a></p>
|
||||
<h3>Lone Asterisk</h3>
|
||||
<ul>
|
||||
<li>
|
||||
This should not be italic.
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Broken Lists</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Item 1</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Item 2</p>
|
||||
<p>Continuation of item 2 without proper indentation.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Long Text Wrapping</h3>
|
||||
<p>This is a very long paragraph that does not have any line breaks and is intended to test how the Markdown engine handles text wrapping when there are no explicit line breaks within the text.</p>
|
||||
<hr>
|
||||
<h2>Conclusion</h2>
|
||||
<p>This document contains a wide range of Markdown elements, including headers, lists, blockquotes, inline and block code, tables, images, links, math, and special characters. It also tests recursive and edge cases to ensure the Markdown engine is robust.</p>
|
||||
</div>`;
|
||||
// debug(preview);
|
||||
// If the template changes, update the expected value by copying the expected value
|
||||
// new ClipboardHelper().addText(parseTemplateString(preview)).copy();
|
||||
assert.equal(preview, expected);
|
||||
addon.api.template.removeTemplate(key);
|
||||
});
|
||||
|
||||
it("api.template.runTextTemplate", async function () {
|
||||
addon.api.template.setTemplate({
|
||||
name: "[text]Test",
|
||||
text: "<h1>Test</h1>\n<p>${targetNoteItem.id}</p>",
|
||||
});
|
||||
const note = new Zotero.Item("note");
|
||||
await note.saveTx();
|
||||
const html = await addon.api.template.runTextTemplate("[text]Test", {
|
||||
targetNoteId: note.id,
|
||||
});
|
||||
assert.equal(html, `<h1>Test</h1>\n<p>${note.id}</p>`);
|
||||
await Zotero.Items.erase(note.id);
|
||||
addon.api.template.removeTemplate("[text]Test");
|
||||
});
|
||||
|
||||
it("api.template.runItemTemplate", async function () {
|
||||
// Also test the use of Markdown pragma
|
||||
addon.api.template.setTemplate({
|
||||
name: "[item]Test",
|
||||
text: `
|
||||
// @beforeloop-begin
|
||||
// @use-markdown
|
||||
# Hi! This only renders once
|
||||
// @beforeloop-end
|
||||
// @default-begin
|
||||
<p>Title: <span style="color: #ffcb00">]\${topItem.getField("title")}</span></p>
|
||||
\${{
|
||||
const note = Zotero.Items.get(targetNoteItem.id);
|
||||
return "<p>" + note.id + "</p>";
|
||||
}}$
|
||||
// @default-end
|
||||
// @afterloop-begin
|
||||
> Done! But Markdown is not rendered correctly. Try to add
|
||||
\`// @use-markdown\` pragma before this line.
|
||||
// @afterloop-end
|
||||
`,
|
||||
});
|
||||
const items = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const item = new Zotero.Item("book");
|
||||
item.setField("title", `Title ${i}`);
|
||||
await item.saveTx();
|
||||
items.push(item);
|
||||
}
|
||||
const note = new Zotero.Item("note");
|
||||
await note.saveTx();
|
||||
const html = await addon.api.template.runItemTemplate("[item]Test", {
|
||||
itemIds: items.map((item) => item.id),
|
||||
targetNoteId: note.id,
|
||||
});
|
||||
// new ClipboardHelper().addText(parseTemplateString(html)).copy();
|
||||
const expected = `<h1>Hi! This only renders once</h1>
|
||||
<p>Title: <span style="color: #ffcb00">]Title 0</span></p>
|
||||
<p>6</p>
|
||||
<p>Title: <span style="color: #ffcb00">]Title 1</span></p>
|
||||
<p>6</p>
|
||||
<p>Title: <span style="color: #ffcb00">]Title 2</span></p>
|
||||
<p>6</p>
|
||||
> Done! But Markdown is not rendered correctly. Try to add
|
||||
`;
|
||||
assert.equal(html, expected);
|
||||
for (const item of items) {
|
||||
await Zotero.Items.erase(item.id);
|
||||
}
|
||||
await Zotero.Items.erase(note.id);
|
||||
addon.api.template.removeTemplate("[item]Test");
|
||||
});
|
||||
});
|
||||
|
||||
function importTemplate() {
|
||||
const shareCode = `
|
||||
# This template is specifically for importing/sharing, using better
|
||||
# notes 'import from clipboard': copy the content and
|
||||
# goto Zotero menu bar, click Tools->New Template from Clipboard.
|
||||
# Do not copy-paste this to better notes template editor directly.
|
||||
name: "[text]TestGen"
|
||||
zoteroVersion: "7.0.12-beta.1+31bbf2acf"
|
||||
pluginVersion: "2.2.3-beta.2"
|
||||
savedAt: "2025-01-06T09:12:14.939Z"
|
||||
content: |-
|
||||
${getNoteContent()
|
||||
.split("\n")
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n")}
|
||||
`;
|
||||
return getAddon().hooks.onImportTemplateFromClipboard(shareCode, {
|
||||
quiet: true,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import { BasicTool } from "zotero-plugin-toolkit";
|
||||
import { waitForNoteWindow, waitForTabSelectEvent } from "../utils/wait";
|
||||
import { resetAll } from "../utils/status";
|
||||
|
||||
describe("Workspace", function () {
|
||||
const tool = new BasicTool();
|
||||
|
||||
this.beforeAll(async function () {
|
||||
await resetAll();
|
||||
});
|
||||
|
||||
this.afterEach(async function () {
|
||||
await resetAll();
|
||||
});
|
||||
|
||||
it("should open note in tab", async function () {
|
||||
const note = new Zotero.Item("note");
|
||||
await note.saveTx();
|
||||
|
||||
const promise = waitForTabSelectEvent();
|
||||
|
||||
// An example of how to debug the test
|
||||
debug("Calling viewItems");
|
||||
|
||||
tool.getGlobal("ZoteroPane").viewItems([note]);
|
||||
await promise;
|
||||
|
||||
const selectedID = tool.getGlobal("Zotero_Tabs").selectedID;
|
||||
const selectedTab = tool.getGlobal("Zotero_Tabs")._getTab(selectedID);
|
||||
|
||||
expect(selectedTab.tab.data.itemID).to.be.equal(note.id);
|
||||
});
|
||||
|
||||
it("should open note in window if shift key is pressed", async function () {
|
||||
const note = new Zotero.Item("note");
|
||||
await note.saveTx();
|
||||
|
||||
const promise = waitForNoteWindow();
|
||||
|
||||
tool.getGlobal("ZoteroPane").viewItems([note], { shiftKey: true });
|
||||
const win = await promise;
|
||||
|
||||
expect(win).to.be.not.null;
|
||||
|
||||
const editor = win!.document.querySelector(
|
||||
"#zotero-note-editor",
|
||||
) as EditorElement;
|
||||
|
||||
expect(editor).to.be.not.null;
|
||||
|
||||
expect(editor.item?.id).to.be.equal(note.id);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["./**/*.ts", "typings", "../node_modules/zotero-types"],
|
||||
"exclude": []
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
declare interface EditorCore {
|
||||
debouncedUpdate: Function;
|
||||
disableDrag: boolean;
|
||||
docChanged: boolean;
|
||||
isAttachmentNote: false;
|
||||
metadata: {
|
||||
_citationItems: { itemData: { [k: string]: any } }[];
|
||||
uris: string[];
|
||||
};
|
||||
nodeViews: any[];
|
||||
onUpdateState: Function;
|
||||
options: {
|
||||
isAttachmentNote: false;
|
||||
onImportImages: Function;
|
||||
onInsertObject: Function;
|
||||
onOpenAnnotation: Function;
|
||||
onOpenCitationPage: Function;
|
||||
onOpenCitationPopup: Function;
|
||||
onOpenContextMenu: Function;
|
||||
onOpenURL: Function;
|
||||
onShowCitationItem: Function;
|
||||
onSubscribe: Function;
|
||||
onUnsubscribe: Function;
|
||||
onUpdate: Function;
|
||||
onUpdateCitationItemsList: Function;
|
||||
placeholder: boolean;
|
||||
readOnly: boolean;
|
||||
reloaded: boolean;
|
||||
smartQuotes: boolean;
|
||||
unsaved: boolean;
|
||||
value: string;
|
||||
};
|
||||
pluginState: { [k: string]: any };
|
||||
provider: import("react").Provider;
|
||||
readOnly: boolean;
|
||||
reloaded: boolean;
|
||||
view: import("prosemirror-view").EditorView & {
|
||||
docView: NodeViewDesc;
|
||||
};
|
||||
}
|
||||
|
||||
declare type EditorAPI =
|
||||
typeof import("../src/extras/editorScript").BetterNotesEditorAPI;
|
||||
|
||||
declare interface EditorElement extends XULBoxElement {
|
||||
_iframe: HTMLIFrameElement;
|
||||
_editorInstance: Zotero.EditorInstance;
|
||||
_initialized?: boolean;
|
||||
mode?: "edit" | "view";
|
||||
viewMode?: string;
|
||||
parent?: Zotero.Item;
|
||||
item?: Zotero.Item;
|
||||
getCurrentInstance(): Zotero.EditorInstance;
|
||||
initEditor(): Promise<void>;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import type * as chai from "chai";
|
||||
|
||||
declare global {
|
||||
const expect: typeof chai.expect;
|
||||
const assert: typeof chai.assert;
|
||||
const debug: (...data: any[]) => void;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { config } from "../../package.json";
|
||||
|
||||
export { getAddon };
|
||||
|
||||
function getAddon(): import("../../src/addon").default {
|
||||
// @ts-ignore - plugin instance
|
||||
return Zotero[config.addonRef];
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
export async function getTempDirectory() {
|
||||
let path = "";
|
||||
let attempts = 3;
|
||||
const zoteroTmpDirPath = Zotero.getTempDirectory().path;
|
||||
while (attempts--) {
|
||||
path = PathUtils.join(zoteroTmpDirPath, Zotero.Utilities.randomString());
|
||||
try {
|
||||
await IOUtils.makeDirectory(path, { ignoreExisting: false });
|
||||
break;
|
||||
} catch (e) {
|
||||
if (!attempts) throw e; // Throw on last attempt
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
export function getNoteContent() {
|
||||
return `<div data-schema-version="9"><h1>Markdown Test Document</h1>
|
||||
<h2>Headers</h2>
|
||||
<h1>H1 Header</h1>
|
||||
<h2>H2 Header</h2>
|
||||
<h3>H3 Header</h3>
|
||||
<h4>H4 Header</h4>
|
||||
<h5>H5 Header</h5>
|
||||
<h6>H6 Header</h6>
|
||||
<h2>Emphasis</h2>
|
||||
<p><em>This text is italicized.</em> <em>This text is also italicized.</em></p>
|
||||
<p><strong>This text is bold.</strong> <strong>This text is also bold.</strong></p>
|
||||
<p><strong><em>This text is bold and italicized.</em></strong> <strong><em>This text is also bold and italicized.</em></strong></p>
|
||||
<h2>Links</h2>
|
||||
<p><a href="https://example.com" title="Title" rel="noopener noreferrer nofollow">Link with title</a> <a href="https://example.com" rel="noopener noreferrer nofollow">Link without title</a></p>
|
||||
<h2>Images</h2>
|
||||
<p></p>
|
||||
<h2>Blockquotes</h2>
|
||||
<blockquote>
|
||||
<p>This is a blockquote.</p>
|
||||
<blockquote>
|
||||
<p>Nested blockquote.</p>
|
||||
</blockquote>
|
||||
<p>Back to the outer blockquote.</p>
|
||||
</blockquote>
|
||||
<h2>Lists</h2>
|
||||
<h3>Unordered List</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Item 1</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Subitem 1.1</p>
|
||||
<ul>
|
||||
<li>
|
||||
Subitem 1.1.1
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
Item 2
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Ordered List</h3>
|
||||
<ol>
|
||||
<li>
|
||||
<p>First item</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Subitem 1.1</p>
|
||||
<ol>
|
||||
<li>
|
||||
Subitem 1.1.1
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>
|
||||
Second item
|
||||
</li>
|
||||
</ol>
|
||||
<h2>Code</h2>
|
||||
<h3>Inline Code</h3>
|
||||
<p>Here is some <code>inline code</code>.</p>
|
||||
<h3>Code Block</h3>
|
||||
<pre>def hello_world():
|
||||
print("Hello, world!")
|
||||
</pre>
|
||||
<h2>Horizontal Rules</h2>
|
||||
<hr>
|
||||
<p>This is text between horizontal rules</p>
|
||||
<hr>
|
||||
<h2>Tables</h2>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
<p>Header 1</p>
|
||||
</th>
|
||||
<th>
|
||||
<p>Header 2</p>
|
||||
</th>
|
||||
<th>
|
||||
<p>Header 3</p>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>Row 1</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>Data 1.2</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>Data 1.3</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>Row 2</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>Data 2.2</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>Data 2.3</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>Math</h2>
|
||||
<h3>Inline Math</h3>
|
||||
<p>This is an inline math equation: <span class="math">$E = mc^2$</span>.</p>
|
||||
<h3>Block Math</h3>
|
||||
<p>Below is a block math equation:</p>
|
||||
<pre class="math">$$\\\\int_a^b f(x) \\\\, dx = F(b) - F(a)$$</pre>
|
||||
<h3>Complex Math</h3>
|
||||
<p>Solve the quadratic equation:</p>
|
||||
<pre class="math">$$x = \\\\frac{-b \\\\pm \\\\sqrt{b^2 - 4ac}}{2a}$$</pre>
|
||||
<h2>Nested Elements</h2>
|
||||
<h3>Nested Code and Lists</h3>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Ordered list item</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Unordered subitem</p>
|
||||
<pre>console.log("Nested code block");
|
||||
</pre>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>This is a nested math</p>
|
||||
<pre class="math">$$y=x^2$$</pre>
|
||||
</li>
|
||||
<li>
|
||||
This is a inline math<span class="math">$123$</span>
|
||||
</li>
|
||||
<li>
|
||||
<p>This is a line table</p>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<p>1</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>2</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>3</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>4</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>5</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>6</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>7</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>8</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>9</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</li>
|
||||
</ol>
|
||||
<h2>Special Characters</h2>
|
||||
<p>Escape sequences for special characters: * _ \\\` [ ] ( ) # + - .</p>
|
||||
<h2>HTML in Markdown</h2>
|
||||
<p>This is a HTML block inside Markdown.</p>
|
||||
<h2>Highlight Text</h2>
|
||||
<p>Highlight <span style="background-color: rgba(255, 102, 102, 0.5)">text</span> is here</p>
|
||||
<h2>Colored Text</h2>
|
||||
<p>Colored <span style="color: rgb(255, 32, 32)">text</span> is here</p>
|
||||
<h2>Task Lists</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Completed item
|
||||
</li>
|
||||
<li>
|
||||
Incomplete item
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Strikethrough</h2>
|
||||
<p><span style="text-decoration: line-through">This text is strikethrough.</span></p>
|
||||
<h2>Recursive Elements</h2>
|
||||
<h3>Recursive Links and Emphasis</h3>
|
||||
<p><strong><a href="https://example.com" rel="noopener noreferrer nofollow">Bold link</a></strong></p>
|
||||
<h3>Recursive Emphasis</h3>
|
||||
<p><strong><em>Bold and nested italic within bold.</em></strong></p>
|
||||
<h2>Image</h2>
|
||||
<p>IMAGE_PLACEHOLDER</p>
|
||||
<h2>Citation</h2>
|
||||
<p>CITATION_PLACEHOLDER</p>
|
||||
<h2>Edge Cases</h2>
|
||||
<h3>Empty Link</h3>
|
||||
<p><a href="https://" rel="noopener noreferrer nofollow">Link</a></p>
|
||||
<h3>Zotero Link</h3>
|
||||
<p><a href="zotero://note/u/123456" rel="noopener noreferrer nofollow">Zotero Link</a></p>
|
||||
<h3>Lone Asterisk</h3>
|
||||
<ul>
|
||||
<li>
|
||||
This should not be italic.
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Broken Lists</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Item 1</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Item 2</p>
|
||||
<p>Continuation of item 2 without proper indentation.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Long Text Wrapping</h3>
|
||||
<p>This is a very long paragraph that does not have any line breaks and is intended to test how the Markdown engine handles text wrapping when there are no explicit line breaks within the text.</p>
|
||||
<hr>
|
||||
<h2>Conclusion</h2>
|
||||
<p>This document contains a wide range of Markdown elements, including headers, lists, blockquotes, inline and block code, tables, images, links, math, and special characters. It also tests recursive and edge cases to ensure the Markdown engine is robust.</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export function parseTemplateString(input: string): string {
|
||||
return input
|
||||
.replace(/\\/g, "\\\\") // Escape backslashes
|
||||
.replace(/`/g, "\\`") // Escape backticks
|
||||
.replace(/\$/g, "\\$") // Escape dollar signs
|
||||
.replace(/{/g, "\\{") // Escape opening braces
|
||||
.replace(/}/g, "\\}"); // Escape closing braces
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
export async function resetData() {
|
||||
// Delete collections, items, tags
|
||||
const collections = await Zotero.Collections.getAllIDs(
|
||||
Zotero.Libraries.userLibraryID,
|
||||
);
|
||||
await Zotero.Collections.erase(collections);
|
||||
|
||||
const items = await Zotero.Items.getAllIDs(Zotero.Libraries.userLibraryID);
|
||||
await Zotero.Items.erase(items);
|
||||
}
|
||||
|
||||
export async function resetTabs() {
|
||||
const win = Zotero.getMainWindow();
|
||||
const Zotero_Tabs = win.Zotero_Tabs;
|
||||
Zotero_Tabs.closeAll();
|
||||
}
|
||||
|
||||
export async function resetAll() {
|
||||
await resetTabs();
|
||||
await resetData();
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
export async function waitNoMoreThan<T>(
|
||||
promise: Promise<T>,
|
||||
timeout: number = 3000,
|
||||
message: string = "Timeout",
|
||||
) {
|
||||
let resolved = false;
|
||||
|
||||
return Promise.any([
|
||||
promise.then((result) => {
|
||||
resolved = true;
|
||||
return result;
|
||||
}),
|
||||
Zotero.Promise.delay(timeout).then(() => {
|
||||
if (resolved) return;
|
||||
throw new Error(message);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function waitForNotifierEvent(
|
||||
event: _ZoteroTypes.Notifier.Event,
|
||||
type: _ZoteroTypes.Notifier.Type,
|
||||
timeout: number = 3000,
|
||||
) {
|
||||
if (!event) throw new Error("event not provided");
|
||||
let resolved = false;
|
||||
|
||||
return waitNoMoreThan(
|
||||
new Promise((resolve, reject) => {
|
||||
const notifierID = Zotero.Notifier.registerObserver(
|
||||
{
|
||||
notify: function (ev, type, ids, extraData) {
|
||||
if (ev == event) {
|
||||
Zotero.Notifier.unregisterObserver(notifierID);
|
||||
resolved = true;
|
||||
|
||||
resolve({
|
||||
ids: ids,
|
||||
extraData: extraData,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
[type],
|
||||
"test",
|
||||
101,
|
||||
);
|
||||
}),
|
||||
timeout,
|
||||
);
|
||||
}
|
||||
|
||||
export function waitForTabSelectEvent(timeout: number = 3000) {
|
||||
return waitForNotifierEvent("select", "tab", timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a window with a specific URL to open. Returns a promise for the window, and
|
||||
* optionally passes the window to a callback immediately for use with modal dialogs,
|
||||
* which prevent async code from continuing
|
||||
*/
|
||||
export async function waitForWindow(uri: string, timeout: number = 3000) {
|
||||
return waitNoMoreThan(
|
||||
new Promise<Window>((resolve, reject) => {
|
||||
const loadObserver = function (ev: Event) {
|
||||
ev.originalTarget?.removeEventListener("load", loadObserver, false);
|
||||
const href = (ev.target as Window)?.location.href;
|
||||
Zotero.debug("Window opened: " + href);
|
||||
|
||||
if (href != uri) {
|
||||
Zotero.debug(`Ignoring window ${href} in waitForWindow()`);
|
||||
return;
|
||||
}
|
||||
|
||||
Services.ww.unregisterNotification(winObserver);
|
||||
const win = ev.target?.ownerGlobal;
|
||||
// Give window code time to run on load
|
||||
win?.setTimeout(function () {
|
||||
resolve(win);
|
||||
});
|
||||
};
|
||||
const winObserver = {
|
||||
observe: function (subject: Window, topic: string, data: any) {
|
||||
if (topic != "domwindowopened") return;
|
||||
subject.addEventListener("load", loadObserver, false);
|
||||
},
|
||||
} as nsIObserver;
|
||||
Services.ww.registerNotification(winObserver);
|
||||
}),
|
||||
timeout,
|
||||
);
|
||||
}
|
||||
|
||||
export async function waitForNoteWindow() {
|
||||
return await waitForWindow("chrome://zotero/content/note.xhtml");
|
||||
}
|
||||
|
|
@ -10,5 +10,5 @@
|
|||
"strict": true
|
||||
},
|
||||
"include": ["src", "typings", "node_modules/zotero-types"],
|
||||
"exclude": ["build", "addon"]
|
||||
"exclude": ["build", "addon", "test"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
// Generated by zotero-plugin-scaffold
|
||||
/* prettier-ignore */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
// prettier-ignore
|
||||
declare namespace _ZoteroTypes {
|
||||
interface Prefs {
|
||||
PluginPrefsMap: {
|
||||
"syncNoteIds": string;
|
||||
"syncPeriodSeconds": number;
|
||||
"syncAttachmentFolder": string;
|
||||
"autoAnnotation": boolean;
|
||||
"insertLinkPosition": string;
|
||||
"workspace.outline.expandLevel": number;
|
||||
"workspace.outline.keepLinks": boolean;
|
||||
"editor.noteLinkPreviewType": string;
|
||||
"editor.useMagicKey": boolean;
|
||||
"editor.useMarkdownPaste": boolean;
|
||||
"openNote.takeover": boolean;
|
||||
"openNote.defaultAsWindow": boolean;
|
||||
"exportNotes.takeover": boolean;
|
||||
"annotationNote.enableTagSync": boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"addons": {
|
||||
"Knowledge4Zotero@windingwind.com": {
|
||||
"updates": [
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"update_link": "https://github.com/windingwind/zotero-better-notes/releases/download/v2.0.0/better-notes-for-zotero.xpi",
|
||||
"update_hash": "sha512:eb8bcbcb70848ba0a51eb20a6ee0f597428a4df76e3a3db85f4eaae43f6ebd500e32fdf6d274a090e25dd52951d5e0f3989521f90c253de7b257c6974dfa5644",
|
||||
"applications": {
|
||||
"zotero": {
|
||||
"strict_min_version": "7.0.0-beta.70",
|
||||
"strict_max_version": "7.0.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
19
update.json
19
update.json
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"addons": {
|
||||
"Knowledge4Zotero@windingwind.com": {
|
||||
"updates": [
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"update_link": "https://github.com/windingwind/zotero-better-notes/releases/download/v2.0.0/better-notes-for-zotero.xpi",
|
||||
"update_hash": "sha512:eb8bcbcb70848ba0a51eb20a6ee0f597428a4df76e3a3db85f4eaae43f6ebd500e32fdf6d274a090e25dd52951d5e0f3989521f90c253de7b257c6974dfa5644",
|
||||
"applications": {
|
||||
"zotero": {
|
||||
"strict_min_version": "7.0.0-beta.70",
|
||||
"strict_max_version": "7.0.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
19
update.rdf
19
update.rdf
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"addons": {
|
||||
"Knowledge4Zotero@windingwind.com": {
|
||||
"updates": [
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"update_link": "https://github.com/windingwind/zotero-better-notes/releases/download/v2.0.0/better-notes-for-zotero.xpi",
|
||||
"update_hash": "sha512:eb8bcbcb70848ba0a51eb20a6ee0f597428a4df76e3a3db85f4eaae43f6ebd500e32fdf6d274a090e25dd52951d5e0f3989521f90c253de7b257c6974dfa5644",
|
||||
"applications": {
|
||||
"zotero": {
|
||||
"strict_min_version": "7.0.0-beta.70",
|
||||
"strict_max_version": "7.0.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,10 @@ import pkg from "./package.json";
|
|||
import { defineConfig } from "zotero-plugin-scaffold";
|
||||
import { replaceInFile } from "replace-in-file";
|
||||
|
||||
const TEST_PREFS = {};
|
||||
// Disable user guide, keep in sync with src/modules/userGuide.ts
|
||||
TEST_PREFS[`${pkg.config.prefsPrefix}.latestTourVersion`] = 1;
|
||||
|
||||
export default defineConfig({
|
||||
source: ["src", "addon"],
|
||||
dist: "build",
|
||||
|
|
@ -48,8 +52,11 @@ export default defineConfig({
|
|||
target: ["firefox115"],
|
||||
},
|
||||
],
|
||||
prefs: {
|
||||
prefix: pkg.config.prefsPrefix,
|
||||
},
|
||||
hooks: {
|
||||
"build:replace": (ctx) => {
|
||||
"build:bundle": (ctx) => {
|
||||
return replaceInFile({
|
||||
files: ["README.md"],
|
||||
from: /^ {2}- \[Latest Version.*/gm,
|
||||
|
|
@ -64,6 +71,14 @@ export default defineConfig({
|
|||
all: true,
|
||||
},
|
||||
},
|
||||
test: {
|
||||
entries: ["test/"],
|
||||
prefs: TEST_PREFS,
|
||||
abortOnFail: true,
|
||||
exitOnFinish: false,
|
||||
hooks: {},
|
||||
waitForPlugin: `() => Zotero.${pkg.config.addonRef}.data.initialized`,
|
||||
},
|
||||
|
||||
// If you need to see a more detailed build log, uncomment the following line:
|
||||
// logLevel: "trace",
|
||||
|
|
|
|||
Loading…
Reference in New Issue