Module:SscCitedWorks: Difference between revisions

From Bodhicitta
(List of works quoted in the Śikṣāsamuccaya, counting quotations not segments)
(Count ContainsQuote segments as well as Quote ones)
 
(7 intermediate revisions by the same user not shown)
Line 6: Line 6:
-- The join is the RKTS catalogue number. A quote segment records its source as
-- The join is the RKTS catalogue number. A quote segment records its source as
-- "Ratnameghasūtra (RKTSK 231)", and the text page holds the same id in
-- "Ratnameghasūtra (RKTSK 231)", and the text page holds the same id in
-- DrlPageName, so no title matching is involved.
-- the page itself, since QuoteSource is now a page property.
--
--
-- COUNTS ARE QUOTATIONS, NOT SEGMENTS. One quotation is often split across
-- COUNTS ARE QUOTATIONS, NOT SEGMENTS. One quotation is often split across
Line 27: Line 27:
end
end


-- Quotation counts keyed by RKTS id.
-- Quotation counts keyed by the cited page.
local function quotationsById()
--
-- Selected on SegmentFormat, NOT on [[QuoteSource::+]].
--
-- The wildcard on QuoteSource under-reports here: a segment whose value was
-- retargeted by a new redirect stays invisible to it until something re-stores
-- that segment. Measured after a curator added redirects in Sep 2026, it saw
-- 594 of the SSC's 633 quote segments and this list silently lost 13 works.
--
-- SegmentFormat is a plain text value the wildcard handles correctly, so the
-- segment set is taken from it and QuoteSource is read from the printout —
-- segments with no source are skipped in the loop below.
local function quotationsByPage()
local query = mw.smw.ask{
local query = mw.smw.ask{
'[[' .. SSC_PREFIX .. ']][[QuoteSource::+]]',
-- Quote segments AND commentary segments that embed one (ContainsQuote).
'?QuoteSource#=QuoteSource',
-- Both carry a QuoteSource, and the panel on the cited work shows both, so
-- this list must count both or the two pages disagree.
'[[' .. SSC_PREFIX .. ']][[SegmentFormat::Quote||Verse inside a quote]] OR [[' ..
SSC_PREFIX .. ']][[ContainsQuote::true]]',
-- '#-' gives the bare page name: plain ?QuoteSource returns the display
-- title (no 'Texts/'), and '#' inserts a space after the namespace.
'?QuoteSource#-=QuoteSource',
'?SegmentOrder#=SegmentOrder',
'?SegmentOrder#=SegmentOrder',
'?TranslationChapter#=TranslationChapter',
'?TranslationChapter#=TranslationChapter',
Line 38: Line 55:
if not query then return {} end
if not query then return {} end


local segments = {}
local bySource = {}
for _, row in ipairs(query) do
for _, row in ipairs(query) do
local order = tonumber(firstValue(row.SegmentOrder))
local order = tonumber(firstValue(row.SegmentOrder))
local source = firstValue(row.QuoteSource)
if order then
if order and type(source) == 'string' then
local chapter = tostring(firstValue(row.TranslationChapter) or '')
table.insert(segments, {
-- A segment can name SEVERAL works when a passage is shared between
order  = order,
-- them, so every value counts towards its own work. Taking only the
source = source,
-- first credited the whole passage to one and left the other empty.
chapter = tostring(firstValue(row.TranslationChapter) or ''),
local sources = row.QuoteSource
})
if type(sources) ~= 'table' then sources = { sources } end
for _, source in ipairs(sources) do
-- The printout carries the page the value resolved to, redirects
-- followed, so counts key on the work itself rather than on
-- whichever alias a segment happened to use. The Samādhirājasūtra
-- is cited under five names and must count as one work.
local page = source
if type(page) == 'table' then page = page.fulltext end
if type(page) == 'string' then page = mw.text.trim(page) end
if type(page) == 'string' and page ~= '' then
bySource[page] = bySource[page] or {}
table.insert(bySource[page], { order = order, chapter = chapter })
end
end
end
end
end
end
table.sort(segments, function(a, b) return a.order < b.order end)


-- Runs are found PER SOURCE, not by walking the whole sequence. Two different
-- Runs are found PER SOURCE, not by walking the whole sequence. Two different
-- works quoted in adjacent segments are two quotations, and walking the
-- works quoted in adjacent segments are two quotations, and walking the
-- combined list makes each one look like it interrupts the other. Grouping
-- combined list makes each look like it interrupts the other. Grouping within
-- within a source also matches Module:SscCitations, whose per-page counts are
-- a source also matches Module:SscCitations, whose per-page counts are what a
-- what a reader sees on the text page itself — the two must agree.
-- reader sees on the text page itself — the two must agree.
local bySource = {}
for _, seg in ipairs(segments) do
-- Anchored on the closing parenthesis, the same boundary
-- Module:SscCitations uses. Without it "RKTSK 9" also matches
-- "RKTSK 96" and "RKTSK 99"; every QuoteSource that carries an id closes
-- the bracket immediately after it, so this is exact.
local canon, number = seg.source:match('(RKTS[KT])%s*([%d%-]+)%s*%)')
if canon then
local id = canon:upper() .. ' ' .. number
bySource[id] = bySource[id] or {}
table.insert(bySource[id], seg)
end
end
 
local counts = {}
local counts = {}
for id, list in pairs(bySource) do
for page, list in pairs(bySource) do
table.sort(list, function(a, b) return a.order < b.order end)
table.sort(list, function(a, b) return a.order < b.order end)
local n, previous = 0, nil
local n, previous = 0, nil
Line 82: Line 97:
previous = seg
previous = seg
end
end
counts[id] = n
counts[page] = n
end
end
return counts
return counts
Line 88: Line 103:


function p.main(frame)
function p.main(frame)
local counts = quotationsById()
local counts = quotationsByPage()


local ids = {}
local names = {}
for id in pairs(counts) do table.insert(ids, id) end
for page in pairs(counts) do table.insert(names, page) end
if #ids == 0 then return '' end
if #names == 0 then return '' end
table.sort(ids)
table.sort(names)


-- One query for every quoted id, rather than one per work.
-- One query for every cited page, rather than one per work. Values repeat
-- the page name only, per SMW's OR syntax: [[P::a||b||c]]. Repeating the
-- property ([[P::a||P::b]]) silently returns almost nothing.
local pages = mw.smw.ask{
local pages = mw.smw.ask{
'[[DrlPageName::' .. table.concat(ids, '||') .. ']]',
'[[' .. table.concat(names, '||') .. ']]',
'?#-=Page',
'?#-=Page',
'?DrlPageName#=DrlPageName',
'?Display title of=Title',
'?Display title of=Title',
'?FullTitleTib=FullTitleTib',
'?FullTitleTib=FullTitleTib',
Line 111: Line 127:
for _, row in ipairs(pages) do
for _, row in ipairs(pages) do
local page = firstValue(row.Page)
local page = firstValue(row.Page)
local id = firstValue(row.DrlPageName)
-- counts is keyed by the resolved page (see quotationsByPage), which is
if page and page ~= SSC_PAGE and id and counts[id] then
-- what this query returns, so the two line up directly.
local n = page and counts[page]
if page and page ~= SSC_PAGE and n then
local label = firstValue(row.Title)
local label = firstValue(row.Title)
if not label or label == '' then label = page:gsub('^Texts/', '') end
if not label or label == '' then label = page:gsub('^Texts/', '') end
Line 121: Line 139:
trans = firstValue(row.FullTitleTrans),
trans = firstValue(row.FullTitleTrans),
class = firstValue(row.Classification),
class = firstValue(row.Classification),
count = counts[id],
count = n,
})
})
end
end
Line 138: Line 156:


