suma 0.2.5 → 0.3.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/.github/workflows/rake.yml +3 -0
- data/.github/workflows/release.yml +5 -1
- data/.gitignore +10 -1
- data/.rubocop_todo.yml +237 -28
- data/CLAUDE.md +102 -0
- data/Gemfile +3 -1
- data/README.adoc +188 -1
- data/exe/suma +1 -1
- data/lib/suma/cli/build.rb +2 -8
- data/lib/suma/cli/check_svg_quality.rb +172 -0
- data/lib/suma/cli/compare.rb +6 -158
- data/lib/suma/cli/convert_jsdai.rb +0 -2
- data/lib/suma/cli/core.rb +119 -0
- data/lib/suma/cli/export.rb +1 -10
- data/lib/suma/cli/extract_terms.rb +10 -654
- data/lib/suma/cli/generate_register.rb +34 -0
- data/lib/suma/cli/generate_schemas.rb +8 -124
- data/lib/suma/cli/reformat.rb +0 -1
- data/lib/suma/cli/validate.rb +0 -2
- data/lib/suma/cli/validate_links.rb +14 -291
- data/lib/suma/cli.rb +12 -102
- data/lib/suma/collection_config.rb +0 -2
- data/lib/suma/collection_manifest.rb +7 -111
- data/lib/suma/eengine/wrapper.rb +0 -1
- data/lib/suma/eengine.rb +8 -0
- data/lib/suma/express_schema.rb +43 -31
- data/lib/suma/jsdai/figure.rb +0 -3
- data/lib/suma/jsdai/figure_xml.rb +12 -9
- data/lib/suma/jsdai.rb +5 -8
- data/lib/suma/link_validator.rb +211 -0
- data/lib/suma/manifest_traverser.rb +92 -0
- data/lib/suma/processor.rb +76 -105
- data/lib/suma/register_manifest_generator.rb +163 -0
- data/lib/suma/schema_category.rb +83 -0
- data/lib/suma/schema_collection.rb +28 -63
- data/lib/suma/schema_comparer.rb +117 -0
- data/lib/suma/schema_compiler.rb +86 -0
- data/lib/suma/schema_discovery.rb +75 -0
- data/lib/suma/schema_exporter.rb +7 -35
- data/lib/suma/schema_index.rb +53 -0
- data/lib/suma/schema_manifest_generator.rb +113 -0
- data/lib/suma/schema_naming.rb +111 -0
- data/lib/suma/schema_template/document.rb +141 -0
- data/lib/suma/schema_template/plain.rb +46 -0
- data/lib/suma/schema_template.rb +19 -0
- data/lib/suma/svg_quality/batch_report.rb +78 -0
- data/lib/suma/svg_quality/formatters/json_formatter.rb +30 -0
- data/lib/suma/svg_quality/formatters/terminal_formatter.rb +168 -0
- data/lib/suma/svg_quality/formatters/yaml_formatter.rb +32 -0
- data/lib/suma/svg_quality/formatters.rb +12 -0
- data/lib/suma/svg_quality/report.rb +52 -0
- data/lib/suma/svg_quality.rb +30 -0
- data/lib/suma/term_extractor.rb +466 -0
- data/lib/suma/urn.rb +61 -0
- data/lib/suma/utils.rb +10 -2
- data/lib/suma/version.rb +1 -1
- data/lib/suma.rb +34 -5
- data/suma.gemspec +3 -2
- metadata +53 -9
- data/lib/suma/export_standalone_schema.rb +0 -14
- data/lib/suma/schema_attachment.rb +0 -130
- data/lib/suma/schema_document.rb +0 -132
data/lib/suma/cli.rb
CHANGED
|
@@ -1,108 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "thor"
|
|
4
|
-
require_relative "thor_ext"
|
|
5
|
-
require_relative "cli/validate"
|
|
6
|
-
require "expressir"
|
|
7
|
-
require "expressir/cli"
|
|
8
|
-
|
|
9
3
|
module Suma
|
|
10
4
|
module Cli
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
require_relative "cli/build"
|
|
24
|
-
Cli::Build.start
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
desc "generate-schemas METANORMA_MANIFEST_FILE SCHEMA_MANIFEST_FILE",
|
|
28
|
-
"Generate EXPRESS schema manifest file from Metanorma site manifest"
|
|
29
|
-
option :exclude_paths, type: :string, default: nil, aliases: "-e",
|
|
30
|
-
desc: "Exclude schemas paths by pattern " \
|
|
31
|
-
"(e.g. `*_lf.exp`)"
|
|
32
|
-
def generate_schemas(_metanorma_manifest_file, _schema_manifest_file)
|
|
33
|
-
require_relative "cli/generate_schemas"
|
|
34
|
-
Cli::GenerateSchemas.start
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
desc "reformat EXPRESS_FILE_PATH",
|
|
38
|
-
"Reformat EXPRESS files"
|
|
39
|
-
option :recursive, type: :boolean, default: false, aliases: "-r",
|
|
40
|
-
desc: "Reformat EXPRESS files under the specified " \
|
|
41
|
-
"path recursively"
|
|
42
|
-
def reformat(_express_file_path)
|
|
43
|
-
require_relative "cli/reformat"
|
|
44
|
-
Cli::Reformat.start
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
desc "extract-terms SCHEMA_MANIFEST_FILE GLOSSARIST_OUTPUT_PATH",
|
|
48
|
-
"Extract terms from SCHEMA_MANIFEST_FILE into " \
|
|
49
|
-
"Glossarist v2 format"
|
|
50
|
-
option :language_code, type: :string, default: "eng", aliases: "-l",
|
|
51
|
-
desc: "Language code for the Glossarist"
|
|
52
|
-
def extract_terms(_schema_manifest_file, _glossarist_output_path)
|
|
53
|
-
require_relative "cli/extract_terms"
|
|
54
|
-
Cli::ExtractTerms.start
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
desc "convert-jsdai XML_FILE IMAGE_FILE OUTPUT_DIR",
|
|
58
|
-
"Convert JSDAI XML and image files to SVG and EXP files"
|
|
59
|
-
def convert_jsdai(_xml_file, _image_file, _output_dir)
|
|
60
|
-
require_relative "cli/convert_jsdai"
|
|
61
|
-
Cli::ConvertJsdai.start
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
desc "export *FILES",
|
|
65
|
-
"Export EXPRESS schemas from manifest files or " \
|
|
66
|
-
"standalone EXPRESS files"
|
|
67
|
-
option :output, type: :string, aliases: "-o", required: true,
|
|
68
|
-
desc: "Output directory path"
|
|
69
|
-
option :annotations, type: :boolean, default: false,
|
|
70
|
-
desc: "Include annotations (remarks/comments)"
|
|
71
|
-
option :zip, type: :boolean, default: false,
|
|
72
|
-
desc: "Create ZIP archive of exported schemas"
|
|
73
|
-
def export(*_files)
|
|
74
|
-
require_relative "cli/export"
|
|
75
|
-
Cli::Export.start
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
desc "compare TRIAL_SCHEMA REFERENCE_SCHEMA",
|
|
79
|
-
"Compare EXPRESS schemas using eengine and generate Change YAML"
|
|
80
|
-
option :output, type: :string, aliases: "-o",
|
|
81
|
-
desc: "Output Change YAML file path"
|
|
82
|
-
option :version, type: :string, aliases: "-v", required: true,
|
|
83
|
-
desc: "Version number for this change version"
|
|
84
|
-
option :mode, type: :string, default: "resource",
|
|
85
|
-
desc: "Schema comparison mode (resource/module)"
|
|
86
|
-
option :trial_stepmod, type: :string,
|
|
87
|
-
desc: "Override auto-detected trial repo root"
|
|
88
|
-
option :reference_stepmod, type: :string,
|
|
89
|
-
desc: "Override auto-detected reference repo root"
|
|
90
|
-
option :verbose, type: :boolean, default: false,
|
|
91
|
-
desc: "Enable verbose output"
|
|
92
|
-
def compare(_trial_schema, _reference_schema)
|
|
93
|
-
require_relative "cli/compare"
|
|
94
|
-
Cli::Compare.start
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
desc "validate SUBCOMMAND ...ARGS", "Validate express documents"
|
|
98
|
-
subcommand "validate", Cli::Validate
|
|
99
|
-
|
|
100
|
-
desc "expressir SUBCOMMAND ...ARGS", "Expressir commands"
|
|
101
|
-
subcommand "expressir", Expressir::Cli
|
|
102
|
-
|
|
103
|
-
def self.exit_on_failure?
|
|
104
|
-
true
|
|
105
|
-
end
|
|
106
|
-
end
|
|
5
|
+
autoload :Core, "suma/cli/core"
|
|
6
|
+
autoload :Build, "suma/cli/build"
|
|
7
|
+
autoload :CheckSvgQuality, "suma/cli/check_svg_quality"
|
|
8
|
+
autoload :Compare, "suma/cli/compare"
|
|
9
|
+
autoload :ConvertJsdai, "suma/cli/convert_jsdai"
|
|
10
|
+
autoload :Export, "suma/cli/export"
|
|
11
|
+
autoload :ExtractTerms, "suma/cli/extract_terms"
|
|
12
|
+
autoload :GenerateRegister, "suma/cli/generate_register"
|
|
13
|
+
autoload :GenerateSchemas, "suma/cli/generate_schemas"
|
|
14
|
+
autoload :Reformat, "suma/cli/reformat"
|
|
15
|
+
autoload :Validate, "suma/cli/validate"
|
|
16
|
+
autoload :ValidateLinks, "suma/cli/validate_links"
|
|
107
17
|
end
|
|
108
18
|
end
|
|
@@ -4,11 +4,17 @@ require "metanorma"
|
|
|
4
4
|
require "expressir"
|
|
5
5
|
|
|
6
6
|
module Suma
|
|
7
|
+
# Pure data model for one node of a Metanorma collection manifest.
|
|
8
|
+
#
|
|
9
|
+
# CollectionManifest extends the Metanorma config manifest to add the
|
|
10
|
+
# `schemas_only` flag and an `entry` sub-collection. It owns only state:
|
|
11
|
+
# attributes, YAML mappings, and the `schema_config` slot populated by
|
|
12
|
+
# SchemaDiscovery. Tree-walking logic lives in ManifestTraverser; schema
|
|
13
|
+
# I/O lives in SchemaDiscovery.
|
|
7
14
|
class CollectionManifest < Metanorma::Collection::Config::Manifest
|
|
8
15
|
attribute :schemas_only, Lutaml::Model::Type::Boolean
|
|
9
16
|
attribute :entry, CollectionManifest, collection: true,
|
|
10
17
|
initialize_empty: true
|
|
11
|
-
# attribute :schema_source, Lutaml::Model::Type::String
|
|
12
18
|
attr_accessor :schema_config
|
|
13
19
|
|
|
14
20
|
yaml do
|
|
@@ -33,115 +39,5 @@ module Suma
|
|
|
33
39
|
def docref_from_yaml(model, value)
|
|
34
40
|
model.entry = CollectionManifest.from_yaml(value.to_yaml)
|
|
35
41
|
end
|
|
36
|
-
|
|
37
|
-
# Recursively exports schema configuration by traversing collection manifests.
|
|
38
|
-
#
|
|
39
|
-
# This method builds an EXPRESS Schema Manifest (Expressir::SchemaManifest) by:
|
|
40
|
-
# 1. Starting with an empty or existing Expressir::SchemaManifest
|
|
41
|
-
# 2. Recursively traversing child entries to collect schemas
|
|
42
|
-
# 3. Using Expressir::SchemaManifest#concat to combine manifests
|
|
43
|
-
#
|
|
44
|
-
# The actual schema manifest operations (creation, concatenation, serialization)
|
|
45
|
-
# are handled by Expressir's SchemaManifest class, keeping the logic DRY.
|
|
46
|
-
#
|
|
47
|
-
# @param path [String] Base path for resolving relative schema paths
|
|
48
|
-
# @return [Expressir::SchemaManifest] Combined schema manifest
|
|
49
|
-
def export_schema_config(path)
|
|
50
|
-
export_config = @schema_config || Expressir::SchemaManifest.new
|
|
51
|
-
return export_config unless entry
|
|
52
|
-
|
|
53
|
-
entry.each do |x|
|
|
54
|
-
child_config = x.export_schema_config(path)
|
|
55
|
-
# Use Expressir's concat method to combine schema manifests
|
|
56
|
-
export_config.concat(child_config) if child_config
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
export_config
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def lookup(attr_sym, match)
|
|
63
|
-
results = entry.select { |e| e.send(attr_sym) == match }
|
|
64
|
-
results << self if send(attr_sym) == match
|
|
65
|
-
results
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def process_entry(schema_output_path)
|
|
69
|
-
return [self] unless entry
|
|
70
|
-
|
|
71
|
-
ret = entry.each_with_object([]) do |e, m|
|
|
72
|
-
add = e.expand_schemas_only(schema_output_path)
|
|
73
|
-
m.concat(add)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
self.entry = ret
|
|
77
|
-
[self]
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def expand_schemas_only(schema_output_path)
|
|
81
|
-
return process_entry(schema_output_path) unless file
|
|
82
|
-
|
|
83
|
-
update_schema_config
|
|
84
|
-
|
|
85
|
-
return process_entry(schema_output_path) unless schemas_only
|
|
86
|
-
|
|
87
|
-
# If we are going to keep the schemas-only file and compile it, we can't
|
|
88
|
-
# have it showing up in output.
|
|
89
|
-
self.index = false
|
|
90
|
-
|
|
91
|
-
[self, added_collection_manifest(schema_output_path)]
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def remove_schemas_only_sources
|
|
95
|
-
ret = entry.each_with_object([]) do |e, m|
|
|
96
|
-
e.schemas_only or m << e
|
|
97
|
-
end
|
|
98
|
-
self.entry = ret
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def entries(schema_output_path)
|
|
102
|
-
@schema_config.schemas.map do |schema|
|
|
103
|
-
fname = [File.basename(schema.path, ".exp"), ".xml"].join
|
|
104
|
-
|
|
105
|
-
CollectionManifest.new(
|
|
106
|
-
identifier: schema.id,
|
|
107
|
-
title: schema.id,
|
|
108
|
-
file: File.join(schema_output_path, schema.id, "doc_#{fname}"),
|
|
109
|
-
# schema_source: schema.path
|
|
110
|
-
)
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def added_collection_manifest(schema_output_path)
|
|
115
|
-
doc = CollectionConfig.from_file(file)
|
|
116
|
-
doc_id = doc.bibdata.id
|
|
117
|
-
|
|
118
|
-
# we need to separate this file from the following new entries
|
|
119
|
-
added = CollectionManifest.new(
|
|
120
|
-
title: "Collection",
|
|
121
|
-
type: "collection",
|
|
122
|
-
identifier: "#{identifier}_",
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
added.entry = [
|
|
126
|
-
CollectionManifest.new(
|
|
127
|
-
title: doc_id,
|
|
128
|
-
type: "document",
|
|
129
|
-
entry: entries(schema_output_path),
|
|
130
|
-
),
|
|
131
|
-
]
|
|
132
|
-
|
|
133
|
-
added
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def update_schema_config
|
|
137
|
-
# If there is collection.yml, this is a document collection, we process
|
|
138
|
-
# schemas.yaml.
|
|
139
|
-
if File.basename(file) == "collection.yml"
|
|
140
|
-
schemas_yaml_path = File.join(File.dirname(file), "schemas.yaml")
|
|
141
|
-
if schemas_yaml_path && File.exist?(schemas_yaml_path)
|
|
142
|
-
@schema_config = Expressir::SchemaManifest.from_file(schemas_yaml_path)
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
42
|
end
|
|
147
43
|
end
|
data/lib/suma/eengine/wrapper.rb
CHANGED
data/lib/suma/eengine.rb
ADDED
data/lib/suma/express_schema.rb
CHANGED
|
@@ -1,11 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "utils"
|
|
4
3
|
require "fileutils"
|
|
5
4
|
require "expressir"
|
|
6
5
|
|
|
7
6
|
module Suma
|
|
8
7
|
class ExpressSchema
|
|
8
|
+
module Type
|
|
9
|
+
RESOURCE = :resource
|
|
10
|
+
MODULE_ARM = :module_arm
|
|
11
|
+
MODULE_MIM = :module_mim
|
|
12
|
+
BUSINESS_OBJECT_MODEL = :business_object_model
|
|
13
|
+
CORE_MODEL = :core_model
|
|
14
|
+
STANDALONE = :standalone
|
|
15
|
+
|
|
16
|
+
ID_SUFFIXES = {
|
|
17
|
+
"_arm" => :MODULE_ARM,
|
|
18
|
+
"_mim" => :MODULE_MIM,
|
|
19
|
+
"_bom" => :BUSINESS_OBJECT_MODEL,
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
PATH_SEGMENTS = {
|
|
23
|
+
"/resources/" => :RESOURCE,
|
|
24
|
+
"/modules/" => :MODULE_ARM,
|
|
25
|
+
"/core_model/" => :CORE_MODEL,
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
def self.classify(id:, path:)
|
|
29
|
+
name = id&.downcase || ""
|
|
30
|
+
|
|
31
|
+
ID_SUFFIXES.each do |suffix, type|
|
|
32
|
+
return const_get(type) if name.end_with?(suffix)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
path_str = path.to_s
|
|
36
|
+
PATH_SEGMENTS.each do |segment, type|
|
|
37
|
+
return const_get(type) if path_str.include?(segment)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
STANDALONE
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
9
44
|
attr_accessor :path, :id, :parsed, :output_path, :is_standalone_file
|
|
10
45
|
|
|
11
46
|
def initialize(id:, path:, output_path:, is_standalone_file: false)
|
|
@@ -16,14 +51,7 @@ module Suma
|
|
|
16
51
|
end
|
|
17
52
|
|
|
18
53
|
def type
|
|
19
|
-
|
|
20
|
-
if path_str.include?("/resources/")
|
|
21
|
-
"resources"
|
|
22
|
-
elsif path_str.include?("/modules/")
|
|
23
|
-
"modules"
|
|
24
|
-
else
|
|
25
|
-
"unknown_type"
|
|
26
|
-
end
|
|
54
|
+
@type ||= classify
|
|
27
55
|
end
|
|
28
56
|
|
|
29
57
|
def parsed
|
|
@@ -50,11 +78,8 @@ module Suma
|
|
|
50
78
|
|
|
51
79
|
def build_output_filename
|
|
52
80
|
if @is_standalone_file
|
|
53
|
-
# For standalone files, output directly to output_path
|
|
54
81
|
File.join(@output_path, "#{@id}.exp")
|
|
55
82
|
else
|
|
56
|
-
# For manifest schemas, preserve directory structure
|
|
57
|
-
# Note: @output_path already contains the category (resources/modules)
|
|
58
83
|
parent_dir = File.basename(File.dirname(@path))
|
|
59
84
|
File.join(@output_path, parent_dir, File.basename(@path))
|
|
60
85
|
end
|
|
@@ -65,29 +90,16 @@ module Suma
|
|
|
65
90
|
schema_type = with_annotations ? "annotated" : "plain"
|
|
66
91
|
Utils.log "Save #{schema_type} schema: #{relative_path}"
|
|
67
92
|
|
|
68
|
-
# return if File.exist?(filename_plain)
|
|
69
93
|
FileUtils.mkdir_p(File.dirname(filename_plain))
|
|
70
94
|
|
|
71
95
|
content = with_annotations ? parsed.to_s(no_remarks: false) : to_plain
|
|
72
96
|
File.write(filename_plain, content)
|
|
73
97
|
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# col = Suma::SchemaCollection.new(
|
|
78
|
-
# config_yaml: 'suma-schemas.yaml',
|
|
79
|
-
# output_path_docs: 'schema_docs',
|
|
80
|
-
# output_path_schemas: 'plain_schemas'
|
|
81
|
-
# )
|
|
82
|
-
|
|
83
|
-
# docs = col.compile
|
|
84
98
|
|
|
85
|
-
|
|
86
|
-
# {
|
|
87
|
-
# plain_schema_path: schema.filename_plain,
|
|
88
|
-
# schema_doc_path: col.doc_from_schema_name(schema.id).output_xml_path
|
|
89
|
-
# }
|
|
90
|
-
# end
|
|
99
|
+
private
|
|
91
100
|
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
def classify
|
|
102
|
+
Type.classify(id: @id, path: @path)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/suma/jsdai/figure.rb
CHANGED
|
@@ -11,10 +11,11 @@ module Suma
|
|
|
11
11
|
attribute :href, :string
|
|
12
12
|
|
|
13
13
|
xml do
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
map_attribute "
|
|
17
|
-
map_attribute "
|
|
14
|
+
element "img.area"
|
|
15
|
+
ordered
|
|
16
|
+
map_attribute "shape", to: :shape, render_empty: true
|
|
17
|
+
map_attribute "coords", to: :coords, render_empty: true
|
|
18
|
+
map_attribute "href", to: :href, render_empty: true
|
|
18
19
|
end
|
|
19
20
|
end
|
|
20
21
|
|
|
@@ -24,8 +25,9 @@ module Suma
|
|
|
24
25
|
attribute :areas, FigureXmlImageArea, collection: true
|
|
25
26
|
|
|
26
27
|
xml do
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
element "img"
|
|
29
|
+
ordered
|
|
30
|
+
map_attribute "src", to: :src, render_empty: true
|
|
29
31
|
map_element "img.area", to: :areas
|
|
30
32
|
end
|
|
31
33
|
end
|
|
@@ -37,9 +39,10 @@ module Suma
|
|
|
37
39
|
attribute :img, FigureXmlImage
|
|
38
40
|
|
|
39
41
|
xml do
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
map_attribute "
|
|
42
|
+
element "imgfile.content"
|
|
43
|
+
ordered
|
|
44
|
+
map_attribute "module", to: :module, render_empty: true
|
|
45
|
+
map_attribute "file", to: :file, render_empty: true
|
|
43
46
|
map_element "img", to: :img
|
|
44
47
|
end
|
|
45
48
|
end
|
data/lib/suma/jsdai.rb
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Suma
|
|
2
4
|
module Jsdai
|
|
5
|
+
autoload :Figure, "suma/jsdai/figure"
|
|
6
|
+
autoload :FigureImage, "suma/jsdai/figure_image"
|
|
7
|
+
autoload :FigureXml, "suma/jsdai/figure_xml"
|
|
3
8
|
end
|
|
4
9
|
end
|
|
5
|
-
|
|
6
|
-
require_relative "jsdai/figure"
|
|
7
|
-
|
|
8
|
-
# Configure XML adapter to Nokogiri because Ox goes into a "stack level too
|
|
9
|
-
# deep" error, for unknown reasons
|
|
10
|
-
Lutaml::Model::Config.configure do |config|
|
|
11
|
-
config.xml_adapter_type = :nokogiri
|
|
12
|
-
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "expressir"
|
|
4
|
+
|
|
5
|
+
module Suma
|
|
6
|
+
LinkValidationResult = Struct.new(:file, :line, :link, :reason,
|
|
7
|
+
keyword_init: true)
|
|
8
|
+
|
|
9
|
+
class LinkValidator
|
|
10
|
+
def initialize(index)
|
|
11
|
+
@index = index
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def validate(links_by_file)
|
|
15
|
+
unresolved = []
|
|
16
|
+
|
|
17
|
+
links_by_file.each do |file, links|
|
|
18
|
+
line_index = build_link_line_index(file)
|
|
19
|
+
validate_file(file, links, line_index, unresolved)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
unresolved
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def build_link_line_index(file)
|
|
28
|
+
content = File.read(file)
|
|
29
|
+
index = {}
|
|
30
|
+
content.lines.each_with_index do |line, idx|
|
|
31
|
+
line.scan(/<<express:([^,>]+)(?:,[^>]+)?>>/).flatten.each do |link|
|
|
32
|
+
index[link] ||= idx
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
index
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def validate_file(file, links, line_index, unresolved)
|
|
39
|
+
links.each do |link|
|
|
40
|
+
line_idx = line_index[link]
|
|
41
|
+
next unless line_idx
|
|
42
|
+
|
|
43
|
+
parts = link.split(".")
|
|
44
|
+
|
|
45
|
+
if parts.size == 1
|
|
46
|
+
validate_schema_only(parts[0], file, line_idx, link, unresolved)
|
|
47
|
+
else
|
|
48
|
+
validate_element(parts, file, line_idx, link, unresolved)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def validate_schema_only(schema_name, file, line_idx, link, unresolved)
|
|
54
|
+
schema = @index.find_schema(schema_name)
|
|
55
|
+
|
|
56
|
+
return if schema
|
|
57
|
+
|
|
58
|
+
unresolved << LinkValidationResult.new(
|
|
59
|
+
file: file,
|
|
60
|
+
line: line_idx + 1,
|
|
61
|
+
link: link,
|
|
62
|
+
reason: "Schema '#{schema_name}' not found",
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def validate_element(parts, file, line_idx, link, unresolved)
|
|
67
|
+
schema_name = parts[0]
|
|
68
|
+
element_name = parts[1]
|
|
69
|
+
|
|
70
|
+
schema = @index.find_schema(schema_name)
|
|
71
|
+
|
|
72
|
+
unless schema
|
|
73
|
+
unresolved << LinkValidationResult.new(
|
|
74
|
+
file: file,
|
|
75
|
+
line: line_idx + 1,
|
|
76
|
+
link: link,
|
|
77
|
+
reason: "Schema '#{schema_name}' not found",
|
|
78
|
+
)
|
|
79
|
+
return
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
element = @index.find_element(schema_name, element_name)
|
|
83
|
+
|
|
84
|
+
unless element
|
|
85
|
+
unresolved << LinkValidationResult.new(
|
|
86
|
+
file: file,
|
|
87
|
+
line: line_idx + 1,
|
|
88
|
+
link: link,
|
|
89
|
+
reason: "Element '#{element_name}' not found in schema '#{schema_name}'",
|
|
90
|
+
)
|
|
91
|
+
return
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
return unless parts.size > 2
|
|
95
|
+
|
|
96
|
+
error = validate_deep_path(schema, element, parts[2..], file, line_idx,
|
|
97
|
+
link)
|
|
98
|
+
unresolved << error if error
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def validate_deep_path(schema, element, path_parts, file, line_idx,
|
|
102
|
+
full_link)
|
|
103
|
+
current = element
|
|
104
|
+
current_path = "#{schema.id}.#{element.id}"
|
|
105
|
+
|
|
106
|
+
path_parts.each do |part|
|
|
107
|
+
case current
|
|
108
|
+
when Expressir::Model::Declarations::Entity
|
|
109
|
+
attribute = current.attributes&.find do |a|
|
|
110
|
+
a.id.downcase == part.downcase
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
unless attribute
|
|
114
|
+
return LinkValidationResult.new(
|
|
115
|
+
file: file,
|
|
116
|
+
line: line_idx + 1,
|
|
117
|
+
link: full_link,
|
|
118
|
+
reason: "Attribute '#{part}' not found in entity '#{current_path}'",
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
current = attribute
|
|
123
|
+
current_path += ".#{part}"
|
|
124
|
+
|
|
125
|
+
when Expressir::Model::Declarations::Type
|
|
126
|
+
underlying = current.underlying_type
|
|
127
|
+
|
|
128
|
+
if underlying.is_a?(Expressir::Model::DataTypes::Enumeration)
|
|
129
|
+
enum_value = underlying.items.find do |e|
|
|
130
|
+
e.id.downcase == part.downcase
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
unless enum_value
|
|
134
|
+
return LinkValidationResult.new(
|
|
135
|
+
file: file,
|
|
136
|
+
line: line_idx + 1,
|
|
137
|
+
link: full_link,
|
|
138
|
+
reason: "Enumeration value '#{part}' not found in type '#{current_path}'",
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
current = enum_value
|
|
143
|
+
current_path += ".#{part}"
|
|
144
|
+
|
|
145
|
+
elsif underlying
|
|
146
|
+
base_type = find_base_type(schema, underlying)
|
|
147
|
+
|
|
148
|
+
unless base_type
|
|
149
|
+
return LinkValidationResult.new(
|
|
150
|
+
file: file,
|
|
151
|
+
line: line_idx + 1,
|
|
152
|
+
link: full_link,
|
|
153
|
+
reason: "Base type not found for '#{current_path}'",
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
current = base_type
|
|
158
|
+
|
|
159
|
+
else
|
|
160
|
+
return LinkValidationResult.new(
|
|
161
|
+
file: file,
|
|
162
|
+
line: line_idx + 1,
|
|
163
|
+
link: full_link,
|
|
164
|
+
reason: "Cannot navigate deeper from type '#{current_path}'",
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
else
|
|
169
|
+
return LinkValidationResult.new(
|
|
170
|
+
file: file,
|
|
171
|
+
line: line_idx + 1,
|
|
172
|
+
link: full_link,
|
|
173
|
+
reason: "Cannot navigate deeper from '#{current_path}'",
|
|
174
|
+
)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def find_base_type(schema, type_ref)
|
|
182
|
+
return nil if %w[INTEGER REAL STRING BOOLEAN NUMBER BINARY
|
|
183
|
+
LOGICAL].include?(type_ref.to_s.upcase)
|
|
184
|
+
|
|
185
|
+
if type_ref.is_a?(String)
|
|
186
|
+
find_schema_element(schema, type_ref)
|
|
187
|
+
elsif type_ref.is_a?(Expressir::Model::ModelElement)
|
|
188
|
+
type_ref
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def find_schema_element(schema, element_name)
|
|
193
|
+
[
|
|
194
|
+
schema.entities,
|
|
195
|
+
schema.types,
|
|
196
|
+
schema.constants,
|
|
197
|
+
schema.functions,
|
|
198
|
+
schema.rules,
|
|
199
|
+
schema.procedures,
|
|
200
|
+
schema.subtype_constraints,
|
|
201
|
+
].each do |collection|
|
|
202
|
+
element = collection&.find do |e|
|
|
203
|
+
e.id.downcase == element_name.downcase
|
|
204
|
+
end
|
|
205
|
+
return element if element
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
nil
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|