glossarist 2.13.9 → 2.13.10

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: 5e42372897ecf000813b37db02afdc6461039b9ee4618182515793d548bd9951
4
- data.tar.gz: 33f5ba8dfb1b35782fc3711aa9ec7a0f1f675c55a0876c830975d4ca14770761
3
+ metadata.gz: 16c44282632761dbbfeb3b63ff619b519648f0af7306ee5aa85fba1606244bd6
4
+ data.tar.gz: 37e8af5033c014195228ba11f50528b7aca8c06424533196ea0c318d5f78c7f0
5
5
  SHA512:
6
- metadata.gz: 02d246dd723fd2f2834c54ac2e3d8f4a38251e543d7b677ed0ada8cd11af05cddfd41999f6772bbf8235ba486af8d76a603585185f4fd8475bcfaac22ce72a59
7
- data.tar.gz: d1f34a759e7505afee75884ca7d5cbaa0b02f547ce8f1464317e00fc31fb011b4cd9820faa4a13d9eaa3e87c5daae74d93236cf940759d8b0cd7367996c00f78
6
+ metadata.gz: 8dadb718f3a39e49167eb1d8aa8eeada22f826c049be68dcfe151cd0980edb0e80006a51b21f64912dcc9e9d4ef21ff47618002b6d7d1496b88669cb39932620
7
+ data.tar.gz: 29f66da8f1f743c7f5424e966bf4b19a94e83264146d9c3156243f0f0a39e728874fcc4cbf6f2a2d8c77bd4799406083162d27b5f675dd1fe0aa88246091222d
data/PROMPT-NOW.md ADDED
@@ -0,0 +1,227 @@
1
+ # PROMPT-NOW: parseMention extension + legacy <<>> deprecation + terminology alignment + raw-HTML validation
2
+
3
+ > **Context:** concept-browser is implementing a strict DATA/DEPLOYMENT boundary for inline content. This requires all Glossarist libraries (JS, Ruby, and the concept-model schema) to parse the same unified `{{kind:target}}` syntax and align terminology. The JS library changes are tracked in a parallel PROMPT-NOW.md; this prompt mirrors them for the Ruby library. Self-contained.
4
+
5
+ ## Background: the principle
6
+
7
+ **Dataset authors** write `{{kind:target}}` mentions in concept text. They don't know where their dataset will be deployed.
8
+
9
+ **Concept-browser** resolves every mention at runtime via `ReferenceResolver`. For this to work, `parse_mention` must produce a canonical `{kind, target, label}` shape for every mention kind — identical to the JS library's output.
10
+
11
+ The current parser recognizes `cite:`, `urn:`, `fig:`/`figure:`, `table:`/`tbl:`, `formula:`/`eq:`, and bare designation/numeric. Three new kinds are needed: `link`, `image`, `bib`.
12
+
13
+ ---
14
+
15
+ ## P1: parse_mention extension — `link`, `image`, `bib` kinds
16
+
17
+ ### New kinds to parse
18
+
19
+ ```ruby
20
+ Glossarist.parse_mention("{{link:https://example.com/page}}")
21
+ # => { kind: 'link-ref', uri: 'https://example.com/page', label: nil }
22
+
23
+ Glossarist.parse_mention("{{link:https://example.com, click here}}")
24
+ # => { kind: 'link-ref', uri: 'https://example.com', label: 'click here' }
25
+
26
+ Glossarist.parse_mention("{{image:diagram.png}}")
27
+ # => { kind: 'image-ref', src: 'diagram.png', alt: nil }
28
+
29
+ Glossarist.parse_mention("{{image:diagram.png, The diagram}}")
30
+ # => { kind: 'image-ref', src: 'diagram.png', alt: 'The diagram' }
31
+
32
+ Glossarist.parse_mention("{{bib:ref_1}}")
33
+ # => { kind: 'bib-ref', id: 'ref_1', label: nil }
34
+
35
+ Glossarist.parse_mention("{{bib:ref_1, ISO 704}}")
36
+ # => { kind: 'bib-ref', id: 'ref_1', label: 'ISO 704' }
37
+ ```
38
+
39
+ ### Design notes
40
+
41
+ - `link` uses `uri` (not `id`) because the target is a URL, not a dataset-local handle. The URL is canonical — external to all datasets, deployment-independent.
42
+ - `image` uses `src` (not `uri`) and `alt` (not `label`) because it's an embed, not a link. The `alt` field is the accessibility text.
43
+ - `bib` uses `id` because it references a dataset-local bibliography entry. This is the case-3-only path — the author explicitly wants a flat bibliographic record, not a concept resolution.
44
+
45
+ The parsed shapes MUST match the JS library's output exactly (same keys, same values) so concept-browser can consume either library's output interchangeably.
46
+
47
+ ---
48
+
49
+ ## P2: Legacy `<<target, caption>>` deprecation
50
+
51
+ ### The problem
52
+
53
+ The AsciiDoc xref syntax `<<target, caption>>` has been overloaded for:
54
+ 1. **Non-concept entity xrefs** (legitimate AsciiDoc use — should map to `{{fig/table/formula:target, caption}}`)
55
+ 2. **Bibliography lookups** (wrong — bypasses the resolution cascade)
56
+ 3. **Concept citations** (wrong — should use `{{cite:target, caption}}`)
57
+
58
+ ### Proposed change
59
+
60
+ When the parser encounters `<<target, caption>>`:
61
+
62
+ 1. **Emit a deprecation warning** via Ruby's `Warning` module:
63
+
64
+ ```ruby
65
+ Warning.warn("[glossarist] <<#{target}, #{caption}>> is deprecated. " \
66
+ "Use {{fig/table/formula:#{target}, #{caption}}} for non-concept entities, " \
67
+ "or {{cite:#{target}, #{caption}}} for concept citations.")
68
+ ```
69
+
70
+ 2. **Re-parse** based on what `target` resolves to:
71
+ - If `target` matches a figure/table/formula entity in the dataset → treat as `{{kind:target, caption}}`
72
+ - Otherwise → treat as `{{cite:target, caption}}` (let concept-browser's resolution cascade handle it)
73
+
74
+ 3. **Do NOT** treat as a bibliography lookup. Bibliography is reached only via `{{bib:id}}` (explicit) or via the resolution cascade's case-3 fallback (implicit).
75
+
76
+ ### RuboCop cop (optional)
77
+
78
+ Consider a custom RuboCop cop that flags `<<...,...>>` in concept text fixtures:
79
+
80
+ ```ruby
81
+ # .rubocop.yml
82
+ require: glossarist/rubocop
83
+
84
+ Glossarist/NoLegacyXrefSyntax:
85
+ Enabled: true
86
+ Description: 'Use {{kind:target}} instead of <<target, caption>>'
87
+ ```
88
+
89
+ ---
90
+
91
+ ## P3: Terminology alignment
92
+
93
+ ### The rename
94
+
95
+ Rename `NonVerbalEntity` → `NonConceptEntity` in Ruby model classes (where referring to Figure/Table/Formula). The terminology fix:
96
+
97
+ - **Non-verbal** refers to the modality of expression (non-verbal designation: symbol, formula expression). Properties OF concepts.
98
+ - **Non-concept** refers to entities that are NOT concepts at all (Figure, Table, Formula). Standalone dataset entities.
99
+
100
+ ### Proposed change
101
+
102
+ ```ruby
103
+ # BEFORE
104
+ module Glossarist
105
+ class NonVerbalEntity < GlossaristModel; end # ambiguous
106
+ class SharedNonVerbalEntity < NonVerbalEntity; end
107
+ class NonVerbRep < GlossaristModel; end # concept-local designation
108
+ end
109
+
110
+ # AFTER
111
+ module Glossarist
112
+ class NonConceptEntity < GlossaristModel; end # clear — NOT a concept
113
+ class NonVerbRep < GlossaristModel; end # unchanged — IS a designation
114
+
115
+ # Deprecated alias for backward compat
116
+ # @deprecated Use NonConceptEntity instead.
117
+ NonVerbalEntity = NonConceptEntity
118
+ SharedNonVerbalEntity = NonConceptEntity
119
+ end
120
+ ```
121
+
122
+ Keep `NonVerbRep` unchanged — it IS a non-verbal representation (a designation of a concept).
123
+
124
+ Provide deprecated aliases for one release cycle.
125
+
126
+ ---
127
+
128
+ ## P4: Raw-HTML validation
129
+
130
+ ### The problem
131
+
132
+ Concept text sometimes contains raw HTML instead of typed mention syntax:
133
+
134
+ ```yaml
135
+ # BAD — raw HTML in YAML
136
+ definition:
137
+ - content: See <a href="http://std.iec.ch/...">IEV</a> for details.
138
+ ```
139
+
140
+ This bypasses the renderer, is brittle, has no accessibility contract, and can embed deployment-specific URLs.
141
+
142
+ ### Proposed change
143
+
144
+ Add a validator:
145
+
146
+ ```ruby
147
+ module Glossarist
148
+ module Validators
149
+ class NoRawHtml
150
+ def self.call(text)
151
+ issues = []
152
+
153
+ # <a href="URL">label</a> → {{link:URL, label}}
154
+ text.scan(/<a\s+href="([^"]+)"[^>]*>([^<]*)<\/a>/i) do |url, label|
155
+ issues << {
156
+ severity: 'warning',
157
+ match: "<a href=\"#{url}\">#{label}</a>",
158
+ suggestion: label && !label.empty? ? "{{link:#{url}, #{label}}}" : "{{link:#{url}}}",
159
+ message: 'Use {{link:}} instead of raw <a> tags',
160
+ }
161
+ end
162
+
163
+ # <img src="SRC" alt="ALT"> → {{image:SRC, ALT}}
164
+ text.scan(/<img\s+src="([^"]+)"(?:\s+alt="([^"]*)")?[^>]*>/i) do |src, alt|
165
+ suggestion = alt ? "{{image:#{src}, #{alt}}}" : "{{image:#{src}}}"
166
+ issues << {
167
+ severity: 'warning',
168
+ match: "<img src=\"#{src}\"#{" alt=\"#{alt}\"" if alt}>",
169
+ suggestion: suggestion,
170
+ message: 'Use {{image:}} instead of raw <img> tags',
171
+ }
172
+ end
173
+
174
+ issues
175
+ end
176
+ end
177
+ end
178
+ end
179
+ ```
180
+
181
+ Replacement suggestions:
182
+
183
+ | Raw HTML | Suggested replacement |
184
+ |---|---|
185
+ | `<a href="URL">label</a>` | `{{link:URL, label}}` |
186
+ | `<a href="URL">URL</a>` | `{{link:URL}}` |
187
+ | `<img src="SRC">` | `{{image:SRC}}` |
188
+ | `<img src="SRC" alt="ALT">` | `{{image:SRC, ALT}}` |
189
+ | `<iframe src="URL">` | `{{link:URL}}` (iframes not supported as embeds) |
190
+
191
+ The validator is opt-in — concept-browser (or the data pipeline) can call it during data loading to warn dataset authors.
192
+
193
+ ---
194
+
195
+ ## Summary
196
+
197
+ | Change | Impact |
198
+ |---|---|
199
+ | P1: parse_mention extension | New parsed shapes for `link`, `image`, `bib` (must match JS output exactly) |
200
+ | P2: Legacy `<<>>` deprecation | Warning + re-parse; optional RuboCop cop |
201
+ | P3: Terminology rename | `NonVerbalEntity` → `NonConceptEntity` (with deprecated alias) |
202
+ | P4: Raw-HTML validator | New `Glossarist::Validators::NoRawHtml` class |
203
+
204
+ ## Coordination
205
+
206
+ - **concept-model** needs schema changes for the new kinds and terminology (parallel PROMPT-NOW.md).
207
+ - **glossarist-js** needs the same parser extension (parallel PROMPT-NOW.md).
208
+ - **concept-browser** wires the resolvers once both libraries ship the new parse output.
209
+ - A **shared contract test fixture** (a concept with every kind of mention) should be consumed by all implementations to prevent syntax drift. The fixture lives in concept-model; both libraries import it.
210
+
211
+ ## Cross-library contract
212
+
213
+ The parsed output from `parse_mention` MUST be identical between the Ruby and JS libraries:
214
+
215
+ ```ruby
216
+ # Ruby
217
+ Glossarist.parse_mention("{{link:https://example.com, click here}}")
218
+ # => { kind: 'link-ref', uri: 'https://example.com', label: 'click here' }
219
+ ```
220
+
221
+ ```ts
222
+ // JS
223
+ parseMention("{{link:https://example.com, click here}}")
224
+ // => { kind: 'link-ref', uri: 'https://example.com', label: 'click here' }
225
+ ```
226
+
227
+ Same keys. Same values. Same symbol-as-string for `kind`. This allows concept-browser to consume either library's output without translation.
@@ -13,7 +13,7 @@ module Glossarist
13
13
  # subfigures.
14
14
  #
15
15
  # Caption, description, and alt are localized (hash keyed by ISO 639 code).
16
- class Figure < SharedNonVerbalEntity
16
+ class Figure < SharedNonConceptEntity
17
17
  attribute :images, FigureImage, collection: true
18
18
  attribute :subfigures, Figure, collection: true
19
19
 
@@ -7,7 +7,7 @@ module Glossarist
7
7
  # shared across concepts. The mathematical expression is stored in a
8
8
  # notation format (LaTeX, MathML, AsciiMath). Caption, description, and
9
9
  # alt are localized for accessibility.
10
- class Formula < SharedNonVerbalEntity
10
+ class Formula < SharedNonConceptEntity
11
11
  attribute :expression, :hash
12
12
  attribute :notation, :string
13
13
 
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ # Reference kinds produced by parse_mention for the unified
5
+ # {{kind:target}} / {{kind:target, label}} mention syntax.
6
+ #
7
+ # The shape produced for each kind is the cross-library contract —
8
+ # glossarist-js produces the same keys, same values, so concept-browser
9
+ # can consume either library's output interchangeably without translation.
10
+ module MentionKinds
11
+ # {{link:URL}} or {{link:URL, label}} — external URL link.
12
+ # `uri` is canonical (deployment-independent); `label` is the display
13
+ # text (nil means render the URL itself).
14
+ LINK_REF = "link-ref"
15
+
16
+ # {{image:src}} or {{image:src, alt}} — embedded image.
17
+ # `src` is the image source; `alt` is accessibility text (nil means
18
+ # no a11y label). Distinct from link-ref because it's an embed, not
19
+ # a navigation link.
20
+ IMAGE_REF = "image-ref"
21
+
22
+ # {{bib:id}} or {{bib:id, label}} — dataset-local bibliography entry.
23
+ # `id` references an entry in the dataset's bibliography.yaml. This is
24
+ # the case-3-only path — the author explicitly wants a flat
25
+ # bibliographic record, not a concept resolution.
26
+ BIB_REF = "bib-ref"
27
+
28
+ # {{cite:id}} or {{cite:id, label}} — concept citation.
29
+ CITE_REF = "cite-ref"
30
+
31
+ # {{fig:id}} / {{figure:id}} — non-concept figure entity xref.
32
+ FIG_REF = "fig-ref"
33
+
34
+ # {{table:id}} / {{tbl:id}} — non-concept table entity xref.
35
+ TABLE_REF = "table-ref"
36
+
37
+ # {{formula:id}} / {{eq:id}} — non-concept formula entity xref.
38
+ FORMULA_REF = "formula-ref"
39
+
40
+ # {{123}} or {{123, label}} — local concept by numeric id.
41
+ LOCAL_CONCEPT_REF = "local-concept-ref"
42
+
43
+ # {{designation}} — concept by designation text.
44
+ DESIGNATION_REF = "designation-ref"
45
+
46
+ # {{urn:...}} — URN reference.
47
+ URN_REF = "urn-ref"
48
+ end
49
+
50
+ # Parsed mention result — a plain Hash with symbol keys. Built by
51
+ # Glossarist.parse_mention. The shape per kind is documented on
52
+ # MentionKinds constants above.
53
+ #
54
+ # Examples:
55
+ #
56
+ # Glossarist.parse_mention("{{link:https://example.com, click here}}")
57
+ # # => { kind: "link-ref", uri: "https://example.com", label: "click here" }
58
+ #
59
+ # Glossarist.parse_mention("{{image:diagram.png, The diagram}}")
60
+ # # => { kind: "image-ref", src: "diagram.png", alt: "The diagram" }
61
+ #
62
+ # Glossarist.parse_mention("{{bib:ref_1, ISO 704}}")
63
+ # # => { kind: "bib-ref", id: "ref_1", label: "ISO 704" }
64
+ module MentionParser
65
+ module_function
66
+
67
+ # Parse a single {{...}} mention into a canonical Hash.
68
+ #
69
+ # Returns nil if the input is not a valid mention string.
70
+ # Raises ArgumentError if the mention is malformed (e.g., unknown
71
+ # kind prefix).
72
+ def parse_mention(text)
73
+ return nil unless text.is_a?(String)
74
+
75
+ match = text.match(/\A\s*\{\{([^}]+)\}\}\s*\z/)
76
+ return nil unless match
77
+
78
+ content = match[1].strip
79
+ parse_content(content)
80
+ end
81
+
82
+ # Parse the inside of a {{...}} mention (without the braces).
83
+ def parse_content(content)
84
+ content = content.to_s.strip
85
+ return nil if content.empty?
86
+
87
+ identifier, label = split_identifier_and_label(content)
88
+ dispatch_by_identifier(identifier, label)
89
+ end
90
+
91
+ def split_identifier_and_label(content)
92
+ return [content, nil] unless content.include?(",")
93
+
94
+ parts = content.split(",", 2)
95
+ [parts[0].strip, parts[1].to_s.strip]
96
+ end
97
+ private_class_method :split_identifier_and_label
98
+
99
+ def dispatch_by_identifier(identifier, label)
100
+ case identifier
101
+ when /\Alink:/i then parse_link(identifier, label)
102
+ when /\Aimage:/i then parse_image(identifier, label)
103
+ when /\Abib:/i then parse_bib(identifier, label)
104
+ when /\Acite:/i then parse_cite(identifier, label)
105
+ when /\A(fig|figure):/i then parse_entity(identifier, label, MentionKinds::FIG_REF, "fig:")
106
+ when /\A(table|tbl):/i then parse_entity(identifier, label, MentionKinds::TABLE_REF, "table:")
107
+ when /\A(formula|eq):/i then parse_entity(identifier, label, MentionKinds::FORMULA_REF, "formula:")
108
+ when /\Aurn:/i then parse_urn(identifier, label)
109
+ when /\A\d[\d.-]*\z/ then parse_local_concept(identifier, label)
110
+ else parse_designation(identifier, label)
111
+ end
112
+ end
113
+ private_class_method :dispatch_by_identifier
114
+
115
+ def strip_label(label)
116
+ label.nil? || label.empty? ? nil : label
117
+ end
118
+ private_class_method :strip_label
119
+
120
+ def parse_link(identifier, label)
121
+ uri = identifier.sub(/\Alink:/i, "").strip
122
+ { kind: MentionKinds::LINK_REF, uri: uri, label: strip_label(label) }
123
+ end
124
+ private_class_method :parse_link
125
+
126
+ def parse_image(identifier, label)
127
+ src = identifier.sub(/\Aimage:/i, "").strip
128
+ { kind: MentionKinds::IMAGE_REF, src: src, alt: strip_label(label) }
129
+ end
130
+ private_class_method :parse_image
131
+
132
+ def parse_bib(identifier, label)
133
+ id = identifier.sub(/\Abib:/i, "").strip
134
+ { kind: MentionKinds::BIB_REF, id: id, label: strip_label(label) }
135
+ end
136
+ private_class_method :parse_bib
137
+
138
+ def parse_cite(identifier, label)
139
+ id = strip_quote_wrapping(identifier.sub(/\Acite:/i, "").strip)
140
+ { kind: MentionKinds::CITE_REF, id: id, label: strip_label(label) }
141
+ end
142
+ private_class_method :parse_cite
143
+
144
+ def parse_entity(identifier, label, kind, prefix)
145
+ id = identifier.sub(/\A(?:fig|figure|table|tbl|formula|eq):/i, "").strip
146
+ { kind: kind, id: id, label: strip_label(label) }
147
+ end
148
+ private_class_method :parse_entity
149
+
150
+ def parse_urn(identifier, label)
151
+ { kind: MentionKinds::URN_REF, urn: identifier, label: strip_label(label) }
152
+ end
153
+ private_class_method :parse_urn
154
+
155
+ def parse_local_concept(identifier, label)
156
+ { kind: MentionKinds::LOCAL_CONCEPT_REF, id: identifier, label: strip_label(label) }
157
+ end
158
+ private_class_method :parse_local_concept
159
+
160
+ def parse_designation(identifier, label)
161
+ { kind: MentionKinds::DESIGNATION_REF, designation: label || identifier }
162
+ end
163
+ private_class_method :parse_designation
164
+
165
+ # cite: identifiers may be wrapped in double quotes; the wrapping
166
+ # is removed and the doubled "“”" inside unescaped.
167
+ def strip_quote_wrapping(s)
168
+ return s unless s.start_with?('"') && s.end_with?('"') && s.length >= 2
169
+
170
+ s[1..-2].gsub('""', '"')
171
+ end
172
+ private_class_method :strip_quote_wrapping
173
+ end
174
+ end
@@ -1,21 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Glossarist
4
- # Shared payload for every non-verbal representation, whether it lives
5
- # inline on a concept (NonVerbRep) or as a dataset-shared file
6
- # (Figure / Table / Formula).
4
+ # Shared payload for every non-concept entity entities that are NOT
5
+ # concepts at all (Figure, Table, Formula), as opposed to non-verbal
6
+ # designations OF concepts (NonVerbRep).
7
7
  #
