first commit

This commit is contained in:
Rhys 2026-05-03 19:23:16 +02:00
commit 2eb6d5f3a4
9 changed files with 1484 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules/
main.js
*.js.map

188
README.md Normal file
View file

@ -0,0 +1,188 @@
# DocDrop
An [Obsidian](https://obsidian.md) plugin that converts PDF files to Markdown using Microsoft's [MarkItDown](https://github.com/microsoft/markitdown) CLI tool.
Right-click any PDF in your vault and have it converted to a clean Markdown file in seconds — entirely on your machine, no cloud required (unless you opt in to the advanced AI features).
---
## Requirements
- **Obsidian** 1.0.0 or later (desktop only — this plugin uses Node.js `child_process` and cannot run on Obsidian Mobile)
- **Python 3.10+**
- **MarkItDown** CLI installed:
```bash
pip install markitdown
```
Verify installation:
```bash
markitdown --version
```
**Optional — for AI-powered OCR of scanned PDFs:**
```bash
pip install markitdown-ocr openai
```
---
## Installation
### From the Obsidian Community Plugin Browser (recommended)
1. Open Obsidian → **Settings** → **Community Plugins**
2. Disable Safe Mode if prompted
3. Click **Browse** and search for **DocDrop**
4. Click **Install**, then **Enable**
### Manual installation
1. Download `main.js`, `manifest.json`, and `styles.css` from the [latest release](https://github.com/rgottwald/docdrop/releases)
2. Copy them into your vault at:
```
<your-vault>/.obsidian/plugins/docdrop/
```
3. Reload Obsidian and enable the plugin under **Settings → Community Plugins**
### Build from source
```bash
git clone https://github.com/rgottwald/docdrop
cd docdrop
npm install
npm run build
```
Then copy `main.js`, `manifest.json`, and `styles.css` into your vault's plugin folder as above.
---
## Usage
### Right-click menu
Right-click any PDF file in the Obsidian file explorer and select **Convert to Markdown with DocDrop**. The converted `.md` file is saved in the same folder as the PDF (or a custom folder — see Settings).
### Command palette
Open the command palette (`Cmd/Ctrl+P`) and run:
```
DocDrop: Convert active PDF to Markdown
```
This converts whichever PDF file is currently open/active.
### Output file
The output file is named identically to the PDF but with a `.md` extension. If a file with that name already exists, you will be prompted to confirm before overwriting.
---
## Settings
Open **Settings → DocDrop** to configure the plugin.
### General
| Setting | Description |
|---|---|
| **Executable path** | Full path to the `markitdown` binary, or just `markitdown` if it is on your system PATH. Change this if you get a "command not found" error — set it to the full path, e.g. `/Library/Frameworks/Python.framework/Versions/3.12/bin/markitdown`. |
| **Output location** | Where converted Markdown files are saved. "Same folder as PDF" places the `.md` file next to the source PDF. "Custom folder" lets you specify any vault-relative folder (it must already exist). |
### Conversion options
| Setting | Description |
|---|---|
| **Keep images** | Embeds images from the PDF as base64 data directly in the Markdown file. This preserves visuals at the cost of a much larger output file. Leave off if you only need the text content. |
| **MIME type hint** | Tells MarkItDown what kind of file it is receiving (e.g. `application/pdf`). Almost never needed — MarkItDown detects the file type automatically. Only set this if conversion produces wrong results. |
| **Character encoding hint** | Tells MarkItDown what text encoding to use (e.g. `UTF-8`, `ISO-8859-1`). Leave blank unless converted text contains garbled or misread characters. |
### markitdown-ocr plugin (optional)
[markitdown-ocr](https://github.com/microsoft/markitdown) is a separately-installed plugin that adds AI-powered OCR — it uses a vision-capable language model (such as OpenAI GPT-4o) to read text from images inside PDFs. This is especially useful for scanned documents or PDFs that are essentially images of pages rather than text.
**Install the plugin first:**
```bash
pip install markitdown-ocr
```
| Setting | Description |
|---|---|
| **Enable markitdown-ocr** | Activates all installed MarkItDown plugins, including markitdown-ocr. Has no effect if the plugin is not installed. |
| **OpenAI API key** | Your secret key from OpenAI (or a compatible provider). Required for markitdown-ocr to call the vision model. Get one at [platform.openai.com](https://platform.openai.com) → API keys. Looks like `sk-proj-...`. |
| **AI model** | The vision model markitdown-ocr uses to read images. Must support image/vision input. Default: `gpt-4o`. Cheaper alternative: `gpt-4o-mini` (slightly less accurate). |
| **OpenAI API base URL** | Override the API server. Leave blank for the default OpenAI servers. Set this if you use Azure OpenAI (e.g. `https://your-resource.openai.azure.com/`) or a self-hosted compatible service. |
> **Cost note:** markitdown-ocr sends image data to OpenAI's API, which is billed per token/image. For large or image-heavy PDFs this can add up. Check [OpenAI pricing](https://openai.com/pricing) before enabling on many files.
### Azure Document Intelligence (optional)
[Azure AI Document Intelligence](https://azure.microsoft.com/en-us/products/ai-services/ai-document-intelligence) is a paid Microsoft Azure cloud service that uses AI to extract text from PDFs with significantly higher accuracy than offline conversion — especially for scanned documents, handwritten text, tables, and complex multi-column layouts.
Requires an active Microsoft Azure subscription with the Document Intelligence resource provisioned.
| Setting | Description |
|---|---|
| **Use Document Intelligence** | Send the PDF to Azure for conversion instead of processing it locally. Requires internet access and a paid Azure subscription. |
| **Endpoint URL** | The URL of your Azure Document Intelligence resource. Find it in the [Azure Portal](https://portal.azure.com): open your Document Intelligence resource → **Keys and Endpoint**. Looks like `https://your-resource-name.cognitiveservices.azure.com/`. |
| **API key** | The secret key that authenticates with Azure. Found in the same place: Azure Portal → your Document Intelligence resource → **Keys and Endpoint****KEY 1** or **KEY 2**. Either key works. |
> **Cost note:** Azure Document Intelligence bills per page processed. Review [Azure pricing](https://azure.microsoft.com/en-us/pricing/details/ai-document-intelligence/) before use.
---
## Troubleshooting
**"ENOENT" or "command not found" error**
MarkItDown cannot be found. Obsidian's process does not inherit your shell PATH. Solutions:
- Set **Executable path** in plugin settings to the full absolute path, e.g.:
`/Library/Frameworks/Python.framework/Versions/3.12/bin/markitdown`
- Find the path by running `which markitdown` in your terminal.
**Converted file has garbled characters**
Try setting the **Character encoding hint** to `UTF-8` in settings.
**Scanned PDF produces no text (or very little)**
The PDF contains images of pages rather than real text. Enable **markitdown-ocr** (with a valid OpenAI API key) or **Azure Document Intelligence** for AI-powered OCR.
**Azure Document Intelligence returns 401 Unauthorized**
Your API key is incorrect or has been regenerated. Copy a fresh key from the Azure Portal → your Document Intelligence resource → Keys and Endpoint.
---
## Privacy & data
- **Default (offline) conversion:** All processing happens locally on your machine. No data leaves your computer.
- **markitdown-ocr:** PDF image data is sent to OpenAI's API (or your configured base URL). Governed by [OpenAI's privacy policy](https://openai.com/policies/privacy-policy).
- **Azure Document Intelligence:** PDF data is sent to Microsoft Azure. Governed by [Microsoft's privacy policy](https://privacy.microsoft.com) and your Azure service agreement.
- **API keys** are stored in your vault at `.obsidian/plugins/docdrop/data.json`. Do not commit this file to a public repository.
---
## Credits
This plugin would not be possible without:
- **[MarkItDown](https://github.com/microsoft/markitdown)** by [Microsoft](https://github.com/microsoft) — the core CLI tool that powers all PDF-to-Markdown conversion. Created by Adam Fourney and the Microsoft AutoGen team. Licensed under the MIT License.
- **[markitdown-ocr](https://github.com/microsoft/markitdown)** by [Microsoft](https://github.com/microsoft) — the optional OCR plugin for AI-powered image text extraction. Licensed under the MIT License.
- **[Obsidian](https://obsidian.md)** by Obsidian — the extensible knowledge base application this plugin is built for.
- **[Azure AI Document Intelligence](https://azure.microsoft.com/en-us/products/ai-services/ai-document-intelligence)** by Microsoft — the optional cloud AI service for high-accuracy document processing.
- **[OpenAI](https://openai.com)** — the AI platform used by markitdown-ocr for vision-based OCR.
---
## License
MIT License. See [LICENSE](LICENSE) for details.
This plugin is not affiliated with, endorsed by, or officially supported by Microsoft, Obsidian, or OpenAI.

40
esbuild.config.mjs Normal file
View file

@ -0,0 +1,40 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const prod = process.argv[2] === "production";
const context = await esbuild.context({
entryPoints: ["src/main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
minify: prod,
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}

10
manifest.json Normal file
View file

@ -0,0 +1,10 @@
{
"id": "docdrop",
"name": "DocDrop",
"version": "1.0.0",
"minAppVersion": "1.0.0",
"description": "Convert PDF files to Markdown using the markitdown CLI.",
"author": "Rhys Gottwald",
"authorUrl": "",
"isDesktopOnly": true
}

648
package-lock.json generated Normal file
View file

@ -0,0 +1,648 @@
{
"name": "markitdown-pdf-converter",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "markitdown-pdf-converter",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"obsidian": "latest"
},
"devDependencies": {
"@types/node": "^22.0.0",
"builtin-modules": "^4.0.0",
"esbuild": "^0.25.5",
"typescript": "^5.8.3"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.38.6",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT",
"peer": true
},
"node_modules/@types/codemirror": {
"version": "5.60.8",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz",
"integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==",
"license": "MIT",
"dependencies": {
"@types/tern": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.19.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
"integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/tern": {
"version": "0.23.9",
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
"integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==",
"license": "MIT",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/builtin-modules": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz",
"integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT",
"peer": true
},
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/netbsd-x64": "0.25.12",
"@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.25.12",
"@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/sunos-x64": "0.25.12",
"@esbuild/win32-arm64": "0.25.12",
"@esbuild/win32-ia32": "0.25.12",
"@esbuild/win32-x64": "0.25.12"
}
},
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/obsidian": {
"version": "1.12.3",
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz",
"integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==",
"license": "MIT",
"dependencies": {
"@types/codemirror": "5.60.8",
"moment": "2.29.4"
},
"peerDependencies": {
"@codemirror/state": "6.5.0",
"@codemirror/view": "6.38.6"
}
},
"node_modules/style-mod": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
"license": "MIT",
"peer": true
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT",
"peer": true
}
}
}

21
package.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "docdrop",
"version": "1.0.0",
"description": "Obsidian plugin: convert PDFs to Markdown via Microsoft's markitdown",
"main": "main.js",
"type": "module",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc --noEmit --skipLibCheck && node esbuild.config.mjs production"
},
"license": "MIT",
"dependencies": {
"obsidian": "latest"
},
"devDependencies": {
"@types/node": "^22.0.0",
"builtin-modules": "^4.0.0",
"esbuild": "^0.25.5",
"typescript": "^5.8.3"
}
}

556
src/main.ts Normal file
View file

@ -0,0 +1,556 @@
import {
App,
Modal,
Notice,
Plugin,
PluginSettingTab,
Setting,
TAbstractFile,
TFile,
TFolder,
} from "obsidian";
import { execFile } from "child_process";
// ─── Settings ────────────────────────────────────────────────────────────────
interface DocDropSettings {
executablePath: string;
outputMode: "same" | "custom";
customOutputFolder: string;
keepDataUris: boolean;
usePlugins: boolean;
openAiApiKey: string;
openAiBaseUrl: string;
openAiModel: string;
useDocIntel: boolean;
docIntelEndpoint: string;
docIntelApiKey: string;
mimeType: string;
charset: string;
}
const DEFAULT_SETTINGS: DocDropSettings = {
executablePath: "markitdown",
outputMode: "same",
customOutputFolder: "",
keepDataUris: false,
usePlugins: false,
openAiApiKey: "",
openAiBaseUrl: "",
openAiModel: "gpt-4o",
useDocIntel: false,
docIntelEndpoint: "",
docIntelApiKey: "",
mimeType: "",
charset: "",
};
// ─── Overwrite Confirmation Modal ────────────────────────────────────────────
class OverwriteModal extends Modal {
private fileName: string;
private onConfirm: () => void;
private onCancel: () => void;
constructor(
app: App,
fileName: string,
onConfirm: () => void,
onCancel: () => void
) {
super(app);
this.fileName = fileName;
this.onConfirm = onConfirm;
this.onCancel = onCancel;
}
onOpen(): void {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h2", { text: "File already exists" });
contentEl.createEl("p", {
text: `"${this.fileName}" already exists. Overwrite it?`,
});
const buttonRow = contentEl.createDiv({ cls: "modal-button-container" });
buttonRow
.createEl("button", { text: "Overwrite", cls: "mod-warning" })
.addEventListener("click", () => {
this.close();
this.onConfirm();
});
buttonRow
.createEl("button", { text: "Cancel" })
.addEventListener("click", () => {
this.close();
this.onCancel();
});
}
onClose(): void {
this.contentEl.empty();
}
}
// ─── Settings Tab ────────────────────────────────────────────────────────────
class DocDropSettingTab extends PluginSettingTab {
plugin: DocDropPlugin;
constructor(app: App, plugin: DocDropPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h2", { text: "DocDrop" });
new Setting(containerEl)
.setName("Executable path")
.setDesc(
'Full path to the markitdown binary, or just "markitdown" if it is on your PATH.'
)
.addText((text) =>
text
.setPlaceholder("markitdown")
.setValue(this.plugin.settings.executablePath)
.onChange(async (value) => {
this.plugin.settings.executablePath = value.trim() || "markitdown";
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Output location")
.setDesc("Where to save converted Markdown files.")
.addDropdown((drop) =>
drop
.addOption("same", "Same folder as PDF")
.addOption("custom", "Custom folder")
.setValue(this.plugin.settings.outputMode)
.onChange(async (value) => {
this.plugin.settings.outputMode = value as "same" | "custom";
await this.plugin.saveSettings();
this.display();
})
);
if (this.plugin.settings.outputMode === "custom") {
new Setting(containerEl)
.setName("Custom output folder")
.setDesc(
'Vault-relative path, e.g. "Converted". The folder must already exist.'
)
.addText((text) =>
text
.setPlaceholder("Converted")
.setValue(this.plugin.settings.customOutputFolder)
.onChange(async (value) => {
this.plugin.settings.customOutputFolder = value.trim();
await this.plugin.saveSettings();
})
);
}
containerEl.createEl("h3", { text: "Conversion options" });
new Setting(containerEl)
.setName("Keep images")
.setDesc(
"Preserve images embedded in the PDF as base64 data inside the Markdown file. " +
"Turning this on makes the output file much larger but keeps all visuals inline. " +
"Leave off if you only need the text."
)
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.keepDataUris)
.onChange(async (value) => {
this.plugin.settings.keepDataUris = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("MIME type hint")
.setDesc(
"Tells markitdown what kind of file it is receiving, e.g. \"application/pdf\". " +
"Normally not needed — markitdown detects the type automatically. " +
"Only set this if conversion produces wrong results and you suspect a detection failure."
)
.addText((text) =>
text
.setPlaceholder("application/pdf")
.setValue(this.plugin.settings.mimeType)
.onChange(async (value) => {
this.plugin.settings.mimeType = value.trim();
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Character encoding hint")
.setDesc(
"Tells markitdown what text encoding the file uses, e.g. \"UTF-8\" or \"ISO-8859-1\". " +
"Leave blank unless converted text contains garbled characters — markitdown detects encoding automatically."
)
.addText((text) =>
text
.setPlaceholder("UTF-8")
.setValue(this.plugin.settings.charset)
.onChange(async (value) => {
this.plugin.settings.charset = value.trim();
await this.plugin.saveSettings();
})
);
// ── markitdown-ocr ──────────────────────────────────────────────────────
containerEl.createEl("h3", { text: "markitdown-ocr plugin (optional)" });
containerEl.createEl("p", {
text:
"markitdown-ocr is a free, separately-installed plugin that uses an AI vision model (like " +
"OpenAI's GPT-4o) to read text from images inside PDFs — useful for scanned documents or " +
"PDFs that are just pictures of pages. Install it first with: pip install markitdown-ocr. " +
"You will need an OpenAI account (or a compatible service) to provide the AI.",
cls: "setting-item-description",
});
new Setting(containerEl)
.setName("Enable markitdown-ocr")
.setDesc(
"Activate any installed markitdown plugins, including markitdown-ocr. " +
"Has no effect if markitdown-ocr is not installed. " +
"Install it with: pip install markitdown-ocr"
)
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.usePlugins)
.onChange(async (value) => {
this.plugin.settings.usePlugins = value;
await this.plugin.saveSettings();
this.display();
})
);
if (this.plugin.settings.usePlugins) {
new Setting(containerEl)
.setName("OpenAI API key")
.setDesc(
"Your secret API key from OpenAI (or a compatible service). " +
"Required for markitdown-ocr to call the AI vision model. " +
"Get one at platform.openai.com → API keys. Looks like: sk-proj-..."
)
.addText((text) => {
text
.setPlaceholder("sk-proj-...")
.setValue(this.plugin.settings.openAiApiKey)
.onChange(async (value) => {
this.plugin.settings.openAiApiKey = value.trim();
await this.plugin.saveSettings();
});
text.inputEl.type = "password";
});
new Setting(containerEl)
.setName("AI model")
.setDesc(
"The vision-capable AI model markitdown-ocr will use to read images. " +
"Must support image/vision input. " +
"\"gpt-4o\" is the default and works well. Other options: gpt-4o-mini (cheaper, slightly less accurate)."
)
.addText((text) =>
text
.setPlaceholder("gpt-4o")
.setValue(this.plugin.settings.openAiModel)
.onChange(async (value) => {
this.plugin.settings.openAiModel = value.trim() || "gpt-4o";
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("OpenAI API base URL (optional)")
.setDesc(
"Override the API server markitdown-ocr connects to. " +
"Leave blank to use the default OpenAI servers. " +
"Set this if you are using Azure OpenAI or a self-hosted compatible service " +
"(e.g. https://your-resource.openai.azure.com/). "
)
.addText((text) =>
text
.setPlaceholder("https://api.openai.com/v1")
.setValue(this.plugin.settings.openAiBaseUrl)
.onChange(async (value) => {
this.plugin.settings.openAiBaseUrl = value.trim();
await this.plugin.saveSettings();
})
);
}
// ── Azure Document Intelligence ─────────────────────────────────────────
containerEl.createEl("h3", { text: "Azure Document Intelligence (optional)" });
containerEl.createEl("p", {
text:
"Document Intelligence is a paid Microsoft Azure cloud service that uses AI to read PDFs with " +
"much higher accuracy than offline conversion — especially for scanned documents, handwriting, " +
"tables, and complex layouts. Requires an Azure account. " +
"Leave these blank to use free offline conversion instead.",
cls: "setting-item-description",
});
new Setting(containerEl)
.setName("Use Document Intelligence")
.setDesc(
"Send the PDF to Microsoft Azure for conversion instead of processing it locally. " +
"Produces better results for scanned or image-based PDFs, but requires internet access and a paid Azure subscription."
)
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.useDocIntel)
.onChange(async (value) => {
this.plugin.settings.useDocIntel = value;
await this.plugin.saveSettings();
this.display();
})
);
if (this.plugin.settings.useDocIntel) {
new Setting(containerEl)
.setName("Endpoint URL")
.setDesc(
"The URL of your Azure Document Intelligence resource. " +
"Find it in the Azure Portal: open your Document Intelligence resource → Keys and Endpoint. " +
"Looks like: https://your-resource-name.cognitiveservices.azure.com/"
)
.addText((text) =>
text
.setPlaceholder("https://your-resource.cognitiveservices.azure.com/")
.setValue(this.plugin.settings.docIntelEndpoint)
.onChange(async (value) => {
this.plugin.settings.docIntelEndpoint = value.trim();
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("API key")
.setDesc(
"The secret key that authenticates you with Azure Document Intelligence. " +
"Find it in the Azure Portal: open your Document Intelligence resource → Keys and Endpoint → KEY 1 or KEY 2. " +
"Either key works."
)
.addText((text) => {
text
.setPlaceholder("Your Azure API key")
.setValue(this.plugin.settings.docIntelApiKey)
.onChange(async (value) => {
this.plugin.settings.docIntelApiKey = value.trim();
await this.plugin.saveSettings();
});
text.inputEl.type = "password";
});
}
}
}
// ─── Main Plugin ─────────────────────────────────────────────────────────────
export default class DocDropPlugin extends Plugin {
settings!: DocDropSettings;
async onload(): Promise<void> {
await this.loadSettings();
this.addSettingTab(new DocDropSettingTab(this.app, this));
this.addCommand({
id: "convert-active-pdf",
name: "Convert active PDF to Markdown",
callback: () => {
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile) {
new Notice("No file is currently open.");
return;
}
if (activeFile.extension.toLowerCase() !== "pdf") {
new Notice("The active file is not a PDF.");
return;
}
this.convertPdf(activeFile);
},
});
this.registerEvent(
this.app.workspace.on("file-menu", (menu, file: TAbstractFile) => {
if (!(file instanceof TFile)) return;
if (file.extension.toLowerCase() !== "pdf") return;
menu.addItem((item) => {
item
.setTitle("Convert to Markdown with DocDrop")
.setIcon("file-text")
.onClick(() => this.convertPdf(file));
});
})
);
}
onunload(): void {}
async loadSettings(): Promise<void> {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings(): Promise<void> {
await this.saveData(this.settings);
}
private async convertPdf(pdfFile: TFile): Promise<void> {
const adapter = this.app.vault.adapter;
const basePath = (adapter as any).getBasePath() as string;
const absolutePdfPath = `${basePath}/${pdfFile.path}`;
const outputVaultPath = this.resolveOutputPath(pdfFile);
if (outputVaultPath === null) {
new Notice(
`Custom output folder "${this.settings.customOutputFolder}" does not exist in the vault.`
);
return;
}
const existing = this.app.vault.getAbstractFileByPath(outputVaultPath);
if (existing instanceof TFile) {
new OverwriteModal(
this.app,
outputVaultPath,
() => this.runConversion(absolutePdfPath, outputVaultPath, existing),
() => new Notice("Conversion cancelled.")
).open();
return;
}
await this.runConversion(absolutePdfPath, outputVaultPath, null);
}
private resolveOutputPath(pdfFile: TFile): string | null {
const baseName = pdfFile.basename;
if (this.settings.outputMode === "same") {
const parentPath = pdfFile.parent?.path ?? "";
// vault root parent path is "/" in some Obsidian versions; normalise to ""
const folder = parentPath === "/" ? "" : parentPath;
return folder ? `${folder}/${baseName}.md` : `${baseName}.md`;
}
const folderPath = this.settings.customOutputFolder.replace(/\/$/, "");
if (folderPath === "") {
return `${baseName}.md`;
}
const folder = this.app.vault.getAbstractFileByPath(folderPath);
if (!(folder instanceof TFolder)) {
return null;
}
return `${folderPath}/${baseName}.md`;
}
private async runConversion(
absolutePdfPath: string,
outputVaultPath: string,
existingFile: TFile | null
): Promise<void> {
const progressNotice = new Notice(`Converting "${outputVaultPath}"…`, 0);
try {
const markdown = await this.invokeMarkItDown(absolutePdfPath);
progressNotice.hide();
if (existingFile) {
await this.app.vault.modify(existingFile, markdown);
} else {
await this.app.vault.create(outputVaultPath, markdown);
}
new Notice(`Converted: ${outputVaultPath}`, 5000);
} catch (err: unknown) {
progressNotice.hide();
const message = err instanceof Error ? err.message : String(err);
new Notice(`DocDrop failed:\n${message}`, 8000);
console.error("[DocDrop] conversion error", err);
}
}
private invokeMarkItDown(absolutePdfPath: string): Promise<string> {
const extraPaths = [
"/usr/local/bin",
"/opt/homebrew/bin",
"/opt/homebrew/sbin",
"/Library/Frameworks/Python.framework/Versions/3.12/bin",
"/Library/Frameworks/Python.framework/Versions/3.11/bin",
"/Library/Frameworks/Python.framework/Versions/3.10/bin",
"/usr/bin",
"/bin",
];
const augmentedPath = [
...(process.env.PATH?.split(":") ?? []),
...extraPaths,
].join(":");
const args: string[] = [absolutePdfPath];
if (this.settings.keepDataUris) args.push("--keep-data-uris");
if (this.settings.usePlugins) args.push("--use-plugins");
if (this.settings.mimeType) args.push("--mime-type", this.settings.mimeType);
if (this.settings.charset) args.push("--charset", this.settings.charset);
if (this.settings.useDocIntel) {
args.push("--use-docintel");
if (this.settings.docIntelEndpoint) {
args.push("--endpoint", this.settings.docIntelEndpoint);
}
}
const extraEnv: Record<string, string> = { PATH: augmentedPath };
if (this.settings.useDocIntel && this.settings.docIntelApiKey) {
extraEnv["AZURE_API_KEY"] = this.settings.docIntelApiKey;
}
if (this.settings.usePlugins && this.settings.openAiApiKey) {
extraEnv["OPENAI_API_KEY"] = this.settings.openAiApiKey;
}
if (this.settings.usePlugins && this.settings.openAiModel) {
extraEnv["OPENAI_MODEL"] = this.settings.openAiModel;
}
if (this.settings.usePlugins && this.settings.openAiBaseUrl) {
extraEnv["OPENAI_BASE_URL"] = this.settings.openAiBaseUrl;
}
return new Promise((resolve, reject) => {
execFile(
this.settings.executablePath,
args,
{
encoding: "utf8",
maxBuffer: 50 * 1024 * 1024,
env: { ...process.env, ...extraEnv },
},
(error, stdout, stderr) => {
if (error) {
const detail = stderr?.trim() || error.message;
reject(new Error(`Exit code ${error.code}: ${detail}`));
return;
}
if (stderr?.trim()) {
console.warn("[DocDrop] stderr:", stderr.trim());
}
resolve(stdout);
}
);
});
}
}

1
styles.css Normal file
View file

@ -0,0 +1 @@
/* markitdown-pdf-converter styles */

17
tsconfig.json Normal file
View file

@ -0,0 +1,17 @@
{
"compilerOptions": {
"baseUrl": "src",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowSyntheticDefaultImports": true,
"isolatedModules": true,
"lib": ["DOM", "ES5", "ES6", "ES7"],
"moduleResolution": "bundler",
"noImplicitAny": true,
"strictNullChecks": true,
"useUnknownInCatchVariables": true
},
"include": ["src/**/*.ts"]
}