1use serde::Deserialize;
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Deserialize)]
6pub struct HtmlConfig {
7 pub html: HtmlOptions,
9 pub elements: ElementOptions,
11 pub attributes: AttributeMappings,
13 pub syntect: Option<crate::html::syntect::SyntectConfigStyle>,
15}
16#[derive(Debug, Clone, Deserialize)]
18pub struct HtmlOptions {
19 pub escape_html: bool,
21 pub break_on_newline: bool,
23 pub xhtml_style: bool,
25 pub pretty_print: bool,
27}
28
29#[derive(Debug, Clone, Deserialize)]
31pub struct ElementOptions {
32 pub headings: HeadingOptions,
34 pub links: LinkOptions,
36 pub code_blocks: CodeBlockOptions,
38}
39
40#[derive(Debug, Clone, Deserialize)]
42pub struct HeadingOptions {
43 pub add_ids: bool,
45 pub id_prefix: String,
47 #[serde(deserialize_with = "deserialize_heading_map")]
49 pub level_classes: HashMap<u8, String>,
50}
51
52#[derive(Debug, Clone, Deserialize)]
54pub struct LinkOptions {
55 pub nofollow_external: bool,
57 pub open_external_blank: bool,
59}
60
61#[derive(Debug, Clone, Deserialize)]
63pub struct CodeBlockOptions {
64 pub default_language: Option<String>,
66 pub line_numbers: bool,
68}
69
70#[derive(Debug, Clone, Deserialize)]
72pub struct AttributeMappings {
73 #[serde(deserialize_with = "deserialize_nested_string_map")]
75 pub element_attributes: HashMap<String, HashMap<String, String>>,
76}
77
78impl Default for HtmlConfig {
79 fn default() -> Self {
80 HtmlConfig {
81 html: HtmlOptions {
82 escape_html: false,
83 break_on_newline: true,
84 xhtml_style: false,
85 pretty_print: true,
86 },
87 elements: ElementOptions {
88 headings: HeadingOptions {
89 add_ids: true,
90 id_prefix: "heading-".to_string(),
91 level_classes: HashMap::new(),
92 },
93 links: LinkOptions {
94 nofollow_external: true,
95 open_external_blank: true,
96 },
97 code_blocks: CodeBlockOptions {
98 default_language: None,
99 line_numbers: false,
100 },
101 },
102 attributes: AttributeMappings {
103 element_attributes: HashMap::new(),
104 },
105 #[cfg(feature = "syntect")]
106 syntect: None,
107 }
108 }
109}
110
111fn deserialize_heading_map<'de, D>(deserializer: D) -> Result<HashMap<u8, String>, D::Error>
112where
113 D: serde::Deserializer<'de>,
114{
115 use serde::de::Error;
116 use serde_json::Value;
117
118 let value = Value::deserialize(deserializer)?;
119
120 match value {
121 Value::Object(map) => {
122 let mut result = HashMap::new();
123 for (k, v) in map {
124 let level = k.parse::<u8>().map_err(D::Error::custom)?;
125 if !(1..=6).contains(&level) {
126 return Err(D::Error::custom(format!(
127 "heading level must be between 1 and 6, got {}",
128 level
129 )));
130 }
131 let class = v
132 .as_str()
133 .ok_or_else(|| D::Error::custom("value must be a string"))?
134 .to_string();
135 result.insert(level, class);
136 }
137 Ok(result)
138 }
139 _ => Err(D::Error::custom("expected a map")),
140 }
141}
142
143fn deserialize_nested_string_map<'de, D>(
144 deserializer: D,
145) -> Result<HashMap<String, HashMap<String, String>>, D::Error>
146where
147 D: serde::Deserializer<'de>,
148{
149 use serde::de::Error;
150 use serde_json::Value;
151
152 let value = Value::deserialize(deserializer)?;
153
154 match value {
155 Value::Object(outer_map) => {
156 let mut result = HashMap::new();
157 for (outer_key, inner_value) in outer_map {
158 match inner_value {
159 Value::Object(inner_map) => {
160 let mut inner_result = HashMap::new();
161 for (inner_key, value) in inner_map {
162 let str_value = value
163 .as_str()
164 .ok_or_else(|| D::Error::custom("value must be a string"))?
165 .to_string();
166 inner_result.insert(inner_key, str_value);
167 }
168 result.insert(outer_key, inner_result);
169 }
170 _ => return Err(D::Error::custom("expected a nested map")),
171 }
172 }
173 Ok(result)
174 }
175 _ => Err(D::Error::custom("expected a map")),
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use serde_json::json;
183
184 #[test]
185 fn test_default_config() {
186 let config = HtmlConfig::default();
187 assert!(!config.html.escape_html);
188 assert!(config.html.break_on_newline);
189 assert!(!config.html.xhtml_style);
190 assert!(config.html.pretty_print);
191 }
192
193 #[test]
194 fn test_heading_map_deserialization() {
195 let json = json!({
196 "1": "title",
197 "2": "subtitle",
198 "6": "small-title"
199 });
200
201 let map: HashMap<u8, String> = deserialize_heading_map(json).unwrap();
202 assert_eq!(map.get(&1).unwrap(), "title");
203 assert_eq!(map.get(&2).unwrap(), "subtitle");
204 assert_eq!(map.get(&6).unwrap(), "small-title");
205 }
206
207 #[test]
208 fn test_invalid_heading_level() {
209 let json = json!({
210 "7": "invalid"
211 });
212
213 let result: Result<HashMap<u8, String>, _> = deserialize_heading_map(json);
214 assert!(result.is_err());
215 }
216
217 #[test]
218 fn test_attribute_map_deserialization() {
219 let json = json!({
220 "h1": {
221 "class": "title",
222 "data-level": "1"
223 },
224 "pre": {
225 "class": "code-block"
226 }
227 });
228
229 let map: HashMap<String, HashMap<String, String>> =
230 deserialize_nested_string_map(json).unwrap();
231 assert_eq!(map.get("h1").unwrap().get("class").unwrap(), "title");
232 assert_eq!(map.get("h1").unwrap().get("data-level").unwrap(), "1");
233 assert_eq!(map.get("pre").unwrap().get("class").unwrap(), "code-block");
234 }
235}