@@ -14,7 +14,7 @@ use crate::handler::{
1414} ;
1515use crate :: headings:: { Heading , slugify} ;
1616use crate :: links:: resolve_link;
17- use crate :: reqs:: { ReqDefinition , RuleId , SourceSpan , parse_req_marker} ;
17+ use crate :: reqs:: { InlineCodeSpan , ReqDefinition , RuleId , SourceSpan , parse_req_marker} ;
1818
1919/// Parse context representing the current nested structure we're inside.
2020/// This replaces the ad-hoc state variables with a proper stack.
@@ -245,6 +245,10 @@ pub struct Document {
245245 /// HTML snippets to inject into the page's `<head>` (or body end).
246246 /// Already deduplicated by key during rendering.
247247 pub head_injections : Vec < String > ,
248+
249+ /// All inline code spans (backtick-delimited) found in the document.
250+ /// Spans include byte offsets covering the backtick delimiters.
251+ pub inline_code_spans : Vec < InlineCodeSpan > ,
248252}
249253
250254/// Convert a byte offset to a 1-indexed line number.
@@ -288,6 +292,7 @@ pub async fn render(markdown: &str, options: &RenderOptions) -> Result<Document>
288292 let mut reqs: Vec < ReqDefinition > = Vec :: new ( ) ;
289293 let mut elements: Vec < DocElement > = Vec :: new ( ) ;
290294 let mut code_samples: Vec < CodeSample > = Vec :: new ( ) ;
295+ let mut inline_code_spans: Vec < InlineCodeSpan > = Vec :: new ( ) ;
291296 let mut head_injection_map: BTreeMap < String , String > = BTreeMap :: new ( ) ;
292297
293298 // Output HTML - built directly as we process
@@ -319,6 +324,19 @@ pub async fn render(markdown: &str, options: &RenderOptions) -> Result<Document>
319324 |stack : & [ ParseContext < ' _ > ] | stack_contains ( stack, |c| c. is_blockquote ( ) ) ;
320325
321326 for ( event, range) in parser {
327+ // Collect all inline code spans centrally. pulldown_cmark only emits
328+ // Event::Code for genuine backtick spans, never for fenced code block
329+ // content, so this naturally excludes code blocks (even blockquoted ones).
330+ if let Event :: Code ( code) = & event {
331+ inline_code_spans. push ( InlineCodeSpan {
332+ content : code. to_string ( ) ,
333+ span : SourceSpan {
334+ offset : range. start ,
335+ length : range. len ( ) ,
336+ } ,
337+ } ) ;
338+ }
339+
322340 // If inside a blockquote, route events there
323341 if is_inside_blockquote ( & context_stack) {
324342 match & event {
@@ -791,6 +809,7 @@ pub async fn render(markdown: &str, options: &RenderOptions) -> Result<Document>
791809 code_samples,
792810 elements,
793811 head_injections : head_injection_map. into_values ( ) . collect ( ) ,
812+ inline_code_spans,
794813 } )
795814}
796815
@@ -2455,4 +2474,43 @@ Third paragraph.
24552474 doc. html
24562475 ) ;
24572476 }
2477+
2478+ #[ tokio:: test]
2479+ async fn test_inline_code_spans_collected ( ) {
2480+ let md = "See `r[auth.login]` and `r[data.format]` for details." ;
2481+ let doc = render ( md, & RenderOptions :: default ( ) ) . await . unwrap ( ) ;
2482+
2483+ assert_eq ! ( doc. inline_code_spans. len( ) , 2 ) ;
2484+ assert_eq ! ( doc. inline_code_spans[ 0 ] . content, "r[auth.login]" ) ;
2485+ assert_eq ! ( doc. inline_code_spans[ 1 ] . content, "r[data.format]" ) ;
2486+ // Span should cover the backtick delimiters
2487+ assert ! ( doc. inline_code_spans[ 0 ] . span. length > "r[auth.login]" . len( ) ) ;
2488+ }
2489+
2490+ #[ tokio:: test]
2491+ async fn test_inline_code_spans_skip_fenced_code_blocks ( ) {
2492+ let md = "```rust\n // r[auth.login]\n ```\n \n See `r[real.ref]` here." ;
2493+ let doc = render ( md, & RenderOptions :: default ( ) ) . await . unwrap ( ) ;
2494+
2495+ assert_eq ! ( doc. inline_code_spans. len( ) , 1 ) ;
2496+ assert_eq ! ( doc. inline_code_spans[ 0 ] . content, "r[real.ref]" ) ;
2497+ }
2498+
2499+ #[ tokio:: test]
2500+ async fn test_inline_code_spans_skip_blockquoted_fenced_code_blocks ( ) {
2501+ let md = "> ```rust\n > // r[auth.login]\n > ```\n \n See `r[real.ref]` here." ;
2502+ let doc = render ( md, & RenderOptions :: default ( ) ) . await . unwrap ( ) ;
2503+
2504+ assert_eq ! ( doc. inline_code_spans. len( ) , 1 ) ;
2505+ assert_eq ! ( doc. inline_code_spans[ 0 ] . content, "r[real.ref]" ) ;
2506+ }
2507+
2508+ #[ tokio:: test]
2509+ async fn test_inline_code_spans_inside_blockquote_prose ( ) {
2510+ let md = "> See `r[auth.login]` for details." ;
2511+ let doc = render ( md, & RenderOptions :: default ( ) ) . await . unwrap ( ) ;
2512+
2513+ assert_eq ! ( doc. inline_code_spans. len( ) , 1 ) ;
2514+ assert_eq ! ( doc. inline_code_spans[ 0 ] . content, "r[auth.login]" ) ;
2515+ }
24582516}
0 commit comments