Skip to main content

htmd/element_handler/
code.rs

1use 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    // In faithful mode, all children of a code tag must be text to translate
17    // as markdown.
18    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    // Determine the type: inline code or a code block.
33    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    // Case: <code>There is a literal backtick (`) here</code>
115    //   to: ``There is a literal backtick (`) here``
116    let mut use_double_backticks = false;
117    // Case: <code>`starting with a backtick</code>
118    //   to: `` `starting with a backtick ``
119    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
150/// Newlines become spaces (+ an extra space if not in the middle of the code)
151fn 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}