11package org .commonmark .ext .gfm .alerts ;
22
33import org .commonmark .Extension ;
4+ import org .commonmark .node .Emphasis ;
45import org .commonmark .node .Node ;
6+ import org .commonmark .node .SourceSpan ;
7+ import org .commonmark .node .StrongEmphasis ;
8+ import org .commonmark .node .Text ;
9+ import org .commonmark .parser .IncludeSourceSpans ;
510import org .commonmark .parser .Parser ;
611import org .commonmark .renderer .html .HtmlRenderer ;
712import org .commonmark .testutil .RenderingTestCase ;
813import org .junit .jupiter .api .Test ;
914
15+ import java .util .List ;
1016import java .util .Set ;
1117
1218import static org .assertj .core .api .Assertions .assertThat ;
1521public class AlertsTest extends RenderingTestCase {
1622
1723 private static final Set <Extension > EXTENSIONS = Set .of (AlertsExtension .create ());
18- private static final Parser PARSER = Parser .builder ().extensions (EXTENSIONS ).build ();
24+ private static final Parser PARSER = Parser .builder ().extensions (EXTENSIONS ).includeSourceSpans ( IncludeSourceSpans . BLOCKS_AND_INLINES ). build ();
1925 private static final HtmlRenderer HTML_RENDERER = HtmlRenderer .builder ().extensions (EXTENSIONS ).build ();
2026
2127 private static final Set <Extension > EXTENSIONS_CUSTOM_TITLES = Set .of (AlertsExtension .builder ().allowCustomTitles (true ).build ());
2228 private static final Parser PARSER_CUSTOM_TITLES = Parser .builder ()
2329 .extensions (EXTENSIONS_CUSTOM_TITLES )
30+ .includeSourceSpans (IncludeSourceSpans .BLOCKS_AND_INLINES )
2431 .build ();
2532 private static final HtmlRenderer HTML_RENDERER_CUSTOM_TITLES = HtmlRenderer .builder ()
2633 .extensions (EXTENSIONS_CUSTOM_TITLES )
@@ -111,6 +118,26 @@ public void overrideStandardTypeTitle() {
111118 "</div>\n " );
112119 }
113120
121+ // Custom type validation
122+
123+ @ Test
124+ public void customTypeMustBeUppercase () {
125+ assertThrows (IllegalArgumentException .class , () ->
126+ AlertsExtension .builder ().addCustomType ("info" , "Information" ).build ());
127+ }
128+
129+ @ Test
130+ public void customTypeMustNotBeEmpty () {
131+ assertThrows (IllegalArgumentException .class , () ->
132+ AlertsExtension .builder ().addCustomType ("" , "Title" ).build ());
133+ }
134+
135+ @ Test
136+ public void customTypeTitleMustNotBeEmpty () {
137+ assertThrows (IllegalArgumentException .class , () ->
138+ AlertsExtension .builder ().addCustomType ("INFO" , "" ).build ());
139+ }
140+
114141 // Custom titles
115142
116143 @ Test
@@ -383,26 +410,6 @@ public void nestedAlerts() {
383410 assertThat (renderer .render (parser .parse (source ))).isEqualTo (expected );
384411 }
385412
386- // Custom type validation
387-
388- @ Test
389- public void customTypeMustBeUppercase () {
390- assertThrows (IllegalArgumentException .class , () ->
391- AlertsExtension .builder ().addCustomType ("info" , "Information" ).build ());
392- }
393-
394- @ Test
395- public void customTypeMustNotBeEmpty () {
396- assertThrows (IllegalArgumentException .class , () ->
397- AlertsExtension .builder ().addCustomType ("" , "Title" ).build ());
398- }
399-
400- @ Test
401- public void customTypeTitleMustNotBeEmpty () {
402- assertThrows (IllegalArgumentException .class , () ->
403- AlertsExtension .builder ().addCustomType ("INFO" , "" ).build ());
404- }
405-
406413 // AST
407414
408415 @ Test
@@ -428,4 +435,106 @@ public void customTypeParsedAsAlertNode() {
428435 assertThat (alert .getType ()).isEqualTo ("INFO" );
429436 }
430437
438+ // Source positions
439+
440+ @ Test
441+ public void titleSourcePositionPreserved () {
442+ String source = "> [!NOTE] Custom title\n > Body text" ;
443+ Node document = PARSER_CUSTOM_TITLES .parse (source );
444+ Alert alert = (Alert ) document .getFirstChild ();
445+ AlertTitle title = (AlertTitle ) alert .getFirstChild ();
446+
447+ // "Custom title" is at column 10, length 12 in line 0
448+ assertThat (title .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 10 , 10 , 12 )));
449+ }
450+
451+ @ Test
452+ public void titleSourcePositionPreservedBetweenBlocks () {
453+ String source = "- List\n \n > [!NOTE] Custom title\n > Body text\n \n Plain paragraph" ;
454+ Node document = PARSER_CUSTOM_TITLES .parse (source );
455+ Alert alert = (Alert ) document .getFirstChild ().getNext ();
456+ AlertTitle title = (AlertTitle ) alert .getFirstChild ();
457+
458+ // "Custom title" is at column 10, length 12 in line 2
459+ assertThat (title .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (2 , 10 , 18 , 12 )));
460+ }
461+
462+ @ Test
463+ public void titleSourcePositionWithLeadingAndTrailingSpaces () {
464+ String source = "> [!NOTE] Custom title \n > Body text" ;
465+ Node document = PARSER_CUSTOM_TITLES .parse (source );
466+ Alert alert = (Alert ) document .getFirstChild ();
467+ AlertTitle title = (AlertTitle ) alert .getFirstChild ();
468+
469+ // Both leading and trailing spaces are trimmed
470+ assertThat (title .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 13 , 13 , 12 )));
471+ }
472+
473+ @ Test
474+ public void titleWithInlineFormattingSourcePosition () {
475+ String source = "> [!NOTE] Custom _title_\n > Body text" ;
476+ Node document = PARSER_CUSTOM_TITLES .parse (source );
477+ Alert alert = (Alert ) document .getFirstChild ();
478+ AlertTitle title = (AlertTitle ) alert .getFirstChild ();
479+
480+ // "Custom _title_" is at column 10, length 14
481+ assertThat (title .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 10 , 10 , 14 )));
482+
483+ // First child: "Custom " text node
484+ Node firstText = title .getFirstChild ();
485+ assertThat (firstText ).isInstanceOf (Text .class );
486+ assertThat (((Text ) firstText ).getLiteral ()).isEqualTo ("Custom " );
487+ assertThat (firstText .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 10 , 10 , 7 )));
488+
489+ // Second child: emphasis node containing "title"
490+ Node emphasis = firstText .getNext ();
491+ assertThat (emphasis ).isInstanceOf (Emphasis .class );
492+ assertThat (emphasis .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 17 , 17 , 7 )));
493+
494+ // Text inside emphasis: "title"
495+ Node titleText = emphasis .getFirstChild ();
496+ assertThat (titleText ).isInstanceOf (Text .class );
497+ assertThat (((Text ) titleText ).getLiteral ()).isEqualTo ("title" );
498+ assertThat (titleText .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 18 , 18 , 5 )));
499+ }
500+
501+ @ Test
502+ public void titleWithNestedInlineFormattingSourcePosition () {
503+ String source = "> [!NOTE] Text with **bold _and italic_**\n > Body text" ;
504+ Node document = PARSER_CUSTOM_TITLES .parse (source );
505+ Alert alert = (Alert ) document .getFirstChild ();
506+ AlertTitle title = (AlertTitle ) alert .getFirstChild ();
507+
508+ // "Custom _title_" is at column 10, length 14
509+ assertThat (title .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 10 , 10 , 31 )));
510+
511+ // First child: "Text with " text node
512+ Node firstText = title .getFirstChild ();
513+ assertThat (firstText ).isInstanceOf (Text .class );
514+ assertThat (((Text ) firstText ).getLiteral ()).isEqualTo ("Text with " );
515+ assertThat (firstText .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 10 , 10 , 10 )));
516+
517+ // Second child: strong emphasis node
518+ Node strong = firstText .getNext ();
519+ assertThat (strong ).isInstanceOf (StrongEmphasis .class );
520+ assertThat (strong .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 20 , 20 , 21 )));
521+
522+ // Inside strong: "bold " text
523+ Node boldText = strong .getFirstChild ();
524+ assertThat (boldText ).isInstanceOf (Text .class );
525+ assertThat (((Text ) boldText ).getLiteral ()).isEqualTo ("bold " );
526+ assertThat (boldText .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 22 , 22 , 5 )));
527+
528+ // Inside strong: emphasis node with "and italic"
529+ Node emphasis = boldText .getNext ();
530+ assertThat (emphasis ).isInstanceOf (Emphasis .class );
531+ assertThat (emphasis .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 27 , 27 , 12 )));
532+
533+ // Text inside emphasis: "and italic"
534+ Node italicText = emphasis .getFirstChild ();
535+ assertThat (italicText ).isInstanceOf (Text .class );
536+ assertThat (((Text ) italicText ).getLiteral ()).isEqualTo ("and italic" );
537+ assertThat (italicText .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 28 , 28 , 10 )));
538+ }
539+
431540}
0 commit comments