Модуль:Disambig

Материал из Википедии — свободной энциклопедии
Перейти к навигации Перейти к поиску
Документация

На данный момент реализует вывод шаблона {{Неоднозначность}} и часть вывода {{Категория-неоднозначность}}. Со временем будет дописан, чтобы выполнять разного рода проверки на страницах значений.

Код вызова модуля:

{{#invoke:Disambig|main}}

Данные

Вызывает и обрабатывает Module:Disambig/data.json для данных о разных типах значений. Все доступные типы (кроме служебных) можно увидеть на Шаблон:Неоднозначность#Поддерживаемые типы. Данные на странице данных представлены в следующем виде:

	"ключ": {
		"aliases": [
			"алиас 1",
            "алиас 2",
            "алиас_3"
		],
		"image": "иконка",
		"desc": "описание формата «Список статей о X»",
		"short": "необязательно: краткое описание того же формата",
		"about": "уточнение формата «о конкретном X»",
		"seeAlsoCategory": "для персоналий: категория со списком всех статей с названием",
		"category": "подкатегория формата «Страницы значений:X»",
		"categorySortPrefixes": [
			"префиксы в названии через запятую, которые следует поставить в конец при сортировке в категориях"
		]
	}

По возможности следует добавлять минимальное число алиасов (желательно 0).

Минимальный пустой шаблон для вставки нового типа на страницу:

	"ключ": {
		"image": "",
		"desc": "",
		"about": "",
		"category": ""
	}

Новые типы должны обсуждаться на Обсуждение проекта:Страницы значений перед добавлением.

Функции

  • p.main / p._main — вызов шаблона {{Неоднозначность}}.
  • p.alias — вызов шаблона {{Неоднозначность}} из шаблонов-обёрток для их корректной подстановки и учёта во включениях основного шаблона.
  • p.category / p._category — генерация списка категорий в шаблоне {{Категория-неоднозначность}}.
  • p.doc / p._doc — генерация автоматической таблицы документации известных типов значений на странице Шаблон:Неоднозначность/doc.
  • p.templateData / p._templateData — добавление известных типов значений в блок TemplateData на странице документации.
require( 'strict' )
local p = {}

local getArgs = require( 'Module:Arguments' ).getArgs
local escapePattern = require( 'Module:String' )._escapePattern
local nestedQuotes

local currentTitle = mw.title.getCurrentTitle()

-- [[Module:Disambig/styles.css]]
local templateStylesPage = 'Module:Disambig/styles.css'
local data = mw.loadJsonData( 'Module:Disambig/data.json' )

local configData = data[ '--config' ]
local defaultData = data[ '--disambig' ]
local errorData = data[ '--error' ]

local function getAliases()
	local result = {}
	for key, val in pairs( data ) do
		if val[ 'aliases' ] then
			for i, alias in ipairs( val[ 'aliases' ] ) do
				result[ alias ] = key
			end
		end
	end
	
	return result
end

local aliases = getAliases()

local function isEmpty( str )
	return str == nil or str == ''
end

local function escapeRepl( text )
	-- Replace % to %% to avoid %1, %2, %3 being treated as groups
	return ( mw.ustring.gsub( text, '%%', '%%%' ) )
end

local function getConfig( key, isRequired )
	if configData == nil then
		return error( 'getConfig: config is missing in Module:Disambig/data.json' )
	end
	if isRequired ~= false and isEmpty( configData[ key ] ) then
		return error( string.format( 'getConfig: "%s" key is missing in Module:Disambig/data.json' ) )
	end
	
	return configData[ key ] or nil
end

local function isNotOther( dtype )
	local aliasesOther = data[ '--other' ][ 'aliases' ]
	for i, alias in ipairs( aliasesOther ) do
		if dtype == alias then
			return false
		end
	end
	
	return true
end

local function pageNameBase( page )
	return ( mw.ustring.gsub( page, '^%s*(.+)%s+%b()%s*$', '%1' ) )
end

local function setCategory( title, key )
	if isEmpty( title ) then
		return ''
	end
	if not isEmpty( key ) then
		title = title .. '|' .. key
	end
	
	return string.format( '[[Category:%s]]', title )
end

local function formatText( str, params, frame )
	if isEmpty( str ) then
		return ''
	end
	
	-- Replace spaces after specified patterns to non-breaking spaces
	local nbspPatterns = getConfig( 'nbspPatterns', false )
	if type( nbspPatterns ) ~= 'table' then
		nbspPatterns = {}
	end
	for i, val in ipairs( nbspPatterns ) do
		-- last symbol is Unicode   to allow replacements near   itself
		str = mw.ustring.gsub( str, '(%s)(' .. val .. ') ', '%1%2 ' )
	end
	
	-- Replace provided parameters in form of {param}
	if params ~= nil and type( params ) == 'table' then
		for key, val in pairs( params ) do
			str = mw.ustring.gsub( str, '%{' .. key .. '%}', escapeRepl( val ) )
		end
	end
	
	-- Preprocess if frame was provided
	if frame ~= nil then
		return frame:preprocess( str )
	end
	return str
end

local function formatNestedQuotes( str )
	if str:find( '«' ) ~= nil then
		nestedQuotes = nestedQuotes or require( 'Модуль:Вложенные кавычки' )._main
		return nestedQuotes( str )
	end
	
	return str
end

local function getFixMessage( about )
	local aboutStr = getConfig( 'aboutDefault' )
	if not isEmpty( about ) then
		aboutStr = formatText( getConfig( 'about' ), {
			about = about,
		} )
	end
	
	local page = currentTitle.fullText
	local wlh = mw.title.new( 'Special:WhatLinksHere/' .. page )
	return formatText( getConfig( 'fixLinks' ), {
		about = aboutStr,
		url = wlh:fullUrl( 'namespace=0&hidetrans=1' ),
	} )
end

local function getPseudoBlock( text, class )
	local result = mw.html.create( 'div' )
	result:addClass( 'ts-disambig-block ' .. ( class or '' ) )
	
	result:tag( 'div' )
		:addClass( 'ts-disambig-image' )
	
	result:tag( 'div' )
		:addClass( 'ts-disambig-text' )
		:wikitext( text )
	
	return result
end

local function getProjectLink( node )
	if node == nil then
		return error( 'getProjectLink: no node' )
	end
	
	local projectPage = getConfig( 'projectPage', false )
	local projectIcon = getConfig( 'projectIcon', false )
	local projectLabel = getConfig( 'projectLabel', false )
	if isEmpty( projectPage ) or isEmpty( projectIcon ) or isEmpty( projectLabel ) then
		return
	end
	
	local projTitle = mw.title.new( projectPage )
	node:tag( 'div' )
		:addClass( 'ts-disambig-projectLink plainlinks group-user-show' )
		:tag( 'span' )
			:wikitext( string.format(
				'[[File:%s|20x20px|link=|alt=]][%s %s]',
				projectIcon,
				projTitle:fullUrl(),
				projectLabel
			) )
end

local seeAlsoLinks = {}
local function getSeeAlso( category, isSingle )
	local page = pageNameBase( currentTitle.fullText )
	local seeAlso = formatText( getConfig( 'seeAlso' ) )
	
	-- Get search link with intitle: with titles with words
	local intitleLink = '.'
	if isSingle and mw.ustring.find( page, '%a' ) ~= nil then
		local pageParts = mw.text.split( mw.ustring.gsub( page, '[,«»?!—%%]', '' ), ' ' )
		if not isEmpty( pageParts[ 1 ] ) then
			local intitle = mw.uri.encode( 'intitle:"' .. table.concat( pageParts, '" intitle:"' ) .. '"' )
			local intitleLabel = formatText( getConfig( 'seeAlsoIntitle' ) )
			intitleLink = string.format(
				', [%s %s].',
				mw.title.new( 'Special:Search' ):fullUrl(
					string.format( 'search=%s&sort=create_timestamp_desc', intitle )
				),
				intitleLabel
			)
		end
	end
	
	if not isEmpty( category ) then
		if seeAlsoLinks[ category ] == true then
			return ''
		end
		seeAlsoLinks[ category ] = true
		
		local cat = mw.title.new( 'Category:' .. category )
		local catLabel = formatText( getConfig( 'seeAlsoCategory' ) )
		return string.format(
			'%s [%s <span title="%s">%s</span>]',
			seeAlso,
			cat:fullUrl( 'from=' .. mw.uri.encode( page ) ),
			cat.fullText,
			catLabel
		) .. intitleLink
	end
	
	local pageText = formatNestedQuotes( page )
	local prefixIndexLabel = formatText( getConfig( 'seeAlsoPrefixIndex' ), {
		prefix = pageText,
	} )
	return string.format(
		'%s [[Special:PrefixIndex/%s|%s]]',
		seeAlso,
		page,
		prefixIndexLabel
	) .. intitleLink
end

local function getWrapper( node )
	return node:tag( 'div' )
		:attr( 'id', 'disambig' )
		:attr( 'role', 'note' )
		:addClass( 'ts-disambig-mbox metadata plainlinks' )
end

-- Renders a single disambiguation type message
function p._renderPart( frame, data, isSingle, dtype )
	if frame == nil then
		frame = mw.getCurrentFrame()
	end
	
	if type( data ) ~= 'table' or isEmpty( data[ 'desc' ] ) or isEmpty( data[ 'image' ] ) then
		return p._renderPart( frame, errorData, isSingle, dtype )
	end
	
	local node = mw.html.create( '' )
	local result = node:tag( isSingle and 'div' or 'li' )
	result:addClass( 'ts-disambig-block' )
	
	local imageSize = isSingle and '30x40px' or '20x20px'
	result:tag( 'div' )
		:addClass( 'ts-disambig-image' )
		:wikitext(
			string.format( '[[File:%s|%s|class=noresize|link=|alt=]]', data.image, imageSize )
		)
	
	local seeAlsoCat = data[ 'seeAlsoCategory' ]
	
	local text = formatText( data[ 'desc' ] )
	local textDiv = result:tag( 'div' )
		:addClass( 'ts-disambig-text' )
	if isSingle then
		if text:find( 'class="error"' ) ~= nil then
			text = formatText( text, {
				type = dtype,
			}, frame )
		else
			local fullStop = text:find( '%.$' ) == nil and '.' or ''
			text = string.format( '[[%s|%s%s]]', getConfig( 'helpPage' ), text, fullStop )
		end
		textDiv:tag( 'div' ):wikitext( text )
		
		if dtype == '--category' then
			textDiv:tag( 'div' ):wikitext( formatText( data[ 'about' ] ) )
		else
			textDiv:tag( 'div' ):wikitext( getFixMessage( data[ 'about' ] ) )
		
			node:node( getPseudoBlock( getSeeAlso( seeAlsoCat, isSingle ) ) )
		end
	else
		if data[ 'short' ] then
			text = formatText( data[ 'short' ], nil, frame )
		end
		
		if text:find( 'class="error"' ) ~= nil then
			text = formatText( text, {
				type = dtype,
			}, frame )
		else
			text = formatText( text ) .. '.'
		end
		
		if data[ 'short' ] == '' then
			text = ''
		end
		
		local addendum = not isEmpty( seeAlsoCat ) and getSeeAlso( seeAlsoCat, isSingle ) or ''
		textDiv:wikitext( text .. addendum )
	end
	
	local defaultNs = data[ 'categoryNs' ] or 0
	if currentTitle.namespace == defaultNs then
		-- Assign default category here since it won’t be otherwise
		if isSingle and currentTitle.namespace == 0 then
			textDiv:wikitext( setCategory( defaultData[ 'category' ] ) )
		end
		
		-- Assign a default sort key if title matches a prefix in config
		local defaultSortPrefixes = data[ 'categorySortPrefixes' ]
		if isEmpty( defaultSortPrefixes ) ~= nil and type( defaultSortPrefixes ) == 'table' then
			local cleanTitle = mw.ustring.gsub( pageNameBase( currentTitle.text ), '[«»„“"\']', '' )
			for i, prefix in ipairs( defaultSortPrefixes ) do
				local pMatch = mw.ustring.match( cleanTitle, '^' .. escapePattern( prefix ) .. ' ' )
				if pMatch ~= nil then
					local sortKey = mw.ustring.gsub( cleanTitle, pMatch, '' )
					sortKey = mw.text.trim( sortKey )
					if sortKey ~= "" then
						textDiv:wikitext( frame:preprocess(
							string.format( '{{DEFAULTSORT:%s, %s}}', sortKey, pMatch )
						) )
						break
					end
				end
			end
		end
		
		-- Assign the category
		textDiv:wikitext( setCategory( data[ 'category' ] ) )
		
		if not isEmpty( data[ 'add' ] ) then
			textDiv:wikitext( formatText( data[ 'add' ], nil, frame ) )
		end
	end

	return node
end

-- Chooses which disambiguation type message to show
function p._getPart( frame, dtype, isSingle )
	if isEmpty( dtype ) then
		return p._renderPart( frame, errorData, isSingle, '*' ), dtype
	end
	
	dtype = mw.ustring.lower( dtype )
	if dtype:find( '/' ) then
		dtype = mw.text.split( dtype, '/' )[ 1 ]
	end
	
	local alias = aliases[ dtype ]
	if data[ dtype ] == nil and alias == nil then
		return p._renderPart( frame, errorData, isSingle, dtype ), dtype
	end
	
	if data[ dtype ] ~= nil then
		return p._renderPart( frame, data[ dtype ], isSingle, dtype ), dtype
	end
	
	return p._renderPart( frame, data[ alias ], isSingle, dtype ), alias
end

-- Documentation for [[Template:Disambig]]
function p._doc( frame )
	if frame == nil then
		frame = mw.getCurrentFrame()
	end
	local result = mw.html.create( 'table' )
		:addClass( 'wikitable sortable plainlinks' )
	
	result
		:tag( 'caption' )
		:wikitext( getConfig( 'docTable' ) )
	
	result:tag( 'tr' )
		:tag( 'th' )
			:attr( 'scope', 'col' )
			:wikitext( getConfig( 'docColumnCode' ) )
		:tag( 'th' )
			:attr( 'scope', 'col' )
			:wikitext( getConfig( 'docColumnOutput' ) )
	
	for dtype, val in pairs( data ) do
		local als = val[ 'aliases' ]
		if dtype:find( '^%-%-' ) == nil or dtype == '--other' then
			local displayedType = dtype
			local aliases = ''
			if dtype == '--other' then
				displayedType = als[ 1 ]
			end
			if als then
				for i, alias in ipairs( als ) do
					if dtype ~= '--other' or i ~= 1 then
						aliases = string.format( '%s, <code>%s</code>', aliases, alias )
					end
				end
				aliases = aliases:gsub( ', ', '', 1 )
				aliases = string.format( '<br><i style="opacity:0.85">%s</i>', aliases )
			end
			
			-- Replace <li> to <div> here
			local template = tostring( p._getPart( frame, dtype, false ) )
			template = mw.ustring.gsub( template, '<(/?)li', '<%1div' )
			
			result:tag( 'tr' )
				:tag( 'td' )
					:attr( 'style', 'font-size:85%' )
					:wikitext( string.format( '<code>%s</code>%s', displayedType, aliases ) )
				:tag( 'td' )
					:attr( 'style', 'font-style:italic' )
					:wikitext( template )
		end
	end
	
	return tostring( result )
end

function p.doc( frame )
	return p._doc( frame )
end

-- Generate suggested values in TemplateData blocks
function p._templateData( frame, content )
	if isEmpty( content ) then
		return ''
	end
	if frame == nil then
		frame = mw.getCurrentFrame()
	end
	
	
	local typeCount = 1
	local suggestedValues = {}
	for dtype, val in pairs( data ) do
		if dtype:find( '^%-%-' ) == nil or dtype == '--other' then
			if dtype == '--other' then
				suggestedValues[ typeCount ] = val[ 'aliases' ][ 1 ]
			else
				suggestedValues[ typeCount ] = dtype
			end
			typeCount = typeCount + 1
		end
	end
	suggestedValues = table.concat( suggestedValues, '", "' )
	
	content = mw.ustring.gsub(
		content,
		'"suggestedvalues": %[%]',
		string.format( '"suggestedvalues": ["%s"]', suggestedValues )
	)
	return frame:extensionTag{
		name = 'templatedata',
		content = content,
	}
end

function p.templateData( frame )
	local args = getArgs( frame )
	local content = args[ 1 ]
	
	return p._templateData( frame, content )
end

-- Checks for errors in page code
local function checkErrors( frame, args )
	if currentTitle.namespace ~= 0 then
		return ''
	end
	
	-- Check if current page is a redirect
	if currentTitle.isRedirect then
		return setCategory( errorData[ 'category' ], '*' )
	end
	
	local content = currentTitle:getContent()
	if isEmpty( content ) then
		return ''
	end
	content = mw.text.trim( content )

	-- Case-insensitive template name
	local template = frame:getParent():getTitle()
	local mwTitle = mw.title.new( template )
	local templatePattern = string.format(
		"[%s%s]%s",
		mw.ustring.upper( mw.ustring.sub( mwTitle.text, 1, 1 ) ),
		mw.ustring.lower( mw.ustring.sub( mwTitle.text, 1, 1 ) ),
		mw.ustring.gsub( escapePattern( mwTitle.text ), '^.', '' )
	)
	
	-- Check if it is at the start
	if mw.ustring.find( content, '{{' .. templatePattern ) == 1 then
		return setCategory( errorData[ 'category' ], '↓' )
	end
	
	-- Check if it is not the last template
	if mw.ustring.find( content, '{{' .. templatePattern .. '[^%}]-}}.-{{' ) ~= nil then
		return setCategory( errorData[ 'category' ], '↓' )
	end
	
	for key, val in pairs( args ) do
		if type( key ) ~= 'number' then
			return setCategory( errorData[ 'category' ], '~' )
		end
	end
	
	return ''
end

-- Protects templates from substitution by substituting them with their own parameters
function p._substing( frame, args, template )
	if args == nil then
		args = getArgs( frame, {
			parentOnly = true,
		} )
	end
	local mTemplateInvocation = require( 'Module:Template invocation' )
	local name = mTemplateInvocation.name( template or frame:getParent():getTitle() )
	
	return mTemplateInvocation.invocation( name, args )
end

-- Renders {{Disambig}} template
function p._main( frame, args )
	if frame == nil then
		frame = mw.getCurrentFrame()
	end
	local result = mw.html.create( 'div' )
		:addClass( 'ts-disambig' )
	
	local reflist = result:tag( 'div' )
		:addClass( 'ts-disambig-reflist' )
	
	reflist:tag( 'div' )
		:attr( 'role', 'heading' )
		:attr( 'aria-level', 2 )
		:wikitext( getConfig( 'reflistLabel' ) )
	reflist:wikitext( frame:extensionTag{
		name = 'references',
	} )
	
	local disambig = getWrapper( result )
	
	if isEmpty( args[ 1 ] ) then
		local hasTypes = false
		for i, dtype in pairs( args ) do
			if tonumber( i ) ~= nil and not isEmpty( dtype ) then
				hasTypes = true
				break
			end
		end
		
		if not hasTypes then
			disambig:node( p._getPart( frame, '--disambig', true ) )
		else
			disambig:node( p._renderPart( frame, errorData, true, '()' ) )
		end
	elseif isEmpty( args[ 2 ] ) then
		local dtype = args[ 1 ]
		disambig:node( p._getPart( frame, dtype, true ) )
	else
		disambig:node( p._getPart( frame, '--disambig', true ) )
		
		disambig:node( getPseudoBlock( getConfig( 'typesLabel' ), 'ts-disambig-listIntro' ) )
		
		local list = disambig:tag( 'ul' )
			:attr( 'role', 'list' )
			:addClass( 'ts-disambig-list' )
		local hasOther = false
		
		local usedTypes = {}
		for i, dtype in ipairs( args ) do
			if isNotOther( dtype ) then
				local part, usedType = p._getPart( frame, dtype, false )
				if not usedTypes[ usedType ] then
					list:node( part )
					usedTypes[ usedType ] = true
				else
					list:node( p._renderPart( frame, errorData, false, dtype ) )
				end
			else
				hasOther = true
			end
		end
		
		if hasOther then
			list:node( p._getPart( frame, '--other', false ) )
		end
	end
	
	getProjectLink( result )
	
	result = tostring( result )
	if currentTitle.namespace == 0 then
		result = '__DISAMBIG__' .. frame:extensionTag{
			name = 'indicator',
			content = string.format( '[[File:Disambig.svg|20px|link=%s|%s]]', getConfig( 'helpPage' ), getConfig( 'helpLabel' ) ),
			args = { name = '0-disambig' },
		} .. result
	end
	
	return frame:extensionTag{
		name = 'templatestyles',
		args = { src = templateStylesPage },
	} .. result
end

function p.main( frame )
	if mw.isSubsting() then
		return p._substing( frame )
	end
	
	local args = getArgs( frame )
	return p._main( frame, args ) .. checkErrors( frame, args )
end

-- Renders alias templates with substing capabilities
function p.alias( frame )
	local args = getArgs( frame )
	local template = args[ '$template' ]
	
	args[ '$template' ] = nil
	if mw.isSubsting() then
		return p._substing( frame, args, 'Template:' .. template )
	end
	
	return frame:expandTemplate{
		title = template,
		args = { args[ 1 ] },
	} .. checkErrors( frame, args )
end

-- Renders {{Category disambiguation}} template
function p._category( frame, args )
	if frame == nil then
		frame = mw.getCurrentFrame()
	end
	
	if isEmpty( args[ 2 ] ) and isEmpty( args[ 4 ] ) then
		return error( getConfig( 'categoryError' ) )
	end
	
	local params = {}
	for key, val in pairs( args ) do
		if type( key ) == 'number' then
			if isEmpty( params[ key - 1 ] ) then
				params[ key - 1 ] = params[ key - 1 ] or ''
			end
			params[ key ] = val
		end
	end
	
	local result = mw.html.create( 'div' )
		:addClass( 'ts-disambig' )
	local disambig = getWrapper( result )
	
	disambig:node( p._getPart( frame, '--category', true ) )
	
	disambig:node( getPseudoBlock( getConfig( 'categoryIntro' ), 'ts-disambig-listIntro' ) )
	
	local list = disambig:tag( 'ul' )
		:attr( 'role', 'list' )
		:addClass( 'ts-disambig-list ts-disambig-categoryList' )
	for i, catName in ipairs( params ) do
		if i % 2 == 0 and not isEmpty( catName ) then
			local catLabel = formatNestedQuotes( catName )
			local text = string.format( '[[:Category:%s|%s]]', catName, catLabel )

			local label = args[ i - 1 ]
			if not isEmpty( label ) then
				text = string.format(
					'<i>%s</i> — %s ',
					label,
					formatText( getConfig( 'categoryInLabel' ), {
						category = text,
					} )
				)
			else
				text = string.format( '<b>%s</b>', text )
			end
			
			list:node( getPseudoBlock( text ) )
		end
	end
	
	getProjectLink( result )
	
	result = tostring( result )
	if currentTitle.namespace == 14 then
		result = '__DISAMBIG__' .. '__EXPECTUNUSEDCATEGORY__' .. result
	end
	
	return frame:extensionTag{
		name = 'templatestyles',
		args = { src = templateStylesPage },
	} .. result
end

function p.category( frame )
	if mw.isSubsting() then
		return p._substing( frame )
	end
	
	return p._category( frame, getArgs( frame ) )
end

return p