feat(theme): add plugin-llms and <PageContextMenu /> component (#753)
This commit is contained in:
parent
5b780c28d0
commit
20728f504d
@ -3,6 +3,7 @@ import { defineMermaidConfig } from '@vuepress/plugin-markdown-chart/client'
|
|||||||
import { defineAsyncComponent, h } from 'vue'
|
import { defineAsyncComponent, h } from 'vue'
|
||||||
import { Layout } from 'vuepress-theme-plume/client'
|
import { Layout } from 'vuepress-theme-plume/client'
|
||||||
import VPPostItem from 'vuepress-theme-plume/components/Posts/VPPostItem.vue'
|
import VPPostItem from 'vuepress-theme-plume/components/Posts/VPPostItem.vue'
|
||||||
|
import PageContextMenu from 'vuepress-theme-plume/features/PageContextMenu.vue'
|
||||||
import { defineClientConfig } from 'vuepress/client'
|
import { defineClientConfig } from 'vuepress/client'
|
||||||
import AsideNav from '~/components/AsideNav.vue'
|
import AsideNav from '~/components/AsideNav.vue'
|
||||||
import { setupThemeColors } from '~/composables/theme-colors.js'
|
import { setupThemeColors } from '~/composables/theme-colors.js'
|
||||||
@ -25,6 +26,7 @@ export default defineClientConfig({
|
|||||||
layouts: {
|
layouts: {
|
||||||
Layout: h(Layout, null, {
|
Layout: h(Layout, null, {
|
||||||
'aside-outline-after': () => h(AsideNav),
|
'aside-outline-after': () => h(AsideNav),
|
||||||
|
'doc-title-after': () => h(PageContextMenu),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}) as ClientConfig
|
}) as ClientConfig
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export const themeConfig: ThemeCollectionItem = defineCollection({
|
|||||||
'shiki',
|
'shiki',
|
||||||
'search',
|
'search',
|
||||||
'reading-time',
|
'reading-time',
|
||||||
|
'llms',
|
||||||
'markdown-enhance',
|
'markdown-enhance',
|
||||||
'markdown-power',
|
'markdown-power',
|
||||||
'markdown-image',
|
'markdown-image',
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export const themeConfig: ThemeCollectionItem = defineCollection({
|
|||||||
'shiki',
|
'shiki',
|
||||||
'search',
|
'search',
|
||||||
'reading-time',
|
'reading-time',
|
||||||
|
'llms',
|
||||||
'markdown-enhance',
|
'markdown-enhance',
|
||||||
'markdown-power',
|
'markdown-power',
|
||||||
'markdown-image',
|
'markdown-image',
|
||||||
|
|||||||
@ -3,9 +3,7 @@ import fs from 'node:fs'
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { viteBundler } from '@vuepress/bundler-vite'
|
import { viteBundler } from '@vuepress/bundler-vite'
|
||||||
import { addViteOptimizeDepsInclude, addViteSsrExternal } from '@vuepress/helper'
|
import { addViteOptimizeDepsInclude, addViteSsrExternal } from '@vuepress/helper'
|
||||||
import { llmsPlugin } from '@vuepress/plugin-llms'
|
|
||||||
import { defineUserConfig } from 'vuepress'
|
import { defineUserConfig } from 'vuepress'
|
||||||
import { tocGetter } from './llmstxtTOC.js'
|
|
||||||
import { theme } from './theme.js'
|
import { theme } from './theme.js'
|
||||||
|
|
||||||
const pnpmWorkspace = fs.readFileSync(path.resolve(__dirname, '../../pnpm-workspace.yaml'), 'utf-8')
|
const pnpmWorkspace = fs.readFileSync(path.resolve(__dirname, '../../pnpm-workspace.yaml'), 'utf-8')
|
||||||
@ -47,21 +45,6 @@ export default defineUserConfig({
|
|||||||
'~/composables': path.resolve(__dirname, './themes/composables'),
|
'~/composables': path.resolve(__dirname, './themes/composables'),
|
||||||
},
|
},
|
||||||
|
|
||||||
plugins: [
|
|
||||||
llmsPlugin({
|
|
||||||
llmsTxtTemplateGetter: {
|
|
||||||
description: (_, { currentLocale }) => {
|
|
||||||
return currentLocale === '/'
|
|
||||||
? '一个简约易用的,功能丰富的 vuepress 文档&博客 主题'
|
|
||||||
: 'An easy-to-use and feature-rich vuepress documentation and blog theme'
|
|
||||||
},
|
|
||||||
details: '',
|
|
||||||
toc: tocGetter,
|
|
||||||
},
|
|
||||||
locale: 'all',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
|
|
||||||
bundler: viteBundler(),
|
bundler: viteBundler(),
|
||||||
shouldPrefetch: false,
|
shouldPrefetch: false,
|
||||||
|
|
||||||
|
|||||||
@ -1,131 +0,0 @@
|
|||||||
import type { LLMPage, LLMState } from '@vuepress/plugin-llms'
|
|
||||||
import type { ThemeSidebarItem } from 'vuepress-theme-plume'
|
|
||||||
import { generateTOCLink as rawGenerateTOCLink } from '@vuepress/plugin-llms'
|
|
||||||
import { ensureEndingSlash, ensureLeadingSlash } from 'vuepress/shared'
|
|
||||||
import { path } from 'vuepress/utils'
|
|
||||||
import { enCollections, zhCollections } from './collections/index.js'
|
|
||||||
|
|
||||||
function normalizePath(prefix: string, path = ''): string {
|
|
||||||
if (path.startsWith('/'))
|
|
||||||
return path
|
|
||||||
|
|
||||||
return `${ensureEndingSlash(prefix)}${path}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function withBase(url = '', base = '/'): string {
|
|
||||||
if (!url)
|
|
||||||
return ''
|
|
||||||
if (url.startsWith(base))
|
|
||||||
return normalizePath(url)
|
|
||||||
return path.join(base, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
function genStarsWith(stars: string | undefined, locale: string) {
|
|
||||||
return (url: string): boolean => {
|
|
||||||
if (!stars)
|
|
||||||
return false
|
|
||||||
return url.startsWith(withBase(stars, locale))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tocGetter(llmPages: LLMPage[], llmState: LLMState): string {
|
|
||||||
const { currentLocale } = llmState
|
|
||||||
const isZh = currentLocale === '/'
|
|
||||||
const collections = isZh ? zhCollections : enCollections
|
|
||||||
|
|
||||||
let tableOfContent = ''
|
|
||||||
const usagePages: LLMPage[] = []
|
|
||||||
|
|
||||||
collections
|
|
||||||
.filter(item => item.type === 'post')
|
|
||||||
.forEach(({ title, linkPrefix, link }) => {
|
|
||||||
tableOfContent += `### ${title}\n\n`
|
|
||||||
const withLinkPrefix = genStarsWith(linkPrefix, currentLocale)
|
|
||||||
const withLink = genStarsWith(link, currentLocale)
|
|
||||||
const withFallback = genStarsWith('/article/', currentLocale)
|
|
||||||
const list: string[] = []
|
|
||||||
llmPages.forEach((page) => {
|
|
||||||
if (withLinkPrefix(page.path) || withLink(page.path) || withFallback(page.path)) {
|
|
||||||
usagePages.push(page)
|
|
||||||
list.push(rawGenerateTOCLink(page, llmState))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
tableOfContent += `${list.filter(Boolean).join('')}\n`
|
|
||||||
})
|
|
||||||
|
|
||||||
const generateTOCLink = (path: string): string => {
|
|
||||||
const filepath = path.endsWith('/') ? `${path}README.md` : path.endsWith('.md') ? path : `${path || 'README'}.md`
|
|
||||||
const link = path.endsWith('/') ? `${path}index.html` : `${path}.html`
|
|
||||||
const page = llmPages.find((item) => {
|
|
||||||
return ensureLeadingSlash(item.filePathRelative || '') === filepath || link === item.path
|
|
||||||
})
|
|
||||||
|
|
||||||
if (page) {
|
|
||||||
usagePages.push(page)
|
|
||||||
return rawGenerateTOCLink(page, llmState)
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const processAutoSidebar = (prefix: string): string[] => {
|
|
||||||
const list: string[] = []
|
|
||||||
llmPages.forEach((page) => {
|
|
||||||
if (ensureLeadingSlash(page.filePathRelative || '').startsWith(prefix)) {
|
|
||||||
usagePages.push(page)
|
|
||||||
list.push(rawGenerateTOCLink(page, llmState))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return list.filter(Boolean)
|
|
||||||
}
|
|
||||||
|
|
||||||
const processSidebar = (items: (string | ThemeSidebarItem)[], prefix: string): string[] => {
|
|
||||||
const result: string[] = []
|
|
||||||
items.forEach((item) => {
|
|
||||||
if (typeof item === 'string') {
|
|
||||||
result.push(generateTOCLink(normalizePath(prefix, item)))
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (item.link) {
|
|
||||||
result.push(generateTOCLink(normalizePath(prefix, item.link)))
|
|
||||||
}
|
|
||||||
if (item.items === 'auto') {
|
|
||||||
result.push(...processAutoSidebar(normalizePath(prefix, item.prefix)))
|
|
||||||
}
|
|
||||||
else if (item.items?.length) {
|
|
||||||
result.push(...processSidebar(item.items, normalizePath(prefix, item.prefix)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collections
|
|
||||||
collections
|
|
||||||
.filter(collection => collection.type === 'doc')
|
|
||||||
.forEach(({ dir, title, sidebar = [] }) => {
|
|
||||||
tableOfContent += `### ${title}\n\n`
|
|
||||||
const prefix = normalizePath(ensureLeadingSlash(withBase(dir, currentLocale)))
|
|
||||||
if (sidebar === 'auto') {
|
|
||||||
tableOfContent += `${processAutoSidebar(prefix).join('')}\n`
|
|
||||||
}
|
|
||||||
else if (sidebar.length) {
|
|
||||||
const home = generateTOCLink(ensureEndingSlash(prefix))
|
|
||||||
const list = processSidebar(sidebar, prefix)
|
|
||||||
if (home && !list.includes(home)) {
|
|
||||||
list.unshift(home)
|
|
||||||
}
|
|
||||||
tableOfContent += `${list.join('')}\n`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Others
|
|
||||||
const unUsagePages = llmPages.filter(page => !usagePages.includes(page))
|
|
||||||
if (unUsagePages.length) {
|
|
||||||
tableOfContent += '### Others\n\n'
|
|
||||||
tableOfContent += unUsagePages
|
|
||||||
.map(page => rawGenerateTOCLink(page, llmState))
|
|
||||||
.join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
return tableOfContent
|
|
||||||
}
|
|
||||||
@ -76,4 +76,16 @@ export const theme: Theme = plumeTheme({
|
|||||||
content: 'vuepress-theme-plume',
|
content: 'vuepress-theme-plume',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
llmstxt: {
|
||||||
|
locale: 'all',
|
||||||
|
llmsTxtTemplateGetter: {
|
||||||
|
description: (_, { currentLocale }) => {
|
||||||
|
return currentLocale === '/'
|
||||||
|
? '一个简约易用的,功能丰富的 vuepress 文档&博客 主题'
|
||||||
|
: 'An easy-to-use and feature-rich vuepress documentation and blog theme'
|
||||||
|
},
|
||||||
|
details: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
104
docs/config/plugins/llms.md
Normal file
104
docs/config/plugins/llms.md
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
title: LLMs txt
|
||||||
|
createTime: 2025/11/19 14:48:35
|
||||||
|
permalink: /config/plugins/llmstxt/
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
为站点添加 [llms.txt](https://llmstxt.org/),以提供对 LLM 友好的内容。
|
||||||
|
|
||||||
|
**关联插件**: [@vuepress/plugin-llms](https://ecosystem.vuejs.press/zh/plugins/ai/llms.html)
|
||||||
|
|
||||||
|
## 为什么需要 llms.txt?
|
||||||
|
|
||||||
|
大型语言模型越来越依赖网站信息,但面临一个关键限制:上下文窗口太小,无法完整处理大多数网站。将包含导航、广告和 JavaScript 的复杂 HTML 页面转换为适合 LLM 的纯文本既困难又不精确。
|
||||||
|
|
||||||
|
虽然网站同时为人类读者和 LLM 服务,但后者受益于在一个可访问的位置收集的更简洁、专家级的信息。这对于开发环境等使用案例尤其重要,因为 LLM 需要快速访问编程文档和 API。
|
||||||
|
|
||||||
|
向网站添加 `/llms.txt` Markdown 文件,以提供对 LLM 友好的内容。此文件提供了简短的背景信息、指南和指向详细 Markdown 文件的链接。
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
插件通过检索你的文档源目录中的所有 Markdown 文件,并将其转换为 LLM 友好的纯文本文件。
|
||||||
|
|
||||||
|
::: file-tree
|
||||||
|
|
||||||
|
- .vuepress/dist
|
||||||
|
- llms.txt
|
||||||
|
- llms-full.txt
|
||||||
|
- markdown-examples.html
|
||||||
|
- markdown-examples.md
|
||||||
|
- …
|
||||||
|
:::
|
||||||
|
|
||||||
|
点击以下链接查看本文档站点的 llms.txt 文件:
|
||||||
|
|
||||||
|
- [llms.txt](/llms.txt){.no-icon}
|
||||||
|
- [llms-full.txt](/llms-full.txt){.no-icon}
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
插件仅在生产构建时,即执行 `vuepress build` 命令时,生成 `llms.txt` 文件,以及其它 LLM 友好的文档文件,并将它们输出到 `.vuepress/dist` 目录中。
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
[完整功能说明请查看 **插件官方文档**](https://ecosystem.vuejs.press/zh/plugins/ai/llms.html#%E6%8F%92%E4%BB%B6%E5%8A%9F%E8%83%BD){.read-more}
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
主题默认不启用此功能,你可以通过 `llmstxt` 配置项启用它:
|
||||||
|
|
||||||
|
```ts title=".vuepress/config.ts"
|
||||||
|
import { defineUserConfig } from 'vuepress'
|
||||||
|
import { plumeTheme } from 'vuepress-theme-plume'
|
||||||
|
|
||||||
|
export default defineUserConfig({
|
||||||
|
theme: plumeTheme({
|
||||||
|
// 使用主题内置的默认配置
|
||||||
|
// llmstxt: true,
|
||||||
|
|
||||||
|
// 使用自定义配置
|
||||||
|
llmstxt: {
|
||||||
|
locale: '/',
|
||||||
|
// ...其它配置
|
||||||
|
},
|
||||||
|
|
||||||
|
// 也可以在 `plugins.llmstxt` 配置,但不推荐
|
||||||
|
plugins: {
|
||||||
|
llmstxt: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
[完整配置项说明请查看 **插件官方文档**](https://ecosystem.vuejs.press/zh/plugins/ai/llms.html#options){.read-more}
|
||||||
|
|
||||||
|
## 组件
|
||||||
|
|
||||||
|
为进一步增强 文档站点 与 LLMs 的互动,你可以在文档站点中添加 `<PageContextMenu />` 组件。
|
||||||
|
该组件不作为内置组件,而是主题额外的 `features` 实现,因此你需要手动引入它,
|
||||||
|
并在合适的位置,通过 [组件插槽](../../guide/custom/slots.md) 添加到文档站点中:
|
||||||
|
|
||||||
|
```ts title=".vuepress/client.ts"
|
||||||
|
import { defineAsyncComponent, h } from 'vue'
|
||||||
|
import { Layout } from 'vuepress-theme-plume/client'
|
||||||
|
import PageContextMenu from 'vuepress-theme-plume/features/PageContextMenu.vue' // [!code ++]
|
||||||
|
import { defineClientConfig } from 'vuepress/client'
|
||||||
|
|
||||||
|
export default defineClientConfig({
|
||||||
|
layouts: {
|
||||||
|
Layout: h(Layout, null, {
|
||||||
|
// 将 PageContextMenu 添加到 doc-title-after 插槽,即文章标题的右侧
|
||||||
|
'doc-title-after': () => h(PageContextMenu), // [!code ++]
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
你可以在当前页面的标题的右侧,体验该组件的功能。
|
||||||
|
|
||||||
|
::: important
|
||||||
|
此组件完全依赖于 `@vuepress/plugin-llms` 插件,仅当你启用了此插件功能后,才能使用它。
|
||||||
|
|
||||||
|
因此,此组件提供的功能 **仅在构建后的生产包中才可用** 。
|
||||||
|
:::
|
||||||
112
docs/en/config/plugins/llms.md
Normal file
112
docs/en/config/plugins/llms.md
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
---
|
||||||
|
title: LLMs txt
|
||||||
|
createTime: 2025/11/19 14:48:35
|
||||||
|
permalink: /en/config/plugins/llmstxt/
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Add [llms.txt](https://llmstxt.org/) to your site to provide LLM-friendly content.
|
||||||
|
|
||||||
|
**Related Plugin**: [@vuepress/plugin-llms](https://ecosystem.vuejs.press/plugins/ai/llms.html)
|
||||||
|
|
||||||
|
## Why llms.txt?
|
||||||
|
|
||||||
|
Large Language Models increasingly rely on website information but face a key limitation:
|
||||||
|
their context window is too small to fully process most websites.
|
||||||
|
Converting complex HTML pages containing navigation, ads, and JavaScript into LLM-friendly plain text is both difficult and imprecise.
|
||||||
|
|
||||||
|
While websites serve both human readers and LLMs, the latter benefit from more concise,
|
||||||
|
expert-level information collected in one accessible location.
|
||||||
|
This is particularly important for use cases like development environments where LLMs need quick access to programming documentation and APIs.
|
||||||
|
|
||||||
|
Add a `/llms.txt` Markdown file to your website to provide LLM-friendly content.
|
||||||
|
This file provides brief background information, guidelines, and links to detailed Markdown files.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
The plugin retrieves all Markdown files from your documentation source directory and converts them into LLM-friendly plain text files.
|
||||||
|
|
||||||
|
::: file-tree
|
||||||
|
|
||||||
|
- .vuepress/dist
|
||||||
|
- llms.txt
|
||||||
|
- llms-full.txt
|
||||||
|
- markdown-examples.html
|
||||||
|
- markdown-examples.md
|
||||||
|
- …
|
||||||
|
:::
|
||||||
|
|
||||||
|
Click the links below to view the llms.txt files for this documentation site:
|
||||||
|
|
||||||
|
- [llms.txt](/llms.txt){.no-icon}
|
||||||
|
- [llms-full.txt](/llms-full.txt){.no-icon}
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
The plugin only generates `llms.txt` files and other LLM-friendly documentation files during
|
||||||
|
production builds, i.e., when executing the `vuepress build` command, and outputs them to the `.vuepress/dist` directory.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
[View the complete feature description in the **Plugin Official Documentation**](https://ecosystem.vuejs.press/plugins/ai/llms.html#%E6%8F%92%E4%BB%B6%E5%8A%9F%E8%83%BD){.read-more}
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
This feature is not enabled by default in the theme. You can enable it through the `llmstxt` configuration option:
|
||||||
|
|
||||||
|
```ts title=".vuepress/config.ts"
|
||||||
|
import { defineUserConfig } from 'vuepress'
|
||||||
|
import { plumeTheme } from 'vuepress-theme-plume'
|
||||||
|
|
||||||
|
export default defineUserConfig({
|
||||||
|
theme: plumeTheme({
|
||||||
|
// Use the theme's built-in default configuration
|
||||||
|
// llmstxt: true,
|
||||||
|
|
||||||
|
// Use custom configuration
|
||||||
|
llmstxt: {
|
||||||
|
locale: '/',
|
||||||
|
// ...other configurations
|
||||||
|
},
|
||||||
|
|
||||||
|
// Can also configure via `plugins.llmstxt`, but not recommended
|
||||||
|
plugins: {
|
||||||
|
llmstxt: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
[View the complete configuration options in the **Plugin Official Documentation**](https://ecosystem.vuejs.press/plugins/ai/llms.html#options){.read-more}
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
To further enhance interaction between your documentation site and LLMs,
|
||||||
|
you can add the `<PageContextMenu />` component to your documentation site.
|
||||||
|
|
||||||
|
This component is not built-in but is implemented as an additional feature of the theme.
|
||||||
|
Therefore, you need to manually import it and place it in an appropriate location through [component slots](../../guide/custom/slots.md):
|
||||||
|
|
||||||
|
```ts title=".vuepress/client.ts"
|
||||||
|
import { defineAsyncComponent, h } from 'vue'
|
||||||
|
import { Layout } from 'vuepress-theme-plume/client'
|
||||||
|
import PageContextMenu from 'vuepress-theme-plume/features/PageContextMenu.vue' // [!code ++]
|
||||||
|
import { defineClientConfig } from 'vuepress/client'
|
||||||
|
|
||||||
|
export default defineClientConfig({
|
||||||
|
layouts: {
|
||||||
|
Layout: h(Layout, null, {
|
||||||
|
// Add PageContextMenu to the doc-title-after slot, i.e., to the right of the article title
|
||||||
|
'doc-title-after': () => h(PageContextMenu), // [!code ++]
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can experience this component's functionality to the right of the current page's title.
|
||||||
|
|
||||||
|
::: important
|
||||||
|
This component relies entirely on the `@vuepress/plugin-llms` plugin and can only be used when you have enabled this plugin's functionality.
|
||||||
|
|
||||||
|
Therefore, the functionality provided by this component **is only available in the built production package**.
|
||||||
|
:::
|
||||||
@ -106,6 +106,8 @@ You can preview <https://plume-layout-slots.netlify.app> to see the positions of
|
|||||||
- `doc-footer-before`
|
- `doc-footer-before`
|
||||||
- `doc-before`
|
- `doc-before`
|
||||||
- `doc-after`
|
- `doc-after`
|
||||||
|
- `doc-title-before`
|
||||||
|
- `doc-title-after`
|
||||||
- `doc-meta-top`
|
- `doc-meta-top`
|
||||||
- `doc-meta-bottom`
|
- `doc-meta-bottom`
|
||||||
- `doc-meta-before`
|
- `doc-meta-before`
|
||||||
|
|||||||
@ -104,6 +104,8 @@ export default defineClientConfig({
|
|||||||
- `doc-footer-before`
|
- `doc-footer-before`
|
||||||
- `doc-before`
|
- `doc-before`
|
||||||
- `doc-after`
|
- `doc-after`
|
||||||
|
- `doc-title-before`
|
||||||
|
- `doc-title-after`
|
||||||
- `doc-meta-top`
|
- `doc-meta-top`
|
||||||
- `doc-meta-bottom`
|
- `doc-meta-bottom`
|
||||||
- `doc-meta-before`
|
- `doc-meta-before`
|
||||||
|
|||||||
@ -19,7 +19,6 @@
|
|||||||
"@lunariajs/core": "catalog:dev",
|
"@lunariajs/core": "catalog:dev",
|
||||||
"@simonwep/pickr": "catalog:dev",
|
"@simonwep/pickr": "catalog:dev",
|
||||||
"@vuepress/bundler-vite": "catalog:vuepress",
|
"@vuepress/bundler-vite": "catalog:vuepress",
|
||||||
"@vuepress/plugin-llms": "catalog:vuepress",
|
|
||||||
"@vuepress/shiki-twoslash": "catalog:vuepress",
|
"@vuepress/shiki-twoslash": "catalog:vuepress",
|
||||||
"chart.js": "catalog:prod",
|
"chart.js": "catalog:prod",
|
||||||
"echarts": "catalog:prod",
|
"echarts": "catalog:prod",
|
||||||
|
|||||||
@ -30,6 +30,8 @@ export default defineClientConfig({
|
|||||||
'doc-footer-before': () => h(SlotDemo, { name: 'doc-footer-before' }),
|
'doc-footer-before': () => h(SlotDemo, { name: 'doc-footer-before' }),
|
||||||
'doc-before': () => h(SlotDemo, { name: 'doc-before', mt: 16 }),
|
'doc-before': () => h(SlotDemo, { name: 'doc-before', mt: 16 }),
|
||||||
'doc-after': () => h(SlotDemo, { name: 'doc-after' }),
|
'doc-after': () => h(SlotDemo, { name: 'doc-after' }),
|
||||||
|
'doc-title-before': () => h(SlotDemo, { name: 'doc-title-before', h: 24 }),
|
||||||
|
'doc-title-after': () => h(SlotDemo, { name: 'doc-title-after', h: 24 }),
|
||||||
'doc-meta-before': () => h(SlotDemo, { name: 'doc-meta-before', h: 24 }),
|
'doc-meta-before': () => h(SlotDemo, { name: 'doc-meta-before', h: 24 }),
|
||||||
'doc-meta-after': () => h(SlotDemo, { name: 'doc-meta-after', h: 24 }),
|
'doc-meta-after': () => h(SlotDemo, { name: 'doc-meta-after', h: 24 }),
|
||||||
'doc-meta-top': () => h(SlotDemo, { name: 'doc-meta-top' }),
|
'doc-meta-top': () => h(SlotDemo, { name: 'doc-meta-top' }),
|
||||||
|
|||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -551,9 +551,6 @@ importers:
|
|||||||
'@vuepress/bundler-vite':
|
'@vuepress/bundler-vite':
|
||||||
specifier: catalog:vuepress
|
specifier: catalog:vuepress
|
||||||
version: 2.0.0-rc.26(@types/node@24.10.1)(jiti@2.5.1)(less@4.4.2)(sass-embedded@1.93.3)(sass@1.94.0)(stylus@0.64.0)(typescript@5.9.3)(yaml@2.8.1)
|
version: 2.0.0-rc.26(@types/node@24.10.1)(jiti@2.5.1)(less@4.4.2)(sass-embedded@1.93.3)(sass@1.94.0)(stylus@0.64.0)(typescript@5.9.3)(yaml@2.8.1)
|
||||||
'@vuepress/plugin-llms':
|
|
||||||
specifier: catalog:vuepress
|
|
||||||
version: 2.0.0-rc.118(typescript@5.9.3)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@24.10.1)(jiti@2.5.1)(less@4.4.2)(sass-embedded@1.93.3)(sass@1.94.0)(stylus@0.64.0)(typescript@5.9.3)(yaml@2.8.1))(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)))
|
|
||||||
'@vuepress/shiki-twoslash':
|
'@vuepress/shiki-twoslash':
|
||||||
specifier: catalog:vuepress
|
specifier: catalog:vuepress
|
||||||
version: 2.0.0-rc.118(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3))(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@24.10.1)(jiti@2.5.1)(less@4.4.2)(sass-embedded@1.93.3)(sass@1.94.0)(stylus@0.64.0)(typescript@5.9.3)(yaml@2.8.1))(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)))
|
version: 2.0.0-rc.118(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3))(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@24.10.1)(jiti@2.5.1)(less@4.4.2)(sass-embedded@1.93.3)(sass@1.94.0)(stylus@0.64.0)(typescript@5.9.3)(yaml@2.8.1))(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)))
|
||||||
@ -817,6 +814,9 @@ importers:
|
|||||||
'@vuepress/plugin-git':
|
'@vuepress/plugin-git':
|
||||||
specifier: catalog:vuepress
|
specifier: catalog:vuepress
|
||||||
version: 2.0.0-rc.118(typescript@5.9.3)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@24.10.1)(jiti@2.5.1)(less@4.4.2)(sass-embedded@1.93.3)(sass@1.94.0)(stylus@0.64.0)(typescript@5.9.3)(yaml@2.8.1))(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)))
|
version: 2.0.0-rc.118(typescript@5.9.3)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@24.10.1)(jiti@2.5.1)(less@4.4.2)(sass-embedded@1.93.3)(sass@1.94.0)(stylus@0.64.0)(typescript@5.9.3)(yaml@2.8.1))(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)))
|
||||||
|
'@vuepress/plugin-llms':
|
||||||
|
specifier: catalog:vuepress
|
||||||
|
version: 2.0.0-rc.118(typescript@5.9.3)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@24.10.1)(jiti@2.5.1)(less@4.4.2)(sass-embedded@1.93.3)(sass@1.94.0)(stylus@0.64.0)(typescript@5.9.3)(yaml@2.8.1))(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)))
|
||||||
'@vuepress/plugin-markdown-chart':
|
'@vuepress/plugin-markdown-chart':
|
||||||
specifier: catalog:vuepress
|
specifier: catalog:vuepress
|
||||||
version: 2.0.0-rc.118(chart.js@4.5.1)(echarts@6.0.0)(flowchart.ts@3.0.1)(markdown-it@14.1.0)(markmap-lib@0.18.12(markmap-common@0.18.9))(markmap-toolbar@0.18.12(markmap-common@0.18.9))(markmap-view@0.18.12(markmap-common@0.18.9))(mermaid@11.12.1)(typescript@5.9.3)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@24.10.1)(jiti@2.5.1)(less@4.4.2)(sass-embedded@1.93.3)(sass@1.94.0)(stylus@0.64.0)(typescript@5.9.3)(yaml@2.8.1))(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)))
|
version: 2.0.0-rc.118(chart.js@4.5.1)(echarts@6.0.0)(flowchart.ts@3.0.1)(markdown-it@14.1.0)(markmap-lib@0.18.12(markmap-common@0.18.9))(markmap-toolbar@0.18.12(markmap-common@0.18.9))(markmap-view@0.18.12(markmap-common@0.18.9))(mermaid@11.12.1)(typescript@5.9.3)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@24.10.1)(jiti@2.5.1)(less@4.4.2)(sass-embedded@1.93.3)(sass@1.94.0)(stylus@0.64.0)(typescript@5.9.3)(yaml@2.8.1))(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)))
|
||||||
|
|||||||
@ -113,6 +113,7 @@
|
|||||||
"@vuepress/plugin-copy-code": "catalog:vuepress",
|
"@vuepress/plugin-copy-code": "catalog:vuepress",
|
||||||
"@vuepress/plugin-docsearch": "catalog:vuepress",
|
"@vuepress/plugin-docsearch": "catalog:vuepress",
|
||||||
"@vuepress/plugin-git": "catalog:vuepress",
|
"@vuepress/plugin-git": "catalog:vuepress",
|
||||||
|
"@vuepress/plugin-llms": "catalog:vuepress",
|
||||||
"@vuepress/plugin-markdown-chart": "catalog:vuepress",
|
"@vuepress/plugin-markdown-chart": "catalog:vuepress",
|
||||||
"@vuepress/plugin-markdown-hint": "catalog:vuepress",
|
"@vuepress/plugin-markdown-hint": "catalog:vuepress",
|
||||||
"@vuepress/plugin-markdown-image": "catalog:vuepress",
|
"@vuepress/plugin-markdown-image": "catalog:vuepress",
|
||||||
|
|||||||
@ -147,6 +147,12 @@ watch(
|
|||||||
<slot name="doc-bottom" />
|
<slot name="doc-bottom" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #doc-title-before>
|
||||||
|
<slot name="doc-title-before" />
|
||||||
|
</template>
|
||||||
|
<template #doc-title-after>
|
||||||
|
<slot name="doc-title-after" />
|
||||||
|
</template>
|
||||||
<template #doc-meta-before>
|
<template #doc-meta-before>
|
||||||
<slot name="doc-meta-before" />
|
<slot name="doc-meta-before" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -122,6 +122,12 @@ watch(
|
|||||||
|
|
||||||
<slot name="doc-meta-top" />
|
<slot name="doc-meta-top" />
|
||||||
<VPDocMeta>
|
<VPDocMeta>
|
||||||
|
<template #doc-title-before>
|
||||||
|
<slot name="doc-title-before" />
|
||||||
|
</template>
|
||||||
|
<template #doc-title-after>
|
||||||
|
<slot name="doc-title-after" />
|
||||||
|
</template>
|
||||||
<template #doc-meta-before>
|
<template #doc-meta-before>
|
||||||
<slot name="doc-meta-before" />
|
<slot name="doc-meta-before" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -53,11 +53,15 @@ const hasMeta = computed(() =>
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1 class="vp-doc-title page-title" :class="{ padding: !hasMeta }">
|
<div class="vp-doc-title">
|
||||||
<VPBadge v-if="page.frontmatter.draft" type="warning" text="DRAFT" />
|
<slot name="doc-title-before" />
|
||||||
{{ page.title }}
|
<h1 class="page-title" :class="{ padding: !hasMeta }">
|
||||||
<VPBadge v-if="badge" :type="badge.type || 'tip'" :text="badge.text" />
|
<VPBadge v-if="page.frontmatter.draft" type="warning" text="DRAFT" />
|
||||||
</h1>
|
{{ page.title }}
|
||||||
|
<VPBadge v-if="badge" :type="badge.type || 'tip'" :text="badge.text" />
|
||||||
|
</h1>
|
||||||
|
<slot name="doc-title-after" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="hasMeta" class="vp-doc-meta">
|
<div v-if="hasMeta" class="vp-doc-meta">
|
||||||
<slot name="doc-meta-before" />
|
<slot name="doc-meta-before" />
|
||||||
@ -90,7 +94,18 @@ const hasMeta = computed(() =>
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.vp-doc-title {
|
@media (min-width: 768px) {
|
||||||
|
.vp-doc-title {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
margin-bottom: 0.7rem;
|
margin-bottom: 0.7rem;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@ -99,7 +114,7 @@ const hasMeta = computed(() =>
|
|||||||
transition: color var(--vp-t-color);
|
transition: color var(--vp-t-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vp-doc-title.padding {
|
.page-title.padding {
|
||||||
padding-bottom: 4rem;
|
padding-bottom: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
306
theme/src/client/features/components/PageContextMenu.vue
Normal file
306
theme/src/client/features/components/PageContextMenu.vue
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onClickOutside, useClipboard, useToggle } from '@vueuse/core'
|
||||||
|
import { computed, onMounted, ref, useTemplateRef } from 'vue'
|
||||||
|
import { withBase } from 'vuepress/client'
|
||||||
|
import { ensureEndingSlash } from 'vuepress/shared'
|
||||||
|
import { useData } from '../../composables/index.js'
|
||||||
|
|
||||||
|
import '@vuepress/helper/transition/fade-in.css'
|
||||||
|
|
||||||
|
interface MenuItem {
|
||||||
|
link: string
|
||||||
|
text: string
|
||||||
|
tagline: string
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { claude = true, chatgpt = true } = defineProps<{
|
||||||
|
claude?: boolean
|
||||||
|
chatgpt?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { page, frontmatter, theme } = useData()
|
||||||
|
|
||||||
|
const markdownLink = computed(() => {
|
||||||
|
const url = withBase(page.value.path)
|
||||||
|
if (url.endsWith('.html'))
|
||||||
|
return `${url.slice(0, -5)}.md`
|
||||||
|
return `${ensureEndingSlash(url)}index.md`
|
||||||
|
})
|
||||||
|
|
||||||
|
const message = computed(() => {
|
||||||
|
if (__VUEPRESS_SSR__) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return encodeURIComponent(
|
||||||
|
(theme.value.askAIMessage ?? 'Read {link} and answer content-related questions.')
|
||||||
|
.replace('{link}', location.origin + markdownLink.value),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const menuList = computed(() => {
|
||||||
|
const list: MenuItem[] = []
|
||||||
|
|
||||||
|
list.push({
|
||||||
|
link: markdownLink.value,
|
||||||
|
text: theme.value.viewMarkdown ?? 'View as Markdown',
|
||||||
|
tagline: theme.value.viewMarkdownTagline ?? 'View this page as plain text',
|
||||||
|
icon: 'vpi-markdown',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (chatgpt) {
|
||||||
|
list.push({
|
||||||
|
link: `https://chat.openai.com/?prompt=${message.value}`,
|
||||||
|
text: (theme.value.askAIText ?? 'Open in {name}').replace('{name}', 'ChatGPT'),
|
||||||
|
tagline: (theme.value.askAITagline ?? 'Ask {name} about this page').replace('{name}', 'ChatGPT'),
|
||||||
|
icon: 'vpi-chatgpt',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claude) {
|
||||||
|
list.push({
|
||||||
|
link: `https://claude.ai/new?q=${message.value}`,
|
||||||
|
text: (theme.value.askAIText ?? 'Open in {name}').replace('{name}', 'Claude'),
|
||||||
|
tagline: (theme.value.askAITagline ?? 'Ask {name} about this page').replace('{name}', 'Claude'),
|
||||||
|
icon: 'vpi-claude',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
})
|
||||||
|
|
||||||
|
const markdownContent = ref('')
|
||||||
|
const loaded = ref(true)
|
||||||
|
const { copy, copied } = useClipboard()
|
||||||
|
|
||||||
|
async function onCopy() {
|
||||||
|
if (!markdownContent.value) {
|
||||||
|
loaded.value = false
|
||||||
|
await fetchMarkdownContent()
|
||||||
|
loaded.value = true
|
||||||
|
}
|
||||||
|
markdownContent.value && copy(markdownContent.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
let promise: Promise<void> | null = null
|
||||||
|
async function fetchMarkdownContent() {
|
||||||
|
if (promise)
|
||||||
|
return
|
||||||
|
promise = fetch(location.origin + markdownLink.value)
|
||||||
|
.then(res => res.text())
|
||||||
|
.then((text) => {
|
||||||
|
markdownContent.value = text.trimStart().replace(/^---[\s\S]+?---/, '').trimStart()
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
promise = null
|
||||||
|
})
|
||||||
|
await promise
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const idl = window.requestIdleCallback || window.requestAnimationFrame || (cb => setTimeout(cb, 0))
|
||||||
|
idl(fetchMarkdownContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
const menuRef = useTemplateRef('menu')
|
||||||
|
const toggleRef = useTemplateRef('toggle')
|
||||||
|
const [open, toggleMenu] = useToggle(false)
|
||||||
|
onClickOutside(menuRef, () => toggleMenu(false), { ignore: [toggleRef] })
|
||||||
|
|
||||||
|
const copyPageText = computed(() => {
|
||||||
|
const copyText = theme.value.copyPageText ?? 'Copy page'
|
||||||
|
const copiedText = theme.value.copiedPageText ?? 'Copied !'
|
||||||
|
const copyingText = theme.value.copingPageText ?? 'Copying..'
|
||||||
|
return copied.value ? copiedText : !loaded.value ? copyingText : copyText
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="frontmatter.llmstxt !== false" class="vp-page-context-menu">
|
||||||
|
<div class="page-context-button" type="button">
|
||||||
|
<span class="page-context-copy" @click="onCopy">
|
||||||
|
<span class="vpi-copy" :class="{ loading: !loaded, copied }" />
|
||||||
|
<span class="text">{{ copyPageText }}</span>
|
||||||
|
</span>
|
||||||
|
<span ref="toggle" class="page-context-toggle" :class="{ open }" @click="() => toggleMenu()">
|
||||||
|
<span class="vpi-chevron-down" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Transition name="fade-in">
|
||||||
|
<ul v-show="open" ref="menu" class="page-context-menu">
|
||||||
|
<li>
|
||||||
|
<a href="javascript:void(0)" @click="onCopy">
|
||||||
|
<span class="vpi-copy" :class="{ loading: !loaded, copied }" />
|
||||||
|
<span>
|
||||||
|
{{ copyPageText }}
|
||||||
|
<small>{{ theme.copyTagline ?? 'Copy page as Markdown for LLMs' }}</small>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li v-for="item in menuList" :key="item.text">
|
||||||
|
<a
|
||||||
|
:href="item.link" target="_blank" rel="noopener noreferrer"
|
||||||
|
:aria-label="item.text" data-allow-mismatch
|
||||||
|
>
|
||||||
|
<span :class="item.icon" />
|
||||||
|
<span>
|
||||||
|
{{ item.text }} <span class="vpi-external-link" />
|
||||||
|
<small>{{ item.tagline }}</small>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.vp-page-context-menu {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
align-self: flex-start;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-context-button {
|
||||||
|
height: 32px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: solid 1px var(--vp-c-divider);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-context-button,
|
||||||
|
.page-context-copy {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-context-copy {
|
||||||
|
gap: 4px;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-context-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
border-left: solid 1px var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-context-toggle {
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-context-copy,
|
||||||
|
.page-context-toggle {
|
||||||
|
height: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: transparent;
|
||||||
|
transition: background-color var(--vp-t-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-context-copy:hover,
|
||||||
|
.page-context-toggle:hover,
|
||||||
|
.page-context-toggle.open {
|
||||||
|
background-color: var(--vp-c-bg-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-context-copy .text {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-context-toggle .vpi-chevron-down {
|
||||||
|
transition: transform var(--vp-t-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-context-toggle.open .vpi-chevron-down {
|
||||||
|
transform: rotate(270deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vpi-copy {
|
||||||
|
--icon: var(--code-copy-icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vpi-copy.copied {
|
||||||
|
--icon: var(--code-copied-icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vpi-copy.loading {
|
||||||
|
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-dasharray='16' stroke-dashoffset='16' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M12 3c4.97 0 9 4.03 9 9'%3E%3Canimate fill='freeze' attributeName='stroke-dashoffset' dur='0.2s' values='16;0'/%3E%3CanimateTransform attributeName='transform' dur='1.5s' repeatCount='indefinite' type='rotate' values='0 12 12;360 12 12'/%3E%3C/path%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-context-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 12px);
|
||||||
|
left: 0;
|
||||||
|
z-index: 20;
|
||||||
|
width: max-content;
|
||||||
|
padding: 8px 4px;
|
||||||
|
list-style: none;
|
||||||
|
background-color: var(--vp-c-bg);
|
||||||
|
border: solid 1px var(--vp-c-divider);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: var(--vp-shadow-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.page-context-menu {
|
||||||
|
right: 0;
|
||||||
|
left: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-context-menu li a {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background-color var(--vp-t-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-context-menu li a:hover {
|
||||||
|
background-color: var(--vp-c-bg-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-context-menu li a > [class*="vpi-"] {
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
border: solid 1px var(--vp-c-divider);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-context-menu li a small {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-context-menu .vpi-external-link {
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vpi-markdown {
|
||||||
|
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M20.56 18H3.44C2.65 18 2 17.37 2 16.59V7.41C2 6.63 2.65 6 3.44 6h17.12c.79 0 1.44.63 1.44 1.41v9.18c0 .78-.65 1.41-1.44 1.41M6.81 15.19v-3.66l1.92 2.35l1.92-2.35v3.66h1.93V8.81h-1.93l-1.92 2.35l-1.92-2.35H4.89v6.38zM19.69 12h-1.92V8.81h-1.92V12h-1.93l2.89 3.28z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.vpi-chatgpt {
|
||||||
|
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z'/%3E%3Cpath fill='%23000' d='M10 2a4 4 0 0 1 3.46 1.99l.098.182l.638-.368a4 4 0 0 1 5.475 5.446l-.113.186l.638.368a4 4 0 0 1-1.979 7.464L18 17.264V18a4 4 0 0 1-7.459 2.01l-.1-.182l-.637.368a4 4 0 0 1-5.475-5.446l.113-.186l-.638-.368a4 4 0 0 1 1.979-7.464L6 6.736V6a4 4 0 0 1 4-4m4.702 10.788l-.068 4.059a1 1 0 0 1-.391.777l-.109.072l-1.956 1.13a2.002 2.002 0 0 0 3.817-.677L16 18v-4.434l-1.298-.779Zm-2.033 1.946l-3.55 1.97a1 1 0 0 1-.985-.008l-1.956-1.13a2.001 2.001 0 0 0 2.626 2.898l3.84-2.217zm2.687-5.415l-1.323.735l3.482 2.089A1 1 0 0 1 18 13v2.259a2.002 2.002 0 0 0 1.196-3.723zM6 8.74a2.001 2.001 0 0 0-1.328 3.64l.132.083l3.84 2.217l1.323-.735l-3.482-2.088a1 1 0 0 1-.477-.728L6 11zM10 4a2 2 0 0 0-2 2v4.434l1.298.779l.068-4.06a1 1 0 0 1 .5-.85l1.956-1.129A2 2 0 0 0 10 4m7.928 2.268a2 2 0 0 0-2.594-.805l-.138.073l-3.84 2.217l-.025 1.513l3.55-1.97a1 1 0 0 1 .868-.05l.117.058l1.957 1.13c.442-.62.51-1.465.105-2.166'/%3E%3C/g%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.vpi-claude {
|
||||||
|
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='m5.92 15.3l3.94-2.2l.06-.2l-.06-.1h-.2L9 12.76l-2.24-.06l-1.96-.1l-1.9-.1l-.48-.1l-.42-.6l.04-.3l.4-.26l.58.04l1.26.1l1.9.12l1.38.08l2.04.24h.32l.04-.14l-.1-.08l-.08-.08L7.8 10.2L5.68 8.8l-1.12-.82l-.6-.4l-.3-.4l-.12-.84l.54-.6l.74.06l.18.04l.74.58l1.6 1.22L9.4 9.2l.3.24l.12-.08l.02-.06l-.14-.22L8.6 7L7.4 4.92l-.54-.86l-.14-.52c-.06-.2-.08-.4-.08-.6l.6-.84l.36-.1l.84.12l.32.28l.52 1.2l.82 1.86l1.3 2.52l.4.76l.2.68l.06.2h.14v-.1l.1-1.44l.2-1.74l.2-2.24l.06-.64l.32-.76l.6-.4l.52.22l.4.58l-.06.36L14.32 5l-.52 2.42l-.3 1.64h.18l.2-.22l.82-1.08l1.38-1.72l.6-.7l.72-.74l.46-.36h.86l.62.94l-.28.98l-.88 1.12l-.74.94l-1.06 1.42l-.64 1.14l.06.08h.14l2.4-.52l1.28-.22l1.52-.26l.7.32l.08.32l-.28.68l-1.64.4l-1.92.4l-2.86.66l-.04.02l.04.06l1.28.12l.56.04h1.36l2.52.2l.66.4l.38.54l-.06.4l-1.02.52l-1.36-.32l-3.2-.76l-1.08-.26h-.16v.08l.92.9l1.66 1.5l2.12 1.94l.1.48l-.26.4l-.28-.04l-1.84-1.4l-.72-.6l-1.6-1.36h-.1v.14l.36.54l1.96 2.94l.1.9l-.14.28l-.52.2l-.54-.12l-1.16-1.6l-1.2-1.8l-.94-1.64l-.1.08l-.58 6.04l-.26.3l-.6.24l-.5-.4l-.28-.6l.28-1.24l.32-1.6l.26-1.28l.24-1.58l.14-.52v-.04h-.14l-1.2 1.66l-1.8 2.46l-1.44 1.52l-.34.14l-.6-.3l.06-.56l.32-.46l2-2.56l1.2-1.58l.8-.92l-.02-.1h-.06l-5.28 3.44l-.94.12l-.4-.4l.04-.6l.2-.2l1.6-1.1z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -100,6 +100,12 @@ useCloseSidebarOnEscape(isSidebarOpen, closeSidebar)
|
|||||||
<template #page-bottom>
|
<template #page-bottom>
|
||||||
<slot name="page-bottom" />
|
<slot name="page-bottom" />
|
||||||
</template>
|
</template>
|
||||||
|
<template #doc-title-before>
|
||||||
|
<slot name="doc-title-before" />
|
||||||
|
</template>
|
||||||
|
<template #doc-title-after>
|
||||||
|
<slot name="doc-title-after" />
|
||||||
|
</template>
|
||||||
<template #doc-meta-before>
|
<template #doc-meta-before>
|
||||||
<slot name="doc-meta-before" />
|
<slot name="doc-meta-before" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -28,6 +28,7 @@ const EXCLUDE_LIST: (keyof ThemeOptions)[] = [
|
|||||||
'watermark',
|
'watermark',
|
||||||
'readingTime',
|
'readingTime',
|
||||||
'copyCode',
|
'copyCode',
|
||||||
|
'llmstxt',
|
||||||
]
|
]
|
||||||
// 过滤不需要出现在多语言配置中的字段
|
// 过滤不需要出现在多语言配置中的字段
|
||||||
const EXCLUDE_LOCALE_LIST: (keyof ThemeOptions)[] = [...EXCLUDE_LIST, 'blog', 'appearance']
|
const EXCLUDE_LOCALE_LIST: (keyof ThemeOptions)[] = [...EXCLUDE_LIST, 'blog', 'appearance']
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export const PLUGINS_SUPPORTED_FIELDS: (keyof ThemeBuiltinPlugins)[] = [
|
|||||||
'readingTime',
|
'readingTime',
|
||||||
'watermark',
|
'watermark',
|
||||||
'replaceAssets',
|
'replaceAssets',
|
||||||
|
'llmstxt',
|
||||||
]
|
]
|
||||||
|
|
||||||
export const MARKDOWN_CHART_FIELDS: (keyof MarkdownChartPluginOptions)[] = [
|
export const MARKDOWN_CHART_FIELDS: (keyof MarkdownChartPluginOptions)[] = [
|
||||||
|
|||||||
@ -51,6 +51,16 @@ export const deLocale: ThemeLocaleText = {
|
|||||||
message:
|
message:
|
||||||
'Unterstützt von <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
|
'Unterstützt von <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
copyPageText: 'Seite kopieren',
|
||||||
|
copiedPageText: 'Kopieren !',
|
||||||
|
copingPageText: 'Wird kopiert..',
|
||||||
|
copyTagline: 'Seite als Markdown für LLMs kopieren',
|
||||||
|
viewMarkdown: 'Als Markdown anzeigen',
|
||||||
|
viewMarkdownTagline: 'Diese Seite als Nur-Text anzeigen',
|
||||||
|
askAIText: 'In {name} öffnen',
|
||||||
|
askAITagline: '{name} zu dieser Seite befragen',
|
||||||
|
askAIMessage: 'Lese {link} und beantworte Fragen zum Inhalt.',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dePresetLocale: PresetLocale = {
|
export const dePresetLocale: PresetLocale = {
|
||||||
|
|||||||
@ -38,6 +38,16 @@ export const enLocale: ThemeLocaleText = {
|
|||||||
message:
|
message:
|
||||||
'Powered by <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
|
'Powered by <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
copyPageText: 'Copy page',
|
||||||
|
copiedPageText: 'Copied !',
|
||||||
|
copingPageText: 'Copying..',
|
||||||
|
copyTagline: 'Copy page as Markdown for LLMs',
|
||||||
|
viewMarkdown: 'View as Markdown',
|
||||||
|
viewMarkdownTagline: 'View this page as plain text',
|
||||||
|
askAIText: 'Open in {name}',
|
||||||
|
askAITagline: 'Ask {name} about this page',
|
||||||
|
askAIMessage: 'Read {link} and answer content-related questions.',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enPresetLocale: PresetLocale = {
|
export const enPresetLocale: PresetLocale = {
|
||||||
|
|||||||
@ -51,6 +51,16 @@ export const frLocale: ThemeLocaleText = {
|
|||||||
message:
|
message:
|
||||||
'Propulsé par <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
|
'Propulsé par <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
copyPageText: 'Copier la page',
|
||||||
|
copiedPageText: 'Copie réussie',
|
||||||
|
copingPageText: 'Copie en cours..',
|
||||||
|
copyTagline: 'Copier la page au format Markdown pour une utilisation avec des LLM',
|
||||||
|
viewMarkdown: 'Voir en Markdown',
|
||||||
|
viewMarkdownTagline: 'Voir cette page en texte brut',
|
||||||
|
askAIText: 'Ouvrir dans {name}',
|
||||||
|
askAITagline: 'Interroger {name} sur cette page',
|
||||||
|
askAIMessage: 'Lisez {link} et répondez aux questions concernant son contenu.',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const frPresetLocale: PresetLocale = {
|
export const frPresetLocale: PresetLocale = {
|
||||||
|
|||||||
@ -51,6 +51,16 @@ export const jaLocale: ThemeLocaleText = {
|
|||||||
message:
|
message:
|
||||||
'<a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a> によって提供されています',
|
'<a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a> によって提供されています',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
copyPageText: 'ページをコピー',
|
||||||
|
copiedPageText: 'コピーしました',
|
||||||
|
copingPageText: 'コピー中..',
|
||||||
|
copyTagline: 'ページをMarkdown形式でコピーしてLLMで使用',
|
||||||
|
viewMarkdown: 'Markdown形式で表示',
|
||||||
|
viewMarkdownTagline: 'このページをプレーンテキストで表示',
|
||||||
|
askAIText: '{name} で開く',
|
||||||
|
askAITagline: 'このページについて {name} に質問する',
|
||||||
|
askAIMessage: '{link} を読み、内容に関する質問に答えてください。',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const jaPresetLocale: PresetLocale = {
|
export const jaPresetLocale: PresetLocale = {
|
||||||
|
|||||||
@ -51,6 +51,16 @@ export const koLocale: ThemeLocaleText = {
|
|||||||
message:
|
message:
|
||||||
'Powered by <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
|
'Powered by <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
copyPageText: '페이지 복사',
|
||||||
|
copiedPageText: '복사 완료',
|
||||||
|
copingPageText: '복사 중..',
|
||||||
|
copyTagline: '페이지를 마크다운 형식으로 복사하여 LLM에서 사용',
|
||||||
|
viewMarkdown: 'Markdown 형식으로 보기',
|
||||||
|
viewMarkdownTagline: '이 페이지를 일반 텍스트로 보기',
|
||||||
|
askAIText: '{name} 에서 열기',
|
||||||
|
askAITagline: '이 페이지에 대해 {name} 에 질문하기',
|
||||||
|
askAIMessage: '{link} 을(를) 읽고 내용과 관련된 질문에 답변해 주세요.',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const koPresetLocale: PresetLocale = {
|
export const koPresetLocale: PresetLocale = {
|
||||||
|
|||||||
@ -51,6 +51,16 @@ export const ruLocale: ThemeLocaleText = {
|
|||||||
message:
|
message:
|
||||||
'Работает на <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
|
'Работает на <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
copyPageText: 'Копировать страницу',
|
||||||
|
copiedPageText: 'Скопировано успешно',
|
||||||
|
copingPageText: 'Копируется...',
|
||||||
|
copyTagline: 'Скопировать страницу в формате Markdown для использования в LLM',
|
||||||
|
viewMarkdown: 'Просмотреть в Markdown',
|
||||||
|
viewMarkdownTagline: 'Просмотреть эту страницу в виде простого текста',
|
||||||
|
askAIText: 'Открыть в {name}',
|
||||||
|
askAITagline: 'Спросить {name} об этой странице',
|
||||||
|
askAIMessage: 'Прочитайте {link} и ответьте на вопросы, связанные с содержанием.',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ruPresetLocale: PresetLocale = {
|
export const ruPresetLocale: PresetLocale = {
|
||||||
|
|||||||
@ -51,6 +51,16 @@ export const zhTwLocale: ThemeLocaleText = {
|
|||||||
message:
|
message:
|
||||||
'Powered by <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
|
'Powered by <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
copyPageText: '複製頁面',
|
||||||
|
copiedPageText: '複製成功',
|
||||||
|
copingPageText: '複製中..',
|
||||||
|
copyTagline: '將頁面以 Markdown 格式複製供 LLMs 使用',
|
||||||
|
viewMarkdown: '以 Markdown 格式檢視',
|
||||||
|
viewMarkdownTagline: '以純文字檢視此頁面',
|
||||||
|
askAIText: '在 {name} 中開啟',
|
||||||
|
askAITagline: '向 {name} 提問有關此頁面',
|
||||||
|
askAIMessage: '閱讀 {link} 並回答內容相關的問題。',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const zhTwPresetLocale: PresetLocale = {
|
export const zhTwPresetLocale: PresetLocale = {
|
||||||
|
|||||||
@ -50,6 +50,16 @@ export const zhLocale: ThemeLocaleText = {
|
|||||||
message:
|
message:
|
||||||
'Powered by <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
|
'Powered by <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
copyPageText: '复制页面',
|
||||||
|
copiedPageText: '复制成功',
|
||||||
|
copingPageText: '复制中..',
|
||||||
|
copyTagline: '将页面以 Markdown 格式复制供 LLMs 使用',
|
||||||
|
viewMarkdown: '以 Markdown 格式查看',
|
||||||
|
viewMarkdownTagline: '以纯文本查看此页面',
|
||||||
|
askAIText: '在 {name} 中打开',
|
||||||
|
askAITagline: '向 {name} 提问有关此页面',
|
||||||
|
askAIMessage: '阅读 {link} 并回答内容相关的问题。',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const zhPresetLocale: PresetLocale = {
|
export const zhPresetLocale: PresetLocale = {
|
||||||
|
|||||||
142
theme/src/node/plugins/llms.ts
Normal file
142
theme/src/node/plugins/llms.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import type { LLMPage, LlmsPluginOptions, LLMState } from '@vuepress/plugin-llms'
|
||||||
|
import type { App, PluginConfig } from 'vuepress'
|
||||||
|
import type { ThemeSidebarItem } from '../../shared/index.js'
|
||||||
|
import { generateTOCLink as rawGenerateTOCLink, llmsPlugin as rawLlmsPlugin } from '@vuepress/plugin-llms'
|
||||||
|
import { ensureEndingSlash, ensureLeadingSlash, isPlainObject } from 'vuepress/shared'
|
||||||
|
import { getThemeConfig } from '../loadConfig/index.js'
|
||||||
|
import { withBase } from '../utils/index.js'
|
||||||
|
|
||||||
|
export function llmsPlugin(app: App, userOptions: true | LlmsPluginOptions): PluginConfig {
|
||||||
|
if (!app.env.isBuild)
|
||||||
|
return []
|
||||||
|
|
||||||
|
const { llmsTxtTemplateGetter, ...userLLMsTxt } = isPlainObject(userOptions) ? userOptions : {}
|
||||||
|
|
||||||
|
function tocGetter(llmPages: LLMPage[], llmState: LLMState): string {
|
||||||
|
const options = getThemeConfig()
|
||||||
|
const { currentLocale } = llmState
|
||||||
|
const collections = options.locales?.[currentLocale]?.collections || []
|
||||||
|
|
||||||
|
if (!collections.length)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
let tableOfContent = ''
|
||||||
|
const usagePages: LLMPage[] = []
|
||||||
|
|
||||||
|
collections
|
||||||
|
.filter(item => item.type === 'post')
|
||||||
|
.forEach(({ title, linkPrefix, link }) => {
|
||||||
|
tableOfContent += `### ${title}\n\n`
|
||||||
|
const withLinkPrefix = genStarsWith(linkPrefix, currentLocale)
|
||||||
|
const withLink = genStarsWith(link, currentLocale)
|
||||||
|
const withFallback = genStarsWith('/article/', currentLocale)
|
||||||
|
const list: string[] = []
|
||||||
|
llmPages.forEach((page) => {
|
||||||
|
if (withLinkPrefix(page.path) || withLink(page.path) || withFallback(page.path)) {
|
||||||
|
usagePages.push(page)
|
||||||
|
list.push(rawGenerateTOCLink(page, llmState))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
tableOfContent += `${list.filter(Boolean).join('')}\n`
|
||||||
|
})
|
||||||
|
|
||||||
|
const generateTOCLink = (path: string): string => {
|
||||||
|
const filepath = path.endsWith('/') ? `${path}README.md` : path.endsWith('.md') ? path : `${path || 'README'}.md`
|
||||||
|
const link = path.endsWith('/') ? `${path}index.html` : `${path}.html`
|
||||||
|
const page = llmPages.find((item) => {
|
||||||
|
return ensureLeadingSlash(item.filePathRelative || '') === filepath || link === item.path
|
||||||
|
})
|
||||||
|
|
||||||
|
if (page) {
|
||||||
|
usagePages.push(page)
|
||||||
|
return rawGenerateTOCLink(page, llmState)
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const processAutoSidebar = (prefix: string): string[] => {
|
||||||
|
const list: string[] = []
|
||||||
|
llmPages.forEach((page) => {
|
||||||
|
if (ensureLeadingSlash(page.filePathRelative || '').startsWith(prefix)) {
|
||||||
|
usagePages.push(page)
|
||||||
|
list.push(rawGenerateTOCLink(page, llmState))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return list.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
const processSidebar = (items: (string | ThemeSidebarItem)[], prefix: string): string[] => {
|
||||||
|
const result: string[] = []
|
||||||
|
items.forEach((item) => {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
result.push(generateTOCLink(normalizePath(prefix, item)))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (item.link) {
|
||||||
|
result.push(generateTOCLink(normalizePath(prefix, item.link)))
|
||||||
|
}
|
||||||
|
if (item.items === 'auto') {
|
||||||
|
result.push(...processAutoSidebar(normalizePath(prefix, item.prefix)))
|
||||||
|
}
|
||||||
|
else if (item.items?.length) {
|
||||||
|
result.push(...processSidebar(item.items, normalizePath(prefix, item.prefix)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collections
|
||||||
|
collections
|
||||||
|
.filter(collection => collection.type === 'doc')
|
||||||
|
.forEach(({ dir, title, sidebar = [] }) => {
|
||||||
|
tableOfContent += `### ${title}\n\n`
|
||||||
|
const prefix = normalizePath(ensureLeadingSlash(withBase(dir, currentLocale)))
|
||||||
|
if (sidebar === 'auto') {
|
||||||
|
tableOfContent += `${processAutoSidebar(prefix).join('')}\n`
|
||||||
|
}
|
||||||
|
else if (sidebar.length) {
|
||||||
|
const home = generateTOCLink(ensureEndingSlash(prefix))
|
||||||
|
const list = processSidebar(sidebar, prefix)
|
||||||
|
if (home && !list.includes(home)) {
|
||||||
|
list.unshift(home)
|
||||||
|
}
|
||||||
|
tableOfContent += `${list.join('')}\n`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Others
|
||||||
|
const unUsagePages = llmPages.filter(page => !usagePages.includes(page))
|
||||||
|
if (unUsagePages.length) {
|
||||||
|
tableOfContent += '### Others\n\n'
|
||||||
|
tableOfContent += unUsagePages
|
||||||
|
.map(page => rawGenerateTOCLink(page, llmState))
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableOfContent
|
||||||
|
}
|
||||||
|
|
||||||
|
return [rawLlmsPlugin({
|
||||||
|
...userLLMsTxt,
|
||||||
|
llmsTxtTemplateGetter: {
|
||||||
|
toc: tocGetter,
|
||||||
|
...llmsTxtTemplateGetter,
|
||||||
|
},
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
|
||||||
|
function genStarsWith(stars: string | undefined, locale: string) {
|
||||||
|
return (url: string): boolean => {
|
||||||
|
if (!stars)
|
||||||
|
return false
|
||||||
|
return url.startsWith(withBase(stars, locale))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePath(prefix: string, path = ''): string {
|
||||||
|
if (path.startsWith('/'))
|
||||||
|
return path
|
||||||
|
|
||||||
|
return `${ensureEndingSlash(prefix)}${path}`
|
||||||
|
}
|
||||||
@ -18,6 +18,7 @@ import { watermarkPlugin } from '@vuepress/plugin-watermark'
|
|||||||
import { getThemeConfig } from '../loadConfig/index.js'
|
import { getThemeConfig } from '../loadConfig/index.js'
|
||||||
import { codePlugins } from './code.js'
|
import { codePlugins } from './code.js'
|
||||||
import { gitPlugin } from './git.js'
|
import { gitPlugin } from './git.js'
|
||||||
|
import { llmsPlugin } from './llms.js'
|
||||||
import { markdownPlugins } from './markdown.js'
|
import { markdownPlugins } from './markdown.js'
|
||||||
|
|
||||||
export function setupPlugins(
|
export function setupPlugins(
|
||||||
@ -114,6 +115,11 @@ export function setupPlugins(
|
|||||||
plugins.push(replaceAssetsPlugin(replaceAssets))
|
plugins.push(replaceAssetsPlugin(replaceAssets))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const llmstxt = options.llmstxt ?? pluginOptions.llmstxt
|
||||||
|
if (llmstxt) {
|
||||||
|
plugins.push(...llmsPlugin(app, llmstxt))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 站点地图,仅在生产构建时,且 hostname 存在时生效
|
* 站点地图,仅在生产构建时,且 hostname 存在时生效
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -396,4 +396,14 @@ export interface ThemeLocaleText {
|
|||||||
* 加密时输入框的 placeholder
|
* 加密时输入框的 placeholder
|
||||||
*/
|
*/
|
||||||
encryptPlaceholder?: string
|
encryptPlaceholder?: string
|
||||||
|
|
||||||
|
copyPageText?: string
|
||||||
|
copiedPageText?: string
|
||||||
|
copingPageText?: string
|
||||||
|
copyTagline?: string
|
||||||
|
viewMarkdown?: string
|
||||||
|
viewMarkdownTagline?: string
|
||||||
|
askAIText?: string
|
||||||
|
askAITagline?: string
|
||||||
|
askAIMessage?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { CommentPluginOptions } from '@vuepress/plugin-comment'
|
import type { CommentPluginOptions } from '@vuepress/plugin-comment'
|
||||||
import type { CopyCodePluginOptions } from '@vuepress/plugin-copy-code'
|
import type { CopyCodePluginOptions } from '@vuepress/plugin-copy-code'
|
||||||
import type { ChangelogOptions, ContributorsOptions } from '@vuepress/plugin-git'
|
import type { ChangelogOptions, ContributorsOptions } from '@vuepress/plugin-git'
|
||||||
|
import type { LlmsPluginOptions } from '@vuepress/plugin-llms'
|
||||||
import type { ReadingTimePluginOptions } from '@vuepress/plugin-reading-time'
|
import type { ReadingTimePluginOptions } from '@vuepress/plugin-reading-time'
|
||||||
import type { ReplaceAssetsPluginOptions } from '@vuepress/plugin-replace-assets'
|
import type { ReplaceAssetsPluginOptions } from '@vuepress/plugin-replace-assets'
|
||||||
import type { ShikiPluginOptions } from '@vuepress/plugin-shiki'
|
import type { ShikiPluginOptions } from '@vuepress/plugin-shiki'
|
||||||
@ -94,7 +95,10 @@ export interface ThemeFeatureOptions {
|
|||||||
/**
|
/**
|
||||||
* 站点搜索配置
|
* 站点搜索配置
|
||||||
*
|
*
|
||||||
* @default { provider: 'local' }
|
* @default
|
||||||
|
* ```
|
||||||
|
* { provider: 'local' }
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
search?: boolean | SearchOptions
|
search?: boolean | SearchOptions
|
||||||
|
|
||||||
@ -132,4 +136,9 @@ export interface ThemeFeatureOptions {
|
|||||||
* 资源链接替换
|
* 资源链接替换
|
||||||
*/
|
*/
|
||||||
replaceAssets?: false | ReplaceAssetsPluginOptions
|
replaceAssets?: false | ReplaceAssetsPluginOptions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* llmstxt 配置
|
||||||
|
*/
|
||||||
|
llmstxt?: boolean | LlmsPluginOptions
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { CachePluginOptions } from '@vuepress/plugin-cache'
|
|||||||
import type { CommentPluginOptions } from '@vuepress/plugin-comment'
|
import type { CommentPluginOptions } from '@vuepress/plugin-comment'
|
||||||
import type { CopyCodePluginOptions } from '@vuepress/plugin-copy-code'
|
import type { CopyCodePluginOptions } from '@vuepress/plugin-copy-code'
|
||||||
import type { DocSearchOptions } from '@vuepress/plugin-docsearch'
|
import type { DocSearchOptions } from '@vuepress/plugin-docsearch'
|
||||||
|
import type { LlmsPluginOptions } from '@vuepress/plugin-llms'
|
||||||
import type { MarkdownChartPluginOptions } from '@vuepress/plugin-markdown-chart'
|
import type { MarkdownChartPluginOptions } from '@vuepress/plugin-markdown-chart'
|
||||||
import type { MarkdownImagePluginOptions } from '@vuepress/plugin-markdown-image'
|
import type { MarkdownImagePluginOptions } from '@vuepress/plugin-markdown-image'
|
||||||
import type { MarkdownIncludePluginOptions } from '@vuepress/plugin-markdown-include'
|
import type { MarkdownIncludePluginOptions } from '@vuepress/plugin-markdown-include'
|
||||||
@ -130,4 +131,9 @@ export interface ThemeBuiltinPlugins {
|
|||||||
* 资源链接替换
|
* 资源链接替换
|
||||||
*/
|
*/
|
||||||
replaceAssets?: false | ReplaceAssetsPluginOptions
|
replaceAssets?: false | ReplaceAssetsPluginOptions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* llmstxt 配置
|
||||||
|
*/
|
||||||
|
llmstxt?: boolean | LlmsPluginOptions
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user