oxc_markdown_parser/syntax/
decode.rs1use std::borrow::Cow;
9
10use super::entities;
11use crate::syntax::escapes_next;
12use crate::{ast::Destination, pos::Segment};
13
14pub fn decode(raw: &str, escapes: bool) -> Cow<'_, str> {
18 if !raw.bytes().any(|b| b == b'\\' || b == b'&') {
19 return Cow::Borrowed(raw);
20 }
21 let mut out = String::with_capacity(raw.len());
22 let bytes = raw.as_bytes();
23 let mut i = 0;
24 while i < bytes.len() {
25 match bytes[i] {
26 b'\\' if escapes && escapes_next(bytes, i) => {
27 out.push(bytes[i + 1] as char);
28 i += 2;
29 }
30 b'&' => {
31 if let Some((text, end)) = entity(raw, i) {
32 out.push_str(&text);
33 i = end;
34 } else {
35 out.push('&');
36 i += 1;
37 }
38 }
39 _ => {
40 let Some(c) = raw[i..].chars().next() else { break };
41 out.push(c);
42 i += c.len_utf8();
43 }
44 }
45 }
46 Cow::Owned(out)
47}
48
49pub fn entity(raw: &str, at: usize) -> Option<(Cow<'static, str>, usize)> {
52 let rest = &raw.as_bytes()[at + 1..];
53 if rest.first() == Some(&b'#') {
54 let (digits, radix, skip) = if matches!(rest.get(1), Some(b'x' | b'X')) {
55 (&rest[2..], 16u32, 2)
56 } else {
57 (&rest[1..], 10u32, 1)
58 };
59 let max_len = if radix == 16 { 6 } else { 7 };
60 let len = digits
61 .iter()
62 .take_while(|b| b.is_ascii_digit() || (radix == 16 && b.is_ascii_hexdigit()))
63 .count();
64 if len == 0 || len > max_len || digits.get(len) != Some(&b';') {
65 return None;
66 }
67 let text = std::str::from_utf8(&digits[..len]).ok()?;
68 let value = u32::from_str_radix(text, radix).ok()?;
69 let c = match char::from_u32(value) {
70 Some(c) if value != 0 => c,
71 _ => '\u{FFFD}',
72 };
73 return Some((Cow::Owned(c.to_string()), at + 1 + skip + len + 1));
74 }
75 let len = rest.iter().take_while(|b| b.is_ascii_alphanumeric()).count();
79 if len == 0 || len > 48 || rest.get(len) != Some(&b';') {
80 return None;
81 }
82 let name = std::str::from_utf8(&rest[..len]).ok()?;
83 let value = entities::lookup(name)?;
84 Some((Cow::Borrowed(value), at + 1 + len + 1))
85}
86
87pub fn title(source: &str, pieces: &[Segment]) -> String {
91 let joined = join_stripped(source, pieces, usize::MAX);
92 decode(&joined[1..joined.len() - 1], true).into_owned()
93}
94
95pub fn html_inline(source: &str, pieces: &[Segment]) -> String {
100 join_stripped(source, pieces, 3)
101}
102
103fn join_stripped(source: &str, pieces: &[Segment], max: usize) -> String {
111 let mut out = String::new();
112 for (i, piece) in pieces.iter().enumerate() {
113 let mut text = piece.span.slice(source);
114 let mut padding = usize::from(piece.padding);
115 if i > 0 {
116 out.push('\n');
117 let mut cols = padding.min(max);
118 padding -= cols;
119 while cols < max {
120 match text.as_bytes().first() {
121 Some(b' ') => cols += 1,
122 Some(b'\t') => cols += 4 - cols % 4,
123 _ => break,
124 }
125 text = &text[1..];
126 }
127 }
128 out.extend(std::iter::repeat_n(' ', padding));
129 out.push_str(text);
130 }
131 out
132}
133
134pub fn destination<'s>(source: &'s str, destination: &Destination) -> Cow<'s, str> {
137 let raw = destination.span.slice(source);
138 let raw = if destination.angle_bracketed { &raw[1..raw.len() - 1] } else { raw };
139 decode(raw, true)
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::Span;
146
147 #[test]
148 fn decodes_escapes_and_entities() {
149 assert!(matches!(decode("plain", true), Cow::Borrowed(_)));
150 assert_eq!(decode(r"\*a\\b\c", true), r"*a\b\c");
151 assert_eq!(decode("& # A &unknown; &", true), "& # A &unknown; &");
152 assert_eq!(decode(r"\&", false), r"\&");
154 assert_eq!(decode(r"\&", true), "&");
155 }
156
157 #[test]
158 fn entity_recognition() {
159 assert_eq!(entity("&x", 0), Some((Cow::Borrowed("&"), 5)));
160 assert_eq!(entity("�", 0), Some((Cow::Borrowed("\u{FFFD}"), 4)));
161 assert_eq!(entity("�", 0), Some((Cow::Borrowed("\u{FFFD}"), 8)));
162 assert_eq!(entity("&", 0), None);
163 assert_eq!(entity("&#;", 0), None);
164 assert_eq!(entity("�", 0), None, "more than 7 decimal digits");
165 }
166
167 #[test]
168 fn title_and_html_inline() {
169 let source = "\"a\n b\"";
170 let pieces = [Segment::new(Span::new(0, 2), 0), Segment::new(Span::new(3, 9), 0)];
171 assert_eq!(title(source, &pieces), "a\nb");
172
173 let source = "<b\n c>";
175 let pieces = [Segment::new(Span::new(0, 2), 0), Segment::new(Span::new(3, 10), 0)];
176 assert_eq!(html_inline(source, &pieces), "<b\n c>");
177 let pieces = [Segment::new(Span::new(0, 2), 0), Segment::new(Span::new(8, 10), 2)];
178 assert_eq!(html_inline(source, &pieces), "<b\nc>");
179 }
180}