metanorma-plugin-glossarist 0.3.8 → 0.3.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: 6af197cf777b6758b7501329133036b1473879481f64286c06e38a3799361b08
4
- data.tar.gz: 0b8e385ddc8117c9f1793b813b304fcad57492d6379d14130152c97c818c699c
3
+ metadata.gz: 0e98a4e95056510d3c6071f7c46e4bd378b2898e7fa75b44e14d5ca423efc4ca
4
+ data.tar.gz: ebbf812935c1fb98b523675892813de831c1ec87d152d0de78d1fd9103d6342d
5
5
  SHA512:
6
- metadata.gz: cd8f7aa3303315bac0579615dd3263fa76858accc6bfc7d4fd4aae8f9847f913023b0c038cc72a06df783710aeed5f655047fdb480c993cb2bbb64bf26b60ce9
7
- data.tar.gz: 9f7c701f371ac937e815a8ce8242125dcbba3c9b7d84d9b1630f3d02530d1a6a0c2a4f7b37e0b2bd7b09f89425d82da48c4f7fb4d4a3419dd3e6a5337c10d245
6
+ metadata.gz: 99d6b54e76a006d51f7f657e5d408139e0dbcb1af14545f17d8f08f157d7c197c52a88934c84238560d5860dc0131fe78b4b4933c155934aea11410239a8cf6e
7
+ data.tar.gz: 34f5d1c25bb615c2a2a3efdac7710b9dd8e06d0c98c4f63bc4f8846475a952715fdf2269ca4dbc970a9bd5e0bdc17327f9f8204ef2c3a5ba42ed30a75d299747
@@ -5,14 +5,20 @@ require "set"
5
5
  module Metanorma
6
6
  module Plugin
7
7
  module Glossarist
8
+ # Renders iev termbank and dataset bibliography entries as AsciiDoc
9
+ # bibliography items for rendered concepts.
10
+ #
11
+ # Bibliography lookups go through the typed Glossarist::BibliographyData
12
+ # model — entries are matched by `id` and read via BibliographyEntry
13
+ # accessors (#reference, #title, #link).
8
14
  class BibliographyRenderer
9
15
  IEV_ENTRY = "* [[[ievtermbank,IEV]]], _IEV: Electropedia_"
10
16
  IEV_ANCHOR = "ievtermbank"
11
17
 
12
- def initialize(existing_anchors: [], bibliography_data: {})
18
+ def initialize(existing_anchors: [], bibliography: nil)
13
19
  @rendered = {}
14
20
  @existing_anchors = Set.new(existing_anchors)
15
- @bibliography_data = bibliography_data
21
+ @bibliography = bibliography
16
22
  end
17
23
 
18
24
  def render_entry(concept, lang: "eng")
@@ -32,15 +38,14 @@ module Metanorma
32
38
  source_entries(l10n)
33
39
  end.flatten
34
40
 
35
- xref_entries = concepts.filter_map do |concept|
41
+ xref = concepts.filter_map do |concept|
36
42
  l10n = concept.localization(lang)
37
43
  next unless l10n
38
44
 
39
45
  xref_entries(l10n)
40
46
  end.flatten
41
47
 
42
- all_entries.concat(xref_entries)
43
-
48
+ all_entries.concat(xref)
44
49
  all_entries.sort.join("\n")
45
50
  end
46
51
 
@@ -75,26 +80,30 @@ module Metanorma
75
80
  xref_ids.filter_map do |ref_id|
76
81
  next if @rendered.value?(ref_id)
77
82
  next if @existing_anchors.include?(ref_id)
78
- next unless @bibliography_data.key?(ref_id)
79
-
80
- anchor = ref_id
81
- @rendered[ref_id] = anchor
83
+ next unless bibliography_entry(ref_id)
82
84
 
83
- format_entry(anchor, ref_id)
85
+ @rendered[ref_id] = ref_id
86
+ format_entry(ref_id, ref_id)
84
87
  end
85
88
  end
86
89
 
87
90
  def format_entry(anchor, ref)
88
- bib = @bibliography_data[ref]
89
- return "* [[[#{anchor},#{ref}]]]" unless bib
91
+ entry = bibliography_entry(ref)
92
+ return "* [[[#{anchor},#{ref}]]]" unless entry
90
93
 
91
- display_ref = bib["reference"] || ref
94
+ display_ref = entry.reference || ref
92
95
  parts = ["* [[[#{anchor},#{display_ref}]]]"]
93
- parts << ", _#{bib['title']}_" if bib["title"]
94
- parts << ". Available at: #{bib['link']} " if bib["link"]
96
+ parts << ", _#{entry.title}_" if entry.title
97
+ parts << ". Available at: #{entry.link} " if entry.link
95
98
  parts.join
96
99
  end
97
100
 
101
+ def bibliography_entry(ref_id)
102
+ return nil unless @bibliography
103
+
104
+ @bibliography.find(ref_id)
105
+ end
106
+
98
107
  def extract_content_xrefs(l10n)
99
108
  parts = []
100
109
  l10n.definition&.each { |d| parts << d.content.to_s }
@@ -3,64 +3,83 @@
3
3
  module Metanorma
4
4
  module Plugin
5
5
  module Glossarist
6
+ # Filters a concept collection by lang, domain, section, tag, generic
7
+ # field paths, and sort_by. Composable: every filter narrows the
8
+ # collection independently.
9
+ #
10
+ # Section filtering supports cascading membership: a concept in
11
+ # section "3.1.1" is also a member of "3.1" and "3" via transitive
12
+ # ancestor traversal. This requires a DatasetRegister collaborator
13
+ # (passed via `#apply`'s second arg) when section filtering is used.
6
14
  class ConceptFilter
7
15
  COLLECTION_FILTERS = %w[lang domain group section tag sort_by].freeze
8
16
  SORT_LAST = ["￿"].freeze
17
+ SECTION_REF_TYPE = "section"
18
+ DOMAIN_REF_TYPE = "domain"
9
19
 
10
20
  def initialize(filters)
11
21
  @filters = filters || {}
12
22
  @resolver = ConceptPathResolver.new
13
23
  end
14
24
 
