🌟現在、鉄壁 鉄壁ヘッドショットには対応済みです。
鉄壁HSは通常HSと同じダメージになります。LMG及びDMR、チャージライフル、ハンマーポイント弾を除き、すべてのダメージ値が一致していることを確認しています。

モジュール:Utility/TableUtil

提供:Apex Data
2021年9月2日 (木) 11:35時点におけるMntone (トーク | 投稿記録)による版 ([LineCell] 参照するオブジェクトのミスを修正)
(差分) ← 古い版 | 最新版 (差分) | 新しい版 → (差分)
ナビゲーションに移動 検索に移動

このモジュールについての説明文ページを モジュール:Utility/TableUtil/doc に作成できます

-- Table Utility for MediaWiki
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- Copyright (C) 2021 mntone. All rights reserved.
-- This script is released under the MIT License.
-- ====================
-- namespace: TableUtil
-- ====================
local tableutil = {}

-- =====
-- Config
-- =====
local aw = require('Module:Utility/Library')
local scale = require('Module:Utility/Scale')
local function __passthrough(num)
	return num
end

-- ==========
-- type: Cell
-- ==========
local __TableUtil__CellType__Cell = 'cell'
local __TableUtil__Cell = {
	__typename = __TableUtil__CellType__Cell,
}

-- [Constructor]
function __TableUtil__Cell.__new(typename, name, opts)
	local obj = {
		typename   = typename,
		name       = name,
	}
	if opts then
		if type(opts.attributes) == 'table' then
			obj.attributes = opts.attributes
		end
		
		if type(opts.cellClass) == 'string' then
			obj.cellClass = opts.cellClass
		end
		
		if type(opts.footerClass) == 'string' then
			obj.footerClass = opts.footerClass
		end
		
		if type(opts.headerClass) == 'string' then
			obj.headerClass = opts.headerClass
		end
		
		if type(opts.headerColspan) == 'number' then
			obj.headerColspan = opts.headerColspan
		else
			obj.headerColspan = 1
		end
		
		if type(opts.headerStyle) == 'table' then
			obj.headerStyle = opts.headerStyle
		end
	else
		obj.headerColspan = 1
	end
	return setmetatable(obj, { __index = __TableUtil__Cell })
end
function __TableUtil__Cell.new(name, opts)
	return __TableUtil__Cell.__new(__TableUtil__CellType__Cell, name, opts)
end

-- [Function] Reset context
function __TableUtil__Cell:resetContext()
end

-- [Function] Render header
function __TableUtil__Cell:renderHeader(headerRow)
	if self.headerColspan <= 0 then
		return nil
	end
	
	local headerCell = headerRow:tag('th'):wikitext(self.name)
	if self.headerClass then
		headerCell:addClass(self.headerClass)
	end
	if self.headerColspan > 1 then
		headerCell:attr('colspan', self.headerColspan)
	end
	if self.headerStyle then
		headerCell:css(self.headerStyle)
	end
	return headerCell
end

-- [Function] Render footer
function __TableUtil__Cell:renderFooter(footerRow)
	if self.headerColspan <= 0 then
		return nil
	end
	
	local footerCell = footerRow:tag('td')
	if self.footerClass then
		footerCell:addClass(self.footerClass)
	end
	if self.headerColspan > 1 then
		footerCell:attr('colspan', self.headerColspan)
	end
	return footerCell
end

-- [Function] Render
function __TableUtil__Cell:render(row)
	local cell = row:tag('td')
	if self.attributes then
		cell:attr(self.attributes)
	end
	if self.cellClass then
		cell:addClass(self.cellClass)
	end
	return cell
end

-- [Export]
tableutil.Cell = __TableUtil__Cell

-- =====================
-- type: ValueCell: Cell
-- =====================
local __TableUtil__CellType__ValueCell = 'value'
local __TableUtil__ValueCell = {
	__typename = __TableUtil__CellType__ValueCell,
}
setmetatable(__TableUtil__ValueCell, { __index = __TableUtil__Cell })

-- [Constructor]
function __TableUtil__ValueCell.__new(typename, name, dataIndex, opts)
	local obj = __TableUtil__Cell.__new(typename, name, opts)
	obj.__cell__render = obj.render
	obj.dataIndex = dataIndex
	if type(opts) == 'table' then
		obj.format = opts.format or __passthrough
	else
		obj.format = __passthrough
	end
	return setmetatable(obj, { __index = __TableUtil__ValueCell })
