@@ -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 {
0 commit comments