Skip to content

Commit bd5e46c

Browse files
authored
markdown: don't nest a link inside a link (#1689)
`print_ty` already writes a named type as a markdown link, and then the html pass in `finish` wrapped that same code span in a second link, so a type reference came out as `<a href="#r"><a href="#r"><code>r</code></a></a>`. Markdown links can't nest, so track whether we're inside one and leave the code span alone. The regression test goes in this crate's `tests/codegen.rs`, which wasn't being run: `test_unit` names crates individually and `--workspace --exclude 'wit-bindgen*'` filters this one out. Add it to that list.
1 parent c36517b commit bd5e46c

3 files changed

Lines changed: 59 additions & 13 deletions

File tree

.github/workflows/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ jobs:
196196
- run: cargo test -p wit-bindgen
197197
- run: cargo test -p wit-bindgen --all-features
198198
- run: cargo test -p wit-bindgen-rust
199+
- run: cargo test -p wit-bindgen-markdown
199200
- run: cargo test --workspace --exclude 'wit-bindgen*'
200201
- run: rustup update nightly-2025-11-10 --no-self-update
201202
- run: rustup default nightly-2025-11-10

crates/markdown/src/lib.rs

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use anyhow::Result;
22
use heck::*;
3-
use pulldown_cmark::{Event, LinkType, Parser, Tag, html};
3+
use pulldown_cmark::{Event, LinkType, Parser, Tag, TagEnd, html};
44
use std::collections::HashMap;
55
use std::fmt::Write;
66
use wit_bindgen_core::{
@@ -207,20 +207,30 @@ impl WorldGenerator for Markdown {
207207
let world = &resolve.worlds[world];
208208
let parser = Parser::new(&self.src);
209209
let mut events = Vec::new();
210+
// Named types are already written as links by `print_ty`, so track
211+
// whether we're inside one: wrapping that code span again would nest
212+
// an `<a>` inside an `<a>`, which isn't valid html. Markdown links
213+
// can't nest, so a bool is enough here.
214+
let mut in_link = false;
210215
for event in parser {
211-
if let Event::Code(code) = &event {
212-
if let Some(dst) = self.hrefs.get(code.as_ref()) {
213-
let tag = Tag::Link {
214-
link_type: LinkType::Inline,
215-
dest_url: dst.as_str().into(),
216-
title: "".into(),
217-
id: "".into(),
218-
};
219-
events.push(Event::Start(tag.clone()));
220-
events.push(event.clone());
221-
events.push(Event::End(tag.into()));
222-
continue;
216+
match &event {
217+
Event::Start(Tag::Link { .. }) => in_link = true,
218+
Event::End(TagEnd::Link) => in_link = false,
219+
Event::Code(code) if !in_link => {
220+
if let Some(dst) = self.hrefs.get(code.as_ref()) {
221+
let tag = Tag::Link {
222+
link_type: LinkType::Inline,
223+
dest_url: dst.as_str().into(),
224+
title: "".into(),
225+
id: "".into(),
226+
};
227+
events.push(Event::Start(tag.clone()));
228+
events.push(event.clone());
229+
events.push(Event::End(tag.into()));
230+
continue;
231+
}
223232
}
233+
_ => {}
224234
}
225235
events.push(event);
226236
}

crates/markdown/tests/codegen.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,38 @@ fn doc_line_starting_with_closing_brace() {
2626
let markdown = String::from_utf8(files.remove("w.md").unwrap()).unwrap();
2727
assert!(markdown.contains("\n <p>}\n"));
2828
}
29+
30+
#[test]
31+
fn type_links_are_not_nested_in_html() {
32+
const WIT: &str = r#"
33+
package a:b;
34+
35+
world w {
36+
export x: interface {
37+
record r {
38+
f: u32,
39+
}
40+
41+
g: func(a: r);
42+
}
43+
}
44+
"#;
45+
46+
let mut resolve = Resolve::default();
47+
let package = resolve.push_str("test.wit", WIT).unwrap();
48+
let world = resolve.select_world(&[package], Some("w")).unwrap();
49+
let mut files = Files::default();
50+
let mut generator = wit_bindgen_markdown::Opts::default().build();
51+
52+
generator.generate(&mut resolve, world, &mut files).unwrap();
53+
54+
let html = String::from_utf8(files.remove("w.html").unwrap()).unwrap();
55+
assert!(
56+
html.contains(r##"<a href="#r"><code>r</code></a>"##),
57+
"expected a single link to `r`:\n{html}"
58+
);
59+
assert!(
60+
!html.contains(r##"<a href="#r"><a "##),
61+
"link to `r` is nested inside another link:\n{html}"
62+
);
63+
}

0 commit comments

Comments
 (0)