MTG Wiki
Register
No edit summary
mNo edit summary
 
(26 intermediate revisions by 2 users not shown)
Line 22: Line 22:
 
local GENERAL_RULE_PATTERN = "General"
 
local GENERAL_RULE_PATTERN = "General"
   
local WIKILINK_FORMAT = "[[%s]]"
+
-- local WIKILINK_FORMAT = "[[%s]]" -- Condider: no-longer used, remove it entirely
 
local WIKILINK_ALT_FORMAT = "[[%s|%s]]"
 
local WIKILINK_ALT_FORMAT = "[[%s|%s]]"
 
local GENERAL_RULE_EXPANDED_TEXT_FORMAT = "General_(%s)"
 
local GENERAL_RULE_EXPANDED_TEXT_FORMAT = "General_(%s)"
Line 51: Line 51:
   
 
--Colors
 
--Colors
local RULES_COLOR = "#FFFFFF"
+
local RULES_COLOR = "var(--theme-page-background-color)"
local GLOSSARY_COLOR = "#DDFFDD"
+
local GLOSSARY_COLOR = "var(--theme-page-accent-mix-color)"
local OBSOLETE_COLOR = "#FFFDD0"
+
local OBSOLETE_COLOR = "var(--theme-body-background-color)"
   
 
-- Escape magic characters in a string.
 
-- Escape magic characters in a string.
 
local sanitize
 
local sanitize
 
do
 
do
local matches =
+
local matches =
  +
{
{
 
["^"] = "%^";
+
["^"] = "%^";
["$"] = "%$";
+
["$"] = "%$";
["("] = "%(";
+
["("] = "%(";
[")"] = "%)";
+
[")"] = "%)";
["%"] = "%%";
+
["%"] = "%%";
["."] = "%.";
+
["."] = "%.";
["["] = "%[";
+
["["] = "%[";
["]"] = "%]";
+
["]"] = "%]";
["*"] = "%*";
+
["*"] = "%*";
["+"] = "%+";
+
["+"] = "%+";
["-"] = "%-";
+
["-"] = "%-";
["?"] = "%?";
+
["?"] = "%?";
  +
}
}
 
   
sanitize = function(s)
+
sanitize = function(s)
return (gsub(s, ".", matches))
+
return (gsub(s, ".", matches))
end
+
end
 
end
 
end
   
Line 82: Line 82:
   
 
local function GetLastUpdate()
 
local function GetLastUpdate()
local lastEffectiveDate = match(CR.text, COMP_RULES_DATE_PATTERN)
+
local lastEffectiveDate = match(CR.text, COMP_RULES_DATE_PATTERN)
-- Italicize and wikilink the set name
+
-- Italicize and wikilink the set name
local setName = format("''[[%s]]''", MOST_RECENT_SET_NAME)
+
local setName = format("''[[%s]]''", MOST_RECENT_SET_NAME)
local lastUpdate = lastEffectiveDate .. "—" .. setName
+
local lastUpdate = lastEffectiveDate .. "—" .. setName
return lastUpdate
+
return lastUpdate
 
end
 
end
   
 
local function SplitLine(ruleLine)
 
local function SplitLine(ruleLine)
-- Finds the index and the rest. If the index has an extra period, it is considered a formatting issue in the CR and is therefore ignored.
+
-- Finds the index and the rest. If the index has an extra period, it is considered a formatting issue in the CR and is therefore ignored.
local i, j, index, rest = find(ruleLine, "^(%d+%.%d*[%.a-kmnp-z]?)%.?%s(.+)")
+
local i, j, index, rest = find(ruleLine, "^(%d+%.%d*[%.a-kmnp-z]?)%.?%s(.+)")
return i, j, index, rest
+
return i, j, index, rest
 
end
 
end
   
 
--[[
 
--[[
Chops up and validates an index.
+
Chops up and validates an index.
Individually breaking down index parts here for ease of comprehension and future maintenance.
+
Individually breaking down index parts here for ease of comprehension and future maintenance.
Yes, a clever soul could streamline this with more complex pattern matching.
+
Yes, a clever soul could streamline this with more complex pattern matching.
Clever and absolute performance are not the goals. Editability by anonymous maintainers is.
+
Clever and absolute performance are not the goals. Editability by anonymous maintainers is.
  +
 
heading, major, and minor should all be number or nil
+
heading, major, and minor should all be number or nil
returns false instead if the string provided is not an index
+
returns false instead if the string provided is not an index
 
--]]
 
--]]
 
local function ParseIndex(index)
 
local function ParseIndex(index)
local heading, major, minor, subrule
+
local heading, major, minor, subrule
  +
 
if match(index, "%s") then
+
if match(index, "%s") then
return false
+
return false
end
+
end
  +
 
local i, j, suffix = find(index, "%.(.+)")
+
local i, _, suffix = find(index, "%.(.+)")
if suffix then
+
if suffix then
-- If the last character in the suffix is alphanumeric (and not L or O), set the subrule
+
-- If the last character in the suffix is alphanumeric (and not L or O), set the subrule
subrule = match(sub(suffix, -1), "(["..INDEX_SUBRULE_CHARACTER_SET.."])")
+
subrule = match(sub(suffix, -1), "(["..INDEX_SUBRULE_CHARACTER_SET.."])")
  +
 
-- If that was successful, cut that character off the suffix.
+
-- If that was successful, cut that character off the suffix.
if subrule then
+
if subrule then
suffix = sub(suffix, 1, -2)
+
suffix = sub(suffix, 1, -2)
end
+
end
  +
 
-- Now we can easily check whether the part between the period and the letter is a number.
+
-- Now we can easily check whether the part between the period and the letter is a number.
-- If so, that's our minor index
+
-- If so, that's our minor index
minor = tonumber(suffix)
+
minor = tonumber(suffix)
assert(type(minor) == "number", "Invalid index!")
+
assert(type(minor) == "number", "Invalid index!")
  +
 
-- Now cut off the entire suffix and let's parse the rest
+
-- Now cut off the entire suffix and let's parse the rest
index = sub(index, 1, i-1)
+
index = sub(index, 1, i-1)
end
+
end
  +
 
-- Getting the heading and major index is just some number manipulation
+
-- Getting the heading and major index is just some number manipulation
index = tonumber(index)
+
index = tonumber(index)
-- assert(type(index) == "number", "Invalid index!")
+
-- assert(type(index) == "number", "Invalid index!")
if not index then
+
if not index then
return false
+
return false
end
+
end
  +
 
if index >= 100 then
+
if index >= 100 then
major = index%100
+
major = index%100
heading = math.floor(index/100)
+
heading = math.floor(index/100)
else
+
else
-- The body of the rules starts at 100, lower values are headings
+
-- The body of the rules starts at 100, lower values are headings
heading = index
+
heading = index
end
+
end
  +
 
return heading, major, minor, subrule
+
return heading, major, minor, subrule
 
end
 
end
   
 
local function IsSubsequentRule(line, heading, major, minor, subrule)
 
local function IsSubsequentRule(line, heading, major, minor, subrule)
local _, _, index = SplitLine(line)
+
local _, _, index = SplitLine(line)
if not index then
+
if not index then
return false
+
return false
end
+
end
  +
 
local h, a, i, s = ParseIndex(index)
+
local h, a, i, _ = ParseIndex(index)
  +
 
