feat: dynamic ish terminal preview
This commit is contained in:
@@ -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
|
||||||
|
|||||||
+3
-1
@@ -5,6 +5,9 @@ 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";
|
||||||
|
|
||||||
export default function (eleventyConfig) {
|
export default function (eleventyConfig) {
|
||||||
|
// 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" });
|
||||||
|
|
||||||
@@ -66,7 +69,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)
|
||||||
|
|||||||
+32
-1
@@ -62,13 +62,44 @@ 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 = {
|
||||||
postDate,
|
postDate,
|
||||||
isoDate,
|
isoDate,
|
||||||
limit,
|
limit,
|
||||||
filterTagList,
|
filterTagList,
|
||||||
readingTime,
|
readingTime,
|
||||||
extractHeadings
|
extractHeadings,
|
||||||
|
extractTerminalPreview,
|
||||||
|
removeTerminalPreview
|
||||||
};
|
};
|
||||||
|
|
||||||
export default filters;
|
export default filters;
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -45,7 +45,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 +90,7 @@
|
|||||||
<time datetime="{{ date | isoDate }}">{{ date | postDate }}</time>
|
<time datetime="{{ date | isoDate }}">{{ date | postDate }}</time>
|
||||||
</span>
|
</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span class="meta-item">{{ content | readingTime(readingTime) }}</span>
|
<span class="meta-item">{{ (content | removeTerminalPreview) | readingTime(readingTime) }}</span>
|
||||||
{% if difficulty %}
|
{% if difficulty %}
|
||||||
<span>•</span>
|
<span>•</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 +99,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="markdown-prose">
|
<div class="markdown-prose">
|
||||||
{{ content | safe }}
|
{{ content | removeTerminalPreview | safe }}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,21 +32,23 @@
|
|||||||
{{ featured.data.excerpt }}
|
{{ featured.data.excerpt }}
|
||||||
</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);">></span>
|
<div class="code-titlebar">
|
||||||
<span>init_cluster.sh</span>
|
<span class="code-title">
|
||||||
</span>
|
<span style="color: var(--accent-cyan);">></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">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
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
|
||||||
|
} 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);
|
||||||
|
|
||||||
|
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/);
|
||||||
|
});
|
||||||
@@ -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\)/);
|
||||||
|
});
|
||||||
@@ -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/);
|
||||||
|
});
|
||||||
@@ -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">/);
|
||||||
|
});
|
||||||
@@ -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), "");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user