Skip to content

feat: add inject_citations tool for Word .docx citation injection#85

Open
mlenburg wants to merge 3 commits into
cookjohn:mainfrom
mlenburg:feature/inject-citations
Open

feat: add inject_citations tool for Word .docx citation injection#85
mlenburg wants to merge 3 commits into
cookjohn:mainfrom
mlenburg:feature/inject-citations

Conversation

@mlenburg

Copy link
Copy Markdown

Summary

Adds a new inject_citations MCP tool that replaces <zcite key="ITEMKEY"/> placeholders in a Word .docx file with native Zotero field codes (ADDIN ZOTERO_ITEM CSL_CITATION), producing a _cited.docx that the Zotero Word Plugin can refresh and reformat in any citation style.

This addresses all feedback from the initial PR review (except the zipSync threading issue, which is deferred per your guidance):

  • Entity unescape order: &amp; is now replaced last in parseZciteTag, preventing &amp;quot; from double-unescaping
  • Fallback removed: instead of silently embedding raw XML inside <w:t> (corrupt OOXML), the injector now throws a descriptive error if a placeholder cannot be matched within a valid <w:r> run
  • <w:r> attribute matching: all run patterns use <w:r[^>]*> to match Word's attribute-bearing runs (e.g. w:rsidR)
  • Path validation: requires absolute path; rejects _cited.docx as input; refuses to overwrite existing output without overwrite=true
  • Opt-in preference gate: enableCitationInjection pref with toggle in the Preferences UI; inject_citations is hidden from the tool list when disabled
  • uris[] populated: each citation item carries a zotero://select/library/items/<KEY> URI so Zotero relinks citations on refresh rather than treating them as unlinked/embedded
  • Randomised citation IDs: Math.random().toString(36) instead of sequential cit1, cit2… to avoid collisions with existing fields
  • Bibliography placement: inserted before <w:sectPr> (OOXML requires it as the last body child)

Tests

Pure functions extracted to citationInjector.pure.ts (no Gecko dependencies) and covered by 36 Mocha + Chai unit tests:

npm test

Tests cover parseZciteTag, formatCitationText, generateCitationFieldCode, validateDocxPath, xmlEscape, and the preprocessing regex (including the non-greedy fix and <w:r> attribute matching).

Note on zipSync

unzipSync/zipSync (fflate) still run synchronously on the main thread. For typical manuscript sizes this completes in well under a second. Async decompression is deferred as noted in your review.

Marc Lenburg and others added 3 commits June 13, 2026 17:47
Adds a new `inject_citations` MCP tool that replaces `<zcite key="ITEMKEY"/>`
placeholders in a Word .docx with native Zotero field codes (ADDIN ZOTERO_ITEM
CSL_CITATION), producing a `_cited.docx` that Zotero's Word Plugin can refresh
and reformat.

Addresses all feedback from PR review:
- Fix entity-unescape order in parseZciteTag (&amp; last to avoid double-unescape)
- Fail loudly on fallback instead of emitting invalid OOXML inside <w:t>
- Path validation: require absolute path, reject _cited.docx as input
- Overwrite protection: refuse to clobber existing output without overwrite=true
- Opt-in preference gate (enableCitationInjection) with UI toggle in Preferences
- Populate uris[] with zotero://select URIs so citations link on Zotero refresh
- Randomise citationID to avoid collisions with existing fields
- Bibliography inserted before <w:sectPr> (OOXML requires it as last body child)
- <w:r[^>]*> in all run patterns to match Word's attribute-bearing runs

Pure functions extracted to citationInjector.pure.ts (no Gecko deps) and tested
with 36 Mocha+Chai unit tests covering parseZciteTag, formatCitationText,
generateCitationFieldCode, validateDocxPath, xmlEscape, and the preprocessing
regex. Run with: npm test

Note: fflate (zip read/write) still runs synchronously on the main thread; async
decompression deferred per maintainer guidance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace `import { PathUtils } from "resource://gre/modules/PathUtils.sys.mjs"`
  with `declare const PathUtils: any` / `declare const IOUtils: any` — esbuild
  cannot resolve resource:// URLs at bundle time; they are ambient globals in
  the Zotero/Gecko runtime
- Cast response.body to string in callInjectCitations to satisfy strict TS
- Add generated i10n type entries for pref-citation-injection-* FTL keys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Zotero.Prefs.get() without a default returns undefined for unset prefs,
causing !enabled to throw even when the toggle is visibly on in the UI.
The gate in streamableMCPServer.ts (which passes true as default) is
sufficient — remove the duplicate check from citationInjector.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant