pub(crate) mod markers; mod primitives;
mod rules;
mod style_block;
mod values;
mod wires;
use crate::Options;
use crate::layout::{LaidOut, PlacedNode};
use crate::resolve::{ShapeKind, VarTable};
use rules::RuleSet;
use values::{escape_xml, format_value, num};
pub fn render(laid_out: &LaidOut, opts: &Options) -> String {
let mut out = String::with_capacity(2048);
let vb = &laid_out.viewbox;
use std::fmt::Write;
writeln!(
out,
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}" width="{}" height="{}" class="lini">"#,
num(vb.x),
num(vb.y),
num(vb.w),
num(vb.h),
num(vb.w),
num(vb.h),
)
.unwrap();
let ruleset = rules::build(laid_out, opts);
style_block::emit(&mut out, &laid_out.vars, &ruleset, opts);
out.push_str(" <defs/>\n");
out.push_str(" <g class=\"lini-scene\">\n");
for node in &laid_out.nodes {
render_node(&mut out, node, 2, &laid_out.vars, &ruleset, opts);
}
out.push_str(" </g>\n");
if laid_out.wires.is_empty() && laid_out.airwires.is_empty() {
out.push_str(" <g class=\"lini-wires\"/>\n");
} else {
out.push_str(" <g class=\"lini-wires\">\n");
let polys: Vec<&[(f64, f64)]> = laid_out.wires.iter().map(|w| &w.path[..]).collect();
let caps: Vec<f64> = laid_out
.wires
.iter()
.map(|w| wires::radius_cap(w, &laid_out.vars))
.collect();
let targets = wires::fillet_targets(&polys, &caps);
for (wire, targets) in laid_out.wires.iter().zip(&targets) {
wires::render_wire(&mut out, wire, targets, &laid_out.vars, &ruleset, opts);
}
for air in &laid_out.airwires {
wires::render_airwire(&mut out, air, &laid_out.vars, opts);
}
out.push_str(" </g>\n");
}
out.push_str("</svg>\n");
out
}
fn render_node(
out: &mut String,
n: &PlacedNode,
depth: usize,
vars: &VarTable,
ruleset: &RuleSet,
opts: &Options,
) {
use std::fmt::Write;
let indent = " ".repeat(depth);
let class_list = values::class_list(n.shape.as_str(), &n.type_chain, &n.applied_styles);
let classes = class_list.join(" ");
let transform = if n.rotation != 0.0 {
format!(
r#" transform="translate({},{}) rotate({})""#,
num(n.cx),
num(n.cy),
num(n.rotation)
)
} else {
format!(r#" transform="translate({},{})""#, num(n.cx), num(n.cy))
};
let id_attr = match &n.id {
Some(id) => format!(r#" data-id="{}""#, escape_xml(id)),
None => String::new(),
};
let style_attr = node_style_attr(n, &class_list, ruleset, vars, opts);
writeln!(
out,
r#"{}<g class="{}"{}{}{}>"#,
indent, classes, id_attr, style_attr, transform
)
.unwrap();
primitives::render_geometry(out, n, depth + 1, vars, opts);
for child in &n.children {
render_node(out, child, depth + 1, vars, ruleset, opts);
}
writeln!(out, "{}</g>", indent).unwrap();
}
fn node_style_attr(
n: &PlacedNode,
classes: &[String],
ruleset: &RuleSet,
vars: &VarTable,
opts: &Options,
) -> String {
let mut decls: Vec<(&str, String)> = Vec::new();
for (lini, css) in rules::PAINT_PROPS {
let value = match (*lini, n.shape) {
("fill", ShapeKind::Text) => n.attrs.get("fill").or_else(|| n.attrs.get("color")),
("color", ShapeKind::Text) => None,
_ => n.attrs.get(lini),
};
let Some(v) = value else { continue };
let formatted = match *lini {
"text-size" => format!("{}px", format_value(v, vars, opts)),
_ => format_value(v, vars, opts),
};
if ruleset.provided(classes, css) != Some(formatted.as_str()) {
decls.push((css, formatted));
}
}
if n.attrs.get("line").is_some() {
let thickness = n.attrs.number("thickness").unwrap_or(1.0);
let dash = values::dasharray_value(&n.attrs, thickness);
let value = if dash.is_empty() {
"none".to_string()
} else {
dash
};
if ruleset.provided(classes, "stroke-dasharray") != Some(value.as_str()) {
decls.push(("stroke-dasharray", value));
}
}
style_attr_from(&decls)
}
fn style_attr_from(decls: &[(&str, String)]) -> String {
if decls.is_empty() {
return String::new();
}
let body: Vec<String> = decls.iter().map(|(p, v)| format!("{}: {}", p, v)).collect();
format!(r#" style="{}""#, body.join("; "))
}