metanorma-plugin-glossarist 0.3.3 → 0.3.6

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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -6
  3. data/.rubocop_todo.yml +55 -246
  4. data/Gemfile +6 -5
  5. data/README.adoc +11 -1
  6. data/lib/metanorma/plugin/glossarist/bibliography_renderer.rb +54 -12
  7. data/lib/metanorma/plugin/glossarist/concept_filter.rb +22 -40
  8. data/lib/metanorma/plugin/glossarist/concept_path_resolver.rb +100 -0
  9. data/lib/metanorma/plugin/glossarist/dataset_preprocessor.rb +55 -84
  10. data/lib/metanorma/plugin/glossarist/dataset_registry.rb +98 -0
  11. data/lib/metanorma/plugin/glossarist/document.rb +8 -20
  12. data/lib/metanorma/plugin/glossarist/liquid/custom_blocks/with_glossarist_context.rb +47 -61
  13. data/lib/metanorma/plugin/glossarist/liquid/custom_filters/filters.rb +10 -4
  14. data/lib/metanorma/plugin/glossarist/liquid/custom_filters.rb +14 -0
  15. data/lib/metanorma/plugin/glossarist/liquid/drop_bracket_access.rb +36 -0
  16. data/lib/metanorma/plugin/glossarist/liquid/drops/localization_collection_drop.rb +47 -0
  17. data/lib/metanorma/plugin/glossarist/liquid/drops/managed_concept_data_drop.rb +29 -0
  18. data/lib/metanorma/plugin/glossarist/liquid/drops/managed_concept_drop.rb +45 -0
  19. data/lib/metanorma/plugin/glossarist/liquid/multiply_local_file_system.rb +1 -1
  20. data/lib/metanorma/plugin/glossarist/liquid.rb +26 -0
  21. data/lib/metanorma/plugin/glossarist/liquid_rendering.rb +26 -0
  22. data/lib/metanorma/plugin/glossarist/liquid_templates/_concept.liquid +51 -0
  23. data/lib/metanorma/plugin/glossarist/sanitize.rb +6 -4
  24. data/lib/metanorma/plugin/glossarist/template_renderer.rb +113 -0
  25. data/lib/metanorma/plugin/glossarist/version.rb +1 -1
  26. data/lib/metanorma-plugin-glossarist.rb +26 -8
  27. data/metanorma-plugin-glossarist.gemspec +1 -1
  28. metadata +17 -9
  29. data/lib/metanorma/plugin/glossarist/citation_helper.rb +0 -13
  30. data/lib/metanorma/plugin/glossarist/concept_renderer.rb +0 -93
  31. data/lib/metanorma/plugin/glossarist/concept_serializer.rb +0 -27
@@ -4,11 +4,12 @@ module Metanorma
4
4
  module Plugin
5
5
  module Glossarist
6
6
  class ConceptFilter
7
- COLLECTION_FILTERS = %w[lang domain group sort_by].freeze
7
+ COLLECTION_FILTERS = %w[lang domain group section tag sort_by].freeze
8
8
  SORT_LAST = ["￿"].freeze
9
9
 
10
10
  def initialize(filters)
11
11
  @filters = filters || {}
12
+ @resolver = ConceptPathResolver.new
12
13
  end
13
14
 
14
15
  def apply(collection)
@@ -17,6 +18,8 @@ module Metanorma
17
18
  if @filters.key?("domain") || @filters.key?("group")
18
19
  result = filter_by_domain(result)
19
20
  end
21
+ result = filter_by_section(result) if @filters.key?("section")
22
+ result = filter_by_tag(result) if @filters.key?("tag")
20
23
  result = filter_by_field(result) if field_filter?
21
24
  result = sort(result) if @filters.key?("sort_by")
22
25
  result
@@ -44,6 +47,20 @@ module Metanorma
44
47
  end
45
48
  end
46
49
 
50
+ def filter_by_tag(collection)
51
+ tag = @filters["tag"]
52
+ collection.select do |c|
53
+ c.data.tags&.include?(tag)
54
+ end
55
+ end
56
+
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}" }
61
+ end
62
+ end
63
+
47
64
  def sort(collection)
48
65
  field = @filters["sort_by"]
49
66
  return collection unless field
@@ -52,14 +69,12 @@ module Metanorma
52
69
  when "term", "default_designation"
53
70
  collection.sort_by { |c| c.default_designation.to_s.downcase }
54
71
  else
55
- parts = parse_path(field)
56
- collection.sort_by { |c| sort_key(c, parts) }
72
+ collection.sort_by { |c| sort_key(c, field) }
57
73
  end
58
74
  end
59
75
 
