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 addCopyButtons = () => { if (!contentRef) return; const codeBlocks = contentRef.querySelectorAll("pre code"); codeBlocks.forEach((codeBlock) => { const pre = codeBlock.parentElement; if (!pre || pre.querySelector(".copy-button")) return; // Create wrapper for positioning pre.style.position = "relative"; // Create copy button const copyButton = document.createElement("button"); copyButton.className = "copy-button absolute top-2 right-2 px-3 py-1.5 text-xs font-medium rounded transition-all duration-200 z-10"; copyButton.style.cssText = "background-color: var(--color-surface0); color: var(--color-text); border: 1px solid var(--color-overlay0);"; copyButton.textContent = "Copy"; copyButton.addEventListener("mouseenter", () => { copyButton.style.backgroundColor = "var(--color-surface1)"; }); copyButton.addEventListener("mouseleave", () => { if (copyButton.textContent === "Copy") { copyButton.style.backgroundColor = "var(--color-surface0)"; } }); copyButton.addEventListener("click", async () => { const code = codeBlock.textContent || ""; try { await navigator.clipboard.writeText(code); copyButton.textContent = "Copied!"; copyButton.style.backgroundColor = "var(--color-green)"; copyButton.style.color = "var(--color-base)"; setTimeout(() => { copyButton.textContent = "Copy"; copyButton.style.backgroundColor = "var(--color-surface0)"; copyButton.style.color = "var(--color-text)"; }, 2000); } catch (err) { console.error("Failed to copy code:", err); copyButton.textContent = "Failed"; copyButton.style.backgroundColor = "var(--color-red)"; setTimeout(() => { copyButton.textContent = "Copy"; copyButton.style.backgroundColor = "var(--color-surface0)"; }, 2000); } }); pre.appendChild(copyButton); }); }; 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.style.cssText = "text-decoration: none; font-size: 0.75em; vertical-align: super;"; 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 headings = contentRef.querySelectorAll("h2"); let referencesSection: HTMLElement | null = null; headings.forEach((heading) => { if (heading.textContent?.trim() === "References") { referencesSection = heading; } }); if (referencesSection) { referencesSection.className = "text-2xl font-bold mb-4 text-text"; // Find the parent container and add styling const parentDiv = referencesSection.parentElement; if (parentDiv) { // Add top border and padding parentDiv.style.cssText = "border-top: 1px solid var(--surface2); margin-top: 4rem; padding-top: 2rem;"; } // Find all paragraphs after the References heading that start with [n] 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}`; // Set the ID for linking currentElement.id = refId; // Add styling currentElement.className = "reference-item transition-colors duration-500 text-sm mb-3"; currentElement.style.cssText = "scroll-margin-top: 100px;"; // Parse and style the content - get everything after [n] let refText = text.substring(match[0].length); // Remove any existing "↑ Back" text (including various Unicode arrow variants) refText = refText.replace(/[↑⬆️]\s*Back\s*$/i, "").trim(); // Create styled content currentElement.innerHTML = ""; // Add bold reference number const refNumSpan = document.createElement("span"); refNumSpan.className = "text-blue font-semibold"; refNumSpan.textContent = `[${refNumber}]`; currentElement.appendChild(refNumSpan); // Add reference text 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); } // Add back button 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" }); // Highlight the reference link briefly target.style.backgroundColor = "rgba(203, 166, 247, 0.2)"; setTimeout(() => { target.style.backgroundColor = ""; }, 2000); } }; currentElement.appendChild(backLink); } } // Check if we've reached another heading (end of references) if ( currentElement.tagName.match(/^H[1-6]$/) && currentElement !== referencesSection ) { break; } currentElement = currentElement.nextElementSibling; } } }; // Load highlight.js only when needed createEffect(() => { if (props.hasCodeBlock && !hljs()) { loadHighlightJS().then(setHljs); } }); // Apply syntax highlighting when hljs loads and when body changes createEffect(() => { const hljsInstance = hljs(); if (hljsInstance && props.hasCodeBlock && contentRef) { setTimeout(() => { hljsInstance.highlightAll(); addCopyButtons(); }, 100); } }); // Process references after content is mounted and when body changes onMount(() => { setTimeout(() => { processReferences(); if (props.hasCodeBlock) { addCopyButtons(); } }, 150); }); createEffect(() => { // Re-process when body changes if (props.body && contentRef) { setTimeout(() => { processReferences(); if (props.hasCodeBlock) { addCopyButtons(); } }, 150); } }); return (
); }