end
function __TableUtil__ValueCell.new(name, dataIndex, opts)
	return __TableUtil__ValueCell.__new(__TableUtil__CellType__ValueCell, name, dataIndex, opts)
end

-- [Function] Render
function __TableUtil__ValueCell:render(row, dataset, i)
	local value = dataset[self.dataIndex]
	local formattedValue = self.format(value)
	local cell = self:__cell__render(row, dataset, i)
		:attr('data-sort-value', value)
		:wikitext(formattedValue)
	return cell
end

-- [Export]
tableutil.ValueCell = __TableUtil__ValueCell

-- =========================
-- type: RankCell: ValueCell
-- =========================
local __TableUtil__CellType__RankCell = 'rank'
local __TableUtil__RankCell = {
	__typename = __TableUtil__CellType__RankCell,
}
setmetatable(__TableUtil__RankCell, { __index = __TableUtil__ValueCell })

-- [Constructor]
function __TableUtil__RankCell.new(name, dataIndex, descending)
	local obj = __TableUtil__ValueCell.__new(__TableUtil__CellType__RankCell, name, dataIndex)
	obj.currentRank  = 0
	obj.currentValue = -1
	obj.descending   = descending
	return setmetatable(obj, { __index = __TableUtil__RankCell })
end

-- [Property] Is descending?
function __TableUtil__RankCell:isDescending()
	return self.descending
end

-- [Function] Reset context
function __TableUtil__RankCell:resetContext(datainfo)
	self.currentRank  = 0
	self.currentValue = -1
end

-- [Function] Render
function __TableUtil__RankCell:render(row, dataset, i)
	local value = dataset[self.dataIndex]
	if self.currentValue ~= value then
		self.currentRank = i
		self.currentValue = value
	end
	local cell = row:tag('th')
		:attr('data-sort-value', self.currentRank)
		:wikitext(self.currentRank)
	return cell
end

-- [Export]
tableutil.RankCell = __TableUtil__RankCell

-- ====================
-- type: LineCell: Cell
-- ====================
local __TableUtil__CellType__LineCell = 'line'
local __TableUtil__LineCell = {
	__typename = __TableUtil__CellType__LineCell,
}
setmetatable(__TableUtil__LineCell, { __index = __TableUtil__ValueCell })

-- [Utils]

local function multipleGradient(colorStop1, colorStop2)
	local gradientFormat = string.format('linear-gradient(to right,transparent %%s%%%%,%s %%s%%%%,%s %%s%%%%,transparent %%s%%%%)', colorStop1, colorStop2)
	return function(minValue, maxValue)
		if minValue == maxValue then
			local value = 0.001 * aw.round(1000 * minValue)
			minValue = value - 0.5
			maxValue = value + 0.5
		else
			minValue = 0.001 * aw.round(1000 * minValue)
			maxValue = 0.001 * aw.round(1000 * maxValue)
		end
		return string.format(gradientFormat, minValue, minValue, maxValue, maxValue)
	end
end

local function singleGradient(colorStop1, colorStop2)
	local gradientFormat
	if colorStop1 == colorStop2 then
		gradientFormat = string.format('linear-gradient(to right,%s %%s%%%%,transparent %%s%%%%)', colorStop1)
	else
		gradientFormat = string.format('linear-gradient(to right,%s,%s %%s%%%%,transparent %%s%%%%)', colorStop1, colorStop2)
	end
	return function(value)
		value = 0.001 * aw.round(1000 * value)
		return string.format(gradientFormat, value, value)
	end
end

