glossarist 2.6.5 → 2.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +1 -4
  3. data/.rubocop_todo.yml +53 -2
  4. data/CLAUDE.md +27 -2
  5. data/README.adoc +532 -56
  6. data/config.yml +68 -1
  7. data/glossarist.gemspec +2 -0
  8. data/lib/glossarist/citation.rb +26 -123
  9. data/lib/glossarist/cli/compare_command.rb +106 -0
  10. data/lib/glossarist/cli/export_command.rb +11 -14
  11. data/lib/glossarist/cli/validate_command.rb +111 -20
  12. data/lib/glossarist/cli.rb +18 -0
  13. data/lib/glossarist/collections/bibliography_collection.rb +4 -2
  14. data/lib/glossarist/collections/localization_collection.rb +2 -0
  15. data/lib/glossarist/comparison_result.rb +35 -0
  16. data/lib/glossarist/concept.rb +1 -1
  17. data/lib/glossarist/concept_collector.rb +44 -0
  18. data/lib/glossarist/concept_comparator.rb +72 -0
  19. data/lib/glossarist/concept_data.rb +20 -0
  20. data/lib/glossarist/concept_diff.rb +15 -0
  21. data/lib/glossarist/concept_document.rb +11 -0
  22. data/lib/glossarist/concept_manager.rb +19 -5
  23. data/lib/glossarist/concept_ref.rb +13 -0
  24. data/lib/glossarist/concept_reference.rb +12 -19
  25. data/lib/glossarist/concept_validator.rb +6 -1
  26. data/lib/glossarist/context_configuration.rb +90 -0
  27. data/lib/glossarist/dataset_validator.rb +8 -4
  28. data/lib/glossarist/designation/abbreviation.rb +0 -2
  29. data/lib/glossarist/designation/base.rb +21 -1
  30. data/lib/glossarist/designation/expression.rb +3 -0
  31. data/lib/glossarist/designation/letter_symbol.rb +0 -4
  32. data/lib/glossarist/designation/prefix.rb +17 -0
  33. data/lib/glossarist/designation/suffix.rb +17 -0
  34. data/lib/glossarist/designation/symbol.rb +0 -2
  35. data/lib/glossarist/gcr_metadata.rb +7 -14
  36. data/lib/glossarist/gcr_package.rb +35 -23
  37. data/lib/glossarist/gcr_validator.rb +38 -17
  38. data/lib/glossarist/glossary_definition.rb +5 -0
  39. data/lib/glossarist/localized_concept.rb +8 -0
  40. data/lib/glossarist/managed_concept.rb +39 -6
  41. data/lib/glossarist/managed_concept_data.rb +22 -2
  42. data/lib/glossarist/non_verb_rep.rb +21 -6
  43. data/lib/glossarist/pronunciation.rb +32 -0
  44. data/lib/glossarist/rdf/ext/jsonld_transform_ext.rb +208 -0
  45. data/lib/glossarist/rdf/ext/mapping_ext.rb +37 -0
  46. data/lib/glossarist/rdf/ext/mapping_rule_ext.rb +27 -0
  47. data/lib/glossarist/rdf/ext/member_rule_ext.rb +34 -0
  48. data/lib/glossarist/rdf/ext/turtle_transform_ext.rb +222 -0
  49. data/lib/glossarist/rdf/ext.rb +39 -0
  50. data/lib/glossarist/rdf/gloss_citation.rb +36 -0
  51. data/lib/glossarist/rdf/gloss_concept.rb +58 -0
  52. data/lib/glossarist/rdf/gloss_concept_date.rb +24 -0
  53. data/lib/glossarist/rdf/gloss_concept_reference.rb +29 -0
  54. data/lib/glossarist/rdf/gloss_concept_source.rb +37 -0
  55. data/lib/glossarist/rdf/gloss_designation.rb +146 -0
  56. data/lib/glossarist/rdf/gloss_detailed_definition.rb +24 -0
  57. data/lib/glossarist/rdf/gloss_grammar_info.rb +57 -0
  58. data/lib/glossarist/rdf/gloss_locality.rb +25 -0
  59. data/lib/glossarist/rdf/gloss_localized_concept.rb +67 -0
  60. data/lib/glossarist/rdf/gloss_non_verbal_rep.rb +31 -0
  61. data/lib/glossarist/rdf/gloss_pronunciation.rb +32 -0
  62. data/lib/glossarist/rdf/gloss_reference.rb +55 -0
  63. data/lib/glossarist/rdf/namespaces/glossarist_namespace.rb +12 -0
  64. data/lib/glossarist/rdf/namespaces/iso_thes_namespace.rb +12 -0
  65. data/lib/glossarist/rdf/namespaces/owl_namespace.rb +12 -0
  66. data/lib/glossarist/rdf/namespaces/prov_namespace.rb +12 -0
  67. data/lib/glossarist/rdf/namespaces/rdf_namespace.rb +12 -0
  68. data/lib/glossarist/rdf/namespaces/skosxl_namespace.rb +12 -0
  69. data/lib/glossarist/rdf/namespaces.rb +8 -2
  70. data/lib/glossarist/rdf/relationships.rb +19 -0
  71. data/lib/glossarist/rdf/v3/configuration.rb +15 -0
  72. data/lib/glossarist/rdf/v3.rb +79 -0
  73. data/lib/glossarist/rdf.rb +22 -2
  74. data/lib/glossarist/reference_extractor.rb +15 -24
  75. data/lib/glossarist/reference_resolver.rb +3 -3
  76. data/lib/glossarist/related_concept.rb +2 -10
  77. data/lib/glossarist/schema_migration.rb +39 -0
  78. data/lib/glossarist/sts/term_mapper.rb +2 -2
  79. data/lib/glossarist/transforms/concept_to_gloss_transform.rb +355 -0
  80. data/lib/glossarist/transforms.rb +2 -2
  81. data/lib/glossarist/urn_resolver.rb +13 -1
  82. data/lib/glossarist/v1/concept.rb +18 -11
  83. data/lib/glossarist/v2/citation.rb +36 -0
  84. data/lib/glossarist/v2/concept_data.rb +46 -0
  85. data/lib/glossarist/v2/concept_document.rb +18 -0
  86. data/lib/glossarist/v2/concept_ref.rb +8 -0
  87. data/lib/glossarist/v2/concept_source.rb +16 -0
  88. data/lib/glossarist/v2/configuration.rb +13 -0
  89. data/lib/glossarist/v2/detailed_definition.rb +14 -0
  90. data/lib/glossarist/v2/localized_concept.rb +9 -0
  91. data/lib/glossarist/v2/managed_concept.rb +25 -0
  92. data/lib/glossarist/v2/managed_concept_data.rb +49 -0
  93. data/lib/glossarist/v2/related_concept.rb +15 -0
  94. data/lib/glossarist/v2.rb +28 -0
  95. data/lib/glossarist/v3/bibliography_entry.rb +19 -0
  96. data/lib/glossarist/v3/bibliography_file.rb +27 -0
  97. data/lib/glossarist/v3/citation.rb +30 -0
  98. data/lib/glossarist/v3/concept_data.rb +46 -0
  99. data/lib/glossarist/v3/concept_document.rb +18 -0
  100. data/lib/glossarist/v3/concept_ref.rb +8 -0
  101. data/lib/glossarist/v3/concept_source.rb +16 -0
  102. data/lib/glossarist/v3/configuration.rb +13 -0
  103. data/lib/glossarist/v3/detailed_definition.rb +14 -0
  104. data/lib/glossarist/v3/image_entry.rb +21 -0
  105. data/lib/glossarist/v3/image_file.rb +31 -0
  106. data/lib/glossarist/v3/localized_concept.rb +9 -0
  107. data/lib/glossarist/v3/managed_concept.rb +26 -0
  108. data/lib/glossarist/v3/managed_concept_data.rb +34 -0
  109. data/lib/glossarist/v3/related_concept.rb +15 -0
  110. data/lib/glossarist/v3.rb +36 -0
  111. data/lib/glossarist/validation/asset_index.rb +4 -3
  112. data/lib/glossarist/validation/bibliography_index.rb +61 -30
  113. data/lib/glossarist/validation/rules/asciidoc_xref_rule.rb +2 -15
  114. data/lib/glossarist/validation/rules/authoritative_source_rule.rb +2 -15
  115. data/lib/glossarist/validation/rules/base.rb +5 -0
  116. data/lib/glossarist/validation/rules/bibliography_yaml_rule.rb +2 -3
  117. data/lib/glossarist/validation/rules/citation_completeness_rule.rb +5 -27
  118. data/lib/glossarist/validation/rules/dataset_context.rb +8 -3
  119. data/lib/glossarist/validation/rules/date_validity_rule.rb +1 -1
  120. data/lib/glossarist/validation/rules/designation_status_rule.rb +0 -1
  121. data/lib/glossarist/validation/rules/designation_type_rule.rb +1 -5
  122. data/lib/glossarist/validation/rules/domain_ref_rule.rb +37 -0
  123. data/lib/glossarist/validation/rules/domain_target_rule.rb +56 -0
  124. data/lib/glossarist/validation/rules/gcr_context.rb +12 -13
  125. data/lib/glossarist/validation/rules/image_reference_rule.rb +2 -17
  126. data/lib/glossarist/validation/rules/locality_completeness_rule.rb +58 -0
  127. data/lib/glossarist/validation/rules/localization_consistency_rule.rb +72 -0
  128. data/lib/glossarist/validation/rules/localization_presence_rule.rb +1 -1
  129. data/lib/glossarist/validation/rules/model_validity_rule.rb +71 -0
  130. data/lib/glossarist/validation/rules/orphaned_bibliography_rule.rb +1 -13
  131. data/lib/glossarist/validation/rules/orphaned_images_rule.rb +16 -11
  132. data/lib/glossarist/validation/rules/ref_shape_rule.rb +68 -0
  133. data/lib/glossarist/validation/rules/related_concept_cycle_rule.rb +1 -3
  134. data/lib/glossarist/validation/rules/related_concept_symmetry_rule.rb +1 -3
  135. data/lib/glossarist/validation/rules/related_concept_target_rule.rb +64 -0
  136. data/lib/glossarist/validation/rules/schema_version_rule.rb +41 -0
  137. data/lib/glossarist/validation/rules/source_type_rule.rb +1 -15
  138. data/lib/glossarist/validation/rules/source_urn_format_rule.rb +65 -0
  139. data/lib/glossarist/validation/rules/uuid_format_rule.rb +33 -0
  140. data/lib/glossarist/validation/rules.rb +10 -43
  141. data/lib/glossarist/validation/validation_issue.rb +14 -11
  142. data/lib/glossarist/validation_result.rb +12 -22
  143. data/lib/glossarist/version.rb +1 -1
  144. data/lib/glossarist.rb +10 -0
  145. data/memory/project-status.md +43 -0
  146. data/scripts/migrate_dataset.rb +180 -0
  147. data/scripts/migrate_isotc204_to_v3.rb +134 -0
  148. data/scripts/migrate_isotc211_to_v3.rb +153 -0
  149. data/scripts/migrate_osgeo_to_v3.rb +155 -0
  150. data/scripts/upgrade_dataset_to_v3.rb +47 -0
  151. metadata +112 -6
  152. data/TODO.integration/01-gcr-package-cli.md +0 -180
  153. data/lib/glossarist/rdf/skos_concept.rb +0 -43
  154. data/lib/glossarist/rdf/skos_vocabulary.rb +0 -25
  155. data/lib/glossarist/transforms/concept_to_skos_transform.rb +0 -131
