Skip to main content

docparser_core/
emit.rs

1//! 输出目标:text / markdown(JSON 直接走 serde)。
2
3use crate::model::{BlockKind, Document};
4
5/// 纯文本:逐页 text,页间空行分隔。
6pub fn to_text(doc: &Document) -> String {
7    doc.pages
8        .iter()
9        .filter(|p| !p.text.trim().is_empty())
10        .map(|p| p.text.as_str())
11        .collect::<Vec<_>>()
12        .join("\n\n")
13}
14
15fn table_md(rows: &[Vec<String>]) -> String {
16    if rows.is_empty() {
17        return String::new();
18    }
19    let width = rows.iter().map(|r| r.len()).max().unwrap_or(0);
20    let norm: Vec<Vec<String>> = rows
21        .iter()
22        .map(|r| {
23            let mut v = r.clone();
24            while v.len() < width {
25                v.push(String::new());
26            }
27            v
28        })
29        .collect();
30    let cell = |r: &[String]| format!("| {} |", r.join(" | "));
31    let mut out = vec![cell(&norm[0])];
32    out.push(format!("| {} |", vec!["---"; width].join(" | ")));
33    for r in &norm[1..] {
34        out.push(cell(r));
35    }
36    out.join("\n")
37}
38
39fn block_md(b: &crate::model::Block) -> String {
40    match b.kind {
41        BlockKind::Heading => {
42            let lvl = (b.level.unwrap_or(1) as usize).clamp(1, 6);
43            format!("{} {}", "#".repeat(lvl), b.content)
44        }
45        BlockKind::List => {
46            let prefix = if b.ordered.unwrap_or(false) {
47                "1."
48            } else {
49                "-"
50            };
51            b.content
52                .lines()
53                .filter(|l| !l.trim().is_empty())
54                .map(|l| format!("{prefix} {l}"))
55                .collect::<Vec<_>>()
56                .join("\n")
57        }
58        BlockKind::Code => format!(
59            "```{}\n{}\n```",
60            b.lang.clone().unwrap_or_default(),
61            b.content
62        ),
63        BlockKind::Table => b
64            .table
65            .as_ref()
66            .map(|t| table_md(&t.rows))
67            .unwrap_or_default(),
68        BlockKind::Quote => b
69            .content
70            .lines()
71            .map(|l| format!("> {l}"))
72            .collect::<Vec<_>>()
73            .join("\n"),
74        _ => b.content.clone(),
75    }
76}
77
78/// Markdown:按块渲染(表格 → Markdown 表格)。
79///
80/// 相邻同类型列表块(如 HTML 的 `<li>` 逐条产出)合并为一个列表,避免中间插空行
81/// 被渲染成多个「松散列表」;跨页不合并。
82pub fn to_markdown(doc: &Document) -> String {
83    let mut out: Vec<String> = Vec::new();
84    for page in &doc.pages {
85        let mut prev_list: Option<bool> = None;
86        for b in page
87            .blocks
88            .iter()
89            .filter(|b| !b.content.is_empty() || b.table.is_some())
90        {
91            let rendered = block_md(b);
92            if rendered.is_empty() {
93                continue;
94            }
95            if b.kind == BlockKind::List {
96                let ordered = b.ordered.unwrap_or(false);
97                if prev_list == Some(ordered) {
98                    if let Some(last) = out.last_mut() {
99                        last.push('\n');
100                        last.push_str(&rendered);
101                    }
102                    continue;
103                }
104                prev_list = Some(ordered);
105            } else {
106                prev_list = None;
107            }
108            out.push(rendered);
109        }
110    }
111    out.join("\n\n")
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use crate::model::{Block, BlockKind, Document, Page};
118
119    fn list(ordered: bool, content: &str) -> Block {
120        Block {
121            kind: BlockKind::List,
122            content: content.to_string(),
123            ordered: Some(ordered),
124            ..Default::default()
125        }
126    }
127
128    #[test]
129    fn adjacent_lists_merge_same_kind_only() {
130        let doc = Document {
131            pages: vec![Page {
132                blocks: vec![
133                    Block {
134                        kind: BlockKind::Heading,
135                        content: "T".into(),
136                        level: Some(2),
137                        ..Default::default()
138                    },
139                    list(false, "a"),
140                    list(false, "b"),
141                    list(true, "x"),
142                ],
143                ..Default::default()
144            }],
145            ..Default::default()
146        };
147        let md = to_markdown(&doc);
148        assert_eq!(md, "## T\n\n- a\n- b\n\n1. x");
149    }
150
151    #[test]
152    fn table_rendered_as_markdown() {
153        let doc = Document {
154            pages: vec![Page {
155                blocks: vec![Block {
156                    kind: BlockKind::Table,
157                    table: Some(crate::model::TableData {
158                        rows: vec![vec!["a".into(), "b".into()], vec!["1".into(), "2".into()]],
159                    }),
160                    ..Default::default()
161                }],
162                ..Default::default()
163            }],
164            ..Default::default()
165        };
166        assert_eq!(to_markdown(&doc), "| a | b |\n| --- | --- |\n| 1 | 2 |");
167    }
168}