///|
/// XML-escape text content so interpolated values cannot break out of the
/// envelope or inject attributes. Single pass; only the five significant
/// characters are rewritten.
fn escape_xml(s : String) -> String {
  let sb = StringBuilder()
  for ch in s {
    match ch {
      '&' => sb.write_string("&")
      '<' => sb.write_string("<")
      '>' => sb.write_string(">")
      '"' => sb.write_string(""")
      '\'' => sb.write_string("'")
      _ => sb.write_char(ch)
    }
  }
  sb.to_string()
}

///|
/// Render one context envelope element:
/// `<{ns}-context type="{kind}" trust="{trust}"[ source="{source}"][ {name}="{value}"…]…`.
/// `ns` is the composing host's bare namespace (e.g. cetas hosts pass
/// `"cetas"` and get ``); it, `kind`, and custom attribute
/// names are trusted host configuration and are not escaped. `source`
/// (rendered only when passed), custom attribute values, and `content`
/// are escaped because they may carry external or dynamic text. Custom
/// attributes render after the standard ones, in the given order.
#alias(render_context_envelope, deprecated="renamed to context_envelope")
pub fn context_envelope(
  ns~ : String,
  kind~ : String,
  trust~ : Bool,
  source? : String,
  attrs? : Array[(String, String)] = [],
  content~ : String,
) -> String {
  let trust_str = if trust { "true" } else { "false" }
  let sb : StringBuilder = StringBuilder()
  sb <+ "<\{ns}-context type=\"\{kind}\" trust=\"\{trust_str}\""
  if source is Some(s) {
    sb <+ " source=\"\{escape_xml(s)}\""
  }
  for pair in attrs {
    let (name, value) = pair
    sb <+ " \{name}=\"\{escape_xml(value)}\""
  }
  sb <+ ">\n\{escape_xml(content)}\n"
  sb.to_string()
}