Модуль:WikidataSelectors

Материал из Тептар
Перейти к навигации Перейти к поиску

Для документации этого модуля может быть создана страница Модуль:WikidataSelectors/doc

local i18n = {
    ["errors"] = {
        ["rank-not-valid"] = "Некорректное значение приоритета (rank)",
        ["cant-parse-condition"] = "Не удалось разобрать условие"
    }
}

local validRanks = {
    'best',
    'preferred',
    'normal',
    'deprecated'
}

-- Внутренняя функция для вывода ошибок (сделана безопасной)
local function throwError( key )
    -- Вместо критической ошибки возвращаем текст, чтобы не ломать страницы
    mw.log( i18n.errors[key] or "Неизвестная ошибка фильтра" )
    return nil
end

local p = {}

--[[
  Безопасная загрузка свойств для сайта Тептар (без зависимости от Wikidata)
]]
function p.load( entityId, propertySelector )
    local propertyId = mw.ustring.match( propertySelector, '^[Pp]%d+' )
    if not propertyId then
        return nil
    end
    propertyId = string.upper( propertyId )

    local allStatements = {}
    
    -- БЕЗОПАСНАЯ ПРОВЕРКА: Если расширение Wikibase отсутствует, отдаем пустую таблицу,
    -- чтобы скрипт фильтрации отработал вхолостую, но без красных ошибок Lua.
    if mw.wikibase and mw.wikibase.getAllStatements then
        allStatements[ propertyId ] = mw.wikibase.getAllStatements( entityId, propertyId )
    else
        allStatements[ propertyId ] = {}
    end

    return p.filter( allStatements, propertySelector )
end

--[[
  Разбор селекторов и фильтрация утверждений
]]
function p.filter( allClaims, propertySelector )
    propertySelector = mw.text.trim( propertySelector or '' )

    local propertyId = mw.ustring.match( propertySelector, '^[Pp]%d+' )
    if not propertyId then
        propertyId = ''
    end
    local initPos = #propertyId + 1
    propertyId = string.upper( propertyId )
    
    if ( not allClaims ) then
        return nil
    end
    local allPropertyClaims = allClaims[propertyId]
    if ( not allPropertyClaims ) then
        return nil
    end

    local rules = p.matchSelectors( propertySelector, initPos )

    local isRanked = false
    for i, subRules in ipairs( rules ) do
        for j, rule in ipairs( subRules ) do
            if rule['type'] == 'rank' then
                isRanked = true
                break
            end
        end
    end
    if not isRanked then
        table.insert( rules, 1, { { type = 'rank', value = 'best' } } )
    end

    allPropertyClaims = p.applyRules( allPropertyClaims, rules )

    return allPropertyClaims
end


