Jump to content
Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:Dice: Difference between revisions

From Teriock
// via Wikitext Extension for VSCode
 
No edit summary
 
(32 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


-- Helper function to trim whitespace
-- Trim whitespace from both ends
local function trim(s)
local function trim(s)
    return s:match("^%s*(.-)%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
end


-- Helper function to escape special characters for URLs
-- Capitalize first letter
local function urlEncode(str)
local function capitalize(s)
    return str:gsub("([^%w%-%.%_%~])", function(c)
  return (s:gsub("^%l", string.upper))
        return string.format("%%%02X", string.byte(c))
    end)
end
end


-- Parse a single die expression and extract components
-- Parse an individual part into prefix, dice, qualifiers, and constant
local function parseDieExpression(expr)
local function parsePart(part)
    local result = {
  local s = trim(part)
        original = expr,
  local prefixCount, prefixFlag, diceCount, diceFaces, qualifiers = nil, nil, nil, nil, {}
        count = "",
  local constValue = nil
        sides = "",
 
        modifiers = "",
  -- Extract qualifiers in [brackets]
        damageTypes = {},
  local main, qualStr = s:match("^(.-)%[([^%]]+)%]$")
        hasEdge = false,
  if qualStr then
        hasProf = false
    s = trim(main)
    }
     for q in qualStr:gmatch("([^ ]+)") do
   
      table.insert(qualifiers, q)
    -- Remove whitespace
    expr = trim(expr)
   
    -- Extract damage types in brackets
    local damageTypePattern = "%[([^%]]+)%]"
     for damageType in expr:gmatch(damageTypePattern) do
        table.insert(result.damageTypes, trim(damageType))
     end
     end
     expr = expr:gsub(damageTypePattern, "")
     table.sort(qualifiers)
   
  end
    -- Check for edge (@f) and proficiency (@p) modifiers
 
     if expr:match("@f") then
  -- Extract parenthesized prefix, e.g. (2@p), (@f), or (2 * @p)
        result.hasEdge = true
  local rawPref = s:match("^%(([^)]+)%)")
        expr = expr:gsub("%(?@f%)?", "1")
  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
   
  end
    if expr:match("@p") then
 
        result.hasProf = true
  -- Extract dice, e.g. 2d6 or d8
        expr = expr:gsub("%(?(%d*)@p%)?", function(num)
  if s:match("^%d*d%d+") then
            return num ~= "" and num or "1"
    local cnt, faces = s:match("^(%d*)d(%d+)")
        end)
    diceCount = tonumber(cnt) or 1
    diceFaces = faces
  else
    -- If not dice, check for a bare numeric constant
    local num = s:match("^(%d+)$")
    if num then
      constValue = tonumber(num)
     end
     end
   
  end
    -- Parse basic die notation (XdY with optional modifiers)
 
    local count, sides, modifiers = expr:match("^(%d*)d(%d+)(.*)$")
  return prefixCount, prefixFlag, diceCount, diceFaces, qualifiers, constValue
     if count and sides then
end
        result.count = count ~= "" and count or "1"
 
        result.sides = sides
-- Main entry for #invoke:Dice|d|<roll>|<type>
        result.modifiers = trim(modifiers or "")
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
     else
         -- Handle standalone modifiers like @p[memory]
      -- parse dice, prefix, or constant
         if expr:match("^@p") then
      local prefixCount, prefixFlag, diceCount, diceFaces, qualifiers, constValue = parsePart(part)
            result.hasProf = true
 
            result.isStandaloneProf = true
      -- If it's truly plain (no dice/prefix/qualifiers), keep old behavior
      if not diceCount and not prefixFlag and #qualifiers == 0 and not constValue 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)
        elseif constValue then
          table.insert(quickParts, tostring(constValue))
         end
         end
    end
   
    return result
end


-- Generate quick roll version (simplified for dice.run)
        -- Prefix template for typed rolls (only before dice, as before)
local function generateQuickRoll(diceExpr)
        if prefixFlag and typ ~= "none" and diceCount then
    local parts = {}
          local tpl = "{{" .. string.upper(prefixFlag)
   
          if prefixCount and prefixCount ~= 1 then tpl = tpl .. "|" .. prefixCount end
    -- Split by + and - while preserving operators
          tpl = tpl .. "}}"
    local segments = {}
          table.insert(content, frame:preprocess(tpl))
    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
 
    end
         -- Build dice link or output constant/pure prefix
    if current ~= "" then
        if diceCount then
        table.insert(segments, {op = (#segments == 0 and "" or "+"), expr = trim(current)})
          local displayDice = diceCount .. "d" .. diceFaces
    end
          local linkLabel = displayDice
   
          if prefixFlag then
    for _, segment in ipairs(segments) do
            if typ == "none" and #qualifiers == 0 then
        if segment.expr ~= "" then
              local prefixLabel = string.upper(prefixFlag)
            local parsed = parseDieExpression(segment.expr)
              if prefixCount and prefixCount > 1 then prefixLabel = prefixCount .. prefixLabel end
            if parsed.count ~= "" and parsed.sides ~= "" then
              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
                -- Handle numeric constants
              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 constValue then
          -- numeric constant (e.g., 4[lethal])
          table.insert(content, tostring(constValue))
        elseif prefixFlag then
          -- pure prefix
          local tpl = "{{" .. string.upper(prefixFlag)
          if prefixCount and prefixCount ~= 1 then tpl = tpl .. "|" .. prefixCount end
          tpl = tpl .. "}}"
          table.insert(content, frame:preprocess(tpl))
        else
          -- fallback: unknown token but with qualifiers; keep original token text
          table.insert(content, part)
         end
         end
    end
   
    local result = table.concat(parts, " ")
    return trim(result:gsub("^%+", ""))
end


-- Generate formatted output based on type
        -- Qualifier labels
local function generateFormattedOutput(diceExpr, diceType)
         for _, q in ipairs(qualifiers) do
    if not diceType or diceType == "" or diceType == "none" then
          table.insert(content, frame:preprocess(" {{L|" .. capitalize(typ) .. "|" .. capitalize(q) .. "}}"))
         -- 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
        i = i + 1
      end
     end
     end
    if current ~= "" then
  end
        table.insert(segments, {op = (#segments == 0 and "" or " + "), expr = trim(current)})
 
    end
  -- assemble quick-roll and output
   
  local quickRoll = table.concat(quickParts, " ")
    for _, segment in ipairs(segments) do
  local inner = table.concat(content)
        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


-- Main function to process dice input
  return html.create('span')
function p.d(frame)
     :addClass('dice')
     local diceInput = frame.args[1] or ""
     :attr('data-full-roll', fullRoll)
    local diceType = frame.args[2] or "none"
     :attr('data-quick-roll', quickRoll)
   
     :attr('data-type', typ)
    if diceInput == "" then
     :wikitext(inner)
        return ""
    :tag("span")
    end
    :addClass("metadata")
   
     :attr("data-full-roll", fullRoll)
    local quickRoll = generateQuickRoll(diceInput)
    :attr("data-quick-roll", quickRoll)
     local formattedOutput = generateFormattedOutput(diceInput, diceType)
    :attr("data-type", "roll")
      
    :attr("data-roll-type", typ)
    -- Handle special display cases
    :done()
     local displayText = formattedOutput
    :allDone()
     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


return p
return p

Latest revision as of 20:08, 19 November 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, qualifiers, and constant
local function parsePart(part)
  local s = trim(part)
  local prefixCount, prefixFlag, diceCount, diceFaces, qualifiers = nil, nil, nil, nil, {}
  local constValue = 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
  else
    -- If not dice, check for a bare numeric constant
    local num = s:match("^(%d+)$")
    if num then
      constValue = tonumber(num)
    end
  end

  return prefixCount, prefixFlag, diceCount, diceFaces, qualifiers, constValue
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, constValue = parsePart(part)

      -- If it's truly plain (no dice/prefix/qualifiers), keep old behavior
      if not diceCount and not prefixFlag and #qualifiers == 0 and not constValue 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)
        elseif constValue then
          table.insert(quickParts, tostring(constValue))
        end

        -- Prefix template for typed rolls (only before dice, as before)
        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 or output constant/pure prefix
        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 constValue then
          -- numeric constant (e.g., 4[lethal])
          table.insert(content, tostring(constValue))
        elseif prefixFlag then
          -- pure prefix
          local tpl = "{{" .. string.upper(prefixFlag)
          if prefixCount and prefixCount ~= 1 then tpl = tpl .. "|" .. prefixCount end
          tpl = tpl .. "}}"
          table.insert(content, frame:preprocess(tpl))
        else
          -- fallback: unknown token but with qualifiers; keep original token text
          table.insert(content, part)
        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)
    :tag("span")
    :addClass("metadata")
    :attr("data-full-roll", fullRoll)
    :attr("data-quick-roll", quickRoll)
    :attr("data-type", "roll")
    :attr("data-roll-type", typ)
    :done()
    :allDone()
end

return p