8
- # The four attributes here are the common a11y + provenance payload every
9
- # non-verbal entity carries, regardless of content type or scope:
8
+ # Terminology alignment (PROMPT-NOW P3):
9
+ # - Non-verbal refers to the modality of expression (non-verbal
10
+ # designation: symbol, formula expression). PROPERTIES OF concepts.
11
+ # - Non-concept refers to entities that are NOT concepts at all
12
+ # (Figure, Table, Formula). STANDALONE dataset entities.
13
+ #
14
+ # The four attributes here are the common a11y + provenance payload
15
+ # every non-concept entity carries, regardless of content type:
10
16
  #
11
17
  # - +caption+: localized short title (a11y / indexing).
12
18
  # - +description+: localized long description (a11y screen readers).
13
19
  # - +alt+: localized alternative text (a11y short screen-reader label).
14
20
  # - +sources+: bibliographic sources for the representation.
15
21
  #
16
- # Identity (+id+, +identifier+) belongs on subclasses that have it; see
17
- # SharedNonVerbalEntity for the dataset-shared variant.
18
- class NonVerbalEntity < Lutaml::Model::Serializable
22
+ # Identity (+id+, +identifier+) belongs on subclasses that have it;
23
+ # see SharedNonConceptEntity for the dataset-shared variant.
24
+ class NonConceptEntity < Lutaml::Model::Serializable
19
25
  attribute :caption, :hash
