From WikiChip
Difference between revisions of "Module:comptable"

(askt: Added template special prop.)
 
(10 intermediate revisions by 2 users not shown)
Line 1: Line 1:
 
local p = {}
 
local p = {}
  
function p.main_header(frame)
+
function p.main(frame)
 
     if frame == mw.getCurrentFrame() then
 
     if frame == mw.getCurrentFrame() then
 
         origArgs = frame:getParent().args
 
         origArgs = frame:getParent().args
Line 21: Line 21:
  
 
return '<tr class="comptable-header"><th>&nbsp;</th>' .. r .. '</tr>'
 
return '<tr class="comptable-header"><th>&nbsp;</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>&nbsp;</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>&nbsp;</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>&nbsp;</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 &#60; and &#62;
 +
            works. ]]
 +
        s = s:gsub('&lt;', '<'):gsub('&gt;', '>')
 +
        -- https://www.mediawiki.org/wiki/Writing_systems/Syntax
 +
        return s:gsub('%-&#123;', '-{'):gsub('&#125;%-', '}-')
 +
    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 02: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>&nbsp;</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>&nbsp;</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>&nbsp;</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>&nbsp;</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 &#60; and &#62;
            works. ]]
        s = s:gsub('&lt;', '<'):gsub('&gt;', '>')
        -- https://www.mediawiki.org/wiki/Writing_systems/Syntax
        return s:gsub('%-&#123;', '-{'):gsub('&#125;%-', '}-')
    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