Skip to content
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

Generate nested tables for struct attributes #43

Merged
merged 3 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
patrickackermann marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
Expand All @@ -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 = """
Expand All @@ -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]
Expand Down Expand Up @@ -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 | (<b>topValue1</b>, <b>topValue2</b> (subValue1, subValue2, subValue3 (<i>subSubValue1</i>, <i>subSubValue2</i>)), <b>topValue3</b>) |


""";

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 =
"<table>" +
"<thead>" +
"<tr><th>Attributname</th><th>Kardinalität</th><th>Typ</th></tr>" +
"</thead>" +
"<tbody>" +
"<tr><td>attr1</td><td>0..1</td><td>Text [10]</td></tr>" +
"<tr><td>attr2</td><td>1</td><td>(<b>value1</b>, <b>value2</b>)</td></tr>" +
"</tbody>" +
"</table>";

var expected = $"""
# TestModel
## TestTopic
### TestClass
| Attributname | Kardinalität | Typ |
| --- | --- | --- |
| attr1 | 1 | TestStruct<br/>{structInlineTable} |
| attr2 | 0..1 | 10..20 |

### TestStruct
| Attributname | Kardinalität | Typ |
| --- | --- | --- |
| attr1 | 0..1 | Text [10] |
| attr2 | 1 | (<b>value1</b>, <b>value2</b>) |


""";

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 =
"<table>" +
"<thead>" +
"<tr><th>Attributname</th><th>Kardinalität</th><th>Typ</th></tr>" +
"</thead>" +
"<tbody>" +
"<tr><td>attr1</td><td>1</td><td>Text [50]</td></tr>" +
"<tr><td>attr2</td><td>0..1</td><td>0..30</td></tr>" +
"</tbody>" +
"</table>";

const string structLevel1InlineTable =
"<table>" +
"<thead>" +
"<tr><th>Attributname</th><th>Kardinalität</th><th>Typ</th></tr>" +
"</thead>" +
"<tbody>" +
"<tr><td>attr1</td><td>0..1</td><td>TestStructLevel2<br/>" + structLevel2InlineTable + "</td></tr>" +
"<tr><td>attr2</td><td>1</td><td>(<b>value1</b>, <b>value2</b>)</td></tr>" +
"</tbody>" +
"</table>";

var expected = $"""
# TestModel
## TestTopic
### TestStructLevel1
| Attributname | Kardinalität | Typ |
| --- | --- | --- |
| attr1 | 0..1 | TestStructLevel2<br/>{structLevel2InlineTable} |
| attr2 | 1 | (<b>value1</b>, <b>value2</b>) |

### TestStructLevel2
| Attributname | Kardinalität | Typ |
| --- | --- | --- |
| attr1 | 1 | Text [50] |
| attr2 | 0..1 | 0..30 |

### TestClass
| Attributname | Kardinalität | Typ |
| --- | --- | --- |
| attr1 | 1 | TestStructLevel1<br/>{structLevel1InlineTable} |
| attr2 | 0..1 | 10..20 |


""";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Geowerkstatt.Interlis.LanguageServer.Visitors;
public class MarkdownDocumentationVisitor : Interlis24AstBaseVisitor<object>
{
private readonly StringBuilder documentation = new StringBuilder();
private bool useHtml;

/// <summary>
/// Generates markdown documentation for the given model.
Expand Down Expand Up @@ -39,14 +40,30 @@ public class MarkdownDocumentationVisitor : Interlis24AstBaseVisitor<object>
/// <param name="modelDef">The INTERLIS class.</param>
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("<table>");
documentation.Append("<thead><tr><th>Attributname</th><th>Kardinalität</th><th>Typ</th></tr></thead>");
documentation.Append("<tbody>");
VisitTableBody();
documentation.Append("</tbody></table>");
}
else
{
documentation.AppendLine($"### {classDef.Name}");
documentation.AppendLine("| Attributname | Kardinalität | Typ |");
documentation.AppendLine("| --- | --- | --- |");
VisitTableBody();
documentation.AppendLine();
}

return null;
}

private void VisitRelatedAssociations(ClassDef classDef)
Expand Down Expand Up @@ -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($"<tr><td>{attributeDef.Name}</td><td>{cardinality}</td><td>");
VisitTypeName(attributeDef.TypeDef);
documentation.Append("</td></tr>");
}
else
{
documentation.Append($"| {attributeDef.Name} | {cardinality} | ");
VisitTypeName(attributeDef.TypeDef);
documentation.AppendLine(" |");
}

return base.VisitAttributeDef(attributeDef);
}

Expand All @@ -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",
Expand All @@ -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 => ("<b>", "</b>"),
1 => ("", ""),
_ => ("<i>", "</i>"),
};
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)})";
}

/// <summary>
/// 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.
/// </summary>
/// <param name="referenceType">The referenced type.</param>
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("<br/>");

var didUseHtml = useHtml;
useHtml = true;
VisitClassDef(classDef);
useHtml = didUseHtml;
}
}

/// <summary>
/// Returns the generated markdown documentation of the visited elements.
/// </summary>
Expand Down