149 lines
5.8 KiB
Plaintext
149 lines
5.8 KiB
Plaintext
---
|
|
import Layout from '../../layouts/Layout.astro';
|
|
import { marked } from 'marked';
|
|
|
|
const { slug } = Astro.params;
|
|
const API_URL = (typeof process !== 'undefined' ? process.env.PUBLIC_API_URL : import.meta.env.PUBLIC_API_URL) || 'http://localhost:3000';
|
|
|
|
interface PostDetail {
|
|
slug: string;
|
|
content: string;
|
|
}
|
|
|
|
let post: PostDetail | null = null;
|
|
let html = '';
|
|
let error = '';
|
|
|
|
// Custom citation pre-processor (Regex-Free!)
|
|
function processCitations(text: string) {
|
|
let result = text;
|
|
|
|
// Keep looking for citation tags until there are none left
|
|
while (result.includes('', startIndex)) {
|
|
|
|
// Safety check: if there's no closing bracket, stop to prevent infinite loops
|
|
if (endIndex === -1) break;
|
|
|
|
// Extract the exact string to replace (e.g., "")
|
|
const fullMatch = result.substring(startIndex, endIndex + 1);
|
|
|
|
// Extract just the numbers/IDs (e.g., "1, 2")
|
|
const citations = result.substring(startIndex + 6, endIndex).trim();
|
|
|
|
// Build the HTML links
|
|
const links = citations.split(',').map((c: string) => {
|
|
const id = c.trim();
|
|
return `<a href="#cite-${id}" class="text-sapphire hover:text-sky transition-colors no-underline">${id}</a>`;
|
|
}).join(', ');
|
|
|
|
// Build the superscript wrapper
|
|
const replacement = `<sup class="bg-surface0 px-1.5 py-0.5 rounded-md border border-surface1 ml-0.5 font-mono text-[10px] md:text-xs shadow-sm align-super">[${links}]</sup>`;
|
|
|
|
// Replace the first occurrence we found, then the loop will handle the rest
|
|
result = result.replace(fullMatch, replacement);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_URL}/api/posts/${slug}`);
|
|
if (response.ok) {
|
|
post = await response.json();
|
|
if (post) {
|
|
// 1. Process citations first
|
|
const processedContent = processCitations(post.content);
|
|
// 2. Then parse the rest of the markdown
|
|
html = await marked.parse(processedContent);
|
|
}
|
|
} else {
|
|
error = 'Post not found';
|
|
}
|
|
} catch (e) {
|
|
const cause = (e as any)?.cause;
|
|
error = `Could not connect to backend at ${API_URL}: ${e instanceof Error ? e.message : String(e)}${cause ? ' (Cause: ' + (cause.message || cause.code || JSON.stringify(cause)) + ')' : ''}`;
|
|
console.error(error);
|
|
}
|
|
|
|
function formatSlug(slug: string) {
|
|
if (!slug) return '';
|
|
return slug
|
|
.split('-')
|
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(' ');
|
|
}
|
|
---
|
|
|
|
<Layout title={post ? formatSlug(post.slug) : 'Post'}>
|
|
<article class="glass p-6 md:p-12 mb-8 md:mb-12 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
|
<header class="mb-8 md:mb-12 border-b border-white/5 pb-8 md:pb-12">
|
|
<a href="/" class="text-blue hover:text-sky transition-colors mb-6 md:mb-8 inline-flex items-center gap-2 group text-sm md:text-base" style="color: var(--blue) !important;">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="md:w-5 md:h-5 transition-transform group-hover:-translate-x-1"><path d="m15 18-6-6 6-6"/></svg>
|
|
Back to list
|
|
</a>
|
|
{post && (
|
|
<div class="flex flex-col md:flex-row md:justify-between md:items-start mt-2 md:mt-4 gap-4">
|
|
<h1 class="text-3xl md:text-5xl font-extrabold text-mauve">
|
|
{formatSlug(post.slug)}
|
|
</h1>
|
|
<a
|
|
href={`/admin/editor?edit=${post.slug}`}
|
|
id="edit-btn"
|
|
class="hidden bg-surface0 hover:bg-surface1 text-blue px-3 py-1.5 md:px-4 md:py-2 rounded border border-surface1 transition-colors items-center gap-2 text-sm md:text-base self-start"
|
|
style="color: var(--blue) !important;"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="md:w-4 md:h-4"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
|
|
Edit
|
|
</a>
|
|
</div>
|
|
)}
|
|
</header>
|
|
|
|
{error && (
|
|
<div class="text-red text-center py-12">
|
|
<h2 class="text-2xl font-bold mb-4">{error}</h2>
|
|
<a href="/" class="text-blue underline">Return home</a>
|
|
</div>
|
|
)}
|
|
|
|
{post && (
|
|
<div class="prose max-w-none" id="post-content" set:html={html} />
|
|
)}
|
|
</article>
|
|
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css" crossorigin="anonymous">
|
|
<script is:inline src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js" crossorigin="anonymous"></script>
|
|
<script is:inline src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js" crossorigin="anonymous"></script>
|
|
|
|
<script is:inline src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
|
|
<script>
|
|
if (localStorage.getItem('admin_token')) {
|
|
const editBtn = document.getElementById('edit-btn');
|
|
if (editBtn) {
|
|
editBtn.style.display = 'inline-flex';
|
|
}
|
|
}
|
|
|
|
// Initialize KaTeX and Highlight.js
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
const content = document.getElementById('post-content');
|
|
if (content) {
|
|
// @ts-ignore
|
|
renderMathInElement(content, {
|
|
delimiters: [
|
|
{left: '$$', right: '$$', display: true},
|
|
{left: '$', right: '$', display: false},
|
|
{left: '\\(', right: '\\)', display: false},
|
|
{left: '\\[', right: '\\]', display: true}
|
|
],
|
|
throwOnError: false
|
|
});
|
|
|
|
// @ts-ignore
|
|
hljs.highlightAll();
|
|
}
|
|
});
|
|
</script>
|
|
</Layout>
|