--[[
  Сбор правил селекторов
]]
function p.matchSelectors( selectorsString, initPos )
    local rules = {}
    local rawRulePattern = '^%s*%[%s*[^%[%]]+%s*%]%s*'
    local rulePattern = '^%s*%[%s*([^%[%]]+)%s*%]%s*$'
    
    if not initPos then
        initPos = 1
    end

    local rawRule = mw.ustring.match( selectorsString, rawRulePattern, initPos )

    while rawRule do
        initPos = initPos + #rawRule
        local rule = mw.ustring.match( rawRule, rulePattern )
        rule = mw.text.trim( rule )

        local subRules = mw.text.split( rule, '%s*,%s*' )
        local commands = {}

        for i, subRule in ipairs( subRules ) do
            local isInversed = false
            if mw.ustring.match( subRule, '^!' ) then
                isInversed = true
                subRule = mw.ustring.match( subRule, '^!%s*(.+)$' )
            end

            if mw.ustring.match( subRule, '^%d+$' ) then
                table.insert( commands, {
                    type = 'position',
                    value = subRule,
                    inversed = isInversed
                } )
    
            elseif mw.ustring.match( subRule, '^rank%s*:%s*(%a+)$' ) then
                local rank = mw.ustring.match( subRule, '^rank%s*:%s*(%a+)$' )
                table.insert( commands, {
                    type = 'rank',
                    value = rank,
                    inversed = isInversed
                } )

            elseif mw.ustring.match( subRule, '^language%s*:%s*([%a%-]+)$' ) then
                local value = mw.ustring.match( subRule, '^language%s*:%s*([%a%-]+)$' )
                table.insert( commands, {
                    type = 'language',
                    value = value,
                    inversed = isInversed
                } )

            elseif mw.ustring.match( subRule, '^language%s*!:%s*([%a%-]+)$' ) then
                local value = mw.ustring.match( subRule, '^language%s*!:%s*([%a%-]+)$' )
                table.insert( commands, {
                    type = 'language',
                    value = value,
                    inversed = not isInversed
                } )

            elseif mw.ustring.match( subRule, '^min$' ) then
                table.insert( commands, { type = 'value_min' } )

            elseif mw.ustring.match( subRule, '^max$' ) then
                table.insert( commands, { type = 'value_max' } )

            elseif mw.ustring.match( subRule, '^min%s*:%s*[Pp]%d+$' ) then
                local value = mw.ustring.match( subRule, ':%s*([Pp]%d+)$' )
                table.insert( commands, {
                    type = 'qualifier_min',
                    qualifier = value
                } )

            elseif mw.ustring.match( subRule, '^max%s*:%s*[Pp]%d+$' ) then
                local value = mw.ustring.match( subRule, ':%s*([Pp]%d+)$' )
                table.insert( commands, {
                    type = 'qualifier_max',
                    qualifier = value
                } )

            elseif mw.ustring.match( subRule, '^unit%s*:%s*[^%[%],:]+$' ) then
                local value = mw.ustring.match( subRule, ':%s*([^%[%],:]+)$' )
                table.insert( commands, {
                    type = 'unit',
                    value = value,
                    inversed = isInversed
                } )

            elseif mw.ustring.match( subRule, '^unit%s*!:%s*[^%[%],:]+$' ) then
                local value = mw.ustring.match( subRule, '!:%s*([^%[%],:]+)$' )
                table.insert( commands, {
                    type = 'unit',
                    value = value,
                    inversed = not isInversed
                } )

            elseif mw.ustring.match( subRule, '^[Pp]%d+$' ) then
                local qualifier = mw.ustring.match( subRule, '^[Pp]%d+' )
                table.insert( commands, {
                    type = 'qualifier',
                    qualifier = qualifier,
                    value = nil,
                    inversed = isInversed
                } )

            elseif mw.ustring.match( subRule, '^[Pp]%d+%s*:%s*[^%[%],:]+$' ) then
                local qualifier = mw.ustring.match( subRule, '^([Pp]%d+)%s*:?' )
                local value = mw.ustring.match( subRule, ':%s*([^%[%],:]+)$' )
                table.insert( commands, {
                    type = 'qualifier',
                    qualifier = qualifier,
                    value = value,
                    inversed = isInversed
                } )

            elseif mw.ustring.match( subRule, '^[Pp]%d+%s*!:%s*[^%[%],:]+$' ) then
                local qualifier = mw.ustring.match( subRule, '^([Pp]%d+)%s*!:?' )
                local value = mw.ustring.match( subRule, '!:%s*([^%[%],:]+)$' )
                table.insert( commands, {
                    type = 'qualifier',
                    qualifier = qualifier,
                    value = value,
                    inversed = not isInversed
                } )

            elseif mw.ustring.match( subRule, '^[Qq]%d+$' ) then
                local value = mw.ustring.match( subRule, '^[Qq]%d+' )
                table.insert( commands, {
                    type = 'value',
                    value = value,
                    inversed = isInversed
                } )
            else
                -- Игнорируем неизвестные условия, вместо того чтобы крашить парсер
                mw.log( i18n.errors['cant-parse-condition'] )
            end
        end

        if #commands > 0 then
            table.insert( rules, commands )
        end

        rawRule = mw.ustring.match( selectorsString, rawRulePattern, initPos )
    end
    
    return rules
end


--[[
  Перехват и фильтрация утверждений
]]
function p.applyRules( claims, rules )
    for i, subRules in ipairs( rules ) do
        local newClaims = {}
        for j, rule in ipairs( subRules ) do
            if rule['type'] == 'rank' then
                table.insert( newClaims, p.filterByRank( claims, rule['value'], rule['inversed'] ) )
            elseif rule['type'] == 'language' then
                table.insert( newClaims, p.filterByLanguage( claims, rule['value'], rule['inversed'] ) )
            elseif rule['type'] == 'unit' then
                table.insert( newClaims, p.filterByUnit( claims, rule['value'], rule['inversed'] ) )
            elseif rule['type'] == 'position' then
                table.insert( newClaims, p.filterByPosition( claims, rule['value'], rule['inversed'] ) )
            elseif rule['type'] == 'qualifier' then
                table.insert( newClaims, p.filterByQualifier( claims, rule['qualifier'], rule['value'], rule['inversed'] ) )
            elseif rule['type'] == 'qualifier_min' then
                table.insert( newClaims, p.filterUtterByQualifier( claims, rule['qualifier'], true ) )
            elseif rule['type'] == 'qualifier_max' then
                table.insert( newClaims, p.filterUtterByQualifier( claims, rule['qualifier'], false ) )
            elseif rule['type'] == 'value' then
                table.insert( newClaims, p.filterByValue( claims, rule['value'], rule['inversed'] ) )
            elseif rule['type'] == 'value_min' then
                table.insert( newClaims, p.filterUtter( claims, true ) )
            elseif rule['type'] == 'value_max' then
                table.insert( newClaims, p.filterUtter( claims, false ) )
            end
        end
        claims = {}

        for j, newSubClaims in ipairs( newClaims ) do
            for k, newClaim in ipairs( newSubClaims ) do
                local isNew = true
                for l, oldClaim in ipairs( claims ) do
                    if oldClaim['id'] == newClaim['id'] then
                        isNew = false
                        break
                    end
                end
                if isNew then
                    table.insert( claims, newClaim )
                end
            end
        end
    end

    return claims
end


function p.filterByRank( claims, rank, inversed )
    if not inversed then inversed = false end
    if not rank then rank = 'best' end
    
    local isValidRank = false
    for i, validRank in ipairs( validRanks ) do
        if rank == validRank then
            isValidRank = true
            break
        end
    end
    if not isValidRank then return claims end -- Безопасный возврат вместо ошибки

    if rank == 'best' then
        rank = 'normal' 
        for i, statement in pairs( claims ) do
            if (statement.rank == 'preferred') then
                rank = 'preferred'
                break
            end
        end
    end

    local resultClaims = {};
    for i, statement in pairs( claims ) do
        if ( statement.rank == rank ) ~= inversed then
            table.insert( resultClaims, statement )
        end
    end

    return resultClaims
end


function p.filterByLanguage( claims, language, inversed )
    if not inversed then inversed = false end

    local resultClaims = {}
    local mulStatement = {}
    for i, statement in ipairs( claims ) do
        local isMatchLanguage = false
        if statement['mainsnak']
                and statement['mainsnak']['datavalue']
                and statement['mainsnak']['datavalue']['value']
                and statement['mainsnak']['datavalue']['value']['language'] then
                
            if statement['mainsnak']['datavalue']['value']['language'] == language then
                isMatchLanguage = true
            end
            if statement['mainsnak']['datavalue']['value']['language'] == 'mul' then
                mulStatement = statement
            end
        end
        if isMatchLanguage ~= inversed then
            table.insert( resultClaims, statement )
        end
    end
    
    if next(resultClaims) == nil and next(mulStatement) ~= nil then
        table.insert( resultClaims, mulStatement )
    end

    return resultClaims
end


function p.filterByUnit( claims, unit, inversed )
    if not inversed then inversed = false end
    unit = 'http://www.wikidata.org/entity/' .. string.upper( unit )

    local resultClaims = {}
    for i, statement in ipairs( claims ) do
        local isMatchUnit = false
        if statement['mainsnak']
                and statement['mainsnak']['datavalue']
                and statement['mainsnak']['datavalue']['value']
                and statement['mainsnak']['datavalue']['value']['unit']
                and statement['mainsnak']['datavalue']['value']['unit'] == unit then
            isMatchUnit = true
        end
        if isMatchUnit ~= inversed then
            table.insert( resultClaims, statement )
            break
        end
    end

    return resultClaims
end


function p.filterByPosition( claims, position, inversed )
    if not inversed then inversed = false end

    local resultClaims = {};
    for statementPosition, statement in ipairs( claims ) do
        if ( statementPosition == tonumber( position ) ) ~= inversed then
            table.insert( resultClaims, statement )
            break
        end
    end

    return resultClaims