60
- def sort_key(concept, parts)
61
- hash = ConceptSerializer.new(concept).to_h
62
- value = dig_path(hash, parts)
76
+ def sort_key(concept, field)
77
+ value = @resolver.resolve(concept, field)
63
78
  value.nil? ? SORT_LAST : natural_sort_key(value.to_s)
64
79
  end
65
80
 
@@ -75,10 +90,8 @@ module Metanorma
75
90
  start_with = path.include?(".start_with(")
76
91
  path, match_value = extract_start_with(path, value, start_with)
77
92
 
78
- parts = parse_path(path)
79
93
  collection.select do |concept|
80
- hash = ConceptSerializer.new(concept).to_h
81
- actual = dig_path(hash, parts)
94
+ actual = @resolver.resolve(concept, path)
82
95
  if start_with
83
96
  actual&.start_with?(match_value)
84
97
  else
@@ -95,37 +108,6 @@ module Metanorma
95
108
 
96
109
  [match[1], match[2]]
97
110
  end
98
-
99
- def parse_path(path)
100
- path.split(".").flat_map do |segment|
101
- if segment.include?("[")
102
- parse_indexed_segment(segment)
103
- else
104
- [segment]
105
- end
106
- end
107
- end
108
-
109
- def parse_indexed_segment(segment)
110
- field, index_part = segment.split("[", 2)
111
- index = index_part&.delete("]'\"")
112
- if index.match?(/\A\d+\z/)
113
- [field, index.to_i]
114
- else
115
- [field, index]
116
- end
117
- end
118
-
119
- def dig_path(hash, parts)
120
- parts.reduce(hash) do |current, key|
121
- case current
122
- when Hash
123
- current[key] || current[key.to_s]
124
- when Array
125
- key.is_a?(Integer) ? current[key] : nil
126
- end
127
- end
128
- end
129
111
  end
130
112
  end
131
113
  end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Plugin
5
+ module Glossarist
6
+ class ConceptPathResolver
7
+ DELEGATED_TO_DATA = %w[localizations tags].freeze
8
+ DATA_ALIASES = { "identifier" => :id }.freeze
9
+
10
+ def resolve(concept, path)
11
+ parts = parse_path(path)
12
+ value = navigate(concept, parts)
13
+ value.is_a?(String) ? value : value.to_s
14
+ end
15
+
16
+ private
17
+
18
+ def parse_path(path)
19
+ path.split(".").flat_map do |segment|
20
+ if segment.include?("[")
21
+ parse_indexed_segment(segment)
22
+ else
23
+ [segment]
24
+ end
25
+ end
26
+ end
27
+
28
+ def parse_indexed_segment(segment)
29
+ field, index_part = segment.split("[", 2)
30
+ index = index_part&.delete("]'\"")
31
+ if index.match?(/\A\d+\z/)
32
+ [field, index.to_i]
33
+ else
34
+ [field, index]
35
+ end
36
+ end
37
+
38
+ def navigate(obj, parts)
39
+ parts.reduce(obj) do |current, key|
40
+ return nil if current.nil?
41
+
42
+ access(current, key)
43
+ end
44
+ end
45
+
46
+ def access(obj, key)
47
+ case key
48
+ when String then access_string_key(obj, key)
49
+ when Integer then access_index(obj, key)
50
+ end
51
+ end
52
+
53
+ def access_string_key(obj, key)
54
+ case obj
55
+ when ::Glossarist::ManagedConcept
56
+ resolve_managed_concept(obj, key)
57
+ when ::Glossarist::ManagedConceptData
58
+ resolve_data_attribute(obj, key)
59
+ when ::Glossarist::Collections::LocalizationCollection
60
+ obj.find_by(:language_code, key)
61
+ when Array
62
+ nil
63
+ when ::Lutaml::Model::Serializable
64
+ resolve_attribute(obj, key)
65
+ when Hash
66
+ obj[key] || obj[key.to_s]
67
+ end
68
+ end
69
+
70
+ def resolve_managed_concept(concept, key)
71
+ return concept.data.public_send(key) if DELEGATED_TO_DATA.include?(key)
72
+
73
+ resolve_attribute(concept, key) { concept.public_send(key.to_sym) }
74
+ rescue NoMethodError
75
+ nil
76
+ end
77
+
78
+ def resolve_data_attribute(data, key)
79
+ aliased = DATA_ALIASES[key]
80
+ return data.public_send(aliased) if aliased && data.class.attributes.key?(aliased)
81
+
82
+ resolve_attribute(data, key)
83
+ end
84
+
85
+ def resolve_attribute(obj, key)
86
+ sym = key.to_sym
87
+ return obj.public_send(sym) if obj.class.attributes.key?(sym)
88
+
89
+ yield if block_given?
90
+ end
91
+
92
+ def access_index(obj, index)
93
+ obj[index]
94
+ rescue NoMethodError
95
+ nil
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -4,14 +4,6 @@ require "asciidoctor"
4
4
  require "asciidoctor/reader"
