refactor: replace mammoth with native DOCX parser using fflate

Remove mammoth dependency and implement lightweight DOCX text extraction using fflate + DOMParser. Extracts text from body, headers/footers, and footnotes/endnotes by parsing w:p/w:t elements. Make readDocument() synchronous. Add PDF.js type definitions to eliminate unsafe any access.
This commit is contained in:
Andrew Beal 2026-06-28 15:08:50 +01:00
parent cf037a09de
commit 6835c9167d
8 changed files with 68 additions and 243 deletions

View file

@ -1,13 +1,30 @@
import { loadPdfJs } from 'obsidian';
import { unzipSync, strFromU8 } from 'fflate';
import mammoth from 'mammoth/mammoth.browser.js';
import type { IPageText } from '../Types/SearchTypes';
import { Exception } from './Exception';
// Minimal structural types for the slice of PDF.js we use. Obsidian's `loadPdfJs()`
// is typed as `Promise<any>` and we don't bundle pdfjs-dist, so we describe just the
// shape we touch to keep this file free of unsafe-`any` access.
interface PdfTextItem { str?: string; }
interface PdfTextContent { items: PdfTextItem[]; }
interface PdfPage {
getTextContent(): Promise<PdfTextContent>;
cleanup?(): void;
}
interface PdfDocument {
numPages: number;
getPage(pageNumber: number): Promise<PdfPage>;
destroy?(): Promise<void>;
}
interface PdfJs {
getDocument(src: { data: Uint8Array }): { promise: Promise<PdfDocument> };
}
// Uses Obsidian's own bundled PDF.js via loadPdfJs() rather than shipping our own
export async function readPDF(arrayBuffer: ArrayBuffer): Promise<IPageText[]> {
try {
const pdfjs = await loadPdfJs();
const pdfjs = (await loadPdfJs()) as PdfJs;
const pdf = await pdfjs.getDocument({ data: new Uint8Array(arrayBuffer) }).promise;
const pageTexts: IPageText[] = [];
@ -15,7 +32,7 @@ export async function readPDF(arrayBuffer: ArrayBuffer): Promise<IPageText[]> {
const page = await pdf.getPage(i);
const content = await page.getTextContent();
const text = content.items
.map((item: { str?: string }) => item.str ?? '')
.map((item) => item.str ?? '')
.join(' ');
pageTexts.push({ text, pageNumber: i });
page.cleanup?.();
@ -41,16 +58,15 @@ export async function readPDF(arrayBuffer: ArrayBuffer): Promise<IPageText[]> {
// Handles document formats: DOCX, PPTX, XLSX, ODT, ODP, ODS.
//
// DOCX goes through mammoth (battle-tested: tables, lists, footnotes). The remaining
// formats are ZIP-of-XML containers we read with fflate + the platform DOMParser, so
// no heavy parser is bundled. We only extract plain text (no OCR, no images) — the LLM
// can read files directly and use its own vision engine when it needs to.
export async function readDocument(arrayBuffer: ArrayBuffer, extension: string): Promise<IPageText[]> {
// Every format is a ZIP-of-XML container we read with fflate + the platform DOMParser,
// so no heavy parser is bundled. We only extract plain text (no OCR, no images) — the
// LLM can read files directly and use its own vision engine when it needs to.
export function readDocument(arrayBuffer: ArrayBuffer, extension: string): IPageText[] {
try {
let text: string;
switch (extension.toLowerCase()) {
case 'docx':
text = (await mammoth.extractRawText({ arrayBuffer })).value;
text = extractDocx(arrayBuffer);
break;
case 'xlsx':
text = extractXlsx(arrayBuffer);
@ -74,7 +90,7 @@ export async function readDocument(arrayBuffer: ArrayBuffer, extension: string):
}
}
// --- Shared helpers for the ZIP-of-XML formats (xlsx / pptx / odf) ---
// --- Shared helpers for the ZIP-of-XML formats (docx / xlsx / pptx / odf) ---
// Throws on legacy OLE2 binaries (.xls/.ppt/.doc) — they aren't ZIPs. Callers wrap
// this so the failure surfaces as a clean "failed to read" message.
@ -92,6 +108,40 @@ function ordinal(path: string): number {
return match ? Number(match[1]) : 0;
}
// --- DOCX: body in word/document.xml; <w:p> = paragraph, <w:t> = text run.
// Headers/footers and foot/endnotes live in sibling parts that share the same
// <w:p>/<w:t> shape, so they run through the same paragraph extractor. ---
function extractDocx(arrayBuffer: ArrayBuffer): string {
const files = unzip(arrayBuffer);
// Joined text of every <w:p> paragraph in a parsed part. Runs are joined with
// '' (not a space) because Word splits a single word across multiple <w:t> runs
// for spell-check / formatting — a space here would shred words like "Hel"+"lo".
// Empty paragraphs (incl. the separator entries in foot/endnotes) are dropped.
const paragraphs = (bytes: Uint8Array): string =>
Array.from(parseXml(bytes).getElementsByTagName('w:p'))
.map((p) =>
Array.from(p.getElementsByTagName('w:t'))
.map((t) => t.textContent ?? '')
.join('')
)
.filter((line) => line.length > 0)
.join('\n');
// Body first, then headers/footers (grouped + numerically ordered), then notes.
const headerFooter = Object.keys(files)
.filter((name) => /^word\/(header|footer)\d+\.xml$/.test(name))
.sort();
const parts: string[] = [];
if (files['word/document.xml']) parts.push(paragraphs(files['word/document.xml']));
for (const name of headerFooter) parts.push(paragraphs(files[name]));
if (files['word/footnotes.xml']) parts.push(paragraphs(files['word/footnotes.xml']));
if (files['word/endnotes.xml']) parts.push(paragraphs(files['word/endnotes.xml']));
return parts.filter((p) => p.length > 0).join('\n\n');
}
// --- XLSX: values live as indices into a shared-strings table ---
function extractXlsx(arrayBuffer: ArrayBuffer): string {
const files = unzip(arrayBuffer);
@ -172,4 +222,4 @@ function extractOdf(arrayBuffer: ArrayBuffer): string {
}
}
return out.join('\n');
}
}

View file

@ -410,8 +410,7 @@ This plugin is built on the shoulders of many excellent projects:
- Official SDKs: [@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript), [@google/genai](https://github.com/google/generative-ai-js), [openai](https://github.com/openai/openai-node)
**Document Processing**
- [mammoth](https://github.com/mwilliamson/mammoth.js) - DOCX text extraction
- [fflate](https://github.com/101arrowz/fflate) - Unzipping Office Open XML / OpenDocument files (PPTX, XLSX, ODT, ODP, ODS)
- [fflate](https://github.com/101arrowz/fflate) - Unzipping Office Open XML / OpenDocument files for text extraction (DOCX, PPTX, XLSX, ODT, ODP, ODS)
- PDF text extraction via Obsidian's bundled [PDF.js](https://mozilla.github.io/pdf.js/) (`loadPdfJs()`)
**UI Framework**

View file

@ -48,7 +48,7 @@ export class InputService {
}
if (isDocumentFile(fileType)) {
const content = await readDocument(await file.arrayBuffer(), fileType);
const content = readDocument(await file.arrayBuffer(), fileType);
attachments.push(new Attachment(
file.name,
MimeType.TEXT_PLAIN,

View file

@ -98,7 +98,7 @@ export class VaultService {
if (isDocumentFile(fileExtension)) {
const arrayBuffer = await this.readBinaryData(file, allowAccessToPluginRoot);
if (arrayBuffer) {
return (await readDocument(arrayBuffer, fileExtension))[0].text;
return (readDocument(arrayBuffer, fileExtension))[0].text;
}
}
@ -418,7 +418,7 @@ export class VaultService {
content = await readPDF(arrayBuffer);
} else if (isDocumentFile(fileExtension)) {
const arrayBuffer = await this.vault.readBinary(file);
content = await readDocument(arrayBuffer, fileExtension);
content = readDocument(arrayBuffer, fileExtension);
} else {
content = [{ text: await this.vault.cachedRead(file), pageNumber: 1 }] as IPageText[];
}

View file

@ -43,11 +43,6 @@ function copyDir(src, dest) {
//
// All such literals come from dependencies, never from our own source, and every
// one is dead code in our usage:
// - bluebird (transitive via mammoth): 7x promise-method JIT compilers, ALL
// gated behind `canEvaluate = typeof navigator == "undefined"`. Obsidian always
// defines `navigator`, so `canEvaluate` is false and bluebird takes its pure-JS
// fallbacks — the `new Function` branches are never entered. (call_get.js x2,
// join.js x3, promisify.js x2.)
// - diff2html/@profoundlogic/hogan: 2x template compilers reachable only via
// Hogan.compile(), which diff2html invokes only for runtime `rawTemplates`.
// We use diff2html's precompiled defaults, so these never run.
@ -57,8 +52,8 @@ function copyDir(src, dest) {
// build FAILS if the count drifts from EXPECTED_NEW_FUNCTION_COUNT, so a dependency
// change can never silently reintroduce a live (or un-neutralised) dynamic-eval path.
//
// 9 = 7 bluebird (canEvaluate-gated) + 2 Hogan.
const EXPECTED_NEW_FUNCTION_COUNT = 9;
// 2 = Hogan.
const EXPECTED_NEW_FUNCTION_COUNT = 2;
const NEW_FUNCTION_STUB = "VKBlockedDynamicFn";
function neutraliseDynamicEval(outfile) {
if (!existsSync(outfile)) return;

10
global.d.ts vendored
View file

@ -11,12 +11,4 @@ declare module '*.svg' {
declare module '*.png' {
const content: string;
export default content;
}
// mammoth ships type declarations for its default entry but not for the browser
// build we import (mammoth/mammoth.browser.js) for mobile safety. The browser build
// exposes the same public API, so we re-use mammoth's own types.
declare module 'mammoth/mammoth.browser.js' {
import mammoth from 'mammoth';
export = mammoth;
}
}

210
package-lock.json generated
View file

@ -21,7 +21,6 @@
"highlight.js": "^11.11.1",
"katex": "^0.17.0",
"lowlight": "^3.3.0",
"mammoth": "^1.12.0",
"openai": "^6.45.0",
"path-browserify": "^1.0.1",
"regex-parser": "^2.3.1",
@ -2299,15 +2298,6 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.13",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@ -2671,12 +2661,6 @@
"node": "*"
}
},
"node_modules/bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
@ -3016,12 +3000,6 @@
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/crelt": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
@ -3250,12 +3228,6 @@
"node": ">=0.3.1"
}
},
"node_modules/dingbat-to-unicode": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
"license": "BSD-2-Clause"
},
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@ -3276,15 +3248,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/duck": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",
"integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
"license": "BSD",
"dependencies": {
"underscore": "^1.13.1"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -5390,12 +5353,6 @@
"node": ">= 4"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@ -6084,18 +6041,6 @@
"node": ">=4.0"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
@ -6157,15 +6102,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
@ -6488,17 +6424,6 @@
"loose-envify": "cli.js"
}
},
"node_modules/lop": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz",
"integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
"license": "BSD-2-Clause",
"dependencies": {
"duck": "^0.1.12",
"option": "~0.2.1",
"underscore": "^1.13.1"
}
},
"node_modules/lowlight": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
@ -6534,39 +6459,6 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/mammoth": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz",
"integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==",
"license": "BSD-2-Clause",
"dependencies": {
"@xmldom/xmldom": "^0.8.6",
"argparse": "~1.0.3",
"base64-js": "^1.5.1",
"bluebird": "~3.4.0",
"dingbat-to-unicode": "^1.0.1",
"jszip": "^3.7.1",
"lop": "^0.4.2",
"path-is-absolute": "^1.0.0",
"underscore": "^1.13.1",
"xmlbuilder": "^10.0.0"
},
"bin": {
"mammoth": "bin/mammoth"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/mammoth/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -7137,12 +7029,6 @@
}
}
},
"node_modules/option": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz",
"integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==",
"license": "BSD-2-Clause"
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -7224,12 +7110,6 @@
"node": ">=8"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -7268,15 +7148,6 @@
"node": ">=8"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@ -7395,12 +7266,6 @@
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@ -7527,33 +7392,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readable-stream/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/readable-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@ -7986,12 +7824,6 @@
"node": ">= 0.4"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@ -8154,12 +7986,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause"
},
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@ -8207,21 +8033,6 @@
"node": ">= 0.4"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string_decoder/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/string.prototype.matchall": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
@ -8872,12 +8683,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/underscore": {
"version": "1.13.8",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
"license": "MIT"
},
"node_modules/undici-types": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
@ -8990,12 +8795,6 @@
"punycode": "^2.1.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/uuid": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz",
@ -9400,15 +9199,6 @@
}
}
},
"node_modules/xmlbuilder": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
"integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/yaml": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",

View file

@ -57,7 +57,6 @@
"highlight.js": "^11.11.1",
"katex": "^0.17.0",
"lowlight": "^3.3.0",
"mammoth": "^1.12.0",
"openai": "^6.45.0",
"path-browserify": "^1.0.1",
"regex-parser": "^2.3.1",