///|
/// A structural CSV parse issue with one-based line and column positions.
pub(all) struct CsvParseError {
message : String
line : Int
column : Int
} derive(Eq, Debug)
///|
/// A checked parse result containing parsed rows and recoverable issues.
pub(all) struct CsvParseReport {
rows : Array[Array[String]]
issues : Array[CsvParseError]
} derive(Eq, Debug)
///|
/// Parse CSV and report recoverable structural issues.
pub fn parse_checked(input : String) -> CsvParseReport {
parse_checked_with_dialect(input, default_dialect())
}
///|
/// Parse delimited text and report recoverable structural issues.
pub fn parse_checked_with_dialect(
input : String,
dialect : CsvDialect,
) -> CsvParseReport {
let rows = parse_with_dialect(input, dialect)
let issues : Array[CsvParseError] = Array::new()
match parse_unclosed_quote_position(input) {
Some((line, column)) =>
issues.push({ message: "unclosed quoted field", line, column })
None => ()
}
let width_issues = parse_width_issues(rows)
for issue in width_issues {
issues.push(issue)
}
{ rows, issues }
}
///|
/// Parse a header-aware table and report recoverable structural issues.
pub fn parse_table_checked(input : String) -> CsvTableParseReport {
parse_table_checked_with_dialect(input, default_dialect())
}
///|
/// A checked table parse result.
pub(all) struct CsvTableParseReport {
table : CsvTable
issues : Array[CsvParseError]
} derive(Eq, Debug)
///|
/// Parse a header-aware table with a custom dialect and report structural issues.
pub fn parse_table_checked_with_dialect(
input : String,
dialect : CsvDialect,
) -> CsvTableParseReport {
let report = parse_checked_with_dialect(input, dialect)
let table = if report.rows.length() == 0 {
{ headers: [], rows: [] }
} else {
let rows : Array[Array[String]] = Array::new()
for i in 1.. Bool {
report.issues.length() == 0
}
///|
/// Return true when a checked table parse report has no issues.
pub fn table_parse_report_ok(report : CsvTableParseReport) -> Bool {
report.issues.length() == 0
}
///|
/// Render parse issues as a compact human-readable report.
pub fn parse_issues_to_text(issues : Array[CsvParseError]) -> String {
if issues.length() == 0 {
"ok"
} else {
let out = StringBuilder()
for i in 0.. 0 {
out.write_char('\n')
}
let issue = issues[i]
out.write_string(
"line \{issue.line}, column \{issue.column}: \{issue.message}",
)
}
out.to_string()
}
}
///|
/// Render parse issues as Markdown.
pub fn parse_issues_to_markdown(issues : Array[CsvParseError]) -> String {
if issues.length() == 0 {
"## Parse Issues\n\nNo parse issues."
} else {
let out = StringBuilder()
out.write_string("## Parse Issues\n\n")
out.write_string("| line | column | message |\n")
out.write_string("| ---: | ---: | --- |\n")
for issue in issues {
out.write_string("| \{issue.line} | \{issue.column} | ")
parse_result_write_markdown_cell(out, issue.message)
out.write_string(" |\n")
}
out.to_string()
}
}
///|
fn parse_width_issues(rows : Array[Array[String]]) -> Array[CsvParseError] {
let issues : Array[CsvParseError] = Array::new()
if rows.length() == 0 {
return issues
}
let expected = rows[0].length()
for i in 1.. (Int, Int)? {
let mut in_quotes = false
let mut after_quote = false
let mut line = 1
let mut column = 1
let mut quote_line = 1
let mut quote_column = 1
for ch in input.iter() {
if ch == '"' {
if in_quotes {
if after_quote {
after_quote = false
} else {
after_quote = true
}
} else {
in_quotes = true
after_quote = false
quote_line = line
quote_column = column
}
} else if after_quote {
in_quotes = false
after_quote = false
}
if ch == '\n' {
line += 1
column = 1
} else {
column += 1
}
}
if in_quotes && !after_quote {
Some((quote_line, quote_column))
} else {
None
}
}
///|
fn parse_result_write_markdown_cell(
out : StringBuilder,
value : String,
) -> Unit {
for ch in value.iter() {
if ch == '|' {
out.write_string("\\|")
} else if ch == '\n' || ch == '\r' {
out.write_char(' ')
} else {
out.write_char(ch)
}
}
}