expressir 2.3.6 → 2.4.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake.yml +1 -1
  3. data/.gitignore +2 -0
  4. data/.rubocop_todo.yml +17 -12
  5. data/CHANGELOG.md +159 -0
  6. data/docs/_guides/ler/step-packages.adoc +385 -0
  7. data/lib/expressir/coverage.rb +4 -4
  8. data/lib/expressir/express/builder_registry.rb +3 -1
  9. data/lib/expressir/express/builders/expression_builder.rb +2 -2
  10. data/lib/expressir/express/builders/helpers.rb +2 -8
  11. data/lib/expressir/express/builders/qualifier_builder.rb +2 -1
  12. data/lib/expressir/express/formatter.rb +32 -164
  13. data/lib/expressir/express/formatters/data_types_formatter.rb +39 -6
  14. data/lib/expressir/express/formatters/declarations_formatter.rb +47 -8
  15. data/lib/expressir/express/formatters/expressions_formatter.rb +20 -5
  16. data/lib/expressir/express/formatters/literals_formatter.rb +11 -1
  17. data/lib/expressir/express/formatters/references_formatter.rb +10 -1
  18. data/lib/expressir/express/formatters/remark_formatter.rb +1 -89
  19. data/lib/expressir/express/formatters/remark_item_formatter.rb +5 -4
  20. data/lib/expressir/express/formatters/statements_formatter.rb +32 -9
  21. data/lib/expressir/express/formatters/supertype_expressions_formatter.rb +7 -3
  22. data/lib/expressir/express/hyperlink_formatter.rb +1 -3
  23. data/lib/expressir/express/line_map.rb +48 -0
  24. data/lib/expressir/express/model_visitor.rb +1 -1
  25. data/lib/expressir/express/pretty_formatter.rb +31 -108
  26. data/lib/expressir/express/remark_attacher.rb +144 -198
  27. data/lib/expressir/express/remark_scanner.rb +180 -0
  28. data/lib/expressir/express/schema_head_formatter.rb +1 -3
  29. data/lib/expressir/express.rb +2 -0
  30. data/lib/expressir/model/concerns.rb +6 -0
  31. data/lib/expressir/model/declarations/interface_item.rb +2 -0
  32. data/lib/expressir/model/declarations/interfaced_item.rb +3 -0
  33. data/lib/expressir/model/declarations/remark_item.rb +3 -0
  34. data/lib/expressir/model/declarations/schema.rb +7 -5
  35. data/lib/expressir/model/exp_file.rb +1 -0
  36. data/lib/expressir/model/identifier.rb +3 -1
  37. data/lib/expressir/model/model_element.rb +3 -3
  38. data/lib/expressir/model/repository.rb +8 -0
  39. data/lib/expressir/model/search_engine.rb +1 -1
  40. data/lib/expressir/package/reader.rb +9 -24
  41. data/lib/expressir/version.rb +1 -1
  42. metadata +6 -2
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module Express
5
+ # Scans EXPRESS source text and extracts all remarks (tail and embedded),
6
+ # correctly tracking embedded-remark nesting so that inline `--` inside
7
+ # a `(* ... *)` documentation block is NOT mistaken for a tail remark.
8
+ #
9
+ # Single responsibility: source string -> Array<Remark>. The scanner has
10
+ # no knowledge of the model; attaching remarks to model elements is the
11
+ # job of {RemarkAttacher}.
12
+ #
13
+ # Reference: ISO 10303-11 §7.1.6.
14
+ # tail_remark = '--' [ remark_tag ] { <any char except newline> } '\n'
15
+ # embedded_remark = '(*' [ remark_tag ]
16
+ # { <any char> | nested embedded_remark } '*)'
17
+ #
18
+ # The scanner is a single-pass state machine with three states:
19
+ # :top — outside any remark; look for `--` or `(*`.
20
+ # :in_tail — inside a tail remark; look only for the next newline.
21
+ # :in_embedded — inside one or more nested embedded remarks; look
22
+ # for the matching `*)` or a nested `(*`, ignoring `--`.
23
+ class RemarkScanner
24
+ # Immutable value object describing a single extracted remark.
25
+ Remark = Struct.new(:position, :line, :text, :tag, :format, keyword_init: true) do
26
+ def tail?
27
+ format == TAIL_FORMAT
28
+ end
29
+
30
+ def embedded?
31
+ format == EMBEDDED_FORMAT
32
+ end
33
+
34
+ def tagged?
35
+ !tag.nil? && !tag.empty?
36
+ end
37
+ end
38
+
39
+ # Format discriminator values.
40
+ TAIL_FORMAT = "tail"
41
+ EMBEDDED_FORMAT = "embedded"
42
+
43
+ # Lexical markers recognised by EXPRESS.
44
+ TAIL_MARKER = "--"
45
+ EMBEDDED_OPEN = "(*"
46
+ EMBEDDED_CLOSE = "*)"
47
+
48
+ # Tag forms (see ISO 10303-11 §7.1.6.3 Remark tag).
49
+ # IP-style informal-proposition tags are recognised only in tail remarks
50
+ # (matching historic behaviour); embedded remarks use the quoted form.
51
+ INFORMAL_PROPOSITION_TAG = /\A(IP\d+):\s*(.*)\z/m
52
+ QUOTED_TAG = /\A"([^"]*)"\s*(.*)\z/m
53
+
54
+ # @param source [String] EXPRESS source text (any encoding).
55
+ def initialize(source)
56
+ @source = source
57
+ @source_bytes = source.b
58
+ @line_map = LineMap.new(@source_bytes)
59
+ end
60
+
61
+ # Returns an Array of {Remark}, in source order (by byte position).
62
+ # @return [Array<Remark>]
63
+ def scan
64
+ run_state_machine
65
+ end
66
+
67
+ alias call scan
68
+
69
+ private
70
+
71
+ def run_state_machine
72
+ remarks = []
73
+ src = @source_bytes
74
+ pos = 0
75
+
76
+ state = :top
77
+ embedded_depth = 0
78
+ tail_start = nil # byte position of first content char after "--"
79
+ tail_line = nil
80
+ embedded_start = nil # byte position of first content char after "(*"
81
+ embedded_line = nil
82
+
83
+ while pos < src.bytesize
84
+ case state
85
+ when :in_tail
86
+ nl = src.index("\n", pos)
87
+ if nl
88
+ emit_tail(remarks, tail_start, nl, tail_line)
89
+ pos = nl + 1
90
+ state = :top
91
+ else
92
+ emit_tail(remarks, tail_start, src.bytesize, tail_line)
93
+ return remarks
94
+ end
95
+ when :in_embedded
96
+ closing = src.index(EMBEDDED_CLOSE, pos)
97
+ nesting = src.index(EMBEDDED_OPEN, pos)
98
+ if nesting && (!closing || nesting < closing)
99
+ embedded_depth += 1
100
+ pos = nesting + EMBEDDED_OPEN.bytesize
101
+ elsif closing
102
+ embedded_depth -= 1
103
+ pos = closing + EMBEDDED_CLOSE.bytesize
104
+ if embedded_depth.zero?
105
+ emit_embedded(remarks, embedded_start, closing, embedded_line)
106
+ state = :top
107
+ end
108
+ else
109
+ # Unclosed embedded remark — terminate scan gracefully.
110
+ return remarks
111
+ end
112
+ else # :top
113
+ next_embed = src.index(EMBEDDED_OPEN, pos)
114
+ next_tail = src.index(TAIL_MARKER, pos)
115
+ if next_embed.nil? && next_tail.nil?
116
+ return remarks
117
+ elsif next_embed && (next_tail.nil? || next_embed <= next_tail)
118
+ embedded_start = next_embed + EMBEDDED_OPEN.bytesize
119
+ embedded_line = @line_map.line_number(next_embed)
120
+ embedded_depth = 1
121
+ state = :in_embedded
122
+ pos = next_embed + EMBEDDED_OPEN.bytesize
123
+ else
124
+ tail_start = next_tail + TAIL_MARKER.bytesize
125
+ tail_line = @line_map.line_number(next_tail)
126
+ state = :in_tail
127
+ pos = next_tail + TAIL_MARKER.bytesize
128
+ end
129
+ end
130
+ end
131
+
132
+ remarks
133
+ end
134
+
135
+ def emit_tail(remarks, content_start, content_end, line)
136
+ # Slice content from the original (UTF-8) source so the returned text
137
+ # preserves the source encoding. Byte offsets come from the BINARY copy.
138
+ raw = @source.byteslice(content_start...content_end)
139
+ tag, content = parse_tail(raw.strip)
140
+ remarks << Remark.new(
141
+ position: content_start - TAIL_MARKER.bytesize,
142
+ line: line,
143
+ text: content,
144
+ tag: tag,
145
+ format: TAIL_FORMAT,
146
+ )
147
+ end
148
+
149
+ def emit_embedded(remarks, content_start, content_end, line)
150
+ raw = @source.byteslice(content_start...content_end)
151
+ tag, content = parse_embedded(raw)
152
+ remarks << Remark.new(
153
+ position: content_start - EMBEDDED_OPEN.bytesize,
154
+ line: line,
155
+ text: content,
156
+ tag: tag,
157
+ format: EMBEDDED_FORMAT,
158
+ )
159
+ end
160
+
161
+ # Tail remarks support both quoted tags ("schema.entity" ...) and the
162
+ # shorthand IP-tag form (IP1: ...). Returns [tag, content]; for untagged
163
+ # remarks tag is nil and content is the stripped text.
164
+ def parse_tail(text)
165
+ match = text.match(INFORMAL_PROPOSITION_TAG) || text.match(QUOTED_TAG)
166
+ match ? match.captures : [nil, text]
167
+ end
168
+
169
+ # Embedded remarks only use the quoted tag form.
170
+ def parse_embedded(content)
171
+ stripped = content.strip
172
+ if (m = stripped.match(QUOTED_TAG))
173
+ [m[1], m[2]]
174
+ else
175
+ [nil, stripped]
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -8,14 +8,12 @@ module Expressir
8
8
  module SchemaHeadFormatter
