mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
The tab-delimited export of a single field has no tab, so import read it as CSV and kept only the text before the first comma. CSV quotes a value that holds a comma, so a single name over several unquoted cells is now read as that tab-delimited text. Exported files are unchanged, and CSV (quoted, multi-line, one or more columns) reads as before.
This commit is contained in:
1 parent
d2d1b16b21
commit
ec5d9da36c
2 files changed
+62
-4
No files matched your search
+11
-4
@@ -1093,9 +1093,7 @@ fn import_table(doc: &mut Document, text: &str) -> Result<Report, DataError> {
|
||||
})
|
||||
.collect()
|
||||
} else if t.lines().next().is_some_and(|l| l.contains('\t')) {
|
||||
let mut lines = t.lines();
|
||||
let (Some(head), Some(row)) = (lines.next(), lines.next()) else { return Err(DataError::UnknownFormat) };
|
||||
head.split('\t').map(str::to_string).zip(row.split('\t')).map(|(k, v)| (k, vec![v.to_string()])).collect()
|
||||
tab_delimited(t)?
|
||||
} else {
|
||||
// CSV with quotes: a quoted value may hold commas, doubled quotes and line breaks.
|
||||
let mut records: Vec<Vec<String>> = Vec::new();
|
||||
@@ -1124,12 +1122,21 @@ fn import_table(doc: &mut Document, text: &str) -> Result<Report, DataError> {
|
||||
// Blank lines (such as the one a trailing line break leaves) are not records.
|
||||
let mut records = records.into_iter().filter(|r| !matches!(r.as_slice(), [only] if only.is_empty()));
|
||||
let (Some(head), Some(row)) = (records.next(), records.next()) else { return Err(DataError::UnknownFormat) };
|
||||
head.into_iter().zip(row).map(|(k, v)| (k, vec![v])).collect()
|
||||
// Text exported from a one-field form has no tab to show for it (#1321). CSV quotes a
|
||||
// value with a comma, so one name over several unquoted cells is that text, not CSV.
|
||||
if head.len() == 1 && row.len() > 1 { tab_delimited(t)? } else { head.into_iter().zip(row).map(|(k, v)| (k, vec![v])).collect() }
|
||||
};
|
||||
apply_values(doc, &values, &mut report);
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
/// Tab-delimited text: a row of names, a row of values.
|
||||
fn tab_delimited(t: &str) -> Result<Vec<(String, Vec<String>)>, DataError> {
|
||||
let mut lines = t.lines();
|
||||
let (Some(head), Some(row)) = (lines.next(), lines.next()) else { return Err(DataError::UnknownFormat) };
|
||||
Ok(head.split('\t').map(str::to_string).zip(row.split('\t')).map(|(k, v)| (k, vec![v.to_string()])).collect())
|
||||
}
|
||||
|
||||
/// Import comments and/or field values from XFDF, FDF, XML, CSV or tab-delimited text
|
||||
/// (detected from the content).
|
||||
pub fn import(doc: &mut Document, bytes: &[u8]) -> Result<Report, DataError> {
|
||||
|
||||
@@ -328,6 +328,57 @@ fn csv_values_may_span_lines() {
|
||||
assert!(import(&mut blank(), b"Name,Agree\r\n\"Ada, Yes\r\n").is_ok());
|
||||
}
|
||||
|
||||
/// One page with one text field, `Name`.
|
||||
fn one_field() -> Document {
|
||||
let mut doc = Document::new_empty();
|
||||
let pages = doc.get(doc.root().unwrap()).as_dict().unwrap().reference(b"Pages").unwrap();
|
||||
let mut p = Dict::new();
|
||||
p.set(b"Type".to_vec(), Object::name("Page"));
|
||||
p.set(b"Parent".to_vec(), Object::Ref(pages));
|
||||
p.set(b"MediaBox".to_vec(), Object::Array(vec![0.into(), 0.into(), 600.into(), 800.into()]));
|
||||
let page = doc.add(p);
|
||||
doc.update_dict(pages, |d| {
|
||||
d.set(b"Count".to_vec(), Object::Int(1));
|
||||
d.set(b"Kids".to_vec(), Object::Array(vec![Object::Ref(page)]));
|
||||
})
|
||||
.unwrap();
|
||||
add_field(&mut doc, 0, [50.0, 700.0, 250.0, 720.0], &NewField::Text { multiline: false }, Some("Name")).unwrap();
|
||||
Document::open(Arc::new(write_full(&doc, &SaveOptions::default()).unwrap())).unwrap()
|
||||
}
|
||||
|
||||
/// #1321: the text export of a one-field form has no tab to tell it from CSV, so a comma in the
|
||||
/// value used to split it and only the text before the comma was imported.
|
||||
#[test]
|
||||
fn one_field_text_data_keeps_its_commas() {
|
||||
for value in ["Hello, world", "Say \"hi\", twice"] {
|
||||
let mut src = one_field();
|
||||
set_value(&mut src, "Name", &FieldValue::Text(value.into())).unwrap();
|
||||
let txt = export_data(&src, Format::Txt);
|
||||
assert_eq!(txt, format!("Name\n{value}\n"));
|
||||
let mut dst = one_field();
|
||||
let r = import(&mut dst, txt.as_bytes()).unwrap();
|
||||
assert_eq!(r.fields, 1, "{r:?}");
|
||||
assert!(r.rejected.is_empty(), "{r:?}");
|
||||
assert_eq!(values(&dst)[0].1, [value]);
|
||||
}
|
||||
}
|
||||
|
||||
/// A one-column CSV is still CSV: quotes come off, and a quoted value keeps its commas and line
|
||||
/// breaks.
|
||||
#[test]
|
||||
fn one_column_csv_still_reads_its_quotes() {
|
||||
let mut src = one_field();
|
||||
set_value(&mut src, "Name", &FieldValue::Text("Hello, world".into())).unwrap();
|
||||
let csv = export_data(&src, Format::Csv);
|
||||
assert_eq!(csv, "Name\n\"Hello, world\"\n");
|
||||
let mut dst = one_field();
|
||||
assert_eq!(import(&mut dst, csv.as_bytes()).unwrap().fields, 1);
|
||||
assert_eq!(values(&dst)[0].1, ["Hello, world"]);
|
||||
let mut dst = one_field();
|
||||
assert_eq!(import(&mut dst, b"Name\r\n\"Ada \"\"the\"\", \r\nCountess\"\r\n").unwrap().fields, 1);
|
||||
assert!(values(&dst)[0].1[0].starts_with("Ada \"the\", ") && values(&dst)[0].1[0].ends_with("Countess"), "{:?}", values(&dst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_select_lists_round_trip_through_xml_csv_and_text() {
|
||||
let src = filled();
|
||||
|
||||
Reference in new issue
Block a user