///|
/// SVG XML Parser
/// Parses SVG markup into SVGNode tree
///|
/// SVG Parser state
priv struct SVGParser {
data : String
mut pos : Int
len : Int
}
///|
/// SVG parse context for reusable resources
priv struct SVGParseContext {
symbols : SymbolRegistry
defs : DefsRegistry
clips : ClipPathRegistry
masks : MaskRegistry
patterns : PatternRegistry
gradients : GradientRegistry
markers : MarkerRegistry
style_stack : Array[InheritedStyle]
viewport_stack : Array[(Double, Double)]
}
///|
fn SVGParseContext::new() -> SVGParseContext {
{
symbols: SymbolRegistry::new(),
defs: DefsRegistry::new(),
clips: ClipPathRegistry::new(),
masks: MaskRegistry::new(),
patterns: PatternRegistry::new(),
gradients: GradientRegistry::new(),
markers: MarkerRegistry::new(),
style_stack: [],
viewport_stack: [],
}
}
///|
priv struct InheritedStyle {
fill : Paint
stroke : StrokeStyle
color : Color?
}
///|
fn InheritedStyle::default() -> InheritedStyle {
{
fill: SolidColor(Color::black()),
stroke: StrokeStyle::default(),
color: None,
}
}
///|
fn SVGParseContext::current_style(self : SVGParseContext) -> InheritedStyle {
if self.style_stack.length() == 0 {
InheritedStyle::default()
} else {
self.style_stack[self.style_stack.length() - 1]
}
}
///|
fn inherited_style_from_node(node : SVGNode) -> InheritedStyle {
{ fill: node.fill, stroke: node.stroke, color: node.color }
}
///|
fn apply_inherited_style(node : SVGNode, inherited : InheritedStyle) -> Unit {
if !node.fill_is_set {
node.fill = inherited.fill
}
if !node.stroke_paint_is_set {
node.stroke = { ..node.stroke, paint: inherited.stroke.paint }
}
if !node.stroke_width_is_set {
node.stroke = { ..node.stroke, width: inherited.stroke.width }
}
if !node.color_is_set {
node.color = inherited.color
}
}
///|
fn apply_inherited_markers(child : SVGNode, parent : SVGNode) -> Unit {
if !child.marker_start_is_set {
child.marker_start = parent.marker_start
}
if !child.marker_mid_is_set {
child.marker_mid = parent.marker_mid
}
if !child.marker_end_is_set {
child.marker_end = parent.marker_end
}
}
///|
fn SVGParseContext::current_viewport(
self : SVGParseContext,
) -> (Double, Double)? {
if self.viewport_stack.length() == 0 {
None
} else {
Some(self.viewport_stack[self.viewport_stack.length() - 1])
}
}
///|
fn SVGParser::new(data : String) -> SVGParser {
{ data, pos: 0, len: data.length() }
}
///|
fn SVGParser::is_end(self : SVGParser) -> Bool {
self.pos >= self.len
}
///|
fn SVGParser::peek(self : SVGParser) -> Char {
if self.pos < self.len {
Int::unsafe_to_char(self.data[self.pos].to_int())
} else {
'\u0000'
}
}
///|
fn SVGParser::advance(self : SVGParser) -> Char {
if self.pos < self.len {
let c = Int::unsafe_to_char(self.data[self.pos].to_int())
self.pos = self.pos + 1
c
} else {
'\u0000'
}
}
///|
fn SVGParser::skip_whitespace(self : SVGParser) -> Unit {
while !self.is_end() {
let c = self.peek()
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
let _ = self.advance()
} else {
break
}
}
}
///|
fn SVGParser::expect_char(self : SVGParser, expected : Char) -> Bool {
if self.peek() == expected {
let _ = self.advance()
true
} else {
false
}
}
///|
fn SVGParser::read_until(self : SVGParser, stop : Char) -> String {
let buf = StringBuilder::new()
while !self.is_end() && self.peek() != stop {
buf.write_char(self.advance())
}
buf.to_string()
}
///|
fn SVGParser::read_name(self : SVGParser) -> String {
let buf = StringBuilder::new()
while !self.is_end() {
let c = self.peek()
if is_name_char(c) {
buf.write_char(self.advance())
} else {
break
}
}
buf.to_string()
}
///|
fn is_name_char(c : Char) -> Bool {
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '-' ||
c == '_' ||
c == ':'
}
///|
fn SVGParser::read_attr_value(self : SVGParser) -> String {
self.skip_whitespace()
if !self.expect_char('=') {
return ""
}
self.skip_whitespace()
let quote = self.peek()
if quote != '"' && quote != '\'' {
return ""
}
let _ = self.advance()
let value = self.read_until(quote)
let _ = self.advance() // Skip closing quote
value
}
///|
/// Parse SVG markup string into SVGNode
pub fn parse_svg(svg_str : String) -> SVGNode? {
match parse_svg_document(svg_str) {
Some(doc) => Some(doc.root)
None => None
}
}
///|
/// Parse SVG markup string into SVGDocument with resources
pub fn parse_svg_document(svg_str : String) -> SVGDocument? {
let parser = SVGParser::new(svg_str)
let ctx = SVGParseContext::new()
match parse_element(parser, ctx, "") {
Some(root) =>
Some({
root,
symbols: ctx.symbols,
clips: ctx.clips,
masks: ctx.masks,
patterns: ctx.patterns,
gradients: ctx.gradients,
markers: ctx.markers,
})
None => None
}
}
///|
fn parse_element(
parser : SVGParser,
ctx : SVGParseContext,
parent_tag : String,
) -> SVGNode? {
parser.skip_whitespace()
// Skip comments and declarations
while !parser.is_end() {
if parser.peek() == '<' {
let _ = parser.advance()
if parser.peek() == '!' || parser.peek() == '?' {
// Skip comment or declaration
skip_comment_or_decl(parser)
parser.skip_whitespace()
continue
}
break
} else {
let _ = parser.advance()
}
}
if parser.is_end() {
return None
}
// Read tag name
let tag_name = parser.read_name()
if tag_name.length() == 0 {
return None
}
// Parse attributes (raw)
let attrs = parse_attributes(parser)
parser.skip_whitespace()
// Check for self-closing tag
let mut self_closing = false
if parser.peek() == '/' {
let _ = parser.advance()
let _ = parser.expect_char('>')
self_closing = true
} else {
// Expect closing '>'
let _ = parser.expect_char('>')
}
match tag_name {
"defs" => {
if !self_closing {
let temp = SVGNode::new(Group)
parse_children(parser, temp, tag_name, ctx)
}
Group |> SVGNode::new |> Some
}
"symbol" => {
let symbol_node = SVGNode::new(Group)
let id = match get_attr(attrs, "id") {
Some(v) => v
None => ""
}
symbol_node.id = id
let view_box = match get_attr(attrs, "viewBox") {
Some(v) => parse_view_box(v)
None => None
}
let width = match get_attr(attrs, "width") {
Some(v) => v |> parse_length |> Some
None => None
}
let height = match get_attr(attrs, "height") {
Some(v) => v |> parse_length |> Some
None => None
}
let display_none = match get_attr(attrs, "display") {
Some(v) => trim_string(v) == "none"
None => false
}
let preserve = match get_attr(attrs, "preserveAspectRatio") {
Some(v) => parse_preserve_aspect_ratio(v)
None => PreserveAspectRatio::default()
}
if !self_closing {
parse_children(parser, symbol_node, tag_name, ctx)
}
let symbol = match view_box {
Some(vb) =>
{
..Symbol::with_viewbox(id, symbol_node, vb),
preserve_aspect_ratio: preserve,
}
None =>
{ ..Symbol::new(id, symbol_node), preserve_aspect_ratio: preserve }
}
let symbol = { ..symbol, width, height, display_none }
ctx.symbols.add(symbol)
Group |> SVGNode::new |> Some
}
"clipPath" => {
if !self_closing {
parse_clip_path(parser, attrs, ctx, tag_name)
} else {
parse_clip_path_from_attrs(attrs, ctx)
}
Group |> SVGNode::new |> Some
}
"mask" => {
if !self_closing {
parse_mask(parser, attrs, ctx, tag_name)
} else {
parse_mask_from_attrs(attrs, ctx)
}
Group |> SVGNode::new |> Some
}
"marker" => {
if !self_closing {
parse_marker(parser, attrs, ctx, tag_name, parent_tag)
} else {
parse_marker_from_attrs(attrs, ctx, parent_tag)
}
Group |> SVGNode::new |> Some
}
"image" => {
let node = parse_image(attrs)
if !self_closing {
skip_to_end_tag(parser, tag_name)
}
node
}
"pattern" => {
if !self_closing {
parse_pattern(parser, attrs, ctx, tag_name, parent_tag)
} else {
parse_pattern_from_attrs(attrs, ctx, parent_tag)
}
Group |> SVGNode::new |> Some
}
"linearGradient" => {
if !self_closing {
parse_linear_gradient(parser, attrs, ctx, tag_name, parent_tag)
} else {
parse_linear_gradient_from_attrs(attrs, ctx, parent_tag)
}
Group |> SVGNode::new |> Some
}
"radialGradient" => {
if !self_closing {
parse_radial_gradient(parser, attrs, ctx, tag_name, parent_tag)
} else {
parse_radial_gradient_from_attrs(attrs, ctx, parent_tag)
}
Group |> SVGNode::new |> Some
}
"use" => {
let node = parse_use(attrs, ctx)
if !self_closing {
skip_to_end_tag(parser, tag_name)
}
match node {
Some(_) => node
None => Group |> SVGNode::new |> Some
}
}
_ => {
// Create node based on tag
let node = create_node_for_tag(tag_name)
// Apply attributes
apply_attributes(node, attrs)
if tag_name == "svg" {
if parent_tag != "" {
if get_attr(attrs, "width") is Some(v) {
node.viewport_width = v |> parse_length |> Some
}
if get_attr(attrs, "height") is Some(v) {
node.viewport_height = v |> parse_length |> Some
}
}
if get_attr(attrs, "overflow") is Some(v) {
if trim_string(v) == "visible" {
node.clip_overflow = false
}
}
let x = match get_attr(attrs, "x") {
Some(v) => parse_length(v)
None => 0.0
}
let y = match get_attr(attrs, "y") {
Some(v) => parse_length(v)
None => 0.0
}
if x != 0.0 || y != 0.0 {
node.transform = Transform::translate(x, y).multiply(node.transform)
}
}
let mut pushed = false
if tag_name == "svg" && !self_closing {
match (node.viewport_width, node.viewport_height) {
(Some(w), Some(h)) => {
ctx.viewport_stack.push((w, h))
pushed = true
}
_ => ()
}
}
// Parse children (for container elements)
if !self_closing {
match node.shape {
Group => parse_children(parser, node, tag_name, ctx)
_ =>
// Skip to end tag for non-container elements
skip_to_end_tag(parser, tag_name)
}
}
if pushed {
let _ = ctx.viewport_stack.pop()
}
Some(node)
}
}
}
///|
fn skip_comment_or_decl(parser : SVGParser) -> Unit {
// Skip until we find '>'
while !parser.is_end() {
if parser.advance() == '>' {
break
}
}
}
///|
fn skip_to_end_tag(parser : SVGParser, _tag_name : String) -> Unit {
// Skip until we find the closing tag
while !parser.is_end() {
if parser.peek() == '<' {
let _ = parser.advance()
if parser.peek() == '/' {
let _ = parser.advance()
let _ = parser.read_name() // Read the tag name
parser.skip_whitespace()
let _ = parser.expect_char('>')
break
}
} else {
let _ = parser.advance()
}
}
}
///|
fn parse_children(
parser : SVGParser,
parent : SVGNode,
parent_tag : String,
ctx : SVGParseContext,
) -> Unit {
let parent_style = ctx.current_style()
apply_inherited_style(parent, parent_style)
ctx.style_stack.push(inherited_style_from_node(parent))
while !parser.is_end() {
parser.skip_whitespace()
if parser.peek() != '<' {
// Skip text content
while !parser.is_end() && parser.peek() != '<' {
let _ = parser.advance()
}
continue
}
let _ = parser.advance()
// Check for end tag
if parser.peek() == '/' {
let _ = parser.advance()
let end_tag = parser.read_name()
if end_tag == parent_tag {
parser.skip_whitespace()
let _ = parser.expect_char('>')
break
}
// Skip malformed end tag
while !parser.is_end() && parser.advance() != '>' {
}
continue
}
// Check for comment/declaration
if parser.peek() == '!' || parser.peek() == '?' {
skip_comment_or_decl(parser)
continue
}
// Parse child element
// Backtrack the '<' we consumed
parser.pos = parser.pos - 1
match parse_element(parser, ctx, parent_tag) {
Some(child) => {
let child_node = child
apply_inherited_style(child_node, ctx.current_style())
apply_inherited_markers(child_node, parent)
parent.children.push(child_node)
if child_node.id.length() > 0 {
ctx.defs.add(child_node.id, child_node)
}
}
None => break
}
}
let _ = ctx.style_stack.pop()
}
///|
fn create_node_for_tag(tag_name : String) -> SVGNode {
let node = match tag_name {
"rect" =>
SVGNode::new(Rect(x=0.0, y=0.0, width=0.0, height=0.0, rx=0.0, ry=0.0))
"circle" => SVGNode::new(Circle(cx=0.0, cy=0.0, r=0.0))
"ellipse" => SVGNode::new(Ellipse(cx=0.0, cy=0.0, rx=0.0, ry=0.0))
"line" => SVGNode::new(Line(x1=0.0, y1=0.0, x2=0.0, y2=0.0))
"polyline" => SVGNode::new(Polyline(points=[]))
"polygon" => SVGNode::new(Polygon(points=[]))
"path" => SVGNode::new(Path(commands=[]))
_ => SVGNode::new(Group) // svg, g, defs, etc. are groups
}
node
}
///|
fn parse_attributes(parser : SVGParser) -> Array[(String, String)] {
let attrs : Array[(String, String)] = []
while !parser.is_end() {
parser.skip_whitespace()
let c = parser.peek()
if c == '>' || c == '/' {
break
}
let attr_name = parser.read_name()
if attr_name.length() == 0 {
break
}
let attr_value = parser.read_attr_value()
attrs.push((attr_name, attr_value))
}
attrs
}
///|
fn apply_attributes(node : SVGNode, attrs : Array[(String, String)]) -> Unit {
for attr in attrs {
apply_presentation_attribute(node, attr.0, attr.1)
}
}
///|
fn apply_attribute(node : SVGNode, name : String, value : String) -> Unit {
match name {
"id" => node.id = value
"style" => apply_style_attributes(node, value)
"viewBox" => node.view_box = parse_view_box(value)
"preserveAspectRatio" => {
node.preserve_aspect_ratio = parse_preserve_aspect_ratio(value)
node.preserve_aspect_ratio_is_set = true
}
"clip-path" =>
if parse_url_ref(value) is Some(id) {
node.set_clip_path(id)
}
"mask" => if parse_url_ref(value) is Some(id) { node.set_mask(id) }
"requiredExtensions" => node.opacity = 0.0
"x" =>
match node.shape {
Rect(..) as r =>
node.shape = Rect(
x=parse_length(value),
y=r.y,
width=r.width,
height=r.height,
rx=r.rx,
ry=r.ry,
)
Image(..) as i =>
node.shape = Image(
x=parse_length(value),
y=i.y,
width=i.width,
height=i.height,
href=i.href,
)
_ => ()
}
"y" =>
match node.shape {
Rect(..) as r =>
node.shape = Rect(
x=r.x,
y=parse_length(value),
width=r.width,
height=r.height,
rx=r.rx,
ry=r.ry,
)
Image(..) as i =>
node.shape = Image(
x=i.x,
y=parse_length(value),
width=i.width,
height=i.height,
href=i.href,
)
_ => ()
}
"width" =>
match node.shape {
Rect(..) as r =>
node.shape = Rect(
x=r.x,
y=r.y,
width=parse_length(value),
height=r.height,
rx=r.rx,
ry=r.ry,
)
Image(..) as i =>
node.shape = Image(
x=i.x,
y=i.y,
width=parse_length(value),
height=i.height,
href=i.href,
)
_ => ()
}
"height" =>
match node.shape {
Rect(..) as r =>
node.shape = Rect(
x=r.x,
y=r.y,
width=r.width,
height=parse_length(value),
rx=r.rx,
ry=r.ry,
)
Image(..) as i =>
node.shape = Image(
x=i.x,
y=i.y,
width=i.width,
height=parse_length(value),
href=i.href,
)
_ => ()
}
"rx" =>
match node.shape {
Rect(..) as r =>
node.shape = Rect(
x=r.x,
y=r.y,
width=r.width,
height=r.height,
rx=parse_length(value),
ry=r.ry,
)
Ellipse(..) as e =>
node.shape = Ellipse(
cx=e.cx,
cy=e.cy,
rx=parse_length(value),
ry=e.ry,
)
_ => ()
}
"ry" =>
match node.shape {
Rect(..) as r =>
node.shape = Rect(
x=r.x,
y=r.y,
width=r.width,
height=r.height,
rx=r.rx,
ry=parse_length(value),
)
Ellipse(..) as e =>
node.shape = Ellipse(
cx=e.cx,
cy=e.cy,
rx=e.rx,
ry=parse_length(value),
)
_ => ()
}
"cx" =>
match node.shape {
Circle(..) as c =>
node.shape = Circle(cx=parse_length(value), cy=c.cy, r=c.r)
Ellipse(..) as e =>
node.shape = Ellipse(
cx=parse_length(value),
cy=e.cy,
rx=e.rx,
ry=e.ry,
)
_ => ()
}
"cy" =>
match node.shape {
Circle(..) as c =>
node.shape = Circle(cx=c.cx, cy=parse_length(value), r=c.r)
Ellipse(..) as e =>
node.shape = Ellipse(
cx=e.cx,
cy=parse_length(value),
rx=e.rx,
ry=e.ry,
)
_ => ()
}
"r" =>
match node.shape {
Circle(..) as c =>
node.shape = Circle(cx=c.cx, cy=c.cy, r=parse_length(value))
_ => ()
}
"x1" =>
match node.shape {
Line(..) as l =>
node.shape = Line(x1=parse_length(value), y1=l.y1, x2=l.x2, y2=l.y2)
_ => ()
}
"y1" =>
match node.shape {
Line(..) as l =>
node.shape = Line(x1=l.x1, y1=parse_length(value), x2=l.x2, y2=l.y2)
_ => ()
}
"x2" =>
match node.shape {
Line(..) as l =>
node.shape = Line(x1=l.x1, y1=l.y1, x2=parse_length(value), y2=l.y2)
_ => ()
}
"y2" =>
match node.shape {
Line(..) as l =>
node.shape = Line(x1=l.x1, y1=l.y1, x2=l.x2, y2=parse_length(value))
_ => ()
}
"d" =>
match node.shape {
Path(..) => node.shape = Path(commands=parse_path(value))
_ => ()
}
"points" =>
match node.shape {
Polyline(..) => node.shape = Polyline(points=parse_points(value))
Polygon(..) => node.shape = Polygon(points=parse_points(value))
_ => ()
}
"fill" => {
node.fill = parse_paint(value)
node.fill_is_set = true
}
"color" => {
node.color = Some(parse_color(value))
node.color_is_set = true
}
"paint-order" => node.paint_order = parse_paint_order(value)
"stroke" => {
node.stroke = {
paint: parse_paint(value),
width: node.stroke.width,
linecap: node.stroke.linecap,
linejoin: node.stroke.linejoin,
miterlimit: node.stroke.miterlimit,
dasharray: node.stroke.dasharray,
dashoffset: node.stroke.dashoffset,
}
node.stroke_paint_is_set = true
}
"stroke-width" => {
node.stroke_width_is_set = true
node.stroke = {
paint: node.stroke.paint,
width: parse_length(value),
linecap: node.stroke.linecap,
linejoin: node.stroke.linejoin,
miterlimit: node.stroke.miterlimit,
dasharray: node.stroke.dasharray,
dashoffset: node.stroke.dashoffset,
}
}
"opacity" => node.opacity = parse_number(value)
"fill-opacity" => node.fill_opacity = parse_number(value)
"stroke-opacity" => node.stroke_opacity = parse_number(value)
"transform" => node.transform = parse_transform(value)
// Stroke detail attributes
"stroke-linecap" =>
node.stroke = { ..node.stroke, linecap: parse_linecap(value) }
"stroke-linejoin" =>
node.stroke = { ..node.stroke, linejoin: parse_linejoin(value) }
"stroke-miterlimit" =>
node.stroke = { ..node.stroke, miterlimit: parse_number(value) }
"stroke-dasharray" =>
node.stroke = { ..node.stroke, dasharray: parse_dasharray(value) }
"stroke-dashoffset" =>
node.stroke = { ..node.stroke, dashoffset: parse_number(value) }
"marker-start" => {
node.marker_start = parse_marker_ref(value)
node.marker_start_is_set = true
}
"marker-mid" => {
node.marker_mid = parse_marker_ref(value)
node.marker_mid_is_set = true
}
"marker-end" => {
node.marker_end = parse_marker_ref(value)
node.marker_end_is_set = true
}
"marker" =>
match parse_marker_ref(value) {
Some(id) => {
node.marker_start = Some(id)
node.marker_mid = Some(id)
node.marker_end = Some(id)
node.marker_start_is_set = true
node.marker_mid_is_set = true
node.marker_end_is_set = true
}
None => {
node.marker_start = None
node.marker_mid = None
node.marker_end = None
node.marker_start_is_set = true
node.marker_mid_is_set = true
node.marker_end_is_set = true
}
}
// Fill rule
"fill-rule" => node.fill_rule = parse_fill_rule(value)
"clip-rule" => node.fill_rule = parse_fill_rule(value)
_ => () // Ignore unknown attributes
}
}
///|
fn apply_presentation_attribute(
node : SVGNode,
name : String,
value : String,
) -> Unit {
if name == "marker" {
return
}
apply_attribute(node, name, value)
}
///|
fn parse_marker_ref(value : String) -> String? {
let v = trim_string(value)
if v.length() == 0 || v == "none" {
return None
}
parse_url_ref(v)
}
///|
fn apply_style_attributes(node : SVGNode, value : String) -> Unit {
let pairs = parse_style_pairs(value)
for pair in pairs {
let name = pair.0.to_lower()
if name == "style" {
continue
}
apply_attribute(node, name, pair.1)
}
}
///|
fn apply_style_attributes_filtered(node : SVGNode, value : String) -> Unit {
let pairs = parse_style_pairs(value)
for pair in pairs {
let name = pair.0.to_lower()
if name == "style" ||
name == "transform" ||
name == "x" ||
name == "y" ||
name == "width" ||
name == "height" {
continue
}
apply_attribute(node, name, pair.1)
}
}
///|
fn apply_presentation_attributes(
node : SVGNode,
attrs : Array[(String, String)],
) -> Unit {
for attr in attrs {
let name = attr.0
if name == "id" ||
name == "x" ||
name == "y" ||
name == "width" ||
name == "height" ||
name == "href" ||
name == "xlink:href" ||
name == "transform" {
continue
}
if name == "style" {
apply_style_attributes_filtered(node, attr.1)
} else {
apply_presentation_attribute(node, name, attr.1)
}
}
}
///|
fn extract_paint_order(attrs : Array[(String, String)]) -> PaintOrder? {
if get_attr(attrs, "paint-order") is Some(v) {
return v |> parse_paint_order |> Some
}
if get_attr(attrs, "style") is Some(style) {
let pairs = parse_style_pairs(style)
for pair in pairs {
let name = pair.0.to_lower()
if name == "paint-order" {
return pair.1 |> parse_paint_order |> Some
}
}
}
None
}
///|
fn apply_paint_order_recursive(node : SVGNode, order : PaintOrder) -> Unit {
node.paint_order = order
for child in node.children {
apply_paint_order_recursive(child, order)
}
}
///|
fn parse_style_pairs(value : String) -> Array[(String, String)] {
let pairs : Array[(String, String)] = []
let mut buf = StringBuilder::new()
for i in 0.. 0 {
parse_style_entry(entry, pairs)
}
buf = StringBuilder::new()
} else {
buf.write_char(c)
}
}
let entry = trim_string(buf.to_string())
if entry.length() > 0 {
parse_style_entry(entry, pairs)
}
pairs
}
///|
fn parse_style_entry(entry : String, pairs : Array[(String, String)]) -> Unit {
let mut colon_index = -1
for i in 0.. 0 {
pairs.push((key, value))
}
}
///|
fn get_attr(attrs : Array[(String, String)], name : String) -> String? {
for attr in attrs {
if attr.0 == name {
return Some(attr.1)
}
}
None
}
///|
fn parse_url_ref(value : String) -> String? {
let v = trim_string(value)
if v.length() == 0 {
return None
}
if string_starts_with(v, "url(") && string_ends_with(v, ")") {
let inner = build_substring(v, 4, v.length() - 1)
let trimmed = trim_string(inner)
if trimmed.length() > 0 && Int::unsafe_to_char(trimmed[0].to_int()) == '#' {
return Some(build_substring(trimmed, 1, trimmed.length()))
}
return Some(trimmed)
}
if Int::unsafe_to_char(v[0].to_int()) == '#' {
return Some(build_substring(v, 1, v.length()))
}
None
}
///|
fn parse_number_list(value : String) -> Array[Double] {
let nums : Array[Double] = []
let mut buf = StringBuilder::new()
for i in 0.. 0 {
nums.push(parse_number(buf.to_string()))
buf = StringBuilder::new()
}
} else {
buf.write_char(c)
}
}
if buf.to_string().length() > 0 {
nums.push(parse_number(buf.to_string()))
}
nums
}
///|
fn split_tokens(value : String) -> Array[String] {
let tokens : Array[String] = []
let mut buf = StringBuilder::new()
for i in 0.. 0 {
tokens.push(buf.to_string())
buf = StringBuilder::new()
}
} else {
buf.write_char(c)
}
}
if buf.to_string().length() > 0 {
tokens.push(buf.to_string())
}
tokens
}
///|
fn parse_view_box(value : String) -> ViewBox? {
let nums = parse_number_list(value)
if nums.length() < 4 {
return None
}
Some(ViewBox::{
min_x: nums[0],
min_y: nums[1],
width: nums[2],
height: nums[3],
})
}
///|
fn parse_preserve_aspect_ratio(value : String) -> PreserveAspectRatio {
let v = trim_string(value)
if v.length() == 0 {
return PreserveAspectRatio::default()
}
let tokens = split_tokens(v)
if tokens.length() == 0 {
return PreserveAspectRatio::default()
}
if tokens[0] == "none" {
return PreserveAspectRatio::{ align: None, meet_or_slice: Meet }
}
let align = match tokens[0] {
"xMinYMin" => XMinYMin
"xMidYMin" => XMidYMin
"xMaxYMin" => XMaxYMin
"xMinYMid" => XMinYMid
"xMidYMid" => XMidYMid
"xMaxYMid" => XMaxYMid
"xMinYMax" => XMinYMax
"xMidYMax" => XMidYMax
"xMaxYMax" => XMaxYMax
_ => XMidYMid
}
let meet_or_slice = if tokens.length() > 1 {
match tokens[1] {
"slice" => Slice
_ => Meet
}
} else {
Meet
}
PreserveAspectRatio::{ align, meet_or_slice }
}
///|
fn parse_mask_units(value : String) -> MaskUnits {
match trim_string(value) {
"userSpaceOnUse" => MaskUnits::UserSpaceOnUse
"objectBoundingBox" => MaskUnits::ObjectBoundingBox
_ => MaskUnits::ObjectBoundingBox
}
}
///|
fn parse_clip_path_units(value : String) -> ClipPathUnits {
match trim_string(value) {
"objectBoundingBox" => ClipPathUnits::ObjectBoundingBox
_ => ClipPathUnits::UserSpaceOnUse
}
}
///|
fn parse_mask_type(value : String) -> MaskType {
match trim_string(value) {
"alpha" => Alpha
_ => Luminance
}
}
///|
fn parse_use(
attrs : Array[(String, String)],
ctx : SVGParseContext,
) -> SVGNode? {
let href = match get_attr(attrs, "href") {
Some(v) => Some(v)
None => get_attr(attrs, "xlink:href")
}
let href = match href {
Some(v) => v
None => return None
}
let x = match get_attr(attrs, "x") {
Some(v) => parse_length(v)
None => 0.0
}
let y = match get_attr(attrs, "y") {
Some(v) => parse_length(v)
None => 0.0
}
let width = match get_attr(attrs, "width") {
Some(v) => v |> parse_length |> Some
None => None
}
let height = match get_attr(attrs, "height") {
Some(v) => v |> parse_length |> Some
None => None
}
let (width, height) = match get_attr(attrs, "style") {
Some(style) => {
let mut w = width
let mut h = height
let pairs = parse_style_pairs(style)
for pair in pairs {
let name = pair.0.to_lower()
if name == "width" {
w = pair.1 |> parse_length |> Some
} else if name == "height" {
h = pair.1 |> parse_length |> Some
}
}
(w, h)
}
None => (width, height)
}
let use_elem = { ..UseElement::new(href, x, y), width, height }
let use_elem = match get_attr(attrs, "transform") {
Some(v) => { ..use_elem, transform: parse_transform(v) }
None => use_elem
}
let viewport_fallback = ctx.current_viewport()
let (base_href, frag) = split_href_fragment(use_elem.href)
let allow_data_url_use = false
match decode_data_svg(base_href) {
Some(svg_data) if allow_data_url_use =>
match parse_svg_document(svg_data) {
Some(doc) => {
let node = match frag {
Some(id) =>
match find_node_by_id(doc.root, id) {
Some(found) => found.clone()
None => return None
}
None => doc.root.clone()
}
let (vb, force_non_uniform) = match node.view_box {
Some(vb) => (Some(vb), false)
None =>
match (node.viewport_width, node.viewport_height) {
(Some(w), Some(h)) =>
(
Some(ViewBox::{
min_x: 0.0,
min_y: 0.0,
width: w,
height: h,
}),
true,
)
_ =>
match viewport_fallback {
Some((vw, vh)) =>
(
Some(ViewBox::{
min_x: 0.0,
min_y: 0.0,
width: vw,
height: vh,
}),
true,
)
None => (None, false)
}
}
}
if force_non_uniform {
node.preserve_aspect_ratio = PreserveAspectRatio::{
align: None,
meet_or_slice: Meet,
}
}
let vw = match use_elem.width {
Some(w) => Some(w)
None =>
match node.viewport_width {
Some(w) => Some(w)
None =>
match viewport_fallback {
Some((vw, _)) => Some(vw)
None => None
}
}
}
let vh = match use_elem.height {
Some(h) => Some(h)
None =>
match node.viewport_height {
Some(h) => Some(h)
None =>
match viewport_fallback {
Some((_, vh)) => Some(vh)
None => None
}
}
}
node.view_box = vb
node.viewport_width = vw
node.viewport_height = vh
let translate = Transform::translate(use_elem.x, use_elem.y)
let base = translate.multiply(use_elem.transform)
node.transform = base.multiply(node.transform)
node.id = get_attr(attrs, "id").unwrap_or("")
apply_presentation_attributes(node, attrs)
if extract_paint_order(attrs) is Some(order) {
apply_paint_order_recursive(node, order)
}
Some(node)
}
None => None
}
_ =>
match use_elem.instantiate(ctx.symbols) {
Some(node) => {
if viewport_fallback is Some((vw, vh)) {
if node.viewport_width is None {
node.viewport_width = Some(vw)
}
if node.viewport_height is None {
node.viewport_height = Some(vh)
}
if node.view_box is None &&
node.viewport_width is Some(_) &&
node.viewport_height is Some(_) {
node.view_box = Some(ViewBox::{
min_x: 0.0,
min_y: 0.0,
width: node.viewport_width.unwrap(),
height: node.viewport_height.unwrap(),
})
node.preserve_aspect_ratio = PreserveAspectRatio::{
align: None,
meet_or_slice: Meet,
}
}
}
node.id = get_attr(attrs, "id").unwrap_or("")
apply_presentation_attributes(node, attrs)
if extract_paint_order(attrs) is Some(order) {
apply_paint_order_recursive(node, order)
}
Some(node)
}
None => {
let id = use_elem.get_id()
match ctx.defs.get(id) {
Some(target) => {
let node = target.clone()
let (vb, force_non_uniform) = match node.view_box {
Some(vb) => (Some(vb), false)
None =>
match (node.viewport_width, node.viewport_height) {
(Some(w), Some(h)) =>
(
Some(ViewBox::{
min_x: 0.0,
min_y: 0.0,
width: w,
height: h,
}),
true,
)
_ =>
match viewport_fallback {
Some((vw, vh)) =>
(
Some(ViewBox::{
min_x: 0.0,
min_y: 0.0,
width: vw,
height: vh,
}),
true,
)
None => (None, false)
}
}
}
if force_non_uniform {
node.preserve_aspect_ratio = PreserveAspectRatio::{
align: None,
meet_or_slice: Meet,
}
}
let vw = match use_elem.width {
Some(w) => Some(w)
None =>
match node.viewport_width {
Some(w) => Some(w)
None =>
match viewport_fallback {
Some((vw, _)) => Some(vw)
None => None
}
}
}
let vh = match use_elem.height {
Some(h) => Some(h)
None =>
match node.viewport_height {
Some(h) => Some(h)
None =>
match viewport_fallback {
Some((_, vh)) => Some(vh)
None => None
}
}
}
node.view_box = vb
node.viewport_width = vw
node.viewport_height = vh
let translate = Transform::translate(use_elem.x, use_elem.y)
let base = translate.multiply(use_elem.transform)
node.transform = base.multiply(node.transform)
node.id = get_attr(attrs, "id").unwrap_or("")
apply_presentation_attributes(node, attrs)
if extract_paint_order(attrs) is Some(order) {
apply_paint_order_recursive(node, order)
}
Some(node)
}
None => None
}
}
}
}
}
///|
fn decode_html_entities(value : String) -> String {
let mut i = 0
let len = value.length()
let buf = StringBuilder::new()
while i < len {
let c = Int::unsafe_to_char(value[i].to_int())
if c == '&' {
if i + 3 < len &&
value[i + 1] == 'l' &&
value[i + 2] == 't' &&
value[i + 3] == ';' {
buf.write_char('<')
i = i + 4
continue
}
if i + 3 < len &&
value[i + 1] == 'g' &&
value[i + 2] == 't' &&
value[i + 3] == ';' {
buf.write_char('>')
i = i + 4
continue
}
if i + 4 < len &&
value[i + 1] == 'a' &&
value[i + 2] == 'm' &&
value[i + 3] == 'p' &&
value[i + 4] == ';' {
buf.write_char('&')
i = i + 5
continue
}
if i + 5 < len &&
value[i + 1] == 'q' &&
value[i + 2] == 'u' &&
value[i + 3] == 'o' &&
value[i + 4] == 't' &&
value[i + 5] == ';' {
buf.write_char('"')
i = i + 6
continue
}
if i + 5 < len &&
value[i + 1] == 'a' &&
value[i + 2] == 'p' &&
value[i + 3] == 'o' &&
value[i + 4] == 's' &&
value[i + 5] == ';' {
buf.write_char('\'')
i = i + 6
continue
}
}
buf.write_char(c)
i = i + 1
}
buf.to_string()
}
///|
fn hex_value_local(c : Char) -> Int? {
if c >= '0' && c <= '9' {
Some(c.to_int() - '0'.to_int())
} else if c >= 'a' && c <= 'f' {
Some(10 + (c.to_int() - 'a'.to_int()))
} else if c >= 'A' && c <= 'F' {
Some(10 + (c.to_int() - 'A'.to_int()))
} else {
None
}
}
///|
fn decode_percent_local(value : String) -> String {
let len = value.length()
let buf = StringBuilder::new()
let mut i = 0
while i < len {
let c = Int::unsafe_to_char(value[i].to_int())
if c == '%' && i + 2 < len {
let c1 = Int::unsafe_to_char(value[i + 1].to_int())
let c2 = Int::unsafe_to_char(value[i + 2].to_int())
match (hex_value_local(c1), hex_value_local(c2)) {
(Some(h1), Some(h2)) => {
let v = h1 * 16 + h2
buf.write_char(Int::unsafe_to_char(v))
i = i + 3
continue
}
_ => ()
}
}
buf.write_char(c)
i = i + 1
}
buf.to_string()
}
///|
fn base64_value_local(c : Char) -> Int? {
if c >= 'A' && c <= 'Z' {
Some(c.to_int() - 'A'.to_int())
} else if c >= 'a' && c <= 'z' {
Some(26 + (c.to_int() - 'a'.to_int()))
} else if c >= '0' && c <= '9' {
Some(52 + (c.to_int() - '0'.to_int()))
} else if c == '+' || c == '-' {
Some(62)
} else if c == '/' || c == '_' {
Some(63)
} else {
None
}
}
///|
fn is_base64_ws_local(c : Char) -> Bool {
c == ' ' || c == '\n' || c == '\r' || c == '\t'
}
///|
fn decode_base64_local(value : String) -> String? {
let len = value.length()
let buf = StringBuilder::new()
let mut q0 = 0
let mut q1 = 0
let mut q2 = 0
let mut q3 = 0
let mut qlen = 0
let mut pad = 0
let mut finished = false
let mut i = 0
while i < len {
let c = Int::unsafe_to_char(value[i].to_int())
if is_base64_ws_local(c) {
i = i + 1
continue
}
if finished {
return None
}
let v = if c == '=' {
pad = pad + 1
-1
} else {
match base64_value_local(c) {
Some(v) => v
None => return None
}
}
if qlen == 0 {
q0 = v
} else if qlen == 1 {
q1 = v
} else if qlen == 2 {
q2 = v
} else {
q3 = v
}
qlen = qlen + 1
if qlen == 4 {
if q0 < 0 || q1 < 0 {
return None
}
if pad == 0 {
if q2 < 0 || q3 < 0 {
return None
}
let b0 = (q0 << 2) | (q1 >> 4)
let b1 = ((q1 & 15) << 4) | (q2 >> 2)
let b2 = ((q2 & 3) << 6) | q3
buf.write_char(Int::unsafe_to_char(b0))
buf.write_char(Int::unsafe_to_char(b1))
buf.write_char(Int::unsafe_to_char(b2))
} else if pad == 1 {
if q2 < 0 || q3 >= 0 {
return None
}
let b0 = (q0 << 2) | (q1 >> 4)
let b1 = ((q1 & 15) << 4) | (q2 >> 2)
buf.write_char(Int::unsafe_to_char(b0))
buf.write_char(Int::unsafe_to_char(b1))
finished = true
} else if pad == 2 {
if q2 >= 0 || q3 >= 0 {
return None
}
let b0 = (q0 << 2) | (q1 >> 4)
buf.write_char(Int::unsafe_to_char(b0))
finished = true
} else {
return None
}
qlen = 0
pad = 0
}
i = i + 1
}
if qlen != 0 {
return None
}
Some(buf.to_string())
}
///|
fn decode_data_svg(href : String) -> String? {
let v = trim_string(href)
if !string_starts_with(v, "data:image/svg+xml") {
return None
}
let comma = find_first(v, ',')
if comma < 0 {
return None
}
let meta = build_substring(v, 0, comma)
let data = build_substring(v, comma + 1, v.length())
let meta_lower = meta.to_lower()
let mut has_base64 = false
let mut start = 0
for i = 0; i <= meta_lower.length(); i = i + 1 {
let is_sep = i == meta_lower.length() ||
Int::unsafe_to_char(meta_lower[i].to_int()) == ';'
if is_sep {
let token = trim_string(build_substring(meta_lower, start, i))
if token == "base64" {
has_base64 = true
}
start = i + 1
}
}
if has_base64 {
let cleaned = decode_percent_local(data)
return decode_base64_local(cleaned)
}
let decoded = decode_html_entities(data)
decoded |> decode_percent_local |> Some
}
///|
fn split_href_fragment(href : String) -> (String, String?) {
let idx = find_first(href, '#')
if idx < 0 {
(href, None)
} else {
let base = build_substring(href, 0, idx)
let frag = build_substring(href, idx + 1, href.length())
(base, frag |> decode_percent_local |> Some)
}
}
///|
fn find_node_by_id(node : SVGNode, id : String) -> SVGNode? {
if node.id == id {
return Some(node)
}
for child in node.children {
if find_node_by_id(child, id) is Some(found) {
return Some(found)
}
}
None
}
///|
fn parse_image(attrs : Array[(String, String)]) -> SVGNode? {
let href = match get_attr(attrs, "href") {
Some(v) => Some(v)
None => get_attr(attrs, "xlink:href")
}
let href = match href {
Some(v) => v
None => return None
}
let x = match get_attr(attrs, "x") {
Some(v) => parse_length(v)
None => 0.0
}
let y = match get_attr(attrs, "y") {
Some(v) => parse_length(v)
None => 0.0
}
let mut width = match get_attr(attrs, "width") {
Some(v) => parse_length(v)
None => 0.0
}
let mut height = match get_attr(attrs, "height") {
Some(v) => parse_length(v)
None => 0.0
}
if height <= 0.0 && width > 0.0 {
height = width
}
if width <= 0.0 && height > 0.0 {
width = height
}
let svg_data = match decode_data_svg(href) {
Some(data) => data
None => ""
}
let node = SVGNode::new(Image(x~, y~, width~, height~, href=svg_data))
apply_attributes(node, attrs)
match node.shape {
Image(..) as img =>
node.shape = Image(
x=img.x,
y=img.y,
width=img.width,
height=img.height,
href=svg_data,
)
_ => ()
}
Some(node)
}
///|
fn parse_clip_path_from_attrs(
attrs : Array[(String, String)],
ctx : SVGParseContext,
) -> Unit {
let id = match get_attr(attrs, "id") {
Some(v) => v
None => ""
}
if id.length() == 0 {
return
}
let clip_rule = match get_attr(attrs, "clip-rule") {
Some(v) => parse_fill_rule(v)
None => NonZero
}
let transform = match get_attr(attrs, "transform") {
Some(v) => parse_transform(v)
None => Transform::identity()
}
let units = match get_attr(attrs, "clipPathUnits") {
Some(v) => parse_clip_path_units(v)
None => ClipPathUnits::UserSpaceOnUse
}
let clip = ClipPath::{ id, shape: Group, transform, clip_rule, units }
ctx.clips.add(clip)
}
///|
fn parse_clip_path(
parser : SVGParser,
attrs : Array[(String, String)],
ctx : SVGParseContext,
tag_name : String,
) -> Unit {
let temp = SVGNode::new(Group)
parse_children(parser, temp, tag_name, ctx)
let id = match get_attr(attrs, "id") {
Some(v) => v
None => ""
}
if id.length() == 0 {
return
}
let clip_rule = match get_attr(attrs, "clip-rule") {
Some(v) => parse_fill_rule(v)
None => NonZero
}
let transform = match get_attr(attrs, "transform") {
Some(v) => parse_transform(v)
None => Transform::identity()
}
let units = match get_attr(attrs, "clipPathUnits") {
Some(v) => parse_clip_path_units(v)
None => ClipPathUnits::UserSpaceOnUse
}
let mut shape : Shape? = None
let mut child_transform = Transform::identity()
for child in temp.children {
match child.shape {
Group => ()
_ => {
shape = Some(child.shape)
child_transform = child.transform
break
}
}
}
match shape {
Some(s) => {
let combined = transform.multiply(child_transform)
let clip = ClipPath::{
id,
shape: s,
transform: combined,
clip_rule,
units,
}
ctx.clips.add(clip)
}
None => ()
}
}
///|
fn parse_mask_from_attrs(
attrs : Array[(String, String)],
ctx : SVGParseContext,
) -> Unit {
parse_mask_with_content(attrs, [], ctx)
}
///|
fn parse_mask(
parser : SVGParser,
attrs : Array[(String, String)],
ctx : SVGParseContext,
tag_name : String,
) -> Unit {
let temp = SVGNode::new(Group)
parse_children(parser, temp, tag_name, ctx)
parse_mask_with_content(attrs, temp.children, ctx)
}
///|
fn parse_mask_with_content(
attrs : Array[(String, String)],
content : Array[SVGNode],
ctx : SVGParseContext,
) -> Unit {
let id = match get_attr(attrs, "id") {
Some(v) => v
None => ""
}
if id.length() == 0 {
return
}
let mask_units = match get_attr(attrs, "maskUnits") {
Some(v) => parse_mask_units(v)
None => MaskUnits::ObjectBoundingBox
}
let mask_content_units = match get_attr(attrs, "maskContentUnits") {
Some(v) => parse_mask_units(v)
None => MaskUnits::UserSpaceOnUse
}
let mask_type = match get_attr(attrs, "mask-type") {
Some(v) => parse_mask_type(v)
None => Luminance
}
let mut has_bounds = false
let mut x = 0.0
let mut y = 0.0
let mut width = 0.0
let mut height = 0.0
let mut x_is_percent = false
let mut y_is_percent = false
let mut width_is_percent = false
let mut height_is_percent = false
if get_attr(attrs, "x") is Some(v) {
let (val, is_percent) = parse_mask_length(v, mask_units)
x = val
x_is_percent = is_percent
has_bounds = true
}
if get_attr(attrs, "y") is Some(v) {
let (val, is_percent) = parse_mask_length(v, mask_units)
y = val
y_is_percent = is_percent
has_bounds = true
}
if get_attr(attrs, "width") is Some(v) {
let (val, is_percent) = parse_mask_length(v, mask_units)
width = val
width_is_percent = is_percent
has_bounds = true
}
if get_attr(attrs, "height") is Some(v) {
let (val, is_percent) = parse_mask_length(v, mask_units)
height = val
height_is_percent = is_percent
has_bounds = true
}
let base = if has_bounds {
Mask::with_bounds(id, content, x, y, width, height)
} else {
Mask::new(id, content)
}
let mask = if has_bounds {
{
..base,
x_is_percent,
y_is_percent,
width_is_percent,
height_is_percent,
mask_units,
mask_content_units,
mask_type,
}
} else {
{ ..base, mask_units, mask_content_units, mask_type }
}
ctx.masks.add(mask)
}
///|
fn is_paint_server_context(parent_tag : String) -> Bool {
parent_tag == "svg" || parent_tag == "defs" || parent_tag == "symbol"
}
///|
fn parse_pattern_from_attrs(
attrs : Array[(String, String)],
ctx : SVGParseContext,
parent_tag : String,
) -> Unit {
if !is_paint_server_context(parent_tag) {
return
}
parse_pattern_with_content(attrs, [], ctx)
}
///|
fn parse_pattern(
parser : SVGParser,
attrs : Array[(String, String)],
ctx : SVGParseContext,
tag_name : String,
parent_tag : String,
) -> Unit {
if !is_paint_server_context(parent_tag) {
skip_to_end_tag(parser, tag_name)
return
}
let temp = SVGNode::new(Group)
parse_children(parser, temp, tag_name, ctx)
parse_pattern_with_content(attrs, temp.children, ctx)
}
///|
fn parse_marker_from_attrs(
attrs : Array[(String, String)],
ctx : SVGParseContext,
_parent_tag : String,
) -> Unit {
let node = SVGNode::new(Group)
apply_attributes(node, attrs)
apply_inherited_style(node, ctx.current_style())
parse_marker_with_content(attrs, node, ctx)
}
///|
fn parse_marker(
parser : SVGParser,
attrs : Array[(String, String)],
ctx : SVGParseContext,
tag_name : String,
_parent_tag : String,
) -> Unit {
let temp = SVGNode::new(Group)
apply_attributes(temp, attrs)
apply_inherited_style(temp, ctx.current_style())
parse_children(parser, temp, tag_name, ctx)
parse_marker_with_content(attrs, temp, ctx)
}
///|
fn parse_marker_with_content(
attrs : Array[(String, String)],
content : SVGNode,
ctx : SVGParseContext,
) -> Unit {
let id = match get_attr(attrs, "id") {
Some(v) => v
None => ""
}
if id.length() == 0 {
return
}
let mut ref_x = 0.0
let mut ref_y = 0.0
let mut marker_width = 3.0
let mut marker_height = 3.0
let mut marker_units = MarkerUnits::StrokeWidth
let mut orient = MarkerOrient::Angle(0.0)
let mut view_box : ViewBox? = None
let mut preserve_aspect_ratio = PreserveAspectRatio::default()
let mut overflow_visible = false
if get_attr(attrs, "refX") is Some(v) {
ref_x = parse_length(v)
}
if get_attr(attrs, "refY") is Some(v) {
ref_y = parse_length(v)
}
if get_attr(attrs, "markerWidth") is Some(v) {
marker_width = parse_length(v)
}
if get_attr(attrs, "markerHeight") is Some(v) {
marker_height = parse_length(v)
}
if get_attr(attrs, "markerUnits") is Some(v) {
marker_units = parse_marker_units(v)
}
if get_attr(attrs, "orient") is Some(v) {
orient = parse_marker_orient(v)
}
if get_attr(attrs, "viewBox") is Some(v) {
view_box = parse_view_box(v)
}
if get_attr(attrs, "preserveAspectRatio") is Some(v) {
preserve_aspect_ratio = parse_preserve_aspect_ratio(v)
}
if get_attr(attrs, "overflow") is Some(v) {
overflow_visible = trim_string(v) == "visible"
}
content.viewport_width = Some(marker_width)
content.viewport_height = Some(marker_height)
// Marker viewBox is handled by marker transform, not by content node.
content.view_box = None
content.preserve_aspect_ratio = PreserveAspectRatio::default()
content.preserve_aspect_ratio_is_set = false
// Marker viewport clipping is handled at render time.
content.clip_overflow = false
let clip_overflow = !overflow_visible
let marker = {
..Marker::new(id, content),
ref_x,
ref_y,
marker_width,
marker_height,
marker_units,
orient,
view_box,
preserve_aspect_ratio,
clip_overflow,
}
ctx.markers.add(marker)
}
///|
fn parse_marker_units(value : String) -> MarkerUnits {
match trim_string(value) {
"userSpaceOnUse" => MarkerUnits::UserSpaceOnUse_
"strokeWidth" => MarkerUnits::StrokeWidth
_ => MarkerUnits::StrokeWidth
}
}
///|
fn parse_marker_orient(value : String) -> MarkerOrient {
let v = trim_string(value)
if v == "auto" {
MarkerOrient::Auto
} else if v == "auto-start-reverse" {
MarkerOrient::AutoStartReverse
} else {
let angle = if string_ends_with(v, "deg") {
parse_number(build_substring(v, 0, v.length() - 3))
} else {
parse_number(v)
}
MarkerOrient::Angle(angle)
}
}
///|
fn parse_pattern_with_content(
attrs : Array[(String, String)],
content : Array[SVGNode],
ctx : SVGParseContext,
) -> Unit {
let id = match get_attr(attrs, "id") {
Some(v) => v
None => ""
}
if id.length() == 0 {
return
}
let mut width = 0.0
let mut height = 0.0
if get_attr(attrs, "width") is Some(v) {
let (val, _) = parse_length_or_percent(v)
width = val
}
if get_attr(attrs, "height") is Some(v) {
let (val, _) = parse_length_or_percent(v)
height = val
}
let pattern_units = match get_attr(attrs, "patternUnits") {
Some(v) => parse_pattern_units(v)
None => ObjectBoundingBox
}
let pattern_content_units = match get_attr(attrs, "patternContentUnits") {
Some(v) => parse_pattern_units(v)
None => UserSpaceOnUse
}
let transform = match get_attr(attrs, "patternTransform") {
Some(v) => parse_transform(v)
None => Transform::identity()
}
let view_box = match get_attr(attrs, "viewBox") {
Some(v) => parse_view_box(v)
None => None
}
let pattern = {
..Pattern::new(id, width, height, content),
pattern_units,
pattern_content_units,
transform,
view_box,
}
ctx.patterns.add(pattern)
}
///|
fn parse_pattern_units(value : String) -> PatternUnits {
match trim_string(value) {
"userSpaceOnUse" => UserSpaceOnUse
"objectBoundingBox" => ObjectBoundingBox
_ => ObjectBoundingBox
}
}
///|
fn parse_gradient_units(value : String) -> GradientUnits {
match trim_string(value) {
"userSpaceOnUse" => UserSpaceOnUse
"objectBoundingBox" => ObjectBoundingBox
_ => ObjectBoundingBox
}
}
///|
fn parse_linear_gradient_from_attrs(
attrs : Array[(String, String)],
ctx : SVGParseContext,
parent_tag : String,
) -> Unit {
if !is_paint_server_context(parent_tag) {
return
}
parse_linear_gradient_with_stops(attrs, [], ctx)
}
///|
fn parse_linear_gradient(
parser : SVGParser,
attrs : Array[(String, String)],
ctx : SVGParseContext,
tag_name : String,
parent_tag : String,
) -> Unit {
if !is_paint_server_context(parent_tag) {
skip_to_end_tag(parser, tag_name)
return
}
let stops = parse_gradient_stops(parser, tag_name)
parse_linear_gradient_with_stops(attrs, stops, ctx)
}
///|
fn parse_linear_gradient_with_stops(
attrs : Array[(String, String)],
stops : Array[GradientStop],
ctx : SVGParseContext,
) -> Unit {
let id = match get_attr(attrs, "id") {
Some(v) => v
None => ""
}
if id.length() == 0 {
return
}
let x1 = parse_gradient_coord(get_attr(attrs, "x1"), 0.0)
let y1 = parse_gradient_coord(get_attr(attrs, "y1"), 0.0)
let x2 = parse_gradient_coord(get_attr(attrs, "x2"), 1.0)
let y2 = parse_gradient_coord(get_attr(attrs, "y2"), 0.0)
let spread_method = match get_attr(attrs, "spreadMethod") {
Some(v) => parse_spread_method(v)
None => Pad
}
let units = match get_attr(attrs, "gradientUnits") {
Some(v) => parse_gradient_units(v)
None => ObjectBoundingBox
}
let transform = match get_attr(attrs, "gradientTransform") {
Some(v) => parse_transform(v)
None => Transform::identity()
}
let grad = {
..LinearGradient::new(x1, y1, x2, y2, stops),
spread_method,
units,
transform,
}
ctx.gradients.add(id, Gradient::Linear(grad))
}
///|
fn parse_radial_gradient_from_attrs(
attrs : Array[(String, String)],
ctx : SVGParseContext,
parent_tag : String,
) -> Unit {
if !is_paint_server_context(parent_tag) {
return
}
parse_radial_gradient_with_stops(attrs, [], ctx)
}
///|
fn parse_radial_gradient(
parser : SVGParser,
attrs : Array[(String, String)],
ctx : SVGParseContext,
tag_name : String,
parent_tag : String,
) -> Unit {
if !is_paint_server_context(parent_tag) {
skip_to_end_tag(parser, tag_name)
return
}
let stops = parse_gradient_stops(parser, tag_name)
parse_radial_gradient_with_stops(attrs, stops, ctx)
}
///|
fn parse_radial_gradient_with_stops(
attrs : Array[(String, String)],
stops : Array[GradientStop],
ctx : SVGParseContext,
) -> Unit {
let id = match get_attr(attrs, "id") {
Some(v) => v
None => ""
}
if id.length() == 0 {
return
}
let cx = parse_gradient_coord(get_attr(attrs, "cx"), 0.5)
let cy = parse_gradient_coord(get_attr(attrs, "cy"), 0.5)
let r = parse_gradient_coord(get_attr(attrs, "r"), 0.5)
let fx = match get_attr(attrs, "fx") {
Some(v) => parse_gradient_coord(Some(v), cx)
None => cx
}
let fy = match get_attr(attrs, "fy") {
Some(v) => parse_gradient_coord(Some(v), cy)
None => cy
}
let spread_method = match get_attr(attrs, "spreadMethod") {
Some(v) => parse_spread_method(v)
None => Pad
}
let units = match get_attr(attrs, "gradientUnits") {
Some(v) => parse_gradient_units(v)
None => ObjectBoundingBox
}
let transform = match get_attr(attrs, "gradientTransform") {
Some(v) => parse_transform(v)
None => Transform::identity()
}
let grad = {
..RadialGradient::new(cx, cy, r, stops),
fx,
fy,
spread_method,
units,
transform,
}
ctx.gradients.add(id, Gradient::Radial(grad))
}
///|
fn parse_gradient_coord(value : String?, default : Double) -> Double {
match value {
Some(v) => {
let (val, _) = parse_length_or_percent(v)
val
}
None => default
}
}
///|
fn parse_spread_method(value : String) -> SpreadMethod {
match trim_string(value) {
"repeat" => Repeat
"reflect" => Reflect
_ => Pad
}
}
///|
fn parse_gradient_stops(
parser : SVGParser,
parent_tag : String,
) -> Array[GradientStop] {
let stops : Array[GradientStop] = []
while !parser.is_end() {
parser.skip_whitespace()
if parser.peek() != '<' {
// Skip text content
while !parser.is_end() && parser.peek() != '<' {
let _ = parser.advance()
}
continue
}
let _ = parser.advance()
// End tag
if parser.peek() == '/' {
let _ = parser.advance()
let end_tag = parser.read_name()
if end_tag == parent_tag {
parser.skip_whitespace()
let _ = parser.expect_char('>')
break
}
while !parser.is_end() && parser.advance() != '>' {
}
continue
}
if parser.peek() == '!' || parser.peek() == '?' {
skip_comment_or_decl(parser)
continue
}
let tag_name = parser.read_name()
if tag_name.length() == 0 {
continue
}
let attrs = parse_attributes(parser)
parser.skip_whitespace()
let mut self_closing = false
if parser.peek() == '/' {
let _ = parser.advance()
let _ = parser.expect_char('>')
self_closing = true
} else {
let _ = parser.expect_char('>')
}
if tag_name == "stop" {
stops.push(parse_gradient_stop(attrs))
}
if !self_closing {
skip_to_end_tag(parser, tag_name)
}
}
stops
}
///|
fn parse_gradient_stop(attrs : Array[(String, String)]) -> GradientStop {
let offset = match get_attr(attrs, "offset") {
Some(v) => parse_gradient_offset(v)
None => 0.0
}
let mut color = Color::black()
let mut opacity = 1.0
if get_attr(attrs, "stop-color") is Some(v) {
color = parse_color(v)
}
if get_attr(attrs, "stop-opacity") is Some(v) {
opacity = parse_number(v)
}
if get_attr(attrs, "style") is Some(style) {
let pairs = parse_style_pairs(style)
for pair in pairs {
let key = pair.0.to_lower()
if key == "stop-color" {
color = parse_color(pair.1)
} else if key == "stop-opacity" {
opacity = parse_number(pair.1)
}
}
}
if opacity < 0.0 {
opacity = 0.0
}
if opacity > 1.0 {
opacity = 1.0
}
let final_color = if opacity < 1.0 {
Color::rgba(
color.r,
color.g,
color.b,
(color.a.to_double() * opacity).to_int(),
)
} else {
color
}
{ offset, color: final_color }
}
///|
fn parse_gradient_offset(value : String) -> Double {
let v = trim_string(value)
let t = if string_ends_with(v, "%") {
let cleaned = build_substring(v, 0, v.length() - 1)
parse_number(cleaned) / 100.0
} else {
parse_number(v)
}
if t < 0.0 {
0.0
} else if t > 1.0 {
1.0
} else {
t
}
}
///|
fn parse_linecap(value : String) -> LineCap {
let v = trim_string(value)
match v {
"round" => Round
"square" => Square
_ => Butt // Default
}
}
///|
fn parse_linejoin(value : String) -> LineJoin {
let v = trim_string(value)
match v {
"round" => Round
"bevel" => Bevel
_ => Miter // Default
}
}
///|
fn parse_dasharray(value : String) -> Array[Double]? {
let v = trim_string(value)
if v == "none" || v.length() == 0 {
return None
}
let result : Array[Double] = []
let mut current = ""
for c in v {
if c == ',' || c == ' ' {
if current.length() > 0 {
result.push(parse_number(current))
current = ""
}
} else {
current = current + c.to_string()
}
}
if current.length() > 0 {
result.push(parse_number(current))
}
if result.is_empty() {
None
} else {
Some(result)
}
}
///|
fn parse_fill_rule(value : String) -> FillRule {
let v = trim_string(value)
match v {
"evenodd" => EvenOdd
_ => NonZero // Default
}
}
///|
fn trim_string(s : String) -> String {
let mut start = 0
let mut end = s.length()
while start < end {
let c = Int::unsafe_to_char(s[start].to_int())
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
start = start + 1
} else {
break
}
}
while end > start {
let c = Int::unsafe_to_char(s[end - 1].to_int())
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
end = end - 1
} else {
break
}
}
build_substring(s, start, end)
}
///|
fn string_starts_with(s : String, prefix : String) -> Bool {
if prefix.length() > s.length() {
return false
}
for i in 0.. Bool {
if suffix.length() > s.length() {
return false
}
let start = s.length() - suffix.length()
for i in 0.. Int {
for i in 0.. Double {
// Simple number parsing (strips common units)
let s = trim_string(s)
if string_starts_with(s, "var(") && string_ends_with(s, ")") {
let inner = build_substring(s, 4, s.length() - 1)
let comma = find_first(inner, ',')
if comma >= 0 {
let fallback = trim_string(
build_substring(inner, comma + 1, inner.length()),
)
if fallback.length() > 0 {
return parse_length(fallback)
}
}
return 0.0
}
// Remove units
let cleaned = if string_ends_with(s, "px") {
build_substring(s, 0, s.length() - 2)
} else if string_ends_with(s, "em") || string_ends_with(s, "pt") {
build_substring(s, 0, s.length() - 2)
} else if string_ends_with(s, "%") {
build_substring(s, 0, s.length() - 1)
} else {
s
}
parse_number(cleaned)
}
///|
fn parse_length_or_percent(s : String) -> (Double, Bool) {
let s = trim_string(s)
if string_ends_with(s, "%") {
let cleaned = build_substring(s, 0, s.length() - 1)
(parse_number(cleaned) / 100.0, true)
} else {
(parse_length(s), false)
}
}
///|
fn parse_mask_length(value : String, units : MaskUnits) -> (Double, Bool) {
let (val, is_percent) = parse_length_or_percent(value)
match units {
MaskUnits::ObjectBoundingBox => (val, false)
MaskUnits::UserSpaceOnUse => (val, is_percent)
}
}
///|
fn parse_number(s : String) -> Double {
let s = trim_string(s)
if s.length() == 0 {
return 0.0
}
let mut result = 0.0
let mut sign = 1.0
let mut i = 0
let len = s.length()
fn char_at(str : String, idx : Int) -> Char {
Int::unsafe_to_char(str[idx].to_int())
}
// Sign
if i < len && char_at(s, i) == '-' {
sign = -1.0
i = i + 1
} else if i < len && char_at(s, i) == '+' {
i = i + 1
}
// Integer part
while i < len {
let c = char_at(s, i)
if c >= '0' && c <= '9' {
result = result * 10.0 + (c.to_int() - 48).to_double()
i = i + 1
} else {
break
}
}
// Fractional part
if i < len && char_at(s, i) == '.' {
i = i + 1
let mut frac = 0.1
while i < len {
let c = char_at(s, i)
if c >= '0' && c <= '9' {
result = result + (c.to_int() - 48).to_double() * frac
frac = frac * 0.1
i = i + 1
} else {
break
}
}
}
sign * result
}
///|
fn parse_paint(value : String) -> Paint {
let value = trim_string(value)
if value.length() == 0 {
return None
}
if value == "none" {
return None
}
if value == "currentColor" {
return CurrentColor
}
// Paint server with optional fallback: url(#id)
if string_starts_with(value, "url(") {
let close_index = find_first(value, ')')
if close_index >= 0 {
let url_part = build_substring(value, 0, close_index + 1)
let rest = trim_string(
build_substring(value, close_index + 1, value.length()),
)
match parse_url_ref(url_part) {
Some(id) => {
let fallback = if rest.length() == 0 {
PaintFallback::NoPaint
} else {
parse_paint_fallback(rest)
}
return PaintServerRef(id, fallback)
}
None => ()
}
}
}
// Parse color
let color = parse_color(value)
SolidColor(color)
}
///|
fn parse_paint_fallback(value : String) -> PaintFallback {
let value = trim_string(value)
if value.length() == 0 || value == "none" {
return PaintFallback::NoPaint
}
if value == "currentColor" {
return PaintFallback::CurrentColor
}
PaintFallback::SolidColor(parse_color(value))
}
///|
fn parse_paint_order(value : String) -> PaintOrder {
let v = trim_string(value)
if v.length() == 0 || v == "normal" {
return PaintOrder::default()
}
let tokens = split_tokens(v)
if tokens.length() == 0 {
return PaintOrder::default()
}
let order : Array[PaintOrderItem] = []
for t in tokens {
match t {
"fill" => if !order.contains(Fill) { order.push(Fill) }
"stroke" => if !order.contains(Stroke) { order.push(Stroke) }
"markers" => if !order.contains(Markers) { order.push(Markers) }
_ => ()
}
}
if order.length() == 0 {
return PaintOrder::default()
}
if !order.contains(Fill) {
order.push(Fill)
}
if !order.contains(Stroke) {
order.push(Stroke)
}
if !order.contains(Markers) {
order.push(Markers)
}
PaintOrder::{ order, }
}
///|
fn parse_color(value : String) -> Color {
let value = trim_string(value)
if value.length() == 0 {
return Color::black()
}
// Named colors
match value {
"black" => return Color::black()
"white" => return Color::white()
"red" => return Color::rgb(255, 0, 0)
"green" => return Color::rgb(0, 128, 0)
"blue" => return Color::rgb(0, 0, 255)
"yellow" => return Color::rgb(255, 255, 0)
"cyan" => return Color::rgb(0, 255, 255)
"magenta" => return Color::rgb(255, 0, 255)
"gray" | "grey" => return Color::rgb(128, 128, 128)
"orange" => return Color::rgb(255, 165, 0)
"purple" => return Color::rgb(128, 0, 128)
"pink" => return Color::rgb(255, 192, 203)
"brown" => return Color::rgb(165, 42, 42)
_ => ()
}
// Hex color
if value.length() > 0 && Int::unsafe_to_char(value[0].to_int()) == '#' {
return parse_hex_color(value)
}
// rgb(r, g, b)
if string_starts_with(value, "rgb") {
return parse_rgb_color(value)
}
Color::black()
}
///|
fn parse_hex_color(value : String) -> Color {
// #RGB or #RRGGBB
fn hex_digit(c : Char) -> Int {
if c >= '0' && c <= '9' {
c.to_int() - 48
} else if c >= 'a' && c <= 'f' {
c.to_int() - 87
} else if c >= 'A' && c <= 'F' {
c.to_int() - 55
} else {
0
}
}
fn char_at(s : String, i : Int) -> Char {
Int::unsafe_to_char(s[i].to_int())
}
if value.length() == 4 {
// #RGB
let r = hex_digit(char_at(value, 1))
let g = hex_digit(char_at(value, 2))
let b = hex_digit(char_at(value, 3))
Color::rgb(r * 17, g * 17, b * 17)
} else if value.length() == 7 {
// #RRGGBB
let r = hex_digit(char_at(value, 1)) * 16 + hex_digit(char_at(value, 2))
let g = hex_digit(char_at(value, 3)) * 16 + hex_digit(char_at(value, 4))
let b = hex_digit(char_at(value, 5)) * 16 + hex_digit(char_at(value, 6))
Color::rgb(r, g, b)
} else {
Color::black()
}
}
///|
fn parse_rgb_color(value : String) -> Color {
// rgb(r, g, b) or rgba(r, g, b, a)
// Find numbers between parentheses
let mut start = 0
while start < value.length() &&
Int::unsafe_to_char(value[start].to_int()) != '(' {
start = start + 1
}
start = start + 1
let mut end = start
while end < value.length() && Int::unsafe_to_char(value[end].to_int()) != ')' {
end = end + 1
}
if start >= end {
return Color::black()
}
// Parse comma-separated numbers
let nums : Array[Int] = []
let mut num_start = start
for i = start; i <= end; i = i + 1 {
let c = if i < value.length() {
Int::unsafe_to_char(value[i].to_int())
} else {
')'
}
if c == ',' || c == ')' {
if num_start < i {
let num_str = build_substring(value, num_start, i)
nums.push(parse_number(num_str).to_int())
}
num_start = i + 1
}
}
if nums.length() >= 3 {
Color::rgb(nums[0], nums[1], nums[2])
} else {
Color::black()
}
}
///|
fn build_substring(s : String, start : Int, end : Int) -> String {
let buf = StringBuilder::new()
for i in start.. Array[(Double, Double)] {
// Parse space/comma separated list of x,y pairs
let result : Array[(Double, Double)] = []
let nums : Array[Double] = []
let mut buf = StringBuilder::new()
for i in 0.. 0 {
nums.push(parse_number(buf.to_string()))
buf = StringBuilder::new()
}
} else {
buf.write_char(c)
}
}
if buf.to_string().length() > 0 {
nums.push(parse_number(buf.to_string()))
}
// Pair up the numbers
let mut i = 0
while i + 1 < nums.length() {
result.push((nums[i], nums[i + 1]))
i = i + 2
}
result
}
///|
pub fn parse_transform(value : String) -> Transform {
// Parse transform attribute: translate(x, y) scale(sx, sy) rotate(angle) etc.
let mut result = Transform::identity()
let mut i = 0
while i < value.length() {
// Skip whitespace
while i < value.length() {
let c = Int::unsafe_to_char(value[i].to_int())
if c == ' ' || c == ',' {
i = i + 1
} else {
break
}
}
if i >= value.length() {
break
}
// Read function name
let name_start = i
while i < value.length() {
let c = Int::unsafe_to_char(value[i].to_int())
if c >= 'a' && c <= 'z' {
i = i + 1
} else {
break
}
}
let name = build_substring(value, name_start, i)
// Find '('
while i < value.length() && Int::unsafe_to_char(value[i].to_int()) != '(' {
i = i + 1
}
i = i + 1 // Skip '('
// Find ')' and parse arguments
let args_start = i
while i < value.length() && Int::unsafe_to_char(value[i].to_int()) != ')' {
i = i + 1
}
let args_str = build_substring(value, args_start, i)
i = i + 1 // Skip ')'
// Parse arguments
let args = parse_transform_args(args_str)
// Apply transform
let t = match name {
"translate" =>
if args.length() >= 2 {
Transform::translate(args[0], args[1])
} else if args.length() >= 1 {
Transform::translate(args[0], 0.0)
} else {
Transform::identity()
}
"scale" =>
if args.length() >= 2 {
Transform::scale(args[0], args[1])
} else if args.length() >= 1 {
Transform::scale(args[0], args[0])
} else {
Transform::identity()
}
"rotate" =>
if args.length() >= 3 {
Transform::rotate_around(
degrees_to_radians(args[0]),
args[1],
args[2],
)
} else if args.length() >= 1 {
Transform::rotate(args[0] |> degrees_to_radians)
} else {
Transform::identity()
}
"skewX" =>
if args.length() >= 1 {
Transform::skew_x(args[0] |> degrees_to_radians)
} else {
Transform::identity()
}
"skewY" =>
if args.length() >= 1 {
Transform::skew_y(args[0] |> degrees_to_radians)
} else {
Transform::identity()
}
"matrix" =>
if args.length() >= 6 {
Transform::matrix(
args[0],
args[1],
args[2],
args[3],
args[4],
args[5],
)
} else {
Transform::identity()
}
_ => Transform::identity()
}
result = result.multiply(t)
}
result
}
///|
fn parse_transform_args(s : String) -> Array[Double] {
let result : Array[Double] = []
let mut buf = StringBuilder::new()
for i in 0.. 0 {
result.push(parse_number(buf.to_string()))
buf = StringBuilder::new()
}
} else {
buf.write_char(c)
}
}
if buf.to_string().length() > 0 {
result.push(parse_number(buf.to_string()))
}
result
}