9
9
  # @!visibility private
10
10
  def self.included(mod)
11
- if !mod.superclass.private_method_defined?(:format_declarations_schema) || !mod.superclass.private_method_defined?(:format_declarations_schema_head)
11
+ unless mod.superclass <= Expressir::Express::Formatter
12
12
  raise Error::FormatterMethodMissingError.new("SchemaHeadFormatter",
13
13
  "format_declarations_schema/format_declarations_schema_head")
14
14
  end
15
15
  end
16
16
 
17
- private
18
-
19
17
  def format_declarations_schema(node)
20
18
  format_declarations_schema_head(node)
21
19
  end
@@ -12,10 +12,12 @@ module Expressir
12
12
  autoload :Formatter, "#{__dir__}/express/formatter"
13
13
  autoload :Formatters, "#{__dir__}/express/formatters"
14
14
  autoload :HyperlinkFormatter, "#{__dir__}/express/hyperlink_formatter"
15
+ autoload :LineMap, "#{__dir__}/express/line_map"
15
16
  autoload :ModelVisitor, "#{__dir__}/express/model_visitor"
16
17
  autoload :Parser, "#{__dir__}/express/parser"
17
18
  autoload :PrettyFormatter, "#{__dir__}/express/pretty_formatter"
18
19
  autoload :RemarkAttacher, "#{__dir__}/express/remark_attacher"
