Module:MaroScaleSandbox: Difference between revisions

From MTG Wiki
Jump to navigation Jump to search
>Jerodast
(I thiiiink this wouldn't make a difference, and is desirable because we DO want to let very long lists word wrap if needed)
>Jerodast
(handle blank table type better; more concise console test)
 
(10 intermediate revisions by the same user not shown)
Line 13: Line 13:
       since the current simple serial list presentation makes ratings appear
       since the current simple serial list presentation makes ratings appear
       uniformly spaced when they're not
       uniformly spaced when they're not
Lua debug console test run:
mw.log(p.TableBuilder(mw.getCurrentFrame():newChild{title="Template:TEST",args={type = "console", entry3 = "Noratings", name3  = "Redlink", note3 = "This entry has no ratings and may be ignored!", printings3 = "Ravnica, Mirrodin, War of the Spark,", ratings2 = "{3,2018-10-17,<sup>[4]</sup>},{4,2019-08-10,<sup>[4]</sup> No entry!},", entry4 = "Mix ratings and printings", ratings4 = "{5,2013-08-10,<sup>[4]</sup>},{6,2018-10-17,<sup>[4]</sup>},{7,2019-08-10,<sup>[4]</sup>},{8,2021-08-10,<sup>[4]</sup>},", printings4 = "rtr, Lorwyn, Rivals Of Ixalan, WAR",}}:newChild{title="Module:MaroScaleSandbox"}))
]]
]]


