Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions apps/site-tasks/build-how-to.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Generates the `how-to` content collection from the single source of
// truth at `crates/compiler/zo-how-to/`. Each example is a `.zo` (code)
// plus an optional sibling `.md` (explanation). The site never reads the
// crate directly — this prebuild step mirrors every example into
// `apps/site/src/content/how-to/<category>/<group?>/<name>.md`, embedding
// the raw code as a YAML literal block scalar and the explanation as the
// body. Run from `prebuild`.

import { readdir, readFile, writeFile, mkdir, rm } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const ROOT = dirname(fileURLToPath(import.meta.url));
const HOWTO = join(ROOT, "..", "..", "crates", "compiler", "zo-how-to");
const OUT = join(ROOT, "..", "site", "src", "content", "how-to");

// Subdirs that may sit next to sources but aren't examples (build
// artifacts, shared fixtures) — never walked.
const SKIP_DIRS = new Set(["deps", "samples"]);

// `001_hello` -> { order: 1, name: "hello" }. No prefix keeps the bare
// stem and sorts last. The prefix is for on-disk ordering only; it never
// reaches the slug or title.
function parsePrefix(stem) {
const match = stem.match(/^(\d+)_(.+)$/);

if (match) return { order: Number(match[1]), name: match[2] };

return { order: Number.POSITIVE_INFINITY, name: stem };
}

// Indent every line by 2 spaces so the raw `.zo` is a valid YAML literal
// block scalar — consistent indentation greater than the `code:` key is
// all YAML needs, so blank lines and `--` comments can't break the parse.
function indentCode(source) {
return source
.replace(/\s+$/, "")
.split("\n")
.map((line) => ` ${line}`)
.join("\n");
}

// Lifts a `title:` from the explanation's optional frontmatter and
// returns the body without it. Tiny on purpose — only `title` matters.
function splitExplanation(raw) {
if (!raw.startsWith("---\n")) return { title: null, body: raw.trim() };

const end = raw.indexOf("\n---\n", 4);

if (end === -1) return { title: null, body: raw.trim() };

const yaml = raw.slice(4, end);
const body = raw.slice(end + 5).trim();
const match = yaml.match(/^title:\s*(.+)$/m);

return { title: match ? match[1].trim() : null, body };
}

function humanize(name) {
return name.replace(/[_-]+/g, " ");
}

// Collect `{ category, group, name, order, title, code, body }` for every
// `.zo` under each category root and its one level of group subdirs.
async function collectExamples() {
const examples = [];

// Top-level domains are whatever directories sit under `zo-how-to`
// (`zo`, `zsx`, `providers`, …) — discovered, not hardcoded, so a
// restructure needs no generator change.
const roots = await readdir(HOWTO, { withFileTypes: true });
const categories = roots
.filter((entry) => entry.isDirectory() && !SKIP_DIRS.has(entry.name))
.map((entry) => entry.name)
.sort();

for (const category of categories) {
const categoryDir = join(HOWTO, category);
const sources = [];

for (const entry of await readdir(categoryDir, { withFileTypes: true })) {
if (entry.isFile() && entry.name.endsWith(".zo")) {
sources.push({ dir: categoryDir, group: null, file: entry.name });
} else if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
const groupDir = join(categoryDir, entry.name);

for (const sub of await readdir(groupDir, { withFileTypes: true })) {
if (sub.isFile() && sub.name.endsWith(".zo")) {
sources.push({ dir: groupDir, group: entry.name, file: sub.name });
}
}
}
}

for (const source of sources) {
const stem = source.file.slice(0, -".zo".length);
const { order, name } = parsePrefix(stem);
const code = await readFile(join(source.dir, source.file), "utf8");

const explanationPath = join(source.dir, `${stem}.md`);
let title = null;
let body = "";

if (existsSync(explanationPath)) {
const explanation = splitExplanation(
await readFile(explanationPath, "utf8"),
);

title = explanation.title;
body = explanation.body;
}

examples.push({
category,
group: source.group,
name,
order,
title: title ?? humanize(name),
code,
body,
});
}
}

return examples;
}

const examples = await collectExamples();

// Rebuild from scratch so deleted/renamed examples never linger.
await rm(OUT, { recursive: true, force: true });

