lutaml-lml 0.1.0
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.
- checksums.yaml +7 -0
- data/lib/lutaml/lml/association_label_resolver.rb +52 -0
- data/lib/lutaml/lml/cli.rb +262 -0
- data/lib/lutaml/lml/data_processor/attribute_processing.rb +81 -0
- data/lib/lutaml/lml/data_processor/collection_processing.rb +37 -0
- data/lib/lutaml/lml/data_processor/instance_processing.rb +63 -0
- data/lib/lutaml/lml/data_processor/value_processing.rb +98 -0
- data/lib/lutaml/lml/data_processor/view_processing.rb +25 -0
- data/lib/lutaml/lml/data_processor.rb +49 -0
- data/lib/lutaml/lml/document_builder.rb +139 -0
- data/lib/lutaml/lml/executor/adapter_helpers.rb +45 -0
- data/lib/lutaml/lml/executor/condition_evaluator.rb +169 -0
- data/lib/lutaml/lml/executor/csv_adapter.rb +88 -0
- data/lib/lutaml/lml/executor/format_adapter.rb +54 -0
- data/lib/lutaml/lml/executor/xml_adapter.rb +102 -0
- data/lib/lutaml/lml/executor.rb +89 -0
- data/lib/lutaml/lml/format/adapter/document.rb +11 -0
- data/lib/lutaml/lml/format/adapter/mapping.rb +19 -0
- data/lib/lutaml/lml/format/adapter/standard_adapter.rb +127 -0
- data/lib/lutaml/lml/format/adapter/transform.rb +11 -0
- data/lib/lutaml/lml/format.rb +29 -0
- data/lib/lutaml/lml/formatter/base.rb +79 -0
- data/lib/lutaml/lml/formatter/graphviz/document_formatter.rb +89 -0
- data/lib/lutaml/lml/formatter/graphviz/html_builder.rb +72 -0
- data/lib/lutaml/lml/formatter/graphviz/node_formatter.rb +74 -0
- data/lib/lutaml/lml/formatter/graphviz/relationship_formatter.rb +130 -0
- data/lib/lutaml/lml/formatter/graphviz.rb +90 -0
- data/lib/lutaml/lml/formatter.rb +8 -0
- data/lib/lutaml/lml/grammar/concerns/associations.rb +76 -0
- data/lib/lutaml/lml/grammar/concerns/attributes.rb +126 -0
- data/lib/lutaml/lml/grammar/concerns/data_structures.rb +84 -0
- data/lib/lutaml/lml/grammar/concerns/definitions.rb +222 -0
- data/lib/lutaml/lml/grammar/concerns/instance_rules.rb +59 -0
- data/lib/lutaml/lml/grammar/concerns/primitives.rb +89 -0
- data/lib/lutaml/lml/grammar/concerns/view_rules.rb +34 -0
- data/lib/lutaml/lml/grammar/concerns.rb +17 -0
- data/lib/lutaml/lml/grammar/core.rb +71 -0
- data/lib/lutaml/lml/grammar/full.rb +12 -0
- data/lib/lutaml/lml/grammar/instances.rb +38 -0
- data/lib/lutaml/lml/grammar.rb +12 -0
- data/lib/lutaml/lml/has_attributes.rb +14 -0
- data/lib/lutaml/lml/import_resolver.rb +89 -0
- data/lib/lutaml/lml/layout/engine.rb +17 -0
- data/lib/lutaml/lml/layout/graph_viz_engine.rb +19 -0
- data/lib/lutaml/lml/layout.rb +8 -0
- data/lib/lutaml/lml/model_compiler.rb +325 -0
- data/lib/lutaml/lml/models/action.rb +10 -0
- data/lib/lutaml/lml/models/association.rb +26 -0
- data/lib/lutaml/lml/models/cardinality.rb +10 -0
- data/lib/lutaml/lml/models/collection.rb +11 -0
- data/lib/lutaml/lml/models/constraint.rb +20 -0
- data/lib/lutaml/lml/models/data_type.rb +31 -0
- data/lib/lutaml/lml/models/diagram.rb +17 -0
- data/lib/lutaml/lml/models/document.rb +45 -0
- data/lib/lutaml/lml/models/enum.rb +28 -0
- data/lib/lutaml/lml/models/fidelity.rb +10 -0
- data/lib/lutaml/lml/models/group.rb +11 -0
- data/lib/lutaml/lml/models/instance.rb +13 -0
- data/lib/lutaml/lml/models/instance_collection.rb +12 -0
- data/lib/lutaml/lml/models/instances_export.rb +10 -0
- data/lib/lutaml/lml/models/instances_import.rb +11 -0
- data/lib/lutaml/lml/models/operation.rb +23 -0
- data/lib/lutaml/lml/models/operation_parameter.rb +11 -0
- data/lib/lutaml/lml/models/package.rb +22 -0
- data/lib/lutaml/lml/models/primitive_type.rb +26 -0
- data/lib/lutaml/lml/models/top_element_attribute.rb +31 -0
- data/lib/lutaml/lml/models/uml_class.rb +84 -0
- data/lib/lutaml/lml/models/value.rb +12 -0
- data/lib/lutaml/lml/models/view_filter.rb +11 -0
- data/lib/lutaml/lml/models/view_import.rb +11 -0
- data/lib/lutaml/lml/parser.rb +22 -0
- data/lib/lutaml/lml/pipeline.rb +64 -0
- data/lib/lutaml/lml/preprocessor.rb +56 -0
- data/lib/lutaml/lml/transform.rb +20 -0
- data/lib/lutaml/lml/version.rb +7 -0
- data/lib/lutaml/lml/view_resolver.rb +48 -0
- data/lib/lutaml/lml/yaml_parser.rb +17 -0
- data/lib/lutaml/lml.rb +67 -0
- metadata +178 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
module Grammar
|
|
6
|
+
module Concerns
|
|
7
|
+
module Primitives
|
|
8
|
+
include Parslet
|
|
9
|
+
|
|
10
|
+
rule(:quotes) { match['"\''] }
|
|
11
|
+
rule(:quotes?) { quotes.maybe }
|
|
12
|
+
rule(:space) { match("\s") }
|
|
13
|
+
rule(:spaces) { space.repeat(1) }
|
|
14
|
+
rule(:spaces?) { spaces.maybe }
|
|
15
|
+
rule(:whitespace) do
|
|
16
|
+
(space | match(" ") | match("\r?\n") | match("\r") | str(";"))
|
|
17
|
+
.repeat(1)
|
|
18
|
+
end
|
|
19
|
+
rule(:whitespace?) { whitespace.maybe }
|
|
20
|
+
rule(:newline) { match('[\r\n]') }
|
|
21
|
+
|
|
22
|
+
rule(:quoted_string) do
|
|
23
|
+
str('"') >> (str('"').absent? >> any).repeat.as(:string) >> str('"')
|
|
24
|
+
end
|
|
25
|
+
rule(:boolean) { (str("true") | str("false")).as(:boolean) }
|
|
26
|
+
rule(:number) { (match("[0-9]").repeat(1) >> str(".") >> match("[0-9]").repeat(1)).as(:float) | match("[0-9]").repeat(1).as(:number) }
|
|
27
|
+
rule(:variable) { (quoted_string | match("[a-zA-Z0-9_]").repeat(1)) }
|
|
28
|
+
rule(:reference) do
|
|
29
|
+
str("reference:(") >>
|
|
30
|
+
(variable >> (str(".") >> variable).repeat).as(:reference) >>
|
|
31
|
+
str(")")
|
|
32
|
+
end
|
|
33
|
+
rule(:range) do
|
|
34
|
+
(variable.as(:start) >> str("..") >> variable.as(:end)).as(:range)
|
|
35
|
+
end
|
|
36
|
+
rule(:namespaced_identifier) do
|
|
37
|
+
variable >> (str("::") >> variable).repeat
|
|
38
|
+
end
|
|
39
|
+
rule(:comment_definition) do
|
|
40
|
+
spaces? >> (str("**") | str("#")) >> (newline.absent? >> any).repeat.as(:comments)
|
|
41
|
+
end
|
|
42
|
+
rule(:comment_multiline_definition) do
|
|
43
|
+
spaces? >> str("*|") >> (str("|*").absent? >> any)
|
|
44
|
+
.repeat.as(:comments) >> whitespace? >> str("|*")
|
|
45
|
+
end
|
|
46
|
+
rule(:class_name_chars) { match('(?:[a-zA-Z0-9 _-]|\:|\.)').repeat(1) }
|
|
47
|
+
rule(:class_name) do
|
|
48
|
+
class_name_chars >>
|
|
49
|
+
(str("(") >>
|
|
50
|
+
class_name_chars >>
|
|
51
|
+
str(")")).maybe
|
|
52
|
+
end
|
|
53
|
+
rule(:cardinality_body_definition) do
|
|
54
|
+
match['0-9a-z\*'].as("min") >>
|
|
55
|
+
str("..").maybe >>
|
|
56
|
+
match['0-9a-z\*'].as("max").maybe
|
|
57
|
+
end
|
|
58
|
+
rule(:cardinality) do
|
|
59
|
+
str("[") >>
|
|
60
|
+
cardinality_body_definition.as(:cardinality) >>
|
|
61
|
+
str("]")
|
|
62
|
+
end
|
|
63
|
+
rule(:cardinality?) { cardinality.maybe }
|
|
64
|
+
|
|
65
|
+
rule(:value) do
|
|
66
|
+
boolean |
|
|
67
|
+
reference |
|
|
68
|
+
range |
|
|
69
|
+
number |
|
|
70
|
+
quoted_string
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
rule(:key_value_pair) do
|
|
74
|
+
variable.as(:key) >> spaces >> str("=").maybe >> spaces? >> value.as(:value)
|
|
75
|
+
end
|
|
76
|
+
rule(:key_value_map) do
|
|
77
|
+
str("{") >> whitespace? >>
|
|
78
|
+
(key_value_pair >> whitespace).repeat.as(:key_value_map) >>
|
|
79
|
+
str("}")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
rule(:kw_visibility_modifier) do
|
|
83
|
+
str("+") | str("-") | str("#") | str("~")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
module Grammar
|
|
6
|
+
module Concerns
|
|
7
|
+
module ViewRules
|
|
8
|
+
include Parslet
|
|
9
|
+
|
|
10
|
+
rule(:view_import_keyword) { str("import") >> spaces }
|
|
11
|
+
|
|
12
|
+
rule(:view_import) do
|
|
13
|
+
view_import_keyword >>
|
|
14
|
+
str('"') >> (str('"').absent? >> any).repeat.as(:path) >> str('"') >>
|
|
15
|
+
whitespace?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
rule(:entity_name_list) do
|
|
19
|
+
class_name.as(:entity_name) >>
|
|
20
|
+
(spaces? >> str(",") >> spaces? >> class_name.as(:entity_name)).repeat
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
rule(:show_directive) do
|
|
24
|
+
str("show") >> spaces >> entity_name_list.as(:show_list) >> whitespace?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
rule(:hide_directive) do
|
|
28
|
+
str("hide") >> spaces >> entity_name_list.as(:hide_list) >> whitespace?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
module Grammar
|
|
6
|
+
module Concerns
|
|
7
|
+
autoload :Primitives, "lutaml/lml/grammar/concerns/primitives"
|
|
8
|
+
autoload :Attributes, "lutaml/lml/grammar/concerns/attributes"
|
|
9
|
+
autoload :Associations, "lutaml/lml/grammar/concerns/associations"
|
|
10
|
+
autoload :Definitions, "lutaml/lml/grammar/concerns/definitions"
|
|
11
|
+
autoload :ViewRules, "lutaml/lml/grammar/concerns/view_rules"
|
|
12
|
+
autoload :InstanceRules, "lutaml/lml/grammar/concerns/instance_rules"
|
|
13
|
+
autoload :DataStructures, "lutaml/lml/grammar/concerns/data_structures"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "parslet"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Lml
|
|
7
|
+
module Grammar
|
|
8
|
+
module Core
|
|
9
|
+
include Parslet
|
|
10
|
+
include Concerns::Primitives
|
|
11
|
+
include Concerns::Attributes
|
|
12
|
+
include Concerns::Associations
|
|
13
|
+
include Concerns::Definitions
|
|
14
|
+
include Concerns::ViewRules
|
|
15
|
+
|
|
16
|
+
CORE_KEYWORDS = %w[
|
|
17
|
+
abstract
|
|
18
|
+
aggregation
|
|
19
|
+
association
|
|
20
|
+
attribute
|
|
21
|
+
bidirectional
|
|
22
|
+
caption
|
|
23
|
+
class
|
|
24
|
+
composition
|
|
25
|
+
data_type
|
|
26
|
+
dependency
|
|
27
|
+
diagram
|
|
28
|
+
directional
|
|
29
|
+
enum
|
|
30
|
+
fontname
|
|
31
|
+
generalizes
|
|
32
|
+
include
|
|
33
|
+
interface
|
|
34
|
+
member
|
|
35
|
+
member_type
|
|
36
|
+
method
|
|
37
|
+
owner
|
|
38
|
+
owner_type
|
|
39
|
+
primitive
|
|
40
|
+
private
|
|
41
|
+
protected
|
|
42
|
+
public
|
|
43
|
+
realizes
|
|
44
|
+
static
|
|
45
|
+
title
|
|
46
|
+
view
|
|
47
|
+
].freeze
|
|
48
|
+
|
|
49
|
+
CORE_KEYWORDS.each do |keyword|
|
|
50
|
+
rule("kw_#{keyword}") { whitespace? >> str(keyword) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# === Require statements ===
|
|
54
|
+
rule(:require_stmt) do
|
|
55
|
+
kw_require >> spaces >> quoted_string.as(:require) >> whitespace?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
rule(:require_block) do
|
|
59
|
+
(require_stmt >> whitespace?).repeat.as(:requires)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
rule(:require_block?) do
|
|
63
|
+
require_block.maybe
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# -- Root (Core: diagram only)
|
|
67
|
+
rule(:diagram) { require_block? >> diagram_definitions }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "parslet"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Lml
|
|
7
|
+
module Grammar
|
|
8
|
+
module Instances
|
|
9
|
+
include Parslet
|
|
10
|
+
include Concerns::InstanceRules
|
|
11
|
+
include Concerns::DataStructures
|
|
12
|
+
|
|
13
|
+
INSTANCE_KEYWORDS = %w[
|
|
14
|
+
collection
|
|
15
|
+
condition
|
|
16
|
+
export
|
|
17
|
+
extends
|
|
18
|
+
format
|
|
19
|
+
import
|
|
20
|
+
includes
|
|
21
|
+
instance
|
|
22
|
+
instances
|
|
23
|
+
models
|
|
24
|
+
require
|
|
25
|
+
template
|
|
26
|
+
validation
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
INSTANCE_KEYWORDS.each do |keyword|
|
|
30
|
+
rule("kw_#{keyword}") { whitespace? >> str(keyword) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# -- Root (Full: diagram + models + instances)
|
|
34
|
+
rule(:diagram) { require_block? >> (models | diagram_definitions | view_definitions | instances | instance) }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
module Grammar
|
|
6
|
+
autoload :Core, "lutaml/lml/grammar/core"
|
|
7
|
+
autoload :Instances, "lutaml/lml/grammar/instances"
|
|
8
|
+
autoload :Full, "lutaml/lml/grammar/full"
|
|
9
|
+
autoload :Concerns, "lutaml/lml/grammar/concerns"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
module HasAttributes
|
|
6
|
+
def update_attributes(attributes = {})
|
|
7
|
+
attributes.to_h.each do |name, value|
|
|
8
|
+
value = value.str if value.is_a?(Parslet::Slice)
|
|
9
|
+
public_send(:"#{name}=", value)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Lml
|
|
7
|
+
class ImportResolver
|
|
8
|
+
def initialize(base_path)
|
|
9
|
+
@base_path = base_path
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def resolve(document)
|
|
13
|
+
entities = {}
|
|
14
|
+
associations = []
|
|
15
|
+
visited = Set.new
|
|
16
|
+
|
|
17
|
+
document.view_imports.each do |import|
|
|
18
|
+
resolve_import(import.path, entities, associations, visited, @base_path)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
collect_local_entities(document, entities, associations)
|
|
22
|
+
|
|
23
|
+
[entities.values, associations]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def resolve_import(path, entities, associations, visited, base_path)
|
|
29
|
+
base_dir = base_path ? File.dirname(base_path) : Dir.pwd
|
|
30
|
+
abs_pattern = File.expand_path(path, base_dir)
|
|
31
|
+
|
|
32
|
+
Dir.glob(abs_pattern).each do |file_path|
|
|
33
|
+
next if visited.include?(file_path)
|
|
34
|
+
visited.add(file_path)
|
|
35
|
+
|
|
36
|
+
if view_file?(file_path)
|
|
37
|
+
resolve_view_file(file_path, entities, associations, visited)
|
|
38
|
+
else
|
|
39
|
+
resolve_model_file(file_path, entities, associations)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def resolve_view_file(file_path, entities, associations, visited)
|
|
45
|
+
doc = Parser.parse_document(File.new(file_path))
|
|
46
|
+
|
|
47
|
+
collect_local_entities(doc, entities, associations)
|
|
48
|
+
|
|
49
|
+
doc.view_imports&.each do |import|
|
|
50
|
+
resolve_import(import.path, entities, associations, visited, file_path)
|
|
51
|
+
end
|
|
52
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
53
|
+
warn "Skipping #{file_path}: #{e.message}"
|
|
54
|
+
[]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def resolve_model_file(file_path, entities, associations)
|
|
58
|
+
content = File.read(file_path)
|
|
59
|
+
wrapped = "diagram Fragment {\n#{content}\n}"
|
|
60
|
+
doc = Parser.parse_document(StringIO.new(wrapped))
|
|
61
|
+
|
|
62
|
+
collect_local_entities(doc, entities, associations)
|
|
63
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
64
|
+
warn "Skipping #{file_path}: #{e.message}"
|
|
65
|
+
[]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def view_file?(file_path)
|
|
69
|
+
head = File.read(file_path, 200, encoding: "UTF-8")
|
|
70
|
+
head.match?(/\b(view|diagram)\s+\w/)
|
|
71
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def collect_local_entities(doc, entities, associations)
|
|
76
|
+
merge_entities(doc.classes, entities)
|
|
77
|
+
merge_entities(doc.enums, entities)
|
|
78
|
+
merge_entities(doc.data_types, entities)
|
|
79
|
+
associations.concat(doc.associations.to_a)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def merge_entities(collection, entities)
|
|
83
|
+
collection.each do |entity|
|
|
84
|
+
entities[entity.name] ||= entity
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Layout
|
|
5
|
+
class Engine
|
|
6
|
+
attr_accessor :input
|
|
7
|
+
|
|
8
|
+
def initialize(input:)
|
|
9
|
+
@input = input
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def render(_type)
|
|
13
|
+
raise ArgumentError, "Implement render method"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby-graphviz"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Lutaml
|
|
7
|
+
module Layout
|
|
8
|
+
class GraphVizEngine < Engine
|
|
9
|
+
def render(type)
|
|
10
|
+
Open3.popen3("dot -T#{type}") do |stdin, stdout, _stderr, _wait|
|
|
11
|
+
stdout.binmode
|
|
12
|
+
stdin.puts(input)
|
|
13
|
+
stdin.close
|
|
14
|
+
stdout.read
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|