├── .gitattributes
├── .github
├── dependabot.yml
└── renovate.json
├── .gitignore
├── .idea
├── .gitignore
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── compiler.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── jsLibraryMappings.xml
├── jsLinters
│ └── eslint.xml
├── misc.xml
├── modules.xml
├── prettier.xml
├── vcs.xml
└── zotero-markdb-connect.iml
├── .prettierignore
├── .zenodo.json
├── LICENSE
├── README.md
├── addon
├── bootstrap.js
├── content
│ ├── icons
│ │ ├── favicon.png
│ │ └── favicon@0.5x.png
│ ├── preferences.css
│ └── preferences.xhtml
├── locale
│ └── en-US
│ │ ├── addon.ftl
│ │ └── preferences.ftl
├── manifest.json
└── prefs.js
├── docs
└── assets
│ └── readme
│ ├── ExternalLinkNotificationScreenshot.png
│ └── MarkDBConnectScreenshot.png
├── eslint.config.mjs
├── package-lock.json
├── package.json
├── prettier.config.mjs
├── src
├── addon.ts
├── dataGlobals.ts
├── hooks.ts
├── index.ts
├── mdbcTypes.d.ts
├── modules
│ ├── create-element.ts
│ ├── mdbcConstants.ts
│ ├── mdbcLogger.ts
│ ├── mdbcParam.ts
│ ├── mdbcScan.ts
│ ├── mdbcStartupHelpers.ts
│ ├── mdbcUX.ts
│ ├── monkey-patch.ts
│ └── preferenceScript.ts
└── utils
│ ├── locale.ts
│ ├── prefs.ts
│ ├── window.ts
│ └── ztoolkit.ts
├── tsconfig.base.json
├── tsconfig.dev.json
├── tsconfig.json
├── tsconfig.prod.json
├── tsconfig.repo.json
├── typings
├── global.d.ts
├── mdbc.d.ts
├── prefs.d.ts
└── zotero.d.ts
├── update-beta.json
├── update.json
├── update.rdf
└── zotero-plugin.config.ts
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 | groups:
13 | all-non-major:
14 | update-types:
15 | - "minor"
16 | - "patch"
17 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended",
5 | ":semanticPrefixChore",
6 | ":prHourlyLimitNone",
7 | ":prConcurrentLimitNone",
8 | ":enableVulnerabilityAlerts",
9 | ":dependencyDashboard",
10 | "group:allNonMajor",
11 | "schedule:weekly"
12 | ],
13 | "labels": ["dependencies"],
14 | "packageRules": [
15 | {
16 | "matchPackageNames": [
17 | "zotero-plugin-toolkit",
18 | "zotero-types",
19 | "zotero-plugin-scaffold"
20 | ],
21 | "schedule": ["at any time"],
22 | "automerge": true
23 | }
24 | ],
25 | "git-submodules": {
26 | "enabled": true
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Local config files ###
2 |
3 |
4 | # dot files
5 | .DS_Store
6 |
7 | ### Runtime ###
8 | node_modules/
9 |
10 | dist/
11 |
12 | yarn.lock
13 | .DS_Store
14 |
15 | ### Editor directories and files ###
16 | .vscode/
17 | # .idea/
18 |
19 | ### Generated files ###
20 | eslint.config.js
21 |
22 | # Node.js
23 | node_modules
24 | pnpm-lock.yaml
25 | yarn.lock
26 |
27 | # TSC
28 | tsconfig.tsbuildinfo
29 |
30 | # Scaffold
31 | .env
32 | .scaffold/
33 | build/
34 | logs/
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # GitHub Copilot persisted chat sessions
7 | /copilot/chatSessions
8 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/jsLibraryMappings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jsLinters/eslint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/prettier.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/zotero-markdb-connect.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | **/.git/
2 | **/.github/
3 | **/.vscode/
4 | **/.idea/
5 | **/build/
6 | **/dist/
7 | **/package-lock.json
8 | **/*.lock
9 | logs
10 | **/node_modules
11 | package-lock.json
12 | yarn.lock
13 | pnpm-lock.yaml
14 | **/*_lintignore*
15 | **/*-lintignore*
16 |
--------------------------------------------------------------------------------
/.zenodo.json:
--------------------------------------------------------------------------------
1 | {
2 | "creators": [
3 | {
4 | "affiliation": "Massachusetts Institute of Technology",
5 | "name": "Houlihan, Dae",
6 | "orcid": "0000-0001-5003-9278"
7 | }
8 | ],
9 | "keywords": ["Zotero", "plugin", "Markdown", "MarkDB-Connect", "Obsidian", "logseq"],
10 | "license": "MIT License",
11 | "upload_type": "software"
12 | }
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Sean Dae Houlihan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/daeh/zotero-markdb-connect/releases/latest) [](https://github.com/daeh/zotero-markdb-connect/releases/latest) [](https://github.com/daeh/zotero-markdb-connect/releases/latest)
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | # MarkDB-Connect (Zotero Markdown DataBase Connect)
12 |
13 | [](https://www.zotero.org) [](https://github.com/windingwind/zotero-plugin-template)
14 |
15 | - **_Scans your Markdown database and adds a colored tag to associated Zotero items._**
16 | - **_Jump to Markdown notes from the contextual menu of Zotero items._**
17 | - **_Supports various Markdown databases, including [Obsidian](https://obsidian.md), [logseq](https://logseq.com), and [Zettlr](https://www.zettlr.com)_**
18 | - **_[Zotero 7](https://www.zotero.org/support/beta_builds) compatible_**
19 |
20 | 
21 |
22 | This is a plugin for [Zotero](https://www.zotero.org), a research source management tool. The _MarkDB-Connect_ plugin searches a user-defined folder for markdown files that include a [Better BibTeX](https://retorque.re/zotero-better-bibtex/) citekey or Zotero-Item-Key, and adds a colored tag to the corresponding Zotero items.
23 |
24 | This plugin was initially designed with the [Obsidian](https://obsidian.md) markdown editor in mind, and was inspired by the [obsidian-citation-plugin](https://github.com/hans/obsidian-citation-plugin) workflow. It offers preliminary support for [logseq](https://logseq.com) and [Zettlr](https://www.zettlr.com). It can be adapted to other databases that store markdown files outside of Zotero, and to other workflows that generate markdown reading notes linked to Zotero items (such as Zotero's `Export Note` feature).
25 |
26 | Please post any bugs, questions, or feature requests in the GitHub repository's [issues](https://github.com/daeh/zotero-markdb-connect/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc).
27 |
28 | ## Plugin Functions
29 |
30 | Adds a colored tag to Zotero items for which there are associated reading notes in an external folder.
31 |
32 | Supports multiple markdown files for a single Zotero item.
33 |
34 | Opens an existing markdown note in [Obsidian](https://obsidian.md), [logseq](https://logseq.com), or the system's default markdown note editor (e.g. [Zettlr](https://www.zettlr.com), [Typora](https://typora.io)) from the contextual menu of a Zotero item.
35 |
36 | ## Installation
37 |
38 | - Download the plugin (the `.xpi` file) from the [latest release](https://github.com/daeh/zotero-markdb-connect/releases/latest)
39 | - Open Zotero (version 7.x)
40 | - From `Tools -> Plugins`
41 | - Select `Install Add-on From File...` from the gear icon ⛭
42 | - Choose the `.xpi` file you downloaded (e.g. `markdb-connect.xpi`)
43 | - Restart Zotero
44 |
45 | > [!NOTE]
46 | > Beginning with `v0.1.0`, _MarkDB-Connect_ will support Zotero 7 exclusively. The last release for Zotero 6 is [`v0.0.27`](https://github.com/daeh/zotero-markdb-connect/releases/tag/v0.0.27).
47 |
48 | ## Setup
49 |
50 | A markdown file can specify which Zotero item it's linked to using either a [Better BibTeX](https://retorque.re/zotero-better-bibtex/) citekey or a Zotero-Item-Key. I recommend using Better BibTeX citekeys when possible.
51 |
52 | 1. Using **Better BibTeX citekeys** to link markdown files to Zotero items.
53 |
54 | - This is recommended if you created the markdown notes with [obsidian-citation-plugin](https://github.com/hans/obsidian-citation-plugin), [BibNotes Formatter](https://github.com/stefanopagliari/bibnotes), or [Obsidian Zotero Integration](https://github.com/mgmeyers/obsidian-zotero-integration).
55 |
56 | - The BetterBibTeX citekey can be taken from the filename, YAML metadata, or body of the markdown note.
57 |
58 | - FYI There's a nice [configuration tutorial](https://publish.obsidian.md/history-notes/Option+-+Link+from+a+Zotero+item+back+to+related+notes+in+Obsidian) detailing a common use case (thanks to Prof. Elena Razlogova).
59 |
60 | 2. Using **Zotero Item Keys** to link markdown files to Zotero items.
61 |
62 | - This is recommended if you created the markdown notes with the `Export Note` feature of Zotero.
63 |
64 | - The markdown note contents should include the Zotero-Item-Key in a consistent format.
65 |
66 | NOTE: multiple markdown files can point to the same Zotero item. But a given markdown file should only be linked to a single Zotero item. A markdown reading note can reference multiple Zotero items throughout the file, but _MarkDB-Connect_ will only link the markdown note to one BetterBibTeX-citekey / Zotero-Item-Key.
67 |
68 | ---
69 |
70 | ### Option 1: Using BetterBibTeX citekeys
71 |
72 | _MarkDB-Connect_ can extract the BetterBibTeX citekey that specifies which Zotero Item a markdown note corresponds to. The BetterBibTeX citekey can be taken from a markdown file's filename, [YAML metadata](https://help.obsidian.md/Advanced+topics/YAML+front+matter), or elsewhere in the file's contents.
73 |
74 |
75 |
76 | configuration details
77 |
78 | - In Zotero's Settings, click the `MarkDB-Connect` preference pane.
79 |
80 | - Specify the location of the folder that contains your markdown reading notes (e.g. `/Users/me/Documents/ObsVault/ReadingNotes/`). The _MarkDB-Connect_ plugin will recursively search this path for markdown files.
81 |
82 | - By default, _MarkDB-Connect_ expects that the filenames of your markdown reading note files begin with `@mycitekey` but can include extra information after it (e.g. a reading note with the BetterBibTeX citekey `shepard1987science` could have the filename `@shepard1987science.md` or `@shepard1987science Toward a universal law of generalization for psychological science.md`).
83 |
84 | - If your BetterBibTeX citekeys contain certain special characters (e.g. `:`, `/`), you will need to extract the citekeys from the markdown file's contents rather than its filename.
85 |
86 | - If the default does not match your use case, you can specify how to extract BetterBibTeX citekeys.
87 |
88 | - **A. filename** - Select `Custom File Filter` and define a RegExp with a single capturing group.
89 |
90 | - E.g. the default is `^@(\S+).*\.md$`, which looks for files beginning with `@` and uses the first word as the BetterBibTeX citekey.
91 |
92 | - **B. metadata** - Select `BetterBibTeX citekey - taken from YAML metadata` and specify a keyword from the notes' YAML frontmatter (here's an [example](#example-markdown-note)).
93 |
94 | - For info on metadata syntax, see [YAML front matter](https://help.obsidian.md/Advanced+topics/YAML+front+matter).
95 |
96 | - **C. contents** - Select `BetterBibTeX citekey - captured with custom RegExp` and define a RegExp with a single capturing group to return exactly 1 match per file.
97 |
98 | - Run the synchronization function from `Tools -> MarkDB-Connect Sync Tags`.
99 |
100 | - This will add a tag (`ObsCite`) to every Zotero item for which there exists a reading note in the external folder you specified.
101 |
102 | - In the `Tags` plane of Zotero, right-click on the `ObsCite` tag and assign it a color, which will mark the tagged items in the preview plane of Zotero. (In the screenshot above, Zotero items associated with reading notes are marked with a 🟦 blue tag.)
103 |
104 |
105 |
106 | ---
107 |
108 | ### Option 2: Using Zotero Item Keys
109 |
110 | _MarkDB-Connect_ can extract the Zotero-Item-Key that specifies which Zotero Item a markdown note corresponds to. The Zotero-Item-Key is taken from the markdown file contents using a custom RegExp pattern.
111 |
112 | Zotero automatically generates Item Keys, they take the form of `ABCD1234`, as in `zotero://select/library/items/ABCD1234`. NB this is not the same as the BetterBibTeX citekey you assigned an item (e.g. `mycitekey` in `zotero://select/items/@mycitekey`).
113 |
114 |
115 |
116 | configuration details
117 |
118 | - In Zotero's Settings, click the `MarkDB-Connect` preference pane.
119 |
120 | - Specify the location of the folder that contains your markdown reading notes (e.g. `/Users/me/Documents/ObsVault/ReadingNotes/`). The _MarkDB-Connect_ plugin will recursively search this path for markdown files.
121 |
122 | - The default behavior is to search for markdown files beginning with `@`.
123 |
124 | - Alternatively, you can define a custom RegExp pattern to match your reading note files.
125 |
126 | - Select the `Match Markdown Files to Zotero Items Using:` `Zotero-Item-Key - captured with custom RegExp` option.
127 |
128 | - Specify a RegExp pattern to extract the Zotero-Item-Key from the markdown contents.
129 |
130 | E.g. if your note has the line
131 |
132 | `- local:: [local zotero](zotero://select/library/items/GZ9DQ2AM)`
133 |
134 | you could extract the Zotero key (`GZ9DQ2AM`) using this RegExp pattern:
135 |
136 | `^- local::.+\/items\/(\w+)\)`
137 |
138 | - Run the synchronization function from `Tools -> MarkDB-Connect Sync Tags`.
139 |
140 | - This will add a tag (`ObsCite`) to every Zotero item for which there exists a reading note in the external folder you specified.
141 |
142 | - In the `Tags` plane of Zotero, right-click on the `ObsCite` tag and assign it a color, which will mark the tagged items in the preview plane of Zotero. (In the screenshot above, Zotero items associated with reading notes are marked with a 🟦 blue tag.)
143 |
144 |
145 |
146 | ---
147 |
148 | ## Example Markdown Note
149 |
150 | In this example markdown note (`@saxe2017emobtom.md`), _MarkDB-Connect_ will use the [YAML metadata](https://help.obsidian.md/Advanced+topics/YAML+front+matter) keyword `citekey` to find the BetterBibTeX citekey (`saxe2017emobtom`) that determines which Zotero item to associate with the markdown file. Notice that the markdown file can include other BetterBibTeX citekeys and Zotero-Item-Keys, which are ignored by the plugin.
151 |
152 | ```markdown
153 | ---
154 | citekey: saxe2017emobtom
155 | zoterouri: zotero://select/library/items/IACZMXU4
156 | bbturi: zotero://select/items/@saxe2017emobtom
157 | doi: 10.1016/j.copsyc.2017.04.019
158 | ---
159 |
160 | # @saxe2017emobtom
161 |
162 | **Formalizing emotion concepts within a Bayesian model of theory of mind**
163 | (2017) _Current Opinion in Psychology_
164 | [Open in Zotero](zotero://select/library/items/IACZMXU4)
165 |
166 | The body of notes can include references to other Zotero items.
167 | The _MarkDB-Connect_ plugin will only link this file to one Zotero item
168 | (in this case, it will use the value of the `citekey` property).
169 |
170 | Here are links to other papers:
171 |
172 | - This one uses [a Zotero URI](zotero://select/library/items/4RJ97IFL)
173 |
174 | - This one uses [a BetterBibTeX URI](zotero://select/items/@anzellotti2021opaque)
175 |
176 | - This one uses an Obsidian wiki link: [[@cusimano2018cogsci]]
177 | ```
178 |
179 |
180 |
181 | Example Templates
182 |
183 | Below are example templates for various Obsidian plugins
184 |
185 | #### Template for [obsidian-citation-plugin](https://github.com/hans/obsidian-citation-plugin)
186 |
187 | ```md
188 | ---
189 | citekey: "{{citekey}}"
190 | title: "{{title}}"
191 | year: {{year}}
192 | authors: [{{authorString}}]
193 | {{#if containerTitle~}} publication: "{{containerTitle}}" {{~else~}} {{~/if}}
194 | {{#if DOI~}} doi: "{{DOI}}" {{~else~}} {{~/if}}
195 | aliases: ["@{{citekey}}", "@{{citekey}} {{title}}"]
196 | tags:
197 | - readingNote
198 | ---
199 |
200 | # @{{citekey}}
201 |
202 | **{{title}}**
203 | {{authorString}}
204 | {{#if year~}} ({{year}}) {{~else~}} {{~/if}} {{~#if containerTitle}} _{{containerTitle~}}_ {{~else~}} {{~/if}}
205 | [Open in Zotero]({{zoteroSelectURI}})
206 | ```
207 |
208 | #### Template for ZotLit
209 |
210 | Make a file (e.g. `zotlit-properties.eta.md`) with the following contents, and point to that file in ZotLit settings: `Template` > `Note Properties`.
211 |
212 | ```eta
213 | citekey: "<%= it.citekey %>"
214 | title: "<%= it.title %>"
215 | <% if (it.date) { %>year: <%= it.date %><% } %>
216 | authors: [<%= it.authors.map(v => v.firstName v.lastName) %>]
217 | <% if (it.publicationTitle) { %>publication: "<%= it.publicationTitle %>"<% } %>
218 | <% if (it.DOI) { %>doi: "<%= it.DOI %>"<% } %>
219 | aliases: ["@<%= it.citekey %>", "@<%= it.citekey %> <%= it.title %>"]
220 | tags:
221 | - readingNote
222 | ```
223 |
224 |
225 |
226 | ## Suppressing the Zotero security notification
227 |
228 | Recent builds of Zotero have introduced a security notification for external links. At present, Zotero does not remember the user's link preferences, so this alert is shown every time an application-specific URI is launched. You can suppress this warning by setting `security.external_protocol_requires_permission` to `false` in Zotero's advanced configuration.
229 |
230 | 
231 |
232 |
233 |
234 | Instructions for modifying Zotero's advanced config
235 |
236 | 1. Open Zotero Settings
237 | 2. Click the "Advanced" tab
238 | 3. Click the "Config Editor" button
239 | 4. Click the "Accept Risk and Continue" button
240 | 5. Search for `security.external_protocol_requires_permission`
241 | 6. Double click the `security.external_protocol_requires_permission` item to toggle its value to `false`
242 |
243 |
244 |
245 | ## Related Projects
246 |
247 | - [obsidian-citation-plugin](https://github.com/hans/obsidian-citation-plugin) by hans
248 | - Obsidian plugin that integrates your Zotero database with Obsidian.
249 | - [BibNotes Formatter](https://github.com/stefanopagliari/bibnotes) by stefanopagliari
250 | - Obsidian plugin to facilitate exporting annotations from Zotero into Obsidian.
251 | - [Obsidian Zotero Integration](https://github.com/mgmeyers/obsidian-zotero-integration) by mgmeyers
252 | - Obsidian plugin to facilitate exporting annotations from Zotero into Obsidian.
253 | - [Zotero 6 'Export Notes' feature](https://forums.zotero.org/discussion/93521/available-for-beta-testing-markdown-export-of-notes/p1) by Zotero
254 | - Zotero 6 beta feature to export notes and annotations from Zotero items as markdown files.
255 | - [Zotero to Markdown](https://github.com/e-alizadeh/Zotero2md) by e-alizadeh
256 | - Python library to export annotations and notes from Zotero items as markdown files.
257 | - [Zotero Better Notes](https://github.com/windingwind/zotero-better-notes) by windingwind
258 | - A Zotero plugin for note management.
259 | - [Logseq Citations Plugin](https://github.com/sawhney17/logseq-citation-manager) by sawhney17
260 | - Logseq plugin that integrates your Zotero database with Logseq.
261 |
262 |
264 |
265 | ## Notes
266 |
267 | [GitHub](https://github.com/daeh/zotero-markdb-connect): Source code repository
268 |
269 | This extension uses the [zotero-plugin-template](https://github.com/windingwind/zotero-plugin-template).
270 |
271 | ## License
272 |
273 | Distributed under the MIT License.
274 |
275 | ## Author
276 |
277 | [](https://daeh.info) [](https://bsky.app/profile/dae.bsky.social)
278 |
--------------------------------------------------------------------------------
/addon/bootstrap.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 |
3 | /**
4 | * Most of this code is from Zotero team's official Make It Red example[1]
5 | * or the Zotero 7 documentation[2].
6 | * [1] https://github.com/zotero/make-it-red
7 | * [2] https://www.zotero.org/support/dev/zotero_7_for_developers
8 | */
9 |
10 | var chromeHandle
11 |
12 | function install(data, reason) {}
13 |
14 | async function startup({ id, version, resourceURI, rootURI }, reason) {
15 | await Zotero.initializationPromise
16 |
17 | // String 'rootURI' introduced in Zotero 7
18 | if (!rootURI) {
19 | rootURI = resourceURI.spec
20 | }
21 |
22 | var aomStartup = Components.classes['@mozilla.org/addons/addon-manager-startup;1'].getService(
23 | Components.interfaces.amIAddonManagerStartup,
24 | )
25 | var manifestURI = Services.io.newURI(rootURI + 'manifest.json')
26 | chromeHandle = aomStartup.registerChrome(manifestURI, [['content', '__addonRef__', rootURI + 'content/']])
27 |
28 | /**
29 | * Global variables for plugin code.
30 | * The `_globalThis` is the global root variable of the plugin sandbox environment
31 | * and all child variables assigned to it is globally accessible.
32 | * See `src/index.ts` for details.
33 | */
34 | const ctx = {
35 | rootURI,
36 | }
37 | ctx._globalThis = ctx
38 |
39 | Services.scriptloader.loadSubScript(`${rootURI}/content/scripts/__addonRef__.js`, ctx)
40 | Zotero.__addonInstance__.hooks.onStartup()
41 | }
42 |
43 | async function onMainWindowLoad({ window }, reason) {
44 | Zotero.__addonInstance__?.hooks.onMainWindowLoad(window)
45 | }
46 |
47 | async function onMainWindowUnload({ window }, reason) {
48 | Zotero.__addonInstance__?.hooks.onMainWindowUnload(window)
49 | }
50 |
51 | function shutdown({ id, version, resourceURI, rootURI }, reason) {
52 | if (reason === APP_SHUTDOWN) {
53 | return
54 | }
55 |
56 | if (typeof Zotero === 'undefined') {
57 | Zotero = Components.classes['@zotero.org/Zotero;1'].getService(Components.interfaces.nsISupports).wrappedJSObject
58 | }
59 | Zotero.__addonInstance__?.hooks.onShutdown()
60 |
61 | Cc['@mozilla.org/intl/stringbundle;1'].getService(Components.interfaces.nsIStringBundleService).flushBundles()
62 |
63 | Cu.unload(`${rootURI}/content/scripts/__addonRef__.js`)
64 |
65 | if (chromeHandle) {
66 | chromeHandle.destruct()
67 | chromeHandle = null
68 | }
69 | }
70 |
71 | function uninstall(data, reason) {}
72 |
--------------------------------------------------------------------------------
/addon/content/icons/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daeh/zotero-markdb-connect/1db3ee2faad330a85f8eb1ec6c359333a4d68a30/addon/content/icons/favicon.png
--------------------------------------------------------------------------------
/addon/content/icons/favicon@0.5x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daeh/zotero-markdb-connect/1db3ee2faad330a85f8eb1ec6c359333a4d68a30/addon/content/icons/favicon@0.5x.png
--------------------------------------------------------------------------------
/addon/content/preferences.css:
--------------------------------------------------------------------------------
1 | groupbox.mdbc-pref-section-box {
2 | border: 2px solid black;
3 | border-radius: 10px;
4 | padding-bottom: 10px;
5 | width: 98%;
6 | min-width: 300px;
7 | /*max-width: 100%;*/
8 | }
9 |
10 | hbox.mdbc-pref-hbox-indent {
11 | display: inline-flex;
12 | flex-wrap: nowrap; /* Prevents flex items from wrapping */
13 | overflow: visible; /* Makes overflow content visible */
14 | align-items: center; /* Vertically centers the items in the line */
15 | justify-content: space-between; /* Puts space before the first and after the last item, but not between items */
16 | /*display: -moz-inline-box;*/
17 | /*display: -webkit-inline-box;*/
18 | padding-left: 10px;
19 | width: 100%;
20 | white-space: pre;
21 | max-width: 100%;
22 | }
23 |
24 | label.mdbc-pref-label-hone {
25 | font-size: 18px;
26 | font-weight: 700;
27 | text-align: center;
28 | }
29 |
30 | caption.mdbc-pref-section-title {
31 | padding: 15px 0 5px 10px;
32 | font-size: 18px;
33 | font-weight: 700;
34 | }
35 |
36 | label.mdbc-pref-subsection-title {
37 | padding: 15px 0 1px 10px;
38 | font-size: 16px;
39 | font-weight: 500;
40 | }
41 |
42 | description.mdbc-pref-description-indent {
43 | padding-left: 10px;
44 | }
45 |
46 | input.mdbc-pref-textbox-flex1 {
47 | -moz-box-flex: 1;
48 | }
49 |
50 | /*input.mdbc-pref-textbox-fullwidth {*/
51 | /* width: 100%;*/
52 | /*}*/
53 |
54 | input[type='text'].mdbc-pref-textbox-partialwidth {
55 | min-width: 80px;
56 | width: 100px;
57 | /*-moz-box-flex: 1;*/
58 | /*width: 100%;*/
59 | flex-grow: 3; /* Make the input expand */
60 | margin-left: 0;
61 | margin-right: 0;
62 | font-family: monospace;
63 | }
64 |
65 | radio.mdbc-pref-radio-main {
66 | padding: 18px 0 1px 2px;
67 | font-size: 16px;
68 | font-weight: 700;
69 | }
70 |
71 | .mdbc-pref-explication-minor {
72 | font-size: 11px;
73 | }
74 |
75 | .mdbc-pref-text-center {
76 | text-align: center;
77 | }
78 |
79 | .mdbc-pref-width-full {
80 | /*width: 100%;*/
81 | width: 600px;
82 | }
83 |
84 | hr.mdbc-pref-hr {
85 | border: 2px solid darkslategrey;
86 | margin-top: 10px;
87 | opacity: 20%;
88 | border-radius: 5px;
89 | }
90 |
91 | div.mdbc-pref-options-div {
92 | background-color: #e5e5e5;
93 | border: 2px solid rgba(0, 0, 0, 0.5);
94 | padding: 5px;
95 | margin-left: 5px;
96 | margin-right: 5px;
97 | border-radius: 10px;
98 | /*width: 100%;*/
99 | width: 94%;
100 | /*max-width: fit-content;*/
101 | }
102 |
103 | span.mdbc-pref-options-code-span {
104 | font-family: monospace;
105 | font-weight: 600;
106 | }
107 | span.mdbc-pref-options-span-first {
108 | text-align: left;
109 | }
110 | span.mdbc-pref-options-span-left {
111 | padding-left: 4px;
112 | text-align: right;
113 | padding-right: 0;
114 | margin-right: 0;
115 | font-size: 13px;
116 | }
117 | span.mdbc-pref-options-span-right {
118 | flex-grow: 1;
119 | text-align: left;
120 | padding-left: 0;
121 | margin-left: 0;
122 | font-size: 13px;
123 | }
124 | code.mdbc-pref-code-indent {
125 | padding-left: 10px;
126 | }
127 |
128 | /*div.testtt3 {*/
129 | /* background-color: #e5e5e5;*/
130 | /* border: 2px solid rgba(1, 0, 0, 0.5);*/
131 | /* padding: 5px;*/
132 | /* margin-left: 5px;*/
133 | /* margin-right: 5px;*/
134 | /* border-radius: 10px;*/
135 | /* !*width: 100%;*!*/
136 | /* width: 600px;*/
137 | /* !*max-width: fit-content;*!*/
138 | /*}*/
139 |
140 | /* Dark theme font color */
141 | @media (prefers-color-scheme: dark) {
142 | div.mdbc-pref-options-div {
143 | background-color: #212121;
144 | border: 2px solid rgba(255, 255, 255, 0.5);
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/addon/content/preferences.xhtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
27 |
28 |
29 |
30 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
48 |
51 |
52 | __addonName__ will search this directory recursively for markdown files.
53 |
54 |
55 |
56 |
57 |
58 |
61 |
67 |
70 | Recursively find markdown files beginning with @ .
71 |
74 |
75 | E.g. a filename could be '@shepard1987science Universal Law.md ', where 'shepard1987science ' is the BetterBibTeX citekey.
78 |
79 |
80 |
81 |
82 |
88 |
91 | Recursively find files matching this RegExp pattern.
92 |
93 |
96 | / /i
106 |
107 |
108 | If the BBT citekey is present in the filename pattern, you can include one capturing group to match it.
109 | E.g. the default RegExp filters for MarkDown files that begin with @ and attempts to use the subsequent non-whitespace characters as the BBT citekey:
112 | /^@(\S+).*\.md$/i
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | Match Markdown Files to Zotero Items Using:
123 |
124 |
125 |
128 |
129 |
135 |
136 |
139 | __addonName__ will search for a BetterBibTeX citekey in the YAML metadata.
140 |
141 |
142 |
143 |
144 | Specify the markdown YAML metadata keyword (optional if BBT citkey is in title, required otherwise).
145 |
146 | YAML keyword:
153 |
154 | If you want to extract the citekey from a markdown note's metadata, indicate the keyword here. A common value is: 'citekey '.
157 | If the specified keyword is not found in the YAML header, MDBC will fallback to searching the filename for a BBT citekey.
158 | If no YAML keyword is given, the citekey will be taken from the filename.
159 |
160 |
161 |
162 |
163 |
164 |
165 |
171 |
172 |
175 | __addonName__ will search for a BetterBibTeX citekey using a custom RegExp.
176 |
177 |
178 |
179 | RegExp to match BBT citekey in file content (optional if BBT citkey is in title, required otherwise)
180 |
181 | RegExp: / /m
190 |
191 | If you want to extract the citekey from a markdown file's contents, specify a RegExp with one capturing group here. It should return exactly one match per file.
194 | If more than one match is returned only the first will be used. If the RegExp does not return any matches, MDBC will fallback to searching the filename for a BBT citekey.
197 | If no pattern is given, the citekey will be taken from the filename.
198 |
199 |
200 |
201 |
202 |
203 |
204 |
210 |
211 |
214 | __addonName__ will search for a Zotero-Item-Key in the note's contents.
215 |
216 |
217 |
218 | Required: Specify a RegExp to match the Zotero-Item-Key from the markdown contents.
219 |
220 | RegExp: / /m
229 |
230 | E.g. for a Zotero item that can be opened with this URI zotero://select/library/items/ABCD1234 the RegExp should return a single match:
232 | 'ABCD1234 '.
234 | N.B. The Zotero-Item-Key is assigned automatically by Zotero. It is not the same as the BetterBibTeX citekey.
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 | Open Markdown Files using:
246 |
247 |
248 |
251 |
252 |
253 |
259 |
260 |
263 |
264 | If you use multiple Vaults, you can specify which Vault should open your markdown reading notes.
265 |
266 | Obsidian Vault name (optional):
272 |
273 | The Vault name replaces {{vault}} in obsidian://open?vault={{vault}}&path={{filepath}}
276 |
277 |
278 |
279 |
280 |
281 | Use only the filename in the URI rather than using the full file path. This is only advised if the default is not working.
284 |
285 |
286 |
287 |
291 |
292 |
295 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
316 |
317 |
320 |
321 |
322 | logseq graph name:
328 |
329 |
332 |
333 |
334 |
335 |
336 | logseq page prefix (optional):
342 |
343 |
346 |
347 |
348 |
349 | The prefix and graph should be entered as url encoded strings that can be directly inserted into the logseq:// URI.
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
366 |
367 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 | Advanced Settings
382 |
383 |
384 |
385 | Specify a Custom Tag Name?
386 |
387 |
388 | The default Zotero tag is 'ObsCite'. If you would prefer the tag to be something else, specify the tag name here.
389 |
390 | tag:
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 | Include Group Libraries?
405 |
406 |
407 | Whether to include items in Group Libraries when matching MD files, or only search for matches in the User Library.
408 |
411 |
414 |
417 |
418 | N.B. you will need to assign a color to the ObsCite tag in each Group Library.
419 |
420 |
421 |
422 |
423 |
424 |
425 | Remove tags from unmatched Zotero items?
426 |
427 |
428 | Whether to remove tags from Zotero items when no Markdown file is found.
429 |
432 |
435 |
438 |
439 | Certain special use cases may prefer to not remove tags from Zotero items.
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 | Troubleshooting
452 |
453 |
454 |
455 | Save Debugging Log
456 |
457 |
458 | If you're encountering errors, you can generate a debugging file to examine yourself and/or to include in a GitHub issue.
459 |
463 |
464 |
465 |
466 |
467 |
468 |
469 | Git Help
470 |
471 |
472 | Open Issue on GitHub
473 | Open Issue on GitHub Open Issue on GitHub Open Issue on GitHub Open Issue on GitHub Open Issue on GitHub Open Issue on GitHub Open Issue on GitHub Open Issue on GitHub
476 |
480 |
481 |
482 |
483 |
484 | Debug Log Level
485 |
486 |
487 | This changes what message are stored in the __addonName__ logger. It doesn't affect the exported Debugging File. Leave this at "Minimal" unless you are developing the addon.
490 |
491 |
492 |
493 |
494 |
498 |
499 |
502 |
505 |
506 |
507 |
508 |
509 |
510 |
511 |
512 |
513 |
514 |
515 |
516 |
517 |
518 |
521 |
522 |
523 |
--------------------------------------------------------------------------------
/addon/locale/en-US/addon.ftl:
--------------------------------------------------------------------------------
1 | startup-begin = MarkDB-Connect loading
2 | startup-syncing = Syncing Notes
3 | startup-finish = MarkDB-Connect ready
4 |
5 | menuitem-prefs = MarkDB-Connect Preferences…
6 | menuitem-sync = MarkDB-Connect Sync Tags
7 | menuitem-troubleshoot = MarkDB-Connect Troubleshooting
8 | contextmenuitem-reveal = Show Markdown File
9 | contextmenuitem-open-default = Open Markdown Note
10 | contextmenuitem-open-obsidian = Open Note in Obsidian
11 | contextmenuitem-open-logseq = Open Note in logseq
12 |
13 | prefs-title = MarkDB-Connect
14 |
15 |
16 | report-syncdebug = Run Sync with Data Logging
17 | report-savedebuglogs = Save Full Debugging Logs
18 |
--------------------------------------------------------------------------------
/addon/locale/en-US/preferences.ftl:
--------------------------------------------------------------------------------
1 | pref-title = Markdown DataBase Connect
2 | pref-enable =
3 | .label = Enable
4 | pref-help = { $name } Build { $version } { $time }
5 |
6 | pref-locating-settings = Locating MD Files
7 | pref-launching-settings = Launching MD Files
8 | pref-advanced-settings = Advanced
9 |
10 | pref-locate-mdfiles = Location of Markdown Reading Notes
11 | pref-locate-mdfiles-desc = Specify how MarkDB-Connect should find the markdown files
12 | pref-vault-source-folder = Folder Containing Markdown Reading Notes:
13 | pref-vault-source-folder-desc = MarkDB-Connect will search this directory recursively for markdown files
14 | pref-filefilterstrategy-title = File Filter
15 | pref-filefilterstrategy-default-label = Default File Filter: recursively find markdown files beginning with {"@"}
16 | pref-filefilterstrategy-default-desc1 = Markdown files should begin with {"@"}{"{"}{"{"}citekey{"}"}{"}"}, where {"{"}{"{"}citekey{"}"}{"}"} is the BetterBibTeX citekey,
17 |
18 | pref-filefilterstrategy-custom-label = Custom File Filter: recursively find files matching this RegExp pattern
19 | pref-filefilterstrategy-custom-desc1 = RegExp (case insensitive):
20 | pref-filefilterstrategy-custom-desc2 = Default pattern is: ^@.+\.md$. NB optional first capturing group used as BBT citekey.
21 |
22 | pref-matchstrategy-bbt-title = Match notes based on BetterBibTeX citekey
23 | pref-matchstrategy-bbt-desc = MarkDB-Connect will search for a BetterBibTeX citekey in the filename and/or metadata.
24 |
25 | pref-dbapp-obsidian-resolve-with-file-checkbox =
26 | .label = Use filename instead of filepath in URI
27 |
28 | calendar-view-toggle-day = Day
29 | .title = Switch to day view
30 |
31 |
--------------------------------------------------------------------------------
/addon/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "__addonName__",
4 | "version": "__buildVersion__",
5 | "description": "__description__",
6 | "homepage_url": "__homepage__",
7 | "author": "__author__",
8 | "icons": {
9 | "48": "content/icons/favicon@0.5x.png",
10 | "96": "content/icons/favicon.png"
11 | },
12 | "applications": {
13 | "zotero": {
14 | "id": "__addonID__",
15 | "update_url": "__updateURL__",
16 | "strict_min_version": "6.999",
17 | "strict_max_version": "7.*"
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/addon/prefs.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | pref('enable', true)
3 |
4 | //
5 |
6 | pref('configuration', '0.0.0')
7 | pref('debugmode', 'minimal')
8 |
9 | pref('sourcedir', '')
10 |
11 | pref('filefilterstrategy', 'default') // ['default', 'customfileregexp'][0]
12 | pref('filepattern', '^@.+\\.md$')
13 |
14 | pref('matchstrategy', 'bbtcitekeyyaml') // ['bbtcitekeyyaml', 'bbtcitekeyregexp', 'zotitemkey'][0]
15 | pref('bbtyamlkeyword', '')
16 | pref('bbtregexp', '')
17 | pref('zotkeyregexp', '')
18 |
19 | pref('mdeditor', 'obsidian') // ['obsidian', 'logseq', 'system'][0]
20 | pref('obsidianvaultname', '')
21 | // pref('obsidianresolvewithfile', false) // [false, true][0]
22 | pref('obsidianresolvespec', 'path') // ['path', 'file'][0]
23 |
24 | pref('logseqgraph', '')
25 | pref('logseqprefix', '')
26 |
27 | pref('grouplibraries', 'user') // ['user', 'group'][0]
28 | pref('removetags', 'keepsynced') // ['keepsynced', 'addonly'][0]
29 | pref('tagstr', 'ObsCite')
30 |
--------------------------------------------------------------------------------
/docs/assets/readme/ExternalLinkNotificationScreenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daeh/zotero-markdb-connect/1db3ee2faad330a85f8eb1ec6c359333a4d68a30/docs/assets/readme/ExternalLinkNotificationScreenshot.png
--------------------------------------------------------------------------------
/docs/assets/readme/MarkDBConnectScreenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daeh/zotero-markdb-connect/1db3ee2faad330a85f8eb1ec6c359333a4d68a30/docs/assets/readme/MarkDBConnectScreenshot.png
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname, resolve } from 'path'
2 | import { fileURLToPath } from 'url'
3 |
4 | import stylisticPlugin from '@stylistic/eslint-plugin'
5 | import typescriptEslintPlugin from '@typescript-eslint/eslint-plugin'
6 | import typescriptEslintParser from '@typescript-eslint/parser'
7 | import prettierConfig from 'eslint-config-prettier'
8 | import importPlugin from 'eslint-plugin-import'
9 | import prettierPlugin from 'eslint-plugin-prettier'
10 | import globals from 'globals'
11 |
12 | const projectDirname = dirname(fileURLToPath(import.meta.url))
13 |
14 | const context = (() => {
15 | if (typeof process.env.NODE_ENV === 'undefined') return 'default'
16 | if (process.env.NODE_ENV === 'development') return 'development'
17 | if (process.env.NODE_ENV === 'production') return 'production'
18 | if (process.env.NODE_ENV === 'repo') return 'repository'
19 | new Error('Invalid NODE_ENV')
20 | return 'error'
21 | })()
22 |
23 | const projectFilesToIgnore =
24 | context === 'repository' ? [] : ['.release-it.ts', 'zotero-plugin.config.ts', '*.config.mjs']
25 |
26 | const tsconfig = (() => {
27 | if (context === 'default') return './tsconfig.json'
28 | if (context === 'development') return './tsconfig.dev.json'
29 | if (context === 'production') return './tsconfig.prod.json'
30 | if (context === 'repository') return './tsconfig.repo.json'
31 | new Error('Invalid context')
32 | return 'error'
33 | })()
34 |
35 | console.log(`env: ${process.env.NODE_ENV}, context: ${context}, tsconfig: ${tsconfig}`)
36 |
37 | const allTsExtensionsArray = ['ts', 'mts', 'cts', 'tsx', 'mtsx']
38 | const allJsExtensionsArray = ['js', 'mjs', 'cjs', 'jsx', 'mjsx']
39 | const allTsExtensions = allTsExtensionsArray.join(',')
40 | const allJsExtensions = allJsExtensionsArray.join(',')
41 | const allExtensions = [...allTsExtensionsArray, ...allJsExtensionsArray].join(',')
42 |
43 | const importRules = {
44 | ...importPlugin.flatConfigs.recommended.rules,
45 | 'import/no-unresolved': 'error',
46 | 'import/namespace': 'off',
47 | 'sort-imports': [
48 | 'error',
49 | {
50 | allowSeparatedGroups: true,
51 | ignoreCase: true,
52 | ignoreDeclarationSort: true,
53 | ignoreMemberSort: false,
54 | memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
55 | },
56 | ],
57 | 'import/order': [
58 | 'error',
59 | {
60 | 'groups': [
61 | 'builtin', // Built-in imports (come from NodeJS native) go first
62 | 'external', // External imports
63 | 'internal', // Absolute imports
64 | 'parent', // Relative imports
65 | 'sibling', // Relative imports
66 | // ['sibling', 'parent'], // Relative imports, the sibling and parent types they can be mingled together
67 | 'index', // index imports
68 | 'type', // type imports
69 | 'object', // object imports
70 | 'unknown', // unknown
71 | ],
72 | 'newlines-between': 'always',
73 | 'alphabetize': {
74 | order: 'asc',
75 | caseInsensitive: true, // ignore case
76 | },
77 | },
78 | ],
79 | }
80 |
81 | const baseRules = {
82 | 'prettier/prettier': 'warn',
83 | '@stylistic/max-len': [
84 | 'warn',
85 | { code: 120, ignoreComments: true, ignoreTrailingComments: true, ignoreStrings: true, ignoreUrls: true },
86 | ],
87 | '@stylistic/indent': ['error', 2, { SwitchCase: 1 }],
88 | '@stylistic/semi': ['error', 'never'],
89 | '@stylistic/quotes': ['warn', 'single', { avoidEscape: true, allowTemplateLiterals: false }],
90 | '@stylistic/object-curly-spacing': ['warn', 'always'],
91 | '@stylistic/array-element-newline': ['error', 'consistent'],
92 | // '@stylistic/multiline-ternary': ['warn', 'always'],
93 | }
94 |
95 | const typescriptRules = {
96 | ...prettierConfig.rules,
97 | ...typescriptEslintPlugin.configs.recommended.rules,
98 | ...typescriptEslintPlugin.configs['recommended-type-checked'].rules,
99 | //
100 | // ...typescriptEslintPlugin.configs.strict.rules,
101 | // ...typescriptEslintPlugin.configs['strict-type-checked'].rules,
102 | //
103 | ...typescriptEslintPlugin.configs['stylistic-type-checked'].rules,
104 | ...stylisticPlugin.configs['disable-legacy'].rules,
105 | ...importRules,
106 | ...baseRules,
107 | }
108 |
109 | const javascriptRules = {
110 | ...prettierConfig.rules,
111 | ...typescriptEslintPlugin.configs.recommended.rules,
112 | ...typescriptEslintPlugin.configs.strict.rules,
113 | ...typescriptEslintPlugin.configs.stylistic.rules,
114 | ...stylisticPlugin.configs['disable-legacy'].rules,
115 | ...importRules,
116 | ...baseRules,
117 | }
118 |
119 | const typescriptRulesDev = {
120 | '@typescript-eslint/no-explicit-any': ['off', { ignoreRestArgs: true }],
121 | '@typescript-eslint/no-unsafe-assignment': ['warn'],
122 | '@typescript-eslint/no-unsafe-member-access': ['off'],
123 | '@typescript-eslint/no-unsafe-return': ['warn'],
124 | '@typescript-eslint/no-unsafe-argument': ['warn'],
125 | '@typescript-eslint/no-unsafe-call': ['off'],
126 | '@typescript-eslint/no-unused-vars': ['off'],
127 | '@typescript-eslint/prefer-nullish-coalescing': ['off'],
128 | '@typescript-eslint/no-inferrable-types': ['off'],
129 | '@typescript-eslint/no-floating-promises': ['warn'],
130 | '@typescript-eslint/require-await': ['warn'],
131 | // '@typescript-eslint/dot-notation': ['off'],
132 | '@typescript-eslint/no-non-null-assertion': 'off',
133 | '@typescript-eslint/ban-ts-comment': [
134 | 'warn',
135 | {
136 | 'ts-expect-error': 'allow-with-description',
137 | 'ts-ignore': 'allow-with-description',
138 | 'ts-nocheck': 'allow-with-description',
139 | 'ts-check': 'allow-with-description',
140 | },
141 | ],
142 | '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
143 | }
144 |
145 | const javascriptRulesDev = {}
146 |
147 | const config = [
148 | {
149 | /* setup parser for all files */
150 | files: [`**/*.{${allExtensions}}`],
151 | languageOptions: {
152 | parser: typescriptEslintParser,
153 | parserOptions: {
154 | ecmaVersion: 'latest', // 2024 sets the ecmaVersion parser option to 15
155 | sourceType: 'module',
156 | tsconfigRootDir: resolve(projectDirname),
157 | project: tsconfig,
158 | },
159 | },
160 | },
161 | {
162 | /* all typescript files, except config files */
163 | files: [`**/*.{${allTsExtensions}}`],
164 | ignores: [`**/*.config.{${allTsExtensions}}`],
165 | languageOptions: {
166 | globals: {
167 | ...globals.browser,
168 | // ...globals.node,
169 | ...globals.es2021,
170 | },
171 | },
172 | plugins: {
173 | '@typescript-eslint': typescriptEslintPlugin,
174 | '@stylistic': stylisticPlugin,
175 | 'import': importPlugin,
176 | 'prettier': prettierPlugin,
177 | },
178 | rules: {
179 | ...typescriptRules,
180 | },
181 | },
182 | {
183 | /* +strict for typescript files NOT in ./src/ folder */
184 | files: [`**/*.{${allTsExtensions}}`],
185 | ignores: [`src/**/*.{${allTsExtensions}}`, 'typings/**/*.d.ts', `**/*.config.{${allTsExtensions}}`],
186 | plugins: {
187 | '@typescript-eslint': typescriptEslintPlugin,
188 | '@stylistic': stylisticPlugin,
189 | },
190 | rules: {
191 | ...typescriptEslintPlugin.configs.strict.rules,
192 | ...typescriptEslintPlugin.configs['strict-type-checked'].rules,
193 | },
194 | },
195 | {
196 | /* +lenient for typescript files in ./src/ folder */
197 | files: [`src/**/*.{${allTsExtensions}}`, 'typings/**/*.d.ts'],
198 | ignores: [`**/*.config.{${allTsExtensions}}`],
199 | settings: {
200 | 'import/resolver': {
201 | typescript: {
202 | project: tsconfig,
203 | alwaysTryTypes: true,
204 | },
205 | node: {
206 | extensions: ['.ts', '.tsx'],
207 | moduleDirectory: ['node_modules', 'src/'],
208 | },
209 | },
210 | 'import/parsers': {
211 | '@typescript-eslint/parser': ['.ts', '.tsx'],
212 | },
213 | },
214 | rules: {
215 | ...typescriptRules,
216 | ...typescriptRulesDev,
217 | 'no-restricted-globals': [
218 | 'error',
219 | { message: 'Use `Zotero.getMainWindow()` instead.', name: 'window' },
220 | {
221 | message: 'Use `Zotero.getMainWindow().document` instead.',
222 | name: 'document',
223 | },
224 | {
225 | message: 'Use `Zotero.getActiveZoteroPane()` instead.',
226 | name: 'ZoteroPane',
227 | },
228 | 'Zotero_Tabs',
229 | ],
230 | },
231 | },
232 | {
233 | /* config files: javascript */
234 | files: [`**/*.config.{${allJsExtensions}}`],
235 | settings: {
236 | 'import/resolver': {
237 | node: {},
238 | typescript: {
239 | extensions: ['.ts', '.d.ts'],
240 | },
241 | },
242 | // 'import/ignore': ['node_modules/firebase'],
243 | },
244 | // languageOptions: {
245 | // globals: {
246 | // ...globals.browser,
247 | // ...globals.node,
248 | // ...globals.es2021,
249 | // },
250 | // },
251 | // 'import/resolver': {
252 | // // node: {},
253 | // typescript: {
254 | // extensions: ['.ts', '.d.ts'],
255 | // },
256 | // },
257 | plugins: {
258 | '@typescript-eslint': typescriptEslintPlugin,
259 | '@stylistic': stylisticPlugin,
260 | 'import': importPlugin,
261 | 'prettier': prettierPlugin,
262 | },
263 | rules: {
264 | ...javascriptRules,
265 | '@typescript-eslint/no-unsafe-assignment': ['off'],
266 | '@typescript-eslint/no-unused-vars': ['off'],
267 | '@typescript-eslint/no-unsafe-member-access': ['off'],
268 | },
269 | },
270 | {
271 | ignores: [
272 | 'build/**',
273 | '.scaffold/**',
274 | 'node_modules/**',
275 | 'scripts/',
276 | '**/*.js',
277 | '**/*.bak',
278 | '**/*-lintignore*',
279 | '**/*_lintignore*',
280 | ...projectFilesToIgnore,
281 | ],
282 | },
283 | ]
284 |
285 | export default config
286 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "markdb-connect",
3 | "version": "0.1.6",
4 | "description": "Zotero plugin that links your Markdown database to Zotero.",
5 | "author": "Dae Houlihan (https://daeh.info)",
6 | "license": "MIT",
7 | "type": "module",
8 | "config": {
9 | "addonName": "MarkDB-Connect",
10 | "addonID": "daeda@mit.edu",
11 | "addonRef": "mdbc",
12 | "addonInstance": "MDBC",
13 | "prefsPrefix": "extensions.zotero.mdbc",
14 | "updateJSON": "https://raw.githubusercontent.com/daeh/zotero-markdb-connect/main/update.json"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/daeh/zotero-markdb-connect.git"
19 | },
20 | "bugs": {
21 | "url": "https://github.com/daeh/zotero-markdb-connect/issues"
22 | },
23 | "homepage": "https://github.com/daeh/zotero-markdb-connect#readme",
24 | "scripts": {
25 | "start": "zotero-plugin serve",
26 | "build": "zotero-plugin build && tsc --noEmit",
27 | "lint": "export ESLINT_USE_FLAT_CONFIG=true && export NODE_ENV=production && prettier --config prettier.config.mjs --write . && eslint --config eslint.config.mjs --fix .",
28 | "lint:repo": "export ESLINT_USE_FLAT_CONFIG=true && export NODE_ENV=repo && prettier --config prettier.config.mjs --write . && eslint --config eslint.config.mjs --fix .",
29 | "update-deps": "npm outdated --depth=0; npm update --save; npm update --save-dev; npm outdated --depth=0",
30 | "release": "zotero-plugin release",
31 | "test": "echo \"Error: no test specified\" && exit 1"
32 | },
33 | "dependencies": {
34 | "zotero-plugin-toolkit": "^5.0.0-0"
35 | },
36 | "devDependencies": {
37 | "@stylistic/eslint-plugin": "^4.2.0",
38 | "@types/node": "^22.14.0",
39 | "@typescript-eslint/eslint-plugin": "^8.29.0",
40 | "@typescript-eslint/parser": "^8.29.0",
41 | "eslint": "^9.24.0",
42 | "eslint-config-prettier": "^10.1.1",
43 | "eslint-import-resolver-typescript": "^4.3.1",
44 | "eslint-plugin-import": "^2.31.0",
45 | "eslint-plugin-prettier": "^5.2.6",
46 | "prettier": "^3.5.3",
47 | "typescript": "^5.8.3",
48 | "typescript-eslint": "^8.29.0",
49 | "zotero-plugin-scaffold": "^0.4.1",
50 | "zotero-types": "^4.0.0-beta.3"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/prettier.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import("prettier").Config} */
2 | const config = {
3 | printWidth: 120,
4 | trailingComma: 'all',
5 | tabWidth: 2,
6 | useTabs: false,
7 | semi: false,
8 | singleQuote: true,
9 | quoteProps: 'consistent',
10 | bracketSpacing: true,
11 | bracketSameLine: true,
12 | arrowParens: 'always',
13 | proseWrap: 'never',
14 | endOfLine: 'lf',
15 | overrides: [
16 | {
17 | files: ['*.xhtml'],
18 | options: {
19 | printWidth: 200,
20 | singleQuote: false,
21 | bracketSameLine: true,
22 | htmlWhitespaceSensitivity: 'css',
23 | singleAttributePerLine: true,
24 | },
25 | },
26 | {
27 | files: ['*.yml'],
28 | options: {
29 | singleQuote: false,
30 | },
31 | },
32 | {
33 | files: ['*.md'],
34 | options: {
35 | proseWrap: 'preserve',
36 | },
37 | },
38 | {
39 | files: ['*.json'],
40 | options: {
41 | printWidth: 400,
42 | trailingComma: 'es5',
43 | tabWidth: 2,
44 | singleQuote: false,
45 | },
46 | },
47 | {
48 | files: ['*.code-workspace', '.vscode/*.json'],
49 | options: {
50 | parser: 'json5',
51 | quoteProps: 'preserve',
52 | trailingComma: 'none',
53 | singleQuote: false,
54 | printWidth: 200,
55 | bracketSameLine: false,
56 | },
57 | },
58 | ],
59 | }
60 |
61 | export default config
62 |
--------------------------------------------------------------------------------
/src/addon.ts:
--------------------------------------------------------------------------------
1 | import { config } from '../package.json'
2 |
3 | import hooks from './hooks'
4 | import { createZToolkit } from './utils/ztoolkit'
5 |
6 | import type { ColumnOptions, DialogHelper } from 'zotero-plugin-toolkit'
7 |
8 | class Addon {
9 | public data: {
10 | alive: boolean
11 | config: typeof config
12 | // Env type, see build.js
13 | env: 'development' | 'production'
14 | ztoolkit: ZToolkit
15 | locale?: {
16 | current: any
17 | }
18 | prefs?: {
19 | window: Window
20 | columns: ColumnOptions[]
21 | rows: Record[]
22 | }
23 | dialog?: DialogHelper
24 | }
25 | // Lifecycle hooks
26 | public hooks: typeof hooks
27 | // APIs
28 | public api: object
29 |
30 | constructor() {
31 | this.data = {
32 | alive: true,
33 | config,
34 | env: __env__,
35 | ztoolkit: createZToolkit(),
36 | }
37 | this.hooks = hooks
38 | this.api = {}
39 | }
40 | }
41 |
42 | export default Addon
43 |
--------------------------------------------------------------------------------
/src/dataGlobals.ts:
--------------------------------------------------------------------------------
1 | import type { Entry } from './mdbcTypes'
2 |
3 | const DataStore: {
4 | cleanrun: boolean
5 | data: Record
6 | zotIds: number[]
7 | } = {
8 | cleanrun: true,
9 | data: {},
10 | zotIds: [],
11 | }
12 |
13 | export class DataManager {
14 | static initialize(): void {
15 | DataStore.cleanrun = true
16 | DataStore.data = {}
17 | DataStore.zotIds = []
18 | }
19 |
20 | static markFail(): void {
21 | DataStore.cleanrun = false
22 | }
23 |
24 | static checkForKey(key: string): boolean {
25 | return Object.keys(DataStore.data).includes(key)
26 | }
27 |
28 | static checkForZotId(itemId: number): boolean {
29 | return DataStore.zotIds.includes(itemId)
30 | }
31 |
32 | static getEntryList(itemId: number): Entry[] {
33 | return DataStore.data[itemId.toString()]
34 | }
35 | static addEntry(zotid: number, entry_res: Entry): void {
36 | if (Object.keys(DataStore.data).includes(zotid.toString())) {
37 | DataStore.data[zotid.toString()].push(entry_res)
38 | } else {
39 | DataStore.data[zotid.toString()] = [entry_res]
40 | DataStore.zotIds.push(zotid)
41 | }
42 | }
43 | static isClean(): boolean {
44 | return DataStore.cleanrun
45 | }
46 | static data() {
47 | return DataStore.data
48 | }
49 | static zotIds() {
50 | return DataStore.zotIds
51 | }
52 | static dump() {
53 | return {
54 | cleanrun: DataStore.cleanrun,
55 | data: DataStore.data,
56 | zotIds: DataStore.zotIds,
57 | }
58 | }
59 | static numberRecords(): number {
60 | return DataStore.zotIds.length
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/hooks.ts:
--------------------------------------------------------------------------------
1 | // import { config } from '../package.json'
2 |
3 | import { DataManager } from './dataGlobals'
4 | import { Elements } from './modules/create-element'
5 | import { Logger } from './modules/mdbcLogger'
6 | import { ScanMarkdownFiles } from './modules/mdbcScan'
7 | import { wrappers } from './modules/mdbcStartupHelpers'
8 | import { Notifier, prefHelpers, Registrar, systemInterface, UIHelpers } from './modules/mdbcUX'
9 | import { unpatch as $unpatch$ } from './modules/monkey-patch'
10 | import { registerPrefsScripts } from './modules/preferenceScript'
11 | import { getString, initLocale } from './utils/locale'
12 | import { createZToolkit } from './utils/ztoolkit'
13 |
14 | async function onStartup() {
15 | await Promise.all([Zotero.initializationPromise, Zotero.unlockPromise, Zotero.uiReadyPromise])
16 |
17 | initLocale()
18 |
19 | await wrappers.startupVersionCheck()
20 |
21 | Registrar.registerPrefs()
22 |
23 | // BasicExampleFactory.registerNotifier()
24 |
25 | // registerPreferenceStyleSheet()
26 |
27 | // await onMainWindowLoad(window)
28 |
29 | await Promise.all(Zotero.getMainWindows().map((win) => onMainWindowLoad(win)))
30 | }
31 |
32 | async function onMainWindowLoad(win: _ZoteroTypes.MainWindow): Promise {
33 | // Create ztoolkit for every window
34 | addon.data.ztoolkit = createZToolkit()
35 |
36 | // @ts-ignore This is a moz feature
37 | // win.MozXULElement.insertFTLIfNeeded(`${addon.data.config.addonRef}-mainWindow.ftl`)
38 |
39 | const popupWin = new ztoolkit.ProgressWindow(addon.data.config.addonName, {
40 | closeOnClick: true,
41 | closeTime: -1,
42 | })
43 | .createLine({
44 | text: getString('startup-begin'),
45 | icon: Notifier.notificationTypes.addon,
46 | type: 'default',
47 | progress: 0,
48 | })
49 | .show()
50 |
51 | // KeyExampleFactory.registerShortcuts();
52 |
53 | popupWin.changeLine({
54 | progress: 30,
55 | text: `[30%] ${getString('startup-syncing')}`,
56 | })
57 |
58 | // TODO Only run Sync if config check passes.
59 | await ScanMarkdownFiles.syncWrapper(false, false)
60 |
61 | popupWin.changeLine({
62 | progress: 80,
63 | text: `[80%] ${getString('startup-finish')}`,
64 | })
65 |
66 | UIHelpers.registerWindowMenuItem_Sync()
67 | if (!DataManager.isClean() || DataManager.numberRecords() === 0 || addon.data.env === 'development') {
68 | UIHelpers.registerWindowMenuItem_Debug()
69 | } else {
70 | ///WIP
71 | // try {
72 | // ztoolkit.Menu.unregister(`${config.addonRef}-tools-menu-troubleshoot`)
73 | // } catch (err) {
74 | // Logger.log('toolsmenu', `ERROR: unregister :: ${err}`)
75 | // }
76 | }
77 | // register(menuPopup: XUL.MenuPopup | keyof typeof MenuSelector, options: MenuitemOptions, insertPosition?: "before" | "after", anchorElement?: XUL.Element): false | undefined;
78 | // unregister(menuId: string): void;
79 |
80 | UIHelpers.registerRightClickMenuItem()
81 |
82 | popupWin.changeLine({
83 | progress: 100,
84 | text: `[100%] ${getString('startup-finish')}`,
85 | })
86 |
87 | if (Logger.mode() !== 'minimal' || addon.data.env === 'development') {
88 | popupWin.addLines(`DebugMode: ${Logger.mode()}`, Notifier.notificationTypes.debug)
89 | }
90 |
91 | if (addon.data.env === 'development') {
92 | popupWin.addLines(`ENV: ${addon.data.env}`, Notifier.notificationTypes.debug)
93 | }
94 |
95 | popupWin.startCloseTimer(3000)
96 | }
97 |
98 | function syncMarkDB() {
99 | //// called from tools menu ////
100 | const displayReport = false
101 | const saveLogsToggle = false
102 |
103 | ScanMarkdownFiles.syncWrapper(displayReport, saveLogsToggle)
104 | .then(() => {
105 | Logger.log('syncMarkDB', 'finished', true, 'info')
106 | })
107 | .catch((err) => {
108 | Logger.log('syncMarkDB', `ERROR :: ${err}`, true, 'error')
109 | })
110 | }
111 |
112 | function syncMarkDBReport() {
113 | //// called from tools menu ////
114 | const displayReport = true
115 | const saveLogsToggle = false
116 |
117 | ScanMarkdownFiles.syncWrapper(displayReport, saveLogsToggle)
118 | .then(() => {
119 | Logger.log('syncMarkDBReport', 'finished', true, 'info')
120 | })
121 | .catch((err) => {
122 | Logger.log('syncMarkDBReport', `ERROR :: ${err}`, true, 'error')
123 | })
124 | }
125 |
126 | function syncMarkDBSaveDebug() {
127 | //// called from prefs ////
128 | const displayReport = false
129 | const saveLogsToggle = true
130 |
131 | ScanMarkdownFiles.syncWrapper(displayReport, saveLogsToggle)
132 | .then(() => {
133 | Logger.log('syncMarkDBSaveDebug', 'finished', true, 'info')
134 | })
135 | .catch((err) => {
136 | Logger.log('syncMarkDBSaveDebug', `ERROR :: ${err}`, true, 'error')
137 | // const loggedMessages = Logger.getMessages()
138 | // await
139 | // ScanMarkdownFiles.displayReportDialog([], loggedMessages)
140 | // await
141 | // systemInterface.dumpDebuggingLog()
142 | })
143 | }
144 |
145 | function saveLogs() {
146 | systemInterface
147 | .dumpDebuggingLog()
148 | .then(() => {
149 | Logger.log('saveDebuggingLog', 'finished', true, 'info')
150 | })
151 | .catch((err) => {
152 | Logger.log('saveDebuggingLog', `ERROR :: ${err}`, true, 'error')
153 | })
154 | }
155 |
156 | function saveJsonFile(data: string, title: string, filename: string) {
157 | systemInterface
158 | .dumpJsonFile(data, title, filename)
159 | .then(() => {
160 | Logger.log('dumpJsonFile', 'finished', true, 'info')
161 | })
162 | .catch((err) => {
163 | Logger.log('dumpJsonFile', `ERROR :: ${err}`, true, 'error')
164 | })
165 | }
166 |
167 | function Data() {
168 | return DataManager.data()
169 | }
170 | function DataZotIds() {
171 | return DataManager.zotIds()
172 | }
173 | function DataStore() {
174 | return DataManager.dump()
175 | }
176 | function Logs() {
177 | return Logger.dump()
178 | }
179 |
180 | async function onMainWindowUnload(win: Window): Promise {
181 | Elements.removeAll()
182 | $unpatch$()
183 | ztoolkit.unregisterAll()
184 | addon.data.dialog?.window?.close()
185 | }
186 |
187 | function onShutdown(): void {
188 | ztoolkit.unregisterAll()
189 | addon.data.dialog?.window?.close()
190 | // Remove addon object
191 | addon.data.alive = false
192 | // @ts-ignore - Plugin instance is not typed
193 | delete Zotero[addon.data.config.addonInstance]
194 | }
195 |
196 | /**
197 | * This function is just an example of dispatcher for Preference UI events.
198 | * Any operations should be placed in a function to keep this function clear.
199 | * @param type event type
200 | * @param data event data
201 | */
202 | async function onPrefsEvent(type: string, data: Record) {
203 | switch (type) {
204 | case 'load':
205 | // await registerPrefsScripts(data.window as Window)
206 | registerPrefsScripts(data.window)
207 | break
208 | case 'chooseVaultFolder':
209 | await prefHelpers.chooseVaultFolder()
210 | break
211 | case 'checkMetadataFormat':
212 | prefHelpers.checkMetadataFormat(data.value as string)
213 | break
214 | case 'checkRegExpValid':
215 | prefHelpers.isValidRegExp(data.value as string)
216 | break
217 | case 'checkTagStr':
218 | prefHelpers.checkTagStr(data.value as string)
219 | break
220 | case 'syncMarkDBSaveDebug':
221 | syncMarkDBSaveDebug()
222 | break
223 | default:
224 | break
225 | }
226 | }
227 |
228 | // Add your hooks here. For element click, etc.
229 | // Keep in mind hooks only do dispatch. Don't add code that does real jobs in hooks.
230 | // Otherwise the code would be hard to read and maintain.
231 |
232 | /*
233 | * E.g.:
234 | * Zotero.MDBC.hooks.DataStore()
235 | * Zotero.MDBC.hooks.Logs()
236 | */
237 | export default {
238 | onStartup,
239 | onShutdown,
240 | onMainWindowLoad,
241 | onMainWindowUnload,
242 | onPrefsEvent,
243 | syncMarkDB,
244 | syncMarkDBReport,
245 | syncMarkDBSaveDebug,
246 | Logs,
247 | DataStore,
248 | Data,
249 | DataZotIds,
250 | saveLogs,
251 | saveJsonFile,
252 | }
253 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { BasicTool } from 'zotero-plugin-toolkit'
2 |
3 | import { config } from '../package.json'
4 |
5 | import Addon from './addon'
6 |
7 | const basicTool = new BasicTool()
8 |
9 | // @ts-ignore - Plugin instance is not typed
10 | if (!basicTool.getGlobal('Zotero')[config.addonInstance]) {
11 | _globalThis.addon = new Addon()
12 | defineGlobal('ztoolkit', () => {
13 | return _globalThis.addon.data.ztoolkit
14 | })
15 | // @ts-ignore - Plugin instance is not typed
16 | Zotero[config.addonInstance] = addon
17 | }
18 |
19 | function defineGlobal(name: Parameters[0]): void
20 | function defineGlobal(name: string, getter: () => any): void
21 | function defineGlobal(name: string, getter?: () => any) {
22 | Object.defineProperty(_globalThis, name, {
23 | get() {
24 | return getter ? getter() : basicTool.getGlobal(name)
25 | },
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/src/mdbcTypes.d.ts:
--------------------------------------------------------------------------------
1 | export type DebugMode = 'minimal' | 'maximal'
2 |
3 | export type LogType = 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'config'
4 |
5 | export type NotificationType =
6 | | 'addon'
7 | | 'success'
8 | | 'error'
9 | | 'warn'
10 | | 'info'
11 | | 'debug'
12 | | 'config'
13 | | 'itemsadded'
14 | | 'itemsremoved'
15 |
16 | export type ZoteroIconFile = `${keyof typeof globalThis._ZoteroTypes.IconFile}`
17 | export type ZoteroIconURI = globalThis._ZoteroTypes.IconURI
18 |
19 | export interface Entry {
20 | citekey: string
21 | citekey_metadata: string
22 | citekey_title: string
23 | zotkeys: string[]
24 | zotids: number[]
25 | name: string
26 | path: string
27 | // filename: string
28 | // filenamebase: string
29 | // displayname: string
30 | }
31 |
32 | export interface NotifyCreateLineOptions {
33 | type?: string
34 | icon?: string
35 | text?: string
36 | progress?: number
37 | idx?: number
38 | }
39 |
40 | export interface notificationData {
41 | title: string
42 | // zotType?: 'default' | 'success' | 'fail'
43 | // iconFile?: ZoteroIconFile
44 | // iconURI?: ZoteroIconURI
45 | body?: string
46 | type?: NotificationType
47 | messageArray?: { body: string; type: NotificationType }[]
48 | }
49 |
50 | export interface messageData {
51 | rowData: {
52 | title: string
53 | message: string
54 | }
55 | saveData?: {
56 | saveButtonTitle: string
57 | saveDialogTitle: string
58 | fileNameSuggest: string
59 | dataGetter: () => string
60 | }
61 | notification?: notificationData
62 | }
63 |
--------------------------------------------------------------------------------
/src/modules/create-element.ts:
--------------------------------------------------------------------------------
1 | //// Adapted from https://github.com/retorquere/zotero-better-bibtex/blob/master/content/create-element.ts ////
2 |
3 | import { config } from '../../package.json'
4 |
5 | // createElementNS() necessary in Zotero 6; createElement() defaults to HTML in Zotero 7
6 | const NAMESPACE = {
7 | HTML: 'http://www.w3.org/1999/xhtml',
8 | XUL: 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul',
9 | }
10 |
11 | type Handler = (event?: any) => void | Promise
12 |
13 | export class Elements {
14 | static all = new Set>()
15 | // static all = new Set()
16 |
17 | static removeAll(): void {
18 | for (const eltRef of this.all) {
19 | try {
20 | // @ts-ignore
21 | eltRef.deref()?.remove()
22 | } catch (err) {}
23 | }
24 | this.all = new Set()
25 | }
26 |
27 | private className: string
28 | constructor(private document: Document) {
29 | this.className = `${config.addonRef}-auto-${Zotero.Utilities.generateObjectKey()}`
30 | }
31 |
32 | public serialize(node: HTMLElement): string {
33 | const s = new XMLSerializer()
34 | return s.serializeToString(node)
35 | }
36 |
37 | create(name: string, attrs: Record = {}): HTMLElement {
38 | const children: HTMLElement[] = (attrs.$ as unknown as HTMLElement[]) || []
39 | delete attrs.$
40 |
41 | const namespace = name.startsWith('html:') ? NAMESPACE.HTML : NAMESPACE.XUL
42 | // name = name.replace('html:', '')
43 | const tagName = name.startsWith('html:') ? name.replace('html:', '') : name
44 |
45 | // const elt: HTMLElement = this.document[namespace === NAMESPACE.XUL ? 'createXULElement' : 'createElement'](
46 | // name,
47 | // ) as HTMLElement
48 |
49 | // prettier-ignore
50 | // @ts-ignore - assume that createXULElement exists on document
51 | // eslint-disable-next-line @stylistic/max-len
52 | const elt: HTMLElement = namespace === NAMESPACE.HTML ? this.document.createElement(tagName) : this.document.createXULElement(tagName)
53 | let attrsclass = ''
54 | try {
55 | attrsclass = attrs.class as string
56 | } catch (err) {}
57 | attrs.class = `${this.className} ${attrsclass || ''}`.trim()
58 | for (const [a, v] of Object.entries(attrs)) {
59 | if (typeof v === 'string') {
60 | elt.setAttribute(a, v)
61 | } else if (typeof v === 'number') {
62 | elt.setAttribute(a, `${v}`)
63 | } else if (a.startsWith('on') && typeof v === 'function') {
64 | elt.addEventListener(a.replace('on', ''), (event) => {
65 | ;(v(event) as Promise)?.catch?.((err) => {
66 | throw err
67 | })
68 | })
69 | } else {
70 | throw new Error(`unexpected attribute ${a}`)
71 | }
72 | }
73 | for (const child of children) {
74 | elt.appendChild(child)
75 | }
76 |
77 | Elements.all.add(new WeakRef(elt))
78 |
79 | return elt
80 | }
81 |
82 | remove(): void {
83 | for (const elt of Array.from(this.document.getElementsByClassName(this.className))) {
84 | elt.remove()
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/modules/mdbcConstants.ts:
--------------------------------------------------------------------------------
1 | import type { DebugMode } from '../mdbcTypes'
2 |
3 | export const paramVals = {
4 | filefilterstrategy: ['default', 'customfileregexp'],
5 | matchstrategy: ['bbtcitekeyyaml', 'bbtcitekeyregexp', 'zotitemkey'],
6 | mdeditor: ['system', 'obsidian', 'logseq'],
7 | // obsidianresolvewithfile: [false, true],
8 | obsidianresolvespec: ['path', 'file'],
9 | grouplibraries: ['user', 'group'],
10 | removetags: ['keepsynced', 'addonly'],
11 | debugmode: ['minimal' satisfies DebugMode, 'maximal' satisfies DebugMode],
12 | } as const
13 |
14 | // Infer parameter types from values
15 | export type ParamVals = typeof paramVals
16 | export type ParamKey = keyof ParamVals
17 | export type ParamValue = ParamVals[T][number]
18 |
19 | // Re-export paramTypes interface if needed for backward compatibility
20 | // export type paramTypes = {
21 | // [K in Exclude]: ParamValue
22 | // }
23 |
--------------------------------------------------------------------------------
/src/modules/mdbcLogger.ts:
--------------------------------------------------------------------------------
1 | import { config } from '../../package.json'
2 | import { getPref } from '../utils/prefs'
3 |
4 | import type { DebugMode, LogType, messageData } from '../mdbcTypes'
5 |
6 | class LogsStore {
7 | static debug: DebugMode = getPref('debugmode') as DebugMode
8 | static time = {
9 | init: Date.now(),
10 | last: Date.now(),
11 | }
12 | static logs: Record = {}
13 | static data: Record = {}
14 | static messages: messageData[] = []
15 | }
16 |
17 | export class Logger {
18 | static dump() {
19 | return { logs: LogsStore.logs, data: LogsStore.data }
20 | }
21 |
22 | static getLogs() {
23 | return LogsStore.logs
24 | }
25 |
26 | static getMessages(): messageData[] {
27 | return LogsStore.messages
28 | }
29 |
30 | static clear(): void {
31 | LogsStore.logs = {}
32 | LogsStore.messages = []
33 | LogsStore.data = {}
34 | }
35 |
36 | static clearMessages(): void {
37 | LogsStore.messages = []
38 | }
39 |
40 | static mode() {
41 | return LogsStore.debug
42 | }
43 |
44 | static setDebugMode(mode: DebugMode) {
45 | LogsStore.debug = mode
46 | }
47 |
48 | private static updateTime() {
49 | const init = LogsStore.time.init
50 | const current = Date.now()
51 | // const last = LogsStore.time.last
52 | const delta = current - init
53 | LogsStore.time.last = current
54 | return delta
55 | }
56 |
57 | static addMessage(messageData: messageData) {
58 | LogsStore.messages.push(messageData)
59 | }
60 |
61 | static addData(key: string, valueIn: T, overwrite = true) {
62 | if (LogsStore.debug === 'minimal') {
63 | LogsStore.data[key] = 'not stored in minimal debugging mode'
64 | } else {
65 | const value: T = JSON.parse(JSON.stringify(valueIn))
66 | if (!(key in LogsStore.data) || LogsStore.data[key] === undefined) {
67 | LogsStore.data[key] = value
68 | } else {
69 | //// if property already exists ////
70 | if (overwrite) {
71 | delete LogsStore.data[key]
72 | LogsStore.data[key] = value
73 | } else if (Array.isArray(LogsStore.data[key])) {
74 | LogsStore.data[key].push(value)
75 | } else {
76 | LogsStore.data[key] = [LogsStore.data[key], value]
77 | }
78 | }
79 | }
80 | }
81 |
82 | static getData(key: string) {
83 | if (key in LogsStore.data) {
84 | return LogsStore.data[key]
85 | }
86 | }
87 |
88 | static addLog(key: string, value: any, overwrite = false) {
89 | const timedelta = this.updateTime()
90 | const timedvalue = { msg: value, td: timedelta }
91 | if (!(key in LogsStore.logs) || LogsStore.logs[key] === undefined) {
92 | LogsStore.logs[key] = timedvalue
93 | } else {
94 | //// if property already exists ////
95 | if (overwrite) {
96 | delete LogsStore.logs[key]
97 | LogsStore.logs[key] = timedvalue
98 | } else if (Array.isArray(LogsStore.logs[key])) {
99 | LogsStore.logs[key].push(timedvalue)
100 | } else {
101 | LogsStore.logs[key] = [LogsStore.logs[key], timedvalue]
102 | }
103 | }
104 | }
105 |
106 | static log(key: string, value: any, overwrite = false, type: LogType = 'info'): void {
107 | let success = false
108 | try {
109 | let toZoteroDebugConsole = false
110 | let toZoteroErrorConsole = false
111 | let toLogsStore = false
112 | if (LogsStore.debug === 'minimal') {
113 | switch (type) {
114 | case 'error':
115 | toZoteroDebugConsole = true
116 | toZoteroErrorConsole = true
117 | toLogsStore = true
118 | break
119 | case 'warn':
120 | toZoteroDebugConsole = true
121 | toZoteroErrorConsole = true
122 | break
123 | case 'info':
124 | break
125 | case 'debug':
126 | toZoteroDebugConsole = true
127 | toZoteroErrorConsole = true
128 | toLogsStore = true
129 | break
130 | case 'trace':
131 | break
132 | case 'config':
133 | break
134 | default:
135 | break
136 | }
137 | } else {
138 | switch (type) {
139 | case 'error':
140 | toZoteroDebugConsole = true
141 | toZoteroErrorConsole = true
142 | toLogsStore = true
143 | break
144 | case 'warn':
145 | toZoteroDebugConsole = true
146 | toZoteroErrorConsole = true
147 | toLogsStore = true
148 | break
149 | case 'info':
150 | toZoteroDebugConsole = true
151 | toZoteroErrorConsole = true
152 | toLogsStore = true
153 | break
154 | case 'debug':
155 | toZoteroDebugConsole = true
156 | toZoteroErrorConsole = true
157 | toLogsStore = true
158 | break
159 | case 'trace':
160 | toZoteroDebugConsole = true
161 | toZoteroErrorConsole = false
162 | toLogsStore = true
163 | break
164 | case 'config':
165 | if (!(type in LogsStore.logs)) {
166 | LogsStore.logs[type] = {} as Record
167 | }
168 | LogsStore.logs[type][key] = value
169 | break
170 | default:
171 | break
172 | }
173 | }
174 |
175 | if (toZoteroDebugConsole) Zotero.debug(`{${config.addonInstance}}[log][${type}] ${key} :: ${value}`)
176 | if (toZoteroErrorConsole) ztoolkit.log(`{${config.addonInstance}}[log][${type}] ${key}`, value)
177 | if (toLogsStore) this.addLog(key, value, overwrite)
178 |
179 | success = true
180 | } catch (err) {
181 | Zotero.debug(`{${config.addonInstance}}[log][ERROR] addDebugLog Error: ${getErrorMessage(err)}`)
182 | ztoolkit.log(`{${config.addonInstance}}[log][ERROR] addDebugLog Error`, err)
183 | }
184 | if (!success) {
185 | try {
186 | LogsStore.logs[key] = [LogsStore.logs[key], value]
187 | } catch (err) {
188 | Zotero.debug(`{${config.addonInstance}}[log][ERROR] addDebugLog-fallback Error: ${getErrorMessage(err)}`)
189 | ztoolkit.log(`{${config.addonInstance}}[log][ERROR] addDebugLog-fallback Error`, err)
190 | }
191 | }
192 | }
193 | }
194 |
195 | export function trace(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
196 | const original = descriptor.value
197 | const identifier = `${target.name}.${String(propertyKey)}`
198 | descriptor.value = function (...args: any) {
199 | try {
200 | Zotero.debug(`{${config.addonInstance}}[call] : ${identifier}`)
201 | if (LogsStore.debug === 'maximal') {
202 | Logger.log('trace', identifier, false, 'trace')
203 | }
204 | return original.apply(this, args)
205 | } catch (err) {
206 | ztoolkit.log(`{${config.addonInstance}}[call][ERROR] : SOME ERROR`)
207 | Zotero.debug(
208 | `{${config.addonInstance}}[call][ERROR] : ${target.name}.${String(propertyKey)} :: ${getErrorMessage(err)}`,
209 | )
210 | ztoolkit.log(`{${config.addonInstance}}[call][ERROR] : ${target.name}.${String(propertyKey)}`, err)
211 | Logger.log('trace', `ERROR : ${identifier} :: ${getErrorMessage(err)}`, false, 'error')
212 | throw err
213 | }
214 | }
215 | return descriptor
216 | }
217 |
218 | export function getErrorMessage(err: unknown): string {
219 | if (err instanceof Error) {
220 | return err.message
221 | }
222 | return String(err)
223 | }
224 |
--------------------------------------------------------------------------------
/src/modules/mdbcParam.ts:
--------------------------------------------------------------------------------
1 | import { config } from '../../package.json'
2 | import { getPref, setPref } from '../utils/prefs'
3 |
4 | import { paramVals } from './mdbcConstants'
5 | import { getErrorMessage, Logger, trace } from './mdbcLogger'
6 | import { Notifier, prefHelpers } from './mdbcUX'
7 |
8 | import type { ParamKey, ParamValue } from './mdbcConstants'
9 |
10 | export class getParam {
11 | static sourcedir() {
12 | ///TYPE: path
13 | const name = 'sourcedir'
14 | const valueDefault = ''
15 | let valid = false
16 | let msg: string[] = []
17 |
18 | const param = { name, value: valueDefault, valid, msg: '' }
19 |
20 | try {
21 | const valueRaw = getPref(name)
22 | msg.push(`pref value: >>${valueRaw}<<.`)
23 |
24 | if (valueRaw === undefined || typeof valueRaw !== 'string') {
25 | msg.push(`type: ${typeof valueRaw}.`)
26 | throw new Error('Vault Path Not Found')
27 | }
28 | if (valueRaw.length === 0) {
29 | msg.push('length: 0.')
30 | throw new Error('Vault Path Not Found')
31 | }
32 |
33 | const zfileSourcedir = Zotero.File.pathToFile(valueRaw)
34 | if (!zfileSourcedir.exists() || !zfileSourcedir.isDirectory()) {
35 | msg.push('Invalid path.')
36 | throw new Error('Vault Path Valid')
37 | }
38 | zfileSourcedir.normalize()
39 | const sourcedirpath = zfileSourcedir.path
40 |
41 | if (sourcedirpath && typeof sourcedirpath === 'string' && sourcedirpath.length > 0) {
42 | valid = true
43 | } else {
44 | msg.push(`sourcedirpath: ${sourcedirpath}.`)
45 | msg.push(`sourcedirpathObj.exists(): ${zfileSourcedir.exists()}.`)
46 | msg.push(`sourcedirpathObj.isDirectory(): ${zfileSourcedir.isDirectory()}.`)
47 | }
48 |
49 | // const value = valid ? sourcedirpath : valueDefault
50 | param.valid = valid
51 | param.value = valid ? sourcedirpath : valueDefault
52 | } catch (err) {
53 | Logger.log('getParam', `ERROR: sourcedirpath :: ${getErrorMessage(err)}`, false, 'error')
54 | Notifier.notify({
55 | title: 'Warning',
56 | body: `Vault Path Not Found. Set the path to your notes in the ${config.addonName} preferences.`,
57 | type: 'error',
58 | })
59 | msg.push(`Error:: ${getErrorMessage(err)}.`)
60 | }
61 |
62 | param.msg = msg.join(' ')
63 | Logger.log(name, param, false, 'config')
64 | return param
65 | }
66 |
67 | @trace
68 | static filefilterstrategy() {
69 | ///TYPE: enum
70 | const name: ParamKey = 'filefilterstrategy'
71 | const valueDefault = paramVals[name][0]
72 | const valid = true
73 |
74 | const valueRaw = getPref(name)
75 |
76 | const valueVerified = paramVals[name].find((validName) => validName === valueRaw)
77 | const value: ParamValue<'filefilterstrategy'> = valueVerified ? valueVerified : valueDefault
78 | const param = { name, value, valid }
79 |
80 | if (valueVerified) {
81 | } else {
82 | Logger.log('getParam', `ERROR: ${name}: invalid value :: ${valueRaw}`, false, 'error')
83 | Logger.log('getParam', `${name}: set to default :: ${valueDefault}`, false, 'error')
84 | setPref(name, valueDefault)
85 | }
86 |
87 | Logger.log(name, param, false, 'config')
88 | return param
89 | }
90 |
91 | @trace
92 | static filepattern() {
93 | ///TYPE: regex
94 | const name = 'filepattern'
95 | const valueDefault = '^@(\\S+).*\\.md$'
96 | const valid = true
97 | let msg = ''
98 | const flags = 'i'
99 |
100 | const valueRaw = getPref(name)
101 | msg += `pref value: >>${valueRaw}<<. `
102 |
103 | const verified = typeof valueRaw === 'string' && valueRaw.length > 0 && prefHelpers.isValidRegExp(valueRaw)
104 | const valueVerified = verified ? valueRaw : valueDefault
105 | const value = new RegExp(valueVerified, flags)
106 | const param = { name, value, valid, msg }
107 |
108 | if (verified) {
109 | } else {
110 | if (valueRaw !== '' && valueRaw !== valueDefault) {
111 | Logger.log('getParam', `ERROR: ${name}: invalid RegExp :: ${valueRaw}. Using default instead.`, false, 'error')
112 | Logger.log('getParam', `${name}: set to default :: ${valueDefault}`, false, 'error')
113 | setPref(name, valueDefault)
114 | }
115 | }
116 |
117 | Logger.log(name, { ...param, value: param.value.toString() }, false, 'config')
118 | return param
119 | }
120 |
121 | @trace
122 | static matchstrategy() {
123 | ///TYPE: enum
124 | const name: ParamKey = 'matchstrategy'
125 | const valueDefault = paramVals[name][0]
126 | const valid = true
127 |
128 | const valueRaw = getPref(name)
129 |
130 | const valueVerified = paramVals[name].find((validName) => validName === valueRaw)
131 | const value: ParamValue<'matchstrategy'> = valueVerified ? valueVerified : valueDefault
132 | const param = { name, value, valid }
133 |
134 | if (valueVerified) {
135 | } else {
136 | Logger.log('getParam', `ERROR: ${name}: invalid value :: ${valueRaw}`, false, 'error')
137 | Logger.log('getParam', `${name}: set to default :: ${valueDefault}`, false, 'error')
138 | setPref(name, valueDefault)
139 | }
140 |
141 | Logger.log(name, param, false, 'config')
142 | return param
143 | }
144 |
145 | @trace
146 | static bbtyamlkeyword() {
147 | ///TYPE: string
148 | const name = 'bbtyamlkeyword'
149 | const valueDefault = ''
150 | let verified = false
151 | let valid = false
152 | let msg: string[] = []
153 | let value = valueDefault
154 |
155 | const valueRaw = getPref(name)
156 | msg.push(`pref value: >>${valueRaw}<<.`)
157 |
158 | /// NB checkMetadataFormat() will show a notification
159 | if (valueRaw && typeof valueRaw === 'string' && valueRaw.length > 0 && prefHelpers.checkMetadataFormat(valueRaw)) {
160 | verified = true
161 | valid = true
162 | value = valueRaw
163 | }
164 |
165 | if (verified) {
166 | msg.push('value passed verification checks.')
167 | } else {
168 | msg.push('value FAILED verification checks.')
169 | if (valueRaw !== '' && valueRaw !== valueDefault) {
170 | msg.push('pref overwritten with default.')
171 | Logger.log(
172 | 'getParam',
173 | `ERROR: ${name}: Invalid value :: >>${valueRaw}<<. Using default >>${valueDefault}<< instead.`,
174 | false,
175 | 'error',
176 | )
177 | Logger.log('getParam', `${name}: set to default :: >>${valueDefault}<<`, false, 'error')
178 | setPref(name, valueDefault)
179 | }
180 | }
181 | const param = { name, value, valid, msg }
182 |
183 | Logger.log(name, param, false, 'config')
184 |
185 | return param
186 | }
187 |
188 | @trace
189 | static bbtregexp() {
190 | ///TYPE: regex
191 | const name = 'bbtregexp'
192 | const valueDefault = ''
193 | let valid = false
194 | let msg = ''
195 | const flags = 'm'
196 |
197 | const valueRaw = getPref(name)
198 | msg += `pref value: >>${valueRaw}<<. `
199 |
200 | const verified = typeof valueRaw === 'string' && valueRaw.length > 0 && prefHelpers.isValidRegExp(valueRaw)
201 | if (verified) {
202 | valid = true
203 | msg += 'valid RegExp. '
204 | }
205 | const valueVerified = verified ? valueRaw : valueDefault
206 | const value = new RegExp(valueVerified, flags)
207 | const param = { name, value, valid, msg }
208 |
209 | if (verified) {
210 | } else {
211 | if (valueRaw !== '' && valueRaw !== valueDefault) {
212 | Logger.log('getParam', `ERROR: ${name}: invalid RegExp :: ${valueRaw}. Using default instead.`, false, 'error')
213 | Logger.log('getParam', `${name}: set to default :: ${valueDefault}`, false, 'error')
214 | setPref(name, valueDefault)
215 | }
216 | }
217 |
218 | Logger.log(name, { ...param, value: param.value.toString() }, false, 'config')
219 | return param
220 | }
221 |
222 | @trace
223 | static zotkeyregexp() {
224 | ///TYPE: regex
225 | const name = 'zotkeyregexp'
226 | const valueDefault = ''
227 | let valid = false
228 | let msg = ''
229 | const flags = ''
230 |
231 | const valueRaw = getPref(name)
232 | msg += `pref value: >>${valueRaw}<<. `
233 |
234 | const verified = typeof valueRaw === 'string' && valueRaw.length > 0 && prefHelpers.isValidRegExp(valueRaw)
235 | if (verified) {
236 | valid = true
237 | msg += 'valid RegExp. '
238 | }
239 | const valueVerified = verified ? valueRaw : valueDefault
240 | const value = new RegExp(valueVerified, flags)
241 | const param = { name, value, valid, msg }
242 |
243 | if (verified) {
244 | } else {
245 | if (valueRaw !== '' && valueRaw !== valueDefault) {
246 | Logger.log('getParam', `ERROR: ${name}: invalid RegExp :: ${valueRaw}. Using default instead.`, false, 'error')
247 | Logger.log('getParam', `${name}: set to default :: ${valueDefault}`, false, 'error')
248 | setPref(name, valueDefault)
249 | }
250 | }
251 |
252 | Logger.log(name, { ...param, value: param.value.toString() }, false, 'config')
253 | return param
254 | }
255 |
256 | @trace
257 | static mdeditor() {
258 | ///TYPE: enum
259 | const name: ParamKey = 'mdeditor'
260 | const valueDefault = paramVals[name][0]
261 | let valid = true
262 |
263 | const valueRaw = getPref(name)
264 |
265 | const valueVerified = paramVals[name].find((validName) => validName === valueRaw)
266 | const value: ParamValue<'mdeditor'> = valueVerified ? valueVerified : valueDefault
267 | const param = { name, value, valid }
268 |
269 | if (valueVerified) {
270 | } else {
271 | Logger.log('getParam', `ERROR: ${name}: invalid value :: ${valueRaw}`, false, 'error')
272 | Logger.log('getParam', `${name}: set to default :: ${valueDefault}`, false, 'error')
273 | setPref(name, valueDefault)
274 | }
275 |
276 | Logger.log(name, param, false, 'config')
277 | return param
278 | }
279 |
280 | @trace
281 | static obsidianresolve() {
282 | ///TYPE: enum
283 | const name: ParamKey = 'obsidianresolvespec'
284 | const valueDefault = paramVals[name][0]
285 | let valid = true
286 |
287 | const valueRaw = getPref(name)
288 |
289 | const valueVerified = paramVals[name].find((validName) => validName === valueRaw)
290 | const value: ParamValue<'obsidianresolvespec'> = valueVerified ? valueVerified : valueDefault
291 | const param = { name, value, valid }
292 |
293 | if (valueVerified) {
294 | } else {
295 | Logger.log('getParam', `ERROR: ${name}: invalid value :: ${valueRaw}`, false, 'error')
296 | Logger.log('getParam', `${name}: set to default :: ${valueDefault}`, false, 'error')
297 | setPref(name, valueDefault)
298 | }
299 |
300 | Logger.log(name, param, false, 'config')
301 | return param
302 | }
303 |
304 | @trace
305 | static obsidianvaultname() {
306 | ///TYPE: string
307 | const name = 'obsidianvaultname'
308 | const valueDefault = ''
309 | let value = valueDefault
310 | let verified = false
311 | let valid = false
312 | let msg: string[] = []
313 |
314 | const valueRaw = getPref(name)
315 | msg.push(`pref value: >>${valueRaw}<<.`)
316 |
317 | /// NB checkMetadataFormat() will show a notification
318 | if (valueRaw && typeof valueRaw === 'string' && valueRaw.length > 0 && prefHelpers.checkMetadataFormat(valueRaw)) {
319 | verified = true
320 | valid = true
321 | value = valueRaw
322 | }
323 |
324 | if (verified) {
325 | msg.push('value passed verification checks.')
326 | } else {
327 | msg.push('value FAILED verification checks.')
328 | if (valueRaw !== '' && valueRaw !== valueDefault) {
329 | msg.push('pref overwritten with default.')
330 | Logger.log(
331 | 'getParam',
332 | `ERROR: ${name}: invalid value :: >>${valueRaw}<<. Using default >>${valueDefault}<< instead.`,
333 | false,
334 | 'error',
335 | )
336 | Logger.log('getParam', `${name}: set to default :: >>${valueDefault}<<`, false, 'error')
337 | setPref(name, valueDefault)
338 | }
339 | }
340 |
341 | const param = { name, value, valid, msg: msg.join(' ') }
342 | Logger.log(name, param, false, 'config')
343 | return param
344 | }
345 |
346 | @trace
347 | static logseqgraph() {
348 | ///TYPE: string
349 | const name = 'logseqgraph'
350 | const valueDefault = ''
351 | let value = valueDefault
352 | let verified = false
353 | let valid = false
354 | let msg: string[] = []
355 |
356 | const valueRaw = getPref(name)
357 | msg.push(`pref value: >>${valueRaw}<<.`)
358 |
359 | /// NB checkMetadataFormat() will show a notification
360 | if (valueRaw && typeof valueRaw === 'string' && valueRaw.length > 0 && prefHelpers.checkMetadataFormat(valueRaw)) {
361 | verified = true
362 | valid = true
363 | value = valueRaw
364 | }
365 |
366 | if (verified) {
367 | msg.push('value passed verification checks.')
368 | } else {
369 | msg.push('value FAILED verification checks.')
370 | if (valueRaw !== '' && valueRaw !== valueDefault) {
371 | msg.push('pref overwritten with default.')
372 | Logger.log(
373 | 'getParam',
374 | `ERROR: ${name}: invalid value :: >>${valueRaw}<<. Using default >>${valueDefault}<< instead.`,
375 | false,
376 | 'error',
377 | )
378 | Logger.log('getParam', `${name}: set to default :: >>${valueDefault}<<`, false, 'error')
379 | setPref(name, valueDefault)
380 | }
381 | }
382 |
383 | const param = { name, value, valid, msg: msg.join(' ') }
384 | Logger.log(name, param, false, 'config')
385 | return param
386 | }
387 |
388 | @trace
389 | static logseqprefix() {
390 | ///TYPE: string
391 | const name = 'logseqprefix'
392 | const valueDefault = ''
393 | let value = valueDefault
394 | let verified = false
395 | let valid = false
396 | let msg: string[] = []
397 |
398 | const valueRaw = getPref(name)
399 | msg.push(`pref value: >>${valueRaw}<<.`)
400 |
401 | /// NB checkMetadataFormat() will show a notification
402 | if (valueRaw && typeof valueRaw === 'string' && valueRaw.length > 0 && prefHelpers.checkMetadataFormat(valueRaw)) {
403 | verified = true
404 | valid = true
405 | value = valueRaw
406 | }
407 |
408 | if (verified) {
409 | msg.push('value passed verification checks.')
410 | } else {
411 | msg.push('value FAILED verification checks.')
412 | if (valueRaw !== '' && valueRaw !== valueDefault) {
413 | msg.push('pref overwritten with default.')
414 | Logger.log(
415 | 'getParam',
416 | `ERROR: ${name}: invalid value :: >>${valueRaw}<<. Using default >>${valueDefault}<< instead.`,
417 | false,
418 | 'error',
419 | )
420 | Logger.log('getParam', `${name}: set to default :: >>${valueDefault}<<`, false, 'error')
421 | setPref(name, valueDefault)
422 | }
423 | }
424 |
425 | const param = { name, value, valid, msg: msg.join(' ') }
426 | Logger.log(name, param, false, 'config')
427 | return param
428 | }
429 |
430 | @trace
431 | static grouplibraries() {
432 | ///TYPE: enum
433 | const name: ParamKey = 'grouplibraries'
434 | const valueDefault = paramVals[name][0]
435 | let valid = true
436 |
437 | const valueRaw = getPref(name)
438 |
439 | const valueVerified = paramVals[name].find((validName) => validName === valueRaw)
440 | const value: ParamValue<'grouplibraries'> = valueVerified ? valueVerified : valueDefault
441 | const param = { name, value, valid }
442 |
443 | if (valueVerified) {
444 | } else {
445 | Logger.log('getParam', `ERROR: ${name}: invalid value :: ${valueRaw}`, false, 'error')
446 | Logger.log('getParam', `${name}: set to default :: ${valueDefault}`, false, 'error')
447 | setPref(name, valueDefault)
448 | }
449 |
450 | Logger.log(name, param, false, 'config')
451 | return param
452 | }
453 |
454 | @trace
455 | static removetags() {
456 | ///TYPE: enum
457 | const name: ParamKey = 'removetags'
458 | const valueDefault = paramVals[name][0]
459 | let valid = true
460 |
461 | const valueRaw = getPref(name)
462 |
463 | const valueVerified = paramVals[name].find((validName) => validName === valueRaw)
464 | const value: ParamValue<'removetags'> = valueVerified ? valueVerified : valueDefault
465 | const param = { name, value, valid }
466 |
467 | if (valueVerified) {
468 | } else {
469 | Logger.log('getParam', `ERROR: ${name}: invalid value :: ${valueRaw}`, false, 'error')
470 | Logger.log('getParam', `${name}: set to default :: ${valueDefault}`, false, 'error')
471 | setPref(name, valueDefault)
472 | }
473 |
474 | Logger.log(name, param, false, 'config')
475 | return param
476 | }
477 |
478 | @trace
479 | static tagstr() {
480 | ///TYPE: string
481 | const name = 'tagstr'
482 | const valueDefault = 'ObsCite'
483 | let value = valueDefault
484 | let verified = false
485 | let valid = true
486 | let msg: string[] = []
487 |
488 | const valueRaw = getPref(name)
489 | msg.push(`pref value: >>${valueRaw}<<.`)
490 |
491 | /// NB checkMetadataFormat() will show a notification
492 | if (valueRaw && typeof valueRaw === 'string' && valueRaw.length > 0 && prefHelpers.checkMetadataFormat(valueRaw)) {
493 | verified = true
494 | valid = true
495 | value = valueRaw
496 | }
497 |
498 | if (verified) {
499 | msg.push('value passed verification checks.')
500 | } else {
501 | msg.push('value FAILED verification checks.')
502 | if (valueRaw !== '' && valueRaw !== valueDefault) {
503 | msg.push('pref overwritten with default.')
504 | Logger.log(
505 | 'getParam',
506 | `ERROR: ${name}: invalid value :: >>${valueRaw}<<. Using default >>${valueDefault}<< instead.`,
507 | false,
508 | 'error',
509 | )
510 | Logger.log('getParam', `${name}: set to default :: >>${valueDefault}<<`, false, 'error')
511 | setPref(name, valueDefault)
512 | }
513 | }
514 |
515 | const param = { name, value, valid, msg: msg.join(' ') }
516 | Logger.log(name, param, false, 'config')
517 | return param
518 | }
519 |
520 | @trace
521 | static debugmode() {
522 | ///TYPE: enum
523 | const name: ParamKey = 'debugmode'
524 | const valueDefault = paramVals[name][0]
525 | let valid = true
526 |
527 | const valueRaw = getPref(name)
528 |
529 | const valueVerified = paramVals[name].find((validName) => validName === valueRaw)
530 | const value: ParamValue<'debugmode'> = valueVerified ? valueVerified : valueDefault
531 | const param = { name, value, valid }
532 |
533 | if (valueVerified) {
534 | } else {
535 | Logger.log('getParam', `ERROR: ${name}: invalid value :: ${valueRaw}`, false, 'error')
536 | Logger.log('getParam', `${name}: set to default :: ${valueDefault}`, false, 'error')
537 | setPref(name, valueDefault)
538 | }
539 |
540 | Logger.log(name, param, false, 'config')
541 | return param
542 | }
543 | }
544 |
--------------------------------------------------------------------------------
/src/modules/mdbcStartupHelpers.ts:
--------------------------------------------------------------------------------
1 | import { config, version } from '../../package.json'
2 | import { getPref, setPref } from '../utils/prefs'
3 |
4 | import { paramVals } from './mdbcConstants'
5 | import { getErrorMessage, Logger, trace } from './mdbcLogger'
6 | import { getParam } from './mdbcParam'
7 | import { Notifier } from './mdbcUX'
8 |
9 | export class wrappers {
10 | @trace
11 | static async fetchAndParseJsonFromGitHub(): Promise {
12 | const url = config.updateJSON
13 | let status: 'match' | 'mismatch' | 'error' = 'error'
14 | try {
15 | // Fetch data from the GitHub repository
16 | const response = await Zotero.HTTP.request('GET', url, {})
17 |
18 | // Check if the response status is 200 (OK)
19 | if (response.status !== 200) {
20 | throw new Error(`Failed to fetch data: Status code ${response.status}`)
21 | }
22 |
23 | // Parse JSON data
24 | try {
25 | const jsonData = JSON.parse(response.responseText)
26 | const addonIds = Object.keys(jsonData.addons)
27 | status = config.addonID === 'dev@daeh.info' && addonIds.includes('daeda@mit.edu') ? 'mismatch' : 'match'
28 | // status = addonIds.includes(config.addonID) ? 'match' : 'mismatch'
29 | Logger.log('fetchAndParseJsonFromGitHub', `JSON data: ${JSON.stringify(Object.keys(jsonData))}`, false, 'debug')
30 | } catch (jsonError) {
31 | throw new Error('Failed to parse JSON data')
32 | }
33 | } catch (error) {
34 | // Handle network errors or other issues
35 | ///TEMP
36 | let message: string
37 | try {
38 | // @ts-ignore
39 | message = error.message
40 | } catch (err) {
41 | message = 'error'
42 | }
43 | Logger.log('fetchAndParseJsonFromGitHub', `Error fetching JSON data: ${message}`, false, 'error')
44 | throw error // Re-throw the error if you want to handle it outside this function
45 | }
46 | return status
47 | }
48 |
49 | @trace
50 | static findPreviousVersion() {
51 | const version_re =
52 | /^(?\d+)\.(?\d+)\.(?\d+)(?[-+]?[0-9A-Za-z]+\.?[0-9A-Za-z]*[-+]?[0-9A-Za-z]*)?$/
53 |
54 | const configurationVersionThis = {
55 | major: 0,
56 | minor: 0,
57 | patch: 0,
58 | release: '',
59 | str: version,
60 | }
61 | const versionThis_rematch = version_re.exec(version)
62 | if (versionThis_rematch?.groups) {
63 | configurationVersionThis.major = parseInt(versionThis_rematch.groups.major)
64 | configurationVersionThis.minor = parseInt(versionThis_rematch.groups.minor)
65 | configurationVersionThis.patch = parseInt(versionThis_rematch.groups.patch)
66 | configurationVersionThis.release = versionThis_rematch.groups.release ? versionThis_rematch.groups.release : ''
67 | }
68 |
69 | let configurationVersionPreviousStr: any = ''
70 | let configurationVersionPrevious = {
71 | major: 0,
72 | minor: 0,
73 | patch: 0,
74 | release: '',
75 | str: '',
76 | }
77 | try {
78 | configurationVersionPreviousStr = getPref('configuration')
79 | if (typeof configurationVersionPreviousStr === 'string') {
80 | configurationVersionPrevious.str = configurationVersionPreviousStr
81 | }
82 | if (typeof configurationVersionPreviousStr === 'string' && version_re.test(configurationVersionPreviousStr)) {
83 | const version_rematch = version_re.exec(configurationVersionPreviousStr)
84 | if (version_rematch?.groups) {
85 | configurationVersionPrevious.major = parseInt(version_rematch.groups.major)
86 | configurationVersionPrevious.minor = parseInt(version_rematch.groups.minor)
87 | configurationVersionPrevious.patch = parseInt(version_rematch.groups.patch)
88 | configurationVersionPrevious.release = version_rematch.groups.release ? version_rematch.groups.release : ''
89 | }
90 | }
91 | } catch (err) {}
92 |
93 | return {
94 | app: configurationVersionThis,
95 | config: configurationVersionPrevious,
96 | }
97 | }
98 |
99 | @trace
100 | static async startupVersionCheck() {
101 | const versionParse = this.findPreviousVersion()
102 |
103 | // Logger.log('startupVersionCheck - versionParse.app', versionParse.app, false, 'debug')
104 | // Logger.log('startupVersionCheck - configurationVersionPrevious', versionParse.config, false, 'debug')
105 |
106 | if (versionParse.config.str !== versionParse.app.str) {
107 | let prezot7 = versionParse.config.major === 0 && versionParse.config.minor < 1
108 | let preprerename1 =
109 | versionParse.config.major === 0 &&
110 | versionParse.config.minor === 1 &&
111 | versionParse.config.patch < 1 &&
112 | !['-rc.1'].includes(versionParse.config.release)
113 |
114 | if (!preprerename1) {
115 | let test0 = getPref('sourcedir')
116 | // Logger.log('startupVersionCheck - preprerename1 - test0', test0, false, 'debug')
117 | if (typeof test0 !== 'string' || test0 === '') {
118 | // @ts-ignore old pref key
119 | let test1 = getPref('source_dir') // preference key prior to v...
120 | // Logger.log('startupVersionCheck - preprerename1 - test1', test1, false, 'debug')
121 | if (test1 && typeof test1 === 'string' && test1.length > 0) {
122 | // Logger.log('startupVersionCheck - preprerename1 - AMHERE0', test1, false, 'debug')
123 | preprerename1 = true
124 | }
125 | }
126 | }
127 | if (!preprerename1 && !prezot7) {
128 | let test0 = getPref('sourcedir')
129 | if (typeof test0 !== 'string' || test0 === '') {
130 | let test1 = Zotero.Prefs.get('extensions.mdbconnect.source_dir', true)
131 | if (test1 && typeof test1 === 'string' && test1.length > 0) {
132 | prezot7 = true
133 | }
134 | }
135 | }
136 |
137 | // Logger.log('startupVersionCheck - preprerename1', preprerename1, false, 'debug')
138 |
139 | // Logger.log('startupVersionCheck - prezot7', prezot7, false, 'debug')
140 |
141 | /// sourcedir
142 | try {
143 | if (preprerename1) {
144 | // @ts-ignore old pref key
145 | const val = getPref('source_dir') // preference key prior to v...
146 | // Logger.log('startupVersionCheck - sourcedir - val', val, false, 'debug')
147 | if (val && typeof val === 'string' && val.length > 0) {
148 | setPref('sourcedir', val)
149 | getParam.sourcedir()
150 | }
151 | } else if (prezot7) {
152 | const val = Zotero.Prefs.get('extensions.mdbconnect.source_dir', true) // as string
153 | // Logger.log('startupVersionCheck - sourcedir - val2', val, false, 'debug')
154 | if (val && typeof val === 'string' && val.length > 0) {
155 | // Logger.log('startupVersionCheck - sourcedir - AMHERE2', val, false, 'debug')
156 | setPref('sourcedir', val)
157 | getParam.sourcedir()
158 | }
159 | }
160 | } catch (err) {
161 | Logger.log('startupDependencyCheck', `sourcedir ERROR: ${getErrorMessage(err)}`, false, 'error')
162 | }
163 |
164 | /// filefilterstrategy
165 | try {
166 | if (preprerename1) {
167 | const val = getPref('filefilterstrategy') // as string
168 | if (val === 'customfileregex') {
169 | setPref('filefilterstrategy', 'customfileregexp')
170 | // } else if (paramVals.filefilterstrategy.includes(val as paramTypes['filefilterstrategy'])) {
171 | } else if (
172 | val &&
173 | typeof val === 'string' &&
174 | paramVals.filefilterstrategy.find((validName) => validName === val)
175 | ) {
176 | setPref('filefilterstrategy', val)
177 | } else {
178 | setPref('filefilterstrategy', paramVals.filefilterstrategy[0])
179 | }
180 | getParam.filefilterstrategy()
181 | } else if (prezot7) {
182 | const val = Zotero.Prefs.get('extensions.mdbconnect.filefilterstrategy', true) // as string
183 | if (val === 'customfileregex') {
184 | setPref('filefilterstrategy', 'customfileregexp')
185 | } else if (
186 | val &&
187 | typeof val === 'string' &&
188 | paramVals.filefilterstrategy.find((validName) => validName === val)
189 | ) {
190 | setPref('filefilterstrategy', val)
191 | } else {
192 | setPref('filefilterstrategy', paramVals.filefilterstrategy[0])
193 | }
194 | getParam.filefilterstrategy()
195 | }
196 | } catch (err) {
197 | Logger.log('startupDependencyCheck', `filefilterstrategy ERROR: ${getErrorMessage(err)}`, false, 'error')
198 | }
199 |
200 | /// filepattern
201 | try {
202 | if (preprerename1) {
203 | const val = getPref('filepattern') //as string
204 | if (val && typeof val === 'string') setPref('filepattern', val)
205 | getParam.filepattern()
206 | } else if (prezot7) {
207 | const val = Zotero.Prefs.get('extensions.mdbconnect.filepattern', true) //as string
208 | if (val && typeof val === 'string') setPref('filepattern', val)
209 | getParam.filepattern()
210 | }
211 | } catch (err) {
212 | Logger.log('startupDependencyCheck', `filepattern ERROR: ${getErrorMessage(err)}`, false, 'error')
213 | }
214 |
215 | /// matchstrategy
216 | try {
217 | if (preprerename1) {
218 | const val = getPref('matchstrategy') // as string
219 | if (val === 'bbtcitekey') {
220 | setPref('matchstrategy', 'bbtcitekeyyaml')
221 | } else if (val && typeof val === 'string' && paramVals.matchstrategy.find((validName) => validName === val)) {
222 | setPref('matchstrategy', val)
223 | } else {
224 | setPref('matchstrategy', paramVals.matchstrategy[0])
225 | }
226 | getParam.matchstrategy()
227 | } else if (prezot7) {
228 | const val = Zotero.Prefs.get('extensions.mdbconnect.matchstrategy', true) // as string
229 | if (val === 'bbtcitekey') {
230 | setPref('matchstrategy', 'bbtcitekeyyaml')
231 | } else if (val && typeof val === 'string' && paramVals.matchstrategy.find((validName) => validName === val)) {
232 | setPref('matchstrategy', val)
233 | } else {
234 | setPref('matchstrategy', paramVals.matchstrategy[0])
235 | }
236 | getParam.matchstrategy()
237 | }
238 | } catch (err) {
239 | Logger.log('startupDependencyCheck', `matchstrategy ERROR: ${getErrorMessage(err)}`, false, 'error')
240 | }
241 |
242 | /// bbtyamlkeyword
243 | try {
244 | if (preprerename1) {
245 | // @ts-ignore old pref key
246 | const val = getPref('metadatakeyword') // preference key prior to v...
247 | if (val && typeof val === 'string') {
248 | setPref('bbtyamlkeyword', val)
249 | }
250 | getParam.bbtyamlkeyword()
251 | } else if (prezot7) {
252 | const val = Zotero.Prefs.get('extensions.mdbconnect.metadatakeyword', true) // as string
253 | if (val && typeof val === 'string') {
254 | setPref('bbtyamlkeyword', val)
255 | }
256 | getParam.bbtyamlkeyword()
257 | }
258 | } catch (err) {
259 | Logger.log('startupDependencyCheck', `bbtyamlkeyword ERROR: ${getErrorMessage(err)}`, false, 'error')
260 | }
261 |
262 | /// zotkeyregexp
263 | try {
264 | if (preprerename1) {
265 | // @ts-ignore old pref key
266 | const val = getPref('zotkeyregex') // preference key prior to v...
267 | if (val && typeof val === 'string') {
268 | setPref('zotkeyregexp', val)
269 | }
270 | getParam.zotkeyregexp()
271 | } else if (prezot7) {
272 | const val = Zotero.Prefs.get('extensions.mdbconnect.zotkeyregex', true) // as string
273 | if (val && typeof val === 'string') {
274 | setPref('zotkeyregexp', val)
275 | }
276 | getParam.zotkeyregexp()
277 | }
278 | } catch (err) {
279 | Logger.log('startupDependencyCheck', `zotkeyregexp ERROR: ${getErrorMessage(err)}`, false, 'error')
280 | }
281 |
282 | /// mdeditor
283 | try {
284 | if (preprerename1) {
285 | // @ts-ignore old pref key
286 | const val = getPref('vaultresolution') // preference key prior to v...
287 | if (val === 'path') {
288 | setPref('mdeditor', 'obsidian')
289 | setPref('obsidianresolvespec', 'path')
290 | } else if (val === 'file') {
291 | setPref('mdeditor', 'obsidian')
292 | setPref('obsidianresolvespec', 'file')
293 | getParam.obsidianresolve()
294 | } else if (val === 'logseq') {
295 | setPref('mdeditor', 'logseq')
296 | } else if (val === 'default') {
297 | setPref('mdeditor', 'system')
298 | } else {
299 | setPref('mdeditor', 'system')
300 | }
301 | getParam.mdeditor()
302 | } else if (prezot7) {
303 | const val = Zotero.Prefs.get('extensions.mdbconnect.vaultresolution', true) // as string
304 | if (val === 'path') {
305 | setPref('mdeditor', 'obsidian')
306 | setPref('obsidianresolvespec', 'path')
307 | } else if (val === 'file') {
308 | setPref('mdeditor', 'obsidian')
309 | setPref('obsidianresolvespec', 'file')
310 | getParam.obsidianresolve()
311 | } else if (val === 'logseq') {
312 | setPref('mdeditor', 'logseq')
313 | } else if (val === 'default') {
314 | setPref('mdeditor', 'system')
315 | } else {
316 | setPref('mdeditor', 'system')
317 | }
318 | getParam.mdeditor()
319 | }
320 | } catch (err) {
321 | Logger.log('startupDependencyCheck', `mdeditor ERROR: ${getErrorMessage(err)}`, false, 'error')
322 | }
323 |
324 | /// obsidianvaultname
325 | try {
326 | if (preprerename1) {
327 | // @ts-ignore old pref key
328 | const val = getPref('vaultname') // preference key prior to v... // && typeof val === 'string' as string
329 | if (val && typeof val === 'string') {
330 | setPref('obsidianvaultname', val)
331 | }
332 | getParam.obsidianvaultname()
333 | } else if (prezot7) {
334 | const val = Zotero.Prefs.get('extensions.mdbconnect.vaultname', true) // as string
335 | if (val && typeof val === 'string') {
336 | setPref('obsidianvaultname', val)
337 | }
338 | getParam.obsidianvaultname()
339 | }
340 | } catch (err) {
341 | Logger.log('startupDependencyCheck', `obsidianvaultname ERROR: ${getErrorMessage(err)}`, false, 'error')
342 | }
343 |
344 | /// logseqgraph
345 | try {
346 | if (preprerename1) {
347 | const val = getPref('logseqgraph') // as string
348 | if (val && typeof val === 'string') {
349 | setPref('logseqgraph', val)
350 | }
351 | getParam.logseqgraph()
352 | } else if (prezot7) {
353 | const val = Zotero.Prefs.get('extensions.mdbconnect.logseqgraph', true) // as string
354 | if (val && typeof val === 'string') {
355 | setPref('logseqgraph', val)
356 | }
357 | getParam.logseqgraph()
358 | }
359 | } catch (err) {
360 | Logger.log('startupDependencyCheck', `logseqgraph ERROR: ${getErrorMessage(err)}`, false, 'error')
361 | }
362 |
363 | /// grouplibraries
364 | try {
365 | if (preprerename1) {
366 | const val = getPref('grouplibraries') // as string
367 | if (val && typeof val === 'string' && paramVals.grouplibraries.find((validName) => validName === val)) {
368 | setPref('grouplibraries', val)
369 | } else setPref('grouplibraries', paramVals.grouplibraries[0])
370 | getParam.grouplibraries()
371 | } else if (prezot7) {
372 | const val = Zotero.Prefs.get('extensions.mdbconnect.grouplibraries', true) // as string
373 | if (val && typeof val === 'string' && paramVals.grouplibraries.find((validName) => validName === val)) {
374 | setPref('grouplibraries', val)
375 | } else setPref('grouplibraries', paramVals.grouplibraries[0])
376 | getParam.grouplibraries()
377 | }
378 | } catch (err) {
379 | Logger.log('startupDependencyCheck', `grouplibraries ERROR: ${getErrorMessage(err)}`, false, 'error')
380 | }
381 |
382 | /// removetags
383 | try {
384 | if (preprerename1) {
385 | const val = getPref('removetags') // as string
386 | if (val && typeof val === 'string' && paramVals.removetags.find((validName) => validName === val)) {
387 | setPref('removetags', val)
388 | } else if (val) {
389 | setPref('removetags', paramVals.removetags[0])
390 | }
391 | getParam.removetags()
392 | } else if (prezot7) {
393 | const val = Zotero.Prefs.get('extensions.mdbconnect.removetags', true) // as string
394 | if (val && typeof val === 'string' && paramVals.removetags.find((validName) => validName === val)) {
395 | setPref('removetags', val)
396 | } else if (val) {
397 | setPref('removetags', paramVals.removetags[0])
398 | }
399 | }
400 | } catch (err) {
401 | Logger.log('startupDependencyCheck', `removetags ERROR: ${getErrorMessage(err)}`, false, 'error')
402 | }
403 |
404 | /// tagstr
405 | try {
406 | if (preprerename1) {
407 | const val = getPref('tagstr') // as string
408 | if (val) {
409 | setPref('tagstr', val)
410 | }
411 | getParam.tagstr()
412 | } else if (prezot7) {
413 | const val = Zotero.Prefs.get('extensions.mdbconnect.tagstr', true) // as string
414 | if (val && typeof val === 'string' && val.length > 0) {
415 | setPref('tagstr', val)
416 | }
417 | getParam.tagstr()
418 | }
419 | } catch (err) {
420 | Logger.log('startupDependencyCheck', `tagstr ERROR: ${getErrorMessage(err)}`, false, 'error')
421 | }
422 |
423 | if (addon.data.env === 'production') {
424 | setPref('configuration', version)
425 | Logger.log(
426 | 'startupDependencyCheck',
427 | `Configuration version set to ${versionParse.app.str}. Was previously ${versionParse.config.str}.`,
428 | false,
429 | 'debug',
430 | )
431 | } else {
432 | Logger.log(
433 | 'startupDependencyCheck',
434 | `Configuration version set to ${versionParse.app.str}. Was previously ${versionParse.config.str}.`,
435 | false,
436 | 'debug',
437 | )
438 | }
439 | }
440 |
441 | if (config.addonID !== 'daeda@mit.edu') {
442 | this.fetchAndParseJsonFromGitHub()
443 | .then((status) => {
444 | if (status === 'mismatch') {
445 | Notifier.notify({
446 | title: 'UPDATE AVAILABLE',
447 | body: `Please visit the ${config.addonName} GitHub repository to download.`,
448 | type: 'warn',
449 | })
450 | Logger.log('fetchAndParseJsonFromGitHub', 'update suggested', false, 'info')
451 | }
452 | })
453 | .catch((err) => {
454 | Logger.log('fetchAndParseJsonFromGitHub', `ERROR :: ${err}`, true, 'error')
455 | })
456 | }
457 | }
458 |
459 | @trace
460 | static async startupConfigCheck() {
461 | let success = true
462 |
463 | if (!getParam.sourcedir().valid) {
464 | success = false
465 | }
466 |
467 | getParam.obsidianresolve()
468 |
469 | // TODO - check for BBT if BBT citekeys are used
470 |
471 | return success
472 | }
473 | }
474 |
--------------------------------------------------------------------------------
/src/modules/mdbcUX.ts:
--------------------------------------------------------------------------------
1 | import { config } from '../../package.json'
2 | import { DataManager } from '../dataGlobals'
3 | import { getString } from '../utils/locale'
4 | import { setPref } from '../utils/prefs'
5 |
6 | import { Elements } from './create-element'
7 | import { getErrorMessage, Logger, trace } from './mdbcLogger'
8 | import { getParam } from './mdbcParam'
9 | import { patch as $patch$ } from './monkey-patch'
10 |
11 | import type { Entry, notificationData, NotificationType, NotifyCreateLineOptions, ZoteroIconURI } from '../mdbcTypes'
12 |
13 | const favIcon = `chrome://${config.addonRef}/content/icons/favicon.png` as const // TODO: move def and import form all modules
14 |
15 | const additionalIcons = [favIcon, 'chrome://zotero/skin/toolbar-item-add@2x.png'] as const
16 | type AddonIconURI = (typeof additionalIcons)[number]
17 | type IconURI = AddonIconURI | ZoteroIconURI
18 |
19 | export class Notifier {
20 | static readonly notificationTypes: Record = {
21 | addon: favIcon,
22 | success: 'chrome://zotero/skin/tick@2x.png',
23 | error: 'chrome://zotero/skin/error@2x.png', //'cross@2x.png',
24 | warn: 'chrome://zotero/skin/warning@2x.png',
25 | info: 'chrome://zotero/skin/prefs-advanced.png',
26 | debug: 'chrome://zotero/skin/treeitem-patent@2x.png',
27 | config: 'chrome://zotero/skin/prefs-general.png',
28 | itemsadded: 'chrome://zotero/skin/toolbar-item-add@2x.png',
29 | itemsremoved: 'chrome://zotero/skin/minus@2x.png',
30 | // xmark@2x.png
31 | }
32 |
33 | static notify(data: notificationData): void {
34 | const header = `${config.addonName} : ${data.title}`
35 |
36 | let messageArray: notificationData['messageArray'] = []
37 | try {
38 | if (!('messageArray' in data) || !Array.isArray(data.messageArray) || data.messageArray.length === 0) {
39 | if (!data.body || !data.type) return
40 | messageArray = [{ body: data.body, type: data.type }]
41 | } else {
42 | messageArray = data.messageArray
43 | }
44 | } catch (err) {
45 | Logger.log('Notifier', `ERROR: ${getErrorMessage(err)}`, false, 'error')
46 | return
47 | }
48 |
49 | const timeout = 5 // seconds
50 | const ms = 1000 // milliseconds
51 | const popupWin = new ztoolkit.ProgressWindow(header, {
52 | // window?: Window,
53 | closeOnClick: true,
54 | closeTime: timeout * ms,
55 | closeOtherProgressWindows: false,
56 | })
57 |
58 | for (const message of messageArray) {
59 | const type = message.type || 'addon'
60 |
61 | const lineOptions: NotifyCreateLineOptions = {
62 | text: message.body,
63 | icon: this.notificationTypes[type],
64 | progress: 100,
65 | }
66 | popupWin.createLine(lineOptions)
67 | }
68 |
69 | popupWin.show()
70 | }
71 | }
72 |
73 | export class systemInterface {
74 | static expandSelection(ids: 'selected' | number | number[]): number[] {
75 | if (Array.isArray(ids)) return ids
76 |
77 | if (ids === 'selected') {
78 | try {
79 | // return Zotero.getActiveZoteroPane().getSelectedItems(true)
80 | return ztoolkit.getGlobal('ZoteroPane').getSelectedItems(true)
81 | } catch (err) {
82 | // zoteroPane.getSelectedItems() doesn't test whether there's a selection and errors out if not
83 | Logger.log('expandSelection', `Could not get selected items: ${getErrorMessage(err)}`, false, 'warn')
84 | return []
85 | }
86 | }
87 |
88 | return [ids]
89 | }
90 |
91 | @trace
92 | static async dumpDebuggingLog() {
93 | const data = JSON.stringify(Logger.dump(), null, 1)
94 | const filename = `${config.addonName.replace('-', '')}-logs.json`
95 |
96 | const filepathstr = await new ztoolkit.FilePicker(
97 | `Save ${config.addonName} Debugging Logs`,
98 | 'save',
99 | [
100 | ['JSON File(*.json)', '*.json'],
101 | ['Any', '*.*'],
102 | ],
103 | filename,
104 | ).open()
105 |
106 | if (!filepathstr) return
107 |
108 | // const fileObj = Zotero.File.pathToFile(pathstr)
109 | // if (fileObj instanceof Components.interfaces.nsIFile) {}
110 | // fileObj.normalize()
111 | // fileObj.isFile()
112 |
113 | Logger.log('saveDebuggingLog', `Saving to ${filepathstr}`, false, 'info')
114 |
115 | await Zotero.File.putContentsAsync(filepathstr, data)
116 | }
117 |
118 | @trace
119 | static async dumpJsonFile(data: string, title: string, filename: string) {
120 | // saveButtonTitle
121 | // saveDialogTitle
122 | // fileNameSuggest
123 | // dataGetter
124 |
125 | // const data = JSON.stringify(Logger.dump(), null, 1)
126 |
127 | // const filename = `${config.addonName.replace('-', '')}-logs.json`
128 |
129 | if (!data) {
130 | Logger.log(
131 | 'saveJsonFile',
132 | `ERROR No data to save. \n filename :: ${filename} \n title :: ${title} \n data :: ${data}`,
133 | false,
134 | 'error',
135 | )
136 | }
137 |
138 | const filepathstr = await new ztoolkit.FilePicker(
139 | title,
140 | 'save',
141 | [
142 | ['JSON File(*.json)', '*.json'],
143 | ['Any', '*.*'],
144 | ],
145 | filename,
146 | ).open()
147 |
148 | if (!filepathstr) return
149 |
150 | // const fileObj = Zotero.File.pathToFile(pathstr)
151 | // if (fileObj instanceof Components.interfaces.nsIFile) {}
152 | // fileObj.normalize()
153 | // fileObj.isFile()
154 |
155 | Logger.log('saveJsonFile', `Saving to ${filepathstr}`, false, 'info')
156 |
157 | await Zotero.File.putContentsAsync(filepathstr, data)
158 | }
159 |
160 | @trace
161 | static showSelectedItemMarkdownInFilesystem(entry_res: Entry): void {
162 | try {
163 | const fileObj = Zotero.File.pathToFile(entry_res.path)
164 | fileObj.normalize()
165 | if (fileObj.isFile()) {
166 | try {
167 | fileObj.reveal()
168 | Logger.log('showSelectedItemMarkdownInFilesystem', `Revealing ${fileObj.path}`, false, 'info')
169 | } catch (err) {
170 | // On platforms that don't support nsIFileObj.reveal() (e.g. Linux), launch the parent directory
171 | Zotero.launchFile(fileObj.parent.path)
172 | Logger.log(
173 | 'showSelectedItemMarkdownInFilesystem',
174 | `Reveal failed, falling back to opening parent directory of ${fileObj.path}`,
175 | false,
176 | 'warn',
177 | )
178 | }
179 | }
180 | } catch (err) {
181 | Logger.log(
182 | 'showSelectedItemMarkdownInFilesystem',
183 | `ERROR :: ${entry_res?.path} :: ${getErrorMessage(err)}`,
184 | false,
185 | 'warn',
186 | )
187 | }
188 | }
189 |
190 | @trace
191 | static openFileSystemPath(entry_res: Entry): void {
192 | try {
193 | const fileObj = Zotero.File.pathToFile(entry_res.path)
194 | fileObj.normalize()
195 | if (fileObj.isFile()) {
196 | Zotero.launchFile(fileObj.path)
197 | Logger.log('openFileSystemPath', `Revealing ${fileObj.path}`, false, 'info')
198 | }
199 | } catch (err) {
200 | Logger.log('openFileSystemPath', `ERROR :: ${entry_res?.path} :: ${getErrorMessage(err)}`, false, 'warn')
201 | }
202 | }
203 |
204 | @trace
205 | static openObsidianURI(entry_res: Entry): void {
206 | try {
207 | const uri_spec = getParam.obsidianresolve().value
208 | const vaultnameParam = getParam.obsidianvaultname()
209 | const vaultKey = vaultnameParam.valid ? `vault=${vaultnameParam.value}&` : ''
210 |
211 | const fileKey =
212 | uri_spec === 'file'
213 | ? `file=${encodeURIComponent(entry_res.name)}`
214 | : `path=${encodeURIComponent(entry_res.path)}`
215 |
216 | const uri = `obsidian://open?${vaultKey}${fileKey}`
217 | Zotero.launchURL(uri)
218 |
219 | Logger.log('openObsidianURI', `Launching ${entry_res.path} :: ${uri}`, false, 'info')
220 | } catch (err) {
221 | Logger.log('openObsidianURI', `ERROR :: ${entry_res?.path} :: ${getErrorMessage(err)}`, false, 'warn')
222 | }
223 | }
224 |
225 | @trace
226 | static openLogseqURI(entry_res: Entry): void {
227 | try {
228 | /// get filename without extension
229 | const fileObj = Zotero.File.pathToFile(entry_res.path)
230 | fileObj.normalize()
231 | // const filename = fileObj.getRelativePath(fileObj.parent)
232 | // const filename = fileObj.displayName
233 | const filename = fileObj.leafName
234 | const filenamebase = filename.replace(/\.md$/i, '')
235 |
236 | /// get graph name
237 | let graphName = ''
238 | const graphNameParam = getParam.logseqgraph()
239 | if (graphNameParam.valid) {
240 | graphName = graphNameParam.value
241 | } else {
242 | /* if graph name not specified, try to get it from the path */
243 | try {
244 | graphName = fileObj.parent.parent.leafName
245 | } catch (err) {
246 | Logger.log('openLogseqURI', `ERROR :: ${entry_res?.path} :: ${getErrorMessage(err)}`, false, 'warn')
247 | /* if candidate graph name not found, abort */
248 | graphName = '' /// will case error below
249 | }
250 | }
251 |
252 | if (graphName === '') {
253 | Notifier.notify({
254 | title: 'Error',
255 | body: `logseq graph name not found. Set the graph name in the ${config.addonName} preferences.`,
256 | type: 'error',
257 | })
258 | throw new Error('graphName not resolved')
259 | }
260 |
261 | /// if using re-encoded note name
262 | // const fileKey = `page=${logseq_prefix_file}${filenamebase}`
263 | /// if using filename
264 | const fileKey = `page=${filenamebase}`
265 | const uri = `logseq://graph/${graphName}?${fileKey}`
266 |
267 | /* prefix not encoded, filename encoded */
268 | Zotero.launchURL(uri)
269 |
270 | Logger.log('openLogseqURI', `Launching ${entry_res.path} :: ${uri}`, false, 'info')
271 | } catch (err) {
272 | Logger.log('openLogseqURI', `ERROR :: ${entry_res?.path} :: ${getErrorMessage(err)}`, false, 'warn')
273 | }
274 | }
275 | }
276 |
277 | export class UIHelpers {
278 | @trace
279 | static registerWindowMenuItem_Sync() {
280 | ztoolkit.Menu.register('menuTools', {
281 | tag: 'menuseparator',
282 | })
283 | // menu->Tools menuitem
284 | ztoolkit.Menu.register('menuTools', {
285 | tag: 'menuitem',
286 | id: `${config.addonRef}-tools-menu-sync`,
287 | label: getString('menuitem-sync'),
288 | oncommand: `Zotero.${config.addonInstance}.hooks.syncMarkDB();`,
289 | })
290 | }
291 |
292 | @trace
293 | static registerWindowMenuItem_Debug() {
294 | // menu->Tools menuitem
295 | ztoolkit.Menu.register('menuTools', {
296 | tag: 'menuitem',
297 | id: `${config.addonRef}-tools-menu-troubleshoot`,
298 | label: getString('menuitem-troubleshoot'),
299 | oncommand: `Zotero.${config.addonInstance}.hooks.syncMarkDBReport();`,
300 | })
301 | // tag: "menuitem",
302 | // id: "zotero-itemmenu-addontemplate-test",
303 | // label: "Addon Template: Menuitem",
304 | // oncommand: "alert('Hello World! Default Menuitem.')",
305 | // icon: menuIcon,
306 | // register(menuPopup: XUL.MenuPopup | keyof typeof MenuSelector, options: MenuitemOptions, insertPosition?: "before" | "after", anchorElement?: XUL.Element): false | undefined;
307 | // unregister(menuId: string): void;
308 | }
309 |
310 | static registerRightClickMenuItem() {
311 | $patch$(
312 | Zotero.getActiveZoteroPane(),
313 | 'buildItemContextMenu',
314 | (original) =>
315 | async function ZoteroPane_buildItemContextMenu() {
316 | // @ts-ignore
317 | await original.apply(this, arguments)
318 |
319 | /*
320 | const doc = Zotero.getMainWindow().document
321 |
322 | const elements2 = new Elements(doc)
323 | const itemmenu2 = doc.getElementById('zotero-itemmenu')
324 |
325 | itemmenu2?.appendChild(elements2.create('menuseparator'))
326 |
327 | itemmenu2?.appendChild(
328 | elements2.create('menuitem', {
329 | id: 'testeee',
330 | label: 'whatttt',
331 | // class: 'menuitem-iconic',
332 | // image: 'chrome://.....svg',
333 | // oncommand: () => openfn(entry_res_list[0]),systemInterface.showSelectedItemMarkdownInFilesystem(entry_res_list[0]),
334 | }),
335 | )
336 | */
337 |
338 | const doc = Zotero.getMainWindow().document
339 |
340 | const itemMenuRevealId = '__addonRef__-itemmenu'
341 | doc.getElementById(itemMenuRevealId)?.remove()
342 |
343 | const itemMenuOpenId = '__addonRef__-itemmenu'
344 | doc.getElementById(itemMenuOpenId)?.remove()
345 |
346 | const itemMenuSeparatorId = '__addonRef__-itemmenu-separator'
347 | doc.getElementById(itemMenuSeparatorId)?.remove()
348 |
349 | //// this ~= Zotero.getActiveZoteroPane() ////
350 | // @ts-ignore
351 | const selectedItemIds: number[] = this.getSelectedItems(true)
352 |
353 | if (!selectedItemIds) return
354 |
355 | if (selectedItemIds.length > 1) return
356 |
357 | const itemId: number = selectedItemIds[0]
358 |
359 | // doc.getElementById('testeee')?.remove()
360 | // const itemmenu2 = doc.getElementById('zotero-itemmenu')
361 | // const elements2 = new Elements(doc)
362 | // itemmenu2?.appendChild(
363 | // elements2.create('menuitem', {
364 | // id: 'testeee',
365 | // label: `TEST5: ${DataManager.zotIds()[0]}`,
366 | // // label: `TEST5: ${DataManager.checkForZotId(itemId)}`,
367 | // // class: 'menuitem-iconic',
368 | // // image: 'chrome://.....svg',
369 | // // oncommand: () => openfn(entry_res_list[0]),systemInterface.showSelectedItemMarkdownInFilesystem(entry_res_list[0]),
370 | // }),
371 | // )
372 |
373 | if (!DataManager.checkForZotId(itemId)) return
374 |
375 | const entry_res_list: Entry[] = DataManager.getEntryList(itemId)
376 |
377 | const numEntries = entry_res_list.length
378 |
379 | if (numEntries == 0) return
380 |
381 | // const elements = new Elements(doc)
382 | //
383 | // const itemmenu = doc.getElementById('zotero-itemmenu')
384 |
385 | const elements = new Elements(doc)
386 | const itemmenu = doc.getElementById('zotero-itemmenu')
387 |
388 | if (!itemmenu) return
389 |
390 | let menuitemopenlabel: string
391 | let openfn: (entry: Entry) => void
392 |
393 | const protocol = getParam.mdeditor().value
394 | switch (protocol) {
395 | case 'obsidian':
396 | menuitemopenlabel = getString('contextmenuitem-open-obsidian')
397 | openfn = (entry: Entry) => systemInterface.openObsidianURI(entry)
398 | break
399 | case 'logseq':
400 | menuitemopenlabel = getString('contextmenuitem-open-logseq')
401 | openfn = (entry: Entry) => systemInterface.openLogseqURI(entry)
402 | break
403 | case 'system':
404 | menuitemopenlabel = getString('contextmenuitem-open-default')
405 | openfn = (entry: Entry) => systemInterface.openFileSystemPath(entry)
406 | break
407 | default:
408 | menuitemopenlabel = getString('contextmenuitem-open-default')
409 | openfn = (entry: Entry) => systemInterface.openFileSystemPath(entry)
410 | break
411 | }
412 |
413 | itemmenu.appendChild(elements.create('menuseparator', { id: itemMenuSeparatorId }))
414 |
415 | if (numEntries == 1) {
416 | itemmenu.appendChild(
417 | elements.create('menuitem', {
418 | id: itemMenuOpenId,
419 | label: menuitemopenlabel,
420 | // class: 'menuitem-iconic',
421 | // image: 'chrome://.....svg',
422 | // oncommand: () => openfn(entry_res_list[0]),systemInterface.showSelectedItemMarkdownInFilesystem(entry_res_list[0]),
423 | oncommand: () => openfn(entry_res_list[0]),
424 | }),
425 | )
426 |
427 | itemmenu.appendChild(
428 | elements.create('menuitem', {
429 | id: itemMenuRevealId,
430 | label: getString('contextmenuitem-reveal'),
431 | oncommand: () => systemInterface.showSelectedItemMarkdownInFilesystem(entry_res_list[0]),
432 | }),
433 | )
434 | } else if (numEntries > 1) {
435 | const menupopupOpen = itemmenu
436 | .appendChild(
437 | elements.create('menu', {
438 | id: itemMenuOpenId,
439 | label: menuitemopenlabel,
440 | }),
441 | )
442 | .appendChild(elements.create('menupopup'))
443 |
444 | const menupopupReveal = itemmenu
445 | .appendChild(
446 | elements.create('menu', {
447 | id: itemMenuRevealId,
448 | label: getString('contextmenuitem-reveal'),
449 | }),
450 | )
451 | .appendChild(elements.create('menupopup'))
452 |
453 | entry_res_list.forEach((entry_res) => {
454 | menupopupOpen.appendChild(
455 | elements.create('menuitem', {
456 | label: entry_res.name,
457 | oncommand: () => openfn(entry_res),
458 | }),
459 | )
460 | menupopupReveal.appendChild(
461 | elements.create('menuitem', {
462 | label: entry_res.name,
463 | oncommand: () => systemInterface.showSelectedItemMarkdownInFilesystem(entry_res),
464 | }),
465 | )
466 | })
467 | }
468 | },
469 | )
470 | }
471 |
472 | @trace
473 | static registerRightClickMenuItem2() {
474 | // https://github.com/retorquere/zotero-open-pdf/blob/3440471098bf1b4315317de0381065b3adf61cd4/lib.ts#L96
475 | // $patch$(Zotero_LocateMenu, 'buildContextMenu', original => async function Zotero_LocateMenu_buildContextMenu(menu: HTMLElement, _showIcons: boolean): Promise {
476 |
477 | // https://github.com/retorquere/zotero-pmcid-fetcher/blob/59449449406e4d0706030184d0c06f02fe14a1e1/lib.ts#L3
478 | // patch(Zotero.getActiveZoteroPane(), 'buildItemContextMenu', original => async function ZoteroPane_buildItemContextMenu() {
479 | // await original.apply(this, arguments) // eslint-disable-line prefer-rest-params
480 | $patch$(
481 | Zotero.getActiveZoteroPane(),
482 | // ztoolkit.getGlobal('ZoteroPane'),
483 | // ztoolkit.getGlobal('Zotero_Tabs').select('zotero-pane'),
484 | 'buildItemContextMenu',
485 | (original) =>
486 | async function ZoteroPane_buildItemContextMenu() {
487 | // @ts-ignore
488 | // await original.apply(this, arguments)
489 | this.document = Zotero.getMainWindow().document
490 | console.log(Zotero.getMainWindow().document)
491 | // @ts-ignore
492 | original.apply(this, arguments)
493 |
494 | // const doc = Zotero.getMainWindow().document
495 |
496 | const itemMenuRevealId = '__addonRef__-itemmenu'
497 | Zotero.getMainWindow().document.getElementById(itemMenuRevealId)?.remove()
498 |
499 | const itemMenuOpenId = '__addonRef__-itemmenu'
500 | Zotero.getMainWindow().document.getElementById(itemMenuOpenId)?.remove()
501 |
502 | const itemMenuSeparatorId = '__addonRef__-itemmenu-separator'
503 | Zotero.getMainWindow().document.getElementById(itemMenuSeparatorId)?.remove()
504 |
505 | //// this ~= Zotero.getActiveZoteroPane() ////
506 | // @ts-ignore
507 | const selectedItemIds: number[] = this.getSelectedItems(true)
508 |
509 | if (!selectedItemIds) return
510 |
511 | if (selectedItemIds.length > 1) return
512 |
513 | const itemId: number = selectedItemIds[0]
514 |
515 | if (!DataManager.checkForZotId(itemId)) return
516 |
517 | const entry_res_list: Entry[] = DataManager.getEntryList(itemId)
518 |
519 | const numEntries = entry_res_list.length
520 |
521 | if (numEntries == 0) return
522 |
523 | const elements = new Elements(Zotero.getMainWindow().document)
524 |
525 | const itemmenu = Zotero.getMainWindow().document.getElementById('zotero-itemmenu')
526 |
527 | if (!itemmenu) return
528 |
529 | let menuitemopenlabel: string
530 | let openfn: (entry: Entry) => void
531 |
532 | const protocol = getParam.mdeditor().value
533 | switch (protocol) {
534 | case 'obsidian':
535 | menuitemopenlabel = getString('contextmenuitem-open-obsidian')
536 | openfn = (entry: Entry) => systemInterface.openObsidianURI(entry)
537 | break
538 | case 'logseq':
539 | menuitemopenlabel = getString('contextmenuitem-open-logseq')
540 | openfn = (entry: Entry) => systemInterface.openLogseqURI(entry)
541 | break
542 | case 'system':
543 | menuitemopenlabel = getString('contextmenuitem-open-default')
544 | openfn = (entry: Entry) => systemInterface.openFileSystemPath(entry)
545 | break
546 | default:
547 | menuitemopenlabel = getString('contextmenuitem-open-default')
548 | openfn = (entry: Entry) => systemInterface.openFileSystemPath(entry)
549 | break
550 | }
551 |
552 | itemmenu?.appendChild(elements.create('menuseparator', { id: itemMenuSeparatorId }))
553 | ////WIP
554 | // itemmenu?.appendChild(ztoolkit.UI.createElement(document, 'menuseparator', { id: itemMenuSeparatorId }))
555 |
556 | if (numEntries == 1) {
557 | itemmenu.appendChild(
558 | elements.create('menuitem', {
559 | id: itemMenuOpenId,
560 | label: menuitemopenlabel,
561 | // class: 'menuitem-iconic',
562 | // image: 'chrome://.....svg',
563 | // oncommand: () => openfn(entry_res_list[0]),systemInterface.showSelectedItemMarkdownInFilesystem(entry_res_list[0]),
564 | oncommand: () => openfn(entry_res_list[0]),
565 | }),
566 | )
567 | ////WIP
568 | // itemmenu.appendChild(
569 | // ztoolkit.UI.createElement(document, 'menuitem', {
570 | // id: itemMenuOpenId,
571 | // attributes: {
572 | // label: menuitemopenlabel,
573 | // },
574 | // // properties: {}
575 | // // classList: ['icon'],
576 | // listeners: [
577 | // {
578 | // type: 'command',
579 | // listener: (event) => {
580 | // openfn(entry_res_list[0])
581 | // // event.preventDefault()
582 | // },
583 | // },
584 | // ],
585 | // }),
586 | // )
587 |
588 | itemmenu.appendChild(
589 | elements.create('menuitem', {
590 | id: itemMenuRevealId,
591 | label: getString('contextmenuitem-reveal'),
592 | oncommand: () => systemInterface.showSelectedItemMarkdownInFilesystem(entry_res_list[0]),
593 | }),
594 | )
595 | ////WIP
596 | // itemmenu.appendChild(
597 | // ztoolkit.UI.createElement(document, 'menuitem', {
598 | // id: itemMenuRevealId,
599 | // attributes: {
600 | // label: getString('contextmenuitem-reveal'),
601 | // },
602 | // // properties: {}
603 | // // classList: ['icon'],
604 | // listeners: [
605 | // {
606 | // type: 'command',
607 | // listener: (event) => {
608 | // systemInterface.showSelectedItemMarkdownInFilesystem(entry_res_list[0])
609 | // // event.preventDefault()
610 | // },
611 | // },
612 | // ],
613 | // }),
614 | // )
615 | } else if (numEntries > 1) {
616 | const menupopupOpen = itemmenu
617 | .appendChild(
618 | elements.create('menu', {
619 | id: itemMenuOpenId,
620 | label: menuitemopenlabel,
621 | }),
622 | )
623 | .appendChild(elements.create('menupopup'))
624 |
625 | const menupopupReveal = itemmenu
626 | .appendChild(
627 | elements.create('menu', {
628 | id: itemMenuRevealId,
629 | label: getString('contextmenuitem-reveal'),
630 | }),
631 | )
632 | .appendChild(elements.create('menupopup'))
633 |
634 | entry_res_list.forEach((entry_res) => {
635 | menupopupOpen.appendChild(
636 | elements.create('menuitem', {
637 | label: entry_res.name,
638 | oncommand: () => openfn(entry_res),
639 | }),
640 | )
641 | menupopupReveal.appendChild(
642 | elements.create('menuitem', {
643 | label: entry_res.name,
644 | oncommand: () => systemInterface.showSelectedItemMarkdownInFilesystem(entry_res),
645 | }),
646 | )
647 | })
648 |
649 | ////WIP
650 | // const menupopupOpen =
651 | // itemmenu.appendChild(
652 | // ztoolkit.UI.createElement(document, 'menu', {
653 | // id: itemMenuOpenId,
654 | // attributes: {
655 | // label: menuitemopenlabel,
656 | // },
657 | // }),
658 | // )
659 | // .appendChild(
660 | // ztoolkit.UI.createElement(document, 'menupopup', {
661 | // // children: [
662 | // // {
663 | // // tag: 'menuitem',
664 | // // attributes: {
665 | // // id: 'zotero-tb-tara-create-backup',
666 | // // label: getString('toolbar-create'),
667 | // // class: 'menuitem-iconic',
668 | // // style: "list-style-image: url('chrome://tara/content/icons/create_icon.png');",
669 | // // oncommand: "alert('create');",
670 | // // },
671 | // // },
672 | // // ],
673 | // children: entry_res_list.map((entry_res) => {
674 | // return {
675 | // tag: 'menuitem',
676 | // attributes: {
677 | // label: entry_res.name,
678 | // },
679 | // listeners: [
680 | // {
681 | // type: 'command',
682 | // listener: (event) => {
683 | // openfn(entry_res)
684 | // // event.preventDefault()
685 | // },
686 | // },
687 | // ],
688 | // }
689 | // }),
690 | // }),
691 | // )
692 |
693 | /////REVERT ME
694 |
695 | // entry_res_list.forEach((entry_res) => {
696 | // menupopupOpen.appendChild(
697 | // ztoolkit.UI.createElement(document, 'menuitem', {
698 | // attributes: {
699 | // label: entry_res.name,
700 | // },
701 | // listeners: [
702 | // {
703 | // type: 'command',
704 | // listener: (event) => {
705 | // openfn(entry_res)
706 | // // event.preventDefault()
707 | // },
708 | // },
709 | // ],
710 | // }),
711 | // )
712 | // // menupopupReveal.appendChild(
713 | // // elements.create('menuitem', {
714 | // // label: entry_res.name,
715 | // // oncommand: () => systemInterface.showSelectedItemMarkdownInFilesystem(entry_res),
716 | // // }),
717 | // // )
718 | // })
719 |
720 | // const menupopupReveal = itemmenu
721 | // .appendChild(
722 | // elements.create('menu', {
723 | // id: itemMenuRevealId,
724 | // label: getString('contextmenuitem-reveal'),
725 | // }),
726 | // )
727 | // .appendChild(elements.create('menupopup'))
728 | //
729 | // entry_res_list.forEach((entry_res) => {
730 | // menupopupOpen.appendChild(
731 | // elements.create('menuitem', {
732 | // label: entry_res.name,
733 | // oncommand: () => openfn(entry_res),
734 | // }),
735 | // )
736 | // menupopupReveal.appendChild(
737 | // elements.create('menuitem', {
738 | // label: entry_res.name,
739 | // oncommand: () => systemInterface.showSelectedItemMarkdownInFilesystem(entry_res),
740 | // }),
741 | // )
742 | // })
743 | // }
744 | }
745 | },
746 | )
747 | }
748 |
749 | @trace
750 | static highlightTaggedRows() {
751 | /* Render primary cell
752 | _renderCell
753 | _renderPrimaryCell
754 | https://github.com/zotero/zotero/blob/32ba987c2892e2aee6046a82c08d69145e758afd/chrome/content/zotero/elements/colorPicker.js#L178
755 | https://github.com/windingwind/ZoteroStyle/blob/6b7c7c95abb7e5d75d0e1fbcc2d824c0c4e2e81a/src/events.ts#L263
756 | https://github.com/ZXLYX/ZoteroStyle/blob/57fa178a1a45e710a73706f0087892cf19c9caf1/src/events.ts#L286
757 | */
758 | const tagstrParam = getParam.tagstr()
759 | if (!tagstrParam.valid) return
760 | const tagstr = tagstrParam.value
761 |
762 | // Select all span elements with aria-label containing "Tag ObsCite."
763 | const spans: NodeListOf = Zotero.getMainWindow().document.querySelectorAll(
764 | `span[aria-label*="Tag ${tagstr}."]`,
765 | )
766 |
767 | // Iterate over the NodeList and change the text color to red
768 | spans.forEach((span) => {
769 | span.style.color = 'red'
770 | })
771 |
772 | // await ztoolkit.ItemTree.register()
773 | }
774 | }
775 |
776 | export class prefHelpers {
777 | @trace
778 | static async chooseVaultFolder() {
779 | const vaultpath = await new ztoolkit.FilePicker('Select Folder containing MD reading notes', 'folder').open()
780 |
781 | try {
782 | if (!vaultpath) throw new Error('No folder selected')
783 |
784 | const vaultpathObj = Zotero.File.pathToFile(vaultpath)
785 | vaultpathObj.normalize()
786 |
787 | if (
788 | vaultpath !== '' &&
789 | vaultpath !== undefined &&
790 | vaultpath != null &&
791 | vaultpathObj.exists() &&
792 | vaultpathObj.isDirectory()
793 | ) {
794 | setPref('sourcedir', vaultpath)
795 | }
796 | } catch (err) {
797 | Logger.log('chooseVaultFolder', `ERROR chooseVaultFolder :: ${getErrorMessage(err)}`, false, 'warn')
798 | }
799 | }
800 |
801 | static isValidRegExp(str: string): boolean {
802 | try {
803 | new RegExp(str)
804 | return true // No error means it's a valid RegExp
805 | } catch (err) {
806 | Logger.log('isValidRegExp', `ERROR: RegExp is not valid:: >> ${str} <<.`, false, 'warn')
807 | return false // An error indicates an invalid RegExp
808 | }
809 | }
810 |
811 | static checkMetadataFormat(metadatakeyword: string): boolean {
812 | if (typeof metadatakeyword === 'string' && metadatakeyword.length > 0) {
813 | const found: string[] = []
814 | const notallowed = [
815 | "'",
816 | '"',
817 | ':',
818 | '\n',
819 | '/',
820 | '\\',
821 | '?',
822 | '*',
823 | '|',
824 | '>',
825 | '<',
826 | ',',
827 | ';',
828 | '=',
829 | '`',
830 | '~',
831 | '!',
832 | '#',
833 | '$',
834 | '%',
835 | '^',
836 | '&',
837 | '(',
838 | ')',
839 | '[',
840 | ']',
841 | '{',
842 | '}',
843 | ' ',
844 | ]
845 | for (const char of notallowed) {
846 | if (metadatakeyword.includes(char)) {
847 | found.push(char)
848 | }
849 | }
850 | if (found.length > 0) {
851 | Logger.log('checkMetadataFormat', `ERROR: metadata id cannot contain: ${found.join(' or ')}.`, false, 'warn')
852 | // TODO handle notification
853 | // Notifier.showNotification(
854 | // 'Warning',
855 | // `Invalid citekey metadata. metadata keyword cannot contain: ${found.join(' or ')}.`,
856 | // false,
857 | // )
858 | return false
859 | } else {
860 | return true
861 | }
862 | } else {
863 | return true
864 | }
865 | }
866 |
867 | static checkTagStr(tagstr: string): boolean {
868 | if (typeof tagstr === 'string' && tagstr.length > 0) {
869 | const found: string[] = []
870 | const notallowed = [
871 | "'",
872 | '"',
873 | ':',
874 | '\n',
875 | '\\',
876 | '?',
877 | '*',
878 | '|',
879 | '>',
880 | '<',
881 | ',',
882 | ';',
883 | '=',
884 | '`',
885 | '~',
886 | '!',
887 | // '#',
888 | '$',
889 | '%',
890 | '^',
891 | '&',
892 | '(',
893 | ')',
894 | '[',
895 | ']',
896 | '{',
897 | '}',
898 | ' ',
899 | ]
900 | // '/',
901 | for (const char of notallowed) {
902 | if (tagstr.includes(char)) {
903 | found.push(char)
904 | }
905 | }
906 | if (found.length > 0) {
907 | Logger.log('checkTagStr', `ERROR: TagStr cannot contain: ${found.join(' or ')}.`, false, 'warn')
908 | // TODO Issue-145 - Deal with config string validation before the preference pane is closed (rather than when the sync is run).
909 | // TODO allow more
910 | // Change invalid value behavior from silent reversion to default into vocal reversion to the last-valid value.
911 | // Notifier.showNotification('Warning', `Invalid tag string. Tag cannot contain: ${found.join(' or ')}.`, false)
912 | return false
913 | } else {
914 | return true
915 | }
916 | } else {
917 | return true
918 | }
919 | }
920 | }
921 |
922 | export class Registrar {
923 | @trace
924 | static registerPrefs() {
925 | Zotero.PreferencePanes.register({
926 | pluginID: addon.data.config.addonID,
927 | src: rootURI + 'content/preferences.xhtml',
928 | label: getString('prefs-title'),
929 | image: `chrome://${addon.data.config.addonRef}/content/icons/favicon.png`,
930 | // image: favIcon,
931 | // defaultXUL: true,
932 | })
933 | }
934 | }
935 |
--------------------------------------------------------------------------------
/src/modules/monkey-patch.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unsafe-argument, @typescript-eslint/ban-types, @typescript-eslint/no-unsafe-return */
2 |
3 | export type Trampoline = Function & { disabled?: boolean }
4 | const trampolines: Trampoline[] = []
5 |
6 | export function patch(object: any, method: string, patcher: (f: Function) => Function, mem?: Trampoline[]): void {
7 | if (typeof object[method] !== 'function') throw new Error(`monkey-patch: ${method} is not a function`)
8 |
9 | const orig = object[method]
10 | const patched = patcher(orig)
11 | object[method] = function trampoline() {
12 | return (trampoline as Trampoline).disabled ? orig.apply(this, arguments) : patched.apply(this, arguments)
13 | }
14 | trampolines.push(object[method])
15 | if (mem) mem.push(object[method])
16 | }
17 |
18 | export function unpatch(functions?: Trampoline[]) {
19 | for (const trampoline of functions || trampolines) {
20 | trampoline.disabled = true
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/modules/preferenceScript.ts:
--------------------------------------------------------------------------------
1 | // import { config } from '../../package.json'
2 | // import { getString } from '../utils/locale'
3 |
4 | export async function registerPrefsScripts(_window: Window) {
5 | // This function is called when the prefs window is opened
6 | // See addon/content/preferences.xhtml onpaneload
7 | if (!addon.data.prefs) {
8 | addon.data.prefs = {
9 | window: _window,
10 | columns: [],
11 | rows: [],
12 | }
13 | } else {
14 | addon.data.prefs.window = _window
15 | }
16 | // updatePrefsUI()
17 | // bindPrefEvents()
18 | }
19 |
20 | /*
21 | async function updatePrefsUI() {
22 | // You can initialize some UI elements on prefs window
23 | // with addon.data.prefs.window.document
24 | // Or bind some events to the elements
25 | const renderLock = ztoolkit.getGlobal('Zotero').Promise.defer()
26 | if (addon.data.prefs?.window == undefined) return
27 | const tableHelper = new ztoolkit.VirtualizedTable(addon.data.prefs?.window)
28 | .setContainerId(`${config.addonRef}-table-container`)
29 | .setProp({
30 | id: `${config.addonRef}-prefs-table`,
31 | // Do not use setLocale, as it modifies the Zotero.Intl.strings
32 | // Set locales directly to columns
33 | columns: addon.data.prefs?.columns,
34 | showHeader: true,
35 | multiSelect: true,
36 | staticColumns: true,
37 | disableFontSizeScaling: true,
38 | })
39 | .setProp('getRowCount', () => addon.data.prefs?.rows.length || 0)
40 | .setProp(
41 | 'getRowData',
42 | (index) =>
43 | addon.data.prefs?.rows[index] || {
44 | title: 'no data',
45 | detail: 'no data',
46 | },
47 | )
48 | // Show a progress window when selection changes
49 | .setProp('onSelectionChange', (selection) => {
50 | new ztoolkit.ProgressWindow(config.addonName)
51 | .createLine({
52 | text: `Selected line: ${addon.data.prefs?.rows
53 | .filter((v, i) => selection.isSelected(i))
54 | .map((row) => row.title)
55 | .join(',')}`,
56 | progress: 100,
57 | })
58 | .show()
59 | })
60 | // When pressing delete, delete selected line and refresh table.
61 | // Returning false to prevent default event.
62 | .setProp('onKeyDown', (event: KeyboardEvent) => {
63 | if (event.key == 'Delete' || (Zotero.isMac && event.key == 'Backspace')) {
64 | addon.data.prefs!.rows =
65 | addon.data.prefs?.rows.filter((v, i) => !tableHelper.treeInstance.selection.isSelected(i)) || []
66 | tableHelper.render()
67 | return false
68 | }
69 | return true
70 | })
71 | // For find-as-you-type
72 | .setProp('getRowString', (index) => addon.data.prefs?.rows[index].title || '')
73 | // Render the table.
74 | .render(-1, () => {
75 | renderLock.resolve()
76 | })
77 | await renderLock.promise
78 | ztoolkit.log('Preference table rendered!')
79 | }
80 |
81 | function bindPrefEvents() {
82 | addon.data
83 | .prefs!.window.document?.querySelector(`#zotero-prefpane-${config.addonRef}-enable`)
84 | ?.addEventListener('command', (e: Event) => {
85 | ztoolkit.log(e)
86 | addon.data.prefs!.window.alert(`Successfully changed to ${(e.target as XUL.Checkbox).checked}!`)
87 | })
88 |
89 | addon.data
90 | .prefs!.window.document?.querySelector(`#zotero-prefpane-${config.addonRef}-input`)
91 | ?.addEventListener('change', (e: Event) => {
92 | ztoolkit.log(e)
93 | addon.data.prefs!.window.alert(`Successfully changed to ${(e.target as HTMLInputElement).value}!`)
94 | })
95 | }
96 | */
97 |
--------------------------------------------------------------------------------
/src/utils/locale.ts:
--------------------------------------------------------------------------------
1 | import { config } from '../../package.json'
2 |
3 | export { initLocale, getString, getLocaleID }
4 |
5 | /**
6 | * Initialize locale data
7 | */
8 | function initLocale() {
9 | const l10n = new (typeof Localization === 'undefined' ? ztoolkit.getGlobal('Localization') : Localization)(
10 | [`${config.addonRef}-addon.ftl`],
11 | true,
12 | )
13 | addon.data.locale = {
14 | current: l10n,
15 | }
16 | }
17 |
18 | /**
19 | * Get locale string, see https://firefox-source-docs.mozilla.org/l10n/fluent/tutorial.html#fluent-translation-list-ftl
20 | * @param localString ftl key
21 | * @param options.branch branch name
22 | * @param options.args args
23 | * @example
24 | * ```ftl
25 | * # addon.ftl
26 | * addon-static-example = This is default branch!
27 | * .branch-example = This is a branch under addon-static-example!
28 | * addon-dynamic-example =
29 | { $count ->
30 | [one] I have { $count } apple
31 | *[other] I have { $count } apples
32 | }
33 | * ```
34 | * ```js
35 | * getString("addon-static-example"); // This is default branch!
36 | * getString("addon-static-example", { branch: "branch-example" }); // This is a branch under addon-static-example!
37 | * getString("addon-dynamic-example", { args: { count: 1 } }); // I have 1 apple
38 | * getString("addon-dynamic-example", { args: { count: 2 } }); // I have 2 apples
39 | * ```
40 | */
41 | function getString(localString: string): string
42 | function getString(localString: string, branch: string): string
43 | function getString(
44 | localeString: string,
45 | options: { branch?: string | undefined; args?: Record },
46 | ): string
47 | function getString(...inputs: any[]) {
48 | if (inputs.length === 1) {
49 | return _getString(inputs[0])
50 | } else if (inputs.length === 2) {
51 | if (typeof inputs[1] === 'string') {
52 | return _getString(inputs[0], { branch: inputs[1] })
53 | } else {
54 | return _getString(inputs[0], inputs[1])
55 | }
56 | } else {
57 | throw new Error('Invalid arguments')
58 | }
59 | }
60 |
61 | function _getString(
62 | localeString: string,
63 | options: { branch?: string | undefined; args?: Record } = {},
64 | ): string {
65 | const localStringWithPrefix = `${config.addonRef}-${localeString}`
66 | const { branch, args } = options
67 | const pattern = addon.data.locale?.current.formatMessagesSync([{ id: localStringWithPrefix, args }])[0]
68 | if (!pattern) {
69 | return localStringWithPrefix
70 | }
71 | if (branch && pattern.attributes) {
72 | for (const attr of pattern.attributes) {
73 | if (attr.name === branch) {
74 | return attr.value
75 | }
76 | }
77 | return pattern.attributes[branch] || localStringWithPrefix
78 | } else {
79 | return pattern.value || localStringWithPrefix
80 | }
81 | }
82 |
83 | function getLocaleID(id: string) {
84 | return `${config.addonRef}-${id}`
85 | }
86 |
--------------------------------------------------------------------------------
/src/utils/prefs.ts:
--------------------------------------------------------------------------------
1 | import { config } from '../../package.json'
2 |
3 | type PluginPrefsMap = _ZoteroTypes.Prefs['PluginPrefsMap']
4 |
5 | const PREFS_PREFIX = config.prefsPrefix
6 |
7 | /**
8 | * Get preference value.
9 | * Wrapper of `Zotero.Prefs.get`.
10 | * @param key
11 | */
12 | export function getPref(key: K) {
13 | return Zotero.Prefs.get(`${PREFS_PREFIX}.${key}`, true) as PluginPrefsMap[K]
14 | }
15 |
16 | /**
17 | * Set preference value.
18 | * Wrapper of `Zotero.Prefs.set`.
19 | * @param key
20 | * @param value
21 | */
22 | export function setPref(key: K, value: PluginPrefsMap[K]) {
23 | return Zotero.Prefs.set(`${PREFS_PREFIX}.${key}`, value, true)
24 | }
25 |
26 | /**
27 | * Clear preference value.
28 | * Wrapper of `Zotero.Prefs.clear`.
29 | * @param key
30 | */
31 | export function clearPref(key: string) {
32 | return Zotero.Prefs.clear(`${PREFS_PREFIX}.${key}`, true)
33 | }
34 |
--------------------------------------------------------------------------------
/src/utils/window.ts:
--------------------------------------------------------------------------------
1 | export { isWindowAlive }
2 |
3 | /**
4 | * Check if the window is alive.
5 | * Useful to prevent opening duplicate windows.
6 | * @param win
7 | */
8 | function isWindowAlive(win?: Window) {
9 | return win && !Components.utils.isDeadWrapper(win) && !win.closed
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/ztoolkit.ts:
--------------------------------------------------------------------------------
1 | import { BasicTool, UITool, unregister, ZoteroToolkit } from 'zotero-plugin-toolkit'
2 |
3 | import { config } from '../../package.json'
4 |
5 | export { createZToolkit }
6 |
7 | function createZToolkit() {
8 | const _ztoolkit = new ZoteroToolkit()
9 | /**
10 | * Alternatively, import toolkit modules you use to minify the plugin size.
11 | * You can add the modules under the `MyToolkit` class below and uncomment the following line.
12 | */
13 | // const _ztoolkit = new MyToolkit();
14 | initZToolkit(_ztoolkit)
15 | return _ztoolkit
16 | }
17 |
18 | function initZToolkit(_ztoolkit: ReturnType) {
19 | const env = __env__
20 | _ztoolkit.basicOptions.log.prefix = `[${config.addonName}]`
21 | _ztoolkit.basicOptions.log.disableConsole = env === 'production'
22 | _ztoolkit.UI.basicOptions.ui.enableElementJSONLog = __env__ === 'development'
23 | _ztoolkit.UI.basicOptions.ui.enableElementDOMLog = __env__ === 'development'
24 | // Getting basicOptions.debug will load global modules like the debug bridge.
25 | // since we want to deprecate it, should avoid using it unless necessary.
26 | // _ztoolkit.basicOptions.debug.disableDebugBridgePassword =
27 | // __env__ === "development";
28 | _ztoolkit.basicOptions.api.pluginID = config.addonID
29 | _ztoolkit.ProgressWindow.setIconURI('default', `chrome://${config.addonRef}/content/icons/favicon.png`)
30 | }
31 |
32 | // @ts-ignore leaving inplace for now
33 | class MyToolkit extends BasicTool {
34 | UI: UITool
35 |
36 | constructor() {
37 | super()
38 | this.UI = new UITool(this)
39 | }
40 |
41 | unregisterAll() {
42 | unregister(this)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "zotero-types/entries/sandbox/",
3 | "compilerOptions": {
4 | "module": "ESNext",
5 | "moduleResolution": "bundler",
6 | "target": "ES2018",
7 | "lib": [
8 | "ESNext",
9 | "DOM",
10 | "DOM.Iterable",
11 | "ES2021.WeakRef"
12 | // "webworker",
13 | ],
14 | "types": ["zotero-types"],
15 | "outDir": "build/dist/",
16 |
17 | /* */
18 | "experimentalDecorators": true,
19 | "resolveJsonModule": true,
20 | "skipLibCheck": true,
21 | "noEmit": true,
22 | // "strict": true,
23 |
24 | /* */
25 | // "isolatedModules": true, // for bundlers like vite. Setting the isolatedModules flag tells TypeScript to warn you if you write certain code that can’t be correctly interpreted by a single-file transpilation process.
26 | // "esModuleInterop": true,
27 |
28 | // "allowImportingTsExtensions": true,
29 | // "allowSyntheticDefaultImports": false,
30 |
31 | /* Linting */
32 | "noFallthroughCasesInSwitch": true,
33 | "forceConsistentCasingInFileNames": true
34 | },
35 | "include": ["src", "typings", "package.json", "node_modules/zotero-types"],
36 | "exclude": ["build", "addon", "**/*-lintignore*", "**/*_lintignore*"]
37 | }
38 |
--------------------------------------------------------------------------------
/tsconfig.dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base",
3 | "compilerOptions": {
4 | "strict": false,
5 | "sourceMap": true,
6 | "removeComments": false,
7 | ////
8 | "noUnusedLocals": false,
9 | "noUnusedParameters": false,
10 | "noImplicitAny": false,
11 | ///
12 | "allowUnreachableCode": false
13 | ///
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.prod"
3 | }
4 |
--------------------------------------------------------------------------------
/tsconfig.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "sourceMap": false,
6 | "removeComments": true,
7 | // "noUnusedLocals": false,
8 | // "noUnusedParameters": false,
9 | // "noImplicitAny": false,
10 | // "noFallthroughCasesInSwitch": false
11 | "allowUnreachableCode": false,
12 | "noUnusedLocals": true,
13 | "noUnusedParameters": false,
14 | "importHelpers": true,
15 | "disableSizeLimit": true,
16 | // "noImplicitAny": false,
17 | "preserveConstEnums": true,
18 | "downlevelIteration": true
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tsconfig.repo.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base",
3 | "compilerOptions": {
4 | "strictNullChecks": true,
5 | /* Enable JavaScript Compilation */
6 | "allowJs": true,
7 | "checkJs": true
8 | },
9 | "include": ["src", "typings", "node_modules/zotero-types", "./*.ts", "./*.mjs", "./*.js", ".release-it.ts"],
10 | "exclude": ["build", "addon"]
11 | }
12 |
--------------------------------------------------------------------------------
/typings/global.d.ts:
--------------------------------------------------------------------------------
1 | declare const _globalThis: {
2 | [key: string]: any
3 | Zotero: _ZoteroTypes.Zotero
4 | ztoolkit: ZToolkit
5 | addon: typeof addon
6 | }
7 |
8 | declare type ZToolkit = ReturnType
9 |
10 | declare const ztoolkit: ZToolkit
11 |
12 | declare const rootURI: string
13 |
14 | declare const addon: import('../src/addon').default
15 |
16 | declare const __env__: 'production' | 'development'
17 |
--------------------------------------------------------------------------------
/typings/mdbc.d.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daeh/zotero-markdb-connect/1db3ee2faad330a85f8eb1ec6c359333a4d68a30/typings/mdbc.d.ts
--------------------------------------------------------------------------------
/typings/prefs.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by zotero-plugin-scaffold
2 | /* prettier-ignore */
3 | /* eslint-disable */
4 | // @ts-nocheck
5 |
6 | // prettier-ignore
7 | declare namespace _ZoteroTypes {
8 | interface Prefs {
9 | PluginPrefsMap: {
10 | "enable": boolean;
11 | "configuration": string;
12 | "debugmode": string;
13 | "sourcedir": string;
14 | "filefilterstrategy": string;
15 | "filepattern": string;
16 | "matchstrategy": string;
17 | "bbtyamlkeyword": string;
18 | "bbtregexp": string;
19 | "zotkeyregexp": string;
20 | "mdeditor": string;
21 | "obsidianvaultname": string;
22 | "obsidianresolvespec": string;
23 | "logseqgraph": string;
24 | "logseqprefix": string;
25 | "grouplibraries": string;
26 | "removetags": string;
27 | "tagstr": string;
28 | };
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/typings/zotero.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/update-beta.json:
--------------------------------------------------------------------------------
1 | {
2 | "addons": {
3 | "daeda@mit.edu": {
4 | "updates": [
5 | {
6 | "version": "0.1.6",
7 | "update_link": "https://github.com/daeh/zotero-markdb-connect/releases/download/v0.1.6/markdb-connect.xpi",
8 | "applications": {
9 | "zotero": {
10 | "strict_min_version": "6.999",
11 | "strict_max_version": "7.*"
12 | }
13 | }
14 | },
15 | {
16 | "version": "0.0.27",
17 | "update_link": "https://github.com/daeh/zotero-markdb-connect/releases/download/v0.0.27/markdb-connect-0.0.27.xpi",
18 | "applications": {
19 | "gecko": {
20 | "strict_min_version": "60.9",
21 | "strict_max_version": "60.9"
22 | }
23 | }
24 | }
25 | ]
26 | },
27 | "dev@daeh.info": {
28 | "updates": [
29 | {
30 | "version": "0.1.6",
31 | "update_link": "https://github.com/daeh/zotero-markdb-connect/releases/latest/download/markdb-connect.xpi",
32 | "applications": {
33 | "zotero": {
34 | "strict_min_version": "6.999"
35 | }
36 | }
37 | }
38 | ]
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/update.json:
--------------------------------------------------------------------------------
1 | {
2 | "addons": {
3 | "daeda@mit.edu": {
4 | "updates": [
5 | {
6 | "version": "0.1.6",
7 | "update_link": "https://github.com/daeh/zotero-markdb-connect/releases/latest/download/markdb-connect.xpi",
8 | "applications": {
9 | "zotero": {
10 | "strict_min_version": "6.999",
11 | "strict_max_version": "7.*"
12 | }
13 | }
14 | },
15 | {
16 | "version": "0.0.27",
17 | "update_link": "https://github.com/daeh/zotero-markdb-connect/releases/download/v0.0.27/markdb-connect-0.0.27.xpi",
18 | "applications": {
19 | "gecko": {
20 | "strict_min_version": "60.9",
21 | "strict_max_version": "60.9"
22 | }
23 | }
24 | }
25 | ]
26 | },
27 | "dev@daeh.info": {
28 | "updates": [
29 | {
30 | "version": "0.1.6",
31 | "update_link": "https://github.com/daeh/zotero-markdb-connect/releases/latest/download/markdb-connect.xpi",
32 | "applications": {
33 | "zotero": {
34 | "strict_min_version": "6.999"
35 | }
36 | }
37 | }
38 | ]
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/update.rdf:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 | 0.0.27
10 |
11 |
12 | zotero@chnm.gmu.edu
13 | 5.0.0
14 | 6.0.*
15 | https://github.com/daeh/zotero-markdb-connect/releases/download/v0.0.27/markdb-connect-0.0.27.xpi
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/zotero-plugin.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'zotero-plugin-scaffold'
2 | import pkg from './package.json'
3 | import { copyFileSync } from 'fs'
4 |
5 | export default defineConfig({
6 | source: ['src', 'addon'],
7 | dist: '.scaffold/build',
8 | name: pkg.config.addonName,
9 | id: pkg.config.addonID,
10 | namespace: pkg.config.addonRef,
11 | // updateURL: `https://github.com/{{owner}}/{{repo}}/releases/download/release/${
12 | // pkg.version.includes('-') ? 'update-beta.json' : 'update.json'
13 | // }`,
14 | updateURL: `https://raw.githubusercontent.com/{{owner}}/{{repo}}/main/${
15 | pkg.version.includes('-') ? 'update-beta.json' : 'update.json'
16 | }`,
17 | xpiDownloadLink: 'https://github.com/{{owner}}/{{repo}}/releases/download/v{{version}}/{{xpiName}}.xpi',
18 |
19 | build: {
20 | assets: ['addon/**/*.*'],
21 | define: {
22 | ...pkg.config,
23 | author: pkg.author,
24 | description: pkg.description,
25 | homepage: pkg.homepage,
26 | buildVersion: pkg.version,
27 | buildTime: '{{buildTime}}',
28 | },
29 | prefs: {
30 | prefix: pkg.config.prefsPrefix,
31 | },
32 | esbuildOptions: [
33 | {
34 | entryPoints: ['src/index.ts'],
35 | define: {
36 | __env__: `"${process.env.NODE_ENV}"`,
37 | },
38 | bundle: true,
39 | target: 'firefox115',
40 | outfile: `.scaffold/build/addon/content/scripts/${pkg.config.addonRef}.js`,
41 | },
42 | ],
43 | // If you want to checkout update.json into the repository, uncomment the following lines:
44 | makeUpdateJson: {
45 | hash: false,
46 | },
47 | hooks: {
48 | 'build:makeUpdateJSON': (ctx) => {
49 | try {
50 | copyFileSync('.scaffold/build/update.json', 'update_gitignore.json')
51 | } catch (err) {
52 | console.log('Some Error: ', err)
53 | }
54 | try {
55 | copyFileSync('.scaffold/build/update-beta.json', 'update-beta_gitignore.json')
56 | } catch (err) {
57 | console.log('Some Error: ', err)
58 | }
59 | },
60 | },
61 | },
62 | // release: {
63 | // bumpp: {
64 | // execute: "npm run build",
65 | // },
66 | // },
67 |
68 | // If you need to see a more detailed build log, uncomment the following line:
69 | // logLevel: "trace",
70 | })
71 |
--------------------------------------------------------------------------------