Module:SscCitations: Difference between revisions

From Bodhicitta
(Bounded id match, self-citation guard, cache the query)
(VSCode edit)
Line 75: Line 75:
'?TranslationChapter#=TranslationChapter',
'?TranslationChapter#=TranslationChapter',
'?TranslationPageNumber#=TranslationPageNumber',
'?TranslationPageNumber#=TranslationPageNumber',
'?SourcePageNumber#=SourcePageNumber',
'?SegmentTranslation#=SegmentTranslation',
'?SegmentTranslation#=SegmentTranslation',
limit = SEGMENT_LIMIT
limit = SEGMENT_LIMIT
Line 93: Line 94:
page    = firstValue(row.Page),
page    = firstValue(row.Page),
chapter = firstValue(row.TranslationChapter),
chapter = firstValue(row.TranslationChapter),
folio  = firstValue(row.SourcePageNumber),
pageNum = firstValue(row.TranslationPageNumber),
pageNum = firstValue(row.TranslationPageNumber),
words  = words,
words  = words,
Line 121: Line 123:
lastOrder = seg.order,
lastOrder = seg.order,
chapter  = seg.chapter,
chapter  = seg.chapter,
folio    = seg.folio,
pageNum  = seg.pageNum,
pageNum  = seg.pageNum,
words    = seg.words,
words    = seg.words,

Revision as of 17:26, 25 August 2026

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

local p = {}

-- Citations of a text in the Śikṣāsamuccaya.
--
-- The SSC is segmented and aligned in the Translation Memory namespace, one page
-- per segment. A segment that quotes another work carries SegmentFormat=Quote
-- and a QuoteSource naming the work, usually with its RKTS catalogue number:
--
--   QuoteSource=Ratnameghasūtra (RKTSK 231)
--
-- Text pages hold the same identifier in DrlPageName ("RKTSK 231"), so that is
-- the join: no title matching, unaffected by spelling, and it keeps Kangyur
-- (RKTSK) and Tengyur (RKTST) texts distinct. 60 of the 63 works the SSC quotes
-- have a page here, covering 92% of quote segments.
--
-- THE POINT OF THIS MODULE: one quotation is often split across several
-- consecutive segments. Counting segments therefore overstates the citations
-- badly — the Pitāputrasamāgamanasūtra has 62 quote segments but is quoted 14
-- times, and one run is 36 segments long. Across the SSC, 633 segments are 345
-- quotations. SMW has no notion of "consecutive", so the grouping is done here.

-- The SSC page itself. Its own segments quote it 83 times through internal
-- cross-references, which is an artefact of the alignment rather than a
-- citation of one work by another, so the block is suppressed there.
local SSC_PAGE = 'Texts/Śikṣāsamuccaya'

local SSC_PREFIX = '~Translation Memory:016-Tsadra-SSC-Root/*'
local SEGMENT_LIMIT = 1000

-- A quotation past this many words of translated text is flagged as long.
-- Word count, not segment count: segments vary wildly in size, and the single
-- longest quotation in the corpus (Ratnolkādhāraṇī, 8,330 words) is ONE
-- segment, so a segment count would have called it small. The median quotation
-- is 139 words and the 75th percentile 218, so 400 is clearly beyond normal
-- without being so rare it never shows.
local LONG_QUOTE_WORDS = 400

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

-- The RKTS id for this page, e.g. "RKTSK 231". Absent on anything that is not a
-- canonical text, which is what keeps the feature off every other page without
-- needing a flag.
local function rktsId(frame)
	local title = mw.title.getCurrentTitle().prefixedText
	if title == SSC_PAGE then return nil end
	local query = mw.smw.ask{
		'[[' .. title .. ']]',
		'?DrlPageName#=DrlPageName',
		limit = 1
	}
	if not query or not query[1] then return nil end
	local id = firstValue(query[1].DrlPageName)
	if type(id) ~= 'string' then return nil end
	id = mw.text.trim(id)
	if id:match('^RKTS[KT]%s') then return id end
	return nil
end

