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 { 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}