Modul:Index template
Penampilan
| This module depends on the following other modules: |
This is the current module to implement logic for MediaWiki:Proofreadpage index template, based on Wikidata - Wikisource Integration Modules on MediaWiki with significant changes:
- Reconfiguration of index page panel structure (which includes image, metadata, pagelist, and remarks/notes sections) to follow the structure on Module:Proofreadpage index template.
- Adaptation new metadata fields from Module:Proofreadpage index template: (1) language parameter and categorization, ensuring manually-entered language code would return language canonical names; (2) transclusion status; (3) validation date; (4) ISBN/OCLC/LCCN/ARK/DOI; (5) talk page remark related functionality; (6) missing file tracking category; (7) full image spec tracking category; and (8) audiovisual thumbtime cover
- Sufficient gap between image and metadata
- Sufficient gap between labels and values of metadata
- Author property value changed to P50 instead of P253075
- Ensured all file types under the source parameter could be wikilinked.
- The wikilinks for title, author, translator and editor fields could be generated automatically or manually
- Simplified the invocation of the module at MediaWiki:Proofreadpage index template so that no explicit mentioning of parameters and indicators.
- Integration of Modul:Index data into Modul:Index template, with the new name Modul:Index template/data.
- Replaced the usage of transclusion_checker indicator (external toolforge link) to transclusion check gadget.
- Utilized the Modul:Arguments for getArgs.
--[=[
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