updated template(engine) + added janusweb propertly

This commit is contained in:
Leon van Kammen 2026-07-29 22:44:24 +02:00
parent 6a14efeba6
commit 1efb6b0053
23 changed files with 1066 additions and 152 deletions

2
.gitignore vendored
View file

@ -1,6 +1,6 @@
experienceA
experienceB
result
src/v
src/janusweb
janusweb*
myverse

View file

@ -4,8 +4,10 @@ package.path = package.path .. ";middleware/?.lua"
package.path = package.path .. ";cmd/?.lua"
package.path = package.path .. ";plugin/?.lua"
util = require "util"
json = require "json"
util = require "util"
json = require "json"
liluat = require "liluat"
template = require "template"
-- special script called by main redbean process at startup
HidePath('/usr/share/zoneinfo/')
@ -22,10 +24,16 @@ app = require("soakbean") {
cmd={
help = {file="cmd/help.lua", info="display all possible flags"},
server = {file="cmd/server.lua", info="starts the webserver"},
admin = {file="cmd/server.lua", info="launch adminpanel to configure stuff (/admin)"},
createplugin = {file="cmd/createplugin.lua", info="generates skeleton plugin"},
generate = {file="cmd/generate.lua", info="generate www-folder with XR experiences"},
test = {file="cmd/test.lua", info="run testsuite (selftest)"}
}
},
addPlugin = function(file)
local p = require(file)
table.insert(app.plugin,p)
return p
end
}
-- api skeleton (implemented via soakbean's app.on(...))
@ -35,16 +43,16 @@ app.init = function() end
-- load config
app.opts = require('config')(app)
-- load system plugins
app.plugin.moderation = require('plugin/moderation')
app.plugin.diskscan = require('plugin/diskscan')
app.plugin.xrfragments = require('plugin/xrfragments')
app.plugin.janusxr = require('plugin/janusxr')
app.plugin.manyfold = require('plugin/manyfold')
app.plugin.git = require('plugin/git')
app.plugin.clouddrive = require('plugin/clouddrive')
app.plugin.webhook = require('plugin/webhook')
app.plugin.markdown = require('plugin/markdown')
-- load system plugins (in specific order for admin panel)
app.plugin.diskscan = app.addPlugin('plugin/diskscan')
app.plugin.moderation = app.addPlugin('plugin/moderation')
app.plugin.xrfragments = app.addPlugin('plugin/xrfragments')
app.plugin.janusxr = app.addPlugin('plugin/janusxr')
app.plugin.manyfold = app.addPlugin('plugin/manyfold')
app.plugin.git = app.addPlugin('plugin/git')
app.plugin.clouddrive = app.addPlugin('plugin/clouddrive')
app.plugin.webhook = app.addPlugin('plugin/webhook')
app.plugin.markdown = app.addPlugin('plugin/markdown')
-- ensure plugin config-defaults
foreach( app.plugin, function(name,plugin)
@ -56,13 +64,6 @@ foreach( app.plugin, function(name,plugin)
end)
end)
-- set order preference (optional)
app.plugin[1] = app.plugin.diskscan
app.plugin[2] = app.plugin.moderation
app.plugin[3] = app.plugin.janusxr
app.plugin[4] = app.plugin.xrfragments
app.plugin[5] = app.plugin.manyfold
app.runcmd(app) -- run clicmds
print = function(...) Log(kLogInfo, ... or "''") end -- nice redbean logging from now on
@ -87,69 +88,8 @@ end)
app --
.use( require("middleware/httpauth")() )
.use( function(req,res,next)
local tile = require('page/partial/tile')
local rootpath = "/" .. app.opts.plugin_diskscan_dirname
local urlpath = GetPath()
local isdir = urlpath:sub(-1) == "/"
local nslashes = util.countChar('/',urlpath)
res.items = {}
local match = function(a,b) return util.stripPath(a) == util.stripPath(b) end
if urlpath:match("^"..rootpath) then
local redirect = false
if not app.cache.url[ urlpath ] then
foreach( app.cache.items, function(k,v)
if v.parent then
trace( "path = " .. util.stripPath(v.path) .. " " ..
"parent = " .. util.stripPath(v.parent.path) .. " " ..
"urlpath = " .. util.stripPath(urlpath)
)
if match(v.path, urlpath) then -- show parent folder if any
local parent = app.cache.items[ v.parent.id ]
if v.portal then
redirect = v.portal.url
end
if match(parent.path, app.opts.plugin_diskscan_dir) then -- add rootfolder if needed
local root = app.cache.items[ app.opts.plugin_diskscan_dir ]
res.items[ root.id ] = root
end
end
-- add items from dir
if match(v.parent.path, urlpath) then
local parent = app.cache.items[ v.parent.id ]
res.items[ v.id ] = v
--if parent.parent then
-- res.items[ v.id ] = v
--else
-- res.items[ parent.id ] = parent
--end
end
end
end)
local data = util.merge({items=''},{ opts = app.opts })
foreach( res.items, function(k,v)
data.items = data.items .. tile( util.flatten(v,"item") )
end)
app.cache.url[ urlpath ] = util.template( LoadAsset("page/dir.html"), data)
end
if redirect then
print("REDIRECT! " .. redirect )
res.status(303)
res.header('Location', redirect )
print_r(res)
next()
else
res.status(200)
res.header('content-type','text/html; charset=utf-8')
res.body( app.cache.url[ urlpath ] )
next()
end
else
next()
end
end)
.use( require("json").middleware() ) -- try plug'n'play json API middleware
.use( require('page/dir').middleware ) -- directory browser
.use( app.router( app.url ) ) -- try url router
.use( app.response() ) -- try serve app response (if any)
.use( function(req,next) Route() end) -- fallback default redbean fileserver

