diff --git a/.gitignore b/.gitignore index 42e3f2e..8e44a59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ experienceA experienceB result -src/v +src/janusweb janusweb* myverse diff --git a/src/.init.lua b/src/.init.lua index f6e79f2..b6424b5 100644 --- a/src/.init.lua +++ b/src/.init.lua @@ -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 diff --git a/src/.lua/liluat.lua b/src/.lua/liluat.lua new file mode 100644 index 0000000..83c72ff --- /dev/null +++ b/src/.lua/liluat.lua @@ -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 + diff --git a/src/.lua/soakbean.lua b/src/.lua/soakbean.lua index 06ef10d..05d4074 100644 --- a/src/.lua/soakbean.lua +++ b/src/.lua/soakbean.lua @@ -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) diff --git a/src/.lua/template.lua b/src/.lua/template.lua new file mode 100644 index 0000000..0a0496e --- /dev/null +++ b/src/.lua/template.lua @@ -0,0 +1,130 @@ +-- local compile = require "tmpl" +-- +-- local s = [[ +-- +-- +-- +-- {! this is a comment !} +-- +-- {{page.title}} +-- +-- +-- +--

{{escaped}}

+--

{* unescaped *} +--

{% echo 'haHAA' %}

+-- {[template2]} +-- +-- +-- ]] +-- +-- local s2 = [[ +-- +-- {% if haHAA then %} +-- {[template2, sub]} +-- {% end %} +-- ]] +-- +-- local template = compile(s) +-- local ctx = { +-- page = { title = 'Demo', bg = '332233' }, +-- escaped = 'escaped: < &', +-- unescaped = 'unescaped: •', +-- 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('[&<>"\'/]', { + ['&'] = '&', + ['<'] = '<', + ['>'] = '>', + ['"'] = '"', + ["'"] = ''', + ['/'] = '/' + }) +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 diff --git a/src/.lua/util.lua b/src/.lua/util.lua index 4cd661a..f523540 100644 --- a/src/.lua/util.lua +++ b/src/.lua/util.lua @@ -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 diff --git a/src/cmd/server.lua b/src/cmd/server.lua index 0c9602d..1db732c 100644 --- a/src/cmd/server.lua +++ b/src/cmd/server.lua @@ -1,3 +1,6 @@ return function(app,argv) print("starting server") + if arg[1] == 'admin' then + LaunchBrowser('/admin') + end end diff --git a/src/css/xrforge.css b/src/css/xrforge.css index 6f76da8..20ce660 100644 --- a/src/css/xrforge.css +++ b/src/css/xrforge.css @@ -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; diff --git a/src/css/xrforge.max.css b/src/css/xrforge.max.css index 264c348..1fa424b 100644 --- a/src/css/xrforge.max.css +++ b/src/css/xrforge.max.css @@ -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; +} + diff --git a/src/media b/src/media new file mode 120000 index 0000000..46e3b51 --- /dev/null +++ b/src/media @@ -0,0 +1 @@ +janusweb/media \ No newline at end of file diff --git a/src/page/about.html b/src/page/about.html index 757bbe6..89f6104 100644 --- a/src/page/about.html +++ b/src/page/about.html @@ -61,7 +61,7 @@ div#xrecosystem{

Turn files into AR/VR 🥽 experiences

- A static website generator, XR hub, server & client. + A static website generator for interconnected XR experiences, server & client.

@@ -91,9 +91,11 @@ div#xrecosystem{

Why people want XRForge

+ In short: its hyperscalable portal-system and ability projecting existing 2D ecosystems into XR.
+ XRForge interconnects experiences via portals, according to the JanusXR and XR Fragments standard.
The metaverse has shown: people like 3D but certain existing 2D ecosystems are king.
They're just more cost-efficient to use.
- Hence, XRForge promotes projecting these ecosystems as virtual hyperlinked worlds.
+ Hence, XRForge incorporates the opensource JanusXR-client called JanusWeb, which promotes projecting these ecosystems as virtual hyperlinked worlds (via XR translators).
XRForge can seed itself via local or remote datastores diff --git a/src/page/admin/index.html b/src/page/admin/index.html index cb87fbd..23663b7 100644 --- a/src/page/admin/index.html +++ b/src/page/admin/index.html @@ -9,12 +9,20 @@ ${header} ${notification}
+
+ ${tabs} + ${for i,tab in pairs(tabs) do} + + + ${end} - ${plugins} -
-
- + ${plugins} + +
+
+ +
diff --git a/src/page/admin/index.lua b/src/page/admin/index.lua index 6c15696..9781d51 100644 --- a/src/page/admin/index.lua +++ b/src/page/admin/index.lua @@ -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 diff --git a/src/page/admin/plugin.html b/src/page/admin/plugin.html index f01ea28..6d9bb17 100644 --- a/src/page/admin/plugin.html +++ b/src/page/admin/plugin.html @@ -1,7 +1,9 @@ -
+ +
- + ${opts}
@@ -11,11 +13,17 @@
${plugin.info}

+ $(if #_.plugin.info > 2 then) +
+ ${plugin.info} +
+ $(end) +

+
+
+
- -
-
diff --git a/src/page/dir.html b/src/page/dir.html index 4f84333..2deae47 100644 --- a/src/page/dir.html +++ b/src/page/dir.html @@ -1,13 +1,35 @@ ${header} +${if _.parent ~= nil and _.parent.parent == nil then}

${opts.title}

${opts.subtitle}

+${end} + +${if _.parent ~= nil and _.parent.parent ~= nil then} +

${parent.name}

+${end}
- ${items} + ${html_items}
${footer} + + + + + + + + ${for k,v in pairs(items) do} + + + + + ${end} + + + diff --git a/src/page/dir.lua b/src/page/dir.lua index 16f4b66..0c7aa3e 100644 --- a/src/page/dir.lua +++ b/src/page/dir.lua @@ -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} -
-

Indexing files: please try again in few seconds

- ${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 +} diff --git a/src/page/index.html b/src/page/index.html index c702d94..ca87171 100644 --- a/src/page/index.html +++ b/src/page/index.html @@ -1,5 +1,5 @@ ${header} -