Ajoute le pipeline PageSpec (validation, rendu, publication /p/{slug}),
les démos TipTap /demo, et le simulateur Carnot (modes frigo/PAC/moteur,
énergie kJ vs puissance W, unités K/°C/°F) avec correctifs d’équations KaTeX.
Co-authored-by: Cursor <cursoragent@cursor.com>
92 lines
2.1 KiB
TypeScript
92 lines
2.1 KiB
TypeScript
'use client'
|
|
|
|
import { useMemo } from 'react'
|
|
import katex from 'katex'
|
|
import { marked } from 'marked'
|
|
import { sanitizeRichHtml } from '@/lib/sanitize-content'
|
|
import 'katex/dist/katex.min.css'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
/** Light markdown + inline $KaTeX$ for page prose / callouts / speak. */
|
|
export function PageMd({
|
|
md,
|
|
className,
|
|
}: {
|
|
md: string
|
|
className?: string
|
|
}) {
|
|
const html = useMemo(() => {
|
|
const placeholders: string[] = []
|
|
const withSlots = md.replace(/\$([^$]+)\$/g, (_, tex: string) => {
|
|
const i = placeholders.length
|
|
try {
|
|
placeholders.push(
|
|
katex.renderToString(tex, { displayMode: false, throwOnError: false })
|
|
)
|
|
} catch {
|
|
placeholders.push(tex)
|
|
}
|
|
return `%%KATEX${i}%%`
|
|
})
|
|
|
|
let out = marked.parse(withSlots, { gfm: true, breaks: true }) as string
|
|
placeholders.forEach((frag, i) => {
|
|
out = out.replace(`%%KATEX${i}%%`, frag)
|
|
})
|
|
return sanitizeRichHtml(out)
|
|
}, [md])
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'prose prose-neutral dark:prose-invert max-w-none prose-p:leading-relaxed prose-headings:tracking-tight',
|
|
className
|
|
)}
|
|
dangerouslySetInnerHTML={{ __html: html }}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export function PageFormula({
|
|
tex,
|
|
caption,
|
|
}: {
|
|
tex: string
|
|
caption?: string
|
|
}) {
|
|
const html = useMemo(() => {
|
|
try {
|
|
return katex.renderToString(tex, {
|
|
displayMode: true,
|
|
throwOnError: false,
|
|
})
|
|
} catch {
|
|
return tex
|
|
}
|
|
}, [tex])
|
|
|
|
return (
|
|
<figure
|
|
className="my-5 overflow-x-auto rounded-xl border px-4 py-4 md:px-5"
|
|
style={{
|
|
background: 'var(--pp-paper)',
|
|
borderColor: 'var(--pp-line)',
|
|
borderLeft: '4px solid var(--pp-plum)',
|
|
}}
|
|
>
|
|
<div
|
|
className="text-[1.15em] [&_.katex-display]:m-0"
|
|
dangerouslySetInnerHTML={{ __html: html }}
|
|
/>
|
|
{caption ? (
|
|
<figcaption
|
|
className="mt-2.5 text-sm"
|
|
style={{ color: 'var(--pp-muted)' }}
|
|
>
|
|
{caption}
|
|
</figcaption>
|
|
) : null}
|
|
</figure>
|
|
)
|
|
}
|