Skip to main content

docparser_core/plugins/
markdown.rs

1//! Markdown 块级解析:直接解语法,不渲染成 PDF 再反推。
2//!
3//! 对齐 Python `MdParseCapability._parse_md`:标题 / 无序列表 / 有序列表 / 引用 /
4//! 代码围栏 / 管道表格 / 段落(连续行以空格合并)。
5//! 这是**块级**解析,不做行内语法(粗体/链接/行内代码)渲染,`content` 保留原文。
6
7use crate::model::{Block, BlockKind, Document, Page, TableData};
8use crate::plugin::{FormatPlugin, RawDoc};
9
10/// `#{1,6} ` 标题。
11fn heading(line: &str) -> Option<(u8, String)> {
12    let t = line.trim_start();
13    let hashes = t.chars().take_while(|c| *c == '#').count();
14    if hashes == 0 || hashes > 6 {
15        return None;
16    }
17    let rest = &t[hashes..];
18    if !(rest.starts_with(' ') || rest.starts_with('\t')) {
19        return None;
20    }
21    Some((hashes as u8, rest.trim().to_string()))
22}
23
24/// `> ` 引用。
25fn quote(line: &str) -> Option<String> {
26    let t = line.trim_start();
27    let rest = t.strip_prefix('>')?;
28    Some(rest.strip_prefix(' ').unwrap_or(rest).trim().to_string())
29}
30
31/// `- / * / + ` 无序列表项。
32fn ulist_item(line: &str) -> Option<String> {
33    let t = line.trim_start();
34    for marker in ["- ", "* ", "+ "] {
35        if let Some(rest) = t.strip_prefix(marker) {
36            return Some(rest.trim().to_string());
37        }
38    }
39    None
40}
41
42/// `1. / 1)` 有序列表项。
43fn olist_item(line: &str) -> Option<String> {
44    let t = line.trim_start();
45    let digits = t.chars().take_while(|c| c.is_ascii_digit()).count();
46    if digits == 0 {
47        return None;
48    }
49    let rest = &t[digits..];
50    let rest = rest.strip_prefix('.').or_else(|| rest.strip_prefix(')'))?;
51    if !(rest.starts_with(' ') || rest.starts_with('\t')) {
52        return None;
53    }
54    Some(rest.trim().to_string())
55}
56
57/// 表格分隔行:仅由 `| - : 空白` 组成,且同时含 `-` 与 `|`。
58fn is_table_sep(line: &str) -> bool {
59    let t = line.trim();
60    !t.is_empty()
61        && t.chars().all(|c| matches!(c, '|' | '-' | ':' | ' ' | '\t'))
62        && t.contains('-')
63        && t.contains('|')
64}
65
66fn split_row(line: &str) -> Vec<String> {
67    let t = line.trim();
68    let t = t.strip_prefix('|').unwrap_or(t);
69    let t = t.strip_suffix('|').unwrap_or(t);
70    t.split('|').map(|c| c.trim().to_string()).collect()
71}
72
73fn flush_para(blocks: &mut Vec<Block>, para: &mut Vec<String>) {
74    if para.is_empty() {
75        return;
76    }
77    let content = para.join(" ");
78    para.clear();
79    let content = content.trim();
80    if !content.is_empty() {
81        blocks.push(Block {
82            kind: BlockKind::Paragraph,
83            content: content.to_string(),
84            ..Default::default()
85        });
86    }
87}
88
89/// 解析 Markdown 文本为块序列。
90pub fn parse(text: &str) -> Vec<Block> {
91    let lines: Vec<&str> = text.lines().collect();
92    let n = lines.len();
93    let mut blocks: Vec<Block> = Vec::new();
94    let mut para: Vec<String> = Vec::new();
95    let mut i = 0usize;
96
97    while i < n {
98        let line = lines[i];
99        let trimmed = line.trim_start();
100
101        // 代码围栏
102        if trimmed.starts_with("```") {
103            flush_para(&mut blocks, &mut para);
104            let lang = trimmed.trim_start_matches('`').trim().to_string();
105            let mut buf: Vec<&str> = Vec::new();
106            i += 1;
107            while i < n && !lines[i].trim_start().starts_with("```") {
108                buf.push(lines[i]);
109                i += 1;
110            }
111            if i < n {
112                i += 1; // 结束围栏
113            }
114            blocks.push(Block {
115                kind: BlockKind::Code,
116                content: buf.join("\n"),
117                lang: if lang.is_empty() { None } else { Some(lang) },
118                ..Default::default()
119            });
120            continue;
121        }
122
123        if line.trim().is_empty() {
124            flush_para(&mut blocks, &mut para);
125            i += 1;
126            continue;
127        }
128
129        if let Some((level, content)) = heading(line) {
130            flush_para(&mut blocks, &mut para);
131            blocks.push(Block {
132                kind: BlockKind::Heading,
133                content,
134                level: Some(level),
135                ..Default::default()
136            });
137            i += 1;
138            continue;
139        }
140
141        if let Some(content) = quote(line) {
142            flush_para(&mut blocks, &mut para);
143            blocks.push(Block {
144                kind: BlockKind::Quote,
145                content,
146                ..Default::default()
147            });
148            i += 1;
149            continue;
150        }
151
152        // 管道表格(必须有分隔行,否则按段落处理)
153        if trimmed.starts_with('|') && i + 1 < n && is_table_sep(lines[i + 1]) {
154            flush_para(&mut blocks, &mut para);
155            let header = split_row(line);
156            let mut rows: Vec<Vec<String>> = vec![header.clone()];
157            i += 2;
158            while i < n && lines[i].trim_start().starts_with('|') {
159                rows.push(split_row(lines[i]));
160                i += 1;
161            }
162            blocks.push(Block {
163                kind: BlockKind::Table,
164                content: header.join(" | "),
165                table: Some(TableData { rows }),
166                ..Default::default()
167            });
168            continue;
169        }
170
171        if let Some(first) = ulist_item(line) {
172            flush_para(&mut blocks, &mut para);
173            let mut items = vec![first];
174            i += 1;
175            while i < n {
176                match ulist_item(lines[i]) {
177                    Some(it) => {
178                        items.push(it);
179                        i += 1;
180                    }
181                    None => break,
182                }
183            }
184            blocks.push(Block {
185                kind: BlockKind::List,
186                content: items.join("\n"),
187                ordered: Some(false),
188                ..Default::default()
189            });
190            continue;
191        }
192
193        if let Some(first) = olist_item(line) {
194            flush_para(&mut blocks, &mut para);
195            let mut items = vec![first];
196            i += 1;
197            while i < n {
198                match olist_item(lines[i]) {
199                    Some(it) => {
200                        items.push(it);
201                        i += 1;
202                    }
203                    None => break,
204                }
205            }
206            blocks.push(Block {
207                kind: BlockKind::List,
208                content: items.join("\n"),
209                ordered: Some(true),
210                ..Default::default()
211            });
212            continue;
213        }
214
215        para.push(line.trim().to_string());
216        i += 1;
217    }
218
219    flush_para(&mut blocks, &mut para);
220    blocks
221}
222
223/// md 插件:原文交给 [`parse`],整页一组块。
224pub struct MarkdownPlugin;
225
226impl FormatPlugin for MarkdownPlugin {
227    fn id(&self) -> &str {
228        "md"
229    }
230    fn formats(&self) -> &[&str] {
231        &["md"]
232    }
233    fn default_parse_chain(&self) -> Vec<&'static str> {
234        vec!["text.read.md"]
235    }
236    fn default_process_chain(&self) -> Vec<&'static str> {
237        vec![]
238    }
239    fn assemble(&self, raw: &RawDoc) -> Document {
240        let text = raw
241            .pages
242            .first()
243            .and_then(|p| p.items.first())
244            .map(|i| i.text.as_str())
245            .unwrap_or_default();
246        Document {
247            pages: vec![Page {
248                blocks: parse(text),
249                text: text.to_string(),
250                ..Default::default()
251            }],
252            ..Default::default()
253        }
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    fn kinds(blocks: &[Block]) -> Vec<BlockKind> {
262        blocks.iter().map(|b| b.kind).collect()
263    }
264
265    #[test]
266    fn parses_headings_and_paragraphs() {
267        let blocks = parse("# 标题\n正文一\n正文二\n\n## 二级\n");
268        assert_eq!(
269            kinds(&blocks),
270            vec![BlockKind::Heading, BlockKind::Paragraph, BlockKind::Heading]
271        );
272        assert_eq!(blocks[0].content, "标题");
273        assert_eq!(blocks[0].level, Some(1));
274        assert_eq!(blocks[1].content, "正文一 正文二"); // 连续行合并为一段
275        assert_eq!(blocks[2].level, Some(2));
276    }
277
278    #[test]
279    fn parses_lists_and_quote() {
280        let blocks = parse("- a\n- b\n\n1. x\n2. y\n\n> 引用\n");
281        assert_eq!(
282            kinds(&blocks),
283            vec![BlockKind::List, BlockKind::List, BlockKind::Quote]
284        );
285        assert_eq!(blocks[0].content, "a\nb");
286        assert_eq!(blocks[0].ordered, Some(false));
287        assert_eq!(blocks[1].ordered, Some(true));
288        assert_eq!(blocks[2].content, "引用");
289    }
290
291    #[test]
292    fn parses_code_fence_with_lang() {
293        let blocks = parse("```rust\nfn main() {}\n```\n");
294        assert_eq!(blocks.len(), 1);
295        assert_eq!(blocks[0].kind, BlockKind::Code);
296        assert_eq!(blocks[0].lang.as_deref(), Some("rust"));
297        assert_eq!(blocks[0].content, "fn main() {}");
298    }
299
300    #[test]
301    fn parses_pipe_table() {
302        let blocks = parse("| a | b |\n| --- | --- |\n| 1 | 2 |\n");
303        assert_eq!(blocks.len(), 1);
304        let rows = &blocks[0].table.as_ref().unwrap().rows;
305        assert_eq!(rows.len(), 2); // 表头 + 1 数据行(分隔行不计入)
306        assert_eq!(rows[0], vec!["a", "b"]);
307        assert_eq!(rows[1], vec!["1", "2"]);
308        assert_eq!(blocks[0].content, "a | b");
309    }
310
311    #[test]
312    fn plain_pipe_line_without_separator_is_paragraph() {
313        let blocks = parse("| a | b |\nnot a table\n");
314        assert_eq!(kinds(&blocks), vec![BlockKind::Paragraph]);
315    }
316
317    #[test]
318    fn unmatched_fence_does_not_hang() {
319        let blocks = parse("```\nabc");
320        assert_eq!(blocks.len(), 1);
321        assert_eq!(blocks[0].kind, BlockKind::Code);
322    }
323}