From WikiChip
Difference between revisions of "Module:comptable"
(askt: Added template special prop.) |
|||
(3 intermediate revisions by 2 users not shown) | |||
Line 65: | Line 65: | ||
return '<tr class="comptable-header"><th class="unsortable">Model</th>' .. r .. '</tr>' | return '<tr class="comptable-header"><th class="unsortable">Model</th>' .. r .. '</tr>' | ||
+ | end | ||
+ | |||
+ | --------------------------------------------- | ||
+ | |||
+ | function p.main2(frame) | ||
+ | if frame == mw.getCurrentFrame() then | ||
+ | origArgs = frame:getParent().args | ||
+ | else | ||
+ | origArgs = frame | ||
+ | end | ||
+ | |||
+ | local r = '' | ||
+ | |||
+ | for i = 2, 30 do | ||
+ | local val = origArgs[i] | ||
+ | if not val then break end | ||
+ | if string.find(val, ":") then | ||
+ | r = r .. string.gsub(val, "(%d+):(.+)", '<th colspan="%1">%2</th>') | ||
+ | else | ||
+ | r = r .. '<th>' .. val .. '</th>' | ||
+ | end | ||
+ | end | ||
+ | |||
+ | return '<tr class="comptable2-header"><th> </th>' .. r .. '</tr>' | ||
+ | end | ||
+ | |||
+ | function p.lsep2(frame) | ||
+ | if frame == mw.getCurrentFrame() then | ||
+ | origArgs = frame:getParent().args | ||
+ | else | ||
+ | origArgs = frame | ||
+ | end | ||
+ | |||
+ | local r = '' | ||
+ | |||
+ | for i = 2, 30 do | ||
+ | local val = origArgs[i] | ||
+ | if not val then break end | ||
+ | if string.find(val, ":") then | ||
+ | r = r .. string.gsub(val, "(%d+):(.+)", '<th colspan="%1" style="text-align: left;">%2</th>') | ||
+ | else | ||
+ | r = r .. '<th style="text-align: left;">' .. val .. '</th>' | ||
+ | end | ||
+ | end | ||
+ | |||
+ | return '<tr class="comptable2-header"><th> </th>' .. r .. '</tr>' | ||
+ | end | ||
+ | |||
+ | function p.cols2(frame) | ||
+ | if frame == mw.getCurrentFrame() then | ||
+ | origArgs = frame:getParent().args | ||
+ | else | ||
+ | origArgs = frame | ||
+ | end | ||
+ | |||
+ | local r = '' | ||
+ | |||
+ | for i = 2, 30 do | ||
+ | local val = origArgs[i] | ||
+ | if not val then break end | ||
+ | if string.match(val, '^%%.+') then | ||
+ | r = r .. '<th data-sort-type="number">' .. string.sub(val, 2) .. '</th>' | ||
+ | else | ||
+ | r = r .. '<th>' .. val .. '</th>' | ||
+ | end | ||
+ | end | ||
+ | |||
+ | return '<tr class="comptable2-header"><th class="unsortable">Model</th>' .. r .. '</tr>' | ||
+ | end | ||
+ | |||
+ | function p.askt(frame) | ||
+ | local origArgs = frame.args | ||
+ | if not (origArgs[1] or origArgs.condition) and frame:getParent() then | ||
+ | -- Use args of template containing the #invoke. | ||
+ | origArgs = frame:getParent().args | ||
+ | end | ||
+ | |||
+ | local function undo_nowiki(s) | ||
+ | s = mw.text.unstripNoWiki(s) | ||
+ | --[[ Unfortunate because we can't tell if nowiki escaped the | ||
+ | characters or a user did, and we need them for query | ||
+ | conditions and to output HTML tags. But < and > | ||
+ | works. ]] | ||
+ | s = s:gsub('<', '<'):gsub('>', '>') | ||
+ | -- https://www.mediawiki.org/wiki/Writing_systems/Syntax | ||
+ | return s:gsub('%-{', '-{'):gsub('}%-', '}-') | ||
+ | end | ||
+ | |||
+ | local pound, eqsign, vbar = 35, 61, 124 | ||
+ | |||
+ | local function check_querycond(s) | ||
+ | local cond, i, j, k = false, 1 | ||
+ | j, k = s:find('^%s*') | ||
+ | if j then i = k + 1 end | ||
+ | while i <= #s and s:byte(i) ~= vbar do | ||
+ | j = nil | ||
+ | if cond then | ||
+ | j, k = s:find('^OR%s*', i) | ||
+ | end | ||
+ | if j then | ||
+ | i = k + 1 | ||
+ | cond = false | ||
+ | else | ||
+ | j, k = s:find('^%[%[[^%]]+%]%]%s*', i) | ||
+ | if j then | ||
+ | i = k + 1 | ||
+ | cond = true | ||
+ | else | ||
+ | error('Missing/invalid condition', 2) | ||
+ | end | ||
+ | end | ||
+ | end | ||
+ | if not cond then | ||
+ | error('Missing condition', 2) | ||
+ | end | ||
+ | if i > #s then return s, nil end | ||
+ | return s:sub(1, i - 1), s:sub(i + 1, -1) | ||
+ | end | ||
+ | |||
+ | local function check_number(n, default, arg_name) | ||
+ | if not n then return default end | ||
+ | n = tonumber(n) | ||
+ | if not n or n < 0 then | ||
+ | error('Invalid ' .. arg_name, 2) | ||
+ | end | ||
+ | return math.floor(n) | ||
+ | end | ||
+ | |||
+ | local special_props = { '#querycondition', '#querylimit', | ||
+ | '#resultoffset', '#rowcount', '#rownumber', '#userparam', | ||
+ | '#template' } | ||
+ | |||
+ | -- May be nil. | ||
+ | local userparam = origArgs.userparam | ||
+ | |||
+ | local function transform_template(querycond, s) | ||
+ | local query = { querycond } -- mw.smw.ask() parameter | ||
+ | |||
+ | local prop_index = {} | ||
+ | local n_props = 0 | ||
+ | for i, v in ipairs(special_props) do | ||
+ | prop_index[v] = i | ||
+ | end | ||
+ | local function append_prop(prop) | ||
+ | local index = prop_index[prop] | ||
+ | if not index then | ||
+ | n_props = n_props + 1 | ||
+ | index = 'L' .. n_props | ||
+ | prop_index[prop] = index | ||
+ | if querycond then | ||
+ | table.insert(query, string.format('?%s=%s', prop, index)) | ||
+ | end | ||
+ | end | ||
+ | return index | ||
+ | end | ||
+ | |||
+ | local i, depth = 1, 0 | ||
+ | |||
+ | local function parse() | ||
+ | if depth > 4 then error('Too many defaults', 2) end | ||
+ | depth = depth + 1 | ||
+ | |||
+ | local templ = {} | ||
+ | |||
+ | while i <= #s do | ||
+ | local stop = #s + 1 | ||
+ | if depth > 1 then | ||
+ | local j = s:find('}}}', i) | ||
+ | if j then stop = j end | ||
+ | end | ||
+ | |||
+ | local j, k, prop = s:find('{{{%s*([%w#][^#=|}]*)', i) | ||
+ | if j == nil or j >= stop then | ||
+ | if i < stop then | ||
+ | local prefix = s:sub(i, stop - 1) | ||
+ | table.insert(templ, { prefix=prefix }) | ||
+ | end | ||
+ | i = stop | ||
+ | break | ||
+ | end | ||
+ | local prefix = s:sub(i, j - 1) | ||
+ | i = k + 1 -- skip {{{ and prop name | ||
+ | prop = prop:match('(.-)%s*$') -- remove trailing whitespace | ||
+ | local format = nil | ||
+ | if prop:byte(1) == pound then | ||
+ | if not prop_index[prop] then | ||
+ | error('Unknown special property "' .. prop .. '"', 2) | ||
+ | elseif prop == '#userparam' and not userparam then | ||
+ | error('No value for "#userparam" specified', 2) | ||
+ | end | ||
+ | elseif not querycond then | ||
+ | error('Unexpected property "' .. prop .. | ||
+ | '" in intro/outro template', 2) | ||
+ | else | ||
+ | -- {{{property#format}}} | ||
+ | j, k, format = s:find('^(#[^#=|}]+)', i) | ||
+ | if j then i = k + 1 end | ||
+ | end | ||
+ | |||
+ | local default = {} | ||
+ | local c = s:byte(i) | ||
+ | if c == eqsign then | ||
+ | error('Property labels not supported', 2) | ||
+ | elseif c == pound then | ||
+ | error('Invalid format for property "' .. prop .. '"', 2) | ||
+ | elseif c == vbar then | ||
+ | i = i + 1 | ||
+ | -- {{{property|default}}} recursion | ||
+ | default = parse() | ||
+ | end | ||
+ | |||
+ | if not s:find('^}}}', i) then | ||
+ | error('Undelimited property "' .. prop .. '"', 2) | ||
+ | end | ||
+ | i = i + 3 | ||
+ | |||
+ | if prop == 'page' then | ||
+ | prop = '' -- SMW ask mainlabel: '?' or '?#-' | ||
+ | if format and format ~= '#-' then | ||
+ | error('Special property "page" supports only format #-', 2) | ||
+ | end | ||
+ | end | ||
+ | if format == '#tick' then -- not supported in SMW < 3.0 | ||
+ | format = function(value) | ||
+ | if type(value) == 'boolean' then | ||
+ | return value and '✔' or '✘' | ||
+ | end | ||
+ | return tostring(value) or '' | ||
+ | end | ||
+ | else | ||
+ | if format then prop = prop .. format end | ||
+ | format = function(value) | ||
+ | return tostring(value) or '' | ||
+ | end | ||
+ | end | ||
+ | |||
+ | table.insert(templ, { prefix=prefix, index=append_prop(prop), | ||
+ | default=default, format=format }) | ||
+ | end | ||
+ | |||
+ | depth = depth - 1 | ||
+ | |||
+ | return templ | ||
+ | end | ||
+ | |||
+ | local templ = parse() | ||
+ | return templ, query | ||
+ | end | ||
+ | |||
+ | local valuesep = origArgs.valuesep | ||
+ | if not valuesep then valuesep = ', ' end | ||
+ | |||
+ | local function concat_values(format, values) | ||
+ | if values == nil then return '' end | ||
+ | if type(values) ~= 'table' then return format(values) end | ||
+ | local output = {} | ||
+ | for i, value in ipairs(values) do | ||
+ | value = format(value) | ||
+ | if value ~= '' then table.insert(output, value) end | ||
+ | end | ||
+ | return table.concat(output, valuesep) | ||
+ | end | ||
+ | |||
+ | local function substitute(output, templ, values, row) | ||
+ | for i, f in ipairs(templ) do | ||
+ | table.insert(output, f.prefix) | ||
+ | if f.index then | ||
+ | local text | ||
+ | if type(f.index) == 'string' then | ||
+ | text = concat_values(f.format, row[f.index]) | ||
+ | else | ||
+ | text = values[f.index] or '' | ||
+ | end | ||
+ | if text == '' then | ||
+ | substitute(output, f.default, values, row) | ||
+ | else | ||
+ | table.insert(output, text) | ||
+ | end | ||
+ | end | ||
+ | end | ||
+ | end | ||
+ | |||
+ | local function dump(t) | ||
+ | if t == nil then return 'nil' end | ||
+ | if type(t) == 'string' then return '"' .. t .. '"' end | ||
+ | if type(t) == 'table' then | ||
+ | s = '{' | ||
+ | for k, v in pairs(t) do | ||
+ | s = s .. string.format('[%s]=%s,', dump(k), dump(v)) | ||
+ | end | ||
+ | return s .. '}' | ||
+ | end | ||
+ | return tostring(t) or '?' | ||
+ | end | ||
+ | local function escape_concat(s, sep) | ||
+ | if not s then s = '(nil)' end | ||
+ | if type(s) == 'table' then s = table.concat(s, sep) end | ||
+ | -- do return s end | ||
+ | s = s:gsub('[<%[{]', function(c) | ||
+ | return string.format('&#%d;', c:byte()) end) | ||
+ | return s | ||
+ | end | ||
+ | local function test() | ||
+ | local function test(pat, a, e1, e2) | ||
+ | local success, r1, r2 = pcall(check_querycond, a) | ||
+ | if not success then | ||
+ | if not pat then | ||
+ | error('Function failed unexpectedly: ' .. r1, 2) | ||
+ | elseif not r1:find(pat) then | ||
+ | error(string.format('Unexpected error msg\n✘ %s\n✔ %s\n', | ||
+ | r1, pat), 2) | ||
+ | end | ||
+ | elseif pat then | ||
+ | error('Function succeeded unexpectedly', 2) | ||
+ | elseif r1 ~= e1 or r2 ~= e2 then | ||
+ | error(string.format('Unexpected result\n✘ %s,%s\n✔ %s,%s\n', | ||
+ | r1, r2, e1, e2), 2) | ||
+ | end | ||
+ | end | ||
+ | |||
+ | test('Missing cond.*', ' ') | ||
+ | test('.*inv.*', 'x') | ||
+ | test('.*inv.*', '[x]') | ||
+ | test('.*inv.*', '[[x') | ||
+ | test('.*inv.*', '[[]]') | ||
+ | test('.*inv.*', 'OR[[x]]') | ||
+ | test('Missing cond.*', '[[x]]OR') | ||
+ | local s = ' [[x]] ' | ||
+ | test(nil, s, s, nil) | ||
+ | local s = '[[x::foo]][[x::~*bar*]] [[x<<x]]' | ||
+ | test(nil, s, s, nil) | ||
+ | local s = ' \n [[x||x]] OR \n [[x>>x]]OR[[x]]\n' | ||
+ | test(nil, s, s, nil) | ||
+ | test(nil, '[[x||x]] \n ||[[x]]', '[[x||x]] \n ', '|[[x]]') | ||
+ | test(nil, '[[x]]| \nx | ', '[[x]]', ' \nx | ') | ||
+ | |||
+ | local function test(pat, a, exp_cond, exp_outp) | ||
+ | local success, r1, r2 = pcall(transform_template, cond, a) | ||
+ | if not success then | ||
+ | if not pat then | ||
+ | error('Function failed unexpectedly: ' .. r1, 2) | ||
+ | elseif not r1:find(pat) then | ||
+ | error(string.format('Unexpected error msg\n✘ %s\n✔ %s\n', | ||
+ | r1, pat), 2) | ||
+ | end | ||
+ | return | ||
+ | elseif pat then | ||
+ | error('Function succeeded unexpectedly', 2) | ||
+ | end | ||
+ | if cond then | ||
+ | r2 = escape_concat(r2, '|') | ||
+ | exp_cond = escape_concat(exp_cond) | ||
+ | if r2 ~= exp_cond then | ||
+ | error(string.format('Unexpected query\n✘ %s\n✔ %s\n', | ||
+ | r2, exp_cond), 2) | ||
+ | end | ||
+ | end | ||
+ | local output = {} | ||
+ | local values = { '[[x]]', 'B', 'C', 'D', 'E', userparam or '' } | ||
+ | local row = { 'G', L1='H', L2=42, L3='', L4=true, L5=false } | ||
+ | substitute(output, r1, values, row) | ||
+ | output = escape_concat(output) | ||
+ | exp_outp = escape_concat(exp_outp) | ||
+ | if output ~= exp_outp then | ||
+ | error(string.format('Unexpected output\n✘ %s\n✔ %s\n', | ||
+ | output, exp_outp), 2) | ||
+ | end | ||
+ | end | ||
+ | |||
+ | local function test1() | ||
+ | local s = ' |<br />\nfoo||{# {{\n ' | ||
+ | test(nil, s, '[[x]]', s) | ||
+ | test(nil, 'a{{{=b}}}c', '[[x]]', 'a{{{=b}}}c') | ||
+ | test('Unknown.*', '{{{#}}}') | ||
+ | test('Unknown.*', 'a{{{ #-}}}c') | ||
+ | test('Unknown.*', '{{{#foo}}}') | ||
+ | test('Unknown.*', '{{{# rowcount}}}') | ||
+ | test('Invalid format.*', '{{{#rowcount#hex}}}') | ||
+ | test('.*label.*', '{{{#rowcount=x|foo}}}') | ||
+ | test(nil, '{{{#rowcount|foo=x}}}', '[[x]]', 'D') | ||
+ | test(nil, 'a{{{#querycondition}}}c', '[[x]]', 'a[[x]]c') | ||
+ | test(nil, 'a{{{ #querylimit}}}b', '[[x]]', 'aBb') | ||
+ | test(nil, 'a{{{#resultoffset }}}b', '[[x]]', 'aCb') | ||
+ | test(nil, 'a{{{#rowcount|default}}}b', '[[x]]', 'aDb') | ||
+ | test(nil, 'a{{{#rownumber}}}b', '[[x]]', 'aEb') | ||
+ | userparam = nil | ||
+ | test('No value.*', 'a{{{#userparam}}}b') | ||
+ | userparam = 'X' | ||
+ | test(nil, 'a{{{#userparam}}}b', '[[x]]', 'aXb') | ||
+ | end | ||
+ | |||
+ | cond = nil | ||
+ | test('Unexp.*', 'x{{{x') | ||
+ | test('Unexp.*', '{{{a}}}}') | ||
+ | test('Unexp.*', 'a{{{ b # GHz[-] }}}\n{{{d}}}e') | ||
+ | test('Unexp.*', 'a{{{b=c|foo}}}d') | ||
+ | test('Unexp.*', 'a{{{b#GHz=c|foo}}}d') | ||
+ | test('Unexp.*', 'a{{{page}}}c') | ||
+ | test('Unexp.*', 'a{{{page#-}}}c') | ||
+ | test1() | ||
+ | |||
+ | cond = '[[x]]' | ||
+ | test('Undelim.*', 'x{{{x') | ||
+ | test(nil, 'a{{{}}}c', '[[x]]', 'a{{{}}}c') | ||
+ | test(nil, 'a{{{b}}}c', '[[x]]|?b=L1', 'aHc') | ||
+ | test(nil, 'a{{{ b }}}', '[[x]]|?b=L1', 'aH') | ||
+ | test(nil, 'a{{{{ b}}}c', '[[x]]|?b=L1', 'a{Hc') | ||
+ | test(nil, 'a{{{{{{{{ b}}}c', '[[x]]|?b=L1', 'a{{{{{Hc') | ||
+ | test(nil, '{{{b}}}}c', '[[x]]|?b=L1', 'H}c') | ||
+ | test(nil, '{{{b}}}}}}}}c', '[[x]]|?b=L1', 'H}}}}}c') | ||
+ | test(nil, 'a{{{ b # GHz[-] }}}\n{{{d}}}e', | ||
+ | '[[x]]|?b# GHz[-] =L1|?d=L2', 'aH\n42e') | ||
+ | test(nil, '{{{a}}}{{{b#tick}}}c{{{d#tick}}}e{{{f#tick}}}' .. | ||
+ | '{{{g#tick}}}h{{{i#tick}}}j{{{k#tick|x}}}l', | ||
+ | '[[x]]|?a=L1|?b=L2|?d=L3|?f=L4|?g=L5|?i=L6|?k=L7', 'H42ce✔✘hjxl') | ||
+ | test('.*label.*', 'a{{{b=c|foo}}}d') | ||
+ | test('.*label.*', 'a{{{b#GHz=c|foo}}}d') | ||
+ | test(nil, 'a{{{=b}}}c', '[[x]]', 'a{{{=b}}}c') | ||
+ | test(nil, 'a{{{page}}}c', '[[x]]|?=L1', 'aHc') | ||
+ | test(nil, 'a{{{page#-}}}c', '[[x]]|?#-=L1', 'aHc') | ||
+ | test('page.*format', '{{{page#GHz}}}') | ||
+ | test('page.*format', '{{{page# -}}}') | ||
+ | test1() | ||
+ | test(nil, '{{{a}}}{{{b}}}{{{c}}}{{{d}}}', | ||
+ | '[[x]]|?a=L1|?b=L2|?c=L3|?d=L4', 'H42true') | ||
+ | test(nil, '{{{a|}}}{{{b|}}}{{{c|}}}{{{d|}}}', | ||
+ | '[[x]]|?a=L1|?b=L2|?c=L3|?d=L4', 'H42true') | ||
+ | test(nil, '{{{a|1}}}{{{b|2}}}{{{c|3}}}{{{d|4}}}', | ||
+ | '[[x]]|?a=L1|?b=L2|?c=L3|?d=L4', 'H423true') | ||
+ | test(nil, '{{{a#1}}}{{{b#2}}}{{{c#3|d#3}}}{{{e|f#4}}}', | ||
+ | '[[x]]|?a#1=L1|?b#2=L2|?c#3=L3|?e=L4', 'H42d#3true') | ||
+ | test(nil, '{{{a}}}{{{b}}}{{{c| \n3 }}}', | ||
+ | '[[x]]|?a=L1|?b=L2|?c=L3', 'H42 \n3 ') | ||
+ | test(nil, '{{{a}}}{{{b}}}{{{c|d{{{e}}}{{{f}}}{{{g}}}#f}}}', | ||
+ | '[[x]]|?a=L1|?b=L2|?e=L3|?f=L4|?g=L5|?c=L6', 'H42dtruefalse#f') | ||
+ | test(nil, '{{{a}}}{{{b}}}{{{c|d{{{b}}}e}}}', | ||
+ | '[[x]]|?a=L1|?b=L2|?c=L3', 'H42d42e') | ||
+ | test(nil, '{{{a}}}{{{b}}}{{{c|{{{d}}}{{{e}}}{{{f}}}{{{g|{{{a|x}}}}}}}}}', | ||
+ | '[[x]]|?a=L1|?b=L2|?d=L3|?e=L4|?f=L5|?g=L6|?c=L7', 'H42truefalseH') | ||
+ | test(nil, '{{{a|{{{b|{{{c|{{{d|x}}}}}}}}}}}}', | ||
+ | '[[x]]|?d=L1|?c=L2|?b=L3|?a=L4', 'true') | ||
+ | test('Too many.*', '{{{a|{{{b|{{{c|{{{d|{{{e|x}}}}}}}}}}}}}}}') | ||
+ | test(nil, '{{{a}}}{{{b}}}{{{c|{{ifeq:{{{#rowcount}}}|1|2|{{{a}}}}}}}}', | ||
+ | '[[x]]|?a=L1|?b=L2|?c=L3', 'H42{{ifeq:D|1|2|H}}') | ||
+ | test('Unknown.*', '{{{a}}}{{{b}}}{{{c|{{{#duckcount}}}}}}') | ||
+ | test(nil, '{{{a}}}{{{b}}}{{{c|{{{}}}', | ||
+ | '[[x]]|?a=L1|?b=L2|?c=L3', 'H42{{{') | ||
+ | test(nil, '{{{a}}}{{{b}}}{{{c|}}}}}}', | ||
+ | '[[x]]|?a=L1|?b=L2|?c=L3', 'H42}}}') | ||
+ | |||
+ | mw.log('Test passed.') | ||
+ | return 'Test passed.' | ||
+ | end | ||
+ | |||
+ | if not mw.smw then | ||
+ | error('Semantic Mediawiki is not available', 1) | ||
+ | end | ||
+ | |||
+ | local querycond = origArgs.condition | ||
+ | local template, template2 = origArgs.template | ||
+ | if querycond then | ||
+ | querycond, template2 = check_querycond(querycond) | ||
+ | else | ||
+ | querycond = origArgs[1] | ||
+ | if not querycond then error('Missing condition', 1) end | ||
+ | if querycond == 'test' then return test() end | ||
+ | querycond, template2 = check_querycond(undo_nowiki(querycond)) | ||
+ | if not template then | ||
+ | template = origArgs[2] | ||
+ | if not template then | ||
+ | template = template2 | ||
+ | template2 = nil | ||
+ | end | ||
+ | end | ||
+ | end | ||
+ | if template2 then error('Invalid condition', 1) end | ||
+ | if not template then error('Missing template', 1) end | ||
+ | |||
+ | local querylimit = check_number(origArgs.limit, 500, 'limit') | ||
+ | local resultoffset = check_number(origArgs.offset, 0, 'offset') | ||
+ | |||
+ | local itempl, otempl | ||
+ | if origArgs.introtemplate then | ||
+ | itempl = transform_template(nil, undo_nowiki(origArgs.introtemplate)) | ||
+ | end | ||
+ | if origArgs.outrotemplate then | ||
+ | otempl = transform_template(nil, undo_nowiki(origArgs.outrotemplate)) | ||
+ | end | ||
+ | template = undo_nowiki(template) | ||
+ | local rtempl, query = transform_template(querycond, template) | ||
+ | |||
+ | query.limit = querylimit | ||
+ | query.offset = resultoffset | ||
+ | |||
+ | local sort = origArgs.sort | ||
+ | if sort and sort ~= '' then | ||
+ | sort = string.gsub(',' .. sort .. ',', '%s*,[%s,]*', ',') | ||
+ | -- Blank = mainlabel, this is also the default. | ||
+ | sort = sort:gsub(',page,', ',,'):match('^,(.-),$') | ||
+ | if sort ~= '' then query.sort = sort end | ||
+ | end | ||
+ | |||
+ | local order = origArgs.order | ||
+ | if order and order ~= '' then | ||
+ | query.order = order | ||
+ | end | ||
+ | |||
+ | local output = {} | ||
+ | |||
+ | if origArgs[1] == "test2" then | ||
+ | table.insert(output, escape_concat('query:' .. dump(query))) | ||
+ | end | ||
+ | |||
+ | local results = mw.smw.ask(query) | ||
+ | if type(results) ~= 'table' or #results < 1 then | ||
+ | local default = origArgs.default | ||
+ | if not default then default = '' end | ||
+ | return default | ||
+ | end | ||
+ | |||
+ | local values = { querycond, querylimit, resultoffset, | ||
+ | #results, 0, userparam or '', template } -- rownumber = 0 | ||
+ | |||
+ | if origArgs[1] == "test2" then | ||
+ | for k, v in pairs(results) do | ||
+ | s = string.format('results[%s]:%s', dump(k), dump(v)) | ||
+ | table.insert(output, escape_concat(s)) | ||
+ | end | ||
+ | table.insert(output, escape_concat('values:' .. dump(values))) | ||
+ | table.insert(output, escape_concat('rtempl:' .. dump(rtempl))) | ||
+ | return table.concat(output, '<br/>') | ||
+ | end | ||
+ | |||
+ | if itempl then substitute(output, itempl, values, {}) end | ||
+ | |||
+ | for i, row in ipairs(results) do | ||
+ | assert(i >= 1) -- rownumber = 1 to #results incl | ||
+ | values[5] = i | ||
+ | if type(row) ~= 'table' then row = {} end | ||
+ | substitute(output, rtempl, values, row) | ||
+ | end | ||
+ | |||
+ | if otempl then | ||
+ | values[5] = 0 | ||
+ | substitute(output, otempl, values, {}) | ||
+ | end | ||
+ | |||
+ | return frame:preprocess(table.concat(output)) | ||
end | end | ||
return p | return p |
Latest revision as of 01:10, 17 May 2023
Documentation for this module may be created at Module:comptable/doc
local p = {}
function p.main(frame)
if frame == mw.getCurrentFrame() then
origArgs = frame:getParent().args
else
origArgs = frame
end
local r = ''
for i = 2, 30 do
local val = origArgs[i]
if not val then break end
if string.find(val, ":") then
r = r .. string.gsub(val, "(%d+):(.+)", '<th colspan="%1">%2</th>')
else
r = r .. '<th>' .. val .. '</th>'
end
end
return '<tr class="comptable-header"><th> </th>' .. r .. '</tr>'
end
function p.lsep(frame)
if frame == mw.getCurrentFrame() then
origArgs = frame:getParent().args
else
origArgs = frame
end
local r = ''
for i = 2, 30 do
local val = origArgs[i]
if not val then break end
if string.find(val, ":") then
r = r .. string.gsub(val, "(%d+):(.+)", '<th colspan="%1" style="text-align: left;">%2</th>')
else
r = r .. '<th style="text-align: left;">' .. val .. '</th>'
end
end
return '<tr class="comptable-header"><th> </th>' .. r .. '</tr>'
end
function p.cols(frame)
if frame == mw.getCurrentFrame() then
origArgs = frame:getParent().args
else
origArgs = frame
end
local r = ''
for i = 2, 30 do
local val = origArgs[i]
if not val then break end
if string.match(val, '^%%.+') then
r = r .. '<th data-sort-type="number">' .. string.sub(val, 2) .. '</th>'
else
r = r .. '<th>' .. val .. '</th>'
end
end
return '<tr class="comptable-header"><th class="unsortable">Model</th>' .. r .. '</tr>'
end
---------------------------------------------
function p.main2(frame)
if frame == mw.getCurrentFrame() then
origArgs = frame:getParent().args
else
origArgs = frame
end
local r = ''
for i = 2, 30 do
local val = origArgs[i]
if not val then break end
if string.find(val, ":") then
r = r .. string.gsub(val, "(%d+):(.+)", '<th colspan="%1">%2</th>')
else
r = r .. '<th>' .. val .. '</th>'
end
end
return '<tr class="comptable2-header"><th> </th>' .. r .. '</tr>'
end
function p.lsep2(frame)
if frame == mw.getCurrentFrame() then
origArgs = frame:getParent().args
else
origArgs = frame
end
local r = ''
for i = 2, 30 do
local val = origArgs[i]
if not val then break end
if string.find(val, ":") then
r = r .. string.gsub(val, "(%d+):(.+)", '<th colspan="%1" style="text-align: left;">%2</th>')
else
r = r .. '<th style="text-align: left;">' .. val .. '</th>'
end
end
return '<tr class="comptable2-header"><th> </th>' .. r .. '</tr>'
end
function p.cols2(frame)
if frame == mw.getCurrentFrame() then
origArgs = frame:getParent().args
else
origArgs = frame
end
local r = ''
for i = 2, 30 do
local val = origArgs[i]
if not val then break end
if string.match(val, '^%%.+') then
r = r .. '<th data-sort-type="number">' .. string.sub(val, 2) .. '</th>'
else
r = r .. '<th>' .. val .. '</th>'
end
end
return '<tr class="comptable2-header"><th class="unsortable">Model</th>' .. r .. '</tr>'
end
function p.askt(frame)
local origArgs = frame.args
if not (origArgs[1] or origArgs.condition) and frame:getParent() then
-- Use args of template containing the #invoke.
origArgs = frame:getParent().args
end
local function undo_nowiki(s)
s = mw.text.unstripNoWiki(s)
--[[ Unfortunate because we can't tell if nowiki escaped the
characters or a user did, and we need them for query
conditions and to output HTML tags. But < and >
works. ]]
s = s:gsub('<', '<'):gsub('>', '>')
-- https://www.mediawiki.org/wiki/Writing_systems/Syntax
return s:gsub('%-{', '-{'):gsub('}%-', '}-')
end
local pound, eqsign, vbar = 35, 61, 124
local function check_querycond(s)
local cond, i, j, k = false, 1
j, k = s:find('^%s*')
if j then i = k + 1 end
while i <= #s and s:byte(i) ~= vbar do
j = nil
if cond then
j, k = s:find('^OR%s*', i)
end
if j then
i = k + 1
cond = false
else
j, k = s:find('^%[%[[^%]]+%]%]%s*', i)
if j then
i = k + 1
cond = true
else
error('Missing/invalid condition', 2)
end
end
end
if not cond then
error('Missing condition', 2)
end
if i > #s then return s, nil end
return s:sub(1, i - 1), s:sub(i + 1, -1)
end
local function check_number(n, default, arg_name)
if not n then return default end
n = tonumber(n)
if not n or n < 0 then
error('Invalid ' .. arg_name, 2)
end
return math.floor(n)
end
local special_props = { '#querycondition', '#querylimit',
'#resultoffset', '#rowcount', '#rownumber', '#userparam',
'#template' }
-- May be nil.
local userparam = origArgs.userparam
local function transform_template(querycond, s)
local query = { querycond } -- mw.smw.ask() parameter
local prop_index = {}
local n_props = 0
for i, v in ipairs(special_props) do
prop_index[v] = i
end
local function append_prop(prop)
local index = prop_index[prop]
if not index then
n_props = n_props + 1
index = 'L' .. n_props
prop_index[prop] = index
if querycond then
table.insert(query, string.format('?%s=%s', prop, index))
end
end
return index
end
local i, depth = 1, 0
local function parse()
if depth > 4 then error('Too many defaults', 2) end
depth = depth + 1
local templ = {}
while i <= #s do
local stop = #s + 1
if depth > 1 then
local j = s:find('}}}', i)
if j then stop = j end
end
local j, k, prop = s:find('{{{%s*([%w#][^#=|}]*)', i)
if j == nil or j >= stop then
if i < stop then
local prefix = s:sub(i, stop - 1)
table.insert(templ, { prefix=prefix })
end
i = stop
break
end
local prefix = s:sub(i, j - 1)
i = k + 1 -- skip {{{ and prop name
prop = prop:match('(.-)%s*$') -- remove trailing whitespace
local format = nil
if prop:byte(1) == pound then
if not prop_index[prop] then
error('Unknown special property "' .. prop .. '"', 2)
elseif prop == '#userparam' and not userparam then
error('No value for "#userparam" specified', 2)
end
elseif not querycond then
error('Unexpected property "' .. prop ..
'" in intro/outro template', 2)
else
-- {{{property#format}}}
j, k, format = s:find('^(#[^#=|}]+)', i)
if j then i = k + 1 end
end
local default = {}
local c = s:byte(i)
if c == eqsign then
error('Property labels not supported', 2)
elseif c == pound then
error('Invalid format for property "' .. prop .. '"', 2)
elseif c == vbar then
i = i + 1
-- {{{property|default}}} recursion
default = parse()
end
if not s:find('^}}}', i) then
error('Undelimited property "' .. prop .. '"', 2)
end
i = i + 3
if prop == 'page' then
prop = '' -- SMW ask mainlabel: '?' or '?#-'
if format and format ~= '#-' then
error('Special property "page" supports only format #-', 2)
end
end
if format == '#tick' then -- not supported in SMW < 3.0
format = function(value)
if type(value) == 'boolean' then
return value and '✔' or '✘'
end
return tostring(value) or ''
end
else
if format then prop = prop .. format end
format = function(value)
return tostring(value) or ''
end
end
table.insert(templ, { prefix=prefix, index=append_prop(prop),
default=default, format=format })
end
depth = depth - 1
return templ
end
local templ = parse()
return templ, query
end
local valuesep = origArgs.valuesep
if not valuesep then valuesep = ', ' end
local function concat_values(format, values)
if values == nil then return '' end
if type(values) ~= 'table' then return format(values) end
local output = {}
for i, value in ipairs(values) do
value = format(value)
if value ~= '' then table.insert(output, value) end
end
return table.concat(output, valuesep)
end
local function substitute(output, templ, values, row)
for i, f in ipairs(templ) do
table.insert(output, f.prefix)
if f.index then
local text
if type(f.index) == 'string' then
text = concat_values(f.format, row[f.index])
else
text = values[f.index] or ''
end
if text == '' then
substitute(output, f.default, values, row)
else
table.insert(output, text)
end
end
end
end
local function dump(t)
if t == nil then return 'nil' end
if type(t) == 'string' then return '"' .. t .. '"' end
if type(t) == 'table' then
s = '{'
for k, v in pairs(t) do
s = s .. string.format('[%s]=%s,', dump(k), dump(v))
end
return s .. '}'
end
return tostring(t) or '?'
end
local function escape_concat(s, sep)
if not s then s = '(nil)' end
if type(s) == 'table' then s = table.concat(s, sep) end
-- do return s end
s = s:gsub('[<%[{]', function(c)
return string.format('&#%d;', c:byte()) end)
return s
end
local function test()
local function test(pat, a, e1, e2)
local success, r1, r2 = pcall(check_querycond, a)
if not success then
if not pat then
error('Function failed unexpectedly: ' .. r1, 2)
elseif not r1:find(pat) then
error(string.format('Unexpected error msg\n✘ %s\n✔ %s\n',
r1, pat), 2)
end
elseif pat then
error('Function succeeded unexpectedly', 2)
elseif r1 ~= e1 or r2 ~= e2 then
error(string.format('Unexpected result\n✘ %s,%s\n✔ %s,%s\n',
r1, r2, e1, e2), 2)
end
end
test('Missing cond.*', ' ')
test('.*inv.*', 'x')
test('.*inv.*', '[x]')
test('.*inv.*', '[[x')
test('.*inv.*', '[[]]')
test('.*inv.*', 'OR[[x]]')
test('Missing cond.*', '[[x]]OR')
local s = ' [[x]] '
test(nil, s, s, nil)
local s = '[[x::foo]][[x::~*bar*]] [[x<<x]]'
test(nil, s, s, nil)
local s = ' \n [[x||x]] OR \n [[x>>x]]OR[[x]]\n'
test(nil, s, s, nil)
test(nil, '[[x||x]] \n ||[[x]]', '[[x||x]] \n ', '|[[x]]')
test(nil, '[[x]]| \nx | ', '[[x]]', ' \nx | ')
local function test(pat, a, exp_cond, exp_outp)
local success, r1, r2 = pcall(transform_template, cond, a)
if not success then
if not pat then
error('Function failed unexpectedly: ' .. r1, 2)
elseif not r1:find(pat) then
error(string.format('Unexpected error msg\n✘ %s\n✔ %s\n',
r1, pat), 2)
end
return
elseif pat then
error('Function succeeded unexpectedly', 2)
end
if cond then
r2 = escape_concat(r2, '|')
exp_cond = escape_concat(exp_cond)
if r2 ~= exp_cond then
error(string.format('Unexpected query\n✘ %s\n✔ %s\n',
r2, exp_cond), 2)
end
end
local output = {}
local values = { '[[x]]', 'B', 'C', 'D', 'E', userparam or '' }
local row = { 'G', L1='H', L2=42, L3='', L4=true, L5=false }
substitute(output, r1, values, row)
output = escape_concat(output)
exp_outp = escape_concat(exp_outp)
if output ~= exp_outp then
error(string.format('Unexpected output\n✘ %s\n✔ %s\n',
output, exp_outp), 2)
end
end
local function test1()
local s = ' |<br />\nfoo||{# {{\n '
test(nil, s, '[[x]]', s)
test(nil, 'a{{{=b}}}c', '[[x]]', 'a{{{=b}}}c')
test('Unknown.*', '{{{#}}}')
test('Unknown.*', 'a{{{ #-}}}c')
test('Unknown.*', '{{{#foo}}}')
test('Unknown.*', '{{{# rowcount}}}')
test('Invalid format.*', '{{{#rowcount#hex}}}')
test('.*label.*', '{{{#rowcount=x|foo}}}')
test(nil, '{{{#rowcount|foo=x}}}', '[[x]]', 'D')
test(nil, 'a{{{#querycondition}}}c', '[[x]]', 'a[[x]]c')
test(nil, 'a{{{ #querylimit}}}b', '[[x]]', 'aBb')
test(nil, 'a{{{#resultoffset }}}b', '[[x]]', 'aCb')
test(nil, 'a{{{#rowcount|default}}}b', '[[x]]', 'aDb')
test(nil, 'a{{{#rownumber}}}b', '[[x]]', 'aEb')
userparam = nil
test('No value.*', 'a{{{#userparam}}}b')
userparam = 'X'
test(nil, 'a{{{#userparam}}}b', '[[x]]', 'aXb')
end
cond = nil
test('Unexp.*', 'x{{{x')
test('Unexp.*', '{{{a}}}}')
test('Unexp.*', 'a{{{ b # GHz[-] }}}\n{{{d}}}e')
test('Unexp.*', 'a{{{b=c|foo}}}d')
test('Unexp.*', 'a{{{b#GHz=c|foo}}}d')
test('Unexp.*', 'a{{{page}}}c')
test('Unexp.*', 'a{{{page#-}}}c')
test1()
cond = '[[x]]'
test('Undelim.*', 'x{{{x')
test(nil, 'a{{{}}}c', '[[x]]', 'a{{{}}}c')
test(nil, 'a{{{b}}}c', '[[x]]|?b=L1', 'aHc')
test(nil, 'a{{{ b }}}', '[[x]]|?b=L1', 'aH')
test(nil, 'a{{{{ b}}}c', '[[x]]|?b=L1', 'a{Hc')
test(nil, 'a{{{{{{{{ b}}}c', '[[x]]|?b=L1', 'a{{{{{Hc')
test(nil, '{{{b}}}}c', '[[x]]|?b=L1', 'H}c')
test(nil, '{{{b}}}}}}}}c', '[[x]]|?b=L1', 'H}}}}}c')
test(nil, 'a{{{ b # GHz[-] }}}\n{{{d}}}e',
'[[x]]|?b# GHz[-] =L1|?d=L2', 'aH\n42e')
test(nil, '{{{a}}}{{{b#tick}}}c{{{d#tick}}}e{{{f#tick}}}' ..
'{{{g#tick}}}h{{{i#tick}}}j{{{k#tick|x}}}l',
'[[x]]|?a=L1|?b=L2|?d=L3|?f=L4|?g=L5|?i=L6|?k=L7', 'H42ce✔✘hjxl')
test('.*label.*', 'a{{{b=c|foo}}}d')
test('.*label.*', 'a{{{b#GHz=c|foo}}}d')
test(nil, 'a{{{=b}}}c', '[[x]]', 'a{{{=b}}}c')
test(nil, 'a{{{page}}}c', '[[x]]|?=L1', 'aHc')
test(nil, 'a{{{page#-}}}c', '[[x]]|?#-=L1', 'aHc')
test('page.*format', '{{{page#GHz}}}')
test('page.*format', '{{{page# -}}}')
test1()
test(nil, '{{{a}}}{{{b}}}{{{c}}}{{{d}}}',
'[[x]]|?a=L1|?b=L2|?c=L3|?d=L4', 'H42true')
test(nil, '{{{a|}}}{{{b|}}}{{{c|}}}{{{d|}}}',
'[[x]]|?a=L1|?b=L2|?c=L3|?d=L4', 'H42true')
test(nil, '{{{a|1}}}{{{b|2}}}{{{c|3}}}{{{d|4}}}',
'[[x]]|?a=L1|?b=L2|?c=L3|?d=L4', 'H423true')
test(nil, '{{{a#1}}}{{{b#2}}}{{{c#3|d#3}}}{{{e|f#4}}}',
'[[x]]|?a#1=L1|?b#2=L2|?c#3=L3|?e=L4', 'H42d#3true')
test(nil, '{{{a}}}{{{b}}}{{{c| \n3 }}}',
'[[x]]|?a=L1|?b=L2|?c=L3', 'H42 \n3 ')
test(nil, '{{{a}}}{{{b}}}{{{c|d{{{e}}}{{{f}}}{{{g}}}#f}}}',
'[[x]]|?a=L1|?b=L2|?e=L3|?f=L4|?g=L5|?c=L6', 'H42dtruefalse#f')
test(nil, '{{{a}}}{{{b}}}{{{c|d{{{b}}}e}}}',
'[[x]]|?a=L1|?b=L2|?c=L3', 'H42d42e')
test(nil, '{{{a}}}{{{b}}}{{{c|{{{d}}}{{{e}}}{{{f}}}{{{g|{{{a|x}}}}}}}}}',
'[[x]]|?a=L1|?b=L2|?d=L3|?e=L4|?f=L5|?g=L6|?c=L7', 'H42truefalseH')
test(nil, '{{{a|{{{b|{{{c|{{{d|x}}}}}}}}}}}}',
'[[x]]|?d=L1|?c=L2|?b=L3|?a=L4', 'true')
test('Too many.*', '{{{a|{{{b|{{{c|{{{d|{{{e|x}}}}}}}}}}}}}}}')
test(nil, '{{{a}}}{{{b}}}{{{c|{{ifeq:{{{#rowcount}}}|1|2|{{{a}}}}}}}}',
'[[x]]|?a=L1|?b=L2|?c=L3', 'H42{{ifeq:D|1|2|H}}')
test('Unknown.*', '{{{a}}}{{{b}}}{{{c|{{{#duckcount}}}}}}')
test(nil, '{{{a}}}{{{b}}}{{{c|{{{}}}',
'[[x]]|?a=L1|?b=L2|?c=L3', 'H42{{{')
test(nil, '{{{a}}}{{{b}}}{{{c|}}}}}}',
'[[x]]|?a=L1|?b=L2|?c=L3', 'H42}}}')
mw.log('Test passed.')
return 'Test passed.'
end
if not mw.smw then
error('Semantic Mediawiki is not available', 1)
end
local querycond = origArgs.condition
local template, template2 = origArgs.template
if querycond then
querycond, template2 = check_querycond(querycond)
else
querycond = origArgs[1]
if not querycond then error('Missing condition', 1) end
if querycond == 'test' then return test() end
querycond, template2 = check_querycond(undo_nowiki(querycond))
if not template then
template = origArgs[2]
if not template then
template = template2
template2 = nil
end
end
end
if template2 then error('Invalid condition', 1) end
if not template then error('Missing template', 1) end
local querylimit = check_number(origArgs.limit, 500, 'limit')
local resultoffset = check_number(origArgs.offset, 0, 'offset')
local itempl, otempl
if origArgs.introtemplate then
itempl = transform_template(nil, undo_nowiki(origArgs.introtemplate))
end
if origArgs.outrotemplate then
otempl = transform_template(nil, undo_nowiki(origArgs.outrotemplate))
end
template = undo_nowiki(template)
local rtempl, query = transform_template(querycond, template)
query.limit = querylimit
query.offset = resultoffset
local sort = origArgs.sort
if sort and sort ~= '' then
sort = string.gsub(',' .. sort .. ',', '%s*,[%s,]*', ',')
-- Blank = mainlabel, this is also the default.
sort = sort:gsub(',page,', ',,'):match('^,(.-),$')
if sort ~= '' then query.sort = sort end
end
local order = origArgs.order
if order and order ~= '' then
query.order = order
end
local output = {}
if origArgs[1] == "test2" then
table.insert(output, escape_concat('query:' .. dump(query)))
end
local results = mw.smw.ask(query)
if type(results) ~= 'table' or #results < 1 then
local default = origArgs.default
if not default then default = '' end
return default
end
local values = { querycond, querylimit, resultoffset,
#results, 0, userparam or '', template } -- rownumber = 0
if origArgs[1] == "test2" then
for k, v in pairs(results) do
s = string.format('results[%s]:%s', dump(k), dump(v))
table.insert(output, escape_concat(s))
end
table.insert(output, escape_concat('values:' .. dump(values)))
table.insert(output, escape_concat('rtempl:' .. dump(rtempl)))
return table.concat(output, '<br/>')
end
if itempl then substitute(output, itempl, values, {}) end
for i, row in ipairs(results) do
assert(i >= 1) -- rownumber = 1 to #results incl
values[5] = i
if type(row) ~= 'table' then row = {} end
substitute(output, rtempl, values, row)
end
if otempl then
values[5] = 0
substitute(output, otempl, values, {})
end
return frame:preprocess(table.concat(output))
end
return p