Skip to content

Commit 6a82268

Browse files
committed
Add Java API for getting correct markdown parts from descriptions
1 parent b036873 commit 6a82268

File tree

10 files changed

+456
-3
lines changed

10 files changed

+456
-3
lines changed

core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/HasDescription.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.util.List;
1818
import java.util.Locale;
1919
import java.util.Set;
20+
import java.util.stream.Collectors;
2021

2122
import org.eclipse.esmf.metamodel.datatype.LangString;
2223

@@ -78,4 +79,11 @@ default String getDescription( final Locale locale ) {
7879
return getDescription( Locale.ENGLISH );
7980
} );
8081
}
82+
83+
default Set<String> getDescriptions( final Locale locale ) {
84+
return getDescriptions().stream()
85+
.filter( description -> description.getLanguageTag().equals( locale ) )
86+
.map( LangString::getValue )
87+
.collect( Collectors.toSet());
88+
}
8189
}

core/esmf-aspect-meta-model-java/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@
6363
<artifactId>record-builder-processor</artifactId>
6464
<scope>provided</scope>
6565
</dependency>
66+
<dependency>
67+
<groupId>org.commonmark</groupId>
68+
<artifactId>commonmark</artifactId>
69+
<version>0.24.0</version>
70+
</dependency>
6671

