Skip to content

Commit b3578a8

Browse files
committed
doc: add website live example
1 parent 8e575e7 commit b3578a8

20 files changed

Lines changed: 1513 additions & 0 deletions

File tree

.github/workflows/pages.yml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: GitHub Pages
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
pull_request:
8+
workflow_dispatch:
9+
10+
jobs:
11+
build:
12+
name: Build Web Site
13+
if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/master'
14+
runs-on: ubuntu-22.04
15+
concurrency:
16+
group: ${{ github.workflow }}-${{ github.ref }}
17+
steps:
18+
- uses: actions/checkout@v5
19+
with:
20+
submodules: recursive
21+
fetch-depth: 0
22+
23+
- name: Build Soluna (WASM)
24+
uses: ./.github/actions/soluna
25+
id: build
26+
with:
27+
soluna_path: "."
28+
29+
- name: Install web build tools
30+
run: |
31+
sudo apt-get update
32+
sudo apt-get install -y lua5.4 zip
33+
34+
- name: Prepare web assets
35+
run: |
36+
lua5.4 script/build_web.lua \
37+
--soluna . \
38+
--site web \
39+
--wasm "${{ steps.build.outputs.SOLUNA_WASM_PATH }}" \
40+
--js "${{ steps.build.outputs.SOLUNA_JS_PATH }}"
41+
42+
- name: Setup Hugo
43+
uses: peaceiris/actions-hugo@v3
44+
with:
45+
hugo-version: "0.119.0"
46+
47+
- name: Build
48+
run: hugo --source web --minify
49+
50+
- name: Upload static files as artifact
51+
id: deployment
52+
uses: actions/upload-pages-artifact@v3
53+
with:
54+
path: web/public
55+
56+
deploy:
57+
name: Deploy to GitHub Pages
58+
if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/master'
59+
needs: [build]
60+
runs-on: ubuntu-22.04
61+
permissions:
62+
pages: write
63+
id-token: write
64+
environment:
65+
name: github-pages
66+
url: ${{ steps.deployment.outputs.page_url }}
67+
steps:
68+
- name: Deploy to GitHub Pages
69+
id: deployment
70+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ bin/macos
55
bin/mingw
66
bin/msvc
77
bin/emcc
8+
web/public
9+
web/data
10+
web/static/runtime

