Skip to content

Commit 08ac52d

Browse files
committed
doc: rework
1 parent 512a4c3 commit 08ac52d

26 files changed

Lines changed: 259 additions & 286 deletions

.github/workflows/pages.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
- name: Setup Hugo
4343
uses: peaceiris/actions-hugo@v3
4444
with:
45-
hugo-version: "0.119.0"
45+
hugo-version: "0.154.5"
4646

4747
- name: Build
4848
run: hugo --source web --minify

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ web/public
99
web/data
1010
web/static/runtime
1111
web/static/data
12+
web/content/examples/*.md
13+
!web/content/examples/_index.md

script/build_web.lua

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ local function write_file(path, data)
2626
f:close()
2727
end
2828

29+
local function write_lines(path, lines)
30+
write_file(path, table.concat(lines, "\n") .. "\n")
31+
end
32+
2933
local function read_lines(path)
3034
local f = assert(io.open(path, "r"))
3135
local lines = {}
@@ -104,6 +108,29 @@ local function json_encode(value)
104108
end
105109
end
106110

111+
local function yaml_quote(value)
112+
return value:gsub("\\", "\\\\"):gsub("\"", "\\\"")
113+
end
114+
115+
local function shortcode_quote(value)
116+
return value:gsub("\\", "\\\\"):gsub("\"", "\\\"")
117+
end
118+
119+
local function html_escape(value)
120+
return value
121+
:gsub("&", "&")
122+
:gsub("<", "&lt;")
123+
:gsub(">", "&gt;")
124+
end
125+
126+
local function write_front_matter(lines, fields)
127+
lines[#lines + 1] = "---"
128+
for _, field in ipairs(fields) do
129+
lines[#lines + 1] = string.format("%s: \"%s\"", field.key, yaml_quote(field.value))
130+
end
131+
lines[#lines + 1] = "---"
132+
end
133+
107134
local function titleize(name)
108135
local parts = {}
109136
for part in name:gmatch("[^_%-%s]+") do
@@ -178,6 +205,102 @@ local function parse_args(argv)
178205
return opts
179206
end
180207

208+
local function write_examples_content(site_dir, examples)
209+
local examples_dir = site_dir .. "/content/examples"
210+
run("mkdir -p " .. shell_quote(examples_dir))
211+
212+
for _, example in ipairs(examples) do
213+
local lines = {
214+
"---",
215+
string.format("title: \"%s\"", yaml_quote(example.title)),
216+
string.format("description: \"%s\"", yaml_quote("Soluna example: " .. example.entry)),
217+
string.format("example_id: \"%s\"", yaml_quote(example.id)),
218+
string.format("entry: \"%s\"", yaml_quote(example.entry)),
219+
"---",
220+
}
221+
write_lines(examples_dir .. "/" .. example.id .. ".md", lines)
222+
end
223+
end
224+
225+
local function write_docs_content(site_dir, docs)
226+
local docs_dir = site_dir .. "/content/docs"
227+
run("mkdir -p " .. shell_quote(docs_dir))
228+
229+
local lines = {}
230+
write_front_matter(lines, {
231+
{ key = "title", value = "Docs" },
232+
{ key = "description", value = "Soluna API reference." },
233+
})
234+
lines[#lines + 1] = ""
235+
lines[#lines + 1] = "{{< menubar >}}[home]({{< relurl \"/\" >}}) · [contents](#contents) · [index](#index){{< /menubar >}}"
236+
lines[#lines + 1] = ""
237+
lines[#lines + 1] = "{{< heading level=\"1\" id=\"contents\" text=\"Contents\" >}}"
238+
lines[#lines + 1] = ""
239+
for _, module in ipairs(docs) do
240+
lines[#lines + 1] = string.format("- [%s](#%s)", module.title, module.module)
241+
end
242+
lines[#lines + 1] = ""
243+
lines[#lines + 1] = "{{< heading level=\"1\" id=\"index\" text=\"Index\" >}}"
244+
lines[#lines + 1] = ""
245+
for _, module in ipairs(docs) do
246+
for i, block in ipairs(module.blocks) do
247+
local signature = block.signature or "@block"
248+
lines[#lines + 1] = string.format("- [%s](#%s-%d)", signature, module.module, i)
249+
end
250+
end
251+
lines[#lines + 1] = ""
252+
for _, module in ipairs(docs) do
253+
lines[#lines + 1] = string.format("{{< heading level=\"1\" id=\"%s\" text=\"%s\" >}}", shortcode_quote(module.module), shortcode_quote(module.title))
254+
if module.module ~= "" and module.title ~= "" and module.module:lower() ~= module.title:lower() then
255+
lines[#lines + 1] = ""
256+
lines[#lines + 1] = string.format("{{< small >}}%s{{< /small >}}", module.module)
257+
end
258+
lines[#lines + 1] = ""
259+
for i, block in ipairs(module.blocks) do
260+
local signature = block.signature or "@block"
261+
lines[#lines + 1] = string.format("{{< heading level=\"3\" id=\"%s-%d\" text=\"%s\" code=\"true\" >}}", shortcode_quote(module.module), i, shortcode_quote(signature))
262+
lines[#lines + 1] = ""
263+
if block.docs and #block.docs > 0 then
264+
local paragraph = {}
265+
local has_list = false
266+
local function flush_paragraph()
267+
if #paragraph > 0 then
268+
lines[#lines + 1] = table.concat(paragraph, " ")
269+
lines[#lines + 1] = ""
270+
paragraph = {}
271+
end
272+
end
273+
for _, doc_line in ipairs(block.docs) do
274+
if doc_line:match("^%- ") then
275+
flush_paragraph()
276+
lines[#lines + 1] = doc_line
277+
has_list = true
278+
else
279+
if has_list then
280+
lines[#lines + 1] = ""
281+
has_list = false
282+
end
283+
paragraph[#paragraph + 1] = doc_line
284+
end
285+
end
286+
flush_paragraph()
287+
if has_list then
288+
lines[#lines + 1] = ""
289+
end
290+
end
291+
if block.annos and #block.annos > 0 then
292+
lines[#lines + 1] = "{{< pre >}}"
293+
for _, anno in ipairs(block.annos) do
294+
lines[#lines + 1] = "@" .. anno
295+
end
296+
lines[#lines + 1] = "{{< /pre >}}"
297+
lines[#lines + 1] = ""
298+
end
299+
end
300+
end
301+
write_lines(docs_dir .. "/_index.md", lines)
302+
end
303+
181304
local opts = parse_args(arg)
182305
local soluna_dir = opts.soluna
183306
local site_dir = opts.site
@@ -227,6 +350,9 @@ for _, path in ipairs(doc_paths) do
227350
end
228351
end
229352

353+
write_examples_content(site_dir, examples)
354+
write_docs_content(site_dir, docs)
355+
230356
local examples_payload = json_encode({
231357
generated_at = os.date("!%Y-%m-%dT%H:%M:%SZ"),
232358
examples = examples,
Lines changed: 21 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
const qs = (selector, root = document) => root.querySelector(selector);
2-
const qsa = (selector, root = document) => Array.from(root.querySelectorAll(selector));
32

43
function getBasePath() {
54
if (typeof window.SOLUNA_BASE === "string" && window.SOLUNA_BASE.length > 0) {
@@ -12,134 +11,27 @@ function getBasePath() {
1211
if (segments.length === 0) return "";
1312
if (segments.length === 1) return `/${segments[0]}`;
1413
const last = segments[segments.length - 1];
15-
if (["examples", "docs", "play"].includes(last)) {
16-
return `/${segments.slice(0, -1).join("/")}`;
14+
const penultimate = segments[segments.length - 2];
15+
if (["examples", "docs"].includes(last)) {
16+
const base = segments.slice(0, -1).join("/");
17+
return base ? `/${base}` : "";
1718
}
18-
return `/${segments.join("/")}`;
19-
}
20-
21-
async function loadJson(url) {
22-
const response = await fetch(url);
23-
if (!response.ok) {
24-
throw new Error(`Failed to load ${url}`);
25-
}
26-
return response.json();
27-
}
28-
29-
async function resolveExamples() {
30-
const base = getBasePath();
31-
let examples = window.SOLUNA_EXAMPLES || [];
32-
if (Array.isArray(examples)) return examples;
33-
if (examples && Array.isArray(examples.examples)) return examples.examples;
34-
try {
35-
const data = await loadJson(`${base}/data/examples.json`);
36-
if (data && Array.isArray(data.examples)) return data.examples;
37-
} catch (err) {
38-
console.warn("Failed to load examples.json", err);
19+
if (["examples", "docs"].includes(penultimate)) {
20+
const base = segments.slice(0, -2).join("/");
21+
return base ? `/${base}` : "";
3922
}
40-
return [];
41-
}
42-
43-
async function resolveDocs() {
44-
const base = getBasePath();
45-
let docs = window.SOLUNA_DOCS || [];
46-
if (Array.isArray(docs)) return docs;
47-
if (docs && Array.isArray(docs.modules)) return docs.modules;
48-
try {
49-
const data = await loadJson(`${base}/data/docs.json`);
50-
if (data && Array.isArray(data.modules)) return data.modules;
51-
} catch (err) {
52-
console.warn("Failed to load docs.json", err);
53-
}
54-
return [];
55-
}
56-
57-
function renderExamples(target, examples, limit) {
58-
if (!target) return;
59-
if (!Array.isArray(examples)) {
60-
examples = [];
61-
}
62-
const base = getBasePath();
63-
target.innerHTML = "";
64-
const list = typeof limit === "number" ? examples.slice(0, limit) : examples;
65-
list.forEach((example) => {
66-
const card = document.createElement("a");
67-
card.className = "card";
68-
card.href = `${base}/play/?example=${encodeURIComponent(example.id)}`;
69-
card.innerHTML = `
70-
<div class="card-meta">Example</div>
71-
<h3 class="card-title">${example.title}</h3>
72-
<p class="card-desc">Entry: ${example.entry}</p>
73-
`;
74-
target.appendChild(card);
75-
});
76-
}
77-
78-
function renderDocsGrid(target, modules) {
79-
if (!target) return;
80-
const base = getBasePath();
81-
target.innerHTML = "";
82-
modules.forEach((mod) => {
83-
const card = document.createElement("a");
84-
card.className = "card";
85-
card.href = `${base}/docs/#${encodeURIComponent(mod.module)}`;
86-
card.innerHTML = `
87-
<div class="card-meta">Module</div>
88-
<h3 class="card-title">${mod.title}</h3>
89-
<p class="card-desc">Blocks: ${mod.blocks.length}</p>
90-
`;
91-
target.appendChild(card);
92-
});
93-
}
94-
95-
function setupExampleSearch(input, target, examples) {
96-
if (!input || !target) return;
97-
input.addEventListener("input", () => {
98-
const query = input.value.trim().toLowerCase();
99-
const filtered = examples.filter((example) => {
100-
const haystack = `${example.id} ${example.title} ${example.entry}`.toLowerCase();
101-
return haystack.includes(query);
102-
});
103-
renderExamples(target, filtered);
104-
});
105-
}
106-
107-
function setupDocsSearch(input) {
108-
if (!input) return;
109-
const sections = qsa(".docs-section");
110-
input.addEventListener("input", () => {
111-
const query = input.value.trim().toLowerCase();
112-
sections.forEach((section) => {
113-
const title = (section.dataset.title || "").toLowerCase();
114-
const module = (section.dataset.module || "").toLowerCase();
115-
const match = title.includes(query) || module.includes(query);
116-
section.classList.toggle("is-hidden", query.length > 0 && !match);
117-
});
118-
});
119-
}
120-
121-
async function initHome() {
122-
const examples = await resolveExamples();
123-
const docs = await resolveDocs();
124-
const grid = qs("[data-examples-grid]");
125-
const limit = grid ? Number(grid.dataset.limit || "") : undefined;
126-
renderExamples(grid, examples, Number.isFinite(limit) ? limit : undefined);
127-
renderDocsGrid(qs("[data-docs-grid]"), docs);
128-
const exampleCount = qs("[data-example-count]");
129-
if (exampleCount) exampleCount.textContent = String(examples.length);
130-
const docsCount = qs("[data-docs-count]");
131-
if (docsCount) docsCount.textContent = String(docs.length);
132-
}
133-
134-
async function initExamples() {
135-
const examples = await resolveExamples();
136-
const grid = qs("[data-examples-grid]");
137-
renderExamples(grid, examples);
138-
setupExampleSearch(qs("[data-example-search]"), grid, examples);
23+
return `/${segments.join("/")}`;
13924
}
14025

141-
async function initDocs() {
142-
setupDocsSearch(qs("[data-docs-search]"));
26+
function getExampleId() {
27+
const path = window.location.pathname || "/";
28+
const trimmed = path.replace(/\/index\.html$/, "").replace(/\/$/, "");
29+
if (trimmed === "") return null;
30+
const segments = trimmed.split("/").filter(Boolean);
31+
if (segments.length === 0) return null;
32+
const last = segments[segments.length - 1];
33+
if (last === "examples") return null;
34+
return last;
14335
}
14436

14537
function createZip(entries) {
@@ -272,33 +164,10 @@ function loadRuntimeScript(src) {
272164
}
273165

274166
async function initPlay() {
275-
const examples = await resolveExamples();
276-
if (examples.length === 0) return;
167+
const exampleId = getExampleId();
168+
if (!exampleId) return;
277169
const base = getBasePath();
278170

279-
const params = new URLSearchParams(window.location.search);
280-
const initialId = params.get("example") || examples[0].id;
281-
const selected = examples.find((item) => item.id === initialId) || examples[0];
282-
283-
const tabs = qs("[data-play-tabs]");
284-
if (tabs) {
285-
tabs.innerHTML = "";
286-
examples.forEach((example) => {
287-
const tab = document.createElement("button");
288-
tab.className = "play-tab";
289-
tab.textContent = example.title;
290-
if (example.id === selected.id) {
291-
tab.classList.add("active");
292-
}
293-
tab.addEventListener("click", () => {
294-
const nextUrl = new URL(window.location.href);
295-
nextUrl.searchParams.set("example", example.id);
296-
window.location.href = nextUrl.toString();
297-
});
298-
tabs.appendChild(tab);
299-
});
300-
}
301-
302171
const codeTarget = qs("#code-content");
303172
const consoleTarget = qs("#console-output");
304173
const appendConsole = (text, isError) => {
@@ -325,7 +194,7 @@ async function initPlay() {
325194

326195
let sourceText = "";
327196
try {
328-
sourceText = await loadText(`${base}/runtime/test/${selected.id}.lua`);
197+
sourceText = await loadText(`${base}/runtime/test/${exampleId}.lua`);
329198
if (codeTarget) codeTarget.textContent = sourceText;
330199
} catch (err) {
331200
setStatus("Failed to load example source.");
@@ -411,9 +280,5 @@ async function initPlay() {
411280
}
412281

413282
document.addEventListener("DOMContentLoaded", () => {
414-
const page = document.body.dataset.page;
415-
if (page === "home") initHome();
416-
if (page === "examples") initExamples();
417-
if (page === "docs") initDocs();
418-
if (page === "play") initPlay();
283+
initPlay();
419284
});

0 commit comments

Comments
 (0)