require( 'strict' )
local p = {}
local mwLang = mw.getContentLanguage()
local getArgs = require('Module:Arguments').getArgs
local DebugLog = require('Module:DebugLog')
local config
local DEFAULT_CONFIG = 'Module:Песочница/Abiyoyo/Autosorting/config'
local schemas = mw.loadData('Module:Песочница/Abiyoyo/Autosorting/schemas')
local debLog = DebugLog:new()
local globals = {
ignoreNSChecks = false,
}
--------------------------------------------------------------------------------
-- Функции общего назначения
--------------------------------------------------------------------------------
-- проверка пустого значения
local function isEmpty(val)
return val == nil or val == ''
end
-- Проверка наличия value среди значений tab
local function hasValue( tab, value )
for _, val in pairs(tab) do
if val == value then return true end
end
return false
end
--------------------------------------------------------------------------------
--- Фильтры
-- вызываются из processFilters() в виде filterFunc(options, frameArgs)
--------------------------------------------------------------------------------
local function isEmptyParam(options, frameArgs)
debLog:write('invoked', 'isEmptyParam')
if not options then return true end
if type(options) ~= 'table' then return true end
if options.param == nil then return true end
if frameArgs[options.param] == nil or frameArgs[options.param] == '' then
return true
end
return false
end
local function isNotEmptyParam(options, frameArgs)
debLog:write('invoked', 'isNotEmptyParam')
return not isEmptyParam(options, frameArgs)
end
local function isEqParam(options, frameArgs)
return true
end
local function isNotEqParam(options, frameArgs)
return true
end
--- Nasmespace filter
local function isNamespace(options, frameArgs)
if not options then return true end
if type(options) ~= 'table' then
return globals.ignoreNSChecks or false
end
if options.namespaces == nil then return true end
local namespace = mw.title.getCurrentTitle().namespace
if hasValue(options.namespaces, namespace) then
return true
end
return globals.ignoreNSChecks or false
end
--- Not nasmespace filter
local function isNotNamespace(options, frameArgs)
if not options then return true end
if type(options) ~= 'table' then return true end
if options.namespaces == nil then return true end
local namespace = mw.title.getCurrentTitle().namespace
if hasValue( options.namespaces, namespace ) then
return globals.ignoreNSChecks or false
end
return true
end
local FILTER_FUNCTIONS = {
['parameter-is-empty'] = isEmptyParam,
['parameter-not-empty'] = isNotEmptyParam,
['parameter-equals'] = isEqParam,
['parameter-not-equals'] = isNotEqParam,
['namespace-is'] = isNamespace,
['namespace-is-not'] = isNotNamespace,
default = function() return true end,
}
--------------------------------------------------------------------------------
-- Функции для формирования имён категорий
--------------------------------------------------------------------------------
-- получает из пресета preset части имени категории и возвращает
-- таблицу с ними
local function catParts(preset)
local result = {}
result.sep = preset.sep
result.innersep = preset.innersep
result.lbracket = preset.lbracket
result.rbracket = preset.rbracket
result.prefix = preset.prefix
result.postfix = preset.postfix
return result
end
-- Формирует строку с "сортировочной" частью имени категории, используется в
-- функции formatCategoryName() для обработки параметра values.
-- values - или (1) строка <string1>
-- или (2) таблица вида {<string1>, <string2>, ... }
-- или (3) таблица вида { { <string11>, <string12> }, ... }
-- на выходе выдает:
-- (1) => <string1>
-- (2) => <string1><sep><string2>...
-- (3) => <string11><innersep><string12><sep><string21><innersep><string22>...
local function formatChain(values , sep, innersep)
values = values or ''
sep = sep or ';'
innersep = innersep or ':'
if type(values) == 'string' then return values end -- вариант (1)
if type(values) ~= 'table' then return '' end
if next(values) == nil then return '' end
if type( select(2, next(values)) ) == 'string' then -- вариант (2)
return table.concat(values, sep)
elseif type( select(2, next(values)) ) == 'table' then -- вариант (3)
local result = ''
for k, subtable in ipairs(values) do
result = result .. table.concat(subtable, innersep)
end
return result
end
deblog:write('unexpected value', 'formatSortChain', 'error')
return ''
end
-- Формирует название категории. Возвращает строку с именем категории по шаблону,
-- заданому в конфиге (config.presets),
--
-- preset - таблица с пресетом
-- values - содержание "сортировочной" части. Формат описан в formatChain()
local function formatCategoryName(preset, values)
values = values or ''
local result = ''
-- Получение фрагментов имени категории
local p = catParts(preset)
local chain = formatChain(values , p.sep, p.innersep)
return table.concat{ p.prefix, p.lbracket, chain, p.rbracket, p.postfix }
end
-- форматирует строку категории по имени и ключу сортировки
local function formatCategory(name, sortKey)
if isEmpty(name) then
debLog:write('No category name', 'formatCategory', 'warn')
return ''
end
local sortStr = ( isEmpty(sortKey) and '' ) or ( '|' .. tostring(sortKey) )
return table.concat{'[', '[Category:', tostring(name), sortStr, ']', ']'}
end
--------------------------------------------------------------------------------
-- Функции, используемые обработчиками
--------------------------------------------------------------------------------
-- Получить соответствие категории критериям
local function isValidCategory(preset, catName, property, checkLimits)
local allowRed = false
if not isEmpty(preset) then
allowRed = preset.allowred or false
end
if not checkLimits then
local success, title = pcall(mw.title.new, 'Category:' .. catName)
if success and allowRed then
return true
elseif success and not isEmpty(title) and title.exists then
return true
end
end
return false
end
-- Получить категорию для отсутствия изображений
local function getFileCategory( frame, name, property, entityId )
if isEmpty(name) then
return ''
end
frame = frame or mw.getCurrentFrame()
local propValues = getWikidataProperty( frame, property, entityId, 3 )
local catName = getCategoryName( name, property )
local instanceOf = getWikidataProperty( frame, 'p31', entityId, 3 )
instanceOf = #instanceOf > 0 and instanceOf[ 1 ] or ''
local pageTitle = mw.title.getCurrentTitle().fullText
local result = string.format('['..'[Category:%s|%s%s]'..']', catName,
instanceOf, pageTitle )
return #propValues, result
end
-- Берет таблицу schema и на сновании неё возвращает пару значений: статус и
-- таблицу значений в соответствии со схемой. Значения берутся из конфига или из
-- параметров фрейма. Используется для загрузки настроек обработчиков.
local function populateSettings(func, schema, frameArgs, currentRule)
local myname = 'populateSettings'
local result = {}
-- обертка для mw.ustring.find
local function patternCheckPassed(settingVal, pattern)
if pattern ~= nil and type(settingVal) == 'string' then
return mw.ustring.find(settingVal, pattern)
end
return true
end
-- проверка типов, тип м.б. передан строкой или таблицей
local function typeCheckPassed(settingVal, typeDesc)
if isEmpty(typeDesc) then return true end
if type(typeDesc) == 'string' then
typeDesc = {typeDesc}
end
for _, curType in ipairs(typeDesc) do
if type(settingVal) == curType then return true end
end
return false
end
for settingName, propsTab in pairs(schema) do
local valueIndex = propsTab.valueIndex or settingName
local settingVal = currentRule.options[settingName]
-- нет в конфиге
if settingVal == nil then
if propsTab.mandatory then
debLog:write('Mandatory setting is empty: '..settingName, myname, 'warn')
return false
-- записываем только если не установлен флаг overrided или значения
-- нет. Если есть и флаг true, пропускаем
elseif not propsTab['overrided'] or isEmpty(result[valueIndex]) then
-- пишем дважды на случай, если settingName ~= valueIndex
result[settingName] = propsTab.default
result[valueIndex] = propsTab.default
end
-- тип не совпадает
elseif not typeCheckPassed(settingVal, propsTab['type']) then
debLog:write('Type mismatch in config: ' .. settingName, myname, 'warn')
return false
-- паттерн не совпадает
elseif not patternCheckPassed(settingVal, propsTab['pattern']) then
local msg = string.format('Pattern mismatch: %s', settingName)
debLog:write(msg, myname, 'warn')
return false
elseif propsTab['external'] then --настройки, читаемые из фрейма
local settExtVal = frameArgs[settingVal]
if not patternCheckPassed(settExtVal, propsTab['valuePattern']) then
local err = string.format('Pattern mismatch: %s', settingVal)
debLog:write(err, myname, 'warn')
return false
elseif not propsTab['overrided'] or isEmpty(result[valueIndex]) then
-- если помечено overrided, то заполняем если иных значений нет
result[settingName] = settingVal
result[valueIndex] = settExtVal
elseif propsTab['overrided'] then
-- в любом случае пишем сам параметр
result[settingName] = settingVal
end
elseif not propsTab['overrided'] or isEmpty(result[valueIndex]) then
result[settingName] = settingVal
result[valueIndex] = settingVal
elseif propsTab['overrided'] then
result[settingName] = settingVal
end
end
return true, result
end
--------------------------------------------------------------------------------
--[[ Обработчики ]]
-- Вызываются из processRule() в виде
-- p[funcName](frameArgs, preset, currentRule)
-- frameArgs - параметры фрейма
-- preset - таблица с пресетом. Валидность проверяется
-- на этапе препроцессинга. В обработчиках не проверяется.
-- currentRule - таблица с описанием иекущего правила из конфига. Валидируется
-- на этапе препроцессинга.
--------------------------------------------------------------------------------
--[[ function p._sortByProperty() ]]
-- Формирует категории в разрезе указанного свойства
-- Описание используемых настроек конфига и соответствующей логики см. в коде.
function p._sortByProperty( frameArgs, preset, currentRule, entityId )
local myname = '_sortByProperty'
debLog:write('Invoked', myname)
local settingsSchema = schemas.workers._sortByProperty.configOptions
------
-- 1) загрузка из конфига и валидация по схеме в таблицу configOptions
-- 2) getSettings (configOptions, frameArgs) => settings - здесь описаны правила трансформации
-- в виде { [<setting name>] = f({configOptions}, {frameArgs}) }
-- например f может быть:
-- setting1 = configOption[index1] or frameArgs[index2] - аргументы фрейма используются как дефолтное значение
-- setting2 = frameArgs[index3] or configOption[index4] - аргументы фрейма перекрывают конфиг
-- setting3 = frameArgs[ configOption[index5] ] or configOption[index6] - из фрейма берется параметр с именем,
-- заданным в конфиге, и перекрывает конфиг
-- setting4 = configOption[i7] or frameArgs[ configOption[i8] ] - из фрейма бьерется параметр с именем,
-- заданным в конфиге, и используется как дефолт
-- setting5 = configOption[i9] or configOption[i10] - одна из двух опций конфига должна быть задана, первая с приоритетом,
-- может использоваться как булева опция "или"
-- setting6 = configOption[i10] and configOption[i11] - "обе должны быть заданы" (как булева опция "и")
-- или "вторая опция при условии наличия первой"
-- итого набор правил трансформации:
-- A [or/and] B, где А и В принимают значения из:
-- configOption[<index>]
-- frameArgs[<index>]
-- frameArgs[ configOption[<index>] ]
-- configOptions:
-- ...
-- ifNotEmptyPar = 'not-empty-argument',
-- ifNotEmpty = 'config value',
--...
-- in {settings} result should be:
-- settings.ifNotEmptyVal = frameArgs[ configOption[ifNotEmptyPar] ] or configOption[ifNotEmpty]
-- settings.checkIfNotEmptyVal = configOption[ifNotEmptyPar] or configOption[ifNotEmpty] // effectively boolean
local succ, settings = populateSettings(myname, settingsSchema,
frameArgs, currentRule)
if succ then
debLog:write('Settings populated: ' .. mw.dumpObject(settings), myname)
else
debLog:write('Settings population failed', myname, 'warn')
return nil
end
-- [[логические препроверки]]
-- передано значение, которое д.б. пустым
if settings.ifEmptyVal and settings.ifEmptyPar then
local msg = string.format('Value not empty: |%s=%s, skipped',
settings.ifEmptyPar, settings.ifEmptyVal)
debLog:write(msg, myname )
return ''
end
-- не передано значение, которое д.б. НЕпустым
if not settings.ifNotEmptyVal and settings.ifNotEmptyPar then
local msg = string.format('Value is empty: |%s=%s, skipped',
settings.ifNotEmptyPar, settings.ifNotEmptyVal)
debLog:write(msg, myname)
return ''
end
-- получение ID сущности Викиданных
entityId = entityId or frameArgs['from']
if isEmpty(entityId) then
entityId = mw.wikibase.getEntityIdForCurrentPage()
end
-- Если сущности нет
if isEmpty(entityId) then
debLog:write('No Entity ID for current page', myname)
-- попытка использовать дефолтное значение
if not isEmpty(settings.propDefVal) then
-- сюда формирование результата на основе дефолтного значения
else -- дефолтного тоже нет
-- сюда результаты сортировки вида "тип не указан"/"значение св-ва не указано"
return ''
end
-- сюда часть снизу
end
-- получение свойства
local statements = mw.wikibase.getBestStatements(entityId, settings.property)
if isEmpty(statements) or next(statements) == nil then
debLog:write('No claims with property ' .. settings.property, myname)
return ''
end
--временно:
debLog:write('Result from WD: ' .. mw.dumpObject(statements[1].mainsnak.datavalue.value), myname)
-- на данном этапе проперти загружены и есть как минимум один результат с клеймом
-- постпроверки условий (или перенести перед?)
-- доделать заглушки
-- проверка, что свойство propId содержит значение propVal
local function ifPropContains(entityId, propId, propVal)
-- заглушка
return true
end
-- проверка, что свойство propId НЕ содержит значение propVal
local function ifPropNotContains(entityId, propId, propVal)
-- заглушка
return true
end
local function ifEmptyProp(entityId, propId)
-- заглушка
return true
end
local function ifNotEmptyProp(entityId, propId)
-- заглушка
return true
end
-- Проверка, что свойство содержит требуемое в настройках значение
if ifPropContains(entityId, settings.ifPropContainsID, settings.ifPropContainsVal) then
debLog:write('Property contains required value, ok', myname )
else
debLog:write('Property does not contain required value', myname)
return ''
end
-- Проверка, что свойство НЕ содержит требуемое в настройках значение
if ifPropNotContains(entityId, settings.ifPropContainsID, settings.ifPropContainsVal) then
debLog:write('Property does not contain value required to be not contained, ok', myname )
else
debLog:write('Property contains value required to be not contained', myname)
return ''
end
-- Проверка, что свойства не содержат никаких утверждений
if ifEmptyProp(entityId, settings.ifEmptyPropsVal) then
debLog:write('Properties required to be empty are empty, ok', myname )
else
debLog:write('Properties required to be empty are not empty', myname)
return ''
end
-- Проверка, что свойства содержат хоть какие-то утверждения
if ifNotEmptyProp(entityId, settings.ifNotEmptyPropsVal) then
debLog:write('Properties required to be not empty are not empty, ok', myname )
else
debLog:write('Properties required to be not empty are empty', myname)
return ''
end
local function getPropLabel(entityId, propId)
-- заглушка
return 'someproplabel'
end
local propLabel = settings.propLabel
if isEmpty(propLabel) then
propLabel = getPropLabel(entityId, settings.property)
end
-- ... формирование результата и проверка категорий
local chains = {}
for i, statement in ipairs(statements) do
-- тут может быть обычное вэлью, надо дописывать
debLog:write(statement.mainsnak.datavalue.value, myname,'warn')
if type(statement.mainsnak.datavalue.value) == 'table' then
local valueLabel = mw.wikibase.getLabel(statement.mainsnak.datavalue.value.id)
local chain = {}
chains[i]={propLabel, valueLabel} -- это неправильно, не хватает уровня вложенности, но потом все равно менять
end
end
-- это временно и неправильно
local catName = formatCategoryName(preset, chains)
local cat = formatCategory (catName)
local result = cat
--[[
-- проверка, что есть следующий уровень
local thenBy = currentRule.thenBy
-- рекурсивный вызов
if not isEmpty( thenBy ) and type( thenBy ) == 'table' then
p._sortByProperty(frameArgs, preset, thenBy)
end
--]]
return result
end
function p._catByProperty(frameArgs, preset, currentRule)
return nil
end
function p._catLocalFileWithoutWD(frameArgs, preset, currentRule)
return nil
end
function p._catNoEntity(frameArgs, preset, currentRule)
return nil
end
function p._category(frameArgs, preset, currentRule)
if currentRule.options.category ~= nil then
return formatCategory(currentRule.options.category)
else
return nil
end
end
--------------------------------------------------------------------------------
-- General processing
--------------------------------------------------------------------------------
--- Load config from path 'configPath'
--@param configPath string
--@return true on success or or nil
local function getConfig(configPath)
if configPath == nil or configPath == '' then
debLog:write('No log provided', 'getConfig', 'error')
return nil
end
local success, result = pcall(mw.loadData, configPath)
if success then
config = result
debLog:write('Config loaded: '..configPath, 'getConfig')
return true
end
debLog:write(result, 'getConfig', 'error')
return nil
end
--- Get 'options' frome config and sets them to 'globals'
--@param node - 'config' node
--@return globals table or nil if nothing is set
local function setGlobalOptions()
if type(config.options) ~= 'table' then return nil end
for k, option in pairs(config.options) do
if option == 'ignoreNSChecks' then
globals.ignoreNSChecks = globals.ignoreNSChecks or option
else
globals[k] = option
end
end
debLog:write('Globals set: '..mw.dumpObject(globals), 'setGlobalOptions')
return globals
end
--- Global options processing
--@param frameArgs
--@return true on ok, false on checks failed
local function processGlobalOptions(frameArgs)
local nocat = frameArgs[globals.nocatParamName]
if nocat ~= nil and nocat ~= '' then
globals.nocat = nocat
debLog:write('Categorization denied', 'cp:processGlobalOptions')
return false
end
local from = frameArgs[globals.fromParamName]
if from ~= nil and from ~= '' then
if not mw.wikibase.isValidEntityId(from) then
debLog:write('EntityID not valid: '.. from, 'processGlobalOptions')
return false
end
if not mw.wikibase.entityExists(from) then
debLog:write('Entity does not exist:'.. from, 'processGlobalOptions')
return false
end
globals.from = from
end
debLog:write('Globals: '.. mw.dumpObject(globals), 'processGlobalOptions')
return true
end
--- Takes a named node in config with name 'name from node 'upNode', adds
-- default values, defined on the same level, and returns resulting table
-- @param upNode - previous node. e.g. 'a' for: a = { <name> = { } }
-- @param name string
local function getConfigNodeWithDefaults(upNode, name)
local node = upNode[name]
local default = upNode['default']
if type(node) == nil then return default end
if type(node) ~= 'table' then return node end
if type(default) ~= 'table' then return node end
local result = {}
for k, v in pairs(default) do
result[k] = v
end
for k, v in pairs(node) do
result[k] = v
end
return result
end
--- Gets filter table from table 'node'
-- @function getFilters
-- @param node
-- @return table or nil
local function getFilters(node)
local filters = node['filters']
if type(filters) ~= 'table' then return nil end
return filters
end
--- Вызывает один за другим фильтры из массива filters
-- Если фильтров нет или что-то сконфигурировано неверно, возвращает true
-- Если хотя бы один из фильтров отработал и выдал false, возвращает false
-- Если все фильтры вернули true, возвращает true
-- @function processFilters
-- @param filtMap таблица-список фильтров вида {<filterID> = <func>, ...}
-- @param filters массив с фильтрами вида
-- { {name = <filterID>, options = {<opt1> = val1, ... } ... }
-- @param frameArgs таблица с параметрами фрейма
-- @return true если все фильтры вернули true или проблемы с параметрами
-- false если хотя бы один вернул false
local function processFilters(filtMap, filters, frameArgs)
debLog:write('invoked ', 'processFilters')
if type(filtMap) ~= 'table' then return true end
if type(filters) ~= 'table' then return true end
local function default() return true end
for i, filter in ipairs(filters) do
debLog:write('processing name: '.. tostring(filter.id), 'processFilters')
local filterFunc = filtMap[filter.id] or filtMap[default] or default
if not filterFunc(filter.options, frameArgs) then
return false
end
end
return true
end
--- Get preset 'name' from table 'node' and enriches it with default values
--@function getPreset
--@param name string
--@return table or nil
local function preparePreset(name)
local workPreset = getConfigNodeWithDefaults(config.presets, name)
if type(workPreset) ~= 'table' then return nil end
return workPreset
end
--- Loads list of strings with rules' IDs from array 'node', and returns an
-- array with those of them, which are also exist as indexes in 'ruleMap'
-- @return array with valid rules
local function getRules(ruleMap, node)
local rules = node['rules']
if type(rules) ~= 'table' then return nil end
local validRules = {}
local i = 1
for _, ruleName in ipairs(rules) do
if ruleMap[ruleName] then
validRules[i] = ruleName
i = i + 1
end
end
if next(validRules) == nil then
debLog:write('No valid rulenames found', 'getRules', 'warn')
return nil
end
debLog:write('Valid rules found: '.. mw.dumpObject(validRules), 'getRules')
return validRules
end
--- Executes a function, corresponding to rule with id 'ruleName'
--@param ruleName string with rule id
--@param node node with rule
--@param frameArgs table
--@param preset table with preset
--@return result of a called function or nil
local function processRule(node, frameArgs, preset)
local myname = 'processRule'
debLog:write('Invoked', myname)
-- check func name presence and validity
if type(p[node.func]) ~= 'function' then
debLog:write('Function does not exist: ' .. node.func, myname, 'warn')
return nil
end
-- function call
local funcOptions = node.options -- вставить в вызов вместо пресета
local funcResult = p[node.func](frameArgs, preset, node)
if funcResult == nil then
debLog:write(node.func .. '() returned nil', myname, 'warn')
return ''
end
debLog:write(node.func .. ' returned: '..mw.text.nowiki(funcResult), myname)
return funcResult
end
---Processes rules from array of IDs in ruleList
--@return concatenated result of rules
local function processRuleList(ruleList, node, frameArgs, preset)
local result = ''
for _, ruleName in ipairs(ruleList) do
local ruleResult = processRule(node[ruleName], frameArgs, preset)
if ruleResult == nil then
debLog:write('Rule returned nil', 'processRuleList', 'warn')
else
result = table.concat{result, ruleResult}
end
end
return result
end
--------------------------------------------------------------------------------
-- Методы
--------------------------------------------------------------------------------
-- временная обертка для старых методов, потом содержимое перенести в main
local function pcall_main(func, ...)
local success, mainstate, result = pcall(p._main, unpack(arg))
if not success then
debLog:write(mainstate, '_main', 'error')
return ''
elseif not mainstate then
debLog:write(result, '_main')
return ''
else
return result
end
end
-- Шаблон сортировки по типам
function p.byType(frame)
local frameArgs = getArgs(frame)
local presetName = frameArgs[1]
return pcall_main(p._main, frameArgs, presetName)
end
-- Шаблон сортировки по странам
function p.byCountry(frame)
local frameArgs = getArgs(frame)
local presetName = frameArgs[1]
return pcall_main(p._main, frameArgs, presetName)
end
-- Шаблон сортировки по изображениям
function p.byImage(frame)
local frameArgs = getArgs(frame)
local presetName = 'статьи без изображений'
return pcall_main(p._main, frameArgs, presetName)
end
--- Call for debug and logs
function p.loggedCall(frame)
globals.ignoreNSChecks = true
debLog.enabled = true
local frameArgs = getArgs(frame)
debLog:write('Invoked with args: '.. mw.dumpObject(frameArgs), 'loggedCall')
local functionName = frameArgs['function']
local success, result = pcall(p[functionName], frame)
if not success then
debLog:write(result, 'loggedCall', 'error')
return debLog:getAll()
end
return debLog:getAll() .. mw.text.nowiki(result)
end
---Основная функция.
-- параметр presetName нужен для поддержки старых методов типа byImage и т.п.
-- Больше низачем не нужен, потом надо убрать
function p._main (frameArgs, presetName)
debLog:write('Invoked with args: '..mw.dumpObject(frameArgs), '_main')
--совместимость со старыми методами вызова
local presetName = presetName or frameArgs['preset']
-- inits and checks
local configPath = frameArgs['config'] or DEFAULT_CONFIG
if not getConfig(configPath) then
return nil, 'Config load failed'
end
setGlobalOptions()
if not processGlobalOptions(frameArgs) then
return nil, 'Global options interruption'
end
-- get preset enriched with defaults
local workPreset = preparePreset(presetName)
if workPreset == nil then
return nil, 'Preset is empty'
end
-- check page against preset filters
local filters = getFilters(workPreset)
if not processFilters(FILTER_FUNCTIONS, filters, frameArgs) then
return nil, 'No filter matches'
end
-- get valid rules from preset and check vs. rules' definitions
local ruleList = getRules(config.rules, workPreset)
if ruleList == nil then
return nil, 'Rule list is nil'
end
-- process rules and concat result
local rulesResult = processRuleList(ruleList, config.rules, frameArgs, workPreset)
if rulesResult == nil then
return nil, 'Rules result is nil'
end
return true, rulesResult
end
function p.main(frame)
local frameArgs = getArgs(frame)
return pcall_main(p._main, frameArgs)
end
return p