Skip to content
This repository was archived by the owner on Jun 22, 2026. It is now read-only.

Commit 3ae6349

Browse files
Add InlineCodeSpan collection to Document
Collect all inline backtick code spans during rendering, with byte offsets from the source. pulldown_cmark only emits Event::Code for genuine backtick spans (never fenced code block content), so this naturally excludes code blocks even inside blockquotes. Collection is centralized at the top of the main render loop, before blockquote routing, so spans inside blockquotes are captured too.
1 parent 702b0ca commit 3ae6349

3 files changed

Lines changed: 70 additions & 3 deletions

File tree

src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ pub use headings::{Heading, slugify};
4141
pub use links::resolve_link;
4242
pub use render::{DocElement, Document, Paragraph, RenderOptions, render};
4343
pub use reqs::{
44-
ExtractedReqs, ReqDefinition, ReqLevel, ReqMetadata, ReqStatus, ReqWarning, ReqWarningKind,
45-
Rfc2119Keyword, RuleId, SourceSpan, detect_rfc2119_keywords, parse_rule_id,
44+
ExtractedReqs, InlineCodeSpan, ReqDefinition, ReqLevel, ReqMetadata, ReqStatus, ReqWarning,
45+
ReqWarningKind, Rfc2119Keyword, RuleId, SourceSpan, detect_rfc2119_keywords, parse_rule_id,
4646
};
4747

4848
pub use ast::{Alignment, Block, Inline, parse as parse_ast, render_to_markdown};

src/render.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use crate::handler::{
1414
};
1515
use crate::headings::{Heading, slugify};
1616
use 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\nSee `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\nSee `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
}

src/reqs.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ pub struct SourceSpan {
1717
pub length: usize,
1818
}
1919

20+
/// An inline code span (backtick-delimited) found in the markdown source.
21+
#[derive(Debug, Clone, PartialEq, Eq, Facet)]
22+
pub struct InlineCodeSpan {
23+
/// The text content inside the backticks
24+
pub content: String,
25+
/// Source span covering the entire code span including backtick delimiters
26+
pub span: SourceSpan,
27+
}
28+
2029
/// Structured rule identifier with optional version.
2130
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Facet)]
2231
pub struct RuleId {

0 commit comments

Comments
 (0)