20
26
  attribute :description, :hash
21
27
  attribute :alt, :hash
@@ -42,4 +48,7 @@ module Glossarist
42
48
  from_yaml(File.read(path, encoding: "utf-8"))
43
49
  end
44
50
  end
51
+
52
+ # @deprecated Use NonConceptEntity instead. Kept for one release
53
+ # cycle to ease migration of downstream callers.
45
54
  end
@@ -4,17 +4,26 @@ module Glossarist
4
4
  # A concept-local non-verbal representation (ISO 10241-1 §6.5).
5
5
  #
6
6
  # NonVerbRep is the inline form attached directly to a concept's data.
7
- # The dataset-shared form is Figure / Table / Formula. The two share the
8
- # same a11y + provenance payload via NonVerbalEntity; NonVerbRep differs
9
- # only in that it has no dataset-wide identity (no +id+, no +identifier+)
10
- # — its identity is its position inside the parent concept.
7
+ # The dataset-shared form is Figure / Table / Formula. The two share
8
+ # the same a11y + provenance payload via NonConceptEntity; NonVerbRep
9
+ # differs only in that it has no dataset-wide identity (no +id+, no
10
+ # +identifier+) — its identity is its position inside the parent
11
+ # concept.
12
+ #
13
+ # Note on terminology (PROMPT-NOW P3):
14
+ # NonVerbRep IS a non-verbal representation — it's a designation
15
+ # (a way of expressing a concept, distinct from verbal designation
16
+ # like terms). It is NOT a non-concept entity (which is a Figure /
17
+ # Table / Formula standalone). Both happen to inherit from
18
+ # NonConceptEntity for the shared a11y + provenance payload; the
19
+ # name NonVerbRep preserves the modality distinction.
11
20
  #
