What problem are you trying to solve?
Type: chore
Affected files: src/modules/convert.ts, src/modules/hooks.ts, src/utils/frontmatter.ts, src/utils/window.ts, src/utils/prefs.ts, test/, .env.example, package.json, .github/workflows/ci.yml
Summary
A bundle of small hygiene improvements that surfaced during a code review. Each individual item is too small to warrant its own issue but together they tighten the codebase meaningfully. The bundle is organized into three sub-tasks; a single PR addressing all three is reasonable, or they can be picked off independently.
Sub-task 1: Remove dead code
src/utils/frontmatter.ts exports stripExistingFrontmatter but no caller imports it. Either remove the export, or wire it in at the natural integration point — step 7 of convertAttachmentInner in convert.ts, just before prepending the new frontmatter — to handle the (unusual but not impossible) case where docling-serve returns markdown that already begins with a YAML block. Removal is the smaller change; wiring it in is the more conservative choice. Pick one.
src/utils/window.ts exports isWindowAlive but no caller imports it. Remove the file unless a planned consumer exists in the near-term roadmap.
src/hooks.ts defines an empty onNotify function labeled "Phase 2: auto-convert notifier lands here." The actual notifier registration lives in src/modules/notifier.ts and goes through Zotero.Notifier.registerObserver. The empty onNotify is leftover scaffold and will confuse contributors looking for the notifier logic. Remove it from hooks.ts and from the default export object.
The inFlightItems dedupe in src/modules/convert.ts is somewhat redundant with the batch-level batchInFlight flag in addon.data, since the latter prevents a second batch from starting at all. Keep inFlightItems (it provides defence-in-depth against callers from new code paths) but add a JSDoc comment explaining why both exist.
Sub-task 2: Small validations and polish
Server URL validation. The current code in testServerConnection only strips trailing slashes. If a user pastes localhost:5001 (missing scheme) or http://localhost:5001/api (extra path), the resulting fetch calls fail with confusing errors. Validate the URL via the URL constructor inside testServerConnection and surface a concrete error message if it's not a valid HTTP(S) URL with no path component. Block save in the preferences pane on invalid input with a red-bordered field, matching the existing pattern for the OCR languages field.
Surface conversion duration in the toast. docling-serve returns processing_time in its response and the code logs it but doesn't display it. For a batch completion toast, format as "OK 28 · skipped 2 · failed 0 — took 4m 12s". The sum of processing_time across all items in the batch is a reasonable proxy. Aggregating happens in runBatch in menu.ts and the parallel path in notifier.ts.
.env.example formatting. Switch KEY = value (spaces around =) to KEY=value. dotenv tolerates the current form but some parsers don't.
Node version pinning. Either add "engines": { "node": ">=20" } to package.json or commit a .nvmrc with 20. Document in CONTRIBUTING.md that Node 20+ is required. The CI workflow already uses setup-js@main which pins implicitly; explicit pinning helps local contributors.
Typed pref accessor. Number(getPref(...)) patterns appear in several places with ?? default fallbacks. Consider a small wrapper getPrefAs<T>(key, default) in src/utils/prefs.ts that handles the coercion and default in one call site, reducing the surface area for the Zotero.Prefs.get "undefined during initial install" edge case.
Sub-task 3: Expand test coverage
The current test/ directory has one smoke test confirming the plugin instance is defined. The codebase has several pure functions that are good candidates for unit testing without requiring a live Zotero environment.
buildConvertForm (src/modules/convert.ts). Inputs are bytes, filename, and the Web APIs object. Outputs are form fields. Good test cases:
- Default preferences produce the expected base set of fields.
- A valid Advanced JSON payload correctly overrides base fields (the JSON merge is documented to win).
- An invalid Advanced JSON payload throws a typed error.
ocrLang with multiple comma-separated values produces multiple ocr_lang form fields, not a single comma-joined one.
- A nested object value in Advanced JSON is JSON-stringified, not flattened.
buildFrontmatter (src/utils/frontmatter.ts). Inputs are a mocked Zotero.Item. Outputs are YAML text. Good test cases:
- A title containing a double quote is correctly escaped.
- A title containing a backslash is correctly escaped.
- Multiple authors are emitted as a YAML inline list.
- A creator with only
lastName produces a single-name entry, not lastName, undefined.
- An item with no metadata returns an empty string, not a stub
---\n---\n.
- A
date field of "April 2024" extracts year: 2024.
- An empty parent returns an empty string.
stripExistingFrontmatter (src/utils/frontmatter.ts). If the function is kept (per sub-task 1):
- A markdown starting with
---\nfoo: bar\n---\nbody returns body.
- A markdown not starting with
--- is returned unchanged.
- A markdown with
--- but no closing --- is returned unchanged.
truncateMiddle (src/utils/format.ts). Edge cases:
- A string shorter than
max is returned unchanged.
max ≤ 3 returns the first max characters with no ellipsis.
- A balanced truncation produces head and tail of roughly equal length.
These are all plain Mocha tests that can run via the existing npm run test infrastructure. They need only minimal mocks (a FormData/Blob polyfill from Node's built-in undici, and a small stub for the Zotero.Item shape used by buildFrontmatter). The result is a meaningful regression net for any future changes to the form-builder logic.
Acceptance criteria
Sub-task 1.
stripExistingFrontmatter is either removed or has at least one caller.
isWindowAlive is either removed or has at least one caller.
- The empty
onNotify in hooks.ts is removed from both the implementation and the default export.
inFlightItems retains a JSDoc comment explaining its relationship to batchInFlight.
Sub-task 2.
- Pasting
localhost:5001 (missing scheme) into the server URL field produces an actionable error.
- Batch-completion toasts include total processing time when at least one item completed successfully.
.env.example uses KEY=value (no spaces).
package.json declares a Node version requirement.
Sub-task 3.
- At least the four test files listed above exist in
test/ with the test cases described.
npm run test passes locally and in CI.
- The CI workflow is updated if needed to install any new test-only dev dependencies.
Notes for the implementer
These sub-tasks are independently mergeable. If splitting into separate PRs, ordering would be (1) dead-code removal first (no behaviour change, easy review), (2) test coverage second (locks in current behaviour before any refactor), (3) small validations last (now backed by tests).
Proposed solution (optional)
No response
Alternatives considered
No response
How much would this affect your workflow?
None
Anything else?
No response
What problem are you trying to solve?
Type: chore
Affected files:
src/modules/convert.ts,src/modules/hooks.ts,src/utils/frontmatter.ts,src/utils/window.ts,src/utils/prefs.ts,test/,.env.example,package.json,.github/workflows/ci.ymlSummary
A bundle of small hygiene improvements that surfaced during a code review. Each individual item is too small to warrant its own issue but together they tighten the codebase meaningfully. The bundle is organized into three sub-tasks; a single PR addressing all three is reasonable, or they can be picked off independently.
Sub-task 1: Remove dead code
src/utils/frontmatter.tsexportsstripExistingFrontmatterbut no caller imports it. Either remove the export, or wire it in at the natural integration point — step 7 ofconvertAttachmentInnerinconvert.ts, just before prepending the new frontmatter — to handle the (unusual but not impossible) case wheredocling-servereturns markdown that already begins with a YAML block. Removal is the smaller change; wiring it in is the more conservative choice. Pick one.src/utils/window.tsexportsisWindowAlivebut no caller imports it. Remove the file unless a planned consumer exists in the near-term roadmap.src/hooks.tsdefines an emptyonNotifyfunction labeled "Phase 2: auto-convert notifier lands here." The actual notifier registration lives insrc/modules/notifier.tsand goes throughZotero.Notifier.registerObserver. The emptyonNotifyis leftover scaffold and will confuse contributors looking for the notifier logic. Remove it fromhooks.tsand from the default export object.The
inFlightItemsdedupe insrc/modules/convert.tsis somewhat redundant with the batch-levelbatchInFlightflag inaddon.data, since the latter prevents a second batch from starting at all. KeepinFlightItems(it provides defence-in-depth against callers from new code paths) but add a JSDoc comment explaining why both exist.Sub-task 2: Small validations and polish
Server URL validation. The current code in
testServerConnectiononly strips trailing slashes. If a user pasteslocalhost:5001(missing scheme) orhttp://localhost:5001/api(extra path), the resultingfetchcalls fail with confusing errors. Validate the URL via theURLconstructor insidetestServerConnectionand surface a concrete error message if it's not a valid HTTP(S) URL with no path component. Block save in the preferences pane on invalid input with a red-bordered field, matching the existing pattern for the OCR languages field.Surface conversion duration in the toast.
docling-servereturnsprocessing_timein its response and the code logs it but doesn't display it. For a batch completion toast, format as"OK 28 · skipped 2 · failed 0 — took 4m 12s". The sum ofprocessing_timeacross all items in the batch is a reasonable proxy. Aggregating happens inrunBatchinmenu.tsand the parallel path innotifier.ts..env.exampleformatting. SwitchKEY = value(spaces around=) toKEY=value. dotenv tolerates the current form but some parsers don't.Node version pinning. Either add
"engines": { "node": ">=20" }topackage.jsonor commit a.nvmrcwith20. Document in CONTRIBUTING.md that Node 20+ is required. The CI workflow already usessetup-js@mainwhich pins implicitly; explicit pinning helps local contributors.Typed pref accessor.
Number(getPref(...))patterns appear in several places with?? defaultfallbacks. Consider a small wrappergetPrefAs<T>(key, default)insrc/utils/prefs.tsthat handles the coercion and default in one call site, reducing the surface area for theZotero.Prefs.get"undefined during initial install" edge case.Sub-task 3: Expand test coverage
The current
test/directory has one smoke test confirming the plugin instance is defined. The codebase has several pure functions that are good candidates for unit testing without requiring a live Zotero environment.buildConvertForm(src/modules/convert.ts). Inputs are bytes, filename, and the Web APIs object. Outputs are form fields. Good test cases:ocrLangwith multiple comma-separated values produces multipleocr_langform fields, not a single comma-joined one.buildFrontmatter(src/utils/frontmatter.ts). Inputs are a mockedZotero.Item. Outputs are YAML text. Good test cases:lastNameproduces a single-name entry, notlastName, undefined.---\n---\n.datefield of"April 2024"extractsyear: 2024.stripExistingFrontmatter(src/utils/frontmatter.ts). If the function is kept (per sub-task 1):---\nfoo: bar\n---\nbodyreturnsbody.---is returned unchanged.---but no closing---is returned unchanged.truncateMiddle(src/utils/format.ts). Edge cases:maxis returned unchanged.max≤ 3 returns the firstmaxcharacters with no ellipsis.These are all plain Mocha tests that can run via the existing
npm run testinfrastructure. They need only minimal mocks (aFormData/Blobpolyfill from Node's built-inundici, and a small stub for theZotero.Itemshape used bybuildFrontmatter). The result is a meaningful regression net for any future changes to the form-builder logic.Acceptance criteria
Sub-task 1.
stripExistingFrontmatteris either removed or has at least one caller.isWindowAliveis either removed or has at least one caller.onNotifyinhooks.tsis removed from both the implementation and the default export.inFlightItemsretains a JSDoc comment explaining its relationship tobatchInFlight.Sub-task 2.
localhost:5001(missing scheme) into the server URL field produces an actionable error..env.exampleusesKEY=value(no spaces).package.jsondeclares a Node version requirement.Sub-task 3.
test/with the test cases described.npm run testpasses locally and in CI.Notes for the implementer
These sub-tasks are independently mergeable. If splitting into separate PRs, ordering would be (1) dead-code removal first (no behaviour change, easy review), (2) test coverage second (locks in current behaviour before any refactor), (3) small validations last (now backed by tests).
Proposed solution (optional)
No response
Alternatives considered
No response
How much would this affect your workflow?
None
Anything else?
No response