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 {
105 rows,
106 ..Default::default()
107 }),
108 ..Default::default()
109 }]
110 };
111 Document {
112 pages: vec![Page {
113 blocks,
114 text,
115 ..Default::default()
116 }],
117 ..Default::default()
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn plain_rows() {
128 assert_eq!(
129 parse_rows("a,b\n1,2\n"),
130 vec![vec!["a", "b"], vec!["1", "2"]]
131 );
132 }
133
134 #[test]
135 fn quoted_field_with_comma_and_newline() {
136 let rows = parse_rows("name,note\n\"张,三\",\"第一行\n第二行\"\n");
137 assert_eq!(rows[1][0], "张,三");
138 assert_eq!(rows[1][1], "第一行\n第二行");
139 }
140
141 #[test]
142 fn escaped_quotes_and_crlf() {
143 let rows = parse_rows("a,\"say \"\"hi\"\"\"\r\nb,c\r\n");
144 assert_eq!(rows[0], vec!["a", "say \"hi\""]);
145 assert_eq!(rows[1], vec!["b", "c"]);
146 }
147
148 #[test]
149 fn blank_lines_are_skipped() {
150 assert_eq!(parse_rows("a\n\nb\n"), vec![vec!["a"], vec!["b"]]);
151 }
152
153 #[test]
154 fn empty_input_is_empty() {
155 assert!(parse_rows("").is_empty());
156 }
157
158 #[test]
159 fn plugin_emits_table_block() {
160 let raw = RawDoc {
161 pages: vec![crate::plugin::RawPage {
162 tables: vec![vec![
163 vec!["h1".into(), "h2".into()],
164 vec!["1".into(), "2".into()],
165 ]],
166 ..Default::default()
167 }],
168 ..Default::default()
169 };
170 let doc = CsvPlugin.assemble(&raw);
171 assert_eq!(doc.pages[0].blocks.len(), 1);
172 assert_eq!(doc.pages[0].blocks[0].kind, BlockKind::Table);
173 assert_eq!(doc.pages[0].text, "h1,h2\n1,2");
174 }
175}