refactor(md): with @mdit-vue plugins & ussOutline by document.querySelector

This commit is contained in:
YunYouJun 2022-10-03 21:46:54 +08:00
parent 6d9bff1127
commit 49bbdb3fe1
26 changed files with 583 additions and 248 deletions

View File

@ -7,7 +7,7 @@
"build": "npm run build:ssg && npm run rss",
"build:spa": "valaxy build",
"build:ssg": "valaxy build --ssg",
"dev": "nodemon -w \"../../packages/valaxy/dist/*.js\" --exec \"valaxy .\"",
"dev": "nodemon -w \"../../packages/valaxy/dist/*.cjs\" --exec \"valaxy .\"",
"rss": "valaxy rss",
"serve": "vite preview"
},

View File

@ -5,7 +5,7 @@
"build": "npm run build:ssg && npm run rss",
"build:spa": "valaxy build",
"build:ssg": "valaxy build --ssg",
"dev": "nodemon -w \"../packages/valaxy/dist/*.js\" --exec \"valaxy .\"",
"dev": "nodemon -w \"../packages/valaxy/dist/*.cjs\" --exec \"valaxy .\"",
"rss": "valaxy rss",
"serve": "vite preview"
},

View File

@ -10,9 +10,9 @@ const { t } = useI18n()
<ul>
<li>
<a href="/docs" :title="t('docs.view_docs')">
<router-link to="/docs" :title="t('docs.view_docs')">
{{ t('docs.view_docs') }}
</a>
</router-link>
</li>
<li>
<router-link class="flex justify-center" to="/examples">

View File

@ -64,7 +64,9 @@ const prevPost = computed(() => posts.value[findCurrentIndex() + 1])
Next Article
</h2>
<div class="link">
<a :href="nextPost.href">{{ nextPost.title }}</a>
<router-link :to="nextPost.href">
{{ nextPost.title }}
</router-link>
</div>
</div>
<div v-if="prevPost && prevPost.href" class="py-8">
@ -72,11 +74,15 @@ const prevPost = computed(() => posts.value[findCurrentIndex() + 1])
Previous Article
</h2>
<div class="link">
<a :href="prevPost.href">{{ prevPost.title }}</a>
<router-link :to="prevPost.href">
{{ prevPost.title }}
</router-link>
</div>
</div>
<div class="pt-8">
<a class="link" href="/"> Back to Home</a>
<router-link class="link" to="/">
Back to Home
</router-link>
</div>
</footer>
</div>

View File

@ -1,10 +1,10 @@
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import { useData, useFrontmatter } from 'valaxy'
import { useFrontmatter } from 'valaxy'
import { useAppStore } from 'valaxy/client/stores/app'
import { provide, ref } from 'vue'
const frontmatter = useFrontmatter()
const data = useData()
const { t } = useI18n()
const app = useAppStore()
</script>
@ -27,7 +27,7 @@ const app = useAppStore()
{{ t('sidebar.toc') }}
</h2>
<YunToc v-if="frontmatter.toc !== false" :headers="data.headers || []" />
<YunOutline v-if="frontmatter.toc !== false" />
<div class="flex-grow" />

View File

