Compare commits

..

18 Commits

Author SHA1 Message Date
windingwind 4f410a5974 feat: add tests 2024-12-09 17:14:40 +01:00
windingwind 6fd2bab661 update: ci 2024-12-09 15:45:23 +01:00
windingwind f1afa15ab6 update: ci 2024-12-09 15:43:36 +01:00
windingwind 6061c00c4e try to fix fetch error 2024-12-09 15:03:08 +01:00
windingwind 4bdb52d86c update: ci 2024-12-09 14:53:33 +01:00
windingwind e94a163ae5 update: ci 2024-12-09 12:30:37 +01:00
windingwind e507bfbe1f update: deps 2024-12-09 11:57:38 +01:00
windingwind 353e0866b8 fix: build 2024-12-08 22:47:13 +01:00
windingwind 085d393ddb add: TS tests 2024-12-08 22:29:39 +01:00
windingwind 32993ba7cc fix: ci 2024-12-08 16:44:45 +01:00
windingwind 8b4b55a44a fix: ci 2024-12-08 15:37:27 +01:00
windingwind 06680647bf fix: run test command 2024-12-08 15:05:09 +01:00
windingwind 984b6a13aa fix: dep 2024-12-08 14:49:05 +01:00
windingwind a2a6ff9d4b fix: ci 2024-12-08 14:44:28 +01:00
windingwind 7a47fa7b58 fix: ci 2024-12-08 14:41:58 +01:00
windingwind ead049c331 fix: ci 2024-12-08 14:24:30 +01:00
windingwind f962547d54 feat: add ci 2024-12-08 14:19:30 +01:00
windingwind 6a8c7da9c6 feat: add tests 2024-12-08 10:44:02 +01:00
57 changed files with 5140 additions and 3294 deletions

View File

@ -43,6 +43,7 @@ body:
- Item
- Text
- QuickInsert
- QuickBackLink
- QuickImport
- QuickNote
- ExportMDFileName

3
.gitignore vendored
View File

@ -5,5 +5,4 @@ pnpm-lock.yaml
yarn.lock
zotero-cmd.json
.DS_Store
.env
.scaffold
.env

View File

@ -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}",
"});",
],
},
"});"
]
}
}

View File

@ -90,7 +90,7 @@ and:
- Download the plugin (.xpi file) from below.
- [Latest Version: 2.2.5](https://github.com/windingwind/zotero-better-notes/releases/download/v2.2.5/better-notes-for-zotero.xpi)
- [Latest Version: 2.1.7](https://github.com/windingwind/zotero-better-notes/releases/download/v2.1.7/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)
@ -240,17 +240,6 @@ 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
@ -267,31 +256,14 @@ 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): Translate PDF, EPub, webpage, metadata, annotations, notes to the target language.
- [Translate for Zotero](https://github.com/windingwind/zotero-pdf-translate): PDF translation for Zotero
- [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

View File

@ -2,6 +2,7 @@
* 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"
);

View File

@ -21,6 +21,9 @@
</style>
<script>
var browser;
var { Services } = ChromeUtils.import(
"resource://gre/modules/Services.jsm",
);
var { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm",
);

View File

@ -44,6 +44,10 @@
</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,

View File

@ -45,6 +45,10 @@
</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,

View File

@ -14,7 +14,7 @@
"id": "__addonID__",
"update_url": "__updateURL__",
"strict_min_version": "7.0.0-beta.70",
"strict_max_version": "7.1.*"
"strict_max_version": "7.0.*"
}
}
}

View File

@ -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 Tools->New Template from Clipboard.
# goto Zotero menu bar, click Edit->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 `Tools`->`New Template from Clipboard`.
2. Goto Zotero menubar, click `Edit`->`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,9 +88,7 @@ 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.
> 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).
> The template with this pragma should not contain any separator (`---` or `<hr>`) in the content.
### `// @author`
@ -150,7 +148,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 Tools->New Template from Clipboard.
# goto Zotero menu bar, click Edit->New Template from Clipboard.
# Do not copy-paste this to better notes template editor directly.
name: "[Item] Example Item Template"
content: |-
@ -216,6 +214,7 @@ 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 |
@ -409,7 +408,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 Tools->New Template from Clipboard.
# goto Zotero menu bar, click Edit->New Template from Clipboard.
# Do not copy-paste this to better notes template editor directly.
name: "[TYPE] TEMPLATE NAME"
content: |-

