Modulo:Marker utilities

El Vikivojaĝo
Salti al navigilo Salti al serĉilo

Versiokontrolo[redakti]

Versionomo en Vikidatumoj: 2021-11-29 Ok!


--[[
	Functions library for Marker and vCard modules
	In non-Wikivoyage projects, sister-project links functions have to be adapted.
]]--

-- module variable and administration
local mu = {
	moduleInterface = {
		suite  = 'Marker utilities',
		serial = '2021-11-29',
		item   = 58187612
	}
}

-- module import
require( 'Module:No globals' )
local cd = require( 'Module:Coordinates' )
local mg = mw.loadData( 'Module:Marker utilities/Groups' )
local mi = require( 'Module:Marker utilities/i18n' )
local mm = require( 'Module:Marker utilities/Maki icons' )
local mt = require( 'Module:Marker utilities/Types' )   -- types to groups like drink, eat, go, see, sleep, ...
local uc = require( 'Module:UrlCheck' )
local wu = require( 'Module:Wikidata utilities' )

function mu.isSet( arg )
	return arg and arg ~= ''
end

function mu.tableInsert( tab, value )
	if mu.isSet( value ) then
		table.insert( tab, value )
	end
end

-- text manipulation
function mu.textTrim( s )
	return s:match( '^[%c%s]*(.-)[%c%s]*$' )
end

-- sep is a single character separator but no magic character
function mu.textSplit( s, sep )
	local result = {}
	for str in s:gmatch( '([^' .. sep .. ']+)' ) do
		str = mu.textTrim( str )
		if str ~= '' then
			mu.tableInsert( result, mu.textTrim( str ) )
		end
	end
	return result
end

function mu.getAliases( tab, indx )
	local result = {}
	if not tab then
		return result
	end
	local iItem
	for key, item in pairs( tab ) do
		iItem = item[ indx ]
		if iItem then
			if type( iItem ) == 'table' then
				for _, q in ipairs( iItem ) do
					result[ q ] = key
				end
			else
				result[ iItem ] = key
			end
		end
	end
	return result
end

-- maintenance tools
function mu.initMaintenance( name )
	mu.name         = name -- module name
	mu.params       = {}   -- table of unknown parameters
	mu.aliases      = {}   -- table of duplicate parameter aliases
	mu.maintenance  = {}   -- table of error strings

	mu.types = mu.getAliases( mt.types, 'wd' )            -- Q id to type table
	mu.typeAliases = mu.getAliases( mt.types, 'alias' )   -- table for type aliases
	mu.groupAliases = mu.getAliases( mg.groups, 'alias' ) -- table for group aliases
end

function mu.addWrongParameter( param )
	table.insert( mu.params, "''" .. param .. "''" )
end

function mu.addDuplicateAlias( alias )
	table.insert( mu.aliases, "''" .. alias .. "''" )
end

function mu.addMaintenance( s )
	local function contains( new )
		for _, value in ipairs( mu.maintenance ) do
			if value == new then
				return true
			end
		end
		return false
	end
	if not contains( s ) then
		table.insert( mu.maintenance, s )
	end
end

function mu.getMaintenance()
	local s = ''
	if #mu.params > 0 then
		if #mu.params == 1 then
			s = mw.ustring.format( mi.maintenance.unknownParam, mu.params[ 1 ] )
		else
			s = mw.ustring.format( mi.maintenance.unknownParams,
				table.concat( mu.params, ', ' ) )
		end
		s = s .. mw.ustring.format( mi.maintenance.wrongParam, mu.name )
	end
	if #mu.aliases > 0 then
		s = s .. mw.ustring.format( mi.maintenance.duplicateAliases,
			table.concat( mu.aliases, ', ' ) )
	end
	return s .. table.concat( mu.maintenance, ' ' )
end

function mu.getCategories( formatStr )
	return wu.getCategories( formatStr )
end

-- args: template arguments consisting of argument name as key and a value
-- validKeys: table with argument name as key used by the script and
--    a string or a table of strings for argument names used by the local wiki
function mu.checkArguments( args, validKeys )
	if not args or not validKeys or not next( validKeys ) then
		return
	end

	for key, _ in pairs( args ) do
		local invalid = true
		for _, validKey in pairs( validKeys ) do
			if type( validKey ) == 'string' then
				validKey = { validKey }
			end
			for i = 1, #validKey, 1 do
				if key == validKey[ i ] then
					invalid = false
					break
				end
			end
		end
		if invalid then
			mu.addWrongParameter( key )
		end
	end
	return
end

-- returns an argument value from a set of keys
function mu.getArgument( args, keys )
	local value = nil
	if not args or not keys then
		return value
	end

	if type( keys ) == 'string' and keys ~= '' then
		keys = { keys }
	end
	if type( keys ) == 'table' then
		for _, key in ipairs( keys ) do
			if args[ key ] then
				if not value then
					value = args[ key ]
				else
					mu.addDuplicateAlias( keys[ 1 ] )
					break
				end
			end
		end
	end
	return value
end