20
+ autoload :RemarkScanner, "#{__dir__}/express/remark_scanner"
19
21
  autoload :ResolveReferencesModelVisitor,
20
22
  "#{__dir__}/express/resolve_references_model_visitor"
21
23
  autoload :SchemaHeadFormatter, "#{__dir__}/express/schema_head_formatter"
@@ -2,10 +2,16 @@
2
2
 
3
3
  module Expressir
4
4
  module Model
5
+ # Marker for types that have an id attribute
6
+ module HasId; end
7
+
5
8
  # Marker for types that can have remark_items children
6
9
  # These are types where RemarkItem children can be created
7
10
  module HasRemarkItems; end
8
11
 
12
+ # Marker for types that have a `remarks` string collection
13
+ module HasRemarks; end
14
+
9
15
  # Marker for scope containers (can contain declarations)
10
16
  # Includes schemas, functions, procedures, rules, entities, types, and files
11
17
  module ScopeContainer; end
@@ -4,6 +4,8 @@ module Expressir
4
4
  # Specified in ISO 10303-11:2004
5
5
  # - section 11 Interface specification
6
6
  class InterfaceItem < ModelElement
7
+ include HasId
8
+
7
9
  attribute :ref, ModelElement
8
10
  attribute :id, :string
9
11
  attribute :_class, :string, default: -> { self.class.name }