data/config.yml CHANGED
@@ -26,23 +26,90 @@ designation:
26
26
  base:
27
27
  normative_status:
28
28
  - preferred
29
- - deprecated
30
29
  - admitted
30
+ - deprecated
31
+ - superseded
31
32
  - <símbolo> # in iev-data => '<symbol>'
32
33
  - 티에스 # in iev-data => translates to 'TS' I think it is a synonym and not status.
33
34
  - prąd startowy # in iev-data => 'starting current' I think it is a synonym and not status.
35
+ relationship_type:
36
+ # Term-level (designation-to-designation within the same concept entry)
37
+ - abbreviated_form_for
38
+ - short_form_for
34
39
 
35
40
  related_concept:
36
41
  type:
42
+ # Lifecycle (ISO 10241-1)
37
43
  - deprecates
38
44
  - supersedes
39
45
  - superseded_by
46
+ # Hierarchical (ISO 10241-1 / ISO 25964)
40
47
  - narrower
41
48
  - broader
49
+ # Hierarchical sub-types — ISO 25964 generic (BTG/NTG)
50
+ - broader_generic
51
+ - narrower_generic
52
+ # Hierarchical sub-types — ISO 25964 partitive (BTP/NTP)
53
+ - broader_partitive
54
+ - narrower_partitive
55
+ # Hierarchical sub-types — ISO 25964 instantial (BTI/NTI)
56
+ - broader_instantial
57
+ - narrower_instantial
58
+ # Equivalence (ISO 10241-1 / ISO 25964 exactMatch / SKOS)
42
59
  - equivalent