6772
<!-- Test dependencies -->
6873
<dependency>
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package org.eclipse.esmf.aspectmodel.utils;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.Set;
6+
import java.util.regex.Matcher;
7+
import java.util.regex.Pattern;
8+
9+
/**
10+
* Utility class for extracting and rendering structured content blocks (such as NOTE, EXAMPLE, SOURCE)
11+
* from SAMM-compliant Markdown descriptions.
12+
* <p>
13+
* This class supports parsing multi-line Markdown-style input and extracting semantically significant
14+
* sections such as {@code > NOTE: ...}, {@code > EXAMPLE: ...}, and {@code > SOURCE: ...}.
15+
* These blocks can be retrieved as plain text or rendered into HTML using {@link MarkdownHtmlRenderer}.
16+
*/
17+
public class DescriptionsUtils {
18+
19+
private DescriptionsUtils() {
20+
}
21+
22+
/**
23+
* A regex pattern used to identify special SAMM-style Markdown blocks.
24+
* Matches lines beginning with {@code > NOTE:}, {@code > EXAMPLE:}, or {@code > SOURCE:},
25+
* optionally followed by a number (e.g., {@code > EXAMPLE 2: ...}).
26+
*/
27+
private static final Pattern BLOCK_PATTERN = Pattern.compile(
28+
"^>\\s*(NOTE|EXAMPLE|SOURCE)(\\s+\\d+)?:\\s*(.*)",
29+
Pattern.CASE_INSENSITIVE
30+
);
31+
32+
/**
33+
* Extracts all {@code NOTE} blocks from the given set of Markdown description strings.
34+
*
35+
* @param descriptions A set of multi-line Markdown descriptions.
36+
* @return A list of extracted NOTE block contents.
37+
*/
38+
public static List<String> notes( final Set<String> descriptions ) {
39+
return extractBlock( descriptions, "NOTE" );
40+
}
41+
42+
/**
43+
* Extracts all {@code EXAMPLE} blocks from the given set of Markdown description strings.
44+
*
45+
* @param descriptions A set of multi-line Markdown descriptions.
46+
* @return A list of extracted EXAMPLE block contents.
47+
*/
48+
public static List<String> examples( final Set<String> descriptions ) {
49+
return extractBlock( descriptions, "EXAMPLE" );
50+
}
51+
52+
/**
53+
* Extracts all {@code SOURCE} blocks from the given set of Markdown description strings.
54+
*
55+
* @param descriptions A set of multi-line Markdown descriptions.
56+
* @return A list of extracted SOURCE block contents.
57+
*/
58+
public static List<String> sources( final Set<String> descriptions ) {
59+
return extractBlock( descriptions, "SOURCE" );
60+
}
61+
62+
/**
63+
* Renders the given set of Markdown description strings into semantic HTML.
64+
* Uses {@link MarkdownHtmlRenderer} to process both special blocks and general Markdown syntax.
65+
*
66+
* @param descriptions A set of Markdown description strings.
67+
* @return The HTML representation of the combined input.
68+
*/
69+
public static String toHtml( final Set<String> descriptions ) {
70+
return MarkdownHtmlRenderer.renderHtmlFromDescriptions( descriptions );
71+
}
72+
73+
/**
74+
* Extracts all blocks of a specified type (e.g., NOTE, EXAMPLE, SOURCE) from a set of Markdown strings.
75+
* <p>
76+
* Each block is expected to begin with a {@code > TYPE:} line and may span multiple lines,
77+
* each of which begins with {@code >}.
78+
*
79+
* @param descriptions A set of multi-line Markdown description strings.
80+
* @param type The type of block to extract ("NOTE", "EXAMPLE", or "SOURCE").
81+
* @return A list of extracted block contents for the specified type.
82+
*/
83+
private static List<String> extractBlock( final Set<String> descriptions, final String type ) {
84+
List<String> result = new ArrayList<>();
85+
for ( String desc : descriptions ) {
86+
extractFromDescription( desc, type, result );
87+
}
88+
return result;
89+
}
90+
91+
private static void extractFromDescription( final String desc, final String type, final List<String> result ) {
92+
String[] lines = desc.split( "\\R" );
93+
boolean[] insideBlock = { false };
94+
StringBuilder blockContent = new StringBuilder();
95+
96+
for ( String line : lines ) {
97+
handleLine( line, type, insideBlock, blockContent, result );
98+
}
99+
100+
if ( insideBlock[0] && !blockContent.isEmpty() ) {
101+
result.add( blockContent.toString().strip() );
102+
}
103+
}
104+
105+
private static void handleLine( final String line, final String type, boolean[] insideBlock,
106+
StringBuilder blockContent, List<String> result ) {
107+
Matcher matcher = BLOCK_PATTERN.matcher( line );
108+
if ( matcher.find() ) {
109+
String currentType = matcher.group( 1 ).toUpperCase();
110+
String content = matcher.group( 3 ); // Corrected: group(3) is the actual content
111+
112+
flushBlock( insideBlock, blockContent, result );
113+
114+
if ( currentType.equals( type.toUpperCase() ) ) {
115+
blockContent.append( content ).append( "\n" );
116+
insideBlock[0] = true;
117+
} else {
118+
insideBlock[0] = false;
119+
}
120+
} else if ( insideBlock[0] && line.startsWith( ">" ) ) {
121+
blockContent.append( line.substring( 1 ).stripLeading() ).append( "\n" );
122+
} else if ( insideBlock[0] ) {
123+
flushBlock( insideBlock, blockContent, result );
124+
}
125+
}
126+
127+
private static void flushBlock( boolean[] insideBlock, StringBuilder blockContent, List<String> result ) {
128+
if ( insideBlock[0] && !blockContent.isEmpty() ) {
129+
result.add( blockContent.toString().strip() );
130+
blockContent.setLength( 0 );
131+
insideBlock[0] = false;
132+
}
133+
}
134+
}
135+
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package org.eclipse.esmf.aspectmodel.utils;
2+
3+
import java.util.ArrayList;
4+
import java.util.LinkedHashMap;
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.Set;
8+
import java.util.regex.Matcher;
9+
import java.util.regex.Pattern;
10+
import java.util.stream.Collectors;
11+
12+
import org.commonmark.node.Node;
13+
import org.commonmark.parser.Parser;
14+
import org.commonmark.renderer.html.HtmlRenderer;
15+
16+
/**
17+
* A utility class for converting SAMM-flavored Markdown descriptions into HTML.
18+
* <p>
19+
* This renderer supports a limited subset of Markdown syntax and introduces
20+
* custom processing for specific annotated blocks commonly used in SAMM descriptions,
21+
* such as {@code > NOTE: ...}, {@code > EXAMPLE: ...}, and {@code > SOURCE: ...}.
22+
* These blocks are extracted and rendered into semantically meaningful HTML
23+
* structures (e.g., {@code <div class="note">}, {@code <ul class="example-list">}, etc.).
24+
* Remaining content is rendered using the CommonMark parser.
25+
*/
26+
public class MarkdownHtmlRenderer {
27+
28+
private static final String CLOSE_DIV_TAG = "</div>";
29+
30+
/**
31+
* A reusable CommonMark parser instance for processing standard Markdown syntax.
32+
*/
33+
private static final Parser PARSER = Parser.builder().build();
34+
35+
/**
36+
* A reusable CommonMark HTML renderer instance.
37+
*/
38+
private static final HtmlRenderer RENDERER = HtmlRenderer.builder().build();
39+
40+
/**
41+
* Private constructor to prevent instantiation. This class is intended to be used statically.
42+
*/
43+
private MarkdownHtmlRenderer() {
44+
}
45+
46+
/**
47+
* Converts a set of multi-line Markdown descriptions into a single HTML string.
48+
* Each entry in the set is processed independently and merged in the resulting output.
49+
*
50+
* @param descriptions A set of Markdown description blocks to render.
51+
* @return Combined HTML output representing all given descriptions.
52+
*/
53+
public static String renderHtmlFromDescriptions( final Set<String> descriptions ) {
54+
StringBuilder result = new StringBuilder();
55+
for ( String desc : descriptions ) {
56+
result.append( processSpecialBlocks( desc ) ).append( "\n" );
57+
}
58+
return result.toString();
59+
}
60+
61+
/**
62+
* Parses a single Markdown block:
63+
* <ul>
64+
* <li>Identifies and extracts special block types: NOTE, EXAMPLE, and SOURCE</li>
65+
* <li>Renders those blocks using custom HTML wrappers</li>
66+
* <li>Processes the remaining Markdown using the CommonMark renderer</li>
67+
* </ul>
68+
*
69+
* @param rawMarkdown The full Markdown string to process.
70+
* @return The rendered HTML output.
71+
*/
72+
private static String processSpecialBlocks( final String rawMarkdown ) {
73+
String[] lines = stripLines( rawMarkdown );
74+
StringBuilder markdownBuffer = new StringBuilder();
75+
Map<String, List<String>> specialBlocks = collectSpecialBlocks( lines, markdownBuffer );
76+
77+
StringBuilder html = new StringBuilder();
78+
specialBlocks.forEach( ( type, items ) -> html.append( renderSpecialBlock( type, items ) ) );
79+
80+
Node parsed = PARSER.parse( markdownBuffer.toString() );
81+
html.append( RENDERER.render( parsed ) );
82+
return html.toString();
83+
}
84+
85+
/**
86+
* Renders a list of extracted special blocks into HTML.
87+
* <p>
88+
* - For {@code NOTE} and {@code SOURCE}, each entry is rendered in a {@code <div>} with a matching class.<br>
89+
* - For {@code EXAMPLE}, a single example is rendered as a {@code <div>}; multiple examples as a {@code <ul>}.
90+
*
91+
* @param type The type of the special block (e.g., "NOTE", "EXAMPLE", "SOURCE").
92+
* @param items The list of block contents for that type.
93+
* @return The rendered HTML string for the block.
94+
*/
95+
private static String renderSpecialBlock( final String type, final List<String> items ) {
96+
if ( items.isEmpty() ) {
97+
return "";
98+
}
99+
100+
return switch ( type ) {
101+
case "NOTE", "SOURCE" -> items.stream()
102+
.map( text -> "<div class=\"" + type.toLowerCase() + "\">"
103+
+ renderMarkdownInline( text.strip() ) + CLOSE_DIV_TAG + "\n" )
104+
.collect( Collectors.joining() );
105+
106+
case "EXAMPLE" -> {
107+
if ( items.size() == 1 ) {
108+
yield "<div class=\"example\">" + renderMarkdownInline( items.get( 0 ).strip() ) + CLOSE_DIV_TAG + "\n";
109+
} else {
110+
StringBuilder sb = new StringBuilder( "<ul class=\"example-list\">\n" );
111+
for ( String item : items ) {
112+
sb.append( "<li>" ).append( renderMarkdownInline( item.strip() ) ).append( "</li>\n" );
113+
}
114+
sb.append( "</ul>\n" );
115+
yield sb.toString();
116+
}
117+
}
118+
119+
default -> items.stream()
120+
.map( text -> "<div class=\"block\">" + renderMarkdownInline( text.strip() ) + CLOSE_DIV_TAG + "\n" )
121+
.collect( Collectors.joining() );
122+
};
123+
}
124+
125+
/**
126+
* Collects all special block entries (NOTE, EXAMPLE, SOURCE) from the input lines.
127+
* Lines not belonging to special blocks are appended to the {@code markdownBuffer}.
128+
*
129+
* @param lines Stripped lines from the raw markdown block.
130+
* @param markdownBuffer Buffer to store non-special markdown content.
131+
* @return A map of special block types to their associated content.
132+
*/
133+
private static Map<String, List<String>> collectSpecialBlocks( final String[] lines, final StringBuilder markdownBuffer ) {
134+
Pattern pattern = Pattern.compile( "^>\\s*(NOTE|EXAMPLE|SOURCE)(\\s+\\d+)?:\\s*(.*)", Pattern.CASE_INSENSITIVE );
135+
Map<String, List<String>> specialBlocks = new LinkedHashMap<>();
136+
137+
String currentType = null;
138+
StringBuilder block = new StringBuilder();
139+
140+
for ( String line : lines ) {
141+
Matcher matcher = pattern.matcher( line );
142+
if ( matcher.find() ) {
143+
flushBlock( currentType, block, specialBlocks );
144+
currentType = matcher.group( 1 ).toUpperCase();
145+
block.append( matcher.group( 3 ) ).append( "\n" );
146+
} else if ( currentType != null && line.startsWith( ">" ) ) {
147+
block.append( line.substring( 1 ).stripLeading() ).append( "\n" );
148+
} else {
149+
flushBlock( currentType, block, specialBlocks );
150+
currentType = null;
151+
markdownBuffer.append( line ).append( "\n" );
152+
}
153+
}
154+
155+
flushBlock( currentType, block, specialBlocks );
156+
return specialBlocks;
157+
}
158+
159+
/**
160+
* Flushes the current block to the target map if non-empty.
161+
*
162+
* @param currentType The type of block being collected.
163+
* @param block The current content buffer for the block.
164+
* @param target The target map of blocks.
165+
*/
166+
private static void flushBlock( final String currentType, final StringBuilder block, final Map<String, List<String>> target ) {
167+
if ( currentType != null && !block.isEmpty() ) {
168+
target.computeIfAbsent( currentType, k -> new ArrayList<>() ).add( block.toString().strip() );
169+
block.setLength( 0 );
170+
}
171+
}
172+
173+
/**
174+
* Splits the raw markdown string into lines and strips leading whitespace from each line.
175+
*
176+
* @param rawMarkdown The original multi-line markdown string.
177+
* @return An array of trimmed lines.
178+
*/
179+
private static String[] stripLines( final String rawMarkdown ) {
180+
String[] rawLines = rawMarkdown.split( "\\R", -1 );
181+
String[] lines = new String[rawLines.length];
182+
for ( int i = 0; i < rawLines.length; i++ ) {
183+
lines[i] = rawLines[i].stripLeading();
184+
}
185+
return lines;
186+
}
187+
188+
/**
189+
* Renders a single markdown line (inline) to HTML using CommonMark.
190+
* This is used for special blocks (e.g., NOTE/EXAMPLE/SOURCE) where
191+
* markdown is allowed but not block-level structure.
192+
*
193+
* @param text Markdown content.
194+
* @return HTML output as string.
195+
*/
196+
private static String renderMarkdownInline( final String text ) {
197+
Node node = PARSER.parse( text );
198+
return RENDERER.render( node ).trim();
199+
}
200+
}
201+
202+

core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/metamodel/ModelSubtypingTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
import org.junit.jupiter.api.Test;
2020

21-
public class ModelSubtypingTest {
21+
class ModelSubtypingTest {
2222
@Test
2323
void testScalarCasting() {
2424
assertThat( xsd.byte_.isTypeOrSubtypeOf( xsd.integer ) ).isTrue();

0 commit comments

Comments
 (0)