Pergi ke kandungan

Modul:Index template

Permanently protected module
Daripada Wikisumber

--[=[
Latest update: 15th April 2026
This is a module to implement logic for [[MediaWiki:Proofreadpage index template]],
to render the book/index metadata panel for Malay Wikisource index pages.
]=]

local ISO_639          = require('Modul:ISO 639')
local messageBox       = require('Module:Message box')
local category_handler = require('Module:Category handler')._main
local cfg              = require('Modul:Index template/config')

-- Low-level markup helpers 

local function construct_cat(cat)
    return '[[Kategori:' .. cat .. ']]'
end

local function construct_cat_link(cat, text)
    return '[[:Kategori:' .. cat .. '|' .. (text or cat) .. ']]'
end

-- Builds a single labelled metadata row.
local function construct_field(id, content)
    if id == nil or content == nil then return nil end
    if not cfg.headings[id] then error(cfg.missing_heading_id(id)) end
    return mw.html.create('tr')
        :attr('id',   'ws-index-' .. id .. '-row')
        :addClass('ws-index-row')
        :tag('th')
            :attr('scope', 'row')
            :attr('id',    'ws-index-' .. id .. '-label')
            :addClass('ws-index-label')
            :wikitext(cfg.headings[id].txt)
            :done()
        :tag('td')
            :attr('id', 'ws-index-' .. id .. '-value')
            :addClass('ws-index-value')
            :wikitext(content)
            :allDone()
end

-- Conditionally adds a metadata row and the appropriate tracking category.
-- If `content` is truthy the row is added plus `with_cat` (if given).
-- Otherwise only `without_cat` is emitted (if given).
local function add_field(tbl, html, id, content, with_cat, without_cat)
    if content then
        tbl:node(construct_field(id, content))
        if with_cat then html:wikitext(construct_cat(with_cat)) end
    elseif without_cat then
        html:wikitext(construct_cat(without_cat))
    end
end

-- Wikidata link helpers 

local function addWikidataToLink(page, label, category)
    local title = mw.title.new(page)
    if title == nil then
        return '[[' .. page .. '|' .. label .. ']]'
    end
    if title.isRedirect then title = title.redirectTarget end

    local tag    = mw.html.create('span')
    local itemId = mw.wikibase.getEntityIdForTitle(title.fullText)
    tag:wikitext('[[' .. page .. '|' .. label .. ']]')
    if itemId then
        tag:wikitext(
            ' [[Image:Wikidata.svg|10px|link=d:' .. itemId .. '|Lihat maklumat di Wikidata]]'
        )
        if category then tag:wikitext('[[Kategori:' .. category .. ']]') end
    end
    return tostring(tag)
end

-- Walks wikilinks inside `wikitext` and augments each with a Wikidata icon
-- when the linked page has a Wikidata entity.
local function withWikidataLink(wikitext, category)
    if wikitext == nil then return nil end
    -- Try bare [[Page]] links first …
    local result = mw.ustring.gsub(wikitext, '%[%[([^|%]]*)%]%]', function(page)
        return addWikidataToLink(page, mw.ustring.gsub(page, '%.*/', ''), category)
    end)
    if result ~= wikitext then return result end
    -- … then [[Page|Label]] links.
    return mw.ustring.gsub(wikitext, '%[%[([^|]*)|([^|%]]*)%]%]', function(page, link)
        return addWikidataToLink(page, link, category)
    end)
end

-- Formats a contributor field (author / translator / editor).
-- With a Wikidata item: augments links with Wikidata icons.
-- Without: wraps bare (non-wikilinked) names in {{Al|}}.
local function format_person(value, item)
    if item then return withWikidataLink(value) end
    if value:find('%[%[') then return value end
    return '{{Al|' .. value .. '}}'
end

-- Progress / transclusion status rows 

local function construct_status_field(args, statusArgs)
    local key        = statusArgs.key
    local config_key = statusArgs.config_key or key

    local index_status = args[key] or '_missing'
    local sd           = cfg[config_key][index_status] or cfg[config_key]['_default']

    local txt = sd.txt
    if type(txt) == 'function' then txt = txt(index_status) end

    local display = sd.error
        and ('<span class="error">' .. txt .. '</span>')
        or  construct_cat_link(sd.cat, txt)

    return {
        row = construct_field(key, display),
        cat = construct_cat(sd.cat),
    }
end

-- Indicator builder 

