suma 0.4.0 → 0.5.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.
- checksums.yaml +4 -4
- data/.github/workflows/rake.yml +3 -3
- data/.github/workflows/release.yml +1 -5
- data/.rubocop.yml +16 -12
- data/.rubocop_todo.yml +149 -178
- data/Gemfile +5 -1
- data/lib/suma/cli/build.rb +5 -0
- data/lib/suma/cli/core.rb +32 -4
- data/lib/suma/eengine_converter.rb +9 -8
- data/lib/suma/link_validator/step.rb +149 -0
- data/lib/suma/link_validator.rb +20 -97
- data/lib/suma/note_processor.rb +270 -0
- data/lib/suma/processor.rb +19 -3
- data/lib/suma/schema_comparer.rb +1 -1
- data/lib/suma/staged_collection_builder.rb +188 -0
- data/lib/suma/term_extractor.rb +9 -195
- data/lib/suma/version.rb +1 -1
- data/lib/suma.rb +2 -0
- metadata +6 -6
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "expressir"
|
|
4
|
+
|
|
5
|
+
module Suma
|
|
6
|
+
class LinkValidator
|
|
7
|
+
# Per-node-type navigation strategies for +validate_deep_path+.
|
|
8
|
+
#
|
|
9
|
+
# Each Step handles one EXPRESS construct (entity, type, ...) and
|
|
10
|
+
# knows how to walk one path segment from it. The dispatcher in
|
|
11
|
+
# LinkValidator picks the right Step by +handles?+.
|
|
12
|
+
#
|
|
13
|
+
# Adding a new navigable construct means adding a new Step class
|
|
14
|
+
# and registering it in REGISTRY — no edits to existing Steps or
|
|
15
|
+
# to validate_deep_path (open/closed principle).
|
|
16
|
+
module Step
|
|
17
|
+
Context = Struct.new(:file, :line, :link, :schema, :path,
|
|
18
|
+
keyword_init: true)
|
|
19
|
+
|
|
20
|
+
# Common failure-result builder. Step subclasses call this to
|
|
21
|
+
# surface an unresolved link.
|
|
22
|
+
module Failure
|
|
23
|
+
def failure(reason, context)
|
|
24
|
+
LinkValidationResult.new(
|
|
25
|
+
file: context.file,
|
|
26
|
+
line: context.line + 1,
|
|
27
|
+
link: context.link,
|
|
28
|
+
reason: reason,
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Navigates one path segment from an Entity by attribute name.
|
|
34
|
+
class Entity
|
|
35
|
+
include Failure
|
|
36
|
+
|
|
37
|
+
def self.handles?(node)
|
|
38
|
+
node.is_a?(Expressir::Model::Declarations::Entity)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def navigate(node, part, context)
|
|
42
|
+
attribute = node.attributes&.find { |a| a.id.downcase == part.downcase }
|
|
43
|
+
unless attribute
|
|
44
|
+
return failure("Attribute '#{part}' not found in entity '#{context.path}'",
|
|
45
|
+
context)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
attribute
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Navigates one path segment from a Type by either enumeration
|
|
53
|
+
# value (for enum types) or by re-targeting to the underlying
|
|
54
|
+
# named type (for non-enum types).
|
|
55
|
+
class Type
|
|
56
|
+
include Failure
|
|
57
|
+
|
|
58
|
+
PRIMITIVE_TYPE_NAMES = %w[INTEGER REAL STRING BOOLEAN NUMBER BINARY
|
|
59
|
+
LOGICAL].freeze
|
|
60
|
+
|
|
61
|
+
def self.handles?(node)
|
|
62
|
+
node.is_a?(Expressir::Model::Declarations::Type)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def navigate(node, part, context)
|
|
66
|
+
underlying = node.underlying_type
|
|
67
|
+
unless underlying
|
|
68
|
+
return failure("Cannot navigate deeper from type '#{context.path}'",
|
|
69
|
+
context)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if underlying.is_a?(Expressir::Model::DataTypes::Enumeration)
|
|
73
|
+
navigate_enum(underlying, part, context)
|
|
74
|
+
else
|
|
75
|
+
navigate_underlying(underlying, context)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def navigate_enum(underlying, part, context)
|
|
82
|
+
enum_value = underlying.items.find { |e| e.id.downcase == part.downcase }
|
|
83
|
+
unless enum_value
|
|
84
|
+
return failure("Enumeration value '#{part}' not found in type '#{context.path}'",
|
|
85
|
+
context)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
enum_value
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def navigate_underlying(underlying, context)
|
|
92
|
+
base_type = resolve_base_type(context.schema, underlying)
|
|
93
|
+
unless base_type
|
|
94
|
+
return failure("Base type not found for '#{context.path}'",
|
|
95
|
+
context)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
base_type
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def resolve_base_type(schema, type_ref)
|
|
102
|
+
return nil if PRIMITIVE_TYPE_NAMES.include?(type_ref.to_s.upcase)
|
|
103
|
+
return find_schema_element(schema, type_ref) if type_ref.is_a?(String)
|
|
104
|
+
|
|
105
|
+
type_ref if type_ref.is_a?(Expressir::Model::ModelElement)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def find_schema_element(schema, name)
|
|
109
|
+
SchemaElementLookup.find(schema, name)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Lookup helper shared across Step classes that need to resolve
|
|
114
|
+
# a named schema element (used by Type for underlying-type
|
|
115
|
+
# resolution). Lives behind its own module so Steps don't reach
|
|
116
|
+
# into LinkValidator's private methods.
|
|
117
|
+
module SchemaElementLookup
|
|
118
|
+
def self.find(schema, name)
|
|
119
|
+
collections(schema).each do |collection|
|
|
120
|
+
element = collection&.find { |e| e.id.downcase == name.downcase }
|
|
121
|
+
return element if element
|
|
122
|
+
end
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.collections(schema)
|
|
127
|
+
[
|
|
128
|
+
schema.entities,
|
|
129
|
+
schema.types,
|
|
130
|
+
schema.constants,
|
|
131
|
+
schema.functions,
|
|
132
|
+
schema.rules,
|
|
133
|
+
schema.procedures,
|
|
134
|
+
schema.subtype_constraints,
|
|
135
|
+
]
|
|
136
|
+
end
|
|
137
|
+
private_class_method :collections
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
REGISTRY = [Entity, Type].freeze
|
|
141
|
+
|
|
142
|
+
# Returns the Step class that handles +node+, or +nil+ if no
|
|
143
|
+
# registered Step matches.
|
|
144
|
+
def self.for(node)
|
|
145
|
+
REGISTRY.find { |step| step.handles?(node) }
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
data/lib/suma/link_validator.rb
CHANGED
|
@@ -8,6 +8,8 @@ module Suma
|
|
|
8
8
|
keyword_init: true)
|
|
9
9
|
|
|
10
10
|
class LinkValidator
|
|
11
|
+
autoload :Step, "suma/link_validator/step"
|
|
12
|
+
|
|
11
13
|
def initialize(index)
|
|
12
14
|
@index = index
|
|
13
15
|
end
|
|
@@ -100,113 +102,34 @@ module Suma
|
|
|
100
102
|
end
|
|
101
103
|
|
|
102
104
|
def validate_deep_path(schema, element, path_parts, file, line_idx,
|
|
103
|
-
full_link)
|
|
105
|
+
full_link)
|
|
104
106
|
current = element
|
|
105
107
|
current_path = "#{schema.id}.#{element.id}"
|
|
108
|
+
context = Step::Context.new(file: file, line: line_idx, link: full_link,
|
|
109
|
+
schema: schema, path: current_path)
|
|
106
110
|
|
|
107
111
|
path_parts.each do |part|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
attribute = current.attributes&.find do |a|
|
|
111
|
-
a.id.downcase == part.downcase
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
unless attribute
|
|
115
|
-
return LinkValidationResult.new(
|
|
116
|
-
file: file,
|
|
117
|
-
line: line_idx + 1,
|
|
118
|
-
link: full_link,
|
|
119
|
-
reason: "Attribute '#{part}' not found in entity '#{current_path}'",
|
|
120
|
-
)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
current = attribute
|
|
124
|
-
current_path += ".#{part}"
|
|
125
|
-
|
|
126
|
-
when Expressir::Model::Declarations::Type
|
|
127
|
-
underlying = current.underlying_type
|
|
128
|
-
|
|
129
|
-
if underlying.is_a?(Expressir::Model::DataTypes::Enumeration)
|
|
130
|
-
enum_value = underlying.items.find do |e|
|
|
131
|
-
e.id.downcase == part.downcase
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
unless enum_value
|
|
135
|
-
return LinkValidationResult.new(
|
|
136
|
-
file: file,
|
|
137
|
-
line: line_idx + 1,
|
|
138
|
-
link: full_link,
|
|
139
|
-
reason: "Enumeration value '#{part}' not found in type '#{current_path}'",
|
|
140
|
-
)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
current = enum_value
|
|
144
|
-
current_path += ".#{part}"
|
|
145
|
-
|
|
146
|
-
elsif underlying
|
|
147
|
-
base_type = find_base_type(schema, underlying)
|
|
148
|
-
|
|
149
|
-
unless base_type
|
|
150
|
-
return LinkValidationResult.new(
|
|
151
|
-
file: file,
|
|
152
|
-
line: line_idx + 1,
|
|
153
|
-
link: full_link,
|
|
154
|
-
reason: "Base type not found for '#{current_path}'",
|
|
155
|
-
)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
current = base_type
|
|
159
|
-
|
|
160
|
-
else
|
|
161
|
-
return LinkValidationResult.new(
|
|
162
|
-
file: file,
|
|
163
|
-
line: line_idx + 1,
|
|
164
|
-
link: full_link,
|
|
165
|
-
reason: "Cannot navigate deeper from type '#{current_path}'",
|
|
166
|
-
)
|
|
167
|
-
end
|
|
112
|
+
step = Step.for(current)
|
|
113
|
+
return unsupported_step_failure(context) unless step
|
|
168
114
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
)
|
|
176
|
-
end
|
|
115
|
+
outcome = step.new.navigate(current, part, context)
|
|
116
|
+
return outcome if outcome.is_a?(LinkValidationResult)
|
|
117
|
+
|
|
118
|
+
current = outcome
|
|
119
|
+
current_path += ".#{part}"
|
|
120
|
+
context.path = current_path
|
|
177
121
|
end
|
|
178
122
|
|
|
179
123
|
nil
|
|
180
124
|
end
|
|
181
125
|
|
|
182
|
-
def
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
type_ref
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def find_schema_element(schema, element_name)
|
|
194
|
-
[
|
|
195
|
-
schema.entities,
|
|
196
|
-
schema.types,
|
|
197
|
-
schema.constants,
|
|
198
|
-
schema.functions,
|
|
199
|
-
schema.rules,
|
|
200
|
-
schema.procedures,
|
|
201
|
-
schema.subtype_constraints,
|
|
202
|
-
].each do |collection|
|
|
203
|
-
element = collection&.find do |e|
|
|
204
|
-
e.id.downcase == element_name.downcase
|
|
205
|
-
end
|
|
206
|
-
return element if element
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
nil
|
|
126
|
+
def unsupported_step_failure(context)
|
|
127
|
+
LinkValidationResult.new(
|
|
128
|
+
file: context.file,
|
|
129
|
+
line: context.line + 1,
|
|
130
|
+
link: context.link,
|
|
131
|
+
reason: "Cannot navigate deeper from '#{context.path}'",
|
|
132
|
+
)
|
|
210
133
|
end
|
|
211
134
|
end
|
|
212
135
|
end
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "glossarist"
|
|
4
|
+
|
|
5
|
+
module Suma
|
|
6
|
+
# Cleans raw EXPRESS entity remarks into a list of
|
|
7
|
+
# +Glossarist::V3::DetailedDefinition+ notes.
|
|
8
|
+
#
|
|
9
|
+
# Pipeline:
|
|
10
|
+
# 1. trim to first sentence / first complete list
|
|
11
|
+
# 2. convert express xrefs to URN mentions
|
|
12
|
+
# 3. drop redundant "X is a type of {{...}}" opening notes
|
|
13
|
+
# 4. drop notes containing image references or <<...>> blocks
|
|
14
|
+
# 5. drop notes that duplicate the first definition
|
|
15
|
+
#
|
|
16
|
+
# Pure: no I/O, no schema knowledge. The schema domain label and
|
|
17
|
+
# URN composition are injected via +xref_to_mention+ so this module
|
|
18
|
+
# stays independent of +Suma::Urn+ and the surrounding extractor.
|
|
19
|
+
class NoteProcessor
|
|
20
|
+
REDUNDANT_NOTE_REGEX =
|
|
21
|
+
%r{
|
|
22
|
+
^An? # Starts with "A" or "An"
|
|
23
|
+
\s.*?\sis\sa\stype\sof # Text followed by "is a type of"
|
|
24
|
+
(\sa|\san)? # Optional " a" or " an"
|
|
25
|
+
\s\{\{[^\}]*\}\} # Text in double curly braces
|
|
26
|
+
\s*?\.?$ # Optional whitespace and period at the end
|
|
27
|
+
}x
|
|
28
|
+
|
|
29
|
+
IMAGE_REF_SUBSTRING = "image::"
|
|
30
|
+
DOUBLE_ANGLE_PATTERN = /<<(.*?){1,999}>>/
|
|
31
|
+
SEE_TAIL_PATTERN = /\s+\(see(.*?){1,999}\)/
|
|
32
|
+
XREF_WITH_DISPLAY = /<<express:([\w.]+),([\w. ][\w. ]*)>>/
|
|
33
|
+
XREF_WITHOUT_DISPLAY = /<<express:([\w.]+)>>/
|
|
34
|
+
INLINE_COMMENT_PATTERN = /\n\/\/.*?\n/
|
|
35
|
+
BULLET_LIST_PATTERN = /^\s*[*\-+]\s+/
|
|
36
|
+
NUMBERED_LIST_PATTERN = /^\s*\d+\.\s+/
|
|
37
|
+
CONTINUATION_PLUS = /^\+\s*$/
|
|
38
|
+
CONTINUATION_DASHES = /^--\s*$/
|
|
39
|
+
INDENTED_PATTERN = /^\s{2,}/
|
|
40
|
+
SENTENCE_BREAK_NEWLINE = /\.\n/
|
|
41
|
+
SENTENCE_BREAK_SPACE = /\. /
|
|
42
|
+
|
|
43
|
+
attr_reader :remarks, :definitions, :xref_to_mention
|
|
44
|
+
|
|
45
|
+
def initialize(remarks, definitions:, xref_to_mention:)
|
|
46
|
+
@remarks = remarks
|
|
47
|
+
@definitions = definitions
|
|
48
|
+
@xref_to_mention = xref_to_mention
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.call(remarks, definitions:, xref_to_mention:)
|
|
52
|
+
new(remarks, definitions: definitions, xref_to_mention: xref_to_mention).call
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def call
|
|
56
|
+
notes = build_initial_notes
|
|
57
|
+
notes = only_keep_first_sentence(notes)
|
|
58
|
+
notes = remove_see_content(notes)
|
|
59
|
+
notes = remove_redundant_note(notes)
|
|
60
|
+
notes = remove_invalid_references(notes)
|
|
61
|
+
compare_with_definitions(notes, definitions)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def build_initial_notes
|
|
67
|
+
trimmed = trim_definition(remarks)
|
|
68
|
+
return [] unless trimmed && !trimmed.empty?
|
|
69
|
+
|
|
70
|
+
[Glossarist::V3::DetailedDefinition.new(content: trimmed)]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def trim_definition(definition)
|
|
74
|
+
return nil if definition.nil? || definition.empty?
|
|
75
|
+
|
|
76
|
+
definition_str = array_to_paragraphs(definition)
|
|
77
|
+
return nil if definition_str.empty?
|
|
78
|
+
|
|
79
|
+
paragraphs = definition_str.split("\n\n")
|
|
80
|
+
combine_paragraphs(paragraphs)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def array_to_paragraphs(definition)
|
|
84
|
+
definition.is_a?(Array) ? definition.join("\n\n") : definition.to_s
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def combine_paragraphs(paragraphs)
|
|
88
|
+
combined = first_block(paragraphs)
|
|
89
|
+
combined = "#{combined}\n"
|
|
90
|
+
combined = combined.gsub(INLINE_COMMENT_PATTERN, "\n")
|
|
91
|
+
combined.strip!
|
|
92
|
+
convert_xref(combined)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def first_block(paragraphs)
|
|
96
|
+
first_paragraph = paragraphs.first
|
|
97
|
+
return apply_first_sentence_logic(first_paragraph) unless list_header?(paragraphs)
|
|
98
|
+
|
|
99
|
+
complete_list = extract_complete_list(paragraphs, 1)
|
|
100
|
+
"#{first_paragraph}\n\n#{complete_list}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def list_header?(paragraphs)
|
|
104
|
+
paragraphs.length > 1 &&
|
|
105
|
+
paragraphs.first.end_with?(":") &&
|
|
106
|
+
starts_with_list?(paragraphs[1])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def apply_first_sentence_logic(paragraph)
|
|
110
|
+
new_content = paragraph
|
|
111
|
+
.split(SENTENCE_BREAK_NEWLINE).first.strip
|
|
112
|
+
.split(SENTENCE_BREAK_SPACE).first.strip
|
|
113
|
+
|
|
114
|
+
new_content.end_with?(".") ? new_content : "#{new_content}."
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def extract_complete_list(paragraphs, start_index)
|
|
118
|
+
return paragraphs[start_index] if start_index >= paragraphs.length
|
|
119
|
+
|
|
120
|
+
reduce_list_paragraphs(paragraphs, start_index)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def reduce_list_paragraphs(paragraphs, start_index)
|
|
124
|
+
combined = paragraphs[start_index].dup
|
|
125
|
+
state = ContinuationState.new(combined.include?("--") && !combined.match?(/--.*--/m))
|
|
126
|
+
|
|
127
|
+
((start_index + 1)...paragraphs.length).each do |i|
|
|
128
|
+
break unless apply_paragraph_step?(combined, paragraphs[i], state)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
combined
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Returns true if iteration should continue, false to stop.
|
|
135
|
+
def apply_paragraph_step?(combined, para, state)
|
|
136
|
+
case classify_paragraph(para, state)
|
|
137
|
+
when :continuation_toggle
|
|
138
|
+
state.toggle!
|
|
139
|
+
append!(combined, para)
|
|
140
|
+
when :continuation
|
|
141
|
+
append!(combined, para)
|
|
142
|
+
when :list_member
|
|
143
|
+
append!(combined, para)
|
|
144
|
+
state.toggle! if opens_continuation_block?(para)
|
|
145
|
+
else
|
|
146
|
+
return false
|
|
147
|
+
end
|
|
148
|
+
true
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def append!(combined, para)
|
|
152
|
+
combined << "\n\n#{para}"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def opens_continuation_block?(para)
|
|
156
|
+
para.include?("--") && !para.match?(/--.*--/m)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def classify_paragraph(para, state)
|
|
160
|
+
return :continuation_toggle if para.match?(CONTINUATION_DASHES) || para.end_with?("--")
|
|
161
|
+
return :continuation if state.in_block?
|
|
162
|
+
return :list_member if starts_with_list?(para) || list_continuation?(para)
|
|
163
|
+
|
|
164
|
+
:stop
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Mutable flag tracking whether we are inside a "-- ... --"
|
|
168
|
+
# continuation block while walking paragraphs.
|
|
169
|
+
class ContinuationState
|
|
170
|
+
def initialize(in_block)
|
|
171
|
+
@in_block = in_block
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def in_block?
|
|
175
|
+
@in_block
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def toggle!
|
|
179
|
+
@in_block = !@in_block
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
private_constant :ContinuationState
|
|
183
|
+
|
|
184
|
+
def list_continuation?(content)
|
|
185
|
+
return false if content.nil? || content.empty?
|
|
186
|
+
|
|
187
|
+
content.match?(CONTINUATION_PLUS) ||
|
|
188
|
+
content.match?(CONTINUATION_DASHES) ||
|
|
189
|
+
content.match?(INDENTED_PATTERN) ||
|
|
190
|
+
content.start_with?("which", "where", "that")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def starts_with_list?(content)
|
|
194
|
+
return false if content.nil? || content.empty?
|
|
195
|
+
|
|
196
|
+
content.match?(BULLET_LIST_PATTERN) || content.match?(NUMBERED_LIST_PATTERN)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def only_keep_first_sentence(notes)
|
|
200
|
+
notes.each do |note|
|
|
201
|
+
next unless note&.content
|
|
202
|
+
next if preserve_complete_structure?(note.content)
|
|
203
|
+
|
|
204
|
+
new_content = note.content
|
|
205
|
+
.split(SENTENCE_BREAK_NEWLINE).first.strip
|
|
206
|
+
.split(SENTENCE_BREAK_SPACE).first.strip
|
|
207
|
+
note.content = new_content.end_with?(".") ? new_content : "#{new_content}."
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def preserve_complete_structure?(content)
|
|
212
|
+
return false if content.nil? || content.empty?
|
|
213
|
+
|
|
214
|
+
lines = content.split("\n")
|
|
215
|
+
return false unless first_line_is_header?(lines)
|
|
216
|
+
return false if first_line_has_period?(lines.first.strip)
|
|
217
|
+
|
|
218
|
+
remaining_content = lines[1..].join("\n")
|
|
219
|
+
starts_with_list?(remaining_content.strip)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def first_line_is_header?(lines)
|
|
223
|
+
lines.first&.strip&.end_with?(":") && lines.length > 1
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def first_line_has_period?(first_line)
|
|
227
|
+
first_line.count(".").positive?
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def remove_see_content(notes)
|
|
231
|
+
notes.each do |note|
|
|
232
|
+
note.content = note.content.gsub(SEE_TAIL_PATTERN, "")
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def remove_redundant_note(notes)
|
|
237
|
+
notes.reject do |note|
|
|
238
|
+
note.content.match?(REDUNDANT_NOTE_REGEX) &&
|
|
239
|
+
!note.content.include?("\n")
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def remove_invalid_references(notes)
|
|
244
|
+
notes.reject do |note|
|
|
245
|
+
note.content.include?(IMAGE_REF_SUBSTRING) ||
|
|
246
|
+
note.content.match?(DOUBLE_ANGLE_PATTERN)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def compare_with_definitions(notes, definitions)
|
|
251
|
+
return [] if notes&.first&.content == definitions&.first&.content
|
|
252
|
+
|
|
253
|
+
notes
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def convert_xref(content)
|
|
257
|
+
content
|
|
258
|
+
.gsub(XREF_WITHOUT_DISPLAY) do |_match|
|
|
259
|
+
full_ref = Regexp.last_match(1)
|
|
260
|
+
display = full_ref.split(".").last
|
|
261
|
+
xref_to_mention.call(full_ref, display)
|
|
262
|
+
end
|
|
263
|
+
.gsub(XREF_WITH_DISPLAY) do |_match|
|
|
264
|
+
full_ref = Regexp.last_match(1)
|
|
265
|
+
display = Regexp.last_match(2)
|
|
266
|
+
xref_to_mention.call(full_ref, display)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
data/lib/suma/processor.rb
CHANGED
|
@@ -4,15 +4,21 @@ require "metanorma"
|
|
|
4
4
|
|
|
5
5
|
module Suma
|
|
6
6
|
class Processor
|
|
7
|
+
# Emitted collection manifest that both the normal and staged builds render.
|
|
8
|
+
COLLECTION_OUTPUT_PATH = "collection-output.yaml"
|
|
9
|
+
|
|
7
10
|
attr_reader :metanorma_yaml_path, :output_directory, :schemas_all_path,
|
|
8
11
|
:compile_flag
|
|
9
12
|
|
|
10
13
|
def initialize(metanorma_yaml_path:, schemas_all_path:, compile: true,
|
|
11
|
-
output_directory: "_site")
|
|
14
|
+
output_directory: "_site", staged: false)
|
|
12
15
|
@metanorma_yaml_path = metanorma_yaml_path
|
|
13
16
|
@schemas_all_path = schemas_all_path
|
|
14
17
|
@compile_flag = compile
|
|
15
18
|
@output_directory = output_directory
|
|
19
|
+
# Opt-in memory-bounded staged build (metanorma/suma#94); the default
|
|
20
|
+
# single-process build below is unchanged when false.
|
|
21
|
+
@staged = staged
|
|
16
22
|
end
|
|
17
23
|
|
|
18
24
|
def run
|
|
@@ -64,11 +70,21 @@ module Suma
|
|
|
64
70
|
collection_config, output_directory
|
|
65
71
|
)
|
|
66
72
|
|
|
67
|
-
|
|
73
|
+
if @staged
|
|
74
|
+
# build_collection has written the emitted manifest; stage each member in
|
|
75
|
+
# its own process, then reinflate. Bounds peak memory to a single member.
|
|
76
|
+
StagedCollectionBuilder.new(
|
|
77
|
+
collection_config_path: COLLECTION_OUTPUT_PATH,
|
|
78
|
+
output_directory: output_directory,
|
|
79
|
+
coverpage: collection_opts[:coverpage],
|
|
80
|
+
).build
|
|
81
|
+
else
|
|
82
|
+
metanorma_collection.render(collection_opts)
|
|
83
|
+
end
|
|
68
84
|
end
|
|
69
85
|
|
|
70
86
|
def build_collection(collection_config, output_directory)
|
|
71
|
-
new_collection_config_path =
|
|
87
|
+
new_collection_config_path = COLLECTION_OUTPUT_PATH
|
|
72
88
|
ManifestTraverser.new(collection_config.manifest).remove_schemas_only_sources
|
|
73
89
|
collection_config.to_file(new_collection_config_path)
|
|
74
90
|
|
data/lib/suma/schema_comparer.rb
CHANGED
|
@@ -91,7 +91,7 @@ module Suma
|
|
|
91
91
|
existing_schema = Expressir::Changes::SchemaChange.from_file(output_path)
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
-
converter = EengineConverter.new(
|
|
94
|
+
converter = EengineConverter.new(schema_name, File.read(xml_path))
|
|
95
95
|
change_schema = converter.convert(
|
|
96
96
|
version: options[:version],
|
|
97
97
|
existing_change_schema: existing_schema,
|