Merge branch 'main' into code-block-title

pull/5007/head
bluwy 1 month ago
commit e021c43e8e
No known key found for this signature in database

@ -19,7 +19,7 @@ Hi! We're really excited that you are interested in contributing to VitePress. B
- It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging.
- Commit messages must follow the [commit message convention](./commit-convention.md) so that changelogs can be automatically generated.
- Commit messages must follow the [commit message convention](/.github/commit-convention.md) so that changelogs can be automatically generated.
## Development Setup

@ -13,4 +13,4 @@
---
> [!TIP]
> The author of this PR can publish a _preview release_ by commenting `/publish` below.
> The author can publish a _preview release_ by commenting `/publish` after creating the PR.

@ -38,9 +38,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm

@ -18,7 +18,7 @@ jobs:
if: github.repository == 'vuejs/vitepress'
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
- uses: dessant/lock-threads@v6
with:
issue-inactive-days: 7
pr-inactive-days: 7

@ -12,7 +12,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v7
- name: Create Release for Tag
id: release_tag

@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
node_version: [20, 22, 24, latest]
node_version: [22, 24, 26]
include:
- os: windows-latest
node_version: 24
@ -27,13 +27,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v7
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6
- name: Set node version to ${{ matrix.node_version }}
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node_version }}
cache: pnpm

1
.gitignore vendored

@ -16,3 +16,4 @@ node_modules
pnpm-global
TODOs.md
*.timestamp-*.mjs
.claude

@ -7,3 +7,6 @@ template
temp
!CHANGELOG.md
.temp
# generated by scripts/subsetFonts.py
src/client/theme-default/styles/fonts.css