table.insert(out, '<div class="row filterable">')
table.insert(out, '<div class="row filterable">')
-- The vajra sits ON the border between the two columns (right: -26px on a
-- position-relative parent), tying them together. Desktop only.
table.insert(out, '<div class="col-md-3 col-lg-2 pr-md-5 pt-md-1 pb-1 text-center ' ..
table.insert(out, '<div class="col-md-3 col-lg-2 pr-md-5 pt-md-1 pb-1 text-center ' ..
'text-md-right border-right timeline-border font-serif position-relative">' ..
'text-md-right border-right timeline-border font-serif position-relative">' ..
'<div class="ssc-cited-vajra d-none d-md-block position-absolute">' ..
'[[File:Red Horizontal Vajra.png|50px|link=|]]</div>' ..
'<span class="ssc-cited-count">' .. w.count .. '</span>' ..
'<span class="ssc-cited-count">' .. w.count .. '</span>' ..
'<span class="ssc-cited-label">' ..
'<span class="ssc-cited-label">' ..

Latest revision as of 14:03, 22 September 2026

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

local p = {}

-- The list of works quoted in the Śikṣāsamuccaya, generated from the aligned
-- translation memory.
--
-- The join is the RKTS catalogue number. A quote segment records its source as
-- "Ratnameghasūtra (RKTSK 231)", and the text page holds the same id in
-- the page itself, since QuoteSource is now a page property.
--
-- COUNTS ARE QUOTATIONS, NOT SEGMENTS. One quotation is often split across
-- consecutive segments, so counting segments overstates badly: the
-- Pitāputrasamāgamanasūtra has 62 quote segments but is quoted 14 times, and one
-- of its runs is 36 segments long. Across the SSC, 633 segments are 345
-- quotations. SMW cannot express "consecutive", so the grouping happens here.
--
-- A chapter change breaks a run: two adjacent quotes from one work in different
-- chapters are two quotations. No run in the current data crosses a chapter
-- boundary, but the rule is correct and costs nothing.

local SSC_PREFIX = '~Translation Memory:016-Tsadra-SSC-Root/*'
local SSC_PAGE = 'Texts/Śikṣāsamuccaya'
local SEGMENT_LIMIT = 5000

local function firstValue(v)
	if type(v) == 'table' then return v[1] end
	return v
end

-- Quotation counts keyed by the cited page.
--
-- Selected on SegmentFormat, NOT on [[QuoteSource::+]].
--
-- The wildcard on QuoteSource under-reports here: a segment whose value was
-- retargeted by a new redirect stays invisible to it until something re-stores
-- that segment. Measured after a curator added redirects in Sep 2026, it saw
-- 594 of the SSC's 633 quote segments and this list silently lost 13 works.
--
-- SegmentFormat is a plain text value the wildcard handles correctly, so the
-- segment set is taken from it and QuoteSource is read from the printout —
-- segments with no source are skipped in the loop below.
local function quotationsByPage()
	local query = mw.smw.ask{
		-- Quote segments AND commentary segments that embed one (ContainsQuote).
		-- Both carry a QuoteSource, and the panel on the cited work shows both, so
		-- this list must count both or the two pages disagree.
		'[[' .. SSC_PREFIX .. ']][[SegmentFormat::Quote||Verse inside a quote]] OR [[' ..
			SSC_PREFIX .. ']][[ContainsQuote::true]]',
		-- '#-' gives the bare page name: plain ?QuoteSource returns the display
		-- title (no 'Texts/'), and '#' inserts a space after the namespace.
		'?QuoteSource#-=QuoteSource',
		'?SegmentOrder#=SegmentOrder',
		'?TranslationChapter#=TranslationChapter',
		limit = SEGMENT_LIMIT
	}
	if not query then return {} end

	local bySource = {}
	for _, row in ipairs(query) do
		local order = tonumber(firstValue(row.SegmentOrder))
		if order then
			local chapter = tostring(firstValue(row.TranslationChapter) or '')
			-- A segment can name SEVERAL works when a passage is shared between
			-- them, so every value counts towards its own work. Taking only the
			-- first credited the whole passage to one and left the other empty.
			local sources = row.QuoteSource
			if type(sources) ~= 'table' then sources = { sources } end
			for _, source in ipairs(sources) do
				-- The printout carries the page the value resolved to, redirects
				-- followed, so counts key on the work itself rather than on
				-- whichever alias a segment happened to use. The Samādhirājasūtra
				-- is cited under five names and must count as one work.
				local page = source
				if type(page) == 'table' then page = page.fulltext end
				if type(page) == 'string' then page = mw.text.trim(page) end
				if type(page) == 'string' and page ~= '' then
					bySource[page] = bySource[page] or {}
					table.insert(bySource[page], { order = order, chapter = chapter })
				end
			end
		end
	end

	-- Runs are found PER SOURCE, not by walking the whole sequence. Two different
	-- works quoted in adjacent segments are two quotations, and walking the
	-- combined list makes each look like it interrupts the other. Grouping within
	-- a source also matches Module:SscCitations, whose per-page counts are what a
	-- reader sees on the text page itself — the two must agree.
	local counts = {}
	for page, list in pairs(bySource) do
		table.sort(list, function(a, b) return a.order < b.order end)
		local n, previous = 0, nil
		for _, seg in ipairs(list) do
			local continues = previous
				and seg.chapter == previous.chapter
				and seg.order == previous.order + 1
			if not continues then n = n + 1 end
			previous = seg
		end
		counts[page] = n
	end
	return counts
end

function p.main(frame)
	local counts = quotationsByPage()

	local names = {}
	for page in pairs(counts) do table.insert(names, page) end
	if #names == 0 then return '' end
	table.sort(names)

	-- One query for every cited page, rather than one per work. Values repeat
	-- the page name only, per SMW's OR syntax: [[P::a||b||c]]. Repeating the
	-- property ([[P::a||P::b]]) silently returns almost nothing.
	local pages = mw.smw.ask{
		'[[' .. table.concat(names, '||') .. ']]',
		'?#-=Page',
		'?Display title of=Title',
		'?FullTitleTib=FullTitleTib',
		'?Fulltitletrans=FullTitleTrans',
		'?Classification=Classification',
		limit = 500
	}
	if not pages then return '' end

	local works = {}
	for _, row in ipairs(pages) do
		local page = firstValue(row.Page)
		-- counts is keyed by the resolved page (see quotationsByPage), which is
		-- what this query returns, so the two line up directly.
		local n = page and counts[page]
		if page and page ~= SSC_PAGE and n then
			local label = firstValue(row.Title)
			if not label or label == '' then label = page:gsub('^Texts/', '') end
			table.insert(works, {
				page  = page,
				label = label,
				tib   = firstValue(row.FullTitleTib),
				trans = firstValue(row.FullTitleTrans),
				class = firstValue(row.Classification),
				count = n,
			})
		end
	end
	if #works == 0 then return '' end

	-- Alphabetical by title, matching the hand-written page it replaces. Sorting
	-- by count would bury the sūtras quoted once among the busiest works.
	table.sort(works, function(a, b) return a.label < b.label end)

	local out = {}
	for _, w in ipairs(works) do
		local author = {}
		if w.tib and w.tib ~= '' then table.insert(author, 'Tib. ' .. w.tib) end
		if w.trans and w.trans ~= '' then table.insert(author, 'Eng. ' .. w.trans) end

		table.insert(out, '<div class="row filterable">')
		-- The vajra sits ON the border between the two columns (right: -26px on a
		-- position-relative parent), tying them together. Desktop only.
		table.insert(out, '<div class="col-md-3 col-lg-2 pr-md-5 pt-md-1 pb-1 text-center ' ..
			'text-md-right border-right timeline-border font-serif position-relative">' ..
			'<div class="ssc-cited-vajra d-none d-md-block position-absolute">' ..
			'[[File:Red Horizontal Vajra.png|50px|link=|]]</div>' ..
			'<span class="ssc-cited-count">' .. w.count .. '</span>' ..
			'<span class="ssc-cited-label">' ..
			(w.count == 1 and 'quotation' or 'quotations') .. '</span></div>')
		table.insert(out, frame:expandTemplate{
			title = 'TimelineTileHorizontalTranslations',
			args = {
				wrapperclasses = 'col-md-9 px-2 px-md-5 py-0 font-serif',
				texts = '[[' .. w.page .. '|' .. w.label .. ']]',
				author = table.concat(author, ' &nbsp;·&nbsp; '),
				description = w.class or '',
			}
		})
		table.insert(out, '</div>')
	end

	return frame:preprocess(table.concat(out, '\n'))
end

return p