mirror of
https://github.com/flatulentfowl/docdrop.git
synced 2026-07-22 06:49:52 +00:00
first commit
This commit is contained in:
commit
2eb6d5f3a4
9 changed files with 1484 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
main.js
|
||||
*.js.map
|
||||
188
README.md
Normal file
188
README.md
Normal 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
40
esbuild.config.mjs
Normal 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
10
manifest.json
Normal 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
648
package-lock.json
generated
Normal 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
21
package.json
Normal 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
556
src/main.ts
Normal 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
1
styles.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
/* markitdown-pdf-converter styles */
|
||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal 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"]
|
||||
}
|
||||
Loading…
Reference in a new issue