Модуль: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, '</%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()
			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 reloadLink
	if args['обновить'] then
		reloadLink = yesno(args['обновить'], false)
	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
						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
	
	-- формируем шапку
	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 standardMode or talkpageMode then
		headerContent = headerContent .. '<p><b>' .. #topics .. '</b> ' .. ru:plural(#topics, 'тема', 'темы', 'тем') .. ' на <b>' .. #pages .. '</b> ' .. ru:plural(#pages, 'странице', 'страницах') .. '</p>\n'
	elseif afdMode then
		if #pages ~= 0 then
			headerContent = headerContent .. '<p>За <b>' .. #pages .. '</b> ' .. ru:plural(#pages, 'день', 'дня', 'дней') .. '</p>\n'
		end
	elseif pageListMode then
		headerContent = headerContent .. '<p><b>' .. #pageListData .. '</b> ' .. ru:plural(#pageListData, 'страница', 'страницы', 'страниц') .. '</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') .. ' Обновить]&nbsp;</span>(обновлялось в ' .. ru:formatDate('H:i j xg Y') .. ' (UTC))\n'
	end
	if separateTopicListPage then
		headerContent = headerContent .. (reloadLink and ' &nbsp;<b>·</b> &nbsp;' or '') .. '<span class="plainlinks">[' .. separateTopicListPage:fullUrl('action=edit') .. ' Редактировать список тем]</span>\n'
	end
	if reloadLink or separateTopicPage then
		headerContent = headerContent .. '</p>\n'
	end
	
	if not afdMode and fullTopicsToShow > 50 and #topics > 50 then
		headerContent = headerContent .. '<p style="font-size:85%;">Для более быстрой загрузки страницы и экономии ресурсов сервера сократите число тем, содержимое которых выводится на странице, уменьшив значение параметра <code>полных тем</code> в шаблоне.</p>\n'
	end
	
	-- формируем таблицу
	if pageListData and pageListData[1] then
		local tableContent =
			   '{| class="wikitable wide sortable"\n'
			.. '! width="2%" | №'
			.. '!! Страница '
			.. '!! width="25%" | Последнее сообщение '
			.. '!! 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="line-height:1.4; 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="line-height:1.4; display:block; text-decoration:inherit;" class="topicWatch-outFragmentLink" title="Ссылка прямо на реплику работает только при установленном скрипте topicWatch.js">' .. 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 =
			   '{| class="wikitable wide sortable topicWatch-topicTable"' .. (fullTopicsToShow ~= 0 and ' id="topicTable' .. randomNum .. '"' or '') .. '\n'
			.. (talkpageMode and talkpageCount > 1 and '' or '! width="2%" | № !')
			.. '! Тема '
			.. '!! width="25%" | Последнее сообщение '
			.. '!! width="10%" | Сообщений '
			.. '!! width="10%" | Авторов\n'
		for k, v in pairs(topics) do
			tableContent = tableContent
				.. '|-' .. (needHighlightOutLinks and k > fullTopicsToShow and ' style="background-color:#f2f2f2;"' or '') .. '\n'
				.. (talkpageMode and talkpageCount > 1 and '' or '| style="text-align:center;" | ' .. v.num .. '\n')
				.. '| ' .. 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
							) .. ' ←.C2.A0' .. v.pageTitle
						)
					 or v.pageTitle .. (
						    v.subsectionHeading
						and v.subsectionHeading .. ' (' .. v.sectionHeadingLink .. ')'
						 or v.sectionHeadingLink
						)
				) .. '|<span ' .. (k <= fullTopicsToShow and '' or 'title="Откроется на отдельной странице" ') .. (
					    talkpageMode and talkpageCount == 1
					and 'style="line-height:1.4; display:block; font-size:110%;">'
					 or 'style="line-height:1.4; display:block;"><span style="visibility:hidden;">§ </span><small>' .. v.pageTitle .. '</small><br>§ '
				) .. v.sectionHeading .. ( -- может быть предварительный после оспоренного, поэтому ярлык оспаривания раньше
					    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 ''
				) .. '</span>]]\n'
				.. '| style="/* Chrome */ word-break:break-word; /* ? */ word-wrap:break-word;" | ' .. conceal(v.lastMsgDateTimestamp, 'lastMsgDate') .. (
					    k <= fullTopicsToShow
					and '[[#' .. v.lastMsgAnchor .. '|<span style="line-height:1.4; display:block; text-decoration:inherit;">'
					 or '[[' .. v.pageTitle .. '#' .. v.lastMsgAnchor .. '|<span style="line-height:1.4; display:block; text-decoration:inherit;" class="topicWatch-outFragmentLink" title="Ссылка прямо на реплику работает только при установленном скрипте topicWatch.js">'
				) .. v.lastMsgDateString .. '<br>от ' .. v.lastMsgAuthor .. '</span>]]\n'
				.. '| style="text-align:center;" | ' .. conceal(v.msgCount, 'msgCount') .. v.msgCount .. '\n'
				.. '| style="text-align:center;" | ' .. conceal(#v.authors, 'authorCount') .. '<span style="border-bottom:1px dotted; cursor:help;" title="' .. table.concat(v.authors, ', ') .. '">' .. #v.authors .. '</span>\n'
			if k == topicsToShow then break end
		end
		tableContent = tableContent .. '|}\n'
		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; margin-left:0.0em;">' .. (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