-- Every quote segment naming this id, grouped into quotations.
local function fetchQuotations(id)
	-- Matched with a wildcard rather than an exact value: QuoteSource holds the
	-- whole label, "Ratnameghasūtra (RKTSK 231)", not the bare id.
	--
	-- The trailing ")" is essential. Without it "RKTSK 12" also matches
	-- "RKTSK 127" and "RKTSK 129", which turned the Aṣṭasāhasrikā's 13
	-- quotations into 39. Every one of the 65 QuoteSource values that carries an
	-- id closes the parenthesis immediately after it, so this is a safe anchor.
	local query = mw.smw.ask{
		'[[' .. SSC_PREFIX .. ']][[QuoteSource::~*' .. id .. ')*]]',
		'?#-=Page',
		'?SegmentOrder#=SegmentOrder',
		'?TranslationChapter#=TranslationChapter',
		'?TranslationPageNumber#=TranslationPageNumber',
		'?SourcePageNumber#=SourcePageNumber',
		'?SegmentTranslation#=SegmentTranslation',
		limit = SEGMENT_LIMIT
	}
	if not query then return {} end

	local segments = {}
	for _, row in ipairs(query) do
		local order = tonumber(firstValue(row.SegmentOrder))
		if order then
			local text = firstValue(row.SegmentTranslation)
			local words = 0
			if type(text) == 'string' then
				for _ in text:gmatch('%S+') do words = words + 1 end
			end
			table.insert(segments, {
				order   = order,
				page    = firstValue(row.Page),
				chapter = firstValue(row.TranslationChapter),
				folio   = firstValue(row.SourcePageNumber),
				pageNum = firstValue(row.TranslationPageNumber),
				words   = words,
			})
		end
	end

	table.sort(segments, function(a, b) return a.order < b.order end)

	-- Collapse runs of consecutive SegmentOrder into one quotation, anchored to
	-- the lowest order so a link lands at the start of the passage rather than
	-- somewhere in its middle.
	--
	-- A chapter change breaks a run: two adjacent quotes from the same work in
	-- different chapters of the SSC are two quotations. No run in the current
	-- data actually crosses a chapter boundary, but the rule is correct and
	-- costs nothing.
	local quotations = {}
	for _, seg in ipairs(segments) do
		local last = quotations[#quotations]
		if last and seg.order == last.lastOrder + 1 and seg.chapter == last.chapter then
			last.lastOrder = seg.order
			last.words = last.words + seg.words
		else
			table.insert(quotations, {
				page      = seg.page,
				order     = seg.order,
				lastOrder = seg.order,
				chapter   = seg.chapter,
				folio     = seg.folio,
				pageNum   = seg.pageNum,
				words     = seg.words,
			})
		end
	end

	return quotations
end

-- Number of distinct quotations, for the button label and for deciding whether
-- to render anything at all.
-- The template asks for the count to decide whether to render, then for the
-- list. Cached so that is one query per page rather than three.
local cache = nil
local function quotations(frame)
	if cache == nil then
		local id = rktsId(frame)
		cache = id and fetchQuotations(id) or {}
	end
	return cache
end

function p.count(frame)
	local list = quotations(frame)
	if #list == 0 then return '' end
	return tostring(#list)
end

-- The list itself.
function p.list(frame)
	local list = quotations(frame)
	if #list == 0 then return '' end

	local out = {'<div class="ssc-citations">'}
	for i, q in ipairs(list) do
		table.insert(out, '<div class="ssc-citation">')
		table.insert(out, '<span class="ssc-citation-index">' .. i .. '</span>')
		table.insert(out, '<span class="ssc-citation-body">')
		if q.chapter and q.chapter ~= '' then
			table.insert(out, '<span class="ssc-citation-chapter">Chapter ' ..
				tostring(q.chapter) .. '</span>')
		end
		if q.pageNum and q.pageNum ~= '' then
			table.insert(out, '<span class="ssc-citation-page">p.&nbsp;' ..
				tostring(q.pageNum) .. '</span>')
		end
		if q.words > LONG_QUOTE_WORDS then
			table.insert(out, '<span class="ssc-citation-long">long quotation</span>')
		end
		table.insert(out, '</span>')
		if q.page then
			table.insert(out, '<span class="ssc-citation-link">[[' .. q.page ..
				'|Read the passage]]</span>')
		end
		table.insert(out, '</div>')
	end
	table.insert(out, '</div>')

	return frame:preprocess(table.concat(out))
end

return p