Line 51: Line 54:
local MAX_ENTRIES = 500
local MAX_ENTRIES = 500
-- A list of all sets which have been Standard-legal since the introduction of the Storm Scale (September 2012)
-- A list of all sets which have been Standard-legal since the introduction of the Storm Scale (September 2012)
local LEGAL_EXPANSIONS = {
-- See initLegalExpansions func for how this is generated from LEGAL_EXPANSIONS_DATA
["Return to Ravnica"] = "2012-10-05",
local LEGAL_EXPANSIONS = {}
["Gatecrash"] = "2013-02-01",
local LEGAL_EXPANSIONS_DATA = {
["Dragon's Maze"] = "2013-05-03",
{"2012-10-05", "RTR", "Return to Ravnica"},
["Magic 2014"] = "2013-06-13",
{"2013-02-01", "GTC", "Gatecrash"},
["Theros"] = "2013-09-27",
{"2013-05-03", "DGM", "Dragon's Maze"},
["Born of the Gods"] = "2014-02-07",
{"2013-06-13", "M14", "Magic 2014"},
["Journey into Nyx"] = "2014-05-02",
{"2013-09-27", "THS", "Theros"},
["Magic 2015"] = "2014-07-18",
{"2014-02-07", "BNG", "Born of the Gods"},
["Khans of Tarkir"] = "2014-09-26",
{"2014-05-02", "JOU", "Journey into Nyx"},
["Fate Reforged"] = "2015-01-23",
{"2014-07-18", "M15", "Magic 2015"},
["Dragons of Tarkir"] = "2015-03-17",
{"2014-09-26", "KTK", "Khans of Tarkir"},
["Magic Origins"] = "2015-07-15",
{"2015-01-23", "FRF", "Fate Reforged"},
["Battle for Zendikar"] = "2015-10-02",
{"2015-03-17", "DTK", "Dragons of Tarkir"},
["Oath of the Gatewatch"] = "2016-01-22",
{"2015-07-15", "ORI", "Magic Origins"},
["Shadows over Innistrad"] = "2016-04-08",
{"2015-10-02", "BFZ", "Battle for Zendikar"},
["Eldritch Moon"] = "2016-06-22",
{"2016-01-22", "OGW", "Oath of the Gatewatch"},
["Kaladesh"] = "2016-09-30",
{"2016-04-08", "SOI", "Shadows over Innistrad"},
["Aether Revolt"] = "2016-01-20",
{"2016-06-22", "EMN", "Eldritch Moon"},
["Amonkhet"] = "2017-04-28",
{"2016-09-30", "KLD", "Kaladesh"},
["Hour of Devastation"] = "2017-07-14",
{"2016-01-20", "AER", "Aether Revolt"},
["Ixalan"]                  = "2017-09-29",
{"2017-04-28", "AKH", "Amonkhet"},
["Rivals of Ixalan"]        = "2018-01-19",
{"2017-07-14", "HOU", "Hour of Devastation"},
["Dominaria"]                = "2018-04-27",
{"2017-09-29", "XLN", "Ixalan"},
["Core Set 2019"]            = "2018-07-13",
{"2018-01-19", "RIX", "Rivals of Ixalan"},
["Guilds of Ravnica"]        = "2018-10-05",
{"2018-04-27", "DOM", "Dominaria"},
["Ravnica Allegiance"]      = "2019-01-25",
{"2018-07-13", "M19", "Core Set 2019"},
["War of the Spark"]        = "2019-05-03",
{"2018-10-05", "GRN", "Guilds of Ravnica"},
["Core Set 2020"]            = "2019-07-12",
{"2019-01-25", "RNA", "Ravnica Allegiance"},
["Throne of Eldraine"]      = "2019-10-04",
{"2019-05-03", "WAR", "War of the Spark"},
["Theros Beyond Death"]      = "2020-01-24",
{"2019-07-12", "M20", "Core Set 2020"},
["Ikora: Lair of Behemoths"] = "2020-05-15",
{"2019-10-04", "ELD", "Throne of Eldraine"},
["Core Set 2021"]            = "2020-07-03",
{"2020-01-24", "THB", "Theros Beyond Death"},
["Zendikar Rising"]          = "2020-09-25",
{"2020-05-15", "IKO", "Ikora: Lair of Behemoths"},
["Kaldheim"]                = "2021-02-05",
{"2020-07-03", "M21", "Core Set 2021"},
["Strixhaven"]              = "2021-04-23",
{"2020-09-25", "ZNR", "Zendikar Rising"},
["Dungeons & Dragons: Adventures in the Forgotten Realms"] = "2021-07-23",
{"2021-02-05", "KHM", "Kaldheim"},
["Innistrad: Midnight Hunt"] = "2021-09-24",
{"2021-04-23", "STX", "Strixhaven"},
["Innistrad: Crimson Vow"]  = "2021-11-19",
{"2021-07-23", "AFR", "Dungeons & Dragons: Adventures in the Forgotten Realms"},
["Kamigawa: Neon Dynasty"]  = "2022-02-18",
{"2021-09-24", "MID", "Innistrad: Midnight Hunt"},
["Streets of New Capenna"]  = "2022-04-29",
{"2021-11-19", "VOW", "Innistrad: Crimson Vow"},
["Dominaria United"]        = "2020-09-09",
{"2022-02-18", "NEO", "Kamigawa: Neon Dynasty"},
["The Brothers' War"]        = "2020-11-18",
{"2022-04-29", "SNC", "Streets of New Capenna"},
{"2020-09-09", "DMU", "Dominaria United"},
{"2020-11-18", "BRO", "The Brothers' War"},
}
}