if subrule then
+
if subrule then
-- We can cheat like hell if we're dealing with subrules because there's no further nesting
+
-- We can cheat like hell if we're dealing with subrules because there's no further nesting
-- Therefore, the next line is the next rule by definition
+
-- Therefore, the next line is the next rule by definition
-- That said, if this ever changes, this snippet might be useful
+
-- That said, if this ever changes, this snippet might be useful
--[[
+
--[[
-- Yes, this makes assumptions about character encoding and subrules never numbering more than 24 for a given rule
+
-- Yes, this makes assumptions about character encoding and subrules never numbering more than 24 for a given rule
local i = find(INDEX_SUBRULE_CHARACTER_SET, subrule) + 1
+
local i = find(INDEX_SUBRULE_CHARACTER_SET, subrule) + 1
nextIndex = sub(INDEX_SUBRULE_CHARACTER_SET, i, i)
+
nextIndex = sub(INDEX_SUBRULE_CHARACTER_SET, i, i)
--]]
+
--]]
return true
+
return true
elseif i and minor and i > minor then
+
elseif i and minor and i > minor then
return true
+
return true
elseif a and major and a > major then
+
elseif a and major and a > major then
return true
+
return true
elseif h and heading and h > heading then
+
elseif h and heading and h > heading then
return true
+
return true
end
+
end
 
end
 
end
   
 
local function GetNestingDepth(index)
 
local function GetNestingDepth(index)
local depth
+
local depth
  +
if match(index, "["..INDEX_SUBRULE_CHARACTER_SET.."]") then
 
  +
if match(index, "["..INDEX_SUBRULE_CHARACTER_SET.."]") then
-- Subrules, e.g. 103.7a
 
  +
-- Subrules, e.g. 103.7a
depth = 4
 
  +
depth = 4
elseif match(index, "%d%.%d") then
 
  +
elseif match(index, "%d+%.%d+%.") then
-- Rules, e.g. 112.1
 
  +
-- Rules, e.g. 112.1.
depth = 3
 
  +
depth = 3
elseif match(index, "%d%d+") then
 
  +
elseif match(index, "^%d%d%d%.") or match(index, "^%d+%.%d+") then
-- Titles, e.g. 102
 
  +
-- Titles, e.g. 102. or 1.2 for normal numbering conventions
depth = 2
 
  +
depth = 2
else
 
  +
else
-- Headings, e.g. 1
 
  +
-- Headings, e.g. 1.
depth = 1
 
  +
depth = 1
end
 
  +
end
 
  +
return depth
 
  +
return depth
 
end
 
end
   
 
local function Titleize(title)
 
local function Titleize(title)
local link, t, bare
+
local link, t, bare
if match(title,"%(Obsolete%)$") then
+
if match(title,"%(Obsolete%)$") then
bare = sub(title,0,-12)
+
bare = sub(title,0,-12)
link = lower(bare)
+
link = lower(bare)
link = gsub(link, "^(%S)", upper)
+
link = gsub(link, "^(%S)", upper)
t = format(WIKILINK_ALT_FORMAT, link, bare)
+
t = format(WIKILINK_ALT_FORMAT, link, bare)
t = t..OBSOLETE_PATTERN
+
t = t..OBSOLETE_PATTERN
else
+
else
link = lower(title)
+
link = lower(title)
-- convert the first letter back to uppercase
+
-- convert the first letter back to uppercase
link = gsub(link, "^(%S)", upper)
+
link = gsub(link, "^(%S)", upper)
t = format(WIKILINK_ALT_FORMAT, link, title)
+
t = format(WIKILINK_ALT_FORMAT, link, title)
end
+
end
return t
+
return t
 
end
 
end
   
 
local function StylizeRule(ruleLine)
 
local function StylizeRule(ruleLine)
local i, j, index, rest = SplitLine(ruleLine)
+
local _, _, index, rest = SplitLine(ruleLine)
if not index then
+
if not index then
if find(ruleLine, "Example:") then
+
if find(ruleLine, "Example:") then
ruleLine = "<p class=\"crExample\">''" .. gsub(ruleLine, "(Example:)", "'''%1'''") .. "''</p>"
+
ruleLine = "<p class=\"crExample\">''" .. gsub(ruleLine, "(Example:)", "'''%1'''") .. "''</p>"
end
+
end
return ruleLine, true
+
return ruleLine
end
+
end
 
local h, a, i, s = ParseIndex(index)
 
-- Major indices and any rule shorter than five words should be a title, so try linking it!
 