60
+ # Approximate equivalence (ISO 25964 closeMatch / SKOS)
61
+ - close_match
62
+ # Cross-vocabulary mapping (SKOS)
63
+ - broad_match
64
+ - narrow_match
65
+ - related_match
66
+ # Comparative (ISO 10241-1)
43
67
  - compare
44
68
  - contrast
69
+ # Associative (ISO 10241-1 / ISO 25964 RT / TBX crossReference)
45
70
  - see
71
+ # Associative sub-types (ISO 25964 / TBX)
72
+ - related_concept
73
+ - related_concept_broader
74
+ - related_concept_narrower
75
+ # Associative — spatial/temporal (ISO 25964 / TBX)
76
+ - sequentially_related_concept
77
+ - spatially_related_concept
78
+ - temporally_related_concept
79
+ # Lexical (ISO 12620 / TBX)
80
+ - homograph
81
+ - false_friend
82
+
83
+ iso12620:
84
+ term_type:
85
+ # ISO 12620 term type classification for designations.
86
+ # Orthographic / structural:
87
+ - abbreviation
88
+ - acronym
89
+ - clipped_term
90
+ - full_form
91
+ - initialism
92
+ - short_form
93
+ - transliterated_form
94
+ - transcribed_form
95
+ - variant
96
+ # Symbolic / formulaic:
97
+ - equation
98
+ - formula
99
+ - logical_expression
100
+ - symbol
101
+ # Usage / provenance:
102
+ - common_name
103
+ - entry_term
104
+ - internationalism
105
+ - international_scientific_term
106
+ - part_number
107
+ - phraseological_unit
108
+ - shortcut
109
+ - sku
110
+ - standard_text
111
+ - synonym
112
+ - synonymous_phrase
46
113
 
