// Fixed vocabulary, typed data and escaped text; arbitrary XML is not accepted.
///|
priv struct XmlOutput {
data : @buffer.Buffer
}
///|
fn XmlOutput::new() -> XmlOutput {
{ data: @buffer.Buffer(), }
}
///|
fn XmlOutput::write_string(self : XmlOutput, s : String) -> Unit raise OdsError {
let bytes = @utf8.encode(s[:])
if self.data.length() + bytes.length() > 16777216 {
raise Invalid("content XML exceeds 16 MiB")
}
self.data.write_bytes(bytes[:])
}
///|
fn XmlOutput::to_string(self : XmlOutput) -> String {
@utf8.decode_lossy(self.data.to_bytes()[:])
}
///|
fn xml_escape(s : String) -> String {
let b = StringBuilder()
for c in s {
match c {
'&' => b.write_string("&")
'<' => b.write_string("<")
'>' => b.write_string(">")
'"' => b.write_string(""")
'\'' => b.write_string("'")
'\n' => b.write_string("
")
'\r' => b.write_string("
")
'\t' => b.write_string(" ")
_ => b.write_char(c)
}
}
b.to_string()
}
///|
fn text_paragraph(s : String) -> String {
let b = StringBuilder()
b.write_string("")
let mut spaces = 0
for c in s {
if c == ' ' {
spaces += 1
continue
}
write_text_spaces(b, spaces)
spaces = 0
match c {
'\t' => b.write_string("")
'\n' => b.write_string("")
_ => b.write_string(xml_escape(c.to_string()))
}
}
write_text_spaces(b, spaces)
b.write_string("")
b.to_string()
}
///|
fn write_text_spaces(out : StringBuilder, count : Int32) -> Unit {
if count == 1 {
out.write_string("")
} else if count > 1 {
out.write_string("")
}
}
///|
fn pad(n : Int32, size : Int32) -> String {
let s = n.to_string()
let b = StringBuilder()
for _ in 0..<(size - s.length()) {
b.write_string("0")
}
b.write_string(s)
b.to_string()
}
///|
fn Cell::xml(self : Cell, extra : String) -> String {
// ODF paragraph whitespace cannot represent a literal CR. Preserve that
// typed string in office:string-value instead of emitting a misleading p.
let attribute_text = match self.value {
Text(s) => s.contains("\r")
_ => false
}
let b = StringBuilder()
b.write_string(" ""
Text(s) => {
// Ordinary strings use explicit paragraph controls without duplicated
// attributes. CR strings use the typed attribute only; no visual claim.
b.write_string(" office:value-type=\"string\"")
if attribute_text {
b.write_string(" office:string-value=\"\{xml_escape(s)}\"")
}
s
}
Number(n) => {
let s = finite_number_text(n)
b.write_string(" office:value-type=\"float\" office:value=\"\{s}\"")
s
}
Percentage(n) => {
let s = finite_number_text(n)
b.write_string(" office:value-type=\"percentage\" office:value=\"\{s}\"")
s
}
Currency(n, code) => {
let s = finite_number_text(n)
b.write_string(
" office:value-type=\"currency\" office:value=\"\{s}\" office:currency=\"\{code}\"",
)
code + " " + s
}
Duration(s) => {
b.write_string(
" office:value-type=\"time\" office:time-value=\"\{xml_escape(s)}\"",
)
s
}
DateTime(s) => {
b.write_string(
" office:value-type=\"date\" office:date-value=\"\{xml_escape(s)}\"",
)
s
}
Boolean(v) => {
let s = if v { "true" } else { "false" }
b.write_string(
" office:value-type=\"boolean\" office:boolean-value=\"\{s}\"",
)
s
}
Date(y, m, d) => {
let s = "\{pad(y, 4)}-\{pad(m, 2)}-\{pad(d, 2)}"
b.write_string(" office:value-type=\"date\" office:date-value=\"\{s}\"")
s
}
}
b.write_string(">")
if self.value != Empty && !attribute_text {
b.write_string(text_paragraph(display))
}
b.write_string("")
b.to_string()
}
///|
fn finite_number_text(n : Double) -> String {
if n == 0.0 && 1.0 / n < 0.0 {
"-0"
} else {
n.to_string()
}
}