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,
})