Skip to main content

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.5.11";
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/// `unprintable`; a quote left open at the end of the source is
996/// `unterminated_string`. Loop exhaustion is what detects the open quote,
997/// so an odd number of quotes (`"""`, `"""""`) is caught rather than read
998/// as a terminated string.
999///
1000/// A quote that is not exactly one UTF-16 code unit leaves the matcher
1001/// inert, and the factory answers `None` rather than installing one. The
1002/// canonical opens a quoted field with
1003/// `quoteMap = { [options.string.quote]: true }` and tests
1004/// `quoteMap[src[sI]]`, where `src[sI]` is ONE UTF-16 code unit, so it
1005/// can only ever fire for a quote that is one code unit: the empty
1006/// string, a multi-character quote such as `ab`, and an astral character
1007/// such as U+1F600, which JavaScript holds as a surrogate PAIR, are all
1008/// inert there. Go carries the same guard, with the same reasoning.
1009pub fn csv_string_matcher(
1010    quote: impl Into<String>,
1011) -> impl Fn(&Options) -> Option<ImperativeLexMatcher> + Send + Sync + 'static {
1012    let quote: String = quote.into();
1013    let single_code_unit = {
1014        let mut characters = quote.chars();
1015        match (characters.next(), characters.next()) {
1016            (Some(character), None) => character.len_utf16() == 1,
1017            _ => false,
1018        }
1019    };
1020    move |options: &Options| {
1021        if !single_code_unit {
1022            return None;
1023        }
1024        let quote = quote.clone();
1025        let line_chars: Vec<char> = options.line.chars.chars().collect();
1026        Some(Arc::new(
1027            move |lexer: &mut tabnas::Lexer<'_>, _rule: &mut Rule, _context: &mut Context| {
1028                let rest = lexer.remaining();
1029                if !rest.starts_with(quote.as_str()) {
1030                    return None;
1031                }
1032                let point = lexer.point();
1033                let mut text = String::new();
1034                let mut at = quote.len();
1035                let mut closed = false;
1036
1037                while at < rest.len() {
1038                    let tail = &rest[at..];
1039                    if tail.starts_with(quote.as_str()) {
1040                        at += quote.len();
1041                        // A doubled quote is one literal quote; a single
1042                        // one closes the field.
1043                        if rest[at..].starts_with(quote.as_str()) {
1044                            text.push_str(&quote);
1045                            at += quote.len();
1046                        } else {
1047                            closed = true;
1048                            break;
1049                        }
1050                        continue;
1051                    }
1052                    let character = tail.chars().next().expect("inside the source");
1053                    if line_chars.contains(&character) {
1054                        text.push(character);
1055                    } else if (character as u32) < 32 {
1056                        // The diagnostic points at the control character,
1057                        // as it does in TypeScript.
1058                        lexer.advance_chars(rest[..at].chars().count());
1059                        return Some(lexer.bad("unprintable"));
1060                    } else {
1061                        text.push(character);
1062                    }
1063                    at += character.len_utf8();
1064                }
1065
1066                if !closed {
1067                    let start = point.site.pos;
1068                    let end = start + rest[..at].chars().count();
1069                    return Some(lexer.bad_span("unterminated_string", start, end));
1070                }
1071
1072                let source = rest[..at].to_string();
1073                let token = lexer.token("#ST", TIN_ST, Value::String(text), source.as_str(), point);
1074                lexer.advance_chars(source.chars().count());
1075                Some(token)
1076            },
1077        ))
1078    }
1079}
1080
1081// ---------------------------------------------------------------------------
1082// The documents
1083// ---------------------------------------------------------------------------
1084
1085/// Whole numbers as integers. The grammar text is parsed by jsonic, whose
1086/// numbers are all `f64`, and the engine's document loader reads an
1087/// alternate's `b` and `n` as integers.
1088fn integral(value: &mut serde_json::Value) {
1089    match value {
1090        serde_json::Value::Number(number) => {
1091            if let Some(float) = number.as_f64() {
1092                if float.fract() == 0.0 && float.abs() < 9.0e15 {
1093                    if float < 0.0 {
1094                        *value = serde_json::Value::from(float as i64);
1095                    } else {
1096                        *value = serde_json::Value::from(float as u64);
1097                    }
1098                }
1099            }
1100        }
1101        serde_json::Value::Array(items) => items.iter_mut().for_each(integral),
1102        serde_json::Value::Object(entries) => entries.values_mut().for_each(integral),
1103        _ => {}
1104    }
1105}
1106
1107/// The embedded grammar as a serialized document: parsed by a standard
1108/// jsonic instance, as the TypeScript `new Tabnas().use(jsonic).parse(
1109/// grammarText)` and the Go `parseGrammarText` do.
1110fn grammar_document() -> Result<serde_json::Value, PluginError> {
1111    let parsed = tabnas_jsonic::parse(GRAMMAR_TEXT).map_err(|error| {
1112        PluginError(format!("csv: the embedded grammar does not parse: {error}"))
1113    })?;
1114    let mut document = parsed.to_json();
1115    if !document.is_object() || document.get("rule").is_none() {
1116        return Err(PluginError(
1117            "csv: the embedded grammar has no `rule` table".to_string(),
1118        ));
1119    }
1120    integral(&mut document);
1121    Ok(document)
1122}
1123
1124/// The per-instance engine options, mirroring `jsonicOptions` in
1125/// `ts/src/csv.ts`. The static options (`rule.start`, `lex.emptyResult`,
1126/// `error`, `hint`) live in the grammar text.
1127fn options_document(
1128    options: &CsvOptions,
1129    settings: &Settings,
1130    flags: &LexFlags,
1131) -> serde_json::Value {
1132    let strict = settings.strict;
1133
1134    // Fixed-token overrides: in strict mode the JSON structural tokens and
1135    // the `:` key separator are switched off; a configured field separator
1136    // rebinds `#CA`.
1137    let mut fixed = serde_json::Map::new();
1138    if strict {
1139        for name in ["#OB", "#CB", "#OS", "#CS", "#CL"] {
1140            fixed.insert(name.to_string(), serde_json::Value::Null);
1141        }
1142    }
1143    if let Some(separation) = options
1144        .field
1145        .separation
1146        .as_deref()
1147        .filter(|separation| !separation.is_empty())
1148    {
1149        fixed.insert("#CA".to_string(), json!(separation));
1150    }
1151
1152    // IGNORE set: `#LN` leaves it so row breaks are significant; in strict
1153    // mode `#SP` leaves it too so whitespace inside a field survives. This
1154    // engine merges a token set INDEX-WISE onto the one already installed,
1155    // as the canonical TypeScript deep merge does: a `null` drops that
1156    // position and a shorter array keeps the tail of the set it overlays.
1157    // So the override is spelled position for position over the jsonic
1158    // default `[#SP, #LN, #CM]`, exactly as `ts/src/csv.ts` spells it, and
1159    // the trailing position is named rather than left off: an override that
1160    // stops short of the installed length would keep whatever the tail
1161    // holds, and this one has to answer the same on a derived instance,
1162    // where the set it overlays is already the reduced one.
1163    let ignore = if strict {
1164        json!([null, null, "#CM"])
1165    } else {
1166        json!(["#SP", null, "#CM"])
1167    };
1168
1169    let mut line = json!({ "single": settings.record_empty });
1170    if let Some(separators) = options.record.separators.as_deref() {
1171        line["chars"] = json!(separators);
1172        line["rowChars"] = json!(separators);
1173    }
1174
1175    let mut document = json!({
1176        "options": {
1177            "rule": { "exclude": if strict { "jsonic,imp" } else { "imp" } },
1178            "fixed": { "token": fixed },
1179            "tokenSet": { "IGNORE": ignore },
1180            "number": { "lex": flags.number },
1181            "value": { "lex": flags.value },
1182            "comment": { "lex": flags.comment },
1183            "line": line,
1184        },
1185    });
1186    if flags.csv_strings {
1187        document["options"]["lex"] = json!({
1188            "match": {
1189                "stringcsv": { "order": CSV_STRING_ORDER, "make": CSV_STRING },
1190            },
1191        });
1192    }
1193    document
1194}
1195
1196/// The lexer switches the mode decides.
1197struct LexFlags {
1198    number: bool,
1199    value: bool,
1200    comment: bool,
1201    csv_strings: bool,
1202}
1203
1204fn apply_document(parser: &mut Tabnas, document: serde_json::Value) -> Result<(), PluginError> {
1205    let spec = GrammarSpec::from_value(document).map_err(|error| PluginError(error.0))?;
1206    parser
1207        .grammar(&spec)
1208        .map_err(|error| PluginError(error.0))?;
1209    Ok(())
1210}
1211
1212// ---------------------------------------------------------------------------
1213// The list / elem / val rules, modified in code
1214// ---------------------------------------------------------------------------
1215
1216/// An alternate over token sequences, with the fields the CSV rules use.
1217fn alt(s: Vec<Vec<Tin>>, b: usize, g: &str) -> AltSpec {
1218    let mut alt = AltSpec::new();
1219    alt.s = s;
1220    alt.b = b;
1221    alt.g = g.to_string();
1222    alt
1223}
1224
1225fn push_alt(name: &str) -> AltSpec {
1226    let mut alt = AltSpec::new();
1227    alt.p = Some(name.to_string());
1228    alt
1229}
1230
1231fn with_push(mut alt: AltSpec, name: &str) -> AltSpec {
1232    alt.p = Some(name.to_string());
1233    alt
1234}
1235
1236fn with_replace(mut alt: AltSpec, name: &str) -> AltSpec {
1237    alt.r = Some(name.to_string());
1238    alt
1239}
1240
1241/// `rs.open([...])` / `rs.close([...])` with no `append`: the whole list
1242/// goes in front of the existing alternates, in its own order.
1243fn prepend_open(spec: &mut RuleSpec, alts: Vec<AltSpec>) {
1244    for alt in alts.into_iter().rev() {
1245        spec.prepend_open(alt);
1246    }
1247}
1248
1249fn prepend_close(spec: &mut RuleSpec, alts: Vec<AltSpec>) {
1250    for alt in alts.into_iter().rev() {
1251        spec.prepend_close(alt);
1252    }
1253}
1254
1255/// The tokens the CSV rules match.
1256struct Tokens {
1257    ln: Tin,
1258    ca: Tin,
1259    sp: Tin,
1260    zz: Tin,
1261    val: Vec<Tin>,
1262}
1263
1264/// An empty element before a separator: `,` pushes the empty value and
1265/// marks the element done, so `elem` before-close does not push again.
1266fn empty_before_comma(empty: Value) -> impl Fn(&mut Rule, &mut Context) + Send + Sync + 'static {
1267    move |rule, _context| {
1268        list_push(&mut rule.node.borrow_mut(), empty.clone());
1269        rule.u_mut().insert("done".to_string(), Value::Bool(true));
1270    }
1271}
1272
1273/// An empty element at the end of the line: `,\n` / `,<eof>`.
1274fn empty_at_end(empty: Value) -> impl Fn(&mut Rule, &mut Context) + Send + Sync + 'static {
1275    move |rule, _context| {
1276        list_push(&mut rule.node.borrow_mut(), empty.clone());
1277    }
1278}
1279
1280/// Allocate the per-record field array. Under the engine's node model a
1281/// pushed child rule is seeded with the parent's node, and jsonic's
1282/// `list` before-open only allocates for an implicit list, which the CSV
1283/// `record -> list` path never is: without this the fields would land on
1284/// the inherited document array.
1285fn allocate_fields(rule: &mut Rule, _context: &mut Context) {
1286    set_node(rule, Value::array(Vec::new()));
1287}
1288
1289/// Strict mode: `list`, `elem` and `val` are replaced entirely. The JSON
1290/// structural tokens are off, so only the CSV alternates are needed.
1291fn strict_rules(parser: &mut Tabnas, tokens: &Tokens, empty: &Value) {
1292    let Tokens {
1293        ln,
1294        ca,
1295        sp,
1296        zz,
1297        val,
1298    } = tokens;
1299    let (ln, ca, sp, zz) = (*ln, *ca, *sp, *zz);
1300
1301    parser.define_rule("list", |spec| {
1302        spec.clear();
1303        spec.add_bo(allocate_fields);
1304        // Do not consume the LN that closes an empty record.
1305        spec.add_open(alt(vec![vec![ln]], 1, ""));
1306        spec.add_open(push_alt("elem"));
1307        // LN ends the record.
1308        spec.add_close(alt(vec![vec![ln]], 1, "end"));
1309        spec.add_close(alt(vec![vec![zz]], 0, "end"));
1310    });
1311
1312    let before_comma = empty.clone();
1313    let at_end = empty.clone();
1314    parser.define_rule("elem", move |spec| {
1315        spec.clear();
1316        let mut comma = alt(vec![vec![ca]], 1, "");
1317        comma.add_action(empty_before_comma(before_comma));
1318        spec.add_open(comma);
1319        spec.add_open(push_alt("val"));
1320
1321        let mut trailing = alt(vec![vec![ca], vec![ln, zz]], 1, "comma");
1322        trailing.add_action(empty_at_end(at_end));
1323        spec.add_close(trailing);
1324        // Next element.
1325        spec.add_close(with_replace(alt(vec![vec![ca]], 0, "comma"), "elem"));
1326        // LN ends the record.
1327        spec.add_close(alt(vec![vec![ln]], 1, "end"));
1328        spec.add_close(alt(vec![vec![zz]], 0, "end"));
1329
1330        // Push the parsed field value, unless an open alternate already did.
1331        spec.add_bc(|rule, _context| {
1332            if !flag(rule, "done") && !rule.child_node.is_undefined() {
1333                let child = rule.child_node.clone();
1334                list_push(&mut rule.node.borrow_mut(), child);
1335            }
1336        });
1337    });
1338
1339    let val = val.clone();
1340    parser.define_rule("val", move |spec| {
1341        spec.clear();
1342        // Reset the parent-seeded node so an empty value resolves to no
1343        // value, not to the inherited field array.
1344        spec.add_bo(|rule, _context| set_node(rule, Value::Undefined));
1345        // Text and space concatenation.
1346        spec.add_open(with_push(alt(vec![val.clone(), vec![sp]], 2, ""), "text"));
1347        spec.add_open(with_push(alt(vec![vec![sp]], 1, ""), "text"));
1348        // A plain value token.
1349        spec.add_open(alt(vec![val.clone()], 0, ""));
1350        // LN ends the record.
1351        spec.add_open(alt(vec![vec![ln]], 1, ""));
1352        // Coalesce: a child (text) node wins; else the matched scalar
1353        // token; else no value (an implicit empty field).
1354        spec.add_bc(|rule, context| {
1355            let unset = rule.node.borrow().is_undefined();
1356            if unset {
1357                if !rule.child_node.is_undefined() {
1358                    let child = rule.child_node.clone();
1359                    set_node(rule, child);
1360                } else if rule.os() != 0 {
1361                    let value = rule.resolve_open_value(0, context);
1362                    set_node(rule, value);
1363                }
1364            }
1365        });
1366    });
1367}
1368
1369/// Non-strict mode: the CSV alternates go in FRONT of jsonic's, so the
1370/// default value alternates (handling `[1,2]`, `{x:1}` and the like) stay
1371/// available for an embedded value.
1372fn relaxed_rules(parser: &mut Tabnas, tokens: &Tokens, empty: &Value) {
1373    let Tokens {
1374        ln,
1375        ca,
1376        sp,
1377        zz,
1378        val,
1379    } = tokens;
1380    let (ln, ca, sp, zz) = (*ln, *ca, *sp, *zz);
1381
1382    parser.define_rule("list", |spec| {
1383        spec.add_bo(allocate_fields);
1384        // Do not consume the LN that closes an empty record.
1385        prepend_open(spec, vec![alt(vec![vec![ln]], 1, "")]);
1386        // The elem-push fallback goes after the jsonic defaults.
1387        spec.add_open(push_alt("elem"));
1388        // LN ends the record; in front so these win over jsonic's close
1389        // alternates.
1390        prepend_close(
1391            spec,
1392            vec![alt(vec![vec![ln]], 1, "end"), alt(vec![vec![zz]], 0, "end")],
1393        );
1394    });
1395
1396    let before_comma = empty.clone();
1397    let at_end = empty.clone();
1398    parser.define_rule("elem", move |spec| {
1399        let mut comma = alt(vec![vec![ca]], 1, "");
1400        comma.add_action(empty_before_comma(before_comma));
1401        prepend_open(spec, vec![comma]);
1402
1403        let mut trailing = alt(vec![vec![ca], vec![ln, zz]], 1, "comma");
1404        trailing.add_action(empty_at_end(at_end));
1405        prepend_close(spec, vec![trailing, alt(vec![vec![ln]], 1, "end")]);
1406    });
1407
1408    let val = val.clone();
1409    parser.define_rule("val", move |spec| {
1410        prepend_open(
1411            spec,
1412            vec![
1413                with_push(alt(vec![val.clone(), vec![sp]], 2, ""), "text"),
1414                with_push(alt(vec![vec![sp]], 1, ""), "text"),
1415                alt(vec![vec![ln]], 1, ""),
1416            ],
1417        );
1418    });
1419}
1420
1421// ---------------------------------------------------------------------------
1422// Public surface
1423// ---------------------------------------------------------------------------
1424
1425/// Whether the CSV grammar is already on this instance. A plugin re-run
1426/// (a derived instance rebuilds its plugins on a fresh rule set; an
1427/// instance that already carries the grammar must not gain a second copy
1428/// of every alternate) is judged by the one rule only csv installs.
1429fn grammar_installed(parser: &Tabnas) -> bool {
1430    parser.rule_names().iter().any(|name| name == "csv")
1431}
1432
1433/// Install the CSV plugin on `parser`, which must already carry the jsonic
1434/// grammar: the counterpart of the TypeScript `Csv` plugin function and
1435/// the Go `Csv`.
1436///
1437/// ```
1438/// let mut parser = tabnas_jsonic::make();
1439/// tabnas_csv::csv(&mut parser, &tabnas_csv::CsvOptions::default())?;
1440/// let value = parser.parse("a,b\n1,2")?;
1441/// assert_eq!(value.to_json().to_string(), r#"[{"a":"1","b":"2"}]"#);
1442/// # Ok::<(), Box<dyn std::error::Error>>(())
1443/// ```
1444///
1445/// In order: the closures the grammar names are registered, the
1446/// per-instance engine options are applied, the embedded grammar is
1447/// installed with every alternate tagged `csv`, and the `list`, `elem` and
1448/// `val` rules are modified in code (replaced in strict mode, extended in
1449/// non-strict mode, so embedded jsonic values keep working).
1450pub fn csv(parser: &mut Tabnas, options: &CsvOptions) -> Result<(), PluginError> {
1451    if grammar_installed(parser) {
1452        return Ok(());
1453    }
1454
1455    let strict = options.strict;
1456
1457    // In strict mode jsonic value parsing is off and every field is raw
1458    // text. In non-strict mode the three-state options default to on.
1459    let (trim, comment, number, value) = if strict {
1460        (
1461            options.trim.unwrap_or(false),
1462            options.comment.unwrap_or(false),
1463            options.number.unwrap_or(false),
1464            options.value.unwrap_or(false),
1465        )
1466    } else {
1467        (
1468            options.trim.unwrap_or(true),
1469            options.comment.unwrap_or(true),
1470            options.number.unwrap_or(true),
1471            options.value.unwrap_or(true),
1472        )
1473    };
1474    let csv_strings = if strict {
1475        options.string.csv != Some(false)
1476    } else {
1477        options.string.csv == Some(true)
1478    };
1479
1480    let settings = Settings {
1481        strict,
1482        objres: options.object,
1483        header: options.header,
1484        trim,
1485        record_empty: options.record.empty,
1486        empty: Value::from_json(&options.field.empty),
1487        nonameprefix: options.field.nonameprefix.clone(),
1488        names: options.field.names.clone(),
1489        exact: options.field.exact,
1490        stream: options.stream.clone(),
1491    };
1492    let flags = LexFlags {
1493        number,
1494        value,
1495        comment,
1496        csv_strings,
1497    };
1498
1499    register_refs(parser, &settings, &options.string.quote);
1500    apply_document(parser, options_document(options, &settings, &flags))?;
1501
1502    let spec =
1503        GrammarSpec::from_value(grammar_document()?).map_err(|error| PluginError(error.0))?;
1504    parser
1505        .grammar_with_setting(&spec, &GrammarSetting::groups("csv"))
1506        .map_err(|error| PluginError(format!("csv: failed to apply the grammar: {}", error.0)))?;
1507
1508    // Usually [#TX, #ST, #NR, #VL].
1509    let tokens = Tokens {
1510        ln: parser.token("#LN"),
1511        ca: parser.token("#CA"),
1512        sp: parser.token("#SP"),
1513        zz: parser.token("#ZZ"),
1514        val: parser.token_set("VAL").unwrap_or_default(),
1515    };
1516    if strict {
1517        strict_rules(parser, &tokens, &settings.empty);
1518    } else {
1519        relaxed_rules(parser, &tokens, &settings.empty);
1520    }
1521    Ok(())
1522}
1523
1524/// The plugin form of [`csv`], for [`Tabnas::use_plugin`], with the
1525/// documented defaults: the counterpart of `tn.use(Csv, options)` in
1526/// TypeScript and `j.UseDefaults(Csv, Defaults, options)` in Go. The
1527/// option bag `use_plugin` takes is deep-merged over the defaults and
1528/// read as a [`CsvOptions`].
1529///
1530/// ```
1531/// use tabnas::Value;
1532///
1533/// let mut parser = tabnas_jsonic::make();
1534/// let options = Value::from_json(&serde_json::json!({ "object": false }));
1535/// parser.use_plugin(tabnas_csv::plugin(), Some(options))?;
1536/// assert_eq!(parser.parse("a,b\n1,2")?.to_json().to_string(), r#"[["1","2"]]"#);
1537/// # Ok::<(), Box<dyn std::error::Error>>(())
1538/// ```
1539pub fn plugin() -> Plugin {
1540    Plugin::new(PLUGIN_NAME, |parser, options| {
1541        let options = CsvOptions::from_value(options).map_err(PluginError)?;
1542        csv(parser, &options)
1543    })
1544    .with_defaults(CsvOptions::default().to_value())
1545}
1546
1547/// The plugin over typed options, including a [`Stream`] callback, which
1548/// cannot travel in the option bag. The bag `use_plugin` passes is
1549/// recorded on the instance as the plugin's options but the typed ones
1550/// are what the grammar is built from.
1551pub fn plugin_with(options: CsvOptions) -> Plugin {
1552    let defaults = options.to_value();
1553    Plugin::new(PLUGIN_NAME, move |parser, _bag| csv(parser, &options)).with_defaults(defaults)
1554}
1555
1556/// Build a CSV parser with the given options: the counterpart of
1557/// `new Tabnas().use(jsonic).use(Csv, options)` and the Go
1558/// `jsonic.Make()` + `UseDefaults(Csv, Defaults, options)`.
1559///
1560/// Infallible by design: the jsonic base and the embedded grammar are
1561/// fixed, so a failure here is a bug in this crate rather than anything a
1562/// caller did.
1563///
1564/// ```
1565/// use tabnas_csv::CsvOptions;
1566///
1567/// let parser = tabnas_csv::make_with(CsvOptions { header: false, object: false, ..Default::default() });
1568/// assert_eq!(parser.parse("a,b\n1,2")?.to_json().to_string(), r#"[["a","b"],["1","2"]]"#);
1569/// # Ok::<(), tabnas_csv::CsvError>(())
1570/// ```
1571pub fn make_with(options: CsvOptions) -> Tabnas {
1572    let mut parser = tabnas_jsonic::make();
1573    parser
1574        .use_plugin(plugin_with(options), None)
1575        .expect("the csv grammar documents are fixed and valid");
1576    parser
1577}
1578
1579/// Build a CSV parser with the default options.
1580///
1581/// ```
1582/// let parser = tabnas_csv::make();
1583/// assert_eq!(parser.parse("a\n\"b\"\"c\"")?.to_json().to_string(), r#"[{"a":"b\"c"}]"#);
1584/// assert_eq!(parser.parse("a\n\"b").unwrap_err().code, "unterminated_string");
1585/// # Ok::<(), tabnas_csv::CsvError>(())
1586/// ```
1587pub fn make() -> Tabnas {
1588    make_with(CsvOptions::default())
1589}
1590
1591/// Parse a CSV source string with the shared default parser.
1592///
1593/// The engine is built once, on first use, and reused after that. Reuse
1594/// is safe: [`Tabnas::parse`] takes `&self` and builds a fresh parse
1595/// context per call, and `Tabnas` is `Send + Sync`, so concurrent callers
1596/// share one installed grammar instead of each rebuilding it, which is
1597/// what dominates a small parse.
1598///
1599/// Use [`make`] or [`make_with`] instead when the parser needs
1600/// configuring: that returns a fresh instance and leaves this one alone.
1601///
1602/// ```
1603/// let value = tabnas_csv::parse("a,b\n1,2\n3,4")?;
1604/// assert_eq!(value.to_json().to_string(), r#"[{"a":"1","b":"2"},{"a":"3","b":"4"}]"#);
1605/// # Ok::<(), tabnas_csv::CsvError>(())
1606/// ```
1607pub fn parse(src: &str) -> Result<Value, CsvError> {
1608    static DEFAULT: OnceLock<Tabnas> = OnceLock::new();
1609    DEFAULT.get_or_init(make).parse(src)
1610}