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 +4 -4
- data/lib/metanorma/plugin/glossarist/bibliography_renderer.rb +24 -15
- data/lib/metanorma/plugin/glossarist/concept_filter.rb +55 -33
- data/lib/metanorma/plugin/glossarist/dataset_preprocessor.rb +89 -47
- data/lib/metanorma/plugin/glossarist/dataset_registry.rb +113 -48
- data/lib/metanorma/plugin/glossarist/liquid/custom_blocks/with_glossarist_context.rb +1 -1
- data/lib/metanorma/plugin/glossarist/non_verbal_formatters/base.rb +64 -0
- data/lib/metanorma/plugin/glossarist/non_verbal_formatters/figure.rb +59 -0
- data/lib/metanorma/plugin/glossarist/non_verbal_formatters/formula.rb +31 -0
- data/lib/metanorma/plugin/glossarist/non_verbal_formatters/table.rb +49 -0
- data/lib/metanorma/plugin/glossarist/non_verbal_formatters.rb +20 -0
- data/lib/metanorma/plugin/glossarist/non_verbal_renderer.rb +84 -0
- data/lib/metanorma/plugin/glossarist/section_cascade.rb +62 -0
- data/lib/metanorma/plugin/glossarist/section_renderer.rb +66 -0
- data/lib/metanorma/plugin/glossarist/template_renderer.rb +15 -6
- data/lib/metanorma/plugin/glossarist/version.rb +1 -1
- data/lib/metanorma-plugin-glossarist.rb +6 -0
- data/metanorma-plugin-glossarist.gemspec +1 -1
- metadata +12 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e98a4e95056510d3c6071f7c46e4bd378b2898e7fa75b44e14d5ca423efc4ca
|
|
4
|
+
data.tar.gz: ebbf812935c1fb98b523675892813de831c1ec87d152d0de78d1fd9103d6342d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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: [],
|
|
18
|
+
def initialize(existing_anchors: [], bibliography: nil)
|
|
13
19
|
@rendered = {}
|
|
14
20
|
@existing_anchors = Set.new(existing_anchors)
|
|
15
|
-
@
|
|
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
|
-
|
|
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(
|
|
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
|
|
79
|
-
|
|
80
|
-
anchor = ref_id
|
|
81
|
-
@rendered[ref_id] = anchor
|
|
83
|
+
next unless bibliography_entry(ref_id)
|
|
82
84
|
|
|
83
|
-
|
|
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
|
-
|
|
89
|
-
return "* [[[#{anchor},#{ref}]]]" unless
|
|
91
|
+
entry = bibliography_entry(ref)
|
|
92
|
+
return "* [[[#{anchor},#{ref}]]]" unless entry
|
|
90
93
|
|
|
91
|
-
display_ref =
|
|
94
|
+
display_ref = entry.reference || ref
|
|
92
95
|
parts = ["* [[[#{anchor},#{display_ref}]]]"]
|
|
93
|
-
parts << ", _#{
|
|
94
|
-
parts << ". Available at: #{
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
45
|
-
collection.select do |
|
|
46
|
-
|
|
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
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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 =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
|
104
|
-
|
|
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
|
|
129
|
+
return nil unless match
|
|
108
130
|
|
|
109
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
11
|
-
@
|
|
12
|
-
@
|
|
13
|
-
@
|
|
29
|
+
@stores = {}
|
|
30
|
+
@registers = {}
|
|
31
|
+
@bibliographies = {}
|
|
32
|
+
@non_verbal = {}
|
|
33
|
+
@context_paths = {}
|
|
14
34
|
end
|
|
15
35
|
|
|
16
36
|
def register(document, contexts)
|
|
17
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
32
|
-
return dataset if dataset
|
|
46
|
+
return concepts_for(dataset_name) if @context_paths.key?(dataset_name)
|
|
33
47
|
|
|
34
|
-
|
|
48
|
+
path = relative_file_path(document, dataset_name) if document
|
|
49
|
+
return unless path
|
|
35
50
|
|
|
36
|
-
path
|
|
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
|
|
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
|
-
|
|
62
|
+
@context_paths[key]
|
|
63
|
+
end
|
|
51
64
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
permitted_classes: [Symbol, Date])
|
|
90
|
-
return unless entries.is_a?(Array)
|
|
127
|
+
private
|
|
91
128
|
|
|
92
|
-
|
|
93
|
-
|
|
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.
|
|
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
|
|
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
|
|
@@ -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.
|
|
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.
|
|
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-
|
|
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.
|
|
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.
|
|
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
|