glossarist 2.6.6 → 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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +90 -29
  3. data/glossarist.gemspec +2 -0
  4. data/lib/glossarist/citation.rb +26 -123
  5. data/lib/glossarist/cli/compare_command.rb +106 -0
  6. data/lib/glossarist/cli/export_command.rb +11 -14
  7. data/lib/glossarist/cli/validate_command.rb +111 -20
  8. data/lib/glossarist/cli.rb +18 -0
  9. data/lib/glossarist/collections/bibliography_collection.rb +4 -2
  10. data/lib/glossarist/collections/localization_collection.rb +2 -0
  11. data/lib/glossarist/comparison_result.rb +35 -0
  12. data/lib/glossarist/concept_collector.rb +44 -0
  13. data/lib/glossarist/concept_comparator.rb +72 -0
  14. data/lib/glossarist/concept_data.rb +16 -0
  15. data/lib/glossarist/concept_diff.rb +15 -0
  16. data/lib/glossarist/concept_document.rb +11 -0
  17. data/lib/glossarist/concept_manager.rb +19 -5
  18. data/lib/glossarist/concept_ref.rb +13 -0
  19. data/lib/glossarist/concept_validator.rb +6 -1
  20. data/lib/glossarist/context_configuration.rb +90 -0
  21. data/lib/glossarist/dataset_validator.rb +8 -4
  22. data/lib/glossarist/designation/prefix.rb +17 -0
  23. data/lib/glossarist/designation/suffix.rb +17 -0
  24. data/lib/glossarist/gcr_metadata.rb +7 -14
  25. data/lib/glossarist/gcr_package.rb +35 -23
  26. data/lib/glossarist/gcr_validator.rb +38 -17
  27. data/lib/glossarist/localized_concept.rb +8 -0
  28. data/lib/glossarist/managed_concept.rb +39 -6
  29. data/lib/glossarist/managed_concept_data.rb +2 -1
  30. data/lib/glossarist/rdf/ext/jsonld_transform_ext.rb +208 -0
  31. data/lib/glossarist/rdf/ext/mapping_ext.rb +37 -0
  32. data/lib/glossarist/rdf/ext/mapping_rule_ext.rb +27 -0
  33. data/lib/glossarist/rdf/ext/member_rule_ext.rb +34 -0
  34. data/lib/glossarist/rdf/ext/turtle_transform_ext.rb +222 -0
  35. data/lib/glossarist/rdf/ext.rb +39 -0
  36. data/lib/glossarist/rdf/gloss_citation.rb +36 -0
  37. data/lib/glossarist/rdf/gloss_concept.rb +58 -0
  38. data/lib/glossarist/rdf/gloss_concept_date.rb +24 -0
  39. data/lib/glossarist/rdf/gloss_concept_reference.rb +29 -0
  40. data/lib/glossarist/rdf/gloss_concept_source.rb +37 -0
  41. data/lib/glossarist/rdf/gloss_designation.rb +146 -0
  42. data/lib/glossarist/rdf/gloss_detailed_definition.rb +24 -0
  43. data/lib/glossarist/rdf/gloss_grammar_info.rb +57 -0
  44. data/lib/glossarist/rdf/gloss_locality.rb +25 -0
  45. data/lib/glossarist/rdf/gloss_localized_concept.rb +67 -0
  46. data/lib/glossarist/rdf/gloss_non_verbal_rep.rb +31 -0
  47. data/lib/glossarist/rdf/gloss_pronunciation.rb +32 -0
  48. data/lib/glossarist/rdf/gloss_reference.rb +55 -0
  49. data/lib/glossarist/rdf/namespaces/glossarist_namespace.rb +12 -0
  50. data/lib/glossarist/rdf/namespaces/iso_thes_namespace.rb +12 -0
  51. data/lib/glossarist/rdf/namespaces/owl_namespace.rb +12 -0
  52. data/lib/glossarist/rdf/namespaces/prov_namespace.rb +12 -0
  53. data/lib/glossarist/rdf/namespaces/rdf_namespace.rb +12 -0
  54. data/lib/glossarist/rdf/namespaces/skosxl_namespace.rb +12 -0
  55. data/lib/glossarist/rdf/namespaces.rb +8 -2
  56. data/lib/glossarist/rdf/relationships.rb +19 -0
  57. data/lib/glossarist/rdf/v3/configuration.rb +15 -0
  58. data/lib/glossarist/rdf/v3.rb +79 -0
  59. data/lib/glossarist/rdf.rb +22 -2
  60. data/lib/glossarist/reference_extractor.rb +12 -19
  61. data/lib/glossarist/reference_resolver.rb +3 -3
  62. data/lib/glossarist/related_concept.rb +2 -10
  63. data/lib/glossarist/schema_migration.rb +39 -0
  64. data/lib/glossarist/sts/term_mapper.rb +2 -2
  65. data/lib/glossarist/transforms/concept_to_gloss_transform.rb +355 -0
  66. data/lib/glossarist/transforms.rb +2 -2
  67. data/lib/glossarist/v1/concept.rb +17 -17
  68. data/lib/glossarist/v2/citation.rb +36 -0
  69. data/lib/glossarist/v2/concept_data.rb +46 -0
  70. data/lib/glossarist/v2/concept_document.rb +18 -0
  71. data/lib/glossarist/v2/concept_ref.rb +8 -0
  72. data/lib/glossarist/v2/concept_source.rb +16 -0
  73. data/lib/glossarist/v2/configuration.rb +13 -0
  74. data/lib/glossarist/v2/detailed_definition.rb +14 -0
  75. data/lib/glossarist/v2/localized_concept.rb +9 -0
  76. data/lib/glossarist/v2/managed_concept.rb +25 -0
  77. data/lib/glossarist/v2/managed_concept_data.rb +49 -0
  78. data/lib/glossarist/v2/related_concept.rb +15 -0
  79. data/lib/glossarist/v2.rb +28 -0
  80. data/lib/glossarist/v3/bibliography_entry.rb +19 -0
  81. data/lib/glossarist/v3/bibliography_file.rb +27 -0
  82. data/lib/glossarist/v3/citation.rb +30 -0
  83. data/lib/glossarist/v3/concept_data.rb +46 -0
  84. data/lib/glossarist/v3/concept_document.rb +18 -0
  85. data/lib/glossarist/v3/concept_ref.rb +8 -0
  86. data/lib/glossarist/v3/concept_source.rb +16 -0
  87. data/lib/glossarist/v3/configuration.rb +13 -0
  88. data/lib/glossarist/v3/detailed_definition.rb +14 -0
  89. data/lib/glossarist/v3/image_entry.rb +21 -0
  90. data/lib/glossarist/v3/image_file.rb +31 -0
  91. data/lib/glossarist/v3/localized_concept.rb +9 -0
  92. data/lib/glossarist/v3/managed_concept.rb +26 -0
  93. data/lib/glossarist/v3/managed_concept_data.rb +34 -0
  94. data/lib/glossarist/v3/related_concept.rb +15 -0
  95. data/lib/glossarist/v3.rb +36 -0
  96. data/lib/glossarist/validation/bibliography_index.rb +61 -30
  97. data/lib/glossarist/validation/rules/asciidoc_xref_rule.rb +2 -15
  98. data/lib/glossarist/validation/rules/authoritative_source_rule.rb +2 -15
  99. data/lib/glossarist/validation/rules/base.rb +5 -0
  100. data/lib/glossarist/validation/rules/bibliography_yaml_rule.rb +2 -3
  101. data/lib/glossarist/validation/rules/citation_completeness_rule.rb +5 -27
  102. data/lib/glossarist/validation/rules/dataset_context.rb +8 -3
  103. data/lib/glossarist/validation/rules/date_validity_rule.rb +1 -1
  104. data/lib/glossarist/validation/rules/designation_status_rule.rb +0 -1
  105. data/lib/glossarist/validation/rules/designation_type_rule.rb +1 -5
  106. data/lib/glossarist/validation/rules/domain_ref_rule.rb +37 -0
  107. data/lib/glossarist/validation/rules/domain_target_rule.rb +56 -0
  108. data/lib/glossarist/validation/rules/gcr_context.rb +12 -13
  109. data/lib/glossarist/validation/rules/image_reference_rule.rb +2 -17
  110. data/lib/glossarist/validation/rules/locality_completeness_rule.rb +58 -0
  111. data/lib/glossarist/validation/rules/localization_consistency_rule.rb +72 -0
  112. data/lib/glossarist/validation/rules/localization_presence_rule.rb +1 -1
  113. data/lib/glossarist/validation/rules/model_validity_rule.rb +71 -0
  114. data/lib/glossarist/validation/rules/orphaned_bibliography_rule.rb +1 -13
  115. data/lib/glossarist/validation/rules/orphaned_images_rule.rb +16 -11
  116. data/lib/glossarist/validation/rules/ref_shape_rule.rb +68 -0
  117. data/lib/glossarist/validation/rules/related_concept_cycle_rule.rb +1 -3
  118. data/lib/glossarist/validation/rules/related_concept_symmetry_rule.rb +1 -3
  119. data/lib/glossarist/validation/rules/related_concept_target_rule.rb +64 -0
  120. data/lib/glossarist/validation/rules/schema_version_rule.rb +41 -0
  121. data/lib/glossarist/validation/rules/source_type_rule.rb +1 -15
  122. data/lib/glossarist/validation/rules/source_urn_format_rule.rb +65 -0
  123. data/lib/glossarist/validation/rules/uuid_format_rule.rb +33 -0
  124. data/lib/glossarist/validation/rules.rb +10 -43
  125. data/lib/glossarist/validation/validation_issue.rb +14 -11
  126. data/lib/glossarist/validation_result.rb +12 -22
  127. data/lib/glossarist/version.rb +1 -1
  128. data/lib/glossarist.rb +9 -0
  129. data/memory/project-status.md +43 -0
  130. data/scripts/migrate_dataset.rb +180 -0
  131. data/scripts/migrate_isotc204_to_v3.rb +134 -0
  132. data/scripts/migrate_isotc211_to_v3.rb +153 -0
  133. data/scripts/migrate_osgeo_to_v3.rb +155 -0
  134. data/scripts/upgrade_dataset_to_v3.rb +47 -0
  135. metadata +111 -6
  136. data/TODO.integration/01-gcr-package-cli.md +0 -180
  137. data/lib/glossarist/rdf/skos_concept.rb +0 -43
  138. data/lib/glossarist/rdf/skos_vocabulary.rb +0 -25
  139. data/lib/glossarist/transforms/concept_to_skos_transform.rb +0 -131