-- Builds one <indicator> tag from an indicator config entry.
-- `link` and `caption` may be plain strings or functions called with `args`.
-- An optional `include` predicate (also called with `args`) gates rendering;
-- defaults to always-show when absent.
local function construct_indicator(args, iData)
    local include = iData.include or cfg.indicator_defaults.include
    if not include(args) then return '' end

    local parts = { iData.image, iData.width or cfg.indicator_defaults.width }

    if iData.alt then table.insert(parts, 'alt=' .. iData.alt) end

	if iData.class then table.insert(parts, 'class=' .. iData.class) end

    local link = iData.link
    if type(link) == 'function' then link = link(args) end
    if link then table.insert(parts, 'link=' .. link) end

    local caption = iData.caption
    if type(caption) == 'function' then caption = caption(args) end
    if caption then table.insert(parts, caption) end

    return mw.getCurrentFrame():extensionTag{
        name    = 'indicator',
        content = '[[' .. table.concat(parts, '|') .. ']]',
        args    = { name = iData.name },
    }
end

-- Talk-page remarks 

local p = {}

function p._index_talk_remarks(args)
    if not args.talkPageTitle then
        args.talkPageTitle = mw.title.getCurrentTitle().talkPageTitle
    end

    local text = cfg.talkremarks.text(args)
    local cat  = category_handler({ construct_cat(cfg.talkremarks.cat) }) or ''

    local notes = ''
    if args.notes then
        notes = mw.html.create('div')
            :addClass('ombox-content')
            :css({
                ['text-align']   = 'left',
                ['max-height']   = '5.5em',
                ['overflow']     = 'scroll',
                ['padding']      = '0.25em',
                ['margin']       = '0.25em',
                ['border-style'] = 'dashed',
            })
            :newline()
            :wikitext(args.notes .. cat)
            :newline()
            :allDone()
    end

    return messageBox.main('ombox', {
        type      = 'content',
        image     = '[[File:Ambox important.svg|24px]]',
        style     = 'box-sizing:border-box;margin:-0.93em auto 0.0em;text-align:center;width:100%;',
        textstyle = 'font-size:93%;text-decoration:none;',
        text      = text .. tostring(notes),
    })
end

function p.index_talk_remarks(frame)
    local args = {}
    for k, v in pairs(frame.args) do args[k] = v end
    args.talkPageTitle = mw.title.getCurrentTitle().talkPageTitle
    return p._index_talk_remarks(args)
end

-- Language resolution 

-- Resolves the display language string and emits language-related tracking
-- categories onto `html`.  Returns the display string, or nil if unavailable.
local function resolve_language(args, item, html)
    local displayLang   = nil
    local languageCount = 0

    if item then
        local languages = item:formatPropertyValues(
            'P407', { mw.wikibase.entity.claimRanks.RANK_NORMAL }
        ).value
        if languages and mw.text.trim(languages) ~= '' then
            displayLang = languages
            for language in languages:gmatch('([^,]+)') do
                language = mw.text.trim(language)
                if language ~= '' then
                    languageCount = languageCount + 1
                    html:wikitext(construct_cat(
                        'Laman indeks karya yang asal dalam ' .. language
                    ))
                end
            end
        end
    end

    if not displayLang and args.language then
        local displayLangs = {}
        for _, l in ipairs(mw.text.split(args.language, ',%s*', false)) do
            local lang = mw.text.trim(l)
            if lang ~= '' then
                local langName = ISO_639.language_name(lang, lang)
                table.insert(displayLangs, langName)
                languageCount = languageCount + 1
                html:wikitext(construct_cat(
                    'Laman indeks karya yang asal dalam bahasa ' .. langName
                ))
            end
        end
        if #displayLangs > 0 then
            displayLang = table.concat(displayLangs, ', ')
        end
    end

    if displayLang then
        html:wikitext(construct_cat('Karya dengan maklumat bahasa'))
        if languageCount > 1 then
            html:wikitext(construct_cat('Laman indeks karya yang asal dalam pelbagai bahasa'))
        end
    else
        html:wikitext(construct_cat('Laman indeks karya tanpa maklumat bahasa'))
    end

    return displayLang
end

-- Metadata table 

