spb/spboucher.ai Public
spboucher.ai — personal website of Simon-Pierre Boucher.
TypeScript 93.7%
HTML 5.3%
CSS 0.9%
1/*2 blog-content.tsx3 spboucher.ai Web4 Author: Simon-Pierre Boucher5 Mail: contact@spboucher.ai6*/78import katex from "katex";9import "katex/dist/katex.min.css";1011import type { BlogBlock } from "@/lib/blog";1213function renderTex(tex: string, displayMode: boolean): string {14 return katex.renderToString(tex, {15 displayMode,16 throwOnError: false,17 strict: false,18 });19}2021/** Render the **bold** and inline-math \( ... \) spans used by the essays. */22function renderInline(text: string) {23 return text.split(/(\*\*[^*]+\*\*|\\\(.+?\\\))/g).map((part, i) => {24 if (part.startsWith("**") && part.endsWith("**")) {25 return (26 <strong key={i} className="font-semibold text-foreground">27 {part.slice(2, -2)}28 </strong>29 );30 }31 if (part.startsWith("\\(") && part.endsWith("\\)")) {32 return (33 <span34 key={i}35 dangerouslySetInnerHTML={{36 __html: renderTex(part.slice(2, -2).trim(), false),37 }}38 />39 );40 }41 return <span key={i}>{part}</span>;42 });43}4445/** Long-form essay body with sharp editorial typography. */46export function BlogContent({ blocks }: { blocks: BlogBlock[] }) {47 let paragraphIndex = 0;4849 return (50 <div className="space-y-5">51 {blocks.map((block, i) => {52 switch (block.type) {53 case "math":54 return (55 <div56 key={i}57 className="overflow-x-auto border-y border-border bg-muted/40 px-5 py-4 text-foreground/90"58 dangerouslySetInnerHTML={{59 __html: renderTex(block.tex, true),60 }}61 />62 );63 case "h2":64 return (65 <div key={i} className="pt-7">66 <span67 aria-hidden="true"68 className="block h-1 w-10 rounded-full bg-gradient-to-r from-primary to-primary/30"69 />70 <h2 className="mt-4 font-display text-2xl font-bold tracking-tight">71 {block.text}72 </h2>73 </div>74 );75 case "quote":76 return (77 <blockquote78 key={i}79 className="border-l-2 border-primary/60 pl-5 font-display text-xl font-medium leading-snug tracking-tight text-foreground/90"80 >81 {renderInline(block.text)}82 </blockquote>83 );84 case "ul":85 return (86 <ul key={i} className="space-y-2 pl-1">87 {block.items.map((item) => (88 <li89 key={item}90 className="flex items-start gap-3 leading-relaxed text-muted-foreground"91 >92 <span93 aria-hidden="true"94 className="mt-[0.6em] h-1.5 w-1.5 shrink-0 rounded-full bg-primary/70"95 />96 <span>{renderInline(item)}</span>97 </li>98 ))}99 </ul>100 );101 case "p": {102 paragraphIndex += 1;103 const isLead = paragraphIndex === 1;104 return (105 <p106 key={i}107 className={108 isLead109 ? "text-lg leading-relaxed text-foreground/90 sm:text-xl"110 : "leading-relaxed text-muted-foreground"111 }112 >113 {renderInline(block.text)}114 </p>115 );116 }117 }118 })}119 </div>120 );121}122