expressir 2.1.30 → 2.1.31
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/docs.yml +98 -0
- data/.github/workflows/links.yml +100 -0
- data/.github/workflows/rake.yml +4 -0
- data/.github/workflows/release.yml +5 -0
- data/.github/workflows/validate_schemas.yml +1 -1
- data/.gitignore +3 -0
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +244 -39
- data/Gemfile +2 -1
- data/README.adoc +621 -54
- data/docs/Gemfile +12 -0
- data/docs/_config.yml +141 -0
- data/docs/_guides/changes/changes-format.adoc +778 -0
- data/docs/_guides/changes/importing-eengine.adoc +898 -0
- data/docs/_guides/changes/index.adoc +396 -0
- data/docs/_guides/changes/programmatic-usage.adoc +1038 -0
- data/docs/_guides/changes/validating-changes.adoc +681 -0
- data/docs/_guides/cli/benchmark-performance.adoc +834 -0
- data/docs/_guides/cli/coverage-analysis.adoc +921 -0
- data/docs/_guides/cli/format-schemas.adoc +547 -0
- data/docs/_guides/cli/index.adoc +8 -0
- data/docs/_guides/cli/managing-changes.adoc +927 -0
- data/docs/_guides/cli/validate-ascii.adoc +645 -0
- data/docs/_guides/cli/validate-schemas.adoc +534 -0
- data/docs/_guides/index.adoc +165 -0
- data/docs/_guides/ler/creating-packages.adoc +664 -0
- data/docs/_guides/ler/index.adoc +305 -0
- data/docs/_guides/ler/loading-packages.adoc +707 -0
- data/docs/_guides/ler/package-formats.adoc +748 -0
- data/docs/_guides/ler/querying-packages.adoc +826 -0
- data/docs/_guides/ler/validating-packages.adoc +750 -0
- data/docs/_guides/liquid/basic-templates.adoc +813 -0
- data/docs/_guides/liquid/documentation-generation.adoc +1042 -0
- data/docs/_guides/liquid/drops-reference.adoc +829 -0
- data/docs/_guides/liquid/filters-and-tags.adoc +912 -0
- data/docs/_guides/liquid/index.adoc +468 -0
- data/docs/_guides/manifests/creating-manifests.adoc +483 -0
- data/docs/_guides/manifests/index.adoc +307 -0
- data/docs/_guides/manifests/resolving-manifests.adoc +557 -0
- data/docs/_guides/manifests/validating-manifests.adoc +713 -0
- data/docs/_guides/ruby-api/formatting-schemas.adoc +605 -0
- data/docs/_guides/ruby-api/index.adoc +257 -0
- data/docs/_guides/ruby-api/parsing-files.adoc +421 -0
- data/docs/_guides/ruby-api/search-engine.adoc +609 -0
- data/docs/_guides/ruby-api/working-with-repository.adoc +577 -0
- data/docs/_pages/data-model.adoc +665 -0
- data/docs/_pages/express-language.adoc +506 -0
- data/docs/_pages/getting-started.adoc +414 -0
- data/docs/_pages/index.adoc +116 -0
- data/docs/_pages/introduction.adoc +256 -0
- data/docs/_pages/ler-packages.adoc +837 -0
- data/docs/_pages/parsers.adoc +683 -0
- data/docs/_pages/schema-manifests.adoc +431 -0
- data/docs/_references/index.adoc +228 -0
- data/docs/_tutorials/creating-ler-package.adoc +735 -0
- data/docs/_tutorials/documentation-coverage.adoc +795 -0
- data/docs/_tutorials/index.adoc +221 -0
- data/docs/_tutorials/liquid-templates.adoc +806 -0
- data/docs/_tutorials/parsing-your-first-schema.adoc +522 -0
- data/docs/_tutorials/querying-schemas.adoc +751 -0
- data/docs/_tutorials/working-with-multiple-schemas.adoc +676 -0
- data/docs/index.adoc +242 -0
- data/docs/lychee.toml +84 -0
- data/examples/demo_ler_usage.sh +86 -0
- data/examples/ler/README.md +111 -0
- data/examples/ler/simple_example.ler +0 -0
- data/examples/ler/simple_schema.exp +33 -0
- data/examples/ler_build.rb +75 -0
- data/examples/ler_cli.rb +79 -0
- data/examples/ler_demo_complete.rb +276 -0
- data/examples/ler_query.rb +91 -0
- data/examples/ler_query_examples.rb +305 -0
- data/examples/ler_stats.rb +81 -0
- data/examples/phase3_demo.rb +159 -0
- data/examples/query_demo_simple.rb +131 -0
- data/expressir.gemspec +2 -0
- data/lib/expressir/cli.rb +12 -4
- data/lib/expressir/commands/manifest.rb +427 -0
- data/lib/expressir/commands/package.rb +1274 -0
- data/lib/expressir/commands/validate.rb +70 -37
- data/lib/expressir/commands/validate_ascii.rb +607 -0
- data/lib/expressir/commands/validate_load.rb +88 -0
- data/lib/expressir/express/formatter.rb +5 -1
- data/lib/expressir/express/formatters/remark_item_formatter.rb +25 -0
- data/lib/expressir/express/parser.rb +33 -0
- data/lib/expressir/manifest/resolver.rb +213 -0
- data/lib/expressir/manifest/validator.rb +195 -0
- data/lib/expressir/model/declarations/entity.rb +6 -0
- data/lib/expressir/model/dependency_resolver.rb +270 -0
- data/lib/expressir/model/indexes/entity_index.rb +103 -0
- data/lib/expressir/model/indexes/reference_index.rb +148 -0
- data/lib/expressir/model/indexes/type_index.rb +149 -0
- data/lib/expressir/model/interface_validator.rb +384 -0
- data/lib/expressir/model/repository.rb +400 -5
- data/lib/expressir/model/repository_validator.rb +295 -0
- data/lib/expressir/model/search_engine.rb +525 -0
- data/lib/expressir/model.rb +4 -94
- data/lib/expressir/package/builder.rb +200 -0
- data/lib/expressir/package/metadata.rb +81 -0
- data/lib/expressir/package/reader.rb +165 -0
- data/lib/expressir/schema_manifest.rb +11 -1
- data/lib/expressir/version.rb +1 -1
- data/lib/expressir.rb +15 -2
- metadata +114 -4
- data/docs/benchmarking.adoc +0 -107
- data/docs/liquid_drops.adoc +0 -1547
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
require "paint"
|
|
5
|
+
require "table_tennis"
|
|
6
|
+
|
|
7
|
+
module Expressir
|
|
8
|
+
module Commands
|
|
9
|
+
# Represents a non-ASCII character with its details and replacement
|
|
10
|
+
class NonAsciiCharacter
|
|
11
|
+
attr_reader :char, :hex, :utf8, :is_math, :replacement,
|
|
12
|
+
:replacement_type, :occurrences
|
|
13
|
+
|
|
14
|
+
def initialize(char, hex, utf8, is_math, replacement, replacement_type)
|
|
15
|
+
@char = char
|
|
16
|
+
@hex = hex
|
|
17
|
+
@utf8 = utf8
|
|
18
|
+
@is_math = is_math
|
|
19
|
+
@replacement = replacement
|
|
20
|
+
@replacement_type = replacement_type
|
|
21
|
+
@occurrences = []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def add_occurrence(line_number, column, line)
|
|
25
|
+
@occurrences << {
|
|
26
|
+
line_number: line_number,
|
|
27
|
+
column: column,
|
|
28
|
+
line: line,
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def replacement_text
|
|
33
|
+
@is_math ? "AsciiMath: #{@replacement}" : "ISO 10303-11: #{@replacement}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def occurrence_count
|
|
37
|
+
@occurrences.size
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_h
|
|
41
|
+
{
|
|
42
|
+
character: @char,
|
|
43
|
+
hex: @hex,
|
|
44
|
+
utf8: @utf8,
|
|
45
|
+
is_math: @is_math,
|
|
46
|
+
replacement_type: @replacement_type,
|
|
47
|
+
replacement: @replacement,
|
|
48
|
+
occurrence_count: occurrence_count,
|
|
49
|
+
occurrences: @occurrences,
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Represents all non-ASCII characters in a file
|
|
55
|
+
class FileViolations
|
|
56
|
+
attr_reader :path, :filename, :directory, :violations, :unique_characters
|
|
57
|
+
|
|
58
|
+
def initialize(file_path)
|
|
59
|
+
@path = file_path
|
|
60
|
+
@filename = File.basename(file_path)
|
|
61
|
+
@directory = File.dirname(file_path)
|
|
62
|
+
@characters = {} # Map of characters to NonAsciiCharacter objects
|
|
63
|
+
@violations = [] # List of violations (line, column, etc.)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def add_violation(line_number, column, match, char_details, line)
|
|
67
|
+
violation = {
|
|
68
|
+
line_number: line_number,
|
|
69
|
+
column: column,
|
|
70
|
+
match: match,
|
|
71
|
+
char_details: char_details,
|
|
72
|
+
line: line,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@violations << violation
|
|
76
|
+
|
|
77
|
+
# Register each character
|
|
78
|
+
char_details.each do |detail|
|
|
79
|
+
char = detail[:char]
|
|
80
|
+
unless @characters[char]
|
|
81
|
+
@characters[char] = NonAsciiCharacter.new(
|
|
82
|
+
char,
|
|
83
|
+
detail[:hex],
|
|
84
|
+
detail[:utf8],
|
|
85
|
+
detail[:is_math],
|
|
86
|
+
detail[:replacement],
|
|
87
|
+
detail[:replacement_type],
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
@characters[char].add_occurrence(line_number, column, line)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def violation_count
|
|
96
|
+
@violations.size
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def unique_characters
|
|
100
|
+
@characters.values
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def display_path
|
|
104
|
+
"#{File.basename(@directory)}/#{@filename}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def full_path
|
|
108
|
+
File.expand_path(@path)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def to_h
|
|
112
|
+
{
|
|
113
|
+
file: display_path,
|
|
114
|
+
count: violation_count,
|
|
115
|
+
non_ascii_characters: unique_characters.map(&:to_h),
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Collection of all violations across multiple files
|
|
121
|
+
class NonAsciiViolationCollection
|
|
122
|
+
attr_reader :file_violations, :total_files
|
|
123
|
+
|
|
124
|
+
def initialize(check_remarks: false)
|
|
125
|
+
@file_violations = {} # Map of file paths to FileViolations objects
|
|
126
|
+
@total_files = 0
|
|
127
|
+
@unicode_to_asciimath = nil
|
|
128
|
+
@check_remarks = check_remarks
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def process_file(file)
|
|
132
|
+
@total_files += 1
|
|
133
|
+
|
|
134
|
+
# Initialize the mapping once
|
|
135
|
+
@unicode_to_asciimath ||= build_unicode_to_asciimath_map
|
|
136
|
+
|
|
137
|
+
file_violations = process_file_violations(file)
|
|
138
|
+
return if file_violations.violations.empty?
|
|
139
|
+
|
|
140
|
+
@file_violations[file] = file_violations
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def files_with_violations
|
|
144
|
+
@file_violations.size
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def total_violations
|
|
148
|
+
@file_violations.values.sum(&:violation_count)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def unique_character_count
|
|
152
|
+
# Get total unique characters across all files
|
|
153
|
+
all_chars = Set.new
|
|
154
|
+
@file_violations.each_value do |file_violation|
|
|
155
|
+
file_violation.unique_characters.each do |char|
|
|
156
|
+
all_chars.add(char.char)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
all_chars.size
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def total_occurrence_count
|
|
163
|
+
# Sum all occurrences of all characters across all files
|
|
164
|
+
@file_violations.values.sum do |file_violation|
|
|
165
|
+
file_violation.unique_characters.sum(&:occurrence_count)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def to_yaml_data
|
|
170
|
+
{
|
|
171
|
+
summary: {
|
|
172
|
+
total_files: @total_files,
|
|
173
|
+
files_with_violations: files_with_violations,
|
|
174
|
+
total_violations: total_violations,
|
|
175
|
+
total_unique_characters: unique_character_count,
|
|
176
|
+
total_occurrences: total_occurrence_count,
|
|
177
|
+
},
|
|
178
|
+
violations: @file_violations.transform_keys do |k|
|
|
179
|
+
File.expand_path(k)
|
|
180
|
+
end.transform_values(&:to_h),
|
|
181
|
+
}
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def print_text_output
|
|
185
|
+
# Print each file's violations if any
|
|
186
|
+
unless @file_violations.empty?
|
|
187
|
+
@file_violations.each_value do |file_violation|
|
|
188
|
+
puts "\n#{Paint[file_violation.display_path, :cyan, :bold]}:"
|
|
189
|
+
|
|
190
|
+
file_violation.violations.each do |v|
|
|
191
|
+
puts " #{Paint['Line',
|
|
192
|
+
:blue]} #{Paint[v[:line_number],
|
|
193
|
+
:yellow]}, #{Paint['Column',
|
|
194
|
+
:blue]} #{Paint[v[:column],
|
|
195
|
+
:yellow]}:"
|
|
196
|
+
puts " #{v[:line]}"
|
|
197
|
+
puts " #{' ' * v[:column]}#{Paint['^' * v[:match].length,
|
|
198
|
+
:red]} #{Paint['Non-ASCII sequence',
|
|
199
|
+
:red]}"
|
|
200
|
+
|
|
201
|
+
v[:char_details].each do |cd|
|
|
202
|
+
character = file_violation.unique_characters.find do |c|
|
|
203
|
+
c.char == cd[:char]
|
|
204
|
+
end
|
|
205
|
+
next unless character
|
|
206
|
+
|
|
207
|
+
puts " #{Paint["\"#{cd[:char]}\"",
|
|
208
|
+
:yellow]} - Hex: #{Paint[cd[:hex],
|
|
209
|
+
:magenta]}, UTF-8 bytes: #{Paint[cd[:utf8],
|
|
210
|
+
:magenta]}"
|
|
211
|
+
puts " #{Paint['Replacement:',
|
|
212
|
+
:green]} #{character.replacement_text}"
|
|
213
|
+
end
|
|
214
|
+
puts ""
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
puts " #{Paint['Found',
|
|
218
|
+
:green]} #{Paint[file_violation.violation_count,
|
|
219
|
+
:red]} #{Paint['non-ASCII sequence(s) in',
|
|
220
|
+
:green]} #{Paint[file_violation.filename,
|
|
221
|
+
:cyan]}\n"
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Always print summary
|
|
226
|
+
validation_scope = @check_remarks ? "code and remarks" : "code only (remarks excluded)"
|
|
227
|
+
puts "\n#{Paint['Summary:', :blue, :bold]}"
|
|
228
|
+
puts " #{Paint['Validation scope:',
|
|
229
|
+
:green]} #{Paint[validation_scope,
|
|
230
|
+
:cyan]}"
|
|
231
|
+
puts " #{Paint['Scanned',
|
|
232
|
+
:green]} #{Paint[@total_files,
|
|
233
|
+
:yellow]} #{Paint['EXPRESS file(s)',
|
|
234
|
+
:green]}"
|
|
235
|
+
puts " #{Paint['Found',
|
|
236
|
+
:green]} #{Paint[total_violations,
|
|
237
|
+
:red]} #{Paint['non-ASCII sequence(s) in',
|
|
238
|
+
:green]} #{Paint[files_with_violations,
|
|
239
|
+
:red]} #{Paint['file(s)',
|
|
240
|
+
:green]}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def print_table_output
|
|
244
|
+
return if @file_violations.empty?
|
|
245
|
+
|
|
246
|
+
# Build rows array
|
|
247
|
+
rows = []
|
|
248
|
+
total_occurrences = 0
|
|
249
|
+
|
|
250
|
+
@file_violations.each_value do |file_violation|
|
|
251
|
+
file_violation.unique_characters.each do |character|
|
|
252
|
+
occurrence_count = character.occurrence_count
|
|
253
|
+
total_occurrences += occurrence_count
|
|
254
|
+
|
|
255
|
+
rows << {
|
|
256
|
+
file: file_violation.display_path,
|
|
257
|
+
symbol: "\"#{character.char}\" (#{character.hex})",
|
|
258
|
+
replacement: character.replacement_text,
|
|
259
|
+
occurrences: occurrence_count,
|
|
260
|
+
}
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Add total row
|
|
265
|
+
rows << {
|
|
266
|
+
file: "TOTAL",
|
|
267
|
+
symbol: "#{unique_character_count} unique",
|
|
268
|
+
replacement: "",
|
|
269
|
+
occurrences: total_occurrences,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
# Use TableTennis to render
|
|
273
|
+
options = {
|
|
274
|
+
title: "Non-ASCII Characters Summary",
|
|
275
|
+
columns: %i[file symbol replacement occurrences],
|
|
276
|
+
headers: {
|
|
277
|
+
file: "File",
|
|
278
|
+
symbol: "Symbol",
|
|
279
|
+
replacement: "Replacement",
|
|
280
|
+
occurrences: "Occurrences",
|
|
281
|
+
},
|
|
282
|
+
mark: ->(row) { row[:file] == "TOTAL" },
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
puts "\n#{TableTennis.new(rows, options)}\n"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
private
|
|
289
|
+
|
|
290
|
+
def process_file_violations(file)
|
|
291
|
+
file_violations = FileViolations.new(file)
|
|
292
|
+
|
|
293
|
+
if @check_remarks
|
|
294
|
+
# Check remarks too - validate original file content
|
|
295
|
+
File.readlines(file,
|
|
296
|
+
encoding: "UTF-8").each_with_index do |line, line_idx|
|
|
297
|
+
line_number = line_idx + 1
|
|
298
|
+
|
|
299
|
+
# Skip if line only contains ASCII
|
|
300
|
+
next unless /[^\x00-\x7F]/.match?(line)
|
|
301
|
+
|
|
302
|
+
# Find all non-ASCII sequences
|
|
303
|
+
line.chomp.scan(/([^\x00-\x7F]+)/) do |match|
|
|
304
|
+
match = match[0]
|
|
305
|
+
column = line.index(match)
|
|
306
|
+
|
|
307
|
+
# Process each character in the sequence
|
|
308
|
+
char_details = match.chars.filter_map do |c|
|
|
309
|
+
process_non_ascii_char(c)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Skip if no non-ASCII characters found
|
|
313
|
+
next if char_details.empty?
|
|
314
|
+
|
|
315
|
+
file_violations.add_violation(line_number, column, match,
|
|
316
|
+
char_details, line.chomp)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
else
|
|
320
|
+
# Default: exclude remarks - use model-based approach
|
|
321
|
+
# Parse the EXPRESS file to get the model
|
|
322
|
+
repository = Expressir::Express::Parser.from_file(file)
|
|
323
|
+
|
|
324
|
+
# Format each schema without remarks to get plain EXPRESS code
|
|
325
|
+
repository.schemas.each do |schema|
|
|
326
|
+
formatted_schema = schema.to_s(no_remarks: true)
|
|
327
|
+
|
|
328
|
+
# Check the formatted schema (without remarks) for non-ASCII
|
|
329
|
+
formatted_schema.lines.each_with_index do |line, line_idx|
|
|
330
|
+
line_number = line_idx + 1
|
|
331
|
+
|
|
332
|
+
# Skip if line only contains ASCII
|
|
333
|
+
next unless /[^\x00-\x7F]/.match?(line)
|
|
334
|
+
|
|
335
|
+
# Find all non-ASCII sequences
|
|
336
|
+
line.chomp.scan(/([^\x00-\x7F]+)/) do |match|
|
|
337
|
+
match = match[0]
|
|
338
|
+
column = line.index(match)
|
|
339
|
+
|
|
340
|
+
# Process each character in the sequence
|
|
341
|
+
char_details = match.chars.filter_map do |c|
|
|
342
|
+
process_non_ascii_char(c)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Skip if no non-ASCII characters found
|
|
346
|
+
next if char_details.empty?
|
|
347
|
+
|
|
348
|
+
file_violations.add_violation(line_number, column, match,
|
|
349
|
+
char_details, line.chomp)
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
file_violations
|
|
356
|
+
rescue Expressir::Express::Error::SchemaParseFailure
|
|
357
|
+
# If file can't be parsed, fall back to checking original content
|
|
358
|
+
# This ensures we still catch non-ASCII even in invalid EXPRESS
|
|
359
|
+
File.readlines(file,
|
|
360
|
+
encoding: "UTF-8").each_with_index do |line, line_idx|
|
|
361
|
+
line_number = line_idx + 1
|
|
362
|
+
|
|
363
|
+
# Skip if line only contains ASCII
|
|
364
|
+
next unless /[^\x00-\x7F]/.match?(line)
|
|
365
|
+
|
|
366
|
+
# Find all non-ASCII sequences
|
|
367
|
+
line.chomp.scan(/([^\x00-\x7F]+)/) do |match|
|
|
368
|
+
match = match[0]
|
|
369
|
+
column = line.index(match)
|
|
370
|
+
|
|
371
|
+
# Process each character in the sequence
|
|
372
|
+
char_details = match.chars.filter_map do |c|
|
|
373
|
+
process_non_ascii_char(c)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Skip if no non-ASCII characters found
|
|
377
|
+
next if char_details.empty?
|
|
378
|
+
|
|
379
|
+
file_violations.add_violation(line_number, column, match,
|
|
380
|
+
char_details, line.chomp)
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
file_violations
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def process_non_ascii_char(char)
|
|
388
|
+
# Skip ASCII characters
|
|
389
|
+
return nil if char.ord <= 0x7F
|
|
390
|
+
|
|
391
|
+
code_point = char.ord
|
|
392
|
+
hex = "0x#{code_point.to_s(16)}"
|
|
393
|
+
utf8 = code_point.chr(Encoding::UTF_8).bytes.map do |b|
|
|
394
|
+
"0x#{b.to_s(16)}"
|
|
395
|
+
end.join(" ")
|
|
396
|
+
|
|
397
|
+
# Check if it's a math symbol
|
|
398
|
+
if asciimath = @unicode_to_asciimath[char]
|
|
399
|
+
return {
|
|
400
|
+
char: char,
|
|
401
|
+
hex: hex,
|
|
402
|
+
utf8: utf8,
|
|
403
|
+
is_math: true,
|
|
404
|
+
replacement: asciimath,
|
|
405
|
+
replacement_type: "asciimath",
|
|
406
|
+
}
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Not a math symbol, use ISO encoding
|
|
410
|
+
{
|
|
411
|
+
char: char,
|
|
412
|
+
hex: hex,
|
|
413
|
+
utf8: utf8,
|
|
414
|
+
is_math: false,
|
|
415
|
+
replacement: encode_iso_10303_11(char),
|
|
416
|
+
replacement_type: "iso-10303-11",
|
|
417
|
+
}
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def encode_iso_10303_11(char)
|
|
421
|
+
code_point = char.ord
|
|
422
|
+
|
|
423
|
+
# Format the encoded value with double quotes
|
|
424
|
+
if code_point < 0x10000
|
|
425
|
+
"\"#{sprintf('%08X', code_point)}\"" # e.g., "00000041" for 'A'
|
|
426
|
+
else
|
|
427
|
+
# For higher code points, use all four octets
|
|
428
|
+
group = (code_point >> 24) & 0xFF
|
|
429
|
+
plane = (code_point >> 16) & 0xFF
|
|
430
|
+
row = (code_point >> 8) & 0xFF
|
|
431
|
+
cell = code_point & 0xFF
|
|
432
|
+
|
|
433
|
+
"\"#{sprintf('%02X%02X%02X%02X', group, plane, row, cell)}\""
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def build_unicode_to_asciimath_map
|
|
438
|
+
# Pre-defined mapping of common math symbols
|
|
439
|
+
{
|
|
440
|
+
# Greek letters
|
|
441
|
+
"α" => "alpha",
|
|
442
|
+
"β" => "beta",
|
|
443
|
+
"γ" => "gamma",
|
|
444
|
+
"Γ" => "Gamma",
|
|
445
|
+
"δ" => "delta",
|
|
446
|
+
"Δ" => "Delta",
|
|
447
|
+
"ε" => "epsilon",
|
|
448
|
+
"ζ" => "zeta",
|
|
449
|
+
"η" => "eta",
|
|
450
|
+
"θ" => "theta",
|
|
451
|
+
"Θ" => "Theta",
|
|
452
|
+
"ι" => "iota",
|
|
453
|
+
"κ" => "kappa",
|
|
454
|
+
"λ" => "lambda",
|
|
455
|
+
"Λ" => "Lambda",
|
|
456
|
+
"μ" => "mu",
|
|
457
|
+
"ν" => "nu",
|
|
458
|
+
"ξ" => "xi",
|
|
459
|
+
"Ξ" => "Xi",
|
|
460
|
+
"π" => "pi",
|
|
461
|
+
"Π" => "Pi",
|
|
462
|
+
"ρ" => "rho",
|
|
463
|
+
"σ" => "sigma",
|
|
464
|
+
"Σ" => "Sigma",
|
|
465
|
+
"τ" => "tau",
|
|
466
|
+
"υ" => "upsilon",
|
|
467
|
+
"φ" => "phi",
|
|
468
|
+
"Φ" => "Phi",
|
|
469
|
+
"χ" => "chi",
|
|
470
|
+
"ψ" => "psi",
|
|
471
|
+
"Ψ" => "Psi",
|
|
472
|
+
"ω" => "omega",
|
|
473
|
+
"Ω" => "Omega",
|
|
474
|
+
|
|
475
|
+
# Math operators
|
|
476
|
+
"×" => "xx",
|
|
477
|
+
"÷" => "div",
|
|
478
|
+
"±" => "pm",
|
|
479
|
+
"∓" => "mp",
|
|
480
|
+
"∞" => "oo",
|
|
481
|
+
"≤" => "le",
|
|
482
|
+
"≥" => "ge",
|
|
483
|
+
"≠" => "ne",
|
|
484
|
+
"≈" => "~~",
|
|
485
|
+
"≅" => "cong",
|
|
486
|
+
"≡" => "equiv",
|
|
487
|
+
"∈" => "in",
|
|
488
|
+
"∉" => "notin",
|
|
489
|
+
"⊂" => "subset",
|
|
490
|
+
"⊃" => "supset",
|
|
491
|
+
"∩" => "cap",
|
|
492
|
+
"∪" => "cup",
|
|
493
|
+
"∧" => "and",
|
|
494
|
+
"∨" => "or",
|
|
495
|
+
"¬" => "neg",
|
|
496
|
+
"∀" => "forall",
|
|
497
|
+
"∃" => "exists",
|
|
498
|
+
"∄" => "nexists",
|
|
499
|
+
"∇" => "grad",
|
|
500
|
+
"∂" => "del",
|
|
501
|
+
"∑" => "sum",
|
|
502
|
+
"∏" => "prod",
|
|
503
|
+
"∫" => "int",
|
|
504
|
+
"∮" => "oint",
|
|
505
|
+
"√" => "sqrt",
|
|
506
|
+
"⊥" => "perp",
|
|
507
|
+
"‖" => "norm",
|
|
508
|
+
"→" => "rarr",
|
|
509
|
+
"←" => "larr",
|
|
510
|
+
"↔" => "harr",
|
|
511
|
+
"⇒" => "rArr",
|
|
512
|
+
"⇐" => "lArr",
|
|
513
|
+
"⇔" => "hArr",
|
|
514
|
+
}
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# ValidateAscii command for checking EXPRESS files for non-ASCII characters
|
|
519
|
+
class ValidateAscii < Base
|
|
520
|
+
def run(express_file_path) # rubocop:disable Metrics/AbcSize
|
|
521
|
+
# Check if input is a manifest file
|
|
522
|
+
if File.file?(express_file_path) && File.extname(express_file_path) == ".yaml"
|
|
523
|
+
validate_from_manifest(express_file_path)
|
|
524
|
+
return
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Original file/directory validation logic
|
|
528
|
+
if File.file?(express_file_path)
|
|
529
|
+
unless File.exist?(express_file_path)
|
|
530
|
+
raise Errno::ENOENT, "Specified EXPRESS file " \
|
|
531
|
+
"`#{express_file_path}` not found."
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
if File.extname(express_file_path) != ".exp"
|
|
535
|
+
raise ArgumentError, "Specified file `#{express_file_path}` is " \
|
|
536
|
+
"not an EXPRESS file."
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
exp_files = [express_file_path]
|
|
540
|
+
elsif options[:recursive]
|
|
541
|
+
# Support the relative path with glob pattern
|
|
542
|
+
base_path = File.expand_path(express_file_path)
|
|
543
|
+
exp_files = Dir.glob("#{base_path}/**/*.exp")
|
|
544
|
+
else
|
|
545
|
+
# Non-recursive option
|
|
546
|
+
base_path = File.expand_path(express_file_path)
|
|
547
|
+
exp_files = Dir.glob("#{base_path}/*.exp")
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
if exp_files.empty?
|
|
551
|
+
raise Errno::ENOENT, "No EXPRESS files found in " \
|
|
552
|
+
"`#{express_file_path}`."
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
process_files(exp_files)
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
private
|
|
559
|
+
|
|
560
|
+
def validate_from_manifest(manifest_path)
|
|
561
|
+
unless File.exist?(manifest_path)
|
|
562
|
+
say "Error: Manifest file not found: #{manifest_path}", :red
|
|
563
|
+
exit 1
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
say "Loading manifest: #{manifest_path}..." if options[:verbose]
|
|
567
|
+
|
|
568
|
+
# Load manifest
|
|
569
|
+
manifest = Expressir::SchemaManifest.from_file(manifest_path)
|
|
570
|
+
|
|
571
|
+
# Extract paths from manifest
|
|
572
|
+
exp_files = manifest.schemas.map(&:path).reject do |p|
|
|
573
|
+
p.nil? || p.empty?
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
if exp_files.empty?
|
|
577
|
+
say "Error: No valid schema paths found in manifest", :red
|
|
578
|
+
exit 1
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
say "Validating #{exp_files.size} schema(s) from manifest for ASCII compliance..." if options[:verbose]
|
|
582
|
+
|
|
583
|
+
process_files(exp_files)
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def process_files(exp_files)
|
|
587
|
+
# Process all files and collect violations
|
|
588
|
+
collection = NonAsciiViolationCollection.new(
|
|
589
|
+
check_remarks: options[:check_remarks],
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
exp_files.each do |exp_file|
|
|
593
|
+
collection.process_file(exp_file)
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# Output results based on format
|
|
597
|
+
if options[:yaml]
|
|
598
|
+
require "yaml"
|
|
599
|
+
puts collection.to_yaml_data.to_yaml
|
|
600
|
+
else
|
|
601
|
+
collection.print_text_output
|
|
602
|
+
collection.print_table_output if collection.files_with_violations.positive?
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Commands
|
|
5
|
+
class ValidateLoad < Base
|
|
6
|
+
def run(paths)
|
|
7
|
+
# Determine if input is a manifest or direct paths
|
|
8
|
+
if paths.size == 1 && File.extname(paths.first) == ".yaml"
|
|
9
|
+
validate_from_manifest(paths.first)
|
|
10
|
+
else
|
|
11
|
+
validate_from_paths(paths)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def validate_from_manifest(manifest_path)
|
|
18
|
+
unless File.exist?(manifest_path)
|
|
19
|
+
say "Error: Manifest file not found: #{manifest_path}", :red
|
|
20
|
+
exit 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
say "Loading manifest: #{manifest_path}..." if options[:verbose]
|
|
24
|
+
|
|
25
|
+
# Load manifest
|
|
26
|
+
manifest = Expressir::SchemaManifest.from_file(manifest_path)
|
|
27
|
+
|
|
28
|
+
# Extract paths from manifest
|
|
29
|
+
paths = manifest.schemas.map(&:path).reject { |p| p.nil? || p.empty? }
|
|
30
|
+
|
|
31
|
+
if paths.empty?
|
|
32
|
+
say "Error: No valid schema paths found in manifest", :red
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
say "Validating #{paths.size} schema(s) from manifest..." if options[:verbose]
|
|
37
|
+
|
|
38
|
+
validate_from_paths(paths)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def validate_from_paths(paths)
|
|
42
|
+
no_version = []
|
|
43
|
+
no_valid = []
|
|
44
|
+
|
|
45
|
+
paths.each do |path|
|
|
46
|
+
x = Pathname.new(path).realpath.relative_path_from(Dir.pwd)
|
|
47
|
+
say "Validating #{x}"
|
|
48
|
+
ret = validate_schema(path)
|
|
49
|
+
|
|
50
|
+
if ret.nil?
|
|
51
|
+
no_valid << "Failed to parse: #{x}"
|
|
52
|
+
next
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
ret.each do |schema_id|
|
|
56
|
+
no_version << "Missing version string: schema `#{schema_id}` | #{x}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
print_validation_errors(:failed_to_parse, no_valid)
|
|
61
|
+
print_validation_errors(:missing_version_string, no_version)
|
|
62
|
+
|
|
63
|
+
exit 1 unless [no_valid, no_version].all?(&:empty?)
|
|
64
|
+
|
|
65
|
+
say "Validation passed for all EXPRESS schemas."
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def validate_schema(path)
|
|
69
|
+
repository = Expressir::Express::Parser.from_file(path)
|
|
70
|
+
repository.schemas.inject([]) do |acc, schema|
|
|
71
|
+
acc << schema.id unless schema.version&.value
|
|
72
|
+
acc
|
|
73
|
+
end
|
|
74
|
+
rescue StandardError
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def print_validation_errors(type, array)
|
|
79
|
+
return if array.empty?
|
|
80
|
+
|
|
81
|
+
say "#{'*' * 20} RESULTS: #{type.to_s.upcase.tr('_', ' ')} #{'*' * 20}"
|
|
82
|
+
array.each do |msg|
|
|
83
|
+
say msg
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|