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#[derive(Debug, Clone)]
16pub struct HtmlOptions {
17 pub inline_images: bool,
19}
20
21impl Default for HtmlOptions {
22 fn default() -> Self {
23 Self {
24 inline_images: true,
25 }
26 }
27}
28
29pub struct HtmlInput {
31 pub document: CT_Document,
32 pub styles: CT_Styles,
33 pub numbering: Option<CT_Numbering>,
34 pub images: HashMap<String, ImageData>,
36 pub hyperlink_urls: HashMap<String, String>,
38}
39
40pub struct ImageData {
42 pub data: Vec<u8>,
43 pub content_type: String,
44}
45
46pub 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
55pub 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
67pub 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}