Модуль:Вложенный список (Bk;rl,&Flk'yuudw vhnvkt)

Перейти к навигации Перейти к поиску
Документация

Данный модуль реализует шаблон {{Вложенный список}}. После перехода на модуль стало возможно заменить старый код шаблона на более доступный и семантичный и начать отслеживать ошибки в коде выводимых страниц.

Код вызова модуля: {{#invoke:Вложенный список|main}}.

Категории

{{Вложенный список}} добавляет ряд штрафных категорий. Для упрощения их исправления модулем добавляются отображаемые при предпросмотре сообщения со ссылками на страницы, из-за которых шаблоном добавляется штрафная категория.

  • Википедия:Страницы с ошибками шаблона Вложенный список (136) — ставится в случаях:
    • несуществования страницы или отсутствия текста при попытке вставить вложенный список (например, в случае некорректной разметки в нём).
    • наличия технических ошибок «Медиавики» в выводе вложенного списка.
    • наличия в выводе шаблонов:
    • ошибок в разметке списков с точки зрения вики-кода и доступности:
      • начала вложенного списка не с разметки элемента списка *.
      • вложенных списков с неправильной вложенностью (через :* или с ** или с ; в начале).
    • вложенных списков с заголовками 1-го и 2-го уровня (==) в выводе.
    • вложенных списков с вертикальной чертой (----) в выводе.
    • вложенных списков с ссылками в тексте (все ссылки должны оформлены через <ref>).
    • вложенных списков, в которых вложенные списки не входят в подсписки ({{NL|Пример}} вместо * {{NL|Пример}}).
  • Википедия:Страницы с шаблоном Вложенный список с неверным типом страницы значений (1524) ставится в случаях, когда использование вложенного списка противоречит руководству Википедия:Неоднозначность#Вложенные списки (страница не входит в категории тёзок и однофамильцев).
    В некоторых случаях подобное исправляется через объединение подстраницы вложенного списка с основной страницей, в других через замену на простую ссылку или исправление типов на подстранице вложенного списка.
  • Википедия:Страницы с шаблоном Вложенный список без ссылок (224) ставится в случаях, когда в коде вложенного списка нет ссылок на другие страницы.
    В некоторых случаях такая категория может ставиться, если в начале пункта списка до ссылки есть какой-то произвольный текст. В таких случаях этот текст нужно разместить после ссылки или после тире.
require( 'strict' )
--
-- Implements {{Вложенный список}} in one module
-- Allows us to potentially move towards a more accessible markup for the template when the time is right
-- Previously, the template broke the lists {{Вложенный список}} or {{NL2}} or {{NL3}} are in
-- Heavily borrows from https://en.wikipedia.org/wiki/Module:Excerpt_slideshow
--
local getArgs = require( 'Module:Arguments' ).getArgs

local templatePageName = 'Вложенный список'
local modulePageName = 'Вложенный список'
local disambigPageSuffix = ' (значения)'
local editLinkText = 'править'

-- [[:Категория:Википедия:Страницы с ошибками шаблона Вложенный список]]
local errorCat = 'Википедия:Страницы с ошибками шаблона Вложенный список'

-- [[:Категория:Википедия:Страницы с шаблоном Вложенный список без ссылок]]
-- Remove the line to stop tracking this category
local linklessCat = 'Википедия:Страницы с шаблоном Вложенный список без ссылок'

-- [[:Категория:Википедия:Страницы с шаблоном Вложенный список с неверным типом страницы значений]]
-- Remove the line to stop tracking this category
local wrongTypeCat = 'Википедия:Страницы с шаблоном Вложенный список с неверным типом страницы значений'

local goodTypeCategories = {
	[ 'Страницы значений:Тёзки' ] = true,
	[ 'Страницы значений:Однофамильцы' ] = true,
	[ 'Страницы значений:Однофамильцы-тёзки' ] = true,
	[ 'Страницы значений:Полные тёзки' ] = true,
}

local errorMsgs = {
	noTitle = 'Нет названия страницы.',
	invalidTitle = 'Неправильное название страницы <code>%s</code>.',
	noContent = 'Ошибка при включении страницы «[[%s]]».',
	hasErrors = 'Ошибка в коде вложенного списка «[[%s]]» ([[Шаблон:Вложенный список#Штрафные категории|см. документацию]]).',
	noLinks = 'На странице «[[%s]]» нет ссылок на другие страницы.',
	notDisambig = 'На странице «[[%s]]» находится статья.',
	wrongType = 'Страница «[[%s]]» не является страницей тёзок или однофамильцев. См. [[ВП:Н/ВС]].',
}

-- Adds a replacement to a redirected variant when substed
local substWithRedirects = true

local moduleInvocationCode = '{{#invoke:' .. modulePageName .. '|main|'
local nestedCheckArg = '$$no_checks'
local currentTitle = mw.title.getCurrentTitle()

local p = {}

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

local function addWarning( title, message )
	if title ~= nil and message == nil then
		message = string.format( noContent, title )
	end
	if title ~= nil and message ~= nil then
		message = string.format( message, title )
	end
	mw.addWarning( string.format( '[[Template:%s|%s]]: %s', templatePageName, templatePageName, message ) )
end

local function templatePattern( str )
	return string.format(
		"[%s%s]%s",
		mw.ustring.upper( mw.ustring.sub( str, 1, 1 ) ),
		mw.ustring.lower( mw.ustring.sub( str, 1, 1 ) ),
		mw.ustring.gsub( str, '^.', '' )
	)
end

local function noGoodCategories( mwTitle )
	if mwTitle == nil then
		return true
	end
	
	local success, data = pcall( function()
		return mwTitle.categories
	end )
	if not success then
		return false
	end
	
	for _, category in ipairs( data ) do
		if goodTypeCategories[ category ] == true then
			return false
		end
	end
	
	return true
end

local function findTwice( str, pattern )
	if isEmpty( str ) then
		return 0
	end
	
	local first, firstPos = mw.ustring.find( str, pattern )
	if first == nil then
		return 0
	end
	
	return mw.ustring.find( str, pattern, firstPos ) ~= nil and 2 or 1
end

local function checkForErrors( sublist )
	-- Templates/syntax that should not be there
	if
		mw.ustring.find( sublist, 'class="error' ) ~= nil
		or mw.ustring.find( sublist, 'id="disambig"' ) ~= nil -- {{disambig}}
		or mw.ustring.find( sublist, 'class="hatnote' ) ~= nil -- {{hatnote}}
		or mw.ustring.find( sublist, ' ambox ambox-' ) ~= nil -- {{ambox}}
		or mw.ustring.find( sublist, '__TOC__' ) ~= nil -- __TOC__
		or mw.ustring.find( sublist, 'UNIQ--references' ) ~= nil -- {{references}}
	then
		return true, '❌'
	end
	
	-- Incorrect list markup
	if
		mw.ustring.find( sublist, '^%s*%*' ) ~= 1 -- does not start with a list
		or mw.ustring.find( sublist, '^%s*%*%*' ) ~= nil
		or mw.ustring.find( sublist, '\n%:%*' ) ~= nil
	then
		return true, '*'
	end
	
	-- Other incorrect markup
	if
		mw.ustring.find( sublist, '\n;%s*[А-ЯA-Z]' ) ~= nil -- incorrect bold text markup
		or mw.ustring.find( sublist, '\n=[^=]' ) ~= nil -- 1st level heading
		or mw.ustring.find( sublist, '\n==[^=]' ) ~= nil -- 2nd level heading
		or mw.ustring.find( sublist, '\n%-%-%-%-' ) ~= nil -- contains ---- in code
		or mw.ustring.find( sublist, '\n<div class="ts%-NL">' ) ~= nil -- {{NL}} after a line break
	then
		return true, '🙈'
	end
	
	-- links inline
	if mw.ustring.find( sublist, '[%[%s]https?:%/%/' ) ~= nil then
		return true, '🔗'
	end
	
	return false, ' '
end

local function getEditLink( mwTitle )
	local span = mw.html.create( 'span' )
		:addClass( 'ts-NL-edit mw-editsection-like plainlinks noprint navigation-not-searchable group-user-show' )
		:wikitext( string.format(
			'<span class="mw-editsection-bracket">[</span>[%s %s]<span class="mw-editsection-bracket">]</span>',
			mwTitle:fullUrl( 'action=edit' ),
			editLinkText
		) )
	
	return tostring( span )
end

local function getIntro( title, text, addendum, mwTitle )
	if isEmpty( title ) then
		return error( 'getIntro: no title' )
	end
	text = text and mw.text.trim( text ) or ''
	addendum = addendum or ''
	
	-- Remove bracketed suffix automatically from a disambig page name
	if isEmpty( text ) and mw.ustring.find( title, disambigPageSuffix, 1, true ) ~= nil then
		text = mw.ustring.gsub( title, '^%s*(.+)%s+%b()%s*$', '%1' )
	end
	
	if not isEmpty( text ) then
		text = '|' .. text
	end
	
	local colon = ':'
	if not isEmpty( addendum ) then
		colon = ''
		addendum = string.format( ' %s:', addendum )
	end
	
	local intro = string.format(
		'<span class="dabhide">[[%s%s]]%s</span>%s',
		title,
		text,
		colon,
		addendum
	)
	
	return intro .. getEditLink( mwTitle )
end

local function getError( title, msg, setCategory )
	if setCategory == nil then
		setCategory = true
	end
	
	local errorText = msg
	if title ~= nil and title ~= '' then
		errorText = string.format( msg, title )
	end
	
	addWarning( nil, errorText )
	
	return '<div class="error">' .. errorText .. '</div>'
		.. ( setCategory and string.format( '[[Category:%s]]', errorCat ) or '' )
end

-- Prevent template loop by replacing template calls to the calls to this module
local function replaceSubtemplates( content )
	if isEmpty( content ) then
		return content
	end
	
	local invocation = moduleInvocationCode .. nestedCheckArg .. '=1|'
	content = mw.ustring.gsub( content, '{{' .. templatePattern( templatePageName ) .. '/?[23]?|', '{{NL|' )
	content = mw.ustring.gsub( content, '{{' .. templatePattern( 'NL' ) .. '/?[23]?%|', invocation )
	
	return content
end

--[[
	@param {String} wikitext: Wikitext of just the list (i.e. each line is a list item)
	@param {String} symbol:   Special character used in the wikitext markup for the list, e.g. '*' or '#'
	@param {String} outerTag: Text portion of the tag for each list or sublist, e.g. 'ul' or 'ol'
	@param {String} innerTag: Text portion of the tag for each list item, e.g. 'li'
]]
local wikitextToHtmlList = function( wikitext, symbol, outerTag, innerTag )
	local listParts = {}
	for level, item in mw.ustring.gmatch( '\n' .. wikitext .. '\n', '\n(%' .. symbol .. '+)(.-)%f[\n]' ) do
	    table.insert( listParts, { level=level, item=item } )
	end
	table.insert( listParts, { level='', item='' } )
	
	local htmlList = {}
	for i, this in ipairs( listParts ) do
		local isFirstItem = ( i == 1 )
		local isLastItem = ( i == #listParts )
	    local lastLevel = isFirstItem and '' or listParts[ i - 1 ][ 'level' ]
	    local tags
	    if #lastLevel == #this.level then
	    	tags = '</'..innerTag..'><'..innerTag..'>'
	    elseif #this.level > #lastLevel then
	    	tags = string.rep( '<'..outerTag..'><'..innerTag..'>', #this.level - #lastLevel )
	    elseif isLastItem then
	    	tags = string.rep( '</'..innerTag..'></'..outerTag..'>', #lastLevel )
	    else -- ( #this.level < #lastLevel ) and not last item
	    	tags = string.rep( '</'..innerTag..'></'..outerTag..'>', #lastLevel - #this.level ) .. '</'..innerTag..'><'..innerTag..'>'
	    end
	    table.insert( htmlList, tags .. this.item )
	end
	return table.concat( htmlList )
end


--[[
	@param {String} wikitext: Wikitext excerpt containg zero or more lists
	@param {String} symbol:   Special character used in the wikitext markup for the list, e.g. '*' or '#'
	@param {String} outerTag: Text portion of the tag for each list or sublist, e.g. 'ul' or 'ol'
	@param {String} innerTag: Text portion of the tag for each list item, e.g. 'li'
]]
local gsubWikitextLists = function( wikitext, symbol, outerTag, innerTag )
	-- temporarily remove list linebreaks... 
	wikitext = mw.ustring.gsub( wikitext .. '\n', '\n%' .. symbol, '¿¿¿' .. symbol )
	-- ...so we can grab the whole list (and just the list)...
	return mw.ustring.gsub(
		wikitext,
		'¿¿¿%'..symbol..'[^\n]+', 
		function( listWikitext )
			-- ...and then reinstate linebreaks...
			listWikitext = mw.ustring.gsub( listWikitext, '¿¿¿%' .. symbol, '\n' .. symbol )
			-- ...and finally do the conversion
			return wikitextToHtmlList( listWikitext, symbol, outerTag, innerTag )
		end
	)
end

-- Protects the templates from substitution by substituting them with their own parameters
function p._substing( frame )
	local args = getArgs( frame, {
		parentOnly = true,
	} )
	local mTemplateInvocation = require( 'Module:Template invocation' )
	local name = mTemplateInvocation.name( frame:getParent():getTitle() )
	
	if substWithRedirects then
		name = mw.ustring.gsub( name, 'Вложенный список/?%d?', 'NL' )
		name = mw.ustring.gsub( name, 'NL/?%d?', 'NL' )
	end
	
	return mTemplateInvocation.invocation( name, args )
end


function p.main( frame )
	if mw.isSubsting() then
		return p._substing( frame )
	end
	
	local args = getArgs( frame )
	local title = args[ 1 ]
	local linkText = args[ 2 ]
	local appendedText = args[ 3 ]
	
	if isEmpty( title ) then
		return getError( nil, errorMsgs.noTitle, currentTitle.namespace == 0 )
	end
	
	-- frame:expandTemplate is used because mw.title:getContent() does not handle redirects
	local mwTitle = mw.title.new( title )
	
	-- Invalid title
	if mwTitle == nil then
		return getError( title, errorMsgs.invalidTitle )
	end
	
	local origMwTitle = mwTitle
	if mwTitle.redirectTarget then
		mwTitle = mwTitle.redirectTarget
	end
	
	-- The page or redirect target is equal to current page
	if mw.title.equals( mwTitle, currentTitle ) then
		return getIntro( title, linkText, appendedText, origMwTitle )
			.. getError( title, errorMsgs.noContent )
	end
	
	local content = mwTitle:getContent()
	content = replaceSubtemplates( content )
	local sublist = frame:preprocess( content )
	
	-- The page returns empty list
	if isEmpty( content ) or isEmpty( sublist ) or mw.text.trim( sublist ) == '' then
		return getIntro( title, linkText, appendedText, origMwTitle )
			.. getError( title, errorMsgs.noContent )
	end
	
	-- Reject transclusions that look like articles
	if mw.ustring.find( sublist, ' class="infobox' ) ~= nil
		or mw.ustring.find( sublist, ' class="navbox' ) ~= nil then
		return getIntro( title, linkText, appendedText, origMwTitle )
			.. getError( title, errorMsgs.notDisambig )
	end
	
	-- Remove 3rd+ level subheadings for easier section insertion (before error checks)
	sublist = mw.ustring.gsub( sublist, '\n(===+)(.-)%1\n-', '' )
	
	-- Check sublist for wikitext markup errors
	local hasErrors, errorType = checkForErrors( sublist )
	
	-- Replace list markers with HTML list openers
	sublist = gsubWikitextLists( '\n' .. sublist, '*', 'ul', 'li' )
	sublist = gsubWikitextLists( '\n' .. sublist, '#', 'ol', 'li' )
	
	-- Remove the bold text around links automatically
	sublist = mw.ustring.gsub( sublist, "<li>%s*'''%s*%[%[([^%]]+)%]%]%s*'''", '<li>[[%1]]' )
	
	-- Trim and replace double line breaks to avoid breaking the list
	sublist = mw.text.trim( sublist )
	sublist = mw.ustring.gsub( sublist, '\n\n', '<p>' )
	
	-- Merge adjacent lists
	sublist = mw.ustring.gsub( sublist, '</ul>\n-<ul>', '' )
	
	-- Replace remaining
	sublist = mw.ustring.gsub( sublist, '\n', '<br>' )
	
	-- Disable nested checks
	local notNestedCheck = isEmpty( args[ nestedCheckArg ] )
	
	-- Check if the included page has more than two links
	if linklessCat then
		-- Two plain wikitext links
		local linkCount = findTwice( mw.text.killMarkers( content ), '%*%s*\'*["«„]?\'*%[%[' )
		if linkCount < 2 then
			-- Two module invocations mean two different links
			linkCount = linkCount + findTwice( content, moduleInvocationCode )
		end
		if linkCount < 2 then
			-- [[Модуль:Не переведено]]
			linkCount = linkCount
				+ findTwice( sublist, ' версия статьи «' )
				+ findTwice( sublist, ' title="Элемент статьи «' )
		end
		
		if linkCount < 2 then
			-- Only add a category on the page itself
			if notNestedCheck then
				sublist = sublist .. string.format( '[[Category:%s]]', linklessCat )
			end
			addWarning( title, errorMsgs.noLinks )
		end
	end
	
	-- Check if the included page has wrong type
	if wrongTypeCat and notNestedCheck then
		if noGoodCategories( mwTitle ) then
			sublist = sublist .. string.format( '[[Category:%s]]', wrongTypeCat )
			addWarning( title, errorMsgs.wrongType )
		end
	end
	
	if hasErrors then
		sublist = sublist .. string.format( '[[Category:%s|%s]]', errorCat, errorType )
		addWarning( title, errorMsgs.hasErrors )
	end
	
	local intro = getIntro( title, linkText, appendedText, mwTitle )
	return '<div class="ts-NL">' .. intro .. sublist .. '</div>'
end

return p