lutaml-lml 0.1.8 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/lutaml/lml/cli.rb +20 -4
  3. data/lib/lutaml/lml/data_processor/attribute_processing.rb +49 -9
  4. data/lib/lutaml/lml/data_processor/value_declaration_processing.rb +62 -0
  5. data/lib/lutaml/lml/data_processor.rb +3 -0
  6. data/lib/lutaml/lml/document_builder.rb +230 -6
  7. data/lib/lutaml/lml/format/adapter/standard_adapter.rb +17 -4
  8. data/lib/lutaml/lml/formatter/elk.rb +42 -0
  9. data/lib/lutaml/lml/formatter/graphviz/node_formatter.rb +2 -1
  10. data/lib/lutaml/lml/formatter.rb +1 -0
  11. data/lib/lutaml/lml/grammar/lml.artifact.json +8659 -3193
  12. data/lib/lutaml/lml/grammar/lml.parg +236 -90
  13. data/lib/lutaml/lml/layout/elk_engine.rb +15 -0
  14. data/lib/lutaml/lml/layout/elk_graph_builder.rb +129 -0
  15. data/lib/lutaml/lml/layout/elk_svg_markers.rb +45 -0
  16. data/lib/lutaml/lml/layout/elk_svg_renderer.rb +119 -0
  17. data/lib/lutaml/lml/layout/graph_viz_engine.rb +1 -1
  18. data/lib/lutaml/lml/layout.rb +4 -0
  19. data/lib/lutaml/lml/model_compiler.rb +231 -19
  20. data/lib/lutaml/lml/models/data_type.rb +1 -0
  21. data/lib/lutaml/lml/models/document.rb +31 -0
  22. data/lib/lutaml/lml/models/enum.rb +2 -0
  23. data/lib/lutaml/lml/models/instance.rb +1 -0
  24. data/lib/lutaml/lml/models/instance_collection.rb +1 -0
  25. data/lib/lutaml/lml/models/mapping_rule.rb +16 -0
  26. data/lib/lutaml/lml/models/namespace.rb +14 -0
  27. data/lib/lutaml/lml/models/operation_parameter.rb +1 -0
  28. data/lib/lutaml/lml/models/reference.rb +23 -0
  29. data/lib/lutaml/lml/models/serialization_mapping.rb +17 -0
  30. data/lib/lutaml/lml/models/top_element_attribute.rb +16 -0
  31. data/lib/lutaml/lml/models/uml_class.rb +2 -1
  32. data/lib/lutaml/lml/preprocessor.rb +49 -8
  33. data/lib/lutaml/lml/transform.rb +1 -1
  34. data/lib/lutaml/lml/validator.rb +344 -0
  35. data/lib/lutaml/lml/version.rb +1 -1
  36. data/lib/lutaml/lml.rb +5 -0
  37. metadata +15 -4
@@ -10,180 +10,326 @@
10
10
  # `[ x as k ]` yields k: nil when absent (Parslet omits the key —
11
11
  # consumers treat nil and absent as equivalent).
