Skip to content

Commit ede442b

Browse files
fix(doc): strip blockquote > prefixes from JSDoc code blocks (#34866)
Fixes #25980 --------- Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
1 parent 3fe48f6 commit ede442b

4 files changed

Lines changed: 163 additions & 10 deletions

File tree

cli/util/extract.rs

Lines changed: 134 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,6 @@ fn extract_files_from_regex_blocks(
318318
let files = blocks_regex
319319
.captures_iter(source)
320320
.filter_map(|block| {
321-
let is_markdown_blockquote = block
322-
.name("blockquote")
323-
.is_some_and(|blockquote| !blockquote.as_str().is_empty());
324321
block.name("attributes")?;
325322

326323
let maybe_attributes: Option<Vec<_>> = block
@@ -341,6 +338,22 @@ fn extract_files_from_regex_blocks(
341338

342339
let line_count = block.get(0).unwrap().as_str().split('\n').count();
343340

341+
// Detect whether this code block is nested inside a blockquote. A
342+
// blockquoted fence opens with something like `* > ```ts`: the comment
343+
// `*` marker and surrounding whitespace come before the blockquote `>`
344+
// markers. Strip those off the fence line, then a remaining leading `>`
345+
// means the body lines need their blockquote markers removed. A plain
346+
// `* ```ts` fence has no markers, so non-blockquote blocks are left
347+
// untouched (preserving a literal `>` in, say, a template literal).
348+
let fence_start = block.get(0).unwrap().start();
349+
let before_fence = &source[..fence_start];
350+
let fence_line_prefix = before_fence
351+
.rfind('\n')
352+
.map_or(before_fence, |i| &before_fence[i + 1..]);
353+
let is_markdown_blockquote = fence_line_prefix
354+
.trim_start_matches([' ', '\t', '*'])
355+
.starts_with('>');
356+
344357
let body = block.name("body").unwrap();
345358
extract_file_from_block(
346359
specifier,
@@ -419,15 +432,32 @@ fn extract_file_from_block(
419432
let mut shebang = None;
420433
let mut is_first_line = true;
421434
for line in text.lines() {
422-
let line = if is_markdown_blockquote {
423-
strip_markdown_blockquote_marker(line)
424-
} else {
425-
line
426-
};
427-
let Some(line) = lines_regex.captures(line) else {
435+
let Some(captures) = lines_regex.captures(line) else {
428436
continue;
429437
};
430-
let text = line.get(1).or_else(|| line.get(3)).unwrap().as_str();
438+
let captured = captures
439+
.get(1)
440+
.or_else(|| captures.get(3))
441+
.unwrap()
442+
.as_str();
443+
// The comment/markdown line prefix is removed by `lines_regex` above. For a
444+
// blockquoted block, strip any remaining `> ` markers here, looping so that
445+
// nested quotes (`> > `) are fully removed. Non-blockquote blocks are left
446+
// untouched so a literal `>` in the code (e.g. in a template literal) is
447+
// preserved.
448+
let text = if is_markdown_blockquote {
449+
let mut text = captured;
450+
loop {
451+
let stripped = strip_markdown_blockquote_marker(text);
452+
if stripped.len() == text.len() {
453+
break;
454+
}
455+
text = stripped;
456+
}
457+
text
458+
} else {
459+
captured
460+
};
431461
// Strip shebang from the very first line to forward it to `Deno.test`.
432462
if is_first_line && text.starts_with("#!") {
433463
shebang = Some(parse_shebang(text));
@@ -2258,6 +2288,100 @@ Deno.test("file:///main.ts#3-6.ts", async ()=>{
22582288
media_type: MediaType::TypeScript,
22592289
}],
22602290
},
2291+
// https://github.com/denoland/deno/issues/25980
2292+
// Code blocks nested inside blockquotes in JSDoc comments should have
2293+
// the blockquote `> ` prefix stripped so the extracted source is valid.
2294+
Test {
2295+
input: Input {
2296+
source: r#"/**
2297+
* ```ts
2298+
* console.log("outside");
2299+
* ```
2300+
*
2301+
* > [!NOTE]
2302+
* > This is a note.
2303+
* >
2304+
* > ```ts
2305+
* > console.log("inside blockquote");
2306+
* > ```
2307+
*/
2308+
export function foo() {}
2309+
"#,
2310+
specifier: "file:///main.ts",
2311+
},
2312+
expected: vec![
2313+
Expected {
2314+
source: r#"import { foo } from "file:///main.ts";
2315+
Deno.test("file:///main.ts#2-5.ts", async ()=>{
2316+
console.log("outside");
2317+
});
2318+
"#,
2319+
specifier: "file:///main.ts#2-5.ts",
2320+
media_type: MediaType::TypeScript,
2321+
},
2322+
Expected {
2323+
source: r#"import { foo } from "file:///main.ts";
2324+
Deno.test("file:///main.ts#9-12.ts", async ()=>{
2325+
console.log("inside blockquote");
2326+
});
2327+
"#,
2328+
specifier: "file:///main.ts#9-12.ts",
2329+
media_type: MediaType::TypeScript,
2330+
},
2331+
],
2332+
},
2333+
// A code block nested two blockquote levels deep (`> > `) must have both
2334+
// levels of `> ` stripped so the extracted source is valid.
2335+
Test {
2336+
input: Input {
2337+
source: r#"/**
2338+
* > > ```ts
2339+
* > > console.log("nested");
2340+
* > > ```
2341+
*/
2342+
export function foo() {}
2343+
"#,
2344+
specifier: "file:///main.ts",
2345+
},
2346+
expected: vec![Expected {
2347+
source: r#"import { foo } from "file:///main.ts";
2348+
Deno.test("file:///main.ts#2-5.ts", async ()=>{
2349+
console.log("nested");
2350+
});
2351+
"#,
2352+
specifier: "file:///main.ts#2-5.ts",
2353+
media_type: MediaType::TypeScript,
2354+
}],
2355+
},
2356+
// Regression: a code block NOT inside a blockquote that contains a line
2357+
// starting with `> ` (e.g. inside a template literal) must preserve it.
2358+
Test {
2359+
input: Input {
2360+
source: r#"/**
2361+
* ```ts
2362+
* const s = `
2363+
* > real content
2364+
* `;
2365+
* console.log(s);
2366+
* ```
2367+
*/
2368+
export function bar() {}
2369+
"#,
2370+
specifier: "file:///main.ts",
2371+
},
2372+
expected: vec![Expected {
2373+
source: r#"import { bar } from "file:///main.ts";
2374+
Deno.test("file:///main.ts#2-8.ts", async ()=>{
2375+
const s = `
2376+
> real content
2377+
`;
2378+
console.log(s);
2379+
});
2380+
"#,
2381+
specifier: "file:///main.ts#2-8.ts",
2382+
media_type: MediaType::TypeScript,
2383+
}],
2384+
},
22612385
];
22622386

22632387
for test in tests {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"args": "test --doc main.ts",
3+
"exitCode": 0,
4+
"output": "main.out"
5+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Check [WILDCARD]main.ts
2+
Check [WILDCARD]main.ts#2-5.ts
3+
Check [WILDCARD]main.ts#9-12.ts
4+
running 0 tests from ./main.ts
5+
running 1 test from ./main.ts#2-5.ts
6+
[WILDCARD]/main.ts#2-5.ts ... ok ([WILDLINE])
7+
running 1 test from ./main.ts#9-12.ts
8+
[WILDCARD]/main.ts#9-12.ts ... ok ([WILDLINE])
9+
10+
ok | 2 passed | 0 failed ([WILDLINE])
11+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* ```ts
3+
* console.log("outside blockquote");
4+
* ```
5+
*
6+
* > [!NOTE]
7+
* > This is a note.
8+
* >
9+
* > ```ts
10+
* > console.log("inside blockquote");
11+
* > ```
12+
*/
13+
export function foo() {}

0 commit comments

Comments
 (0)