11package org .commonmark .ext .gfm .alerts ;
22
33import org .commonmark .Extension ;
4- import org .commonmark .node .Node ;
4+ import org .commonmark .node .Emphasis ;
5+ import org .commonmark .node .SourceSpan ;
6+ import org .commonmark .node .StrongEmphasis ;
7+ import org .commonmark .node .Text ;
8+ import org .commonmark .parser .IncludeSourceSpans ;
59import org .commonmark .parser .Parser ;
610import org .commonmark .renderer .html .HtmlRenderer ;
711import org .commonmark .testutil .RenderingTestCase ;
812import org .junit .jupiter .api .Test ;
913
14+ import java .util .List ;
1015import java .util .Set ;
1116
1217import static org .assertj .core .api .Assertions .assertThat ;
1520public class AlertsTest extends RenderingTestCase {
1621
1722 private static final Set <Extension > EXTENSIONS = Set .of (AlertsExtension .create ());
18- private static final Parser PARSER = Parser .builder ().extensions (EXTENSIONS ).build ();
23+ private static final Parser PARSER = Parser .builder ().extensions (EXTENSIONS ).includeSourceSpans ( IncludeSourceSpans . BLOCKS_AND_INLINES ). build ();
1924 private static final HtmlRenderer HTML_RENDERER = HtmlRenderer .builder ().extensions (EXTENSIONS ).build ();
2025
2126 private static final Set <Extension > EXTENSIONS_CUSTOM_TITLES = Set .of (AlertsExtension .builder ().allowCustomTitles (true ).build ());
2227 private static final Parser PARSER_CUSTOM_TITLES = Parser .builder ()
2328 .extensions (EXTENSIONS_CUSTOM_TITLES )
29+ .includeSourceSpans (IncludeSourceSpans .BLOCKS_AND_INLINES )
2430 .build ();
2531 private static final HtmlRenderer HTML_RENDERER_CUSTOM_TITLES = HtmlRenderer .builder ()
2632 .extensions (EXTENSIONS_CUSTOM_TITLES )
@@ -39,12 +45,12 @@ private void assertRenderingCustomTitles(String source, String expectedResult) {
3945
4046 @ Test
4147 public void customType () {
42- Extension extension = AlertsExtension .builder ()
48+ var extension = AlertsExtension .builder ()
4349 .addCustomType ("INFO" , "Information" )
4450 .build ();
4551
46- Parser parser = Parser .builder ().extensions (Set .of (extension )).build ();
47- HtmlRenderer renderer = HtmlRenderer .builder ().extensions (Set .of (extension )).build ();
52+ var parser = Parser .builder ().extensions (Set .of (extension )).build ();
53+ var renderer = HtmlRenderer .builder ().extensions (Set .of (extension )).build ();
4854
4955 assertThat (renderer .render (parser .parse ("> [!INFO]\n > Custom alert" ))).isEqualTo (
5056 "<div class=\" markdown-alert markdown-alert-info\" data-alert-type=\" info\" >\n " +
@@ -55,14 +61,14 @@ public void customType() {
5561
5662 @ Test
5763 public void multipleCustomTypes () {
58- Extension extension = AlertsExtension .builder ()
64+ var extension = AlertsExtension .builder ()
5965 .addCustomType ("INFO" , "Information" )
6066 .addCustomType ("SUCCESS" , "Success!" )
6167 .addCustomType ("DANGER" , "Danger!" )
6268 .build ();
6369
64- Parser parser = Parser .builder ().extensions (Set .of (extension )).build ();
65- HtmlRenderer renderer = HtmlRenderer .builder ().extensions (Set .of (extension )).build ();
70+ var parser = Parser .builder ().extensions (Set .of (extension )).build ();
71+ var renderer = HtmlRenderer .builder ().extensions (Set .of (extension )).build ();
6672
6773 assertThat (renderer .render (parser .parse ("> [!INFO]\n > Info content\n \n > [!SUCCESS]\n > Success content\n \n > [!DANGER]\n > Danger content" ))).isEqualTo (
6874 "<div class=\" markdown-alert markdown-alert-info\" data-alert-type=\" info\" >\n " +
@@ -81,12 +87,12 @@ public void multipleCustomTypes() {
8187
8288 @ Test
8389 public void standardTypesWithCustomConfigured () {
84- Extension extension = AlertsExtension .builder ()
90+ var extension = AlertsExtension .builder ()
8591 .addCustomType ("INFO" , "Information" )
8692 .build ();
8793
88- Parser parser = Parser .builder ().extensions (Set .of (extension )).build ();
89- HtmlRenderer renderer = HtmlRenderer .builder ().extensions (Set .of (extension )).build ();
94+ var parser = Parser .builder ().extensions (Set .of (extension )).build ();
95+ var renderer = HtmlRenderer .builder ().extensions (Set .of (extension )).build ();
9096
9197 assertThat (renderer .render (parser .parse ("> [!NOTE]\n > Standard type" ))).isEqualTo (
9298 "<div class=\" markdown-alert markdown-alert-note\" data-alert-type=\" note\" >\n " +
@@ -97,12 +103,12 @@ public void standardTypesWithCustomConfigured() {
97103
98104 @ Test
99105 public void overrideStandardTypeTitle () {
100- Extension extension = AlertsExtension .builder ()
106+ var extension = AlertsExtension .builder ()
101107 .addCustomType ("NOTE" , "Nota" )
102108 .build ();
103109
104- Parser parser = Parser .builder ().extensions (Set .of (extension )).build ();
105- HtmlRenderer renderer = HtmlRenderer .builder ().extensions (Set .of (extension )).build ();
110+ var parser = Parser .builder ().extensions (Set .of (extension )).build ();
111+ var renderer = HtmlRenderer .builder ().extensions (Set .of (extension )).build ();
106112
107113 assertThat (renderer .render (parser .parse ("> [!NOTE]\n > Localized title" ))).isEqualTo (
108114 "<div class=\" markdown-alert markdown-alert-note\" data-alert-type=\" note\" >\n " +
@@ -111,6 +117,26 @@ public void overrideStandardTypeTitle() {
111117 "</div>\n " );
112118 }
113119
120+ // Custom type validation
121+
122+ @ Test
123+ public void customTypeMustBeUppercase () {
124+ assertThrows (IllegalArgumentException .class , () ->
125+ AlertsExtension .builder ().addCustomType ("info" , "Information" ).build ());
126+ }
127+
128+ @ Test
129+ public void customTypeMustNotBeEmpty () {
130+ assertThrows (IllegalArgumentException .class , () ->
131+ AlertsExtension .builder ().addCustomType ("" , "Title" ).build ());
132+ }
133+
134+ @ Test
135+ public void customTypeTitleMustNotBeEmpty () {
136+ assertThrows (IllegalArgumentException .class , () ->
137+ AlertsExtension .builder ().addCustomType ("INFO" , "" ).build ());
138+ }
139+
114140 // Custom titles
115141
116142 @ Test
@@ -335,9 +361,9 @@ public void noNestedAlertsByDefaultLeadingEmptyLines() {
335361
336362 @ Test
337363 public void nestedAlerts () {
338- Extension extension = AlertsExtension .builder ().allowNestedAlerts (true ).build ();
339- Parser parser = Parser .builder ().extensions (Set .of (extension )).build ();
340- HtmlRenderer renderer = HtmlRenderer .builder ().extensions (Set .of (extension )).build ();
364+ var extension = AlertsExtension .builder ().allowNestedAlerts (true ).build ();
365+ var parser = Parser .builder ().extensions (Set .of (extension )).build ();
366+ var renderer = HtmlRenderer .builder ().extensions (Set .of (extension )).build ();
341367
342368 var source = String .join ("\n " ,
343369 "> [!TIP]" ,
@@ -383,49 +409,131 @@ public void nestedAlerts() {
383409 assertThat (renderer .render (parser .parse (source ))).isEqualTo (expected );
384410 }
385411
386- // Custom type validation
412+ // AST
387413
388414 @ Test
389- public void customTypeMustBeUppercase () {
390- assertThrows (IllegalArgumentException .class , () ->
391- AlertsExtension .builder ().addCustomType ("info" , "Information" ).build ());
415+ public void alertParsedAsAlertNode () {
416+ var document = PARSER .parse ("> [!NOTE]\n > This is a note" );
417+ var firstChild = document .getFirstChild ();
418+ assertThat (firstChild ).isInstanceOf (Alert .class );
419+ var alert = (Alert ) firstChild ;
420+ assertThat (alert .getType ()).isEqualTo ("NOTE" );
392421 }
393422
394423 @ Test
395- public void customTypeMustNotBeEmpty () {
396- assertThrows (IllegalArgumentException .class , () ->
397- AlertsExtension .builder ().addCustomType ("" , "Title" ).build ());
424+ public void customTypeParsedAsAlertNode () {
425+ var extension = AlertsExtension .builder ()
426+ .addCustomType ("INFO" , "Information" )
427+ .build ();
428+
429+ var parser = Parser .builder ().extensions (Set .of (extension )).build ();
430+
431+ var document = parser .parse ("> [!INFO]\n > Custom alert" );
432+ var alert = (Alert ) document .getFirstChild ();
433+
434+ assertThat (alert .getType ()).isEqualTo ("INFO" );
398435 }
399436
437+ // Source positions
438+
400439 @ Test
401- public void customTypeTitleMustNotBeEmpty () {
402- assertThrows (IllegalArgumentException .class , () ->
403- AlertsExtension .builder ().addCustomType ("INFO" , "" ).build ());
440+ public void titleSourcePositionPreserved () {
441+ var source = "> [!NOTE] Custom title\n > Body text" ;
442+ var document = PARSER_CUSTOM_TITLES .parse (source );
443+ var alert = (Alert ) document .getFirstChild ();
444+ var title = (AlertTitle ) alert .getFirstChild ();
445+
446+ // "Custom title" is at column 10, length 12 in line 0
447+ assertThat (title .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 10 , 10 , 12 )));
404448 }
405449
406- // AST
450+ @ Test
451+ public void titleSourcePositionPreservedBetweenBlocks () {
452+ var source = "- List\n \n > [!NOTE] Custom title\n > Body text\n \n Plain paragraph" ;
453+ var document = PARSER_CUSTOM_TITLES .parse (source );
454+ var alert = (Alert ) document .getFirstChild ().getNext ();
455+ var title = (AlertTitle ) alert .getFirstChild ();
456+
457+ // "Custom title" is at column 10, length 12 in line 2
458+ assertThat (title .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (2 , 10 , 18 , 12 )));
459+ }
407460
408461 @ Test
409- public void alertParsedAsAlertNode () {
410- Node document = PARSER .parse ("> [!NOTE]\n > This is a note" );
411- Node firstChild = document .getFirstChild ();
412- assertThat (firstChild ).isInstanceOf (Alert .class );
413- Alert alert = (Alert ) firstChild ;
414- assertThat (alert .getType ()).isEqualTo ("NOTE" );
462+ public void titleSourcePositionWithLeadingAndTrailingSpaces () {
463+ var source = "> [!NOTE] Custom title \n > Body text" ;
464+ var document = PARSER_CUSTOM_TITLES .parse (source );
465+ var alert = (Alert ) document .getFirstChild ();
466+ var title = (AlertTitle ) alert .getFirstChild ();
467+
468+ // Both leading and trailing spaces are trimmed
469+ assertThat (title .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 13 , 13 , 12 )));
415470 }
416471
417472 @ Test
418- public void customTypeParsedAsAlertNode () {
419- Extension extension = AlertsExtension .builder ()
420- .addCustomType ("INFO" , "Information" )
421- .build ();
473+ public void titleWithInlineFormattingSourcePosition () {
474+ var source = "> [!NOTE] Custom _title_\n > Body text" ;
475+ var document = PARSER_CUSTOM_TITLES .parse (source );
476+ var alert = (Alert ) document .getFirstChild ();
477+ var title = (AlertTitle ) alert .getFirstChild ();
422478
423- Parser parser = Parser .builder ().extensions (Set .of (extension )).build ();
479+ // "Custom _title_" is at column 10, length 14
480+ assertThat (title .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 10 , 10 , 14 )));
424481
425- Node document = parser .parse ("> [!INFO]\n > Custom alert" );
426- Alert alert = (Alert ) document .getFirstChild ();
482+ // First child: "Custom " text node
483+ var firstText = title .getFirstChild ();
484+ assertThat (firstText ).isInstanceOf (Text .class );
485+ assertThat (((Text ) firstText ).getLiteral ()).isEqualTo ("Custom " );
486+ assertThat (firstText .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 10 , 10 , 7 )));
427487
428- assertThat (alert .getType ()).isEqualTo ("INFO" );
488+ // Second child: emphasis node containing "title"
489+ var emphasis = firstText .getNext ();
490+ assertThat (emphasis ).isInstanceOf (Emphasis .class );
491+ assertThat (emphasis .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 17 , 17 , 7 )));
492+
493+ // Text inside emphasis: "title"
494+ var titleText = emphasis .getFirstChild ();
495+ assertThat (titleText ).isInstanceOf (Text .class );
496+ assertThat (((Text ) titleText ).getLiteral ()).isEqualTo ("title" );
497+ assertThat (titleText .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 18 , 18 , 5 )));
498+ }
499+
500+ @ Test
501+ public void titleWithNestedInlineFormattingSourcePosition () {
502+ var source = "> [!NOTE] Text with **bold _and italic_**\n > Body text" ;
503+ var document = PARSER_CUSTOM_TITLES .parse (source );
504+ var alert = (Alert ) document .getFirstChild ();
505+ var title = (AlertTitle ) alert .getFirstChild ();
506+
507+ // "Custom _title_" is at column 10, length 14
508+ assertThat (title .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 10 , 10 , 31 )));
509+
510+ // First child: "Text with " text node
511+ var firstText = title .getFirstChild ();
512+ assertThat (firstText ).isInstanceOf (Text .class );
513+ assertThat (((Text ) firstText ).getLiteral ()).isEqualTo ("Text with " );
514+ assertThat (firstText .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 10 , 10 , 10 )));
515+
516+ // Second child: strong emphasis node
517+ var strong = firstText .getNext ();
518+ assertThat (strong ).isInstanceOf (StrongEmphasis .class );
519+ assertThat (strong .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 20 , 20 , 21 )));
520+
521+ // Inside strong: "bold " text
522+ var boldText = strong .getFirstChild ();
523+ assertThat (boldText ).isInstanceOf (Text .class );
524+ assertThat (((Text ) boldText ).getLiteral ()).isEqualTo ("bold " );
525+ assertThat (boldText .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 22 , 22 , 5 )));
526+
527+ // Inside strong: emphasis node with "and italic"
528+ var emphasis = boldText .getNext ();
529+ assertThat (emphasis ).isInstanceOf (Emphasis .class );
530+ assertThat (emphasis .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 27 , 27 , 12 )));
531+
532+ // Text inside emphasis: "and italic"
533+ var italicText = emphasis .getFirstChild ();
534+ assertThat (italicText ).isInstanceOf (Text .class );
535+ assertThat (((Text ) italicText ).getLiteral ()).isEqualTo ("and italic" );
536+ assertThat (italicText .getSourceSpans ()).isEqualTo (List .of (SourceSpan .of (0 , 28 , 28 , 10 )));
429537 }
430538
431539}
0 commit comments