47
114
  abbreviation:
48
115
  type:
data/glossarist.gemspec CHANGED
@@ -32,9 +32,11 @@ Gem::Specification.new do |spec|
32
32
  spec.require_paths = ["lib"]
33
33
 
34
34
  spec.add_dependency "lutaml-model", "~> 0.8.5"
35
+ spec.add_dependency "paint", "~> 2.3"
35
36
  spec.add_dependency "relaton", ">= 2.0.0", "< 3"
36
37
  spec.add_dependency "rubyzip", ">= 2.3", "< 3"
37
38
  spec.add_dependency "sts", "~> 0.5.6"
39
+ spec.add_dependency "table_tennis", "~> 0.0"
38
40
  spec.add_dependency "tbx", "~> 0.1"
39
41
  spec.add_dependency "thor"
40
42
  end
@@ -1,122 +1,41 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Glossarist
2
4
  class Citation < Lutaml::Model::Serializable
3
- # Unstructured (plain text) reference.
4
- # @return [String]
5
- attribute :text, :string
6
-
7
- # Source in structured reference.
8
- # @return [String]
9
- attribute :source, :string
10
-
11
- # Document ID in structured reference.
12
- # @return [String]
13
- attribute :id, :string
14
-
15
- # Document version in structured reference.
16
- # @return [String]
17
- attribute :version, :string
5
+ class Ref < Lutaml::Model::Serializable
6
+ attribute :source, :string
7
+ attribute :id, :string
8
+ attribute :version, :string
9
+
10
+ key_value do
11
+ map :source, to: :source
12
+ map :id, to: :id
13
+ map :version, to: :version
14
+ end
15
+ end
18
16
 
19
- # @return [String]
20
- # Referred locality of the document.
17
+ attribute :ref, Ref
21
18
  attribute :locality, Locality
22
-
23
- # Link to document.
24
- # @return [String]
25
19
  attribute :link, :string
26
-
27
- # Original ref text before parsing.
28
- # @return [String]
29
- # @note This attribute is likely to be removed or reworked in future.
30
- # It is arguably not relevant to Glossarist itself.
31
20
  attribute :original, :string
32
-
33
- attribute :ref, :string
34
-
35
21
  attribute :custom_locality, CustomLocality, collection: true
36
22
 
37
23
  key_value do
38
- map :id, to: :id, with: { from: :id_from_yaml, to: :id_to_yaml }
39
- map :text, to: :text, with: { from: :text_from_yaml, to: :text_to_yaml }
40
- map :source, to: :source,
41
- with: { from: :source_from_yaml, to: :source_to_yaml }
42
- map :version, to: :version,
43
- with: { from: :version_from_yaml, to: :version_to_yaml }
44
- map :ref, to: :ref, with: { from: :ref_from_yaml, to: :ref_to_yaml }
24
+ map :ref, to: :ref
45
25
  map %i[clause locality],
