tabnas_csv/lib.rs
1// Copyright (c) 2021-2026 Richard Rodger, MIT License
2
3// The engine's error carries a code, position, hint and a formatted
4// report, so it is large by design and `Result<_, TabnasError>` trips
5// clippy's `result_large_err`. The engine allows the lint at its own
6// crate root for the same reason, and so does tabnas-jsonic; boxing here
7// would make `parse` return a different shape from `Tabnas::parse` and
8// from the other two ports.
9#![allow(clippy::result_large_err)]
10
11//! The CSV grammar plugin for the `tabnas` parsing engine.
12//!
13//! CSV text becomes an array of objects (one per row, keyed by the header
14//! row) or an array of arrays, with RFC 4180 `""`-escaped quoting, custom
15//! field and record separators, streaming, and a strict / non-strict mode.
16//! In strict mode (the default) every field is raw text; in non-strict
17//! mode a field body can hold embedded jsonic (`[1,2]`, `{x:1}`).
18//!
19//! The plugin is not standalone: it layers on the relaxed-JSON grammar of
20//! [`tabnas_jsonic`] (the `val` / `map` / `list` / `pair` / `elem` rules)
21//! and reuses the engine's lexer, comment handling and lifecycle hooks,
22//! exactly as the canonical TypeScript plugin in `ts/src/csv.ts` does.
23//!
24//! ```
25//! let value = tabnas_csv::parse("name,age\nAlice,30\nBob,25")?;
26//! assert_eq!(
27//! value.to_json().to_string(),
28//! r#"[{"name":"Alice","age":"30"},{"name":"Bob","age":"25"}]"#
29//! );
30//! # Ok::<(), tabnas_csv::CsvError>(())
31//! ```
32//!
33//! TypeScript is canonical: `ts/src/csv.ts` defines behaviour and option
34//! defaults, and `csv-grammar.jsonic` at the repository root is the
35//! grammar every runtime embeds. The shared fixtures in `test/spec/*.tsv`
36//! are the parity contract across TypeScript, Go and Rust.
37
38use std::cell::RefCell;
39use std::fmt;
40use std::rc::Rc;
41use std::sync::{Arc, OnceLock};
42
43use indexmap::IndexMap;
44use serde::{Deserialize, Serialize};
45use serde_json::json;
46use tabnas::{
47 ActionError, AltSpec, Context, GrammarSetting, GrammarSpec, ImperativeLexMatcher, Options,
48 Plugin, PluginError, Rule, RuleSpec, Tabnas, Tin, Token, Value, TIN_BD, TIN_ST,
49};
50
51/// This crate's version. It MUST equal `ts/package.json` "version": the
52/// release orchestrator rewrites both, and `tests/version_test.rs` fails
53/// the build if they drift. Mirrors `VERSION` in `ts/src/csv.ts` and
54/// `const VERSION` in `go/csv.go`.
55pub const VERSION: &str = "0.6.4";
56
57/// The README's Rust examples run as doctests, so a stale one fails the
58/// gate rather than misleading the reader. Its `toml` and `bash` fences
59/// are skipped; rustdoc runs only the `rust` ones.
60#[cfg(doctest)]
61#[doc = include_str!("../README.md")]
62mod readme_examples {}
63
64/// The error a failed parse produces, re-exported so callers need not
65/// depend on the engine crate directly. Its `code` is the contract the
66/// shared fixtures pin: `csv_extra_field`, `csv_missing_field`, and the
67/// engine's own `unexpected` and `unterminated_string`.
68pub use tabnas::TabnasError as CsvError;
69
70// --- BEGIN EMBEDDED csv-grammar.jsonic ---
71const GRAMMAR_TEXT: &str = r##"
72# CSV Grammar Definition
73# Parsed by a standard Jsonic instance and passed to jsonic.grammar()
74# Function references (@ prefixed) are resolved against the refs map
75#
76# Token naming:
77# #LN - line ending (removed from per-instance IGNORE set)
78# #SP - whitespace (removed from per-instance IGNORE set in strict mode)
79# #CA - comma / field separator
80# #ZZ - end of input
81# #VAL - token set: text, string, number, value literals
82#
83# Rules csv, newline, record, text are fully defined here.
84# Rules list, elem, val are modified in code (strict mode defines from scratch;
85# non-strict prepends to existing defaults to preserve JSON parsing).
86
87{
88 options: rule: { start: csv }
89 options: lex: { emptyResult: [] }
90 # Placeholders are {braces}: the engine's error injector (strinject in
91 # @tabnas/parser) substitutes {name} from the details object passed to
92 # token.bad(). A $name is left in the message verbatim.
93 options: error: {
94 csv_extra_field: 'unexpected extra field value: {fsrc}'
95 csv_missing_field: 'missing field'
96 }
97 options: hint: {
98 csv_extra_field: 'Row {row} has too many fields (the first of which is: {fsrc}). Only {len}\nfields per row are expected.'
99 csv_missing_field: 'Row {row} has too few fields. {len} fields per row are expected.'
100 }
101
102 rule: csv: open: [
103 { s: '#ZZ' }
104 { s: '#LN' p: newline c: '@not-record-empty' }
105 { p: record }
106 ]
107
108 rule: newline: open: [
109 { s: '#LN #LN' r: newline }
110 { s: '#LN' r: newline }
111 { s: '#ZZ' }
112 { r: record }
113 ]
114 rule: newline: close: [
115 { s: '#LN #LN' r: newline g: end }
116 { s: '#LN' r: newline g: end }
117 { s: '#ZZ' g: end }
118 { r: record }
119 ]
120
121 rule: record: open: [
122 { p: list }
123 ]
124 rule: record: close: [
125 { s: '#ZZ' g: end }
126 { s: '#LN #ZZ' b: 1 g: end }
127 { s: '#LN' r: '@record-close-next' g: end }
128 ]
129
130 rule: text: open: [
131 { s: ['#VAL' '#SP'] b: 1 r: text n: { text: 1 } g: 'csv,space,follows' a: '@text-follows' }
132 { s: ['#SP' '#VAL'] r: text n: { text: 1 } g: 'csv,space,leads' a: '@text-leads' }
133 { s: ['#SP' '#CA #LN #ZZ'] b: 1 n: { text: 1 } g: 'csv,end' a: '@text-end' }
134 { s: '#SP' n: { text: 1 } g: 'csv,space' a: '@text-space' p: '@text-space-push' }
135 {}
136 ]
137}
138"##;
139// --- END EMBEDDED csv-grammar.jsonic ---
140
141/// The grammar source every runtime embeds, verbatim: the text of
142/// `csv-grammar.jsonic` at the repository root. Exposed so a test can hold
143/// the embedded copy to the file on disk.
144pub fn grammar_text() -> &'static str {
145 GRAMMAR_TEXT
146}
147
148/// The name every csv plugin registers under, and so the namespace of its
149/// plugin options (`parser.plugin_options("csv")`).
150const PLUGIN_NAME: &str = "csv";
151
152// ---------------------------------------------------------------------------
153// Named function references.
154//
155// The grammar names every closure, and the closures are registered on the
156// instance BEFORE the document is installed, because the document is what
157// looks them up. The lifecycle names follow the engine's `@<rule>-<phase>`
158// convention and are wired onto their rule automatically.
159// ---------------------------------------------------------------------------
160
161const CSV_BO: &str = "@csv-bo";
162const CSV_AC: &str = "@csv-ac";
163const RECORD_BC: &str = "@record-bc";
164const TEXT_BC: &str = "@text-bc";
165const TEXT_FOLLOWS: &str = "@text-follows";
166const TEXT_LEADS: &str = "@text-leads";
167const TEXT_END: &str = "@text-end";
168const TEXT_SPACE: &str = "@text-space";
169const NOT_RECORD_EMPTY: &str = "@not-record-empty";
170const RECORD_CLOSE_NEXT: &str = "@record-close-next";
171const TEXT_SPACE_PUSH: &str = "@text-space-push";
172/// The `options.lex.match.stringcsv.make` reference: the RFC 4180 string
173/// matcher factory.
174const CSV_STRING: &str = "@csv-string";
175
176/// The matcher's priority. Below the engine's own bands (fixed literals sit
177/// at 2e6, quoted strings at 5e6), so a `""`-escaped field is cut before
178/// the standard string matcher can see its opening quote. The same order
179/// the TypeScript and Go plugins register.
180const CSV_STRING_ORDER: f64 = 100_000.0;
181
182// ---------------------------------------------------------------------------
183// Options
184// ---------------------------------------------------------------------------
185
186/// What a streaming parse reports, in order: `Start`, one `Record` per
187/// data row, then `End`. The counterpart of the TypeScript `stream(what,
188/// record)` callback's `'start'`, `'record'` and `'end'` events.
189#[derive(Debug, Clone, PartialEq)]
190pub enum StreamEvent {
191 /// The parse has begun.
192 Start,
193 /// One parsed record, shaped by `object` exactly as it would have been
194 /// stored in the result.
195 Record(Value),
196 /// The parse has finished.
197 End,
198}
199
200/// A streaming callback: records are handed to it as they are parsed and
201/// are not stored in the result, which comes back empty.
202#[derive(Clone)]
203pub struct Stream(Arc<dyn Fn(StreamEvent) + Send + Sync>);
204
205impl Stream {
206 /// Wrap a callback.
207 pub fn new(callback: impl Fn(StreamEvent) + Send + Sync + 'static) -> Self {
208 Stream(Arc::new(callback))
209 }
210
211 fn send(&self, event: StreamEvent) {
212 (self.0)(event);
213 }
214}
215
216impl fmt::Debug for Stream {
217 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
218 formatter.write_str("Stream(<function>)")
219 }
220}
221
222/// Field options. See [`CsvOptions`].
223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
224#[serde(default)]
225pub struct FieldOptions {
226 /// The field separator, when it is not a comma. May be several
227 /// characters (`"~~"`).
228 pub separation: Option<String>,
229 /// The key prefix for a field beyond the header's columns: the third
230 /// field of a two-column document is `field~2`.
231 pub nonameprefix: String,
232 /// The value an empty field takes. Any JSON value; the default is the
233 /// empty string.
234 pub empty: serde_json::Value,
235 /// Explicit column names, used in place of a header row.
236 pub names: Option<Vec<String>>,
237 /// Fail the parse with `csv_extra_field` or `csv_missing_field` when a
238 /// row's field count differs from the header's (or from
239 /// `names.len()`). Inert without a known field list.
240 pub exact: bool,
241}
242
243impl Default for FieldOptions {
244 fn default() -> Self {
245 FieldOptions {
246 separation: None,
247 nonameprefix: "field~".to_string(),
248 empty: serde_json::Value::String(String::new()),
249 names: None,
250 exact: false,
251 }
252 }
253}
254
255/// Record options. See [`CsvOptions`].
256#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
257#[serde(default)]
258pub struct RecordOptions {
259 /// The record separator characters, when they are not the default
260 /// `\n` / `\r\n` / `\r`.
261 pub separators: Option<String>,
262 /// Keep an empty line as a record of empty fields, rather than
263 /// skipping it.
264 pub empty: bool,
265}
266
267/// String options. See [`CsvOptions`].
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
269#[serde(default)]
270pub struct StringOptions {
271 /// The quote character of a quoted field.
272 pub quote: String,
273 /// Whether to use the RFC 4180 `""`-escaping string matcher. `None`
274 /// means on in strict mode and off in non-strict mode; `Some(false)`
275 /// in strict mode leaves quoted fields to the jsonic string matcher
276 /// with its backslash escapes.
277 pub csv: Option<bool>,
278}
279
280impl Default for StringOptions {
281 fn default() -> Self {
282 StringOptions {
283 quote: "\"".to_string(),
284 csv: None,
285 }
286 }
287}
288
289/// The plugin options: the counterpart of the TypeScript `CsvOptions` and
290/// `Csv.defaults`, and of the Go `Defaults` map. [`Default`] is the
291/// documented default set.
292///
293/// The three-state options (`trim`, `comment`, `number`, `value`) are
294/// `Option<bool>`: `None` is the TypeScript `null`, which means "off in
295/// strict mode, on in non-strict mode".
296#[derive(Debug, Clone, Serialize, Deserialize)]
297#[serde(default)]
298pub struct CsvOptions {
299 /// Drop the whitespace around an unquoted field.
300 pub trim: Option<bool>,
301 /// Lex `#` comments, so a comment line is no record and a trailing
302 /// comment is not field text.
303 pub comment: Option<bool>,
304 /// Lex numbers, so `1e2` is the number `100` rather than text.
305 pub number: Option<bool>,
306 /// Lex the value keywords, so `true`, `false` and `null` are values
307 /// rather than text.
308 pub value: Option<bool>,
309 /// The first row names the columns.
310 pub header: bool,
311 /// Each record is an object keyed by column name; `false` gives an
312 /// array of field values.
313 pub object: bool,
314 /// Every field is raw text. `false` lets a field body hold embedded
315 /// jsonic (`[1,2]`, `{x:1}`, a backslash-escaped string) and turns
316 /// `trim`, `comment`, `number` and `value` on unless set.
317 pub strict: bool,
318 /// Field options.
319 pub field: FieldOptions,
320 /// Record options.
321 pub record: RecordOptions,
322 /// String options.
323 pub string: StringOptions,
324 /// Stream records to a callback instead of collecting them. A closure
325 /// cannot travel in the plugin option bag, so it is carried here,
326 /// outside serialization, and reaches the plugin through
327 /// [`plugin_with`] and [`make_with`].
328 #[serde(skip)]
329 pub stream: Option<Stream>,
330}
331
332impl Default for CsvOptions {
333 fn default() -> Self {
334 CsvOptions {
335 trim: None,
336 comment: None,
337 number: None,
338 value: None,
339 header: true,
340 object: true,
341 strict: true,
342 field: FieldOptions::default(),
343 record: RecordOptions::default(),
344 string: StringOptions::default(),
345 stream: None,
346 }
347 }
348}
349
350impl CsvOptions {
351 /// Read options out of a plugin option bag, the form
352 /// [`Tabnas::use_plugin`] merges and hands to the plugin. A missing key
353 /// takes its default; an unknown key is ignored, as in the other two
354 /// runtimes.
355 pub fn from_value(value: &Value) -> Result<Self, String> {
356 serde_json::from_value(value.to_json()).map_err(|error| format!("csv options: {error}"))
357 }
358
359 /// The options as a plugin option bag, the form [`Plugin::with_defaults`]
360 /// and [`Tabnas::use_plugin`] take. The `stream` callback does not
361 /// travel; see [`plugin_with`].
362 pub fn to_value(&self) -> Value {
363 let json = serde_json::to_value(self).expect("the option struct serializes");
364 Value::from_json(&json)
365 }
366}
367
368/// The settings the closures capture: the options normalised the way the
369/// TypeScript plugin normalises them at the top of `Csv`.
370#[derive(Clone)]
371struct Settings {
372 strict: bool,
373 objres: bool,
374 header: bool,
375 trim: bool,
376 record_empty: bool,
377 empty: Value,
378 nonameprefix: String,
379 names: Option<Vec<String>>,
380 exact: bool,
381 stream: Option<Stream>,
382}
383
384// ---------------------------------------------------------------------------
385// Node helpers
386// ---------------------------------------------------------------------------
387
388/// Assign a rule's node: the Rust spelling of TypeScript `r.node = v`.
389///
390/// A pushed or replaced rule SHARES its parent's node cell, so writing
391/// through `rule.node.borrow_mut()` would overwrite the parent's node too.
392/// Assigning installs a fresh cell instead. The cell is borrowed directly
393/// only to mutate a container the rule genuinely shares: pushing a field
394/// onto the enclosing record, or a record onto the document.
395fn set_node(rule: &mut Rule, value: Value) {
396 rule.node = Rc::new(RefCell::new(value));
397}
398
399fn list_push(node: &mut Value, value: Value) {
400 match node {
401 Value::Array(items) => Arc::make_mut(items).push(value),
402 Value::ListRef(list) => Arc::make_mut(list).value.push(value),
403 _ => {}
404 }
405}
406
407fn items_of(node: &Value) -> Vec<Value> {
408 match node {
409 Value::Array(items) => items.as_ref().clone(),
410 Value::ListRef(list) => list.value.clone(),
411 _ => Vec::new(),
412 }
413}
414
415fn flag(rule: &Rule, name: &str) -> bool {
416 matches!(rule.u.get(name), Some(Value::Bool(true)))
417}
418
419fn counter(rule: &Rule, name: &str) -> i32 {
420 rule.n.get(name).copied().unwrap_or(0)
421}
422
423/// JavaScript's `Number::toString` (ECMA-262 6.1.6.1.20), which is what
424/// the canonical TypeScript `'' + value` spells, and so what a numeric cell
425/// becomes as field text or as a column name.
426///
427/// Rust's own `f64` formatting differs from it in three ways that reach a
428/// header key. It keeps the sign of negative zero (`-0`, where JavaScript
429/// says `0`). It never switches to exponent form, where JavaScript does so
430/// at `1e21` and at `1e-7`. And `format!("{n:.0}")` prints a large integral
431/// float's exact binary value (`123456789012345683968`) rather than its
432/// shortest round-tripping digits (`123456789012345680000`).
433fn js_number_to_string(number: f64) -> String {
434 if number.is_nan() {
435 return "NaN".to_string();
436 }
437 // Catches -0.0 as well: JavaScript spells both zeros "0".
438 if number == 0.0 {
439 return "0".to_string();
440 }
441 if number < 0.0 {
442 return format!("-{}", js_number_to_string(-number));
443 }
444 if number.is_infinite() {
445 return "Infinity".to_string();
446 }
447
448 // The specification wants the shortest digit string `s` that round-trips
449 // (length `k`), and `n`, the position of the decimal point relative to
450 // it. Rust's `{:e}` yields digits of exactly that shortest length.
451 let shortest = format!("{number:e}");
452 let shortest_k = shortest
453 .split_once('e')
454 .map(|(mantissa, _)| mantissa.chars().filter(char::is_ascii_digit).count())
455 .expect("a finite f64 always formats with an exponent");
456
457 // Re-render to that same length to settle a tie. Where two digit
458 // strings of length `k` are equally close to `number`, the
459 // specification takes the one ending in an even digit; Rust's shortest
460 // form does not, but its exactly-rounded fixed-precision form does.
461 let exponential = format!("{:.*e}", shortest_k - 1, number);
462 let (mantissa, exponent) = exponential
463 .split_once('e')
464 .expect("a finite f64 always formats with an exponent");
465 // Rounding can leave trailing zeros (and, on a carry, one digit too
466 // many); dropping them keeps `s` shortest, which is what `k` means.
467 let digits = mantissa
468 .chars()
469 .filter(|digit| *digit != '.')
470 .collect::<String>();
471 let digits = digits.trim_end_matches('0');
472 let digits = if digits.is_empty() { "0" } else { digits };
473 let k = digits.len() as i32;
474 let n = exponent
475 .parse::<i32>()
476 .expect("a formatted exponent is an integer")
477 + 1;
478
479 // The four cases of the specification, in its order. The range bounds
480 // are `k <= n <= 21`, `0 < n <= 21` and `-6 < n <= 0`.
481 if (k..=21).contains(&n) {
482 // Integral, with n - k trailing zeros to restore.
483 let mut text = digits.to_string();
484 text.push_str(&"0".repeat((n - k) as usize));
485 text
486 } else if (1..=21).contains(&n) {
487 let point = n as usize;
488 format!("{}.{}", &digits[..point], &digits[point..])
489 } else if (-5..=0).contains(&n) {
490 format!("0.{}{}", "0".repeat(-n as usize), digits)
491 } else {
492 // Exponent form. `n - 1` is never 0 here, so the sign is never "+0".
493 let sign = if n - 1 < 0 { '-' } else { '+' };
494 let power = (n - 1).abs();
495 if k == 1 {
496 format!("{digits}e{sign}{power}")
497 } else {
498 format!("{}.{}e{sign}{power}", &digits[..1], &digits[1..])
499 }
500 }
501}
502
503/// A JavaScript `'' + value`: what the text actions concatenate. Text and
504/// string tokens carry their text; a number or keyword (`number` /
505/// `value` on) spells itself the way JavaScript does.
506fn value_text(value: &Value) -> String {
507 match value {
508 Value::Undefined => String::new(),
509 Value::Null => "null".to_string(),
510 Value::Bool(flag) => flag.to_string(),
511 Value::Number(number) => js_number_to_string(*number),
512 Value::String(text) => text.clone(),
513 Value::Text(text) => text.string.clone(),
514 other => other.to_json().to_string(),
515 }
516}
517
518/// A token's value as text (`r.o0.val`), or its source when it carries no
519/// value at all.
520fn token_text(token: &Token) -> String {
521 if token.val.is_undefined() {
522 token.src.as_str().to_string()
523 } else {
524 value_text(&token.val)
525 }
526}
527
528/// A header cell as a column name: `obj[cell] = ...` applies
529/// ToPropertyKey, which for anything but a symbol is ToString.
530///
531/// Called from the one place the canonical converts a cell, the object
532/// shaping in [`record_before_close`], and never on the header row
533/// itself, which is stored raw. A reading that takes no name from a cell
534/// -- `object: false`, `header: false`, a header-only document, or a
535/// `field.exact` mismatch, which is checked first -- never reaches this.
536///
537/// `None` means JavaScript cannot make a primitive of the value at all,
538/// which is every OBJECT. A jsonic object is allocated with a null
539/// prototype, so it inherits neither `toString` nor `Symbol.toPrimitive`
540/// and the canonical runtime throws
541/// `TypeError: Cannot convert object to primitive value` instead of
542/// naming the column. This port cannot raise a JavaScript `TypeError`, so
543/// it refuses the document; `DIVERGENCE.md` records that choice.
544///
545/// That holds for a PARSED object. An object supplied as an OPTION value
546/// is the other case, and the canonical answers it differently: the
547/// option merge rebuilds a plain source object onto `Object.prototype` on
548/// the way into the bag, so `String` names the column `[object Object]`
549/// there and nothing throws, even for one the caller made with
550/// `Object.create(null)`.
551///
552/// `from_option` is which of the two this cell is, and the caller knows
553/// because there is exactly one option that can put a value in a header
554/// row: `field.empty`, dropped into a syntactically empty cell before any
555/// rule runs, so `,a` under `{field: {empty: <value>}}` takes its first
556/// column name from it. The flag is set where the options are READ rather
557/// than guessed from the value, because a `Value::Object` is the same
558/// type either way, and it travels INTO [`join_text`], so an object
559/// nested in an option array is answered the same as one at the top.
560///
561/// Falling through to [`value_text`] is what made the refusal necessary
562/// in the first place: its last-resort arm renders the value as JSON,
563/// which named the column `{"x":1.0}` for `{x:1}` and `[1.0,2.0]` for
564/// `[1,2]`, neither of which the canonical runtime produces for any
565/// input.
566fn key_text(value: &Value, from_option: bool) -> Option<String> {
567 match value {
568 Value::Undefined
569 | Value::Null
570 | Value::Bool(_)
571 | Value::Number(_)
572 | Value::String(_)
573 | Value::Text(_) => Some(value_text(value)),
574 Value::Array(items) => join_text(items, from_option),
575 Value::Object(_) if from_option => Some("[object Object]".to_string()),
576 // A PARSED object, and the two reference wrappers the engine keeps
577 // for its own bookkeeping, have no ToString to apply.
578 _ => None,
579 }
580}
581
582/// Whether two values are the SAME container, rather than two containers
583/// that compare equal. It is how a cell is recognised as the one
584/// `field.empty` supplied: an option value is cloned into the cell, and a
585/// clone of a container is a refcount bump on the same allocation, while
586/// the parser allocates its own. Equality would not do, because a parsed
587/// `{q:1}` equals an option `{q:1}` and only one of them may be named.
588fn same_container(value: &Value, other: &Value) -> bool {
589 match (value, other) {
590 (Value::Object(left), Value::Object(right)) => Arc::ptr_eq(left, right),
591 (Value::Array(left), Value::Array(right)) => Arc::ptr_eq(left, right),
592 _ => false,
593 }
594}
595
596/// `Array.prototype.toString`, which is `join(',')` with no separator
597/// argument (ECMA-262 23.1.3.17 and 23.1.3.34): every element is
598/// converted by the same rules, `null` and `undefined` become the empty
599/// string, and a nested array joins recursively, so `[1,[2,3]]` flattens
600/// to `1,2,3` and `[]` is the empty string.
601///
602/// A null ELEMENT is the empty string while a null CELL is `null`: the
603/// empty string comes from `join`, not from ToString, so it applies only
604/// inside an array.
605///
606/// The recursion needs no depth bound and no seen-set. A parsed value is
607/// a TREE: the engine folds each finished rule's value into its parent
608/// and never stores a reference to an ancestor, so no element can reach
609/// its own array and the walk always terminates. Its depth is the
610/// document's bracket nesting, which `tabnas-jsonic` already bounds at
611/// 127 containers with the parse budget this plugin inherits.
612///
613/// An OPTION value does reach this, through `field.empty`, and it cannot
614/// contain itself either, which is where the Go port differs.
615/// `field.empty` is a `serde_json::Value` and `field.names` a
616/// `Vec<String>`, both owned acyclic trees. The engine's own `Value`
617/// shares containers behind an `Arc`, but building a cycle out of one
618/// needs a second handle on a container while its refcount is still 1,
619/// which the type does not allow. Go's option is an unconstrained `any`,
620/// a caller can spell `a := make([]any, 1); a[0] = a`, and there the same
621/// walk had to be bounded.
622///
623/// `from_option` travels from [`key_text`], so an object nested in an
624/// option array is named `[object Object]` where a parsed one refuses.
625fn join_text(items: &[Value], from_option: bool) -> Option<String> {
626 let mut joined = String::new();
627 for (index, item) in items.iter().enumerate() {
628 if 0 < index {
629 joined.push(',');
630 }
631 match item {
632 Value::Null | Value::Undefined => {}
633 other => joined.push_str(&key_text(other, from_option)?),
634 }
635 }
636 Some(joined)
637}
638
639fn number(value: usize) -> Value {
640 Value::Number(value as f64)
641}
642
643// ---------------------------------------------------------------------------
644// The closures the grammar names
645// ---------------------------------------------------------------------------
646
647/// The text of the previous text rule's node: the accumulator a chained
648/// text rule (`r: text`) extends.
649fn prev_text(rule: &Rule) -> String {
650 rule.prev_rule
651 .as_ref()
652 .map(|prev| value_text(&prev.node.borrow()))
653 .unwrap_or_default()
654}
655
656/// `r.node = v.node = text`, where `v` is the rule itself on the first
657/// text rule of a chain and the previous one afterwards.
658fn assign_text(rule: &mut Rule, chained: bool, text: String) {
659 let value = Value::String(text);
660 if chained {
661 if let Some(prev) = rule.prev_rule.as_ref() {
662 *prev.node.borrow_mut() = value.clone();
663 }
664 }
665 set_node(rule, value);
666}
667
668/// Text then space: `x ` continues a field, so keep the text and read on.
669fn text_follows(rule: &mut Rule, _context: &mut Context) -> Result<(), ActionError> {
670 let chained = counter(rule, "text") != 1;
671 let head = if chained {
672 prev_text(rule)
673 } else {
674 String::new()
675 };
676 let text = head + &rule.o0().map(token_text).unwrap_or_default();
677 assign_text(rule, chained, text);
678 Ok(())
679}
680
681/// Space then text: the space is field text unless it leads a trimmed
682/// field.
683fn text_leads(trim: bool) -> impl Fn(&mut Rule, &mut Context) -> Result<(), ActionError> {
684 move |rule, _context| {
685 let count = counter(rule, "text");
686 let chained = count != 1;
687 let head = if chained {
688 prev_text(rule)
689 } else {
690 String::new()
691 };
692 let space = if 2 <= count || !trim {
693 rule.o0().map(|token| token.src.as_str().to_string())
694 } else {
695 None
696 };
697 let text = head
698 + &space.unwrap_or_default()
699 + &rule
700 .o1()
701 .map(|token| token.src.as_str().to_string())
702 .unwrap_or_default();
703 assign_text(rule, chained, text);
704 Ok(())
705 }
706}
707
708/// Trailing space before a separator, line end or end of source: field
709/// text unless trimmed.
710fn text_end(trim: bool) -> impl Fn(&mut Rule, &mut Context) -> Result<(), ActionError> {
711 move |rule, _context| {
712 let chained = counter(rule, "text") != 1;
713 let head = if chained {
714 prev_text(rule)
715 } else {
716 String::new()
717 };
718 let space = if trim {
719 None
720 } else {
721 rule.o0().map(|token| token.src.as_str().to_string())
722 };
723 assign_text(rule, chained, head + &space.unwrap_or_default());
724 Ok(())
725 }
726}
727
728/// A lone space: in strict mode it is field text (unless trimmed); in
729/// non-strict mode the action does nothing and the alternate pushes `val`
730/// to read an embedded value.
731fn text_space(
732 strict: bool,
733 trim: bool,
734) -> impl Fn(&mut Rule, &mut Context) -> Result<(), ActionError> {
735 let end = text_end(trim);
736 move |rule, context| {
737 if strict {
738 end(rule, context)
739 } else {
740 Ok(())
741 }
742 }
743}
744
745/// `text` before-close: hand the field text (or the embedded value a
746/// pushed `val` produced) to the enclosing `val`. This is `r.parent.node =
747/// ...` in the canonical grammar, and it writes into the parent's cell.
748fn text_before_close(rule: &mut Rule, _context: &mut Context) -> Result<(), ActionError> {
749 let value = if rule.child_node.is_undefined() {
750 rule.node.borrow().clone()
751 } else {
752 rule.child_node.clone()
753 };
754 if let Some(parent) = rule.parent_node.as_ref() {
755 *parent.borrow_mut() = value;
756 }
757 Ok(())
758}
759
760fn record_index(context: &Context) -> usize {
761 match context.u.get("recordI") {
762 Some(Value::Number(index)) => *index as usize,
763 _ => 0,
764 }
765}
766
767/// `record` before-close: the header row names the columns; every later
768/// row becomes a record, checked against the field count under
769/// `field.exact`, shaped by `object`, and either streamed or pushed onto
770/// the document.
771fn record_before_close(
772 settings: Settings,
773) -> impl Fn(
774 &mut Rule,
775 &mut Context,
776 Option<&tabnas::RuleSnapshot>,
777 Option<Token>,
778) -> Result<Option<Token>, ActionError> {
779 move |rule, context, _next, out| {
780 let record_i = record_index(context);
781 // The field list is held as the RAW cells, the way the canonical
782 // holds `ctx.u.fields`, because a cell becomes a column NAME only
783 // where an object record is built. `field.names` widens into the
784 // same shape so the two sources stay interchangeable.
785 // `fields_from_options` is the provenance of the WHOLE list: a
786 // header row is parsed, `field.names` is an option. A parsed
787 // header row can still hold an option value, because `field.empty`
788 // is dropped into an empty cell before any rule runs, so the flag
789 // is taken per cell below as well.
790 let (fields, fields_from_options): (Option<Vec<Value>>, bool) =
791 match context.u.get("fields") {
792 Some(Value::Array(names)) => (Some(names.as_ref().clone()), false),
793 _ => (
794 settings.names.as_ref().map(|names| {
795 names
796 .iter()
797 .map(|name| Value::String(name.clone()))
798 .collect()
799 }),
800 true,
801 ),
802 };
803
804 if record_i == 0 && settings.header {
805 // Kept exactly as parsed, which is what the canonical keeps:
806 // `ctx.u.fields = r.child.node`. Nothing is converted here,
807 // and an empty list is still a field list, as it is in
808 // JavaScript where `[]` is truthy.
809 context.u.insert(
810 "fields".to_string(),
811 Value::array(items_of(&rule.child_node)),
812 );
813 } else {
814 let mut record = items_of(&rule.child_node);
815
816 // The field-count check is independent of the result SHAPE: it
817 // only needs a known field list (from the header row or
818 // `field.names`), so it fires for arrays as well as objects.
819 if let Some(fields) = fields.as_ref() {
820 if settings.exact && record.len() != fields.len() {
821 let code = if record.len() > fields.len() {
822 "csv_extra_field"
823 } else {
824 "csv_missing_field"
825 };
826 // The messages interpolate {row}, {len} and {fsrc}, so
827 // the details are supplied. `row` counts from 1 and
828 // includes the header line, so it matches the line
829 // number a spreadsheet or editor reports.
830 let mut details = vec![
831 ("row".to_string(), number(record_i + 1)),
832 ("len".to_string(), number(fields.len())),
833 ];
834 if let Some(extra) = record.get(fields.len()) {
835 details.push(("fsrc".to_string(), extra.clone()));
836 }
837 let mut token = context.t0().cloned().unwrap_or_else(|| {
838 Token::new("#BD", TIN_BD, Value::Undefined, "", Default::default())
839 });
840 token.bad_with_details(code, details);
841 return Ok(Some(token));
842 }
843 }
844
845 let filled = |cell: Option<&Value>| match cell {
846 Some(value) if !value.is_undefined() => value.clone(),
847 _ => settings.empty.clone(),
848 };
849
850 let shaped = if settings.objres {
851 let mut object = IndexMap::new();
852 let mut index = 0;
853 if let Some(fields) = fields.as_ref() {
854 for (position, cell) in fields.iter().enumerate() {
855 // This is the ONE place a header cell becomes a
856 // column name, and the only place the canonical
857 // converts one: `obj[fields[fI]] = ...` puts the
858 // raw cell through ToPropertyKey, which for
859 // anything but a symbol is ToString.
860 //
861 // A PARSED object has no ToString at all, and
862 // the canonical runtime throws a TypeError here
863 // rather than naming the column. This port cannot
864 // raise one, so it refuses the document with the
865 // engine's inherited `unexpected` code instead of
866 // inventing a name; `DIVERGENCE.md` records that
867 // choice. Refusing at the header row instead was
868 // too early: it also refused an `object: false`
869 // parse and a header-only document, neither of
870 // which ever asks for a name, and both of which
871 // the canonical returns a value for.
872 //
873 // An object an OPTION supplied is named
874 // `[object Object]`, which is what the canonical
875 // does for it, and `from_option` is how the two
876 // are told apart. The type cannot tell them
877 // apart: both are `Value::Object`.
878 let from_option =
879 fields_from_options || same_container(cell, &settings.empty);
880 let Some(name) = key_text(cell, from_option) else {
881 let mut token = context.t0().cloned().unwrap_or_else(|| {
882 Token::new("#BD", TIN_BD, Value::Undefined, "", Default::default())
883 });
884 token.bad("unexpected");
885 return Ok(Some(token));
886 };
887 object.insert(name, filled(record.get(position)));
888 }
889 index = fields.len();
890 }
891 while index < record.len() {
892 object.insert(
893 format!("{}{index}", settings.nonameprefix),
894 filled(record.get(index)),
895 );
896 index += 1;
897 }
898 Value::object(object)
899 } else {
900 for cell in record.iter_mut() {
901 if cell.is_undefined() {
902 *cell = settings.empty.clone();
903 }
904 }
905 Value::array(record)
906 };
907
908 match settings.stream.as_ref() {
909 Some(stream) => stream.send(StreamEvent::Record(shaped)),
910 None => list_push(&mut rule.node.borrow_mut(), shaped),
911 }
912 }
913
914 context
915 .u
916 .insert("recordI".to_string(), number(record_i + 1));
917 Ok(out)
918 }
919}
920
921/// Register every closure the grammar names, and the string matcher
922/// factory. Before the documents, because the documents look them up.
923fn register_refs(parser: &mut Tabnas, settings: &Settings, quote: &str) {
924 let stream = settings.stream.clone();
925 parser.state_action_ref(CSV_BO, move |rule, context| {
926 context.u.insert("recordI".to_string(), number(0));
927 if let Some(stream) = stream.as_ref() {
928 stream.send(StreamEvent::Start);
929 }
930 set_node(rule, Value::array(Vec::new()));
931 Ok(())
932 });
933
934 let stream = settings.stream.clone();
935 parser.state_action_ref(CSV_AC, move |_rule, _context| {
936 if let Some(stream) = stream.as_ref() {
937 stream.send(StreamEvent::End);
938 }
939 Ok(())
940 });
941
942 parser.state_action_with_next_ref(RECORD_BC, record_before_close(settings.clone()));
943 parser.state_action_ref(TEXT_BC, text_before_close);
944
945 parser.action_with_context(TEXT_FOLLOWS, text_follows);
946 parser.action_with_context(TEXT_LEADS, text_leads(settings.trim));
947 parser.action_with_context(TEXT_END, text_end(settings.trim));
948 parser.action_with_context(TEXT_SPACE, text_space(settings.strict, settings.trim));
949
950 let record_empty = settings.record_empty;
951 parser.alt_condition(NOT_RECORD_EMPTY, move |_rule, _context| !record_empty);
952 parser.alt_replace(RECORD_CLOSE_NEXT, move |_rule, _context| {
953 Some(if record_empty { "record" } else { "newline" }.to_string())
954 });
955
956 let strict = settings.strict;
957 parser.alt_push(TEXT_SPACE_PUSH, move |_rule, _context| {
958 Some(if strict { "" } else { "val" }.to_string())
959 });
960
961 parser.lex_match_factory_ref(CSV_STRING, csv_string_matcher(quote));
962}
963
964// ---------------------------------------------------------------------------
965// The RFC 4180 string matcher
966// ---------------------------------------------------------------------------
967
968/// The custom CSV string matcher factory: `"a""b"` is the text `a"b`.
969///
970/// A reduced copy of the standard jsonic string matcher, with the doubled
971/// quote as the only escape. It is the counterpart of the TypeScript
972/// `buildCsvStringMatcher` export and the Go `BuildCsvStringMatcher`, in
973/// the shape the engine's `options.lex.match.<name>.make` reference takes:
974/// register it with [`Tabnas::lex_match_factory_ref`] and name it from an
975/// options document.
976///
977/// ```
978/// use tabnas::GrammarSpec;
979///
980/// let mut parser = tabnas_jsonic::make();
981/// parser.lex_match_factory_ref("@csv-string", tabnas_csv::csv_string_matcher("\""));
982/// let spec = GrammarSpec::from_value(serde_json::json!({
983/// "options": { "lex": { "match": {
984/// "stringcsv": { "order": 100000, "make": "@csv-string" },
985/// } } },
986/// }))?;
987/// parser.grammar(&spec)?;
988/// assert_eq!(parser.parse(r#""a""b""#)?, tabnas::Value::String("a\"b".to_string()));
989/// # Ok::<(), Box<dyn std::error::Error>>(())
990/// ```
991///
992/// The matcher runs only where the engine wants a string token, and only
993/// at the quote character. A line character inside the quotes is field
994/// text (a quoted field may span lines). Any other control character is
995/// field text too when the engine's `string.allowControl` option is on,
996/// as the CSV plugin sets it, and `unprintable` when it is off, as it is
997/// by default on a plain instance. A quote left open at the end of the
998/// source is `unterminated_string`. Loop exhaustion is what detects the
999/// open quote, so an odd number of quotes (`"""`, `"""""`) is caught
1000/// rather than read as a terminated string.
1001///
1002/// A quote that is not exactly one UTF-16 code unit leaves the matcher
1003/// inert, and the factory answers `None` rather than installing one. The
1004/// canonical opens a quoted field with
1005/// `quoteMap = { [options.string.quote]: true }` and tests
1006/// `quoteMap[src[sI]]`, where `src[sI]` is ONE UTF-16 code unit, so it
1007/// can only ever fire for a quote that is one code unit: the empty
1008/// string, a multi-character quote such as `ab`, and an astral character
1009/// such as U+1F600, which JavaScript holds as a surrogate PAIR, are all
1010/// inert there. Go carries the same guard, with the same reasoning.
1011pub fn csv_string_matcher(
1012 quote: impl Into<String>,
1013) -> impl Fn(&Options) -> Option<ImperativeLexMatcher> + Send + Sync + 'static {
1014 let quote: String = quote.into();
1015 let single_code_unit = {
1016 let mut characters = quote.chars();
1017 match (characters.next(), characters.next()) {
1018 (Some(character), None) => character.len_utf16() == 1,
1019 _ => false,
1020 }
1021 };
1022 move |options: &Options| {
1023 if !single_code_unit {
1024 return None;
1025 }
1026 let quote = quote.clone();
1027 let line_chars: Vec<char> = options.line.chars.chars().collect();
1028 let allow_control = options.string.allow_control;
1029 Some(Arc::new(
1030 move |lexer: &mut tabnas::Lexer<'_>, _rule: &mut Rule, _context: &mut Context| {
1031 let rest = lexer.remaining();
1032 if !rest.starts_with(quote.as_str()) {
1033 return None;
1034 }
1035 let point = lexer.point();
1036 let mut text = String::new();
1037 let mut at = quote.len();
1038 let mut closed = false;
1039
1040 while at < rest.len() {
1041 let tail = &rest[at..];
1042 if tail.starts_with(quote.as_str()) {
1043 at += quote.len();
1044 // A doubled quote is one literal quote; a single
1045 // one closes the field.
1046 if rest[at..].starts_with(quote.as_str()) {
1047 text.push_str("e);
1048 at += quote.len();
1049 } else {
1050 closed = true;
1051 break;
1052 }
1053 continue;
1054 }
1055 let character = tail.chars().next().expect("inside the source");
1056 if line_chars.contains(&character) {
1057 text.push(character);
1058 } else if (character as u32) < 32 && !allow_control {
1059 // Any other control character is field text when
1060 // the engine's `string.allowControl` is on, which
1061 // the plugin turns on; only with it off is the
1062 // character refused. The diagnostic points at the
1063 // control character, as it does in TypeScript.
1064 lexer.advance_chars(rest[..at].chars().count());
1065 return Some(lexer.bad("unprintable"));
1066 } else {
1067 text.push(character);
1068 }
1069 at += character.len_utf8();
1070 }
1071
1072 if !closed {
1073 let start = point.site.pos;
1074 let end = start + rest[..at].chars().count();
1075 return Some(lexer.bad_span("unterminated_string", start, end));
1076 }
1077
1078 let source = rest[..at].to_string();
1079 let token = lexer.token("#ST", TIN_ST, Value::String(text), source.as_str(), point);
1080 lexer.advance_chars(source.chars().count());
1081 Some(token)
1082 },
1083 ))
1084 }
1085}
1086
1087// ---------------------------------------------------------------------------
1088// The documents
1089// ---------------------------------------------------------------------------
1090
1091/// Whole numbers as integers. The grammar text is parsed by jsonic, whose
1092/// numbers are all `f64`, and the engine's document loader reads an
1093/// alternate's `b` and `n` as integers.
1094fn integral(value: &mut serde_json::Value) {
1095 match value {
1096 serde_json::Value::Number(number) => {
1097 if let Some(float) = number.as_f64() {
1098 if float.fract() == 0.0 && float.abs() < 9.0e15 {
1099 if float < 0.0 {
1100 *value = serde_json::Value::from(float as i64);
1101 } else {
1102 *value = serde_json::Value::from(float as u64);
1103 }
1104 }
1105 }
1106 }
1107 serde_json::Value::Array(items) => items.iter_mut().for_each(integral),
1108 serde_json::Value::Object(entries) => entries.values_mut().for_each(integral),
1109 _ => {}
1110 }
1111}
1112
1113/// The embedded grammar as a serialized document: parsed by a standard
1114/// jsonic instance, as the TypeScript `new Tabnas().use(jsonic).parse(
1115/// grammarText)` and the Go `parseGrammarText` do.
1116fn grammar_document() -> Result<serde_json::Value, PluginError> {
1117 let parsed = tabnas_jsonic::parse(GRAMMAR_TEXT).map_err(|error| {
1118 PluginError(format!("csv: the embedded grammar does not parse: {error}"))
1119 })?;
1120 let mut document = parsed.to_json();
1121 if !document.is_object() || document.get("rule").is_none() {
1122 return Err(PluginError(
1123 "csv: the embedded grammar has no `rule` table".to_string(),
1124 ));
1125 }
1126 integral(&mut document);
1127 Ok(document)
1128}
1129
1130/// The per-instance engine options, mirroring `jsonicOptions` in
1131/// `ts/src/csv.ts`. The static options (`rule.start`, `lex.emptyResult`,
1132/// `error`, `hint`) live in the grammar text.
1133fn options_document(
1134 options: &CsvOptions,
1135 settings: &Settings,
1136 flags: &LexFlags,
1137) -> serde_json::Value {
1138 let strict = settings.strict;
1139
1140 // Fixed-token overrides: in strict mode the JSON structural tokens and
1141 // the `:` key separator are switched off; a configured field separator
1142 // rebinds `#CA`.
1143 let mut fixed = serde_json::Map::new();
1144 if strict {
1145 for name in ["#OB", "#CB", "#OS", "#CS", "#CL"] {
1146 fixed.insert(name.to_string(), serde_json::Value::Null);
1147 }
1148 }
1149 if let Some(separation) = options
1150 .field
1151 .separation
1152 .as_deref()
1153 .filter(|separation| !separation.is_empty())
1154 {
1155 fixed.insert("#CA".to_string(), json!(separation));
1156 }
1157
1158 // IGNORE set: `#LN` leaves it so row breaks are significant; in strict
1159 // mode `#SP` leaves it too so whitespace inside a field survives. This
1160 // engine merges a token set INDEX-WISE onto the one already installed,
1161 // as the canonical TypeScript deep merge does: a `null` drops that
1162 // position and a shorter array keeps the tail of the set it overlays.
1163 // So the override is spelled position for position over the jsonic
1164 // default `[#SP, #LN, #CM]`, exactly as `ts/src/csv.ts` spells it, and
1165 // the trailing position is named rather than left off: an override that
1166 // stops short of the installed length would keep whatever the tail
1167 // holds, and this one has to answer the same on a derived instance,
1168 // where the set it overlays is already the reduced one.
1169 let ignore = if strict {
1170 json!([null, null, "#CM"])
1171 } else {
1172 json!(["#SP", null, "#CM"])
1173 };
1174
1175 let mut line = json!({ "single": settings.record_empty });
1176 if let Some(separators) = options.record.separators.as_deref() {
1177 line["chars"] = json!(separators);
1178 line["rowChars"] = json!(separators);
1179 }
1180
1181 let mut document = json!({
1182 "options": {
1183 "rule": { "exclude": if strict { "jsonic,imp" } else { "imp" } },
1184 "fixed": { "token": fixed },
1185 "tokenSet": { "IGNORE": ignore },
1186 "number": { "lex": flags.number },
1187 "value": { "lex": flags.value },
1188 "comment": { "lex": flags.comment },
1189 // A quoted field holds every character but an unescaped quote,
1190 // the C0 controls included, as other CSV readers accept them:
1191 // the CSV renderer writes a cell holding a TAB, an ASCII 30 or
1192 // an ASCII 31 between quotes, because CSV has no other spelling
1193 // for it. The jsonic string matcher, which reads the quote
1194 // characters the CSV matcher does not own and every quote under
1195 // `string.csv: false` or in non-strict mode, refuses a raw
1196 // control as `unprintable` unless this is on, and
1197 // `csv_string_matcher` honours the same option.
1198 "string": { "allowControl": true },
1199 "line": line,
1200 },
1201 });
1202 if flags.csv_strings {
1203 document["options"]["lex"] = json!({
1204 "match": {
1205 "stringcsv": { "order": CSV_STRING_ORDER, "make": CSV_STRING },
1206 },
1207 });
1208 }
1209 document
1210}
1211
1212/// The lexer switches the mode decides.
1213struct LexFlags {
1214 number: bool,
1215 value: bool,
1216 comment: bool,
1217 csv_strings: bool,
1218}
1219
1220fn apply_document(parser: &mut Tabnas, document: serde_json::Value) -> Result<(), PluginError> {
1221 let spec = GrammarSpec::from_value(document).map_err(|error| PluginError(error.0))?;
1222 parser
1223 .grammar(&spec)
1224 .map_err(|error| PluginError(error.0))?;
1225 Ok(())
1226}
1227
1228// ---------------------------------------------------------------------------
1229// The list / elem / val rules, modified in code
1230// ---------------------------------------------------------------------------
1231
1232/// An alternate over token sequences, with the fields the CSV rules use.
1233fn alt(s: Vec<Vec<Tin>>, b: usize, g: &str) -> AltSpec {
1234 let mut alt = AltSpec::new();
1235 alt.s = s;
1236 alt.b = b;
1237 alt.g = g.to_string();
1238 alt
1239}
1240
1241fn push_alt(name: &str) -> AltSpec {
1242 let mut alt = AltSpec::new();
1243 alt.p = Some(name.to_string());
1244 alt
1245}
1246
1247fn with_push(mut alt: AltSpec, name: &str) -> AltSpec {
1248 alt.p = Some(name.to_string());
1249 alt
1250}
1251
1252fn with_replace(mut alt: AltSpec, name: &str) -> AltSpec {
1253 alt.r = Some(name.to_string());
1254 alt
1255}
1256
1257/// `rs.open([...])` / `rs.close([...])` with no `append`: the whole list
1258/// goes in front of the existing alternates, in its own order.
1259fn prepend_open(spec: &mut RuleSpec, alts: Vec<AltSpec>) {
1260 for alt in alts.into_iter().rev() {
1261 spec.prepend_open(alt);
1262 }
1263}
1264
1265fn prepend_close(spec: &mut RuleSpec, alts: Vec<AltSpec>) {
1266 for alt in alts.into_iter().rev() {
1267 spec.prepend_close(alt);
1268 }
1269}
1270
1271/// The tokens the CSV rules match.
1272struct Tokens {
1273 ln: Tin,
1274 ca: Tin,
1275 sp: Tin,
1276 zz: Tin,
1277 val: Vec<Tin>,
1278}
1279
1280/// An empty element before a separator: `,` pushes the empty value and
1281/// marks the element done, so `elem` before-close does not push again.
1282fn empty_before_comma(empty: Value) -> impl Fn(&mut Rule, &mut Context) + Send + Sync + 'static {
1283 move |rule, _context| {
1284 list_push(&mut rule.node.borrow_mut(), empty.clone());
1285 rule.u_mut().insert("done".to_string(), Value::Bool(true));
1286 }
1287}
1288
1289/// An empty element at the end of the line: `,\n` / `,<eof>`.
1290fn empty_at_end(empty: Value) -> impl Fn(&mut Rule, &mut Context) + Send + Sync + 'static {
1291 move |rule, _context| {
1292 list_push(&mut rule.node.borrow_mut(), empty.clone());
1293 }
1294}
1295
1296/// Allocate the per-record field array. Under the engine's node model a
1297/// pushed child rule is seeded with the parent's node, and jsonic's
1298/// `list` before-open only allocates for an implicit list, which the CSV
1299/// `record -> list` path never is: without this the fields would land on
1300/// the inherited document array.
1301fn allocate_fields(rule: &mut Rule, _context: &mut Context) {
1302 set_node(rule, Value::array(Vec::new()));
1303}
1304
1305/// Strict mode: `list`, `elem` and `val` are replaced entirely. The JSON
1306/// structural tokens are off, so only the CSV alternates are needed.
1307fn strict_rules(parser: &mut Tabnas, tokens: &Tokens, empty: &Value) {
1308 let Tokens {
1309 ln,
1310 ca,
1311 sp,
1312 zz,
1313 val,
1314 } = tokens;
1315 let (ln, ca, sp, zz) = (*ln, *ca, *sp, *zz);
1316
1317 parser.define_rule("list", |spec| {
1318 spec.clear();
1319 spec.add_bo(allocate_fields);
1320 // Do not consume the LN that closes an empty record.
1321 spec.add_open(alt(vec![vec![ln]], 1, ""));
1322 spec.add_open(push_alt("elem"));
1323 // LN ends the record.
1324 spec.add_close(alt(vec![vec![ln]], 1, "end"));
1325 spec.add_close(alt(vec![vec![zz]], 0, "end"));
1326 });
1327
1328 let before_comma = empty.clone();
1329 let at_end = empty.clone();
1330 parser.define_rule("elem", move |spec| {
1331 spec.clear();
1332 let mut comma = alt(vec![vec![ca]], 1, "");
1333 comma.add_action(empty_before_comma(before_comma));
1334 spec.add_open(comma);
1335 spec.add_open(push_alt("val"));
1336
1337 let mut trailing = alt(vec![vec![ca], vec![ln, zz]], 1, "comma");
1338 trailing.add_action(empty_at_end(at_end));
1339 spec.add_close(trailing);
1340 // Next element.
1341 spec.add_close(with_replace(alt(vec![vec![ca]], 0, "comma"), "elem"));
1342 // LN ends the record.
1343 spec.add_close(alt(vec![vec![ln]], 1, "end"));
1344 spec.add_close(alt(vec![vec![zz]], 0, "end"));
1345
1346 // Push the parsed field value, unless an open alternate already did.
1347 spec.add_bc(|rule, _context| {
1348 if !flag(rule, "done") && !rule.child_node.is_undefined() {
1349 let child = rule.child_node.clone();
1350 list_push(&mut rule.node.borrow_mut(), child);
1351 }
1352 });
1353 });
1354
1355 let val = val.clone();
1356 parser.define_rule("val", move |spec| {
1357 spec.clear();
1358 // Reset the parent-seeded node so an empty value resolves to no
1359 // value, not to the inherited field array.
1360 spec.add_bo(|rule, _context| set_node(rule, Value::Undefined));
1361 // Text and space concatenation.
1362 spec.add_open(with_push(alt(vec![val.clone(), vec![sp]], 2, ""), "text"));
1363 spec.add_open(with_push(alt(vec![vec![sp]], 1, ""), "text"));
1364 // A plain value token.
1365 spec.add_open(alt(vec![val.clone()], 0, ""));
1366 // LN ends the record.
1367 spec.add_open(alt(vec![vec![ln]], 1, ""));
1368 // Coalesce: a child (text) node wins; else the matched scalar
1369 // token; else no value (an implicit empty field).
1370 spec.add_bc(|rule, context| {
1371 let unset = rule.node.borrow().is_undefined();
1372 if unset {
1373 if !rule.child_node.is_undefined() {
1374 let child = rule.child_node.clone();
1375 set_node(rule, child);
1376 } else if rule.os() != 0 {
1377 let value = rule.resolve_open_value(0, context);
1378 set_node(rule, value);
1379 }
1380 }
1381 });
1382 });
1383}
1384
1385/// Non-strict mode: the CSV alternates go in FRONT of jsonic's, so the
1386/// default value alternates (handling `[1,2]`, `{x:1}` and the like) stay
1387/// available for an embedded value.
1388fn relaxed_rules(parser: &mut Tabnas, tokens: &Tokens, empty: &Value) {
1389 let Tokens {
1390 ln,
1391 ca,
1392 sp,
1393 zz,
1394 val,
1395 } = tokens;
1396 let (ln, ca, sp, zz) = (*ln, *ca, *sp, *zz);
1397
1398 parser.define_rule("list", |spec| {
1399 spec.add_bo(allocate_fields);
1400 // Do not consume the LN that closes an empty record.
1401 prepend_open(spec, vec![alt(vec![vec![ln]], 1, "")]);
1402 // The elem-push fallback goes after the jsonic defaults.
1403 spec.add_open(push_alt("elem"));
1404 // LN ends the record; in front so these win over jsonic's close
1405 // alternates.
1406 prepend_close(
1407 spec,
1408 vec![alt(vec![vec![ln]], 1, "end"), alt(vec![vec![zz]], 0, "end")],
1409 );
1410 });
1411
1412 let before_comma = empty.clone();
1413 let at_end = empty.clone();
1414 parser.define_rule("elem", move |spec| {
1415 let mut comma = alt(vec![vec![ca]], 1, "");
1416 comma.add_action(empty_before_comma(before_comma));
1417 prepend_open(spec, vec![comma]);
1418
1419 let mut trailing = alt(vec![vec![ca], vec![ln, zz]], 1, "comma");
1420 trailing.add_action(empty_at_end(at_end));
1421 prepend_close(spec, vec![trailing, alt(vec![vec![ln]], 1, "end")]);
1422 });
1423
1424 let val = val.clone();
1425 parser.define_rule("val", move |spec| {
1426 prepend_open(
1427 spec,
1428 vec![
1429 with_push(alt(vec![val.clone(), vec![sp]], 2, ""), "text"),
1430 with_push(alt(vec![vec![sp]], 1, ""), "text"),
1431 alt(vec![vec![ln]], 1, ""),
1432 ],
1433 );
1434 });
1435}
1436
1437// ---------------------------------------------------------------------------
1438// Public surface
1439// ---------------------------------------------------------------------------
1440
1441/// Whether the CSV grammar is already on this instance. A plugin re-run
1442/// (a derived instance rebuilds its plugins on a fresh rule set; an
1443/// instance that already carries the grammar must not gain a second copy
1444/// of every alternate) is judged by the one rule only csv installs.
1445fn grammar_installed(parser: &Tabnas) -> bool {
1446 parser.rule_names().iter().any(|name| name == "csv")
1447}
1448
1449/// Install the CSV plugin on `parser`, which must already carry the jsonic
1450/// grammar: the counterpart of the TypeScript `Csv` plugin function and
1451/// the Go `Csv`.
1452///
1453/// ```
1454/// let mut parser = tabnas_jsonic::make();
1455/// tabnas_csv::csv(&mut parser, &tabnas_csv::CsvOptions::default())?;
1456/// let value = parser.parse("a,b\n1,2")?;
1457/// assert_eq!(value.to_json().to_string(), r#"[{"a":"1","b":"2"}]"#);
1458/// # Ok::<(), Box<dyn std::error::Error>>(())
1459/// ```
1460///
1461/// In order: the closures the grammar names are registered, the
1462/// per-instance engine options are applied, the embedded grammar is
1463/// installed with every alternate tagged `csv`, and the `list`, `elem` and
1464/// `val` rules are modified in code (replaced in strict mode, extended in
1465/// non-strict mode, so embedded jsonic values keep working).
1466pub fn csv(parser: &mut Tabnas, options: &CsvOptions) -> Result<(), PluginError> {
1467 if grammar_installed(parser) {
1468 return Ok(());
1469 }
1470
1471 let strict = options.strict;
1472
1473 // In strict mode jsonic value parsing is off and every field is raw
1474 // text. In non-strict mode the three-state options default to on.
1475 let (trim, comment, number, value) = if strict {
1476 (
1477 options.trim.unwrap_or(false),
1478 options.comment.unwrap_or(false),
1479 options.number.unwrap_or(false),
1480 options.value.unwrap_or(false),
1481 )
1482 } else {
1483 (
1484 options.trim.unwrap_or(true),
1485 options.comment.unwrap_or(true),
1486 options.number.unwrap_or(true),
1487 options.value.unwrap_or(true),
1488 )
1489 };
1490 let csv_strings = if strict {
1491 options.string.csv != Some(false)
1492 } else {
1493 options.string.csv == Some(true)
1494 };
1495
1496 let settings = Settings {
1497 strict,
1498 objres: options.object,
1499 header: options.header,
1500 trim,
1501 record_empty: options.record.empty,
1502 empty: Value::from_json(&options.field.empty),
1503 nonameprefix: options.field.nonameprefix.clone(),
1504 names: options.field.names.clone(),
1505 exact: options.field.exact,
1506 stream: options.stream.clone(),
1507 };
1508 let flags = LexFlags {
1509 number,
1510 value,
1511 comment,
1512 csv_strings,
1513 };
1514
1515 register_refs(parser, &settings, &options.string.quote);
1516 apply_document(parser, options_document(options, &settings, &flags))?;
1517
1518 let spec =
1519 GrammarSpec::from_value(grammar_document()?).map_err(|error| PluginError(error.0))?;
1520 parser
1521 .grammar_with_setting(&spec, &GrammarSetting::groups("csv"))
1522 .map_err(|error| PluginError(format!("csv: failed to apply the grammar: {}", error.0)))?;
1523
1524 // Usually [#TX, #ST, #NR, #VL].
1525 let tokens = Tokens {
1526 ln: parser.token("#LN"),
1527 ca: parser.token("#CA"),
1528 sp: parser.token("#SP"),
1529 zz: parser.token("#ZZ"),
1530 val: parser.token_set("VAL").unwrap_or_default(),
1531 };
1532 if strict {
1533 strict_rules(parser, &tokens, &settings.empty);
1534 } else {
1535 relaxed_rules(parser, &tokens, &settings.empty);
1536 }
1537 Ok(())
1538}
1539
1540/// The plugin form of [`csv`], for [`Tabnas::use_plugin`], with the
1541/// documented defaults: the counterpart of `tn.use(Csv, options)` in
1542/// TypeScript and `j.UseDefaults(Csv, Defaults, options)` in Go. The
1543/// option bag `use_plugin` takes is deep-merged over the defaults and
1544/// read as a [`CsvOptions`].
1545///
1546/// ```
1547/// use tabnas::Value;
1548///
1549/// let mut parser = tabnas_jsonic::make();
1550/// let options = Value::from_json(&serde_json::json!({ "object": false }));
1551/// parser.use_plugin(tabnas_csv::plugin(), Some(options))?;
1552/// assert_eq!(parser.parse("a,b\n1,2")?.to_json().to_string(), r#"[["1","2"]]"#);
1553/// # Ok::<(), Box<dyn std::error::Error>>(())
1554/// ```
1555pub fn plugin() -> Plugin {
1556 Plugin::new(PLUGIN_NAME, |parser, options| {
1557 let options = CsvOptions::from_value(options).map_err(PluginError)?;
1558 csv(parser, &options)
1559 })
1560 .with_defaults(CsvOptions::default().to_value())
1561}
1562
1563/// The plugin over typed options, including a [`Stream`] callback, which
1564/// cannot travel in the option bag. The bag `use_plugin` passes is
1565/// recorded on the instance as the plugin's options but the typed ones
1566/// are what the grammar is built from.
1567pub fn plugin_with(options: CsvOptions) -> Plugin {
1568 let defaults = options.to_value();
1569 Plugin::new(PLUGIN_NAME, move |parser, _bag| csv(parser, &options)).with_defaults(defaults)
1570}
1571
1572/// Build a CSV parser with the given options: the counterpart of
1573/// `new Tabnas().use(jsonic).use(Csv, options)` and the Go
1574/// `jsonic.Make()` + `UseDefaults(Csv, Defaults, options)`.
1575///
1576/// Infallible by design: the jsonic base and the embedded grammar are
1577/// fixed, so a failure here is a bug in this crate rather than anything a
1578/// caller did.
1579///
1580/// ```
1581/// use tabnas_csv::CsvOptions;
1582///
1583/// let parser = tabnas_csv::make_with(CsvOptions { header: false, object: false, ..Default::default() });
1584/// assert_eq!(parser.parse("a,b\n1,2")?.to_json().to_string(), r#"[["a","b"],["1","2"]]"#);
1585/// # Ok::<(), tabnas_csv::CsvError>(())
1586/// ```
1587pub fn make_with(options: CsvOptions) -> Tabnas {
1588 let mut parser = tabnas_jsonic::make();
1589 parser
1590 .use_plugin(plugin_with(options), None)
1591 .expect("the csv grammar documents are fixed and valid");
1592 parser
1593}
1594
1595/// Build a CSV parser with the default options.
1596///
1597/// ```
1598/// let parser = tabnas_csv::make();
1599/// assert_eq!(parser.parse("a\n\"b\"\"c\"")?.to_json().to_string(), r#"[{"a":"b\"c"}]"#);
1600/// assert_eq!(parser.parse("a\n\"b").unwrap_err().code, "unterminated_string");
1601/// # Ok::<(), tabnas_csv::CsvError>(())
1602/// ```
1603pub fn make() -> Tabnas {
1604 make_with(CsvOptions::default())
1605}
1606
1607/// Parse a CSV source string with the shared default parser.
1608///
1609/// The engine is built once, on first use, and reused after that. Reuse
1610/// is safe: [`Tabnas::parse`] takes `&self` and builds a fresh parse
1611/// context per call, and `Tabnas` is `Send + Sync`, so concurrent callers
1612/// share one installed grammar instead of each rebuilding it, which is
1613/// what dominates a small parse.
1614///
1615/// Use [`make`] or [`make_with`] instead when the parser needs
1616/// configuring: that returns a fresh instance and leaves this one alone.
1617///
1618/// ```
1619/// let value = tabnas_csv::parse("a,b\n1,2\n3,4")?;
1620/// assert_eq!(value.to_json().to_string(), r#"[{"a":"1","b":"2"},{"a":"3","b":"4"}]"#);
1621/// # Ok::<(), tabnas_csv::CsvError>(())
1622/// ```
1623pub fn parse(src: &str) -> Result<Value, CsvError> {
1624 static DEFAULT: OnceLock<Tabnas> = OnceLock::new();
1625 DEFAULT.get_or_init(make).parse(src)
1626}
1627
1628/// One optional alchemy source and the entry point a host calls.
1629#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1630pub struct TranslationPart {
1631 /// The entry point a host calls.
1632 pub entry: &'static str,
1633 /// The source text, or `None` for an entry supplied by alchemy.
1634 pub source: Option<&'static str>,
1635}
1636
1637/// The package-local structural translation interface.
1638#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1639pub struct TranslationParts {
1640 /// The complete `tabnas.plugin.json` text.
1641 pub manifest: &'static str,
1642 /// An optional lift from the grammar's events to its first read shape.
1643 pub lift: Option<TranslationPart>,
1644 /// An optional render from the write shape to text.
1645 pub render: Option<TranslationPart>,
1646}
1647
1648const TRANSLATION: TranslationParts = TranslationParts {
1649 manifest: include_str!("../translate/manifest.json"),
1650 lift: None,
1651 render: Some(TranslationPart {
1652 entry: "csv",
1653 source: None,
1654 }),
1655};
1656
1657/// Return CSV's immutable translation parts.
1658#[must_use]
1659pub const fn translate() -> Option<TranslationParts> {
1660 Some(TRANSLATION)
1661}
1662
1663/// The plugin's manifest, `tabnas.plugin.json`, as the repository carries
1664/// it. Its `translate` object is what a host that translates reads: the
1665/// shapes CSV is read as (`tree`, one object per record, keyed by the
1666/// header) and written from (`records`), the render that writes it, which
1667/// is the `csv` render alchemy carries rather than a file of this
1668/// repository's, and the sentences that say what that render changes. The
1669/// crate embeds its own copy, `translate/manifest.json`, since a packaged
1670/// crate holds nothing outside `rs/`; `tests/translate_test.rs` holds the
1671/// copy to the file.
1672///
1673/// ```
1674/// assert!(tabnas_csv::manifest_text().contains("\"translate\""));
1675/// ```
1676pub fn manifest_text() -> &'static str {
1677 TRANSLATION.manifest
1678}