local function build_metadata(args, item, html)
    local t = mw.html.create('table'):attr('id', 'ws-index-metadata')

    -- Title 
    if args.title then
        local titleContent
        if item then
            titleContent = withWikidataLink(args.title)
        elseif args.title:find('%[%[') then
            titleContent = args.title
        else
            titleContent = '[[' .. args.title .. ']]'
        end
        t:node(construct_field('title', titleContent))
    else
        mw.addWarning('Anda perlu isi medan tajuk borang.')
    end

    -- Subtitle 
    add_field(t, html, 'subtitle', withWikidataLink(args.subtitle))

    -- Language (complex; categories emitted inside resolve_language) 
    add_field(t, html, 'language', resolve_language(args, item, html))

    -- Simple bibliographic fields 
    add_field(t, html, 'volume',
        args.volume,
        'Karya dengan maklumat nombor jilid',
        'Karya tanpa maklumat nombor jilid')
    add_field(t, html, 'edition',
        args.edition,
        'Karya dengan maklumat edisi',
        'Karya tanpa maklumat edisi')

    -- Author (also emits per-author categories from Wikidata) 
    if args.author then
        t:node(construct_field('author', format_person(args.author, item)))
        html:wikitext(construct_cat('Karya dengan maklumat pengarang'))
        if item then
            local authors = item:formatPropertyValues(
                'P50', { mw.wikibase.entity.claimRanks.RANK_NORMAL }
            ).value or ''
            for author in authors:gmatch('([^,]+)') do
                html:wikitext(construct_cat('Karya ' .. mw.text.trim(author)))
            end
        end
    else
        html:wikitext(construct_cat('Karya tanpa maklumat pengarang'))
    end

    -- Contributor fields 
    add_field(t, html, 'translator',
        args.translator  and format_person(args.translator, item),
        'Karya dengan maklumat penterjemah',
        'Karya tanpa maklumat penterjemah')
    add_field(t, html, 'editor',
        args.editor      and format_person(args.editor, item),
        'Karya dengan maklumat penyunting',
        'Karya tanpa maklumat penyunting')
    add_field(t, html, 'illustrator',
        args.illustrator and withWikidataLink(args.illustrator),
        'Karya dengan maklumat pengilustrasi',
        'Karya tanpa maklumat pengilustrasi')
    add_field(t, html, 'publisher',
        args.publisher   and withWikidataLink(args.publisher),
        'Karya dengan maklumat penerbit',
        'Karya tanpa maklumat penerbit')

    -- Address / Published-in (mutually exclusive) 
    if args.address then
        t:node(construct_field('address', withWikidataLink(args.address)))
        html:wikitext(construct_cat('Karya dengan maklumat alamat'))
    elseif args.publishedin then
        t:node(construct_field('publishedin', withWikidataLink(args.publishedin)))
        html:wikitext(construct_cat('Karya dengan maklumat alamat penerbitan'))
    else
        html:wikitext(construct_cat('Karya tanpa maklumat alamat penerbitan'))
    end

    -- Year / Inception (mutually exclusive) 
    if args.year then
        t:node(construct_field('year', withWikidataLink(args.year)))
        html:wikitext(construct_cat('Karya dengan maklumat tarikh'))
        html:wikitext(construct_cat('Karya tahun ' .. args.year))
    elseif args.inception then
        t:node(construct_field('inception', withWikidataLink(args.inception)))
        html:wikitext(construct_cat('Karya dengan maklumat tarikh terawal'))
    else
        html:wikitext(construct_cat('Karya tanpa maklumat tarikh'))
    end

    add_field(t, html, 'printer',
        args.printer and withWikidataLink(args.printer),
        'Karya dengan maklumat pencetak',
        'Karya tanpa maklumat pencetak')

    -- Source (linkable formats get a file-page link) 
    local src = args.source
    t:node(construct_field('source',
        src and cfg.linkable_sources[src]
            and '[[:Fail:' .. mw.title.getCurrentTitle().text .. '|' .. src .. ']]'
            or  src
    ))

    -- Progress & transclusion status 
    local progress_data     = construct_status_field(args, { key = 'progress',     config_key = 'status' })
    local transclusion_data = construct_status_field(args, { key = 'transclusion' })
    t:node(progress_data.row);     html:wikitext(progress_data.cat)
    t:node(transclusion_data.row); html:wikitext(transclusion_data.cat)

    -- Validation date 
    local vdate = args.validation_date
    if vdate then
        local vcat = cfg.validation_cats.dated(vdate)
        t:node(construct_field('validation_date', construct_cat_link(vcat, vdate)))
        html:wikitext(construct_cat(vcat))
    elseif args.progress == 'T' then
        html:wikitext(construct_cat(cfg.validation_cats.undated))
    end

    -- Standard identifiers 
    for _, id in ipairs({ 'isbn', 'oclc', 'lccn', 'ark', 'doi' }) do
        local val = args[id]
        if val then
            local link_fn = cfg.url_gens[id]
            t:node(construct_field(id, link_fn and link_fn(val, val) or val))
        end
    end

    add_field(t, html, 'volumes', args.volumes)

    return t
