From 6a822689d18b27e5643f43e3920cdf696c80030a Mon Sep 17 00:00:00 2001 From: Yauhenikapl Date: Wed, 14 May 2025 14:31:11 +0300 Subject: [PATCH 01/18] Add Java API for getting correct markdown parts from descriptions --- .../esmf/metamodel/HasDescription.java | 8 + core/esmf-aspect-meta-model-java/pom.xml | 5 + .../aspectmodel/utils/DescriptionsUtils.java | 135 ++++++++++++ .../utils/MarkdownHtmlRenderer.java | 202 ++++++++++++++++++ .../esmf/metamodel/ModelSubtypingTest.java | 2 +- .../esmf/utils/DescriptionUtilsTest.java | 100 +++++++++ .../AspectModelDocumentationGenerator.java | 2 + .../src/main/java/velocity_implicit.vm | 1 + .../html/common-documentation-lib.vm | 2 +- .../html/property-documentation-lib.vm | 2 +- 10 files changed, 456 insertions(+), 3 deletions(-) create mode 100644 core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java create mode 100644 core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java create mode 100644 core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/utils/DescriptionUtilsTest.java diff --git a/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/HasDescription.java b/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/HasDescription.java index 1d5f95467..68fb3c33b 100644 --- a/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/HasDescription.java +++ b/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/HasDescription.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Locale; import java.util.Set; +import java.util.stream.Collectors; import org.eclipse.esmf.metamodel.datatype.LangString; @@ -78,4 +79,11 @@ default String getDescription( final Locale locale ) { return getDescription( Locale.ENGLISH ); } ); } + + default Set getDescriptions( final Locale locale ) { + return getDescriptions().stream() + .filter( description -> description.getLanguageTag().equals( locale ) ) + .map( LangString::getValue ) + .collect( Collectors.toSet()); + } } diff --git a/core/esmf-aspect-meta-model-java/pom.xml b/core/esmf-aspect-meta-model-java/pom.xml index 7bc8251cf..61d4f4f73 100644 --- a/core/esmf-aspect-meta-model-java/pom.xml +++ b/core/esmf-aspect-meta-model-java/pom.xml @@ -63,6 +63,11 @@ record-builder-processor provided + + org.commonmark + commonmark + 0.24.0 + diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java new file mode 100644 index 000000000..c20540753 --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java @@ -0,0 +1,135 @@ +package org.eclipse.esmf.aspectmodel.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility class for extracting and rendering structured content blocks (such as NOTE, EXAMPLE, SOURCE) + * from SAMM-compliant Markdown descriptions. + *