-- groups translation for map legend into Wiki language
function mu.translateGroup( group )
	if not mu.isSet( group ) then
		group = mt.types.error.group
	end
	local t = mg.groups[ group ]
	if t then
		t = t.map or t.label or t.alias or group
		if type( t ) == 'table' then
			t = t[ 1 ]
		end
		return t
	end
	return group
end

function mu.addWdClass( isFromWikidata )
	return isFromWikidata and ' wikidata-content' or ''
end

-- getting DMS coordinates
function mu.dmsCoordinates( lat, long, name, fromWD, extraParams, withBracket )
-- local function outputCoordinates( lat, long, name )
	local latDMS, _, latMs = cd.getDMSString( lat, 4, 'lat', '', '', mi.defaultDmsFormat )
	local longDMS, _, longMs = cd.getDMSString( long, 4, 'long', '', '', mi.defaultDmsFormat )

	local r = '[' .. mi.coordURL .. latMs .. '_' .. longMs .. '_'
		.. mw.uri.encode( extraParams )	.. '&locname=' .. mw.uri.encode( name )
		.. ' ' .. tostring( mw.html.create( 'span' )
				:addClass( 'coordStyle' )
				:attr( 'title', mi.texts.latitude )
				:wikitext( latDMS )
			)
		.. ' ' .. tostring( mw.html.create( 'span' )
				:addClass( 'coordStyle' )
				:attr( 'title', mi.texts.longitude )
				:wikitext( longDMS )
			)
		.. ']'
	if withBracket then
		r = '(' .. r .. ')'
	end
	return tostring( mw.html.create( 'span' )
		:attr( 'class', 'listing-dms-coordinates printNoLink plainlinks'
			.. mu.addWdClass( fromWD ) )
		:wikitext( r ) )
end

local function replaceWithSpace( s, pattern )
	s = s:find( pattern ) and s:gsub( pattern, ' ' ) or s
	return s
end

