1// @ts-expect-error no types for react-dom/server 2import { renderToString } from 'react-dom/server' 3import React from 'react' 4import esbuild from 'esbuild' 7import { marked } from 'marked' 9marked.use({breaks: true}) 11export const preserveLeadingSpaces = (md: string) => { 12 const parts = md.split(/(```[\s\S]*?```)/g) 13 return parts.map(part => part.startsWith('```') ? part : part.replace(/^( +)/gm, (_: string, spaces: string) => ' '.repeat(spaces.length))).join('') 21const jsxCache = new Map() 23export const loadJsxComponent = async (jsxPath: string, exportName = 'default') => { 24 const cacheKey = `${jsxPath}:${exportName}` 25 const cached = jsxCache.get(cacheKey) 26 if (cached && cached.inputs.every((p: string) => fs.existsSync(p))) { 27 const maxMtime = Math.max(...cached.inputs.map((p: string) => fs.statSync(p).mtimeMs)) 28 if (cached.mtime === maxMtime) return cached.component 31 const result = await esbuild.build({ 32 entryPoints: [jsxPath], 38 external: ['react', 'react-dom', 'react/jsx-runtime'], 41 const inputs = Object.keys(result.metafile.inputs).map(p => path.resolve(p)) 42 const mtime = Math.max(...inputs.map(p => fs.statSync(p).mtimeMs)) 43 const tmpPath = path.join(ptTmpDir, `jsx-${Date.now()}.mjs`) 44 fs.writeFileSync(tmpPath, result.outputFiles[0].text) 45 const mod = await import(tmpPath) 46 fs.unlinkSync(tmpPath) 47 const component = mod[exportName] 48 jsxCache.set(cacheKey, {mtime, inputs, component}) 52export const renderJsxToHtml = async (jsxPath: string, exportName: string, props: Record<string, unknown>) => { 53 const Component = await loadJsxComponent(jsxPath, exportName) 54 return '<!DOCTYPE html>' + renderToString(React.createElement(Component, props)) 57const extractTranscriptContent = (html: string) => { 58 const startMarker = '<!--TRANSCRIPT_START-->', endMarker = '<!--TRANSCRIPT_END-->' 59 const startIdx = html.indexOf(startMarker), endIdx = html.indexOf(endMarker) 60 if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return null 61 return html.slice(startIdx + startMarker.length, endIdx) 64const injectTranscripts = async (html: string) => { 65 const transcriptRegex = /<p>\{\{transcript:(ses_[a-zA-Z0-9]+)(?::startAtStr=(.+?))?\}\}<\/p>/g 66 const matches = [...html.matchAll(transcriptRegex)] 67 for (const match of matches) { 68 const [fullMatch, sessionId, startAtStr] = match 71 const content = extractTranscriptContent(transcriptHtml) 72 if (!content) { html = html.replace(fullMatch, `<p>Could not parse transcript: ${sessionId}</p>`); continue } 75 html = html.replace(fullMatch, `<p>Transcript error: ${e.message}</p>`) 81export const renderMarkdownPage = async ({markdownPath, markdownStr, title, styles, homeHref, bodyClasses, rawHtmlSuffix}: {markdownPath?: string, markdownStr?: string, title?: string, styles?: string, homeHref?: string, bodyClasses?: string, rawHtmlSuffix?: string}) => { 82 const rawMd = markdownStr || (markdownPath && fs.existsSync(markdownPath) ? fs.readFileSync(markdownPath, 'utf8') : '') 83 let markdownHtml = marked(preserveLeadingSpaces(rawMd)) 84 markdownHtml = await injectTranscripts(markdownHtml) 85 return `<!DOCTYPE html> 88 <meta charset="UTF-8"> 90 <title>${title || 'Page'}</title> 91 <style>${styles || ''}</style> 93<body class="blogpost-page${bodyClasses ? ' ' + bodyClasses : ''}"> 94 ${homeHref ? `<a href="${homeHref}" class="home-link">🌳</a>` : ''} 95 <div class="markdown-content">${markdownHtml || ''}</div> 96 ${rawHtmlSuffix || ''}