Module:Cargo

-- TABLE DEFINITIONS -- declared through the module so it has access to the field names -- which allows the module to work around an issue with query values -- that happen to include words matching one of the field names

-- In each table, the row has two to three values: --  1. field name (matches the appropriate template parameter where possible) --  2. field type (see https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Cargo/Storing_data) --  3. a delimiter if a list of values is needed in the field (omitting this makes the field single-value) -- UNDER CONSTRUCTION local defs = { Cards = { { 'name', 'String' }, -- card name { 'image', 'Page' }, -- card image { 'cardSet', 'String' }, -- name of set { 'color', 'String' }, -- card color (currently assuming color will match burn gem) { 'colorIndex', 'Integer' }, -- color as a number for correct sorting (1-6 for blue, yellow, red, green, orange, purple) { 'burnGem', 'String' }, -- burn gem color (including as future-proofing in case any future cards have a color that doesn't match) { 'burnGemIndex', 'Integer' }, -- burn gem color as a number for correct sorting (1-6 for blue, yellow, red, green, orange, purple) { 'type', 'String' }, -- minion, spell, lane enchantment, or artifact { 'typeIndex', 'Integer' }, -- type as a number for correct sorting (1-4 for minion, spell, lane enchantment, or artifact) { 'manaCost', 'Integer' }, -- cost in mana { 'blueCost', 'Integer' }, -- cost in blue gems { 'yellowCost', 'Integer' }, -- cost in yellow gems { 'redCost', 'Integer' }, -- cost in red gems { 'greenCost', 'Integer' }, -- cost in green gems { 'orangeCost', 'Integer' }, -- cost in orange gems { 'purpleCost', 'Integer' }, -- cost in purple gems { 'rarity', 'String' }, -- card's subtype as indicated at the bottom { 'rarityIndex', 'Integer' }, -- rarity as a number for correct sorting (1-4 for common-mythic) { 'power', 'Integer' }, -- power value { 'health', 'Integer' }, -- health value { 'keywords', 'String', ',' }, -- keywords listed in an aside to cards when "zooming in" on them { 'effect', 'Wikitext' }, -- card's effect { 'flavor', 'Wikitext' }, -- card's flavor text { 'subtype', 'String' }, -- card's subtype as indicated at the bottom }, }

-- An ugly hack to prevent Cargo from puking when a string value happens to have -- a field name in it. Breaks the value up into individual characters and builds -- an SQL CONCAT around it so it doesn't get put back together until it actually -- hits the database. local function splode( pre, value, post ) local tokens = {} if pre and pre ~= '' then table.insert( tokens, '"' .. pre .. '"' ) end

for chr in string.gmatch( value, '.' ) do       if chr == '"' then            table.insert( tokens, '"\\\""' ) else table.insert( tokens, '"' .. chr .. '"' ) end end

if post and post ~= '' then table.insert( tokens, '"' .. post .. '"' ) end

return "CONCAT(" .. table.concat( tokens, "," ) .. ")" end

-- Determines if the ugly "splode" hack is going to be needed and uses it if so. -- Otherwise, simply quotes the string and escapes quotes inside. local function quoteOrSplode( frame, pre, value, post ) pre = pre or '' post = post or '' local tables = mw.text.split( frame.args.tables, ",", true ) for _, tableName in ipairs( tables ) do       local def = defs[tableName]

if def then for _, field in ipairs( def ) do               local fieldName = string.lower( field[1] ) if string.find( string.lower( value ), fieldName ) then return splode( pre, value, post ) end end end end

return '"' .. pre .. string.gsub( value, '"', '\\"' ) .. post .. '"' end

-- Finds the delimiter of a list field, provided the defs table above was used -- in declaring the table or the defs correctly match the table declaration. local function delimiter( tableName, fieldName ) local def = defs[tableName]

if def then for _, field in ipairs( def ) do           if ( field[1] == fieldName ) then return field[3] end end end

return nil end

-- Provides a workaround for difficulties in using multiple HOLDS or HOLDS NOT -- in a single query. This uses an equivalent regex that will (in most cases) -- give the same result as a HOLDS, but can be used multiple times in the same -- query. local function holds( frame, name, arg ) local mode = string.sub( name, 1, 1 ) local op = " RLIKE " if mode == '!' then op = " NOT" .. op end local tokens = mw.text.split( string.sub( name, 2, -1 ), ".", true )

local sep = delimiter( tokens[1], tokens[2] )

if sep == '\\' then sep = '\\\\' elseif sep == '|' then sep = '\\\\\\|' end

local pre = '(^|' .. sep .. ')' local post = '(' .. sep .. '|$)' arg = quoteOrSplode( frame, pre, arg, post )

return tokens[1] .. '.' .. tokens[2] .. '__full' .. op .. arg end

-- Retrieves the value for a placeholder and correctly formats it, applying any -- workarounds as needed. local function value( frame, token ) local name = string.sub( token, 2, -2 ) local arg = frame.args[ name ] or '' local mode = string.sub( name, 1, 1 )

if mode == '#' then return tostring( tonumber( arg ) or '' ) elseif mode == '$' then return quoteOrSplode( frame, pre, arg, post ) elseif mode == '%' or mode == '!' then return holds( frame, name, string.gsub( arg, '"', '\\"' ) ) else error( 'Placeholder names must start with "#", "$", "%", or "!": "' ..               name .. '" does not' ) end end

-- Tokenizes a clause to allow placeholders to be correctly identified only -- outside of quoted text. local function tokenize( text ) local tokens = {} local start = nil local token = '' local escaped = false text = text or ''

for chr in string.gmatch( text, "." ) do   	if start then token = token .. chr if chr == start and not escaped then table.insert( tokens, { start = chr, value = token } ) token = '' start = nil end escaped = ( chr == '\\' and not escaped ) else if string.match( chr, '[%?"\']' ) then               table.insert( tokens, { start = nil, value = token } )                token = chr                start = chr            else                token = token .. chr            end    	end    end    if token ~= '' then    	table.insert( tokens, { start = start, value = token } )    end

return tokens end

-- Processes placeholders in a clause using syntax somewhat similar to -- parameterized queries, formatting values supplied in the matching #invoke -- args and swapping those values with their placeholders local function parameterize( frame, arg ) local tokens = tokenize( frame.args[arg] ) local clause = ''

for _, token in ipairs( tokens ) do       if ( token.start == '?' ) then clause = clause .. value( frame, token.value ) else clause = clause .. token.value end end

return clause end

local p = {}

-- invokes #cargo_declare for the given table if there is a matching -- table name in defs above function p.declare( frame ) local def = defs[frame.args['_table']]

if def then local args = { '_table=' .. frame.args['_table'] } for _, field in ipairs( def ) do           if field[3] then table.insert( args, '|' .. field[1] .. '=List (' .. field[3] .. ') of ' .. field[2] ) else table.insert( args, '|' .. field[1] .. '=' .. field[2]) end end

return frame:preprocess( '' ) end end

-- performs a query in a manner similar to #cargo_query with -- format=template specified function p.query( frame ) local results = mw.ext.cargo.query( frame.args.tables, frame.args.fields, {       where = parameterize( frame, 'where' ),        join = frame.args.join,        groupBy = frame.args.groupBy,        having = parameterize( frame, 'having' ),        orderBy = frame.args.orderBy,        limit = frame.args.limit,        offset = frame.args.offset,    } )

local out if #results > 0 then out = frame.args.intro or '' for index = 1, #results, 1 do           out = out .. '\n' .. frame:expandTemplate{ title = frame.args.template, args = results[index] } end out = out .. '\n' .. ( frame.args.outro or '' ) else out = frame.args.default or '' end

if frame.args.debug == "yes" then out = out .. "\n\n" .. frame.args.where .. "\n\nResults: " .. #results end

return out end

function p.debug( frame ) local out = ' \n' out = out ..'tables = ' .. frame.args.tables .. "\n" out = out ..'fields = ' .. frame.args.fields .. "\n" out = out ..'where = ' .. parameterize( frame, 'where' ) .. "\n" out = out ..'join = ' .. frame.args.join .. "\n" out = out ..'groupBy = ' .. frame.args.groupBy .. "\n" out = out ..'having = ' .. parameterize( frame, 'having' ) .. "\n" out = out ..'orderBy = ' .. frame.args.orderBy .. "\n" out = out ..'limit = ' .. frame.args.limit .. "\n" out = out ..'offset = ' .. frame.args.offset .. "\n" out = out .. ' \n'

return out end

return p