satteri_pulldown_cmark/
utils.rs1use alloc::borrow::Cow;
13use alloc::string::String;
14use core::ops::Range;
15
16use crate::{CowStr, Event};
17
18#[must_use]
21pub fn strip_leading_bom(source: &str) -> &str {
22 source.strip_prefix('\u{feff}').unwrap_or(source)
23}
24
25pub fn decode_html_entities(s: &str) -> Cow<'_, str> {
33 if !s.contains('&') {
34 return Cow::Borrowed(s);
35 }
36 let bytes = s.as_bytes();
37 let mut out = String::with_capacity(s.len());
38 let mut i = 0;
39 while i < bytes.len() {
40 if bytes[i] == b'&' {
41 let (consumed, replacement) = crate::scanners::scan_entity(&bytes[i..]);
42 if consumed > 0 {
43 if let Some(rep) = replacement {
44 out.push_str(&rep);
45 }
46 i += consumed;
47 continue;
48 }
49 }
50 let b = bytes[i];
51 let ch_len = if b < 0xC0 {
52 1
53 } else if b < 0xE0 {
54 2
55 } else if b < 0xF0 {
56 3
57 } else {
58 4
59 };
60 out.push_str(&s[i..i + ch_len]);
61 i += ch_len;
62 }
63 Cow::Owned(out)
64}
65
66#[derive(Debug)]
68pub struct TextMergeStream<'a, I> {
69 inner: TextMergeWithOffset<'a, DummyOffsets<I>>,
70}
71
72impl<'a, I> TextMergeStream<'a, I>
73where
74 I: Iterator<Item = Event<'a>>,
75{
76 pub fn new(iter: I) -> Self {
77 Self {
78 inner: TextMergeWithOffset::new(DummyOffsets(iter)),
79 }
80 }
81}
82
83impl<'a, I> Iterator for TextMergeStream<'a, I>
84where
85 I: Iterator<Item = Event<'a>>,
86{
87 type Item = Event<'a>;
88
89 fn next(&mut self) -> Option<Self::Item> {
90 self.inner.next().map(|(event, _)| event)
91 }
92}
93
94#[derive(Debug)]
95struct DummyOffsets<I>(I);
96
97impl<'a, I> Iterator for DummyOffsets<I>
98where
99 I: Iterator<Item = Event<'a>>,
100{
101 type Item = (Event<'a>, Range<usize>);
102
103 fn next(&mut self) -> Option<Self::Item> {
104 self.0.next().map(|event| (event, 0..0))
105 }
106}
107
108#[derive(Debug)]
112pub struct TextMergeWithOffset<'a, I> {
113 iter: I,
114 last_event: Option<(Event<'a>, Range<usize>)>,
115}
116
117impl<'a, I> TextMergeWithOffset<'a, I>
118where
119 I: Iterator<Item = (Event<'a>, Range<usize>)>,
120{
121 pub fn new(iter: I) -> Self {
122 Self {
123 iter,
124 last_event: None,
125 }
126 }
127
128 pub fn inner(&self) -> &I {
130 &self.iter
131 }
132}
133
134impl<'a, I> Iterator for TextMergeWithOffset<'a, I>
135where
136 I: Iterator<Item = (Event<'a>, Range<usize>)>,
137{
138 type Item = (Event<'a>, Range<usize>);
139
140 fn next(&mut self) -> Option<Self::Item> {
141 match (self.last_event.take(), self.iter.next()) {
142 (
143 Some((Event::Text(last_text), last_offset)),
144 Some((Event::Text(next_text), next_offset)),
145 ) => {
146 let mut string_buf: String = last_text.into_string();
148 string_buf.push_str(&next_text);
149 let mut offset = last_offset;
150 offset.end = next_offset.end;
151 loop {
152 match self.iter.next() {
154 Some((Event::Text(next_text), next_offset)) => {
155 string_buf.push_str(&next_text);
156 offset.end = next_offset.end;
157 }
158 next_event => {
159 self.last_event = next_event;
160 if string_buf.is_empty() {
161 break self.next();
163 } else {
164 break Some((
165 Event::Text(CowStr::Boxed(string_buf.into_boxed_str())),
166 offset,
167 ));
168 }
169 }
170 }
171 }
172 }
173 (None, Some(next_event)) => {
174 self.last_event = Some(next_event);
176 self.next()
177 }
178 (None, None) => {
179 None
181 }
182 (last_event, next_event) => {
183 self.last_event = next_event;
185 last_event
186 }
187 }
188 }
189}
190
191#[cfg(test)]
192mod test {
193 use alloc::vec::Vec;
194
195 use super::*;
196 use crate::Parser;
197
198 #[test]
199 fn text_merge_stream_indent() {
200 let source = r#"
201 first line
202 second line
203"#;
204 let parser = TextMergeStream::new(Parser::new(source));
205 let text_events: Vec<_> = parser.filter(|e| matches!(e, Event::Text(_))).collect();
206 assert_eq!(
207 text_events,
208 [Event::Text("first line\nsecond line\n".into())]
209 );
210 }
211
212 #[test]
213 fn text_merge_with_offset_indent() {
214 let source = r#"
215 first line
216 second line
217"#;
218 let parser = TextMergeWithOffset::new(Parser::new(source).into_offset_iter());
219 let text_events: Vec<_> = parser
220 .filter(|e| matches!(e, (Event::Text(_), _)))
221 .collect();
222 assert_eq!(
223 text_events,
224 [(Event::Text("first line\nsecond line\n".into()), 5..32)]
225 );
226 }
227
228 #[test]
229 fn text_merge_empty_is_discarded() {
230 let events = [
231 Event::Rule,
232 Event::Text("".into()),
233 Event::Text("".into()),
234 Event::Rule,
235 ];
236 let result: Vec<_> = TextMergeStream::new(events.into_iter()).collect();
237 assert_eq!(result, [Event::Rule, Event::Rule]);
238 }
239}