Spaces:
Running
Running
Download studwright/app.js from CyberMax-tools/cybermax: direct link, hf CLI and curl.
- Browser
- Download file 6.6 kB
-
https://huggingface.co/spaces/CyberMax-tools/cybermax/resolve/main/studwright/app.js
- Command line
-
hf download hf://spaces/CyberMax-tools/cybermax/studwright/app.js
-
curl -L -o app.js https://huggingface.co/spaces/CyberMax-tools/cybermax/resolve/main/studwright/app.js
6.6 kB
| // Studwright hub page: module catalogue, test list and an in-browser Luau playground for the free modules. | |
| // © 2026 CyberMax. | |
| const $ = (s) => document.querySelector(s); | |
| const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); | |
| const toast = (msg) => { const t = $('#toast'); t.textContent = msg; t.classList.add('on'); setTimeout(() => t.classList.remove('on'), 2200); }; | |
| const EXAMPLES = { | |
| format: `local Format = require(Studwright.Format) | |
| print(Format.abbreviate(1530000)) -- coins on a simulator HUD | |
| print(Format.abbreviate(999999)) -- never rounds up into the wrong suffix | |
| print(Format.abbreviate(2.5e15)) | |
| print(Format.commas(1234567)) | |
| print(Format.clock(3725), Format.duration(3725)) | |
| print("You placed " .. Format.ordinal(22)) | |
| print("Legendary drop chance: " .. Format.percent(0.0125))`, | |
| signal: `local Signal = require(Studwright.Signal) | |
| local coinsChanged = Signal.new() | |
| local hud = coinsChanged:Connect(function(new, delta) | |
| print(("HUD: %d coins (%+d)"):format(new, delta)) | |
| end) | |
| coinsChanged:Once(function() print("first coins ever: show a tip") end) | |
| coinsChanged:Fire(10, 10) | |
| coinsChanged:Fire(25, 15) | |
| hud:Disconnect() | |
| coinsChanged:Fire(30, 5) -- nothing listening any more | |
| print("still connected?", hud.Connected)`, | |
| cleanup: `local Cleanup = require(Studwright.Cleanup) | |
| local Signal = require(Studwright.Signal) | |
| local round = Cleanup.new() | |
| local tick = Signal.new() | |
| round:Connect(tick, function(n) print("round tick", n) end) | |
| round:Add(function() print("map reset") end) | |
| round:Add({ Destroy = function() print("boss despawned") end }) | |
| tick:Fire(1) | |
| print(round:Count(), "things to clean") | |
| round:Clean() -- reverse order: boss, map, then the connection | |
| tick:Fire(2) -- prints nothing: the connection is gone`, | |
| }; | |
| // ---- module catalogue + tests --------------------------------------------------------------- | |
| async function loadCatalogue() { | |
| const data = await (await fetch('modules.json')).json(); | |
| $('#mods').innerHTML = data.modules.map((m) => ` | |
| <details class="mod${m.free ? ' free' : ''}"> | |
| <summary><span class="nm">${esc(m.name)}</span><span class="side ${m.side.toLowerCase()}">${esc(m.side)}</span>${m.free ? '<span class="free-badge">FREE</span>' : ''} | |
| <span class="sm">${esc(m.summary)}</span></summary> | |
| <pre><code>${esc(m.usage || '')}</code></pre> | |
| <p class="hint">${m.lines} lines${m.free ? ` · <a href="free/${esc(m.name)}.luau" target="_blank" rel="noopener">view the full source</a>` : ''}</p> | |
| </details>`).join(''); | |
| } | |
| async function loadTests() { | |
| const r = await fetch('tests.json'); | |
| if (!r.ok) return; | |
| const t = await r.json(); | |
| $('#test-count').textContent = t.pass; | |
| const items = t.tests.map((x) => `<li class="${x.ok ? 'ok' : 'bad'}">${x.ok ? '✓' : '✗'} ${esc(x.name)}</li>`); | |
| const list = $('#tests'); | |
| list.innerHTML = items.slice(0, 12).join(''); | |
| $('#more-tests').addEventListener('click', (e) => { list.innerHTML = items.join(''); e.target.remove(); }); | |
| } | |
| // ---- playground -------------------------------------------------------------------------------- | |
| let LuauState = null; | |
| const sources = {}; | |
| const PRELUDE = ` | |
| local __out = {} | |
| local function print(...) | |
| local n = select("#", ...) | |
| local parts = table.create(n) | |
| for i = 1, n do parts[i] = tostring((select(i, ...))) end | |
| table.insert(__out, table.concat(parts, " ")) | |
| end | |
| local function warn(...) print("warning:", ...) end | |
| local task = {} | |
| function task.spawn(f, ...) | |
| local th = type(f) == "thread" and f or coroutine.create(f) | |
| local ok, err = coroutine.resume(th, ...) | |
| if not ok then print("error: " .. tostring(err)) end | |
| return th | |
| end | |
| task.defer = task.spawn | |
| function task.cancel(th) if coroutine.status(th) == "suspended" then coroutine.close(th) end end | |
| function task.wait() return 0 end | |
| local function typeof(v) return type(v) end | |
| `; | |
| async function loadVM() { | |
| try { | |
| ({ LuauState } = await import('./vendor/luau-web/index.js')); | |
| for (const n of ['Signal', 'Cleanup', 'Format']) sources[n] = await (await fetch(`free/${n}.luau`)).text(); | |
| $('#run').disabled = false; | |
| $('#vm').textContent = 'Luau VM ready.'; | |
| } catch (e) { | |
| $('#vm').textContent = 'Could not load the Luau VM in this browser. You can still download the free modules below.'; | |
| } | |
| } | |
| function bundle(user) { | |
| const defs = Object.entries(sources).map(([n, src]) => `__defs[${JSON.stringify(n)}] = function(script)\n${src}\nend`).join('\n'); | |
| return `${PRELUDE} | |
| local __defs, __cache = {}, {} | |
| local __folder = setmetatable({}, { __index = function(_, k) return { __module = k } end }) | |
| local function require(ref) | |
| local name = ref.__module | |
| assert(__defs[name], "only the free modules (Signal, Cleanup, Format) run here; the kit has all 16") | |
| if __cache[name] == nil then __cache[name] = __defs[name]({ Parent = __folder, Name = name }) end | |
| return __cache[name] | |
| end | |
| ${defs} | |
| local Studwright = __folder | |
| local __ok, __err = pcall(function() | |
| ${user} | |
| end) | |
| if not __ok then print("error: " .. tostring(__err)) end | |
| return table.concat(__out, "\\n")`; | |
| } | |
| async function run() { | |
| const btn = $('#run'); | |
| btn.disabled = true; | |
| const out = $('#out'); | |
| try { | |
| const st = await LuauState.createAsync({}); | |
| const fn = st.loadstring(bundle($('#code').value), 'playground', false); | |
| if (typeof fn !== 'function') { | |
| out.textContent = 'Syntax error: ' + String(fn).replace(/\[string "playground"\]:\d+: /, ''); | |
| } else { | |
| const res = await fn(); | |
| out.textContent = (Array.isArray(res) ? res[0] : res) || '(no output)'; | |
| } | |
| } catch (e) { | |
| out.textContent = 'error: ' + (e && e.message ? e.message : e); | |
| } | |
| btn.disabled = false; | |
| } | |
| function pick(name) { | |
| document.querySelectorAll('#examples .chip').forEach((c) => c.classList.toggle('on', c.dataset.ex === name)); | |
| $('#code').value = EXAMPLES[name]; | |
| $('#out').textContent = 'Press Run.'; | |
| } | |
| $('#examples').addEventListener('click', (e) => { const b = e.target.closest('[data-ex]'); if (b) pick(b.dataset.ex); }); | |
| $('#run').addEventListener('click', run); | |
| $('#code').addEventListener('keydown', (e) => { | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); if (!$('#run').disabled) run(); } | |
| if (e.key === 'Tab') { e.preventDefault(); const t = e.target; const s = t.selectionStart; t.setRangeText(' ', s, t.selectionEnd, 'end'); } | |
| }); | |
| $('#btn-free').addEventListener('click', () => toast('Free modules downloading: drag Studwright.rbxmx into Studio.')); | |
| pick('format'); | |
| loadCatalogue(); | |
| loadTests(); | |
| loadVM(); | |