Skip to main content

docparser_core/plugins/
csv.rs

1//! CSV 解析(RFC4180 语义):双引号包裹、`""` 转义、字段内逗号/换行、CRLF。
2//!
3//! 对齐 Python `csv.reader` 的常用行为:空行不算一行记录。
4//! 不做分隔符嗅探(`,` 固定),也不做类型推断——表格原样进 IR。
5
6use crate::model::{Block, BlockKind, Document, Page, TableData};
7use crate::plugin::{FormatPlugin, RawDoc};
8
9/// 解析 CSV 文本为行 × 列。
10pub 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
60/// 收行:丢弃整行为空的记录(对齐 `csv.reader` 对空行的处理)。
61fn 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
71/// csv 插件:一页一个表格块。
72pub 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}