glossarist 2.13.10 → 2.13.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 16c44282632761dbbfeb3b63ff619b519648f0af7306ee5aa85fba1606244bd6
4
- data.tar.gz: 37e8af5033c014195228ba11f50528b7aca8c06424533196ea0c318d5f78c7f0
3
+ metadata.gz: 91c65595f05c44c51808471afe8444211ea1f6dec412d8fe0ad74eee187b11b2
4
+ data.tar.gz: 9354da2e19de5935ed2e6415436f2b7344e8ad841520a7ee9b70be7c4ddd49da
5
5
  SHA512:
6
- metadata.gz: 8dadb718f3a39e49167eb1d8aa8eeada22f826c049be68dcfe151cd0980edb0e80006a51b21f64912dcc9e9d4ef21ff47618002b6d7d1496b88669cb39932620
7
- data.tar.gz: 29f66da8f1f743c7f5424e966bf4b19a94e83264146d9c3156243f0f0a39e728874fcc4cbf6f2a2d8c77bd4799406083162d27b5f675dd1fe0aa88246091222d
6
+ metadata.gz: a2288cbc327785082b55fa370c622877e7dd41e90545edb8db7063e3fd29d57d974b040197fb015e4a83a6bc99f1ce44c583fbe692aced5b5f2251dbd542382d
7
+ data.tar.gz: 919ec57825c62a7749afce56ec9bf5f49e7f18a89d537c60290d92e6bf35b97eeb4118f40369e81119113ebf945b371d0fccc942d4416df14e31d2af2359bd7d
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module Mentions
5
+ class InvalidMentionError < StandardError
6
+ attr_reader :raw, :reason, :position
7
+
8
+ def initialize(raw:, reason:, position: 0)
9
+ @raw = raw
10
+ @reason = reason
11
+ @position = position
12
+ super("Invalid mention at position #{position}: #{reason}")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module Mentions
5
+ # Scans a string for {{kind:target}} patterns and returns a mixed
6
+ # array of text segments and parsed mention objects.
7
+ #
8
+ # Invalid mentions raise InvalidMentionError — they are never
9
+ # silently passed through.
10
+ #
11
+ # @see concept-model docs/design/inline-mentions.md
12
+ module Parser
13
+ MENTION_REGEX = /\{\{([^}]+)\}\}/.freeze
14
+
15
+ KINDS = %w[concept cite fig table formula bib link image].freeze
16
+
17
+ module_function
18
+
19
+ # @param text [String] the source text to scan
20
+ # @return [Array<Hash>] segments — text and mention objects
21
+ # @raise [InvalidMentionError] on invalid mention syntax
22
+ def parse(text)
23
+ return [{ kind: "text", content: "" }] unless text.is_a?(String) && !text.empty?
24
+
25
+ segments = []
26
+ pos = 0
27
+
28
+ text.to_enum(:scan, MENTION_REGEX).each do |match|
29
+ raw_content = match[0]
30
+ match_start = Regexp.last_match.offset(0)[0]
31
+
32
+ if match_start > pos
33
+ segments << { kind: "text", content: text[pos...match_start] }
34
+ end
35
+
36
+ segment = parse_mention_content(
37
+ raw_content,
38
+ raw: "{{#{raw_content}}}",
39
+ position: match_start,
40
+ )
41
+ segments << segment
42
+ pos = Regexp.last_match.offset(0)[1]
43
+ end
44
+
45
+ segments << { kind: "text", content: text[pos..] } if pos < text.length
46
+ segments
47
+ end
48
+
49
+ def parse_mention_content(content, raw:, position:)
50
+ content = content.strip
51
+
52
+ unless content =~ /\A(\w+):(.*)\z/m
53
+ raise InvalidMentionError.new(
54
+ raw: raw,
55
+ position: position,
56
+ reason: "Missing kind prefix; use {{concept:DATASET:ID}} or " \
57
+ "{{cite:DATASET:ID}} — got #{raw}",
58
+ )
59
+ end
60
+
61
+ kind = Regexp.last_match(1).downcase
62
+ rest = Regexp.last_match(2).strip
63
+
64
+ unless KINDS.include?(kind)
65
+ raise InvalidMentionError.new(
66
+ raw: raw,
67
+ position: position,
68
+ reason: "Unknown kind '#{kind}'; valid kinds: #{KINDS.join(', ')}",
69
+ )
70
+ end
71
+
72
+ target_str, label = split_target_and_label(rest)
73
+
74
+ target = parse_target(target_str, kind: kind, raw: raw, position: position)
75
+
76
+ validate_target!(kind, target, raw: raw, position: position)
77
+
78
+ {
79
+ kind: kind,
80
+ target: target,
81
+ label: strip_label(label),
82
+ raw: raw,
83
+ start: position,
84
+ end: position + raw.length,
85
+ }
86
+ end
87
+
88
+ def split_target_and_label(rest)
89
+ return [rest, nil] unless rest.include?(",")
90
+
91
+ parts = rest.split(",", 2)
92
+ [parts[0].strip, parts[1].to_s.strip]
93
+ end
94
+ private_class_method :split_target_and_label
95
+
96
+ def parse_target(target_str, kind:, raw:, position:)
97
+ case target_str
98
+ when /\Aurn:/i
99
+ { type: "urn", urn: target_str }
100
+ when /\Ahttps?:\/\//i
101
+ { type: "url", url: target_str }
102
+ when /\A[a-zA-Z][a-zA-Z0-9_-]*:[^:]+/
103
+ split_dataset_qualified(target_str)
104
+ when %r{\A[a-zA-Z0-9_./-]+\z}
105
+ if target_str.include?("/")
106
+ { type: "path", path: target_str }
107
+ else
108
+ { type: "entity_id", id: target_str }
109
+ end
110
+ else
111
+ { type: "entity_id", id: target_str }
112
+ end
113
+ end
114
+ private_class_method :parse_target
115
+
116
+ def split_dataset_qualified(target_str)
117
+ last_colon = target_str.rindex(":")
118
+ dataset = target_str[0...last_colon]
119
+ id = target_str[(last_colon + 1)..]
120
+ { type: "dataset_qualified", dataset: dataset, id: id }
121
+ end
122
+ private_class_method :split_dataset_qualified
123
+
124
+ def validate_target!(kind, target, raw:, position:)
125
+ type = target[:type]
126
+
127
+ case kind
128
+ when "concept", "cite"
129
+ unless %w[urn dataset_qualified].include?(type)
130
+ raise InvalidMentionError.new(
131
+ raw: raw,
132
+ position: position,
133
+ reason: "#{kind} target must be DATASET:ID or URN; " \
134
+ "got #{type} #{target_value_for_error(target).inspect}",
135
+ )
136
+ end
137
+ when "fig", "table", "formula"
138
+ unless %w[entity_id urn].include?(type)
139
+ raise InvalidMentionError.new(
140
+ raw: raw,
141
+ position: position,
142
+ reason: "#{kind} target must be plain ID or URN; " \
143
+ "got #{type} #{target_value_for_error(target).inspect}",
144
+ )
145
+ end
146
+ when "bib"
147
+ unless type == "entity_id"
148
+ raise InvalidMentionError.new(
149
+ raw: raw,
150
+ position: position,
151
+ reason: "bib target must be a plain ID (same-dataset); " \
152
+ "got #{type} #{target_value_for_error(target).inspect}",
153
+ )
154
+ end
155
+ when "link"
156
+ unless type == "url"
157
+ raise InvalidMentionError.new(
158
+ raw: raw,
159
+ position: position,
160
+ reason: "link target must be https:// or http:// URL; " \
161
+ "got #{target_value_for_error(target).inspect}",
162
+ )
163
+ end
164
+ when "image"
165
+ unless %w[path url].include?(type)
166
+ raise InvalidMentionError.new(
167
+ raw: raw,
168
+ position: position,
169
+ reason: "image target must be a path or URL; " \
170
+ "got #{type} #{target_value_for_error(target).inspect}",
171
+ )
172
+ end
173
+ end
174
+ end
175
+ private_class_method :validate_target!
176
+
177
+ def target_value_for_error(target)
178
+ target[:urn] || target[:url] || target[:id] ||
179
+ target[:dataset] ? "#{target[:dataset]}:#{target[:id]}" : target[:path]
180
+ end
181
+ private_class_method :target_value_for_error
182
+
183
+ def strip_label(label)
184
+ label.nil? || label.empty? ? nil : label
185
+ end
186
+ private_class_method :strip_label
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module Mentions
5
+ # Resolves parsed mention segments against a resolution context.
6
+ # The context provides lookup callables; the resolver dispatches
7
+ # by mention kind + target type.
8
+ module Resolver
9
+ module_function
10
+
11
+ # @param mention [Hash] a parsed mention from Parser.parse
12
+ # @param context [Hash] resolution context with lookup callables
13
+ # @return [Hash] resolved mention with :status
14
+ def resolve(mention, context)
15
+ return mention if mention[:kind] == "text"
16
+
17
+ case mention[:kind]
18
+ when "concept", "cite"
19
+ resolve_concept(mention, context)
20
+ when "fig", "table", "formula"
21
+ resolve_entity(mention, context)
22
+ when "bib"
23
+ resolve_bib(mention, context)
24
+ when "link"
25
+ { status: "external", kind: "link",
26
+ url: mention[:target][:url], label: mention[:label] }
27
+ when "image"
28
+ resolve_image(mention)
29
+ else
30
+ { status: "unresolved", kind: mention[:kind],
31
+ target: mention[:target], label: mention[:label] }
32
+ end
33
+ end
34
+
35
+ def resolve_all(mentions, context)
36
+ mentions.map { |m| resolve(m, context) }
37
+ end
38
+
39
+ def resolve_concept(mention, context)
40
+ target = mention[:target]
41
+ concept = lookup_concept(target, context)
42
+
43
+ return unresolved(mention) unless concept
44
+
45
+ result = {
46
+ status: "resolved",
47
+ kind: mention[:kind],
48
+ concept: concept,
49
+ label: mention[:label],
50
+ }
51
+
52
+ if mention[:kind] == "cite" && context[:concept]
53
+ result[:source] = find_matching_source(target, context[:concept])
54
+ end
55
+
56
+ result
57
+ end
58
+ private_class_method :resolve_concept
59
+
60
+ def resolve_entity(mention, context)
61
+ target = mention[:target]
62
+ entity = nil
63
+
64
+ case target[:type]
65
+ when "entity_id"
66
+ resolver = context[:resolve_entity]
67
+ entity = resolver&.call(mention[:kind], target[:id]) if resolver
68
+ when "urn"
69
+ entity = nil
70
+ end
71
+
72
+ if entity
73
+ { status: "resolved", kind: mention[:kind],
74
+ entity: entity, label: mention[:label] }
75
+ else
76
+ unresolved(mention)
77
+ end
78
+ end
79
+ private_class_method :resolve_entity
80
+
81
+ def resolve_bib(mention, context)
82
+ target = mention[:target]
83
+ entry = context[:resolve_bib_entry]&.call(target[:id])
84
+
85
+ if entry
86
+ { status: "resolved", kind: "bib",
87
+ entry: entry, label: mention[:label] }
88
+ else
89
+ unresolved(mention)
90
+ end
91
+ end
92
+ private_class_method :resolve_bib
93
+
94
+ def resolve_image(mention)
95
+ target = mention[:target]
96
+ if target[:type] == "url"
97
+ { status: "external_image", kind: "image",
98
+ url: target[:url], label: mention[:label] }
99
+ else
100
+ { status: "local_image", kind: "image",
101
+ path: target[:path], label: mention[:label] }
102
+ end
103
+ end
104
+ private_class_method :resolve_image
105
+
106
+ def lookup_concept(target, context)
107
+ resolver = context[:resolve_concept]
108
+ return nil unless resolver
109
+
110
+ case target[:type]
111
+ when "dataset_qualified"
112
+ resolver.call(dataset: target[:dataset], id: target[:id])
113
+ when "urn"
114
+ resolver.call(urn: target[:urn])
115
+ end
116
+ end
117
+ private_class_method :lookup_concept
118
+
119
+ def find_matching_source(target, concept)
120
+ sources = concept.respond_to?(:data) ? concept.data&.sources : concept.sources
121
+ return nil unless sources
122
+
123
+ case target[:type]
124
+ when "dataset_qualified"
125
+ sources.find do |s|
126
+ next unless s.respond_to?(:origin) && s.origin.respond_to?(:ref)
127
+ ref = s.origin.ref
128
+ ref && ref.source == target[:dataset] && ref.id == target[:id]
129
+ end
130
+ when "urn"
131
+ nil
132
+ end
133
+ end
134
+ private_class_method :find_matching_source
135
+
136
+ def unresolved(mention)
137
+ { status: "unresolved", kind: mention[:kind],
138
+ target: mention[:target], label: mention[:label] }
139
+ end
140
+ private_class_method :unresolved
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ # Inline mentions — fully declarative {{kind:target}} syntax per
5
+ # concept-model docs/design/inline-mentions.md.
6
+ #
7
+ # Every mention target is a CANONICAL IDENTIFIER. No magic
8
+ # combinations, no bare text, no procedural indirection. The parser
9
+ # REJECTS any non-declarative form with a clear error.
10
+ module Mentions
11
+ autoload :InvalidMentionError, "glossarist/mentions/invalid_mention_error"
12
+ autoload :Parser, "glossarist/mentions/parser"
13
+ autoload :Resolver, "glossarist/mentions/resolver"
14
+ end
15
+ end
@@ -4,5 +4,5 @@
4
4
  #
