/* global process */
import { defineConfig } from 'vitepress'
import { createLogger } from 'vite'
import { MermaidMarkdown } from 'vitepress-plugin-mermaid'
import { createRequire } from 'module'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const require = createRequire(import.meta.url)
const markdownItFootnote = require('markdown-it-footnote')
const markdownItContainer = require('markdown-it-container')
const katex = require('katex')
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const packageJsonPath = path.resolve(__dirname, '../../package.json')
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
const docsRoot = path.resolve(__dirname, '..')
const assetManifestPath = path.resolve(
docsRoot,
'public/optimized/asset-manifest.json'
)
function loadAssetManifest() {
if (!fs.existsSync(assetManifestPath)) return { assets: {} }
try {
return JSON.parse(fs.readFileSync(assetManifestPath, 'utf8'))
} catch {
return { assets: {} }
}
}
const assetManifest = loadAssetManifest()
const isVercel = process.env.VERCEL === '1' || !!process.env.VERCEL_URL
function parseRepository() {
const repositoryUrl =
process.env.GITHUB_REPOSITORY ||
packageJson.repository?.url ||
packageJson.repository ||
''
if (repositoryUrl.includes('/')) {
if (repositoryUrl.includes(':')) {
const sshMatch = repositoryUrl.match(/github\.com:(.+?)\/(.+?)(\.git)?$/)
if (sshMatch) {
return { owner: sshMatch[1], repo: sshMatch[2] }
}
}
const normalized = repositoryUrl
.replace(/^https?:\/\/github\.com\//, '')
.replace(/^git@github\.com:/, '')
.replace(/\.git$/, '')
if (normalized.includes('/')) {
const [owner, repo] = normalized.split('/')
return { owner, repo }
}
}
return { owner: 'walkinglabs', repo: packageJson.name || 'course-template' }
}
const { owner, repo } = parseRepository()
const base = process.env.BASE || (isVercel ? '/' : `/${repo}/`)
const siteUrl = process.env.SITE_URL || `https://${owner}.github.io/${repo}`
const editLinkPattern = `https://github.com/${owner}/${repo}/edit/main/docs/:path`
const enableLocalSearch = process.env.LOCAL_SEARCH === '1'
const mermaidConfig = {
securityLevel: 'loose',
startOnLoad: false
}
function mermaidConfigPlugin() {
const virtualModuleId = 'virtual:mermaid-config'
const resolvedVirtualModuleId = `\0${virtualModuleId}`
return {
name: 'local-mermaid-config',
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId
}
},
load(id) {
if (id === resolvedVirtualModuleId) {
return `export default ${JSON.stringify(mermaidConfig)}`
}
}
}
}
function normalizeBrokenDocPathPlugin() {
const canonicalSegments = ['appendix_math', 'linear-algebra-basics']
return {
name: 'normalize-broken-doc-path',
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (!req.url) return next()
const url = new URL(req.url, 'http://localhost')
if (!url.pathname.includes('appendix')) return next()
const decodedPathname = decodeURIComponent(url.pathname)
const normalizedDecodedPathname = decodedPathname
.replace(/\s+/g, '')
.replace(/appendix_m+ath/gi, canonicalSegments[0])
.replace(/linea+r-algebra-basics/gi, canonicalSegments[1])
if (normalizedDecodedPathname !== decodedPathname) {
const normalizedPathname = normalizedDecodedPathname
.split('/')
.map((segment) => encodeURIComponent(segment))
.join('/')
const redirectTarget = `${normalizedPathname}${url.search}`
res.statusCode = 302
res.setHeader('Location', redirectTarget)
res.end()
return
}
next()
})
}
}
}
function escapeHtml(value) {
return value
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
}
function slugifySearchHeading(value) {
return value
.normalize('NFKD')
.replace(/[\u0300-\u036F]/g, '')
.replace(/[\x00-\x1f]/g, '')
.replace(/[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'“”‘’<>,.?/]+/g, '-')
.replace(/-{2,}/g, '-')
.replace(/^-+|-+$/g, '')
.replace(/^(\d)/, '_$1')
.toLowerCase()
}
function stripMarkdown(value) {
return value
.replace(/`([^`]+)`/g, '$1')
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/[*_~>#-]/g, '')
.trim()
}
function renderSearchMarkdown(src) {
const html = []
const slugCounts = new Map()
let inFence = false
for (const rawLine of src.replace(/^---[\s\S]*?---\n/, '').split('\n')) {
const line = rawLine.trim()
if (line.startsWith('```')) {
inFence = !inFence
continue
}
if (!line || inFence || line.startsWith(':::')) {
continue
}
const heading = /^(#{1,6})\s+(.+)$/.exec(line)
if (heading) {
const level = heading[1].length
const title = stripMarkdown(heading[2])
const escapedTitle = escapeHtml(title)
const baseSlug = slugifySearchHeading(title)
const count = slugCounts.get(baseSlug) || 0
slugCounts.set(baseSlug, count + 1)
const slug = count === 0 ? baseSlug : `${baseSlug}-${count}`
html.push(
`
${escapeHtml(stripMarkdown(line))}
`) } return html.join('\n') } function isValidMathDelimiter(state, pos) { const max = state.posMax const prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1 const nextChar = pos + 1 < max ? state.src.charCodeAt(pos + 1) : -1 return { canOpen: nextChar !== 0x20 && nextChar !== 0x09, canClose: prevChar !== 0x20 && prevChar !== 0x09 && (nextChar < 0x30 || nextChar > 0x39) } } function mathInline(state, silent) { if (state.src[state.pos] !== '$') return false // Display math inline: $$...$$ inside a paragraph/list/blockquote line. // Must be tried before the single-$ branch so the closing $$ isn't read as // two consecutive empty inline-math delimiters. if (state.pos + 1 < state.posMax && state.src[state.pos + 1] === '$') { const start = state.pos + 2 const close = state.src.indexOf('$$', start) if (close !== -1 && close < state.posMax) { if (!silent) { const token = state.push('math_inline', 'math', 0) token.markup = '$$' token.content = state.src.slice(start, close) token.displayMode = true } state.pos = close + 2 return true } } let delimiter = isValidMathDelimiter(state, state.pos) if (!delimiter.canOpen) { if (!silent) state.pending += '$' state.pos += 1 return true } const start = state.pos + 1 const max = state.posMax let match = start while ((match = state.src.indexOf('$', match)) !== -1) { if (match >= max) { match = -1 break } let pos = match - 1 while (state.src[pos] === '\\') pos -= 1 if ((match - pos) % 2 === 1) break match += 1 } if (match === -1) { if (!silent) state.pending += '$' state.pos = start return true } if (match - start === 0) { if (!silent) state.pending += '$$' state.pos = start + 1 return true } delimiter = isValidMathDelimiter(state, match) if (!delimiter.canClose) { if (!silent) state.pending += '$' state.pos = start return true } if (!silent) { const token = state.push('math_inline', 'math', 0) token.markup = '$' token.content = state.src.slice(start, match) } state.pos = match + 1 return true } function mathBlock(state, start, end, silent) { let pos = state.bMarks[start] + state.tShift[start] const max = state.eMarks[start] if (pos + 2 > max) return false if (state.src.slice(pos, pos + 2) !== '$$') return false pos += 2 let firstLine = state.src.slice(pos, max) let lastLine = '' let found = false let next = start if (silent) return true if (firstLine.trim().slice(-2) === '$$') { firstLine = firstLine.trim().slice(0, -2) found = true } while (!found) { next++ if (next >= end) break pos = state.bMarks[next] + state.tShift[next] const lineMax = state.eMarks[next] if (pos < lineMax && state.tShift[next] < state.blkIndent) break if (state.src.slice(pos, lineMax).trim().slice(-2) === '$$') { const lastPos = state.src.slice(0, lineMax).lastIndexOf('$$') lastLine = state.src.slice(pos, lastPos) found = true } } state.line = next + 1 const token = state.push('math_block', 'math', 0) token.block = true token.content = (firstLine && firstLine.trim() ? `${firstLine}\n` : '') + state.getLines(start + 1, next, state.tShift[start], true) + (lastLine && lastLine.trim() ? lastLine : '') token.map = [start, state.line] token.markup = '$$' return true } function renderKatex(content, displayMode) { try { return katex.renderToString(content, { displayMode, output: 'html', throwOnError: true, // Enable error throwing for debugging strict: false, trust: true }) } catch (error) { // Log detailed error information with file context const fs = require('fs') const path = require('path') const markdownFile = path.join( process.cwd(), 'docs/chapter10_ppo/ppo-math.md' ) console.error('\n' + '='.repeat(80)) console.error('❌ KaTeX Rendering Error') console.error('='.repeat(80)) console.error( `Mode: ${displayMode ? 'Display Math ($$...$$)' : 'Inline Math ($...$)'}` ) console.error( `Expression: ${content.substring(0, 200)}${content.length > 200 ? '...' : ''}` ) console.error(`Error: ${error.message}`) if (error.position !== undefined) { console.error(`Position in expression: ${error.position}`) const start = Math.max(0, error.position - 50) const end = Math.min(content.length, error.position + 50) console.error(`Context: ...${content.substring(start, end)}...`) } // Try to find this expression in the markdown file try { const mdContent = fs.readFileSync(markdownFile, 'utf8') const searchStr = content.substring(0, Math.min(100, content.length)) const index = mdContent.indexOf(searchStr) if (index !== -1) { const lineNum = mdContent.substring(0, index).split('\n').length console.error( `Location: ${markdownFile}, approximately line ${lineNum}` ) // Show surrounding lines const lines = mdContent.split('\n') const startLine = Math.max(0, lineNum - 3) const endLine = Math.min(lines.length, lineNum + 2) console.error('\nSurrounding context:') for (let i = startLine; i < endLine; i++) { const marker = i + 1 === lineNum ? '>>> ' : ' ' console.error(`${marker}${i + 1}: ${lines[i].substring(0, 100)}`) } } } catch (e) { // Ignore file reading errors } console.error('='.repeat(80) + '\n') // Return error HTML for visibility in the page return `Mode: ${displayMode ? 'Display' : 'Inline'}
Expression: ${content.substring(0, 150)}${content.length > 150 ? '...' : ''}
Error: ${error.message}
Check browser console for detailed stack trace and file location.
${renderKatex(tokens[idx].content, true)}
\n` } function footnoteTitlePlugin(md) { md.renderer.rules.footnote_block_open = (tokens, idx, options, env) => { const previousContentToken = [...tokens] .slice(0, idx) .reverse() .find((token) => token.type === 'inline' && token.content?.trim()) const previousContent = (previousContentToken?.content || '') .replace(/[*_`#]/g, '') .trim() const hasManualTitle = /^(参考文献|References)[::]?$/.test(previousContent) const title = env.relativePath?.startsWith('en/') ? 'References' : '参考文献' const heading = hasManualTitle ? '' : `${title}
\n` } return '