@@ -4,6 +4,9 @@ module Expressir
4
4
  # Specified in ISO 10303-11:2004
5
5
  # - section 11 Interface specification
6
6
  class InterfacedItem < ModelElement
7
+ include HasId
8
+ include HasRemarks
9
+
7
10
  attribute :id, :string
8
11
  attribute :remarks, :string, collection: true
9
12
  attribute :remark_items, RemarkItem, collection: true
@@ -3,6 +3,9 @@ module Expressir
3
3
  module Declarations
4
4
  # Implicit item with remarks
5
5
  class RemarkItem < ModelElement
6
+ include HasId
7
+ include HasRemarks
8
+
6
9
  attribute :id, :string
7
10
  attribute :remarks, :string, collection: true
8
11
  attribute :_class, :string, default: -> { self.class.name }
@@ -70,7 +70,7 @@ module Expressir
70
70
  end
71
71
 
72
72
  def full_source
73
- Expressir::Express::Formatter.format(self)
73
+ @full_source ||= Expressir::Express::Formatter.format(self)
74
74
  end
75
75
 
76
76
  def formatted
@@ -78,11 +78,13 @@ module Expressir
78
78
  end
79
79
 
80
80
  def source
81
- formatter = Class.new(Expressir::Express::Formatter) do
82
- include Expressir::Express::SchemaHeadFormatter
83
- include Expressir::Express::HyperlinkFormatter
81
+ @source ||= begin
82
+ formatter = Class.new(Expressir::Express::Formatter) do
83
+ include Expressir::Express::SchemaHeadFormatter
84
+ include Expressir::Express::HyperlinkFormatter
85
+ end
86
+ formatter.format(self)
84
87
  end
85
- formatter.format(self)
86
88
  end
87
89
 
88
90
  private
@@ -11,6 +11,7 @@ module Expressir
11
11
  # A Repository contains multiple ExpFile instances when parsing multiple files,
12
12
  # or a single ExpFile when parsing a single file.
13
13
  class ExpFile < ModelElement
14
+ include HasId
14
15
  include ScopeContainer
15
16
 
16
17
  attribute :path, :string
@@ -2,8 +2,10 @@ module Expressir
2
2
  module Model
3
3
  module Identifier
4
4
  def self.included(mod)
5
- # Auto-include marker - all Identifier types can have remark_items
5
+ # Auto-include markers - all Identifier types have id and can have remark_items and remarks
6
+ mod.include(HasId)
6
7
  mod.include(HasRemarkItems)
8
+ mod.include(HasRemarks)
7
9
 
8
10
  mod.attribute :id, :string
9
11
  mod.attribute :remarks, :string, collection: true
@@ -120,7 +120,7 @@ module Expressir
120
120
  loop do
121
121
  # Skip adding the id if this is a RemarkItem that belongs to an InformalPropositionRule
122
122
  # and has the same id as its parent
123
- if !(current_node.is_a? References::SimpleReference) && current_node.class.method_defined?(:id) &&
123
+ if !(current_node.is_a? References::SimpleReference) && current_node.is_a?(HasId) &&
124
124
  !(is_a?(Declarations::RemarkItem) &&
125
125
  parent.is_a?(Declarations::InformalPropositionRule) &&
126
126
  id == parent.id)
@@ -200,7 +200,7 @@ module Expressir
200
200
  self.untagged_remarks ||= []
201
201
  return unless remark_info.is_a?(RemarkInfo)
202
202
 
203
- self.untagged_remarks << remark_info
203
+ untagged_remarks << remark_info
204
204
  end
205
205
 
206
206
  private
@@ -208,7 +208,7 @@ module Expressir
208
208
  # @return [nil]
209
209
  def attach_parent_to_children
210
210
  self.class.attributes.each_pair do |symbol, _lutaml_attr|
211
- value = send(symbol)
211
+ value = public_send(symbol)
212
212
 
213
213
  case value
214
214
  when Array
@@ -24,6 +24,14 @@ module Expressir
24
24
  # Index instances (lazy-loaded)