15
- def apply(collection)
16
- result = collection
25
+ # Applies all configured filters in canonical order.
26
+ # @param collection [Enumerable<ManagedConcept>]
27
+ # @param register [Glossarist::DatasetRegister, nil] required for
28
+ # cascading section filtering; ignored otherwise.
29
+ # @return [Array<ManagedConcept>]
30
+ def apply(collection, register: nil)
31
+ result = collection.to_a
17
32
  result = filter_by_lang(result) if @filters.key?("lang")
18
33
  if @filters.key?("domain") || @filters.key?("group")
19
34
  result = filter_by_domain(result)
20
35
  end
21
- result = filter_by_section(result) if @filters.key?("section")
36
+ if @filters.key?("section")
37
+ result = filter_by_section(result,
38
+ register)
39
+ end
22
40
  result = filter_by_tag(result) if @filters.key?("tag")
23
- result = filter_by_field(result) if field_filter?
41
+ result = filter_by_field(result) if field_filters?
24
42
  result = sort(result) if @filters.key?("sort_by")
25
43
  result
26
44
  end
27
45
 
28
46
  private
29
47
 
30
- def field_filter?
48
+ def field_filters?
31
49
  (@filters.keys - COLLECTION_FILTERS).any?
32
50
  end
33
51
 
34
- def field_filter_key
35
- (@filters.keys - COLLECTION_FILTERS).first
36
- end
37
-
38
52
  def filter_by_lang(collection)
39
53
  lang = @filters["lang"]
40
54
  collection.reject { |c| c.localization(lang).nil? }
41
55
  end
42
56
 
43
57
  def filter_by_domain(collection)
44
- domain = @filters["domain"] || @filters["group"]
45
- collection.select do |c|
46
- c.data.domains&.any? { |d| d.concept_id == domain }
58
+ domain_id = @filters["domain"] || @filters["group"]
59
+ collection.select do |concept|
60
+ domain_ids(concept).include?(domain_id)
47
61
  end
48
62
  end
49
63
 
50
64
  def filter_by_tag(collection)
51
65
  tag = @filters["tag"]
52
- collection.select do |c|
53
- c.data.tags&.include?(tag)
54
- end
66
+ collection.select { |c| c.data.tags&.include?(tag) }
55
67
  end
56
68
 
57
- def filter_by_section(collection)
58
- section_id = @filters["section"]
59
- collection.select do |c|
60
- c.data.domains&.any? { |d| d.concept_id == "section-#{section_id}" }
69
+ def filter_by_section(collection, register)
70
+ target_id = @filters["section"]
71
+ cascade = SectionCascade.new(register)
72
+ collection.select do |concept|
73
+ cascade.member?(concept, target_id)
61
74
  end
62
75
  end
63
76
 
77
+ def domain_ids(concept)
78
+ Array(concept.data&.domains)
79
+ .select { |d| d.ref_type == DOMAIN_REF_TYPE }
80
+ .filter_map(&:concept_id)
81
+ end
82
+
64
83
  def sort(collection)
65
84
  field = @filters["sort_by"]
66
85
  return collection unless field
@@ -84,29 +103,32 @@ module Metanorma
84
103
  end
85
104
 
86
105
  def filter_by_field(collection)
87
- path = field_filter_key
88
- value = @filters[path]
89
-
90
- start_with = path.include?(".start_with(")
91
- path, match_value = extract_start_with(path, value, start_with)
106
+ path, value, start_with = field_filter_spec
107
+ start_with_match_value = extract_start_with_value(path, value)
108
+ path, match_value = if start_with_match_value
109
+ [start_with_match_value[:path],
110
+ start_with_match_value[:value]]
111
+ else
112
+ [path, value]
113
+ end
92
114
 
93
115
  collection.select do |concept|
94
116
  actual = @resolver.resolve(concept, path)
95
- if start_with
96
- actual&.start_with?(match_value)
97
- else
98
- actual == value
99
- end
117
+ start_with ? actual&.start_with?(match_value) : actual == value
100
118
  end
101
119
  end
102
120
 
103
- def extract_start_with(path, value, start_with)
104
- return [path, value] unless start_with
121
+ def field_filter_spec
122
+ path = (@filters.keys - COLLECTION_FILTERS).first
123
+ start_with = path.include?(".start_with(")
124
+ [path, @filters[path], start_with]
125
+ end
105
126
 
127
+ def extract_start_with_value(path, _value)
106
128
  match = path.match(/^([^.]+(?:\.[^.]+)*)\.start_with\(([^)]+)\)$/)
107
- return [path, value] unless match
129
+ return nil unless match
108
130
 
109
- [match[1], match[2]]
131
+ { path: match[1], value: match[2] }
110
132
  end
111
133
  end
112
134
  end
@@ -15,6 +15,7 @@ module Metanorma
15
15
  BLOCK_REGEX = /^\[glossarist,(.+?),(.+?)\]$/m
16
16
  BIBLIOGRAPHY_REGEX = /^glossarist::render_bibliography\[(.*?)\]$/m
17
17
  BIBLIOGRAPHY_ENTRY_REGEX = /^glossarist::render_bibliography_entry\[(.*?)\]$/m
18
+ NON_VERBAL_REGEX = /^glossarist::render_(figures|tables|formulas)\[(.*?)\]$/m
18
19
 