end

-- Cover image 

--[=[
Decision tree (evaluated in order):
  1. Full [[...]] wikilink spec supplied → use as-is
  2. Bare filename (has extension)       → strip namespace prefix, rewrap
  3. Audiovisual timestamp string        → treat as thumbtime= value (→ case 5)
  4. Multipage (DjVu/PDF) + file exists  → page= thumbnail
  5. Audiovisual + file exists           → thumbtime= thumbnail
  6. Any other type + file exists        → plain file link
  7. File does not exist                 → placeholder + missing category
--]=]
local function build_cover(args)
    local image_number = tonumber(args.image)
    -- Treat a bare timestamp string (e.g. "83" or "1:23") as a thumbtime value.
    if args.image and not image_number
        and args.source_type == 'audiovisual'
        and mw.ustring.match(args.image, '^%d+[%d:]*$') then
        image_number = args.image
    end

    local image_spec
    local cats = {}

    if not image_number and args.image and mw.ustring.find(args.image, '^%[%[') then
        -- Case 1: full wikilink spec
        image_spec = args.image
        if args.source_type ~= 'image' then
            table.insert(cats, construct_cat(cfg.cover_cats.fullspec))
        end

    elseif not image_number and args.image and mw.ustring.find(args.image, '%.%w+$') then
        -- Case 2: bare filename — strip any recognised File namespace prefix
        local image_name = args.image
        local file_ns    = mw.site.namespaces[6]
        local prefixes   = { file_ns.name, file_ns.canonicalName }
        for _, alias in ipairs(file_ns.aliases) do
            table.insert(prefixes, alias)
        end
        for _, prefix in ipairs(prefixes) do
            image_name = mw.ustring.gsub(image_name, '^' .. prefix .. ':',         '')
            image_name = mw.ustring.gsub(image_name, '^' .. prefix:lower() .. ':', '')
        end
        image_spec = '[[' .. mw.title.makeTitle('Fail', image_name).prefixedText
            .. '|' .. cfg.cover.width .. '|class=ws-cover]]'
        if args.source_type ~= 'image' then
            table.insert(cats, construct_cat(cfg.cover_cats.fullspec))
        end

    elseif args.source_type == 'multipage' and args.file_exists then
        -- Case 4: DjVu / PDF page thumbnail
        image_spec = '[[' .. args.fileTitle.prefixedText
            .. '|' .. cfg.cover.width
            .. '|page=' .. (image_number or 1)
            .. '|class=ws-cover]]'

    elseif args.source_type == 'audiovisual' and args.file_exists then
        -- Case 5: audio / video timestamp thumbnail
        image_spec = '[[' .. args.fileTitle.prefixedText
            .. '|' .. cfg.cover.width
            .. '|thumbtime=' .. (image_number or 0)
            .. '|class=ws-cover]]'

    elseif args.file_exists then
        -- Case 6: image or unknown type
        image_spec = '[[' .. args.fileTitle.prefixedText
            .. '|' .. cfg.cover.width .. '|class=ws-cover]]'

    else
        -- Case 7: file missing — fall back to placeholder
        local image_link = args.fileTitle.prefixedText
        if not mw.ustring.find(args.fileTitle.rootText, '^.*%.%w+') then
            image_link = 'Special:Upload'
        end
        image_spec = '[[' .. cfg.cover.image
            .. '|' .. cfg.cover.width
            .. '|link=' .. image_link
            .. '|class=ws-cover]]'
        table.insert(cats, construct_cat(cfg.cover_cats.missing))
    end

    return image_spec .. table.concat(cats)
end

-- Argument setup 

-- Resolves all derived values and performs every expensive DB lookup exactly
-- once.  Anything that touches the DB (file.exists, title.exists) lives here
-- and nowhere else, making the cost immediately visible to maintainers.
local function process_args(args)
    -- Apply config defaults for any args not supplied by the template.
    for k, v in pairs(cfg.defaults or {}) do
        if args[k] == nil then args[k] = v end
    end

    -- Allow pageTitle to be pre-set (e.g. from a sandbox or test harness).
    -- In normal template rendering it is always nil, so getCurrentTitle() is used.
    args.pageTitle     = (args.pageTitle and mw.title.new(args.pageTitle))
                         or mw.title.getCurrentTitle()
    args.talkPageTitle = args.pageTitle.talkPageTitle
    args.fileTitle     = mw.title.makeTitle('Fail', args.pageTitle.rootText)
    args.source_type   = cfg.cover_cats.file_types[args.source]
    args.file_exists   = args.fileTitle.file.exists      -- expensive: DB lookup
    args.talk_exists   = args.talkPageTitle.exists       -- expensive: DB lookup
    return args
