Module:Buffer

From WikiProjectMed
Jump to navigation Jump to search

Documentation for this module may be created at Module:Buffer/doc

  1 --[[=============================
  2 This Module was written by Alexander Zhikun He, also known as, User:Codehydro on the English Wikipedia
  3 
  4 All methods were developed independently and any resemblance to other string buffer libraries would be coincidental.
  5 Furthermore, many methods will not work when compiled by standard Lua libraries as they depend on behaviors unique to
  6 the MediaMiki Scribunto mod, which, for example, has a getmetatable() method that always returns nil on non-tables.
  7 https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual
  8 
  9 Source code comments may be thin at some points because they are intended to be supplemented by the documentation page:
 10 https://en.wikipedia.org/wiki/Module:Buffer/doc
 11 
 12 Licensed under Creative Commons Attribution-ShareAlike 3.0 Unported License
 13 https://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License
 14 
 15 https://en.wikipedia.org/wiki/Module:Buffer
 16 https://en.wikipedia.org/wiki/User:Codehydro
 17 =============================--]]
 18 local function Valid(v)--type validation
 19 	if v and v~=true then--reject nil/boolean; faster than 2 type() comparisons
 20 		local str = tostring(v)--functions not filtered since unlikely passed by accident (Scribunto does not have userdata/thread types)
 21 		if str~=v and str=='table' then return rawget(v, 1) and table.concat(v) end--tostring(string-type) returns same ref; same refs compare faster than type()
 22 		if str~='' then return str end--numbers are coerced to string per table.concat op; appending in string form saves ops on repeat concat
 23 	end
 24 end
 25 local noOp, MBpairs = function()end do local iMap, vMap, oMap, pIter, pOther, pFast, Next--Map
 26 	local function init()--init = noOp after first run
 27 		function Next(t) return next, t end--slightly faster to do this than to use select()
 28 		function pIter(t, k) k = (iMap[t] or MBpairs(t, true) and iMap[t])[not k and 1 or vMap[t][k]] return k, t[k] end--don't use rawget; accepting unmapped tables does not measurably affect performance.
 29 		function pOther(t, k) k = (oMap[t] or MBpairs(t, true) and oMap[t])[nil==k and 1 or vMap[t][k]] return k, t[k] end--comparison to nil because false is a valid key
 30 		function pFast(t, k) k = not k and 1 or k < (vMap[t] or #t) and k + 1 or nil return k, t[k] end--mapless iterator; almost as fast as native ipairs; slight performance penalty when length not cached
 31 							   --k and k < (vMap[t] or #t) and k + 1 or not k and 1 or nil return k, t[k] end--mapless iterator; almost as fast as native ipairs; slight performance penalty when length not cached
 32 		local mk = {__mode = 'k'}--use mode 'k'; found that mode 'kv' sometimes garbage collects maps mid-loop (may not error because iterators auto re-map, but that's expensive)
 33 		init, iMap, vMap, oMap = noOp, setmetatable({}, mk), setmetatable({}, mk), setmetatable({}, mk)--iMap is numeric keys, oMap is non-numeric keys, and vMap points to next key
 34 	end
 35 	function MBpairs(t, ...)--pairs always iterates in order
 36 		local iter, ex = ...
 37 		iter = iter==init()--nil
 38 		if iter and not oMap[t] and ex==nil and rawget(t, 1)~=nil and next(t, #t)==nil then--while possible to miss keys, more thorough check would negate the benefit of pFast
 39 			vMap[t] = #t return pFast, t, nil
 40 		elseif ... or not vMap[t] or select('#', ...)~=1 then
 41 			local ti, tn, to, n = {}, {}, {}, #t--reduces table lookups
 42 			iMap[t], vMap[t], oMap[t] = ti, tn, to
 43 			for k = 1, n do ti[k], tn[k] = k, k + 1 end--stage one avoids number type checking op in stage two for most numeric keys
 44 			for k in (ex or Next)(t) do
 45 				if not tn[k] then table.insert(tonumber(k)~=k and to or ti, k) end
 46 			end
 47 			if #ti~=n then
 48 				table.sort(ti)
 49 				for k = 1, #ti do tn[ti[k]] = k + 1 end--somewhat wasteful, but trying to avoid overwriting can be even more expensive
 50 			end
 51 			for k = 1, #to do tn[to[k]] = k + 1 end
 52 		end
 53 		return iter and pIter or oMap[t] and pOther or noOp, t--noOp for mapless
 54 	end
 55 end
 56 local parent, rawkey, spec do--new scope for variables not reused outside (reduces number of var names that need to checked outside of scope)
 57 	local mkv = {__mode='kv', __call=function(t,k,v)t[k]=v return k end}--shared meta for Buffer parent property, raw mode, and specialized functions
 58 	parent, rawkey, spec = setmetatable({}, mkv), setmetatable({}, mkv), setmetatable({}, mkv)--shared meta less memory
 59 end
 60 
 61 local MB, MBi, MBmix, buffHTML, gfuncs, noCache, Element do--minimize number of locals per scope to reduce time spent sifting through irrelevant variable names
 62 	local _stream do local stream--keep stream near top of scope
 63 		local function init(f)--init = noOp after first run
 64 			local function each(self, ...)
 65 				for k = 1, select('#', ...) do
 66 					k = Valid(select(k, ...))--slightly faster than table.insert(self, (Valid(select(k, ...))))
 67 					if k then table.insert(self, k) end
 68 				end
 69 				return self
 70 			end
 71 			init, stream, _stream = noOp, {
 72 				__call = function(t, v) v = v and Valid(v) return v and table.insert(t, v) or t end,--last_concat cleared before entering stream mode
 73 				__index = function(t, i) return i=='each' and each or MB.__index(t, i) and setmetatable(t, MB)[i] end,--no table look up minimizes resources to retrieve the only stream function
 74 				__tostring = function(t) return setmetatable(t, MB)() end
 75 			} for k, v in next, MB do stream[k] = stream[k] or v end
 76 			setmetatable(stream, getmetatable(MB))
 77 		end
 78 		function _stream(self, ...) self.last_concat = init() return setmetatable(self, stream):each(...) end
 79 	end
 80 	local function isMBfunc(Buffer, s, ...)--helper for :getParent()-like methods (including getBuffer which does not return a parent)
 81 		return s and (select('#', ...)==0 and--eventually should figure out to make this work for :getHTML which is very similar
 82 				(not rawkey[s] and tostring(s):match'^_.*' and MB.__index(Buffer, s) and MB.__index(Buffer, s)(Buffer) or MBmix(Buffer, s))--unprefixed function names append as a string
 83 				or assert(MB.__index(Buffer, s), ('" %s " does not match any available Module:Buffer function'):format(s))(Buffer, ...)--getParent is a one-way trip so one-time assert not expensive
 84 			) or Buffer
 85 	end
 86 	local function MBselect(n, ...)--helper for :_out and :_str
 87 		local n, seps = n - 1, {select(2, ...)}
 88 		if type(seps[n])=='table' then 
 89 			if buffHTML and rawget(seps[n], buffHTML) then return ... end
 90 			setmetatable(seps, {__index = setmetatable(seps[n], {__index = function(t) return rawget(t, 1) end})})[n] = nil
 91 		end
 92 		return ..., seps
 93 	end
 94 	local _inHTML do local lastBuffer, lastHTML
 95 		local function init(...)--init replaced and new version called on return
 96 			local create, mwFunc = mw.html.create do
 97 				local mwHTMLmeta = getmetatable(create())
 98 				buffHTML, mwFunc, _inHTML = setmetatable(mw.clone(mwHTMLmeta), getmetatable(MB)), mwHTMLmeta.__index--buffHTML declared near top of module; remove _inHTML from outer scope
 99 				function init(nodes, ...)
100 					local name, args, tag = select(... and type(...)=='table' and 1 or 2, nil, ...)
101 					tag = create(Valid(name), args)
102 					if nodes then table.insert(nodes, tag.parent and tag or rawset(tag, 'parent', parent[nodes])) end
103 					if args then
104 						local a, b = args.selfClosing, args.parent
105 						args.selfClosing, args.parent = nil
106 						if next(args) then Element._add(parent(tag.nodes, tag), args) end
107 						args.selfClosing, args.parent = a, b--in case args is reused
108 					end
109 					return tag
110 				end
111 				for k, v in next, {[mw] = mwHTMLmeta,
112 					__call = function(h, v) return MBmix(spec[h.nodes] and h.nodes or spec(setmetatable(parent(h.nodes, h), MB), Element), v) end,
113 					__concat = false,--false means take from MB
114 					__eq = false
115 				} do buffHTML[k] = v or MB[k] end
116 			end
117 			local nonSelf, BHi = {tag=true,done=true,allDone=true}, buffHTML.__index do local g
118 				g = {__index = function(t, i)
119 					if gfuncs and gfuncs[i] then g.__index, gfuncs = gfuncs return g.__index[i] end
120 				end}
121 				setmetatable(nonSelf, g)
122 				setmetatable(BHi, g)
123 			end
124 			for k in next, nonSelf do--any HTML objects returned by these funcs will be granted Module:Buffer enhancements
125 				local func = mwFunc[k]
126 				BHi[k] = function(t, ...) local HTML = func(t, ...) return parent[HTML] and HTML or setmetatable(parent(HTML, t), buffHTML) end
127 			end
128 			do local function joinNode(HTML, sep)
129 					local nodes, join = HTML.nodes
130 					if noCache and rawkey[sep] or Valid(sep) then join, HTML.nodes = tostring(rawset(HTML, 'nodes', {MB.__call(nodes, sep)})), nodes end
131 					return join or tostring(HTML)
132 				end
133 				for k, v in next, {
134 					getParent = function(HTML, ...) lastHTML = HTML return MBi.getParent(HTML:allDone(), ...) end,--return to Buffer that created the HTML tree
135 					getBuffer = function(HTML, ...) lastHTML = HTML return isMBfunc(lastBuffer, ...) end,--return to last used
136 					killParent = function(HTML, ...) MBi.killParent(HTML:allDone(), ...) return HTML end,
137 					_out = function(HTML, ...)
138 						if ...==0 then MBi._out(HTML.nodes, ...) return HTML end
139 						lastHTML, HTML = HTML, HTML:allDone()
140 						local n, ops, seps = select('#', ...)
141 						if n > 1 then
142 							local ops, seps = MBselect(n, ...)
143 							return parent[HTML]:_in(joinNode(HTML, rawget(seps, 0))):_out(ops, rawset(seps, buffHTML, true))
144 						end
145 						return parent[HTML]:_(joinNode(HTML, ...))
146 					end,
147 					_str = function(HTML, ...)--does not set lastHTML
148 						if ...==0 then return joinNode(HTML, select(2, ...)) end--passing 0 strings without calling allDone()
149 						local HTML, n = HTML:allDone(), select('#', ...)
150 						if n > 1 then
151 							local ops, seps = MBselect(n, ...)
152 							return parent[HTML]:_in(joinNode(HTML, rawget(seps, 1))):_str(ops, rawset(seps, buffHTML, true))
153 						end
154 						return joinNode(HTML, ...)
155 					end,
156 					_parent = function(HTML, ...) table.insert(HTML.nodes, parent[HTML:allDone()]:_str(...)) return HTML end
157 				} do BHi[k] = v end
158 			end
159 			do local htmlArg, skip, outFuncs = {parent=true,selfClosing=true,tagName=true}, {}
160 				do local out local function func(nodes, ...) return out(parent[nodes], ...) end
161 					outFuncs = setmetatable({
162 						tag = function(nodes, ...) return parent(setmetatable(init(nodes, ...), buffHTML), parent[nodes]) end,
163 						done = function(b, ops)
164 							b = parent[b] 
165 							while b.parent and ops~=0 do b, ops = b.parent, ops and ops - 1 or 0 end
166 							return b
167 						end
168 					}, {__index = function(nodes, i)
169 						if rawget(BHi, i) then out = BHi[i] return func end--rawget to exclude globals
170 					end})
171 				end
172 				Element = {
173 					_add = function(nodes, t)
174 						for k, v in MBpairs(t), t, skip[t] do (v~=true and MBmix or noOp)(nodes, v) end
175 						local HTML = parent[nodes] for k, v in MBpairs(t, false) do
176 							if htmlArg[k] then HTML[k] = v
177 							elseif v and v~=true then
178 								if nonSelf[k] then
179 									if k=='tag' then
180 										if type(v)=='table' then
181 											skip[v], k = 1, rawset(create(Valid(v[1])), 'parent', HTML)
182 											Element._add(spec(parent(k.nodes, k, table.insert(nodes, k)), Element), v)
183 											if k.selfClosing then k.nodes = nil else spec[k.nodes], parent[k.nodes] = nil end--free memory/reduce clutter; parent ref will auto-unset when k.nodes is nil
184 											if not k.tagName then k.styles, k.attributes = nil end
185 										else table.insert(nodes, create(v)) end
186 									elseif mwFunc[k] then
187 										if k=='done' and tonumber(v)~=v and v[1] and tonumber(v[1])==v[1] then skip[v] = 1 end
188 										MBmix(outFuncs[k](nodes, skip[v] and v[1]).nodes, v)
189 									elseif v[1] or v[2] then
190 										k = MBi[k](nodes, unpack(v, 1, rawset(skip, v, k=='_B' and 1 or 2)[v]))
191 										Element._add(getmetatable(k) and rawget(k, 'nodes') or k, v)--if k is not a table, then v should not contain any extra keys or this may error.
192 									else MBi[k](nodes, v) end--k probably == '_G' or '_R'
193 								elseif mwFunc[k] then
194 									if type(v)~='table' or rawget(v, 'nodes') then mwFunc[k](HTML, v)
195 									else
196 										local css = k=='css'
197 										for x, y in MBpairs(v, true) do (y and y~=true and mwFunc[k] or noOp)(HTML, css and x:gsub('_', '-') or x, y) end--iterate non-numbers first
198 										for _, y in MBpairs(v, nil) do (y and y~=true and mwFunc[k] or noOp)(HTML, y) end--don't bother with gsub since text must be quoted anyhow
199 									end
200 								elseif rawget(Element, k) or rawget(MBi, k) then
201 									if tonumber(v)==v or v[1]==nil or getmetatable(v) then (Element[k] or MBi[k])(nodes, v)--v is probably string-able object, or a table to be handled by :_all
202 									else (Element[k] or MBi[k])(nodes, unpack(v, 1, table.maxn(v))) end--v is definately a table
203 								else mwFunc.css(HTML, k:gsub('_', '-', 1), tostring(v)) end--oddly enough, :_add clocked its fastest runtime after adding auto-gsub as a feature
204 								skip[v] = nil
205 							end
206 						end
207 						return nodes
208 					end
209 				}
210 				local tempMeta = {mode='v', copy={styles=true,attributes=true}}
211 				function tempMeta.__index(t, i) return tempMeta.copy[i] and rawset(t, i, MBi._cc(false, 0, t.orig[i]))[i] or t.orig[i] end
212 				rawkey[setmetatable(Element, {__index = outFuncs, __concat=function(Element, v) return setmetatable({nodes=spec({}, Element),orig=parent[v]}, tempMeta) end})] = math.huge
213 			end
214 			function MBi:getHTML(...)
215 				lastBuffer = self
216 				if ... then
217 					if select('#', ...)==1 then return not rawkey[s] and tostring(...):match'^_' and BHi[...] and BHi[...](lastHTML) or lastHTML(...)
218 					else return assert(BHi[...], ('" %s " does not match any mw.html or Buffer-mw.html function'):format(tostring(...)))(lastHTML, select(2, ...)) end
219 				end
220 				return lastHTML
221 			end
222 			function MBi:_html(...) return MBi._(self, lastHTML, select(spec[self]==Element and select('#', ...)==0 and 1 or 2, true, ...)) end
223 			return init(...)
224 		end
225 		function _inHTML(self, ...)
226 			local HTML = init(nil, ...)
227 			if HTML.selfClosing and spec[self]==Element then self.last_concat = table.insert(self, HTML) return self end
228 			lastBuffer, lastHTML = self, setmetatable(parent(HTML, self), buffHTML)--set after 'args' table processed by :_add
229 			return HTML
230 		end
231 	end
232 	local _var, unbuild do local prev, rebuild
233 		local function init(...)--init replaced before return
234 			local function pick(b, v) return b and table.insert(b, v) or v end
235 			local function c(a, num) return rawset(a.a or a, 0, a[0] and a[0] + a.c or num and a[1] or a[1]:byte())[0] end
236 			local same, build, alt = {__tostring = function(a, b) return a.a[0] and pick(b, a.a.string and string.char(a.a[0]) or a.a.table and a.a[1][a.a[0]] or a.a[0]) end}, {
237 				__index = {c = 1},
238 				__tostring = function(t) return t:_build() end,
239 				table = function(a, b) local i = next(a[1], a[0]) or a[0]==#a[1] and next(a[1]) return pick(b, rawset(a.a or a, 0, i)[1][i]) end,--change rate (a.c) ignored since users control the table's contents
240 				number = function(a, b) return pick(b, c(a, true)) end,
241 				string = function(a, b) return pick(b, string.char(c(a))) end
242 			}, {__index = function(a, i) return a.a[i] end, __tostring = function(a, b) return (rawget(a, 0) and a[0]==tostring(a[0]) and rawset(a, 0, a[0]:byte()) or a).a._build(a, b) end}
243 			local function shift(t, c)
244 				t[0] = t[0] and t[0] + c or t:_build() and t[0] - t.c + c
245 				if t.table then t[0] = (t[0] - 1) % #t[1] + 1 end
246 			end
247 			function rebuild(...)
248 				local v, c = ...
249 				if v or select('#', ...)==0 then
250 					if v and not c then return prev end
251 					local meta, c = select(v and 1 or 3, alt, c, same, 0)
252 					return setmetatable({a = prev, _build = meta.__tostring, c = c}, meta)
253 				elseif v==nil then--no-op
254 				elseif c then shift(prev, c)--v == false
255 				else prev:_build() end
256 			end
257 			init, noCache = function(v, c) prev = setmetatable({v, c = c, _build = build[type(v)] or v, [type(v)] = true, alt = {}}, build) return prev end, true
258 			return init(...)
259 		end
260 		function unbuild(sep)
261 			for k, v in MBpairs(sep, nil) do
262 				k = getmetatable(v) if k and (k==build or k==alt) then shift(v.a or v, -v.c) end
263 			end
264 		end
265 		function _var(self, ...)
266 			local obj if ... and ...~=true then obj = init(...)
267 			elseif prev then
268 				if ...~=false then obj = rebuild(...)
269 				else rebuild(...) end
270 			end
271 			return obj and MBi._(self, obj, nil, true) or self
272 		end
273 	end
274 	local lib; MBi = setmetatable({stream = _stream,
275 		_inHTML = _inHTML,
276 		_var = _var,
277 		_ = function(self, v, ...)
278 			local at, raw = select(select('#', ...)==1 and ...==true and 1 or 2, nil, ...)
279 			if raw then rawkey[self] = math.huge else v = Valid(v) end
280 			if v or raw then
281 				if at or rawkey[self] then raw = #self end--if length increases by more than one after table.insert, then set rawkey[self] = math.huge; rawkey[self] may be equal to a previous 'at'
282 				at, self.last_concat = at and (tonumber(at)~=at and raw + at or at)
283 				table.insert(self, select(at and 1 or 2, at, v))
284 				if at and at < 0 or raw and #self - raw > 1 then rawkey[self] = math.huge elseif at and #self==raw then rawkey[self] = rawkey[self] and math.max(rawkey[self], at) or at end
285 			end--above line looks bizarre because one table.insert op may make length jump from 0 to 8: local wtf={[2]=2,[4]=4,[8]=8}mw.log(#wtf,table.insert(wtf,1),#wtf)
286 			return self
287 		end,
288 		_nil = function(self, at, ...)
289 			if ...~=true and ...~=false then--faster than type(...) ~= 'boolean'
290 				if not at or at=='0' then
291 					self[#self] = ... if ... then rawkey[self] = math.huge end
292 				else
293 					local n, v = tonumber(at), ...
294 					if n~=at then 
295 						if n then n = #self + at
296 						elseif at~=true and select('#', ...)==0 then v, n = at, #self end
297 					end
298 					if n then 
299 						if v==nil and n > 0 then table.remove(self, n)
300 						else self[math.floor(n)], rawkey[self] = v, math.huge end--floor position for consistency with Table library
301 					end
302 				end
303 				self.last_concat = nil
304 			end
305 			return self
306 		end,
307 		_all = function(self, t, valKey)
308 			for k, v in MBpairs(t) do MBmix(self, v, valKey) end
309 			for k, v in valKey and MBpairs(t, false) or noOp, t do
310 				if tonumber(v) then MBi._(self, k, v)--self not always a buffer
311 				elseif rawget(MBi, k) and v and v~=true then
312 					if v[1]==nil or getmetatable(v) then MBi[k](self, v)
313 					else MBi[k](self, unpack(v, 1, table.maxn(v))) end
314 				end
315 			end
316 			return self
317 		end,
318 		_str = function(t, ...)
319 			local n = select('#', ...)
320 			if n > 1 then
321 				local k, ops, seps, r = 2, MBselect(n, ...)
322 				r = MB(t(seps[1]))
323 				while parent[t] and ops > 1 and r:_(parent[t](seps[k]), 1) do t, k, ops = parent[t], k + 1, ops - 1 end
324 				return table.concat(r, seps[k] or nil)
325 			end
326 			return MB.__call(t, ...)
327 		end,
328 		_in = function (self, ...) return parent(MB(...), self) end,
329 		_out = function(t, ...)
330 			if ...==0 then return parent(t, parent[t], MBi._cc(t, t, MB.__call(t, (select(2, ...))), getmetatable(t))) end--love how :_cc needed nothing new to implement this *self pat on back*
331 			local n = select('#', ...)
332 			if n > 1 then
333 				local k, ops, seps = 1, MBselect(n, ...)
334 				while parent[t] and ops > 0 do t, k, ops = parent[t]:_(t(seps[k])), k + 1, ops - 1 end
335 			elseif parent[t] then return parent[t]:_(t(...)) end
336 			return t
337 		end,
338 		_cc = function(self, clear, copy, meta)
339 			if clear then
340 				if rawequal(clear, copy) then return self, spec[MBi._cc] and setmetatable(spec[MBi._cc], MB)--rawequal to avoid re-string via __eq in case both are different Buffer objects
341 				elseif copy==true then copy = self end
342 				if clear~=0 then
343 					assert(type(clear)=='table', debug.traceback('Buffer:_cc can only "clear" tables. Did you forget to call with a colon?', 2))--errors can be hard to trace without this
344 					for k in self and next or noOp, clear do rawset(clear, k, nil) end
345 				else return MBi._cc(false, {unpack(copy)}, copy) end--copy length w/o empty strings; recursion to avoid self = false causing garbage collection (non-weak child may exist)
346 				if self==false or copy and type(copy)=='table' then--self==false means copy is a table (saves a type op for recursive calls)
347 					meta = meta or getmetatable(copy)
348 					if self and #copy > 1 then--preserves length with empty strings; developed from studying http://www.lua.org/source/5.1/ltable.c.html		
349 						local n, null, i, e = #copy, {}, math.ldexp(2, select(2, math.frexp(#copy)) - 2)
350 						e, spec[MBi._cc], parent[null] = i - 1, null, clear
351 						for k = 1, e do table.insert(clear, false) end
352 						while i<=n do table.insert(clear, i, '') i, null[i] = i + math.ldexp(2, select(2, math.frexp(n - i)) - 2), '' end
353 						for k = 1, e do rawset(clear, k, nil) end
354 					end
355 					for k, v in next, copy do rawset(clear, k, type(v)=='table' and MBi._cc(false, 0, v) or v) end
356 				elseif copy then rawset(clear, 1, (Valid(copy))) end
357 				rawkey[setmetatable(clear, meta)], parent[clear] = rawkey[copy], parent[copy]
358 			end
359 			return self and rawset(self, 'last_concat', nil) or clear
360 		end,
361 		_parent = function(self, ...) return parent[self] and MBi._(self, parent[self]:_str(...)) or self end,
362 		getParent = function(self, ...) return isMBfunc(parent[self] or parent[parent(self, setmetatable({}, MB))], ...) end,
363 		killParent = function(self, ...) return parent[self] and isMBfunc(parent[self], ...) and parent(self) or self end,
364 		_build = function(self, t) table.insert(t, self()) end,--for compatibility with mw.html:node()
365 		last_concat = false--prevent library check
366 	}, {__index = function(t, i)--import string, mw.text, and mw.ustring libraries on an as-needed basis
367 		local func = string[i] or mw.text[i] or mw.ustring[i] or type(i)=='string' and mw.ustring[i:match'^u(.+)'] if func then
368 			lib	= lib or function (s, f, ...)
369 				if parent[s] and next(s)==nil then return s:_((f(tostring(parent[Element and (spec[s]==Element and s:allDone() or spec[parent[s]]==Element and parent[s]) or s]), ...))) end
370 				return f(tostring(s), ...)--not using ternary/logical operators here to allow multiple return values
371 			end
372 			return rawset(t, i, i:match'^u?gsub' and function(self, p, r, ...)return lib(self, func, p, r or '', ...)end--Why are ugsub/gsub special? because empty strings are against my religion!
373 				or function(self, ...)return lib(self, func, ...)end)[i]
374 		end
375 	end})
376 end
377 
378 function MBmix(t, v, ...) return v and ((type(v)~='table' or getmetatable(v)) and MBi._(t, v) or (select('#', ...)==0 and spec[t] and spec[t]._add or MBi._all)(t, v, ...)) or t end--:_all always passes two args
379 
380 local _G, new_G = _G--localize _G for console testing (console _G ~= module _G)
381 return setmetatable({__index = function(t, i) return spec[t] and spec[t][i] or MBi[i] end,
382 	__call = function(t, ...)
383 		local rawsep, sep, i, j, raw = noCache and rawkey[...] and ..., ...
384 		if i or j or rawsep or Valid(sep) then
385 			raw, sep, i, j = rawkey[spec[t]] or rawkey[t], rawsep or Valid(sep), i and (i~=tonumber(i) and i + #t or i), j and (j~=tonumber(j) and j + #t or j)
386 			if rawsep or raw and (raw>=(j or #t) or i < 1) then
387 				raw, i, j = {}, i and math.floor(i), j and math.floor(j)--floor for consistency with table.concat(t, sep, i, j), which ignores decimals
388 				raw.lc, t.last_concat = t.last_concat--temporarily unset last_concat to prevent disqualification from mapless iteration
389 				for k, v in MBpairs(t) do
390 					if raw[1] or not i or k>=i then if j and k > j then break end
391 						if raw.s then raw.s = table.insert(raw, tostring(sep)) end--if sep contains v and v is a Buffer-variable, sep must be strung before v
392 						k = Valid(v) if k then
393 							raw.s = rawsep or sep and raw[1] and table.insert(raw, sep)
394 							table.insert(raw, k)
395 						end
396 					end
397 				end
398 				if rawsep and not raw.s then raw[#raw] = unbuild(sep) end--unbuild rawsep if final index in t was invalid
399 				t.last_concat = raw.lc return table.concat(raw)
400 			end
401 			return table.concat(t, sep, i and math.max(i, 1), j and math.min(j, #t))
402 		end
403 		return MB.__tostring(t)
404 	end,
405 	__tostring = function(t)
406 		if t.last_concat then return t.last_concat end
407 		local r = rawkey[spec[t]] or rawkey[t]
408 		r = table.concat(r and r>=#t and MBi._all({}, t) or t)
409 		return (noCache or rawset(t, 'last_concat', r)) and r
410 	end,
411 	__concat = function(a, b)
412 		if buffHTML then
413 			for k = 1, 2 do local v = select(k, a, b)--faster than for k, v in pairs{a, b} do
414 				if v and spec[v] and spec[v]==Element then
415 					if parent[v].selfClosing then
416 						if rawequal(a, b) then return (not noCache or parent[v].tagName) and v:_str(0):rep(2) or v:_str(0)..v:_str(0) end--rawequal avoids premature tostring of Buffer:_var objects;
417 						b, a = select(k, b, parent[v], a)
418 					else local temp = Element .. v --helper method; returns a mirror of parent[v]
419 						MBmix(MBmix(parent(temp.nodes, temp), a), k==1 and spec[b]==Element and parent[b] or b)
420 						return buffHTML.__tostring(setmetatable(temp, {__index=parent[v], __mode='v'}))--switch from tempMeta to avoid MBi._cc op of styles/attributes
421 					end
422 				end
423 			end
424 		end
425 		return table.concat(MBmix(MBmix({}, a), b))
426 	end,
427 	__pairs = MBpairs,
428 	__ipairs = MBpairs,
429 	__eq = function(a, b) return tostring(a)==tostring(b) end--avoid a==b in this module; use rawequal(a,b) when they may be different Buffers (premature tostring waste ops and is bad for Buffer:_var)
430 }, {__tostring = function()return''end,
431 	__call = function(self, ...) MB = MB or self
432 		if new_G then if ... and _G and ...==_G then new_G = ... end
433 		elseif ... and (...==_G or type(...)=='table' and (...)._G==...) then
434 			local Nil, mG = {}, (...):getmetatable() or (...):setmetatable{}:getmetatable()
435 			new_G, _G, gfuncs = ..., ..., {--gfuncs stored for Buffer:_inHTML; new_G is a is a Module:Buffer local declared just before the final return statement.
436 				_G = function(self, i, ...)
437 					local X, save = rawget(new_G, i), select('#', ...)==0 and self or ...
438 					if i and i~=true and not (X and save and rawequal(X, save)) and rawset(new_G, i, save) and (X~=nil or save==nil and new_G[i]~=nil) then--rawequal in case X is another buffer
439 						local mG = getmetatable(new_G) or {__call=mG.__call}
440 						if mG.__index then pcall(rawset, mG.__index, i, X)
441 						else mG.__index = setmetatable(new_G, mG) and {[i] = X} end
442 					end
443 					return self, ...--avoiding __eq with rawequal(self,save) is overkill since buffers can self-save without being passed as save
444 				end,
445 				_R = function(self, i, v, m)
446 					if i~='new_G' then if i and i~=true then rawset(new_G, i , v) end
447 					elseif not v or v==true or v._G~=_G then new_G = setmetatable(v~=true and v or {}, {__call = mG.__call, __index = v~=true and m~=true and (m or new_G) or nil})
448 					else new_G, (not m and (m~=nil or v==new_G) and Nil or getmetatable(v)).__index = v, m~=true and (m or new_G) or nil end--setting Nil.__index is noOp
449 					return self
450 				end,
451 				_2 = function(self, ...)
452 					if new_G[...]~=nil then return new_G[...] end--higher priority so Buffer:_G('new_G', ...) can prevent an overwrite
453 					if ...=='new_G' then return rawset((select('#', ...)~=1 and MBi._R(new_G, ...) or new_G), '_G', _G) end
454 					return select(select('#', ...)==1 and 1 or 2, self:_G(...))--return only one value; 'return select(2, self:_G(...)) or self' doesn't work for returning nil
455 				end,
456 				_B = function(self, v) return v or v==nil and Nil end
457 			} for k, v in next, gfuncs do MBi[k] = v end 
458 			setmetatable(Nil,{__concat=MB.__concat,__newindex=noOp,__call=noOp,__tostring=noOp,__metatable=MB,__index=setmetatable({_B=MBi._B,_=function()return Nil end,last_concat=''},
459 				{__index=function(t,i)return (MBi[i] or i and not tonumber(i)) and t._ or nil end})})
460 			function mG.__call(G, k, ...) return (k._G or G.type(k)=='table') and (G.select('#', ...)~=1 and G.rawset(k, ...) or G:rawset(..., k) and k) or G:rawset(k, (...)) and ... end
461 		end
462 		local new = setmetatable({}, self)
463 		if ... and (...)==new_G then return select(2, ...) and MBmix(new:_G((select(2, ...))), select(3, ...)) or new end
464 		return ... and MBi._(new, ...) or new
465 	end,
466 	__index = function(t, i)
467 		MB = MB or t return MBi[i] and function(...) return MBi[i](setmetatable({}, t), select(...==t and 2 or 1,...)) end
468 	end
469 })