1use std::mem::MaybeUninit;
2
3use crate::SpecialChar;
4use crate::section::InlineSpan;
5use crate::simd::{ByteSet, find_byte, find_byte_set};
6
7#[inline]
10fn count_leading_byte(bytes: &[u8], needle: u8) -> usize {
11 let mut n = 0;
12 while n < bytes.len() && bytes[n] == needle {
13 n += 1;
14 }
15 n
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Inline<'src> {
21 Text(&'src str),
22 Bold(InlineSpan),
23 Italic(InlineSpan),
24 Link {
25 text: InlineSpan,
26 url: &'src str,
27 title: Option<&'src str>,
28 },
29 Image {
30 alt: &'src str,
31 url: &'src str,
32 title: Option<&'src str>,
33 },
34 Code(&'src str),
35 SoftBreak,
36 HardBreak,
37}
38
39impl<'src> From<&'src str> for Inline<'src> {
40 fn from(s: &'src str) -> Self {
41 Self::Text(s)
42 }
43}
44
45static SPECIAL_SET: ByteSet = ByteSet::new(&[
47 SpecialChar::Newline.byte(),
48 SpecialChar::Asterisk.byte(),
49 SpecialChar::Underscore.byte(),
50 SpecialChar::OpenBracket.byte(),
51 SpecialChar::ExclamationMark.byte(),
52 SpecialChar::Backslash.byte(),
53 SpecialChar::Backtick.byte(),
54]);
55
56static BRACKET_CLOSE_SET: ByteSet = ByteSet::new(&[
59 SpecialChar::OpenBracket.byte(),
60 SpecialChar::CloseBracket.byte(),
61 SpecialChar::Backslash.byte(),
62]);
63static PAREN_CLOSE_SET: ByteSet = ByteSet::new(&[
64 SpecialChar::OpenParen.byte(),
65 SpecialChar::CloseParen.byte(),
66 SpecialChar::Backslash.byte(),
67]);
68
69static STAR_DELIM_SET: ByteSet =
71 ByteSet::new(&[SpecialChar::Asterisk.byte(), SpecialChar::Backslash.byte()]);
72static UNDER_DELIM_SET: ByteSet = ByteSet::new(&[
73 SpecialChar::Underscore.byte(),
74 SpecialChar::Backslash.byte(),
75]);
76
77#[derive(Clone, Copy, PartialEq, Eq)]
79enum CharClass {
80 Whitespace,
81 Punctuation,
82 Other,
83}
84
85impl CharClass {
86 const fn of(ch: char) -> Self {
87 if ch.is_whitespace() {
88 Self::Whitespace
89 } else if ch.is_ascii_punctuation() || unicode_punctuation(ch) {
90 Self::Punctuation
91 } else {
92 Self::Other
93 }
94 }
95
96 #[inline]
98 const fn of_ascii(b: u8) -> Self {
99 if b.is_ascii_whitespace() {
100 Self::Whitespace
101 } else if b.is_ascii_punctuation() {
102 Self::Punctuation
103 } else {
104 Self::Other
105 }
106 }
107}
108
109const fn unicode_punctuation(ch: char) -> bool {
113 if ch.is_ascii() {
114 return false;
115 }
116 matches!(ch,
117 '\u{00A1}'..='\u{00BF}' | '\u{2010}'..='\u{2027}' | '\u{2030}'..='\u{205E}' | '\u{2190}'..='\u{23FF}' | '\u{2500}'..='\u{2BFF}' | '\u{3000}'..='\u{303F}' | '\u{FE30}'..='\u{FE6F}' | '\u{FF01}'..='\u{FF0F}' | '\u{FF1A}'..='\u{FF20}' | '\u{FF3B}'..='\u{FF40}' | '\u{FF5B}'..='\u{FF65}' )
129}
130
131#[derive(Clone, Copy)]
135enum DelimiterAvail {
136 Both,
138 ItalicOnly,
140 BoldOnly,
142 None,
144}
145
146impl DelimiterAvail {
147 const fn can_bold(self) -> bool {
148 matches!(self, Self::Both | Self::BoldOnly)
149 }
150
151 const fn can_italic(self) -> bool {
152 matches!(self, Self::Both | Self::ItalicOnly)
153 }
154
155 const fn bold_failed(&mut self) {
156 *self = match *self {
157 Self::Both => Self::ItalicOnly,
158 Self::BoldOnly => Self::None,
159 other => other,
160 };
161 }
162
163 const fn italic_failed(&mut self) {
164 *self = match *self {
165 Self::Both => Self::BoldOnly,
166 Self::ItalicOnly => Self::None,
167 other => other,
168 };
169 }
170
171 const fn from_count(count: usize) -> Self {
172 match count {
173 0 | 1 => Self::None,
174 2 | 3 => Self::ItalicOnly,
175 _ => Self::Both,
176 }
177 }
178}
179
180struct EmphasisState {
181 star: DelimiterAvail,
182 under: DelimiterAvail,
183}
184
185impl EmphasisState {
186 const fn assume_both() -> Self {
187 Self {
188 star: DelimiterAvail::Both,
189 under: DelimiterAvail::Both,
190 }
191 }
192
193 fn from_bytes(bytes: &[u8]) -> Self {
194 static EMPH_SET: ByteSet =
195 ByteSet::new(&[SpecialChar::Asterisk.byte(), SpecialChar::Underscore.byte()]);
196 let mut stars: u8 = 0;
197 let mut unders: u8 = 0;
198 let mut i = 0;
199 loop {
200 let Some(pos) = find_byte_set(bytes, i, &EMPH_SET) else {
201 break;
202 };
203 if bytes[pos] == SpecialChar::Asterisk {
204 stars = stars.saturating_add(1);
205 } else {
206 unders = unders.saturating_add(1);
207 }
208 if stars >= 4 && unders >= 4 {
209 break;
210 }
211 i = pos + 1;
212 }
213 Self {
214 star: DelimiterAvail::from_count(stars as usize),
215 under: DelimiterAvail::from_count(unders as usize),
216 }
217 }
218
219 const fn avail_mut(&mut self, is_star: bool) -> &mut DelimiterAvail {
220 if is_star {
221 &mut self.star
222 } else {
223 &mut self.under
224 }
225 }
226}
227
228struct InlineBuf<'src, const CAP: usize> {
233 stack: [MaybeUninit<Inline<'src>>; CAP],
234 len: usize,
235 overflow: Vec<Inline<'src>>,
236}
237
238impl<'src, const CAP: usize> InlineBuf<'src, CAP> {
239 #[inline]
240 const fn new() -> Self {
241 Self {
242 stack: [const { MaybeUninit::uninit() }; CAP],
244 len: 0,
245 overflow: Vec::new(),
246 }
247 }
248
249 #[allow(clippy::inline_always)]
250 #[inline(always)]
251 fn push(&mut self, item: Inline<'src>) {
252 if self.len < CAP {
253 self.stack[self.len] = MaybeUninit::new(item);
254 self.len += 1;
255 } else {
256 self.push_slow(item);
257 }
258 }
259
260 #[cold]
261 fn push_slow(&mut self, item: Inline<'src>) {
262 if self.overflow.is_empty() {
263 self.overflow = Vec::with_capacity(CAP * 2);
265 let len = self.len;
268 let ptr = self.stack.as_ptr().cast::<Inline>();
269 let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
270 self.overflow.extend_from_slice(slice);
271 }
272 self.overflow.push(item);
273 }
274
275 #[inline]
277 const fn initialized_stack(&self) -> &[Inline<'src>] {
278 unsafe { std::slice::from_raw_parts(self.stack.as_ptr().cast::<Inline>(), self.len) }
280 }
281
282 #[inline]
283 fn flush_to_pool(self, pool: &mut Vec<Inline<'src>>) -> InlineSpan {
284 let start = pool_offset(pool.len());
285 if self.overflow.is_empty() {
286 pool.extend_from_slice(self.initialized_stack());
287 InlineSpan::new(start, pool_offset(self.len))
288 } else {
289 let len = pool_offset(self.overflow.len());
290 pool.extend(self.overflow);
291 InlineSpan::new(start, len)
292 }
293 }
294}
295
296#[allow(clippy::inline_always)]
299#[inline(always)]
300pub fn pool_offset(pool_len: usize) -> u32 {
301 u32::try_from(pool_len).expect("inline pool exceeds u32::MAX elements")
302}
303
304impl<'src> Inline<'src> {
305 const EMPH_SCAN_THRESHOLD: usize = 256;
307
308 #[must_use]
313 pub fn parse(input: &'src str, pool: &mut Vec<Self>) -> InlineSpan {
314 Self::parse_configured::<16, 32>(input, pool)
315 }
316
317 pub(crate) fn parse_configured<const MAX_DEPTH: u8, const CAP: usize>(
319 input: &'src str,
320 pool: &mut Vec<Self>,
321 ) -> InlineSpan {
322 let bytes = input.as_bytes();
323 if find_byte_set(bytes, 0, &SPECIAL_SET).is_none() {
325 if input.is_empty() {
326 return InlineSpan::EMPTY;
327 }
328 let start = pool_offset(pool.len());
329 pool.push(Self::Text(input));
330 return InlineSpan::new(start, 1);
331 }
332 let emph = if bytes.len() < Self::EMPH_SCAN_THRESHOLD {
333 EmphasisState::assume_both()
334 } else {
335 EmphasisState::from_bytes(bytes)
336 };
337 Self::parse_with_emph::<MAX_DEPTH, CAP>(input, bytes, emph, pool, 0)
338 }
339
340 fn parse_inner<const MAX_DEPTH: u8, const CAP: usize>(
341 input: &'src str,
342 pool: &mut Vec<Self>,
343 depth: u8,
344 ) -> InlineSpan {
345 let bytes = input.as_bytes();
346 Self::parse_with_emph::<MAX_DEPTH, CAP>(
347 input,
348 bytes,
349 EmphasisState::assume_both(),
350 pool,
351 depth,
352 )
353 }
354
355 fn parse_with_emph<const MAX_DEPTH: u8, const CAP: usize>(
356 input: &'src str,
357 bytes: &[u8],
358 emph: EmphasisState,
359 pool: &mut Vec<Self>,
360 depth: u8,
361 ) -> InlineSpan {
362 let mut buf = InlineBuf::<CAP>::new();
363 Self::parse_into_buf::<MAX_DEPTH, CAP>(input, bytes, emph, pool, &mut buf, depth);
364 buf.flush_to_pool(pool)
365 }
366
367 pub fn parse_flat_into(input: &'src str, pool: &mut Vec<Self>) {
374 Self::parse_flat_into_configured::<16, 32>(input, pool);
375 }
376
377 pub(crate) fn parse_flat_into_configured<const MAX_DEPTH: u8, const CAP: usize>(
379 input: &'src str,
380 pool: &mut Vec<Self>,
381 ) {
382 let bytes = input.as_bytes();
383 if find_byte_set(bytes, 0, &SPECIAL_SET).is_none() {
385 if !input.is_empty() {
386 pool.push(Self::Text(input));
387 }
388 return;
389 }
390 let emph = if bytes.len() < Self::EMPH_SCAN_THRESHOLD {
391 EmphasisState::assume_both()
392 } else {
393 EmphasisState::from_bytes(bytes)
394 };
395 let mut buf = InlineBuf::<CAP>::new();
397 Self::parse_into_buf::<MAX_DEPTH, CAP>(input, bytes, emph, pool, &mut buf, 0);
398 if buf.overflow.is_empty() {
400 pool.extend_from_slice(buf.initialized_stack());
401 } else {
402 pool.extend(buf.overflow);
403 }
404 }
405
406 fn parse_into_buf<const MAX_DEPTH: u8, const CAP: usize>(
407 input: &'src str,
408 bytes: &[u8],
409 mut emph: EmphasisState,
410 pool: &mut Vec<Self>,
411 buf: &mut InlineBuf<'src, CAP>,
412 depth: u8,
413 ) {
414 let mut plain_start = 0;
415 let mut i = 0;
416
417 while let Some(pos) = find_byte_set(bytes, i, &SPECIAL_SET) {
419 i = pos;
420 let b = bytes[i];
421
422 if b == SpecialChar::Newline {
423 Self::emit_line_break::<CAP>(input, bytes, plain_start, i, buf);
424 plain_start = i + 1;
425 i = plain_start;
426 continue;
427 }
428
429 if b == SpecialChar::Backslash
432 && let Some(&next) = bytes.get(i + 1)
433 && next.is_ascii_punctuation()
434 {
435 if let Some(text) = input.get(plain_start..i)
436 && !text.is_empty()
437 {
438 buf.push(Self::Text(text));
439 }
440 plain_start = i + 1;
441 i += 2;
442 continue;
443 }
444
445 if b == SpecialChar::Backtick
447 && let Some((code, end)) = Self::try_parse_inline_code(input, bytes, i)
448 {
449 if let Some(text) = input.get(plain_start..i)
450 && !text.is_empty()
451 {
452 buf.push(Self::Text(text));
453 }
454 buf.push(Self::Code(code));
455 plain_start = end;
456 i = end;
457 continue;
458 }
459
460 if b == SpecialChar::ExclamationMark
462 && bytes.get(i + 1) == SpecialChar::OpenBracket
463 && let Some((alt, url, title, end)) =
464 Self::try_parse_bracket_paren(input, bytes, i + 1)
465 {
466 if let Some(text) = input.get(plain_start..i)
467 && !text.is_empty()
468 {
469 buf.push(Self::Text(text));
470 }
471 buf.push(Self::Image { alt, url, title });
472 plain_start = end;
473 i = end;
474 continue;
475 }
476
477 if b == SpecialChar::OpenBracket
479 && let Some((text_str, url, title, end)) =
480 Self::try_parse_bracket_paren(input, bytes, i)
481 {
482 if let Some(text) = input.get(plain_start..i)
483 && !text.is_empty()
484 {
485 buf.push(Self::Text(text));
486 }
487 let text_span =
488 Self::parse_inner::<MAX_DEPTH, CAP>(text_str, pool, depth.saturating_add(1));
489 buf.push(Self::Link {
490 text: text_span,
491 url,
492 title,
493 });
494 plain_start = end;
495 i = end;
496 continue;
497 }
498
499 if let Some((elem, end)) = Self::try_parse_emphasis::<MAX_DEPTH, CAP>(
501 input, bytes, i, b, &mut emph, pool, depth,
502 ) {
503 if let Some(text) = input.get(plain_start..i)
504 && !text.is_empty()
505 {
506 buf.push(Self::Text(text));
507 }
508 buf.push(elem);
509 plain_start = end;
510 i = end;
511 continue;
512 }
513
514 i += 1;
515 }
516
517 if let Some(text) = input.get(plain_start..)
518 && !text.is_empty()
519 {
520 buf.push(Self::Text(text));
521 }
522 }
523
524 #[inline]
527 fn emit_line_break<const CAP: usize>(
528 input: &'src str,
529 bytes: &[u8],
530 plain_start: usize,
531 newline_pos: usize,
532 buf: &mut InlineBuf<'src, CAP>,
533 ) {
534 let preceding = bytes.get(plain_start..newline_pos).unwrap_or_default();
535 let (trim_end, is_hard) = if preceding.last() == SpecialChar::Backslash {
536 (newline_pos - 1, true)
537 } else {
538 let mut spaces = 0;
540 let mut j = preceding.len();
541 while j > 0 && preceding[j - 1] == SpecialChar::Space {
542 spaces += 1;
543 j -= 1;
544 }
545 if spaces >= 2 {
546 (newline_pos - spaces, true)
547 } else {
548 (newline_pos, false)
549 }
550 };
551 if let Some(text) = input.get(plain_start..trim_end)
552 && !text.is_empty()
553 {
554 buf.push(Self::Text(text));
555 }
556 buf.push(if is_hard {
557 Self::HardBreak
558 } else {
559 Self::SoftBreak
560 });
561 }
562
563 #[inline]
564 fn try_parse_emphasis<const MAX_DEPTH: u8, const CAP: usize>(
565 input: &'src str,
566 bytes: &[u8],
567 i: usize,
568 b: u8,
569 emph: &mut EmphasisState,
570 pool: &mut Vec<Self>,
571 depth: u8,
572 ) -> Option<(Self, usize)> {
573 let is_star = b == SpecialChar::Asterisk;
574 if !is_star && b != SpecialChar::Underscore {
575 return None;
576 }
577 if depth >= MAX_DEPTH {
579 return None;
580 }
581 let avail = emph.avail_mut(is_star);
582
583 if avail.can_bold() && bytes.get(i + 1) == Some(&b) {
585 if let Some((inner, end)) = Self::try_parse_delimited(input, bytes, i, b, 2) {
586 let span = Self::parse_inner::<MAX_DEPTH, CAP>(inner, pool, depth + 1);
587 return Some((Self::Bold(span), end));
588 }
589 avail.bold_failed();
590 }
591
592 if avail.can_italic() {
594 if let Some((inner, end)) = Self::try_parse_delimited(input, bytes, i, b, 1) {
595 let span = Self::parse_inner::<MAX_DEPTH, CAP>(inner, pool, depth + 1);
596 return Some((Self::Italic(span), end));
597 }
598 avail.italic_failed();
599 }
600
601 None
602 }
603
604 fn find_matching_close(
607 bytes: &[u8],
608 start: usize,
609 open: SpecialChar,
610 close: SpecialChar,
611 ) -> Option<usize> {
612 let set = if open == SpecialChar::OpenBracket {
614 &BRACKET_CLOSE_SET
615 } else {
616 &PAREN_CLOSE_SET
617 };
618 let mut depth = 0u32;
619 let mut j = start;
620 loop {
621 let pos = find_byte_set(bytes, j, set)?;
622 let b = bytes[pos];
623 if b == SpecialChar::Backslash
624 && bytes.get(pos + 1).is_some_and(u8::is_ascii_punctuation)
625 {
626 j = pos + 2;
627 continue;
628 }
629 if b == open {
630 depth += 1;
631 } else if b == close {
632 if depth == 0 {
633 return Some(pos);
634 }
635 depth -= 1;
636 }
637 j = pos + 1;
638 }
639 }
640
641 fn try_parse_bracket_paren(
642 input: &'src str,
643 bytes: &[u8],
644 start: usize,
645 ) -> Option<(&'src str, &'src str, Option<&'src str>, usize)> {
646 if bytes.get(start) != SpecialChar::OpenBracket {
647 return None;
648 }
649
650 let bracket_start = start + 1;
651 let bracket_end = Self::find_matching_close(
652 bytes,
653 bracket_start,
654 SpecialChar::OpenBracket,
655 SpecialChar::CloseBracket,
656 )?;
657
658 let paren_pos = bracket_end + 1;
659 if bytes.get(paren_pos) != SpecialChar::OpenParen {
660 return None;
661 }
662
663 let paren_start = paren_pos + 1;
664 let paren_end = Self::find_matching_close(
665 bytes,
666 paren_start,
667 SpecialChar::OpenParen,
668 SpecialChar::CloseParen,
669 )?;
670
671 let paren_content = input.get(paren_start..paren_end)?;
672 let (url, title) = Self::split_url_title(paren_content);
673
674 Some((
675 input.get(bracket_start..bracket_end)?,
676 url,
677 title,
678 paren_end + 1,
679 ))
680 }
681
682 fn split_url_title(content: &'src str) -> (&'src str, Option<&'src str>) {
698 let trimmed = content.trim();
699 if trimmed.len() < 3 {
702 return (trimmed, None);
703 }
704
705 let bytes = trimmed.as_bytes();
706 let last = bytes[bytes.len() - 1];
707 let (open, close) = match SpecialChar::from_byte(last) {
708 Some(SpecialChar::DoubleQuote) => (SpecialChar::DoubleQuote, SpecialChar::DoubleQuote),
709 Some(SpecialChar::SingleQuote) => (SpecialChar::SingleQuote, SpecialChar::SingleQuote),
710 Some(SpecialChar::CloseParen) => (SpecialChar::OpenParen, SpecialChar::CloseParen),
711 _ => return (trimmed, None),
713 };
714
715 let mut j = bytes.len() - 2;
717 loop {
718 if bytes[j] == open {
719 if j > 0 && bytes[j - 1].is_ascii_whitespace() {
721 let url = trimmed.get(..j).unwrap_or(trimmed).trim_end();
722 let title = trimmed.get(j + 1..bytes.len() - 1).unwrap_or("");
723 return (url, Some(title));
724 }
725 if open != close {
728 if j == 0 {
729 break;
730 }
731 j -= 1;
732 continue;
733 }
734 break;
736 }
737 if j == 0 {
738 break;
739 }
740 j -= 1;
741 }
742
743 (trimmed, None)
745 }
746
747 #[inline]
748 fn char_class_before(bytes: &[u8], pos: usize) -> CharClass {
751 if pos == 0 {
752 return CharClass::Whitespace;
753 }
754 let b = bytes[pos - 1];
755 if b < 0x80 {
757 return CharClass::of_ascii(b);
758 }
759 let mut start = pos - 1;
761 while start > 0 && bytes[start] & 0xC0 == 0x80 {
762 start -= 1;
763 }
764 let ch = std::str::from_utf8(&bytes[start..pos])
765 .ok()
766 .and_then(|s| s.chars().next())
767 .unwrap_or(' ');
768 CharClass::of(ch)
769 }
770
771 #[inline]
772 fn char_class_after(bytes: &[u8], pos: usize) -> CharClass {
775 if pos >= bytes.len() {
776 return CharClass::Whitespace;
777 }
778 let b = bytes[pos];
779 if b < 0x80 {
781 return CharClass::of_ascii(b);
782 }
783 let ch = std::str::from_utf8(&bytes[pos..])
785 .ok()
786 .and_then(|s| s.chars().next())
787 .unwrap_or(' ');
788 CharClass::of(ch)
789 }
790
791 fn try_parse_delimited(
792 input: &'src str,
793 bytes: &[u8],
794 start: usize,
795 marker: u8,
796 count: usize,
797 ) -> Option<(&'src str, usize)> {
798 let inner_start = start + count;
799 bytes.get(inner_start)?;
800
801 let is_star = marker == SpecialChar::Asterisk;
802
803 let before_open = Self::char_class_before(bytes, start);
809 let after_open = Self::char_class_after(bytes, inner_start);
810
811 let left_flanking = after_open != CharClass::Whitespace
812 && (after_open != CharClass::Punctuation || before_open != CharClass::Other);
813 if !left_flanking {
814 return None;
815 }
816 if !is_star {
817 let right_flanking_open = before_open != CharClass::Whitespace
819 && (before_open != CharClass::Punctuation || after_open != CharClass::Other);
820 if right_flanking_open && before_open != CharClass::Punctuation {
821 return None;
822 }
823 }
824
825 let delim_set = if is_star {
827 &STAR_DELIM_SET
828 } else {
829 &UNDER_DELIM_SET
830 };
831
832 let mut i = inner_start;
833 loop {
834 let Some(pos) = find_byte_set(bytes, i, delim_set) else {
835 break;
836 };
837 i = pos;
838 let b = bytes[i];
839
840 if b == SpecialChar::Backslash && bytes.get(i + 1).is_some_and(u8::is_ascii_punctuation)
841 {
842 i += 2;
843 continue;
844 }
845
846 if b != marker {
847 i += 1;
848 continue;
849 }
850
851 let all_match = (1..count).all(|j| bytes.get(i + j) == Some(&marker));
853 if !all_match {
854 i += 1;
855 continue;
856 }
857
858 let close_end = i + count;
859 let before_close = Self::char_class_before(bytes, i);
860 let after_close = Self::char_class_after(bytes, close_end);
861
862 let right_flanking = before_close != CharClass::Whitespace
867 && (before_close != CharClass::Punctuation || after_close != CharClass::Other);
868 if !right_flanking {
869 i += 1;
870 continue;
871 }
872 if !is_star {
873 let left_flanking_close = after_close != CharClass::Whitespace
875 && (after_close != CharClass::Punctuation || before_close != CharClass::Other);
876 if left_flanking_close && after_close != CharClass::Punctuation {
877 i += 1;
878 continue;
879 }
880 }
881
882 return Some((input.get(inner_start..i)?, close_end));
883 }
884
885 None
886 }
887
888 fn try_parse_inline_code(
892 input: &'src str,
893 bytes: &[u8],
894 start: usize,
895 ) -> Option<(&'src str, usize)> {
896 let backtick_count = count_leading_byte(&bytes[start..], SpecialChar::Backtick.byte());
897 if backtick_count == 0 {
898 return None;
899 }
900
901 let content_start = start + backtick_count;
902 let mut i = content_start;
903 while i < bytes.len() {
904 i = find_byte(bytes, i, SpecialChar::Backtick.byte())?;
906
907 let close_count = count_leading_byte(&bytes[i..], SpecialChar::Backtick.byte());
909
910 if close_count == backtick_count {
911 let mut cs = content_start;
914 let mut ce = i;
915 if ce - cs >= 2
916 && bytes.get(cs) == SpecialChar::Space
917 && bytes.get(ce - 1) == SpecialChar::Space
918 {
919 cs += 1;
920 ce -= 1;
921 }
922 return Some((input.get(cs..ce)?, i + close_count));
923 }
924 i += close_count;
925 }
926
927 None
928 }
929}