mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-02 22:50:27 -06:00
Merge pr/673: fix(import): preserve XLSX numeric and cached text values (#673)
This commit is contained in:
+2
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: pre-alpha** · next: alpha, ~5–7 h away; beta ~28 points and ~420–750 h beyond that (the core-workflow gate, not the percentage: ready for real work is already ~47%)
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-13 · **Change:** minor (text threads by hand; the alpha gate waits on Primary Text Frame page fixes; Japanese print, import and table controls localized; Live corners (#186); Optical kerning from outlines (#181); window and menu quit confirmation; failed writes preserve live save and collection state; spring-loaded Zoom navigation; orderly native exit recovery; Pages panel parents; cancelable macOS Dock Quit; text stroke behind fill; Based on Parent; object styles carry effects; table anchors keep their own paragraphs; grouped HTML/EPUB reading order; Links source paths) · **Target:** Adobe InDesign 2026
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-13 · **Change:** minor (text threads by hand; the alpha gate waits on Primary Text Frame page fixes; Japanese print, import and table controls localized; Live corners (#186); Optical kerning from outlines (#181); window and menu quit confirmation; failed writes preserve live save and collection state; spring-loaded Zoom navigation; orderly native exit recovery; Pages panel parents; cancelable macOS Dock Quit; text stroke behind fill; Based on Parent; object styles carry effects; table anchors keep their own paragraphs; grouped HTML/EPUB reading order; Links source paths; XLSX cached cell values retain numeric precision and text) · **Target:** Adobe InDesign 2026
|
||||
|
||||
DesignCraft aims at full Adobe InDesign parity, and to be better: faster, open (documented JSON
|
||||
format + IDML), scriptable by agents (MCP), and available on the web.
|
||||
@@ -128,6 +128,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
|
||||
Newest first.
|
||||
|
||||
- **2026-10-12** · Placed Excel tables and Excel data-merge records keep numeric precision, including small values, and preserve numeric-looking cached formula strings as text. No readiness percentage change.
|
||||
- **2026-10-13** · Links rows show their source paths; selecting a link shows its full path wrapped inside the panel, so same-named files from different folders can be distinguished. No readiness percentage change.
|
||||
- **2026-10-13** · HTML and reflowable EPUB order grouped stories and graphics by their transformed spread position, including nested groups and rotated frames. Explicit Article order is unchanged. No readiness percentage change.
|
||||
- **2026-10-13** · A selected placed graphic shows its own brown bounding box with handles: dragging one resizes the graphic inside its frame (Shift keeps proportions, Alt from the centre) and leaves the frame as it is; `transform.set` takes the graphic's new spread `bounds`. Builds on #415 and #550. No readiness percentage change.
|
||||
|
||||
@@ -40,6 +40,8 @@ fn value(c: &El, shared: &[String]) -> String {
|
||||
match c.attr("t") {
|
||||
Some("s") => v().trim().parse::<usize>().ok().and_then(|i| shared.get(i).cloned()).unwrap_or_default(),
|
||||
Some("inlineStr") => c.child("is").map(El::text).unwrap_or_default(),
|
||||
// A cached formula string is text even when it looks like a number.
|
||||
Some("str") => v(),
|
||||
Some("b") => {
|
||||
if v().trim() == "1" {
|
||||
"TRUE".into()
|
||||
@@ -49,16 +51,34 @@ fn value(c: &El, shared: &[String]) -> String {
|
||||
}
|
||||
_ => {
|
||||
let raw = v();
|
||||
// Whole numbers without the trailing ".0" Excel never shows.
|
||||
match raw.trim().parse::<f64>() {
|
||||
Ok(f) if f.fract() == 0.0 && f.abs() < 1e15 => format!("{}", f as i64),
|
||||
Ok(f) => format!("{}", (f * 1e10).round() / 1e10),
|
||||
Err(_) => raw,
|
||||
Ok(f) if f.is_finite() => general(f),
|
||||
_ => raw,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A number as Excel's General format shows it: at most 15 significant digits (Excel stores
|
||||
/// doubles with 17, so `=0.1+0.2` is cached as 0.30000000000000004 and shown as 0.3), whole
|
||||
/// numbers without ".0", and an exponent for very large or very small values.
|
||||
fn general(f: f64) -> String {
|
||||
let r = format!("{f:.14e}").parse::<f64>().unwrap_or(f);
|
||||
let a = r.abs();
|
||||
if r == 0.0 {
|
||||
"0".into()
|
||||
} else if (1e-9..1e15).contains(&a) {
|
||||
// Shortest text that reads back as the rounded value; whole numbers drop the ".0".
|
||||
r.to_string()
|
||||
} else {
|
||||
let s = format!("{r:E}");
|
||||
match s.split_once('E') {
|
||||
Some((m, e)) if !e.starts_with('-') => format!("{m}E+{e}"),
|
||||
_ => s,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One worksheet as a grid of cached cell text, empty cells kept, for data merge.
|
||||
/// The first used row is the header. Later rows that are entirely empty are left out.
|
||||
/// Cells past the header are kept so the caller can warn. Over 200 columns or 100,000
|
||||
@@ -334,4 +354,56 @@ mod tests {
|
||||
assert_eq!(rows[3][1], "1");
|
||||
assert!(records(&book, Some("Missing")).is_err());
|
||||
}
|
||||
|
||||
fn one_value(cell: &str) -> Vec<u8> {
|
||||
workbook_with(&format!(
|
||||
r#"<worksheet><sheetData><row r="1"><c r="A1" t="inlineStr"><is><t>Value</t></is></c></row><row r="2">{cell}</row></sheetData></worksheet>"#
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numeric_values_keep_their_precision_in_tables_and_records() {
|
||||
for text in ["1e-12", "-0.0000000000123", "1.23456789012345", "1e300", "12.0", "-2.5e-300"] {
|
||||
let book = one_value(&format!(r#"<c r="A2"><v>{text}</v></c>"#));
|
||||
let placed = crate::import("values.xlsx", &book).unwrap();
|
||||
let table = placed.story.tables.values().next().unwrap();
|
||||
let rows = crate::xlsx_records(&book, None).unwrap();
|
||||
for got in [&table.cell(1, 0).unwrap().text.text, &rows[1][0]] {
|
||||
assert_eq!(got.parse::<f64>().unwrap(), text.parse::<f64>().unwrap(), "cell {text} became {got}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numeric_values_show_as_excel_general_format() {
|
||||
for (text, shown) in [
|
||||
("0.30000000000000004", "0.3"),
|
||||
("2.0000000000000004", "2"),
|
||||
("12.0", "12"),
|
||||
("-0.5", "-0.5"),
|
||||
("0", "0"),
|
||||
("123456789012345", "123456789012345"),
|
||||
("1e15", "1E+15"),
|
||||
("1e300", "1E+300"),
|
||||
("1e-12", "1E-12"),
|
||||
("-1.5e-10", "-1.5E-10"),
|
||||
("inf", "inf"),
|
||||
] {
|
||||
let book = one_value(&format!(r#"<c r="A2"><v>{text}</v></c>"#));
|
||||
let rows = crate::xlsx_records(&book, None).unwrap();
|
||||
assert_eq!(rows[1][0], shown, "cell {text}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_string_values_are_not_reformatted_as_numbers() {
|
||||
for text in ["00123", "1e3", "12.0", " 42 "] {
|
||||
let book = one_value(&format!(r#"<c r="A2" t="str"><f>"{text}"</f><v>{text}</v></c>"#));
|
||||
let placed = crate::import("values.xlsx", &book).unwrap();
|
||||
let table = placed.story.tables.values().next().unwrap();
|
||||
let rows = crate::xlsx_records(&book, None).unwrap();
|
||||
assert_eq!(table.cell(1, 0).unwrap().text.text, text);
|
||||
assert_eq!(rows[1][0], text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; Windows CI passes) · **Target:** Adobe InDesign 2026 (21.6)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (XLSX cached values preserve numeric precision and text; shared print spooler; Windows CI passes) · **Target:** Adobe InDesign 2026 (21.6)
|
||||
|
||||
Every format InDesign 2026 reads or writes, with DesignCraft's support, fidelity and tests. The
|
||||
InDesign list comes from its bundle's `Info.plist` (`CFBundleDocumentTypes`,
|
||||
@@ -62,7 +62,7 @@ score below).
|
||||
| HEIC | `.heic` | ✓ | ✗ | | <1% |
|
||||
| Word | `.docx` | ✓ styles, footnotes, tables, track changes, options | ✓ styles by name, footnotes, tables, tabs, breaks, moved text (#257), bullets and numbering from `numbering.xml` (#626; levels become list levels, parent numbers in a label level references), style mapping and conflicts | `.doc` not supported (InDesign reads it) | 2% |
|
||||
| RTF | `.rtf` | ✓ | ✓ | | <1% |
|
||||
| Excel | `.xlsx`, `.xls` | ✓ | ~ `.xlsx` first sheet's used range | no sheet/range options; `.xls` ✗ | <1% |
|
||||
| Excel | `.xlsx`, `.xls` | ✓ | ~ `.xlsx` first sheet's used range | cached numbers keep their precision and formula strings stay text; no sheet/range options; `.xls` ✗ | <1% |
|
||||
| Plain text, Markdown | `.txt`, `.md` | ✓ (txt) | ✓ | | <1% |
|
||||
| InDesign Tagged Text | `.txt` | ✓ | ✓ import and export | | <1% |
|
||||
| CSV / TSV (Data Merge) | | ✓ | ✓ (+ Excel) | | <1% |
|
||||
@@ -107,4 +107,5 @@ for real work** (estimated). INDD alone moves this dimension by up to 30 points.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | XLSX table placement and data merge preserve small numeric values and cached formula strings; synthetic regressions in `textimport::xlsx::tests`. Scores unchanged. |
|
||||
| 2026-10-10 | major | Created from InDesign 2026's declared document types and filters, `plan/indesign/08-file-formats.md` and the code |
|
||||
|
||||
Reference in New Issue
Block a user