end

-- Main entry point 

local function indexTemplate(frame)
    local styles = frame:extensionTag{
        name = 'templatestyles',
        args = { src = 'Modul:Index template/styles.css' },
    }

    local data       = (require 'Modul:Index_template/data').indexDataWithWikidata(frame)
    local args, item = data.args, data.item

    args = process_args(args)

    -- Sort index pages correctly in categories (falls back to page title).
    local sortkey = mw.getCurrentFrame():callParserFunction(
        'DEFAULTSORT', { args.pageTitle.text }
    )

    -- Scan the talk page for formatting notes unless notes were supplied directly.
    local talkremarks = ''
    local talk_page_exists = args.talk_exists
    if talk_page_exists and not args.notes then
        local content = args.talkPageTitle.content
        for _, keyword in ipairs(cfg.talkremarks.keywords) do
            local pattern = keyword.alone
                and '==( *'         .. keyword.pattern .. ' *)=='
                or  '==([^=%n]*'    .. keyword.pattern .. '[^=%n]*)=='
            local section = mw.ustring.match(content, pattern)
            if section then
                args.notes = mw.getCurrentFrame():callParserFunction(
                    '#lsth',
                    args.talkPageTitle.prefixedText,
                    mw.text.trim(section)
                )
                break
            end
        end
    end
    if talk_page_exists then
        talkremarks = p._index_talk_remarks(args)
    end

    local html = mw.html.create()

    -- Wikidata indicator 
    if item then
        html:wikitext('[[Kategori:Karya dengan ID Wikidata]]')
        html:wikitext(
            '<indicator name="wikidata">[[File:Wikidata.svg|20px|ID Wikidata|link=d:'
            .. item.id .. ']]</indicator>'
        )
    else
        html:wikitext('[[Kategori:Karya tanpa ID Wikidata]]')
    end

	-- Tool indicators 
    for _, v in ipairs(cfg.indicators) do
        html:wikitext(construct_indicator(args, v))
    end

    local metadataTable = build_metadata(args, item, html)
    local coverImage    = build_cover(args)

    -- Outer layout: [cover + metadata + pagelist] | [remarks] 
    local outerRow = html:tag('table')
        :attr('id', 'ws-index-container')
        :tag('tr')

    outerRow:tag('td')
        :attr('id', 'ws-index-main-cell')
        :tag('table')
            :attr('id', 'ws-index-main-table')
            :tag('tr'):tag('td')
                :tag('div')
                    :attr('id', 'ws-index-cover-container')
                    :wikitext(coverImage)
                    :done()
                :node(metadataTable)
                :done()
            :tag('tr'):tag('td')
                :tag('div')
                    :attr('id', 'ws-index-pagelist-container')
                    :addClass('mw-collapsible')
                    :tag('em'):wikitext(cfg.pagelist.pages.txt):done()
                    :wikitext(' ')
                    :tag('span')
                        :attr('id', 'ws-index-pagelist-legend')
                        :wikitext(cfg.pagelist.legend.txt)
                        :done()
                    :tag('div')
                        :attr('id', 'ws-index-pagelist')
                        :addClass('index-pagelist mw-collapsible-content')
                        :newline()
                        :wikitext(args.pages and mw.text.trim(args.pages))
                        :newline()

    if args.remarks then
        outerRow:tag('td')
            :attr('id', 'ws-index-remarks')
            :newline()
            :wikitext(frame:preprocess(args.remarks))
    else
        outerRow:tag('td'):attr('id', 'ws-index-remarks-empty')
    end

    -- Notes block 
    if args.notes then
        html:tag('div'):attr('id', 'ws-index-notes'):wikitext(args.notes)
    end

    -- Tracking categories 
    if cfg.type_cats[args.type] then
        html:wikitext(construct_cat(cfg.type_cats[args.type]))
    end
    if cfg.source_cats[args.source] then
        html:wikitext(construct_cat(cfg.source_cats[args.source]))
    elseif args.source ~= 'lain-lain' then
        html:wikitext(construct_cat('Indeks fail format lain'))
    end

    html:wikitext(construct_cat('Indeks'))
    if not args.remarks then
        html:wikitext(construct_cat('Laman berindeks'))
    end

    return talkremarks .. styles .. tostring(html) .. sortkey
end

function p.indexTemplate(frame)
    return indexTemplate(frame)
end

return p