-- [Constructor]
function __TableUtil__LineCell.__new(typename, name, dataIndex, opts)
	local obj = __TableUtil__Cell.__new(typename, name, opts)
	obj.__cell__render       = obj.render
	obj.__cell__renderFooter = obj.renderFooter
	obj.dataIndex = dataIndex
	if type(opts) == 'table' then
		obj.itemName           = opts.itemName   or 'Item 1'
		obj.startIndex         = opts.startIndex or dataIndex
		obj.endIndex           = opts.endIndex   or dataIndex
		obj.colorStops         = opts.colorStops or '#A7D7F9'
		obj.hoverColor         = opts.hoverColor or '#5FB6F4'
		obj._hasSubGraph       = opts.subStartIndex and opts.subEndIndex
		if obj._hasSubGraph then
			obj.height         = opts.height or 1.0
			obj.subItemName    = opts.subItemName   or 'Item 2'
			obj.subStartIndex  = opts.subStartIndex or opts.subDataIndex
			obj.subEndIndex    = opts.subEndIndex   or opts.subDataIndex
			obj.subColorStops  = opts.subColorStops or '#F9C9A7'
			obj.subHoverColor  = opts.subHoverColor or '#F49D5F'
			obj.subHeight      = opts.subHeight or 0.4
		else
			obj.height         = opts.height or 1.2
		end
		if opts.gridEnabled then
			obj.gridEnabled    = opts.gridEnabled
		else
			obj.gridEnabled    = true
		end
		obj.hoverEnabled       = opts.hoverEnabled       or false
		obj.hoverFormat        = opts.hoverFormat        or __passthrough
		obj.minimumRate        = opts.minimumRate        or 0.95
		obj.maximumValue       = opts.maximumValue       or math.huge
		obj.maximumRate        = opts.maximumRate        or 1.05
		obj.preferredGridCount = opts.preferredGridCount or 4
		obj.scale              = opts.scale              or scale.LinearScale.new():setClamp(true)
		if obj.scale.typename ~= scale.LinearScale.__typename then
			obj.minimumValue   = opts.minimumValue       or -math.huge
		else
			obj.minimumValue   = opts.minimumValue       or 0
		end
		obj.ticksFormat        = opts.ticksFormat        or __passthrough
	else
		obj.itemName           = 'Item 1'
		obj.startIndex         = dataIndex
		obj.endIndex           = dataIndex
		obj.colorStops         = '#A7D7F9'
		obj.hoverColor         = '#5FB6F4'
		obj._hasSubGraph       = false
		obj.subColorStops      = '#F9C9A7'
		obj.subHoverColor      = '#F49D5F'
		obj.gridEnabled        = true
		obj.height             = 1.2
		obj.hoverEnabled       = false
		obj.hoverFormat        = __passthrough
		obj.minimumRate        = 0.95
		obj.minimumValue       = 0
		obj.maximumValue       = math.huge
		obj.maximumRate        = 1.05
		obj.preferredGridCount = 4
		obj.scale              = scale.LinearScale.new():setClamp(true)
		obj.ticksFormat        = __passthrough
	end
	obj._gridCount = 2
	return setmetatable(obj, { __index = __TableUtil__LineCell })
end
function __TableUtil__LineCell.new(name, dataIndex, opts)
	return __TableUtil__LineCell.__new(__TableUtil__CellType__LineCell, name, dataIndex, opts)
end

-- [Property] Height
function __TableUtil__LineCell:setHeight(value)
	self.height = value
	return self
end

