feat: enhance image handling by converting Obsidian wiki-link images to standard Markdown format and update MarpExport to support this functionality

This commit is contained in:
Steffen John 2026-01-13 16:15:24 +01:00
parent 1a6b11be0f
commit 715093b773
7 changed files with 643 additions and 69 deletions

5
.gitignore vendored
View file

@ -5,6 +5,7 @@
*.iml
.idea
# npm
node_modules
@ -26,3 +27,7 @@ tests/coverage
vault/.obsidian
lib
# claude
.claude/*
tmpclaude-*

525
PROJECT.md Normal file
View file

@ -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).

76
package-lock.json generated
View file

@ -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",

View file

@ -104,7 +104,7 @@ 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);
const marpCli = new MarpExport(this.settings, this.app);
await marpCli.export(file,type);
}
}

View file

@ -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('/');
}
}

View file

@ -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,6 +25,17 @@ export class MarpExport {
const resourcesPath = filesTool.getLibDirectory(file.vault);
const marpEngineConfig = filesTool.getMarpEngine(file.vault);
// 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);

View file

@ -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) {
@ -132,14 +132,18 @@ export class MarpPreviewView extends ItemView {
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\(&quot;http)background-image:url\(&quot;/g, `background-image:url(&quot;${basePath}`);