12
12
  grammar Lml version "1.0.0" {
13
+ # ---- Trivia (parsanol-ruby#134 skip rule) ----
14
+ # Grammar-native trivia replaces the pre-parse comment stripping:
15
+ # `//` line and `/* */` block comments (RS 3001 par. Comment) plus
16
+ # blanks/tabs are consumed and ignored between tokens. Newlines stay
17
+ # grammar-visible (line-oriented rules keep their semantics; the
18
+ # preprocessor preserved them too). Atomic token rules keep string
19
+ # literals and identifiers contiguous.
20
+ skip = trivia
21
+ trivia = 1*( ( 1*( " " / %x09 ) ) / ( line_comment [ ( [ %x0D ] %x0A / %x0D ) ] ) / block_comment )
22
+ line_comment = "//" ( *( !newline any_char ) )
23
+ block_comment = "/*" ( *( !"*/" any_char ) ) "*/"
24
+ atomic kw_association_sp = "association" 1*" "
25
+ atomic kw_artifact_sp = "artifact" 1*" "
26
+ atomic kw_attribute_sp = "attribute" 1*" "
27
+ atomic kw_caption_sp = "caption" 1*" "
28
+ atomic kw_class_sp = "class" 1*" "
29
+ atomic kw_visibility_sp = "visibility" 1*" "
30
+ atomic kw_version_sp = "version" 1*" "
31
+ version_declaration = kw_version_sp quoted_string as version
32
+ atomic kw_abstract_sp = "abstract" 1*" "
33
+ atomic kw_collection_sp = "collection" 1*" "
34
+ atomic kw_condition_sp = "condition" 1*" "
35
+ atomic kw_data_type_sp = "data_type" 1*" "
36
+ atomic kw_definition_sp = "definition" 1*" "
37
+ atomic kw_derive_sp = "derive" 1*" "
38
+ atomic kw_element_sp = "element" 1*" "
39
+ atomic kw_enum_sp = "enum" 1*" "
40
+ atomic kw_fontname_sp = "fontname" 1*" "
41
+ atomic kw_format_sp = "format" 1*" "
42
+ atomic kw_hide_sp = "hide" 1*" "
43
+ atomic kw_import_sp = "import" 1*" "
44
+ atomic kw_instance_sp = "instance" 1*" "
45
+ atomic kw_isa_sp = "isa" 1*" "
46
+ atomic kw_map_sp = "map" 1*" "
47
+ atomic kw_map_attribute_sp = "map_attribute" 1*" "
48
+ atomic kw_map_content_sp = "map_content" 1*" "
49
+ atomic kw_map_element_sp = "map_element" 1*" "
50
+ atomic kw_map_key_sp = "map_key" 1*" "
51
+ atomic kw_map_value_sp = "map_value" 1*" "
52
+ atomic kw_mapping_sp = "mapping" 1*" "
53
+ atomic kw_member_sp = "member" 1*" "
54
+ atomic kw_member_type_sp = "member_type" 1*" "
55
+ atomic kw_namespace_sp = "namespace" 1*" "
56
+ atomic kw_owner_sp = "owner" 1*" "
57
+ atomic kw_owner_type_sp = "owner_type" 1*" "
58
+ atomic kw_package_sp = "package" 1*" "
59
+ atomic kw_pattern_sp = "pattern" 1*" "
60
+ atomic kw_prefix_sp = "prefix" 1*" "
61
+ atomic kw_primitive_sp = "primitive" 1*" "
62
+ atomic kw_require_sp = "require" 1*" "
63
+ atomic kw_show_sp = "show" 1*" "
64
+ atomic kw_string_format_sp = "string_format" 1*" "
65
+ atomic kw_title_sp = "title" 1*" "
66
+ atomic kw_to_instance_sp = "to_instance" 1*" "
67
+ atomic kw_uri_sp = "uri" 1*" "
68
+ atomic kw_value_sp = "value" 1*" "
69
+ atomic kw_values_sp = "values" 1*" "
13
70
  # ---- Character primitives ----
14
71
  alpha = %x41-5A / %x61-7A
15
- word = %x41-5A / %x61-7A / %x30-39 / "_"
72
+ atomic word = %x41-5A / %x61-7A / %x30-39 / "_"
16
73
  cardchar = %x30-39 / %x61-7A / "*"
17
74
  # Parslet's `any`: any Unicode character (not just bytes)
18
75
  any_char = %x00-10FFFF
19
76
  # [^\s\n\r] — regex \s covers space, tab, CR, LF, VT, FF
20
- nonspace = !" " !%x09 !%x0A !%x0B !%x0C !%x0D any_char
77
+ atomic nonspace = !" " !%x09 !%x0A !%x0B !%x0C !%x0D any_char
21
78
  quote = %x22 / %x27
22
79
  newline = %x0D / %x0A
23
80
 
24
- ws = 1*" "
25
- whitespace = 1*( " " / %x09 / [ %x0D ] %x0A / %x0D / ";" )
81
+ whitespace = 1*( ( 1*( " " / %x09 ) ) / ( [ %x0D ] %x0A ) / %x0D / ";" / line_comment / block_comment )
26
82
 
27
83
  # match['"\'']
28
84
  quotes = %x22 / %x27
29
85
 
30
86
  # quoted('"', :string) | quoted("'", :string)
31
- dq_string = %x22 ( *( !%x22 any_char ) ) as string %x22
32
- sq_string = %x27 ( *( !%x27 any_char ) ) as string %x27
87
+ atomic dq_string = %x22 until "\"" as string %x22
88
+ atomic sq_string = %x27 until "'" as string %x27
33
89
  quoted_string = dq_string / sq_string
34
90
  # any_quoted(:path) for view import
35
- dq_path = %x22 ( *( !%x22 any_char ) ) as path %x22
36
- sq_path = %x27 ( *( !%x27 any_char ) ) as path %x27
91
+ atomic dq_path = %x22 until "\"" as path %x22
92
+ atomic sq_path = %x27 until "'" as path %x27
37
93
 
38
94
  boolean = ( "true" / "false" ) as boolean
39
95
  number = ( 1*%x30-39 "." 1*%x30-39 ) as float / ( 1*%x30-39 ) as number
40
96
  variable = quoted_string / 1*word
41
- reference = "reference:(" ( variable *( "." variable ) ) as reference ")"
97
+ reference = ( "reference:(" / "ref:(" ) ( variable *( "." variable ) ) as reference ")"
42
98
  range = ( variable as start ".." variable as end ) as range
43
99
  namespaced_identifier = variable *( "::" variable )
44
- comment_definition = [ ws ] ( "**" / "#" ) ( *( !newline any_char ) ) as comments
45
- comment_multiline_definition = [ ws ] "*|" ( *( !"|*" any_char ) ) as comments [ whitespace ] "|*"
100
+ comment_definition = ( "**" / "#" ) ( *( !newline ANY ) ) as comments
101
+ comment_multiline_definition = "*|" until "|*" as comments [ whitespace ] "|*"
46
102
 
47
103
  class_name_chars = 1*( %x41-5A / %x61-7A / %x30-39 / " " / "_" / "-" / ":" / "." )
48
104
  class_name = class_name_chars [ "(" class_name_chars ")" ]
49
105
  cardinality_body_definition = cardchar as min [ ".." [ cardchar as max ] ]
50
106
  cardinality = "[" cardinality_body_definition as cardinality "]"
51
107
 
52
- value = boolean / reference / range / number / quoted_string
53
- key_value_pair = variable as key ws [ "=" ] [ ws ] value as value
54
- key_value_map = "{" [ whitespace ] ( *( key_value_pair whitespace ) ) as key_value_map "}"
108
+ data_value = boolean / reference / range / number / quoted_string
109
+ key_value_pair = variable as key [ "=" ] data_value as value
110
+ key_value_map = "{" [ whitespace ] ( *( key_value_pair [ whitespace ] ) ) as key_value_map "}"
55
111
  kw_visibility_modifier = "+" / "-" / "#" / "~"
56
112
 
57
113
  # ---- Attributes ----
58
- attribute = comment_definition / ( variable as key [ ws ] [ "+" as add ] [ "=" ] [ ws ] attribute_value as value )
59
- attributes = ( *( attribute_line / whitespace ) ) as attributes
60
- attribute_line = [ ws ] attribute [ [ "," ] whitespace ]
114
+ attribute = comment_definition / ( variable as key [ "+" as add ] [ "=" ] attribute_value as value )
115
+ attributes = ( *( definition_body / definition_line / attribute_line / whitespace ) ) as attributes
116
+ attribute_line = attribute [ [ "," ] whitespace ]
61
117
  visibility = kw_visibility_modifier as visibility_modifier
62
118
  attribute_stereotype = "<<" ( 1*( %x41-5A / %x61-7A / %x30-39 / "_" / "-" / "/" ) ) as stereotype ">>"
63
- typechar = %x41-5A / %x61-7A / %x30-39 / "_" / "-" / " " / ":" / "/" / "+"
64
- namechar = %x41-5A / %x61-7A / %x30-39 / "_" / "-" / "/" / "+"
65
- attribute_type = [ ":" ] [ ws ] [ attribute_stereotype ] [ ws ] [ quotes as type_literal ] ( 1*typechar ) as type [ quotes ] [ ws ]
119
+ atomic type_token = 1*( !( "//" / "/*" ) typechar )
120
+ atomic typechar = %x41-5A / %x61-7A / %x30-39 / "_" / "-" / " " / ":" / "/" / "+"
121
+ atomic namechar = %x41-5A / %x61-7A / %x30-39 / "_" / "-" / "/" / "+"
122
+ attribute_type = [ ":" ] [ attribute_stereotype ] [ quotes as type_literal ] ( type_token ) as type [ quotes ]
66
123
  attribute_name = ( 1*namechar ) as name
67
124
  attribute_definition_name = ( quotes ( 1*( %x41-5A / %x61-7A / %x30-39 / "_" / "-" / " " / "/" / "+" ) ) as name quotes ) / attribute_name
68
- attribute_definition = ( [ visibility ] as visibility [ ws ] attribute_definition_name [ ws ] [ attribute_type ] [ cardinality ] [ class_body ] ) as attributes
125
+ attribute_definition = ( [ visibility ] as visibility attribute_definition_name [ attribute_type ] [ cardinality ] [ class_body ] ) as attributes
69
126
 
70
- keyword_type_argument = "type" [ ws ] ( 1*nonspace ) as type [ whitespace ]
71
- keyword_cardinality_argument = "cardinality" [ ws ] cardinality_body_definition as cardinality [ whitespace ]
72
- keyword_any_argument = [ ws ] ( 1*nonspace ) as name ws [ "=" ] [ ws ] attribute_value as value [ whitespace ]
73
- keyword_attribute_options = *( keyword_type_argument / keyword_cardinality_argument / ( keyword_any_argument ) as properties )
127
+ keyword_type_argument = "type" ( 1*nonspace ) as type [ whitespace ]
128
+ keyword_cardinality_argument = "cardinality" cardinality_body_definition as cardinality [ whitespace ]
129
+ keyword_any_argument = ( keyword_any_name ) as name [ "=" / ":" ] attribute_value as value [ whitespace ]
130
+ atomic keyword_any_name = 1*( !( "{" / "}" ) nonspace )
131
+ keyword_values_argument = kw_values_sp ( ( value_set_body ) as value_set / ( variable ) as value_set_ref ) [ whitespace ]
132
+ keyword_pattern_argument = ( kw_pattern_sp "/" ( *( ( "\\" any_char ) / ( !"/" any_char ) ) ) as pattern "/" [ whitespace ] )
133
+ value_set_body = "{" [ whitespace ] ( *( ( ( data_value / variable ) as item ) [ "," ] [ whitespace ] ) ) as items "}"
134
+ keyword_attribute_options = *( keyword_type_argument / keyword_cardinality_argument / keyword_values_argument / keyword_pattern_argument / ( keyword_any_argument ) as properties )
74
135
  keyword_attribute_body = "{" [ whitespace ] keyword_attribute_options [ whitespace ] "}"
75
- keyword_attribute_definition = ( "attribute" ws attribute_name [ ws ] keyword_attribute_body ) as attributes
136
+ keyword_attribute_definition = ( kw_attribute_sp attribute_name keyword_attribute_body ) as attributes
76
137
 
77
138
  # ---- Associations ----
78
- owner_cardinality = [ ws ] "[" cardinality_body_definition as owner_end_cardinality "]"
79
- owner_attribute_name = "#" [ visibility ] [ ws ] variable as owner_end_attribute_name [ ws ]
80
- owner_definition = [ whitespace ] "owner" ws variable as owner_end [ owner_attribute_name ] [ owner_cardinality ]
81
- owner_type = [ whitespace ] "owner_type" ws variable as owner_end_type
82
- member_cardinality = [ ws ] "[" cardinality_body_definition as member_end_cardinality "]"
83
- member_attribute_name = "#" [ visibility ] [ ws ] variable as member_end_attribute_name [ ws ]
84
- member_definition = [ whitespace ] "member" ws variable as member_end [ member_attribute_name ] [ member_cardinality ]
85
- member_type = [ whitespace ] "member_type" ws variable as member_end_type
139
+ owner_cardinality = "[" cardinality_body_definition as owner_end_cardinality "]"
140
+ owner_attribute_name = "#" [ visibility ] variable as owner_end_attribute_name
141
+ owner_definition = [ whitespace ] kw_owner_sp variable as owner_end [ owner_attribute_name ] [ owner_cardinality ]
142
+ owner_type = [ whitespace ] kw_owner_type_sp variable as owner_end_type
143
+ member_cardinality = "[" cardinality_body_definition as member_end_cardinality "]"
144
+ member_attribute_name = "#" [ visibility ] variable as member_end_attribute_name
145
+ member_definition = [ whitespace ] kw_member_sp variable as member_end [ member_attribute_name ] [ member_cardinality ]
146
+ member_type = [ whitespace ] kw_member_type_sp variable as member_end_type
86
147
  association_inner_definition = ( owner_type / member_type / owner_definition / member_definition / comment_definition / comment_multiline_definition ) [ whitespace ]
87
- association_body = [ ws ] "{" [ whitespace ] ( *( association_inner_definition ) ) as members "}"
88
- association_definition = [ whitespace ] "association" ws [ variable as name ] [ ws ] association_body
148
+ association_body = "{" [ whitespace ] ( *( association_inner_definition ) ) as members "}"
149
+ association_definition = [ whitespace ] kw_association_sp [ variable as name ] association_body
89
150
 
90
151
  # ---- Definitions ----
91
- class_modifier = [ ( [ whitespace ] ( "abstract" / "interface" ) ) as modifier ws ]
152
+ # ---- Operations (class methods) ----
153
+ # Symbol visibility reuses the attribute prefix (+ - # ~); the
154
+ # transform maps it to public/private/protected/friendly.
155
+ operation_definition = [ whitespace ] [ visibility ] ( 1*word ) as name "(" [ ( operation_params ) as owned_parameter ] ")" [ operation_return ] [ operation_body ] [ whitespace ]
156
+ operation_params = operation_param *( "," [ whitespace ] operation_param )
157
+ operation_return = ":" ( type_token ) as return_type [ cardinality ]
158
+ # A parenthesized alternative is not an Entity, so an optional body
159
+ # of `x as k` around it gets no skip injection - the blank after "="
160
+ # was unconsumable. Reference a rule instead.
161
+ operation_param = ( [ ( "in" / "out" / "inout" ) as direction 1*" " ] ( 1*word ) as name [ ":" ] param_type as type [ "=" ] param_default_value as param_default )
162
+ param_default_value = [ quoted_string / ( 1*word ) / number ]
163
+ # The attribute pattern: separator as its own optional, value as a
164
+ # rule sibling. A `x as k` optional body gets no interior skip
165
+ # injection, so [ ":" type as t ] cannot consume the blank after
166
+ # the separator.
167
+ param_type = [ type_token ]
168
+ operation_body = "{" [ whitespace ] [ ( *( ( 1*word ) as stereotype [ "," ] [ whitespace ] ) ) as stereotype ] [ whitespace ] "}"
169
+
170
+ class_modifier = [ [ whitespace ] modifier_token ]
171
+ atomic modifier_token = ( ( "abstract" / "interface" ) as modifier 1*" " )
172
+ # RS 3001 par. Declaration modifiers: keyword prefix (sugar) and the
173
+ # canonical in-block declarations. The `package` visibility keyword is
174
+ # guarded: it must be followed by a declaration keyword, so a bare
175
+ # `package P { }` still binds as a package declaration.
176
+ atomic element_visibility = ( ( "public" / "private" / "protected" / ( "package" &( 1*" " ( kw_class_sp / kw_enum_sp / kw_data_type_sp / kw_primitive_sp / kw_package_sp ) ) ) ) as visibility 1*" " )
177
+ visibility_declaration = kw_visibility_sp ( "public" / "private" / "protected" / "package" ) as visibility
178
+ abstract_declaration = kw_abstract_sp ( "true" / "false" ) as abstract
179
+ # ---- Namespaces (RS 3010 extension: lutaml-model XmlNamespace) ----
180
+ namespace_uri_line = ( kw_uri_sp ( quoted_string / ( 1*word ) ) as uri [ whitespace ] )
181
+ namespace_prefix_line = ( kw_prefix_sp ( quoted_string / ( 1*word ) ) as prefix [ whitespace ] )
182
+ namespace_body = "{" [ whitespace ] ( *( namespace_uri_line / namespace_prefix_line ) ) as members "}"
183
+ namespace_inline = ( ( quoted_string ) as uri [ kw_prefix_sp ( quoted_string / ( 1*word ) ) as prefix ] )
184
+ namespace_definition = ( [ whitespace ] kw_namespace_sp ( 1*word ) as name ( namespace_body / namespace_inline ) [ whitespace ] ) as namespaces
185
+
186
+ # ---- Serialization mappings (RS 3010 extension: sibling-linked) ----
187
+ serialization_body = "{" [ whitespace ] ( *( serialization_line ) ) as rules "}"
188
+ serialization_line = ( mapping_element_name_line / mapping_element_line / mapping_attribute_line / mapping_content_line / mapping_namespace_line / mapping_mixed_line / legacy_map_line / mapping_map_line / mapping_map_key_line / mapping_map_value_line ) [ whitespace ]
189
+ legacy_map_line = ( kw_map_sp dq_string as wire "," "to:" ( quoted_string / ( 1*word ) ) as to [ whitespace ] ) as legacy_map
190
+ mapping_element_name_line = ( kw_element_sp dq_string as element_name [ whitespace ] ) as element_name
191
+ mapping_element_line = ( kw_map_element_sp ( element_args ) ) as map_element
192
+ element_args = ( dq_string as wire [ ( 1*word ) as field ] / ( 1*word ) as field )
193
+ mapping_attribute_line = ( kw_map_attribute_sp ( attribute_args ) ) as map_attribute
194
+ attribute_args = ( dq_string as wire [ ( 1*word ) as field ] / ( 1*word ) as field )
195
+ mapping_content_line = ( kw_map_content_sp ( 1*word ) as field ) as map_content
196
+ mapping_namespace_line = ( kw_namespace_sp ( 1*word ) as namespace_ref ) as map_namespace
197
+ mapping_mixed_line = ( "mixed_content" ) as mixed_content
198
+ mapping_map_line = ( kw_map_sp map_args ) as map
199
+ map_args = ( dq_string as wire ( 1*word ) as field / dq_string as wire / ( 1*word ) as field )
200
+ mapping_map_key_line = ( kw_map_key_sp kw_to_instance_sp ( 1*word ) as field ) as map_key
201
+ mapping_map_value_line = ( kw_map_value_sp map_value_mode ( 1*word ) as field ) as map_value
202
+ atomic map_value_mode = ( ( "to_instance" / "as_attribute" ) as mode 1*" " )
203
+ mapping_definition = ( [ whitespace ] kw_mapping_sp ( 1*word ) as format ( 1*word ) as target serialization_body [ whitespace ] ) as serialization_mappings
204
+
92
205
  # ---- Mapping blocks (lutaml-model wire names) ----
93
206
  map_name = ( quoted_string / 1*( word / "-" / "." ) )
94
- map_line = [ ws ] "map" ws ( map_name ) as wire "," ws "to:" ws ( map_name ) as to [ whitespace ]
95
- mapping_body = [ ws ] "{" [ whitespace ] ( *( map_line ) ) as maps "}"
96
- mapping_section = [ whitespace ] "mapping" ws ( variable ) as format mapping_body
207
+ map_line = kw_map_sp ( map_name ) as wire "," "to:" ( map_name ) as to [ whitespace ]
208
+ mapping_body = "{" [ whitespace ] ( *( map_line ) ) as maps "}"
209
+ class_mapping_section = ( [ whitespace ] kw_mapping_sp ( 1*word ) as format serialization_body [ whitespace ] ) as serialization_mappings
97
210
 
98
- derive_definition = ( [ whitespace ] "derive" ws attribute_name [ ws ] ":" [ ws ] [ quotes ] ( 1*typechar ) as type [ quotes ] ) as derived_field [ whitespace ]
99
- string_format_item = ( [ ws ] ( "artifact" / "root" ) as key ws ( quoted_string / 1*nonspace ) as value [ whitespace ] )
211
+ derive_definition = ( [ whitespace ] kw_derive_sp attribute_name ":" [ quotes ] ( type_token ) as type [ quotes ] ) as derived_field [ whitespace ]
212
+ string_format_item = ( sf_key_token ( quoted_string / 1*nonspace ) as value [ whitespace ] )
213
+ atomic sf_key_token = ( ( "artifact" / "root" ) as key 1*" " )
100
214
  string_format_body = "{" [ whitespace ] ( *( string_format_item ) ) as items [ whitespace ] "}"
101
- string_format_definition = ( [ whitespace ] "string_format" ws ( variable ) as format [ ws ] string_format_body ) as string_format
215
+ string_format_definition = ( [ whitespace ] kw_string_format_sp ( variable ) as format string_format_body ) as string_format
102
216
 
103
- class_inner_definitions = definition_body / mapping_section / derive_definition / string_format_definition / ( !( "attribute" ws ) attribute_definition ) / keyword_attribute_definition / comment_definition / comment_multiline_definition
217
+ class_inner_definitions = visibility_declaration / abstract_declaration / definition_body / definition_line / class_mapping_section / derive_definition / string_format_definition / ( operation_definition ) as operations / attribute_comma_definition / ( !( kw_attribute_sp ) attribute_definition ) / keyword_attribute_definition / comment_definition / comment_multiline_definition
104
218
  class_inner_definition = class_inner_definitions [ whitespace ]
105
- class_body = [ ws ] "{" [ whitespace ] ( *( class_inner_definition ) ) as members "}"
106
- parent_class = [ ws ] "<" [ ws ] ( class_name_chars ) as parent_class
107
- class_definition = class_modifier [ whitespace ] "class" ws class_name as name [ parent_class ] [ ws ] [ attribute_stereotype ] [ class_body ]
219
+ class_body = "{" [ whitespace ] ( *( class_inner_definition ) ) as members "}"
220
+ parent_class = "<" ( class_name_chars ) as parent_class
221
+ class_definition = [ element_visibility ] class_modifier [ whitespace ] kw_class_sp class_name as name [ parent_class ] [ attribute_stereotype ] [ class_body ]
108
222
 
109
223
  # Balanced-brace definition text: nested "{...}" and "\"-escapes allowed
110
224
  definition_content = *( ( "\\" any_char ) / ( "{" definition_content "}" ) / ( !"}" any_char ) )
111
- definition_body = [ ws ] "definition" [ whitespace ] "{" ( definition_content ) as definition "}"
225
+ definition_body = "definition" [ whitespace ] "{" ( definition_content ) as definition "}"
226
+ definition_line = ( kw_definition_sp ( dq_string / sq_string ) ) as definition [ whitespace ]
112
227
 
113
- member_field_line = [ ws ] attribute_name [ ws ] ( 1*nonspace ) as member_field_type [ whitespace ]
228
+ member_field_line = attribute_name ( 1*nonspace ) as member_field_type [ whitespace ]
114
229
  member_fields_definition = ( [ whitespace ] "member_fields" [ whitespace ] "{" [ whitespace ] ( *( member_field_line ) ) as member_fields [ whitespace ] "}" ) as member_fields_decl
115
- enum_member_with_payload = ( [ whitespace ] attribute_name [ ws ] ( class_body / keyword_attribute_body ) ) as attributes
230
+ attribute_comma_definition = ( [ whitespace ] kw_attribute_sp attribute_name "," ( type_token ) as type [ "{" [ whitespace ] keyword_attribute_options [ whitespace ] "}" ] [ whitespace ] ) as attributes
231
+ enum_member_with_payload = ( [ whitespace ] attribute_name ( class_body / keyword_attribute_body ) ) as attributes
232
+
233
+ # ---- Canonical value construct (RS 3001 par. Value / par. Enums) ----
234
+ # `value <name> { block }` is the canonical enum member; the bare
235
+ # identifier member is its documented shortcut. from_table expands to
236
+ # value members at compile time.
237
+ value_name = quoted_string / map_name
238
+ value_inner_definition = ( definition_body / definition_line / instance / attribute_line / comment_definition / comment_multiline_definition ) [ whitespace ]
239
+ value_body = "{" [ whitespace ] ( *( value_inner_definition ) ) as members "}"
240
+ value_definition = ( [ whitespace ] kw_value_sp ( value_name ) as value_name [ value_body ] [ whitespace ] ) as value_decl
116
241
 
117
- enum_inner_definitions = definition_body / member_fields_definition / enum_member_with_payload / attribute_definition / comment_definition / comment_multiline_definition
242
+ from_table_definition = ( [ whitespace ] "from_table" ( map_name ) as from_table_name [ "{" [ whitespace ] *( kw_artifact_sp ( map_name ) as artifact_table [ whitespace ] ) "}" ] [ whitespace ] ) as from_table_decl
243
+
244
+ enum_values_shorthand = ( [ whitespace ] kw_values_sp value_set_body [ whitespace ] ) as value_set_decl
245
+ enum_inner_definitions = visibility_declaration / abstract_declaration / definition_body / definition_line / member_fields_definition / from_table_definition / enum_values_shorthand / value_definition / enum_member_with_payload / attribute_comma_definition / attribute_definition / comment_definition / comment_multiline_definition
118
246
  enum_inner_definition = enum_inner_definitions [ whitespace ]
119
- enum_body = [ ws ] "{" [ whitespace ] ( *( enum_inner_definition ) ) as members "}"
120
- enum_definition = [ whitespace ] "enum" ws [ quotes ] class_name as name [ quotes ] [ attribute_stereotype ] [ enum_body ]
121
- data_type_body = [ ws ] "{" [ whitespace ] ( *( enum_inner_definition ) ) as members "}"
122
- data_type_definition = [ whitespace ] "data_type" ws [ quotes ] class_name as name [ quotes ] [ attribute_stereotype ] [ data_type_body ]
123
- primitive_definition = [ whitespace ] "primitive" ws [ quotes ] class_name as name [ quotes ]
247
+ enum_body = "{" [ whitespace ] ( *( enum_inner_definition ) ) as members "}"
248
+ enum_definition = [ whitespace ] [ element_visibility ] kw_enum_sp [ quotes ] class_name as name [ quotes ] [ attribute_stereotype ] [ enum_body ]
249
+ data_type_body = "{" [ whitespace ] ( *( enum_inner_definition ) ) as members "}"
250
+ data_type_definition = [ whitespace ] [ element_visibility ] kw_data_type_sp [ quotes ] class_name as name [ quotes ] [ attribute_stereotype ] [ data_type_body ]
251
+ primitive_definition = [ whitespace ] [ element_visibility ] kw_primitive_sp [ quotes ] class_name as name [ quotes ]
124
252
 
125
- diagram_inner_definitions = title_definition / caption_definition / fontname_definition / ( class_definition ) as classes / ( enum_definition ) as enums / ( primitive_definition ) as primitives / ( data_type_definition ) as data_types / ( association_definition ) as associations / comment_definition / comment_multiline_definition
253
+ package_body = "{" [ whitespace ] ( *( diagram_inner_definition ) ) as members "}"
254
+ package_definition = ( [ whitespace ] [ element_visibility ] kw_package_sp class_name as name package_body [ whitespace ] ) as packages
255
+ diagram_inner_definitions = namespace_definition / mapping_definition / ( class_definition ) as classes / ( enum_definition ) as enums / ( primitive_definition ) as primitives / ( data_type_definition ) as data_types / package_definition / title_definition / caption_definition / fontname_definition / ( association_definition ) as associations / comment_definition / comment_multiline_definition
126
256
  diagram_inner_definition = diagram_inner_definitions [ whitespace ]
127
- diagram_body = [ ws ] "{" [ whitespace ] ( *( diagram_inner_definition ) ) as members "}"
128
- diagram_definition = [ whitespace ] "diagram" [ ws ] class_name as name diagram_body [ whitespace ]
257
+ diagram_body = "{" [ whitespace ] ( *( diagram_inner_definition ) ) as members "}"
258
+ diagram_definition = [ whitespace ] "diagram" class_name as name diagram_body [ whitespace ]
129
259
  diagram_definitions = diagram_definition [ whitespace ]
130
- model_definitions = ( *( diagram_inner_definition ) ) as members
260
+ model_definitions = ( 1*( diagram_inner_definition ) ) as members
131
261
 
132
262
  view_only_definitions = ( view_import ) as view_imports / show_directive / hide_directive
133
263
  view_inner_definitions = view_only_definitions / diagram_inner_definitions
134
264
  view_inner_definition = view_inner_definitions [ whitespace ]
135
- view_body = [ ws ] "{" [ whitespace ] ( *( view_inner_definition ) ) as members "}"
136
- view_definition = [ whitespace ] "view" [ ws ] class_name as name view_body [ whitespace ]
265
+ view_body = "{" [ whitespace ] ( *( view_inner_definition ) ) as members "}"
266
+ view_definition = [ whitespace ] "view" class_name as name view_body [ whitespace ]
137
267
  view_definitions = view_definition [ whitespace ]
138
268
 
139
- title_text = ( %x22 ( 1*( !%x22 any_char ) ) as title %x22 ) / ( %x27 ( 1*( !%x27 any_char ) ) as title %x27 ) / ( 1*( %x41-5A / %x61-7A / %x30-39 / "_" / "-" / " " / "," / "." / ":" / ";" / "(" / ")" / "+" / "'" / "/" ) ) as title
140
- title_definition = [ whitespace ] "title" ws title_text
141
- caption_text = ( %x22 ( 1*( !%x22 any_char ) ) as caption %x22 ) / ( %x27 ( 1*( !%x27 any_char ) ) as caption %x27 ) / ( 1*( %x41-5A / %x61-7A / %x30-39 / "_" / "-" / " " / "," / "." / ":" / ";" / "(" / ")" / "+" / "'" / "/" ) ) as caption
142
- caption_definition = [ whitespace ] "caption" ws caption_text
269
+ atomic title_text = ( %x22 ( 1*( !%x22 ANY ) ) as title %x22 ) / ( %x27 ( 1*( !%x27 ANY ) ) as title %x27 ) / ( 1*( %x41-5A / %x61-7A / %x30-39 / "_" / "-" / " " / "," / "." / ":" / ";" / "(" / ")" / "+" / "'" / "/" ) ) as title
270
+ title_definition = [ whitespace ] kw_title_sp title_text
271
+ atomic caption_text = ( %x22 ( 1*( !%x22 ANY ) ) as caption %x22 ) / ( %x27 ( 1*( !%x27 ANY ) ) as caption %x27 ) / ( 1*( %x41-5A / %x61-7A / %x30-39 / "_" / "-" / " " / "," / "." / ":" / ";" / "(" / ")" / "+" / "'" / "/" ) ) as caption
272
+ caption_definition = [ whitespace ] kw_caption_sp caption_text
143
273
  fontname_text = [ quotes ] ( 1*( %x41-5A / %x61-7A / %x30-39 / "_" / "-" / " " ) ) as fontname [ quotes ]
144
- fontname_definition = [ whitespace ] "fontname" ws fontname_text
274
+ fontname_definition = [ whitespace ] kw_fontname_sp fontname_text
145
275
 
146
276
  # ---- Views ----
147
277
  # `import` here carries a required trailing space (word boundary)
148
- view_import = [ whitespace ] "import" ws ( dq_path / sq_path ) [ whitespace ]
278
+ view_import = [ whitespace ] kw_import_sp ( dq_path / sq_path ) [ whitespace ]
149
279
  entity_name_atom = ( 1*( %x41-5A / %x61-7A / %x30-39 / "_" / "." / ":" / "-" ) ) as entity_name
150
- entity_name_list = entity_name_atom *( [ ws ] "," [ ws ] entity_name_atom )
151
- show_directive = [ whitespace ] "show" ws ( entity_name_list ) as show_list [ whitespace ]
152
- hide_directive = [ whitespace ] "hide" ws ( entity_name_list ) as hide_list [ whitespace ]
280
+ entity_name_list = entity_name_atom *( "," entity_name_atom )
281
+ show_directive = [ whitespace ] kw_show_sp ( entity_name_list ) as show_list [ whitespace ]
282
+ hide_directive = [ whitespace ] kw_hide_sp ( entity_name_list ) as hide_list [ whitespace ]
153
283
 
154
284
  # ---- Requires ----
155
- require_stmt = [ whitespace ] "require" ws quoted_string as require [ whitespace ]
285
+ require_stmt = [ whitespace ] kw_require_sp quoted_string as require [ whitespace ]
156
286
  require_block = ( *( require_stmt [ whitespace ] ) ) as requires
157
287
 
158
288
  # ---- Instances ----
159
- instance = keyword_instance / class_instance
160
- isa_definition = [ whitespace ] "isa" ws ( namespaced_identifier ) as isa [ whitespace ]
161
- keyword_instance = ( [ whitespace ] "instance" [ ws ( namespaced_identifier ) as instance_type ] [ ws ] "{" [ whitespace ] [ isa_definition ] ( ( [ ws ] instance ) / attributes ) "}" ) as instance [ whitespace ]
289
+ instance = instance_named / keyword_instance / class_instance
290
+ instance_named = ( [ whitespace ] kw_instance_sp quoted_string as name [ ( namespaced_identifier ) as instance_type ] "{" [ whitespace ] ( ( instance ) / attributes ) "}" [ whitespace ] ) as instance
291
+ isa_definition = [ whitespace ] kw_isa_sp ( namespaced_identifier ) as isa [ whitespace ]
292
+ keyword_instance = ( [ whitespace ] "instance" [ ( namespaced_identifier ) as instance_type ] "{" [ whitespace ] [ isa_definition ] ( ( instance ) / attributes ) "}" ) as instance [ whitespace ]
162
293
  class_instance = ( variable as instance_type [ whitespace ] quoted_string as name [ whitespace ] [ [ whitespace ] "extends" [ whitespace ] quoted_string as parent [ whitespace ] ] "{" [ whitespace ] [ lml_instance_body ] "}" ) as instance [ whitespace ]
163
294
  lml_instance_body = lml_instance_members [ whitespace ]
164
295
  instance_template = [ whitespace ] "template" [ whitespace ] "{" [ whitespace ] attributes [ whitespace ] "}"
165
296
  lml_instance_members = ( instance_template ) as template / attributes
166
- models = [ whitespace ] "models" [ whitespace ] variable as name [ whitespace ] "{" ( *( ( ( string_format_definition ) as default_string_formats ) / model_body ) ) as members "}" [ whitespace ]
167
- model_body = ( ( class_definition ) as classes / ( enum_definition ) as enums ) [ whitespace ]
297
+ models = [ whitespace ] "models" [ whitespace ] variable as name [ version_declaration ] [ whitespace ] "{" ( *( ( ( string_format_definition ) as default_string_formats ) / model_body ) ) as members "}" [ whitespace ]
298
+ model_body = diagram_inner_definition
168
299
 
169
300
  # ---- Data structures ----
170
- attribute_value = instance / list / key_value_map / value / 1*( !%x0A any_char )
171
- list_item = instance / value
172
- list = "[" [ whitespace ] ( *( list_item [ ws ] [ "," ] [ whitespace ] ) ) as list [ whitespace ] "]"
173
- collection = [ whitespace ] "collection" ws quoted_string as name [ ws ] "{" [ whitespace ] [ includes ] [ whitespace ] [ validation ] [ whitespace ] "}" [ whitespace ]
174
- includes = [ whitespace ] "includes" [ ws ] ( list ) as includes
175
- validation = [ whitespace ] "validation" [ ws ] "{" [ whitespace ] ( *( condition ) ) as validations "}"
176
- condition = [ whitespace ] "condition" ws quoted_string as condition [ whitespace ]
177
- import = [ whitespace ] "import" [ ws ] "{" [ whitespace ] ( *( import_definition ) ) as imports "}" [ whitespace ]
178
- import_definition = ( 1*nonspace ) as format_type [ ws ] quoted_string as file [ whitespace ] "{" [ whitespace ] attributes [ whitespace ] "}" [ whitespace ]
179
- instances = [ whitespace ] "instances" [ whitespace ] "{" [ whitespace ] [ instances_body ] "}" [ whitespace ]
301
+ attribute_value = instance / list / key_value_map / data_value / 1*( !%x0A ANY )
302
+ list_item = instance / data_value
303
+ list = "[" [ whitespace ] ( *( list_item [ "," ] [ whitespace ] ) ) as list [ whitespace ] "]"
304
+ collection = [ whitespace ] kw_collection_sp quoted_string as name "{" [ whitespace ] [ includes ] [ whitespace ] [ validation ] [ whitespace ] "}" [ whitespace ]
305
+ includes = [ whitespace ] "includes" ( list ) as includes
306
+ validation = [ whitespace ] "validation" "{" [ whitespace ] ( *( condition ) ) as validations "}"
307
+ condition = [ whitespace ] kw_condition_sp quoted_string as condition [ whitespace ]
308
+ import = [ whitespace ] "import" "{" [ whitespace ] ( *( import_definition ) ) as imports "}" [ whitespace ]
309
+ import_definition = ( 1*nonspace ) as format_type quoted_string as file [ whitespace ] "{" [ whitespace ] attributes [ whitespace ] "}" [ whitespace ]
310
+ instances = [ whitespace ] "instances" [ ( quoted_string / variable ) as name ] [ whitespace ] "{" [ whitespace ] [ instances_body ] "}" [ whitespace ]
180
311
  instances_body = ( *( instances_member [ whitespace ] ) ) as instances
181
312
  instances_member = import / ( collection ) as collections / export / instance
182
313
  export = [ whitespace ] "export" [ whitespace ] "{" [ whitespace ] ( *( export_format [ whitespace ] ) ) as exports "}" [ whitespace ]
183
- export_format = [ whitespace ] "format" ws variable as format_type [ whitespace ] "{" [ whitespace ] attributes "}" [ whitespace ]
314
+ export_format = [ whitespace ] kw_format_sp variable as format_type [ whitespace ] "{" [ whitespace ] attributes "}" [ whitespace ]
315
+
316
+ # ---- Root (RS 3001: a document interleaves declarations) ----
317
+ diagram = [ require_block ] ( *( top_level_definition ) ) as members
184
318
 
185
- # ---- Root (grammar/instances.rb: Full) ----
186
- diagram = [ require_block ] ( models / diagram_definitions / view_definitions / instances / instance / model_definitions )
319
+ top_level_definition = ( models / diagram_definitions / view_definitions / instances / instance / model_definitions ) [ whitespace ]
187
320
  }