-- [Function] Reset context
function __TableUtil__LineCell:resetContext(datainfo)
	local startValue, stopValue
	if self.minimumValue == -math.huge then
		local minValue = datainfo[self.startIndex].minValue
		if self._hasSubGraph then
			local subMinValue = datainfo[self.subStartIndex].minValue
			if minValue < subMinValue then
				minValue = subMinValue
			end
		end
		startValue = self.minimumRate * minValue
	else
		startValue = self.minimumValue
	end
	if self.maximumValue == math.huge then
		local maxValue = datainfo[self.endIndex].maxValue
		if self._hasSubGraph then
			local subMaxValue = datainfo[self.subEndIndex].maxValue
			if maxValue < subMaxValue then
				maxValue = subMaxValue
			end
		end
		stopValue = self.maximumRate * maxValue
	else
		stopValue = self.maximumValue
	end
	self.scale
		:setDomain({ startValue, stopValue })
		:nice(self.preferredGridCount)
	if self.gridEnabled then
		self._ticks = self.scale:ticks(self.preferredGridCount)
	end
	
	if self.startIndex == self.endIndex then
		if type(self.colorStops) == 'table' then
			self._gradientFormat = singleGradient(self.colorStops[1], self.colorStops[2])
		else
			self._gradientFormat = singleGradient(self.colorStops, self.colorStops)
		end
	else
		if type(self.colorStops) == 'table' then
			self._gradientFormat = multipleGradient(self.colorStops[1], self.colorStops[2])
		else
			self._gradientFormat = multipleGradient(self.colorStops, self.colorStops)
		end
	end
	if self._hasSubGraph then
		if self.subStartIndex == self.subEndIndex then
			if type(self.subColorStops) == 'table' then
				self._subGradientFormat = singleGradient(self.subColorStops[1], self.subColorStops[2])
			else
				self._subGradientFormat = singleGradient(self.subColorStops, self.subColorStops)
			end
		else
			if type(self.subColorStops) == 'table' then
				self._subGradientFormat = multipleGradient(self.subColorStops[1], self.subColorStops[2])
			else
				self._subGradientFormat = multipleGradient(self.subColorStops, self.subColorStops)
			end
		end
		
		if self.gridEnabled then
			if self.scale.typename == scale.LogScale.__typename then
				local ticksCount = #self._ticks + 1
				self._classes = string.format('st-grid st-grid-%d st-grid-log%d st-line st-line-h%d-h%d', ticksCount, ticksCount, 10 * self.height, 10 * self.subHeight)
			else
				self._classes = string.format('st-grid st-grid-%d st-line st-line-h%d-h%d', #self._ticks + 1, 10 * self.height, 10 * self.subHeight)
			end
		else
			self._classes = string.format('st-line st-line-h%d-h%d', 10 * self.height, 10 * self.subHeight)
		end
	elseif self.gridEnabled then
		if self.scale.typename == scale.LogScale.__typename then
			local ticksCount = #self._ticks + 1
			self._classes = string.format('st-grid st-grid-%d st-grid-log%d st-line st-line-h%d', ticksCount, ticksCount, 10 * self.height)
		else
			self._classes = string.format('st-grid st-grid-%d st-line st-line-h%d', #self._ticks + 1, 10 * self.height)
		end
	else
		self._classes = string.format('st-line st-line-h%d', 10 * self.height)
	end
end

-- [Function] Render footer
function __TableUtil__LineCell:renderFooter(footerRow)
	local footerCell = self:__cell__renderFooter(footerRow)
	if self.gridEnabled and footerCell then
		footerCell:addClass('st-content')
		if self.mixinValue then
			footerCell:addClass('st-mobile-content')
		end
		
		local list = footerCell:tag('ul')
			:addClass('st-axis')
			:addClass(string.format('st-axis-%d', #self._ticks + 1))
		local firstTickLabel = self.ticksFormat(self.scale.domain[1])
		list:tag('li'):wikitext(firstTickLabel)
		for _, tick in ipairs(self._ticks) do
			local tickLabel = self.ticksFormat(tick)
			list:tag('li'):wikitext(tickLabel)
		end
		local lastTickLabel = self.ticksFormat(self.scale.domain[2])
		list:tag('li'):wikitext(lastTickLabel)
		
		if self._hasSubGraph then
			local legend = footerCell:tag('ul'):addClass('st-legend')
			local item = legend:tag('li')
			local mark = item:tag('span'):addClass('st-legend-mark'):wikitext('■')
			if type(self.colorStops) == 'table' then
				mark:css('background-image', string.format('linear-gradient(to right,%s,%s)!important', self.colorStops[1], self.colorStops[2]))
			else
				mark:css('background-color', self.colorStops)
			end
			item:wikitext(string.format('&nbsp;%s', self.itemName))
			
			local subItem = legend:tag('li')
			local subMark = subItem:tag('span'):addClass('st-legend-mark'):wikitext('■')
			if type(self.subColorStops) == 'table' then
				subMark:css('background-image', string.format('linear-gradient(to right,%s,%s)!important', self.subColorStops[1], self.subColorStops[2]))
			else
				subMark:css('background-color', self.subColorStops)
			end
			subItem:wikitext(string.format('&nbsp;%s', self.subItemName))
		end
	end
	return footerCell
end

-- [Function] Render
function __TableUtil__LineCell:render(row, dataset, i)
	local value = dataset[self.dataIndex]
	local cell = self:__cell__render(row, dataset, i)
		:addClass(self._classes)
		:attr('data-sort-value', value)
	
	local gradient
	if self.startIndex ~= self.endIndex then
		local minimumValue = dataset[self.startIndex]
		local maximumValue = dataset[self.endIndex]
		local minimumPercentage = self.scale:scale(minimumValue)
		local maximumPercentage = self.scale:scale(maximumValue)
		gradient = self._gradientFormat(minimumPercentage, maximumPercentage)
		
		if self.hoverEnabled then
			cell:tag('span')
				:addClass('st-label')
				:addClass('st-label-item1')
				:addClass('st-label-lower')
				:css('color', self.hoverColor)
				:css('right', string.format('%s%%', aw.roundx(100 - minimumPercentage, 3)))
				:wikitext(self.hoverFormat(minimumValue))
			cell:tag('span')
				:addClass('st-label')
				:addClass('st-label-item1')
				:addClass('st-label-upper')
				:css('color', self.hoverColor)
				:css('left', string.format('%s%%', aw.roundx(maximumPercentage, 3)))
				:wikitext(self.hoverFormat(maximumValue))
		end
	else
		local percentage = self.scale:scale(value)
		gradient = self._gradientFormat(percentage)
		
		if self.hoverEnabled then
			cell:tag('span')
				:addClass('st-label')
				:addClass('st-label-item1')
				:addClass('st-label-upper')
				:css('color', self.hoverColor)
				:css('left', string.format('%s%%', aw.roundx(percentage, 3)))
				:wikitext(self.hoverFormat(value))
		end
	end
	if self._hasSubGraph then
		local subGradient
		if self.subStartIndex ~= self.subEndIndex then
			local minimumValue2 = dataset[self.subStartIndex]
			local maximumValue2 = dataset[self.subEndIndex]
			local minimumPercentage = self.scale:scale(minimumValue2)
			local maximumPercentage = self.scale:scale(maximumValue2)
			subGradient = self._subGradientFormat(minimumPercentage, maximumPercentage)
			
			if self.hoverEnabled then
				cell:tag('span')
					:addClass('st-label')
					:addClass('st-label-item2')
					:addClass('st-label-lower')
					:css('color', self.subHoverColor)
					:css('right', string.format('%s%%', aw.roundx(100 - minimumPercentage, 3)))
					:wikitext(self.hoverFormat(minimumValue2))
				cell:tag('span')
					:addClass('st-label')
					:addClass('st-label-item2')
					:addClass('st-label-upper')
					:css('color', self.subHoverColor)
					:css('left', string.format('%s%%', aw.roundx(maximumPercentage, 3)))
					:wikitext(self.hoverFormat(maximumValue2))
			end
		else
			local value2 = dataset[self.subDataIndex]
			local percentage = self.scale:scale(value)
			subGradient = self._subGradientFormat(percentage)
			
			if self.hoverEnabled then
				cell:tag('span')
					:addClass('st-label')
					:addClass('st-label-item2')
					:addClass('st-label-upper')
					:css('color', self.subHoverColor)
					:css('left', string.format('%s%%', aw.roundx(percentage, 3)))
					:wikitext(self.hoverFormat(value2))
			end
		end
		if not self.mixinValue then
			cell:css('background-image', string.format('%s,%s!important', gradient, subGradient))
		else
			cell:css('background-image', string.format('%s,%s', gradient, subGradient))
		end
	else
		if not self.mixinValue then
			cell:css('background-image', string.format('%s!important', gradient))
		else
			cell:css('background-image', gradient)
		end
	end
	return cell
end

-- [Export]
tableutil.LineCell = __TableUtil__LineCell

-- =================
-- type: Value+XCell
-- =================
local function transformValueCell(superklass, klasstype)
	local valuetype = string.format('value+%s', klasstype)
	local valueKlass = {
		__typename = valuetype,
	}
	setmetatable(valueKlass, { __index = superklass })
	
	function valueKlass.new(name, dataIndex, opts)
		local obj = superklass.__new(valuetype, name, dataIndex, opts)
		obj.__super__render       = obj.render
		obj.__super__renderHeader = obj.renderHeader
		obj.__super__renderFooter = obj.renderFooter
		obj.mixinValue = true
		if type(opts) == 'table' then
			obj.format = opts.format or __passthrough
		else
			obj.format = __passthrough
		end
		return setmetatable(obj, { __index = valueKlass })
	end
	function valueKlass:render(row, dataset, i)
		local value = dataset[self.dataIndex]
		local formattedValue = self.format(value)
		local cell = self:__super__render(row, dataset, i)
		return cell:wikitext(formattedValue)
	end
	return valueKlass
end

-- [Export]
tableutil.LineValueCell = transformValueCell(__TableUtil__LineCell, __TableUtil__CellType__LineCell)

-- ===================
-- func: specifyUICell
-- ===================
function tableutil.specifyUICell(klasstype, renderFunction, propNames)
	local specifictype = string.format('ui+%s', klasstype)
	local specificKlass = {
		__typename = specifictype,
	}
	setmetatable(specificKlass, { __index = __TableUtil__Cell })
	
	function specificKlass.new(name, dataIndex, opts)
		local obj = __TableUtil__Cell.__new(specifictype, name, opts)
		obj.__cell__render = obj.render
		obj.dataIndex      = dataIndex
		if propNames and opts then
			for _, name in ipairs(propNames) do
				obj[name] = opts[name]
			end
		end
		return setmetatable(obj, { __index = specificKlass })
	end
	specificKlass.render = renderFunction
	return specificKlass
end

-- =================
-- func: createTable
-- =================
function tableutil.createTable(dataset, metadata, opts)
	local tbl = mw.html.create('table')
		:addClass('numbertable')
		:addClass('sortable')
		:addClass('st')
	if opts and type(opts.tableClass) == 'string' then
		tbl:addClass(opts.tableClass)
	end
	
	-- Build header
	local headerRow = tbl:tag('tr'):addClass('row-sticky')
	local sortIndex = 0
	local isDesc = false
	for _, m in ipairs(metadata) do
		if m.typename == tableutil.RankCell.__typename then
			sortIndex = m.dataIndex
			isDesc    = m:isDescending()
		end
		
		-- Render header
		m:renderHeader(headerRow)
	end
	
	-- Sort data
	if sortIndex ~= 0 then
		local sortFunction
		if isDesc then
			sortFunction = function(a, b)
				return a[sortIndex] > b[sortIndex]
			end
		else
			sortFunction = function(a, b)
				return a[sortIndex] < b[sortIndex]
			end
		end
		table.sort(dataset, sortFunction)
	end
	
	-- Preprocess data
	local datainfo = {}
	for _, ary in ipairs(dataset) do
		for key, d in pairs(ary) do
			if type(d) == 'number' and d ~= math.huge then
				if not datainfo[key] then
					datainfo[key] = {
						minValue = d,
						maxValue = d,
					}
				else
					if datainfo[key].minValue > d then
						datainfo[key].minValue = d
					end
					if datainfo[key].maxValue < d then
						datainfo[key].maxValue = d
					end
				end
			end
		end
	end
	
	-- Reset context
	for _, m in ipairs(metadata) do
		-- Reset context for building process
		m:resetContext(datainfo)
	end
	
	-- Build cell
	for i, d in ipairs(dataset) do
		local row = tbl:tag('tr')
		for k, m in ipairs(metadata) do
			m:render(row, d, i)
		end
	end
	
	-- Build footer
	local footerRow = tbl:tag('tr'):addClass('sortbottom')
	for _, m in ipairs(metadata) do
		-- Render footer
		m:renderFooter(footerRow)
	end

	return tbl
end

-- =================
-- test: Simple test
-- =================
function tableutil.__test()
	local metadata = {
		tableutil.RankCell.new('', 1),
		tableutil.ValueCell.new('Value', 1),
		tableutil.LineCell.new('Line', 1, { cellClass = 'st-graph' }),
		tableutil.LineCell.new('LineGradient', 1, { cellClass = 'st-graph', colorStops = { '#F8B3BC', '#F26D7D' }, height = 1.4 }),
		tableutil.LineValueCell.new('LineValue', 2, { cellClass = 'st-graph st-textshadow' }),
		tableutil.LineCell.new('Line (Range)', 1, { cellClass = 'st-graph', startIndex = 1, endIndex = 2, scale = scale.LogScale.new() }),
		tableutil.LineCell.new('Line (Range & Sub)', 1, { cellClass = 'st-graph', startIndex = 1, endIndex = 2, subStartIndex = 1, subEndIndex = 3, hoverEnabled = true }),
	}
	local node = tableutil.createTable({{ 45, 110, 140 }, { 40, 60, 90 }, { 40, 120, 190 }, { 50, 90, 110 }}, metadata)
	return tostring(node)
end

return tableutil