suma 0.4.0 → 0.5.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 +4 -4
- 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/schema_comparer.rb +1 -1
- data/lib/suma/term_extractor.rb +9 -195
- data/lib/suma/version.rb +1 -1
- data/lib/suma.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 165792c4ce4a7a5a23cf0c8fdc649882f68235e65fcc6890a0d11ee5997c063a
|
|
4
|
+
data.tar.gz: 05fdff4f378207ba636169838b4d3631fcfd30009f254ab5eed85a191c12ec5a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 227dd455539080a5b257f1d5cbb563c5a2fe78856d7eaac4035402060f9415dcbb80ec87928a971a77eccf8585e23c0f7d9e9fd866a8a2cea4471bb098f40ed5
|
|
7
|
+
data.tar.gz: 47b7edf861d91dc264acd794d6ffdeb1c952aa83da822da3816e450429f81104088d3af68322245a9c70ecf920bcda1762c916e049f0c16c67208f6035960b8f
|
data/lib/suma/cli/core.rb
CHANGED
|
@@ -40,14 +40,42 @@ module Suma
|
|
|
40
40
|
desc "extract-terms SCHEMA_MANIFEST_FILE GLOSSARIST_OUTPUT_PATH",
|
|
41
41
|
"Extract terms from SCHEMA_MANIFEST_FILE into " \
|
|
42
42
|
"Glossarist v3 format"
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
option :urn, type: :string, required: true, aliases: "-u",
|
|
44
|
+
desc: "URN for the dataset source " \
|
|
45
|
+
"(used for section references)"
|
|
46
|
+
option :language_code, type: :string, default: "eng", aliases: "-l",
|
|
47
|
+
desc: "Language code for the Glossarist"
|
|
48
|
+
def extract_terms(schema_manifest_file, glossarist_output_path)
|
|
49
|
+
TermExtractor.new(
|
|
50
|
+
schema_manifest_file,
|
|
51
|
+
glossarist_output_path,
|
|
52
|
+
urn: options[:urn],
|
|
53
|
+
language_code: options[:language_code],
|
|
54
|
+
).call
|
|
45
55
|
end
|
|
46
56
|
|
|
47
57
|
desc "generate-register SCHEMA_MANIFEST_FILE OUTPUT_PATH",
|
|
48
58
|
"Generate a Glossarist register.yaml with hierarchical sections"
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
option :urn, type: :string, required: true, aliases: "-u",
|
|
60
|
+
desc: "URN prefix for the dataset"
|
|
61
|
+
option :id, type: :string, required: true,
|
|
62
|
+
desc: "Dataset identifier (e.g. iso10303-2-express)"
|
|
63
|
+
option :ref, type: :string, required: true,
|
|
64
|
+
desc: "Human-readable reference label"
|
|
65
|
+
option :language_code, type: :string, default: "eng", aliases: "-l",
|
|
66
|
+
desc: "Language code for section names"
|
|
67
|
+
option :owner, type: :string, default: Suma::RegisterManifestGenerator::DEFAULT_OWNER,
|
|
68
|
+
desc: "Owner of the dataset (e.g. 'ISO/TC 184/SC 4')"
|
|
69
|
+
def generate_register(schema_manifest_file, output_path)
|
|
70
|
+
RegisterManifestGenerator.new(
|
|
71
|
+
schema_manifest_file,
|
|
72
|
+
output_path,
|
|
73
|
+
urn: options[:urn],
|
|
74
|
+
id: options[:id],
|
|
75
|
+
ref: options[:ref],
|
|
76
|
+
language_code: options[:language_code],
|
|
77
|
+
owner: options[:owner],
|
|
78
|
+
).generate
|
|
51
79
|
end
|
|
52
80
|
|
|
53
81
|
desc "convert-jsdai XML_FILE IMAGE_FILE OUTPUT_DIR",
|
|
@@ -3,24 +3,25 @@
|
|
|
3
3
|
require "expressir/commands/changes_import_eengine"
|
|
4
4
|
|
|
5
5
|
module Suma
|
|
6
|
-
# Converts eengine comparison XML to Expressir::Changes::SchemaChange
|
|
7
|
-
#
|
|
6
|
+
# Converts eengine comparison XML to Expressir::Changes::SchemaChange.
|
|
7
|
+
#
|
|
8
|
+
# Pure converter: takes XML content as a string (no I/O). The caller
|
|
9
|
+
# is responsible for reading the file. This keeps the object cheap
|
|
10
|
+
# to construct and easy to test in isolation — no fixture file
|
|
11
|
+
# required just to instantiate.
|
|
8
12
|
class EengineConverter
|
|
9
|
-
def initialize(
|
|
10
|
-
@xml_path = xml_path
|
|
13
|
+
def initialize(schema_name, xml_content)
|
|
11
14
|
@schema_name = schema_name
|
|
12
|
-
@xml_content =
|
|
15
|
+
@xml_content = xml_content
|
|
13
16
|
end
|
|
14
17
|
|
|
15
|
-
# Convert the eengine XML to a ChangeSchema
|
|
18
|
+
# Convert the eengine XML to a ChangeSchema.
|
|
16
19
|
#
|
|
17
20
|
# @param version [String] Version number for this change version
|
|
18
21
|
# @param existing_change_schema [Expressir::Changes::SchemaChange, nil]
|
|
19
22
|
# Existing schema to append to, or nil to create new
|
|
20
23
|
# @return [Expressir::Changes::SchemaChange] The updated change schema
|
|
21
24
|
def convert(version:, existing_change_schema: nil)
|
|
22
|
-
# Use Expressir's built-in conversion which properly handles
|
|
23
|
-
# HTML elements in descriptions
|
|
24
25
|
Expressir::Commands::ChangesImportEengine.from_xml(
|
|
25
26
|
@xml_content,
|
|
26
27
|
@schema_name,
|
|
@@ -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/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,
|
data/lib/suma/term_extractor.rb
CHANGED
|
@@ -6,15 +6,6 @@ require "glossarist"
|
|
|
6
6
|
|
|
7
7
|
module Suma
|
|
8
8
|
class TermExtractor
|
|
9
|
-
REDUNDANT_NOTE_REGEX =
|
|
10
|
-
%r{
|
|
11
|
-
^An? # Starts with "A" or "An"
|
|
12
|
-
\s.*?\sis\sa\stype\sof # Text followed by "is a type of"
|
|
13
|
-
(\sa|\san)? # Optional " a" or " an"
|
|
14
|
-
\s\{\{[^\}]*\}\} # Text in double curly braces
|
|
15
|
-
\s*?\.?$ # Optional whitespace and period at the end
|
|
16
|
-
}x
|
|
17
|
-
|
|
18
9
|
def initialize(schema_manifest_file, output_path, urn:,
|
|
19
10
|
language_code: "eng")
|
|
20
11
|
@schema_manifest_file = File.expand_path(schema_manifest_file)
|
|
@@ -140,7 +131,11 @@ module Suma
|
|
|
140
131
|
data.domain = schema_domain
|
|
141
132
|
data.sources = [source_ref] if source_ref
|
|
142
133
|
|
|
143
|
-
notes =
|
|
134
|
+
notes = NoteProcessor.call(
|
|
135
|
+
entity.remarks,
|
|
136
|
+
definitions: data.definition,
|
|
137
|
+
xref_to_mention: method(:xref_to_mention),
|
|
138
|
+
)
|
|
144
139
|
data.notes = notes if notes && !notes.empty?
|
|
145
140
|
data.examples = []
|
|
146
141
|
end
|
|
@@ -166,6 +161,10 @@ module Suma
|
|
|
166
161
|
"{{#{urn},#{display}}}"
|
|
167
162
|
end
|
|
168
163
|
|
|
164
|
+
def xref_to_mention(full_ref, display)
|
|
165
|
+
urn_mention(express_entity_urn(full_ref), display)
|
|
166
|
+
end
|
|
167
|
+
|
|
169
168
|
def get_section_ref(schema)
|
|
170
169
|
return nil unless @urn
|
|
171
170
|
|
|
@@ -219,183 +218,6 @@ module Suma
|
|
|
219
218
|
[Glossarist::V3::DetailedDefinition.new(content: definition)]
|
|
220
219
|
end
|
|
221
220
|
|
|
222
|
-
def get_entity_notes(entity, schema_domain, definitions)
|
|
223
|
-
notes = []
|
|
224
|
-
|
|
225
|
-
if entity.remarks && !entity.remarks.empty?
|
|
226
|
-
trimmed_def = trim_definition(entity.remarks)
|
|
227
|
-
if trimmed_def && !trimmed_def.empty?
|
|
228
|
-
notes << Glossarist::V3::DetailedDefinition.new(
|
|
229
|
-
content: convert_express_xref(trimmed_def, schema_domain),
|
|
230
|
-
)
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
notes = only_keep_first_sentence(notes)
|
|
235
|
-
notes = remove_see_content(notes)
|
|
236
|
-
notes = remove_redundant_note(notes)
|
|
237
|
-
notes = remove_invalid_references(notes)
|
|
238
|
-
compare_with_definitions(notes, definitions)
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
def only_keep_first_sentence(notes)
|
|
242
|
-
notes.each do |note|
|
|
243
|
-
if note&.content && should_preserve_complete_structure?(note.content)
|
|
244
|
-
next
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
if note&.content
|
|
248
|
-
new_content = note.content
|
|
249
|
-
.split(".\n").first.strip
|
|
250
|
-
.split(". ").first.strip
|
|
251
|
-
note.content = new_content.end_with?(".") ? new_content : "#{new_content}."
|
|
252
|
-
end
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def should_preserve_complete_structure?(content)
|
|
257
|
-
return false if content.nil? || content.empty?
|
|
258
|
-
|
|
259
|
-
lines = content.split("\n")
|
|
260
|
-
first_paragraph = lines.first&.strip
|
|
261
|
-
|
|
262
|
-
if first_paragraph&.end_with?(":") && lines.length > 1
|
|
263
|
-
if first_paragraph.count(".").positive?
|
|
264
|
-
return false
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
remaining_content = lines[1..].join("\n")
|
|
268
|
-
return starts_with_list?(remaining_content.strip)
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
false
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
def compare_with_definitions(notes, definitions)
|
|
275
|
-
if notes&.first&.content == definitions&.first&.content
|
|
276
|
-
return []
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
notes
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
def remove_invalid_references(notes)
|
|
283
|
-
notes.reject do |note|
|
|
284
|
-
note.content.include?("image::") ||
|
|
285
|
-
note.content.match?(/<<(.*?){1,999}>>/)
|
|
286
|
-
end
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def remove_redundant_note(notes)
|
|
290
|
-
notes.reject do |note|
|
|
291
|
-
note.content.match?(REDUNDANT_NOTE_REGEX) &&
|
|
292
|
-
!note.content.include?("\n")
|
|
293
|
-
end
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
def remove_see_content(notes)
|
|
297
|
-
notes.each do |note|
|
|
298
|
-
note.content = note.content.gsub(/\s+\(see(.*?){1,999}\)/, "")
|
|
299
|
-
end
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
def starts_with_list?(content)
|
|
303
|
-
return false if content.nil? || content.empty?
|
|
304
|
-
|
|
305
|
-
content.match?(/^\s*[*\-+]\s+/) || content.match?(/^\s*\d+\.\s+/)
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
def trim_definition(definition)
|
|
309
|
-
return nil if definition.nil? || definition.empty?
|
|
310
|
-
|
|
311
|
-
definition_str = definition.is_a?(Array) ? definition.join("\n\n") : definition.to_s
|
|
312
|
-
|
|
313
|
-
return nil if definition_str.empty?
|
|
314
|
-
|
|
315
|
-
paragraphs = definition_str.split("\n\n")
|
|
316
|
-
first_paragraph = paragraphs.first
|
|
317
|
-
|
|
318
|
-
combined = if paragraphs.length == 1
|
|
319
|
-
apply_first_sentence_logic(first_paragraph)
|
|
320
|
-
elsif first_paragraph.end_with?(":") && paragraphs.length > 1 && starts_with_list?(paragraphs[1])
|
|
321
|
-
complete_list = extract_complete_list(paragraphs, 1)
|
|
322
|
-
"#{first_paragraph}\n\n#{complete_list}"
|
|
323
|
-
else
|
|
324
|
-
apply_first_sentence_logic(first_paragraph)
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
combined = "#{combined}\n"
|
|
328
|
-
combined.gsub!(/\n\/\/.*?\n/, "\n")
|
|
329
|
-
combined.strip!
|
|
330
|
-
|
|
331
|
-
express_reference_to_mention(combined)
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
def apply_first_sentence_logic(paragraph)
|
|
335
|
-
new_content = paragraph
|
|
336
|
-
.split(".\n").first.strip
|
|
337
|
-
.split(". ").first.strip
|
|
338
|
-
|
|
339
|
-
new_content.end_with?(".") ? new_content : "#{new_content}."
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
def extract_complete_list(paragraphs, start_index)
|
|
343
|
-
return paragraphs[start_index] if start_index >= paragraphs.length
|
|
344
|
-
|
|
345
|
-
combined = paragraphs[start_index].dup
|
|
346
|
-
current_index = start_index + 1
|
|
347
|
-
in_continuation_block = combined.include?("--") && !combined.match?(/--.*--/m)
|
|
348
|
-
|
|
349
|
-
while current_index < paragraphs.length
|
|
350
|
-
next_para = paragraphs[current_index]
|
|
351
|
-
|
|
352
|
-
if next_para.match?(/^--\s*$/) || next_para.end_with?("--")
|
|
353
|
-
in_continuation_block = !in_continuation_block
|
|
354
|
-
combined += "\n\n#{next_para}"
|
|
355
|
-
current_index += 1
|
|
356
|
-
next
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
if in_continuation_block
|
|
360
|
-
combined += "\n\n#{next_para}"
|
|
361
|
-
current_index += 1
|
|
362
|
-
next
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
if starts_with_list?(next_para) || is_list_continuation?(next_para)
|
|
366
|
-
combined += "\n\n#{next_para}"
|
|
367
|
-
current_index += 1
|
|
368
|
-
in_continuation_block = true if next_para.include?("--") && !next_para.match?(/--.*--/m)
|
|
369
|
-
else
|
|
370
|
-
break
|
|
371
|
-
end
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
combined
|
|
375
|
-
end
|
|
376
|
-
|
|
377
|
-
def is_list_continuation?(content)
|
|
378
|
-
return false if content.nil? || content.empty?
|
|
379
|
-
|
|
380
|
-
content.match?(/^\+\s*$/) ||
|
|
381
|
-
content.match?(/^--\s*$/) ||
|
|
382
|
-
content.match?(/^\s{2,}/) ||
|
|
383
|
-
content.start_with?("which", "where", "that")
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
def express_reference_to_mention(description)
|
|
387
|
-
description
|
|
388
|
-
.gsub(/<<express:([\w.]+)>>/) do |_match|
|
|
389
|
-
full_ref = Regexp.last_match[1]
|
|
390
|
-
entity_id = full_ref.split(".").last
|
|
391
|
-
urn_mention(express_entity_urn(full_ref), entity_id)
|
|
392
|
-
end.gsub(/<<express:([\w.]+),([\w. ][\w. ]*)>>/) do |_match|
|
|
393
|
-
full_ref = Regexp.last_match[1]
|
|
394
|
-
display = Regexp.last_match(2)
|
|
395
|
-
urn_mention(express_entity_urn(full_ref), display)
|
|
396
|
-
end
|
|
397
|
-
end
|
|
398
|
-
|
|
399
221
|
def entity_name_to_text(entity_id)
|
|
400
222
|
entity_id.downcase.gsub("_", " ")
|
|
401
223
|
end
|
|
@@ -425,13 +247,5 @@ module Suma
|
|
|
425
247
|
"#{entity_name_to_text(entity.id)} #{entity_ref}"
|
|
426
248
|
end
|
|
427
249
|
end
|
|
428
|
-
|
|
429
|
-
def convert_express_xref(content, _schema_domain)
|
|
430
|
-
content.gsub(/<<express:([\w.]+),([\w. ][\w. ]*)>>/) do
|
|
431
|
-
full_ref = Regexp.last_match(1)
|
|
432
|
-
display = Regexp.last_match(2)
|
|
433
|
-
urn_mention(express_entity_urn(full_ref), display)
|
|
434
|
-
end
|
|
435
|
-
end
|
|
436
250
|
end
|
|
437
251
|
end
|
data/lib/suma/version.rb
CHANGED
data/lib/suma.rb
CHANGED
|
@@ -16,6 +16,7 @@ module Suma
|
|
|
16
16
|
autoload :LinkValidation, "suma/link_validation"
|
|
17
17
|
autoload :LinkValidator, "suma/link_validator"
|
|
18
18
|
autoload :ManifestTraverser, "suma/manifest_traverser"
|
|
19
|
+
autoload :NoteProcessor, "suma/note_processor"
|
|
19
20
|
autoload :RegisterManifestGenerator, "suma/register_manifest_generator"
|
|
20
21
|
autoload :SchemaCategory, "suma/schema_category"
|
|
21
22
|
autoload :SchemaCollection, "suma/schema_collection"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: suma
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -214,7 +214,9 @@ files:
|
|
|
214
214
|
- lib/suma/jsdai/figure_xml.rb
|
|
215
215
|
- lib/suma/link_validation.rb
|
|
216
216
|
- lib/suma/link_validator.rb
|
|
217
|
+
- lib/suma/link_validator/step.rb
|
|
217
218
|
- lib/suma/manifest_traverser.rb
|
|
219
|
+
- lib/suma/note_processor.rb
|
|
218
220
|
- lib/suma/processor.rb
|
|
219
221
|
- lib/suma/register_manifest_generator.rb
|
|
220
222
|
- lib/suma/schema_category.rb
|