perf: improve markdown container plugin (#320)
* perf: improve markdown container plugin * chore: add unit test * chore: improve styles
This commit is contained in:
parent
611f625185
commit
30d707036e
@ -0,0 +1,33 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`demoWrapperPlugin > should work 1`] = `
|
||||
"<div class="demo-wrapper">
|
||||
<div class="demo-head">
|
||||
<div class="demo-ctrl"><i></i><i></i><i></i></div>
|
||||
|
||||
</div>
|
||||
<div class="demo-container" >
|
||||
<p>content</p>
|
||||
</div></div><div class="demo-wrapper has-title">
|
||||
<div class="demo-head">
|
||||
<div class="demo-ctrl"><i></i><i></i><i></i></div>
|
||||
<h4 class="demo-title"><p>test</p></h4>
|
||||
</div>
|
||||
<div class="demo-container" >
|
||||
<p>content</p>
|
||||
</div></div><div class="demo-wrapper only-img no-padding has-height">
|
||||
<div class="demo-head">
|
||||
<div class="demo-ctrl"><i></i><i></i><i></i></div>
|
||||
|
||||
</div>
|
||||
<div class="demo-container" style="--demo-container-height: 100px;">
|
||||
<p><a href="/img.jpg">xxx</a></p>
|
||||
</div></div><div class="demo-wrapper only-img no-padding has-height">
|
||||
<div class="demo-head">
|
||||
<div class="demo-ctrl"><i></i><i></i><i></i></div>
|
||||
|
||||
</div>
|
||||
<div class="demo-container" style="--demo-container-height: 100px;">
|
||||
<p><a href="/img.jpg">xxx</a></p>
|
||||
</div></div>"
|
||||
`;
|
||||
@ -0,0 +1,22 @@
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createContainerPlugin } from '../src/node/container/createContainer.js'
|
||||
|
||||
describe('createContainerPlugin', () => {
|
||||
it('should work with default options', () => {
|
||||
const md = new MarkdownIt()
|
||||
createContainerPlugin(md, 'test')
|
||||
|
||||
expect(md.render(':::test\ncontent\n:::')).toContain('class="custom-container test"')
|
||||
})
|
||||
|
||||
it('should work with custom render', () => {
|
||||
const md = new MarkdownIt()
|
||||
createContainerPlugin(md, 'test', {
|
||||
before: () => `<div class="test">`,
|
||||
after: () => `</div>`,
|
||||
})
|
||||
|
||||
expect(md.render(':::test\ncontent\n:::')).toContain('class="test"')
|
||||
})
|
||||
})
|
||||
29
plugins/plugin-md-power/__test__/demoWrapperPlugin.spec.ts
Normal file
29
plugins/plugin-md-power/__test__/demoWrapperPlugin.spec.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { demoWrapperPlugin } from '../src/node/container/demoWrapper.js'
|
||||
|
||||
describe('demoWrapperPlugin', () => {
|
||||
const md = new MarkdownIt().use(demoWrapperPlugin)
|
||||
it('should work', () => {
|
||||
const code = `\
|
||||
::: demo-wrapper
|
||||
content
|
||||
:::
|
||||
|
||||
::: demo-wrapper title="test"
|
||||
content
|
||||
:::
|
||||
|
||||
::: demo-wrapper no-padding img height="100px"
|
||||
|
||||
[xxx](/img.jpg)
|
||||
:::
|
||||
|
||||
::: demo-wrapper no-padding img height="100"
|
||||
|
||||
[xxx](/img.jpg)
|
||||
:::
|
||||
`
|
||||
expect(md.render(code)).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
@ -1,4 +1,3 @@
|
||||
/* --------------------- demo-wrapper ------------------------ */
|
||||
.vp-doc .demo-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -139,74 +138,3 @@
|
||||
margin: 40px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------- Steps --------------------------- */
|
||||
.vp-doc .vp-steps {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .vp-steps > ol,
|
||||
.vp-doc .vp-steps > ul {
|
||||
padding-inline-start: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.vp-doc .vp-steps > ol > li,
|
||||
.vp-doc .vp-steps > ul > li {
|
||||
position: relative;
|
||||
min-height: 22px;
|
||||
padding-bottom: 1px;
|
||||
padding-left: 44px;
|
||||
}
|
||||
|
||||
.vp-doc .vp-steps > ol > li::before,
|
||||
.vp-doc .vp-steps > ul > li::before {
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
top: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 28px;
|
||||
color: var(--vp-c-text-1);
|
||||
text-align: center;
|
||||
content: counter(list-item);
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
border: solid 1px var(--vp-c-divider);
|
||||
border-radius: 100%;
|
||||
transition: var(--vp-t-color);
|
||||
transition-property: color, background-color, border-color;
|
||||
}
|
||||
|
||||
.vp-doc .vp-steps > ol > li:not(:last-of-type)::after,
|
||||
.vp-doc .vp-steps > ul > li:not(:last-of-type)::after {
|
||||
position: absolute;
|
||||
inset-inline-start: 14px;
|
||||
top: 34px;
|
||||
bottom: 5px;
|
||||
width: 1px;
|
||||
content: "";
|
||||
background-color: var(--vp-c-divider);
|
||||
transition: background-color var(--vp-t-color);
|
||||
}
|
||||
|
||||
.vp-doc .vp-steps > ol > li > :first-child,
|
||||
.vp-doc .vp-steps > ul > li > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.vp-doc .vp-steps > ol > li > :first-child:where(h1,h2,h3,h4,h5,h6),
|
||||
.vp-doc .vp-steps > ul > li > :first-child:where(h1,h2,h3,h4,h5,h6) {
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.vp-doc .vp-steps > ol > li > :first-child:where(p) {
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.vp-doc .vp-steps > ol > li + li,
|
||||
.vp-doc .vp-steps > ul > li + li {
|
||||
margin-top: 1px;
|
||||
}
|
||||
4
plugins/plugin-md-power/src/client/styles/index.css
Normal file
4
plugins/plugin-md-power/src/client/styles/index.css
Normal file
@ -0,0 +1,4 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
@import url("./demo-wrapper.css");
|
||||
@import url("./steps.css");
|
||||
69
plugins/plugin-md-power/src/client/styles/steps.css
Normal file
69
plugins/plugin-md-power/src/client/styles/steps.css
Normal file
@ -0,0 +1,69 @@
|
||||
.vp-doc .vp-steps {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .vp-steps > ol,
|
||||
.vp-doc .vp-steps > ul {
|
||||
padding-inline-start: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.vp-doc .vp-steps > ol > li,
|
||||
.vp-doc .vp-steps > ul > li {
|
||||
position: relative;
|
||||
min-height: 22px;
|
||||
padding-bottom: 1px;
|
||||
padding-left: 44px;
|
||||
}
|
||||
|
||||
.vp-doc .vp-steps > ol > li::before,
|
||||
.vp-doc .vp-steps > ul > li::before {
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
top: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 28px;
|
||||
color: var(--vp-c-text-1);
|
||||
text-align: center;
|
||||
content: counter(list-item);
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
border: solid 1px var(--vp-c-divider);
|
||||
border-radius: 100%;
|
||||
transition: var(--vp-t-color);
|
||||
transition-property: color, background-color, border-color;
|
||||
}
|
||||
|
||||
.vp-doc .vp-steps > ol > li:not(:last-of-type)::after,
|
||||
.vp-doc .vp-steps > ul > li:not(:last-of-type)::after {
|
||||
position: absolute;
|
||||
inset-inline-start: 14px;
|
||||
top: 34px;
|
||||
bottom: 5px;
|
||||
width: 1px;
|
||||
content: "";
|
||||
background-color: var(--vp-c-divider);
|
||||
transition: background-color var(--vp-t-color);
|
||||
}
|
||||
|
||||
.vp-doc .vp-steps > ol > li > :first-child,
|
||||
.vp-doc .vp-steps > ul > li > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.vp-doc .vp-steps > ol > li > :first-child:where(h1,h2,h3,h4,h5,h6),
|
||||
.vp-doc .vp-steps > ul > li > :first-child:where(h1,h2,h3,h4,h5,h6) {
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.vp-doc .vp-steps > ol > li > :first-child:where(p) {
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.vp-doc .vp-steps > ol > li + li,
|
||||
.vp-doc .vp-steps > ul > li + li {
|
||||
margin-top: 1px;
|
||||
}
|
||||
@ -1,21 +1,12 @@
|
||||
import type Token from 'markdown-it/lib/token.mjs'
|
||||
import type { Markdown } from 'vuepress/markdown'
|
||||
import container from 'markdown-it-container'
|
||||
import { createContainerPlugin } from './createContainer.js'
|
||||
|
||||
const alignList = ['left', 'center', 'right', 'justify']
|
||||
|
||||
export function alignPlugin(md: Markdown): void {
|
||||
for (const name of alignList) {
|
||||
md.use(container, name, {
|
||||
validate: (info: string) => info.trim() === name,
|
||||
render: (tokens: Token[], idx: number): string => {
|
||||
if (tokens[idx].nesting === 1) {
|
||||
return `<div style="text-align:${name}">`
|
||||
}
|
||||
else {
|
||||
return '</div>'
|
||||
}
|
||||
},
|
||||
createContainerPlugin(md, name, {
|
||||
before: () => `<div style="text-align:${name}">`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
39
plugins/plugin-md-power/src/node/container/card.ts
Normal file
39
plugins/plugin-md-power/src/node/container/card.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import type { Markdown } from 'vuepress/markdown'
|
||||
import { resolveAttrs } from '.././utils/resolveAttrs.js'
|
||||
import { createContainerPlugin } from './createContainer.js'
|
||||
|
||||
interface CardAttrs {
|
||||
title?: string
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export function cardPlugin(md: Markdown) {
|
||||
/**
|
||||
* ::: card title="xxx" icon="xxx"
|
||||
* xxx
|
||||
* :::
|
||||
*/
|
||||
createContainerPlugin(md, 'card', {
|
||||
before(info) {
|
||||
const { attrs } = resolveAttrs<CardAttrs>(info)
|
||||
const { title, icon } = attrs
|
||||
return `<VPCard${title ? ` title="${title}"` : ''}${icon ? ` icon="${icon}"` : ''}>`
|
||||
},
|
||||
after: () => '</VPCard>',
|
||||
})
|
||||
|
||||
/**
|
||||
* :::: card-grid
|
||||
* ::: card
|
||||
* xxx
|
||||
* :::
|
||||
* ::: card
|
||||
* xxx
|
||||
* :::
|
||||
* ::::
|
||||
*/
|
||||
createContainerPlugin(md, 'card-grid', {
|
||||
before: () => '<VPCardGrid>',
|
||||
after: () => '</VPCardGrid>',
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import type Token from 'markdown-it/lib/token.mjs'
|
||||
import type { Markdown } from 'vuepress/markdown'
|
||||
import container from 'markdown-it-container'
|
||||
|
||||
export interface ContainerOptions {
|
||||
before?: (info: string, tokens: Token[], idx: number) => string
|
||||
after?: (info: string, tokens: Token[], idx: number) => string
|
||||
}
|
||||
|
||||
export function createContainerPlugin(md: Markdown, type: string, options: ContainerOptions = {}) {
|
||||
const render = (tokens: Token[], index: number): string => {
|
||||
const token = tokens[index]
|
||||
const info = token.info.trim().slice(type.length).trim() || ''
|
||||
if (token.nesting === 1) {
|
||||
return options.before?.(info, tokens, index) || `<div class="custom-container ${type}">`
|
||||
}
|
||||
else {
|
||||
return options.after?.(info, tokens, index) || '</div>'
|
||||
}
|
||||
}
|
||||
|
||||
md.use(container, type, { render })
|
||||
}
|
||||
46
plugins/plugin-md-power/src/node/container/demoWrapper.ts
Normal file
46
plugins/plugin-md-power/src/node/container/demoWrapper.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import type { Markdown } from 'vuepress/markdown'
|
||||
import { resolveAttrs } from '.././utils/resolveAttrs.js'
|
||||
import { createContainerPlugin } from './createContainer.js'
|
||||
|
||||
interface DemoWrapperAttrs {
|
||||
title?: string
|
||||
img?: string
|
||||
noPadding?: boolean
|
||||
height?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* :::demo-wrapper img no-padding title="xxx" height="100px"
|
||||
* :::
|
||||
*/
|
||||
export function demoWrapperPlugin(md: Markdown): void {
|
||||
createContainerPlugin(md, 'demo-wrapper', {
|
||||
before: (info: string) => {
|
||||
const { attrs } = resolveAttrs<DemoWrapperAttrs>(info)
|
||||
const wrapperClasses: string[] = ['demo-wrapper']
|
||||
let containerStyle = ''
|
||||
if (attrs.title)
|
||||
wrapperClasses.push('has-title')
|
||||
|
||||
if (attrs.img)
|
||||
wrapperClasses.push('only-img')
|
||||
|
||||
if (attrs.noPadding)
|
||||
wrapperClasses.push('no-padding')
|
||||
|
||||
if (attrs.height) {
|
||||
const h = Number.parseFloat(attrs.height) === Number(attrs.height) ? `${attrs.height}px` : attrs.height
|
||||
containerStyle += `--demo-container-height: ${h};`
|
||||
wrapperClasses.push('has-height')
|
||||
}
|
||||
|
||||
return `<div class="${wrapperClasses.join(' ')}">
|
||||
<div class="demo-head">
|
||||
<div class="demo-ctrl"><i></i><i></i><i></i></div>
|
||||
${attrs.title ? `<h4 class="demo-title"><p>${attrs.title}</p></h4>` : ''}
|
||||
</div>
|
||||
<div class="demo-container" ${containerStyle ? `style="${containerStyle}"` : ''}>\n`
|
||||
},
|
||||
after: () => '</div></div>',
|
||||
})
|
||||
}
|
||||
@ -33,8 +33,6 @@ export function fileTreePlugin(md: Markdown, options: FileTreeOptions = {}) {
|
||||
return getFileIcon(filename, type)
|
||||
}
|
||||
|
||||
const validate = (info: string): boolean => info.trim().startsWith(type)
|
||||
|
||||
const render = (tokens: Token[], idx: number): string => {
|
||||
const { attrs } = resolveAttrs<FileTreeAttrs>(tokens[idx].info.slice(type.length - 1))
|
||||
|
||||
@ -81,7 +79,7 @@ export function fileTreePlugin(md: Markdown, options: FileTreeOptions = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
md.use(container, type, { validate, render })
|
||||
md.use(container, type, { render })
|
||||
}
|
||||
|
||||
export function resolveTreeNodeInfo(
|
||||
|
||||
@ -3,10 +3,13 @@ import type { Markdown } from 'vuepress/markdown'
|
||||
import type { MarkdownPowerPluginOptions } from '../../shared/index.js'
|
||||
import { isPlainObject } from '@vuepress/helper'
|
||||
import { alignPlugin } from './align.js'
|
||||
import { cardPlugin } from './card.js'
|
||||
import { codeTabs } from './codeTabs.js'
|
||||
import { demoWrapperPlugin } from './demoWrapper.js'
|
||||
import { fileTreePlugin } from './fileTree.js'
|
||||
import { langReplPlugin } from './langRepl.js'
|
||||
import { npmToPlugins } from './npmTo.js'
|
||||
import { stepsPlugin } from './steps.js'
|
||||
import { tabs } from './tabs.js'
|
||||
|
||||
export async function containerPlugin(
|
||||
@ -21,11 +24,22 @@ export async function containerPlugin(
|
||||
// ::: code-tabs
|
||||
codeTabs(md, options.codeTabs)
|
||||
|
||||
// ::: demo-wrapper
|
||||
demoWrapperPlugin(md)
|
||||
|
||||
// ::: steps
|
||||
stepsPlugin(md)
|
||||
|
||||
// ::: card / card-grid
|
||||
cardPlugin(md)
|
||||
|
||||
if (options.npmTo) {
|
||||
// ::: npm-to
|
||||
npmToPlugins(md, typeof options.npmTo === 'boolean' ? {} : options.npmTo)
|
||||
}
|
||||
|
||||
if (options.repl)
|
||||
// ::: rust-repl / go-repl / kotlin-repl
|
||||
await langReplPlugin(app, md, options.repl)
|
||||
|
||||
if (options.fileTree) {
|
||||
|
||||
@ -191,7 +191,6 @@ const MANAGERS_CONFIG: CommandConfigs = {
|
||||
|
||||
export function npmToPlugins(md: Markdown, options: NpmToOptions = {}): void {
|
||||
const type = 'npm-to'
|
||||
const validate = (info: string): boolean => info.trim().startsWith(type)
|
||||
|
||||
const opt = isArray(options) ? { tabs: options } : options
|
||||
const defaultTabs = opt.tabs?.length ? opt.tabs : DEFAULT_TABS
|
||||
@ -214,7 +213,7 @@ export function npmToPlugins(md: Markdown, options: NpmToOptions = {}): void {
|
||||
return ''
|
||||
}
|
||||
|
||||
md.use(container, type, { validate, render })
|
||||
md.use(container, type, { render })
|
||||
}
|
||||
|
||||
function resolveNpmTo(lines: string[], info: string, idx: number, tabs: NpmToPackageManager[]): string {
|
||||
|
||||
17
plugins/plugin-md-power/src/node/container/steps.ts
Normal file
17
plugins/plugin-md-power/src/node/container/steps.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import type { Markdown } from 'vuepress/markdown'
|
||||
import { createContainerPlugin } from './createContainer.js'
|
||||
|
||||
/**
|
||||
* :::steps
|
||||
* 1. 步骤 1
|
||||
* xxx
|
||||
* 2. 步骤 2
|
||||
* xxx
|
||||
* 3. ...
|
||||
* :::
|
||||
*/
|
||||
export function stepsPlugin(md: Markdown) {
|
||||
createContainerPlugin(md, 'steps', {
|
||||
before: () => '<div class="vp-steps">',
|
||||
})
|
||||
}
|
||||
45
plugins/plugin-md-power/src/node/enhance/docsTitle.ts
Normal file
45
plugins/plugin-md-power/src/node/enhance/docsTitle.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import type { Markdown, MarkdownEnv } from 'vuepress/markdown'
|
||||
|
||||
const REG_HEADING = /^#\s*?([^#\s].*)?\n/
|
||||
|
||||
/**
|
||||
* 适配 主题的 文档页面标题,将 markdown 中的 h1 标题提取到 frontmatter 中,并将其删除,
|
||||
* 以避免重复显示标题。
|
||||
*/
|
||||
export function docsTitlePlugin(md: Markdown): void {
|
||||
const render = md.render
|
||||
md.render = (source, env: MarkdownEnv) => {
|
||||
if (!env.filePathRelative)
|
||||
return render(source, env)
|
||||
|
||||
let { matter, content } = parseSource(source.trim())
|
||||
let title = ''
|
||||
content = content.trim().replace(REG_HEADING, (_, match) => {
|
||||
title = match.trim()
|
||||
return ''
|
||||
})
|
||||
source = `${matter}\n${content}`
|
||||
const result = render(source, env)
|
||||
if (title) {
|
||||
env.frontmatter ??= {}
|
||||
env.frontmatter.title ??= title
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
function parseSource(source: string) {
|
||||
const char = '---'
|
||||
|
||||
if (!source.startsWith(char)) {
|
||||
return { matter: '', content: source }
|
||||
}
|
||||
else {
|
||||
const end = source.indexOf(`\n${char}`)
|
||||
const len = char.length + 1
|
||||
return {
|
||||
matter: source.slice(0, end + len),
|
||||
content: source.slice(end + len),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ import type { MarkdownPowerPluginOptions } from '../shared/index.js'
|
||||
import { addViteOptimizeDepsInclude } from '@vuepress/helper'
|
||||
import { containerPlugin } from './container/index.js'
|
||||
import { embedSyntaxPlugin } from './embed/index.js'
|
||||
import { docsTitlePlugin } from './enhance/docsTitle.js'
|
||||
import { imageSizePlugin } from './enhance/imageSize.js'
|
||||
import { inlineSyntaxPlugin } from './inline/index.js'
|
||||
import { prepareConfigFile } from './prepareConfigFile.js'
|
||||
@ -21,11 +22,16 @@ export function markdownPowerPlugin(
|
||||
|
||||
extendsBundlerOptions(bundlerOptions, app) {
|
||||
if (options.repl) {
|
||||
addViteOptimizeDepsInclude(bundlerOptions, app, ['shiki/core', 'shiki/wasm', 'shiki/engine/oniguruma'])
|
||||
addViteOptimizeDepsInclude(
|
||||
bundlerOptions,
|
||||
app,
|
||||
['shiki/core', 'shiki/wasm', 'shiki/engine/oniguruma'],
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
extendsMarkdown: async (md, app) => {
|
||||
docsTitlePlugin(md)
|
||||
embedSyntaxPlugin(md, options)
|
||||
inlineSyntaxPlugin(md, options)
|
||||
|
||||
|
||||
@ -71,6 +71,8 @@ export async function prepareConfigFile(app: App, options: MarkdownPowerPluginOp
|
||||
import { defineClientConfig } from 'vuepress/client'
|
||||
${Array.from(imports.values()).join('\n')}
|
||||
|
||||
import '${CLIENT_FOLDER}styles/index.css'
|
||||
|
||||
export default defineClientConfig({
|
||||
enhance({ router, app }) {
|
||||
${Array.from(enhances.values())
|
||||
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@ -361,9 +361,6 @@ importers:
|
||||
'@vuepress/plugin-git':
|
||||
specifier: 2.0.0-rc.56
|
||||
version: 2.0.0-rc.56(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@20.12.10)(jiti@1.21.6)(sass-embedded@1.80.3)(sass@1.80.3)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))
|
||||
'@vuepress/plugin-markdown-container':
|
||||
specifier: 2.0.0-rc.54
|
||||
version: 2.0.0-rc.54(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@20.12.10)(jiti@1.21.6)(sass-embedded@1.80.3)(sass@1.80.3)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))
|
||||
'@vuepress/plugin-markdown-hint':
|
||||
specifier: 2.0.0-rc.56
|
||||
version: 2.0.0-rc.56(markdown-it@14.1.0)(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@20.12.10)(jiti@1.21.6)(sass-embedded@1.80.3)(sass@1.80.3)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))
|
||||
@ -2166,11 +2163,6 @@ packages:
|
||||
peerDependencies:
|
||||
vuepress: 2.0.0-rc.18
|
||||
|
||||
'@vuepress/plugin-markdown-container@2.0.0-rc.54':
|
||||
resolution: {integrity: sha512-00TzBHfBDd6nbZlmVRgWdmLb1MFcCg22FcD39n6gwgcFym9C/oZMDcdPnxsRT+sKPGqtxrlJYxKOXdp3Z8OJCA==}
|
||||
peerDependencies:
|
||||
vuepress: 2.0.0-rc.18
|
||||
|
||||
'@vuepress/plugin-markdown-hint@2.0.0-rc.56':
|
||||
resolution: {integrity: sha512-qVOlqBIMjySormRde0uo/rILIC8BP59GIz+lRk8XpO5G92ejmJlRck27Pjrzm5NngR+pOonWfZ7yjGtT35U6nA==}
|
||||
peerDependencies:
|
||||
@ -8192,12 +8184,6 @@ snapshots:
|
||||
execa: 9.4.1
|
||||
vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@20.12.10)(jiti@1.21.6)(sass-embedded@1.80.3)(sass@1.80.3)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))
|
||||
|
||||
'@vuepress/plugin-markdown-container@2.0.0-rc.54(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@20.12.10)(jiti@1.21.6)(sass-embedded@1.80.3)(sass@1.80.3)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))':
|
||||
dependencies:
|
||||
'@types/markdown-it': 14.1.2
|
||||
markdown-it-container: 4.0.0
|
||||
vuepress: 2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@20.12.10)(jiti@1.21.6)(sass-embedded@1.80.3)(sass@1.80.3)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))
|
||||
|
||||
'@vuepress/plugin-markdown-hint@2.0.0-rc.56(markdown-it@14.1.0)(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3))(vuepress@2.0.0-rc.18(@vuepress/bundler-vite@2.0.0-rc.18(@types/node@20.12.10)(jiti@1.21.6)(sass-embedded@1.80.3)(sass@1.80.3)(typescript@5.6.3)(yaml@2.5.1))(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)))':
|
||||
dependencies:
|
||||
'@mdit/plugin-alert': 0.13.1(markdown-it@14.1.0)
|
||||
|
||||
@ -103,7 +103,6 @@
|
||||
"@vuepress/plugin-comment": "2.0.0-rc.56",
|
||||
"@vuepress/plugin-docsearch": "2.0.0-rc.56",
|
||||
"@vuepress/plugin-git": "2.0.0-rc.56",
|
||||
"@vuepress/plugin-markdown-container": "2.0.0-rc.54",
|
||||
"@vuepress/plugin-markdown-hint": "2.0.0-rc.56",
|
||||
"@vuepress/plugin-markdown-image": "2.0.0-rc.56",
|
||||
"@vuepress/plugin-markdown-math": "2.0.0-rc.56",
|
||||
|
||||
@ -8,7 +8,6 @@
|
||||
@import url("./utils.css");
|
||||
@import url("./content.css");
|
||||
@import url("./code.css");
|
||||
@import url("./custom-block.css");
|
||||
@import url("./hint-container.css");
|
||||
@import url("./twoslash.css");
|
||||
@import url("./md-enhance.css");
|
||||
|
||||
@ -1,109 +0,0 @@
|
||||
import type { Plugin } from 'vuepress/core'
|
||||
import { markdownContainerPlugin as containerPlugin } from '@vuepress/plugin-markdown-container'
|
||||
|
||||
export const customContainerPlugins: Plugin[] = [
|
||||
/**
|
||||
* :::demo-wrapper img no-padding title="xxx" height="100px"
|
||||
* :::
|
||||
*/
|
||||
containerPlugin({
|
||||
type: 'demo-wrapper',
|
||||
before(info) {
|
||||
const title = resolveAttr(info, 'title')
|
||||
const wrapperClasses: string[] = ['demo-wrapper']
|
||||
let containerStyle = ''
|
||||
if (title)
|
||||
wrapperClasses.push('has-title')
|
||||
|
||||
if (info.includes('img'))
|
||||
wrapperClasses.push('only-img')
|
||||
|
||||
if (info.includes('no-padding'))
|
||||
wrapperClasses.push('no-padding')
|
||||
|
||||
const height = resolveAttr(info, 'height')
|
||||
if (height) {
|
||||
const h = Number.parseFloat(height) === Number(height) ? `${height}px` : height
|
||||
containerStyle += `--demo-container-height: ${h};`
|
||||
wrapperClasses.push('has-height')
|
||||
}
|
||||
|
||||
return `<div class="${wrapperClasses.join(' ')}">
|
||||
<div class="demo-head">
|
||||
<div class="demo-ctrl"><i></i><i></i><i></i></div>
|
||||
${title ? `<h4 class="demo-title"><p>${title}</p></h4>` : ''}
|
||||
</div>
|
||||
<div class="demo-container" ${containerStyle ? `style="${containerStyle}"` : ''}>\n`
|
||||
},
|
||||
after() {
|
||||
return '</div></div>'
|
||||
},
|
||||
}),
|
||||
/**
|
||||
* :::steps
|
||||
* 1. 步骤 1
|
||||
* xxx
|
||||
* 2. 步骤 2
|
||||
* xxx
|
||||
* 3. ...
|
||||
* :::
|
||||
*/
|
||||
containerPlugin({
|
||||
type: 'steps',
|
||||
before() {
|
||||
return '<div class="vp-steps">'
|
||||
},
|
||||
after() {
|
||||
return '</div>'
|
||||
},
|
||||
}),
|
||||
/**
|
||||
* ::: card title="xxx" icon="xxx"
|
||||
* xxx
|
||||
* :::
|
||||
*/
|
||||
containerPlugin({
|
||||
type: 'card',
|
||||
before(info) {
|
||||
const title = resolveAttr(info, 'title')
|
||||
const icon = resolveAttr(info, 'icon')
|
||||
return `<VPCard${title ? ` title="${title}"` : ''}${icon ? ` icon="${icon}"` : ''}>`
|
||||
},
|
||||
after() {
|
||||
return '</VPCard>'
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* :::: card-grid
|
||||
* ::: card
|
||||
* xxx
|
||||
* :::
|
||||
* ::: card
|
||||
* xxx
|
||||
* :::
|
||||
* ::::
|
||||
*/
|
||||
containerPlugin({
|
||||
type: 'card-grid',
|
||||
before() {
|
||||
return '<VPCardGrid>'
|
||||
},
|
||||
after() {
|
||||
return '</VPCardGrid>'
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
/**
|
||||
* Resolve the specified attribute from token info
|
||||
*/
|
||||
function resolveAttr(info: string, attr: string): string | null {
|
||||
// try to match specified attr mark
|
||||
const pattern = `\\b${attr}\\s*=\\s*(?<quote>['"])(?<content>.+?)\\k<quote>(\\s|$)`
|
||||
const regex = new RegExp(pattern, 'i')
|
||||
const match = info.match(regex)
|
||||
|
||||
// return content if matched, null if not specified
|
||||
return match?.groups?.content ?? null
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import type { App, PluginConfig } from 'vuepress/core'
|
||||
import type { PlumeThemePluginOptions } from '../../shared/index.js'
|
||||
import { isPlainObject } from '@vuepress/helper'
|
||||
import { cachePlugin } from '@vuepress/plugin-cache'
|
||||
import { commentPlugin } from '@vuepress/plugin-comment'
|
||||
import { docsearchPlugin } from '@vuepress/plugin-docsearch'
|
||||
@ -21,8 +22,6 @@ import { type MarkdownEnhancePluginOptions, mdEnhancePlugin } from 'vuepress-plu
|
||||
import { markdownPowerPlugin } from 'vuepress-plugin-md-power'
|
||||
import { resolveDocsearchOptions, resolveSearchOptions } from '../config/index.js'
|
||||
import { deleteAttrs } from '../utils/index.js'
|
||||
import { customContainerPlugins } from './containerPlugins.js'
|
||||
import { markdownTitlePlugin } from './markdown-title.js'
|
||||
|
||||
export interface SetupPluginOptions {
|
||||
app: App
|
||||
@ -40,12 +39,9 @@ export function getPlugins({
|
||||
const isProd = app.env.isBuild
|
||||
|
||||
const plugins: PluginConfig = [
|
||||
markdownTitlePlugin(),
|
||||
fontsPlugin(),
|
||||
contentUpdatePlugin(),
|
||||
markdownHintPlugin({ hint: true, alert: true, injectStyles: false }),
|
||||
|
||||
...customContainerPlugins,
|
||||
]
|
||||
|
||||
if (pluginOptions.readingTime !== false) {
|
||||
@ -132,7 +128,7 @@ export function getPlugins({
|
||||
plugins.push(watermarkPlugin({
|
||||
delay: 300,
|
||||
enabled: true,
|
||||
...typeof pluginOptions.watermark === 'object' ? pluginOptions.watermark : {},
|
||||
...isPlainObject(pluginOptions.watermark) ? pluginOptions.watermark : {},
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@ -1,2 +1 @@
|
||||
export * from './containerPlugins.js'
|
||||
export * from './getPlugins.js'
|
||||
|
||||
@ -1,48 +0,0 @@
|
||||
import type { Plugin } from 'vuepress/core'
|
||||
import type { MarkdownEnv } from 'vuepress/markdown'
|
||||
|
||||
const REG_HEADING = /^#\s*?([^#\s].*)?\n/
|
||||
|
||||
export function markdownTitlePlugin(): Plugin {
|
||||
return {
|
||||
name: '@vuepress-plume/plugin-markdown-title',
|
||||
|
||||
extendsMarkdown(md) {
|
||||
const render = md.render
|
||||
md.render = (source, env: MarkdownEnv) => {
|
||||
if (!env.filePathRelative)
|
||||
return render(source, env)
|
||||
|
||||
let { matter, content } = parseSource(source.trim())
|
||||
let title = ''
|
||||
content = content.trim().replace(REG_HEADING, (_, match) => {
|
||||
title = match.trim()
|
||||
return ''
|
||||
})
|
||||
source = `${matter}\n${content}`
|
||||
const result = render(source, env)
|
||||
if (title) {
|
||||
env.frontmatter ??= {}
|
||||
env.frontmatter.title ??= title
|
||||
}
|
||||
return result
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function parseSource(source: string) {
|
||||
const char = '---'
|
||||
|
||||
if (!source.startsWith(char)) {
|
||||
return { matter: '', content: source }
|
||||
}
|
||||
else {
|
||||
const end = source.indexOf(`\n${char}`)
|
||||
const len = char.length + 1
|
||||
return {
|
||||
matter: source.slice(0, end + len),
|
||||
content: source.slice(end + len),
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user