12
21
  # +type+ discriminates the kind of non-verbal content: "image", "table",
13
22
  # or "formula". When +type+ is "image", +images+ carries one or more
14
23
  # FigureImage variants (responsive, format fallback, dark/light). The
15
24
  # caption/description/alt fields are localized (hash keyed by ISO 639
16
25
  # code) for accessibility.
17
- class NonVerbRep < NonVerbalEntity
26
+ class NonVerbRep < NonConceptEntity
18
27
  attribute :type, :string
19
28
  attribute :images, FigureImage, collection: true, initialize_empty: true
20
29
 
@@ -223,8 +223,56 @@ module Glossarist
223
223
  concept_refs + asset_refs
224
224
  end
225
225
 
226
- def resolve_asciidoc_xref(target)
227
- BibliographicReference.new(anchor: target.strip)
226
+ # AsciiDoc cross-reference: <<target>> or <<target, caption>>
227
+ #
228
+ # PROMPT-NOW P2: the `<<...,...>>` syntax has been overloaded for
229
+ # three different uses, only one of which is legitimate (non-concept
230
+ # entity xrefs). The other two (bibliography lookup, concept
231
+ # citation) bypass the unified resolution cascade.
232
+ #
233
+ # Behavior on encounter:
234
+ # 1. Emit a deprecation warning via Ruby's Warning module.
235
+ # 2. Re-parse to a typed reference based on the target's shape:
236
+ # - explicit fig:/table:/formula: prefix → typed entity xref
237
+ # - numeric id → local ConceptReference (legacy cite)
238
+ # - anything else → ConceptReference (let the resolver handle it)
239
+ # 3. Do NOT treat as a bibliography lookup. Bibliography is reached
240
+ # only via the new {{bib:id}} mention kind.
241
+ def resolve_asciidoc_xref(target, caption = nil)
242
+ target = target.to_s.strip
243
+ caption = caption.to_s.strip unless caption.nil?
244
+ warn_deprecated_xref(target, caption)
245
+
246
+ case target
247
+ when /\Afig(?:ure)?:/i then resolve_non_verbal_mention("fig:", target, caption, FigureReference)
248
+ when /\Atable:/i then resolve_non_verbal_mention("table:", target, caption, TableReference)
249
+ when /\Atbl:/i then resolve_non_verbal_mention("tbl:", target, caption, TableReference)
250
+ when /\Aformula:/i then resolve_non_verbal_mention("formula:", target, caption, FormulaReference)
251
+ when /\Aeq:/i then resolve_non_verbal_mention("eq:", target, caption, FormulaReference)
252
+ when /\A\d[\d.-]*\z/ then resolve_local(caption || target, target)
253
+ else resolve_designation(target, caption)
254
+ end
255
+ end
256
+
257
+ def warn_deprecated_xref(target, caption)
258
+ return unless Warning.respond_to?(:warn)
259
+
260
+ suffix = ", #{caption}" if caption && !caption.empty?
261
+ Warning.warn(
262
+ "[glossarist] <<#{target}#{suffix}>> is deprecated. " \
263
+ "Use {{fig/table/formula:#{target}#{suffix}}} for non-concept entities, " \
264
+ "or {{cite:#{target}#{suffix}}} for concept citations.\n",
265
+ )
266
+ end
267
+
268
+ # {{bib:id}} → BibliographicReference. This is the explicit
269
+ # bibliography-lookup path (PROMPT-NOW P2). Replaces the old
270
+ # <<anchor>> bibliography shortcut which is now deprecated.
271
+ def resolve_bib_key(identifier, display)
272
+ cleaned = identifier.delete_prefix("bib:").strip
273
+ return nil if cleaned.empty?
274
+
275
+ BibliographicReference.new(anchor: cleaned)
228
276
  end
