htmd/element_handler/
code.rs1use std::rc::Rc;
2
3use html5ever::Attribute;
4use markup5ever_rcdom::{Node, NodeData};
5
6use crate::{
7 Element,
8 element_handler::{HandlerResult, Handlers, serialize_element},
9 node_util::{get_node_tag_name, get_parent_node},
10 options::{CodeBlockFence, CodeBlockStyle, TranslationMode},
11 serialize_if_faithful,
12 text_util::{JoinOnStringIterator, TrimDocumentWhitespace, concat_strings},
13};
14
15pub(super) fn code_handler(handlers: &dyn Handlers, element: Element) -> Option<HandlerResult> {
16 if handlers.options().translation_mode == TranslationMode::Faithful
19 && !element
20 .node
21 .children
22 .borrow()
23 .iter()
24 .all(|node| matches!(node.data, NodeData::Text { .. }))
25 {
26 return Some(HandlerResult {
27 content: serialize_element(handlers, &element),
28 markdown_translated: false,
29 });
30 }
31
32 let parent_node = get_parent_node(element.node);
34 let is_code_block = parent_node
35 .as_ref()
36 .map(|parent| get_node_tag_name(parent).is_some_and(|t| t == "pre"))
37 .unwrap_or(false);
38 if is_code_block {
39 handle_code_block(handlers, element, &parent_node.unwrap())
40 } else {
41 handle_inline_code(handlers, element)
42 }
43}
44
45fn handle_code_block(
46 handlers: &dyn Handlers,
47 element: Element,
48 parent: &Rc<Node>,
49) -> Option<HandlerResult> {
50 let content = handlers.walk_children(element.node).content;
51 let content = content.strip_suffix('\n').unwrap_or(&content);
52 if handlers.options().code_block_style == CodeBlockStyle::Fenced {
53 let fence = if handlers.options().code_block_fence == CodeBlockFence::Tildes {
54 get_code_fence_marker("~", content)
55 } else {
56 get_code_fence_marker("`", content)
57 };
58 let language = find_language_from_attrs(element.attrs).or_else(|| {
59 if let NodeData::Element { ref attrs, .. } = parent.data {
60 find_language_from_attrs(&attrs.borrow())
61 } else {
62 None
63 }
64 });
65 serialize_if_faithful!(handlers, element, if language.is_none() { 0 } else { 1 });
66 let mut result = String::from(&fence);
67 if let Some(ref lang) = language {
68 result.push_str(lang);
69 }
70 result.push('\n');
71 result.push_str(content);
72 result.push('\n');
73 result.push_str(&fence);
74 Some(result.into())
75 } else {
76 serialize_if_faithful!(handlers, element, 0);
77 let code = content
78 .lines()
79 .map(|line| concat_strings!(" ", line))
80 .join("\n");
81 Some(code.into())
82 }
83}
84
85fn get_code_fence_marker(symbol: &str, content: &str) -> String {
86 let three_chars = symbol.repeat(3);
87 if content.contains(&three_chars) {
88 let four_chars = symbol.repeat(4);
89 if content.contains(&four_chars) {
90 symbol.repeat(5)
91 } else {
92 four_chars
93 }
94 } else {
95 three_chars
96 }
97}
98
99fn find_language_from_attrs(attrs: &[Attribute]) -> Option<String> {
100 attrs
101 .iter()
102 .find(|attr| &attr.name.local == "class")
103 .map(|attr| {
104 attr.value
105 .split(' ')
106 .find(|cls| cls.starts_with("language-"))
107 .map(|lang| lang.split('-').skip(1).join("-"))
108 })
109 .unwrap_or(None)
110}
111
112fn handle_inline_code(handlers: &dyn Handlers, element: Element) -> Option<HandlerResult> {
113 serialize_if_faithful!(handlers, element, 0);
114 let mut use_double_backticks = false;
117 let mut surround_with_spaces = false;
120 let content = handlers.walk_children(element.node).content;
121 let chars = content.chars().collect::<Vec<char>>();
122 let len = chars.len();
123 for (idx, c) in chars.iter().enumerate() {
124 if c == &'`' {
125 let prev = if idx > 0 { chars[idx - 1] } else { '\0' };
126 let next = if idx < len - 1 { chars[idx + 1] } else { '\0' };
127 if prev != '`' && next != '`' {
128 use_double_backticks = true;
129 surround_with_spaces = idx == 0;
130 break;
131 }
132 }
133 }
134 let content = if handlers.options().preformatted_code {
135 handle_preformatted_code(&content)
136 } else {
137 content.trim_document_whitespace().to_string()
138 };
139 if use_double_backticks {
140 if surround_with_spaces {
141 Some(concat_strings!("`` ", content, " ``").into())
142 } else {
143 Some(concat_strings!("``", content, "``").into())
144 }
145 } else {
146 Some(concat_strings!("`", content, "`").into())
147 }
148}
149
150fn handle_preformatted_code(code: &str) -> String {
152 let mut result = String::new();
153 let mut is_prev_ch_new_line = false;
154 let mut in_middle = false;
155 for ch in code.chars() {
156 if ch == '\n' {
157 result.push(' ');
158 is_prev_ch_new_line = true;
159 } else {
160 if is_prev_ch_new_line && !in_middle {
161 result.push(' ');
162 }
163 result.push(ch);
164 is_prev_ch_new_line = false;
165 in_middle = true;
166 }
167 }
168 if is_prev_ch_new_line {
169 result.push(' ');
170 }
171 result
172}