Модуль:Topic monitoring

Материал из Викиновостей, свободного источника новостей
Документация

Реализация шаблона {{Мониторинг тем}}. Основан на модуле Get page content.

См. также

local p = {}

local function conceal(text, class)
	 class = class or ''
	 return '<span style="display:none; speak:none;" class="topicWatch-' .. class .. '">' .. text .. '</span>' -- содержимое шаблона {{~}}
end

local function findInTable(table, value)
	for k, v in pairs(table) do
		if v == value then
			return true
		end
	end
	return false
end

local function cleanSectionHeading(heading)
	-- The following patterns reproduce [[Участник:Jack who built the house/transferHeadingToSummary.js]]
	heading = mw.ustring.gsub(heading, '%[%[:?[^|%]]*|([^%]]*)%]%]', '%1')
	heading = mw.ustring.gsub(heading, '%[%[:?([^%]]*)%]%]', '%1')
	heading = mw.ustring.gsub(heading, "'''(.-)'''", '%1')
	heading = mw.ustring.gsub(heading, "''(.-)''", '%1')
	heading = mw.ustring.gsub(heading, '</?%w+ ?/?>', '')
	heading = mw.ustring.gsub(heading, '<%w+ [%w ]-=[^<>]->', '')
	heading = mw.ustring.gsub(heading, '  +', ' ')
	heading = mw.text.trim(heading)
	return heading
end

local function sectionHeadingToLink(sectionHeading)
	local sectionHeadingLink = sectionHeading
	-- The following reproduces processURI function of [[Участник:Jack who built the house/copyWikilinks.js]]
	--[[sectionHeadingLink = mw.ustring.gsub(sectionHeadingLink, '<', '%%3C')
	sectionHeadingLink = mw.ustring.gsub(sectionHeadingLink, '>', '%%3E')
	sectionHeadingLink = mw.ustring.gsub(sectionHeadingLink, '%[', '%%5B')
	sectionHeadingLink = mw.ustring.gsub(sectionHeadingLink, '%]', '%%5D')
	sectionHeadingLink = mw.ustring.gsub(sectionHeadingLink, '{', '%%7B')
	sectionHeadingLink = mw.ustring.gsub(sectionHeadingLink, '|', '%%7C')
	sectionHeadingLink = mw.ustring.gsub(sectionHeadingLink, '}', '%%7D')
	sectionHeadingLink = mw.ustring.gsub(sectionHeadingLink, ' ', '.C2.A0')]]
	return '#' .. sectionHeadingLink
end

local function killHeadingMarkers(content)
	content = mw.ustring.gsub(
		content,
		string.char(127) .. '\'"`UNIQ%-%-h%-%d+%-%-QINU`"\'' .. string.char(127),
		''
	)
	return content
end

