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"