Skip to content

Add support for markdown formatting in descriptions #759

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -140,14 +139,13 @@ private static String renderSpecialBlock( final String type, final List<String>
* @return A map of special block types to their associated content.
*/
private static Map<String, List<String>> collectSpecialBlocks( final String[] lines, final StringBuilder markdownBuffer ) {
Pattern pattern = Pattern.compile( "^>\\s*(NOTE|EXAMPLE|SOURCE)(\\s+\\d+)?:\\s*(.*)", Pattern.CASE_INSENSITIVE );
Map<String, List<String>> 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();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,95 +1,135 @@
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;

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 testExtractNotes_singleNote() {
final String description = "> NOTE: This is a note.\n> Continued on the next line.";
final List<String> notes = DescriptionsUtils.notes( description );
assertEquals( 1, notes.size() );
assertEquals( "This is a note.\nContinued on the next line.", notes.get( 0 ) );
void testExtractNotesSingleNote() {
final List<String> notes = DescriptionsUtils.notes( testDescription );
assertThat( notes ).hasSize( 1 );
assertEquals( "This is a note block.\nIt supports multiple lines.\nHere's a second line of the note.", notes.get( 0 ) );
}

@Test
void testExtractExamples_multipleExamples() {
final String description =
"""
> EXAMPLE 1: First example.
> More detail.

> EXAMPLE 2: Second example.
""";
final List<String> examples = DescriptionsUtils.examples( description );
void testExtractExamplesMultipleExamples() {
final List<String> 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 <em>italic</em> and <strong>bold</strong> text." );
}

@Test
void testExtractSources_withLink() {
final String description = "> SOURCE: Source with [link](https://example.com)";
final List<String> sources = DescriptionsUtils.sources( description );
void testExtractSourcesWithLink() {
final List<String> sources = DescriptionsUtils.sources( testDescription );
assertEquals( 1, sources.size() );
assertTrue( 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
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() {
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 );

assertTrue( html.contains( "<div class=\"note\">" ) );
assertTrue( html.contains( "This is a note." ) );
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( testDescription );

assertThat( html ).contains( "<div class=\"note\">" );
assertThat( html ).contains( "This is a note block.\nIt supports multiple lines.\nHere's a second line of the note." );
assertTrue( html.contains( "<ul class=\"example-list\">" ) || html.contains( "<div class=\"example\">" ) );
assertTrue( html.contains( "First example." ) );
assertTrue( html.contains( "<div class=\"source\">" ) );
assertTrue( html.contains( "Source information here." ) );
assertTrue( html.contains( "<strong>markdown</strong>" ) );
assertTrue( html.contains( "<ol>" ) );
assertThat( html ).contains(
"This is the first example block.\nIt can span several lines, and supports <em>italic</em> and <strong>bold</strong> text." );
assertThat( html ).contains( "<div class=\"source\">" );
assertThat( html ).contains( "ISO 12345:2023, section 4.2.1\nwith an inline <a href=\"https://www.example.com/spec\">link</a>." );
assertThat( html ).contains( "<ol>" );
}

@Test
void testMarkdownRenderingBulletList() {
String html = DescriptionsUtils.toHtml( testDescription );
assertThat( html ).contains( "<ul>" );
assertThat( html ).contains( "<li>Item A</li>" );
assertThat( html ).contains( "<li>Item B</li>" );
assertThat( html ).contains( "<li>Item C</li>" );
}

@Test
void testMarkdownRenderingOrderedList() {
String html = DescriptionsUtils.toHtml( testDescription );
assertThat( html ).contains( "<ol>" );
assertThat( html ).contains( "<li>First</li>" );
assertThat( html ).contains( "<li>Second</li>" );
assertThat( html ).contains( "<li>Third</li>" );
}

@Test
void testMarkdownRenderingWithLink() {
String html = DescriptionsUtils.toHtml( testDescription );
assertThat( html ).contains( "<a href=\"https://example.com\">Visit Example</a>" );
}

@Test
void testHtmlOutputDoesNotContainMarkdownSyntax() {
String html = DescriptionsUtils.toHtml( testDescription );
assertThat( html ).doesNotContain( "[Visit Example](https://example.com)" );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ void testAspectWithCollectionOfSimpleType() throws Throwable {
void testScriptTagIsEscaped() throws IOException {
assertThat( generateHtmlDocumentation( TestAspect.ASPECT_WITH_SCRIPT_TAGS ) )
.isNotEmpty()
.doesNotContain( "<script>alert('Should not be alerted');</script>" );
.doesNotContain( "Test preferred name with script: <script>alert('Should not be alerted');</script>" );
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 : <urn:samm:org.eclipse.esmf.test:1.0.0#> .
@prefix samm: <urn:samm:org.eclipse.esmf.samm:meta-model:2.2.0#> .
@prefix samm-c: <urn:samm:org.eclipse.esmf.samm:characteristic:2.2.0#> .

: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 .
Loading