25
25
  attr_reader :entity_index, :type_index, :reference_index
26
26
 
27
+ # Restore deserialized indexes (used by Package::Reader)
28
+ def restore_indexes(entity_index: nil, type_index: nil,
29
+ reference_index: nil)
30
+ @entity_index = entity_index if entity_index
31
+ @type_index = type_index if type_index
32
+ @reference_index = reference_index if reference_index
33
+ end
34
+
27
35
  # Internal schema storage for direct manipulation
28
36
  attr_reader :_schemas
29
37
 
@@ -327,7 +327,7 @@ module Expressir
327
327
  # Guard against nil schemas array
328
328
  return [] unless schemas
329
329
 
330
- schemas.flat_map { |s| s.send(method) || [] }
330
+ schemas.flat_map { |s| s.public_send(method) || [] }
331
331
  end
332
332
 
333
333
  # Collect all attributes from entities
@@ -132,32 +132,17 @@ module Expressir
132
132
  # @param repository [Model::Repository] Repository instance
133
133
  # @return [void]
134
134
  def load_indexes(zip, repository)
135
- # Load entity index
136
- entity_index_entry = zip.find_entry("entity_index.marshal")
137
- if entity_index_entry
138
- repository.instance_variable_set(
139
- :@entity_index,
140
- Marshal.load(entity_index_entry.get_input_stream.read),
141
- )
135
+ indexes = {}
136
+
137
+ %w[entity_index type_index reference_index].each do |name|
138
+ entry = zip.find_entry("#{name}.marshal")
139
+ if entry
140
+ indexes[name.to_sym] =
141
+ Marshal.load(entry.get_input_stream.read)
142
+ end
142
143
  end
143
144
 
144
- # Load type index
145
- type_index_entry = zip.find_entry("type_index.marshal")
146
- if type_index_entry
147
- repository.instance_variable_set(
148
- :@type_index,
149
- Marshal.load(type_index_entry.get_input_stream.read),
150
- )
151
- end
152
-
153
- # Load reference index
154
- reference_index_entry = zip.find_entry("reference_index.marshal")
155
- if reference_index_entry
156
- repository.instance_variable_set(
157
- :@reference_index,
158
- Marshal.load(reference_index_entry.get_input_stream.read),
159
- )
160
- end
145
+ repository.restore_indexes(**indexes) unless indexes.empty?
161
146
  end
162
147
  end
163
148
  end
@@ -1,3 +1,3 @@
1
1
  module Expressir
2
- VERSION = "2.3.6".freeze
2
+ VERSION = "2.4.0".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: expressir
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.6
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-14 00:00:00.000000000 Z
11
+ date: 2026-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -233,6 +233,7 @@ files:
233
233
  - ".rspec"
234
234
  - ".rubocop.yml"
235
235
  - ".rubocop_todo.yml"
236
+ - CHANGELOG.md
236
237
  - Gemfile
237
238
  - README.adoc
238
239
  - Rakefile
@@ -263,6 +264,7 @@ files:
263
264
  - docs/_guides/ler/loading-packages.adoc
264
265
  - docs/_guides/ler/package-formats.adoc
265
266
  - docs/_guides/ler/querying-packages.adoc
267
+ - docs/_guides/ler/step-packages.adoc
266
268
  - docs/_guides/ler/validating-packages.adoc
267
269
  - docs/_guides/liquid/basic-templates.adoc
268
270
  - docs/_guides/liquid/documentation-generation.adoc
@@ -396,10 +398,12 @@ files:
396
398
  - lib/expressir/express/formatters/statements_formatter.rb
397
399
  - lib/expressir/express/formatters/supertype_expressions_formatter.rb
398
400
  - lib/expressir/express/hyperlink_formatter.rb
401
+ - lib/expressir/express/line_map.rb
399
402
  - lib/expressir/express/model_visitor.rb
400
403
  - lib/expressir/express/parser.rb
401
404
  - lib/expressir/express/pretty_formatter.rb
402
405
  - lib/expressir/express/remark_attacher.rb
406
+ - lib/expressir/express/remark_scanner.rb
403
407
  - lib/expressir/express/resolve_references_model_visitor.rb
404
408
  - lib/expressir/express/schema_head_formatter.rb
405
409
  - lib/expressir/express/streaming_builder.rb