better templating

This commit is contained in:
Leon van Kammen 2026-07-30 20:53:12 +02:00
parent 1efb6b0053
commit 64e036c632
17 changed files with 1446 additions and 76 deletions

View file

@ -62,3 +62,11 @@ $ DEBUG=1 result/bin/xrforge.com
* server is using lua-scripting via cosmopolitan libc's [redbean](https://redbean.dev) * server is using lua-scripting via cosmopolitan libc's [redbean](https://redbean.dev)
> So yes, the codebase/html/css is intentionally 90-ish: that's **exactly why it works** on potato-devices AND modern devices. > So yes, the codebase/html/css is intentionally 90-ish: that's **exactly why it works** on potato-devices AND modern devices.
## Credits / Made with
* [redbean](https://redbean.dev)
* [soakbean](https://github.com/coderofsalvation/soakbean)
* [xrfragments](https://xrfragment.org)
* [janusxr](https://github.com/jbaicoianu/janusweb)
* [janusweb](https://janusxr.org)

1212
src/.lua/markdown.lua Normal file

File diff suppressed because it is too large Load diff

View file

@ -51,49 +51,80 @@ sb = {
end end
end, end,
-- print( util.tpl("${name} is ${value}", {name = "foo", value = "bar"}) ) -- usage:
-- "foo is bar" --
-- print( util.tpl("${foo(1)}", { foo = function(i) return i+1 end}) ) -- tpl([[
-- 2 -- Hello ${world}
-- print( util.tpl("${if foo then} bar ${end}", { foo = true }) ) -- foo() = ${foo(1)}
-- bar --
tpl = function(s, tab) -- {if true then
tab = tab or sb.data -- print(world)
return (s:gsub('($%b{})', function(w) -- print("hello universe")
local inner = w:sub(3, -2):match("^%s*(.-)%s*$") -- end}
-- Execute as raw Lua code if it contains spaces (e.g. "if bar then") or equals "end" --
if inner:find("%s") or inner == "end" then -- {for i in pairs(numbers) do
local fn = (loadstring or load)("return function(tab) " .. inner .. " end") -- print(i)
if fn then -- end}
setfenv(fn(), setmetatable(tab, { __index = _G }))() --
end -- {for k,v in pairs(items) do
return "" -- print(k .. " " .. v.x)
end -- end}
-- ]],{
-- world="WORLD",
-- foo = function(i) return i+1 end,
-- numbers = {2,3,4},
-- items = {{x=2},{x=3}}
-- })
-- -- Hello WORLD
-- -- foo() = 2
-- -- WORLD
-- -- hello universe
-- -- 1
-- -- 2
-- -- 3
-- -- 1 2
-- -- 2 3
local k, arg = w:match("^%${%s*([%w_]+)%s*%(?(.-)%)?%s*}") tpl = function(s, __data)
local v = tab[k] local __env = {}
if type(v) == "function" then setmetatable(__env, {
return tostring(v(load("return " .. arg)()) or '') __index = function(t, key) return __data[key] or _G[key] end,
__newindex = function(t, key, value) __data[key] = value end
})
-- lets evaluate inline lua linearly
local luastr = 'local str=""\n print = function(s) str = str .. s .. "\\n" end\n'
local islua = false
for str in string.gmatch(s, "([^\n]+)") do
if str:match("{if ") or str:match("{for ") then
luastr = luastr .. str:gsub("{","") .. "\n"
islua = true
elseif str:match("end}") then
luastr = luastr .. "end\n"
islua = false
else
if islua then
luastr = luastr .. str .. "\n"
else
luastr = luastr .. "str = str .. \"" .. str:gsub('"','\\"') .. "\\n\"\n"
end
end end
return v ~= nil and tostring(v) or w end
end)) local s1 = load( luastr .. "\n return str", "chunkname", "t", __env )()
end, -- evaluate template vars
tpl = function(s, tab) local s2 = (s1:gsub('($%b{})', function(w)
tab = tab or sb.data
return (s:gsub('($%b{})', function(w)
local inner = w:sub(3, -2):match("^%s*(.-)%s*$") local inner = w:sub(3, -2):match("^%s*(.-)%s*$")
-- Execute as raw Lua code if it contains spaces or equals "end" -- Execute as raw Lua code if it contains spaces or equals "end"
if inner:find("%s") or inner == "end" then if inner:find("%s") or inner == "end" then
local fn = (loadstring or load)("return function(tab) " .. inner .. " end") local fn = (loadstring or load)("return function(__env) " .. inner .. " end")
if fn then if fn then
setfenv(fn(), setmetatable(tab, { __index = _G }))() setfenv(fn(), setmetatable(__env, { __index = _G }))()
end end
return "" return ""
end end
-- Allow dots in variable names along with alphanumeric characters and underscores -- Allow dots in variable names along with alphanumeric characters and underscores
local path, arg = w:match("^%${%s*([%w_%.]+)" .. "%s*%(?(.-)%)?%s*}") local path, arg = w:match("^%${%s*([%w_%.]+)" .. "%s*%(?(.-)%)?%s*}")
-- Traverse the nested table path (e.g. "foo.bar" -> tab["foo"]["bar"]) -- Traverse the nested table path (e.g. "foo.bar" -> __env["foo"]["bar"])
local v = tab local v = __env
if path then if path then
for key in path:gmatch("[%w_]+") do for key in path:gmatch("[%w_]+") do
if type(v) == "table" then if type(v) == "table" then
@ -111,6 +142,7 @@ sb = {
end end
return v ~= nil and tostring(v) or w return v ~= nil and tostring(v) or w
end)) end))
return s2
end, end,
write = function(str) write = function(str)

View file

@ -84,12 +84,12 @@ end
util.template = function(content,data) util.template = function(content,data)
local mdata = util.merge(data or app,{}) local mdata = util.merge(data or app,{})
mdata = util.merge(mdata, {opts = app.opts}) mdata = util.merge(mdata, {opts = app.opts})
mdata.title = mdata.title or app.opts.subtitle
mdata.header_include = mdata.header_include or '' mdata.header_include = mdata.header_include or ''
mdata.header = app.tpl( LoadAsset('page/header.html'), mdata ) mdata.header = app.tpl( LoadAsset('page/header.html'), mdata )
mdata.footer = app.tpl( LoadAsset('page/footer.html'), mdata ) mdata.footer = app.tpl( LoadAsset('page/footer.html'), mdata )
-- set view url -- set view url
mdata.url = mdata.url or util.getURL() mdata.url = mdata.url or util.getURL()
print_r(mdata)
return app.tpl( content, mdata ) return app.tpl( content, mdata )
end end

View file

@ -155,10 +155,14 @@ pre{
font-size:20px; font-size:20px;
} }
h1,h2,h3,h4,h5{ h1,h2,h3,h4,h5{
font-size: 16px; font-size: 17px;
padding-top: 25px; padding-top: 25px;
padding-bottom:25px; padding-bottom:25px;
} }
h1{
font-size: 29px;
margin: 15px 0px;
}
.sym{ font-size:100px; color:#000; } .sym{ font-size:100px; color:#000; }
@ -253,6 +257,13 @@ b.tag{
border-radius: 5px; border-radius: 5px;
} }
blockquote{
background: #224;
padding: 15px;
margin: 15px 0px;
display: inline-block;
}
/* progressive enhancement quirks */ /* progressive enhancement quirks */
.spectrum{ display:none } .spectrum{ display:none }

View file

@ -72,8 +72,8 @@ img.icon{
width: 17px; width: 17px;
} }
iframe#viewer{ #viewer{
position: fixed; position: absolute;
width: 100%; width: 100%;
height: calc( 99.7vh - 59px); height: calc( 99.7vh - 59px);
margin-top: 0px; margin-top: 0px;

View file

@ -91,7 +91,7 @@ div#xrecosystem{
<h3>Why people want XRForge</h3> <h3>Why people want XRForge</h3>
<div> <div>
In short: its hyperscalable portal-system and ability <b>projecting existing 2D ecosystems</b> into XR.<br> In short: a hyperscalable portal-system and <b>projection</b> of existing 2D ecosystems <b>into XR.</b><br>
XRForge interconnects experiences via portals, according to the <a href="https://coderofsalvation.github.io/janus-guide/" target="_blank">JanusXR</a> and <a href="https://xrfragment.org" target="_blank">XR Fragments standard</a>.<br> XRForge interconnects experiences via portals, according to the <a href="https://coderofsalvation.github.io/janus-guide/" target="_blank">JanusXR</a> and <a href="https://xrfragment.org" target="_blank">XR Fragments standard</a>.<br>
The metaverse has shown: people <b>like 3D</b> but certain <b>existing</b> 2D ecosystems are king.<br> The metaverse has shown: people <b>like 3D</b> but certain <b>existing</b> 2D ecosystems are king.<br>
They're just more cost-efficient to use.<br> They're just more cost-efficient to use.<br>

View file

@ -10,12 +10,10 @@ ${notification}
<form method="POST"> <form method="POST">
<div class="tab-frame"> <div class="tab-frame">
${tabs} {for i,tab in pairs(tabs) do
${for i,tab in pairs(tabs) do} print('<input type="radio" checked name="tab" id="tab' .. i .. '">')
<input type="radio" checked name="tab" id="tab${i}"> print('<label for="tab' .. i .. '">' .. tab .. '</label>')
<label for="tab${i}">${tab}</label> end}
${end}
${plugins} ${plugins}

View file

@ -14,11 +14,13 @@
<td></td> <td></td>
<td></td> <td></td>
<td class=""> <td class="">
$(if #_.plugin.info > 2 then) {if #plugin.info > 2 then
<div class="col3 info"> print([[
${plugin.info} <div class="col3 info">
</div> ${plugin.info}
$(end) </div>
]])
end}
<br/><br/> <br/><br/>
</td> </td>
</tr> </tr>

View file

@ -1,16 +1,20 @@
${header} ${header}
${if _.parent ~= nil and _.parent.parent == nil then} {if parent ~= nil and parent.parent == nil then
<div style="height:120px;" class="jumbotron"> print([[
<h1>${opts.title}</h1> <div style="height:120px;" class="jumbotron">
${opts.subtitle} <h1>${opts.title}</h1>
</div> ${opts.subtitle}
<hr style="margin-top:0"> </div>
${end} <hr style="margin-top:0">
]])
end}
${if _.parent ~= nil and _.parent.parent ~= nil then} {if parent ~= nil and parent.parent ~= nil then
<h2>${parent.name}</h2> print([[
${end} <h2>${parent.name}</h2>
]])
end}
<div class="grid"> <div class="grid">
${html_items} ${html_items}
@ -20,16 +24,23 @@ ${footer}
<!-- JanusXR portalroom, see https://janusxr.org for markup --> <!-- JanusXR portalroom, see https://janusxr.org for markup -->
<fireboxroom> <fireboxroom>
<room pos="0 0 0" skybox="true" showavatar="false" use_local_asset="room_plane"> <assets>
<AssetShader id="xrforgeportal" src="${opts.baseurl}shader/xrforgeportal.shadertoy.frag" shadertype="shadertoy"></AssetShader>
{for i,v in pairs(items) do
print('\t<assetimage src="' .. v.thumbnail .. '" id="img' .. i .. '"/>')
end}
</assets>
<room pos="0 0 -4" skybox="true" showavatar="false" use_local_asset="room_plane">
<light js_id="dan9-janus:light/light_cone_angle=0-6" pos="0 3 2" collision_trigger="false" /> <light js_id="dan9-janus:light/light_cone_angle=0-6" pos="0 3 2" collision_trigger="false" />
<text js_id="location" pos="0 2.6 2" billboard="y" col="#BBFFFF" scale="5 5 1" text="https://xrforge.isvery.ninja/models" rotation="-180 0 -180" emissive="0 0 0" roughness="0.3" metalness="0" /> <text js_id="location" pos="0 6 -5" billboard="y" col="#BBFFFF" scale="10 10 1" text="${url}" rotation="-180 0 -180" emissive="0 0 0" roughness="0.3" metalness="0" />
<CircularLayout pos="0 0 0" radius="4.5" scale="1 1 1" > {for i,v in pairs(items) do
${for k,v in pairs(items) do} local width = 3
<object id="o_${i}"> local center = -((#items*width)/1.3) + (i*width)
<link url="${v.url}" image_id="${v.thumbnail}" pos="0 0 0.001" draw_glow="true" scale="3 3 0.1" thumb_id="thumb_0" title="${v.title}"/> print('\t\t<object js_id="portal_' .. i .. '" pos="' .. center .. ' 0 0" rotation="0 180 0">')
<text text="${v.title}" pos="0 2.3 0.3" font_scale="false" scale="0.15 0.15 0.2" /> print('\t\t <link url="' .. v.url .. '" thumb_id="img' .. i .. '" title_col="0.4 0.37 0.4" pos="0 0 0.001" draw_glow="true" scale="3 3 0.1" />')
</object> print('\t\t <link url="' .. v.url .. '" shader_id="xrforgeportal" pos="0 0 0.003" draw_glow="false" scale="3 3 0.1" />')
${end} print('\t\t <text text="' .. v.title .. '" pos="0 3.4 0" col="1 1 1" font_scale="false" scale="0.2 0.2 0.2" />')
</CircularLayout> print('\t\t</object>')
end}
</room> </room>
</fireboxroom> </fireboxroom>

View file

@ -60,7 +60,8 @@ return {
table.insert(data.items, util.merge({x = i*2, i = i},v) ) table.insert(data.items, util.merge({x = i*2, i = i},v) )
i=i+1 i=i+1
if v.view3D then if v.view3D then
data.view3D = v data.item = v
data.title = data.item.title
local port = ':' .. GetPort() local port = ':' .. GetPort()
if port == 80 then port = '' end if port == 80 then port = '' end
data.url = GetScheme() .. '://' .. GetHost() .. port .. v.url data.url = GetScheme() .. '://' .. GetHost() .. port .. v.url
@ -68,8 +69,8 @@ return {
data.html_items = data.html_items .. tile({item=v}) data.html_items = data.html_items .. tile({item=v})
end end
end) end)
if data.view3D then if data.item then
app.cache.url[ urlpath ] = util.templateFile( "page/index.html", data) app.cache.url[ urlpath ] = util.templateFile( "page/item.html", data)
else else
app.cache.url[ urlpath ] = util.templateFile( "page/dir.html", data) app.cache.url[ urlpath ] = util.templateFile( "page/dir.html", data)
end end

View file

@ -5,7 +5,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<base href="${opts.baseurl}" /> <base href="${opts.baseurl}" />
<title>XRForge - selfsovereign interoperable XR experiences</title> <title>${title}</title>
<link rel="stylesheet" href="css/xrforge.css"></link> <link rel="stylesheet" href="css/xrforge.css"></link>
${header_include} ${header_include}
<noscript> <noscript>
@ -13,7 +13,6 @@
</noscript> </noscript>
</head> </head>
<body background="#000000" class="${page}"> <body background="#000000" class="${page}">
<table width="100%" class="header"> <table width="100%" class="header">
<tr> <tr>
<td width="100"> <td width="100">

View file

@ -1,5 +1,22 @@
${header} ${header}
<iframe border="0" frameborder="0" src="/janusweb/index.html#janus.url=${url}" id="viewer"/> <iframe border="0" frameborder="0" src="/janusweb/index.html#janus.url=${url}" id="viewer"></iframe>
<script>
// minimal JS to sync parent/iframe URL-locations
const iframe = document.querySelector("#viewer")
iframe.addEventListener('load', () => {
const iframeWindow = iframe.contentWindow;
iframeWindow.addEventListener('hashchange', (event) => {
const params = new URLSearchParams( String(iframeWindow.location.hash).replace(/^#/,'?') )
const url = params.get('janus.url')
window.history.pushState( url, iframe.contentDocument.title, url );
setTimeout( () => {
document.title = iframe.contentDocument.title
}, 1000 )
});
});
</script>
${footer} ${footer}

30
src/page/item.html Normal file
View file

@ -0,0 +1,30 @@
${header}
<iframe border="0" frameborder="0" src="/janusweb/index.html#janus.url=${url}&janus.referrer=${item.parent.url}" id="viewer" style="height:45vh"></iframe>
<div style="height:45vh"></div>
<div class="spectrum" style="position: absolute; left: 0px; right: 0px;">
<div></div>
</div>
{if item.parent.info then
print(item.parent.info)
end}
{if not item.parent.info then
print('<h1>${item.title}</h1>\n\n')
print('This item has no description (yet)')
end}
${footer}
<!-- JanusXR portalroom, see https://janusxr.org for markup -->
<fireboxroom>
<assets>
<assetshader id="xrforgeportal" src="${opts.baseurl}shader/xrforgeportal.shadertoy.frag" shadertype="shadertoy"/>
</assets>
<room pos="0 0 -4" showavatar="false">
<light js_id="dan9-janus:light/light_cone_angle=0-6" pos="0 3 2" collision_trigger="false" />
<text js_id="location" pos="0 6 -5" billboard="y" col="#BBFFFF" scale="10 10 1" text="${url}" rotation="-180 0 -180" emissive="0 0 0" roughness="0.3" metalness="0" />
<object id="${url}"/>
</room>
</fireboxroom>

View file

@ -1,10 +1,18 @@
local util = require('util') local util = require('util')
local markdown = require("markdown")
app.on('scan_experience', function(exp) app.on('scan_experience', function(exp)
--print_r(exp) --print_r(exp)
end) end)
app.on('scan_experience_file', function(exp) app.on('scan_experience_file', function(exp)
local me = app.plugin.markdown
local patterns = util.split(app.opts.plugin_markdown_files," ")
foreach( patterns, function(k,v)
if exp.path:match(v) then
exp.parent.info = '<div id="info">' .. markdown( util.readfile(exp.path) ) .. '</div>'
end
end)
end) end)
app.on('set', function(k,v) app.on('set', function(k,v)
@ -20,7 +28,7 @@ local plugin = {
key='app.opts.plugin_markdown_files', key='app.opts.plugin_markdown_files',
title='Markdown files', title='Markdown files',
info='Space-separated, <a href="http://lua-users.org/wiki/PatternsTutorial" target="_blank">lua patterns</a> are allowed', info='Space-separated, <a href="http://lua-users.org/wiki/PatternsTutorial" target="_blank">lua patterns</a> are allowed',
default="*.md", default="README.md",
values='' values=''
}, },
{ {

View file

@ -0,0 +1,41 @@
// Source: https://www.shadertoy.com/view/XtlczH
const float speed = 0.01;
const float twistFrequency = .2;
const float pi = 3.14;
const float tau = pi * 2.0;
const vec4 bg = vec4(0);
const float edgeWidth = 3.0;
const float portalRadius = 9.0;
const float twistEdgeMag = 0.1;
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord / iResolution.xy;
vec2 space = 2.0*vec2(fragCoord.xy - 0.5*iResolution.xy)/iResolution.y;
vec2 unitVector = normalize(space);
space *= 1.0;
float magnitude = length(space);
float v1 = sin(space.x + iTime);
float v2 = sin(space.y + iTime);
float v3 = sin(space.x + space.y + iTime);
float v4 = sin(magnitude + 1.7 * iTime);
float v = v1 + v2 + v3 + v4;
float theta = acos(unitVector.x);
if (space.y < 0.0)
theta = tau - theta;
float twist = theta + magnitude * twistFrequency + -iTime * speed;
twist = mod(twist, tau);
twist *= smoothstep(tau, pi, twist);
twist *= smoothstep(0.0, 4.0, magnitude);
twist = sin(twist);
twist *= 8.0;
v += twist;
vec4 portal = vec4((sin(v * 2.0) + 1.0) * 0.2, 0, (sin(v * 0.4 + 0.3 * pi) + 1.0) * 0.2 + 0.1, 1);
portal.w = .9;
fragColor = mix(bg, portal, smoothstep(portalRadius + edgeWidth + (sin(twist * twistEdgeMag) * 0.5 + 0.5), portalRadius, magnitude)) * vec4(1,1,1, (((sin(iTime*2.0)+1.0)/2.0) * 0.4) );
}

View file

@ -41,7 +41,7 @@ build(){
rebuild(){ rebuild(){
cd src cd src
zip -yr ../result/bin/xrforge.com .lua img page plugin cmd css .init.lua .args test myverse zip -yr ../result/bin/xrforge.com .lua shader img page plugin cmd css .init.lua .args test myverse
} }
"$@" "$@"