From 715093b7734576edc6fd00439713bb2b951353e0 Mon Sep 17 00:00:00 2001 From: Steffen John Date: Tue, 13 Jan 2026 16:15:24 +0100 Subject: [PATCH] feat: enhance image handling by converting Obsidian wiki-link images to standard Markdown format and update MarpExport to support this functionality --- .gitignore | 5 + PROJECT.md | 525 +++++++++++++++++++++++++++++++++++ package-lock.json | 76 ++--- src/main.ts | 4 +- src/utilities/filePath.ts | 64 ++++- src/utilities/marpExport.ts | 22 +- src/views/marpPreviewView.ts | 16 +- 7 files changed, 643 insertions(+), 69 deletions(-) create mode 100644 PROJECT.md diff --git a/.gitignore b/.gitignore index 7b0f48f..c4d10b9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.iml .idea + # npm node_modules @@ -26,3 +27,7 @@ tests/coverage vault/.obsidian lib + +# claude +.claude/* +tmpclaude-* diff --git a/PROJECT.md b/PROJECT.md new file mode 100644 index 0000000..8d0b431 --- /dev/null +++ b/PROJECT.md @@ -0,0 +1,525 @@ +# Obsidian Marp Slides - Technical Documentation + +> For user documentation and getting started guides, see [README.md](README.md) and the [online documentation](https://samuele-cozzi.github.io/obsidian-marp-slides/). + +## Overview + +**Obsidian Marp Slides** is a plugin that integrates [Marp](https://marp.app/) (Markdown Presentation Ecosystem) into [Obsidian](https://obsidian.md/), enabling users to create, preview, and export slide presentations directly from Markdown files. + +| Property | Value | +|----------|-------| +| Plugin ID | `marp-slides` | +| Version | 0.45.6 | +| Author | Samuele Cozzi | +| License | MIT | +| Min Obsidian Version | 0.15.0 | + +--- + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Obsidian App │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌──────────────────┐ ┌───────────────┐ │ +│ │ MarpSlides │───▶│ MarpPreviewView │───▶│ Marp Core │ │ +│ │ (Plugin) │ │ (ItemView) │ │ (Renderer) │ │ +│ └──────┬──────┘ └──────────────────┘ └───────────────┘ │ +│ │ │ +│ │ ┌──────────────────┐ ┌───────────────┐ │ +│ └──────────▶│ MarpExport │───▶│ Marp CLI │ │ +│ │ (Exporter) │ │ (Export) │ │ +│ └────────┬─────────┘ └───────────────┘ │ +│ │ │ +│ ┌────────▼─────────┐ │ +│ │ FilePath │ │ +│ │ (Utilities) │ │ +│ └──────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Data Flow + +**Preview Pipeline:** +1. User opens Markdown file and triggers "Slide Preview" command +2. `MarpSlides` retrieves active `MarkdownView` and creates `MarpPreviewView` +3. `MarpPreviewView` uses Marp Core to render Markdown → HTML/CSS +4. Rendered slides displayed in split pane with file base path for assets +5. File change events trigger re-render automatically + +**Export Pipeline:** +1. User triggers export command (PDF/HTML/PPTX/PNG) +2. `MarpSlides` creates `MarpExport` instance with current settings +3. `FilePath` resolves file paths, themes, and resources +4. `MarpExport` constructs CLI arguments and invokes Marp CLI +5. Marp CLI uses Chrome/Chromium for PDF/PPTX rendering + +--- + +## Project Structure + +``` +obsidian-marp-slides/ +├── src/ +│ ├── main.ts # Plugin entry point, commands, settings +│ ├── config/ +│ │ └── marp.config.js # Marp engine configuration for markdown-it plugins +│ ├── utilities/ +│ │ ├── settings.ts # Settings interface and defaults +│ │ ├── marpExport.ts # Export functionality (PDF, HTML, PPTX, PNG) +│ │ ├── filePath.ts # File/path resolution utilities +│ │ ├── libs.ts # External library management +│ │ └── icons.ts # SVG icon definitions +│ └── views/ +│ └── marpPreviewView.ts # Slide preview rendering +├── tests/ +│ ├── filePath.test.ts # Path utility tests +│ └── __mocks__/ +│ └── obsidian.ts # Obsidian API mocks +├── docs/ # User documentation +├── vault/samples/ # Sample presentations +├── .github/workflows/ +│ └── release-please.yml # CI/CD pipeline +├── esbuild.config.mjs # Build configuration +├── tsconfig.json # TypeScript configuration +├── jest.config.js # Test configuration +├── package.json # Dependencies and scripts +├── manifest.json # Obsidian plugin metadata +├── styles.css # Plugin styling +└── version-bump.mjs # Version management script +``` + +--- + +## Core Components + +### MarpSlides (`src/main.ts:10-147`) + +Main plugin class extending Obsidian's `Plugin`. + +**Responsibilities:** +- Plugin lifecycle management (`onload`, `onunload`) +- Command registration (preview, export) +- Settings management +- Event listeners (file changes, cursor position) + +**Key Methods:** +| Method | Line | Description | +|--------|------|-------------| +| `onload()` | 16 | Initializes plugin, registers views/commands | +| `loadSettings()` | 90 | Loads persisted settings | +| `saveSettings()` | 94 | Persists settings to disk | +| `showPreviewSlide()` | 112 | Opens preview pane | +| `exportFile()` | 104 | Triggers export operation | +| `onChange()` | 98 | Handles file modification events | + +**Registered Commands:** +- `marp-slides:preview` - Slide Preview +- `marp-slides:export-pdf` - Export PDF +- `marp-slides:export-pdf-notes` - Export PDF with Notes +- `marp-slides:export-html` - Export HTML +- `marp-slides:export-pptx` - Export PPTX +- `marp-slides:export-png` - Export PNG + +--- + +### MarpPreviewView (`src/views/marpPreviewView.ts:16-166`) + +Custom view for rendering slides, extending Obsidian's `ItemView`. + +**Responsibilities:** +- Marp Core initialization and configuration +- Theme loading from vault +- Slide rendering (Markdown → HTML) +- Cursor-to-slide synchronization +- Export action buttons + +**Key Methods:** +| Method | Line | Description | +|--------|------|-------------| +| `onOpen()` | 58 | Initializes container, loads themes | +| `displaySlides()` | 131 | Renders Markdown to HTML slides | +| `onLineChanged()` | 89 | Scrolls to slide based on cursor | +| `addActions()` | 97 | Adds export buttons to view header | + +**Marp Configuration** (lines 29-40): +```typescript +new Marp({ + container: { tag: 'div', id: '__marp-vscode' }, + slideContainer: { tag: 'div', 'data-marp-vscode-slide-wrapper': '' }, + html: this.settings.EnableHTML, + inlineSVG: { enabled: true, backdropSelector: false }, + math: this.settings.MathTypesettings, + minifyCSS: true, + script: false +}); +``` + +--- + +### MarpExport (`src/utilities/marpExport.ts:8-158`) + +Handles exporting presentations to various formats. + +**Responsibilities:** +- Building Marp CLI argument arrays +- Managing export types and options +- Browser path resolution +- Error handling for missing Chrome + +**Key Methods:** +| Method | Line | Description | +|--------|------|-------------| +| `export()` | 16 | Main export orchestrator | +| `run()` | 101 | Sets up environment and executes CLI | +| `runMarpCli()` | 136 | Executes Marp CLI with arguments | + +**Supported Export Types:** +| Type | CLI Flags | Output | +|------|-----------|--------| +| `pdf` | `--pdf` | PDF file | +| `pdf-with-notes` | `--pdf --pdf-notes --pdf-outlines` | PDF with speaker notes | +| `pptx` | `--pptx` | PowerPoint file | +| `png` | `--images --png` | PNG images | +| `html` | `--html --template [mode]` | HTML file | +| `preview` | `--html --preview` | Live preview server | + +--- + +### FilePath (`src/utilities/filePath.ts:4-112`) + +Utility class for file and path resolution. + +**Responsibilities:** +- Vault base path resolution +- Absolute vs relative link format handling +- Theme directory resolution +- Plugin directory management + +**Key Methods:** +| Method | Line | Description | +|--------|------|-------------| +| `getCompleteFileBasePath()` | 41 | Gets resource base path for assets | +| `getCompleteFilePath()` | 56 | Gets full file path for export | +| `getThemePath()` | 80 | Resolves custom theme directory | +| `getLibDirectory()` | 99 | Gets markdown-it plugins directory | +| `getMarpEngine()` | 106 | Gets Marp engine config path | + +--- + +### MarpSlidesSettings (`src/utilities/settings.ts:1-21`) + +Settings interface and defaults. + +```typescript +interface MarpSlidesSettings { + CHROME_PATH: string; // Custom browser path for export + ThemePath: string; // Custom theme CSS directory + EnableHTML: boolean; // Allow HTML in Markdown + MathTypesettings: string; // 'mathjax' or 'katex' + HTMLExportMode: string; // 'bare' or 'bespoke' + EXPORT_PATH: string; // Custom export output directory + EnableSyncPreview: boolean; // Sync preview with cursor + EnableMarkdownItPlugins: boolean; // Enable markdown-it extensions +} +``` + +**Default Values:** +| Setting | Default | +|---------|---------| +| CHROME_PATH | `''` (auto-detect) | +| ThemePath | `''` (none) | +| EnableHTML | `false` | +| MathTypesettings | `'mathjax'` | +| HTMLExportMode | `'bare'` | +| EXPORT_PATH | `''` (same as source) | +| EnableSyncPreview | `true` | +| EnableMarkdownItPlugins | `false` | + +--- + +### Libs (`src/utilities/libs.ts:8-61`) + +Manages external markdown-it plugin libraries. + +**Responsibilities:** +- Check if libraries exist locally +- Download compiled plugins from GitHub releases +- Extract ZIP archive and cache plugins + +**Library Source:** `https://github.com/samuele-cozzi/obsidian-marp-slides/releases/download/lib-v3/lib.zip` + +**Included Plugins:** +- `markdown-it-container` - Custom containers +- `markdown-it-mark` - Text highlighting +- `markdown-it-kroki` - Diagram rendering via Kroki.io + +--- + +### LineSelectionListener (`src/main.ts:255-300`) + +Experimental feature for cursor-to-slide synchronization. + +**Implementation:** Extends `EditorSuggest` (non-intrusive approach to track cursor) + +**How it works:** +1. Listens to cursor position changes +2. Counts slide separators (`---`) before cursor +3. Parses YAML frontmatter to adjust slide count +4. Scrolls preview to corresponding slide + +--- + +## Technology Stack + +### Runtime Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| `@marp-team/marp-core` | ^3.9.0 | Core slide rendering engine | +| `@marp-team/marp-cli` | ^2.5.0 | Export engine (PDF, PPTX, HTML, PNG) | +| `@marp-team/marpit` | ^2.6.1 | Markdown presentation framework | +| `gray-matter` | ^4.0.3 | YAML frontmatter parsing | +| `fs-extra` | ^11.2.0 | Extended file system operations | +| `jszip` | ^3.10.1 | ZIP handling for library distribution | +| `request` | ^2.88.2 | HTTP requests for library download | + +### Development Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| `typescript` | ^4.9.5 | Type-safe development | +| `esbuild` | 0.17.3 | Fast bundler | +| `jest` | ^29.7.0 | Testing framework | +| `ts-jest` | ^29.1.2 | TypeScript support for Jest | +| `obsidian` | ^1.5.7-1 | Obsidian API types | +| `@typescript-eslint/*` | 5.29.0 | Linting | + +### External Requirements + +- **Chrome/Chromium/Edge** - Required for PDF, PPTX, and PNG export +- **Node.js** - Development and build environment + +--- + +## Development Setup + +### Prerequisites + +- Node.js (v16+) +- npm +- Obsidian (for testing) + +### Installation + +```bash +# Clone the repository +git clone https://github.com/samuele-cozzi/obsidian-marp-slides.git +cd obsidian-marp-slides + +# Install dependencies +npm install +``` + +### Build Commands + +| Command | Description | +|---------|-------------| +| `npm run dev` | Watch mode with inline sourcemaps | +| `npm run build` | TypeScript check + production build | +| `npm run test` | Run tests with coverage | +| `npm run test:watch` | Run tests in watch mode | +| `npm run version` | Bump version in manifest | + +### Development Workflow + +1. **Start watch mode:** + ```bash + npm run dev + ``` + +2. **Link to Obsidian vault:** + - Copy or symlink the project directory to your vault's `.obsidian/plugins/marp-slides/` + - Or set up the vault's plugin directory to point to your development folder + +3. **Enable plugin:** + - Open Obsidian Settings → Community Plugins + - Enable "Marp Slides" + - Use "Reload app without saving" (Ctrl/Cmd+R) after changes + +4. **Debug:** + - Open Developer Tools (Ctrl/Cmd+Shift+I) + - Check Console for logs and errors + +### Build Configuration (`esbuild.config.mjs`) + +- **Entry:** `main.ts` +- **Output:** `main.js` +- **Format:** CommonJS +- **Target:** ES2018 +- **External:** `obsidian`, `electron`, `@codemirror/*` +- **Production:** Minified, no sourcemap +- **Development:** Inline sourcemap + +--- + +## Testing + +### Framework + +- **Jest** with **ts-jest** preset +- Coverage reporting via **lcov** +- Mocks for Obsidian API + +### Running Tests + +```bash +# Run all tests with coverage +npm run test + +# Watch mode +npm run test:watch +``` + +### Test Structure + +``` +tests/ +├── filePath.test.ts # Path resolution tests +├── coverage/ # Coverage reports (generated) +└── __mocks__/ + └── obsidian.ts # Obsidian API mocks +``` + +### Coverage + +Coverage reports are generated in `tests/coverage/` and uploaded to CodeClimate during CI. + +--- + +## CI/CD Pipeline + +### GitHub Actions Workflow (`.github/workflows/release-please.yml`) + +**Trigger:** Push to `main` branch + +### Jobs + +#### 1. release-please +- Uses `google-github-actions/release-please-action@v3` +- Analyzes commits for version bump +- Creates release PR if warranted +- Generates changelog in `docs/CHANGELOG.md` + +#### 2. release-plugin (if release created) +1. Updates `manifest.json` version +2. Commits version update +3. Builds plugin (`npm install && npm run build`) +4. Runs tests with CodeClimate coverage upload +5. Packages artifacts: + - `main.js` + - `manifest.json` + - `styles.css` + - `obsidian-marp-slides-{version}.zip` +6. Uploads to GitHub release + +### Release Artifacts + +| File | Description | +|------|-------------| +| `main.js` | Compiled plugin code | +| `manifest.json` | Plugin metadata | +| `styles.css` | Plugin styling | +| `obsidian-marp-slides-{version}.zip` | Complete plugin package | + +--- + +## Configuration Options Reference + +### CHROME_PATH +**Type:** `string` | **Default:** `''` + +Custom path to Chrome, Chromium, or Edge browser for PDF/PPTX/PNG export. If empty, Marp CLI auto-detects installed browsers. + +### ThemePath +**Type:** `string` | **Default:** `''` + +Vault-relative path to directory containing custom Marp theme CSS files. Themes are loaded on preview open. + +### EXPORT_PATH +**Type:** `string` | **Default:** `''` + +Custom output directory for exports. If empty, exports to same directory as source file. Does not affect HTML export. + +### EnableHTML +**Type:** `boolean` | **Default:** `false` + +Allow HTML elements in Marp Markdown. Use with caution. + +### MathTypesettings +**Type:** `'mathjax' | 'katex'` | **Default:** `'mathjax'` + +Math rendering library. Can be overridden per-slide via frontmatter. + +### HTMLExportMode +**Type:** `'bare' | 'bespoke'` | **Default:** `'bare'` + +HTML export template. `bespoke` is experimental and provides interactive features. + +### EnableSyncPreview +**Type:** `boolean` | **Default:** `true` + +(Experimental) Synchronize slide preview with editor cursor position. + +### EnableMarkdownItPlugins +**Type:** `boolean` | **Default:** `false` + +(Experimental) Enable markdown-it plugins for containers, marks, and Kroki diagrams. + +--- + +## Obsidian API Integration + +### Used APIs + +| API | Usage | +|-----|-------| +| `Plugin` | Base class for plugin | +| `ItemView` | Custom preview view | +| `MarkdownView` | Access editor content | +| `PluginSettingTab` | Settings UI | +| `EditorSuggest` | Cursor position tracking | +| `Vault` | File operations | +| `FileSystemAdapter` | Path resolution | +| `WorkspaceLeaf` | View management | + +### Registered Entities + +| Type | ID/Name | +|------|---------| +| View | `marp-preview-view` | +| Icons | `slides-preview-marp`, `slides-marp-export-pdf`, `slides-marp-export-pptx`, `slides-marp-slide-present` | +| Commands | 6 commands (see MarpSlides section) | +| Ribbon | Preview button | + +--- + +## Known Limitations + +- **Wiki Links** not supported in slides +- **Mobile App** plugin is in alpha state +- **Export** (except HTML) requires Chrome/Chromium/Edge installed +- **Sync Preview** is experimental and may have edge cases + +--- + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make changes with tests +4. Submit a pull request + +For bug reports and feature requests, use [GitHub Issues](https://github.com/samuele-cozzi/obsidian-marp-slides/issues). diff --git a/package-lock.json b/package-lock.json index 21c155d..6b59bdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,6 @@ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -150,6 +149,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -1046,7 +1046,6 @@ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, - "peer": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" }, @@ -1062,7 +1061,6 @@ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true, - "peer": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -1072,7 +1070,6 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, - "peer": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -1096,7 +1093,6 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, - "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -1106,7 +1102,6 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, - "peer": true, "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", "debug": "^4.3.1", @@ -1121,7 +1116,6 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, - "peer": true, "engines": { "node": ">=12.22" }, @@ -1134,8 +1128,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -1987,6 +1980,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.29.0.tgz", "integrity": "sha512-ruKWTv+x0OOxbzIw9nW5oWlUopvP/IQDjB5ZqmTglLIoDTctLlAJpAQFpNPJP/ZI7hTT9sARBosEfaKbcFuECw==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.29.0", "@typescript-eslint/types": "5.29.0", @@ -2137,8 +2131,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/accepts": { "version": "1.3.8", @@ -2170,7 +2163,6 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, - "peer": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -2534,6 +2526,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001587", "electron-to-chromium": "^1.4.668", @@ -2973,8 +2966,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/deepmerge": { "version": "4.3.1", @@ -3013,7 +3005,8 @@ "node_modules/devtools-protocol": { "version": "0.0.1107588", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1107588.tgz", - "integrity": "sha512-yIR+pG9x65Xko7bErCUSQaDLrO/P1p3JUzEk7JCU4DowPcGHkTGUGQapcfcLc4qj0UaALwZ+cr0riFgiqpixcg==" + "integrity": "sha512-yIR+pG9x65Xko7bErCUSQaDLrO/P1p3JUzEk7JCU4DowPcGHkTGUGQapcfcLc4qj0UaALwZ+cr0riFgiqpixcg==", + "peer": true }, "node_modules/diff-sequences": { "version": "29.6.3", @@ -3040,7 +3033,6 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, - "peer": true, "dependencies": { "esutils": "^2.0.2" }, @@ -3112,6 +3104,7 @@ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.3.tgz", "integrity": "sha512-9n3AsBRe6sIyOc6kmoXg2ypCLgf3eZSraWFRpnkto+svt8cZNuKTkb1bhQcitBcvIqjNiK7K0J3KPmwGSfkA8g==", "hasInstallScript": true, + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -3188,7 +3181,6 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "peer": true, "engines": { "node": ">=10" }, @@ -3309,7 +3301,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -3326,7 +3317,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "peer": true, "engines": { "node": ">=4.0" } @@ -3336,7 +3326,6 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, - "peer": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -3357,7 +3346,6 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, - "peer": true, "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -3387,7 +3375,6 @@ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, - "peer": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -3400,7 +3387,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "peer": true, "engines": { "node": ">=4.0" } @@ -3440,7 +3426,6 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3579,8 +3564,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/fastq": { "version": "1.17.1", @@ -3612,7 +3596,6 @@ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, - "peer": true, "dependencies": { "flat-cache": "^3.0.4" }, @@ -3636,7 +3619,6 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, - "peer": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -3653,7 +3635,6 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, - "peer": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -3667,8 +3648,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/forever-agent": { "version": "0.6.1", @@ -3826,7 +3806,6 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, - "peer": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -3865,8 +3844,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "peer": true + "dev": true }, "node_modules/gray-matter": { "version": "4.0.3", @@ -4222,7 +4200,6 @@ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, - "peer": true, "engines": { "node": ">=8" } @@ -4342,6 +4319,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -4929,8 +4907,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -4951,8 +4928,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/json-stringify-safe": { "version": "5.0.1", @@ -5027,7 +5003,6 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, - "peer": true, "dependencies": { "json-buffer": "3.0.1" } @@ -5063,7 +5038,6 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, - "peer": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -5098,7 +5072,6 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "peer": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -5124,8 +5097,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/lru-cache": { "version": "5.1.1", @@ -5446,7 +5418,6 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, - "peer": true, "dependencies": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", @@ -5479,7 +5450,6 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, - "peer": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -5724,7 +5694,6 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, - "peer": true, "engines": { "node": ">= 0.8.0" } @@ -6072,7 +6041,6 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -6459,8 +6427,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/supports-color": { "version": "7.2.0", @@ -6542,8 +6509,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/through": { "version": "2.3.8", @@ -6700,7 +6666,6 @@ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, - "peer": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -6722,7 +6687,6 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, - "peer": true, "engines": { "node": ">=10" }, @@ -6735,6 +6699,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "devOptional": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6858,8 +6823,7 @@ "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/walker": { "version": "1.0.8", diff --git a/src/main.ts b/src/main.ts index 33c08c4..4c0e7ef 100644 --- a/src/main.ts +++ b/src/main.ts @@ -103,8 +103,8 @@ export default class MarpSlides extends Plugin { async exportFile(type: string){ const file = this.app.workspace.getActiveFile(); - if(file !== null){ - const marpCli = new MarpExport(this.settings); + if(file !== null){ + const marpCli = new MarpExport(this.settings, this.app); await marpCli.export(file,type); } } diff --git a/src/utilities/filePath.ts b/src/utilities/filePath.ts index 91fa0c1..59cf9c9 100644 --- a/src/utilities/filePath.ts +++ b/src/utilities/filePath.ts @@ -1,4 +1,4 @@ -import { Vault, normalizePath, FileSystemAdapter, TFile } from 'obsidian'; +import { Vault, normalizePath, FileSystemAdapter, TFile, App } from 'obsidian'; import { MarpSlidesSettings } from './settings'; export class FilePath { @@ -109,4 +109,66 @@ export class FilePath { //console.log(path); return path; } + + /** + * Convert Obsidian wiki-link image syntax to standard Markdown. + * Transforms ![[image.png]] to ![image.png](path/to/image.png) + */ + public convertImageWikiLinks(markdown: string, sourceFile: TFile, app: App): string { + // Image extensions to convert + const imageExtensions = /\.(png|jpg|jpeg|gif|svg|webp|bmp)$/i; + + // Regex: ![[filename]] or ![[filename|alt text]] + const wikiLinkRegex = /!\[\[([^\]|]+?)(?:\|([^\]]*))?\]\]/g; + + return markdown.replace(wikiLinkRegex, (match, filename, altText) => { + // Only process image files + if (!imageExtensions.test(filename)) { + return match; + } + + // Use Obsidian's link resolver to find the file + const linkedFile = app.metadataCache.getFirstLinkpathDest(filename, sourceFile.path); + + if (linkedFile) { + // Build path based on link format setting + let imagePath: string; + if (this.isAbsoluteLinkFormat(sourceFile)) { + // Absolute: path from vault root + imagePath = linkedFile.path; + } else { + // Relative: path from source file's folder + imagePath = this.getRelativePathFromFile(sourceFile, linkedFile); + } + + const alt = altText || filename; + return `![${alt}](${imagePath})`; + } + + // File not found - return original + return match; + }); + } + + /** + * Calculate relative path from source file to target file. + */ + private getRelativePathFromFile(sourceFile: TFile, targetFile: TFile): string { + const sourceParts = sourceFile.parent?.path.split('/').filter(p => p) || []; + const targetParts = targetFile.path.split('/').filter(p => p); + + // Find common prefix length + let commonLength = 0; + while (commonLength < sourceParts.length && + commonLength < targetParts.length - 1 && + sourceParts[commonLength] === targetParts[commonLength]) { + commonLength++; + } + + // Build relative path + const upCount = sourceParts.length - commonLength; + const relativeParts = [...Array(upCount).fill('..'), ...targetParts.slice(commonLength)]; + + return relativeParts.join('/'); + } } \ No newline at end of file diff --git a/src/utilities/marpExport.ts b/src/utilities/marpExport.ts index b4613ec..67d946a 100644 --- a/src/utilities/marpExport.ts +++ b/src/utilities/marpExport.ts @@ -1,16 +1,19 @@ import marpCli, { CLIError, CLIErrorCode } from '@marp-team/marp-cli' -import { TFile } from 'obsidian'; +import { TFile, App } from 'obsidian'; import { MarpSlidesSettings } from './settings'; import { FilePath } from './filePath'; +import { writeFileSync, readFileSync } from 'fs-extra'; export class MarpCLIError extends Error {} export class MarpExport { private settings : MarpSlidesSettings; + private app : App | null; - constructor(settings: MarpSlidesSettings) { + constructor(settings: MarpSlidesSettings, app: App | null = null) { this.settings = settings; + this.app = app; } async export(file: TFile, type: string){ @@ -22,9 +25,20 @@ export class MarpExport { const resourcesPath = filesTool.getLibDirectory(file.vault); const marpEngineConfig = filesTool.getMarpEngine(file.vault); - if (completeFilePath != ''){ + // Convert wiki-link images to standard markdown before export + if (this.app && completeFilePath != '') { + try { + const originalContent = readFileSync(completeFilePath, 'utf-8'); + const processedContent = filesTool.convertImageWikiLinks(originalContent, file, this.app); + writeFileSync(completeFilePath, processedContent, 'utf-8'); + } catch (e) { + console.error('Failed to process wiki-links for export:', e); + } + } + + if (completeFilePath != ''){ //console.log(completeFilePath); - + const argv: string[] = [completeFilePath,'--allow-local-files']; //const argv: string[] = ['--engine', '@marp-team/marp-core', completeFilePath,'--allow-local-files']; diff --git a/src/views/marpPreviewView.ts b/src/views/marpPreviewView.ts index b100414..0aa2b5f 100644 --- a/src/views/marpPreviewView.ts +++ b/src/views/marpPreviewView.ts @@ -95,7 +95,7 @@ export class MarpPreviewView extends ItemView { } async addActions() { - const marpCli = new MarpExport(this.settings); + const marpCli = new MarpExport(this.settings, this.app); this.addAction('image', 'Export as PNG', () => { if (this.file) { @@ -129,17 +129,21 @@ export class MarpPreviewView extends ItemView { } async displaySlides(view : MarkdownView) { - + if (view.file != null) { this.file = view.file; - const basePath = (new FilePath(this.settings)).getCompleteFileBasePath(view.file); + const filePath = new FilePath(this.settings); + const basePath = filePath.getCompleteFileBasePath(view.file); const markdownText = view.data; - + + // Convert wiki-link images to standard markdown + const processedMarkdown = filePath.convertImageWikiLinks(markdownText, view.file, this.app); + const container = this.containerEl.children[1]; container.empty(); - - let { html, css } = this.marp.render(markdownText); + + let { html, css } = this.marp.render(processedMarkdown); // Replace Backgorund Url for images html = html.replace(/(?!background-image:url\("http)background-image:url\("/g, `background-image:url("${basePath}`);