46
26
  to: :locality,
47
- with: { from: :clause_from_yaml, to: :clause_to_yaml }
27
+ with: { from: :locality_from_yaml, to: :locality_to_yaml }
48
28
  map :link, to: :link
49
29
  map :original, to: :original
50
30
  map %i[custom_locality customLocality], to: :custom_locality
51
31
  end
52
32
 
53
- def ref_from_yaml(model, value)
54
- model.ref = value
55
- end
56
-
57
- def ref_to_yaml(model, doc)
58
- doc["ref"] = if model.structured?
59
- ref_hash(model)
60
- else
61
- model.text
62
- end
63
- end
64
-
65
- def id_from_yaml(model, value)
66
- model.id = value
67
- end
68
-
69
- def id_to_yaml(_model, _doc)
70
- # skip, will be handled in ref
71
- end
72
-
73
- def text_from_yaml(model, value)
74
- model.text = value
75
- end
76
-
77
- def text_to_yaml(_model, _doc)
78
- # skip, will be handled in ref
33
+ def label
34
+ parts = [ref&.source, ref&.id].compact
35
+ parts.empty? ? nil : parts.join(" ")
79
36
  end
80
37
 
81
- def source_from_yaml(model, value)
82
- model.source = value
83
- end
84
-
85
- def source_to_yaml(_model, _doc)
86
- # skip, will be handled in ref
87
- end
88
-
89
- def version_from_yaml(model, value)
90
- model.version = value
91
- end
92
-
93
- def version_to_yaml(_model, _doc)
94
- # skip, will be handled in ref
95
- end
96
-
97
- def ref_hash(model = self)
98
- {
99
- "source" => model.source,
100
- "id" => model.id,
101
- "version" => model.version,
102
- }.compact
103
- end
104
-
105
- def ref=(ref)
106
- if ref.is_a?(Hash)
107
- @source = ref["source"]
108
- @id = ref["id"]
109
- @version = ref["version"]
110
- else
111
- @text = ref
112
- end
113
- end
114
-
115
- def clause_from_yaml(model, value) # rubocop:disable Metrics/AbcSize
116
- # accepts old format like
117
- # clause: "11"
118
- # or new format like
119
- # locality: { type: "clause", reference_from: "11", reference_to: "12" }
38
+ def locality_from_yaml(model, value)
120
39
  locality = Locality.new
121
40
 
122
41
  if value.is_a?(Hash)
@@ -132,29 +51,13 @@ module Glossarist
132
51
  model.locality = locality
133
52
  end
134
53
 
135
- def clause_to_yaml(model, doc) # rubocop:disable Metrics/AbcSize
136
- if model.locality
137
- doc["locality"] = {}
138
- doc["locality"]["type"] = model.locality.type
139
-
140
- if model.locality.reference_from
141
- doc["locality"]["reference_from"] = model.locality.reference_from
142
- end
143
-
144
- if model.locality.reference_to
145
- doc["locality"]["reference_to"] = model.locality.reference_to
146
- end
147
- end
148
- end
149
-
150
- def plain?
151
- (source && id && version).nil?
152
- end
54
+ def locality_to_yaml(model, doc)
55
+ return unless model.locality
153
56
 
154
- # Whether it is a structured ref.
155
- # @return [Boolean]
156
- def structured?
157
- !plain?
57
+ doc["locality"] = {}
58
+ doc["locality"]["type"] = model.locality.type
59
+ doc["locality"]["reference_from"] = model.locality.reference_from if model.locality.reference_from
60
+ doc["locality"]["reference_to"] = model.locality.reference_to if model.locality.reference_to
158
61
  end
159
62
  end
160
63
  end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paint"
