lutaml-lml 0.3.3 → 0.3.5
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 +4 -4
- data/exe/lutaml-lml-lsp +11 -0
- data/lib/lutaml/lml/cli.rb +166 -17
- data/lib/lutaml/lml/data_processor/attribute_processing.rb +4 -2
- data/lib/lutaml/lml/data_processor/value_declaration_processing.rb +1 -0
- data/lib/lutaml/lml/data_processor.rb +8 -1
- data/lib/lutaml/lml/diagnostic.rb +149 -0
- data/lib/lutaml/lml/document_builder.rb +63 -21
- data/lib/lutaml/lml/format/adapter/standard_adapter.rb +22 -23
- data/lib/lutaml/lml/formatter/canonical.rb +736 -0
- data/lib/lutaml/lml/formatter.rb +2 -0
- data/lib/lutaml/lml/grammar/lml.artifact.json +2946 -2512
- data/lib/lutaml/lml/grammar/lml.parg +47 -9
- data/lib/lutaml/lml/grammar.rb +13 -0
- data/lib/lutaml/lml/import_resolver.rb +13 -4
- data/lib/lutaml/lml/lsp/conversions.rb +78 -0
- data/lib/lutaml/lml/lsp/keywords.rb +43 -0
- data/lib/lutaml/lml/lsp/server.rb +378 -0
- data/lib/lutaml/lml/lsp/store.rb +68 -0
- data/lib/lutaml/lml/lsp/transport.rb +53 -0
- data/lib/lutaml/lml/lsp.rb +16 -0
- data/lib/lutaml/lml/model_compiler/serialization_mappings.rb +36 -10
- data/lib/lutaml/lml/model_compiler.rb +25 -2
- data/lib/lutaml/lml/models/association.rb +3 -0
- data/lib/lutaml/lml/models/constraint.rb +3 -0
- data/lib/lutaml/lml/models/data_type.rb +3 -0
- data/lib/lutaml/lml/models/diagram.rb +3 -0
- data/lib/lutaml/lml/models/document.rb +10 -3
- data/lib/lutaml/lml/models/enum.rb +4 -1
- data/lib/lutaml/lml/models/instance.rb +3 -0
- data/lib/lutaml/lml/models/instance_collection.rb +3 -0
- data/lib/lutaml/lml/models/member_field.rb +2 -0
- data/lib/lutaml/lml/models/namespace.rb +2 -0
- data/lib/lutaml/lml/models/operation.rb +4 -1
- data/lib/lutaml/lml/models/package.rb +3 -0
- data/lib/lutaml/lml/models/primitive_type.rb +6 -3
- data/lib/lutaml/lml/models/serialization_mapping.rb +2 -0
- data/lib/lutaml/lml/models/top_element_attribute.rb +6 -0
- data/lib/lutaml/lml/models/uml_class.rb +6 -0
- data/lib/lutaml/lml/models/value.rb +3 -0
- data/lib/lutaml/lml/pipeline.rb +25 -8
- data/lib/lutaml/lml/positioned.rb +13 -0
- data/lib/lutaml/lml/preprocessor.rb +44 -13
- data/lib/lutaml/lml/quoting.rb +60 -0
- data/lib/lutaml/lml/source_position.rb +20 -0
- data/lib/lutaml/lml/symbol_index.rb +200 -0
- data/lib/lutaml/lml/transform.rb +49 -2
- data/lib/lutaml/lml/validator.rb +134 -21
- data/lib/lutaml/lml/version.rb +1 -1
- data/lib/lutaml/lml.rb +18 -1
- metadata +18 -4
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Lml
|
|
5
|
+
# Expands `include` directives before parsing. Unterminated block
|
|
6
|
+
# comments are rejected here; line comments are grammar trivia.
|
|
5
7
|
class Preprocessor
|
|
6
8
|
attr_reader :input_file
|
|
7
9
|
|
|
@@ -16,7 +18,7 @@ module Lutaml
|
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
def call
|
|
19
|
-
expand_lines(@input_file.read, @input_file.base_dir, [])
|
|
21
|
+
expand_lines(@input_file.read, @input_file.base_dir, [], @input_file.path)
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
private
|
|
@@ -25,24 +27,38 @@ module Lutaml
|
|
|
25
27
|
# skip (parsanol-ruby#134); only include directives are expanded
|
|
26
28
|
# here. Line structure is preserved so parse-error positions
|
|
27
29
|
# match the source.
|
|
28
|
-
def expand_lines(text, base_dir, chain)
|
|
29
|
-
validate_block_comments(text)
|
|
30
|
+
def expand_lines(text, base_dir, chain, file_path)
|
|
31
|
+
validate_block_comments(text, file_path)
|
|
30
32
|
text.split(/\r?\n/)
|
|
31
|
-
.
|
|
33
|
+
.each_with_index
|
|
34
|
+
.flat_map { |line, index| expand_line(line, base_dir, chain, file_path, index + 1) }
|
|
32
35
|
.join("\n")
|
|
33
36
|
end
|
|
34
37
|
|
|
35
|
-
def expand_line(line, base_dir, chain)
|
|
38
|
+
def expand_line(line, base_dir, chain, file_path, line_number)
|
|
36
39
|
path = include_path(line, base_dir)
|
|
37
40
|
return [line] unless path
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
if chain.include?(path)
|
|
43
|
+
raise Error.new(
|
|
44
|
+
"circular include detected: #{path}",
|
|
45
|
+
diagnostic: Diagnostic.build(
|
|
46
|
+
'parse/LML-003',
|
|
47
|
+
message: "circular include detected: #{path}",
|
|
48
|
+
file: file_path, line: line_number, column: 1
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
end
|
|
40
52
|
|
|
41
|
-
expand_lines(read_included(path
|
|
53
|
+
expand_lines(read_included(path, file_path, line_number),
|
|
54
|
+
File.dirname(path), chain + [path], path)
|
|
42
55
|
end
|
|
43
56
|
|
|
44
57
|
def include_path(line, base_dir)
|
|
45
|
-
match
|
|
58
|
+
# Linear-time match: `\S.*` cannot overlap the leading `[ \t]+`,
|
|
59
|
+
# so a line of spaces after `include` fails in O(n) rather than
|
|
60
|
+
# backtracking quadratically (CodeQL ReDoS).
|
|
61
|
+
match = line.match(/\A[ \t]*include[ \t]+(\S.*)\z/)
|
|
46
62
|
return nil unless match
|
|
47
63
|
|
|
48
64
|
File.expand_path(trim_trailing_comment(match[1].strip), base_dir)
|
|
@@ -52,17 +68,24 @@ module Lutaml
|
|
|
52
68
|
path.sub(%r{\s+(//|/\*).*}, '')
|
|
53
69
|
end
|
|
54
70
|
|
|
55
|
-
def read_included(path)
|
|
71
|
+
def read_included(path, file_path, line_number)
|
|
56
72
|
File.read(path, encoding: 'UTF-8')
|
|
57
|
-
rescue
|
|
58
|
-
raise Error
|
|
73
|
+
rescue SystemCallError => e
|
|
74
|
+
raise Error.new(
|
|
75
|
+
"cannot read include #{path}: #{e.message}",
|
|
76
|
+
diagnostic: Diagnostic.build(
|
|
77
|
+
'parse/LML-003',
|
|
78
|
+
message: "cannot read include: #{path}",
|
|
79
|
+
file: file_path, line: line_number, column: 1
|
|
80
|
+
),
|
|
81
|
+
)
|
|
59
82
|
end
|
|
60
83
|
|
|
61
84
|
# Quote-aware scan: a `/*` opened outside a string literal or a
|
|
62
85
|
# line comment must close before end of input. The grammar's
|
|
63
86
|
# trivia would otherwise reject the text with an unrelated
|
|
64
87
|
# expected-token error.
|
|
65
|
-
def validate_block_comments(text)
|
|
88
|
+
def validate_block_comments(text, file_path)
|
|
66
89
|
quote = nil
|
|
67
90
|
pos = 0
|
|
68
91
|
open_index = nil
|
|
@@ -92,7 +115,15 @@ module Lutaml
|
|
|
92
115
|
return unless open_index
|
|
93
116
|
|
|
94
117
|
line = text[0...open_index].count("\n") + 1
|
|
95
|
-
|
|
118
|
+
column = open_index - (text.rindex("\n", open_index) || -1)
|
|
119
|
+
raise Error.new(
|
|
120
|
+
"unterminated block comment at line #{line}",
|
|
121
|
+
diagnostic: Diagnostic.build(
|
|
122
|
+
'parse/LML-002',
|
|
123
|
+
message: 'unterminated block comment',
|
|
124
|
+
file: file_path, line: line, column: column
|
|
125
|
+
),
|
|
126
|
+
)
|
|
96
127
|
end
|
|
97
128
|
end
|
|
98
129
|
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# The single source of truth for scalar quoting on the emit side:
|
|
6
|
+
# bare only for runs of [A-Za-z0-9_] (the grammar's word rule);
|
|
7
|
+
# double quotes with the two RS 3001 escapes otherwise. Values,
|
|
8
|
+
# attribute keys, and the canonical form all emit through here.
|
|
9
|
+
module Quoting
|
|
10
|
+
BARE_WORD = /\A\w+\z/
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# @param scalar [String, #to_s]
|
|
15
|
+
# @return [String] the emitted form: parseable back to scalar
|
|
16
|
+
def quote_scalar(scalar)
|
|
17
|
+
str = scalar.to_s
|
|
18
|
+
return str if str.match?(BARE_WORD)
|
|
19
|
+
|
|
20
|
+
quote_always(str)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Double-quoted form even when the scalar is a bare word. Used
|
|
24
|
+
# where the grammar only accepts quoted strings (definitions,
|
|
25
|
+
# requires, instance data values, wire names).
|
|
26
|
+
#
|
|
27
|
+
# @param scalar [String, #to_s]
|
|
28
|
+
# @return [String] the double-quoted, escaped form
|
|
29
|
+
def quote_always(scalar)
|
|
30
|
+
escaped = scalar.to_s.gsub('\\') { |bs| bs * 2 }.gsub('"') { |q| "\\#{q}" }
|
|
31
|
+
"\"#{escaped}\""
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Float#to_s may use exponent notation, for which the language has
|
|
35
|
+
# no literal. Expand it to a plain decimal that parses back to the
|
|
36
|
+
# same Float.
|
|
37
|
+
#
|
|
38
|
+
# @param number [Numeric]
|
|
39
|
+
# @return [String] the emitted form
|
|
40
|
+
def number_scalar(number)
|
|
41
|
+
text = number.to_s
|
|
42
|
+
match = text.match(/\A(-?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)\z/)
|
|
43
|
+
return text unless match
|
|
44
|
+
|
|
45
|
+
integer = match[2]
|
|
46
|
+
digits = integer + match[3].to_s
|
|
47
|
+
point = integer.length + match[4].to_i
|
|
48
|
+
plain =
|
|
49
|
+
if point <= 0
|
|
50
|
+
"0.#{'0' * -point}#{digits}"
|
|
51
|
+
elsif point >= digits.length
|
|
52
|
+
"#{digits}#{'0' * (point - digits.length)}.0"
|
|
53
|
+
else
|
|
54
|
+
"#{digits[0...point]}.#{digits[point..]}"
|
|
55
|
+
end
|
|
56
|
+
"#{match[1]}#{plain}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# A position in the parsed source: 1-based line and column, with the
|
|
6
|
+
# byte offset and captured length when the parser provides them.
|
|
7
|
+
# Attached to model objects as non-serialized metadata so diagnostics
|
|
8
|
+
# can cite it without changing the document model's wire form.
|
|
9
|
+
class SourcePosition
|
|
10
|
+
attr_reader :line, :column, :offset, :length
|
|
11
|
+
|
|
12
|
+
def initialize(line:, column:, offset: nil, length: nil)
|
|
13
|
+
@line = line
|
|
14
|
+
@column = column
|
|
15
|
+
@offset = offset
|
|
16
|
+
@length = length
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# A flat, positioned index of the document's symbols and the uses
|
|
6
|
+
# that reference them. Backs navigation, hover, and completion in
|
|
7
|
+
# tooling, and is available as Document#symbols.
|
|
8
|
+
class SymbolIndex
|
|
9
|
+
Entry = Struct.new(:name, :kind, :container, :position, :detail, :record,
|
|
10
|
+
keyword_init: true)
|
|
11
|
+
|
|
12
|
+
# LSP SymbolKind values.
|
|
13
|
+
KINDS = {
|
|
14
|
+
package: 4, class: 5, method: 6, field: 8,
|
|
15
|
+
enum: 10, instance: 19, enum_member: 22, data_type: 23
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
attr_reader :entries
|
|
19
|
+
|
|
20
|
+
def initialize(document)
|
|
21
|
+
@entries = []
|
|
22
|
+
return unless document
|
|
23
|
+
|
|
24
|
+
walk_document(document)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def definitions(name)
|
|
28
|
+
@entries.select { |entry| entry.name.to_s == name.to_s }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def uses(name)
|
|
32
|
+
token = name.to_s
|
|
33
|
+
@entries.select do |entry|
|
|
34
|
+
if entry.record.is_a?(TopElementAttribute)
|
|
35
|
+
reference?(entry.record, token)
|
|
36
|
+
elsif entry.record.is_a?(Instance)
|
|
37
|
+
[entry.record.type.to_s, entry.record.isa.to_s].include?(token)
|
|
38
|
+
else
|
|
39
|
+
false
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def attribute_entries
|
|
45
|
+
@entries.select { |entry| entry.kind == :field }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def entity_entries
|
|
49
|
+
@entries.reject { |entry| %i[field method enum_member].include?(entry.kind) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def names
|
|
53
|
+
@entries.map(&:name).compact.uniq
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def walk_document(document)
|
|
59
|
+
walk_members(document, nil)
|
|
60
|
+
walk_instance(document.instance, nil) if document.instance
|
|
61
|
+
walk_collection(document.instances) if document.instances
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def walk_members(owner, container)
|
|
65
|
+
Array(owner.classes).each { |klass| walk_class(klass, container) }
|
|
66
|
+
Array(owner.enums).each { |enum| walk_enum(enum, container) }
|
|
67
|
+
Array(owner.data_types).each { |type| walk_data_type(type, container) }
|
|
68
|
+
Array(owner.packages).each { |package| walk_package(package, container) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def walk_package(package, container)
|
|
72
|
+
name = package.name.to_s
|
|
73
|
+
add(name, :package, container, package.source_position, nil, package)
|
|
74
|
+
walk_members(package, name)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def walk_class(klass, container)
|
|
78
|
+
name = klass.name.to_s
|
|
79
|
+
add(name, :class, container, klass.source_position, parent_detail(klass), klass)
|
|
80
|
+
Array(klass.attributes).each do |attribute|
|
|
81
|
+
add(attribute.name, :field, name, attribute.source_position,
|
|
82
|
+
attribute_detail(attribute), attribute)
|
|
83
|
+
end
|
|
84
|
+
Array(klass.operations).each do |operation|
|
|
85
|
+
add(operation.name, :method, name, operation.source_position,
|
|
86
|
+
operation_detail(operation), operation)
|
|
87
|
+
end
|
|
88
|
+
Array(klass.data_types).each { |type| walk_data_type(type, name) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def walk_enum(enum, container)
|
|
92
|
+
name = enum.name.to_s
|
|
93
|
+
add(name, :enum, container, enum.source_position, nil, enum)
|
|
94
|
+
Array(enum.attributes).each do |member|
|
|
95
|
+
add(member.name, :enum_member, name, member.source_position,
|
|
96
|
+
enum_member_detail(member), member)
|
|
97
|
+
end
|
|
98
|
+
Array(enum.member_fields).each do |field|
|
|
99
|
+
add(field.name, :field, name, field.source_position,
|
|
100
|
+
field.type.to_s, field)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def walk_data_type(type, container)
|
|
105
|
+
name = type.name.to_s
|
|
106
|
+
add(name, :data_type, container, type.source_position, nil, type)
|
|
107
|
+
Array(type.attributes).each do |attribute|
|
|
108
|
+
add(attribute.name, :field, name, attribute.source_position,
|
|
109
|
+
attribute_detail(attribute), attribute)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def walk_collection(collection)
|
|
114
|
+
return unless collection.is_a?(InstanceCollection)
|
|
115
|
+
|
|
116
|
+
name = collection.name.to_s
|
|
117
|
+
add(name, :package, nil, collection.source_position, 'instances', collection)
|
|
118
|
+
Array(collection.instances).each { |instance| walk_instance(instance, name) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def walk_instance(instance, container)
|
|
122
|
+
name = instance_label(instance)
|
|
123
|
+
add(name, :instance, container, instance.source_position, name, instance)
|
|
124
|
+
Array(instance.attributes).each do |attribute|
|
|
125
|
+
next if attribute.name.to_s.empty?
|
|
126
|
+
|
|
127
|
+
add(attribute.name, :field, name, attribute.source_position,
|
|
128
|
+
attribute_value_detail(attribute), attribute)
|
|
129
|
+
Array(attribute.instances).each { |nested| walk_instance(nested, name) }
|
|
130
|
+
end
|
|
131
|
+
walk_instance(instance.instance, name) if instance.instance
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def add(name, kind, container, position, detail, record)
|
|
135
|
+
return if name.to_s.empty? || position.nil?
|
|
136
|
+
|
|
137
|
+
@entries << Entry.new(
|
|
138
|
+
name: name.to_s, kind: kind, container: container,
|
|
139
|
+
position: position, detail: detail, record: record
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def parent_detail(klass)
|
|
144
|
+
klass.parent_class.to_s.empty? ? nil : "< #{klass.parent_class}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def attribute_detail(attribute)
|
|
148
|
+
detail = attribute.type.to_s.dup
|
|
149
|
+
cardinality = attribute.cardinality
|
|
150
|
+
if cardinality
|
|
151
|
+
max = cardinality.max.to_s
|
|
152
|
+
detail << " [#{cardinality.min}#{"..#{max}" unless max.empty?}]"
|
|
153
|
+
end
|
|
154
|
+
detail.empty? ? nil : detail
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def operation_detail(operation)
|
|
158
|
+
params = Array(operation.owned_parameter)
|
|
159
|
+
.map { |param| "#{param.name}: #{param.type}" }.join(', ')
|
|
160
|
+
detail = "(#{params})"
|
|
161
|
+
detail += " : #{operation.return_type}" if operation.return_type.to_s != ''
|
|
162
|
+
detail
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def enum_member_detail(member)
|
|
166
|
+
description = Array(member.attributes).find { |a| a.name == 'description' }
|
|
167
|
+
description&.type&.to_s
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def attribute_value_detail(attribute)
|
|
171
|
+
value = attribute.value
|
|
172
|
+
value = value.value if value.is_a?(LiteralValue)
|
|
173
|
+
value.nil? ? attribute.type.to_s : value.to_s
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def instance_label(instance)
|
|
177
|
+
type = instance.type.to_s
|
|
178
|
+
name = instance.name.to_s
|
|
179
|
+
return name unless name.empty?
|
|
180
|
+
return type unless type.empty?
|
|
181
|
+
|
|
182
|
+
'instance'
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def reference?(attribute, token)
|
|
186
|
+
type = attribute.type.to_s
|
|
187
|
+
return true if type == token || type.split('[]').first == token
|
|
188
|
+
|
|
189
|
+
value = attribute.value
|
|
190
|
+
value = value.value if value.is_a?(LiteralValue)
|
|
191
|
+
return false unless value.is_a?(Hash)
|
|
192
|
+
|
|
193
|
+
reference = value[:reference] || value['reference']
|
|
194
|
+
return false unless reference
|
|
195
|
+
|
|
196
|
+
reference.to_s.split('.').include?(token)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
data/lib/lutaml/lml/transform.rb
CHANGED
|
@@ -4,11 +4,13 @@ require 'parsanol'
|
|
|
4
4
|
|
|
5
5
|
module Lutaml
|
|
6
6
|
module Lml
|
|
7
|
+
# Converts the raw parsanol tree into the processed hash, retaining
|
|
8
|
+
# source positions as non-serialized metadata.
|
|
7
9
|
class Transform < Parsanol::Transform
|
|
8
10
|
VISIBILITY_MAP = {
|
|
9
11
|
'-' => 'private',
|
|
10
12
|
'#' => 'protected',
|
|
11
|
-
'~' => 'package'
|
|
13
|
+
'~' => 'package',
|
|
12
14
|
}.freeze
|
|
13
15
|
|
|
14
16
|
rule(visibility_modifier: simple(:visibility_value)) do
|
|
@@ -50,7 +52,52 @@ module Lutaml
|
|
|
50
52
|
BLOCK_KEYS = %i[definition comments].freeze
|
|
51
53
|
|
|
52
54
|
def apply(tree, context = nil)
|
|
53
|
-
|
|
55
|
+
processed = super
|
|
56
|
+
attach_positions(tree, processed)
|
|
57
|
+
strip_block_layout(processed)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Retains the source position of name/key captures as a :position
|
|
61
|
+
# hash beside the transformed value. The document builder moves it
|
|
62
|
+
# onto the model as non-serialized metadata (see Positioned).
|
|
63
|
+
def attach_positions(raw, processed)
|
|
64
|
+
case raw
|
|
65
|
+
when Hash
|
|
66
|
+
return unless processed.is_a?(Hash)
|
|
67
|
+
|
|
68
|
+
slice = capture_slice(raw[:name]) || capture_slice(raw[:key]) ||
|
|
69
|
+
capture_slice(raw[:value_name]) || capture_slice(raw[:lml_version])
|
|
70
|
+
processed[:position] = position_hash(slice) if slice
|
|
71
|
+
|
|
72
|
+
raw.each_key do |key|
|
|
73
|
+
attach_positions(raw[key], processed[key]) if processed.key?(key)
|
|
74
|
+
end
|
|
75
|
+
when Array
|
|
76
|
+
return unless processed.is_a?(Array)
|
|
77
|
+
|
|
78
|
+
raw.each_with_index do |item, index|
|
|
79
|
+
attach_positions(item, processed[index]) if index < processed.length
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# A name capture is either a bare token (a Slice) or a quoted
|
|
85
|
+
# string (a hash holding the inner Slice).
|
|
86
|
+
def capture_slice(node)
|
|
87
|
+
return node if node.is_a?(Parsanol::Slice)
|
|
88
|
+
return node[:string] if node.is_a?(Hash) && node[:string].is_a?(Parsanol::Slice)
|
|
89
|
+
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def position_hash(slice)
|
|
94
|
+
line, column = slice.line_and_column
|
|
95
|
+
{
|
|
96
|
+
line: line,
|
|
97
|
+
column: column,
|
|
98
|
+
offset: slice.offset,
|
|
99
|
+
length: slice.content.to_s.bytesize,
|
|
100
|
+
}
|
|
54
101
|
end
|
|
55
102
|
|
|
56
103
|
def strip_block_layout(node)
|