12 Commits
30 changed files with 1005 additions and 63 deletions
+3
View File
@@ -5,3 +5,6 @@ _site/
*.log *.log
.env .env
docker-compose.yml docker-compose.yml
# Local browser fixtures for document-based development
src/posts/*.md
+14 -3
View File
@@ -3,8 +3,18 @@ import markdownIt from "markdown-it";
import markdownItAnchor from "markdown-it-anchor"; import markdownItAnchor from "markdown-it-anchor";
import { filters } from "./src/_filters/index.js"; import { filters } from "./src/_filters/index.js";
import { postComparator, isPostPublic } from "./src/_utils/sort-posts.js"; import { postComparator, isPostPublic } from "./src/_utils/sort-posts.js";
import { validateLinkedArticles } from "./src/_utils/linked-articles.js";
export default function (eleventyConfig) { export default function (eleventyConfig) {
let buildTimestamp = new Date().toISOString();
eleventyConfig.on("eleventy.before", () => {
buildTimestamp = new Date().toISOString();
});
eleventyConfig.addGlobalData("buildTimestamp", () => buildTimestamp);
// Build ignored local post fixtures without making them Git-tracked.
eleventyConfig.setUseGitIgnore(false);
// 1. Static Asset Passthrough Copy // 1. Static Asset Passthrough Copy
eleventyConfig.addPassthroughCopy({ "src/assets": "assets" }); eleventyConfig.addPassthroughCopy({ "src/assets": "assets" });
@@ -41,8 +51,10 @@ export default function (eleventyConfig) {
// 5. Custom Collections // 5. Custom Collections
// Pinned / featured prioritized, chronological blog posts collection (excludes drafts and hidden posts) // Pinned / featured prioritized, chronological blog posts collection (excludes drafts and hidden posts)
eleventyConfig.addCollection("posts", (collectionApi) => { eleventyConfig.addCollection("posts", (collectionApi) => {
return collectionApi const allPosts = collectionApi.getFilteredByGlob("src/posts/*.md");
.getFilteredByGlob("src/posts/*.md") validateLinkedArticles(allPosts);
return allPosts
.filter((post) => isPostPublic(post)) .filter((post) => isPostPublic(post))
.sort(postComparator); .sort(postComparator);
}); });
@@ -66,7 +78,6 @@ export default function (eleventyConfig) {
return [...tagSet].sort(); return [...tagSet].sort();
}); });
// 6. Dev Server Options (dynamic port binding) // 6. Dev Server Options (dynamic port binding)
eleventyConfig.setServerOptions({ eleventyConfig.setServerOptions({
port: parseInt(process.env.PORT || "8080", 10) port: parseInt(process.env.PORT || "8080", 10)
+2 -2
View File
@@ -425,8 +425,8 @@
<strong>PRIMARY DESTINATIONS:</strong> <strong>PRIMARY DESTINATIONS:</strong>
<span style="color: var(--accent-cyan); margin-left: 8px;">[1] Technical Blog (5 Articles)</span> &bull; <span style="color: var(--accent-cyan); margin-left: 8px;">[1] Technical Blog (5 Articles)</span> &bull;
<span>[2] Forgejo Git Repos</span> &bull; <span>[2] Forgejo Git Repos</span> &bull;
<span>[3] Public Cryptographic &amp; PDF Tools</span> &bull; <!-- <span>[3] Public Cryptographic &amp; PDF Tools</span> &bull; -->
<span>[4] Cluster Telemetry</span> <!-- <span>[4] Cluster Telemetry</span> -->
</div> </div>
</section> </section>
+16 -4
View File
@@ -57,20 +57,32 @@ export function normalizeObsidianMarkdown(content) {
// 2. Convert wikilinks with label: [[target-slug|Custom Label]] -> [Custom Label](/posts/target-slug/) // 2. Convert wikilinks with label: [[target-slug|Custom Label]] -> [Custom Label](/posts/target-slug/)
normalized = normalized.replace(/\[\[([^|\]]+)\|([^\]]+)\]\]/g, (match, target, label) => { normalized = normalized.replace(/\[\[([^|\]]+)\|([^\]]+)\]\]/g, (match, target, label) => {
const slug = slugify(target); return `[${label.trim()}](${wikilinkUrl(target)})`;
return `[${label.trim()}](/posts/${slug}/)`;
}); });
// 3. Convert simple wikilinks: [[target-slug]] -> [target-slug](/posts/target-slug/) // 3. Convert simple wikilinks: [[target-slug]] -> [target-slug](/posts/target-slug/)
normalized = normalized.replace(/\[\[([^\]]+)\]\]/g, (match, target) => { normalized = normalized.replace(/\[\[([^\]]+)\]\]/g, (match, target) => {
const trimmed = target.trim(); const trimmed = target.trim();
const slug = slugify(trimmed); return `[${trimmed}](${wikilinkUrl(trimmed)})`;
return `[${trimmed}](/posts/${slug}/)`;
}); });
return normalized; return normalized;
} }
function wikilinkUrl(target) {
const trimmed = target.trim();
const fragmentIndex = trimmed.indexOf("#");
if (fragmentIndex === -1) {
return `/posts/${slugify(trimmed)}/`;
}
const noteSlug = slugify(trimmed.slice(0, fragmentIndex));
const headerSlug = slugify(trimmed.slice(fragmentIndex + 1));
const fragment = headerSlug ? `#${headerSlug}` : "";
return `/posts/${noteSlug}/${fragment}`;
}
function slugify(text) { function slugify(text) {
return text return text
.toLowerCase() .toLowerCase()
+25
View File
@@ -0,0 +1,25 @@
---
layout: layouts/base.njk
permalink: /404.html
title: Page not found | River Rooks
excerpt: The requested page could not be found.
---
<section class="not-found tile" aria-labelledby="not-found-title">
<header class="tile-header">
<span class="tile-title">// ROUTE STATUS</span>
<span class="tile-meta">HTTP 404 &bull; NOT FOUND</span>
</header>
<div class="not-found-body">
<h1 class="not-found-code" id="not-found-title">404<span class="not-found-cursor" aria-hidden="true">_</span></h1>
<div class="not-found-message">
<p class="not-found-label">REQUEST FAILED / NO MATCHING ROUTE</p>
<h2>Page not found.</h2>
<p>The address you requested doesn't point to a page here. Check the URL, or choose a known route below.</p>
<nav class="not-found-links" aria-label="Recovery links">
<a href="/">&gt; Back to home</a>
<a href="/dispatches/">&gt; Browse dispatches</a>
</nav>
</div>
</div>
</section>
+28 -22
View File
@@ -58,30 +58,30 @@ export default {
url: `https://git.riverrooks.dev/venus`, url: `https://git.riverrooks.dev/venus`,
external: true external: true
}, },
{ // {
label: "RSS Feed", // label: "RSS Feed",
url: "/feed.xml" // url: "/feed.xml"
} // }
], ],
services: [ services: [
{ {
name: "Gitea", name: "Gitea",
url: `https://git.riverrooks.dev/venus`, url: `https://git.riverrooks.dev/venus`,
tag: `git.${siteDomain}:2222`, tag: `git.${siteDomain}:443`,
desc: "Self-hosted Git code repositories." desc: "Self-hosted Git code repositories."
}, },
{ // {
name: "CyberChef", // name: "CyberChef",
url: `https://chef.${siteDomain}`, // url: `https://chef.${siteDomain}`,
tag: `chef.${siteDomain}:443`, // tag: `chef.${siteDomain}:443`,
desc: "Industry tool for analyzing and decoding data." // desc: "Industry tool for analyzing and decoding data."
}, // },
{ // {
name: "Aperisolve", // name: "Aperisolve",
url: `https://aperisolve.${siteDomain}`, // url: `https://aperisolve.${siteDomain}`,
tag: `aperisolve.${siteDomain}:443`, // tag: `aperisolve.${siteDomain}:443`,
desc: "Basic image steganography suite." // desc: "Basic image steganography suite."
}, // },
{ {
name: "Streams", name: "Streams",
url: `https://streams.${siteDomain}`, url: `https://streams.${siteDomain}`,
@@ -90,16 +90,22 @@ export default {
}, },
{ {
name: "Calibre", name: "Calibre",
url: `https://library.${siteDomain}`, url: `https://books.${siteDomain}`,
tag: `books.${siteDomain}:443`, tag: `books.${siteDomain}:443`,
desc: `Calibre web frontend for managing and reading eBooks.` desc: `Calibre web frontend for managing and reading eBooks.`
}, },
{ {
name: "Whoami", name: "Wireguard",
url: `https://whoami.${siteDomain}`, url: `https://wg.${siteDomain}`,
tag: `whoami.${siteDomain}:443`, tag: `wg.${siteDomain}:443`,
desc: `Simple service to display request headers and connection information.` desc: `Self-hosted Wireguard VPN server.(Requires login)`
} }
// {
// name: "Whoami",
// url: `https://whoami.${siteDomain}`,
// tag: `whoami.${siteDomain}:443`,
// desc: `Simple service to display request headers and connection information.`
// }
], ],
topics: [ topics: [
{ {
+43 -1
View File
@@ -1,9 +1,18 @@
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import markdownIt from "markdown-it";
import { resolveLinkedArticles } from "../_utils/linked-articles.js";
/** /**
* Custom Eleventy Filters for River Rooks Blog * Custom Eleventy Filters for River Rooks Blog
*/ */
const excerptMarkdown = markdownIt({ html: false });
export const renderExcerptMarkdown = (excerpt) => {
if (typeof excerpt !== "string") return "";
return excerptMarkdown.renderInline(excerpt);
};
export const postDate = (dateObj) => { export const postDate = (dateObj) => {
if (!dateObj) return ""; if (!dateObj) return "";
const date = typeof dateObj === "string" ? new Date(dateObj) : dateObj; const date = typeof dateObj === "string" ? new Date(dateObj) : dateObj;
@@ -62,13 +71,46 @@ export const extractHeadings = (htmlContent) => {
return headings; return headings;
}; };
const leadingTerminalPreviewHeading = /^\s*<h1\b[^>]*\bid=(["'])terminal-preview\1[^>]*>[\s\S]*?<\/h1>/i;
const terminalPreviewBounds = (htmlContent) => {
if (!htmlContent || typeof htmlContent !== "string") return null;
const heading = htmlContent.match(leadingTerminalPreviewHeading);
if (!heading) return null;
const contentAfterHeading = htmlContent.slice(heading[0].length);
const nextArticleHeading = contentAfterHeading.search(/<h[1-6]\b/i);
if (nextArticleHeading === -1) return null;
const leadingArticleWhitespace = contentAfterHeading
.slice(0, nextArticleHeading)
.match(/\s*$/)?.[0] || "";
return {
preview: contentAfterHeading.slice(0, nextArticleHeading),
article: leadingArticleWhitespace + contentAfterHeading.slice(nextArticleHeading)
};
};
export const extractTerminalPreview = (htmlContent) => terminalPreviewBounds(htmlContent)?.preview || "";
export const removeTerminalPreview = (htmlContent) => {
if (!htmlContent || typeof htmlContent !== "string") return "";
return terminalPreviewBounds(htmlContent)?.article || htmlContent;
};
export const filters = { export const filters = {
renderExcerptMarkdown,
postDate, postDate,
isoDate, isoDate,
limit, limit,
filterTagList, filterTagList,
readingTime, readingTime,
extractHeadings extractHeadings,
extractTerminalPreview,
removeTerminalPreview,
resolveLinkedArticles
}; };
export default filters; export default filters;
+1
View File
@@ -29,6 +29,7 @@
{% include "partials/footer.njk" %} {% include "partials/footer.njk" %}
</div> </div>
<script type="module" src="/assets/js/build-timer.js"></script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
</body> </body>
</html> </html>
+15 -4
View File
@@ -11,7 +11,7 @@
<!-- Left Sticky Sidebar: Document Outline & Telemetry --> <!-- Left Sticky Sidebar: Document Outline & Telemetry -->
<aside class="article-sidebar" aria-label="Article Sidebar"> <aside class="article-sidebar" aria-label="Article Sidebar">
<!-- Dynamic Table of Contents --> <!-- Dynamic Table of Contents -->
{% set headings = content | extractHeadings %} {% set headings = (content | removeTerminalPreview) | extractHeadings %}
{% if headings.length > 0 %} {% if headings.length > 0 %}
<div class="sidebar-tile"> <div class="sidebar-tile">
<div class="sidebar-heading"> <div class="sidebar-heading">
@@ -31,6 +31,17 @@
<!-- Article Telemetry Tile --> <!-- Article Telemetry Tile -->
<div class="sidebar-tile"> <div class="sidebar-tile">
<div class="sidebar-heading">DISPATCH TELEMETRY</div> <div class="sidebar-heading">DISPATCH TELEMETRY</div>
{% set linkedPosts = collections.posts | resolveLinkedArticles(linkedArticles) %}
{% if linkedPosts.length > 0 %}
<section aria-labelledby="other-articles-heading">
<div id="other-articles-heading" class="sidebar-heading">OTHER ARTICLES</div>
<nav class="toc-nav" aria-label="Other Articles">
{% for linkedPost in linkedPosts %}
<a href="{{ linkedPost.url }}" class="toc-link">{{ linkedPost.data.title }}</a>
{% endfor %}
</nav>
</section>
{% endif %}
<div class="sidebar-stats-box" style="padding: 0; background: transparent; border: none;"> <div class="sidebar-stats-box" style="padding: 0; background: transparent; border: none;">
{% if hidden and hidden != "false" and hidden != "0" and hidden != "no" %} {% if hidden and hidden != "false" and hidden != "0" and hidden != "no" %}
<div class="stat-row"> <div class="stat-row">
@@ -45,7 +56,7 @@
<div class="stat-row"> <div class="stat-row">
<span>DURATION:</span> <span>DURATION:</span>
<span class="stat-val">{{ content | readingTime(readingTime) }}</span> <span class="stat-val">{{ (content | removeTerminalPreview) | readingTime(readingTime) }}</span>
</div> </div>
{% if difficulty %} {% if difficulty %}
<div class="stat-row"> <div class="stat-row">
@@ -90,7 +101,7 @@
<time datetime="{{ date | isoDate }}">{{ date | postDate }}</time> <time datetime="{{ date | isoDate }}">{{ date | postDate }}</time>
</span> </span>
<span>&bull;</span> <span>&bull;</span>
<span class="meta-item">{{ content | readingTime(readingTime) }}</span> <span class="meta-item">{{ (content | removeTerminalPreview) | readingTime(readingTime) }}</span>
{% if difficulty %} {% if difficulty %}
<span>&bull;</span> <span>&bull;</span>
<span class="meta-item" style="color: var(--accent-amber);">DIFFICULTY: {{ difficulty }}</span> <span class="meta-item" style="color: var(--accent-amber);">DIFFICULTY: {{ difficulty }}</span>
@@ -99,7 +110,7 @@
</header> </header>
<div class="markdown-prose"> <div class="markdown-prose">
{{ content | safe }} {{ content | removeTerminalPreview | safe }}
</div> </div>
</article> </article>
</div> </div>
+18 -16
View File
@@ -29,24 +29,26 @@
</h2> </h2>
<p class="featured-prose"> <p class="featured-prose">
{{ featured.data.excerpt }} {{ featured.data.excerpt | renderExcerptMarkdown | safe }}
</p> </p>
<!-- Terminal Code Preview Snippet --> {% if featured.data.terminal == true %}
<div class="terminal-code-box"> {% set terminalPreview = featured.templateContent | extractTerminalPreview %}
<div class="code-titlebar"> {% if terminalPreview %}
<span class="code-title"> <div class="terminal-code-box terminal-preview-box">
<span style="color: var(--accent-cyan);">&gt;</span> <div class="code-titlebar">
<span>init_cluster.sh</span> <span class="code-title">
</span> <span style="color: var(--accent-cyan);">&gt;</span>
<span>POSIX sh</span> <span>TERMINAL PREVIEW</span>
</div> </span>
<div class="code-content"> <span>MARKDOWN</span>
<pre><code class="language-bash"># System initialization and node health check </div>
./cluster.sh --verify-nodes <div class="terminal-preview-content">
curl -s https://api.local/healthz | jq .status</code></pre> {{ terminalPreview | safe }}
</div> </div>
</div> </div>
{% endif %}
{% endif %}
<div class="featured-cta-row"> <div class="featured-cta-row">
<div class="tag-cluster"> <div class="tag-cluster">
+3 -1
View File
@@ -4,10 +4,12 @@
<span>RUNTIME: ELEVENTY 3.x STATIC</span> <span>RUNTIME: ELEVENTY 3.x STATIC</span>
<span>&bull;</span> <span>&bull;</span>
<span>THEME: NATIVE TERMINAL DARK</span> <span>THEME: NATIVE TERMINAL DARK</span>
<span>&bull;</span>
<time id="build-age" data-build-timestamp="{{ buildTimestamp }}" aria-live="polite">REBUILT: INITIALIZING</time>
</div> </div>
<div class="footer-right"> <div class="footer-right">
<span>&copy; {{ currentYear or "2026" }} {{ site.author }}</span> <span>&copy; {{ currentYear or "2026" }} {{ site.author }}</span>
<a href="https://git.{{ site.domain }}" target="_blank" rel="noopener noreferrer" class="footer-link">Source Forge ↗</a> <a href="https://git.{{ site.domain }}" target="_blank" rel="noopener noreferrer" class="footer-link">Source Code ↗</a>
</div> </div>
</footer> </footer>
+1 -1
View File
@@ -11,7 +11,7 @@
</h3> </h3>
{% if post.data.excerpt %} {% if post.data.excerpt %}
<p class="feed-excerpt">{{ post.data.excerpt }}</p> <p class="feed-excerpt">{{ post.data.excerpt | renderExcerptMarkdown | safe }}</p>
{% endif %} {% endif %}
<div class="feed-tags-row"> <div class="feed-tags-row">
+48
View File
@@ -0,0 +1,48 @@
export const validateLinkedArticles = (posts) => {
const titles = new Set(posts.map((post) => post.data.title));
for (const post of posts) {
const { linkedArticles } = post.data;
if (linkedArticles === undefined) continue;
if (!Array.isArray(linkedArticles)) {
throw new Error(
`Post '${post.inputPath}' must define 'linkedArticles' as a YAML list of article titles`
);
}
const linkedTitleSet = new Set();
for (const title of linkedArticles) {
if (typeof title !== "string" || title.trim() === "") {
throw new Error(
`Post '${post.inputPath}' must define 'linkedArticles' with non-empty article titles`
);
}
if (title === post.data.title) {
throw new Error(`Post '${post.inputPath}' cannot link to itself`);
}
if (linkedTitleSet.has(title)) {
throw new Error(
`Post '${post.inputPath}' cannot contain duplicate article titles in 'linkedArticles'`
);
}
linkedTitleSet.add(title);
if (!titles.has(title)) {
throw new Error(
`Post '${post.inputPath}' links to unknown article '${title}'`
);
}
}
}
};
export const resolveLinkedArticles = (posts, titles) => {
if (!Array.isArray(posts) || !Array.isArray(titles)) return [];
const postsByTitle = new Map(posts.map((post) => [post.data.title, post]));
return titles.map((title) => postsByTitle.get(title)).filter(Boolean);
};
+29
View File
@@ -65,6 +65,35 @@
font-family: var(--font-code); font-family: var(--font-code);
} }
.terminal-preview-box {
margin: 0;
}
.terminal-preview-content {
padding: 14px 16px;
color: var(--text-primary);
font-size: 0.82rem;
line-height: 1.55;
}
.terminal-preview-content > :first-child {
margin-top: 0;
}
.terminal-preview-content > :last-child {
margin-bottom: 0;
}
.terminal-preview-content pre {
margin: 10px 0;
padding: 10px 12px;
overflow-x: auto;
background-color: var(--bg-surface);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
font-family: var(--font-code);
}
/* Syntax Highlighting Tokens (Prism / 11ty Syntax Highlight) */ /* Syntax Highlighting Tokens (Prism / 11ty Syntax Highlight) */
.token.comment, .token.comment,
.token.prolog, .token.prolog,
+86
View File
@@ -0,0 +1,86 @@
/* Missing route: a compact diagnostic panel in the site's tiling vocabulary. */
.not-found {
gap: 0;
padding: 20px 24px 28px;
}
.not-found-body {
display: grid;
grid-template-columns: minmax(0, 0.8fr) minmax(0, 1fr);
align-items: center;
gap: 24px;
min-height: 320px;
}
.not-found-code {
font-family: var(--font-mono);
font-size: clamp(5rem, 14vw, 10rem);
line-height: 1;
letter-spacing: -0.09em;
color: var(--accent-amber);
white-space: nowrap;
}
.not-found-cursor {
color: var(--text-muted);
}
.not-found-message {
border-left: 1px solid var(--border-color);
padding: 16px 0 16px 24px;
max-width: 48ch;
}
.not-found-label {
font: 500 0.75rem var(--font-mono);
letter-spacing: 0.06em;
color: var(--accent-amber);
margin-bottom: 12px;
}
.not-found-message h2 {
font: 700 clamp(1.3rem, 3vw, 2rem) / 1.25 var(--font-mono);
margin-bottom: 12px;
}
.not-found-message > p:not(.not-found-label) {
color: var(--text-muted);
max-width: 42ch;
}
.not-found-links {
display: flex;
flex-wrap: wrap;
gap: 8px 24px;
margin-top: 24px;
font: 500 0.85rem var(--font-mono);
}
.not-found-links a {
color: var(--accent-cyan);
text-decoration: none;
}
.not-found-links a:hover {
color: var(--bg-canvas);
background: var(--accent-cyan);
}
@media (max-width: 680px) {
.not-found {
padding: 16px;
}
.not-found-body {
grid-template-columns: 1fr;
gap: 8px;
min-height: 0;
padding: 32px 0 8px;
}
.not-found-message {
border-left: 0;
border-top: 1px solid var(--border-color);
padding: 20px 0 0;
}
}
+1
View File
@@ -19,3 +19,4 @@
@import "./components/code-box.css"; @import "./components/code-box.css";
@import "./components/article.css"; @import "./components/article.css";
@import "./components/footer.css"; @import "./components/footer.css";
@import "./components/not-found.css";
+30
View File
@@ -0,0 +1,30 @@
export const formatBuildAge = (buildTimestamp, nowTimestamp = new Date().toISOString()) => {
const buildTime = Date.parse(buildTimestamp);
const now = Date.parse(nowTimestamp);
if (!Number.isFinite(buildTime) || !Number.isFinite(now)) return "unknown";
const elapsedSeconds = Math.max(0, Math.floor((now - buildTime) / 1000));
const days = Math.floor(elapsedSeconds / 86400);
const hours = Math.floor((elapsedSeconds % 86400) / 3600);
const minutes = Math.floor((elapsedSeconds % 3600) / 60);
const seconds = elapsedSeconds % 60;
if (days > 0) return `${days}d ${hours}h ago`;
if (hours > 0) return `${hours}h ${minutes}m ago`;
if (minutes > 0) return `${minutes}m ${seconds}s ago`;
return `${seconds}s ago`;
};
if (typeof document !== "undefined") {
const timer = document.getElementById("build-age");
if (timer) {
const updateBuildAge = () => {
timer.textContent = `REBUILT: ${formatBuildAge(timer.dataset.buildTimestamp)}`;
};
updateBuildAge();
window.setInterval(updateBuildAge, 1000);
}
}
+3 -3
View File
@@ -10,7 +10,7 @@ title: "River Rooks | Homelab Gateway"
<div class="hero-landing-text"> <div class="hero-landing-text">
<p> <p>
I am River Rooks, a cybersecurity student and the University of Alabama. This website is the public-facing gateway to my homelab, which hosts a variety of services and tools for personal and professional use. Here, you can find my technical blog and a collection of public tools that host. I am River Rooks, a cyber security student at the University of Alabama. This website is the public-facing gateway to my homelab, which hosts a variety of services and tools for personal and professional use. Here, you can find my technical blog and a collection of public tools that host.
</p> </p>
<p> <p>
I deploy and maintain a variety of services in my homelab for research, development, and learning purposes. This includes web applications, databases, and other tools that I use to enhance my skills in cybersecurity and DevOps. I deploy and maintain a variety of services in my homelab for research, development, and learning purposes. This includes web applications, databases, and other tools that I use to enhance my skills in cybersecurity and DevOps.
@@ -23,8 +23,8 @@ title: "River Rooks | Homelab Gateway"
<strong>PRIMARY DESTINATIONS:</strong> <strong>PRIMARY DESTINATIONS:</strong>
<a href="/dispatches/" style="color: var(--accent-cyan); text-decoration: none; margin-left: 8px;">[1] Technical Blog ({{ collections.posts.length }} Articles)</a> &bull; <a href="/dispatches/" style="color: var(--accent-cyan); text-decoration: none; margin-left: 8px;">[1] Technical Blog ({{ collections.posts.length }} Articles)</a> &bull;
<a href="https://git.{{ site.domain }}" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: none;">[2] Gitea Repos</a> &bull; <a href="https://git.{{ site.domain }}" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: none;">[2] Gitea Repos</a> &bull;
<span>[3] Public Tools</span> &bull; <a href="https://github.com/skoorrevir" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: none;">[3] GitHub Repos</a> &bull;
<span>[4] Cluster Telemetry</span> <a href="https://wg.{{ site.domain }}" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: none;">[4] Wireguard VPN</a> &bull;
</div> </div>
</section> </section>
+62
View File
@@ -0,0 +1,62 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import configureEleventy from "../eleventy.config.js";
import { formatBuildAge } from "../src/assets/js/build-timer.js";
test("Build age: formats elapsed time as a compact live footer value", () => {
assert.equal(
formatBuildAge("2026-10-20T12:00:00.000Z", "2026-10-20T12:00:00.000Z"),
"0s ago"
);
assert.equal(
formatBuildAge("2026-10-20T12:00:00.000Z", "2026-10-20T12:01:01.000Z"),
"1m 1s ago"
);
assert.equal(
formatBuildAge("2026-10-20T12:00:00.000Z", "2026-10-21T14:03:04.000Z"),
"1d 2h ago"
);
assert.equal(
formatBuildAge("2026-10-20T12:00:00.000Z", "2026-10-20T11:59:59.000Z"),
"0s ago"
);
});
test("Build age: Eleventy exposes one ISO timestamp for each rebuild", () => {
const globalData = new Map();
const events = new Map();
configureEleventy({
setUseGitIgnore() {},
addPassthroughCopy() {},
addPlugin() {},
setLibrary() {},
addFilter() {},
addCollection() {},
addGlobalData(name, value) {
globalData.set(name, value);
},
on(name, callback) {
events.set(name, callback);
},
setServerOptions() {}
});
events.get("eleventy.before")();
assert.match(
globalData.get("buildTimestamp")(),
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
);
});
test("Build age: footer provides an accessible live timer backed by the build timestamp", () => {
const footer = fs.readFileSync(path.resolve("src/_includes/partials/footer.njk"), "utf-8");
const baseLayout = fs.readFileSync(path.resolve("src/_includes/layouts/base.njk"), "utf-8");
assert.match(
footer,
/<time id="build-age" data-build-timestamp="{{ buildTimestamp }}" aria-live="polite">REBUILT: INITIALIZING<\/time>/
);
assert.match(baseLayout, /<script type="module" src="\/assets\/js\/build-timer\.js"><\/script>/);
});
+64
View File
@@ -0,0 +1,64 @@
import test from "node:test";
import assert from "node:assert/strict";
import nunjucks from "nunjucks";
import path from "node:path";
import { filters } from "../src/_filters/index.js";
const includesDir = path.resolve("src/_includes");
const markdownExcerpt = "Read the [deployment guide](/guides/deploy/) before continuing.";
function renderTemplate(template, context) {
const env = new nunjucks.Environment(new nunjucks.FileSystemLoader(includesDir), { autoescape: false });
for (const [name, filter] of Object.entries(filters)) {
env.addFilter(name, filter);
}
return env.render(template, context);
}
test("Excerpt Markdown: renders Markdown links as safe inline HTML", () => {
assert.equal(
filters.renderExcerptMarkdown(markdownExcerpt),
'Read the <a href="/guides/deploy/">deployment guide</a> before continuing.'
);
});
test("Excerpt Markdown: renders links in post-card previews", () => {
const output = renderTemplate("partials/post-card.njk", {
post: {
fileSlug: "example-dispatch",
url: "/dispatches/example/",
date: "2026-10-06",
data: {
title: "Example dispatch",
excerpt: markdownExcerpt,
tags: ["systems"]
},
templateContent: "A short article."
}
});
assert.match(output, /<p class="feed-excerpt">\s*Read the <a href="\/guides\/deploy\/">deployment guide<\/a> before continuing\.\s*<\/p>/);
});
test("Excerpt Markdown: renders links in featured previews and escapes raw HTML", () => {
const output = renderTemplate("partials/featured-card.njk", {
collections: {
posts: [{
url: "/dispatches/example/",
date: "2026-10-06",
data: {
featured: true,
tags: ["systems"],
title: "Example dispatch",
excerpt: `${markdownExcerpt} <script>alert('unsafe')</script>`
},
templateContent: ""
}]
}
});
assert.match(output, /<a href="\/guides\/deploy\/">deployment guide<\/a>/);
assert.match(output, /&lt;script&gt;alert\('unsafe'\)&lt;\/script&gt;/);
assert.doesNotMatch(output, /<script>/);
});
+62
View File
@@ -0,0 +1,62 @@
import test from "node:test";
import assert from "node:assert/strict";
import nunjucks from "nunjucks";
import path from "node:path";
import {
extractTerminalPreview,
filterTagList,
isoDate,
postDate,
readingTime,
renderExcerptMarkdown
} from "../src/_filters/index.js";
const includesDir = path.resolve("src/_includes");
function renderFeaturedCard(terminal) {
const env = new nunjucks.Environment(new nunjucks.FileSystemLoader(includesDir), { autoescape: false });
env.addFilter("extractTerminalPreview", extractTerminalPreview);
env.addFilter("filterTagList", filterTagList);
env.addFilter("isoDate", isoDate);
env.addFilter("postDate", postDate);
env.addFilter("readingTime", readingTime);
env.addFilter("renderExcerptMarkdown", renderExcerptMarkdown);
const featured = {
url: "/dispatches/example/",
date: "2026-10-06",
data: {
featured: true,
terminal,
tags: ["systems"],
title: "Example dispatch",
excerpt: "An example featured dispatch used to verify the terminal preview."
},
templateContent: `
<h1 id="terminal-preview">Terminal preview</h1>
<p>Inspect the first five lines:</p>
<pre><code class="language-bash">cat /dispatches/example | head -n 5</code></pre>
<h1 id="investigation">Investigation</h1>
<p>The public article begins here.</p>
`
};
return env.render("partials/featured-card.njk", {
collections: { posts: [featured] }
});
}
test("Featured terminal preview: renders Markdown output only when terminal frontmatter is true", () => {
const output = renderFeaturedCard(true);
assert.match(output, /class="[^"]*terminal-code-box[^"]*"/);
assert.match(output, /Inspect the first five lines:/);
assert.match(output, /cat \/dispatches\/example \| head -n 5/);
assert.doesNotMatch(output, /Terminal preview/);
assert.doesNotMatch(output, /init_cluster\.sh/);
});
test("Featured terminal preview: does not render when terminal frontmatter is false or absent", () => {
assert.doesNotMatch(renderFeaturedCard(false), /terminal-code-box/);
assert.doesNotMatch(renderFeaturedCard(undefined), /terminal-code-box/);
});
+18 -1
View File
@@ -3,10 +3,27 @@ import assert from "node:assert/strict";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import http from "node:http"; import http from "node:http";
import { createServer, triggerEleventyBuild } from "../server.js"; import { createServer, normalizeObsidianMarkdown, triggerEleventyBuild } from "../server.js";
const TEST_SECRET = "super-secret-test-token"; const TEST_SECRET = "super-secret-test-token";
test("Obsidian normalization: preserves linked note header fragments", () => {
const normalized = normalizeObsidianMarkdown(
"See [[a note#header one]] and [[another note#Second Header|the other section]]."
);
assert.match(
normalized,
/\[a note#header one\]\(\/posts\/a-note\/#header-one\)/,
"A wikilink header must become a URL fragment on the linked note"
);
assert.match(
normalized,
/\[the other section\]\(\/posts\/another-note\/#second-header\)/,
"Aliased wikilinks must retain their linked-note header fragment"
);
});
test("Ingestion API: rejects requests without proper Bearer token", async () => { test("Ingestion API: rejects requests without proper Bearer token", async () => {
const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-")); const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-"));
const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-")); const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-"));
+155
View File
@@ -0,0 +1,155 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { resolveLinkedArticles, validateLinkedArticles } from "../src/_utils/linked-articles.js";
import { filters } from "../src/_filters/index.js";
import configureEleventy from "../eleventy.config.js";
test("Linked articles: accepts an article's list when every name identifies another post", () => {
assert.doesNotThrow(() => {
validateLinkedArticles([
{
inputPath: "src/posts/2026-10-06-terminal-preview-demo.md",
data: {
title: "Terminal Preview Demo",
linkedArticles: ["Markdown Preview Demo"]
}
},
{
inputPath: "src/posts/2026-10-05-markdown-preview-demo.md",
data: { title: "Markdown Preview Demo" }
}
]);
});
});
test("Linked articles: rejects a non-list frontmatter value", () => {
assert.throws(
() => {
validateLinkedArticles([
{
inputPath: "src/posts/2026-10-06-terminal-preview-demo.md",
data: {
title: "Terminal Preview Demo",
linkedArticles: "Markdown Preview Demo"
}
},
{
inputPath: "src/posts/2026-10-05-markdown-preview-demo.md",
data: { title: "Markdown Preview Demo" }
}
]);
},
/must define 'linkedArticles' as a YAML list/
);
});
test("Linked articles: rejects empty and unknown article names", () => {
const posts = [
{
inputPath: "src/posts/2026-10-06-terminal-preview-demo.md",
data: { title: "Terminal Preview Demo", linkedArticles: [""] }
},
{
inputPath: "src/posts/2026-10-05-markdown-preview-demo.md",
data: { title: "Markdown Preview Demo" }
}
];
assert.throws(() => validateLinkedArticles(posts), /non-empty article titles/);
posts[0].data.linkedArticles = ["Missing Dispatch"];
assert.throws(() => validateLinkedArticles(posts), /links to unknown article 'Missing Dispatch'/);
});
test("Linked articles: rejects self-references and duplicate navigation targets", () => {
const posts = [
{
inputPath: "src/posts/2026-10-06-terminal-preview-demo.md",
data: {
title: "Terminal Preview Demo",
linkedArticles: ["Terminal Preview Demo"]
}
},
{
inputPath: "src/posts/2026-10-05-markdown-preview-demo.md",
data: { title: "Markdown Preview Demo" }
}
];
assert.throws(() => validateLinkedArticles(posts), /cannot link to itself/);
posts[0].data.linkedArticles = ["Markdown Preview Demo", "Markdown Preview Demo"];
assert.throws(() => validateLinkedArticles(posts), /cannot contain duplicate article titles/);
});
test("Linked articles: the Eleventy posts collection rejects invalid frontmatter", () => {
const collections = new Map();
configureEleventy({
setUseGitIgnore() {},
addPassthroughCopy() {},
addPlugin() {},
setLibrary() {},
addFilter() {},
addCollection(name, callback) {
collections.set(name, callback);
},
addGlobalData() {},
on() {},
setServerOptions() {}
});
assert.throws(
() => {
collections.get("posts")({
getFilteredByGlob() {
return [
{
inputPath: "src/posts/2026-10-06-terminal-preview-demo.md",
data: {
title: "Terminal Preview Demo",
linkedArticles: ["Missing Dispatch"]
}
}
];
}
});
},
/links to unknown article 'Missing Dispatch'/
);
});
test("Linked articles: resolves named posts in the frontmatter order for navigation", () => {
const posts = [
{
url: "/dispatches/terminal-preview-demo/",
data: { title: "Terminal Preview Demo" }
},
{
url: "/dispatches/markdown-preview-demo/",
data: { title: "Markdown Preview Demo" }
}
];
assert.deepEqual(
resolveLinkedArticles(posts, ["Markdown Preview Demo", "Terminal Preview Demo"]),
[posts[1], posts[0]]
);
});
test("Linked articles: exposes navigation resolution to Nunjucks", () => {
assert.strictEqual(filters.resolveLinkedArticles, resolveLinkedArticles);
});
test("Linked articles: post layout renders Other Articles at the top of dispatch telemetry", () => {
const postLayout = fs.readFileSync(
path.resolve("src/_includes/layouts/post.njk"),
"utf-8"
);
assert.match(
postLayout,
/{% set linkedPosts = collections\.posts \| resolveLinkedArticles\(linkedArticles\) %}[\s\S]*?<div[^>]+class="sidebar-heading">OTHER ARTICLES<\/div>[\s\S]*?<nav[^>]+aria-label="Other Articles">[\s\S]*?href="{{ linkedPost\.url }}"/
);
});
+10
View File
@@ -0,0 +1,10 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
const config = fs.readFileSync(path.resolve("eleventy.config.js"), "utf-8");
test("Eleventy config: builds local ignored post fixtures", () => {
assert.match(config, /setUseGitIgnore\(false\)/);
});
+43
View File
@@ -0,0 +1,43 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { createServer } from "../server.js";
const siteDir = path.resolve("_site");
test("unknown routes render the themed 404 page with a real 404 status", async () => {
execFileSync("npm", ["run", "build"], {
stdio: "pipe",
env: { ...process.env, SITE_DOMAIN: process.env.SITE_DOMAIN || "riverrooks.dev" }
});
const html = fs.readFileSync(path.join(siteDir, "404.html"), "utf8");
assert.match(html, /<title>Page not found/);
assert.match(html, /<h1[^>]*>[^<]*404/);
assert.match(html, /href="\/"/);
assert.match(html, /href="\/dispatches\/"/);
assert.match(html, /href="\/assets\/css\/main\.css/);
const css = fs.readFileSync(path.join(siteDir, "assets/css/main.css"), "utf8");
assert.match(css, /components\/not-found\.css/);
const pageCss = fs.readFileSync(path.join(siteDir, "assets/css/components/not-found.css"), "utf8");
assert.doesNotMatch(pageCss, /(?:backdrop-filter|box-shadow|text-shadow|linear-gradient|radial-gradient)\s*:/);
const server = createServer(siteDir, path.resolve("src/posts"), path.resolve("src/assets/images/posts"));
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const origin = `http://127.0.0.1:${server.address().port}`;
for (const route of ["/missing-page/", "/assets/missing.css"]) {
const response = await fetch(`${origin}${route}`);
assert.equal(response.status, 404);
assert.match(response.headers.get("content-type"), /text\/html/);
assert.match(await response.text(), /Page not found/);
}
const home = await fetch(origin);
assert.equal(home.status, 200);
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
+162
View File
@@ -0,0 +1,162 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
const pluginPath = path.resolve("/home/venus/Documents/Notes/.obsidian/plugins/blog-publisher/main.js");
// Pure helper function to be tested and exported/used by plugin
export function insertOrUpdateFrontmatterProperty(content, key, value) {
if (typeof content !== "string") return "";
const fmRegex = /^---\r?\n([\s\S]*?)\r?\n---/;
const match = content.match(fmRegex);
if (match) {
let fmBlock = match[1];
const keyRegex = new RegExp(`^${key}:.*$`, "m");
if (value === null || value === undefined) {
// Remove property if value is null
fmBlock = fmBlock.replace(new RegExp(`^${key}:.*\\r?\\n?`, "m"), "");
} else if (keyRegex.test(fmBlock)) {
// Replace existing property
fmBlock = fmBlock.replace(keyRegex, `${key}: "${value}"`);
} else {
// Append property to frontmatter
fmBlock = `${fmBlock}\n${key}: "${value}"`;
}
return content.replace(fmRegex, `---\n${fmBlock.trim()}\n---`);
} else if (value) {
return `---\n${key}: "${value}"\n---\n\n${content}`;
}
return content;
}
export function computePublicUrl(serverUrl, postPath) {
const base = (serverUrl || "").replace(/\/+$/, "");
const p = (postPath || "").startsWith("/") ? postPath : `/${postPath || ""}`;
return `${base}${p}`;
}
test("Obsidian Plugin: computePublicUrl normalizes slashes correctly", () => {
assert.equal(
computePublicUrl("https://riverrooks.dev", "/posts/test-note/"),
"https://riverrooks.dev/posts/test-note/"
);
assert.equal(
computePublicUrl("https://riverrooks.dev/", "/posts/test-note/"),
"https://riverrooks.dev/posts/test-note/"
);
assert.equal(
computePublicUrl("https://riverrooks.dev///", "posts/test-note/"),
"https://riverrooks.dev/posts/test-note/"
);
assert.equal(
computePublicUrl("http://localhost:8080", "/posts/test-note/"),
"http://localhost:8080/posts/test-note/"
);
});
test("Obsidian Plugin: insertOrUpdateFrontmatterProperty adds public-url when missing", () => {
const original = `---
title: "Sample Note"
date: 2026-09-23
publish: true
---
# Content here`;
const updated = insertOrUpdateFrontmatterProperty(
original,
"public-url",
"https://riverrooks.dev/posts/sample-note/"
);
assert.match(
updated,
/public-url:\s*"https:\/\/riverrooks\.dev\/posts\/sample-note\/"/,
"Must insert public-url into frontmatter"
);
assert.match(updated, /title:\s*"Sample Note"/, "Preserves title");
assert.match(updated, /publish:\s*true/, "Preserves publish flag");
assert.match(updated, /# Content here/, "Preserves body content");
});
test("Obsidian Plugin: insertOrUpdateFrontmatterProperty updates existing public-url", () => {
const original = `---
title: "Updated Note"
date: 2026-09-23
public-url: "https://old.domain/posts/sample-note/"
---
Body`;
const updated = insertOrUpdateFrontmatterProperty(
original,
"public-url",
"https://riverrooks.dev/posts/updated-note/"
);
assert.match(
updated,
/public-url:\s*"https:\/\/riverrooks\.dev\/posts\/updated-note\/"/,
"Must update public-url to new URL"
);
assert.doesNotMatch(
updated,
/old\.domain/,
"Old URL must be replaced"
);
});
test("Obsidian Plugin: insertOrUpdateFrontmatterProperty removes public-url on unpublish", () => {
const original = `---
title: "Unpublished Note"
date: 2026-09-23
public-url: "https://riverrooks.dev/posts/sample-note/"
---
Body`;
const updated = insertOrUpdateFrontmatterProperty(original, "public-url", null);
assert.doesNotMatch(updated, /public-url:/, "Must remove public-url when value is null");
assert.match(updated, /title:\s*"Unpublished Note"/, "Preserves other frontmatter");
});
test("Obsidian Plugin: Adversarial edge cases for frontmatter property insertion", () => {
// 1. Empty content
assert.equal(insertOrUpdateFrontmatterProperty("", "public-url", "https://url"), "---\npublic-url: \"https://url\"\n---\n\n");
// 2. Non-string inputs
assert.equal(insertOrUpdateFrontmatterProperty(null, "public-url", "https://url"), "");
assert.equal(insertOrUpdateFrontmatterProperty(undefined, "public-url", "https://url"), "");
// 3. Note without frontmatter
const plain = "# Just a header\nSome text.";
const result = insertOrUpdateFrontmatterProperty(plain, "public-url", "https://url");
assert.match(result, /^---\npublic-url:\s*"https:\/\/url"\n---/);
});
test("Obsidian Plugin: main.js in vault includes public-url insertion and recursion guard", () => {
assert.ok(fs.existsSync(pluginPath), `Plugin main.js must exist at ${pluginPath}`);
const pluginContent = fs.readFileSync(pluginPath, "utf-8");
// 1. Must handle public-url
assert.match(pluginContent, /public-url/i, "main.js must reference 'public-url'");
// 2. Must guard against infinite loops during frontmatter updates
assert.match(
pluginContent,
/isUpdatingFrontmatter/i,
"main.js must maintain a guard to prevent metadataCache recursion loops"
);
// 3. Must use processFrontMatter or frontmatter updater
assert.match(
pluginContent,
/processFrontMatter|updateFrontmatter/i,
"main.js must implement frontmatter updating logic"
);
});
+10
View File
@@ -0,0 +1,10 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
const postLayout = fs.readFileSync(path.resolve("src/_includes/layouts/post.njk"), "utf-8");
test("Post layout: omits the leading terminal preview from the public article body", () => {
assert.match(postLayout, /content\s*\|\s*removeTerminalPreview\s*\|\s*safe/);
});
+1 -5
View File
@@ -41,11 +41,7 @@ test("Site Config: correctly parses SITE_DOMAIN and derives service subdomains",
const gitService = site.services.find((s) => s.name === "Gitea"); const gitService = site.services.find((s) => s.name === "Gitea");
assert.ok(gitService, "Gitea service exists"); assert.ok(gitService, "Gitea service exists");
assert.equal(gitService.url, "https://git.riverrooks.dev/venus"); assert.equal(gitService.url, "https://git.riverrooks.dev/venus");
assert.equal(gitService.tag, "git.testcluster.internal:2222"); assert.equal(gitService.tag, "git.testcluster.internal:443");
const chefService = site.services.find((s) => s.name === "CyberChef");
assert.ok(chefService, "CyberChef service exists");
assert.equal(chefService.url, "https://chef.testcluster.internal");
// Verify navigation links // Verify navigation links
const navHome = site.nav.find((n) => n.label === "Home"); const navHome = site.nav.find((n) => n.label === "Home");
+15
View File
@@ -0,0 +1,15 @@
import test from "node:test";
import assert from "node:assert/strict";
import { extractTerminalPreview, removeTerminalPreview } from "../src/_filters/index.js";
test("Terminal previews: stop before the first subsequent Markdown heading at any level", () => {
const renderedPost = `
<h1 id="terminal-preview">Terminal preview</h1>
<p>Plain Markdown preview text is supported.</p>
<h2 id="findings">Findings</h2>
<p>The article begins here.</p>
`;
assert.match(extractTerminalPreview(renderedPost), /Plain Markdown preview text/);
assert.match(removeTerminalPreview(renderedPost), /^\n <h2 id="findings">/);
});
+37
View File
@@ -0,0 +1,37 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
extractTerminalPreview,
removeTerminalPreview
} from "../src/_filters/index.js";
test("Terminal previews: extracts rendered Markdown between the leading preview and article headings", () => {
const renderedPost = `
<h1 id="terminal-preview">Terminal preview</h1>
<p>Use the following command:</p>
<pre class="language-bash"><code class="language-bash">cat /dispatches/example | head -n 5</code></pre>
<h1 id="investigation">Investigation</h1>
<p>The public article starts here.</p>
`;
assert.equal(
extractTerminalPreview(renderedPost),
`\n <p>Use the following command:</p>\n <pre class="language-bash"><code class="language-bash">cat /dispatches/example | head -n 5</code></pre>\n `
);
assert.equal(
removeTerminalPreview(renderedPost),
`\n <h1 id="investigation">Investigation</h1>\n <p>The public article starts here.</p>\n `
);
});
test("Terminal previews: ignores missing, misplaced, and malformed preview headings", () => {
const regularPost = "<h1 id=\"article\">Article</h1><p>Visible text.</p>";
const misplacedPreview = "<h1 id=\"article\">Article</h1><h1 id=\"terminal-preview\">Terminal preview</h1><p>Do not extract.</p>";
assert.equal(extractTerminalPreview(regularPost), "");
assert.equal(removeTerminalPreview(regularPost), regularPost);
assert.equal(extractTerminalPreview(misplacedPreview), "");
assert.equal(removeTerminalPreview(misplacedPreview), misplacedPreview);
assert.equal(extractTerminalPreview(null), "");
assert.equal(removeTerminalPreview(null), "");
});