///|
fn xml_text(text : String) -> Result[String, RqaError] {
  if text.length() > 512 {
    return Err(Limit("SVG title length"))
  }
  let out = StringBuilder()
  for ch in text.iter() {
    if ch.to_int() < 32 && ch != '\n' && ch != '\r' && ch != '\t' {
      return Err(Invalid("SVG XML control character"))
    }
    match ch {
      '&' => out.write_string("&")
      '<' => out.write_string("<")
      '>' => out.write_string(">")
      '"' => out.write_string(""")
      '\'' => out.write_string("'")
      _ => out.write_char(ch)
    }
  }
  Ok(out.to_string())
}

///|
/// A static SVG document with no active content, external resources or injected markup.
/// Columns are x, rows are y increasing downward. Gray means excluded, white eligible,
/// dark blue recurrent. The budget caps output primitives independently of analysis.
pub fn Recurrence::to_svg(
  self : Recurrence,
  cell_size : Int32,
  title : String,
  max_cells : Int32,
) -> Result[String, RqaError] {
  if cell_size < 1 || cell_size > 32 {
    return Err(Invalid("SVG cell size 1..32"))
  }
  if max_cells < 1 || max_cells > 65536 || self.height > max_cells / self.width {
    return Err(Limit("SVG cell budget <=65536"))
  }
  let escaped = match xml_text(title) {
    Err(e) => return Err(e)
    Ok(s) => s
  }
  let width = self.width * cell_size
  let height = self.height * cell_size
  let out = StringBuilder()
  out.write_string(
    "\n",
  )
  out.write_string("\{escaped}\n")
  out.write_string("\n")
  for i in 0..\n",
        )
      }
    }
  }
  out.write_string("\n")
  Ok(out.to_string())
}