5
5
  require "glossarist"
6
6
 
7
- require_relative "sanitize"
8
- require_relative "concept_renderer"
9
- require_relative "bibliography_renderer"
10
- require_relative "concept_serializer"
11
- require_relative "document"
12
- require_relative "liquid/custom_filters/filters"
13
- require_relative "liquid/custom_blocks/with_glossarist_context"
14
-
15
7
  module Metanorma
16
8
  module Plugin
17
9
  module Glossarist
@@ -23,19 +15,22 @@ module Metanorma
23
15
  BIBLIOGRAPHY_REGEX = /^glossarist::render_bibliography\[(.*?)\]$/m
24
16
  BIBLIOGRAPHY_ENTRY_REGEX = /^glossarist::render_bibliography_entry\[(.*?)\]$/m
25
17
 
18
+ BIB_ANCHOR_REGEX = /^\*\s*\[\[\[([^,]+)/
19
+
26
20
  def initialize(config = {})
27
21
  super
28
22
  @config = config
29
- @datasets = {}
30
- @title_depth = 2
31
- @bibliography_renderer = BibliographyRenderer.new
32
- @seen_glossarist = false
33
- @context_names = []
34
23
  end
35
24
 
36
25
  def process(document, reader)
37
26
  input_lines = reader.lines.to_enum
38
27
  @config[:file_system] = relative_file_path(document, "")
28
+ @registry = DatasetRegistry.new
29
+ @rendered_concepts = []
30
+ @title_depth = 2
31
+ @existing_bib_anchors = []
32
+ @seen_glossarist = false
33
+
39
34
  processed_doc = prepare_document(document, input_lines)
40
35
  log(document, processed_doc.to_s) if @seen_glossarist
41
36
  Asciidoctor::PreprocessorReader.new(document,
@@ -57,6 +52,7 @@ module Metanorma
57
52
  skip_dataset: false)
58
53
  liquid_doc = Document.new
59
54
  liquid_doc.file_system = @config[:file_system]
55
+ liquid_doc.registry = @registry
60
56
 
61
57
  loop do
62
58
  current_line = input_lines.next
@@ -88,14 +84,16 @@ module Metanorma
88
84
  @title_depth = current_line.sub(/ .*$/,
89
85
  "").size
90
86
  end
87
+ if (match = current_line.match(BIB_ANCHOR_REGEX))
88
+ @existing_bib_anchors << match[1]
89
+ end
91
90
  liquid_doc.add_content(current_line)
92
91
  end
93
92
  end
94
93
 
95
94
  def process_dataset_tag(document, input_lines, liquid_doc, match)
96
95
  @seen_glossarist = true
97
- @context_names << prepare_dataset_contexts(document, match[1])
98
- @context_names.flatten!
96
+ @registry.register(document, match[1])
99
97
  liquid_doc.add_content(prepare_document(document, input_lines).to_s,
100
98
  render: false)
101
99
  end
@@ -138,16 +136,7 @@ module Metanorma
138
136
  end
139
137
 
140
138
  def get_context_path(document, key)
141
- if @context_names && !@context_names.empty?
142
- context_names = @context_names.map(&:strip)
143
- found = context_names.find do |context|
144
- context_name, = context.split("=")
145
- context_name == key
146
- end
147
- return found.split("=").last.strip if found
148
- end
149
-
150
- relative_file_path(document, key)
139
+ @registry.context_path(key) || relative_file_path(document, key)
151
140
  end
152
141
 
153
142
  def process_render_tag(liquid_doc, match)
@@ -157,92 +146,74 @@ module Metanorma
157
146
  concept_name = matches[1]
158
147
  options = parse_options(matches[2..])
159
148
 
160
- concept = find_concept(context_name, concept_name)
149
+ concept = @registry.find_concept(context_name, concept_name)
161
150
  return unless concept
162
151
 
163
- renderer = ConceptRenderer.new(concept,
164
- depth: @title_depth,
165
- anchor_prefix: options["anchor-prefix"])
166
- liquid_doc.add_content(renderer.render)
152
+ @rendered_concepts << concept
153
+ renderer = TemplateRenderer.new(file_system: @config[:file_system])
154
+ rendered = renderer.render_concept(concept,
155
+ depth: @title_depth,
156
+ anchor_prefix: options["anchor-prefix"])
157
+ liquid_doc.add_content("\n#{rendered}")
167
158
  end
168
159
 
160
+ RENDER_OPTIONS = %w[anchor-prefix].freeze
161
+
169
162
  def process_import_tag(liquid_doc, match)
170
163
  @seen_glossarist = true
171
164
  matches = match[1].split(",").map(&:strip)
172
165
  context_name = matches[0]
173
166
  options = parse_options(matches[1..])
174
- dataset = @datasets[context_name.strip]
167
+ dataset = @registry.resolve_dataset(nil, context_name)
175
168
  return unless dataset
176
169
 
177
- rendered = dataset.filter_map do |concept|
178
- designation = concept.default_designation
179
- next unless designation
180
-
181
- renderer = ConceptRenderer.new(concept,
182
- depth: @title_depth,
183
- anchor_prefix: options["anchor-prefix"])
184
- renderer.render
185
- end.join("\n\n")
186
-
187
- liquid_doc.add_content(rendered)
170
+ filter_options = options.except(*RENDER_OPTIONS)
171
+ concepts = ConceptFilter.new(filter_options).apply(dataset)
172
+ concepts = concepts.select(&:default_designation)
173
+ @rendered_concepts.concat(concepts)
174
+ renderer = TemplateRenderer.new(file_system: @config[:file_system])
175
+ rendered = renderer.render_concepts(concepts,
176
+ depth: @title_depth,
177
+ anchor_prefix: options["anchor-prefix"])
178
+ liquid_doc.add_content("\n#{rendered}")
188
179
  end
189
180
 
190
181
  def process_bibliography(document, liquid_doc, match)
191
182
  @seen_glossarist = true
192
183
  dataset_name = match[1].strip
193
- dataset = resolve_dataset(document, dataset_name)
194
- return unless dataset
195
-
196
- liquid_doc.add_content(@bibliography_renderer.render_all(dataset))
184
+ concepts = if @rendered_concepts.empty?
185
+ @registry.resolve_dataset(
186
+ document, dataset_name
187
+ )
188
+ else
189
+ @rendered_concepts
190
+ end
191
+ return unless concepts && !concepts.empty?
192
+
193
+ renderer = BibliographyRenderer.new(
194
+ existing_anchors: @existing_bib_anchors,
195
+ bibliography_data: @registry.bibliography_data,
196
+ )
197
+ liquid_doc.add_content(renderer.render_all(concepts))
197
198
  end
198
199
 
199
200
  def process_bibliography_entry(document, liquid_doc, match)
200
201
  @seen_glossarist = true
201
202
  dataset_name, concept_name = match[1].split(",").map(&:strip)
202
- concept = find_concept(dataset_name, concept_name, document)
203
+ concept = @registry.find_concept(dataset_name, concept_name, document)
203
204
  return unless concept
204
205
 
205
- entry = @bibliography_renderer.render_entry(concept)
206
+ renderer = BibliographyRenderer.new(
207
+ existing_anchors: @existing_bib_anchors,
208
+ bibliography_data: @registry.bibliography_data,
209
+ )
210
+ entry = renderer.render_entry(concept)
206
211
  liquid_doc.add_content(entry) if entry
207
212
  end
208
213
 
209
- def prepare_dataset_contexts(document, contexts)
210
- contexts.split(";").map do |context|
211
- context_name, file_path = context.split(":").map(&:strip)
212
- path = relative_file_path(document, file_path)
213
- dataset = load_dataset(path)
214
- @datasets[context_name] = dataset.to_a
215
- "#{context_name}=#{path}"
216
- end
217
- end
218
-
219
- def load_dataset(path)
220
- collection = ::Glossarist::ManagedConceptCollection.new
221
- collection.load_from_files(path)
222
- collection
223
- end
224
-
225
- def find_concept(dataset_name, concept_name, document = nil)
226
- dataset = resolve_dataset(document, dataset_name)
227
- return unless dataset
228
-
229
- dataset.find do |concept|
230
- concept.default_designation == concept_name
231
- end
232
- end
233
-
234
- def resolve_dataset(document, dataset_name)
235
- dataset = @datasets[dataset_name]
236
- return dataset if dataset
237
-
238
- return unless document
239
-
240
- path = relative_file_path(document, dataset_name)
241
- collection = load_dataset(path)
242
- @datasets[dataset_name] = collection.to_a
243
- end
244
-
245
214
  def relative_file_path(document, file_path)
215
+ return file_path if File.absolute_path?(file_path)
216
+
246
217
  docfile_directory = File.dirname(
247
218
  document.attributes["docfile"] || ".",
248
219
  )
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "glossarist"
4
+
5
+ module Metanorma
6
+ module Plugin
7
+ module Glossarist
8
+ class DatasetRegistry
9
+ def initialize
10
+ @datasets = {}
11
+ @path_cache = {}
12
+ @bibliography_data = {}
13
+ @context_names = []
14
+ end
15
+
16
+ def register(document, contexts)
17
+ paths = contexts.split(";").map do |context|
18
+ context_name, file_path = context.split(":", 2).map(&:strip)
19
+ path = relative_file_path(document, file_path)
20
+ @datasets[context_name] = load_dataset(path).to_a
21
+ "#{context_name}=#{path}"
22
+ end
23
+ @context_names.concat(paths)
24
+ end
25
+
26
+ def load_cached(path)
27
+ @path_cache[path] ||= load_dataset(path)
28
+ end
29
+
30
+ def resolve_dataset(document, dataset_name)
31
+ dataset = @datasets[dataset_name]
32
+ return dataset if dataset
33
+
34
+ return unless document
35
+
36
+ path = relative_file_path(document, dataset_name)
37
+ @datasets[dataset_name] = load_dataset(path).to_a
38
+ end
39
+
40
+ def find_concept(dataset_name, concept_name, document = nil)
41
+ dataset = resolve_dataset(document, dataset_name)
42
+ return unless dataset
43
+
44
+ dataset.find do |concept|
45
+ concept.default_designation == concept_name
46
+ end
47
+ end
48
+
49
+ def context_path(key)
50
+ return nil if @context_names.empty?
51
+
52
+ found = @context_names.find do |context|
53
+ context_name, = context.split("=")
54
+ context_name.strip == key
55
+ end
56
+ found&.split("=")&.last&.strip
57
+ end
58
+
59
+ def bibliography_data
60
+ @bibliography_data
61
+ end
62
+
63
+ private
64
+
65
+ def load_dataset(path)
66
+ @path_cache[path] ||= begin
67
+ collection = ::Glossarist::ManagedConceptCollection.new
68
+ collection.load_from_files(path)
69
+ load_bibliography_data(path)
70
+ collection
71
+ end
72
+ end
73
+
74
+ def load_bibliography_data(dataset_path)
75
+ bib_path = File.join(dataset_path, "bibliography.yaml")
76
+ return unless File.exist?(bib_path)
77
+
78
+ entries = YAML.safe_load_file(bib_path,
79
+ permitted_classes: [Symbol, Date])
80
+ return unless entries.is_a?(Array)
81
+
82
+ @bibliography_data = entries.each_with_object({}) do |entry, hash|
83
+ hash[entry["id"]] = entry if entry["id"]
84
+ end
85
+ end
86
+
87
+ def relative_file_path(document, file_path)
88
+ return file_path if File.absolute_path?(file_path)
89
+
90
+ docfile_directory = File.dirname(
91
+ document.attributes["docfile"] || ".",
92
+ )
93
+ document.path_resolver.system_path(file_path, docfile_directory)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -1,13 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "liquid"
4
- require_relative "liquid/multiply_local_file_system"
5
-
6
3
  module Metanorma
7
4
  module Plugin
8
5
  module Glossarist
9
6
  class Document
10
- attr_accessor :content, :file_system
7
+ attr_accessor :file_system, :registry
11
8
 
12
9
  def initialize
13
10
  @content = []
@@ -15,7 +12,13 @@ module Metanorma
15
12
 
16
13
  def add_content(content, options = {})
17
14
  @content << if options[:render]
18
- render_liquid(content, options)
15
+ LiquidRendering.render(
16
+ content,
17
+ include_paths: [file_system,
18
+ options[:template]].compact,
19
+ patterns: LiquidRendering::DOCUMENT_PATTERNS,
20
+ registry: registry,
21
+ )
19
22
  else
20
23
  content
21
24
  end
@@ -24,21 +27,6 @@ module Metanorma
24
27
  def to_s
25
28
  @content.compact.join("\n")
26
29
  end
27
-
28
- private
29
-
30
- def render_liquid(file_content, options = {})
31
- include_paths = [file_system, options[:template]].compact
32
- template = ::Liquid::Template.parse(file_content)
33
- template.registers[:file_system] = ::Metanorma::Plugin::Glossarist::Liquid::LocalFileSystem.new(
34
- include_paths, ["%s.liquid", "_%s.liquid", "_%s.adoc"]
35
- )
36
- rendered = template.render
37
-
38
- return rendered unless template.errors.any?
39
-
40
- raise template.errors.first.cause
41
- end
42
30
  end
43
31
  end
44
32
  end