Merge branch 'main' into set-scrollbar-gutter

set-scrollbar-gutter
bluwy 1 month ago
commit ff0e9834a5
No known key found for this signature in database

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

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

@ -27,13 +27,13 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v7
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v6
- name: Set node version to ${{ matrix.node_version }} - name: Set node version to ${{ matrix.node_version }}
uses: actions/setup-node@v6 uses: actions/setup-node@v7
with: with:
node-version: ${{ matrix.node_version }} node-version: ${{ matrix.node_version }}
cache: pnpm cache: pnpm

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

@ -1,3 +1,117 @@
## [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) ## [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 ### Bug Fixes
@ -140,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. - `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. - 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) ## [2.0.0-alpha.12](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.11...v2.0.0-alpha.12) (2025-08-20)
@ -819,7 +934,7 @@ Users who intentionally reference non-existent files or want to document include
### BREAKING CHANGES ### 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) # [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. 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://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://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://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://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,9 +155,7 @@ export default defineConfig({
title: 'Example', title: 'Example',
description: 'An example app using VitePress.', description: 'An example app using VitePress.',
markdown: { markdown: {
image: { image: { lazyLoad: true }
lazyLoading: true
}
}, },
themeConfig: { themeConfig: {
nav, nav,

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

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

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

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

@ -39,7 +39,7 @@ afterAll(async () => {
test.each(variations)('init %s', async (_, { theme, useTs }) => { test.each(variations)('init %s', async (_, { theme, useTs }) => {
const root = getTempRoot() const root = getTempRoot()
await rm(root, { recursive: true, force: true }) await rm(root, { recursive: true, force: true })
scaffold({ root, theme, useTs, injectNpmScripts: false }) await scaffold({ root, theme, useTs, injectNpmScripts: false })
const port = await getPort() const port = await getPort()
const server = await createServer(root, { port }) const server = await createServer(root, { port })

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

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

@ -1,15 +1,36 @@
import { resolveLocaleLink } from 'client/theme-default/composables/langs' import { resolveLocaleLink } from 'client/theme-default/composables/langs'
import type { VitePressData } from 'vitepress' import type { Route, VitePressData } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { ref } from 'vue' import { ref } from 'vue'
function createData( // `currentPage` is the current page's relative path (like
themeConfig: DefaultTheme.Config, // `route.data.relativePath`, but with a leading slash), plus any query and
relativePath = 'guide/getting-started.md', // hash of the current URL.
function resolve(
currentPage: string,
{
themeConfig = {},
cleanUrls = false, cleanUrls = false,
hash = '#install' targetLocale = 'fr',
targetLocaleLink = '/fr/',
currentLocaleLink = '/',
linkToCorrespondingPage = true
}: {
themeConfig?: DefaultTheme.Config
cleanUrls?: boolean
targetLocale?: string
targetLocaleLink?: string
currentLocaleLink?: string
linkToCorrespondingPage?: boolean
} = {}
) { ) {
return { 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({ site: ref({
cleanUrls, cleanUrls,
locales: { locales: {
@ -18,52 +39,108 @@ function createData(
}, },
themeConfig themeConfig
}), }),
page: ref({ relativePath }), theme: ref(themeConfig)
theme: ref(themeConfig),
hash: ref(hash)
} as unknown as VitePressData<DefaultTheme.Config> } as unknown as VitePressData<DefaultTheme.Config>
return resolveLocaleLink(data, route, {
targetLocale,
targetLocaleLink,
currentLocaleLink,
linkToCorrespondingPage
})
} }
describe('client/theme-default/composables/langs', () => { describe('client/theme-default/composables/langs', () => {
test('resolves corresponding links with the default router', () => { describe('resolveLocaleLink', () => {
expect(resolveLocaleLink(createData({}), 'fr', '/fr/', '/', true)).toBe( 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' '/fr/guide/getting-started.html#install'
) )
}) })
test('resolves clean index links with the default router', () => { test('drops the .html extension when clean URLs are enabled', () => {
expect( expect(
resolveLocaleLink( resolve('/guide/getting-started.md#install', { cleanUrls: true })
createData({}, 'en/guide/index.md', true, '#intro'), ).toBe('/fr/guide/getting-started#install')
'fr', })
'/fr/',
'/en/', test('resolves index pages to directory links', () => {
true expect(resolve('/guide/index.md')).toBe('/fr/guide/')
expect(resolve('/guide/index.md', { cleanUrls: true })).toBe(
'/fr/guide/'
) )
).toBe('/fr/guide/#intro')
}) })
test('keeps locale root links when i18n routing is disabled', () => { 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( expect(
resolveLocaleLink( resolve('/en/guide/index.md?query#intro', {
createData({ i18nRouting: false }), currentLocaleLink: '/en/',
'fr', cleanUrls: true
'/fr/', })
'/', ).toBe('/fr/guide/?query#intro')
true })
)
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') ).toBe('/fr/#install')
}) })
test('uses custom i18n routing functions for corresponding links', () => { test('delegates to custom i18n routing functions', () => {
const data = createData({ expect(
i18nRouting(data, hash, targetLocale) { resolve('/guide/getting-started.md#install', {
return `${data.site.value.locales[targetLocale].link}mapped/${data.page.value.relativePath}${hash}` 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')
expect(resolveLocaleLink(data, 'fr', '/fr/', '/', true)).toBe( })
'/fr/mapped/guide/getting-started.md#install' })
)
}) })
}) })

@ -188,8 +188,8 @@ describe('client/theme-default/support/sidebar', () => {
] ]
} }
expect(hasActiveLink('active-1', item)).toBe(true) expect(hasActiveLink('active-1', '', item)).toBe(true)
expect(hasActiveLink('inactive', item)).toBe(false) expect(hasActiveLink('inactive', '', item)).toBe(false)
}) })
test('checks `SidebarItem[]`', () => { test('checks `SidebarItem[]`', () => {
@ -210,9 +210,9 @@ describe('client/theme-default/support/sidebar', () => {
} }
] ]
expect(hasActiveLink('active-1', item)).toBe(true) expect(hasActiveLink('active-1', '', item)).toBe(true)
expect(hasActiveLink('active-3', item)).toBe(true) expect(hasActiveLink('active-3', '', item)).toBe(true)
expect(hasActiveLink('inactive', item)).toBe(false) expect(hasActiveLink('inactive', '', item)).toBe(false)
}) })
}) })
}) })

@ -1,15 +1,18 @@
import type { MarkdownItAsync } from 'markdown-it-async' import type { MarkdownItAsync } from 'markdown-it-async'
import { mergeConfig } from 'node/config' import { mergeConfig, type UserConfig } from 'node/config'
describe('node/config', () => { describe('node/config', () => {
test('merges markdown config hooks from extended configs', async () => { test('merges markdown hooks from extended configs', async () => {
const calls: string[] = [] const calls: string[] = []
const md = {} as MarkdownItAsync const md = {} as MarkdownItAsync
const merged = mergeConfig( const merged = mergeConfig<UserConfig, UserConfig>(
{ {
markdown: { markdown: {
lineNumbers: true, lineNumbers: true,
preConfig() {
calls.push('base-pre')
},
config() { config() {
calls.push('base') calls.push('base')
} }
@ -18,7 +21,10 @@ describe('node/config', () => {
{ {
markdown: { markdown: {
attrs: { attrs: {
allowedAttributes: ['id'] allowed: ['id']
},
async preConfig() {
calls.push('extended-pre')
}, },
async config() { async config() {
calls.push('extended') calls.push('extended')
@ -29,11 +35,39 @@ describe('node/config', () => {
expect(merged.markdown?.lineNumbers).toBe(true) expect(merged.markdown?.lineNumbers).toBe(true)
expect(merged.markdown?.attrs).toEqual({ expect(merged.markdown?.attrs).toEqual({
allowedAttributes: ['id'] 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) await merged.markdown?.config?.(md)
expect(calls).toEqual(['base', 'extended']) 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"')
})
})

@ -1,8 +1,13 @@
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { import {
dedent, createMarkdownRenderer,
findRegion, disposeMdItInstance,
rawPathToToken type MarkdownOptions
} from 'node/markdown/plugins/snippet' } 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) => { const removeEmptyKeys = <T extends Record<string, unknown>>(obj: T) => {
return Object.fromEntries( return Object.fromEntries(
@ -11,7 +16,8 @@ const removeEmptyKeys = <T extends Record<string, unknown>>(obj: T) => {
} }
/* prettier-ignore */ /* 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' }], ['./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', { 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.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}', { 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#' }], ['/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 [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.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}', { 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#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 {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('node/markdown/plugins/snippet', () => {
describe('dedent', () => { describe('parseSnippetPath', () => {
test('when 0-level is minimal, do not remove spaces', () => { test.each(parseSnippetPathMap)('%s', (rawPath, parsed) => {
expect( expect(removeEmptyKeys(parseSnippetPath(rawPath))).toEqual(parsed)
dedent( })
[
//
'fn main() {',
' println!("Hello");',
'}'
].join('\n')
)
).toMatchInlineSnapshot(`
"fn main() {
println!("Hello");
}"
`)
}) })
test('when 4-level is minimal, remove 4 spaces', () => { describe('rendering', () => {
expect( let root: string
dedent( let warnings: string[]
[
// const logger = {
' let a = {', warn: (msg: string) => {
' value: 42', warnings.push(msg)
' };' }
].join('\n') }
)
).toMatchInlineSnapshot(` beforeEach(async () => {
"let a = { root = await mkdtemp(path.join(tmpdir(), 'vitepress-snippet-'))
value: 42 warnings = []
};"
`)
}) })
test('when only 1 line is passed, dedent it', () => { afterEach(async () => {
expect(dedent(' let a = 42;')).toEqual('let a = 42;') await rm(root, { recursive: true, force: true })
}) })
test('handle tabs as well', () => { async function write(name: string, src: string) {
expect( const file = path.join(root, name)
dedent( await mkdir(path.dirname(file), { recursive: true })
[ await writeFile(file, src)
// }
' let a = {',
' value: 42', async function render(
' };' src: string,
].join('\n') options: MarkdownOptions = {},
env: Partial<MarkdownEnv> = {}
) {
disposeMdItInstance()
const md = await createMarkdownRenderer(
root,
{ highlight: (code) => code, ...options },
'/',
logger
) )
).toMatchInlineSnapshot(` const fullEnv: MarkdownEnv = {
"let a = { path: path.join(root, 'index.md'),
value: 42 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('resolves @ against srcDir', async () => {
test.each(rawPathTokenMap)('%s', (rawPath, token) => { await write('nested/snip.js', 'const nested = 1\n')
expect(removeEmptyKeys(rawPathToToken(rawPath))).toEqual(token)
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', () => { test('parses a snippet without a space after the marker', async () => {
it('returns null when no region markers are present', () => { await write('snip.ts', 'const a = 1\n')
const lines = ['function foo() {', ' console.log("hello");', '}']
expect(findRegion(lines, 'foo')).toBeNull() const { html } = await render('<<<./snip.ts')
expect(html).toContain('const a = 1')
}) })
it('ignores non-matching region names', () => { test('does not dedent whole-file imports', async () => {
const lines = [ await write('indented.ts', ' const a = 1\n const b = 2\n')
'// #region regionA',
'some code here', const { html } = await render('<<< ./indented.ts')
'// #endregion regionA' expect(html).toContain(' const a = 1')
]
expect(findRegion(lines, 'regionC')).toBeNull()
}) })
it('returns null if a region start marker exists without a matching end marker', () => { test('passes attrs to the highlighter and keeps them out of the title', async () => {
const lines = [ await write('snip.ts', 'const a = 1\nconst b = 2\n')
'// #region missingEnd',
'console.log("inside region");', const calls: { lang: string; attrs: string }[] = []
'console.log("still inside");' await render('<<< ./snip.ts{1 ts twoslash} [my title]', {
] highlight: (code, lang, attrs) => {
expect(findRegion(lines, 'missingEnd')).toBeNull() calls.push({ lang, attrs })
return code
}
}) })
it('returns null if an end marker exists without a preceding start marker', () => { expect(calls).toHaveLength(1)
const lines = [ expect(calls[0].lang).toBe('ts')
'// #endregion ghostRegion', expect(calls[0].attrs).toContain('twoslash')
'console.log("stray end marker");' expect(calls[0].attrs).toContain('{1}')
] expect(calls[0].attrs).not.toContain('my title')
expect(findRegion(lines, 'ghostRegion')).toBeNull()
}) })
it('detects C#/JavaScript style region markers with matching tags', () => { test('resolves relative paths against the real file path', async () => {
const lines = [ await write('sub/snip.js', 'const real = 1\n')
'Console.WriteLine("Before region");',
'#region hello', const { html } = await render(
'Console.WriteLine("Hello, World!");', '<<< ./snip.js',
'#endregion hello', {},
'Console.WriteLine("After region");' {
] path: path.join(root, 'rewritten/index.md'),
const result = findRegion(lines, 'hello') realPath: path.join(root, 'sub/index.md')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'Console.WriteLine("Hello, World!");'
)
} }
)
expect(html).toContain('const real = 1')
}) })
it('detects region markers even when the end marker omits the region name', () => { test('concatenates all regions with the requested name', async () => {
const lines = [ await write(
'Console.WriteLine("Before region");', 'regions.ts',
'#region hello', [
'Console.WriteLine("Hello, World!");', '// #region one',
'#endregion', 'const a = 1',
'Console.WriteLine("After region");' '// #endregion one',
] 'const outside = 2',
const result = findRegion(lines, 'hello') '// #region one',
expect(result).not.toBeNull() 'const b = 3',
if (result) { '// #endregion',
expect(lines.slice(result.start, result.end).join('\n')).toBe( ''
'Console.WriteLine("Hello, World!");' ].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', () => { test('dedents extracted regions', async () => {
const lines = [ await write(
' Console.WriteLine("Before region");', 'indent.ts',
' #region hello', [
' Console.WriteLine("Hello, World!");', 'function f() {',
' #endregion hello', ' // #region inner',
' Console.WriteLine("After region");' ' const x = 1',
] ' // #endregion inner',
const result = findRegion(lines, 'hello') '}',
expect(result).not.toBeNull() ''
if (result) { ].join('\n')
expect(lines.slice(result.start, result.end).join('\n')).toBe(
' Console.WriteLine("Hello, World!");'
) )
}
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 nested = [
const lines = [ '// #region outer',
'let regexp: RegExp[] = [];', 'let a = 1',
'// #region foo', '// #region nested',
'let start = -1;', 'let b = 2',
'// #endregion foo' '// #endregion nested',
] '/* #region css */',
const result = findRegion(lines, 'foo') 'let c = 3',
expect(result).not.toBeNull() '/* #endregion css */',
if (result) { '// #endregion outer',
expect(lines.slice(result.start, result.end).join('\n')).toBe( ''
'let start = -1;' ].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', () => { test('strips every marker style with stripRegionMarkers: all', async () => {
const lines = [ await write('nested.ts', nested)
'.body-content {',
'/* #region foo */', const region = await render('<<< ./nested.ts#outer', {
' padding-left: 15px;', snippet: { stripRegionMarkers: 'all' }
'/* #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;'
)
}
}) })
expect(region.html).not.toContain('#region')
expect(region.html).toContain('let b = 2')
it('detects HTML style region markers', () => { const whole = await render('<<< ./nested.ts', {
const lines = [ snippet: { stripRegionMarkers: 'all' }
'<div>Some content</div>', })
'<!-- #region foo -->', expect(whole.html).not.toContain('#region')
' <h1>Hello world</h1>', expect(whole.html).toContain('let a = 1')
'<!-- #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>'
)
}
}) })
it('detects Visual Basic style region markers (with case-insensitive "End")', () => { test('keeps marker lines with stripRegionMarkers: false', async () => {
const lines = [ await write('nested.ts', nested)
'Console.WriteLine("VB")',
'#Region VBRegion', const region = await render('<<< ./nested.ts#outer', {
' Console.WriteLine("Inside region")', snippet: { stripRegionMarkers: false }
'#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")'
)
}
}) })
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 { html } = await render(
const lines = ['::#region foo', 'echo off', '::#endregion foo'] '::: code-group\n\n<<< ./snip.ts{1 js twoslash} [custom title]\n\n:::'
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'echo off'
) )
} expect(html).toContain('language-js')
expect(html).toContain('custom title')
expect(html).not.toContain('twoslash')
}) })
it('detects C/C++ style region markers using #pragma', () => { test('throws when the file is missing', async () => {
const lines = [ await expect(render('<<< ./missing.ts')).rejects.toThrow(
'#pragma region foo', /Code snippet path not found/
'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;'
) )
}
}) })
it('returns the first complete region when multiple regions exist', () => { test('throws when the path is a directory', async () => {
const lines = [ await write('dir/file.ts', 'const a = 1\n')
'// #region foo', await expect(render('<<< ./dir')).rejects.toThrow(/directory/)
'first region content', })
'// #endregion foo',
'// #region foo', test('throws when the region is missing', async () => {
'second region content', await write('snip.ts', 'const a = 1\n')
'// #endregion foo' await expect(render('<<< ./snip.ts#nope')).rejects.toThrow(
] /region "nope" not found/i
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'first region content'
) )
}
}) })
it('handles nested regions with different names properly', () => { test('silent mode renders nothing and warns', async () => {
const lines = [ await write('snip.ts', 'const a = 1\n')
'// #region foo',
"console.log('line before nested');", const missingFile = await render('<<< ./missing.ts', {
'// #region bar', snippet: { silent: true }
"console.log('nested content');", })
'// #endregion bar', expect(missingFile.html).not.toContain('<pre')
'// #endregion foo' expect(missingFile.env.includes).toEqual([path.join(root, 'missing.ts')])
]
const result = findRegion(lines, 'foo') const missingRegion = await render('<<< ./snip.ts#nope', {
expect(result).not.toBeNull() snippet: { silent: true }
if (result) { })
const extracted = lines.slice(result.start, result.end).join('\n') expect(missingRegion.html).not.toContain('<pre')
const expected = [
"console.log('line before nested');", expect(warnings).toHaveLength(2)
'// #region bar', expect(warnings[0]).toContain('missing.ts')
"console.log('nested content');", expect(warnings[1]).toContain('nope')
'// #endregion bar' })
].join('\n')
expect(extracted).toBe(expected) 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'
])
})
})
})