19
20
  BIB_ANCHOR_REGEX = /^\*\s*\[\[\[([^,]+)/
20
21
 
@@ -69,32 +70,47 @@ module Metanorma
69
70
  end
70
71
 
71
72
  def process_line(document, input_lines, current_line, liquid_doc)
72
- if (match = current_line.match(DATASET_ATTR_REGEX))
73
- process_dataset_tag(document, input_lines, liquid_doc, match)
74
- elsif (match = current_line.match(RENDER_REGEX))
75
- process_render_tag(liquid_doc, match)
76
- elsif (match = current_line.match(IMPORT_SECTIONS_REGEX))
77
- process_import_sections_tag(document, liquid_doc, match)
78
- elsif (match = current_line.match(IMPORT_REGEX))
79
- process_import_tag(liquid_doc, match)
80
- elsif (match = current_line.match(BIBLIOGRAPHY_REGEX))
81
- process_bibliography(document, liquid_doc, match)
82
- elsif (match = current_line.match(BIBLIOGRAPHY_ENTRY_REGEX))
83
- process_bibliography_entry(document, liquid_doc, match)
84
- elsif (match = current_line.match(BLOCK_REGEX))
85
- process_glossarist_block(document, liquid_doc, input_lines, match)
73
+ handler = directive_handler(current_line)
74
+ if handler
75
+ handler.call(document, input_lines, liquid_doc)
86
76
  else
87
- if /^==+ \S/.match?(current_line)
88
- @title_depth = current_line.sub(/ .*$/,
89
- "").size
90
- end
91
- if (match = current_line.match(BIB_ANCHOR_REGEX))
92
- @existing_bib_anchors << match[1]
93
- end
94
- liquid_doc.add_content(current_line)
77
+ handle_plain_line(current_line, liquid_doc)
78
+ end
79
+ end
80
+
81
+ # Returns a callable that handles the directive on +line+, or nil
82
+ # for plain content. Keeping the dispatch table small (regex →
83
+ # block) lets us add a new directive by appending one entry.
84
+ def directive_handler(line)
85
+ if (m = line.match(DATASET_ATTR_REGEX))
86
+ ->(doc, lines, ldoc) { process_dataset_tag(doc, lines, ldoc, m) }
87
+ elsif (m = line.match(RENDER_REGEX))
88
+ ->(_, _, ldoc) { process_render_tag(ldoc, m) }
89
+ elsif (m = line.match(IMPORT_SECTIONS_REGEX))
90
+ ->(doc, _, ldoc) { process_import_sections_tag(doc, ldoc, m) }
91
+ elsif (m = line.match(IMPORT_REGEX))
92
+ ->(_, _, ldoc) { process_import_tag(ldoc, m) }
93
+ elsif (m = line.match(BIBLIOGRAPHY_REGEX))
94
+ ->(doc, _, ldoc) { process_bibliography(doc, ldoc, m) }
95
+ elsif (m = line.match(BIBLIOGRAPHY_ENTRY_REGEX))
96
+ ->(doc, _, ldoc) { process_bibliography_entry(doc, ldoc, m) }
97
+ elsif (m = line.match(NON_VERBAL_REGEX))
98
+ ->(_, _, ldoc) { process_non_verbal(ldoc, m) }
99
+ elsif (m = line.match(BLOCK_REGEX))
100
+ ->(doc, lines, ldoc) { process_glossarist_block(doc, ldoc, lines, m) }
95
101
  end
96
102
  end
97
103
 
104
+ def handle_plain_line(current_line, liquid_doc)
105
+ if /^==+ \S/.match?(current_line)
106
+ @title_depth = current_line.sub(/ .*$/, "").size
107
+ end
108
+ if (match = current_line.match(BIB_ANCHOR_REGEX))
109
+ @existing_bib_anchors << match[1]
110
+ end
111
+ liquid_doc.add_content(current_line)
112
+ end
113
+
98
114
  def process_dataset_tag(document, input_lines, liquid_doc, match)
99
115
  @seen_glossarist = true
100
116
  @registry.register(document, match[1])
@@ -157,7 +173,8 @@ module Metanorma
157
173
  renderer = @renderer
158
174
  rendered = renderer.render_concept(concept,
159
175
  depth: @title_depth,
160
- anchor_prefix: options["anchor-prefix"])
176
+ anchor_prefix: options["anchor-prefix"],
177
+ non_verbal: non_verbal_for(context_name))
161
178
  liquid_doc.add_content("\n#{rendered}")
162
179
  end
163
180
 
@@ -172,13 +189,15 @@ module Metanorma
172
189
  return unless dataset
173
190
 
174
191
  filter_options = options.except(*RENDER_OPTIONS)
175
- concepts = ConceptFilter.new(filter_options).apply(dataset)
192
+ concepts = ConceptFilter.new(filter_options)
193
+ .apply(dataset, register: @registry.register_for(context_name))
176
194
  concepts = concepts.select(&:default_designation)
177
195
  @rendered_concepts.concat(concepts)
178
196
  renderer = @renderer
179
197
  rendered = renderer.render_concepts(concepts,
180
198
  depth: @title_depth,
181
- anchor_prefix: options["anchor-prefix"])
199
+ anchor_prefix: options["anchor-prefix"],
200
+ non_verbal: non_verbal_for(context_name))
182
201
  liquid_doc.add_content("\n#{rendered}")
183
202
  end
184
203
 
@@ -188,39 +207,47 @@ module Metanorma
188
207
  context_name = matches[0]
189
208
  options = parse_options(matches[1..])
190
209
 
191
- sections = @registry.register_sections(context_name)
210
+ register = @registry.register_for(context_name)
211
+ sections = register&.sections
192
212
  return unless sections && !sections.empty?
193
213
 
194
214
  dataset = @registry.resolve_dataset(nil, context_name)
195
215
  return unless dataset
196
216
 
197
- renderer = @renderer
198
- sort_by = options["sort_by"] || "term"
217
+ parts = render_sections(dataset, register, sections, context_name, options)
218
+ liquid_doc.add_content("\n#{parts.join("\n\n")}")
219
+ end
220
+
221
+ def render_sections(dataset, register, sections, context_name, options)
199
222
  section_filter = SectionFilter.new(
200
223
  exclude: (options["section_exclude"] || "").split("|"),
201
224
  include: (options["section_include"] || "").split("|"),
202
225
  )
203
226
  filtered = section_filter.apply(sections)
204
- parts = render_section_concepts(filtered, dataset, renderer,
205
- sort_by, options)
206
- liquid_doc.add_content("\n#{parts.join("\n\n")}")
227
+ renderer = SectionRenderer.new(
228
+ dataset: dataset,
229
+ register: register,
230
+ renderer: @renderer,
231
+ depth: @title_depth,
232
+ sort_by: options["sort_by"] || SectionRenderer::DEFAULT_SORT_BY,
233
+ anchor_prefix: options["anchor-prefix"],
234
+ non_verbal: non_verbal_for(context_name),
235
+ )
236
+ renderer.render(filtered) do |concepts|
237
+ @rendered_concepts.concat(concepts)
238
+ end
207
239
  end
208
240
 
209
- def render_section_concepts(sections, dataset, renderer, sort_by,
210
- options)
211
- sections.filter_map do |section|
212
- filter_options = { "section" => section.id, "sort_by" => sort_by }
213
- concepts = ConceptFilter.new(filter_options).apply(dataset)
214
- concepts = concepts.select(&:default_designation)
215
- next if concepts.empty?
241
+ # Builds a NonVerbalRenderer scoped to a dataset context, or
242
+ # returns nil if the dataset has no figures/tables/formulas. The
243
+ # resulting renderer is passed through to TemplateRenderer and
244
+ # SectionRenderer so concept-attached refs render as AsciiDoc
245
+ # blocks alongside the concept body.
246
+ def non_verbal_for(context_name)
247
+ collections = @registry.non_verbal_collections(context_name)
248
+ return nil if collections.empty?
216
249
 
217
- @rendered_concepts.concat(concepts)
218
- heading = "#{'=' * (@title_depth + 1)} #{section.name || section.id}"
219
- rendered = renderer.render_concepts(concepts,
220
- depth: @title_depth + 1,
221
- anchor_prefix: options["anchor-prefix"])
222
- "#{heading}\n\n#{rendered}"
223
- end
250
+ NonVerbalRenderer.new(collections: collections)
224
251
  end
225
252
 
226
253
  def process_bibliography(document, liquid_doc, match)
@@ -237,7 +264,7 @@ module Metanorma
237
264
 
238
265
  renderer = BibliographyRenderer.new(
239
266
  existing_anchors: @existing_bib_anchors,
240
- bibliography_data: @registry.bibliography_data,
267
+ bibliography: @registry.bibliography_for(dataset_name),
241
268
  )
242
269
  liquid_doc.add_content(renderer.render_all(concepts))
243
270
  end
@@ -250,12 +277,27 @@ module Metanorma
250
277
 
251
278
  renderer = BibliographyRenderer.new(
252
279
  existing_anchors: @existing_bib_anchors,
253
- bibliography_data: @registry.bibliography_data,
280
+ bibliography: @registry.bibliography_for(dataset_name),
254
281
  )
255
282
  entry = renderer.render_entry(concept)
256
283
  liquid_doc.add_content(entry) if entry
257
284
  end
258
285
 
286
+ # Renders all dataset-level entities of a non-verbal kind
287
+ # (figures, tables, formulas) as AsciiDoc blocks. The directive
288
+ # shape is +glossarist::render_<kind>[dataset]+.
289
+ def process_non_verbal(liquid_doc, match)
290
+ @seen_glossarist = true
291
+ kind = :"#{match[1]}"
292
+ dataset_name = match[2].strip
293
+ collection = @registry.non_verbal_collection(dataset_name, kind)
294
+ return unless collection
295
+
296
+ renderer = NonVerbalRenderer.new(collections: { kind => collection })
297
+ rendered = renderer.render_kind(kind)
298
+ liquid_doc.add_content("\n#{rendered}") unless rendered.empty?
299
+ end
300
+
259
301
  def relative_file_path(document, file_path)
260
302
  return file_path if File.absolute_path?(file_path)
261
303
 
@@ -5,95 +5,160 @@ require "glossarist"
5
5
  module Metanorma
6
6
  module Plugin
7
7
  module Glossarist
8
+ # Resolves, caches, and exposes dataset models for a document.
9
+ #
10
+ # Single source of truth for everything the preprocessor needs from a
11
+ # glossarist dataset: concepts, section hierarchy, bibliography, and
12
+ # dataset-level non-verbal entities (figures, tables, formulas).
13
+ # Each is exposed as the typed Glossarist model object so callers
14
+ # never poke at raw YAML hashes.
8
15
  class DatasetRegistry
16
+ BIBLIOGRAPHY_FILENAME = "bibliography.yaml"
17
+ REGISTER_FILENAME = "register.yaml"
18
+
19
+ # Map of plural kind symbol → (collection class, subdirectory name).
20
+ # Adding a new non-verbal kind = adding one entry here. The accessor
21
+ # `{kind}_for` and loader are derived from this table.
22
+ NON_VERBAL_KINDS = {
23
+ figures: [::Glossarist::Collections::FigureCollection, "figures"],
24
+ tables: [::Glossarist::Collections::TableCollection, "tables"],
25
+ formulas: [::Glossarist::Collections::FormulaCollection, "formulas"],
26
+ }.freeze
27
+
9
28
  def initialize
10
- @datasets = {}
11
- @path_cache = {}
12
- @bibliography_data = {}
13
- @context_names = []
29
+ @stores = {}
30
+ @registers = {}
31
+ @bibliographies = {}
32
+ @non_verbal = {}
33
+ @context_paths = {}
14
34
  end
15
35
 
16
36
  def register(document, contexts)
17
- paths = contexts.split(";").map do |context|
37
+ contexts.split(";").map do |context|
18
38
  context_name, file_path = context.split(":", 2).map(&:strip)
19
39
  path = relative_file_path(document, file_path)
20
- @datasets[context_name] = load_dataset(path).to_a
40
+ @context_paths[context_name] = path
21
41
  "#{context_name}=#{path}"
22
42
  end
23
- @context_names.concat(paths)
24
- end
25
-
26
- def load_cached(path)
27
- @path_cache[path] ||= load_dataset(path)
28
43
  end
29
44
 
30
45
  def resolve_dataset(document, dataset_name)
31
- dataset = @datasets[dataset_name]
32
- return dataset if dataset
46
+ return concepts_for(dataset_name) if @context_paths.key?(dataset_name)
33
47
 
34
- return unless document
48
+ path = relative_file_path(document, dataset_name) if document
49
+ return unless path
35
50
 
36
- path = relative_file_path(document, dataset_name)
37
- @datasets[dataset_name] = load_dataset(path).to_a
51
+ concepts_at(path)
38
52
  end
39
53
 
40
54
  def find_concept(dataset_name, concept_name, document = nil)
41
55
  dataset = resolve_dataset(document, dataset_name)
42
56
  return unless dataset
43
57
 
44
- dataset.find do |concept|
45
- concept.default_designation == concept_name
46
- end
58
+ dataset.find { |concept| concept.default_designation == concept_name }
47
59
  end
48
60
 
49
61
  def context_path(key)
50
- return nil if @context_names.empty?
62
+ @context_paths[key]
63
+ end
51
64
 
52
- found = @context_names.find do |context|
53
- context_name, = context.split("=")
54
- context_name.strip == key
55
- end
56
- found&.split("=")&.last&.strip
65
+ # Returns the DatasetRegister for a registered context, or nil.
66
+ # The DatasetRegister is the single source of truth for section
67
+ # hierarchy and concept→section membership (cascading ancestors).
68
+ def register_for(context_name)
69
+ path = @context_paths[context_name]
70
+ return nil unless path
71
+
72
+ register_at(path)
57
73
  end
58
74
 
59
75
  def register_sections(context_name)
60
- dataset_path = context_path(context_name)
61
- return nil unless dataset_path
76
+ register_for(context_name)&.sections
77
+ end
78
+
79
+ # Returns the BibliographyData for a registered context, or nil.
80
+ # Exposed as the typed model so callers iterate entries via
81
+ # BibliographyEntry accessors (#id, #reference, #title, #link).
82
+ def bibliography_for(context_name)
83
+ path = @context_paths[context_name]
84
+ return nil unless path
62
85
 
63
- @register_cache ||= {}
64
- @register_cache[dataset_path] ||=
65
- ::Glossarist::DatasetRegister.from_directory(dataset_path)
66
- @register_cache[dataset_path]&.sections
86
+ bibliography_at(path)
67
87
  end
68
88
 
69
- def bibliography_data
70
- @bibliography_data
89
+ # Returns concepts cached at an absolute path. Used by Liquid
90
+ # blocks that receive a pre-resolved absolute path.
91
+ def concepts_at(path)
92
+ store_for(path).concepts
71
93
  end
72
94
 
73
- private
95
+ # Returns the typed NonVerbalCollection for a registered context
96
+ # (e.g. FigureCollection), or nil if the dataset has no such
97
+ # subdirectory. +kind+ is one of the keys of NON_VERBAL_KINDS.
98
+ def non_verbal_collection(context_name, kind)
99
+ unless NON_VERBAL_KINDS.key?(kind)
100
+ raise ArgumentError, "unknown non-verbal kind: #{kind.inspect}"
101
+ end
102
+
103
+ path = @context_paths[context_name]
104
+ return nil unless path
105
+
106
+ collection_class, subdir = NON_VERBAL_KINDS.fetch(kind)
107
+ non_verbal_at(path, kind, subdir, collection_class)
108
+ end
74
109
 
75
- def load_dataset(path)
76
- @path_cache[path] ||= begin
77
- collection = ::Glossarist::ManagedConceptCollection.new
78
- collection.load_from_files(path)
79
- load_bibliography_data(path)
80
- collection
110
+ NON_VERBAL_KINDS.each_key do |kind|
111
+ define_method("#{kind}_for") do |context_name|
112
+ non_verbal_collection(context_name, kind)
81
113
  end
82
114
  end
83
115
 
84
- def load_bibliography_data(dataset_path)
85
- bib_path = File.join(dataset_path, "bibliography.yaml")
86
- return unless File.exist?(bib_path)
116
+ # Returns all available non-verbal collections for a context as a
117
+ # hash keyed by kind symbol (e.g. +{ figures: FigureCollection }+).
118
+ # Kinds whose subdirectory doesn't exist are omitted. Convenient
119
+ # for building a NonVerbalRenderer in one call.
120
+ def non_verbal_collections(context_name)
121
+ NON_VERBAL_KINDS.each_with_object({}) do |(kind, _), memo|
122
+ collection = non_verbal_collection(context_name, kind)
123
+ memo[kind] = collection if collection
124
+ end
125
+ end
87
126
 
88
- entries = YAML.safe_load_file(bib_path,
89
- permitted_classes: [Symbol, Date])
90
- return unless entries.is_a?(Array)
127
+ private
91
128
 
92
- @bibliography_data = entries.each_with_object({}) do |entry, hash|
93
- hash[entry["id"]] = entry if entry["id"]
129
+ def concepts_for(context_name)
130
+ path = @context_paths[context_name]
131
+ path ? concepts_at(path) : nil
132
+ end
133
+
134
+ def store_for(path)
135
+ @stores[path] ||= begin
136
+ store = ::Glossarist::GlossaryStore.new
137
+ store.load_directory(path)
138
+ store
94
139
  end
95
140
  end
96
141
 
142
+ def register_at(path)
143
+ @registers[path] ||=
144
+ ::Glossarist::DatasetRegister.from_directory(path)
145
+ end
146
+
147
+ def bibliography_at(path)
148
+ @bibliographies[path] ||= begin
149
+ file = File.join(path, BIBLIOGRAPHY_FILENAME)
150
+ ::Glossarist::BibliographyData.from_file(file)
151
+ end
152
+ end
153
+
154
+ def non_verbal_at(path, kind, subdir, collection_class)
155
+ dir = File.join(path, subdir)
156
+ return nil unless File.directory?(dir)
157
+
158
+ @non_verbal[path] ||= {}
159
+ @non_verbal[path][kind] ||= collection_class.from_directory(dir)
160
+ end
161
+
97
162
  def relative_file_path(document, file_path)
98
163
  return file_path if File.absolute_path?(file_path)
99
164
 
@@ -31,7 +31,7 @@ module Metanorma
31
31
 
32
32
  @contexts.each do |local_context|
33
33
  path = local_context[:file_path].strip
34
- collection = registry ? registry.load_cached(path) : load_collection(path)
34
+ collection = registry ? registry.concepts_at(path) : load_collection(path)
35
35
  filtered = ConceptFilter.new(@raw_filters).apply(collection)
36
36
  context[local_context[:name]] = filtered.map do |c|
37
37
  ManagedConceptDrop.new(c)
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Plugin
5
+ module Glossarist
6
+ module NonVerbalFormatters
7
+ # Shared rendering helpers for non-verbal entity formatters.
8
+ #
9
+ # Each formatter owns the kind-specific body (image block, table
10
+ # block, stem block) and delegates anchor, caption, and a11y
11
+ # framing to this base. Localized text is resolved by ISO 639 code
12
+ # with graceful fallback to the first available value.
13
+ class Base
14
+ def initialize(entity, lang: "eng")
15
+ @entity = entity
16
+ @lang = lang
17
+ end
18
+
19
+ def to_asciidoc
20
+ parts = [anchor_line, caption_line, body].compact.reject(&:empty?)
21
+ "#{parts.join("\n")}\n"
22
+ end
23
+
24
+ protected
25
+
26
+ attr_reader :entity, :lang
27
+
28
+ # Subclasses implement this with the kind-specific AsciiDoc body
29
+ # (e.g. image::, table, stem block).
30
+ def body
31
+ raise NotImplementedError
32
+ end
33
+
34
+ def anchor_line
35
+ id = entity.id
36
+ id ? "[[#{id}]]" : nil
37
+ end
38
+
39
+ def caption_line
40
+ text = localized(entity.caption)
41
+ text ? ".#{text}" : nil
42
+ end
43
+
44
+ def alt_text
45
+ localized(entity.alt)
46
+ end
47
+
48
+ def description_text
49
+ localized(entity.description)
50
+ end
51
+
52
+ # Picks the value for the requested language, falling back to
53
+ # the first available value if missing. Returns nil for empty
54
+ # or absent hashes.
55
+ def localized(hash)
56
+ return nil if hash.nil? || hash.empty?
57
+
58
+ hash[lang] || hash.values.first
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Plugin
5
+ module Glossarist
6
+ module NonVerbalFormatters
7
+ # Renders a Glossarist::Figure as an AsciiDoc image block.
8
+ #
9
+ # Picks a single best image variant: vector (SVG) preferred for
10
+ # resolution-independence, then any first image. Subfigures are
11
+ # rendered recursively as separate image blocks so each carries
12
+ # its own anchor and caption.
13
+ class Figure < Base
14
+ ROLE_PRIORITY = %w[vector raster print light dark].freeze
15
+
16
+ protected
17
+
18
+ def body
19
+ image = best_image
20
+ return subfigure_blocks if image.nil?
21
+
22
+ line = "image::#{image.src}[#{image_attrs(image)}]"
23
+ subfigure_blocks ? "#{line}\n\n#{subfigure_blocks}" : line
24
+ end
25
+
26
+ private
27
+
28
+ def best_image
29
+ images = Array(entity.images)
30
+ return nil if images.empty?
31
+
32
+ ROLE_PRIORITY.each do |role|
33
+ found = images.find { |img| img.role == role }
34
+ return found if found
35
+ end
36
+ images.first
37
+ end
38
+
39
+ def image_attrs(image)
40
+ attrs = []
41
+ attrs << alt_text if alt_text
42
+ attrs << "width=#{image.width}" if image.width
43
+ attrs << "height=#{image.height}" if image.height
44
+ attrs.join(",")
45
+ end
46
+
47
+ def subfigure_blocks
48
+ subs = Array(entity.subfigures)
49
+ return nil if subs.empty?
50
+
51
+ subs.map do |sub|
52
+ self.class.new(sub, lang: lang).to_asciidoc
53
+ end.join("\n").strip
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Plugin
5
+ module Glossarist
6
+ module NonVerbalFormatters
7
+ # Renders a Glossarist::Formula as an AsciiDoc stem block.
8
+ #
9
+ # The expression hash is keyed by language (matching caption/alt);
10
+ # the +notation+ field carries the markup language (latex, mathml,
11
+ # asciimath) but the body is format-agnostic — Metanorma's stem
12
+ # block accepts any notation supported by the renderer.
13
+ class Formula < Base
14
+ protected
15
+
16
+ def body
17
+ expr = localized(entity.expression)
18
+ return "" if expr.nil? || expr.empty?
19
+
20
+ <<~STEM
21
+ [stem]
22
+ ++++
23
+ #{expr}
24
+ ++++
25
+ STEM
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Plugin
5
+ module Glossarist
6
+ module NonVerbalFormatters
7
+ # Renders a Glossarist::Table as an AsciiDoc table block.
8
+ #
9
+ # Two payload shapes are supported:
10
+ # - +format: structured+ — +content+ has +headers+ and +rows+
11
+ # arrays, rendered as an AsciiDoc table.
12
+ # - +format: asciidoc+ (or any non-structured) — +content+ is a
13
+ # raw markup string emitted verbatim between caption and the
14
+ # next block.
15
+ class Table < Base
16
+ STRUCTURED = "structured"
17
+
18
+ protected
19
+
20
+ def body
21
+ entity.format == STRUCTURED ? structured_table : raw_block
22
+ end
23
+
24
+ private
25
+
26
+ def structured_table
27
+ lines = ["|==="]
28
+ append_headers(lines)
29
+ Array(entity.content&.dig("rows")).each do |row|
30
+ lines << "|#{Array(row).join(' |')}"
31
+ end
32
+ lines << "|==="
33
+ lines.join("\n")
34
+ end
35
+
36
+ def append_headers(lines)
37
+ headers = Array(entity.content&.dig("headers"))
38
+ lines << "|#{headers.join(' |')}" unless headers.empty?
39
+ end
40
+
41
+ def raw_block
42
+ content = entity.content
43
+ (content&.dig("asciidoc") || content&.dig("text")).to_s
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Plugin
5
+ module Glossarist
6
+ # Namespace for per-kind AsciiDoc formatters for dataset-level
7
+ # non-verbal entities. Adding a new kind = adding a new formatter
8
+ # class and registering it in NonVerbalRenderer::FORMATTERS.
9
+ module NonVerbalFormatters
10
+ autoload :Base, "metanorma/plugin/glossarist/non_verbal_formatters/base"
11
+ autoload :Figure,
12
+ "metanorma/plugin/glossarist/non_verbal_formatters/figure"
13
+ autoload :Table,
14
+ "metanorma/plugin/glossarist/non_verbal_formatters/table"
15
+ autoload :Formula,
16
+ "metanorma/plugin/glossarist/non_verbal_formatters/formula"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Plugin
5
+ module Glossarist
6
+ # Renders dataset-level non-verbal entities (Figure, Table, Formula)
7
+ # as AsciiDoc blocks. MECE sibling to BibliographyRenderer: where
8
+ # BibliographyRenderer owns citation provenance, NonVerbalRenderer
9
+ # owns the rendering of authored figures/tables/formulas.
10
+ #
11
+ # Per-kind formatting is delegated to a formatter class registered
12
+ # in +FORMATTERS+. Adding a new kind = adding one formatter class
13
+ # and one entry here; the dispatcher itself never changes shape.
14
+ class NonVerbalRenderer
15
+ FORMATTERS = {
16
+ figures: NonVerbalFormatters::Figure,
17
+ tables: NonVerbalFormatters::Table,
18
+ formulas: NonVerbalFormatters::Formula,
19
+ }.freeze
20
+
21
+ # @param collections [Hash{Symbol => NonVerbalCollection, nil}]
22
+ # one entry per non-verbal kind, e.g.
23
+ # `{ figures: FigureCollection, tables: ..., formulas: ... }`.
24
+ # Missing or nil entries are silently skipped.
25
+ def initialize(collections:, lang: "eng")
26
+ @collections = collections
27
+ @lang = lang
28
+ end
29
+
30
+ # Render every entity in the named collection.
31
+ #
32
+ # @param kind [Symbol] key in FORMATTERS (e.g. +:figures+)
33
+ # @return [String] AsciiDoc blocks joined by blank lines, or ""
34
+ def render_kind(kind)
35
+ collection = @collections[kind]
36
+ return "" if collection.nil? || collection.entries.empty?
37
+
38
+ entries = collection.entries
39
+ "#{entries.map { |e| format_one(kind, e) }.join("\n\n")}\n"
40
+ end
41
+
42
+ # Render the non-verbal entities referenced by a concept's
43
+ # figures/tables/formulas ref collections, in deterministic order
44
+ # (figures, tables, formulas). Unknown refs are skipped silently —
45
+ # they will surface as missing anchors during Metanorma rendering.
46
+ #
47
+ # @param concept [Glossarist::ManagedConcept]
48
+ # @return [String]
49
+ def render_concept_refs(concept)
50
+ FORMATTERS.keys.filter_map do |kind|
51
+ refs = concept_refs(concept, kind)
52
+ next if refs.empty?
53
+
54
+ blocks = refs.filter_map { |ref| render_ref(kind, ref) }
55
+ next if blocks.empty?
56
+
57
+ "#{blocks.join("\n\n")}\n"
58
+ end.join("\n")
59
+ end
60
+
61
+ private
62
+
63
+ def render_ref(kind, ref)
64
+ collection = @collections[kind]
65
+ return nil unless collection
66
+
67
+ entity = collection.by_id(ref.entity_id)
68
+ return nil unless entity
69
+
70
+ format_one(kind, entity)
71
+ end
72
+
73
+ def format_one(kind, entity)
74
+ FORMATTERS.fetch(kind).new(entity, lang: @lang).to_asciidoc
75
+ end
76
+
77
+ def concept_refs(concept, kind)
78
+ refs = concept.data&.public_send(kind)
79
+ Array(refs)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Plugin
5
+ module Glossarist
6
+ # Resolves whether a concept is a member of a target section, including
7
+ # via cascading ancestor traversal (concept-model: gloss:hasChildSection
8
+ # is owl:TransitiveProperty — a concept in "3.1.1" is also in "3.1"
9
+ # and "3").
10
+ #
11
+ # Resolution order:
12
+ # 1. DatasetRegister#concept_section_ids(concept) when register is
13
+ # available — the canonical V3 path with full cascading.
14
+ # 2. Direct ConceptReference domain match (ref_type: "section") —
15
+ # legacy/fallback when no register is provided. Still applies
16
+ # cascading by walking the register's section tree if available.
17
+ class SectionCascade
18
+ SECTION_REF_TYPE = "section"
19
+
20
+ def initialize(register = nil)
21
+ @register = register
22
+ end
23
+
24
+ # True if the concept belongs to target_id or any of target_id's
25
+ # descendant sections (cascading membership).
26
+ def member?(concept, target_id)
27
+ return false unless concept&.data
28
+
29
+ if @register
30
+ cascade_member?(concept, target_id)
31
+ else
32
+ local_member?(concept, target_id)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def cascade_member?(concept, target_id)
39
+ concept_ids = @register.concept_section_ids(concept)
40
+ return false if concept_ids.nil? || concept_ids.empty?
41
+
42
+ concept_ids.any?(target_id)
43
+ end
44
+
45
+ def local_member?(concept, target_id)
46
+ Array(concept.data.domains).any? do |domain|
47
+ domain.ref_type == SECTION_REF_TYPE &&
48
+ (domain.concept_id == target_id ||
49
+ descendant_of?(domain.concept_id, target_id))
50
+ end
51
+ end
52
+
53
+ def descendant_of?(child_id, ancestor_id)
54
+ return false unless @register
55
+
56
+ ancestors = @register.section_ancestor_ids(child_id)
57
+ ancestors.include?(ancestor_id)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Plugin
5
+ module Glossarist
6
+ # Renders concepts grouped by section, with cascading membership and
7
+ # configurable sort order.
8
+ #
9
+ # Extracted from DatasetPreprocessor to keep section rendering as a
10
+ # single MECE concern: it owns section → concepts resolution, heading
11
+ # depth, and per-section rendering. Callers retain ownership of the
12
+ # rendered-concept accumulator (passed via the block) so the
13
+ # preprocessor's global state stays in one place.
14
+ class SectionRenderer
15
+ DEFAULT_SORT_BY = "term"
16
+
17
+ # @param dataset [Enumerable<ManagedConcept>] the full dataset
18
+ # @param register [Glossarist::DatasetRegister, nil] for cascading
19
+ # @param renderer [TemplateRenderer] concept renderer
20
+ # @param depth [Integer] base heading depth for sections
21
+ # @param options [Hash] :sort_by, :anchor_prefix, :non_verbal
22
+ def initialize(dataset:, register:, renderer:, depth:, **options)
23
+ @dataset = dataset
24
+ @register = register
25
+ @renderer = renderer
26
+ @depth = depth
27
+ @sort_by = options[:sort_by] || DEFAULT_SORT_BY
28
+ @anchor_prefix = options[:anchor_prefix]
29
+ @non_verbal = options[:non_verbal]
30
+ end
31
+
32
+ # @param sections [Array<Glossarist::Section>]
33
+ # @yield [Array<ManagedConcept>] concepts matched for each section
34
+ # @return [Array<String>] one rendered block per non-empty section
35
+ def render(sections)
36
+ sections.filter_map do |section|
37
+ concepts = concepts_for(section)
38
+ next if concepts.empty?
39
+
40
+ yield concepts if block_given?
41
+
42
+ block_for(section, concepts)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def concepts_for(section)
49
+ filter_options = { "section" => section.id, "sort_by" => @sort_by }
50
+ concepts = ConceptFilter.new(filter_options)
51
+ .apply(@dataset, register: @register)
52
+ concepts.select(&:default_designation)
53
+ end
54
+
55
+ def block_for(section, concepts)
56
+ heading = "#{'=' * (@depth + 1)} #{section.name || section.id}"
57
+ body = @renderer.render_concepts(concepts,
58
+ depth: @depth + 1,
59
+ anchor_prefix: @anchor_prefix,
60
+ non_verbal: @non_verbal)
61
+ "#{heading}\n\n#{body}"
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -12,13 +12,17 @@ module Metanorma
12
12
  @template_cache = {}
13
13
  end
14
14
 
15
- def render_concepts(concepts, depth:, anchor_prefix: nil)
15
+ def render_concepts(concepts, depth:, anchor_prefix: nil,
16
+ non_verbal: nil)
16
17
  tree = build_concept_tree(concepts)
17
- parts = tree.map { |c| render_tree_node(c, depth, anchor_prefix) }
18
+ parts = tree.map do |node|
19
+ render_tree_node(node, depth, anchor_prefix, non_verbal)
20
+ end
18
21
  normalize_whitespace(parts.join("\n\n"))
19
22
  end
20
23
 
21
- def render_concept(concept, depth:, anchor_prefix: nil)
24
+ def render_concept(concept, depth:, anchor_prefix: nil,
25
+ non_verbal: nil)
22
26
  l10n = concept.localization(@lang)
23
27
  context = {
24
28
  "concept" => concept.to_liquid,
@@ -28,6 +32,9 @@ module Metanorma
28
32
  }
29
33
  template_content = cached_template(concept)
30
34
  rendered = render_template(template_content, context)
35
+ if non_verbal
36
+ rendered += "\n\n#{non_verbal.render_concept_refs(concept)}"
37
+ end
31
38
  normalize_whitespace(rendered)
32
39
  end
33
40
 
@@ -41,12 +48,14 @@ module Metanorma
41
48
  end
42
49
  end
43
50
 
44
- def render_tree_node((concept, children), depth, anchor_prefix)
51
+ def render_tree_node((concept, children), depth, anchor_prefix,
52
+ non_verbal)
45
53
  result = render_concept(concept, depth: depth,
46
- anchor_prefix: anchor_prefix)
54
+ anchor_prefix: anchor_prefix,
55
+ non_verbal: non_verbal)
47
56
  children.each do |child_node|