229
277
 
230
278
  def resolve_image_ref(path)
@@ -292,10 +340,13 @@ module Glossarist
292
340
  ) { |ext, content| ext.resolve_mention(content) }
293
341
 
294
342
  # AsciiDoc cross-references: <<anchor>> or <<anchor,display text>>
343
+ # Captures both target and caption so resolve_asciidoc_xref can
344
+ # re-dispatch (PROMPT-NOW P2 — <<>> is deprecated, re-parse to
345
+ # the right kind based on target).
295
346
  register_pattern(
296
347
  name: :asciidoc_xref,
297
- regex: /<<([^,>\n]+?)(?:,[^>\n]*)?>>/,
298
- ) { |ext, target| ext.resolve_asciidoc_xref(target) }
348
+ regex: /<<([^,>\n]+?)(?:,([^>\n]*))?>>/,
349
+ ) { |ext, target, caption| ext.resolve_asciidoc_xref(target, caption) }
299
350
 
300
351
  # Image references: image::path[] or image:path[]
301
352
  register_pattern(
@@ -307,6 +358,10 @@ module Glossarist
307
358
  ext.resolve_cite_key(identifier, display)
308
359
  end
309
360
 
361
+ register_identifier_resolver("bib:") do |ext, identifier, display|
362
+ ext.resolve_bib_key(identifier, display)
363
+ end
364
+
310
365
  register_identifier_resolver("fig:") do |ext, identifier, display|
311
366
  ext.resolve_non_verbal_mention("fig:", identifier, display, FigureReference)
312
367
  end
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Glossarist
4
- # Dataset-shared non-verbal entity — a NonVerbalEntity with a stable
4
+ # Dataset-shared non-concept entity — a NonConceptEntity with a stable
5
5
  # identity. Figure, Table, and Formula inherit from this; NonVerbRep
6
- # (concept-local, positional) inherits from NonVerbalEntity directly.
6
+ # (concept-local, positional) inherits from NonConceptEntity directly.
7
7
  #
8
8
  # The +id+ is the stable identifier used for cross-referencing
9
9
  # (e.g. +figures/fig_A.23.yaml+ → +id: fig_A.23+). The +identifier+ is
10
10
  # the human-readable label (e.g. +"A.23"+) used for display and AsciiDoc
11
11
  # xref targets like +<<fig_A.23>>+.
12
- class SharedNonVerbalEntity < NonVerbalEntity
12
+ class SharedNonConceptEntity < NonConceptEntity
13
13
  attribute :id, :string
14
14
  attribute :identifier, :string
15
15
 
@@ -26,4 +26,7 @@ module Glossarist
26
26
  [id].compact
27
27
  end
28
28
  end
29
+
30
+ # @deprecated Use SharedNonConceptEntity instead. Kept for one
31
+ # release cycle to ease migration.
29
32
  end
@@ -7,7 +7,7 @@ module Glossarist
7
7
  # across concepts. The content is stored as structured data (rows/columns)
8
8
  # or as a markup string (HTML, Markdown, AsciiDoc). Caption, description,
9
9
  # and alt are localized for accessibility.
10
- class Table < SharedNonVerbalEntity
10
+ class Table < SharedNonConceptEntity
11
11
  attribute :content, :hash
12
12
  attribute :format, :string
13
13
 
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module Validators
5
+ # Flags raw HTML in concept text that should be expressed as typed
6
+ # mention syntax instead. Raw HTML bypasses the renderer, is
7
+ # brittle, has no accessibility contract, and can embed
8
+ # deployment-specific URLs.
9
+ #
10
+ # Suggested replacements:
11
+ #
12
+ # <a href="URL">label</a> → {{link:URL, label}}
13
+ # <a href="URL">URL</a> → {{link:URL}}
14
+ # <img src="SRC"> → {{image:SRC}}
15
+ # <img src="SRC" alt="ALT">→ {{image:SRC, ALT}}
16
+ # <iframe src="URL"> → {{link:URL}}
17
+ #
18
+ # Usage:
19
+ # issues = Glossarist::Validators::NoRawHtml.call(concept_text)
20
+ # issues.each { |i| warn i[:message] }
21
+ class NoRawHtml
22
+ # Patterns are intentionally conservative — only tags that have
23
+ # a direct typed-mention replacement. Other HTML tags (<b>, <i>,
24
+ # <sup>, etc.) are left to the renderer's HTML sanitiser.
25
+ LINK_PATTERN = /<a\s+href="([^"]+)"[^>]*>([^<]*)<\/a>/i.freeze
26
+ IMAGE_PATTERN = /<img\s+src="([^"]+)"(?:\s+alt="([^"]*)")?[^>]*>/i.freeze
27
+ IFRAME_PATTERN = /<iframe\s+src="([^"]+)"[^>]*>/i.freeze
28
+
29
+ class << self
30
+ # @param text [String] the concept text to check
31
+ # @return [Array<Hash>] issues with severity, match, suggestion, message
32
+ def call(text)
33
+ return [] unless text.is_a?(String)
34
+
35
+ [].tap do |issues|
36
+ scan_links(text, issues)
37
+ scan_images(text, issues)
38
+ scan_iframes(text, issues)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def scan_links(text, issues)
45
+ text.scan(LINK_PATTERN) do |url, label|
46
+ suggestion = label && !label.strip.empty? ?
47
+ "{{link:#{url}, #{label.strip}}}" :
48
+ "{{link:#{url}}}"
49
+ issues << {
50
+ severity: "warning",
51
+ match: %(<a href="#{url}">#{label}</a>),
52
+ suggestion: suggestion,
53
+ message: "Use {{link:#{url}}} instead of raw <a> tags",
54
+ }
55
+ end
56
+ end
57
+
58
+ def scan_images(text, issues)
59
+ text.scan(IMAGE_PATTERN) do |src, alt|
60
+ suggestion = alt && !alt.strip.empty? ?
61
+ "{{image:#{src}, #{alt.strip}}}" :
62
+ "{{image:#{src}}}"
63
+ issues << {
64
+ severity: "warning",
65
+ match: %(<img src="#{src}"#{" alt=\"#{alt}\"" if alt}>),
66
+ suggestion: suggestion,
67
+ message: "Use {{image:#{src}}} instead of raw <img> tags",
68
+ }
69
+ end
70
+ end
71
+
72
+ def scan_iframes(text, issues)
73
+ text.scan(IFRAME_PATTERN).each do |match|
74
+ url = match.is_a?(Array) ? match[0] : match
75
+ issues << {
76
+ severity: "warning",
77
+ match: %(<iframe src="#{url}">),
78
+ suggestion: "{{link:#{url}}}",
79
+ message: "iframes are not supported as embeds; use {{link:}} instead",
80
+ }
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ # Namespace for standalone validators (not Validation::Rule classes).
5
+ # These are opt-in utilities that consumers call explicitly — they
6
+ # are not auto-registered into the Validation::Rules pipeline.
7
+ module Validators
8
+ autoload :NoRawHtml, "glossarist/validators/no_raw_html"
9
+ end
10
+ end
@@ -4,5 +4,5 @@
4
4
  #
5
5
 
6
6
  module Glossarist
7
- VERSION = "2.13.9"
7
+ VERSION = "2.13.10"
8
8
  end
data/lib/glossarist.rb CHANGED
@@ -40,8 +40,8 @@ module Glossarist
40
40
  autoload :ConceptEnricher, "glossarist/concept_enricher"
41
41
  autoload :Config, "glossarist/config"
42
42
  autoload :LocalizedString, "glossarist/localized_string"
43
- autoload :NonVerbalEntity, "glossarist/non_verbal_entity"
44
- autoload :SharedNonVerbalEntity, "glossarist/shared_non_verbal_entity"
43
+ autoload :NonConceptEntity, "glossarist/non_concept_entity"
44
+ autoload :SharedNonConceptEntity, "glossarist/shared_non_concept_entity"
45
45
  autoload :NonVerbalReference, "glossarist/non_verbal_reference"
46
46
  autoload :Figure, "glossarist/figure"
47
47
  autoload :FigureImage, "glossarist/figure_image"
@@ -65,6 +65,8 @@ 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
70
  autoload :NonVerbRep, "glossarist/non_verb_rep"
69
71
  autoload :Pronunciation, "glossarist/pronunciation"
70
72
  autoload :RelatedConcept, "glossarist/related_concept"
@@ -86,10 +88,35 @@ module Glossarist
86
88
  autoload :GlossaryDefinition, "glossarist/glossary_definition"
87
89
  autoload :GlossaryStore, "glossarist/glossary_store"
88
90
  autoload :Tasks, "glossarist/tasks"
91
+ autoload :Validators, "glossarist/validators"
89
92
 
90
93
  LANG_CODES = %w[eng ara deu fra spa ita jpn kor pol por srp swe zho rus fin
91
94
  dan nld msa nob nno].freeze
92
95
 
96
+ # @deprecated Use NonConceptEntity. Removed in next minor release.
97
+ def self.const_missing(name)
98
+ case name
99
+ when :NonVerbalEntity
100
+ warn "[glossarist] Glossarist::NonVerbalEntity is deprecated; " \
101
+ "use Glossarist::NonConceptEntity instead."
102
+ NonConceptEntity
103
+ when :SharedNonVerbalEntity
104
+ warn "[glossarist] Glossarist::SharedNonVerbalEntity is deprecated; " \
105
+ "use Glossarist::SharedNonConceptEntity instead."
106
+ SharedNonConceptEntity
107
+ else
108
+ super
109
+ end
110
+ end
111
+
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)
118
+ end
119
+
93
120
  SCHEMA_VERSION = "3"
94
121
  V3_SCHEMA_VERSION = "3"
95
122
 
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.9
4
+ version: 2.13.10
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-01 00:00:00.000000000 Z
11
+ date: 2026-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model
@@ -195,6 +195,7 @@ files:
195
195
  - CLAUDE.md
196
196
  - Gemfile
197
197
  - LICENSE.txt
198
+ - PROMPT-NOW.md
198
199
  - README.adoc
199
200
  - Rakefile
200
201
  - config.yml
@@ -291,8 +292,9 @@ files:
291
292
  - lib/glossarist/managed_concept.rb
292
293
  - lib/glossarist/managed_concept_collection.rb
293
294
  - lib/glossarist/managed_concept_data.rb
295
+ - lib/glossarist/mention_parser.rb
296
+ - lib/glossarist/non_concept_entity.rb
294
297
  - lib/glossarist/non_verb_rep.rb
295
- - lib/glossarist/non_verbal_entity.rb
296
298
  - lib/glossarist/non_verbal_reference.rb
297
299
  - lib/glossarist/pronunciation.rb
298
300
  - lib/glossarist/rdf.rb
@@ -352,7 +354,7 @@ files:
352
354
  - lib/glossarist/schema_migration/v0_to_v1.rb
353
355
  - lib/glossarist/schema_migration/v2_to_v3.rb
354
356
  - lib/glossarist/section.rb
355
- - lib/glossarist/shared_non_verbal_entity.rb
357
+ - lib/glossarist/shared_non_concept_entity.rb
356
358
  - lib/glossarist/sts.rb
357
359
  - lib/glossarist/sts/extracted_designation.rb
358
360
  - lib/glossarist/sts/extracted_lang_set.rb
@@ -480,6 +482,8 @@ files:
480
482
  - lib/glossarist/validation/shacl_validator.rb
481
483
  - lib/glossarist/validation/validation_issue.rb
482
484
  - lib/glossarist/validation_result.rb
485
+ - lib/glossarist/validators.rb
486
+ - lib/glossarist/validators/no_raw_html.rb
483
487
  - lib/glossarist/version.rb
484
488
  - memory/project-status.md
485
489
  - relaton-bib-2.0.0.gem