pull/5390/merge
我这一生 如绿豆冰 2 weeks ago committed by GitHub
commit 59b17c814e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -91,6 +91,10 @@ const sidebar: DefaultTheme.Config['sidebar'] = {
{ {
text: '& &#60;Test Page &> <code>code</code>', text: '& &#60;Test Page &> <code>code</code>',
link: '/text-literals/' link: '/text-literals/'
},
{
text: 'Markdown `<Label &>`',
link: '/theme-labels/'
} }
] ]
}, },

@ -19,6 +19,20 @@ describe('test multi sidebar sort root', () => {
'Team & Sponsors', 'Team & Sponsors',
'Sidebar Hash' 'Sidebar Hash'
]) ])
expect(await sidebarLocator.nth(1).innerHTML()).toBe(
'&amp; &lt;Text Literals &amp;&gt; <code>code</code>'
)
})
test('renders inline markdown in sidebar labels', async () => {
const markdownLabel = page.locator(
'.VPSidebarItem.level-1 a[href$="/theme-labels/"] .text'
)
expect(await markdownLabel.innerHTML()).toBe(
'Markdown <code>&lt;Label &amp;&gt;</code>'
)
}) })
}) })

@ -1,7 +1,99 @@
import type { MarkdownItAsync } from 'markdown-it-async' import type { MarkdownItAsync } from 'markdown-it-async'
import { mergeConfig, type UserConfig } from 'node/config' import { mergeConfig, resolveSiteData, type UserConfig } from 'node/config'
describe('node/config', () => { describe('node/config', () => {
test('renders inline markdown in default theme text fields', async () => {
const site = await resolveSiteData(process.cwd(), {
themeConfig: {
nav: [
{ text: 'Vue `<script setup>`', link: '/guide' },
{
text: '**Reference**',
items: [
{ text: 'API `<T>`', link: '/api' },
{
text: 'Nested `<Menu>`',
items: [{ text: '`Child`', link: '/child' }]
}
]
}
],
sidebar: [
{
text: 'Guide `<script setup>`',
docFooterText: 'Footer `<Foo>`',
items: [
{ text: '**Intro**', link: '/intro' },
{ text: '<code>Raw HTML</code>', link: '/html' }
]
}
],
docFooter: {
prev: 'Previous `<Page>`',
next: false
}
}
})
expect(site.themeConfig.nav[0].text).toBe(
'Vue <code>&lt;script setup&gt;</code>'
)
expect(site.themeConfig.nav[1].text).toBe('<strong>Reference</strong>')
expect(site.themeConfig.nav[1].items[0].text).toBe(
'API <code>&lt;T&gt;</code>'
)
expect(site.themeConfig.nav[1].items[1].text).toBe(
'Nested <code>&lt;Menu&gt;</code>'
)
expect(site.themeConfig.nav[1].items[1].items[0].text).toBe(
'<code>Child</code>'
)
expect(site.themeConfig.sidebar[0].text).toBe(
'Guide <code>&lt;script setup&gt;</code>'
)
expect(site.themeConfig.sidebar[0].docFooterText).toBe(
'Footer <code>&lt;Foo&gt;</code>'
)
expect(site.themeConfig.sidebar[0].items[0].text).toBe(
'<strong>Intro</strong>'
)
expect(site.themeConfig.sidebar[0].items[1].text).toBe(
'<code>Raw HTML</code>'
)
expect(site.themeConfig.docFooter.prev).toBe(
'Previous <code>&lt;Page&gt;</code>'
)
expect(site.themeConfig.docFooter.next).toBe(false)
})
test('renders inline markdown in locale and additional default theme configs', async () => {
const site = await resolveSiteData(process.cwd(), {
locales: {
zh: {
label: 'Chinese',
themeConfig: {
sidebar: [{ text: 'Locale `<script setup>`' }]
}
}
},
additionalConfig: {
'/guide/': {
themeConfig: {
nav: [{ text: 'Guide `<Item>`', link: '/guide/' }]
}
}
}
})
expect(site.locales.zh.themeConfig?.sidebar?.[0].text).toBe(
'Locale <code>&lt;script setup&gt;</code>'
)
expect(
typeof site.additionalConfig !== 'function' &&
site.additionalConfig?.['/guide/'].themeConfig?.nav?.[0].text
).toBe('Guide <code>&lt;Item&gt;</code>')
})
test('merges markdown hooks from extended configs', async () => { test('merges markdown hooks from extended configs', async () => {
const calls: string[] = [] const calls: string[] = []
const md = {} as MarkdownItAsync const md = {} as MarkdownItAsync

@ -88,6 +88,8 @@ export default {
The configuration for the nav menu item. More details in [Default Theme: Nav](./default-theme-nav#navigation-links). The configuration for the nav menu item. More details in [Default Theme: Nav](./default-theme-nav#navigation-links).
The `text` fields support inline Markdown and HTML.
```ts ```ts
export default { export default {
themeConfig: { themeConfig: {
@ -136,6 +138,8 @@ interface NavItemWithChildren {
The configuration for the sidebar menu item. More details in [Default Theme: Sidebar](./default-theme-sidebar). The configuration for the sidebar menu item. More details in [Default Theme: Sidebar](./default-theme-sidebar).
The `text` and `docFooterText` fields support inline Markdown and HTML.
```ts ```ts
export default { export default {
themeConfig: { themeConfig: {
@ -162,7 +166,7 @@ export interface SidebarMulti {
export type SidebarItem = { export type SidebarItem = {
/** /**
* The text label of the item. * The text label of the item. Supports inline Markdown and HTML.
*/ */
text?: string text?: string
@ -192,6 +196,7 @@ export type SidebarItem = {
/** /**
* Customize text that appears on the footer of previous/next page. * Customize text that appears on the footer of previous/next page.
* Supports inline Markdown and HTML.
*/ */
docFooterText?: string docFooterText?: string
@ -410,6 +415,8 @@ Learn more in [Default Theme: Carbon Ads](./default-theme-carbon-ads)
Can be used to customize text appearing above previous and next links. Helpful if not writing docs in English. Also can be used to disable prev/next links globally. If you want to selectively enable/disable prev/next links, you can use [frontmatter](./default-theme-prev-next-links). Can be used to customize text appearing above previous and next links. Helpful if not writing docs in English. Also can be used to disable prev/next links globally. If you want to selectively enable/disable prev/next links, you can use [frontmatter](./default-theme-prev-next-links).
The `prev` and `next` string values support inline Markdown and HTML.
```ts ```ts
export default { export default {
themeConfig: { themeConfig: {

@ -57,7 +57,15 @@ export default {
} }
``` ```
The `text` is the actual text displayed in nav, and the `link` is the link that will be navigated to when the text is clicked. For the link, set path to the actual file without `.md` prefix, and always start with `/`. The `text` is the actual text displayed in nav, and supports inline Markdown and HTML. The `link` is the link that will be navigated to when the text is clicked. For the link, set path to the actual file without `.md` prefix, and always start with `/`.
```js
export default {
themeConfig: {
nav: [{ text: 'Vue `<script setup>`', link: '/guide' }]
}
}
```
The `link` can also be a function that accepts [`PageData`](./runtime-api#usedata) as the argument and returns the path. The `link` can also be a function that accepts [`PageData`](./runtime-api#usedata) as the argument and returns the path.

@ -27,6 +27,21 @@ export default {
The simplest form of the sidebar menu is passing in a single array of links. The first level item defines the "section" for the sidebar. It should contain `text`, which is the title of the section, and `items` which are the actual navigation links. The simplest form of the sidebar menu is passing in a single array of links. The first level item defines the "section" for the sidebar. It should contain `text`, which is the title of the section, and `items` which are the actual navigation links.
The `text` fields in sidebar items support inline Markdown and HTML:
```js
export default {
themeConfig: {
sidebar: [
{
text: 'Vue `<script setup>`',
items: [{ text: 'API `<T>`', link: '/api' }]
}
]
}
}
```
```js ```js
export default { export default {
themeConfig: { themeConfig: {

@ -27,6 +27,11 @@ import {
type SiteData type SiteData
} from './shared' } from './shared'
import type { RawConfigExports, SiteConfig, UserConfig } from './siteConfig' import type { RawConfigExports, SiteConfig, UserConfig } from './siteConfig'
import {
resolveAdditionalDefaultThemeConfigs,
resolveDefaultThemeConfig,
resolveLocaleDefaultThemeConfigs
} from './themeConfig'
import { glob } from './utils/glob' import { glob } from './utils/glob'
export { resolvePages } from './plugins/dynamicRoutesPlugin' export { resolvePages } from './plugins/dynamicRoutesPlugin'
@ -360,6 +365,12 @@ export async function resolveSiteData(
): Promise<SiteData> { ): Promise<SiteData> {
userConfig = userConfig || (await resolveUserConfig(root, command, mode))[0] userConfig = userConfig || (await resolveUserConfig(root, command, mode))[0]
const themeConfig = resolveDefaultThemeConfig(userConfig.themeConfig || {})
const locales = resolveLocaleDefaultThemeConfigs(userConfig.locales) || {}
const additionalConfig = resolveAdditionalDefaultThemeConfigs(
userConfig.additionalConfig
)
return { return {
lang: userConfig.lang || 'en-US', lang: userConfig.lang || 'en-US',
dir: userConfig.dir || 'ltr', dir: userConfig.dir || 'ltr',
@ -372,11 +383,11 @@ export async function resolveSiteData(
prefetchLinks: userConfig.router?.prefetchLinks ?? true prefetchLinks: userConfig.router?.prefetchLinks ?? true
}, },
appearance: userConfig.appearance ?? true, appearance: userConfig.appearance ?? true,
themeConfig: userConfig.themeConfig || {}, themeConfig,
locales: userConfig.locales || {}, locales,
cleanUrls: !!userConfig.cleanUrls, cleanUrls: !!userConfig.cleanUrls,
contentProps: userConfig.contentProps, contentProps: userConfig.contentProps,
additionalConfig: userConfig.additionalConfig additionalConfig
} }
} }

@ -0,0 +1,172 @@
import MarkdownIt from 'markdown-it'
import type { UserConfig } from './siteConfig'
import { isObject } from './shared'
type AdditionalConfig = NonNullable<UserConfig['additionalConfig']>
type AdditionalConfigLoader = Extract<
AdditionalConfig,
(filePath: string) => unknown
>
const inlineMarkdown = new MarkdownIt({ html: true, linkify: true })
export function resolveDefaultThemeConfig<T>(themeConfig: T): T {
if (!isObject(themeConfig)) return themeConfig
const resolved = { ...themeConfig } as Record<string, any>
if (Array.isArray(resolved.nav)) {
resolved.nav = resolveNavItems(resolved.nav)
}
if (resolved.sidebar) {
resolved.sidebar = resolveSidebar(resolved.sidebar)
}
if (isObject(resolved.docFooter)) {
resolved.docFooter = resolveDocFooter(resolved.docFooter)
}
return resolved as T
}
export function resolveLocaleDefaultThemeConfigs<T>(
locales: T | undefined
): T | undefined {
if (!isObject(locales)) return locales
return Object.fromEntries(
Object.entries(locales).map(([key, locale]) => {
if (!isObject(locale) || !isObject(locale.themeConfig)) {
return [key, locale]
}
return [
key,
{
...locale,
themeConfig: resolveDefaultThemeConfig(locale.themeConfig)
}
]
})
) as T
}
export function resolveAdditionalDefaultThemeConfigs<
T extends AdditionalConfig
>(additionalConfig: T | undefined): T | undefined {
if (!additionalConfig) return additionalConfig
if (typeof additionalConfig === 'function') {
return ((filePath: string) => {
const configs = (additionalConfig as AdditionalConfigLoader)(filePath)
return configs?.map(resolveAdditionalConfig)
}) as T
}
return Object.fromEntries(
Object.entries(additionalConfig).map(([key, config]) => [
key,
resolveAdditionalConfig(config)
])
) as T
}
function resolveAdditionalConfig<T>(config: T): T {
if (!isObject(config) || !isObject(config.themeConfig)) {
return config
}
return {
...config,
themeConfig: resolveDefaultThemeConfig(config.themeConfig)
}
}
function resolveNavItems<T>(items: T[]): T[] {
return items.map(resolveNavItem)
}
function resolveNavItem<T>(item: T): T {
if (!isObject(item)) return item
const resolved = resolveText(item)
if (Array.isArray(item.items)) {
resolved.items = resolveNavItems(item.items)
}
return resolved as T
}
function resolveSidebar<T>(sidebar: T): T {
if (Array.isArray(sidebar)) {
return resolveSidebarItems(sidebar) as T
}
if (!isObject(sidebar)) return sidebar
return Object.fromEntries(
Object.entries(sidebar).map(([key, value]) => {
if (Array.isArray(value)) {
return [key, resolveSidebarItems(value)]
}
if (isObject(value) && Array.isArray(value.items)) {
return [key, { ...value, items: resolveSidebarItems(value.items) }]
}
return [key, value]
})
) as T
}
function resolveSidebarItems<T>(items: T[]): T[] {
return items.map(resolveSidebarItem)
}
function resolveSidebarItem<T>(item: T): T {
if (!isObject(item)) return item
const resolved = resolveText(item)
if (typeof item.docFooterText === 'string') {
resolved.docFooterText = renderInlineMarkdown(item.docFooterText)
}
if (Array.isArray(item.items)) {
resolved.items = resolveSidebarItems(item.items)
}
return resolved as T
}
function resolveDocFooter<T>(docFooter: T): T {
if (!isObject(docFooter)) return docFooter
const resolved = { ...docFooter } as Record<string, any>
if (typeof docFooter.prev === 'string') {
resolved.prev = renderInlineMarkdown(docFooter.prev)
}
if (typeof docFooter.next === 'string') {
resolved.next = renderInlineMarkdown(docFooter.next)
}
return resolved as T
}
function resolveText<T>(item: T): Record<string, any> {
const resolved = { ...item } as Record<string, any>
if (typeof (item as Record<string, any>).text === 'string') {
resolved.text = renderInlineMarkdown((item as Record<string, any>).text)
}
return resolved
}
function renderInlineMarkdown(text: string): string {
return inlineMarkdown.renderInline(text)
}

@ -218,6 +218,9 @@ export namespace DefaultTheme {
} }
export interface NavItemWithLink { export interface NavItemWithLink {
/**
* The text label of the item. Supports inline Markdown and HTML.
*/
text: string text: string
link: string | ((payload: PageData) => string) link: string | ((payload: PageData) => string)
items?: never items?: never
@ -233,11 +236,17 @@ export namespace DefaultTheme {
} }
export interface NavItemChildren { export interface NavItemChildren {
/**
* The text label of the item. Supports inline Markdown and HTML.
*/
text?: string text?: string
items: NavItemWithLink[] items: NavItemWithLink[]
} }
export interface NavItemWithChildren { export interface NavItemWithChildren {
/**
* The text label of the item. Supports inline Markdown and HTML.
*/
text?: string text?: string
items: (NavItemComponent | NavItemChildren | NavItemWithLink)[] items: (NavItemComponent | NavItemChildren | NavItemWithLink)[]
@ -283,7 +292,7 @@ export namespace DefaultTheme {
export type SidebarItem = { export type SidebarItem = {
/** /**
* The text label of the item. * The text label of the item. Supports inline Markdown and HTML.
*/ */
text?: string text?: string
@ -313,6 +322,7 @@ export namespace DefaultTheme {
/** /**
* Customize text that appears on the footer of previous/next page. * Customize text that appears on the footer of previous/next page.
* Supports inline Markdown and HTML.
*/ */
docFooterText?: string docFooterText?: string
@ -344,6 +354,7 @@ export namespace DefaultTheme {
export interface DocFooter { export interface DocFooter {
/** /**
* Custom label for previous page button. Can be set to `false` to disable. * Custom label for previous page button. Can be set to `false` to disable.
* Supports inline Markdown and HTML.
* *
* @default 'Previous page' * @default 'Previous page'
*/ */
@ -351,6 +362,7 @@ export namespace DefaultTheme {
/** /**
* Custom label for next page button. Can be set to `false` to disable. * Custom label for next page button. Can be set to `false` to disable.
* Supports inline Markdown and HTML.
* *
* @default 'Next page' * @default 'Next page'
*/ */

Loading…
Cancel
Save