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 {
166                    rows,
167                    ..Default::default()
168                }),
169                ..Default::default()
170            });
171            continue;
172        }
173
174        if let Some(first) = ulist_item(line) {
175            flush_para(&mut blocks, &mut para);
176            let mut items = vec![first];
177            i += 1;
178            while i < n {
179                match ulist_item(lines[i]) {
180                    Some(it) => {
181                        items.push(it);
182                        i += 1;
183                    }
184                    None => break,
185                }
186            }
187            blocks.push(Block {
188                kind: BlockKind::List,
189                content: items.join("\n"),
190                ordered: Some(false),
191                ..Default::default()
192            });
193            continue;
194        }
195
196        if let Some(first) = olist_item(line) {
197            flush_para(&mut blocks, &mut para);
198            let mut items = vec![first];
199            i += 1;
200            while i < n {
201                match olist_item(lines[i]) {
202                    Some(it) => {
203                        items.push(it);
204                        i += 1;
205                    }
206                    None => break,
207                }
208            }
209            blocks.push(Block {
210                kind: BlockKind::List,
211                content: items.join("\n"),
212                ordered: Some(true),
213                ..Default::default()
214            });
215            continue;
216        }
217
218        para.push(line.trim().to_string());
219        i += 1;
220    }
221
222    flush_para(&mut blocks, &mut para);
223    blocks
224}
225
226/// md 插件:原文交给 [`parse`],整页一组块。
227pub struct MarkdownPlugin;
228
229impl FormatPlugin for MarkdownPlugin {
230    fn id(&self) -> &str {
231        "md"
232    }
233    fn formats(&self) -> &[&str] {
234        &["md"]
235    }
236    fn default_parse_chain(&self) -> Vec<&'static str> {
237        vec!["text.read.md"]
238    }
239    fn default_process_chain(&self) -> Vec<&'static str> {
240        vec![]
241    }
242    fn assemble(&self, raw: &RawDoc) -> Document {
243        let text = raw
244            .pages
245            .first()
246            .and_then(|p| p.items.first())
247            .map(|i| i.text.as_str())
248            .unwrap_or_default();
249        Document {
250            pages: vec![Page {
251                blocks: parse(text),
252                text: text.to_string(),
253                ..Default::default()
254            }],
255            ..Default::default()
256        }
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    fn kinds(blocks: &[Block]) -> Vec<BlockKind> {
265        blocks.iter().map(|b| b.kind).collect()
266    }
267
268    #[test]
269    fn parses_headings_and_paragraphs() {
270        let blocks = parse("# 标题\n正文一\n正文二\n\n## 二级\n");
271        assert_eq!(
272            kinds(&blocks),
273            vec![BlockKind::Heading, BlockKind::Paragraph, BlockKind::Heading]
274        );
275        assert_eq!(blocks[0].content, "标题");
276        assert_eq!(blocks[0].level, Some(1));
277        assert_eq!(blocks[1].content, "正文一 正文二"); // 连续行合并为一段
278        assert_eq!(blocks[2].level, Some(2));
279    }
280
281    #[test]
282    fn parses_lists_and_quote() {
283        let blocks = parse("- a\n- b\n\n1. x\n2. y\n\n> 引用\n");
284        assert_eq!(
285            kinds(&blocks),
286            vec![BlockKind::List, BlockKind::List, BlockKind::Quote]
287        );
288        assert_eq!(blocks[0].content, "a\nb");
289        assert_eq!(blocks[0].ordered, Some(false));
290        assert_eq!(blocks[1].ordered, Some(true));
291        assert_eq!(blocks[2].content, "引用");
292    }
293
294    #[test]
295    fn parses_code_fence_with_lang() {
296        let blocks = parse("```rust\nfn main() {}\n```\n");
297        assert_eq!(blocks.len(), 1);
298        assert_eq!(blocks[0].kind, BlockKind::Code);
299        assert_eq!(blocks[0].lang.as_deref(), Some("rust"));
300        assert_eq!(blocks[0].content, "fn main() {}");
301    }
302
303    #[test]
304    fn parses_pipe_table() {
305        let blocks = parse("| a | b |\n| --- | --- |\n| 1 | 2 |\n");
306        assert_eq!(blocks.len(), 1);
307        let rows = &blocks[0].table.as_ref().unwrap().rows;
308        assert_eq!(rows.len(), 2); // 表头 + 1 数据行(分隔行不计入)
309        assert_eq!(rows[0], vec!["a", "b"]);
310        assert_eq!(rows[1], vec!["1", "2"]);
311        assert_eq!(blocks[0].content, "a | b");
312    }
313
314    #[test]
315    fn plain_pipe_line_without_separator_is_paragraph() {
316        let blocks = parse("| a | b |\nnot a table\n");
317        assert_eq!(kinds(&blocks), vec![BlockKind::Paragraph]);
318    }
319
320    #[test]
321    fn unmatched_fence_does_not_hang() {
322        let blocks = parse("```\nabc");
323        assert_eq!(blocks.len(), 1);
324        assert_eq!(blocks[0].kind, BlockKind::Code);
325    }
326}