#![allow(clippy::result_large_err)]
use std::cell::RefCell;
use std::fmt;
use std::rc::Rc;
use std::sync::{Arc, OnceLock};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tabnas::{
ActionError, AltSpec, Context, GrammarSetting, GrammarSpec, ImperativeLexMatcher, Options,
Plugin, PluginError, Rule, RuleSpec, Tabnas, Tin, Token, Value, TIN_BD, TIN_ST,
};
pub const VERSION: &str = "0.6.5";
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
mod readme_examples {}
pub use tabnas::TabnasError as CsvError;
const GRAMMAR_TEXT: &str = r##"
# CSV Grammar Definition
# Parsed by a standard Jsonic instance and passed to jsonic.grammar()
# Function references (@ prefixed) are resolved against the refs map
#
# Token naming:
# #LN - line ending (removed from per-instance IGNORE set)
# #SP - whitespace (removed from per-instance IGNORE set in strict mode)
# #CA - comma / field separator
# #ZZ - end of input
# #VAL - token set: text, string, number, value literals
#
# Rules csv, newline, record, text are fully defined here.
# Rules list, elem, val are modified in code (strict mode defines from scratch;
# non-strict prepends to existing defaults to preserve JSON parsing).
{
options: rule: { start: csv }
options: lex: { emptyResult: [] }
# Placeholders are {braces}: the engine's error injector (strinject in
# @tabnas/parser) substitutes {name} from the details object passed to
# token.bad(). A $name is left in the message verbatim.
options: error: {
csv_extra_field: 'unexpected extra field value: {fsrc}'
csv_missing_field: 'missing field'
}
options: hint: {
csv_extra_field: 'Row {row} has too many fields (the first of which is: {fsrc}). Only {len}\nfields per row are expected.'
csv_missing_field: 'Row {row} has too few fields. {len} fields per row are expected.'
}
rule: csv: open: [
{ s: '#ZZ' }
{ s: '#LN' p: newline c: '@not-record-empty' }
{ p: record }
]
rule: newline: open: [
{ s: '#LN #LN' r: newline }
{ s: '#LN' r: newline }
{ s: '#ZZ' }
{ r: record }
]
rule: newline: close: [
{ s: '#LN #LN' r: newline g: end }
{ s: '#LN' r: newline g: end }
{ s: '#ZZ' g: end }
{ r: record }
]
rule: record: open: [
{ p: list }
]
rule: record: close: [
{ s: '#ZZ' g: end }
{ s: '#LN #ZZ' b: 1 g: end }
{ s: '#LN' r: '@record-close-next' g: end }
]
rule: text: open: [
{ s: ['#VAL' '#SP'] b: 1 r: text n: { text: 1 } g: 'csv,space,follows' a: '@text-follows' }
{ s: ['#SP' '#VAL'] r: text n: { text: 1 } g: 'csv,space,leads' a: '@text-leads' }
{ s: ['#SP' '#CA #LN #ZZ'] b: 1 n: { text: 1 } g: 'csv,end' a: '@text-end' }
{ s: '#SP' n: { text: 1 } g: 'csv,space' a: '@text-space' p: '@text-space-push' }
{}
]
}
"##;
pub fn grammar_text() -> &'static str {
GRAMMAR_TEXT
}
const PLUGIN_NAME: &str = "csv";
const CSV_BO: &str = "@csv-bo";
const CSV_AC: &str = "@csv-ac";
const RECORD_BC: &str = "@record-bc";
const TEXT_BC: &str = "@text-bc";
const TEXT_FOLLOWS: &str = "@text-follows";
const TEXT_LEADS: &str = "@text-leads";
const TEXT_END: &str = "@text-end";
const TEXT_SPACE: &str = "@text-space";
const NOT_RECORD_EMPTY: &str = "@not-record-empty";
const RECORD_CLOSE_NEXT: &str = "@record-close-next";
const TEXT_SPACE_PUSH: &str = "@text-space-push";
const CSV_STRING: &str = "@csv-string";
const CSV_STRING_ORDER: f64 = 100_000.0;
#[derive(Debug, Clone, PartialEq)]
pub enum StreamEvent {
Start,
Record(Value),
End,
}
#[derive(Clone)]
pub struct Stream(Arc<dyn Fn(StreamEvent) + Send + Sync>);
impl Stream {
pub fn new(callback: impl Fn(StreamEvent) + Send + Sync + 'static) -> Self {
Stream(Arc::new(callback))
}
fn send(&self, event: StreamEvent) {
(self.0)(event);
}
}
impl fmt::Debug for Stream {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("Stream(<function>)")
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct FieldOptions {
pub separation: Option<String>,
pub nonameprefix: String,
pub empty: serde_json::Value,
pub names: Option<Vec<String>>,
pub exact: bool,
}
impl Default for FieldOptions {
fn default() -> Self {
FieldOptions {
separation: None,
nonameprefix: "field~".to_string(),
empty: serde_json::Value::String(String::new()),
names: None,
exact: false,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct RecordOptions {
pub separators: Option<String>,
pub empty: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct StringOptions {
pub quote: String,
pub csv: Option<bool>,
}
impl Default for StringOptions {
fn default() -> Self {
StringOptions {
quote: "\"".to_string(),
csv: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct CsvOptions {
pub trim: Option<bool>,
pub comment: Option<bool>,
pub number: Option<bool>,
pub value: Option<bool>,
pub header: bool,
pub object: bool,
pub strict: bool,
pub field: FieldOptions,
pub record: RecordOptions,
pub string: StringOptions,
#[serde(skip)]
pub stream: Option<Stream>,
}
impl Default for CsvOptions {
fn default() -> Self {
CsvOptions {
trim: None,
comment: None,
number: None,
value: None,
header: true,
object: true,
strict: true,
field: FieldOptions::default(),
record: RecordOptions::default(),
string: StringOptions::default(),
stream: None,
}
}
}
impl CsvOptions {
pub fn from_value(value: &Value) -> Result<Self, String> {
serde_json::from_value(value.to_json()).map_err(|error| format!("csv options: {error}"))
}
pub fn to_value(&self) -> Value {
let json = serde_json::to_value(self).expect("the option struct serializes");
Value::from_json(&json)
}
}
#[derive(Clone)]
struct Settings {
strict: bool,
objres: bool,
header: bool,
trim: bool,
record_empty: bool,
empty: Value,
nonameprefix: String,
names: Option<Vec<String>>,
exact: bool,
stream: Option<Stream>,
}
fn set_node(rule: &mut Rule, value: Value) {
rule.node = Rc::new(RefCell::new(value));
}
fn list_push(node: &mut Value, value: Value) {
match node {
Value::Array(items) => Arc::make_mut(items).push(value),
Value::ListRef(list) => Arc::make_mut(list).value.push(value),
_ => {}
}
}
fn items_of(node: &Value) -> Vec<Value> {
match node {
Value::Array(items) => items.as_ref().clone(),
Value::ListRef(list) => list.value.clone(),
_ => Vec::new(),
}
}
fn flag(rule: &Rule, name: &str) -> bool {
matches!(rule.u.get(name), Some(Value::Bool(true)))
}
fn counter(rule: &Rule, name: &str) -> i32 {
rule.n.get(name).copied().unwrap_or(0)
}
fn js_number_to_string(number: f64) -> String {
if number.is_nan() {
return "NaN".to_string();
}
if number == 0.0 {
return "0".to_string();
}
if number < 0.0 {
return format!("-{}", js_number_to_string(-number));
}
if number.is_infinite() {
return "Infinity".to_string();
}
let shortest = format!("{number:e}");
let shortest_k = shortest
.split_once('e')
.map(|(mantissa, _)| mantissa.chars().filter(char::is_ascii_digit).count())
.expect("a finite f64 always formats with an exponent");
let exponential = format!("{:.*e}", shortest_k - 1, number);
let (mantissa, exponent) = exponential
.split_once('e')
.expect("a finite f64 always formats with an exponent");
let digits = mantissa
.chars()
.filter(|digit| *digit != '.')
.collect::<String>();
let digits = digits.trim_end_matches('0');
let digits = if digits.is_empty() { "0" } else { digits };
let k = digits.len() as i32;
let n = exponent
.parse::<i32>()
.expect("a formatted exponent is an integer")
+ 1;
if (k..=21).contains(&n) {
let mut text = digits.to_string();
text.push_str(&"0".repeat((n - k) as usize));
text
} else if (1..=21).contains(&n) {
let point = n as usize;
format!("{}.{}", &digits[..point], &digits[point..])
} else if (-5..=0).contains(&n) {
format!("0.{}{}", "0".repeat(-n as usize), digits)
} else {
let sign = if n - 1 < 0 { '-' } else { '+' };
let power = (n - 1).abs();
if k == 1 {
format!("{digits}e{sign}{power}")
} else {
format!("{}.{}e{sign}{power}", &digits[..1], &digits[1..])
}
}
}
fn value_text(value: &Value) -> String {
match value {
Value::Undefined => String::new(),
Value::Null => "null".to_string(),
Value::Bool(flag) => flag.to_string(),
Value::Number(number) => js_number_to_string(*number),
Value::String(text) => text.clone(),
Value::Text(text) => text.string.clone(),
other => other.to_json().to_string(),
}
}
fn token_text(token: &Token) -> String {
if token.val.is_undefined() {
token.src.as_str().to_string()
} else {
value_text(&token.val)
}
}
fn key_text(value: &Value, from_option: bool) -> Option<String> {
match value {
Value::Undefined
| Value::Null
| Value::Bool(_)
| Value::Number(_)
| Value::String(_)
| Value::Text(_) => Some(value_text(value)),
Value::Array(items) => join_text(items, from_option),
Value::Object(_) if from_option => Some("[object Object]".to_string()),
_ => None,
}
}
fn same_container(value: &Value, other: &Value) -> bool {
match (value, other) {
(Value::Object(left), Value::Object(right)) => Arc::ptr_eq(left, right),
(Value::Array(left), Value::Array(right)) => Arc::ptr_eq(left, right),
_ => false,
}
}
fn join_text(items: &[Value], from_option: bool) -> Option<String> {
let mut joined = String::new();
for (index, item) in items.iter().enumerate() {
if 0 < index {
joined.push(',');
}
match item {
Value::Null | Value::Undefined => {}
other => joined.push_str(&key_text(other, from_option)?),
}
}
Some(joined)
}
fn number(value: usize) -> Value {
Value::Number(value as f64)
}
fn prev_text(rule: &Rule) -> String {
rule.prev_rule
.as_ref()
.map(|prev| value_text(&prev.node.borrow()))
.unwrap_or_default()
}
fn assign_text(rule: &mut Rule, chained: bool, text: String) {
let value = Value::String(text);
if chained {
if let Some(prev) = rule.prev_rule.as_ref() {
*prev.node.borrow_mut() = value.clone();
}
}
set_node(rule, value);
}
fn text_follows(rule: &mut Rule, _context: &mut Context) -> Result<(), ActionError> {
let chained = counter(rule, "text") != 1;
let head = if chained {
prev_text(rule)
} else {
String::new()
};
let text = head + &rule.o0().map(token_text).unwrap_or_default();
assign_text(rule, chained, text);
Ok(())
}
fn text_leads(trim: bool) -> impl Fn(&mut Rule, &mut Context) -> Result<(), ActionError> {
move |rule, _context| {
let count = counter(rule, "text");
let chained = count != 1;
let head = if chained {
prev_text(rule)
} else {
String::new()
};
let space = if 2 <= count || !trim {
rule.o0().map(|token| token.src.as_str().to_string())
} else {
None
};
let text = head
+ &space.unwrap_or_default()
+ &rule
.o1()
.map(|token| token.src.as_str().to_string())
.unwrap_or_default();
assign_text(rule, chained, text);
Ok(())
}
}
fn text_end(trim: bool) -> impl Fn(&mut Rule, &mut Context) -> Result<(), ActionError> {
move |rule, _context| {
let chained = counter(rule, "text") != 1;
let head = if chained {
prev_text(rule)
} else {
String::new()
};
let space = if trim {
None
} else {
rule.o0().map(|token| token.src.as_str().to_string())
};
assign_text(rule, chained, head + &space.unwrap_or_default());
Ok(())
}
}
fn text_space(
strict: bool,
trim: bool,
) -> impl Fn(&mut Rule, &mut Context) -> Result<(), ActionError> {
let end = text_end(trim);
move |rule, context| {
if strict {
end(rule, context)
} else {
Ok(())
}
}
}
fn text_before_close(rule: &mut Rule, _context: &mut Context) -> Result<(), ActionError> {
let value = if rule.child_node.is_undefined() {
rule.node.borrow().clone()
} else {
rule.child_node.clone()
};
if let Some(parent) = rule.parent_node.as_ref() {
*parent.borrow_mut() = value;
}
Ok(())
}
fn record_index(context: &Context) -> usize {
match context.u.get("recordI") {
Some(Value::Number(index)) => *index as usize,
_ => 0,
}
}
fn record_before_close(
settings: Settings,
) -> impl Fn(
&mut Rule,
&mut Context,
Option<&tabnas::RuleSnapshot>,
Option<Token>,
) -> Result<Option<Token>, ActionError> {
move |rule, context, _next, out| {
let record_i = record_index(context);
let (fields, fields_from_options): (Option<Vec<Value>>, bool) =
match context.u.get("fields") {
Some(Value::Array(names)) => (Some(names.as_ref().clone()), false),
_ => (
settings.names.as_ref().map(|names| {
names
.iter()
.map(|name| Value::String(name.clone()))
.collect()
}),
true,
),
};
if record_i == 0 && settings.header {
context.u.insert(
"fields".to_string(),
Value::array(items_of(&rule.child_node)),
);
} else {
let mut record = items_of(&rule.child_node);
if let Some(fields) = fields.as_ref() {
if settings.exact && record.len() != fields.len() {
let code = if record.len() > fields.len() {
"csv_extra_field"
} else {
"csv_missing_field"
};
let mut details = vec![
("row".to_string(), number(record_i + 1)),
("len".to_string(), number(fields.len())),
];
if let Some(extra) = record.get(fields.len()) {
details.push(("fsrc".to_string(), extra.clone()));
}
let mut token = context.t0().cloned().unwrap_or_else(|| {
Token::new("#BD", TIN_BD, Value::Undefined, "", Default::default())
});
token.bad_with_details(code, details);
return Ok(Some(token));
}
}
let filled = |cell: Option<&Value>| match cell {
Some(value) if !value.is_undefined() => value.clone(),
_ => settings.empty.clone(),
};
let shaped = if settings.objres {
let mut object = IndexMap::new();
let mut index = 0;
if let Some(fields) = fields.as_ref() {
for (position, cell) in fields.iter().enumerate() {
let from_option =
fields_from_options || same_container(cell, &settings.empty);
let Some(name) = key_text(cell, from_option) else {
let mut token = context.t0().cloned().unwrap_or_else(|| {
Token::new("#BD", TIN_BD, Value::Undefined, "", Default::default())
});
token.bad("unexpected");
return Ok(Some(token));
};
object.insert(name, filled(record.get(position)));
}
index = fields.len();
}
while index < record.len() {
object.insert(
format!("{}{index}", settings.nonameprefix),
filled(record.get(index)),
);
index += 1;
}
Value::object(object)
} else {
for cell in record.iter_mut() {
if cell.is_undefined() {
*cell = settings.empty.clone();
}
}
Value::array(record)
};
match settings.stream.as_ref() {
Some(stream) => stream.send(StreamEvent::Record(shaped)),
None => list_push(&mut rule.node.borrow_mut(), shaped),
}
}
context
.u
.insert("recordI".to_string(), number(record_i + 1));
Ok(out)
}
}
fn register_refs(parser: &mut Tabnas, settings: &Settings, quote: &str) {
let stream = settings.stream.clone();
parser.state_action_ref(CSV_BO, move |rule, context| {
context.u.insert("recordI".to_string(), number(0));
if let Some(stream) = stream.as_ref() {
stream.send(StreamEvent::Start);
}
set_node(rule, Value::array(Vec::new()));
Ok(())
});
let stream = settings.stream.clone();
parser.state_action_ref(CSV_AC, move |_rule, _context| {
if let Some(stream) = stream.as_ref() {
stream.send(StreamEvent::End);
}
Ok(())
});
parser.state_action_with_next_ref(RECORD_BC, record_before_close(settings.clone()));
parser.state_action_ref(TEXT_BC, text_before_close);
parser.action_with_context(TEXT_FOLLOWS, text_follows);
parser.action_with_context(TEXT_LEADS, text_leads(settings.trim));
parser.action_with_context(TEXT_END, text_end(settings.trim));
parser.action_with_context(TEXT_SPACE, text_space(settings.strict, settings.trim));
let record_empty = settings.record_empty;
parser.alt_condition(NOT_RECORD_EMPTY, move |_rule, _context| !record_empty);
parser.alt_replace(RECORD_CLOSE_NEXT, move |_rule, _context| {
Some(if record_empty { "record" } else { "newline" }.to_string())
});
let strict = settings.strict;
parser.alt_push(TEXT_SPACE_PUSH, move |_rule, _context| {
Some(if strict { "" } else { "val" }.to_string())
});
parser.lex_match_factory_ref(CSV_STRING, csv_string_matcher(quote));
}
pub fn csv_string_matcher(
quote: impl Into<String>,
) -> impl Fn(&Options) -> Option<ImperativeLexMatcher> + Send + Sync + 'static {
let quote: String = quote.into();
let single_code_unit = {
let mut characters = quote.chars();
match (characters.next(), characters.next()) {
(Some(character), None) => character.len_utf16() == 1,
_ => false,
}
};
move |options: &Options| {
if !single_code_unit {
return None;
}
let quote = quote.clone();
let line_chars: Vec<char> = options.line.chars.chars().collect();
let allow_control = options.string.allow_control;
Some(Arc::new(
move |lexer: &mut tabnas::Lexer<'_>, _rule: &mut Rule, _context: &mut Context| {
let rest = lexer.remaining();
if !rest.starts_with(quote.as_str()) {
return None;
}
let point = lexer.point();
let mut text = String::new();
let mut at = quote.len();
let mut closed = false;
while at < rest.len() {
let tail = &rest[at..];
if tail.starts_with(quote.as_str()) {
at += quote.len();
if rest[at..].starts_with(quote.as_str()) {
text.push_str("e);
at += quote.len();
} else {
closed = true;
break;
}
continue;
}
let character = tail.chars().next().expect("inside the source");
if line_chars.contains(&character) {
text.push(character);
} else if (character as u32) < 32 && !allow_control {
lexer.advance_chars(rest[..at].chars().count());
return Some(lexer.bad("unprintable"));
} else {
text.push(character);
}
at += character.len_utf8();
}
if !closed {
let start = point.site.pos;
let end = start + rest[..at].chars().count();
return Some(lexer.bad_span("unterminated_string", start, end));
}
let source = rest[..at].to_string();
let token = lexer.token("#ST", TIN_ST, Value::String(text), source.as_str(), point);
lexer.advance_chars(source.chars().count());
Some(token)
},
))
}
}
fn integral(value: &mut serde_json::Value) {
match value {
serde_json::Value::Number(number) => {
if let Some(float) = number.as_f64() {
if float.fract() == 0.0 && float.abs() < 9.0e15 {
if float < 0.0 {
*value = serde_json::Value::from(float as i64);
} else {
*value = serde_json::Value::from(float as u64);
}
}
}
}
serde_json::Value::Array(items) => items.iter_mut().for_each(integral),
serde_json::Value::Object(entries) => entries.values_mut().for_each(integral),
_ => {}
}
}
fn grammar_document() -> Result<serde_json::Value, PluginError> {
let parsed = tabnas_jsonic::parse(GRAMMAR_TEXT).map_err(|error| {
PluginError(format!("csv: the embedded grammar does not parse: {error}"))
})?;
let mut document = parsed.to_json();
if !document.is_object() || document.get("rule").is_none() {
return Err(PluginError(
"csv: the embedded grammar has no `rule` table".to_string(),
));
}
integral(&mut document);
Ok(document)
}
fn options_document(
options: &CsvOptions,
settings: &Settings,
flags: &LexFlags,
) -> serde_json::Value {
let strict = settings.strict;
let mut fixed = serde_json::Map::new();
if strict {
for name in ["#OB", "#CB", "#OS", "#CS", "#CL"] {
fixed.insert(name.to_string(), serde_json::Value::Null);
}
}
if let Some(separation) = options
.field
.separation
.as_deref()
.filter(|separation| !separation.is_empty())
{
fixed.insert("#CA".to_string(), json!(separation));
}
let ignore = if strict {
json!([null, null, "#CM"])
} else {
json!(["#SP", null, "#CM"])
};
let mut line = json!({ "single": settings.record_empty });
if let Some(separators) = options.record.separators.as_deref() {
line["chars"] = json!(separators);
line["rowChars"] = json!(separators);
}
let mut document = json!({
"options": {
"rule": { "exclude": if strict { "jsonic,imp" } else { "imp" } },
"fixed": { "token": fixed },
"tokenSet": { "IGNORE": ignore },
"number": { "lex": flags.number },
"value": { "lex": flags.value },
"comment": { "lex": flags.comment },
"string": { "allowControl": true },
"line": line,
},
});
if flags.csv_strings {
document["options"]["lex"] = json!({
"match": {
"stringcsv": { "order": CSV_STRING_ORDER, "make": CSV_STRING },
},
});
}
document
}
struct LexFlags {
number: bool,
value: bool,
comment: bool,
csv_strings: bool,
}
fn apply_document(parser: &mut Tabnas, document: serde_json::Value) -> Result<(), PluginError> {
let spec = GrammarSpec::from_value(document).map_err(|error| PluginError(error.0))?;
parser
.grammar(&spec)
.map_err(|error| PluginError(error.0))?;
Ok(())
}
fn alt(s: Vec<Vec<Tin>>, b: usize, g: &str) -> AltSpec {
let mut alt = AltSpec::new();
alt.s = s;
alt.b = b;
alt.g = g.to_string();
alt
}
fn push_alt(name: &str) -> AltSpec {
let mut alt = AltSpec::new();
alt.p = Some(name.to_string());
alt
}
fn with_push(mut alt: AltSpec, name: &str) -> AltSpec {
alt.p = Some(name.to_string());
alt
}
fn with_replace(mut alt: AltSpec, name: &str) -> AltSpec {
alt.r = Some(name.to_string());
alt
}
fn prepend_open(spec: &mut RuleSpec, alts: Vec<AltSpec>) {
for alt in alts.into_iter().rev() {
spec.prepend_open(alt);
}
}
fn prepend_close(spec: &mut RuleSpec, alts: Vec<AltSpec>) {
for alt in alts.into_iter().rev() {
spec.prepend_close(alt);
}
}
struct Tokens {
ln: Tin,
ca: Tin,
sp: Tin,
zz: Tin,
val: Vec<Tin>,
}
fn empty_before_comma(empty: Value) -> impl Fn(&mut Rule, &mut Context) + Send + Sync + 'static {
move |rule, _context| {
list_push(&mut rule.node.borrow_mut(), empty.clone());
rule.u_mut().insert("done".to_string(), Value::Bool(true));
}
}
fn empty_at_end(empty: Value) -> impl Fn(&mut Rule, &mut Context) + Send + Sync + 'static {
move |rule, _context| {
list_push(&mut rule.node.borrow_mut(), empty.clone());
}
}
fn allocate_fields(rule: &mut Rule, _context: &mut Context) {
set_node(rule, Value::array(Vec::new()));
}
fn strict_rules(parser: &mut Tabnas, tokens: &Tokens, empty: &Value) {
let Tokens {
ln,
ca,
sp,
zz,
val,
} = tokens;
let (ln, ca, sp, zz) = (*ln, *ca, *sp, *zz);
parser.define_rule("list", |spec| {
spec.clear();
spec.add_bo(allocate_fields);
spec.add_open(alt(vec![vec![ln]], 1, ""));
spec.add_open(push_alt("elem"));
spec.add_close(alt(vec![vec![ln]], 1, "end"));
spec.add_close(alt(vec![vec![zz]], 0, "end"));
});
let before_comma = empty.clone();
let at_end = empty.clone();
parser.define_rule("elem", move |spec| {
spec.clear();
let mut comma = alt(vec![vec![ca]], 1, "");
comma.add_action(empty_before_comma(before_comma));
spec.add_open(comma);
spec.add_open(push_alt("val"));
let mut trailing = alt(vec![vec![ca], vec![ln, zz]], 1, "comma");
trailing.add_action(empty_at_end(at_end));
spec.add_close(trailing);
spec.add_close(with_replace(alt(vec![vec![ca]], 0, "comma"), "elem"));
spec.add_close(alt(vec![vec![ln]], 1, "end"));
spec.add_close(alt(vec![vec![zz]], 0, "end"));
spec.add_bc(|rule, _context| {
if !flag(rule, "done") && !rule.child_node.is_undefined() {
let child = rule.child_node.clone();
list_push(&mut rule.node.borrow_mut(), child);
}
});
});
let val = val.clone();
parser.define_rule("val", move |spec| {
spec.clear();
spec.add_bo(|rule, _context| set_node(rule, Value::Undefined));
spec.add_open(with_push(alt(vec![val.clone(), vec![sp]], 2, ""), "text"));
spec.add_open(with_push(alt(vec![vec![sp]], 1, ""), "text"));
spec.add_open(alt(vec![val.clone()], 0, ""));
spec.add_open(alt(vec![vec![ln]], 1, ""));
spec.add_bc(|rule, context| {
let unset = rule.node.borrow().is_undefined();
if unset {
if !rule.child_node.is_undefined() {
let child = rule.child_node.clone();
set_node(rule, child);
} else if rule.os() != 0 {
let value = rule.resolve_open_value(0, context);
set_node(rule, value);
}
}
});
});
}
fn relaxed_rules(parser: &mut Tabnas, tokens: &Tokens, empty: &Value) {
let Tokens {
ln,
ca,
sp,
zz,
val,
} = tokens;
let (ln, ca, sp, zz) = (*ln, *ca, *sp, *zz);
parser.define_rule("list", |spec| {
spec.add_bo(allocate_fields);
prepend_open(spec, vec![alt(vec![vec![ln]], 1, "")]);
spec.add_open(push_alt("elem"));
prepend_close(
spec,
vec![alt(vec![vec![ln]], 1, "end"), alt(vec![vec![zz]], 0, "end")],
);
});
let before_comma = empty.clone();
let at_end = empty.clone();
parser.define_rule("elem", move |spec| {
let mut comma = alt(vec![vec![ca]], 1, "");
comma.add_action(empty_before_comma(before_comma));
prepend_open(spec, vec![comma]);
let mut trailing = alt(vec![vec![ca], vec![ln, zz]], 1, "comma");
trailing.add_action(empty_at_end(at_end));
prepend_close(spec, vec![trailing, alt(vec![vec![ln]], 1, "end")]);
});
let val = val.clone();
parser.define_rule("val", move |spec| {
prepend_open(
spec,
vec![
with_push(alt(vec![val.clone(), vec![sp]], 2, ""), "text"),
with_push(alt(vec![vec![sp]], 1, ""), "text"),
alt(vec![vec![ln]], 1, ""),
],
);
});
}
fn grammar_installed(parser: &Tabnas) -> bool {
parser.rule_names().iter().any(|name| name == "csv")
}
pub fn csv(parser: &mut Tabnas, options: &CsvOptions) -> Result<(), PluginError> {
if grammar_installed(parser) {
return Ok(());
}
let strict = options.strict;
let (trim, comment, number, value) = if strict {
(
options.trim.unwrap_or(false),
options.comment.unwrap_or(false),
options.number.unwrap_or(false),
options.value.unwrap_or(false),
)
} else {
(
options.trim.unwrap_or(true),
options.comment.unwrap_or(true),
options.number.unwrap_or(true),
options.value.unwrap_or(true),
)
};
let csv_strings = if strict {
options.string.csv != Some(false)
} else {
options.string.csv == Some(true)
};
let settings = Settings {
strict,
objres: options.object,
header: options.header,
trim,
record_empty: options.record.empty,
empty: Value::from_json(&options.field.empty),
nonameprefix: options.field.nonameprefix.clone(),
names: options.field.names.clone(),
exact: options.field.exact,
stream: options.stream.clone(),
};
let flags = LexFlags {
number,
value,
comment,
csv_strings,
};
register_refs(parser, &settings, &options.string.quote);
apply_document(parser, options_document(options, &settings, &flags))?;
let spec =
GrammarSpec::from_value(grammar_document()?).map_err(|error| PluginError(error.0))?;
parser
.grammar_with_setting(&spec, &GrammarSetting::groups("csv"))
.map_err(|error| PluginError(format!("csv: failed to apply the grammar: {}", error.0)))?;
let tokens = Tokens {
ln: parser.token("#LN"),
ca: parser.token("#CA"),
sp: parser.token("#SP"),
zz: parser.token("#ZZ"),
val: parser.token_set("VAL").unwrap_or_default(),
};
if strict {
strict_rules(parser, &tokens, &settings.empty);
} else {
relaxed_rules(parser, &tokens, &settings.empty);
}
Ok(())
}
pub fn plugin() -> Plugin {
Plugin::new(PLUGIN_NAME, |parser, options| {
let options = CsvOptions::from_value(options).map_err(PluginError)?;
csv(parser, &options)
})
.with_defaults(CsvOptions::default().to_value())
}
pub fn plugin_with(options: CsvOptions) -> Plugin {
let defaults = options.to_value();
Plugin::new(PLUGIN_NAME, move |parser, _bag| csv(parser, &options)).with_defaults(defaults)
}
pub fn make_with(options: CsvOptions) -> Tabnas {
let mut parser = tabnas_jsonic::make();
parser
.use_plugin(plugin_with(options), None)
.expect("the csv grammar documents are fixed and valid");
parser
}
pub fn make() -> Tabnas {
make_with(CsvOptions::default())
}
pub fn parse(src: &str) -> Result<Value, CsvError> {
static DEFAULT: OnceLock<Tabnas> = OnceLock::new();
DEFAULT.get_or_init(make).parse(src)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TranslationPart {
pub entry: &'static str,
pub source: Option<&'static str>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TranslationParts {
pub manifest: &'static str,
pub lift: Option<TranslationPart>,
pub render: Option<TranslationPart>,
}
const TRANSLATION: TranslationParts = TranslationParts {
manifest: include_str!("../translate/manifest.json"),
lift: None,
render: Some(TranslationPart {
entry: "csv",
source: None,
}),
};
#[must_use]
pub const fn translate() -> Option<TranslationParts> {
Some(TRANSLATION)
}
pub fn manifest_text() -> &'static str {
TRANSLATION.manifest
}