Документация

Реализация шаблона {{Мониторинг тем}}. Основан на модуле 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['обновить'], false)
    else
        reloadLink = false
    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 .. '&nbsp;<b>·</b> <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)'
    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'
            .. '! scope="col" width="2%" | №\n'
            .. '! scope="col" | Страница\n'
            .. '! scope="col" width="25%" class="nowrap" | Последнее сообщение\n'
            .. '! scope="col" width="10%" | Тем\n'
            .. '! scope="col" 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:var(--color-inverted, #fff); 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           = 'var(--border-color-destructive--active, #b32424)',
                    preclosure        = 'var(--border-color-success, #096450)',
                    challengedClosure = 'var(--border-color-destructive, #d73333)',
                    partialClosure    = 'var(--border-color-progressive, #36c)',
                }
             or {
                    closure           = 'var(--border-color-interactive, #72777d)',
                    preclosure        = 'var(--border-color-interactive, #72777d)',
                    challengedClosure = 'var(--border-color-interactive, #72777d)',
                    partialClosure    = 'var(--border-color-interactive, #72777d)',
                }
        
        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 var(--border-color-content-added, #afb6e9); background:var(--background-color-progressive-subtle, #eaf3ff); color:inherit; 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 var(--border-color-base, #afd2e9); padding:2px 1.5em 4px 0; text-align:right; font-weight:normal;" | № !')
                .. '! style="border-bottom:1px solid var(--border-color-base, #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 var(--border-color-base, #afd2e9); padding:2px 2em 4px 0; text-align:left; font-weight:normal;" class="nowrap" | Последнее сообщение '
                .. '!! style="border-bottom:1px solid var(--border-color-base, #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:var(--color-inverted, #fff); 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:var(--color-inverted, #fff); 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:var(--color-inverted, #fff); 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:var(--color-inverted, #fff); 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 .. ' ' .. ru:plural(#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:var(--background-color-neutral); color:inherit;"' 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