use std::fmt::Write;
use base64::{prelude::BASE64_STANDARD, Engine};
use disposition_ir_model::entity::EntityTailwindClasses;
use disposition_svg_model::{SvgEdgeInfo, SvgElements, SvgNodeInfo};
use disposition_taffy_model::{TEXT_FONT_SIZE, TEXT_LINE_HEIGHT};
use crate::NOTO_SANS_MONO_TTF;
const ENCRE_CSS_VARIABLES: &str = "svg {
--en-border-spacing-x: 0;
--en-border-spacing-y: 0;
--en-translate-x: 0;
--en-translate-y: 0;
--en-translate-z: 0;
--en-rotate-x: 0;
--en-rotate-y: 0;
--en-rotate-z: 0;
--en-skew-x: 0;
--en-skew-y: 0;
--en-scale-x: 1;
--en-scale-y: 1;
--en-scale-z: 1;
--en-pan-x: ;
--en-pan-y: ;
--en-pinch-zoom: ;
--en-scroll-snap-strictness: proximity;
--en-ordinal: ;
--en-slashed-zero: ;
--en-numeric-figure: ;
--en-numeric-spacing: ;
--en-numeric-fraction: ;
--en-ring-inset: ;
--en-ring-offset-width: 0px;
--en-ring-offset-color: #fff;
--en-ring-color: currentColor;
--en-ring-offset-shadow: 0 0 #0000;
--en-ring-shadow: 0 0 #0000;
--en-shadow: 0 0 #0000;
--en-shadow-colored: 0 0 #0000;
--en-blur: ;
--en-brightness: ;
--en-contrast: ;
--en-grayscale: ;
--en-hue-rotate: ;
--en-invert: ;
--en-saturate: ;
--en-sepia: ;
--en-drop-shadow: ;
--en-backdrop-blur: ;
--en-backdrop-brightness: ;
--en-backdrop-contrast: ;
--en-backdrop-grayscale: ;
--en-backdrop-hue-rotate: ;
--en-backdrop-invert: ;
--en-backdrop-opacity: ;
--en-backdrop-saturate: ;
--en-backdrop-sepia: ;
}";
#[derive(Clone, Copy, Debug)]
pub struct SvgElementsToSvgMapper;
impl SvgElementsToSvgMapper {
pub fn map(svg_elements: &SvgElements) -> String {
let SvgElements {
svg_width,
svg_height,
svg_node_infos,
svg_edge_infos,
svg_process_infos: _,
tailwind_classes,
css,
} = svg_elements;
let mut content_buffer = String::with_capacity(4096);
let mut styles_buffer = String::with_capacity(2048);
writeln!(
&mut styles_buffer,
"text {{ \
font-family: 'Noto Sans Mono', \
ui-monospace, \
SFMono-Regular, \
Menlo, \
Monaco, \
Consolas, \
'Liberation Mono', \
monospace; \
font-size: {TEXT_FONT_SIZE}px; \
line-height: {TEXT_LINE_HEIGHT}px; \
}}"
)
.unwrap();
writeln!(
&mut styles_buffer,
"@font-face {{ \
font-family: 'Noto Sans Mono'; \
src: url(data:application/x-font-ttf;base64,{}) format('truetype'); \
}}",
BASE64_STANDARD.encode(NOTO_SANS_MONO_TTF)
)
.unwrap();
Self::render_nodes(&mut content_buffer, svg_node_infos, tailwind_classes);
Self::render_edges(&mut content_buffer, svg_edge_infos, tailwind_classes);
let escaped_classes: Vec<String> = tailwind_classes
.values()
.map(|classes| Self::escape_ids_in_brackets(classes))
.collect();
let tailwind_classes_iter = escaped_classes.iter().map(String::as_str).chain(
svg_node_infos
.iter()
.flat_map(|svg_node_info| svg_node_info.wrapper_tailwind_classes.iter())
.map(|wrapper_tailwind_classes| wrapper_tailwind_classes.as_ref()),
);
let encre_css_config = {
let mut encre_css_config = encre_css::Config::default();
encre_css_config.preflight = encre_css::Preflight::new_custom(ENCRE_CSS_VARIABLES);
encre_css_config
};
let generated_css =
encre_css::generate(tailwind_classes_iter, &encre_css_config).replace("&", "&");
let mut style_content =
String::with_capacity(generated_css.len() + styles_buffer.len() + css.len());
style_content.push_str(&generated_css);
if !styles_buffer.is_empty() {
if !style_content.is_empty() {
style_content.push('\n');
}
style_content.push_str(&styles_buffer);
}
if !css.is_empty() {
if !style_content.is_empty() {
style_content.push('\n');
}
style_content.push_str(css.as_str());
}
let mut buffer = String::with_capacity(128 + style_content.len() + content_buffer.len());
write!(
buffer,
"<svg \
xmlns=\"http://www.w3.org/2000/svg\" \
width=\"{svg_width}\" \
height=\"{svg_height}\" \
class=\"group\"\
>"
)
.unwrap();
if !style_content.is_empty() {
write!(buffer, "<style>{style_content}</style>").unwrap();
}
buffer.push_str(&content_buffer);
buffer.push_str("</svg>");
buffer
}
fn render_nodes(
content_buffer: &mut String,
svg_node_infos: &[SvgNodeInfo<'_>],
tailwind_classes: &EntityTailwindClasses<'_>,
) {
svg_node_infos.iter().for_each(|svg_node_info| {
let node_id = &svg_node_info.node_id;
let tab_index = svg_node_info.tab_index;
let path_d = &svg_node_info.path_d_collapsed;
let class_attr = {
let tailwind_classes = tailwind_classes
.get(node_id.as_ref())
.cloned()
.unwrap_or_default();
Self::class_attr_escaped(tailwind_classes)
};
write!(
content_buffer,
r#"<g id="{node_id}"{class_attr} tabindex="{tab_index}">"#
)
.unwrap();
write!(content_buffer, r#"<path d="{path_d}" class="wrapper"#).unwrap();
if let Some(wrapper_tw) = svg_node_info.wrapper_tailwind_classes.as_ref() {
write!(content_buffer, " {wrapper_tw}").unwrap();
}
write!(content_buffer, r#"" />"#).unwrap();
if let Some(ref circle) = svg_node_info.circle {
let circle_path_d = &circle.path_d;
write!(content_buffer, r#"<path d="{circle_path_d}" />"#).unwrap();
}
svg_node_info.text_spans.iter().for_each(|span| {
let text_x = span.x;
let text_y = span.y;
let text_content = &span.text;
write!(
content_buffer,
"<text \
x=\"{text_x}\" \
y=\"{text_y}\" \
stroke-width=\"0\" \
>{text_content}</text>"
)
.unwrap();
});
content_buffer.push_str("</g>");
});
}
fn render_edges(
content_buffer: &mut String,
svg_edge_infos: &[SvgEdgeInfo<'_>],
tailwind_classes: &EntityTailwindClasses<'_>,
) {
svg_edge_infos.iter().for_each(|svg_edge_info| {
let edge_id = &svg_edge_info.edge_id;
let edge_group_id = &svg_edge_info.edge_group_id;
let path_d = &svg_edge_info.path_d;
let arrow_head_path_d = &svg_edge_info.arrow_head_path_d;
let class_attr = {
let edge_classes = tailwind_classes
.get(edge_id.as_ref())
.map(|s| s.as_str())
.unwrap_or("");
let edge_group_classes = tailwind_classes
.get(edge_group_id.as_ref())
.map(|s| s.as_str())
.unwrap_or("");
let combined = if edge_classes.is_empty() {
edge_group_classes.to_string()
} else if edge_group_classes.is_empty() {
edge_classes.to_string()
} else {
format!("{edge_group_classes}\n{edge_classes}")
};
Self::class_attr_escaped(combined)
};
let arrow_head_entity_key = format!("{edge_id}_arrow_head");
let arrow_head_class_attr = if let Ok(arrow_head_id) =
disposition_model_common::Id::try_from(arrow_head_entity_key)
{
let extra = tailwind_classes
.get(&arrow_head_id)
.map(|s| s.as_str())
.unwrap_or("");
if extra.is_empty() {
Self::class_attr_escaped("arrow_head".to_string())
} else {
Self::class_attr_escaped(format!("arrow_head\n{extra}"))
}
} else {
Self::class_attr_escaped("arrow_head".to_string())
};
write!(
content_buffer,
"<g \
id=\"{edge_id}\"\
{class_attr}\
>\
<path \
d=\"{path_d}\" \
fill=\"none\" \
/>\
<g \
{arrow_head_class_attr} \
>\
<path \
d=\"{arrow_head_path_d}\" \
/>\
</g>
</g>"
)
.unwrap();
});
}
fn class_attr_escaped(tailwind_classes: String) -> String {
if tailwind_classes.is_empty() {
String::new()
} else {
let ampersand_count = tailwind_classes.matches('&').count();
let mut classes_str =
String::with_capacity(tailwind_classes.len() + ampersand_count * 5 + 10);
classes_str.push_str(r#" class=""#);
tailwind_classes.chars().for_each(|c| {
if c == '&' {
classes_str.push_str("&");
} else {
classes_str.push(c);
}
});
classes_str.push('"');
classes_str
}
}
pub fn escape_ids_in_brackets(classes: &str) -> String {
let mut bracket_depth: u32 = 0;
let mut is_parsing_id = false;
classes
.chars()
.fold(String::with_capacity(classes.len()), |mut result, c| {
match c {
'[' => {
bracket_depth += 1;
is_parsing_id = false;
result.push(c);
}
']' => {
bracket_depth = bracket_depth.saturating_sub(1);
is_parsing_id = false;
result.push(c);
}
'#' if bracket_depth > 0 => {
is_parsing_id = true;
result.push(c);
}
'"' if bracket_depth > 0 => {
result.push_str(""");
}
'\'' if bracket_depth > 0 => {
result.push_str("'");
}
'(' if bracket_depth > 0 => {
result.push_str("(");
}
')' if bracket_depth > 0 => {
result.push_str(")");
}
'_' if bracket_depth > 0 && is_parsing_id => {
result.push_str("_");
}
':' | ' ' | ',' | '.' | '>' | '+' | '~' | '(' | ')' if is_parsing_id => {
is_parsing_id = false;
result.push(c);
}
_ => {
result.push(c);
}
}
result
})
}
}