6600
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "zotero-better-notes",
"version": "2.2.5",
"version": "2.1.7",
"description": "Everything about note management. All in Zotero.",
"config": {
"addonName": "Better Notes for Zotero",
@ -18,7 +18,6 @@
"release": "zotero-plugin release",
"lint": "prettier --write . && eslint . --ext .ts --fix",
"test": "zotero-plugin test --abort-on-fail --exit-on-finish",
"test-dev": "zotero-plugin test --abort-on-fail",
"update-deps": "npm update --save"
},
"repository": {
@ -32,16 +31,16 @@
},
"homepage": "https://github.com/windingwind/zotero-better-notes#readme",
"dependencies": {
"asciidoctor": "^3.0.4",
"dexie": "^4.0.11",
"diff": "^5.2.0",
"hast-util-to-html": "^9.0.4",
"asciidoctor": "^3.0.2",
"dexie": "^4.0.4",
"diff": "^5.1.0",
"hast-util-to-html": "^9.0.0",
"hast-util-to-mdast": "^8.4.1",
"hast-util-to-text": "^4.0.2",
"hast-util-to-text": "^4.0.0",
"hastscript": "^8.0.0",
"html-docx-js": "^0.3.1",
"html-docx-js-typescript": "^0.1.5",
"katex": "^0.16.21",
"katex": "^0.16.9",
"path-browserify": "^1.0.1",
"rehype-format": "^4.0.1",
"rehype-parse": "^8.0.5",
@ -58,38 +57,38 @@
"unist-util-visit": "^5.0.0",
"unist-util-visit-parents": "^6.0.1",
"yamljs": "^0.3.0",
"zotero-plugin-toolkit": "^4.1.1"
"zotero-plugin-scaffold": "^0.1.8-beta.3",
"zotero-plugin-toolkit": "^4.0.11"
},
"devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@prettier/plugin-xml": "^3.4.1",
"@prettier/plugin-xml": "^3.2.2",
"@types/browser-or-node": "^1.3.2",
"@types/chai": "^5.0.1",
"@types/diff": "^5.2.3",
"@types/diff": "^5.0.9",
"@types/html-docx-js": "^0.3.4",
"@types/katex": "^0.16.7",
"@types/mocha": "^10.0.10",
"@types/node": "^20.17.14",
"@types/path-browserify": "^1.0.3",
"@types/node": "^20.10.4",
"@types/path-browserify": "^1.0.2",
"@types/seedrandom": "^3.0.8",
"@types/yamljs": "^0.2.34",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"chokidar-cli": "^3.0.0",
"concurrently": "^8.2.2",
"cross-env": "^7.0.3",
"eslint": "^8.57.1",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.4.2",
"prosemirror-model": "^1.24.1",
"prettier": "^3.1.1",
"prosemirror-model": "^1.19.4",
"prosemirror-state": "^1.4.3",
"prosemirror-transform": "^1.10.2",
"prosemirror-view": "^1.37.1",
"prosemirror-transform": "^1.8.0",
"prosemirror-view": "^1.32.6",
"replace-in-file": "^7.2.0",
"typescript": "^5.7.3",
"xslt3": "^2.7.0",
"zotero-plugin-scaffold": "^0.2.0-beta.20",
"zotero-types": "^3.1.6"
"typescript": "^5.3.3",
"xslt3": "^2.6.0",
"zotero-types": "^3.0.2"
},
"eslintConfig": {
"env": {

View File

@ -24,7 +24,7 @@ class Addon {
window: Window;
};
export: {
pdf: { promise?: _ZoteroTypes.Promise.PromiseObject };
pdf: { promise?: _ZoteroTypes.PromiseObject };
};
sync: {
data?: LargePrefHelper;
@ -66,7 +66,7 @@ class Addon {
templates: string[];
};
picker: {
mode: "insert" | "create" | "export" | "pick";
mode: "insert" | "create" | "export";
data: Record<string, any>;
};
};

View File

@ -34,7 +34,6 @@ import {
runTemplate,
runTextTemplate,
runItemTemplate,
runQuickInsertTemplate,
} from "./modules/template/api";
import {
getTemplateKeys,
@ -125,7 +124,6 @@ const template = {
runTemplate,
runTextTemplate,
runItemTemplate,
runQuickInsertTemplate,
getTemplateKeys,
getTemplateText,
setTemplate,

View File

@ -5,7 +5,6 @@ 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) {

View File

@ -66,10 +66,11 @@ 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.sourceNoteIDs = [this.currentNote!.id];
io.content = content;
io.lineIndex = this.getIndexToInsert();
}
@ -215,11 +216,21 @@ export class InboundCreator extends PluginCEBase {
async getContentToInsert() {
if (!this.currentNote || !this.targetNote) return "";
return await this._addon.api.template.runQuickInsertTemplate(
this.currentNote,
this.targetNote,
{ dryRun: true },
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 content;
}
getIndexToInsert() {

View File

@ -36,10 +36,6 @@ export class NotePicker extends PluginCEBase {
_cachedLibraryIDs: number[] = [];
_cachedSelectedNoteIDs: number[] = [];
_disableSelectionChange = false;
get content() {
return MozXULElement.parseXULToFragment(`
<linkset>
@ -125,8 +121,8 @@ export class NotePicker extends PluginCEBase {
}
destroy(): void {
this.collectionsView?.unregister();
this.itemsView?.unregister();
this.collectionsView.unregister();
if (this.itemsView) this.itemsView.unregister();
unregisterPrefObserver(this._prefObserverID);
}
@ -399,9 +395,6 @@ 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
@ -409,48 +402,19 @@ 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(",")
@ -474,23 +438,13 @@ 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,
selectedNotes: this.getSelectedNotes(selection),
},
}),
);
return true;
}
getSelectedNotes(selection?: { selected: Set<number> }): Zotero.Item[] {

View File

@ -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,10 +240,19 @@ export class OutboundCreator extends PluginCEBase {
if (!this.currentNote || !this.targetNotes?.length) return "";
let content = "";
for (const note of this.targetNotes) {
content += await this._addon.api.template.runQuickInsertTemplate(
note,
this.currentNote,
{ dryRun: true },
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 += "\n";
}

View File

@ -40,10 +40,6 @@ 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": "一级标题",

View File

@ -144,7 +144,7 @@ class PluginState {
height: auto;
}
.link-preview .primary-editor li {
white-space: normal;
white-space: nowrap;
}
</style>`),
]);

View File

@ -11,7 +11,6 @@ declare const _currentEditorInstance: {
interface MagicKeyOptions {
insertTemplate?: () => void;
refreshTemplates?: () => void;
insertLink?: (type: "inbound" | "outbound") => void;
copyLink?: (mode: "section" | "line") => void;
openAttachment?: () => void;
@ -21,7 +20,6 @@ interface MagicKeyOptions {
interface MagicCommand {
messageId?: string;
searchParts?: string[];
title?: string;
icon?: string;
command: (state: EditorState) => void | Transaction;
@ -36,35 +34,30 @@ 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?.();
},
@ -74,28 +67,18 @@ 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)",
@ -135,70 +118,60 @@ 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();
},
@ -381,24 +354,9 @@ 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!;
@ -406,6 +364,9 @@ 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();

View File

@ -7,7 +7,7 @@ let io: {
};
accepted: boolean;
useBuiltInExport: boolean;
deferred: _ZoteroTypes.Promise.DeferredPromise<void>;
deferred: _ZoteroTypes.DeferredPromise<void>;
embedLink: boolean;
standaloneLink: boolean;
exportNote: boolean;

View File

@ -10,7 +10,7 @@ let io: {
currentNoteID: number;
currentLineIndex?: number;
openedNoteIDs?: number[];
deferred: _ZoteroTypes.Promise.DeferredPromise<void>;
deferred: _ZoteroTypes.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(

View File

@ -11,8 +11,7 @@ let args = window.arguments[0] as any;
if (!args._initPromise) {
args = args.wrappedJSObject;
}
const templateData = (args.templates as string[]) || [];
templateData.sort();
const templateData = args.templates;
const multiSelect = args.multiSelect;
let tableHelper: VirtualizedTableHelper;
@ -28,7 +27,6 @@ function accept() {
);
}
// @ts-ignore - plugin instance
const getString = (Zotero[config.addonRef] as typeof addon).api.utils.getString;
function initTable() {

View File

@ -48,7 +48,6 @@ 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([
@ -80,7 +79,7 @@ async function onStartup() {
setSyncing();
await Promise.all(Zotero.getMainWindows().map(onMainWindowLoad));
await onMainWindowLoad(Zotero.getMainWindow());
// For testing
addon.data.initialized = true;
@ -106,10 +105,7 @@ async function onMainWindowLoad(win: _ZoteroTypes.MainWindow): Promise<void> {
patchExportItems(win);
// TEMP: This doesn't work, maybe better to wait for the support from Zotero
// patchOpenTabMenu(win);
await restoreNoteTabs();
restoreNoteTabs();
showUserGuide(win);
}
@ -124,7 +120,6 @@ function onShutdown(): void {
ztoolkit.unregisterAll();
// Remove addon object
addon.data.alive = false;
// @ts-ignore plugin instance
delete Zotero[config.addonInstance];
}
@ -141,13 +136,6 @@ 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);
}
@ -312,5 +300,6 @@ export default {
onCreateNoteFromTemplate,
onCreateNoteFromMD,
onCreateNote,
restoreNoteTabs,
onShowUserGuide,
};

View File

@ -4,7 +4,6 @@ import { config } from "../package.json";
const basicTool = new BasicTool();
// @ts-ignore - plugin instance
if (!basicTool.getGlobal("Zotero")[config.addonInstance]) {
// Set global variables
defineGlobal("window");
@ -17,7 +16,6 @@ if (!basicTool.getGlobal("Zotero")[config.addonInstance]) {
return _globalThis.addon.data.ztoolkit;
},
});
// @ts-ignore - plugin instance
Zotero[config.addonInstance] = addon;
}

View File

@ -12,101 +12,48 @@ function registerReaderAnnotationButton() {
(event) => {
const { doc, append, params, reader } = event;
const annotationData = params.annotation;
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();
},
append(
ztoolkit.UI.createElement(doc, "div", {
tag: "div",
classList: ["icon"],
properties: {
innerHTML: ICONS.readerQuickNote,
},
],
enableElementRecord: false,
});
updateAnnotationNoteButton(
button,
reader._item.libraryID,
annotationData.id,
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,
}),
);
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,

View File

@ -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" as any,
funcSign: "exportItems",
patcher: (origin) =>
function () {
if (!getPref("exportNotes.takeover")) {

View File

@ -1,170 +0,0 @@
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);
}

View File

@ -3,12 +3,7 @@ import { itemPicker } from "../../utils/itemPicker";
import { getString } from "../../utils/locale";
import { fill, slice } from "../../utils/str";
export {
runTemplate,
runTextTemplate,
runItemTemplate,
runQuickInsertTemplate,
};
export { runTemplate, runTextTemplate, runItemTemplate };
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
@ -242,26 +237,6 @@ 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) {

View File

@ -62,12 +62,7 @@ function removeTemplate(keyName: string | undefined): void {
addon.data.template.data?.deleteKey(keyName);
}
function importTemplateFromClipboard(
text?: string,
options: {
quiet?: boolean;
} = {},
) {
function importTemplateFromClipboard(text?: string) {
if (!text) {
text = Zotero.Utilities.Internal.getClipboard("text/plain") || "";
}
@ -88,10 +83,7 @@ function importTemplateFromClipboard(
showHint("The copied template is invalid");
return;
}
if (
!options.quiet &&
!window.confirm(`Import template "${template.name}"?`)
) {
if (!window.confirm(`Import template "${template.name}"?`)) {
return;
}
setTemplate({ name: template.name, text: template.content });
@ -99,5 +91,4 @@ function importTemplateFromClipboard(
if (addon.data.template.editor.window) {
addon.data.template.editor.window.refresh();
}
return template.name;
}

View File

@ -199,9 +199,8 @@ export async function showTemplateEditor() {
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"),

View File

@ -20,19 +20,14 @@ async function showTemplatePicker(
mode: "export",
data?: Record<string, never>,
): Promise<void>;
async function showTemplatePicker(mode: "pick"): Promise<string[]>;
async function showTemplatePicker(): Promise<any>;
async function showTemplatePicker(): Promise<void>;
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;
}
@ -73,12 +68,11 @@ async function insertTemplateCallback(name: string) {
targetNoteId: targetNoteItem.id,
});
}
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);
await addLineToNote(
targetNoteItem,
html,
addon.data.template.picker.data.lineIndex,
);
}
async function createTemplateNoteCallback(name: string) {

View File

@ -93,6 +93,25 @@ 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());

View File

@ -98,7 +98,6 @@ 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[
@ -109,12 +108,7 @@ async function renderSection(
linkData[
{ inbound: "fromKey", outbound: "toKey" }[type] as "fromKey" | "toKey"
],
)) as Zotero.Item | false;
if (!targetItem) {
continue;
}
count++;
)) as Zotero.Item;
const linkParams = {
workspaceUID: (body.closest("bn-workspace") as Workspace)?.dataset.uid,
@ -166,6 +160,7 @@ async function renderSection(
body.append(row);
}
const count = inLinks.length;
setCount(count);
}

View File

@ -30,7 +30,6 @@ 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);
},
},

View File

@ -60,17 +60,16 @@ export function onTabSelect(tabType: string) {
ZoteroContextPane.update();
}
export async function restoreNoteTabs() {
export 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(await Zotero.Items.getAsync(tab.data.itemID), {
openWorkspaceTab(Zotero.Items.get(tab.data.itemID), {
select: !!tab.selected,
});
}
Zotero.Session.debounceSave();
}
export function onUpdateNoteTabsTitle(noteItems: Zotero.Item[]) {

View File

@ -161,11 +161,11 @@ function serializeAnnotations(
}
let template: string = "";
if (["highlight", "underline"].includes(annotation.type)) {
if (annotation.type === "highlight") {
template = Zotero.Prefs.get(
"annotations.noteTemplates.highlight",
) as string;
} else if (["note", "text"].includes(annotation.type)) {
} else if (annotation.type === "note") {
template = Zotero.Prefs.get("annotations.noteTemplates.note") as string;
} else if (annotation.type === "image") {
template = "<p>{{image}}<br/>{{citation}} {{comment}}</p>";

View File

@ -262,23 +262,18 @@ function annotations2html(
async function note2html(
noteItems: Zotero.Item | Zotero.Item[],
options: {
targetNoteItem?: Zotero.Item;
html?: string;
dryRun?: boolean;
} = {},
options: { targetNoteItem?: Zotero.Item; html?: string } = {},
) {
if (!Array.isArray(noteItems)) {
noteItems = [noteItems];
}
const { targetNoteItem, dryRun } = options;
const { targetNoteItem } = options;
let html = options.html;
if (!html) {
html = noteItems.map((item) => item.getNote()).join("\n");
}
if (!dryRun && targetNoteItem?.isNote()) {
const str = await copyEmbeddedImagesInHTML(html, targetNoteItem, noteItems);
return str;
if (targetNoteItem?.isNote()) {
return await copyEmbeddedImagesInHTML(html, targetNoteItem, noteItems);
}
return await renderNoteHTML(html, noteItems);
}
@ -428,12 +423,11 @@ async function rehype2remark(rehype: HRoot) {
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();
const cached = children[children.length - 1];
if (current?.type && !paragraphNodes.includes(current?.type)) {
if (current?.type !== "list") {
if (cached?.type === "paragraph") {
cached.children.push(current);
} else {

View File

@ -247,13 +247,6 @@ 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))
@ -528,9 +521,6 @@ function initEditorPlugins(editor: Zotero.EditorInstance) {
lineIndex: getLineAtCursor(editor),
});
},
refreshTemplates: () => {
addon.hooks.onRefreshTemplatesInNote(editor);
},
insertLink: (mode: "inbound" | "outbound") => {
openLinkCreator(editor._item, {
lineIndex: getLineAtCursor(editor),

View File

@ -8,11 +8,7 @@ export function getNoteLinkParams(link: string) {
if (id === "u") {
libraryID = Zotero.Libraries.userLibraryID;
} else {
const libID = Zotero.Groups.getLibraryIDFromGroupID(Number(id));
if (!libID) {
throw new Error("Invalid group ID");
}
libraryID = libID;
libraryID = Zotero.Groups.getLibraryIDFromGroupID(id);
}
const line = url.searchParams.get("line");
return {

View File

@ -41,20 +41,10 @@ async function openLinkCreator(
await io.deferred.promise;
const targetNote = Zotero.Items.get(io.targetNoteID);
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 content = io.content;
const lineIndex = io.lineIndex;
if (!targetNote || !content) return;
await addLineToNote(targetNote, content, lineIndex);
}

View File

@ -77,8 +77,7 @@ async function addLineToNote(
return;
}
const noteLines = await getLinesInNote(note);
// 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) {
if (lineIndex < 0 || lineIndex >= noteLines.length) {
lineIndex = noteLines.length;
}
ztoolkit.log(`insert to ${lineIndex}, it used to be ${noteLines[lineIndex]}`);
@ -87,12 +86,7 @@ async function addLineToNote(
const editor = getEditorInstance(note.id);
if (editor && !forceMetadata) {
// The note is opened. Add line via note editor
// 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",
);
const pos = getPositionAtLine(editor, lineIndex, "start");
ztoolkit.log("Add note line via note editor", pos);
insert(editor, html, pos);
// The selection is automatically moved to the next line
@ -328,10 +322,6 @@ 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);
}
});
@ -371,27 +361,17 @@ async function copyEmbeddedImagesInHTML(
doc.querySelectorAll(`img[data-attachment-key="${attachment.key}"]`),
) as HTMLImageElement[];
if (nodes.length) {
let copiedAttachment: Zotero.Item | undefined;
let copiedAttachment: Zotero.Item;
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,
});
});
if (!copiedAttachment) {
continue;
}
nodes.forEach((node) =>
node?.setAttribute("data-attachment-key", copiedAttachment!.key),
nodes.forEach(
(node) =>
node?.setAttribute("data-attachment-key", copiedAttachment.key),
);
}
}

View File

@ -1,38 +1,18 @@
import { config } from "../../package.json";
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 getPref(key: string) {
return Zotero.Prefs.get(`${config.prefsPrefix}.${key}`, true);
}
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) {
export function setPref(key: string, value: string | number | boolean) {
return Zotero.Prefs.set(`${config.prefsPrefix}.${key}`, value, true);
}
function clearPref<K extends keyof _PluginPrefsMap>(key: K): void;
function clearPref(key: string): void;
function clearPref(key: string) {
export function clearPref(key: string) {
return Zotero.Prefs.clear(`${config.prefsPrefix}.${key}`, true);
}
function getPrefJSON(key: string) {
export function getPrefJSON(key: string) {
try {
return JSON.parse(String(getPref(key) || "{}"));
} catch (e) {
@ -41,15 +21,10 @@ function getPrefJSON(key: string) {
return {};
}
function registerPrefObserver<K extends keyof _PluginPrefsMap>(
key: K,
callback: (value: _PluginPrefsMap[K]) => void,
): symbol;
function registerPrefObserver(
export 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,
@ -57,6 +32,6 @@ function registerPrefObserver(key: string, callback: (value: any) => void) {
);
}
function unregisterPrefObserver(observerID: symbol) {
export function unregisterPrefObserver(observerID: symbol) {
return Zotero.Prefs.unregisterObserver(observerID);
}

View File

@ -1,255 +0,0 @@
/* 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);
});
});

View File

@ -1,7 +1,7 @@
import { getAddon } from "../utils/global";
import { config } from "../../package.json";
describe("Startup", function () {
it("should have plugin instance defined", function () {
assert.isNotEmpty(getAddon());
assert.isNotEmpty(Zotero[config.addonRef]);
});
});

View File

@ -1,389 +0,0 @@
/* 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():
&nbsp; &nbsp;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,
});
}

View File

@ -1,8 +0,0 @@
import { config } from "../../package.json";
export { getAddon };
function getAddon(): import("../../src/addon").default {
// @ts-ignore - plugin instance
return Zotero[config.addonRef];
}

View File

@ -1,16 +0,0 @@
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;
}

View File

@ -1,250 +0,0 @@
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():
&nbsp; &nbsp;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
}

View File

@ -7,8 +7,8 @@
"resolveJsonModule": true,
"skipLibCheck": true,
"alwaysStrict": false,
"strict": true
"strict": true,
},
"include": ["src", "typings", "node_modules/zotero-types"],
"exclude": ["build", "addon", "test"]
"exclude": ["build", "addon", "test"],
}

26
typings/prefs.d.ts vendored
View File

@ -1,26 +0,0 @@
// 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;
};
}
}

19
update-beta.json Normal file
View File

@ -0,0 +1,19 @@
{
"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 Normal file
View File

@ -0,0 +1,19 @@
{
"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 Normal file
View File

@ -0,0 +1,19 @@
{
"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.*"
}
}
}
]
}
}
}

View File

@ -52,11 +52,8 @@ export default defineConfig({
target: ["firefox115"],
},
],
prefs: {
prefix: pkg.config.prefsPrefix,
},
hooks: {
"build:bundle": (ctx) => {
"build:replace": (ctx) => {
return replaceInFile({
files: ["README.md"],
from: /^ {2}- \[Latest Version.*/gm,