Skip to main content

rdocx_html/
lib.rs

1#![doc = include_str!("../README.md")]
2
3mod css;
4mod emitter;
5mod markdown;
6mod sanitize;
7
8use std::collections::HashMap;
9
10use rdocx_oxml::document::CT_Document;
11use rdocx_oxml::numbering::CT_Numbering;
12use rdocx_oxml::styles::CT_Styles;
13
14/// Options for HTML conversion.
15#[derive(Debug, Clone)]
16pub struct HtmlOptions {
17    /// Whether to inline images as base64 data URIs (default: true).
18    pub inline_images: bool,
19}
20
21impl Default for HtmlOptions {
22    fn default() -> Self {
23        Self {
24            inline_images: true,
25        }
26    }
27}
28
29/// Input for HTML conversion.
30pub struct HtmlInput {
31    pub document: CT_Document,
32    pub styles: CT_Styles,
33    pub numbering: Option<CT_Numbering>,
34    /// Images keyed by embed/relationship ID.
35    pub images: HashMap<String, ImageData>,
36    /// Hyperlink URLs keyed by relationship ID.
37    pub hyperlink_urls: HashMap<String, String>,
38}
39
40/// Image data for HTML embedding.
41pub struct ImageData {
42    pub data: Vec<u8>,
43    pub content_type: String,
44}
45
46/// Convert a DOCX document to a complete HTML document string.
47pub fn to_html_document(input: &HtmlInput, options: &HtmlOptions) -> String {
48    let body = to_html_fragment(input, options);
49    let css = css::generate_base_css();
50    format!(
51        "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<style>\n{css}\n</style>\n</head>\n<body>\n{body}\n</body>\n</html>"
52    )
53}
54
55/// Convert a DOCX document to an HTML fragment (body content only).
56pub fn to_html_fragment(input: &HtmlInput, options: &HtmlOptions) -> String {
57    emitter::emit_body(
58        &input.document.body,
59        &input.styles,
60        input.numbering.as_ref(),
61        &input.images,
62        &input.hyperlink_urls,
63        options,
64    )
65}
66
67/// Convert a DOCX document to Markdown.
68pub fn to_markdown(input: &HtmlInput) -> String {
69    markdown::emit_markdown(
70        &input.document.body,
71        &input.styles,
72        input.numbering.as_ref(),
73        &input.hyperlink_urls,
74    )
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use rdocx_oxml::document::{BodyContent, CT_Document};
81    use rdocx_oxml::styles::CT_Styles;
82    use rdocx_oxml::text::CT_P;
83
84    fn simple_input(text: &str) -> HtmlInput {
85        let mut doc = CT_Document::new();
86        let mut p = CT_P::new();
87        p.add_run(text);
88        doc.body.add_paragraph(p);
89
90        HtmlInput {
91            document: doc,
92            styles: CT_Styles::new_default(),
93            numbering: None,
94            images: HashMap::new(),
95            hyperlink_urls: HashMap::new(),
96        }
97    }
98
99    #[test]
100    fn html_document_basic() {
101        let input = simple_input("Hello, World!");
102        let html = to_html_document(&input, &HtmlOptions::default());
103        assert!(html.contains("<!DOCTYPE html>"));
104        assert!(html.contains("Hello, World!"));
105        assert!(html.contains("<p"));
106    }
107
108    #[test]
109    fn html_fragment_basic() {
110        let input = simple_input("Test paragraph");
111        let html = to_html_fragment(&input, &HtmlOptions::default());
112        assert!(html.contains("Test paragraph"));
113        assert!(html.contains("<p"));
114        assert!(!html.contains("<!DOCTYPE"));
115    }
116
117    #[test]
118    fn markdown_basic() {
119        let input = simple_input("Test paragraph");
120        let md = to_markdown(&input);
121        assert!(md.contains("Test paragraph"));
122    }
123
124    #[test]
125    fn html_heading() {
126        let mut doc = CT_Document::new();
127        let mut p = CT_P::new();
128        p.add_run("Chapter 1");
129        p.properties = Some(rdocx_oxml::properties::CT_PPr {
130            style_id: Some("Heading1".to_string()),
131            ..Default::default()
132        });
133        doc.body.add_paragraph(p);
134
135        let input = HtmlInput {
136            document: doc,
137            styles: CT_Styles::new_default(),
138            numbering: None,
139            images: HashMap::new(),
140            hyperlink_urls: HashMap::new(),
141        };
142
143        let html = to_html_fragment(&input, &HtmlOptions::default());
144        assert!(html.contains("<h1"));
145        assert!(html.contains("Chapter 1"));
146    }
147
148    #[test]
149    fn html_table() {
150        let mut doc = CT_Document::new();
151        let mut tbl = rdocx_oxml::table::CT_Tbl::new();
152        let mut row = rdocx_oxml::table::CT_Row::new();
153        let mut cell = rdocx_oxml::table::CT_Tc::new();
154        let mut p = CT_P::new();
155        p.add_run("Cell text");
156        cell.content = vec![rdocx_oxml::table::CellContent::Paragraph(p)];
157        row.cells.push(cell);
158        tbl.rows.push(row);
159        doc.body.content.push(BodyContent::Table(tbl));
160
161        let input = HtmlInput {
162            document: doc,
163            styles: CT_Styles::new_default(),
164            numbering: None,
165            images: HashMap::new(),
166            hyperlink_urls: HashMap::new(),
167        };
168
169        let html = to_html_fragment(&input, &HtmlOptions::default());
170        assert!(html.contains("<table"));
171        assert!(html.contains("<td"));
172        assert!(html.contains("Cell text"));
173    }
174
175    #[test]
176    fn markdown_heading() {
177        let mut doc = CT_Document::new();
178        let mut p = CT_P::new();
179        p.add_run("Title");
180        p.properties = Some(rdocx_oxml::properties::CT_PPr {
181            style_id: Some("Heading1".to_string()),
182            ..Default::default()
183        });
184        doc.body.add_paragraph(p);
185
186        let input = HtmlInput {
187            document: doc,
188            styles: CT_Styles::new_default(),
189            numbering: None,
190            images: HashMap::new(),
191            hyperlink_urls: HashMap::new(),
192        };
193
194        let md = to_markdown(&input);
195        assert!(md.contains("# Title"));
196    }
197
198    #[test]
199    fn html_bold_italic() {
200        let mut doc = CT_Document::new();
201        let mut p = CT_P::new();
202        let mut r = rdocx_oxml::text::CT_R::new("bold text");
203        r.properties = Some(rdocx_oxml::properties::CT_RPr {
204            bold: Some(true),
205            italic: Some(true),
206            ..Default::default()
207        });
208        p.runs.push(r);
209        doc.body.add_paragraph(p);
210
211        let input = HtmlInput {
212            document: doc,
213            styles: CT_Styles::new_default(),
214            numbering: None,
215            images: HashMap::new(),
216            hyperlink_urls: HashMap::new(),
217        };
218
219        let html = to_html_fragment(&input, &HtmlOptions::default());
220        assert!(html.contains("<strong"));
221        assert!(html.contains("<em"));
222        assert!(html.contains("bold text"));
223    }
224}