export page data from page component

pull/1/head
Evan You 4 years ago
parent 96c30a6d83
commit 9cbfa9529d

@ -4,6 +4,6 @@ import { useRoute } from '../composables/router'
export const Content = {
setup() {
const route = useRoute()
return () => (route.component ? h(route.component) : null)
return () => (route.contentComponent ? h(route.contentComponent) : null)
}
}

@ -6,7 +6,8 @@ const NotFound = Theme.NotFound || (() => '404 Not Found')
/**
* @typedef {{
* path: string
* component: import('vue').Component | null
* contentComponent: import('vue').Component | null
* pageData: { path: string } | null
* }} Route
*/
@ -20,7 +21,8 @@ const RouteSymbol = Symbol()
*/
const getDefaultRoute = () => ({
path: location.pathname,
component: null
contentComponent: null,
pageData: null
})
export function useRouter() {
@ -98,7 +100,10 @@ function loadPage(route, scrollPosition = 0) {
import(`${pagePath}.md?t=${Date.now()}`)
.then(async (m) => {
if (route.path === pendingPath) {
route.component = m.default
route.contentComponent = m.default
route.pageData = m.__pageData
console.log(route.pageData)
await nextTick()
window.scrollTo({
left: 0,
@ -108,8 +113,10 @@ function loadPage(route, scrollPosition = 0) {
}
})
.catch((err) => {
if (route.path === pendingPath) {
route.component = NotFound
if (!err.message.match(/fetch/)) {
throw err
} else if (route.path === pendingPath) {
route.contentComponent = NotFound
}
})
}

@ -1,6 +1,5 @@
{
"compilerOptions": {
"baseUrl": "../",
"lib": ["ESNext", "DOM"],
"moduleResolution": "node",
"checkJs": true,

@ -1,5 +1,5 @@
import MarkdownIt from 'markdown-it'
import { parseHeaders } from '../utils/parseHeaders'
import { parseHeader } from '../utils/parseHeader'
import { highlight } from './plugins/highlight'
import { slugify } from './plugins/slugify'
import { highlightLinePlugin } from './plugins/highlightLines'
@ -10,6 +10,7 @@ import { snippetPlugin } from './plugins/snippet'
import { hoistPlugin } from './plugins/hoist'
import { preWrapperPlugin } from './plugins/preWrapper'
import { linkPlugin } from './plugins/link'
import { extractHeaderPlugin, Header } from './plugins/header'
const emoji = require('markdown-it-emoji')
const anchor = require('markdown-it-anchor')
@ -31,6 +32,7 @@ export interface MarkdownOpitons extends MarkdownIt.Options {
export interface MarkdownParsedData {
hoistedTags?: string[]
links?: string[]
headers?: Header[]
}
export interface MarkdownRenderer {
@ -55,6 +57,7 @@ export const createMarkdownRenderer = (
.use(snippetPlugin)
.use(hoistPlugin)
.use(containerPlugin)
.use(extractHeaderPlugin)
.use(linkPlugin, {
target: '_blank',
rel: 'noopener noreferrer',
@ -81,7 +84,7 @@ export const createMarkdownRenderer = (
{
slugify,
includeLevel: [2, 3],
format: parseHeaders
format: parseHeader
},
options.toc
)

@ -1,35 +0,0 @@
import path from 'path'
import { createMarkdownRenderer, MarkdownOpitons } from './markdown'
import LRUCache from 'lru-cache'
const matter = require('gray-matter')
const debug = require('debug')('vitepress:md')
const cache = new LRUCache<string, string>({ max: 1024 })
export function createMarkdownToVueRenderFn(
root: string,
options: MarkdownOpitons = {}
) {
const md = createMarkdownRenderer(options)
return (src: string, file: string) => {
file = path.relative(root, file)
const cached = cache.get(src)
if (cached) {
debug(`[cache hit] ${file}`)
return cached
}
const start = Date.now()
const { content, data } = matter(src)
const { html } = md.render(content)
// TODO validate links?
const vueSrc =
`<template>${html}</template>` + (data.hoistedTags || []).join('\n')
debug(`[render] ${file} in ${Date.now() - start}ms.`, data)
cache.set(src, vueSrc)
return vueSrc
}
}

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

@ -18,12 +18,11 @@ export const linkPlugin = (
const hrefAttr = token.attrs![hrefIndex]
const url = hrefAttr[1]
const isExternal = /^https?:/.test(url)
const isSourceLink = /(\/|\.md|\.html)(#.*)?$/.test(url)
if (isExternal) {
Object.entries(externalAttrs).forEach(([key, val]) => {
token.attrSet(key, val)
})
} else if (isSourceLink) {
} else if (!url.startsWith('#')) {
normalizeHref(hrefAttr)
}
}
@ -31,13 +30,8 @@ export const linkPlugin = (
}
function normalizeHref(hrefAttr: [string, string]) {
const data = md.__data
let url = hrefAttr[1]
// convert link to filename and export it for existence check
const links = data.links || (data.links = [])
links.push(url)
const indexMatch = url.match(indexRE)
if (indexMatch) {
const [, path, , hash] = indexMatch
@ -51,6 +45,11 @@ export const linkPlugin = (
url = ensureBeginningDotSlash(url)
}
// export it for existence check
const data = md.__data
const links = data.links || (data.links = [])
links.push(url)
// markdown-it encodes the uri
hrefAttr[1] = decodeURI(url)
}

@ -0,0 +1,88 @@
import path from 'path'
import matter from 'gray-matter'
import LRUCache from 'lru-cache'
import { createMarkdownRenderer, MarkdownOpitons } from './markdown/markdown'
import { Header } from './markdown/plugins/header'
import { deeplyParseHeader } from './utils/parseHeader'
const debug = require('debug')('vitepress:md')
const cache = new LRUCache<string, string>({ max: 1024 })
export function createMarkdownToVueRenderFn(
root: string,
options: MarkdownOpitons = {}
) {
const md = createMarkdownRenderer(options)
return (src: string, file: string, lastUpdated: number) => {
file = path.relative(root, file)
const cached = cache.get(src)
if (cached) {
debug(`[cache hit] ${file}`)
return cached
}
const start = Date.now()
const { content, data: frontmatter } = matter(src)
const { html, data } = md.render(content)
// TODO validate data.links?
// inject page data
const additionalBlocks = injectPageData(
data.hoistedTags || [],
content,
frontmatter,
data.headers || [],
lastUpdated
)
const vueSrc =
`<template><div class="vitepress-content">${html}</div></template>\n` +
additionalBlocks.join('\n')
debug(`[render] ${file} in ${Date.now() - start}ms.`)
cache.set(src, vueSrc)
return vueSrc
}
}
const scriptRE = /<\/script>/
function injectPageData(
tags: string[],
content: string,
frontmatter: object,
headers: Header[],
lastUpdated: number
) {
const code = `\nexport const __pageData = ${JSON.stringify({
title: inferTitle(frontmatter, content),
frontmatter,
headers,
lastUpdated
})}`
const existingScriptIndex = tags.findIndex((tag) => scriptRE.test(tag))
if (existingScriptIndex > -1) {
tags[existingScriptIndex] = tags[existingScriptIndex].replace(
scriptRE,
code + `</script>`
)
} else {
tags.push(`<script>${code}\nexport default {}</script>`)
}
return tags
}
const inferTitle = (frontmatter: any, content: string) => {
if (frontmatter.home) {
return 'Home'
}
if (frontmatter.title) {
return deeplyParseHeader(frontmatter.title)
}
const match = content.match(/^\s*#+\s+(.*)/m)
if (match) {
return deeplyParseHeader(match[1].trim())
}
}

@ -21,11 +21,6 @@ export interface SiteData<ThemeConfig = any> {
description: string
base: string
themeConfig: ThemeConfig
pages: PageData[]
}
export interface PageData {
path: string
}
export interface ResolvedConfig<ThemeConfig = any> {
@ -60,7 +55,7 @@ export async function resolveConfig(root: string): Promise<ResolvedConfig> {
}
export async function resolveSiteData(root: string): Promise<SiteData> {
// 1. load user config
// load user config
const configPath = getConfigPath(root)
let hasUserConfig = false
try {
@ -73,16 +68,10 @@ export async function resolveSiteData(root: string): Promise<SiteData> {
delete require.cache[configPath]
const userConfig: UserConfig = hasUserConfig ? require(configPath) : {}
// 2. TODO scan pages data
// 3. resolve site data
const site: SiteData = {
return {
title: userConfig.title || 'VitePress',
description: userConfig.description || 'A VitePress site',
base: userConfig.base || '/',
themeConfig: userConfig.themeConfig || {},
pages: []
themeConfig: userConfig.themeConfig || {}
}
return site
}

@ -11,7 +11,7 @@ import {
getConfigPath,
resolveSiteData
} from './resolveConfig'
import { createMarkdownToVueRenderFn } from './markdown/markdownToVue'
import { createMarkdownToVueRenderFn } from './markdownToVue'
import { APP_PATH, SITE_DATA_REQUEST_PATH } from './utils/pathResolver'
const debug = require('debug')('vitepress:serve')
@ -43,7 +43,12 @@ function createVitePressPlugin(config: ResolvedConfig): Plugin {
if (file.endsWith('.md')) {
debugHmr(`reloading ${file}`)
const content = await cachedRead(null, file)
watcher.handleVueReload(file, Date.now(), markdownToVue(content, file))
const timestamp = Date.now()
watcher.handleVueReload(
file,
timestamp,
markdownToVue(content, file, timestamp)
)
}
})
@ -85,7 +90,7 @@ function createVitePressPlugin(config: ResolvedConfig): Plugin {
await cachedRead(ctx, file)
// let vite know this is supposed to be treated as vue file
ctx.vue = true
ctx.body = markdownToVue(ctx.body, file)
ctx.body = markdownToVue(ctx.body, file, ctx.lastModified.getTime())
debug(ctx.url, ctx.status)
return next()
}

@ -1,6 +1,5 @@
{
"compilerOptions": {
"baseUrl": "../",
"outDir": "../dist",
"module": "commonjs",
"lib": ["ESNext"],

@ -46,7 +46,7 @@ const compose = (...processors: ((str: string) => string)[]) => {
}
// Unescape html, parse emojis and remove some md tokens.
export const parseHeaders = compose(
export const parseHeader = compose(
unescapeHtml,
parseEmojis,
removeMarkdownTokens,
@ -56,7 +56,7 @@ export const parseHeaders = compose(
// Also clean the html that isn't wrapped by code.
// Because we want to support using VUE components in headers.
// e.g. https://vuepress.vuejs.org/guide/using-vue.html#badge
export const deeplyParseHeaders = compose(
export const deeplyParseHeader = compose(
removeNonCodeWrappedHTML,
parseHeaders
parseHeader
)
Loading…
Cancel
Save