function p.main(frame)
	if not getArgs then
		getArgs = require('Модуль:Arguments').getArgs
	end
	local yesno = require('Module:Yesno')
	local args = getArgs(frame, {removeBlanks = false})
	local ru = mw.getLanguage('ru')
	local errorInfo = {}
	
	-- обрабатываем параметры
	local talkpageMode = args['режим'] == 'страницы'
	local afdMode = args['режим'] == 'КУ'
	local pageListMode = args['режим'] == 'список страниц'
	local standardMode
	if not talkpageMode and not afdMode and not pageListMode then
		standardMode = true
	end
	
	local separateTopicListPage, separateTopicList
	if standardMode and not args[1] then
		separateTopicListPage = mw.title.new(args['список'] or mw.title.getCurrentTitle().prefixedText .. '/список')
		if separateTopicListPage.exists then
			local content = separateTopicListPage:getContent()
			if content:find('{{') then
				content = frame:preprocess(content)
			end
			separateTopicList = mw.text.split(content, '\n')
		end
		if not separateTopicList or (separateTopicList[1] == '' and not separateTopicList[2]) then
			separateTopicList = {}
		end
	end
	
	local afdItems, nominationsNum
	if afdMode then
		local afdListPage = mw.title.new('Википедия:К удалению') -- Участник:Jack who built the house/песочница2
		nominationsNum = tonumber(args['номинаций']) or 50
		
		afdItems = {}
		local afdListContent = afdListPage:getContent()
		afdListContent = mw.ustring.gsub(afdListContent, '<small>.-<\/small>', '')
		afdListContent = mw.ustring.gsub(afdListContent, '<s>.-<\/s>', '')
		local iterator = mw.ustring.gmatch(afdListContent, '{{Удаление статей|([^|]+)|([^\n]+)}}')
		reversedIteratorTable = {}
		for day, topics in iterator do
			table.insert(reversedIteratorTable, 1, {day = day, topics = topics})
		end
		for k, v in pairs(reversedIteratorTable) do
			day = v.day
			topics = v.topics
			local afdPage = 'Википедия:К удалению/' .. ru:formatDate('j xg Y', day)
			
			local iterator = mw.text.gsplit(topics, ' • ', true)
			for v2 in iterator do
				if v2 ~= '' then
					v2 = cleanSectionHeading(v2)
					table.insert(afdItems, afdPage .. '#' .. v2)
				end
				if #afdItems >= nominationsNum + 50 then break end
			end
			if #afdItems >= nominationsNum + 50 then break end
		end
	end
	
	local topicsToShow = tonumber(args['тем']) or 10000
	
	-- число тем, содержимое которых выводить непосредственно на странице, а не ссылаться на другую
	local fullTopicsToShow = not pageListMode and (
			   tonumber(args['полных тем'])
			or (
				afdMode and 1000 or 10
			   )
		)
	
	if topicsToShow == 0 and fullTopicsToShow == 0 then return '' end
	
	local style = args['стиль'] == 'форумный' and args['стиль'] or 'wikitable';

	local showTopicCount
	if args['показывать количество тем'] then
		showTopicCount = yesno(args['показывать количество тем'], true)
	else
		showTopicCount = true
	end
	
	local reloadLink
	if args['обновить'] then
		reloadLink = yesno(args['обновить'], true)
	else
		reloadLink = true
	end
	
	local sortFullTopics, sortTopics
	if afdMode then
		sortFullTopics = false
		sortTopics = false
	elseif args['сортировка полных тем'] then
		sortFullTopics = yesno(args['сортировка полных тем'], false)
		sortTopics = true
	else
		sortFullTopics = true
		sortTopics = true
	end
	
	-- проверяем и обрабатываем части названий, чистим от дубликатов
	-- wrongItems — элементы, с которыми что-то не так уже на этапе распознавания адреса
	-- itemsToRemove — элементы, которые предлагается удалить из списка отслеживания
	-- errorInfo (объявлено выше) — сообщения об ошибке
	local items, wrongItems, itemsToRemove = {}, {}, {}
	for k, v in pairs(separateTopicList or afdItems or args) do
		if type(k) == 'number' then
			if v ~= '' then
				local newItem = {
					originalFullTitle = v,
					titleObject = mw.title.new(v),
				}
				if newItem.titleObject and newItem.titleObject.prefixedText ~= '' then
					if newItem.titleObject.fragment ~= '' then
						newItem.sectionHeading = mw.text.encode(mw.uri.decode(newItem.titleObject.fragment), '<>%[%]{|}')
						if not talkpageMode and not pageListMode then
							newItem.resultMode = false
							newItem.sectionHeading = mw.ustring.gsub(newItem.sectionHeading, '\\итог$', function (s)
								newItem.resultMode = true
								return ''
							end)
							newItem.sectionHeading = mw.ustring.gsub(newItem.sectionHeading, '\\\\\\.*$', function (s) -- \\\Итог
								newItem.subsectionHeading = s
								return ''
							end)
						end
					end
					
					if not newItem.sectionHeading and not talkpageMode and not pageListMode then
						table.insert(wrongItems, v)
						table.insert(itemsToRemove, v)
					else
						local found = false
						for k2, v2 in pairs(items) do
							if v2.titleObject.prefixedText == newItem.titleObject.prefixedText and (not newItem.sectionHeading or v2.sectionHeading == newItem.sectionHeading) and (not newItem.subsectionHeading or v2.subsectionHeading == newItem.subsectionHeading) then
								found = true
								break
							end
						end
						if not found then
							table.insert(items, newItem)
						else
							table.insert(itemsToRemove, v)
						end
					end
				else
					table.insert(wrongItems, v)
					table.insert(itemsToRemove, v)
				end
			else
				table.insert(itemsToRemove, v)
			end
		end
	end
	
	if #wrongItems > 0 then
		local wrongItemsString =
			    #wrongItems == 1
			and 'Что-то не так с элементом'
			 or 'Что-то не так со следующими элементами:'
		for k, v in pairs(wrongItems) do
			if k ~= 1 then
				wrongItemsString = wrongItemsString .. ', '
			end
			wrongItemsString = wrongItemsString .. ' «' .. v .. '»'
		end
		wrongItemsString = wrongItemsString .. '.'
		table.insert(errorInfo, wrongItemsString)
	end
	
	-- запрашиваем данные
	local allData = {}
	local talkpageCount
	local onlyPageTitle
	local pageListData
	if talkpageMode then
		talkpageCount = 0
		local parse_talkpage_content = require('Модуль:Get page content')._parse_talkpage_content
		
		for k, v in pairs(items) do
			local argsToPass = {}
			argsToPass[1] = v.titleObject
			argsToPass['короткие заголовки'] = not items[2] and true or false
			if fullTopicsToShow == 0 then
				argsToPass['только статистика'] = true
			end
			
			local allDataFromPage = parse_talkpage_content(argsToPass)
			if type(allDataFromPage) == 'table' then
				if allDataFromPage[1] then
					local pageTitle = v.titleObject.prefixedText
					talkpageCount = talkpageCount + 1
					for k2, v2 in pairs(allDataFromPage) do
						v2.pageTitle = pageTitle
						v2.canonicalPath = v2.pageTitle .. '#' .. v2.sectionHeading .. (v2.subsectionHeading and '\\\\\\' .. v2.subsectionHeading or '')
						table.insert(allData, v2)
					end
					if talkpageCount == 1 then
						onlyPageTitle = pageTitle
					end
				elseif allDataFromPage.pageNotExistMessage then
					table.insert(itemsToRemove, v.originalFullTitle)
					table.insert(errorInfo, allDataFromPage.pageNotExistMessage)
				end
			else
				table.insert(errorInfo, 'Не удалось получить данные о странице «' .. v.titleObject.prefixedText .. '» от модуля получения содержимого страницы.')
			end
		end
	elseif pageListMode then
		pageListData = {}
		local parse_talkpage_content = require('Модуль:Get page content')._parse_talkpage_content
		
		for k, v in pairs(items) do
			local argsToPass = {}
			argsToPass[1] = v.titleObject
			argsToPass['только статистика'] = true
			argsToPass['режим списков страниц'] = true
			
			local allDataFromPage = parse_talkpage_content(argsToPass)
			if type(allDataFromPage) == 'table' then
				if allDataFromPage[1] then
					local pageTitle = v.titleObject.prefixedText
					local pageData = {
						num = k,
						pageTitle = pageTitle,
						msgCount = 0,
						topicCount = 0,
						lastMsgDateTimestamp = 0
					}
					for k2, v2 in pairs(allDataFromPage) do
						if v2.msgCount then
							pageData.msgCount = pageData.msgCount + v2.msgCount
							if v2.lastMsgDateTimestamp > pageData.lastMsgDateTimestamp then
								--pageData.lastMsgDate = v2.lastMsgDate
								pageData.lastMsgDateString = v2.lastMsgDateString
								pageData.lastMsgDateTimestamp = v2.lastMsgDateTimestamp
								pageData.lastMsgAuthor = v2.lastMsgAuthor
							end
						end
						pageData.topicCount = pageData.topicCount + 1
						if v2.warningHeading and not pageData.warningHeading then
							pageData.warningHeading = v2.warningHeading
						end
					end
					table.insert(pageListData, pageData)
				elseif allDataFromPage.pageNotExistMessage then
					table.insert(itemsToRemove, v.originalFullTitle)
					table.insert(errorInfo, allDataFromPage.pageNotExistMessage)
				end
			else
				table.insert(errorInfo, 'Не удалось получить данные о странице «' .. v.titleObject.prefixedText .. '» от модуля получения содержимого страницы.')
			end
		end
	else
		local parse_section_content = require('Модуль:Get page content')._parse_section_content
		
		for k, v in pairs(items) do
			local argsToPass = {}
			table.insert(argsToPass, v.titleObject)
			table.insert(argsToPass, v.sectionHeading)
			argsToPass['итог'] = v.resultMode
			argsToPass['подраздел'] = v.subsectionHeading
			argsToPass['как данные'] = true
			if fullTopicsToShow == 0 then
				argsToPass['только статистика'] = true
			else
				argsToPass['стандартный заголовок'] = true
			end
			if afdMode then
				argsToPass['режим КУ'] = true
			end
			
			local data = parse_section_content(argsToPass)
			if type(data) == 'table' then
				if data.pageNotExistMessage then
					table.insert(itemsToRemove, v.originalFullTitle)
					table.insert(errorInfo, data.pageNotExistMessage)
				elseif data.sectionNotExistMessage then
					table.insert(itemsToRemove, v.originalFullTitle)
					table.insert(errorInfo, data.sectionNotExistMessage)
				else
					if topicsToShow ~= 0 and data.msgCount then
						data.pageTitle = v.titleObject.prefixedText
						data.sectionHeading = v.sectionHeading
						if argsToPass['подраздел'] then
							data.subsectionHeading = argsToPass['подраздел']
						end
						data.canonicalPath = data.pageTitle .. '#' .. data.sectionHeading .. (data.subsectionHeading and '\\\\\\' .. data.subsectionHeading or '')
					end
					if afdMode then
						if not data.closureHeading then
							table.insert(allData, data)
							if #allData >= nominationsNum then break end
						end
					else
						table.insert(allData, data)
					end
				end
			else
				table.insert(errorInfo, 'Не удалось получить данные о странице «' .. v.titleObject.prefixedText .. '» от модуля получения содержимого страницы.')
			end
		end
	end
	
	for k, v in pairs(allData) do
		allData[k].num = k
	end
	
	-- если включена сортировка и полных тем, и тем в таблице, мы можем сразу отсортировать общий массив
	if sortTopics and (fullTopicsToShow == 0 or sortFullTopics) then
		table.sort(allData, function (data1, data2)
			-- благодаря 10000 - dataN.num темы частично сохраняют изначальный порядок
			local lastMsgDate1Timestamp = data1.lastMsgDateTimestamp or 10000 - data1.num
			local lastMsgDate2Timestamp = data2.lastMsgDateTimestamp or 10000 - data2.num
			
			return lastMsgDate1Timestamp > lastMsgDate2Timestamp
		end)
	end
	
	-- считаем число тем и страниц, откладываем отсутствующие
	local pages, topics, fullTopics = {}, {}, {}
	local randomNum
	if allData[1] then
		if fullTopicsToShow ~= 0 then
			randomNum = tostring(os.clock()):sub(-7)
		end
		
		for k, v in pairs(allData) do
			table.insert(fullTopics, v)
			if v.msgCount then
				table.insert(topics, v)
				if not findInTable(pages, v.pageTitle) then
					table.insert(pages, v.pageTitle)
				end
			end
		end
	end
	
	-- если отключена сортировка полных тем, сортируем здесь только темы в таблице
	if sortTopics and (fullTopicsToShow ~= 0 and not sortFullTopics) then
		table.sort(topics, function (data1, data2)
			-- благодаря 10000 - dataN.num темы частично сохраняют изначальный порядок
			local lastMsgDate1Timestamp = data1.lastMsgDateTimestamp or 10000 - data1.num
			local lastMsgDate2Timestamp = data2.lastMsgDateTimestamp or 10000 - data2.num
			
			return lastMsgDate1Timestamp > lastMsgDate2Timestamp
		end)
	end
	
	if #topics > topicsToShow then
		pages = {}
		for k, v in pairs(topics) do
			if not findInTable(pages, v.pageTitle) then
				table.insert(pages, v.pageTitle)
			end
			if k == topicsToShow then
				for i = k + 1, #topics do
					table.remove(topics, k + 1)
				end
				break
			end
		end
	end
	
	-- формируем шапку
	local headerContent = ''
	if talkpageMode and talkpageCount == 1 then
		headerContent = headerContent .. '<div style="font-size:1.5em;">[[' .. onlyPageTitle .. ']]</div>\n'
	elseif afdMode then
		headerContent = headerContent .. '<div style="font-size:1.5em;">' .. #topics .. ' ' .. ru:plural(#topics, 'самая старая незакрытая номинация', 'самые старые незакрытые номинации', 'самых старых незакрытых номинаций') .. ' «[[ВП:К удалению|К удалению]]»</div>\n'
	end
	
	if showTopicCount then
		if standardMode or talkpageMode then
			headerContent = headerContent .. '<p><b>' .. #topics .. '</b> ' .. ru:plural(#topics, 'тема', 'темы', 'тем') .. ' на <b>' .. #pages .. '</b> ' .. ru:plural(#pages, 'странице', 'страницах')
		elseif afdMode then
			if #pages ~= 0 then
				headerContent = headerContent .. '<p>За <b>' .. #pages .. '</b> ' .. ru:plural(#pages, 'день', 'дня', 'дней')
			end
		elseif pageListMode then
			headerContent = headerContent .. '<p><b>' .. #pageListData .. '</b> ' .. ru:plural(#pageListData, 'страница', 'страницы', 'страниц')
		end
		if separateTopicListPage then
			headerContent = headerContent .. (reloadLink and '&nbsp;<b>·</b> ' or '') .. '<span class="plainlinks">[' .. separateTopicListPage:fullUrl('action=edit') .. ' Редактировать список тем]</span>\n'
		end
		headerContent = headerContent .. '</p>\n'
	end
	
	if reloadLink then
		headerContent = headerContent .. '<p ' .. (
			    not false  -- пока оставим
			and 'style="margin-top:1.5em;"'
			 or ''
		) .. '><span style="font-size:1.5em;" class="plainlinks purgelink">[' .. mw.title.getCurrentTitle():fullUrl('action=purge') .. ' Обновить]</span>&nbsp;<b>·</b> обновлялось в ' .. ru:formatDate('H:i j xg Y') .. ' (UTC)<br><i>На 3 ноября 2017 года автоматическое обновление производится медленно из-за проблем на серверах, поэтому кнопка была временно возвращена по умолчанию.</i>'
	end
	if reloadLink or separateTopicPage then
		headerContent = headerContent .. '</p>\n'
	end
	
	if not afdMode and not pageListMode and fullTopicsToShow > 50 and #topics > 50 then
		headerContent = headerContent .. '<p style="font-size:85%;">Для более быстрой загрузки страницы и экономии ресурсов сервера сократите число тем, содержимое которых выводится на странице, уменьшив значение параметра <code>полных тем</code> в шаблоне.</p>\n'
	end
	
	-- формируем таблицу
	local outFragmentLink_title = 'Ссылка прямо на реплику в настоящий момент не работает'
	if pageListData and pageListData[1] then
		local tableContent =
			   '{| class="wikitable wide sortable" style="line-height:1.4;"\n'
			.. '! width="2%" | №'
			.. '!! Страница '
			.. '!! width="25%" class="nowrap" | Последнее сообщение '
			.. '!! width="10%" | Тем '
			.. '!! width="10%" | Сообщений\n'
		for k, v in pairs(pageListData) do
			if v.msgCount then
				tableContent = tableContent
					.. '|-\n'
					.. '| style="text-align:center;" | ' .. v.num .. '\n'
					.. '| ' .. conceal(v.pageTitle, 'wikilink') .. '[[' .. v.pageTitle .. '|<span style="display:block; font-size:110%;">' .. v.pageTitle .. (
							    v.warningHeading
							and '<span style="display:inline-block; margin-left:1em; padding:0 8px; border-radius:5px; font-size:83.33%; line-height:1.5; letter-spacing:1px; background-color:#a07; color:#f9f9f9; white-space:nowrap;">ПРЕДУПР.</span>'
							 or ''
						) .. '</span>]]\n'
					.. '| style="/* Chrome */ word-break:break-word; /* ? */ word-wrap:break-word;" | ' .. (
						    v.msgCount > 0
						and conceal(v.lastMsgDateTimestamp, 'lastMsgDate') .. '[[' .. v.pageTitle .. '#' .. v.lastMsgDateString .. '_' .. v.lastMsgAuthor .. '|<span style="display:block; text-decoration:inherit;" class="topicWatch-outFragmentLink" title="' .. outFragmentLink_title .. '">' .. v.lastMsgDateString .. '<br>от ' .. v.lastMsgAuthor .. '</span>]]\n'
						 or '—\n'
					)
					.. '| style="text-align:center;" | ' .. conceal(v.msgCount, 'topicCount') .. v.topicCount .. '\n'
					.. '| style="text-align:center;" | ' .. conceal(v.msgCount, 'msgCount') .. v.msgCount .. '\n'
			end
		end
		tableContent = tableContent .. '|}\n'
		headerContent = headerContent .. tableContent
	elseif topicsToShow ~= 0 and #topics ~= 0 then
		local needHighlightOutLinks = fullTopicsToShow ~= 0 and fullTopicsToShow < math.min(#topics, topicsToShow)
		local labelColors =
			    afdMode
			and {
					closure           = '#800',
					preclosure        = '#25820e',
					challengedClosure = '#a07',
					partialClosure    = '#00a',
				}
			 or {
					closure           = '#999',
					preclosure        = '#999',
					challengedClosure = '#999',
					partialClosure    = '#999',
				}
		
		local tableContent
		if style == 'wikitable' then
			tableContent =
				   '{| class="wikitable wide sortable topicWatch-topicTable"' .. (fullTopicsToShow ~= 0 and ' id="topicTable' .. randomNum .. '"' or '') .. ' style="line-height:1.4;"\n'
				.. (talkpageMode and talkpageCount > 1 and '' or '! width="2%" | № !')
				.. '! Тема '
				.. '!! width="25%" class="nowrap" | Последнее сообщение '
				.. '!! width="10%" | Сообщений '
				.. '!! width="10%" | Авторов\n'
		elseif style == 'форумный' then
			tableContent =
				   '{| cellspacing="0" cellpadding="0" style="margin:1em 0; padding:8px 13px; border:1px solid #afd2e9; background:#f1f7fb; line-height:1.4;"\n'
				.. '|\n'
				.. '{| cellspacing="0" cellpadding="0" class="sortable topicWatch-topicTable"' .. (fullTopicsToShow ~= 0 and ' id="topicTable' .. randomNum .. '"' or '') .. '\n'
				.. '|-\n'
				.. (talkpageMode and talkpageCount > 1 and '' or '! style="border-bottom:1px solid #afd2e9; padding:2px 1.5em 4px 0; text-align:right; font-weight:normal;" | № !')
				.. '! style="border-bottom:1px solid #afd2e9; padding:2px 2em 4px 0; text-align:left; font-weight:normal;" | ' .. (talkpageMode and talkpageCount == 1 and '' or '<span style="visibility:hidden;">§ </span>') .. 'Тема '
				.. '!! style="border-bottom:1px solid #afd2e9; padding:2px 2em 4px 0; text-align:left; font-weight:normal;" class="nowrap" | Последнее сообщение '
				.. '!! style="border-bottom:1px solid #afd2e9; padding:2px 1.5em 4px 0; text-align:left; font-weight:normal;" | Сообщений\n'
				.. '|-\n'
				.. '| colspan="3" style="padding-top:6px;" |\n'
		end
		for k, v in pairs(topics) do
			local labels = (  -- может быть предварительный после оспоренного, поэтому ярлык оспаривания раньше
					    v.challengedClosureHeading
					and '<span style="display:inline-block; margin-left:1em; padding:0 8px; border-radius:5px; font-size:83.33%; line-height:1.5; letter-spacing:1px; background-color:' .. labelColors.challengedClosure .. '; color:#f9f9f9; white-space:nowrap;" title="В теме есть подраздел с названием «' .. v.challengedClosureHeading .. '»"' .. (afdMode and '' or ' class="topicWatch-label-challengedClosure"') .. '>ОСПОРЕНО</span>'
					 or ''
				) .. (
					    v.preclosureHeading
					and '<span style="display:inline-block; margin-left:1em; padding:0 8px; border-radius:5px; font-size:83.33%; line-height:1.5; letter-spacing:1px; background-color:' .. labelColors.preclosure .. '; color:#f9f9f9; white-space:nowrap;" title="В теме есть подраздел с названием «' .. v.preclosureHeading .. '»"' .. (afdMode and '' or ' class="topicWatch-label-preclosure"') .. '>ПРЕДЫТОГ</span>'
					 or ''
				) .. (
					    v.partialClosureHeading
					and '<span style="display:inline-block; margin-left:1em; padding:0 8px; border-radius:5px; font-size:83.33%; line-height:1.5; letter-spacing:1px; background-color:' .. labelColors.partialClosure .. '; color:#f9f9f9; white-space:nowrap;" title="В теме есть подподраздел с названием «' .. v.partialClosureHeading .. '»"' .. (afdMode and '' or ' class="topicWatch-label-partialClosure"') .. '>ЧАСТ. ИТОГ</span>'
					 or ''
				) .. (
					    v.closureHeading
					and '<span style="display:inline-block; margin-left:1em; padding:0 8px; border-radius:5px; font-size:83.33%; line-height:1.5; letter-spacing:1px; background-color:' .. labelColors.closure .. '; color:#f9f9f9; white-space:nowrap;" title="В теме есть подраздел с названием «' .. v.closureHeading .. '»"' .. (afdMode and '' or ' class="topicWatch-label-closure"') .. '>ИТОГ</span>'
					 or ''
				)
			
			local topicString, lastMsgString, msgCountString, authorCountString, numString
			local topicAttrs, lastMsgAttrs, msgCountAttrs, authorCountAttrs, numAttrs
			if style == 'wikitable' then
				numString = v.num
				topicString = conceal(v.canonicalPath, 'wikilink') .. '[[' .. (
						    k <= fullTopicsToShow
						and (
							    talkpageMode and talkpageCount == 1
							and v.sectionHeadingLink
							 or (
								    v.subsectionHeading
								and v.subsectionHeading .. ' (' .. v.sectionHeadingLink .. ')'
								 or v.sectionHeadingLink
								) .. ' ← ' .. v.pageTitle
							)
						 or v.pageTitle .. (
							    v.subsectionHeading
							and v.subsectionHeading .. ' (' .. v.sectionHeadingLink .. ')'
							 or v.sectionHeadingLink
							)
					) .. '|<span ' .. (k <= fullTopicsToShow and fullTopicsToShow ~= 0 and '' or 'title="Откроется на отдельной странице" ') .. (
						    talkpageMode and talkpageCount == 1
						and 'style="display:block; font-size:110%;">'
						 or 'style="display:block;"><span style="visibility:hidden;">§ </span><small>' .. v.pageTitle .. '</small><br><span style="display:inline-block;">§ </span>'
					) .. v.sectionHeading .. labels .. '</span>]]'  -- TODO: учёт подраздела
				lastMsgString = conceal(v.lastMsgDateTimestamp, 'lastMsgDate') .. (
						    k <= fullTopicsToShow
						and '[[#' .. v.lastMsgAnchor .. '|<span style="display:block; text-decoration:inherit;">'
						 or '[[' .. v.pageTitle .. '#' .. v.lastMsgAnchor .. '|<span style="display:block; text-decoration:inherit;" class="topicWatch-outFragmentLink" title="' .. outFragmentLink_title .. '">'
					) .. v.lastMsgDateString .. '<br>от ' .. v.lastMsgAuthor .. '</span>]]'
				msgCountString = conceal(v.msgCount, 'msgCount') .. v.msgCount
				authorCountString = conceal(#v.authors, 'authorCount') .. '<span style="border-bottom:1px dotted; cursor:help;" title="' .. table.concat(v.authors, ', ') .. '">' .. #v.authors .. '</span>'
				
				numAttrs = 'style="text-align:center;"'
				topicAttrs = ''
				lastMsgAttrs = 'style="/* Chrome */ word-break:break-word; /* ? */ word-wrap:break-word;"'
				msgCountAttrs = 'style="text-align:center;"'
				authorCountAttrs = 'style="text-align:center;"'
			elseif style == 'форумный' then
				numString =     talkpageMode and talkpageCount == 1
							and '<span style="font-size:110%; visibility:hidden;">l</span>' .. v.num .. '<span style="font-size:110%; visibility:hidden;">l</span>'  -- способ, рекомендуемый Горбуновым, чтобы строки были на одной линии
							 or v.num
				topicString = conceal(v.canonicalPath, 'wikilink')  .. '[[' .. v.pageTitle .. (
						    v.subsectionHeading
						and v.subsectionHeading .. ' (' .. v.sectionHeadingLink .. ')'
						 or v.sectionHeadingLink
					) .. '|' .. (
						    talkpageMode and talkpageCount == 1
						and '<span style="display:block; font-size:110%;">'
						 or '<span style="display:block;"><span style="display:inline-block;"><span style="visibility:hidden;">§ </span><small>' .. mw.ustring.gsub(v.pageTitle, 'Википедия:Форум/', '') .. '</small></span><br><span style="display:inline-block;">§ </span>'
					) .. v.sectionHeading .. labels .. '</span>]]'
				lastMsgString = conceal(v.lastMsgDateTimestamp, 'lastMsgDate') .. (
						    k <= fullTopicsToShow
						and '[[#' .. v.lastMsgAnchor .. '|<span style="display:block; text-decoration:inherit;">'
						 or '[[' .. v.pageTitle .. '#' .. v.lastMsgAnchor .. '|<span style="display:block; text-decoration:inherit;" class="topicWatch-outFragmentLink" title="' .. outFragmentLink_title .. '">'
					) .. v.lastMsgDateString .. '<br>от ' .. v.lastMsgAuthor .. '</span>]]'
				msgCountString = conceal(v.msgCount, 'msgCount') .. '<span style="border-bottom:1px dotted; cursor:help;" title="От ' .. #v.authors .. ' авторов:&#10;' .. table.concat(v.authors, ', ') .. '">' .. v.msgCount .. '</span>'
				
				numAttrs = 'style="padding:3px 0; vertical-align:top; text-align:right; padding-right:8px;"'
				topicAttrs = 'style="padding:3px 2em 3px 0; vertical-align:top;"'
				lastMsgAttrs = 'style="padding:3px 0.5em 3px 0; vertical-align:top; /* Chrome */ word-break:break-word; /* ? */ word-wrap:break-word;"'
				msgCountAttrs = 'style="padding:3px 0; vertical-align:top; text-align:center; vertical-align:top;"'
			end
			
			tableContent = tableContent
				.. '|-' .. (style ~= 'форумный' and needHighlightOutLinks and k > fullTopicsToShow and ' style="background-color:#f2f2f2;"' or '') .. '\n'
				.. (talkpageMode and talkpageCount > 1 and '' or '| ' .. numAttrs .. ' | ' .. numString .. '\n')
				.. '| ' .. topicAttrs .. ' | ' .. topicString .. '\n'
				.. '| ' .. lastMsgAttrs .. ' | ' .. lastMsgString .. '\n'
				.. '| ' .. msgCountAttrs .. ' | ' .. msgCountString .. '\n'
				.. (style == 'форумный' and '' or '| ' .. authorCountAttrs .. ' | ' .. authorCountString .. '\n')
		end
		tableContent = tableContent .. '|}\n'
		if style == 'форумный' then
			tableContent = tableContent .. '|}\n'
		end
		headerContent = headerContent .. tableContent
	end
	
	-- формируем сообщение(-я) об ошибке
	if not items[1] then
		if standardMode then
			table.insert(errorInfo, 'В списке тем для отслеживания пока пусто.')
		elseif talkpageMode or pageListMode then
			table.insert(errorInfo, 'В списке страниц пусто.')
		elseif afdMode then
			table.insert(errorInfo, 'Не удалось найти темы.')
		end
	end
	if errorInfo[1] then
		headerContent = headerContent
			.. '<div style="font-style:italic; margin:1em 0;">\n'
			.. table.concat(errorInfo, '<br>')
			.. '</div>\n'
	end
	local itemsToRemoveString
	if itemsToRemove[1] then
		itemsToRemoveString = '<ul style="display:none;" class="topicWatch-itemsToRemove">\n'
		for k, v in pairs(itemsToRemove) do
			itemsToRemoveString = itemsToRemoveString .. '<li>' .. v .. '</li>\n'
		end
		itemsToRemoveString = itemsToRemoveString .. '</ul>\n'
		headerContent = headerContent .. itemsToRemoveString
	end
	
	-- формируем сами темы
	local content = ''
	if fullTopicsToShow ~= 0 then
		headerContent = headerContent .. '__TOC__\n'
		for k, v in pairs(fullTopics) do
			if v.msgCount then
				v.sectionContent = mw.ustring.gsub(v.sectionContent, v.lastMsgDateString .. ' %(UTC%)', '<cite id="' .. v.lastMsgAnchor:gsub('"', '&quot;') .. '" style="font-style:normal;">%0</cite>')
			end
			content = content
				.. killHeadingMarkers(frame:preprocess(v.sectionContent)) .. '\n'
				.. '<div style="margin-top:1em;">' .. (topicsToShow ~= 0 and '[[#topicTable' .. randomNum .. '|↑ К списку тем]]' or '[[#toc|↑ К содержанию]]') .. '</div>\n'
			if k == fullTopicsToShow then break end
		end
	end
	
	content = headerContent .. content
	
	return content
end

return p