Mercurial Hosting > freedit
changeset 44:96f0c3d65698
add /bbcode
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Thu, 10 Nov 2022 23:18:58 -0700 |
parents | 298c71e0c854 |
children | 2d4f00755092 |
files | src/bbcode/Bbcode.luan src/bbcode/bbcode.css src/bbcode/bbcode.js src/bbcode/test.html src/bbcode/test.js.luan src/lib/Bbcode.luan src/lib/Shared.luan src/save_edit.js.luan src/thread.html.luan |
diffstat | 9 files changed, 513 insertions(+), 342 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/bbcode/Bbcode.luan Thu Nov 10 23:18:58 2022 -0700 @@ -0,0 +1,338 @@ +local Luan = require "luan:Luan.luan" +local error = Luan.error +local type = Luan.type or error() +local ipairs = Luan.ipairs or error() +local pairs = Luan.pairs or error() +local stringify = Luan.stringify or error() +local Io = require "luan:Io.luan" +local output_of = Io.output_of or error() +local Parsers = require "luan:Parsers.luan" +local bbcode_parse = Parsers.bbcode_parse or error() +local Html = require "luan:Html.luan" +local html_encode = Html.encode or error() +local html_parse = Html.parse or error() +local Table = require "luan:Table.luan" +local is_list = Table.is_list or error() +local concat = Table.concat or error() +local String = require "luan:String.luan" +local regex = String.regex or error() +local ends_with = String.ends_with or error() +local User = require "site:/lib/User.luan" +local Shared = require "site:/lib/Shared.luan" +local list_to_set = Shared.list_to_set or error() +local to_list = Shared.to_list or error() +local Logging = require "luan:logging/Logging.luan" +local logger = Logging.logger "Bbcode" + + +local Bbcode = {} + +local starting_cr_regex = regex[[^\n]] + +local to_html +local html = {} + +function html.b(bbcode,options) + %><b><% to_html(bbcode.contents,options) %></b><% +end + +function html.i(bbcode,options) + %><i><% to_html(bbcode.contents,options) %></i><% +end + +function html.u(bbcode,options) + %><u><% to_html(bbcode.contents,options) %></u><% +end + +function html.s(bbcode,options) + %><s><% to_html(bbcode.contents,options) %></s><% +end + +function html.sup(bbcode,options) + %><sup><% to_html(bbcode.contents,options) %></sup><% +end + +function html.brackets(bbcode,options) + %>[<% to_html(bbcode.contents,options) %>]<% +end + +function html.url(bbcode,options) + local url = bbcode.param + if url == nil then + url = html_encode(bbcode.contents) + %><a href="<%=url%>"><%=url%></a><% + else + url = html_encode(url) + %><a href="<%=url%>"><% to_html(bbcode.contents,options) %></a><% + end +end + +function html.code(bbcode,options) + local s = starting_cr_regex.gsub(bbcode.contents,"") + %><code><%= html_encode(s) %></code><% + options.strip_newline = true +end + +function html.img(bbcode,options) + %><img src="<%= html_encode(bbcode.contents) %>"><% +end + +function html.color(bbcode,options) + %><span style="color:<%=bbcode.param%>"><% to_html(bbcode.contents,options) %></span><% +end + +function html.size(bbcode,options) + %><span style="font-size:<%=bbcode.param%>"><% to_html(bbcode.contents,options) %></span><% +end + +function html.quote(bbcode,options) + %><blockquote><% + local user_name = bbcode.param + if user_name ~= nil then + local user = User.get_by_name(user_name) + if user == nil then + %><%= user_name %> wrote:<% + else + %><a href="/user_something"><%= user_name %></a> wrote:<% + end + else + options.strip_newline = true + end + to_html(bbcode.contents,options) + %></blockquote><% + options.strip_newline = true +end + +local function video_iframe(url) + %><iframe width="560" height="315" frameborder="0" allowfullscreen src="<%=url%>"></iframe><% +end + +local video_handlers = {} +do + local ptn1 = regex[[^\Qhttps://youtu.be/\E([a-zA-Z0-9_-]+)(?:\?t=([0-9]+))?]] + local ptn2 = regex[[^\Qhttps://www.youtube.com/watch?v=\E([a-zA-Z0-9_-]+)(?:&t=([0-9]+)s)?]] + function video_handlers.youtube(url) + local id, start = ptn1.match(url) + if id == nil then + id, start = ptn2.match(url) + end + if id == nil then + return false + end + url = "https://www.youtube.com/embed/"..id + if start ~= nil then + url = url.."?start="..start + end + video_iframe(url) + return true + end +end +do + local ptn = regex[[^\Qhttps://rumble.com/embed/\E[a-z0-9]+/\?pub=[a-z0-9]+]] + function video_handlers.rumble(url) + if not ptn.matches(url) then + return false + end + video_iframe(url) + return true + end +end +do + local ptn = regex[[^\Qhttps://www.bitchute.com/video/\E([a-zA-Z0-9]+)/]] + function video_handlers.bitchute(url) + local id = ptn.match(url) + if id == nil then + return false + end + url = "https://www.bitchute.com/embed/"..id.."/" + video_iframe(url) + return true + end +end +do + local ptn = regex[[^\Qhttps://vimeo.com/\E([0-9]+)]] + function video_handlers.vimeo(url) + local id = ptn.match(url) + if id == nil then + return false + end + url = "https://player.vimeo.com/video/"..id + video_iframe(url) + return true + end +end +do + local ptn = regex[[^\Qhttps://dai.ly/\E([a-z0-9]+)]] + function video_handlers.dailymotion(url) + local id = ptn.match(url) + if id == nil then + return false + end + url = "https://www.dailymotion.com/embed/video/"..id + video_iframe(url) + return true + end +end +do + local ptn = regex[[^\Qhttps://www.tiktok.com/\E[^/]+/video/([0-9]+)]] + function video_handlers.tiktok(url) + local id = ptn.match(url) + if id == nil then + return false + end + %><blockquote class="tiktok-embed" data-video-id="<%=id%>" style="max-width: 560px; margin-left: 0;"><section></section></blockquote><% + %><script async src="https://www.tiktok.com/embed.js"></script><% + return true + end +end +do + local ptn = regex[[\.[a-zA-Z0-9]+$]] + function video_handlers.file(url) + if not ptn.matches(url) then + return false + end + %><video controls width="560"><source src="<%=html_encode(url)%>"></video><% + return true + end +end + +function html.video(bbcode,options) + local url = bbcode.contents + for _, handle in pairs(video_handlers) do + if handle(url) then return end + end + url = html_encode(url) + %><a href="<%=url%>"><%=url%></a><% +end + +local function list_to_html(bbcode,options) + local list = to_list(bbcode.contents) + for _, item in ipairs(list) do + if type(item) == "table" and item.name == "li" then + %><li><% to_html(item.contents,options) %></li><% + end + end + options.strip_newline = true +end + +function html.ul(bbcode,options) + %><ul><% + list_to_html(bbcode,options) + %></ul><% +end + +function html.ol(bbcode,options) + %><ol><% + list_to_html(bbcode,options) + %></ol><% +end + +function to_html(bbcode,options) + if options.strip_newline then + if type(bbcode) == "string" then + bbcode = starting_cr_regex.gsub(bbcode,"") + end + options.strip_newline = false + end + if type(bbcode) == "string" then + %><%= html_encode(bbcode) %><% + else + type(bbcode) == "table" or error() + if is_list(bbcode) then + for _, v in ipairs(bbcode) do + to_html(v,options) + end + else + local fn = html[bbcode.name] or error(bbcode.name.." not handled") + fn(bbcode,options) + end + end +end + +function Bbcode.to_html(bbcode) + bbcode = bbcode_parse(bbcode) + %><div message><% + to_html(bbcode,{strip_newline=false}) + %></div><% +end + + +local doesnt_nest = list_to_set{ + "url" + "code" + "img" + "video" +} + +local url_regex = regex[[(^|\s)(https?://\S+)]] + +local function preprocess(bbcode) + if type(bbcode) == "string" then + bbcode = url_regex.gsub( bbcode, "$1[url]$2[/url]" ) + %><%= bbcode %><% + else + type(bbcode) == "table" or error() + if is_list(bbcode) then + for _, v in ipairs(bbcode) do + preprocess(v) + end + else + local name = bbcode.name + local param = bbcode.param + %>[<%=name%><% + if param ~= nil then + %>=<%=param%><% + end + %>]<% + if doesnt_nest[name] then + %><%=bbcode.contents%><% + else + preprocess(bbcode.contents) + end + if name == "code" and param ~= nil then + %>[/<%=name%>=<%=param%>]<% + else + %>[/<%=name%>]<% + end + end + end +end + +-- url handling +function Bbcode.preprocess(bbcode) + bbcode = bbcode_parse(bbcode) + return output_of(function() + preprocess(bbcode) + end) +end + +function Bbcode.remove_html(text) + local ends_with_newline = ends_with(text,"\n") + local t = {} + local html = html_parse(text) + local is_first = true + for _, v in ipairs(html) do + if type(v) == "string" then + t[#t+1] = v + else + local name = v.name + if name == "div" then + if not is_first then + t[#t+1] = "\n" + end + elseif name == "/div" or name == "br" then + -- ignore + else + error("unexpected tag: "..name) + end + end + is_first = false + end + if not ends_with_newline then + t[#t+1] = "\n" + end + return concat(t) +end + + +return Bbcode
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/bbcode/bbcode.css Thu Nov 10 23:18:58 2022 -0700 @@ -0,0 +1,13 @@ +div[bbcode] * { + box-sizing: border-box; +} +div[bbcode] textarea { + font: inherit; + padding: 7px; + border-color: #DDDDDD; + width: 100%; +} + +div[bbcode] input[type="file"] { + display: none; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/bbcode/bbcode.js Thu Nov 10 23:18:58 2022 -0700 @@ -0,0 +1,87 @@ +function fixTextarea(textarea) { + let height = textarea.scrollHeight; + if( height > textarea.clientHeight ) { + textarea.style.height = (height+2) + "px"; + } +} + +function fileButtonClick(button) { + button.parentNode.querySelector('input[type="file"]').click(); +} + +function upload(input,callback) { + let file = input.files[0]; + input.value = null; + let request = new XMLHttpRequest(); + let url = 'https://upload.uploadcare.com/base/'; + request.open( 'POST', url ); + request.onload = function() { + if( request.status !== 200 ) { + let err = 'ajax failed: ' + request.status; + if( request.responseText ) { + err += '\n' + request.responseText; + document.write('<pre>'+request.responseText+'</pre>'); + } + console.log(err); + ajax( '/error_log.js', 'err='+encodeURIComponent(err) ); + return; + } + let response = JSON.parse(request.responseText); + let filename = file.name; + let url = 'https://ucarecdn.com/' + response.file + '/' + filename; + callback(input,url,filename); + }; + let formData = new FormData(); + formData.append( 'UPLOADCARE_PUB_KEY', 'fe3d30f3088a50941d45' ); + formData.append( 'file', file ); + request.send(formData); +} + +function getBbcodeDiv(node) { + do { + //console.log(node); + if( node.getAttribute('bbcode') !== null ) + return node; + } while( node = node.parentNode ); +} + +function uploaded(input,url,filename) { + let div = getBbcodeDiv(input); + let textarea = div.querySelector('textarea'); + textarea.focus(); + textarea.setRangeText(url,textarea.selectionStart,textarea.selectionEnd,'select'); +} + +function bbcodeCreate(div,options) { + if( typeof(div) === 'string' ) + div = document.querySelector(div); + let content = options.content || ''; + let save = options.save; + let cancel = options.cancel; + let html = `\ + <div bbcode> + <textarea oninput="fixTextarea(this)"></textarea> + <p> + <input type=file onchange="upload(this,uploaded)"> + <button type=button onclick="fileButtonClick(this)">Upload File</button> +` ; + if(save) { + html += `\ + <button type=button save>save</button> +` ; + } + if(cancel) { + html += `\ + <button type=button cancel>cancel</button> +` ; + } + html += `\ + </p> + </div> +` ; + div.innerHTML = html; + if(save) + div.querySelector('button[save]').addEventListener('click',save); + if(cancel) + div.querySelector('button[cancel]').addEventListener('click',cancel); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/bbcode/test.html Thu Nov 10 23:18:58 2022 -0700 @@ -0,0 +1,54 @@ +<!doctype html> +<html> + <head> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <style> + @import "bbcode.css"; + + [message] { + white-space: pre-wrap; + line-height: 1.4; + } + </style> + <script src="bbcode.js"></script> + <script> + // from /site.js + function ajax(url,postData) { + let request = new XMLHttpRequest(); + let method = postData ? 'POST' : 'GET'; + request.open( method, url ); + if( postData ) + request.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' ); + request.onload = function() { + if( request.status !== 200 ) { + window.console && console.log( 'ajax failed: ' + request.status ); + if( request.responseText ) + document.write('<pre>'+request.responseText+'</pre>'); + return; + } + eval( request.responseText ); + }; + request.send(postData); + } + + function save(event) { + let div = getBbcodeDiv(event.target); + let text = div.querySelector('textarea').value; + ajax( 'test.js?text=' + encodeURIComponent(text) ); + } + + function init() { + bbcodeCreate('p[edit]',{ + save: save + }); + } + </script> + </head> + <body onload='init()'> + <p>top</p> + <p edit></p> + <p>result:</p> + <p result><div message></div></p> + <p>bottom</p> + </body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/bbcode/test.js.luan Thu Nov 10 23:18:58 2022 -0700 @@ -0,0 +1,19 @@ +local Luan = require "luan:Luan.luan" +local error = Luan.error +local Parsers = require "luan:Parsers.luan" +local json_string = Parsers.json_string or error() +local Io = require "luan:Io.luan" +local output_of = Io.output_of or error() +local Http = require "luan:http/Http.luan" +local Bbcode = require "site:/bbcode/Bbcode.luan" +local bbcode_to_html = Bbcode.to_html or error() + + +return function() + local text = Http.request.parameters.text or error() + local html = output_of(function() bbcode_to_html(text) end) + Io.stdout = Http.response.text_writer() +%> + document.querySelector('p[result]').innerHTML = <%= json_string(html) %>; +<% +end
--- a/src/lib/Bbcode.luan Wed Nov 09 23:05:01 2022 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,338 +0,0 @@ -local Luan = require "luan:Luan.luan" -local error = Luan.error -local type = Luan.type or error() -local ipairs = Luan.ipairs or error() -local pairs = Luan.pairs or error() -local stringify = Luan.stringify or error() -local Io = require "luan:Io.luan" -local output_of = Io.output_of or error() -local Parsers = require "luan:Parsers.luan" -local bbcode_parse = Parsers.bbcode_parse or error() -local Html = require "luan:Html.luan" -local html_encode = Html.encode or error() -local html_parse = Html.parse or error() -local Table = require "luan:Table.luan" -local is_list = Table.is_list or error() -local concat = Table.concat or error() -local String = require "luan:String.luan" -local regex = String.regex or error() -local ends_with = String.ends_with or error() -local User = require "site:/lib/User.luan" -local Shared = require "site:/lib/Shared.luan" -local list_to_set = Shared.list_to_set or error() -local to_list = Shared.to_list or error() -local Logging = require "luan:logging/Logging.luan" -local logger = Logging.logger "Bbcode" - - -local Bbcode = {} - -local starting_cr_regex = regex[[^\n]] - -local to_html -local html = {} - -function html.b(bbcode,options) - %><b><% to_html(bbcode.contents,options) %></b><% -end - -function html.i(bbcode,options) - %><i><% to_html(bbcode.contents,options) %></i><% -end - -function html.u(bbcode,options) - %><u><% to_html(bbcode.contents,options) %></u><% -end - -function html.s(bbcode,options) - %><s><% to_html(bbcode.contents,options) %></s><% -end - -function html.sup(bbcode,options) - %><sup><% to_html(bbcode.contents,options) %></sup><% -end - -function html.brackets(bbcode,options) - %>[<% to_html(bbcode.contents,options) %>]<% -end - -function html.url(bbcode,options) - local url = bbcode.param - if url == nil then - url = html_encode(bbcode.contents) - %><a href="<%=url%>"><%=url%></a><% - else - url = html_encode(url) - %><a href="<%=url%>"><% to_html(bbcode.contents,options) %></a><% - end -end - -function html.code(bbcode,options) - local s = starting_cr_regex.gsub(bbcode.contents,"") - %><code><%= html_encode(s) %></code><% - options.strip_newline = true -end - -function html.img(bbcode,options) - %><img src="<%= html_encode(bbcode.contents) %>"><% -end - -function html.color(bbcode,options) - %><span style="color:<%=bbcode.param%>"><% to_html(bbcode.contents,options) %></span><% -end - -function html.size(bbcode,options) - %><span style="font-size:<%=bbcode.param%>"><% to_html(bbcode.contents,options) %></span><% -end - -function html.quote(bbcode,options) - %><blockquote><% - local user_name = bbcode.param - if user_name ~= nil then - local user = User.get_by_name(user_name) - if user == nil then - %><%= user_name %> wrote:<% - else - %><a href="/user_something"><%= user_name %></a> wrote:<% - end - else - options.strip_newline = true - end - to_html(bbcode.contents,options) - %></blockquote><% - options.strip_newline = true -end - -local function video_iframe(url) - %><iframe width="560" height="315" frameborder="0" allowfullscreen src="<%=url%>"></iframe><% -end - -local video_handlers = {} -do - local ptn1 = regex[[^\Qhttps://youtu.be/\E([a-zA-Z0-9_-]+)(?:\?t=([0-9]+))?]] - local ptn2 = regex[[^\Qhttps://www.youtube.com/watch?v=\E([a-zA-Z0-9_-]+)(?:&t=([0-9]+)s)?]] - function video_handlers.youtube(url) - local id, start = ptn1.match(url) - if id == nil then - id, start = ptn2.match(url) - end - if id == nil then - return false - end - url = "https://www.youtube.com/embed/"..id - if start ~= nil then - url = url.."?start="..start - end - video_iframe(url) - return true - end -end -do - local ptn = regex[[^\Qhttps://rumble.com/embed/\E[a-z0-9]+/\?pub=[a-z0-9]+]] - function video_handlers.rumble(url) - if not ptn.matches(url) then - return false - end - video_iframe(url) - return true - end -end -do - local ptn = regex[[^\Qhttps://www.bitchute.com/video/\E([a-zA-Z0-9]+)/]] - function video_handlers.bitchute(url) - local id = ptn.match(url) - if id == nil then - return false - end - url = "https://www.bitchute.com/embed/"..id.."/" - video_iframe(url) - return true - end -end -do - local ptn = regex[[^\Qhttps://vimeo.com/\E([0-9]+)]] - function video_handlers.vimeo(url) - local id = ptn.match(url) - if id == nil then - return false - end - url = "https://player.vimeo.com/video/"..id - video_iframe(url) - return true - end -end -do - local ptn = regex[[^\Qhttps://dai.ly/\E([a-z0-9]+)]] - function video_handlers.dailymotion(url) - local id = ptn.match(url) - if id == nil then - return false - end - url = "https://www.dailymotion.com/embed/video/"..id - video_iframe(url) - return true - end -end -do - local ptn = regex[[^\Qhttps://www.tiktok.com/\E[^/]+/video/([0-9]+)]] - function video_handlers.tiktok(url) - local id = ptn.match(url) - if id == nil then - return false - end - %><blockquote class="tiktok-embed" data-video-id="<%=id%>" style="max-width: 560px; margin-left: 0;"><section></section></blockquote><% - %><script async src="https://www.tiktok.com/embed.js"></script><% - return true - end -end -do - local ptn = regex[[\.[a-zA-Z0-9]+$]] - function video_handlers.file(url) - if not ptn.matches(url) then - return false - end - %><video controls width="560"><source src="<%=html_encode(url)%>"></video><% - return true - end -end - -function html.video(bbcode,options) - local url = bbcode.contents - for _, handle in pairs(video_handlers) do - if handle(url) then return end - end - url = html_encode(url) - %><a href="<%=url%>"><%=url%></a><% -end - -local function list_to_html(bbcode,options) - local list = to_list(bbcode.contents) - for _, item in ipairs(list) do - if type(item) == "table" and item.name == "li" then - %><li><% to_html(item.contents,options) %></li><% - end - end - options.strip_newline = true -end - -function html.ul(bbcode,options) - %><ul><% - list_to_html(bbcode,options) - %></ul><% -end - -function html.ol(bbcode,options) - %><ol><% - list_to_html(bbcode,options) - %></ol><% -end - -function to_html(bbcode,options) - if options.strip_newline then - if type(bbcode) == "string" then - bbcode = starting_cr_regex.gsub(bbcode,"") - end - options.strip_newline = false - end - if type(bbcode) == "string" then - %><%= html_encode(bbcode) %><% - else - type(bbcode) == "table" or error() - if is_list(bbcode) then - for _, v in ipairs(bbcode) do - to_html(v,options) - end - else - local fn = html[bbcode.name] or error(bbcode.name.." not handled") - fn(bbcode,options) - end - end -end - -function Bbcode.to_html(bbcode) - bbcode = bbcode_parse(bbcode) - %><div message><% - to_html(bbcode,{strip_newline=false}) - %></div><% -end - - -local doesnt_nest = list_to_set{ - "url" - "code" - "img" - "video" -} - -local url_regex = regex[[(^|\s)(https?://\S+)]] - -local function preprocess(bbcode) - if type(bbcode) == "string" then - bbcode = url_regex.gsub( bbcode, "$1[url]$2[/url]" ) - %><%= bbcode %><% - else - type(bbcode) == "table" or error() - if is_list(bbcode) then - for _, v in ipairs(bbcode) do - preprocess(v) - end - else - local name = bbcode.name - local param = bbcode.param - %>[<%=name%><% - if param ~= nil then - %>=<%=param%><% - end - %>]<% - if doesnt_nest[name] then - %><%=bbcode.contents%><% - else - preprocess(bbcode.contents) - end - if name == "code" and param ~= nil then - %>[/<%=name%>=<%=param%>]<% - else - %>[/<%=name%>]<% - end - end - end -end - --- url handling -function Bbcode.preprocess(bbcode) - bbcode = bbcode_parse(bbcode) - return output_of(function() - preprocess(bbcode) - end) -end - -function Bbcode.remove_html(text) - local ends_with_newline = ends_with(text,"\n") - local t = {} - local html = html_parse(text) - local is_first = true - for _, v in ipairs(html) do - if type(v) == "string" then - t[#t+1] = v - else - local name = v.name - if name == "div" then - if not is_first then - t[#t+1] = "\n" - end - elseif name == "/div" or name == "br" then - -- ignore - else - error("unexpected tag: "..name) - end - end - is_first = false - end - if not ends_with_newline then - t[#t+1] = "\n" - end - return concat(t) -end - - -return Bbcode
--- a/src/lib/Shared.luan Wed Nov 09 23:05:01 2022 -0700 +++ b/src/lib/Shared.luan Thu Nov 10 23:18:58 2022 -0700 @@ -104,7 +104,7 @@ Shared.delete_post = delete_post function Shared.show_post(post,now) - local Bbcode = require "site:/lib/Bbcode.luan" + local Bbcode = require "site:/bbcode/Bbcode.luan" local bbcode_to_html = Bbcode.to_html or error() %> <div post="<%=post.id%>">
--- a/src/save_edit.js.luan Wed Nov 09 23:05:01 2022 -0700 +++ b/src/save_edit.js.luan Thu Nov 10 23:18:58 2022 -0700 @@ -6,7 +6,7 @@ local output_of = Io.output_of or error() local Http = require "luan:http/Http.luan" local Post = require "site:/lib/Post.luan" -local Bbcode = require "site:/lib/Bbcode.luan" +local Bbcode = require "site:/bbcode/Bbcode.luan" local bbcode_to_html = Bbcode.to_html or error() local Db = require "site:/lib/Db.luan" local Shared = require "site:/lib/Shared.luan"
--- a/src/thread.html.luan Wed Nov 09 23:05:01 2022 -0700 +++ b/src/thread.html.luan Thu Nov 10 23:18:58 2022 -0700 @@ -17,8 +17,6 @@ local forum_title = Forum.title or error() local Db = require "site:/lib/Db.luan" local Post = require "site:/lib/Post.luan" -local Bbcode = require "site:/lib/Bbcode.luan" -local bbcode_to_html = Bbcode.to_html or error() local User = require "site:/lib/User.luan"