import { createEffect, createSignal, onMount } from "solid-js"; import type { HLJSApi } from "highlight.js"; import MermaidRenderer from "./MermaidRenderer"; export interface PostBodyClientProps { body: string; hasCodeBlock: boolean; } async function loadHighlightJS(): Promise { const [ hljsModule, javascript, typescript, python, rust, c, cpp, csharp, ocaml, lua, swift, bash, css, xml, // handles HTML go, glsl, json, markdown, yaml, sql, diff, toml ] = await Promise.all([ import("highlight.js/lib/core"), import("highlight.js/lib/languages/javascript"), import("highlight.js/lib/languages/typescript"), import("highlight.js/lib/languages/python"), import("highlight.js/lib/languages/rust"), import("highlight.js/lib/languages/c"), import("highlight.js/lib/languages/cpp"), import("highlight.js/lib/languages/csharp"), import("highlight.js/lib/languages/ocaml"), import("highlight.js/lib/languages/lua"), import("highlight.js/lib/languages/swift"), import("highlight.js/lib/languages/bash"), import("highlight.js/lib/languages/css"), import("highlight.js/lib/languages/xml"), import("highlight.js/lib/languages/go"), import("highlight.js/lib/languages/glsl"), import("highlight.js/lib/languages/json"), import("highlight.js/lib/languages/markdown"), import("highlight.js/lib/languages/yaml"), import("highlight.js/lib/languages/sql"), import("highlight.js/lib/languages/diff"), import("highlight.js/lib/languages/ini"), // handles TOML import("highlight.js/styles/github-dark.css") ]); const hljs = hljsModule.default; hljs.registerLanguage("javascript", javascript.default); hljs.registerLanguage("typescript", typescript.default); hljs.registerLanguage("python", python.default); hljs.registerLanguage("rust", rust.default); hljs.registerLanguage("c", c.default); hljs.registerLanguage("cpp", cpp.default); hljs.registerLanguage("csharp", csharp.default); hljs.registerLanguage("ocaml", ocaml.default); hljs.registerLanguage("lua", lua.default); hljs.registerLanguage("swift", swift.default); hljs.registerLanguage("bash", bash.default); hljs.registerLanguage("sh", bash.default); // alias hljs.registerLanguage("css", css.default); hljs.registerLanguage("html", xml.default); hljs.registerLanguage("xml", xml.default); hljs.registerLanguage("go", go.default); hljs.registerLanguage("glsl", glsl.default); hljs.registerLanguage("json", json.default); hljs.registerLanguage("markdown", markdown.default); hljs.registerLanguage("yaml", yaml.default); hljs.registerLanguage("yml", yaml.default); // alias hljs.registerLanguage("sql", sql.default); hljs.registerLanguage("diff", diff.default); hljs.registerLanguage("toml", toml.default); hljs.registerLanguage("js", javascript.default); hljs.registerLanguage("ts", typescript.default); hljs.registerLanguage("jsx", javascript.default); hljs.registerLanguage("tsx", typescript.default); return hljs; } export default function PostBodyClient(props: PostBodyClientProps) { let contentRef: HTMLDivElement | undefined; const [hljs, setHljs] = createSignal(null); const processCodeBlocks = () => { if (!contentRef) return; const codeBlocks = contentRef.querySelectorAll("pre code"); codeBlocks.forEach((codeBlock) => { const pre = codeBlock.parentElement; if (!pre) return; if (pre.dataset.type === "mermaid") return; const existingHeader = pre.previousElementSibling; if ( existingHeader?.classList.contains("language-header") && existingHeader.querySelector(".copy-button") ) { return; } pre.style.backgroundColor = "#1a1a1a"; const classes = Array.from(codeBlock.classList); const languageClass = classes.find((cls) => cls.startsWith("language-")); const language = languageClass?.replace("language-", "") || ""; if (language) { const languageHeader = document.createElement("div"); languageHeader.className = "language-header"; languageHeader.style.backgroundColor = "#1a1a1a"; const languageLabel = document.createElement("span"); languageLabel.textContent = language; languageHeader.appendChild(languageLabel); const copyButton = document.createElement("button"); copyButton.className = "copy-button"; copyButton.textContent = "Copy"; copyButton.dataset.codeBlock = "true"; copyButton.dataset.codeBlockId = `code-${Math.random().toString(36).substr(2, 9)}`; codeBlock.dataset.codeBlockId = copyButton.dataset.codeBlockId; languageHeader.appendChild(copyButton); pre.parentElement?.insertBefore(languageHeader, pre); } const codeText = codeBlock.textContent || ""; const lines = codeText.split("\n"); const lineCount = lines[lines.length - 1] === "" ? lines.length - 1 : lines.length; if (lineCount > 0 && !pre.querySelector(".line-numbers")) { const lineNumbers = document.createElement("div"); lineNumbers.className = "line-numbers"; for (let i = 1; i <= lineCount; i++) { const lineNum = document.createElement("div"); lineNum.textContent = i.toString(); lineNumbers.appendChild(lineNum); } pre.appendChild(lineNumbers); } }); }; const processReferences = () => { if (!contentRef) return; const supElements = contentRef.querySelectorAll("sup"); supElements.forEach((sup) => { const text = sup.textContent?.trim() || ""; const match = text.match(/^\[(.+?)\]$/); if (match) { const refNumber = match[1]; const refId = `ref-${refNumber}`; const refBackId = `ref-${refNumber}-back`; sup.id = refBackId; sup.innerHTML = ""; const link = document.createElement("a"); link.href = `#${refId}`; link.textContent = `[${refNumber}]`; link.className = "reference-link text-blue hover:text-sky no-underline cursor-pointer"; link.onclick = (e) => { e.preventDefault(); const target = document.getElementById(refId); if (target) { target.scrollIntoView({ behavior: "smooth", block: "center" }); target.style.backgroundColor = "rgba(137, 180, 250, 0.2)"; setTimeout(() => { target.style.backgroundColor = ""; }, 2000); } }; sup.appendChild(link); } }); const marker = contentRef.querySelector( "span[id='references-section-start']" ) as HTMLElement | null; const referencesHeadingText = marker?.getAttribute("data-heading") || "References"; const headings = contentRef.querySelectorAll("h2"); let referencesSection: HTMLElement | null = null; headings.forEach((heading) => { if (heading.textContent?.trim() === referencesHeadingText) { referencesSection = heading; } }); if (referencesSection) { referencesSection.className = "text-2xl font-bold mb-4 text-text"; const parentDiv = referencesSection.parentElement; if (parentDiv) { parentDiv.classList.add("references-heading"); } let currentElement = referencesSection.nextElementSibling; while (currentElement) { if (currentElement.tagName === "P") { const text = currentElement.textContent?.trim() || ""; const match = text.match(/^\[(.+?)\]\s*/); if (match) { const refNumber = match[1]; const refId = `ref-${refNumber}`; currentElement.id = refId; currentElement.className = "reference-item transition-colors duration-500 text-sm mb-3"; let refText = text.substring(match[0].length); refText = refText.replace(/[↑⬆️]\s*Back\s*$/i, "").trim(); currentElement.innerHTML = ""; const refNumSpan = document.createElement("span"); refNumSpan.className = "text-blue font-semibold"; refNumSpan.textContent = `[${refNumber}]`; currentElement.appendChild(refNumSpan); if (refText) { const refTextSpan = document.createElement("span"); refTextSpan.className = "ml-2"; refTextSpan.textContent = refText; currentElement.appendChild(refTextSpan); } else { const refTextSpan = document.createElement("span"); refTextSpan.className = "ml-2 text-subtext0 italic"; refTextSpan.textContent = "Add your reference text here"; currentElement.appendChild(refTextSpan); } const backLink = document.createElement("a"); backLink.href = `#ref-${refNumber}-back`; backLink.className = "text-mauve hover:text-pink ml-2 text-xs cursor-pointer"; backLink.textContent = "↑ Back"; backLink.onclick = (e) => { e.preventDefault(); const target = document.getElementById(`ref-${refNumber}-back`); if (target) { target.scrollIntoView({ behavior: "smooth", block: "center" }); target.style.backgroundColor = "rgba(203, 166, 247, 0.2)"; setTimeout(() => { target.style.backgroundColor = ""; }, 2000); } }; currentElement.appendChild(backLink); } } if ( currentElement.tagName.match(/^H[1-6]$/) && currentElement !== referencesSection ) { break; } currentElement = currentElement.nextElementSibling; } } }; createEffect(() => { if (props.hasCodeBlock && !hljs()) { loadHighlightJS().then(setHljs); } }); createEffect(() => { const hljsInstance = hljs(); if (hljsInstance && props.hasCodeBlock && contentRef) { setTimeout(() => { hljsInstance.highlightAll(); processCodeBlocks(); }, 100); } }); onMount(() => { setTimeout(() => { processReferences(); if (props.hasCodeBlock) { processCodeBlocks(); } }, 150); if (contentRef) { const handleCopyButtonInteraction = async (e: Event) => { const target = e.target as HTMLElement; if (e.type === "click" && target.classList.contains("copy-button")) { const codeBlockId = target.dataset.codeBlockId; const codeBlock = codeBlockId ? contentRef?.querySelector( `code[data-code-block-id="${codeBlockId}"]` ) : null; if (!codeBlock) return; const code = codeBlock.textContent || ""; try { await navigator.clipboard.writeText(code); target.textContent = "Copied!"; target.classList.add("copied"); setTimeout(() => { target.textContent = "Copy"; target.classList.remove("copied"); }, 2000); } catch (err) { console.error("Failed to copy code:", err); target.textContent = "Failed"; target.classList.add("failed"); setTimeout(() => { target.textContent = "Copy"; target.classList.remove("failed"); }, 2000); } } }; contentRef.addEventListener("click", handleCopyButtonInteraction); } }); createEffect(() => { if (props.body && contentRef) { setTimeout(() => { processReferences(); if (props.hasCodeBlock) { processCodeBlocks(); } }, 150); } }); return (
); }