references and spinner fixes

This commit is contained in:
Michael Freno
2025-12-22 01:18:49 -05:00
parent 12b36815df
commit 281654081d
9 changed files with 484 additions and 89 deletions

View File

@@ -1,5 +1,4 @@
import { createEffect } from "solid-js";
import { createSignal } from "solid-js";
import { createEffect, createSignal, onMount } from "solid-js";
import type { HLJSApi } from "highlight.js";
import MermaidRenderer from "./MermaidRenderer";
@@ -98,6 +97,161 @@ export default function PostBodyClient(props: PostBodyClientProps) {
let contentRef: HTMLDivElement | undefined;
const [hljs, setHljs] = createSignal<HLJSApi | null>(null);
// Process superscript references and enhance the References section
const processReferences = () => {
if (!contentRef) return;
const foundRefs = new Map<string, HTMLElement>();
// Find all <sup> elements with [n] pattern
const supElements = contentRef.querySelectorAll("sup");
supElements.forEach((sup) => {
const text = sup.textContent?.trim() || "";
// Match patterns like [1], [2], [a], [*], etc.
const match = text.match(/^\[(.+?)\]$/);
if (match) {
const refNumber = match[1];
const refId = `ref-${refNumber}`;
const refBackId = `ref-${refNumber}-back`;
// Add ID to the sup element itself for back navigation
sup.id = refBackId;
// Replace sup content with a clickable link
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;";
// Add smooth scroll behavior
link.onclick = (e) => {
e.preventDefault();
const target = document.getElementById(refId);
if (target) {
target.scrollIntoView({ behavior: "smooth", block: "center" });
// Highlight the reference briefly
target.style.backgroundColor = "rgba(137, 180, 250, 0.2)";
setTimeout(() => {
target.style.backgroundColor = "";
}, 2000);
}
};
sup.appendChild(link);
}
});
// Find and enhance the References section
const headings = contentRef.querySelectorAll("h2");
let referencesSection: HTMLElement | null = null;
headings.forEach((heading) => {
if (heading.textContent?.trim() === "References") {
referencesSection = heading;
}
});
if (referencesSection) {
// Style the References heading
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()) {
@@ -115,6 +269,22 @@ export default function PostBodyClient(props: PostBodyClientProps) {
}
});
// Process references after content is mounted and when body changes
onMount(() => {
setTimeout(() => {
processReferences();
}, 150);
});
createEffect(() => {
// Re-process when body changes
if (props.body && contentRef) {
setTimeout(() => {
processReferences();
}, 150);
}
});
return (
<div class="mx-auto max-w-4xl px-4 pt-32 md:pt-40">
<div

View File

@@ -17,6 +17,8 @@ import { Node } from "@tiptap/core";
import { createLowlight, common } from "lowlight";
import { Mermaid } from "./extensions/Mermaid";
import TextAlign from "@tiptap/extension-text-align";
import Superscript from "@tiptap/extension-superscript";
import Subscript from "@tiptap/extension-subscript";
import css from "highlight.js/lib/languages/css";
import js from "highlight.js/lib/languages/javascript";
import ts from "highlight.js/lib/languages/typescript";
@@ -196,7 +198,9 @@ const KEYBOARD_SHORTCUTS: ShortcutCategory[] = [
{ keys: "⌘ B", keysAlt: "Ctrl B", description: "Bold" },
{ keys: "⌘ I", keysAlt: "Ctrl I", description: "Italic" },
{ keys: "⌘ ⇧ X", keysAlt: "Ctrl Shift X", description: "Strikethrough" },
{ keys: "⌘ E", keysAlt: "Ctrl E", description: "Inline Code" }
{ keys: "⌘ E", keysAlt: "Ctrl E", description: "Inline Code" },
{ keys: "⌘ .", keysAlt: "Ctrl .", description: "Superscript" },
{ keys: "⌘ ,", keysAlt: "Ctrl ,", description: "Subscript" }
]
},
{
@@ -432,7 +436,9 @@ export default function TextEditor(props: TextEditorProps) {
types: ["heading", "paragraph"],
alignments: ["left", "center", "right", "justify"],
defaultAlignment: "left"
})
}),
Superscript,
Subscript
],
content: props.preSet || `<p><em><b>Hello!</b> World</em></p>`,
editorProps: {
@@ -474,6 +480,8 @@ export default function TextEditor(props: TextEditorProps) {
onUpdate: ({ editor }) => {
untrack(() => {
props.updateContent(editor.getHTML());
// Auto-manage references section
setTimeout(() => updateReferencesSection(editor), 100);
});
},
onSelectionUpdate: ({ editor }) => {
@@ -505,13 +513,179 @@ export default function TextEditor(props: TextEditorProps) {
(newContent) => {
const instance = editor();
if (instance && newContent && instance.getHTML() !== newContent) {
instance.commands.setContent(newContent, false); // false = don't emit update event
instance.commands.setContent(newContent, { emitUpdate: false });
}
},
{ defer: true }
)
);
// Auto-manage references section
const updateReferencesSection = (editorInstance: any) => {
if (!editorInstance) return;
const doc = editorInstance.state.doc;
const foundRefs = new Set<string>();
// Scan document for superscript marks containing [n] patterns
doc.descendants((node: any) => {
if (node.isText && node.marks) {
const hasSuperscript = node.marks.some(
(mark: any) => mark.type.name === "superscript"
);
if (hasSuperscript) {
const text = node.text || "";
const match = text.match(/^\[(.+?)\]$/);
if (match) {
foundRefs.add(match[1]);
}
}
}
});
// If no references found, remove references section if it exists
if (foundRefs.size === 0) {
let hasReferencesSection = false;
let hrPos = -1;
let sectionStartPos = -1;
doc.descendants((node: any, pos: number) => {
if (node.type.name === "heading" && node.textContent === "References") {
hasReferencesSection = true;
sectionStartPos = pos;
}
});
if (hasReferencesSection && sectionStartPos > 0) {
// Find the HR before References heading
doc.nodesBetween(
Math.max(0, sectionStartPos - 50),
sectionStartPos,
(node: any, pos: number) => {
if (node.type.name === "horizontalRule") {
hrPos = pos;
}
}
);
// Delete from HR to end of document
if (hrPos >= 0) {
const tr = editorInstance.state.tr;
tr.delete(hrPos, doc.content.size);
editorInstance.view.dispatch(tr);
}
}
return;
}
// Convert Set to sorted array
const refNumbers = Array.from(foundRefs).sort((a, b) => {
const numA = parseInt(a);
const numB = parseInt(b);
if (!isNaN(numA) && !isNaN(numB)) {
return numA - numB;
}
return a.localeCompare(b);
});
// Check if References section already exists
let referencesHeadingPos = -1;
let existingRefs = new Set<string>();
doc.descendants((node: any, pos: number) => {
if (node.type.name === "heading" && node.textContent === "References") {
referencesHeadingPos = pos;
}
// Check for existing reference list items
if (referencesHeadingPos >= 0 && node.type.name === "paragraph") {
const match = node.textContent.match(/^\[(.+?)\]/);
if (match) {
existingRefs.add(match[1]);
}
}
});
// If references section doesn't exist, create it
if (referencesHeadingPos === -1) {
const content: any[] = [
{ type: "horizontalRule" },
{
type: "heading",
attrs: { level: 2 },
content: [{ type: "text", text: "References" }]
}
];
// Add each reference as a paragraph
refNumbers.forEach((refNum) => {
content.push({
type: "paragraph",
content: [
{
type: "text",
text: `[${refNum}] `,
marks: [{ type: "bold" }]
} as any,
{
type: "text",
text: "Add your reference text here"
}
]
});
});
// Insert at the end
const tr = editorInstance.state.tr;
tr.insert(
doc.content.size,
editorInstance.schema.nodeFromJSON({ type: "doc", content }).content
);
editorInstance.view.dispatch(tr);
} else {
// Update existing references section - add missing refs
const newRefs = refNumbers.filter((ref) => !existingRefs.has(ref));
if (newRefs.length > 0) {
// Find position after References heading to insert new refs
let insertPos = referencesHeadingPos;
doc.nodesBetween(
referencesHeadingPos,
doc.content.size,
(node: any, pos: number) => {
if (pos > insertPos) {
insertPos = pos + node.nodeSize;
}
}
);
const content: any[] = [];
newRefs.forEach((refNum) => {
content.push({
type: "paragraph",
content: [
{
type: "text",
text: `[${refNum}] `,
marks: [{ type: "bold" }]
} as any,
{
type: "text",
text: "Add your reference text here"
}
]
});
});
const tr = editorInstance.state.tr;
content.forEach((item) => {
tr.insert(insertPos, editorInstance.schema.nodeFromJSON(item));
insertPos += editorInstance.schema.nodeFromJSON(item).nodeSize;
});
editorInstance.view.dispatch(tr);
}
}
};
const setLink = () => {
const instance = editor();
if (!instance) return;
@@ -1057,6 +1231,34 @@ export default function TextEditor(props: TextEditorProps) {
>
Code
</button>
<button
type="button"
onClick={() =>
instance().chain().focus().toggleSuperscript().run()
}
class={`${
instance().isActive("superscript")
? "bg-crust"
: "hover:bg-crust"
} bg-opacity-30 hover:bg-opacity-30 rounded px-2 py-1`}
title="Superscript (Reference)"
>
X<sup>n</sup>
</button>
<button
type="button"
onClick={() =>
instance().chain().focus().toggleSubscript().run()
}
class={`${
instance().isActive("subscript")
? "bg-crust"
: "hover:bg-crust"
} bg-opacity-30 hover:bg-opacity-30 rounded px-2 py-1`}
title="Subscript"
>
X<sub>n</sub>
</button>
<button
type="button"
onClick={() =>
@@ -1321,6 +1523,34 @@ export default function TextEditor(props: TextEditorProps) {
>
<s>S</s>
</button>
<button
type="button"
onClick={() =>
instance().chain().focus().toggleSuperscript().run()
}
class={`${
instance().isActive("superscript")
? "bg-surface2"
: "hover:bg-surface1"
} rounded px-2 py-1 text-xs`}
title="Superscript (for references)"
>
X<sup class="text-[0.6em]">n</sup>
</button>
<button
type="button"
onClick={() =>
instance().chain().focus().toggleSubscript().run()
}
class={`${
instance().isActive("subscript")
? "bg-surface2"
: "hover:bg-surface1"
} rounded px-2 py-1 text-xs`}
title="Subscript"
>
X<sub class="text-[0.6em]">n</sub>
</button>
<div class="border-surface2 mx-1 border-l"></div>
<button
type="button"