dev
Documentation icon Module documentation
[create]

The documentation for this module is missing. Click here to create it.

-- Version:	1.2
-- License:	MIT
-- Author:	t7ru [[User:Gabonnie]]
local _modules = {}
local _base_require = require
local function require(name)
    if _modules[name] then return _modules[name] end
    return _base_require(name)
end

_modules['01_header'] = (function()
local header = {}

header.PUNCT = {}
for c in string.gmatch('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~', ".") do
	header.PUNCT[c] = true
end

local ustringLower = (type(mw) == "table" and mw.ustring and mw.ustring.lower)
	or string.lower

local mwTrim = mw.text.trim

function header.normalizeLabel(lbl)
	return ustringLower(mwTrim(lbl):gsub("[ \t\r\n]+", " "))
end

return header
end)()

_modules['02_utils'] = (function()
local utils = {}

function utils.indentOf(s)
	local n = 0
	for i = 1, #s do
		local c = s:sub(i, i)
		if c == " " then
			n = n + 1
		elseif c == "\t" then
			n = n + (4 - n % 4)
		else
			break
		end
	end
	return n
end

function utils.stripN(s, n)
	local gone, i = 0, 1
	while i <= #s and gone < n do
		local c = s:sub(i, i)
		if c == " " then
			gone = gone + 1; i = i + 1
		elseif c == "\t" then
			local step = 4 - gone % 4
			if gone + step <= n then
				gone = gone + step; i = i + 1
			else
				return string.rep(" ", n - gone) .. s:sub(i + 1)
			end
		else
			break
		end
	end
	return s:sub(i)
end

function utils.colWidth(s)
	local n = 0
	for k = 1, #s do
		local c = s:sub(k, k)
		if c == "\t" then n = n + (4 - n % 4) else n = n + 1 end
	end
	return n
end

function utils.isThematicBreak(line)
	if utils.indentOf(line) >= 4 then return false end
	local s = line:gsub("[ \t]", "")
	if #s < 3 then return false end
	local c = s:sub(1, 1)
	if c ~= "-" and c ~= "*" and c ~= "_" then return false end
	for k = 2, #s do
		if s:sub(k, k) ~= c then return false end
	end
	return true
end

function utils.flanking(s, b, e, PUNCT)
	local bef                = b > 1 and s:sub(b - 1, b - 1) or " "
	local aft                = e < #s and s:sub(e + 1, e + 1) or " "
	local befSpace           = bef == " " or bef == "\t" or bef == "\n" or bef == "\r"
	local aftSpace           = aft == " " or aft == "\t" or aft == "\n" or aft == "\r"
	local befPunct, aftPunct = PUNCT[bef], PUNCT[aft]
	local left               = (not aftSpace) and (not aftPunct or befSpace or befPunct)
	local right              = (not befSpace) and (not befPunct or aftSpace or aftPunct)
	return left, right
end

return utils
end)()

