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>
63 lines
1.6 KiB
TypeScript
63 lines
1.6 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
import katex from 'katex'
|
|
import { marked } from 'marked'
|
|
import { sanitizeRichHtml } from '@/lib/sanitize-content'
|
|
import 'katex/dist/katex.min.css'
|
|
|
|
/**
|
|
* Render demo `speak`: light markdown + inline $KaTeX$ via the note pipeline pieces.
|
|
*/
|
|
export function DemoSpeak({ speak, className }: { speak: string; className?: string }) {
|
|
const html = useMemo(() => {
|
|
const placeholders: string[] = []
|
|
const withSlots = speak.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 md = marked.parse(withSlots, { gfm: true, breaks: true }) as string
|
|
placeholders.forEach((frag, i) => {
|
|
md = md.replace(`%%KATEX${i}%%`, frag)
|
|
})
|
|
return sanitizeRichHtml(md)
|
|
}, [speak])
|
|
|
|
return (
|
|
<div
|
|
className={className}
|
|
dangerouslySetInnerHTML={{ __html: html }}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export function useDarkMode(): boolean {
|
|
const [isDark, setIsDark] = useState(() =>
|
|
typeof document !== 'undefined'
|
|
? document.documentElement.classList.contains('dark')
|
|
: false
|
|
)
|
|
|
|
useEffect(() => {
|
|
const check = () =>
|
|
setIsDark(document.documentElement.classList.contains('dark'))
|
|
check()
|
|
const obs = new MutationObserver(check)
|
|
obs.observe(document.documentElement, {
|
|
attributes: true,
|
|
attributeFilter: ['class'],
|
|
})
|
|
return () => obs.disconnect()
|
|
}, [])
|
|
|
|
return isDark
|
|
}
|