+ * This class supports parsing multi-line Markdown-style input and extracting semantically significant + * sections such as {@code > NOTE: ...}, {@code > EXAMPLE: ...}, and {@code > SOURCE: ...}. + * These blocks can be retrieved as plain text or rendered into HTML using {@link MarkdownHtmlRenderer}. + */ +public class DescriptionsUtils { + + private DescriptionsUtils() { + } + + /** + * A regex pattern used to identify special SAMM-style Markdown blocks. + * Matches lines beginning with {@code > NOTE:}, {@code > EXAMPLE:}, or {@code > SOURCE:}, + * optionally followed by a number (e.g., {@code > EXAMPLE 2: ...}). + */ + private static final Pattern BLOCK_PATTERN = Pattern.compile( + "^>\\s*(NOTE|EXAMPLE|SOURCE)(\\s+\\d+)?:\\s*(.*)", + Pattern.CASE_INSENSITIVE + ); + + /** + * Extracts all {@code NOTE} blocks from the given set of Markdown description strings. + * + * @param descriptions A set of multi-line Markdown descriptions. + * @return A list of extracted NOTE block contents. + */ + public static List notes( final Set descriptions ) { + return extractBlock( descriptions, "NOTE" ); + } + + /** + * Extracts all {@code EXAMPLE} blocks from the given set of Markdown description strings. + * + * @param descriptions A set of multi-line Markdown descriptions. + * @return A list of extracted EXAMPLE block contents. + */ + public static List examples( final Set descriptions ) { + return extractBlock( descriptions, "EXAMPLE" ); + } + + /** + * Extracts all {@code SOURCE} blocks from the given set of Markdown description strings. + * + * @param descriptions A set of multi-line Markdown descriptions. + * @return A list of extracted SOURCE block contents. + */ + public static List sources( final Set descriptions ) { + return extractBlock( descriptions, "SOURCE" ); + } + + /** + * Renders the given set of Markdown description strings into semantic HTML. + * Uses {@link MarkdownHtmlRenderer} to process both special blocks and general Markdown syntax. + * + * @param descriptions A set of Markdown description strings. + * @return The HTML representation of the combined input. + */ + public static String toHtml( final Set descriptions ) { + return MarkdownHtmlRenderer.renderHtmlFromDescriptions( descriptions ); + } + + /** + * Extracts all blocks of a specified type (e.g., NOTE, EXAMPLE, SOURCE) from a set of Markdown strings. + *

+ * Each block is expected to begin with a {@code > TYPE:} line and may span multiple lines, + * each of which begins with {@code >}. + * + * @param descriptions A set of multi-line Markdown description strings. + * @param type The type of block to extract ("NOTE", "EXAMPLE", or "SOURCE"). + * @return A list of extracted block contents for the specified type. + */ + private static List extractBlock( final Set descriptions, final String type ) { + List result = new ArrayList<>(); + for ( String desc : descriptions ) { + extractFromDescription( desc, type, result ); + } + return result; + } + + private static void extractFromDescription( final String desc, final String type, final List result ) { + String[] lines = desc.split( "\\R" ); + boolean[] insideBlock = { false }; + StringBuilder blockContent = new StringBuilder(); + + for ( String line : lines ) { + handleLine( line, type, insideBlock, blockContent, result ); + } + + if ( insideBlock[0] && !blockContent.isEmpty() ) { + result.add( blockContent.toString().strip() ); + } + } + + private static void handleLine( final String line, final String type, boolean[] insideBlock, + StringBuilder blockContent, List result ) { + Matcher matcher = BLOCK_PATTERN.matcher( line ); + if ( matcher.find() ) { + String currentType = matcher.group( 1 ).toUpperCase(); + String content = matcher.group( 3 ); // Corrected: group(3) is the actual content + + flushBlock( insideBlock, blockContent, result ); + + if ( currentType.equals( type.toUpperCase() ) ) { + blockContent.append( content ).append( "\n" ); + insideBlock[0] = true; + } else { + insideBlock[0] = false; + } + } else if ( insideBlock[0] && line.startsWith( ">" ) ) { + blockContent.append( line.substring( 1 ).stripLeading() ).append( "\n" ); + } else if ( insideBlock[0] ) { + flushBlock( insideBlock, blockContent, result ); + } + } + + private static void flushBlock( boolean[] insideBlock, StringBuilder blockContent, List result ) { + if ( insideBlock[0] && !blockContent.isEmpty() ) { + result.add( blockContent.toString().strip() ); + blockContent.setLength( 0 ); + insideBlock[0] = false; + } + } +} + diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java new file mode 100644 index 000000000..41c3f78ba --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java @@ -0,0 +1,202 @@ +package org.eclipse.esmf.aspectmodel.utils; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; + +/** + * A utility class for converting SAMM-flavored Markdown descriptions into HTML. + *

+ * This renderer supports a limited subset of Markdown syntax and introduces + * custom processing for specific annotated blocks commonly used in SAMM descriptions, + * such as {@code > NOTE: ...}, {@code > EXAMPLE: ...}, and {@code > SOURCE: ...}. + * These blocks are extracted and rendered into semantically meaningful HTML + * structures (e.g., {@code

}, {@code
    }, etc.). + * Remaining content is rendered using the CommonMark parser. + */ +public class MarkdownHtmlRenderer { + + private static final String CLOSE_DIV_TAG = "
"; + + /** + * A reusable CommonMark parser instance for processing standard Markdown syntax. + */ + private static final Parser PARSER = Parser.builder().build(); + + /** + * A reusable CommonMark HTML renderer instance. + */ + private static final HtmlRenderer RENDERER = HtmlRenderer.builder().build(); + + /** + * Private constructor to prevent instantiation. This class is intended to be used statically. + */ + private MarkdownHtmlRenderer() { + } + + /** + * Converts a set of multi-line Markdown descriptions into a single HTML string. + * Each entry in the set is processed independently and merged in the resulting output. + * + * @param descriptions A set of Markdown description blocks to render. + * @return Combined HTML output representing all given descriptions. + */ + public static String renderHtmlFromDescriptions( final Set descriptions ) { + StringBuilder result = new StringBuilder(); + for ( String desc : descriptions ) { + result.append( processSpecialBlocks( desc ) ).append( "\n" ); + } + return result.toString(); + } + + /** + * Parses a single Markdown block: + *
    + *
  • Identifies and extracts special block types: NOTE, EXAMPLE, and SOURCE
  • + *
  • Renders those blocks using custom HTML wrappers
  • + *
  • Processes the remaining Markdown using the CommonMark renderer
  • + *
+ * + * @param rawMarkdown The full Markdown string to process. + * @return The rendered HTML output. + */ + private static String processSpecialBlocks( final String rawMarkdown ) { + String[] lines = stripLines( rawMarkdown ); + StringBuilder markdownBuffer = new StringBuilder(); + Map> specialBlocks = collectSpecialBlocks( lines, markdownBuffer ); + + StringBuilder html = new StringBuilder(); + specialBlocks.forEach( ( type, items ) -> html.append( renderSpecialBlock( type, items ) ) ); + + Node parsed = PARSER.parse( markdownBuffer.toString() ); + html.append( RENDERER.render( parsed ) ); + return html.toString(); + } + + /** + * Renders a list of extracted special blocks into HTML. + *

+ * - For {@code NOTE} and {@code SOURCE}, each entry is rendered in a {@code

} with a matching class.
+ * - For {@code EXAMPLE}, a single example is rendered as a {@code
}; multiple examples as a {@code
    }. + * + * @param type The type of the special block (e.g., "NOTE", "EXAMPLE", "SOURCE"). + * @param items The list of block contents for that type. + * @return The rendered HTML string for the block. + */ + private static String renderSpecialBlock( final String type, final List items ) { + if ( items.isEmpty() ) { + return ""; + } + + return switch ( type ) { + case "NOTE", "SOURCE" -> items.stream() + .map( text -> "
    " + + renderMarkdownInline( text.strip() ) + CLOSE_DIV_TAG + "\n" ) + .collect( Collectors.joining() ); + + case "EXAMPLE" -> { + if ( items.size() == 1 ) { + yield "
    " + renderMarkdownInline( items.get( 0 ).strip() ) + CLOSE_DIV_TAG + "\n"; + } else { + StringBuilder sb = new StringBuilder( "
      \n" ); + for ( String item : items ) { + sb.append( "
    • " ).append( renderMarkdownInline( item.strip() ) ).append( "
    • \n" ); + } + sb.append( "
    \n" ); + yield sb.toString(); + } + } + + default -> items.stream() + .map( text -> "
    " + renderMarkdownInline( text.strip() ) + CLOSE_DIV_TAG + "\n" ) + .collect( Collectors.joining() ); + }; + } + + /** + * Collects all special block entries (NOTE, EXAMPLE, SOURCE) from the input lines. + * Lines not belonging to special blocks are appended to the {@code markdownBuffer}. + * + * @param lines Stripped lines from the raw markdown block. + * @param markdownBuffer Buffer to store non-special markdown content. + * @return A map of special block types to their associated content. + */ + private static Map> collectSpecialBlocks( final String[] lines, final StringBuilder markdownBuffer ) { + Pattern pattern = Pattern.compile( "^>\\s*(NOTE|EXAMPLE|SOURCE)(\\s+\\d+)?:\\s*(.*)", Pattern.CASE_INSENSITIVE ); + Map> specialBlocks = new LinkedHashMap<>(); + + String currentType = null; + StringBuilder block = new StringBuilder(); + + for ( String line : lines ) { + Matcher matcher = pattern.matcher( line ); + if ( matcher.find() ) { + flushBlock( currentType, block, specialBlocks ); + currentType = matcher.group( 1 ).toUpperCase(); + block.append( matcher.group( 3 ) ).append( "\n" ); + } else if ( currentType != null && line.startsWith( ">" ) ) { + block.append( line.substring( 1 ).stripLeading() ).append( "\n" ); + } else { + flushBlock( currentType, block, specialBlocks ); + currentType = null; + markdownBuffer.append( line ).append( "\n" ); + } + } + + flushBlock( currentType, block, specialBlocks ); + return specialBlocks; + } + + /** + * Flushes the current block to the target map if non-empty. + * + * @param currentType The type of block being collected. + * @param block The current content buffer for the block. + * @param target The target map of blocks. + */ + private static void flushBlock( final String currentType, final StringBuilder block, final Map> target ) { + if ( currentType != null && !block.isEmpty() ) { + target.computeIfAbsent( currentType, k -> new ArrayList<>() ).add( block.toString().strip() ); + block.setLength( 0 ); + } + } + + /** + * Splits the raw markdown string into lines and strips leading whitespace from each line. + * + * @param rawMarkdown The original multi-line markdown string. + * @return An array of trimmed lines. + */ + private static String[] stripLines( final String rawMarkdown ) { + String[] rawLines = rawMarkdown.split( "\\R", -1 ); + String[] lines = new String[rawLines.length]; + for ( int i = 0; i < rawLines.length; i++ ) { + lines[i] = rawLines[i].stripLeading(); + } + return lines; + } + + /** + * Renders a single markdown line (inline) to HTML using CommonMark. + * This is used for special blocks (e.g., NOTE/EXAMPLE/SOURCE) where + * markdown is allowed but not block-level structure. + * + * @param text Markdown content. + * @return HTML output as string. + */ + private static String renderMarkdownInline( final String text ) { + Node node = PARSER.parse( text ); + return RENDERER.render( node ).trim(); + } +} + + diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/metamodel/ModelSubtypingTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/metamodel/ModelSubtypingTest.java index 58b811366..de5e51aee 100644 --- a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/metamodel/ModelSubtypingTest.java +++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/metamodel/ModelSubtypingTest.java @@ -18,7 +18,7 @@ import org.junit.jupiter.api.Test; -public class ModelSubtypingTest { +class ModelSubtypingTest { @Test void testScalarCasting() { assertThat( xsd.byte_.isTypeOrSubtypeOf( xsd.integer ) ).isTrue(); diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/utils/DescriptionUtilsTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/utils/DescriptionUtilsTest.java new file mode 100644 index 000000000..229f557cd --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/utils/DescriptionUtilsTest.java @@ -0,0 +1,100 @@ +package org.eclipse.esmf.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Set; + +import org.eclipse.esmf.aspectmodel.utils.DescriptionsUtils; + +import org.junit.jupiter.api.Test; + +class DescriptionsUtilsTest { + @Test + void testExtractNotes_singleNote() { + Set descriptions = Set.of( + "> NOTE: This is a note.\n> Continued on the next line." + ); + List notes = DescriptionsUtils.notes( descriptions ); + assertEquals( 1, notes.size() ); + assertEquals( "This is a note.\nContinued on the next line.", notes.get( 0 ) ); + } + + @Test + void testExtractExamples_multipleExamples() { + Set descriptions = Set.of( + "> EXAMPLE 1: First example.\n> More detail.", + "> EXAMPLE 2: Second example." + ); + List examples = DescriptionsUtils.examples( descriptions ); + + assertEquals( 2, examples.size() ); + assertEquals( "First example.\nMore detail.", examples.get( 0 ) ); + assertEquals( "Second example.", examples.get( 1 ) ); + } + + @Test + void testExtractSources_withLink() { + Set descriptions = Set.of( + "> SOURCE: Source with [link](https://example.com)" + ); + List sources = DescriptionsUtils.sources( descriptions ); + assertEquals( 1, sources.size() ); + assertTrue( sources.get( 0 ).contains( "[link](https://example.com)" ) ); + } + + @Test + void testMixedBlockTypes() { + Set descriptions = Set.of( + "> NOTE: A note block.", + "> EXAMPLE: An example block.", + "> SOURCE: A source block." + ); + assertEquals( 1, DescriptionsUtils.notes( descriptions ).size() ); + assertEquals( 1, DescriptionsUtils.examples( descriptions ).size() ); + assertEquals( 1, DescriptionsUtils.sources( descriptions ).size() ); + } + + @Test + void testNoBlocks() { + Set descriptions = Set.of( + "This is a plain description without any special blocks." + ); + assertTrue( DescriptionsUtils.notes( descriptions ).isEmpty() ); + assertTrue( DescriptionsUtils.examples( descriptions ).isEmpty() ); + assertTrue( DescriptionsUtils.sources( descriptions ).isEmpty() ); + } + + @Test + public void testToHtml_withAllBlockTypes() { + Set descriptions = Set.of( + """ + > NOTE: This is a note. + > With multiple lines. + + > EXAMPLE 1: First example. + > Additional example content. + + > EXAMPLE 2: Second example. + + > SOURCE: Source information here. + + Some **markdown** content here. + 1. Ordered + 2. List + """ + ); + + String html = DescriptionsUtils.toHtml( descriptions ); + + assertTrue( html.contains( "
    " ) ); + assertTrue( html.contains( "This is a note." ) ); + assertTrue( html.contains( "
      " ) || html.contains( "
      " ) ); + assertTrue( html.contains( "First example." ) ); + assertTrue( html.contains( "
      " ) ); + assertTrue( html.contains( "Source information here." ) ); + assertTrue( html.contains( "markdown" ) ); + assertTrue( html.contains( "
        " ) ); + } +} diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGenerator.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGenerator.java index 55a8b4dea..9d0a8668d 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGenerator.java +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGenerator.java @@ -33,6 +33,7 @@ import org.eclipse.esmf.aspectmodel.generator.diagram.AspectModelDiagramGenerator; import org.eclipse.esmf.aspectmodel.generator.diagram.DiagramGenerationConfig; import org.eclipse.esmf.aspectmodel.generator.diagram.DiagramGenerationConfigBuilder; +import org.eclipse.esmf.aspectmodel.utils.DescriptionsUtils; import org.eclipse.esmf.aspectmodel.visitor.AspectStreamTraversalVisitor; import org.eclipse.esmf.metamodel.Aspect; import org.eclipse.esmf.metamodel.Scalar; @@ -77,6 +78,7 @@ public Stream generate() { final Map templateContext = new HashMap<>(); templateContext.put( "aspectModel", aspect() ); templateContext.put( "aspectModelHelper", new AspectModelHelper() ); + templateContext.put( "descriptionsUtils", DescriptionsUtils.class ); templateContext.put( "Scalar", Scalar.class ); final Set targetLanguages = config.locale() == null diff --git a/core/esmf-aspect-model-document-generators/src/main/java/velocity_implicit.vm b/core/esmf-aspect-model-document-generators/src/main/java/velocity_implicit.vm index efe0e43fc..3b521a67a 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/velocity_implicit.vm +++ b/core/esmf-aspect-model-document-generators/src/main/java/velocity_implicit.vm @@ -33,3 +33,4 @@ #* @vtlvariable name="languageConstraint" type="org.eclipse.esmf.metamodel.constraint.LanguageConstraint" *# #* @vtlvariable name="list" type="org.eclipse.esmf.metamodel.characteristic.List" *# #* @vtlvariable name="code" type="org.eclipse.esmf.metamodel.characteristic.Code" *# +#* @vtlvariable name="descriptionsUtils" type="org.eclipse.esmf.aspectmodel.utils.DescriptionsUtils" *# diff --git a/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/common-documentation-lib.vm b/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/common-documentation-lib.vm index 17a4dfec7..54cddb5db 100644 --- a/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/common-documentation-lib.vm +++ b/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/common-documentation-lib.vm @@ -37,5 +37,5 @@ #end #macro ( description $description ) -$description +#evaluate( $description ) #end diff --git a/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/property-documentation-lib.vm b/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/property-documentation-lib.vm index d9a401c3d..2b941abd6 100644 --- a/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/property-documentation-lib.vm +++ b/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/property-documentation-lib.vm @@ -17,7 +17,7 @@ #paragraph( $property.getPreferredName( $i18n.getLocale() ) "${aspectModelHelper.buildAnchor( $property, $parentElement, 'property' )}" $weight ) #if( $property.getDescription( $i18n.getLocale() ) ) - #description( $property.getDescription( $i18n.getLocale() ) ) + #description( $descriptionsUtils.toHtml( $property.getDescriptions( $i18n.getLocale() ) ) ) #end
        From a760c776b8f85f78e792980460a41cdf5e0f8585 Mon Sep 17 00:00:00 2001 From: Yauhenikapl Date: Wed, 14 May 2025 14:32:10 +0300 Subject: [PATCH 02/18] Revert "Add Java API for getting correct markdown parts from descriptions" This reverts commit 6a822689d18b27e5643f43e3920cdf696c80030a. --- .../esmf/metamodel/HasDescription.java | 8 - core/esmf-aspect-meta-model-java/pom.xml | 5 - .../aspectmodel/utils/DescriptionsUtils.java | 135 ------------ .../utils/MarkdownHtmlRenderer.java | 202 ------------------ .../esmf/metamodel/ModelSubtypingTest.java | 2 +- .../esmf/utils/DescriptionUtilsTest.java | 100 --------- .../AspectModelDocumentationGenerator.java | 2 - .../src/main/java/velocity_implicit.vm | 1 - .../html/common-documentation-lib.vm | 2 +- .../html/property-documentation-lib.vm | 2 +- 10 files changed, 3 insertions(+), 456 deletions(-) delete mode 100644 core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java delete mode 100644 core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java delete mode 100644 core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/utils/DescriptionUtilsTest.java diff --git a/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/HasDescription.java b/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/HasDescription.java index 68fb3c33b..1d5f95467 100644 --- a/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/HasDescription.java +++ b/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/HasDescription.java @@ -17,7 +17,6 @@ import java.util.List; import java.util.Locale; import java.util.Set; -import java.util.stream.Collectors; import org.eclipse.esmf.metamodel.datatype.LangString; @@ -79,11 +78,4 @@ default String getDescription( final Locale locale ) { return getDescription( Locale.ENGLISH ); } ); } - - default Set getDescriptions( final Locale locale ) { - return getDescriptions().stream() - .filter( description -> description.getLanguageTag().equals( locale ) ) - .map( LangString::getValue ) - .collect( Collectors.toSet()); - } } diff --git a/core/esmf-aspect-meta-model-java/pom.xml b/core/esmf-aspect-meta-model-java/pom.xml index 61d4f4f73..7bc8251cf 100644 --- a/core/esmf-aspect-meta-model-java/pom.xml +++ b/core/esmf-aspect-meta-model-java/pom.xml @@ -63,11 +63,6 @@ record-builder-processor provided - - org.commonmark - commonmark - 0.24.0 - diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java deleted file mode 100644 index c20540753..000000000 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java +++ /dev/null @@ -1,135 +0,0 @@ -package org.eclipse.esmf.aspectmodel.utils; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Utility class for extracting and rendering structured content blocks (such as NOTE, EXAMPLE, SOURCE) - * from SAMM-compliant Markdown descriptions. - *

        - * This class supports parsing multi-line Markdown-style input and extracting semantically significant - * sections such as {@code > NOTE: ...}, {@code > EXAMPLE: ...}, and {@code > SOURCE: ...}. - * These blocks can be retrieved as plain text or rendered into HTML using {@link MarkdownHtmlRenderer}. - */ -public class DescriptionsUtils { - - private DescriptionsUtils() { - } - - /** - * A regex pattern used to identify special SAMM-style Markdown blocks. - * Matches lines beginning with {@code > NOTE:}, {@code > EXAMPLE:}, or {@code > SOURCE:}, - * optionally followed by a number (e.g., {@code > EXAMPLE 2: ...}). - */ - private static final Pattern BLOCK_PATTERN = Pattern.compile( - "^>\\s*(NOTE|EXAMPLE|SOURCE)(\\s+\\d+)?:\\s*(.*)", - Pattern.CASE_INSENSITIVE - ); - - /** - * Extracts all {@code NOTE} blocks from the given set of Markdown description strings. - * - * @param descriptions A set of multi-line Markdown descriptions. - * @return A list of extracted NOTE block contents. - */ - public static List notes( final Set descriptions ) { - return extractBlock( descriptions, "NOTE" ); - } - - /** - * Extracts all {@code EXAMPLE} blocks from the given set of Markdown description strings. - * - * @param descriptions A set of multi-line Markdown descriptions. - * @return A list of extracted EXAMPLE block contents. - */ - public static List examples( final Set descriptions ) { - return extractBlock( descriptions, "EXAMPLE" ); - } - - /** - * Extracts all {@code SOURCE} blocks from the given set of Markdown description strings. - * - * @param descriptions A set of multi-line Markdown descriptions. - * @return A list of extracted SOURCE block contents. - */ - public static List sources( final Set descriptions ) { - return extractBlock( descriptions, "SOURCE" ); - } - - /** - * Renders the given set of Markdown description strings into semantic HTML. - * Uses {@link MarkdownHtmlRenderer} to process both special blocks and general Markdown syntax. - * - * @param descriptions A set of Markdown description strings. - * @return The HTML representation of the combined input. - */ - public static String toHtml( final Set descriptions ) { - return MarkdownHtmlRenderer.renderHtmlFromDescriptions( descriptions ); - } - - /** - * Extracts all blocks of a specified type (e.g., NOTE, EXAMPLE, SOURCE) from a set of Markdown strings. - *

        - * Each block is expected to begin with a {@code > TYPE:} line and may span multiple lines, - * each of which begins with {@code >}. - * - * @param descriptions A set of multi-line Markdown description strings. - * @param type The type of block to extract ("NOTE", "EXAMPLE", or "SOURCE"). - * @return A list of extracted block contents for the specified type. - */ - private static List extractBlock( final Set descriptions, final String type ) { - List result = new ArrayList<>(); - for ( String desc : descriptions ) { - extractFromDescription( desc, type, result ); - } - return result; - } - - private static void extractFromDescription( final String desc, final String type, final List result ) { - String[] lines = desc.split( "\\R" ); - boolean[] insideBlock = { false }; - StringBuilder blockContent = new StringBuilder(); - - for ( String line : lines ) { - handleLine( line, type, insideBlock, blockContent, result ); - } - - if ( insideBlock[0] && !blockContent.isEmpty() ) { - result.add( blockContent.toString().strip() ); - } - } - - private static void handleLine( final String line, final String type, boolean[] insideBlock, - StringBuilder blockContent, List result ) { - Matcher matcher = BLOCK_PATTERN.matcher( line ); - if ( matcher.find() ) { - String currentType = matcher.group( 1 ).toUpperCase(); - String content = matcher.group( 3 ); // Corrected: group(3) is the actual content - - flushBlock( insideBlock, blockContent, result ); - - if ( currentType.equals( type.toUpperCase() ) ) { - blockContent.append( content ).append( "\n" ); - insideBlock[0] = true; - } else { - insideBlock[0] = false; - } - } else if ( insideBlock[0] && line.startsWith( ">" ) ) { - blockContent.append( line.substring( 1 ).stripLeading() ).append( "\n" ); - } else if ( insideBlock[0] ) { - flushBlock( insideBlock, blockContent, result ); - } - } - - private static void flushBlock( boolean[] insideBlock, StringBuilder blockContent, List result ) { - if ( insideBlock[0] && !blockContent.isEmpty() ) { - result.add( blockContent.toString().strip() ); - blockContent.setLength( 0 ); - insideBlock[0] = false; - } - } -} - diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java deleted file mode 100644 index 41c3f78ba..000000000 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java +++ /dev/null @@ -1,202 +0,0 @@ -package org.eclipse.esmf.aspectmodel.utils; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import org.commonmark.node.Node; -import org.commonmark.parser.Parser; -import org.commonmark.renderer.html.HtmlRenderer; - -/** - * A utility class for converting SAMM-flavored Markdown descriptions into HTML. - *

        - * This renderer supports a limited subset of Markdown syntax and introduces - * custom processing for specific annotated blocks commonly used in SAMM descriptions, - * such as {@code > NOTE: ...}, {@code > EXAMPLE: ...}, and {@code > SOURCE: ...}. - * These blocks are extracted and rendered into semantically meaningful HTML - * structures (e.g., {@code

        }, {@code
          }, etc.). - * Remaining content is rendered using the CommonMark parser. - */ -public class MarkdownHtmlRenderer { - - private static final String CLOSE_DIV_TAG = "
        "; - - /** - * A reusable CommonMark parser instance for processing standard Markdown syntax. - */ - private static final Parser PARSER = Parser.builder().build(); - - /** - * A reusable CommonMark HTML renderer instance. - */ - private static final HtmlRenderer RENDERER = HtmlRenderer.builder().build(); - - /** - * Private constructor to prevent instantiation. This class is intended to be used statically. - */ - private MarkdownHtmlRenderer() { - } - - /** - * Converts a set of multi-line Markdown descriptions into a single HTML string. - * Each entry in the set is processed independently and merged in the resulting output. - * - * @param descriptions A set of Markdown description blocks to render. - * @return Combined HTML output representing all given descriptions. - */ - public static String renderHtmlFromDescriptions( final Set descriptions ) { - StringBuilder result = new StringBuilder(); - for ( String desc : descriptions ) { - result.append( processSpecialBlocks( desc ) ).append( "\n" ); - } - return result.toString(); - } - - /** - * Parses a single Markdown block: - *
          - *
        • Identifies and extracts special block types: NOTE, EXAMPLE, and SOURCE
        • - *
        • Renders those blocks using custom HTML wrappers
        • - *
        • Processes the remaining Markdown using the CommonMark renderer
        • - *
        - * - * @param rawMarkdown The full Markdown string to process. - * @return The rendered HTML output. - */ - private static String processSpecialBlocks( final String rawMarkdown ) { - String[] lines = stripLines( rawMarkdown ); - StringBuilder markdownBuffer = new StringBuilder(); - Map> specialBlocks = collectSpecialBlocks( lines, markdownBuffer ); - - StringBuilder html = new StringBuilder(); - specialBlocks.forEach( ( type, items ) -> html.append( renderSpecialBlock( type, items ) ) ); - - Node parsed = PARSER.parse( markdownBuffer.toString() ); - html.append( RENDERER.render( parsed ) ); - return html.toString(); - } - - /** - * Renders a list of extracted special blocks into HTML. - *

        - * - For {@code NOTE} and {@code SOURCE}, each entry is rendered in a {@code

        } with a matching class.
        - * - For {@code EXAMPLE}, a single example is rendered as a {@code
        }; multiple examples as a {@code
          }. - * - * @param type The type of the special block (e.g., "NOTE", "EXAMPLE", "SOURCE"). - * @param items The list of block contents for that type. - * @return The rendered HTML string for the block. - */ - private static String renderSpecialBlock( final String type, final List items ) { - if ( items.isEmpty() ) { - return ""; - } - - return switch ( type ) { - case "NOTE", "SOURCE" -> items.stream() - .map( text -> "
          " - + renderMarkdownInline( text.strip() ) + CLOSE_DIV_TAG + "\n" ) - .collect( Collectors.joining() ); - - case "EXAMPLE" -> { - if ( items.size() == 1 ) { - yield "
          " + renderMarkdownInline( items.get( 0 ).strip() ) + CLOSE_DIV_TAG + "\n"; - } else { - StringBuilder sb = new StringBuilder( "
            \n" ); - for ( String item : items ) { - sb.append( "
          • " ).append( renderMarkdownInline( item.strip() ) ).append( "
          • \n" ); - } - sb.append( "
          \n" ); - yield sb.toString(); - } - } - - default -> items.stream() - .map( text -> "
          " + renderMarkdownInline( text.strip() ) + CLOSE_DIV_TAG + "\n" ) - .collect( Collectors.joining() ); - }; - } - - /** - * Collects all special block entries (NOTE, EXAMPLE, SOURCE) from the input lines. - * Lines not belonging to special blocks are appended to the {@code markdownBuffer}. - * - * @param lines Stripped lines from the raw markdown block. - * @param markdownBuffer Buffer to store non-special markdown content. - * @return A map of special block types to their associated content. - */ - private static Map> collectSpecialBlocks( final String[] lines, final StringBuilder markdownBuffer ) { - Pattern pattern = Pattern.compile( "^>\\s*(NOTE|EXAMPLE|SOURCE)(\\s+\\d+)?:\\s*(.*)", Pattern.CASE_INSENSITIVE ); - Map> specialBlocks = new LinkedHashMap<>(); - - String currentType = null; - StringBuilder block = new StringBuilder(); - - for ( String line : lines ) { - Matcher matcher = pattern.matcher( line ); - if ( matcher.find() ) { - flushBlock( currentType, block, specialBlocks ); - currentType = matcher.group( 1 ).toUpperCase(); - block.append( matcher.group( 3 ) ).append( "\n" ); - } else if ( currentType != null && line.startsWith( ">" ) ) { - block.append( line.substring( 1 ).stripLeading() ).append( "\n" ); - } else { - flushBlock( currentType, block, specialBlocks ); - currentType = null; - markdownBuffer.append( line ).append( "\n" ); - } - } - - flushBlock( currentType, block, specialBlocks ); - return specialBlocks; - } - - /** - * Flushes the current block to the target map if non-empty. - * - * @param currentType The type of block being collected. - * @param block The current content buffer for the block. - * @param target The target map of blocks. - */ - private static void flushBlock( final String currentType, final StringBuilder block, final Map> target ) { - if ( currentType != null && !block.isEmpty() ) { - target.computeIfAbsent( currentType, k -> new ArrayList<>() ).add( block.toString().strip() ); - block.setLength( 0 ); - } - } - - /** - * Splits the raw markdown string into lines and strips leading whitespace from each line. - * - * @param rawMarkdown The original multi-line markdown string. - * @return An array of trimmed lines. - */ - private static String[] stripLines( final String rawMarkdown ) { - String[] rawLines = rawMarkdown.split( "\\R", -1 ); - String[] lines = new String[rawLines.length]; - for ( int i = 0; i < rawLines.length; i++ ) { - lines[i] = rawLines[i].stripLeading(); - } - return lines; - } - - /** - * Renders a single markdown line (inline) to HTML using CommonMark. - * This is used for special blocks (e.g., NOTE/EXAMPLE/SOURCE) where - * markdown is allowed but not block-level structure. - * - * @param text Markdown content. - * @return HTML output as string. - */ - private static String renderMarkdownInline( final String text ) { - Node node = PARSER.parse( text ); - return RENDERER.render( node ).trim(); - } -} - - diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/metamodel/ModelSubtypingTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/metamodel/ModelSubtypingTest.java index de5e51aee..58b811366 100644 --- a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/metamodel/ModelSubtypingTest.java +++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/metamodel/ModelSubtypingTest.java @@ -18,7 +18,7 @@ import org.junit.jupiter.api.Test; -class ModelSubtypingTest { +public class ModelSubtypingTest { @Test void testScalarCasting() { assertThat( xsd.byte_.isTypeOrSubtypeOf( xsd.integer ) ).isTrue(); diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/utils/DescriptionUtilsTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/utils/DescriptionUtilsTest.java deleted file mode 100644 index 229f557cd..000000000 --- a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/utils/DescriptionUtilsTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.eclipse.esmf.utils; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.List; -import java.util.Set; - -import org.eclipse.esmf.aspectmodel.utils.DescriptionsUtils; - -import org.junit.jupiter.api.Test; - -class DescriptionsUtilsTest { - @Test - void testExtractNotes_singleNote() { - Set descriptions = Set.of( - "> NOTE: This is a note.\n> Continued on the next line." - ); - List notes = DescriptionsUtils.notes( descriptions ); - assertEquals( 1, notes.size() ); - assertEquals( "This is a note.\nContinued on the next line.", notes.get( 0 ) ); - } - - @Test - void testExtractExamples_multipleExamples() { - Set descriptions = Set.of( - "> EXAMPLE 1: First example.\n> More detail.", - "> EXAMPLE 2: Second example." - ); - List examples = DescriptionsUtils.examples( descriptions ); - - assertEquals( 2, examples.size() ); - assertEquals( "First example.\nMore detail.", examples.get( 0 ) ); - assertEquals( "Second example.", examples.get( 1 ) ); - } - - @Test - void testExtractSources_withLink() { - Set descriptions = Set.of( - "> SOURCE: Source with [link](https://example.com)" - ); - List sources = DescriptionsUtils.sources( descriptions ); - assertEquals( 1, sources.size() ); - assertTrue( sources.get( 0 ).contains( "[link](https://example.com)" ) ); - } - - @Test - void testMixedBlockTypes() { - Set descriptions = Set.of( - "> NOTE: A note block.", - "> EXAMPLE: An example block.", - "> SOURCE: A source block." - ); - assertEquals( 1, DescriptionsUtils.notes( descriptions ).size() ); - assertEquals( 1, DescriptionsUtils.examples( descriptions ).size() ); - assertEquals( 1, DescriptionsUtils.sources( descriptions ).size() ); - } - - @Test - void testNoBlocks() { - Set descriptions = Set.of( - "This is a plain description without any special blocks." - ); - assertTrue( DescriptionsUtils.notes( descriptions ).isEmpty() ); - assertTrue( DescriptionsUtils.examples( descriptions ).isEmpty() ); - assertTrue( DescriptionsUtils.sources( descriptions ).isEmpty() ); - } - - @Test - public void testToHtml_withAllBlockTypes() { - Set descriptions = Set.of( - """ - > NOTE: This is a note. - > With multiple lines. - - > EXAMPLE 1: First example. - > Additional example content. - - > EXAMPLE 2: Second example. - - > SOURCE: Source information here. - - Some **markdown** content here. - 1. Ordered - 2. List - """ - ); - - String html = DescriptionsUtils.toHtml( descriptions ); - - assertTrue( html.contains( "
          " ) ); - assertTrue( html.contains( "This is a note." ) ); - assertTrue( html.contains( "
            " ) || html.contains( "
            " ) ); - assertTrue( html.contains( "First example." ) ); - assertTrue( html.contains( "
            " ) ); - assertTrue( html.contains( "Source information here." ) ); - assertTrue( html.contains( "markdown" ) ); - assertTrue( html.contains( "
              " ) ); - } -} diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGenerator.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGenerator.java index 9d0a8668d..55a8b4dea 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGenerator.java +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGenerator.java @@ -33,7 +33,6 @@ import org.eclipse.esmf.aspectmodel.generator.diagram.AspectModelDiagramGenerator; import org.eclipse.esmf.aspectmodel.generator.diagram.DiagramGenerationConfig; import org.eclipse.esmf.aspectmodel.generator.diagram.DiagramGenerationConfigBuilder; -import org.eclipse.esmf.aspectmodel.utils.DescriptionsUtils; import org.eclipse.esmf.aspectmodel.visitor.AspectStreamTraversalVisitor; import org.eclipse.esmf.metamodel.Aspect; import org.eclipse.esmf.metamodel.Scalar; @@ -78,7 +77,6 @@ public Stream generate() { final Map templateContext = new HashMap<>(); templateContext.put( "aspectModel", aspect() ); templateContext.put( "aspectModelHelper", new AspectModelHelper() ); - templateContext.put( "descriptionsUtils", DescriptionsUtils.class ); templateContext.put( "Scalar", Scalar.class ); final Set targetLanguages = config.locale() == null diff --git a/core/esmf-aspect-model-document-generators/src/main/java/velocity_implicit.vm b/core/esmf-aspect-model-document-generators/src/main/java/velocity_implicit.vm index 3b521a67a..efe0e43fc 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/velocity_implicit.vm +++ b/core/esmf-aspect-model-document-generators/src/main/java/velocity_implicit.vm @@ -33,4 +33,3 @@ #* @vtlvariable name="languageConstraint" type="org.eclipse.esmf.metamodel.constraint.LanguageConstraint" *# #* @vtlvariable name="list" type="org.eclipse.esmf.metamodel.characteristic.List" *# #* @vtlvariable name="code" type="org.eclipse.esmf.metamodel.characteristic.Code" *# -#* @vtlvariable name="descriptionsUtils" type="org.eclipse.esmf.aspectmodel.utils.DescriptionsUtils" *# diff --git a/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/common-documentation-lib.vm b/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/common-documentation-lib.vm index 54cddb5db..17a4dfec7 100644 --- a/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/common-documentation-lib.vm +++ b/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/common-documentation-lib.vm @@ -37,5 +37,5 @@ #end #macro ( description $description ) -#evaluate( $description ) +$description #end diff --git a/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/property-documentation-lib.vm b/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/property-documentation-lib.vm index 2b941abd6..d9a401c3d 100644 --- a/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/property-documentation-lib.vm +++ b/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/property-documentation-lib.vm @@ -17,7 +17,7 @@ #paragraph( $property.getPreferredName( $i18n.getLocale() ) "${aspectModelHelper.buildAnchor( $property, $parentElement, 'property' )}" $weight ) #if( $property.getDescription( $i18n.getLocale() ) ) - #description( $descriptionsUtils.toHtml( $property.getDescriptions( $i18n.getLocale() ) ) ) + #description( $property.getDescription( $i18n.getLocale() ) ) #end
              From 9afa8f2dfe0311a06f9991e8ac5ec9d83da084e4 Mon Sep 17 00:00:00 2001 From: Yauhenikapl Date: Wed, 14 May 2025 14:37:56 +0300 Subject: [PATCH 03/18] Add Java API for getting correct markdown parts from descriptions --- .../esmf/metamodel/HasDescription.java | 8 + core/esmf-aspect-meta-model-java/pom.xml | 5 + .../aspectmodel/utils/DescriptionsUtils.java | 148 ++++++++++++ .../utils/MarkdownHtmlRenderer.java | 215 ++++++++++++++++++ .../utils/DescriptionUtilsTest.java | 98 ++++++++ .../esmf/metamodel/ModelSubtypingTest.java | 2 +- .../AspectModelDocumentationGenerator.java | 2 + .../src/main/java/velocity_implicit.vm | 1 + .../html/common-documentation-lib.vm | 2 +- .../html/property-documentation-lib.vm | 2 +- ...AspectModelDocumentationGeneratorTest.java | 32 +-- 11 files changed, 496 insertions(+), 19 deletions(-) create mode 100644 core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java create mode 100644 core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java create mode 100644 core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionUtilsTest.java diff --git a/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/HasDescription.java b/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/HasDescription.java index 1d5f95467..8f4df4b05 100644 --- a/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/HasDescription.java +++ b/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/HasDescription.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Locale; import java.util.Set; +import java.util.stream.Collectors; import org.eclipse.esmf.metamodel.datatype.LangString; @@ -78,4 +79,11 @@ default String getDescription( final Locale locale ) { return getDescription( Locale.ENGLISH ); } ); } + + default Set getDescriptions( final Locale locale ) { + return getDescriptions().stream() + .filter( description -> description.getLanguageTag().equals( locale ) ) + .map( LangString::getValue ) + .collect( Collectors.toSet() ); + } } diff --git a/core/esmf-aspect-meta-model-java/pom.xml b/core/esmf-aspect-meta-model-java/pom.xml index 7bc8251cf..61d4f4f73 100644 --- a/core/esmf-aspect-meta-model-java/pom.xml +++ b/core/esmf-aspect-meta-model-java/pom.xml @@ -63,6 +63,11 @@ record-builder-processor provided + + org.commonmark + commonmark + 0.24.0 + diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java new file mode 100644 index 000000000..d2f865752 --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2025 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package org.eclipse.esmf.aspectmodel.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility class for extracting and rendering structured content blocks (such as NOTE, EXAMPLE, SOURCE) + * from SAMM-compliant Markdown descriptions. + *

              + * This class supports parsing multi-line Markdown-style input and extracting semantically significant + * sections such as {@code > NOTE: ...}, {@code > EXAMPLE: ...}, and {@code > SOURCE: ...}. + * These blocks can be retrieved as plain text or rendered into HTML using {@link MarkdownHtmlRenderer}. + */ +public class DescriptionsUtils { + + private DescriptionsUtils() { + } + + /** + * A regex pattern used to identify special SAMM-style Markdown blocks. + * Matches lines beginning with {@code > NOTE:}, {@code > EXAMPLE:}, or {@code > SOURCE:}, + * optionally followed by a number (e.g., {@code > EXAMPLE 2: ...}). + */ + private static final Pattern BLOCK_PATTERN = Pattern.compile( + "^>\\s*(NOTE|EXAMPLE|SOURCE)(\\s+\\d+)?:\\s*(.*)", + Pattern.CASE_INSENSITIVE + ); + + /** + * Extracts all {@code NOTE} blocks from the given set of Markdown description strings. + * + * @param descriptions A set of multi-line Markdown descriptions. + * @return A list of extracted NOTE block contents. + */ + public static List notes( final Set descriptions ) { + return extractBlock( descriptions, "NOTE" ); + } + + /** + * Extracts all {@code EXAMPLE} blocks from the given set of Markdown description strings. + * + * @param descriptions A set of multi-line Markdown descriptions. + * @return A list of extracted EXAMPLE block contents. + */ + public static List examples( final Set descriptions ) { + return extractBlock( descriptions, "EXAMPLE" ); + } + + /** + * Extracts all {@code SOURCE} blocks from the given set of Markdown description strings. + * + * @param descriptions A set of multi-line Markdown descriptions. + * @return A list of extracted SOURCE block contents. + */ + public static List sources( final Set descriptions ) { + return extractBlock( descriptions, "SOURCE" ); + } + + /** + * Renders the given set of Markdown description strings into semantic HTML. + * Uses {@link MarkdownHtmlRenderer} to process both special blocks and general Markdown syntax. + * + * @param descriptions A set of Markdown description strings. + * @return The HTML representation of the combined input. + */ + public static String toHtml( final Set descriptions ) { + return MarkdownHtmlRenderer.renderHtmlFromDescriptions( descriptions ); + } + + /** + * Extracts all blocks of a specified type (e.g., NOTE, EXAMPLE, SOURCE) from a set of Markdown strings. + *

              + * Each block is expected to begin with a {@code > TYPE:} line and may span multiple lines, + * each of which begins with {@code >}. + * + * @param descriptions A set of multi-line Markdown description strings. + * @param type The type of block to extract ("NOTE", "EXAMPLE", or "SOURCE"). + * @return A list of extracted block contents for the specified type. + */ + private static List extractBlock( final Set descriptions, final String type ) { + List result = new ArrayList<>(); + for ( String desc : descriptions ) { + extractFromDescription( desc, type, result ); + } + return result; + } + + private static void extractFromDescription( final String desc, final String type, final List result ) { + String[] lines = desc.split( "\\R" ); + boolean[] insideBlock = { false }; + StringBuilder blockContent = new StringBuilder(); + + for ( String line : lines ) { + handleLine( line, type, insideBlock, blockContent, result ); + } + + if ( insideBlock[0] && !blockContent.isEmpty() ) { + result.add( blockContent.toString().strip() ); + } + } + + private static void handleLine( final String line, final String type, boolean[] insideBlock, + StringBuilder blockContent, List result ) { + Matcher matcher = BLOCK_PATTERN.matcher( line ); + if ( matcher.find() ) { + String currentType = matcher.group( 1 ).toUpperCase(); + String content = matcher.group( 3 ); // Corrected: group(3) is the actual content + + flushBlock( insideBlock, blockContent, result ); + + if ( currentType.equals( type.toUpperCase() ) ) { + blockContent.append( content ).append( "\n" ); + insideBlock[0] = true; + } else { + insideBlock[0] = false; + } + } else if ( insideBlock[0] && line.startsWith( ">" ) ) { + blockContent.append( line.substring( 1 ).stripLeading() ).append( "\n" ); + } else if ( insideBlock[0] ) { + flushBlock( insideBlock, blockContent, result ); + } + } + + private static void flushBlock( boolean[] insideBlock, StringBuilder blockContent, List result ) { + if ( insideBlock[0] && !blockContent.isEmpty() ) { + result.add( blockContent.toString().strip() ); + blockContent.setLength( 0 ); + insideBlock[0] = false; + } + } +} + diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java new file mode 100644 index 000000000..fb726755a --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2025 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package org.eclipse.esmf.aspectmodel.utils; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; + +/** + * A utility class for converting SAMM-flavored Markdown descriptions into HTML. + *

              + * This renderer supports a limited subset of Markdown syntax and introduces + * custom processing for specific annotated blocks commonly used in SAMM descriptions, + * such as {@code > NOTE: ...}, {@code > EXAMPLE: ...}, and {@code > SOURCE: ...}. + * These blocks are extracted and rendered into semantically meaningful HTML + * structures (e.g., {@code

              }, {@code
                }, etc.). + * Remaining content is rendered using the CommonMark parser. + */ +public class MarkdownHtmlRenderer { + + private static final String CLOSE_DIV_TAG = "
              "; + + /** + * A reusable CommonMark parser instance for processing standard Markdown syntax. + */ + private static final Parser PARSER = Parser.builder().build(); + + /** + * A reusable CommonMark HTML renderer instance. + */ + private static final HtmlRenderer RENDERER = HtmlRenderer.builder().build(); + + /** + * Private constructor to prevent instantiation. This class is intended to be used statically. + */ + private MarkdownHtmlRenderer() { + } + + /** + * Converts a set of multi-line Markdown descriptions into a single HTML string. + * Each entry in the set is processed independently and merged in the resulting output. + * + * @param descriptions A set of Markdown description blocks to render. + * @return Combined HTML output representing all given descriptions. + */ + public static String renderHtmlFromDescriptions( final Set descriptions ) { + StringBuilder result = new StringBuilder(); + for ( String desc : descriptions ) { + result.append( processSpecialBlocks( desc ) ).append( "\n" ); + } + return result.toString(); + } + + /** + * Parses a single Markdown block: + *
                + *
              • Identifies and extracts special block types: NOTE, EXAMPLE, and SOURCE
              • + *
              • Renders those blocks using custom HTML wrappers
              • + *
              • Processes the remaining Markdown using the CommonMark renderer
              • + *
              + * + * @param rawMarkdown The full Markdown string to process. + * @return The rendered HTML output. + */ + private static String processSpecialBlocks( final String rawMarkdown ) { + String[] lines = stripLines( rawMarkdown ); + StringBuilder markdownBuffer = new StringBuilder(); + Map> specialBlocks = collectSpecialBlocks( lines, markdownBuffer ); + + StringBuilder html = new StringBuilder(); + specialBlocks.forEach( ( type, items ) -> html.append( renderSpecialBlock( type, items ) ) ); + + Node parsed = PARSER.parse( markdownBuffer.toString() ); + html.append( RENDERER.render( parsed ) ); + return html.toString(); + } + + /** + * Renders a list of extracted special blocks into HTML. + *

              + * - For {@code NOTE} and {@code SOURCE}, each entry is rendered in a {@code

              } with a matching class.
              + * - For {@code EXAMPLE}, a single example is rendered as a {@code
              }; multiple examples as a {@code
                }. + * + * @param type The type of the special block (e.g., "NOTE", "EXAMPLE", "SOURCE"). + * @param items The list of block contents for that type. + * @return The rendered HTML string for the block. + */ + private static String renderSpecialBlock( final String type, final List items ) { + if ( items.isEmpty() ) { + return ""; + } + + return switch ( type ) { + case "NOTE", "SOURCE" -> items.stream() + .map( text -> "
                " + + renderMarkdownInline( text.strip() ) + CLOSE_DIV_TAG + "\n" ) + .collect( Collectors.joining() ); + + case "EXAMPLE" -> { + if ( items.size() == 1 ) { + yield "
                " + renderMarkdownInline( items.get( 0 ).strip() ) + CLOSE_DIV_TAG + "\n"; + } else { + StringBuilder sb = new StringBuilder( "
                  \n" ); + for ( String item : items ) { + sb.append( "
                • " ).append( renderMarkdownInline( item.strip() ) ).append( "
                • \n" ); + } + sb.append( "
                \n" ); + yield sb.toString(); + } + } + + default -> items.stream() + .map( text -> "
                " + renderMarkdownInline( text.strip() ) + CLOSE_DIV_TAG + "\n" ) + .collect( Collectors.joining() ); + }; + } + + /** + * Collects all special block entries (NOTE, EXAMPLE, SOURCE) from the input lines. + * Lines not belonging to special blocks are appended to the {@code markdownBuffer}. + * + * @param lines Stripped lines from the raw markdown block. + * @param markdownBuffer Buffer to store non-special markdown content. + * @return A map of special block types to their associated content. + */ + private static Map> collectSpecialBlocks( final String[] lines, final StringBuilder markdownBuffer ) { + Pattern pattern = Pattern.compile( "^>\\s*(NOTE|EXAMPLE|SOURCE)(\\s+\\d+)?:\\s*(.*)", Pattern.CASE_INSENSITIVE ); + Map> specialBlocks = new LinkedHashMap<>(); + + String currentType = null; + StringBuilder block = new StringBuilder(); + + for ( String line : lines ) { + Matcher matcher = pattern.matcher( line ); + if ( matcher.find() ) { + flushBlock( currentType, block, specialBlocks ); + currentType = matcher.group( 1 ).toUpperCase(); + block.append( matcher.group( 3 ) ).append( "\n" ); + } else if ( currentType != null && line.startsWith( ">" ) ) { + block.append( line.substring( 1 ).stripLeading() ).append( "\n" ); + } else { + flushBlock( currentType, block, specialBlocks ); + currentType = null; + markdownBuffer.append( line ).append( "\n" ); + } + } + + flushBlock( currentType, block, specialBlocks ); + return specialBlocks; + } + + /** + * Flushes the current block to the target map if non-empty. + * + * @param currentType The type of block being collected. + * @param block The current content buffer for the block. + * @param target The target map of blocks. + */ + private static void flushBlock( final String currentType, final StringBuilder block, final Map> target ) { + if ( currentType != null && !block.isEmpty() ) { + target.computeIfAbsent( currentType, k -> new ArrayList<>() ).add( block.toString().strip() ); + block.setLength( 0 ); + } + } + + /** + * Splits the raw markdown string into lines and strips leading whitespace from each line. + * + * @param rawMarkdown The original multi-line markdown string. + * @return An array of trimmed lines. + */ + private static String[] stripLines( final String rawMarkdown ) { + String[] rawLines = rawMarkdown.split( "\\R", -1 ); + String[] lines = new String[rawLines.length]; + for ( int i = 0; i < rawLines.length; i++ ) { + lines[i] = rawLines[i].stripLeading(); + } + return lines; + } + + /** + * Renders a single markdown line (inline) to HTML using CommonMark. + * This is used for special blocks (e.g., NOTE/EXAMPLE/SOURCE) where + * markdown is allowed but not block-level structure. + * + * @param text Markdown content. + * @return HTML output as string. + */ + private static String renderMarkdownInline( final String text ) { + Node node = PARSER.parse( text ); + return RENDERER.render( node ).trim(); + } +} + + diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionUtilsTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionUtilsTest.java new file mode 100644 index 000000000..cb3a28073 --- /dev/null +++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionUtilsTest.java @@ -0,0 +1,98 @@ +package org.eclipse.esmf.aspectmodel.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +class DescriptionsUtilsTest { + @Test + void testExtractNotes_singleNote() { + Set descriptions = Set.of( + "> NOTE: This is a note.\n> Continued on the next line." + ); + List notes = DescriptionsUtils.notes( descriptions ); + assertEquals( 1, notes.size() ); + assertEquals( "This is a note.\nContinued on the next line.", notes.get( 0 ) ); + } + + @Test + void testExtractExamples_multipleExamples() { + Set descriptions = Set.of( + "> EXAMPLE 1: First example.\n> More detail.", + "> EXAMPLE 2: Second example." + ); + List examples = DescriptionsUtils.examples( descriptions ); + + assertEquals( 2, examples.size() ); + assertEquals( "First example.\nMore detail.", examples.get( 0 ) ); + assertEquals( "Second example.", examples.get( 1 ) ); + } + + @Test + void testExtractSources_withLink() { + Set descriptions = Set.of( + "> SOURCE: Source with [link](https://example.com)" + ); + List sources = DescriptionsUtils.sources( descriptions ); + assertEquals( 1, sources.size() ); + assertTrue( sources.get( 0 ).contains( "[link](https://example.com)" ) ); + } + + @Test + void testMixedBlockTypes() { + Set descriptions = Set.of( + "> NOTE: A note block.", + "> EXAMPLE: An example block.", + "> SOURCE: A source block." + ); + assertEquals( 1, DescriptionsUtils.notes( descriptions ).size() ); + assertEquals( 1, DescriptionsUtils.examples( descriptions ).size() ); + assertEquals( 1, DescriptionsUtils.sources( descriptions ).size() ); + } + + @Test + void testNoBlocks() { + Set descriptions = Set.of( + "This is a plain description without any special blocks." + ); + assertTrue( DescriptionsUtils.notes( descriptions ).isEmpty() ); + assertTrue( DescriptionsUtils.examples( descriptions ).isEmpty() ); + assertTrue( DescriptionsUtils.sources( descriptions ).isEmpty() ); + } + + @Test + public void testToHtml_withAllBlockTypes() { + Set descriptions = Set.of( + """ + > NOTE: This is a note. + > With multiple lines. + + > EXAMPLE 1: First example. + > Additional example content. + + > EXAMPLE 2: Second example. + + > SOURCE: Source information here. + + Some **markdown** content here. + 1. Ordered + 2. List + """ + ); + + String html = DescriptionsUtils.toHtml( descriptions ); + + assertTrue( html.contains( "
                " ) ); + assertTrue( html.contains( "This is a note." ) ); + assertTrue( html.contains( "
                  " ) || html.contains( "
                  " ) ); + assertTrue( html.contains( "First example." ) ); + assertTrue( html.contains( "
                  " ) ); + assertTrue( html.contains( "Source information here." ) ); + assertTrue( html.contains( "markdown" ) ); + assertTrue( html.contains( "
                    " ) ); + } +} diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/metamodel/ModelSubtypingTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/metamodel/ModelSubtypingTest.java index 58b811366..de5e51aee 100644 --- a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/metamodel/ModelSubtypingTest.java +++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/metamodel/ModelSubtypingTest.java @@ -18,7 +18,7 @@ import org.junit.jupiter.api.Test; -public class ModelSubtypingTest { +class ModelSubtypingTest { @Test void testScalarCasting() { assertThat( xsd.byte_.isTypeOrSubtypeOf( xsd.integer ) ).isTrue(); diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGenerator.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGenerator.java index 55a8b4dea..9d0a8668d 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGenerator.java +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGenerator.java @@ -33,6 +33,7 @@ import org.eclipse.esmf.aspectmodel.generator.diagram.AspectModelDiagramGenerator; import org.eclipse.esmf.aspectmodel.generator.diagram.DiagramGenerationConfig; import org.eclipse.esmf.aspectmodel.generator.diagram.DiagramGenerationConfigBuilder; +import org.eclipse.esmf.aspectmodel.utils.DescriptionsUtils; import org.eclipse.esmf.aspectmodel.visitor.AspectStreamTraversalVisitor; import org.eclipse.esmf.metamodel.Aspect; import org.eclipse.esmf.metamodel.Scalar; @@ -77,6 +78,7 @@ public Stream generate() { final Map templateContext = new HashMap<>(); templateContext.put( "aspectModel", aspect() ); templateContext.put( "aspectModelHelper", new AspectModelHelper() ); + templateContext.put( "descriptionsUtils", DescriptionsUtils.class ); templateContext.put( "Scalar", Scalar.class ); final Set targetLanguages = config.locale() == null diff --git a/core/esmf-aspect-model-document-generators/src/main/java/velocity_implicit.vm b/core/esmf-aspect-model-document-generators/src/main/java/velocity_implicit.vm index efe0e43fc..3b521a67a 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/velocity_implicit.vm +++ b/core/esmf-aspect-model-document-generators/src/main/java/velocity_implicit.vm @@ -33,3 +33,4 @@ #* @vtlvariable name="languageConstraint" type="org.eclipse.esmf.metamodel.constraint.LanguageConstraint" *# #* @vtlvariable name="list" type="org.eclipse.esmf.metamodel.characteristic.List" *# #* @vtlvariable name="code" type="org.eclipse.esmf.metamodel.characteristic.Code" *# +#* @vtlvariable name="descriptionsUtils" type="org.eclipse.esmf.aspectmodel.utils.DescriptionsUtils" *# diff --git a/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/common-documentation-lib.vm b/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/common-documentation-lib.vm index 17a4dfec7..54cddb5db 100644 --- a/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/common-documentation-lib.vm +++ b/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/common-documentation-lib.vm @@ -37,5 +37,5 @@ #end #macro ( description $description ) -$description +#evaluate( $description ) #end diff --git a/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/property-documentation-lib.vm b/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/property-documentation-lib.vm index d9a401c3d..2b941abd6 100644 --- a/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/property-documentation-lib.vm +++ b/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/property-documentation-lib.vm @@ -17,7 +17,7 @@ #paragraph( $property.getPreferredName( $i18n.getLocale() ) "${aspectModelHelper.buildAnchor( $property, $parentElement, 'property' )}" $weight ) #if( $property.getDescription( $i18n.getLocale() ) ) - #description( $property.getDescription( $i18n.getLocale() ) ) + #description( $descriptionsUtils.toHtml( $property.getDescriptions( $i18n.getLocale() ) ) ) #end
                    diff --git a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java index e3df0070c..df6a81961 100644 --- a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java +++ b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java @@ -28,10 +28,10 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -public class AspectModelDocumentationGeneratorTest { +class AspectModelDocumentationGeneratorTest { @ParameterizedTest @EnumSource( value = TestAspect.class ) - public void testGeneration( final TestAspect testAspect ) { + void testGeneration( final TestAspect testAspect ) { assertThatCode( () -> { final String html = generateHtmlDocumentation( testAspect ); assertThat( html ).doesNotContain( "UnnamedCharacteristic" ); @@ -41,7 +41,7 @@ public void testGeneration( final TestAspect testAspect ) { } @Test - public void testAspectWithEntityCollection() throws Throwable { + void testAspectWithEntityCollection() throws Throwable { final String htmlResult = generateHtmlDocumentation( TestAspect.ASPECT_WITH_ENTITY_COLLECTION ); assertThat( htmlResult ).isNotEmpty(); @@ -53,7 +53,7 @@ public void testAspectWithEntityCollection() throws Throwable { } @Test - public void testAspectWithCollectionOfSimpleType() throws Throwable { + void testAspectWithCollectionOfSimpleType() throws Throwable { final String htmlResult = generateHtmlDocumentation( TestAspect.ASPECT_WITH_COLLECTION_OF_SIMPLE_TYPE ); assertThat( htmlResult ).isNotEmpty(); @@ -65,14 +65,14 @@ public void testAspectWithCollectionOfSimpleType() throws Throwable { } @Test - public void testScriptTagIsEscaped() throws IOException { + void testScriptTagIsEscaped() throws IOException { assertThat( generateHtmlDocumentation( TestAspect.ASPECT_WITH_SCRIPT_TAGS ) ) .isNotEmpty() .doesNotContain( "" ); } @Test - public void testRubyGemUpdateCommandIsNotExecuted() throws IOException { + void testRubyGemUpdateCommandIsNotExecuted() throws IOException { try ( final ByteArrayOutputStream stdOut = new ByteArrayOutputStream() ) { System.setOut( new PrintStream( stdOut ) ); generateHtmlDocumentation( TestAspect.ASPECT_WITH_RUBY_GEM_UPDATE_COMMAND ); @@ -81,7 +81,7 @@ public void testRubyGemUpdateCommandIsNotExecuted() throws IOException { } @Test - public void testHtmlTagsAreEscaped() throws IOException { + void testHtmlTagsAreEscaped() throws IOException { assertThat( generateHtmlDocumentation( TestAspect.ASPECT_WITH_HTML_TAGS ) ) .isNotEmpty() .doesNotContain( "" ) @@ -90,20 +90,20 @@ public void testHtmlTagsAreEscaped() throws IOException { } @Test - public void testEncodedTextIsNotDecoded() throws IOException { + void testEncodedTextIsNotDecoded() throws IOException { assertThat( generateHtmlDocumentation( TestAspect.ASPECT_WITH_ENCODED_STRINGS ) ) .doesNotContain( "This is an Aspect with encoded text." ) .contains( "VGhpcyBpcyBhbiBBc3BlY3Qgd2l0aCBlbmNvZGVkIHRleHQu" ); } @Test - public void testAspectModelUrnIsDisplayed() throws IOException { + void testAspectModelUrnIsDisplayed() throws IOException { assertThat( generateHtmlDocumentation( TestAspect.ASPECT_WITH_HTML_TAGS ) ) .contains( "urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithHtmlTags" ); } @Test - public void testDocInfosAreDisplayed() throws IOException { + void testDocInfosAreDisplayed() throws IOException { assertThat( generateHtmlDocumentation( TestAspect.ASPECT_WITH_HTML_TAGS ) ) .contains( ".toc-list" ) .contains( "aspect-model-diagram" ) @@ -115,13 +115,13 @@ public void testDocInfosAreDisplayed() throws IOException { } @Test - public void testDocumentationIsNotEmptyForModelWithoutLanguageTags() throws IOException { + void testDocumentationIsNotEmptyForModelWithoutLanguageTags() throws IOException { final String aspectWithoutLanguageTags = generateHtmlDocumentation( TestAspect.ASPECT_WITHOUT_LANGUAGE_TAGS ); assertThat( aspectWithoutLanguageTags ).isNotEmpty(); } @Test - public void testAspectWithAbstractSingleEntityExpectSuccess() throws IOException { + void testAspectWithAbstractSingleEntityExpectSuccess() throws IOException { final String documentation = generateHtmlDocumentation( TestAspect.ASPECT_WITH_ABSTRACT_SINGLE_ENTITY ); assertThat( documentation ).contains( "

                    testPropertyTest Property

                    " ); @@ -146,7 +146,7 @@ public void testAspectWithAbstractEntityExpectSuccess() throws IOException { } @Test - public void testAspectWithCollectionWithAbstractEntityExpectSuccess() throws IOException { + void testAspectWithCollectionWithAbstractEntityExpectSuccess() throws IOException { final String documentation = generateHtmlDocumentation( TestAspect.ASPECT_WITH_COLLECTION_WITH_ABSTRACT_ENTITY ); assertThat( documentation ).contains( "

                    generateHtmlDocumentation( TestAspect.ASPECT_WITH_QUANTIFIABLE_WITHOUT_UNIT ) ) @@ -168,7 +168,7 @@ public void testAspectWithQuantifiableWithoutUnit() throws IOException { } @Test - public void testAspectWithConstraintWithSeeAttribute() throws IOException { + void testAspectWithConstraintWithSeeAttribute() throws IOException { final String documentation = generateHtmlDocumentation( TestAspect.ASPECT_WITH_CONSTRAINT_WITH_SEE_ATTRIBUTE ); assertThat( documentation ).contains( "

                    Date: Thu, 15 May 2025 09:30:48 +0300 Subject: [PATCH 04/18] Fix styles --- .../esmf/aspectmodel/utils/DescriptionsUtils.java | 8 ++++---- .../aspectmodel/utils/MarkdownHtmlRenderer.java | 14 +++++++------- ...onUtilsTest.java => DescriptionsUtilsTest.java} | 0 3 files changed, 11 insertions(+), 11 deletions(-) rename core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/{DescriptionUtilsTest.java => DescriptionsUtilsTest.java} (100%) diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java index d2f865752..73886b4a5 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java @@ -22,8 +22,8 @@ /** * Utility class for extracting and rendering structured content blocks (such as NOTE, EXAMPLE, SOURCE) * from SAMM-compliant Markdown descriptions. - *

                    - * This class supports parsing multi-line Markdown-style input and extracting semantically significant + * + *

                    This class supports parsing multi-line Markdown-style input and extracting semantically significant * sections such as {@code > NOTE: ...}, {@code > EXAMPLE: ...}, and {@code > SOURCE: ...}. * These blocks can be retrieved as plain text or rendered into HTML using {@link MarkdownHtmlRenderer}. */ @@ -85,8 +85,8 @@ public static String toHtml( final Set descriptions ) { /** * Extracts all blocks of a specified type (e.g., NOTE, EXAMPLE, SOURCE) from a set of Markdown strings. - *

                    - * Each block is expected to begin with a {@code > TYPE:} line and may span multiple lines, + * + *

                    Each block is expected to begin with a {@code > TYPE:} line and may span multiple lines, * each of which begins with {@code >}. * * @param descriptions A set of multi-line Markdown description strings. diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java index fb726755a..d517c5815 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java @@ -28,8 +28,8 @@ /** * A utility class for converting SAMM-flavored Markdown descriptions into HTML. - *

                    - * This renderer supports a limited subset of Markdown syntax and introduces + * + *

                    This renderer supports a limited subset of Markdown syntax and introduces * custom processing for specific annotated blocks commonly used in SAMM descriptions, * such as {@code > NOTE: ...}, {@code > EXAMPLE: ...}, and {@code > SOURCE: ...}. * These blocks are extracted and rendered into semantically meaningful HTML @@ -97,11 +97,11 @@ private static String processSpecialBlocks( final String rawMarkdown ) { /** * Renders a list of extracted special blocks into HTML. - *

                    + * * - For {@code NOTE} and {@code SOURCE}, each entry is rendered in a {@code

                    } with a matching class.
                    * - For {@code EXAMPLE}, a single example is rendered as a {@code
                    }; multiple examples as a {@code
                      }. * - * @param type The type of the special block (e.g., "NOTE", "EXAMPLE", "SOURCE"). + * @param type The type of the special block (e.g., "NOTE", "EXAMPLE", "SOURCE"). * @param items The list of block contents for that type. * @return The rendered HTML string for the block. */ @@ -139,7 +139,7 @@ private static String renderSpecialBlock( final String type, final List * Collects all special block entries (NOTE, EXAMPLE, SOURCE) from the input lines. * Lines not belonging to special blocks are appended to the {@code markdownBuffer}. * - * @param lines Stripped lines from the raw markdown block. + * @param lines Stripped lines from the raw markdown block. * @param markdownBuffer Buffer to store non-special markdown content. * @return A map of special block types to their associated content. */ @@ -173,8 +173,8 @@ private static Map> collectSpecialBlocks( final String[] li * Flushes the current block to the target map if non-empty. * * @param currentType The type of block being collected. - * @param block The current content buffer for the block. - * @param target The target map of blocks. + * @param block The current content buffer for the block. + * @param target The target map of blocks. */ private static void flushBlock( final String currentType, final StringBuilder block, final Map> target ) { if ( currentType != null && !block.isEmpty() ) { diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionUtilsTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java similarity index 100% rename from core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionUtilsTest.java rename to core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java From 88e81dafc6210af1cf0504b07c5e00f6fb4cc33d Mon Sep 17 00:00:00 2001 From: Yauhenikapl Date: Thu, 15 May 2025 09:33:52 +0300 Subject: [PATCH 05/18] Fix styles --- .../eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java | 4 ++-- .../esmf/aspectmodel/utils/MarkdownHtmlRenderer.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java index 73886b4a5..9a9a89719 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java @@ -23,7 +23,7 @@ * Utility class for extracting and rendering structured content blocks (such as NOTE, EXAMPLE, SOURCE) * from SAMM-compliant Markdown descriptions. * - *

                      This class supports parsing multi-line Markdown-style input and extracting semantically significant + *

                      This class supports parsing multi-line Markdown-style input and extracting semantically significant * sections such as {@code > NOTE: ...}, {@code > EXAMPLE: ...}, and {@code > SOURCE: ...}. * These blocks can be retrieved as plain text or rendered into HTML using {@link MarkdownHtmlRenderer}. */ @@ -86,7 +86,7 @@ public static String toHtml( final Set descriptions ) { /** * Extracts all blocks of a specified type (e.g., NOTE, EXAMPLE, SOURCE) from a set of Markdown strings. * - *

                      Each block is expected to begin with a {@code > TYPE:} line and may span multiple lines, + *

                      Each block is expected to begin with a {@code > TYPE:} line and may span multiple lines, * each of which begins with {@code >}. * * @param descriptions A set of multi-line Markdown description strings. diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java index d517c5815..d2e902a82 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java @@ -29,7 +29,7 @@ /** * A utility class for converting SAMM-flavored Markdown descriptions into HTML. * - *

                      This renderer supports a limited subset of Markdown syntax and introduces + *

                      This renderer supports a limited subset of Markdown syntax and introduces * custom processing for specific annotated blocks commonly used in SAMM descriptions, * such as {@code > NOTE: ...}, {@code > EXAMPLE: ...}, and {@code > SOURCE: ...}. * These blocks are extracted and rendered into semantically meaningful HTML @@ -98,8 +98,8 @@ private static String processSpecialBlocks( final String rawMarkdown ) { /** * Renders a list of extracted special blocks into HTML. * - * - For {@code NOTE} and {@code SOURCE}, each entry is rendered in a {@code

                      } with a matching class.
                      - * - For {@code EXAMPLE}, a single example is rendered as a {@code
                      }; multiple examples as a {@code
                        }. + *

                        - For {@code NOTE} and {@code SOURCE}, each entry is rendered in a {@code

                        } with a matching class.
                        + * - For {@code EXAMPLE}, a single example is rendered as a {@code
                        }; multiple examples as a {@code
                          }. * * @param type The type of the special block (e.g., "NOTE", "EXAMPLE", "SOURCE"). * @param items The list of block contents for that type. From 7be368bb3c567672626e11bf883eadc3f184ef59 Mon Sep 17 00:00:00 2001 From: Yauhenikapl Date: Thu, 15 May 2025 09:55:36 +0300 Subject: [PATCH 06/18] Update logic --- .../esmf/metamodel/HasDescription.java | 8 --- .../aspectmodel/utils/DescriptionsUtils.java | 30 +++++----- .../utils/MarkdownHtmlRenderer.java | 10 +--- .../utils/DescriptionsUtilsTest.java | 55 ++++++++----------- .../html/property-documentation-lib.vm | 2 +- 5 files changed, 41 insertions(+), 64 deletions(-) diff --git a/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/HasDescription.java b/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/HasDescription.java index 8f4df4b05..1d5f95467 100644 --- a/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/HasDescription.java +++ b/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/metamodel/HasDescription.java @@ -17,7 +17,6 @@ import java.util.List; import java.util.Locale; import java.util.Set; -import java.util.stream.Collectors; import org.eclipse.esmf.metamodel.datatype.LangString; @@ -79,11 +78,4 @@ default String getDescription( final Locale locale ) { return getDescription( Locale.ENGLISH ); } ); } - - default Set getDescriptions( final Locale locale ) { - return getDescriptions().stream() - .filter( description -> description.getLanguageTag().equals( locale ) ) - .map( LangString::getValue ) - .collect( Collectors.toSet() ); - } } diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java index 9a9a89719..d35287965 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java @@ -45,42 +45,42 @@ private DescriptionsUtils() { /** * Extracts all {@code NOTE} blocks from the given set of Markdown description strings. * - * @param descriptions A set of multi-line Markdown descriptions. + * @param description A line Markdown description. * @return A list of extracted NOTE block contents. */ - public static List notes( final Set descriptions ) { - return extractBlock( descriptions, "NOTE" ); + public static List notes( final String description ) { + return extractBlock( description, "NOTE" ); } /** * Extracts all {@code EXAMPLE} blocks from the given set of Markdown description strings. * - * @param descriptions A set of multi-line Markdown descriptions. + * @param description A line Markdown description. * @return A list of extracted EXAMPLE block contents. */ - public static List examples( final Set descriptions ) { - return extractBlock( descriptions, "EXAMPLE" ); + public static List examples( final String description ) { + return extractBlock( description, "EXAMPLE" ); } /** * Extracts all {@code SOURCE} blocks from the given set of Markdown description strings. * - * @param descriptions A set of multi-line Markdown descriptions. + * @param description A line Markdown description. * @return A list of extracted SOURCE block contents. */ - public static List sources( final Set descriptions ) { - return extractBlock( descriptions, "SOURCE" ); + public static List sources( final String description ) { + return extractBlock( description, "SOURCE" ); } /** * Renders the given set of Markdown description strings into semantic HTML. * Uses {@link MarkdownHtmlRenderer} to process both special blocks and general Markdown syntax. * - * @param descriptions A set of Markdown description strings. + * @param description A line of Markdown description string. * @return The HTML representation of the combined input. */ - public static String toHtml( final Set descriptions ) { - return MarkdownHtmlRenderer.renderHtmlFromDescriptions( descriptions ); + public static String toHtml( final String description ) { + return MarkdownHtmlRenderer.renderHtmlFromDescriptions( description ); } /** @@ -93,11 +93,9 @@ public static String toHtml( final Set descriptions ) { * @param type The type of block to extract ("NOTE", "EXAMPLE", or "SOURCE"). * @return A list of extracted block contents for the specified type. */ - private static List extractBlock( final Set descriptions, final String type ) { + private static List extractBlock( final String descriptions, final String type ) { List result = new ArrayList<>(); - for ( String desc : descriptions ) { - extractFromDescription( desc, type, result ); - } + extractFromDescription( descriptions, type, result ); return result; } diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java index d2e902a82..2354f00f0 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java @@ -60,15 +60,11 @@ private MarkdownHtmlRenderer() { * Converts a set of multi-line Markdown descriptions into a single HTML string. * Each entry in the set is processed independently and merged in the resulting output. * - * @param descriptions A set of Markdown description blocks to render. + * @param description A line of Markdown description blocks to render. * @return Combined HTML output representing all given descriptions. */ - public static String renderHtmlFromDescriptions( final Set descriptions ) { - StringBuilder result = new StringBuilder(); - for ( String desc : descriptions ) { - result.append( processSpecialBlocks( desc ) ).append( "\n" ); - } - return result.toString(); + public static String renderHtmlFromDescriptions( final String description ) { + return processSpecialBlocks( description ) + "\n"; } /** diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java index cb3a28073..f3a1389e4 100644 --- a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java +++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java @@ -4,28 +4,24 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; -import java.util.Set; import org.junit.jupiter.api.Test; class DescriptionsUtilsTest { @Test void testExtractNotes_singleNote() { - Set descriptions = Set.of( - "> NOTE: This is a note.\n> Continued on the next line." - ); - List notes = DescriptionsUtils.notes( descriptions ); + String description = "> NOTE: This is a note.\n> Continued on the next line."; + List notes = DescriptionsUtils.notes( description ); assertEquals( 1, notes.size() ); assertEquals( "This is a note.\nContinued on the next line.", notes.get( 0 ) ); } @Test void testExtractExamples_multipleExamples() { - Set descriptions = Set.of( - "> EXAMPLE 1: First example.\n> More detail.", - "> EXAMPLE 2: Second example." - ); - List examples = DescriptionsUtils.examples( descriptions ); + String description = + "> EXAMPLE 1: First example.\n> More detail.\n" + + "> EXAMPLE 2: Second example."; + List examples = DescriptionsUtils.examples( description ); assertEquals( 2, examples.size() ); assertEquals( "First example.\nMore detail.", examples.get( 0 ) ); @@ -34,39 +30,34 @@ void testExtractExamples_multipleExamples() { @Test void testExtractSources_withLink() { - Set descriptions = Set.of( - "> SOURCE: Source with [link](https://example.com)" - ); - List sources = DescriptionsUtils.sources( descriptions ); + String description = "> SOURCE: Source with [link](https://example.com)"; + List sources = DescriptionsUtils.sources( description ); assertEquals( 1, sources.size() ); assertTrue( sources.get( 0 ).contains( "[link](https://example.com)" ) ); } @Test void testMixedBlockTypes() { - Set descriptions = Set.of( - "> NOTE: A note block.", - "> EXAMPLE: An example block.", - "> SOURCE: A source block." - ); - assertEquals( 1, DescriptionsUtils.notes( descriptions ).size() ); - assertEquals( 1, DescriptionsUtils.examples( descriptions ).size() ); - assertEquals( 1, DescriptionsUtils.sources( descriptions ).size() ); + String description = + "> NOTE: A note block.\n" + + "> EXAMPLE: An example block.\n" + + "> SOURCE: A source block."; + assertEquals( 1, DescriptionsUtils.notes( description ).size() ); + assertEquals( 1, DescriptionsUtils.examples( description ).size() ); + assertEquals( 1, DescriptionsUtils.sources( description ).size() ); } @Test void testNoBlocks() { - Set descriptions = Set.of( - "This is a plain description without any special blocks." - ); - assertTrue( DescriptionsUtils.notes( descriptions ).isEmpty() ); - assertTrue( DescriptionsUtils.examples( descriptions ).isEmpty() ); - assertTrue( DescriptionsUtils.sources( descriptions ).isEmpty() ); + String description = "This is a plain description without any special blocks."; + assertTrue( DescriptionsUtils.notes( description ).isEmpty() ); + assertTrue( DescriptionsUtils.examples( description ).isEmpty() ); + assertTrue( DescriptionsUtils.sources( description ).isEmpty() ); } @Test - public void testToHtml_withAllBlockTypes() { - Set descriptions = Set.of( + void testToHtml_withAllBlockTypes() { + final String description = """ > NOTE: This is a note. > With multiple lines. @@ -82,9 +73,9 @@ public void testToHtml_withAllBlockTypes() { 1. Ordered 2. List """ - ); + ; - String html = DescriptionsUtils.toHtml( descriptions ); + final String html = DescriptionsUtils.toHtml( description ); assertTrue( html.contains( "
                          " ) ); assertTrue( html.contains( "This is a note." ) ); diff --git a/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/property-documentation-lib.vm b/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/property-documentation-lib.vm index 2b941abd6..2ee07b335 100644 --- a/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/property-documentation-lib.vm +++ b/core/esmf-aspect-model-document-generators/src/main/resources/docu/templates/html/property-documentation-lib.vm @@ -17,7 +17,7 @@ #paragraph( $property.getPreferredName( $i18n.getLocale() ) "${aspectModelHelper.buildAnchor( $property, $parentElement, 'property' )}" $weight ) #if( $property.getDescription( $i18n.getLocale() ) ) - #description( $descriptionsUtils.toHtml( $property.getDescriptions( $i18n.getLocale() ) ) ) + #description( $descriptionsUtils.toHtml( $property.getDescription( $i18n.getLocale() ) ) ) #end
                          From 5d29f2ab00a5787b21ee51a50ef0620a744bbbe7 Mon Sep 17 00:00:00 2001 From: Yauhenikapl Date: Thu, 15 May 2025 10:01:25 +0300 Subject: [PATCH 07/18] Update tests --- .../utils/DescriptionsUtilsTest.java | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java index f3a1389e4..a2fc18705 100644 --- a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java +++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java @@ -10,18 +10,22 @@ class DescriptionsUtilsTest { @Test void testExtractNotes_singleNote() { - String description = "> NOTE: This is a note.\n> Continued on the next line."; - List notes = DescriptionsUtils.notes( description ); + final String description = "> NOTE: This is a note.\n> Continued on the next line."; + final List notes = DescriptionsUtils.notes( description ); assertEquals( 1, notes.size() ); assertEquals( "This is a note.\nContinued on the next line.", notes.get( 0 ) ); } @Test void testExtractExamples_multipleExamples() { - String description = - "> EXAMPLE 1: First example.\n> More detail.\n" + - "> EXAMPLE 2: Second example."; - List examples = DescriptionsUtils.examples( description ); + final String description = + """ + > EXAMPLE 1: First example. + > More detail. + + > EXAMPLE 2: Second example. + """; + final List examples = DescriptionsUtils.examples( description ); assertEquals( 2, examples.size() ); assertEquals( "First example.\nMore detail.", examples.get( 0 ) ); @@ -30,18 +34,21 @@ void testExtractExamples_multipleExamples() { @Test void testExtractSources_withLink() { - String description = "> SOURCE: Source with [link](https://example.com)"; - List sources = DescriptionsUtils.sources( description ); + final String description = "> SOURCE: Source with [link](https://example.com)"; + final List sources = DescriptionsUtils.sources( description ); assertEquals( 1, sources.size() ); assertTrue( sources.get( 0 ).contains( "[link](https://example.com)" ) ); } @Test void testMixedBlockTypes() { - String description = - "> NOTE: A note block.\n" + - "> EXAMPLE: An example block.\n" + - "> SOURCE: A source block."; + final String description = + """ + > NOTE: A note block. + > EXAMPLE: An example block. + + > SOURCE: A source block. + """; assertEquals( 1, DescriptionsUtils.notes( description ).size() ); assertEquals( 1, DescriptionsUtils.examples( description ).size() ); assertEquals( 1, DescriptionsUtils.sources( description ).size() ); @@ -49,7 +56,7 @@ void testMixedBlockTypes() { @Test void testNoBlocks() { - String description = "This is a plain description without any special blocks."; + final String description = "This is a plain description without any special blocks."; assertTrue( DescriptionsUtils.notes( description ).isEmpty() ); assertTrue( DescriptionsUtils.examples( description ).isEmpty() ); assertTrue( DescriptionsUtils.sources( description ).isEmpty() ); @@ -72,8 +79,7 @@ void testToHtml_withAllBlockTypes() { Some **markdown** content here. 1. Ordered 2. List - """ - ; + """; final String html = DescriptionsUtils.toHtml( description ); From a0de13ed3d9006a2748e8961237caaf19375d504 Mon Sep 17 00:00:00 2001 From: Yauhenikapl Date: Fri, 16 May 2025 10:54:53 +0300 Subject: [PATCH 08/18] Update project tot new version parent 20 --- core/esmf-aspect-meta-model-java/pom.xml | 1 - pom.xml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/core/esmf-aspect-meta-model-java/pom.xml b/core/esmf-aspect-meta-model-java/pom.xml index 2a89f3617..b054dd58e 100644 --- a/core/esmf-aspect-meta-model-java/pom.xml +++ b/core/esmf-aspect-meta-model-java/pom.xml @@ -66,7 +66,6 @@ org.commonmark commonmark - 0.24.0 diff --git a/pom.xml b/pom.xml index bd8ab81dd..9bc5ca5eb 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ org.eclipse.esmf esmf-parent - 19 + 20 esmf-sdk-parent From 17b1e9e8e58cf736aadece27026a6f90f0a0a7ca Mon Sep 17 00:00:00 2001 From: Yauhenikapl Date: Fri, 16 May 2025 12:01:47 +0300 Subject: [PATCH 09/18] Add tests and refactoring --- .../aspectmodel/utils/DescriptionsUtils.java | 3 +- .../utils/MarkdownHtmlRenderer.java | 4 +- .../utils/DescriptionsUtilsTest.java | 92 +++++++++++++++---- ...AspectModelDocumentationGeneratorTest.java | 32 +++---- 4 files changed, 94 insertions(+), 37 deletions(-) diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java index d35287965..0a9be5b07 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java @@ -15,7 +15,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -37,7 +36,7 @@ private DescriptionsUtils() { * Matches lines beginning with {@code > NOTE:}, {@code > EXAMPLE:}, or {@code > SOURCE:}, * optionally followed by a number (e.g., {@code > EXAMPLE 2: ...}). */ - private static final Pattern BLOCK_PATTERN = Pattern.compile( + static final Pattern BLOCK_PATTERN = Pattern.compile( "^>\\s*(NOTE|EXAMPLE|SOURCE)(\\s+\\d+)?:\\s*(.*)", Pattern.CASE_INSENSITIVE ); diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java index 2354f00f0..7725ccf0d 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/MarkdownHtmlRenderer.java @@ -17,7 +17,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -140,14 +139,13 @@ private static String renderSpecialBlock( final String type, final List * @return A map of special block types to their associated content. */ private static Map> collectSpecialBlocks( final String[] lines, final StringBuilder markdownBuffer ) { - Pattern pattern = Pattern.compile( "^>\\s*(NOTE|EXAMPLE|SOURCE)(\\s+\\d+)?:\\s*(.*)", Pattern.CASE_INSENSITIVE ); Map> specialBlocks = new LinkedHashMap<>(); String currentType = null; StringBuilder block = new StringBuilder(); for ( String line : lines ) { - Matcher matcher = pattern.matcher( line ); + Matcher matcher = DescriptionsUtils.BLOCK_PATTERN.matcher( line ); if ( matcher.find() ) { flushBlock( currentType, block, specialBlocks ); currentType = matcher.group( 1 ).toUpperCase(); diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java index a2fc18705..2c0c41232 100644 --- a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java +++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java @@ -1,5 +1,6 @@ package org.eclipse.esmf.aspectmodel.utils; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -9,15 +10,15 @@ class DescriptionsUtilsTest { @Test - void testExtractNotes_singleNote() { + void testExtractNotesSingleNote() { final String description = "> NOTE: This is a note.\n> Continued on the next line."; final List notes = DescriptionsUtils.notes( description ); - assertEquals( 1, notes.size() ); + assertThat( notes ).hasSize( 1 ); assertEquals( "This is a note.\nContinued on the next line.", notes.get( 0 ) ); } @Test - void testExtractExamples_multipleExamples() { + void testExtractExamplesMultipleExamples() { final String description = """ > EXAMPLE 1: First example. @@ -33,11 +34,11 @@ void testExtractExamples_multipleExamples() { } @Test - void testExtractSources_withLink() { + void testExtractSourcesWithLink() { final String description = "> SOURCE: Source with [link](https://example.com)"; final List sources = DescriptionsUtils.sources( description ); assertEquals( 1, sources.size() ); - assertTrue( sources.get( 0 ).contains( "[link](https://example.com)" ) ); + assertThat( sources.get( 0 ) ).contains( "[link](https://example.com)" ); } @Test @@ -57,13 +58,13 @@ void testMixedBlockTypes() { @Test void testNoBlocks() { final String description = "This is a plain description without any special blocks."; - assertTrue( DescriptionsUtils.notes( description ).isEmpty() ); - assertTrue( DescriptionsUtils.examples( description ).isEmpty() ); - assertTrue( DescriptionsUtils.sources( description ).isEmpty() ); + assertThat( DescriptionsUtils.notes( description ) ).isEmpty(); + assertThat( DescriptionsUtils.examples( description ) ).isEmpty(); + assertThat( DescriptionsUtils.sources( description ) ).isEmpty(); } @Test - void testToHtml_withAllBlockTypes() { + void testToHtmlWithAllBlockTypes() { final String description = """ > NOTE: This is a note. @@ -83,13 +84,72 @@ void testToHtml_withAllBlockTypes() { final String html = DescriptionsUtils.toHtml( description ); - assertTrue( html.contains( "
                          " ) ); - assertTrue( html.contains( "This is a note." ) ); + assertThat( html ).contains( "
                          " ); + assertThat( html ).contains( "This is a note." ); assertTrue( html.contains( "
                            " ) || html.contains( "
                            " ) ); - assertTrue( html.contains( "First example." ) ); - assertTrue( html.contains( "
                            " ) ); - assertTrue( html.contains( "Source information here." ) ); - assertTrue( html.contains( "markdown" ) ); - assertTrue( html.contains( "
                              " ) ); + assertThat( html ).contains( "First example." ); + assertThat( html ).contains( "
                              " ); + assertThat( html ).contains( "Source information here." ); + assertThat( html ).contains( "markdown" ); + assertThat( html ).contains( "
                                " ); + } + + @Test + void testMarkdownRenderingBulletList() { + String description = """ + This is a list: + * Item A + * Item B + * Item C + """; + String html = DescriptionsUtils.toHtml( description ); + assertThat( html ).contains( "
                                  " ); + assertThat( html ).contains( "
                                • Item A
                                • " ); + assertThat( html ).contains( "
                                • Item B
                                • " ); + assertThat( html ).contains( "
                                • Item C
                                • " ); + } + + @Test + void testMarkdownRenderingOrderedList() { + String description = """ + Steps: + 1. First + 2. Second + 3. Third + """; + String html = DescriptionsUtils.toHtml( description ); + assertThat( html ).contains( "
                                    " ); + assertThat( html ).contains( "
                                  1. First
                                  2. " ); + assertThat( html ).contains( "
                                  3. Second
                                  4. " ); + assertThat( html ).contains( "
                                  5. Third
                                  6. " ); + } + + @Test + void testMarkdownRenderingSpecialBlock() { + String description = + """ + > NOTE: This is a note. + > Continued here. + """; + String html = DescriptionsUtils.toHtml( description ); + assertThat( html ).contains( "
                                    " ); + assertThat( html ).contains( "This is a note." ); + assertThat( html ).contains( "Continued here." ); + } + + @Test + void testMarkdownRenderingWithLink() { + String description = + "Here is a [link](https://example.com) in the text."; + String html = DescriptionsUtils.toHtml( description ); + assertThat( html ).contains( "link" ); + } + + @Test + void testHtmlOutputDoesNotContainMarkdownSyntax() { + String description = + "This is a [link](https://example.com)."; + String html = DescriptionsUtils.toHtml( description ); + assertThat( html ).doesNotContain( "[link](https://example.com)" ); } } diff --git a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java index df6a81961..e3df0070c 100644 --- a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java +++ b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java @@ -28,10 +28,10 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -class AspectModelDocumentationGeneratorTest { +public class AspectModelDocumentationGeneratorTest { @ParameterizedTest @EnumSource( value = TestAspect.class ) - void testGeneration( final TestAspect testAspect ) { + public void testGeneration( final TestAspect testAspect ) { assertThatCode( () -> { final String html = generateHtmlDocumentation( testAspect ); assertThat( html ).doesNotContain( "UnnamedCharacteristic" ); @@ -41,7 +41,7 @@ void testGeneration( final TestAspect testAspect ) { } @Test - void testAspectWithEntityCollection() throws Throwable { + public void testAspectWithEntityCollection() throws Throwable { final String htmlResult = generateHtmlDocumentation( TestAspect.ASPECT_WITH_ENTITY_COLLECTION ); assertThat( htmlResult ).isNotEmpty(); @@ -53,7 +53,7 @@ void testAspectWithEntityCollection() throws Throwable { } @Test - void testAspectWithCollectionOfSimpleType() throws Throwable { + public void testAspectWithCollectionOfSimpleType() throws Throwable { final String htmlResult = generateHtmlDocumentation( TestAspect.ASPECT_WITH_COLLECTION_OF_SIMPLE_TYPE ); assertThat( htmlResult ).isNotEmpty(); @@ -65,14 +65,14 @@ void testAspectWithCollectionOfSimpleType() throws Throwable { } @Test - void testScriptTagIsEscaped() throws IOException { + public void testScriptTagIsEscaped() throws IOException { assertThat( generateHtmlDocumentation( TestAspect.ASPECT_WITH_SCRIPT_TAGS ) ) .isNotEmpty() .doesNotContain( "" ); } @Test - void testRubyGemUpdateCommandIsNotExecuted() throws IOException { + public void testRubyGemUpdateCommandIsNotExecuted() throws IOException { try ( final ByteArrayOutputStream stdOut = new ByteArrayOutputStream() ) { System.setOut( new PrintStream( stdOut ) ); generateHtmlDocumentation( TestAspect.ASPECT_WITH_RUBY_GEM_UPDATE_COMMAND ); @@ -81,7 +81,7 @@ void testRubyGemUpdateCommandIsNotExecuted() throws IOException { } @Test - void testHtmlTagsAreEscaped() throws IOException { + public void testHtmlTagsAreEscaped() throws IOException { assertThat( generateHtmlDocumentation( TestAspect.ASPECT_WITH_HTML_TAGS ) ) .isNotEmpty() .doesNotContain( "" ) @@ -90,20 +90,20 @@ void testHtmlTagsAreEscaped() throws IOException { } @Test - void testEncodedTextIsNotDecoded() throws IOException { + public void testEncodedTextIsNotDecoded() throws IOException { assertThat( generateHtmlDocumentation( TestAspect.ASPECT_WITH_ENCODED_STRINGS ) ) .doesNotContain( "This is an Aspect with encoded text." ) .contains( "VGhpcyBpcyBhbiBBc3BlY3Qgd2l0aCBlbmNvZGVkIHRleHQu" ); } @Test - void testAspectModelUrnIsDisplayed() throws IOException { + public void testAspectModelUrnIsDisplayed() throws IOException { assertThat( generateHtmlDocumentation( TestAspect.ASPECT_WITH_HTML_TAGS ) ) .contains( "urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithHtmlTags" ); } @Test - void testDocInfosAreDisplayed() throws IOException { + public void testDocInfosAreDisplayed() throws IOException { assertThat( generateHtmlDocumentation( TestAspect.ASPECT_WITH_HTML_TAGS ) ) .contains( ".toc-list" ) .contains( "aspect-model-diagram" ) @@ -115,13 +115,13 @@ void testDocInfosAreDisplayed() throws IOException { } @Test - void testDocumentationIsNotEmptyForModelWithoutLanguageTags() throws IOException { + public void testDocumentationIsNotEmptyForModelWithoutLanguageTags() throws IOException { final String aspectWithoutLanguageTags = generateHtmlDocumentation( TestAspect.ASPECT_WITHOUT_LANGUAGE_TAGS ); assertThat( aspectWithoutLanguageTags ).isNotEmpty(); } @Test - void testAspectWithAbstractSingleEntityExpectSuccess() throws IOException { + public void testAspectWithAbstractSingleEntityExpectSuccess() throws IOException { final String documentation = generateHtmlDocumentation( TestAspect.ASPECT_WITH_ABSTRACT_SINGLE_ENTITY ); assertThat( documentation ).contains( "

                                    testPropertyTest Property

                                    " ); @@ -146,7 +146,7 @@ void testAspectWithAbstractEntityExpectSuccess() throws IOException { } @Test - void testAspectWithCollectionWithAbstractEntityExpectSuccess() throws IOException { + public void testAspectWithCollectionWithAbstractEntityExpectSuccess() throws IOException { final String documentation = generateHtmlDocumentation( TestAspect.ASPECT_WITH_COLLECTION_WITH_ABSTRACT_ENTITY ); assertThat( documentation ).contains( "

                                    generateHtmlDocumentation( TestAspect.ASPECT_WITH_QUANTIFIABLE_WITHOUT_UNIT ) ) @@ -168,7 +168,7 @@ void testAspectWithQuantifiableWithoutUnit() throws IOException { } @Test - void testAspectWithConstraintWithSeeAttribute() throws IOException { + public void testAspectWithConstraintWithSeeAttribute() throws IOException { final String documentation = generateHtmlDocumentation( TestAspect.ASPECT_WITH_CONSTRAINT_WITH_SEE_ATTRIBUTE ); assertThat( documentation ).contains( "

                                    Date: Fri, 16 May 2025 12:32:34 +0300 Subject: [PATCH 10/18] fix tests --- ...AspectModelDocumentationGeneratorTest.java | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java index e3df0070c..5a2f7a5ec 100644 --- a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java +++ b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java @@ -28,10 +28,10 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -public class AspectModelDocumentationGeneratorTest { +class AspectModelDocumentationGeneratorTest { @ParameterizedTest @EnumSource( value = TestAspect.class ) - public void testGeneration( final TestAspect testAspect ) { + void testGeneration( final TestAspect testAspect ) { assertThatCode( () -> { final String html = generateHtmlDocumentation( testAspect ); assertThat( html ).doesNotContain( "UnnamedCharacteristic" ); @@ -41,7 +41,7 @@ public void testGeneration( final TestAspect testAspect ) { } @Test - public void testAspectWithEntityCollection() throws Throwable { + void testAspectWithEntityCollection() throws Throwable { final String htmlResult = generateHtmlDocumentation( TestAspect.ASPECT_WITH_ENTITY_COLLECTION ); assertThat( htmlResult ).isNotEmpty(); @@ -53,7 +53,7 @@ public void testAspectWithEntityCollection() throws Throwable { } @Test - public void testAspectWithCollectionOfSimpleType() throws Throwable { + void testAspectWithCollectionOfSimpleType() throws Throwable { final String htmlResult = generateHtmlDocumentation( TestAspect.ASPECT_WITH_COLLECTION_OF_SIMPLE_TYPE ); assertThat( htmlResult ).isNotEmpty(); @@ -65,14 +65,15 @@ public void testAspectWithCollectionOfSimpleType() throws Throwable { } @Test - public void testScriptTagIsEscaped() throws IOException { + void testScriptTagIsEscaped() throws IOException { + System.out.println("TEST: " + generateHtmlDocumentation( TestAspect.ASPECT_WITH_SCRIPT_TAGS )); assertThat( generateHtmlDocumentation( TestAspect.ASPECT_WITH_SCRIPT_TAGS ) ) .isNotEmpty() - .doesNotContain( "" ); + .doesNotContain( "Test preferred name with script: " ); } @Test - public void testRubyGemUpdateCommandIsNotExecuted() throws IOException { + void testRubyGemUpdateCommandIsNotExecuted() throws IOException { try ( final ByteArrayOutputStream stdOut = new ByteArrayOutputStream() ) { System.setOut( new PrintStream( stdOut ) ); generateHtmlDocumentation( TestAspect.ASPECT_WITH_RUBY_GEM_UPDATE_COMMAND ); @@ -81,7 +82,7 @@ public void testRubyGemUpdateCommandIsNotExecuted() throws IOException { } @Test - public void testHtmlTagsAreEscaped() throws IOException { + void testHtmlTagsAreEscaped() throws IOException { assertThat( generateHtmlDocumentation( TestAspect.ASPECT_WITH_HTML_TAGS ) ) .isNotEmpty() .doesNotContain( "" ) @@ -90,20 +91,20 @@ public void testHtmlTagsAreEscaped() throws IOException { } @Test - public void testEncodedTextIsNotDecoded() throws IOException { + void testEncodedTextIsNotDecoded() throws IOException { assertThat( generateHtmlDocumentation( TestAspect.ASPECT_WITH_ENCODED_STRINGS ) ) .doesNotContain( "This is an Aspect with encoded text." ) .contains( "VGhpcyBpcyBhbiBBc3BlY3Qgd2l0aCBlbmNvZGVkIHRleHQu" ); } @Test - public void testAspectModelUrnIsDisplayed() throws IOException { + void testAspectModelUrnIsDisplayed() throws IOException { assertThat( generateHtmlDocumentation( TestAspect.ASPECT_WITH_HTML_TAGS ) ) .contains( "urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithHtmlTags" ); } @Test - public void testDocInfosAreDisplayed() throws IOException { + void testDocInfosAreDisplayed() throws IOException { assertThat( generateHtmlDocumentation( TestAspect.ASPECT_WITH_HTML_TAGS ) ) .contains( ".toc-list" ) .contains( "aspect-model-diagram" ) @@ -115,13 +116,13 @@ public void testDocInfosAreDisplayed() throws IOException { } @Test - public void testDocumentationIsNotEmptyForModelWithoutLanguageTags() throws IOException { + void testDocumentationIsNotEmptyForModelWithoutLanguageTags() throws IOException { final String aspectWithoutLanguageTags = generateHtmlDocumentation( TestAspect.ASPECT_WITHOUT_LANGUAGE_TAGS ); assertThat( aspectWithoutLanguageTags ).isNotEmpty(); } @Test - public void testAspectWithAbstractSingleEntityExpectSuccess() throws IOException { + void testAspectWithAbstractSingleEntityExpectSuccess() throws IOException { final String documentation = generateHtmlDocumentation( TestAspect.ASPECT_WITH_ABSTRACT_SINGLE_ENTITY ); assertThat( documentation ).contains( "

                                    testPropertyTest Property

                                    " ); @@ -146,7 +147,7 @@ public void testAspectWithAbstractEntityExpectSuccess() throws IOException { } @Test - public void testAspectWithCollectionWithAbstractEntityExpectSuccess() throws IOException { + void testAspectWithCollectionWithAbstractEntityExpectSuccess() throws IOException { final String documentation = generateHtmlDocumentation( TestAspect.ASPECT_WITH_COLLECTION_WITH_ABSTRACT_ENTITY ); assertThat( documentation ).contains( "

                                    generateHtmlDocumentation( TestAspect.ASPECT_WITH_QUANTIFIABLE_WITHOUT_UNIT ) ) @@ -168,7 +169,7 @@ public void testAspectWithQuantifiableWithoutUnit() throws IOException { } @Test - public void testAspectWithConstraintWithSeeAttribute() throws IOException { + void testAspectWithConstraintWithSeeAttribute() throws IOException { final String documentation = generateHtmlDocumentation( TestAspect.ASPECT_WITH_CONSTRAINT_WITH_SEE_ATTRIBUTE ); assertThat( documentation ).contains( "

                                    Date: Fri, 16 May 2025 12:35:52 +0300 Subject: [PATCH 11/18] fix styles --- .../generator/docu/AspectModelDocumentationGeneratorTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java index 5a2f7a5ec..10708faa4 100644 --- a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java +++ b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java @@ -66,7 +66,7 @@ void testAspectWithCollectionOfSimpleType() throws Throwable { @Test void testScriptTagIsEscaped() throws IOException { - System.out.println("TEST: " + generateHtmlDocumentation( TestAspect.ASPECT_WITH_SCRIPT_TAGS )); + System.out.println( "TEST: " + generateHtmlDocumentation( TestAspect.ASPECT_WITH_SCRIPT_TAGS ) ); assertThat( generateHtmlDocumentation( TestAspect.ASPECT_WITH_SCRIPT_TAGS ) ) .isNotEmpty() .doesNotContain( "Test preferred name with script: " ); From e7be12ec3a814d0c644cc79fc05d85ce66ecb5ab Mon Sep 17 00:00:00 2001 From: Yauhenikapl Date: Fri, 16 May 2025 12:36:53 +0300 Subject: [PATCH 12/18] fix styles --- .../generator/docu/AspectModelDocumentationGeneratorTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java index 10708faa4..bf41de51d 100644 --- a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java +++ b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java @@ -66,7 +66,6 @@ void testAspectWithCollectionOfSimpleType() throws Throwable { @Test void testScriptTagIsEscaped() throws IOException { - System.out.println( "TEST: " + generateHtmlDocumentation( TestAspect.ASPECT_WITH_SCRIPT_TAGS ) ); assertThat( generateHtmlDocumentation( TestAspect.ASPECT_WITH_SCRIPT_TAGS ) ) .isNotEmpty() .doesNotContain( "Test preferred name with script: " ); From 5258e3ca2e8fb732e182834e24b5a29e184105b6 Mon Sep 17 00:00:00 2001 From: Yauhenikapl Date: Fri, 16 May 2025 15:17:39 +0300 Subject: [PATCH 13/18] Update tests and add markdown description Model --- .../utils/DescriptionsUtilsTest.java | 146 ++++++++---------- .../org/eclipse/esmf/test/TestAspect.java | 1 + .../1.0.0/AspectWithMarkdownDescription.ttl | 51 ++++++ 3 files changed, 115 insertions(+), 83 deletions(-) create mode 100644 core/esmf-test-aspect-models/src/main/resources/valid/org.eclipse.esmf.test/1.0.0/AspectWithMarkdownDescription.ttl diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java index 2c0c41232..24a38252f 100644 --- a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java +++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java @@ -5,54 +5,64 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; +import java.util.Locale; +import org.eclipse.esmf.aspectmodel.AspectModelFile; +import org.eclipse.esmf.metamodel.AspectModel; +import org.eclipse.esmf.test.TestAspect; +import org.eclipse.esmf.test.TestResources; + +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; class DescriptionsUtilsTest { + + private static String testDescription; + + @BeforeAll + public static void init() { + final AspectModel aspectModel = TestResources.load( TestAspect.ASPECT_WITH_MARKDOWN_DESCRIPTION ); + final AspectModelFile originalFile = aspectModel.files().iterator().next(); + testDescription = originalFile.elements().getFirst().getDescription( Locale.ENGLISH ); + } + @Test void testExtractNotesSingleNote() { - final String description = "> NOTE: This is a note.\n> Continued on the next line."; - final List notes = DescriptionsUtils.notes( description ); + final List notes = DescriptionsUtils.notes( testDescription ); assertThat( notes ).hasSize( 1 ); - assertEquals( "This is a note.\nContinued on the next line.", notes.get( 0 ) ); + assertEquals( "This is a note block.\nIt supports multiple lines.\nHere's a second line of the note.", notes.get( 0 ) ); } @Test void testExtractExamplesMultipleExamples() { - final String description = - """ - > EXAMPLE 1: First example. - > More detail. - - > EXAMPLE 2: Second example. - """; - final List examples = DescriptionsUtils.examples( description ); + final List examples = DescriptionsUtils.examples( testDescription ); assertEquals( 2, examples.size() ); - assertEquals( "First example.\nMore detail.", examples.get( 0 ) ); - assertEquals( "Second example.", examples.get( 1 ) ); + assertEquals( "This is the first example block.\nIt can span several lines, and supports *italic* and **bold** text.", + examples.get( 0 ) ); + assertEquals( "This is the second example.\nAlso multiline, for testing multiple example entries.", examples.get( 1 ) ); + } + + @Test + void testExtractExamplesMultipleExamplesWithBoldAndItalicText() { + final String html = DescriptionsUtils.toHtml( testDescription ); + + assertThat( html ).contains( + "This is the first example block.\nIt can span several lines, and supports italic and bold text." ); } @Test void testExtractSourcesWithLink() { - final String description = "> SOURCE: Source with [link](https://example.com)"; - final List sources = DescriptionsUtils.sources( description ); + final List sources = DescriptionsUtils.sources( testDescription ); assertEquals( 1, sources.size() ); - assertThat( sources.get( 0 ) ).contains( "[link](https://example.com)" ); + assertThat( sources.get( 0 ) ).contains( "ISO 12345:2023, section 4.2.1\n" + "with an inline [link](https://www.example.com/spec)." ); } @Test void testMixedBlockTypes() { - final String description = - """ - > NOTE: A note block. - > EXAMPLE: An example block. - - > SOURCE: A source block. - """; - assertEquals( 1, DescriptionsUtils.notes( description ).size() ); - assertEquals( 1, DescriptionsUtils.examples( description ).size() ); - assertEquals( 1, DescriptionsUtils.sources( description ).size() ); + assertEquals( 1, DescriptionsUtils.notes( testDescription ).size() ); + assertEquals( 2, DescriptionsUtils.examples( testDescription ).size() ); + assertEquals( 1, DescriptionsUtils.sources( testDescription ).size() ); } @Test @@ -65,44 +75,37 @@ void testNoBlocks() { @Test void testToHtmlWithAllBlockTypes() { - final String description = - """ - > NOTE: This is a note. - > With multiple lines. - - > EXAMPLE 1: First example. - > Additional example content. - - > EXAMPLE 2: Second example. - - > SOURCE: Source information here. - - Some **markdown** content here. - 1. Ordered - 2. List - """; - - final String html = DescriptionsUtils.toHtml( description ); + final String description = """ + > NOTE: This is a note. + > With multiple lines. + + > EXAMPLE 1: First example. + > Additional example content. + + > EXAMPLE 2: Second example. + + > SOURCE: Source information here. + + Some **markdown** content here. + 1. Ordered + 2. List + """; + + final String html = DescriptionsUtils.toHtml( testDescription ); assertThat( html ).contains( "
                                    " ); - assertThat( html ).contains( "This is a note." ); + assertThat( html ).contains( "This is a note block.\nIt supports multiple lines.\nHere's a second line of the note." ); assertTrue( html.contains( "
                                      " ) || html.contains( "
                                      " ) ); - assertThat( html ).contains( "First example." ); + assertThat( html ).contains( + "This is the first example block.\nIt can span several lines, and supports italic and bold text." ); assertThat( html ).contains( "
                                      " ); - assertThat( html ).contains( "Source information here." ); - assertThat( html ).contains( "markdown" ); + assertThat( html ).contains( "ISO 12345:2023, section 4.2.1\nwith an inline link." ); assertThat( html ).contains( "
                                        " ); } @Test void testMarkdownRenderingBulletList() { - String description = """ - This is a list: - * Item A - * Item B - * Item C - """; - String html = DescriptionsUtils.toHtml( description ); + String html = DescriptionsUtils.toHtml( testDescription ); assertThat( html ).contains( "
                                          " ); assertThat( html ).contains( "
                                        • Item A
                                        • " ); assertThat( html ).contains( "
                                        • Item B
                                        • " ); @@ -111,45 +114,22 @@ void testMarkdownRenderingBulletList() { @Test void testMarkdownRenderingOrderedList() { - String description = """ - Steps: - 1. First - 2. Second - 3. Third - """; - String html = DescriptionsUtils.toHtml( description ); + String html = DescriptionsUtils.toHtml( testDescription ); assertThat( html ).contains( "
                                            " ); assertThat( html ).contains( "
                                          1. First
                                          2. " ); assertThat( html ).contains( "
                                          3. Second
                                          4. " ); assertThat( html ).contains( "
                                          5. Third
                                          6. " ); } - @Test - void testMarkdownRenderingSpecialBlock() { - String description = - """ - > NOTE: This is a note. - > Continued here. - """; - String html = DescriptionsUtils.toHtml( description ); - assertThat( html ).contains( "
                                            " ); - assertThat( html ).contains( "This is a note." ); - assertThat( html ).contains( "Continued here." ); - } - @Test void testMarkdownRenderingWithLink() { - String description = - "Here is a [link](https://example.com) in the text."; - String html = DescriptionsUtils.toHtml( description ); - assertThat( html ).contains( "link" ); + String html = DescriptionsUtils.toHtml( testDescription ); + assertThat( html ).contains( "Visit Example" ); } @Test void testHtmlOutputDoesNotContainMarkdownSyntax() { - String description = - "This is a [link](https://example.com)."; - String html = DescriptionsUtils.toHtml( description ); - assertThat( html ).doesNotContain( "[link](https://example.com)" ); + String html = DescriptionsUtils.toHtml( testDescription ); + assertThat( html ).doesNotContain( "[Visit Example](https://example.com)" ); } } diff --git a/core/esmf-test-aspect-models/src/main/java/org/eclipse/esmf/test/TestAspect.java b/core/esmf-test-aspect-models/src/main/java/org/eclipse/esmf/test/TestAspect.java index 09ec20fc6..0b559b671 100644 --- a/core/esmf-test-aspect-models/src/main/java/org/eclipse/esmf/test/TestAspect.java +++ b/core/esmf-test-aspect-models/src/main/java/org/eclipse/esmf/test/TestAspect.java @@ -116,6 +116,7 @@ public enum TestAspect implements TestModel { ASPECT_WITH_LIST_AND_ELEMENT_CONSTRAINT, ASPECT_WITH_LIST_ENTITY_ENUMERATION, ASPECT_WITH_LIST_WITH_LENGTH_CONSTRAINT, + ASPECT_WITH_MARKDOWN_DESCRIPTION, ASPECT_WITH_MEASUREMENT, ASPECT_WITH_MEASUREMENT_WITH_UNIT, ASPECT_WITH_MULTILANGUAGE_EXAMPLE_VALUE, diff --git a/core/esmf-test-aspect-models/src/main/resources/valid/org.eclipse.esmf.test/1.0.0/AspectWithMarkdownDescription.ttl b/core/esmf-test-aspect-models/src/main/resources/valid/org.eclipse.esmf.test/1.0.0/AspectWithMarkdownDescription.ttl new file mode 100644 index 000000000..8b5c342c1 --- /dev/null +++ b/core/esmf-test-aspect-models/src/main/resources/valid/org.eclipse.esmf.test/1.0.0/AspectWithMarkdownDescription.ttl @@ -0,0 +1,51 @@ +# Copyright (c) 2025 Robert Bosch Manufacturing Solutions GmbH +# +# See the AUTHORS file(s) distributed with this work for additional +# information regarding authorship. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 + +@prefix : . +@prefix samm: . +@prefix samm-c: . + +:AspectWithMarkdownDescription a samm:Aspect ; + samm:properties ( :myProperty ) ; + samm:operations ( ) . + +:myProperty a samm:Property ; + samm:description """ +This is a sample concept demonstrating **Markdown** support in samm:description. + +> NOTE: This is a note block. +> It supports multiple lines. +> Here's a second line of the note. + +> EXAMPLE 1: This is the first example block. +> It can span several lines, and supports *italic* and **bold** text. + +> EXAMPLE 2: This is the second example. +> Also multiline, for testing multiple example entries. + +> SOURCE: ISO 12345:2023, section 4.2.1 +> with an inline [link](https://www.example.com/spec). + +Unordered list: +* Item A +* Item B +* Item C + +Ordered list: +1. First +2. Second +3. Third + +You can also include inline links like [Visit Example](https://example.com). + +Another paragraph after a blank line to simulate text flow and paragraph breaks. + """@en ; + samm:characteristic samm-c:Text . From 8d753c62de0827c84b76d2852b9c76ca6158fcb0 Mon Sep 17 00:00:00 2001 From: Yauhenikapl Date: Fri, 16 May 2025 15:51:24 +0300 Subject: [PATCH 14/18] Update tests and refactoring --- .../aspectmodel/utils/DescriptionsUtils.java | 19 ++++++-- .../utils/DescriptionsUtilsTest.java | 8 ++++ ...AspectModelDocumentationGeneratorTest.java | 20 ++++++++ .../1.0.0/AspectWithMarkdownDescription.ttl | 48 +++++++++---------- 4 files changed, 68 insertions(+), 27 deletions(-) diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java index 0a9be5b07..ce43a17fc 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtils.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * Utility class for extracting and rendering structured content blocks (such as NOTE, EXAMPLE, SOURCE) @@ -88,13 +89,13 @@ public static String toHtml( final String description ) { *

                                            Each block is expected to begin with a {@code > TYPE:} line and may span multiple lines, * each of which begins with {@code >}. * - * @param descriptions A set of multi-line Markdown description strings. + * @param description A line Markdown description string. * @param type The type of block to extract ("NOTE", "EXAMPLE", or "SOURCE"). * @return A list of extracted block contents for the specified type. */ - private static List extractBlock( final String descriptions, final String type ) { + private static List extractBlock( final String description, final String type ) { List result = new ArrayList<>(); - extractFromDescription( descriptions, type, result ); + extractFromDescription( stripIndent( description ), type, result ); return result; } @@ -141,5 +142,17 @@ private static void flushBlock( boolean[] insideBlock, StringBuilder blockConten insideBlock[0] = false; } } + + static String stripIndent( final String string ) { + final int indent = string.lines() + .filter( line -> !line.isEmpty() ) + .map( line -> line.indexOf( line.trim() ) ) + .filter( offset -> offset > 0 ) + .min( Integer::compareTo ) + .orElse( 0 ); + return string.lines() + .map( line -> indent <= line.length() ? line.substring( indent ) : line ) + .collect( Collectors.joining( "\n" ) ); + } } diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java index 24a38252f..b9d058ee0 100644 --- a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java +++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/utils/DescriptionsUtilsTest.java @@ -132,4 +132,12 @@ void testHtmlOutputDoesNotContainMarkdownSyntax() { String html = DescriptionsUtils.toHtml( testDescription ); assertThat( html ).doesNotContain( "[Visit Example](https://example.com)" ); } + + @Test + void testStripIndentSingleLine() { + String input = " only one line"; + String expected = "only one line"; + String result = DescriptionsUtils.stripIndent( input ); + assertEquals( expected, result ); + } } diff --git a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java index bf41de51d..c113a6826 100644 --- a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java +++ b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/docu/AspectModelDocumentationGeneratorTest.java @@ -20,10 +20,12 @@ import java.io.IOException; import java.io.PrintStream; +import org.eclipse.esmf.aspectmodel.utils.DescriptionsUtils; import org.eclipse.esmf.metamodel.Aspect; import org.eclipse.esmf.test.TestAspect; import org.eclipse.esmf.test.TestResources; +import org.assertj.core.api.AssertionsForClassTypes; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -179,6 +181,24 @@ void testAspectWithConstraintWithSeeAttribute() throws IOException { "

                                          7. http://example.com/me2
                                          8. " ); } + @Test + void testMarkdownRenderingWithLink() throws IOException { + final String htmlResult = generateHtmlDocumentation( TestAspect.ASPECT_WITH_MARKDOWN_DESCRIPTION ); + AssertionsForClassTypes.assertThat( htmlResult ).contains( "Visit Example" ); + } + + @Test + void testAspectWithMarkdownDescription() throws IOException { + final String htmlResult = generateHtmlDocumentation( TestAspect.ASPECT_WITH_MARKDOWN_DESCRIPTION ); + AssertionsForClassTypes.assertThat( htmlResult ).doesNotContain( "[link](https://www.example.com/spec)" ); + } + + @Test + void testHtmlOutputDoesNotContainMarkdownSyntax() throws IOException { + final String htmlResult = generateHtmlDocumentation( TestAspect.ASPECT_WITH_MARKDOWN_DESCRIPTION ); + AssertionsForClassTypes.assertThat( htmlResult ).doesNotContain( "[Visit Example](https://example.com)" ); + } + private String generateHtmlDocumentation( final TestAspect testAspect ) throws IOException { final Aspect aspect = TestResources.load( testAspect ).aspect(); final AspectModelDocumentationGenerator aspectModelDocumentationGenerator = new AspectModelDocumentationGenerator( aspect ); diff --git a/core/esmf-test-aspect-models/src/main/resources/valid/org.eclipse.esmf.test/1.0.0/AspectWithMarkdownDescription.ttl b/core/esmf-test-aspect-models/src/main/resources/valid/org.eclipse.esmf.test/1.0.0/AspectWithMarkdownDescription.ttl index 8b5c342c1..2f2437f48 100644 --- a/core/esmf-test-aspect-models/src/main/resources/valid/org.eclipse.esmf.test/1.0.0/AspectWithMarkdownDescription.ttl +++ b/core/esmf-test-aspect-models/src/main/resources/valid/org.eclipse.esmf.test/1.0.0/AspectWithMarkdownDescription.ttl @@ -14,38 +14,38 @@ @prefix samm-c: . :AspectWithMarkdownDescription a samm:Aspect ; - samm:properties ( :myProperty ) ; - samm:operations ( ) . + samm:properties ( :myProperty ) ; + samm:operations ( ) . :myProperty a samm:Property ; - samm:description """ -This is a sample concept demonstrating **Markdown** support in samm:description. + samm:description """ + This is a sample concept demonstrating **Markdown** support in samm:description. -> NOTE: This is a note block. -> It supports multiple lines. -> Here's a second line of the note. + > NOTE: This is a note block. + > It supports multiple lines. + > Here's a second line of the note. -> EXAMPLE 1: This is the first example block. -> It can span several lines, and supports *italic* and **bold** text. + > EXAMPLE 1: This is the first example block. + > It can span several lines, and supports *italic* and **bold** text. -> EXAMPLE 2: This is the second example. -> Also multiline, for testing multiple example entries. + > EXAMPLE 2: This is the second example. + > Also multiline, for testing multiple example entries. -> SOURCE: ISO 12345:2023, section 4.2.1 -> with an inline [link](https://www.example.com/spec). + > SOURCE: ISO 12345:2023, section 4.2.1 + > with an inline [link](https://www.example.com/spec). -Unordered list: -* Item A -* Item B -* Item C + Unordered list: + * Item A + * Item B + * Item C -Ordered list: -1. First -2. Second -3. Third + Ordered list: + 1. First + 2. Second + 3. Third -You can also include inline links like [Visit Example](https://example.com). + You can also include inline links like [Visit Example](https://example.com). -Another paragraph after a blank line to simulate text flow and paragraph breaks. - """@en ; + Another paragraph after a blank line to simulate text flow and paragraph breaks. + """@en ; samm:characteristic samm-c:Text . From f5e61741ed1e11c2e7f8fdc6f121b2dcd3f7100d Mon Sep 17 00:00:00 2001 From: Yauhenikapl Date: Fri, 16 May 2025 16:15:55 +0300 Subject: [PATCH 15/18] Fix tests --- .../sql/databricks/DatabricksColumnDefinitionParserTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksColumnDefinitionParserTest.java b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksColumnDefinitionParserTest.java index 68501ce67..bfdfe8fcf 100644 --- a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksColumnDefinitionParserTest.java +++ b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksColumnDefinitionParserTest.java @@ -32,7 +32,9 @@ void testMinimalDefinition() { } @ParameterizedTest - @EnumSource( value = TestAspect.class ) + @EnumSource( value = TestAspect.class, mode = EnumSource.Mode.EXCLUDE, names = { + "ASPECT_WITH_MARKDOWN_DESCRIPTION" + } ) void testParseSqlForAspectModel( final TestAspect testAspect ) { final String sql = sql( testAspect ); final String parsedAndSerializedSql = sql.lines() From 2d80da9542f988ed433e71c885d83d20b26b636a Mon Sep 17 00:00:00 2001 From: "Textor Andreas (BCI/ESW17)" Date: Mon, 19 May 2025 11:22:20 +0200 Subject: [PATCH 16/18] Fix formatting --- .../org/eclipse/esmf/metamodel/builder/SammBuilder.java | 8 ++++---- .../AspectModelDatabricksDenormalizedSqlVisitor.java | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/metamodel/builder/SammBuilder.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/metamodel/builder/SammBuilder.java index 60ec982a3..a2d66f10c 100644 --- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/metamodel/builder/SammBuilder.java +++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/metamodel/builder/SammBuilder.java @@ -1389,12 +1389,12 @@ public static ScalarValue value( final double doubleValue ) { /* Intentionally no value(int) method here, because an int value could imply different XSD types */ public static ScalarValue value( final Object value, final Scalar type ) { - MetaModelBaseAttributes metaModelBaseAttributes; + final MetaModelBaseAttributes metaModelBaseAttributes; - if ( value instanceof ModelElement modelElement ) { - boolean hasUrn = modelElement.urn() != null; + if ( value instanceof final ModelElement modelElement ) { + final boolean hasUrn = modelElement.urn() != null; - MetaModelBaseAttributes.Builder builder = MetaModelBaseAttributes.builder() + final MetaModelBaseAttributes.Builder builder = MetaModelBaseAttributes.builder() .isAnonymous( !hasUrn ); if ( hasUrn ) { diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/AspectModelDatabricksDenormalizedSqlVisitor.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/AspectModelDatabricksDenormalizedSqlVisitor.java index 85756fbb0..8fa133edf 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/AspectModelDatabricksDenormalizedSqlVisitor.java +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/AspectModelDatabricksDenormalizedSqlVisitor.java @@ -196,7 +196,7 @@ public String visitProperty( final Property property, final Context context ) { } return property.getCharacteristic().get().accept( this, context.copy() - .prefix( (context.prefix().isEmpty() ? "" : context.prefix() + LEVEL_DELIMITER) + columnName( property ) ) + .prefix( ( context.prefix().isEmpty() ? "" : context.prefix() + LEVEL_DELIMITER ) + columnName( property ) ) .currentProperty( property ) .build() ); } @@ -278,7 +278,7 @@ public String visitCollection( final Collection collection, final Context contex private String processComplexType( final ComplexType entity, final Context context, final String parentPrefix, final boolean isDefaultList ) { - StringBuilder columns = new StringBuilder(); + final StringBuilder columns = new StringBuilder(); final String lineDelimiter = ",\n "; entity.getAllProperties().forEach( property -> { @@ -333,7 +333,7 @@ private DatabricksType.DatabricksStruct entityToStruct( final ComplexType entity return Stream.empty(); } - boolean isOptional = isInsideNestedType || property.isOptional(); + final boolean isOptional = isInsideNestedType || property.isOptional(); return Stream.of( new DatabricksType.DatabricksStructEntry( columnName( property ), databricksType, isOptional, Optional.ofNullable( property.getDescription( config.commentLanguage() ) ) ) ); From b36167bd41b3034b9e4c027326a504bfb79d887d Mon Sep 17 00:00:00 2001 From: "Textor Andreas (BCI/ESW17)" Date: Mon, 19 May 2025 11:22:40 +0200 Subject: [PATCH 17/18] Fix Databricks SQL generation to work with multiline comments --- ...ModelDatabricksDenormalizedSqlVisitor.java | 9 ++----- .../DatabricksColumnDefinition.java | 2 +- .../DatabricksCommentDefinition.java | 24 +++++++++++++++++++ .../sql/databricks/DatabricksType.java | 2 +- .../DatabricksColumnDefinitionParserTest.java | 4 +--- 5 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksCommentDefinition.java diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/AspectModelDatabricksDenormalizedSqlVisitor.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/AspectModelDatabricksDenormalizedSqlVisitor.java index 8fa133edf..cdc399694 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/AspectModelDatabricksDenormalizedSqlVisitor.java +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/AspectModelDatabricksDenormalizedSqlVisitor.java @@ -165,17 +165,12 @@ public String visitStructureElement( final StructureElement structureElement, fi return result.toString(); } - private String escapeComment( final String comment ) { - return comment.replace( "'", "\\'" ); - } - @Override public String visitAspect( final Aspect aspect, final Context context ) { - final String columnDeclarations = visitStructureElement( aspect, context ); final String comment = config.includeTableComment() ? Optional.ofNullable( aspect.getDescription( config.commentLanguage() ) ).map( description -> - "COMMENT '" + escapeComment( description ) + "'\n" ).orElse( "" ) + new DatabricksCommentDefinition( description ) + "\n" ).orElse( "" ) : ""; return "%s %s (\n%s%s)\n%sTBLPROPERTIES ('%s'='%s');\n".formatted( config.createTableCommandPrefix(), @@ -242,7 +237,7 @@ public String visitCharacteristic( final Characteristic characteristic, final Co private String column( final String columnName, final String columnType, final boolean isNullable, final Optional comment ) { return "%s %s%s".formatted( columnName, columnType, isNullable ? "" : " NOT NULL" ) - + comment.map( args -> " COMMENT '%s'".formatted( escapeComment( args ) ) ).orElse( "" ); + + comment.map( args -> " " + new DatabricksCommentDefinition( args ) ).orElse( "" ); } @Override diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksColumnDefinition.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksColumnDefinition.java index dde72421f..f5714bb72 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksColumnDefinition.java +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksColumnDefinition.java @@ -38,6 +38,6 @@ public String toString() { name(), type(), nullable ? "" : " NOT NULL", - comment.map( c -> " COMMENT '" + c.replaceAll( "'", "\\\\'" ) + "'" ).orElse( "" ) ); + comment.map( theComment -> " " + new DatabricksCommentDefinition( theComment ) ).orElse( "" ) ); } } diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksCommentDefinition.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksCommentDefinition.java new file mode 100644 index 000000000..e2d797b07 --- /dev/null +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksCommentDefinition.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package org.eclipse.esmf.aspectmodel.generator.sql.databricks; + +public record DatabricksCommentDefinition( String comment ) { + @Override + public String toString() { + return "COMMENT '" + comment + .replace( "'", "\\'" ) + .replace( "\n", "\\n" ) + + "'"; + } +} diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksType.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksType.java index 6a86011e0..5f5269430 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksType.java +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksType.java @@ -80,7 +80,7 @@ record DatabricksStructEntry( String name, DatabricksType type, boolean nullable @Override public String toString() { return name + ": " + type + ( nullable ? "" : " NOT NULL" ) - + comment.map( c -> " COMMENT '%s'".formatted( c.replace( "'", "\\'" ) ) ).orElse( "" ); + + comment.map( theComment -> " " + new DatabricksCommentDefinition( theComment ) ).orElse( "" ); } } diff --git a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksColumnDefinitionParserTest.java b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksColumnDefinitionParserTest.java index bfdfe8fcf..68501ce67 100644 --- a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksColumnDefinitionParserTest.java +++ b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksColumnDefinitionParserTest.java @@ -32,9 +32,7 @@ void testMinimalDefinition() { } @ParameterizedTest - @EnumSource( value = TestAspect.class, mode = EnumSource.Mode.EXCLUDE, names = { - "ASPECT_WITH_MARKDOWN_DESCRIPTION" - } ) + @EnumSource( value = TestAspect.class ) void testParseSqlForAspectModel( final TestAspect testAspect ) { final String sql = sql( testAspect ); final String parsedAndSerializedSql = sql.lines() From a7f3a715d3c3b6ad764eeef511917da54d9c7f97 Mon Sep 17 00:00:00 2001 From: "Textor Andreas (BCI/ESW17)" Date: Mon, 19 May 2025 12:46:21 +0200 Subject: [PATCH 18/18] Make line break escaping work on Windows --- .../generator/sql/databricks/DatabricksCommentDefinition.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksCommentDefinition.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksCommentDefinition.java index e2d797b07..5c557a937 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksCommentDefinition.java +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksCommentDefinition.java @@ -18,7 +18,7 @@ public record DatabricksCommentDefinition( String comment ) { public String toString() { return "COMMENT '" + comment .replace( "'", "\\'" ) - .replace( "\n", "\\n" ) + .replace( System.lineSeparator(), "\\n" ) + "'"; } }