-- (this is probably a stupid assumption let's see how long before we get burned)
 
if (h and a and not i) then
 
if find(rest, GENERAL_RULE_PATTERN) then
 
-- Each heading in the CR has a "General" section, expand that to "General_(<name of header>)"
 
local headingName
 
for line in gmatch(CR.text, LINE_PATTERN) do
 
headingName = match(line, "^"..h.."%. (.+)")
 
if headingName then
 
break
 
end
 
end
 
assert(headingName)
 
headingName = lower(headingName)
 
headingName = gsub(headingName, "^(%S)", upper)
 
local expandedLink = format(GENERAL_RULE_EXPANDED_TEXT_FORMAT, headingName)
 
if match(rest,"%(Obsolete%)$") then
 
local bare = sub(rest,0,-12)
 
rest = format(WIKILINK_ALT_FORMAT, expandedLink, bare)..OBSOLETE_PATTERN
 
else
 
rest = format(WIKILINK_ALT_FORMAT, expandedLink, rest)
 
end
 
elseif match(index, "90[1-4]") then
 
-- The casual format articles usually have a "<Name of format>_(format)" nameing pattern
 
   
  +
local h, a, i, _ = ParseIndex(index)
local expandedLink = format(CASUAL_FORMAT_EXPANDED_TEXT_FORMAT, rest)
 
  +
-- Major indices and any rule shorter than five words should be a title, so try linking it!
rest = format(WIKILINK_ALT_FORMAT, expandedLink, rest)
 
  +
-- (this is probably a stupid assumption let's see how long before we get burned)
else
 
  +
if (h and a and not i) then
rest = Titleize(rest)
 
  +
if find(rest, GENERAL_RULE_PATTERN) then
end
 
  +
-- Each heading in the CR has a "General" section, expand that to "General_(<name of header>)"
else
 
  +
local headingName
local _, numWords = gsub(rest, "%S+", "")
 
  +
for line in gmatch(CR.text, LINE_PATTERN) do
if numWords < 5 then
 
  +
headingName = match(line, "^"..h.."%. (.+)")
rest = Titleize(rest)
 
  +
if headingName then
end
 
  +
break
end
 
  +
end
 
  +
end
return format("'''%s''' %s", index, rest)
 
  +
assert(headingName)
  +
headingName = lower(headingName)
  +
headingName = gsub(headingName, "^(%S)", upper)
  +
local expandedLink = format(GENERAL_RULE_EXPANDED_TEXT_FORMAT, headingName)
  +
if match(rest,"%(Obsolete%)$") then
  +
local bare = sub(rest,0,-12)
  +
rest = format(WIKILINK_ALT_FORMAT, expandedLink, bare)..OBSOLETE_PATTERN
  +
else
  +
rest = format(WIKILINK_ALT_FORMAT, expandedLink, rest)
  +
end
  +
elseif match(index, "90[1-4]") then
  +
-- The casual format articles usually have a "<Name of format>_(format)" nameing pattern
  +
  +
local expandedLink = format(CASUAL_FORMAT_EXPANDED_TEXT_FORMAT, rest)
  +
rest = format(WIKILINK_ALT_FORMAT, expandedLink, rest)
  +
else
  +
rest = Titleize(rest)
  +
end
  +
else
  +
local _, numWords = gsub(rest, "%S+", "")
  +
if numWords < 5 then
  +
rest = Titleize(rest)
  +
end
  +
end
  +
  +
return format("'''%s''' %s", index, rest)
 
end
 
end
   
 
-- Creates a mw.html object matching the styling of the old CR template
 
-- Creates a mw.html object matching the styling of the old CR template
 
local function CreateRulesDiv(output,source,update,color)
 
local function CreateRulesDiv(output,source,update,color)
local div = mw.html.create("div")
+
local div = mw.html.create("div")
div:addClass("crDiv")
+
div:addClass("crDiv")
div:addClass("crBox")
+
div:addClass("crBox")
div:css("background-color", color)
+
div:css("background-color", color)
local ns = mw.title.getCurrentTitle().namespace
+
local ns = mw.title.getCurrentTitle().namespace
if color == OBSOLETE_COLOR and ns == 0 then
+
if color == OBSOLETE_COLOR and ns == 0 then
div:wikitext(OBSOLETE_CAT)
+
div:wikitext(OBSOLETE_CAT)
end
+
end
div:wikitext(format(LAST_UPDATED_FORMAT, source, update))
+
div:wikitext(format(LAST_UPDATED_FORMAT, source, update))
div:newline()
+
div:newline()
if type(output) == "string" then
+
if type(output) == "string" then
local line = StylizeRule(output)
+
local line = StylizeRule(output)
div:wikitext("* ", line)
+
div:wikitext("* ", line)
else
+
else
local indentLevel
+
local indentLevel
local prevMax = 0
+
local prevMax = 0
local outputLine, isExample, maxIndent, index, _
+
local outputLine, maxIndent, index, _
for _, line in ipairs(output) do
+
for _, line in ipairs(output) do
outputLine, isExample = StylizeRule(line)
+
outputLine = StylizeRule(line)
_, _, index = SplitLine(line)
+
_, _, index = SplitLine(line)
if index then
+
if index then
div:newline()
+
div:newline()
maxIndent = GetNestingDepth(index)
+
maxIndent = GetNestingDepth(index)
if not indentLevel then
+
if not indentLevel then
indentLevel = 1
+
indentLevel = 1
  +
else
else
 
indentLevel = indentLevel + (maxIndent - prevMax)
+
indentLevel = indentLevel + (maxIndent - prevMax)
  +
end
end
 
prevMax = maxIndent
+
prevMax = maxIndent
outputLine = rep("*", indentLevel) .. outputLine
+
outputLine = rep("*", indentLevel) .. outputLine
else
+
else
if not find(outputLine, "Example:") then
+
if not find(outputLine, "Example:") then
outputLine = "<br/>" .. outputLine
+
outputLine = "<br/>" .. outputLine
  +
else
else
 
outputLine = outputLine
+
outputLine = outputLine
  +
end
end
 
  +
end
end
 
  +
 
div:wikitext(outputLine)
+
div:wikitext(outputLine)
end
+
end
end
+
end
  +
 
return div
+
return div
 
end
 
end
   
 
-- Creates a mw.html object with a slightly different background color
 
-- Creates a mw.html object with a slightly different background color
 
local function CreateGlossaryDiv(output,source,update)
 
local function CreateGlossaryDiv(output,source,update)
local div = mw.html.create("div")
+
local div = mw.html.create("div")
div:addClass("crBox")
+
div:addClass("crBox")
 
local ns = mw.title.getCurrentTitle().namespace
 
div:wikitext(format(LAST_UPDATED_FORMAT, source, update))
 
div:newline()
 
   
  +
local ns = mw.title.getCurrentTitle().namespace
for i, line in ipairs(output) do
 
  +
div:wikitext(format(LAST_UPDATED_FORMAT, source, update))
-- Bold the name of the entry for clarity
 
  +
div:newline()
if i == 1 then
 
  +
if match(line,"%(Obsolete%)$") then
 
  +
for i, line in ipairs(output) do
-- Link and categorize obsolete terms
 
  +
-- Bold the name of the entry for clarity
div:css("background-color", OBSOLETE_COLOR)
 
  +
if i == 1 then
div:wikitext(";" .. gsub(line,"%(Obsolete%)","%([[Obsolete]]%)") .. "\n")
 
  +
if match(line,"%(Obsolete%)$") then
if ns == 0 then
 
  +
-- Link and categorize obsolete terms
div:wikitext(OBSOLETE_CAT)
 
  +
div:css("background-color", OBSOLETE_COLOR)
end
 
  +
div:wikitext(";" .. gsub(line,"%(Obsolete%)","%([[Obsolete]]%)") .. "\n")
else
 
  +
if ns == 0 then
div:css("background-color", GLOSSARY_COLOR)
 
  +
div:wikitext(OBSOLETE_CAT)
div:wikitext(";" .. line .. "\n")
 
  +
end
end
 
else
+
else
  +
div:css("background-color", GLOSSARY_COLOR)
div:wikitext(":" .. line .. "\n")
 
  +
div:wikitext(";" .. line .. "\n")
end
 
  +
end
end
 
  +
else
-- Add glossary category in mainspace articles
 
  +
div:wikitext(":" .. line .. "\n")
if ns == 0 then
 
  +
end
div:wikitext(GLOSSARY_CAT)
 
end
+
end
  +
-- Add glossary category in mainspace articles
 
  +
if ns == 0 then
return div
 
  +
div:wikitext(GLOSSARY_CAT)
  +
end
  +
  +
return div
 
end
 
end
   
 
function CR.TOC(frame)
 
function CR.TOC(frame)
local index = frame.args[1]
+
local index = frame.args[1]
local heading, major, minor, subrule
+
local heading, major, minor, subrule
-- If there's no index, we want the full TOC. Otherwise, pass it in for validation.
+
-- If there's no index, we want the full TOC. Otherwise, pass it in for validation.
local fullTOC
+
local fullTOC
if (not index) or type(index)=="string" and index=="" then
+
if (not index) or type(index)=="string" and index=="" then
heading = 1
+
heading = 1
fullTOC = true
+
fullTOC = true
else
+
else
heading, major, minor, subrule = ParseIndex(index)
+
heading, major, minor, subrule = ParseIndex(index)
assert(heading and heading>=1 and heading<=10 and not major and not minor and not subrule, "Invalid table of contents index!")
+
assert(heading and heading>=1 and heading<=10 and not major and not minor and not subrule, "Invalid table of contents index!")
end
+
end
  +
 
local output = {}
+
local output = {}
  +
 
local collecting = false
+
local collecting = false
for line in gmatch(CR.text, LINE_PATTERN) do
+
for line in gmatch(CR.text, LINE_PATTERN) do
if match(line, RULE_LINE_PATTERN) then
+
if match(line, RULE_LINE_PATTERN) then
if match(line, "^"..heading.."%.") then
+
if match(line, "^"..heading.."%.") then
collecting = true
+
collecting = true
elseif (not fullTOC) and IsSubsequentRule(line, heading, major, minor, subrule) then
+
elseif (not fullTOC) and IsSubsequentRule(line, heading, major, minor, subrule) then
break
+
break
  +
end
end
 
  +
 
-- NOT elseif. We want to start collecting lines on the same line we match the target heading
+
-- NOT elseif. We want to start collecting lines on the same line we match the target heading
if collecting then
+
if collecting then
tinsert(output, line)
+
tinsert(output, line)
  +
end
end
 
elseif match(line, TOC_END_LINE_PATTERN) then
+
elseif match(line, TOC_END_LINE_PATTERN) then
break
+
break
end
+
end
end
+
end
  +
 
assert(#output > 0, "Index not found! ", index)
+
assert(#output > 0, "Index not found! ", index)
return tostring(CreateRulesDiv(output,CR_SOURCE,GetLastUpdate(),RULES_COLOR))
+
return tostring(CreateRulesDiv(output,CR_SOURCE,GetLastUpdate(),RULES_COLOR))
 
end
 
end
   
 
-- Basically CR.full but with the full text of an index
 
-- Basically CR.full but with the full text of an index
 
function CR.title(frame)
 
function CR.title(frame)
local title = frame.args[1]
+
local title = frame.args[1]
title = sanitize(title)
+
title = sanitize(title)
  +
 
local output = {}
+
local output = {}
local passedTOC = false
+
local passedTOC = false
local collecting = false
+
local collecting = false
local heading, major, minor, subrule -- this is a stupid hack to continue using the original index-based stuff
+
local heading, major, minor, subrule -- this is a stupid hack to continue using the original index-based stuff
for line in gmatch(CR.text, LINE_PATTERN) do
+
for line in gmatch(CR.text, LINE_PATTERN) do
if (not passedTOC) and match(line, TOC_END_LINE_PATTERN) then
+
if (not passedTOC) and match(line, TOC_END_LINE_PATTERN) then
passedTOC = true
+
passedTOC = true
elseif match(line, BODY_END_LINE_PATTERN) then
+
elseif match(line, BODY_END_LINE_PATTERN) then
break
+
break
elseif passedTOC then
+
elseif passedTOC then
if match(line, title.."$") then
+
if match(line, title.."$") then
collecting = true
+
collecting = true
-- Stupid hack see above
+
-- Stupid hack see above
local _, _, i = SplitLine(line)
+
local _, _, i = SplitLine(line)
heading, major, minor, subrule = ParseIndex(i)
+
heading, major, minor, subrule = ParseIndex(i)
elseif collecting and IsSubsequentRule(line, heading, major, minor, subrule) then
+
elseif collecting and IsSubsequentRule(line, heading, major, minor, subrule) then
break
+
break
  +
end
end
 
  +
 
-- NOT elseif. We want to start collecting lines on the same line we match the target heading
+
-- NOT elseif. We want to start collecting lines on the same line we match the target heading
-- ignore whitespace
+
-- ignore whitespace
if collecting and match(line, "%S") then
+
if collecting and match(line, "%S") then
tinsert(output, line)
+
tinsert(output, line)
  +
end
end
 
end
+
end
end
+
end
  +
 
assert(#output > 0, "Index not found! " .. title)
+
assert(#output > 0, "Index not found! " .. title)
if output then
+
if output then
return tostring(CreateRulesDiv(output,CR_SOURCE,GetLastUpdate(),RULES_COLOR))
+
return tostring(CreateRulesDiv(output,CR_SOURCE,GetLastUpdate(),RULES_COLOR))
else
+
else
return nil
+
return nil
end
+
end
 
end
 
end
   
 
function CR.only(frame)
 
function CR.only(frame)
local index, additionalLevels = frame.args[1], tonumber(frame.args[2])
+
local index, additionalLevels = frame.args[1], tonumber(frame.args[2])
local heading, major, minor, subrule = ParseIndex(index)
+
local heading, major, minor, subrule = ParseIndex(index)
  +
 
local output = {}
+
local output = {}
local passedTOC = false
+
local passedTOC = false
local collecting = false
+
local collecting = false
local ruleDepth, lineDepth = GetNestingDepth(index)
+
local ruleDepth, lineDepth = GetNestingDepth(index)
for line in gmatch(CR.text, LINE_PATTERN) do
+
for line in gmatch(CR.text, LINE_PATTERN) do
if (not passedTOC) and match(line, TOC_END_LINE_PATTERN) then
+
if (not passedTOC) and match(line, TOC_END_LINE_PATTERN) then
passedTOC = true
+
passedTOC = true
elseif match(line, BODY_END_LINE_PATTERN) then
+
elseif match(line, BODY_END_LINE_PATTERN) then
break
+
break
elseif passedTOC then
+
elseif passedTOC then
if match(line, RULE_LINE_PATTERN) then
+
if match(line, RULE_LINE_PATTERN) then
if match(line, "^"..index) then
+
if match(line, "^"..index) then
collecting = true
+
collecting = true
elseif collecting and IsSubsequentRule(line, heading, major, minor, subrule) then
+
elseif collecting and IsSubsequentRule(line, heading, major, minor, subrule) then
  +
break
break
 
  +
end
end
 
  +
end
end
 
  +
 
-- NOT elseif. We want to start collecting lines on the same line we match the target heading
+
-- NOT elseif. We want to start collecting lines on the same line we match the target heading
-- ignore whitespace
+
-- ignore whitespace
if collecting and match(line, "%S") then
+
if collecting and match(line, "%S") then
if additionalLevels then
+
if additionalLevels then
local _, _, index = SplitLine(line)
+
local _, _, additionalIndex = SplitLine(line)
-- This looks a little weird.
+
-- This looks a little weird.
-- We only update lineDepth in the case that we're looking at a rules index
+
-- We only update lineDepth in the case that we're looking at a rules index
-- But we capture any line for which it or the preceding index is within our targeting scope
+
-- But we capture any line for which it or the preceding index is within our targeting scope
-- (examples, mostly)
+
-- (examples, mostly)
  +
if additionalIndex then
if index then
 
lineDepth = GetNestingDepth(index)
+
lineDepth = GetNestingDepth(additionalIndex)
  +
end
end
 
if lineDepth <= ruleDepth + additionalLevels then
+
if lineDepth <= ruleDepth + additionalLevels then
tinsert(output, line)
+
tinsert(output, line)
  +
end
end
 
  +
else
else
 
tinsert(output, line)
+
tinsert(output, line)
  +
break
break
 
  +
end
end
 
  +
end
end
 
end
+
end
end
+
end
  +
 
assert(#output > 0, "Index not found! " .. index)
+
assert(#output > 0, "Index not found! " .. index)
return tostring(CreateRulesDiv(output,CR_SOURCE,GetLastUpdate(),RULES_COLOR))
+
return tostring(CreateRulesDiv(output,CR_SOURCE,GetLastUpdate(),RULES_COLOR))
 
end
 
end
   
 
function CR.full(frame)
 
function CR.full(frame)
local index = frame.args[1]
+
local index = frame.args[1]
local heading, major, minor, subrule = ParseIndex(index)
+
local heading, major, minor, subrule = ParseIndex(index)
  +
 
local output = {}
+
local output = {}
local passedTOC = false
+
local passedTOC = false
local collecting = false
+
local collecting = false
for line in gmatch(CR.text, LINE_PATTERN) do
+
for line in gmatch(CR.text, LINE_PATTERN) do
if (not passedTOC) and match(line, TOC_END_LINE_PATTERN) then
+
if (not passedTOC) and match(line, TOC_END_LINE_PATTERN) then
passedTOC = true
+
passedTOC = true
elseif match(line, BODY_END_LINE_PATTERN) then
+
elseif match(line, BODY_END_LINE_PATTERN) then
break
+
break
elseif passedTOC then
+
elseif passedTOC then
if match(line, RULE_LINE_PATTERN) then
+
if match(line, RULE_LINE_PATTERN) then
if match(line, "^"..index) then
+
if match(line, "^"..index) then
collecting = true
+
collecting = true
elseif collecting and IsSubsequentRule(line, heading, major, minor, subrule) then
+
elseif collecting and IsSubsequentRule(line, heading, major, minor, subrule) then
  +
break
break
 
  +
end
end
 
  +
end
end
 
  +
 
-- NOT elseif. We want to start collecting lines on the same line we match the target heading
+
-- NOT elseif. We want to start collecting lines on the same line we match the target heading
-- ignore whitespace
+
-- ignore whitespace
if collecting and match(line, "%S") then
+
if collecting and match(line, "%S") then
tinsert(output, line)
+
tinsert(output, line)
  +
end
end
 
end
+
end
end
+
end
  +
 
assert(#output > 0, "Index not found! " .. index)
+
assert(#output > 0, "Index not found! " .. index)
return tostring(CreateRulesDiv(output,CR_SOURCE,GetLastUpdate(),RULES_COLOR))
+
return tostring(CreateRulesDiv(output,CR_SOURCE,GetLastUpdate(),RULES_COLOR))
 
end
 
end
   
 
function CR.glossary(frame)
 
function CR.glossary(frame)
local lookup = frame.args[1]
+
local lookup = frame.args[1]
lookup = sanitize(lookup)
+
lookup = sanitize(lookup)
  +
 
local output = {}
+
local output = {}
local passedTOC, passedBody = false, false
+
local passedTOC, passedBody = false, false
local collecting = false
+
local collecting = false
for line in gmatch(CR.text, LINE_PATTERN) do
+
for line in gmatch(CR.text, LINE_PATTERN) do
if (not passedTOC) and match(line, TOC_END_LINE_PATTERN) then
+
if (not passedTOC) and match(line, TOC_END_LINE_PATTERN) then
passedTOC = true
+
passedTOC = true
elseif (not passedBody) and match(line, BODY_END_LINE_PATTERN) then
+
elseif (not passedBody) and match(line, BODY_END_LINE_PATTERN) then
passedBody = true
+
passedBody = true
elseif passedTOC and passedBody and match(line, GLOSSARY_END_LINE_PATTERN) then
+
elseif passedTOC and passedBody and match(line, GLOSSARY_END_LINE_PATTERN) then
break
+
break
elseif passedBody then
+
elseif passedBody then
if match(line, "^"..lookup.."$") or match(line, "^"..lookup.." %(Obsolete%)$") then
+
if match(line, "^"..lookup.."$") or match(line, "^"..lookup.." %(Obsolete%)$") then
collecting = true
+
collecting = true
elseif collecting and not match(line, "%w") then
+
elseif collecting and not match(line, "%w") then
break
+
break
  +
end
end
 
  +
 
-- NOT elseif. We want to start collecting lines on the same line we match the target heading
+
-- NOT elseif. We want to start collecting lines on the same line we match the target heading
-- ignore whitespace
+
-- ignore whitespace
if collecting then
+
if collecting then
tinsert(output, line)
+
tinsert(output, line)
  +
end
end
 
end
+
end
end
+
end
  +
 
assert(#output > 0, "Glossary entry not found! " .. lookup)
+
assert(#output > 0, "Glossary entry not found! " .. lookup)
return tostring(CreateGlossaryDiv(output,GLOSSARY_SOURCE,GetLastUpdate()))
+
return tostring(CreateGlossaryDiv(output,GLOSSARY_SOURCE,GetLastUpdate()))
 
end
 
end
   
 
function CR.CRTemplateCall(frame)
 
function CR.CRTemplateCall(frame)
local toc, exact, lookup, glossary
+
local toc, exact, lookup, glossary
  +
 
for key, value in pairs(frame.args) do
+
for key, value in pairs(frame.args) do
if ((key == "toc") and value ~= "") or (value == "toc") then
+
if ((key == "toc") and value ~= "") or (value == "toc") then
toc = true
+
toc = true
elseif ((key == "exact") and value ~= "") or (value == "exact") then
+
elseif ((key == "exact") and value ~= "") or (value == "exact") then
exact = true
+
exact = true
elseif ((key == "glossary") and value ~= "") or (value == "glossary") then
+
elseif ((key == "glossary") and value ~= "") or (value == "glossary") then
glossary = true
+
glossary = true
elseif value and value ~= "" then
+
elseif value and value ~= "" then
assert(not lookup, "Unknown error, multiple lookups ")
+
assert(not lookup, "Unknown error, multiple lookups ")
lookup = value
+
lookup = value
end
+
end
end
+
end
assert(lookup or toc, "No lookup provided")
+
assert(lookup or toc, "No lookup provided")
  +
 
if toc then
+
if toc then
if not lookup then
+
if not lookup then
return CR.TOC({args={}})
+
return CR.TOC({args={}})
elseif tonumber(lookup) < 10 then
+
elseif tonumber(lookup) < 10 then
return CR.TOC({args={lookup}})
+
return CR.TOC({args={lookup}})
else
+
else
return CR.only({args={lookup, 1}})
+
return CR.only({args={lookup, 1}})
end
+
end
elseif exact then
+
elseif exact then
return CR.only({args={lookup}})
+
return CR.only({args={lookup}})
elseif glossary then
+
elseif glossary then
return CR.glossary({args={lookup}})
+
return CR.glossary({args={lookup}})
else
+
else
if ParseIndex(lookup) then
+
if ParseIndex(lookup) then
return CR.full({args={lookup}})
+
return CR.full({args={lookup}})
else
+
else
local output = CR.title({args={lookup}})
+
local output = CR.title({args={lookup}})
if output then
+
if output then
return output
+
return output
else
+
else
return CR.glossary({args={lookup}})
+
return CR.glossary({args={lookup}})
  +
end
end
 
end
+
end
end
+
end
 
end
 
end
   
 
function CR.FormatRaw(frame)
 
function CR.FormatRaw(frame)
-- can't just unpack these because Scribunto screws with the metatable
+
-- can't just unpack these because Scribunto screws with the metatable
local source, update, rawRulesText, format = frame.args[1], frame.args[2], frame.args[3], frame.args[4]
+
local source, update, rawRulesText, rulesFormat = frame.args[1], frame.args[2], frame.args[3], frame.args[4]
   
-- split the lines so that it'll format correctly
+
-- split the lines so that it'll format correctly
local lines = {}
+
local lines = {}
for line in gmatch(rawRulesText, LINE_PATTERN) do
+
for line in gmatch(rawRulesText, LINE_PATTERN) do
tinsert(lines, line)
+
tinsert(lines, line)
end
+
end
if format == "glossary" then
+
if rulesFormat == "glossary" then
return tostring(CreateGlossaryDiv(lines,source,update))
+
return tostring(CreateGlossaryDiv(lines,source,update))
elseif find(rawRulesText,"(Obsolete)") then
+
elseif find(rawRulesText,"(Obsolete)") then
return tostring(CreateRulesDiv(lines,source,update,OBSOLETE_COLOR))
+
return tostring(CreateRulesDiv(lines,source,update,OBSOLETE_COLOR))
else
+
else
return tostring(CreateRulesDiv(lines,source,update,RULES_COLOR))
+
return tostring(CreateRulesDiv(lines,source,update,RULES_COLOR))
end
+
end
 
end
 
end
   

Latest revision as of 11:54, 18 June 2021

Welcome to the CR, or Comprehensive Rules, Lua module for MTG Wiki. This module exists to minimize the labor of copying of Comprehensive Rules text to the wiki.

Please use the module indirectly via {{CR}}. Using the module directly is more likely to break in the future.

Bug reports and feature requests

Please use the module's talk page to discuss small non-critical bugs in output or to request changes to the text generated or its style.

Maintenance

Most maintenance should be simple. Two changes need to be made when a new version of the Comp Rules is available: updating the name of the newest set, and copy and pasting the newest rules.

Standard

Updating the most recent set name

Update {{CURRENTSET}} with the plain (unlinked) name of the set.

Updating the full Comprehensive Rules text

Main article: Module:CR/rules

At the top of the rules subpage, you should see:

return {
-- BEGIN COMP RULES
text = [[

And following that line, the entire plaintext Comprehensive Rules is included, spanning several thousand lines, followed by:

]]
-- END COMP RULES
}

The most recent official version of the Comprehensive Rules can currently be found on the Wizards of the Coast website.

No further editing of this module or of the rules themselves should be necessary. The module ignores blank lines and can find its way by the indices in the CR.

Nonstandard

If a plaintext version of the Comp Rules update is unavailable, but could be created from another version of the Comp Rules, this module:

  • ignores blank lines
  • expects the table of contents at the beginning of the Comp Rules to be present
  • expects each rules index in the table of contents and the body of the rules to be separated by newlines
  • expects each line in the table of contents and the body of the rules to begin with the rules index for that line
  • expects the table of contents to be immediately followed, and separated from body of the rules, by the string TOC_END_LINE
  • expects the body of the rules to be immediately followed by BODY_END_LINE

In the unlikely event that Wizards of the Coast should overhaul the formatting of the Comp Rules, this module will require a major rewrite.

If such a rewrite is necessary, or something else has gone horribly wrong, try contacting any of the following previous maintainers:

  • corveroth@gmail.com

Direct Usage

In the event that a template does not serve your purposes, if you must invoke this module directly, the following functions are provided:

  • CR.TOC returns a listing from the table of contents section of the Comprehensive Rules.
When called with an integer value, it returns the list for a section.
For example, {{#invoke:CR|TOC|1}} will return the table of contents for Game Concepts.
When called with no value, it returns the full table of contents.
{{#invoke:CR|TOC|}}
  • CR.full returns the text at the given index in the Comprehensive Rules including all nested subsections.
For example, {{#invoke:CR|full|112}} will return the entire section on Abilities.
  • CR.title returns the text matching the specified title line in the Comprehensive Rules. Aside from using a title rather than an index, it operates identically to CR.full.
{{#invoke:CR|title|Abilities}} will return the entire section on Abilities, just like {{#invoke:CR|full|112}} (as of this writing).
  • CR.only returns only the text at the specified index, not including subsections.
For example, {{#invoke:CR|only|111.1}} will return the definition of a Spell, without the 111.1a and 111.1b subsections detailing copies.
  • CR.only also accepts a second parameter specifying a number of additional levels; a compromise between CR.only and CR.full.
For example, {{#invoke:CR|only|702|1}} will the list of keyword abilities, without their descriptions.
  • CR.glossary returns the text matching the specified title line in the Glossary of the Comprehensive Rules.

Generally, CR.title or CR.full will yield the desired results, because the Comprehensive Rules are shallowly nested. CR.title is recommended because titles are less likely to change than indices. That is, while the section detailing Trample will likely retain a title index containing simply "Trample", it may no longer reside at index 702.19 as further keywords are added to rule 702.

CR.only is provided for the sake of completeness, and may be useful for cases such as contrasting related rules at distant indices.

CR.TOC is expected to see use on pages covering broad rules topics. CR.only|index|levels is an alternative for narrower, but still broad topics, such as Keyword abilities.


local CR = {}

-- load rules text
CR.text =  mw.loadData( 'Module:CR/rules' ).text

-- locals for performance
local format, find, match, sub, gsub, gmatch, rep, lower, upper = string.format, string.find, string.match, string.sub, string.gsub, string.gmatch, string.rep, string.lower, string.upper
local tonumber, tostring, type, assert, ipairs = tonumber, tostring, type, assert, ipairs
local tinsert = table.insert

-- *_PATTERN: used to match CR text
-- *_FORMAT: used for formatting output

-- We need to split the text via LINE_PATTERN before we can use the beginning-of-string caret
-- Otherwise, we would need a more complicated pattern to match all possible indices
-- (Off the top of my head, I'm not sure Lua's pattern matching *could* do it in one pattern. It's not a full regex suite.)
local LINE_PATTERN = "(.-)\n"
local RULE_LINE_PATTERN = "^%d"
local TOC_END_LINE_PATTERN = "^Glossary"
local BODY_END_LINE_PATTERN  = "^Glossary"
local GLOSSARY_END_LINE_PATTERN = "^Credits"
local GENERAL_RULE_PATTERN = "General"

-- local WIKILINK_FORMAT = "[[%s]]" -- Condider: no-longer used, remove it entirely
local WIKILINK_ALT_FORMAT = "[[%s|%s]]"
local GENERAL_RULE_EXPANDED_TEXT_FORMAT = "General_(%s)"
local CASUAL_FORMAT_EXPANDED_TEXT_FORMAT = "%s_(format)"

-- Note that, per the Introduction to the Comprehensive Rules, the subrules skip the letters L and O to avoid confusion with numerals.
local INDEX_SUBRULE_CHARACTER_SET = "ABCDEFGHIJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz"

-- This is the format of the line in the Comp Rules where the date is found. As of last edit, it's the second line of text.
-- The date itself is wrapped in parentheses to extract that text alone.
local COMP_RULES_DATE_PATTERN = "These rules are effective as of ([^%.]+)."

-- "From the Comprehensive Rules (March 27, 2015—Dragons of Tarkir)"
local LAST_UPDATED_FORMAT = "<p class=\"crTitle\">From the %s (%s)</p>"

-- String to add one obsolete rules int the RulesDiv.
local OBSOLETE_PATTERN = "&nbsp;'''([[Obsolete]])'''"

--Settings

--Sources
local CR_SOURCE = "''[[Comprehensive Rules]]''"
local GLOSSARY_SOURCE = "glossary of the "..CR_SOURCE

--Categories
local GLOSSARY_CAT = "[[Category:Glossary]]"
local OBSOLETE_CAT = "[[Category:Obsolete]]"

--Colors
local RULES_COLOR = "var(--theme-page-background-color)"
local GLOSSARY_COLOR = "var(--theme-page-accent-mix-color)"
local OBSOLETE_COLOR = "var(--theme-body-background-color)"

-- Escape magic characters in a string.
local sanitize
do
    local matches =
    {
        ["^"] = "%^";
        ["$"] = "%$";
        ["("] = "%(";
        [")"] = "%)";
        ["%"] = "%%";
        ["."] = "%.";
        ["["] = "%[";
        ["]"] = "%]";
        ["*"] = "%*";
        ["+"] = "%+";
        ["-"] = "%-";
        ["?"] = "%?";
    }

    sanitize = function(s)
        return (gsub(s, ".", matches))
    end
end

local MOST_RECENT_SET_NAME = mw.getCurrentFrame():expandTemplate({title = "CURRENTSET" })

local function GetLastUpdate()
    local lastEffectiveDate = match(CR.text, COMP_RULES_DATE_PATTERN)
    -- Italicize and wikilink the set name
    local setName = format("''[[%s]]''", MOST_RECENT_SET_NAME)
    local lastUpdate = lastEffectiveDate .. "—" .. setName
    return lastUpdate
end

local function SplitLine(ruleLine)
    -- Finds the index and the rest. If the index has an extra period, it is considered a formatting issue in the CR and is therefore ignored.
    local i, j, index, rest = find(ruleLine, "^(%d+%.%d*[%.a-kmnp-z]?)%.?%s(.+)")
    return i, j, index, rest
end

--[[
    Chops up and validates an index.
    Individually breaking down index parts here for ease of comprehension and future maintenance.
    Yes, a clever soul could streamline this with more complex pattern matching.
    Clever and absolute performance are not the goals. Editability by anonymous maintainers is.

    heading, major, and minor should all be number or nil
    returns false instead if the string provided is not an index
--]]
local function ParseIndex(index)
    local heading, major, minor, subrule

    if match(index, "%s") then
        return false
    end

    local i, _, suffix = find(index, "%.(.+)")
    if suffix then
        -- If the last character in the suffix is alphanumeric (and not L or O), set the subrule
        subrule = match(sub(suffix, -1), "(["..INDEX_SUBRULE_CHARACTER_SET.."])")

        -- If that was successful, cut that character off the suffix.
        if subrule then
            suffix = sub(suffix, 1, -2)
        end

        -- Now we can easily check whether the part between the period and the letter is a number.
        -- If so, that's our minor index
        minor = tonumber(suffix)
        assert(type(minor) == "number", "Invalid index!")

        -- Now cut off the entire suffix and let's parse the rest
        index = sub(index, 1, i-1)
    end

    -- Getting the heading and major index is just some number manipulation
    index = tonumber(index)
    -- assert(type(index) == "number", "Invalid index!")
    if not index then
        return false
    end

    if index >= 100 then
        major = index%100
        heading = math.floor(index/100)
    else
        -- The body of the rules starts at 100, lower values are headings
        heading = index
    end

    return heading, major, minor, subrule
end

local function IsSubsequentRule(line, heading, major, minor, subrule)
    local _, _, index = SplitLine(line)
    if not index then
        return false
    end

    local h, a, i, _ = ParseIndex(index)

    if subrule then
        -- We can cheat like hell if we're dealing with subrules because there's no further nesting
        -- Therefore, the next line is the next rule by definition
        -- That said, if this ever changes, this snippet might be useful
        --[[
            -- Yes, this makes assumptions about character encoding and subrules never numbering more than 24 for a given rule
            local i = find(INDEX_SUBRULE_CHARACTER_SET, subrule) + 1
            nextIndex = sub(INDEX_SUBRULE_CHARACTER_SET, i, i)
        --]]
        return true
    elseif i and minor and i > minor then
        return true
    elseif a and major and a > major then
        return true
    elseif h and heading and h > heading then
        return true
    end
end

local function GetNestingDepth(index)
    local depth

    if match(index, "["..INDEX_SUBRULE_CHARACTER_SET.."]") then
        -- Subrules, e.g. 103.7a
        depth = 4
    elseif match(index, "%d+%.%d+%.") then
        -- Rules, e.g. 112.1.
        depth = 3
    elseif match(index, "^%d%d%d%.") or match(index, "^%d+%.%d+") then
        -- Titles, e.g. 102. or 1.2 for normal numbering conventions
        depth = 2
    else
        -- Headings, e.g. 1.
        depth = 1
    end

    return depth
end

local function Titleize(title)
    local link, t, bare
    if match(title,"%(Obsolete%)$") then
        bare = sub(title,0,-12)
        link = lower(bare)
        link = gsub(link, "^(%S)", upper)
        t = format(WIKILINK_ALT_FORMAT, link, bare)
        t = t..OBSOLETE_PATTERN
    else
        link = lower(title)
        -- convert the first letter back to uppercase
        link = gsub(link, "^(%S)", upper)
        t = format(WIKILINK_ALT_FORMAT, link, title)
    end
    return t
end

local function StylizeRule(ruleLine)
    local _, _, index, rest = SplitLine(ruleLine)
    if not index then
        if find(ruleLine, "Example:") then
            ruleLine = "<p class=\"crExample\">''" .. gsub(ruleLine, "(Example:)", "'''%1'''") .. "''</p>"
        end
        return ruleLine
    end

    local h, a, i, _ = ParseIndex(index)
    -- Major indices and any rule shorter than five words should be a title, so try linking it!
    -- (this is probably a stupid assumption let's see how long before we get burned)
    if (h and a and not i) then
        if find(rest, GENERAL_RULE_PATTERN) then
            -- Each heading in the CR has a "General" section, expand that to "General_(<name of header>)"
            local headingName
            for line in gmatch(CR.text, LINE_PATTERN) do
                headingName = match(line, "^"..h.."%. (.+)")
                if headingName then
                    break
                end
            end
            assert(headingName)
            headingName = lower(headingName)
            headingName = gsub(headingName, "^(%S)", upper)
            local expandedLink = format(GENERAL_RULE_EXPANDED_TEXT_FORMAT, headingName)
            if match(rest,"%(Obsolete%)$") then
                local bare = sub(rest,0,-12)
                rest = format(WIKILINK_ALT_FORMAT, expandedLink, bare)..OBSOLETE_PATTERN
            else
                rest = format(WIKILINK_ALT_FORMAT, expandedLink, rest)
            end
        elseif match(index, "90[1-4]") then
            -- The casual format articles usually have a "<Name of format>_(format)" nameing pattern

            local expandedLink = format(CASUAL_FORMAT_EXPANDED_TEXT_FORMAT, rest)
            rest = format(WIKILINK_ALT_FORMAT, expandedLink, rest)
        else
            rest = Titleize(rest)
        end
    else
        local _, numWords = gsub(rest, "%S+", "")
        if numWords < 5 then
            rest = Titleize(rest)
        end
    end

    return format("'''%s''' %s", index, rest)
end

-- Creates a mw.html object matching the styling of the old CR template
local function CreateRulesDiv(output,source,update,color)
    local div = mw.html.create("div")
    div:addClass("crDiv")
    div:addClass("crBox")
    div:css("background-color", color)
    local ns = mw.title.getCurrentTitle().namespace
    if color == OBSOLETE_COLOR and ns == 0 then
        div:wikitext(OBSOLETE_CAT)
    end
    div:wikitext(format(LAST_UPDATED_FORMAT, source, update))
    div:newline()
    if type(output) == "string" then
        local line = StylizeRule(output)
        div:wikitext("* ", line)
    else
        local indentLevel
        local prevMax = 0
        local outputLine, maxIndent, index, _
        for _, line in ipairs(output) do
            outputLine = StylizeRule(line)
            _, _, index = SplitLine(line)
            if index then
                div:newline()
                maxIndent = GetNestingDepth(index)
                if not indentLevel then
                    indentLevel = 1
                else
                    indentLevel = indentLevel + (maxIndent - prevMax)
                end
                prevMax = maxIndent
                outputLine = rep("*",  indentLevel) .. outputLine
            else
                if not find(outputLine, "Example:") then
                    outputLine = "<br/>" .. outputLine
                else
                    outputLine = outputLine
                end
            end

            div:wikitext(outputLine)
        end
    end

    return div
end

-- Creates a mw.html object with a slightly different background color
local function CreateGlossaryDiv(output,source,update)
    local div = mw.html.create("div")
    div:addClass("crBox")

    local ns = mw.title.getCurrentTitle().namespace
    div:wikitext(format(LAST_UPDATED_FORMAT, source, update))
    div:newline()

    for i, line in ipairs(output) do
        -- Bold the name of the entry for clarity
        if i == 1 then
            if match(line,"%(Obsolete%)$") then
                -- Link and categorize obsolete terms
                div:css("background-color", OBSOLETE_COLOR)
                div:wikitext(";" .. gsub(line,"%(Obsolete%)","%([[Obsolete]]%)") .. "\n")
                if ns == 0 then
                    div:wikitext(OBSOLETE_CAT)
                end
            else
                div:css("background-color", GLOSSARY_COLOR)
                div:wikitext(";" .. line .. "\n")
            end
        else
            div:wikitext(":" .. line .. "\n")
        end
    end
    -- Add glossary category in mainspace articles
    if ns == 0 then
        div:wikitext(GLOSSARY_CAT)
    end

    return div
end

function CR.TOC(frame)
    local index = frame.args[1]
    local heading, major, minor, subrule
    -- If there's no index, we want the full TOC. Otherwise, pass it in for validation.
    local fullTOC
    if (not index) or type(index)=="string" and index=="" then
        heading = 1
        fullTOC = true
    else
        heading, major, minor, subrule = ParseIndex(index)
        assert(heading and heading>=1 and heading<=10 and not major and not minor and not subrule, "Invalid table of contents index!")
    end

    local output = {}

    local collecting = false
    for line in gmatch(CR.text, LINE_PATTERN) do
        if match(line, RULE_LINE_PATTERN) then
            if match(line, "^"..heading.."%.") then
                collecting = true
            elseif (not fullTOC) and IsSubsequentRule(line, heading, major, minor, subrule) then
                break
            end

            -- NOT elseif. We want to start collecting lines on the same line we match the target heading
            if collecting then
                tinsert(output, line)
            end
        elseif match(line, TOC_END_LINE_PATTERN) then
            break
        end
    end

    assert(#output > 0, "Index not found! ", index)
    return tostring(CreateRulesDiv(output,CR_SOURCE,GetLastUpdate(),RULES_COLOR))
end

-- Basically CR.full but with the full text of an index
function CR.title(frame)
    local title = frame.args[1]
    title = sanitize(title)

    local output = {}
    local passedTOC = false
    local collecting = false
    local heading, major, minor, subrule -- this is a stupid hack to continue using the original index-based stuff
    for line in gmatch(CR.text, LINE_PATTERN) do
        if (not passedTOC) and match(line, TOC_END_LINE_PATTERN) then
            passedTOC = true
        elseif match(line, BODY_END_LINE_PATTERN) then
            break
        elseif passedTOC then
            if match(line, title.."$") then
                collecting = true
                -- Stupid hack see above
                local _, _, i = SplitLine(line)
                heading, major, minor, subrule = ParseIndex(i)
            elseif collecting and IsSubsequentRule(line, heading, major, minor, subrule) then
                break
            end

            -- NOT elseif. We want to start collecting lines on the same line we match the target heading
            -- ignore whitespace
            if collecting and match(line, "%S") then
                tinsert(output, line)
            end
        end
    end

    assert(#output > 0, "Index not found! " .. title)
    if output then
        return tostring(CreateRulesDiv(output,CR_SOURCE,GetLastUpdate(),RULES_COLOR))
    else
        return nil
    end
end

function CR.only(frame)
    local index, additionalLevels = frame.args[1], tonumber(frame.args[2])
    local heading, major, minor, subrule = ParseIndex(index)

    local output = {}
    local passedTOC = false
    local collecting = false
    local ruleDepth, lineDepth = GetNestingDepth(index)
    for line in gmatch(CR.text, LINE_PATTERN) do
        if (not passedTOC) and match(line, TOC_END_LINE_PATTERN) then
            passedTOC = true
        elseif match(line, BODY_END_LINE_PATTERN) then
            break
        elseif passedTOC then
            if match(line, RULE_LINE_PATTERN) then
                if match(line, "^"..index) then
                    collecting = true
                elseif collecting and IsSubsequentRule(line, heading, major, minor, subrule) then
                    break
                end
            end

            -- NOT elseif. We want to start collecting lines on the same line we match the target heading
            -- ignore whitespace
            if collecting and match(line, "%S") then
                if additionalLevels then
                    local _, _, additionalIndex = SplitLine(line)
                    -- This looks a little weird.
                    -- We only update lineDepth in the case that we're looking at a rules index
                    -- But we capture any line for which it or the preceding index is within our targeting scope
                    -- (examples, mostly)
                    if additionalIndex then
                        lineDepth = GetNestingDepth(additionalIndex)
                    end
                    if lineDepth <= ruleDepth + additionalLevels then
                        tinsert(output, line)
                    end
                else
                    tinsert(output, line)
                    break
                end
            end
        end
    end

    assert(#output > 0, "Index not found! " .. index)
    return tostring(CreateRulesDiv(output,CR_SOURCE,GetLastUpdate(),RULES_COLOR))
end

function CR.full(frame)
    local index = frame.args[1]
    local heading, major, minor, subrule = ParseIndex(index)

    local output = {}
    local passedTOC = false
    local collecting = false
    for line in gmatch(CR.text, LINE_PATTERN) do
        if (not passedTOC) and match(line, TOC_END_LINE_PATTERN) then
            passedTOC = true
        elseif match(line, BODY_END_LINE_PATTERN) then
            break
        elseif passedTOC then
            if match(line, RULE_LINE_PATTERN) then
                if match(line, "^"..index) then
                    collecting = true
                elseif collecting and IsSubsequentRule(line, heading, major, minor, subrule) then
                    break
                end
            end

            -- NOT elseif. We want to start collecting lines on the same line we match the target heading
            -- ignore whitespace
            if collecting and match(line, "%S") then
                tinsert(output, line)
            end
        end
    end

    assert(#output > 0, "Index not found! " .. index)
    return tostring(CreateRulesDiv(output,CR_SOURCE,GetLastUpdate(),RULES_COLOR))
end

function CR.glossary(frame)
    local lookup = frame.args[1]
    lookup = sanitize(lookup)

    local output = {}
    local passedTOC, passedBody = false, false
    local collecting = false
    for line in gmatch(CR.text, LINE_PATTERN) do
        if (not passedTOC) and match(line, TOC_END_LINE_PATTERN) then
            passedTOC = true
        elseif (not passedBody) and match(line, BODY_END_LINE_PATTERN) then
            passedBody = true
        elseif passedTOC and passedBody and match(line, GLOSSARY_END_LINE_PATTERN) then
            break
        elseif passedBody then
            if match(line, "^"..lookup.."$") or match(line, "^"..lookup.." %(Obsolete%)$") then
                collecting = true
            elseif collecting and not match(line, "%w") then
                break
            end

            -- NOT elseif. We want to start collecting lines on the same line we match the target heading
            -- ignore whitespace
            if collecting then
                tinsert(output, line)
            end
        end
    end

    assert(#output > 0, "Glossary entry not found! " .. lookup)
    return tostring(CreateGlossaryDiv(output,GLOSSARY_SOURCE,GetLastUpdate()))
end

function CR.CRTemplateCall(frame)
    local toc, exact, lookup, glossary

    for key, value in pairs(frame.args) do
        if ((key == "toc") and value ~= "") or (value == "toc") then
            toc = true
        elseif ((key == "exact") and value ~= "") or (value == "exact") then
            exact = true
        elseif ((key == "glossary") and value ~= "") or (value == "glossary") then
            glossary = true
        elseif value and value ~= "" then
            assert(not lookup, "Unknown error, multiple lookups ")
            lookup = value
        end
    end
    assert(lookup or toc, "No lookup provided")

    if toc then
        if not lookup then
            return CR.TOC({args={}})
        elseif tonumber(lookup) < 10 then
            return CR.TOC({args={lookup}})
        else
            return CR.only({args={lookup, 1}})
        end
    elseif exact then
        return CR.only({args={lookup}})
    elseif glossary then
        return CR.glossary({args={lookup}})
    else
        if ParseIndex(lookup) then
            return CR.full({args={lookup}})
        else
            local output = CR.title({args={lookup}})
            if output then
                return output
            else
                return CR.glossary({args={lookup}})
            end
        end
    end
end

function CR.FormatRaw(frame)
    -- can't just unpack these because Scribunto screws with the metatable
    local source, update, rawRulesText, rulesFormat = frame.args[1], frame.args[2], frame.args[3], frame.args[4]

    -- split the lines so that it'll format correctly
    local lines = {}
    for line in gmatch(rawRulesText, LINE_PATTERN) do
        tinsert(lines, line)
    end
    if rulesFormat == "glossary" then
        return tostring(CreateGlossaryDiv(lines,source,update))
    elseif find(rawRulesText,"(Obsolete)") then
        return tostring(CreateRulesDiv(lines,source,update,OBSOLETE_COLOR))
    else
        return tostring(CreateRulesDiv(lines,source,update,RULES_COLOR))
    end
end

return CR