From 64e036c63229a3eaec89c58b5169ceeb01b1904d Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Thu, 30 Jul 2026 20:53:12 +0200 Subject: [PATCH] better templating --- README.md | 8 + src/.lua/markdown.lua | 1212 +++++++++++++++++++++++ src/.lua/soakbean.lua | 96 +- src/.lua/util.lua | 2 +- src/css/xrforge.css | 13 +- src/css/xrforge.max.css | 4 +- src/page/about.html | 2 +- src/page/admin/index.html | 10 +- src/page/admin/plugin.html | 12 +- src/page/dir.html | 51 +- src/page/dir.lua | 7 +- src/page/header.html | 3 +- src/page/index.html | 19 +- src/page/item.html | 30 + src/plugin/markdown.lua | 10 +- src/shader/xrforgeportal.shadertoy.frag | 41 + util/xrforge.sh | 2 +- 17 files changed, 1446 insertions(+), 76 deletions(-) create mode 100644 src/.lua/markdown.lua create mode 100644 src/page/item.html create mode 100644 src/shader/xrforgeportal.shadertoy.frag diff --git a/README.md b/README.md index eaeb40a..c6f4266 100644 --- a/README.md +++ b/README.md @@ -62,3 +62,11 @@ $ DEBUG=1 result/bin/xrforge.com * 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. + +## 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) diff --git a/src/.lua/markdown.lua b/src/.lua/markdown.lua new file mode 100644 index 0000000..636e8f9 --- /dev/null +++ b/src/.lua/markdown.lua @@ -0,0 +1,1212 @@ +#!/usr/bin/env lua + +---------------------------------------------------------------------- +-- Utility functions +---------------------------------------------------------------------- + +local unpack = table.unpack or unpack + +-- Returns the result of mapping the values in table t through the function f +local function map(t, f) + local out = {} + for k,v in pairs(t) do out[k] = f(v,k) end + return out +end + +-- Functional style if statement. (NOTE: no short circuit evaluation) +local function iff(t, a, b) if t then return a else return b end end + +-- Splits the text into an array of separate lines. +local function split(text, sep) + sep = sep or "\n" + local lines = {} + local pos = 1 + while true do + local b,e = text:find(sep, pos) + if not b then table.insert(lines, text:sub(pos)) break end + table.insert(lines, text:sub(pos, b-1)) + pos = e + 1 + end + return lines +end + +-- Converts tabs to spaces +local function detab(text) + local tab_width = 4 + local function rep(match) + local spaces = -match:len() + while spaces<1 do spaces = spaces + tab_width end + return match .. string.rep(" ", spaces) + end + text = text:gsub("([^\n]-)\t", rep) + return text +end + +-- Applies string.find for every pattern in the list and returns the first match +local function find_first(s, patterns, index) + local res = {} + for _,p in ipairs(patterns) do + local match = {s:find(p, index)} + if #match>0 and (#res==0 or match[1] < res[1]) then res = match end + end + return unpack(res) +end + +-- If a replacement array is specified, the range [start, stop] in the array is replaced +-- with the replacement array and the resulting array is returned. Without a replacement +-- array the section of the array between start and stop is returned. +local function splice(array, start, stop, replacement) + if replacement then + local n = stop - start + 1 + while n > 0 do + table.remove(array, start) + n = n - 1 + end + for _,v in ipairs(replacement) do + table.insert(array, start, v) + end + return array + else + local res = {} + for i = start,stop do + table.insert(res, array[i]) + end + return res + end +end + +-- Outdents the text one step. +local function outdent(text) + text = "\n" .. text + text = text:gsub("\n ? ? ?", "\n") + text = text:sub(2) + return text +end + +-- Indents the text one step. +local function indent(text) + text = text:gsub("\n", "\n ") + return text +end + +-- Does a simple tokenization of html data. Returns the data as a list of tokens. +-- Each token is a table with a type field (which is either "tag" or "text") and +-- a text field (which contains the original token data). +local function tokenize_html(html) + local tokens = {} + local pos = 1 + while true do + local start = find_first(html, {"", start) + elseif html:match("^<%?", start) then + _,stop = html:find("?>", start) + else + _,stop = html:find("%b<>", start) + end + if not stop then + -- error("Could not match html tag " .. html:sub(start,start+30)) + table.insert(tokens, {type="text", text=html:sub(start, start)}) + pos = start + 1 + else + table.insert(tokens, {type="tag", text=html:sub(start, stop)}) + pos = stop + 1 + end + end + return tokens +end + +---------------------------------------------------------------------- +-- Hash +---------------------------------------------------------------------- + +-- This is used to "hash" data into alphanumeric strings that are unique +-- in the document. (Note that this is not cryptographic hash, the hash +-- function is not one-way.) The hash procedure is used to protect parts +-- of the document from further processing. + +local HASH = { + -- Has the hash been inited. + inited = false, + + -- The unique string prepended to all hash values. This is to ensure + -- that hash values do not accidently coincide with an actual existing + -- string in the document. + identifier = "", + + -- Counter that counts up for each new hash instance. + counter = 0, + + -- Hash table. + table = {} +} + +-- Inits hashing. Creates a hash_identifier that doesn't occur anywhere +-- in the text. +local function init_hash(text) + HASH.inited = true + HASH.identifier = "" + HASH.counter = 0 + HASH.table = {} + + local s = "HASH" + local counter = 0 + local id + while true do + id = s .. counter + if not text:find(id, 1, true) then break end + counter = counter + 1 + end + HASH.identifier = id +end + +-- Returns the hashed value for s. +local function hash(s) + assert(HASH.inited) + if not HASH.table[s] then + HASH.counter = HASH.counter + 1 + local id = HASH.identifier .. HASH.counter .. "X" + HASH.table[s] = id + end + return HASH.table[s] +end + +---------------------------------------------------------------------- +-- Protection +---------------------------------------------------------------------- + +-- The protection module is used to "protect" parts of a document +-- so that they are not modified by subsequent processing steps. +-- Protected parts are saved in a table for later unprotection + +-- Protection data +local PD = { + -- Saved blocks that have been converted + blocks = {}, + + -- Block level tags that will be protected + tags = {"p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", + "pre", "table", "dl", "ol", "ul", "script", "noscript", "form", "fieldset", + "iframe", "math", "ins", "del"} +} + +-- Pattern for matching a block tag that begins and ends in the leftmost +-- column and may contain indented subtags, i.e. +--
+-- A nested block. +--
+-- Nested data. +--
+--
+local function block_pattern(tag) + return "\n<" .. tag .. ".-\n[ \t]*\n" +end + +-- Pattern for matching a block tag that begins and ends with a newline +local function line_pattern(tag) + return "\n<" .. tag .. ".-[ \t]*\n" +end + +-- Protects the range of characters from start to stop in the text and +-- returns the protected string. +local function protect_range(text, start, stop) + local s = text:sub(start, stop) + local h = hash(s) + PD.blocks[h] = s + text = text:sub(1,start) .. h .. text:sub(stop) + return text +end + +-- Protect every part of the text that matches any of the patterns. The first +-- matching pattern is protected first, etc. +local function protect_matches(text, patterns) + while true do + local start, stop = find_first(text, patterns) + if not start then break end + text = protect_range(text, start, stop) + end + return text +end + +-- Protects blocklevel tags in the specified text +local function protect(text) + -- First protect potentially nested block tags + text = protect_matches(text, map(PD.tags, block_pattern)) + -- Then protect block tags at the line level. + text = protect_matches(text, map(PD.tags, line_pattern)) + -- Protect
and comment tags + text = protect_matches(text, {"\n]->[ \t]*\n"}) + text = protect_matches(text, {"\n[ \t]*\n"}) + return text +end + +-- Returns true if the string s is a hash resulting from protection +local function is_protected(s) + return PD.blocks[s] +end + +-- Unprotects the specified text by expanding all the nonces +local function unprotect(text) + for k,v in pairs(PD.blocks) do + v = v:gsub("%%", "%%%%") + text = text:gsub(k, v) + end + return text +end + + +---------------------------------------------------------------------- +-- Block transform +---------------------------------------------------------------------- + +-- The block transform functions transform the text on the block level. +-- They work with the text as an array of lines rather than as individual +-- characters. + +-- Returns true if the line is a ruler of (char) characters. +-- The line must contain at least three char characters and contain only spaces and +-- char characters. +local function is_ruler_of(line, char) + if not line:match("^[ %" .. char .. "]*$") then return false end + if not line:match("%" .. char .. ".*%" .. char .. ".*%" .. char) then return false end + return true +end + +-- Identifies the block level formatting present in the line +local function classify(line) + local info = {line = line, text = line} + + if line:match("^ ") then + info.type = "indented" + info.outdented = line:sub(5) + return info + end + + for _,c in ipairs({'*', '-', '_', '='}) do + if is_ruler_of(line, c) then + info.type = "ruler" + info.ruler_char = c + return info + end + end + + if line == "" then + info.type = "blank" + return info + end + + if line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") then + local m1, m2 = line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") + info.type = "header" + info.level = m1:len() + info.text = m2 + return info + end + + if line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") then + local number, text = line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") + info.type = "list_item" + info.list_type = "numeric" + info.number = 0 + number + info.text = text + return info + end + + if line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") then + local bullet, text = line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") + info.type = "list_item" + info.list_type = "bullet" + info.bullet = bullet + info.text= text + return info + end + + if line:match("^>[ \t]?(.*)") then + info.type = "blockquote" + info.text = line:match("^>[ \t]?(.*)") + return info + end + + if is_protected(line) then + info.type = "raw" + info.html = unprotect(line) + return info + end + + info.type = "normal" + return info +end + +-- Find headers constisting of a normal line followed by a ruler and converts them to +-- header entries. +local function headers(array) + local i = 1 + while i <= #array - 1 do + if array[i].type == "normal" and array[i+1].type == "ruler" and + (array[i+1].ruler_char == "-" or array[i+1].ruler_char == "=") then + local info = {line = array[i].line} + info.text = info.line + info.type = "header" + info.level = iff(array[i+1].ruler_char == "=", 1, 2) + table.remove(array, i+1) + array[i] = info + end + i = i + 1 + end + return array +end + +-- Forward declarations +local block_transform, span_transform, encode_code + +-- Convert lines to html code +local function blocks_to_html(lines, no_paragraphs) + local out = {} + local i = 1 + while i <= #lines do + local line = lines[i] + if line.type == "ruler" then + table.insert(out, "
") + elseif line.type == "raw" then + table.insert(out, line.html) + elseif line.type == "normal" then + local s = line.line + + while i+1 <= #lines and lines[i+1].type == "normal" do + i = i + 1 + s = s .. "\n" .. lines[i].line + end + + if no_paragraphs then + table.insert(out, span_transform(s)) + else + table.insert(out, "

