@@ -62,56 +62,13 @@ pub(in crate::docstring) fn starts_with_markdown_list_item(line: &str) -> bool {
6262///
6363/// For example, this returns `true` for ``"`value`"`` and `false` for
6464/// ``"`value` trailing"``.
65- pub ( in crate :: docstring) fn is_markdown_code_span ( text : & str ) -> bool {
66- find_backtick_run ( text, TextSize :: ZERO ) . and_then ( |opening| markdown_code_span ( text, opening) )
67- == Some ( TextRange :: up_to ( TextSize :: of ( text) ) )
68- }
69-
70- /// Returns the byte range of the first consecutive backtick run at or after `from`.
71- ///
72- /// For example, searching ``"value `code`"`` from the start returns the range covering the
73- /// opening ``"`"``.
74- pub ( in crate :: docstring) fn find_backtick_run ( text : & str , from : TextSize ) -> Option < TextRange > {
75- let from = from. to_usize ( ) ;
76- let start = from + text. get ( from..) ?. find ( '`' ) ?;
77- let len = text[ start..]
78- . bytes ( )
79- . take_while ( |byte| * byte == b'`' )
80- . count ( ) ;
81- Some ( TextRange :: new (
82- TextSize :: of ( & text[ ..start] ) ,
83- TextSize :: of ( & text[ ..start + len] ) ,
84- ) )
85- }
86-
87- /// Returns the Markdown code span delimited by `opening`, if it has a matching closing run.
88- ///
89- /// For example, the opening run in "``value`with:ticks`` trailing" produces the range covering
90- /// "``value`with:ticks``".
91- pub ( in crate :: docstring) fn markdown_code_span (
92- text : & str ,
93- opening : TextRange ,
94- ) -> Option < TextRange > {
95- let mut search_from = opening. end ( ) ;
96- loop {
97- let closing = find_backtick_run ( text, search_from) ?;
98- if closing. len ( ) == opening. len ( ) {
99- return Some ( opening. cover ( closing) ) ;
100- }
101- search_from = closing. end ( ) ;
102- }
103- }
65+ pub ( crate ) fn is_markdown_code_span ( text : & str ) -> bool {
66+ let mut fragments = BacktickFragments :: new ( text) ;
67+ let Some ( BacktickFragment :: Span ( _) ) = fragments. next ( ) else {
68+ return false ;
69+ } ;
10470
105- /// Returns whether the backtick run at `index` is escaped by a preceding backslash.
106- ///
107- /// For example, the backtick in ``"\`"`` is escaped, while the backtick in ``"\\`"`` is not.
108- pub ( in crate :: docstring) fn is_backtick_run_escaped ( text : & str , index : usize ) -> bool {
109- !text[ ..index]
110- . bytes ( )
111- . rev ( )
112- . take_while ( |byte| * byte == b'\\' )
113- . count ( )
114- . is_multiple_of ( 2 )
71+ fragments. next ( ) . is_none ( )
11572}
11673
11774/// Losslessly partitions source text around complete, unescaped backtick spans.
@@ -155,33 +112,29 @@ impl<'a> Iterator for BacktickFragments<'a> {
155112 type Item = BacktickFragment < ' a > ;
156113
157114 fn next ( & mut self ) -> Option < Self :: Item > {
158- if self . last_fragment_end == TextSize :: of ( self . scanner . source ) {
159- return None ;
160- }
161-
162- let span = loop {
115+ let span = if let Some ( span) = self . pending_span . take ( ) {
163116 // Emit the span saved while returning its preceding text on the previous call.
164- if let Some ( span) = self . pending_span . take ( ) {
165- break span;
166- }
167-
168- // Without another backtick run, the remaining source is all plain text.
169- let Some ( opening) = self . scanner . next ( ) else {
170- return self . take_remaining_text ( ) ;
171- } ;
117+ span
118+ } else {
119+ loop {
120+ // Without another backtick run, the remaining source is all plain text.
121+ let Some ( opening) = self . scanner . next ( ) else {
122+ return self . take_remaining_text ( ) ;
123+ } ;
124+
125+ // Escaped runs are literal source text, so continue looking for the next possible
126+ // opening without emitting a fragment boundary.
127+ if opening. is_escaped ( ) {
128+ continue ;
129+ }
172130
173- // Escaped runs are literal source text, so continue looking for the next possible
174- // opening without emitting a fragment boundary.
175- if opening. is_escaped ( ) {
176- continue ;
131+ // Without a closing delimiter, callers cannot treat the opening or any later runs as
132+ // structured markup. Emit the remainder as one text fragment.
133+ let Some ( span) = self . scanner . eat_span ( opening) else {
134+ return self . take_remaining_text ( ) ;
135+ } ;
136+ break span;
177137 }
178-
179- // Without a closing delimiter, callers cannot treat the opening or any later runs as
180- // structured markup. Emit the remainder as one text fragment.
181- let Some ( span) = self . scanner . eat_span ( opening) else {
182- return self . take_remaining_text ( ) ;
183- } ;
184- break span;
185138 } ;
186139
187140 if self . last_fragment_end < span. start ( ) {
@@ -329,11 +282,6 @@ impl<'a> BacktickScanner<'a> {
329282 delimiter_len : opening. range . len ( ) ,
330283 } )
331284 }
332-
333- /// Returns the scanner's cursor at its current position.
334- pub ( in crate :: docstring) fn into_cursor ( self ) -> Cursor < ' a > {
335- self . cursor
336- }
337285}
338286
339287impl Iterator for BacktickScanner < ' _ > {
@@ -514,28 +462,41 @@ pub(super) fn split_trailing_parenthetical(value: &str) -> Option<(&str, &str)>
514462 let mut outermost_opening = None ;
515463 let mut cursor = Cursor :: new ( value) ;
516464
517- while let Some ( character) = cursor. bump ( ) {
518- let index = cursor. offset ( ) . to_usize ( ) - character. len_utf8 ( ) ;
465+ loop {
466+ let start = cursor. offset ( ) ;
467+ let Some ( character) = cursor. bump ( ) else {
468+ break ;
469+ } ;
470+
519471 match character {
520- '\'' | '"' => consume_quoted_string ( & mut cursor, character) ,
521- '`' if !is_backtick_run_escaped ( value, index) => {
522- let opening = find_backtick_run ( value, TextSize :: of ( & value[ ..index] ) ) ?;
523- let span = markdown_code_span ( value, opening) . unwrap_or ( opening) ;
524- cursor. skip_bytes ( ( span. end ( ) - cursor. offset ( ) ) . to_usize ( ) ) ;
472+ quote @ ( '\'' | '"' ) => consume_quoted_string ( & mut cursor, quote) ,
473+ '`' => {
474+ let mut scanner = BacktickScanner :: starts_at ( start, value) ;
475+ let opening = scanner. next ( ) ?;
476+ if opening. is_escaped ( ) {
477+ // The loop has consumed only the first, escaped backtick. Leave the rest of
478+ // the run for the next iteration, where it may open a shorter span.
479+ continue ;
480+ }
481+
482+ let end = scanner
483+ . eat_span ( opening)
484+ . map_or_else ( || opening. end ( ) , |span| span. end ( ) ) ;
485+ cursor. skip_bytes ( ( end - cursor. offset ( ) ) . to_usize ( ) ) ;
525486 }
526487 '(' => {
527488 if depth == 0 {
528- outermost_opening = Some ( index ) ;
489+ outermost_opening = Some ( start ) ;
529490 }
530491 depth += 1 ;
531492 }
532493 ')' => {
533494 depth = depth. checked_sub ( 1 ) ?;
534495 if depth == 0 && cursor. is_eof ( ) {
535496 let opening = outermost_opening?;
536- let prefix = value[ .. opening] . trim ( ) ;
537- let contents = value [ opening + '(' . len_utf8 ( ) ..index ] . trim ( ) ;
538- return Some ( ( prefix, contents) ) ;
497+ let ( prefix, parenthetical ) = value. split_at ( opening. to_usize ( ) ) ;
498+ let contents = parenthetical . strip_prefix ( '(' ) ? . strip_suffix ( ')' ) ? ;
499+ return Some ( ( prefix. trim ( ) , contents. trim ( ) ) ) ;
539500 }
540501 }
541502 _ => { }
@@ -576,7 +537,7 @@ mod tests {
576537 let span = scanner. eat_span ( opening) . expect ( "a matching backtick run" ) ;
577538 assert ! ( !span. is_single( ) ) ;
578539 assert_eq ! ( span. content( ) , "code" ) ;
579- assert_eq ! ( scanner. into_cursor ( ) . as_str( ) , " suffix" ) ;
540+ assert_eq ! ( scanner. as_str( ) , " suffix" ) ;
580541 }
581542
582543 #[ test]
@@ -700,6 +661,22 @@ mod tests {
700661 ) ;
701662 }
702663
664+ #[ test]
665+ fn ignores_parentheses_inside_code_spans_after_escaped_backtick ( ) {
666+ assert_eq ! (
667+ split_trailing_parenthetical( r"value (\``)`)" ) ,
668+ Some ( ( "value" , r"\``)`" ) )
669+ ) ;
670+ }
671+
672+ #[ test]
673+ fn treats_unmatched_backticks_as_plain_parenthetical_text ( ) {
674+ assert_eq ! (
675+ split_trailing_parenthetical( "value (`unfinished)" ) ,
676+ Some ( ( "value" , "`unfinished" ) )
677+ ) ;
678+ }
679+
703680 #[ test]
704681 fn ignores_parentheses_after_escaped_quotes ( ) {
705682 assert_eq ! (
0 commit comments