for (const example of examples) {
const dir = example.group
? join(OUT, example.category, example.group)
: join(OUT, example.category);

await mkdir(dir, { recursive: true });

const frontmatter = [
"---",
`category: ${example.category}`,
example.group ? `group: ${example.group}` : null,
`title: ${JSON.stringify(example.title)}`,
Number.isFinite(example.order) ? `order: ${example.order}` : null,
"code: |",
indentCode(example.code),
"---",
"",
example.body,
"",
]
.filter((line) => line !== null)
.join("\n");

await writeFile(join(dir, `${example.name}.md`), frontmatter);
}

console.log(`[how-to] generated ${examples.length} example(s) -> ${OUT}`);
2 changes: 2 additions & 0 deletions apps/site/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ dist/
public/install.sh
# generated by scripts/build-llms-full.mjs — source is content/initiation/en/
public/docs/llms.txt
# generated by site-tasks/build-how-to.mjs — source is crates/compiler/zo-how-to/
src/content/how-to/

# dependencies
node_modules/
Expand Down
1 change: 1 addition & 0 deletions apps/site/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"foundations_description": "Angetrieben von modernen, verlässlichen Fundamenten für eine <b class='green-100'>hochleistungsfähige</b> <b class='green-100'>Grafik</b>- und <b class='green-100'>Web</b>-Integration.",
"foundations_title": "## fundamente",
"header_initiation": "Einführung",
"header_how_to": "Anleitungen",
"header_spec": "Spec",
"header_news": "News",
"hero_tagline": "Verwandle deine <b class='green-100'>Gedanken</b> sofort in <b class='green-100'>typsichere</b> Software und <b class='green-100'>Ui</b>.",
Expand Down
1 change: 1 addition & 0 deletions apps/site/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"foundations_description": "Powered by modern and reliable foundations to deliver <b class='green-100'>high-performance</b> <b class='green-100'>graphics</b> and <b class='green-100'>web</b> integration.",
"foundations_title": "## foundations",
"header_initiation": "Initiation",
"header_how_to": "How-to",
"header_spec": "Spec",
"header_news": "News",
"hero_tagline": "Turn your <b class='green-100'>thoughts</b> into <b class='green-100'>type-safe</b> software and <b class='green-100'>Ui</b> instantly.",
Expand Down
1 change: 1 addition & 0 deletions apps/site/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"foundations_description": "Propulsé par des fondations modernes et fiables pour offrir une intégration <b class='green-100'>haute performance</b> <b class='green-100'>graphique</b> et <b class='green-100'>web</b>.",
"foundations_title": "## fondations",
"header_initiation": "Initiation",
"header_how_to": "Tutoriels",
"header_spec": "Spec",
"header_news": "Actus",
"hero_tagline": "Transforme tes <b class='green-100'>pensées</b> en logiciels à <b class='green-100'>typage sûr</b> et en <b class='green-100'>UI</b> instantanément.",
Expand Down
1 change: 1 addition & 0 deletions apps/site/messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"foundations_description": "現代的で信頼できる基盤に支えられ、<b class='green-100'>高性能</b>な<b class='green-100'>グラフィックス</b>と<b class='green-100'>ウェブ</b>統合を実現する。",
"foundations_title": "## 基盤",
"header_initiation": "入門",
"header_how_to": "チュートリアル",
"header_spec": "仕様",
"header_news": "ニュース",
"hero_tagline": "あなたの<b class='green-100'>考え</b>を、<b class='green-100'>型安全</b>なソフトウェアと <b class='green-100'>Ui</b> に即座に変える。",
Expand Down
1 change: 1 addition & 0 deletions apps/site/messages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"foundations_description": "由现代且可靠的基础驱动,提供<b class='green-100'>高性能</b>的<b class='green-100'>图形</b>与<b class='green-100'>网页</b>集成。",
"foundations_title": "## 基础",
"header_initiation": "入门",
"header_how_to": "教程",
"header_spec": "规范",
"header_news": "新闻",
"hero_tagline": "把你的<b class='green-100'>想法</b>瞬间变成<b class='green-100'>类型安全</b>的软件和 <b class='green-100'>Ui</b>。",
Expand Down
6 changes: 4 additions & 2 deletions apps/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
"node": "24.x"
},
"scripts": {
"predev": "npm run zo:howto",
"dev": "astro dev",
"prebuild": "npm run zo:install && npm run zo:docs",
"prebuild": "npm run zo:install && npm run zo:docs && npm run zo:howto",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"zo:install": "cp ../../tasks/zo-install.sh public/install.sh",
"zo:docs": "node ../site-tasks/build-llms-full.mjs"
"zo:docs": "node ../site-tasks/build-llms-full.mjs",
"zo:howto": "node ../site-tasks/build-how-to.mjs"
},
"dependencies": {
"@astrojs/sitemap": "^3.7.3",
Expand Down
2 changes: 2 additions & 0 deletions apps/site/src/components/structure/header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const { class: className, id } = Astro.props;
<header class:list={["header", className]} id={id} role="banner">
<Link class="header-btn header-btn--home f-cirka" href="/" label="zo" />
<Link class="header-btn header-btn--initiation" href="/initiation" label={textTransform(m.header_initiation())} />
<Link class="header-btn header-btn--how-to" href="/how-to" label={textTransform(m.header_how_to())} />
<Link class="header-btn header-btn--spec" href="/spec" label={textTransform(m.header_spec())} />
<Link class="header-btn header-btn--news" href="/news" label={textTransform(m.header_news())} />
<LocaleSwitcher />
Expand Down Expand Up @@ -124,6 +125,7 @@ const { class: className, id } = Astro.props;
}