" .. span_transform(s) .. "

") + end + elseif line.type == "header" then + local s = "" .. span_transform(line.text) .. "" + table.insert(out, s) + else + table.insert(out, line.line) + end + i = i + 1 + end + return out +end + +-- Find list blocks and convert them to protected data blocks +local function lists(array, sublist) + local function process_list(arr) + local function any_blanks(arr) + for i = 1, #arr do + if arr[i].type == "blank" then return true end + end + return false + end + + local function split_list_items(arr) + local acc = {arr[1]} + local res = {} + for i=2,#arr do + if arr[i].type == "list_item" then + table.insert(res, acc) + acc = {arr[i]} + else + table.insert(acc, arr[i]) + end + end + table.insert(res, acc) + return res + end + + local function process_list_item(lines, block) + while lines[#lines].type == "blank" do + table.remove(lines) + end + + local itemtext = lines[1].text + for i=2,#lines do + itemtext = itemtext .. "\n" .. outdent(lines[i].line) + end + if block then + itemtext = block_transform(itemtext, true) + if not itemtext:find("
") then itemtext = indent(itemtext) end
+            return "    
  • " .. itemtext .. "
  • " + else + local lines = split(itemtext) + lines = map(lines, classify) + lines = lists(lines, true) + lines = blocks_to_html(lines, true) + itemtext = table.concat(lines, "\n") + if not itemtext:find("
    ") then itemtext = indent(itemtext) end
    +            return "    
  • " .. itemtext .. "
  • " + end + end + + local block_list = any_blanks(arr) + local items = split_list_items(arr) + local out = "" + for _, item in ipairs(items) do + out = out .. process_list_item(item, block_list) .. "\n" + end + if arr[1].list_type == "numeric" then + return "
      \n" .. out .. "
    " + else + return "
      \n" .. out .. "
    " + end + end + + -- Finds the range of lines composing the first list in the array. A list + -- starts with (^ list_item) or (blank list_item) and ends with + -- (blank* $) or (blank normal). + -- + -- A sublist can start with just (list_item) does not need a blank... + local function find_list(array, sublist) + local function find_list_start(array, sublist) + if array[1].type == "list_item" then return 1 end + if sublist then + for i = 1,#array do + if array[i].type == "list_item" then return i end + end + else + for i = 1, #array-1 do + if array[i].type == "blank" and array[i+1].type == "list_item" then + return i+1 + end + end + end + return nil + end + local function find_list_end(array, start) + local pos = #array + for i = start, #array-1 do + if array[i].type == "blank" and array[i+1].type ~= "list_item" + and array[i+1].type ~= "indented" and array[i+1].type ~= "blank" then + pos = i-1 + break + end + end + while pos > start and array[pos].type == "blank" do + pos = pos - 1 + end + return pos + end + + local start = find_list_start(array, sublist) + if not start then return nil end + return start, find_list_end(array, start) + end + + while true do + local start, stop = find_list(array, sublist) + if not start then break end + local text = process_list(splice(array, start, stop)) + local info = { + line = text, + type = "raw", + html = text + } + array = splice(array, start, stop, {info}) + end + + -- Convert any remaining list items to normal + for _,line in ipairs(array) do + if line.type == "list_item" then line.type = "normal" end + end + + return array +end + +-- Find and convert blockquote markers. +local function blockquotes(lines) + local function find_blockquote(lines) + local start + for i,line in ipairs(lines) do + if line.type == "blockquote" then + start = i + break + end + end + if not start then return nil end + + local stop = #lines + for i = start+1, #lines do + if lines[i].type == "blank" or lines[i].type == "blockquote" then + elseif lines[i].type == "normal" then + if lines[i-1].type == "blank" then stop = i-1 break end + else + stop = i-1 break + end + end + while lines[stop].type == "blank" do stop = stop - 1 end + return start, stop + end + + local function process_blockquote(lines) + local raw = lines[1].text + for i = 2,#lines do + raw = raw .. "\n" .. lines[i].text + end + local bt = block_transform(raw) + if not bt:find("
    ") then bt = indent(bt) end
    +      return "
    \n " .. bt .. + "\n
    " + end + + while true do + local start, stop = find_blockquote(lines) + if not start then break end + local text = process_blockquote(splice(lines, start, stop)) + local info = { + line = text, + type = "raw", + html = text + } + lines = splice(lines, start, stop, {info}) + end + return lines +end + +-- Find and convert codeblocks. +local function codeblocks(lines) + local function find_codeblock(lines) + local start + for i,line in ipairs(lines) do + if line.type == "indented" then start = i break end + end + if not start then return nil end + + local stop = #lines + for i = start+1, #lines do + if lines[i].type ~= "indented" and lines[i].type ~= "blank" then + stop = i-1 + break + end + end + while lines[stop].type == "blank" do stop = stop - 1 end + return start, stop + end + + local function process_codeblock(lines) + local raw = detab(encode_code(outdent(lines[1].line))) + for i = 2,#lines do + raw = raw .. "\n" .. detab(encode_code(outdent(lines[i].line))) + end + return "
    " .. raw .. "\n
    " + end + + while true do + local start, stop = find_codeblock(lines) + if not start then break end + local text = process_codeblock(splice(lines, start, stop)) + local info = { + line = text, + type = "raw", + html = text + } + lines = splice(lines, start, stop, {info}) + end + return lines +end + +-- Perform all the block level transforms +function block_transform(text, sublist) + local lines = split(text) + lines = map(lines, classify) + lines = headers(lines) + lines = lists(lines, sublist) + lines = codeblocks(lines) + lines = blockquotes(lines) + lines = blocks_to_html(lines) + local text = table.concat(lines, "\n") + return text +end + +---------------------------------------------------------------------- +-- Span transform +---------------------------------------------------------------------- + +-- Functions for transforming the text at the span level. + +-- These characters may need to be escaped because they have a special +-- meaning in markdown. +local escape_chars = "'\\`*_{}[]()>#+-.!'" +local escape_table = {} + +local function init_escape_table() + escape_table = {} + for i = 1,#escape_chars do + local c = escape_chars:sub(i,i) + escape_table[c] = hash(c) + end +end + +-- Adds a new escape to the escape table. +local function add_escape(text) + if not escape_table[text] then + escape_table[text] = hash(text) + end + return escape_table[text] +end + +-- Encode backspace-escaped characters in the markdown source. +local function encode_backslash_escapes(t) + for i=1,escape_chars:len() do + local c = escape_chars:sub(i,i) + t = t:gsub("\\%" .. c, escape_table[c]) + end + return t +end + +-- Escape characters that should not be disturbed by markdown. +local function escape_special_chars(text) + local tokens = tokenize_html(text) + + local out = "" + for _, token in ipairs(tokens) do + local t = token.text + if token.type == "tag" then + -- In tags, encode * and _ so they don't conflict with their use in markdown. + t = t:gsub("%*", escape_table["*"]) + t = t:gsub("%_", escape_table["_"]) + else + t = encode_backslash_escapes(t) + end + out = out .. t + end + return out +end + +-- Unescape characters that have been encoded. +local function unescape_special_chars(t) + local tin = t + for k,v in pairs(escape_table) do + k = k:gsub("%%", "%%%%") + t = t:gsub(v,k) + end + if t ~= tin then t = unescape_special_chars(t) end + return t +end + +-- Encode/escape certain characters inside Markdown code runs. +-- The point is that in code, these characters are literals, +-- and lose their special Markdown meanings. +function encode_code(s) + s = s:gsub("%&", "&") + s = s:gsub("<", "<") + s = s:gsub(">", ">") + for k,v in pairs(escape_table) do + s = s:gsub("%"..k, v) + end + return s +end + +-- Handle backtick blocks. +local function code_spans(s) + s = s:gsub("\\\\", escape_table["\\"]) + s = s:gsub("\\`", escape_table["`"]) + + local pos = 1 + while true do + local start, stop = s:find("`+", pos) + if not start then return s end + local count = stop - start + 1 + -- Find a matching numbert of backticks + local estart, estop = s:find(string.rep("`", count), stop+1) + local brstart = s:find("\n", stop+1) + if estart and (not brstart or estart < brstart) then + local code = s:sub(stop+1, estart-1) + code = code:gsub("^[ \t]+", "") + code = code:gsub("[ \t]+$", "") + code = code:gsub(escape_table["\\"], escape_table["\\"] .. escape_table["\\"]) + code = code:gsub(escape_table["`"], escape_table["\\"] .. escape_table["`"]) + code = "" .. encode_code(code) .. "" + code = add_escape(code) + s = s:sub(1, start-1) .. code .. s:sub(estop+1) + pos = start + code:len() + else + pos = stop + 1 + end + end + return s +end + +-- Encode alt text... enodes &, and ". +local function encode_alt(s) + if not s then return s end + s = s:gsub('&', '&') + s = s:gsub('"', '"') + s = s:gsub('<', '<') + return s +end + +-- Forward declaration for link_db as returned by strip_link_definitions. +local link_database + +-- Handle image references +local function images(text) + local function reference_link(alt, id) + alt = encode_alt(alt:match("%b[]"):sub(2,-2)) + id = id:match("%[(.*)%]"):lower() + if id == "" then id = text:lower() end + link_database[id] = link_database[id] or {} + if not link_database[id].url then return nil end + local url = link_database[id].url or id + url = encode_alt(url) + local title = encode_alt(link_database[id].title) + if title then title = " title=\"" .. title .. "\"" else title = "" end + return add_escape ('' .. alt .. '") + end + + local function inline_link(alt, link) + alt = encode_alt(alt:match("%b[]"):sub(2,-2)) + local url, title = link:match("%(?[ \t]*['\"](.+)['\"]") + url = url or link:match("%(?%)") + url = encode_alt(url) + title = encode_alt(title) + if title then + return add_escape('' .. alt .. '') + else + return add_escape('' .. alt .. '') + end + end + + text = text:gsub("!(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) + text = text:gsub("!(%b[])(%b())", inline_link) + return text +end + +-- Handle anchor references +local function anchors(text) + local function reference_link(text, id) + text = text:match("%b[]"):sub(2,-2) + id = id:match("%b[]"):sub(2,-2):lower() + if id == "" then id = text:lower() end + link_database[id] = link_database[id] or {} + if not link_database[id].url then return nil end + local url = link_database[id].url or id + url = encode_alt(url) + local title = encode_alt(link_database[id].title) + if title then title = " title=\"" .. title .. "\"" else title = "" end + return add_escape("") .. text .. add_escape("") + end + + local function inline_link(text, link) + text = text:match("%b[]"):sub(2,-2) + local url, title = link:match("%(?[ \t]*['\"](.+)['\"]") + title = encode_alt(title) + url = url or link:match("%(?%)") or "" + url = encode_alt(url) + if title then + return add_escape("") .. text .. "" + else + return add_escape("") .. text .. add_escape("") + end + end + + text = text:gsub("(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) + text = text:gsub("(%b[])(%b())", inline_link) + return text +end + +-- Handle auto links, i.e. . +local function auto_links(text) + local function link(s) + return add_escape("") .. s .. "" + end + -- Encode chars as a mix of dec and hex entitites to (perhaps) fool + -- spambots. + local function encode_email_address(s) + -- Use a deterministic encoding to make unit testing possible. + -- Code 45% hex, 45% dec, 10% plain. + local hex = {code = function(c) return "&#x" .. string.format("%x", c:byte()) .. ";" end, count = 1, rate = 0.45} + local dec = {code = function(c) return "&#" .. c:byte() .. ";" end, count = 0, rate = 0.45} + local plain = {code = function(c) return c end, count = 0, rate = 0.1} + local codes = {hex, dec, plain} + local function swap(t,k1,k2) local temp = t[k2] t[k2] = t[k1] t[k1] = temp end + + local out = "" + for i = 1,s:len() do + for _,code in ipairs(codes) do code.count = code.count + code.rate end + if codes[1].count < codes[2].count then swap(codes,1,2) end + if codes[2].count < codes[3].count then swap(codes,2,3) end + if codes[1].count < codes[2].count then swap(codes,1,2) end + + local code = codes[1] + local c = s:sub(i,i) + -- Force encoding of "@" to make email address more invisible. + if c == "@" and code == plain then code = codes[2] end + out = out .. code.code(c) + code.count = code.count - 1 + end + return out + end + local function mail(s) + s = unescape_special_chars(s) + local address = encode_email_address("mailto:" .. s) + local text = encode_email_address(s) + return add_escape("") .. text .. "" + end + -- links + text = text:gsub("<(https?:[^'\">%s]+)>", link) + text = text:gsub("<(ftp:[^'\">%s]+)>", link) + + -- mail + text = text:gsub("%s]+)>", mail) + text = text:gsub("<([-.%w]+%@[-.%w]+)>", mail) + return text +end + +-- Encode free standing amps (&) and angles (<)... note that this does not +-- encode free >. +local function amps_and_angles(s) + -- encode amps not part of &..; expression + local pos = 1 + while true do + local amp = s:find("&", pos) + if not amp then break end + local semi = s:find(";", amp+1) + local stop = s:find("[ \t\n&]", amp+1) + if not semi or (stop and stop < semi) or (semi - amp) > 15 then + s = s:sub(1,amp-1) .. "&" .. s:sub(amp+1) + pos = amp+1 + else + pos = amp+1 + end + end + + -- encode naked <'s + s = s:gsub("<([^a-zA-Z/?$!])", "<%1") + s = s:gsub("<$", "<") + + -- what about >, nothing done in the original markdown source to handle them + return s +end + +-- Handles emphasis markers (* and _) in the text. +local function emphasis(text) + for _, s in ipairs {"%*%*", "%_%_"} do + text = text:gsub(s .. "([^%s][%*%_]?)" .. s, "%1") + text = text:gsub(s .. "([^%s][^<>]-[^%s][%*%_]?)" .. s, "%1") + end + for _, s in ipairs {"%*", "%_"} do + text = text:gsub(s .. "([^%s_])" .. s, "%1") + text = text:gsub(s .. "([^%s_])" .. s, "%1") + text = text:gsub(s .. "([^%s_][^<>_]-[^%s_])" .. s, "%1") + text = text:gsub(s .. "([^<>_]-[^<>_]-[^<>_]-)" .. s, "%1") + end + return text +end + +-- Handles line break markers in the text. +local function line_breaks(text) + return text:gsub(" +\n", "
    \n") +end + +-- Perform all span level transforms. +function span_transform(text) + text = code_spans(text) + text = escape_special_chars(text) + text = images(text) + text = anchors(text) + text = auto_links(text) + text = amps_and_angles(text) + text = emphasis(text) + text = line_breaks(text) + return text +end + +---------------------------------------------------------------------- +-- Markdown +---------------------------------------------------------------------- + +-- Cleanup the text by normalizing some possible variations to make further +-- processing easier. +local function cleanup(text) + -- Standardize line endings + text = text:gsub("\r\n", "\n") -- DOS to UNIX + text = text:gsub("\r", "\n") -- Mac to UNIX + + -- Convert all tabs to spaces + text = detab(text) + + -- Strip lines with only spaces and tabs + while true do + local subs + text, subs = text:gsub("\n[ \t]+\n", "\n\n") + if subs == 0 then break end + end + + return "\n" .. text .. "\n" +end + +-- Strips link definitions from the text and stores the data in a lookup table. +local function strip_link_definitions(text) + local linkdb = {} + + local function link_def(id, url, title) + id = id:match("%[(.+)%]"):lower() + linkdb[id] = linkdb[id] or {} + linkdb[id].url = url or linkdb[id].url + linkdb[id].title = title or linkdb[id].title + return "" + end + + local def_no_title = "\n ? ? ?(%b[]):[ \t]*\n?[ \t]*]+)>?[ \t]*" + local def_title1 = def_no_title .. "[ \t]+\n?[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" + local def_title2 = def_no_title .. "[ \t]*\n[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" + local def_title3 = def_no_title .. "[ \t]*\n?[ \t]+[\"'(]([^\n]+)[\"')][ \t]*" + + text = text:gsub(def_title1, link_def) + text = text:gsub(def_title2, link_def) + text = text:gsub(def_title3, link_def) + text = text:gsub(def_no_title, link_def) + return text, linkdb +end + +-- Main markdown processing function +local function markdown(text) + init_hash(text) + init_escape_table() + + text = cleanup(text) + text = protect(text) + text, link_database = strip_link_definitions(text) + text = block_transform(text) + text = unescape_special_chars(text) + return text +end + +---------------------------------------------------------------------- +-- End of module +---------------------------------------------------------------------- + +-- For compatibility, set markdown function as a global +_G.markdown = markdown + +-- Class for parsing command-line options +local OptionParser = {} +OptionParser.__index = OptionParser + +-- Creates a new option parser +function OptionParser:new() + local o = {short = {}, long = {}} + setmetatable(o, self) + return o +end + +-- Calls f() whenever a flag with specified short and long name is encountered +function OptionParser:flag(short, long, f) + local info = {type = "flag", f = f} + if short then self.short[short] = info end + if long then self.long[long] = info end +end + +-- Calls f(param) whenever a parameter flag with specified short and long name is encountered +function OptionParser:param(short, long, f) + local info = {type = "param", f = f} + if short then self.short[short] = info end + if long then self.long[long] = info end +end + +-- Calls f(v) for each non-flag argument +function OptionParser:arg(f) + self.arg = f +end + +-- Runs the option parser for the specified set of arguments. Returns true if all arguments +-- where successfully parsed and false otherwise. +function OptionParser:run(args) + local pos = 1 + while pos <= #args do + local arg = args[pos] + if arg == "--" then + for i=pos+1,#args do + if self.arg then self.arg(args[i]) end + return true + end + end + if arg:match("^%-%-") then + local info = self.long[arg:sub(3)] + if not info then print("Unknown flag: " .. arg) return false end + if info.type == "flag" then + info.f() + pos = pos + 1 + else + local param = args[pos+1] + if not param then print("No parameter for flag: " .. arg) return false end + info.f(param) + pos = pos+2 + end + elseif arg:match("^%-") then + for i=2,arg:len() do + local c = arg:sub(i,i) + local info = self.short[c] + if not info then print("Unknown flag: -" .. c) return false end + if info.type == "flag" then + info.f() + else + if i == arg:len() then + local param = args[pos+1] + if not param then print("No parameter for flag: -" .. c) return false end + info.f(param) + pos = pos + 1 + else + local param = arg:sub(i+1) + info.f(param) + end + break + end + end + pos = pos + 1 + else + if self.arg then self.arg(arg) end + pos = pos + 1 + end + end + return true +end + +local function read_file(path, descr) + local file = io.open(path) or error("Could not open " .. descr .. " file: " .. path) + local contents = file:read("*a") or error("Could not read " .. descr .. " from " .. path) + file:close() + return contents +end + +-- Handles the case when markdown is run from the command line +local function run_command_line(arg) + -- Generate output for input s given options + local function run(s, options) + s = markdown(s) + if not options.wrap_header then return s end + local header + if options.header then + header = read_file(options.header, "header") + else + header = [[ + + + + + TITLE + + + +]] + local title = options.title or s:match("

    (.-)

    ") or s:match("

    (.-)

    ") or + s:match("

    (.-)

    ") or "Untitled" + header = header:gsub("TITLE", title) + if options.inline_style then + local style = read_file(options.stylesheet, "style sheet") + header = header:gsub('', + "") + else + header = header:gsub("STYLESHEET", options.stylesheet) + end + header = header:gsub("CHARSET", options.charset) + end + local footer = "" + if options.footer then + footer = read_file(options.footer, "footer") + end + return header .. s .. footer + end + + -- Generate output path name from input path name given options. + local function outpath(path, options) + if options.append then return path .. ".html" end + local m = path:match("^(.+%.html)[^/\\]+$") if m then return m end + m = path:match("^(.+%.)[^/\\]*$") if m and path ~= m .. "html" then return m .. "html" end + return path .. ".html" + end + + -- Default commandline options + local options = { + wrap_header = true, + header = nil, + footer = nil, + charset = "utf-8", + title = nil, + stylesheet = "default.css", + inline_style = false + } + local help = [[ +Usage: markdown.lua [OPTION] [FILE] +Runs the markdown text markup to HTML converter on each file specified on the +command line. If no files are specified, runs on standard input. + +No header: + -n, --no-wrap Don't wrap the output in ... tags. +Custom header: + -e, --header FILE Use content of FILE for header. + -f, --footer FILE Use content of FILE for footer. +Generated header: + -c, --charset SET Specifies charset (default utf-8). + -i, --title TITLE Specifies title (default from first

    tag). + -s, --style STYLE Specifies style sheet file (default default.css). + -l, --inline-style Include the style sheet file inline in the header. +Generated files: + -a, --append Append .html extension (instead of replacing). +Other options: + -h, --help Print this help text. + -t, --test Run the unit tests. +]] + + local run_stdin = true + local op = OptionParser:new() + op:flag("n", "no-wrap", function () options.wrap_header = false end) + op:param("e", "header", function (x) options.header = x end) + op:param("f", "footer", function (x) options.footer = x end) + op:param("c", "charset", function (x) options.charset = x end) + op:param("i", "title", function(x) options.title = x end) + op:param("s", "style", function(x) options.stylesheet = x end) + op:flag("l", "inline-style", function() options.inline_style = true end) + op:flag("a", "append", function() options.append = true end) + op:flag("t", "test", function() + local n = arg[0]:gsub("markdown%.lua", "markdown-tests.lua") + local f = io.open(n) + if f then + f:close() + package.loaded.markdown = markdown + dofile(n) + else + error("Cannot find markdown-tests.lua") + end + run_stdin = false + end) + op:flag("h", "help", function() print(help) run_stdin = false end) + op:arg(function(path) + local s = read_file(path, "input") + s = run(s, options) + local file = io.open(outpath(path, options), "w") or error("Could not open output file: " .. outpath(path, options)) + file:write(s) + file:close() + run_stdin = false + end + ) + + if not op:run(arg) then + print(help) + run_stdin = false + end + + if run_stdin then + local s = io.read("*a") + s = run(s, options) + io.write(s) + end +end + +-- If we are being run from the command-line, act accordingly +if arg and arg[0]:find("markdown%.lua$") then + run_command_line(arg) +else + return markdown +end diff --git a/src/.lua/soakbean.lua b/src/.lua/soakbean.lua index 05d4074..a6ecc7b 100644 --- a/src/.lua/soakbean.lua +++ b/src/.lua/soakbean.lua @@ -51,49 +51,80 @@ sb = { end end, - -- print( util.tpl("${name} is ${value}", {name = "foo", value = "bar"}) ) - -- "foo is bar" - -- 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 +-- usage: +-- +-- tpl([[ +-- Hello ${world} +-- foo() = ${foo(1)} +-- +-- {if true then +-- print(world) +-- print("hello universe") +-- end} +-- +-- {for i in pairs(numbers) do +-- print(i) +-- end} +-- +-- {for k,v in pairs(items) do +-- print(k .. " " .. v.x) +-- 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*}") - local v = tab[k] - if type(v) == "function" then - return tostring(v(load("return " .. arg)()) or '') + tpl = function(s, __data) + local __env = {} + setmetatable(__env, { + __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 - 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) + end + local s1 = load( luastr .. "\n return str", "chunkname", "t", __env )() + -- evaluate template vars + local s2 = (s1: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") + local fn = (loadstring or load)("return function(__env) " .. inner .. " end") if fn then - setfenv(fn(), setmetatable(tab, { __index = _G }))() + setfenv(fn(), setmetatable(__env, { __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 + -- Traverse the nested table path (e.g. "foo.bar" -> __env["foo"]["bar"]) + local v = __env if path then for key in path:gmatch("[%w_]+") do if type(v) == "table" then @@ -111,6 +142,7 @@ sb = { end return v ~= nil and tostring(v) or w end)) + return s2 end, write = function(str) diff --git a/src/.lua/util.lua b/src/.lua/util.lua index f523540..32829b0 100644 --- a/src/.lua/util.lua +++ b/src/.lua/util.lua @@ -84,12 +84,12 @@ end util.template = function(content,data) local mdata = util.merge(data or app,{}) mdata = util.merge(mdata, {opts = app.opts}) + mdata.title = mdata.title or app.opts.subtitle 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 diff --git a/src/css/xrforge.css b/src/css/xrforge.css index 20ce660..8893da6 100644 --- a/src/css/xrforge.css +++ b/src/css/xrforge.css @@ -155,10 +155,14 @@ pre{ font-size:20px; } h1,h2,h3,h4,h5{ - font-size: 16px; + font-size: 17px; padding-top: 25px; padding-bottom:25px; } +h1{ + font-size: 29px; + margin: 15px 0px; +} .sym{ font-size:100px; color:#000; } @@ -253,6 +257,13 @@ b.tag{ border-radius: 5px; } +blockquote{ + background: #224; + padding: 15px; + margin: 15px 0px; + display: inline-block; +} + /* progressive enhancement quirks */ .spectrum{ display:none } diff --git a/src/css/xrforge.max.css b/src/css/xrforge.max.css index 1fa424b..a8f1e7a 100644 --- a/src/css/xrforge.max.css +++ b/src/css/xrforge.max.css @@ -72,8 +72,8 @@ img.icon{ width: 17px; } -iframe#viewer{ - position: fixed; +#viewer{ + position: absolute; width: 100%; height: calc( 99.7vh - 59px); margin-top: 0px; diff --git a/src/page/about.html b/src/page/about.html index 89f6104..8393cbf 100644 --- a/src/page/about.html +++ b/src/page/about.html @@ -91,7 +91,7 @@ div#xrecosystem{

    Why people want XRForge

    - In short: its hyperscalable portal-system and ability projecting existing 2D ecosystems into XR.
    + In short: a hyperscalable portal-system and projection of 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.
    diff --git a/src/page/admin/index.html b/src/page/admin/index.html index 23663b7..89b65b6 100644 --- a/src/page/admin/index.html +++ b/src/page/admin/index.html @@ -10,12 +10,10 @@ ${notification}
    - ${tabs} - ${for i,tab in pairs(tabs) do} - - - ${end} - + {for i,tab in pairs(tabs) do + print('') + print('') + end} ${plugins} diff --git a/src/page/admin/plugin.html b/src/page/admin/plugin.html index 6d9bb17..9a5bc3c 100644 --- a/src/page/admin/plugin.html +++ b/src/page/admin/plugin.html @@ -14,11 +14,13 @@ - $(if #_.plugin.info > 2 then) -
    - ${plugin.info} -
    - $(end) + {if #plugin.info > 2 then + print([[ +
    + ${plugin.info} +
    + ]]) + end}

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

    ${opts.title}

    - ${opts.subtitle} -
    -
    -${end} +{if parent ~= nil and parent.parent == nil then + print([[ +
    +

    ${opts.title}

    + ${opts.subtitle} +
    +
    + ]]) +end} -${if _.parent ~= nil and _.parent.parent ~= nil then} -

    ${parent.name}

    -${end} +{if parent ~= nil and parent.parent ~= nil then + print([[ +

    ${parent.name}

    + ]]) +end}
    ${html_items} @@ -20,16 +24,23 @@ ${footer} - + + + {for i,v in pairs(items) do + print('\t') + end} + + - - - ${for k,v in pairs(items) do} - - - - - ${end} - + + {for i,v in pairs(items) do + local width = 3 + local center = -((#items*width)/1.3) + (i*width) + print('\t\t') + print('\t\t ') + print('\t\t ') + print('\t\t ') + print('\t\t') + end} diff --git a/src/page/dir.lua b/src/page/dir.lua index 0c7aa3e..90c8353 100644 --- a/src/page/dir.lua +++ b/src/page/dir.lua @@ -60,7 +60,8 @@ return { table.insert(data.items, util.merge({x = i*2, i = i},v) ) i=i+1 if v.view3D then - data.view3D = v + data.item = v + data.title = data.item.title local port = ':' .. GetPort() if port == 80 then port = '' end data.url = GetScheme() .. '://' .. GetHost() .. port .. v.url @@ -68,8 +69,8 @@ return { 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) + if data.item then + app.cache.url[ urlpath ] = util.templateFile( "page/item.html", data) else app.cache.url[ urlpath ] = util.templateFile( "page/dir.html", data) end diff --git a/src/page/header.html b/src/page/header.html index cc3f0e5..d2b1efd 100644 --- a/src/page/header.html +++ b/src/page/header.html @@ -5,7 +5,7 @@ - XRForge - selfsovereign interoperable XR experiences + ${title} ${header_include} -
    diff --git a/src/page/index.html b/src/page/index.html index ca87171..2c312d4 100644 --- a/src/page/index.html +++ b/src/page/index.html @@ -1,5 +1,22 @@ ${header} - + + + ${footer} diff --git a/src/page/item.html b/src/page/item.html new file mode 100644 index 0000000..f8c30fb --- /dev/null +++ b/src/page/item.html @@ -0,0 +1,30 @@ +${header} + + + +
    +
    +
    +
    +{if item.parent.info then + print(item.parent.info) +end} +{if not item.parent.info then + print('

    ${item.title}

    \n\n') + print('This item has no description (yet)') +end} + + +${footer} + + + + + + + + + + + + diff --git a/src/plugin/markdown.lua b/src/plugin/markdown.lua index 52ad4cc..8b492ce 100644 --- a/src/plugin/markdown.lua +++ b/src/plugin/markdown.lua @@ -1,10 +1,18 @@ local util = require('util') +local markdown = require("markdown") app.on('scan_experience', function(exp) --print_r(exp) end) 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 = '
    ' .. markdown( util.readfile(exp.path) ) .. '
    ' + end + end) end) app.on('set', function(k,v) @@ -20,7 +28,7 @@ local plugin = { key='app.opts.plugin_markdown_files', title='Markdown files', info='Space-separated, lua patterns are allowed', - default="*.md", + default="README.md", values='' }, { diff --git a/src/shader/xrforgeportal.shadertoy.frag b/src/shader/xrforgeportal.shadertoy.frag new file mode 100644 index 0000000..04f98fc --- /dev/null +++ b/src/shader/xrforgeportal.shadertoy.frag @@ -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) ); +} diff --git a/util/xrforge.sh b/util/xrforge.sh index 1b73f1b..4ebe2ec 100755 --- a/util/xrforge.sh +++ b/util/xrforge.sh @@ -41,7 +41,7 @@ build(){ rebuild(){ 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 } "$@"