script/build_web.lua

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
local function trim(s)
2+
return (s:gsub("^%s+", ""):gsub("%s+$", ""))
3+
end
4+
5+
local function shell_quote(value)
6+
return "'" .. value:gsub("'", "'\\''") .. "'"
7+
end
8+
9+
local function run(cmd)
10+
local ok = os.execute(cmd)
11+
if ok ~= true and ok ~= 0 then
12+
error("command failed: " .. cmd)
13+
end
14+
end
15+
16+
local function read_file(path)
17+
local f = assert(io.open(path, "rb"))
18+
local data = f:read("*a")
19+
f:close()
20+
return data
21+
end
22+
23+
local function write_file(path, data)
24+
local f = assert(io.open(path, "wb"))
25+
f:write(data)
26+
f:close()
27+
end
28+
29+
local function read_lines(path)
30+
local f = assert(io.open(path, "r"))
31+
local lines = {}
32+
for line in f:lines() do
33+
lines[#lines + 1] = line
34+
end
35+
f:close()
36+
return lines
37+
end
38+
39+
local function exec_lines(cmd)
40+
local p = assert(io.popen(cmd, "r"))
41+
local lines = {}
42+
for line in p:lines() do
43+
if line ~= "" then
44+
lines[#lines + 1] = line
45+
end
46+
end
47+
p:close()
48+
return lines
49+
end
50+
51+
local function is_array(tbl)
52+
local count = 0
53+
for k, _ in pairs(tbl) do
54+
if type(k) ~= "number" then
55+
return false
56+
end
57+
count = count + 1
58+
end
59+
for i = 1, count do
60+
if tbl[i] == nil then
61+
return false
62+
end
63+
end
64+
return true
65+
end
66+
67+
local function json_escape(value)
68+
return value
69+
:gsub("\\", "\\\\")
70+
:gsub("\"", "\\\"")
71+
:gsub("\b", "\\b")
72+
:gsub("\f", "\\f")
73+
:gsub("\n", "\\n")
74+
:gsub("\r", "\\r")
75+
:gsub("\t", "\\t")
76+
end
77+
78+
local function json_encode(value)
79+
local t = type(value)
80+
if t == "nil" then
81+
return "null"
82+
elseif t == "boolean" then
83+
return value and "true" or "false"
84+
elseif t == "number" then
85+
return tostring(value)
86+
elseif t == "string" then
87+
return "\"" .. json_escape(value) .. "\""
88+
elseif t == "table" then
89+
if is_array(value) then
90+
local parts = {}
91+
for i = 1, #value do
92+
parts[#parts + 1] = json_encode(value[i])
93+
end
94+
return "[" .. table.concat(parts, ",") .. "]"
95+
end
96+
local parts = {}
97+
for k, v in pairs(value) do
98+
parts[#parts + 1] = json_encode(k) .. ":" .. json_encode(v)
99+
end
100+
table.sort(parts)
101+
return "{" .. table.concat(parts, ",") .. "}"
102+
else
103+
error("unsupported json type: " .. t)
104+
end
105+
end
106+
107+
local function titleize(name)
108+
local parts = {}
109+
for part in name:gmatch("[^_%-%s]+") do
110+
parts[#parts + 1] = part:sub(1, 1):upper() .. part:sub(2)
111+
end
112+
return table.concat(parts, " ")
113+
end
114+
115+
local function parse_doc_file(path)
116+
local blocks = {}
117+
local doc_lines = {}
118+
local annos = {}
119+
120+
local function flush(signature)
121+
if #doc_lines == 0 and #annos == 0 then
122+
return
123+
end
124+
blocks[#blocks + 1] = {
125+
signature = signature,
126+
docs = doc_lines,
127+
annos = annos,
128+
}
129+
doc_lines = {}
130+
annos = {}
131+
end
132+
133+
for _, line in ipairs(read_lines(path)) do
134+
if line:match("^%-%-%-@") then
135+
annos[#annos + 1] = trim(line:gsub("^%-%-%-@", ""))
136+
elseif line:match("^%-%-%-") then
137+
doc_lines[#doc_lines + 1] = trim(line:gsub("^%-%-%-%s?", ""))
138+
else
139+
local trimmed = trim(line)
140+
if trimmed ~= "" and (#doc_lines > 0 or #annos > 0) then
141+
flush(trimmed)
142+
end
143+
end
144+
end
145+
flush(nil)
146+
return blocks
147+
end
148+
149+
local function parse_args(argv)
150+
local opts = {
151+
soluna = ".",
152+
site = "web",
153+
wasm = nil,
154+
js = nil,
155+
}
156+
local i = 1
157+
while i <= #argv do
158+
local arg = argv[i]
159+
if arg == "--soluna" then
160+
opts.soluna = argv[i + 1]
161+
i = i + 2
162+
elseif arg == "--site" then
163+
opts.site = argv[i + 1]
164+
i = i + 2
165+
elseif arg == "--wasm" then
166+
opts.wasm = argv[i + 1]
167+
i = i + 2
168+
elseif arg == "--js" then
169+
opts.js = argv[i + 1]
170+
i = i + 2
171+
else
172+
error("unknown argument: " .. arg)
173+
end
174+
end
175+
if not opts.wasm or not opts.js then
176+
error("missing --wasm or --js argument")
177+
end
178+
return opts
179+
end
180+
181+
local opts = parse_args(arg)
182+
local soluna_dir = opts.soluna
183+
local site_dir = opts.site
184+
local runtime_dir = site_dir .. "/static/runtime"
185+
local data_dir = site_dir .. "/data"
186+
187+
run("mkdir -p " .. shell_quote(runtime_dir))
188+
run("mkdir -p " .. shell_quote(runtime_dir .. "/test"))
189+
run("mkdir -p " .. shell_quote(data_dir))
190+
191+
run("cp " .. shell_quote(opts.wasm) .. " " .. shell_quote(runtime_dir .. "/soluna.wasm"))
192+
run("cp " .. shell_quote(opts.js) .. " " .. shell_quote(runtime_dir .. "/soluna.js"))
193+
194+
run("cd " .. shell_quote(soluna_dir) .. " && zip -r " .. shell_quote(runtime_dir .. "/asset.zip") .. " asset")
195+
run("cp -R " .. shell_quote(soluna_dir .. "/test/.") .. " " .. shell_quote(runtime_dir .. "/test"))
196+
197+
local examples = {}
198+
local example_paths = exec_lines("find " .. shell_quote(soluna_dir .. "/test") .. " -maxdepth 1 -type f -name '*.lua' -print")
199+
table.sort(example_paths)
200+
for _, path in ipairs(example_paths) do
201+
local name = path:match("([^/]+)%.lua$")
202+
if name then
203+
local content = read_file(path)
204+
if not content:find("font%.system") then
205+
examples[#examples + 1] = {
206+
id = name,
207+
title = titleize(name),
208+
entry = "test/" .. name .. ".lua",
209+
}
210+
end
211+
end
212+
end
213+
214+
local docs = {}
215+
local doc_paths = exec_lines("find " .. shell_quote(soluna_dir .. "/docs") .. " -maxdepth 1 -type f -name '*.lua' -print")
216+
table.sort(doc_paths)
217+
for _, path in ipairs(doc_paths) do
218+
local name = path:match("([^/]+)%.lua$")
219+
if name then
220+
docs[#docs + 1] = {
221+
module = name,
222+
title = titleize(name),
223+
blocks = parse_doc_file(path),
224+
}
225+
end
226+
end
227+
228+
write_file(data_dir .. "/examples.json", json_encode({
229+
generated_at = os.date("!%Y-%m-%dT%H:%M:%SZ"),
230+
examples = examples,
231+
}))
232+
233+
write_file(data_dir .. "/docs.json", json_encode({
234+
generated_at = os.date("!%Y-%m-%dT%H:%M:%SZ"),
235+
modules = docs,
236+
}))

web/config.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
baseURL = "/"
2+
title = "Soluna Web Lab"
3+
languageCode = "en-us"
4+
paginate = 12
5+
relativeURLs = true
6+
canonifyURLs = true
7+
8+
[params]
9+
description = "Soluna documentation and live examples running in the browser."

web/content/_index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
title: "Soluna Web Lab"
3+
description: "Live docs and examples for the Soluna engine."
4+
---

web/content/docs/_index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
title: "Docs"
3+
description: "Soluna API reference."
4+
---

web/content/examples/_index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
title: "Examples"
3+
description: "Gallery of Soluna test entries."
4+
---

web/content/play/_index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
title: "Play"
3+
description: "Run Soluna examples in the browser."
4+
---

web/layouts/_default/baseof.html

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}</title>
7+
<meta name="description" content="{{ with .Params.description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}" />
8+
<link rel="stylesheet" href="{{ "site.css" | relURL }}" />
9+
</head>
10+
<body data-page="{{ .Section | default "home" }}">
11+
<div class="page">
12+
{{ partial "nav.html" . }}
13+
<main>
14+
{{ block "main" . }}{{ end }}
15+
</main>
16+
{{ partial "footer.html" . }}
17+
</div>
18+
<script>
19+
window.SOLUNA_BASE = "{{ "/" | relURL }}".replace(/\/$/, "");
20+
</script>
21+
<script src="{{ "site.js" | relURL }}"></script>
22+
{{ block "page_script" . }}{{ end }}
23+
</body>
24+
</html>

0 commit comments

Comments
 (0)