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

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

@ -1,5 +1,5 @@
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import { parseHeaders } from '../utils/parseHeaders' import { parseHeader } from '../utils/parseHeader'
import { highlight } from './plugins/highlight' import { highlight } from './plugins/highlight'
import { slugify } from './plugins/slugify' import { slugify } from './plugins/slugify'
import { highlightLinePlugin } from './plugins/highlightLines' import { highlightLinePlugin } from './plugins/highlightLines'
@ -10,6 +10,7 @@ import { snippetPlugin } from './plugins/snippet'
import { hoistPlugin } from './plugins/hoist' import { hoistPlugin } from './plugins/hoist'
import { preWrapperPlugin } from './plugins/preWrapper' import { preWrapperPlugin } from './plugins/preWrapper'
import { linkPlugin } from './plugins/link' import { linkPlugin } from './plugins/link'
import { extractHeaderPlugin, Header } from './plugins/header'
const emoji = require('markdown-it-emoji') const emoji = require('markdown-it-emoji')
const anchor = require('markdown-it-anchor') const anchor = require('markdown-it-anchor')
@ -31,6 +32,7 @@ export interface MarkdownOpitons extends MarkdownIt.Options {
export interface MarkdownParsedData { export interface MarkdownParsedData {
hoistedTags?: string[] hoistedTags?: string[]
links?: string[] links?: string[]
headers?: Header[]
} }
export interface MarkdownRenderer { export interface MarkdownRenderer {
@ -55,6 +57,7 @@ export const createMarkdownRenderer = (
.use(snippetPlugin) .use(snippetPlugin)
.use(hoistPlugin) .use(hoistPlugin)
.use(containerPlugin) .use(containerPlugin)
.use(extractHeaderPlugin)
.use(linkPlugin, { .use(linkPlugin, {
target: '_blank', target: '_blank',
rel: 'noopener noreferrer', rel: 'noopener noreferrer',
@ -81,7 +84,7 @@ export const createMarkdownRenderer = (
{ {
slugify, slugify,
includeLevel: [2, 3], includeLevel: [2, 3],
format: parseHeaders format: parseHeader
}, },
options.toc 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 hrefAttr = token.attrs![hrefIndex]
const url = hrefAttr[1] const url = hrefAttr[1]
const isExternal = /^https?:/.test(url) const isExternal = /^https?:/.test(url)
const isSourceLink = /(\/|\.md|\.html)(#.*)?$/.test(url)
if (isExternal) { if (isExternal) {
Object.entries(externalAttrs).forEach(([key, val]) => { Object.entries(externalAttrs).forEach(([key, val]) => {
token.attrSet(key, val) token.attrSet(key, val)
}) })
} else if (isSourceLink) { } else if (!url.startsWith('#')) {
normalizeHref(hrefAttr) normalizeHref(hrefAttr)
} }
} }
@ -31,13 +30,8 @@ export const linkPlugin = (
} }
function normalizeHref(hrefAttr: [string, string]) { function normalizeHref(hrefAttr: [string, string]) {
const data = md.__data
let url = hrefAttr[1] 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) const indexMatch = url.match(indexRE)
if (indexMatch) { if (indexMatch) {
const [, path, , hash] = indexMatch const [, path, , hash] = indexMatch
@ -51,6 +45,11 @@ export const linkPlugin = (
url = ensureBeginningDotSlash(url) 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 // markdown-it encodes the uri
hrefAttr[1] = decodeURI(url) 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 description: string
base: string base: string
themeConfig: ThemeConfig themeConfig: ThemeConfig
pages: PageData[]
}
export interface PageData {
path: string
} }
export interface ResolvedConfig<ThemeConfig = any> { 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> { export async function resolveSiteData(root: string): Promise<SiteData> {
// 1. load user config // load user config
const configPath = getConfigPath(root) const configPath = getConfigPath(root)
let hasUserConfig = false let hasUserConfig = false
try { try {
@ -73,16 +68,10 @@ export async function resolveSiteData(root: string): Promise<SiteData> {
delete require.cache[configPath] delete require.cache[configPath]
const userConfig: UserConfig = hasUserConfig ? require(configPath) : {} const userConfig: UserConfig = hasUserConfig ? require(configPath) : {}
// 2. TODO scan pages data return {
// 3. resolve site data
const site: SiteData = {
title: userConfig.title || 'VitePress', title: userConfig.title || 'VitePress',
description: userConfig.description || 'A VitePress site', description: userConfig.description || 'A VitePress site',
base: userConfig.base || '/', base: userConfig.base || '/',
themeConfig: userConfig.themeConfig || {}, themeConfig: userConfig.themeConfig || {}
pages: []
} }
return site
} }

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

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

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