יחידה:ParamValidator: הבדלים בין גרסאות בדף

הוצאת פונקציות שירות ליחידה נפרדת
מ גרסה אחת של הדף wikipedia:he:יחידה:ParamValidator יובאה
 
(48 גרסאות ביניים של 4 משתמשים אינן מוצגות)
שורה 4: שורה 4:


the source of this module is in //he.wikipedia.org/wiki/Module:ParamValidator
the source of this module is in //he.wikipedia.org/wiki/Module:ParamValidator
main purpose: use "templatedata" to verify the parameters passed to a template
Terminology: "numeric parameter" means order-based parameter. e.g. if the template is transcluded like so {{x  | k |  | a = m | b = }}
"a" and "b" are "named" parameters, and there are 2 "numeric", or order based parameters, 1 and 2.
we say that the value of a is "m", the value of 1 is "k", and "b" and 2 are "empty".


This module exports two functions: calculateViolations( frame, subpages ), and validateParams( frame ).  
This module exports two functions: calculateViolations( frame, subpages ), and validateParams( frame ).  
שורה 29: שורה 35:
* "empty-required": missing or empty parameter marked as "required" in tempaltedata
* "empty-required": missing or empty parameter marked as "required" in tempaltedata
* "incompatible": a non-empty parameter passed to the template, incompatible with the parameter type defined in templatedata  
* "incompatible": a non-empty parameter passed to the template, incompatible with the parameter type defined in templatedata  
* "duplicate": a value is passed for the same parameter (or any of its aliases) more than once




שורה 34: שורה 41:
it expects a parameter named "options", which contains the definition of the output. typically, it's used by placing something like so:
it expects a parameter named "options", which contains the definition of the output. typically, it's used by placing something like so:


