diff --git a/language-server/src/Geowerkstatt.Interlis.LanguageServer.Test/Visitors/MarkdownDocumentationVisitorTest.cs b/language-server/src/Geowerkstatt.Interlis.LanguageServer.Test/Visitors/MarkdownDocumentationVisitorTest.cs index 66deaeb..7415f9d 100644 --- a/language-server/src/Geowerkstatt.Interlis.LanguageServer.Test/Visitors/MarkdownDocumentationVisitorTest.cs +++ b/language-server/src/Geowerkstatt.Interlis.LanguageServer.Test/Visitors/MarkdownDocumentationVisitorTest.cs @@ -15,7 +15,7 @@ MODEL TestModel (de) AT "http://models.geow.cloud" VERSION "1" = attr2: MANDATORY BOOLEAN; END TestClass; END TestTopic; - END TestModel; + END TestModel. """; private const string TestModelAssociation = """ @@ -36,7 +36,7 @@ MODEL TestModel (de) AT "http://models.geow.cloud" VERSION "1" = AssocB -<> {1} ClassB; END Assoc1; END TestTopic; - END TestModel; + END TestModel. """; private const string TestModelEnumeration = """ @@ -52,7 +52,48 @@ MODEL TestModel (de) AT "http://models.geow.cloud" VERSION "1" = ); END TestClass; END TestTopic; - END TestModel; + END TestModel. + """; + + private const string TestModelNestedStruct = """ + INTERLIS 2.4; + + MODEL TestModel (de) AT "http://models.geow.cloud" VERSION "1" = + TOPIC TestTopic = + CLASS TestClass = + attr1: MANDATORY TestStruct; + attr2: 10..20; + END TestClass; + + STRUCTURE TestStruct = + attr1: TEXT*10; + attr2: MANDATORY (value1, value2); + END TestStruct; + END TestTopic; + END TestModel. + """; + + private const string TestModelDoubleNestedStruct = """ + INTERLIS 2.4; + + MODEL TestModel (de) AT "http://models.geow.cloud" VERSION "1" = + TOPIC TestTopic = + STRUCTURE TestStructLevel1 = + attr1: TestStructLevel2; + attr2: MANDATORY (value1, value2); + END TestStructLevel1; + + STRUCTURE TestStructLevel2 = + attr1: MANDATORY TEXT*50; + attr2: 0..30; + END TestStructLevel2; + + CLASS TestClass = + attr1: MANDATORY TestStructLevel1; + attr2: 10..20; + END TestClass; + END TestTopic; + END TestModel. """; [TestMethod] @@ -127,7 +168,108 @@ public void TestInterlisFileEnumeration() ### TestClass | Attributname | Kardinalität | Typ | | --- | --- | --- | - | attr1 | 0..1 | (**topValue1**, **topValue2** (subValue1, subValue2, subValue3 (*subSubValue1*, *subSubValue2*)), **topValue3**) | + | attr1 | 0..1 | (topValue1, topValue2 (subValue1, subValue2, subValue3 (subSubValue1, subSubValue2)), topValue3) | + + + """; + + Assert.AreEqual(expected.ReplaceLineEndings(), documentation.ReplaceLineEndings()); + } + + [TestMethod] + public void TestInterlisFileNestedStruct() + { + var reader = new InterlisReader(); + var interlisFile = reader.ReadFile(new StringReader(TestModelNestedStruct)); + + var visitor = new MarkdownDocumentationVisitor(); + visitor.VisitInterlisFile(interlisFile); + var documentation = visitor.GetDocumentation(); + + const string structInlineTable = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
AttributnameKardinalitätTyp
attr10..1Text [10]
attr21(value1, value2)
"; + + var expected = $""" + # TestModel + ## TestTopic + ### TestClass + | Attributname | Kardinalität | Typ | + | --- | --- | --- | + | attr1 | 1 | TestStruct
{structInlineTable} | + | attr2 | 0..1 | 10..20 | + + ### TestStruct + | Attributname | Kardinalität | Typ | + | --- | --- | --- | + | attr1 | 0..1 | Text [10] | + | attr2 | 1 | (value1, value2) | + + + """; + + Assert.AreEqual(expected.ReplaceLineEndings(), documentation.ReplaceLineEndings()); + } + + [TestMethod] + public void TestInterlisFileDoubleNestedStruct() + { + var reader = new InterlisReader(); + var interlisFile = reader.ReadFile(new StringReader(TestModelDoubleNestedStruct)); + + var visitor = new MarkdownDocumentationVisitor(); + visitor.VisitInterlisFile(interlisFile); + var documentation = visitor.GetDocumentation(); + + const string structLevel2InlineTable = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
AttributnameKardinalitätTyp
attr11Text [50]
attr20..10..30
"; + + const string structLevel1InlineTable = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
AttributnameKardinalitätTyp
attr10..1TestStructLevel2
" + structLevel2InlineTable + "
attr21(value1, value2)
"; + + var expected = $""" + # TestModel + ## TestTopic + ### TestStructLevel1 + | Attributname | Kardinalität | Typ | + | --- | --- | --- | + | attr1 | 0..1 | TestStructLevel2
{structLevel2InlineTable} | + | attr2 | 1 | (value1, value2) | + + ### TestStructLevel2 + | Attributname | Kardinalität | Typ | + | --- | --- | --- | + | attr1 | 1 | Text [50] | + | attr2 | 0..1 | 0..30 | + + ### TestClass + | Attributname | Kardinalität | Typ | + | --- | --- | --- | + | attr1 | 1 | TestStructLevel1
{structLevel1InlineTable} | + | attr2 | 0..1 | 10..20 | """; diff --git a/language-server/src/Geowerkstatt.Interlis.LanguageServer/Visitors/MarkdownDocumentationVisitor.cs b/language-server/src/Geowerkstatt.Interlis.LanguageServer/Visitors/MarkdownDocumentationVisitor.cs index d9b4e0d..f786b88 100644 --- a/language-server/src/Geowerkstatt.Interlis.LanguageServer/Visitors/MarkdownDocumentationVisitor.cs +++ b/language-server/src/Geowerkstatt.Interlis.LanguageServer/Visitors/MarkdownDocumentationVisitor.cs @@ -11,6 +11,7 @@ namespace Geowerkstatt.Interlis.LanguageServer.Visitors; public class MarkdownDocumentationVisitor : Interlis24AstBaseVisitor { private readonly StringBuilder documentation = new StringBuilder(); + private bool useHtml; /// /// Generates markdown documentation for the given model. @@ -39,14 +40,30 @@ public class MarkdownDocumentationVisitor : Interlis24AstBaseVisitor /// The INTERLIS class. public override object? VisitClassDef([NotNull] ClassDef classDef) { - documentation.AppendLine($"### {classDef.Name}"); - documentation.AppendLine("| Attributname | Kardinalität | Typ |"); - documentation.AppendLine("| --- | --- | --- |"); - var result = base.VisitClassDef(classDef); - VisitRelatedAssociations(classDef); - documentation.AppendLine(); - - return result; + void VisitTableBody() + { + base.VisitClassDef(classDef); + VisitRelatedAssociations(classDef); + } + + if (useHtml) + { + documentation.Append(""); + documentation.Append(""); + documentation.Append(""); + VisitTableBody(); + documentation.Append("
AttributnameKardinalitätTyp
"); + } + else + { + documentation.AppendLine($"### {classDef.Name}"); + documentation.AppendLine("| Attributname | Kardinalität | Typ |"); + documentation.AppendLine("| --- | --- | --- |"); + VisitTableBody(); + documentation.AppendLine(); + } + + return null; } private void VisitRelatedAssociations(ClassDef classDef) @@ -83,9 +100,20 @@ private void VisitRelatedAssociation(ClassDef classDef, AttributeDef left, Attri public override object? VisitAttributeDef([NotNull] AttributeDef attributeDef) { var cardinality = CalculateCardinality(attributeDef.TypeDef.Cardinality); - var type = GetTypeName(attributeDef.TypeDef); - documentation.AppendLine($"| {attributeDef.Name} | {cardinality} | {type} |"); + if (useHtml) + { + documentation.Append($"{attributeDef.Name}{cardinality}"); + VisitTypeName(attributeDef.TypeDef); + documentation.Append(""); + } + else + { + documentation.Append($"| {attributeDef.Name} | {cardinality} | "); + VisitTypeName(attributeDef.TypeDef); + documentation.AppendLine(" |"); + } + return base.VisitAttributeDef(attributeDef); } @@ -107,9 +135,15 @@ private static string CalculateCardinality(Cardinality? cardinality) return ""; } - private static string? GetTypeName(TypeDef? type) + private void VisitTypeName(TypeDef? type) { - return type switch + if (type is ReferenceType referenceType) + { + VisitReferenceType(referenceType); + return; + } + + var typeName = type switch { TextType textType => textType.Length == null ? "Text" : $"Text [{textType.Length}]", NumericType numericType => numericType.Min != null && numericType.Max != null ? $"{numericType.Min}..{numericType.Max}" : "Numerisch", @@ -121,28 +155,47 @@ private static string CalculateCardinality(Cardinality? cardinality) _ => "Blackbox", }, EnumerationType enumerationType => FormatEnumerationValues(enumerationType.Values), - ReferenceType referenceType => referenceType.Target.Value?.Path.Last(), TypeRef typeRef => typeRef.Extends?.Path.Last(), RoleType roleType => string.Join(", ", roleType.Targets.Select(target => target.Value?.Path.Last()).Where(target => target is not null)), _ => type?.ToString(), }; + documentation.Append(typeName); } private static string FormatEnumerationValues(EnumerationValuesList enumerationValues, int depth = 0) { - const string bold = "**"; - const string italic = "*"; - - var formatting = depth switch + var (formatStart, formatEnd) = depth switch { - 0 => bold, - 1 => "", - _ => italic, + 0 => ("", ""), + 1 => ("", ""), + _ => ("", ""), }; - var formattedValues = enumerationValues.Select(v => $"{formatting}{v.Name}{formatting}{(v.SubValues.Count == 0 ? "" : " " + FormatEnumerationValues(v.SubValues, depth + 1))}"); + var formattedValues = enumerationValues.Select(v => $"{formatStart}{v.Name}{formatEnd}{(v.SubValues.Count == 0 ? "" : " " + FormatEnumerationValues(v.SubValues, depth + 1))}"); return $"({string.Join(", ", formattedValues)})"; } + /// + /// Appends the name of the referenced type to the documentation. + /// If the referenced type is a structure, its attributes and associations are also documented using an HTML table. + /// + /// The referenced type. + private void VisitReferenceType(ReferenceType referenceType) + { + var reference = referenceType.Target.Value; + var typeName = reference?.Path.Last(); + documentation.Append(typeName); + + if (reference?.Target is ClassDef classDef && classDef.IsStructure) + { + documentation.Append("
"); + + var didUseHtml = useHtml; + useHtml = true; + VisitClassDef(classDef); + useHtml = didUseHtml; + } + } + /// /// Returns the generated markdown documentation of the visited elements. ///