4
+
5
+ module Glossarist
6
+ class CLI
7
+ class CompareCommand
8
+ def initialize(new_path, old_path, options)
9
+ @new_path = new_path
10
+ @old_path = old_path
11
+ @options = options
12
+ end
13
+
14
+ def run
15
+ new_concepts = ConceptCollector.collect(@new_path)
16
+ old_concepts = ConceptCollector.collect(@old_path)
17
+
18
+ result = ConceptComparator.new(
19
+ new_concepts: new_concepts,
20
+ old_concepts: old_concepts,
21
+ ).compare(show_diffs: !@options[:no_diffs])
22
+
23
+ report(result)
24
+ end
25
+
26
+ private
27
+
28
+ def report(result)
29
+ case @options[:format]
30
+ when "json"
31
+ output result.to_json
32
+ when "yaml"
33
+ output result.to_yaml
34
+ else
35
+ print_text_report(result)
36
+ end
37
+ end
38
+
39
+ def print_text_report(result)
40
+ puts
41
+ puts Paint["Concept Comparison", :bold]
42
+ puts
43
+
44
+ print_counts(result)
45
+ print_new_only(result)
46
+ print_old_only(result)
47
+ print_similarity(result)
48
+
49
+ puts
50
+ end
51
+
52
+ def print_counts(result)
53
+ puts " #{Paint['New:', :bold]} #{result.new_count} concepts"
54
+ puts " #{Paint['Old:', :bold]} #{result.old_count} concepts"
55
+ puts " #{Paint['Matched:', :bold]} #{result.matched.length}"
56
+ puts " #{Paint['New only:', :bold]} #{result.new_only.length}"
57
+ puts " #{Paint['Old only:', :bold]} #{result.old_only.length}"
58
+ end
59
+
60
+ def print_new_only(result)
61
+ return unless result.new_only.any?
62
+
63
+ puts
64
+ puts " #{Paint['New concepts (not in old):', :green, :bold]}"
65
+ result.new_only.each { |id| puts " + #{id}" }
66
+ end
67
+
68
+ def print_old_only(result)
69
+ return unless result.old_only.any?
70
+
71
+ puts
72
+ puts " #{Paint['Removed concepts (not in new):', :red, :bold]}"
73
+ result.old_only.each { |id| puts " - #{id}" }
74
+ end
75
+
76
+ def print_similarity(result)
77
+ return unless result.diffs.any?
78
+
79
+ puts
80
+ puts " #{Paint['Per-concept similarity:', :bold]}"
81
+ result.diffs.each do |diff|
82
+ color = similarity_color(diff.similarity)
83
+ puts " #{diff.concept_id}: #{Paint["#{diff.similarity}%", color]}"
84
+ end
85
+ end
86
+
87
+ def similarity_color(value)
88
+ if value >= 100
89
+ :green
90
+ elsif value >= 90
91
+ :yellow
92
+ else
93
+ :red
94
+ end
95
+ end
96
+
97
+ def output(content)
98
+ if @options[:report]
99
+ File.write(@options[:report], content)
100
+ else
101
+ puts content
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -48,8 +48,8 @@ module Glossarist
48
48
  end
49
49
 
50
50
  def resolve_metadata_from_package(package)
51
- @options[:shortname] ||= package.metadata["shortname"]
52
- @options[:uri_prefix] ||= package.metadata["uri_prefix"]
51
+ @options[:shortname] ||= package.metadata.shortname
52
+ @options[:uri_prefix] ||= package.metadata.uri_prefix
53
53
  end
54
54
 
55
55
  def resolve_shortname
@@ -73,17 +73,15 @@ module Glossarist
73
73
  end
74
74
 
75
75
  def export_jsonld(concepts, name, output_dir)
76
- require "glossarist/transforms/concept_to_skos_transform"
77
- vocab = Transforms::ConceptToSkosTransform.transform_document(concepts,
78
- transform_options)
79
- File.write(File.join(output_dir, "#{name}.jsonld"), vocab.to_jsonld)
76
+ require "glossarist/transforms/concept_to_gloss_transform"
77
+ transform = Transforms::ConceptToGlossTransform.new(nil, transform_options)
78
+ File.write(File.join(output_dir, "#{name}.jsonld"), transform.to_jsonld(concepts))
80
79
  end
81
80
 
82
81
  def export_turtle(concepts, name, output_dir)
83
- require "glossarist/transforms/concept_to_skos_transform"
84
- vocab = Transforms::ConceptToSkosTransform.transform_document(concepts,
85
- transform_options)
86
- File.write(File.join(output_dir, "#{name}.ttl"), vocab.to_turtle)
82
+ require "glossarist/transforms/concept_to_gloss_transform"
83
+ transform = Transforms::ConceptToGlossTransform.new(nil, transform_options)
84
+ File.write(File.join(output_dir, "#{name}.ttl"), transform.to_turtle(concepts))
87
85
  end
88
86
 
89
87
  def export_tbx(concepts, name, output_dir)
@@ -94,12 +92,11 @@ module Glossarist
94
92
  end
95
93
 
