diff --git a/cli/package.json b/cli/package.json index 81da4bfa..69b0cad1 100644 --- a/cli/package.json +++ b/cli/package.json @@ -40,8 +40,8 @@ "sort-package-json": "catalog:prod" }, "plume-deps": { - "vuepress": "2.0.0-rc.26", - "vue": "^3.5.26", + "vuepress": "2.0.0-rc.28", + "vue": "^3.5.32", "http-server": "^14.1.1", "typescript": "^5.9.3" }, diff --git a/docs/.vuepress/collections/en/theme-guide.ts b/docs/.vuepress/collections/en/theme-guide.ts index d33acca9..6f2522b5 100644 --- a/docs/.vuepress/collections/en/theme-guide.ts +++ b/docs/.vuepress/collections/en/theme-guide.ts @@ -69,6 +69,7 @@ export const themeGuide: ThemeCollectionItem = defineCollection({ 'chat', 'include', 'env', + 'obsidian', ], }, { diff --git a/docs/.vuepress/collections/zh/theme-guide.ts b/docs/.vuepress/collections/zh/theme-guide.ts index f0ee78fb..a8d8fdb4 100644 --- a/docs/.vuepress/collections/zh/theme-guide.ts +++ b/docs/.vuepress/collections/zh/theme-guide.ts @@ -69,6 +69,7 @@ export const themeGuide: ThemeCollectionItem = defineCollection({ 'chat', 'include', 'env', + 'obsidian', ], }, { diff --git a/docs/.vuepress/theme.ts b/docs/.vuepress/theme.ts index 5753c251..7202c2f3 100644 --- a/docs/.vuepress/theme.ts +++ b/docs/.vuepress/theme.ts @@ -58,6 +58,7 @@ export const theme: Theme = plumeTheme({ jsfiddle: true, demo: true, encrypt: true, + obsidian: true, npmTo: ['pnpm', 'yarn', 'npm'], repl: { go: true, diff --git a/docs/en/guide/markdown/obsidian.md b/docs/en/guide/markdown/obsidian.md new file mode 100644 index 00000000..d321c219 --- /dev/null +++ b/docs/en/guide/markdown/obsidian.md @@ -0,0 +1,338 @@ +--- +title: Obsidian Compatibility +icon: simple-icons:obsidian +createTime: 2026/04/17 21:56:55 +permalink: /en/guide/markdown/obsidian/ +--- + +## Overview + +The theme provides compatibility support for Obsidian's official Markdown extension syntax through the `vuepress-plugin-md-power` plugin, +enabling Obsidian users to write documentation using familiar syntax. + +Currently supported Obsidian extension syntax includes: + +- [Wiki Links](#wiki-links) - Syntax for inter-page linking +- [Embeds](#embeds) - Embed content from other files into the current page +- [Comments](#comments) - Add comments visible only during editing + +::: warning No plans to support extension syntax provided by Obsidian's third-party community plugins +::: + +## Wiki Links + +Wiki Links are syntax for linking to other notes in Obsidian. + +### Syntax + +```md +[[filename]] +[[filename#heading]] +[[filename#heading#subheading]] +[[filename|alias]] +[[filename#heading|alias]] +``` + +### Filename Search Rules + +When using Wiki Links, filenames are matched according to the following rules: + +**Match Priority:** + +1. **Page Title** - Priority matching against page titles +2. **Full Path** - Exact match against file paths +3. **Fuzzy Match** - Match filenames at the end of paths + +**Path Resolution Rules:** + +- **Relative paths** (starting with `.`): Resolved relative to the current file's directory +- **Absolute paths** (not starting with `.`): Searched throughout the document tree, with shortest path taking precedence +- **Directory form** (ending with `/`): Matches `README.md` or `index.html` within that directory + +**Example:** + +Assuming the following document structure: + +```txt +docs/ +├── README.md (title: "Home") +├── guide/ +│ ├── README.md (title: "Guide") +│ └── markdown/ +│ └── obsidian.md +``` + +In `docs/guide/markdown/obsidian.md`: + +| Syntax | Match Result | +| ------------ | ------------------------------------------------------- | +| `[[Home]]` | Matches `docs/README.md` (via title) | +| `[[Guide]]` | Matches `docs/guide/README.md` (via title) | +| `[[./]]` | Matches `docs/guide/markdown/README.md` (relative path) | +| `[[../]]` | Matches `docs/guide/README.md` (parent directory) | +| `[[guide/]]` | Matches `docs/guide/README.md` (directory form) | + +### Examples + +**External Links:** + +**Input:** + +```md +[[https://example.com|External Link]] +``` + +**Output:** + +[[https://example.com|External Link]] + +--- + +**Internal Anchor Links:** + +**Input:** + +```md +[[QR Code]] +[[npm-to]] +[[guide/markdown/math]] +[[#Wiki Links]] +[[file-tree#configuration]] +``` + +**Output:** + +[[QR Code]] + +[[npm-to]] + +[[guide/markdown/math]] + +[[#Wiki Links]] + +[[file-tree#configuration]] + +[Obsidian Official - **Wiki Links**](https://obsidian.md/en/help/links){.readmore} + +## Embeds + +The embed syntax allows you to insert other file resources into the current page. + +### Syntax + +```md +![[filename]] +![[filename#heading]] +![[filename#heading#subheading]] +``` + +Filename search rules are the same as [Wiki Links](#filename-search-rules). + +::: info Resources starting with `/` or having no path prefix like `./` are loaded from the `public` directory +::: + +### Image Embeds + +**Syntax:** + +```md +![[image.png]] +![[image.png|300]] +![[image.png|300x200]] +``` + +Supported formats: `jpg`, `jpeg`, `png`, `gif`, `avif`, `webp`, `svg`, `bmp`, `ico`, `tiff`, `apng`, `jfif`, `pjpeg`, `pjp`, `xbm` + +**Input:** + +```md +![[images/custom-hero.jpg]] +``` + +**Output:** + +![[images/custom-hero.jpg]] + +### PDF Embeds + +> [!NOTE] +> PDF embeds require the `markdown.pdf` plugin to be enabled for proper functionality. + +**Syntax:** + +```md +![[document.pdf]] +![[document.pdf#page=1]] +![[document.pdf#page=1#height=300]] +``` + +--- + +### Audio Embeds + +> [!note] +> Audio embeds require the file path to be correct and the file to exist in the document directory. + +**Input:** + +```md +![[audio.mp3]] +``` + +**Output:** + +![[https://publish-01.obsidian.md/access/cf01a21839823cd6cbe18031acf708c0/Attachments/audio/Excerpt%20from%20Mother%20of%20All%20Demos%20(1968).ogg]] + +Supported formats: `mp3`, `flac`, `wav`, `ogg`, `opus`, `webm`, `acc` + +--- + +### Video Embeds + +> [!note] +> Video embeds require the `markdown.artPlayer` plugin to be enabled for proper functionality. + +**Input:** + +```md +![[video.mp4]] +``` + +**Output:** + +![[https://artplayer.org/assets/sample/video.mp4]] + +Supported formats: `mp4`, `webm`, `mov`, etc. + +--- + +### Content Fragment Embeds + +Content fragments under a specified heading can be embedded using `#heading`: + +**Input:** + +```md +![[my-note]] +![[my-note#heading-one]] +![[my-note#heading-one#subheading]] +``` + +[Obsidian Official - Embeds](https://obsidian.md/en/help/embeds){.readmore} +[Obsidian Official - File Formats](https://obsidian.md/en/help/file-formats){.readmore} + +## Comments + +Content wrapped in `%%` is treated as a comment and will not be rendered on the page. + +### Syntax + +**Inline Comments:** + +```md +This is an %%inline comment%% example. +``` + +**Block Comments:** + +```md +%% +This is a block comment. +It can span multiple lines. +%% +``` + +### Examples + +**Inline Comments:** + +**Input:** + +```md +This is an %%inline comment%% example. +``` + +**Output:** + +This is an %%inline comment%% example. + +--- + +**Block Comments:** + +**Input:** + +```md +Content before the comment + +%% +This is a block comment. + +It can span multiple lines. +%% + +Content after the comment +``` + +**Output:** + +Content before the comment + +%% +This is a block comment. + +It can span multiple lines. +%% + +Content after the comment + +> Related Documentation: [Obsidian Official - Comments](https://obsidian.md/en/help/syntax#%E6%B3%A8%E9%87%8B) + +## Configuration + +You can enable or disable these plugins in the theme configuration: + +```ts title=".vuepress/config.ts" +export default defineUserConfig({ + theme: plumeTheme({ + plugins: { + mdPower: { + // Obsidian compatibility plugin configuration + obsidian: { + wikiLink: true, // Wiki Links + embedLink: true, // Embeds + comment: true, // Comments + }, + pdf: true, // PDF embed functionality + artPlayer: true, // Video embed functionality + } + } + }) +}) +``` + +### Configuration Options + +:::: field-group + +::: field name="wikiLink" type="boolean" default="true" optional +Enable Wiki Links syntax +::: + +::: field name="embedLink" type="boolean" default="true" optional +Enable embed content syntax +::: + +::: field name="comment" type="boolean" default="true" optional +Enable comment syntax +::: + +:::: + +## Notes + +- These plugins provide **compatibility support** and do not fully implement all of Obsidian's functionality +- Some Obsidian-specific features (such as the graph view for internal links, bidirectional links, etc.) are outside the scope of this support +- When embedding content, the embedded page also participates in the theme's build process +- PDF embeds require the `pdf` plugin to be enabled simultaneously +- Video embeds require the `artPlayer` plugin to be enabled simultaneously diff --git a/docs/guide/markdown/obsidian.md b/docs/guide/markdown/obsidian.md new file mode 100644 index 00000000..3033a34e --- /dev/null +++ b/docs/guide/markdown/obsidian.md @@ -0,0 +1,335 @@ +--- +title: Obsidian 兼容 +icon: simple-icons:obsidian +createTime: 2026/04/17 21:56:55 +permalink: /guide/markdown/obsidian/ +--- + +## 概述 + +主题通过 `vuepress-plugin-md-power` 插件提供对 Obsidian 官方 Markdown 扩展语法的兼容性支持,使 Obsidian 用户能够以熟悉的语法撰写文档。 + +当前已支持的 Obsidian 扩展语法包括: + +- [Wiki 链接](#wiki-链接) - 页面间相互链接的语法 +- [嵌入内容](#嵌入内容) - 将其他文件内容嵌入到当前页面 +- [注释](#注释) - 添加仅在编辑时可见的注释 + +::: warning 不计划对 obsidian 社区的第三方插件提供的扩展语法进行支持 +::: + +## Wiki 链接 + +Wiki 链接是 Obsidian 中用于链接到其他笔记的语法。 + +### 语法 + +```md +[[文件名]] +[[文件名#标题]] +[[文件名#标题#子标题]] +[[文件名|别名]] +[[文件名#标题|别名]] +``` + +### 文件名搜索规则 + +当使用 Wiki 链接时,文件名会按照以下规则进行搜索匹配: + +**匹配优先级:** + +1. **页面标题** - 优先匹配页面的标题 +2. **完整路径** - 精确匹配文件路径 +3. **模糊匹配** - 匹配路径结尾的文件名 + +**路径解析规则:** + +- **相对路径**(以 `.` 开头):相对于当前文件所在目录解析 +- **绝对路径**(不以 `.` 开头):在整个文档树中搜索,优先匹配最短路径 +- **目录形式**(以 `/` 结尾):匹配该目录下的 `README.md` 或 `index.html` + +**示例:** + +假设文档结构如下: + +```txt +docs/ +├── README.md (title: "首页") +├── guide/ +│ ├── README.md (title: "指南") +│ └── markdown/ +│ └── obsidian.md +``` + +在 `docs/guide/markdown/obsidian.md` 中: + +| 语法 | 匹配结果 | +| ------------ | ------------------------------------------------ | +| `[[首页]]` | 匹配 `docs/README.md`(通过标题) | +| `[[指南]]` | 匹配 `docs/guide/README.md`(通过标题) | +| `[[./]]` | 匹配 `docs/guide/markdown/README.md`(相对路径) | +| `[[../]]` | 匹配 `docs/guide/README.md`(上级目录) | +| `[[guide/]]` | 匹配 `docs/guide/README.md`(目录形式) | + +### 示例 + +**外部链接:** + +**输入:** + +```md +[[https://example.com|外部链接]] +``` + +**输出:** + +[[https://example.com|外部链接]] + +**内部锚点链接:** + +**输入:** + +```md +[[二维码]] +[[npm-to]] +[[guide/markdown/math]] +[[#Wiki 链接]] +[[file-tree#配置]] +``` + +**输出:** + +[[二维码]] + +[[npm-to]] + +[[guide/markdown/math]] + +[[#Wiki 链接]] + +[[file-tree#配置]] + +[Obsidian 官方 - **Wiki Links**](https://obsidian.md/zh/help/links){.readmore} + +## 嵌入内容 + +嵌入语法允许你将其他文件资源插入到当前页面中。 + +### 语法 + +```md +![[文件名]] +![[文件名#标题]] +![[文件名#标题#子标题]] +``` + +文件名搜索规则与 [Wiki 链接](#文件名搜索规则) 相同。 + +::: info 以 `/` 开头或 无路径前缀如 `./` 形式的,从 `public` 目录中加载资源 +::: + +### 图片嵌入 + +**语法:** + +```md +![[image.png]] +![[image.png|300]] +![[image.png|300x200]] +``` + +支持格式:`jpg`, `jpeg`, `png`, `gif`, `avif`, `webp`, `svg`, `bmp`, `ico`, `tiff`, `apng`, `jfif`, `pjpeg`, `pjp`, `xbm` + +**输入:** + +```md +![[images/custom-hero.jpg]] +``` + +**输出:** + +![[images/custom-hero.jpg]] + +### PDF 嵌入 + +> [!NOTE] +> PDF 嵌入需要启用 `markdown.pdf` 插件才能正常工作。 + +**语法:** + +```md +![[document.pdf]] +![[document.pdf#page=1]] +![[document.pdf#page=1#height=300]] +``` + +--- + +### 音频嵌入 + +> [!note] +> 音频嵌入需要确保文件路径正确,文件存在于文档目录中。 + +**输入:** + +```md +![[audio.mp3]] +``` + +**输出:** + +![[https://publish-01.obsidian.md/access/cf01a21839823cd6cbe18031acf708c0/Attachments/audio/Excerpt%20from%20Mother%20of%20All%20Demos%20(1968).ogg]] + +支持格式:`mp3`, `flac`, `wav`, `ogg`, `opus`, `webm`, `acc` + +--- + +### 视频嵌入 + +> [!note] +> 视频嵌入需要启用 `markdown.artPlayer` 插件才能正常工作。 + +**输入:** + +```md +![[video.mp4]] +``` + +**输出:** + +![[https://artplayer.org/assets/sample/video.mp4]] + +支持格式:`mp4`, `webm`, `mov` 等 + +--- + +### 内容片段嵌入 + +通过 `#标题` 可以嵌入指定标题下的内容片段: + +**输入:** + +```md +![[我的笔记]] +![[我的笔记#标题一]] +![[我的笔记#标题一#子标题]] +``` + +[Obsidian 官方 - 插入文件](https://obsidian.md/zh/help/embeds){.readmore} +[Obsidian 官方 - 文件格式](https://obsidian.md/zh/help/file-formats){.readmore} + +## 注释 + +使用 `%%` 包裹的内容会被当作注释,不会渲染到页面中。 + +### 语法 + +**行内注释:** + +```md +这是一个 %%行内注释%% 示例。 +``` + +**块级注释:** + +```md +%% +这是一个块级注释。 +可以跨越多行。 +%% +``` + +### 示例 + +**行内注释:** + +**输入:** + +```md +这是一个 %%行内注释%% 示例。 +``` + +**输出:** + +这是一个 %%行内注释%% 示例。 + +--- + +**块级注释:** + +**输入:** + +```md +注释之前的内容 + +%% +这是一个块级注释。 + +可以跨越多行。 +%% + +注释之后的内容 +``` + +**输出:** + +注释之前的内容 + +%% +这是一个块级注释。 + +可以跨越多行。 +%% + +注释之后的内容 + +[Obsidian 官方 - 注释](https://obsidian.md/zh/help/syntax#%E6%B3%A8%E9%87%8A){.readmore} + +## 配置 + +你可以在主题配置中启用或禁用这些插件: + +```ts title=".vuepress/config.ts" +export default defineUserConfig({ + theme: plumeTheme({ + plugins: { + mdPower: { + // Obsidian 兼容插件配置 + obsidian: { + wikiLink: true, // Wiki 链接 + embedLink: true, // 嵌入内容 + comment: true, // 注释 + }, + pdf: true, // PDF 嵌入功能 + artPlayer: true, // 视频嵌入功能 + } + } + }) +}) +``` + +### 配置项 + +:::: field-group + +::: field name="wikiLink" type="boolean" default="true" optional +启用 Wiki 链接语法 +::: + +::: field name="embedLink" type="boolean" default="true" optional +启用嵌入内容语法 +::: + +::: field name="comment" type="boolean" default="true" optional +启用注释语法 +::: + +:::: + +## 注意事项 + +- 这些插件提供的是 **兼容性支持**,并非完全实现 Obsidian 的全部功能 +- 部分 Obsidian 特有的功能(如内部链接的图谱视图、双向链接等)不在支持范围内 +- 嵌入内容时,被嵌入的页面也会参与主题的构建过程 +- PDF 嵌入需要同时启用 `pdf` 插件 +- 视频嵌入需要同时启用 `artPlayer` 插件 diff --git a/docs/tsconfig.json b/docs/tsconfig.json index fb07c0c9..a5bf43aa 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../tsconfig.base.json", "compilerOptions": { "baseUrl": ".", + "ignoreDeprecations": "6.0", "paths": { "~/themes/*": ["./.vuepress/themes/*"], "~/components/*": ["./.vuepress/themes/components/*"], diff --git a/eslint.config.js b/eslint.config.js index fee0c4ba..dd467cb5 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -10,6 +10,8 @@ export default config({ 'skills', 'docs/snippet/code-block.snippet.md', 'docs/snippet/whitespace.snippet.md', + 'docs/en/guide/markdown/obsidian.md', + 'docs/guide/markdown/obsidian.md', ], globals: { __VUEPRESS_VERSION__: 'readonly', diff --git a/plugins/plugin-md-power/__test__/obsidianCommentPlugin.spec.ts b/plugins/plugin-md-power/__test__/obsidianCommentPlugin.spec.ts new file mode 100644 index 00000000..1c00c1e6 --- /dev/null +++ b/plugins/plugin-md-power/__test__/obsidianCommentPlugin.spec.ts @@ -0,0 +1,72 @@ +import MarkdownIt from 'markdown-it' +import { describe, expect, it } from 'vitest' +import { commentPlugin } from '../src/node/obsidian/comment.js' + +describe('commentPlugin', () => { + const md = new MarkdownIt().use(commentPlugin) + + it('should ignore inline comment', () => { + const result = md.render('This is %%inline comment%% text.') + expect(result).not.toContain('inline comment') + expect(result).toContain('This is text.') + }) + + it('should ignore block comment', () => { + const result = md.render(`%% block comment %% +more text`) + expect(result).not.toContain('block comment') + expect(result).toContain('more text') + }) + + it('should handle multi-line block comment', () => { + const result = md.render(`%% +This is a block comment +spanning multiple lines +%% + +This is after.`) + expect(result).not.toContain('block comment') + expect(result).not.toContain('spanning multiple lines') + expect(result).toContain('This is after.') + }) + + it('should handle comment at start of line', () => { + const result = md.render('%%comment%% start') + expect(result).toContain('start') + expect(result).not.toContain('comment') + }) + + it('should handle empty comment', () => { + const result = md.render('%%%%') + expect(result).toBeDefined() + }) + + it('should not treat single % as comment', () => { + const result = md.render('50% off') + expect(result).toContain('50%') + expect(result).not.toContain('%%') + }) + + it('should handle nested content after comment', () => { + const result = md.render(`%% +block comment +%% + +## Heading + +paragraph`) + expect(result).toContain('