モジュール:Buffer

--=========================**
This Module was written by Alexander Zhikun He, also known as, User:Codehydro on the English Wikipedia
All methods were developed independently and any resemblance to other string buffer libraries would be coincidental.
Furthermore, many methods will not work when compiled by standard Lua libraries as they depend on behaviors unique to
the MediaMiki Scribunto mod, which, for example, has a getmetatable() method that always returns nil on non-tables.
https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual
Source code comments may be thin at some points because they are intended to be supplemented by the documentation page:
https://en.wikipedia.org/wiki/Module:Buffer/doc
Licensed under Creative Commons Attribution-ShareAlike 3.0 Unported License
https://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License
https://en.wikipedia.org/wiki/Module:Buffer
https://en.wikipedia.org/wiki/User:Codehydro

**=========================--
local function Valid(v)--type validation
	if v and v~=true then--reject nil/boolean; faster than 2 type() comparisons
		local str = tostring(v)--functions not filtered since unlikely passed by accident (Scribunto does not have userdata/thread types)
		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()
		if str~= then return str end--numbers are coerced to string per table.concat op; appending in string form saves ops on repeat concat
	end
end
local noOp, MBpairs = function()end do local iMap, vMap, oMap, pIter, pOther, pFast, Next--Map
	local function init()--init = noOp after first run
		function Next(t) return next, t end--slightly faster to do this than to use select()
		function pIter(t, k) k = (iMapt or MBpairs(t, true) and iMapt)not k and 1 or vMaptk return k, tk end--don't use rawget; accepting unmapped tables does not measurably affect performance.
		function pOther(t, k) k = (oMapt or MBpairs(t, true) and oMapt)nil==k and 1 or vMaptk return k, tk end--comparison to nil because false is a valid key
		function pFast(t, k) k = not k and 1 or k  1 then
							local ops, seps = MBselect(n, ...)
							return parentHTML:_in(joinNode(HTML, rawget(seps, 0))):_out(ops, rawset(seps, buffHTML, true))
						end
						return parentHTML:_(joinNode(HTML, ...))
					end,
					_str = function(HTML, ...)--does not set lastHTML
						if ...==0 then return joinNode(HTML, select(2, ...)) end--passing 0 strings without calling allDone()
						local HTML, n = HTML:allDone(), select('#', ...)
						if n > 1 then
							local ops, seps = MBselect(n, ...)
							return parentHTML:_in(joinNode(HTML, rawget(seps, 1))):_str(ops, rawset(seps, buffHTML, true))
						end
						return joinNode(HTML, ...)
					end,
					_parent = function(HTML, ...) table.insert(HTML.nodes, parentHTML:allDone():_str(...)) return HTML end
				} do BHik = v end
			end
			do local htmlArg, skip, outFuncs = {parent=true,selfClosing=true,tagName=true}, {}
				do local out local function func(nodes, ...) return out(parentnodes, ...) end
					outFuncs = setmetatable({
						tag = function(nodes, ...) return parent(setmetatable(init(nodes, ...), buffHTML), parentnodes) end,
						done = function(b, ops)
							b = parentb 
							while b.parent and ops~=0 do b, ops = b.parent, ops and ops - 1 or 0 end
							return b
						end
					}, {__index = function(nodes, i)
						if rawget(BHi, i) then out = BHii return func end--rawget to exclude globals
					end})
				end
				Element = {
					_add = function(nodes, t)
						for k, v in MBpairs(t), t, skipt do (v~=true and MBmix or noOp)(nodes, v) end
						local HTML = parentnodes for k, v in MBpairs(t, false) do
							if htmlArgk then HTMLk = v
							elseif v and v~=true then
								if nonSelfk then
									if k=='tag' then
										if type(v)=='table' then
											skipv, k = 1, rawset(create(Valid(v1)), 'parent', HTML)
											Element._add(spec(parent(k.nodes, k, table.insert(nodes, k)), Element), v)
											if k.selfClosing then k.nodes = nil else speck.nodes, parentk.nodes = nil end--free memory/reduce clutter; parent ref will auto-unset when k.nodes is nil
											if not k.tagName then k.styles, k.attributes = nil end
										else table.insert(nodes, create(v)) end
									elseif mwFunck then
										if k=='done' and tonumber(v)~=v and v1 and tonumber(v1)==v1 then skipv = 1 end
										MBmix(outFuncsk(nodes, skipv and v1).nodes, v)
									elseif v1 or v2 then
										k = MBik(nodes, unpack(v, 1, rawset(skip, v, k=='_B' and 1 or 2)v))
										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.
									else MBik(nodes, v) end--k probably == '_G' or '_R'
								elseif mwFunck then
									if type(v)~='table' or rawget(v, 'nodes') then mwFunck(HTML, v)
									else
										local css = k=='css'
										for x, y in MBpairs(v, true) do (y and y~=true and mwFunck or noOp)(HTML, css and x:gsub('_', '-') or x, y) end--iterate non-numbers first
										for _, y in MBpairs(v, nil) do (y and y~=true and mwFunck or noOp)(HTML, y) end--don't bother with gsub since text must be quoted anyhow
									end
								elseif rawget(Element, k) or rawget(MBi, k) then
									if tonumber(v)==v or v1==nil or getmetatable(v) then (Elementk or MBik)(nodes, v)--v is probably string-able object, or a table to be handled by :_all
									else (Elementk or MBik)(nodes, unpack(v, 1, table.maxn(v))) end--v is definately a table
								else mwFunc.css(HTML, k:gsub('_', '-', 1), tostring(v)) end--oddly enough, :_add clocked its fastest runtime after adding auto-gsub as a feature
								skipv = nil
							end
						end
						return nodes
					end
				}
				local tempMeta = {mode='v', copy={styles=true,attributes=true}}
				function tempMeta.__index(t, i) return tempMeta.copyi and rawset(t, i, MBi._cc(false, 0, t.origi))i or t.origi end
				rawkeysetmetatable(Element, {__index = outFuncs, __concat=function(Element, v) return setmetatable({nodes=spec({}, Element),orig=parentv}, tempMeta) end}) = math.huge
			end
			function MBi:getHTML(...)
				lastBuffer = self
				if ... then
					if select('#', ...)==1 then return not rawkeys and tostring(...):match'^_' and BHi... and BHi...(lastHTML) or lastHTML(...)
					else return assert(BHi..., ('" %s " does not match any mw.html or Buffer-mw.html function'):format(tostring(...)))(lastHTML, select(2, ...)) end
				end
				return lastHTML
			end
			function MBi:_html(...) return MBi._(self, lastHTML, select(specself==Element and select('#', ...)==0 and 1 or 2, true, ...)) end
			return init(...)
		end
		function _inHTML(self, ...)
			local HTML = init(nil, ...)
			if HTML.selfClosing and specself==Element then self.last_concat = table.insert(self, HTML) return self end
			lastBuffer, lastHTML = self, setmetatable(parent(HTML, self), buffHTML)--set after 'args' table processed by :_add
			return HTML
		end
	end
	local _var, unbuild do local prev, rebuild
		local function init(...)--init replaced before return
			local function pick(b, v) return b and table.insert(b, v) or v end
			local function c(a, num) return rawset(a.a or a, 0, a0 and a0 + a.c or num and a1 or a1:byte())0 end
			local same, build, alt = {__tostring = function(a, b) return a.a0 and pick(b, a.a.string and string.char(a.a0) or a.a.table and a.a1a.a0 or a.a0) end}, {
				__index = {c = 1},
				__tostring = function(t) return t:_build() end,
				table = function(a, b) local i = next(a1, a0) or a0==#a1 and next(a1) return pick(b, rawset(a.a or a, 0, i)1i) end,--change rate (a.c) ignored since users control the table's contents
				number = function(a, b) return pick(b, c(a, true)) end,
				string = function(a, b) return pick(b, string.char(c(a))) end
			}, {__index = function(a, i) return a.ai end, __tostring = function(a, b) return (rawget(a, 0) and a0==tostring(a0) and rawset(a, 0, a0:byte()) or a).a._build(a, b) end}
			local function shift(t, c)
				t0 = t0 and t0 + c or t:_build() and t0 - t.c + c
				if t.table then t0 = (t0 - 1) % #t1 + 1 end
			end
			function rebuild(...)
				local v, c = ...
				if v or select('#', ...)==0 then
					if v and not c then return prev end
					local meta, c = select(v and 1 or 3, alt, c, same, 0)
					return setmetatable({a = prev, _build = meta.__tostring, c = c}, meta)
				elseif v==nil then--no-op
				elseif c then shift(prev, c)--v == false
				else prev:_build() end
			end
			init, noCache = function(v, c) prev = setmetatable({v, c = c, _build = buildtype(v) or v, type(v) = true, alt = {}}, build) return prev end, true
			return init(...)
		end
		function unbuild(sep)
			for k, v in MBpairs(sep, nil) do
				k = getmetatable(v) if k and (k==build or k==alt) then shift(v.a or v, -v.c) end
			end
		end
		function _var(self, ...)
			local obj if ... and ...~=true then obj = init(...)
			elseif prev then
				if ...~=false then obj = rebuild(...)
				else rebuild(...) end
			end
			return obj and MBi._(self, obj, nil, true) or self
		end
	end
	local lib; MBi = setmetatable({stream = _stream,
		_inHTML = _inHTML,
		_var = _var,
		_ = function(self, v, ...)
			local at, raw = select(select('#', ...)==1 and ...==true and 1 or 2, nil, ...)
			if raw then rawkeyself = math.huge else v = Valid(v) end
			if v or raw then
				if at or rawkeyself then raw = #self end--if length increases by more than one after table.insert, then set rawkeyself = math.huge; rawkeyself may be equal to a previous 'at'
				at, self.last_concat = at and (tonumber(at)~=at and raw + at or at)
				table.insert(self, select(at and 1 or 2, at, v))
				if at and at  1 then rawkeyself = math.huge elseif at and #self==raw then rawkeyself = rawkeyself and math.max(rawkeyself, at) or at end
			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)
			return self
		end,
		_nil = function(self, at, ...)
			if ...~=true and ...~=false then--faster than type(...) ~= 'boolean'
				if not at or at=='0' then
					self#self = ... if ... then rawkeyself = math.huge end
				else
					local n, v = tonumber(at), ...
					if n~=at then 
						if n then n = #self + at
						elseif at~=true and select('#', ...)==0 then v, n = at, #self end
					end
					if n then 
						if v==nil and n > 0 then table.remove(self, n)
						else selfmath.floor(n), rawkeyself = v, math.huge end--floor position for consistency with Table library
					end
				end
				self.last_concat = nil
			end
			return self
		end,
		_all = function(self, t, valKey)
			for k, v in MBpairs(t) do MBmix(self, v, valKey) end
			for k, v in valKey and MBpairs(t, false) or noOp, t do
				if tonumber(v) then MBi._(self, k, v)--self not always a buffer
				elseif rawget(MBi, k) and v and v~=true then
					if v1==nil or getmetatable(v) then MBik(self, v)
					else MBik(self, unpack(v, 1, table.maxn(v))) end
				end
			end
			return self
		end,
		_str = function(t, ...)
			local n = select('#', ...)
			if n > 1 then
				local k, ops, seps, r = 2, MBselect(n, ...)
				r = MB(t(seps1))
				while parentt and ops > 1 and r:_(parentt(sepsk), 1) do t, k, ops = parentt, k + 1, ops - 1 end
				return table.concat(r, sepsk or nil)
			end
			return MB.__call(t, ...)
		end,
		_in = function (self, ...) return parent(MB(...), self) end,
		_out = function(t, ...)
			if ...==0 then return parent(t, parentt, 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			local n = select('#', ...)
			if n > 1 then
				local k, ops, seps = 1, MBselect(n, ...)
				while parentt and ops > 0 do t, k, ops = parentt:_(t(sepsk)), k + 1, ops - 1 end
			elseif parentt then return parentt:_(t(...)) end
			return t
		end,
		_cc = function(self, clear, copy, meta)
			if clear then
				if rawequal(clear, copy) then return self, specMBi._cc and setmetatable(specMBi._cc, MB)--rawequal to avoid re-string via __eq in case both are different Buffer objects
				elseif copy==true then copy = self end
				if clear~=0 then
					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
					for k in self and next or noOp, clear do rawset(clear, k, nil) end
				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)
				if self==false or copy and type(copy)=='table' then--self==false means copy is a table (saves a type op for recursive calls)
					meta = meta or getmetatable(copy)
					if self and #copy > 1 then--preserves length with empty strings; developed from studying http://www.lua.org/source/5.1/ltable.c.html		
						local n, null, i, e = #copy, {}, math.ldexp(2, select(2, math.frexp(#copy)) - 2)
						e, specMBi._cc, parentnull = i - 1, null, clear
						for k = 1, e do table.insert(clear, false) end
						while i=(j or #t) or i =i then if j and k > j then break end
						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
						k = Valid(v) if k then
							raw.s = rawsep or sep and raw1 and table.insert(raw, sep)
							table.insert(raw, k)
						end
					end
				end
				if rawsep and not raw.s then raw#raw = unbuild(sep) end--unbuild rawsep if final index in t was invalid
				t.last_concat = raw.lc return table.concat(raw)
			end
			return table.concat(t, sep, i and math.max(i, 1), j and math.min(j, #t))
		end
		return MB.__tostring(t)
	end,
	__tostring = function(t)
		if t.last_concat then return t.last_concat end
		local r = rawkeyspect or rawkeyt
		r = table.concat(r and r>=#t and MBi._all({}, t) or t)
		return (noCache or rawset(t, 'last_concat', r)) and r
	end,
	__concat = function(a, b)
		if buffHTML then
			for k = 1, 2 do local v = select(k, a, b)--faster than for k, v in pairs{a, b} do
				if v and specv and specv==Element then
					if parentv.selfClosing then
						if rawequal(a, b) then return (not noCache or parentv.tagName) and v:_str(0):rep(2) or v:_str(0)..v:_str(0) end--rawequal avoids premature tostring of Buffer:_var objects;
						b, a = select(k, b, parentv, a)
					else local temp = Element .. v --helper method; returns a mirror of parentv
						MBmix(MBmix(parent(temp.nodes, temp), a), k==1 and specb==Element and parentb or b)
						return buffHTML.__tostring(setmetatable(temp, {__index=parentv, __mode='v'}))--switch from tempMeta to avoid MBi._cc op of styles/attributes
					end
				end
			end
		end
		return table.concat(MBmix(MBmix({}, a), b))
	end,
	__pairs = MBpairs,
	__ipairs = MBpairs,
	__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)
}, {__tostring = function()returnend,
	__call = function(self, ...) MB = MB or self
		if new_G then if ... and _G and ...==_G then new_G = ... end
		elseif ... and (...==_G or type(...)=='table' and (...)._G==...) then
			local Nil, mG = {}, (...):getmetatable() or (...):setmetatable{}:getmetatable()
			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.
				_G = function(self, i, ...)
					local X, save = rawget(new_G, i), select('#', ...)==0 and self or ...
					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_Gi~=nil) then--rawequal in case X is another buffer
						local mG = getmetatable(new_G) or {__call=mG.__call}
						if mG.__index then pcall(rawset, mG.__index, i, X)
						else mG.__index = setmetatable(new_G, mG) and {i = X} end
					end
					return self, ...--avoiding __eq with rawequal(self,save) is overkill since buffers can self-save without being passed as save
				end,
				_R = function(self, i, v, m)
					if i~='new_G' then if i and i~=true then rawset(new_G, i , v) end
					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})
					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
					return self
				end,
				_2 = function(self, ...)
					if new_G...~=nil then return new_G... end--higher priority so Buffer:_G('new_G', ...) can prevent an overwrite
					if ...=='new_G' then return rawset((select('#', ...)~=1 and MBi._R(new_G, ...) or new_G), '_G', _G) end
					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
				end,
				_B = function(self, v) return v or v==nil and Nil end
			} for k, v in next, gfuncs do MBik = v end 
			setmetatable(Nil,{__concat=MB.__concat,__newindex=noOp,__call=noOp,__tostring=noOp,__metatable=MB,__index=setmetatable({_B=MBi._B,_=function()return Nil end,last_concat=},
				{__index=function(t,i)return (MBii or i and not tonumber(i)) and t._ or nil end})})
			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
		end
		local new = setmetatable({}, self)
		if ... and (...)==new_G then return select(2, ...) and MBmix(new:_G((select(2, ...))), select(3, ...)) or new end
		return ... and MBi._(new, ...) or new
	end,
	__index = function(t, i)
		MB = MB or t return MBii and function(...) return MBii(setmetatable({}, t), select(...==t and 2 or 1,...)) end
	end
})