SPB Git

spb/spboucher.ai Public

spboucher.ai — personal website of Simon-Pierre Boucher.

TypeScript 93.7% HTML 5.3% CSS 0.9%
3.5 KB · 122 lines tsx
Raw Blame History
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