-- Wikitext for a simple link; displayText is optional as usual
-- sets up LEGAL_EXPANSIONS so that all set data can be queried with either
local function generateLink(target, displayText)
--  the set code, its full name, or alt names; also label the fields
if displayText then
local function initLegalExpansions()
return "[["..displayText.."|"..target.."]]"
for _,setData in ipairs(LEGAL_EXPANSIONS_DATA) do
    --(musing from jerodast - based on my new understanding of Lua, this could
    --  be done with __index metatable for each setData; doesn't seem worth)
for i=2,#setData do
LEGAL_EXPANSIONS[mw.ustring.lower(setData[i])] = setData
end
setData.releaseDate = setData[1]
setData.code = setData[2]
setData.name = setData[3]
-- LEGAL_EXPANSIONS[setData.code] = setData
-- LEGAL_EXPANSIONS[setData.name] = setData
end
end
 
-- Wikitext for a simple link, with alternate "target" if needed
-- Note params are backwards from wikitext format
local function generateLink(displayText, altLink)
if altLink then
return "[["..altLink.."|"..displayText.."]]"
else
else
return "[["..target.."]]"
return "[["..displayText.."]]"
end
end
end
end
Line 123: Line 148:
local function parseRatings(raw)
local function parseRatings(raw)
local outputList = {}
local outputList = {}
if not raw then return 0, outputList end
local newestDate, newestRating = 0, 0
local newestDate, newestRating = 0, 0
local rating, refDate, refString, t
local rating, refDate, refString, t
Line 158: Line 186:
]]
]]
local function addPrintings(timeline, printings)
local function addPrintings(timeline, printings)
local releaseDate, t
local setData, releaseDate, t
-- Parse each list element delimited by commas
-- Parse each list element delimited by commas
gsub(printings, "([^,]+)", function(set)
gsub(printings, "([^,]+)", function(set)
releaseDate = LEGAL_EXPANSIONS[set]
setData = LEGAL_EXPANSIONS[mw.ustring.lower(mw.text.trim(set))]
-- The set provided doesn't match a post-Storm Scale set
if not setData then return end
-- The set name provided doesn't match a post-Storm Scale set
rd = setData.releaseDate
if not releaseDate then return end
-- Dates MUST be yyyy-mm-dd
-- Dates MUST be yyyy-mm-dd
t = os.time({year=sub(releaseDate, 1,4), month=sub(releaseDate, 6,7), day=sub(releaseDate, 9,10),})
t = os.time({year=sub(rd, 1,4), month=sub(rd, 6,7), day=sub(rd, 9,10),})
table.insert(timeline, {text = generateLink(set), t = t})
table.insert(timeline, {text = generateLink(setData.code, setData.name), t = t})
end)
end)
return timeline
return timeline
end
end
Line 192: Line 221:
Entry point for this module. Turns all template arguments into a table as wikitext.
Entry point for this module. Turns all template arguments into a table as wikitext.