@ -1,28 +1,19 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import type { Header } from 'valaxy'
import { ref } from 'vue'
import {
resolveHeaders,
useActiveAnchor,
useFrontmatter,
useOutline,
} from 'valaxy'
import { useThemeConfig } from '../composables'
const props = defineProps<{ headers: Header[] }>()
const frontmatter = useFrontmatter()
const themeConfig = useThemeConfig()
const { locale } = useI18n()
const container = ref()
const marker = ref()
useActiveAnchor(container, marker)
const resolvedHeaders = computed(() => {
return resolveHeaders(props.headers || [])
})
const { headers } = useOutline()
function handleClick({ target: el }: Event) {
const id = `#${(el as HTMLAnchorElement).href!.split('#')[1]}`
@ -45,26 +36,12 @@ function handleClick({ target: el }: Event) {
Table of Contents for current page
</span>
<ul class="va-toc relative z-1">
<li
v-for="{ text, link, children, hidden, lang } in resolvedHeaders"
v-show="!hidden"
:key="link"
class="va-toc-item"
:lang="lang || locale"
>
<a class="outline-link" :href="link" @click="handleClick">
{{ text }}
</a>
<ul v-if="children && frontmatter.outline === 'deep'">
<li v-for="item in children" v-show="!item.hidden" :key="item.link" :lang="lang || locale">
<a class="outline-link" p="l-3" :href="link" @click="handleClick">
{{ item.text }}
</a>
</li>
</ul>
</li>
</ul>
<YunOutlineItem
class="va-toc relative z-1"
:headers="headers"
:on-click="handleClick"
root
/>
</nav>
</div>
</div>
@ -73,10 +50,6 @@ function handleClick({ target: el }: Event) {
<style lang="scss" scoped>
.va-toc {
text-align: left;
.va-toc-item {
color: var(--va-c-text-light);
}
}
.content {

View File

@ -0,0 +1,48 @@
<script setup lang="ts">
import type { MenuItem } from 'valaxy'
import { useI18n } from 'vue-i18n'
defineProps<{
headers: MenuItem[]
onClick: (e: MouseEvent) => void
root?: boolean
}>()
const { locale } = useI18n()
</script>
<template>
<ul :class="root ? 'root' : 'nested'">
<li v-for="{ children, link, title, lang } in headers" :key="link" class="va-toc-item" :lang="lang || locale">
<a class="outline-link" :href="link" @click="onClick">{{ title }}</a>
<template v-if="children?.length">
<YunOutlineItem :headers="children" :on-click="onClick" />
</template>
</li>
</ul>
</template>
<style lang="scss" scoped>
.va-toc {
.va-toc-item {
.outline-link {
color: var(--va-c-text-light);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.5s;
&:hover,
&.active {
color: var(--va-c-brand);
transition: color 0.25s;
}
}
.nested {
padding-left: 0.8rem;
}
}
}
</style>

View File

@ -9,14 +9,14 @@ const router = useRouter()
<template>
<div class="sidebar-panel">
<div class="site-info" m="t-6">
<a class="site-author-avatar" href="/about">
<router-link class="site-author-avatar" to="/about">
<img class="rounded-full" :src="config.author.avatar" alt="avatar">
<span class="site-author-status">{{ config.author.status.emoji }}</span>
</a>
</router-link>
<div class="site-author-name">
<a href="/about">
<router-link to="/about">
{{ config.author.name }}
</a>
</router-link>
</div>
<router-link v-if="router.hasRoute('about-site')" to="/about/site" class="site-name">
{{ config.title }}

View File

@ -62,9 +62,9 @@ const sortedYears = computed(() => {
<time v-if="post.date" class="post-time" font="mono" opacity="80">{{ formatDate(post.date, 'MM-DD') }}</time>
</div>
<h2 class="post-title" font="serif black">
<a :href="post.path" class="post-title-link">
<router-link :to="post.path || ''" class="post-title-link">
{{ post.title }}
</a>
</router-link>
</h2>
</header>
</article>

View File

@ -25,8 +25,13 @@ html.dark {
}
ul {
list-style: initial;
li > p {
margin-bottom: 0;
}
}
code {
font-size: inherit;
}
}

View File

@ -1,10 +1,11 @@
@use "./layout" as *;
@use "./common/button.scss" as *;
@use "./common/markdown.scss" as *;
@forward "star-markdown-css/src/scss/theme/yun.scss" with (
$colors: (
"primary": $c-primary,
)
);
// override the default style of star-markdown-css
@use "./common/markdown.scss" as *;

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, provide, ref } from 'vue'
import { useHead } from '@vueuse/head'
// @ts-expect-error virtual module
import ValaxyUserApp from '/@valaxyjs/UserAppVue'
@ -36,6 +36,9 @@ useHead({
},
],
})
const onContentUpdated = ref()
provide('onContentUpdated', onContentUpdated)
</script>
<template>

View File

@ -1,5 +1,6 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import type { Ref } from 'vue'
import { inject, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAplayer, useCodePen, useCopyCode, wrapTable } from '..'
import type { Post } from '../../types'
@ -9,11 +10,14 @@ const props = defineProps<{
excerpt?: string
}>()
const onContentUpdated = inject('onContentUpdated') as Ref<() => void>
const { t } = useI18n()
const content = ref()
function updateDom() {
wrapTable(content.value)
onContentUpdated.value()
}
onMounted(() => {

View File

@ -1,51 +1,79 @@
import type { Ref } from 'vue'
import { onMounted, onUnmounted, onUpdated } from 'vue'
import { computed, inject, onMounted, onUnmounted, onUpdated, ref } from 'vue'
import { useFrontmatter } from '../composables'
import { useThemeConfig } from '../../client'
import { throttleAndDebounce } from '../utils'
import type { Header } from '../../types'
import type { DefaultThemeConfig, Header } from '../../types'
interface HeaderWithChildren extends Header {
children?: Header[]
hidden?: boolean
export type MenuItem = Omit<Header, 'slug' | 'children'> & {
children?: MenuItem[]
}
interface MenuItemWithLinkAndChildren {
text: string
link: string
children?: MenuItemWithLinkAndChildren[]
hidden?: boolean
lang?: string
export function resolveHeaders(
headers: MenuItem[],
levelsRange: Exclude<DefaultThemeConfig['outline'], false> = [2, 4],
) {
const levels: [number, number]
= typeof levelsRange === 'number'
? [levelsRange, levelsRange]
: levelsRange === 'deep'
? [2, 6]
: levelsRange
return groupHeaders(headers, levels)
}
export function resolveHeaders(headers: Header[]) {
return mapHeaders(groupHeaders(headers))
function groupHeaders(headers: MenuItem[], levelsRange: [number, number]) {
const result: MenuItem[] = []
headers = headers.map(h => ({ ...h }))
headers.forEach((h, index) => {
if (h.level >= levelsRange[0] && h.level <= levelsRange[1]) {
if (addToParent(index, headers, levelsRange))
result.push(h)
}
})
return result
}
function groupHeaders(headers: Header[]): HeaderWithChildren[] {
headers = headers.map(h => Object.assign({}, h))
// function mapHeaders(
// headers: HeaderWithChildren[],
// ): MenuItemWithLinkAndChildren[] {
// return headers.map(header => ({
// text: header.title,
// link: `#${header.slug}`,
// children: header.children ? mapHeaders(header.children) : undefined,
// hidden: header.hidden,
// lang: header.lang,
// }))
// }
let lastH2: HeaderWithChildren | undefined
function addToParent(
currIndex: number,
headers: MenuItem[],
levelsRange: [number, number],
) {
if (currIndex === 0)
return true
for (const h of headers) {
if (h.level === 2)
lastH2 = h
const currentHeader = headers[currIndex]
for (let index = currIndex - 1; index >= 0; index--) {
const header = headers[index]
else if (lastH2 && h.level <= 3)
(lastH2.children || (lastH2.children = [])).push(h)
if (
header.level < currentHeader.level
&& header.level >= levelsRange[0]
&& header.level <= levelsRange[1]
) {
if (header.children == null)
header.children = []
header.children.push(currentHeader)
return false
}
}
return headers.filter(h => h.level === 2)
}
function mapHeaders(
headers: HeaderWithChildren[],
): MenuItemWithLinkAndChildren[] {
return headers.map(header => ({
text: header.title,
link: `#${header.slug}`,
children: header.children ? mapHeaders(header.children) : undefined,
hidden: header.hidden,
lang: header.lang,
}))
return true
}
// magic number to avoid repeated retrieval
@ -166,3 +194,40 @@ function isAnchorActive(
return [false, null]
}
export const useOutline = () => {
const frontmatter = useFrontmatter()
const themeConfig = useThemeConfig()
const headers = ref<MenuItem[]>([])
const pageOutline = computed<DefaultThemeConfig['outline']>(
() => frontmatter.value.outline ?? themeConfig.value.outline,
)
const onContentUpdated = inject('onContentUpdated') as Ref<() => void>
onContentUpdated.value = () => {
headers.value = getHeaders(pageOutline.value)
}
return {
headers,
}
}
export function getHeaders(pageOutline: DefaultThemeConfig['outline']) {
if (pageOutline === false)
return []
const updatedHeaders: MenuItem[] = []
document
.querySelectorAll<HTMLHeadingElement>('h2, h3, h4, h5, h6')
.forEach((el) => {
if (el.textContent && el.id) {
updatedHeaders.push({
level: Number(el.tagName[1]),
title: el.innerText.replace(/\s+#\s*$/, ''),
link: `#${el.id}`,
lang: el.lang,
})
}
})
return resolveHeaders(updatedHeaders, pageOutline)
}

View File

@ -22,6 +22,10 @@
}
.markdown-body {
code {
font-size: inherit;
}
div[class*="language-"] {
position: relative;
background-color: var(--va-code-block-bg);

View File

@ -0,0 +1,40 @@
// ref vitepress src/node/markdown/env.ts
import type { MarkdownSfcBlocks } from '@mdit-vue/plugin-sfc'
import type { CleanUrlsMode, Header } from '../../types'
// Manually declaring all properties as rollup-plugin-dts
// is unable to merge augmented module declarations
export interface MarkdownEnv {
/**
* The raw Markdown content without frontmatter
*/
content?: string
/**
* The excerpt that extracted by `@mdit-vue/plugin-frontmatter`
*
* - Would be the rendered HTML when `renderExcerpt` is enabled
* - Would be the raw Markdown when `renderExcerpt` is disabled
*/
excerpt?: string
/**
* The frontmatter that extracted by `@mdit-vue/plugin-frontmatter`
*/
frontmatter?: Record<string, unknown>
/**
* The headers that extracted by `@mdit-vue/plugin-headers`
*/
headers?: Header[]
/**
* SFC blocks that extracted by `@mdit-vue/plugin-sfc`
*/
sfcBlocks?: MarkdownSfcBlocks
/**
* The title that extracted by `@mdit-vue/plugin-title`
*/
title?: string
path: string
relativePath: string
cleanUrls: CleanUrlsMode
links?: string[]
}

View File

@ -3,21 +3,37 @@ import MarkdownIt from 'markdown-it'
import type { Theme } from 'shiki'
import anchorPlugin from 'markdown-it-anchor'
import attrsPlugin from 'markdown-it-attrs'
import emojiPlugin from 'markdown-it-emoji'
import LinkAttributes from 'markdown-it-link-attributes'
import TOC from 'markdown-it-table-of-contents'
import TaskLists from 'markdown-it-task-lists'
import attrs from 'markdown-it-attrs'
import type { KatexOptions } from 'katex'
import { componentPlugin } from '@mdit-vue/plugin-component'
import {
type FrontmatterPluginOptions,
frontmatterPlugin,
} from '@mdit-vue/plugin-frontmatter'
import {
type HeadersPluginOptions,
headersPlugin,
} from '@mdit-vue/plugin-headers'
import { type SfcPluginOptions, sfcPlugin } from '@mdit-vue/plugin-sfc'
import { titlePlugin } from '@mdit-vue/plugin-title'
import { type TocPluginOptions, tocPlugin } from '@mdit-vue/plugin-toc'
import type { Header } from '../../types'
import Katex from './markdown-it/katex'
import { type Blocks, containerPlugin } from './markdown-it/container'
import { headingPlugin } from './markdown-it/headings'
import { slugify } from './slugify'
import { highlight } from './highlight'
import { highlightLinePlugin, preWrapperPlugin } from './markdown-it/highlightLines'
import { linkPlugin } from './plugins/link'
// import { lineNumberPlugin } from "./plugins/lineNumbers";
export * from './env'
export type ThemeOptions = Theme | { light: Theme; dark: Theme }
export interface MarkdownParsedData {
@ -25,11 +41,8 @@ export interface MarkdownParsedData {
links?: string[]
headers?: Header[]
}
export interface MarkdownRenderer extends MarkdownIt {
__path: string
__relativePath: string
__data: MarkdownParsedData
}
export type MarkdownRenderer = MarkdownIt
export interface MarkdownOptions {
/**
@ -40,14 +53,19 @@ export interface MarkdownOptions {
* config markdown-it
*/
config?: (md: MarkdownIt) => void
anchor?: {
permalink?: anchorPlugin.AnchorOptions['permalink']
}
// https://github.com/Oktavilla/markdown-it-table-of-contents
toc?: {
includeLevel?: number[]
[key: string]: any
anchor?: anchorPlugin.AnchorOptions
attrs?: {
leftDelimiter?: string
rightDelimiter?: string
allowedAttributes?: string[]
disable?: boolean
}
// mdit-vue plugins
frontmatter?: FrontmatterPluginOptions
headers?: HeadersPluginOptions
sfc?: SfcPluginOptions
toc?: TocPluginOptions
katex?: KatexOptions
/**
* shiki
@ -57,30 +75,36 @@ export interface MarkdownOptions {
* Custom block configurations
*/
blocks?: Blocks
externalLinks?: Record<string, string>
}
export async function setupMarkdownPlugins(md: MarkdownIt, mdOptions: MarkdownOptions = {}, isExcerpt = false) {
md
.use(highlightLinePlugin)
export async function setupMarkdownPlugins(
md: MarkdownIt,
mdOptions: MarkdownOptions = {},
isExcerpt = false,
base = '/',
) {
// custom plugins
md.use(highlightLinePlugin)
.use(preWrapperPlugin)
.use(containerPlugin, mdOptions.blocks)
// conflict with {% %}
.use(attrs)
// generate toc in client
if (!isExcerpt)
md.use(headingPlugin, mdOptions?.toc?.includeLevel)
// .use(lineNumberPlugin)
// https://github.com/arve0/markdown-it-attrs
// add classes
md
.use(LinkAttributes, {
matcher: (link: string) => /^https?:\/\//.test(link),
attrs: {
.use(
linkPlugin,
{
target: '_blank',
rel: 'noopener',
rel: 'noreferrer',
...mdOptions.externalLinks,
},
})
base,
)
// conflict with {% %}
// 3rd party plugins
if (!mdOptions.attrs?.disable)
md.use(attrsPlugin, mdOptions.attrs)
// .use(lineNumberPlugin)
md.use(Katex, mdOptions.katex)
md.use(emojiPlugin)
@ -90,24 +114,34 @@ export async function setupMarkdownPlugins(md: MarkdownIt, mdOptions: MarkdownOp
permalink: anchorPlugin.permalink.ariaHidden({}),
...mdOptions.anchor,
})
.use(TOC, {
slugify,
includeLevel: [2, 3, 4],
...mdOptions.toc,
})
}
// mdit-vue plugins
md.use(componentPlugin)
.use(frontmatterPlugin, {
...mdOptions.frontmatter,
} as FrontmatterPluginOptions)
.use(headersPlugin, {
slugify,
...mdOptions.headers,
} as HeadersPluginOptions)
.use(sfcPlugin, {
...mdOptions.sfc,
} as SfcPluginOptions)
.use(titlePlugin)
.use(tocPlugin, {
slugify,
...mdOptions.toc,
} as TocPluginOptions)
md.use(TaskLists)
const originalRender = md.render
md.render = (...args) => {
(md as MarkdownRenderer).__data = {}
return originalRender.call(md, ...args)
}
if (mdOptions.config)
mdOptions.config(md)
// if (options.lineNumbers)
// md.use(lineNumberPlugin)
return md as MarkdownRenderer
}

View File

@ -1,36 +0,0 @@
// ref vitepress
import { resolveTitleFromToken } from '@mdit-vue/shared'
import type MarkdownIt from 'markdown-it'
import type { MarkdownRenderer } from '..'
import { slugify } from '../slugify'
export const headingPlugin = (md: MarkdownIt, include = [1, 2, 3, 4]) => {
md.renderer.rules.heading_open = (tokens, i, options, env, self) => {
const token = tokens[i]
const tags = include.map(item => `h${item}`)
if (tags.includes(token.tag)) {
const content = tokens[i + 1].content
const idAttr = token.attrs!.find(([name]) => name === 'id')
const slug = idAttr && idAttr[1]
const data = (md as MarkdownRenderer).__data
const headers = data.headers || (data.headers = [])
// remove {} after head
const leftDeli = content.indexOf('{')
const title = leftDeli === -1 ? content : content.slice(0, leftDeli).trim()
const matched = content.match(/\{lang=\"(.*)\"\}/)
const lang = matched ? matched[1] : ''
const titleToken = md.parseInline(title, {})[0]
headers.push({
level: parseInt(token.tag.slice(1), 10),
title: resolveTitleFromToken(titleToken, {
shouldAllowHtml: false,
shouldEscapeText: false,
}),
slug: slug || slugify(title),
lang,
})
}
return self.renderToken(tokens, i, options)
}
}

View File

@ -2,14 +2,13 @@
import fs from 'fs'
import path from 'path'
import c from 'picocolors'
import matter from 'gray-matter'
import LRUCache from 'lru-cache'
import _debug from 'debug'
import { resolveTitleFromToken } from '@mdit-vue/shared'
import { EXTERNAL_URL_RE, getGitTimestamp, slash, transformObject } from '../utils'
import type { HeadConfig, PageData } from '../../types'
import type { CleanUrlsMode, HeadConfig, PageData } from '../../types'
import { createMarkdownRenderer } from '.'
import type { MarkdownOptions, MarkdownRenderer } from '.'
import type { MarkdownEnv, MarkdownOptions, MarkdownRenderer } from '.'
const jsStringBreaker = '\u200B'
const vueTemplateBreaker = '<wbr>'
@ -101,6 +100,8 @@ export async function createMarkdownToVueRenderFn(
userDefines: Record<string, any> | undefined,
isBuild = false,
includeLastUpdatedData = false,
// https://vitepress.vuejs.org/config/app-configs#cleanurls-experimental
cleanUrls: CleanUrlsMode = 'with-subfolders',
) {
const md = await createMarkdownRenderer(options)
@ -126,21 +127,33 @@ export async function createMarkdownToVueRenderFn(
// resolve includes
const includes: string[] = []
src = src.replace(includesRE, (_, m1) => {
const includePath = path.join(dir, m1)
const content = fs.readFileSync(includePath, 'utf-8')
includes.push(slash(includePath))
return content
src = src.replace(includesRE, (m, m1) => {
try {
const includePath = path.join(dir, m1)
const content = fs.readFileSync(includePath, 'utf-8')
includes.push(slash(includePath))
return content
}
catch (error) {
return m // silently ignore error if file is not present
}
})
const { content, data: frontmatter } = matter(src)
// reset env before render
const env: MarkdownEnv = {
path: file,
relativePath,
cleanUrls,
}
// reset state before render
md.__path = file
md.__relativePath = relativePath
const html = md.render(content)
const data = md.__data
const html = md.render(src, env)
const {
frontmatter = {},
headers = [],
links = [],
sfcBlocks,
title = '',
} = env
// validate data.links
const deadLinks: string[] = []
@ -157,9 +170,9 @@ export async function createMarkdownToVueRenderFn(
deadLinks.push(url)
}
if (data.links) {
if (links) {
const dir = path.dirname(file)
for (let url of data.links) {
for (let url of links) {
if (/\.(?!html|md)\w+($|\?)/i.test(url))
continue
@ -188,11 +201,11 @@ export async function createMarkdownToVueRenderFn(
// provide load
const pageData: PageData = {
title: inferTitle(md, frontmatter, ''),
titleTemplate: frontmatter.titleTemplate,
title: inferTitle(md, frontmatter, title),
titleTemplate: frontmatter.titleTemplate as any,
description: inferDescription(frontmatter),
frontmatter,
headers: data.headers || [],
headers,
relativePath,
path: path.join(srcDir, relativePath),
}
@ -216,21 +229,30 @@ export async function createMarkdownToVueRenderFn(
'aside',
'aside-custom',
]
const slotsText = slots.map(s => `<template #${s}><slot name="${s}" /></template>`).join('')
const slotsText = slots
.map(s => `<template #${s}><slot name="${s}" /></template>`)
.join('')
return slotsText
}
const vueSrc
= [
...injectPageDataCode(data.hoistedTags || [], pageData, replaceRegex),
`<template><${pageComponent} :frontmatter="frontmatter" :data="data">`,
`<template #main-content-md>${
replaceConstants(html, replaceRegex, vueTemplateBreaker)
}</template>`,
generateSlots(),
'<slot />',
`</${pageComponent}></template>`,
].join('\n')
const vueSrc = [
...injectPageDataCode(
sfcBlocks?.scripts.map(item => item.content) ?? [],
pageData,
replaceRegex,
),
`<template><${pageComponent} :frontmatter="frontmatter" :data="data">`,
`<template #main-content-md>${replaceConstants(
html,
replaceRegex,
vueTemplateBreaker,
)}</template>`,
generateSlots(),
'<slot />',
`</${pageComponent}></template>`,
...(sfcBlocks?.styles.map(item => item.content) ?? []),
...(sfcBlocks?.customBlocks.map(item => item.content) ?? []),
].join('\n')
debug(`[render] ${file} in ${Date.now() - start}ms.`)

View File

@ -0,0 +1,109 @@
// ref vitepress
// markdown-it plugin for:
// 1. adding target="_blank" to external links
// 2. normalize internal links to end with `.html`
import { URL } from 'url'
import type MarkdownIt from 'markdown-it'
import type { MarkdownEnv } from '../env'
import { EXTERNAL_URL_RE } from '../../utils'
const indexRE = /(^|.*\/)index.md(#?.*)$/i
export const linkPlugin = (
md: MarkdownIt,
externalAttrs: Record<string, string>,
base: string,
) => {
md.renderer.rules.link_open = (
tokens,
idx,
options,
env: MarkdownEnv,
self,
) => {
const token = tokens[idx]
const hrefIndex = token.attrIndex('href')
if (hrefIndex >= 0) {
const hrefAttr = token.attrs![hrefIndex]
const url = hrefAttr[1]
const isExternal = EXTERNAL_URL_RE.test(url)
if (isExternal) {
Object.entries(externalAttrs).forEach(([key, val]) => {
token.attrSet(key, val)
})
// catch localhost links as dead link
if (url.replace(EXTERNAL_URL_RE, '').startsWith('//localhost:'))
pushLink(url, env)
}
else if (
// internal anchor links
!url.startsWith('#')
// mail links
&& !url.startsWith('mailto:')
// links to files (other than html/md)
&& !/\.(?!html|md)\w+($|\?)/i.test(url)
) {
normalizeHref(hrefAttr, env)
}
// encode vite-specific replace strings in case they appear in URLs
// this also excludes them from build-time replacements (which injects
// <wbr/> and will break URLs)
hrefAttr[1] = hrefAttr[1]
.replace(/\bimport\.meta/g, 'import%2Emeta')
.replace(/\bprocess\.env/g, 'process%2Eenv')
}
return self.renderToken(tokens, idx, options)
}
function normalizeHref(hrefAttr: [string, string], env: MarkdownEnv) {
let url = hrefAttr[1]
const indexMatch = url.match(indexRE)
if (indexMatch) {
const [, path, hash] = indexMatch
url = path + hash
}
else {
let cleanUrl = url.replace(/[?#].*$/, '')
// transform foo.md -> foo[.html]
if (cleanUrl.endsWith('.md')) {
cleanUrl = cleanUrl.replace(
/\.md$/,
env.cleanUrls === 'disabled' ? '.html' : '',
)
}
// transform ./foo -> ./foo[.html]
if (
env.cleanUrls === 'disabled'
&& !cleanUrl.endsWith('.html')
&& !cleanUrl.endsWith('/')
)
cleanUrl += '.html'
const parsed = new URL(url, 'http://a.com')
url = cleanUrl + parsed.search + parsed.hash
}
// ensure leading . for relative paths
if (!url.startsWith('/') && !/^\.\//.test(url))
url = `./${url}`
// export it for existence check
pushLink(url.replace(/\.html$/, ''), env)
// append base to internal (non-relative) urls
if (url.startsWith('/'))
url = `${base}${url}`.replace(/\/+/g, '/')
// markdown-it encodes the uri
hrefAttr[1] = decodeURI(url)
}
function pushLink(link: string, env: MarkdownEnv) {
const links = env.links || (env.links = [])
links.push(link)
}
}

View File

@ -90,7 +90,7 @@ export async function resolveOptions(options: ValaxyEntryOptions, mode: Resolved
// supported in Node 12+, which is required by Vite.
const pages = (
await fg(['**.md'], {
cwd: userRoot,
cwd: resolve(userRoot, 'pages'),
ignore: ['**/node_modules'],
})
).sort()

View File

@ -133,10 +133,6 @@ export function createValaxyPlugin(options: ResolvedValaxyOptions, serverOptions
markdownToVue = await createMarkdownToVueRenderFn(
options.userRoot,
valaxyConfig.markdown || {
toc: {
includeLevel: [1, 2, 3, 4],
listType: 'ol',
},
katex: {},
},
options.pages,
@ -200,19 +196,6 @@ export function createValaxyPlugin(options: ResolvedValaxyOptions, serverOptions
code.replace('{%', '\{\%')
code.replace('%}', '\%\}')
// const scripts = [
// '<script setup>',
// 'import { useRoute } from "vue-router"',
// 'const route = useRoute()',
// `route.meta.headers = ${JSON.stringify(_md.__data)}`,
// `export const data = JSON.parse(${JSON.stringify(JSON.stringify(pageData))})`,
// `frontmatter.data = JSON.parse(${JSON.stringify(JSON.stringify(pageData))})`,
// '</script>',
// ]
// const li = code.lastIndexOf('</script>')
// code = code.slice(0, li) + scripts.join('\n') + code.slice(li + 9)
// transform .md files into vueSrc so plugin-vue can handle it
const { vueSrc, deadLinks, includes } = await markdownToVue(
code,

View File

@ -85,7 +85,6 @@
"markdown-it-attrs": "^4.1.4",
"markdown-it-container": "^3.0.0",
"markdown-it-emoji": "^2.0.2",
"markdown-it-link-attributes": "^4.0.1",
"markdown-it-table-of-contents": "^0.6.0",
"markdown-it-task-lists": "^2.1.1",
"nprogress": "^0.2.0",
@ -109,12 +108,17 @@
"yargs": "^17.6.0"
},
"devDependencies": {
"@mdit-vue/plugin-component": "^0.11.1",
"@mdit-vue/plugin-frontmatter": "^0.11.1",
"@mdit-vue/plugin-headers": "^0.11.1",
"@mdit-vue/plugin-sfc": "^0.11.1",
"@mdit-vue/plugin-title": "^0.11.1",
"@mdit-vue/plugin-toc": "^0.11.1",
"@mdit-vue/shared": "^0.11.0",
"@types/cross-spawn": "^6.0.2",
"@types/ejs": "^3.1.1",
"@types/katex": "^0.14.0",
"@types/markdown-it": "^12.2.3",
"@types/markdown-it-link-attributes": "^3.0.1",
"@types/nprogress": "^0.2.0",
"@types/yargs": "^17.0.13",
"debug": "^4.3.4",

View File

@ -1,4 +1,11 @@
export type DefaultThemeConfig = Record<string, any>
export type DefaultThemeConfig = {
/**
* Custom header levels of outline in the aside component.
*
* @default 2
*/
outline?: number | [number, number] | 'deep' | false
} & Record<string, any>
export interface SocialLink {
/**

View File

@ -1,9 +1,33 @@
import type { Post } from './posts'
export type CleanUrlsMode =
| 'disabled'
| 'without-subfolders'
| 'with-subfolders'
export interface Header {
/**
* The level of the header
*
* `1` to `6` for `<h1>` to `<h6>`
*/
level: number
/**
* The title of the header
*/
title: string
/**
* The slug of the header
*
* Typically the `id` attr of the header anchor
*/
slug: string
/**
* Link of the header
*
* Typically using `#${slug}` as the anchor hash
*/
link: string
/**
* i18n
*/

View File

@ -94,12 +94,17 @@ importers:
'@iconify-json/carbon': ^1.1.8
'@iconify-json/ri': ^1.1.3
'@intlify/vite-plugin-vue-i18n': ^6.0.3
'@mdit-vue/plugin-component': ^0.11.1
'@mdit-vue/plugin-frontmatter': ^0.11.1
'@mdit-vue/plugin-headers': ^0.11.1
'@mdit-vue/plugin-sfc': ^0.11.1
'@mdit-vue/plugin-title': ^0.11.1
'@mdit-vue/plugin-toc': ^0.11.1
'@mdit-vue/shared': ^0.11.0
'@types/cross-spawn': ^6.0.2
'@types/ejs': ^3.1.1
'@types/katex': ^0.14.0
'@types/markdown-it': ^12.2.3
'@types/markdown-it-link-attributes': ^3.0.1
'@types/nprogress': ^0.2.0
'@types/yargs': ^17.0.13
'@vitejs/plugin-vue': ^3.1.2
@ -127,7 +132,6 @@ importers:
markdown-it-attrs: ^4.1.4
markdown-it-container: ^3.0.0
markdown-it-emoji: ^2.0.2
markdown-it-link-attributes: ^4.0.1
markdown-it-table-of-contents: ^0.6.0
markdown-it-task-lists: ^2.1.1
nprogress: ^0.2.0
@ -177,7 +181,6 @@ importers:
markdown-it-attrs: 4.1.4_markdown-it@13.0.1
markdown-it-container: 3.0.0
markdown-it-emoji: 2.0.2
markdown-it-link-attributes: 4.0.1
markdown-it-table-of-contents: 0.6.0
markdown-it-task-lists: 2.1.1
nprogress: 0.2.0
@ -200,12 +203,17 @@ importers:
vue-router: 4.1.5_vue@3.2.40
yargs: 17.6.0
devDependencies:
'@mdit-vue/plugin-component': 0.11.1
'@mdit-vue/plugin-frontmatter': 0.11.1
'@mdit-vue/plugin-headers': 0.11.1
'@mdit-vue/plugin-sfc': 0.11.1
'@mdit-vue/plugin-title': 0.11.1
'@mdit-vue/plugin-toc': 0.11.1
'@mdit-vue/shared': 0.11.0
'@types/cross-spawn': 6.0.2
'@types/ejs': 3.1.1
'@types/katex': 0.14.0
'@types/markdown-it': 12.2.3
'@types/markdown-it-link-attributes': 3.0.1
'@types/nprogress': 0.2.0
'@types/yargs': 17.0.13
debug: 4.3.4
@ -859,6 +867,57 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.14
dev: false
/@mdit-vue/plugin-component/0.11.1:
resolution: {integrity: sha512-fCqyYPwEXFa182/Vz6g8McDi3SCIwm3yHWkWddHx+QNn0gMGFqkhJVcz/wjCIA3oCoWUBWM80aZ09ZuoQiOmvQ==}
dependencies:
'@types/markdown-it': 12.2.3
markdown-it: 13.0.1
dev: true
/@mdit-vue/plugin-frontmatter/0.11.1:
resolution: {integrity: sha512-AdZJInjD1pTJXlfhuoBS5ycuIQ3ewBfY0R/XHM3TRDEaDHQJHxouUCpCyijZmpdljTU45lFetIowaKtAi7GBog==}
dependencies:
'@mdit-vue/types': 0.11.0
'@types/markdown-it': 12.2.3
gray-matter: 4.0.3
markdown-it: 13.0.1
dev: true
/@mdit-vue/plugin-headers/0.11.1:
resolution: {integrity: sha512-eBUonsEkXP2Uf2MIXSWZGCcLCIMSA1XfThJwhzSAosoa7fO5aw52LKCweddmn7zLQvgQh7p7382sFAhCc2KXog==}
dependencies:
'@mdit-vue/shared': 0.11.0
'@mdit-vue/types': 0.11.0
'@types/markdown-it': 12.2.3
markdown-it: 13.0.1
dev: true
/@mdit-vue/plugin-sfc/0.11.1:
resolution: {integrity: sha512-3AjQXqExzT9FWGNOeTBqK1pbt1UA5anrZvjo7OO2PJ3lrfZd0rbjionFkmW/VW1912laHUraIP6n74mUNqPuWw==}
dependencies:
'@mdit-vue/types': 0.11.0
'@types/markdown-it': 12.2.3
markdown-it: 13.0.1
dev: true
/@mdit-vue/plugin-title/0.11.1:
resolution: {integrity: sha512-lvgR1pSgwX5D3tmLGyYBsfd3GbEoscqYsLTE8Vg+rCY8LfSrHdwrOD3Eg+SM2KyS5+gn+Zw4nS0S1yxOIVZBCQ==}
dependencies:
'@mdit-vue/shared': 0.11.0
'@mdit-vue/types': 0.11.0
'@types/markdown-it': 12.2.3
markdown-it: 13.0.1
dev: true
/@mdit-vue/plugin-toc/0.11.1:
resolution: {integrity: sha512-1tkGb1092ZgLhoSmE5hkC6U0IRGG5bWhUY4p14npV4cwqntciXEoXRqPA1jGEDh5hnofZC0bHbeS3uKxsmAEew==}
dependencies:
'@mdit-vue/shared': 0.11.0
'@mdit-vue/types': 0.11.0
'@types/markdown-it': 12.2.3
markdown-it: 13.0.1
dev: true
/@mdit-vue/shared/0.11.0:
resolution: {integrity: sha512-eiGe42y7UYpjO6/8Lg6OpAtzZrRU9k8dhpX1e/kJMTcL+tn+XkqRMJJ8I2pdrOQMSkgvIva5FNAriykqFzkdGg==}
dependencies:
@ -961,12 +1020,6 @@ packages:
/@types/linkify-it/3.0.2:
resolution: {integrity: sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==}
/@types/markdown-it-link-attributes/3.0.1:
resolution: {integrity: sha512-K8RnNb1q8j7rDOJbMF7AnlhCC/45BjrQ8z3WZWOrvkBIl8u9RXvmBdG/hfpnmK1JhhEZcmFEKWt+ilW1Mly+2Q==}
dependencies:
'@types/markdown-it': 12.2.3
dev: true
/@types/markdown-it/12.2.3:
resolution: {integrity: sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==}
dependencies:
@ -1781,7 +1834,6 @@ packages:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
dependencies:
sprintf-js: 1.0.3
dev: false
/argparse/2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@ -3428,7 +3480,6 @@ packages:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
hasBin: true
dev: false
/esquery/1.4.0:
resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==}
@ -3550,7 +3601,6 @@ packages:
engines: {node: '>=0.10.0'}
dependencies:
is-extendable: 0.1.1
dev: false
/extract-comments/1.1.0:
resolution: {integrity: sha512-dzbZV2AdSSVW/4E7Ti5hZdHWbA+Z80RJsJhr5uiL10oyjl/gy7/o+HI1HwK4/WSZhlq4SNKU3oUzXlM13Qx02Q==}
@ -3911,7 +3961,6 @@ packages:
kind-of: 6.0.3
section-matter: 1.0.0
strip-bom-string: 1.0.0
dev: false
/gzip-size/6.0.0:
resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
@ -4231,7 +4280,6 @@ packages:
/is-extendable/0.1.1:
resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
engines: {node: '>=0.10.0'}
dev: false
/is-extglob/2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
@ -4408,7 +4456,6 @@ packages:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
dev: false
/js-yaml/4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
@ -4526,7 +4573,6 @@ packages:
/kind-of/6.0.3:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
dev: false
/kleur/3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
@ -4733,10 +4779,6 @@ packages:
resolution: {integrity: sha512-zLftSaNrKuYl0kR5zm4gxXjHaOI3FAOEaloKmRA5hijmJZvSjmxcokOLlzycb/HXlUFWzXqpIEoyEMCE4i9MvQ==}
dev: false
/markdown-it-link-attributes/4.0.1:
resolution: {integrity: sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ==}
dev: false
/markdown-it-table-of-contents/0.6.0:
resolution: {integrity: sha512-jHvEjZVEibyW97zEYg19mZCIXO16lHbvRaPDkEuOfMPBmzlI7cYczMZLMfUvwkhdOVQpIxu3gx6mgaw46KsNsQ==}
engines: {node: '>6.4.0'}
@ -5703,7 +5745,6 @@ packages:
dependencies:
extend-shallow: 2.0.1
kind-of: 6.0.3
dev: false
/select-hose/2.0.0:
resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==}
@ -5932,7 +5973,6 @@ packages:
/sprintf-js/1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
dev: false
/stable/0.1.8:
resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==}
@ -6018,7 +6058,6 @@ packages:
/strip-bom-string/1.0.0:
resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==}
engines: {node: '>=0.10.0'}
dev: false
/strip-bom/3.0.0:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}