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 +4 -4
- data/lib/glossarist/mentions/invalid_mention_error.rb +16 -0
- data/lib/glossarist/mentions/parser.rb +189 -0
- data/lib/glossarist/mentions/resolver.rb +143 -0
- data/lib/glossarist/mentions.rb +15 -0
- data/lib/glossarist/version.rb +1 -1
- data/lib/glossarist.rb +6 -8
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 91c65595f05c44c51808471afe8444211ea1f6dec412d8fe0ad74eee187b11b2
|
|
4
|
+
data.tar.gz: 9354da2e19de5935ed2e6415436f2b7344e8ad841520a7ee9b70be7c4ddd49da
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/glossarist/version.rb
CHANGED
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 :
|
|
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
|
|
113
|
-
# (
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
|
|
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.
|
|
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-
|
|
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
|