535
src/.lua/liluat.lua Normal file
View file

@ -0,0 +1,535 @@
--[[
-- liluat - Lightweight Lua Template engine
--
-- Project page: https://github.com/FSMaxB/liluat
--
-- liluat is based on slt2 by henix, see https://github.com/henix/slt2
--
-- Copyright © 2016 Max Bruckner
-- Copyright © 2011-2016 henix
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is furnished
-- to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
-- IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--]]
local liluat = {
private = {} --used to expose private functions for testing
}
-- print the current version
liluat.version = function ()
return "1.2.0"
end
-- returns a string containing the fist line until the last line
local function string_lines(lines, first, last)
-- allow negative line numbers
first = (first >= 1) and first or 1
local start_position
local current_position = 1
local line_counter = 1
repeat
if line_counter == first then
start_position = current_position
end
current_position = lines:find('\n', current_position + 1, true)
line_counter = line_counter + 1
until (line_counter == (last + 1)) or (not current_position)
return lines:sub(start_position, current_position)
end
liluat.private.string_lines = string_lines
-- escape a string for use in lua patterns
-- (this simply prepends all non alphanumeric characters with '%'
local function escape_pattern(text)
return text:gsub("([^%w])", "%%%1" --[[function (match) return "%"..match end--]])
end
liluat.private.escape_pattern = escape_pattern
-- recursively copy a table
local function clone_table(table)
local clone = {}
for key, value in pairs(table) do
if type(value) == "table" then
clone[key] = clone_table(value)
else
clone[key] = value
end
end
return clone
end
liluat.private.clone_table = clone_table
-- recursively merge two tables, the second one has precedence
-- if 'shallow' is set, the second table isn't copied recursively,
-- its content is only referenced instead
local function merge_tables(a, b, shallow)
a = a or {}
b = b or {}
local merged = clone_table(a)
for key, value in pairs(b) do
if (type(value) == "table") and (not shallow) then
if a[key] then
merged[key] = merge_tables(a[key], value)
else
merged[key] = clone_table(value)
end
else
merged[key] = value
end
end
return merged
end
liluat.private.merge_tables = merge_tables
local default_options = {
start_tag = "{{",
end_tag = "}}",
trim_right = "code",
trim_left = "code"
}
-- initialise table of options (use the provided, default otherwise)
local function initialise_options(options)
return merge_tables(default_options, options)
end
-- creates an iterator that iterates over all chunks in the given template
-- a chunk is either a template delimited by start_tag and end_tag or a normal text
-- the iterator also returns the type of the chunk as second return value
local function all_chunks(template, options)
options = initialise_options(options)
-- pattern to match a template chunk
local template_pattern = escape_pattern(options.start_tag) .. "([+-]?)(.-)([+-]?)" .. escape_pattern(options.end_tag)
local include_pattern = "^"..escape_pattern(options.start_tag) .. "[+-]?include:(.-)[+-]?" .. escape_pattern(options.end_tag)
local expression_pattern = "^"..escape_pattern(options.start_tag) .. "[+-]?=(.-)[+-]?" .. escape_pattern(options.end_tag)
local position = 1
return function ()
if not position then
return nil
end
local template_start, template_end, trim_left, template_capture, trim_right = template:find(template_pattern, position)
local chunk = {}
if template_start == position then -- next chunk is a template chunk
if trim_left == "+" then
chunk.trim_left = false
elseif trim_left == "-" then
chunk.trim_left = true
end
if trim_right == "+" then
chunk.trim_right = false
elseif trim_right == "-" then
chunk.trim_right = true
end
local include_start, include_end, include_capture = template:find(include_pattern, position)
local expression_start, expression_end, expression_capture
if not include_start then
expression_start, expression_end, expression_capture = template:find(expression_pattern, position)
end
if include_start then
chunk.type = "include"
chunk.text = include_capture
elseif expression_start then
chunk.type = "expression"
chunk.text = expression_capture
else
chunk.type = "code"
chunk.text = template_capture
end
position = template_end + 1
return chunk
elseif template_start then -- next chunk is a text chunk
chunk.type = "text"
chunk.text = template:sub(position, template_start - 1)
position = template_start
return chunk
else -- no template chunk found --> either text chunk until end of file or no chunk at all
chunk.text = template:sub(position)
chunk.type = "text"
position = nil
return (#chunk.text > 0) and chunk or nil
end
end
end
liluat.private.all_chunks = all_chunks
local function read_entire_file(path)
assert(path)
local file = assert(io.open(path))
local file_content = file:read('*a')
file:close()
return file_content
end
liluat.private.read_entire_file = read_entire_file
-- a whitelist of allowed functions
local sandbox_whitelist = {
ipairs = ipairs,
next = next,
pairs = pairs,
rawequal = rawequal,
rawget = rawget,
rawset = rawset,
select = select,
tonumber = tonumber,
tostring = tostring,
type = type,
unpack = unpack,
string = string,
table = table,
math = math,
os = {
date = os.date,
difftime = os.difftime,
time = os.time,
},
coroutine = coroutine
}
-- puts line numbers in front of a string and optionally highlights a single line
local function prepend_line_numbers(lines, first, highlight)
first = (first and (first >= 1)) and first or 1
lines = lines:gsub("\n$", "") -- make sure the last line isn't empty
lines = lines:gsub("^\n", "") -- make sure the first line isn't empty
local current_line = first + 1
return string.format("%3d: ", first) .. lines:gsub('\n', function ()
local highlight_char = ' '
if current_line == tonumber(highlight) then
highlight_char = '> '
end
local replacement = string.format("\n%3d:%s", current_line, highlight_char)
current_line = current_line + 1
return replacement
end)
end
liluat.private.prepend_line_numbers = prepend_line_numbers
-- creates a function in a sandbox from a given code,
-- name of the execution context and an environment
-- that will be available inside the sandbox,
-- optionally overwrite the whitelist
local function sandbox(code, name, environment, whitelist, reference)
whitelist = whitelist or sandbox_whitelist
name = name or 'unknown'
-- prepare the environment
environment = merge_tables(whitelist, environment, reference)
local func
local error_message
if setfenv then --Lua 5.1 and compatible
if code:byte(1) == 27 then
error("Lua bytecode not permitted.", 2)
end
func, error_message = loadstring(code)
if func then
setfenv(func, environment)
end
else -- Lua 5.2 and later
func, error_message = load(code, name, 't', environment)
end
-- handle compile error and print pretty error message
if not func then
local line_number, message = error_message:match(":(%d+):(.*)")
-- lines before and after the error
local lines = string_lines(code, line_number - 3, line_number + 3)
error(
'Syntax error in sandboxed code "' .. name .. '" in line ' .. line_number .. ':\n'
.. message .. '\n\n'
.. prepend_line_numbers(lines, line_number - 3, line_number),
3
)
end
return func
end
liluat.private.sandbox = sandbox
local function parse_string_literal(string_literal)
return sandbox('return' .. string_literal, nil, nil, {})()
end
liluat.private.parse_string_literal = parse_string_literal
-- add an include to the include_list and throw an error if
-- an inclusion cycle is detected
local function add_include_and_detect_cycles(include_list, path)
local parent = include_list[0]
while parent do -- while the root hasn't been reached
if parent[path] then
error("Cyclic inclusion detected")
end
parent = parent[0]
end
include_list[path] = {
[0] = include_list
}
end
liluat.private.add_include_and_detect_cycles = add_include_and_detect_cycles
-- extract the name of a directory from a path
local function dirname(path)
return path:match("^(.*/).-$") or ""
end
liluat.private.dirname = dirname
-- splits a template into chunks
-- chunks are either a template delimited by start_tag and end_tag
-- or a text chunk (everything else)
-- @return table
local function parse(template, options, output, include_list, current_path)
options = initialise_options(options)
current_path = current_path or "." -- current include path
include_list = include_list or {} -- a list of files that were included
local output = output or {}
for chunk in all_chunks(template, options) do
-- handle includes
if chunk.type == "include" then -- include chunk
local include_path_literal = chunk.text
local path = parse_string_literal(include_path_literal)
-- build complete path
if path:find("^/") then
--absolute path, don't modify
elseif options.base_path then
path = options.base_path .. "/" .. path
else
path = dirname(current_path) .. path
end
add_include_and_detect_cycles(include_list, path)
local included_template = read_entire_file(path)
parse(included_template, options, output, include_list[path], path)
elseif (chunk.type == "text") and output[#output] and (output[#output].type == "text") then
-- ensure that no two text chunks follow each other
output[#output].text = output[#output].text .. chunk.text
else -- other chunk
table.insert(output, chunk)
end
end
return output
end
liluat.private.parse = parse
-- inline included template files
-- @return string
function liluat.inline(template, options, start_path)
options = initialise_options(options)
local output = {}
for _,chunk in ipairs(parse(template, options, nil, nil, start_path)) do
if chunk.type == "expression" then
table.insert(output, options.start_tag .. "=" .. chunk.text .. options.end_tag)
elseif chunk.type == "code" then
table.insert(output, options.start_tag .. chunk.text .. options.end_tag)
else
table.insert(output, chunk.text)
end
end
return table.concat(output)
end
-- @return { string }
function liluat.get_dependencies(template, options, start_path)
options = initialise_options(options)
local include_list = {}
parse(template, options, nil, include_list, start_path)
local dependencies = {}
local have_seen = {} -- list of includes that were already added
local function recursive_traversal(list)
for key, value in pairs(list) do
if (type(key) == "string") and (not have_seen[key]) then
have_seen[key] = true
table.insert(dependencies, key)
recursive_traversal(value)
end
end
end
recursive_traversal(include_list)
return dependencies
end
-- compile a template into lua code
-- @return { name = string, code = string / function}
function liluat.compile(template, options, template_name, start_path)
options = initialise_options(options)
template_name = template_name or 'liluat.compile'
local output_function = "__liluat_output_function"
-- split the template string into chunks
local lexed_template = parse(template, options, nil, nil, start_path)
-- table of code fragments the template is compiled into
local lua_code = {}
for i, chunk in ipairs(lexed_template) do
-- check if the chunk is a template (either code or expression)
if chunk.type == "expression" then
table.insert(lua_code, output_function..'('..chunk.text..')')
elseif chunk.type == "code" then
table.insert(lua_code, chunk.text)
else --text chunk
-- determine if this block needs to be trimmed right
-- (strip newline)
local trim_right = false
if lexed_template[i - 1] and (lexed_template[i - 1].trim_right == true) then
trim_right = true
elseif lexed_template[i - 1] and (lexed_template[i - 1].trim_right == false) then
trim_right = false
elseif options.trim_right == "all" then
trim_right = true
elseif options.trim_right == "code" then
trim_right = lexed_template[i - 1] and (lexed_template[i - 1].type == "code")
elseif options.trim_right == "expression" then
trim_right = lexed_template[i - 1] and (lexed_template[i - 1].type == "expression")
end
-- determine if this block needs to be trimmed left
-- (strip whitespaces in front)
local trim_left = false
if lexed_template[i + 1] and (lexed_template[i + 1].trim_left == true) then
trim_left = true
elseif lexed_template[i + 1] and (lexed_template[i + 1].trim_left == false) then
trim_left = false
elseif options.trim_left == "all" then
trim_left = true
elseif options.trim_left == "code" then
trim_left = lexed_template[i + 1] and (lexed_template[i + 1].type == "code")
elseif options.trim_left == "expression" then
trim_left = lexed_template[i + 1] and (lexed_template[i + 1].type == "expression")
end
if trim_right and trim_left then
-- both at once
if i == 1 then
if chunk.text:find("^.*\n") then
chunk.text = chunk.text:match("^(.*\n)%s-$")
elseif chunk.text:find("^%s-$") then
chunk.text = ""
end
elseif chunk.text:find("^\n") then --have to trim a newline
if chunk.text:find("^\n.*\n") then --at least two newlines
chunk.text = chunk.text:match("^\n(.*\n)%s-$") or chunk.text:match("^\n(.*)$")
elseif chunk.text:find("^\n%s-$") then
chunk.text = ""
else
chunk.text = chunk.text:gsub("^\n", "")
end
else
chunk.text = chunk.text:match("^(.*\n)%s-$") or chunk.text
end
elseif trim_left then
if i == 1 and chunk.text:find("^%s-$") then
chunk.text = ""
else
chunk.text = chunk.text:match("^(.*\n)%s-$") or chunk.text
end
elseif trim_right then
chunk.text = chunk.text:gsub("^\n", "")
end
if not (chunk.text == "") then
table.insert(lua_code, output_function..'('..string.format("%q", chunk.text)..')')
end
end
end
return {
name = template_name,
code = table.concat(lua_code, '\n')
}
end
-- compile a file
-- @return { name = string, code = string / function }
function liluat.compile_file(filename, options)
return liluat.compile(read_entire_file(filename), options, filename, filename)
end
-- @return a coroutine function
function liluat.render_coroutine(template, environment, options)
options = initialise_options(options)
environment = merge_tables({__liluat_output_function = coroutine.yield}, environment, options.reference)
return sandbox(template.code, template.name, environment, nil, options.reference)
end
-- @return string
function liluat.render(t, env, options)
options = initialise_options(options)
local result = {}
-- add closure that renders the text into the result table
env = merge_tables({
__liluat_output_function = function (text)
table.insert(result, text) end
},
env,
options.reference
)
-- compile and run the lua code
local render_function = sandbox(t.code, t.name, env, nil, options.reference)
local status, error_message = pcall(render_function)
if not status then
local line_number, message = error_message:match(":(%d+):(.*)")
message = message or ''
line_number = line_number or 0
-- lines before and after the error
local lines = string_lines(t.code, line_number - 3, line_number + 3)
error(
'Runtime error in sandboxed code "' .. t.name .. '" in line ' .. line_number .. ':\n'
.. message .. '\n\n'
.. prepend_line_numbers(lines, line_number - 3, line_number),
2
)
end
return table.concat(result)
end
return liluat

View file

@ -22,8 +22,9 @@ sb = {
if type(v) == "function" then
sb.data[k] = (function(k,v)
return function(a,b,c,d,e,f)
v(a,b,c,d,e,f)
local ret = v(a,b,c,d,e,f)
sb.pub(k,v,a,b,c,d,e,f)
return ret
end
end)(k,v)
else
@ -52,9 +53,64 @@ sb = {
-- print( util.tpl("${name} is ${value}", {name = "foo", value = "bar"}) )
-- "foo is bar"
tpl = function(s,tab)
if not s then return "" end
return (s:gsub('($%b{})', function(w) return tab[w:sub(3, -2)] or w end))
-- print( util.tpl("${foo(1)}", { foo = function(i) return i+1 end}) )
-- 2
-- print( util.tpl("${if foo then} bar ${end}", { foo = true }) )
-- bar
tpl = function(s, tab)
tab = tab or sb.data
return (s:gsub('($%b{})', function(w)
local inner = w:sub(3, -2):match("^%s*(.-)%s*$")
-- 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
local fn = (loadstring or load)("return function(tab) " .. inner .. " end")
if fn then
setfenv(fn(), setmetatable(tab, { __index = _G }))()
end
return ""
end
local k, arg = w:match("^%${%s*([%w_]+)%s*%(?(.-)%)?%s*}")
local v = tab[k]
if type(v) == "function" then
return tostring(v(load("return " .. arg)()) or '')
end
return v ~= nil and tostring(v) or w
end))
end,
tpl = function(s, tab)
tab = tab or sb.data
return (s:gsub('($%b{})', function(w)
local inner = w:sub(3, -2):match("^%s*(.-)%s*$")
-- Execute as raw Lua code if it contains spaces or equals "end"
if inner:find("%s") or inner == "end" then
local fn = (loadstring or load)("return function(tab) " .. inner .. " end")
if fn then
setfenv(fn(), setmetatable(tab, { __index = _G }))()
end
return ""
end
-- Allow dots in variable names along with alphanumeric characters and underscores
local path, arg = w:match("^%${%s*([%w_%.]+)" .. "%s*%(?(.-)%)?%s*}")
-- Traverse the nested table path (e.g. "foo.bar" -> tab["foo"]["bar"])
local v = tab
if path then
for key in path:gmatch("[%w_]+") do
if type(v) == "table" then
v = v[key]
else
v = nil
break
end
end
else
v = nil
end
if type(v) == "function" then
return tostring(v(load("return " .. arg)()) or '')
end
return v ~= nil and tostring(v) or w
end))
end,
write = function(str)

130
src/.lua/template.lua Normal file
View file

@ -0,0 +1,130 @@
-- local compile = require "tmpl"
--
-- local s = [[
-- <!DOCTYPE html>
-- <html>
-- <head>
-- {! this is a comment !}
-- <meta charset="utf-8">
-- <title>{{page.title}}</title>
-- <style>
-- body {
-- background-color: #{{page.bg}};
-- }
-- </style>
-- </head>
-- <body>
-- <h3>{{escaped}}</h3>
-- <h3>{* unescaped *}
-- <h3>{% echo 'haHAA' %}</h3>
-- {[template2]}
-- </body>
-- </html>
-- ]]
--
-- local s2 = [[
-- <ul>
-- {% for i = 1, 3 do %}
-- <li>{{i}}{{unit}}</li>
-- {% end %}
-- </ul>
-- {% if haHAA then %}
-- {[template2, sub]}
-- {% end %}
-- ]]
--
-- local template = compile(s)
-- local ctx = {
-- page = { title = 'Demo', bg = '332233' },
-- escaped = 'escaped: < &',
-- unescaped = 'unescaped: &bull;</h3>',
-- template2 = compile(s2),
-- haHAA = true,
-- unit = 'g',
-- sub = {
-- -- custom escape function
-- escape = function(s)
-- i = tonumber(s)
-- if i then return i * 3 else return s end
-- end,
-- haHAA = false,
-- unit = 'kg'
-- }
-- }
--
-- for s in template(ctx) do
-- io.write(s)
-- end
return function(str)
local t = {[[
local ctx = ...
local _out = {}
local _ENV = setmetatable(ctx, { __index = _ENV })
local function echo(s)
if s and s ~= '' then
table.insert(_out, s)
end
end
local function include(template, sub_ctx)
if sub_ctx then
sub_ctx.escape = sub_ctx.escape or _ENV.escape
else
sub_ctx = _ENV
end
table.insert(_out, template(sub_ctx))
end
_ENV.escape = escape or function(i)
return tostring(i or ''):gsub('[&<>"\'/]', {
['&'] = '&amp;',
['<'] = '&lt;',
['>'] = '&gt;',
['"'] = '&quot;',
["'"] = '&#39;',
['/'] = '&#47;'
})
end
]]}
local function f(pos)
local start, stop, c = pos - 1
repeat
start, stop = str:find('%b{}', start + 1)
if not start then
table.insert(t, ' echo([=====[\n')
table.insert(t, str:sub(pos, #str))
table.insert(t, ']=====]) ')
return
end
c = str:sub(start + 1, start + 1)
until c:match('[{%*%[%%!]')
table.insert(t, ' echo([=====[\n')
table.insert(t, str:sub(pos, start - 1))
table.insert(t, ']=====]) ')
if c == '{' then
table.insert(t, ' echo(escape(')
table.insert(t, str:sub(start + 2, stop - 2))
table.insert(t, ')) ')
elseif c == '*' then
table.insert(t, ' echo(')
table.insert(t, str:sub(start + 2, stop - 2))
table.insert(t, ') ')
elseif c == '[' then
table.insert(t, ' include(')
table.insert(t, str:sub(start + 2, stop - 2))
table.insert(t, ') ')
elseif c == '%' then
table.insert(t, str:sub(start + 2, stop - 2))
end
f(stop + 1)
end
f(1)
-- Return the concatenated string directly from the compiled function
table.insert(t, [[ return table.concat(_out) ]])
return load(table.concat(t))
end

View file

@ -59,7 +59,7 @@ util.flatten = function(tbl, prefix, result)
return result
end
util.pagerender = function(file,data)
util.pagerender = function(file,data,serve)
return function(req,res,next)
data = data or {opts = app.opts}
data.page = file:gsub(".*","")
@ -70,12 +70,26 @@ util.pagerender = function(file,data)
end
end
util.templateFile = function(file,data)
trace('templateFile(' .. file .. ')')
return util.template( LoadAsset(file), data)
end
util.getURL = function()
local port = ':' .. GetPort()
if port == 80 then port = '' end
return GetScheme() .. '://' .. GetHost() .. port .. GetPath()
end
util.template = function(content,data)
local mdata = util.merge(data or app,{})
mdata = util.merge(mdata, util.flatten(app.opts,"opts") )
mdata = util.merge(mdata, {opts = app.opts})
mdata.header_include = mdata.header_include or ''
mdata.header = app.tpl( LoadAsset('page/header.html'), mdata )
mdata.footer = app.tpl( LoadAsset('page/footer.html'), mdata )
-- set view url
mdata.url = mdata.url or util.getURL()
print_r(mdata)
return app.tpl( content, mdata )
end
@ -240,6 +254,28 @@ function util.split(inputstr, sep)
return t
end
function util.pluck(path, tbl, default)
default = default or ''
if type(tbl) ~= "table" or type(path) ~= "string" then
return default
end
local current = tbl
-- Match non-dot segments in the path key string
for key in string.gmatch(path, "[^.]+") do
if type(current) ~= "table" then
return default
end
current = current[key]
if current == nil then
return default
end
end
return current
end
function util.stripPath(str)
return str:gsub("^./",""):gsub("^/",""):gsub("/$","")
end

View file

@ -1,3 +1,6 @@
return function(app,argv)
print("starting server")
if arg[1] == 'admin' then
LaunchBrowser('/admin')
end
end

View file

@ -201,14 +201,14 @@ ul{
img.icon{ display:none; }
/* tab */
/* vertical tab */
.tab{
.vtab{
border-bottom: 1px solid #224;
margin-bottom: 25px;
margin-left: 140px;
}
.tab .label{
.vtab .label{
display: block;
background: #000;
margin-bottom: -2px;
@ -239,6 +239,11 @@ img.icon{ display:none; }
filter: drop-shadow(1px 1px 5px #0FF9) drop-shadow(-1px -1px 5px #F079);
}
.info{
background:#224;
padding:10px;
}
b.tag{
display:inline-block;
background: #77f;

View file

@ -171,4 +171,70 @@ u span{
width: 100%;
}
/* css tabs */
.vtab { display:none } /* hide (fallback) vertical tabs */
div.tab-frame > input{ display:none;}
div.tab-frame > label{
display:inline-block;
padding:5px 10px;
cursor:pointer;
background:#112;
border-bottom:1px solid #447;
padding-right:10px;
color:#AAD;
margin-bottom:20px;
}
div.tab-frame > label b.tag{
color:#888;
background:#000;
}
div.tab-frame > input:checked + label{
border-bottom:4px solid #A8F;
cursor:default;
background:#224;
color:#FFF;
font-weight:bold;
}
div.tab-frame div.tab{
display:none;
clear:left
}
div.tab-frame > input:nth-of-type(1):checked ~ .tab:nth-of-type(1),
div.tab-frame > input:nth-of-type(2):checked ~ .tab:nth-of-type(2),
div.tab-frame > input:nth-of-type(3):checked ~ .tab:nth-of-type(3),
div.tab-frame > input:nth-of-type(4):checked ~ .tab:nth-of-type(4),
div.tab-frame > input:nth-of-type(5):checked ~ .tab:nth-of-type(5),
div.tab-frame > input:nth-of-type(6):checked ~ .tab:nth-of-type(6),
div.tab-frame > input:nth-of-type(7):checked ~ .tab:nth-of-type(7),
div.tab-frame > input:nth-of-type(8):checked ~ .tab:nth-of-type(8),
div.tab-frame > input:nth-of-type(9):checked ~ .tab:nth-of-type(9),
div.tab-frame > input:nth-of-type(10):checked ~ .tab:nth-of-type(10),
div.tab-frame > input:nth-of-type(11):checked ~ .tab:nth-of-type(11),
div.tab-frame > input:nth-of-type(12):checked ~ .tab:nth-of-type(12),
div.tab-frame > input:nth-of-type(13):checked ~ .tab:nth-of-type(13),
div.tab-frame > input:nth-of-type(14):checked ~ .tab:nth-of-type(14),
div.tab-frame > input:nth-of-type(15):checked ~ .tab:nth-of-type(15),
div.tab-frame > input:nth-of-type(16):checked ~ .tab:nth-of-type(16),
div.tab-frame > input:nth-of-type(17):checked ~ .tab:nth-of-type(17){ display:block;
}
div.tab-frame > div.info{
display:none;
}
div.tab-frame label.info{
border:none;
padding:0;
}
div.tab-frame > input:checked + label.info{
border:none;
}
div.tab-frame > input#info:checked ~ .info{
display:block;
cursor:pointer;
}

1
src/media Symbolic link
View file

@ -0,0 +1 @@
janusweb/media

View file

@ -61,7 +61,7 @@ div#xrecosystem{
<div id="logo"></div>
<h2>Turn files into AR/VR 🥽 experiences</h2>
<center>
A static website generator, XR hub, server &amp; client.
A static website generator for interconnected XR experiences, server &amp; client.
<br/>
<br/>
<div class="btn">
@ -91,9 +91,11 @@ div#xrecosystem{
<h3>Why people want XRForge</h3>
<div>
In short: its hyperscalable portal-system and ability <b>projecting existing 2D ecosystems</b> into XR.<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>
They're just more cost-efficient to use.<br>
Hence, XRForge promotes <b>projecting these ecosystems</b> as virtual <b>hyperlinked</b> worlds.<br>
Hence, XRForge incorporates the opensource <a href="https://coderofsalvation.github.io/janus-guide/" target="_blank">JanusXR</a>-client called <a href="https://github.com/jbaicoianu/janusweb" target="_blank">JanusWeb</a>, which promotes <b>projecting these ecosystems</b> as virtual <b>hyperlinked</b> worlds (via XR translators).<br>
XRForge can seed itself via local or remote
<u tabindex="0">datastores
<span>

View file

@ -9,12 +9,20 @@ ${header}
${notification}
<form method="POST">
<div class="tab-frame">
${tabs}
${for i,tab in pairs(tabs) do}
<input type="radio" checked name="tab" id="tab${i}">
<label for="tab${i}">${tab}</label>
${end}
${plugins}
<div style="margin-left:187px">
<div class="btn">
<input type="submit" class="btn secondary" value="Save" style=""/>
${plugins}
<div style="margin-left:187px">
<div class="btn">
<input type="submit" class="btn secondary" value="Save" style=""/>
</div>
</div>
</div>
</form>

View file

@ -2,6 +2,7 @@ local util = require("util")
local data = util.merge({notification=''},{ opts = app.opts })
data.plugins = ""
data.tabs = {}
function pluginInput(opt)
local html = ""
@ -46,13 +47,14 @@ end
function pluginUI(seen)
return function(name,plugin)
if seen[ plugin.name ] then return end -- already processed
table.insert( data.tabs, plugin.name ) -- insert tabname
seen[ plugin.name ] = true
local plugindata = util.flatten(plugin,"plugin")
local plugindata = {plugin=plugin}
plugindata.opts = ""
foreach(plugin.opts, function(k,opt)
plugindata.opts = plugindata.opts .. pluginInput(opt)
end)
data.plugins = data.plugins .. util.template( LoadAsset('/page/admin/plugin.html'), plugindata)
data.plugins = data.plugins .. app.tpl( LoadAsset('/page/admin/plugin.html'), plugindata)
end
end

View file

@ -1,7 +1,9 @@
<div class="tab">
<!--
<div class="vtab">
<div class="label col2">${plugin.name}</div>
</div>
<div class="margin1">
-->
<div class="tab margin1">
<table width="100%">
<tr>
<th width="165"/>
@ -11,11 +13,17 @@
<tr>
<td></td>
<td></td>
<td class="col3">${plugin.info}<br/><br/></td>
<td class="">
$(if #_.plugin.info > 2 then)
<div class="col3 info">
${plugin.info}
</div>
$(end)
<br/><br/>
</td>
</tr>
${opts}
</table>
<br/>
<br/>
</div>
<br/>
<br/>

View file

@ -1,13 +1,35 @@
${header}
${if _.parent ~= nil and _.parent.parent == nil then}
<div style="height:120px;" class="jumbotron">
<h1>${opts.title}</h1>
${opts.subtitle}
</div>
<hr style="margin-top:0">
${end}
${if _.parent ~= nil and _.parent.parent ~= nil then}
<h2>${parent.name}</h2>
${end}
<div class="grid">
${items}
${html_items}
</div>
${footer}
<!-- JanusXR portalroom, see https://janusxr.org for markup -->
<fireboxroom>
<room pos="0 0 0" 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" />
<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" />
<CircularLayout pos="0 0 0" radius="4.5" scale="1 1 1" >
${for k,v in pairs(items) do}
<object id="o_${i}">
<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}"/>
<text text="${v.title}" pos="0 2.3 0.3" font_scale="false" scale="0.15 0.15 0.2" />
</object>
${end}
</CircularLayout>
</room>
</fireboxroom>

View file

@ -1,35 +1,92 @@
local util = require("util")
local tile = require('page/partial/tile')
--if not app.cache['experiences.html'] and util.count(app.cache.items) > 0 then
-- print("processing! " .. util.count(app.cache.items) .. " items")
-- app.cache['experiences.html'] = util.memoize(
-- function()
-- local data = util.merge({tiles=''},{ opts = app.opts })
-- foreach(app.cache.items, function(k,v)
-- data.tiles = data.tiles .. tile( util.flatten(v,"item") )
-- end)
-- local html = util.template( LoadAsset("page/experiences.html"),data)
-- return html
-- end,
-- 60*5 -- re-run every 5 mins (otherwise return cache)
-- )
--end
-- todo: clean up code once the code below is no longer in flux
return {
middleware = function(req,res,next)
local tile = require('page/partial/tile')
local rootpath = "/" .. app.opts.plugin_diskscan_dirname
local urlpath = GetPath()
local isdir = urlpath:sub(-1) == "/"
local nslashes = util.countChar('/',urlpath)
res.items = {}
local match = function(a,b) return util.stripPath(a) == util.stripPath(b) end
if urlpath:match("^"..rootpath) and not urlpath:gsub(".*/",""):match(".") then
local redirect = false
if not app.cache.url[ urlpath ] then
foreach( app.cache.items, function(k,v)
if v.parent then
trace( "path = " .. util.stripPath(v.path) .. " " ..
"parent = " .. util.stripPath(v.parent.path) .. " " ..
"urlpath = " .. util.stripPath(urlpath)
)
if match(v.path, urlpath) then -- show parent folder if any
local parent = app.cache.items[ v.parent.id ]
if v.portal then
redirect = v.portal.url
end
if match(parent.path, app.opts.plugin_diskscan_dir) then -- add rootfolder if needed
local root = app.cache.items[ app.opts.plugin_diskscan_dir ]
--res.items[ root.id ] = root
end
end
if util.count(app.cache.items) == 0 then
SetStatus(200)
SetHeader('Content-Type', 'text/html; charset=utf-8')
local html = util.template( [[
${header}
<br/>
<h2>Indexing files: please try again in few seconds</h2>
${footer}
]],data)
Write( html )
else
SetStatus(200)
SetHeader('Content-Type', 'text/html; charset=utf-8')
--Write( app.cache['experiences.html']() )
end
-- add items from dir
if match(v.parent.path, urlpath) then
local parent = app.cache.items[ v.parent.id ]
res.items[ v.id ] = v
res.parent = parent
if v.thumbnail then
print_r(v.name .. " => " .. v.thumbnail)
end
--if parent.parent then
-- res.items[ v.id ] = v
--else
-- res.items[ parent.id ] = parent
--end
end
end
end)
local data = util.merge({
html_items='',
items = {},
parent=res.parent
},{ opts = app.opts })
data.tile = tile
local i = 0
foreach( res.items, function(k,v)
-- position items for janusxr JML (dir.html)
table.insert(data.items, util.merge({x = i*2, i = i},v) )
i=i+1
if v.view3D then
data.view3D = v
local port = ':' .. GetPort()
if port == 80 then port = '' end
data.url = GetScheme() .. '://' .. GetHost() .. port .. v.url
else
data.html_items = data.html_items .. tile({item=v})
end
end)
if data.view3D then
app.cache.url[ urlpath ] = util.templateFile( "page/index.html", data)
else
app.cache.url[ urlpath ] = util.templateFile( "page/dir.html", data)
end
end
if redirect then
print("REDIRECT! " .. redirect )
res.status(303)
res.header('Location', redirect )
next()
else
res.status(200)
res.header('content-type','text/html; charset=utf-8')
res.body( app.cache.url[ urlpath ] )
next()
end
else
next()
end
end
}

View file

@ -1,5 +1,5 @@
${header}
<iframe border="0" frameborder="0" src="/v/index.html#janus.url=/" id="viewer"/>
<iframe border="0" frameborder="0" src="/janusweb/index.html#janus.url=${url}" id="viewer"/>
${footer}

View file

@ -4,7 +4,8 @@ if not app.cache['index.html'] then
app.cache['index.html'] = util.memoize(
function()
print("processing!")
local html = util.template( LoadAsset("page/index.html"),app)
local data = { url = util.getURL() .. app.opts.plugin_diskscan_dirname }
local html = util.template( LoadAsset("page/index.html"),data)
return html
end,
60*5 -- re-run every 5 mins (otherwise return cache)

View file

@ -3,10 +3,12 @@ local util = require('util')
return function(opts)
return util.template([[
<div class="tile">
<div class="img" style="background-image:url(https://xrforge.isvery.ninja/models/dpjnj8pt1cjc/model_files/bzjvdm7l523l.png?derivative=preview);"></div>
<div class="text">
<img src="img/folder.svg" class="icon"/>&nbsp;${item.title}
</div>
<a href="${item.url}">
<div class="img" style="background-image: url(${item.thumbnail});"></div>
<div class="text">
<img src="img/folder.svg" class="icon"/>&nbsp;${item.title}
</div>
</a>
</div>
]],opts)
end

View file

@ -8,7 +8,7 @@ end)
-- this registers the plugin UI /funcs
return {
name = "Content (from disk)",
name = "Local Content",
info = '',
opts = {
{
@ -75,12 +75,23 @@ return {
name = name,
title = name,
sidecarfile = {},
thumbnail = '',
tag = {},
id = ino,
file = {stat=stat,state=state,kind=kind,ino=ino,off=off,attr=attr,fullpath=fullpath,name=name,path=path}
}
experience.parent = parent
-- check for thumbnail
local thumbExt = {"png","jpg","webp"}
foreach( thumbExt, function(k,ext)
local thumbnailFile = fullpath .. "/" .. name .. "." .. ext
if util.fileExist( thumbnailFile ) then
print("✅ └─ plugin/diskscan: detected .".. ext:lower() .. " thumbnail file")
experience.thumbnail = "/" .. util.stripPath(thumbnailFile)
end
end)
if seen[ino] then return end -- dont process dirs/files twice
if (kind == unix.DT_LNK) then

View file

@ -20,6 +20,7 @@ app.on('scan_experience_file', function(exp)
end
app.pub("scan_experience_janusxr", nil, {exp=exp, item=item}) -- be nice to other plugins
app.cache.items[ exp.file.ino ] = exp
exp.view3D = true
print("✅ └─ plugin/janusxr: " .. exp.file.name)
end
else
@ -44,6 +45,13 @@ local plugin = {
default="/janusxr",
values=''
},
{
key='app.opts.plugin_janusxr_janusweb_corsproxies',
title='CORS proxies',
info='Proxies to surf the immersive deep web of JanusXR',
default="https://p.janusxr.org https://cors.xrforge.org",
values=''
},
{
key='app.opts.plugin_janusxr_wrap_image',
title='wrap images',
@ -72,6 +80,20 @@ local plugin = {
default="random",
values={random="random room",plane="wirefloor",room1="room1",room2="room2",room3="room3",room4="room4",room5="room5",room6="room6",room7="room7"}
},
{
key='app.opts.plugin_janusxr_scraper',
title='Scraper',
info='List experiences by periodically scraping JanusXR URLs for <a href="https://coderofsalvation.github.io/janus-guide/#/build/room?id=link" target="_blank">&lt;link&gt;</a> portals',
default="disabled",
values={enabled="enabled",disabled="disabled"}
},
{
key='app.opts.plugin_janusxr_scraper',
title='Scraper URLs',
info='Space-separated list of URLs to scrape',
default='https://xrforge.isvery.ninja https://www.janusxr.org',
values=''
},
}
}

View file

@ -15,11 +15,16 @@ app.on('scan_experience_file', function(exp)
-- check sidecarfiles: https://xrfragment.org/#sidecar%20files
local sidecarExts = {"png","vtt","ogg","json"}
foreach( sidecarExts, function(k,sidecarExt)
if util.fileExist( exp.path:gsub("."..ext.."$", "."..sidecarExt) ) then
exp.sidecarfile[ sidecarExt ] = true
local sfile = exp.path:gsub("."..ext.."$", "."..sidecarExt)
if util.fileExist( sfile ) then
exp.sidecarfile[ sidecarExt ] = "/" .. util.stripPath(sfile)
exp.view3D = true
end
end)
-- set thumbnail if any
if exp.sidecarfile.png then
exp.thumnbail = exp.sidecarfile.png
end
local jsonfile = exp.path:gsub("."..ext.."$",".json")
-- scan for hrefs in json sidecarfile
if util.fileExist( jsonfile ) then
@ -43,7 +48,7 @@ end)
-- this registers the plugin UI /funcs
return {
name = "XR Fragments",
info = '<a href="https://xrfragment.org" target="_blank">deeplinking for 3D files</a>',
info = 'XR Fragments treats 3D files with <a href="https://xrfragment.org/#sidecar%20files" target="_blank">sidecar file(s)</a> (foo.glb+foo.png e.g.) as experience and/or detect <a href="https://xrfragment.org" target="_blank">deeplinking of 3D files</a> via <a href="https://xrfragment.org#href" target="_blank">href-metadata</a>',
opts = {
{
key='app.opts.plugin_xrfragments_filetypes',

View file

@ -20,11 +20,13 @@ download(){
}
janusweb(){
set -x
version=1.7.5
test -f janusweb-${version}.tar.gz || {
wget "https://github.com/coderofsalvation/janusweb/releases/download/v1.7.5-rc/janusweb-1.7.5.tar.gz"
tar -xvf janusweb-${version}.tar.gz
mv janusweb-${version} src/v
wget "https://github.com/coderofsalvation/janusweb/releases/download/v${version}-rc/janusweb-${version}.tar.gz"
}
test -d janusweb-${version} || tar -xvf janusweb-${version}.tar.gz
test -d src/janusweb || cp -r janusweb-${version} src/janusweb
}
"$@"
@ -39,7 +41,7 @@ build(){
rebuild(){
cd src
zip -yr ../result/bin/xrforge.com .lua page plugin cmd css .init.lua .args test myverse
zip -yr ../result/bin/xrforge.com .lua img page plugin cmd css .init.lua .args test myverse
}
"$@"