188
321
 
189
322
  entry diagram: diagram
323
+
324
+ # Compile-time contract tests for the trivia/skip behavior (run by
325
+ # `rake parg` and any in-memory compile; a failure fails the build).
326
+ test diagram {
327
+ accept "// leading\nclass Ceramic { }\n"
328
+ accept "class /* inline */ Ceramic { }\n"
329
+ accept "class Ceramic { } // trailing"
330
+ accept "instances {\n P \"p\" {\n url = \"http://a//b\"\n }\n}\n"
331
+ reject "classCeramic Foo { }"
332
+ reject "abstractclass Ceramic { }"
333
+ reject "class\tCeramic { }"
334
+ reject "class Ceramic { /* unterminated"
335
+ }
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Layout
5
+ # In-process layout engine backed by elkrb (pure-Ruby ELK). The input
6
+ # is an already-built Elkrb::Graph; rendering produces standalone SVG.
7
+ class ElkEngine < Engine
8
+ def render(_type)
9
+ require 'elkrb'
10
+ Elkrb::Layout::LayoutEngine.layout(@input, algorithm: 'layered')
11
+ ElkSvgRenderer.new.render(@input)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Layout
5
+ # Builds an Elkrb::Graph: a node per classifier (name, definition,
6
+ # members); an edge per association and parent specializer.
7
+ class ElkGraphBuilder
8
+ CHAR_WIDTH = 7.2
9
+ LINE_HEIGHT = 16.0
10
+ PADDING = 14.0
11
+ MIN_WIDTH = 120.0
12
+ COLLECTIONS = %i[classes enums primitives data_types].freeze
13
+ def build(document)
14
+ graph = elkrb_graph
15
+ add_entities(graph, document)
16
+ add_association_edges(graph, document)
17
+ add_parent_edges(graph, document)
18
+ graph
19
+ end
20
+
21
+ private
22
+
23
+ def elkrb_graph
24
+ require 'elkrb'
25
+ Elkrb::Graph::Graph.new(id: 'diagram')
26
+ rescue LoadError
27
+ raise Error, 'The elkrb gem is required for ELK layout: add `gem "elkrb"` to your Gemfile'
28
+ end
29
+
30
+ def entities(document)
31
+ ([document] + Array(document.packages)).flat_map { |scope| scope_entities(scope) }.flatten.compact
32
+ end
33
+
34
+ def scope_entities(scope)
35
+ COLLECTIONS.filter_map do |collection|
36
+ next unless scope.respond_to?(collection)
37
+
38
+ scope.public_send(collection)
39
+ end
40
+ end
41
+
42
+ def node_for(entity)
43
+ lines = label_lines(entity)
44
+ Elkrb::Graph::Node.new(
45
+ id: entity_id_for(entity.name),
46
+ width: node_width(lines),
47
+ height: PADDING + lines.size * LINE_HEIGHT,
48
+ labels: lines.each_with_index.map do |line, i|
49
+ Elkrb::Graph::Label.new(text: line, x: 0, y: i * LINE_HEIGHT)
50
+ end
51
+ )
52
+ end
53
+
54
+ def label_lines(entity)
55
+ lines = [entity.name.to_s]
56
+ if entity.respond_to?(:definition) && !entity.definition.to_s.empty?
57
+ lines << entity.definition.to_s.gsub("\n", ' ')
58
+ end
59
+ Array(entity.attributes).each do |attr|
60
+ lines << attr_line(attr)
61
+ end
62
+ lines
63
+ end
64
+
65
+ def attr_line(attr)
66
+ line = "+ #{attr.name}"
67
+ line += ": #{attr.type}" if attr.type && !attr.type.to_s.empty?
68
+ line
69
+ end
70
+
71
+ def node_width(lines)
72
+ widest = lines.map(&:length).max || 0
73
+ [widest * CHAR_WIDTH + PADDING, MIN_WIDTH].max
74
+ end
75
+
76
+ def entity_id(entity)
77
+ entity.name.to_s.gsub(/[^0-9a-zA-Z_]/, '_')
78
+ end
79
+
80
+ def associations(document)
81
+ Array(document.associations)
82
+ end
83
+
84
+ def parent_edges(document)
85
+ Array(document.classes).filter_map do |klass|
86
+ next unless klass.respond_to?(:parent_class) && klass.parent_class
87
+
88
+ { owner: klass.parent_class, member: klass.name.to_s,
89
+ member_end_type: 'generalization', name: nil }
90
+ end
91
+ end
92
+
93
+ def add_entities(graph, document)
94
+ entities(document).each { |entity| graph.children << node_for(entity) }
95
+ end
96
+
97
+ def add_association_edges(graph, document)
98
+ associations(document).each { |assoc| graph.edges << edge_for(association_spec(assoc)) }
99
+ end
100
+
101
+ def add_parent_edges(graph, document)
102
+ parent_edges(document).each { |edge| graph.edges << edge_for(edge) }
103
+ end
104
+
105
+ def edge_for(spec)
106
+ edge = elkrb_edge(spec)
107
+ edge.properties = { 'member_end_type' => spec[:member_end_type].to_s }
108
+ edge.labels = [Elkrb::Graph::Label.new(text: spec[:name].to_s)] if spec[:name]
109
+ edge
110
+ end
111
+
112
+ def elkrb_edge(spec)
113
+ require 'elkrb'
114
+ Elkrb::Graph::Edge.new(
115
+ id: "edge_#{spec[:owner]}_#{spec[:member]}",
116
+ sources: [entity_id_for(spec[:owner])],
117
+ targets: [entity_id_for(spec[:member])]
118
+ )
119
+ end
120
+
121
+ def association_spec(assoc)
122
+ { owner: assoc.owner_end.to_s, member: assoc.member_end.to_s,
123
+ member_end_type: assoc.member_end_type.to_s, name: assoc.name }
124
+ end
125
+
126
+ def entity_id_for(name) = name.to_s.gsub(/[^0-9a-zA-Z_]/, '_')
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Layout
5
+ # SVG arrow-marker vocabulary and the <defs> block for ElkSvgRenderer.
6
+ # Marker ids are keyed by association member_end_type.
7
+ module ElkSvgMarkers
8
+ EDGE_STROKE = '#666666'
9
+
10
+ ARROW_MARKERS = {
11
+ 'association' => 'arrow',
12
+ 'aggregation' => 'diamond',
13
+ 'composition' => 'diamond_filled',
14
+ 'generalization' => 'triangle',
15
+ 'uses' => 'arrow',
16
+ 'direct' => 'arrow'
17
+ }.freeze
18
+
19
+ MARKER_SHAPES = {
20
+ 'arrow' => %(<path d="M 0 0 L 10 5 L 0 10 z" fill="#{EDGE_STROKE}"/>),
21
+ 'triangle' => %(<path d="M 0 0 L 12 5 L 0 10 z" fill="white" stroke="#{EDGE_STROKE}"/>),
22
+ 'diamond' => %(<path d="M 0 4 L 7 0 L 14 4 L 7 8 z" fill="white" stroke="#{EDGE_STROKE}"/>),
23
+ 'diamond_filled' => %(<path d="M 0 4 L 7 0 L 14 4 L 7 8 z" fill="#{EDGE_STROKE}"/>)
24
+ }.freeze
25
+
26
+ def arrow_marker(edge)
27
+ edge_type = edge.properties&.fetch('member_end_type', nil)
28
+ ARROW_MARKERS.fetch(edge_type.to_s, 'arrow')
29
+ end
30
+
31
+ def defs
32
+ MARKER_SHAPES.filter_map { |id, path| marker_def(id, path) }.join("\n")
33
+ end
34
+
35
+ private
36
+
37
+ def marker_def(id, path)
38
+ w, h = id.start_with?('diamond') ? %w[14 8] : %w[10 10]
39
+ %(<marker id="#{id}" viewBox="0 0 #{w} #{h}" refX="9" refY="4" ) +
40
+ %(markerWidth="#{w}" markerHeight="#{h}" ) +
41
+ %(orient="auto-start-reverse">#{path}</marker>)
42
+ end
43
+ end
44
+ end
45
+ end