@ -31,7 +31,7 @@ describe('node/markdownToVue', () => {
siteConfig siteConfig
) )
const result = await render(src, file, 'public') const result = await render(src, file)
expect(result.deadLinks).toContainEqual({ expect(result.deadLinks).toContainEqual({
url: './missing', url: './missing',
@ -58,7 +58,7 @@ describe('node/markdownToVue', () => {
siteConfig siteConfig
) )
const result = await render(src, file, 'public') const result = await render(src, file)
expect(result.deadLinks).toContainEqual({ expect(result.deadLinks).toContainEqual({
url: './missing', url: './missing',
@ -113,7 +113,7 @@ describe('node/markdownToVue', () => {
siteConfig siteConfig
) )
const result = await render(src, file, 'public') const result = await render(src, file)
expect(result.vueSrc).toContain('<p>target text</p>') expect(result.vueSrc).toContain('<p>target text</p>')
expect(result.vueSrc).toContain('<h3 id="child"') expect(result.vueSrc).toContain('<h3 id="child"')
@ -148,11 +148,7 @@ describe('node/markdownToVue', () => {
siteConfig siteConfig
) )
const result = await render( const result = await render('# Home\n', 'C:/site/docs/en/index.md')
'# Home\n',
'C:/site/docs/en/index.md',
'public'
)
expect(result.pageData.relativePath).toBe('index.md') expect(result.pageData.relativePath).toBe('index.md')
}) })

@ -1,7 +1,9 @@
import MiniSearch from 'minisearch' import MiniSearch from 'minisearch'
import { resolveConfig } from 'node/config' import { resolveConfig } from 'node/config'
import { disposeMdItInstance } from 'node/markdown/markdown'
import { createMarkdownToVueRenderFn } from 'node/markdownToVue'
import { localSearchPlugin } from 'node/plugins/localSearchPlugin' import { localSearchPlugin } from 'node/plugins/localSearchPlugin'
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import path from 'node:path' import path from 'node:path'
@ -12,6 +14,9 @@ describe('node/plugins/localSearchPlugin', () => {
beforeEach(() => { beforeEach(() => {
nodeEnv = process.env.NODE_ENV nodeEnv = process.env.NODE_ENV
process.env.NODE_ENV = 'production' 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 () => { afterEach(async () => {
@ -62,8 +67,14 @@ describe('node/plugins/localSearchPlugin', () => {
const siteConfig = await resolveConfig(root, 'build', 'production') const siteConfig = await resolveConfig(root, 'build', 'production')
const plugin = await localSearchPlugin(siteConfig) const plugin = await localSearchPlugin(siteConfig)
const indexModule = (await plugin.load?.call( // vite calls configResolved before any other hook
{} as never, await (plugin.configResolved as any)?.call(
{},
{ publicDir: siteConfig.publicDir }
)
const indexModule = (await (plugin.load as any)?.handler.call(
{},
'/@localSearchIndex' '/@localSearchIndex'
)) as string )) as string
@ -73,10 +84,16 @@ describe('node/plugins/localSearchPlugin', () => {
expect(indexModule).toContain('"zh": () => import(\'@localSearchIndexzh\')') expect(indexModule).toContain('"zh": () => import(\'@localSearchIndexzh\')')
const rootIndex = loadIndex( const rootIndex = loadIndex(
(await plugin.load?.call({} as never, '/@localSearchIndexroot')) as string (await (plugin.load as any)?.handler.call(
{},
'/@localSearchIndexroot'
)) as string
) )
const zhIndex = loadIndex( const zhIndex = loadIndex(
(await plugin.load?.call({} as never, '/@localSearchIndexzh')) as string (await (plugin.load as any)?.handler.call(
{},
'/@localSearchIndexzh'
)) as string
) )
expect(rootIndex.search('rootonlytoken')).toMatchObject([ expect(rootIndex.search('rootonlytoken')).toMatchObject([
@ -89,6 +106,126 @@ describe('node/plugins/localSearchPlugin', () => {
]) ])
expect(zhIndex.search('rootonlytoken')).toEqual([]) 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) { function loadIndex(serializedModule: string) {

@ -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 localIconLoader
} from 'vitepress-plugin-group-icons' } from 'vitepress-plugin-group-icons'
import llmstxt from 'vitepress-plugin-llms' 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 prod = !!process.env.NETLIFY
const siteUrl = 'https://vitepress.dev' const siteUrl = 'https://vitepress.dev'
@ -35,7 +42,6 @@ export default defineConfig({
lastUpdated: true, lastUpdated: true,
cleanUrls: true, cleanUrls: true,
metaChunk: true,
markdown: { markdown: {
math: true, math: true,
@ -51,35 +57,6 @@ export default defineConfig({
} }
], ],
config(md) { 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) md.use(groupIconMdPlugin)
} }
}, },
@ -122,15 +99,16 @@ export default defineConfig({
carbonAds: { code: 'CEBDT27Y', placement: 'vuejsorg' } carbonAds: { code: 'CEBDT27Y', placement: 'vuejsorg' }
}, },
// prettier-ignore
locales: { locales: {
root: { label: 'English', lang: 'en-US', dir: 'ltr' }, root: { label: 'English', lang: 'en-US', dir: 'ltr' },
zh: { label: '简体中文', lang: 'zh-Hans', dir: 'ltr' }, zh: { label: '简体中文', lang: 'zh-Hans', dir: 'ltr', markdown: zhMarkdown },
pt: { label: 'Português', lang: 'pt-BR', dir: 'ltr' }, pt: { label: 'Português', lang: 'pt-BR', dir: 'ltr', markdown: ptMarkdown },
ru: { label: 'Русский', lang: 'ru-RU', dir: 'ltr' }, ru: { label: 'Русский', lang: 'ru-RU', dir: 'ltr', markdown: ruMarkdown },
es: { label: 'Español', lang: 'es', dir: 'ltr' }, es: { label: 'Español', lang: 'es', dir: 'ltr', markdown: esMarkdown },
ko: { label: '한국어', lang: 'ko-KR', dir: 'ltr' }, ko: { label: '한국어', lang: 'ko-KR', dir: 'ltr', markdown: koMarkdown },
fa: { label: 'فارسی', lang: 'fa-IR', dir: 'rtl' }, fa: { label: 'فارسی', lang: 'fa-IR', dir: 'rtl', markdown: faMarkdown },
ja: { label: '日本語', lang: 'ja', dir: 'ltr' } ja: { label: '日本語', lang: 'ja', dir: 'ltr', markdown: jaMarkdown }
}, },
vite: { vite: {
@ -145,10 +123,7 @@ export default defineConfig({
} }
}), }),
prod && llmstxt({ workDir: 'en', ignoreFiles: ['index.md'] }) prod && llmstxt({ workDir: 'en', ignoreFiles: ['index.md'] })
], ]
experimental: {
enableNativePlugin: true
}
}, },
// prettier-ignore // 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)) { :root:where(:lang(fa)) {
--vp-font-family-base: --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'; 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
} }
@ -36,8 +57,31 @@
padding: 18px; padding: 18px;
} }
/* used in reference/default-theme-search */ .VPFeature .icon span {
img[src='/search.png'] { display: inline-block;
width: 100%; width: 1em;
aspect-ratio: 1 / 1; 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");
}
} }

@ -6,7 +6,7 @@ description: Learn how to reference and handle static assets such as images, med
## Referencing Static Assets ## 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 ```md
![An image](./image.png) ![An image](./image.png)

@ -62,11 +62,14 @@ import Layout from './Layout.vue'
export default { export default {
Layout, Layout,
enhanceApp({ app, router, siteData }) { 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: 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] ```ts [.vitepress/theme/index.ts]
@ -226,10 +229,10 @@ Finally, if the theme provides types for its theme config:
```ts [.vitepress/config.ts] ```ts [.vitepress/config.ts]
import baseConfig from 'awesome-vitepress-theme/config' import baseConfig from 'awesome-vitepress-theme/config'
import { defineConfigWithTheme } from 'vitepress' import { defineConfig } from 'vitepress'
import type { ThemeConfig } from 'awesome-vitepress-theme' import type { ThemeConfig } from 'awesome-vitepress-theme'
export default defineConfigWithTheme<ThemeConfig>({ export default defineConfig<ThemeConfig>({
extends: baseConfig, extends: baseConfig,
themeConfig: { themeConfig: {
// Type is `ThemeConfig` // Type is `ThemeConfig`

@ -119,7 +119,7 @@ export default {
} satisfies Theme } 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 ## Layout Slots
@ -309,7 +309,7 @@ Coming soon.
## Overriding Internal Components ## 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 ```ts
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'

@ -45,7 +45,7 @@ $ deno add -D vitepress@next
::: tip NOTE ::: 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`. **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 ## Separate directory for each locale
The following is a perfectly fine structure: 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. description: VitePress built-in Markdown extensions including custom containers, code blocks with syntax highlighting, line highlighting, code groups, and more.
outline: deep
--- ---
# Markdown Extensions # Markdown Extensions
@ -100,6 +101,36 @@ For more details, see [Frontmatter](../reference/frontmatter-config).
| col 2 is | centered | \$12 | | col 2 is | centered | \$12 |
| zebra stripes | are neat | \$1 | | 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: ## Emoji :tada:
**Input** **Input**
@ -112,7 +143,7 @@ For more details, see [Frontmatter](../reference/frontmatter-config).
:tada: :100: :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 ## 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 ### 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** **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` ### `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** **Syntax**
@ -291,7 +411,7 @@ Wraps in a `<div class="vp-raw">`
## GitHub-flavored Alerts ## 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 ```md
> [!NOTE] > [!NOTE]
@ -676,6 +796,16 @@ You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/co
<<< @/snippets/snippet-with-region.js#snippet{1} <<< @/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: You can also specify the language inside the braces (`{}`) like this:
```md ```md
@ -690,7 +820,9 @@ You can also specify the language inside the braces (`{}`) like this:
<<< @/snippets/snippet.cs{1,2,4-6 c#:line-numbers} <<< @/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 ## Code Groups
@ -898,7 +1030,7 @@ You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/co
``` ```
::: warning ::: 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: 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:
@ -948,6 +1080,28 @@ and include it like this:
<!--@@include: ./parts/basics.md#custom-id--> <!--@@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 ## 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: 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:
@ -994,14 +1148,14 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$
## Image Lazy Loading ## 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 ```js
export default { export default {
markdown: { markdown: {
image: { image: {
// image lazy loading is disabled by default // image lazy loading is disabled by default
lazyLoading: true lazyLoad: true
} }
} }
} }
@ -1013,15 +1167,15 @@ VitePress uses [markdown-it](https://github.com/markdown-it/markdown-it) as the
```js ```js
import { defineConfig } from 'vitepress' import { defineConfig } from 'vitepress'
import markdownItAnchor from 'markdown-it-anchor' import { headerLink } from '@mdit/plugin-anchor'
import markdownItFoo from 'markdown-it-foo' import markdownItFoo from 'markdown-it-foo'
export default defineConfig({ export default defineConfig({
markdown: { markdown: {
// options for markdown-it-anchor // options for @mdit/plugin-anchor
// https://github.com/valeriangalliat/markdown-it-anchor#usage // https://mdit-plugins.github.io/anchor.html
anchor: { anchor: {
permalink: markdownItAnchor.permalink.headerLink() permalink: headerLink()
}, },
// options for @mdit-vue/plugin-toc // 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. - `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 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. - 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`. - `carbonAds.carbon` is changed to `carbonAds.code`.
## Frontmatter Config ## Frontmatter Config

@ -39,7 +39,7 @@ onMounted(() => {
### Conditional Import ### 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 ```js
if (!import.meta.env.SSR) { 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 ## 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 # .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. 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). Just want to try it out? Skip to the [Quickstart](./getting-started).
:::
</div>
## Use Cases ## Use Cases
- **Documentation** - **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. 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. 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. - **[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 alt: VitePress
features: features:
- icon: 📝 - icon: <span class="memo"></span>
title: Focus on your content title: Focus on your content
details: Effortlessly create beautiful documentation sites with just markdown. 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 title: Enjoy the Vite DX
details: Instant server start, lightning fast hot updates, and leverage Vite ecosystem plugins. 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 title: Customize with Vue
details: Use Vue syntax and components directly in markdown, or build custom themes 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 title: Ship fast sites
details: Fast initial load with static HTML, fast post-load navigation with client-side routing. details: Fast initial load with static HTML, fast post-load navigation with client-side routing.
--- ---

@ -25,23 +25,23 @@ export default {
## i18nRouting ## i18nRouting
- Type: `boolean | ((data: VitePressData<DefaultTheme.Config>, hash: string, targetLocale: string) => string)` - 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`. 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 hash, and the target locale key, and returns the target link. 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 ```ts
import { defineConfig } from 'vitepress' import { defineConfig } from 'vitepress'
export default defineConfig({ export default defineConfig({
themeConfig: { themeConfig: {
i18nRouting(data, hash, targetLocale) { i18nRouting(data, route, targetLocale) {
const target = data.site.value.locales[targetLocale] const target = data.site.value.locales[targetLocale]
const targetLink = const targetLink =
target.link || (targetLocale === 'root' ? '/' : `/${targetLocale}/`) target.link || (targetLocale === 'root' ? '/' : `/${targetLocale}/`)
return `${targetLink}${data.page.value.relativePath.replace(/\.md$/, '')}${hash}` return `${targetLink}${route.data.relativePath.replace(/\.md$/, '')}${route.hash}`
} }
} }
}) })

@ -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' }
]
}
]
}
]
}
}
```

@ -140,7 +140,7 @@ For custom themes, the same router is available from [`enhanceApp`](../guide/cus
- **Type**: `(path: string) => string` - **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" /> ## `<Content />` <Badge type="info" text="component" />

@ -461,7 +461,7 @@ export default {
- Type: `string` - Type: `string`
- Default: `./.vitepress/cache` - 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 ```ts
export default { export default {
@ -503,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" /> ### mpa <Badge type="warning" text="experimental" />
- Type: `boolean` - Type: `boolean`
@ -543,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). 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 ## Customization
@ -567,7 +560,7 @@ Set `markdown.headers` to `true` or pass [`@mdit-vue/plugin-headers`](https://gi
- Type: `import('vite').UserConfig` - 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 ```js
export default { export default {

@ -1,9 +1,30 @@
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' import {
defineAdditionalConfig,
type DefaultTheme,
type MarkdownLocaleOptions
} from 'vitepress'
const require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json') 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({ export default defineAdditionalConfig({
description: 'Generador de Sitios Estáticos desarrollado con Vite y Vue.', description: 'Generador de Sitios Estáticos desarrollado con Vite y Vue.',
@ -267,8 +288,6 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
afterToolCallText: 'Buscado', afterToolCallText: 'Buscado',
stoppedStreamingText: 'Has detenido esta respuesta', stoppedStreamingText: 'Has detenido esta respuesta',
errorTitleText: 'Error de chat', errorTitleText: 'Error de chat',
threadDepthExceededMessage:
'Esta conversación se ha cerrado para mantener respuestas precisas.',
startNewConversationButtonText: 'Iniciar una nueva conversación' 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} ## 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 ```md
![Una imagen](./imagen.png) ![Una imagen](./imagen.png)

@ -119,7 +119,7 @@ export default {
} satisfies Theme } 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} ## _Slots_ en el Layout {#layout-slots}
@ -309,7 +309,7 @@ En breve.
## Substituyendo Componentes Internos {#overriding-internal-components} ## 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 ```ts
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'

@ -41,7 +41,7 @@ $ bun add -D vitepress@next
::: tip NOTA ::: 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. 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} # Extensiones Markdown {#markdown-extensions}
@ -112,7 +113,7 @@ Para más detalles, vea [Frontmatter](../reference/frontmatter-config).
:tada: :100: :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) ## Tabla de Contenido (TOC)
@ -232,7 +233,7 @@ export default defineConfig({
### `raw` ### `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** **Sintaxis**
@ -888,14 +889,14 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$
## _Lazy Loading_ de Imagenes {#image-lazy-loading} ## _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 ```js
export default { export default {
markdown: { markdown: {
image: { image: {
// la carga perezosa de imagenes está desactivada por defecto // 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 ```js
import { defineConfig } from 'vitepress' import { defineConfig } from 'vitepress'
import markdownItAnchor from 'markdown-it-anchor' import { headerLink } from '@mdit/plugin-anchor'
import markdownItFoo from 'markdown-it-foo' import markdownItFoo from 'markdown-it-foo'
export default defineConfig({ export default defineConfig({
markdown: { markdown: {
// opciones para markdown-it-anchor // opciones para @mdit/plugin-anchor
// https://github.com/valeriangalliat/markdown-it-anchor#usage // https://mdit-plugins.github.io/anchor.html
anchor: { anchor: {
permalink: markdownItAnchor.permalink.headerLink() permalink: headerLink()
}, },
// opciones para @mdit-vue/plugin-toc // opciones para @mdit-vue/plugin-toc

@ -39,7 +39,7 @@ onMounted(() => {
### Importación Condicional {#conditional-import} ### 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 ```js
if (!import.meta.env.SSR) { 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} ## 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 # .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. 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). ¿Quieres probarlo? Ve directo al [Inicio Rápido](./getting-started).
:::
</div>
## Casos de Uso {#use-cases} ## Casos de Uso {#use-cases}
- **Documentación** - **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. 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. 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. - **[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 alt: VitePress
features: features:
- icon: 📝 - icon: <span class="memo"></span>
title: Concéntrese en su contenido title: Concéntrese en su contenido
details: Cree lindos sitios de documentación apenas con markdown. 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 title: Disfruta de la experiencia Vite
details: Inicio instantaneo de servidor, actualizaciones ultrarrápidas, y plugins del ecosistema 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 title: Personaliza con Vue
details: Usa la sintaxis y componentes Vue directamente en markdown, o construye temas personalizados 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 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. details: Carga inicial rápida con HTML estático, navegación rápida con enrutamiento del lado del cliente.
--- ---

@ -430,7 +430,7 @@ export default {
- Tipo: `string` - Tipo: `string`
- Predeterminado: `./.vitepress/cache` - 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 ```ts
export default { 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). 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} ## 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` - 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 ```js
export default { export default {

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

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

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

@ -119,7 +119,7 @@ export default {
} satisfies Theme } 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} ## slot ‌های طرح {#layout-slots}
@ -311,7 +311,7 @@ provide('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => {
## جایگزینی کامپوننت‌های داخلی {#overriding-internal-components} ## جایگزینی کامپوننت‌های داخلی {#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 ```ts
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'

@ -41,7 +41,7 @@ $ bun add -D vitepress@next
::: tip نکته ::: 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 در ویت‌پرس شامل کانتینرهای سفارشی، بلوک‌های کد با هایلایت، گروه‌های کد و موارد دیگر. description: افزونه‌های داخلی Markdown در ویت‌پرس شامل کانتینرهای سفارشی، بلوک‌های کد با هایلایت، گروه‌های کد و موارد دیگر.
outline: deep
--- ---
# افزونه‌های Markdown {#markdown-extensions} # افزونه‌های Markdown {#markdown-extensions}
@ -112,7 +113,7 @@ lang: fa-IR
:tada: :100: :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} ## فهرست مطالب {#table-of-contents}
@ -880,14 +881,14 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$
## بارگذاری lazy تصویر {#image-lazy-loading} ## بارگذاری lazy تصویر {#image-lazy-loading}
می‌توانید بارگذاری تنبلی را برای هر تصویر اضافه شده از طریق Markdown با تنظیم `lazyLoading` به `true` در فایل پیکربندی فعال کنید: می‌توانید بارگذاری تنبلی را برای هر تصویر اضافه شده از طریق Markdown با تنظیم `lazyLoad` به `true` در فایل پیکربندی فعال کنید:
```js ```js
export default { export default {
markdown: { markdown: {
image: { image: {
// بارگذاری تنبلی تصویر به طور پیش‌فرض غیرفعال است // بارگذاری تنبلی تصویر به طور پیش‌فرض غیرفعال است
lazyLoading: true lazyLoad: true
} }
} }
} }
@ -899,15 +900,15 @@ export default {
```js ```js
import { defineConfig } from 'vitepress' import { defineConfig } from 'vitepress'
import markdownItAnchor from 'markdown-it-anchor' import { headerLink } from '@mdit/plugin-anchor'
import markdownItFoo from 'markdown-it-foo' import markdownItFoo from 'markdown-it-foo'
export default defineConfig({ export default defineConfig({
markdown: { markdown: {
// گزینه‌های markdown-it-anchor // گزینه‌های @mdit/plugin-anchor
// https://github.com/valeriangalliat/markdown-it-anchor#usage // https://mdit-plugins.github.io/anchor.html
anchor: { anchor: {
permalink: markdownItAnchor.permalink.headerLink() permalink: headerLink()
}, },
// گزینه‌های @mdit-vue/plugin-toc // گزینه‌های @mdit-vue/plugin-toc

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

@ -39,7 +39,7 @@ onMounted(() => {
### وارد کردن شرطی {#conditional-import} ### وارد کردن شرطی {#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 ```js
if (!import.meta.env.SSR) { if (!import.meta.env.SSR) {

@ -205,7 +205,7 @@ Hello {{ 1 + 1 }}
## استفاده از پیش‌پردازنده‌های CSS {#using-css-pre-processors} ## استفاده از پیش‌پردازنده‌های 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 # .scss و .sass

@ -6,17 +6,15 @@ description: ویت‌پرس یک تولیدکننده سایت ایستا بر
ویت‌پرس یک [تولید کننده سایت ایستا](https://en.wikipedia.org/wiki/Static_site_generator) (SSG) است که برای ساخت وب‌سایت‌های سریع و محتوا محور طراحی شده است. به طور خلاصه، ویت‌پرس محتوای منبع شما که به زبان [Markdown](https://en.wikipedia.org/wiki/Markdown) نوشته شده است را گرفته، یک تم بر روی آن اعمال می‌کند و صفحات HTML ایستا تولید می‌کند که به راحتی در هر جایی قابل استقرار هستند. ویت‌پرس یک [تولید کننده سایت ایستا](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) بروید. فقط می‌خواهید آن را امتحان کنید؟ به [شروع سریع](./getting-started) بروید.
:::
</div>
## موارد استفاده {#use-cases} ## موارد استفاده {#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/) نیز بر پایه ویت‌پرس ساخته شده است، اما از یک تم سفارشی که بین چندین ترجمه مشترک است استفاده می‌کند. [مستندات رسمی Vue.js](https://vuejs.org/) نیز بر پایه ویت‌پرس ساخته شده است، اما از یک تم سفارشی که بین چندین ترجمه مشترک است استفاده می‌کند.
@ -30,7 +28,7 @@ description: ویت‌پرس یک تولیدکننده سایت ایستا بر
ویت‌پرس هدف ارائه یک تجربه عالی برای توسعه دهنده (DX) هنگام کار با محتوای Markdown را دارد. ویت‌پرس هدف ارائه یک تجربه عالی برای توسعه دهنده (DX) هنگام کار با محتوای Markdown را دارد.
- **[قدرت گرفته از Vite:](https://vitejs.dev/)** شروع سرور فوری، با بازتاب ویرایش‌ها به صورت آنی (<100ms) بدون بارگذاری مجدد صفحه. - **[قدرت گرفته از Vite:](https://vite.dev/)** شروع سرور فوری، با بازتاب ویرایش‌ها به صورت آنی (<100ms) بدون بارگذاری مجدد صفحه.
- **[افزونه‌های داخلی Markdown:](./markdown)** استفاده از Frontmatter، جداول، syntax highlighting... هرچه که بخواهید. ویت‌پرس به ویژه ویژگی‌های پیشرفته زیادی برای کار با بلوک‌های کد فراهم می‌کند، که آن را برای مستندات فنی بسیار مناسب می‌کند. - **[افزونه‌های داخلی Markdown:](./markdown)** استفاده از Frontmatter، جداول، syntax highlighting... هرچه که بخواهید. ویت‌پرس به ویژه ویژگی‌های پیشرفته زیادی برای کار با بلوک‌های کد فراهم می‌کند، که آن را برای مستندات فنی بسیار مناسب می‌کند.

@ -21,16 +21,16 @@ hero:
alt: ویت‌پرس alt: ویت‌پرس
features: features:
- icon: 📝 - icon: <span class="memo"></span>
title: تمرکز روی محتوا title: تمرکز روی محتوا
details: ایجاد سایت‌های مستند‌سازی زیبا بدون زحمت و فقط با Markdown 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 title: لذت از تجربه توسعه با Vite
details: شروع فوری سرور، به‌روزرسانی‌های سریع و استفاده از افزونه‌های اکوسیستم 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 title: شخصی‌سازی با Vue
details: استفاده مستقیم از syntax و کامپوننت‌های Vue در Markdown، یا ایجاد تم‌های شخصی به کمک Vue details: استفاده مستقیم از syntax و کامپوننت‌های Vue در Markdown، یا ایجاد تم‌های شخصی به کمک Vue
- icon: 🚀 - icon: <span class="rocket"></span>
title: ارسال سایت های سریع title: ارسال سایت های سریع
details: بارگذاری اولیه سریع با HTML ایستا، ناوبری سریع پس از بارگیری با مسیریابی سمت کلاینت details: بارگذاری اولیه سریع با HTML ایستا، ناوبری سریع پس از بارگیری با مسیریابی سمت کلاینت
--- ---

@ -432,7 +432,7 @@ export default {
- نوع: `string` - نوع: `string`
- پیش‌فرض: `./.vitepress/cache` - پیش‌فرض: `./.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 ```ts
export default { 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} ### mpa <Badge type="warning" text="experimental" /> {#mpa}
- نوع: `boolean` - نوع: `boolean`
@ -514,7 +507,7 @@ export default {
آیا زمان آخرین به‌روزرسانی برای هر صفحه با استفاده از Git دریافت شود. این زمان در داده‌های هر صفحه گنجانده خواهد شد و از طریق [`useData`](./runtime-api#usedata) قابل دسترسی خواهد بود. آیا زمان آخرین به‌روزرسانی برای هر صفحه با استفاده از Git دریافت شود. این زمان در داده‌های هر صفحه گنجانده خواهد شد و از طریق [`useData`](./runtime-api#usedata) قابل دسترسی خواهد بود.
وقتی از تم پیش‌فرض استفاده می‌کنید، فعال کردن این گزینه زمان آخرین به‌روزرسانی هر صفحه را نمایش می‌دهد. می‌توانید متن را از طریق گزینه [`themeConfig.lastUpdatedText`](./default-theme-config#lastupdatedtext) سفارشی کنید. وقتی از تم پیش‌فرض استفاده می‌کنید، فعال کردن این گزینه زمان آخرین به‌روزرسانی هر صفحه را نمایش می‌دهد. می‌توانید متن را از طریق گزینه [`themeConfig.lastUpdated.text`](./default-theme-config#lastupdated) سفارشی کنید.
## سفارشی‌سازی {#customization} ## سفارشی‌سازی {#customization}
@ -536,7 +529,7 @@ export default {
- نوع: `import('vite').UserConfig` - نوع: `import('vite').UserConfig`
پیکربندی خام [Vite Config](https://vitejs.dev/config/) را به سرور توسعه داخلی / بسته‌بند Vite ارسال کنید. پیکربندی خام [Vite Config](https://vite.dev/config/) را به سرور توسعه داخلی / بسته‌بند Vite ارسال کنید.
```js ```js
export default { export default {

@ -1,9 +1,30 @@
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' import {
defineAdditionalConfig,
type DefaultTheme,
type MarkdownLocaleOptions
} from 'vitepress'
const require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json') 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({ export default defineAdditionalConfig({
description: 'Vite と Vue による静的サイトジェネレーター', description: 'Vite と Vue による静的サイトジェネレーター',
@ -231,8 +252,6 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
afterToolCallText: '検索しました', afterToolCallText: '検索しました',
stoppedStreamingText: 'この応答を停止しました', stoppedStreamingText: 'この応答を停止しました',
errorTitleText: 'チャットエラー', errorTitleText: 'チャットエラー',
threadDepthExceededMessage:
'回答の正確性を保つため、この会話は終了しました。',
startNewConversationButtonText: '新しい会話を開始' startNewConversationButtonText: '新しい会話を開始'
} }
} }

@ -6,7 +6,7 @@ description: VitePressで画像、メディア、フォントなどの静的ア
## 静的アセットの参照 {#referencing-static-assets} ## 静的アセットの参照 {#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 ```md
![画像](./image.png) ![画像](./image.png)

@ -122,7 +122,7 @@ export default {
} satisfies Theme } 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} ## レイアウトスロット {#layout-slots}
@ -311,7 +311,7 @@ provide('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => {
## 内部コンポーネントの置き換え {#overriding-internal-components} ## 内部コンポーネントの置き換え {#overriding-internal-components}
Vite の [エイリアス](https://vitejs.dev/config/shared-options.html#resolve-alias) を使って、デフォルトテーマのコンポーネントを独自のものに置き換えられます。 Vite の [エイリアス](https://vite.dev/config/shared-options.html#resolve-alias) を使って、デフォルトテーマのコンポーネントを独自のものに置き換えられます。
```ts ```ts
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'

@ -40,7 +40,7 @@ $ bun add -D vitepress@next
::: :::
::: tip 注意 ::: 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} ### セットアップウィザード {#setup-wizard}

@ -1,5 +1,6 @@
--- ---
description: VitePress組み込みのMarkdown拡張機能。カスタムコンテナ、コードブロックのシンタックスハイライト、行ハイライト、コードグループなど。 description: VitePress組み込みのMarkdown拡張機能。カスタムコンテナ、コードブロックのシンタックスハイライト、行ハイライト、コードグループなど。
outline: deep
--- ---
# Markdown 拡張 {#markdown-extensions} # Markdown 拡張 {#markdown-extensions}
@ -110,7 +111,7 @@ lang: ja-JP
:tada: :100: :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} ## 目次 {#table-of-contents}
@ -229,7 +230,7 @@ export default defineConfig({
### 追加属性 {#additional-attributes} ### 追加属性 {#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` ### `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} ## 画像の遅延読み込み {#image-lazy-loading}
Markdown で追加した各画像に対して遅延読み込みを有効化するには、設定ファイルで `lazyLoading` を `true` にします: Markdown で追加した各画像に対して遅延読み込みを有効化するには、設定ファイルで `lazyLoad` を `true` にします:
```js ```js
export default { export default {
markdown: { markdown: {
image: { image: {
// 既定では画像の遅延読み込みは無効 // 既定では画像の遅延読み込みは無効
lazyLoading: true lazyLoad: true
} }
} }
} }
@ -1015,15 +1016,15 @@ VitePress は Markdown レンダラーとして [markdown-it](https://github.com
```js ```js
import { defineConfig } from 'vitepress' import { defineConfig } from 'vitepress'
import markdownItAnchor from 'markdown-it-anchor' import { headerLink } from '@mdit/plugin-anchor'
import markdownItFoo from 'markdown-it-foo' import markdownItFoo from 'markdown-it-foo'
export default defineConfig({ export default defineConfig({
markdown: { markdown: {
// markdown-it-anchor のオプション // @mdit/plugin-anchor のオプション
// https://github.com/valeriangalliat/markdown-it-anchor#usage // https://mdit-plugins.github.io/anchor.html
anchor: { anchor: {
permalink: markdownItAnchor.permalink.headerLink() permalink: headerLink()
}, },
// @mdit-vue/plugin-toc のオプション // @mdit-vue/plugin-toc のオプション

@ -39,7 +39,7 @@ onMounted(() => {
### 条件付きインポート {#conditional-import} ### 条件付きインポート {#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 ```js
if (!import.meta.env.SSR) { if (!import.meta.env.SSR) {

@ -203,7 +203,7 @@ Hello {{ 1 + 1 }}
## CSS プリプロセッサの利用 {#using-css-pre-processors} ## 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 # .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 ページを生成します。 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) へどうぞ。 まずは試してみたい? [クイックスタート](./getting-started) へどうぞ。
:::
</div>
## ユースケース {#use-cases} ## ユースケース {#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 をベースにしています(複数言語で共有されるカスタムテーマを使用)。 [公式の Vue.js ドキュメント](https://vuejs.org/) も VitePress をベースにしています(複数言語で共有されるカスタムテーマを使用)。
@ -30,7 +28,7 @@ VitePress は、高速でコンテンツ中心の Web サイトを構築する
VitePress は、Markdown コンテンツを扱う際の優れた開発体験DXを目指しています。 VitePress は、Markdown コンテンツを扱う際の優れた開発体験DXを目指しています。
- **[Vite 駆動](https://vitejs.dev/)**:即時サーバー起動、編集はページリロードなしで常に瞬時(<100ms)に反映。 - **[Vite 駆動](https://vite.dev/)**:即時サーバー起動、編集はページリロードなしで常に瞬時(<100ms)に反映。
- **[ビルトインの Markdown 拡張](./markdown)**Frontmatter、表、シンタックスハイライト…必要なものはひと通り。特にコードブロック周りの機能が充実しており、高度な技術ドキュメントに最適です。 - **[ビルトインの Markdown 拡張](./markdown)**Frontmatter、表、シンタックスハイライト…必要なものはひと通り。特にコードブロック周りの機能が充実しており、高度な技術ドキュメントに最適です。

@ -21,16 +21,16 @@ hero:
alt: VitePress alt: VitePress
features: features:
- icon: 📝 - icon: <span class="memo"></span>
title: コンテンツに集中 title: コンテンツに集中
details: Markdown だけで、美しいドキュメントサイトを簡単に作成できます。 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 の開発体験を享受 title: Vite の開発体験を享受
details: 即時サーバー起動、超高速ホットリロード、そして 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 でカスタマイズ title: Vue でカスタマイズ
details: Markdown 内で直接 Vue 構文やコンポーネントを利用したり、Vue で独自テーマを構築できます。 details: Markdown 内で直接 Vue 構文やコンポーネントを利用したり、Vue で独自テーマを構築できます。
- icon: 🚀 - icon: <span class="rocket"></span>
title: 高速サイトを公開 title: 高速サイトを公開
details: 静的 HTML による高速初期ロードと、クライアントサイドルーティングによる快適なページ遷移。 details: 静的 HTML による高速初期ロードと、クライアントサイドルーティングによる快適なページ遷移。
--- ---

@ -430,7 +430,7 @@ export default {
- 型: `string` - 型: `string`
- 既定値: `./.vitepress/cache` - 既定値: `./.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 ```ts
export default { export default {
@ -472,13 +472,6 @@ export default {
} }
``` ```
### metaChunk <Badge type="warning" text="experimental" />
- 型: `boolean`
- 既定値: `false`
`true` にすると、各ページのメタデータを初期 HTML にインラインせず、別の JavaScript チャンクに抽出します。これにより各ページの HTML ペイロードが小さくなり、メタデータをキャッシュ可能にすることで、多数のページがあるサイトでサーバ帯域を削減できます。
### mpa <Badge type="warning" text="experimental" /> ### mpa <Badge type="warning" text="experimental" />
- 型: `boolean` - 型: `boolean`
@ -512,7 +505,7 @@ export default {
Git を使って各ページの最終更新時刻を取得します。タイムスタンプは各ページのデータに含まれ、[`useData`](./runtime-api#usedata) から参照できます。 Git を使って各ページの最終更新時刻を取得します。タイムスタンプは各ページのデータに含まれ、[`useData`](./runtime-api#usedata) から参照できます。
デフォルトテーマ使用時にこのオプションを有効にすると、各ページの最終更新時刻が表示されます。テキストは [`themeConfig.lastUpdatedText`](./default-theme-config#lastupdatedtext) でカスタマイズ可能です。 デフォルトテーマ使用時にこのオプションを有効にすると、各ページの最終更新時刻が表示されます。テキストは [`themeConfig.lastUpdated.text`](./default-theme-config#lastupdated) でカスタマイズ可能です。
## カスタマイズ {#customization} ## カスタマイズ {#customization}
@ -534,7 +527,7 @@ export default {
- 型: `import('vite').UserConfig` - 型: `import('vite').UserConfig`
内部の Vite 開発サーバ/バンドラへ生の [Vite Config](https://vitejs.dev/config/) を渡します。 内部の Vite 開発サーバ/バンドラへ生の [Vite Config](https://vite.dev/config/) を渡します。
```js ```js
export default { export default {

@ -1,9 +1,30 @@
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' import {
defineAdditionalConfig,
type DefaultTheme,
type MarkdownLocaleOptions
} from 'vitepress'
const require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json') 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({ export default defineAdditionalConfig({
description: 'Vite 및 Vue 기반 정적 사이트 생성기.', description: 'Vite 및 Vue 기반 정적 사이트 생성기.',
@ -303,8 +324,6 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
afterToolCallText: '검색함', afterToolCallText: '검색함',
stoppedStreamingText: '이 응답을 중지했습니다', stoppedStreamingText: '이 응답을 중지했습니다',
errorTitleText: '채팅 오류', errorTitleText: '채팅 오류',
threadDepthExceededMessage:
'정확성을 유지하기 위해 이 대화는 종료되었습니다.',
startNewConversationButtonText: '새 대화 시작' startNewConversationButtonText: '새 대화 시작'
} }
} }

@ -6,7 +6,7 @@ description: VitePress에서 이미지, 미디어, 글꼴 등 정적 에셋을
## 정적 에셋 참조하기 {#referencing-static-assets} ## 정적 에셋 참조하기 {#referencing-static-assets}
모든 마크다운 파일은 Vue 컴포넌트로 컴파일되어 [Vite](https://vitejs.dev/guide/assets.html)에 의해 처리됩니다. 모든 에셋은 상대 URL을 사용하여 참조할 수 있으며, **참조해야 합니다**: 모든 마크다운 파일은 Vue 컴포넌트로 컴파일되어 [Vite](https://vite.dev/guide/assets.html)에 의해 처리됩니다. 모든 에셋은 상대 URL을 사용하여 참조할 수 있으며, **참조해야 합니다**:
```md ```md
![이미지](./image.png) ![이미지](./image.png)

@ -119,7 +119,7 @@ export default {
} satisfies Theme } 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} ## 레이아웃 슬롯 {#layout-slots}
@ -309,7 +309,7 @@ provide('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => {
## 내부 컴포넌트 재정의하기 {#overriding-internal-components} ## 내부 컴포넌트 재정의하기 {#overriding-internal-components}
Vite의 [별칭](https://vitejs.dev/config/shared-options.html#resolve-alias)을 사용하여 기본 테마 컴포넌트를 커스텀 컴포넌트로 대체할 수 있습니다: Vite의 [별칭](https://vite.dev/config/shared-options.html#resolve-alias)을 사용하여 기본 테마 컴포넌트를 커스텀 컴포넌트로 대체할 수 있습니다:
```ts ```ts
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'

@ -41,7 +41,7 @@ $ bun add -D vitepress@next
::: tip 참고 ::: tip 참고
VitePress는 ESM 전용 패키지입니다. `require()`를 사용하여 가져오지 마시고, `package.json``"type": "module"`이 포함되어 있는지 확인하거나, 관련 파일(예: `.vitepress/config.js`)의 확장자를 `.mjs`/`.mts`로 변경하세요. 자세한 내용은 [Vite 문제 해결 가이드](http://vitejs.dev/ko/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/ko/guide/troubleshooting.html#this-package-is-esm-only)를 참고하세요. 또한, 비동기 CJS 컨텍스트에서는 `await import('vitepress')`를 사용할 수 있습니다.
::: :::

@ -1,5 +1,6 @@
--- ---
description: 커스텀 컨테이너, 구문 강조 코드 블록, 라인 하이라이팅, 코드 그룹 등 VitePress 내장 마크다운 확장 기능을 소개합니다. description: 커스텀 컨테이너, 구문 강조 코드 블록, 라인 하이라이팅, 코드 그룹 등 VitePress 내장 마크다운 확장 기능을 소개합니다.
outline: deep
--- ---
# 마크다운 확장 기능 {#markdown-extensions} # 마크다운 확장 기능 {#markdown-extensions}
@ -112,7 +113,7 @@ lang: en-US
:tada: :100: :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} ## 목차 {#table-of-contents}
@ -231,7 +232,7 @@ export default defineConfig({
### `raw` ### `raw`
이것은 VitePress와 스타일 및 라우터 충돌을 방지하기 위해 사용할 수 있는 특별한 컨테이너입니다. 컴포넌트 라이브러리를 문서화할 때 특히 유용합니다. 더 나은 분리를 위해 [whyframe](https://whyframe.dev/docs/integrations/vitepress)을 참고해 볼 수 있습니다. 이것은 VitePress와 스타일 및 라우터 충돌을 방지하기 위해 사용할 수 있는 특별한 컨테이너입니다. 컴포넌트 라이브러리를 문서화할 때 특히 유용합니다.
**문법** **문법**
@ -925,14 +926,14 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$
## 이미지 지연 로딩 {#image-lazy-loading} ## 이미지 지연 로딩 {#image-lazy-loading}
마크다운을 통해 추가된 각 이미지에 대해 지연 로딩을 활성화하려면 구성 파일에서 `lazyLoading`을 `true`로 설정하세요: 마크다운을 통해 추가된 각 이미지에 대해 지연 로딩을 활성화하려면 구성 파일에서 `lazyLoad`을 `true`로 설정하세요:
```js ```js
export default { export default {
markdown: { markdown: {
image: { image: {
// 이미지 지연 로딩은 기본적으로 비활성화 되어 있습니다 // 이미지 지연 로딩은 기본적으로 비활성화 되어 있습니다
lazyLoading: true lazyLoad: true
} }
} }
} }
@ -944,15 +945,15 @@ VitePress는 마크다운 렌더러로 [markdown-it](https://github.com/markdown
```js ```js
import { defineConfig } from 'vitepress' import { defineConfig } from 'vitepress'
import markdownItAnchor from 'markdown-it-anchor' import { headerLink } from '@mdit/plugin-anchor'
import markdownItFoo from 'markdown-it-foo' import markdownItFoo from 'markdown-it-foo'
export default defineConfig({ export default defineConfig({
markdown: { markdown: {
// markdown-it-anchor의 옵션 // @mdit/plugin-anchor의 옵션
// https://github.com/valeriangalliat/markdown-it-anchor#usage // https://mdit-plugins.github.io/anchor.html
anchor: { anchor: {
permalink: markdownItAnchor.permalink.headerLink() permalink: headerLink()
}, },
// @mdit-vue/plugin-toc의 옵션 // @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. - `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 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. - 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`. - `carbonAds.carbon` is changed to `carbonAds.code`.
## Frontmatter Config {#frontmatter-config} ## Frontmatter Config {#frontmatter-config}

@ -39,7 +39,7 @@ onMounted(() => {
### 조건부 가져오기 {#conditional-import} ### 조건부 가져오기 {#conditional-import}
`import.meta.env.SSR` 플래그([Vite 환경 변수](https://vitejs.dev/guide/env-and-mode.html#env-variables)의 일부)를 사용하여 종속성을 조건부로 "import" 할 수도 있습니다: `import.meta.env.SSR` 플래그([Vite 환경 변수](https://vite.dev/guide/env-and-mode.html#env-variables)의 일부)를 사용하여 종속성을 조건부로 "import" 할 수도 있습니다:
```js ```js
if (!import.meta.env.SSR) { if (!import.meta.env.SSR) {

@ -204,7 +204,7 @@ Vue 보간 문법을 회피하려면, `<span>` 또는 다른 엘리먼트에 `v-
## CSS 전처리기 사용하기 {#using-css-pre-processors} ## 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 # .scss 및 .sass

@ -6,17 +6,15 @@ description: VitePress는 Vite와 Vue로 구동되는 빠르고 콘텐츠 중심
VitePress는 빠르고 컨텐츠 중심의 웹사이트를 구축하기 위해 설계된 [정적 사이트 생성기](https://en.wikipedia.org/wiki/Static_site_generator) (SSG)입니다. 다시말해 VitePress는 [마크다운](https://en.wikipedia.org/wiki/Markdown)으로 작성된 소스 컨텐츠를 가져와서 테마를 적용하고, 어디에나 쉽게 배포할 수 있는 정적 HTML 페이지를 생성합니다. VitePress는 빠르고 컨텐츠 중심의 웹사이트를 구축하기 위해 설계된 [정적 사이트 생성기](https://en.wikipedia.org/wiki/Static_site_generator) (SSG)입니다. 다시말해 VitePress는 [마크다운](https://en.wikipedia.org/wiki/Markdown)으로 작성된 소스 컨텐츠를 가져와서 테마를 적용하고, 어디에나 쉽게 배포할 수 있는 정적 HTML 페이지를 생성합니다.
<div class="tip custom-block" style="padding-top: 8px"> ::: tip {no-title}
그냥 한번 사용해보고 싶으신가요? [빠른 시작](./getting-started)으로 건너뛰세요. 그냥 한번 사용해보고 싶으신가요? [빠른 시작](./getting-started)으로 건너뛰세요.
:::
</div>
## 사용 사례 {#use-cases} ## 사용 사례 {#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 기반으로 되어 있으며, 여러 번역본에 걸쳐 공유되는 커스텀 테마를 사용합니다. [Vue.js 공식 문서](https://vuejs.org/)도 VitePress 기반으로 되어 있으며, 여러 번역본에 걸쳐 공유되는 커스텀 테마를 사용합니다.
@ -30,7 +28,7 @@ VitePress는 빠르고 컨텐츠 중심의 웹사이트를 구축하기 위해
VitePress는 마크다운 컨텐츠를 다룰 때 훌륭한 개발자 경험(DX)을 제공하고자 합니다. VitePress는 마크다운 컨텐츠를 다룰 때 훌륭한 개발자 경험(DX)을 제공하고자 합니다.
- **[Vite로 작동](https://vitejs.dev/)**: 즉각적인 서버 시작 가능, 페이지 새로고침 없이 즉시(<100ms) 수정 사항 반영. - **[Vite로 작동](https://vite.dev/)**: 즉각적인 서버 시작 가능, 페이지 새로고침 없이 즉시(<100ms) 수정 사항 반영.
- **[내장된 마크다운 확장 기능](./markdown)**: 서문, 표, 구문 강조 등 무엇이든 가능. 특히 VitePress는 코드 블록 작업을 위한 고급 기능을 많이 제공하여 기술적 문서에 이상적. - **[내장된 마크다운 확장 기능](./markdown)**: 서문, 표, 구문 강조 등 무엇이든 가능. 특히 VitePress는 코드 블록 작업을 위한 고급 기능을 많이 제공하여 기술적 문서에 이상적.

@ -21,16 +21,16 @@ hero:
alt: VitePress alt: VitePress
features: features:
- icon: 📝 - icon: <span class="memo"></span>
title: 콘텐츠에 집중 title: 콘텐츠에 집중
details: 마크다운으로만 아름다운 문서 사이트를 쉽게 만들기. details: 마크다운으로만 아름다운 문서 사이트를 쉽게 만들기.
- 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 DX(개발자 경험) 즐겨보기 title: Vite DX(개발자 경험) 즐겨보기
details: 즉각적인 서버 시작, 매우 빠른 업데이트, 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로 커스터마이징 title: Vue로 커스터마이징
details: Vue 문법과 컴포넌트를 마크다운에서 직접 사용하거나 Vue로 커스텀 테마를 구축. details: Vue 문법과 컴포넌트를 마크다운에서 직접 사용하거나 Vue로 커스텀 테마를 구축.
- icon: 🚀 - icon: <span class="rocket"></span>
title: 웹사이트를 빠르게 제공 title: 웹사이트를 빠르게 제공
details: 정적 HTML로 빠른 초기 로딩, 클라이언트 측 라우팅을 통한 빠른 탐색. details: 정적 HTML로 빠른 초기 로딩, 클라이언트 측 라우팅을 통한 빠른 탐색.
--- ---

@ -430,7 +430,7 @@ export default {
- 타입: `string` - 타입: `string`
- 기본값: `./.vitepress/cache` - 기본값: `./.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 ```ts
export default { export default {
@ -472,13 +472,6 @@ export default {
} }
``` ```
### metaChunk <Badge type="warning" text="실험적" />
- 타입: `boolean`
- 기본값: `false`
`true`로 설정하면 페이지 메타데이터를 초기 HTML에 인라인으로 삽입하는 대신 별도의 JavaScript 청크로 추출합니다. 이렇게 하면 각 페이지의 HTML 페이로드가 작아지고 페이지 메타데이터를 캐시할 수 있어, 사이트에 많은 페이지가 있을 때 서버 대역폭을 줄일 수 있습니다.
### mpa <Badge type="warning" text="실험적" /> ### mpa <Badge type="warning" text="실험적" />
- 타입: `boolean` - 타입: `boolean`
@ -512,7 +505,7 @@ export default {
각 페이지의 마지막 업데이트 타임스탬프를 Git을 사용하여 가져올지 여부를 설정합니다. 타임스탬프는 각 페이지의 페이지 데이터에 포함되며, [`useData`](./runtime-api#usedata)를 통해 접근할 수 있습니다. 각 페이지의 마지막 업데이트 타임스탬프를 Git을 사용하여 가져올지 여부를 설정합니다. 타임스탬프는 각 페이지의 페이지 데이터에 포함되며, [`useData`](./runtime-api#usedata)를 통해 접근할 수 있습니다.
기본 테마를 사용할 때, 이 옵션을 활성화하면 각 페이지의 마지막 업데이트 시간이 표시됩니다. [`themeConfig.lastUpdatedText`](./default-theme-config#lastupdatedtext) 옵션을 통해 텍스트를 커스터마이징할 수 있습니다. 기본 테마를 사용할 때, 이 옵션을 활성화하면 각 페이지의 마지막 업데이트 시간이 표시됩니다. [`themeConfig.lastUpdated.text`](./default-theme-config#lastupdated) 옵션을 통해 텍스트를 커스터마이징할 수 있습니다.
## 커스터마이징 {#customization} ## 커스터마이징 {#customization}
@ -534,7 +527,7 @@ export default {
- 타입: `import('vite').UserConfig` - 타입: `import('vite').UserConfig`
내부 Vite 개발 서버/번들러에 직접 [Vite 구성](https://vitejs.dev/config/)을 전달합니다. 내부 Vite 개발 서버/번들러에 직접 [Vite 구성](https://vite.dev/config/)을 전달합니다.
```js ```js
export default { export default {

@ -12,10 +12,10 @@
"devDependencies": { "devDependencies": {
"@lunariajs/core": "^0.1.1", "@lunariajs/core": "^0.1.1",
"markdown-it-mathjax3": "^4.3.2", "markdown-it-mathjax3": "^4.3.2",
"open-cli": "^8.0.0", "open-cli": "^9.0.0",
"postcss-rtlcss": "^6.0.0", "postcss-rtlcss": "^6.0.0",
"vitepress": "workspace:*", "vitepress": "workspace:*",
"vitepress-plugin-group-icons": "^1.7.5", "vitepress-plugin-group-icons": "^1.7.6",
"vitepress-plugin-llms": "^1.13.2" "vitepress-plugin-llms": "^1.13.4"
} }
} }

@ -1,9 +1,30 @@
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' import {
defineAdditionalConfig,
type DefaultTheme,
type MarkdownLocaleOptions
} from 'vitepress'
const require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json') const pkg = require('vitepress/package.json')
export const markdown: MarkdownLocaleOptions = {
container: {
tipLabel: 'DICA',
infoLabel: 'INFORMAÇÃO',
warningLabel: 'AVISO',
dangerLabel: 'PERIGO',
detailsLabel: 'Detalhes',
noteLabel: 'NOTA',
importantLabel: 'IMPORTANTE',
cautionLabel: 'CUIDADO'
},
codeCopyButton: {
tooltipText: 'Copiar código',
copiedText: 'Copiado'
}
}
export default defineAdditionalConfig({ export default defineAdditionalConfig({
description: 'Gerador de Site Estático desenvolvido com Vite e Vue.', description: 'Gerador de Site Estático desenvolvido com Vite e Vue.',
@ -264,8 +285,6 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
afterToolCallText: 'Pesquisado', afterToolCallText: 'Pesquisado',
stoppedStreamingText: 'Você interrompeu esta resposta', stoppedStreamingText: 'Você interrompeu esta resposta',
errorTitleText: 'Erro no chat', errorTitleText: 'Erro no chat',
threadDepthExceededMessage:
'Esta conversa foi encerrada para manter respostas precisas.',
startNewConversationButtonText: 'Iniciar uma nova conversa' startNewConversationButtonText: 'Iniciar uma nova conversa'
} }
} }

@ -6,7 +6,7 @@ description: Aprenda a referenciar e manipular ativos estáticos como imagens, m
## Referenciando Ativos Estáticos {#referencing-static-assets} ## Referenciando Ativos Estáticos {#referencing-static-assets}
Todos os arquivos Markdown são compilados em componentes Vue e processados por [Vite](https://vitejs.dev/guide/assets.html). Você pode **e deve** referenciar quaisquer ativos usando URLs relativas: Todos os arquivos Markdown são compilados em componentes Vue e processados por [Vite](https://vite.dev/guide/assets.html). Você pode **e deve** referenciar quaisquer ativos usando URLs relativas:
```md ```md
![Uma imagem](./imagem.png) ![Uma imagem](./imagem.png)

@ -119,7 +119,7 @@ export default {
} satisfies Theme } satisfies Theme
``` ```
Como estamos usando Vite, você também pode aproveitar a [funcionalidade de importação glob](https://vitejs.dev/guide/features.html#glob-import) do Vite para registrar automaticamente um diretório de componentes. Como estamos usando Vite, você também pode aproveitar a [funcionalidade de importação glob](https://vite.dev/guide/features.html#glob-import) do Vite para registrar automaticamente um diretório de componentes.
## _Slots_ no Layout {#layout-slots} ## _Slots_ no Layout {#layout-slots}
@ -309,7 +309,7 @@ Em breve.
## Substituindo Componentes Internos {#overriding-internal-components} ## Substituindo Componentes Internos {#overriding-internal-components}
Você pode usar os [aliases](https://vitejs.dev/config/shared-options.html#resolve-alias) Vite para substituir os componentes do tema padrão pelos seus personalizados: Você pode usar os [aliases](https://vite.dev/config/shared-options.html#resolve-alias) Vite para substituir os componentes do tema padrão pelos seus personalizados:
```ts ```ts
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'

@ -41,7 +41,7 @@ $ bun add -D vitepress@next
::: tip NOTA ::: tip NOTA
VitePress é um pacote apenas para ESM. Não use `require()` para importá-lo, e certifique de que o `package.json` mais próximo contém `"type": "module"`, ou mude a extensão do arquivo de seus arquivos releavantes como `.vitepress/config.js` para `.mjs`/`.mts`. Refira-se ao [Guia de resolução de problemas Vite](http://vitejs.dev/guide/troubleshooting.html#this-package-is-esm-only) para mais detalhes. Além disso, dentro de contextos de JavaScript comum assíncronos, você pode usar `await import('vitepress')`. VitePress é um pacote apenas para ESM. Não use `require()` para importá-lo, e certifique de que o `package.json` mais próximo contém `"type": "module"`, ou mude a extensão do arquivo de seus arquivos releavantes como `.vitepress/config.js` para `.mjs`/`.mts`. Refira-se ao [Guia de resolução de problemas Vite](http://vite.dev/guide/troubleshooting.html#this-package-is-esm-only) para mais detalhes. Além disso, dentro de contextos de JavaScript comum assíncronos, você pode usar `await import('vitepress')`.
::: :::

@ -1,5 +1,6 @@
--- ---
description: Extensões Markdown integradas do VitePress, incluindo containers personalizados, blocos de código com destaque de sintaxe, grupos de código e mais. description: Extensões Markdown integradas do VitePress, incluindo containers personalizados, blocos de código com destaque de sintaxe, grupos de código e mais.
outline: deep
--- ---
# Extensões Markdown {#markdown-extensions} # Extensões Markdown {#markdown-extensions}
@ -112,7 +113,7 @@ Para mais detalhes, veja [Frontmatter](../reference/frontmatter-config).
:tada: :100: :tada: :100:
Uma [lista de todos os emojis](https://github.com/markdown-it/markdown-it-emoji/blob/master/lib/data/full.mjs) está disponível. Uma [lista de todos os emojis](https://github.com/mdit-plugins/mdit-plugins/blob/main/packages/plugin-emoji/src/data/full.ts) está disponível.
## Tabela de Conteúdo (TOC) ## Tabela de Conteúdo (TOC)
@ -231,7 +232,7 @@ export default defineConfig({
### `raw` ### `raw`
Este é um recipiente especial que pode ser usado para evitar conflitos de estilo e roteador com VitePress. Isso é especialmente útil ao documentar bibliotecas de componentes. Você também pode verificar [whyframe](https://whyframe.dev/docs/integrations/vitepress) para melhor isolamento. Este é um recipiente especial que pode ser usado para evitar conflitos de estilo e roteador com VitePress. Isso é especialmente útil ao documentar bibliotecas de componentes.
**Sintaxe** **Sintaxe**
@ -887,14 +888,14 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$
## _Lazy Loading_ de Imagens {#image-lazy-loading} ## _Lazy Loading_ de Imagens {#image-lazy-loading}
Você pode ativar o "carregamento folgado" para cada imagem adicionada via markdown definindo `lazyLoading` como `true` no seu arquivo de configuração: Você pode ativar o "carregamento folgado" para cada imagem adicionada via markdown definindo `lazyLoad` como `true` no seu arquivo de configuração:
```js ```js
export default { export default {
markdown: { markdown: {
image: { image: {
// o carregamento folgado de imagens está desativado por padrão // o carregamento folgado de imagens está desativado por padrão
lazyLoading: true lazyLoad: true
} }
} }
} }
@ -906,15 +907,15 @@ VitePress usa [markdown-it](https://github.com/markdown-it/markdown-it) como int
```js ```js
import { defineConfig } from 'vitepress' import { defineConfig } from 'vitepress'
import markdownItAnchor from 'markdown-it-anchor' import { headerLink } from '@mdit/plugin-anchor'
import markdownItFoo from 'markdown-it-foo' import markdownItFoo from 'markdown-it-foo'
export default defineConfig({ export default defineConfig({
markdown: { markdown: {
// opções para markdown-it-anchor // opções para @mdit/plugin-anchor
// https://github.com/valeriangalliat/markdown-it-anchor#usage // https://mdit-plugins.github.io/anchor.html
anchor: { anchor: {
permalink: markdownItAnchor.permalink.headerLink() permalink: headerLink()
}, },
// opções para @mdit-vue/plugin-toc // opções para @mdit-vue/plugin-toc

@ -39,7 +39,7 @@ onMounted(() => {
### Importação Condicional {#conditional-import} ### Importação Condicional {#conditional-import}
Você também pode importar condicionalmente uma dependência usando o sinalizador `import.meta.env.SSR` (parte das [variáveis de ambiente Vite](https://vitejs.dev/guide/env-and-mode.html#env-variables)): Você também pode importar condicionalmente uma dependência usando o sinalizador `import.meta.env.SSR` (parte das [variáveis de ambiente Vite](https://vite.dev/guide/env-and-mode.html#env-variables)):
```js ```js
if (!import.meta.env.SSR) { if (!import.meta.env.SSR) {

@ -203,7 +203,7 @@ Observe que isso pode impedir que certos tokens sejam realçados corretamente.
## Usando Pré-processadores CSS {#using-css-pre-processors} ## Usando Pré-processadores CSS {#using-css-pre-processors}
O VitePress possui [suporte embutido](https://vitejs.dev/guide/features.html#css-pre-processors) para pré-processadores CSS: arquivos `.scss`, `.sass`, `.less`, `.styl` e `.stylus`. Não é necessário instalar plugins específicos do Vite para eles, mas o próprio pré-processador correspondente deve ser instalado: O VitePress possui [suporte embutido](https://vite.dev/guide/features.html#css-pre-processors) para pré-processadores CSS: arquivos `.scss`, `.sass`, `.less`, `.styl` e `.stylus`. Não é necessário instalar plugins específicos do Vite para eles, mas o próprio pré-processador correspondente deve ser instalado:
``` ```
# .scss e .sass # .scss e .sass

@ -6,17 +6,15 @@ description: VitePress é um gerador de sites estáticos projetado para criar si
O VitePress é um [Gerador de Site Estático](https://en.wikipedia.org/wiki/Static_site_generator) (SSG) projetado para criar sites rápidos e centrados em conteúdo. Em suma, VitePress utiliza seu conteúdo-fonte escrito em [Markdown](https://en.wikipedia.org/wiki/Markdown), aplica um tema a ele e gera páginas HTML estáticas que podem ser facilmente implantadas em qualquer lugar. O VitePress é um [Gerador de Site Estático](https://en.wikipedia.org/wiki/Static_site_generator) (SSG) projetado para criar sites rápidos e centrados em conteúdo. Em suma, VitePress utiliza seu conteúdo-fonte escrito em [Markdown](https://en.wikipedia.org/wiki/Markdown), aplica um tema a ele e gera páginas HTML estáticas que podem ser facilmente implantadas em qualquer lugar.
<div class="tip custom-block" style="padding-top: 8px"> ::: tip {no-title}
Quer apenas experimentar? Pule para o [Início Rápido](./getting-started). Quer apenas experimentar? Pule para o [Início Rápido](./getting-started).
:::
</div>
## Casos de Uso {#use-cases} ## Casos de Uso {#use-cases}
- **Documentação** - **Documentação**
VitePress vem com um tema padrão projetado para documentação técnica. Ele alimenta esta página que você está lendo agora, juntamente com a documentação [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/) e [muitos outros](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code). VitePress vem com um tema padrão projetado para documentação técnica. Ele alimenta esta página que você está lendo agora, juntamente com a documentação [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/) e [muitos outros](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code).
A [documentação oficial Vue.js](https://vuejs.org/) também é baseada em VitePress, mas usa um tema personalizado compartilhado entre várias traduções. A [documentação oficial Vue.js](https://vuejs.org/) também é baseada em VitePress, mas usa um tema personalizado compartilhado entre várias traduções.
@ -30,7 +28,7 @@ Quer apenas experimentar? Pule para o [Início Rápido](./getting-started).
VitePress visa proporcionar excelente Experiência de Desenvolvedor (DX) ao trabalhar com conteúdo em Markdown. VitePress visa proporcionar excelente Experiência de Desenvolvedor (DX) ao trabalhar com conteúdo em Markdown.
- **[Alimentado por Vite:](https://vitejs.dev/)** inicialização instantânea do servidor, com edições sempre refletidas instantaneamente (<100ms) sem recarregamento de página. - **[Alimentado por Vite:](https://vite.dev/)** inicialização instantânea do servidor, com edições sempre refletidas instantaneamente (<100ms) sem recarregamento de página.
- **[Extensões Markdown Integradas:](./markdown)** Frontmatter, tabelas, destaque de sintaxe... você escolhe. Especificamente, VitePress fornece muitos recursos avançados para trabalhar com blocos de código, tornando-o ideal para documentação altamente técnica. - **[Extensões Markdown Integradas:](./markdown)** Frontmatter, tabelas, destaque de sintaxe... você escolhe. Especificamente, VitePress fornece muitos recursos avançados para trabalhar com blocos de código, tornando-o ideal para documentação altamente técnica.

@ -21,16 +21,16 @@ hero:
alt: VitePress alt: VitePress
features: features:
- icon: 📝 - icon: <span class="memo"></span>
title: Foco no seu conteúdo title: Foco no seu conteúdo
details: Cria sites de documentação belos e sem esforço apenas com markdown. details: Cria sites de documentação belos e sem esforço apenas com 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: Aproveite a experiência Vite title: Aproveite a experiência Vite
details: Início de servidor instantâneo, atualizações ultrarrápidas, e plugins do ecossistema Vite. details: Início de servidor instantâneo, atualizações ultrarrápidas, e plugins do ecossistema 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: Personalize com Vue title: Personalize com Vue
details: Use sintaxe e componentes Vue diretamente em markdown, ou construa temas personalizados com Vue. details: Use sintaxe e componentes Vue diretamente em markdown, ou construa temas personalizados com Vue.
- icon: 🚀 - icon: <span class="rocket"></span>
title: Entregue Sites Rápidos title: Entregue Sites Rápidos
details: Carregamento inicial rápido com HTML estático, navegação rápida com roteamento no lado do cliente. details: Carregamento inicial rápido com HTML estático, navegação rápida com roteamento no lado do cliente.
--- ---

@ -430,7 +430,7 @@ export default {
- Tipo: `string` - Tipo: `string`
- Padrão: `./.vitepress/cache` - Padrão: `./.vitepress/cache`
O diretório para arquivos de cache, relativo à [raiz do projeto](../guide/routing#root-and-source-directory). Veja também: [cacheDir](https://vitejs.dev/config/shared-options.html#cachedir). O diretório para arquivos de cache, relativo à [raiz do projeto](../guide/routing#root-and-source-directory). Veja também: [cacheDir](https://vite.dev/config/shared-options.html#cachedir).
```ts ```ts
export default { export default {
@ -503,7 +503,7 @@ Esta opção injeta um script em linha que restaura as configurações dos usuá
Para obter o selo de tempo da última atualização para cada página usando o Git. O selo de data será incluído nos dados de cada página, acessíveis via [`useData`](./runtime-api#usedata). Para obter o selo de tempo da última atualização para cada página usando o Git. O selo de data será incluído nos dados de cada página, acessíveis via [`useData`](./runtime-api#usedata).
Ao usar o tema padrão, habilitar esta opção exibirá o horário da última atualização de cada página. Você pode personalizar o texto via opção [`themeConfig.lastUpdatedText`](./default-theme-config#lastupdatedtext). Ao usar o tema padrão, habilitar esta opção exibirá o horário da última atualização de cada página. Você pode personalizar o texto via opção [`themeConfig.lastUpdated.text`](./default-theme-config#lastupdated).
## Personalização {#customization} ## Personalização {#customization}
@ -525,7 +525,7 @@ Verifique a [declaração de tipo e jsdocs](https://github.com/vuejs/vitepress/b
- Tipo: `import('vite').UserConfig` - Tipo: `import('vite').UserConfig`
Passe a [Configuração Vite](https://vitejs.dev/config/) crua para o servidor interno / empacotador Vite. Passe a [Configuração Vite](https://vite.dev/config/) crua para o servidor interno / empacotador Vite.
```js ```js
export default { export default {

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

Loading…
Cancel
Save