More actions
Content deleted Content added
// via Wikitext Extension for VSCode |
// via Wikitext Extension for VSCode |
||
(25 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
-- Module:Dice |
|||
-- Processes dice input strings and outputs HTML spans with roll links, templates, and data attributes |
|||
local p = {} |
local p = {} |
||
local html = mw.html |
|||
-- Trim whitespace from both ends |
|||
-- Helper function to trim whitespace |
|||
local function trim(s) |
local function trim(s) |
||
return (s:gsub("^%s*(.-)%s*$", "%1")) |
|||
end |
end |
||
-- Split the full roll string into parts and operators (+, -) |
|||
-- Helper function to escape special characters for URLs |
|||
local function |
local function splitParts(s) |
||
local parts = {} |
|||
return str:gsub("([^%w%-%.%_%~])", function(c) |
|||
local i = 1 |
|||
return string.format("%%%02X", string.byte(c)) |
|||
while i <= #s do |
|||
local c = s:sub(i,i) |
|||
if c == "+" or c == "-" then |
|||
table.insert(parts, c) |
|||
i = i + 1 |
|||
elseif c == " " then |
|||
i = i + 1 |
|||
else |
|||
local j = s:find("[%+%-]", i) |
|||
if j then |
|||
table.insert(parts, trim(s:sub(i, j-1))) |
|||
i = j |
|||
else |
|||
table.insert(parts, trim(s:sub(i))) |
|||
break |
|||
end |
|||
end |
|||
end |
|||
return parts |
|||
end |
end |
||
-- Capitalize first letter |
|||
-- Parse a single die expression and extract components |
|||
local function |
local function capitalize(s) |
||
return (s:gsub("^%l", string.upper)) |
|||
local result = { |
|||
end |
|||
original = expr, |
|||
count = "", |
|||
-- Parse an individual part into prefix, dice, and qualifiers |
|||
sides = "", |
|||
local function parsePart(part) |
|||
modifiers = "", |
|||
local s = trim(part) |
|||
damageTypes = {}, |
|||
local prefixCount, prefixFlag, diceCount, diceFaces, qualifiers = nil, nil, nil, nil, {} |
|||
hasEdge = false, |
|||
hasProf = false |
|||
-- Extract qualifiers in [brackets] |
|||
} |
|||
local main, qualStr = s:match("^(.-)%[([^%]]+)%]$") |
|||
if qualStr then |
|||
-- Remove whitespace |
|||
s = trim(main) |
|||
for q in qualStr:gmatch("([^ ]+)") do |
|||
table.insert(qualifiers, q) |
|||
-- Extract damage types in brackets |
|||
local damageTypePattern = "%[([^%]]+)%]" |
|||
for damageType in expr:gmatch(damageTypePattern) do |
|||
table.insert(result.damageTypes, trim(damageType)) |
|||
end |
end |
||
table.sort(qualifiers) |
|||
expr = expr:gsub(damageTypePattern, "") |
|||
end |
|||
-- Check for edge (@f) and proficiency (@p) modifiers |
|||
-- Extract parenthesized prefix, e.g. (2@p), (@f), or (2 * @p) |
|||
if expr:match("@f") then |
|||
local rawPref = s:match("^%(([^)]+)%)") |
|||
result.hasEdge = true |
|||
if rawPref then |
|||
expr = expr:gsub("%(?@f%)?", "1") |
|||
local num, flag = rawPref:match("^(%d+)%s*%*%s*@(%a+)$") |
|||
if not num then num, flag = rawPref:match("^(%d*)@(%a+)$") end |
|||
prefixCount = tonumber(num) or 1 |
|||
prefixFlag = flag |
|||
s = s:gsub("^%b()", "", 1) |
|||
else |
|||
local pure = s:match("^@(%a+)$") |
|||
if pure then |
|||
prefixCount = 1 |
|||
prefixFlag = pure |
|||
s = "" |
|||
end |
end |
||
end |
|||
if expr:match("@p") then |
|||
-- Extract dice, e.g. 2d6 or d8 |
|||
result.hasProf = true |
|||
if s:match("^%d*d%d+") then |
|||
expr = expr:gsub("%(?(%d*)@p%)?", function(num) |
|||
local cnt, faces = s:match("^(%d*)d(%d+)") |
|||
return num ~= "" and num or "1" |
|||
diceCount = tonumber(cnt) or 1 |
|||
diceFaces = faces |
|||
end |
|||
end |
|||
-- Parse basic die notation (XdY with optional modifiers) |
|||
return prefixCount, prefixFlag, diceCount, diceFaces, qualifiers |
|||
local count, sides, modifiers = expr:match("^(%d*)d(%d+)(.*)$") |
|||
end |
|||
if count and sides then |
|||
result.count = count ~= "" and count or "1" |
|||
-- Main entry for #invoke:Dice|d|<roll>|<type> |
|||
result.sides = sides |
|||
function p.d(frame) |
|||
result.modifiers = trim(modifiers or "") |
|||
local fullRoll = frame.args[1] or "" |
|||
local typ = frame.args[2] or "none" |
|||
local parts = splitParts(fullRoll) |
|||
local quickParts, content = {}, {} |
|||
for _, part in ipairs(parts) do |
|||
if part == "+" or part == "-" then |
|||
-- operator |
|||
table.insert(quickParts, part) |
|||
table.insert(content, " " .. part .. " ") |
|||
else |
else |
||
-- parse dice, prefix, or constant |
|||
local prefixCount, prefixFlag, diceCount, diceFaces, qualifiers = parsePart(part) |
|||
if expr:match("^@p") then |
|||
-- fallback for plain constants |
|||
result.hasProf = true |
|||
if not diceCount and not prefixFlag then |
|||
result.isStandaloneProf = true |
|||
table.insert(quickParts, part) |
|||
table.insert(content, part) |
|||
else |
|||
-- Build quick-roll sequence (ignore flags) |
|||
if diceCount then |
|||
table.insert(quickParts, diceCount .. "d" .. diceFaces) |
|||
end |
end |
||
end |
|||
return result |
|||
end |
|||
-- Prefix template for typed rolls |
|||
-- Generate quick roll version (simplified for dice.run) |
|||
if prefixFlag and typ ~= "none" and diceCount then |
|||
local function generateQuickRoll(diceExpr) |
|||
local |
local tpl = "{{" .. string.upper(prefixFlag) |
||
if prefixCount and prefixCount ~= 1 then tpl = tpl .. "|" .. prefixCount end |
|||
tpl = tpl .. "}}" |
|||
table.insert(content, frame:preprocess(tpl)) |
|||
local segments = {} |
|||
local current = "" |
|||
local i = 1 |
|||
while i <= #diceExpr do |
|||
local char = diceExpr:sub(i, i) |
|||
if char == "+" or char == "-" then |
|||
if current ~= "" then |
|||
table.insert(segments, {op = (#segments == 0 and "" or "+"), expr = trim(current)}) |
|||
current = "" |
|||
end |
|||
table.insert(segments, {op = char, expr = ""}) |
|||
else |
|||
current = current .. char |
|||
end |
end |
||
i = i + 1 |
|||
-- Build dice link |
|||
end |
|||
if diceCount then |
|||
local displayDice = diceCount .. "d" .. diceFaces |
|||
table.insert(segments, {op = (#segments == 0 and "" or "+"), expr = trim(current)}) |
|||
local linkLabel = displayDice |
|||
end |
|||
if prefixFlag then |
|||
if typ == "none" and #qualifiers == 0 then |
|||
for _, segment in ipairs(segments) do |
|||
local prefixLabel = string.upper(prefixFlag) |
|||
if segment.expr ~= "" then |
|||
if prefixCount and prefixCount > 1 then prefixLabel = prefixCount .. prefixLabel end |
|||
local parsed = parseDieExpression(segment.expr) |
|||
linkLabel = prefixLabel .. "d" .. diceFaces |
|||
local quickDie = parsed.count .. "d" .. parsed.sides |
|||
table.insert(parts, segment.op .. quickDie) |
|||
elseif parsed.isStandaloneProf then |
|||
-- Skip standalone proficiency modifiers in quick roll |
|||
else |
else |
||
linkLabel = "d" .. diceFaces |
|||
local num = segment.expr:match("^(%d+)") |
|||
if num then |
|||
table.insert(parts, segment.op .. num) |
|||
end |
|||
end |
end |
||
end |
|||
table.insert(content, |
|||
string.format("[https://dice.run/#/d/%dd%s %s]", diceCount, diceFaces, linkLabel) |
|||
) |
|||
elseif prefixFlag then |
|||
-- pure prefix |
|||
table.insert(content, frame:preprocess("{{" .. string.upper(prefixFlag) .. "}}")) |
|||
end |
end |
||
end |
|||
local result = table.concat(parts, " ") |
|||
return trim(result:gsub("^%+", "")) |
|||
end |
|||
-- Qualifier labels |
|||
-- Generate formatted output based on type |
|||
for _, q in ipairs(qualifiers) do |
|||
local function generateFormattedOutput(diceExpr, diceType) |
|||
table.insert(content, frame:preprocess(" {{L|" .. capitalize(typ) .. "|" .. capitalize(q) .. "}}")) |
|||
if not diceType or diceType == "" or diceType == "none" then |
|||
-- Simple format for no type |
|||
return "[https://dice.run/#/d/" .. urlEncode(generateQuickRoll(diceExpr)) .. "]" |
|||
end |
|||
local parts = {} |
|||
local segments = {} |
|||
local current = "" |
|||
local i = 1 |
|||
-- Split expression into segments |
|||
while i <= #diceExpr do |
|||
local char = diceExpr:sub(i, i) |
|||
if char == "+" or char == "-" then |
|||
if current ~= "" then |
|||
table.insert(segments, {op = (#segments == 0 and "" or " + "), expr = trim(current)}) |
|||
current = "" |
|||
end |
|||
if char == "+" and #segments > 0 then |
|||
table.insert(segments, {op = " + ", expr = ""}) |
|||
elseif char == "-" then |
|||
table.insert(segments, {op = " - ", expr = ""}) |
|||
end |
|||
else |
|||
current = current .. char |
|||
end |
end |
||
end |
|||
end |
end |
||
end |
|||
if current ~= "" then |
|||
table.insert(segments, {op = (#segments == 0 and "" or " + "), expr = trim(current)}) |
|||
end |
|||
for _, segment in ipairs(segments) do |
|||
if segment.expr ~= "" then |
|||
local parsed = parseDieExpression(segment.expr) |
|||
local partText = "" |
|||
if parsed.count ~= "" and parsed.sides ~= "" then |
|||
local quickDie = parsed.count .. "d" .. parsed.sides |
|||
local displayDie = (parsed.count == "1" and "d" or parsed.count) .. parsed.sides |
|||
if parsed.hasEdge then |
|||
partText = "{{F}}[https://dice.run/#/d/" .. urlEncode(quickDie) .. " " .. displayDie .. "]" |
|||
elseif parsed.hasProf then |
|||
local profNum = parsed.count ~= "1" and parsed.count or "" |
|||
partText = "{{P|" .. profNum .. "}}[https://dice.run/#/d/" .. urlEncode(quickDie) .. " " .. displayDie .. "]" |
|||
else |
|||
partText = "[https://dice.run/#/d/" .. urlEncode(quickDie) .. " " .. quickDie .. "]" |
|||
end |
|||
-- Add damage type templates |
|||
for _, damageType in ipairs(parsed.damageTypes) do |
|||
local typeWords = {} |
|||
for word in damageType:gmatch("%S+") do |
|||
table.insert(typeWords, word:gsub("^%l", string.upper)) |
|||
end |
|||
partText = partText .. " {{L|" .. (diceType:gsub("^%l", string.upper)) .. "|" .. table.concat(typeWords, " ") .. "}}" |
|||
end |
|||
elseif parsed.isStandaloneProf then |
|||
partText = "{{P}}" |
|||
for _, damageType in ipairs(parsed.damageTypes) do |
|||
local typeWords = {} |
|||
for word in damageType:gmatch("%S+") do |
|||
table.insert(typeWords, word:gsub("^%l", string.upper)) |
|||
end |
|||
partText = partText .. " {{L|" .. (diceType:gsub("^%l", string.upper)) .. "|" .. table.concat(typeWords, " ") .. "}}" |
|||
end |
|||
else |
|||
-- Handle numeric constants |
|||
local num = segment.expr:match("^(%d+)") |
|||
if num then |
|||
partText = num |
|||
end |
|||
end |
|||
if partText ~= "" then |
|||
table.insert(parts, segment.op .. partText) |
|||
end |
|||
elseif segment.op ~= "" then |
|||
-- Add standalone operators |
|||
table.insert(parts, segment.op) |
|||
end |
|||
end |
|||
local result = table.concat(parts, "") |
|||
return trim(result:gsub("^ %+ ", "")) |
|||
end |
|||
-- assemble quick-roll and output |
|||
-- Main function to process dice input |
|||
local quickRoll = table.concat(quickParts, " ") |
|||
function p.d(frame) |
|||
local inner = table.concat(content) |
|||
local diceType = frame.args[2] or "none" |
|||
return html.create('span') |
|||
:addClass('dice') |
|||
if diceInput == "" then |
|||
:attr('data-full-roll', fullRoll) |
|||
return "" |
|||
:attr('data-quick-roll', quickRoll) |
|||
end |
|||
:attr('data-type', typ) |
|||
:wikitext(inner) |
|||
local quickRoll = generateQuickRoll(diceInput) |
|||
:allDone() |
|||
local formattedOutput = generateFormattedOutput(diceInput, diceType) |
|||
-- Handle special display cases |
|||
local displayText = formattedOutput |
|||
if diceInput:match("%(2@p%)d4") and not (diceType and diceType ~= "" and diceType ~= "none") then |
|||
displayText = "[https://dice.run/#/d/" .. urlEncode(quickRoll) .. " 2Pd4]" |
|||
end |
|||
return '<span class="dice" data-full-roll="' .. diceInput .. '" data-quick-roll="' .. quickRoll .. '" data-type="' .. diceType .. '">' .. displayText .. '</span>' |
|||
end |
end |
||
Latest revision as of 01:02, 20 June 2025
Documentation for this module may be created at Module:Dice/doc
-- Module:Dice
-- Processes dice input strings and outputs HTML spans with roll links, templates, and data attributes
local p = {}
local html = mw.html
-- Trim whitespace from both ends
local function trim(s)
return (s:gsub("^%s*(.-)%s*$", "%1"))
end
-- Split the full roll string into parts and operators (+, -)
local function splitParts(s)
local parts = {}
local i = 1
while i <= #s do
local c = s:sub(i,i)
if c == "+" or c == "-" then
table.insert(parts, c)
i = i + 1
elseif c == " " then
i = i + 1
else
local j = s:find("[%+%-]", i)
if j then
table.insert(parts, trim(s:sub(i, j-1)))
i = j
else
table.insert(parts, trim(s:sub(i)))
break
end
end
end
return parts
end
-- Capitalize first letter
local function capitalize(s)
return (s:gsub("^%l", string.upper))
end
-- Parse an individual part into prefix, dice, and qualifiers
local function parsePart(part)
local s = trim(part)
local prefixCount, prefixFlag, diceCount, diceFaces, qualifiers = nil, nil, nil, nil, {}
-- Extract qualifiers in [brackets]
local main, qualStr = s:match("^(.-)%[([^%]]+)%]$")
if qualStr then
s = trim(main)
for q in qualStr:gmatch("([^ ]+)") do
table.insert(qualifiers, q)
end
table.sort(qualifiers)
end
-- Extract parenthesized prefix, e.g. (2@p), (@f), or (2 * @p)
local rawPref = s:match("^%(([^)]+)%)")
if rawPref then
local num, flag = rawPref:match("^(%d+)%s*%*%s*@(%a+)$")
if not num then num, flag = rawPref:match("^(%d*)@(%a+)$") end
prefixCount = tonumber(num) or 1
prefixFlag = flag
s = s:gsub("^%b()", "", 1)
else
local pure = s:match("^@(%a+)$")
if pure then
prefixCount = 1
prefixFlag = pure
s = ""
end
end
-- Extract dice, e.g. 2d6 or d8
if s:match("^%d*d%d+") then
local cnt, faces = s:match("^(%d*)d(%d+)")
diceCount = tonumber(cnt) or 1
diceFaces = faces
end
return prefixCount, prefixFlag, diceCount, diceFaces, qualifiers
end
-- Main entry for #invoke:Dice|d|<roll>|<type>
function p.d(frame)
local fullRoll = frame.args[1] or ""
local typ = frame.args[2] or "none"
local parts = splitParts(fullRoll)
local quickParts, content = {}, {}
for _, part in ipairs(parts) do
if part == "+" or part == "-" then
-- operator
table.insert(quickParts, part)
table.insert(content, " " .. part .. " ")
else
-- parse dice, prefix, or constant
local prefixCount, prefixFlag, diceCount, diceFaces, qualifiers = parsePart(part)
-- fallback for plain constants
if not diceCount and not prefixFlag then
table.insert(quickParts, part)
table.insert(content, part)
else
-- Build quick-roll sequence (ignore flags)
if diceCount then
table.insert(quickParts, diceCount .. "d" .. diceFaces)
end
-- Prefix template for typed rolls
if prefixFlag and typ ~= "none" and diceCount then
local tpl = "{{" .. string.upper(prefixFlag)
if prefixCount and prefixCount ~= 1 then tpl = tpl .. "|" .. prefixCount end
tpl = tpl .. "}}"
table.insert(content, frame:preprocess(tpl))
end
-- Build dice link
if diceCount then
local displayDice = diceCount .. "d" .. diceFaces
local linkLabel = displayDice
if prefixFlag then
if typ == "none" and #qualifiers == 0 then
local prefixLabel = string.upper(prefixFlag)
if prefixCount and prefixCount > 1 then prefixLabel = prefixCount .. prefixLabel end
linkLabel = prefixLabel .. "d" .. diceFaces
else
linkLabel = "d" .. diceFaces
end
end
table.insert(content,
string.format("[https://dice.run/#/d/%dd%s %s]", diceCount, diceFaces, linkLabel)
)
elseif prefixFlag then
-- pure prefix
table.insert(content, frame:preprocess("{{" .. string.upper(prefixFlag) .. "}}"))
end
-- Qualifier labels
for _, q in ipairs(qualifiers) do
table.insert(content, frame:preprocess(" {{L|" .. capitalize(typ) .. "|" .. capitalize(q) .. "}}"))
end
end
end
end
-- assemble quick-roll and output
local quickRoll = table.concat(quickParts, " ")
local inner = table.concat(content)
return html.create('span')
:addClass('dice')
:attr('data-full-roll', fullRoll)
:attr('data-quick-roll', quickRoll)
:attr('data-type', typ)
:wikitext(inner)
:allDone()
end
return p