metanorma-plugin-glossarist 0.3.8 → 0.3.9
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 +20 -24
- data/lib/metanorma/plugin/glossarist/dataset_registry.rb +62 -48
- data/lib/metanorma/plugin/glossarist/liquid/custom_blocks/with_glossarist_context.rb +1 -1
- data/lib/metanorma/plugin/glossarist/section_cascade.rb +62 -0
- data/lib/metanorma/plugin/glossarist/section_renderer.rb +64 -0
- data/lib/metanorma/plugin/glossarist/version.rb +1 -1
- data/lib/metanorma-plugin-glossarist.rb +2 -0
- data/metanorma-plugin-glossarist.gemspec +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c239554930b872ac67bfa16144dbc77cabd558ff26f623c0b4cdfe53d56541c1
|
|
4
|
+
data.tar.gz: 03a9282d88ea2ed4087d70bc23d4cd41d2e31327890d0c427ca48d4e403a3a65
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e1fcb83d207dfc99e857e4648cc69033c22072661d60081e9f001399666aad2534b84a9efafc95f3cdd324bcaa8446de37bb19ad2b9efb364c107a349d7cb27b
|
|
7
|
+
data.tar.gz: ebb50f86f097af9e82c93fc8f4112971ba407b66a793e9ed662ea9151eca3e91105e62fbded44a42a9d8cf86b6d738bd57e5331be84f1f2a2a657bb992cc77e7
|
|
@@ -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
|
|
@@ -172,7 +172,8 @@ module Metanorma
|
|
|
172
172
|
return unless dataset
|
|
173
173
|
|
|
174
174
|
filter_options = options.except(*RENDER_OPTIONS)
|
|
175
|
-
concepts = ConceptFilter.new(filter_options)
|
|
175
|
+
concepts = ConceptFilter.new(filter_options)
|
|
176
|
+
.apply(dataset, register: @registry.register_for(context_name))
|
|
176
177
|
concepts = concepts.select(&:default_designation)
|
|
177
178
|
@rendered_concepts.concat(concepts)
|
|
178
179
|
renderer = @renderer
|
|
@@ -188,38 +189,33 @@ module Metanorma
|
|
|
188
189
|
context_name = matches[0]
|
|
189
190
|
options = parse_options(matches[1..])
|
|
190
191
|
|
|
191
|
-
|
|
192
|
+
register = @registry.register_for(context_name)
|
|
193
|
+
sections = register&.sections
|
|
192
194
|
return unless sections && !sections.empty?
|
|
193
195
|
|
|
194
196
|
dataset = @registry.resolve_dataset(nil, context_name)
|
|
195
197
|
return unless dataset
|
|
196
198
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
+
parts = render_sections(dataset, register, sections, options)
|
|
200
|
+
liquid_doc.add_content("\n#{parts.join("\n\n")}")
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def render_sections(dataset, register, sections, options)
|
|
199
204
|
section_filter = SectionFilter.new(
|
|
200
205
|
exclude: (options["section_exclude"] || "").split("|"),
|
|
201
206
|
include: (options["section_include"] || "").split("|"),
|
|
202
207
|
)
|
|
203
208
|
filtered = section_filter.apply(sections)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
concepts = ConceptFilter.new(filter_options).apply(dataset)
|
|
214
|
-
concepts = concepts.select(&:default_designation)
|
|
215
|
-
next if concepts.empty?
|
|
216
|
-
|
|
209
|
+
renderer = SectionRenderer.new(
|
|
210
|
+
dataset: dataset,
|
|
211
|
+
register: register,
|
|
212
|
+
renderer: @renderer,
|
|
213
|
+
depth: @title_depth,
|
|
214
|
+
sort_by: options["sort_by"] || SectionRenderer::DEFAULT_SORT_BY,
|
|
215
|
+
anchor_prefix: options["anchor-prefix"],
|
|
216
|
+
)
|
|
217
|
+
renderer.render(filtered) do |concepts|
|
|
217
218
|
@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
219
|
end
|
|
224
220
|
end
|
|
225
221
|
|
|
@@ -237,7 +233,7 @@ module Metanorma
|
|
|
237
233
|
|
|
238
234
|
renderer = BibliographyRenderer.new(
|
|
239
235
|
existing_anchors: @existing_bib_anchors,
|
|
240
|
-
|
|
236
|
+
bibliography: @registry.bibliography_for(dataset_name),
|
|
241
237
|
)
|
|
242
238
|
liquid_doc.add_content(renderer.render_all(concepts))
|
|
243
239
|
end
|
|
@@ -250,7 +246,7 @@ module Metanorma
|
|
|
250
246
|
|
|
251
247
|
renderer = BibliographyRenderer.new(
|
|
252
248
|
existing_anchors: @existing_bib_anchors,
|
|
253
|
-
|
|
249
|
+
bibliography: @registry.bibliography_for(dataset_name),
|
|
254
250
|
)
|
|
255
251
|
entry = renderer.render_entry(concept)
|
|
256
252
|
liquid_doc.add_content(entry) if entry
|
|
@@ -5,92 +5,106 @@ 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, and bibliography.
|
|
12
|
+
# Each is exposed as the typed Glossarist model object so callers
|
|
13
|
+
# never poke at raw YAML hashes.
|
|
8
14
|
class DatasetRegistry
|
|
15
|
+
BIBLIOGRAPHY_FILENAME = "bibliography.yaml"
|
|
16
|
+
REGISTER_FILENAME = "register.yaml"
|
|
17
|
+
|
|
9
18
|
def initialize
|
|
10
|
-
@
|
|
11
|
-
@
|
|
12
|
-
@
|
|
13
|
-
@
|
|
19
|
+
@stores = {}
|
|
20
|
+
@registers = {}
|
|
21
|
+
@bibliographies = {}
|
|
22
|
+
@context_paths = {}
|
|
14
23
|
end
|
|
15
24
|
|
|
16
25
|
def register(document, contexts)
|
|
17
|
-
|
|
26
|
+
contexts.split(";").map do |context|
|
|
18
27
|
context_name, file_path = context.split(":", 2).map(&:strip)
|
|
19
28
|
path = relative_file_path(document, file_path)
|
|
20
|
-
@
|
|
29
|
+
@context_paths[context_name] = path
|
|
21
30
|
"#{context_name}=#{path}"
|
|
22
31
|
end
|
|
23
|
-
@context_names.concat(paths)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def load_cached(path)
|
|
27
|
-
@path_cache[path] ||= load_dataset(path)
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
def resolve_dataset(document, dataset_name)
|
|
31
|
-
|
|
32
|
-
return dataset if dataset
|
|
35
|
+
return concepts_for(dataset_name) if @context_paths.key?(dataset_name)
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
path = relative_file_path(document, dataset_name) if document
|
|
38
|
+
return unless path
|
|
35
39
|
|
|
36
|
-
path
|
|
37
|
-
@datasets[dataset_name] = load_dataset(path).to_a
|
|
40
|
+
concepts_at(path)
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
def find_concept(dataset_name, concept_name, document = nil)
|
|
41
44
|
dataset = resolve_dataset(document, dataset_name)
|
|
42
45
|
return unless dataset
|
|
43
46
|
|
|
44
|
-
dataset.find
|
|
45
|
-
concept.default_designation == concept_name
|
|
46
|
-
end
|
|
47
|
+
dataset.find { |concept| concept.default_designation == concept_name }
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
def context_path(key)
|
|
50
|
-
|
|
51
|
+
@context_paths[key]
|
|
52
|
+
end
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
# Returns the DatasetRegister for a registered context, or nil.
|
|
55
|
+
# The DatasetRegister is the single source of truth for section
|
|
56
|
+
# hierarchy and concept→section membership (cascading ancestors).
|
|
57
|
+
def register_for(context_name)
|
|
58
|
+
path = @context_paths[context_name]
|
|
59
|
+
return nil unless path
|
|
60
|
+
|
|
61
|
+
register_at(path)
|
|
57
62
|
end
|
|
58
63
|
|
|
59
64
|
def register_sections(context_name)
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
register_for(context_name)&.sections
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns the BibliographyData for a registered context, or nil.
|
|
69
|
+
# Exposed as the typed model so callers iterate entries via
|
|
70
|
+
# BibliographyEntry accessors (#id, #reference, #title, #link).
|
|
71
|
+
def bibliography_for(context_name)
|
|
72
|
+
path = @context_paths[context_name]
|
|
73
|
+
return nil unless path
|
|
62
74
|
|
|
63
|
-
|
|
64
|
-
@register_cache[dataset_path] ||=
|
|
65
|
-
::Glossarist::DatasetRegister.from_directory(dataset_path)
|
|
66
|
-
@register_cache[dataset_path]&.sections
|
|
75
|
+
bibliography_at(path)
|
|
67
76
|
end
|
|
68
77
|
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
# Returns concepts cached at an absolute path. Used by Liquid
|
|
79
|
+
# blocks that receive a pre-resolved absolute path.
|
|
80
|
+
def concepts_at(path)
|
|
81
|
+
store_for(path).concepts
|
|
71
82
|
end
|
|
72
83
|
|
|
73
84
|
private
|
|
74
85
|
|
|
75
|
-
def
|
|
76
|
-
@
|
|
77
|
-
|
|
78
|
-
collection.load_from_files(path)
|
|
79
|
-
load_bibliography_data(path)
|
|
80
|
-
collection
|
|
81
|
-
end
|
|
86
|
+
def concepts_for(context_name)
|
|
87
|
+
path = @context_paths[context_name]
|
|
88
|
+
path ? concepts_at(path) : nil
|
|
82
89
|
end
|
|
83
90
|
|
|
84
|
-
def
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
def store_for(path)
|
|
92
|
+
@stores[path] ||= begin
|
|
93
|
+
store = ::Glossarist::GlossaryStore.new
|
|
94
|
+
store.load_directory(path)
|
|
95
|
+
store
|
|
96
|
+
end
|
|
97
|
+
end
|
|
87
98
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
def register_at(path)
|
|
100
|
+
@registers[path] ||=
|
|
101
|
+
::Glossarist::DatasetRegister.from_directory(path)
|
|
102
|
+
end
|
|
91
103
|
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
def bibliography_at(path)
|
|
105
|
+
@bibliographies[path] ||= begin
|
|
106
|
+
file = File.join(path, BIBLIOGRAPHY_FILENAME)
|
|
107
|
+
::Glossarist::BibliographyData.from_file(file)
|
|
94
108
|
end
|
|
95
109
|
end
|
|
96
110
|
|
|
@@ -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,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,64 @@
|
|
|
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
|
|
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
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param sections [Array<Glossarist::Section>]
|
|
32
|
+
# @yield [Array<ManagedConcept>] concepts matched for each section
|
|
33
|
+
# @return [Array<String>] one rendered block per non-empty section
|
|
34
|
+
def render(sections)
|
|
35
|
+
sections.filter_map do |section|
|
|
36
|
+
concepts = concepts_for(section)
|
|
37
|
+
next if concepts.empty?
|
|
38
|
+
|
|
39
|
+
yield concepts if block_given?
|
|
40
|
+
|
|
41
|
+
block_for(section, concepts)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def concepts_for(section)
|
|
48
|
+
filter_options = { "section" => section.id, "sort_by" => @sort_by }
|
|
49
|
+
concepts = ConceptFilter.new(filter_options)
|
|
50
|
+
.apply(@dataset, register: @register)
|
|
51
|
+
concepts.select(&:default_designation)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def block_for(section, concepts)
|
|
55
|
+
heading = "#{'=' * (@depth + 1)} #{section.name || section.id}"
|
|
56
|
+
body = @renderer.render_concepts(concepts,
|
|
57
|
+
depth: @depth + 1,
|
|
58
|
+
anchor_prefix: @anchor_prefix)
|
|
59
|
+
"#{heading}\n\n#{body}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -22,7 +22,9 @@ module Metanorma
|
|
|
22
22
|
autoload :Liquid, "metanorma/plugin/glossarist/liquid"
|
|
23
23
|
autoload :LiquidRendering, "metanorma/plugin/glossarist/liquid_rendering"
|
|
24
24
|
autoload :Sanitize, "metanorma/plugin/glossarist/sanitize"
|
|
25
|
+
autoload :SectionCascade, "metanorma/plugin/glossarist/section_cascade"
|
|
25
26
|
autoload :SectionFilter, "metanorma/plugin/glossarist/section_filter"
|
|
27
|
+
autoload :SectionRenderer, "metanorma/plugin/glossarist/section_renderer"
|
|
26
28
|
autoload :TemplateRenderer,
|
|
27
29
|
"metanorma/plugin/glossarist/template_renderer"
|
|
28
30
|
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.9
|
|
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-18 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
|
|
@@ -112,7 +112,9 @@ files:
|
|
|
112
112
|
- lib/metanorma/plugin/glossarist/liquid_rendering.rb
|
|
113
113
|
- lib/metanorma/plugin/glossarist/liquid_templates/_concept.liquid
|
|
114
114
|
- lib/metanorma/plugin/glossarist/sanitize.rb
|
|
115
|
+
- lib/metanorma/plugin/glossarist/section_cascade.rb
|
|
115
116
|
- lib/metanorma/plugin/glossarist/section_filter.rb
|
|
117
|
+
- lib/metanorma/plugin/glossarist/section_renderer.rb
|
|
116
118
|
- lib/metanorma/plugin/glossarist/template_renderer.rb
|
|
117
119
|
- lib/metanorma/plugin/glossarist/version.rb
|
|
118
120
|
- metanorma-plugin-glossarist.gemspec
|