pub(crate) fn build_string_table_xlsx(
sheet_name: &str,
headers: &[&str],
rows: &[Vec<&str>],
) -> Vec<u8> {
let mut zip = ZipWriter::new(Cursor::new(Vec::new()));
let file_options = FileOptions::default().compression_method(CompressionMethod::Deflated);
write_zip_file(
&mut zip,
"[Content_Types].xml",
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
</Types>"#,
file_options,
);
write_zip_file(
&mut zip,
"_rels/.rels",
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
</Relationships>"#,
file_options,
);
write_zip_file(
&mut zip,
"xl/workbook.xml",
&format!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheets><sheet name="{}" sheetId="1" r:id="rId1"/></sheets>
</workbook>"#,
escape_xml_text(sheet_name)
),
file_options,
);
write_zip_file(
&mut zip,
"xl/_rels/workbook.xml.rels",
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
</Relationships>"#,
file_options,
);
let mut shared_strings = Vec::new();
shared_strings.extend(headers.iter().copied());
for row in rows {
shared_strings.extend(row.iter().copied());
}
let shared_string_items = shared_strings
.iter()
.map(|value| format!("<si><t>{}</t></si>", escape_xml_text(value)))
.collect::<String>();
write_zip_file(
&mut zip,
"xl/sharedStrings.xml",
&format!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="{count}" uniqueCount="{count}">
{shared_string_items}
</sst>"#,
count = shared_strings.len()
),
file_options,
);
write_zip_file(
&mut zip,
"xl/styles.xml",
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs></styleSheet>"#,
file_options,
);
let mut next_shared_index = 0usize;
let mut sheet_rows = String::new();
sheet_rows.push_str(&xlsx_row(1, headers.len(), &mut next_shared_index));
for (row_index, row) in rows.iter().enumerate() {
assert_eq!(row.len(), headers.len());
sheet_rows.push_str(&xlsx_row(row_index + 2, row.len(), &mut next_shared_index));
}
write_zip_file(
&mut zip,
"xl/worksheets/sheet1.xml",
&format!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>{sheet_rows}</sheetData>
</worksheet>"#
),
file_options,
);
zip.finish().expect("finish string table xlsx").into_inner()
}
fn xlsx_row(row_number: usize, width: usize, next_shared_index: &mut usize) -> String {
let mut row = format!(r#"<row r="{row_number}">"#);
for col_index in 0..width {
let cell = format!("{}{}", excel_col_name(col_index), row_number);
row.push_str(&format!(
r#"<c r="{cell}" t="s"><v>{}</v></c>"#,
*next_shared_index
));
*next_shared_index += 1;
}
row.push_str("</row>");
row
}
fn excel_col_name(mut index: usize) -> String {
let mut chars = Vec::new();
loop {
let rem = index % 26;
chars.push((b'A' + rem as u8) as char);
index /= 26;
if index == 0 {
break;
}
index -= 1;
}
chars.iter().rev().collect()
}
fn escape_xml_text(value: &str) -> String {
value
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}