Module:LibraryRows

From Bodhicitta
Revision as of 18:55, 2 October 2026 by Jeremi (talk | contribs) (Keep the glossary out of the deferred row chunks (each is its own parse))

Documentation for this module may be created at Module:LibraryRows/doc

-- Bibliographic rows for the library hub pages (Library, Library/Books, ...).
--
-- The same rows the topic pages use (Template:TopicBiblioRow): cover, citation,
-- class and date, an "About this item" disclosure. They replace the tile grid,
-- which was heavier to render and showed less.
--
--   {{#invoke:LibraryRows|controls|classes=Books;Articles;Classical Works|q:Multimedia=[[...]]}}
--       text filter plus class pills, as on topic pages (tws.js drives both)
--   {{#invoke:LibraryRows|section|heading=Books|link=Library/Books|query=[[Category:Books]]|title=yes}}
--       one class: a counted heading, then the rows (title=yes: title line above the citation)
--
-- Rows load in deferred chunks of CHUNK, so a long class does not hold up the
-- page; the number of chunks comes from the count, so nothing is truncated (the
-- old tile chunks stopped at a fixed 1,100).

local p = {}

local CHUNK = 100

-- Printout names must match Template:TopicBiblioRow's parameters (named args).
-- BcaAuthor is taken raw (#) so the row can build the link itself.
local PRINTOUTS = '|?Citation|?BcaAuthor#=BcaAuthor|?Class|?Title|?FrontCoverImg|?Pubdate|?TileDescription|?Description'

local function trim(s)
	s = mw.text.trim(s or '')
	return s ~= '' and s or nil
end

local function count(frame, query)
	local n = frame:preprocess('{{#ask:' .. query .. '|format=count}}')
	return tonumber((n or ''):match('%d+')) or 0
end

function p.controls(frame)
	local args = frame.args
	local classes = trim(args.classes) or ''
	local base = trim(args.base) or '[[Category:Library Items]]'
	local pills = {}
	local total = 0
	for class in mw.text.gsplit(classes, '%s*;%s*') do
		if class ~= '' then
			-- A pill counts what its section lists: pass q:<Class>=<query> when the
			-- section uses a narrower query than "every item of this class".
			local n = count(frame, trim(args['q:' .. class]) or (base .. '[[Class::' .. class .. ']]'))
			if n > 0 then
				total = total + n
				table.insert(pills, '<span class="topic-biblio-pill" data-filter-class="' .. mw.text.encode(class) ..
					'">' .. class .. ' <span class="rel-count">' .. mw.language.getContentLanguage():formatNum(n) .. '</span></span>')
			end
		end
	end
	local all = '<span class="topic-biblio-pill is-active" data-filter-class="">All <span class="rel-count">' ..
		mw.language.getContentLanguage():formatNum(total) .. '</span></span>'
	-- No whitespace between the containers and their children: a newline there
	-- makes MediaWiki wrap the run in a <p>, which then becomes the only flex child.
	return '<div class="topic-biblio-controls"><div class="topic-biblio-filter">' ..
		frame:preprocess('{{#widget:filterInput|placeholder=' .. (trim(args.placeholder) or 'Filter the library...') .. '}}') ..
		'</div><div class="topic-biblio-pills">' .. all .. table.concat(pills) .. '</div></div>'
end

function p.section(frame)
	local args = frame.args
	local query = trim(args.query)
	if not query then return '' end
	local heading = trim(args.heading)
	local link = trim(args.link)
	local sort = trim(args.sort) or 'Citation'
	local order = trim(args.order) or 'asc'
	-- title=yes: each row leads with the work's title above the citation.
	local userparam = (trim(args.title) == 'yes') and '|userparam=title' or ''

	local n = count(frame, query)
	if n == 0 then return '' end

	local out = { '<div class="topic-biblio-section">' }
	if heading then
		local label = link and ('[[' .. link .. '|' .. heading .. ']]') or heading
		table.insert(out, '<h2 class="link-no-border"><span class="rel-count">' ..
			mw.language.getContentLanguage():formatNum(n) .. '</span> ' .. label .. '</h2>')
	end
	table.insert(out, '<div class="topic-biblio-list">')
	for offset = 0, n - 1, CHUNK do
		table.insert(out, frame:preprocess('{{#ask:' .. query .. PRINTOUTS ..
			'|format=template|template=TopicBiblioRow|named args=yes|link=none' ..
			'|sort=' .. sort .. '|order=' .. order ..
			userparam ..
			'|limit=' .. CHUNK .. '|offset=' .. offset ..
			-- Each deferred chunk is parsed on its own, so the page's __NOGLOSSARY__
			-- would not reach it: the glossary would mark up every chunk (~2,300
			-- terms across the Library). A wrapper whose class is exactly
			-- "noglossary" keeps it out: measured 800 -> 440 ms and 388 -> 285 KB
			-- for a chunk of 100 rows.
			'|intro=<div class="noglossary">|outro=</div>' ..
			'|searchlabel=|default=|@deferred}}'))
	end
	table.insert(out, '</div></div>')
	return table.concat(out)
end

return p