docparser_core/plugins/
csv.rs1use crate::model::{Block, BlockKind, Document, Page, TableData};
7use crate::plugin::{FormatPlugin, RawDoc};
8
9pub fn parse_rows(text: &str) -> Vec<Vec<String>> {
11 let mut rows: Vec<Vec<String>> = Vec::new();
12 let mut row: Vec<String> = Vec::new();
13 let mut field = String::new();
14 let mut in_quotes = false;
15 let mut row_touched = false;
16 let mut chars = text.chars().peekable();
17
18 while let Some(c) = chars.next() {
19 match c {
20 '"' if in_quotes => {
21 if chars.peek() == Some(&'"') {
22 field.push('"');
23 chars.next();
24 } else {
25 in_quotes = false;
26 }
27 }
28 '"' if field.is_empty() && !in_quotes => {
29 in_quotes = true;
30 row_touched = true;
31 }
32 ',' if !in_quotes => {
33 row.push(std::mem::take(&mut field));
34 row_touched = true;
35 }
36 '\r' if !in_quotes => {
37 if chars.peek() == Some(&'\n') {
38 chars.next();
39 }
40 row.push(std::mem::take(&mut field));
41 push_row(&mut rows, &mut row, &mut row_touched);
42 }
43 '\n' if !in_quotes => {
44 row.push(std::mem::take(&mut field));
45 push_row(&mut rows, &mut row, &mut row_touched);
46 }
47 _ => {
48 field.push(c);
49 row_touched = true;
50 }
51 }
52 }
53 if row_touched || !field.is_empty() {
54 row.push(field);
55 rows.push(row);
56 }
57 rows
58}
59
60fn push_row(rows: &mut Vec<Vec<String>>, row: &mut Vec<String>, row_touched: &mut bool) {
62 let empty = row.len() == 1 && row[0].is_empty();
63 if !empty {
64 rows.push(std::mem::take(row));
65 } else {
66 row.clear();
67 }
68 *row_touched = false;
69}
70
71pub struct CsvPlugin;
73
74impl FormatPlugin for CsvPlugin {
75 fn id(&self) -> &str {
76 "csv"
77 }
78 fn formats(&self) -> &[&str] {
79 &["csv"]
80 }
81 fn default_parse_chain(&self) -> Vec<&'static str> {
82 vec!["text.read.csv"]
83 }
84 fn default_process_chain(&self) -> Vec<&'static str> {
85 vec![]
86 }
87 fn assemble(&self, raw: &RawDoc) -> Document {
88 let rows: Vec<Vec<String>> = raw
89 .pages
90 .first()
91 .and_then(|p| p.tables.first().cloned())
92 .unwrap_or_default();
93 let text = rows
94 .iter()
95 .map(|r| r.join(","))
96 .collect::<Vec<_>>()
97 .join("\n");
98 let blocks = if rows.is_empty() {
99 vec![]
100 } else {
101 vec![Block {
102 kind: BlockKind::Table,
103 content: rows[0].join(" | "),
104 table: Some(TableData { rows }),
105 ..Default::default()
106 }]
107 };
108 Document {
109 pages: vec![Page {
110 blocks,
111 text,
112 ..Default::default()
113 }],
114 ..Default::default()
115 }
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn plain_rows() {
125 assert_eq!(
126 parse_rows("a,b\n1,2\n"),
127 vec![vec!["a", "b"], vec!["1", "2"]]
128 );
129 }
130
131 #[test]
132 fn quoted_field_with_comma_and_newline() {
133 let rows = parse_rows("name,note\n\"张,三\",\"第一行\n第二行\"\n");
134 assert_eq!(rows[1][0], "张,三");
135 assert_eq!(rows[1][1], "第一行\n第二行");
136 }
137
138 #[test]
139 fn escaped_quotes_and_crlf() {
140 let rows = parse_rows("a,\"say \"\"hi\"\"\"\r\nb,c\r\n");
141 assert_eq!(rows[0], vec!["a", "say \"hi\""]);
142 assert_eq!(rows[1], vec!["b", "c"]);
143 }
144
145 #[test]
146 fn blank_lines_are_skipped() {
147 assert_eq!(parse_rows("a\n\nb\n"), vec![vec!["a"], vec!["b"]]);
148 }
149
150 #[test]
151 fn empty_input_is_empty() {
152 assert!(parse_rows("").is_empty());
153 }
154
155 #[test]
156 fn plugin_emits_table_block() {
157 let raw = RawDoc {
158 pages: vec![crate::plugin::RawPage {
159 tables: vec![vec![
160 vec!["h1".into(), "h2".into()],
161 vec!["1".into(), "2".into()],
162 ]],
163 ..Default::default()
164 }],
165 ..Default::default()
166 };
167 let doc = CsvPlugin.assemble(&raw);
168 assert_eq!(doc.pages[0].blocks.len(), 1);
169 assert_eq!(doc.pages[0].blocks[0].kind, BlockKind::Table);
170 assert_eq!(doc.pages[0].text, "h1,h2\n1,2");
171 }
172}