48
57
  result += "\n" + render_tree_node(child_node, depth + 1,
49
- anchor_prefix)
58
+ anchor_prefix, non_verbal)
50
59
  end
51
60
  result
52
61
  end
@@ -3,7 +3,7 @@
3
3
  module Metanorma
4
4
  module Plugin
5
5
  module Glossarist
6
- VERSION = "0.3.8"
6
+ VERSION = "0.3.10"
7
7
  end
8
8
  end
9
9
  end
@@ -21,8 +21,14 @@ module Metanorma
21
21
  autoload :Document, "metanorma/plugin/glossarist/document"
22
22
  autoload :Liquid, "metanorma/plugin/glossarist/liquid"
23
23
  autoload :LiquidRendering, "metanorma/plugin/glossarist/liquid_rendering"
24
+ autoload :NonVerbalFormatters,
25
+ "metanorma/plugin/glossarist/non_verbal_formatters"
26
+ autoload :NonVerbalRenderer,
27
+ "metanorma/plugin/glossarist/non_verbal_renderer"
24
28
  autoload :Sanitize, "metanorma/plugin/glossarist/sanitize"
29
+ autoload :SectionCascade, "metanorma/plugin/glossarist/section_cascade"
25
30
  autoload :SectionFilter, "metanorma/plugin/glossarist/section_filter"
