use crate::core::{extract_control_primitives, extract_style_primitives};
use crate::render::{
CHAR_WIDTH_RATIO, FALLBACK_WIDTH, LINE_HEIGHT_RATIO, build_svg_footer, build_svg_header,
escape_char_xml, escape_xml, indent, inject_embed_font_css,
};
use crate::{CommandGroup, ControlSpec, LayoutSpec, StyleSpec};
pub(crate) fn render_repl_svg(
groups: &[CommandGroup],
style: &StyleSpec,
layout: &LayoutSpec,
control: &ControlSpec,
) -> String {
const GROUP_DELAY_MS: u32 = 750;
const COMMAND_GAP: u32 = 8;
let (frame_delay, fade_duration, _) = extract_control_primitives(control);
let (font_size, font_family, fill_color, background_color) = extract_style_primitives(style);
let line_height = (font_size as f32 * LINE_HEIGHT_RATIO).round() as u32;
let char_spacing = (font_size as f32 * CHAR_WIDTH_RATIO).round() as u32;
let padding = layout.padding.unwrap_or(0);
let total_lines: usize = groups.iter().map(|g| 1 + 1 + g.output.len()).sum();
let vh = total_lines as u32 * line_height + padding * 2;
let vw = layout
.width
.unwrap_or_else(|| compute_auto_width(groups, char_spacing, padding));
let mut out = String::new();
out.push_str(&build_svg_header(
layout,
vw,
vh,
font_family,
font_size,
fill_color,
background_color,
));
let (_, font_face) = inject_embed_font_css(style);
out.push_str(&font_face);
out.push_str(&format!(
"{indent}<text text-anchor=\"start\" dominant-baseline=\"middle\">\n",
indent = indent(1)
));
let mut y = padding;
let mut global_char_index = 0;
for group in groups {
let prompt_delay = global_char_index * frame_delay;
build_prompt_line(
&mut out,
&group.prompt,
y,
padding,
prompt_delay,
fade_duration,
);
let prompt_len = group.prompt.chars().count() as u32;
let x = padding + prompt_len * char_spacing + COMMAND_GAP;
build_command_line(
&mut out,
&group.command,
x,
y,
frame_delay,
&mut global_char_index,
char_spacing,
fade_duration,
);
y += line_height;
let output_delay = global_char_index * frame_delay;
build_output_lines(
&mut out,
&group.output,
y,
padding,
line_height,
output_delay,
fade_duration,
);
y += (group.output.len() as u32) * line_height;
global_char_index += GROUP_DELAY_MS / frame_delay.max(1);
}
out.push_str(&format!("{}</text>\n", indent(1)));
out.push_str(build_svg_footer());
out
}
fn build_prompt_line(
out: &mut String,
prompt: &str,
y: u32,
x: u32,
begin_ms: u32,
char_duration: u32,
) {
out.push_str(&format!(
"{i2}<tspan x=\"{x}\" y=\"{y}\" style=\"opacity:0\">\n",
i2 = indent(2),
x = x,
y = y,
));
out.push_str(&format!(
"{i3}<animate attributeName=\"opacity\" from=\"0\" to=\"1\" begin=\"{begin}ms\" dur=\"{duration}ms\" fill=\"freeze\" />\n",
i3 = indent(3),
begin = begin_ms,
duration = char_duration,
));
out.push_str(&format!(
"{i3}{}\n{i2}</tspan>\n",
escape_xml(prompt),
i3 = indent(3),
i2 = indent(2),
));
}
#[allow(clippy::too_many_arguments)]
fn build_command_line(
out: &mut String,
command: &str,
mut x: u32,
y: u32,
speed: u32,
global_index: &mut u32,
char_spacing: u32,
char_duration: u32,
) {
for ch in command.chars() {
let begin_ms = *global_index * speed;
out.push_str(&format!(
"{i2}<tspan x=\"{x}\" y=\"{y}\" style=\"opacity:0\">\n",
i2 = indent(2),
x = x,
y = y,
));
out.push_str(&format!(
"{i3}<animate attributeName=\"opacity\" from=\"0\" to=\"1\" begin=\"{begin}ms\" dur=\"{duration}ms\" fill=\"freeze\" />\n",
i3 = indent(3),
begin = begin_ms,
duration = char_duration,
));
out.push_str(&format!(
"{i3}{}\n{i2}</tspan>\n",
escape_char_xml(ch),
i3 = indent(3),
i2 = indent(2)
));
x += char_spacing;
*global_index += 1;
}
}
fn build_output_lines(
out: &mut String,
lines: &[String],
mut y: u32,
x: u32,
line_height: u32,
begin_ms: u32,
char_duration: u32,
) {
for line in lines {
out.push_str(&format!(
r#"{i2}<tspan x="{x}" y="{y}" style="opacity:0" xml:space="preserve">{text}
{i3}<animate attributeName="opacity" from="0" to="1" begin="{begin}ms" dur="{duration}ms" fill="freeze" />
{i2}</tspan>
"#,
i2 = indent(2),
i3 = indent(3),
x = x,
y = y,
text = escape_xml(line),
begin = begin_ms,
duration = char_duration,
));
y += line_height;
}
}
fn compute_auto_width(groups: &[CommandGroup], char_spacing: u32, padding: u32) -> u32 {
let max_len = groups
.iter()
.map(|g| {
let prompt_len = g.prompt.chars().count();
let command_len = g.command.chars().count();
let output_max = g
.output
.iter()
.map(|s| s.chars().count())
.max()
.unwrap_or(0);
prompt_len + command_len.max(output_max)
})
.max()
.unwrap_or(0);
let calculated_width = (max_len as u32 * char_spacing) + padding * 2;
calculated_width.max(FALLBACK_WIDTH)
}