打开/关闭菜单
打开/关闭外观设置菜单
打开/关闭个人菜单
未登录
登录后可编辑和发表评论。

Module:SVG模拟

来自Vocawiki

此模块的文档可以在Module:SVG模拟/doc创建

local function format_styles(styles, additional_style)
	local t = {}
	for k, v in pairs(styles) do
		t[#t+1] = k..': '..v..';'
	end
	t[#t+1] = additional_style
	return table.concat(t, ' ')
end

local function ensure_number(str, err_msg)
	return assert(tonumber(str, 10), err_msg)
end

local components = {}

function components.path(props)
	local path = props.d or props[1]
	if not path then
		error('缺少路径参数(`d`或`1`)')
	end
	path = mw.text.trim(path)
	if path == '' then
		error('路径参数(`d`或`1`)为空')
	end

	local fill = props.fill or '#000'
	local fill_rule = props['fill-rule']

	local styles = {
		position = 'absolute',
		inset = '0',
		['clip-path'] = fill_rule and ("path("..fill_rule..", '"..path.."')") or ("path('"..path.."')"),
		background = fill,
	}

	return '<span style="'..format_styles(styles, props.style)..'"></span>'
end

do
	local sep = '%s+'  -- 为了简洁性和一致性,有意不支持逗号分隔
	local num = '(%d+[%.%d]*)'
	local viewbox_pattern = '^%s*'..num..sep..num..sep..num..sep..num..'%s*$'

	local function parse_viewbox(str)
		local viewbox = {}
		local min_x, min_y, w, h = str:match(viewbox_pattern)
		if not min_x then
			error('viewBox属性值('..str..')不合法')
		end
		viewbox.min_x = ensure_number(min_x)
		viewbox.min_y = ensure_number(min_y)
		viewbox.width = ensure_number(w)
		viewbox.height = ensure_number(h)
		return viewbox
	end

	---@param v unknown
	---@return {type: 'number', value: number, string: string} | {type: 'length', value: number, unit: string, string: string} | {type: 'unknown', string: string}
	local function Value(v)
		local typ = type(v)
		if typ == 'number' then
			return {
				type = 'number',
				value = v,
				string = tostring(v),
			}
		elseif typ == 'string' then
			local n = tonumber(v, 10)
			if n then
				return {
					type = 'number',
					value = n,
					string = v,
				}
			end
			local num_str, unit = v:match('^([%+%-]?%d+[%.%d]*)([%%%a]+)$')
			if not num_str then
				return {
					type = 'unknown',
					string = v,
				}
			end
			local value = tonumber(num_str, 10)
			if value and unit == 'px' then
				return {
					type = 'length',
					value = value,
					unit = 'px',
					string = v,
				}
			end
			return {
				type = 'unknown',
				string = v,
			}
		else
			error('无法解析的值:'..v)
		end
	end

	local function get_px(v)
		if v.type == 'number' or v.unit == 'px' then
			return v.value
		end
		return nil
	end

	function components.svg(props)
		local tag = props.tag or 'span'
		local width = Value(assert(props.width, '缺少width参数'))
		local height = Value(assert(props.height, '缺少height参数'))
		local width_px = get_px(width)
		local height_px = get_px(height)

		local viewbox
		local scale  ---@type number | string
		if props.viewBox then
			viewbox = parse_viewbox(props.viewBox)
			if width_px then
				scale = width_px / viewbox.width
			else
				scale = ('tan(atan2(%s, %spx))'):format(width.string, viewbox.width)
			end
		elseif width_px and height_px then
			viewbox = {
				min_x = 0, min_y = 0, width = width.value, height = height.value,
			}
			scale = 1
		else
			error('缺少viewBox参数,也无法从width、height属性推断')
		end
		local children = assert(props[1], '缺少参数1(子节点)')

		if scale == 1 then
			local styles = {
				display = 'inline-block',
				width = ('%spx'):format(viewbox.width),
				height = ('%spx'):format(viewbox.height),
				position = 'relative',
				overflow = 'hidden',
			}
			return table.concat({
				'<', tag, ' style="', format_styles(styles, props.style), '">',
				children,
				'</', tag, '>',
			})
		end

		local wrapper_styles = {
			display = 'inline-block',
			width = width.type == 'number' and (width.string..'px') or width.string,
			height = height.type == 'number' and (height.string..'px') or height.string,
			position = 'relative',
			overflow = 'hidden',
		}
		local viewbox_styles = {
			width = ('%spx'):format(viewbox.width),
			height = ('%spx'):format(viewbox.height),
			position = 'absolute',
			transform = ('scale(%s)'):format(scale),
			['transform-origin'] = '0 0',
		}

		return table.concat({
			'<', tag, ' style="', format_styles(wrapper_styles, props.style), '">',
			'<span style="', format_styles(viewbox_styles), '">',
			children,
			'</span>',
			'</', tag, '>',
		})
	end
end

local module = {}
function module.svg_from_parent_enable_data_url(frame)
	local parent = frame:getParent()
	local args = parent.args
	if args.svg then
		local svg_data_url = require('Module:SVG Data URL')._main
		return svg_data_url(args)
	end
	return components.svg(args)
end

return setmetatable(module, {
	-- 访问 "<组件名>" 时,缓存并返回 f: (props) -> 组件;
	-- 访问 "<组件名>_from_frame" 时,缓存并返回 f: (frame) -> 组件,将`frame.args`作为组件的`props`;
	-- 访问 "<组件名>_from_parent" 时,缓存并返回 f: (frame) -> 组件,将`frame:getParent().args`作为组件的`props`;
	__index = function (self, k)
		local component_func = components[k]
		if component_func then
			self[k] = component_func
			return component_func
		end

		if type(k) ~= 'string' then return nil end

		local component_name, suffix = k:match('^(..-)_(.+)$')
		component_func = components[component_name]
		if not component_func then return nil end

		local ret
		if suffix == 'from_frame' then
			ret = function (frame)
				return component_func(frame.args)
			end
		elseif suffix == 'from_parent' then
			ret = function (frame)
				return component_func(frame:getParent().args)
			end
		else
			return nil
		end

		self[k] = ret
		return ret
	end,
})