pull/477/head
Georges Gomes 4 years ago
commit 280667ef40

3
.gitignore vendored

@ -7,4 +7,5 @@
dist
node_modules
TODOs.md
.vscode
.vscode
.idea

@ -1,3 +1,37 @@
## [0.21.3](https://github.com/vuejs/vitepress/compare/v0.21.2...v0.21.3) (2022-01-06)
### Bug Fixes
- prioritize vue installed in user project root ([9b3243b](https://github.com/vuejs/vitepress/commit/9b3243b75752209943af5b247f5d38e641d4ff6d))
## [0.21.2](https://github.com/vuejs/vitepress/compare/v0.21.1...v0.21.2) (2022-01-06)
## [0.21.1](https://github.com/vuejs/vitepress/compare/v0.21.0...v0.21.1) (2022-01-06)
### Performance Improvements
- do not include head config in client bundle for production ([6f3a96f](https://github.com/vuejs/vitepress/commit/6f3a96f06daec4baad4420b54137a7afb1512e7f))
# [0.21.0](https://github.com/vuejs/vitepress/compare/v0.20.10...v0.21.0) (2022-01-06)
### Bug Fixes
- Chinese file link build failed ([#425](https://github.com/vuejs/vitepress/issues/425)) ([ae029ae](https://github.com/vuejs/vitepress/commit/ae029ae9e17fa6df1d2f89043f1891271e9c5b9b)), closes [#424](https://github.com/vuejs/vitepress/issues/424)
- initial render of 404 pages ([#418](https://github.com/vuejs/vitepress/issues/418)) ([a3bf52f](https://github.com/vuejs/vitepress/commit/a3bf52fed53e82b9756c844f6bdd576662d2e726))
- remove `.` for mjs in `supportedConfigExtensions` ([#447](https://github.com/vuejs/vitepress/issues/447)) ([fb6a4ad](https://github.com/vuejs/vitepress/commit/fb6a4ad3e008af9ce4393fb3ca37645f4efba951))
- **serve:** respect base config in serve mode ([#470](https://github.com/vuejs/vitepress/issues/470)) ([08a0b12](https://github.com/vuejs/vitepress/commit/08a0b129928cef44e613ff410d769a7ac7bf5fa3)), closes [#416](https://github.com/vuejs/vitepress/issues/416)
- set tempDir outside package root ([#439](https://github.com/vuejs/vitepress/issues/439)) ([bd35451](https://github.com/vuejs/vitepress/commit/bd35451ed42d7b5c47e2b49a7e659807cd7d7a0c)), closes [#435](https://github.com/vuejs/vitepress/issues/435)
- use algolia search lang ([#459](https://github.com/vuejs/vitepress/issues/459)) ([444562c](https://github.com/vuejs/vitepress/commit/444562c3a763bab7a9c0ebfca5eec635e142a61f))
### Features
- add details custom container ([#455](https://github.com/vuejs/vitepress/issues/455)) ([a8f147f](https://github.com/vuejs/vitepress/commit/a8f147f153efdd17989a02eb620c3ae9ab0d13dd))
- catch localhost links as dead links ([7387649](https://github.com/vuejs/vitepress/commit/7387649ff7c621402e49e26493b4eed25006fb4b))
- expose `__path` and `__relativePath` on md instance for md plugins ([4cec660](https://github.com/vuejs/vitepress/commit/4cec660401d8d01830e5a11b9c66bc0ac5a935db))
- improve typescript support for config file, add `defineConfigWithTheme` ([#465](https://github.com/vuejs/vitepress/issues/465)) ([ba41bb9](https://github.com/vuejs/vitepress/commit/ba41bb90551c01b9f84de2d2d3bc1920ce2ebe93))
- properly remove `{#custom-anchor}` syntax in headers ([6120da2](https://github.com/vuejs/vitepress/commit/6120da25a87f6bec3918be804e95f2b3c8afb6c8))
- user configurable `outDir` ([#448](https://github.com/vuejs/vitepress/issues/448)) ([5b04bb9](https://github.com/vuejs/vitepress/commit/5b04bb9eb5ced720414f4b0d729fde36432dd451))
## [0.20.10](https://github.com/vuejs/vitepress/compare/v0.20.9...v0.20.10) (2021-12-25)
### Features

@ -1,5 +1,6 @@
import { defineConfig } from '../../src/node'
export default {
export default defineConfig({
lang: 'en-US',
title: 'VitePress',
description: 'Vite & Vue powered static site generator.',
@ -51,7 +52,7 @@ export default {
'/': getGuideSidebar()
}
}
}
})
function getGuideSidebar() {
return [

@ -47,41 +47,4 @@ module.exports = {
}
```
VitePress will automatically add a `language` _facetFilter_ to the `searchParameters.facetFilter` array with the correct language value. **Make sure to properly configure your DocSearch config as well** by adding `language` as a _custom attribute for faceting_ and by setting it based on the `lang` attribute of the `<html>` element. Here is a short example of DocSearch config:
```json
{
"index_name": "<the name of your library>",
"start_urls": [
{
"url": "<your deployed url>"
}
],
"stop_urls": ["(?:(?<!\\.html)(?<!/))$"],
"selectors": {
"lvl0": {
"selector": ".sidebar > .sidebar-links > .sidebar-link .sidebar-link-item.active",
"global": true,
"default_value": "Documentation"
},
"lvl1": ".content h1",
"lvl2": ".content h2",
"lvl3": ".content h3",
"lvl4": ".content h4",
"lvl5": ".content h5",
"lvl6": ".content p, .content li",
"text": ".content [class^=language-]",
"language": {
"selector": "/html/@lang",
"type": "xpath",
"global": true,
"default_value": "en-US"
}
},
"custom_settings": {
"attributesForFaceting": ["language"]
}
}
```
You can take a look at the [DocSearch config used by Vue Router](https://github.com/algolia/docsearch-configs/blob/master/configs/next_router_vuejs.json) for a complete example.
VitePress will automatically add a `lang` _facetFilter_ to the `searchParameters.facetFilter` array with the correct language value. Algolia automatically adds the correct facet filter based on the `lang` attribute on the `<html>` tag. This will match search results with the currently viewed language of the page.

@ -1,5 +1,7 @@
# Configuration
## Overview
Without any configuration, the page is pretty minimal, and the user has no way to navigate around the site. To customize your site, lets first create a `.vitepress` directory inside your docs directory. This is where all VitePress-specific files will be placed. Your project structure is probably like this:
```bash
@ -21,3 +23,57 @@ module.exports = {
```
Check out the [Config Reference](/config/basics) for a full list of options.
## Config Intellisense
Since VitePress ships with TypeScript typings, you can leverage your IDE's intellisense with jsdoc type hints:
```js
/**
* @type {import('vitepress').UserConfig}
*/
const config = {
// ...
}
export default config
```
Alternatively, you can use the `defineConfig` helper at which should provide intellisense without the need for jsdoc annotations:
```js
import { defineConfig } from 'vitepress'
export default defineConfig({
// ...
})
```
VitePress also directly supports TS config files. You can use `.vitepress/config.ts` with the `defineConfig` helper as well.
## Typed Theme Config
By default, `defineConfig` helper leverages the theme config type from default theme:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
// Type is `DefaultTheme.Config`
}
})
```
If you use a custom theme and want type checks for the theme config, you'll need to use `defineConfigWithTheme` instead, and pass the config type for your custom theme via a generic argument:
```ts
import { defineConfigWithTheme } from 'vitepress'
import { ThemeConfig } from 'your-theme'
export default defineConfigWithTheme<ThemeConfig>({
themeConfig: {
// Type is `ThemeConfig`
}
})
```

@ -38,7 +38,7 @@ $ yarn docs:build
$ yarn docs:serve
```
The `serve` command will boot up local static web server that serves the files from `.vitepress/dist` at http://localhost:5000. It's an easy way to check if the production build looks OK in your local environment.
The `serve` command will boot up local static web server that serves the files from `.vitepress/dist` at `http://localhost:5000`. It's an easy way to check if the production build looks OK in your local environment.
You may configure the port of the server py passing `--port` flag as an argument.
@ -50,7 +50,7 @@ You may configure the port of the server py passing `--port` flag as an argument
}
```
Now the `docs:serve` method will launch the server at http://localhost:8080.
Now the `docs:serve` method will launch the server at `http://localhost:8080`.
## GitHub Pages
@ -136,7 +136,7 @@ deploy:
If you are deploying to `https://<USERNAME or GROUP>.gitlab.io/<REPO>/`, for example your repository is at `https://gitlab.com/<USERNAME>/<REPO>`, then set `base` to `'/<REPO>/'`.
2. Set `dest` in `.vitepress/config.js` to `public`.
2. Set `outDir` in `.vitepress/config.js` to `../public`.
3. Create a file called `.gitlab-ci.yml` in the root of your project with the content below. This will build and deploy your site whenever you make changes to your content:

@ -44,7 +44,7 @@ This section will help you build a basic VitePress documentation site from groun
$ yarn docs:dev
```
VitePress will start a hot-reloading development server at http://localhost:3000.
VitePress will start a hot-reloading development server at `http://localhost:3000`.
By now, you should have a basic but functional VitePress documentation site.

@ -138,6 +138,10 @@ This is a warning
::: danger
This is a dangerous warning
:::
::: details
This is a details block, which does not work in Internet Explorer or Edge.
:::
```
**Output**
@ -158,15 +162,25 @@ This is a warning
This is a dangerous warning
:::
::: details
This is a details block, which does not work in Internet Explorer or Edge.
:::
### Custom Title
**Input**
```md
````md
::: danger STOP
Danger zone, do not proceed
:::
::: details Click me to view the code
```js
console.log('Hello, VitePress!')
```
:::
````
**Output**
@ -174,6 +188,12 @@ Danger zone, do not proceed
Danger zone, do not proceed
:::
::: details Click me to view the code
```js
console.log('Hello, VitePress!')
```
:::
## Syntax Highlighting in Code Blocks
VitePress uses [Prism](https://prismjs.com/) to highlight language syntax in Markdown code blocks, using coloured text. Prism supports a wide variety of programming languages. All you need to do is append a valid language alias to the beginning backticks for the code block:

@ -1,6 +1,6 @@
{
"name": "vitepress",
"version": "0.20.10",
"version": "0.21.3",
"description": "Vite & Vue powered static site generator",
"main": "dist/node/index.js",
"typings": "types/index.d.ts",

@ -26,8 +26,8 @@ const getDefaultRoute = (): Route => ({
component: null,
// this will be set upon initial page load, which is before
// the app is mounted, so it's guaranteed to be available in
// components
data: null as any
// components. We just need enough data for 404 pages to render.
data: { frontmatter: {} } as any
})
interface PageModule {

@ -55,7 +55,7 @@ const { lang } = useData()
// if the user has multiple locales, the search results should be filtered
// based on the language
const facetFilters: string[] = props.multilang
? ['language:' + lang.value]
? ['lang:' + lang.value]
: []
if (props.options.searchParameters?.facetFilters) {
@ -66,10 +66,10 @@ watch(
lang,
(newLang, oldLang) => {
const index = facetFilters.findIndex(
(filter) => filter === 'language:' + oldLang
(filter) => filter === 'lang:' + oldLang
)
if (index > -1) {
facetFilters.splice(index, 1, 'language:' + newLang)
facetFilters.splice(index, 1, 'lang:' + newLang)
}
}
)

@ -24,6 +24,9 @@ const { site, theme, localePath } = useData()
font-size: 1.3rem;
font-weight: 600;
color: var(--c-text);
display: flex;
justify-content: center;
align-items: center;
}
.nav-bar-title:hover {

@ -22,7 +22,7 @@ export function useSideBar() {
return []
}
// if it's `atuo`, render headers of the current page
// if it's `auto`, render headers of the current page
if (frontSidebar === 'auto') {
return resolveAutoSidebar(headers, sidebarDepth)
}

@ -1,146 +1 @@
export namespace DefaultTheme {
export interface Config {
logo?: string
nav?: NavItem[] | false
sidebar?: SideBarConfig | MultiSideBarConfig
/**
* GitHub repository following the format <user>/<project>.
*
* @example `"vuejs/vue-next"`
*/
repo?: string
/**
* Customize the header label. Defaults to GitHub/Gitlab/Bitbucket
* depending on the provided repo.
*
* @example `"Contribute!"`
*/
repoLabel?: string
/**
* If your docs are in a different repository from your main project.
*
* @example `"vuejs/docs-next"`
*/
docsRepo?: string
/**
* If your docs are not at the root of the repo.
*
* @example `"docs"`
*/
docsDir?: string
/**
* If your docs are in a different branch. Defaults to `master`.
*
* @example `"next"`
*/
docsBranch?: string
/**
* Enable links to edit pages at the bottom of the page.
*/
editLinks?: boolean
/**
* Custom text for edit link. Defaults to "Edit this page".
*/
editLinkText?: string
/**
* Show last updated time at the bottom of the page. Defaults to `false`.
* If given a string, it will be displayed as a prefix (default value:
* "Last Updated").
*/
lastUpdated?: string | boolean
prevLinks?: boolean
nextLinks?: boolean
locales?: Record<string, LocaleConfig & Omit<Config, 'locales'>>
algolia?: AlgoliaSearchOptions
carbonAds?: {
carbon: string
custom?: string
placement: string
}
}
// navbar --------------------------------------------------------------------
export type NavItem = NavItemWithLink | NavItemWithChildren
export interface NavItemBase {
text: string
target?: string
rel?: string
ariaLabel?: string
activeMatch?: string
}
export interface NavItemWithLink extends NavItemBase {
link: string
}
export interface NavItemWithChildren extends NavItemBase {
items: NavItemWithLink[]
}
// sidebar -------------------------------------------------------------------
export type SideBarConfig = SideBarItem[] | 'auto' | false
export interface MultiSideBarConfig {
[path: string]: SideBarConfig
}
export type SideBarItem = SideBarLink | SideBarGroup
export interface SideBarLink {
text: string
link: string
}
export interface SideBarGroup {
text: string
link?: string
/**
* @default false
*/
collapsable?: boolean
children: SideBarItem[]
}
// algolia ------------------------------------------------------------------
// partially copied from @docsearch/react/dist/esm/DocSearch.d.ts
export interface AlgoliaSearchOptions {
appId?: string
apiKey: string
indexName: string
placeholder?: string
searchParameters?: any
disableUserPersonalization?: boolean
initialQuery?: string
}
// locales -------------------------------------------------------------------
export interface LocaleConfig {
/**
* Text for the language dropdown.
*/
selectText?: string
/**
* Label for this locale in the language dropdown.
*/
label?: string
}
}
export { DefaultTheme } from '../shared'

@ -8,6 +8,7 @@ import { Theme } from 'vitepress'
import Layout from './Layout.vue'
import NotFound from './NotFound.vue'
export { DefaultTheme } from './config'
const theme: Theme = {
Layout,
NotFound

@ -12,5 +12,5 @@
"vitepress": ["index.ts"]
}
},
"include": [".", "../../types/shared.d.ts"]
"include": ["."]
}

@ -14,13 +14,24 @@ export const DEFAULT_THEME_PATH = path.join(DIST_CLIENT_PATH, 'theme-default')
export const SITE_DATA_ID = '@siteData'
export const SITE_DATA_REQUEST_PATH = '/' + SITE_DATA_ID
export function resolveAliases(themeDir: string): AliasOptions {
const vueRuntimePath = 'vue/dist/vue.runtime.esm-bundler.js'
export function resolveAliases(root: string, themeDir: string): AliasOptions {
const paths: Record<string, string> = {
'/@theme': themeDir,
'/@shared': SHARED_PATH,
[SITE_DATA_ID]: SITE_DATA_REQUEST_PATH
}
// prioritize vue installed in project root and fallback to
// vue that comes with vitepress itself.
let vuePath
try {
vuePath = require.resolve(vueRuntimePath, { paths: [root] })
} catch (e) {
vuePath = require.resolve(vueRuntimePath)
}
const aliases: Alias[] = [
...Object.keys(paths).map((p) => ({
find: p,
@ -40,7 +51,7 @@ export function resolveAliases(themeDir: string): AliasOptions {
// vitepress itself
{
find: /^vue$/,
replacement: require.resolve('vue/dist/vue.runtime.esm-bundler.js')
replacement: vuePath
}
]

@ -21,8 +21,20 @@ export async function renderPage(
const routePath = `/${page.replace(/\.md$/, '')}`
const siteData = resolveSiteDataByRoute(config.site, routePath)
router.go(routePath)
// lazy require server-renderer for production build
const content = await require('vue/server-renderer').renderToString(app)
// prioritize project root over vitepress' own dep
let rendererPath
try {
rendererPath = require.resolve('vue/server-renderer', {
paths: [config.root]
})
} catch (e) {
rendererPath = require.resolve('vue/server-renderer')
}
// render page
const content = await require(rendererPath).renderToString(app)
const pageName = page.replace(/\//g, '_')
// server build doesn't need hash

@ -14,9 +14,10 @@ import {
SiteData,
HeadConfig,
LocaleConfig,
createLangDictionary
createLangDictionary,
DefaultTheme
} from './shared'
import { resolveAliases, APP_PATH, DEFAULT_THEME_PATH } from './alias'
import { resolveAliases, DEFAULT_THEME_PATH } from './alias'
import { MarkdownOptions } from './markdown/markdown'
import _debug from 'debug'
@ -27,7 +28,7 @@ const debug = _debug('vitepress:config')
export type { MarkdownOptions }
export interface UserConfig<ThemeConfig = any> {
extends?: RawConfigExports
extends?: RawConfigExports<ThemeConfig>
lang?: string
base?: string
title?: string
@ -37,7 +38,7 @@ export interface UserConfig<ThemeConfig = any> {
locales?: Record<string, LocaleConfig>
markdown?: MarkdownOptions
/**
* Opitons to pass on to `@vitejs/plugin-vue`
* Options to pass on to `@vitejs/plugin-vue`
*/
vue?: VuePluginOptions
/**
@ -47,6 +48,7 @@ export interface UserConfig<ThemeConfig = any> {
srcDir?: string
srcExclude?: string[]
outDir?: string
shouldPreload?: (link: string, page: string) => boolean
/**
@ -64,10 +66,10 @@ export interface UserConfig<ThemeConfig = any> {
cleanUrls?: boolean
}
export type RawConfigExports =
| UserConfig
| Promise<UserConfig>
| (() => UserConfig | Promise<UserConfig>)
export type RawConfigExports<ThemeConfig = any> =
| UserConfig<ThemeConfig>
| Promise<UserConfig<ThemeConfig>>
| (() => UserConfig<ThemeConfig> | Promise<UserConfig<ThemeConfig>>)
export interface SiteConfig<ThemeConfig = any>
extends Pick<
@ -92,7 +94,16 @@ const resolve = (root: string, file: string) =>
/**
* Type config helper
*/
export function defineConfig(config: RawConfigExports) {
export function defineConfig(config: UserConfig<DefaultTheme.Config>) {
return config
}
/**
* Type config helper for custom theme config
*/
export function defineConfigWithTheme<ThemeConfig>(
config: UserConfig<ThemeConfig>
) {
return config
}
@ -104,6 +115,9 @@ export async function resolveConfig(
const [userConfig, configPath] = await resolveUserConfig(root, command, mode)
const site = await resolveSiteData(root, userConfig)
const srcDir = path.resolve(root, userConfig.srcDir || '.')
const outDir = userConfig.outDir
? path.resolve(root, userConfig.outDir)
: resolve(root, 'dist')
// resolve theme path
const userThemeDir = resolve(root, 'theme')
@ -131,11 +145,11 @@ export async function resolveConfig(
themeDir,
pages,
configPath,
outDir: resolve(root, 'dist'),
tempDir: path.resolve(APP_PATH, 'temp'),
outDir,
tempDir: resolve(root, '.temp'),
cleanUrls: !!userConfig.cleanUrls,
markdown: userConfig.markdown,
alias: resolveAliases(themeDir),
alias: resolveAliases(root, themeDir),
vue: userConfig.vue,
vite: userConfig.vite,
shouldPreload: userConfig.shouldPreload,
@ -145,7 +159,7 @@ export async function resolveConfig(
return config
}
const supportedConfigExtensions = ['js', 'ts', '.mjs', 'mts']
const supportedConfigExtensions = ['js', 'ts', 'mjs', 'mts']
async function resolveUserConfig(
root: string,

@ -4,4 +4,11 @@ export * from './serve/serve'
export * from './config'
export * from './markdown/markdown'
export type { SiteData, HeadConfig, LocaleConfig } from '../../types/shared'
// shared types
export type {
SiteData,
HeadConfig,
Header,
LocaleConfig,
DefaultTheme
} from '../../types/shared'

@ -10,7 +10,7 @@ import { snippetPlugin } from './plugins/snippet'
import { hoistPlugin } from './plugins/hoist'
import { preWrapperPlugin } from './plugins/preWrapper'
import { linkPlugin } from './plugins/link'
import { extractHeaderPlugin } from './plugins/header'
import { headingPlugin } from './plugins/headings'
import { Header } from '../shared'
import anchor from 'markdown-it-anchor'
import attrs from 'markdown-it-attrs'
@ -40,9 +40,10 @@ export interface MarkdownParsedData {
headers?: Header[]
}
export interface MarkdownRenderer {
export interface MarkdownRenderer extends MarkdownIt {
__path: string
__relativePath: string
__data: MarkdownParsedData
render: (src: string, env?: any) => { html: string; data: any }
}
export type { Header }
@ -65,7 +66,7 @@ export const createMarkdownRenderer = (
.use(snippetPlugin, siteConfig.srcDir)
.use(hoistPlugin)
.use(containerPlugin)
.use(extractHeaderPlugin)
.use(headingPlugin)
.use(linkPlugin, {
target: '_blank',
rel: 'noopener noreferrer',
@ -95,17 +96,5 @@ export const createMarkdownRenderer = (
md.use(lineNumberPlugin)
}
// wrap render so that we can return both the html and extracted data.
const render = md.render
const wrappedRender: MarkdownRenderer['render'] = (src) => {
;(md as any).__data = {}
const html = render.call(md, src)
return {
html,
data: (md as any).__data
}
}
;(md as any).render = wrappedRender
return md as any
return md as MarkdownRenderer
}

@ -7,6 +7,7 @@ export const containerPlugin = (md: MarkdownIt) => {
.use(...createContainer('info', 'INFO'))
.use(...createContainer('warning', 'WARNING'))
.use(...createContainer('danger', 'WARNING'))
.use(...createContainer('details', 'Details'))
// explicitly escape Vue syntax
.use(container, 'v-pre', {
render: (tokens: Token[], idx: number) =>
@ -31,11 +32,14 @@ function createContainer(klass: string, defaultTitle: string): ContainerArgs {
const token = tokens[idx]
const info = token.info.trim().slice(klass.length).trim()
if (token.nesting === 1) {
if (klass === 'details') {
return `<details class="${klass} custom-block">${info ? `<summary>${info}</summary>` : ''}\n`
}
return `<div class="${klass} custom-block"><p class="custom-block-title">${
info || defaultTitle
}</p>\n`
} else {
return `</div>\n`
return klass === 'details' ? `</details>\n` : `</div>\n`
}
}
}

@ -1,16 +1,16 @@
import MarkdownIt from 'markdown-it'
import { MarkdownParsedData } from '../markdown'
import { MarkdownRenderer } from '../markdown'
import { deeplyParseHeader } from '../../utils/parseHeader'
import { slugify } from './slugify'
import MarkdownIt from 'markdown-it'
export const extractHeaderPlugin = (md: MarkdownIt, include = ['h2', 'h3']) => {
export const headingPlugin = (md: MarkdownIt, include = ['h2', 'h3']) => {
md.renderer.rules.heading_open = (tokens, i, options, env, self) => {
const token = tokens[i]
if (include.includes(token.tag)) {
const title = tokens[i + 1].content
const idAttr = token.attrs!.find(([name]) => name === 'id')
const slug = idAttr && idAttr[1]
const data = (md as any).__data as MarkdownParsedData
const data = (md as MarkdownRenderer).__data
const headers = data.headers || (data.headers = [])
headers.push({
level: parseInt(token.tag.slice(1), 10),

@ -1,5 +1,5 @@
import MarkdownIt from 'markdown-it'
import { MarkdownParsedData } from '../markdown'
import { MarkdownRenderer } from '../markdown'
// hoist <script> and <style> tags out of the returned html
// so that they can be placed outside as SFC blocks.
@ -8,7 +8,7 @@ export const hoistPlugin = (md: MarkdownIt) => {
md.renderer.rules.html_block = (tokens, idx) => {
const content = tokens[idx].content
const data = (md as any).__data as MarkdownParsedData
const data = (md as MarkdownRenderer).__data
const hoistedTags = data.hoistedTags || (data.hoistedTags = [])
if (RE.test(content.trim())) {
hoistedTags.push(content)

@ -3,7 +3,7 @@
// 2. normalize internal links to end with `.html`
import MarkdownIt from 'markdown-it'
import { MarkdownParsedData } from '../markdown'
import { MarkdownRenderer } from '../markdown'
import { URL } from 'url'
import { EXTERNAL_URL_RE } from '../../shared'
@ -25,6 +25,10 @@ export const linkPlugin = (
Object.entries(externalAttrs).forEach(([key, val]) => {
token.attrSet(key, val)
})
// catch localhost links as dead link
if (url.replace(EXTERNAL_URL_RE, '').startsWith('//localhost:')) {
pushLink(url)
}
} else if (
// internal anchor links
!url.startsWith('#') &&
@ -71,11 +75,15 @@ export const linkPlugin = (
}
// export it for existence check
const data = (md as any).__data as MarkdownParsedData
const links = data.links || (data.links = [])
links.push(url.replace(/\.html$/, ''))
pushLink(url.replace(/\.html$/, ''))
// markdown-it encodes the uri
hrefAttr[1] = decodeURI(url)
}
function pushLink(link: string) {
const data = (md as MarkdownRenderer).__data
const links = data.links || (data.links = [])
links.push(link)
}
}

@ -4,7 +4,7 @@ import matter from 'gray-matter'
import LRUCache from 'lru-cache'
import { createMarkdownRenderer, MarkdownOptions } from './markdown/markdown'
import { deeplyParseHeader } from './utils/parseHeader'
import { PageData, HeadConfig } from './shared'
import { PageData, HeadConfig, EXTERNAL_URL_RE } from './shared'
import { slash } from './utils/slash'
import chalk from 'chalk'
import _debug from 'debug'
@ -68,7 +68,14 @@ export function createMarkdownToVueRenderFn(
})
const { content, data: frontmatter } = matter(src)
let { html, data } = md.render(content)
// reset state before render
md.__path = file
md.__relativePath = relativePath
md.__data = {}
let html = md.render(content)
const data = md.__data
if (isBuild) {
// avoid env variables being replaced by vite
@ -86,29 +93,40 @@ export function createMarkdownToVueRenderFn(
}
// validate data.links
const deadLinks = []
const deadLinks: string[] = []
const recordDeadLink = (url: string) => {
console.warn(
chalk.yellow(
`\n(!) Found dead link ${chalk.cyan(url)} in file ${chalk.white.dim(
file
)}`
)
)
deadLinks.push(url)
}
if (data.links) {
const dir = path.dirname(file)
for (let url of data.links) {
if (url.replace(EXTERNAL_URL_RE, '').startsWith('//localhost:')) {
recordDeadLink(url)
continue
}
url = url.replace(/[?#].*$/, '').replace(/\.(html|md)$/, '')
if (url.endsWith('/')) url += `index`
const resolved = slash(
url.startsWith('/')
? url.slice(1)
: path.relative(srcDir, path.resolve(dir, url))
const resolved = decodeURIComponent(
slash(
url.startsWith('/')
? url.slice(1)
: path.relative(srcDir, path.resolve(dir, url))
)
)
if (
!pages.includes(resolved) &&
!fs.existsSync(path.resolve(dir, publicDir, `${resolved}.html`))
) {
console.warn(
chalk.yellow(
`\n(!) Found dead link ${chalk.cyan(
url
)} in file ${chalk.white.dim(file)}`
)
)
deadLinks.push(url)
recordDeadLink(url)
}
}
}
@ -117,7 +135,7 @@ export function createMarkdownToVueRenderFn(
title: inferTitle(frontmatter, content),
description: inferDescription(frontmatter),
frontmatter,
headers: data.headers,
headers: data.headers || [],
relativePath,
// TODO use git timestamp?
lastUpdated: Math.round(fs.statSync(file).mtimeMs)

@ -122,7 +122,12 @@ export function createVitePressPlugin(
load(id) {
if (id === SITE_DATA_REQUEST_PATH) {
return `export default ${JSON.stringify(JSON.stringify(siteData))}`
let data = siteData
// head info is not needed by the client in production build
if (config.command === 'build') {
data = { ...siteData, head: [] }
}
return `export default ${JSON.stringify(JSON.stringify(data))}`
}
},

@ -3,6 +3,18 @@ import compression from 'compression'
import { resolveConfig } from '../config'
import polka from 'polka'
function trimChar(str: string, char: string) {
while (str.charAt(0) === char) {
str = str.substring(1)
}
while (str.charAt(str.length - 1) === char) {
str = str.substring(0, str.length - 1)
}
return str
}
export interface ServeOptions {
root?: string
port?: number
@ -11,6 +23,7 @@ export interface ServeOptions {
export async function serve(options: ServeOptions = {}) {
const port = options.port !== undefined ? options.port : 5000
const site = await resolveConfig(options.root, 'serve', 'production')
const base = trimChar(site?.site?.base ?? "", "/")
const compress = compression()
const serve = sirv(site.outDir, {
@ -27,10 +40,19 @@ export async function serve(options: ServeOptions = {}) {
}
})
polka()
.use(compress, serve)
.listen(port, (err: any) => {
if (err) throw err
console.log(`Built site served at http://localhost:${port}/\n`)
})
if (base) {
polka()
.use(base, compress, serve)
.listen(port, (err: any) => {
if (err) throw err
console.log(`Built site served at http://localhost:${port}/${base}/\n`)
})
} else {
polka()
.use(compress, serve)
.listen(port, (err: any) => {
if (err) throw err
console.log(`Built site served at http://localhost:${port}/\n`)
})
}
}

@ -12,14 +12,14 @@
import emojiData from 'markdown-it-emoji/lib/data/full.json'
const parseEmojis = (str: string) => {
return String(str).replace(
return str.replace(
/:(.+?):/g,
(placeholder, key) => emojiData[key] || placeholder
)
}
const unescapeHtml = (html: string) =>
String(html)
html
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&#x3A;/g, ':')
@ -27,11 +27,14 @@ const unescapeHtml = (html: string) =>
.replace(/&gt;/g, '>')
const removeMarkdownTokens = (str: string) =>
String(str)
str
.replace(/(\[(.[^\]]+)\]\((.[^)]+)\))/g, '$2') // []()
.replace(/(`|\*{1,3}|_)(.*?[^\\])\1/g, '$2') // `{t}` | *{t}* | **{t}** | ***{t}*** | _{t}_
.replace(/(\\)(\*|_|`|\!|<|\$)/g, '$2') // remove escape char '\'
const remvoeCustomAnchor = (str: string) =>
str.replace(/\{#([a-z0-9\-_]+?)\}\s*$/, '') // {#custom-header}
const trim = (str: string) => str.trim()
// This method remove the raw HTML but reserve the HTML wrapped by `<code>`.
@ -54,6 +57,7 @@ const compose = (...processors: ((str: string) => string)[]) => {
export const parseHeader = compose(
unescapeHtml,
parseEmojis,
remvoeCustomAnchor,
removeMarkdownTokens,
trim
)

@ -5,7 +5,8 @@ export type {
PageData,
HeadConfig,
LocaleConfig,
Header
Header,
DefaultTheme,
} from '../../types/shared'
export const EXTERNAL_URL_RE = /^https?:/i

@ -3,5 +3,5 @@
"compilerOptions": {
"baseUrl": "."
},
"include": [".", "../../types/shared.d.ts"]
"include": ["."]
}

@ -0,0 +1,146 @@
export namespace DefaultTheme {
export interface Config {
logo?: string
nav?: NavItem[] | false
sidebar?: SideBarConfig | MultiSideBarConfig
/**
* GitHub repository following the format <user>/<project>.
*
* @example `"vuejs/vue-next"`
*/
repo?: string
/**
* Customize the header label. Defaults to GitHub/Gitlab/Bitbucket
* depending on the provided repo.
*
* @example `"Contribute!"`
*/
repoLabel?: string
/**
* If your docs are in a different repository from your main project.
*
* @example `"vuejs/docs-next"`
*/
docsRepo?: string
/**
* If your docs are not at the root of the repo.
*
* @example `"docs"`
*/
docsDir?: string
/**
* If your docs are in a different branch. Defaults to `master`.
*
* @example `"next"`
*/
docsBranch?: string
/**
* Enable links to edit pages at the bottom of the page.
*/
editLinks?: boolean
/**
* Custom text for edit link. Defaults to "Edit this page".
*/
editLinkText?: string
/**
* Show last updated time at the bottom of the page. Defaults to `false`.
* If given a string, it will be displayed as a prefix (default value:
* "Last Updated").
*/
lastUpdated?: string | boolean
prevLinks?: boolean
nextLinks?: boolean
locales?: Record<string, LocaleConfig & Omit<Config, 'locales'>>
algolia?: AlgoliaSearchOptions
carbonAds?: {
carbon: string
custom?: string
placement: string
}
}
// navbar --------------------------------------------------------------------
export type NavItem = NavItemWithLink | NavItemWithChildren
export interface NavItemBase {
text: string
target?: string
rel?: string
ariaLabel?: string
activeMatch?: string
}
export interface NavItemWithLink extends NavItemBase {
link: string
}
export interface NavItemWithChildren extends NavItemBase {
items: NavItemWithLink[]
}
// sidebar -------------------------------------------------------------------
export type SideBarConfig = SideBarItem[] | 'auto' | false
export interface MultiSideBarConfig {
[path: string]: SideBarConfig
}
export type SideBarItem = SideBarLink | SideBarGroup
export interface SideBarLink {
text: string
link: string
}
export interface SideBarGroup {
text: string
link?: string
/**
* @default false
*/
collapsable?: boolean
children: SideBarItem[]
}
// algolia ------------------------------------------------------------------
// partially copied from @docsearch/react/dist/esm/DocSearch.d.ts
export interface AlgoliaSearchOptions {
appId?: string
apiKey: string
indexName: string
placeholder?: string
searchParameters?: any
disableUserPersonalization?: boolean
initialQuery?: string
}
// locales -------------------------------------------------------------------
export interface LocaleConfig {
/**
* Text for the language dropdown.
*/
selectText?: string
/**
* Label for this locale in the language dropdown.
*/
label?: string
}
}

2
types/index.d.ts vendored

@ -1,4 +1,2 @@
export * from './shared'
export * from '../dist/node/index'
export * from '../dist/client/index'
export * from '../dist/client/theme-default/config'

34
types/shared.d.ts vendored

@ -1,12 +1,14 @@
// types shared between server and client
export interface LocaleConfig {
lang: string
title?: string
description?: string
head?: HeadConfig[]
label?: string
selectText?: string
export { DefaultTheme } from './default-theme'
export interface PageData {
relativePath: string
title: string
description: string
headers: Header[]
frontmatter: Record<string, any>
lastUpdated: number
}
export interface SiteData<ThemeConfig = any> {
@ -48,17 +50,17 @@ export type HeadConfig =
| [string, Record<string, string>]
| [string, Record<string, string>, string]
export interface PageData {
relativePath: string
title: string
description: string
headers: Header[]
frontmatter: Record<string, any>
lastUpdated: number
}
export interface Header {
level: number
title: string
slug: string
}
export interface LocaleConfig {
lang: string
title?: string
description?: string
head?: HeadConfig[]
label?: string
selectText?: string
}

Loading…
Cancel
Save