See Template:MaroScale for parameter documentation. Note that for any entry###,
See Template:MaroScale for parameter documentation.
ratings### is required or the entry will be ignored.
  * any entry-level params without a corresponding entry### param will be ignored
]]
]]
function MaroScaleSandbox.TableBuilder(frame)
function MaroScaleSandbox.TableBuilder(frame)
Line 199: Line 228:
-- frame.args:            Module/invoke parameters (none expected)
-- frame.args:            Module/invoke parameters (none expected)
-- frame:getParent().args: Template parameters (nameI, entryI, noteI, etc)
-- frame:getParent().args: Template parameters (nameI, entryI, noteI, etc)
initLegalExpansions()
-- Imitating navbox, here. I trust they know what they're doing.
-- Imitating navbox, here. I trust they know what they're doing.
-- Read the arguments in the order they'll be output in, to make references number in the right order.
-- Read the arguments in the order they'll be output in, to make references number in the right order.
--[[ Commented out by jerodast:
--[[ Commented out by jerodast:
I don't understand why this is necessary; pairs already works on .args
I don't understand why this is necessary. Pairs already works
and the list will be re-sorted anyway...
on .args and the list will be re-sorted anyway.
     local _
     local _
     _ = args.type
     _ = args.type
Line 220: Line 251:
local entryNums = {}
local entryNums = {}
for k, v in pairs(args) do
for k, v in pairs(args) do
-- Extract param numbers based on ratings### parameter (required)
-- Extract param numbers based on entry### parameter (required)
local eNum = match('' .. k, '^ratings(%d+)$')
local eNum = match('' .. k, '^entry(%d+)$')
if eNum then
if eNum then
table.insert(entryNums, {num = tonumber(eNum), entry = args["entry"..eNum]})
table.insert(entryNums, {num = tonumber(eNum), entry = v})
end
end
end
end
Line 237: Line 268:
local efnGroup
if args.type then efnGroup = args.type .. "_stormscale_footnote"
else              efnGroup = "stormscale_footnote"
end
local hasNotes = false
local hasNotes = false
Line 257: Line 292:
hasNotes = true
hasNotes = true
-- Use {{efn}} template (explanatory footnote)
-- Use {{efn}} template (explanatory footnote)
linkedEntry = linkedEntry .. frame:expandTemplate({title = "efn", args = { note }})
linkedEntry = linkedEntry .. frame:expandTemplate(
{title = "efn", args = { note, group = efnGroup }}
)
end
end
Line 276: Line 313:
-- Second cell
-- Second cell
row:tag("td"):wikitext(lastRating)
if lastRating == 0 then
row:tag("td"):wikitext("None"):attr("data-sort-value",0)
else
row:tag("td"):wikitext(lastRating)
end
-- Third cell
-- Third cell
Line 302: Line 343:
--  ...I think. (Maybe it breaks completely.)
--  ...I think. (Maybe it breaks completely.)
if hasNotes then
if hasNotes then
return tostring(tbl) .. frame:expandTemplate({title = "notelist"})
return tostring(tbl) .. frame:expandTemplate({title = "notelist", args = {group = efnGroup}})
else
else
return tbl
return tbl

Latest revision as of 08:23, 6 July 2022

This is a sandbox page for experimenting with the Module:MaroScale lua script. Click for /doc access.

Goals:

  • Learn a bit more how it's used/about Modules
  • Make some updates/improvements (see script comments for details)

User:jerodast (talk) will request to delete this page once any useful changes are merged to core MaroScale page.

Since this module gets arguments from frame:getParent(), its use is best exemplified / tested via the calling template. These examples don't use real references, which in practice would be true citations with a full reference footnote. In these examples, ratings will always be EXPECTED to be in ascending order once sorted by date, to easily verify correctness.

Let's put[a] in some[b] fake footnotes to test compatibility of tables & other footnotes.[c]

PlaneLatest rankingTimeline
Innistrad11[4]
Ravnica[d]11[4]
  1. First fake footnote
  2. Second
  3. Final fake footnote!
  4. Worst plane ever
MechanicLatest rankingTimeline
-1/-1 counters21[4], 2[4]
Grind65[4], 6[4]
Rhystic99[4]
Creature typeLatest rankingTimeline
Beeble108[4], 10[4]
Dauthi66[4]
Djinn44[2] Reprinted in Khans of Tarkir block and Dominaria.
Dragon11[1] Iconic
Elf11[1] Characteristic
Goblin11[1] Characteristic
Human11[1] Characteristic
Hydra11[1] Iconic
Werewolf33[2] A 6 outside of Innistrad.
PlaneswalkerLatest rankingTimeline
Kiora44[4]
Nahiri43[4], 4[4]
Tibalt75[4], 6[4], 7[4]
Urza99[4]

Quick links for live pages:

TESTLatest rankingTimeline
Entry is the VISIBLE title55[7]
Mix ratings and printings (Lorwyn is there but should be ignored)8RTR, 5[4], RIX, 6[4], WAR, 7[4], 8[4]
More notes[a]42[6], 4[6]
Noratings[b]NoneWAR
There is no entry2 even tho there's a ratings2 haha loserNone
  1. This is another footnote
  2. entry3 has no ratings and may be ignored! Also has Ravnica and Mirrodin which will definitely be ignored.

Footnotes


--[[ Sandbox page for the MaroScale lua script

jerodast will request to delete this page once any useful changes are merged to
  the live MaroScale page

Goals for jerodast:
  * (DONE?) learn a bit more how it's used, AND about Modules in general
  * (DONE) update LEGAL_EXPANSIONS (although it's not used at all rn lol)
  * allow for not-a-single-number phrasings to be given an approximate number
      for sorting but still display, e.g. "about a 9" (treated as 9) or
      "7 or 8" (treated as 7.5)
  * explore possibility of having dates show up in the list of ratings changes,
      since the current simple serial list presentation makes ratings appear
      uniformly spaced when they're not

Lua debug console test run:
	mw.log(p.TableBuilder(mw.getCurrentFrame():newChild{title="Template:TEST",args={type = "console", entry3 = "Noratings", name3  = "Redlink", note3 = "This entry has no ratings and may be ignored!", printings3 = "Ravnica, Mirrodin, War of the Spark,", ratings2 = "{3,2018-10-17,<sup>[4]</sup>},{4,2019-08-10,<sup>[4]</sup> No entry!},", entry4 = "Mix ratings and printings", ratings4 = "{5,2013-08-10,<sup>[4]</sup>},{6,2018-10-17,<sup>[4]</sup>},{7,2019-08-10,<sup>[4]</sup>},{8,2021-08-10,<sup>[4]</sup>},", printings4 = "rtr, Lorwyn, Rivals Of Ixalan, WAR",}}:newChild{title="Module:MaroScaleSandbox"}))
]]

--[[ Beeble scale currently includes notes simply by placing them after the ref
     part of an entry:
    
	| entry25  = Phelddagrif
	| ratings25= {9,2018-10-21,<ref>{{EzTumblr|http://markrosewater.tumblr.com/post/179292907638/|title=Where is Phelddagriff on the Beeble Scale?|October 21, 2018}}</ref>}, 
	{9,2018-11-26,<ref>{{EzTumblr|http://markrosewater.tumblr.com/post/180506854918/|title=Where are phelddagrifs on the beeble scale? |November 26, 2018}}</ref> (actually 9 ½.)},
	
	Shows as:           9[32], 9[33] (actually 9 ½.)
	
	
	| entry4   = Dwarf
	| ratings4 = {1,2013-01-23,<ref>{{EzTumblr|http://markrosewater.tumblr.com/post/41287858160/|title=I like your "storm scale" invention so I hope...|2013-01-23}}</ref>},
	{1,2013-10-25,<ref>{{EzTumblr|http://markrosewater.tumblr.com/post/65064665515/|title=Where are dwarves on the storm scale? I know that orcs and dwarves...|2013-10-25}}</ref>},
	{1,2016-06-04,<ref>{{EzTumblr|http://markrosewater.tumblr.com/post/145378547208/|title=Hello Mr. Rosewater! Can you tell us where are dwarves on the Storm Scale?|2016-06-04}}</ref> Reprinted in ''[ [Kaladesh] ]''}, 
	{2,2018-03-20,<ref>{{EzTumblr|http://markrosewater.tumblr.com/post/172073275828/|title=What’s the current outlook for Dwarves in Magic?|2018-03-20}}</ref>}, 
	{3,2018-10-23,<ref>{{EzTumblr|http://markrosewater.tumblr.com/post/17934156953/|title=Where are dwarves on the Beeble Scale?|October 23, 2018}}</ref>}, 
	{2,2019-08-21,<ref name="D&O">{{EzTumblr|https://markrosewater.tumblr.com/post/187167710678/|title=So what are Dwarves and Orcs on the Beeble scale?|August 21, 2019}}</ref>},
	{2,2019-09-30,<ref>{{EzTumblr|https://markrosewater.tumblr.com/post/188050238433/|title=Hi Mark, Since we got some pretty sweet Dwarf cards in Eldraine|September 30, 2019}}</ref>}, 
	{4,2020-03-29,<ref>{{EzTumblr|http://markrosewater.tumblr.com/post/613949342837096448/|title=Judging by my playgroup and another Question Marks...|2020-03-29}}</ref>},
	
	Shows as:     1[5], 1[6], 1[7] Reprinted in Kaladesh, 2[8], 3[9], 2[10], 2[11], 4[12]
]]

-- Little bit of Lua for building tables for the Storm and Rabiah scales.
-- Mechanics have been re-ranked in the past, and it's probably interesting to track their drift.
-- Rather than force editors to track the newest one, let's see if we can't do it in code.

-- Invoked by {{#invoke:MaroScale|TableBuilder}} from the MW template.

local MaroScaleSandbox = {}

local sub, match, gsub = mw.ustring.sub, mw.ustring.match, mw.ustring.gsub
-- ustring equivalents of string library that work on UTF-8 aka MediaWiki text

local MAX_ENTRIES = 500
-- A list of all sets which have been Standard-legal since the introduction of the Storm Scale (September 2012)
-- See initLegalExpansions func for how this is generated from LEGAL_EXPANSIONS_DATA
local LEGAL_EXPANSIONS = {}
local LEGAL_EXPANSIONS_DATA = {
	{"2012-10-05", "RTR", "Return to Ravnica"},
	{"2013-02-01", "GTC", "Gatecrash"},
	{"2013-05-03", "DGM", "Dragon's Maze"},
	{"2013-06-13", "M14", "Magic 2014"},
	{"2013-09-27", "THS", "Theros"},
	{"2014-02-07", "BNG", "Born of the Gods"},
	{"2014-05-02", "JOU", "Journey into Nyx"},
	{"2014-07-18", "M15", "Magic 2015"},
	{"2014-09-26", "KTK", "Khans of Tarkir"},
	{"2015-01-23", "FRF", "Fate Reforged"},
	{"2015-03-17", "DTK", "Dragons of Tarkir"},
	{"2015-07-15", "ORI", "Magic Origins"},
	{"2015-10-02", "BFZ", "Battle for Zendikar"},
	{"2016-01-22", "OGW", "Oath of the Gatewatch"},
	{"2016-04-08", "SOI", "Shadows over Innistrad"},
	{"2016-06-22", "EMN", "Eldritch Moon"},
	{"2016-09-30", "KLD", "Kaladesh"},
	{"2016-01-20", "AER", "Aether Revolt"},
	{"2017-04-28", "AKH", "Amonkhet"},
	{"2017-07-14", "HOU", "Hour of Devastation"},
	{"2017-09-29", "XLN", "Ixalan"},
	{"2018-01-19", "RIX", "Rivals of Ixalan"},
	{"2018-04-27", "DOM", "Dominaria"},
	{"2018-07-13", "M19", "Core Set 2019"},
	{"2018-10-05", "GRN", "Guilds of Ravnica"},
	{"2019-01-25", "RNA", "Ravnica Allegiance"},
	{"2019-05-03", "WAR", "War of the Spark"},
	{"2019-07-12", "M20", "Core Set 2020"},
	{"2019-10-04", "ELD", "Throne of Eldraine"},
	{"2020-01-24", "THB", "Theros Beyond Death"},
	{"2020-05-15", "IKO", "Ikora: Lair of Behemoths"},
	{"2020-07-03", "M21", "Core Set 2021"},
	{"2020-09-25", "ZNR", "Zendikar Rising"},
	{"2021-02-05", "KHM", "Kaldheim"},
	{"2021-04-23", "STX", "Strixhaven"},
	{"2021-07-23", "AFR", "Dungeons & Dragons: Adventures in the Forgotten Realms"},
	{"2021-09-24", "MID", "Innistrad: Midnight Hunt"},
	{"2021-11-19", "VOW", "Innistrad: Crimson Vow"},
	{"2022-02-18", "NEO", "Kamigawa: Neon Dynasty"},
	{"2022-04-29", "SNC", "Streets of New Capenna"},
	{"2020-09-09", "DMU", "Dominaria United"},
	{"2020-11-18", "BRO", "The Brothers' War"},
}

-- sets up LEGAL_EXPANSIONS so that all set data can be queried with either
--   the set code, its full name, or alt names; also label the fields
local function initLegalExpansions()
	for _,setData in ipairs(LEGAL_EXPANSIONS_DATA) do
	    --(musing from jerodast - based on my new understanding of Lua, this could
	    --  be done with __index metatable for each setData; doesn't seem worth)
		
		for i=2,#setData do
			LEGAL_EXPANSIONS[mw.ustring.lower(setData[i])] = setData
		end
		
		setData.releaseDate = setData[1]
		setData.code = setData[2]
		setData.name = setData[3]
		-- LEGAL_EXPANSIONS[setData.code] = setData
		-- LEGAL_EXPANSIONS[setData.name] = setData
	end
end

-- Wikitext for a simple link, with alternate "target" if needed
-- Note params are backwards from wikitext format
local function generateLink(displayText, altLink)
	if altLink then
		return "[["..altLink.."|"..displayText.."]]"
	else
		return "[["..displayText.."]]"
	end
end

--[[ parseRatings(raw)
Parses raw entry text into a sortable sequence (Lua table) and determines most
  recent rating.

PARAM:
  raw - text for one mechanic's storm scale ratings/entries, in the comma-
        separated, curly-braced format {rating,date,ref},{...},{...},...
            rating: integer only
            date:   must be exactly yyyy-mm-dd format, always hyphen-separated
            ref:    source, presumably in ref tags; technically could be any text
RETURN:
 (newestRating, - latest rating (number)
  outputList)   - list (Lua sequence/table) of Lua objects {text,t}
                      text: wikitext
                      t:    lua time object with y,m,d
]]
local function parseRatings(raw)
	local outputList = {}
	
	if not raw then return 0, outputList end
	
	local newestDate, newestRating = 0, 0
	local rating, refDate, refString, t
	
	-- Extract data from each curly-braced entry
	gsub(raw, "{(.-)}", function(a)
		rating, refDate, refString = match(a, "(%d+),([%d%-]+),(.+)")
		rating = tonumber(rating)
		
		-- Dates MUST be yyyy-mm-dd
		t = os.time({year=sub(refDate, 1,4), month=sub(refDate, 6,7), day=sub(refDate, 9,10),})
		-- Find most recent entry as we go
		if t > newestDate then
			newestDate = t
			newestRating = rating
		end
		
		-- Add to sequence of entries, with ref concatenated on rating
		table.insert(outputList, {text = rating..refString, t = t})
	end)
	
	return newestRating, outputList
end

--[[ addPrintings(timeline, printings)
Parses raw list of sets and adds them into date-sortable sequence (Lua table)

PARAMS:
  timeline - existing sequence (Lua table) to add to, each entry with format
      {text, t}, where text is wikitext and t is a Lua time object w/y,m,d
  printings - comma separated list of set names in plaintext
RETURN:
  timeline - same format as input parameter, but with new entries containining
    wikitext for each set, and including wikilinks
]]
local function addPrintings(timeline, printings)
	local setData, releaseDate, t
	-- Parse each list element delimited by commas
	gsub(printings, "([^,]+)", function(set)
		setData = LEGAL_EXPANSIONS[mw.ustring.lower(mw.text.trim(set))]
		
		-- The set provided doesn't match a post-Storm Scale set
		if not setData then return end
		
		rd = setData.releaseDate
		
		-- Dates MUST be yyyy-mm-dd
		t = os.time({year=sub(rd, 1,4), month=sub(rd, 6,7), day=sub(rd, 9,10),})
		
		table.insert(timeline, {text = generateLink(setData.code, setData.name), t = t})
	end)
	return timeline
end

--[[ sortTimeline(events)
Sorts a timeline as generated by parseRatings & addPrintings, by each entry's date

PARAM: events - sequence (Lua table); each entry must have a "t" component which
         is a Lua date object with y,m,d
RETURN: sorted events
]]
local function sortTimeline(events)
	-- Sort all events into chronological order
	table.sort(events, function(a,b) return a.t < b.t end)
	return events
end

--[[ MaroScaleSandbox.TableBuilder(frame)

Entry point for this module. Turns all template arguments into a table as wikitext.

See Template:MaroScale for parameter documentation.
  * any entry-level params without a corresponding entry### param will be ignored
]]
function MaroScaleSandbox.TableBuilder(frame)
	local args = frame:getParent().args
	-- frame.args:             Module/invoke parameters (none expected)
	-- frame:getParent().args: Template parameters (nameI, entryI, noteI, etc)
	
	initLegalExpansions()
	
	-- Imitating navbox, here. I trust they know what they're doing.
	-- Read the arguments in the order they'll be output in, to make references number in the right order.
	--[[ Commented out by jerodast:
			I don't understand why this is necessary. Pairs already works
			on .args and the list will be re-sorted anyway.
    local _
    _ = args.type
    _ = args.above
    for i = 1, MAX_ENTRIES do
        _ = args["name" .. tostring(i)]
        _ = args["entry" .. tostring(i)]
        _ = args["note" .. tostring(i)]
        _ = args["ratings" .. tostring(i)]
        _ = args["printings" .. tostring(i)]
    end
    ]]
	
	-- Get parameter numbers for each entry into sequence, sort it by NAME
	local entryNums = {}
	for k, v in pairs(args) do
		-- Extract param numbers based on entry### parameter (required)
		local eNum = match('' .. k, '^entry(%d+)$')
		if eNum then
			table.insert(entryNums, {num = tonumber(eNum), entry = v})	
		end
	end
	table.sort(entryNums, function(a,b) return a.entry < b.entry end)

	
	-- Build the table header row
	local tbl = mw.html.create("table"):addClass("wikitable"):addClass("sortable")
	tbl:tag("tr")
		:tag("th"):wikitext(args.type):done()    -- "Mechanic", "Plane", etc
		:tag("th"):wikitext("Latest ranking"):done()
		:tag("th"):wikitext("Timeline"):addClass("unsortable"):done() -- Last col can't be sorted
	
	
	local efnGroup
	if args.type then efnGroup = args.type .. "_stormscale_footnote"
	else              efnGroup = "stormscale_footnote"
	end
	local hasNotes = false
	
	-- Reusable loop vars for performance
	local name, entry, note, ratings, printings
	local linkedEntry, lastRating, timeline
	local row, list, cellText
	
	-- For each entry identified and sorted by name earlier...
	for i, eData in ipairs(entryNums) do
		eNum = eData.num   -- original parameter number for this entry
		entry = eData.entry;
		-- Retrieve the relevant args for this row
		name, note = args["name"..eNum], args["note"..eNum]
		ratings, printings = args["ratings"..eNum], args["printings"..eNum]
		
		-- Create a wikilink for the entry, using name as the target article if provided
		linkedEntry = generateLink(entry, name)
		if note then
			hasNotes = true
			-- Use {{efn}} template (explanatory footnote)
			linkedEntry = linkedEntry .. frame:expandTemplate(
				{title = "efn", args = { note, group = efnGroup }}
			)
		end
		
		-- Decode the ratings into a sequence (Lua table)
		lastRating, timeline = parseRatings(ratings)
		-- Add any printings
		if printings then
			timeline = addPrintings(timeline, printings)
		end
		-- Sort chronologically
		timeline = sortTimeline(timeline)
		
		-- Make the row
		row = tbl:tag("tr")
		
		-- First cell
		row:tag("td"):wikitext(linkedEntry)
		
		-- Second cell
		if lastRating == 0 then
			row:tag("td"):wikitext("None"):attr("data-sort-value",0)
		else
			row:tag("td"):wikitext(lastRating)
		end
			
		-- Third cell
		-- Each rating is included as an item in an inline HTML list - which may be excessive.
		--[[ Commented out by jerodast - let's just try regular wikitext.
		list = row:tag("td"):tag("ul"):cssText("display:block; margin-top:0; margin-left:0;")
		
		for j, event in ipairs(timeline) do
			if j ~= #timeline then
				list:tag("li"):cssText("display:inline-block;"):wikitext(event.text .. ",&nbsp;")
			else
				list:tag("li"):cssText("display:inline-block;"):wikitext(event.text)
			end
		end
		]]
	    list = {}
		for j, event in ipairs(timeline) do table.insert(list, event.text) end
		row:tag("td"):wikitext(table.concat(list,", "))
	end
	
	-- Include full footnotes below the table. Note this will include any other
	--   explanatory footnotes on the page, and if a notelist is included in the
	--   References section near the bottom page as it usually is, all notes
	--   from the table will be duplicated there.
	--   ...I think. (Maybe it breaks completely.)
	if hasNotes then
		return tostring(tbl) .. frame:expandTemplate({title = "notelist", args = {group = efnGroup}})
	else
		return tbl
	end
end

return MaroScaleSandbox