_modules['03_inlines'] = (function()
local header                     = require('01_header')
local utils                      = require('02_utils')

local PUNCT                      = header.PUNCT
local normalizeLabel             = header.normalizeLabel
local flanking                   = utils.flanking

local sFind, sGsub, sMatch, sSub =
	string.find, string.gsub, string.match, string.sub
local tConcat, tInsert           = table.concat, table.insert

local inlines                    = {}

function inlines.parseInlines(s, refs)
	local toks = {}
	local function protect(x)
		tInsert(toks, x)
		return "\127MDTK" .. #toks .. "\127"
	end

	-- §6.1 Code spans
	do
		local out, i, n = {}, 1, #s
		while i <= n do
			local b = sFind(s, "`", i, true)
			if not b then
				tInsert(out, sSub(s, i)); break
			end
			if b > i then tInsert(out, sSub(s, i, b - 1)) end
			local e = b
			while e < n and sSub(s, e + 1, e + 1) == "`" do e = e + 1 end
			local rlen = e - b + 1
			local j, found = e + 1, false
			while j <= n do
				local cb = sFind(s, "`", j, true)
				if not cb then break end
				local ce = cb
				while ce < n and sSub(s, ce + 1, ce + 1) == "`" do ce = ce + 1 end
				if ce - cb + 1 == rlen then
					local cont = sGsub(sSub(s, e + 1, cb - 1), "\n", " ")
					if sMatch(cont, "^ ") and sMatch(cont, " $")
						and sFind(cont, "[^ ]") then
						cont = sSub(cont, 2, -2)
					end
					tInsert(out, protect("<code>" .. mw.text.nowiki(cont) .. "</code>"))
					i = ce + 1; found = true; break
				end
				j = ce + 1
			end
			if not found then
				tInsert(out, sSub(s, b, e)); i = e + 1
			end
		end
		s = tConcat(out)
	end

	-- §6.5 Autolinks
	s = sGsub(s, "<([a-zA-Z][a-zA-Z0-9+%.%-]+:[^%s<>]*)>",
		function(u) return protect("[" .. u .. " " .. u .. "]") end)
	s = sGsub(s,
		"<([a-zA-Z0-9%.!#$%%&'%*%+/=%?%^_`{|}~%-]+@[a-zA-Z0-9][a-zA-Z0-9%-%.]*%.[a-zA-Z][a-zA-Z0-9%-]*)>",
		function(em) return protect("[mailto:" .. em .. " " .. em .. "]") end)

	-- §2.4 Backslash escapes
	s = sGsub(s, "\\([!\"#$%%&'()*+,%-./\\:;<=>?@%[%]^_`{|}~])",
		function(c) return protect(c) end)

	-- §6.7 Hard line breaks
	s = sGsub(s, "\\\n", protect("<br />\n"))
	s = sGsub(s, "  +\n", protect("<br />\n"))

	-- §6.4 Images
	s = sGsub(s, "!%[(.-)%]%s*%[([^%]]*)%]", function(alt, lbl)
		local ref = refs[normalizeLabel(lbl == "" and alt or lbl)]
		if ref then return protect("[[File:" .. ref.url .. "|alt=" .. alt .. "]]") end
	end)
	s = sGsub(s, '!%[(.-)%]%(([^%s)"\'<>]+)[ \t]+"[^"]*"[ \t]*%)',
		function(alt, url) return protect("[[File:" .. url .. "|alt=" .. alt .. "]]") end)
	s = sGsub(s, "!%[(.-)%]%(([^%s)'\"<>]+)[ \t]+'[^']*'[ \t]*%)",
		function(alt, url) return protect("[[File:" .. url .. "|alt=" .. alt .. "]]") end)
	s = sGsub(s, "!%[(.-)%]%(<([^>]*)>[ \t]*%)",
		function(alt, url) return protect("[[File:" .. url .. "|alt=" .. alt .. "]]") end)
	s = sGsub(s, "!%[(.-)%]%(([^%s)\"'<>]*)[ \t]*%)",
		function(alt, url) return protect("[[File:" .. url .. "|alt=" .. alt .. "]]") end)
	s = sGsub(s, "!%[([^%]%[]+)%]", function(alt)
		local ref = refs[normalizeLabel(alt)]
		if ref then return protect("[[File:" .. ref.url .. "|alt=" .. alt .. "]]") end
	end)

	local function formatLink(text, url)
		if sMatch(url, "^%a+:") or sMatch(url, "^//") then
			return protect("[" .. url .. " " .. text .. "]")
		else
			return protect("[[" .. url .. "|" .. text .. "]]")
		end
	end

	-- §6.3 Links
	s = sGsub(s, "%[(.-)%]%s*%[([^%]]+)%]", function(text, lbl)
		local ref = refs[normalizeLabel(lbl)]
		if ref then return formatLink(text, ref.url) end
	end)
	s = sGsub(s, "%[([^%]%[]+)%]%s*%[%]", function(text)
		local ref = refs[normalizeLabel(text)]
		if ref then return formatLink(text, ref.url) end
	end)
	s = sGsub(s, "%[(.-)%]%(<([^>%s]*)>[ \t]*%)",
		function(text, url) return formatLink(text, url) end)
	s = sGsub(s, '%[(.-)%]%(([^%s)"\'<>]+)[ \t]+"[^"]*"[ \t]*%)',
		function(text, url) return formatLink(text, url) end)
	s = sGsub(s, "%[(.-)%]%(([^%s)'\"<>]+)[ \t]+'[^']*'[ \t]*%)",
		function(text, url) return formatLink(text, url) end)
	s = sGsub(s, "%[(.-)%]%(([^%s)\"'<>]*)[ \t]*%)",
		function(text, url) return formatLink(text, url) end)
	s = sGsub(s, "%[([^%]%[]+)%]", function(text)
		local ref = refs[normalizeLabel(text)]
		if ref then return formatLink(text, ref.url) end
	end)

	-- §6.2 Emphasis and strong emphasis (frickin' delimiter stack algorithm)
	do
		local runs = {}
		local i, n = 1, #s
		while i <= n do
			local c = sSub(s, i, i)
			if c == "*" or c == "_" then
				local j = i
				while j < n and sSub(s, j + 1, j + 1) == c do j = j + 1 end
				local left, right = flanking(s, i, j, PUNCT)
				local canOpen, canClose
				if c == "*" then
					canOpen = left; canClose = right
				else
					local bef = i > 1 and sSub(s, i - 1, i - 1) or " "
					local aft = j < n and sSub(s, j + 1, j + 1) or " "
					canOpen   = left and (not right or PUNCT[bef])
					canClose  = right and (not left or PUNCT[aft])
				end
				tInsert(runs, {
					s = i,
					e = j,
					orig = j - i + 1,
					char = c,
					canOpen = canOpen,
					canClose = canClose,
					lc = 0,
					rc = 0
				})
				i = j + 1
			else
				i = i + 1
			end
		end

		local inserts = {}
		local function ins(pos, tag)
			if not inserts[pos] then inserts[pos] = {} end
			tInsert(inserts[pos], tag)
		end

		local ob = {}
		local ci = 1
		while ci <= #runs do
			local r   = runs[ci]
			local avr = r.orig - r.lc - r.rc
			if r.canClose and avr > 0 then
				local base = ob[r.char] or 0
				local foi  = nil
				for oi = ci - 1, base + 1, -1 do
					local op = runs[oi]
					if op.char == r.char and op.canOpen
						and (op.orig - op.lc - op.rc) > 0 then
						local ok = true
						if op.canClose or r.canOpen then
							local sum = op.orig + r.orig
							if sum % 3 == 0
								and not (op.orig % 3 == 0 and r.orig % 3 == 0) then
								ok = false
							end
						end
						if ok then
							foi = oi; break
						end
					end
				end
				if foi then
					local op  = runs[foi]
					local avo = op.orig - op.lc - op.rc
					avr       = r.orig - r.lc - r.rc
					local use = (avo >= 2 and avr >= 2) and 2 or 1
					local tag = use == 2 and "'''" or "''"
					ins(op.e - op.rc - use + 1, tag)
					op.rc = op.rc + use
					ins(r.s + r.lc, tag)
					r.lc = r.lc + use
				else
					ob[r.char] = ci - 1
					ci = ci + 1
				end
			else
				ci = ci + 1
			end
		end

		local dead = {}
		for _, run in ipairs(runs) do
			for k = run.s, run.s + run.lc - 1 do dead[k] = true end
			for k = run.e - run.rc + 1, run.e do dead[k] = true end
		end

		local out = {}
		for pos = 1, #s do
			if inserts[pos] then
				for _, tag in ipairs(inserts[pos]) do tInsert(out, tag) end
			end
			if not dead[pos] then tInsert(out, sSub(s, pos, pos)) end
		end
		if inserts[#s + 1] then
			for _, tag in ipairs(inserts[#s + 1]) do tInsert(out, tag) end
		end
		s = tConcat(out)
	end

	s = sGsub(s, "'''?", protect)

	local escaped = {}
	local last = 1
	for ts, id, te in s:gmatch("()\127MDTK(%d+)\127()") do
		tInsert(escaped, mw.text.nowiki(sSub(s, last, ts - 1)))
		tInsert(escaped, toks[tonumber(id)])
		last = te
	end
	tInsert(escaped, mw.text.nowiki(sSub(s, last)))
	return tConcat(escaped)
end

return inlines
end)()

_modules['04_blocks'] = (function()
local header                           = require('01_header')
local utils                            = require('02_utils')
local inlines                          = require('03_inlines')

local normalizeLabel                   = header.normalizeLabel
local indentOf                         = utils.indentOf
local stripN                           = utils.stripN
local colWidth                         = utils.colWidth
local isThematicBreak                  = utils.isThematicBreak
local parseInlines                     = inlines.parseInlines

local sFind, sGsub, sMatch, sRep, sSub =
	string.find, string.gsub, string.match, string.rep, string.sub
local tConcat, tInsert, tRemove        =
	table.concat, table.insert, table.remove

local mwTrim                           = mw.text.trim
local mwSplit                          = mw.text.split -- could be worth replacing: https://w.wiki/JzBY

local function itemHeader(l)
	local pre, bull = sMatch(l, "^([ \t]*)([%-%+%*])[ \t]")
	if bull then
		local rest = sSub(l, #pre + 3)
		return "bullet", bull, nil, colWidth(pre) + 2, rest
	end
	local pre2, num, delim = sMatch(l, "^([ \t]*)(%d%d?%d?%d?%d?%d?%d?%d?%d?)([%.%)])[ \t]")
	if num then
		local rest = sSub(l, #pre2 + #num + 3)
		return "ordered", delim, tonumber(num), colWidth(pre2) + #num + 2, rest
	end
end

local p = {}

function p.parse(text, frame)
	frame = frame or mw.getCurrentFrame()

	text = mw.text.unstripNoWiki(text)
	text = sGsub(text, "\r\n?", "\n")
	text = sGsub(text, "%z", "") -- §2.3 Insecure characters (U+0000)

	local refs = {}

	-- §4.7 Link reference definitions
	local function absorb(prefix, lbl, url, title)
		local k = normalizeLabel(lbl)
		if not refs[k] then refs[k] = { url = url, title = title or "" } end
		return "\n" .. (prefix or "")
	end

	local t = "\n" .. text .. "\n"

	-- 3-line, 2-line, and 1-line definitions with titles
	t = sGsub(t,
		'\n([ \t]*>?[ \t]*)%[([^%]]+)%]:[ \t]*\n?[ \t]*>?[ \t]*([^%s\n]+)[ \t]*\n?[ \t]*>?[ \t]*["\']([^"\'\n]*)["\'][ \t]*\n',
		function(pfx, l, u, ti) return absorb(pfx, l, u, ti) end)
	t = sGsub(t,
		'\n([ \t]*>?[ \t]*)%[([^%]]+)%]:[ \t]*\n?[ \t]*>?[ \t]*([^%s\n]+)[ \t]*\n?[ \t]*>?[ \t]*%(([^)\n]*)%)[ \t]*\n',
		function(pfx, l, u, ti) return absorb(pfx, l, u, ti) end)

	-- 2-line and 1-line definitions without titles
	t = sGsub(t, '\n([ \t]*>?[ \t]*)%[([^%]]+)%]:[ \t]*\n?[ \t]*>?[ \t]*([^%s\n]+)[ \t]*\n',
		function(pfx, l, u) return absorb(pfx, l, u, "") end)

	text = sSub(t, 2, -2)

	local lines = mwSplit(text, "\n")
	local blocks = {}
	local i, n = 1, #lines
	local inFence = false
	local fChar, fMin, fLang, fInd, fBuf = "", 0, "text", 0, {}

	while i <= n do
		local line = lines[i]
		local ind = indentOf(line)

		-- §4.5 Fenced code blocks (content lines)
		if inFence then
			local fc = sMatch(line, "^[ \t]*([`~]+)[ \t]*$")
			if fc and sSub(fc, 1, 1) == fChar and #fc >= fMin then
				inFence = false
				tInsert(blocks, frame:extensionTag('syntaxhighlight', tConcat(fBuf, "\n"), { lang = fLang }))
				fBuf = {}
			else
				tInsert(fBuf, stripN(line, fInd))
			end
			i = i + 1

			-- §4.9 Blank lines
		elseif mwTrim(line) == "" then
			if #blocks > 0 and blocks[#blocks] ~= "" then
				tInsert(blocks, "")
			end
			i = i + 1
		else
			local atxHh       = ind < 4 and sMatch(line, "^[ \t]*(#+)") or nil
			local atxRest     = atxHh and sMatch(line, "^[ \t]*#+(.*)")
			local isAtx       = atxHh and #atxHh <= 6 and
				(atxRest == "" or sSub(atxRest, 1, 1) == " " or sSub(atxRest, 1, 1) == "\t")

			-- §4.5 Opening fenced code blocks
			local fenceMark   = ind < 4 and sMatch(line, "^[ \t]*([`~][%`~][%`~]+)") or nil
			local isFenceOpen = false

			if fenceMark then
				local char = sSub(fenceMark, 1, 1)
				local fInfo = mwTrim(sMatch(line, "^[ \t]*[`~]+(.*)") or "")
				if not (char == "`" and sFind(fInfo, "`", 1, true)) then
					isFenceOpen = true
					fLang = mwTrim(sMatch(fInfo, "^[^ \t]+") or "")
					if fLang == "" then fLang = "text" end
				end
			end

			if isFenceOpen then
				fChar   = sSub(fenceMark, 1, 1)
				fMin    = #fenceMark
				fInd    = ind
				inFence = true; fBuf = {}
				i       = i + 1

				-- §4.2 ATX headings
			elseif isAtx then
				local body = mwTrim(atxRest)
				body = sGsub(body, "[ \t]+#+[ \t]*$", "")
				body = mwTrim(body)
				local eq = sRep("=", #atxHh)
				if body ~= "" then
					tInsert(blocks, eq .. " " .. parseInlines(body, refs) .. " " .. eq)
				else
					tInsert(blocks, eq .. " " .. eq)
				end
				i = i + 1

				-- §4.1 Thematic breaks
			elseif isThematicBreak(line) then
				tInsert(blocks, "----")
				i = i + 1

				-- §5.1 Block quotes
			elseif ind < 4 and sMatch(line, "^[ \t]*>") then
				local bq = {}
				local bqTypes = {}
				while i <= n do
					local cur = lines[i]
					local curInd = indentOf(cur)
					if curInd < 4 and sMatch(cur, "^[ \t]*>") then
						tInsert(bq, (sGsub(cur, "^[ \t]*>[ \t]?", "")))
						tInsert(bqTypes, "marker")
						i = i + 1
					elseif mwTrim(cur) ~= "" and curInd < 4 and not isThematicBreak(cur)
						and not sMatch(cur, "^[ \t]*[`~][%`~][%`~]+")
						and not sMatch(cur, "^[ \t]*[%-%+%*][ \t]")
						and not sMatch(cur, "^[ \t]*%d+[%.%)][ \t]")
						and not sMatch(cur, "^[ \t]*#") then
						tInsert(bq, cur)
						tInsert(bqTypes, "lazy")
						i = i + 1
					else
						break
					end
				end
				while #bq > 0 and mwTrim(bq[#bq]) == "" do
					tRemove(bq)
					tRemove(bqTypes)
				end

				local processedBq = {}
				for idx = 1, #bq do
					if idx > 1 and bqTypes[idx] == "lazy" then
						processedBq[#processedBq] = processedBq[#processedBq] .. " " .. bq[idx]
					else
						tInsert(processedBq, bq[idx])
					end
				end

				local bqHtml = mw.html.create('blockquote')
				bqHtml:wikitext(p.parse(tConcat(processedBq, "\n"), frame))
				tInsert(blocks, tostring(bqHtml))

				-- §5.2 / §5.3 List items and lists
			elseif sMatch(line, "^[ \t]*[%-%+%*][ \t]") or sMatch(line, "^[ \t]*%d+[%.%)][ \t]") then
				local ltype, lmarker, startNum = itemHeader(line)
				local items = {}
				local loose = false

				while i <= n do
					local cur = lines[i]
					if mwTrim(cur) == "" then break end

					local ctype, cmarker, cnum, ccol, firstContent = itemHeader(cur)
					if not ctype or ctype ~= ltype or cmarker ~= lmarker then break end

					i = i + 1
					local ilines = { firstContent }

					while i <= n do
						local nl = lines[i]
						if mwTrim(nl) == "" then
							local j = i + 1
							while j <= n and mwTrim(lines[j]) == "" do j = j + 1 end

							if j > n then
								i = j; break
							end

							local ntype, nmarker = itemHeader(lines[j])
							local nextInd        = indentOf(lines[j])

							if nextInd >= ccol then
								loose = true
								tInsert(ilines, "")
								i = j
							elseif ntype and ntype == ltype and nmarker == lmarker then
								loose = true
								i = j; break
							else
								i = j; break
							end
						else
							local nextInd = indentOf(nl)
							if nextInd >= ccol then
								tInsert(ilines, stripN(nl, ccol))
								i = i + 1
							elseif not itemHeader(nl)
								and not sMatch(nl, "^[ \t]*>")
								and not isThematicBreak(nl)
								and not sMatch(nl, "^[ \t]*[`~][`~][`~]+")
								and not sMatch(nl, "^[ \t]*#+[ \t]") then
								tInsert(ilines, nl)
								i = i + 1
							else
								break
							end
						end
					end

					tInsert(items, { lines = ilines, num = cnum })
				end

				local listEl = mw.html.create(ltype == "ordered" and "ol" or "ul")
				if ltype == "ordered" and startNum and startNum ~= 1 then
					listEl:attr("start", tostring(startNum))
				end

				for _, item in ipairs(items) do
					local li  = listEl:tag("li")
					local raw = tConcat(item.lines, "\n")
					local content

					if not loose and #item.lines == 1 then
						content = parseInlines(raw, refs)
					else
						content = mwTrim(p.parse(raw, frame))
						if loose then
							local parts = {}
							for block in (content .. "\n\n"):gmatch("(.-)\n\n") do
								block = mwTrim(block)
								if block ~= "" then
									if sMatch(block, "^<") then
										tInsert(parts, block)
									else
										tInsert(parts, "<p>" .. block .. "</p>")
									end
								end
							end
							content = tConcat(parts, "\n")
						else
							content = sGsub(content, "^<p>(.-)</p>$", "%1")
						end
					end

					li:wikitext(content)
				end

				tInsert(blocks, tostring(listEl))

				-- §4.4 Indented code blocks
			elseif ind >= 4 then
				local cBuf = {}
				while i <= n do
					local cl = lines[i]
					if indentOf(cl) >= 4 then
						tInsert(cBuf, stripN(cl, 4)); i = i + 1
					elseif mwTrim(cl) == "" then
						tInsert(cBuf, ""); i = i + 1
					else
						break
					end
				end
				while #cBuf > 0 and mwTrim(cBuf[#cBuf]) == "" do tRemove(cBuf) end
				if #cBuf > 0 then
					tInsert(blocks, frame:extensionTag('syntaxhighlight', tConcat(cBuf, "\n"), { lang = "text" }))
				end

				-- §4.8 Paragraphs / §4.3 Setext headings
			else
				local para   = {}
				local setext = nil

				while i <= n do
					local cur = lines[i]
					if mwTrim(cur) == "" then break end

					local ct  = mwTrim(cur)
					local cid = indentOf(cur)

					-- §4.3 Setext heading underline check
					if #para > 0 then
						if cid < 4 and sMatch(ct, "^=+$") then
							setext = "h1"; i = i + 1; break
						elseif cid < 4 and sMatch(ct, "^%-%-+$") then
							setext = "h2"; i = i + 1; break
						end
					end

					if #para > 0 then
						if cid < 4 and sMatch(cur, "^[ \t]*[`~][%`~][%`~]+") then break end
						if cid < 4 then
							local hh = sMatch(cur, "^[ \t]*(#+)")
							local hr = hh and sMatch(cur, "^[ \t]*#+(.*)")
							if hh and #hh <= 6 and (hr == "" or sSub(hr, 1, 1) == " "
									or sSub(hr, 1, 1) == "\t") then
								break
							end
						end
						if isThematicBreak(cur) then break end
						if cid < 4 and sMatch(cur, "^[ \t]*>") then break end
						if sMatch(cur, "^[ \t]*[%-%+%*][ \t]") then break end
						if sMatch(cur, "^[ \t]*1[%.%)][ \t]") then break end
					end

					tInsert(para, cur)
					i = i + 1
				end

				if #para > 0 then
					local body = mwTrim(tConcat(para, "\n"))
					if setext then
						body = sGsub(body, "\n", " ")
						if setext == "h1" then
							tInsert(blocks, "= " .. parseInlines(body, refs) .. " =")
						elseif setext == "h2" then
							tInsert(blocks, "== " .. parseInlines(body, refs) .. " ==")
						end
					else
						-- §6.8 Soft line breaks
						tInsert(blocks, parseInlines(body, refs))
					end
				end
			end
		end
	end

	if inFence then
		tInsert(blocks, frame:extensionTag('syntaxhighlight', tConcat(fBuf, "\n"), { lang = fLang }))
	end

	return tConcat(blocks, "\n")
end

function p.main(frame)
	local text = frame.args[1] or frame:getParent().args[1] or ""
	if text == "" then return "" end
	return p.parse(text, frame)
end

return p
end)()

return _modules['04_blocks']