Для документации этого модуля может быть создана страница Модуль:Коллективы/doc

-- Bard-Wiki collective pages. Lua is presentation/read-only; storage is in templates.
local p = {}
local groupCache
local function text(v) return mw.text.nowiki(tostring(v or '')) end
local function clean(v) return mw.text.trim(tostring(v or '')) end
local function quote(v) return "'" .. tostring(v):gsub('\\','\\\\'):gsub("'","\\'") .. "'" end
local function split(v, sep)
    local out = {}
    for _, part in ipairs(mw.text.split(v or '', sep or ';', true)) do
        part = clean(part); if part ~= '' then out[#out+1] = part end
    end
    return out
end
local function link(v,label)
    local title = mw.title.new(clean(v))
    if not title then return text(label or v) end
    return '[[:' .. title.prefixedText .. '|' .. text(label or v) .. ']]'
end
local function canonical(v)
    local t = mw.title.new(clean(v))
    for i=1,5 do
        if not t or not t.isRedirect or not t.redirectTarget then break end
        t=t.redirectTarget
    end
    return t and t.prefixedText or clean(v)
end
function p.canonicalPerson(frame) return canonical(frame.args[1]) end
local function date(v)
    v=clean(v)
    if v=='' or v:match('^%d%d%d%d$') then return text(v) end
    local ok,result=pcall(function() return mw.getContentLanguage():formatDate('j xg Y',v) end)
    return text(ok and result or v)
end
local function query(tables,fields,args)
    args=args or {}; args.limit=500
    local rows,offset={},0
    repeat
        args.offset=offset
        local batch=mw.ext.cargo.query(tables,fields,args)
        for _,r in ipairs(batch) do rows[#rows+1]=r end
        if #batch<500 then return rows end
        offset=offset+500
    until offset>=10000
    error('Список слишком велик; требуется постраничный просмотр.')
end
local function groups()
    if not groupCache then
        groupCache=query('Collective','_pageName=page,Name,Type,OtherNames,Description,Test', {orderBy='_pageID'})
    end
    return groupCache
end
local function aliases(row)
    local out,seen={},{}
    for _,name in ipairs({row.page or '', row.Name or '', unpack(split(row.OtherNames))}) do
        if name~='' and not seen[name] then out[#out+1]=name;seen[name]=true end
    end
    return out
end
local function findGroup(name)
    local key=mw.ustring.lower(clean(name):gsub('_',' '))
    for _,g in ipairs(groups()) do
        for _,a in ipairs(aliases(g)) do
            if mw.ustring.lower(a:gsub('_',' '))==key then return g end
        end
    end
end
local function linkedList(value,sep)
    local out={};for _,v in ipairs(split(value,sep)) do out[#out+1]=link(v) end
    return table.concat(out,'<br />')
end
function p.card(frame)
    local a=frame:getParent().args
    local box=mw.html.create('table'):addClass('infobox bw-collective-card')
    box:tag('tr'):tag('th'):attr('colspan',2):wikitext(text(a.Name or mw.title.getCurrentTitle().text))
    local function row(label,value)
        if value and value~='' then
            local tr=box:tag('tr');tr:tag('th'):wikitext(label);tr:tag('td'):wikitext(value)
        end
    end
    if clean(a.Image)~='' then
        local file=mw.title.new(a.Image,6)
        if file and file.namespace==6 then
            box:tag('tr'):tag('td'):attr('colspan',2):addClass('bw-collective-photo'):wikitext('[['..file.prefixedText..'|frameless|280px|'..text(a.ImageDes)..']]<br />'..text(a.ImageDes))
        end
    end
    row('Тип',text(a.Type));row('Полное название',text(a.FullName))
    row('Другие названия',text(a.OtherNames));row('Основан',date(a.Founded))
    row('Завершение деятельности',date(a.Ended));row('Место создания',linkedList(a.PlaceFounded))
    row('Страны',linkedList(a.Country));row('Города',linkedList(a.City))
    row('Жанры',text(a.Genre));row('Основатели',linkedList(a.Founders))
    row('Клубы и объединения',linkedList(a.Clubs));row('Официальные ссылки',a.Websites)
    row('Контакты',a.Contacts)
    return tostring(box)..'\n\n'..text(a.Description)..'\n'
end
-- Separate #invoke calls do not share Lua globals. Variables keeps this flag
-- within one page parse, including PageForms previews with repeated templates.
local function headingOnce(frame,key,heading)
    if frame:callParserFunction('#var',{key})=='1' then return '' end
    frame:callParserFunction('#vardefine',{key,'1'})
    return '\n== '..heading..' ==\n'
end
function p.member(frame)
    local a=frame:getParent().args
    if clean(a.Person)=='' then return '' end
    local out=headingOnce(frame,'bw-collective-member-heading','Состав')
    local status=clean(a.Status)
    if clean(a.EndDate)~='' then status='Бывший' end
    if status=='Не уточнено' or status=='Действующий' then status='' end
    local period=''
    if clean(a.StartDate)~='' then period='с '..date(a.StartDate) end
    if clean(a.EndDate)~='' then period=period..' по '..date(a.EndDate) end
    local item=mw.html.create('div'):addClass('bw-collective-member')
    local target=canonical(a.Person)
    local person=mw.ext.cargo.query('Persona','Name',{where='_pageName='..quote(target),limit=1})
    local label=person[1] and clean(person[1].Name) or ''
    if label=='' then label=a.Person end
    local memberTitle=mw.title.new(target)
    local memberLink=link(target,label)
    if memberTitle and memberTitle.namespace==0 and memberTitle.interwiki=='' and not memberTitle.exists then
        local first=target:match('^([^ ]+)') or target
        local last=target:match('([^ ]+)$') or target
        local full=target:find(',',1,true) and target or last..', '..first
        local values='Persona[Name]='..mw.uri.encode(label,'PATH')..'&Persona[FullName]='..mw.uri.encode(full,'PATH')
        local args={target=target,form='Persona',['query string']=values}
        if mw.title.getCurrentTitle().namespace==0 then args[1]='create page' end
        memberLink=frame:callParserFunction('#formredlink',args)
    end
    item:tag('strong'):wikitext(memberLink)
    if clean(a.Role)~='' then item:wikitext(' — '..text(a.Role)) end
    local details={}
    if status~='' then details[#details+1]=text(status) end
    if period~='' then details[#details+1]=period end
    if #details>0 then
        item:tag('span'):addClass('bw-collective-period'):wikitext(' · '..table.concat(details,' · '))
    end
    if clean(a.Note)~='' then item:tag('div'):addClass('bw-collective-note'):wikitext(text(a.Note)) end
    return out..tostring(item)..'\n'
end
function p.period(frame)
    local a=frame:getParent().args
    if clean(a.StartDate)=='' and clean(a.EndDate)=='' and clean(a.Note)=='' then return '' end
    local out=headingOnce(frame,'bw-collective-period-heading','Периоды деятельности')
    local value=clean(a.StartDate)~='' and date(a.StartDate) or 'Начало не уточнено'
    value=value..(clean(a.EndDate)~='' and ' — '..date(a.EndDate) or '')
    if clean(a.Note)~='' then value=value..' — '..text(a.Note) end
    return out..'<div class="bw-collective-period-row">'..value..'</div>\n'
end
function p.personGroups(frame)
    local a=frame.args
    local page=canonical(a.page or mw.title.getCurrentTitle().prefixedText)
    local terms={page};if clean(a.name)~='' then terms[#terms+1]=a.name end
    local conditions={};for _,n in ipairs(terms) do conditions[#conditions+1]='Person='..quote(n) end
    local found=query('CollectiveMember','_pageName=page', {where=table.concat(conditions,' OR '),orderBy='_pageID'})
    local out,seen={},{}
    local function add(name)
        local g=findGroup(name); local target=g and g.page or name
        if g and g.Test=='1' then return end
        if not seen[target] then out[#out+1]=link(target,g and g.Name or name);seen[target]=true end
    end
    for _,r in ipairs(found) do add(r.page) end
    for _,name in ipairs(split(a.legacy,',')) do add(name) end
    return table.concat(out,'<br />')
end
function p.author(frame)
    local name=clean(frame.args[1]);if name=='' then return '' end
    local g=findGroup(name)
    if g then return link(g.page,name) end
    local title=mw.title.new(name)
    if not title then return text(name) end
    if title.exists then return link(name) end
    local collective=false
    for _,n in ipairs(split(frame.args[2],',')) do
        if mw.ustring.lower(n)==mw.ustring.lower(name) then collective=true end
    end
    local form=collective and 'Collective' or 'Persona'
    local values
    -- PageForms' query decoder preserves literal plus signs. Encode spaces as
    -- %20 rather than + so auto-created names keep their original spelling.
    if collective then values='Collective[Name]='..mw.uri.encode(name,'PATH')..'&Collective[Type]='..mw.uri.encode('Коллектив','PATH')
    else
        local last=name:match('([^ ]+)$') or name
        local first=name:match('^([^ ]+)') or name
        values='Persona[Name]='..mw.uri.encode(name,'PATH')..'&Persona[FullName]='..mw.uri.encode(last..', '..first,'PATH')
    end
    return frame:callParserFunction('#formredlink',{target=name,form=form,['query string']=values,'create page'})
end
function p.performerValues()
    local values,seen={},{}
    local function add(n)
        n=clean(n)
        if n~='' and not n:find(',',1,true) and not seen[n] then values[#values+1]=n;seen[n]=true end
    end
    for _,r in ipairs(query('Persona','Name',{orderBy='_pageID'})) do add(r.Name) end
    for _,g in ipairs(groups()) do if g.Test~='1' then add(g.Name) end end
    table.sort(values)
    return table.concat(values,',')
end
local function tableStart(headers)
    local t=mw.html.create('table'):addClass('wikitable bw-collective-table')
    local tr=t:tag('tr');for _,h in ipairs(headers) do tr:tag('th'):wikitext(h) end
    return t
end
local function cell(tr,value) tr:tag('td'):wikitext(value or '') end
function p.repertoire(frame)
    local page=mw.title.getCurrentTitle().prefixedText
    local g=findGroup(page)
    if not g then return '\n== Исполнения песен ==\nПока нет добавленных исполнений.\n' end
    local where={}
    for _,name in ipairs(aliases(g)) do where[#where+1]='p.performer HOLDS '..quote(name) end
    local rows=query('Performance=p,song=s','p._ID=id,p._pageName=page,p.performer=performers,p.link=url,p.type=kind,p.comm=note,s.Name=name,s.Composer=composer,s.Lyric=lyric,s.FirstString=first',
        {where='('..table.concat(where,' OR ')..')',['join']='p._pageID=s._pageID',orderBy='s.Name,p._ID'})
    local out='\n== Исполнения песен ==\n'
    if #rows==0 then out=out..'Пока нет добавленных исполнений. Их можно указать в разделе «Исполнение» на страницах песен.\n'
    else
        local t=tableStart({'Песня','Авторы','Первая строка','Запись','Исполнители / комментарий'})
        local seen={}
        for _,r in ipairs(rows) do
            if not seen[r.id] then
                seen[r.id]=true;local tr=t:tag('tr')
                cell(tr,link(r.page,r.name or r.page))
                cell(tr,text(r.composer==r.lyric and r.composer or table.concat({r.composer or '',r.lyric or ''},' / ')))
                cell(tr,text(r.first))
                local url=r.url or ''
                cell(tr,url:match('^https?://') and not url:find('[%s<>%[%]{}|]') and '['..url..' '..text(r.kind or 'Запись')..']' or text(r.kind))
                cell(tr,text(r.performers)..(clean(r.note)~='' and '<br />'..text(r.note) or ''))
            end
        end
        out=out..'<div class="bw-collective-scroll">'..tostring(t)..'</div>\n'
    end
    where={}
    for _,name in ipairs(aliases(g)) do
        for _,field in ipairs({'Composer','Lyric','Translation'}) do where[#where+1]=field..' HOLDS '..quote(name) end
    end
    local songs=query('song','_pageName=page,Name,FirstString',{where=table.concat(where,' OR '),orderBy='Name,_pageID'})
    if #songs>0 then
        out=out..'\n== Авторские песни ==\n'
        local t=tableStart({'Песня','Первая строка'});local seen={}
        for _,r in ipairs(songs) do if not seen[r.page] then local tr=t:tag('tr');cell(tr,link(r.page,r.Name));cell(tr,text(r.FirstString));seen[r.page]=true end end
        out=out..'<div class="bw-collective-scroll">'..tostring(t)..'</div>\n'
    end
    return out
end
function p.catalog()
    local out={};local buckets={}
    for _,g in ipairs(groups()) do
        if g.Test~='1' then local kind=clean(g.Type);if kind=='' then kind='Коллектив' end
            buckets[kind]=buckets[kind] or {};table.insert(buckets[kind],g)
        end
    end
    for _,kind in ipairs({'Дуэт','Трио','Ансамбль','Группа','Творческий проект','Коллектив'}) do
        if buckets[kind] then
            out[#out+1]='\n== '..kind..' ==\n'
            table.sort(buckets[kind],function(a,b)return a.Name<b.Name end)
            for _,g in ipairs(buckets[kind]) do out[#out+1]='* '..link(g.page,g.Name)..(clean(g.Description)~='' and ' — '..text(g.Description) or '')..'\n' end
        end
    end
    return #out>0 and table.concat(out) or 'Пока нет заполненных страниц коллективов.'
end
return p