Jump to content
Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Reads the fields of {{Term attestation}}, shows the row and stores its subobject. String functions are not available on this wiki, so the checks are made here.

Functions

{{#invoke:Term attestation|row}}
Called by the template, which passes source_title and source_display: the source page's Property:Has research title and its display title, looked up with #show unless the row gives source_label. The module reads the template's other fields itself. Returns one table row, with an anchor term- followed by the row's id, and stores one subobject of the same name, linked back to the page by Property:Has term page. A direct #invoke may give the fields itself.

Checks

  • Not stored when id is not lower-case letters, digits and hyphens; term, language, denotes, source, ref or locator is empty; language_code is not three lower-case letters; or source holds markup. The row shows each error and says that it is not stored.
  • Stored without the field when a transliteration has no system; a concept or place holds markup; the coordinates are not decimal degrees or have no place; the year is not a single year (read by Module:Timeline); circa has no year; or derived_from is not Page#id or lacks any of etymology_source, etymology_ref and etymology_locator. The row shows the error beside the field.
  • Values outside the allowed lists of Property:Has term denotes and Property:Has transliteration system are refused by the store, which marks them with a warning.
  • The Source column links the source page under source_label, else the research title, else the display title, else the page name. The label is display only.
  • Coordinates are rounded to two decimal places, as the timeline and waypoint subobjects round theirs. Bold and italic marks are removed from stored text.

-- {{Term attestation}}: one row of a table of recorded terms, and the term subobject behind it.
-- A term attestation records that one source, at one place in it, gives a word in a language with a meaning,
-- for a place and a year. It is stored only when its source, citation and locator are all given.
local p = {}

local Timeline = require('Module:Timeline')

local E = {
	id = 'Give an id of lower-case letters, digits and hyphens, unique on the page',
	term = 'Give the term as the source writes it',
	language = 'Give the language by name',
	code = 'Give the language as a three-letter ISO 639-3 code, for example ben; und when the source does not say',
	denotes = 'Say what the term denotes',
	source = 'Give the page of the source',
	ref = 'Give the name of the citation of the source on this page',
	locator = 'Give where in the source: page, answer or verse, and the words quoted',
	system = 'Say which spelling system the transliteration follows',
	name = 'Give the bare page title: the template makes the link',
	year = 'Give a single year, negative before the Common Era',
	circa = 'A circa mark needs a year',
	coord = 'Coordinates must be decimal degrees, for example 26.85, 80.95',
	nowhere = 'Coordinates need a place',
	derived = 'Give the attestation it derives from as Page#id',
	etymology = 'A derivation needs the source that states it: etymology_source, etymology_ref and etymology_locator',
}
local NOT_STORED = 'Not stored until the errors in this row are fixed'

local function err(key)
	return '<span class="error">' .. E[key] .. '</span>'
end

local function trim(s)
	return mw.text.trim(s or '')
end

-- Plain text for the store: the bold and italic quote marks are display only.
local function plain(s)
	return (s:gsub("'''''", ''):gsub("'''", ''):gsub("''", ''))
end

-- A bare page title, or nil and the error key when the field holds markup.
local function title(s)
	s = trim(s)
	if s == '' then
		return nil
	end
	if s:find('[%[%]{}|<>]') then
		return nil, 'name'
	end
	return s
end

local function list(s)
	local out = {}
	for _, part in ipairs(mw.text.split(s or '', ';', true)) do
		part = trim(part)
		if part ~= '' then
			table.insert(out, part)
		end
	end
	return out
end

-- Two decimal places, as the timeline and waypoint subobjects round their points.
local function round2(x)
	local r = math.floor(math.abs(x) * 100 + 0.5) / 100
	if x < 0 then
		r = -r
	end
	return r
end

local function showYear(y, circa)
	local s = (y < 0) and ((-y) .. ' BCE') or tostring(y)
	if circa then
		s = 'c. ' .. s
	end
	return s
end

local function yes(s)
	s = trim(s):lower()
	return s == 'yes' or s == 'y' or s == 'true' or s == '1'
end

-- Read and check every field. Returns the record and the list of error keys, each tagged with the cell it
-- belongs to; `store` is false when a required field is missing or wrong.
function p._read(a, pagename)
	local r, errs, store = {}, {}, true
	local function bad(key, cell, blocks)
		table.insert(errs, {key = key, cell = cell})
		if blocks then
			store = false
		end
	end

	r.id = trim(a.id)
	if not r.id:match('^[a-z0-9][a-z0-9%-]*$') then
		bad('id', 'term', true)
	end
	r.term = trim(a.term)
	if r.term == '' then
		bad('term', 'term', true)
	end
	r.translit = trim(a.transliteration)
	r.system = trim(a.system)
	if r.translit ~= '' and r.system == '' then
		bad('system', 'term', false)
	end
	r.language = trim(a.language)
	if r.language == '' then
		bad('language', 'language', true)
	end
	r.code = trim(a.language_code)
	if not r.code:match('^%l%l%l$') then
		bad('code', 'language', true)
	end
	r.denotes = list(a.denotes)
	if #r.denotes == 0 then
		bad('denotes', 'meaning', true)
	end
	r.gloss = trim(a.gloss)
	local e
	r.concept, e = title(a.concept)
	if e then
		bad(e, 'meaning', false)
	end
	r.places = {}
	for _, s in ipairs(list(a.place)) do
		local t, pe = title(s)
		if t then
			table.insert(r.places, t)
		else
			bad(pe, 'place', false)
		end
	end
	local cs = trim(a.coordinates)
	if cs ~= '' then
		local lat, lon = cs:match('^(%-?%d+%.?%d*)%s*,%s*(%-?%d+%.?%d*)$')
		lat, lon = tonumber(lat), tonumber(lon)
		if not (lat and lon and math.abs(lat) <= 90 and math.abs(lon) <= 180) then
			bad('coord', 'place', false)
		elseif #r.places == 0 then
			bad('nowhere', 'place', false)
		else
			r.coords = round2(lat) .. ',' .. round2(lon)
		end
	end
	local ys = trim(a.year)
	r.circa = yes(a.circa)
	if ys ~= '' then
		r.year = Timeline._year(ys)
		if not r.year or r.year == 0 then
			r.year = nil
			bad('year', 'year', false)
		end
	elseif r.circa then
		bad('circa', 'year', false)
	end
	r.source, e = title(a.source)
	if e then
		bad(e, 'source', true)
	elseif not r.source then
		bad('source', 'source', true)
	end
	r.ref = trim(a.ref)
	if r.ref == '' or r.ref:find('[<>"|]') then
		bad('ref', 'source', true)
	end
	r.locator = trim(a.locator)
	if r.locator == '' then
		bad('locator', 'source', true)
	end
	r.cite = trim(a.cite) ~= 'no'

	-- An optional derivation, stored only with the source that states it.
	local ds = trim(a.derived_from)
	if ds ~= '' then
		local dpage, did = ds:match('^(.-)#([a-z0-9][a-z0-9%-]*)$')
		local dt
		if dpage then
			dt, e = title(dpage)
		end
		if not dpage or e then
			bad('derived', 'term', false)
		else
			r.derived = {page = dt or pagename, id = did}
			r.etymon = trim(a.etymon)
			r.etym_source, e = title(a.etymology_source)
			r.etym_ref = trim(a.etymology_ref)
			r.etym_locator = trim(a.etymology_locator)
			if e or not r.etym_source or r.etym_ref == '' or r.etym_ref:find('[<>"|]') or r.etym_locator == '' then
				r.derived = nil
				bad('etymology', 'term', false)
			end
		end
	end
	return r, errs, store
end

-- The values of the subobject, in order, as "Property=value" strings.
function p._values(r, pagename)
	local v = {'@linkWith=Has term page'}
	local function add(prop, val)
		if val and val ~= '' then
			table.insert(v, prop .. '=' .. val)
		end
	end
	add('Has term', plain(r.term))
	if r.system ~= '' then
		add('Has term transliteration', plain(r.translit))
	end
	add('Has transliteration system', r.system)
	add('Has term language', r.language)
	add('Has term language code', r.code)
	for _, d in ipairs(r.denotes) do
		add('Has term denotes', d)
	end
	add('Has term gloss', plain(r.gloss))
	add('Has term concept', r.concept)
	for _, pl in ipairs(r.places) do
		add('Has term place', pl)
	end
	add('Has coordinates', r.coords)
	if r.year then
		add('Has term year', tostring(r.year))
		add('Has term year circa', r.circa and 'true' or 'false')
	end
	add('Has term source', r.source)
	add('Has term ref name', r.ref)
	add('Has term locator', plain(r.locator))
	if r.derived then
		add('Has term derived from', r.derived.page .. '#term-' .. r.derived.id)
		add('Has etymology source', r.etym_source)
		add('Has etymology ref name', r.etym_ref)
		add('Has etymology locator', plain(r.etym_locator))
	end
	return v
end

local function cite(frame, name)
	return frame:extensionTag('ref', '', {name = name})
end

-- A label for a link: plain text, entities decoded, no characters that would break the link.
local function label(s)
	s = mw.text.trim(mw.text.decode(s or ''))
	s = s:gsub('[%[%]{}|<>]', ''):gsub('%s+', ' ')
	return s
end

-- The words the Source column shows for the source page: `source_label` when the row gives one, else the
-- title stored on the source page (Has research title, looked up by the template), else its display title
-- (Display title of), else the page name itself. Display only: the stored value is the page.
function p._sourceText(source, override, title, display)
	local choices = {override, title, display}
	for i = 1, 3 do
		local s = label(choices[i])
		if s ~= '' then
			if s == source then
				return '[[' .. source .. ']]'
			end
			return '[[' .. source .. '|' .. s .. ']]'
		end
	end
	return '[[' .. source .. ']]'
end

-- {{#invoke:Term attestation|row}}, called from {{Term attestation}}: the table row and the subobject.
-- The fields are the template's; a direct #invoke may give them itself (used to test the module unsaved).
function p.row(frame)
	local parent = frame:getParent()
	local a = frame.args
	if parent and (parent.args.id or parent.args.term or parent.args.source) then
		a = parent.args
	end
	local pagename = mw.title.getCurrentTitle().prefixedText
	local r, errs, store = p._read(a, pagename)

	local notes = {term = {}, language = {}, meaning = {}, place = {}, year = {}, source = {}}
	for _, x in ipairs(errs) do
		table.insert(notes[x.cell], err(x.key))
	end
	local function cell(name, s)
		if #notes[name] > 0 then
			s = trim(s .. ' ' .. table.concat(notes[name], ' '))
		end
		return s
	end

	-- Term: the word as the source writes it, its transliteration, and a derivation if any.
	local t = r.term ~= '' and r.term or ''
	if r.translit ~= '' then
		local tr = "''" .. r.translit .. "''"
		if r.system ~= '' then
			tr = '<span title="' .. mw.text.encode(r.system) .. '">' .. tr .. '</span>'
		end
		t = t .. '<br />' .. tr
	end
	if r.derived then
		local here = r.derived.page == pagename
		local target = (here and '' or r.derived.page) .. '#term-' .. r.derived.id
		local label = r.etymon ~= '' and ("''" .. r.etymon .. "''") or 'an earlier form'
		local via = here and '' or (' on [[' .. r.derived.page .. ']]')
		t = t .. '<br /><small>From [[' .. target .. '|' .. label .. ']]' .. via .. ', per [[' ..
			r.etym_source .. ']], ' .. r.etym_locator .. (r.cite and cite(frame, r.etym_ref) or '') .. '</small>'
	end

	local lang = r.language
	if r.code ~= '' then
		lang = lang .. ' <small>(' .. mw.text.nowiki(r.code) .. ')</small>'
	end

	local meaning = r.gloss
	local kinds = table.concat(r.denotes, ', ')
	if r.concept then
		kinds = (kinds ~= '' and (kinds .. ': ') or '') .. '[[' .. r.concept .. ']]'
	end
	if kinds ~= '' then
		meaning = (meaning ~= '' and (meaning .. '<br />') or '') .. '<small>' .. kinds .. '</small>'
	end

	local places = {}
	for _, pl in ipairs(r.places) do
		table.insert(places, '[[' .. pl .. ']]')
	end

	local year, sortkey = '', ''
	if r.year then
		year = showYear(r.year, r.circa)
		sortkey = 'data-sort-value="' .. r.year .. '" | '
	end

	local src = ''
	if r.source then
		src = p._sourceText(r.source, a.source_label, frame.args.source_title, frame.args.source_display)
		if r.locator ~= '' then
			src = src .. ', ' .. r.locator
		end
		if r.cite and r.ref ~= '' and not r.ref:find('[<>"|]') then
			src = src .. cite(frame, r.ref)
		end
	end
	if not store then
		table.insert(notes.source, '<span class="error">' .. NOT_STORED .. '</span>')
	end

	local anchor = r.id:match('^[a-z0-9][a-z0-9%-]*$') and (' id="term-' .. r.id .. '"') or ''
	local out = {
		'|-' .. anchor,
		'| ' .. cell('term', t),
		'| ' .. cell('language', lang),
		'| ' .. cell('meaning', meaning),
		'| ' .. cell('place', table.concat(places, ', ')),
		'| ' .. sortkey .. cell('year', year),
		'| ' .. cell('source', src),
	}
	local row = table.concat(out, '\n')
	if store then
		local args = {'term-' .. r.id}
		for _, x in ipairs(p._values(r, pagename)) do
			table.insert(args, x)
		end
		row = row .. frame:callParserFunction('#subobject', args)
	end
	return row
end

return p