diff --git a/.github/contributing.md b/.github/contributing.md index 5738cbc2..c2f14f4a 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -19,7 +19,7 @@ Hi! We're really excited that you are interested in contributing to VitePress. B - It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging. -- Commit messages must follow the [commit message convention](./commit-convention.md) so that changelogs can be automatically generated. +- Commit messages must follow the [commit message convention](/.github/commit-convention.md) so that changelogs can be automatically generated. ## Development Setup diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f8cd3d5a..560d76ff 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -13,4 +13,4 @@ --- > [!TIP] -> The author of this PR can publish a _preview release_ by commenting `/publish` below. +> The author can publish a _preview release_ by commenting `/publish` after creating the PR. diff --git a/.github/workflows/cr.yml b/.github/workflows/cr.yml index 7cc01dbb..ae327e48 100644 --- a/.github/workflows/cr.yml +++ b/.github/workflows/cr.yml @@ -38,9 +38,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v6 + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 with: node-version: 24 cache: pnpm diff --git a/.github/workflows/lock-threads.yml b/.github/workflows/lock-threads.yml index c76a2acb..1dc547f1 100644 --- a/.github/workflows/lock-threads.yml +++ b/.github/workflows/lock-threads.yml @@ -18,7 +18,7 @@ jobs: if: github.repository == 'vuejs/vitepress' runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v5 + - uses: dessant/lock-threads@v6 with: issue-inactive-days: 7 pr-inactive-days: 7 diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 1c675000..43dd8b09 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v7 - name: Create Release for Tag id: release_tag diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a89cc38c..694d2434 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node_version: [20, 22, 24, latest] + node_version: [22, 24, 26] include: - os: windows-latest node_version: 24 @@ -27,13 +27,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v7 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: Set node version to ${{ matrix.node_version }} - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: ${{ matrix.node_version }} cache: pnpm diff --git a/.gitignore b/.gitignore index 64331052..e6e95ca9 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ node_modules pnpm-global TODOs.md *.timestamp-*.mjs +.claude diff --git a/.prettierignore b/.prettierignore index 32f2a7ad..d23ab1d0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,3 +7,6 @@ template temp !CHANGELOG.md .temp + +# generated by scripts/subsetFonts.py +src/client/theme-default/styles/fonts.css diff --git a/CHANGELOG.md b/CHANGELOG.md index b3c1b636..f150ae78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,160 @@ +## [2.0.0-alpha.19](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.18...v2.0.0-alpha.19) (2026-08-02) + +### Bug Fixes + +- **build:** apply rewrites when computing createContentLoader urls ([9e2148d](https://github.com/vuejs/vitepress/commit/9e2148d75ed966467a438403ad5b0a69df83480b)) +- **build:** clear markdown cache with rewritten path on include change ([5eac447](https://github.com/vuejs/vitepress/commit/5eac4473189228da7a917a1a35a67233c94513c9)), closes [#5035](https://github.com/vuejs/vitepress/issues/5035) +- **build:** compose markdown `preConfig` hook when extending configs ([c39a85a](https://github.com/vuejs/vitepress/commit/c39a85a2ac88dca978d6a7b07fac3353fe0ae7fe)), closes [#5205](https://github.com/vuejs/vitepress/issues/5205) +- **build:** don't apply docsearch css transform to pages matching its filter ([fa0e48c](https://github.com/vuejs/vitepress/commit/fa0e48c8df8f66fb76f7df4d7d662a5c7b3c3c7c)) +- **build:** don't rely on checkout directory name when externalizing types ([e6ba9d8](https://github.com/vuejs/vitepress/commit/e6ba9d8caa3866215095f63b62290f3110e523fd)) +- **build:** report links to pre-rewrite paths of rewritten pages as dead ([3cf3f37](https://github.com/vuejs/vitepress/commit/3cf3f37b17054ff38347d4d0a2e76e03d68e2e7a)) +- **build:** resolve additional configs by source path ([5e4a9e7](https://github.com/vuejs/vitepress/commit/5e4a9e799f620bcd3de973272d8343a515fd4992)) +- **build:** resolve rewrites against externally injected pages too ([769c10e](https://github.com/vuejs/vitepress/commit/769c10ee65a61882724e2b748634326b576bb079)) +- **build:** respect cleanUrls in content loader render ([027f046](https://github.com/vuejs/vitepress/commit/027f0461e073702c2d5b7e96006cf34a51825f45)), closes [#4331](https://github.com/vuejs/vitepress/issues/4331) [#5299](https://github.com/vuejs/vitepress/issues/5299) +- **build:** track include importers by module id ([c961655](https://github.com/vuejs/vitepress/commit/c9616559f55f378b92e2f3b8bac31336db540699)) +- **build:** prevent duplicate IDs in MiniSearch ([#5303](https://github.com/vuejs/vitepress/issues/5303)) ([505278c](https://github.com/vuejs/vitepress/commit/505278c2cb06c5ed4ca094b193d6dc912376818c)) +- **build:** remove stackTraceLimit Infinity for DEBUG ([#5318](https://github.com/vuejs/vitepress/issues/5318)) ([865d04b](https://github.com/vuejs/vitepress/commit/865d04b2528863f265e39e1b05278c9654f8599a)) +- **build:** retry file reads when out of file descriptors ([da71173](https://github.com/vuejs/vitepress/commit/da71173afc8eece9af49ff25ad1bff9bbd676096)) +- **cli:** avoid onAfterConfigResolve ([3fbaf9c](https://github.com/vuejs/vitepress/commit/3fbaf9c4d21e62062490c58c13cbf1b074417004)) +- **client:** make the route the single source of truth for the URL hash ([dcb7a75](https://github.com/vuejs/vitepress/commit/dcb7a75532c5472060ec379d25a5fafbc7932637)) +- **markdown:** preserve user-defined attributes ([3f4530b](https://github.com/vuejs/vitepress/commit/3f4530b0e2b339cc408ab38b8b922a9834bb396e)), closes [#5123](https://github.com/vuejs/vitepress/issues/5123) +- **markdown:** remove extra whitespace from container markup ([b6d9cb8](https://github.com/vuejs/vitepress/commit/b6d9cb83c747f9a021aa277b0c64b5bf3a40bf41)) +- **markdown:** skip circular includes ([3372516](https://github.com/vuejs/vitepress/commit/3372516152df0b871634492ac6d3207bedf89d4e)) +- **markdown:** remove deprecated `cjkFriendly` option ([27762ea](https://github.com/vuejs/vitepress/commit/27762eac86aa5c5d998de128734c2a8c10f78e23)) +- **markdown:** rename image option `lazyLoading` to `lazyLoad` ([078786a](https://github.com/vuejs/vitepress/commit/078786a1b3e0793f55cb14d819df93900041ccb0)) +- **search:** index changed files with srcDir-relative paths in dev ([6b64f35](https://github.com/vuejs/vitepress/commit/6b64f3561b0da8fcf48a6ccff7b08036ad858ef3)), closes [#3374](https://github.com/vuejs/vitepress/issues/3374) +- **search:** only index pages on dev updates ([559fb24](https://github.com/vuejs/vitepress/commit/559fb24a2238b2c0b97c36c808c092dda98bb2f7)) +- **search:** serve a fresh search index after dev updates ([1534a67](https://github.com/vuejs/vitepress/commit/1534a67d866e4922a7eab1a1c166db7f11855c59)) +- **search:** skip pages that fail to render instead of crashing dev server ([3ffefa2](https://github.com/vuejs/vitepress/commit/3ffefa2550d1242da4a798e18cb308cb33ad815b)) +- **theme:** use ul and li for lists ([#5326](https://github.com/vuejs/vitepress/issues/5326)) ([3f99872](https://github.com/vuejs/vitepress/commit/3f99872468f861458f4f37d6115cfd1c1a7b15ae)) +- **theme:** align docsearch breakpoints with the default theme ([90c28d4](https://github.com/vuejs/vitepress/commit/90c28d41ce0a67f6e096ce9fd355f3b0c7b1dab7)), closes [#5213](https://github.com/vuejs/vitepress/issues/5213) +- **theme:** align local search breakpoint ([#5217](https://github.com/vuejs/vitepress/issues/5217)) ([a425113](https://github.com/vuejs/vitepress/commit/a425113572d94c39d0c3369eac5adb7a87cfb798)) +- **theme:** correct anchor scroll margins across viewports ([dabc5e9](https://github.com/vuejs/vitepress/commit/dabc5e95ce210ae14cb33a08e6784826e4e2d544)) +- **theme:** ensure outline marker follows click ([#3879](https://github.com/vuejs/vitepress/issues/3879)) ([31287c0](https://github.com/vuejs/vitepress/commit/31287c0b69b330bc1645268b1bebd21f9a40c238)) +- **theme:** external link icon not showing in navbar links ([225c94a](https://github.com/vuejs/vitepress/commit/225c94afd2c355a33fbbb93871fb9b064da475b8)), closes [#5306](https://github.com/vuejs/vitepress/issues/5306) +- **theme:** pass target and rel to prev/next page links ([#5297](https://github.com/vuejs/vitepress/issues/5297)) ([6b5e770](https://github.com/vuejs/vitepress/commit/6b5e7704a01500d87a3702f7b27f95a4bdcfa10d)) +- **theme:** preserve url params when switching languages ([#5312](https://github.com/vuejs/vitepress/issues/5312)) ([9ee401d](https://github.com/vuejs/vitepress/commit/9ee401d7adefc39fd960990cc032be5464e4eb27)) +- **theme:** prevent TypeError when navigating to page without outline ([#5329](https://github.com/vuejs/vitepress/issues/5329)) ([9376c58](https://github.com/vuejs/vitepress/commit/9376c58abec557dd8c5b63f991a1d1068586f175)) +- **theme:** remove font-synthesis style ([#5309](https://github.com/vuejs/vitepress/issues/5309)) ([c34769c](https://github.com/vuejs/vitepress/commit/c34769c2e67969881b9cc8abbccf6d3cc6a5b647)) +- **theme:** safari not showing external link icon properly ([7118402](https://github.com/vuejs/vitepress/commit/711840222700804dbb6fb39ee9b9580a3e6220e7)) +- **theme:** rebuild the base styles on tailwind's preflight ([f1ee913](https://github.com/vuejs/vitepress/commit/f1ee91335ecc562511f5e97214f41c043e6944fa)) +- **theme:** route cjk punctuation to matching system fonts ([91b06b6](https://github.com/vuejs/vitepress/commit/91b06b67a302ebfa210e888fe9d03d27fb291b31)) +- **theme:** remove deprecated `disableDetailedView` local search option ([cec4998](https://github.com/vuejs/vitepress/commit/cec499869f02313337993a7f2ad381f0f9d9dafd)) +- **theme:** remove deprecated `lastUpdatedText` option ([18d1b47](https://github.com/vuejs/vitepress/commit/18d1b4713c6634cc60e6b4a95430e05d51ec4812)) +- **theme:** remove deprecated `outlineTitle` option ([95c0420](https://github.com/vuejs/vitepress/commit/95c042039c62a9235e223f8da05a2075aa2234d7)) +- **types:** declare Badge as a global component ([ca8ba5b](https://github.com/vuejs/vitepress/commit/ca8ba5baec6c8fa551ee29917bd072f84406e68b)) +- **types:** import EnhanceAppContext from package entry in theme.d.ts ([8ff1f3e](https://github.com/vuejs/vitepress/commit/8ff1f3e2dd34fd443c0fa80cf3962f9d67e81cf5)), closes [#5156](https://github.com/vuejs/vitepress/issues/5156) + +### Features + +- add web-types.json for JetBrains IDE support ([1d448c9](https://github.com/vuejs/vitepress/commit/1d448c9fc1ec90849c2a18c49cab5e275f3dc019)), closes [#5157](https://github.com/vuejs/vitepress/issues/5157) +- auto-add width/height to local images to avoid layout shift ([#5311](https://github.com/vuejs/vitepress/issues/5311)) ([3868b64](https://github.com/vuejs/vitepress/commit/3868b64e419223279eaae800766b244cde9bc85f)) +- **client:** declare $frontmatter and $params as ComponentCustomProperties ([0535009](https://github.com/vuejs/vitepress/commit/0535009f44b2f9ddf8e30b59700bacdb9a9b5e75)) +- **client:** declare Content and ClientOnly components as GlobalComponents ([#5156](https://github.com/vuejs/vitepress/issues/5156)) ([6629577](https://github.com/vuejs/vitepress/commit/6629577b48fe2785cf6865197cb326eff9f2a9d8)), closes [#5154](https://github.com/vuejs/vitepress/issues/5154) +- **cli:** show vite version in startup log ([#5328](https://github.com/vuejs/vitepress/issues/5328)) ([4666fc2](https://github.com/vuejs/vitepress/commit/4666fc277609f8bb916e6a54eb0ac9327784d073)) +- enable metaChunk by default ([#5325](https://github.com/vuejs/vitepress/issues/5325)) ([8e42f31](https://github.com/vuejs/vitepress/commit/8e42f3159a1d5d8576bee0b736f56e943d3cb858)) +- **init:** use async fs in scaffolding ([38b59f9](https://github.com/vuejs/vitepress/commit/38b59f925206120b1a20b1fcfb650b45f8147175)) +- **markdown:** accept booleans for plugin options ([c2be0bf](https://github.com/vuejs/vitepress/commit/c2be0bf936e46c196151b79ba683a30490f2d164)) +- **markdown:** add region marker engine ([b303dd3](https://github.com/vuejs/vitepress/commit/b303dd341df7c0ca1e5b63c086084b2baad68c19)) +- **markdown:** allow disabling table `tabindex` attribute ([bffe1e1](https://github.com/vuejs/vitepress/commit/bffe1e14125220d465a94cc629260e19bff48e0c)) +- **markdown:** support disabling built-in markdown plugins ([b8d9c8f](https://github.com/vuejs/vitepress/commit/b8d9c8f9a92ec4e8c877d6c28fee26b3c379876c)), closes [#4484](https://github.com/vuejs/vitepress/issues/4484) [#4556](https://github.com/vuejs/vitepress/issues/4556) +- **markdown:** support footnotes ([430a890](https://github.com/vuejs/vitepress/commit/430a890a17910593e26e9654b141ef3855ecceac)) +- **markdown:** support GitHub-style task lists ([97f8781](https://github.com/vuejs/vitepress/commit/97f87817ead5b7a26e0dfc535cfd60ec0597d130)), closes [#413](https://github.com/vuejs/vitepress/issues/413) [#1923](https://github.com/vuejs/vitepress/issues/1923) [#3648](https://github.com/vuejs/vitepress/issues/3648) [#5110](https://github.com/vuejs/vitepress/issues/5110) +- **markdown:** support per-locale markdown strings ([faaa4a1](https://github.com/vuejs/vitepress/commit/faaa4a124ed7a5b2f1cfc8f9b7686a3785d2012b)), closes [#4431](https://github.com/vuejs/vitepress/issues/4431) +- **markdown:** support registering custom containers ([962f00e](https://github.com/vuejs/vitepress/commit/962f00e7a3c22dd021729e732e12a758b42aef1c)), closes [#3591](https://github.com/vuejs/vitepress/issues/3591) [#3603](https://github.com/vuejs/vitepress/issues/3603) [#4228](https://github.com/vuejs/vitepress/issues/4228) +- **markdown:** support title-less containers ([4c7a030](https://github.com/vuejs/vitepress/commit/4c7a030bc1eecd0ed441a2f56cde73e59ff90abe)), closes [#4928](https://github.com/vuejs/vitepress/issues/4928) [#4932](https://github.com/vuejs/vitepress/issues/4932) [#4929](https://github.com/vuejs/vitepress/issues/4929) +- **markdown:** rebase relative urls in included files ([6a337ef](https://github.com/vuejs/vitepress/commit/6a337efece9697dcb1e4ae4fe657af97089570f6)) +- **markdown:** group code copy button strings into one option ([2fa0ded](https://github.com/vuejs/vitepress/commit/2fa0dedbb241e901569c2e20690e8e5917f4bd26)), closes [#4431](https://github.com/vuejs/vitepress/issues/4431) +- **markdown:** replace markdown-it-attrs with @mdit/plugin-attrs ([18380b0](https://github.com/vuejs/vitepress/commit/18380b0eb78765f49d86918c86c9fee9e7197135)) +- **markdown:** replace markdown-it-emoji with @mdit/plugin-emoji ([018887f](https://github.com/vuejs/vitepress/commit/018887fa1d03031e9c6cc96606be22df51581e35)) +- **markdown:** replace markdown-it-anchor with @mdit/plugin-anchor ([7550517](https://github.com/vuejs/vitepress/commit/75505179160bf16a88cd5648719615e982c08e41)) +- **markdown:** support `attrs: false` for disabling attrs plugin ([e235dbe](https://github.com/vuejs/vitepress/commit/e235dbeb8aef1213d0de9efafe0ccb758acd267a)) +- **markdown:** support `note`, `important`, `caution` markdown containers ([#5161](https://github.com/vuejs/vitepress/issues/5161)) ([3b560a0](https://github.com/vuejs/vitepress/commit/3b560a0efa8bdbf6f621413b3e8a27b19f4a638f)), closes [#4427](https://github.com/vuejs/vitepress/issues/4427) [#3928](https://github.com/vuejs/vitepress/issues/3928) +- **theme:** allow internal social links ([51ff681](https://github.com/vuejs/vitepress/commit/51ff681f4e5caf7dfff1f5c1c79a70aaf4785081)), closes [#5305](https://github.com/vuejs/vitepress/issues/5305) +- **theme:** cover all Inter glyphs, generate font subsets from a script ([705c1be](https://github.com/vuejs/vitepress/commit/705c1be0f88643f9fdbeb09d1835374d70afbebd)) +- update option stability annotations ([ab1896f](https://github.com/vuejs/vitepress/commit/ab1896fbf4da4b90f900d845a8410b7350ea9edd)) + +### Performance Improvements + +- **build:** reuse lastUpdated from markdown rendering in sitemap generation ([fdd68e3](https://github.com/vuejs/vitepress/commit/fdd68e37a720d2fd898033c5f695e125ee93634b)) +- **md:** bypass gray-matter's unbounded cache ([4f8703d](https://github.com/vuejs/vitepress/commit/4f8703d61ccefb61823d4f7bac67f142759d81ca)) +- **md:** limit the compile cache's memory usage ([2fb6bda](https://github.com/vuejs/vitepress/commit/2fb6bdabf03b617ccfa1d00c9261242bc5d93e56)) +- use hook filters in vite plugins ([fa24c6d](https://github.com/vuejs/vitepress/commit/fa24c6d67b4a572a56ac76aeb9f97ca3cd03170d)) + +### Reverts + +- Revert "fix: prevent DocSearch SVG clipping in WebKit" (#5304) ([c8313a4](https://github.com/vuejs/vitepress/commit/c8313a4bcd24af21cd829d7c4fd3792ecd0c17a7)), closes [#5304](https://github.com/vuejs/vitepress/issues/5304) [#5240](https://github.com/vuejs/vitepress/issues/5240) + +### BREAKING CHANGES + +- `markdown.anchor` options are now typed by `@mdit/plugin-anchor`. Common options (`level`, `slugify`, `permalink`, `getTokensText`, `tabIndex`, etc.) are unchanged, but the deprecated markdown-it-anchor permalink options (`permalinkSymbol`, `renderPermalink`, ...) are no longer accepted. Permalink builders like `headerLink` are named exports of `@mdit/plugin-anchor` instead of properties of the plugin. +- `markdown.attrs` options are now typed by `@mdit/plugin-attrs`: `leftDelimiter`, `rightDelimiter`, and `allowedAttributes` are renamed to `left`, `right`, and `allowed`. A `rule` option is available for toggling individual attribute rules (VitePress disables `fence` by default). +- `markdown.codeCopyButtonTitle` is now `markdown.codeCopyButton.tooltipText`, and its default changed from "Copy Code" to "Copy code". The `--vp-code-copy-copied-text-content` CSS variable and its built-in per-language `:lang()` defaults are removed - set `codeCopyButton.copiedText` (per locale) instead. +- `useData().hash` has been removed. Read the hash from `useRoute()` instead. +- callers of vitepress' build() will now notice the version banner gets printed. To disable that, pass a noop function: + + ```ts + build(root, { + onAfterConfigResolve() {} + }) + ``` + +- custom `themeConfig.i18nRouting` functions now receive the current `Route` as their second argument instead of the hash +- normalize-level defaults differ from the old reset - native select and number-input chrome is restored, headings inherit font size and weight, the hidden attribute is enforced with !important, and every element starts with zero margin/padding and border-style: solid. Sites layering custom CSS on the default theme may notice. +- relative urls in included markdown files resolve against the included file rather than the including page. Partials written for one specific location may need their links updated, or markdown.include.rebaseRelativeUrls set to false to keep resolving them from the including page. Absolute and external urls are unaffected. Note that the marker comments shift the line numbers reported for dead links following an include, which already pointed into the include-expanded source rather than the original file. +- **init:** scaffold() now returns a `Promise` and must be awaited. +- The `defs` property of `markdown.emoji` has been renamed to `definitions`. +- The `markdown.attrs.disable` option has been removed. Set `markdown.attrs` to `false` instead. +- The `markdown.image.lazyLoading` option has been renamed to `markdown.image.lazyLoad`. +- The deprecated `disableDetailedView` option of local search has been removed. Use `detailedView: false` instead. +- The deprecated `markdown.cjkFriendly` option has been removed. Use `markdown.cjkFriendlyEmphasis` instead. +- The deprecated `themeConfig.lastUpdatedText` option has been removed. Use `themeConfig.lastUpdated.text` instead. +- The deprecated `themeConfig.outlineTitle` option has been removed. Use `themeConfig.outline.label` instead. +- The Inter4CJK font family has been renamed to 'Inter Core'. Custom --vp-font-family-base overrides referencing Inter4CJK must be updated. + +## [2.0.0-alpha.18](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.17...v2.0.0-alpha.18) (2026-07-06) + +### Bug Fixes + +- **build:** apply `base` to links with download attribute ([#5186](https://github.com/vuejs/vitepress/issues/5186)) ([01987c4](https://github.com/vuejs/vitepress/commit/01987c4c0838478a8fda81f427775f28476c5557)) +- **build:** normalize rewrite drive letters ([#5245](https://github.com/vuejs/vitepress/issues/5245)) ([5c50b99](https://github.com/vuejs/vitepress/commit/5c50b99724815a6fb3d2311e801dcad4aeb9b412)) +- **build:** show dead link line numbers ([#5230](https://github.com/vuejs/vitepress/issues/5230)) ([c37bde6](https://github.com/vuejs/vitepress/commit/c37bde6308fb9f202e224f5eb38c0fac35468ea6)) +- compose markdown config when extending configs ([#5236](https://github.com/vuejs/vitepress/issues/5236)) ([04f4fba](https://github.com/vuejs/vitepress/commit/04f4fbadbdc05c75dbf3564c21404540d570d43b)) +- delete undefined values while merging sidepanel props ([cc30b10](https://github.com/vuejs/vitepress/commit/cc30b10b60beb862f85915055dc97651703bf250)) +- disable pluginTimings and invalidAnnotation for now ([9315fc1](https://github.com/vuejs/vitepress/commit/9315fc182229f13ee793dc44107947b2fe6ab50e)) +- don't invalidate framework chunk when a new asset is added ([c0e2e18](https://github.com/vuejs/vitepress/commit/c0e2e1809464c48435a22a0aa9468ff5e562791d)) +- escape description in head ([d96bf1d](https://github.com/vuejs/vitepress/commit/d96bf1dc616599609d8a24af7183aee6a7b9ae07)) +- index rewritten local search pages by locale ([#5241](https://github.com/vuejs/vitepress/issues/5241)) ([80cf265](https://github.com/vuejs/vitepress/commit/80cf2650aa5fa4b49093509f60766cc5b28c19bc)) +- keep translation links in the current tab ([#5158](https://github.com/vuejs/vitepress/issues/5158)) ([202ee70](https://github.com/vuejs/vitepress/commit/202ee7026054ac5c721bbdbf196426628b0c9b18)) +- normalize `/index` to `/` ([856858d](https://github.com/vuejs/vitepress/commit/856858d26a78f2e19e2c8ee23c2e85a95dbfdd29)), closes [#5165](https://github.com/vuejs/vitepress/issues/5165) +- preserve Agent Studio DocSearch options ([#5254](https://github.com/vuejs/vitepress/issues/5254)) ([f29ffdb](https://github.com/vuejs/vitepress/commit/f29ffdbb33022eb41327fec836f9cfbc16bf01d8)) +- preserve external sidebar links with base ([#5243](https://github.com/vuejs/vitepress/issues/5243)) ([ddf178a](https://github.com/vuejs/vitepress/commit/ddf178a170967527bafe7c9b262fb66aa10ec9de)) +- prevent DocSearch SVG clipping in WebKit ([#5240](https://github.com/vuejs/vitepress/issues/5240)) ([a357e5e](https://github.com/vuejs/vitepress/commit/a357e5ef67ed266861877eb08da753b2141a784f)) +- strip frontmatter before heading includes ([#5246](https://github.com/vuejs/vitepress/issues/5246)) ([e68fade](https://github.com/vuejs/vitepress/commit/e68fade75d2259b10695e85277f5483a084e3ae7)) +- **theme:** avatars misaligned in team member cards ([6730fb8](https://github.com/vuejs/vitepress/commit/6730fb84620c852b516de59f6f7c39c4f04f0e37)), closes [#5160](https://github.com/vuejs/vitepress/issues/5160) +- **theme:** correct mixed LTR/RTL text rendering in code blocks ([73f7b0b](https://github.com/vuejs/vitepress/commit/73f7b0b984853758d41e897dd43e5e93a1066266)) +- **theme:** keep external link icon inline ([#5232](https://github.com/vuejs/vitepress/issues/5232)) ([756a88c](https://github.com/vuejs/vitepress/commit/756a88cfa2f8f71400362327d2255cb8b5ccacfa)) +- **theme:** prevent `sub` and `sup` elements from affecting line height ([19357f9](https://github.com/vuejs/vitepress/commit/19357f9d337472572a500b8d2af8ef97932bfdda)), closes [#5173](https://github.com/vuejs/vitepress/issues/5173) +- use resolveDynamicComponent instead of resolveComponent ([9da1e3e](https://github.com/vuejs/vitepress/commit/9da1e3e70f41b7b8cb81f791307c778d65854f7a)) + +### Features + +- add macOS local search navigation shortcuts ([#5237](https://github.com/vuejs/vitepress/issues/5237)) ([e635e9e](https://github.com/vuejs/vitepress/commit/e635e9e5ea2876a293954970c6a00d6e09cb50a5)) +- add support for `format` option in Carbon options ([#5188](https://github.com/vuejs/vitepress/issues/5188)) ([6ee01bf](https://github.com/vuejs/vitepress/commit/6ee01bf30534cbf7847fe8c94fbf23efd9637f68)) +- allow custom i18n routing ([#5239](https://github.com/vuejs/vitepress/issues/5239)) ([eef5742](https://github.com/vuejs/vitepress/commit/eef57427f4861939ff27f07d71587e68118064e2)) +- allow VPContent to use custom components ([#5176](https://github.com/vuejs/vitepress/issues/5176)) ([c0b38d5](https://github.com/vuejs/vitepress/commit/c0b38d52c270e4efb59effa8ab7207535c48ec05)) +- **markdown:** expose Shiki color replacements in markdown options ([#5153](https://github.com/vuejs/vitepress/issues/5153)) ([fccc617](https://github.com/vuejs/vitepress/commit/fccc6171024f1b0087dd963dbe5a6236081b5d3d)) +- migrate to vite 8 ([228eef1](https://github.com/vuejs/vitepress/commit/228eef187ae33d1676a79e466d289c0e6e9ab321)) +- show local search loading state ([#5252](https://github.com/vuejs/vitepress/issues/5252)) ([7e2273a](https://github.com/vuejs/vitepress/commit/7e2273a3e470da2777c2510b51262e85879f2ee9)) +- support scroll-margin / scroll-padding ([6cce766](https://github.com/vuejs/vitepress/commit/6cce76685da39f8b5c75da047f847f82f70b9c4e)) +- support social link target option ([#5242](https://github.com/vuejs/vitepress/issues/5242)) ([d0159c8](https://github.com/vuejs/vitepress/commit/d0159c8a850cbd2a010a3d44bdeb97c3db651d0e)) + +### BREAKING CHANGES + +- VitePress now uses Vite 8. If you are using Vite plugins in your config, please check the [Vite 8 migration guide](https://vite.dev/guide/migration) for any breaking changes that may affect you. +- Node 20 support is dropped. v22 or higher is needed. +- `scrollOffset` from config is removed. Users wanting to customize scroll offset should customize `scroll-margin-top` via CSS instead. `smoothScroll` support from `router.go` is also removed as it didn't work as expected for most users. Users wanting smooth scrolling should set `scroll-behavior: smooth` in CSS, ideally inside a `@media (prefers-reduced-motion: no-preference)` block. + ## [2.0.0-alpha.17](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.16...v2.0.0-alpha.17) (2026-03-19) ### Bug Fixes @@ -97,6 +254,7 @@ Users who intentionally reference non-existent files or want to document include - `markdown-it-attrs` is disabled for fenced code blocks. For most users no change is required. If you want to add classes to code blocks, do it using shiki transformers instead. - Rename `cjkFriendly` to `cjkFriendlyEmphasis` in your vitepress config. Most people should be unaffected unless they want to disable the CJK emphasis behavior added v2.0.0-alpha.12. +- `useData().lastUpdated` now defaults to 0 if `lastUpdated` is enabled in config. No change is needed unless you were manually doing strict comparison in your layout or components. ## [2.0.0-alpha.12](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.11...v2.0.0-alpha.12) (2025-08-20) @@ -776,7 +934,7 @@ Users who intentionally reference non-existent files or want to document include ### BREAKING CHANGES -- VitePress now runs on Vite 5. Please refer https://vitejs.dev/guide/migration for breaking changes and migration guide if you're relying on some Vite-specific things. +- VitePress now runs on Vite 5. Please refer https://vite.dev/guide/migration for breaking changes and migration guide if you're relying on some Vite-specific things. # [1.0.0-rc.25](https://github.com/vuejs/vitepress/compare/v1.0.0-rc.24...v1.0.0-rc.25) (2023-11-05) diff --git a/README.md b/README.md index 02700308..ed8200dd 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,10 @@ Copyright (c) 2019-present, Yuxi (Evan) You This project would not be possible without the support of these amazing companies and tools. -| | | | -| ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| | | | +| --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | VoidZero's logo | **[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. | | StackBlitz's logo | **[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. | | Algolia's logo | **[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. | | BrowserStack's logo | **[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. | +| Netlify's logo | **[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. | diff --git a/__tests__/e2e/.vitepress/config.ts b/__tests__/e2e/.vitepress/config.ts index 121761e7..7ec72c30 100644 --- a/__tests__/e2e/.vitepress/config.ts +++ b/__tests__/e2e/.vitepress/config.ts @@ -155,13 +155,19 @@ export default defineConfig({ title: 'Example', description: 'An example app using VitePress.', markdown: { - image: { - lazyLoading: true - } + image: { lazyLoad: true } }, themeConfig: { nav, sidebar, + socialLinks: [ + { + icon: 'github', + link: '/home', + ariaLabel: 'Home social link', + target: '_self' + } + ], search: { provider: 'local', options: { diff --git a/__tests__/e2e/assets/vitepress logo.png b/__tests__/e2e/assets/vitepress logo.png new file mode 100644 index 00000000..6cb0b384 Binary files /dev/null and b/__tests__/e2e/assets/vitepress logo.png differ diff --git a/__tests__/e2e/assets/vitepress.png b/__tests__/e2e/assets/vitepress.png new file mode 100644 index 00000000..6cb0b384 Binary files /dev/null and b/__tests__/e2e/assets/vitepress.png differ diff --git a/__tests__/e2e/data-loading/data.test.ts b/__tests__/e2e/data-loading/data.test.ts index 21cfa061..4a4e87a8 100644 --- a/__tests__/e2e/data-loading/data.test.ts +++ b/__tests__/e2e/data-loading/data.test.ts @@ -1,4 +1,4 @@ -import fs from 'node:fs/promises' +import { writeFile, unlink } from 'node:fs/promises' import { fileURLToPath } from 'node:url' describe('static data file support in vite 3', () => { @@ -48,20 +48,20 @@ describe('static data file support in vite 3', () => { const b = fileURLToPath(new URL('./data/b.json', import.meta.url)) try { - await fs.writeFile(a, JSON.stringify({ a: false }, null, 2) + '\n') + await writeFile(a, JSON.stringify({ a: false }, null, 2) + '\n') await page.waitForFunction( () => document.querySelector('pre#basic')?.textContent === JSON.stringify([{ a: false }, { b: true }], null, 2) ) } finally { - await fs.writeFile(a, JSON.stringify({ a: true }, null, 2) + '\n') + await writeFile(a, JSON.stringify({ a: true }, null, 2) + '\n') } let err = true try { - await fs.unlink(b) + await unlink(b) await page.waitForFunction( () => document.querySelector('pre#basic')?.textContent === @@ -70,19 +70,19 @@ describe('static data file support in vite 3', () => { err = false } finally { if (err) { - await fs.writeFile(b, JSON.stringify({ b: true }, null, 2) + '\n') + await writeFile(b, JSON.stringify({ b: true }, null, 2) + '\n') } } try { - await fs.writeFile(b, JSON.stringify({ b: false }, null, 2) + '\n') + await writeFile(b, JSON.stringify({ b: false }, null, 2) + '\n') await page.waitForFunction( () => document.querySelector('pre#basic')?.textContent === JSON.stringify([{ a: true }, { b: false }], null, 2) ) } finally { - await fs.writeFile(b, JSON.stringify({ b: true }, null, 2) + '\n') + await writeFile(b, JSON.stringify({ b: true }, null, 2) + '\n') } }) diff --git a/__tests__/e2e/env.d.ts b/__tests__/e2e/env.d.ts new file mode 100644 index 00000000..a99cf76c --- /dev/null +++ b/__tests__/e2e/env.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent + export default component +} diff --git a/__tests__/e2e/home.test.ts b/__tests__/e2e/home.test.ts index 76868361..b6bd6ff9 100644 --- a/__tests__/e2e/home.test.ts +++ b/__tests__/e2e/home.test.ts @@ -32,4 +32,13 @@ describe('render correct content', async () => { const outlineLinksCount = await outlineLinksLocator.count() expect(outlineLinksCount).toEqual(4) }) + + test('social link target override', async () => { + const socialLink = page.locator( + '.VPNavBarSocialLinks a[aria-label="Home social link"]' + ) + + expect(await socialLink.getAttribute('href')).toBe('/home') + expect(await socialLink.getAttribute('target')).toBe('_self') + }) }) diff --git a/__tests__/e2e/local-search/local-search.test.ts b/__tests__/e2e/local-search/local-search.test.ts index 9496dc6b..07ec1926 100644 --- a/__tests__/e2e/local-search/local-search.test.ts +++ b/__tests__/e2e/local-search/local-search.test.ts @@ -3,15 +3,69 @@ describe('local search', () => { await goto('/') }) + test.runIf(!process.env.VITE_TEST_BUILD)( + 'shows progress while loading search index', + async () => { + const indexRoute = /@localSearchIndexroot/ + let delayedIndex = false + + await page.route(indexRoute, async (route) => { + delayedIndex = true + await new Promise((resolve) => setTimeout(resolve, 800)) + await route.continue() + }) + + try { + await page.locator('.VPNavBarSearchButton').click() + + const loading = page.locator('.search-loading') + const results = page.locator('.results') + + await page.waitForFunction(() => + document + .querySelector('.search-loading') + ?.classList.contains('active') + ) + + expect(delayedIndex).toBe(true) + expect(await loading.getAttribute('role')).toBe('status') + expect(await loading.getAttribute('aria-label')).toBe( + 'Loading search results' + ) + expect(await results.getAttribute('aria-busy')).toBe('true') + + await page.waitForFunction( + () => + !document + .querySelector('.search-loading') + ?.classList.contains('active') + ) + + expect(await results.getAttribute('aria-busy')).toBe('false') + } finally { + await page.unroute(indexRoute) + } + } + ) + test('exclude content from search results', async () => { await page.locator('.VPNavBarSearchButton').click() const input = await page.waitForSelector('input#localsearch-input') await input.type('local') - await page.waitForSelector('ul#localsearch-list', { state: 'visible' }) - const searchResults = page.locator('#localsearch-list') + await page.waitForFunction(() => { + const options = [ + ...document.querySelectorAll('#localsearch-list li[role=option]') + ] + + return ( + options.length === 1 && + options[0].textContent?.includes('Local search included') + ) + }) + expect(await searchResults.locator('li[role=option]').count()).toBe(1) expect( @@ -28,4 +82,69 @@ describe('local search', () => { .count() ).toBe(0) }) + + test('uses the same desktop breakpoint as the nav bar', async () => { + try { + for (const { width, isDesktop } of [ + { width: 767, isDesktop: false }, + { width: 768, isDesktop: true } + ]) { + await page.setViewportSize({ width, height: 600 }) + await goto('/') + await page.locator('.VPNavBarSearchButton').click() + await page.waitForSelector('input#localsearch-input') + + expect(await page.locator('.VPNavBarHamburger').isVisible()).toBe( + !isDesktop + ) + expect(await page.locator('.search-actions.before').isVisible()).toBe( + !isDesktop + ) + } + } finally { + await page.setViewportSize({ width: 1280, height: 720 }) + } + }) + + test('navigate results with macOS Ctrl shortcuts', async () => { + await page.evaluate(() => document.documentElement.classList.add('mac')) + await page.locator('.VPNavBarSearchButton').click() + + const input = await page.waitForSelector('input#localsearch-input') + await input.type('lorem') + + await page.waitForFunction(() => { + return ( + document.querySelectorAll('#localsearch-list li[role=option]').length > + 1 + ) + }) + + expect(await input.getAttribute('aria-activedescendant')).toBe( + 'localsearch-item-0' + ) + + await pressMacCtrl('n') + expect(await input.getAttribute('aria-activedescendant')).toBe( + 'localsearch-item-1' + ) + + await pressMacCtrl('p') + expect(await input.getAttribute('aria-activedescendant')).toBe( + 'localsearch-item-0' + ) + }) }) + +function pressMacCtrl(key: string) { + return page.evaluate((key) => { + window.dispatchEvent( + new KeyboardEvent('keydown', { + key, + ctrlKey: true, + bubbles: true, + cancelable: true + }) + ) + }, key) +} diff --git a/__tests__/e2e/markdown-extensions/index.md b/__tests__/e2e/markdown-extensions/index.md index 252df395..866197c1 100644 --- a/__tests__/e2e/markdown-extensions/index.md +++ b/__tests__/e2e/markdown-extensions/index.md @@ -56,6 +56,18 @@ This is a dangerous warning. This is a details block. ::: +::: note +This is a note. +::: + +::: important +This is an important note. +::: + +::: caution +This is a caution note. +::: + ### Custom Title ::: danger STOP diff --git a/__tests__/e2e/tsconfig.json b/__tests__/e2e/tsconfig.json new file mode 100644 index 00000000..44878152 --- /dev/null +++ b/__tests__/e2e/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.json", + "include": ["**/*", ".vitepress/**/*"] +} diff --git a/__tests__/init/init.test.ts b/__tests__/init/init.test.ts index 7766b060..11ea8c32 100644 --- a/__tests__/init/init.test.ts +++ b/__tests__/init/init.test.ts @@ -1,6 +1,6 @@ -import fs from 'fs-extra' import getPort from 'get-port' import { nanoid } from 'nanoid' +import { rm } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath, URL } from 'node:url' import { chromium } from 'playwright-chromium' @@ -33,13 +33,13 @@ const variations = themes.flatMap((theme) => afterAll(async () => { await page.close() await browser.close() - await fs.remove(tempDir) + await rm(tempDir, { recursive: true, force: true }) }) test.each(variations)('init %s', async (_, { theme, useTs }) => { const root = getTempRoot() - await fs.remove(root) - scaffold({ root, theme, useTs, injectNpmScripts: false }) + await rm(root, { recursive: true, force: true }) + await scaffold({ root, theme, useTs, injectNpmScripts: false }) const port = await getPort() const server = await createServer(root, { port }) diff --git a/__tests__/init/tsconfig.json b/__tests__/init/tsconfig.json new file mode 100644 index 00000000..3c43903c --- /dev/null +++ b/__tests__/init/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.json" +} diff --git a/__tests__/tsconfig.json b/__tests__/tsconfig.json index b8fef70a..366c4ab8 100644 --- a/__tests__/tsconfig.json +++ b/__tests__/tsconfig.json @@ -1,13 +1,8 @@ { "extends": "../tsconfig.json", "compilerOptions": { + "noEmit": true, "isolatedModules": false, - "baseUrl": ".", - "types": ["node", "vitest/globals"], - "paths": { - "client/*": ["../src/client/*"], - "node/*": ["../src/node/*"], - "shared/*": ["../src/shared/*"] - } + "types": ["node", "vitest/globals"] } } diff --git a/__tests__/unit/client/theme-default/composables/langs.test.ts b/__tests__/unit/client/theme-default/composables/langs.test.ts new file mode 100644 index 00000000..39fe021c --- /dev/null +++ b/__tests__/unit/client/theme-default/composables/langs.test.ts @@ -0,0 +1,146 @@ +import { resolveLocaleLink } from 'client/theme-default/composables/langs' +import type { Route, VitePressData } from 'vitepress' +import type { DefaultTheme } from 'vitepress/theme' +import { ref } from 'vue' + +// `currentPage` is the current page's relative path (like +// `route.data.relativePath`, but with a leading slash), plus any query and +// hash of the current URL. +function resolve( + currentPage: string, + { + themeConfig = {}, + cleanUrls = false, + targetLocale = 'fr', + targetLocaleLink = '/fr/', + currentLocaleLink = '/', + linkToCorrespondingPage = true + }: { + themeConfig?: DefaultTheme.Config + cleanUrls?: boolean + targetLocale?: string + targetLocaleLink?: string + currentLocaleLink?: string + linkToCorrespondingPage?: boolean + } = {} +) { + const { pathname, search, hash } = new URL(currentPage, 'http://a.com') + const route = { + data: { relativePath: pathname.slice(1) }, + query: search, + hash + } as Route + const data = { + site: ref({ + cleanUrls, + locales: { + root: { label: 'English', lang: 'en-US' }, + fr: { label: 'Français', lang: 'fr-FR', link: '/fr/' } + }, + themeConfig + }), + theme: ref(themeConfig) + } as unknown as VitePressData + + return resolveLocaleLink(data, route, { + targetLocale, + targetLocaleLink, + currentLocaleLink, + linkToCorrespondingPage + }) +} + +describe('client/theme-default/composables/langs', () => { + describe('resolveLocaleLink', () => { + describe('locale home links (linkToCorrespondingPage: false)', () => { + test('links to the target locale home', () => { + expect( + resolve('/guide/getting-started.md', { + linkToCorrespondingPage: false + }) + ).toBe('/fr/') + }) + + test('preserves query and hash', () => { + expect( + resolve('/guide/getting-started.md?a=1#install', { + linkToCorrespondingPage: false + }) + ).toBe('/fr/?a=1#install') + }) + + test('ignores custom i18n routing functions', () => { + expect( + resolve('/guide/getting-started.md', { + linkToCorrespondingPage: false, + themeConfig: { i18nRouting: () => '/custom/' } + }) + ).toBe('/fr/') + }) + }) + + describe('corresponding page links (linkToCorrespondingPage: true)', () => { + test('rewrites the current page path into the target locale', () => { + expect(resolve('/guide/getting-started.md#install')).toBe( + '/fr/guide/getting-started.html#install' + ) + }) + + test('drops the .html extension when clean URLs are enabled', () => { + expect( + resolve('/guide/getting-started.md#install', { cleanUrls: true }) + ).toBe('/fr/guide/getting-started#install') + }) + + test('resolves index pages to directory links', () => { + expect(resolve('/guide/index.md')).toBe('/fr/guide/') + expect(resolve('/guide/index.md', { cleanUrls: true })).toBe( + '/fr/guide/' + ) + }) + + test('resolves the site root page to the target locale home', () => { + expect(resolve('/index.md')).toBe('/fr/') + }) + + test('strips the current locale prefix before rewriting', () => { + expect( + resolve('/en/guide/index.md?query#intro', { + currentLocaleLink: '/en/', + cleanUrls: true + }) + ).toBe('/fr/guide/?query#intro') + }) + + test('rewrites into the root locale', () => { + expect( + resolve('/fr/guide/getting-started.md#install', { + targetLocale: 'root', + targetLocaleLink: '/', + currentLocaleLink: '/fr/' + }) + ).toBe('/guide/getting-started.html#install') + }) + + test('links to the target locale home when i18n routing is disabled', () => { + expect( + resolve('/guide/getting-started.md#install', { + themeConfig: { i18nRouting: false } + }) + ).toBe('/fr/#install') + }) + + test('delegates to custom i18n routing functions', () => { + expect( + resolve('/guide/getting-started.md#install', { + themeConfig: { + i18nRouting(data, route, targetLocale) { + return `${data.site.value.locales[targetLocale].link}mapped/${route.data.relativePath}${route.hash}` + } + } + }) + ).toBe('/fr/mapped/guide/getting-started.md#install') + }) + }) + }) +}) diff --git a/__tests__/unit/client/theme-default/support/docsearch.test.ts b/__tests__/unit/client/theme-default/support/docsearch.test.ts index 4da8f113..78c5ee68 100644 --- a/__tests__/unit/client/theme-default/support/docsearch.test.ts +++ b/__tests__/unit/client/theme-default/support/docsearch.test.ts @@ -1,5 +1,6 @@ import { buildAskAiConfig, + buildSidePanelProps, hasAskAi, hasKeywordSearch, mergeLangFacetFilters, @@ -192,5 +193,110 @@ describe('client/theme-default/support/docsearch', () => { ) expect(result.searchParameters?.facetFilters).toEqual(['lang:en']) }) + + test('preserves Agent Studio search parameters by index', () => { + const result = buildAskAiConfig( + { + assistantId: 'assistant123', + agentStudio: true, + searchParameters: { + index: { + distinct: false + } + } + } as any, + { + appId: 'app', + apiKey: 'key', + indexName: 'index', + searchParameters: { + facetFilters: ['tag:docs'] + } + } as any, + 'en' + ) + + expect(result.searchParameters).toEqual({ + index: { + distinct: false + } + }) + expect(result.searchParameters).not.toHaveProperty('facetFilters') + }) + + test('does not add legacy facet filters to Agent Studio config', () => { + const result = buildAskAiConfig( + { + assistantId: 'assistant123', + agentStudio: true + } as any, + { + appId: 'app', + apiKey: 'key', + indexName: 'index', + searchParameters: { + facetFilters: ['tag:docs'] + } + } as any, + 'en' + ) + + expect(result.searchParameters).toBeUndefined() + }) + }) + + describe('buildSidePanelProps', () => { + test('passes resolved Ask AI options to the side panel', () => { + const result = buildSidePanelProps( + { + assistantId: 'assistant123', + agentStudio: true, + searchParameters: { + index: { + facetFilters: ['lang:en'] + } + }, + suggestedQuestions: true, + useStagingEnv: true, + sidePanel: { + button: { + variant: 'inline' + }, + panel: { + width: 420, + suggestedQuestions: true + } + } + } as any, + { + appId: 'app', + apiKey: 'key', + indexName: 'index' + } as any + ) + + expect(result).toEqual({ + container: '#vp-docsearch-sidepanel', + appId: 'app', + apiKey: 'key', + indexName: 'index', + assistantId: 'assistant123', + agentStudio: true, + searchParameters: { + index: { + facetFilters: ['lang:en'] + } + }, + suggestedQuestions: true, + useStagingEnv: true, + button: { + variant: 'inline' + }, + panel: { + width: 420, + suggestedQuestions: true + } + }) + }) }) }) diff --git a/__tests__/unit/client/theme-default/support/sidebar.test.ts b/__tests__/unit/client/theme-default/support/sidebar.test.ts index f33dcbd9..c5a4d44f 100644 --- a/__tests__/unit/client/theme-default/support/sidebar.test.ts +++ b/__tests__/unit/client/theme-default/support/sidebar.test.ts @@ -1,4 +1,8 @@ -import { getSidebar, hasActiveLink } from 'client/theme-default/support/sidebar' +import { + getFlatSideBarLinks, + getSidebar, + hasActiveLink +} from 'client/theme-default/support/sidebar' describe('client/theme-default/support/sidebar', () => { describe('getSidebar', () => { @@ -103,6 +107,75 @@ describe('client/theme-default/support/sidebar', () => { ) }) }) + + test('applies base only to internal links', () => { + expect( + getSidebar( + { + '/en/': { + base: '/en/', + items: [ + { + text: 'Guide', + items: [ + { text: 'Intro', link: 'intro' }, + { text: 'Root', link: '/root' }, + { text: 'External', link: 'https://example.com/' } + ] + } + ] + } + }, + '/en/intro' + ) + ).toStrictEqual([ + { + text: 'Guide', + items: [ + { text: 'Intro', link: '/en/intro' }, + { text: 'Root', link: '/en/root' }, + { text: 'External', link: 'https://example.com/' } + ] + } + ]) + }) + }) + + describe('getFlatSideBarLinks', () => { + test('flattens nested items and preserves link metadata', () => { + const sidebar = [ + { + text: 'Group', + items: [ + { text: 'Intro', link: '/intro' }, + { + text: 'External', + link: 'https://example.com/', + target: '_self', + rel: 'noopener', + docFooterText: 'Go external' + } + ] + } + ] + + expect(getFlatSideBarLinks(sidebar)).toStrictEqual([ + { + text: 'Intro', + link: '/intro', + docFooterText: undefined, + rel: undefined, + target: undefined + }, + { + text: 'External', + link: 'https://example.com/', + docFooterText: 'Go external', + rel: 'noopener', + target: '_self' + } + ]) + }) }) describe('hasActiveLink', () => { @@ -115,8 +188,8 @@ describe('client/theme-default/support/sidebar', () => { ] } - expect(hasActiveLink('active-1', item)).toBe(true) - expect(hasActiveLink('inactive', item)).toBe(false) + expect(hasActiveLink('active-1', '', item)).toBe(true) + expect(hasActiveLink('inactive', '', item)).toBe(false) }) test('checks `SidebarItem[]`', () => { @@ -137,9 +210,9 @@ describe('client/theme-default/support/sidebar', () => { } ] - expect(hasActiveLink('active-1', item)).toBe(true) - expect(hasActiveLink('active-3', item)).toBe(true) - expect(hasActiveLink('inactive', item)).toBe(false) + expect(hasActiveLink('active-1', '', item)).toBe(true) + expect(hasActiveLink('active-3', '', item)).toBe(true) + expect(hasActiveLink('inactive', '', item)).toBe(false) }) }) }) diff --git a/__tests__/unit/node/config.test.ts b/__tests__/unit/node/config.test.ts new file mode 100644 index 00000000..df4af72c --- /dev/null +++ b/__tests__/unit/node/config.test.ts @@ -0,0 +1,73 @@ +import type { MarkdownItAsync } from 'markdown-it-async' +import { mergeConfig, type UserConfig } from 'node/config' + +describe('node/config', () => { + test('merges markdown hooks from extended configs', async () => { + const calls: string[] = [] + const md = {} as MarkdownItAsync + + const merged = mergeConfig( + { + markdown: { + lineNumbers: true, + preConfig() { + calls.push('base-pre') + }, + config() { + calls.push('base') + } + } + }, + { + markdown: { + attrs: { + allowed: ['id'] + }, + async preConfig() { + calls.push('extended-pre') + }, + async config() { + calls.push('extended') + } + } + } + ) + + expect(merged.markdown?.lineNumbers).toBe(true) + expect(merged.markdown?.attrs).toEqual({ + allowed: ['id'] + }) + + await merged.markdown?.preConfig?.(md) + await merged.markdown?.config?.(md) + + expect(calls).toEqual(['base-pre', 'extended-pre', 'base', 'extended']) + }) + + test('keeps one-sided markdown hooks when the other config omits them', async () => { + const calls: string[] = [] + const md = {} as MarkdownItAsync + + const merged = mergeConfig( + { + markdown: { + preConfig() { + calls.push('base-pre') + } + } + }, + { + markdown: { + config() { + calls.push('extended') + } + } + } + ) + + await merged.markdown?.preConfig?.(md) + await merged.markdown?.config?.(md) + + expect(calls).toEqual(['base-pre', 'extended']) + }) +}) diff --git a/__tests__/unit/node/contentLoader.test.ts b/__tests__/unit/node/contentLoader.test.ts new file mode 100644 index 00000000..197745f6 --- /dev/null +++ b/__tests__/unit/node/contentLoader.test.ts @@ -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') + }) +}) diff --git a/__tests__/unit/node/markdown/markdown.test.ts b/__tests__/unit/node/markdown/markdown.test.ts new file mode 100644 index 00000000..5db12abd --- /dev/null +++ b/__tests__/unit/node/markdown/markdown.test.ts @@ -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(' { + 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('
') + expect(enabled).toContain('class="copy"') + + const disabled = await render(src, { preWrapper: false }) + expect(disabled).not.toContain('
') + 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('<<< ./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\nmore' + const enabled = await render(src) + expect(enabled).toContain('

\n

') + + const disabled = await render(src, { component: false }) + expect(disabled).toContain('

text\n\nmore

') + }) + + 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('「テスト」') + expect(await render(src, { cjkFriendlyEmphasis: false })).not.toContain( + '' + ) + }) + + 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(' { + 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('') + expect(await render('`code`{.cls}')).toContain('class="cls"') + expect(await render('text {.cls}')).toContain('

') + expect(await render('- item\n{.cls}')).toContain('

    ') + expect(await render('| a |\n| --- |\n| b |\n\n{.cls}')).toContain( + ' { + test('renders checkboxes with their checked state', async () => { + const html = await render('- [ ] todo\n- [x] done') + expect(html).toContain('
      ') + expect(html).toContain('
    • ') + + const inputs = html.match(/]*>/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(' { + 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"' + ) + } + }) +}) diff --git a/__tests__/unit/node/markdown/plugins/containers.test.ts b/__tests__/unit/node/markdown/plugins/containers.test.ts new file mode 100644 index 00000000..18e69025 --- /dev/null +++ b/__tests__/unit/node/markdown/plugins/containers.test.ts @@ -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 +) { + 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(` + "

      TIP

      +

      content of tip

      +
      +

      INFO

      +

      content of info

      +
      +

      WARNING

      +

      content of warning

      +
      +

      DANGER

      +

      content of danger

      +
      +

      NOTE

      +

      content of note

      +
      +

      IMPORTANT

      +

      content of important

      +
      +

      CAUTION

      +

      content of caution

      +
      + " + `) + }) + + test('renders details as a disclosure with summary', async () => { + expect(await render('::: details\nhidden content\n:::')) + .toMatchInlineSnapshot(` + "
      Details +

      hidden content

      +
      + " + `) + }) + + 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(` + "

      STOP

      +

      Danger zone, do not proceed

      +
      +

      A bold title with code

      +

      content

      +
      +
      Click me to toggle the code +
      js
      console.log('hi')
      +      
      +
      + " + `) + }) + + test('resolves reference links in titles', async () => { + const src = [ + '::: tip See [the guide][guide]', + 'content', + ':::', + '', + '[guide]: /guide/' + ].join('\n') + expect(await render(src)).toMatchInlineSnapshot(` + "

      See the guide

      +

      content

      +
      + " + `) + }) + + 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(` + "

      提示

      +

      提示内容

      +
      +
      详细信息 +

      详情内容

      +
      + " + `) + }) + + 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(` + "

      SUCCESS

      +

      You have completed the walkthrough!

      +
      +
      +

      content

      +
      + " + `) + }) + + 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(` + "
      Click me +

      content

      +
      +

      Custom

      +

      content

      +
      + " + `) + }) + + 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(` + "

      Custom

      +

      content

      +
      + " + `) + }) + + 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(` + "
      +

      content

      +
      +
      +

      content

      +
      +
      Details +

      still needs its summary

      +
      + " + `) + }) + + 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('
      ') + + const allowed = await render( + '::: tip Custom {.extra-class data-x=1}\ncontent\n:::', + { attrs: { allowed: ['class'] } } + ) + expect(allowed).toContain('
      ') + 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(` + "
      Click me {open} +

      content

      +
      + " + `) + }) + + 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 `
      `', + ':::' + ].join('\n') + expect(await render(src)).toMatchInlineSnapshot(` + "
      +

      {{ this will be displayed as-is }}

      +
      +
      +

      Wraps in a <div class="vp-raw">

      +
      + " + `) + }) + + 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(` + "
      +
      js
      const a = 1
      +      
      +
      ts
      const a: number = 1
      +      
      +
      + " + `) + }) + + test('supports nesting via longer fences', async () => { + const src = [ + ':::: info Outer', + 'outer content', + '', + '::: details Inner', + 'inner content', + ':::', + '::::' + ].join('\n') + expect(await render(src)).toMatchInlineSnapshot(` + "

      Outer

      +

      outer content

      +
      Inner +

      inner content

      +
      +
      + " + `) + }) + + test('auto-closes unclosed containers', async () => { + expect(await render('::: warning\nno closing fence')) + .toMatchInlineSnapshot(` + "

      WARNING

      +

      no closing fence

      +
      + " + `) + }) + + test('parses fences without a space before the name', async () => { + expect(await render(':::tip\ncontent\n:::')).toMatchInlineSnapshot(` + "

      TIP

      +

      content

      +
      + " + `) + }) + + test('leaves non-container fence lines alone', async () => { + expect(await render('::: unknown\ncontent\n:::')).toMatchInlineSnapshot(` + "

      ::: unknown + content + :::

      + " + `) + }) +}) + +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(` + "

      NOTE

      +

      note content

      +
      +

      TIP

      +

      tip content

      +
      +

      IMPORTANT

      +

      important content

      +
      +

      WARNING

      +

      warning content

      +
      +

      CAUTION

      +

      caution content

      +
      + " + `) + }) + + test('matches markers case-insensitively', async () => { + expect(await render('> [!tip]\n> content')).toMatchInlineSnapshot(` + "

      TIP

      +

      content

      +
      + " + `) + }) + + test('supports custom titles after the marker', async () => { + expect(await render('> [!WARNING] Custom Title\n> content')) + .toMatchInlineSnapshot(` + "

      Custom Title

      +

      content

      +
      + " + `) + }) + + test('respects custom labels from container options', async () => { + expect( + await render('> [!TIP]\n> content', { container: { tipLabel: '提示' } }) + ).toMatchInlineSnapshot(` + "

      提示

      +

      content

      +
      + " + `) + }) + + 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(` + "

      SUCCESS

      +

      done

      +
      +

      With title

      +

      done

      +
      + " + `) + }) + + 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(` + "

      NOTE

      +

      first paragraph + lazy continuation

      +
        +
      • list item
      • +
      +
      js
      const a = 1
      +      
      +
      + " + `) + }) + + test('converts markers without content', async () => { + expect(await render('> [!NOTE]')).toMatchInlineSnapshot(` + "

      NOTE

      +

      +
      + " + `) + }) + + 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(` + "
      +

      just a quote

      +
      +
      +

      [!FOO] + not an alert

      +
      +

      paragraph [!NOTE] not at blockquote start

      + " + `) + }) + + test('can be disabled via gfmAlerts: false', async () => { + expect(await render('> [!NOTE]\n> content', { gfmAlerts: false })) + .toMatchInlineSnapshot(` + "
      +

      [!NOTE] + content

      +
      + " + `) + }) +}) + +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(` + "

      提示

      +

      内容

      +
      +
      详细信息 +

      内容

      +
      +

      成功

      +

      内容

      +
      + " + `) + }) + + 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(` + "

      ROOT TIP

      +

      content

      +
      + " + `) + }) + + test('explicit titles win over locale defaults', async () => { + expect( + await render('::: tip Custom Title\n内容\n:::', options, { + localeIndex: 'zh' + }) + ).toMatchInlineSnapshot(` + "

      Custom Title

      +

      内容

      +
      + " + `) + }) + + test('resolves alert titles for the active locale', async () => { + expect( + await render('> [!TIP]\n> 内容\n\n> [!SUCCESS]\n> 内容', options, { + localeIndex: 'zh' + }) + ).toMatchInlineSnapshot(` + "

      提示

      +

      内容

      +
      +

      成功

      +

      内容

      +
      + " + `) + }) + + 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!"') + }) +}) diff --git a/__tests__/unit/node/markdown/plugins/image.test.ts b/__tests__/unit/node/markdown/plugins/image.test.ts new file mode 100644 index 00000000..c6a346cc --- /dev/null +++ b/__tests__/unit/node/markdown/plugins/image.test.ts @@ -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( + `"

      logo

      "` + ) + }) + + 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"') + }) + }) +}) diff --git a/__tests__/unit/node/markdown/plugins/include.test.ts b/__tests__/unit/node/markdown/plugins/include.test.ts new file mode 100644 index 00000000..053bd58e --- /dev/null +++ b/__tests__/unit/node/markdown/plugins/include.test.ts @@ -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 = {} + ) { + 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\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( + '\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( + '\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( + '\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('\n') + expect(relative.html).toContain('C-content') + + const rooted = await render('\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\r\n') + expect(html).toContain('B-content') + }) + + test('expands nested includes with relative resolution', async () => { + await write( + 'sub/inside.md', + 'inside\n\n\n' + ) + await write('sub/subsub/deep.md', 'deep-content\n') + + const { html, env } = await render('\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\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\n') + await write('b.md', 'B-content\n\n\n') + + const { html } = await render( + 'A-content\n\n\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\n') + await write('c.md', 'C-content\n\n\n') + await write('d.md', 'D-content\n') + + const { html } = await render( + '\n\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('\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('\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-content', + '', + 'outside-content', + '' + ].join('\n') + ) + + const { html } = await render('\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', + [ + '', + 'first', + '', + 'outside', + '', + 'second', + '', + '' + ].join('\n') + ) + + const { html } = await render('\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', + [ + '', + 'one', + 'two', + 'three', + '', + '' + ].join('\n') + ) + + const { html } = await render('\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('\n') + expect(from.html).toContain('two') + expect(from.html).toContain('three') + expect(from.html).not.toContain('one') + + const to = await render('\n') + expect(to.html).toContain('one') + expect(to.html).toContain('two') + expect(to.html).not.toContain('three') + + const both = await render('\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('\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('\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\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\n```\n' + ) + expect(html).toContain('region line') + expect(html).not.toContain('outside line') + }) + + test('leaves empty include paths untouched', async () => { + const { html } = await render('\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('\n') + expect(html).toContain('@include: ./b.md') + }) + + test('can be disabled', async () => { + await write('b.md', 'B-content\n') + + const { html } = await render('\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('\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('\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('\n')).rejects.toThrow( + /range/i + ) + await expect(render('\n')).rejects.toThrow( + /range/i + ) + await expect(render('\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\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('\n', { + include: { silent: true } + }) + expect(region.html).not.toContain('@include') + + const range = await render('\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('\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( + '\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\n\n![oneimg](./one.png)\n' + ) + await write('b/two.md', '![twoimg](./two.png)\n') + + const { html } = await render('\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
      \ntail\n
      \n' + ) + + const { html } = await render( + '\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\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 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('\n') + expect(html).toContain('href="https://example.com/x"') + expect(html).toContain('href="/abs/target.html"') + }) +}) diff --git a/__tests__/unit/node/markdown/plugins/link.test.ts b/__tests__/unit/node/markdown/plugins/link.test.ts index e5e09978..5e4bc7f1 100644 --- a/__tests__/unit/node/markdown/plugins/link.test.ts +++ b/__tests__/unit/node/markdown/plugins/link.test.ts @@ -48,4 +48,17 @@ describe('node/markdown/plugins/link', () => { 'href="/foo.html?title=Cat&oldid=916388819#:~:text=Claws-,Like%20almost,the%20Felidae%2C,-cats"' ) }) + + test('records source line numbers for collected links', async () => { + const env: { + cleanUrls: boolean + links?: string[] + linkLines?: number[] + } = { cleanUrls: false } + + await md.renderAsync('Intro\n\n[Missing](./missing.md)\n', env) + + expect(env.links).toEqual(['./missing']) + expect(env.linkLines).toEqual([3]) + }) }) diff --git a/__tests__/unit/node/markdown/plugins/snippet.test.ts b/__tests__/unit/node/markdown/plugins/snippet.test.ts index ba4b343e..50f389c1 100644 --- a/__tests__/unit/node/markdown/plugins/snippet.test.ts +++ b/__tests__/unit/node/markdown/plugins/snippet.test.ts @@ -1,8 +1,13 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' import { - dedent, - findRegion, - rawPathToToken -} from 'node/markdown/plugins/snippet' + createMarkdownRenderer, + disposeMdItInstance, + type MarkdownOptions +} from 'node/markdown/markdown' +import { parseSnippetPath } from 'node/markdown/plugins/snippet' +import type { MarkdownEnv } from 'node/shared' const removeEmptyKeys = >(obj: T) => { return Object.fromEntries( @@ -11,7 +16,8 @@ const removeEmptyKeys = >(obj: T) => { } /* prettier-ignore */ -const rawPathTokenMap: [string, Partial<{ filepath: string, extension: string, title: string, region: string, lines: string, lang: string }>][] = [ +const parseSnippetPathMap: [string, Partial<{ filepath: string, extension: string, title: string, region: string, lines: string, lang: string, attrs: string }>][] = [ + // paths may contain spaces and dots, and the title defaults to the file name ['/path/to/file.extension', { filepath: '/path/to/file.extension', extension: 'extension', title: 'file.extension' }], ['./path/to/file.extension', { filepath: './path/to/file.extension', extension: 'extension', title: 'file.extension' }], ['/path to/file.extension', { filepath: '/path to/file.extension', extension: 'extension', title: 'file.extension' }], @@ -28,298 +34,368 @@ const rawPathTokenMap: [string, Partial<{ filepath: string, extension: string, t ['./path.to/file', { filepath: './path.to/file', title: 'file' }], ['/path .to/file', { filepath: '/path .to/file', title: 'file' }], ['./path .to/file', { filepath: './path .to/file', title: 'file' }], - ['/path/to/file.extension#region', { filepath: '/path/to/file.extension', extension: 'extension', title: 'file.extension', region: '#region' }], + // the extension comes from the file name, so dots in directories and + // dotfiles resolve, and it is not lowercased + ['/path/to/.extension', { filepath: '/path/to/.extension', extension: 'extension', title: '.extension' }], + ['/path/.to/file.extension', { filepath: '/path/.to/file.extension', extension: 'extension', title: 'file.extension' }], + ['/path/.to/.extension', { filepath: '/path/.to/.extension', extension: 'extension', title: '.extension' }], + ['/path/.to/file', { filepath: '/path/.to/file', title: 'file' }], + ['./script.ps1', { filepath: './script.ps1', extension: 'ps1', title: 'script.ps1' }], + ['./File.TS', { filepath: './File.TS', extension: 'TS', title: 'File.TS' }], + // suffixes that are not alphanumeric are not treated as an extension, so + // the language has to be given explicitly for these + ['./main.c++', { filepath: './main.c++', title: 'main.c++' }], + ['./main.c++ {c++}', { filepath: './main.c++', title: 'main.c++', lang: 'c++' }], + ['@/.vscode/scss.code-snippets', { filepath: '@/.vscode/scss.code-snippets', title: 'scss.code-snippets' }], + // region names may contain dots, dashes, digits and underscores + ['/path/to/file.extension#region', { filepath: '/path/to/file.extension', extension: 'extension', title: 'file.extension', region: 'region' }], + ['./file.ts#my.region', { filepath: './file.ts', extension: 'ts', title: 'file.ts', region: 'my.region' }], + ['./file.ts#complex-name_123', { filepath: './file.ts', extension: 'ts', title: 'file.ts', region: 'complex-name_123' }], + // inside the braces: optional highlight lines, then the language override + // (which may contain special characters), then attributes ['./path/to/file.extension {c#}', { filepath: './path/to/file.extension', extension: 'extension', title: 'file.extension', lang: 'c#' }], + ['./path/to/file {C++}', { filepath: './path/to/file', title: 'file', lang: 'C++' }], ['/path to/file.extension {1,2,4-6}', { filepath: '/path to/file.extension', extension: 'extension', title: 'file.extension', lines: '1,2,4-6' }], ['/path to/file.extension {1,2,4-6 c#}', { filepath: '/path to/file.extension', extension: 'extension', title: 'file.extension', lines: '1,2,4-6', lang: 'c#' }], + ['./file.ts{1 ts:line-numbers}', { filepath: './file.ts', extension: 'ts', title: 'file.ts', lines: '1', lang: 'ts:line-numbers' }], + // everything after the language is kept verbatim, so several attributes and + // quoted values with spaces reach the fence info + ['./file.ts{1,2 ts twoslash}', { filepath: './file.ts', extension: 'ts', title: 'file.ts', lines: '1,2', lang: 'ts', attrs: 'twoslash' }], + ['./file.ts{ts twoslash noext}', { filepath: './file.ts', extension: 'ts', title: 'file.ts', lang: 'ts', attrs: 'twoslash noext' }], + ['./file.ts{1 ts key="a b" twoslash}', { filepath: './file.ts', extension: 'ts', title: 'file.ts', lines: '1', lang: 'ts', attrs: 'key="a b" twoslash' }], + // a lone word in the braces is the language, not an attribute + ['./file.ts{twoslash}', { filepath: './file.ts', extension: 'ts', title: 'file.ts', lang: 'twoslash' }], + // stray whitespace in the braces is tolerated + ['./file.ts{ ts twoslash }', { filepath: './file.ts', extension: 'ts', title: 'file.ts', lang: 'ts', attrs: 'twoslash' }], + ['./file.ts{1, 2}', { filepath: './file.ts', extension: 'ts', title: 'file.ts', lines: '1,2' }], + ['./file.ts{}', { filepath: './file.ts', extension: 'ts', title: 'file.ts' }], + // an explicit title overrides the file name and may itself contain brackets ['/path.to/file.extension [title]', { filepath: '/path.to/file.extension', extension: 'extension', title: 'title' }], - ['./path.to/file.extension#region {c#}', { filepath: './path.to/file.extension', extension: 'extension', title: 'file.extension', region: '#region', lang: 'c#' }], - ['/path/to/file#region {1,2,4-6}', { filepath: '/path/to/file', title: 'file', region: '#region', lines: '1,2,4-6' }], - ['./path/to/file#region {1,2,4-6 c#}', { filepath: './path/to/file', title: 'file', region: '#region', lines: '1,2,4-6', lang: 'c#' }], + ['./path.to/file.extension#region {c#}', { filepath: './path.to/file.extension', extension: 'extension', title: 'file.extension', region: 'region', lang: 'c#' }], + ['/path/to/file#region {1,2,4-6}', { filepath: '/path/to/file', title: 'file', region: 'region', lines: '1,2,4-6' }], + ['./path/to/file#region {1,2,4-6 c#}', { filepath: './path/to/file', title: 'file', region: 'region', lines: '1,2,4-6', lang: 'c#' }], ['/path to/file {1,2,4-6 c#} [title]', { filepath: '/path to/file', title: 'title', lines: '1,2,4-6', lang: 'c#' }], - ['./path to/file#region {1,2,4-6 c#} [title]', { filepath: './path to/file', title: 'title', region: '#region', lines: '1,2,4-6', lang: 'c#' }], + ['./path to/file#region {1,2,4-6 c#} [title]', { filepath: './path to/file', title: 'title', region: 'region', lines: '1,2,4-6', lang: 'c#' }], + ['./file.ts#region{1,2 ts twoslash} [my title]', { filepath: './file.ts', extension: 'ts', title: 'my title', region: 'region', lines: '1,2', lang: 'ts', attrs: 'twoslash' }], + ['./snippet.js [title [with brackets]]', { filepath: './snippet.js', extension: 'js', title: 'title [with brackets]' }], + // the space before the title is optional + ['./foo.js[custom]', { filepath: './foo.js', extension: 'js', title: 'custom' }], + ['./demo.js{1,3}[Demo]', { filepath: './demo.js', extension: 'js', title: 'Demo', lines: '1,3' }], + ['./demo.js#region[Demo]', { filepath: './demo.js', extension: 'js', title: 'Demo', region: 'region' }], + ['@/src/ExampleMod.java{15-21}[java]', { filepath: '@/src/ExampleMod.java', extension: 'java', title: 'java', lines: '15-21' }], + // windows-style separators resolve the file name the same way + ['..\\path to\\file.extension', { filepath: '..\\path to\\file.extension', extension: 'extension', title: 'file.extension' }], + ['C:\\path\\file.ts#region {1 ts}', { filepath: 'C:\\path\\file.ts', extension: 'ts', title: 'file.ts', region: 'region', lines: '1', lang: 'ts' }] ] describe('node/markdown/plugins/snippet', () => { - describe('dedent', () => { - test('when 0-level is minimal, do not remove spaces', () => { - expect( - dedent( - [ - // - 'fn main() {', - ' println!("Hello");', - '}' - ].join('\n') - ) - ).toMatchInlineSnapshot(` - "fn main() { - println!("Hello"); - }" - `) + describe('parseSnippetPath', () => { + test.each(parseSnippetPathMap)('%s', (rawPath, parsed) => { + expect(removeEmptyKeys(parseSnippetPath(rawPath))).toEqual(parsed) }) + }) + + describe('rendering', () => { + let root: string + let warnings: string[] + + const logger = { + warn: (msg: string) => { + warnings.push(msg) + } + } - test('when 4-level is minimal, remove 4 spaces', () => { - expect( - dedent( - [ - // - ' let a = {', - ' value: 42', - ' };' - ].join('\n') - ) - ).toMatchInlineSnapshot(` - "let a = { - value: 42 - };" - `) + beforeEach(async () => { + root = await mkdtemp(path.join(tmpdir(), 'vitepress-snippet-')) + warnings = [] }) - test('when only 1 line is passed, dedent it', () => { - expect(dedent(' let a = 42;')).toEqual('let a = 42;') + afterEach(async () => { + await rm(root, { recursive: true, force: true }) }) - test('handle tabs as well', () => { - expect( - dedent( - [ - // - ' let a = {', - ' value: 42', - ' };' - ].join('\n') - ) - ).toMatchInlineSnapshot(` - "let a = { - value: 42 - };" - `) + 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 = {} + ) { + 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('imports a whole file, deriving language and title', async () => { + await write('snip.ts', 'const a = 1\nconst b = 2\n') + + const { html, env } = await render('<<< ./snip.ts') + expect(html).toContain('language-ts') + expect(html).toContain('const a = 1') + expect(html).toContain('const b = 2') + expect(env.includes).toEqual([path.join(root, 'snip.ts')]) }) - }) - describe('rawPathToToken', () => { - test.each(rawPathTokenMap)('%s', (rawPath, token) => { - expect(removeEmptyKeys(rawPathToToken(rawPath))).toEqual(token) + test('resolves @ against srcDir', async () => { + await write('nested/snip.js', 'const nested = 1\n') + + const { html } = await render( + '<<< @/nested/snip.js', + {}, + { path: path.join(root, 'sub/dir/index.md') } + ) + expect(html).toContain('const nested = 1') }) - }) - describe('findRegion', () => { - it('returns null when no region markers are present', () => { - const lines = ['function foo() {', ' console.log("hello");', '}'] - expect(findRegion(lines, 'foo')).toBeNull() + 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') }) - it('ignores non-matching region names', () => { - const lines = [ - '// #region regionA', - 'some code here', - '// #endregion regionA' - ] - expect(findRegion(lines, 'regionC')).toBeNull() + test('parses a snippet without a space after the marker', async () => { + await write('snip.ts', 'const a = 1\n') + + const { html } = await render('<<<./snip.ts') + expect(html).toContain('const a = 1') }) - it('returns null if a region start marker exists without a matching end marker', () => { - const lines = [ - '// #region missingEnd', - 'console.log("inside region");', - 'console.log("still inside");' - ] - expect(findRegion(lines, 'missingEnd')).toBeNull() + test('does not dedent whole-file imports', async () => { + await write('indented.ts', ' const a = 1\n const b = 2\n') + + const { html } = await render('<<< ./indented.ts') + expect(html).toContain(' const a = 1') }) - it('returns null if an end marker exists without a preceding start marker', () => { - const lines = [ - '// #endregion ghostRegion', - 'console.log("stray end marker");' - ] - expect(findRegion(lines, 'ghostRegion')).toBeNull() + test('passes attrs to the highlighter and keeps them out of the title', async () => { + await write('snip.ts', 'const a = 1\nconst b = 2\n') + + const calls: { lang: string; attrs: string }[] = [] + await render('<<< ./snip.ts{1 ts twoslash} [my title]', { + highlight: (code, lang, attrs) => { + calls.push({ lang, attrs }) + return code + } + }) + + expect(calls).toHaveLength(1) + expect(calls[0].lang).toBe('ts') + expect(calls[0].attrs).toContain('twoslash') + expect(calls[0].attrs).toContain('{1}') + expect(calls[0].attrs).not.toContain('my title') }) - it('detects C#/JavaScript style region markers with matching tags', () => { - const lines = [ - 'Console.WriteLine("Before region");', - '#region hello', - 'Console.WriteLine("Hello, World!");', - '#endregion hello', - 'Console.WriteLine("After region");' - ] - const result = findRegion(lines, 'hello') - expect(result).not.toBeNull() - if (result) { - expect(lines.slice(result.start, result.end).join('\n')).toBe( - 'Console.WriteLine("Hello, World!");' - ) - } + test('resolves relative paths against the real file path', async () => { + await write('sub/snip.js', 'const real = 1\n') + + const { html } = await render( + '<<< ./snip.js', + {}, + { + path: path.join(root, 'rewritten/index.md'), + realPath: path.join(root, 'sub/index.md') + } + ) + expect(html).toContain('const real = 1') }) - it('detects region markers even when the end marker omits the region name', () => { - const lines = [ - 'Console.WriteLine("Before region");', - '#region hello', - 'Console.WriteLine("Hello, World!");', - '#endregion', - 'Console.WriteLine("After region");' - ] - const result = findRegion(lines, 'hello') - expect(result).not.toBeNull() - if (result) { - expect(lines.slice(result.start, result.end).join('\n')).toBe( - 'Console.WriteLine("Hello, World!");' - ) - } + test('concatenates all regions with the requested name', async () => { + await write( + 'regions.ts', + [ + '// #region one', + 'const a = 1', + '// #endregion one', + 'const outside = 2', + '// #region one', + 'const b = 3', + '// #endregion', + '' + ].join('\n') + ) + + const { html } = await render('<<< ./regions.ts#one') + expect(html).toContain('const a = 1') + expect(html).toContain('const b = 3') + expect(html).not.toContain('const outside') }) - it('handles indented region markers correctly', () => { - const lines = [ - ' Console.WriteLine("Before region");', - ' #region hello', - ' Console.WriteLine("Hello, World!");', - ' #endregion hello', - ' Console.WriteLine("After region");' - ] - const result = findRegion(lines, 'hello') - expect(result).not.toBeNull() - if (result) { - expect(lines.slice(result.start, result.end).join('\n')).toBe( - ' Console.WriteLine("Hello, World!");' - ) - } + test('dedents extracted regions', async () => { + await write( + 'indent.ts', + [ + 'function f() {', + ' // #region inner', + ' const x = 1', + ' // #endregion inner', + '}', + '' + ].join('\n') + ) + + const { html } = await render('<<< ./indent.ts#inner') + expect(html).toContain('const x = 1') + expect(html).not.toContain(' const x = 1') }) - it('detects TypeScript style region markers', () => { - const lines = [ - 'let regexp: RegExp[] = [];', - '// #region foo', - 'let start = -1;', - '// #endregion foo' - ] - const result = findRegion(lines, 'foo') - expect(result).not.toBeNull() - if (result) { - expect(lines.slice(result.start, result.end).join('\n')).toBe( - 'let start = -1;' - ) - } + const nested = [ + '// #region outer', + 'let a = 1', + '// #region nested', + 'let b = 2', + '// #endregion nested', + '/* #region css */', + 'let c = 3', + '/* #endregion css */', + '// #endregion outer', + '' + ].join('\n') + + test('strips markers of the matched style by default', async () => { + await write('nested.ts', nested) + + const region = await render('<<< ./nested.ts#outer') + // the double-slash markers matched the region, the css ones did not + expect(region.html).not.toContain('#region nested') + expect(region.html).toContain('#region css') + expect(region.html).toContain('let b = 2') + expect(region.html).toContain('let c = 3') + + // whole-file imports keep their markers + const whole = await render('<<< ./nested.ts') + expect(whole.html).toContain('#region outer') + expect(whole.html).toContain('let a = 1') }) - it('detects CSS style region markers', () => { - const lines = [ - '.body-content {', - '/* #region foo */', - ' padding-left: 15px;', - '/* #endregion foo */', - ' padding-right: 15px;', - '}' - ] - const result = findRegion(lines, 'foo') - expect(result).not.toBeNull() - if (result) { - expect(lines.slice(result.start, result.end).join('\n')).toBe( - ' padding-left: 15px;' - ) - } + test('strips every marker style with stripRegionMarkers: all', async () => { + await write('nested.ts', nested) + + const region = await render('<<< ./nested.ts#outer', { + snippet: { stripRegionMarkers: 'all' } + }) + expect(region.html).not.toContain('#region') + expect(region.html).toContain('let b = 2') + + const whole = await render('<<< ./nested.ts', { + snippet: { stripRegionMarkers: 'all' } + }) + expect(whole.html).not.toContain('#region') + expect(whole.html).toContain('let a = 1') }) - it('detects HTML style region markers', () => { - const lines = [ - '
      Some content
      ', - '', - '

      Hello world

      ', - '', - '
      Other content
      ' - ] - const result = findRegion(lines, 'foo') - expect(result).not.toBeNull() - if (result) { - expect(lines.slice(result.start, result.end).join('\n')).toBe( - '

      Hello world

      ' - ) - } + test('keeps marker lines with stripRegionMarkers: false', async () => { + await write('nested.ts', nested) + + const region = await render('<<< ./nested.ts#outer', { + snippet: { stripRegionMarkers: false } + }) + expect(region.html).toContain('#region nested') + expect(region.html).toContain('#region css') }) - it('detects Visual Basic style region markers (with case-insensitive "End")', () => { - const lines = [ - 'Console.WriteLine("VB")', - '#Region VBRegion', - ' Console.WriteLine("Inside region")', - '#End Region VBRegion', - 'Console.WriteLine("Done")' - ] - const result = findRegion(lines, 'VBRegion') - expect(result).not.toBeNull() - if (result) { - expect(lines.slice(result.start, result.end).join('\n')).toBe( - ' Console.WriteLine("Inside region")' - ) - } + test('applies lang, highlight lines, attrs and title to the fence', async () => { + await write('snip.ts', 'const a = 1\nconst b = 2\n') + + const { html } = await render( + '::: code-group\n\n<<< ./snip.ts{1 js twoslash} [custom title]\n\n:::' + ) + expect(html).toContain('language-js') + expect(html).toContain('custom title') + expect(html).not.toContain('twoslash') }) - it('detects Bat style region markers', () => { - const lines = ['::#region foo', 'echo off', '::#endregion foo'] - const result = findRegion(lines, 'foo') - expect(result).not.toBeNull() - if (result) { - expect(lines.slice(result.start, result.end).join('\n')).toBe( - 'echo off' - ) - } + test('throws when the file is missing', async () => { + await expect(render('<<< ./missing.ts')).rejects.toThrow( + /Code snippet path not found/ + ) }) - it('detects C/C++ style region markers using #pragma', () => { - const lines = [ - '#pragma region foo', - 'int a = 1;', - '#pragma endregion foo' - ] - const result = findRegion(lines, 'foo') - expect(result).not.toBeNull() - if (result) { - expect(lines.slice(result.start, result.end).join('\n')).toBe( - 'int a = 1;' - ) - } + test('throws when the path is a directory', async () => { + await write('dir/file.ts', 'const a = 1\n') + await expect(render('<<< ./dir')).rejects.toThrow(/directory/) }) - it('returns the first complete region when multiple regions exist', () => { - const lines = [ - '// #region foo', - 'first region content', - '// #endregion foo', - '// #region foo', - 'second region content', - '// #endregion foo' - ] - const result = findRegion(lines, 'foo') - expect(result).not.toBeNull() - if (result) { - expect(lines.slice(result.start, result.end).join('\n')).toBe( - 'first region content' - ) - } + test('throws when the region is missing', async () => { + await write('snip.ts', 'const a = 1\n') + await expect(render('<<< ./snip.ts#nope')).rejects.toThrow( + /region "nope" not found/i + ) }) - it('handles nested regions with different names properly', () => { - const lines = [ - '// #region foo', - "console.log('line before nested');", - '// #region bar', - "console.log('nested content');", - '// #endregion bar', - '// #endregion foo' - ] - const result = findRegion(lines, 'foo') - expect(result).not.toBeNull() - if (result) { - const extracted = lines.slice(result.start, result.end).join('\n') - const expected = [ - "console.log('line before nested');", - '// #region bar', - "console.log('nested content');", - '// #endregion bar' - ].join('\n') - expect(extracted).toBe(expected) + test('silent mode renders nothing and warns', async () => { + await write('snip.ts', 'const a = 1\n') + + const missingFile = await render('<<< ./missing.ts', { + snippet: { silent: true } + }) + expect(missingFile.html).not.toContain(' { + const escaped = await render('\\<<< ./snip.ts') + expect(escaped.html).toContain('<<< ./snip.ts') + + const indented = await render(' <<< ./snip.ts') + expect(indented.html).toContain('<<< ./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') }) }) }) diff --git a/__tests__/unit/node/markdown/regions.test.ts b/__tests__/unit/node/markdown/regions.test.ts new file mode 100644 index 00000000..f14930bc --- /dev/null +++ b/__tests__/unit/node/markdown/regions.test.ts @@ -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 = [ + '', + '

      Hello world

      ', + '', + '', + '

      Other

      ', + '' + ] + expect(extract(lines, 'foo')).toBe('

      Hello world

      ') + expect(extract(lines, 'bar')).toBe('

      Other

      ') + }) + + 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 = [ + '', + '', + '' + ] + const regions = findRegions(lines, 'shared') + expect(regions).toHaveLength(4) + expect(extract(lines, 'shared')).toBe( + [ + '
      template part
      ', + '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 = [ + '', + '', + '', + '' + ] + expect(extract(lines, 'demo')).toBe( + [ + '', + '' + ].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 = [ + '', + 'Use markers like this:', + '', + '```js', + '// #region foo', + 'const a = 1', + '// #endregion', + '```', + '' + ] + 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 = [ + '', + 'How regions work:', + '', + '```js', + '// #region example', + 'const a = 1', + '```', + '' + ] + 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 = [ + '', + '// #endregion', + 'body', + '' + ] + 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' + ]) + }) + }) +}) diff --git a/__tests__/unit/node/markdownToVue.test.ts b/__tests__/unit/node/markdownToVue.test.ts new file mode 100644 index 00000000..1d675f31 --- /dev/null +++ b/__tests__/unit/node/markdownToVue.test.ts @@ -0,0 +1,155 @@ +import { resolveConfig } from 'node/config' +import { createMarkdownToVueRenderFn } from 'node/markdownToVue' +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' + +describe('node/markdownToVue', () => { + let root: string | undefined + + afterEach(async () => { + if (root) { + await rm(root, { recursive: true, force: true }) + root = undefined + } + }) + + test('records source line numbers for dead links', async () => { + root = await mkdtemp(path.join(tmpdir(), 'vitepress-dead-link-')) + + const file = path.join(root, 'index.md') + const src = '# Home\n\nIntro\n\n[Missing](./missing.md)\n' + await writeFile(file, src) + + const siteConfig = await resolveConfig(root, 'build', 'production') + const render = await createMarkdownToVueRenderFn( + siteConfig.srcDir, + { cache: false }, + '/', + false, + false, + siteConfig + ) + + const result = await render(src, file) + + expect(result.deadLinks).toContainEqual({ + url: './missing', + file, + line: 5 + }) + }) + + test('records source line numbers after frontmatter', async () => { + root = await mkdtemp(path.join(tmpdir(), 'vitepress-dead-link-')) + + const file = path.join(root, 'index.md') + const src = + '---\ntitle: Home\n---\n# Home\n\nIntro\n\n[Missing](./missing.md)\n' + await writeFile(file, src) + + const siteConfig = await resolveConfig(root, 'build', 'production') + const render = await createMarkdownToVueRenderFn( + siteConfig.srcDir, + { cache: false }, + '/', + false, + false, + siteConfig + ) + + const result = await render(src, file) + + expect(result.deadLinks).toContainEqual({ + url: './missing', + file, + line: 8 + }) + }) + + test('selects included heading sections after frontmatter', async () => { + root = await mkdtemp(path.join(tmpdir(), 'vitepress-include-')) + + const file = path.join(root, 'index.md') + const source = path.join(root, 'source.md') + await writeFile( + source, + [ + '---', + 'description: Source description', + '---', + '# Intro', + '', + 'intro text', + '', + '## Shared', + '', + 'shared before target', + '', + '## Target', + '', + 'target text', + '', + '### Child', + '', + 'child text', + '', + '## Shared', + '', + 'shared after target', + '' + ].join('\n') + ) + const src = '' + await writeFile(file, src) + + const siteConfig = await resolveConfig(root, 'build', 'production') + const render = await createMarkdownToVueRenderFn( + siteConfig.srcDir, + { cache: false }, + '/', + false, + false, + siteConfig + ) + + const result = await render(src, file) + + expect(result.vueSrc).toContain('

      target text

      ') + expect(result.vueSrc).toContain('

      child text

      ') + expect(result.vueSrc).not.toContain('Source description') + expect(result.vueSrc).not.toContain('intro text') + expect(result.vueSrc).not.toContain('shared before target') + expect(result.vueSrc).not.toContain('shared after target') + }) + + test('applies rewrites with mismatched Windows drive letter case', async () => { + root = await mkdtemp(path.join(tmpdir(), 'vitepress-rewrite-')) + + const file = path.join(root, 'index.md') + await writeFile(file, '# Home\n') + + const siteConfig = await resolveConfig(root, 'build', 'production') + siteConfig.srcDir = 'c:/site/docs' + siteConfig.pages = ['en/index.md'] + siteConfig.rewrites = { + map: { 'en/index.md': 'index.md' }, + inv: { 'index.md': 'en/index.md' } + } + ;(siteConfig as any).__dirty = true + + const render = await createMarkdownToVueRenderFn( + siteConfig.srcDir, + { cache: false }, + '/', + false, + false, + siteConfig + ) + + const result = await render('# Home\n', 'C:/site/docs/en/index.md') + + expect(result.pageData.relativePath).toBe('index.md') + }) +}) diff --git a/__tests__/unit/node/plugins/localSearchPlugin.test.ts b/__tests__/unit/node/plugins/localSearchPlugin.test.ts new file mode 100644 index 00000000..ffbf3e46 --- /dev/null +++ b/__tests__/unit/node/plugins/localSearchPlugin.test.ts @@ -0,0 +1,239 @@ +import MiniSearch from 'minisearch' +import { resolveConfig } from 'node/config' +import { disposeMdItInstance } from 'node/markdown/markdown' +import { createMarkdownToVueRenderFn } from 'node/markdownToVue' +import { localSearchPlugin } from 'node/plugins/localSearchPlugin' +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' + +describe('node/plugins/localSearchPlugin', () => { + let root: string | undefined + let nodeEnv: string | undefined + + beforeEach(() => { + nodeEnv = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + // createMarkdownRenderer is a module-level singleton keyed on nothing — + // reset it so each test's config actually reaches the renderer + disposeMdItInstance() + }) + + afterEach(async () => { + if (nodeEnv === undefined) { + delete process.env.NODE_ENV + } else { + process.env.NODE_ENV = nodeEnv + } + + if (root) { + await rm(root, { recursive: true, force: true }) + root = undefined + } + }) + + test('indexes rewritten pages by rewritten locale path', async () => { + root = await mkdtemp(path.join(tmpdir(), 'vitepress-local-search-')) + const configDir = path.join(root, '.vitepress') + await mkdir(configDir) + + await writeFile( + path.join(root, 'index.md'), + '# English home\n\nrootonlytoken\n' + ) + await writeFile( + path.join(root, 'zh.md'), + '# Chinese home\n\nlocaleonlytoken\n' + ) + await writeFile( + path.join(configDir, 'config.ts'), + [ + 'export default {', + ' rewrites: {', + " 'index.md': 'guide.md',", + " 'zh.md': 'zh/guide.md'", + ' },', + ' locales: {', + " root: { label: 'English', lang: 'en' },", + " zh: { label: 'Chinese', lang: 'zh' }", + ' },', + ' themeConfig: {', + " search: { provider: 'local' }", + ' }', + '}' + ].join('\n') + ) + + const siteConfig = await resolveConfig(root, 'build', 'production') + const plugin = await localSearchPlugin(siteConfig) + + // vite calls configResolved before any other hook + await (plugin.configResolved as any)?.call( + {}, + { publicDir: siteConfig.publicDir } + ) + + const indexModule = (await (plugin.load as any)?.handler.call( + {}, + '/@localSearchIndex' + )) as string + + expect(indexModule).toContain( + '"root": () => import(\'@localSearchIndexroot\')' + ) + expect(indexModule).toContain('"zh": () => import(\'@localSearchIndexzh\')') + + const rootIndex = loadIndex( + (await (plugin.load as any)?.handler.call( + {}, + '/@localSearchIndexroot' + )) as string + ) + const zhIndex = loadIndex( + (await (plugin.load as any)?.handler.call( + {}, + '/@localSearchIndexzh' + )) as string + ) + + expect(rootIndex.search('rootonlytoken')).toMatchObject([ + { id: '/guide.html#english-home' } + ]) + expect(rootIndex.search('localeonlytoken')).toEqual([]) + + expect(zhIndex.search('localeonlytoken')).toMatchObject([ + { id: '/zh/guide.html#chinese-home' } + ]) + expect(zhIndex.search('rootonlytoken')).toEqual([]) + }) + + test('applies per-locale markdown options (#5350)', async () => { + root = await mkdtemp(path.join(tmpdir(), 'vitepress-local-search-')) + const configDir = path.join(root, '.vitepress') + await mkdir(configDir) + await mkdir(path.join(root, 'zh')) + + const content = '::: tip\ntipcontent\n:::\n' + await writeFile(path.join(root, 'index.md'), `# Home\n\n${content}`) + await writeFile( + path.join(root, 'zh', 'index.md'), + `# Chinese home\n\n${content}` + ) + await writeFile( + path.join(configDir, 'config.ts'), + [ + 'export default {', + ' locales: {', + " root: { label: 'English', lang: 'en' },", + ' zh: {', + " label: 'Chinese',", + " lang: 'zh',", + " markdown: { container: { tipLabel: 'zhtiplabel' } }", + ' }', + ' },', + ' themeConfig: {', + " search: { provider: 'local' }", + ' }', + '}' + ].join('\n') + ) + + const siteConfig = await resolveConfig(root, 'build', 'production') + const plugin = await localSearchPlugin(siteConfig) + + // configResolved hooks run concurrently, so the search plugin may be the + // one that creates the shared markdown renderer — page renders must still + // pick up per-locale options from it + await (plugin.configResolved as any)?.call( + {}, + { publicDir: siteConfig.publicDir } + ) + + const render = await createMarkdownToVueRenderFn( + siteConfig.srcDir, + siteConfig.markdown ?? {}, + siteConfig.site.base, + false, + false, + siteConfig + ) + + const rootFile = path.join(root, 'index.md') + const zhFile = path.join(root, 'zh', 'index.md') + const rootPage = await render(await readFile(rootFile, 'utf-8'), rootFile) + const zhPage = await render(await readFile(zhFile, 'utf-8'), zhFile) + expect(rootPage.vueSrc).toContain('TIP') + expect(zhPage.vueSrc).toContain('zhtiplabel') + + // the indexed text must use the localized labels too + await (plugin.load as any)?.handler.call({}, '/@localSearchIndex') + const rootIndex = loadIndex( + (await (plugin.load as any)?.handler.call( + {}, + '/@localSearchIndexroot' + )) as string + ) + const zhIndex = loadIndex( + (await (plugin.load as any)?.handler.call( + {}, + '/@localSearchIndexzh' + )) as string + ) + expect(zhIndex.search('zhtiplabel')).toHaveLength(1) + expect(rootIndex.search('zhtiplabel')).toEqual([]) + expect(rootIndex.search('tip')).toHaveLength(1) + }) + + test('warns and skips pages that fail to render', async () => { + root = await mkdtemp(path.join(tmpdir(), 'vitepress-local-search-')) + const configDir = path.join(root, '.vitepress') + await mkdir(configDir) + + await writeFile(path.join(root, 'index.md'), '# Home\n\nhealthytoken\n') + await writeFile( + path.join(root, 'broken.md'), + '# Broken\n\n\n' + ) + await writeFile( + path.join(configDir, 'config.ts'), + "export default { themeConfig: { search: { provider: 'local' } } }" + ) + + const siteConfig = await resolveConfig(root, 'build', 'production') + const warn = vi + .spyOn(siteConfig.logger, 'warn') + .mockImplementation(() => {}) + const plugin = await localSearchPlugin(siteConfig) + + await (plugin.configResolved as any)?.call( + {}, + { publicDir: siteConfig.publicDir } + ) + + // the include throws, but indexing the remaining pages must still resolve — + // in dev this runs unawaited and a rejection would take the server down + await (plugin.load as any)?.handler.call({}, '/@localSearchIndex') + + const index = loadIndex( + (await (plugin.load as any)?.handler.call( + {}, + '/@localSearchIndexroot' + )) as string + ) + + expect(index.search('healthytoken')).toHaveLength(1) + expect(warn).toHaveBeenCalledWith( + expect.stringContaining('Failed to index broken.md for search') + ) + }) +}) + +function loadIndex(serializedModule: string) { + const serializedIndex = JSON.parse( + serializedModule.slice('export default '.length) + ) + return MiniSearch.loadJSON(serializedIndex, { + fields: ['title', 'titles', 'text'], + storeFields: ['title', 'titles'] + }) +} diff --git a/__tests__/unit/node/utils/fs.test.ts b/__tests__/unit/node/utils/fs.test.ts new file mode 100644 index 00000000..7072a471 --- /dev/null +++ b/__tests__/unit/node/utils/fs.test.ts @@ -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') + }) +}) diff --git a/__tests__/unit/tsconfig.json b/__tests__/unit/tsconfig.json new file mode 100644 index 00000000..f7ff2329 --- /dev/null +++ b/__tests__/unit/tsconfig.json @@ -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"] + } + } +} diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index b8093713..1be8a317 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -9,6 +9,13 @@ import { localIconLoader } from 'vitepress-plugin-group-icons' import llmstxt from 'vitepress-plugin-llms' +import { markdown as esMarkdown } from '../es/config' +import { markdown as faMarkdown } from '../fa/config' +import { markdown as jaMarkdown } from '../ja/config' +import { markdown as koMarkdown } from '../ko/config' +import { markdown as ptMarkdown } from '../pt/config' +import { markdown as ruMarkdown } from '../ru/config' +import { markdown as zhMarkdown } from '../zh/config' const prod = !!process.env.NETLIFY const siteUrl = 'https://vitepress.dev' @@ -35,7 +42,6 @@ export default defineConfig({ lastUpdated: true, cleanUrls: true, - metaChunk: true, markdown: { math: true, @@ -51,35 +57,6 @@ export default defineConfig({ } ], config(md) { - // TODO: remove when https://github.com/vuejs/vitepress/issues/4431 is fixed - const fence = md.renderer.rules.fence! - md.renderer.rules.fence = function (tokens, idx, options, env, self) { - const { localeIndex = 'root' } = env - const codeCopyButtonTitle = (() => { - switch (localeIndex) { - case 'es': - return 'Copiar código' - case 'fa': - return 'کپی کد' - case 'ko': - return '코드 복사' - case 'pt': - return 'Copiar código' - case 'ru': - return 'Скопировать код' - case 'zh': - return '复制代码' - case 'ja': - return 'コードをコピー' - default: - return 'Copy code' - } - })() - return fence(tokens, idx, options, env, self).replace( - '', - `` - ) - } md.use(groupIconMdPlugin) } }, @@ -122,15 +99,16 @@ export default defineConfig({ carbonAds: { code: 'CEBDT27Y', placement: 'vuejsorg' } }, + // prettier-ignore locales: { root: { label: 'English', lang: 'en-US', dir: 'ltr' }, - zh: { label: '简体中文', lang: 'zh-Hans', dir: 'ltr' }, - pt: { label: 'Português', lang: 'pt-BR', dir: 'ltr' }, - ru: { label: 'Русский', lang: 'ru-RU', dir: 'ltr' }, - es: { label: 'Español', lang: 'es', dir: 'ltr' }, - ko: { label: '한국어', lang: 'ko-KR', dir: 'ltr' }, - fa: { label: 'فارسی', lang: 'fa-IR', dir: 'rtl' }, - ja: { label: '日本語', lang: 'ja', dir: 'ltr' } + zh: { label: '简体中文', lang: 'zh-Hans', dir: 'ltr', markdown: zhMarkdown }, + pt: { label: 'Português', lang: 'pt-BR', dir: 'ltr', markdown: ptMarkdown }, + ru: { label: 'Русский', lang: 'ru-RU', dir: 'ltr', markdown: ruMarkdown }, + es: { label: 'Español', lang: 'es', dir: 'ltr', markdown: esMarkdown }, + ko: { label: '한국어', lang: 'ko-KR', dir: 'ltr', markdown: koMarkdown }, + fa: { label: 'فارسی', lang: 'fa-IR', dir: 'rtl', markdown: faMarkdown }, + ja: { label: '日本語', lang: 'ja', dir: 'ltr', markdown: jaMarkdown } }, vite: { @@ -145,10 +123,7 @@ export default defineConfig({ } }), prod && llmstxt({ workDir: 'en', ignoreFiles: ['index.md'] }) - ], - experimental: { - enableNativePlugin: true - } + ] }, // prettier-ignore diff --git a/docs/.vitepress/theme/styles.css b/docs/.vitepress/theme/styles.css index 2635e80d..54c5d424 100644 --- a/docs/.vitepress/theme/styles.css +++ b/docs/.vitepress/theme/styles.css @@ -1,6 +1,27 @@ +@font-face { + font-family: 'Vazirmatn'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('https://cdn.jsdelivr.net/npm/vazirmatn@33.0.3/fonts/webfonts/Vazirmatn%5Bwght%5D.woff2') + format('woff2'); + /* google fonts' arabic range, plus guillemets and ellipsis so persian + punctuation renders in vazirmatn instead of inter */ + unicode-range: + U+00AB, U+00BB, U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, + U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+2026, U+204F, U+2E41, + U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, + U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, + U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, + U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, + U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, + U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, + U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1; +} + :root:where(:lang(fa)) { --vp-font-family-base: - 'Vazirmatn', 'Inter', ui-sans-serif, system-ui, sans-serif, + 'Vazirmatn', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; } @@ -36,8 +57,31 @@ padding: 18px; } -/* used in reference/default-theme-search */ -img[src='/search.png'] { - width: 100%; - aspect-ratio: 1 / 1; +.VPFeature .icon span { + display: inline-block; + width: 1em; + height: 1em; + background-position: center; + background-repeat: no-repeat; + background-size: contain; + + &.memo { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cpath fill='%23efd8b1' d='M0 20.3S28.2 50.2 31.2 64c0 0 8.1-17.9 32.8-25.4c0 0-8.8-20.6-32.2-37c0 0-23.9 7.2-31.8 18.7'/%3E%3Cpath fill='%23fff6d7' d='M3.2 7.9s22 38.2 22.4 52.5c0 0 11-14.8 36.4-14c0 0-4.8-22.9-24.6-46.3c0-.1-24.5-.9-34.2 7.8'/%3E%3Cg fill='%237d8b91'%3E%3Cpath d='M19.9 9.4c-.3.4.2-.4 0 0'/%3E%3Cpath d='M19.9 9.4c.2-.3.3-.8.6-1.1c.4-.3.8-.1 1.2-.4c.8-.6 1.2-1.6 1.9-2.3c.8-.8 1.8-1.3 2.9-1.6c1.1-.4 2.2-1.2 3.3-1.2c-.5 1.5-1.5 2.8-1.7 4.5c-.1.9 0 1.8.1 2.7c.2-.7.4-1.4.6-2c.4-.9 1.3-.8 2.1-1.1c.8-.4 1.5-1.1 2.2-1.7c.3.5 1.4-.3 1.6.4c.1.3 2.8-.1 3-.2c-.6 0-2.7.1-2.8-.4c-.3-.1-.9-.2-1.3-.2v-.7c-.6.4-1.2.9-1.8 1.4c-.9.7-2 .5-2.9 1.2c.3-1 .9-1.9 1.3-2.9c.2-.4.3-.9.4-1.3s0-.2-.3-.4c-1.4-.4-2.8.7-4 1.2c-1.1.5-2.1 1.1-2.9 1.9c-.5.5-1.3 2.3-2 2.5c-1.4 0-.9 1.3-1.8 1.7c-.5.2-.8.2-1.3.5c-.5.4-1.2 1.1-2 1c0-.6 0-1.2-.2-1.8c-.8.4-1.1 1.1-1.8 1.6c.2-1.6.9-2.9.9-4.6c-.9.6-1.4 1.3-1.9 2.2c-.6 1.1-1.1 2.9-2.3 3.5c0-1.5.7-3 .5-4.6c-.3.8-.3 1.8-.5 2.5c-.2.9-.4 1.8-.4 2.8c2.2-.3 2.6-3.5 3.9-4.9c-.3 1.4-.8 2.7-.7 4.2c.9-.5 1.3-1.5 2.1-2.1c.3.9-.4 2.1 1.1 1.4c.6-.3 1.1-.8 1.7-1.2c.3-.2.9 0 1.2-.5m21.5 4.4c-.4-.4-1.1-.5-1.6-.9c-.6-.5-1.6-.3-2.2-.1c-1.6.4-2.7.8-4.2 0c0 .5.1 2.5-.7 2.4c-.7 0-.5-2.1-.6-2.8c-.4.6-.7 1.1-.7 1.8c0 .5.2.8-.2 1.3c-.6.5-1.5.4-2.2.3c-.2-1.1.1-2.4.2-3.6c-.2 1-.5 2-.7 3.1c-.2.8-.9.9-1.5.7c-.1-.2.1-1 .1-1.2c-.6 1.1-1 2.5-1.8 3.5c-.4.5-1.3 1-1.7.2c-.3-.5-.1-1.2 0-1.8c-.5.6-1 2.3-1.8 2.3c.1-.4.4-.8-.1-1c-.2.4-.4.9-.7 1.4c.2-.6.4-1.3.5-2c-1.3.8-2.1 1.9-2.8 3.2c0-.2.1-.7.1-.9c-.1.4-.9 1.9-.4 2.2c.7-1.1 1.3-2.3 2.3-3.2c-.5 1.5-1.1 2.8-1.4 4.3c.6-.7 1.2-1.5 1.7-2.3c.1-.3.3-.6.4-.9c.3-.8.3-.2.9-.6c.5-.4.8-1.1 1.1-1.7c-.1 1.8 1.4 2.1 2.4.7c.4-.6.7-1.2 1-1.8l.8.2c.4 0 .7 0 1-.2c1.1.4 2.8.5 3.5-.8c1.2 1.3 2-.7 2.1-1.7c1.2.5 2.3-.1 3.5-.5c.7-.2 2-.8 2.5 0c.2.3.7.3 1.2.4M30.8 24.1s.1 0 0 0m0 0c0-.4-.3-.6-.7-.3c-.5.4-.2-.3-.2-.7c0-.5.1-.9.1-1.4c-.2.4-.5.8-.7 1.2c-.4.7-.3 2-1.3 2.1c.2-.5.3-1 .4-1.5c-.5.5-1.1.8-1.1 1.6c0 1-1 3.4-2.3 3.1c.1-2 1.2-3.8 1.7-5.7c-2 1.2-3.7 4.2-3.9 6.4c1-1.7 1.7-3.6 3.1-5c-.4 1.5-.9 2.9-1 4.4c0 .3.8.1.9.1c.7-.2 1.2-.9 1.5-1.5c.1-.2.3-.5.4-.7c.3-.6.4-.2.9-.5c.7-.3.7-1.2 1.4-1.1s2.2-.1 2.7-.4c-.6 0-1.2.1-1.9-.1m-.5-.4l.2.2c-.1 0-.2-.1-.2-.2m13.8 18.7c.5-.7-.2-.1 0 0'/%3E%3Cpath d='M58.4 39.7c-1.8-1.3-3.9-2.1-5.9-3.2c.4.6 1 1.3 1.3 2c-1 .1-.9 1.9-1 1.9c-.5-.3-1.9 0-2.5.1c0-.1 0-.2.1-.3c-1.8 1.2-3.3 3-5.7 2.5c1.1-1.7 2-3.6 1.9-5.7c-1.8.6-2.8 2.3-3.6 3.9c-.2-.6-1-1.1-1.5-.6c-.6.5-.9 1.5-1.1 2.2c-.3.9-.4 1.9-.5 2.8c0 .7-.1.8.6.8c1.7 0 3.2-2 4-3.2c2.2.6 3.5-.4 5.2-1.6c0 .8.6 1.3 1.4 1.1s1.2-1.2 1.8-1.2c.5 0 .8.2 1.3.2s1.1-.1 1.6-.2c-.6-.7-.8-1.2-1-2.1c1.2.3 2.3.6 3.6.6m-16.2 4.9c.2-.2.3-.3 0 0c-.3.2-.2.2-.1.1c-.5.4-1.1.9-1.8.9c0-1.5.2-3.3.9-4.6c.4-.6.9-.8 1.5-.3c.4.4-.1.8-.1 1.2c.1-.2.2-.4.3-.5c.1.6.4 1 1 1.3c-.5.6-1 1.3-1.7 1.9m1.9-2.2c-2.4-1 .7-4.1 2.1-4.7c-.2 1.7-1.1 3.3-2.1 4.7m6.9-.8c-.8.2-.8-.6-.7-1.1c.5.2 1 .4 1.6.5c-.2.3-.5.6-.9.6m2.7-.7v-.1zm.2-.6c.1-.1.1-.3.1-.4c-.3.1-.5.2-.8.3c0-.3-.1-1.1.3-1.2c.8-.4.8.9.9 1.4c-.2 0-.4 0-.5-.1m.9-1.8h-.2c-.1-.1 0-.1.2 0'/%3E%3C/g%3E%3Cpath fill='%23ffce31' d='m35.658 20.801l16.124-16.12l7.565 7.567l-16.124 16.12z'/%3E%3Cpath fill='%23ed4c5c' d='m62.6 2.3l-1-1c-1.8-1.8-4.8-1.8-6.6 0l-3.3 3.3l7.6 7.6l3.3-3.3c1.9-1.8 1.9-4.7 0-6.6'/%3E%3Cpath fill='%2393a2aa' d='m49.703 6.679l2.05-2.05l7.566 7.567l-2.051 2.05z'/%3E%3Cpath fill='%23c7d3d8' d='m50.552 7.527l2.05-2.05l5.94 5.939l-2.05 2.05z'/%3E%3Cpath fill='%23fed0ac' d='m35.6 20.8l-3.3 8.6l2.3 2.3l8.6-3.3z'/%3E%3Cpath fill='%23333' d='M31.8 30.9c-.5 1.2.2 1.8 1.3 1.3l4.2-1.6l-3.9-3.9z'/%3E%3Cpath fill='%23ffdf85' d='M35.672 20.82L49.744 6.75l2.545 2.545l-14.071 14.071z'/%3E%3Cpath fill='%23ff8736' d='m40.656 25.869l14.07-14.074l2.545 2.546l-14.07 14.073z'/%3E%3C/svg%3E"); + } + + &.rocket { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cg fill='%23ff9d27'%3E%3Cpath d='M10.9 48.7c4-4 4.4-5 6.9-2.5s1.5 2.8-2.5 6.9c-3 3-6.8 2.4-6.8 2.4s-.6-3.8 2.4-6.8'/%3E%3Cpath d='M18.5 52.8c1.6-4.2 2.1-4.7-.2-6s-2.3-.4-3.8 3.8c-1.2 3.1.2 5.9.2 5.9s2.7-.5 3.8-3.7'/%3E%3C/g%3E%3Cpath fill='%23fdf516' d='M16.2 48.9c.9-2.3.9-2.8 2.1-2.1c1.3.7 1 1 .1 3.3c-.6 1.7-2.1 2.1-2.1 2.1s-.7-1.5-.1-3.3'/%3E%3Cpath fill='%23ff9d27' d='M17.1 45.7c-1.3-2.3-1.8-1.8-6-.2c-3.1 1.2-3.7 3.8-3.7 3.8s2.8 1.4 5.9.2c4.2-1.6 5.1-1.6 3.8-3.8'/%3E%3Cg fill='%23fdf516'%3E%3Cpath d='M15 47.8c2.3-.9 2.8-.9 2.1-2.1c-.7-1.3-1-1-3.3-.1c-1.7.6-2.1 2.1-2.1 2.1s1.6.7 3.3.1'/%3E%3Cpath d='M13.9 47.6c2.2-2.2 2.4-2.8 3.8-1.4s.8 1.6-1.4 3.8c-1.7 1.7-3.8 1.3-3.8 1.3s-.2-2 1.4-3.7'/%3E%3C/g%3E%3Cpath fill='%233baacf' d='M18.5 38C12.3 27.6 2 31.9 2 31.9s14.7-14.7 24.6-4.8z'/%3E%3Cpath fill='%23428bc1' d='m23.3 30.3l3.2-3.2C16.7 17.2 2 31.9 2 31.9s12.9-9.2 21.3-1.6'/%3E%3Cpath fill='%233baacf' d='M26 45.5C36.4 51.7 32.1 62 32.1 62s14.7-14.7 4.8-24.6z'/%3E%3Cpath fill='%23428bc1' d='m33.7 40.7l3.2-3.2c9.9 9.9-4.8 24.6-4.8 24.6s9.2-13 1.6-21.4'/%3E%3Cpath fill='%23c5d0d8' d='M48.8 30.9C37.1 42.5 24.2 48.8 19.7 44.3s1.8-17.4 13.4-29.1c13.6-13.6 28.7-13 28.7-13s.5 15.1-13 28.7'/%3E%3Cpath fill='%23dae3ea' d='M45.8 27.6C34.2 39.2 22.6 46.8 19.9 44.1s4.9-14.3 16.5-25.9C50 4.6 62 2 62 2s-2.6 12-16.2 25.6'/%3E%3Cpath fill='%23c94747' d='M24.3 47.5c-.5.5-1.3.5-1.8 0l-6-6c-.5-.5-.5-1.4 0-1.9l1.8-1.8l7.8 7.8z'/%3E%3Cpath fill='%23f15744' d='M22.6 45.7c-.5.5-1.1.7-1.4.4l-3.4-3.4c-.3-.3-.1-.9.4-1.4l1.8-1.8l4.4 4.4z'/%3E%3Cpath fill='%233e4347' d='M20.9 48.2c-.3.3-1 .3-1.3 0l-3.9-3.9c-.3-.3-.2-.9.1-1.2l1.2-1.2l5.1 5.1z'/%3E%3Cpath fill='%2362727a' d='M20.1 47.4c-.3.3-.9.4-1.1.2l-2.7-2.7c-.2-.2-.1-.7.3-1l1.2-1.2l3.5 3.5z'/%3E%3Cpath fill='%23c94747' d='M61.8 2.2S56.4 2 49.1 4.8l10.1 10.1C62 7.6 61.8 2.2 61.8 2.2'/%3E%3Cpath fill='%23f15744' d='M61.8 2.2s-4.3.9-10.8 4.6l6.2 6.2c3.7-6.5 4.6-10.8 4.6-10.8'/%3E%3Ccircle cx='43.5' cy='20.5' r='5' fill='%23edf4f9'/%3E%3Ccircle cx='43.5' cy='20.5' r='3.3' fill='%233baacf'/%3E%3Ccircle cx='33.5' cy='30.5' r='5' fill='%23edf4f9'/%3E%3Ccircle cx='33.5' cy='30.5' r='3.3' fill='%233baacf'/%3E%3Cg fill='%23fff'%3E%3Cpath d='M48.9 6.9c-.3.3-.9.3-1.2 0s-.3-.9 0-1.2s.9-.3 1.2 0s.3.9 0 1.2'/%3E%3Ccircle cx='50.6' cy='8.6' r='.8'/%3E%3Ccircle cx='53' cy='11' r='.8'/%3E%3Ccircle cx='55.3' cy='13.4' r='.8'/%3E%3Ccircle cx='57.7' cy='15.7' r='.8'/%3E%3C/g%3E%3C/svg%3E"); + } + + &.vite { + background-image: url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='1 6.8 30 18.4'%3E%3Cmask id='SVGgudELdDz'%3E%3Cpath fill='%23fff' d='M40.05 45.7c-.67.85-2.02.38-2.02-.69v-10.3a2.26 2.26 0 0 0-2.27-2.26H24.4a1.13 1.13 0 0 1-.92-1.8l7.48-10.46c1.07-1.5 0-3.58-1.84-3.58H15.34a1.13 1.13 0 0 1-.92-1.79l9.7-13.57c.2-.3.55-.48.92-.48h28.89c.92 0 1.46 1.04.92 1.79l-7.48 10.47a2.26 2.26 0 0 0 1.84 3.58H60.6c.94 0 1.47 1.09.89 1.83z'/%3E%3C/mask%3E%3Cg fill='none'%3E%3Cg mask='url(%23SVGgudELdDz)' transform='translate(1 6.8)scale(.393)'%3E%3Cpath fill='%239135ff' d='M0 0h62v47H0z'/%3E%3Cg filter='url(%23SVGNp06lekD)'%3E%3Cellipse cx='24.46' cy='37.75' fill='%23eee6ff' rx='5.51' ry='14.7' transform='rotate(89.8 24.46 37.75)'/%3E%3C/g%3E%3Cg filter='url(%23SVGNp06lekD)'%3E%3Cellipse cx='4.76' cy='18.96' fill='%23eee6ff' rx='10.4' ry='29.85' transform='rotate(89.8 4.76 18.96)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='4.24' cy='17.5' fill='%238900ff' rx='5.51' ry='30.49' transform='rotate(89.8 4.24 17.5)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='8.95' cy='35.5' fill='%238900ff' rx='5.51' ry='30.6' transform='rotate(89.8 8.95 35.5)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='10.48' cy='36.65' fill='%238900ff' rx='5.51' ry='30.6' transform='rotate(89.8 10.48 36.65)'/%3E%3C/g%3E%3Cg filter='url(%23SVGNp06lekD)'%3E%3Cellipse cx='67.34' cy='12.3' fill='%23eee6ff' rx='14.07' ry='22.08' transform='rotate(-86.7 67.34 12.3)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='68.44' cy='15.74' fill='%238900ff' rx='3.47' ry='21.5' transform='rotate(-91 68.44 15.74)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='68.44' cy='15.74' fill='%238900ff' rx='3.47' ry='21.5' transform='rotate(-91 68.44 15.74)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='14.59' cy='9.74' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(39.5 14.6 9.74)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='61.73' cy='-5.32' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(37.9 61.73 -5.32)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='55.62' cy='7.1' fill='%2300c2ff' rx='5.97' ry='9.67' transform='rotate(37.9 55.62 7.1)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='12.33' cy='39.1' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(37.9 12.33 39.1)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='12.33' cy='39.1' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(37.9 12.33 39.1)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='49.86' cy='30.68' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(37.9 49.86 30.68)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='52.62' cy='33.17' fill='%2300c2ff' rx='5.97' ry='15.3' transform='rotate(37.9 52.62 33.17)'/%3E%3C/g%3E%3C/g%3E%3Cpath fill='%2308060e' d='M3.72 6.8C.1 11.98.08 20 3.72 25.2h2.45c-3.64-5.2-3.62-13.22 0-18.4zm24.56 0h-2.45c3.62 5.18 3.64 13.2 0 18.4h2.45c3.64-5.2 3.62-13.22 0-18.4'/%3E%3Cdefs%3E%3Cfilter id='SVGNp06lekD' width='4' height='4' x='-2' y='-2' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='7.66'/%3E%3C/filter%3E%3Cfilter id='SVGQv8P6csY' width='4' height='4' x='-2' y='-2' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='4.6'/%3E%3C/filter%3E%3C/defs%3E%3C/g%3E%3C/svg%3E"); + .dark & { + background-image: url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='1 6.8 30 18.4'%3E%3Cmask id='SVGgudELdDz'%3E%3Cpath fill='%23fff' d='M40.05 45.7c-.67.85-2.02.38-2.02-.69v-10.3a2.26 2.26 0 0 0-2.27-2.26H24.4a1.13 1.13 0 0 1-.92-1.8l7.48-10.46c1.07-1.5 0-3.58-1.84-3.58H15.34a1.13 1.13 0 0 1-.92-1.79l9.7-13.57c.2-.3.55-.48.92-.48h28.89c.92 0 1.46 1.04.92 1.79l-7.48 10.47a2.26 2.26 0 0 0 1.84 3.58H60.6c.94 0 1.47 1.09.89 1.83z'/%3E%3C/mask%3E%3Cg fill='none'%3E%3Cg mask='url(%23SVGgudELdDz)' transform='translate(1 6.8)scale(.393)'%3E%3Cpath fill='%239135ff' d='M0 0h62v47H0z'/%3E%3Cg filter='url(%23SVGNp06lekD)'%3E%3Cellipse cx='24.46' cy='37.75' fill='%23eee6ff' rx='5.51' ry='14.7' transform='rotate(89.8 24.46 37.75)'/%3E%3C/g%3E%3Cg filter='url(%23SVGNp06lekD)'%3E%3Cellipse cx='4.76' cy='18.96' fill='%23eee6ff' rx='10.4' ry='29.85' transform='rotate(89.8 4.76 18.96)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='4.24' cy='17.5' fill='%238900ff' rx='5.51' ry='30.49' transform='rotate(89.8 4.24 17.5)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='8.95' cy='35.5' fill='%238900ff' rx='5.51' ry='30.6' transform='rotate(89.8 8.95 35.5)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='10.48' cy='36.65' fill='%238900ff' rx='5.51' ry='30.6' transform='rotate(89.8 10.48 36.65)'/%3E%3C/g%3E%3Cg filter='url(%23SVGNp06lekD)'%3E%3Cellipse cx='67.34' cy='12.3' fill='%23eee6ff' rx='14.07' ry='22.08' transform='rotate(-86.7 67.34 12.3)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='68.44' cy='15.74' fill='%238900ff' rx='3.47' ry='21.5' transform='rotate(-91 68.44 15.74)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='68.44' cy='15.74' fill='%238900ff' rx='3.47' ry='21.5' transform='rotate(-91 68.44 15.74)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='14.59' cy='9.74' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(39.5 14.6 9.74)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='61.73' cy='-5.32' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(37.9 61.73 -5.32)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='55.62' cy='7.1' fill='%2300c2ff' rx='5.97' ry='9.67' transform='rotate(37.9 55.62 7.1)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='12.33' cy='39.1' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(37.9 12.33 39.1)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='12.33' cy='39.1' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(37.9 12.33 39.1)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='49.86' cy='30.68' fill='%238900ff' rx='4.41' ry='29.11' transform='rotate(37.9 49.86 30.68)'/%3E%3C/g%3E%3Cg filter='url(%23SVGQv8P6csY)'%3E%3Cellipse cx='52.62' cy='33.17' fill='%2300c2ff' rx='5.97' ry='15.3' transform='rotate(37.9 52.62 33.17)'/%3E%3C/g%3E%3C/g%3E%3Cpath fill='%23fff' d='M3.72 6.8C.1 11.98.08 20 3.72 25.2h2.45c-3.64-5.2-3.62-13.22 0-18.4zm24.56 0h-2.45c3.62 5.18 3.64 13.2 0 18.4h2.45c3.64-5.2 3.62-13.22 0-18.4'/%3E%3Cdefs%3E%3Cfilter id='SVGNp06lekD' width='4' height='4' x='-2' y='-2' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='7.66'/%3E%3C/filter%3E%3Cfilter id='SVGQv8P6csY' width='4' height='4' x='-2' y='-2' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='4.6'/%3E%3C/filter%3E%3C/defs%3E%3C/g%3E%3C/svg%3E"); + } + width: 1.3em; + } + + &.vue { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='2 3.92 28 24.15'%3E%3Cpath fill='%2341b883' d='M24.4 3.925H30l-14 24.15L2 3.925h10.71l3.29 5.6l3.22-5.6Z'%3E%3C/path%3E%3Cpath fill='%2341b883' d='m2 3.925l14 24.15l14-24.15h-5.6L16 18.415L7.53 3.925Z'%3E%3C/path%3E%3Cpath fill='%2335495e' d='M7.53 3.925L16 18.485l8.4-14.56h-5.18L16 9.525l-3.29-5.6Z'%3E%3C/path%3E%3C/svg%3E"); + } } diff --git a/docs/config.ts b/docs/config.ts index 8620d955..8f38b2b4 100644 --- a/docs/config.ts +++ b/docs/config.ts @@ -1,4 +1,4 @@ -import { createRequire } from 'module' +import { createRequire } from 'node:module' import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' const require = createRequire(import.meta.url) diff --git a/docs/en/guide/asset-handling.md b/docs/en/guide/asset-handling.md index 00819d1a..63394fd0 100644 --- a/docs/en/guide/asset-handling.md +++ b/docs/en/guide/asset-handling.md @@ -6,7 +6,7 @@ description: Learn how to reference and handle static assets such as images, med ## Referencing Static Assets -All Markdown files are compiled into Vue components and processed by [Vite](https://vitejs.dev/guide/assets.html). You can, **and should**, reference any assets using relative URLs: +All Markdown files are compiled into Vue components and processed by [Vite](https://vite.dev/guide/assets.html). You can, **and should**, reference any assets using relative URLs: ```md ![An image](./image.png) diff --git a/docs/en/guide/custom-theme.md b/docs/en/guide/custom-theme.md index c0ce8597..957c27dc 100644 --- a/docs/en/guide/custom-theme.md +++ b/docs/en/guide/custom-theme.md @@ -62,11 +62,32 @@ import Layout from './Layout.vue' export default { Layout, enhanceApp({ app, router, siteData }) { - // ... + // app.component(...) + // app.use(...) } } ``` +The `enhanceApp` hook allows you to access the [Vue app instance](https://vuejs.org/api/application.html) and other runtime data, which can be used to [register global components](./extending-default-theme.md#registering-global-components), integrate with Vue libraries, etc. + +The `router` value is the same VitePress router instance returned by [`useRouter()`](../reference/runtime-api#userouter). To listen for route changes, assign handlers on the router: + +```ts [.vitepress/theme/index.ts] +export default { + enhanceApp({ router }) { + router.onBeforeRouteChange = (to) => { + console.log('navigating to', to) + } + + router.onAfterRouteChange = (to) => { + console.log('navigated to', to) + } + } +} +``` + +Return `false` from `onBeforeRouteChange` or `onBeforePageLoad` to cancel navigation. + The default export is the only contract for a custom theme, and only the `Layout` property is required. So technically, a VitePress theme can be as simple as a single Vue component. Inside your layout component, it works just like a normal Vite + Vue 3 application. Do note the theme also needs to be [SSR-compatible](./ssr-compat). @@ -208,10 +229,10 @@ Finally, if the theme provides types for its theme config: ```ts [.vitepress/config.ts] import baseConfig from 'awesome-vitepress-theme/config' -import { defineConfigWithTheme } from 'vitepress' +import { defineConfig } from 'vitepress' import type { ThemeConfig } from 'awesome-vitepress-theme' -export default defineConfigWithTheme({ +export default defineConfig({ extends: baseConfig, themeConfig: { // Type is `ThemeConfig` diff --git a/docs/en/guide/deploy.md b/docs/en/guide/deploy.md index 9c01d0ee..b54f8fa4 100644 --- a/docs/en/guide/deploy.md +++ b/docs/en/guide/deploy.md @@ -166,6 +166,13 @@ Don't enable options like _Auto Minify_ for HTML code. It will remove comments f with: node-version: 24 cache: npm # or pnpm / yarn + - name: Cache VitePress + uses: actions/cache@v4 + with: + path: docs/.vitepress/cache + key: ${{ runner.os }}-vitepress-${{ hashFiles('docs/**', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb') }} + restore-keys: | + ${{ runner.os }}-vitepress- - name: Setup Pages uses: actions/configure-pages@v4 - name: Install dependencies diff --git a/docs/en/guide/extending-default-theme.md b/docs/en/guide/extending-default-theme.md index 5ff15f5b..8a109e34 100644 --- a/docs/en/guide/extending-default-theme.md +++ b/docs/en/guide/extending-default-theme.md @@ -119,7 +119,7 @@ export default { } satisfies Theme ``` -Since we are using Vite, you can also leverage Vite's [glob import feature](https://vitejs.dev/guide/features.html#glob-import) to auto register a directory of components. +Since we are using Vite, you can also leverage Vite's [glob import feature](https://vite.dev/guide/features.html#glob-import) to auto register a directory of components. ## Layout Slots @@ -309,7 +309,7 @@ Coming soon. ## Overriding Internal Components -You can use Vite's [aliases](https://vitejs.dev/config/shared-options.html#resolve-alias) to replace default theme components with your custom ones: +You can use Vite's [aliases](https://vite.dev/config/shared-options.html#resolve-alias) to replace default theme components with your custom ones: ```ts import { fileURLToPath, URL } from 'node:url' diff --git a/docs/en/guide/getting-started.md b/docs/en/guide/getting-started.md index e1dc739a..a181192d 100644 --- a/docs/en/guide/getting-started.md +++ b/docs/en/guide/getting-started.md @@ -12,7 +12,7 @@ You can try VitePress directly in your browser on [StackBlitz](https://vitepress ### Prerequisites -- [Node.js](https://nodejs.org/) version 20 or higher. +- [Node.js](https://nodejs.org/) version 22 or higher. - Terminal for accessing VitePress via its command line interface (CLI). - Text Editor with [Markdown](https://en.wikipedia.org/wiki/Markdown) syntax support. - [VSCode](https://code.visualstudio.com/) is recommended, along with the [official Vue extension](https://marketplace.visualstudio.com/items?itemName=Vue.volar). @@ -37,11 +37,15 @@ $ yarn add -D vitepress@next vue $ bun add -D vitepress@next ``` +```sh [deno] +$ deno add -D vitepress@next +``` + ::: ::: tip NOTE -VitePress is an ESM-only package. Don't use `require()` to import it, and make sure your nearest `package.json` contains `"type": "module"`, or change the file extension of your relevant files like `.vitepress/config.js` to `.mjs`/`.mts`. Refer to [Vite's troubleshooting guide](http://vitejs.dev/guide/troubleshooting.html#this-package-is-esm-only) for more details. Also, inside async CJS contexts, you can use `await import('vitepress')` instead. +VitePress is an ESM-only package. Don't use `require()` to import it, and make sure your nearest `package.json` contains `"type": "module"`, or change the file extension of your relevant files like `.vitepress/config.js` to `.mjs`/`.mts`. Refer to [Vite's troubleshooting guide](http://vite.dev/guide/troubleshooting.html#this-package-is-esm-only) for more details. Also, inside async CJS contexts, you can use `await import('vitepress')` instead. ::: diff --git a/docs/en/guide/i18n.md b/docs/en/guide/i18n.md index 8a57bb78..f7f285e6 100644 --- a/docs/en/guide/i18n.md +++ b/docs/en/guide/i18n.md @@ -57,6 +57,37 @@ Refer [`DefaultTheme.Config`](https://github.com/vuejs/vitepress/blob/main/types **Pro tip:** Config file can be stored at `docs/.vitepress/config/index.ts` too. It might help you organize stuff by creating a configuration file per locale and then merge and export them from `index.ts`. +## Per-locale Markdown Strings + +Strings baked into pages by the markdown renderer - the default titles of [custom containers](./markdown#custom-containers) and [GitHub-flavored alerts](./markdown#github-flavored-alerts), and the strings of the code copy button - can be overridden per locale with the `markdown` key of a locale entry: + +```ts [docs/.vitepress/config.ts] +import { defineConfig } from 'vitepress' + +export default defineConfig({ + locales: { + root: { label: 'English', lang: 'en' }, + zh: { + label: '简体中文', + lang: 'zh-Hans', + markdown: { + container: { + tipLabel: '提示', + warningLabel: '警告' + // ...the other labels, and titles of `customContainers` + }, + codeCopyButton: { + tooltipText: '复制代码', + copiedText: '已复制' + } + } + } + } +}) +``` + +Values fall back to the root-level `markdown` options when a locale leaves them unset. Locale entries can only override the titles of containers registered at the root level - registering new containers per locale is not supported. Also note that since the markdown renderer is created once for the whole site, these can only be declared in the main config file, not in additional configs. + ## Separate directory for each locale The following is a perfectly fine structure: diff --git a/docs/en/guide/markdown.md b/docs/en/guide/markdown.md index 505bbb00..06872418 100644 --- a/docs/en/guide/markdown.md +++ b/docs/en/guide/markdown.md @@ -1,5 +1,6 @@ --- description: VitePress built-in Markdown extensions including custom containers, code blocks with syntax highlighting, line highlighting, code groups, and more. +outline: deep --- # Markdown Extensions @@ -100,6 +101,36 @@ For more details, see [Frontmatter](../reference/frontmatter-config). | col 2 is | centered | \$12 | | zebra stripes | are neat | \$1 | +## Task Lists + +**Input** + +```md +- [ ] Write the press release +- [x] Update the website +``` + +**Output** + +- [ ] Write the press release +- [x] Update the website + +## Footnotes + +**Input** + +```md +Footnotes are supported[^1], including inline ones^[This is an inline footnote.]. + +[^1]: Definitions can contain **markdown** and are rendered at the end of the page. +``` + +**Output** + +Footnotes are supported[^1], including inline ones^[This is an inline footnote.]. + +[^1]: Definitions can contain **markdown** and are rendered at the end of the page. + ## Emoji :tada: **Input** @@ -112,7 +143,7 @@ For more details, see [Frontmatter](../reference/frontmatter-config). :tada: :100: -A [list of all emojis](https://github.com/markdown-it/markdown-it-emoji/blob/master/lib/data/full.mjs) is available. +A [list of all emojis](https://github.com/mdit-plugins/mdit-plugins/blob/main/packages/plugin-emoji/src/data/full.ts) is available. ## Table of Contents @@ -229,9 +260,82 @@ export default defineConfig({ }) ``` +On multilingual sites, these labels can also be overridden per locale - see [Per-locale Markdown Strings](./i18n#per-locale-markdown-strings). + +### Registering New Containers + +Beyond the built-in types, you can register additional containers by mapping their names to their default titles: + +```ts +// config.ts +export default defineConfig({ + // ... + markdown: { + container: { + customContainers: { + success: 'SUCCESS' + } + } + } + // ... +}) +``` + +Registered names work like the built-in ones - including custom titles, attributes, and the [GitHub-style alert syntax](#github-flavored-alerts): + +```md +::: success +You have completed the walkthrough! +::: + +> [!SUCCESS] Custom title +> This renders the same way. +``` + +New containers ship without any styling, so add some in your theme using the container name as the class. For this example, the default theme's palette already provides fitting colors: + +```css +/* .vitepress/theme/custom.css */ +.custom-block.success { + border-color: transparent; + color: var(--vp-c-text-1); + background-color: var(--vp-c-success-soft); +} +``` + +### Nesting + +The `:::` markers follow the same rules as fenced code blocks (` ``` `): a fence is only closed by a matching fence that is **at least as long** as the one that opened it. To nest containers (or to mix them with [code groups](#code-groups)) make the outer fence longer than the ones inside it. + +**Input** + +`````md +:::: info Outer container +This box contains another container. + +::: details Inner container +```js +console.log('Hello, VitePress!') +``` +::: +:::: +````` + +**Output** + +:::: info Outer container +This box contains another container. + +::: details Inner container +```js +console.log('Hello, VitePress!') +``` +::: +:::: + ### Additional Attributes -You can add additional attributes to the custom containers. We use [markdown-it-attrs](https://github.com/arve0/markdown-it-attrs) for this feature, and it is supported on almost all markdown elements. For example, you can set the `open` attribute to make the details block open by default: +You can add additional attributes to the custom containers. We use [@mdit/plugin-attrs](https://mdit-plugins.github.io/attrs.html) for this feature, and it is supported on almost all markdown elements. For example, you can set the `open` attribute to make the details block open by default: **Input** @@ -251,9 +355,25 @@ console.log('Hello, VitePress!') ``` ::: +The special `no-title` attribute renders a container without a title element (it has no effect on `details`, which always needs its summary): + +**Input** + +```md +::: tip {no-title} +Just want to try it out? Skip to the [Quickstart](./getting-started). +::: +``` + +**Output** + +::: tip {no-title} +Just want to try it out? Skip to the [Quickstart](./getting-started). +::: + ### `raw` -This is a special container that can be used to prevent style and router conflicts with VitePress. This is especially useful when you're documenting component libraries. You might also wanna check out [whyframe](https://whyframe.dev/docs/integrations/vitepress) for better isolation. +This is a special container that can be used to prevent style and router conflicts with VitePress. This is especially useful when you're documenting component libraries. **Syntax** @@ -291,7 +411,7 @@ Wraps in a `
      ` ## GitHub-flavored Alerts -VitePress also supports [GitHub-flavored alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) to render as callouts. They will be rendered the same as the [custom containers](#custom-containers). +VitePress also supports [GitHub-flavored alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) to render as callouts. They will be rendered the same as the [custom containers](#custom-containers). Unlike on GitHub, text placed right after the marker becomes the title of the alert (`> [!NOTE] Custom Title`), and [containers you registered yourself](#registering-new-containers) work here too. ```md > [!NOTE] @@ -694,6 +814,16 @@ You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/co <<< @/snippets/snippet-with-region.js#snippet{1} +If a file contains multiple regions with the same name, all of them are imported and concatenated — including regions written in different comment styles, such as a `` in the template and a `// #region` in the script of the same Vue SFC. The marker comments delimiting them are removed from the output; set `markdown.snippet.stripRegionMarkers` to `'all'` to also remove markers of other comment styles nested inside the region, or to `false` to keep all of them. + +::: tip +Region names may contain letters, digits, `_`, `-` and `.`. Since the name is taken from the end of the path, a file whose name itself contains a `#` needs an explicit region — write `<<< ./my#file.js#region` rather than `<<< ./my#file.js`. +::: + +::: warning +Importing a file or region that does not exist throws a build error. Set `markdown.snippet.silent: true` to log a warning and render nothing instead. +::: + You can also specify the language inside the braces (`{}`) like this: ```md @@ -708,7 +838,9 @@ You can also specify the language inside the braces (`{}`) like this: <<< @/snippets/snippet.cs{1,2,4-6 c#:line-numbers} ``` -This is helpful if source language cannot be inferred from your file extension. +This is helpful if source language cannot be inferred from your file extension. Only alphanumeric extensions are inferred, so files like `main.c++` or `scss.code-snippets` need the language spelled out this way. + +Anything after the language inside the braces is passed along to the code block as extra attributes — for example, `<<< @/snippets/snippet.ts{ts twoslash}` enables twoslash processing when [`@shikijs/vitepress-twoslash`](https://shiki.style/packages/vitepress#twoslash) is configured. Note that attributes may not contain square brackets. ## Code Groups @@ -916,7 +1048,7 @@ You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/co ``` ::: warning -Note that this does not throw errors if your file is not present. Hence, when using this feature make sure that the contents are being rendered as expected. +Including a missing file, region, heading anchor, or an out-of-range line selection throws a build error. Set `markdown.include.silent: true` to log a warning and skip the inclusion instead. ::: Instead of VS Code regions, you can also use header anchors to include a specific section of the file. For example, if you have a header in your markdown file like this: @@ -966,6 +1098,28 @@ and include it like this: ``` +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 + +``` +```` + +**Output** + +```js + +``` + +Note that the included lines are inserted verbatim (indentation is preserved), and content containing backticks needs a longer outer fence. + ## Math Equations This is currently opt-in. To enable it, you need to install `markdown-it-mathjax3` and set `markdown.math` to `true` in your config file: @@ -1012,14 +1166,14 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$ ## Image Lazy Loading -You can enable lazy loading for each image added via markdown by setting `lazyLoading` to `true` in your config file: +You can enable lazy loading for each image added via markdown by setting `lazyLoad` to `true` in your config file: ```js export default { markdown: { image: { // image lazy loading is disabled by default - lazyLoading: true + lazyLoad: true } } } @@ -1031,15 +1185,15 @@ VitePress uses [markdown-it](https://github.com/markdown-it/markdown-it) as the ```js import { defineConfig } from 'vitepress' -import markdownItAnchor from 'markdown-it-anchor' +import { headerLink } from '@mdit/plugin-anchor' import markdownItFoo from 'markdown-it-foo' export default defineConfig({ markdown: { - // options for markdown-it-anchor - // https://github.com/valeriangalliat/markdown-it-anchor#usage + // options for @mdit/plugin-anchor + // https://mdit-plugins.github.io/anchor.html anchor: { - permalink: markdownItAnchor.permalink.headerLink() + permalink: headerLink() }, // options for @mdit-vue/plugin-toc diff --git a/docs/en/guide/migration-from-vitepress-0.md b/docs/en/guide/migration-from-vitepress-0.md index 29ab9a2a..342ab1ea 100644 --- a/docs/en/guide/migration-from-vitepress-0.md +++ b/docs/en/guide/migration-from-vitepress-0.md @@ -14,7 +14,7 @@ If you're coming from VitePress 0.x version, there're several breaking changes d - `repo`, `repoLabel`, `docsDir`, `docsBranch`, `editLinks`, `editLinkText` are removed in favor of more flexible api. - For adding GitHub link with icon to the nav, use [Social Links](../reference/default-theme-nav#navigation-links) feature. - For adding "Edit this page" feature, use [Edit Link](../reference/default-theme-edit-link) feature. -- `lastUpdated` option is now split into `config.lastUpdated` and `themeConfig.lastUpdatedText`. +- `lastUpdated` option is now split into `config.lastUpdated` and `themeConfig.lastUpdated.text`. - `carbonAds.carbon` is changed to `carbonAds.code`. ## Frontmatter Config diff --git a/docs/en/guide/routing.md b/docs/en/guide/routing.md index 2631c632..c858ec13 100644 --- a/docs/en/guide/routing.md +++ b/docs/en/guide/routing.md @@ -329,7 +329,7 @@ The paths loader module is run in Node.js and only executed during build time. Y Generating paths from local files: ```js -import fs from 'fs' +import fs from 'node:fs' export default { paths() { diff --git a/docs/en/guide/ssr-compat.md b/docs/en/guide/ssr-compat.md index 99171ad6..f532b16a 100644 --- a/docs/en/guide/ssr-compat.md +++ b/docs/en/guide/ssr-compat.md @@ -39,7 +39,7 @@ onMounted(() => { ### Conditional Import -You can also conditionally import a dependency using the `import.meta.env.SSR` flag (part of [Vite env variables](https://vitejs.dev/guide/env-and-mode.html#env-variables)): +You can also conditionally import a dependency using the `import.meta.env.SSR` flag (part of [Vite env variables](https://vite.dev/guide/env-and-mode.html#env-variables)): ```js if (!import.meta.env.SSR) { diff --git a/docs/en/guide/using-vue.md b/docs/en/guide/using-vue.md index a9b9a389..4d6c7e87 100644 --- a/docs/en/guide/using-vue.md +++ b/docs/en/guide/using-vue.md @@ -204,7 +204,7 @@ Note that this might prevent certain tokens from being syntax highlighted proper ## Using CSS Pre-processors -VitePress has [built-in support](https://vitejs.dev/guide/features.html#css-pre-processors) for CSS pre-processors: `.scss`, `.sass`, `.less`, `.styl` and `.stylus` files. There is no need to install Vite-specific plugins for them, but the corresponding pre-processor itself must be installed: +VitePress has [built-in support](https://vite.dev/guide/features.html#css-pre-processors) for CSS pre-processors: `.scss`, `.sass`, `.less`, `.styl` and `.stylus` files. There is no need to install Vite-specific plugins for them, but the corresponding pre-processor itself must be installed: ``` # .scss and .sass diff --git a/docs/en/guide/what-is-vitepress.md b/docs/en/guide/what-is-vitepress.md index b63eb7ea..91358ac5 100644 --- a/docs/en/guide/what-is-vitepress.md +++ b/docs/en/guide/what-is-vitepress.md @@ -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. -
      - +::: tip {no-title} Just want to try it out? Skip to the [Quickstart](./getting-started). - -
      +::: ## Use Cases - **Documentation** - VitePress ships with a default theme designed for technical documentation. It powers this page you are reading right now, along with the documentation for [Vite](https://vitejs.dev/), [Rollup](https://rollupjs.org/), [Pinia](https://pinia.vuejs.org/), [VueUse](https://vueuse.org/), [Vitest](https://vitest.dev/), [D3](https://d3js.org/), [UnoCSS](https://unocss.dev/), [Iconify](https://iconify.design/) and [many more](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code). + VitePress ships with a default theme designed for technical documentation. It powers this page you are reading right now, along with the documentation for [Vite](https://vite.dev/), [Rollup](https://rollupjs.org/), [Pinia](https://pinia.vuejs.org/), [VueUse](https://vueuse.org/), [Vitest](https://vitest.dev/), [D3](https://d3js.org/), [UnoCSS](https://unocss.dev/), [Iconify](https://iconify.design/) and [many more](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code). The [official Vue.js documentation](https://vuejs.org/) is also based on VitePress, but uses a custom theme shared between multiple translations. @@ -30,7 +28,7 @@ Just want to try it out? Skip to the [Quickstart](./getting-started). VitePress aims to provide a great Developer Experience (DX) when working with Markdown content. -- **[Vite-Powered:](https://vitejs.dev/)** instant server start, with edits always instantly reflected (<100ms) without page reload. +- **[Vite-Powered:](https://vite.dev/)** instant server start, with edits always instantly reflected (<100ms) without page reload. - **[Built-in Markdown Extensions:](./markdown)** Frontmatter, tables, syntax highlighting... you name it. Specifically, VitePress provides many advanced features for working with code blocks, making it ideal for highly technical documentation. diff --git a/docs/en/index.md b/docs/en/index.md index ce9015b2..93f8f3f6 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -21,16 +21,16 @@ hero: alt: VitePress features: - - icon: 📝 + - icon: title: Focus on your content details: Effortlessly create beautiful documentation sites with just markdown. - - icon: + - icon: title: Enjoy the Vite DX details: Instant server start, lightning fast hot updates, and leverage Vite ecosystem plugins. - - icon: + - icon: title: Customize with Vue details: Use Vue syntax and components directly in markdown, or build custom themes with Vue. - - icon: 🚀 + - icon: title: Ship fast sites details: Fast initial load with static HTML, fast post-load navigation with client-side routing. --- diff --git a/docs/en/reference/default-theme-carbon-ads.md b/docs/en/reference/default-theme-carbon-ads.md index c19ead68..e12f6b12 100644 --- a/docs/en/reference/default-theme-carbon-ads.md +++ b/docs/en/reference/default-theme-carbon-ads.md @@ -11,7 +11,8 @@ export default { themeConfig: { carbonAds: { code: 'your-carbon-code', - placement: 'your-carbon-placement' + placement: 'your-carbon-placement', + format: 'classic' } } } @@ -19,8 +20,10 @@ export default { These values are used to call carbon CDN script as shown below. +The `format` option supports `classic`, `responsive`, and `cover`. + ```js -`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}` +`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}&format=${format}` ``` To learn more about Carbon Ads configuration, please visit [Carbon Ads website](https://www.carbonads.net/). diff --git a/docs/en/reference/default-theme-config.md b/docs/en/reference/default-theme-config.md index 5b4b0fba..02c221ec 100644 --- a/docs/en/reference/default-theme-config.md +++ b/docs/en/reference/default-theme-config.md @@ -25,10 +25,28 @@ export default { ## i18nRouting -- Type: `boolean` +- Type: `boolean | ((data: VitePressData, route: Route, targetLocale: string) => string)` Changing locale to say `zh` will change the URL from `/foo` (or `/en/foo/`) to `/zh/foo`. You can disable this behavior by setting `themeConfig.i18nRouting` to `false`. +Set `themeConfig.i18nRouting` to a function to customize the locale link. The function receives the current VitePress data, the current route, and the target locale key, and returns the target link. + +```ts +import { defineConfig } from 'vitepress' + +export default defineConfig({ + themeConfig: { + i18nRouting(data, route, targetLocale) { + const target = data.site.value.locales[targetLocale] + const targetLink = + target.link || (targetLocale === 'root' ? '/' : `/${targetLocale}/`) + + return `${targetLink}${route.data.relativePath.replace(/\.md$/, '')}${route.hash}` + } + } +}) +``` + ## logo - Type: `ThemeableImage` @@ -235,6 +253,7 @@ export default { // You can add any icon from simple-icons (https://simpleicons.org/): { icon: 'github', link: 'https://github.com/vuejs/vitepress' }, { icon: 'twitter', link: '...' }, + { icon: 'discord', link: '/community', target: '_self' }, // You can also add custom icons by passing SVG as string: { icon: { @@ -254,6 +273,7 @@ interface SocialLink { icon: string | { svg: string } link: string ariaLabel?: string + target?: string } ``` @@ -368,6 +388,7 @@ export default { carbonAds: { code: 'your-carbon-code', placement: 'your-carbon-placement' + format: 'classic' } } } @@ -377,6 +398,7 @@ export default { export interface CarbonAdsOptions { code: string placement: string + format?: 'classic' | 'responsive' | 'cover' } ``` diff --git a/docs/en/reference/default-theme-search.md b/docs/en/reference/default-theme-search.md index a3138294..3c3fc41b 100644 --- a/docs/en/reference/default-theme-search.md +++ b/docs/en/reference/default-theme-search.md @@ -291,6 +291,12 @@ export default defineConfig({ }) ``` +Use `askAi.sidePanel.panel.suggestedQuestions` for side panel suggested +questions. Algolia's standalone Ask AI examples also mention +`askAi.suggestedQuestions`, but that top-level option is not enough for +VitePress side panel mode and does not make the integrated keyword-search +modal display suggested questions on first open. + If you need to disable the keyboard shortcut, use the `keyboardShortcuts` option at the sidepanel root level: ```ts diff --git a/docs/en/reference/default-theme-sidebar.md b/docs/en/reference/default-theme-sidebar.md index cd44425d..559cd61d 100644 --- a/docs/en/reference/default-theme-sidebar.md +++ b/docs/en/reference/default-theme-sidebar.md @@ -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' } + ] + } + ] + } + ] + } +} +``` diff --git a/docs/en/reference/runtime-api.md b/docs/en/reference/runtime-api.md index 95a74e72..b69808ee 100644 --- a/docs/en/reference/runtime-api.md +++ b/docs/en/reference/runtime-api.md @@ -62,6 +62,8 @@ interface PageData { } ``` +`page.headers` is populated only when [`markdown.headers`](./site-config#markdown) is enabled. Without that option, it remains an empty array. The default theme outline reads rendered headings from the page content, so it can still appear when `page.headers` is empty. + **Example:** ```vue @@ -122,11 +124,23 @@ interface Router { } ``` +Assign route-change handlers on the router instance: + +```ts +const router = useRouter() + +router.onBeforeRouteChange = (to) => { + console.log('navigating to', to) +} +``` + +For custom themes, the same router is available from [`enhanceApp`](../guide/custom-theme#theme-interface). + ## `withBase` - **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). ## `` diff --git a/docs/en/reference/site-config.md b/docs/en/reference/site-config.md index 9e7234b0..a9d9c4f1 100644 --- a/docs/en/reference/site-config.md +++ b/docs/en/reference/site-config.md @@ -134,13 +134,43 @@ export default defineConfigWithTheme({ You can configure the underlying [Markdown-It](https://github.com/markdown-it/markdown-it) instance using the [markdown](#markdown) option in your VitePress config. +### Page-Level Overrides + +Some settings can be overridden for specific pages using frontmatter. + +See [Frontmatter Config](./frontmatter-config) for details. + +### Directory-Level Overrides + +Some config settings can be overridden at the directory level, allowing all pages in that directory to share settings without needing to repeat them in the frontmatter of each page. + +This is achieved by adding a file called `config.ts` (or `.js`, `.mjs`, or `.mts`) in the relevant directory. This file should export a config object using `export default`, similar to the main config file. + +Nested directories inherit settings from their parent directory, with configuration overrides being merged accordingly. + +The `defineAdditionalConfig` helper can be used to get TypeScript-powered intellisense for the available options, though as with `defineConfig` its use is optional. + +For example, for a site with multiple languages we might want a different `description` for each language. We could add `es/config.ts` with the following content: + +```ts +import { defineAdditionalConfig } from 'vitepress' + +export default defineAdditionalConfig({ + description: 'Generador de Sitios Estáticos desarrollado con Vite y Vue.' +}) +``` + +This `description` would then be used for all pages in the `es` directory. + +Alternatively, when using the built-in i18n features, the settings for a locale directory can be overridden via the `locales` setting in the main configuration file. See [Internationalization](../guide/i18n) for details. + ## Site Metadata ### title - Type: `string` - Default: `VitePress` -- Can be overridden per page via [frontmatter](./frontmatter-config#title) +- Can be overridden per page via [frontmatter](./frontmatter-config#title) or at the [directory level](#directory-level-overrides) Title for the site. When using the default theme, this will be displayed in the nav bar. @@ -161,7 +191,7 @@ The title of the page will be `Hello | My Awesome Site`. ### titleTemplate - Type: `string | boolean` -- Can be overridden per page via [frontmatter](./frontmatter-config#titletemplate) +- Can be overridden per page via [frontmatter](./frontmatter-config#titletemplate) or at the [directory level](#directory-level-overrides) Allows customizing each page's title suffix or the entire title. For example: @@ -194,7 +224,7 @@ The option can be set to `false` to disable title suffixes. - Type: `string` - Default: `A VitePress site` -- Can be overridden per page via [frontmatter](./frontmatter-config#description) +- Can be overridden per page via [frontmatter](./frontmatter-config#description) or at the [directory level](#directory-level-overrides) Description for the site. This will render as a `` tag in the page HTML. @@ -208,7 +238,7 @@ export default { - Type: `HeadConfig[]` - Default: `[]` -- Can be appended per page via [frontmatter](./frontmatter-config#head) +- Can be appended per page via [frontmatter](./frontmatter-config#head) or at the [directory level](#directory-level-overrides) Additional elements to render in the `` tag in the page HTML. The user-added tags are rendered before the closing `head` tag, after VitePress tags. @@ -320,6 +350,7 @@ export default { - Type: `string` - Default: `en-US` +- Can be overridden at the [directory level](#directory-level-overrides) The lang attribute for the site. This will render as a `` tag in the page HTML. @@ -430,7 +461,7 @@ export default { - Type: `string` - Default: `./.vitepress/cache` -The directory for cache files, relative to [project root](../guide/routing#root-and-source-directory). See also: [cacheDir](https://vitejs.dev/config/shared-options.html#cachedir). +The directory for cache files, relative to [project root](../guide/routing#root-and-source-directory). See also: [cacheDir](https://vite.dev/config/shared-options.html#cachedir). ```ts export default { @@ -472,13 +503,6 @@ export default { } ``` -### metaChunk - -- 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 - Type: `boolean` @@ -512,7 +536,7 @@ This option injects an inline script that restores users settings from local sto Whether to get the last updated timestamp for each page using Git. The timestamp will be included in each page's page data, accessible via [`useData`](./runtime-api#usedata). -When using the default theme, enabling this option will display each page's last updated time. You can customize the text via [`themeConfig.lastUpdatedText`](./default-theme-config#lastupdatedtext) option. +When using the default theme, enabling this option will display each page's last updated time. You can customize the text via [`themeConfig.lastUpdated.text`](./default-theme-config#lastupdated) option. ## Customization @@ -530,11 +554,13 @@ export default { Check the [type declaration and jsdocs](https://github.com/vuejs/vitepress/blob/main/src/node/markdown/markdown.ts) for all the options available. +Set `markdown.headers` to `true` or pass [`@mdit-vue/plugin-headers`](https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-headers) options to collect headings into [`useData().page.headers`](./runtime-api#usedata). This option is disabled by default. + ### vite - Type: `import('vite').UserConfig` -Pass raw [Vite Config](https://vitejs.dev/config/) to internal Vite dev server / bundler. +Pass raw [Vite Config](https://vite.dev/config/) to internal Vite dev server / bundler. ```js export default { @@ -607,7 +633,7 @@ interface SSGContext { - Type: `(context: TransformContext) => Awaitable` -`transformHead` is a build hook to transform the head before generating each page. It will allow you to add head entries that cannot be statically added to your VitePress config. You only need to return extra entries, they will be merged automatically with the existing ones. +`transformHead` is a build hook to add extra tags to the `` of each page. It allows you to add head entries that cannot be statically added to your VitePress config. You only need to return extra entries, they will be merged automatically with the existing ones. ::: warning Don't mutate anything inside the `context`. @@ -635,44 +661,38 @@ interface TransformContext { } ``` -Note that this hook is only called when generating the site statically. It is not called during dev. If you need to add dynamic head entries during dev, you can use the [`transformPageData`](#transformpagedata) hook instead: +This hook is only called when performing a build, it is not called during dev. -```ts -export default { - transformPageData(pageData) { - pageData.frontmatter.head ??= [] - pageData.frontmatter.head.push([ - 'meta', - { - name: 'og:title', - content: - pageData.frontmatter.layout === 'home' - ? `VitePress` - : `${pageData.title} | VitePress` - } - ]) - } -} -``` +The extra tags will be added to the static HTML files generated by the build. They will not be updated during client-side navigation. -#### Example: Adding a canonical URL `` +In many cases, using the [`transformPageData`](#transformpagedata) hook is a cleaner solution. That hook will also be applied to both client-side navigation and during dev. But if generating the head tags is computationally expensive then `transformHead` will avoid that overhead during dev. + +#### Example: Adding `og:image` meta ```ts export default { - transformPageData(pageData) { - const canonicalUrl = `https://example.com/${pageData.relativePath}` - .replace(/index\.md$/, '') - .replace(/\.md$/, '.html') + async transformHead(context) { + if (context.page === '404.md') { + return + } - pageData.frontmatter.head ??= [] - pageData.frontmatter.head.push([ - 'link', - { rel: 'canonical', href: canonicalUrl } - ]) + // The implementation details of `generatePageImage` would depend + // on your requirements. Here we assume it generates a suitable + // image for each page and returns the image URL. + const imageUrl = await generatePageImage(context) + + return [[ + 'meta', + { name: 'og:image', content: imageUrl } + ]] } } ``` +Here we're assuming that the image URL is dynamic and time-consuming to generate. Using `transformHead` avoids that overhead during development. + +For simpler cases, it may be possible to use the [`head`](./frontmatter-config#head) setting in frontmatter, or [`transformPageData`](#transformpagedata). + ### transformHtml - Type: `(code: string, id: string, context: TransformContext) => Awaitable` @@ -721,3 +741,39 @@ interface TransformPageContext { siteConfig: SiteConfig } ``` + +#### Example: Adding a `` + +```ts +export default { + transformPageData(pageData) { + const title = pageData.frontmatter.layout === 'home' + ? 'VitePress' + : `${pageData.title} | VitePress` + + pageData.frontmatter.head ??= [] + pageData.frontmatter.head.push([ + 'meta', + { name: 'og:title', content: title } + ]) + } +} +``` + +#### Example: Adding a canonical URL `` + +```ts +export default { + transformPageData(pageData) { + const canonicalUrl = `https://example.com/${pageData.relativePath}` + .replace(/index\.md$/, '') + .replace(/\.md$/, '.html') + + pageData.frontmatter.head ??= [] + pageData.frontmatter.head.push([ + 'link', + { rel: 'canonical', href: canonicalUrl } + ]) + } +} +``` diff --git a/docs/es/config.ts b/docs/es/config.ts index fee38361..0427767e 100644 --- a/docs/es/config.ts +++ b/docs/es/config.ts @@ -1,9 +1,30 @@ -import { createRequire } from 'module' -import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' +import { createRequire } from 'node:module' +import { + defineAdditionalConfig, + type DefaultTheme, + type MarkdownLocaleOptions +} from 'vitepress' const require = createRequire(import.meta.url) const pkg = require('vitepress/package.json') +export const markdown: MarkdownLocaleOptions = { + container: { + tipLabel: 'CONSEJO', + infoLabel: 'INFORMACIÓN', + warningLabel: 'ADVERTENCIA', + dangerLabel: 'PELIGRO', + detailsLabel: 'Detalles', + noteLabel: 'NOTA', + importantLabel: 'IMPORTANTE', + cautionLabel: 'PRECAUCIÓN' + }, + codeCopyButton: { + tooltipText: 'Copiar código', + copiedText: 'Copiado' + } +} + export default defineAdditionalConfig({ description: 'Generador de Sitios Estáticos desarrollado con Vite y Vue.', @@ -267,8 +288,6 @@ function searchOptions(): Partial { afterToolCallText: 'Buscado', stoppedStreamingText: 'Has detenido esta respuesta', errorTitleText: 'Error de chat', - threadDepthExceededMessage: - 'Esta conversación se ha cerrado para mantener respuestas precisas.', startNewConversationButtonText: 'Iniciar una nueva conversación' } } diff --git a/docs/es/guide/asset-handling.md b/docs/es/guide/asset-handling.md index c3580347..30c9034d 100644 --- a/docs/es/guide/asset-handling.md +++ b/docs/es/guide/asset-handling.md @@ -6,7 +6,7 @@ description: Aprende cómo referenciar y manejar recursos estáticos como imáge ## Referenciando Assets Estáticos {#referencing-static-assets} -Todos los archivos Markdown son compilados en componentes Vue y procesados por [Vite](https://vitejs.dev/guide/assets.html). Usted puede **y debe** referenciar cualquier asset usando URLs relativas: +Todos los archivos Markdown son compilados en componentes Vue y procesados por [Vite](https://vite.dev/guide/assets.html). Usted puede **y debe** referenciar cualquier asset usando URLs relativas: ```md ![Una imagen](./imagen.png) diff --git a/docs/es/guide/extending-default-theme.md b/docs/es/guide/extending-default-theme.md index b9b9a67f..1e4a7885 100644 --- a/docs/es/guide/extending-default-theme.md +++ b/docs/es/guide/extending-default-theme.md @@ -119,7 +119,7 @@ export default { } satisfies Theme ``` -Como estamos usando Vite, puede también aprovechar la [funcionalidad de importación glob](https://vitejs.dev/guide/features.html#glob-import) de Vite para registrar automaticamente un directorio de componetes. +Como estamos usando Vite, puede también aprovechar la [funcionalidad de importación glob](https://vite.dev/guide/features.html#glob-import) de Vite para registrar automaticamente un directorio de componetes. ## _Slots_ en el Layout {#layout-slots} @@ -309,7 +309,7 @@ En breve. ## Substituyendo Componentes Internos {#overriding-internal-components} -Puede usar los [aliases](https://vitejs.dev/config/shared-options.html#resolve-alias) Vite para substituir los componentes del tema por defecto por los suyos personalizados: +Puede usar los [aliases](https://vite.dev/config/shared-options.html#resolve-alias) Vite para substituir los componentes del tema por defecto por los suyos personalizados: ```ts import { fileURLToPath, URL } from 'node:url' diff --git a/docs/es/guide/getting-started.md b/docs/es/guide/getting-started.md index ce36352a..9461c808 100644 --- a/docs/es/guide/getting-started.md +++ b/docs/es/guide/getting-started.md @@ -12,7 +12,7 @@ Puede experimentar VitePress directamente en su navegador en [StackBlitz](https: ### Prerrequisitos {#prerequisites} -- [Node.js](https://nodejs.org/) versión 20 o superior. +- [Node.js](https://nodejs.org/) versión 22 o superior. - Terminal para acessar VitePress a través de su interfaz de linea de comando (CLI). - Editor de texto con soporte a sintaxis [Markdown](https://en.wikipedia.org/wiki/Markdown). - [VSCode](https://code.visualstudio.com/) es recomendado, junto con la [extensión oficial Vue](https://marketplace.visualstudio.com/items?itemName=Vue.volar). @@ -41,7 +41,7 @@ $ bun add -D vitepress@next ::: tip NOTA -VitePress es un paquete apenas para ESM. No use `require()` para importarlo, y asegurese de que el `package.json` más cercano contiene `"type": "module"`, o cambie la extensión de archivo de sus archivos relevantes como `.vitepress/config.js` a `.mjs`/`.mts`. Consulte la [Guía de resolución de problemas Vite](http://vitejs.dev/guide/troubleshooting.html#this-package-is-esm-only) para más detalles. Además de eso, dentro de contextos de JavaScript asíncronos, puede usar `await import('vitepress')`. +VitePress es un paquete apenas para ESM. No use `require()` para importarlo, y asegurese de que el `package.json` más cercano contiene `"type": "module"`, o cambie la extensión de archivo de sus archivos relevantes como `.vitepress/config.js` a `.mjs`/`.mts`. Consulte la [Guía de resolución de problemas Vite](http://vite.dev/guide/troubleshooting.html#this-package-is-esm-only) para más detalles. Además de eso, dentro de contextos de JavaScript asíncronos, puede usar `await import('vitepress')`. ::: diff --git a/docs/es/guide/markdown.md b/docs/es/guide/markdown.md index 17a9eae3..25e819b1 100644 --- a/docs/es/guide/markdown.md +++ b/docs/es/guide/markdown.md @@ -1,5 +1,6 @@ --- description: Extensiones Markdown integradas en VitePress, incluyendo contenedores personalizados, bloques de código con resaltado de sintaxis, resaltado de líneas, grupos de código y más. +outline: deep --- # Extensiones Markdown {#markdown-extensions} @@ -112,7 +113,7 @@ Para más detalles, vea [Frontmatter](../reference/frontmatter-config). :tada: :100: -Una [lista de todos los emojis](https://github.com/markdown-it/markdown-it-emoji/blob/master/lib/data/full.mjs) está disponible. +Una [lista de todos los emojis](https://github.com/mdit-plugins/mdit-plugins/blob/main/packages/plugin-emoji/src/data/full.ts) está disponible. ## Tabla de Contenido (TOC) @@ -232,7 +233,7 @@ export default defineConfig({ ### `raw` -Este es un recipiente especial que puee ser usado para evitar conflictos de estilo y enrutador con VitePress. Esto es especialmente útil al documentar bibliotecas de componentes. Puede tambien verificar [whyframe](https://whyframe.dev/docs/integrations/vitepress) para mejor aislamiento. +Este es un recipiente especial que puee ser usado para evitar conflictos de estilo y enrutador con VitePress. Esto es especialmente útil al documentar bibliotecas de componentes. **Sintaxis** @@ -888,14 +889,14 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$ ## _Lazy Loading_ de Imagenes {#image-lazy-loading} -Puede activar la "carga perezosa" para cada imagen adicionada via markdown definiendo `lazyLoading` como `true` en su archivo de configuración: +Puede activar la "carga perezosa" para cada imagen adicionada via markdown definiendo `lazyLoad` como `true` en su archivo de configuración: ```js export default { markdown: { image: { // la carga perezosa de imagenes está desactivada por defecto - lazyLoading: true + lazyLoad: true } } } @@ -907,15 +908,15 @@ VitePress usa [markdown-it](https://github.com/markdown-it/markdown-it) como int ```js import { defineConfig } from 'vitepress' -import markdownItAnchor from 'markdown-it-anchor' +import { headerLink } from '@mdit/plugin-anchor' import markdownItFoo from 'markdown-it-foo' export default defineConfig({ markdown: { - // opciones para markdown-it-anchor - // https://github.com/valeriangalliat/markdown-it-anchor#usage + // opciones para @mdit/plugin-anchor + // https://mdit-plugins.github.io/anchor.html anchor: { - permalink: markdownItAnchor.permalink.headerLink() + permalink: headerLink() }, // opciones para @mdit-vue/plugin-toc diff --git a/docs/es/guide/routing.md b/docs/es/guide/routing.md index e3c42162..e4d2894b 100644 --- a/docs/es/guide/routing.md +++ b/docs/es/guide/routing.md @@ -290,7 +290,7 @@ El módulo de carga de paths es ejecutado en Node.js y apenas durante el momento Generando paths a partir de archivos locales: ```js -import fs from 'fs' +import fs from 'node:fs' export default { paths() { diff --git a/docs/es/guide/ssr-compat.md b/docs/es/guide/ssr-compat.md index 38021653..77ca3dac 100644 --- a/docs/es/guide/ssr-compat.md +++ b/docs/es/guide/ssr-compat.md @@ -39,7 +39,7 @@ onMounted(() => { ### Importación Condicional {#conditional-import} -También puede importar una dependencia condicionalmente utilizando la bandera `import.meta.env.SSR` (que forma parte de las [variables de entorno Vite](https://vitejs.dev/guide/env-and-mode.html#env-variables)): +También puede importar una dependencia condicionalmente utilizando la bandera `import.meta.env.SSR` (que forma parte de las [variables de entorno Vite](https://vite.dev/guide/env-and-mode.html#env-variables)): ```js if (!import.meta.env.SSR) { diff --git a/docs/es/guide/using-vue.md b/docs/es/guide/using-vue.md index 2682acfd..7d6fe5a2 100644 --- a/docs/es/guide/using-vue.md +++ b/docs/es/guide/using-vue.md @@ -204,7 +204,7 @@ Observe que esto puede impedir que ciertos tokens sean realzados correctamente. ## Usando Preprocesadores CSS {#using-css-pre-processors} -VitePress poseé [soporte embutido](https://vitejs.dev/guide/features.html#css-pre-processors) para preprocesadores CSS: archivos `.scss`, `.sass`, `.less`, `.styl` e `.stylus`. No es necesario instalar plugins específicos de Vite para ellos, pero el propio preprocesados correspondiente debe ser instalado: +VitePress poseé [soporte embutido](https://vite.dev/guide/features.html#css-pre-processors) para preprocesadores CSS: archivos `.scss`, `.sass`, `.less`, `.styl` e `.stylus`. No es necesario instalar plugins específicos de Vite para ellos, pero el propio preprocesados correspondiente debe ser instalado: ``` # .scss e .sass diff --git a/docs/es/guide/what-is-vitepress.md b/docs/es/guide/what-is-vitepress.md index 22c4b946..03f8d865 100644 --- a/docs/es/guide/what-is-vitepress.md +++ b/docs/es/guide/what-is-vitepress.md @@ -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. -
      - +::: tip {no-title} ¿Quieres probarlo? Ve directo al [Inicio Rápido](./getting-started). - -
      +::: ## Casos de Uso {#use-cases} - **Documentación** - VitePress incluye un tema por defecto diseñado para documentación técnica. Este tema es el que se utiliza en la página que estás leyendo ahora, así como en la documentación de [Vite](https://vitejs.dev/), [Rollup](https://rollupjs.org/), [Pinia](https://pinia.vuejs.org/), [VueUse](https://vueuse.org/), [Vitest](https://vitest.dev/), [D3](https://d3js.org/), [UnoCSS](https://unocss.dev/), [Iconify](https://iconify.design/) y [muchos otros](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code). + VitePress incluye un tema por defecto diseñado para documentación técnica. Este tema es el que se utiliza en la página que estás leyendo ahora, así como en la documentación de [Vite](https://vite.dev/), [Rollup](https://rollupjs.org/), [Pinia](https://pinia.vuejs.org/), [VueUse](https://vueuse.org/), [Vitest](https://vitest.dev/), [D3](https://d3js.org/), [UnoCSS](https://unocss.dev/), [Iconify](https://iconify.design/) y [muchos otros](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code). La [documentación oficial Vue.js](https://vuejs.org/) también está basada en VitePress, pero utiliza un tema personalizado compartido entre varias traducciones. @@ -30,7 +28,7 @@ VitePress es un [Generador de Sitios Estáticos](https://en.wikipedia.org/wiki/S VitePress busca ofrecer una excelente Experiencia de Desarrollador (DX) al trabajar con contenido Markdown. -- **[Con tecnología Vite:](https://vitejs.dev/)** inicio instantáneo del servidor, con los cambios reflejados al instante (<100ms) sin recargar la página. +- **[Con tecnología Vite:](https://vite.dev/)** inicio instantáneo del servidor, con los cambios reflejados al instante (<100ms) sin recargar la página. - **[Extensiones Markdown Integradas:](./markdown)** Frontmatter, tablas, destaque de sintaxis... tú decides. Específicamente, VitePress proporciona muchos recursos para trabajar con bloques de código, tornándolo ideal para documentación altamente técnica. diff --git a/docs/es/index.md b/docs/es/index.md index b4c8673f..1eb31f82 100644 --- a/docs/es/index.md +++ b/docs/es/index.md @@ -21,16 +21,16 @@ hero: alt: VitePress features: - - icon: 📝 + - icon: title: Concéntrese en su contenido details: Cree lindos sitios de documentación apenas con markdown. - - icon: + - icon: title: Disfruta de la experiencia Vite details: Inicio instantaneo de servidor, actualizaciones ultrarrápidas, y plugins del ecosistema Vite. - - icon: + - icon: title: Personaliza con Vue details: Usa la sintaxis y componentes Vue directamente en markdown, o construye temas personalizados con Vue. - - icon: 🚀 + - icon: 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. --- diff --git a/docs/es/reference/default-theme-carbon-ads.md b/docs/es/reference/default-theme-carbon-ads.md index 41da600e..a665c611 100644 --- a/docs/es/reference/default-theme-carbon-ads.md +++ b/docs/es/reference/default-theme-carbon-ads.md @@ -11,7 +11,8 @@ export default { themeConfig: { carbonAds: { code: 'tu-código-carbon', - placement: 'tu-vinculación-carbon' + placement: 'tu-vinculación-carbon', + format: 'classic' } } } @@ -19,8 +20,10 @@ export default { Estos valores se utilizan para llamar al script en CDN de carbon como se muestra a continuación. +La opción `format` admite `classic`, `responsive` y `cover`. + ```js -`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}` +`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}&format=${format}` ``` Para obtener más información de la configuración de Carbono Ads, por favor visite [Site Carbon Ads](https://www.carbonads.net/). diff --git a/docs/es/reference/default-theme-config.md b/docs/es/reference/default-theme-config.md index a15608ff..0c86e1f7 100644 --- a/docs/es/reference/default-theme-config.md +++ b/docs/es/reference/default-theme-config.md @@ -353,16 +353,20 @@ export default { themeConfig: { carbonAds: { code: 'su-código-carbon', - placement: 'su-colocación-carbon' + placement: 'su-colocación-carbon', + format: 'classic' } } } ``` +La opción `format` admite `classic`, `responsive` y `cover`. + ```ts export interface CarbonAdsOptions { code: string placement: string + format?: 'classic' | 'responsive' | 'cover' } ``` diff --git a/docs/es/reference/site-config.md b/docs/es/reference/site-config.md index 21792600..7bc0c751 100644 --- a/docs/es/reference/site-config.md +++ b/docs/es/reference/site-config.md @@ -430,7 +430,7 @@ export default { - Tipo: `string` - Predeterminado: `./.vitepress/cache` -El directorio para los archivos de caché, en relación con el [raiz del proyecto](../guide/routing#root-and-source-directory). Vea también: [cacheDir](https://vitejs.dev/config/shared-options.html#cachedir). +El directorio para los archivos de caché, en relación con el [raiz del proyecto](../guide/routing#root-and-source-directory). Vea también: [cacheDir](https://vite.dev/config/shared-options.html#cachedir). ```ts export default { @@ -503,7 +503,7 @@ Esta opción inyecta un script en línea que restaura la configuración de los u Para obtener la marca de tiempo de la última actualización para cada página usando Git. El sello de fecha se incluirá en los datos de cada página, accesible a través de [`useData`](./runtime-api#usedata). -Cuando se utiliza el tema predeterminado, al habilitar esta opción se mostrará la última hora de actualización de cada página. Puedes personalizar el texto mediante la opción [`themeConfig.lastUpdatedText`](./default-theme-config#lastupdatedtext). +Cuando se utiliza el tema predeterminado, al habilitar esta opción se mostrará la última hora de actualización de cada página. Puedes personalizar el texto mediante la opción [`themeConfig.lastUpdated.text`](./default-theme-config#lastupdated). ## Personalización {#customization} @@ -525,7 +525,7 @@ Consulte la [declaración de tipo y jsdocs](https://github.com/vuejs/vitepress/b - Tipo: `import('vite').UserConfig` -Pase la [Configuración Vite](https://vitejs.dev/config/) sin procesar al servidor interno / empaquetador Vite. +Pase la [Configuración Vite](https://vite.dev/config/) sin procesar al servidor interno / empaquetador Vite. ```js export default { diff --git a/docs/fa/config.ts b/docs/fa/config.ts index 4cb8c245..a59bc6a5 100644 --- a/docs/fa/config.ts +++ b/docs/fa/config.ts @@ -1,17 +1,39 @@ -import { createRequire } from 'module' -import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' +import { createRequire } from 'node:module' +import { + defineAdditionalConfig, + type DefaultTheme, + type MarkdownLocaleOptions +} from 'vitepress' const require = createRequire(import.meta.url) const pkg = require('vitepress/package.json') +export const markdown: MarkdownLocaleOptions = { + container: { + tipLabel: 'نکته', + infoLabel: 'اطلاعات', + warningLabel: 'اخطار', + dangerLabel: 'خطر', + detailsLabel: 'جزئیات', + noteLabel: 'یادداشت', + importantLabel: 'مهم', + cautionLabel: 'احتیاط' + }, + codeCopyButton: { + tooltipText: 'کپی کد', + copiedText: 'کپی شد' + } +} + export default defineAdditionalConfig({ description: 'ژنراتور استاتیک وب‌سایت با Vite و Vue', - // prettier-ignore head: [ - ['link', { rel: 'preconnect', href: 'https://fonts.googleapis.com' }], - ['link', { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' }], - ['link', { href: 'https://fonts.googleapis.com/css2?family=Vazirmatn:wght@100..900&display=swap', rel: 'stylesheet' }], + [ + 'link', + // for the vazirmatn font-face defined in .vitepress/theme/styles.css + { rel: 'preconnect', href: 'https://cdn.jsdelivr.net', crossorigin: '' } + ] ], themeConfig: { @@ -264,8 +286,6 @@ function searchOptions(): Partial { afterToolCallText: 'جستجو برای', stoppedStreamingText: 'شما این پاسخ را متوقف کردید', errorTitleText: 'خطای گفتگو', - threadDepthExceededMessage: - 'برای حفظ دقت پاسخ ها، این گفت وگو بسته شد.', startNewConversationButtonText: 'شروع گفت وگوی جدید' } } diff --git a/docs/fa/guide/asset-handling.md b/docs/fa/guide/asset-handling.md index cda20c7d..ade26214 100644 --- a/docs/fa/guide/asset-handling.md +++ b/docs/fa/guide/asset-handling.md @@ -6,7 +6,7 @@ description: نحوه ارجاع و مدیریت منابع ایستا مانن ## ارجاع به منابع ایستا {#referencing-static-assets} -تمام فایل‌های Markdown به کامپوننت‌های Vue تبدیل و توسط [Vite](https://vitejs.dev/guide/assets.html) پردازش می‌شوند. شما می‌توانید، **و باید**، هر نوع دارایی را با استفاده از URL‌های نسبی مرجع قرار دهید: +تمام فایل‌های Markdown به کامپوننت‌های Vue تبدیل و توسط [Vite](https://vite.dev/guide/assets.html) پردازش می‌شوند. شما می‌توانید، **و باید**، هر نوع دارایی را با استفاده از URL‌های نسبی مرجع قرار دهید: ```md ![تصویر](./image.png) diff --git a/docs/fa/guide/deploy.md b/docs/fa/guide/deploy.md index 47d33b5b..30de6e9c 100644 --- a/docs/fa/guide/deploy.md +++ b/docs/fa/guide/deploy.md @@ -189,7 +189,7 @@ Cache-Control: max-age=31536000,immutable uses: actions/deploy-pages@v4 ``` -::: warning هشدار + ::: warning هشدار مطمئن شوید که گزینه `base` در ویت‌پرس به‌درستی پیکربندی شده است. برای اطلاعات بیشتر به [تنظیم مسیر پایه عمومی](#setting-a-public-base-path) مراجعه کنید. ::: diff --git a/docs/fa/guide/extending-default-theme.md b/docs/fa/guide/extending-default-theme.md index 76418ed2..ccea29a6 100644 --- a/docs/fa/guide/extending-default-theme.md +++ b/docs/fa/guide/extending-default-theme.md @@ -119,7 +119,7 @@ export default { } satisfies Theme ``` -از آنجا که از Vite استفاده می‌کنیم، می‌توانید از ویژگی [import glob](https://vitejs.dev/guide/features.html#glob-import) در Vite برای خودکار ثبت یک پوشه از مولفه‌ها استفاده کنید. +از آنجا که از Vite استفاده می‌کنیم، می‌توانید از ویژگی [import glob](https://vite.dev/guide/features.html#glob-import) در Vite برای خودکار ثبت یک پوشه از مولفه‌ها استفاده کنید. ## slot ‌های طرح {#layout-slots} @@ -311,7 +311,7 @@ provide('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => { ## جایگزینی کامپوننت‌های داخلی {#overriding-internal-components} -شما می‌توانید با استفاده از [alias های Vite](https://vitejs.dev/config/shared-options.html#resolve-alias)، کامپوننت‌های تم پیش‌فرض را با کامپوننت‌های سفارشی خود جایگزین کنید: +شما می‌توانید با استفاده از [alias های Vite](https://vite.dev/config/shared-options.html#resolve-alias)، کامپوننت‌های تم پیش‌فرض را با کامپوننت‌های سفارشی خود جایگزین کنید: ```ts import { fileURLToPath, URL } from 'node:url' diff --git a/docs/fa/guide/getting-started.md b/docs/fa/guide/getting-started.md index 33af8299..97775e0d 100644 --- a/docs/fa/guide/getting-started.md +++ b/docs/fa/guide/getting-started.md @@ -12,7 +12,7 @@ description: نصب و راه‌اندازی ویت‌پرس. یاد بگیری ### پیش‌نیازها {#prerequisites} -- [Node.js](https://nodejs.org/) نسخه 20 یا بالاتر. +- [Node.js](https://nodejs.org/) نسخه 22 یا بالاتر. - ترمینال برای دسترسی به ویت‌پرس از طریق رابط خط فرمان (CLI). - ویرایشگر متنی با پشتیبانی از [Markdown](https://en.wikipedia.org/wiki/Markdown). - [VSCode](https://code.visualstudio.com/) به همراه [افزونه رسمی Vue](https://marketplace.visualstudio.com/items?itemName=Vue.volar). @@ -41,7 +41,7 @@ $ bun add -D vitepress@next ::: tip نکته -ویت‌پرس یک بسته فقط ESM است. از `require()` برای وارد کردن آن استفاده نکنید و اطمینان حاصل کنید که نزدیک‌ترین `package.json` شما شامل `"type": "module"` است، یا پسوند فایل‌های مربوطه خود مانند `.vitepress/config.js` را به `.mjs`/`.mts` تغییر دهید. برای جزئیات بیشتر به [راهنمای عیب‌یابی Vite](http://vitejs.dev/guide/troubleshooting.html#this-package-is-esm-only) مراجعه کنید. همچنین، در زمینه‌های async CJS می‌توانید از `await import('vitepress')` استفاده کنید. +ویت‌پرس یک بسته فقط ESM است. از `require()` برای وارد کردن آن استفاده نکنید و اطمینان حاصل کنید که نزدیک‌ترین `package.json` شما شامل `"type": "module"` است، یا پسوند فایل‌های مربوطه خود مانند `.vitepress/config.js` را به `.mjs`/`.mts` تغییر دهید. برای جزئیات بیشتر به [راهنمای عیب‌یابی Vite](http://vite.dev/guide/troubleshooting.html#this-package-is-esm-only) مراجعه کنید. همچنین، در زمینه‌های async CJS می‌توانید از `await import('vitepress')` استفاده کنید. ::: diff --git a/docs/fa/guide/markdown.md b/docs/fa/guide/markdown.md index 05072193..e4f369e0 100644 --- a/docs/fa/guide/markdown.md +++ b/docs/fa/guide/markdown.md @@ -1,5 +1,6 @@ --- description: افزونه‌های داخلی Markdown در ویت‌پرس شامل کانتینرهای سفارشی، بلوک‌های کد با هایلایت، گروه‌های کد و موارد دیگر. +outline: deep --- # افزونه‌های Markdown {#markdown-extensions} @@ -112,7 +113,7 @@ lang: fa-IR :tada: :100: -یک [لیست از همه اموجی ها](https://github.com/markdown-it/markdown-it-emoji/blob/master/lib/data/full.mjs) در دسترس است. +یک [لیست از همه اموجی ها](https://github.com/mdit-plugins/mdit-plugins/blob/main/packages/plugin-emoji/src/data/full.ts) در دسترس است. ## فهرست مطالب {#table-of-contents} @@ -880,14 +881,14 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$ ## بارگذاری lazy تصویر {#image-lazy-loading} -می‌توانید بارگذاری تنبلی را برای هر تصویر اضافه شده از طریق Markdown با تنظیم `lazyLoading` به `true` در فایل پیکربندی فعال کنید: +می‌توانید بارگذاری تنبلی را برای هر تصویر اضافه شده از طریق Markdown با تنظیم `lazyLoad` به `true` در فایل پیکربندی فعال کنید: ```js export default { markdown: { image: { // بارگذاری تنبلی تصویر به طور پیش‌فرض غیرفعال است - lazyLoading: true + lazyLoad: true } } } @@ -899,15 +900,15 @@ export default { ```js import { defineConfig } from 'vitepress' -import markdownItAnchor from 'markdown-it-anchor' +import { headerLink } from '@mdit/plugin-anchor' import markdownItFoo from 'markdown-it-foo' export default defineConfig({ markdown: { - // گزینه‌های markdown-it-anchor - // https://github.com/valeriangalliat/markdown-it-anchor#usage + // گزینه‌های @mdit/plugin-anchor + // https://mdit-plugins.github.io/anchor.html anchor: { - permalink: markdownItAnchor.permalink.headerLink() + permalink: headerLink() }, // گزینه‌های @mdit-vue/plugin-toc diff --git a/docs/fa/guide/migration-from-vitepress-0.md b/docs/fa/guide/migration-from-vitepress-0.md index 211bb0f9..131e0d2b 100644 --- a/docs/fa/guide/migration-from-vitepress-0.md +++ b/docs/fa/guide/migration-from-vitepress-0.md @@ -14,7 +14,7 @@ - `repo`، `repoLabel`، `docsDir`، `docsBranch`، `editLinks`، `editLinkText` به منظور API انعطاف‌پذیرتر حذف شده‌اند. - برای اضافه کردن لینک GitHub با آیکون به نوار ناوبری، از ویژگی [پیوندهای اجتماعی](../reference/default-theme-nav#navigation-links) استفاده کنید. - برای اضافه کردن ویژگی "ویرایش این صفحه"، از ویژگی [پیوند ویرایش](../reference/default-theme-edit-link) استفاده کنید. -- گزینه `lastUpdated` حالا به `config.lastUpdated` و `themeConfig.lastUpdatedText` تقسیم شده است. +- گزینه `lastUpdated` حالا به `config.lastUpdated` و `themeConfig.lastUpdated.text` تقسیم شده است. - `carbonAds.carbon` به `carbonAds.code` تغییر کرده است. ## پیکربندی Frontmatter diff --git a/docs/fa/guide/routing.md b/docs/fa/guide/routing.md index f159a876..b8c3a99b 100644 --- a/docs/fa/guide/routing.md +++ b/docs/fa/guide/routing.md @@ -295,7 +295,7 @@ export default { تولید مسیرها از فایل‌های محلی: ```js -import fs from 'fs' +import fs from 'node:fs' export default { paths() { diff --git a/docs/fa/guide/ssr-compat.md b/docs/fa/guide/ssr-compat.md index 75e698a7..76f91f90 100644 --- a/docs/fa/guide/ssr-compat.md +++ b/docs/fa/guide/ssr-compat.md @@ -39,7 +39,7 @@ onMounted(() => { ### وارد کردن شرطی {#conditional-import} -می‌توانید همچنین وابستگی را با استفاده از `import.meta.env.SSR` (قسمتی از [متغیرهای env Vite](https://vitejs.dev/guide/env-and-mode.html#env-variables)) به شرط وارد کنید: +می‌توانید همچنین وابستگی را با استفاده از `import.meta.env.SSR` (قسمتی از [متغیرهای env Vite](https://vite.dev/guide/env-and-mode.html#env-variables)) به شرط وارد کنید: ```js if (!import.meta.env.SSR) { diff --git a/docs/fa/guide/using-vue.md b/docs/fa/guide/using-vue.md index 1b618f60..d0b25e46 100644 --- a/docs/fa/guide/using-vue.md +++ b/docs/fa/guide/using-vue.md @@ -205,7 +205,7 @@ Hello {{ 1 + 1 }} ## استفاده از پیش‌پردازنده‌های CSS {#using-css-pre-processors} -ویت‌پرس از [پشتیبانی داخلی](https://vitejs.dev/guide/features.html#css-pre-processors) برای پیش‌پردازنده‌های CSS مانند فایل‌های `.scss`، `.sass`، `.less`، `.styl` و `.stylus` پشتیبانی می‌کند. برای استفاده از آنها نیازی به نصب پلاگین‌های خاص Vite نیست، اما خود پیش‌پردازنده مربوطه باید نصب شده باشد: +ویت‌پرس از [پشتیبانی داخلی](https://vite.dev/guide/features.html#css-pre-processors) برای پیش‌پردازنده‌های CSS مانند فایل‌های `.scss`، `.sass`، `.less`، `.styl` و `.stylus` پشتیبانی می‌کند. برای استفاده از آنها نیازی به نصب پلاگین‌های خاص Vite نیست، اما خود پیش‌پردازنده مربوطه باید نصب شده باشد: ``` # .scss و .sass diff --git a/docs/fa/guide/what-is-vitepress.md b/docs/fa/guide/what-is-vitepress.md index 44fde866..4ad21397 100644 --- a/docs/fa/guide/what-is-vitepress.md +++ b/docs/fa/guide/what-is-vitepress.md @@ -6,17 +6,15 @@ description: ویت‌پرس یک تولیدکننده سایت ایستا بر ویت‌پرس یک [تولید کننده سایت ایستا](https://en.wikipedia.org/wiki/Static_site_generator) (SSG) است که برای ساخت وب‌سایت‌های سریع و محتوا محور طراحی شده است. به طور خلاصه، ویت‌پرس محتوای منبع شما که به زبان [Markdown](https://en.wikipedia.org/wiki/Markdown) نوشته شده است را گرفته، یک تم بر روی آن اعمال می‌کند و صفحات HTML ایستا تولید می‌کند که به راحتی در هر جایی قابل استقرار هستند. -
      - +::: tip {no-title} فقط می‌خواهید آن را امتحان کنید؟ به [شروع سریع](./getting-started) بروید. - -
      +::: ## موارد استفاده {#use-cases} - **مستندسازی** - ویت‌پرس با یک تم پیش‌فرض طراحی شده برای مستندات فنی ارائه می‌شود. این صفحه‌ای که اکنون در حال خواندن آن هستید و همچنین مستندات [Vite](https://vitejs.dev/)، [Rollup](https://rollupjs.org/)، [Pinia](https://pinia.vuejs.org/)، [VueUse](https://vueuse.org/)، [Vitest](https://vitest.dev/)، [D3](https://d3js.org/)، [UnoCSS](https://unocss.dev/)، [Iconify](https://iconify.design/) و [بسیاری دیگر](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code) با استفاده از ویت‌پرس ساخته شده‌اند. + ویت‌پرس با یک تم پیش‌فرض طراحی شده برای مستندات فنی ارائه می‌شود. این صفحه‌ای که اکنون در حال خواندن آن هستید و همچنین مستندات [Vite](https://vite.dev/)، [Rollup](https://rollupjs.org/)، [Pinia](https://pinia.vuejs.org/)، [VueUse](https://vueuse.org/)، [Vitest](https://vitest.dev/)، [D3](https://d3js.org/)، [UnoCSS](https://unocss.dev/)، [Iconify](https://iconify.design/) و [بسیاری دیگر](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code) با استفاده از ویت‌پرس ساخته شده‌اند. [مستندات رسمی Vue.js](https://vuejs.org/) نیز بر پایه ویت‌پرس ساخته شده است، اما از یک تم سفارشی که بین چندین ترجمه مشترک است استفاده می‌کند. @@ -30,7 +28,7 @@ description: ویت‌پرس یک تولیدکننده سایت ایستا بر ویت‌پرس هدف ارائه یک تجربه عالی برای توسعه دهنده (DX) هنگام کار با محتوای Markdown را دارد. -- **[قدرت گرفته از Vite:](https://vitejs.dev/)** شروع سرور فوری، با بازتاب ویرایش‌ها به صورت آنی (<100ms) بدون بارگذاری مجدد صفحه. +- **[قدرت گرفته از Vite:](https://vite.dev/)** شروع سرور فوری، با بازتاب ویرایش‌ها به صورت آنی (<100ms) بدون بارگذاری مجدد صفحه. - **[افزونه‌های داخلی Markdown:](./markdown)** استفاده از Frontmatter، جداول، syntax highlighting... هرچه که بخواهید. ویت‌پرس به ویژه ویژگی‌های پیشرفته زیادی برای کار با بلوک‌های کد فراهم می‌کند، که آن را برای مستندات فنی بسیار مناسب می‌کند. diff --git a/docs/fa/index.md b/docs/fa/index.md index d36e6a2c..c8f263fd 100644 --- a/docs/fa/index.md +++ b/docs/fa/index.md @@ -21,16 +21,16 @@ hero: alt: ویت‌پرس features: - - icon: 📝 + - icon: title: تمرکز روی محتوا details: ایجاد سایت‌های مستند‌سازی زیبا بدون زحمت و فقط با Markdown - - icon: + - icon: title: لذت از تجربه توسعه با Vite details: شروع فوری سرور، به‌روزرسانی‌های سریع و استفاده از افزونه‌های اکوسیستم Vite - - icon: + - icon: title: شخصی‌سازی با Vue details: استفاده مستقیم از syntax و کامپوننت‌های Vue در Markdown، یا ایجاد تم‌های شخصی به کمک Vue - - icon: 🚀 + - icon: title: ارسال سایت های سریع details: بارگذاری اولیه سریع با HTML ایستا، ناوبری سریع پس از بارگیری با مسیریابی سمت کلاینت --- diff --git a/docs/fa/reference/default-theme-carbon-ads.md b/docs/fa/reference/default-theme-carbon-ads.md index 6533d51b..6b9309c1 100644 --- a/docs/fa/reference/default-theme-carbon-ads.md +++ b/docs/fa/reference/default-theme-carbon-ads.md @@ -11,7 +11,8 @@ export default { themeConfig: { carbonAds: { code: 'your-carbon-code', - placement: 'your-carbon-placement' + placement: 'your-carbon-placement', + format: 'classic' } } } @@ -19,8 +20,10 @@ export default { این مقادیر برای فراخوانی اسکریپت CDN Carbon به شکل زیر استفاده می‌شوند. +گزینه `format` از مقادیر `classic`، `responsive` و `cover` پشتیبانی می‌کند. + ```js -`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}` +`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}&format=${format}` ``` برای یادگیری بیشتر درباره پیکربندی تبلیغات Carbon، لطفاً به [وب‌سایت Carbon Ads](https://www.carbonads.net/) مراجعه کنید. diff --git a/docs/fa/reference/default-theme-config.md b/docs/fa/reference/default-theme-config.md index 391422a2..aaa21b28 100644 --- a/docs/fa/reference/default-theme-config.md +++ b/docs/fa/reference/default-theme-config.md @@ -382,16 +382,20 @@ export default { themeConfig: { carbonAds: { code: 'your-carbon-code', - placement: 'your-carbon-placement' + placement: 'your-carbon-placement', + format: 'classic' } } } ``` +گزینه `format` از مقادیر `classic`، `responsive` و `cover` پشتیبانی می‌کند. + ```ts export interface CarbonAdsOptions { code: string placement: string + format?: 'classic' | 'responsive' | 'cover' } ``` diff --git a/docs/fa/reference/site-config.md b/docs/fa/reference/site-config.md index 2fd3cdc5..072cfa08 100644 --- a/docs/fa/reference/site-config.md +++ b/docs/fa/reference/site-config.md @@ -432,7 +432,7 @@ export default { - نوع: `string` - پیش‌فرض: `./.vitepress/cache` -دایرکتوری برای فایل‌های کش، نسبت به [ریشه پروژه](../guide/routing#root-and-source-directory). همچنین ببینید: [cacheDir](https://vitejs.dev/config/shared-options.html#cachedir). +دایرکتوری برای فایل‌های کش، نسبت به [ریشه پروژه](../guide/routing#root-and-source-directory). همچنین ببینید: [cacheDir](https://vite.dev/config/shared-options.html#cachedir). ```ts export default { @@ -474,13 +474,6 @@ export default { } ``` -### metaChunk {#metachunk} - -- نوع: `boolean` -- پیش‌فرض: `false` - -زمانی که به `true` تنظیم شود، فراداده‌های صفحات را به یک قسمت جداگانه جاوااسکریپت استخراج می‌کند به جای درون‌گذاری آن در HTML اولیه. این کار باعث کاهش بار HTML هر صفحه می‌شود و فراداده‌های صفحات قابل کش شدن می‌شود، که منجر به کاهش پهنای باند سرور می‌شود وقتی که صفحات زیادی در سایت دارید. - ### mpa {#mpa} - نوع: `boolean` @@ -514,7 +507,7 @@ export default { آیا زمان آخرین به‌روزرسانی برای هر صفحه با استفاده از Git دریافت شود. این زمان در داده‌های هر صفحه گنجانده خواهد شد و از طریق [`useData`](./runtime-api#usedata) قابل دسترسی خواهد بود. -وقتی از تم پیش‌فرض استفاده می‌کنید، فعال کردن این گزینه زمان آخرین به‌روزرسانی هر صفحه را نمایش می‌دهد. می‌توانید متن را از طریق گزینه [`themeConfig.lastUpdatedText`](./default-theme-config#lastupdatedtext) سفارشی کنید. +وقتی از تم پیش‌فرض استفاده می‌کنید، فعال کردن این گزینه زمان آخرین به‌روزرسانی هر صفحه را نمایش می‌دهد. می‌توانید متن را از طریق گزینه [`themeConfig.lastUpdated.text`](./default-theme-config#lastupdated) سفارشی کنید. ## سفارشی‌سازی {#customization} @@ -536,7 +529,7 @@ export default { - نوع: `import('vite').UserConfig` -پیکربندی خام [Vite Config](https://vitejs.dev/config/) را به سرور توسعه داخلی / بسته‌بند Vite ارسال کنید. +پیکربندی خام [Vite Config](https://vite.dev/config/) را به سرور توسعه داخلی / بسته‌بند Vite ارسال کنید. ```js export default { diff --git a/docs/ja/config.ts b/docs/ja/config.ts index 0631ca82..e997b9dd 100644 --- a/docs/ja/config.ts +++ b/docs/ja/config.ts @@ -1,9 +1,30 @@ -import { createRequire } from 'module' -import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' +import { createRequire } from 'node:module' +import { + defineAdditionalConfig, + type DefaultTheme, + type MarkdownLocaleOptions +} from 'vitepress' const require = createRequire(import.meta.url) const pkg = require('vitepress/package.json') +export const markdown: MarkdownLocaleOptions = { + container: { + tipLabel: 'ヒント', + infoLabel: '情報', + warningLabel: '警告', + dangerLabel: '危険', + detailsLabel: '詳細', + noteLabel: 'メモ', + importantLabel: '重要', + cautionLabel: '注意' + }, + codeCopyButton: { + tooltipText: 'コードをコピー', + copiedText: 'コピー完了' + } +} + export default defineAdditionalConfig({ description: 'Vite と Vue による静的サイトジェネレーター', @@ -231,8 +252,6 @@ function searchOptions(): Partial { afterToolCallText: '検索しました', stoppedStreamingText: 'この応答を停止しました', errorTitleText: 'チャットエラー', - threadDepthExceededMessage: - '回答の正確性を保つため、この会話は終了しました。', startNewConversationButtonText: '新しい会話を開始' } } diff --git a/docs/ja/guide/asset-handling.md b/docs/ja/guide/asset-handling.md index 319963ca..42ae9d61 100644 --- a/docs/ja/guide/asset-handling.md +++ b/docs/ja/guide/asset-handling.md @@ -6,7 +6,7 @@ description: VitePressで画像、メディア、フォントなどの静的ア ## 静的アセットの参照 {#referencing-static-assets} -すべての Markdown ファイルは Vue コンポーネントにコンパイルされ、[Vite](https://vitejs.dev/guide/assets.html) によって処理されます。Markdown 内では、相対 URL を使ってアセットを参照することが **推奨されます**。 +すべての Markdown ファイルは Vue コンポーネントにコンパイルされ、[Vite](https://vite.dev/guide/assets.html) によって処理されます。Markdown 内では、相対 URL を使ってアセットを参照することが **推奨されます**。 ```md ![画像](./image.png) diff --git a/docs/ja/guide/extending-default-theme.md b/docs/ja/guide/extending-default-theme.md index 8e26243c..2abe4d84 100644 --- a/docs/ja/guide/extending-default-theme.md +++ b/docs/ja/guide/extending-default-theme.md @@ -122,7 +122,7 @@ export default { } satisfies Theme ``` -Vite を使っているため、Vite の [glob import 機能](https://vitejs.dev/guide/features.html#glob-import) を利用してディレクトリ内のコンポーネントを自動登録することもできます。 +Vite を使っているため、Vite の [glob import 機能](https://vite.dev/guide/features.html#glob-import) を利用してディレクトリ内のコンポーネントを自動登録することもできます。 ## レイアウトスロット {#layout-slots} @@ -311,7 +311,7 @@ provide('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => { ## 内部コンポーネントの置き換え {#overriding-internal-components} -Vite の [エイリアス](https://vitejs.dev/config/shared-options.html#resolve-alias) を使って、デフォルトテーマのコンポーネントを独自のものに置き換えられます。 +Vite の [エイリアス](https://vite.dev/config/shared-options.html#resolve-alias) を使って、デフォルトテーマのコンポーネントを独自のものに置き換えられます。 ```ts import { fileURLToPath, URL } from 'node:url' diff --git a/docs/ja/guide/getting-started.md b/docs/ja/guide/getting-started.md index 5b9a8e2a..91a85076 100644 --- a/docs/ja/guide/getting-started.md +++ b/docs/ja/guide/getting-started.md @@ -12,7 +12,7 @@ description: VitePressのインストール、セットアップ、開発サー ### 前提条件 {#prerequisites} -- [Node.js](https://nodejs.org/) バージョン 20 以上 +- [Node.js](https://nodejs.org/) バージョン 22 以上 - VitePress をコマンドラインインターフェース (CLI) で操作するためのターミナル - [Markdown](https://en.wikipedia.org/wiki/Markdown) 構文に対応したテキストエディタ - 推奨: [VSCode](https://code.visualstudio.com/) と [公式 Vue 拡張](https://marketplace.visualstudio.com/items?itemName=Vue.volar) @@ -40,7 +40,7 @@ $ bun add -D vitepress@next ::: ::: tip 注意 -VitePress は ESM 専用パッケージです。`require()` を使ってインポートせず、最も近い `package.json` に `"type": "module"` を含めるか、`.vitepress/config.js` を `.mjs` / `.mts` に変更してください。詳しくは [Vite のトラブルシューティングガイド](http://vitejs.dev/guide/troubleshooting.html#this-package-is-esm-only) を参照してください。また、非同期 CJS コンテキスト内では `await import('vitepress')` を使用できます。 +VitePress は ESM 専用パッケージです。`require()` を使ってインポートせず、最も近い `package.json` に `"type": "module"` を含めるか、`.vitepress/config.js` を `.mjs` / `.mts` に変更してください。詳しくは [Vite のトラブルシューティングガイド](http://vite.dev/guide/troubleshooting.html#this-package-is-esm-only) を参照してください。また、非同期 CJS コンテキスト内では `await import('vitepress')` を使用できます。 ::: ### セットアップウィザード {#setup-wizard} diff --git a/docs/ja/guide/markdown.md b/docs/ja/guide/markdown.md index fc839fc3..190e3182 100644 --- a/docs/ja/guide/markdown.md +++ b/docs/ja/guide/markdown.md @@ -1,5 +1,6 @@ --- description: VitePress組み込みのMarkdown拡張機能。カスタムコンテナ、コードブロックのシンタックスハイライト、行ハイライト、コードグループなど。 +outline: deep --- # Markdown 拡張 {#markdown-extensions} @@ -110,7 +111,7 @@ lang: ja-JP :tada: :100: -すべての絵文字の [一覧はこちら](https://github.com/markdown-it/markdown-it-emoji/blob/master/lib/data/full.mjs)。 +すべての絵文字の [一覧はこちら](https://github.com/mdit-plugins/mdit-plugins/blob/main/packages/plugin-emoji/src/data/full.ts)。 ## 目次 {#table-of-contents} @@ -229,7 +230,7 @@ export default defineConfig({ ### 追加属性 {#additional-attributes} -カスタムコンテナには追加の属性を付与できます。この機能には [markdown-it-attrs](https://github.com/arve0/markdown-it-attrs) を使用しており、ほぼすべての Markdown 要素でサポートされます。たとえば `open` 属性を付けると、details ブロックをデフォルトで開いた状態にできます。 +カスタムコンテナには追加の属性を付与できます。この機能には [@mdit/plugin-attrs](https://mdit-plugins.github.io/attrs.html) を使用しており、ほぼすべての Markdown 要素でサポートされます。たとえば `open` 属性を付けると、details ブロックをデフォルトで開いた状態にできます。 **入力** @@ -251,7 +252,7 @@ console.log('こんにちは、VitePress!') ### `raw` -これは、VitePress でのスタイルやルーターの衝突を防ぐための特別なコンテナです。コンポーネントライブラリのドキュメント化に特に有用です。より強力な分離が必要であれば、[whyframe](https://whyframe.dev/docs/integrations/vitepress) も検討してください。 +これは、VitePress でのスタイルやルーターの衝突を防ぐための特別なコンテナです。コンポーネントライブラリのドキュメント化に特に有用です。 **構文** @@ -996,14 +997,14 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$ ## 画像の遅延読み込み {#image-lazy-loading} -Markdown で追加した各画像に対して遅延読み込みを有効化するには、設定ファイルで `lazyLoading` を `true` にします: +Markdown で追加した各画像に対して遅延読み込みを有効化するには、設定ファイルで `lazyLoad` を `true` にします: ```js export default { markdown: { image: { // 既定では画像の遅延読み込みは無効 - lazyLoading: true + lazyLoad: true } } } @@ -1015,15 +1016,15 @@ VitePress は Markdown レンダラーとして [markdown-it](https://github.com ```js import { defineConfig } from 'vitepress' -import markdownItAnchor from 'markdown-it-anchor' +import { headerLink } from '@mdit/plugin-anchor' import markdownItFoo from 'markdown-it-foo' export default defineConfig({ markdown: { - // markdown-it-anchor のオプション - // https://github.com/valeriangalliat/markdown-it-anchor#usage + // @mdit/plugin-anchor のオプション + // https://mdit-plugins.github.io/anchor.html anchor: { - permalink: markdownItAnchor.permalink.headerLink() + permalink: headerLink() }, // @mdit-vue/plugin-toc のオプション diff --git a/docs/ja/guide/routing.md b/docs/ja/guide/routing.md index f624bee0..a77e6062 100644 --- a/docs/ja/guide/routing.md +++ b/docs/ja/guide/routing.md @@ -304,7 +304,7 @@ export default { ローカルファイルから生成する例: ```js -import fs from 'fs' +import fs from 'node:fs' export default { paths() { diff --git a/docs/ja/guide/ssr-compat.md b/docs/ja/guide/ssr-compat.md index ec94a4a4..01a58c38 100644 --- a/docs/ja/guide/ssr-compat.md +++ b/docs/ja/guide/ssr-compat.md @@ -39,7 +39,7 @@ onMounted(() => { ### 条件付きインポート {#conditional-import} -[`import.meta.env.SSR`](https://vitejs.dev/guide/env-and-mode.html#env-variables) フラグ(Vite の環境変数の一部)を使って、依存関係を条件付きでインポートすることもできます。 +[`import.meta.env.SSR`](https://vite.dev/guide/env-and-mode.html#env-variables) フラグ(Vite の環境変数の一部)を使って、依存関係を条件付きでインポートすることもできます。 ```js if (!import.meta.env.SSR) { diff --git a/docs/ja/guide/using-vue.md b/docs/ja/guide/using-vue.md index 9cbc7608..cdbbb01f 100644 --- a/docs/ja/guide/using-vue.md +++ b/docs/ja/guide/using-vue.md @@ -203,7 +203,7 @@ Hello {{ 1 + 1 }} ## CSS プリプロセッサの利用 {#using-css-pre-processors} -VitePress は CSS プリプロセッサ(`.scss`、`.sass`、`.less`、`.styl`、`.stylus`)を[標準サポート](https://vitejs.dev/guide/features.html#css-pre-processors)しています。Vite 固有のプラグインは不要ですが、各プリプロセッサ本体のインストールは必要です。 +VitePress は CSS プリプロセッサ(`.scss`、`.sass`、`.less`、`.styl`、`.stylus`)を[標準サポート](https://vite.dev/guide/features.html#css-pre-processors)しています。Vite 固有のプラグインは不要ですが、各プリプロセッサ本体のインストールは必要です。 ``` # .scss / .sass diff --git a/docs/ja/guide/what-is-vitepress.md b/docs/ja/guide/what-is-vitepress.md index 1920adaf..88e8c9d9 100644 --- a/docs/ja/guide/what-is-vitepress.md +++ b/docs/ja/guide/what-is-vitepress.md @@ -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 ページを生成します。 -
      - +::: tip {no-title} まずは試してみたい? [クイックスタート](./getting-started) へどうぞ。 - -
      +::: ## ユースケース {#use-cases} - **ドキュメント** - VitePress には技術ドキュメント向けに設計されたデフォルトテーマが同梱されています。今あなたが読んでいるこのページのほか、[Vite](https://vitejs.dev/)、[Rollup](https://rollupjs.org/)、[Pinia](https://pinia.vuejs.org/)、[VueUse](https://vueuse.org/)、[Vitest](https://vitest.dev/)、[D3](https://d3js.org/)、[UnoCSS](https://unocss.dev/)、[Iconify](https://iconify.design/) など、[まだまだたくさん](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code)のドキュメントサイトで使われています。 + VitePress には技術ドキュメント向けに設計されたデフォルトテーマが同梱されています。今あなたが読んでいるこのページのほか、[Vite](https://vite.dev/)、[Rollup](https://rollupjs.org/)、[Pinia](https://pinia.vuejs.org/)、[VueUse](https://vueuse.org/)、[Vitest](https://vitest.dev/)、[D3](https://d3js.org/)、[UnoCSS](https://unocss.dev/)、[Iconify](https://iconify.design/) など、[まだまだたくさん](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code)のドキュメントサイトで使われています。 [公式の Vue.js ドキュメント](https://vuejs.org/) も VitePress をベースにしています(複数言語で共有されるカスタムテーマを使用)。 @@ -30,7 +28,7 @@ VitePress は、高速でコンテンツ中心の Web サイトを構築する VitePress は、Markdown コンテンツを扱う際の優れた開発体験(DX)を目指しています。 -- **[Vite 駆動](https://vitejs.dev/)**:即時サーバー起動、編集はページリロードなしで常に瞬時(<100ms)に反映。 +- **[Vite 駆動](https://vite.dev/)**:即時サーバー起動、編集はページリロードなしで常に瞬時(<100ms)に反映。 - **[ビルトインの Markdown 拡張](./markdown)**:Frontmatter、表、シンタックスハイライト…必要なものはひと通り。特にコードブロック周りの機能が充実しており、高度な技術ドキュメントに最適です。 diff --git a/docs/ja/index.md b/docs/ja/index.md index b5be770b..665ad78e 100644 --- a/docs/ja/index.md +++ b/docs/ja/index.md @@ -21,16 +21,16 @@ hero: alt: VitePress features: - - icon: 📝 + - icon: title: コンテンツに集中 details: Markdown だけで、美しいドキュメントサイトを簡単に作成できます。 - - icon: + - icon: title: Vite の開発体験を享受 details: 即時サーバー起動、超高速ホットリロード、そして Vite エコシステムのプラグイン活用。 - - icon: + - icon: title: Vue でカスタマイズ details: Markdown 内で直接 Vue 構文やコンポーネントを利用したり、Vue で独自テーマを構築できます。 - - icon: 🚀 + - icon: title: 高速サイトを公開 details: 静的 HTML による高速初期ロードと、クライアントサイドルーティングによる快適なページ遷移。 --- diff --git a/docs/ja/reference/default-theme-carbon-ads.md b/docs/ja/reference/default-theme-carbon-ads.md index 77383ce7..db0e76f7 100644 --- a/docs/ja/reference/default-theme-carbon-ads.md +++ b/docs/ja/reference/default-theme-carbon-ads.md @@ -11,7 +11,8 @@ export default { themeConfig: { carbonAds: { code: 'your-carbon-code', - placement: 'your-carbon-placement' + placement: 'your-carbon-placement', + format: 'classic' } } } @@ -19,8 +20,10 @@ export default { これらの値は、次のように Carbon の CDN スクリプトを呼び出すために使用されます。 +`format` オプションには `classic`、`responsive`、`cover` を指定できます。 + ```js -`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}` +`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}&format=${format}` ``` Carbon Ads の設定について詳しくは、[Carbon Ads のウェブサイト](https://www.carbonads.net/)を参照してください。 diff --git a/docs/ja/reference/default-theme-config.md b/docs/ja/reference/default-theme-config.md index cc0770a4..9da3db10 100644 --- a/docs/ja/reference/default-theme-config.md +++ b/docs/ja/reference/default-theme-config.md @@ -367,16 +367,20 @@ export default { themeConfig: { carbonAds: { code: 'your-carbon-code', - placement: 'your-carbon-placement' + placement: 'your-carbon-placement', + format: 'classic' } } } ``` +`format` オプションには `classic`、`responsive`、`cover` を指定できます。 + ```ts export interface CarbonAdsOptions { code: string placement: string + format?: 'classic' | 'responsive' | 'cover' } ``` diff --git a/docs/ja/reference/site-config.md b/docs/ja/reference/site-config.md index 44636f89..6d7c3130 100644 --- a/docs/ja/reference/site-config.md +++ b/docs/ja/reference/site-config.md @@ -430,7 +430,7 @@ export default { - 型: `string` - 既定値: `./.vitepress/cache` -キャッシュファイル用ディレクトリ([プロジェクトルート](../guide/routing#root-and-source-directory) からの相対パス)。参考: [cacheDir](https://vitejs.dev/config/shared-options.html#cachedir) +キャッシュファイル用ディレクトリ([プロジェクトルート](../guide/routing#root-and-source-directory) からの相対パス)。参考: [cacheDir](https://vite.dev/config/shared-options.html#cachedir) ```ts export default { @@ -472,13 +472,6 @@ export default { } ``` -### metaChunk - -- 型: `boolean` -- 既定値: `false` - -`true` にすると、各ページのメタデータを初期 HTML にインラインせず、別の JavaScript チャンクに抽出します。これにより各ページの HTML ペイロードが小さくなり、メタデータをキャッシュ可能にすることで、多数のページがあるサイトでサーバ帯域を削減できます。 - ### mpa - 型: `boolean` @@ -512,7 +505,7 @@ export default { Git を使って各ページの最終更新時刻を取得します。タイムスタンプは各ページのデータに含まれ、[`useData`](./runtime-api#usedata) から参照できます。 -デフォルトテーマ使用時にこのオプションを有効にすると、各ページの最終更新時刻が表示されます。テキストは [`themeConfig.lastUpdatedText`](./default-theme-config#lastupdatedtext) でカスタマイズ可能です。 +デフォルトテーマ使用時にこのオプションを有効にすると、各ページの最終更新時刻が表示されます。テキストは [`themeConfig.lastUpdated.text`](./default-theme-config#lastupdated) でカスタマイズ可能です。 ## カスタマイズ {#customization} @@ -534,7 +527,7 @@ export default { - 型: `import('vite').UserConfig` -内部の Vite 開発サーバ/バンドラへ生の [Vite Config](https://vitejs.dev/config/) を渡します。 +内部の Vite 開発サーバ/バンドラへ生の [Vite Config](https://vite.dev/config/) を渡します。 ```js export default { diff --git a/docs/ko/config.ts b/docs/ko/config.ts index 4a076036..af4b435f 100644 --- a/docs/ko/config.ts +++ b/docs/ko/config.ts @@ -1,9 +1,30 @@ -import { createRequire } from 'module' -import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' +import { createRequire } from 'node:module' +import { + defineAdditionalConfig, + type DefaultTheme, + type MarkdownLocaleOptions +} from 'vitepress' const require = createRequire(import.meta.url) const pkg = require('vitepress/package.json') +export const markdown: MarkdownLocaleOptions = { + container: { + tipLabel: '팁', + infoLabel: '정보', + warningLabel: '경고', + dangerLabel: '위험', + detailsLabel: '세부 정보', + noteLabel: '노트', + importantLabel: '중요', + cautionLabel: '주의' + }, + codeCopyButton: { + tooltipText: '코드 복사', + copiedText: '복사됨' + } +} + export default defineAdditionalConfig({ description: 'Vite 및 Vue 기반 정적 사이트 생성기.', @@ -303,8 +324,6 @@ function searchOptions(): Partial { afterToolCallText: '검색함', stoppedStreamingText: '이 응답을 중지했습니다', errorTitleText: '채팅 오류', - threadDepthExceededMessage: - '정확성을 유지하기 위해 이 대화는 종료되었습니다.', startNewConversationButtonText: '새 대화 시작' } } diff --git a/docs/ko/guide/asset-handling.md b/docs/ko/guide/asset-handling.md index 5d7a9bbb..3b9efb02 100644 --- a/docs/ko/guide/asset-handling.md +++ b/docs/ko/guide/asset-handling.md @@ -6,7 +6,7 @@ description: VitePress에서 이미지, 미디어, 글꼴 등 정적 에셋을 ## 정적 에셋 참조하기 {#referencing-static-assets} -모든 마크다운 파일은 Vue 컴포넌트로 컴파일되어 [Vite](https://vitejs.dev/guide/assets.html)에 의해 처리됩니다. 모든 에셋은 상대 URL을 사용하여 참조할 수 있으며, **참조해야 합니다**: +모든 마크다운 파일은 Vue 컴포넌트로 컴파일되어 [Vite](https://vite.dev/guide/assets.html)에 의해 처리됩니다. 모든 에셋은 상대 URL을 사용하여 참조할 수 있으며, **참조해야 합니다**: ```md ![이미지](./image.png) diff --git a/docs/ko/guide/extending-default-theme.md b/docs/ko/guide/extending-default-theme.md index e87bfa1a..2c450b8b 100644 --- a/docs/ko/guide/extending-default-theme.md +++ b/docs/ko/guide/extending-default-theme.md @@ -119,7 +119,7 @@ export default { } satisfies Theme ``` -Vite를 사용하므로, Vite의 [glob import 기능](https://vitejs.dev/guide/features.html#glob-import)을 활용하여 컴포넌트 디렉터리를 자동으로 등록할 수 있습니다. +Vite를 사용하므로, Vite의 [glob import 기능](https://vite.dev/guide/features.html#glob-import)을 활용하여 컴포넌트 디렉터리를 자동으로 등록할 수 있습니다. ## 레이아웃 슬롯 {#layout-slots} @@ -309,7 +309,7 @@ provide('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => { ## 내부 컴포넌트 재정의하기 {#overriding-internal-components} -Vite의 [별칭](https://vitejs.dev/config/shared-options.html#resolve-alias)을 사용하여 기본 테마 컴포넌트를 커스텀 컴포넌트로 대체할 수 있습니다: +Vite의 [별칭](https://vite.dev/config/shared-options.html#resolve-alias)을 사용하여 기본 테마 컴포넌트를 커스텀 컴포넌트로 대체할 수 있습니다: ```ts import { fileURLToPath, URL } from 'node:url' diff --git a/docs/ko/guide/getting-started.md b/docs/ko/guide/getting-started.md index 5e28c2d8..807dca71 100644 --- a/docs/ko/guide/getting-started.md +++ b/docs/ko/guide/getting-started.md @@ -12,7 +12,7 @@ description: VitePress를 시작하세요. 문서 사이트를 설치하고, 스 ### 사전 준비 사항 {#prerequisites} -- [Node.js](https://nodejs.org/) 버전 20 이상. +- [Node.js](https://nodejs.org/) 버전 22 이상. - VitePress를 명령줄 인터페이스(CLI)를 통해 접근하기 위한 터미널. - [마크다운](https://en.wikipedia.org/wiki/Markdown) 문법 지원이 있는 텍스트 에디터. - [VSCode](https://code.visualstudio.com/)와 [공식 Vue 확장 프로그램](https://marketplace.visualstudio.com/items?itemName=Vue.volar) 사용을 권장합니다. @@ -41,7 +41,7 @@ $ bun add -D vitepress@next ::: tip 참고 -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')`를 사용할 수 있습니다. ::: diff --git a/docs/ko/guide/markdown.md b/docs/ko/guide/markdown.md index 12d832df..f0c7ce91 100644 --- a/docs/ko/guide/markdown.md +++ b/docs/ko/guide/markdown.md @@ -1,5 +1,6 @@ --- description: 커스텀 컨테이너, 구문 강조 코드 블록, 라인 하이라이팅, 코드 그룹 등 VitePress 내장 마크다운 확장 기능을 소개합니다. +outline: deep --- # 마크다운 확장 기능 {#markdown-extensions} @@ -112,7 +113,7 @@ lang: en-US :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} @@ -231,7 +232,7 @@ export default defineConfig({ ### `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} -마크다운을 통해 추가된 각 이미지에 대해 지연 로딩을 활성화하려면 구성 파일에서 `lazyLoading`을 `true`로 설정하세요: +마크다운을 통해 추가된 각 이미지에 대해 지연 로딩을 활성화하려면 구성 파일에서 `lazyLoad`을 `true`로 설정하세요: ```js export default { markdown: { image: { // 이미지 지연 로딩은 기본적으로 비활성화 되어 있습니다 - lazyLoading: true + lazyLoad: true } } } @@ -944,15 +945,15 @@ VitePress는 마크다운 렌더러로 [markdown-it](https://github.com/markdown ```js import { defineConfig } from 'vitepress' -import markdownItAnchor from 'markdown-it-anchor' +import { headerLink } from '@mdit/plugin-anchor' import markdownItFoo from 'markdown-it-foo' export default defineConfig({ markdown: { - // markdown-it-anchor의 옵션 - // https://github.com/valeriangalliat/markdown-it-anchor#usage + // @mdit/plugin-anchor의 옵션 + // https://mdit-plugins.github.io/anchor.html anchor: { - permalink: markdownItAnchor.permalink.headerLink() + permalink: headerLink() }, // @mdit-vue/plugin-toc의 옵션 diff --git a/docs/ko/guide/migration-from-vitepress-0.md b/docs/ko/guide/migration-from-vitepress-0.md index f3ba5293..d1dad78c 100644 --- a/docs/ko/guide/migration-from-vitepress-0.md +++ b/docs/ko/guide/migration-from-vitepress-0.md @@ -14,7 +14,7 @@ If you're coming from VitePress 0.x version, there're several breaking changes d - `repo`, `repoLabel`, `docsDir`, `docsBranch`, `editLinks`, `editLinkText` are removed in favor of more flexible api. - For adding GitHub link with icon to the nav, use [Social Links](../reference/default-theme-nav#navigation-links) feature. - For adding "Edit this page" feature, use [Edit Link](../reference/default-theme-edit-link) feature. -- `lastUpdated` option is now split into `config.lastUpdated` and `themeConfig.lastUpdatedText`. +- `lastUpdated` option is now split into `config.lastUpdated` and `themeConfig.lastUpdated.text`. - `carbonAds.carbon` is changed to `carbonAds.code`. ## Frontmatter Config {#frontmatter-config} diff --git a/docs/ko/guide/routing.md b/docs/ko/guide/routing.md index b0a8e23e..35c7621e 100644 --- a/docs/ko/guide/routing.md +++ b/docs/ko/guide/routing.md @@ -291,7 +291,7 @@ export default { 로컬 파일에서 경로 생성: ```js -import fs from 'fs' +import fs from 'node:fs' export default { paths() { diff --git a/docs/ko/guide/ssr-compat.md b/docs/ko/guide/ssr-compat.md index 9e58ec18..a2e9cc63 100644 --- a/docs/ko/guide/ssr-compat.md +++ b/docs/ko/guide/ssr-compat.md @@ -39,7 +39,7 @@ onMounted(() => { ### 조건부 가져오기 {#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 if (!import.meta.env.SSR) { diff --git a/docs/ko/guide/using-vue.md b/docs/ko/guide/using-vue.md index 9f595b7f..5c93dabc 100644 --- a/docs/ko/guide/using-vue.md +++ b/docs/ko/guide/using-vue.md @@ -204,7 +204,7 @@ Vue 보간 문법을 회피하려면, `` 또는 다른 엘리먼트에 `v- ## 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 diff --git a/docs/ko/guide/what-is-vitepress.md b/docs/ko/guide/what-is-vitepress.md index 2c2cdaf2..fc34e9af 100644 --- a/docs/ko/guide/what-is-vitepress.md +++ b/docs/ko/guide/what-is-vitepress.md @@ -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 페이지를 생성합니다. -
      - +::: tip {no-title} 그냥 한번 사용해보고 싶으신가요? [빠른 시작](./getting-started)으로 건너뛰세요. - -
      +::: ## 사용 사례 {#use-cases} - **문서화** - VitePress는 기술 문서를 위해 설계된 기본 테마가 함께 제공됩니다. 지금 읽고 있는 이 페이지와 [Vite](https://vitejs.dev/), [Rollup](https://rollupjs.org/), [Pinia](https://pinia.vuejs.org/), [VueUse](https://vueuse.org/), [Vitest](https://vitest.dev/), [D3](https://d3js.org/), [UnoCSS](https://unocss.dev/), [Iconify](https://iconify.design/) 및 [다양한 프로젝트](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code) 문서는 모두 이 테마를 기반으로 합니다. + VitePress는 기술 문서를 위해 설계된 기본 테마가 함께 제공됩니다. 지금 읽고 있는 이 페이지와 [Vite](https://vite.dev/), [Rollup](https://rollupjs.org/), [Pinia](https://pinia.vuejs.org/), [VueUse](https://vueuse.org/), [Vitest](https://vitest.dev/), [D3](https://d3js.org/), [UnoCSS](https://unocss.dev/), [Iconify](https://iconify.design/) 및 [다양한 프로젝트](https://github.com/search?q=/%22vitepress%22:+/+path:/(?:package%7Cdeno)%5C.jsonc?$/+NOT+is:fork+NOT+is:archived&type=code) 문서는 모두 이 테마를 기반으로 합니다. [Vue.js 공식 문서](https://vuejs.org/)도 VitePress 기반으로 되어 있으며, 여러 번역본에 걸쳐 공유되는 커스텀 테마를 사용합니다. @@ -30,7 +28,7 @@ VitePress는 빠르고 컨텐츠 중심의 웹사이트를 구축하기 위해 VitePress는 마크다운 컨텐츠를 다룰 때 훌륭한 개발자 경험(DX)을 제공하고자 합니다. -- **[Vite로 작동](https://vitejs.dev/)**: 즉각적인 서버 시작 가능, 페이지 새로고침 없이 즉시(<100ms) 수정 사항 반영. +- **[Vite로 작동](https://vite.dev/)**: 즉각적인 서버 시작 가능, 페이지 새로고침 없이 즉시(<100ms) 수정 사항 반영. - **[내장된 마크다운 확장 기능](./markdown)**: 서문, 표, 구문 강조 등 무엇이든 가능. 특히 VitePress는 코드 블록 작업을 위한 고급 기능을 많이 제공하여 기술적 문서에 이상적. diff --git a/docs/ko/index.md b/docs/ko/index.md index d5a6c1a2..fa302470 100644 --- a/docs/ko/index.md +++ b/docs/ko/index.md @@ -21,16 +21,16 @@ hero: alt: VitePress features: - - icon: 📝 + - icon: title: 콘텐츠에 집중 details: 마크다운으로만 아름다운 문서 사이트를 쉽게 만들기. - - icon: + - icon: title: Vite DX(개발자 경험) 즐겨보기 details: 즉각적인 서버 시작, 매우 빠른 업데이트, Vite 생태계 플러그인을 활용. - - icon: + - icon: title: Vue로 커스터마이징 details: Vue 문법과 컴포넌트를 마크다운에서 직접 사용하거나 Vue로 커스텀 테마를 구축. - - icon: 🚀 + - icon: title: 웹사이트를 빠르게 제공 details: 정적 HTML로 빠른 초기 로딩, 클라이언트 측 라우팅을 통한 빠른 탐색. --- diff --git a/docs/ko/reference/default-theme-carbon-ads.md b/docs/ko/reference/default-theme-carbon-ads.md index 4dcdf832..d6e7150a 100644 --- a/docs/ko/reference/default-theme-carbon-ads.md +++ b/docs/ko/reference/default-theme-carbon-ads.md @@ -11,7 +11,8 @@ export default { themeConfig: { carbonAds: { code: 'your-carbon-code', - placement: 'your-carbon-placement' + placement: 'your-carbon-placement', + format: 'classic' } } } @@ -19,8 +20,10 @@ export default { 이 값들은 아래와 같이 카본 CDN 스크립트를 호출하는 데 사용됩니다. +`format` 옵션에는 `classic`, `responsive`, `cover`를 사용할 수 있습니다. + ```js -`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}` +`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}&format=${format}` ``` 카본 광고 구성에 대해 더 알고 싶다면 [카본 광고 웹사이트](https://www.carbonads.net/)를 방문하세요. diff --git a/docs/ko/reference/default-theme-config.md b/docs/ko/reference/default-theme-config.md index c61833e1..181febd5 100644 --- a/docs/ko/reference/default-theme-config.md +++ b/docs/ko/reference/default-theme-config.md @@ -366,16 +366,20 @@ export default { themeConfig: { carbonAds: { code: 'your-carbon-code', - placement: 'your-carbon-placement' + placement: 'your-carbon-placement', + format: 'classic' } } } ``` +`format` 옵션에는 `classic`, `responsive`, `cover`를 사용할 수 있습니다. + ```ts export interface CarbonAdsOptions { code: string placement: string + format?: 'classic' | 'responsive' | 'cover' } ``` diff --git a/docs/ko/reference/site-config.md b/docs/ko/reference/site-config.md index f22569a9..074cf43f 100644 --- a/docs/ko/reference/site-config.md +++ b/docs/ko/reference/site-config.md @@ -430,7 +430,7 @@ export default { - 타입: `string` - 기본값: `./.vitepress/cache` -캐시 파일을 위한 디렉터리입니다. [프로젝트 루트](../guide/routing#root-and-source-directory)에 상대적입니다. [cacheDir](https://vitejs.dev/config/shared-options.html#cachedir)을 참고하세요. +캐시 파일을 위한 디렉터리입니다. [프로젝트 루트](../guide/routing#root-and-source-directory)에 상대적입니다. [cacheDir](https://vite.dev/config/shared-options.html#cachedir)을 참고하세요. ```ts export default { @@ -472,13 +472,6 @@ export default { } ``` -### metaChunk - -- 타입: `boolean` -- 기본값: `false` - -`true`로 설정하면 페이지 메타데이터를 초기 HTML에 인라인으로 삽입하는 대신 별도의 JavaScript 청크로 추출합니다. 이렇게 하면 각 페이지의 HTML 페이로드가 작아지고 페이지 메타데이터를 캐시할 수 있어, 사이트에 많은 페이지가 있을 때 서버 대역폭을 줄일 수 있습니다. - ### mpa - 타입: `boolean` @@ -512,7 +505,7 @@ export default { 각 페이지의 마지막 업데이트 타임스탬프를 Git을 사용하여 가져올지 여부를 설정합니다. 타임스탬프는 각 페이지의 페이지 데이터에 포함되며, [`useData`](./runtime-api#usedata)를 통해 접근할 수 있습니다. -기본 테마를 사용할 때, 이 옵션을 활성화하면 각 페이지의 마지막 업데이트 시간이 표시됩니다. [`themeConfig.lastUpdatedText`](./default-theme-config#lastupdatedtext) 옵션을 통해 텍스트를 커스터마이징할 수 있습니다. +기본 테마를 사용할 때, 이 옵션을 활성화하면 각 페이지의 마지막 업데이트 시간이 표시됩니다. [`themeConfig.lastUpdated.text`](./default-theme-config#lastupdated) 옵션을 통해 텍스트를 커스터마이징할 수 있습니다. ## 커스터마이징 {#customization} @@ -534,7 +527,7 @@ export default { - 타입: `import('vite').UserConfig` -내부 Vite 개발 서버/번들러에 직접 [Vite 구성](https://vitejs.dev/config/)을 전달합니다. +내부 Vite 개발 서버/번들러에 직접 [Vite 구성](https://vite.dev/config/)을 전달합니다. ```js export default { diff --git a/docs/package.json b/docs/package.json index bf99e78d..9c2be9c1 100644 --- a/docs/package.json +++ b/docs/package.json @@ -12,10 +12,10 @@ "devDependencies": { "@lunariajs/core": "^0.1.1", "markdown-it-mathjax3": "^4.3.2", - "open-cli": "^8.0.0", - "postcss-rtlcss": "^5.7.1", + "open-cli": "^9.0.0", + "postcss-rtlcss": "^6.0.0", "vitepress": "workspace:*", - "vitepress-plugin-group-icons": "1.7.1", - "vitepress-plugin-llms": "^1.12.0" + "vitepress-plugin-group-icons": "^1.7.6", + "vitepress-plugin-llms": "^1.13.4" } } diff --git a/docs/pt/config.ts b/docs/pt/config.ts index 5f71d84f..cb0afb9d 100644 --- a/docs/pt/config.ts +++ b/docs/pt/config.ts @@ -1,9 +1,30 @@ -import { createRequire } from 'module' -import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' +import { createRequire } from 'node:module' +import { + defineAdditionalConfig, + type DefaultTheme, + type MarkdownLocaleOptions +} from 'vitepress' const require = createRequire(import.meta.url) const pkg = require('vitepress/package.json') +export const markdown: MarkdownLocaleOptions = { + container: { + tipLabel: '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({ description: 'Gerador de Site Estático desenvolvido com Vite e Vue.', @@ -264,8 +285,6 @@ function searchOptions(): Partial { afterToolCallText: 'Pesquisado', stoppedStreamingText: 'Você interrompeu esta resposta', errorTitleText: 'Erro no chat', - threadDepthExceededMessage: - 'Esta conversa foi encerrada para manter respostas precisas.', startNewConversationButtonText: 'Iniciar uma nova conversa' } } diff --git a/docs/pt/guide/asset-handling.md b/docs/pt/guide/asset-handling.md index f7b4a937..620beb49 100644 --- a/docs/pt/guide/asset-handling.md +++ b/docs/pt/guide/asset-handling.md @@ -6,7 +6,7 @@ description: Aprenda a referenciar e manipular ativos estáticos como imagens, m ## 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 ![Uma imagem](./imagem.png) diff --git a/docs/pt/guide/extending-default-theme.md b/docs/pt/guide/extending-default-theme.md index 31527390..99db5b75 100644 --- a/docs/pt/guide/extending-default-theme.md +++ b/docs/pt/guide/extending-default-theme.md @@ -119,7 +119,7 @@ export default { } 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} @@ -309,7 +309,7 @@ Em breve. ## 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 import { fileURLToPath, URL } from 'node:url' diff --git a/docs/pt/guide/getting-started.md b/docs/pt/guide/getting-started.md index 182a3738..5f083143 100644 --- a/docs/pt/guide/getting-started.md +++ b/docs/pt/guide/getting-started.md @@ -12,7 +12,7 @@ Você pode experimentar VitePress diretamente no seu navegador em [StackBlitz](h ### Pré-requisitos {#prerequisites} -- [Node.js](https://nodejs.org/) na versão 20 ou superior. +- [Node.js](https://nodejs.org/) na versão 22 ou superior. - Terminal para acessar VitePress através da sua interface de linha de comando (CLI). - Editor de texto com suporte a sintaxe [Markdown](https://en.wikipedia.org/wiki/Markdown). - [VSCode](https://code.visualstudio.com/) é recomendado, junto com a [extensão oficial Vue](https://marketplace.visualstudio.com/items?itemName=Vue.volar). @@ -41,7 +41,7 @@ $ bun add -D vitepress@next ::: 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')`. ::: diff --git a/docs/pt/guide/markdown.md b/docs/pt/guide/markdown.md index 2d46ce29..29058709 100644 --- a/docs/pt/guide/markdown.md +++ b/docs/pt/guide/markdown.md @@ -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. +outline: deep --- # Extensões Markdown {#markdown-extensions} @@ -112,7 +113,7 @@ Para mais detalhes, veja [Frontmatter](../reference/frontmatter-config). :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) @@ -231,7 +232,7 @@ export default defineConfig({ ### `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** @@ -887,14 +888,14 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$ ## _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 export default { markdown: { image: { // 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 import { defineConfig } from 'vitepress' -import markdownItAnchor from 'markdown-it-anchor' +import { headerLink } from '@mdit/plugin-anchor' import markdownItFoo from 'markdown-it-foo' export default defineConfig({ markdown: { - // opções para markdown-it-anchor - // https://github.com/valeriangalliat/markdown-it-anchor#usage + // opções para @mdit/plugin-anchor + // https://mdit-plugins.github.io/anchor.html anchor: { - permalink: markdownItAnchor.permalink.headerLink() + permalink: headerLink() }, // opções para @mdit-vue/plugin-toc diff --git a/docs/pt/guide/routing.md b/docs/pt/guide/routing.md index 996f75bb..eb97ff01 100644 --- a/docs/pt/guide/routing.md +++ b/docs/pt/guide/routing.md @@ -290,7 +290,7 @@ O módulo de carregamento de caminhos é executado no Node.js e apenas durante o Gerando caminhos a partir de arquivos locais: ```js -import fs from 'fs' +import fs from 'node:fs' export default { paths() { diff --git a/docs/pt/guide/ssr-compat.md b/docs/pt/guide/ssr-compat.md index fcd36f38..ecd95d81 100644 --- a/docs/pt/guide/ssr-compat.md +++ b/docs/pt/guide/ssr-compat.md @@ -39,7 +39,7 @@ onMounted(() => { ### 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 if (!import.meta.env.SSR) { diff --git a/docs/pt/guide/using-vue.md b/docs/pt/guide/using-vue.md index 5878bebc..d4fdd415 100644 --- a/docs/pt/guide/using-vue.md +++ b/docs/pt/guide/using-vue.md @@ -203,7 +203,7 @@ Observe que isso pode impedir que certos tokens sejam realçados corretamente. ## 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 diff --git a/docs/pt/guide/what-is-vitepress.md b/docs/pt/guide/what-is-vitepress.md index 45b83db9..3119c893 100644 --- a/docs/pt/guide/what-is-vitepress.md +++ b/docs/pt/guide/what-is-vitepress.md @@ -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. -
      - +::: tip {no-title} Quer apenas experimentar? Pule para o [Início Rápido](./getting-started). - -
      +::: ## Casos de Uso {#use-cases} - **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. @@ -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. -- **[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. diff --git a/docs/pt/index.md b/docs/pt/index.md index ca621454..6143921b 100644 --- a/docs/pt/index.md +++ b/docs/pt/index.md @@ -21,16 +21,16 @@ hero: alt: VitePress features: - - icon: 📝 + - icon: title: Foco no seu conteúdo details: Cria sites de documentação belos e sem esforço apenas com markdown. - - icon: + - icon: title: Aproveite a experiência Vite details: Início de servidor instantâneo, atualizações ultrarrápidas, e plugins do ecossistema Vite. - - icon: + - icon: title: Personalize com Vue details: Use sintaxe e componentes Vue diretamente em markdown, ou construa temas personalizados com Vue. - - icon: 🚀 + - icon: title: Entregue Sites Rápidos details: Carregamento inicial rápido com HTML estático, navegação rápida com roteamento no lado do cliente. --- diff --git a/docs/pt/reference/default-theme-carbon-ads.md b/docs/pt/reference/default-theme-carbon-ads.md index da7738ac..67c5f26e 100644 --- a/docs/pt/reference/default-theme-carbon-ads.md +++ b/docs/pt/reference/default-theme-carbon-ads.md @@ -11,7 +11,8 @@ export default { themeConfig: { carbonAds: { code: 'seu-código-carbon', - placement: 'sua-veiculação-carbon' + placement: 'sua-veiculação-carbon', + format: 'classic' } } } @@ -19,8 +20,10 @@ export default { Esses valores são usados para chamar o sript em CDN do carbon como mostrado abaixo. +A opção `format` aceita `classic`, `responsive` e `cover`. + ```js -`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}` +`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}&format=${format}` ``` Para aprender mais sobre a configuração Carbon Ads, por favor visite [Site Carbon Ads](https://www.carbonads.net/). diff --git a/docs/pt/reference/default-theme-config.md b/docs/pt/reference/default-theme-config.md index 516ab87a..b0c6b699 100644 --- a/docs/pt/reference/default-theme-config.md +++ b/docs/pt/reference/default-theme-config.md @@ -353,16 +353,20 @@ export default { themeConfig: { carbonAds: { code: 'seu-código-carbon', - placement: 'sua-veiculação-carbon' + placement: 'sua-veiculação-carbon', + format: 'classic' } } } ``` +A opção `format` aceita `classic`, `responsive` e `cover`. + ```ts export interface CarbonAdsOptions { code: string placement: string + format?: 'classic' | 'responsive' | 'cover' } ``` diff --git a/docs/pt/reference/site-config.md b/docs/pt/reference/site-config.md index 88b639f3..068fb43f 100644 --- a/docs/pt/reference/site-config.md +++ b/docs/pt/reference/site-config.md @@ -430,7 +430,7 @@ export default { - Tipo: `string` - 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 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). -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} @@ -525,7 +525,7 @@ Verifique a [declaração de tipo e jsdocs](https://github.com/vuejs/vitepress/b - 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 export default { diff --git a/docs/public/robots.txt b/docs/public/robots.txt new file mode 100644 index 00000000..375c46f4 --- /dev/null +++ b/docs/public/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Content-Signal: search=yes, ai-train=yes, ai-input=yes +Disallow: + +Sitemap: https://vitepress.dev/sitemap.xml diff --git a/docs/ru/config.ts b/docs/ru/config.ts index 515cffba..f880f54b 100644 --- a/docs/ru/config.ts +++ b/docs/ru/config.ts @@ -1,9 +1,30 @@ -import { createRequire } from 'module' -import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' +import { createRequire } from 'node:module' +import { + defineAdditionalConfig, + type DefaultTheme, + type MarkdownLocaleOptions +} from 'vitepress' const require = createRequire(import.meta.url) const pkg = require('vitepress/package.json') +export const markdown: MarkdownLocaleOptions = { + container: { + tipLabel: 'СОВЕТ', + infoLabel: 'ИНФОРМАЦИЯ', + warningLabel: 'ПРЕДУПРЕЖДЕНИЕ', + dangerLabel: 'ОПАСНОСТЬ', + detailsLabel: 'Подробная информация', + noteLabel: 'ЗАМЕТКА', + importantLabel: 'ВАЖНО', + cautionLabel: 'ВНИМАНИЕ' + }, + codeCopyButton: { + tooltipText: 'Скопировать код', + copiedText: 'Скопировано' + } +} + export default defineAdditionalConfig({ description: 'Генератор статических сайтов на основе Vite и Vue.', @@ -189,7 +210,7 @@ function searchOptions(): Partial { clearButtonAriaLabel: 'Очистить запрос', closeButtonText: 'Закрыть', closeButtonAriaLabel: 'Закрыть', - placeholderText: 'Поиск по документации или задайте вопрос Ask AI', + placeholderText: 'Искать в документации или задать вопрос Ask AI', placeholderTextAskAi: 'Задайте другой вопрос...', placeholderTextAskAiStreaming: 'Отвечаю...', searchInputLabel: 'Поиск', @@ -262,8 +283,6 @@ function searchOptions(): Partial { afterToolCallText: 'Искал', stoppedStreamingText: 'Вы остановили этот ответ', errorTitleText: 'Ошибка чата', - threadDepthExceededMessage: - 'Этот разговор закрыт, чтобы сохранить точность ответов.', startNewConversationButtonText: 'Начать новый разговор' } } diff --git a/docs/ru/guide/asset-handling.md b/docs/ru/guide/asset-handling.md index 692e2c97..b2f26719 100644 --- a/docs/ru/guide/asset-handling.md +++ b/docs/ru/guide/asset-handling.md @@ -6,7 +6,7 @@ description: Узнайте, как ссылаться на статически ## Ссылки на статические ресурсы {#referencing-static-assets} -Все файлы Markdown компилируются в компоненты Vue и обрабатываются [Vite](https://vitejs.dev/guide/assets.html). Вы можете, **и должны**, ссылаться на любые ресурсы, используя относительные URL: +Все файлы Markdown компилируются в компоненты Vue и обрабатываются [Vite](https://vite-docs.ru/guide/assets.html). Вы можете, **и должны**, ссылаться на любые ресурсы, используя относительные URL: ```md ![Изображение](./image.png) diff --git a/docs/ru/guide/cms.md b/docs/ru/guide/cms.md index c55040c0..b3d12dee 100644 --- a/docs/ru/guide/cms.md +++ b/docs/ru/guide/cms.md @@ -1,5 +1,5 @@ --- -description: Подключите VitePress к безголовой CMS с помощью динамических маршрутов и загрузчиков данных. +description: Подключите VitePress к CMS без встроенного интерфейса с помощью динамических маршрутов и загрузчиков данных. outline: deep --- diff --git a/docs/ru/guide/custom-theme.md b/docs/ru/guide/custom-theme.md index fdbd1caf..24d75bae 100644 --- a/docs/ru/guide/custom-theme.md +++ b/docs/ru/guide/custom-theme.md @@ -62,11 +62,32 @@ import Layout from './Layout.vue' export default { Layout, enhanceApp({ app, router, siteData }) { - // ... + // app.component(...) + // app.use(...) } } ``` +Хук `enhanceApp` предоставляет доступ к [экземпляру приложения Vue](https://ru.vuejs.org/api/application.html) и другим данным времени выполнения. Это позволяет, например, [регистрировать глобальные компоненты](./extending-default-theme.md#registering-global-components), интегрироваться с библиотеками Vue и выполнять другие подобные задачи. + +Значение `router` представляет собой тот же экземпляр маршрутизатора VitePress, который возвращает [`useRouter()`](../reference/runtime-api#userouter). Чтобы отслеживать изменения маршрутов, назначьте обработчики для маршрутизатора: + +```ts [.vitepress/theme/index.ts] +export default { + enhanceApp({ router }) { + router.onBeforeRouteChange = (to) => { + console.log('navigating to', to) + } + + router.onAfterRouteChange = (to) => { + console.log('navigated to', to) + } + } +} +``` + +Верните `false` из `onBeforeRouteChange` или `onBeforePageLoad`, чтобы отменить переход. + Экспорт по умолчанию является единственным контрактом для пользовательской темы, и только свойство `Layout` является обязательным. Таким образом, технически тема VitePress может быть простой, как один компонент Vue. Внутри компонент макета работает так же, как и обычное приложение Vite + Vue 3. Обратите внимание, что тема также должна быть [SSR-совместимой](./ssr-compat). @@ -156,7 +177,7 @@ const { page, frontmatter } = useData() ## Распространение пользовательской темы {#distributing-a-custom-theme} -Самый простой способ распространить пользовательскую тему — предоставить её в виде [репозитория шаблонов на GitHub](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-template-repository). +Самый простой способ распространить пользовательскую тему — предоставить её в виде [репозитория шаблонов на GitHub](https://docs.github.com/ru/repositories/creating-and-managing-repositories/creating-a-template-repository). Если вы хотите распространить тему в виде пакета npm, выполните следующие действия: @@ -208,10 +229,10 @@ export default { ```ts [.vitepress/config.ts] import baseConfig from 'awesome-vitepress-theme/config' -import { defineConfigWithTheme } from 'vitepress' +import { defineConfig } from 'vitepress' import type { ThemeConfig } from 'awesome-vitepress-theme' -export default defineConfigWithTheme({ +export default defineConfig({ extends: baseConfig, themeConfig: { // Тип `ThemeConfig` diff --git a/docs/ru/guide/deploy.md b/docs/ru/guide/deploy.md index f92761e5..56cc8a0a 100644 --- a/docs/ru/guide/deploy.md +++ b/docs/ru/guide/deploy.md @@ -166,6 +166,13 @@ Cache-Control: max-age=31536000,immutable with: node-version: 24 cache: npm # или pnpm / yarn + - name: Cache VitePress + uses: actions/cache@v4 + with: + path: docs/.vitepress/cache + key: ${{ runner.os }}-vitepress-${{ hashFiles('docs/**', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb') }} + restore-keys: | + ${{ runner.os }}-vitepress- - name: Setup Pages uses: actions/configure-pages@v4 - name: Install dependencies diff --git a/docs/ru/guide/extending-default-theme.md b/docs/ru/guide/extending-default-theme.md index c3883625..0d1320a6 100644 --- a/docs/ru/guide/extending-default-theme.md +++ b/docs/ru/guide/extending-default-theme.md @@ -120,7 +120,7 @@ export default { } satisfies Theme ``` -Поскольку мы используем Vite, можно применять [глобальную функцию импорта](https://vitejs.dev/guide/features.html#glob-import) Vite для автоматической регистрации каталога компонентов. +Поскольку мы используем Vite, можно применять [глобальную функцию импорта](https://vite-docs.ru/guide/features.html#glob-import) Vite для автоматической регистрации каталога компонентов. ## Слоты макета {#layout-slots} @@ -310,7 +310,7 @@ provide('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => { ## Переопределение внутренних компонентов {#overriding-internal-components} -Вы можете использовать [псевдонимы](https://vitejs.dev/config/shared-options.html#resolve-alias) Vite, чтобы заменить стандартные компоненты темы на свои собственные: +Вы можете использовать [псевдонимы](https://vite-docs.ru/config/shared-options.html#resolve-alias) Vite, чтобы заменить стандартные компоненты темы на свои собственные: ```ts import { fileURLToPath, URL } from 'node:url' diff --git a/docs/ru/guide/getting-started.md b/docs/ru/guide/getting-started.md index b2b81aac..e589707d 100644 --- a/docs/ru/guide/getting-started.md +++ b/docs/ru/guide/getting-started.md @@ -12,7 +12,7 @@ description: Начните работу с VitePress. Узнайте, как у ### Требования {#prerequisites} -- [Node.js](https://nodejs.org/) версии 20 или выше. +- [Node.js](https://nodejs.org/) версии 22 или выше. - Терминал для доступа к VitePress через интерфейс командной строки (CLI). - Текстовый редактор с поддержкой синтаксиса [Markdown](https://ru.wikipedia.org/wiki/Markdown). - Рекомендуется использовать [VSCode](https://code.visualstudio.com/), а также [официальное расширение Vue](https://marketplace.visualstudio.com/items?itemName=Vue.volar). @@ -37,11 +37,15 @@ $ yarn add -D vitepress@next vue $ bun add -D vitepress@next ``` +```sh [deno] +$ deno add -D vitepress@next +``` + ::: ::: tip ПРИМЕЧАНИЕ -VitePress — это пакет, предназначенный только для ESM. Не используйте `require()` для импорта, и убедитесь, что ближайший `package.json` содержит `"type": "module"`, или измените расширение соответствующих файлов, например, `.vitepress/config.js` на `.mjs`/`.mts`. Более подробную информацию см. в [Руководстве по устранению неполадок Vite](https://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](https://vite-docs.ru/guide/troubleshooting.html#this-package-is-esm-only). Кроме того, внутри асинхронных контекстов CJS можно использовать `await import('vitepress')` вместо этого. ::: diff --git a/docs/ru/guide/i18n.md b/docs/ru/guide/i18n.md index 87858c68..6ef839fe 100644 --- a/docs/ru/guide/i18n.md +++ b/docs/ru/guide/i18n.md @@ -57,6 +57,37 @@ interface LocaleSpecificConfig { **Совет:** Конфигурационный файл можно хранить и в `docs/.vitepress/config/index.ts`. Это может помочь вам организовать работу, создав конфигурационный файл для каждой локали, а затем объединить и экспортировать их из `index.ts`. +## Локализованные Markdown-строки {#per-locale-markdown-strings} + +Строки, встраиваемые в страницы Markdown-рендерером, — стандартные заголовки [пользовательских контейнеров](./markdown#custom-containers), [оповещений в стиле GitHub](./markdown#github-flavored-alerts), а также строки кнопки копирования кода — можно переопределить для каждой локали с помощью ключа `markdown` в записи соответствующей локали: + +```ts [docs/.vitepress/config.ts] +import { defineConfig } from 'vitepress' + +export default defineConfig({ + locales: { + root: { label: 'English', lang: 'en' }, + zh: { + label: 'Русский', + lang: 'ru', + markdown: { + container: { + tipLabel: 'Подсказка', + warningLabel: 'Предупреждение' + // ...остальные метки, а также заголовки `customContainers` + }, + codeCopyButton: { + tooltipText: 'Копировать код', + copiedText: 'Скопировано' + } + } + } + } +}) +``` + +Если значение не задано для конкретной локали, используется соответствующее значение из параметров `markdown` корневого уровня. Записи локалей могут переопределять только заголовки контейнеров, зарегистрированных на корневом уровне — регистрация новых контейнеров отдельно для каждой локали не поддерживается. Также обратите внимание, что, поскольку Markdown-рендерер создаётся один раз для всего сайта, эти параметры можно объявлять только в основном файле конфигурации, а не в дополнительных конфигурационных файлах. + ## Отдельный каталог для каждой локали {#separate-directory-for-each-locale} Пример многоязычной структуры: diff --git a/docs/ru/guide/markdown.md b/docs/ru/guide/markdown.md index 836f2e11..ae58bfb8 100644 --- a/docs/ru/guide/markdown.md +++ b/docs/ru/guide/markdown.md @@ -1,5 +1,6 @@ --- description: Встроенные расширения Markdown в VitePress, включая пользовательские контейнеры, блоки кода с подсветкой синтаксиса, выделение строк, группы кода и многое другое. +outline: deep --- # Расширения Markdown {#markdown-extensions} @@ -62,7 +63,7 @@ VitePress поставляется со встроенными расширен Исходящие ссылки автоматически получают значение `target="_blank" rel="noreferrer"`: -- [vuejs.org](https://vuejs.org) +- [vuejs.org](https://ru.vuejs.org) - [VitePress on GitHub](https://github.com/vuejs/vitepress) ## Метаданные {#frontmatter} @@ -100,6 +101,36 @@ lang: ru-RU | столбец 2 | отцентрован | \$12 | | полосатые строки | как полоски у зебры | \$1 | +## Списки задач {#task-lists} + +**Разметка** + +```md +- [ ] Написать пресс-релиз +- [x] Обновить сайт +``` + +**Результат** + +- [ ] Написать пресс-релиз +- [x] Обновить сайт + +## Сноски {#footnotes} + +**Разметка** + +```md +Сноски поддерживаются[^1], включая встроенные^[Это встроенная сноска.]. + +[^1]: Определения могут содержать **Markdown** и отображаются в конце страницы. +``` + +**Результат** + +Сноски поддерживаются[^1], включая встроенные^[Это встроенная сноска.]. + +[^1]: Определения могут содержать **Markdown** и отображаются в конце страницы. + ## Эмодзи :tada: {#emoji} **Разметка** @@ -112,7 +143,7 @@ lang: ru-RU :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} @@ -233,9 +264,82 @@ export default defineConfig({ }) ``` +На многоязычных сайтах эти метки также можно переопределить для каждой локали отдельно — см. раздел [Локализованные Markdown-строки](./i18n#per-locale-markdown-strings). + +### Регистрация новых контейнеров {#registering-new-containers} + +Помимо встроенных типов, вы можете зарегистрировать дополнительные контейнеры, сопоставив их имена с их заголовками по умолчанию: + +```ts +// config.ts +export default defineConfig({ + // ... + markdown: { + container: { + customContainers: { + success: 'УСПЕШНО' + } + } + } + // ... +}) +``` + +Зарегистрированные имена работают так же, как и встроенные — включая пользовательские заголовки, атрибуты и [синтаксис оповещений в стиле GitHub](#github-flavored-alerts): + +```md +::: +Вы успешно завершили руководство! +::: + +> [!SUCCESS] Пользовательский заголовок +> Этот вариант отображается точно так же. +``` + +Для новых контейнеров стили по умолчанию отсутствуют, поэтому добавьте их в своей теме, используя имя контейнера в качестве класса. В этом примере палитра темы по умолчанию уже содержит подходящие цвета: + +```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} + +Маркеры `:::` подчиняются тем же правилам, что и ограждения блоков кода (` ``` `): ограждение закрывается только соответствующим маркером, который имеет **не меньшую длину**, чем открывающий. Чтобы вкладывать контейнеры друг в друга (или сочетать их с [группами кодов](#code-groups)), сделайте внешнее ограждение длиннее внутренних. + +**Разметка** + +`````md +:::: info Внешний контейнер +Этот блок содержит ещё один контейнер. + +::: details Внутренний контейнер +```js +console.log('Привет, VitePress!') +``` +::: +:::: +````` + +**Результат** + +:::: info Внешний контейнер +Этот блок содержит ещё один контейнер. + +::: details Внутренний контейнер +```js +console.log('Привет, VitePress!') +``` +::: +:::: + ### Дополнительные атрибуты {#additional-attributes} -Вы можете добавить дополнительные атрибуты к пользовательским контейнерам. Мы используем [markdown-it-attrs](https://github.com/arve0/markdown-it-attrs) для этой функции, и она поддерживается почти для всех элементов Markdown. Например, можно установить атрибут `open`, чтобы сделать блок подробностей открытым по умолчанию: +Вы можете добавить дополнительные атрибуты к пользовательским контейнерам. Мы используем [@mdit/plugin-attrs](https://mdit-plugins.github.io/attrs.html) для этой функции, и она поддерживается почти для всех элементов Markdown. Например, можно установить атрибут `open`, чтобы сделать блок подробностей открытым по умолчанию: **Разметка** @@ -255,9 +359,25 @@ console.log('Привет, VitePress!') ``` ::: +Специальный атрибут `no-title` отображает контейнер без элемента заголовка (он не влияет на `details`, поскольку этому контейнеру всегда требуется сводка): + +**Разметка** + +```md +::: tip {no-title} +Хотите просто попробовать? Перейдите сразу к разделу [Первые шаги](./getting-started). +::: +``` + +**Результат** + +::: tip {no-title} +Хотите просто попробовать? Перейдите сразу к разделу [Первые шаги](./getting-started). +::: + ### `raw` {#raw} -Это специальный контейнер, который можно использовать для предотвращения конфликтов стилей и маршрутизаторов с VitePress. Это особенно полезно при документировании библиотек компонентов. Вы также можете посмотреть [whyframe](https://whyframe.dev/docs/integrations/vitepress) для лучшей изоляции. +Это специальный контейнер, который можно использовать для предотвращения конфликтов стилей и маршрутизаторов с VitePress. Это особенно полезно при документировании библиотек компонентов. **Синтаксис** @@ -295,7 +415,7 @@ console.log('Привет, VitePress!') ## Оповещения в стиле GitHub {#github-flavored-alerts} -VitePress также поддерживает [Оповещения в стиле GitHub](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) для отображения в виде вставок. Они будут отображаться так же, как и [пользовательские контейнеры](#custom-containers). +VitePress также поддерживает [Оповещения в стиле GitHub](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) для отображения в виде вставок. Они будут отображаться так же, как и [пользовательские контейнеры](#custom-containers). В отличие от GitHub, текст, размещённый сразу после маркера, становится заголовком оповещения (`> [!NOTE] Пользовательский заголовок`), и здесь также работают [контейнеры, зарегистрированные вами самостоятельно](#registering-new-containers). ```md > [!NOTE] @@ -678,6 +798,16 @@ const line4 = 'Строка 4' <<< @/snippets/snippet-with-region.js#snippet{1} +Если файл содержит несколько регионов с одинаковым именем, все они импортируются и объединяются — включая регионы, записанные с использованием разных стилей комментариев, например `` в шаблоне и `// #region` в скрипте одного и того же однофайлового компонента Vue. Комментарии-маркеры, ограничивающие регионы, удаляются из результата. Установите `markdown.snippet.stripRegionMarkers` в `'all'`, чтобы также удалить маркеры других стилей комментариев, вложенные в регион, или в `false`, чтобы сохранить все маркеры. + +:::tip +Имена регионов могут содержать буквы, цифры, символы `_`, `-` и `.`. Поскольку имя региона берётся из конца пути, для файла, имя которого само содержит символ `#`, необходимо явно указать регион — используйте `<<< ./my#file.js#region` вместо `<<< ./my#file.js`. +::: + +:::warning +Импорт файла или региона, который не существует, вызывает ошибку сборки. Установите `markdown.snippet.silent: true`, чтобы вместо этого записывать предупреждение в журнал и ничего не выводить. +::: + Кроме того, можно указать язык внутри фигурных скобок (`{}`) следующим образом: ```md @@ -692,7 +822,9 @@ const line4 = 'Строка 4' <<< @/snippets/snippet.cs{1,2,4-6 c#:line-numbers} ``` -Это полезно, если исходный язык нельзя определить по расширению вашего файла. +Это полезно, если исходный язык нельзя определить по расширению вашего файла. Автоматически определяются только буквенно-цифровые расширения, поэтому для таких файлов, как `main.c++` или `scss.code-snippets`, язык необходимо указывать явно. + +Всё, что следует после языка внутри фигурных скобок, передаётся в блок кода как дополнительные атрибуты. Например, `<<< @/snippets/snippet.ts{ts twoslash}` включает обработку twoslash, если настроен пакет [`@shikijs/vitepress-twoslash`](https://shiki.style/packages/vitepress#twoslash). Обратите внимание, что атрибуты не могут содержать квадратные скобки. ## Группы кодов {#code-groups} @@ -900,7 +1032,7 @@ export default config ``` ::: warning ПРЕДУПРЕЖДЕНИЕ -Обратите внимание, что это не приводит к ошибкам, если ваш файл отсутствует. Поэтому при использовании этой функции убедитесь, что содержимое отображается так, как ожидается. +Включение файла, региона, якоря заголовка или диапазона строк, который не существует, приводит к ошибке сборки. Установите `markdown.include.silent: true`, чтобы вместо этого выводить предупреждение в журнал и пропускать включение. ::: Вместо регионов VS Code вы также можете использовать якоря заголовков, чтобы включить определённый раздел файла. Например, если у вас есть заголовок в вашем markdown-файле, например: @@ -950,6 +1082,28 @@ export default config ``` +Относительные ссылки и изображения внутри включаемых файлов разрешаются относительно расположения **включаемого** файла, поэтому частичный файл может ссылаться на соседние файлы независимо от того, с какой страницы он подключён. Установите `markdown.include.rebaseRelativeUrls: false`, чтобы они вместо этого разрешались относительно страницы, которая выполняет включение. + +### Включение файлов с кодом {#including-code-files} + +Поскольку включение выполняется до разбора блоков кода, эта директива также работает внутри кодовых ограждений. В сочетании с диапазоном строк это позволяет показывать только часть файла с кодом — это альтернатива [импорту сниппетов](#import-code-snippets), когда использование регионов невозможно: + +**Разметка** + +````md +```js + +``` +```` + +**Результат** + +```js + +``` + +Обратите внимание, что включаемые строки вставляются дословно (отступы сохраняются), а если содержимое включает символы обратных кавычек (`` ` ``), необходимо использовать более длинное внешнее ограждение блока кода. + ## Математические уравнения {#math-equations} В настоящее время эта фича предоставляется по желанию. Чтобы включить её, вам нужно установить `markdown-it-mathjax3` и установить значение `true` для опции `markdown.math` в вашем файле конфигурации: @@ -996,14 +1150,14 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$ ## Ленивая загрузка изображений {#image-lazy-loading} -Вы можете включить ленивую загрузку для каждого изображения, добавленного через markdown, установив значение `true` для опции `lazyLoading` в вашем файле конфигурации: +Вы можете включить ленивую загрузку для каждого изображения, добавленного через markdown, установив значение `true` для опции `lazyLoad` в вашем файле конфигурации: ```js export default { markdown: { image: { // ленивая загрузка изображений отключена по умолчанию - lazyLoading: true + lazyLoad: true } } } @@ -1015,15 +1169,15 @@ VitePress использует [markdown-it](https://github.com/markdown-it/mark ```js import { defineConfig } from 'vitepress' -import markdownItAnchor from 'markdown-it-anchor' +import { headerLink } from '@mdit/plugin-anchor' import markdownItFoo from 'markdown-it-foo' export default defineConfig({ markdown: { - // опции для markdown-it-anchor - // https://github.com/valeriangalliat/markdown-it-anchor#usage + // опции для @mdit/plugin-anchor + // https://mdit-plugins.github.io/anchor.html anchor: { - permalink: markdownItAnchor.permalink.headerLink() + permalink: headerLink() }, // опции для @mdit-vue/plugin-toc diff --git a/docs/ru/guide/migration-from-vitepress-0.md b/docs/ru/guide/migration-from-vitepress-0.md index 4d5c7426..5c339a57 100644 --- a/docs/ru/guide/migration-from-vitepress-0.md +++ b/docs/ru/guide/migration-from-vitepress-0.md @@ -14,7 +14,7 @@ - `repo`, `repoLabel`, `docsDir`, `docsBranch`, `editLinks`, `editLinkText` удалены в пользу более гибкого api. - Для добавления ссылки GitHub с иконкой в навигацию используйте функцию [Социальные ссылки](../reference/default-theme-nav#navigation-links). - Для добавления ссылки «Редактировать эту страницу» используйте функцию [Ссылка для редактирования](../reference/default-theme-edit-link). -- Опция `lastUpdated` теперь разделена на `config.lastUpdated` и `themeConfig.lastUpdatedText`. +- Опция `lastUpdated` теперь разделена на `config.lastUpdated` и `themeConfig.lastUpdated.text`. - Опция `carbonAds.carbon` заменена на `carbonAds.code`. ## Конфигурация метаданных diff --git a/docs/ru/guide/routing.md b/docs/ru/guide/routing.md index d7971f77..37e48109 100644 --- a/docs/ru/guide/routing.md +++ b/docs/ru/guide/routing.md @@ -329,7 +329,7 @@ export default { Генерация путей из локальных файлов: ```js -import fs from 'fs' +import fs from 'node:fs' export default { paths() { diff --git a/docs/ru/guide/ssr-compat.md b/docs/ru/guide/ssr-compat.md index 5473d368..b9fa9aa7 100644 --- a/docs/ru/guide/ssr-compat.md +++ b/docs/ru/guide/ssr-compat.md @@ -37,9 +37,9 @@ 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` (часть [переменных окружения Vite](https://vite-docs.ru/guide/env-and-mode.html#env-variables)): ```js if (!import.meta.env.SSR) { diff --git a/docs/ru/guide/using-vue.md b/docs/ru/guide/using-vue.md index f0e933b1..cc4022f2 100644 --- a/docs/ru/guide/using-vue.md +++ b/docs/ru/guide/using-vue.md @@ -201,7 +201,7 @@ HTML, обёрнутый ``, будет отображаться как е ## Использование препроцессоров CSS {#using-css-pre-processors} -VitePress имеет [встроенную поддержку](https://vitejs.dev/guide/features.html#css-pre-processors) для препроцессоров CSS: файлы `.scss`, `.sass`, `.less`, `.styl` и `.stylus`. Для них не нужно устанавливать специфические для Vite плагины, но сам соответствующий препроцессор должен быть установлен: +VitePress имеет [встроенную поддержку](https://vite-docs.ru/guide/features.html#css-pre-processors) для препроцессоров CSS: файлы `.scss`, `.sass`, `.less`, `.styl` и `.stylus`. Для них не нужно устанавливать специфические для Vite плагины, но сам соответствующий препроцессор должен быть установлен: ::: code-group diff --git a/docs/ru/guide/what-is-vitepress.md b/docs/ru/guide/what-is-vitepress.md index ea21140c..e0d33a64 100644 --- a/docs/ru/guide/what-is-vitepress.md +++ b/docs/ru/guide/what-is-vitepress.md @@ -6,11 +6,9 @@ description: VitePress — это генератор статических са VitePress — это [Генератор статических сайтов](https://ru.wikipedia.org/wiki/%D0%93%D0%B5%D0%BD%D0%B5%D1%80%D0%B0%D1%82%D0%BE%D1%80%D1%8B_%D1%81%D1%82%D0%B0%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D1%85_%D1%81%D0%B0%D0%B9%D1%82%D0%BE%D0%B2) (ГСС), предназначенный для быстрого создания сайтов, ориентированных на контент. В двух словах, VitePress берёт ваш исходный контент, написанный на [Markdown](https://ru.wikipedia.org/wiki/Markdown), применяет к нему тему и генерирует статические HTML-страницы, которые можно легко развернуть в любом месте. -
      - +::: tip {no-title} Хотите попробовать прямо сейчас? Перейдите к главе [Первые шаги](./getting-started). - -
      +::: ## Примеры использования {#use-cases} @@ -18,7 +16,7 @@ VitePress — это [Генератор статических сайтов](ht VitePress поставляется с темой по умолчанию, предназначенной для технической документации. Именно она обеспечивает работу этой страницы, которую вы сейчас читаете, а также документации для [Vite](https://vite-docs.ru/), [Rollup](https://rollupjs.org/), [Pinia](https://pinia-ru.netlify.app), [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://ru.vuejs.org/) также основана на VitePress, но использует кастомную тему, общую для нескольких переводов. - **Блоги, портфолио и маркетинговые сайты** @@ -30,7 +28,7 @@ VitePress — это [Генератор статических сайтов](ht VitePress стремится обеспечить отличные возможности для разработчиков при работе с содержимым в формате Markdown. -- **[На базе Vite:](https://vitejs.dev/)** мгновенный запуск сервера, правки всегда отражаются мгновенно (<100 мс) без перезагрузки страницы. +- **[На базе Vite:](https://vite-docs.ru/)** мгновенный запуск сервера, правки всегда отражаются мгновенно (<100 мс) без перезагрузки страницы. - **[Встроенные расширения Markdown:](./markdown)** Frontmatter, таблицы, подсветка синтаксиса... называйте как хотите. В частности, VitePress предоставляет множество расширенных возможностей для работы с блоками кода, что делает его идеальным для создания технической документации. diff --git a/docs/ru/index.md b/docs/ru/index.md index 2fb82def..5d151a85 100644 --- a/docs/ru/index.md +++ b/docs/ru/index.md @@ -21,16 +21,16 @@ hero: alt: VitePress features: - - icon: 📝 + - icon: title: Сосредоточьтесь на своем контенте details: Легко создавайте красивые сайты с документацией, используя только Markdown. - - icon: + - icon: title: Наслаждайтесь опытом разработчиков Vite details: Мгновенный запуск сервера, молниеносные горячие обновления и использование плагинов экосистемы Vite. - - icon: + - icon: title: Настройка с помощью Vue details: Используйте синтаксис Vue и компоненты прямо в Markdown или создавайте собственные темы с помощью Vue. - - icon: 🚀 + - icon: title: Быстрый запуск веб-сайтов details: Быстрая начальная загрузка с помощью статического HTML, быстрая навигация после загрузки с помощью маршрутизации на стороне клиента. --- diff --git a/docs/ru/reference/default-theme-carbon-ads.md b/docs/ru/reference/default-theme-carbon-ads.md index bf923bd3..f8720a64 100644 --- a/docs/ru/reference/default-theme-carbon-ads.md +++ b/docs/ru/reference/default-theme-carbon-ads.md @@ -11,7 +11,8 @@ export default { themeConfig: { carbonAds: { code: 'код-рекламы', - placement: 'место-размещения-рекламы' + placement: 'место-размещения-рекламы', + format: 'classic' } } } @@ -19,8 +20,10 @@ export default { Эти значения используются для вызова сценария Carbon CDN, как показано ниже: +Параметр `format` поддерживает значения `classic`, `responsive` и `cover`. + ```js -;`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}` +`//cdn.carbonads.com/carbon.js?serve=${code}&placement=${placement}&format=${format}` ``` Чтобы узнать больше о настройке Carbon Ads, посетите [веб-сайт Carbon Ads](https://www.carbonads.net/). diff --git a/docs/ru/reference/default-theme-config.md b/docs/ru/reference/default-theme-config.md index 827b77e9..afd17954 100644 --- a/docs/ru/reference/default-theme-config.md +++ b/docs/ru/reference/default-theme-config.md @@ -25,10 +25,28 @@ export default { ## i18nRouting -- Тип: `boolean` +- Тип: `boolean | ((data: VitePressData, route: Route, targetLocale: string) => string)` При смене локали на `ru` URL изменится с `/foo` (или `/en/foo/`) на `/ru/foo`. Вы можете отключить это поведение, установив для параметра `themeConfig.i18nRouting` значение `false`. +Установите для `themeConfig.i18nRouting` функцию, чтобы настроить ссылку локали. Эта функция получает текущие данные VitePress, текущий маршрут и ключ целевой локали, а затем возвращает целевую ссылку. + +```ts +import { defineConfig } from 'vitepress' + +export default defineConfig({ + themeConfig: { + i18nRouting(data, route, targetLocale) { + const target = data.site.value.locales[targetLocale] + const targetLink = + target.link || (targetLocale === 'root' ? '/' : `/${targetLocale}/`) + + return `${targetLink}${route.data.relativePath.replace(/\.md$/, '')}${route.hash}` + } + } +}) +``` + ## logo - Тип: `ThemeableImage` @@ -235,6 +253,7 @@ export default { // Можно добавить любую иконку из simple-icons (https://simpleicons.org/): { icon: 'github', link: 'https://github.com/vuejs/vitepress' }, { icon: 'twitter', link: '...' }, + { icon: 'discord', link: '/community', target: '_self' }, // Можно добавить пользовательские иконки, передав SVG в виде строки: { icon: { @@ -254,6 +273,7 @@ interface SocialLink { icon: string | { svg: string } link: string ariaLabel?: string + target?: string } ``` @@ -367,16 +387,20 @@ export default { themeConfig: { carbonAds: { code: 'код-рекламы', - placement: 'место-размещения-рекламы' + placement: 'место-размещения-рекламы', + format: 'classic' } } } ``` +Параметр `format` поддерживает значения `classic`, `responsive` и `cover`. + ```ts export interface CarbonAdsOptions { code: string placement: string + format?: 'classic' | 'responsive' | 'cover' } ``` diff --git a/docs/ru/reference/default-theme-search.md b/docs/ru/reference/default-theme-search.md index 838e7cea..efead057 100644 --- a/docs/ru/reference/default-theme-search.md +++ b/docs/ru/reference/default-theme-search.md @@ -276,7 +276,6 @@ export default defineConfig({ askAi: { assistantId: 'XXXYYY', sidePanel: { - // Отражает API @docsearch/sidepanel-js SidepanelProps panel: { variant: 'floating', // или 'inline' side: 'right', @@ -292,6 +291,8 @@ export default defineConfig({ }) ``` +Используйте `askAi.sidePanel.panel.suggestedQuestions` для настройки рекомендуемых вопросов в боковой панели. В примерах автономного Ask AI от Algolia также упоминается `askAi.suggestedQuestions`, однако одного этого параметра верхнего уровня недостаточно для режима боковой панели VitePress, и он не позволяет встроенному модальному окну поиска по ключевым словам отображать рекомендуемые вопросы при первом открытии. + Если вам нужно отключить сочетание клавиш, используйте опцию `keyboardShortcuts` боковой панели: ```ts diff --git a/docs/ru/reference/default-theme-sidebar.md b/docs/ru/reference/default-theme-sidebar.md index f1e0e5f9..b77ed922 100644 --- a/docs/ru/reference/default-theme-sidebar.md +++ b/docs/ru/reference/default-theme-sidebar.md @@ -182,3 +182,63 @@ export default { } } ``` + +## Префикс пути {#path-prefix} + +Если структура вашей документации содержит глубоко вложенные каталоги или группы, расположенные в одном подкаталоге, вы можете использовать параметр `base`, чтобы автоматически добавлять префикс пути ко всем вложенным элементам `items` внутри этой группы. Это избавляет от необходимости повторять один и тот же префикс пути для каждого `link`. + +Параметр `base` поддерживается как в конфигурациях с несколькими боковыми панелями, так и во вложенных группах боковой панели. + +### В нескольких боковых панелях {#in-multiple-sidebars} + +Вы можете определить `base` в корне конфигурации раздела боковой панели: + +```js {5} +export default { + themeConfig: { + sidebar: { + '/guide/': { + base: '/guide/', + items: [ + // Эта ссылка будет разрешена в `/guide/introduction` + { text: 'Введение', link: 'introduction' }, + // Эта ссылка будет разрешена в `/guide/getting-started` + { text: 'Первые шаги', link: 'getting-started' } + ] + } + } + } +} +``` + +### Во вложенных группах {#in-nested-groups} + +Параметр `base` также можно использовать во вложенных группах боковой панели. Он применяется к непосредственным дочерним элементам этой группы: + +```js{6,13} +export default { + themeConfig: { + sidebar: [ + { + text: 'Справочник', + base: '/reference/', + items: [ + // Эта ссылка будет разрешена в `/reference/site-config` + { text: 'Конфигурация сайта', link: 'site-config' }, + { + text: 'Тема по умолчанию', + // Вложенный `base` переопределяет префикс пути родительской группы + base: '/reference/default-theme-', + items: [ + // Эта ссылка будет разрешена в `/reference/default-theme-nav` + { text: 'Навигация', link: 'nav' }, + // Эта ссылка будет разрешена в `/reference/default-theme-sidebar` + { text: 'Сайдбар', link: 'sidebar' } + ] + } + ] + } + ] + } +} +``` diff --git a/docs/ru/reference/runtime-api.md b/docs/ru/reference/runtime-api.md index efbaae88..5157bd67 100644 --- a/docs/ru/reference/runtime-api.md +++ b/docs/ru/reference/runtime-api.md @@ -62,6 +62,8 @@ interface PageData { } ``` +`page.headers` заполняется только в том случае, если включён параметр [markdown.headers](./site-config#markdown). Без него это свойство остаётся пустым массивом. Оглавление в теме по умолчанию получает заголовки из уже отрендеренного содержимого страницы, поэтому оно может отображаться, даже если `page.headers` пуст. + **Пример:** ```vue @@ -122,6 +124,18 @@ interface Router { } ``` +Назначьте обработчики изменения маршрутов для экземпляра маршрутизатора: + +```ts +const router = useRouter() + +router.onBeforeRouteChange = (to) => { + console.log('переход к', to) +} +``` + +В пользовательских темах этот же экземпляр маршрутизатора доступен через [`enhanceApp`](../guide/custom-theme#theme-interface). + ## `withBase` {#withbase} - **Тип**: `(path: string) => string` diff --git a/docs/ru/reference/site-config.md b/docs/ru/reference/site-config.md index d8c0d255..6c593eda 100644 --- a/docs/ru/reference/site-config.md +++ b/docs/ru/reference/site-config.md @@ -134,13 +134,43 @@ export default defineConfigWithTheme({ Вы можете настроить базовый экземпляр [Markdown-It](https://github.com/markdown-it/markdown-it) с помощью опции [markdown](#markdown) в конфигурации VitePress. +### Переопределение на уровне страницы {#page-level-overrides} + +Некоторые настройки можно переопределить для отдельных страниц с помощью метаданных. + +Подробности см. в разделе [Конфигурация метаданных](./frontmatter-config). + +### Переопределение на уровне директории {#directory-level-overrides} + +Некоторые параметры конфигурации можно переопределить на уровне директории, что позволяет всем страницам в этой директории использовать общие настройки без необходимости повторять их в блоке метаданных каждой страницы. + +Для этого добавьте файл с именем `config.ts` (или `.js`, `.mjs` или `.mts`) в соответствующую директорию. Этот файл должен экспортировать объект конфигурации с помощью `export default`, аналогично основному файлу конфигурации. + +Вложенные директории наследуют настройки от родительской директории, при этом переопределения конфигурации объединяются соответствующим образом. + +Вспомогательную функцию `defineAdditionalConfig` можно использовать для получения подсказок TypeScript по доступным параметрам, однако, как и в случае с `defineConfig`, её использование необязательно. + +Например, для сайта с несколькими языками может потребоваться разное значение `description` для каждого языка. Мы можем добавить файл `es/config.ts` со следующим содержимым: + +```ts +import { defineAdditionalConfig } from 'vitepress' + +export default defineAdditionalConfig({ + description: 'Generador de Sitios Estáticos desarrollado con Vite y Vue.' +}) +``` + +Этот `description` затем будет использоваться для всех страниц в директории `es`. + +В качестве альтернативы, при использовании встроенных возможностей i18n настройки для директории локали можно переопределить через параметр `locales` в основном файле конфигурации. Подробности см. в разделе [Интернационализация](../guide/i18n). + ## Метаданные сайта {#site-metadata} ### title {#title} - Тип: `string` - По умолчанию: `VitePress` -- Можно переопределить для каждой страницы с помощью [метаданных](./frontmatter-config#title) +- Можно переопределить для каждой страницы с помощью [метаданных](./frontmatter-config#title) или на [уровне директории](#directory-level-overrides) Название для сайта. При использовании темы по умолчанию оно будет отображаться в панели навигации. @@ -161,7 +191,7 @@ export default { ### titleTemplate {##titletemplate} - Тип: `string | boolean` -- Можно переопределить для каждой страницы с помощью [метаданных](./frontmatter-config#titletemplate) +- Можно переопределить для каждой страницы с помощью [метаданных](./frontmatter-config#titletemplate) или на [уровне директории](#directory-level-overrides) Позволяет настраивать суффикс заголовка каждой страницы или весь заголовок. Например: @@ -194,7 +224,7 @@ export default { - Тип: `string` - По умолчанию: `A VitePress site` -- Можно переопределить для каждой страницы с помощью [метаданных](./frontmatter-config#description) +- Можно переопределить для каждой страницы с помощью [метаданных](./frontmatter-config#description) или на [уровне директории](#directory-level-overrides) Описание для сайта. Это будет отображаться как тег `` в HTML-странице. @@ -208,7 +238,7 @@ export default { - Тип: `HeadConfig[]` - По умолчанию: `[]` -- Можно добавлять на страницу через [метаданные](./frontmatter-config#head) +- Можно добавлять на страницу через [метаданные](./frontmatter-config#head) или на [уровне директории](#directory-level-overrides) Дополнительные элементы для отображения в теге `` в HTML-странице. Добавленные пользователем теги выводятся перед закрывающим тегом `head`, после тегов VitePress. @@ -320,6 +350,7 @@ export default { - Тип: `string` - По умолчанию: `en-US` +- Может быть переопределено [на уровне директории](#directory-level-overrides) Атрибут lang для сайта. Будет выглядеть как тег `` в HTML-странице. @@ -430,7 +461,7 @@ export default { - Тип: `string` - По умолчанию: `./.vitepress/cache` -Каталог для файлов кэша, относительно [корня проекта](../guide/routing#root-and-source-directory). См. также: [cacheDir](https://vitejs.dev/config/shared-options.html#cachedir). +Каталог для файлов кэша, относительно [корня проекта](../guide/routing#root-and-source-directory). См. также: [cacheDir](https://vite-docs.ru/config/shared-options.html#cachedir). ```ts export default { @@ -472,13 +503,6 @@ export default { } ``` -### metaChunk {#metachunk} - -- Тип: `boolean` -- По умолчанию: `false` - -Если установлено значение `true`, метаданные страницы извлекаются в отдельный фрагмент JavaScript, а не вставляются в исходный HTML. Это уменьшает полезную нагрузку HTML каждой страницы и делает метаданные страниц кэшируемыми, что позволяет снизить пропускную способность сервера при наличии большого количества страниц на сайте. - ### mpa {#mpa} - Тип: `boolean` @@ -512,7 +536,7 @@ export default { Получать ли временную метку последнего обновления для каждой страницы с помощью Git. Временная метка будет включена в данные каждой страницы, доступные через [`useData`](./runtime-api#usedata). -При использовании темы по умолчанию включение этой опции приведёт к отображению времени последнего обновления каждой страницы. Вы можете настроить текст с помощью опции [`themeConfig.lastUpdatedText`](./default-theme-config#lastupdatedtext). +При использовании темы по умолчанию включение этой опции приведёт к отображению времени последнего обновления каждой страницы. Вы можете настроить текст с помощью опции [`themeConfig.lastUpdated.text`](./default-theme-config#lastupdated). ## Кастомизация {#customization} @@ -530,11 +554,13 @@ export default { Проверьте [объявление типа и jsdocs](https://github.com/vuejs/vitepress/blob/main/src/node/markdown/markdown.ts) на наличие всех доступных опций. +Установите `markdown.headers` в значение `true` или передайте параметры [`@mdit-vue/plugin-headers`](https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-headers), чтобы собирать заголовки в [`useData().page.headers`](./runtime-api#usedata). Этот параметр отключён по умолчанию. + ### vite {#vite} - Тип: `import('vite').UserConfig` -Передаёт необработанную [конфигурацию Vite](https://vitejs.dev/config/) внутреннему серверу разработки / сборщику Vite. +Передаёт необработанную [конфигурацию Vite](https://vite-docs.ru/config/) внутреннему серверу разработки / сборщику Vite. ```js export default { @@ -607,7 +633,7 @@ interface SSGContext { - Тип: `(context: TransformContext) => Awaitable` -`transformHead` — это хук сборки для преобразования заголовка перед генерацией каждой страницы. Это позволит вам добавить в конфигурацию VitePress записи, которые не могут быть добавлены статически. Вам нужно только вернуть дополнительные записи, они будут автоматически объединены с существующими. +`transformHead` — это хук сборки для добавления дополнительных тегов в `` каждой страницы. Он позволяет добавлять элементы в head, которые невозможно статически добавить в конфигурацию VitePress. Вам нужно только вернуть дополнительные элементы — они будут автоматически объединены с уже существующими. ::: warning ПРЕДУПРЕЖДЕНИЕ Не мутируйте ничего внутри `context`. @@ -635,44 +661,38 @@ interface TransformContext { } ``` -Обратите внимание, что этот хук вызывается только при статической генерации сайта. Он не вызывается во время разработки. Если вам нужно добавить динамические записи в голову во время разработки, вместо этого вы можете использовать хук [`transformPageData`](#transformpagedata): +Этот хук вызывается только при выполнении сборки и не вызывается в режиме разработки. -```ts -export default { - transformPageData(pageData) { - pageData.frontmatter.head ??= [] - pageData.frontmatter.head.push([ - 'meta', - { - name: 'og:title', - content: - pageData.frontmatter.layout === 'home' - ? `VitePress` - : `${pageData.title} | VitePress` - } - ]) - } -} -``` +Дополнительные теги будут добавлены в статические HTML-файлы, созданные во время сборки. Они не будут обновляться при навигации на стороне клиента. + +Во многих случаях более подходящим решением будет использование хука [`transformPageData`](#transformpagedata). Этот хук также применяется как при клиентской навигации, так и в режиме разработки. Однако если генерация тегов head требует значительных вычислительных ресурсов, `transformHead` позволяет избежать этих затрат во время разработки. -#### Пример: Добавление канонического URL-адреса `` {#example-adding-a-canonical-url-link} +#### Пример: добавление мета-тега `og:image` {#example-adding-og-image-meta} ```ts export default { - transformPageData(pageData) { - const canonicalUrl = `https://example.com/${pageData.relativePath}` - .replace(/index\.md$/, '') - .replace(/\.md$/, '.html') + async transformHead(context) { + if (context.page === '404.md') { + return + } - pageData.frontmatter.head ??= [] - pageData.frontmatter.head.push([ - 'link', - { rel: 'canonical', href: canonicalUrl } - ]) + // Детали реализации `generatePageImage` зависят от ваших требований. + // Здесь мы предполагаем, что она создаёт подходящее изображение + // для каждой страницы и возвращает URL изображения. + const imageUrl = await generatePageImage(context) + + return [[ + 'meta', + { name: 'og:image', content: imageUrl } + ]] } } ``` +Здесь мы предполагаем, что URL изображения является динамическим и требует много времени для генерации. Использование `transformHead` позволяет избежать этих затрат во время разработки. + +Для более простых случаев может быть достаточно использовать параметр [`head`](./frontmatter-config#head) в метаданных или [`transformPageData`](#transformpagedata). + ### transformHtml {#transformhtml} - Тип: `(code: string, id: string, context: TransformContext) => Awaitable` @@ -721,3 +741,39 @@ interface TransformPageContext { siteConfig: SiteConfig } ``` + +#### Пример: добавление `` {#example-adding-a-meta-name-og-title} + +```ts +export default { + transformPageData(pageData) { + const title = pageData.frontmatter.layout === 'home' + ? 'VitePress' + : `${pageData.title} | VitePress` + + pageData.frontmatter.head ??= [] + pageData.frontmatter.head.push([ + 'meta', + { name: 'og:title', content: title } + ]) + } +} +``` + +#### Пример: добавление `` с каноническим URL {#example-adding-a-canonical-url-link} + +```ts +export default { + transformPageData(pageData) { + const canonicalUrl = `https://example.com/${pageData.relativePath}` + .replace(/index\.md$/, '') + .replace(/\.md$/, '.html') + + pageData.frontmatter.head ??= [] + pageData.frontmatter.head.push([ + 'link', + { rel: 'canonical', href: canonicalUrl } + ]) + } +} +``` diff --git a/docs/snippets/algolia-i18n.ts b/docs/snippets/algolia-i18n.ts index c57816c3..0dfa5107 100644 --- a/docs/snippets/algolia-i18n.ts +++ b/docs/snippets/algolia-i18n.ts @@ -89,7 +89,6 @@ export default defineConfig({ afterToolCallText: '已搜索', stoppedStreamingText: '你已停止此回复', errorTitleText: '聊天错误', - threadDepthExceededMessage: '为保持回答准确,此对话已关闭。', startNewConversationButtonText: '开始新的对话' } } diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 00000000..c3617830 --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "types": ["node"] + }, + "include": ["**/*", ".vitepress/**/*"] +} diff --git a/docs/zh/config.ts b/docs/zh/config.ts index 91687c54..e558711b 100644 --- a/docs/zh/config.ts +++ b/docs/zh/config.ts @@ -1,9 +1,30 @@ -import { createRequire } from 'module' -import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' +import { createRequire } from 'node:module' +import { + defineAdditionalConfig, + type DefaultTheme, + type MarkdownLocaleOptions +} from 'vitepress' const require = createRequire(import.meta.url) const pkg = require('vitepress/package.json') +export const markdown: MarkdownLocaleOptions = { + container: { + tipLabel: '提示', + infoLabel: '信息', + warningLabel: '警告', + dangerLabel: '危险', + detailsLabel: '详细信息', + noteLabel: '注意', + importantLabel: '重要', + cautionLabel: '小心' + }, + codeCopyButton: { + tooltipText: '复制代码', + copiedText: '已复制' + } +} + export default defineAdditionalConfig({ description: '由 Vite 和 Vue 驱动的静态站点生成器', @@ -209,7 +230,7 @@ function searchOptions(): Partial { closeText: '关闭', backToSearchText: '返回搜索', closeKeyAriaLabel: 'Esc 键', - poweredByText: '由…提供支持' + poweredByText: '搜索提供' }, errorScreen: { titleText: '无法获取结果', @@ -250,7 +271,6 @@ function searchOptions(): Partial { afterToolCallText: '已搜索', stoppedStreamingText: '你已停止此回复', errorTitleText: '聊天错误', - threadDepthExceededMessage: '为保持回答准确,此对话已关闭。', startNewConversationButtonText: '开始新的对话' } } @@ -301,7 +321,7 @@ function searchOptions(): Partial { '我会搜索你的文档,快速帮你找到设置指南、功能细节和故障排除提示。' }, logo: { - poweredByText: '由…提供支持' + poweredByText: '搜索提供' } } } diff --git a/docs/zh/guide/asset-handling.md b/docs/zh/guide/asset-handling.md index 2d0328dc..209a96d3 100644 --- a/docs/zh/guide/asset-handling.md +++ b/docs/zh/guide/asset-handling.md @@ -6,7 +6,7 @@ description: 了解如何在 VitePress 中引用和处理静态资源,如图 ## 引用静态资源 {#referencing-static-assets} -所有的 Markdown 文件都会被编译成 Vue 组件,并由 [Vite](https://cn.vitejs.dev/guide/assets.html) 处理。可以**并且应该**使用相对路径来引用资源: +所有的 Markdown 文件都会被编译成 Vue 组件,并由 [Vite](https://cn.vite.dev/guide/assets.html) 处理。可以**并且应该**使用相对路径来引用资源: ```md ![An image](./image.png) diff --git a/docs/zh/guide/deploy.md b/docs/zh/guide/deploy.md index 75c99ed3..79ddf35a 100644 --- a/docs/zh/guide/deploy.md +++ b/docs/zh/guide/deploy.md @@ -52,7 +52,7 @@ description: 将 VitePress 站点部署到 Netlify、Vercel、GitHub Pages 等 默认情况下,我们假设站点将部署在域名 (`/`) 的根路径上。如果站点在子路径中提供服务,例如 `https://mywebsite.com/blog/`,则需要在 VitePress 配置中将 [`base`](../reference/site-config#base) 选项设置为 `'/blog/'`。 -**例**:如果你使用的是 Github(或 GitLab)页面并部署到 `user.github.io/repo/`,请将 `base` 设置为 `/repo/`。 +**例**:如果你使用的是 GitHub(或 GitLab)页面并部署到 `user.github.io/repo/`,请将 `base` 设置为 `/repo/`。 ## HTTP 缓存标头 {#http-cache-headers} @@ -201,7 +201,7 @@ Cache-Control: max-age=31536000,immutable ### GitLab Pages -1. 如果你想部署到 `https:// .gitlab.io/ /`,将 VitePress 配置中的 `outDir` 设置为 `../public`。将 `base` 选项配置为 `'//'`。如果你部署到自定义域名、用户或组织页面,或在 GitLab 中启用了“Use unique domain”设置,则不需要 `base`。 +1. 如果你想部署到 `https://.gitlab.io//`,将 VitePress 配置中的 `outDir` 设置为 `../public`。将 `base` 选项配置为 `'//'`。如果你部署到自定义域名、用户或组织页面,或在 GitLab 中启用了“Use unique domain”设置,则不需要 `base`。 2. 在项目的根目录中创建一个名为 `.gitlab-ci.yml` 的文件,其中包含以下内容。每当你更改内容时,这都会构建和部署你的站点: diff --git a/docs/zh/guide/extending-default-theme.md b/docs/zh/guide/extending-default-theme.md index 88488fa7..974fca81 100644 --- a/docs/zh/guide/extending-default-theme.md +++ b/docs/zh/guide/extending-default-theme.md @@ -119,7 +119,7 @@ export default { } satisfies Theme ``` -因为我们使用 Vite,还可以利用 Vite 的 [glob 导入功能](https://cn.vitejs.dev/guide/features.html#glob-import)来自动注册一个组件目录。 +因为我们使用 Vite,还可以利用 Vite 的 [glob 导入功能](https://cn.vite.dev/guide/features.html#glob-import)来自动注册一个组件目录。 ## 布局插槽 {#layout-slots} @@ -308,7 +308,7 @@ provide('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => { ## 重写内部组件 {#overriding-internal-components} -可以使用 Vite 的 [aliases](https://vitejs.dev/config/shared-options.html#resolve-alias) 来用自定义组件替换默认主题的组件: +可以使用 Vite 的 [aliases](https://vite.dev/config/shared-options.html#resolve-alias) 来用自定义组件替换默认主题的组件: ```ts import { fileURLToPath, URL } from 'node:url' diff --git a/docs/zh/guide/getting-started.md b/docs/zh/guide/getting-started.md index 03af7b1e..1f4bf0c1 100644 --- a/docs/zh/guide/getting-started.md +++ b/docs/zh/guide/getting-started.md @@ -12,7 +12,7 @@ description: 快速上手 VitePress。了解如何安装、创建项目结构并 ### 前置准备 {#prerequisites} -- [Node.js](https://nodejs.org/) 20 及以上版本。 +- [Node.js](https://nodejs.org/) 22 及以上版本。 - 通过命令行界面 (CLI) 访问 VitePress 的终端。 - 支持 [Markdown](https://en.wikipedia.org/wiki/Markdown) 语法的编辑器。 - 推荐 [VSCode](https://code.visualstudio.com/) 及其[官方 Vue 扩展](https://marketplace.visualstudio.com/items?itemName=Vue.volar)。 @@ -41,7 +41,7 @@ $ bun add -D vitepress@next ::: tip 注意 -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')` 代替。 ::: diff --git a/docs/zh/guide/i18n.md b/docs/zh/guide/i18n.md index 4a5ccc59..96b16705 100644 --- a/docs/zh/guide/i18n.md +++ b/docs/zh/guide/i18n.md @@ -55,7 +55,7 @@ interface LocaleSpecificConfig { 有关自定义默认主题的文本占位符的信息,请参考 [`DefaultTheme.Config`](https://github.com/vuejs/vitepress/blob/main/types/default-theme.d.ts) 接口。不要在 locale 级别覆盖 `themeConfig.algolia` 或 `themeConfig.carbonAds`。想获取多语言搜索的信息,请参考 [Algolia 文档](../reference/default-theme-search#i18n)。 -**提示**:配置文件也可以是 `docs/.vitepress/config/index.ts`。通过为每个语言环境创建一个配置文件,然后从 `index.ts` 合并并导出它们,可以更好的组织文件。 +**提示**:配置文件也可以是 `docs/.vitepress/config/index.ts`。通过为每个语言环境创建一个配置文件,然后从 `index.ts` 合并并导出它们,可以更好地组织文件。 ## 为本地化设置子目录 {#separate-directory-for-each-locale} @@ -79,7 +79,7 @@ docs/ /* /en/:splat 302 ``` -**提示:** 如果使用上述的方法,可以使用`nf_lang` cookie 来保存用户的语言选择。例如,可以在主题中添加以下代码: +**提示:** 如果使用上述的方法,可以使用 `nf_lang` cookie 来保存用户的语言选择。例如,可以在主题中添加以下代码: ```ts [docs/.vitepress/theme/index.ts] import DefaultTheme from 'vitepress/theme' diff --git a/docs/zh/guide/markdown.md b/docs/zh/guide/markdown.md index 1ae240f7..2476c713 100644 --- a/docs/zh/guide/markdown.md +++ b/docs/zh/guide/markdown.md @@ -1,5 +1,6 @@ --- description: VitePress 内置的 Markdown 扩展,包括自定义容器、代码块语法高亮、行高亮、代码组等功能。 +outline: deep --- # Markdown 扩展 {#markdown-extensions} @@ -112,7 +113,7 @@ lang: en-US :tada: :100: -这里可以找到[所有支持的 emoji 列表](https://github.com/markdown-it/markdown-it-emoji/blob/master/lib/data/full.mjs)。 +这里可以找到[所有支持的 emoji 列表](https://github.com/mdit-plugins/mdit-plugins/blob/main/packages/plugin-emoji/src/data/full.ts)。 ## 目录表 (TOC) {#table-of-contents} @@ -231,7 +232,7 @@ export default defineConfig({ ### `raw` -这是一个特殊的容器,可以用来防止与 VitePress 的样式和路由冲突。这在记录组件库时特别有用。可能还想查看 [whyframe](https://whyframe.dev/docs/integrations/vitepress) 以获得更好的隔离。 +这是一个特殊的容器,可以用来防止与 VitePress 的样式和路由冲突。这在记录组件库时特别有用。 **语法** @@ -888,14 +889,14 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$ ## 图片懒加载 {#image-lazy-loading} -通过在配置文件中将 `lazyLoading` 设置为 `true`,可以为通过 markdown 添加的每张图片启用懒加载。 +通过在配置文件中将 `lazyLoad` 设置为 `true`,可以为通过 markdown 添加的每张图片启用懒加载。 ```js export default { markdown: { image: { // 默认禁用;设置为 true 可为所有图片启用懒加载。 - lazyLoading: true + lazyLoad: true } } } @@ -907,15 +908,15 @@ VitePress 使用 [markdown-it](https://github.com/markdown-it/markdown-it) 作 ```js import { defineConfig } from 'vitepress' -import markdownItAnchor from 'markdown-it-anchor' +import { headerLink } from '@mdit/plugin-anchor' import markdownItFoo from 'markdown-it-foo' export default defineConfig({ markdown: { - // markdown-it-anchor 的选项 - // https://github.com/valeriangalliat/markdown-it-anchor#usage + // @mdit/plugin-anchor 的选项 + // https://mdit-plugins.github.io/anchor.html anchor: { - permalink: markdownItAnchor.permalink.headerLink() + permalink: headerLink() }, // @mdit-vue/plugin-toc 的选项 // https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-toc#options diff --git a/docs/zh/guide/migration-from-vitepress-0.md b/docs/zh/guide/migration-from-vitepress-0.md index fc2bf97a..34656f00 100644 --- a/docs/zh/guide/migration-from-vitepress-0.md +++ b/docs/zh/guide/migration-from-vitepress-0.md @@ -1,4 +1,4 @@ -n# 从 VitePress 0.x 迁移 {#migration-from-vitepress-0-x} +# 从 VitePress 0.x 迁移 {#migration-from-vitepress-0-x} 如果你来自 VitePress 0.x 版本,VitePress 有了一些重大更改。请按照本指南了解如何将应用程序迁移到最新的 VitePress。 @@ -11,13 +11,13 @@ n# 从 VitePress 0.x 迁移 {#migration-from-vitepress-0-x} - `sidebar` 选项改变了它的结构。 - `children` 现在命名为 `items`。 - 顶级侧边栏不包含 `link`。我们打算把它改回来。 -- 删除了 `repo`、`repoLabel`、`docsDir`、`docsBranch`、`editLinks`、`editLinkText`,以支持更灵活的api。 +- 删除了 `repo`、`repoLabel`、`docsDir`、`docsBranch`、`editLinks`、`editLinkText`,以支持更灵活的 API。 - 要将带有图标的 GitHub 链接添加到导航,请使用 [社交链接](../reference/default-theme-config#nav) 功能。 - 要添加“编辑此页面”功能,请使用 [编辑链接](../reference/default-theme-edit-link) 功能。 -- `lastUpdated` 选项现在分为` config.lastUpdated` 和 `themeConfig.lastUpdatedText`。 -- `carbonAds.carbon` 更改为 `carbonAds.code`. +- `lastUpdated` 选项现在分为 `config.lastUpdated` 和 `themeConfig.lastUpdated.text`。 +- `carbonAds.carbon` 更改为 `carbonAds.code`。 ## frontmatter 配置 {#frontmatter-config} - `home: true` 选项已更改为 `layout: home`。此外,还修改了许多与主页相关的设置以提供附加功能。详情请参阅 [主页指南](../reference/default-theme-home-page)。 -- `footer` 选项移至 [`themeConfig.footer`](../reference/default-theme-footer). +- `footer` 选项移至 [`themeConfig.footer`](../reference/default-theme-footer)。 diff --git a/docs/zh/guide/routing.md b/docs/zh/guide/routing.md index db652f97..06fa2eba 100644 --- a/docs/zh/guide/routing.md +++ b/docs/zh/guide/routing.md @@ -291,7 +291,7 @@ export default { 从本地文件生成路径: ```js -import fs from 'fs' +import fs from 'node:fs' export default { paths() { diff --git a/docs/zh/guide/ssr-compat.md b/docs/zh/guide/ssr-compat.md index c4f4dfd8..16b567cc 100644 --- a/docs/zh/guide/ssr-compat.md +++ b/docs/zh/guide/ssr-compat.md @@ -39,7 +39,7 @@ onMounted(() => { ### 条件导入 {#conditional-import} -也可以使用 `import.meta.env.SSR` 标志 ([Vite 环境变量](https://cn.vitejs.dev/guide/env-and-mode.html#env-variables)的一部分) 来有条件地导入依赖项: +也可以使用 `import.meta.env.SSR` 标志 ([Vite 环境变量](https://cn.vite.dev/guide/env-and-mode.html#env-variables)的一部分) 来有条件地导入依赖项: ```js if (!import.meta.env.SSR) { diff --git a/docs/zh/guide/using-vue.md b/docs/zh/guide/using-vue.md index 4dc856d9..de868f39 100644 --- a/docs/zh/guide/using-vue.md +++ b/docs/zh/guide/using-vue.md @@ -2,7 +2,7 @@ description: 在 VitePress 的 Markdown 文件中直接使用 Vue 组件和动态模板功能。 --- -# 在 Markdown 使用 Vue {#using-vue-in-markdown} +# 在 Markdown 中使用 Vue {#using-vue-in-markdown} 在 VitePress 中,每个 Markdown 文件都被编译成 HTML,而且将其作为 [Vue 单文件组件](https://cn.vuejs.org/guide/scaling-up/sfc.html)处理。这意味着可以在 Markdown 中使用任何 Vue 功能,包括动态模板、使用 Vue 组件或通过添加 ` diff --git a/src/client/theme-default/components/VPDocAsideCarbonAds.vue b/src/client/theme-default/components/VPDocAsideCarbonAds.vue index d10598dd..a04d4f67 100644 --- a/src/client/theme-default/components/VPDocAsideCarbonAds.vue +++ b/src/client/theme-default/components/VPDocAsideCarbonAds.vue @@ -1,6 +1,6 @@ diff --git a/src/client/theme-default/components/VPHomeContent.vue b/src/client/theme-default/components/VPHomeContent.vue index 3e026235..010b95fb 100644 --- a/src/client/theme-default/components/VPHomeContent.vue +++ b/src/client/theme-default/components/VPHomeContent.vue @@ -11,6 +11,7 @@ const { width: vw } = useWindowSize({
      diff --git a/src/client/theme-default/components/VPImage.vue b/src/client/theme-default/components/VPImage.vue index 8a2f5131..0a014d3c 100644 --- a/src/client/theme-default/components/VPImage.vue +++ b/src/client/theme-default/components/VPImage.vue @@ -40,7 +40,7 @@ defineOptions({ inheritAttrs: false }) html:not(.dark) .VPImage.dark { display: none; } -.dark .VPImage.light { +html.dark .VPImage.light { display: none; } diff --git a/src/client/theme-default/components/VPLink.vue b/src/client/theme-default/components/VPLink.vue index f087a88c..36977697 100644 --- a/src/client/theme-default/components/VPLink.vue +++ b/src/client/theme-default/components/VPLink.vue @@ -2,14 +2,16 @@ import { computed } from 'vue' import { isLinkExternal, normalizeLink } from '../support/utils' -const props = defineProps<{ +const props = withDefaults(defineProps<{ tag?: string href?: string noIcon?: boolean external?: boolean target?: string rel?: string -}>() +}>(), { + external: undefined, +}) const tag = computed(() => props.tag ?? (props.href ? 'a' : 'span')) const isExternal = computed(() => diff --git a/src/client/theme-default/components/VPLocalNavOutlineDropdown.vue b/src/client/theme-default/components/VPLocalNavOutlineDropdown.vue index cf842baa..7aded0f9 100644 --- a/src/client/theme-default/components/VPLocalNavOutlineDropdown.vue +++ b/src/client/theme-default/components/VPLocalNavOutlineDropdown.vue @@ -65,9 +65,10 @@ function scrollToTop() { @@ -32,9 +34,13 @@ const { theme } = useData() display: none; } +.list { + display: flex; +} + @media (min-width: 768px) { .VPNavBarMenu { - display: flex; + display: block; } } diff --git a/src/client/theme-default/components/VPNavBarMenuGroup.vue b/src/client/theme-default/components/VPNavBarMenuGroup.vue index 85350d47..61004f3f 100644 --- a/src/client/theme-default/components/VPNavBarMenuGroup.vue +++ b/src/client/theme-default/components/VPNavBarMenuGroup.vue @@ -1,7 +1,7 @@ diff --git a/src/client/theme-default/components/VPNavScreenMenuGroupLink.vue b/src/client/theme-default/components/VPNavScreenMenuGroupLink.vue index 0ed634c0..53932dff 100644 --- a/src/client/theme-default/components/VPNavScreenMenuGroupLink.vue +++ b/src/client/theme-default/components/VPNavScreenMenuGroupLink.vue @@ -1,7 +1,7 @@ diff --git a/src/client/theme-default/components/VPNavScreenMenuGroupSection.vue b/src/client/theme-default/components/VPNavScreenMenuGroupSection.vue index a7e2cd38..e094a707 100644 --- a/src/client/theme-default/components/VPNavScreenMenuGroupSection.vue +++ b/src/client/theme-default/components/VPNavScreenMenuGroupSection.vue @@ -11,7 +11,11 @@ defineProps<{ diff --git a/src/client/theme-default/components/VPNavScreenMenuLink.vue b/src/client/theme-default/components/VPNavScreenMenuLink.vue index ff91a20c..c6b6bacf 100644 --- a/src/client/theme-default/components/VPNavScreenMenuLink.vue +++ b/src/client/theme-default/components/VPNavScreenMenuLink.vue @@ -1,7 +1,7 @@ diff --git a/src/client/theme-default/components/VPNavScreenTranslations.vue b/src/client/theme-default/components/VPNavScreenTranslations.vue index 108ac60c..c81b33ce 100644 --- a/src/client/theme-default/components/VPNavScreenTranslations.vue +++ b/src/client/theme-default/components/VPNavScreenTranslations.vue @@ -3,7 +3,9 @@ import { ref } from 'vue' import { useLangs } from '../composables/langs' import VPLink from './VPLink.vue' -const { localeLinks, currentLang } = useLangs({ correspondingLink: true }) +const { localeLinks, currentLang } = useLangs({ + linkToCorrespondingPage: true +}) const isOpen = ref(false) function toggle() { @@ -33,6 +35,7 @@ function toggle() { :hreflang="locale.lang" rel="alternate" :dir="locale.dir" + data-allow-mismatch="attribute" > {{ locale.text }} diff --git a/src/client/theme-default/components/VPSidebarItem.vue b/src/client/theme-default/components/VPSidebarItem.vue index d71d1062..e4c63f2b 100644 --- a/src/client/theme-default/components/VPSidebarItem.vue +++ b/src/client/theme-default/components/VPSidebarItem.vue @@ -94,16 +94,16 @@ function onCaretClick() {

      -
      - -
      +
    • +
    diff --git a/src/client/theme-default/components/VPSkipLink.vue b/src/client/theme-default/components/VPSkipLink.vue index 965ffdcc..bcff7d19 100644 --- a/src/client/theme-default/components/VPSkipLink.vue +++ b/src/client/theme-default/components/VPSkipLink.vue @@ -1,6 +1,6 @@ diff --git a/src/client/theme-default/components/VPSponsors.vue b/src/client/theme-default/components/VPSponsors.vue index e7e739dc..00654512 100644 --- a/src/client/theme-default/components/VPSponsors.vue +++ b/src/client/theme-default/components/VPSponsors.vue @@ -1,7 +1,7 @@ diff --git a/src/client/theme-default/components/VPTeamMembers.vue b/src/client/theme-default/components/VPTeamMembers.vue index 0337eb5a..6b83e4a6 100644 --- a/src/client/theme-default/components/VPTeamMembers.vue +++ b/src/client/theme-default/components/VPTeamMembers.vue @@ -17,11 +17,11 @@ const classes = computed(() => [props.size, `count-${props.members.length}`]) @@ -63,4 +63,15 @@ const classes = computed(() => [props.size, `count-${props.members.length}`]) margin: 0 auto; max-width: 1152px; } + +/* Reset styles from vp-doc if used in markdown */ +.vp-doc .VPTeamMembers .container { + list-style: none; + margin: 0 auto; + padding: 0; +} +.vp-doc .VPTeamMembers .item { + margin: 0; + padding: 0; +} diff --git a/src/client/theme-default/composables/flyout.ts b/src/client/theme-default/composables/flyout.ts index 9b10a704..5f80e93f 100644 --- a/src/client/theme-default/composables/flyout.ts +++ b/src/client/theme-default/composables/flyout.ts @@ -1,5 +1,5 @@ +import { inBrowser } from 'vitepress' import { onUnmounted, readonly, type Ref, ref, watch } from 'vue' -import { inBrowser } from '../../shared' interface UseFlyoutOptions { el: Ref diff --git a/src/client/theme-default/composables/langs.ts b/src/client/theme-default/composables/langs.ts index 625f1175..64ca65ce 100644 --- a/src/client/theme-default/composables/langs.ts +++ b/src/client/theme-default/composables/langs.ts @@ -1,9 +1,24 @@ +import type { DefaultTheme } from 'vitepress/theme' import { computed } from 'vue' +import { useRoute } from '../../app/router' +import type { Route, VitePressData } from '../../shared' import { ensureStartingSlash } from '../support/utils' import { useData } from './data' -export function useLangs({ correspondingLink = false } = {}) { - const { site, localeIndex, page, theme, hash } = useData() +export function useLangs({ + linkToCorrespondingPage = false +}: { + /** + * Link each entry of the translations menu to the current page's + * equivalent in that locale (resolved by `resolveLocaleLink`) instead of + * that locale's home page. + */ + linkToCorrespondingPage?: boolean +} = {}) { + const data = useData() + const route = useRoute() + const { site, localeIndex } = data + const currentLang = computed(() => ({ label: site.value.locales[localeIndex.value]?.label, link: @@ -17,36 +32,83 @@ export function useLangs({ correspondingLink = false } = {}) { ? [] : { text: value.label, - link: - normalizeLink( + link: resolveLocaleLink(data, route, { + targetLocale: key, + targetLocaleLink: value.link || (key === 'root' ? '/' : `/${key}/`), - theme.value.i18nRouting !== false && correspondingLink, - page.value.relativePath.slice( - currentLang.value.link.length - 1 - ), - !site.value.cleanUrls - ) + hash.value, + currentLocaleLink: currentLang.value.link, + linkToCorrespondingPage + }), lang: value.lang, dir: value.dir } ) ) - return { localeLinks, currentLang } + return { currentLang, localeLinks } +} + +/** + * Resolves the link used for switching from the current page to + * `targetLocale`. Without `linkToCorrespondingPage`, this is simply the home + * of the target locale. With it, the current page's path is rewritten into + * the target locale (honoring `cleanUrls`) — unless + * `themeConfig.i18nRouting` is `false` (the locale home is used instead) or + * a function (which then fully controls the resolution). + * + * The current query and hash are carried over, except when a custom + * `i18nRouting` function is used. + */ +export function resolveLocaleLink( + data: VitePressData, + route: Route, + { + targetLocale, + targetLocaleLink, + currentLocaleLink, + linkToCorrespondingPage + }: { + /** Key of the target locale in `site.locales`, e.g. `'fr'` or `'root'`. */ + targetLocale: string + /** Home link of the target locale, e.g. `'/fr/'`. */ + targetLocaleLink: string + /** Home link of the locale the current page is in, e.g. `'/'`. */ + currentLocaleLink: string + /** Link to the current page's equivalent instead of the locale home. */ + linkToCorrespondingPage: boolean + } +) { + const { site, theme } = data + const i18nRouting = theme.value.i18nRouting + + if (linkToCorrespondingPage && typeof i18nRouting === 'function') { + return i18nRouting(data, route, targetLocale) + } + + return ( + normalizeLink( + targetLocaleLink, + i18nRouting !== false && linkToCorrespondingPage, + route.data.relativePath.slice(currentLocaleLink.length - 1), + !site.value.cleanUrls + ) + + route.query + + route.hash + ) } function normalizeLink( - link: string, - addPath: boolean, - path: string, - addExt: boolean + localeLink: string, + appendPagePath: boolean, + pagePath: string, + addHtmlExt: boolean ) { - return addPath - ? link.replace(/\/$/, '') + + return appendPagePath + ? localeLink.replace(/\/$/, '') + ensureStartingSlash( - path + pagePath .replace(/(^|\/)index\.md$/, '$1') - .replace(/\.md$/, addExt ? '.html' : '') + .replace(/\.md$/, addHtmlExt ? '.html' : '') ) - : link + : localeLink } diff --git a/src/client/theme-default/composables/layout.ts b/src/client/theme-default/composables/layout.ts index 37943eec..d6efb7ec 100644 --- a/src/client/theme-default/composables/layout.ts +++ b/src/client/theme-default/composables/layout.ts @@ -74,7 +74,7 @@ interface RegisterWatchersOptions { } export function registerWatchers({ closeSidebar }: RegisterWatchersOptions) { - const { frontmatter, page, theme } = useData() + const { theme, page, frontmatter } = useData() watch( () => [page.value.relativePath, theme.value.sidebar] as const, diff --git a/src/client/theme-default/composables/outline.ts b/src/client/theme-default/composables/outline.ts index 752ad15b..067b2b1a 100644 --- a/src/client/theme-default/composables/outline.ts +++ b/src/client/theme-default/composables/outline.ts @@ -1,4 +1,3 @@ -import { getScrollOffset } from 'vitepress' import type { DefaultTheme } from 'vitepress/theme' import { onMounted, onUnmounted, onUpdated, type Ref } from 'vue' import { throttleAndDebounce } from '../support/utils' @@ -14,7 +13,6 @@ export function resolveTitle(theme: DefaultTheme.Config): string { (typeof theme.outline === 'object' && !Array.isArray(theme.outline) && theme.outline.label) || - theme.outlineTitle || 'On this page' ) } @@ -86,10 +84,12 @@ export function useActiveAnchor( const onScroll = throttleAndDebounce(setActiveLink, 100) let prevActiveLink: HTMLAnchorElement | null = null + let ignoreScrollOnce: boolean = false onMounted(() => { requestAnimationFrame(setActiveLink) window.addEventListener('scroll', onScroll) + container.value.addEventListener('click', onClick) }) onUpdated(() => { @@ -101,11 +101,30 @@ export function useActiveAnchor( window.removeEventListener('scroll', onScroll) }) + function onClick(e: MouseEvent) { + if (!isAsideEnabled.value) { + return + } + + const hash = + e.target instanceof Element ? e.target.closest('a')?.hash : null + + if (hash) { + ignoreScrollOnce = true + activateLink(hash) + } + } + function setActiveLink() { if (!isAsideEnabled.value) { return } + if (ignoreScrollOnce) { + ignoreScrollOnce = false + return + } + const scrollY = window.scrollY const innerHeight = window.innerHeight const offsetHeight = document.body.offsetHeight @@ -115,7 +134,9 @@ export function useActiveAnchor( const headers = resolvedHeaders .map(({ element, link }) => ({ link, - top: getAbsoluteTop(element) + top: getAbsoluteTop(element), + scrollMarginTop: + Number.parseFloat(getComputedStyle(element).scrollMarginTop) || 0 })) .filter(({ top }) => !Number.isNaN(top)) .sort((a, b) => a.top - b.top) @@ -140,8 +161,8 @@ export function useActiveAnchor( // find the last header above the top of viewport let activeLink: string | null = null - for (const { link, top } of headers) { - if (top > scrollY + getScrollOffset() + 4) { + for (const { link, top, scrollMarginTop } of headers) { + if (top > scrollY + scrollMarginTop + 4) { break } activeLink = link @@ -158,7 +179,7 @@ export function useActiveAnchor( prevActiveLink = null } else { prevActiveLink = container.value.querySelector( - `a[href="${decodeURIComponent(hash)}"]` + `a[href$="${decodeURIComponent(hash)}"]` ) } @@ -200,8 +221,7 @@ function buildTree( const result: DefaultTheme.OutlineItem[] = [] const stack: ( - | DefaultTheme.OutlineItem - | { level: number; shouldIgnore: true } + DefaultTheme.OutlineItem | { level: number; shouldIgnore: true } )[] = [] data.forEach((item) => { diff --git a/src/client/theme-default/composables/prev-next.ts b/src/client/theme-default/composables/prev-next.ts index 494242a6..258d9856 100644 --- a/src/client/theme-default/composables/prev-next.ts +++ b/src/client/theme-default/composables/prev-next.ts @@ -1,20 +1,24 @@ import { computed } from 'vue' -import { isActive } from '../../shared' +import { isActive, normalize } from '../../shared' import { getFlatSideBarLinks, getSidebar } from '../support/sidebar' +import { uniqBy } from '../support/utils' import { useData } from './data' export function usePrevNext() { - const { page, theme, frontmatter } = useData() + const { theme, page, frontmatter } = useData() - return computed(() => { + return computed<{ + prev?: { text?: string; link?: string; target?: string; rel?: string } + next?: { text?: string; link?: string; target?: string; rel?: string } + }>(() => { const sidebar = getSidebar(theme.value.sidebar, page.value.relativePath) const links = getFlatSideBarLinks(sidebar) // ignore inner-page links with hashes - const candidates = uniqBy(links, (link) => link.link.replace(/[?#].*$/, '')) + const candidates = uniqBy(links, (link) => normalize(link.link)) const index = candidates.findIndex((link) => { - return isActive(page.value.relativePath, link.link) + return isActive(page.value.relativePath, '', link.link, false, true) }) const hidePrev = @@ -40,7 +44,15 @@ export function usePrevNext() { link: (typeof frontmatter.value.prev === 'object' ? frontmatter.value.prev.link - : undefined) ?? candidates[index - 1]?.link + : undefined) ?? candidates[index - 1]?.link, + target: + (typeof frontmatter.value.prev === 'object' + ? frontmatter.value.prev.target + : undefined) ?? candidates[index - 1]?.target, + rel: + (typeof frontmatter.value.prev === 'object' + ? frontmatter.value.prev.rel + : undefined) ?? candidates[index - 1]?.rel }, next: hideNext ? undefined @@ -56,19 +68,16 @@ export function usePrevNext() { link: (typeof frontmatter.value.next === 'object' ? frontmatter.value.next.link - : undefined) ?? candidates[index + 1]?.link + : undefined) ?? candidates[index + 1]?.link, + target: + (typeof frontmatter.value.next === 'object' + ? frontmatter.value.next.target + : undefined) ?? candidates[index + 1]?.target, + rel: + (typeof frontmatter.value.next === 'object' + ? frontmatter.value.next.rel + : undefined) ?? candidates[index + 1]?.rel } - } as { - prev?: { text?: string; link?: string } - next?: { text?: string; link?: string } } }) } - -function uniqBy(array: T[], keyFn: (item: T) => any): T[] { - const seen = new Set() - return array.filter((item) => { - const k = keyFn(item) - return seen.has(k) ? false : seen.add(k) - }) -} diff --git a/src/client/theme-default/composables/sidebar.ts b/src/client/theme-default/composables/sidebar.ts index b693cc89..1b9df6bd 100644 --- a/src/client/theme-default/composables/sidebar.ts +++ b/src/client/theme-default/composables/sidebar.ts @@ -1,17 +1,17 @@ +import { useRoute } from 'vitepress' import type { DefaultTheme } from 'vitepress/theme' import { computed, + nextTick, onMounted, onUnmounted, ref, watch, watchEffect, - watchPostEffect, type ComputedRef } from 'vue' import { isActive } from '../../shared' import { hasActiveLink as containsActiveLink } from '../support/sidebar' -import { useData } from './data' const isOpen = ref(false) @@ -68,7 +68,7 @@ export function useSidebarControl() { export function useSidebarItemControl( item: ComputedRef ) { - const { page, hash } = useData() + const route = useRoute() const collapsed = ref(false) @@ -81,22 +81,39 @@ export function useSidebarItemControl( }) const isActiveLink = ref(false) - const updateIsActiveLink = () => { - isActiveLink.value = isActive(page.value.relativePath, item.value.link) - } - - watch([page, item, hash], updateIsActiveLink) - onMounted(updateIsActiveLink) - - const hasActiveLink = computed(() => { + const hasActiveLink = ref(false) + + function updateActiveLink(): void { + if (item.value.link) { + isActiveLink.value = isActive( + route.data.relativePath, + route.hash, + item.value.link + ) + } else { + isActiveLink.value = false + } if (isActiveLink.value) { - return true + hasActiveLink.value = true + nextTick(() => (collapsed.value = false)) + return + } + if (!item.value.items) { + hasActiveLink.value = false + return + } + hasActiveLink.value = containsActiveLink( + route.data.relativePath, + route.hash, + item.value.items + ) + if (hasActiveLink.value) { + nextTick(() => (collapsed.value = false)) } + } - return item.value.items - ? containsActiveLink(page.value.relativePath, item.value.items) - : false - }) + watch([item, route], updateActiveLink) + onMounted(updateActiveLink) const hasChildren = computed(() => { return !!(item.value.items && item.value.items.length) @@ -106,11 +123,7 @@ export function useSidebarItemControl( collapsed.value = !!(collapsible.value && item.value.collapsed) }) - watchPostEffect(() => { - ;(isActiveLink.value || hasActiveLink.value) && (collapsed.value = false) - }) - - function toggle() { + function toggle(): void { if (collapsible.value) { collapsed.value = !collapsed.value } @@ -120,8 +133,8 @@ export function useSidebarItemControl( collapsed, collapsible, isLink, - isActiveLink, - hasActiveLink, + isActiveLink: isActiveLink as ComputedRef, + hasActiveLink: hasActiveLink as ComputedRef, hasChildren, toggle } diff --git a/src/client/theme-default/fonts/inter-italic-latin.woff2 b/src/client/theme-default/fonts/inter-italic-latin.woff2 index fdc2aa22..94a1ae94 100644 Binary files a/src/client/theme-default/fonts/inter-italic-latin.woff2 and b/src/client/theme-default/fonts/inter-italic-latin.woff2 differ diff --git a/src/client/theme-default/fonts/inter-italic-symbols.woff2 b/src/client/theme-default/fonts/inter-italic-symbols.woff2 new file mode 100644 index 00000000..d8425105 Binary files /dev/null and b/src/client/theme-default/fonts/inter-italic-symbols.woff2 differ diff --git a/src/client/theme-default/fonts/inter-roman-latin.woff2 b/src/client/theme-default/fonts/inter-roman-latin.woff2 index 18a8987f..c428d326 100644 Binary files a/src/client/theme-default/fonts/inter-roman-latin.woff2 and b/src/client/theme-default/fonts/inter-roman-latin.woff2 differ diff --git a/src/client/theme-default/fonts/inter-roman-symbols.woff2 b/src/client/theme-default/fonts/inter-roman-symbols.woff2 new file mode 100644 index 00000000..ba86b3dc Binary files /dev/null and b/src/client/theme-default/fonts/inter-roman-symbols.woff2 differ diff --git a/src/client/theme-default/index.ts b/src/client/theme-default/index.ts index fe52cd65..ff26e67f 100644 --- a/src/client/theme-default/index.ts +++ b/src/client/theme-default/index.ts @@ -1,3 +1,4 @@ +import './without-fonts' import './styles/fonts.css' export * from './without-fonts' diff --git a/src/client/theme-default/styles/base.css b/src/client/theme-default/styles/base.css index 4472602f..ca379eb1 100644 --- a/src/client/theme-default/styles/base.css +++ b/src/client/theme-default/styles/base.css @@ -1,55 +1,40 @@ @layer __vitepress_base { - @media (prefers-reduced-motion: reduce) { - *, - ::before, - ::after { - animation-delay: -1ms !important; - animation-duration: 1ms !important; - animation-iteration-count: 1 !important; - background-attachment: initial !important; - scroll-behavior: auto !important; - transition-duration: 0s !important; - transition-delay: 0s !important; - } - } + /* #region Tailwind Preflight */ + + /* Based on https://github.com/tailwindlabs/tailwindcss/blob/e48c5e80473c25fe5a27fe267fbaf4f6512424a3/packages/tailwindcss/preflight.css, + with the `--theme()` references replaced by vitepress variables, and `bolder` replaced by `600` */ *, + ::after, ::before, - ::after { + ::backdrop, + ::file-selector-button { box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; } - html { - line-height: 1.4; - font-size: 16px; + html, + :host { + line-height: 1.5; -webkit-text-size-adjust: 100%; + tab-size: 4; + font-family: var(--vp-font-family-base); + font-feature-settings: normal; + font-variation-settings: normal; + -webkit-tap-highlight-color: transparent; } - html.dark { - color-scheme: dark; - } - - body { - margin: 0; - width: 100%; - min-width: 320px; - min-height: 100vh; - line-height: 24px; - font-family: var(--vp-font-family-base); - font-size: 16px; - font-weight: 400; - color: var(--vp-c-text-1); - background-color: var(--vp-c-bg); - font-synthesis: style; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - text-autospace: normal; - text-spacing-trim: normal; + hr { + height: 0; + color: inherit; + border-top-width: 1px; } - main { - display: block; + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; } h1, @@ -58,70 +43,73 @@ h4, h5, h6 { - margin: 0; - line-height: 24px; - font-size: 16px; - font-weight: 400; + font-size: inherit; + font-weight: inherit; } - p { - margin: 0; + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; } - strong, - b { + b, + strong { font-weight: 600; } - /** - * Avoid 300ms click delay on touch devices that support the `touch-action` - * CSS property. - * - * In particular, unlike most other browsers, IE11+Edge on Windows 10 on - * touch devices and IE Mobile 10-11 DON'T remove the click delay when - * `` is present. - * However, they DO support removing the click delay via - * `touch-action: manipulation`. - * - * See: - * - http://v4-alpha.getbootstrap.com/content/reboot/#click-delay-optimization-for-touch - * - http://caniuse.com/#feat=css-touch-action - * - http://patrickhlauke.github.io/touch/tests/results/#suppressing-300ms-delay - */ - a, - area, - button, - [role='button'], - input, - label, - select, - summary, - textarea { - touch-action: manipulation; + code, + kbd, + samp, + pre { + font-family: var(--vp-font-family-mono); + font-feature-settings: normal; + font-variation-settings: normal; + font-size: 1em; } - a { - color: inherit; - text-decoration: inherit; + small { + font-size: 80%; } - ol, - ul { - list-style: none; - margin: 0; - padding: 0; + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; } - blockquote { - margin: 0; + sub { + bottom: -0.25em; } - pre, - code, - kbd, - samp { - font-family: var(--vp-font-family-mono); - text-autospace: no-autospace; + sup { + top: -0.5em; + } + + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + + :-moz-focusring:where(:not(iframe)) { + outline: auto; + } + + progress { + vertical-align: baseline; + } + + summary { + display: list-item; + } + + ol, + ul, + menu { + list-style: none; } img, @@ -135,10 +123,6 @@ display: block; } - figure { - margin: 0; - } - img, video { max-width: 100%; @@ -147,87 +131,171 @@ button, input, - optgroup, select, - textarea { - border: 0; - padding: 0; - line-height: inherit; + optgroup, + textarea, + ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; color: inherit; + border-radius: 0; + background-color: transparent; + opacity: 1; } - button { + :where(select:is([multiple], [size])) optgroup { + font-weight: 600; + } + + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + + ::file-selector-button { + margin-inline-end: 4px; + } + + ::placeholder { + opacity: 1; + } + + @supports (not (-webkit-appearance: -apple-pay-button)) or + (contain-intrinsic-size: 1px) { + ::placeholder { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + + textarea { + resize: vertical; + } + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + + ::-webkit-datetime-edit { + display: inline-flex; + } + + ::-webkit-datetime-edit-fields-wrapper { padding: 0; - font-family: inherit; - background-color: transparent; - background-image: none; } - button:enabled, - [role='button']:enabled { - cursor: pointer; + ::-webkit-datetime-edit, + ::-webkit-datetime-edit-year-field, + ::-webkit-datetime-edit-month-field, + ::-webkit-datetime-edit-day-field, + ::-webkit-datetime-edit-hour-field, + ::-webkit-datetime-edit-minute-field, + ::-webkit-datetime-edit-second-field, + ::-webkit-datetime-edit-millisecond-field, + ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; } - button:focus, - button:focus-visible { - outline: 1px dotted; - outline: 4px auto -webkit-focus-ring-color; + ::-webkit-calendar-picker-indicator { + line-height: 1; } - button:focus:not(:focus-visible) { - outline: none !important; + :-moz-ui-invalid { + box-shadow: none; } - input:focus, - textarea:focus, - select:focus { - outline: none; + button, + input:where([type='button'], [type='reset'], [type='submit']), + ::file-selector-button { + appearance: button; } - table { - border-collapse: collapse; + ::-webkit-inner-spin-button, + ::-webkit-outer-spin-button { + height: auto; } - input { - background-color: transparent; + [hidden]:where(:not([hidden='until-found'])) { + display: none !important; } - input:-ms-input-placeholder, - textarea:-ms-input-placeholder { - color: var(--vp-c-text-3); + /* #endregion */ + + /* #region VitePress Overrides */ + + /* https://web.dev/articles/prefers-reduced-motion#bonus_forcing_reduced_motion_on_all_websites */ + @media (prefers-reduced-motion: reduce) { + *, + ::before, + ::after { + animation-delay: -1ms !important; + animation-duration: 1ms !important; + animation-iteration-count: 1 !important; + background-attachment: initial !important; + scroll-behavior: auto !important; + transition-duration: 0s !important; + transition-delay: 0s !important; + } } - input::-ms-input-placeholder, - textarea::-ms-input-placeholder { - color: var(--vp-c-text-3); + :root { + /* FIXME: pins rem-sized bits while the theme is px-based - remove when + moving to relative units (#570) */ + font-size: 16px; + + &:is(.dark) { + color-scheme: dark; + } + &:not(.dark) { + color-scheme: light; + } } - input::placeholder, - textarea::placeholder { - color: var(--vp-c-text-3); + body { + /* FIXME: unlike html's unitless 1.5, this is inherited as-is by elements + with other font sizes - make unitless with #570 */ + line-height: 24px; + color: var(--vp-c-text-1); + background-color: var(--vp-c-bg); + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-autospace: normal; + text-spacing-trim: normal; } - input::-webkit-outer-spin-button, - input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; + pre, + code, + kbd, + samp { + text-autospace: no-autospace; } - input[type='number'] { - -moz-appearance: textfield; + button:enabled, + [role='button']:enabled { + cursor: pointer; } - textarea { - resize: vertical; + button:focus, + input:focus, + textarea:focus, + select:focus { + outline: none; } - select { - -webkit-appearance: none; + button:focus-visible { + outline: 1px dotted; + outline: 4px auto -webkit-focus-ring-color; } - fieldset { - margin: 0; - padding: 0; + input::placeholder, + textarea::placeholder { + color: var(--vp-c-text-3); } h1, @@ -241,22 +309,6 @@ overflow-wrap: break-word; } - sub, - sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; - } - - sub { - bottom: -0.25em; - } - - sup { - top: -0.5em; - } - vite-error-overlay { z-index: 9999; } @@ -269,4 +321,6 @@ display: inline-block; margin: auto; } + + /* #endregion */ } diff --git a/src/client/theme-default/styles/components/custom-block.css b/src/client/theme-default/styles/components/custom-block.css index a960381c..f3445a81 100644 --- a/src/client/theme-default/styles/components/custom-block.css +++ b/src/client/theme-default/styles/components/custom-block.css @@ -1,12 +1,17 @@ .custom-block { border: 1px solid transparent; border-radius: 8px; - padding: 16px 16px 8px; + padding: 8px 16px; line-height: 24px; font-size: var(--vp-custom-block-font-size); color: var(--vp-c-text-2); } +/* containers rendered with `{no-title}` keep the reduced padding */ +.custom-block:has(> .custom-block-title, > summary) { + padding-top: 16px; +} + .custom-block.info { border-color: var(--vp-custom-block-info-border); color: var(--vp-custom-block-info-text); diff --git a/src/client/theme-default/styles/components/vp-doc.css b/src/client/theme-default/styles/components/vp-doc.css index e8f5c83b..b512a731 100644 --- a/src/client/theme-default/styles/components/vp-doc.css +++ b/src/client/theme-default/styles/components/vp-doc.css @@ -1,3 +1,28 @@ +/** + * Anchors + * -------------------------------------------------------------------------- */ + +.vp-doc [id] { + scroll-margin-top: calc( + 47px + var(--vp-layout-top-height, 0px) + + var(--vp-extra-scroll-margin, 24px) + ); + + @media (min-width: 960px) { + scroll-margin-top: calc( + var(--vp-nav-height) + 47px + var(--vp-layout-top-height, 0px) + + var(--vp-extra-scroll-margin, 24px) + ); + } + + @media (min-width: 1280px) { + scroll-margin-top: calc( + var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + + var(--vp-extra-scroll-margin, 24px) + ); + } +} + /** * Headings * -------------------------------------------------------------------------- */ @@ -26,6 +51,8 @@ letter-spacing: -0.02em; line-height: 32px; font-size: 24px; + /* since it already has a top padding of 24px, we don't need extra scroll margin */ + --vp-extra-scroll-margin: 0px; } .vp-doc h3 { @@ -160,6 +187,37 @@ margin: 8px 0 0; } +.vp-doc li > p:first-child { + margin-top: 0; +} + +.vp-doc li > p:last-child { + margin-bottom: 0; +} + +.vp-doc li.task-list-item { + list-style: none; +} + +.vp-doc .task-list-item-checkbox { + margin: 0 4px 2px -1.25rem; + vertical-align: middle; + accent-color: var(--vp-c-brand-1); +} + +/** + * Footnotes + * -------------------------------------------------------------------------- */ + +.vp-doc .footnote-ref a, +.vp-doc .footnote-backref { + text-decoration: none; +} + +.vp-doc .footnotes { + font-size: 14px; +} + /** * Table * -------------------------------------------------------------------------- */ @@ -535,7 +593,7 @@ color: var(--vp-code-copy-code-active-text); background-color: var(--vp-code-copy-code-hover-bg); white-space: nowrap; - content: var(--vp-code-copy-copied-text-content); + content: attr(data-copied); } .vp-doc [class*='language-'] > span.lang { @@ -591,26 +649,32 @@ /* prettier-ignore */ :is(.vp-external-link-icon, .vp-doc a[href*='://'], .vp-doc a[target='_blank']):not(:is(.no-icon, svg a, :has(img, svg)))::after { - display: inline-block; - margin-top: -1px; + display: inline; margin-left: 4px; - width: 11px; - height: 11px; + padding-left: 11px; background: currentColor; color: var(--vp-c-text-3); flex-shrink: 0; --icon: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M0 0h24v24H0V0z' fill='none' /%3E%3Cpath d='M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z' /%3E%3C/svg%3E"); -webkit-mask-image: var(--icon); mask-image: var(--icon); + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 11px 11px; + mask-size: 11px 11px; /*rtl:raw:transform: scaleX(-1);*/ + vertical-align: bottom; + font-size: 10px; } .vp-external-link-icon::after { - content: ''; + content: '\2060'; } /* prettier-ignore */ .external-link-icon-enabled :is(.vp-doc a[href*='://'], .vp-doc a[target='_blank']):not(:is(.no-icon, svg a, :has(img, svg)))::after { - content: ''; + content: '\2060'; color: currentColor; } diff --git a/src/client/theme-default/styles/components/vp-sponsor.css b/src/client/theme-default/styles/components/vp-sponsor.css index 9e677ab9..79de6b73 100644 --- a/src/client/theme-default/styles/components/vp-sponsor.css +++ b/src/client/theme-default/styles/components/vp-sponsor.css @@ -153,3 +153,11 @@ .dark .vp-sponsor-grid-image { filter: grayscale(1) invert(1); } + +/* Reset styles from vp-doc if used in markdown */ +.vp-doc .vp-sponsor-grid, +.vp-doc .vp-sponsor-grid-item { + list-style: none; + margin: 0; + padding: 0; +} diff --git a/src/client/theme-default/styles/fonts.css b/src/client/theme-default/styles/fonts.css index 03b72458..ed6d01df 100644 --- a/src/client/theme-default/styles/fonts.css +++ b/src/client/theme-default/styles/fonts.css @@ -1,15 +1,94 @@ /* webfont-marker-begin */ @import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'); + +:root { + --vp-font-family-base: + 'Inter', -apple-system, BlinkMacSystemFont, sans-serif, 'Apple Color Emoji', + 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; +} /* webfont-marker-end */ +/* Generated by scripts/subsetFonts.py from scripts/fontSubsets.json */ + +@font-face { + font-family: Inter; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('../fonts/inter-italic-cyrillic-ext.woff2') format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} + +@font-face { + font-family: Inter; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('../fonts/inter-italic-cyrillic.woff2') format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} + +@font-face { + font-family: Inter; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('../fonts/inter-italic-greek-ext.woff2') format('woff2'); + unicode-range: U+1F00-1FFF; +} + +@font-face { + font-family: Inter; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('../fonts/inter-italic-greek.woff2') format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} + +@font-face { + font-family: Inter; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('../fonts/inter-italic-vietnamese.woff2') format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} + +@font-face { + font-family: Inter; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('../fonts/inter-italic-latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: Inter; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('../fonts/inter-italic-latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+21A9, U+2212, U+2215, U+2318, U+FEFF, U+FFFD; +} + +@font-face { + font-family: Inter; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('../fonts/inter-italic-symbols.woff2') format('woff2'); + unicode-range: U+02CD, U+02D8-02D9, U+02DB, U+0302, U+0306-0307, U+030A, U+030C, U+030F, U+0313, U+0315, U+031B, U+0326-0328, U+032C, U+0337-0338, U+0342-0343, U+0346-036F, U+0E3F, U+1DC0-1DF5, U+1DFC-1DFF, U+2070-2071, U+2074-208E, U+2090-209C, U+20DB-20DE, U+20E8, U+20F0, U+2100-2101, U+2103, U+2105-2106, U+2109, U+2117, U+211E-2121, U+2126, U+212A-212B, U+212E, U+2132, U+213B, U+214D, U+2150-217F, U+2183-2186, U+2189, U+2190, U+2192, U+2194-2199, U+21AA, U+21B0-21B1, U+21B3-21B5, U+21BA-21BB, U+21D0, U+21D2, U+21D4, U+21DE-21DF, U+21E4-21E5, U+21E7, U+21EA, U+2202, U+2205-2206, U+220F, U+2211, U+221A, U+221E, U+222B, U+2236, U+2248, U+2260, U+2264-2265, U+2295-2298, U+2303-2305, U+2325-2327, U+232B, U+2380, U+2387, U+238B, U+23CE-23CF, U+2423, U+2460-2468, U+24B6-24CF, U+24EA, U+25A0-25A2, U+25AA, U+25B2-25B3, U+25B6-25B7, U+25BA-25BD, U+25C0-25C1, U+25C4-25C7, U+25CA-25CB, U+25CF, U+25E6, U+25EF, U+2600, U+2605-2606, U+263C, U+2661, U+2665, U+266A-266B, U+26A0, U+2713, U+2717, U+2756, U+2764, U+2780-2788, U+27EF, U+27F5-27FA, U+2913, U+2A38, U+2B06, U+2B12-2B13, U+2B1C, U+2B24, U+2E18, U+A92E, U+1F12F-1F149, U+1F16A-1F16B, U+1F850, U+1F852; +} + @font-face { font-family: Inter; font-style: normal; font-weight: 100 900; font-display: swap; src: url('../fonts/inter-roman-cyrillic-ext.woff2') format('woff2'); - unicode-range: - U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; } @font-face { @@ -36,8 +115,7 @@ font-weight: 100 900; font-display: swap; src: url('../fonts/inter-roman-greek.woff2') format('woff2'); - unicode-range: - U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; } @font-face { @@ -46,10 +124,7 @@ font-weight: 100 900; font-display: swap; src: url('../fonts/inter-roman-vietnamese.woff2') format('woff2'); - unicode-range: - U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, - U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, - U+1EA0-1EF9, U+20AB; + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; } @font-face { @@ -58,10 +133,7 @@ font-weight: 100 900; font-display: swap; src: url('../fonts/inter-roman-latin-ext.woff2') format('woff2'); - unicode-range: - U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, - U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, - U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; } @font-face { @@ -70,24 +142,32 @@ font-weight: 100 900; font-display: swap; src: url('../fonts/inter-roman-latin.woff2') format('woff2'); - unicode-range: - U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, - U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, - U+2215, U+FEFF, U+FFFD; + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+21A9, U+2212, U+2215, U+2318, U+FEFF, U+FFFD; } @font-face { font-family: Inter; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('../fonts/inter-roman-symbols.woff2') format('woff2'); + unicode-range: U+02CD, U+02D8-02D9, U+02DB, U+0302, U+0306-0307, U+030A, U+030C, U+030F, U+0313, U+0315, U+031B, U+0326-0328, U+032C, U+0337-0338, U+0342-0343, U+0346-036F, U+0E3F, U+1DC0-1DF5, U+1DFC-1DFF, U+2070-2071, U+2074-208E, U+2090-209C, U+20DB-20DE, U+20E8, U+20F0, U+2100-2101, U+2103, U+2105-2106, U+2109, U+2117, U+211E-2121, U+2126, U+212A-212B, U+212E, U+2132, U+213B, U+214D, U+2150-217F, U+2183-2186, U+2189, U+2190, U+2192, U+2194-2199, U+21AA, U+21B0-21B1, U+21B3-21B5, U+21BA-21BB, U+21D0, U+21D2, U+21D4, U+21DE-21DF, U+21E4-21E5, U+21E7, U+21EA, U+2202, U+2205-2206, U+220F, U+2211, U+221A, U+221E, U+222B, U+2236, U+2248, U+2260, U+2264-2265, U+2295-2298, U+2303-2305, U+2325-2327, U+232B, U+2380, U+2387, U+238B, U+23CE-23CF, U+2423, U+2460-2468, U+24B6-24CF, U+24EA, U+25A0-25A2, U+25AA, U+25B2-25B3, U+25B6-25B7, U+25BA-25BD, U+25C0-25C1, U+25C4-25C7, U+25CA-25CB, U+25CF, U+25E6, U+25EF, U+2600, U+2605-2606, U+263C, U+2661, U+2665, U+266A-266B, U+26A0, U+2713, U+2717, U+2756, U+2764, U+2780-2788, U+27EF, U+27F5-27FA, U+2913, U+2A38, U+2B06, U+2B12-2B13, U+2B1C, U+2B24, U+2E18, U+A92E, U+1F12F-1F149, U+1F16A-1F16B, U+1F850, U+1F852; +} + +/* 'Inter Core' reuses the files above, but leaves out characters that should + be rendered by CJK fonts in CJK documents - see scripts/subsetFonts.py */ + +@font-face { + font-family: 'Inter Core'; font-style: italic; font-weight: 100 900; font-display: swap; src: url('../fonts/inter-italic-cyrillic-ext.woff2') format('woff2'); - unicode-range: - U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; } @font-face { - font-family: Inter; + font-family: 'Inter Core'; font-style: italic; font-weight: 100 900; font-display: swap; @@ -96,7 +176,7 @@ } @font-face { - font-family: Inter; + font-family: 'Inter Core'; font-style: italic; font-weight: 100 900; font-display: swap; @@ -105,79 +185,61 @@ } @font-face { - font-family: Inter; + font-family: 'Inter Core'; font-style: italic; font-weight: 100 900; font-display: swap; src: url('../fonts/inter-italic-greek.woff2') format('woff2'); - unicode-range: - U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; } @font-face { - font-family: Inter; + font-family: 'Inter Core'; font-style: italic; font-weight: 100 900; font-display: swap; src: url('../fonts/inter-italic-vietnamese.woff2') format('woff2'); - unicode-range: - U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, - U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, - U+1EA0-1EF9, U+20AB; + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; } @font-face { - font-family: Inter; + font-family: 'Inter Core'; font-style: italic; font-weight: 100 900; font-display: swap; src: url('../fonts/inter-italic-latin-ext.woff2') format('woff2'); - unicode-range: - U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, - U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, - U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; + unicode-range: U+0100-02BA, U+02BD-02C3, U+02C5, U+02C8, U+02CC, U+02CE-02CF, U+02D1-02D7, U+02DE, U+02E0-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; } @font-face { - font-family: Inter; + font-family: 'Inter Core'; font-style: italic; font-weight: 100 900; font-display: swap; src: url('../fonts/inter-italic-latin.woff2') format('woff2'); - unicode-range: - U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, - U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, - U+2215, U+FEFF, U+FFFD; + unicode-range: U+0000-007D, U+007F-00B6, U+00B8-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-2012, U+2016-2017, U+201A-201B, U+201E-2025, U+2027-203A, U+203C-206F, U+20AC, U+2122, U+2191, U+2193, U+21A9, U+2212, U+2215, U+2318, U+FEFF, U+FFFD; } -/* -Some code points in Inter are not compatible with CJK languages. - -https://www.unicode.org/L2/L2014/14006-sv-western-vs-cjk.pdf -https://www.unicode.org/L2/L2018/18013-svs-proposal.pdf -https://www.unicode.org/L2/L2018/18073-svs-proposal.pdf -https://www.unicode.org/L2/L2023/23212r-quotes-svs-proposal.pdf - -https://github.com/w3c/clreq/blob/f837ffe4a5501e72c17bd02961ce1b440916624f/local.css#L4 -& U+2015 (similar to U+2014 in Japanese) - -All CJK languages: U+00B7, U+2013, U+2014, U+2015, U+2018, U+2019, U+201C, U+201D, U+2026, U+2E3A -zh's request: U+007E (Both are fine in ja because unused there) -zh's request but decision postponed: U+002F (Both are fine in ja; include it into Inter for the time being) -*/ +@font-face { + font-family: 'Inter Core'; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('../fonts/inter-italic-symbols.woff2') format('woff2'); + unicode-range: U+0302, U+0306-0307, U+030A, U+030C, U+030F, U+0313, U+0315, U+031B, U+0326-0328, U+032C, U+0337-0338, U+0342-0343, U+0346-036F, U+0E3F, U+1DC0-1DF5, U+1DFC-1DFF, U+2070-2071, U+2075-207E, U+2080, U+2085-208E, U+2090-209C, U+20DB-20DE, U+20E8, U+20F0, U+2100-2101, U+2106, U+2117, U+211E-2120, U+212A, U+212E, U+2132, U+213B, U+214D, U+2150-2152, U+2155-215A, U+215F, U+216C-216F, U+217A-217F, U+2183-2186, U+21AA, U+21B0-21B1, U+21B3-21B5, U+21BA-21BB, U+21D0, U+21DE-21DF, U+21E4-21E5, U+21EA, U+2205-2206, U+2296-2298, U+2303-2305, U+2325-2327, U+232B, U+2380, U+2387, U+238B, U+23CE-23CF, U+2423, U+24EA, U+25A2, U+25AA, U+25BA-25BB, U+25C4-25C5, U+25CA, U+25E6, U+2600, U+263C, U+266B, U+26A0, U+2713, U+2717, U+2756, U+2764, U+2780-2788, U+27EF, U+27F5-27FA, U+2913, U+2A38, U+2B06, U+2B12-2B13, U+2B24, U+2E18, U+A92E, U+1F12F, U+1F16A-1F16B, U+1F850, U+1F852; +} @font-face { - font-family: Inter4CJK; + font-family: 'Inter Core'; font-style: normal; font-weight: 100 900; font-display: swap; src: url('../fonts/inter-roman-cyrillic-ext.woff2') format('woff2'); - unicode-range: - U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; } @font-face { - font-family: Inter4CJK; + font-family: 'Inter Core'; font-style: normal; font-weight: 100 900; font-display: swap; @@ -186,7 +248,7 @@ zh's request but decision postponed: U+002F (Both are fine in ja; include it int } @font-face { - font-family: Inter4CJK; + font-family: 'Inter Core'; font-style: normal; font-weight: 100 900; font-display: swap; @@ -195,125 +257,73 @@ zh's request but decision postponed: U+002F (Both are fine in ja; include it int } @font-face { - font-family: Inter4CJK; + font-family: 'Inter Core'; font-style: normal; font-weight: 100 900; font-display: swap; src: url('../fonts/inter-roman-greek.woff2') format('woff2'); - unicode-range: - U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; } @font-face { - font-family: Inter4CJK; + font-family: 'Inter Core'; font-style: normal; font-weight: 100 900; font-display: swap; src: url('../fonts/inter-roman-vietnamese.woff2') format('woff2'); - unicode-range: - U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, - U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, - U+1EA0-1EF9, U+20AB; + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; } @font-face { - font-family: Inter4CJK; + font-family: 'Inter Core'; font-style: normal; font-weight: 100 900; font-display: swap; src: url('../fonts/inter-roman-latin-ext.woff2') format('woff2'); - unicode-range: - U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, - U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; + unicode-range: U+0100-02BA, U+02BD-02C3, U+02C5, U+02C8, U+02CC, U+02CE-02CF, U+02D1-02D7, U+02DE, U+02E0-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; } -/* Exclude some ambiguous CJK characters. */ @font-face { - font-family: Inter4CJK; + font-family: 'Inter Core'; font-style: normal; font-weight: 100 900; font-display: swap; src: url('../fonts/inter-roman-latin.woff2') format('woff2'); - unicode-range: - U+0000-007D, U+007F-00B6, U+00B8-00FF, U+0131, U+0152-0153, U+02BB-02BC, - U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-2012, U+2016-2017, - U+201A-201B, U+201E-2025, U+2027-206F, U+2074, U+20AC, U+2122, U+2191, - U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} - -@font-face { - font-family: Inter4CJK; - font-style: italic; - font-weight: 100 900; - font-display: swap; - src: url('../fonts/inter-italic-cyrillic-ext.woff2') format('woff2'); - unicode-range: - U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; -} - -@font-face { - font-family: Inter4CJK; - font-style: italic; - font-weight: 100 900; - font-display: swap; - src: url('../fonts/inter-italic-cyrillic.woff2') format('woff2'); - unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; + unicode-range: U+0000-007D, U+007F-00B6, U+00B8-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-2012, U+2016-2017, U+201A-201B, U+201E-2025, U+2027-203A, U+203C-206F, U+20AC, U+2122, U+2191, U+2193, U+21A9, U+2212, U+2215, U+2318, U+FEFF, U+FFFD; } @font-face { - font-family: Inter4CJK; - font-style: italic; + font-family: 'Inter Core'; + font-style: normal; font-weight: 100 900; font-display: swap; - src: url('../fonts/inter-italic-greek-ext.woff2') format('woff2'); - unicode-range: U+1F00-1FFF; + src: url('../fonts/inter-roman-symbols.woff2') format('woff2'); + unicode-range: U+0302, U+0306-0307, U+030A, U+030C, U+030F, U+0313, U+0315, U+031B, U+0326-0328, U+032C, U+0337-0338, U+0342-0343, U+0346-036F, U+0E3F, U+1DC0-1DF5, U+1DFC-1DFF, U+2070-2071, U+2075-207E, U+2080, U+2085-208E, U+2090-209C, U+20DB-20DE, U+20E8, U+20F0, U+2100-2101, U+2106, U+2117, U+211E-2120, U+212A, U+212E, U+2132, U+213B, U+214D, U+2150-2152, U+2155-215A, U+215F, U+216C-216F, U+217A-217F, U+2183-2186, U+21AA, U+21B0-21B1, U+21B3-21B5, U+21BA-21BB, U+21D0, U+21DE-21DF, U+21E4-21E5, U+21EA, U+2205-2206, U+2296-2298, U+2303-2305, U+2325-2327, U+232B, U+2380, U+2387, U+238B, U+23CE-23CF, U+2423, U+24EA, U+25A2, U+25AA, U+25BA-25BB, U+25C4-25C5, U+25CA, U+25E6, U+2600, U+263C, U+266B, U+26A0, U+2713, U+2717, U+2756, U+2764, U+2780-2788, U+27EF, U+27F5-27FA, U+2913, U+2A38, U+2B06, U+2B12-2B13, U+2B24, U+2E18, U+A92E, U+1F12F, U+1F16A-1F16B, U+1F850, U+1F852; } -@font-face { - font-family: Inter4CJK; - font-style: italic; - font-weight: 100 900; - font-display: swap; - src: url('../fonts/inter-italic-greek.woff2') format('woff2'); - unicode-range: - U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +:root { + --vp-font-family-base: + 'Inter', -apple-system, BlinkMacSystemFont, sans-serif, 'Apple Color Emoji', + 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; } -@font-face { - font-family: Inter4CJK; - font-style: italic; - font-weight: 100 900; - font-display: swap; - src: url('../fonts/inter-italic-vietnamese.woff2') format('woff2'); - unicode-range: - U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, - U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, - U+1EA0-1EF9, U+20AB; +[lang]:where(:lang(zh)) { + --vp-font-family-base: + 'Inter Core', 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', + -apple-system, BlinkMacSystemFont, sans-serif, 'Apple Color Emoji', + 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; } -@font-face { - font-family: Inter4CJK; - font-style: italic; - font-weight: 100 900; - font-display: swap; - src: url('../fonts/inter-italic-latin-ext.woff2') format('woff2'); - unicode-range: - U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, - U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +[lang]:where(:lang(ja)) { + --vp-font-family-base: + 'Inter Core', 'Hiragino Sans', 'Meiryo', 'Yu Gothic', 'Noto Sans CJK JP', + -apple-system, BlinkMacSystemFont, sans-serif, 'Apple Color Emoji', + 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; } -/* Exclude some ambiguous CJK characters. */ -@font-face { - font-family: Inter4CJK; - font-style: italic; - font-weight: 100 900; - font-display: swap; - src: url('../fonts/inter-italic-latin.woff2') format('woff2'); - unicode-range: - U+0000-007D, U+007F-00B6, U+00B8-00FF, U+0131, U+0152-0153, U+02BB-02BC, - U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-2012, U+2016-2017, - U+201A-201B, U+201E-2025, U+2027-206F, U+2074, U+20AC, U+2122, U+2191, - U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +[lang]:where(:lang(ko)) { + --vp-font-family-base: + 'Inter Core', 'Apple SD Gothic Neo', 'Malgun Gothic', 'Noto Sans CJK KR', + -apple-system, BlinkMacSystemFont, sans-serif, 'Apple Color Emoji', + 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; } - -/* Generate the subsetted fonts using: `pyftsubset .woff2 --unicodes="" --layout-features+=pnum,tnum --flavor=woff2 --output-file="inter-