Skip to content

Commit 025bb0a

Browse files
committed
feat(site): integrate Vix documentation under /docs
1 parent 57bd78e commit 025bb0a

1,340 files changed

Lines changed: 392800 additions & 40 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.json‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@
55
"type": "module",
66
"scripts": {
77
"blog:generate": "node scripts/generate-blog-html.mjs",
8-
"dev": "npm run blog:generate && vite",
9-
"build": "npm run blog:generate && vite-ssg build",
10-
"docs:dev": "vitepress dev docs",
11-
"docs:build": "vitepress build docs",
12-
"docs:preview": "vitepress preview docs",
13-
"release": "npm run build && npm run docs:build && node scripts/deploy.js",
8+
"docs:generate": "node scripts/generate-docs-html.mjs",
9+
"dev": "npm run docs:generate && npm run blog:generate && vite",
10+
"build": "npm run docs:generate && npm run blog:generate && vite-ssg build",
11+
"release": "npm run build && node scripts/deploy.js",
1412
"deploy": "npm run release",
1513
"installers:sync": "node scripts/sync-installers.mjs",
1614
"preview": "vite preview",
478 KB
Binary file not shown.
650 KB
Binary file not shown.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2+
import { dirname, join, relative, resolve } from "node:path";
3+
import { fileURLToPath } from "node:url";
4+
import { marked } from "marked";
5+
import { createHighlighter } from "shiki";
6+
7+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
8+
const docsRoot = join(root, "src", "content", "docs");
9+
const generatedRoot = join(root, "src", "generated");
10+
const contentRoot = join(generatedRoot, "docs-content");
11+
const indexFile = join(generatedRoot, "docs-index.json");
12+
const languages = ["cpp", "c", "bash", "cmake", "json", "yaml", "javascript", "typescript", "html", "css", "markdown", "powershell", "toml", "ini", "sql", "xml", "nginx", "dockerfile", "makefile", "python", "vue", "http"];
13+
const highlighter = await createHighlighter({ themes: ["github-dark"], langs: languages });
14+
const loadedLanguages = new Set(highlighter.getLoadedLanguages());
15+
const aliases = { cc: "cpp", cxx: "cpp", sh: "bash", shell: "bash", console: "bash", js: "javascript", ts: "typescript", yml: "yaml", md: "markdown", text: "text", plaintext: "text", txt: "text", dotenv: "ini", properties: "ini" };
16+
17+
function escapeHtml(value) {
18+
return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
19+
}
20+
21+
function walk(directory) {
22+
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
23+
const file = join(directory, entry.name);
24+
return entry.isDirectory() ? walk(file) : entry.name.endsWith(".md") ? [file] : [];
25+
});
26+
}
27+
28+
function pathFor(file) {
29+
const path = relative(docsRoot, file).replaceAll("\\", "/").replace(/\.md$/, "");
30+
return path === "index" ? "" : path.replace(/\/index$/, "");
31+
}
32+
33+
function titleFor(source, path) {
34+
return source.match(/^title:\s*["']?(.+?)["']?\s*$/m)?.[1] || source.match(/^#\s+(.+)$/m)?.[1]?.replace(/[`*_]/g, "") || path.split("/").at(-1).replace(/[-_]/g, " ") || "Vix.cpp Documentation";
35+
}
36+
37+
function sectionTitle(path) {
38+
const known = { api: "API", cli: "CLI", sdks: "SDKs", "app-modules": "Application Modules" };
39+
return known[path] || path.split("/").at(-1).replace(/[-_]/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
40+
}
41+
42+
function resolveDocPath(href, currentPath, paths) {
43+
const [target, hash = ""] = href.split("#");
44+
if (!target || target.startsWith("mailto:") || /^(https?:)?\/\//.test(target)) return href;
45+
if (target === "/vixcpp-documentation.pdf" || target === "/vixcpp-documentation-fr.pdf") return `/docs${target}`;
46+
let candidate;
47+
if (target.startsWith("/")) candidate = target.replace(/^\/+|\/+$/g, "");
48+
else {
49+
const base = currentPath.split("/").slice(0, -1);
50+
candidate = [...base, ...target.split("/")].reduce((parts, part) => {
51+
if (!part || part === ".") return parts;
52+
if (part === "..") { parts.pop(); return parts; }
53+
parts.push(part.replace(/\.md$/, ""));
54+
return parts;
55+
}, []).join("/").replace(/\/index$/, "");
56+
}
57+
candidate = candidate.replace(/\.md$/, "").replace(/\/index$/, "");
58+
return paths.has(candidate) ? `/docs${candidate ? `/${candidate}` : ""}${hash ? `#${hash}` : ""}` : href;
59+
}
60+
61+
function normalizeAdmonitions(source) {
62+
return source.replace(/^:::\s*(warning|tip|danger|info)(?:\s+(.+))?\n([\s\S]*?)^:::\s*$/gm, (_, kind, title, body) =>
63+
`<aside class="docs-callout docs-callout--${kind}"><strong>${title || kind}</strong>\n\n${body.trim()}</aside>`);
64+
}
65+
66+
const files = walk(docsRoot);
67+
const records = files.map((file) => ({ file, path: pathFor(file), source: readFileSync(file, "utf8") }));
68+
const folders = new Set();
69+
for (const record of records) {
70+
const parts = record.path.split("/").filter(Boolean);
71+
for (let length = 1; length < parts.length; length += 1) folders.add(parts.slice(0, length).join("/"));
72+
}
73+
for (const folder of folders) {
74+
if (!records.some((record) => record.path === folder)) records.push({ path: folder, virtual: true, title: sectionTitle(folder) });
75+
}
76+
const paths = new Set(records.map((record) => record.path));
77+
const renderer = new marked.Renderer();
78+
renderer.code = ({ text, lang = "" }) => {
79+
const language = aliases[lang.trim().toLowerCase().split(/\s+/)[0]] || lang.trim().toLowerCase().split(/\s+/)[0];
80+
if (!language || language === "text" || !loadedLanguages.has(language)) return `<pre class="docs-code docs-code--plain"><code>${escapeHtml(text)}</code></pre>`;
81+
return highlighter.codeToHtml(text, { lang: language, theme: "github-dark" }).replace('<pre class="shiki', '<pre class="docs-code shiki');
82+
};
83+
renderer.link = ({ href, title, text }) => `<a href="${escapeHtml(resolveDocPath(href, renderer.currentPath, paths))}"${title ? ` title="${escapeHtml(title)}"` : ""}>${text}</a>`;
84+
marked.use({ renderer });
85+
86+
rmSync(contentRoot, { recursive: true, force: true });
87+
mkdirSync(contentRoot, { recursive: true });
88+
const pages = records.map((record) => {
89+
renderer.currentPath = record.path;
90+
const children = records
91+
.filter((candidate) => candidate.path && candidate.path.startsWith(record.path ? `${record.path}/` : "") && candidate.path.split("/").length === (record.path ? record.path.split("/").length + 1 : 1))
92+
.sort((a, b) => a.path.localeCompare(b.path));
93+
const page = { path: record.path, title: record.path ? (record.title || titleFor(record.source, record.path)) : "Vix.cpp Documentation" };
94+
const html = record.virtual || !record.path
95+
? `<h1>${page.title}</h1><p>Browse the Vix.cpp documentation.</p><ul>${children.map((child) => `<li><a href="/docs/${child.path}">${child.title || titleFor(child.source, child.path)}</a></li>`).join("")}</ul>`
96+
: marked.parse(normalizeAdmonitions(record.source), { async: false });
97+
const output = join(contentRoot, `${record.path || "index"}.json`);
98+
mkdirSync(dirname(output), { recursive: true });
99+
writeFileSync(output, `${JSON.stringify({ html })}\n`);
100+
return page;
101+
}).sort((a, b) => a.path.localeCompare(b.path));
102+
mkdirSync(generatedRoot, { recursive: true });
103+
writeFileSync(indexFile, `${JSON.stringify(pages, null, 2)}\n`);
104+
console.log(`Generated ${pages.length} documentation pages with build-time syntax highlighting.`);

‎vix-site/src/App.vue‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import { useHead } from "@unhead/vue";
55
import SiteHeader from "./components/layout/SiteHeader.vue";
66
import SiteFooter from "./components/layout/SiteFooter.vue";
77
import { findBlogPost } from "./data/blog";
8+
import { findDocsPage } from "./data/docs";
89
910
const route = useRoute();
1011
const meta = computed(() => {
1112
const post =
1213
route.path.startsWith("/blog/") && findBlogPost(route.params.pathMatch);
14+
const doc = route.path.startsWith("/docs") && findDocsPage(route.params.pathMatch);
1315
const pages = {
1416
"/": [
1517
"Vix.cpp | A runtime for C++ applications",
@@ -27,9 +29,15 @@ const meta = computed(() => {
2729
"Blog | Vix.cpp",
2830
"News, release notes, technical deep dives, and development updates from Vix.cpp.",
2931
],
32+
"/docs": [
33+
"Documentation | Vix.cpp",
34+
"Vix.cpp documentation for building native C++ applications.",
35+
],
3036
};
3137
const [title, description] = post
3238
? [`${post.title} | Vix.cpp`, post.description]
39+
: doc
40+
? [`${doc.title} | Vix.cpp`, `Vix.cpp documentation: ${doc.title}.`]
3341
: pages[route.path] || ["Vix.cpp", ""];
3442
const path = post ? `/blog/${post.path}` : route.path;
3543
const url = `https://vixcpp.com${path === "/" ? "/" : path}`;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script setup>
2+
import { computed } from "vue";
3+
import { useRoute } from "vue-router";
4+
5+
const props = defineProps({ node: { type: Object, required: true } });
6+
const route = useRoute();
7+
const path = computed(() => props.node.page?.path || "");
8+
const href = computed(() => `/docs${path.value ? `/${path.value}` : ""}`);
9+
const label = computed(() => props.node.page?.title || props.node.segment.replace(/[-_]/g, " "));
10+
const open = computed(() => route.path.startsWith(href.value));
11+
</script>
12+
13+
<template>
14+
<li class="docs-sidebar-node">
15+
<RouterLink v-if="!node.children?.length" :to="href" :class="{ 'is-active': route.path === href }">{{ label }}</RouterLink>
16+
<details v-else :open="open">
17+
<summary>{{ label }}</summary>
18+
<ul>
19+
<li class="docs-sidebar-node"><RouterLink :to="href" :class="{ 'is-active': route.path === href }">Overview</RouterLink></li>
20+
<DocsSidebarNode v-for="child in node.children" :key="child.segment" :node="child" />
21+
</ul>
22+
</details>
23+
</li>
24+
</template>

‎vix-site/src/content/docs/LICENSE‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 vix
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

0 commit comments

Comments
 (0)