metanorma-plugin-lutaml 0.4.2 → 0.4.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.adoc +312 -10
- data/lib/metanorma-plugin-lutaml.rb +3 -0
- data/lib/metanorma/plugin/lutaml/liquid/multiply_local_file_system.rb +53 -0
- data/lib/metanorma/plugin/lutaml/liquid_templates/_diagrams_block.liquid +11 -0
- data/lib/metanorma/plugin/lutaml/liquid_templates/_packages.liquid +133 -0
- data/lib/metanorma/plugin/lutaml/liquid_templates/_packages_class.liquid +81 -0
- data/lib/metanorma/plugin/lutaml/liquid_templates/_packages_data_dictionary.liquid +177 -0
- data/lib/metanorma/plugin/lutaml/liquid_templates/_packages_data_dictionary_classes.liquid +43 -0
- data/lib/metanorma/plugin/lutaml/liquid_templates/_packages_data_type.liquid +63 -0
- data/lib/metanorma/plugin/lutaml/liquid_templates/_packages_entity_list.liquid +113 -0
- data/lib/metanorma/plugin/lutaml/liquid_templates/_packages_enum.liquid +63 -0
- data/lib/metanorma/plugin/lutaml/lutaml_diagram_base.rb +81 -0
- data/lib/metanorma/plugin/lutaml/lutaml_diagram_block.rb +5 -60
- data/lib/metanorma/plugin/lutaml/lutaml_diagram_block_macro.rb +20 -0
- data/lib/metanorma/plugin/lutaml/lutaml_figure_inline_macro.rb +23 -0
- data/lib/metanorma/plugin/lutaml/lutaml_uml_attributes_table_preprocessor.rb +11 -10
- data/lib/metanorma/plugin/lutaml/lutaml_uml_datamodel_description_preprocessor.rb +188 -0
- data/lib/metanorma/plugin/lutaml/utils.rb +4 -2
- data/lib/metanorma/plugin/lutaml/version.rb +1 -1
- metadata +15 -2
@@ -0,0 +1,113 @@
|
|
1
|
+
{% for package in context.packages %}
|
2
|
+
{% assign package_name = package.name | downcase | replace: ":", "" | replace: " ", "_" %}
|
3
|
+
{% if additional_context.before and additional_context.before.size > 0 %}
|
4
|
+
{% for before in additional_context.before %}
|
5
|
+
{{ before.text }}
|
6
|
+
{% endfor %}
|
7
|
+
{% endif %}
|
8
|
+
{% capture equalsigns %}{% for count in (1..depth) %}={% endfor %}{% endcapture %}{{equalsigns}} {{ package.name }}
|
9
|
+
[[rc_{{ package_name }}-model_section]]
|
10
|
+
{{equalsigns}}= {{ package.name }}
|
11
|
+
|
12
|
+
{% assign before_package_key = 'before;' | append: package.name %}
|
13
|
+
{% if additional_context[before_package_key] and additional_context[before_package_key].size > 0 %}
|
14
|
+
{% for before in additional_context[before_package_key] %}
|
15
|
+
{{ before.text }}
|
16
|
+
{% endfor %}
|
17
|
+
{% endif %}
|
18
|
+
{% if additional_context.diagram_include_block %}
|
19
|
+
{% for diagram_include_block in additional_context.diagram_include_block %}
|
20
|
+
{% include "diagrams_block", package_name: package_name, image_base_path: diagram_include_block.base_path, text: diagram_include_block.text %}
|
21
|
+
{% endfor %}
|
22
|
+
{% endif %}
|
23
|
+
|
24
|
+
{% if additional_context.include_block and additional_context.include_block.size > 0 %}
|
25
|
+
{% for block in additional_context.include_block %}
|
26
|
+
{% capture block_filename %}{{ block.base_path }}{{ package_name }}{% endcapture %}
|
27
|
+
{% capture block_content %}{% include block_filename %}{% endcapture %}
|
28
|
+
{% unless block_content contains "Liquid error" %}
|
29
|
+
{% if block.text %}
|
30
|
+
{{ block.text }}
|
31
|
+
{% endif %}
|
32
|
+
{{ block_content }}
|
33
|
+
{% endunless %}
|
34
|
+
{% endfor %}
|
35
|
+
{% endif %}
|
36
|
+
|
37
|
+
{% assign include_block_package_key = 'include_block;' | append: package.name %}
|
38
|
+
{% if additional_context[include_block_package_key] and additional_context[include_block_package_key].size > 0 %}
|
39
|
+
{% for block in additional_context[include_block_package_key] %}
|
40
|
+
{% capture block_filename %}{{ block.base_path }}{{ package_name }}{% endcapture %}
|
41
|
+
{% capture block_content %}{% include block_filename %}{% endcapture %}
|
42
|
+
{% unless block_content contains "Liquid error" %}
|
43
|
+
{% if block.text %}
|
44
|
+
{{ block.text }}
|
45
|
+
{% endif %}
|
46
|
+
{{ block_content }}
|
47
|
+
{% endunless %}
|
48
|
+
{% endfor %}
|
49
|
+
{% endif %}
|
50
|
+
|
51
|
+
{{equalsigns}}= Class Definitions
|
52
|
+
{% if package.classes.size > 0 %}
|
53
|
+
.Classes used in {{ package.name }}
|
54
|
+
[cols="2a,6a",options="header"]
|
55
|
+
|===
|
56
|
+
|Class |Description
|
57
|
+
|
58
|
+
{% for klass in package.classes %}
|
59
|
+
|<<{{ klass.name }}-section,{{ klass.name }}>>
|
60
|
+
«{{ klass.stereotype }}»
|
61
|
+
|{{ klass.definition }}
|
62
|
+
|
63
|
+
{% endfor %}
|
64
|
+
|===
|
65
|
+
{% endif %}
|
66
|
+
|
67
|
+
{% if package.data_types.size > 0 %}
|
68
|
+
.Data Types used in {{ package.name }}
|
69
|
+
[cols="2,6",options="header"]
|
70
|
+
|===
|
71
|
+
|Name |Description
|
72
|
+
|
73
|
+
{% for klass in package.data_types %}
|
74
|
+
|<<{{ klass.name }}-section,{{ klass.name }}>>
|
75
|
+
|{{ klass.definition }}
|
76
|
+
|
77
|
+
{% endfor %}
|
78
|
+
|
79
|
+
|===
|
80
|
+
{% endif %}
|
81
|
+
|
82
|
+
{% if package.enums.size > 0 %}
|
83
|
+
.Enumerated Classes used in {{ package.name }}
|
84
|
+
[cols="2a,6a",options="header"]
|
85
|
+
|===
|
86
|
+
|Name |Description
|
87
|
+
|
88
|
+
{% for klass in package.enums %}
|
89
|
+
|<<{{ klass.name }}-section,{{ klass.name }}>>
|
90
|
+
|{{ klass.definition }}
|
91
|
+
|
92
|
+
{% endfor %}
|
93
|
+
|
94
|
+
|===
|
95
|
+
{% endif %}
|
96
|
+
|
97
|
+
{% assign after_package_key = 'after;' | append: package.name %}
|
98
|
+
{% if additional_context[after_package_key] %}
|
99
|
+
{{equalsigns}}= Additional Information
|
100
|
+
{% for after in additional_context[after_package_key] %}
|
101
|
+
{{ after.text }}
|
102
|
+
{% endfor %}
|
103
|
+
{% endif %}
|
104
|
+
{% if package.packages.size > 0 and render_nested_packages %}
|
105
|
+
{% assign nested_depth = depth | plus: 1 %}{% include "packages_entity_list", depth: nested_depth, context: package %}
|
106
|
+
{% endif %}
|
107
|
+
{% endfor %}
|
108
|
+
|
109
|
+
{% if additional_context.after and additional_context.after.size > 0 %}
|
110
|
+
{% for after in additional_context.after %}
|
111
|
+
{{ after.text }}
|
112
|
+
{% endfor %}
|
113
|
+
{% endif %}
|
@@ -0,0 +1,63 @@
|
|
1
|
+
{% assign is_klass_spare = klass.name | slice: 0,5 %}
|
2
|
+
{% if is_klass_spare == 'old: ' %}{% continue %}
|
3
|
+
{% elsif is_klass_spare == 'Spare' %}{% continue %}
|
4
|
+
{% endif %}
|
5
|
+
{% assign klass_name = klass.name | downcase | replace: ':', '' | replace: ' ', '_' %}
|
6
|
+
[[tab-P-{{ package_name }}-E-{{ klass_name }}]]
|
7
|
+
.Elements of {{ package.name }}::{{ klass.name }}
|
8
|
+
[width="100%",cols="a,a,a,a,a,a,a,a"]
|
9
|
+
|===
|
10
|
+
|
11
|
+
h|Name: 7+| {{ klass.name }}
|
12
|
+
|
13
|
+
h|Definition: 7+| {{ klass.definition | html2adoc }}
|
14
|
+
|
15
|
+
h|Stereotype: 7+| {{ klass.stereotype | default: 'interface' }}
|
16
|
+
|
17
|
+
{% assign inherited = klass.associations | where: "member_end_type", "inheritance" %}
|
18
|
+
{% if inherited.size > 0 %}
|
19
|
+
h|Inheritance from: 7+| {{ inherited | map: 'member_end' | join: ", " }}
|
20
|
+
{% endif %}
|
21
|
+
|
22
|
+
{% assign generalizations = klass.associations | where: "member_end_type", "generalization" %}
|
23
|
+
{% if generalizations.size > 0 %}
|
24
|
+
h|Generalization of: 7+| {{ generalizations | map: 'member_end' | join: ", " }}
|
25
|
+
{% endif %}
|
26
|
+
|
27
|
+
h|Abstract: 7+| {{ klass.is_abstract }}
|
28
|
+
{% assign aggregations = klass.associations | where: "member_end_type", "aggregation" %}
|
29
|
+
{% if aggregations.size > 0 %}
|
30
|
+
.{{aggregations.size | plus: 1}}+h|Associations:
|
31
|
+
4+|_Association with:_
|
32
|
+
|_Obligation_
|
33
|
+
| _Maximum occurrence_
|
34
|
+
|_Provides:_
|
35
|
+
|
36
|
+
{% for assoc in aggregations %}
|
37
|
+
4+| {{assoc.member_end}}
|
38
|
+
| {% if assoc.member_end_cardinality %}{{ assoc.member_end_cardinality.min }}{% endif %}
|
39
|
+
| {% if assoc.member_end_cardinality %}{{ assoc.member_end_cardinality.max }}{% endif %}
|
40
|
+
| {{ assoc.member_end_attribute_name }}
|
41
|
+
|
42
|
+
{% endfor %}
|
43
|
+
{% else %}
|
44
|
+
|
45
|
+
.1+h|Associations: 7+| (none)
|
46
|
+
{% endif %}
|
47
|
+
{% if klass.values.size > 0 %}
|
48
|
+
.{{klass.values.size | plus: 1}}+h|Values:
|
49
|
+
| _Name_
|
50
|
+
6+| _Definition_
|
51
|
+
|
52
|
+
{% for value in klass.values %}
|
53
|
+
| {{value.name}}
|
54
|
+
6+| {{ value.definition | html2adoc }}
|
55
|
+
|
56
|
+
{% endfor %}
|
57
|
+
{% else %}
|
58
|
+
.1+h|Values:
|
59
|
+
7+| (none)
|
60
|
+
{% endif %}
|
61
|
+
|
62
|
+
|
63
|
+
|===
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "liquid"
|
4
|
+
require "asciidoctor"
|
5
|
+
require "asciidoctor/reader"
|
6
|
+
require "lutaml"
|
7
|
+
require "lutaml/uml"
|
8
|
+
require "lutaml/formatter"
|
9
|
+
require "metanorma/plugin/lutaml/utils"
|
10
|
+
|
11
|
+
module Metanorma
|
12
|
+
module Plugin
|
13
|
+
module Lutaml
|
14
|
+
module LutamlDiagramBase
|
15
|
+
def process(parent, reader, attrs)
|
16
|
+
uml_document = ::Lutaml::Uml::Parsers::Dsl.parse(lutaml_file(parent.document, reader))
|
17
|
+
filename = generate_file(parent, reader, uml_document)
|
18
|
+
through_attrs = generate_attrs(attrs)
|
19
|
+
through_attrs["target"] = filename
|
20
|
+
through_attrs["title"] = uml_document.caption
|
21
|
+
create_image_block(parent, through_attrs)
|
22
|
+
rescue StandardError => e
|
23
|
+
abort(parent, reader, attrs, e.message)
|
24
|
+
end
|
25
|
+
|
26
|
+
def lutaml_file(reader)
|
27
|
+
raise 'Implement me!'
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def abort(parent, reader, attrs, msg)
|
33
|
+
warn(msg)
|
34
|
+
attrs["language"] = "lutaml"
|
35
|
+
source = reader.respond_to?(:source) ? reader.source : reader
|
36
|
+
create_listing_block(
|
37
|
+
parent,
|
38
|
+
source,
|
39
|
+
attrs.reject { |k, _v| k == 1 }
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
# if no :imagesdir: leave image file in lutaml
|
44
|
+
def generate_file(parent, _reader, uml_document)
|
45
|
+
formatter = ::Lutaml::Formatter::Graphviz.new
|
46
|
+
formatter.type = :png
|
47
|
+
|
48
|
+
imagesdir = if parent.document.attr("imagesdir")
|
49
|
+
File.join(parent.document.attr("imagesdir"), "lutaml")
|
50
|
+
else
|
51
|
+
"lutaml"
|
52
|
+
end
|
53
|
+
result_path = Utils.relative_file_path(parent.document, imagesdir)
|
54
|
+
result_pathname = Pathname.new(result_path)
|
55
|
+
result_pathname.mkpath
|
56
|
+
File.writable?(result_pathname) || raise("Destination path #{result_path} not writable for Lutaml!")
|
57
|
+
|
58
|
+
outfile = Tempfile.new(["lutaml", ".png"])
|
59
|
+
outfile.binmode
|
60
|
+
outfile.puts(formatter.format(uml_document))
|
61
|
+
|
62
|
+
# Warning: metanorma/metanorma-standoc#187
|
63
|
+
# Windows Ruby 2.4 will crash if a Tempfile is "mv"ed.
|
64
|
+
# This is why we need to copy and then unlink.
|
65
|
+
filename = File.basename(outfile.path)
|
66
|
+
FileUtils.cp(outfile, result_pathname) && outfile.unlink
|
67
|
+
|
68
|
+
File.join(result_pathname, filename)
|
69
|
+
end
|
70
|
+
|
71
|
+
def generate_attrs(attrs)
|
72
|
+
%w(id align float title role width height alt)
|
73
|
+
.reduce({}) do |memo, key|
|
74
|
+
memo[key] = attrs[key] if attrs.has_key? key
|
75
|
+
memo
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -1,40 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
4
|
-
require "asciidoctor"
|
5
|
-
require "asciidoctor/reader"
|
6
|
-
require "lutaml"
|
7
|
-
require "lutaml/uml"
|
8
|
-
require "metanorma/plugin/lutaml/utils"
|
3
|
+
require "metanorma/plugin/lutaml/lutaml_diagram_base"
|
9
4
|
|
10
5
|
module Metanorma
|
11
6
|
module Plugin
|
12
7
|
module Lutaml
|
13
8
|
class LutamlDiagramBlock < Asciidoctor::Extensions::BlockProcessor
|
9
|
+
include LutamlDiagramBase
|
10
|
+
|
14
11
|
use_dsl
|
15
12
|
named :lutaml_diagram
|
16
13
|
on_context :literal
|
17
14
|
parse_content_as :raw
|
18
15
|
|
19
|
-
def
|
20
|
-
warn(msg)
|
21
|
-
attrs["language"] = "lutaml"
|
22
|
-
create_listing_block(
|
23
|
-
parent,
|
24
|
-
reader.source,
|
25
|
-
attrs.reject { |k, _v| k == 1 }
|
26
|
-
)
|
27
|
-
end
|
16
|
+
def lutaml_file(document, reader)
|
28
17
|
|
29
|
-
|
30
|
-
uml_document = ::Lutaml::Uml::Parsers::Dsl.parse(lutaml_temp(parent.document, reader))
|
31
|
-
filename = generate_file(parent, reader, uml_document)
|
32
|
-
through_attrs = generate_attrs(attrs)
|
33
|
-
through_attrs["target"] = filename
|
34
|
-
through_attrs["title"] = uml_document.caption
|
35
|
-
create_image_block(parent, through_attrs)
|
36
|
-
rescue StandardError => e
|
37
|
-
abort(parent, reader, attrs, e.message)
|
18
|
+
lutaml_temp(document, reader)
|
38
19
|
end
|
39
20
|
|
40
21
|
private
|
@@ -45,42 +26,6 @@ module Metanorma
|
|
45
26
|
temp_file.rewind
|
46
27
|
temp_file
|
47
28
|
end
|
48
|
-
|
49
|
-
# if no :imagesdir: leave image file in lutaml
|
50
|
-
def generate_file(parent, _reader, uml_document)
|
51
|
-
formatter = ::Lutaml::Uml::Formatter::Graphviz.new
|
52
|
-
formatter.type = :png
|
53
|
-
|
54
|
-
imagesdir = if parent.document.attr("imagesdir")
|
55
|
-
File.join(parent.document.attr("imagesdir"), "lutaml")
|
56
|
-
else
|
57
|
-
"lutaml"
|
58
|
-
end
|
59
|
-
result_path = Utils.relative_file_path(parent.document, imagesdir)
|
60
|
-
result_pathname = Pathname.new(result_path)
|
61
|
-
result_pathname.mkpath
|
62
|
-
File.writable?(result_pathname) || raise("Destination path #{result_path} not writable for Lutaml!")
|
63
|
-
|
64
|
-
outfile = Tempfile.new(["lutaml", ".png"])
|
65
|
-
outfile.binmode
|
66
|
-
outfile.puts(formatter.format(uml_document))
|
67
|
-
|
68
|
-
# Warning: metanorma/metanorma-standoc#187
|
69
|
-
# Windows Ruby 2.4 will crash if a Tempfile is "mv"ed.
|
70
|
-
# This is why we need to copy and then unlink.
|
71
|
-
filename = File.basename(outfile.path)
|
72
|
-
FileUtils.cp(outfile, result_pathname) && outfile.unlink
|
73
|
-
|
74
|
-
File.join(result_pathname, filename)
|
75
|
-
end
|
76
|
-
|
77
|
-
def generate_attrs(attrs)
|
78
|
-
%w(id align float title role width height alt)
|
79
|
-
.reduce({}) do |memo, key|
|
80
|
-
memo[key] = attrs[key] if attrs.has_key? key
|
81
|
-
memo
|
82
|
-
end
|
83
|
-
end
|
84
29
|
end
|
85
30
|
end
|
86
31
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "metanorma/plugin/lutaml/lutaml_diagram_base"
|
4
|
+
|
5
|
+
module Metanorma
|
6
|
+
module Plugin
|
7
|
+
module Lutaml
|
8
|
+
class LutamlDiagramBlockMacro < Asciidoctor::Extensions::BlockMacroProcessor
|
9
|
+
include LutamlDiagramBase
|
10
|
+
|
11
|
+
use_dsl
|
12
|
+
named :lutaml_diagram
|
13
|
+
|
14
|
+
def lutaml_file(document, file_path)
|
15
|
+
File.new(Utils.relative_file_path(document, file_path))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Metanorma
|
4
|
+
module Plugin
|
5
|
+
module Lutaml
|
6
|
+
class LutamlFigureInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
|
7
|
+
include LutamlDiagramBase
|
8
|
+
|
9
|
+
use_dsl
|
10
|
+
named :lutaml_figure
|
11
|
+
|
12
|
+
def process(parent, _target, attrs)
|
13
|
+
diagram_key = [attrs["package"], attrs["name"]].compact.join(":")
|
14
|
+
return if parent.document.attributes['lutaml_figure_id'].nil?
|
15
|
+
xmi_id = parent.document.attributes['lutaml_figure_id'][diagram_key]
|
16
|
+
return unless xmi_id
|
17
|
+
|
18
|
+
%Q(<xref target="figure-#{xmi_id}"></xref>)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -14,7 +14,7 @@ module Metanorma
|
|
14
14
|
# @example [lutaml_uml_attributes_table,path/to/lutaml,EntityName]
|
15
15
|
class LutamlUmlAttributesTablePreprocessor < Asciidoctor::Extensions::Preprocessor
|
16
16
|
MARCO_REGEXP =
|
17
|
-
/\[lutaml_uml_attributes_table,([^,]+),?(
|
17
|
+
/\[lutaml_uml_attributes_table,([^,]+),?([^,]+),?(.+?)?\]/
|
18
18
|
# search document for block `datamodel_attributes_table`
|
19
19
|
# read include derectives that goes after that in block and transform
|
20
20
|
# into yaml2text blocks
|
@@ -36,15 +36,16 @@ module Metanorma
|
|
36
36
|
input_lines.each_with_object([]) do |line, result|
|
37
37
|
if match = line.match(MARCO_REGEXP)
|
38
38
|
lutaml_path = match[1]
|
39
|
-
entity_name = match[
|
40
|
-
|
39
|
+
entity_name = match[2]
|
40
|
+
skip_headers = match[3]
|
41
|
+
result.push(*parse_marco(lutaml_path, entity_name, document, skip_headers))
|
41
42
|
else
|
42
43
|
result.push(line)
|
43
44
|
end
|
44
45
|
end
|
45
46
|
end
|
46
47
|
|
47
|
-
def parse_marco(lutaml_path, entity_name, document)
|
48
|
+
def parse_marco(lutaml_path, entity_name, document, skip_headers)
|
48
49
|
lutaml_document = lutaml_document_from_file(document, lutaml_path)
|
49
50
|
.serialized_document
|
50
51
|
entities = [lutaml_document["classes"], lutaml_document["enums"]]
|
@@ -53,12 +54,12 @@ module Metanorma
|
|
53
54
|
entity_definition = entities.detect do |klass|
|
54
55
|
klass["name"] == entity_name.strip
|
55
56
|
end
|
56
|
-
model_representation(entity_definition, document)
|
57
|
+
model_representation(entity_definition, document, skip_headers)
|
57
58
|
end
|
58
59
|
|
59
|
-
def model_representation(entity_definition, document)
|
60
|
+
def model_representation(entity_definition, document, skip_headers)
|
60
61
|
render_result, errors = Utils.render_liquid_string(
|
61
|
-
template_string: table_template,
|
62
|
+
template_string: table_template(skip_headers),
|
62
63
|
context_items: entity_definition,
|
63
64
|
context_name: "definition",
|
64
65
|
document: document
|
@@ -68,9 +69,9 @@ module Metanorma
|
|
68
69
|
end
|
69
70
|
|
70
71
|
# rubocop:disable Layout/IndentHeredoc
|
71
|
-
def table_template
|
72
|
+
def table_template(skip_headers)
|
72
73
|
<<~TEMPLATE
|
73
|
-
=== {{ definition.name }}
|
74
|
+
#{"=== {{ definition.name }}" unless skip_headers}
|
74
75
|
{{ definition.definition }}
|
75
76
|
|
76
77
|
{% if definition.attributes %}
|
@@ -89,7 +90,7 @@ module Metanorma
|
|
89
90
|
|Name |Definition |Mandatory/ Optional/ Conditional |Max Occur |Data Type
|
90
91
|
|
91
92
|
{% for item in definition.attributes %}
|
92
|
-
|{{ item.name }} |{% if item.definition %}{{ item.definition }}{%
|
93
|
+
|{{ item.name }} |{% if item.definition %}{{ item.definition }}{% endif %} |{% if item.cardinality.min == "0" %}O{% else %}M{% endif %} |{% if item.cardinality.max == "*" %}N{% else %}1{% endif %} |{% if item.origin %}<<{{ item.origin }}>>{% endif %} `{{ item.type }}`
|
93
94
|
{% endfor %}
|
94
95
|
|===
|
95
96
|
{% endif %}
|