end


function p.filterByQualifier( claims, qualifierId, value, inversed )
    if not inversed then inversed = false end
    qualifierId = string.upper( qualifierId )

    local resultClaims = {}
    for i, statement in ipairs( claims ) do
        if statement['qualifiers'] and statement['qualifiers'][qualifierId] then
            if value == nil then
                if ( #statement['qualifiers'][qualifierId] > 0 ) ~= inversed then
                    table.insert( resultClaims, statement )
                end
            else
                local isQualifierFound = false
                for j, qualifier in ipairs( statement['qualifiers'][qualifierId] ) do
                    if qualifier['datavalue'] then
                        local qualifierValue = qualifier['datavalue']['value']
                        if qualifier['datavalue']['type'] == 'wikibase-entityid' then
                            qualifierValue = qualifierValue.id
                            value = string.upper( value )
                        end
            
                        if qualifierValue == value then
                            isQualifierFound = true
                            break
                        end
                    end
                end
                if isQualifierFound ~= inversed then
                    table.insert( resultClaims, statement )
                end
            end
        elseif inversed then
            table.insert( resultClaims, statement )
        end
    end
    return resultClaims
end


function p.filterByValue( claims, value, inversed )
    inversed = inversed or false

    local resultClaims = {}
    for i, statement in ipairs( claims ) do
        local statementValue
        if statement['mainsnak'] 
        and statement['mainsnak']['datavalue'] 
        and statement['mainsnak']['datavalue']['type']
        then 
            statementValue = statement['mainsnak']['datavalue']['value']
            if statement['mainsnak']['datavalue']['type'] == 'quantity' then
                statementValue = statementValue.amount
            end
            if statement['mainsnak']['datavalue']['type'] == 'time' then
                statementValue = statementValue.time
            end
            if statement['mainsnak']['datavalue']['type'] == 'wikibase-entityid' then
                statementValue = statementValue.id
                value = string.upper( value )
            end
        end

        if ( statementValue == value ) ~= inversed then
            table.insert( resultClaims, statement )
        end
    end

    return resultClaims
end


function p.filterUtter( claims, asc, inversed )
    local resultValue = nil
    for i, statement in ipairs( claims ) do
        local statementValue
        if statement['mainsnak'] and
            statement['mainsnak']['datavalue'] and
            statement['mainsnak']['datavalue']['type']
        then
            statementValue = statement['mainsnak']['datavalue']['value']
            if statement['mainsnak']['datavalue']['type'] == 'quantity' then
                statementValue = statementValue.amount
            end
            if statement['mainsnak']['datavalue']['type'] == 'time' then
                statementValue = statementValue.time
            end
            if statement['mainsnak']['datavalue']['type'] == 'wikibase-entityid' then
                statementValue = statementValue.id
            end

            if not resultValue or ( statementValue < resultValue ) == asc then
                resultValue = statementValue
            end
        end
    end

    return p.filterByValue( claims, resultValue, inversed )
end


function p.filterUtterByQualifier( claims, qualifierId, asc )
    qualifierId = string.upper( qualifierId )

    local resultValue = nil
    local resultStatement = nil
    for i, statement in ipairs( claims ) do
        if not statement['qualifiers'] and not statement['qualifiers'][qualifierId] then
            if resultStatement == nil then
                resultStatement = statement
            end
        else
            for _, qualifier in ipairs( statement['qualifiers'][qualifierId] ) do
                if qualifier['datavalue'] then
                    local qualifierValue = qualifier['datavalue']['value']
                    if qualifier['datavalue']['type'] == 'quantity' then
                        qualifierValue = qualifierValue.amount
                    end
                    if qualifier['datavalue']['type'] == 'time' then
                        qualifierValue = qualifierValue.time
                    end
                    if qualifier['datavalue']['type'] == 'wikibase-entityid' then
                        qualifierValue = qualifierValue.id
                    end

                    if not resultValue or ( qualifierValue < resultValue ) == asc then
                        resultStatement = statement
                        resultValue = qualifierValue
                    end
                end
            end
        end
    end

    if resultStatement then
        return { resultStatement }
    else
        return {}
    end
end


return p