-- removing line breaks and controls from parameter strings
function mu.removeCtrls( s, onlyInline )
	local descrDiv = false -- div tag instead of span tag for description needed?

	-- remove controls from tags before test
	s = s:gsub( '(<[^>]+>)', function( t )
			return replaceWithSpace( t, '[%z\1-\31]' )
		end )

	local t = replaceWithSpace( s, '</br%s*>' )
	if onlyInline then
		t = replaceWithSpace( t, '<br[^/>]*/*>' )
		t = replaceWithSpace( t, '</*p[^/>]*/*>' )
		t = replaceWithSpace( t, '[%z\1-\31]' )
		-- not %c because \127 is used for Mediawiki tags (strip markers `UNIQ)
	else
		t = replaceWithSpace( t, '[%z\1-\9\11\12\14-\31]' )
		descrDiv = t:find( '[\10\13]' ) or t:find( '<br[^/>]*/*>' ) or
			t:find( '<p[^/>]*>' )
	end
	if t ~= s then
		mu.addMaintenance( mi.maintenance.illegalCtrls )
	end

	if descrDiv then
		mu.addMaintenance( mi.maintenance.descrDiv )
		-- replace line breaks by <br> in block mode
		t = t:gsub( '([^>%]\10\13])[\10\13][\10\13]+([^<%[\10\13])', '%1<br class="next-paragraph" />%2' )
	end

	return replaceWithSpace( t, '%s%s+' ), descrDiv
end

-- adding linked sister icons
function mu.addSisterIcons( sisters, name, id )
	local t = ''
	for key, value in pairs( sisters ) do
		if value ~= '' then
			t = t .. ' ' .. tostring( mw.html.create( 'span' )
				:attr( 'class', 'listing-sister-icon listing-sister-' .. key )
				:wikitext( '[' .. value .. ' '
					.. mw.ustring.format( mi.icons[ key ], name ) .. ']' )
			)
		end
	end

	local s = ''
	if mu.isSet( id ) then
		-- add Wikidata symbol
		s = tostring( mw.html.create( 'span' )
			:attr( 'class', 'listing-sister-icon listing-sister-wikidata' )
			:addClass( t == '' and 'listing-only-wikidata' or nil )
			:wikitext( '[' .. tostring( mw.uri.fullUrl( 'd:' .. id ) )
				.. ' ' .. mw.ustring.format( mi.icons.wikidata, name, id ) .. ']' )
			)
		t = t .. s
	end
	return t, s
end

local function getURL( prefix, page )
	return tostring( mw.uri.fullUrl( prefix .. mw.uri.encode( page, 'WIKI' ) ) )
end

-- getting sister project links
function mu.getWikiLink( langArray, wiki, entity, wikilang )
	local prefix = ''
	if wiki == 'wiki' then
		prefix = 'w:' -- empty in case of Wikivoyage
	end
	local link
	for _, lang in ipairs( langArray ) do
		if lang ~= '' then
			link = wu.getSitelink( entity, lang .. wiki )
			if link then
				return getURL( prefix ..
					( lang ~= wikilang and ( lang .. ':' ) or '' ), link )
			end
		end
	end
	return ''
end

-- adding Wikimedia sister project icons
function mu.makeSisterIcons( args, page, country, entity )
	local sisters = {
		wikivoyage = '', -- link to another branch, usually en, as a sister link
		commons    = '', -- link to Commons category
		wikipedia  = ''  -- link to Wikipedia
	}

	if mu.isSet( args.wikipedia ) then
		sisters.wikipedia = getURL( 'w:', args.wikipedia )
	end
	if mu.isSet( args.wikidata ) then
		if sisters.wikipedia == '' then
			sisters.wikipedia = mu.getWikiLink(
				{ page.lang, mi.langs.name, country.lang }, 'wiki', entity, page.lang
			)
		end
		if args.wikiPage == '' then
			sisters.wikivoyage = mu.getWikiLink(
				{ mi.langs.name, country.lang }, page.globalProject, entity, page.lang
			)
			if sisters.wikivoyage ~= '' then
				mu.addMaintenance( mi.maintenance.linkToOtherWV )
			end
		end
	end
	if args.commonscat ~= '' then
		sisters.commons = getURL( 'c:Category:', args.commonscat )
	end

	return mu.addSisterIcons( sisters, args.givenName.name, args.wikidata )
end

function mu.removeStars( name )
	if name:find( '%*' ) then
		name = mw.text.trim( name:gsub( '%*', '' ) )
		mu.addMaintenance( mi.maintenance.nameWithStar )
	end
	return name
end

-- getting name table with linked and unlinked names
function mu.getName( name, pageTitle )
	local r = {
		exists = false,
		all = '', -- name or linked name
		name = '', -- only name
		pageTitle = '', -- if pageTitle ~= '' name is already linked
	}
	if not mu.isSet( name ) or type( name ) ~= 'string' then
		return r
	end

	r.exists = true
	local s
	local t, c = mw.ustring.gsub( name, '^(.*)%[%[(.*)%]%](.*)$', '%2' )
	if c > 0 then -- is / contains [[...]]
		t = mu.textTrim( t )
		r.all = '[[' .. t .. ']]'
		s, c = mw.ustring.gsub( t, '^(.*)|(.*)$', '%2' )
		if c > 0 then -- is [[...|...]]
			r.name = mu.textTrim( s )
			r.pageTitle = mu.textTrim( mw.ustring.gsub( t, '^(.*)|(.*)$', '%1' ) )
		else
			r.name = t
			r.pageTitle = t
		end
	else
		r.name = name
		r.all = name
		if pageTitle ~= '' then
			r.pageTitle = pageTitle
			r.all = '[[' .. pageTitle .. '|' .. name .. ']]'
		end
	end
	r.name = mu.removeStars( r.name )
	r.all = mu.removeStars( r.all )
	return r
end

-- checking coordinates
function mu.checkCoordinates( args )
	local dms = false
	local t
	if type( args.lat ) == 'boolean' then
		args.lat = ''
	end
	if type( args.long ) == 'boolean' then
		args.long = ''
	end
	if args.lat == '' and args.long == '' then
		return
	elseif args.lat ~= '' and args.long == '' then
		t = args.lat:find( ',', 1, true )
		if t then
			args.long = mu.textTrim( args.lat:sub( t + 1, #args.lat ) )
			args.lat = mu.textTrim( args.lat:sub( 1, t - 1 ) )
		end
	end
	if args.lat == '' or args.long == '' then
		args.lat = ''
		args.long = ''
		mu.addMaintenance( mi.maintenance.wrongCoord )
		return
	end

	t = tonumber( args.lat )
	if t then
		args.lat = math.abs( t ) <= 90 and t or ''
	else
		t = cd.toDec( args.lat, 'lat', 6 )
		args.lat = t.error == 0 and t.dec or ''
		dms = args.lat ~= ''
	end
	if args.lat == '' then
		args.long = ''
		mu.addMaintenance( mi.maintenance.wrongCoord )
		return
	end

	t = tonumber( args.long )
	if t then
		args.long = ( t > -180 and t <= 180 ) and t or ''
	else
		t = cd.toDec( args.long, 'long', 6 )
		args.long = t.error == 0 and t.dec or ''
		dms = dms or args.long ~= ''
	end
	if args.long == '' then
		args.lat = ''
		mu.addMaintenance( mi.maintenance.wrongCoord )
	end
	if dms then
		mu.addMaintenance( mi.maintenance.dmsCoordinate )
	end

	return
end

-- getting a set of parameters for a given type
function mu.getTypeParams( aType )
	return mt.types[ aType ] and mt.types[ aType ] or {}
end

local function encodeSpaces( s )
	if s:find( '[_%s]+' ) then
		s = s:gsub( '[_%s]+', '_' )
	end
	return s
end

-- getting marker type and group
function mu.checkTypeAndGroup( args )
	local s, t
	if mu.isSet( args.group ) then
		mu.addMaintenance( mi.maintenance.groupUsed )
		if mu.groupAliases[ args.group ] then
			args.group = mu.groupAliases[ args.group ]
		end
		s = mg.groups[ args.group ]
		if not s then			
			mu.addMaintenance( mi.maintenance.unknownGroup )
			args.group = ''
		end
	end
	if not mu.isSet( args.type ) then
		args.type = mt.types.error.group
		if args.group == '' then
			args.group = mt.types.error.group
		end
		mu.addMaintenance( mi.maintenance.missingType )
	elseif args.type == mt.types.error.group then
		if args.group == '' then
			args.group = mt.types.error.group
		end
		mu.addMaintenance( mi.maintenance.unknownType )
	else
		-- split seperate types and analyse them
		args.typeTable = {}
		for _, t in ipairs( mu.textSplit( args.type, ',' ) ) do
			t = encodeSpaces( t:lower() )
			if next( mu.typeAliases ) then
				t = mu.typeAliases[ t ] or t
			end
			s = mu.groupAliases[ t ]
			if s then
				t = s
			end
			s = mg.groups[ t ]
			if s then -- type is a group itself
				if s.is and s.is == 'color' then
					mu.addMaintenance( mi.maintenance.typeIsColor )
					if s.alias then
						t = s.alias
						if type( t ) == 'table' then
							t = t[ 1 ]
						end
					end
				elseif not mi.options.noTypeMsgs then
					mu.addMaintenance( mi.maintenance.typeIsGroup )
				end
				if args.group == '' then
					args.group = t
				end
			else
				s = mt.types[ t ]
				if s then
					if args.group == '' then
						args.group = s.group
					end
				else
					mu.addMaintenance( mi.maintenance.unknownType )
					args.group = mt.types.error.group
				end
			end
			table.insert( args.typeTable, t )
		end
		args.type = table.concat( args.typeTable, ',' )
	end
	args.groupTranslated = mu.translateGroup( args.group )
	return
end

local function removeNS( s, nsTable )
	s = s or ''
	if not s:find( ':', 1, true ) then
		return s
	end

	local t = s
	for _, ns in ipairs( nsTable ) do
		t = mw.ustring.gsub( t, '^' .. ns .. ':', '' )
		if s ~= t then
			break
		end
	end
	return t
end

-- image check
function mu.checkImage( image, entity )
	if type( image ) == 'boolean' or image == '' then
		return image
	end

	-- delete namespace
	image = removeNS( image, mi.texts.FileNS )

	-- formal checks
	if image:find( '^https?:' ) then
		image = ''
	else
		local extExists = false
		local im = image:lower()
		for _, ext in ipairs( mi.fileExtensions ) do
			if im:find( '%.' .. ext .. '$' ) then
				extExists = true
				break
			end
		end
		if not extExists then
			image = ''
		end
	end
	if image == '' then
		mu.addMaintenance( mi.maintenance.wrongImgName )
		return image
	end

	local alreadyChecked = false
	if mi.options.mediaCheck and image ~= '' then
		if not mi.options.WDmediaCheck and entity then
			-- check if image is stored in Wikidata
			local imgs = wu.getValues( entity, mi.properties.image, nil )
			if #imgs > 0 then
				for i = 1, #imgs, 1 do
					if imgs[ i ] == image then
						alreadyChecked = true
						break
					end
				end
			end
		end
		if not alreadyChecked then
			-- expensive function call
			local title = mw.title.new( 'Media:' .. image )
			if not title or not title.exists then
				mu.addMaintenance( mw.ustring.format( mi.maintenance.missingImg, image ) )
				image = ''
			end
		end
	end
	return image
end

-- remove namespace from category
function mu.checkCategory( category )
	return removeNS( category, mi.texts.CategoryNS )
end

-- looking for URL query which can contain a tracking parameter
local function lookForQuery( url )
	if mi.options.lookForQuery and mu.isSet( url ) and url:find( '?', 1, true ) then
		for _, p in ipairs( mi.queryExceptions ) do
			if url:find( p ) then
				return
			end
		end
		mu.addMaintenance( mi.maintenance.urlWithQuery )
	end
end

-- url check
function mu.checkUrl( url )
	local domain

	if mu.isSet( url ) then
		local c = uc.isUrl( url, mi.options.skipPathCheck ) -- getting result code
		if c > 2 then
			mu.addMaintenance( mi.maintenance.wrongUrl )
			url = ''
		elseif c == 2 then -- URL contains IP address
			mu.addMaintenance( mi.maintenance.urlWithIP )
		end
		lookForQuery( url )
	end

	if mu.isSet( url ) then
		for _, service in ipairs( mi.services ) do
			domain = service.url:gsub( 'com/.*', 'com' )
				:gsub( 'https://', '' ):gsub( 'www.', '' )
			if url:find( domain, 1, true ) then
				mu.addMaintenance( mi.maintenance.urlIsSocialMedia )
				url = ''
			end
		end
	end

	return url
end

-- making marker symbol
function mu.makeMarkerSymbol( args, title, frame )
	local lon = tonumber( args.long )
	local lat = tonumber( args.lat )
    local tagArgs = {
        zoom = tonumber( args.zoom ),
        latitude = lat,
        longitude = lon,
        show = mg.showAll,
    }
	if mu.isSet( args.text ) then
		tagArgs.text = args.text
		if not args.useIcon then
			tagArgs.class = 'no-icon'
		end
	end
	if mu.isSet( args.mapGroup ) then
		tagArgs.group = args.mapGroup
		tagArgs.show = args.mapGroup
	else
		tagArgs.group = args.groupTranslated
	end
	if mu.isSet( args.image ) then
		tagArgs.description = '[[File:' .. args.image .. '|100x100px|' .. title .. ']]'
	end

	local geoJson = {
		type = 'Feature',
		geometry = {
			type = 'Point',
			coordinates = { lon, lat }
		},
		properties = {
			title = title,
			description = tagArgs.description,
			['marker-symbol'] = args.symbol,
			['marker-color'] = args.color,
			['marker-size'] = 'medium',
		}
	}

	return tostring( mw.html.create( 'span' )
		:attr( 'class', 'listing-map plainlinks printNoLink' )
		:attr( 'title', mi.texts.tooltip )
		:css( {
			[ 'background-color' ] = args.color,
			[ 'border-color' ] = args.color
		} )
		-- frame:extensionTag is expensive
		:wikitext( frame:extensionTag( 'maplink', mw.text.jsonEncode( geoJson ), tagArgs ) )
	)
end

-- getting color from group type
function mu.getColor( aType )
	local c = mg.groups[ aType ]
	if c then
		return c.color, aType
	else
		return mg.groups[ 'error' ].color, 'error'
	end
end

-- replace brackets in names
function mu.replaceBrackets( s )
	s, _ = s:gsub( '%[', '&#x005B;' )
		:gsub( '%]', '&#x005D;' )
	return s
end

function mu.convertForSort( s )
	s = mw.ustring.lower( s )
	for _, obj in ipairs( mi.substitutes ) do
		s = mw.ustring.gsub( s, obj.l, obj.as )
	end
	return s
end

-- generates a table with type documentation
function mu.generateTableTypeList( frame )
	local rows = {}
	local typeList = '<table class="sortable multiline" cellspacing="0">'
		.. '<tr><th>' .. mi.types.label .. '</th><th>'
		.. mi.types.group .. '</th><th>' .. mi.types.type .. '</th></tr>'
	local label
	for key, value in pairs( mt.types ) do
		label = value.label or value.alias or key
		if type( label ) == 'table' then
			label = label[ 1 ] or ''
		end
		label = label:gsub( '_', ' ' )
		table.insert( rows, '<tr><td>' .. label .. '</td><td>' .. value.group
			.. '</td><td>' .. key:gsub( '_', ' ' ) .. '</td></tr>' )
	end
	table.sort( rows,
		function( a, b )
			return mu.convertForSort( a ) < mu.convertForSort( b )
		end
	)
	return typeList .. table.concat( rows, '' ) .. '</table>'
end

-- prepare value data parameter to a tag
function mu.data( s )
	if not mu.isSet( s ) then
		return nil
	else
		return mw.text.decode( s, true ) -- replacing all entities by characters
	end
end

-- adding wrapper and microformats
function mu.makeWrapper( result, args, page, country, show, list, aClass, frame )
	if type( result ) == 'table' then
		result = table.concat( result, ' ' )
	end

	local wrapper = mw.html.create( show.inline and 'span' or 'div' )
		:attr( 'class', 'h-card ' .. aClass )
	if args.noGpx then
		wrapper:addClass( 'vcard-nogpx' )
	else
		wrapper:addClass( 'vcard' )
	end
	if show.outdent and not show.inline then
		wrapper:addClass( 'listing-outdent' )
	end
	if show.inlineSelected then
		wrapper:addClass( 'listing-inline' )
	end
	if args.givenName.name ~= mi.maintenance.missingName then
		wrapper:attr( 'data-name', mu.data( args.givenName.name ) )
	end
	wrapper:attr( 'data-location', mu.data( page.subpageText ) )
		:attr( 'data-location-qid', page.entityId )
		:attr( 'data-wikilang', page.lang )
		:attr( 'data-country', mu.data( country.iso_3166 ) )
		:attr( 'data-country-name', mu.data( country.country ) )
		:attr( 'data-lang', mu.data( country.lang ) )
		:attr( 'data-lang-name', mu.data( country.langName ) )
		:attr( 'data-country-calling-code', mu.data( country.cc ) )
		:attr( 'data-trunk-prefix', mu.data( country.trunkPrefix ) )
		:attr( 'data-dir', mu.data( country.isRTL and 'rtl' or 'ltr' ) )
		:attr( 'data-wiki-dir', mu.data( page.isRTL and 'rtl' or 'ltr' ) )
		:attr( 'data-currency', mu.data( country.addCurrency ) )
	local arg
	for key, value in pairs( list ) do
		arg = args[ key ]:gsub( '<[^<>]*>', '' ) -- remove html tags
		wrapper:attr( value, mu.data( arg ) )
	end
	if not show.noCoord then
		wrapper:node( mw.html.create( 'span' )
			:attr( 'class', 'p-geo geo listing-coordinates' )
			:css( 'display', 'none' )
			:node( mw.html.create( 'span' )
				:attr( 'class', 'p-latitude latitude' )
				:wikitext( args.lat )
			)
			:node( mw.html.create( 'span' )
				:attr( 'class', 'p-longitude longitude' )
				:wikitext( args.long )
			)	
		)
	end
	if not show.name then
		wrapper:node( mw.html.create( 'span' )
			:attr( 'class', 'p-name fn org listing-name' )
			:css( 'display', 'none' )
			:wikitext( args.givenName.name )
		)
	end

	wrapper = tostring( wrapper:wikitext( result ) )

	-- adding coordinates to Mediawiki database
	-- frame:callParserFunction is expensive
	if not show.noCoord and mi.options.secondaryCoords then
		wrapper = wrapper .. frame:callParserFunction{ name = '#coordinates',
			args = { args.lat, args.long, country.extra, name = args.givenName.name } }
	end

	return wrapper
end

-- bdi and bdo tags are not working properly on all browsers. Adding marks
-- (lrm, rlm) is maybe the only way for a correct output
function mu.languageSpan( s, titleHint, page, country )
	if not mu.isSet( s ) then
		return ''
	end

	local c = country.lang
	if c == '' or c == page.lang then
		return s
	end

	local dir
	if country.isRTL then
		dir = 'rtl'
	elseif page.isRTL then
		dir = 'ltr'
	end	

	local t = tostring( mw.html.create( 'span' )
		:attr( 'class', 'wv-lang wv-lang-' .. c )
		:attr( 'lang', c )
		:attr( 'dir', dir )
		:attr( 'title', mw.ustring.format( titleHint , country.langName ) )
		:wikitext( s )
	)

	if country.isRTL and not page.isRTL then
		t = '&rlm;' .. t .. '&lrm;'
	end
	if not country.isRTL and page.isRTL then
		t = '&lrm;' .. t .. '&rlm;'
	end
	return t
end

-- Splitting comma separated lists to an array
-- convert = true: conversion to lowercase characters, spaces to low lines
-- toValue = true: list items handled as array values
-- toValue = false: list items handled as array keys
function mu.split( s, convert, toValue )
	local arr = {}
	if not mu.isSet( s ) then
		return arr
	end
	for _, str in ipairs( mu.textSplit( s, ',' ) ) do
		if convert then
			str = encodeSpaces( str:lower() )
		end
		if toValue then
			table.insert( arr, str )
		else
			arr[ str ] = ''
		end
	end
	return arr
end

-- Splitting comma separated lists to an array of key items
-- checking items with allowed key values of validValues array
function mu.splitAndCheck( s, validValues, validAliases )
	local arr = {}
	local err = {}
	local at, count, item, wrong
	if not mu.isSet( s ) or not validValues then
		return arr, ''
	end
	-- error check
	for k, _ in pairs( mu.split( s, true ) ) do
		wrong = false
		count = ''
		item = k
		if validValues[ item ] and validValues[ item ].e then
			item = validValues[ item ].e
		end
		-- split subtype from count
		at = item:find( ':', 1, true )
		if at then
			count = tonumber( item:sub( at + 1, #item ) ) or ''
			item = mu.textTrim( item:sub( 1, at - 1 ) )
			if count == '' then
				wrong = true
			else
				count = math.floor( count )
				if count < 2 then
					count = ''
				end
			end
		end
		-- check subtype
		if validAliases and validAliases[ item ] then
			item = validAliases[ item ]
		end
		if validValues[ item ] then
			arr[ item ] = count
		else
			wrong = true
		end
		if wrong then
			table.insert( err, k )
		end
	end
	return arr, table.concat( err, ', ' )
end

function mu.getShow( default, value, validValues )
	local show = mu.split( default, true )
	local add, err = mu.splitAndCheck( value, validValues )
	if err ~= '' then
		mu.addMaintenance( mw.ustring.format( mi.maintenance.unknownShow, err ) )
	end
	if add.none or add.coord or add.poi or add.all then
		show.all   = nil -- overwriting defaults
		show.coord = nil
		show.poi   = nil
	end
	for key, value in pairs( add ) do
		show[ key ] = value
	end
	if show.none then
		show.none  = nil
		show.all   = nil
		show.coord = nil
		show.poi   = nil
	end
	if show.all then
		show.all   = nil
		show.coord = ''
		show.poi   = ''
	end

	return show
end

function mu.getCoordinatesFromWikidata( args, fromWikidata, entity )
	if not entity or ( args.lat ~= '' and args.long ~= '' ) then
		return
	end

	-- center coordinates from Wikidata
	local c = wu.getValue( entity, mi.properties.centerCoordinates )
	if c == '' then
		-- coordinates from Wikidata
		c = wu.getValue( entity, mi.properties.coordinates )
	end
	if c ~= '' then
		args.lat = c.latitude
		args.long = c.longitude
		fromWikidata.lat = true
	end
end

function mu.makeSocial( args, fromWikidata, name, checkIt )
	local domain, span, t
	local s = ''

	for _, service in ipairs( mi.services ) do
		-- check values first
		t = args[ service.key ] or ''
		domain = service.url:gsub( 'com/.*', 'com/' )
		if t ~= '' and checkIt then
			if t:match( '^http' ) then
				if not t:match( '^https' ) then
					t = t:gsub( '^http', 'https' )
					mu.addMaintenance( mw.ustring.format(
						mi.maintenance.wrongSocialUrl, service.key ) )
				end
				if uc.isUrl( t, mi.options.skipPathCheck ) > 1 or
					not t:match( '^' .. domain .. '.+$' ) then
					t = ''
					mu.addMaintenance( mw.ustring.format(
						mi.maintenance.wrongSocialUrl, service.key ) )
				end
				if t ~= '' and not fromWikidata[ service.key ] then
					mu.addMaintenance( mw.ustring.format(
						mi.maintenance.socialUrlUsed, service.key ) )
				end
			else
				local match = false
				local sp = service.pattern
				if type( sp ) == 'string' then
					sp = { sp }
				end
				for _, pattern in ipairs( sp ) do
					if mw.ustring.match( t, pattern ) then
						match = true
						break
					end
				end
				if not match then
					t = ''
					mu.addMaintenance( mw.ustring.format(
						mi.maintenance.wrongSocialId, service.key ) )
				end
			end
		end
		args[ service.key ] = t

		-- create symbol link
		if t ~= '' then
			if not t:find( domain, 1, true ) then
				t = mw.ustring.format( service.url, t )				
			end
			span = mw.html.create( 'span' )
				:attr( 'class', 'listing-social-media '
					.. 'listing-social-media-' .. service.key
					.. mu.addWdClass( fromWikidata[ service.key ] ) )
				:attr( 'style', mi.texts.socialStyle )
				:wikitext( '[' .. t .. ' '
					.. mw.ustring.format( mi.icons[ service.key ], name ) .. ']' )
			s = s .. tostring( span )
		end
	end
	return s
end

local function _makeAirport( code, args, fromWikidata )
	local span = mw.html.create( 'span' )
		:attr( 'class', 'listing-' .. code .. '-code'
			.. mu.addWdClass( fromWikidata[ code ] ) )
		:wikitext( args[ code ] )
	return tostring( mw.html.create( 'span' )
		:attr( 'class', 'listing-airport listing-' .. code )
		:wikitext( mw.ustring.format( mi.texts[ code ], tostring( span ) ) )
	)
end

function mu.makeAirport( args, fromWikidata )
	local airport = ''
	if mu.isSet( args.iata ) then
		airport = _makeAirport( 'iata', args, fromWikidata )
	elseif mu.isSet( args.icao ) then
		airport = _makeAirport( 'icao', args, fromWikidata )
	end
	return airport
end

local function _typeSearch( p31 )
	-- p31: array of Wikidata values
	if not p31 or #p31 == 0 then
		return ''
	end

	local firstStep = true

	local function compareLabels( ar )
		if not ar then
			return nil
		elseif type( ar ) == 'string' then
			ar = { ar }
		end
		for i, value in ipairs( ar ) do
			if mu.isSet( value ) then
				value = mw.ustring.lower( encodeSpaces( value ) )
				if mt.types[ value ] then
					return value
				end
			end
		end
		return nil
	end

	local function compareIds( ar )
		local id, t
		for i = 1, #ar, 1 do
			id = ar[ i ].id
			-- md: indexed array of q id - types relations
			t = mu.types[ id ]
			if t then
				return t
			end

			-- checking English label and aliases
			t = compareLabels( mw.wikibase.getLabelByLang( id, 'en' ) )
				or compareLabels( wu.getAliases( id, 'en' ) )
			if t then
				if firstStep and not mi.options.noTypeMsgs then
					firstStep = false
					mu.addMaintenance( mi.maintenance.typeFromWDchain )
				end
				return t
			end
		end
		return nil
	end

	local aType = compareIds( p31 ) -- check p31 ids first, maybe step 2 is not nessary
	if aType then
		if not mi.options.noTypeMsgs then
			mu.addMaintenance( mi.maintenance.typeFromWD )
		end
		return aType
	end

	-- now functions becomes expensive because of multiple wu.getValues calls
	local id, ids
	firstStep = false
	for i = 1, #p31, 1 do -- step 2: analyse P279 chains of first ids
		id = p31[ i ].id -- start id
		local j = 0
		repeat
			ids = wu.getValues( id, mi.properties.subclassOf, nil )
			if #ids > 0 then
				aType = compareIds( ids )
				if aType then
					if not mi.options.noTypeMsgs then
						mu.addMaintenance( mi.maintenance.typeFromWD )
						mu.addMaintenance( mi.maintenance.typeFromWDchain )
					end
					return aType
				end
				id = ids[ 1 ].id
			end
			j = j + 1

		-- limit: maximum levels to analyse
		until j >= mi.searchLimit or #ids == 0
	end

	return ''
end

function mu.typeSearch( p31, entity )
	if p31 and #p31 == 0 then
		p31 = wu.getValues( entity, mi.properties.subclassOf, mi.p31Limit )
	end
	return _typeSearch( p31 )
end

-- returns a single data set from Module:Marker utilities/Maki icons
function mu.getMaki( key )
	return mm.icons[ key ]
end

function mu.getMakiIconName( aType )
	local mType
	if mm.icons[ aType ] then
		mType = aType
	elseif mt.types[ aType ] and mt.types[ aType ].icon then
		mType = mt.types[ aType ].icon
	end
	if mType and mu.isSet( mm.icons[ mType ].im ) then
		return mType
	end
	return nil
end

function mu.checkCommonsCategory( args )
	-- remove namespace from category
	if mu.isSet( args.commonscat ) then
		args.commonscat = mu.checkCategory( args.commonscat )
		if args.commonscat ~= '' then
			mu.addMaintenance( mi.maintenance.commonscat )
		end
	end
end

function mu.getCommonsCategory( args, entity )
	-- getting commonscat from commonswiki sitelink before P373
	-- because sitelink is checked by Wikidata
	if type( args.commonscat ) == 'boolean' then
		args.commonscat = ''
	end

	local t = wu.getSitelink( entity, 'commonswiki' ) or ''
	if t:match( '^Category:.+$' ) then
		t = t:gsub( '^Category:', '' )
	else
		t = wu.getValue( entity, mi.properties.commonsCategory )
		if t == '' then
			local id = wu.getId( entity, mi.properties.mainCategory )
			if id ~= '' then
				t = wu.getSitelink( id, 'commonswiki' ) or ''
				t = t:gsub( '^Category:', '' )
			end
		end
	end
	if t ~= '' and args.commonscat ~= '' then
		mu.addMaintenance( mi.maintenance.commonscatWD )
	end
	if args.commonscat == '' then
		args.commonscat = t
	end
end

-- getting names from Wikidata
function mu.getNamesFromWikidata( args, fromWikidata, page, country, entity )
	-- getting official names
	local officialNames =
		wu.getMonolingualValues( entity, mi.properties.officialName )

	if type( args.name ) == 'boolean' or args.name == '' then
		args.name = officialNames[ page.lang ]
			or mi.langs.name ~= '' and officialNames[ mi.langs.name ]
			or ''
		-- if failed then get labels
		if args.name == '' then
			if page.lang == mi.langs.name then
				args.name = wu.getLabel( entity ) or ''
			else
				args.name = wu.getLabel( entity )
					or mi.langs.name ~= '' and wu.getLabel( entity, mi.langs.name )
					or ''
			end
		end
		if args.name ~= '' then
			mu.addMaintenance( mi.maintenance.nameFromWD )
			fromWikidata.name = true
		end
	end

	-- get local name if no name is available
	if args.name == '' and
		not ( type( args.nameLocal ) == 'string' and args.nameLocal ~= '' ) then
		args.nameLocal = true
	-- no local name if country and wiki language are identical
	elseif args.nameLocal == true and page.lang == country.lang then
		args.nameLocal = ''	
	end

	if args.nameLocal == true then
		args.nameLocal = officialNames[ country.lang ] or ''
		if args.nameLocal == '' then
			args.nameLocal = wu.getLabel( entity, country.lang ) or ''
		end
		if args.name == '' and args.nameLocal ~= '' then
			args.name = args.nameLocal
			args.nameLocal = ''
			mu.addMaintenance( mi.maintenance.nameFromWD )
			fromWikidata.name = true
		end
		if args.name:lower() == args.nameLocal:lower() then
			args.nameLocal = ''
		end
		fromWikidata.nameLocal = args.nameLocal ~= ''
		if fromWikidata.nameLocal then
			mu.addMaintenance( mi.maintenance.localNameFromWD )
		end
	end
end

-- getting link to Wikivoyage
function mu.getArticleLink( args, entity, page )
	local t = wu.getSitelinkTable( entity, page.lang .. page.globalProject )
	if t and t.title ~= page.text then  -- no link to the article itself
		local isLink = false
		for _, badge in ipairs( t.badges ) do
			if badge == mi.qualifiers.intentionalSitelink or 
				badge == mi.qualifiers.redirectSitelink then
				isLink = true
				break
			end
		end
		if not isLink then
			args.wikiPage = t.title
		end
	end
end

-- replacing decimal separator and inserting group separators
function mu.formatNumber( num )
	if mu.isSet( mi.formatnum.decimalPoint ) and mi.formatnum.decimalPoint ~= '.' then
		num = num:gsub( '%.', mi.formatnum.decimalPoint )
	end

	if mu.isSet( mi.formatnum.groupSeparator ) then
		local count
		repeat
			num, count = num:gsub( '^([-+]?%d+)(%d%d%d)',
				'%1%' .. mi.formatnum.groupSeparator .. '%2' ) 
		until count == 0
	end
    return num
end

function mu.addTypesToSubtypes( subtypes, num )
    for key, tab in pairs( mt.types ) do
        if not subtypes[ key ] then
            if type( tab.wd ) == 'table' and #tab.wd == 1 then
                tab.wd = tab.wd[ 1 ]
            end
            subtypes[ key ] =
            	{ g = num, wd = tab.wd, alias = tab.alias, n = tab.label or key }
        end
    end
end

return mu