.header--inline .header-btn--home,
.header--inline .header-btn--how-to,
.header--inline .header-btn--spec,
.header--inline .header-btn--news,
.header--inline .locale-switcher-wrap {
Expand Down
19 changes: 15 additions & 4 deletions apps/site/src/content.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";

// Multi-locale layout: every entry lives under <locale>/.
// Entry `id` becomes "<locale>/<slug>", which downstream code splits to
// match the active request locale (with fallback to baseLocale).
const news = defineCollection({
loader: glob({
pattern: "*/S0*/S0*.md",
Expand Down Expand Up @@ -46,4 +43,18 @@ const faq = defineCollection({
}),
});

export const collections = { news, initiation, spec, faq };
const howto = defineCollection({
loader: glob({
pattern: "**/*.md",
base: "./src/content/how-to",
}),
schema: z.object({
category: z.string(),
group: z.string().optional(),
title: z.string().optional(),
order: z.number().optional(),
code: z.string(),
}),
});

export const collections = { news, initiation, spec, faq, howto };
4 changes: 3 additions & 1 deletion apps/site/src/content/initiation/en/001-prologue.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ title: Prologue

# prologue

[tsh-tsh] This initiation is for the `zo` programming language.
[tsh-tsh]

This initiation is for the `zo` programming language.

What are we waiting for to improve our developer experience? Why does having to wait several seconds, or even minutes, to get feedback on the correctness of our program bother absolutely no one?

Expand Down
6 changes: 3 additions & 3 deletions apps/site/src/content/initiation/en/099-epilogue.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# epilogue

zo is inherit concepts from rust-prehistory (thanks to graydon hoare for the hard work), imba for its simplicity, e4x for its advanced xml extension, cyclone, erlang & go for their concurrent system. zo do not reinvent the wheel, it just applies what already exist and tries to do it in a correct manner.
Your initiation is complete. Now that you have mastered the fundamentals of the `zo` language — it is up to you to get creative and show the world your talent. We have put together a collection of programs for you to browse, sample, replicate, and modify however you like.

You've finished the initiation, you're now ready to build something that matter for you. Even if you don't use zo again, we hope that you've appreciated the journey and whish all the best in your developer experience.
Click the following link to access them: [@how-to](/how-to)

TRiLU!
TRiLU!
4 changes: 2 additions & 2 deletions apps/site/src/content/initiation/fr/001-prologue.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ title: Prologue

# prologue

[tsh-tsh]

[tsh-tsh]
Qu'attendons-nous pour améliorer notre experience en tant que développeur ? Pourquoi devoir attendre plusieurs secondes voire minutes avant d'avoir un retour sur l'exactitude de notre programme ne dérange-t-il personne ?

La qualité de nos outils et infrastructures actuels frôle la médiocrité. Heureusement certains acteurs, réfractaires à cette fumisterie ambiante, continuent perpétuellement de perfectionner notre ecosystème. Accepter leur médiocrité c'est se soumettre face aux diktats de commités d'exécution qui nous imposent leurs produits par vague de marketing intensif.
Expand Down
7 changes: 7 additions & 0 deletions apps/site/src/content/initiation/fr/099-epilogue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# epilogue

Ton initiation est finie, maintenant que tu maîtrises les fondamentaux du langage zo — à toi de faire preuve de créativité pour montrer au monde tout ton talent. Nous avons mis à ta disposition une collection de programmes que tu peux consulter, sampler, répliquer, modifier à ta guise.

Clique sur le lien suivant pour y accéder: [@how-to](/how-to)

TRiLU!
Loading
Loading