<includeonly>{{#invoke:ParamValidatoe | validateParams | options = {{PV default options}} }}</includeonly>
<includeonly>{{#invoke:ParamValidator | validateParams | options = {{PV default options}} }}</includeonly>


at the top of the template (be mindful not to add extra spaces and newlines to the template).
at the top of the template (be mindful not to add extra spaces and newlines to the template).
to bypass some mediawiki limitation, it is also possible to pass the options as "module", like so (use one of the two, but not both):
<includeonly>{{#invoke:ParamValidator | validateParams | module_options = Module:PV default options}} }}</includeonly>
the first form expects a template named "Template:PV default options" which contains the options, and the 2nd form expects a module,
suitable for mw.loadData(), which returns a map of namespace => options (i.e. { [0] = <options>, [2] => <options> } .... )


the options parameter should be a JSON-encoded string, defining the output, and some special behaviors.  
the options parameter should be a JSON-encoded string, defining the output, and some special behaviors.  
שורה 90: שורה 102:
typically, this JSON structure will be placed in a separate template, and retrieved for the module-use as shown above.
typically, this JSON structure will be placed in a separate template, and retrieved for the module-use as shown above.
<includeonly>{{#invoke:ParamValidatoe | validateParams | options = {{PV default options}} | options1 = {"key":"value"} }}</includeonly>
<includeonly>{{#invoke:ParamValidator | validateParams | options = {{PV default options}} | options1 = {"key":"value"} }}</includeonly>
"key" can override any of the options fields described above.
"key" can override any of the options fields described above.


שורה 96: שורה 108:
]=]
]=]


local util = require( 'Module:Utility' )
local util = {
empty = function( s )
return s == nil  or type( s ) == 'string' and (mw.text.trim( s ) == '' or s == '-') -- compatible with module:תבנית מידע 
end
,
extract_options = function( frame, optionsPrefix )
optionsPrefix = optionsPrefix or 'options'
 
local options, n, more = {}
if frame.args['module_options'] then
local module_options = mw.loadData( frame.args['module_options'] )
if type( module_options ) ~= 'table' then return {} end
local title = mw.title.getCurrentTitle()
local local_ptions = module_options[ title.namespace ] or module_options[ title.nsText ] or {}
for k, v in pairs( local_ptions ) do options[k] = v end
end
repeat
ok, more = pcall( mw.text.jsonDecode, frame.args[optionsPrefix .. ( n or '' )] )
if ok and type( more ) == 'table' then
for k, v in pairs( more ) do options[k] = v end
end
n = ( n or 0 ) + 1
until not ok
 
return options
end
,
build_namelist = function ( template_name, sp )
local res = { template_name }
if sp then
if type( sp ) == 'string' then sp = { sp } end
for _, p in ipairs( sp ) do table.insert( res, template_name .. '/' .. p ) end
end
return res
end
,
table_empty = function( t ) -- normally, test if next(t) is nil, but for some perverse reason, non-empty tables returned by loadData return nil...
if type( t ) ~= 'table' then return true end
for a, b in pairs( t ) do return false end
return true
end
,
}
 
local function _readTemplateData( templateName )
local title = mw.title.makeTitle( 0, templateName ) 
local templateContent = title and title.exists and title:getContent() -- template's raw content
local capture =  templateContent and mw.ustring.match( templateContent, '<templatedata%s*>(.*)</templatedata%s*>' ) -- templatedata as text
-- capture = capture and mw.ustring.gsub( capture, '"(%d+)"', tonumber ) -- convert "1": {} to 1: {}. frame.args uses numerical indexes for order-based params.
local trailingComma = capture and mw.ustring.find( capture, ',%s*[%]%}]' ) -- look for ,] or ,} : jsonDecode allows it, but it's verbotten in json
if capture and not trailingComma then return pcall( mw.text.jsonDecode, capture ) end
return false
end
 
local function readTemplateData( templateName )
if type( templateName ) == 'string' then
templateName = { templateName, templateName .. '/' .. docSubPage }
end
if type( templateName ) == "table" then
for _, name in ipairs( templateName ) do
local td, result = _readTemplateData( name )  
if td then return result end
end
end
return nil
end
 


-- this is the function to be called by other modules. it expects the frame, and then an optional list of subpages, e.g. { "Documentation" }.
-- this is the function to be called by other modules. it expects the frame, and then an optional list of subpages, e.g. { "Documentation" }.
-- if second parameter is nil, only tempalte page will be searched for templatedata.
-- if second parameter is nil, only tempalte page will be searched for templatedata.
function calculateViolations( frame, subpages )
local function calculateViolations( frame, subpages )
 
-- used for parameter type validy test. keyed by TD 'type' string. values are function(val) returning bool.
-- this can be made more sophisticated later, e.g. for "wiki page" and such
local type_validators = {
function compatible( val, typ )
['number'] = function( s ) return mw.language.getContentLanguage():parseFormattedNumber( s ) end
if typ == 'number' and not mw.language.getCurrentLanguage():parseNumber( val ) then return false end
}
return true
local function compatible( typ, val )
local func = type_validators[typ]
return type( func ) ~= 'function' or util.empty( val ) or func( val )
end
local function list_empty_or_contains(ar, searched)
if not ar or #ar == 0 then return true end  
for _, val in ipairs(ar) do if val == searched then return true end end
return false
end
end
שורה 111: שורה 200:
local t_args, template_name = t_frame.args, t_frame:getTitle()
local t_args, template_name = t_frame.args, t_frame:getTitle()
local td_source = util.build_namelist( template_name, subpages )
local td_source = util.build_namelist( template_name, subpages )
local templatedata = require( 'Module:ReadTd' ).ReadTemplateData( td_source )
local templatedata = readTemplateData( td_source )
local td_params = templatedata and templatedata.params
local td_params = templatedata and templatedata.params
local all_aliases, all_series = {}, {}
if not td_params then return { ['no-templatedata'] = { [''] = '' } } end
if not td_params then return { ['no-templatedata'] = { [''] = '' } } end
שורה 118: שורה 209:


local res = {} -- before returning to caller, we'll prune empty tables
local res = {} -- before returning to caller, we'll prune empty tables
-- allow for aliases
for _, p in pairs( td_params ) do for _, alias in ipairs( p.aliases or {} ) do
all_aliases[alias] = p
if tonumber(alias) then all_aliases[tonumber(alias)] = p end
end end


-- handle undeclared and deprecated
-- handle undeclared and deprecated
local already_seen = {}
local series = frame.args['series']
for p_name, value in pairs( t_args ) do
for p_name, value in pairs( t_args ) do
local tp_param, noval, numeric, table_name = td_params[p_name], util.empty( value ), tonumber( p_name )
local tp_param, noval, numeric, table_name = td_params[p_name] or all_aliases[p_name], util.empty( value ), tonumber( p_name )
local hasval = not noval
 
if not tp_param and series then -- 2nd chance. check to see if series
for s_name, p in pairs(td_params) do
if mw.ustring.match( p_name, '^' .. s_name .. '%d+' .. '$') then
-- mw.log('found p_name '.. p_name .. '  s_name:' .. s_name, ' p is:', p) debugging series support
tp_param = p
end -- don't bother breaking. td always correct.
end
end
if not tp_param then -- not in TD: this is called undeclared
if not tp_param then -- not in TD: this is called undeclared
שורה 128: שורה 237:
noval and numeric and 'empty-undeclared-numeric' or
noval and numeric and 'empty-undeclared-numeric' or
noval and not numeric and 'empty-undeclared' or
noval and not numeric and 'empty-undeclared' or
not noval and numeric and 'undeclared-numeric' or
hasval and numeric and 'undeclared-numeric' or
'undeclared' -- tzvototi nishar.
'undeclared' -- tzvototi nishar.
else -- in td: test for depracation and mistype. if deprecated, no further tests
else -- in td: test for deprecation and mistype. if deprecated, no further tests
table_name = tp_param.deprecated and not noval and 'deprecated' or
table_name = tp_param.deprecated and hasval and 'deprecated'  
tp_param.deprecated and noval and 'empty-deprecated' or
or tp_param.deprecated and noval and 'empty-deprecated'  
not compatible( tp_param.type, value ) and 'incompatible'
or not compatible( tp_param.type, value ) and 'incompatible'  
or not series and already_seen[tp_param] and hasval and 'duplicate'
or hasval and not list_empty_or_contains(tp_param.suggestedvalues , value) and 'unsuggested-value'
 
already_seen[tp_param] = hasval
end
end
-- report it.
-- report it.
שורה 141: שורה 255:
end
end
end
end
 
-- test for empty/missing paraeters declared "required"  
-- test for empty/missing paraeters declared "required"  
for p_name, param in pairs( td_params ) do  
for p_name, param in pairs( td_params ) do  
if param.required and util.empty( t_args[p_name] ) then  
if param.required and util.empty( t_args[p_name] ) then
res['empty-required'] = res['empty-required'] or {}  
local is_alias
res['empty-required'][p_name] = ''  
for _, alias in ipairs( param.aliases or {} ) do is_alias = is_alias or not util.empty( t_args[alias] ) end
if not is_alias then
res['empty-required'] = res['empty-required'] or {}  
res['empty-required'][p_name] = ''  
end
end
end
end
end
return res
return res
end
-- wraps report in hidden frame
local function wrapReport(report, template_name, options)
if util.empty( report ) then return '' end
local naked = mw.title.new( template_name )['text']
mw.log(report)
report = ( options['wrapper-prefix'] or "<div class = 'paramvalidator-wrapper'><span class='paramvalidator-error'>" )
.. report
.. ( options['wrapper-suffix'] or "</span></div>" )
report = mw.ustring.gsub( report, 'tname_naked', naked )
report = mw.ustring.gsub( report, 'templatename', template_name )
return report
end
end


-- this is the "user" version, called with {{#invoke:}} returns a string, as defined by the options parameter
-- this is the "user" version, called with {{#invoke:}} returns a string, as defined by the options parameter
function validateParams( frame )
local function validateParams( frame )
-- for purple pages:
if frame:getParent().args['skip parameters validation'] then return '[[ קטגוריה:דפים עם שגיאות פרמטריות שקיבלו חנינה]]' end
local options, report, template_name = util.extract_options( frame ), '', frame:getParent():getTitle()
local options, report, template_name = util.extract_options( frame ), '', frame:getParent():getTitle()
local wrap_report = function()
if util.empty( report ) then return '' end
local naked = mw.title.new( template_name )['text']
report = ( options['wrapper-prefix'] or "<div class = 'paramvalidator-wrapper'>" )
.. report
.. ( options['wrapper-suffix'] or "</div>" )
report = mw.ustring.gsub( report, 'tname_naked', naked )
report = mw.ustring.gsub( report, 'templatename', template_name )
return report
end


local ignore = function( p_name )
local ignore = function( p_name )
שורה 178: שורה 301:


local replace_macros = function( s, param_names )
local replace_macros = function( s, param_names )
local function concat_and_escape( t )
local s = table.concat( t, ', ' )
return ( mw.ustring.gsub( s, '%%', '%%%%' ) )
end
if s and ( type( param_names ) == 'table' ) then
if s and ( type( param_names ) == 'table' ) then
local k_ar, kv_ar = {}, {}
local k_ar, kv_ar = {}, {}
שורה 184: שורה 312:
table.insert( kv_ar, k .. ': ' .. v)
table.insert( kv_ar, k .. ': ' .. v)
end
end
s = mw.ustring.gsub( s, 'paramname', table.concat( k_ar, ', ' ) )
s = mw.ustring.gsub( s, 'paramname', concat_and_escape( k_ar ) )  
s = mw.ustring.gsub( s, 'paramandvalue', table.concat( kv_ar, ', ' ) )
s = mw.ustring.gsub( s, 'paramandvalue', concat_and_escape( kv_ar ) )
end
end
return s
return s
שורה 195: שורה 323:
return res
return res
end
end
 
-- assert options is not empty.
-- no option no work.
assert( next( options ), 'expecting valid "options" parameter')
if util.table_empty( options ) then return '' end
 
-- get the errors.
-- get the errors.
local violations = calculateViolations( frame, options['doc-subpage'] )
local violations = calculateViolations( frame, options['doc-subpage'] )
-- special request of bora: use skip_empty_numeric
-- special request of bora: use skip_empty_numeric
if violations['empty-undeclared-numeric'] then  
if violations['empty-undeclared-numeric'] then  
שורה 215: שורה 342:
for pname in pairs( tab ) do if ignore( pname ) then tab[pname] = nil end end
for pname in pairs( tab ) do if ignore( pname ) then tab[pname] = nil end end
-- prune empty violations
-- prune empty violations
if next( tab ) == nil then violations[name] = nil end
if util.table_empty( tab ) then violations[name] = nil end
-- WORK IS DONE. report the errors.
-- WORK IS DONE. report the errors.
-- if report then count it.
-- if report then count it.
שורה 223: שורה 350:
if offenders > 1 then report_params( 'multiple' ) end
if offenders > 1 then report_params( 'multiple' ) end
if offenders ~= 0 then report_params( 'any' ) end -- could have tested for empty( report ), but since we count them anyway...
if offenders ~= 0 then report_params( 'any' ) end -- could have tested for empty( report ), but since we count them anyway...
 
return wrapReport(report, template_name, options)
return wrap_report()
end
end


return {
return {
['validateparams'] = validateParams,
['validateparams'] = validateParams,
['calculateViolations'] = calculateViolations
['calculateViolations'] = calculateViolations,
['wrapReport'] = wrapReport
}
}