ODS writer: drop duplicate XML declaration; harden sheet-name sanitiser.
LibreOffice rejected the generated `.ods` with `Format error at 2,39 in content.xml`. Root cause: `pugi::format_no_declaration` suppresses only the *implicit* declaration auto-added at save time — the explicit `node_declaration` I had appended to the document still got serialised, on top of a manual `<?xml…?>` string prepend in the output. Two declarations back-to-back, invalid XML. Fix: let pugixml emit the explicit declaration node, drop the manual prepend. Also harden the sheet-name sanitiser in the export action: ODS / Excel also forbid `< > &` in raw cell or table names, so the default connection name `bp/J20 <-> payload1/P0` made content.xml entity- escape `<` to `<`, which a few viewers handle but Excel rejects. Clip to 31 chars too (Excel's hard limit) so multi-name connections don't blow up the open. Verified by `soffice --headless --convert-to csv` round-tripping the output without errors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -73,11 +73,14 @@ std::string build_content_xml(const std::vector<OdsSheet> &sheets) {
|
||||
}
|
||||
|
||||
std::ostringstream oss;
|
||||
doc.save(oss, " ", pugi::format_no_declaration | pugi::format_raw);
|
||||
// pugi's `format_no_declaration` drops the `<?xml…?>` line; we want
|
||||
// it. Re-emit explicitly to control encoding.
|
||||
return std::string(R"(<?xml version="1.0" encoding="UTF-8"?>)" "\n")
|
||||
+ oss.str();
|
||||
// We added the declaration node explicitly above, so pugi will emit
|
||||
// it. `format_no_declaration` only suppresses *implicit* injection;
|
||||
// setting it here would leave the explicit node in place and silently
|
||||
// duplicate would-be auto-emission elsewhere. Keep format_raw to
|
||||
// avoid pretty-print overhead — the file is read by software, not
|
||||
// humans.
|
||||
doc.save(oss, "", pugi::format_raw);
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
// Minimum META-INF/manifest.xml that satisfies the spec.
|
||||
|
||||
@@ -98,12 +98,18 @@ void Tui::RegisterExportCommands() {
|
||||
int total = 0;
|
||||
for (auto &ckv : *sys->connections()) {
|
||||
Connection *c = ckv.second;
|
||||
// Sheet names: Excel rejects /\?*:[] characters
|
||||
// (LibreOffice tolerates them but stays portable).
|
||||
// Sheet names: Excel rejects /\?*:[] characters,
|
||||
// ODS forbids < > & in raw cell/table names
|
||||
// (they'd need entity-escaping but a few viewers
|
||||
// choke on them). Sanitise to underscores.
|
||||
std::string sname = c->name;
|
||||
for (char &ch : sname)
|
||||
if (ch == '/' || ch == '\\' || ch == '?' || ch == '*'
|
||||
|| ch == ':' || ch == '[' || ch == ']') ch = '_';
|
||||
|| ch == ':' || ch == '[' || ch == ']'
|
||||
|| ch == '<' || ch == '>' || ch == '&') ch = '_';
|
||||
// Sheet names also can't exceed 31 chars in
|
||||
// Excel; clip aggressively to stay portable.
|
||||
if (sname.size() > 31) sname = sname.substr(0, 31);
|
||||
OdsSheet *s = w.add_sheet(sname);
|
||||
const char *hdr[] = {
|
||||
"transform",
|
||||
|
||||
Reference in New Issue
Block a user