@@ -4,6 +4,11 @@ module Glossarist
4
4
  module Validation
5
5
  module Rules
6
6
  class Base
7
+ def self.inherited(subclass)
8
+ super
9
+ Registry.register(subclass)
10
+ end
11
+
7
12
  def code
8
13
  nil
9
14
  end
@@ -18,9 +18,8 @@ module Glossarist
18
18
  return [] unless bib_content
19
19
 
20
20
  begin
21
- data = YAML.safe_load(bib_content)
22
- return [] if data.nil? || data.is_a?(Hash) || data.is_a?(Array)
23
- rescue Psych::SyntaxError => e
21
+ V3::BibliographyFile.from_yaml(bib_content)
22
+ rescue StandardError => e
24
23
  return [issue(
25
24
  "bibliography.yaml is invalid YAML: #{e.message}",
26
25
  code: code, severity: severity,
@@ -18,46 +18,24 @@ module Glossarist
18
18
  fname = context.file_name
19
19
  issues = []
20
20
 
21
- gather_all_sources(concept).each_with_index do |source, idx|
21
+ concept.localizations.flat_map(&:all_sources).each_with_index do |source, idx|
22
22
  origin = source.origin
23
23
  next unless origin
24
24
 
25
- if origin.text.nil? && origin.source.nil? && origin.id.nil?
25
+ ref = origin.ref
26
+ if ref.nil? || (ref.source.nil? && ref.id.nil?)
26
27
  issues << issue(
27
- "source #{idx + 1} has empty origin (no text, source, or id)",
28
+ "source #{idx + 1} has empty origin (no ref source or id)",
28
29
  code: "GLS-304", severity: severity,
29
30
  location: fname,
30
- suggestion: "Add at minimum an origin.text or origin.source + origin.id",
31
+ suggestion: "Add at minimum an origin.ref with source or id",
31
32
  )
32
33
  end
33
-
34
- next unless origin.structured? && origin.source.nil?
35
-
36
- issues << issue(
37
- "source #{idx + 1} is structured but missing source field",
38
- code: "GLS-304", severity: severity,
39
- location: fname,
40
- suggestion: "Add origin.source to the citation",
41
- )
42
34
  end
43
35
 
44
36
  issues
45
37
  end
46
-
47
- private
48
-
49
- def gather_all_sources(concept)
50
- sources = []
51
- concept.localizations.each do |l10n|
52
- (l10n.data&.sources || []).each { |s| sources << s }
53
- (l10n.data&.definition || []).each { |d| (d.sources || []).each { |s| sources << s } }
54
- (l10n.data&.notes || []).each { |n| (n.sources || []).each { |s| sources << s } }
55
- (l10n.data&.examples || []).each { |e| (e.sources || []).each { |s| sources << s } }
56
- end
57
- sources
58
- end
59
38
  end
60
39
  end
61
40
  end
62
41
  end
63
-
@@ -8,14 +8,19 @@ module Glossarist
8
8
 
9
9
  def initialize(path)
10
10
  @path = File.expand_path(path)
11
- @concepts = nil
11
+ @accumulated_concepts = []
12
12
  @bibliography_index = nil
13
13
  @asset_index = nil
14
14
  @declared_languages = nil
15
15
  end
16
16
 
17
+ def add_concept(concept)
18
+ @accumulated_concepts << concept
19
+ @concept_ids = nil
20
+ end
21
+
17
22
  def concepts
18
- @concepts ||= ConceptCollector.collect(@path)
23
+ @accumulated_concepts
19
24
  end
20
25
 
21
26
  def concept_ids
@@ -77,7 +82,7 @@ module Glossarist
77
82
  reg_path = File.join(@path, "register.yaml")
78
83
  return nil unless File.exist?(reg_path)
79
84
 
80
- YAML.safe_load_file(reg_path)
85
+ RegisterData.from_file(reg_path)
81
86
  end
82
87
 
83
88
  def build_localization_index
@@ -48,7 +48,7 @@ module Glossarist
48
48
 
49
49
  return if date_value.nil?
50
50
 
51
- str = date_value.respond_to?(:iso8601) ? date_value.iso8601 : date_value.to_s
51
+ str = date_value.is_a?(Date) || date_value.is_a?(Time) ? date_value.iso8601 : date_value.to_s
52
52
 
53
53
  begin
54
54
  DateTime.parse(str)
@@ -24,7 +24,6 @@ module Glossarist
24
24
  lang = l10n.language_code || "unknown"
25
25
  terms = l10n.data&.terms || []
26
26
  terms.each_with_index do |term, idx|
27
- next unless term.respond_to?(:normative_status)
28
27
  next if term.normative_status.nil? || term.normative_status.to_s.strip.empty?
29
28
 
30
29
  unless VALID_STATUSES.include?(term.normative_status.to_s)
@@ -43,11 +43,7 @@ module Glossarist
43
43
  private
44
44
 
45
45
  def designation_type(term)
46
- if term.is_a?(Hash)
47
- term["type"]
48
- elsif term.respond_to?(:type)
49
- term.type
50
- end
46
+ term.type if term.is_a?(Designation::Base)
51
47
  end
52
48
  end
53
49
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module Validation
5
+ module Rules
6
+ class DomainRefRule < Base
7
+ def code = "GLS-309"
8
+ def category = :quality
9
+ def severity = "warning"
10
+ def scope = :concept
11
+
12
+ def applicable?(context)
13
+ context.concept.data&.domains&.any?
14
+ end
15
+
16
+ def check(context)
17
+ concept = context.concept
18
+ fname = context.file_name
19
+ issues = []
20
+
21
+ (concept.data.domains || []).each_with_index do |domain, idx|
22
+ has_ref = domain.concept_id || domain.urn
23
+ unless has_ref
24
+ issues << issue(
25
+ "domain #{idx + 1} has neither concept_id nor urn",
26
+ location: fname,
27
+ suggestion: "Provide at least concept_id or urn for the domain reference",
28
+ )
29
+ end
30
+ end
31
+
32
+ issues
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module Validation
5
+ module Rules
6
+ # Validates that domain references point to concepts that exist in the
7
+ # dataset (for local refs with concept_id) or have a valid URN.
8
+ class DomainTargetRule < Base
9
+ URN_RE = %r{\Aurn:[a-z0-9][a-z0-9-]{0,31}:[a-z0-9()+,\-.:=@;$_!*'%/?#]+\z}i.freeze
10
+
11
+ def code = "GLS-111"
12
+ def category = :references
13
+ def severity = "warning"
14
+ def scope = :concept
15
+
16
+ def applicable?(context)
17
+ context.concept.data&.domains&.any?
18
+ end
19
+
20
+ def check(context)
21
+ concept = context.concept
22
+ fname = context.file_name
23
+ issues = []
24
+
25
+ (concept.data.domains || []).each_with_index do |domain, idx|
26
+ if domain.concept_id && local_domain?(domain)
27
+ unless context.concept_ids.include?(domain.concept_id)
28
+ issues << issue(
29
+ "domain #{idx + 1} references '#{domain.concept_id}' not in dataset",
30
+ location: fname,
31
+ suggestion: "Add concept '#{domain.concept_id}' or fix the domain ref",
32
+ )
33
+ end
34
+ elsif domain.urn
35
+ if domain.urn.start_with?("urn:") && !URN_RE.match?(domain.urn)
36
+ issues << issue(
37
+ "domain #{idx + 1} has invalid URN '#{domain.urn}'",
38
+ location: fname,
39
+ suggestion: "Fix the URN format",
40
+ )
41
+ end
42
+ end
43
+ end
44
+
45
+ issues
46
+ end
47
+
48
+ private
49
+
50
+ def local_domain?(domain)
51
+ domain.source.nil? || domain.source.strip.empty?
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -10,21 +10,20 @@ module Glossarist
10
10
 
11
11
  def initialize(zip_path)
12
12
  @zip_path = zip_path
13
+ @accumulated_concepts = []
13
14
  @metadata = nil
14
- @concepts = nil
15
15
  @bibliography_index = nil
16
16
  @asset_index = nil
17
17
  @zip_entries = nil
18
- @localization_index = nil
18
+ end
19
+
20
+ def add_concept(concept)
21
+ @accumulated_concepts << concept
22
+ @concept_ids = nil
19
23
  end
20
24
 
21
25
  def concepts
22
- @concepts ||= begin
23
- pkg = GcrPackage.load(@zip_path)
24
- pkg.concepts
25
- rescue StandardError
26
- []
27
- end
26
+ @accumulated_concepts
28
27
  end
29
28
 
30
29
  def concept_ids
@@ -41,11 +40,11 @@ module Glossarist
41
40
  end
42
41
 
43
42
  def bibliography_index
44
- @bibliography_index ||= begin
45
- bib_yaml = read_zip_file("bibliography.yaml")
46
- BibliographyIndex.build_from_concepts(concepts,
47
- bibliography_yaml: bib_yaml)
48
- end
43
+ @bibliography_index ||= BibliographyIndex.build_from_yaml(
44
+ concepts,
45
+ bibliography_yaml: read_zip_file("bibliography.yaml"),
46
+ images_yaml: read_zip_file("images.yaml"),
47
+ )
49
48
  end
50
49
 
51
50
  def asset_index
@@ -19,15 +19,12 @@ module Glossarist
19
19
  extractor = ReferenceExtractor.new
20
20
  issues = []
21
21
 
22
- # Text-embedded image references (image::path[])
23
22
  concept.localizations.each do |l10n|
24
23
  lang = l10n.language_code || "unknown"
25
- texts = extract_texts(l10n)
26
24
 
27
- texts.each do |text|
25
+ l10n.text_content.each do |text|
28
26
  next unless text
29
- refs = extractor.extract_from_text(text)
30
- refs.each do |ref|
27
+ extractor.extract_from_text(text).each do |ref|
31
28
  next unless ref.is_a?(AssetReference)
32
29
  next if context.asset_index.resolve?(ref.path)
33
30
 
@@ -41,7 +38,6 @@ module Glossarist
41
38
  end
42
39
  end
43
40
 
44
- # Model-level asset references (NonVerbRep, GraphicalSymbol)
45
41
  asset_refs = extractor.extract_asset_refs_from_concept(concept)
46
42
  asset_refs.each do |ref|
47
43
  next if context.asset_index.resolve?(ref.path)
@@ -56,18 +52,7 @@ module Glossarist
56
52
 
57
53
  issues
58
54
  end
59
-
60
- private
61
-
62
- def extract_texts(l10n)
63
- texts = []
64
- (l10n.data&.definition || []).each { |d| texts << d.content if d.content }
65
- (l10n.data&.notes || []).each { |n| texts << n.content if n.content }
66
- (l10n.data&.examples || []).each { |e| texts << e.content if e.content }
67
- texts
68
- end
69
55
  end
70
56
  end
71
57
  end
72
58
  end
73
-
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module Validation
5
+ module Rules
6
+ class LocalityCompletenessRule < Base
7
+ def code = "GLS-308"
8
+ def category = :quality
9
+ def severity = "warning"
10
+ def scope = :concept
11
+
12
+ def applicable?(context)
13
+ context.concept.localizations&.any?
14
+ end
15
+
16
+ def check(context)
17
+ concept = context.concept
18
+ fname = context.file_name
19
+ issues = []
20
+
21
+ all_origins(concept).each_with_index do |origin, idx|
22
+ next unless origin
23
+ next unless origin.locality
24
+
25
+ loc = origin.locality
26
+ if loc.type.nil? || loc.type.to_s.strip.empty?
27
+ issues << issue(
28
+ "source #{idx + 1} locality has no type",
29
+ location: fname,
30
+ suggestion: "Add locality type (e.g. 'clause')",
31
+ )
32
+ end
33
+
34
+ if loc.reference_from.nil? || loc.reference_from.to_s.strip.empty?
35
+ issues << issue(
36
+ "source #{idx + 1} locality has no reference_from",
37
+ location: fname,
38
+ suggestion: "Add locality.reference_from (e.g. '3.1.3.10')",
39
+ )
40
+ end
41
+ end
42
+
43
+ issues
44
+ end
45
+
46
+ private
47
+
48
+ def all_origins(concept)
49
+ origins = []
50
+ concept.localizations.each do |l10n|
51
+ (l10n.data&.sources || []).each { |s| origins << s.origin if s.origin }
52
+ end
53
+ origins
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module Validation
5
+ module Rules
6
+ # Verifies that every entry in localized_concepts map points to a loaded
7
+ # localization, and that every loaded localization has a corresponding
8
+ # entry in the map.
9
+ class LocalizationConsistencyRule < Base
10
+ def code = "GLS-017"
11
+ def category = :integrity
12
+ def severity = "error"
13
+ def scope = :concept
14
+
15
+ def applicable?(context)
16
+ context.concept.localizations&.any? ||
17
+ context.concept.data&.localized_concepts&.any?
18
+ end
19
+
20
+ def check(context)
21
+ concept = context.concept
22
+ fname = context.file_name
23
+ issues = []
24
+
25
+ lc_map = concept.data&.localized_concepts || {}
26
+ loaded_langs = concept.localizations&.map(&:language_code)&.compact || []
27
+
28
+ # Map has entry but no loaded localization
29
+ lc_map.each_key do |lang|
30
+ next if loaded_langs.include?(lang)
31
+
32
+ issues << issue(
33
+ "localized_concepts map has '#{lang}' but no localization loaded",
34
+ location: fname,
35
+ suggestion: "Add a localization for '#{lang}' or remove it from the map",
36
+ )
37
+ end
38
+
39
+ # Loaded localization not in map
40
+ loaded_langs.each do |lang|
41
+ next if lc_map.key?(lang)
42
+
43
+ issues << issue(
44
+ "localization '#{lang}' is loaded but not in localized_concepts map",
45
+ location: fname,
46
+ suggestion: "Add '#{lang}' to the localized_concepts map",
47
+ )
48
+ end
49
+
50
+ # UUID mismatch between map and loaded localization
51
+ concept.localizations.each do |l10n|
52
+ lang = l10n.language_code
53
+ next unless lang
54
+
55
+ expected_uuid = lc_map[lang]
56
+ actual_uuid = l10n.uuid
57
+ next unless expected_uuid && actual_uuid
58
+ next if expected_uuid == actual_uuid
59
+
60
+ issues << issue(
61
+ "UUID mismatch for '#{lang}': map says '#{expected_uuid}', localization is '#{actual_uuid}'",
62
+ location: fname,
63
+ suggestion: "Ensure the UUID in the map matches the localization file",
64
+ )
65
+ end
66
+
67
+ issues
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -16,7 +16,7 @@ module Glossarist
16
16
  return [] if l10ns.any?
17
17
 
18
18
  [issue("#{fname}: no localizations found",
19
- code: code, severity: "error")]
19
+ code: code, severity: "warning")]
20
20
  end
21
21
  end
22
22
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module Validation
5
+ module Rules
6
+ class ModelValidityRule < Base
7
+ def code = "GLS-050"
8
+ def category = :structure
9
+ def severity = "error"
10
+ def scope = :concept
11
+
12
+ def applicable?(context)
13
+ context.concept.is_a?(Lutaml::Model::Serializable)
14
+ end
15
+
16
+ def check(context)
17
+ validate_recursive(context.concept, context.file_name)
18
+ end
19
+
20
+ private
21
+
22
+ def validate_recursive(model, location, path = "")
23
+ return [] unless model.is_a?(Lutaml::Model::Serializable)
24
+
25
+ issues = collect_model_errors(model, location, path)
26
+ issues.concat(recurse_attributes(model, location, path))
27
+ issues
28
+ end
29
+
30
+ def collect_model_errors(model, location, path)
31
+ errors = model.validate
32
+ return [] if errors.empty?
33
+
34
+ prefix = path.empty? ? "" : "#{path}: "
35
+ errors.map { |e| issue("#{prefix}#{e}", location: location) }
36
+ end
37
+
38
+ def recurse_attributes(model, location, path)
39
+ issues = []
40
+
41
+ model.class.attributes.each do |name, _|
42
+ value = model.public_send(name)
43
+ next if value.nil?
44
+
45
+ child_path = build_path(path, name)
46
+ issues.concat(validate_collection(value, location, child_path))
47
+ end
48
+
49
+ issues
50
+ end
51
+
52
+ def validate_collection(value, location, path)
53
+ case value
54
+ when Array
55
+ value.each_with_index.flat_map do |item, idx|
56
+ validate_recursive(item, location, "#{path}[#{idx}]")
57
+ end
58
+ when Lutaml::Model::Serializable
59
+ validate_recursive(value, location, path)
60
+ else
61
+ []
62
+ end
63
+ end
64
+
65
+ def build_path(parent, name)
66
+ parent.empty? ? name.to_s : "#{parent}.#{name}"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -20,8 +20,7 @@ module Glossarist
20
20
 
21
21
  context.concepts.each do |concept|
22
22
  concept.localizations.each do |l10n|
23
- texts = extract_texts(l10n)
24
- texts.each do |text|
23
+ l10n.text_content.each do |text|
25
24
  next unless text
26
25
  extractor.extract_from_text(text).each do |ref|
27
26
  if ref.is_a?(BibliographicReference)
@@ -47,18 +46,7 @@ module Glossarist
47
46
 
48
47
  issues
49
48
  end
50
-
51
- private
52
-
53
- def extract_texts(l10n)
54
- texts = []
55
- (l10n.data&.definition || []).each { |d| texts << d.content if d.content }
56
- (l10n.data&.notes || []).each { |n| texts << n.content if n.content }
57
- (l10n.data&.examples || []).each { |e| texts << e.content if e.content }
58
- texts
59
- end
60
49
  end
61
50
  end
62
51
  end
63
52
  end
64
-
@@ -18,10 +18,8 @@ module Glossarist
18
18
  referenced_paths = Set.new
19
19
 
20
20
  context.concepts.each do |concept|
21
- # Text-embedded image refs
22
21
  concept.localizations.each do |l10n|
23
- texts = extract_texts(l10n)
24
- texts.each do |text|
22
+ l10n.text_content.each do |text|
25
23
  next unless text
26
24
  extractor.extract_from_text(text).each do |ref|
27
25
  if ref.is_a?(AssetReference)
@@ -31,12 +29,20 @@ module Glossarist
31
29
  end
32
30
  end
33
31
 
34
- # Model-level asset refs
35
32
  extractor.extract_asset_refs_from_concept(concept).each do |ref|
36
33
  referenced_paths.add(ref.path)
37
34
  end
38
35
  end
39
36
 
37
+ images_file = load_images_file(context)
38
+ if images_file
39
+ context.bibliography_index.entries.each_value do |entry|
40
+ next unless entry[:source].is_a?(V3::ImageEntry)
41
+ path = entry[:source].path
42
+ referenced_paths.add(path) if path
43
+ end
44
+ end
45
+
40
46
  issues = []
41
47
  context.asset_index.each_path do |path|
42
48
  next if referenced_paths.include?(path)
@@ -54,15 +60,14 @@ module Glossarist
54
60
 
55
61
  private
56
62
 
57
- def extract_texts(l10n)
58
- texts = []
59
- (l10n.data&.definition || []).each { |d| texts << d.content if d.content }
60
- (l10n.data&.notes || []).each { |n| texts << n.content if n.content }
61
- (l10n.data&.examples || []).each { |e| texts << e.content if e.content }
62
- texts
63
+ def load_images_file(context)
64
+ return @images_file if defined?(@images_file)
65
+
66
+ @images_file = V3::ImageFile.from_file(
67
+ File.join(context.path, "images.yaml")
68
+ )
63
69
  end
64
70
  end
65
71
  end
66
72
  end
67
73
  end
68
-