Module:Arguments
Jump to navigation
Jump to search
Documentation for this module may be created at Module:Arguments/doc
1 -- This module provides easy processing of arguments passed to Scribunto from
2 -- #invoke. It is intended for use by other Lua modules, and should not be
3 -- called from #invoke directly.
4
5 local libraryUtil = require('libraryUtil')
6 local checkType = libraryUtil.checkType
7
8 local arguments = {}
9
10 -- Generate four different tidyVal functions, so that we don't have to check the
11 -- options every time we call it.
12
13 local function tidyValDefault(key, val)
14 if type(val) == 'string' then
15 val = val:match('^%s*(.-)%s*$')
16 if val == '' then
17 return nil
18 else
19 return val
20 end
21 else
22 return val
23 end
24 end
25
26 local function tidyValTrimOnly(key, val)
27 if type(val) == 'string' then
28 return val:match('^%s*(.-)%s*$')
29 else
30 return val
31 end
32 end
33
34 local function tidyValRemoveBlanksOnly(key, val)
35 if type(val) == 'string' then
36 if val:find('%S') then
37 return val
38 else
39 return nil
40 end
41 else
42 return val
43 end
44 end
45
46 local function tidyValNoChange(key, val)
47 return val
48 end
49
50 local function matchesTitle(given, title)
51 local tp = type( given )
52 return (tp == 'string' or tp == 'number') and mw.title.new( given ).prefixedText == title
53 end
54
55 local translate_mt = { __index = function(t, k) return k end }
56
57 function arguments.getArgs(frame, options)
58 checkType('getArgs', 1, frame, 'table', true)
59 checkType('getArgs', 2, options, 'table', true)
60 frame = frame or {}
61 options = options or {}
62
63 --[[
64 -- Set up argument translation.
65 --]]
66 options.translate = options.translate or {}
67 if getmetatable(options.translate) == nil then
68 setmetatable(options.translate, translate_mt)
69 end
70 if options.backtranslate == nil then
71 options.backtranslate = {}
72 for k,v in pairs(options.translate) do
73 options.backtranslate[v] = k
74 end
75 end
76 if options.backtranslate and getmetatable(options.backtranslate) == nil then
77 setmetatable(options.backtranslate, {
78 __index = function(t, k)
79 if options.translate[k] ~= k then
80 return nil
81 else
82 return k
83 end
84 end
85 })
86 end
87
88 --[[
89 -- Get the argument tables. If we were passed a valid frame object, get the
90 -- frame arguments (fargs) and the parent frame arguments (pargs), depending
91 -- on the options set and on the parent frame's availability. If we weren't
92 -- passed a valid frame object, we are being called from another Lua module
93 -- or from the debug console, so assume that we were passed a table of args
94 -- directly, and assign it to a new variable (luaArgs).
95 --]]
96 local fargs, pargs, luaArgs
97 if type(frame.args) == 'table' and type(frame.getParent) == 'function' then
98 if options.wrappers then
99 --[[
100 -- The wrappers option makes Module:Arguments look up arguments in
101 -- either the frame argument table or the parent argument table, but
102 -- not both. This means that users can use either the #invoke syntax
103 -- or a wrapper template without the loss of performance associated
104 -- with looking arguments up in both the frame and the parent frame.
105 -- Module:Arguments will look up arguments in the parent frame
106 -- if it finds the parent frame's title in options.wrapper;
107 -- otherwise it will look up arguments in the frame object passed
108 -- to getArgs.
109 --]]
110 local parent = frame:getParent()
111 if not parent then
112 fargs = frame.args
113 else
114 local title = parent:getTitle():gsub('/sandbox$', '')
115 local found = false
116 if matchesTitle(options.wrappers, title) then
117 found = true
118 elseif type(options.wrappers) == 'table' then
119 for _,v in pairs(options.wrappers) do
120 if matchesTitle(v, title) then
121 found = true
122 break
123 end
124 end
125 end
126
127 -- We test for false specifically here so that nil (the default) acts like true.
128 if found or options.frameOnly == false then
129 pargs = parent.args
130 end
131 if not found or options.parentOnly == false then
132 fargs = frame.args
133 end
134 end
135 else
136 -- options.wrapper isn't set, so check the other options.
137 if not options.parentOnly then
138 fargs = frame.args
139 end
140 if not options.frameOnly then
141 local parent = frame:getParent()
142 pargs = parent and parent.args or nil
143 end
144 end
145 if options.parentFirst then
146 fargs, pargs = pargs, fargs
147 end
148 else
149 luaArgs = frame
150 end
151
152 -- Set the order of precedence of the argument tables. If the variables are
153 -- nil, nothing will be added to the table, which is how we avoid clashes
154 -- between the frame/parent args and the Lua args.
155 local argTables = {fargs}
156 argTables[#argTables + 1] = pargs
157 argTables[#argTables + 1] = luaArgs
158
159 --[[
160 -- Generate the tidyVal function. If it has been specified by the user, we
161 -- use that; if not, we choose one of four functions depending on the
162 -- options chosen. This is so that we don't have to call the options table
163 -- every time the function is called.
164 --]]
165 local tidyVal = options.valueFunc
166 if tidyVal then
167 if type(tidyVal) ~= 'function' then
168 error(
169 "bad value assigned to option 'valueFunc'"
170 .. '(function expected, got '
171 .. type(tidyVal)
172 .. ')',
173 2
174 )
175 end
176 elseif options.trim ~= false then
177 if options.removeBlanks ~= false then
178 tidyVal = tidyValDefault
179 else
180 tidyVal = tidyValTrimOnly
181 end
182 else
183 if options.removeBlanks ~= false then
184 tidyVal = tidyValRemoveBlanksOnly
185 else
186 tidyVal = tidyValNoChange
187 end
188 end
189
190 --[[
191 -- Set up the args, metaArgs and nilArgs tables. args will be the one
192 -- accessed from functions, and metaArgs will hold the actual arguments. Nil
193 -- arguments are memoized in nilArgs, and the metatable connects all of them
194 -- together.
195 --]]
196 local args, metaArgs, nilArgs, metatable = {}, {}, {}, {}
197 setmetatable(args, metatable)
198
199 local function mergeArgs(tables)
200 --[[
201 -- Accepts multiple tables as input and merges their keys and values
202 -- into one table. If a value is already present it is not overwritten;
203 -- tables listed earlier have precedence. We are also memoizing nil
204 -- values, which can be overwritten if they are 's' (soft).
205 --]]
206 for _, t in ipairs(tables) do
207 for key, val in pairs(t) do
208 if metaArgs[key] == nil and nilArgs[key] ~= 'h' then
209 local tidiedVal = tidyVal(key, val)
210 if tidiedVal == nil then
211 nilArgs[key] = 's'
212 else
213 metaArgs[key] = tidiedVal
214 end
215 end
216 end
217 end
218 end
219
220 --[[
221 -- Define metatable behaviour. Arguments are memoized in the metaArgs table,
222 -- and are only fetched from the argument tables once. Fetching arguments
223 -- from the argument tables is the most resource-intensive step in this
224 -- module, so we try and avoid it where possible. For this reason, nil
225 -- arguments are also memoized, in the nilArgs table. Also, we keep a record
226 -- in the metatable of when pairs and ipairs have been called, so we do not
227 -- run pairs and ipairs on the argument tables more than once. We also do
228 -- not run ipairs on fargs and pargs if pairs has already been run, as all
229 -- the arguments will already have been copied over.
230 --]]
231
232 metatable.__index = function (t, key)
233 --[[
234 -- Fetches an argument when the args table is indexed. First we check
235 -- to see if the value is memoized, and if not we try and fetch it from
236 -- the argument tables. When we check memoization, we need to check
237 -- metaArgs before nilArgs, as both can be non-nil at the same time.
238 -- If the argument is not present in metaArgs, we also check whether
239 -- pairs has been run yet. If pairs has already been run, we return nil.
240 -- This is because all the arguments will have already been copied into
241 -- metaArgs by the mergeArgs function, meaning that any other arguments
242 -- must be nil.
243 --]]
244 if type(key) == 'string' then
245 key = options.translate[key]
246 end
247 local val = metaArgs[key]
248 if val ~= nil then
249 return val
250 elseif metatable.donePairs or nilArgs[key] then
251 return nil
252 end
253 for _, argTable in ipairs(argTables) do
254 local argTableVal = tidyVal(key, argTable[key])
255 if argTableVal ~= nil then
256 metaArgs[key] = argTableVal
257 return argTableVal
258 end
259 end
260 nilArgs[key] = 'h'
261 return nil
262 end
263
264 metatable.__newindex = function (t, key, val)
265 -- This function is called when a module tries to add a new value to the
266 -- args table, or tries to change an existing value.
267 if type(key) == 'string' then
268 key = options.translate[key]
269 end
270 if options.readOnly then
271 error(
272 'could not write to argument table key "'
273 .. tostring(key)
274 .. '"; the table is read-only',
275 2
276 )
277 elseif options.noOverwrite and args[key] ~= nil then
278 error(
279 'could not write to argument table key "'
280 .. tostring(key)
281 .. '"; overwriting existing arguments is not permitted',
282 2
283 )
284 elseif val == nil then
285 --[[
286 -- If the argument is to be overwritten with nil, we need to erase
287 -- the value in metaArgs, so that __index, __pairs and __ipairs do
288 -- not use a previous existing value, if present; and we also need
289 -- to memoize the nil in nilArgs, so that the value isn't looked
290 -- up in the argument tables if it is accessed again.
291 --]]
292 metaArgs[key] = nil
293 nilArgs[key] = 'h'
294 else
295 metaArgs[key] = val
296 end
297 end
298
299 local function translatenext(invariant)
300 local k, v = next(invariant.t, invariant.k)
301 invariant.k = k
302 if k == nil then
303 return nil
304 elseif type(k) ~= 'string' or not options.backtranslate then
305 return k, v
306 else
307 local backtranslate = options.backtranslate[k]
308 if backtranslate == nil then
309 -- Skip this one. This is a tail call, so this won't cause stack overflow
310 return translatenext(invariant)
311 else
312 return backtranslate, v
313 end
314 end
315 end
316
317 metatable.__pairs = function ()
318 -- Called when pairs is run on the args table.
319 if not metatable.donePairs then
320 mergeArgs(argTables)
321 metatable.donePairs = true
322 end
323 return translatenext, { t = metaArgs }
324 end
325
326 local function inext(t, i)
327 -- This uses our __index metamethod
328 local v = t[i + 1]
329 if v ~= nil then
330 return i + 1, v
331 end
332 end
333
334 metatable.__ipairs = function (t)
335 -- Called when ipairs is run on the args table.
336 return inext, t, 0
337 end
338
339 return args
340 end
341
342 return arguments