glossarist 2.13.10 → 2.13.12
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/concept_ref.rb +60 -19
- 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/v3/abstract_hyperedge.rb +32 -3
- data/lib/glossarist/v3/hyperedge_writer.rb +19 -2
- 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: 5b15e9fa1ab722c42698d7b4f87a9517557aaeffbf44ed56907d20db28a678d3
|
|
4
|
+
data.tar.gz: 92743d7653a3a64f09da6098aea9308ca809db2a2ac4108766590cf5dbea791c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '02780e9a932c36e5d769d8645b37a36c372fabaa3acee9cc85760d5b6734013b524d0a41d06c9dbb06ace71aba2186797dd022f9f055fc761ffa5c605a42a7bb'
|
|
7
|
+
data.tar.gz: b41226d8877739287fafa314e1fd7b5be6574475e839dcff4a218f8a003a618549e0b01a89b7bc38a76d3a708390bd6b7a8f8d2f15afea62b181b41add392fc5
|
|
@@ -4,39 +4,44 @@ module Glossarist
|
|
|
4
4
|
# A typed reference to another concept, used inside n-ary relation
|
|
5
5
|
# members and other internal pointer shapes.
|
|
6
6
|
#
|
|
7
|
-
# The base ConceptRef carries `source`, `id`,
|
|
8
|
-
# `ConceptReference` (in
|
|
9
|
-
# this with `term`, `urn`,
|
|
10
|
-
# cross-vocabulary references and
|
|
7
|
+
# The base ConceptRef carries `source`, `id`, `text`, `external`, and
|
|
8
|
+
# `ellipsis`. The richer `ConceptReference` (in
|
|
9
|
+
# `glossarist/concept_reference.rb`) extends this with `term`, `urn`,
|
|
10
|
+
# `version`, `ref_type` for use in cross-vocabulary references and
|
|
11
|
+
# relevance contexts.
|
|
11
12
|
#
|
|
12
13
|
# A ConceptRef is identified by `source:id` for external references
|
|
13
14
|
# or just `id` for local references. The `qualified_id` class method
|
|
14
15
|
# is the single SSOT for this string format — used by RelationLoader,
|
|
15
16
|
# the RDF transform, and ConceptRef-bearing data structures.
|
|
17
|
+
#
|
|
18
|
+
# == External concepts (parenthetical notation)
|
|
19
|
+
#
|
|
20
|
+
# ISO 704:2022 uses parenthetical notation for external concepts —
|
|
21
|
+
# concepts referenced from this dataset but defined elsewhere. The
|
|
22
|
+
# `external: true` flag marks such a ref. External refs carry `text`
|
|
23
|
+
# (the parenthetical label) but NOT `source`/`id`.
|
|
24
|
+
#
|
|
25
|
+
# == Ellipsis (structural marker)
|
|
26
|
+
#
|
|
27
|
+
# An ellipsis ref marks "further members exist but are not encoded."
|
|
28
|
+
# It is NOT a concept — it's a structural marker on the hyperedge.
|
|
29
|
+
# An ellipsis ref carries NO other fields (no source, id, text).
|
|
16
30
|
class ConceptRef < Lutaml::Model::Serializable
|
|
17
31
|
attribute :source, :string
|
|
18
32
|
attribute :id, :string
|
|
19
33
|
attribute :text, :string
|
|
34
|
+
attribute :external, :boolean, default: -> { false }
|
|
35
|
+
attribute :ellipsis, :boolean, default: -> { false }
|
|
20
36
|
|
|
21
37
|
key_value do
|
|
22
38
|
map :source, to: :source
|
|
23
39
|
map :id, to: :id
|
|
24
40
|
map :text, to: :text
|
|
41
|
+
map :external, to: :external
|
|
42
|
+
map :ellipsis, to: :ellipsis
|
|
25
43
|
end
|
|
26
44
|
|
|
27
|
-
# SSOT for the canonical "qualified identifier" string of a
|
|
28
|
-
# ConceptRef. Returns:
|
|
29
|
-
#
|
|
30
|
-
# - "{source}:{id}" when both source and id are present
|
|
31
|
-
# - "{id}" when only id is present (local ref)
|
|
32
|
-
# - "{source}:{text}" when source and text are present (no id)
|
|
33
|
-
# - "{text}" when only text is present (external
|
|
34
|
-
# concept form, no resolvable id)
|
|
35
|
-
# - nil when the ref is empty / not a ConceptRef
|
|
36
|
-
#
|
|
37
|
-
# Two refs are considered to point at the same concept iff their
|
|
38
|
-
# qualified_id is equal. Tables and registries can use this
|
|
39
|
-
# directly as a hash key.
|
|
40
45
|
def self.qualified_id(ref)
|
|
41
46
|
return nil unless ref.is_a?(ConceptRef)
|
|
42
47
|
|
|
@@ -51,14 +56,50 @@ module Glossarist
|
|
|
51
56
|
end
|
|
52
57
|
end
|
|
53
58
|
|
|
54
|
-
# Per-instance convenience delegating to the class method.
|
|
55
59
|
def qualified_id
|
|
56
60
|
self.class.qualified_id(self)
|
|
57
61
|
end
|
|
58
62
|
|
|
59
|
-
# True iff +other+ refers to the same concept as self.
|
|
60
63
|
def same_concept?(other)
|
|
61
64
|
qualified_id == self.class.qualified_id(other)
|
|
62
65
|
end
|
|
66
|
+
|
|
67
|
+
def resolved?
|
|
68
|
+
!source.to_s.empty? && !id.to_s.empty?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def external?
|
|
72
|
+
external == true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def ellipsis?
|
|
76
|
+
ellipsis == true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def text_only?
|
|
80
|
+
!text.to_s.empty? && !resolved? && !external?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def validate_ref!
|
|
84
|
+
if ellipsis? && (source || id || text || external?)
|
|
85
|
+
raise ArgumentError,
|
|
86
|
+
"ConceptRef with ellipsis: true must not carry any " \
|
|
87
|
+
"other field (source, id, text, external)"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if external? && (source || id)
|
|
91
|
+
raise ArgumentError,
|
|
92
|
+
"ConceptRef with external: true must not carry " \
|
|
93
|
+
"source/id — use text for the parenthetical label"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if !source && !id && !text && !external? && !ellipsis?
|
|
97
|
+
raise ArgumentError,
|
|
98
|
+
"ConceptRef must have at least one of: source+id, text, " \
|
|
99
|
+
"external, or ellipsis"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
self
|
|
103
|
+
end
|
|
63
104
|
end
|
|
64
105
|
end
|
|
@@ -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
|
|
@@ -89,8 +89,10 @@ module Glossarist
|
|
|
89
89
|
# Consumers inject the resolver at query time.
|
|
90
90
|
|
|
91
91
|
# True if the comprehensive resolves (via the supplied resolver)
|
|
92
|
-
# to a concept with status: external.
|
|
92
|
+
# to a concept with status: external. Also true when the
|
|
93
|
+
# comprehensive ConceptRef itself has external: true (inline form).
|
|
93
94
|
def external_comprehensive?(resolver = nil)
|
|
95
|
+
return true if comprehensive.is_a?(ConceptRef) && comprehensive.external?
|
|
94
96
|
return false unless comprehensive.is_a?(ConceptRef)
|
|
95
97
|
return false unless resolver
|
|
96
98
|
|
|
@@ -98,8 +100,8 @@ module Glossarist
|
|
|
98
100
|
concept_is_external?(concept)
|
|
99
101
|
end
|
|
100
102
|
|
|
101
|
-
# Array of members whose refs
|
|
102
|
-
#
|
|
103
|
+
# Array of members whose refs are marked external: true (inline
|
|
104
|
+
# form) OR resolve (via the supplied resolver) to status: external.
|
|
103
105
|
def external_members(resolver = nil)
|
|
104
106
|
return [] unless resolver
|
|
105
107
|
|
|
@@ -125,6 +127,33 @@ module Glossarist
|
|
|
125
127
|
candidates.any? { |ref| !has_provided_by?(ref, resolver) }
|
|
126
128
|
end
|
|
127
129
|
|
|
130
|
+
# ── Ellipsis / external inline helpers (no resolver needed) ──
|
|
131
|
+
|
|
132
|
+
# True if any member has a ConceptRef with external: true.
|
|
133
|
+
def has_external_members?
|
|
134
|
+
members.any? { |m| m.is_a?(HyperedgeMember) && m.ref&.external? }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# True if the comprehensive ConceptRef has external: true.
|
|
138
|
+
def has_external_comprehensive?
|
|
139
|
+
comprehensive.is_a?(ConceptRef) && comprehensive.external?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# True if any member has a ConceptRef with ellipsis: true.
|
|
143
|
+
def has_ellipsis_member?
|
|
144
|
+
members.any? { |m| m.is_a?(HyperedgeMember) && m.ref&.ellipsis? }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Array of members whose refs are marked external: true.
|
|
148
|
+
def inline_external_members
|
|
149
|
+
members.select { |m| m.is_a?(HyperedgeMember) && m.ref&.external? }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Array of members whose refs are marked ellipsis: true.
|
|
153
|
+
def ellipsis_members
|
|
154
|
+
members.select { |m| m.is_a?(HyperedgeMember) && m.ref&.ellipsis? }
|
|
155
|
+
end
|
|
156
|
+
|
|
128
157
|
# Per-file identity derived from comprehensive + criterion.
|
|
129
158
|
# Format: `<comprehensive-id-dir>/<criterion-slug>` where the
|
|
130
159
|
# dir is `<source>-<id>` (downcased, kebab-case) and the slug
|
|
@@ -68,7 +68,7 @@ module Glossarist
|
|
|
68
68
|
hash = hyperedge.to_hash
|
|
69
69
|
hash["$id"] = hyperedge.file_id || hyperedge.derived_file_id
|
|
70
70
|
hash["type"] = hyperedge.class::TYPE_TAG
|
|
71
|
-
|
|
71
|
+
strip_falsey_concept_ref_flags!(hash)
|
|
72
72
|
ordered = {}
|
|
73
73
|
ordered["$id"] = hash.delete("$id")
|
|
74
74
|
ordered["type"] = hash.delete("type")
|
|
@@ -79,11 +79,28 @@ module Glossarist
|
|
|
79
79
|
ordered["criterion"] = hash.delete("criterion") if hash.key?("criterion")
|
|
80
80
|
ordered["sources"] = hash.delete("sources") if hash.key?("sources")
|
|
81
81
|
ordered["notes"] = hash.delete("notes") if hash.key?("notes")
|
|
82
|
-
ordered.merge!(hash)
|
|
82
|
+
ordered.merge!(hash)
|
|
83
|
+
strip_falsey_concept_ref_flags!(ordered)
|
|
83
84
|
ordered.compact!
|
|
84
85
|
|
|
85
86
|
YAML.dump(ordered).gsub(/^---\s*\n/, "---\n")
|
|
86
87
|
end
|
|
88
|
+
|
|
89
|
+
# Recursively strip `external: false` and `ellipsis: false` from
|
|
90
|
+
# ConceptRef hashes inside the hyperedge's wire output. Keeps
|
|
91
|
+
# YAML clean — these flags only appear when true.
|
|
92
|
+
def strip_falsey_concept_ref_flags!(hash)
|
|
93
|
+
hash.each_value do |value|
|
|
94
|
+
case value
|
|
95
|
+
when Hash
|
|
96
|
+
value.delete("external") if value["external"] == false
|
|
97
|
+
value.delete("ellipsis") if value["ellipsis"] == false
|
|
98
|
+
strip_falsey_concept_ref_flags!(value)
|
|
99
|
+
when Array
|
|
100
|
+
value.each { |item| strip_falsey_concept_ref_flags!(item) if item.is_a?(Hash) }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
87
104
|
end
|
|
88
105
|
end
|
|
89
106
|
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.12
|
|
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-10 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
|