Module:Dane

From WikiZagłębie

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

-- Moduł:Dane – wyświetlanie danych z dane.wikizaglebie.org w infoboksach.
--
-- Szablon infoboksu wywołuje {{#invoke:Dane|infobox|<nazwa szablonu>}}; moduł bierze parametry
-- z artykułu, uzupełnia puste wartościami z elementu powiązanego z artykułem i rozwija
-- „Szablon:<nazwa szablonu>/rdzeń” (dotychczasowa treść infoboksu).
--
-- Zasada: wartość wpisana w infoboksie ma pierwszeństwo. Z dane uzupełniany jest tylko parametr
-- oznaczony w mapie „uzupełniaj”: true, i tylko gdy żaden parametr tej samej właściwości
-- nie jest wypełniony (np. zdjęcie/grafika). Mapa: MediaWiki:WZBDataEditor-infobox-map.json
-- (ta sama, której używa zakładka „Edytuj dane”).

local p = {}

local MAP_PAGE = 'MediaWiki:WZBDataEditor-infobox-map.json'

local MONTHS = { 'stycznia', 'lutego', 'marca', 'kwietnia', 'maja', 'czerwca', 'lipca',
	'sierpnia', 'września', 'października', 'listopada', 'grudnia' }
local MONTHS_NOM = { 'styczeń', 'luty', 'marzec', 'kwiecień', 'maj', 'czerwiec', 'lipiec',
	'sierpień', 'wrzesień', 'październik', 'listopad', 'grudzień' }

-- jak EMPTY_VALUES w InfoboxReader.php
local EMPTY = {}
for _, v in ipairs( { '', '?', '??', '???', '-', '–', '—', 'brak', 'brak danych', 'b.d.', 'bd',
	'nieznany', 'nieznana', 'nieustalony', 'nieustalona', 'nie było', 'n/d', 'od', '...', '…' } ) do
	EMPTY[v] = true
end

local ustring = mw.ustring

-- Wikitekst → zwykły tekst (bez komentarzy, przypisów, nawiasów linków i pogrubień)
local function plain( s )
	s = ustring.gsub( s, '<!%-%-.-%-%->', '' )
	s = ustring.gsub( s, '<ref[^>]-/>', '' )
	s = ustring.gsub( s, '<ref[^>]*>.-</ref>', '' )
	s = ustring.gsub( s, '%[%[[^%]|]*|([^%]]*)%]%]', '%1' )
	s = ustring.gsub( s, '%[%[([^%]]*)%]%]', '%1' )
	s = ustring.gsub( s, "'''?", '' )
	return mw.text.trim( s )
end

-- Zaślepki z pustych infoboksów: xxx, ****, 000, 00,0, „xxx (gm. xxx)” (jak PLACEHOLDER_PATTERN)
local function isPlaceholder( t )
	if EMPTY[t] then
		return true
	end
	local core = mw.text.trim( ( ustring.gsub( t, '%s*%(.*%)$', '' ) ) )
	return ( ustring.match( core, '^xx+$' ) or ustring.match( core, '^%*%*+$' )
		or ustring.match( core, '^0+$' ) or ustring.match( core, '^0+[.,]0+$' )
		or ustring.match( core, '^00+%-000$' )
		or ustring.match( ' ' .. t .. ' ', '[^%w]xxx+[^%w]' ) ) ~= nil
end

local function normalizeFile( name )
	name = ustring.gsub( mw.text.trim( name ), '^[Pp]lik:', '' )
	name = ustring.gsub( name, '^[Ff]ile:', '' )
	return ustring.lower( ( ustring.gsub( name, '_', ' ' ) ) )
end

local function isEmpty( value, rule, skippedFiles )
	if value == nil then
		return true
	end
	local t = ustring.lower( plain( value ) )
	if isPlaceholder( t ) then
		return true
	end
	if rule['konwersja'] == 'plik' and skippedFiles[normalizeFile( t )] then
		return true
	end
	-- „pozycja = top” itp. – współrzędne bez cyfr
	return rule['konwersja'] == 'współrzędne' and not ustring.find( t, '%d' )
end

local function roman( n )
	local out = ''
	for _, pair in ipairs( { { 1000, 'M' }, { 900, 'CM' }, { 500, 'D' }, { 400, 'CD' }, { 100, 'C' },
		{ 90, 'XC' }, { 50, 'L' }, { 40, 'XL' }, { 10, 'X' }, { 9, 'IX' }, { 5, 'V' }, { 4, 'IV' }, { 1, 'I' } } ) do
		while n >= pair[1] do
			out = out .. pair[2]
			n = n - pair[1]
		end
	end
	return out
end

-- Data w stylu artykułów WikiZagłębia: [[11 stycznia]] [[1932]]
local function dateText( v )
	local y, mo, d = v.time:match( '^([+-]%d+)%-(%d%d)%-(%d%d)' )
	y, mo, d = tonumber( y ), tonumber( mo ), tonumber( d )
	if not y then
		return nil
	end
	local era = y < 0 and ' p.n.e.' or ''
	y = math.abs( y )
	local precision = v.precision or 11
	if precision >= 11 and mo > 0 and d > 0 then
		return '[[' .. d .. ' ' .. MONTHS[mo] .. ']] [[' .. y .. ']]' .. era
	elseif precision == 10 and mo > 0 then
		return MONTHS_NOM[mo] .. ' [[' .. y .. ']]' .. era
	elseif precision == 9 then
		return '[[' .. y .. ']]' .. era
	elseif precision == 8 then
		local decade = y - y % 10
		return 'lata ' .. decade .. '–' .. ( decade + 9 ) .. era
	elseif precision == 7 then
		return roman( math.floor( ( y - 1 ) / 100 ) + 1 ) .. ' wiek' .. era
	end
	return tostring( y ) .. era
end

local function numberText( v, rule )
	local n = tonumber( v.amount )
	if not n then
		return nil
	end
	local suffix = rule['dopisek'] or ''
	-- małe powierzchnie czytelniej w hektarach
	if suffix == ' km²' and math.abs( n ) < 1 then
		n, suffix = n * 100, ' ha'
	end
	n = math.floor( n * 1e6 + 0.5 ) / 1e6
	return mw.language.getContentLanguage():formatNum( n ) .. suffix
end

local function dms( x, pos, neg )
	local hemisphere = x < 0 and neg or pos
	x = math.abs( x )
	local deg = math.floor( x )
	local minutes = ( x - deg ) * 60
	local min = math.floor( minutes )
	local sec = math.floor( ( minutes - min ) * 60 + 0.5 )
	if sec == 60 then
		sec, min = 0, min + 1
	end
	if min == 60 then
		min, deg = 0, deg + 1
	end
	return string.format( '%d°%02d′%02d″%s', deg, min, sec, hemisphere )
end

-- Element: etykieta (bez części z „wzorca”, np. „gmina $1”) z linkiem do artykułu, jeśli jest
local function itemText( id, rule )
	local label = mw.wikibase.getLabel( id ) or id
	local shown = label
	-- „gmina $1” → „Będzin”; przy „wyrażenie” (np. „Linia kolejowa nr $1”) pełna nazwa
	local prefix, suffix = ( rule['wyrażenie'] and '' or rule['wzorzec'] or '' ):match( '^(.-)%$1(.*)$' )
	if prefix then
		local lower = ustring.lower( label )
		if prefix ~= '' and ustring.sub( lower, 1, ustring.len( prefix ) ) == ustring.lower( prefix ) then
			shown = ustring.sub( shown, ustring.len( prefix ) + 1 )
		end
		if suffix ~= '' and ustring.sub( lower, -ustring.len( suffix ) ) == ustring.lower( suffix ) then
			shown = ustring.sub( shown, 1, -ustring.len( suffix ) - 1 )
		end
		shown = mw.text.trim( shown )
	end
	local page = mw.wikibase.getSitelink( id )
	if rule['bez linku'] then
		-- szablon sam robi [[…]] – nazwa artykułu, gdy jest
		return page or shown
	end
	if page then
		return page == shown and ( '[[' .. page .. ']]' ) or ( '[[' .. page .. '|' .. shown .. ']]' )
	end
	return shown
end

local function snakText( snak, rule )
	if not snak or snak.snaktype ~= 'value' or not snak.datavalue then
		return nil
	end
	local v = snak.datavalue.value
	local valueType = snak.datavalue.type
	if valueType == 'wikibase-entityid' then
		return itemText( v.id or ( 'Q' .. v['numeric-id'] ), rule )
	elseif valueType == 'time' then
		return dateText( v )
	elseif valueType == 'quantity' then
		return numberText( v, rule )
	elseif valueType == 'globecoordinate' then
		return dms( v.latitude, 'N', 'S' ) .. ' ' .. dms( v.longitude, 'E', 'W' )
	elseif valueType == 'monolingualtext' then
		return v.text
	elseif valueType == 'string' then
		return v
	end
	return nil
end

-- Pierwsza wartość, a przy „wiele”: true wszystkie (np. zawody, kluby) rozdzielone „separatorem”;
-- przy „do” (zakres dat, np. okres budowy) – „początek–koniec”; przy „takNie” – „tak” albo nic
local function valueText( entityId, property, rule )
	local statements = mw.wikibase.getBestStatements( entityId, property )
	if rule['konwersja'] == 'takNie' then
		-- pole tak/nie („zabytek”): „tak”, gdy wśród wartości jest element z „wartość”
		for _, statement in ipairs( statements ) do
			local snak = statement.mainsnak
			if snak.snaktype == 'value' and snak.datavalue and snak.datavalue.value.id == rule['wartość'] then
				return 'tak'
			end
		end
		return nil
	end
	if rule['do'] then
		local endStatements = mw.wikibase.getBestStatements( entityId, rule['do'] )
		local startText = snakText( statements[1] and statements[1].mainsnak, rule )
		local endText = snakText( endStatements[1] and endStatements[1].mainsnak, rule )
		if startText and endText then
			return startText .. '–' .. endText
		end
		return startText or ( endText and ( 'do ' .. endText ) )
	end
	if not rule['wiele'] then
		return snakText( statements[1] and statements[1].mainsnak, rule )
	end
	local texts = {}
	for _, statement in ipairs( statements ) do
		local text = snakText( statement.mainsnak, rule )
		if text and text ~= '' then
			table.insert( texts, text )
		end
	end
	return #texts > 0 and table.concat( texts, rule['separator'] or ', ' ) or nil
end

--- Parametry infoboksu uzupełnione z dane (do testów: p._uzupelnij( 'Miasto infobox', {}, 'Q263' ))
-- @param map tabela mapy zamiast strony MediaWiki:… (tylko do testów)
-- @return table args, table lista uzupełnionych parametrów
function p._uzupelnij( templateName, args, entityId, map )
	local filled = {}
	entityId = entityId or mw.wikibase.getEntityIdForCurrentPage()
	if not entityId then
		return args, filled
	end
	map = map or mw.loadJsonData( MAP_PAGE )
	local config = map[templateName]
	if not config or not config['pola'] then
		return args, filled
	end
	local skippedFiles = {}
	for _, name in ipairs( map['_pomijane pliki'] or {} ) do
		skippedFiles[normalizeFile( name )] = true
	end

	-- parametry według właściwości
	local byProperty = {}
	for param, rule in pairs( config['pola'] ) do
		local property = rule['właściwość']
		byProperty[property] = byProperty[property] or {}
		table.insert( byProperty[property], param )
	end

	local result = {}
	for k, v in pairs( args ) do
		result[k] = v
	end
	for property, params in pairs( byProperty ) do
		local target, anyFilled = nil, false
		for _, param in ipairs( params ) do
			local rule = config['pola'][param]
			if not isEmpty( args[param], rule, skippedFiles ) then
				anyFilled = true
				break
			end
			if rule['uzupełniaj'] then
				target = param
			end
		end
		if target and not anyFilled then
			local text = valueText( entityId, property, config['pola'][target] )
			if text and text ~= '' then
				result[target] = text
				table.insert( filled, target )
			end
		end
	end
	return result, filled
end

--- {{#invoke:Dane|infobox|Biogram infobox}} w Szablon:Biogram infobox
function p.infobox( frame )
	local templateName = mw.text.trim( frame.args[1] or '' )
	local core = frame.args['rdzeń'] or ( 'Szablon:' .. templateName .. '/rdzeń' )
	local parent = frame:getParent()
	local args = {}
	for k, v in pairs( parent and parent.args or {} ) do
		args[k] = v
	end
	-- błąd odczytu z dane nie może zepsuć infoboksu – wtedy tylko parametry z artykułu
	local ok, filledArgs = pcall( p._uzupelnij, templateName, args )
	return frame:expandTemplate{ title = core, args = ok and filledArgs or args }
end

return p