5
5
 
6
6
  module Glossarist
7
- VERSION = "2.13.10"
7
+ VERSION = "2.13.11"
8
8
  end
data/lib/glossarist.rb CHANGED
@@ -65,8 +65,7 @@ module Glossarist
65
65
  autoload :ManagedConcept, "glossarist/managed_concept"
66
66
  autoload :ManagedConceptCollection, "glossarist/managed_concept_collection"
67
67
  autoload :ManagedConceptData, "glossarist/managed_concept_data"
68
- autoload :MentionParser, "glossarist/mention_parser"
69
- autoload :MentionKinds, "glossarist/mention_parser"
68
+ autoload :Mentions, "glossarist/mentions"
70
69
  autoload :NonVerbRep, "glossarist/non_verb_rep"
71
70
  autoload :Pronunciation, "glossarist/pronunciation"
72
71
  autoload :RelatedConcept, "glossarist/related_concept"
@@ -109,12 +108,11 @@ module Glossarist
109
108
  end
110
109
  end
111
110
 
112
- # Parse a single {{...}} mention into a canonical Hash. Defined here
113
- # (rather than via autoload) so calling Glossarist.parse_mention(str)
114
- # triggers the MentionParser autoload chain transparently — method
115
- # calls do not fire autoloads on their own.
116
- def self.parse_mention(text)
117
- MentionParser.parse_mention(text)
111
+ # Parse all {{kind:target}} mentions in text. Returns an array of
112
+ # segments (text + mention objects) per concept-model inline-mentions spec.
113
+ # @see Glossarist::Mentions::Parser.parse
114
+ def self.parse_mentions(text)
115
+ Mentions::Parser.parse(text)
118
116
  end
119
117
 
120
118
  SCHEMA_VERSION = "3"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glossarist
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.13.10
4
+ version: 2.13.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-02 00:00:00.000000000 Z
11
+ date: 2026-08-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model
@@ -293,6 +293,10 @@ files:
293
293
  - lib/glossarist/managed_concept_collection.rb
294
294
  - lib/glossarist/managed_concept_data.rb
295
295
  - lib/glossarist/mention_parser.rb
296
+ - lib/glossarist/mentions.rb
297
+ - lib/glossarist/mentions/invalid_mention_error.rb
298
+ - lib/glossarist/mentions/parser.rb
299
+ - lib/glossarist/mentions/resolver.rb
296
300
  - lib/glossarist/non_concept_entity.rb
297
301
  - lib/glossarist/non_verb_rep.rb
298
302
  - lib/glossarist/non_verbal_reference.rb