96
94
  def export_jsonl(concepts, name, output_dir)
97
- require "glossarist/transforms/concept_to_skos_transform"
95
+ require "glossarist/transforms/concept_to_gloss_transform"
98
96
  File.open(File.join(output_dir, "#{name}.jsonl"), "w") do |f|
99
97
  concepts.each do |concept|
100
- skos = Transforms::ConceptToSkosTransform.transform(concept,
101
- transform_options)
102
- f.write(skos.to_jsonld)
98
+ transform = Transforms::ConceptToGlossTransform.new(concept, transform_options)
99
+ f.write(transform.to_jsonl_line)
103
100
  f.write("\n")
104
101
  end
105
102
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "paint"
4
+ require "table_tennis"
5
+
3
6
  module Glossarist
4
7
  class CLI
5
8
  class ValidateCommand
@@ -9,47 +12,135 @@ module Glossarist
9
12
  end
10
13
 
11
14
  def run
12
- result = DatasetValidator.new.validate(
15
+ text_output = @options[:format] == "text"
16
+ validator = DatasetValidator.new(on_progress: text_output ? method(:print_progress) : nil)
17
+ result = validator.validate(
13
18
  @path,
14
19
  strict: @options[:strict],
15
20
  reference_path: @options[:reference_path],
16
21
  )
22
+
23
+ $stderr.print "\r#{' ' * 60}\r" if text_output
17
24
  report(result)
18
- exit_code = result.errors.any? || (@options[:strict] && result.warnings.any?) ? 1 : 0
19
- exit(exit_code) unless exit_code.zero?
25
+ exit(1) unless result.errors.empty? && !strict_failure?(result)
20
26
  end
21
27
 
22
28
  private
23
29
 
30
+ def strict_failure?(result)
31
+ @options[:strict] && result.warnings.any?
32
+ end
33
+
34
+ def print_progress(current, total)
35
+ pct = (current.to_f / total * 100).round
36
+ bar_width = 30
37
+ filled = (current.to_f / total * bar_width).round
38
+ bar = "#{'█' * filled}#{'░' * (bar_width - filled)}"
39
+
40
+ $stderr.print "\r #{Paint['Validating', :bold]} #{bar} #{current}/#{total} (#{pct}%)"
41
+ $stderr.flush
42
+ end
43
+
24
44
  def report(result)
25
45
  case @options[:format]
26
46
  when "json"
27
- require "json"
28
- puts JSON.pretty_generate(result.to_h)
47
+ puts result.to_json
29
48
  when "yaml"
30
- require "yaml"
31
- puts YAML.dump(result.to_h)
49
+ puts result.to_yaml
32
50
  else
33
- report_text(result)
51
+ print_text_output(result)
52
+ print_table_output(result) if result.issues.any?
34
53
  end
35
54
  end
36
55
 
37
- def report_text(result)
38
- if result.valid?
39
- puts "Valid."
40
- else
41
- puts "Invalid."
42
- result.errors.each { |e| puts " ERROR: #{e}" }
56
+ def print_text_output(result)
57
+ puts
58
+ puts Paint["Validating #{@path}", :bold]
59
+ puts
60
+
61
+ if result.issues.empty?
62
+ puts " #{Paint['No issues found.', :green, :bold]}"
63
+ return
43
64
  end
44
65
 
45
- if result.warnings.any?
46
- result.warnings.each do |w|
47
- puts " WARNING: #{w}"
48
- end
66
+ print_grouped_issues(result)
67
+ print_summary_line(result)
68
+ end
69
+
70
+ def print_grouped_issues(result)
71
+ result.issues
72
+ .group_by { |i| i.location || "(dataset)" }
73
+ .sort_by { |loc, issues| [has_errors?(issues) ? 0 : 1, loc] }
74
+ .each { |location, issues| print_location_group(location, issues) }
75
+ end
76
+
77
+ def has_errors?(issues)
78
+ issues.any?(&:error?)
79
+ end
80
+
81
+ def print_location_group(location, issues)
82
+ puts " #{Paint[location, :cyan, :bold]}"
83
+ issues.sort_by { |i| issue_sort_key(i) }
84
+ .each { |issue| print_issue(issue) }
85
+ puts
86
+ end
87
+
88
+ def issue_sort_key(issue)
89
+ [issue.error? ? 0 : 1, issue.code || "￿", issue.message]
90
+ end
91
+
92
+ def print_issue(issue)
93
+ color = issue.error? ? :red : :yellow
94
+ label = Paint[issue.error? ? "ERROR" : " WARN", color, :bold]
95
+ code = Paint["%-8s" % (issue.code || ""), :magenta]
96
+ msg_col = 21
97
+
98
+ puts " #{label} #{code} #{issue.message}"
99
+ puts "#{' ' * msg_col}#{Paint[issue.suggestion, :green]}" if issue.suggestion
100
+ end
101
+
102
+ def print_summary_line(result)
103
+ error_count = result.issues.count(&:error?)
104
+ warning_count = result.issues.count(&:warning?)
105
+
106
+ status = error_count.positive? ? Paint["INVALID", :red, :bold] : Paint["VALID", :green, :bold]
107
+
108
+ details = []
109
+ details << Paint["#{error_count} error(s)", :red] if error_count.positive?
110
+ details << Paint["#{warning_count} warning(s)", :yellow] if warning_count.positive?
111
+
112
+ puts " #{status} #{details.join(', ')}"
113
+ end
114
+
115
+ def print_table_output(result)
116
+ rows = build_summary_rows(result)
117
+ return if rows.empty?
118
+
119
+ options = {
120
+ title: "Issues by Rule",
121
+ columns: %i[code severity count],
122
+ headers: { code: "Rule", severity: "Level", count: "Count" },
123
+ color_scales: { count: :gw },
124
+ mark: ->(row) { row[:severity] == "error" },
125
+ zebra: true,
126
+ }
127
+ puts
128
+ puts TableTennis.new(rows, options)
129
+ end
130
+
131
+ def build_summary_rows(result)
132
+ counts = Hash.new(0)
133
+ severities = {}
134
+
135
+ result.issues.each do |issue|
136
+ key = issue.code || "unknown"
137
+ counts[key] += 1
138
+ severities[key] ||= issue.severity
49
139
  end
50
140
 
51
- total = result.errors.length + result.warnings.length
52
- puts "#{total} issue(s) found."
141
+ counts.sort_by { |_, c| -c }.map do |code, count|
142
+ { code: code, severity: severities[code], count: count }
143
+ end
53
144
  end
54
145
  end
55
146
  end
@@ -9,6 +9,7 @@ module Glossarist
9
9
  autoload :ValidateCommand, "#{__dir__}/cli/validate_command"
10
10
  autoload :ImportCommand, "#{__dir__}/cli/import_command"
11
11
  autoload :ExportCommand, "#{__dir__}/cli/export_command"
12
+ autoload :CompareCommand, "#{__dir__}/cli/compare_command"
12
13
  desc "generate_latex", "Convert Concepts to Latex format"
13
14
 
14
15
  option :concepts_path, aliases: :p, required: true,
@@ -120,6 +121,23 @@ module Glossarist
120
121
  CLI::ExportCommand.new(path, options).run
121
122
  end
122
123
 
124
+ desc "compare NEW_PATH OLD_PATH", "Compare two concept datasets"
125
+ option :format, type: :string, default: "text",
126
+ enum: %w[text json yaml],
127
+ desc: "Output format"
128
+ option :report, type: :string,
129
+ desc: "Write report to file"
130
+ option :no_diffs, type: :boolean, default: false,
131
+ desc: "Skip per-concept diff computation"
132
+ def compare(new_path, old_path)
133
+ CLI::CompareCommand.new(new_path, old_path, options).run
134
+ end
135
+
136
+ desc "version", "Show Glossarist version"
137
+ def version
138
+ puts Glossarist::VERSION
139
+ end
140
+
123
141
  def method_missing(*args)
124
142
  warn "No method found named: #{args[0]}"
125
143
  warn "Run with `--help` or `-h` to see available options"
@@ -39,9 +39,11 @@ module Glossarist
39
39
  def populate_bibliographies(concepts)
40
40
  concepts.each do |concept|
41
41
  concept.default_lang.sources.each do |source|
42
- next if source.origin.text.nil?
42
+ next if source.origin.ref.nil?
43
+ ref_text = source.origin.ref.source
44
+ next if ref_text.nil?
43
45
 
44
- fetch(source.origin.text)
46
+ fetch(ref_text)
45
47
  end
46
48
  end
47
49
  end
@@ -7,6 +7,8 @@ module Glossarist
7
7
 
8
8
  index_by :language_code
9
9
 
10
+ alias :length :size
11
+
10
12
  def [](lang_code)
11
13
  find_by(:language_code, lang_code.to_s)
12
14
  end