[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']