@ -1,3 +1,160 @@
## [2.0.0-alpha.19](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.18...v2.0.0-alpha.19) (2026-08-02)
### Bug Fixes
- **build:** apply rewrites when computing createContentLoader urls ([9e2148d](https://github.com/vuejs/vitepress/commit/9e2148d75ed966467a438403ad5b0a69df83480b))
- **build:** clear markdown cache with rewritten path on include change ([5eac447](https://github.com/vuejs/vitepress/commit/5eac4473189228da7a917a1a35a67233c94513c9)), closes [#5035](https://github.com/vuejs/vitepress/issues/5035)
- **build:** compose markdown `preConfig` hook when extending configs ([c39a85a](https://github.com/vuejs/vitepress/commit/c39a85a2ac88dca978d6a7b07fac3353fe0ae7fe)), closes [#5205](https://github.com/vuejs/vitepress/issues/5205)
- **build:** don't apply docsearch css transform to pages matching its filter ([fa0e48c](https://github.com/vuejs/vitepress/commit/fa0e48c8df8f66fb76f7df4d7d662a5c7b3c3c7c))
- **build:** don't rely on checkout directory name when externalizing types ([e6ba9d8](https://github.com/vuejs/vitepress/commit/e6ba9d8caa3866215095f63b62290f3110e523fd))
- **build:** report links to pre-rewrite paths of rewritten pages as dead ([3cf3f37](https://github.com/vuejs/vitepress/commit/3cf3f37b17054ff38347d4d0a2e76e03d68e2e7a))
- **build:** resolve additional configs by source path ([5e4a9e7](https://github.com/vuejs/vitepress/commit/5e4a9e799f620bcd3de973272d8343a515fd4992))
- **build:** resolve rewrites against externally injected pages too ([769c10e](https://github.com/vuejs/vitepress/commit/769c10ee65a61882724e2b748634326b576bb079))
- **build:** respect cleanUrls in content loader render ([027f046](https://github.com/vuejs/vitepress/commit/027f0461e073702c2d5b7e96006cf34a51825f45)), closes [#4331](https://github.com/vuejs/vitepress/issues/4331) [#5299](https://github.com/vuejs/vitepress/issues/5299)
- **build:** track include importers by module id ([c961655](https://github.com/vuejs/vitepress/commit/c9616559f55f378b92e2f3b8bac31336db540699))
- **build:** prevent duplicate IDs in MiniSearch ([#5303](https://github.com/vuejs/vitepress/issues/5303)) ([505278c](https://github.com/vuejs/vitepress/commit/505278c2cb06c5ed4ca094b193d6dc912376818c))
- **build:** remove stackTraceLimit Infinity for DEBUG ([#5318](https://github.com/vuejs/vitepress/issues/5318)) ([865d04b](https://github.com/vuejs/vitepress/commit/865d04b2528863f265e39e1b05278c9654f8599a))
- **build:** retry file reads when out of file descriptors ([da71173](https://github.com/vuejs/vitepress/commit/da71173afc8eece9af49ff25ad1bff9bbd676096))
- **cli:** avoid onAfterConfigResolve ([3fbaf9c](https://github.com/vuejs/vitepress/commit/3fbaf9c4d21e62062490c58c13cbf1b074417004))
- **client:** make the route the single source of truth for the URL hash ([dcb7a75](https://github.com/vuejs/vitepress/commit/dcb7a75532c5472060ec379d25a5fafbc7932637))
- **markdown:** preserve user-defined attributes ([3f4530b](https://github.com/vuejs/vitepress/commit/3f4530b0e2b339cc408ab38b8b922a9834bb396e)), closes [#5123](https://github.com/vuejs/vitepress/issues/5123)
- **markdown:** remove extra whitespace from container markup ([b6d9cb8](https://github.com/vuejs/vitepress/commit/b6d9cb83c747f9a021aa277b0c64b5bf3a40bf41))
- **markdown:** skip circular includes ([3372516](https://github.com/vuejs/vitepress/commit/3372516152df0b871634492ac6d3207bedf89d4e))
- **markdown:** remove deprecated `cjkFriendly` option ([27762ea](https://github.com/vuejs/vitepress/commit/27762eac86aa5c5d998de128734c2a8c10f78e23))
- **markdown:** rename image option `lazyLoading` to `lazyLoad` ([078786a](https://github.com/vuejs/vitepress/commit/078786a1b3e0793f55cb14d819df93900041ccb0))
- **search:** index changed files with srcDir-relative paths in dev ([6b64f35](https://github.com/vuejs/vitepress/commit/6b64f3561b0da8fcf48a6ccff7b08036ad858ef3)), closes [#3374](https://github.com/vuejs/vitepress/issues/3374)
- **search:** only index pages on dev updates ([559fb24](https://github.com/vuejs/vitepress/commit/559fb24a2238b2c0b97c36c808c092dda98bb2f7))
- **search:** serve a fresh search index after dev updates ([1534a67](https://github.com/vuejs/vitepress/commit/1534a67d866e4922a7eab1a1c166db7f11855c59))
- **search:** skip pages that fail to render instead of crashing dev server ([3ffefa2](https://github.com/vuejs/vitepress/commit/3ffefa2550d1242da4a798e18cb308cb33ad815b))
- **theme:** use ul and li for lists ([#5326](https://github.com/vuejs/vitepress/issues/5326)) ([3f99872](https://github.com/vuejs/vitepress/commit/3f99872468f861458f4f37d6115cfd1c1a7b15ae))
- **theme:** align docsearch breakpoints with the default theme ([90c28d4](https://github.com/vuejs/vitepress/commit/90c28d41ce0a67f6e096ce9fd355f3b0c7b1dab7)), closes [#5213](https://github.com/vuejs/vitepress/issues/5213)
- **theme:** align local search breakpoint ([#5217](https://github.com/vuejs/vitepress/issues/5217)) ([a425113](https://github.com/vuejs/vitepress/commit/a425113572d94c39d0c3369eac5adb7a87cfb798))
- **theme:** correct anchor scroll margins across viewports ([dabc5e9](https://github.com/vuejs/vitepress/commit/dabc5e95ce210ae14cb33a08e6784826e4e2d544))
- **theme:** ensure outline marker follows click ([#3879](https://github.com/vuejs/vitepress/issues/3879)) ([31287c0](https://github.com/vuejs/vitepress/commit/31287c0b69b330bc1645268b1bebd21f9a40c238))
- **theme:** external link icon not showing in navbar links ([225c94a](https://github.com/vuejs/vitepress/commit/225c94afd2c355a33fbbb93871fb9b064da475b8)), closes [#5306](https://github.com/vuejs/vitepress/issues/5306)
- **theme:** pass target and rel to prev/next page links ([#5297](https://github.com/vuejs/vitepress/issues/5297)) ([6b5e770](https://github.com/vuejs/vitepress/commit/6b5e7704a01500d87a3702f7b27f95a4bdcfa10d))
- **theme:** preserve url params when switching languages ([#5312](https://github.com/vuejs/vitepress/issues/5312)) ([9ee401d](https://github.com/vuejs/vitepress/commit/9ee401d7adefc39fd960990cc032be5464e4eb27))
- **theme:** prevent TypeError when navigating to page without outline ([#5329](https://github.com/vuejs/vitepress/issues/5329)) ([9376c58](https://github.com/vuejs/vitepress/commit/9376c58abec557dd8c5b63f991a1d1068586f175))
- **theme:** remove font-synthesis style ([#5309](https://github.com/vuejs/vitepress/issues/5309)) ([c34769c](https://github.com/vuejs/vitepress/commit/c34769c2e67969881b9cc8abbccf6d3cc6a5b647))
- **theme:** safari not showing external link icon properly ([7118402](https://github.com/vuejs/vitepress/commit/711840222700804dbb6fb39ee9b9580a3e6220e7))
- **theme:** rebuild the base styles on tailwind's preflight ([f1ee913](https://github.com/vuejs/vitepress/commit/f1ee91335ecc562511f5e97214f41c043e6944fa))
- **theme:** route cjk punctuation to matching system fonts ([91b06b6](https://github.com/vuejs/vitepress/commit/91b06b67a302ebfa210e888fe9d03d27fb291b31))
- **theme:** remove deprecated `disableDetailedView` local search option ([cec4998](https://github.com/vuejs/vitepress/commit/cec499869f02313337993a7f2ad381f0f9d9dafd))
- **theme:** remove deprecated `lastUpdatedText` option ([18d1b47](https://github.com/vuejs/vitepress/commit/18d1b4713c6634cc60e6b4a95430e05d51ec4812))
- **theme:** remove deprecated `outlineTitle` option ([95c0420](https://github.com/vuejs/vitepress/commit/95c042039c62a9235e223f8da05a2075aa2234d7))
- **types:** declare Badge as a global component ([ca8ba5b](https://github.com/vuejs/vitepress/commit/ca8ba5baec6c8fa551ee29917bd072f84406e68b))
- **types:** import EnhanceAppContext from package entry in theme.d.ts ([8ff1f3e](https://github.com/vuejs/vitepress/commit/8ff1f3e2dd34fd443c0fa80cf3962f9d67e81cf5)), closes [#5156](https://github.com/vuejs/vitepress/issues/5156)
### Features
- add web-types.json for JetBrains IDE support ([1d448c9](https://github.com/vuejs/vitepress/commit/1d448c9fc1ec90849c2a18c49cab5e275f3dc019)), closes [#5157](https://github.com/vuejs/vitepress/issues/5157)
- auto-add width/height to local images to avoid layout shift ([#5311](https://github.com/vuejs/vitepress/issues/5311)) ([3868b64](https://github.com/vuejs/vitepress/commit/3868b64e419223279eaae800766b244cde9bc85f))
- **client:** declare $frontmatter and $params as ComponentCustomProperties ([0535009](https://github.com/vuejs/vitepress/commit/0535009f44b2f9ddf8e30b59700bacdb9a9b5e75))
- **client:** declare Content and ClientOnly components as GlobalComponents ([#5156](https://github.com/vuejs/vitepress/issues/5156)) ([6629577](https://github.com/vuejs/vitepress/commit/6629577b48fe2785cf6865197cb326eff9f2a9d8)), closes [#5154](https://github.com/vuejs/vitepress/issues/5154)
- **cli:** show vite version in startup log ([#5328](https://github.com/vuejs/vitepress/issues/5328)) ([4666fc2](https://github.com/vuejs/vitepress/commit/4666fc277609f8bb916e6a54eb0ac9327784d073))
- enable metaChunk by default ([#5325](https://github.com/vuejs/vitepress/issues/5325)) ([8e42f31](https://github.com/vuejs/vitepress/commit/8e42f3159a1d5d8576bee0b736f56e943d3cb858))
- **init:** use async fs in scaffolding ([38b59f9](https://github.com/vuejs/vitepress/commit/38b59f925206120b1a20b1fcfb650b45f8147175))
- **markdown:** accept booleans for plugin options ([c2be0bf](https://github.com/vuejs/vitepress/commit/c2be0bf936e46c196151b79ba683a30490f2d164))
- **markdown:** add region marker engine ([b303dd3](https://github.com/vuejs/vitepress/commit/b303dd341df7c0ca1e5b63c086084b2baad68c19))
- **markdown:** allow disabling table `tabindex` attribute ([bffe1e1](https://github.com/vuejs/vitepress/commit/bffe1e14125220d465a94cc629260e19bff48e0c))
- **markdown:** support disabling built-in markdown plugins ([b8d9c8f](https://github.com/vuejs/vitepress/commit/b8d9c8f9a92ec4e8c877d6c28fee26b3c379876c)), closes [#4484](https://github.com/vuejs/vitepress/issues/4484) [#4556](https://github.com/vuejs/vitepress/issues/4556)
- **markdown:** support footnotes ([430a890](https://github.com/vuejs/vitepress/commit/430a890a17910593e26e9654b141ef3855ecceac))
- **markdown:** support GitHub-style task lists ([97f8781](https://github.com/vuejs/vitepress/commit/97f87817ead5b7a26e0dfc535cfd60ec0597d130)), closes [#413](https://github.com/vuejs/vitepress/issues/413) [#1923](https://github.com/vuejs/vitepress/issues/1923) [#3648](https://github.com/vuejs/vitepress/issues/3648) [#5110](https://github.com/vuejs/vitepress/issues/5110)
- **markdown:** support per-locale markdown strings ([faaa4a1](https://github.com/vuejs/vitepress/commit/faaa4a124ed7a5b2f1cfc8f9b7686a3785d2012b)), closes [#4431](https://github.com/vuejs/vitepress/issues/4431)
- **markdown:** support registering custom containers ([962f00e](https://github.com/vuejs/vitepress/commit/962f00e7a3c22dd021729e732e12a758b42aef1c)), closes [#3591](https://github.com/vuejs/vitepress/issues/3591) [#3603](https://github.com/vuejs/vitepress/issues/3603) [#4228](https://github.com/vuejs/vitepress/issues/4228)
- **markdown:** support title-less containers ([4c7a030](https://github.com/vuejs/vitepress/commit/4c7a030bc1eecd0ed441a2f56cde73e59ff90abe)), closes [#4928](https://github.com/vuejs/vitepress/issues/4928) [#4932](https://github.com/vuejs/vitepress/issues/4932) [#4929](https://github.com/vuejs/vitepress/issues/4929)
- **markdown:** rebase relative urls in included files ([6a337ef](https://github.com/vuejs/vitepress/commit/6a337efece9697dcb1e4ae4fe657af97089570f6))
- **markdown:** group code copy button strings into one option ([2fa0ded](https://github.com/vuejs/vitepress/commit/2fa0dedbb241e901569c2e20690e8e5917f4bd26)), closes [#4431](https://github.com/vuejs/vitepress/issues/4431)
- **markdown:** replace markdown-it-attrs with @mdit/plugin-attrs ([18380b0](https://github.com/vuejs/vitepress/commit/18380b0eb78765f49d86918c86c9fee9e7197135))
- **markdown:** replace markdown-it-emoji with @mdit/plugin-emoji ([018887f](https://github.com/vuejs/vitepress/commit/018887fa1d03031e9c6cc96606be22df51581e35))
- **markdown:** replace markdown-it-anchor with @mdit/plugin-anchor ([7550517](https://github.com/vuejs/vitepress/commit/75505179160bf16a88cd5648719615e982c08e41))
- **markdown:** support `attrs: false` for disabling attrs plugin ([e235dbe](https://github.com/vuejs/vitepress/commit/e235dbeb8aef1213d0de9efafe0ccb758acd267a))
- **markdown:** support `note`, `important`, `caution` markdown containers ([#5161](https://github.com/vuejs/vitepress/issues/5161)) ([3b560a0](https://github.com/vuejs/vitepress/commit/3b560a0efa8bdbf6f621413b3e8a27b19f4a638f)), closes [#4427](https://github.com/vuejs/vitepress/issues/4427) [#3928](https://github.com/vuejs/vitepress/issues/3928)
- **theme:** allow internal social links ([51ff681](https://github.com/vuejs/vitepress/commit/51ff681f4e5caf7dfff1f5c1c79a70aaf4785081)), closes [#5305](https://github.com/vuejs/vitepress/issues/5305)
- **theme:** cover all Inter glyphs, generate font subsets from a script ([705c1be](https://github.com/vuejs/vitepress/commit/705c1be0f88643f9fdbeb09d1835374d70afbebd))
- update option stability annotations ([ab1896f](https://github.com/vuejs/vitepress/commit/ab1896fbf4da4b90f900d845a8410b7350ea9edd))
### Performance Improvements
- **build:** reuse lastUpdated from markdown rendering in sitemap generation ([fdd68e3](https://github.com/vuejs/vitepress/commit/fdd68e37a720d2fd898033c5f695e125ee93634b))
- **md:** bypass gray-matter's unbounded cache ([4f8703d](https://github.com/vuejs/vitepress/commit/4f8703d61ccefb61823d4f7bac67f142759d81ca))
- **md:** limit the compile cache's memory usage ([2fb6bda](https://github.com/vuejs/vitepress/commit/2fb6bdabf03b617ccfa1d00c9261242bc5d93e56))
- use hook filters in vite plugins ([fa24c6d](https://github.com/vuejs/vitepress/commit/fa24c6d67b4a572a56ac76aeb9f97ca3cd03170d))
### Reverts
- Revert "fix: prevent DocSearch SVG clipping in WebKit" (#5304) ([c8313a4](https://github.com/vuejs/vitepress/commit/c8313a4bcd24af21cd829d7c4fd3792ecd0c17a7)), closes [#5304](https://github.com/vuejs/vitepress/issues/5304) [#5240](https://github.com/vuejs/vitepress/issues/5240)
### BREAKING CHANGES
- `markdown.anchor` options are now typed by `@mdit/plugin-anchor`. Common options (`level`, `slugify`, `permalink`, `getTokensText`, `tabIndex`, etc.) are unchanged, but the deprecated markdown-it-anchor permalink options (`permalinkSymbol`, `renderPermalink`, ...) are no longer accepted. Permalink builders like `headerLink` are named exports of `@mdit/plugin-anchor` instead of properties of the plugin.
- `markdown.attrs` options are now typed by `@mdit/plugin-attrs`: `leftDelimiter`, `rightDelimiter`, and `allowedAttributes` are renamed to `left`, `right`, and `allowed`. A `rule` option is available for toggling individual attribute rules (VitePress disables `fence` by default).
- `markdown.codeCopyButtonTitle` is now `markdown.codeCopyButton.tooltipText`, and its default changed from "Copy Code" to "Copy code". The `--vp-code-copy-copied-text-content` CSS variable and its built-in per-language `:lang()` defaults are removed - set `codeCopyButton.copiedText` (per locale) instead.
- `useData().hash` has been removed. Read the hash from `useRoute()` instead.
- callers of vitepress' build() will now notice the version banner gets printed. To disable that, pass a noop function:
```ts
build(root, {
onAfterConfigResolve() {}
})
```
- custom `themeConfig.i18nRouting` functions now receive the current `Route` as their second argument instead of the hash
- normalize-level defaults differ from the old reset - native select and number-input chrome is restored, headings inherit font size and weight, the hidden attribute is enforced with !important, and every element starts with zero margin/padding and border-style: solid. Sites layering custom CSS on the default theme may notice.
- relative urls in included markdown files resolve against the included file rather than the including page. Partials written for one specific location may need their links updated, or markdown.include.rebaseRelativeUrls set to false to keep resolving them from the including page. Absolute and external urls are unaffected. Note that the marker comments shift the line numbers reported for dead links following an include, which already pointed into the include-expanded source rather than the original file.
- **init:** scaffold() now returns a `Promise<string>` and must be awaited.
- The `defs` property of `markdown.emoji` has been renamed to `definitions`.
- The `markdown.attrs.disable` option has been removed. Set `markdown.attrs` to `false` instead.
- The `markdown.image.lazyLoading` option has been renamed to `markdown.image.lazyLoad`.
- The deprecated `disableDetailedView` option of local search has been removed. Use `detailedView: false` instead.
- The deprecated `markdown.cjkFriendly` option has been removed. Use `markdown.cjkFriendlyEmphasis` instead.
- The deprecated `themeConfig.lastUpdatedText` option has been removed. Use `themeConfig.lastUpdated.text` instead.
- The deprecated `themeConfig.outlineTitle` option has been removed. Use `themeConfig.outline.label` instead.
- The Inter4CJK font family has been renamed to 'Inter Core'. Custom --vp-font-family-base overrides referencing Inter4CJK must be updated.
## [2.0.0-alpha.18](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.17...v2.0.0-alpha.18) (2026-07-06)
### Bug Fixes
- **build:** apply `base` to links with download attribute ([#5186](https://github.com/vuejs/vitepress/issues/5186)) ([01987c4](https://github.com/vuejs/vitepress/commit/01987c4c0838478a8fda81f427775f28476c5557))
- **build:** normalize rewrite drive letters ([#5245](https://github.com/vuejs/vitepress/issues/5245)) ([5c50b99](https://github.com/vuejs/vitepress/commit/5c50b99724815a6fb3d2311e801dcad4aeb9b412))
- **build:** show dead link line numbers ([#5230](https://github.com/vuejs/vitepress/issues/5230)) ([c37bde6](https://github.com/vuejs/vitepress/commit/c37bde6308fb9f202e224f5eb38c0fac35468ea6))
- compose markdown config when extending configs ([#5236](https://github.com/vuejs/vitepress/issues/5236)) ([04f4fba](https://github.com/vuejs/vitepress/commit/04f4fbadbdc05c75dbf3564c21404540d570d43b))
- delete undefined values while merging sidepanel props ([cc30b10](https://github.com/vuejs/vitepress/commit/cc30b10b60beb862f85915055dc97651703bf250))
- disable pluginTimings and invalidAnnotation for now ([9315fc1](https://github.com/vuejs/vitepress/commit/9315fc182229f13ee793dc44107947b2fe6ab50e))
- don't invalidate framework chunk when a new asset is added ([c0e2e18](https://github.com/vuejs/vitepress/commit/c0e2e1809464c48435a22a0aa9468ff5e562791d))
- escape description in head ([d96bf1d](https://github.com/vuejs/vitepress/commit/d96bf1dc616599609d8a24af7183aee6a7b9ae07))
- index rewritten local search pages by locale ([#5241](https://github.com/vuejs/vitepress/issues/5241)) ([80cf265](https://github.com/vuejs/vitepress/commit/80cf2650aa5fa4b49093509f60766cc5b28c19bc))
- keep translation links in the current tab ([#5158](https://github.com/vuejs/vitepress/issues/5158)) ([202ee70](https://github.com/vuejs/vitepress/commit/202ee7026054ac5c721bbdbf196426628b0c9b18))
- normalize `/index` to `/` ([856858d](https://github.com/vuejs/vitepress/commit/856858d26a78f2e19e2c8ee23c2e85a95dbfdd29)), closes [#5165](https://github.com/vuejs/vitepress/issues/5165)
- preserve Agent Studio DocSearch options ([#5254](https://github.com/vuejs/vitepress/issues/5254)) ([f29ffdb](https://github.com/vuejs/vitepress/commit/f29ffdbb33022eb41327fec836f9cfbc16bf01d8))
- preserve external sidebar links with base ([#5243](https://github.com/vuejs/vitepress/issues/5243)) ([ddf178a](https://github.com/vuejs/vitepress/commit/ddf178a170967527bafe7c9b262fb66aa10ec9de))
- prevent DocSearch SVG clipping in WebKit ([#5240](https://github.com/vuejs/vitepress/issues/5240)) ([a357e5e](https://github.com/vuejs/vitepress/commit/a357e5ef67ed266861877eb08da753b2141a784f))
- strip frontmatter before heading includes ([#5246](https://github.com/vuejs/vitepress/issues/5246)) ([e68fade](https://github.com/vuejs/vitepress/commit/e68fade75d2259b10695e85277f5483a084e3ae7))
- **theme:** avatars misaligned in team member cards ([6730fb8](https://github.com/vuejs/vitepress/commit/6730fb84620c852b516de59f6f7c39c4f04f0e37)), closes [#5160](https://github.com/vuejs/vitepress/issues/5160)
- **theme:** correct mixed LTR/RTL text rendering in code blocks ([73f7b0b](https://github.com/vuejs/vitepress/commit/73f7b0b984853758d41e897dd43e5e93a1066266))
- **theme:** keep external link icon inline ([#5232](https://github.com/vuejs/vitepress/issues/5232)) ([756a88c](https://github.com/vuejs/vitepress/commit/756a88cfa2f8f71400362327d2255cb8b5ccacfa))
- **theme:** prevent `sub` and `sup` elements from affecting line height ([19357f9](https://github.com/vuejs/vitepress/commit/19357f9d337472572a500b8d2af8ef97932bfdda)), closes [#5173](https://github.com/vuejs/vitepress/issues/5173)
- use resolveDynamicComponent instead of resolveComponent ([9da1e3e](https://github.com/vuejs/vitepress/commit/9da1e3e70f41b7b8cb81f791307c778d65854f7a))
### Features
- add macOS local search navigation shortcuts ([#5237](https://github.com/vuejs/vitepress/issues/5237)) ([e635e9e](https://github.com/vuejs/vitepress/commit/e635e9e5ea2876a293954970c6a00d6e09cb50a5))
- add support for `format` option in Carbon options ([#5188](https://github.com/vuejs/vitepress/issues/5188)) ([6ee01bf](https://github.com/vuejs/vitepress/commit/6ee01bf30534cbf7847fe8c94fbf23efd9637f68))
- allow custom i18n routing ([#5239](https://github.com/vuejs/vitepress/issues/5239)) ([eef5742](https://github.com/vuejs/vitepress/commit/eef57427f4861939ff27f07d71587e68118064e2))
- allow VPContent to use custom components ([#5176](https://github.com/vuejs/vitepress/issues/5176)) ([c0b38d5](https://github.com/vuejs/vitepress/commit/c0b38d52c270e4efb59effa8ab7207535c48ec05))
- **markdown:** expose Shiki color replacements in markdown options ([#5153](https://github.com/vuejs/vitepress/issues/5153)) ([fccc617](https://github.com/vuejs/vitepress/commit/fccc6171024f1b0087dd963dbe5a6236081b5d3d))
- migrate to vite 8 ([228eef1](https://github.com/vuejs/vitepress/commit/228eef187ae33d1676a79e466d289c0e6e9ab321))
- show local search loading state ([#5252](https://github.com/vuejs/vitepress/issues/5252)) ([7e2273a](https://github.com/vuejs/vitepress/commit/7e2273a3e470da2777c2510b51262e85879f2ee9))
- support scroll-margin / scroll-padding ([6cce766](https://github.com/vuejs/vitepress/commit/6cce76685da39f8b5c75da047f847f82f70b9c4e))
- support social link target option ([#5242](https://github.com/vuejs/vitepress/issues/5242)) ([d0159c8](https://github.com/vuejs/vitepress/commit/d0159c8a850cbd2a010a3d44bdeb97c3db651d0e))
### BREAKING CHANGES
- VitePress now uses Vite 8. If you are using Vite plugins in your config, please check the [Vite 8 migration guide](https://vite.dev/guide/migration) for any breaking changes that may affect you.
- Node 20 support is dropped. v22 or higher is needed.
- `scrollOffset` from config is removed. Users wanting to customize scroll offset should customize `scroll-margin-top` via CSS instead. `smoothScroll` support from `router.go` is also removed as it didn't work as expected for most users. Users wanting smooth scrolling should set `scroll-behavior: smooth` in CSS, ideally inside a `@media (prefers-reduced-motion: no-preference)` block.
## [2.0.0-alpha.17](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.16...v2.0.0-alpha.17) (2026-03-19)
### Bug Fixes
@ -97,6 +254,7 @@ Users who intentionally reference non-existent files or want to document include
- `markdown-it-attrs` is disabled for fenced code blocks. For most users no change is required. If you want to add classes to code blocks, do it using shiki transformers instead.
- Rename `cjkFriendly` to `cjkFriendlyEmphasis` in your vitepress config. Most people should be unaffected unless they want to disable the CJK emphasis behavior added v2.0.0-alpha.12.
- `useData().lastUpdated` now defaults to 0 if `lastUpdated` is enabled in config. No change is needed unless you were manually doing strict comparison in your layout or components.
## [2.0.0-alpha.12](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.11...v2.0.0-alpha.12) (2025-08-20)
@ -776,7 +934,7 @@ Users who intentionally reference non-existent files or want to document include
### BREAKING CHANGES
- VitePress now runs on Vite 5. Please refer https://vitejs.dev/guide/migration for breaking changes and migration guide if you're relying on some Vite-specific things.
- VitePress now runs on Vite 5. Please refer https://vite.dev/guide/migration for breaking changes and migration guide if you're relying on some Vite-specific things.
# [1.0.0-rc.25](https://github.com/vuejs/vitepress/compare/v1.0.0-rc.24...v1.0.0-rc.25) (2023-11-05)

@ -32,8 +32,9 @@ Copyright (c) 2019-present, Yuxi (Evan) You
This project would not be possible without the support of these amazing companies and tools.
| | | |
| ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| <a href="https://voidzero.dev" target="_blank"><img alt="VoidZero's logo" src="https://github.com/voidzero-dev.png" width="80" /></a> | **[VoidZero](https://voidzero.dev)** | The company behind Vite, Vitest, Rolldown, and Oxc. VoidZero has been a long-time sponsor of VitePress. VitePress itself is built on top of this ecosystem of high-performance JavaScript tooling. |
| <a href="https://stackblitz.com" target="_blank"><img alt="StackBlitz's logo" src="https://github.com/stackblitz.png" width="80" /></a> | **[StackBlitz](https://stackblitz.com)** | Creators of WebContainers and a browser-based development platform. Their technology powers the VitePress playground and provides instant preview releases for our pull requests and commits. |
| <a href="https://algolia.com" target="_blank"><img alt="Algolia's logo" src="https://github.com/algolia.png" width="80" /></a> | **[Algolia](https://algolia.com)** | An AI-powered search platform that provides fast, scalable search. Algolia powers DocSearch for VitePress. Their team has been a long-time partner in building and maintaining our search integration. |
| <a href="https://browserstack.com" target="_blank"><img alt="BrowserStack's logo" src="https://github.com/browserstack.png" width="80" /></a> | **[BrowserStack](https://browserstack.com)** | A cloud-based testing platform for websites and mobile apps. BrowserStack provides the cross-browser and real-device infrastructure used to ensure VitePress works consistently across all browsers. |
| <a href="https://netlify.com" target="_blank"><img alt="Netlify's logo" src="https://github.com/netlify.png" width="80" /></a> | **[Netlify](https://netlify.com)** | A platform for building, deploying, and hosting modern web projects. Netlify powers our website deployments with their generous open source plan and provides deploy previews for the pull requests. |

@ -155,13 +155,19 @@ export default defineConfig({
title: 'Example',
description: 'An example app using VitePress.',
markdown: {
image: {
lazyLoading: true
}
image: { lazyLoad: true }
},
themeConfig: {
nav,
sidebar,
socialLinks: [
{
icon: 'github',
link: '/home',
ariaLabel: 'Home social link',
target: '_self'
}
],
search: {
provider: 'local',
options: {

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -1,4 +1,4 @@
import fs from 'node:fs/promises'
import { writeFile, unlink } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
describe('static data file support in vite 3', () => {
@ -48,20 +48,20 @@ describe('static data file support in vite 3', () => {
const b = fileURLToPath(new URL('./data/b.json', import.meta.url))
try {
await fs.writeFile(a, JSON.stringify({ a: false }, null, 2) + '\n')
await writeFile(a, JSON.stringify({ a: false }, null, 2) + '\n')
await page.waitForFunction(
() =>
document.querySelector('pre#basic')?.textContent ===
JSON.stringify([{ a: false }, { b: true }], null, 2)
)
} finally {
await fs.writeFile(a, JSON.stringify({ a: true }, null, 2) + '\n')
await writeFile(a, JSON.stringify({ a: true }, null, 2) + '\n')
}
let err = true
try {
await fs.unlink(b)
await unlink(b)
await page.waitForFunction(
() =>
document.querySelector('pre#basic')?.textContent ===
@ -70,19 +70,19 @@ describe('static data file support in vite 3', () => {
err = false
} finally {
if (err) {
await fs.writeFile(b, JSON.stringify({ b: true }, null, 2) + '\n')
await writeFile(b, JSON.stringify({ b: true }, null, 2) + '\n')
}
}
try {
await fs.writeFile(b, JSON.stringify({ b: false }, null, 2) + '\n')
await writeFile(b, JSON.stringify({ b: false }, null, 2) + '\n')
await page.waitForFunction(
() =>
document.querySelector('pre#basic')?.textContent ===
JSON.stringify([{ a: true }, { b: false }], null, 2)
)
} finally {
await fs.writeFile(b, JSON.stringify({ b: true }, null, 2) + '\n')
await writeFile(b, JSON.stringify({ b: true }, null, 2) + '\n')
}
})

@ -0,0 +1,5 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent
export default component
}

@ -32,4 +32,13 @@ describe('render correct content', async () => {
const outlineLinksCount = await outlineLinksLocator.count()
expect(outlineLinksCount).toEqual(4)
})
test('social link target override', async () => {
const socialLink = page.locator(
'.VPNavBarSocialLinks a[aria-label="Home social link"]'
)
expect(await socialLink.getAttribute('href')).toBe('/home')
expect(await socialLink.getAttribute('target')).toBe('_self')
})
})

@ -3,15 +3,69 @@ describe('local search', () => {
await goto('/')
})
test.runIf(!process.env.VITE_TEST_BUILD)(
'shows progress while loading search index',
async () => {
const indexRoute = /@localSearchIndexroot/
let delayedIndex = false
await page.route(indexRoute, async (route) => {
delayedIndex = true
await new Promise((resolve) => setTimeout(resolve, 800))
await route.continue()
})
try {
await page.locator('.VPNavBarSearchButton').click()
const loading = page.locator('.search-loading')
const results = page.locator('.results')
await page.waitForFunction(() =>
document
.querySelector('.search-loading')
?.classList.contains('active')
)
expect(delayedIndex).toBe(true)
expect(await loading.getAttribute('role')).toBe('status')
expect(await loading.getAttribute('aria-label')).toBe(
'Loading search results'
)
expect(await results.getAttribute('aria-busy')).toBe('true')
await page.waitForFunction(
() =>
!document
.querySelector('.search-loading')
?.classList.contains('active')
)
expect(await results.getAttribute('aria-busy')).toBe('false')
} finally {
await page.unroute(indexRoute)
}
}
)
test('exclude content from search results', async () => {
await page.locator('.VPNavBarSearchButton').click()
const input = await page.waitForSelector('input#localsearch-input')
await input.type('local')
await page.waitForSelector('ul#localsearch-list', { state: 'visible' })
const searchResults = page.locator('#localsearch-list')
await page.waitForFunction(() => {
const options = [
...document.querySelectorAll('#localsearch-list li[role=option]')
]
return (
options.length === 1 &&
options[0].textContent?.includes('Local search included')
)
})
expect(await searchResults.locator('li[role=option]').count()).toBe(1)
expect(
@ -28,4 +82,69 @@ describe('local search', () => {
.count()
).toBe(0)
})
test('uses the same desktop breakpoint as the nav bar', async () => {
try {
for (const { width, isDesktop } of [
{ width: 767, isDesktop: false },
{ width: 768, isDesktop: true }
]) {
await page.setViewportSize({ width, height: 600 })
await goto('/')
await page.locator('.VPNavBarSearchButton').click()
await page.waitForSelector('input#localsearch-input')
expect(await page.locator('.VPNavBarHamburger').isVisible()).toBe(
!isDesktop
)
expect(await page.locator('.search-actions.before').isVisible()).toBe(
!isDesktop
)
}
} finally {
await page.setViewportSize({ width: 1280, height: 720 })
}
})
test('navigate results with macOS Ctrl shortcuts', async () => {
await page.evaluate(() => document.documentElement.classList.add('mac'))
await page.locator('.VPNavBarSearchButton').click()
const input = await page.waitForSelector('input#localsearch-input')
await input.type('lorem')
await page.waitForFunction(() => {
return (
document.querySelectorAll('#localsearch-list li[role=option]').length >
1
)
})
expect(await input.getAttribute('aria-activedescendant')).toBe(
'localsearch-item-0'
)
await pressMacCtrl('n')
expect(await input.getAttribute('aria-activedescendant')).toBe(
'localsearch-item-1'
)
await pressMacCtrl('p')
expect(await input.getAttribute('aria-activedescendant')).toBe(
'localsearch-item-0'
)
})
})
function pressMacCtrl(key: string) {
return page.evaluate((key) => {
window.dispatchEvent(
new KeyboardEvent('keydown', {
key,
ctrlKey: true,
bubbles: true,
cancelable: true
})
)
}, key)
}

@ -56,6 +56,18 @@ This is a dangerous warning.
This is a details block.
:::
::: note
This is a note.
:::
::: important
This is an important note.
:::
::: caution
This is a caution note.
:::
### Custom Title
::: danger STOP

@ -0,0 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["**/*", ".vitepress/**/*"]
}

@ -1,6 +1,6 @@
import fs from 'fs-extra'
import getPort from 'get-port'
import { nanoid } from 'nanoid'
import { rm } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath, URL } from 'node:url'
import { chromium } from 'playwright-chromium'
@ -33,13 +33,13 @@ const variations = themes.flatMap((theme) =>
afterAll(async () => {
await page.close()
await browser.close()
await fs.remove(tempDir)
await rm(tempDir, { recursive: true, force: true })
})
test.each(variations)('init %s', async (_, { theme, useTs }) => {
const root = getTempRoot()
await fs.remove(root)
scaffold({ root, theme, useTs, injectNpmScripts: false })
await rm(root, { recursive: true, force: true })
await scaffold({ root, theme, useTs, injectNpmScripts: false })
const port = await getPort()
const server = await createServer(root, { port })

@ -0,0 +1,3 @@
{
"extends": "../tsconfig.json"
}

@ -1,13 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": true,
"isolatedModules": false,
"baseUrl": ".",
"types": ["node", "vitest/globals"],
"paths": {
"client/*": ["../src/client/*"],
"node/*": ["../src/node/*"],
"shared/*": ["../src/shared/*"]
}
"types": ["node", "vitest/globals"]
}
}

@ -0,0 +1,146 @@
import { resolveLocaleLink } from 'client/theme-default/composables/langs'
import type { Route, VitePressData } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme'
import { ref } from 'vue'
// `currentPage` is the current page's relative path (like
// `route.data.relativePath`, but with a leading slash), plus any query and
// hash of the current URL.
function resolve(
currentPage: string,
{
themeConfig = {},
cleanUrls = false,
targetLocale = 'fr',
targetLocaleLink = '/fr/',
currentLocaleLink = '/',
linkToCorrespondingPage = true
}: {
themeConfig?: DefaultTheme.Config
cleanUrls?: boolean
targetLocale?: string
targetLocaleLink?: string
currentLocaleLink?: string
linkToCorrespondingPage?: boolean
} = {}
) {
const { pathname, search, hash } = new URL(currentPage, 'http://a.com')
const route = {
data: { relativePath: pathname.slice(1) },
query: search,
hash
} as Route
const data = {
site: ref({
cleanUrls,
locales: {
root: { label: 'English', lang: 'en-US' },
fr: { label: 'Français', lang: 'fr-FR', link: '/fr/' }
},
themeConfig
}),
theme: ref(themeConfig)
} as unknown as VitePressData<DefaultTheme.Config>
return resolveLocaleLink(data, route, {
targetLocale,
targetLocaleLink,
currentLocaleLink,
linkToCorrespondingPage
})
}
describe('client/theme-default/composables/langs', () => {
describe('resolveLocaleLink', () => {
describe('locale home links (linkToCorrespondingPage: false)', () => {
test('links to the target locale home', () => {
expect(
resolve('/guide/getting-started.md', {
linkToCorrespondingPage: false
})
).toBe('/fr/')
})
test('preserves query and hash', () => {
expect(
resolve('/guide/getting-started.md?a=1#install', {
linkToCorrespondingPage: false
})
).toBe('/fr/?a=1#install')
})
test('ignores custom i18n routing functions', () => {
expect(
resolve('/guide/getting-started.md', {
linkToCorrespondingPage: false,
themeConfig: { i18nRouting: () => '/custom/' }
})
).toBe('/fr/')
})
})
describe('corresponding page links (linkToCorrespondingPage: true)', () => {
test('rewrites the current page path into the target locale', () => {
expect(resolve('/guide/getting-started.md#install')).toBe(
'/fr/guide/getting-started.html#install'
)
})
test('drops the .html extension when clean URLs are enabled', () => {
expect(
resolve('/guide/getting-started.md#install', { cleanUrls: true })
).toBe('/fr/guide/getting-started#install')
})
test('resolves index pages to directory links', () => {
expect(resolve('/guide/index.md')).toBe('/fr/guide/')
expect(resolve('/guide/index.md', { cleanUrls: true })).toBe(
'/fr/guide/'
)
})
test('resolves the site root page to the target locale home', () => {
expect(resolve('/index.md')).toBe('/fr/')
})
test('strips the current locale prefix before rewriting', () => {
expect(
resolve('/en/guide/index.md?query#intro', {
currentLocaleLink: '/en/',
cleanUrls: true
})
).toBe('/fr/guide/?query#intro')
})
test('rewrites into the root locale', () => {
expect(
resolve('/fr/guide/getting-started.md#install', {
targetLocale: 'root',
targetLocaleLink: '/',
currentLocaleLink: '/fr/'
})
).toBe('/guide/getting-started.html#install')
})
test('links to the target locale home when i18n routing is disabled', () => {
expect(
resolve('/guide/getting-started.md#install', {
themeConfig: { i18nRouting: false }
})
).toBe('/fr/#install')
})
test('delegates to custom i18n routing functions', () => {
expect(
resolve('/guide/getting-started.md#install', {
themeConfig: {
i18nRouting(data, route, targetLocale) {
return `${data.site.value.locales[targetLocale].link}mapped/${route.data.relativePath}${route.hash}`
}
}
})
).toBe('/fr/mapped/guide/getting-started.md#install')
})
})
})
})

@ -1,5 +1,6 @@
import {
buildAskAiConfig,
buildSidePanelProps,
hasAskAi,
hasKeywordSearch,
mergeLangFacetFilters,
@ -192,5 +193,110 @@ describe('client/theme-default/support/docsearch', () => {
)
expect(result.searchParameters?.facetFilters).toEqual(['lang:en'])
})
test('preserves Agent Studio search parameters by index', () => {
const result = buildAskAiConfig(
{
assistantId: 'assistant123',
agentStudio: true,
searchParameters: {
index: {
distinct: false
}
}
} as any,
{
appId: 'app',
apiKey: 'key',
indexName: 'index',
searchParameters: {
facetFilters: ['tag:docs']
}
} as any,
'en'
)
expect(result.searchParameters).toEqual({
index: {
distinct: false
}
})
expect(result.searchParameters).not.toHaveProperty('facetFilters')
})
test('does not add legacy facet filters to Agent Studio config', () => {
const result = buildAskAiConfig(
{
assistantId: 'assistant123',
agentStudio: true
} as any,
{
appId: 'app',
apiKey: 'key',
indexName: 'index',
searchParameters: {
facetFilters: ['tag:docs']
}
} as any,
'en'
)
expect(result.searchParameters).toBeUndefined()
})
})
describe('buildSidePanelProps', () => {
test('passes resolved Ask AI options to the side panel', () => {
const result = buildSidePanelProps(
{
assistantId: 'assistant123',
agentStudio: true,
searchParameters: {
index: {
facetFilters: ['lang:en']
}
},
suggestedQuestions: true,
useStagingEnv: true,
sidePanel: {
button: {
variant: 'inline'
},
panel: {
width: 420,
suggestedQuestions: true
}
}
} as any,
{
appId: 'app',
apiKey: 'key',
indexName: 'index'
} as any
)
expect(result).toEqual({
container: '#vp-docsearch-sidepanel',
appId: 'app',
apiKey: 'key',
indexName: 'index',
assistantId: 'assistant123',
agentStudio: true,
searchParameters: {
index: {
facetFilters: ['lang:en']
}
},
suggestedQuestions: true,
useStagingEnv: true,
button: {
variant: 'inline'
},
panel: {
width: 420,
suggestedQuestions: true
}
})
})
})
})

@ -1,4 +1,8 @@
import { getSidebar, hasActiveLink } from 'client/theme-default/support/sidebar'
import {
getFlatSideBarLinks,
getSidebar,
hasActiveLink
} from 'client/theme-default/support/sidebar'
describe('client/theme-default/support/sidebar', () => {
describe('getSidebar', () => {
@ -103,6 +107,75 @@ describe('client/theme-default/support/sidebar', () => {
)
})
})
test('applies base only to internal links', () => {
expect(
getSidebar(
{
'/en/': {
base: '/en/',
items: [
{
text: 'Guide',
items: [
{ text: 'Intro', link: 'intro' },
{ text: 'Root', link: '/root' },
{ text: 'External', link: 'https://example.com/' }
]
}
]
}
},
'/en/intro'
)
).toStrictEqual([
{
text: 'Guide',
items: [
{ text: 'Intro', link: '/en/intro' },
{ text: 'Root', link: '/en/root' },
{ text: 'External', link: 'https://example.com/' }
]
}
])
})
})
describe('getFlatSideBarLinks', () => {
test('flattens nested items and preserves link metadata', () => {
const sidebar = [
{
text: 'Group',
items: [
{ text: 'Intro', link: '/intro' },
{
text: 'External',
link: 'https://example.com/',
target: '_self',
rel: 'noopener',
docFooterText: 'Go external'
}
]
}
]
expect(getFlatSideBarLinks(sidebar)).toStrictEqual([
{
text: 'Intro',
link: '/intro',
docFooterText: undefined,
rel: undefined,
target: undefined
},
{
text: 'External',
link: 'https://example.com/',
docFooterText: 'Go external',
rel: 'noopener',
target: '_self'
}
])
})
})
describe('hasActiveLink', () => {
@ -115,8 +188,8 @@ describe('client/theme-default/support/sidebar', () => {
]
}
expect(hasActiveLink('active-1', item)).toBe(true)
expect(hasActiveLink('inactive', item)).toBe(false)
expect(hasActiveLink('active-1', '', item)).toBe(true)
expect(hasActiveLink('inactive', '', item)).toBe(false)
})
test('checks `SidebarItem[]`', () => {
@ -137,9 +210,9 @@ describe('client/theme-default/support/sidebar', () => {
}
]
expect(hasActiveLink('active-1', item)).toBe(true)
expect(hasActiveLink('active-3', item)).toBe(true)
expect(hasActiveLink('inactive', item)).toBe(false)
expect(hasActiveLink('active-1', '', item)).toBe(true)
expect(hasActiveLink('active-3', '', item)).toBe(true)
expect(hasActiveLink('inactive', '', item)).toBe(false)
})
})
})

@ -0,0 +1,73 @@
import type { MarkdownItAsync } from 'markdown-it-async'
import { mergeConfig, type UserConfig } from 'node/config'
describe('node/config', () => {
test('merges markdown hooks from extended configs', async () => {
const calls: string[] = []
const md = {} as MarkdownItAsync
const merged = mergeConfig<UserConfig, UserConfig>(
{
markdown: {
lineNumbers: true,
preConfig() {
calls.push('base-pre')
},
config() {
calls.push('base')
}
}
},
{
markdown: {
attrs: {
allowed: ['id']
},
async preConfig() {
calls.push('extended-pre')
},
async config() {
calls.push('extended')
}
}
}
)
expect(merged.markdown?.lineNumbers).toBe(true)
expect(merged.markdown?.attrs).toEqual({
allowed: ['id']
})
await merged.markdown?.preConfig?.(md)
await merged.markdown?.config?.(md)
expect(calls).toEqual(['base-pre', 'extended-pre', 'base', 'extended'])
})
test('keeps one-sided markdown hooks when the other config omits them', async () => {
const calls: string[] = []
const md = {} as MarkdownItAsync
const merged = mergeConfig<UserConfig, UserConfig>(
{
markdown: {
preConfig() {
calls.push('base-pre')
}
}
},
{
markdown: {
config() {
calls.push('extended')
}
}
}
)
await merged.markdown?.preConfig?.(md)
await merged.markdown?.config?.(md)
expect(calls).toEqual(['base-pre', 'extended'])
})
})

@ -0,0 +1,51 @@
import { resolveConfig } from 'node/config'
import { createContentLoader } from 'node/contentLoader'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
describe('node/contentLoader', () => {
let root: string | undefined
afterEach(async () => {
if (root) {
await rm(root, { recursive: true, force: true })
root = undefined
}
delete (global as any).VITEPRESS_CONFIG
})
async function setup(cleanUrls: boolean) {
root = await mkdtemp(path.join(tmpdir(), 'vitepress-content-loader-'))
await writeFile(
path.join(root, 'index.md'),
'# Home\n\n[link](./other.md)\n'
)
await writeFile(path.join(root, 'other.md'), '# Other\n')
const siteConfig = await resolveConfig(root, 'build', 'production')
siteConfig.cleanUrls = cleanUrls
;(global as any).VITEPRESS_CONFIG = siteConfig
}
test('rendered internal links get .html when cleanUrls is false', async () => {
await setup(false)
const data = await createContentLoader('index.md', {
render: true
}).load()
expect(data[0].html).toContain('href="./other.html"')
})
test('rendered internal links are clean when cleanUrls is true', async () => {
await setup(true)
const data = await createContentLoader('index.md', {
render: true
}).load()
expect(data[0].html).toContain('href="./other"')
expect(data[0].html).not.toContain('./other.html')
})
})

@ -0,0 +1,206 @@
import { anchor as anchorPlugin } from '@mdit/plugin-anchor'
import { attrs as attrsPlugin } from '@mdit/plugin-attrs'
import { MarkdownItAsync } from 'markdown-it-async'
import {
createMarkdownRenderer,
disposeMdItInstance,
type MarkdownOptions
} from 'node/markdown/markdown'
async function render(src: string, options: MarkdownOptions = {}) {
disposeMdItInstance()
const md = await createMarkdownRenderer('.', {
highlight: (code) => code,
...options
})
return md.renderAsync(src)
}
describe('node/markdown/markdown', () => {
describe('disabling built-in plugins', () => {
test('anchor', async () => {
const enabled = await render('# Hello World')
expect(enabled).toContain('id="hello-world"')
expect(enabled).toContain('header-anchor')
const disabled = await render('# Hello World', { anchor: false })
expect(disabled).not.toContain('id=')
expect(disabled).not.toContain('header-anchor')
})
test('attrs', async () => {
const enabled = await render('## Title {#custom-id}')
expect(enabled).toContain('id="custom-id"')
const disabled = await render('## Title {#custom-id}', { attrs: false })
expect(disabled).not.toContain('id="custom-id"')
expect(disabled).toContain('{#custom-id}')
})
test('emoji', async () => {
expect(await render(':tada:')).toContain('🎉')
expect(await render(':tada:', { emoji: false })).toContain(':tada:')
})
test('tasklist', async () => {
const src = '- [ ] todo'
expect(await render(src)).toContain('<input type="checkbox"')
const disabled = await render(src, { tasklist: false })
expect(disabled).not.toContain('<input')
expect(disabled).toContain('[ ] todo')
})
test('toc', async () => {
const src = '# Title\n\n[[toc]]'
expect(await render(src)).toContain('table-of-contents')
const disabled = await render(src, { toc: false })
expect(disabled).not.toContain('table-of-contents')
expect(disabled).toContain('[[toc]]')
})
test('preWrapper', async () => {
const src = '```js\nconst a = 1\n```'
const enabled = await render(src)
expect(enabled).toContain('<div class="language-js">')
expect(enabled).toContain('class="copy"')
const disabled = await render(src, { preWrapper: false })
expect(disabled).not.toContain('<div class="language-js">')
expect(disabled).not.toContain('class="copy"')
})
test('preWrapper disables line numbers with it', async () => {
const src = '```js\nconst a = 1\n```'
const enabled = await render(src, { lineNumbers: true })
expect(enabled).toContain('line-numbers-wrapper')
const disabled = await render(src, {
preWrapper: false,
lineNumbers: true
})
expect(disabled).not.toContain('line-numbers-wrapper')
})
test('snippet', async () => {
const disabled = await render('<<< ./foo.js', { snippet: false })
expect(disabled).toContain('&lt;&lt;&lt; ./foo.js')
})
test('image', async () => {
const src = '![img](/foo.png)'
const enabled = await render(src, { image: { lazyLoad: true } })
expect(enabled).toContain('loading="lazy"')
const disabled = await render(src, { image: false })
expect(disabled).not.toContain('loading="lazy"')
})
test('component', async () => {
const src = 'text\n<MyComponent/>\nmore'
const enabled = await render(src)
expect(enabled).toContain('</p>\n<MyComponent/><p>')
const disabled = await render(src, { component: false })
expect(disabled).toContain('<p>text\n<MyComponent/>\nmore</p>')
})
test('tableTabIndex', async () => {
const src = '| a |\n| --- |\n| b |'
expect(await render(src)).toContain('tabindex="0"')
expect(await render(src, { tableTabIndex: false })).not.toContain(
'tabindex'
)
})
test('cjkFriendlyEmphasis', async () => {
const src = 'これは**「テスト」**です'
expect(await render(src)).toContain('<strong>「テスト」</strong>')
expect(await render(src, { cjkFriendlyEmphasis: false })).not.toContain(
'<strong>'
)
})
test('`true` enables a plugin with its default options', async () => {
const html = await render(
'## Title {#custom-id}\n\n[[toc]]\n\n:tada:\n\n- [ ] todo',
{
anchor: true,
attrs: true,
emoji: true,
tasklist: true,
toc: true,
image: true,
component: true
}
)
expect(html).toContain('id="custom-id"')
expect(html).toContain('header-anchor')
expect(html).toContain('table-of-contents')
expect(html).toContain('🎉')
expect(html).toContain('<input type="checkbox"')
})
})
describe('attrs', () => {
test('does not consume fence info', async () => {
// line-highlight / meta syntax must reach the highlighter untouched
const meta = await render('```js{4}\nconst a = 1\n```')
expect(meta).toContain('language-js')
expect(meta).not.toContain('4=""')
// curly attributes have no effect on fenced code blocks
const backtick = await render('```js {.foo}\nconst a = 1\n```')
expect(backtick).not.toContain('class="foo"')
const tilde = await render('~~~js {.foo}\nconst a = 1\n~~~')
expect(tilde).not.toContain('class="foo"')
})
test('applies to inline elements and blocks', async () => {
expect(await render('*hi*{.cls}')).toContain('<em class="cls">')
expect(await render('`code`{.cls}')).toContain('class="cls"')
expect(await render('text {.cls}')).toContain('<p class="cls">')
expect(await render('- item\n{.cls}')).toContain('<ul class="cls">')
expect(await render('| a |\n| --- |\n| b |\n\n{.cls}')).toContain(
'<table class="cls"'
)
})
})
describe('tasklist', () => {
test('renders checkboxes with their checked state', async () => {
const html = await render('- [ ] todo\n- [x] done')
expect(html).toContain('<ul class="task-list-container">')
expect(html).toContain('<li class="task-list-item">')
const inputs = html.match(/<input[^>]*>/g)!
expect(inputs).toHaveLength(2)
expect(inputs[0]).not.toContain('checked')
expect(inputs[1]).toContain('checked')
for (const input of inputs) expect(input).toContain('disabled')
})
test('forwards options to the plugin', async () => {
const html = await render('- [ ] todo', { tasklist: { label: false } })
expect(html).toContain('<input type="checkbox"')
expect(html).not.toContain('<label')
})
})
// attrs applies at a fixed position in the core chain (before linkify),
// while anchor pushes to its end, so anchor always sees user-defined ids
// no matter which plugin is registered first
test('anchor respects ids from attrs regardless of plugin order', async () => {
for (const plugins of [
[attrsPlugin, anchorPlugin],
[anchorPlugin, attrsPlugin]
] as const) {
const md = new MarkdownItAsync()
for (const plugin of plugins) md.use(plugin)
expect(await md.renderAsync('## Title {#custom-id}')).toContain(
'id="custom-id"'
)
}
})
})

@ -0,0 +1,607 @@
import {
createMarkdownRenderer,
disposeMdItInstance,
type MarkdownOptions
} from 'node/markdown/markdown'
import type { MarkdownEnv } from 'node/shared'
async function render(
src: string,
options: MarkdownOptions = {},
env?: Partial<MarkdownEnv>
) {
disposeMdItInstance()
const md = await createMarkdownRenderer('.', {
highlight: (code) => code,
...options
})
return md.renderAsync(src, env)
}
describe('node/markdown/plugins/containers', () => {
test('renders built-in containers with default titles', async () => {
const src = [
'tip',
'info',
'warning',
'danger',
'note',
'important',
'caution'
]
.map((t) => `::: ${t}\ncontent of ${t}\n:::`)
.join('\n\n')
expect(await render(src)).toMatchInlineSnapshot(`
"<div class="tip custom-block"><p class="custom-block-title custom-block-title-default">TIP</p>
<p>content of tip</p>
</div>
<div class="info custom-block"><p class="custom-block-title custom-block-title-default">INFO</p>
<p>content of info</p>
</div>
<div class="warning custom-block"><p class="custom-block-title custom-block-title-default">WARNING</p>
<p>content of warning</p>
</div>
<div class="danger custom-block"><p class="custom-block-title custom-block-title-default">DANGER</p>
<p>content of danger</p>
</div>
<div class="note custom-block"><p class="custom-block-title custom-block-title-default">NOTE</p>
<p>content of note</p>
</div>
<div class="important custom-block"><p class="custom-block-title custom-block-title-default">IMPORTANT</p>
<p>content of important</p>
</div>
<div class="caution custom-block"><p class="custom-block-title custom-block-title-default">CAUTION</p>
<p>content of caution</p>
</div>
"
`)
})
test('renders details as a disclosure with summary', async () => {
expect(await render('::: details\nhidden content\n:::'))
.toMatchInlineSnapshot(`
"<details class="details custom-block"><summary>Details</summary>
<p>hidden content</p>
</details>
"
`)
})
test('renders custom titles, including inline markdown', async () => {
const src = [
'::: danger STOP',
'Danger zone, do not proceed',
':::',
'',
'::: tip A **bold** _title_ with `code`',
'content',
':::',
'',
'::: details Click me to toggle the code',
'```js',
"console.log('hi')",
'```',
':::'
].join('\n')
expect(await render(src)).toMatchInlineSnapshot(`
"<div class="danger custom-block"><p class="custom-block-title">STOP</p>
<p>Danger zone, do not proceed</p>
</div>
<div class="tip custom-block"><p class="custom-block-title">A <strong>bold</strong> <em>title</em> with <code>code</code></p>
<p>content</p>
</div>
<details class="details custom-block"><summary>Click me to toggle the code</summary>
<div class="language-js"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">js</span><pre><code class="language-js">console.log('hi')
</code></pre>
</div></details>
"
`)
})
test('resolves reference links in titles', async () => {
const src = [
'::: tip See [the guide][guide]',
'content',
':::',
'',
'[guide]: /guide/'
].join('\n')
expect(await render(src)).toMatchInlineSnapshot(`
"<div class="tip custom-block"><p class="custom-block-title">See <a href="/guide/">the guide</a></p>
<p>content</p>
</div>
"
`)
})
test('respects custom labels from container options', async () => {
const src = '::: tip\n提示内容\n:::\n\n::: details\n详情内容\n:::'
expect(
await render(src, {
container: { tipLabel: '提示', detailsLabel: '详细信息' }
})
).toMatchInlineSnapshot(`
"<div class="tip custom-block"><p class="custom-block-title custom-block-title-default"></p>
<p></p>
</div>
<details class="details custom-block"><summary></summary>
<p></p>
</details>
"
`)
})
test('registers custom containers from container options', async () => {
const src = [
'::: success',
'You have completed the walkthrough!',
':::',
'',
'::: success Well done {no-title}',
'content',
':::'
].join('\n')
expect(
await render(src, {
container: { customContainers: { success: 'SUCCESS' } }
})
).toMatchInlineSnapshot(`
"<div class="success custom-block"><p class="custom-block-title custom-block-title-default">SUCCESS</p>
<p>You have completed the walkthrough!</p>
</div>
<div class="success custom-block">
<p>content</p>
</div>
"
`)
})
test('rejects invalid custom container names', async () => {
for (const name of ['raw', 'v-pre', 'code-group', 'Bad Name', 'UPPER']) {
await expect(
render('text', { container: { customContainers: { [name]: 'X' } } })
).rejects.toThrow('Invalid custom container name')
}
})
test('supports attrs on the fence line', async () => {
const src = [
'::: details Click me {open}',
'content',
':::',
'',
'::: tip Custom {.extra-class #custom-id}',
'content',
':::'
].join('\n')
expect(await render(src)).toMatchInlineSnapshot(`
"<details open="" class="details custom-block"><summary>Click me</summary>
<p>content</p>
</details>
<div class="extra-class tip custom-block" id="custom-id"><p class="custom-block-title">Custom</p>
<p>content</p>
</div>
"
`)
})
test('supports quoted and bare attr values on the fence line', async () => {
expect(await render('::: tip Custom {data-a="b c" data-d=e}\ncontent\n:::'))
.toMatchInlineSnapshot(`
"<div data-a="b c" data-d="e" class="tip custom-block"><p class="custom-block-title">Custom</p>
<p>content</p>
</div>
"
`)
})
test('skips the title element with a no-title attr', async () => {
const src = [
'::: tip {no-title}',
'content',
':::',
'',
'::: warning Discarded {no-title .extra-class}',
'content',
':::',
'',
'::: details {no-title}',
'still needs its summary',
':::'
].join('\n')
expect(await render(src)).toMatchInlineSnapshot(`
"<div class="tip custom-block">
<p>content</p>
</div>
<div class="extra-class warning custom-block">
<p>content</p>
</div>
<details class="details custom-block"><summary>Details</summary>
<p>still needs its summary</p>
</details>
"
`)
})
test('respects attrs plugin options on the fence line', async () => {
const delimiters = await render(
'::: tip Custom %(.extra-class)%\ncontent\n:::',
{ attrs: { left: '%(', right: ')%' } }
)
expect(delimiters).toContain('<div class="extra-class tip custom-block">')
const allowed = await render(
'::: tip Custom {.extra-class data-x=1}\ncontent\n:::',
{ attrs: { allowed: ['class'] } }
)
expect(allowed).toContain('<div class="extra-class tip custom-block">')
expect(allowed).not.toContain('data-x')
})
test('keeps fence line braces verbatim when attrs are disabled', async () => {
expect(
await render('::: details Click me {open}\ncontent\n:::', {
attrs: false
})
).toMatchInlineSnapshot(`
"<details class="details custom-block"><summary>Click me {open}</summary>
<p>content</p>
</details>
"
`)
})
test('renders v-pre and raw containers as plain wrappers', async () => {
const src = [
'::: v-pre',
'{{ this will be displayed as-is }}',
':::',
'',
'::: raw',
'Wraps in a `<div class="vp-raw">`',
':::'
].join('\n')
expect(await render(src)).toMatchInlineSnapshot(`
"<div v-pre>
<p>{{ this will be displayed as-is }}</p>
</div>
<div class="vp-raw">
<p>Wraps in a <code>&lt;div class=&quot;vp-raw&quot;&gt;</code></p>
</div>
"
`)
})
test('renders code groups with tabs', async () => {
const src = [
'::: code-group',
'',
'```js [config.js]',
'const a = 1',
'```',
'',
'```ts [config.ts]',
'const a: number = 1',
'```',
'',
':::'
].join('\n')
expect(await render(src)).toMatchInlineSnapshot(`
"<div class="vp-code-group"><div class="tabs"><input type="radio" name="group-0" id="tab-1" checked><label data-title="config.js" for="tab-1">config.js</label><input type="radio" name="group-0" id="tab-2" ><label data-title="config.ts" for="tab-2">config.ts</label></div><div class="blocks">
<div class="language-js active"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">js</span><pre><code class="language-js">const a = 1
</code></pre>
</div><div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre><code class="language-ts">const a: number = 1
</code></pre>
</div></div></div>
"
`)
})
test('supports nesting via longer fences', async () => {
const src = [
':::: info Outer',
'outer content',
'',
'::: details Inner',
'inner content',
':::',
'::::'
].join('\n')
expect(await render(src)).toMatchInlineSnapshot(`
"<div class="info custom-block"><p class="custom-block-title">Outer</p>
<p>outer content</p>
<details class="details custom-block"><summary>Inner</summary>
<p>inner content</p>
</details>
</div>
"
`)
})
test('auto-closes unclosed containers', async () => {
expect(await render('::: warning\nno closing fence'))
.toMatchInlineSnapshot(`
"<div class="warning custom-block"><p class="custom-block-title custom-block-title-default">WARNING</p>
<p>no closing fence</p>
</div>
"
`)
})
test('parses fences without a space before the name', async () => {
expect(await render(':::tip\ncontent\n:::')).toMatchInlineSnapshot(`
"<div class="tip custom-block"><p class="custom-block-title custom-block-title-default">TIP</p>
<p>content</p>
</div>
"
`)
})
test('leaves non-container fence lines alone', async () => {
expect(await render('::: unknown\ncontent\n:::')).toMatchInlineSnapshot(`
"<p>::: unknown
content
:::</p>
"
`)
})
})
describe('node/markdown/plugins/containers (github alerts)', () => {
test('renders github alerts like containers', async () => {
const src = [
'> [!NOTE]',
'> note content',
'',
'> [!TIP]',
'> tip content',
'',
'> [!IMPORTANT]',
'> important content',
'',
'> [!WARNING]',
'> warning content',
'',
'> [!CAUTION]',
'> caution content'
].join('\n')
expect(await render(src)).toMatchInlineSnapshot(`
"<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
<p>note content</p>
</div>
<div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p>
<p>tip content</p>
</div>
<div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p>
<p>important content</p>
</div>
<div class="warning custom-block github-alert"><p class="custom-block-title">WARNING</p>
<p>warning content</p>
</div>
<div class="caution custom-block github-alert"><p class="custom-block-title">CAUTION</p>
<p>caution content</p>
</div>
"
`)
})
test('matches markers case-insensitively', async () => {
expect(await render('> [!tip]\n> content')).toMatchInlineSnapshot(`
"<div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p>
<p>content</p>
</div>
"
`)
})
test('supports custom titles after the marker', async () => {
expect(await render('> [!WARNING] Custom Title\n> content'))
.toMatchInlineSnapshot(`
"<div class="warning custom-block github-alert"><p class="custom-block-title">Custom Title</p>
<p>content</p>
</div>
"
`)
})
test('respects custom labels from container options', async () => {
expect(
await render('> [!TIP]\n> content', { container: { tipLabel: '提示' } })
).toMatchInlineSnapshot(`
"<div class="tip custom-block github-alert"><p class="custom-block-title"></p>
<p>content</p>
</div>
"
`)
})
test('renders custom containers as alerts', async () => {
expect(
await render('> [!SUCCESS]\n> done\n\n> [!success] With title\n> done', {
container: { customContainers: { success: 'SUCCESS' } }
})
).toMatchInlineSnapshot(`
"<div class="success custom-block github-alert"><p class="custom-block-title">SUCCESS</p>
<p>done</p>
</div>
<div class="success custom-block github-alert"><p class="custom-block-title">With title</p>
<p>done</p>
</div>
"
`)
})
test('supports block content and lazy continuation', async () => {
const src = [
'> [!NOTE]',
'> first paragraph',
'lazy continuation',
'>',
'> - list item',
'>',
'> ```js',
'> const a = 1',
'> ```'
].join('\n')
expect(await render(src)).toMatchInlineSnapshot(`
"<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
<p>first paragraph
lazy continuation</p>
<ul>
<li>list item</li>
</ul>
<div class="language-js"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">js</span><pre><code class="language-js">const a = 1
</code></pre>
</div></div>
"
`)
})
test('converts markers without content', async () => {
expect(await render('> [!NOTE]')).toMatchInlineSnapshot(`
"<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
<p></p>
</div>
"
`)
})
test('leaves regular blockquotes and unknown markers alone', async () => {
const src = [
'> just a quote',
'',
'> [!FOO]',
'> not an alert',
'',
'paragraph [!NOTE] not at blockquote start'
].join('\n')
expect(await render(src)).toMatchInlineSnapshot(`
"<blockquote>
<p>just a quote</p>
</blockquote>
<blockquote>
<p>[!FOO]
not an alert</p>
</blockquote>
<p>paragraph [!NOTE] not at blockquote start</p>
"
`)
})
test('can be disabled via gfmAlerts: false', async () => {
expect(await render('> [!NOTE]\n> content', { gfmAlerts: false }))
.toMatchInlineSnapshot(`
"<blockquote>
<p>[!NOTE]
content</p>
</blockquote>
"
`)
})
})
describe('node/markdown/plugins/containers (locales)', () => {
const options: MarkdownOptions = {
container: {
tipLabel: 'ROOT TIP',
customContainers: { success: 'SUCCESS' }
},
locales: {
zh: {
container: {
tipLabel: '提示',
detailsLabel: '详细信息',
customContainers: { success: '成功' }
}
}
}
}
test('resolves container titles for the active locale', async () => {
const src =
'::: tip\n内容\n:::\n\n::: details\n内容\n:::\n\n::: success\n内容\n:::'
expect(await render(src, options, { localeIndex: 'zh' }))
.toMatchInlineSnapshot(`
"<div class="tip custom-block"><p class="custom-block-title custom-block-title-default"></p>
<p></p>
</div>
<details class="details custom-block"><summary></summary>
<p></p>
</details>
<div class="success custom-block"><p class="custom-block-title custom-block-title-default"></p>
<p></p>
</div>
"
`)
})
test('falls back to root titles for other locales', async () => {
const src = '::: tip\ncontent\n:::'
const root = await render(src, options)
expect(await render(src, options, { localeIndex: 'es' })).toBe(root)
expect(root).toMatchInlineSnapshot(`
"<div class="tip custom-block"><p class="custom-block-title custom-block-title-default">ROOT TIP</p>
<p>content</p>
</div>
"
`)
})
test('explicit titles win over locale defaults', async () => {
expect(
await render('::: tip Custom Title\n内容\n:::', options, {
localeIndex: 'zh'
})
).toMatchInlineSnapshot(`
"<div class="tip custom-block"><p class="custom-block-title">Custom Title</p>
<p></p>
</div>
"
`)
})
test('resolves alert titles for the active locale', async () => {
expect(
await render('> [!TIP]\n> 内容\n\n> [!SUCCESS]\n> 内容', options, {
localeIndex: 'zh'
})
).toMatchInlineSnapshot(`
"<div class="tip custom-block github-alert"><p class="custom-block-title"></p>
<p></p>
</div>
<div class="success custom-block github-alert"><p class="custom-block-title"></p>
<p></p>
</div>
"
`)
})
test('rejects locale titles for unregistered containers', async () => {
await expect(
render('text', {
locales: { zh: { container: { customContainers: { nope: 'X' } } } }
})
).rejects.toThrow('is not registered in the root markdown config')
})
test('resolves the code copy button strings for the active locale', async () => {
const src = '```js\nconst a = 1\n```'
const opts: MarkdownOptions = {
codeCopyButton: { copiedText: 'Copied!' },
locales: {
zh: {
codeCopyButton: { tooltipText: '复制代码', copiedText: '已复制' }
}
}
}
const zh = await render(src, opts, { localeIndex: 'zh' })
expect(zh).toContain('title="复制代码"')
expect(zh).toContain('data-copied="已复制"')
const es = await render(src, opts, { localeIndex: 'es' })
expect(es).toContain('title="Copy code"')
expect(es).toContain('data-copied="Copied!"')
const root = await render(src, opts)
expect(root).toContain('title="Copy code"')
expect(root).toContain('data-copied="Copied!"')
})
})

@ -0,0 +1,141 @@
import path from 'node:path'
import { MarkdownItAsync } from 'markdown-it-async'
import { attrs as attrsPlugin } from '@mdit/plugin-attrs'
import { imagePlugin, type Options } from 'node/markdown/plugins/image'
const srcDir = path.resolve(import.meta.dirname, '../../../../e2e')
const publicDir = path.join(srcDir, 'public')
const env = { path: path.join(srcDir, 'index.md') }
function createRenderer(options?: Options) {
const md = new MarkdownItAsync()
// same registration order as createMarkdownRenderer
imagePlugin(md, publicDir, options)
attrsPlugin(md as any)
return md
}
describe('node/markdown/plugins/image', () => {
const md = createRenderer()
describe('src normalization', () => {
test('default image output', async () => {
const html = await md.renderAsync('![logo](foo.png)')
expect(html.trim()).toMatchInlineSnapshot(
`"<p><img src="./foo.png" alt="logo"></p>"`
)
})
test.for([
['foo.png', './foo.png'],
['./foo.png', './foo.png'],
['../foo.png', '../foo.png'],
['../../foo.png', '../../foo.png'],
['/foo.png', '/foo.png'],
['https://example.com/foo.png', 'https://example.com/foo.png']
])('normalizes image src: %s → %s', async ([src, expected]) => {
const html = await md.renderAsync(`![logo](${src})`)
expect(html).toContain(`src="${expected}"`)
})
})
describe('dimensions', () => {
test('adds width and height from local image dimensions', async () => {
const html = await md.renderAsync('![logo](./assets/vitepress.png)', env)
expect(html).toContain('width="48"')
expect(html).toContain('height="48"')
})
test('adds width and height from public image dimensions', async () => {
const html = await md.renderAsync('![logo](/vitepress.png)', env)
expect(html).toContain('width="48"')
expect(html).toContain('height="48"')
})
test('adds width and height when the image url is encoded', async () => {
const html = await md.renderAsync(
'![logo](./assets/vitepress%20logo.png)',
env
)
expect(html).toContain('src="./assets/vitepress logo.png"')
expect(html).toContain('width="48"')
expect(html).toContain('height="48"')
})
test('does not override explicit width and height', async () => {
const html = await md.renderAsync(
'![logo](/vitepress.png){width=100 height=200}',
env
)
expect(html).toContain('width="100"')
expect(html).toContain('height="200"')
})
test('scales height proportionally when only width is set', async () => {
const html = await md.renderAsync(
'![logo](/vitepress.png){width=96}',
env
)
// 48x48 image scaled to width=96 → height=96
expect(html).toContain('width="96"')
expect(html).toContain('height="96"')
})
test('scales width proportionally when only height is set', async () => {
const html = await md.renderAsync(
'![logo](/vitepress.png){height=24}',
env
)
// 48x48 image scaled to height=24 → width=24
expect(html).toContain('width="24"')
expect(html).toContain('height="24"')
})
test('ignores non-numeric width when scaling', async () => {
const html = await md.renderAsync(
'![logo](/vitepress.png){width=50%}',
env
)
expect(html).toContain('width="50%"')
expect(html).not.toContain('height=')
})
test('does not add dimensions for external images', async () => {
const html = await md.renderAsync(
'![logo](https://example.com/image.png)',
env
)
expect(html).not.toContain('width=')
expect(html).not.toContain('height=')
})
})
describe('lazy loading', () => {
const mdLazy = createRenderer({ lazyLoad: true })
test('adds loading="lazy" when lazy loading is enabled', async () => {
const html = await mdLazy.renderAsync('![logo](foo.png)')
expect(html).toContain('loading="lazy"')
})
test('does not override user-specified loading strategy', async () => {
const html = await mdLazy.renderAsync('![logo](foo.png){loading=eager}')
expect(html).toContain('loading="eager"')
expect(html).not.toContain('loading="lazy"')
})
})
})

@ -0,0 +1,534 @@
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import {
createMarkdownRenderer,
disposeMdItInstance,
type MarkdownOptions
} from 'node/markdown/markdown'
import { slash, type MarkdownEnv } from 'node/shared'
describe('node/markdown/plugins/include', () => {
let root: string
let warnings: string[]
const logger = {
warn: (msg: string) => {
warnings.push(msg)
}
}
beforeEach(async () => {
root = await mkdtemp(path.join(tmpdir(), 'vitepress-include-'))
warnings = []
})
afterEach(async () => {
await rm(root, { recursive: true, force: true })
})
async function write(name: string, src: string) {
const file = path.join(root, name)
await mkdir(path.dirname(file), { recursive: true })
await writeFile(file, src)
}
async function render(
src: string,
options: MarkdownOptions = {},
env: Partial<MarkdownEnv> = {}
) {
disposeMdItInstance()
const md = await createMarkdownRenderer(
root,
{ highlight: (code) => code, ...options },
'/',
logger
)
const fullEnv: MarkdownEnv = {
path: path.join(root, 'index.md'),
relativePath: 'index.md',
cleanUrls: false,
includes: [],
...env
}
const html = await md.renderAsync(src, fullEnv)
return { html, env: fullEnv }
}
test('includes a relative markdown file', async () => {
await write('b.md', 'B-content\n')
const { html, env } = await render('# A\n\n<!-- @include: ./b.md -->\n')
expect(html).toContain('B-content')
expect(env.includes).toEqual([slash(path.join(root, 'b.md'))])
expect(env.src).toContain('B-content')
})
test('resolves @ against srcDir', async () => {
await write('dir/c.md', 'C-content\n')
const { html } = await render(
'<!-- @include: @/dir/c.md -->\n',
{},
{ path: path.join(root, 'sub/index.md') }
)
expect(html).toContain('C-content')
})
test('resolves @ without a slash against srcDir', async () => {
await write('dir/c.md', 'C-content\n')
const { html } = await render(
'<!-- @include: @dir/c.md -->\n',
{},
{ path: path.join(root, 'sub/index.md') }
)
expect(html).toContain('C-content')
})
test('resolves relative includes against the real file path', async () => {
await write('sub/part.md', 'real-content\n')
const { html } = await render(
'<!-- @include: ./part.md -->\n',
{},
{
path: path.join(root, 'rewritten/index.md'),
realPath: path.join(root, 'sub/index.md')
}
)
expect(html).toContain('real-content')
})
test.runIf(process.platform === 'win32')(
'resolves windows-style paths',
async () => {
await write('dir/c.md', 'C-content\n')
const relative = await render('<!-- @include: .\\dir\\c.md -->\n')
expect(relative.html).toContain('C-content')
const rooted = await render('<!-- @include: @\\dir\\c.md -->\n')
expect(rooted.html).toContain('C-content')
// watched paths are posix-style for includes
expect(relative.env.includes).toEqual([
slash(path.join(root, 'dir/c.md'))
])
}
)
test('handles CRLF sources', async () => {
await write('b.md', 'B-content\n')
const { html } = await render('# A\r\n\r\n<!-- @include: ./b.md -->\r\n')
expect(html).toContain('B-content')
})
test('expands nested includes with relative resolution', async () => {
await write(
'sub/inside.md',
'inside\n\n<!-- @include: ./subsub/deep.md -->\n'
)
await write('sub/subsub/deep.md', 'deep-content\n')
const { html, env } = await render('<!-- @include: ./sub/inside.md -->\n')
expect(html).toContain('inside')
expect(html).toContain('deep-content')
expect(env.includes).toEqual([
slash(path.join(root, 'sub/inside.md')),
slash(path.join(root, 'sub/subsub/deep.md'))
])
})
test('leaves a self-include unexpanded', async () => {
const src = '# A\n\n<!-- @include: ./index.md -->\n'
await write('index.md', src)
const { html } = await render(src)
expect(html).toContain('@include: ./index.md')
})
test('leaves circular includes unexpanded', async () => {
await write('a.md', 'A-content\n\n<!-- @include: ./b.md -->\n')
await write('b.md', 'B-content\n\n<!-- @include: ./a.md -->\n')
const { html } = await render(
'A-content\n\n<!-- @include: ./b.md -->\n',
{},
{ path: path.join(root, 'a.md') }
)
expect(html).toContain('B-content')
expect(html).toContain('@include: ./a.md')
})
test('expands repeated includes outside the ancestor chain', async () => {
await write('b.md', 'B-content\n\n<!-- @include: ./d.md -->\n')
await write('c.md', 'C-content\n\n<!-- @include: ./d.md -->\n')
await write('d.md', 'D-content\n')
const { html } = await render(
'<!-- @include: ./b.md -->\n<!-- @include: ./c.md -->\n'
)
expect(html.match(/D-content/g)).toHaveLength(2)
})
test('strips frontmatter of whole-file markdown includes', async () => {
await write('b.md', '---\ntitle: B\n---\n\nB-content\n')
const { html } = await render('<!-- @include: ./b.md -->\n')
expect(html).toContain('B-content')
expect(html).not.toContain('title: B')
})
test('keeps frontmatter lines in range-only includes', async () => {
await write('b.md', '---\ntitle: B\n---\nline-4\nline-5\n')
const { html } = await render('<!-- @include: ./b.md{4,4} -->\n')
expect(html).toContain('line-4')
expect(html).not.toContain('line-5')
})
test('includes regions and strips frontmatter before locating them', async () => {
await write(
'b.md',
[
'---',
'title: B',
'---',
'<!-- #region part -->',
'region-content',
'<!-- #endregion part -->',
'outside-content',
''
].join('\n')
)
const { html } = await render('<!-- @include: ./b.md#part -->\n')
expect(html).toContain('region-content')
expect(html).not.toContain('outside-content')
})
test('concatenates all regions with the requested name', async () => {
await write(
'b.md',
[
'<!-- #region part -->',
'first',
'<!-- #endregion part -->',
'outside',
'<!-- #region part -->',
'second',
'<!-- #endregion -->',
''
].join('\n')
)
const { html } = await render('<!-- @include: ./b.md#part -->\n')
expect(html).toContain('first')
expect(html).toContain('second')
expect(html).not.toContain('outside')
})
test('applies ranges within the extracted region', async () => {
await write(
'b.md',
[
'<!-- #region part -->',
'one',
'two',
'three',
'<!-- #endregion part -->',
''
].join('\n')
)
const { html } = await render('<!-- @include: ./b.md#part{2,2} -->\n')
expect(html).toContain('two')
expect(html).not.toContain('one')
expect(html).not.toContain('three')
})
test('supports ranges with open ends', async () => {
await write('b.md', 'one\ntwo\nthree\n')
const from = await render('<!-- @include: ./b.md{2,} -->\n')
expect(from.html).toContain('two')
expect(from.html).toContain('three')
expect(from.html).not.toContain('one')
const to = await render('<!-- @include: ./b.md{,2} -->\n')
expect(to.html).toContain('one')
expect(to.html).toContain('two')
expect(to.html).not.toContain('three')
const both = await render('<!-- @include: ./b.md{2,3} -->\n')
expect(both.html).toContain('two')
expect(both.html).toContain('three')
expect(both.html).not.toContain('one')
})
test('includes heading sections by anchor', async () => {
await write(
'source.md',
[
'---',
'description: Source description',
'---',
'# Intro',
'',
'intro text',
'',
'## Shared',
'',
'shared before target',
'',
'## Target',
'',
'target text',
'',
'### Child',
'',
'child text',
'',
'## Shared',
'',
'shared after target',
''
].join('\n')
)
const { html } = await render('<!-- @include: ./source.md#target -->\n')
expect(html).toContain('target text')
expect(html).toContain('child text')
expect(html).not.toContain('Source description')
expect(html).not.toContain('intro text')
expect(html).not.toContain('shared before target')
expect(html).not.toContain('shared after target')
})
test('includes heading sections with custom ids up to EOF', async () => {
await write(
'source.md',
['## My Section {#custom-id}', '', 'section text', ''].join('\n')
)
const { html } = await render('<!-- @include: ./source.md#custom-id -->\n')
expect(html).toContain('section text')
})
test('includes non-markdown files verbatim, also inside fences', async () => {
await write('code.ts', 'const a = 1\nconst b = 2\nconst c = 3\n')
const fenced = await render(
'```ts\n<!-- @include: ./code.ts{2,3} -->\n```\n'
)
expect(fenced.html).toContain('language-ts')
expect(fenced.html).toContain('const b = 2')
expect(fenced.html).toContain('const c = 3')
expect(fenced.html).not.toContain('const a = 1')
})
test('includes regions of non-markdown files', async () => {
await write(
'code.ts',
[
'// #region part',
'region line',
'// #endregion part',
'outside line',
''
].join('\n')
)
const { html } = await render(
'```ts\n<!-- @include: ./code.ts#part -->\n```\n'
)
expect(html).toContain('region line')
expect(html).not.toContain('outside line')
})
test('leaves empty include paths untouched', async () => {
const { html } = await render('<!-- @include: -->\n')
expect(html).toContain('@include:')
})
test('skips expansion without a file path in env', async () => {
disposeMdItInstance()
const md = await createMarkdownRenderer(
root,
{ highlight: (code) => code },
'/',
logger
)
const html = await md.renderAsync('<!-- @include: ./b.md -->\n')
expect(html).toContain('@include: ./b.md')
})
test('can be disabled', async () => {
await write('b.md', 'B-content\n')
const { html } = await render('<!-- @include: ./b.md -->\n', {
include: false
})
expect(html).not.toContain('B-content')
expect(html).toContain('@include: ./b.md')
})
test('throws when the file is missing, recording it as a dependency', async () => {
const env: MarkdownEnv = {
path: path.join(root, 'index.md'),
relativePath: 'index.md',
cleanUrls: false,
includes: []
}
disposeMdItInstance()
const md = await createMarkdownRenderer(
root,
{ highlight: (code) => code },
'/',
logger
)
await expect(
md.renderAsync('<!-- @include: ./missing.md -->\n', env)
).rejects.toThrow(/Include file not found/)
// the missing file is watched so that creating it recovers the page
expect(env.includes).toEqual([slash(path.join(root, 'missing.md'))])
})
test('throws when neither region nor heading matches', async () => {
await write('b.md', '## Some Heading\n\ncontent\n')
await expect(render('<!-- @include: ./b.md#nope -->\n')).rejects.toThrow(
/region or heading "nope" not found/i
)
})
test('throws when the range is out of bounds', async () => {
await write('b.md', 'one\ntwo\nthree\n')
await expect(render('<!-- @include: ./b.md{10,20} -->\n')).rejects.toThrow(
/range/i
)
await expect(render('<!-- @include: ./b.md{3,1} -->\n')).rejects.toThrow(
/range/i
)
await expect(render('<!-- @include: ./b.md{0,2} -->\n')).rejects.toThrow(
/range/i
)
})
test('silent mode renders nothing on errors and warns', async () => {
await write('b.md', 'one\ntwo\n')
const missing = await render(
'before\n\n<!-- @include: ./missing.md -->\n\nafter\n',
{ include: { silent: true } }
)
expect(missing.html).toContain('before')
expect(missing.html).toContain('after')
expect(missing.html).not.toContain('@include')
const region = await render('<!-- @include: ./b.md#nope -->\n', {
include: { silent: true }
})
expect(region.html).not.toContain('@include')
const range = await render('<!-- @include: ./b.md{5,9} -->\n', {
include: { silent: true }
})
expect(range.html).not.toContain('@include')
expect(warnings).toHaveLength(3)
expect(warnings[0]).toContain('missing.md')
expect(warnings[1]).toContain('nope')
expect(warnings[2]).toContain('b.md')
})
test('keeps relative urls as is with rebaseRelativeUrls: false', async () => {
await write('sub/part.md', '![img](./img.png)\n\n[link](./target.md)\n')
const { html } = await render('<!-- @include: ./sub/part.md -->\n', {
include: { rebaseRelativeUrls: false }
})
expect(html).toContain('src="./img.png"')
expect(html).toContain('href="./target.html"')
expect(html).not.toContain('@include-')
})
test('rebases relative urls inside included files by default', async () => {
await write('sub/part.md', '![img](./img.png)\n\n[link](./target.md)\n')
const { html } = await render(
'<!-- @include: ./sub/part.md -->\n\n[after](./after.md)\n'
)
expect(html).toContain('src="./sub/img.png"')
expect(html).toContain('href="./sub/target.html"')
// links outside the included content are unaffected
expect(html).toContain('href="./after.html"')
// the internal markers never reach the output
expect(html).not.toContain('@include')
})
test('rebases urls through nested includes', async () => {
await write(
'a/one.md',
'one\n\n<!-- @include: ../b/two.md -->\n\n![oneimg](./one.png)\n'
)
await write('b/two.md', '![twoimg](./two.png)\n')
const { html } = await render('<!-- @include: ./a/one.md -->\n')
expect(html).toContain('src="./b/two.png"')
expect(html).toContain('src="./a/one.png"')
})
test('rebases urls after an include ending with an html block', async () => {
await write(
'sub/part.md',
'![inside](./inside.png)\n\n<div class="card">\ntail\n</div>\n'
)
const { html } = await render(
'<!-- @include: ./sub/part.md -->\n\n![after](./after.png)\n\n[after](./after.md)\n'
)
expect(html).toContain('src="./sub/inside.png"')
// the stack must be popped even though the marker follows an html block
expect(html).toContain('src="./after.png"')
expect(html).toContain('href="./after.html"')
expect(html).not.toContain('@include-')
})
test('does not leak rebase markers into fenced includes', async () => {
await write('sub/part.md', 'partial line\n')
const { html } = await render(
'```md\n<!-- @include: ./sub/part.md -->\n```\n'
)
expect(html).toContain('partial line')
expect(html).not.toContain('@include-')
})
test('does not leak rebase markers for inline includes', async () => {
await write('sub/part.md', 'partial line\n')
const { html } = await render(
'before <!-- @include: ./sub/part.md --> after\n\n[link](./x.md)\n'
)
expect(html).toContain('partial line')
expect(html).not.toContain('@include-')
// an inline include leaves the surrounding page urls untouched
expect(html).toContain('href="./x.html"')
})
test('does not rebase absolute or external urls', async () => {
await write(
'sub/part.md',
'[ext](https://example.com/x)\n\n[abs](/abs/target.md)\n'
)
const { html } = await render('<!-- @include: ./sub/part.md -->\n')
expect(html).toContain('href="https://example.com/x"')
expect(html).toContain('href="/abs/target.html"')
})
})

@ -48,4 +48,17 @@ describe('node/markdown/plugins/link', () => {
'href="/foo.html?title=Cat&amp;oldid=916388819#:~:text=Claws-,Like%20almost,the%20Felidae%2C,-cats"'
)
})
test('records source line numbers for collected links', async () => {
const env: {
cleanUrls: boolean
links?: string[]
linkLines?: number[]
} = { cleanUrls: false }
await md.renderAsync('Intro\n\n[Missing](./missing.md)\n', env)
expect(env.links).toEqual(['./missing'])
expect(env.linkLines).toEqual([3])
})
})

@ -1,8 +1,13 @@
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import {
dedent,
findRegion,
rawPathToToken
} from 'node/markdown/plugins/snippet'
createMarkdownRenderer,
disposeMdItInstance,
type MarkdownOptions
} from 'node/markdown/markdown'
import { parseSnippetPath } from 'node/markdown/plugins/snippet'
import type { MarkdownEnv } from 'node/shared'
const removeEmptyKeys = <T extends Record<string, unknown>>(obj: T) => {
return Object.fromEntries(
@ -11,7 +16,8 @@ const removeEmptyKeys = <T extends Record<string, unknown>>(obj: T) => {
}
/* prettier-ignore */
const rawPathTokenMap: [string, Partial<{ filepath: string, extension: string, title: string, region: string, lines: string, lang: string }>][] = [
const parseSnippetPathMap: [string, Partial<{ filepath: string, extension: string, title: string, region: string, lines: string, lang: string, attrs: string }>][] = [
// paths may contain spaces and dots, and the title defaults to the file name
['/path/to/file.extension', { filepath: '/path/to/file.extension', extension: 'extension', title: 'file.extension' }],
['./path/to/file.extension', { filepath: './path/to/file.extension', extension: 'extension', title: 'file.extension' }],
['/path to/file.extension', { filepath: '/path to/file.extension', extension: 'extension', title: 'file.extension' }],
@ -28,298 +34,368 @@ const rawPathTokenMap: [string, Partial<{ filepath: string, extension: string, t
['./path.to/file', { filepath: './path.to/file', title: 'file' }],
['/path .to/file', { filepath: '/path .to/file', title: 'file' }],
['./path .to/file', { filepath: './path .to/file', title: 'file' }],
['/path/to/file.extension#region', { filepath: '/path/to/file.extension', extension: 'extension', title: 'file.extension', region: '#region' }],
// the extension comes from the file name, so dots in directories and
// dotfiles resolve, and it is not lowercased
['/path/to/.extension', { filepath: '/path/to/.extension', extension: 'extension', title: '.extension' }],
['/path/.to/file.extension', { filepath: '/path/.to/file.extension', extension: 'extension', title: 'file.extension' }],
['/path/.to/.extension', { filepath: '/path/.to/.extension', extension: 'extension', title: '.extension' }],
['/path/.to/file', { filepath: '/path/.to/file', title: 'file' }],
['./script.ps1', { filepath: './script.ps1', extension: 'ps1', title: 'script.ps1' }],
['./File.TS', { filepath: './File.TS', extension: 'TS', title: 'File.TS' }],
// suffixes that are not alphanumeric are not treated as an extension, so
// the language has to be given explicitly for these
['./main.c++', { filepath: './main.c++', title: 'main.c++' }],
['./main.c++ {c++}', { filepath: './main.c++', title: 'main.c++', lang: 'c++' }],
['@/.vscode/scss.code-snippets', { filepath: '@/.vscode/scss.code-snippets', title: 'scss.code-snippets' }],
// region names may contain dots, dashes, digits and underscores
['/path/to/file.extension#region', { filepath: '/path/to/file.extension', extension: 'extension', title: 'file.extension', region: 'region' }],
['./file.ts#my.region', { filepath: './file.ts', extension: 'ts', title: 'file.ts', region: 'my.region' }],
['./file.ts#complex-name_123', { filepath: './file.ts', extension: 'ts', title: 'file.ts', region: 'complex-name_123' }],
// inside the braces: optional highlight lines, then the language override
// (which may contain special characters), then attributes
['./path/to/file.extension {c#}', { filepath: './path/to/file.extension', extension: 'extension', title: 'file.extension', lang: 'c#' }],
['./path/to/file {C++}', { filepath: './path/to/file', title: 'file', lang: 'C++' }],
['/path to/file.extension {1,2,4-6}', { filepath: '/path to/file.extension', extension: 'extension', title: 'file.extension', lines: '1,2,4-6' }],
['/path to/file.extension {1,2,4-6 c#}', { filepath: '/path to/file.extension', extension: 'extension', title: 'file.extension', lines: '1,2,4-6', lang: 'c#' }],
['./file.ts{1 ts:line-numbers}', { filepath: './file.ts', extension: 'ts', title: 'file.ts', lines: '1', lang: 'ts:line-numbers' }],
// everything after the language is kept verbatim, so several attributes and
// quoted values with spaces reach the fence info
['./file.ts{1,2 ts twoslash}', { filepath: './file.ts', extension: 'ts', title: 'file.ts', lines: '1,2', lang: 'ts', attrs: 'twoslash' }],
['./file.ts{ts twoslash noext}', { filepath: './file.ts', extension: 'ts', title: 'file.ts', lang: 'ts', attrs: 'twoslash noext' }],
['./file.ts{1 ts key="a b" twoslash}', { filepath: './file.ts', extension: 'ts', title: 'file.ts', lines: '1', lang: 'ts', attrs: 'key="a b" twoslash' }],
// a lone word in the braces is the language, not an attribute
['./file.ts{twoslash}', { filepath: './file.ts', extension: 'ts', title: 'file.ts', lang: 'twoslash' }],
// stray whitespace in the braces is tolerated
['./file.ts{ ts twoslash }', { filepath: './file.ts', extension: 'ts', title: 'file.ts', lang: 'ts', attrs: 'twoslash' }],
['./file.ts{1, 2}', { filepath: './file.ts', extension: 'ts', title: 'file.ts', lines: '1,2' }],
['./file.ts{}', { filepath: './file.ts', extension: 'ts', title: 'file.ts' }],
// an explicit title overrides the file name and may itself contain brackets
['/path.to/file.extension [title]', { filepath: '/path.to/file.extension', extension: 'extension', title: 'title' }],
['./path.to/file.extension#region {c#}', { filepath: './path.to/file.extension', extension: 'extension', title: 'file.extension', region: '#region', lang: 'c#' }],
['/path/to/file#region {1,2,4-6}', { filepath: '/path/to/file', title: 'file', region: '#region', lines: '1,2,4-6' }],
['./path/to/file#region {1,2,4-6 c#}', { filepath: './path/to/file', title: 'file', region: '#region', lines: '1,2,4-6', lang: 'c#' }],
['./path.to/file.extension#region {c#}', { filepath: './path.to/file.extension', extension: 'extension', title: 'file.extension', region: 'region', lang: 'c#' }],
['/path/to/file#region {1,2,4-6}', { filepath: '/path/to/file', title: 'file', region: 'region', lines: '1,2,4-6' }],
['./path/to/file#region {1,2,4-6 c#}', { filepath: './path/to/file', title: 'file', region: 'region', lines: '1,2,4-6', lang: 'c#' }],
['/path to/file {1,2,4-6 c#} [title]', { filepath: '/path to/file', title: 'title', lines: '1,2,4-6', lang: 'c#' }],
['./path to/file#region {1,2,4-6 c#} [title]', { filepath: './path to/file', title: 'title', region: '#region', lines: '1,2,4-6', lang: 'c#' }],
['./path to/file#region {1,2,4-6 c#} [title]', { filepath: './path to/file', title: 'title', region: 'region', lines: '1,2,4-6', lang: 'c#' }],
['./file.ts#region{1,2 ts twoslash} [my title]', { filepath: './file.ts', extension: 'ts', title: 'my title', region: 'region', lines: '1,2', lang: 'ts', attrs: 'twoslash' }],
['./snippet.js [title [with brackets]]', { filepath: './snippet.js', extension: 'js', title: 'title [with brackets]' }],
// the space before the title is optional
['./foo.js[custom]', { filepath: './foo.js', extension: 'js', title: 'custom' }],
['./demo.js{1,3}[Demo]', { filepath: './demo.js', extension: 'js', title: 'Demo', lines: '1,3' }],
['./demo.js#region[Demo]', { filepath: './demo.js', extension: 'js', title: 'Demo', region: 'region' }],
['@/src/ExampleMod.java{15-21}[java]', { filepath: '@/src/ExampleMod.java', extension: 'java', title: 'java', lines: '15-21' }],
// windows-style separators resolve the file name the same way
['..\\path to\\file.extension', { filepath: '..\\path to\\file.extension', extension: 'extension', title: 'file.extension' }],
['C:\\path\\file.ts#region {1 ts}', { filepath: 'C:\\path\\file.ts', extension: 'ts', title: 'file.ts', region: 'region', lines: '1', lang: 'ts' }]
]
describe('node/markdown/plugins/snippet', () => {
describe('dedent', () => {
test('when 0-level is minimal, do not remove spaces', () => {
expect(
dedent(
[
//
'fn main() {',
' println!("Hello");',
'}'
].join('\n')
)
).toMatchInlineSnapshot(`
"fn main() {
println!("Hello");
}"
`)
describe('parseSnippetPath', () => {
test.each(parseSnippetPathMap)('%s', (rawPath, parsed) => {
expect(removeEmptyKeys(parseSnippetPath(rawPath))).toEqual(parsed)
})
})
test('when 4-level is minimal, remove 4 spaces', () => {
expect(
dedent(
[
//
' let a = {',
' value: 42',
' };'
].join('\n')
)
).toMatchInlineSnapshot(`
"let a = {
value: 42
};"
`)
describe('rendering', () => {
let root: string
let warnings: string[]
const logger = {
warn: (msg: string) => {
warnings.push(msg)
}
}
beforeEach(async () => {
root = await mkdtemp(path.join(tmpdir(), 'vitepress-snippet-'))
warnings = []
})
test('when only 1 line is passed, dedent it', () => {
expect(dedent(' let a = 42;')).toEqual('let a = 42;')
afterEach(async () => {
await rm(root, { recursive: true, force: true })
})
test('handle tabs as well', () => {
expect(
dedent(
[
//
' let a = {',
' value: 42',
' };'
].join('\n')
async function write(name: string, src: string) {
const file = path.join(root, name)
await mkdir(path.dirname(file), { recursive: true })
await writeFile(file, src)
}
async function render(
src: string,
options: MarkdownOptions = {},
env: Partial<MarkdownEnv> = {}
) {
disposeMdItInstance()
const md = await createMarkdownRenderer(
root,
{ highlight: (code) => code, ...options },
'/',
logger
)
).toMatchInlineSnapshot(`
"let a = {
value: 42
};"
`)
})
const fullEnv: MarkdownEnv = {
path: path.join(root, 'index.md'),
relativePath: 'index.md',
cleanUrls: false,
includes: [],
...env
}
const html = await md.renderAsync(src, fullEnv)
return { html, env: fullEnv }
}
test('imports a whole file, deriving language and title', async () => {
await write('snip.ts', 'const a = 1\nconst b = 2\n')
const { html, env } = await render('<<< ./snip.ts')
expect(html).toContain('language-ts')
expect(html).toContain('const a = 1')
expect(html).toContain('const b = 2')
expect(env.includes).toEqual([path.join(root, 'snip.ts')])
})
describe('rawPathToToken', () => {
test.each(rawPathTokenMap)('%s', (rawPath, token) => {
expect(removeEmptyKeys(rawPathToToken(rawPath))).toEqual(token)
test('resolves @ against srcDir', async () => {
await write('nested/snip.js', 'const nested = 1\n')
const { html } = await render(
'<<< @/nested/snip.js',
{},
{ path: path.join(root, 'sub/dir/index.md') }
)
expect(html).toContain('const nested = 1')
})
test('resolves @ without a slash against srcDir', async () => {
await write('nested/snip.js', 'const nested = 1\n')
const { html } = await render(
'<<< @nested/snip.js',
{},
{ path: path.join(root, 'sub/dir/index.md') }
)
expect(html).toContain('const nested = 1')
})
describe('findRegion', () => {
it('returns null when no region markers are present', () => {
const lines = ['function foo() {', ' console.log("hello");', '}']
expect(findRegion(lines, 'foo')).toBeNull()
test('parses a snippet without a space after the marker', async () => {
await write('snip.ts', 'const a = 1\n')
const { html } = await render('<<<./snip.ts')
expect(html).toContain('const a = 1')
})
it('ignores non-matching region names', () => {
const lines = [
'// #region regionA',
'some code here',
'// #endregion regionA'
]
expect(findRegion(lines, 'regionC')).toBeNull()
test('does not dedent whole-file imports', async () => {
await write('indented.ts', ' const a = 1\n const b = 2\n')
const { html } = await render('<<< ./indented.ts')
expect(html).toContain(' const a = 1')
})
it('returns null if a region start marker exists without a matching end marker', () => {
const lines = [
'// #region missingEnd',
'console.log("inside region");',
'console.log("still inside");'
]
expect(findRegion(lines, 'missingEnd')).toBeNull()
test('passes attrs to the highlighter and keeps them out of the title', async () => {
await write('snip.ts', 'const a = 1\nconst b = 2\n')
const calls: { lang: string; attrs: string }[] = []
await render('<<< ./snip.ts{1 ts twoslash} [my title]', {
highlight: (code, lang, attrs) => {
calls.push({ lang, attrs })
return code
}
})
it('returns null if an end marker exists without a preceding start marker', () => {
const lines = [
'// #endregion ghostRegion',
'console.log("stray end marker");'
]
expect(findRegion(lines, 'ghostRegion')).toBeNull()
expect(calls).toHaveLength(1)
expect(calls[0].lang).toBe('ts')
expect(calls[0].attrs).toContain('twoslash')
expect(calls[0].attrs).toContain('{1}')
expect(calls[0].attrs).not.toContain('my title')
})
it('detects C#/JavaScript style region markers with matching tags', () => {
const lines = [
'Console.WriteLine("Before region");',
'#region hello',
'Console.WriteLine("Hello, World!");',
'#endregion hello',
'Console.WriteLine("After region");'
]
const result = findRegion(lines, 'hello')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'Console.WriteLine("Hello, World!");'
)
test('resolves relative paths against the real file path', async () => {
await write('sub/snip.js', 'const real = 1\n')
const { html } = await render(
'<<< ./snip.js',
{},
{
path: path.join(root, 'rewritten/index.md'),
realPath: path.join(root, 'sub/index.md')
}
)
expect(html).toContain('const real = 1')
})
it('detects region markers even when the end marker omits the region name', () => {
const lines = [
'Console.WriteLine("Before region");',
'#region hello',
'Console.WriteLine("Hello, World!");',
'#endregion',
'Console.WriteLine("After region");'
]
const result = findRegion(lines, 'hello')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'Console.WriteLine("Hello, World!");'
test('concatenates all regions with the requested name', async () => {
await write(
'regions.ts',
[
'// #region one',
'const a = 1',
'// #endregion one',
'const outside = 2',
'// #region one',
'const b = 3',
'// #endregion',
''
].join('\n')
)
}
const { html } = await render('<<< ./regions.ts#one')
expect(html).toContain('const a = 1')
expect(html).toContain('const b = 3')
expect(html).not.toContain('const outside')
})
it('handles indented region markers correctly', () => {
const lines = [
' Console.WriteLine("Before region");',
' #region hello',
' Console.WriteLine("Hello, World!");',
' #endregion hello',
' Console.WriteLine("After region");'
]
const result = findRegion(lines, 'hello')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
' Console.WriteLine("Hello, World!");'
test('dedents extracted regions', async () => {
await write(
'indent.ts',
[
'function f() {',
' // #region inner',
' const x = 1',
' // #endregion inner',
'}',
''
].join('\n')
)
}
const { html } = await render('<<< ./indent.ts#inner')
expect(html).toContain('const x = 1')
expect(html).not.toContain(' const x = 1')
})
it('detects TypeScript style region markers', () => {
const lines = [
'let regexp: RegExp[] = [];',
'// #region foo',
'let start = -1;',
'// #endregion foo'
]
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'let start = -1;'
)
}
const nested = [
'// #region outer',
'let a = 1',
'// #region nested',
'let b = 2',
'// #endregion nested',
'/* #region css */',
'let c = 3',
'/* #endregion css */',
'// #endregion outer',
''
].join('\n')
test('strips markers of the matched style by default', async () => {
await write('nested.ts', nested)
const region = await render('<<< ./nested.ts#outer')
// the double-slash markers matched the region, the css ones did not
expect(region.html).not.toContain('#region nested')
expect(region.html).toContain('#region css')
expect(region.html).toContain('let b = 2')
expect(region.html).toContain('let c = 3')
// whole-file imports keep their markers
const whole = await render('<<< ./nested.ts')
expect(whole.html).toContain('#region outer')
expect(whole.html).toContain('let a = 1')
})
it('detects CSS style region markers', () => {
const lines = [
'.body-content {',
'/* #region foo */',
' padding-left: 15px;',
'/* #endregion foo */',
' padding-right: 15px;',
'}'
]
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
' padding-left: 15px;'
)
}
test('strips every marker style with stripRegionMarkers: all', async () => {
await write('nested.ts', nested)
const region = await render('<<< ./nested.ts#outer', {
snippet: { stripRegionMarkers: 'all' }
})
expect(region.html).not.toContain('#region')
expect(region.html).toContain('let b = 2')
it('detects HTML style region markers', () => {
const lines = [
'<div>Some content</div>',
'<!-- #region foo -->',
' <h1>Hello world</h1>',
'<!-- #endregion foo -->',
'<div>Other content</div>'
]
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
' <h1>Hello world</h1>'
)
}
const whole = await render('<<< ./nested.ts', {
snippet: { stripRegionMarkers: 'all' }
})
expect(whole.html).not.toContain('#region')
expect(whole.html).toContain('let a = 1')
})
it('detects Visual Basic style region markers (with case-insensitive "End")', () => {
const lines = [
'Console.WriteLine("VB")',
'#Region VBRegion',
' Console.WriteLine("Inside region")',
'#End Region VBRegion',
'Console.WriteLine("Done")'
]
const result = findRegion(lines, 'VBRegion')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
' Console.WriteLine("Inside region")'
)
}
test('keeps marker lines with stripRegionMarkers: false', async () => {
await write('nested.ts', nested)
const region = await render('<<< ./nested.ts#outer', {
snippet: { stripRegionMarkers: false }
})
expect(region.html).toContain('#region nested')
expect(region.html).toContain('#region css')
})
test('applies lang, highlight lines, attrs and title to the fence', async () => {
await write('snip.ts', 'const a = 1\nconst b = 2\n')
it('detects Bat style region markers', () => {
const lines = ['::#region foo', 'echo off', '::#endregion foo']
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'echo off'
const { html } = await render(
'::: code-group\n\n<<< ./snip.ts{1 js twoslash} [custom title]\n\n:::'
)
}
expect(html).toContain('language-js')
expect(html).toContain('custom title')
expect(html).not.toContain('twoslash')
})
it('detects C/C++ style region markers using #pragma', () => {
const lines = [
'#pragma region foo',
'int a = 1;',
'#pragma endregion foo'
]
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'int a = 1;'
test('throws when the file is missing', async () => {
await expect(render('<<< ./missing.ts')).rejects.toThrow(
/Code snippet path not found/
)
}
})
it('returns the first complete region when multiple regions exist', () => {
const lines = [
'// #region foo',
'first region content',
'// #endregion foo',
'// #region foo',
'second region content',
'// #endregion foo'
]
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'first region content'
test('throws when the path is a directory', async () => {
await write('dir/file.ts', 'const a = 1\n')
await expect(render('<<< ./dir')).rejects.toThrow(/directory/)
})
test('throws when the region is missing', async () => {
await write('snip.ts', 'const a = 1\n')
await expect(render('<<< ./snip.ts#nope')).rejects.toThrow(
/region "nope" not found/i
)
}
})
it('handles nested regions with different names properly', () => {
const lines = [
'// #region foo',
"console.log('line before nested');",
'// #region bar',
"console.log('nested content');",
'// #endregion bar',
'// #endregion foo'
]
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
const extracted = lines.slice(result.start, result.end).join('\n')
const expected = [
"console.log('line before nested');",
'// #region bar',
"console.log('nested content');",
'// #endregion bar'
].join('\n')
expect(extracted).toBe(expected)
test('silent mode renders nothing and warns', async () => {
await write('snip.ts', 'const a = 1\n')
const missingFile = await render('<<< ./missing.ts', {
snippet: { silent: true }
})
expect(missingFile.html).not.toContain('<pre')
expect(missingFile.env.includes).toEqual([path.join(root, 'missing.ts')])
const missingRegion = await render('<<< ./snip.ts#nope', {
snippet: { silent: true }
})
expect(missingRegion.html).not.toContain('<pre')
expect(warnings).toHaveLength(2)
expect(warnings[0]).toContain('missing.ts')
expect(warnings[1]).toContain('nope')
})
test('escaped and indented markers are not parsed as snippets', async () => {
const escaped = await render('\\<<< ./snip.ts')
expect(escaped.html).toContain('&lt;&lt;&lt; ./snip.ts')
const indented = await render(' <<< ./snip.ts')
expect(indented.html).toContain('&lt;&lt;&lt; ./snip.ts')
expect(indented.env.includes).toEqual([])
})
test.runIf(process.platform === 'win32')(
'resolves windows-style paths',
async () => {
await write('nested/snip.ts', 'const a = 1\n')
const relative = await render('<<< .\\nested\\snip.ts')
expect(relative.html).toContain('const a = 1')
const rooted = await render('<<< @\\nested\\snip.ts')
expect(rooted.html).toContain('const a = 1')
// the watched path stays platform-native for snippets
expect(relative.env.includes).toEqual([
path.join(root, 'nested/snip.ts')
])
}
)
test('normalizes CRLF in imported files', async () => {
await write('crlf.ts', 'const a = 1\r\nconst b = 2\r\n')
const { html } = await render('<<< ./crlf.ts')
expect(html).toContain('const a = 1\nconst b = 2')
expect(html).not.toContain('\r')
})
})
})

@ -0,0 +1,529 @@
import {
dedent,
findRegions,
markers,
stripRegionMarkers
} from 'node/markdown/regions'
const extract = (lines: string[], name: string) =>
findRegions(lines, name)
.flatMap((r) => lines.slice(r.start, r.end))
.join('\n')
describe('node/markdown/regions', () => {
describe('dedent', () => {
test('keeps lines when 0-level is minimal', () => {
expect(dedent(['fn main() {', ' println!("Hello");', '}'])).toEqual([
'fn main() {',
' println!("Hello");',
'}'
])
})
test('removes the common minimal indent', () => {
expect(dedent([' let a = {', ' value: 42', ' };'])).toEqual([
'let a = {',
' value: 42',
'};'
])
})
test('dedents a single line', () => {
expect(dedent([' let a = 42;'])).toEqual(['let a = 42;'])
})
test('handles tabs', () => {
expect(dedent(['\tlet a = {', '\t\tvalue: 42', '\t};'])).toEqual([
'let a = {',
'\tvalue: 42',
'};'
])
})
test('ignores blank lines when computing the minimal indent', () => {
expect(dedent([' a', '', ' b'])).toEqual(['a', '', 'b'])
})
test('keeps whitespace-only input as is', () => {
expect(dedent(['', ' '])).toEqual(['', ' '])
})
})
describe('findRegions', () => {
it('returns no regions without markers', () => {
const lines = ['function foo() {', ' console.log("hello");', '}']
expect(findRegions(lines, 'foo')).toHaveLength(0)
})
it('ignores non-matching and prefix-matching region names', () => {
const lines = [
'// #region regionA',
'some code here',
'// #endregion regionA'
]
expect(findRegions(lines, 'regionC')).toHaveLength(0)
expect(findRegions(lines, 'region')).toHaveLength(0)
})
it('returns no regions for a start marker without a matching end', () => {
const lines = [
'// #region missingEnd',
'console.log("inside region");',
'console.log("still inside");'
]
expect(findRegions(lines, 'missingEnd')).toHaveLength(0)
})
it('returns no regions for an end marker without a preceding start', () => {
const lines = [
'// #endregion ghostRegion',
'console.log("stray end marker");'
]
expect(findRegions(lines, 'ghostRegion')).toHaveLength(0)
})
it('ignores non-marker lines containing the word region', () => {
const lines = [
'const region = "region"',
'// #region hello',
'const x = 1',
'// endregion hello is mentioned here without a comment prefix'
]
expect(findRegions(lines, 'hello')).toHaveLength(0)
})
it('detects C#-style markers', () => {
const lines = [
'Console.WriteLine("Before region");',
'#region hello',
'Console.WriteLine("Hello, World!");',
'#endregion hello',
'Console.WriteLine("After region");'
]
expect(extract(lines, 'hello')).toBe(
'Console.WriteLine("Hello, World!");'
)
})
it('closes a named region with an anonymous end marker', () => {
const lines = [
'#region hello',
'Console.WriteLine("Hello, World!");',
'#endregion',
'Console.WriteLine("After region");'
]
expect(extract(lines, 'hello')).toBe(
'Console.WriteLine("Hello, World!");'
)
})
it('does not close a region with a differently named end marker', () => {
const lines = [
'#region hello',
'Console.WriteLine("Hello, World!");',
'#endregion world'
]
expect(findRegions(lines, 'hello')).toHaveLength(0)
})
it('keeps indentation of indented markers and content', () => {
const lines = [
' #region hello',
' Console.WriteLine("Hello, World!");',
' #endregion hello'
]
expect(extract(lines, 'hello')).toBe(
' Console.WriteLine("Hello, World!");'
)
})
it('detects double-slash markers with and without spacing', () => {
const lines = [
'let regexp: RegExp[] = [];',
'// #region foo',
'let start = -1;',
'//#endregion foo'
]
expect(extract(lines, 'foo')).toBe('let start = -1;')
})
it('detects hash-less double-slash markers like VS Code', () => {
const lines = ['// region foo', 'let start = -1;', '// endregion foo']
expect(extract(lines, 'foo')).toBe('let start = -1;')
})
it('detects CSS-style markers', () => {
const lines = [
'/* #region foo */',
' padding-left: 15px;',
'/*#endregion foo*/'
]
expect(extract(lines, 'foo')).toBe(' padding-left: 15px;')
})
it('detects HTML-style markers, with the hash optional', () => {
const lines = [
'<!-- #region foo -->',
' <h1>Hello world</h1>',
'<!--#endregion foo-->',
'<!-- region bar -->',
' <h2>Other</h2>',
'<!-- endregion bar -->'
]
expect(extract(lines, 'foo')).toBe(' <h1>Hello world</h1>')
expect(extract(lines, 'bar')).toBe(' <h2>Other</h2>')
})
it('detects Visual Basic-style markers', () => {
const lines = [
'#Region VBRegion',
' Console.WriteLine("Inside region")',
'#End Region VBRegion'
]
expect(extract(lines, 'VBRegion')).toBe(
' Console.WriteLine("Inside region")'
)
})
it('ignores the quotes around a Visual Basic region name', () => {
const lines = [
'#Region "Quoted Name"',
' Console.WriteLine("Inside region")',
'#End Region',
'#Region "Other"',
' Console.WriteLine("Other region")',
'#End Region "Other"'
]
expect(extract(lines, 'Quoted Name')).toBe(
' Console.WriteLine("Inside region")'
)
expect(extract(lines, 'Other')).toBe(
' Console.WriteLine("Other region")'
)
})
it('detects bat-style markers with case-insensitive REM', () => {
const lines = [
'@REM #region hello',
'@ECHO OFF',
'::#endregion hello',
'echo out',
'rem #region hello',
'exit 0',
'Rem #endregion hello'
]
expect(extract(lines, 'hello')).toBe('@ECHO OFF\nexit 0')
})
it('detects dash-dash markers, which require the hash', () => {
const lines = [
'-- #region foo',
'select 1;',
'-- #endregion foo',
'--#region bar',
'select 2;',
'--#endregion bar'
]
expect(extract(lines, 'foo')).toBe('select 1;')
expect(extract(lines, 'bar')).toBe('select 2;')
// prose comments are not markers
expect(findRegions(['-- region of interest', 'select 1;'], '')).toEqual(
[]
)
})
it('detects pragma markers, allowing space after the hash', () => {
const lines = [
'#pragma region foo',
'int a = 1;',
'#pragma endregion foo',
'# pragma region bar',
'int b = 2;',
'# pragma endregion bar'
]
expect(extract(lines, 'foo')).toBe('int a = 1;')
expect(extract(lines, 'bar')).toBe('int b = 2;')
})
it('detects paren-star markers', () => {
const lines = ['(* #region foo *)', 'let a = 1', '(* #endregion foo *)']
expect(extract(lines, 'foo')).toBe('let a = 1')
})
it('detects shell and python style hash markers', () => {
const lines = [
'# region hello',
'echo "inside"',
'#\tendregion hello',
'# #region hello',
'exit 0',
'# #endregion'
]
expect(extract(lines, 'hello')).toBe('echo "inside"\nexit 0')
})
it('detects JSON key-style markers with two or more slashes', () => {
const lines = [
'{',
' "// #region hello": "",',
' "one": true,',
' "//#endregion hello": "",',
' "two": false,',
' "/// #region hello": "",',
' "three": true,',
' "//// #endregion hello": ""',
'}'
]
expect(extract(lines, 'hello')).toBe(' "one": true,\n "three": true,')
})
it('concatenates multiple same-named regions in document order', () => {
const lines = [
'// #region hello',
'first region content',
'// #endregion hello',
'other content',
'// #region hello',
'second region content',
'// #endregion',
'// #region hello',
'third region content',
'// #endregion hello'
]
expect(extract(lines, 'hello')).toBe(
'first region content\nsecond region content\nthird region content'
)
})
it('merges same-named regions across different comment styles', () => {
const lines = [
'<template>',
' <!-- #region shared -->',
' <div>template part</div>',
' <!-- #endregion shared -->',
'</template>',
'<script>',
'// #region shared',
'const scriptPart = true',
'// #endregion shared',
'/* #region shared */',
'console.log(scriptPart)',
'/* #endregion shared */',
'</script>',
'<style>',
'/* #region shared */',
'.style-part {}',
'/* #endregion shared */',
'</style>'
]
const regions = findRegions(lines, 'shared')
expect(regions).toHaveLength(4)
expect(extract(lines, 'shared')).toBe(
[
' <div>template part</div>',
'const scriptPart = true',
'console.log(scriptPart)',
'.style-part {}'
].join('\n')
)
})
it('tracks nesting of same-named regions across styles', () => {
const lines = [
'// #region foo',
"console.log('double-slash only');",
'/* #region foo */',
"console.log('nested in both');",
'// #endregion foo',
"console.log('still in outer');",
'/* #endregion foo */',
"console.log('outside');"
]
const regions = findRegions(lines, 'foo')
expect(regions).toHaveLength(1)
expect(regions[0]).toMatchObject({ start: 1, end: 6 })
})
it('closes the innermost open region with an anonymous end marker', () => {
const lines = [
'<!-- #region demo -->',
'<template><div /></template>',
'<script setup>',
'// #region state',
'const count = ref(0)',
'// #endregion',
'function inc() {}',
'</script>',
'<!-- #endregion demo -->'
]
expect(extract(lines, 'demo')).toBe(
[
'<template><div /></template>',
'<script setup>',
'// #region state',
'const count = ref(0)',
'// #endregion',
'function inc() {}',
'</script>'
].join('\n')
)
expect(extract(lines, 'state')).toBe('const count = ref(0)')
})
it('is not closed by an anonymous end marker of a nested region in a fence', () => {
const lines = [
'<!-- #region sample -->',
'Use markers like this:',
'',
'```js',
'// #region foo',
'const a = 1',
'// #endregion',
'```',
'<!-- #endregion sample -->'
]
expect(extract(lines, 'sample')).toBe(
[
'Use markers like this:',
'',
'```js',
'// #region foo',
'const a = 1',
'// #endregion',
'```'
].join('\n')
)
})
it('is closed by an anonymous end marker of its own style', () => {
// a region documenting region syntax: the marker inside the fence is
// never closed, and the anonymous end marker belongs to the region
const lines = [
'<!-- #region real -->',
'How regions work:',
'',
'```js',
'// #region example',
'const a = 1',
'```',
'<!-- #endregion -->'
]
expect(extract(lines, 'real')).toBe(
[
'How regions work:',
'',
'```js',
'// #region example',
'const a = 1',
'```'
].join('\n')
)
})
it('ignores an anonymous end marker with nothing open in its style', () => {
const lines = [
'<!-- #region real -->',
'// #endregion',
'body',
'<!-- #endregion -->'
]
expect(extract(lines, 'real')).toBe('// #endregion\nbody')
})
it('tolerates an unclosed region nested inside the requested one', () => {
const lines = [
'// #region outer',
'const a = 1',
'// #region inner',
'const b = 2',
'// #endregion outer'
]
expect(extract(lines, 'outer')).toBe(
['const a = 1', '// #region inner', 'const b = 2'].join('\n')
)
})
it('keeps differently named nested regions verbatim', () => {
const lines = [
'// #region foo',
"console.log('line before nested');",
'// #region bar',
"console.log('nested content');",
'// #endregion bar',
'// #endregion foo'
]
expect(extract(lines, 'foo')).toBe(
[
"console.log('line before nested');",
'// #region bar',
"console.log('nested content');",
'// #endregion bar'
].join('\n')
)
})
it('supports special characters in region names', () => {
const lines = [
'// #region complex-name_123',
'const x = 1;',
'// #endregion complex-name_123',
'// #region my.region',
'const y = 2;',
'// #endregion my.region'
]
expect(extract(lines, 'complex-name_123')).toBe('const x = 1;')
expect(extract(lines, 'my.region')).toBe('const y = 2;')
})
it('reports the marker style that opened each region', () => {
const lines = ['// #region foo', 'const x = 1;', '// #endregion foo']
const [region] = findRegions(lines, 'foo')
expect(markers).toContain(region.marker)
expect(region.marker.start.test('// #region other')).toBe(true)
expect(region.marker.start.test('# region other')).toBe(false)
})
})
describe('stripRegionMarkers', () => {
it('strips marker lines of every style and name by default', () => {
const lines = [
'// #region name',
'const a = 0;',
'/* #region HELLO */',
'const b = 0;',
'//\t#endregion complex_name-123',
'const c = 0;',
'/*#endregion*/'
]
expect(stripRegionMarkers(lines)).toEqual([
'const a = 0;',
'const b = 0;',
'const c = 0;'
])
})
it('keeps non-marker lines mentioning regions', () => {
const lines = ['const region = "region"', 'let a = 1']
expect(stripRegionMarkers(lines)).toEqual(lines)
})
it('strips only the given marker styles when provided', () => {
const lines = [
'// #region a',
'const x = 1',
'// #endregion a',
'# region b',
'const y = 2',
'# endregion b'
]
const [region] = findRegions(lines, 'a')
expect(stripRegionMarkers(lines, [region.marker])).toEqual([
'const x = 1',
'# region b',
'const y = 2',
'# endregion b'
])
})
})
})

@ -0,0 +1,155 @@
import { resolveConfig } from 'node/config'
import { createMarkdownToVueRenderFn } from 'node/markdownToVue'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
describe('node/markdownToVue', () => {
let root: string | undefined
afterEach(async () => {
if (root) {
await rm(root, { recursive: true, force: true })
root = undefined
}
})
test('records source line numbers for dead links', async () => {
root = await mkdtemp(path.join(tmpdir(), 'vitepress-dead-link-'))
const file = path.join(root, 'index.md')
const src = '# Home\n\nIntro\n\n[Missing](./missing.md)\n'
await writeFile(file, src)
const siteConfig = await resolveConfig(root, 'build', 'production')
const render = await createMarkdownToVueRenderFn(
siteConfig.srcDir,
{ cache: false },
'/',
false,
false,
siteConfig
)
const result = await render(src, file)
expect(result.deadLinks).toContainEqual({
url: './missing',
file,
line: 5
})
})
test('records source line numbers after frontmatter', async () => {
root = await mkdtemp(path.join(tmpdir(), 'vitepress-dead-link-'))
const file = path.join(root, 'index.md')
const src =
'---\ntitle: Home\n---\n# Home\n\nIntro\n\n[Missing](./missing.md)\n'
await writeFile(file, src)
const siteConfig = await resolveConfig(root, 'build', 'production')
const render = await createMarkdownToVueRenderFn(
siteConfig.srcDir,
{ cache: false },
'/',
false,
false,
siteConfig
)
const result = await render(src, file)
expect(result.deadLinks).toContainEqual({
url: './missing',
file,
line: 8
})
})
test('selects included heading sections after frontmatter', async () => {
root = await mkdtemp(path.join(tmpdir(), 'vitepress-include-'))
const file = path.join(root, 'index.md')
const source = path.join(root, 'source.md')
await writeFile(
source,
[
'---',
'description: Source description',
'---',
'# Intro',
'',
'intro text',
'',
'## Shared',
'',
'shared before target',
'',
'## Target',
'',
'target text',
'',
'### Child',
'',
'child text',
'',
'## Shared',
'',
'shared after target',
''
].join('\n')
)
const src = '<!--@include: ./source.md#target-->'
await writeFile(file, src)
const siteConfig = await resolveConfig(root, 'build', 'production')
const render = await createMarkdownToVueRenderFn(
siteConfig.srcDir,
{ cache: false },
'/',
false,
false,
siteConfig
)
const result = await render(src, file)
expect(result.vueSrc).toContain('<p>target text</p>')
expect(result.vueSrc).toContain('<h3 id="child"')
expect(result.vueSrc).toContain('<p>child text</p>')
expect(result.vueSrc).not.toContain('Source description')
expect(result.vueSrc).not.toContain('intro text')
expect(result.vueSrc).not.toContain('shared before target')
expect(result.vueSrc).not.toContain('shared after target')
})
test('applies rewrites with mismatched Windows drive letter case', async () => {
root = await mkdtemp(path.join(tmpdir(), 'vitepress-rewrite-'))
const file = path.join(root, 'index.md')
await writeFile(file, '# Home\n')
const siteConfig = await resolveConfig(root, 'build', 'production')
siteConfig.srcDir = 'c:/site/docs'
siteConfig.pages = ['en/index.md']
siteConfig.rewrites = {
map: { 'en/index.md': 'index.md' },
inv: { 'index.md': 'en/index.md' }
}
;(siteConfig as any).__dirty = true
const render = await createMarkdownToVueRenderFn(
siteConfig.srcDir,
{ cache: false },
'/',
false,
false,
siteConfig
)
const result = await render('# Home\n', 'C:/site/docs/en/index.md')
expect(result.pageData.relativePath).toBe('index.md')
})
})

@ -0,0 +1,239 @@
import MiniSearch from 'minisearch'
import { resolveConfig } from 'node/config'
import { disposeMdItInstance } from 'node/markdown/markdown'
import { createMarkdownToVueRenderFn } from 'node/markdownToVue'
import { localSearchPlugin } from 'node/plugins/localSearchPlugin'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
describe('node/plugins/localSearchPlugin', () => {
let root: string | undefined
let nodeEnv: string | undefined
beforeEach(() => {
nodeEnv = process.env.NODE_ENV
process.env.NODE_ENV = 'production'
// createMarkdownRenderer is a module-level singleton keyed on nothing —
// reset it so each test's config actually reaches the renderer
disposeMdItInstance()
})
afterEach(async () => {
if (nodeEnv === undefined) {
delete process.env.NODE_ENV
} else {
process.env.NODE_ENV = nodeEnv
}
if (root) {
await rm(root, { recursive: true, force: true })
root = undefined
}
})
test('indexes rewritten pages by rewritten locale path', async () => {
root = await mkdtemp(path.join(tmpdir(), 'vitepress-local-search-'))
const configDir = path.join(root, '.vitepress')
await mkdir(configDir)
await writeFile(
path.join(root, 'index.md'),
'# English home\n\nrootonlytoken\n'
)
await writeFile(
path.join(root, 'zh.md'),
'# Chinese home\n\nlocaleonlytoken\n'
)
await writeFile(
path.join(configDir, 'config.ts'),
[
'export default {',
' rewrites: {',
" 'index.md': 'guide.md',",
" 'zh.md': 'zh/guide.md'",
' },',
' locales: {',
" root: { label: 'English', lang: 'en' },",
" zh: { label: 'Chinese', lang: 'zh' }",
' },',
' themeConfig: {',
" search: { provider: 'local' }",
' }',
'}'
].join('\n')
)
const siteConfig = await resolveConfig(root, 'build', 'production')
const plugin = await localSearchPlugin(siteConfig)
// vite calls configResolved before any other hook
await (plugin.configResolved as any)?.call(
{},
{ publicDir: siteConfig.publicDir }
)
const indexModule = (await (plugin.load as any)?.handler.call(
{},
'/@localSearchIndex'
)) as string
expect(indexModule).toContain(
'"root": () => import(\'@localSearchIndexroot\')'
)
expect(indexModule).toContain('"zh": () => import(\'@localSearchIndexzh\')')
const rootIndex = loadIndex(
(await (plugin.load as any)?.handler.call(
{},
'/@localSearchIndexroot'
)) as string
)
const zhIndex = loadIndex(
(await (plugin.load as any)?.handler.call(
{},
'/@localSearchIndexzh'
)) as string
)
expect(rootIndex.search('rootonlytoken')).toMatchObject([
{ id: '/guide.html#english-home' }
])
expect(rootIndex.search('localeonlytoken')).toEqual([])
expect(zhIndex.search('localeonlytoken')).toMatchObject([
{ id: '/zh/guide.html#chinese-home' }
])
expect(zhIndex.search('rootonlytoken')).toEqual([])
})
test('applies per-locale markdown options (#5350)', async () => {
root = await mkdtemp(path.join(tmpdir(), 'vitepress-local-search-'))
const configDir = path.join(root, '.vitepress')
await mkdir(configDir)
await mkdir(path.join(root, 'zh'))
const content = '::: tip\ntipcontent\n:::\n'
await writeFile(path.join(root, 'index.md'), `# Home\n\n${content}`)
await writeFile(
path.join(root, 'zh', 'index.md'),
`# Chinese home\n\n${content}`
)
await writeFile(
path.join(configDir, 'config.ts'),
[
'export default {',
' locales: {',
" root: { label: 'English', lang: 'en' },",
' zh: {',
" label: 'Chinese',",
" lang: 'zh',",
" markdown: { container: { tipLabel: 'zhtiplabel' } }",
' }',
' },',
' themeConfig: {',
" search: { provider: 'local' }",
' }',
'}'
].join('\n')
)
const siteConfig = await resolveConfig(root, 'build', 'production')
const plugin = await localSearchPlugin(siteConfig)
// configResolved hooks run concurrently, so the search plugin may be the
// one that creates the shared markdown renderer — page renders must still
// pick up per-locale options from it
await (plugin.configResolved as any)?.call(
{},
{ publicDir: siteConfig.publicDir }
)
const render = await createMarkdownToVueRenderFn(
siteConfig.srcDir,
siteConfig.markdown ?? {},
siteConfig.site.base,
false,
false,
siteConfig
)
const rootFile = path.join(root, 'index.md')
const zhFile = path.join(root, 'zh', 'index.md')
const rootPage = await render(await readFile(rootFile, 'utf-8'), rootFile)
const zhPage = await render(await readFile(zhFile, 'utf-8'), zhFile)
expect(rootPage.vueSrc).toContain('TIP')
expect(zhPage.vueSrc).toContain('zhtiplabel')
// the indexed text must use the localized labels too
await (plugin.load as any)?.handler.call({}, '/@localSearchIndex')
const rootIndex = loadIndex(
(await (plugin.load as any)?.handler.call(
{},
'/@localSearchIndexroot'
)) as string
)
const zhIndex = loadIndex(
(await (plugin.load as any)?.handler.call(
{},
'/@localSearchIndexzh'
)) as string
)
expect(zhIndex.search('zhtiplabel')).toHaveLength(1)
expect(rootIndex.search('zhtiplabel')).toEqual([])
expect(rootIndex.search('tip')).toHaveLength(1)
})
test('warns and skips pages that fail to render', async () => {
root = await mkdtemp(path.join(tmpdir(), 'vitepress-local-search-'))
const configDir = path.join(root, '.vitepress')
await mkdir(configDir)
await writeFile(path.join(root, 'index.md'), '# Home\n\nhealthytoken\n')
await writeFile(
path.join(root, 'broken.md'),
'# Broken\n\n<!-- @include: ./missing.md -->\n'
)
await writeFile(
path.join(configDir, 'config.ts'),
"export default { themeConfig: { search: { provider: 'local' } } }"
)
const siteConfig = await resolveConfig(root, 'build', 'production')
const warn = vi
.spyOn(siteConfig.logger, 'warn')
.mockImplementation(() => {})
const plugin = await localSearchPlugin(siteConfig)
await (plugin.configResolved as any)?.call(
{},
{ publicDir: siteConfig.publicDir }
)
// the include throws, but indexing the remaining pages must still resolve —
// in dev this runs unawaited and a rejection would take the server down
await (plugin.load as any)?.handler.call({}, '/@localSearchIndex')
const index = loadIndex(
(await (plugin.load as any)?.handler.call(
{},
'/@localSearchIndexroot'
)) as string
)
expect(index.search('healthytoken')).toHaveLength(1)
expect(warn).toHaveBeenCalledWith(
expect.stringContaining('Failed to index broken.md for search')
)
})
})
function loadIndex(serializedModule: string) {
const serializedIndex = JSON.parse(
serializedModule.slice('export default '.length)
)
return MiniSearch.loadJSON(serializedIndex, {
fields: ['title', 'titles', 'text'],
storeFields: ['title', 'titles']
})
}

@ -0,0 +1,34 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { readFile, readTextFile, readTextFileSync } from 'node/utils/fs'
describe('node/utils/fs', () => {
let root: string
beforeEach(async () => {
root = await mkdtemp(path.join(tmpdir(), 'vitepress-fs-'))
})
afterEach(async () => {
await rm(root, { recursive: true, force: true })
})
test('readFile keeps line endings as is', async () => {
const file = path.join(root, 'crlf.txt')
await writeFile(file, 'a\r\nb\rc\nd')
expect(await readFile(file)).toBe('a\r\nb\rc\nd')
})
test('readTextFile normalizes CRLF and CR to LF', async () => {
const file = path.join(root, 'crlf.txt')
await writeFile(file, 'a\r\nb\rc\nd')
expect(await readTextFile(file)).toBe('a\nb\nc\nd')
})
test('readTextFileSync normalizes CRLF and CR to LF', async () => {
const file = path.join(root, 'crlf.txt')
await writeFile(file, 'a\r\nb\rc\nd')
expect(readTextFileSync(file)).toBe('a\nb\nc\nd')
})
})

@ -0,0 +1,18 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"types": [
"node",
"vitest/globals",
"vite/client",
"../../src/client/shims.d.ts"
],
"paths": {
"client/*": ["../../src/client/*"],
"node/*": ["../../src/node/*"],
"shared/*": ["../../src/shared/*"],
"vitepress": ["../../src/client/index.ts"],
"vitepress/theme": ["../../theme.d.ts"]
}
}
}

@ -9,6 +9,13 @@ import {
localIconLoader
} from 'vitepress-plugin-group-icons'
import llmstxt from 'vitepress-plugin-llms'
import { markdown as esMarkdown } from '../es/config'
import { markdown as faMarkdown } from '../fa/config'
import { markdown as jaMarkdown } from '../ja/config'
import { markdown as koMarkdown } from '../ko/config'
import { markdown as ptMarkdown } from '../pt/config'
import { markdown as ruMarkdown } from '../ru/config'
import { markdown as zhMarkdown } from '../zh/config'
const prod = !!process.env.NETLIFY
const siteUrl = 'https://vitepress.dev'
@ -35,7 +42,6 @@ export default defineConfig({
lastUpdated: true,
cleanUrls: true,
metaChunk: true,
markdown: {
math: true,
@ -51,35 +57,6 @@ export default defineConfig({
}
],
config(md) {
// TODO: remove when https://github.com/vuejs/vitepress/issues/4431 is fixed
const fence = md.renderer.rules.fence!
md.renderer.rules.fence = function (tokens, idx, options, env, self) {
const { localeIndex = 'root' } = env
const codeCopyButtonTitle = (() => {
switch (localeIndex) {
case 'es':
return 'Copiar código'
case 'fa':
return 'کپی کد'
case 'ko':
return '코드 복사'
case 'pt':
return 'Copiar código'
case 'ru':
return 'Скопировать код'
case 'zh':
return '复制代码'
case 'ja':
return 'コードをコピー'
default:
return 'Copy code'
}
})()
return fence(tokens, idx, options, env, self).replace(
'<button title="Copy Code" class="copy"></button>',
`<button title="${codeCopyButtonTitle}" class="copy"></button>`
)
}
md.use(groupIconMdPlugin)
}
},
@ -122,15 +99,16 @@ export default defineConfig({
carbonAds: { code: 'CEBDT27Y', placement: 'vuejsorg' }
},
// prettier-ignore
locales: {
root: { label: 'English', lang: 'en-US', dir: 'ltr' },
zh: { label: '简体中文', lang: 'zh-Hans', dir: 'ltr' },
pt: { label: 'Português', lang: 'pt-BR', dir: 'ltr' },
ru: { label: 'Русский', lang: 'ru-RU', dir: 'ltr' },
es: { label: 'Español', lang: 'es', dir: 'ltr' },
ko: { label: '한국어', lang: 'ko-KR', dir: 'ltr' },
fa: { label: 'فارسی', lang: 'fa-IR', dir: 'rtl' },
ja: { label: '日本語', lang: 'ja', dir: 'ltr' }
zh: { label: '简体中文', lang: 'zh-Hans', dir: 'ltr', markdown: zhMarkdown },
pt: { label: 'Português', lang: 'pt-BR', dir: 'ltr', markdown: ptMarkdown },
ru: { label: 'Русский', lang: 'ru-RU', dir: 'ltr', markdown: ruMarkdown },
es: { label: 'Español', lang: 'es', dir: 'ltr', markdown: esMarkdown },
ko: { label: '한국어', lang: 'ko-KR', dir: 'ltr', markdown: koMarkdown },
fa: { label: 'فارسی', lang: 'fa-IR', dir: 'rtl', markdown: faMarkdown },
ja: { label: '日本語', lang: 'ja', dir: 'ltr', markdown: jaMarkdown }
},
vite: {
@ -145,10 +123,7 @@ export default defineConfig({
}
}),
prod && llmstxt({ workDir: 'en', ignoreFiles: ['index.md'] })
],
experimental: {
enableNativePlugin: true
}
]
},
// prettier-ignore

@ -1,6 +1,27 @@
@font-face {
font-family: 'Vazirmatn';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('https://cdn.jsdelivr.net/npm/vazirmatn@33.0.3/fonts/webfonts/Vazirmatn%5Bwght%5D.woff2')
format('woff2');
/* google fonts' arabic range, plus guillemets and ellipsis so persian
punctuation renders in vazirmatn instead of inter */
unicode-range:
U+00AB, U+00BB, U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891,
U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+2026, U+204F, U+2E41,
U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E,
U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22,
U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42,
U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57,
U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A,
U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89,
U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}
:root:where(:lang(fa)) {
--vp-font-family-base:
'Vazirmatn', 'Inter', ui-sans-serif, system-ui, sans-serif,
'Vazirmatn', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
@ -36,8 +57,31 @@
padding: 18px;
}
/* used in reference/default-theme-search */
img[src='/search.png'] {
width: 100%;
aspect-ratio: 1 / 1;
.VPFeature .icon span {
display: inline-block;
width: 1em;
height: 1em;
background-position: center;
background-repeat: no-repeat;
background-size: contain;
&.memo {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cpath fill='%23efd8b1' d='M0 20.3S28.2 50.2 31.2 64c0 0 8.1-17.9 32.8-25.4c0 0-8.8-20.6-32.2-37c0 0-23.9 7.2-31.8 18.7'/%3E%3Cpath fill='%23fff6d7' d='M3.2 7.9s22 38.2 22.4 52.5c0 0 11-14.8 36.4-14c0 0-4.8-22.9-24.6-46.3c0-.1-24.5-.9-34.2 7.8'/%3E%3Cg fill='%237d8b91'%3E%3Cpath d='M19.9 9.4c-.3.4.2-.4 0 0'/%3E%3Cpath d='M19.9 9.4c.2-.3.3-.8.6-1.1c.4-.3.8-.1 1.2-.4c.8-.6 1.2-1.6 1.9-2.3c.8-.8 1.8-1.3 2.9-1.6c1.1-.4 2.2-1.2 3.3-1.2c-.5 1.5-1.5 2.8-1.7 4.5c-.1.9 0 1.8.1 2.7c.2-.7.4-1.4.6-2c.4-.9 1.3-.8 2.1-1.1c.8-.4 1.5-1.1 2.2-1.7c.3.5 1.4-.3 1.6.4c.1.3 2.8-.1 3-.2c-.6 0-2.7.1-2.8-.4c-.3-.1-.9-.2-1.3-.2v-.7c-.6.4-1.2.9-1.8 1.4c-.9.7-2 .5-2.9 1.2c.3-1 .9-1.9 1.3-2.9c.2-.4.3-.9.4-1.3s0-.2-.3-.4c-1.4-.4-2.8.7-4 1.2c-1.1.5-2.1 1.1-2.9 1.9c-.5.5-1.3 2.3-2 2.5c-1.4 0-.9 1.3-1.8 1.7c-.5.2-.8.2-1.3.5c-.5.4-1.2 1.1-2 1c0-.6 0-1.2-.2-1.8c-.8.4-1.1 1.1-1.8 1.6c.2-1.6.9-2.9.9-4.6c-.9.6-1.4 1.3-1.9 2.2c-.6 1.1-1.1 2.9-2.3 3.5c0-1.5.7-3 .5-4.6c-.3.8-.3 1.8-.5 2.5c-.2.9-.4 1.8-.4 2.8c2.2-.3 2.6-3.5 3.9-4.9c-.3 1.4-.8 2.7-.7 4.2c.9-.5 1.3-1.5 2.1-2.1c.3.9-.4 2.1 1.1 1.4c.6-.3 1.1-.8 1.7-1.2c.3-.2.9 0 1.2-.5m21.5 4.4c-.4-.4-1.1-.5-1.6-.9c-.6-.5-1.6-.3-2.2-.1c-1.6.4-2.7.8-4.2 0c0 .5.1 2.5-.7 2.4c-.7 0-.5-2.1-.6-2.8c-.4.6-.7 1.1-.7 1.8c0 .5.2.8-.2 1.3c-.6.5-1.5.4-2.2.3c-.2-1.1.1-2.4.2-3.6c-.2 1-.5 2-.7 3.1c-.2.8-.9.9-1.5.7c-.1-.2.1-1 .1-1.2c-.6 1.1-1 2.5-1.8 3.5c-.4.5-1.3 1-1.7.2c-.3-.5-.1-1.2 0-1.8c-.5.6-1 2.3-1.8 2.3c.1-.4.4-.8-.1-1c-.2.4-.4.9-.7 1.4c.2-.6.4-1.3.5-2c-1.3.8-2.1 1.9-2.8 3.2c0-.2.1-.7.1-.9c-.1.4-.9 1.9-.4 2.2c.7-1.1 1.3-2.3 2.3-3.2c-.5 1.5-1.1 2.8-1.4 4.3c.6-.7 1.2-1.5 1.7-2.3c.1-.3.3-.6.4-.9c.3-.8.3-.2.9-.6c.5-.4.8-1.1 1.1-1.7c-.1 1.8 1.4 2.1 2.4.7c.4-.6.7-1.2 1-1.8l.8.2c.4 0 .7 0 1-.2c1.1.4 2.8.5 3.5-.8c1.2 1.3 2-.7 2.1-1.7c1.2.5 2.3-.1 3.5-.5c.7-.2 2-.8 2.5 0c.2.3.7.3 1.2.4M30.8 24.1s.1 0 0 0m0 0c0-.4-.3-.6-.7-.3c-.5.4-.2-.3-.2-.7c0-.5.1-.9.1-1.4c-.2.4-.5.8-.7 1.2c-.4.7-.3 2-1.3 2.1c.2-.5.3-1 .4-1.5c-.5.5-1.1.8-1.1 1.6c0 1-1 3.4-2.3 3.1c.1-2 1.2-3.8 1.7-5.7c-2 1.2-3.7 4.2-3.9 6.4c1-1.7 1.7-3.6 3.1-5c-.4 1.5-.9 2.9-1 4.4c0 .3.8.1.9.1c.7-.2 1.2-.9 1.5-1.5c.1-.2.3-.5.4-.7c.3-.6.4-.2.9-.5c.7-.3.7-1.2 1.4-1.1s2.2-.1 2.7-.4c-.6 0-1.2.1-1.9-.1m-.5-.4l.2.2c-.1 0-.2-.1-.2-.2m13.8 18.7c.5-.7-.2-.1 0 0'/%3E%3Cpath d='M58.4 39.7c-1.8-1.3-3.9-2.1-5.9-3.2c.4.6 1 1.3 1.3 2c-1 .1-.9 1.9-1 1.9c-.5-.3-1.9 0-2.5.1c0-.1 0-.2.1-.3c-1.8 1.2-3.3 3-5.7 2.5c1.1-1.7 2-3.6 1.9-5.7c-1.8.6-2.8 2.3-3.6 3.9c-.2-.6-1-1.1-1.5-.6c-.6.5-.9 1.5-1.1 2.2c-.3.9-.4 1.9-.5 2.8c0 .7-.1.8.6.8c1.7 0 3.2-2 4-3.2c2.2.6 3.5-.4 5.2-1.6c0 .8.6 1.3 1.4 1.1s1.2-1.2 1.8-1.2c.5 0 .8.2 1.3.2s1.1-.1 1.6-.2c-.6-.7-.8-1.2-1-2.1c1.2.3 2.3.6 3.6.6m-16.2 4.9c.2-.2.3-.3 0 0c-.3.2-.2.2-.1.1c-.5.4-1.1.9-1.8.9c0-1.5.2-3.3.9-4.6c.4-.6.9-.8 1.5-.3c.4.4-.1.8-.1 1.2c.1-.2.2-.4.3-.5c.1.6.4 1 1 1.3c-.5.6-1 1.3-1.7 1.9m1.9-2.2c-2.4-1 .7-4.1 2.1-4.7c-.2 1.7-1.1 3.3-2.1 4.7m6.9-.8c-.8.2-.8-.6-.7-1.1c.5.2 1 .4 1.6.5c-.2.3-.5.6-.9.6m2.7-.7v-.1zm.2-.6c.1-.1.1-.3.1-.4c-.3.1-.5.2-.8.3c0-.3-.1-1.1.3-1.2c.8-.4.8.9.9 1.4c-.2 0-.4 0-.5-.1m.9-1.8h-.2c-.1-.1 0-.1.2 0'/%3E%3C/g%3E%3Cpath fill='%23ffce31' d='m35.658 20.801l16.124-16.12l7.565 7.567l-16.124 16.12z'/%3E%3Cpath fill='%23ed4c5c' d='m62.6 2.3l-1-1c-1.8-1.8-4.8-1.8-6.6 0l-3.3 3.3l7.6 7.6l3.3-3.3c1.9-1.8 1.9-4.7 0-6.6'/%3E%3Cpath fill='%2393a2aa' d='m49.703 6.679l2.05-2.05l7.566 7.567l-2.051 2.05z'/%3E%3Cpath fill='%23c7d3d8' d='m50.552 7.527l2.05-2.05l5.94 5.939l-2.05 2.05z'/%3E%3Cpath fill='%23fed0ac' d='m35.6 20.8l-3.3 8.6l2.3 2.3l8.6-3.3z'/%3E%3Cpath fill='%23333' d='M31.8 30.9c-.5 1.2.2 1.8 1.3 1.3l4.2-1.6l-3.9-3.9z'/%3E%3Cpath fill='%23ffdf85' d='M35.672 20.82L49.744 6.75l2.545 2.545l-14.071 14.071z'/%3E%3Cpath fill='%23ff8736' d='m40.656 25.869l14.07-14.074l2.545 2.546l-14.07 14.073z'/%3E%3C/svg%3E");
}
&.rocket {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cg fill='%23ff9d27'%3E%3Cpath d='M10.9 48.7c4-4 4.4-5 6.9-2.5s1.5 2.8-2.5 6.9c-3 3-6.8 2.4-6.8 2.4s-.6-3.8 2.4-6.8'/%3E%3Cpath d='M18.5 52.8c1.6-4.2 2.1-4.7-.2-6s-2.3-.4-3.8 3.8c-1.2 3.1.2 5.9.2 5.9s2.7-.5 3.8-3.7'/%3E%3C/g%3E%3Cpath fill='%23fdf516' d='M16.2 48.9c.9-2.3.9-2.8 2.1-2.1c1.3.7 1 1 .1 3.3c-.6 1.7-2.1 2.1-2.1 2.1s-.7-1.5-.1-3.3'/%3E%3Cpath fill='%23ff9d27' d='M17.1 45.7c-1.3-2.3-1.8-1.8-6-.2c-3.1 1.2-3.7 3.8-3.7 3.8s2.8 1.4 5.9.2c4.2-1.6 5.1-1.6 3.8-3.8'/%3E%3Cg fill='%23fdf516'%3E%3Cpath d='M15 47.8c2.3-.9 2.8-.9 2.1-2.1c-.7-1.3-1-1-3.3-.1c-1.7.6-2.1 2.1-2.1 2.1s1.6.7 3.3.1'/%3E%3Cpath d='M13.9 47.6c2.2-2.2 2.4-2.8 3.8-1.4s.8 1.6-1.4 3.8c-1.7 1.7-3.8 1.3-3.8 1.3s-.2-2 1.4-3.7'/%3E%3C/g%3E%3Cpath fill='%233baacf' d='M18.5 38C12.3 27.6 2 31.9 2 31.9s14.7-14.7 24.6-4.8z'/%3E%3Cpath fill='%23428bc1' d='m23.3 30.3l3.2-3.2C16.7 17.2 2 31.9 2 31.9s12.9-9.2 21.3-1.6'/%3E%3Cpath fill='%233baacf' d='M26 45.5C36.4 51.7 32.1 62 32.1 62s14.7-14.7 4.8-24.6z'/%3E%3Cpath fill='%23428bc1' d='m33.7 40.7l3.2-3.2c9.9 9.9-4.8 24.6-4.8 24.6s9.2-13 1.6-21.4'/%3E%3Cpath fill='%23c5d0d8' d='M48.8 30.9C37.1 42.5 24.2 48.8 19.7 44.3s1.8-17.4 13.4-29.1c13.6-13.6 28.7-13 28.7-13s.5 15.1-13 28.7'/%3E%3Cpath fill='%23dae3ea' d='M45.8 27.6C34.2 39.2 22.6 46.8 19.9 44.1s4.9-14.3 16.5-25.9C50 4.6 62 2 62 2s-2.6 12-16.2 25.6'/%3E%3Cpath fill='%23c94747' d='M24.3 47.5c-.5.5-1.3.5-1.8 0l-6-6c-.5-.5-.5-1.4 0-1.9l1.8-1.8l7.8 7.8z'/%3E%3Cpath fill='%23f15744' d='M22.6 45.7c-.5.5-1.1.7-1.4.4l-3.4-3.4c-.3-.3-.1-.9.4-1.4l1.8-1.8l4.4 4.4z'/%3E%3Cpath fill='%233e4347' d='M20.9 48.2c-.3.3-1 .3-1.3 0l-3.9-3.9c-.3-.3-.2-.9.1-1.2l1.2-1.2l5.1 5.1z'/%3E%3Cpath fill='%2362727a' d='M20.1 47.4c-.3.3-.9.4-1.1.2l-2.7-2.7c-.2-.2-.1-.7.3-1l1.2-1.2l3.5 3.5z'/%3E%3Cpath fill='%23c94747' d='M61.8 2.2S56.4 2 49.1 4.8l10.1 10.1C62 7.6 61.8 2.2 61.8 2.2'/%3E%3Cpath fill='%23f15744' d='M61.8 2.2s-4.3.9-10.8 4.6l6.2 6.2c3.7-6.5 4.6-10.8 4.6-10.8'/%3E%3Ccircle cx='43.5' cy='20.5' r='5' fill='%23edf4f9'/%3E%3Ccircle cx='43.5' cy='20.5' r='3.3' fill='%233baacf'/%3E%3Ccircle cx='33.5' cy='30.5' r='5' fill='%23edf4f9'/%3E%3Ccircle cx='33.5' cy='30.5' r='3.3' fill='%233baacf'/%3E%3Cg fill='%23fff'%3E%3Cpath d='M48.9 6.9c-.3.3-.9.3-1.2 0s-.3-.9 0-1.2s.9-.3 1.2 0s.3.9 0 1.2'/%3E%3Ccircle cx='50.6' cy='8.6' r='.8'/%3E%3Ccircle cx='53' cy='11' r='.8'/%3E%3Ccircle cx='55.3' cy='13.4' r='.8'/%3E%3Ccircle cx='57.7' cy='15.7' r='.8'/%3E%3C/g%3E%3C/svg%3E");
}
&.vite {
background-image: url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='1 6.8 30 18.4'%3E%3Cmask id='SVGgudELdDz'%3E%3Cpath fill='%23fff' d='M40.05 45.7c-.67.85-2.02.38-2.02-.69v-10.3a2.26 2.26 0 0 0-2.27-2.26H24.4a1.13 1.13 0 0 1-.92-1.8l7.48-10.46c1.07-1.5 0-3.58-1.84-3.58H15.34a1.13 1.13 0 0 1-.92-1.79l9.7-13.57c.2-.3.55-.48.92-.48h28.89c.92 0 1.46 1.04.92 1.79l-7.48 10.47a2.26 2.26 0 0 0 1.84 3.58H60.6c.94 0 1.47 1.09.89 1.83z'/%3E%3C/mask%3E%3Cg fill='none'%3E%3Cg mask='url(%23SVGgudELdDz)' transform='translate(1 6.8)scale(.393)'%3E%3Cpath fill='%239135ff' d='M0 0h62v47H0z'/%3E%3Cg filter='url(%23SVGNp06lekD)'%3E%3Cellipse cx='24.46' cy='37.75' fill='%23eee6ff' rx='5.51' ry='14.7' transform='rotate(89.8 24.46 37.75)'/%3E%3C/g%3E%3Cg filter='url(%23SVGNp06lekD)'%3E%3Cellipse cx='4.76' cy='18.96' fill='%23eee6ff' rx='10.4' ry='29.85' transform='rotate(89.8 4.76 18.96)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='4.24' cy='17.5' fill='%238900ff' rx='5.51' ry='30.49' transform='rotate(89.8 4.24 17.5)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='8.95' cy='35.5' fill='%238900ff' rx='5.51' ry='30.6' transform='rotate(89.8 8.95 35.5)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='10.48' cy='36.65' fill='%238900ff' rx='5.51' ry='30.6' transform='rotate(89.8 10.48 36.65)'/%3E%3C/g%3E%3Cg filter='url(%23SVGNp06lekD)'%3E%3Cellipse cx='67.34' cy='12.3' fill='%23eee6ff' rx='14.07' ry='22.08' transform='rotate(-86.7 67.34 12.3)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='68.44' cy='15.74' fill='%238900ff' rx='3.47' ry='21.5' transform='rotate(-91 68.44 15.74)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='68.44' cy='15.74' fill='%238900ff' rx='3.47' ry='21.5' transform='rotate(-91 68.44 15.74)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='14.59' cy='9.74' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(39.5 14.6 9.74)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='61.73' cy='-5.32' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(37.9 61.73 -5.32)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='55.62' cy='7.1' fill='%2300c2ff' rx='5.97' ry='9.67' transform='rotate(37.9 55.62 7.1)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='12.33' cy='39.1' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(37.9 12.33 39.1)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='12.33' cy='39.1' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(37.9 12.33 39.1)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='49.86' cy='30.68' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(37.9 49.86 30.68)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='52.62' cy='33.17' fill='%2300c2ff' rx='5.97' ry='15.3' transform='rotate(37.9 52.62 33.17)'/%3E%3C/g%3E%3C/g%3E%3Cpath fill='%2308060e' d='M3.72 6.8C.1 11.98.08 20 3.72 25.2h2.45c-3.64-5.2-3.62-13.22 0-18.4zm24.56 0h-2.45c3.62 5.18 3.64 13.2 0 18.4h2.45c3.64-5.2 3.62-13.22 0-18.4'/%3E%3Cdefs%3E%3Cfilter id='SVGNp06lekD' width='4' height='4' x='-2' y='-2' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='7.66'/%3E%3C/filter%3E%3Cfilter id='SVGQv8P6csY' width='4' height='4' x='-2' y='-2' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='4.6'/%3E%3C/filter%3E%3C/defs%3E%3C/g%3E%3C/svg%3E");
.dark & {
background-image: url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='1 6.8 30 18.4'%3E%3Cmask id='SVGgudELdDz'%3E%3Cpath fill='%23fff' d='M40.05 45.7c-.67.85-2.02.38-2.02-.69v-10.3a2.26 2.26 0 0 0-2.27-2.26H24.4a1.13 1.13 0 0 1-.92-1.8l7.48-10.46c1.07-1.5 0-3.58-1.84-3.58H15.34a1.13 1.13 0 0 1-.92-1.79l9.7-13.57c.2-.3.55-.48.92-.48h28.89c.92 0 1.46 1.04.92 1.79l-7.48 10.47a2.26 2.26 0 0 0 1.84 3.58H60.6c.94 0 1.47 1.09.89 1.83z'/%3E%3C/mask%3E%3Cg fill='none'%3E%3Cg mask='url(%23SVGgudELdDz)' transform='translate(1 6.8)scale(.393)'%3E%3Cpath fill='%239135ff' d='M0 0h62v47H0z'/%3E%3Cg filter='url(%23SVGNp06lekD)'%3E%3Cellipse cx='24.46' cy='37.75' fill='%23eee6ff' rx='5.51' ry='14.7' transform='rotate(89.8 24.46 37.75)'/%3E%3C/g%3E%3Cg filter='url(%23SVGNp06lekD)'%3E%3Cellipse cx='4.76' cy='18.96' fill='%23eee6ff' rx='10.4' ry='29.85' transform='rotate(89.8 4.76 18.96)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='4.24' cy='17.5' fill='%238900ff' rx='5.51' ry='30.49' transform='rotate(89.8 4.24 17.5)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='8.95' cy='35.5' fill='%238900ff' rx='5.51' ry='30.6' transform='rotate(89.8 8.95 35.5)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='10.48' cy='36.65' fill='%238900ff' rx='5.51' ry='30.6' transform='rotate(89.8 10.48 36.65)'/%3E%3C/g%3E%3Cg filter='url(%23SVGNp06lekD)'%3E%3Cellipse cx='67.34' cy='12.3' fill='%23eee6ff' rx='14.07' ry='22.08' transform='rotate(-86.7 67.34 12.3)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='68.44' cy='15.74' fill='%238900ff' rx='3.47' ry='21.5' transform='rotate(-91 68.44 15.74)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='68.44' cy='15.74' fill='%238900ff' rx='3.47' ry='21.5' transform='rotate(-91 68.44 15.74)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='14.59' cy='9.74' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(39.5 14.6 9.74)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='61.73' cy='-5.32' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(37.9 61.73 -5.32)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='55.62' cy='7.1' fill='%2300c2ff' rx='5.97' ry='9.67' transform='rotate(37.9 55.62 7.1)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='12.33' cy='39.1' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(37.9 12.33 39.1)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='12.33' cy='39.1' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(37.9 12.33 39.1)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='49.86' cy='30.68' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(37.9 49.86 30.68)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='52.62' cy='33.17' fill='%2300c2ff' rx='5.97' ry='15.3' transform='rotate(37.9 52.62 33.17)'/%3E%3C/g%3E%3C/g%3E%3Cpath fill='%23fff' d='M3.72 6.8C.1 11.98.08 20 3.72 25.2h2.45c-3.64-5.2-3.62-13.22 0-18.4zm24.56 0h-2.45c3.62 5.18 3.64 13.2 0 18.4h2.45c3.64-5.2 3.62-13.22 0-18.4'/%3E%3Cdefs%3E%3Cfilter id='SVGNp06lekD' width='4' height='4' x='-2' y='-2' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='7.66'/%3E%3C/filter%3E%3Cfilter id='SVGQv8P6csY' width='4' height='4' x='-2' y='-2' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='4.6'/%3E%3C/filter%3E%3C/defs%3E%3C/g%3E%3C/svg%3E");
}
width: 1.3em;
}
&.vue {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='2 3.92 28 24.15'%3E%3Cpath fill='%2341b883' d='M24.4 3.925H30l-14 24.15L2 3.925h10.71l3.29 5.6l3.22-5.6Z'%3E%3C/path%3E%3Cpath fill='%2341b883' d='m2 3.925l14 24.15l14-24.15h-5.6L16 18.415L7.53 3.925Z'%3E%3C/path%3E%3Cpath fill='%2335495e' d='M7.53 3.925L16 18.485l8.4-14.56h-5.18L16 9.525l-3.29-5.6Z'%3E%3C/path%3E%3C/svg%3E");
}
}

@ -1,4 +1,4 @@
import { createRequire } from 'module'
import { createRequire } from 'node:module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress'
const require = createRequire(import.meta.url)

@ -6,7 +6,7 @@ description: Learn how to reference and handle static assets such as images, med
## Referencing Static Assets
All Markdown files are compiled into Vue components and processed by [Vite](https://vitejs.dev/guide/assets.html). You can, **and should**, reference any assets using relative URLs:
All Markdown files are compiled into Vue components and processed by [Vite](https://vite.dev/guide/assets.html). You can, **and should**, reference any assets using relative URLs:
```md
![An image](./image.png)

@ -62,11 +62,32 @@ import Layout from './Layout.vue'
export default {
Layout,
enhanceApp({ app, router, siteData }) {
// ...
// app.component(...)
// app.use(...)
}
}
```
The `enhanceApp` hook allows you to access the [Vue app instance](https://vuejs.org/api/application.html) and other runtime data, which can be used to [register global components](./extending-default-theme.md#registering-global-components), integrate with Vue libraries, etc.
The `router` value is the same VitePress router instance returned by [`useRouter()`](../reference/runtime-api#userouter). To listen for route changes, assign handlers on the router:
```ts [.vitepress/theme/index.ts]
export default {
enhanceApp({ router }) {
router.onBeforeRouteChange = (to) => {
console.log('navigating to', to)
}
router.onAfterRouteChange = (to) => {
console.log('navigated to', to)
}
}
}
```
Return `false` from `onBeforeRouteChange` or `onBeforePageLoad` to cancel navigation.
The default export is the only contract for a custom theme, and only the `Layout` property is required. So technically, a VitePress theme can be as simple as a single Vue component.
Inside your layout component, it works just like a normal Vite + Vue 3 application. Do note the theme also needs to be [SSR-compatible](./ssr-compat).
@ -208,10 +229,10 @@ Finally, if the theme provides types for its theme config:
```ts [.vitepress/config.ts]
import baseConfig from 'awesome-vitepress-theme/config'
import { defineConfigWithTheme } from 'vitepress'
import { defineConfig } from 'vitepress'
import type { ThemeConfig } from 'awesome-vitepress-theme'
export default defineConfigWithTheme<ThemeConfig>({
export default defineConfig<ThemeConfig>({
extends: baseConfig,
themeConfig: {
// Type is `ThemeConfig`

@ -166,6 +166,13 @@ Don't enable options like _Auto Minify_ for HTML code. It will remove comments f
with:
node-version: 24
cache: npm # or pnpm / yarn
- name: Cache VitePress
uses: actions/cache@v4
with:
path: docs/.vitepress/cache
key: ${{ runner.os }}-vitepress-${{ hashFiles('docs/**', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb') }}
restore-keys: |
${{ runner.os }}-vitepress-
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install dependencies

@ -119,7 +119,7 @@ export default {
} satisfies Theme
```
Since we are using Vite, you can also leverage Vite's [glob import feature](https://vitejs.dev/guide/features.html#glob-import) to auto register a directory of components.
Since we are using Vite, you can also leverage Vite's [glob import feature](https://vite.dev/guide/features.html#glob-import) to auto register a directory of components.
## Layout Slots
@ -309,7 +309,7 @@ Coming soon.
## Overriding Internal Components
You can use Vite's [aliases](https://vitejs.dev/config/shared-options.html#resolve-alias) to replace default theme components with your custom ones:
You can use Vite's [aliases](https://vite.dev/config/shared-options.html#resolve-alias) to replace default theme components with your custom ones:
```ts
import { fileURLToPath, URL } from 'node:url'

@ -12,7 +12,7 @@ You can try VitePress directly in your browser on [StackBlitz](https://vitepress
### Prerequisites
- [Node.js](https://nodejs.org/) version 20 or higher.
- [Node.js](https://nodejs.org/) version 22 or higher.
- Terminal for accessing VitePress via its command line interface (CLI).
- Text Editor with [Markdown](https://en.wikipedia.org/wiki/Markdown) syntax support.
- [VSCode](https://code.visualstudio.com/) is recommended, along with the [official Vue extension](https://marketplace.visualstudio.com/items?itemName=Vue.volar).
@ -37,11 +37,15 @@ $ yarn add -D vitepress@next vue
$ bun add -D vitepress@next
```
```sh [deno]
$ deno add -D vitepress@next
```
:::
::: tip NOTE
VitePress is an ESM-only package. Don't use `require()` to import it, and make sure your nearest `package.json` contains `"type": "module"`, or change the file extension of your relevant files like `.vitepress/config.js` to `.mjs`/`.mts`. Refer to [Vite's troubleshooting guide](http://vitejs.dev/guide/troubleshooting.html#this-package-is-esm-only) for more details. Also, inside async CJS contexts, you can use `await import('vitepress')` instead.
VitePress is an ESM-only package. Don't use `require()` to import it, and make sure your nearest `package.json` contains `"type": "module"`, or change the file extension of your relevant files like `.vitepress/config.js` to `.mjs`/`.mts`. Refer to [Vite's troubleshooting guide](http://vite.dev/guide/troubleshooting.html#this-package-is-esm-only) for more details. Also, inside async CJS contexts, you can use `await import('vitepress')` instead.
:::

@ -57,6 +57,37 @@ Refer [`DefaultTheme.Config`](https://github.com/vuejs/vitepress/blob/main/types
**Pro tip:** Config file can be stored at `docs/.vitepress/config/index.ts` too. It might help you organize stuff by creating a configuration file per locale and then merge and export them from `index.ts`.
## Per-locale Markdown Strings
Strings baked into pages by the markdown renderer - the default titles of [custom containers](./markdown#custom-containers) and [GitHub-flavored alerts](./markdown#github-flavored-alerts), and the strings of the code copy button - can be overridden per locale with the `markdown` key of a locale entry:
```ts [docs/.vitepress/config.ts]
import { defineConfig } from 'vitepress'
export default defineConfig({
locales: {
root: { label: 'English', lang: 'en' },
zh: {
label: '简体中文',
lang: 'zh-Hans',
markdown: {
container: {
tipLabel: '提示',
warningLabel: '警告'
// ...the other labels, and titles of `customContainers`
},
codeCopyButton: {
tooltipText: '复制代码',
copiedText: '已复制'
}
}
}
}
})
```
Values fall back to the root-level `markdown` options when a locale leaves them unset. Locale entries can only override the titles of containers registered at the root level - registering new containers per locale is not supported. Also note that since the markdown renderer is created once for the whole site, these can only be declared in the main config file, not in additional configs.
## Separate directory for each locale
The following is a perfectly fine structure:

@ -1,5 +1,6 @@
---
description: VitePress built-in Markdown extensions including custom containers, code blocks with syntax highlighting, line highlighting, code groups, and more.
outline: deep
---
# Markdown Extensions
@ -100,6 +101,36 @@ For more details, see [Frontmatter](../reference/frontmatter-config).
| col 2 is | centered | \$12 |
| zebra stripes | are neat | \$1 |
## Task Lists
**Input**
```md
- [ ] Write the press release
- [x] Update the website
```
**Output**
- [ ] Write the press release
- [x] Update the website
## Footnotes
**Input**
```md
Footnotes are supported[^1], including inline ones^[This is an inline footnote.].
[^1]: Definitions can contain **markdown** and are rendered at the end of the page.
```
**Output**
Footnotes are supported[^1], including inline ones^[This is an inline footnote.].
[^1]: Definitions can contain **markdown** and are rendered at the end of the page.
## Emoji :tada:
**Input**
@ -112,7 +143,7 @@ For more details, see [Frontmatter](../reference/frontmatter-config).
:tada: :100:
A [list of all emojis](https://github.com/markdown-it/markdown-it-emoji/blob/master/lib/data/full.mjs) is available.
A [list of all emojis](https://github.com/mdit-plugins/mdit-plugins/blob/main/packages/plugin-emoji/src/data/full.ts) is available.
## Table of Contents
@ -229,9 +260,82 @@ export default defineConfig({
})
```
On multilingual sites, these labels can also be overridden per locale - see [Per-locale Markdown Strings](./i18n#per-locale-markdown-strings).
### Registering New Containers
Beyond the built-in types, you can register additional containers by mapping their names to their default titles:
```ts
// config.ts
export default defineConfig({
// ...
markdown: {
container: {
customContainers: {
success: 'SUCCESS'
}
}
}
// ...
})
```
Registered names work like the built-in ones - including custom titles, attributes, and the [GitHub-style alert syntax](#github-flavored-alerts):
```md
::: success
You have completed the walkthrough!
:::
> [!SUCCESS] Custom title
> This renders the same way.
```
New containers ship without any styling, so add some in your theme using the container name as the class. For this example, the default theme's palette already provides fitting colors:
```css
/* .vitepress/theme/custom.css */
.custom-block.success {
border-color: transparent;
color: var(--vp-c-text-1);
background-color: var(--vp-c-success-soft);
}
```
### Nesting
The `:::` markers follow the same rules as fenced code blocks (` ``` `): a fence is only closed by a matching fence that is **at least as long** as the one that opened it. To nest containers (or to mix them with [code groups](#code-groups)) make the outer fence longer than the ones inside it.
**Input**
`````md
:::: info Outer container
This box contains another container.
::: details Inner container
```js
console.log('Hello, VitePress!')
```
:::
::::
`````
**Output**
:::: info Outer container
This box contains another container.
::: details Inner container
```js
console.log('Hello, VitePress!')
```
:::
::::
### Additional Attributes
You can add additional attributes to the custom containers. We use [markdown-it-attrs](https://github.com/arve0/markdown-it-attrs) for this feature, and it is supported on almost all markdown elements. For example, you can set the `open` attribute to make the details block open by default:
You can add additional attributes to the custom containers. We use [@mdit/plugin-attrs](https://mdit-plugins.github.io/attrs.html) for this feature, and it is supported on almost all markdown elements. For example, you can set the `open` attribute to make the details block open by default:
**Input**
@ -251,9 +355,25 @@ console.log('Hello, VitePress!')
```
:::
The special `no-title` attribute renders a container without a title element (it has no effect on `details`, which always needs its summary):
**Input**
```md
::: tip {no-title}
Just want to try it out? Skip to the [Quickstart](./getting-started).
:::
```
**Output**
::: tip {no-title}
Just want to try it out? Skip to the [Quickstart](./getting-started).
:::
### `raw`
This is a special container that can be used to prevent style and router conflicts with VitePress. This is especially useful when you're documenting component libraries. You might also wanna check out [whyframe](https://whyframe.dev/docs/integrations/vitepress) for better isolation.
This is a special container that can be used to prevent style and router conflicts with VitePress. This is especially useful when you're documenting component libraries.
**Syntax**
@ -291,7 +411,7 @@ Wraps in a `<div class="vp-raw">`
## GitHub-flavored Alerts
VitePress also supports [GitHub-flavored alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) to render as callouts. They will be rendered the same as the [custom containers](#custom-containers).
VitePress also supports [GitHub-flavored alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) to render as callouts. They will be rendered the same as the [custom containers](#custom-containers). Unlike on GitHub, text placed right after the marker becomes the title of the alert (`> [!NOTE] Custom Title`), and [containers you registered yourself](#registering-new-containers) work here too.
```md
> [!NOTE]
@ -694,6 +814,16 @@ You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/co
<<< @/snippets/snippet-with-region.js#snippet{1}
If a file contains multiple regions with the same name, all of them are imported and concatenated — including regions written in different comment styles, such as a `<!-- #region -->` in the template and a `// #region` in the script of the same Vue SFC. The marker comments delimiting them are removed from the output; set `markdown.snippet.stripRegionMarkers` to `'all'` to also remove markers of other comment styles nested inside the region, or to `false` to keep all of them.
::: tip
Region names may contain letters, digits, `_`, `-` and `.`. Since the name is taken from the end of the path, a file whose name itself contains a `#` needs an explicit region — write `<<< ./my#file.js#region` rather than `<<< ./my#file.js`.
:::
::: warning
Importing a file or region that does not exist throws a build error. Set `markdown.snippet.silent: true` to log a warning and render nothing instead.
:::
You can also specify the language inside the braces (`{}`) like this:
```md
@ -708,7 +838,9 @@ You can also specify the language inside the braces (`{}`) like this:
<<< @/snippets/snippet.cs{1,2,4-6 c#:line-numbers}
```
This is helpful if source language cannot be inferred from your file extension.
This is helpful if source language cannot be inferred from your file extension. Only alphanumeric extensions are inferred, so files like `main.c++` or `scss.code-snippets` need the language spelled out this way.
Anything after the language inside the braces is passed along to the code block as extra attributes — for example, `<<< @/snippets/snippet.ts{ts twoslash}` enables twoslash processing when [`@shikijs/vitepress-twoslash`](https://shiki.style/packages/vitepress#twoslash) is configured. Note that attributes may not contain square brackets.
## Code Groups
@ -916,7 +1048,7 @@ You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/co
```
::: warning
Note that this does not throw errors if your file is not present. Hence, when using this feature make sure that the contents are being rendered as expected.
Including a missing file, region, heading anchor, or an out-of-range line selection throws a build error. Set `markdown.include.silent: true` to log a warning and skip the inclusion instead.
:::
Instead of VS Code regions, you can also use header anchors to include a specific section of the file. For example, if you have a header in your markdown file like this:
@ -966,6 +1098,28 @@ and include it like this:
<!--@@include: ./parts/basics.md#custom-id-->
```
Relative links and images inside included files resolve from the _included_ file's location, so a partial can link to its neighbors no matter which page includes it. Set `markdown.include.rebaseRelativeUrls: false` to leave them resolving relative to the including page instead.
### Including Code Files {#including-code-files}
Since inclusion happens before code blocks are parsed, the directive also works inside fences. Combined with a line range, this lets you show only part of a code file — an alternative to [importing snippets](#import-code-snippets) when regions are not an option:
**Input**
````md
```js
<!--@@include: @/snippets/snippet-with-region.js{2,4}-->
```
````
**Output**
```js
<!--@include: @/snippets/snippet-with-region.js{2,4}-->
```
Note that the included lines are inserted verbatim (indentation is preserved), and content containing backticks needs a longer outer fence.
## Math Equations
This is currently opt-in. To enable it, you need to install `markdown-it-mathjax3` and set `markdown.math` to `true` in your config file:
@ -1012,14 +1166,14 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$
## Image Lazy Loading
You can enable lazy loading for each image added via markdown by setting `lazyLoading` to `true` in your config file:
You can enable lazy loading for each image added via markdown by setting `lazyLoad` to `true` in your config file:
```js
export default {
markdown: {
image: {
// image lazy loading is disabled by default
lazyLoading: true
lazyLoad: true
}
}
}
@ -1031,15 +1185,15 @@ VitePress uses [markdown-it](https://github.com/markdown-it/markdown-it) as the
```js
import { defineConfig } from 'vitepress'
import markdownItAnchor from 'markdown-it-anchor'
import { headerLink } from '@mdit/plugin-anchor'
import markdownItFoo from 'markdown-it-foo'
export default defineConfig({
markdown: {
// options for markdown-it-anchor
// https://github.com/valeriangalliat/markdown-it-anchor#usage
// options for @mdit/plugin-anchor
// https://mdit-plugins.github.io/anchor.html
anchor: {
permalink: markdownItAnchor.permalink.headerLink()
permalink: headerLink()
},
// options for @mdit-vue/plugin-toc

@ -14,7 +14,7 @@ If you're coming from VitePress 0.x version, there're several breaking changes d
- `repo`, `repoLabel`, `docsDir`, `docsBranch`, `editLinks`, `editLinkText` are removed in favor of more flexible api.
- For adding GitHub link with icon to the nav, use [Social Links](../reference/default-theme-nav#navigation-links) feature.
- For adding "Edit this page" feature, use [Edit Link](../reference/default-theme-edit-link) feature.
- `lastUpdated` option is now split into `config.lastUpdated` and `themeConfig.lastUpdatedText`.
- `lastUpdated` option is now split into `config.lastUpdated` and `themeConfig.lastUpdated.text`.
- `carbonAds.carbon` is changed to `carbonAds.code`.
## Frontmatter Config

@ -329,7 +329,7 @@ The paths loader module is run in Node.js and only executed during build time. Y
Generating paths from local files:
```js
import fs from 'fs'
import fs from 'node:fs'
export default {
paths() {

@ -39,7 +39,7 @@ onMounted(() => {
### Conditional Import
You can also conditionally import a dependency using the `import.meta.env.SSR` flag (part of [Vite env variables](https://vitejs.dev/guide/env-and-mode.html#env-variables)):
You can also conditionally import a dependency using the `import.meta.env.SSR` flag (part of [Vite env variables](https://vite.dev/guide/env-and-mode.html#env-variables)):
```js
if (!import.meta.env.SSR) {

@ -204,7 +204,7 @@ Note that this might prevent certain tokens from being syntax highlighted proper
## Using CSS Pre-processors
VitePress has [built-in support](https://vitejs.dev/guide/features.html#css-pre-processors) for CSS pre-processors: `.scss`, `.sass`, `.less`, `.styl` and `.stylus` files. There is no need to install Vite-specific plugins for them, but the corresponding pre-processor itself must be installed:
VitePress has [built-in support](https://vite.dev/guide/features.html#css-pre-processors) for CSS pre-processors: `.scss`, `.sass`, `.less`, `.styl` and `.stylus` files. There is no need to install Vite-specific plugins for them, but the corresponding pre-processor itself must be installed:
```
# .scss and .sass

@ -6,17 +6,15 @@ description: VitePress is a static site generator designed for building fast, co
VitePress is a [Static Site Generator](https://en.wikipedia.org/wiki/Static_site_generator) (SSG) designed for building fast, content-centric websites. In a nutshell, VitePress takes your source content written in [Markdown](https://en.wikipedia.org/wiki/Markdown), applies a theme to it, and generates static HTML pages that can be easily deployed anywhere.
<div class="tip custom-block" style="padding-top: 8px">
::: tip {no-title}
Just want to try it out? Skip to the [Quickstart](./getting-started).
</div>
:::
## Use Cases
- **Documentation**
VitePress ships with a default theme designed for technical documentation. It powers this page you are reading right now, along with the documentation for [Vite](https://vitejs.dev/), [Rollup](https://rollupjs.org/), [Pinia](https://pinia.vuejs.org/), [VueUse](https://vueuse.org/), [Vitest](https://vitest.dev/), [D3](https://d3js.org/), [UnoCSS](https://unocss.dev/), [Iconify](https://iconify.design/) and [many more](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code).
VitePress ships with a default theme designed for technical documentation. It powers this page you are reading right now, along with the documentation for [Vite](https://vite.dev/), [Rollup](https://rollupjs.org/), [Pinia](https://pinia.vuejs.org/), [VueUse](https://vueuse.org/), [Vitest](https://vitest.dev/), [D3](https://d3js.org/), [UnoCSS](https://unocss.dev/), [Iconify](https://iconify.design/) and [many more](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code).
The [official Vue.js documentation](https://vuejs.org/) is also based on VitePress, but uses a custom theme shared between multiple translations.
@ -30,7 +28,7 @@ Just want to try it out? Skip to the [Quickstart](./getting-started).
VitePress aims to provide a great Developer Experience (DX) when working with Markdown content.
- **[Vite-Powered:](https://vitejs.dev/)** instant server start, with edits always instantly reflected (<100ms) without page reload.
- **[Vite-Powered:](https://vite.dev/)** instant server start, with edits always instantly reflected (<100ms) without page reload.
- **[Built-in Markdown Extensions:](./markdown)** Frontmatter, tables, syntax highlighting... you name it. Specifically, VitePress provides many advanced features for working with code blocks, making it ideal for highly technical documentation.

@ -21,16 +21,16 @@ hero:
alt: VitePress
features:
- icon: 📝
- icon: <span class="memo"></span>
title: Focus on your content
details: Effortlessly create beautiful documentation sites with just markdown.
- icon: <svg xmlns="http://www.w3.org/2000/svg" width="30" viewBox="0 0 256 256.32"><defs><linearGradient id="a" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"/><stop offset="100%" stop-color="#BD34FE"/></linearGradient><linearGradient id="b" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"/><stop offset="8.333%" stop-color="#FFDD35"/><stop offset="100%" stop-color="#FFA800"/></linearGradient></defs><path fill="url(#a)" d="M255.153 37.938 134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"/><path fill="url(#b)" d="M185.432.063 96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028 72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"/></svg>
- icon: <span class="vite"></span>
title: Enjoy the Vite DX
details: Instant server start, lightning fast hot updates, and leverage Vite ecosystem plugins.
- icon: <svg xmlns="http://www.w3.org/2000/svg" width="30" viewBox="0 0 256 220.8"><path fill="#41B883" d="M204.8 0H256L128 220.8 0 0h97.92L128 51.2 157.44 0h47.36Z"/><path fill="#41B883" d="m0 0 128 220.8L256 0h-51.2L128 132.48 50.56 0H0Z"/><path fill="#35495E" d="M50.56 0 128 133.12 204.8 0h-47.36L128 51.2 97.92 0H50.56Z"/></svg>
- icon: <span class="vue"></span>
title: Customize with Vue
details: Use Vue syntax and components directly in markdown, or build custom themes with Vue.
- icon: 🚀
- icon: <span class="rocket"></span>
title: Ship fast sites
details: Fast initial load with static HTML, fast post-load navigation with client-side routing.
---

@ -11,7 +11,8 @@ export default {
themeConfig: {
carbonAds: {
code: 'your-carbon-code',
placement: 'your-carbon-placement'
placement: 'your-carbon-placement',
format: 'classic'
}
}
}
@ -19,8 +20,10 @@ export default {
These values are used to call carbon CDN script as shown below.
The `format` option supports `classic`, `responsive`, and `cover`.
```js
`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}`
`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}&format=${format}`
```
To learn more about Carbon Ads configuration, please visit [Carbon Ads website](https://www.carbonads.net/).

@ -25,10 +25,28 @@ export default {
## i18nRouting
- Type: `boolean`
- Type: `boolean | ((data: VitePressData<DefaultTheme.Config>, route: Route, targetLocale: string) => string)`
Changing locale to say `zh` will change the URL from `/foo` (or `/en/foo/`) to `/zh/foo`. You can disable this behavior by setting `themeConfig.i18nRouting` to `false`.
Set `themeConfig.i18nRouting` to a function to customize the locale link. The function receives the current VitePress data, the current route, and the target locale key, and returns the target link.
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
i18nRouting(data, route, targetLocale) {
const target = data.site.value.locales[targetLocale]
const targetLink =
target.link || (targetLocale === 'root' ? '/' : `/${targetLocale}/`)
return `${targetLink}${route.data.relativePath.replace(/\.md$/, '')}${route.hash}`
}
}
})
```
## logo
- Type: `ThemeableImage`
@ -235,6 +253,7 @@ export default {
// You can add any icon from simple-icons (https://simpleicons.org/):
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' },
{ icon: 'twitter', link: '...' },
{ icon: 'discord', link: '/community', target: '_self' },
// You can also add custom icons by passing SVG as string:
{
icon: {
@ -254,6 +273,7 @@ interface SocialLink {
icon: string | { svg: string }
link: string
ariaLabel?: string
target?: string
}
```
@ -368,6 +388,7 @@ export default {
carbonAds: {
code: 'your-carbon-code',
placement: 'your-carbon-placement'
format: 'classic'
}
}
}
@ -377,6 +398,7 @@ export default {
export interface CarbonAdsOptions {
code: string
placement: string
format?: 'classic' | 'responsive' | 'cover'
}
```

@ -291,6 +291,12 @@ export default defineConfig({
})
```
Use `askAi.sidePanel.panel.suggestedQuestions` for side panel suggested
questions. Algolia's standalone Ask AI examples also mention
`askAi.suggestedQuestions`, but that top-level option is not enough for
VitePress side panel mode and does not make the integrated keyword-search
modal display suggested questions on first open.
If you need to disable the keyboard shortcut, use the `keyboardShortcuts` option at the sidepanel root level:
```ts

@ -184,3 +184,63 @@ export default {
}
}
```
## Path Prefix
When your documentation structure has deep directories or groups located under the same subdirectory, you can use the `base` option to automatically prepend a path prefix to all nested `items` inside that group. This avoids repeating the same path prefix for every `link`.
The `base` option is supported in both multiple sidebar configurations and nested sidebar groups.
### In Multiple Sidebars
You can define `base` at the root of a sidebar section configuration:
```js {5}
export default {
themeConfig: {
sidebar: {
'/guide/': {
base: '/guide/',
items: [
// This link is resolved to `/guide/introduction`
{ text: 'Introduction', link: 'introduction' },
// This link is resolved to `/guide/getting-started`
{ text: 'Getting Started', link: 'getting-started' }
]
}
}
}
}
```
### In Nested Groups
You can also use `base` inside nested sidebar groups. It will apply to the immediate children of that group:
```js{6,13}
export default {
themeConfig: {
sidebar: [
{
text: 'Reference',
base: '/reference/',
items: [
// This link is resolved to `/reference/site-config`
{ text: 'Site Config', link: 'site-config' },
{
text: 'Default Theme',
// Nested base overrides the parent path prefix
base: '/reference/default-theme-',
items: [
// This link is resolved to `/reference/default-theme-nav`
{ text: 'Nav', link: 'nav' },
// This link is resolved to `/reference/default-theme-sidebar`
{ text: 'Sidebar', link: 'sidebar' }
]
}
]
}
]
}
}
```

@ -62,6 +62,8 @@ interface PageData {
}
```
`page.headers` is populated only when [`markdown.headers`](./site-config#markdown) is enabled. Without that option, it remains an empty array. The default theme outline reads rendered headings from the page content, so it can still appear when `page.headers` is empty.
**Example:**
```vue
@ -122,11 +124,23 @@ interface Router {
}
```
Assign route-change handlers on the router instance:
```ts
const router = useRouter()
router.onBeforeRouteChange = (to) => {
console.log('navigating to', to)
}
```
For custom themes, the same router is available from [`enhanceApp`](../guide/custom-theme#theme-interface).
## `withBase` <Badge type="info" text="helper" />
- **Type**: `(path: string) => string`
Appends the configured [`base`](./site-config#base) to a given URL path. Also see [Base URL](../guide/asset-handling#base-url).
Prepends the configured [`base`](./site-config#base) to a given URL path. Also see [Base URL](../guide/asset-handling#base-url).
## `<Content />` <Badge type="info" text="component" />

@ -134,13 +134,43 @@ export default defineConfigWithTheme<ThemeConfig>({
You can configure the underlying [Markdown-It](https://github.com/markdown-it/markdown-it) instance using the [markdown](#markdown) option in your VitePress config.
### Page-Level Overrides
Some settings can be overridden for specific pages using frontmatter.
See [Frontmatter Config](./frontmatter-config) for details.
### Directory-Level Overrides
Some config settings can be overridden at the directory level, allowing all pages in that directory to share settings without needing to repeat them in the frontmatter of each page.
This is achieved by adding a file called `config.ts` (or `.js`, `.mjs`, or `.mts`) in the relevant directory. This file should export a config object using `export default`, similar to the main config file.
Nested directories inherit settings from their parent directory, with configuration overrides being merged accordingly.
The `defineAdditionalConfig` helper can be used to get TypeScript-powered intellisense for the available options, though as with `defineConfig` its use is optional.
For example, for a site with multiple languages we might want a different `description` for each language. We could add `es/config.ts` with the following content:
```ts
import { defineAdditionalConfig } from 'vitepress'
export default defineAdditionalConfig({
description: 'Generador de Sitios Estáticos desarrollado con Vite y Vue.'
})
```
This `description` would then be used for all pages in the `es` directory.
Alternatively, when using the built-in i18n features, the settings for a locale directory can be overridden via the `locales` setting in the main configuration file. See [Internationalization](../guide/i18n) for details.
## Site Metadata
### title
- Type: `string`
- Default: `VitePress`
- Can be overridden per page via [frontmatter](./frontmatter-config#title)
- Can be overridden per page via [frontmatter](./frontmatter-config#title) or at the [directory level](#directory-level-overrides)
Title for the site. When using the default theme, this will be displayed in the nav bar.
@ -161,7 +191,7 @@ The title of the page will be `Hello | My Awesome Site`.
### titleTemplate
- Type: `string | boolean`
- Can be overridden per page via [frontmatter](./frontmatter-config#titletemplate)
- Can be overridden per page via [frontmatter](./frontmatter-config#titletemplate) or at the [directory level](#directory-level-overrides)
Allows customizing each page's title suffix or the entire title. For example:
@ -194,7 +224,7 @@ The option can be set to `false` to disable title suffixes.
- Type: `string`
- Default: `A VitePress site`
- Can be overridden per page via [frontmatter](./frontmatter-config#description)
- Can be overridden per page via [frontmatter](./frontmatter-config#description) or at the [directory level](#directory-level-overrides)
Description for the site. This will render as a `<meta>` tag in the page HTML.
@ -208,7 +238,7 @@ export default {
- Type: `HeadConfig[]`
- Default: `[]`
- Can be appended per page via [frontmatter](./frontmatter-config#head)
- Can be appended per page via [frontmatter](./frontmatter-config#head) or at the [directory level](#directory-level-overrides)
Additional elements to render in the `<head>` tag in the page HTML. The user-added tags are rendered before the closing `head` tag, after VitePress tags.
@ -320,6 +350,7 @@ export default {
- Type: `string`
- Default: `en-US`
- Can be overridden at the [directory level](#directory-level-overrides)
The lang attribute for the site. This will render as a `<html lang="en-US">` tag in the page HTML.
@ -430,7 +461,7 @@ export default {
- Type: `string`
- Default: `./.vitepress/cache`
The directory for cache files, relative to [project root](../guide/routing#root-and-source-directory). See also: [cacheDir](https://vitejs.dev/config/shared-options.html#cachedir).
The directory for cache files, relative to [project root](../guide/routing#root-and-source-directory). See also: [cacheDir](https://vite.dev/config/shared-options.html#cachedir).
```ts
export default {
@ -472,13 +503,6 @@ export default {
}
```
### metaChunk <Badge type="warning" text="experimental" />
- Type: `boolean`
- Default: `false`
When set to `true`, extract pages metadata to a separate JavaScript chunk instead of inlining it in the initial HTML. This makes each page's HTML payload smaller and makes the pages metadata cacheable, thus reducing server bandwidth when you have many pages in the site.
### mpa <Badge type="warning" text="experimental" />
- Type: `boolean`
@ -512,7 +536,7 @@ This option injects an inline script that restores users settings from local sto
Whether to get the last updated timestamp for each page using Git. The timestamp will be included in each page's page data, accessible via [`useData`](./runtime-api#usedata).
When using the default theme, enabling this option will display each page's last updated time. You can customize the text via [`themeConfig.lastUpdatedText`](./default-theme-config#lastupdatedtext) option.
When using the default theme, enabling this option will display each page's last updated time. You can customize the text via [`themeConfig.lastUpdated.text`](./default-theme-config#lastupdated) option.
## Customization
@ -530,11 +554,13 @@ export default {
Check the [type declaration and jsdocs](https://github.com/vuejs/vitepress/blob/main/src/node/markdown/markdown.ts) for all the options available.
Set `markdown.headers` to `true` or pass [`@mdit-vue/plugin-headers`](https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-headers) options to collect headings into [`useData().page.headers`](./runtime-api#usedata). This option is disabled by default.
### vite
- Type: `import('vite').UserConfig`
Pass raw [Vite Config](https://vitejs.dev/config/) to internal Vite dev server / bundler.
Pass raw [Vite Config](https://vite.dev/config/) to internal Vite dev server / bundler.
```js
export default {
@ -607,7 +633,7 @@ interface SSGContext {
- Type: `(context: TransformContext) => Awaitable<HeadConfig[]>`
`transformHead` is a build hook to transform the head before generating each page. It will allow you to add head entries that cannot be statically added to your VitePress config. You only need to return extra entries, they will be merged automatically with the existing ones.
`transformHead` is a build hook to add extra tags to the `<head>` of each page. It allows you to add head entries that cannot be statically added to your VitePress config. You only need to return extra entries, they will be merged automatically with the existing ones.
::: warning
Don't mutate anything inside the `context`.
@ -635,44 +661,38 @@ interface TransformContext {
}
```
Note that this hook is only called when generating the site statically. It is not called during dev. If you need to add dynamic head entries during dev, you can use the [`transformPageData`](#transformpagedata) hook instead:
This hook is only called when performing a build, it is not called during dev.
```ts
export default {
transformPageData(pageData) {
pageData.frontmatter.head ??= []
pageData.frontmatter.head.push([
'meta',
{
name: 'og:title',
content:
pageData.frontmatter.layout === 'home'
? `VitePress`
: `${pageData.title} | VitePress`
}
])
}
}
```
The extra tags will be added to the static HTML files generated by the build. They will not be updated during client-side navigation.
#### Example: Adding a canonical URL `<link>`
In many cases, using the [`transformPageData`](#transformpagedata) hook is a cleaner solution. That hook will also be applied to both client-side navigation and during dev. But if generating the head tags is computationally expensive then `transformHead` will avoid that overhead during dev.
#### Example: Adding `og:image` meta
```ts
export default {
transformPageData(pageData) {
const canonicalUrl = `https://example.com/${pageData.relativePath}`
.replace(/index\.md$/, '')
.replace(/\.md$/, '.html')
async transformHead(context) {
if (context.page === '404.md') {
return
}
pageData.frontmatter.head ??= []
pageData.frontmatter.head.push([
'link',
{ rel: 'canonical', href: canonicalUrl }
])
// The implementation details of `generatePageImage` would depend
// on your requirements. Here we assume it generates a suitable
// image for each page and returns the image URL.
const imageUrl = await generatePageImage(context)
return [[
'meta',
{ name: 'og:image', content: imageUrl }
]]
}
}
```
Here we're assuming that the image URL is dynamic and time-consuming to generate. Using `transformHead` avoids that overhead during development.
For simpler cases, it may be possible to use the [`head`](./frontmatter-config#head) setting in frontmatter, or [`transformPageData`](#transformpagedata).
### transformHtml
- Type: `(code: string, id: string, context: TransformContext) => Awaitable<string | void>`
@ -721,3 +741,39 @@ interface TransformPageContext {
siteConfig: SiteConfig
}
```
#### Example: Adding a `<meta name="og:title">`
```ts
export default {
transformPageData(pageData) {
const title = pageData.frontmatter.layout === 'home'
? 'VitePress'
: `${pageData.title} | VitePress`
pageData.frontmatter.head ??= []
pageData.frontmatter.head.push([
'meta',
{ name: 'og:title', content: title }
])
}
}
```
#### Example: Adding a canonical URL `<link>`
```ts
export default {
transformPageData(pageData) {
const canonicalUrl = `https://example.com/${pageData.relativePath}`
.replace(/index\.md$/, '')
.replace(/\.md$/, '.html')
pageData.frontmatter.head ??= []
pageData.frontmatter.head.push([
'link',
{ rel: 'canonical', href: canonicalUrl }
])
}
}
```

@ -1,9 +1,30 @@
import { createRequire } from 'module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress'
import { createRequire } from 'node:module'
import {
defineAdditionalConfig,
type DefaultTheme,
type MarkdownLocaleOptions
} from 'vitepress'
const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
export const markdown: MarkdownLocaleOptions = {
container: {
tipLabel: 'CONSEJO',
infoLabel: 'INFORMACIÓN',
warningLabel: 'ADVERTENCIA',
dangerLabel: 'PELIGRO',
detailsLabel: 'Detalles',
noteLabel: 'NOTA',
importantLabel: 'IMPORTANTE',
cautionLabel: 'PRECAUCIÓN'
},
codeCopyButton: {
tooltipText: 'Copiar código',
copiedText: 'Copiado'
}
}
export default defineAdditionalConfig({
description: 'Generador de Sitios Estáticos desarrollado con Vite y Vue.',
@ -267,8 +288,6 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
afterToolCallText: 'Buscado',
stoppedStreamingText: 'Has detenido esta respuesta',
errorTitleText: 'Error de chat',
threadDepthExceededMessage:
'Esta conversación se ha cerrado para mantener respuestas precisas.',
startNewConversationButtonText: 'Iniciar una nueva conversación'
}
}

@ -6,7 +6,7 @@ description: Aprende cómo referenciar y manejar recursos estáticos como imáge
## Referenciando Assets Estáticos {#referencing-static-assets}
Todos los archivos Markdown son compilados en componentes Vue y procesados por [Vite](https://vitejs.dev/guide/assets.html). Usted puede **y debe** referenciar cualquier asset usando URLs relativas:
Todos los archivos Markdown son compilados en componentes Vue y procesados por [Vite](https://vite.dev/guide/assets.html). Usted puede **y debe** referenciar cualquier asset usando URLs relativas:
```md
![Una imagen](./imagen.png)

@ -119,7 +119,7 @@ export default {
} satisfies Theme
```
Como estamos usando Vite, puede también aprovechar la [funcionalidad de importación glob](https://vitejs.dev/guide/features.html#glob-import) de Vite para registrar automaticamente un directorio de componetes.
Como estamos usando Vite, puede también aprovechar la [funcionalidad de importación glob](https://vite.dev/guide/features.html#glob-import) de Vite para registrar automaticamente un directorio de componetes.
## _Slots_ en el Layout {#layout-slots}
@ -309,7 +309,7 @@ En breve.
## Substituyendo Componentes Internos {#overriding-internal-components}
Puede usar los [aliases](https://vitejs.dev/config/shared-options.html#resolve-alias) Vite para substituir los componentes del tema por defecto por los suyos personalizados:
Puede usar los [aliases](https://vite.dev/config/shared-options.html#resolve-alias) Vite para substituir los componentes del tema por defecto por los suyos personalizados:
```ts
import { fileURLToPath, URL } from 'node:url'

@ -12,7 +12,7 @@ Puede experimentar VitePress directamente en su navegador en [StackBlitz](https:
### Prerrequisitos {#prerequisites}
- [Node.js](https://nodejs.org/) versión 20 o superior.
- [Node.js](https://nodejs.org/) versión 22 o superior.
- Terminal para acessar VitePress a través de su interfaz de linea de comando (CLI).
- Editor de texto con soporte a sintaxis [Markdown](https://en.wikipedia.org/wiki/Markdown).
- [VSCode](https://code.visualstudio.com/) es recomendado, junto con la [extensión oficial Vue](https://marketplace.visualstudio.com/items?itemName=Vue.volar).
@ -41,7 +41,7 @@ $ bun add -D vitepress@next
::: tip NOTA
VitePress es un paquete apenas para ESM. No use `require()` para importarlo, y asegurese de que el `package.json` más cercano contiene `"type": "module"`, o cambie la extensión de archivo de sus archivos relevantes como `.vitepress/config.js` a `.mjs`/`.mts`. Consulte la [Guía de resolución de problemas Vite](http://vitejs.dev/guide/troubleshooting.html#this-package-is-esm-only) para más detalles. Además de eso, dentro de contextos de JavaScript asíncronos, puede usar `await import('vitepress')`.
VitePress es un paquete apenas para ESM. No use `require()` para importarlo, y asegurese de que el `package.json` más cercano contiene `"type": "module"`, o cambie la extensión de archivo de sus archivos relevantes como `.vitepress/config.js` a `.mjs`/`.mts`. Consulte la [Guía de resolución de problemas Vite](http://vite.dev/guide/troubleshooting.html#this-package-is-esm-only) para más detalles. Además de eso, dentro de contextos de JavaScript asíncronos, puede usar `await import('vitepress')`.
:::

@ -1,5 +1,6 @@
---
description: Extensiones Markdown integradas en VitePress, incluyendo contenedores personalizados, bloques de código con resaltado de sintaxis, resaltado de líneas, grupos de código y más.
outline: deep
---
# Extensiones Markdown {#markdown-extensions}
@ -112,7 +113,7 @@ Para más detalles, vea [Frontmatter](../reference/frontmatter-config).
:tada: :100:
Una [lista de todos los emojis](https://github.com/markdown-it/markdown-it-emoji/blob/master/lib/data/full.mjs) está disponible.
Una [lista de todos los emojis](https://github.com/mdit-plugins/mdit-plugins/blob/main/packages/plugin-emoji/src/data/full.ts) está disponible.
## Tabla de Contenido (TOC)
@ -232,7 +233,7 @@ export default defineConfig({
### `raw`
Este es un recipiente especial que puee ser usado para evitar conflictos de estilo y enrutador con VitePress. Esto es especialmente útil al documentar bibliotecas de componentes. Puede tambien verificar [whyframe](https://whyframe.dev/docs/integrations/vitepress) para mejor aislamiento.
Este es un recipiente especial que puee ser usado para evitar conflictos de estilo y enrutador con VitePress. Esto es especialmente útil al documentar bibliotecas de componentes.
**Sintaxis**
@ -888,14 +889,14 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$
## _Lazy Loading_ de Imagenes {#image-lazy-loading}
Puede activar la "carga perezosa" para cada imagen adicionada via markdown definiendo `lazyLoading` como `true` en su archivo de configuración:
Puede activar la "carga perezosa" para cada imagen adicionada via markdown definiendo `lazyLoad` como `true` en su archivo de configuración:
```js
export default {
markdown: {
image: {
// la carga perezosa de imagenes está desactivada por defecto
lazyLoading: true
lazyLoad: true
}
}
}
@ -907,15 +908,15 @@ VitePress usa [markdown-it](https://github.com/markdown-it/markdown-it) como int
```js
import { defineConfig } from 'vitepress'
import markdownItAnchor from 'markdown-it-anchor'
import { headerLink } from '@mdit/plugin-anchor'
import markdownItFoo from 'markdown-it-foo'
export default defineConfig({
markdown: {
// opciones para markdown-it-anchor
// https://github.com/valeriangalliat/markdown-it-anchor#usage
// opciones para @mdit/plugin-anchor
// https://mdit-plugins.github.io/anchor.html
anchor: {
permalink: markdownItAnchor.permalink.headerLink()
permalink: headerLink()
},
// opciones para @mdit-vue/plugin-toc

@ -290,7 +290,7 @@ El módulo de carga de paths es ejecutado en Node.js y apenas durante el momento
Generando paths a partir de archivos locales:
```js
import fs from 'fs'
import fs from 'node:fs'
export default {
paths() {

@ -39,7 +39,7 @@ onMounted(() => {
### Importación Condicional {#conditional-import}
También puede importar una dependencia condicionalmente utilizando la bandera `import.meta.env.SSR` (que forma parte de las [variables de entorno Vite](https://vitejs.dev/guide/env-and-mode.html#env-variables)):
También puede importar una dependencia condicionalmente utilizando la bandera `import.meta.env.SSR` (que forma parte de las [variables de entorno Vite](https://vite.dev/guide/env-and-mode.html#env-variables)):
```js
if (!import.meta.env.SSR) {

@ -204,7 +204,7 @@ Observe que esto puede impedir que ciertos tokens sean realzados correctamente.
## Usando Preprocesadores CSS {#using-css-pre-processors}
VitePress poseé [soporte embutido](https://vitejs.dev/guide/features.html#css-pre-processors) para preprocesadores CSS: archivos `.scss`, `.sass`, `.less`, `.styl` e `.stylus`. No es necesario instalar plugins específicos de Vite para ellos, pero el propio preprocesados correspondiente debe ser instalado:
VitePress poseé [soporte embutido](https://vite.dev/guide/features.html#css-pre-processors) para preprocesadores CSS: archivos `.scss`, `.sass`, `.less`, `.styl` e `.stylus`. No es necesario instalar plugins específicos de Vite para ellos, pero el propio preprocesados correspondiente debe ser instalado:
```
# .scss e .sass

@ -6,17 +6,15 @@ description: VitePress es un generador de sitios estáticos diseñado para crear
VitePress es un [Generador de Sitios Estáticos](https://en.wikipedia.org/wiki/Static_site_generator) (SSG) diseñado para construir sitios web rápidos y enfocados en el contenido. En pocas palabras, VitePress toma tu contenido fuente escrito en [Markdown](https://en.wikipedia.org/wiki/Markdown), le aplica un tema y genera páginas HTML estáticas que se pueden desplegar fácilmente en cualquier lugar.
<div class="tip custom-block" style="padding-top: 8px">
::: tip {no-title}
¿Quieres probarlo? Ve directo al [Inicio Rápido](./getting-started).
</div>
:::
## Casos de Uso {#use-cases}
- **Documentación**
VitePress incluye un tema por defecto diseñado para documentación técnica. Este tema es el que se utiliza en la página que estás leyendo ahora, así como en la documentación de [Vite](https://vitejs.dev/), [Rollup](https://rollupjs.org/), [Pinia](https://pinia.vuejs.org/), [VueUse](https://vueuse.org/), [Vitest](https://vitest.dev/), [D3](https://d3js.org/), [UnoCSS](https://unocss.dev/), [Iconify](https://iconify.design/) y [muchos otros](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code).
VitePress incluye un tema por defecto diseñado para documentación técnica. Este tema es el que se utiliza en la página que estás leyendo ahora, así como en la documentación de [Vite](https://vite.dev/), [Rollup](https://rollupjs.org/), [Pinia](https://pinia.vuejs.org/), [VueUse](https://vueuse.org/), [Vitest](https://vitest.dev/), [D3](https://d3js.org/), [UnoCSS](https://unocss.dev/), [Iconify](https://iconify.design/) y [muchos otros](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code).
La [documentación oficial Vue.js](https://vuejs.org/) también está basada en VitePress, pero utiliza un tema personalizado compartido entre varias traducciones.
@ -30,7 +28,7 @@ VitePress es un [Generador de Sitios Estáticos](https://en.wikipedia.org/wiki/S
VitePress busca ofrecer una excelente Experiencia de Desarrollador (DX) al trabajar con contenido Markdown.
- **[Con tecnología Vite:](https://vitejs.dev/)** inicio instantáneo del servidor, con los cambios reflejados al instante (<100ms) sin recargar la página.
- **[Con tecnología Vite:](https://vite.dev/)** inicio instantáneo del servidor, con los cambios reflejados al instante (<100ms) sin recargar la página.
- **[Extensiones Markdown Integradas:](./markdown)** Frontmatter, tablas, destaque de sintaxis... tú decides. Específicamente, VitePress proporciona muchos recursos para trabajar con bloques de código, tornándolo ideal para documentación altamente técnica.

@ -21,16 +21,16 @@ hero:
alt: VitePress
features:
- icon: 📝
- icon: <span class="memo"></span>
title: Concéntrese en su contenido
details: Cree lindos sitios de documentación apenas con markdown.
- icon: <svg xmlns="http://www.w3.org/2000/svg" width="30" viewBox="0 0 256 256.32"><defs><linearGradient id="a" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"/><stop offset="100%" stop-color="#BD34FE"/></linearGradient><linearGradient id="b" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"/><stop offset="8.333%" stop-color="#FFDD35"/><stop offset="100%" stop-color="#FFA800"/></linearGradient></defs><path fill="url(#a)" d="M255.153 37.938 134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"/><path fill="url(#b)" d="M185.432.063 96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028 72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"/></svg>
- icon: <span class="vite"></span>
title: Disfruta de la experiencia Vite
details: Inicio instantaneo de servidor, actualizaciones ultrarrápidas, y plugins del ecosistema Vite.
- icon: <svg xmlns="http://www.w3.org/2000/svg" width="30" viewBox="0 0 256 220.8"><path fill="#41B883" d="M204.8 0H256L128 220.8 0 0h97.92L128 51.2 157.44 0h47.36Z"/><path fill="#41B883" d="m0 0 128 220.8L256 0h-51.2L128 132.48 50.56 0H0Z"/><path fill="#35495E" d="M50.56 0 128 133.12 204.8 0h-47.36L128 51.2 97.92 0H50.56Z"/></svg>
- icon: <span class="vue"></span>
title: Personaliza con Vue
details: Usa la sintaxis y componentes Vue directamente en markdown, o construye temas personalizados con Vue.
- icon: 🚀
- icon: <span class="rocket"></span>
title: Entrega rápida de sitios
details: Carga inicial rápida con HTML estático, navegación rápida con enrutamiento del lado del cliente.
---

@ -11,7 +11,8 @@ export default {
themeConfig: {
carbonAds: {
code: 'tu-código-carbon',
placement: 'tu-vinculación-carbon'
placement: 'tu-vinculación-carbon',
format: 'classic'
}
}
}
@ -19,8 +20,10 @@ export default {
Estos valores se utilizan para llamar al script en CDN de carbon como se muestra a continuación.
La opción `format` admite `classic`, `responsive` y `cover`.
```js
`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}`
`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}&format=${format}`
```
Para obtener más información de la configuración de Carbono Ads, por favor visite [Site Carbon Ads](https://www.carbonads.net/).

@ -353,16 +353,20 @@ export default {
themeConfig: {
carbonAds: {
code: 'su-código-carbon',
placement: 'su-colocación-carbon'
placement: 'su-colocación-carbon',
format: 'classic'
}
}
}
```
La opción `format` admite `classic`, `responsive` y `cover`.
```ts
export interface CarbonAdsOptions {
code: string
placement: string
format?: 'classic' | 'responsive' | 'cover'
}
```

@ -430,7 +430,7 @@ export default {
- Tipo: `string`
- Predeterminado: `./.vitepress/cache`
El directorio para los archivos de caché, en relación con el [raiz del proyecto](../guide/routing#root-and-source-directory). Vea también: [cacheDir](https://vitejs.dev/config/shared-options.html#cachedir).
El directorio para los archivos de caché, en relación con el [raiz del proyecto](../guide/routing#root-and-source-directory). Vea también: [cacheDir](https://vite.dev/config/shared-options.html#cachedir).
```ts
export default {
@ -503,7 +503,7 @@ Esta opción inyecta un script en línea que restaura la configuración de los u
Para obtener la marca de tiempo de la última actualización para cada página usando Git. El sello de fecha se incluirá en los datos de cada página, accesible a través de [`useData`](./runtime-api#usedata).
Cuando se utiliza el tema predeterminado, al habilitar esta opción se mostrará la última hora de actualización de cada página. Puedes personalizar el texto mediante la opción [`themeConfig.lastUpdatedText`](./default-theme-config#lastupdatedtext).
Cuando se utiliza el tema predeterminado, al habilitar esta opción se mostrará la última hora de actualización de cada página. Puedes personalizar el texto mediante la opción [`themeConfig.lastUpdated.text`](./default-theme-config#lastupdated).
## Personalización {#customization}
@ -525,7 +525,7 @@ Consulte la [declaración de tipo y jsdocs](https://github.com/vuejs/vitepress/b
- Tipo: `import('vite').UserConfig`
Pase la [Configuración Vite](https://vitejs.dev/config/) sin procesar al servidor interno / empaquetador Vite.
Pase la [Configuración Vite](https://vite.dev/config/) sin procesar al servidor interno / empaquetador Vite.
```js
export default {

@ -1,17 +1,39 @@
import { createRequire } from 'module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress'
import { createRequire } from 'node:module'
import {
defineAdditionalConfig,
type DefaultTheme,
type MarkdownLocaleOptions
} from 'vitepress'
const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
export const markdown: MarkdownLocaleOptions = {
container: {
tipLabel: 'نکته',
infoLabel: 'اطلاعات',
warningLabel: 'اخطار',
dangerLabel: 'خطر',
detailsLabel: 'جزئیات',
noteLabel: 'یادداشت',
importantLabel: 'مهم',
cautionLabel: 'احتیاط'
},
codeCopyButton: {
tooltipText: 'کپی کد',
copiedText: 'کپی شد'
}
}
export default defineAdditionalConfig({
description: 'ژنراتور استاتیک وب‌سایت با Vite و Vue',
// prettier-ignore
head: [
['link', { rel: 'preconnect', href: 'https://fonts.googleapis.com' }],
['link', { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' }],
['link', { href: 'https://fonts.googleapis.com/css2?family=Vazirmatn:wght@100..900&display=swap', rel: 'stylesheet' }],
[
'link',
// for the vazirmatn font-face defined in .vitepress/theme/styles.css
{ rel: 'preconnect', href: 'https://cdn.jsdelivr.net', crossorigin: '' }
]
],
themeConfig: {
@ -264,8 +286,6 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
afterToolCallText: 'جستجو برای',
stoppedStreamingText: 'شما این پاسخ را متوقف کردید',
errorTitleText: 'خطای گفتگو',
threadDepthExceededMessage:
'برای حفظ دقت پاسخ ها، این گفت وگو بسته شد.',
startNewConversationButtonText: 'شروع گفت وگوی جدید'
}
}

@ -6,7 +6,7 @@ description: نحوه ارجاع و مدیریت منابع ایستا مانن
## ارجاع به منابع ایستا {#referencing-static-assets}
تمام فایل‌های Markdown به کامپوننت‌های Vue تبدیل و توسط [Vite](https://vitejs.dev/guide/assets.html) پردازش می‌شوند. شما می‌توانید، **و باید**، هر نوع دارایی را با استفاده از URLهای نسبی مرجع قرار دهید:
تمام فایل‌های Markdown به کامپوننت‌های Vue تبدیل و توسط [Vite](https://vite.dev/guide/assets.html) پردازش می‌شوند. شما می‌توانید، **و باید**، هر نوع دارایی را با استفاده از URLهای نسبی مرجع قرار دهید:
```md
![تصویر](./image.png)

@ -189,7 +189,7 @@ Cache-Control: max-age=31536000,immutable
uses: actions/deploy-pages@v4
```
::: warning هشدار
::: warning هشدار
مطمئن شوید که گزینه `base` در ویت‌پرس به‌درستی پیکربندی شده است. برای اطلاعات بیشتر به [تنظیم مسیر پایه عمومی](#setting-a-public-base-path) مراجعه کنید.
:::

@ -119,7 +119,7 @@ export default {
} satisfies Theme
```
از آنجا که از Vite استفاده می‌کنیم، می‌توانید از ویژگی [import glob](https://vitejs.dev/guide/features.html#glob-import) در Vite برای خودکار ثبت یک پوشه از مولفه‌ها استفاده کنید.
از آنجا که از Vite استفاده می‌کنیم، می‌توانید از ویژگی [import glob](https://vite.dev/guide/features.html#glob-import) در Vite برای خودکار ثبت یک پوشه از مولفه‌ها استفاده کنید.
## slot ‌های طرح {#layout-slots}
@ -311,7 +311,7 @@ provide('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => {
## جایگزینی کامپوننت‌های داخلی {#overriding-internal-components}
شما می‌توانید با استفاده از [alias های Vite](https://vitejs.dev/config/shared-options.html#resolve-alias)، کامپوننت‌های تم پیش‌فرض را با کامپوننت‌های سفارشی خود جایگزین کنید:
شما می‌توانید با استفاده از [alias های Vite](https://vite.dev/config/shared-options.html#resolve-alias)، کامپوننت‌های تم پیش‌فرض را با کامپوننت‌های سفارشی خود جایگزین کنید:
```ts
import { fileURLToPath, URL } from 'node:url'

@ -12,7 +12,7 @@ description: نصب و راه‌اندازی ویت‌پرس. یاد بگیری
### پیش‌نیازها {#prerequisites}
- [Node.js](https://nodejs.org/) نسخه 20 یا بالاتر.
- [Node.js](https://nodejs.org/) نسخه 22 یا بالاتر.
- ترمینال برای دسترسی به ویت‌پرس از طریق رابط خط فرمان (CLI).
- ویرایشگر متنی با پشتیبانی از [Markdown](https://en.wikipedia.org/wiki/Markdown).
- [VSCode](https://code.visualstudio.com/) به همراه [افزونه رسمی Vue](https://marketplace.visualstudio.com/items?itemName=Vue.volar).
@ -41,7 +41,7 @@ $ bun add -D vitepress@next
::: tip نکته
ویت‌پرس یک بسته فقط ESM است. از `require()` برای وارد کردن آن استفاده نکنید و اطمینان حاصل کنید که نزدیک‌ترین `package.json` شما شامل `"type": "module"` است، یا پسوند فایل‌های مربوطه خود مانند `.vitepress/config.js` را به `.mjs`/`.mts` تغییر دهید. برای جزئیات بیشتر به [راهنمای عیب‌یابی Vite](http://vitejs.dev/guide/troubleshooting.html#this-package-is-esm-only) مراجعه کنید. همچنین، در زمینه‌های async CJS می‌توانید از `await import('vitepress')` استفاده کنید.
ویت‌پرس یک بسته فقط ESM است. از `require()` برای وارد کردن آن استفاده نکنید و اطمینان حاصل کنید که نزدیک‌ترین `package.json` شما شامل `"type": "module"` است، یا پسوند فایل‌های مربوطه خود مانند `.vitepress/config.js` را به `.mjs`/`.mts` تغییر دهید. برای جزئیات بیشتر به [راهنمای عیب‌یابی Vite](http://vite.dev/guide/troubleshooting.html#this-package-is-esm-only) مراجعه کنید. همچنین، در زمینه‌های async CJS می‌توانید از `await import('vitepress')` استفاده کنید.
:::

@ -1,5 +1,6 @@
---
description: افزونه‌های داخلی Markdown در ویت‌پرس شامل کانتینرهای سفارشی، بلوک‌های کد با هایلایت، گروه‌های کد و موارد دیگر.
outline: deep
---
# افزونه‌های Markdown {#markdown-extensions}
@ -112,7 +113,7 @@ lang: fa-IR
:tada: :100:
یک [لیست از همه اموجی ها](https://github.com/markdown-it/markdown-it-emoji/blob/master/lib/data/full.mjs) در دسترس است.
یک [لیست از همه اموجی ها](https://github.com/mdit-plugins/mdit-plugins/blob/main/packages/plugin-emoji/src/data/full.ts) در دسترس است.
## فهرست مطالب {#table-of-contents}
@ -880,14 +881,14 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$
## بارگذاری lazy تصویر {#image-lazy-loading}
می‌توانید بارگذاری تنبلی را برای هر تصویر اضافه شده از طریق Markdown با تنظیم `lazyLoading` به `true` در فایل پیکربندی فعال کنید:
می‌توانید بارگذاری تنبلی را برای هر تصویر اضافه شده از طریق Markdown با تنظیم `lazyLoad` به `true` در فایل پیکربندی فعال کنید:
```js
export default {
markdown: {
image: {
// بارگذاری تنبلی تصویر به طور پیش‌فرض غیرفعال است
lazyLoading: true
lazyLoad: true
}
}
}
@ -899,15 +900,15 @@ export default {
```js
import { defineConfig } from 'vitepress'
import markdownItAnchor from 'markdown-it-anchor'
import { headerLink } from '@mdit/plugin-anchor'
import markdownItFoo from 'markdown-it-foo'
export default defineConfig({
markdown: {
// گزینه‌های markdown-it-anchor
// https://github.com/valeriangalliat/markdown-it-anchor#usage
// گزینه‌های @mdit/plugin-anchor
// https://mdit-plugins.github.io/anchor.html
anchor: {
permalink: markdownItAnchor.permalink.headerLink()
permalink: headerLink()
},
// گزینه‌های @mdit-vue/plugin-toc

@ -14,7 +14,7 @@
- `repo`، `repoLabel`، `docsDir`، `docsBranch`، `editLinks`، `editLinkText` به منظور API انعطاف‌پذیرتر حذف شده‌اند.
- برای اضافه کردن لینک GitHub با آیکون به نوار ناوبری، از ویژگی [پیوندهای اجتماعی](../reference/default-theme-nav#navigation-links) استفاده کنید.
- برای اضافه کردن ویژگی "ویرایش این صفحه"، از ویژگی [پیوند ویرایش](../reference/default-theme-edit-link) استفاده کنید.
- گزینه `lastUpdated` حالا به `config.lastUpdated` و `themeConfig.lastUpdatedText` تقسیم شده است.
- گزینه `lastUpdated` حالا به `config.lastUpdated` و `themeConfig.lastUpdated.text` تقسیم شده است.
- `carbonAds.carbon` به `carbonAds.code` تغییر کرده است.
## پیکربندی Frontmatter

@ -295,7 +295,7 @@ export default {
تولید مسیرها از فایل‌های محلی:
```js
import fs from 'fs'
import fs from 'node:fs'
export default {
paths() {

@ -39,7 +39,7 @@ onMounted(() => {
### وارد کردن شرطی {#conditional-import}
می‌توانید همچنین وابستگی را با استفاده از `import.meta.env.SSR` (قسمتی از [متغیرهای env Vite](https://vitejs.dev/guide/env-and-mode.html#env-variables)) به شرط وارد کنید:
می‌توانید همچنین وابستگی را با استفاده از `import.meta.env.SSR` (قسمتی از [متغیرهای env Vite](https://vite.dev/guide/env-and-mode.html#env-variables)) به شرط وارد کنید:
```js
if (!import.meta.env.SSR) {

@ -205,7 +205,7 @@ Hello {{ 1 + 1 }}
## استفاده از پیش‌پردازنده‌های CSS {#using-css-pre-processors}
ویت‌پرس از [پشتیبانی داخلی](https://vitejs.dev/guide/features.html#css-pre-processors) برای پیش‌پردازنده‌های CSS مانند فایل‌های `.scss`، `.sass`، `.less`، `.styl` و `.stylus` پشتیبانی می‌کند. برای استفاده از آنها نیازی به نصب پلاگین‌های خاص Vite نیست، اما خود پیش‌پردازنده مربوطه باید نصب شده باشد:
ویت‌پرس از [پشتیبانی داخلی](https://vite.dev/guide/features.html#css-pre-processors) برای پیش‌پردازنده‌های CSS مانند فایل‌های `.scss`، `.sass`، `.less`، `.styl` و `.stylus` پشتیبانی می‌کند. برای استفاده از آنها نیازی به نصب پلاگین‌های خاص Vite نیست، اما خود پیش‌پردازنده مربوطه باید نصب شده باشد:
```
# .scss و .sass

@ -6,17 +6,15 @@ description: ویت‌پرس یک تولیدکننده سایت ایستا بر
ویت‌پرس یک [تولید کننده سایت ایستا](https://en.wikipedia.org/wiki/Static_site_generator) (SSG) است که برای ساخت وب‌سایت‌های سریع و محتوا محور طراحی شده است. به طور خلاصه، ویت‌پرس محتوای منبع شما که به زبان [Markdown](https://en.wikipedia.org/wiki/Markdown) نوشته شده است را گرفته، یک تم بر روی آن اعمال می‌کند و صفحات HTML ایستا تولید می‌کند که به راحتی در هر جایی قابل استقرار هستند.
<div class="tip custom-block" style="padding-top: 8px">
::: tip {no-title}
فقط می‌خواهید آن را امتحان کنید؟ به [شروع سریع](./getting-started) بروید.
</div>
:::
## موارد استفاده {#use-cases}
- **مستندسازی**
ویت‌پرس با یک تم پیش‌فرض طراحی شده برای مستندات فنی ارائه می‌شود. این صفحه‌ای که اکنون در حال خواندن آن هستید و همچنین مستندات [Vite](https://vitejs.dev/)، [Rollup](https://rollupjs.org/)، [Pinia](https://pinia.vuejs.org/)، [VueUse](https://vueuse.org/)، [Vitest](https://vitest.dev/)، [D3](https://d3js.org/)، [UnoCSS](https://unocss.dev/)، [Iconify](https://iconify.design/) و [بسیاری دیگر](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code) با استفاده از ویت‌پرس ساخته شده‌اند.
ویت‌پرس با یک تم پیش‌فرض طراحی شده برای مستندات فنی ارائه می‌شود. این صفحه‌ای که اکنون در حال خواندن آن هستید و همچنین مستندات [Vite](https://vite.dev/)، [Rollup](https://rollupjs.org/)، [Pinia](https://pinia.vuejs.org/)، [VueUse](https://vueuse.org/)، [Vitest](https://vitest.dev/)، [D3](https://d3js.org/)، [UnoCSS](https://unocss.dev/)، [Iconify](https://iconify.design/) و [بسیاری دیگر](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code) با استفاده از ویت‌پرس ساخته شده‌اند.
[مستندات رسمی Vue.js](https://vuejs.org/) نیز بر پایه ویت‌پرس ساخته شده است، اما از یک تم سفارشی که بین چندین ترجمه مشترک است استفاده می‌کند.
@ -30,7 +28,7 @@ description: ویت‌پرس یک تولیدکننده سایت ایستا بر
ویت‌پرس هدف ارائه یک تجربه عالی برای توسعه دهنده (DX) هنگام کار با محتوای Markdown را دارد.
- **[قدرت گرفته از Vite:](https://vitejs.dev/)** شروع سرور فوری، با بازتاب ویرایش‌ها به صورت آنی (<100ms) بدون بارگذاری مجدد صفحه.
- **[قدرت گرفته از Vite:](https://vite.dev/)** شروع سرور فوری، با بازتاب ویرایش‌ها به صورت آنی (<100ms) بدون بارگذاری مجدد صفحه.
- **[افزونه‌های داخلی Markdown:](./markdown)** استفاده از Frontmatter، جداول، syntax highlighting... هرچه که بخواهید. ویت‌پرس به ویژه ویژگی‌های پیشرفته زیادی برای کار با بلوک‌های کد فراهم می‌کند، که آن را برای مستندات فنی بسیار مناسب می‌کند.

@ -21,16 +21,16 @@ hero:
alt: ویت‌پرس
features:
- icon: 📝
- icon: <span class="memo"></span>
title: تمرکز روی محتوا
details: ایجاد سایت‌های مستند‌سازی زیبا بدون زحمت و فقط با Markdown
- icon: <svg xmlns="http://www.w3.org/2000/svg" width="30" viewBox="0 0 256 256.32"><defs><linearGradient id="a" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"/><stop offset="100%" stop-color="#BD34FE"/></linearGradient><linearGradient id="b" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"/><stop offset="8.333%" stop-color="#FFDD35"/><stop offset="100%" stop-color="#FFA800"/></linearGradient></defs><path fill="url(#a)" d="M255.153 37.938 134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"/><path fill="url(#b)" d="M185.432.063 96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028 72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"/></svg>
- icon: <span class="vite"></span>
title: لذت از تجربه توسعه با Vite
details: شروع فوری سرور، به‌روزرسانی‌های سریع و استفاده از افزونه‌های اکوسیستم Vite
- icon: <svg xmlns="http://www.w3.org/2000/svg" width="30" viewBox="0 0 256 220.8"><path fill="#41B883" d="M204.8 0H256L128 220.8 0 0h97.92L128 51.2 157.44 0h47.36Z"/><path fill="#41B883" d="m0 0 128 220.8L256 0h-51.2L128 132.48 50.56 0H0Z"/><path fill="#35495E" d="M50.56 0 128 133.12 204.8 0h-47.36L128 51.2 97.92 0H50.56Z"/></svg>
- icon: <span class="vue"></span>
title: شخصی‌سازی با Vue
details: استفاده مستقیم از syntax و کامپوننت‌های Vue در Markdown، یا ایجاد تم‌های شخصی به کمک Vue
- icon: 🚀
- icon: <span class="rocket"></span>
title: ارسال سایت های سریع
details: بارگذاری اولیه سریع با HTML ایستا، ناوبری سریع پس از بارگیری با مسیریابی سمت کلاینت
---

@ -11,7 +11,8 @@ export default {
themeConfig: {
carbonAds: {
code: 'your-carbon-code',
placement: 'your-carbon-placement'
placement: 'your-carbon-placement',
format: 'classic'
}
}
}
@ -19,8 +20,10 @@ export default {
این مقادیر برای فراخوانی اسکریپت CDN Carbon به شکل زیر استفاده می‌شوند.
گزینه `format` از مقادیر `classic`، `responsive` و `cover` پشتیبانی می‌کند.
```js
`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}`
`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}&format=${format}`
```
برای یادگیری بیشتر درباره پیکربندی تبلیغات Carbon، لطفاً به [وب‌سایت Carbon Ads](https://www.carbonads.net/) مراجعه کنید.

@ -382,16 +382,20 @@ export default {
themeConfig: {
carbonAds: {
code: 'your-carbon-code',
placement: 'your-carbon-placement'
placement: 'your-carbon-placement',
format: 'classic'
}
}
}
```
گزینه `format` از مقادیر `classic`، `responsive` و `cover` پشتیبانی می‌کند.
```ts
export interface CarbonAdsOptions {
code: string
placement: string
format?: 'classic' | 'responsive' | 'cover'
}
```

@ -432,7 +432,7 @@ export default {
- نوع: `string`
- پیش‌فرض: `./.vitepress/cache`
دایرکتوری برای فایل‌های کش، نسبت به [ریشه پروژه](../guide/routing#root-and-source-directory). همچنین ببینید: [cacheDir](https://vitejs.dev/config/shared-options.html#cachedir).
دایرکتوری برای فایل‌های کش، نسبت به [ریشه پروژه](../guide/routing#root-and-source-directory). همچنین ببینید: [cacheDir](https://vite.dev/config/shared-options.html#cachedir).
```ts
export default {
@ -474,13 +474,6 @@ export default {
}
```
### metaChunk <Badge type="warning" text="experimental" /> {#metachunk}
- نوع: `boolean`
- پیش‌فرض: `false`
زمانی که به `true` تنظیم شود، فراداده‌های صفحات را به یک قسمت جداگانه جاوااسکریپت استخراج می‌کند به جای درون‌گذاری آن در HTML اولیه. این کار باعث کاهش بار HTML هر صفحه می‌شود و فراداده‌های صفحات قابل کش شدن می‌شود، که منجر به کاهش پهنای باند سرور می‌شود وقتی که صفحات زیادی در سایت دارید.
### mpa <Badge type="warning" text="experimental" /> {#mpa}
- نوع: `boolean`
@ -514,7 +507,7 @@ export default {
آیا زمان آخرین به‌روزرسانی برای هر صفحه با استفاده از Git دریافت شود. این زمان در داده‌های هر صفحه گنجانده خواهد شد و از طریق [`useData`](./runtime-api#usedata) قابل دسترسی خواهد بود.
وقتی از تم پیش‌فرض استفاده می‌کنید، فعال کردن این گزینه زمان آخرین به‌روزرسانی هر صفحه را نمایش می‌دهد. می‌توانید متن را از طریق گزینه [`themeConfig.lastUpdatedText`](./default-theme-config#lastupdatedtext) سفارشی کنید.
وقتی از تم پیش‌فرض استفاده می‌کنید، فعال کردن این گزینه زمان آخرین به‌روزرسانی هر صفحه را نمایش می‌دهد. می‌توانید متن را از طریق گزینه [`themeConfig.lastUpdated.text`](./default-theme-config#lastupdated) سفارشی کنید.
## سفارشی‌سازی {#customization}
@ -536,7 +529,7 @@ export default {
- نوع: `import('vite').UserConfig`
پیکربندی خام [Vite Config](https://vitejs.dev/config/) را به سرور توسعه داخلی / بسته‌بند Vite ارسال کنید.
پیکربندی خام [Vite Config](https://vite.dev/config/) را به سرور توسعه داخلی / بسته‌بند Vite ارسال کنید.
```js
export default {

@ -1,9 +1,30 @@
import { createRequire } from 'module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress'
import { createRequire } from 'node:module'
import {
defineAdditionalConfig,
type DefaultTheme,
type MarkdownLocaleOptions
} from 'vitepress'
const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
export const markdown: MarkdownLocaleOptions = {
container: {
tipLabel: 'ヒント',
infoLabel: '情報',
warningLabel: '警告',
dangerLabel: '危険',
detailsLabel: '詳細',
noteLabel: 'メモ',
importantLabel: '重要',
cautionLabel: '注意'
},
codeCopyButton: {
tooltipText: 'コードをコピー',
copiedText: 'コピー完了'
}
}
export default defineAdditionalConfig({
description: 'Vite と Vue による静的サイトジェネレーター',
@ -231,8 +252,6 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
afterToolCallText: '検索しました',
stoppedStreamingText: 'この応答を停止しました',
errorTitleText: 'チャットエラー',
threadDepthExceededMessage:
'回答の正確性を保つため、この会話は終了しました。',
startNewConversationButtonText: '新しい会話を開始'
}
}

@ -6,7 +6,7 @@ description: VitePressで画像、メディア、フォントなどの静的ア
## 静的アセットの参照 {#referencing-static-assets}
すべての Markdown ファイルは Vue コンポーネントにコンパイルされ、[Vite](https://vitejs.dev/guide/assets.html) によって処理されます。Markdown 内では、相対 URL を使ってアセットを参照することが **推奨されます**。
すべての Markdown ファイルは Vue コンポーネントにコンパイルされ、[Vite](https://vite.dev/guide/assets.html) によって処理されます。Markdown 内では、相対 URL を使ってアセットを参照することが **推奨されます**。
```md
![画像](./image.png)

@ -122,7 +122,7 @@ export default {
} satisfies Theme
```
Vite を使っているため、Vite の [glob import 機能](https://vitejs.dev/guide/features.html#glob-import) を利用してディレクトリ内のコンポーネントを自動登録することもできます。
Vite を使っているため、Vite の [glob import 機能](https://vite.dev/guide/features.html#glob-import) を利用してディレクトリ内のコンポーネントを自動登録することもできます。
## レイアウトスロット {#layout-slots}
@ -311,7 +311,7 @@ provide('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => {
## 内部コンポーネントの置き換え {#overriding-internal-components}
Vite の [エイリアス](https://vitejs.dev/config/shared-options.html#resolve-alias) を使って、デフォルトテーマのコンポーネントを独自のものに置き換えられます。
Vite の [エイリアス](https://vite.dev/config/shared-options.html#resolve-alias) を使って、デフォルトテーマのコンポーネントを独自のものに置き換えられます。
```ts
import { fileURLToPath, URL } from 'node:url'

@ -12,7 +12,7 @@ description: VitePressのインストール、セットアップ、開発サー
### 前提条件 {#prerequisites}
- [Node.js](https://nodejs.org/) バージョン 20 以上
- [Node.js](https://nodejs.org/) バージョン 22 以上
- VitePress をコマンドラインインターフェース (CLI) で操作するためのターミナル
- [Markdown](https://en.wikipedia.org/wiki/Markdown) 構文に対応したテキストエディタ
- 推奨: [VSCode](https://code.visualstudio.com/) と [公式 Vue 拡張](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
@ -40,7 +40,7 @@ $ bun add -D vitepress@next
:::
::: tip 注意
VitePress は ESM 専用パッケージです。`require()` を使ってインポートせず、最も近い `package.json``"type": "module"` を含めるか、`.vitepress/config.js` を `.mjs` / `.mts` に変更してください。詳しくは [Vite のトラブルシューティングガイド](http://vitejs.dev/guide/troubleshooting.html#this-package-is-esm-only) を参照してください。また、非同期 CJS コンテキスト内では `await import('vitepress')` を使用できます。
VitePress は ESM 専用パッケージです。`require()` を使ってインポートせず、最も近い `package.json``"type": "module"` を含めるか、`.vitepress/config.js` を `.mjs` / `.mts` に変更してください。詳しくは [Vite のトラブルシューティングガイド](http://vite.dev/guide/troubleshooting.html#this-package-is-esm-only) を参照してください。また、非同期 CJS コンテキスト内では `await import('vitepress')` を使用できます。
:::
### セットアップウィザード {#setup-wizard}

@ -1,5 +1,6 @@
---
description: VitePress組み込みのMarkdown拡張機能。カスタムコンテナ、コードブロックのシンタックスハイライト、行ハイライト、コードグループなど。
outline: deep
---
# Markdown 拡張 {#markdown-extensions}
@ -110,7 +111,7 @@ lang: ja-JP
:tada: :100:
すべての絵文字の [一覧はこちら](https://github.com/markdown-it/markdown-it-emoji/blob/master/lib/data/full.mjs)。
すべての絵文字の [一覧はこちら](https://github.com/mdit-plugins/mdit-plugins/blob/main/packages/plugin-emoji/src/data/full.ts)。
## 目次 {#table-of-contents}
@ -229,7 +230,7 @@ export default defineConfig({
### 追加属性 {#additional-attributes}
カスタムコンテナには追加の属性を付与できます。この機能には [markdown-it-attrs](https://github.com/arve0/markdown-it-attrs) を使用しており、ほぼすべての Markdown 要素でサポートされます。たとえば `open` 属性を付けると、details ブロックをデフォルトで開いた状態にできます。
カスタムコンテナには追加の属性を付与できます。この機能には [@mdit/plugin-attrs](https://mdit-plugins.github.io/attrs.html) を使用しており、ほぼすべての Markdown 要素でサポートされます。たとえば `open` 属性を付けると、details ブロックをデフォルトで開いた状態にできます。
**入力**
@ -251,7 +252,7 @@ console.log('こんにちは、VitePress!')
### `raw`
これは、VitePress でのスタイルやルーターの衝突を防ぐための特別なコンテナです。コンポーネントライブラリのドキュメント化に特に有用です。より強力な分離が必要であれば、[whyframe](https://whyframe.dev/docs/integrations/vitepress) も検討してください。
これは、VitePress でのスタイルやルーターの衝突を防ぐための特別なコンテナです。コンポーネントライブラリのドキュメント化に特に有用です。
**構文**
@ -996,14 +997,14 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$
## 画像の遅延読み込み {#image-lazy-loading}
Markdown で追加した各画像に対して遅延読み込みを有効化するには、設定ファイルで `lazyLoading` を `true` にします:
Markdown で追加した各画像に対して遅延読み込みを有効化するには、設定ファイルで `lazyLoad` を `true` にします:
```js
export default {
markdown: {
image: {
// 既定では画像の遅延読み込みは無効
lazyLoading: true
lazyLoad: true
}
}
}
@ -1015,15 +1016,15 @@ VitePress は Markdown レンダラーとして [markdown-it](https://github.com
```js
import { defineConfig } from 'vitepress'
import markdownItAnchor from 'markdown-it-anchor'
import { headerLink } from '@mdit/plugin-anchor'
import markdownItFoo from 'markdown-it-foo'
export default defineConfig({
markdown: {
// markdown-it-anchor のオプション
// https://github.com/valeriangalliat/markdown-it-anchor#usage
// @mdit/plugin-anchor のオプション
// https://mdit-plugins.github.io/anchor.html
anchor: {
permalink: markdownItAnchor.permalink.headerLink()
permalink: headerLink()
},
// @mdit-vue/plugin-toc のオプション

@ -304,7 +304,7 @@ export default {
ローカルファイルから生成する例:
```js
import fs from 'fs'
import fs from 'node:fs'
export default {
paths() {

@ -39,7 +39,7 @@ onMounted(() => {
### 条件付きインポート {#conditional-import}
[`import.meta.env.SSR`](https://vitejs.dev/guide/env-and-mode.html#env-variables) フラグVite の環境変数の一部)を使って、依存関係を条件付きでインポートすることもできます。
[`import.meta.env.SSR`](https://vite.dev/guide/env-and-mode.html#env-variables) フラグVite の環境変数の一部)を使って、依存関係を条件付きでインポートすることもできます。
```js
if (!import.meta.env.SSR) {

@ -203,7 +203,7 @@ Hello {{ 1 + 1 }}
## CSS プリプロセッサの利用 {#using-css-pre-processors}
VitePress は CSS プリプロセッサ(`.scss`、`.sass`、`.less`、`.styl`、`.stylus`)を[標準サポート](https://vitejs.dev/guide/features.html#css-pre-processors)しています。Vite 固有のプラグインは不要ですが、各プリプロセッサ本体のインストールは必要です。
VitePress は CSS プリプロセッサ(`.scss`、`.sass`、`.less`、`.styl`、`.stylus`)を[標準サポート](https://vite.dev/guide/features.html#css-pre-processors)しています。Vite 固有のプラグインは不要ですが、各プリプロセッサ本体のインストールは必要です。
```
# .scss / .sass

@ -6,17 +6,15 @@ description: VitePressはViteとVueを基盤とした、高速でコンテンツ
VitePress は、高速でコンテンツ中心の Web サイトを構築するための [静的サイトジェネレーターSSG](https://en.wikipedia.org/wiki/Static_site_generator) です。要するに、VitePress は [Markdown](https://en.wikipedia.org/wiki/Markdown) で書かれたソースコンテンツにテーマを適用し、どこにでも簡単にデプロイできる静的 HTML ページを生成します。
<div class="tip custom-block" style="padding-top: 8px">
::: tip {no-title}
まずは試してみたい? [クイックスタート](./getting-started) へどうぞ。
</div>
:::
## ユースケース {#use-cases}
- **ドキュメント**
VitePress には技術ドキュメント向けに設計されたデフォルトテーマが同梱されています。今あなたが読んでいるこのページのほか、[Vite](https://vitejs.dev/)、[Rollup](https://rollupjs.org/)、[Pinia](https://pinia.vuejs.org/)、[VueUse](https://vueuse.org/)、[Vitest](https://vitest.dev/)、[D3](https://d3js.org/)、[UnoCSS](https://unocss.dev/)、[Iconify](https://iconify.design/) など、[まだまだたくさん](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code)のドキュメントサイトで使われています。
VitePress には技術ドキュメント向けに設計されたデフォルトテーマが同梱されています。今あなたが読んでいるこのページのほか、[Vite](https://vite.dev/)、[Rollup](https://rollupjs.org/)、[Pinia](https://pinia.vuejs.org/)、[VueUse](https://vueuse.org/)、[Vitest](https://vitest.dev/)、[D3](https://d3js.org/)、[UnoCSS](https://unocss.dev/)、[Iconify](https://iconify.design/) など、[まだまだたくさん](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code)のドキュメントサイトで使われています。
[公式の Vue.js ドキュメント](https://vuejs.org/) も VitePress をベースにしています(複数言語で共有されるカスタムテーマを使用)。
@ -30,7 +28,7 @@ VitePress は、高速でコンテンツ中心の Web サイトを構築する
VitePress は、Markdown コンテンツを扱う際の優れた開発体験DXを目指しています。
- **[Vite 駆動](https://vitejs.dev/)**:即時サーバー起動、編集はページリロードなしで常に瞬時(<100ms)に反映。
- **[Vite 駆動](https://vite.dev/)**:即時サーバー起動、編集はページリロードなしで常に瞬時(<100ms)に反映。
- **[ビルトインの Markdown 拡張](./markdown)**Frontmatter、表、シンタックスハイライト…必要なものはひと通り。特にコードブロック周りの機能が充実しており、高度な技術ドキュメントに最適です。

@ -21,16 +21,16 @@ hero:
alt: VitePress
features:
- icon: 📝
- icon: <span class="memo"></span>
title: コンテンツに集中
details: Markdown だけで、美しいドキュメントサイトを簡単に作成できます。
- icon: <svg xmlns="http://www.w3.org/2000/svg" width="30" viewBox="0 0 256 256.32"><defs><linearGradient id="a" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"/><stop offset="100%" stop-color="#BD34FE"/></linearGradient><linearGradient id="b" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"/><stop offset="8.333%" stop-color="#FFDD35"/><stop offset="100%" stop-color="#FFA800"/></linearGradient></defs><path fill="url(#a)" d="M255.153 37.938 134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"/><path fill="url(#b)" d="M185.432.063 96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028 72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"/></svg>
- icon: <span class="vite"></span>
title: Vite の開発体験を享受
details: 即時サーバー起動、超高速ホットリロード、そして Vite エコシステムのプラグイン活用。
- icon: <svg xmlns="http://www.w3.org/2000/svg" width="30" viewBox="0 0 256 220.8"><path fill="#41B883" d="M204.8 0H256L128 220.8 0 0h97.92L128 51.2 157.44 0h47.36Z"/><path fill="#41B883" d="m0 0 128 220.8L256 0h-51.2L128 132.48 50.56 0H0Z"/><path fill="#35495E" d="M50.56 0 128 133.12 204.8 0h-47.36L128 51.2 97.92 0H50.56Z"/></svg>
- icon: <span class="vue"></span>
title: Vue でカスタマイズ
details: Markdown 内で直接 Vue 構文やコンポーネントを利用したり、Vue で独自テーマを構築できます。
- icon: 🚀
- icon: <span class="rocket"></span>
title: 高速サイトを公開
details: 静的 HTML による高速初期ロードと、クライアントサイドルーティングによる快適なページ遷移。
---

@ -11,7 +11,8 @@ export default {
themeConfig: {
carbonAds: {
code: 'your-carbon-code',
placement: 'your-carbon-placement'
placement: 'your-carbon-placement',
format: 'classic'
}
}
}
@ -19,8 +20,10 @@ export default {
これらの値は、次のように Carbon の CDN スクリプトを呼び出すために使用されます。
`format` オプションには `classic`、`responsive`、`cover` を指定できます。
```js
`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}`
`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}&format=${format}`
```
Carbon Ads の設定について詳しくは、[Carbon Ads のウェブサイト](https://www.carbonads.net/)を参照してください。

@ -367,16 +367,20 @@ export default {
themeConfig: {
carbonAds: {
code: 'your-carbon-code',
placement: 'your-carbon-placement'
placement: 'your-carbon-placement',
format: 'classic'
}
}
}
```
`format` オプションには `classic`、`responsive`、`cover` を指定できます。
```ts
export interface CarbonAdsOptions {
code: string
placement: string
format?: 'classic' | 'responsive' | 'cover'
}
```

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save