Skip to content

Commit 4e4f3bc

Browse files
committed
v2.9.0
1 parent bcf6b79 commit 4e4f3bc

181 files changed

Lines changed: 37438 additions & 136 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎vix-site/package-lock.json‎

Lines changed: 1036 additions & 80 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎vix-site/package.json‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
"version": "0.0.0",
55
"type": "module",
66
"scripts": {
7-
"dev": "vite",
8-
"build": "npm run installers:sync && vite build",
7+
"blog:generate": "node scripts/generate-blog-html.mjs",
8+
"dev": "npm run blog:generate && vite",
9+
"build": "npm run blog:generate && vite-ssg build",
910
"docs:dev": "vitepress dev docs",
1011
"docs:build": "vitepress build docs",
1112
"docs:preview": "vitepress preview docs",
@@ -16,10 +17,12 @@
1617
"registry:build": "node tools/build_registry_index.js"
1718
},
1819
"dependencies": {
20+
"@unhead/vue": "^3.4.0",
1921
"dompurify": "^3.3.1",
2022
"marked": "^17.0.1",
2123
"qrcode": "^1.5.4",
2224
"shiki": "^3.22.0",
25+
"vite-ssg": "^28.3.0",
2326
"vue": "^3.5.24",
2427
"vue-router": "^4.6.4"
2528
},
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import {
2+
mkdirSync,
3+
readFileSync,
4+
readdirSync,
5+
rmSync,
6+
writeFileSync,
7+
} from "node:fs";
8+
9+
import { dirname, join, relative, resolve } from "node:path";
10+
11+
import { fileURLToPath } from "node:url";
12+
13+
import { marked } from "marked";
14+
import { createHighlighter } from "shiki";
15+
16+
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
17+
18+
const blogRoot = join(projectRoot, "src", "content", "blog");
19+
20+
const generatedRoot = join(projectRoot, "src", "generated");
21+
22+
const contentRoot = join(generatedRoot, "blog-content");
23+
24+
const indexFile = join(generatedRoot, "blog-index.json");
25+
26+
const languages = [
27+
"cpp",
28+
"c",
29+
"bash",
30+
"cmake",
31+
"json",
32+
"yaml",
33+
"javascript",
34+
"typescript",
35+
"html",
36+
"css",
37+
"markdown",
38+
];
39+
40+
const highlighter = await createHighlighter({
41+
themes: ["github-dark"],
42+
langs: languages,
43+
});
44+
45+
const aliases = {
46+
cc: "cpp",
47+
cxx: "cpp",
48+
49+
sh: "bash",
50+
shell: "bash",
51+
console: "bash",
52+
53+
js: "javascript",
54+
jsx: "javascript",
55+
56+
ts: "typescript",
57+
tsx: "typescript",
58+
59+
yml: "yaml",
60+
61+
md: "markdown",
62+
63+
txt: "text",
64+
plaintext: "text",
65+
};
66+
67+
function normalizeLanguage(language = "") {
68+
const value = language.trim().toLowerCase().split(/\s+/)[0];
69+
70+
return aliases[value] || value;
71+
}
72+
73+
function escapeHtml(value) {
74+
return value
75+
.replace(/&/g, "&")
76+
.replace(/</g, "&lt;")
77+
.replace(/>/g, "&gt;")
78+
.replace(/"/g, "&quot;");
79+
}
80+
81+
function parseFrontmatter(source) {
82+
const match = source.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
83+
84+
const fields = {};
85+
86+
if (match) {
87+
for (const line of match[1].split("\n")) {
88+
const entry = line.match(/^([\w-]+):\s*["']?(.*?)["']?\s*$/);
89+
90+
if (entry) {
91+
fields[entry[1]] = entry[2];
92+
}
93+
}
94+
}
95+
96+
return {
97+
fields,
98+
99+
body: source.slice(match ? match[0].length : 0),
100+
};
101+
}
102+
103+
function titleFromPath(path) {
104+
return path
105+
.split("/")
106+
.at(-1)
107+
.replace(/[-_]/g, " ")
108+
.replace(/\b\w/g, (letter) => letter.toUpperCase());
109+
}
110+
111+
function normalizePath(file) {
112+
const path = relative(blogRoot, file)
113+
.replaceAll("\\", "/")
114+
.replace(/\.md$/, "");
115+
116+
return path === "index" ? "" : path.replace(/\/index$/, "");
117+
}
118+
119+
function normalizeLinks(markdown, path) {
120+
const base = path.split("/").slice(0, -1);
121+
122+
return markdown
123+
.replace(/https?:\/\/blog\.vixcpp\.com\/posts\//g, "/blog/")
124+
.replace(/https?:\/\/blog\.vixcpp\.com\/?/g, "/blog")
125+
.replace(/\]\(\/posts\//g, "](/blog/")
126+
.replace(/\]\((\.\.?\/[^)#]+)\.md(#[^)]+)?\)/g, (_, target, hash = "") => {
127+
const parts = [...base, ...target.split("/")];
128+
129+
const resolved = [];
130+
131+
for (const part of parts) {
132+
if (!part || part === ".") {
133+
continue;
134+
}
135+
136+
if (part === "..") {
137+
resolved.pop();
138+
continue;
139+
}
140+
141+
resolved.push(part);
142+
}
143+
144+
const destination = resolved.join("/").replace(/\/index$/, "");
145+
146+
return `](/blog/${destination}${hash})`;
147+
});
148+
}
149+
150+
function collectMarkdownFiles(directory) {
151+
const files = [];
152+
153+
for (const entry of readdirSync(directory, {
154+
withFileTypes: true,
155+
})) {
156+
const path = join(directory, entry.name);
157+
158+
if (entry.isDirectory()) {
159+
files.push(...collectMarkdownFiles(path));
160+
161+
continue;
162+
}
163+
164+
if (entry.name.endsWith(".md")) {
165+
files.push(path);
166+
}
167+
}
168+
169+
return files;
170+
}
171+
172+
const loadedLanguages = new Set(highlighter.getLoadedLanguages());
173+
174+
const renderer = new marked.Renderer();
175+
176+
renderer.code = ({ text, lang }) => {
177+
const language = normalizeLanguage(lang);
178+
179+
if (!language || language === "text" || !loadedLanguages.has(language)) {
180+
return [
181+
'<pre class="blog-code blog-code--plain">',
182+
`<code>${escapeHtml(text)}</code>`,
183+
"</pre>",
184+
].join("");
185+
}
186+
187+
const highlighted = highlighter.codeToHtml(text, {
188+
lang: language,
189+
theme: "github-dark",
190+
});
191+
192+
return highlighted.replace(
193+
'<pre class="shiki',
194+
'<pre class="blog-code shiki',
195+
);
196+
};
197+
198+
marked.use({
199+
renderer,
200+
});
201+
202+
/*
203+
* Remove stale generated Blog data first.
204+
*/
205+
rmSync(contentRoot, {
206+
recursive: true,
207+
force: true,
208+
});
209+
210+
mkdirSync(contentRoot, {
211+
recursive: true,
212+
});
213+
214+
const posts = [];
215+
216+
for (const file of collectMarkdownFiles(blogRoot)) {
217+
const path = normalizePath(file);
218+
219+
if (!path) {
220+
continue;
221+
}
222+
223+
const source = readFileSync(file, "utf8");
224+
225+
const { fields, body } = parseFrontmatter(source);
226+
227+
const heading = body.match(/^#\s+(.+)$/m)?.[1];
228+
229+
const normalizedBody = normalizeLinks(body, path);
230+
231+
const html = marked.parse(normalizedBody, {
232+
async: false,
233+
});
234+
235+
posts.push({
236+
path,
237+
238+
title: fields.title || heading || titleFromPath(path),
239+
240+
description: fields.description || "",
241+
242+
date: fields.date || "",
243+
244+
author: fields.author || "",
245+
});
246+
247+
const outputFile = join(contentRoot, `${path}.json`);
248+
249+
mkdirSync(dirname(outputFile), {
250+
recursive: true,
251+
});
252+
253+
writeFileSync(outputFile, `${JSON.stringify({ html })}\n`, "utf8");
254+
}
255+
256+
posts.sort(
257+
(a, b) =>
258+
(b.date || "").localeCompare(a.date || "") || a.path.localeCompare(b.path),
259+
);
260+
261+
mkdirSync(generatedRoot, {
262+
recursive: true,
263+
});
264+
265+
writeFileSync(indexFile, `${JSON.stringify(posts, null, 2)}\n`, "utf8");
266+
267+
console.log(
268+
`Generated ${posts.length} Blog posts with build-time syntax highlighting.`,
269+
);

‎vix-site/src/App.vue‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,55 @@
11
<script setup>
2+
import { computed } from "vue";
3+
import { useRoute } from "vue-router";
4+
import { useHead } from "@unhead/vue";
25
import SiteHeader from "./components/layout/SiteHeader.vue";
36
import SiteFooter from "./components/layout/SiteFooter.vue";
7+
import { findBlogPost } from "./data/blog";
8+
9+
const route = useRoute();
10+
const meta = computed(() => {
11+
const post =
12+
route.path.startsWith("/blog/") && findBlogPost(route.params.pathMatch);
13+
const pages = {
14+
"/": [
15+
"Vix.cpp | A runtime for C++ applications",
16+
"Vix.cpp is a runtime for C++ applications. It provides a coherent way to build, run, and operate C++ software throughout its lifecycle.",
17+
],
18+
"/learn": [
19+
"Learn Vix.cpp",
20+
"Learn how Vix.cpp supports C++ application development.",
21+
],
22+
"/community": [
23+
"Community | Vix.cpp",
24+
"Connect with the Vix.cpp community.",
25+
],
26+
"/blog": [
27+
"Blog | Vix.cpp",
28+
"News, technical deep dives, release notes, and development updates from the Vix.cpp project.",
29+
],
30+
};
31+
const [title, description] = post
32+
? [`${post.title} | Vix.cpp`, post.description]
33+
: pages[route.path] || ["Vix.cpp", ""];
34+
const path = post ? `/blog/${post.path}` : route.path;
35+
const url = `https://vixcpp.com${path === "/" ? "/" : path}`;
36+
return {
37+
title,
38+
meta: [
39+
{ name: "description", content: description },
40+
{ property: "og:type", content: post ? "article" : "website" },
41+
{ property: "og:site_name", content: "Vix.cpp" },
42+
{ property: "og:title", content: title },
43+
{ property: "og:description", content: description },
44+
{ property: "og:url", content: url },
45+
{ name: "twitter:card", content: "summary" },
46+
{ name: "twitter:title", content: title },
47+
{ name: "twitter:description", content: description },
48+
],
49+
link: [{ rel: "canonical", href: url }],
50+
};
51+
});
52+
useHead(meta);
453
</script>
554

655
<template>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script setup>
2+
import { loadBlogPostHtml } from "../../data/blog";
3+
4+
const props = defineProps({
5+
path: {
6+
type: String,
7+
required: true,
8+
},
9+
});
10+
11+
const html = await loadBlogPostHtml(props.path);
12+
</script>
13+
14+
<template>
15+
<div class="post-content" v-html="html" />
16+
</template>

0 commit comments

Comments
 (0)