Skip to content

Instantly share code, notes, and snippets.

@cambiata
Last active October 9, 2023 17:09
Show Gist options
  • Save cambiata/aa3e151f49b456bec85665872e5df64e to your computer and use it in GitHub Desktop.
Save cambiata/aa3e151f49b456bec85665872e5df64e to your computer and use it in GitHub Desktop.
SimpleSvg - Proof of concept Fuse for displaying Svg
-- SimpleSvg.Fuse
-- Proof of concept Fuse for displaying Svg content using the fuse drawing api
-- This file should be put in the Fustion/Fuses folder
-- (On windows, something like C:\ProgramData\Blackmagic Design\Fusion\Fuses)
require('SimpleSvgGraphics')
FuRegisterClass("SimpleSvg", CT_SourceTool, {
REGS_Name = "SimpleSvg",
REGS_Category = "WorkInProgress",
REGS_OpIconString = "Min",
REGS_OpDescription = "Minimal Fuse example.",
REG_Source_GlobalCtrls = true,
REG_Source_SizeCtrls = true,
})
function Create()
-- outputs
OutImage = self:AddOutput("Output", "Output", {
LINKID_DataType = "Image",
LINK_Main = 1,
})
InXmlText = self:AddInput("XmlText", "XmlText", {
LINKID_DataType = "Text",
INPID_InputControl = "TextEditControl",
TEC_Lines = 30, -- How many lines high is the Input.
})
InScale = self:AddInput("Scale", "Scale", {
LINKID_DataType = "Number",
INPID_InputControl = "SliderControl",
INP_MinAllowed = 0.1,
INP_MaxScale = 3,
INP_Default = 1,
})
InInvert = self:AddInput("Invert", "Invert", {
LINKID_DataType = "Number",
INPID_InputControl = "CheckboxControl",
INP_Integer = true,
INP_Default = 1,
})
end
function NotifyChanged(inp, param, time)
end
function Process(req)
-- Standard set up for Creator tools
local realwidth = Width;
local realheight = Height;
-- We'll handle proxy ourselves
Width = Width / Scale
Height = Height / Scale
Scale = 1
-- Attributes for new images
local imgattrs = {
IMG_Document = self.Comp,
{ IMG_Channel = "Red", },
{ IMG_Channel = "Green", },
{ IMG_Channel = "Blue", },
{ IMG_Channel = "Alpha", },
IMG_Width = Width,
IMG_Height = Height,
IMG_XScale = XAspect,
IMG_YScale = YAspect,
IMAT_OriginalWidth = realwidth,
IMAT_OriginalHeight = realheight,
IMG_Quality = not req:IsQuick(),
IMG_MotionBlurQuality = not req:IsNoMotionBlur(),
}
-- Set up image
local img = Image(imgattrs)
local out = img:CopyOf()
local p = Pixel({ R = 0, G = 0, B = 0, A = 0 })
img:Fill(p) -- Clear the image so the next frame doesn't contain the
out:Fill(p)
-- Get parameters from controls
local aspect = calcAspect(img)
local scale = InScale:GetValue(req).Value;
local xmlText = InXmlText:GetValue(req).Value;
local invert = InInvert:GetValue(req).Value;
DrawSimpleSvg(out, xmlText, scale, invert, aspect)
OutImage:Set(req, out)
end
function calcAspect(ref_img)
return (ref_img.Height * ref_img.YScale) / (ref_img.Width * ref_img.XScale)
end
-- SimpleSvgGraphics.lua
-- Library file for of SimpleSvg.fuse - proof of concept Fuse for displaying Svg content using the fuse drawing api
-- This file should be put in the Fusion/Modules/Lua folder
-- (on windows something like C:\ProgramData\Blackmagic Design\Fusion\Modules\Lua)
require("SimpleSvgParse")
function DrawSimpleSvg(out, xml, scale, invert, aspect)
if xml == nil or xml == "" then
print('xml is nil')
return;
end
local scaleX = scale * 0.0005
local scaleY = scaleX
local moveX = 0.0
local moveY = 0.0
if invert == 1 then
scaleY = -scaleY
moveY = moveY + aspect
end
local items = ParseSvgText(xml, scaleX, scaleY, moveX, moveY);
DrawItems(out, items)
end
function GetColor(colorString)
if colorString == 'black' then
return Pixel({ R = 0, G = 0, B = 0, A = 1 })
elseif colorString == 'white' then
return Pixel({ R = 1, G = 1, B = 1, A = 1 })
elseif colorString == 'red' then
return Pixel({ R = 1, G = 0, B = 0, A = 1 })
elseif colorString == 'green' then
return Pixel({ R = 0, G = 1, B = 0, A = 1 })
elseif colorString == 'blue' then
return Pixel({ R = 0, G = 0, B = 1, A = 1 })
elseif colorString == 'yellow' then
return Pixel({ R = 1, G = 1, B = 0, A = 1 })
elseif colorString == 'cyan' then
return Pixel({ R = 0, G = 1, B = 1, A = 1 })
elseif colorString == 'magenta' then
return Pixel({ R = 1, G = 0, B = 1, A = 1 })
end
return Pixel({ R = 1, G = 1, B = 1, A = 1 })
end
function DrawItemStroke(out, shape, stroke, strokeWidth)
assert('stroke' ~= nil, "DrawItemStroke: stroke must not be nil")
assert('strokeWidth' ~= nil, "DrawItemStroke: strokeWidth must not be nil")
local ic = ImageChannel(out, 8)
local cs = ChannelStyle()
shape = shape:OutlineOfShape(strokeWidth, "OLT_Solid")
cs.Color = GetColor(stroke)
ic:ShapeFill(shape)
if self.Status == "OK" then
ic:PutToImage("CM_Merge", cs)
end
end
function DrawItemFill(out, shape, fill)
local ic = ImageChannel(out, 8)
local cs = ChannelStyle()
cs.Color = GetColor(fill)
ic:ShapeFill(shape)
if self.Status == "OK" then
ic:PutToImage("CM_Merge", cs)
end
end
function DrawItems(out, items)
for i, v in pairs(items) do
if v[1] == 'line' then
local shape = Shape()
shape:MoveTo(v[2].x1, v[2].y1)
shape:LineTo(v[2].x2, v[2].y2)
DrawItemStroke(out, shape, v[2].stroke, v[2].stroke_width)
elseif v[1] == 'rect' then
if (v[2].fill ~= nil and v[2].fill ~= 'none' and v[2].fill ~= 'transparent') then
local shape = Shape()
shape:MoveTo(v[2].x, v[2].y)
shape:LineTo(v[2].x + v[2].width, v[2].y)
shape:LineTo(v[2].x + v[2].width, v[2].y + v[2].height)
shape:LineTo(v[2].x, v[2].y + v[2].height)
shape:Close()
DrawItemFill(out, shape, v[2].fill)
end
if (v[2].stroke ~= nil and v[2].stroke ~= 'none') then
local shape = Shape()
shape:MoveTo(v[2].x, v[2].y)
shape:LineTo(v[2].x + v[2].width, v[2].y)
shape:LineTo(v[2].x + v[2].width, v[2].y + v[2].height)
shape:LineTo(v[2].x, v[2].y + v[2].height)
shape:Close()
DrawItemStroke(out, shape, v[2].stroke, v[2].stroke_width)
end
elseif v[1] == 'ellipse' then
local x = v[2].cx
local y = v[2].cy
local rx = v[2].rx
local ry = v[2].ry
if (v[2].fill ~= nil and v[2].fill ~= 'none' and v[2].fill ~= 'transparent') then
local shape = Shape()
shape:MoveTo(x, y - ry)
BezierTo2(shape, { X = x, Y = y - ry }, { X = x - rx, Y = y - ry }, { X = x - rx, Y = y },
{ X = x - rx, Y = y }, 20)
BezierTo2(shape, { X = x - rx, Y = y }, { X = x - rx, Y = y + ry }, { X = x, Y = y + ry },
{ X = x, Y = y + ry }, 20)
BezierTo2(shape, { X = x, Y = y + ry }, { X = x + rx, Y = y + ry }, { X = x + rx, Y = y },
{ X = x + rx, Y = y }, 20)
BezierTo2(shape, { X = x + rx, Y = y }, { X = x + rx, Y = y - ry }, { X = x, Y = y - ry },
{ X = x, Y = y - ry }, 20)
shape:Close()
DrawItemFill(out, shape, v[2].fill)
end
if (v[2].stroke ~= nil and v[2].stroke ~= 'none') then
local shape = Shape()
shape:MoveTo(x, y - ry)
BezierTo2(shape, { X = x, Y = y - ry }, { X = x - rx, Y = y - ry }, { X = x - rx, Y = y },
{ X = x - rx, Y = y }, 20)
BezierTo2(shape, { X = x - rx, Y = y }, { X = x - rx, Y = y + ry }, { X = x, Y = y + ry },
{ X = x, Y = y + ry }, 20)
BezierTo2(shape, { X = x, Y = y + ry }, { X = x + rx, Y = y + ry }, { X = x + rx, Y = y },
{ X = x + rx, Y = y }, 20)
BezierTo2(shape, { X = x + rx, Y = y }, { X = x + rx, Y = y - ry }, { X = x, Y = y - ry },
{ X = x, Y = y - ry }, 20)
DrawItemStroke(out, shape, v[2].stroke, v[2].stroke_width)
end
elseif v[1] == 'path' then
local prevX = nil;
local prevY = nil;
for i, item in pairs(v[2].d) do
if item[1] == 'M' then
prevX = item[2][1]
prevY = item[2][2]
elseif item[1] == 'L' then
prevX = item[2][1]
prevY = item[2][2]
elseif item[1] == 'Q' then
item[2][5] = prevX
item[2][6] = prevY
prevX = item[2][3]
prevY = item[2][4]
elseif item[1] == 'C' then
item[2][7] = item[2][5]
item[2][8] = item[2][6]
item[2][5] = prevX
item[2][6] = prevY
elseif item[1] == 'T' then
item[2][1] = prevX
item[2][2] = prevY
elseif item[1] == 'Z' then
end
end
if (v[2].fill ~= nil and v[2].fill ~= 'none' and v[2].fill ~= 'transparent') then
local shape = Shape()
for i, v in pairs(v[2].d) do
if v[1] == 'M' then
shape:MoveTo(v[2][1], v[2][2])
elseif v[1] == 'L' then
shape:LineTo(v[2][1], v[2][2])
elseif v[1] == 'Q' then
BezierTo2(shape, { X = v[2][5], Y = v[2][6] }, { X = v[2][1], Y = v[2][2] },
{ X = v[2][3], Y = v[2][4] }, { X = v[2][3], Y = v[2][4] }, 20)
elseif v[1] == 'T' then
-- print('FILL T');
shape:LineTo(v[2][1], v[2][2])
elseif v[1] == 'C' then
BezierTo2(shape, { X = v[2][5], Y = v[2][6] }, { X = v[2][1], Y = v[2][2] },
{ X = v[2][3], Y = v[2][4] }, { X = v[2][7], Y = v[2][8] }, 20)
end
end
DrawItemFill(out, shape, v[2].fill)
end
if (v[2].stroke ~= nil and v[2].stroke ~= 'none') then
if v[2].stroke_width == nil then v[2].stroke_width = 1 end
local shape = Shape()
for i, v in pairs(v[2].d) do
if v[1] == 'M' then
shape:MoveTo(v[2][1], v[2][2])
elseif v[1] == 'L' then
shape:LineTo(v[2][1], v[2][2])
elseif v[1] == 'Q' then
BezierTo2(shape, { X = v[2][5], Y = v[2][6] }, { X = v[2][1], Y = v[2][2] },
{ X = v[2][3], Y = v[2][4] }, { X = v[2][3], Y = v[2][4] }, 20)
elseif v[1] == 'T' then
-- print('STROKE T', v[2][1], v[2][2]);
shape:LineTo(v[2][1], v[2][2])
elseif v[1] == 'C' then
BezierTo2(shape, { X = v[2][5], Y = v[2][6] }, { X = v[2][1], Y = v[2][2] },
{ X = v[2][3], Y = v[2][4] }, { X = v[2][7], Y = v[2][8] }, 20)
end
end
DrawItemStroke(out, shape, v[2].stroke, v[2].stroke_width)
end
elseif v[1] == 'group' then
print('group')
-- PrintItems(v[2], level + 1);
end
end
end
-- Shape is a Shape object
-- P1 is the start point. P2 is the outgoing handle. P3 is the incoming handle. P4 is the end point.
-- subdivs is the number of line segments used to create the curve.
-- aspect is necessary to convert Y coordinates for non-square images. Could use convertY instead, but
-- that requires passing the img instead. I prefer to calculate the aspect just once.
function BezierTo2(shape, p1, p2, p3, p4, subdivs)
for i = 0, subdivs do
t = SolvePoint(p1, p2, p3, p4, i / subdivs)
shape:LineTo(t.X, t.Y)
end
return shape
end
-- De Casteljaus equation finds x,y coordinates for a given t
-- p1 - p4 are Point DataType: Tables with indices X and Y
-- The return value of p is a table in the same format.
function SolvePoint(p1, p2, p3, p4, t)
-- print('solve--->', p1.X, p1.Y, p2.X, p2.Y, p3.X, p3.Y, p4.X, p4.Y, t)
local p = {}
p.X = (1 - t) ^ 3 * p1.X + 3 * (1 - t) ^ 2 * t * p2.X + 3 * (1 - t) * t ^ 2 * p3.X + t ^ 3 * p4.X
p.Y = (1 - t) ^ 3 * p1.Y + 3 * (1 - t) ^ 2 * t * p2.Y + 3 * (1 - t) * t ^ 2 * p3.Y + t ^ 3 * p4.Y
return p
end
-- SimpleSvgGraphics.lua
-- Library file for of SimpleSvg.fuse - proof of concept Fuse for displaying Svg content using the fuse drawing api
-- This file should be put in the Fusion/Modules/Lua folder
-- (on windows something like C:\ProgramData\Blackmagic Design\Fusion\Modules\Lua)
local xmlparser = require("xmlparser");
local function isNumeric(value)
if value == tostring(tonumber(value)) then
return true
else
return false
end
end
local function getTransformMatrix(transform)
-- print('transform', transform)
if transform == nil then
return nil
end
local transform = transform:gsub("matrix%(", ""):gsub("%)", "");
local t = {}
for field in transform:gmatch('([^,]+)') do
table.insert(t, field)
end
local r = {}
r.scaleX = t[1];
r.scaleY = t[4];
r.moveX = t[5];
r.moveY = t[6];
return r
end
local function transformRect(rect, trf)
if trf == nil then
return rect
end
if trf.scaleX ~= nil then
if rect.stroke_width then
rect.stroke_width = rect.stroke_width * trf.scaleX
end
rect.width = rect.width * trf.scaleX
rect.x = rect.x * trf.scaleX
end
if trf.scaleY ~= nil then
rect.height = rect.height * trf.scaleY
rect.y = rect.y * trf.scaleY
end
if trf.moveX ~= nil then
rect.x = rect.x + trf.moveX
end
if trf.moveY ~= nil then
rect.y = rect.y + trf.moveY
end
return rect
end
local function transformEllipse(rect, trf)
if trf == nil then
return rect
end
if trf.scaleX ~= nil then
if rect.stroke_width then
rect.stroke_width = rect.stroke_width * trf.scaleX
end
rect.cx = rect.cx * trf.scaleX
rect.rx = rect.rx * trf.scaleX
end
if trf.scaleY ~= nil then
rect.cy = rect.cy * trf.scaleY
rect.ry = rect.ry * trf.scaleY
end
if trf.moveX ~= nil then
rect.cx = rect.cx + trf.moveX
end
if trf.moveY ~= nil then
rect.cy = rect.cy + trf.moveY
end
return rect
end
local function transformLine(line, trf)
if trf == nil then
return line
end
if trf.scaleX ~= nil then
if line.stroke_width then
line.stroke_width = line.stroke_width * trf.scaleX
end
line.x2 = line.x2 * trf.scaleX
line.x1 = line.x1 * trf.scaleX
end
if trf.scaleY ~= nil then
line.y2 = line.y2 * trf.scaleY
line.y1 = line.y1 * trf.scaleY
end
if trf.moveX ~= nil then
line.x1 = line.x1 + trf.moveX
line.x2 = line.x2 + trf.moveX
end
if trf.moveY ~= nil then
line.y1 = line.y1 + trf.moveY
line.y2 = line.y2 + trf.moveY
end
return line
end
local function transformPath(path, trf)
if trf == nil then
return path
end
if path.stroke_width then
path.stroke_width = path.stroke_width * trf.scaleX
end
for i, segment in pairs(path.d) do
local t = segment[1]
for j, v in pairs(segment[2]) do
if j % 2 == 1 then -- y
if trf.scaleX then
segment[2][j] = segment[2][j] * trf.scaleX
end
if trf.moveX then
segment[2][j] = segment[2][j] + trf.moveX
end
else
if trf.scaleY then
segment[2][j] = segment[2][j] * trf.scaleY
end
if trf.moveY then -- x
segment[2][j] = segment[2][j] + trf.moveY
end
end
end
end
return path
end
local function transformGroup(group, trf)
for j, v in pairs(group) do
if v[1] == 'line' then
v[2] = transformLine(v[2], trf)
elseif v[1] == 'rect' then
v[2] = transformRect(v[2], trf)
elseif v[1] == 'ellipse' then
v[2] = transformEllipse(v[2], trf)
elseif v[1] == 'path' then
v[2] = transformPath(v[2], trf)
elseif v[1] == 'group' then
v[2] = transformGroup(v[2], trf)
end
end
return group
end
function ParseRect(item)
local itemTag = item.tag;
local itemAttrs = item.attrs;
local item = {}
item["x"] = tonumber(itemAttrs.x);
item["y"] = tonumber(itemAttrs.y);
item["width"] = tonumber(itemAttrs.width);
item["height"] = tonumber(itemAttrs.height);
item["fill"] = itemAttrs.fill;
item["stroke"] = itemAttrs.stroke;
item["stroke_width"] = itemAttrs["stroke-width"];
local trf = getTransformMatrix(itemAttrs.transform)
item = transformRect(item, trf);
return item;
end
function ParseEllipse(item)
local itemTag = item.tag;
local itemAttrs = item.attrs;
local item = {}
item["cx"] = tonumber(itemAttrs.cx);
item["cy"] = tonumber(itemAttrs.cy);
item["rx"] = tonumber(itemAttrs.rx);
item["ry"] = tonumber(itemAttrs.ry);
item["fill"] = itemAttrs.fill;
item["stroke"] = itemAttrs.stroke;
item["stroke_width"] = itemAttrs["stroke-width"];
local trf = getTransformMatrix(itemAttrs.transform)
item = transformRect(item, trf);
return item;
end
function ParseCircle(item)
local itemTag = item.tag;
local itemAttrs = item.attrs;
local item = {}
item["cx"] = tonumber(itemAttrs.cx);
item["cy"] = tonumber(itemAttrs.cy);
item["rx"] = tonumber(itemAttrs.r);
item["ry"] = tonumber(itemAttrs.r);
item["fill"] = itemAttrs.fill;
item["stroke"] = itemAttrs.stroke;
item["stroke_width"] = itemAttrs["stroke-width"];
local trf = getTransformMatrix(itemAttrs.transform)
item = transformRect(item, trf);
return item;
end
function ParseLine(line)
local lineTag = line.tag;
local lineAttrs = line.attrs;
local line = {}
line["x1"] = tonumber(lineAttrs.x1);
line["y1"] = tonumber(lineAttrs.y1);
line["x2"] = tonumber(lineAttrs.x2);
line["y2"] = tonumber(lineAttrs.y2);
line["fill"] = lineAttrs.fill;
line["stroke"] = lineAttrs.stroke;
line["stroke_width"] = lineAttrs["stroke-width"];
line["style"] = lineAttrs.style;
local trf = getTransformMatrix(lineAttrs.transform)
line = transformLine(line, trf);
return line;
end
function ParsePath(path)
function getNumbers(s)
local a = {}
for numString in s.gmatch(s, "[^%s]+") do
local number = tonumber(numString);
table.insert(a, number)
end
return a
end
local pathTag = path.tag;
local pathAttrs = path.attrs;
local s = "";
local t = "";
local segments = {}
for c in pathAttrs.d:gmatch "." do
if c == "," then
s = s .. " "
elseif c == "." then
s = s .. c;
elseif isNumeric(c) or c == " " then
s = s .. c;
else
if s ~= "" then
local numbers = getNumbers(s)
local segment = { t, numbers }
table.insert(segments, segment)
end
s = "";
t = c;
end
end
if t ~= "" then
local numbers = getNumbers(s)
local segment = { t, numbers }
table.insert(segments, segment)
end
local path = {}
path["d"] = segments;
path["fill"] = pathAttrs.fill;
path["stroke"] = pathAttrs.stroke;
path["stroke_width"] = pathAttrs["stroke-width"];
path["style"] = pathAttrs.style;
local trf = getTransformMatrix(pathAttrs.transform)
path = transformPath(path, trf);
return path;
end
function ParseElement(parsedElement)
local elementTag = parsedElement.tag;
local elementAttrs = parsedElement.attrs;
local elementChildren = parsedElement.children;
local items = {};
for i, child in pairs(elementChildren) do
if child.tag == "line" then
local item = ParseLine(child);
table.insert(items, { 'line', item });
elseif child.tag == "rect" then
local item = ParseRect(child);
table.insert(items, { 'rect', item });
elseif child.tag == "ellipse" then
local item = ParseEllipse(child);
table.insert(items, { 'ellipse', item });
elseif child.tag == "circle" then
local item = ParseCircle(child);
table.insert(items, { 'ellipse', item });
elseif child.tag == "path" then
local path = ParsePath(child);
table.insert(items, { 'path', path });
elseif child.tag == "g" then
local groupTrf = getTransformMatrix(child.attrs.transform)
local group = ParseElement(child);
group = transformGroup(group, groupTrf);
table.insert(items, { 'group', group })
end
end
if elementTag == 'svg' then
local width = elementAttrs.width;
assert(type(width) == "string", "svg width is not string")
if string.find(width, "px") then
width = (string.gsub(width, "px", ""))
end
local height = elementAttrs.height;
assert(type(height) == "string", "svg height is not string")
if string.find(height, "px") then
height = (string.gsub(height, "px", ""))
end
print('svg');
print('width', width)
print('height', height)
if elementAttrs.viewBox then
local svgTrf = {}
local i = 0;
for v in string.gmatch(elementAttrs.viewBox, "%S+") do
if i == 0 then
svgTrf.moveX = tonumber(v)
elseif i == 1 then
svgTrf.moveY = tonumber(v)
elseif i == 2 then
svgTrf.scaleX = width / tonumber(v)
elseif i == 3 then
svgTrf.scaleY = height / tonumber(v)
end
i = i + 1
end
items = transformGroup(items, svgTrf);
end
end
return items;
end
function ParseSvgText(xmlText, scaleX, scaleY, moveX, moveY)
print("==================================")
local parsedElement = xmlparser.parse(xmlText, {})
local items = ParseElement(parsedElement.children[1]) -- root element svg
-- PrintItems(items, 0);
items = transformGroup(items, { scaleX = scaleX, scaleY = scaleY, moveX = moveX, moveY = moveY })
-- PrintItems(items, 0);
return items;
end
function PrintItems(items, level)
for i, v in pairs(items) do
if v[1] == 'line' then
print(level, 'line', v[2].x1, v[2].y1, v[2].x2, v[2].y2, v[2].fill, v[2].stroke, v[2].stroke_width,
v[2].style)
elseif v[1] == 'rect' then
print(level, 'rect', v[2].x, v[2].y, v[2].width, v[2].height, v[2].fill, v[2].stroke, v[2].stroke_width,
v[2].style)
elseif v[1] == 'ellipse' then
print(level, 'ellipse', v[2].x, v[2].y, v[2].width, v[2].height, v[2].fill, v[2].stroke, v[2].stroke_width,
v[2].style)
elseif v[1] == 'path' then
print(level, 'path', v[2].d, v[2].fill, v[2].stroke, v[2].stroke_width, v[2].style)
for i, v in pairs(v[2].d) do
print('', v[1])
for j, v2 in pairs(v[2]) do
print('', '', v2)
end
end
elseif v[1] == 'group' then
print(level, 'group')
PrintItems(v[2], level + 1);
end
end
end
function Catch(what)
return what[1]
end
function Try(what)
status, result = pcall(what[1])
if not status then
what[2](result)
end
return result
end
-- from https://github.com/jonathanpoelen/lua-xmlparser
-- This file should be put in the Fusion/Modules/Lua folder
-- (on windows something like C:\ProgramData\Blackmagic Design\Fusion\Modules\Lua)
local io, string, pairs = io, string, pairs
local slashchar = string.byte('/', 1)
local E = string.byte('E', 1)
--! Return the default entity table.
--! @return table
local function defaultEntityTable()
return { quot='"', apos='\'', lt='<', gt='>', amp='&', tab='\t', nbsp=' ', }
end
--! @param[in] s string
--! @param[in] entities table : with entity name as key and value as replacement
--! @return string
local function replaceEntities(s, entities)
return s:gsub('&([^;]+);', entities)
end
--! Add entities to resultEntities then return it.
--! Create new table when resultEntities is nul.
--! Create an entity table from the document entity table.
--! @param[in] docEntities table
--! @param[in,out] resultEntities table|nil
--! @return table
local function createEntityTable(docEntities, resultEntities)
local entities = resultEntities or defaultEntityTable()
for _,e in pairs(docEntities) do
e.value = replaceEntities(e.value, entities)
entities[e.name] = e.value
end
return entities
end
--! Return a document `table`.
--! @code
--! document = {
--! children = {
--! { text=string } or
--! { tag=string,
--! attrs={ [name]=value ... },
--! orderedattrs={ { name=string, value=string }, ... },
--! children={ ... }
--! },
--! ...
--! },
--! entities = { { name=string, value=string }, ... },
--! tentities = { name=value, ... } -- only if evalEntities = true
--! }
--! @endcode
--! If `evalEntities` is `true`, the entities are replaced and
--! a `tentity` member is added to the document `table`.
--! @param[in] s string : xml data
--! @param[in] evalEntities boolean
--! @return table
local function parse(s, evalEntities)
-- remove comments
s = s:gsub('<!%-%-(.-)%-%->', '')
local entities, tentities = {}
if evalEntities then
local pos = s:find('<[_%w]')
if pos then
s:sub(1, pos):gsub('<!ENTITY%s+([_%w]+)%s+(.)(.-)%2', function(name, _, entity)
entities[#entities+1] = {name=name, value=entity}
end)
tentities = createEntityTable(entities)
s = replaceEntities(s:sub(pos), tentities)
end
end
local t, l = {}, {}
local addtext = function(txt)
txt = txt:match'^%s*(.*%S)' or ''
if #txt ~= 0 then
t[#t+1] = {text=txt}
end
end
s:gsub('<([?!/]?)([-:_%w]+)%s*(/?>?)([^<]*)', function(type, name, closed, txt)
-- open
if #type == 0 then
local attrs, orderedattrs = {}, {}
if #closed == 0 then
local len = 0
for all,aname,_,value,starttxt in string.gmatch(txt, "(.-([-_%w]+)%s*=%s*(.)(.-)%3%s*(/?>?))") do
len = len + #all
attrs[aname] = value
orderedattrs[#orderedattrs+1] = {name=aname, value=value}
if #starttxt ~= 0 then
txt = txt:sub(len+1)
closed = starttxt
break
end
end
end
t[#t+1] = {tag=name, attrs=attrs, children={}, orderedattrs=orderedattrs}
if closed:byte(1) ~= slashchar then
l[#l+1] = t
t = t[#t].children
end
addtext(txt)
-- close
elseif '/' == type then
t = l[#l]
l[#l] = nil
addtext(txt)
-- ENTITY
elseif '!' == type then
if E == name:byte(1) then
txt:gsub('([_%w]+)%s+(.)(.-)%2', function(name, _, entity)
entities[#entities+1] = {name=name, value=entity}
end, 1)
end
-- elseif '?' == type then
-- print('? ' .. name .. ' // ' .. attrs .. '$$')
-- elseif '-' == type then
-- print('comment ' .. name .. ' // ' .. attrs .. '$$')
-- else
-- print('o ' .. #p .. ' // ' .. name .. ' // ' .. attrs .. '$$')
end
end)
return {children=t, entities=entities, tentities=tentities}
end
-- Return a tuple `document table, error file`.
-- @param filename[in] string
-- @param evalEntities[in] boolean : see \c parse()
-- @return table : see parse
local function parseFile(filename, evalEntities)
local f, err = io.open(filename)
if f then
local content = f:read'*a'
f:close()
return parse(content, evalEntities), nil
end
return f, err
end
return {
parse = parse,
parseFile = parseFile,
defaultEntityTable = defaultEntityTable,
replaceEntities = replaceEntities,
createEntityTable = createEntityTable,
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment