1use std::mem::MaybeUninit;
2
3use crate::OffsetExt;
4use crate::SpecialChar;
5use crate::section::InlineSpan;
6use crate::simd::{ByteSet, ByteSliceExt};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Inline<'src> {
11 Text(&'src str),
12 Bold(InlineSpan),
13 Italic(InlineSpan),
14 Link {
15 text: InlineSpan,
16 url: &'src str,
17 title: Option<&'src str>,
18 },
19 Image {
20 alt: &'src str,
21 url: &'src str,
22 title: Option<&'src str>,
23 },
24 Code(&'src str),
25 SoftBreak,
26 HardBreak,
27}
28
29impl<'src> From<&'src str> for Inline<'src> {
30 fn from(s: &'src str) -> Self {
31 Self::Text(s)
32 }
33}
34
35static SPECIAL_SET: ByteSet = ByteSet::new(&[
37 SpecialChar::Newline.byte(),
38 SpecialChar::Asterisk.byte(),
39 SpecialChar::Underscore.byte(),
40 SpecialChar::OpenBracket.byte(),
41 SpecialChar::ExclamationMark.byte(),
42 SpecialChar::Backslash.byte(),
43 SpecialChar::Backtick.byte(),
44]);
45
46static BRACKET_CLOSE_SET: ByteSet = ByteSet::new(&[
49 SpecialChar::OpenBracket.byte(),
50 SpecialChar::CloseBracket.byte(),
51 SpecialChar::Backslash.byte(),
52]);
53static PAREN_CLOSE_SET: ByteSet = ByteSet::new(&[
54 SpecialChar::OpenParen.byte(),
55 SpecialChar::CloseParen.byte(),
56 SpecialChar::Backslash.byte(),
57]);
58
59static STAR_DELIM_SET: ByteSet =
61 ByteSet::new(&[SpecialChar::Asterisk.byte(), SpecialChar::Backslash.byte()]);
62static UNDER_DELIM_SET: ByteSet = ByteSet::new(&[
63 SpecialChar::Underscore.byte(),
64 SpecialChar::Backslash.byte(),
65]);
66
67#[derive(Clone, Copy, PartialEq, Eq)]
69enum CharClass {
70 Whitespace,
71 Punctuation,
72 Other,
73}
74
75impl CharClass {
76 const fn of(ch: char) -> Self {
77 if ch.is_whitespace() {
78 Self::Whitespace
79 } else if ch.is_ascii_punctuation() || Self::unicode_punctuation(ch) {
80 Self::Punctuation
81 } else {
82 Self::Other
83 }
84 }
85
86 #[inline]
88 const fn of_ascii(b: u8) -> Self {
89 if b.is_ascii_whitespace() {
90 Self::Whitespace
91 } else if b.is_ascii_punctuation() {
92 Self::Punctuation
93 } else {
94 Self::Other
95 }
96 }
97
98 const fn unicode_punctuation(ch: char) -> bool {
101 if ch.is_ascii() {
102 return false;
103 }
104 matches!(ch,
105 '\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}' )
117 }
118}
119
120#[derive(Clone, Copy)]
124enum DelimiterAvail {
125 Both,
127 ItalicOnly,
129 BoldOnly,
131 None,
133}
134
135impl DelimiterAvail {
136 const fn can_bold(self) -> bool {
137 matches!(self, Self::Both | Self::BoldOnly)
138 }
139
140 const fn can_italic(self) -> bool {
141 matches!(self, Self::Both | Self::ItalicOnly)
142 }
143
144 const fn bold_failed(&mut self) {
145 *self = match *self {
146 Self::Both => Self::ItalicOnly,
147 Self::BoldOnly => Self::None,
148 other => other,
149 };
150 }
151
152 const fn italic_failed(&mut self) {
153 *self = match *self {
154 Self::Both => Self::BoldOnly,
155 Self::ItalicOnly => Self::None,
156 other => other,
157 };
158 }
159
160 const fn from_count(count: usize) -> Self {
161 match count {
162 0 | 1 => Self::None,
163 2 => Self::BoldOnly,
164 _ => Self::Both,
165 }
166 }
167}
168
169struct EmphasisState {
170 star: DelimiterAvail,
171 under: DelimiterAvail,
172}
173
174impl EmphasisState {
175 const fn assume_both() -> Self {
176 Self {
177 star: DelimiterAvail::Both,
178 under: DelimiterAvail::Both,
179 }
180 }
181
182 fn from_bytes(bytes: &[u8]) -> Self {
183 static EMPH_SET: ByteSet =
184 ByteSet::new(&[SpecialChar::Asterisk.byte(), SpecialChar::Underscore.byte()]);
185 let mut stars: u8 = 0;
186 let mut unders: u8 = 0;
187 let mut i = 0;
188 while let Some(pos) = bytes.find_byte_set(i, &EMPH_SET) {
189 if bytes[pos] == SpecialChar::Asterisk {
190 stars = stars.saturating_add(1);
191 } else {
192 unders = unders.saturating_add(1);
193 }
194 if stars >= 4 && unders >= 4 {
195 break;
196 }
197 i = pos + 1;
198 }
199 Self {
200 star: DelimiterAvail::from_count(stars as usize),
201 under: DelimiterAvail::from_count(unders as usize),
202 }
203 }
204
205 const fn avail_mut(&mut self, is_star: bool) -> &mut DelimiterAvail {
206 if is_star {
207 &mut self.star
208 } else {
209 &mut self.under
210 }
211 }
212}
213
214struct InlineBuf<'src, const CAP: usize> {
219 stack: [MaybeUninit<Inline<'src>>; CAP],
220 len: usize,
221 overflow: Vec<Inline<'src>>,
222}
223
224impl<'src, const CAP: usize> InlineBuf<'src, CAP> {
225 #[inline]
226 const fn new() -> Self {
227 Self {
228 stack: [const { MaybeUninit::uninit() }; CAP],
230 len: 0,
231 overflow: Vec::new(),
232 }
233 }
234
235 #[allow(clippy::inline_always)]
236 #[inline(always)]
237 fn push(&mut self, item: Inline<'src>) {
238 if self.len < CAP {
239 self.stack[self.len] = MaybeUninit::new(item);
240 self.len += 1;
241 } else {
242 self.push_slow(item);
243 }
244 }
245
246 #[cold]
247 fn push_slow(&mut self, item: Inline<'src>) {
248 if self.overflow.is_empty() {
249 self.overflow = Vec::with_capacity(CAP * 2);
251 let len = self.len;
254 let ptr = self.stack.as_ptr().cast::<Inline>();
255 let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
256 self.overflow.extend_from_slice(slice);
257 }
258 self.overflow.push(item);
259 }
260
261 #[inline]
263 const fn initialized_stack(&self) -> &[Inline<'src>] {
264 unsafe { std::slice::from_raw_parts(self.stack.as_ptr().cast::<Inline>(), self.len) }
266 }
267
268 #[inline]
269 fn flush_to_pool(self, pool: &mut Vec<Inline<'src>>) -> InlineSpan {
270 let start = pool.len().pool_offset();
271 if self.overflow.is_empty() {
272 pool.extend_from_slice(self.initialized_stack());
273 InlineSpan::new(start, self.len.pool_offset())
274 } else {
275 let len = self.overflow.len().pool_offset();
276 pool.extend(self.overflow);
277 InlineSpan::new(start, len)
278 }
279 }
280}
281
282const EMPH_SCAN_THRESHOLD: usize = 256;
284
285pub struct InlineParser<'src, 'pool, const MAX_DEPTH: u8, const CAP: usize> {
291 input: &'src str,
292 pool: &'pool mut Vec<Inline<'src>>,
293}
294
295impl<'src, 'pool, const MAX_DEPTH: u8, const CAP: usize> InlineParser<'src, 'pool, MAX_DEPTH, CAP> {
296 const fn new(input: &'src str, pool: &'pool mut Vec<Inline<'src>>) -> Self {
297 Self { input, pool }
298 }
299
300 pub(crate) fn parse_configured(
302 input: &'src str,
303 pool: &'pool mut Vec<Inline<'src>>,
304 ) -> InlineSpan {
305 Self::new(input, pool).parse()
306 }
307
308 pub(crate) fn parse_flat_into_configured(input: &'src str, pool: &'pool mut Vec<Inline<'src>>) {
311 Self::new(input, pool).parse_flat();
312 }
313
314 #[must_use]
319 fn parse(&mut self) -> InlineSpan {
320 self.parse_at_depth(0)
321 }
322
323 fn parse_at_depth(&mut self, depth: u8) -> InlineSpan {
324 let bytes = self.input.as_bytes();
325 if bytes.find_byte_set(0, &SPECIAL_SET).is_none() {
327 if self.input.is_empty() {
328 return InlineSpan::EMPTY;
329 }
330 let start = self.pool.len().pool_offset();
331 self.pool.push(Inline::Text(self.input));
332 return InlineSpan::new(start, 1);
333 }
334 let emph = if bytes.len() < EMPH_SCAN_THRESHOLD {
335 EmphasisState::assume_both()
336 } else {
337 EmphasisState::from_bytes(bytes)
338 };
339 let mut buf = InlineBuf::<CAP>::new();
340 self.parse_into_buf(bytes, emph, &mut buf, depth);
341 buf.flush_to_pool(self.pool)
342 }
343
344 fn parse_inner(&mut self, input: &'src str, depth: u8) -> InlineSpan {
345 InlineParser::<MAX_DEPTH, CAP> {
346 input,
347 pool: self.pool,
348 }
349 .parse_at_depth(depth)
350 }
351
352 fn parse_flat(&mut self) {
356 let bytes = self.input.as_bytes();
357 if bytes.find_byte_set(0, &SPECIAL_SET).is_none() {
359 if !self.input.is_empty() {
360 self.pool.push(Inline::Text(self.input));
361 }
362 return;
363 }
364 let emph = if bytes.len() < EMPH_SCAN_THRESHOLD {
365 EmphasisState::assume_both()
366 } else {
367 EmphasisState::from_bytes(bytes)
368 };
369 let mut buf = InlineBuf::<CAP>::new();
371 self.parse_into_buf(bytes, emph, &mut buf, 0);
372 if buf.overflow.is_empty() {
374 self.pool.extend_from_slice(buf.initialized_stack());
375 } else {
376 self.pool.extend(buf.overflow);
377 }
378 }
379
380 fn parse_into_buf(
381 &mut self,
382 bytes: &[u8],
383 mut emph: EmphasisState,
384 buf: &mut InlineBuf<'src, CAP>,
385 depth: u8,
386 ) {
387 let mut plain_start = 0;
388 let mut i = 0;
389
390 while let Some(pos) = bytes.find_byte_set(i, &SPECIAL_SET) {
392 i = pos;
393 let b = bytes[i];
394
395 if b == SpecialChar::Newline {
396 self.emit_line_break(bytes, plain_start, i, buf);
397 plain_start = i + 1;
398 i = plain_start;
399 continue;
400 }
401
402 if b == SpecialChar::Backslash
405 && let Some(&next) = bytes.get(i + 1)
406 && next.is_ascii_punctuation()
407 {
408 if let Some(text) = self.input.get(plain_start..i)
409 && !text.is_empty()
410 {
411 buf.push(Inline::Text(text));
412 }
413 plain_start = i + 1;
414 i += 2;
415 continue;
416 }
417
418 if b == SpecialChar::Backtick
420 && let Some((code, end)) = Self::try_parse_inline_code(self.input, bytes, i)
421 {
422 if let Some(text) = self.input.get(plain_start..i)
423 && !text.is_empty()
424 {
425 buf.push(Inline::Text(text));
426 }
427 buf.push(Inline::Code(code));
428 plain_start = end;
429 i = end;
430 continue;
431 }
432
433 if b == SpecialChar::ExclamationMark
435 && bytes.get(i + 1) == SpecialChar::OpenBracket
436 && let Some((alt, url, title, end)) =
437 Self::try_parse_bracket_paren(self.input, bytes, i + 1)
438 {
439 if let Some(text) = self.input.get(plain_start..i)
440 && !text.is_empty()
441 {
442 buf.push(Inline::Text(text));
443 }
444 buf.push(Inline::Image { alt, url, title });
445 plain_start = end;
446 i = end;
447 continue;
448 }
449
450 if b == SpecialChar::OpenBracket
452 && let Some((text_str, url, title, end)) =
453 Self::try_parse_bracket_paren(self.input, bytes, i)
454 {
455 if let Some(text) = self.input.get(plain_start..i)
456 && !text.is_empty()
457 {
458 buf.push(Inline::Text(text));
459 }
460 let text_span = self.parse_inner(text_str, depth.saturating_add(1));
461 buf.push(Inline::Link {
462 text: text_span,
463 url,
464 title,
465 });
466 plain_start = end;
467 i = end;
468 continue;
469 }
470
471 if let Some((elem, end)) = self.try_parse_emphasis(bytes, i, b, &mut emph, depth) {
473 if let Some(text) = self.input.get(plain_start..i)
474 && !text.is_empty()
475 {
476 buf.push(Inline::Text(text));
477 }
478 buf.push(elem);
479 plain_start = end;
480 i = end;
481 continue;
482 }
483
484 i += 1;
485 }
486
487 if let Some(text) = self.input.get(plain_start..)
488 && !text.is_empty()
489 {
490 buf.push(Inline::Text(text));
491 }
492 }
493
494 #[inline]
497 fn emit_line_break(
498 &self,
499 bytes: &[u8],
500 plain_start: usize,
501 newline_pos: usize,
502 buf: &mut InlineBuf<'src, CAP>,
503 ) {
504 let preceding = bytes.get(plain_start..newline_pos).unwrap_or_default();
505 let (trim_end, is_hard) = if preceding.last() == SpecialChar::Backslash {
506 (newline_pos - 1, true)
507 } else {
508 let mut spaces = 0;
510 let mut j = preceding.len();
511 while j > 0 && preceding[j - 1] == SpecialChar::Space {
512 spaces += 1;
513 j -= 1;
514 }
515 if spaces >= 2 {
516 (newline_pos - spaces, true)
517 } else {
518 (newline_pos, false)
519 }
520 };
521 if let Some(text) = self.input.get(plain_start..trim_end)
522 && !text.is_empty()
523 {
524 buf.push(Inline::Text(text));
525 }
526 buf.push(if is_hard {
527 Inline::HardBreak
528 } else {
529 Inline::SoftBreak
530 });
531 }
532
533 #[inline]
534 fn try_parse_emphasis(
535 &mut self,
536 bytes: &[u8],
537 i: usize,
538 b: u8,
539 emph: &mut EmphasisState,
540 depth: u8,
541 ) -> Option<(Inline<'src>, usize)> {
542 let is_star = b == SpecialChar::Asterisk;
543 if !is_star && b != SpecialChar::Underscore {
544 return None;
545 }
546 if depth >= MAX_DEPTH {
548 return None;
549 }
550 let avail = emph.avail_mut(is_star);
551 let open_run = if is_star {
552 SpecialChar::Asterisk.count_leading_bytes(&bytes[i..])
553 } else {
554 SpecialChar::Underscore.count_leading_bytes(&bytes[i..])
555 };
556
557 if open_run >= 3 && avail.can_bold() && avail.can_italic() {
561 if let Some((inner, end)) = Self::try_parse_delimited(self.input, bytes, i, b, 3) {
562 let close_run_start = end - 3;
566 let exact_close =
567 bytes.get(close_run_start - 1) != Some(&b) && bytes.get(end) != Some(&b);
568 if exact_close {
569 let inner_span = self.parse_inner(inner, depth + 1);
570 let bold_start = self.pool.len().pool_offset();
571 self.pool.push(Inline::Bold(inner_span));
572 let bold_span = InlineSpan::new(bold_start, 1);
573 return Some((Inline::Italic(bold_span), end));
574 }
575 }
576 }
579
580 if avail.can_bold() && bytes.get(i + 1) == Some(&b) {
582 if let Some((inner, end)) = Self::try_parse_delimited(self.input, bytes, i, b, 2) {
583 let span = self.parse_inner(inner, depth + 1);
584 return Some((Inline::Bold(span), end));
585 }
586 avail.bold_failed();
587 }
588
589 if avail.can_italic() {
591 if let Some((inner, end)) = Self::try_parse_delimited(self.input, bytes, i, b, 1) {
592 let span = self.parse_inner(inner, depth + 1);
593 return Some((Inline::Italic(span), end));
594 }
595 avail.italic_failed();
596 }
597
598 None
599 }
600
601 fn find_matching_close(
604 bytes: &[u8],
605 start: usize,
606 open: SpecialChar,
607 close: SpecialChar,
608 ) -> Option<usize> {
609 let set = if open == SpecialChar::OpenBracket {
611 &BRACKET_CLOSE_SET
612 } else {
613 &PAREN_CLOSE_SET
614 };
615 let mut nested = 0u32;
616 let mut j = start;
617 loop {
618 let pos = bytes.find_byte_set(j, set)?;
619 let b = bytes[pos];
620 if b == SpecialChar::Backslash
621 && bytes.get(pos + 1).is_some_and(u8::is_ascii_punctuation)
622 {
623 j = pos + 2;
624 continue;
625 }
626 if b == open {
627 nested += 1;
628 } else if b == close {
629 if nested == 0 {
630 return Some(pos);
631 }
632 nested -= 1;
633 }
634 j = pos + 1;
635 }
636 }
637
638 fn try_parse_bracket_paren(
639 input: &'src str,
640 bytes: &[u8],
641 start: usize,
642 ) -> Option<(&'src str, &'src str, Option<&'src str>, usize)> {
643 if bytes.get(start) != SpecialChar::OpenBracket {
644 return None;
645 }
646
647 let bracket_start = start + 1;
648 let bracket_end = Self::find_matching_close(
649 bytes,
650 bracket_start,
651 SpecialChar::OpenBracket,
652 SpecialChar::CloseBracket,
653 )?;
654
655 let paren_pos = bracket_end + 1;
656 if bytes.get(paren_pos) != SpecialChar::OpenParen {
657 return None;
658 }
659
660 let paren_start = paren_pos + 1;
661 let paren_end = Self::find_matching_close(
662 bytes,
663 paren_start,
664 SpecialChar::OpenParen,
665 SpecialChar::CloseParen,
666 )?;
667
668 let paren_content = input.get(paren_start..paren_end)?;
669 let (url, title) = Self::split_url_title(paren_content);
670
671 Some((
672 input.get(bracket_start..bracket_end)?,
673 url,
674 title,
675 paren_end + 1,
676 ))
677 }
678
679 fn split_url_title(content: &'src str) -> (&'src str, Option<&'src str>) {
695 let trimmed = content.trim();
696 if trimmed.len() < 3 {
699 return (trimmed, None);
700 }
701
702 let bytes = trimmed.as_bytes();
703 let last = bytes[bytes.len() - 1];
704 let (open, close) = match SpecialChar::from_byte(last) {
705 Some(SpecialChar::DoubleQuote) => (SpecialChar::DoubleQuote, SpecialChar::DoubleQuote),
706 Some(SpecialChar::SingleQuote) => (SpecialChar::SingleQuote, SpecialChar::SingleQuote),
707 Some(SpecialChar::CloseParen) => (SpecialChar::OpenParen, SpecialChar::CloseParen),
708 _ => return (trimmed, None),
710 };
711
712 let mut j = bytes.len() - 2;
714 loop {
715 if bytes[j] == open {
716 if j > 0 && bytes[j - 1].is_ascii_whitespace() {
718 let url = trimmed.get(..j).unwrap_or(trimmed).trim_end();
719 let title = trimmed.get(j + 1..bytes.len() - 1).unwrap_or("");
720 return (url, Some(title));
721 }
722 if open != close {
725 if j == 0 {
726 break;
727 }
728 j -= 1;
729 continue;
730 }
731 break;
733 }
734 if j == 0 {
735 break;
736 }
737 j -= 1;
738 }
739
740 (trimmed, None)
742 }
743
744 #[inline]
745 fn char_class_before(bytes: &[u8], pos: usize) -> CharClass {
748 if pos == 0 {
749 return CharClass::Whitespace;
750 }
751 let b = bytes[pos - 1];
752 if b < 0x80 {
754 return CharClass::of_ascii(b);
755 }
756 let mut start = pos - 1;
758 while start > 0 && bytes[start] & 0xC0 == 0x80 {
759 start -= 1;
760 }
761 let ch = std::str::from_utf8(&bytes[start..pos])
762 .ok()
763 .and_then(|s| s.chars().next())
764 .unwrap_or(' ');
765 CharClass::of(ch)
766 }
767
768 #[inline]
769 fn char_class_after(bytes: &[u8], pos: usize) -> CharClass {
772 if pos >= bytes.len() {
773 return CharClass::Whitespace;
774 }
775 let b = bytes[pos];
776 if b < 0x80 {
778 return CharClass::of_ascii(b);
779 }
780 let ch = std::str::from_utf8(&bytes[pos..])
782 .ok()
783 .and_then(|s| s.chars().next())
784 .unwrap_or(' ');
785 CharClass::of(ch)
786 }
787
788 fn try_parse_delimited(
789 input: &'src str,
790 bytes: &[u8],
791 start: usize,
792 marker: u8,
793 count: usize,
794 ) -> Option<(&'src str, usize)> {
795 let inner_start = start + count;
796 bytes.get(inner_start)?;
797
798 let is_star = marker == SpecialChar::Asterisk;
799
800 let before_open = Self::char_class_before(bytes, start);
806 let after_open = Self::char_class_after(bytes, inner_start);
807
808 let left_flanking = after_open != CharClass::Whitespace
809 && (after_open != CharClass::Punctuation || before_open != CharClass::Other);
810 if !left_flanking {
811 return None;
812 }
813 if !is_star {
814 let right_flanking_open = before_open != CharClass::Whitespace
816 && (before_open != CharClass::Punctuation || after_open != CharClass::Other);
817 if right_flanking_open && before_open != CharClass::Punctuation {
818 return None;
819 }
820 }
821
822 let delim_set = if is_star {
824 &STAR_DELIM_SET
825 } else {
826 &UNDER_DELIM_SET
827 };
828
829 let mut i = inner_start;
830 while let Some(pos) = bytes.find_byte_set(i, delim_set) {
831 i = pos;
832 let b = bytes[i];
833
834 if b == SpecialChar::Backslash && bytes.get(i + 1).is_some_and(u8::is_ascii_punctuation)
835 {
836 i += 2;
837 continue;
838 }
839
840 if b != marker {
841 i += 1;
842 continue;
843 }
844
845 let all_match = (1..count).all(|j| bytes.get(i + j) == Some(&marker));
847 if !all_match {
848 i += 1;
849 continue;
850 }
851
852 let close_end = i + count;
853 let before_close = Self::char_class_before(bytes, i);
854 let after_close = Self::char_class_after(bytes, close_end);
855
856 let right_flanking = before_close != CharClass::Whitespace
861 && (before_close != CharClass::Punctuation || after_close != CharClass::Other);
862 if !right_flanking {
863 i += 1;
864 continue;
865 }
866 if !is_star {
867 let left_flanking_close = after_close != CharClass::Whitespace
869 && (after_close != CharClass::Punctuation || before_close != CharClass::Other);
870 if left_flanking_close && after_close != CharClass::Punctuation {
871 i += 1;
872 continue;
873 }
874 }
875
876 return Some((input.get(inner_start..i)?, close_end));
877 }
878
879 None
880 }
881
882 fn try_parse_inline_code(
886 input: &'src str,
887 bytes: &[u8],
888 start: usize,
889 ) -> Option<(&'src str, usize)> {
890 let backtick_count = SpecialChar::Backtick.count_leading_bytes(&bytes[start..]);
891 if backtick_count == 0 {
892 return None;
893 }
894
895 let content_start = start + backtick_count;
896 let mut i = content_start;
897 while i < bytes.len() {
898 i = bytes.find_byte(i, SpecialChar::Backtick.byte())?;
900
901 let close_count = SpecialChar::Backtick.count_leading_bytes(&bytes[i..]);
903
904 if close_count == backtick_count {
905 let mut cs = content_start;
908 let mut ce = i;
909 if ce - cs >= 2
910 && bytes.get(cs) == SpecialChar::Space
911 && bytes.get(ce - 1) == SpecialChar::Space
912 {
913 cs += 1;
914 ce -= 1;
915 }
916 return Some((input.get(cs..ce)?, i + close_count));
917 }
918 i += close_count;
919 }
920
921 None
922 }
923}