import parsing
This commit is contained in:
2
server
2
server
Submodule server updated: 891aa0038b...b7504c894c
@@ -573,6 +573,7 @@ import-target-tables = Cílová tabulka
|
|||||||
import-choose-table = Vyberte tabulku
|
import-choose-table = Vyberte tabulku
|
||||||
import-csv-file = Soubor CSV
|
import-csv-file = Soubor CSV
|
||||||
import-csv-data = Data CSV
|
import-csv-data = Data CSV
|
||||||
|
import-csv-format-hint = Použijte CSV v UTF-8 s oddělovačem čárka a každé pole uzavřete do dvojitých uvozovek, například: "name","number","active"
|
||||||
import-error-title = CSV se nepodařilo importovat
|
import-error-title = CSV se nepodařilo importovat
|
||||||
import-import-rows = Importovat řádky
|
import-import-rows = Importovat řádky
|
||||||
import-success-title = Import dokončen
|
import-success-title = Import dokončen
|
||||||
@@ -617,6 +618,7 @@ import-success-ignored-system = { $columns } přiděluje server, importované ř
|
|||||||
import-err-missing-type = Chybí typ sloupce '{ $column }'
|
import-err-missing-type = Chybí typ sloupce '{ $column }'
|
||||||
import-err-permission = Vyžaduje se oprávnění k importu. Tabulka navíc potřebuje oprávnění k vkládání, aby do ní šlo načítat.
|
import-err-permission = Vyžaduje se oprávnění k importu. Tabulka navíc potřebuje oprávnění k vkládání, aby do ní šlo načítat.
|
||||||
import-err-unterminated-quote = CSV obsahuje neuzavřenou uvozovkovou hodnotu.
|
import-err-unterminated-quote = CSV obsahuje neuzavřenou uvozovkovou hodnotu.
|
||||||
|
import-err-strict-csv = Neplatný formát CSV. Každé pole musí být uzavřeno v dvojitých uvozovkách, pole musí být oddělena čárkami a každý záznam musí zůstat na jednom řádku.
|
||||||
import-err-empty = CSV je prázdné.
|
import-err-empty = CSV je prázdné.
|
||||||
import-err-bad-boolean = Neplatná booleovská hodnota '{ $value }'
|
import-err-bad-boolean = Neplatná booleovská hodnota '{ $value }'
|
||||||
import-err-bad-integer = Neplatná celočíselná hodnota '{ $value }'
|
import-err-bad-integer = Neplatná celočíselná hodnota '{ $value }'
|
||||||
|
|||||||
@@ -563,6 +563,7 @@ import-target-tables = Target table
|
|||||||
import-choose-table = Choose a table
|
import-choose-table = Choose a table
|
||||||
import-csv-file = CSV file
|
import-csv-file = CSV file
|
||||||
import-csv-data = CSV data
|
import-csv-data = CSV data
|
||||||
|
import-csv-format-hint = Use UTF-8 CSV with a comma separator and every field in double quotes, for example: "name","number","active"
|
||||||
import-error-title = Could not import CSV
|
import-error-title = Could not import CSV
|
||||||
import-import-rows = Import rows
|
import-import-rows = Import rows
|
||||||
import-success-title = Import complete
|
import-success-title = Import complete
|
||||||
@@ -604,6 +605,7 @@ import-success-ignored-system = { $columns } are assigned by the server, so the
|
|||||||
import-err-missing-type = Missing type for column '{ $column }'
|
import-err-missing-type = Missing type for column '{ $column }'
|
||||||
import-err-permission = Import permission is required. A table also needs insert permission before it can be loaded.
|
import-err-permission = Import permission is required. A table also needs insert permission before it can be loaded.
|
||||||
import-err-unterminated-quote = CSV contains an unterminated quoted value.
|
import-err-unterminated-quote = CSV contains an unterminated quoted value.
|
||||||
|
import-err-strict-csv = Invalid CSV format. Every field must be enclosed in double quotes, fields must be separated by commas, and each record must stay on one line.
|
||||||
import-err-empty = CSV is empty.
|
import-err-empty = CSV is empty.
|
||||||
import-err-bad-boolean = Invalid boolean value '{ $value }'
|
import-err-bad-boolean = Invalid boolean value '{ $value }'
|
||||||
import-err-bad-integer = Invalid integer value '{ $value }'
|
import-err-bad-integer = Invalid integer value '{ $value }'
|
||||||
|
|||||||
@@ -573,6 +573,7 @@ import-target-tables = Cieľová tabuľka
|
|||||||
import-choose-table = Vyberte tabuľku
|
import-choose-table = Vyberte tabuľku
|
||||||
import-csv-file = Súbor CSV
|
import-csv-file = Súbor CSV
|
||||||
import-csv-data = Údaje CSV
|
import-csv-data = Údaje CSV
|
||||||
|
import-csv-format-hint = Použite CSV v UTF-8 s oddeľovačom čiarka a každé pole uzavrite do dvojitých úvodzoviek, napríklad: "name","number","active"
|
||||||
import-error-title = CSV sa nepodarilo importovať
|
import-error-title = CSV sa nepodarilo importovať
|
||||||
import-import-rows = Importovať riadky
|
import-import-rows = Importovať riadky
|
||||||
import-success-title = Import dokončený
|
import-success-title = Import dokončený
|
||||||
@@ -617,6 +618,7 @@ import-success-ignored-system = { $columns } prideľuje server, importované ria
|
|||||||
import-err-missing-type = Chýba typ stĺpca '{ $column }'
|
import-err-missing-type = Chýba typ stĺpca '{ $column }'
|
||||||
import-err-permission = Vyžaduje sa oprávnenie na import. Tabuľka navyše potrebuje oprávnenie na vkladanie, aby sa do nej dalo načítať.
|
import-err-permission = Vyžaduje sa oprávnenie na import. Tabuľka navyše potrebuje oprávnenie na vkladanie, aby sa do nej dalo načítať.
|
||||||
import-err-unterminated-quote = CSV obsahuje neuzavretú úvodzovkovú hodnotu.
|
import-err-unterminated-quote = CSV obsahuje neuzavretú úvodzovkovú hodnotu.
|
||||||
|
import-err-strict-csv = Neplatný formát CSV. Každé pole musí byť uzavreté v dvojitých úvodzovkách, polia musia byť oddelené čiarkami a každý záznam musí zostať na jednom riadku.
|
||||||
import-err-empty = CSV je prázdny.
|
import-err-empty = CSV je prázdny.
|
||||||
import-err-bad-boolean = Neplatná boolovská hodnota '{ $value }'
|
import-err-bad-boolean = Neplatná boolovská hodnota '{ $value }'
|
||||||
import-err-bad-integer = Neplatná celočíselná hodnota '{ $value }'
|
import-err-bad-integer = Neplatná celočíselná hodnota '{ $value }'
|
||||||
|
|||||||
@@ -1,51 +1,57 @@
|
|||||||
use crate::{i18n::Locale, tr};
|
use crate::{i18n::Locale, tr};
|
||||||
|
|
||||||
|
/// Parses the one CSV shape accepted by data transfer: UTF-8, comma separated,
|
||||||
|
/// and every field enclosed in double quotes. Keeping one spelling makes the
|
||||||
|
/// file self-describing and leaves commas inside values unambiguous.
|
||||||
pub(crate) fn parse_csv(locale: Locale, input: &str) -> Result<Vec<Vec<String>>, String> {
|
pub(crate) fn parse_csv(locale: Locale, input: &str) -> Result<Vec<Vec<String>>, String> {
|
||||||
// The byte-order mark Excel writes at the head of a "CSV UTF-8" file is
|
|
||||||
// encoding metadata, not a character of the first field, so decoding
|
|
||||||
// consumes it. Nothing else about the file is touched here: what the
|
|
||||||
// fields contain is the user's, and the import validates it rather than
|
|
||||||
// tidying it up.
|
|
||||||
let input = input.strip_prefix('\u{feff}').unwrap_or(input);
|
let input = input.strip_prefix('\u{feff}').unwrap_or(input);
|
||||||
|
if input.is_empty() {
|
||||||
|
return Err(tr!(locale, "import-err-empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut chars = input.chars().peekable();
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
let mut row = Vec::new();
|
let mut row = Vec::new();
|
||||||
let mut field = String::new();
|
|
||||||
let mut chars = input.chars().peekable();
|
loop {
|
||||||
let mut quoted = false;
|
match chars.next() {
|
||||||
while let Some(ch) = chars.next() {
|
Some('"') => {}
|
||||||
match ch {
|
Some(_) => return Err(tr!(locale, "import-err-strict-csv")),
|
||||||
'"' if quoted && chars.peek() == Some(&'"') => {
|
None if row.is_empty() => break,
|
||||||
field.push('"');
|
None => return Err(tr!(locale, "import-err-strict-csv")),
|
||||||
let _ = chars.next();
|
}
|
||||||
}
|
|
||||||
'"' => quoted = !quoted,
|
let mut field = String::new();
|
||||||
',' if !quoted => row.push(std::mem::take(&mut field)),
|
loop {
|
||||||
'\n' if !quoted => {
|
match chars.next() {
|
||||||
if field.ends_with('\r') {
|
Some('"') if chars.peek() == Some(&'"') => {
|
||||||
field.pop();
|
field.push('"');
|
||||||
}
|
let _ = chars.next();
|
||||||
row.push(std::mem::take(&mut field));
|
}
|
||||||
if row.iter().any(|value| !value.is_empty()) {
|
Some('"') => break,
|
||||||
rows.push(std::mem::take(&mut row));
|
// A strict import row is one physical line. This also avoids
|
||||||
} else {
|
// hiding a missing closing quote many lines above the error.
|
||||||
row.clear();
|
Some('\n' | '\r') => return Err(tr!(locale, "import-err-strict-csv")),
|
||||||
}
|
Some(ch) => field.push(ch),
|
||||||
}
|
None => return Err(tr!(locale, "import-err-unterminated-quote")),
|
||||||
_ => field.push(ch),
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if quoted {
|
|
||||||
return Err(tr!(locale, "import-err-unterminated-quote"));
|
|
||||||
}
|
|
||||||
if field.ends_with('\r') {
|
|
||||||
field.pop();
|
|
||||||
}
|
|
||||||
if !field.is_empty() || !row.is_empty() {
|
|
||||||
row.push(field);
|
row.push(field);
|
||||||
if row.iter().any(|value| !value.is_empty()) {
|
|
||||||
rows.push(row);
|
match chars.next() {
|
||||||
|
Some(',') => continue,
|
||||||
|
Some('\n') => rows.push(std::mem::take(&mut row)),
|
||||||
|
Some('\r') if chars.next() == Some('\n') => {
|
||||||
|
rows.push(std::mem::take(&mut row));
|
||||||
|
}
|
||||||
|
Some(_) => return Err(tr!(locale, "import-err-strict-csv")),
|
||||||
|
None => {
|
||||||
|
rows.push(row);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if rows.is_empty() {
|
if rows.is_empty() {
|
||||||
Err(tr!(locale, "import-err-empty"))
|
Err(tr!(locale, "import-err-empty"))
|
||||||
} else {
|
} else {
|
||||||
@@ -53,18 +59,15 @@ pub(crate) fn parse_csv(locale: Locale, input: &str) -> Result<Vec<Vec<String>>,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes the same fully quoted format the importer accepts.
|
||||||
pub(crate) fn write_record(output: &mut String, fields: &[String]) {
|
pub(crate) fn write_record(output: &mut String, fields: &[String]) {
|
||||||
for (index, field) in fields.iter().enumerate() {
|
for (index, field) in fields.iter().enumerate() {
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
output.push(',');
|
output.push(',');
|
||||||
}
|
}
|
||||||
if field.contains([',', '"', '\n', '\r']) {
|
output.push('"');
|
||||||
output.push('"');
|
output.push_str(&field.replace('"', "\"\""));
|
||||||
output.push_str(&field.replace('"', "\"\""));
|
output.push('"');
|
||||||
output.push('"');
|
|
||||||
} else {
|
|
||||||
output.push_str(field);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
output.push('\n');
|
output.push('\n');
|
||||||
}
|
}
|
||||||
@@ -74,10 +77,48 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quoted_multiline_csv_round_trips() {
|
fn fully_quoted_csv_round_trips_commas_and_quotes() {
|
||||||
let fields = vec!["name".to_string(), "hello, \"world\"\nnext".to_string()];
|
let fields = vec![
|
||||||
|
"Acme, s.r.o.".to_string(),
|
||||||
|
"Customer said \"send it today\"".to_string(),
|
||||||
|
];
|
||||||
let mut csv = String::new();
|
let mut csv = String::new();
|
||||||
write_record(&mut csv, &fields);
|
write_record(&mut csv, &fields);
|
||||||
assert_eq!(parse_csv(crate::i18n::Locale::default(), &csv).unwrap(), vec![fields]);
|
|
||||||
|
assert_eq!(
|
||||||
|
csv,
|
||||||
|
"\"Acme, s.r.o.\",\"Customer said \"\"send it today\"\"\"\n"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_csv(crate::i18n::Locale::default(), &csv).unwrap(),
|
||||||
|
vec![fields]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_field_must_be_quoted() {
|
||||||
|
for csv in [
|
||||||
|
"name,num\nAcme,123\n",
|
||||||
|
"\"name\",num\n\"Acme\",\"123\"\n",
|
||||||
|
"\"name\",\"num\"\n\"Acme\",123\n",
|
||||||
|
"\"name\" ,\"num\"\n",
|
||||||
|
] {
|
||||||
|
assert!(parse_csv(crate::i18n::Locale::default(), csv).is_err(), "{csv}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepts_utf8_bom_and_crlf() {
|
||||||
|
let csv = "\u{feff}\"name\",\"num\"\r\n\"Acme\",\"123\"\r\n";
|
||||||
|
assert_eq!(
|
||||||
|
parse_csv(crate::i18n::Locale::default(), csv).unwrap(),
|
||||||
|
vec![vec!["name", "num"], vec!["Acme", "123"]]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiline_fields_are_rejected() {
|
||||||
|
let csv = "\"name\",\"note\"\n\"Acme\",\"first\nsecond\"\n";
|
||||||
|
assert!(parse_csv(crate::i18n::Locale::default(), csv).is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,10 +635,18 @@ mod tests {
|
|||||||
/// header is left exactly as it arrived.
|
/// header is left exactly as it arrived.
|
||||||
#[test]
|
#[test]
|
||||||
fn the_byte_order_mark_is_decoded_away_and_nothing_else_is() {
|
fn the_byte_order_mark_is_decoded_away_and_nothing_else_is() {
|
||||||
let rows = parse_csv(Locale::default(), "\u{feff}label,amount\nAcme,10\n").unwrap();
|
let rows = parse_csv(
|
||||||
|
Locale::default(),
|
||||||
|
"\u{feff}\"label\",\"amount\"\n\"Acme\",\"10\"\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(rows[0], vec!["label", "amount"]);
|
assert_eq!(rows[0], vec!["label", "amount"]);
|
||||||
|
|
||||||
let rows = parse_csv(Locale::default(), "LABEL, amount \nAcme,10\n").unwrap();
|
let rows = parse_csv(
|
||||||
|
Locale::default(),
|
||||||
|
"\"LABEL\",\" amount \"\n\"Acme\",\"10\"\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(rows[0], vec!["LABEL", " amount "]);
|
assert_eq!(rows[0], vec!["LABEL", " amount "]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
onchange="this.files[0]?.text().then(value => document.getElementById('csv-data').value = value)">
|
onchange="this.files[0]?.text().then(value => document.getElementById('csv-data').value = value)">
|
||||||
</label>
|
</label>
|
||||||
<label class="wide">{{ nav.tr("import-csv-data") }}<textarea id="csv-data" name="csv_data" rows="14" required>{{ page.form.csv_data }}</textarea></label>
|
<label class="wide">{{ nav.tr("import-csv-data") }}<textarea id="csv-data" name="csv_data" rows="14" required>{{ page.form.csv_data }}</textarea></label>
|
||||||
|
<small class="wide">{{ nav.tr("import-csv-format-hint") }}</small>
|
||||||
</div>
|
</div>
|
||||||
<label class="check">
|
<label class="check">
|
||||||
<input type="checkbox" name="import_system_columns" value="true"{% if page.form.import_system_columns() %} checked{% endif %}>
|
<input type="checkbox" name="import_system_columns" value="true"{% if page.form.import_system_columns() %} checked{% endif %}>
|
||||||
|
|||||||
Reference in New Issue
Block a user