31
+ autoload :SectionRenderer, "metanorma/plugin/glossarist/section_renderer"
26
32
  autoload :TemplateRenderer,
27
33
  "metanorma/plugin/glossarist/template_renderer"
28
34
  end
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.required_ruby_version = ">= 3.1.0"
27
27
 
28
28
  spec.add_dependency "asciidoctor"
29
- spec.add_dependency "glossarist", "~> 2.8", ">= 2.8.7"
29
+ spec.add_dependency "glossarist", "~> 2.8", ">= 2.8.16"
30
30
  spec.add_dependency "liquid"
31
31
  spec.add_dependency "metanorma-utils"
32
32
  spec.metadata["rubygems_mfa_required"] = "true"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: metanorma-plugin-glossarist
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.8
4
+ version: 0.3.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-15 00:00:00.000000000 Z
11
+ date: 2026-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: asciidoctor
@@ -33,7 +33,7 @@ dependencies:
33
33
  version: '2.8'
34
34
  - - ">="
35
35
  - !ruby/object:Gem::Version
36
- version: 2.8.7
36
+ version: 2.8.16
37
37
  type: :runtime
38
38
  prerelease: false
39
39
  version_requirements: !ruby/object:Gem::Requirement
@@ -43,7 +43,7 @@ dependencies:
43
43
  version: '2.8'
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: 2.8.7
46
+ version: 2.8.16
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: liquid
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -111,8 +111,16 @@ files:
111
111
  - lib/metanorma/plugin/glossarist/liquid/multiply_local_file_system.rb
112
112
  - lib/metanorma/plugin/glossarist/liquid_rendering.rb
113
113
  - lib/metanorma/plugin/glossarist/liquid_templates/_concept.liquid
114
+ - lib/metanorma/plugin/glossarist/non_verbal_formatters.rb
115
+ - lib/metanorma/plugin/glossarist/non_verbal_formatters/base.rb
116
+ - lib/metanorma/plugin/glossarist/non_verbal_formatters/figure.rb
117
+ - lib/metanorma/plugin/glossarist/non_verbal_formatters/formula.rb
118
+ - lib/metanorma/plugin/glossarist/non_verbal_formatters/table.rb
119
+ - lib/metanorma/plugin/glossarist/non_verbal_renderer.rb
114
120
  - lib/metanorma/plugin/glossarist/sanitize.rb
121
+ - lib/metanorma/plugin/glossarist/section_cascade.rb
115
122
  - lib/metanorma/plugin/glossarist/section_filter.rb
123
+ - lib/metanorma/plugin/glossarist/section_renderer.rb
116
124
  - lib/metanorma/plugin/glossarist/template_renderer.rb
117
125
  - lib/metanorma/plugin/glossarist/version.rb
118
126
  - metanorma-plugin-glossarist.gemspec