metanorma-plugin-lutaml 0.7.37 → 0.7.39

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake.yml +5 -1
  3. data/.gitignore +2 -1
  4. data/.rubocop.yml +11 -2
  5. data/.rubocop_todo.yml +238 -0
  6. data/CLAUDE.md +78 -0
  7. data/Gemfile +8 -12
  8. data/Gemfile.devel +2 -0
  9. data/Rakefile +17 -0
  10. data/docs/usages/enterprise_architect.adoc +17 -0
  11. data/docs/usages/express.adoc +55 -0
  12. data/lib/metanorma/plugin/lutaml/base_structured_text_preprocessor.rb +6 -6
  13. data/lib/metanorma/plugin/lutaml/data2_text_preprocessor.rb +1 -0
  14. data/lib/metanorma/plugin/lutaml/express_remarks_decorator.rb +2 -2
  15. data/lib/metanorma/plugin/lutaml/json2_text_preprocessor.rb +1 -0
  16. data/lib/metanorma/plugin/lutaml/liquid/multiply_local_file_system.rb +2 -2
  17. data/lib/metanorma/plugin/lutaml/liquid_templates/_klass_table.liquid +6 -6
  18. data/lib/metanorma/plugin/lutaml/lutaml_ea_diagram_block_macro.rb +1 -1
  19. data/lib/metanorma/plugin/lutaml/lutaml_ea_xmi_base.rb +164 -15
  20. data/lib/metanorma/plugin/lutaml/lutaml_ea_xmi_preprocessor.rb +2 -2
  21. data/lib/metanorma/plugin/lutaml/lutaml_enum_table_block_macro.rb +8 -42
  22. data/lib/metanorma/plugin/lutaml/lutaml_gml_dictionary_block.rb +1 -0
  23. data/lib/metanorma/plugin/lutaml/lutaml_klass_table_block_macro.rb +9 -42
  24. data/lib/metanorma/plugin/lutaml/lutaml_preprocessor.rb +8 -6
  25. data/lib/metanorma/plugin/lutaml/lutaml_table_inline_macro.rb +1 -0
  26. data/lib/metanorma/plugin/lutaml/lutaml_uml_datamodel_description_preprocessor.rb +2 -2
  27. data/lib/metanorma/plugin/lutaml/lutaml_xmi_uml_preprocessor.rb +2 -2
  28. data/lib/metanorma/plugin/lutaml/source_extractor.rb +3 -3
  29. data/lib/metanorma/plugin/lutaml/utils.rb +52 -8
  30. data/lib/metanorma/plugin/lutaml/version.rb +1 -1
  31. data/lib/metanorma/plugin/lutaml/yaml2_text_preprocessor.rb +1 -0
  32. data/metanorma-plugin-lutaml.gemspec +4 -4
  33. metadata +25 -11
  34. data/.hound.yml +0 -5
@@ -40,7 +40,7 @@ module Metanorma
40
40
  (?<config_path>.+) # Capture config path
41
41
  )? # End of optional group
42
42
  $ # End of the pattern
43
- }x.freeze
43
+ }x
44
44
 
45
45
  # search document for block `lutaml_ea_xmi`
46
46
  # or `lutaml_uml_datamodel_description`
@@ -63,16 +63,22 @@ module Metanorma
63
63
  end
64
64
 
65
65
  yaml_config.ea_extension&.each do |ea_extension_path|
66
- # resolve paths of ea extensions based on the location of
67
- # config yaml file
68
66
  ea_extension_full_path = File.expand_path(
69
67
  ea_extension_path, File.dirname(yaml_config_path)
70
68
  )
71
- Xmi::EaRoot.load_extension(ea_extension_full_path)
69
+ unless Xmi::EaRoot.loaded_extensions.value?(ea_extension_full_path)
70
+ Xmi::EaRoot.load_extension(ea_extension_full_path)
71
+ end
72
72
  end
73
73
 
74
74
  guidance = get_guidance(document, yaml_config.guidance)
75
- result_document = parse_result_document(full_path, guidance)
75
+ cache_key = [full_path, guidance]
76
+
77
+ @@lutaml_doc_cache ||= {}
78
+ result_document = @@lutaml_doc_cache[cache_key] ||= parse_result_document(
79
+ full_path, guidance
80
+ )
81
+
76
82
  document.attributes["lutaml_xmi_cache"] ||= {}
77
83
  document.attributes["lutaml_xmi_cache"][full_path] = result_document
78
84
  result_document
@@ -203,8 +209,8 @@ module Metanorma
203
209
  all_children_packages = lutaml_document.packages
204
210
  .map(&:children_packages).flatten
205
211
  package_flat_packages = lambda do |pks|
206
- pks.each_with_object({}) do |package, res|
207
- res[package.name] = package.xmi_id
212
+ pks.to_h do |package|
213
+ [package.name, package.xmi_id]
208
214
  end
209
215
  end
210
216
  children_pks = package_flat_packages.call(all_children_packages)
@@ -292,7 +298,7 @@ module Metanorma
292
298
  def package_level(lutaml_document, level)
293
299
  return lutaml_document if level <= 0
294
300
 
295
- package_level(lutaml_document["packages"].first, level - 1)
301
+ package_level(lutaml_document.packages.first, level - 1)
296
302
  end
297
303
 
298
304
  def create_context_object( # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
@@ -307,12 +313,12 @@ module Metanorma
307
313
 
308
314
  if options.packages.nil?
309
315
  contexts[context_name]["render_nested_packages"] = true
310
- contexts[context_name]["packages"] = root_package["packages"]
316
+ contexts[context_name]["packages"] = root_package.packages
311
317
 
312
318
  return contexts
313
319
  end
314
320
 
315
- all_packages = [root_package, *root_package["children_packages"]]
321
+ all_packages = [root_package, *root_package.children_packages]
316
322
  contexts[context_name].merge!(
317
323
  {
318
324
  "packages" => sort_and_filter_out_packages(all_packages, options),
@@ -330,7 +336,7 @@ module Metanorma
330
336
  )
331
337
  contexts = {}
332
338
  contexts[context_name] = {
333
- "name" => root_package["name"],
339
+ "name" => root_package.name,
334
340
  "root_packages" => [root_package],
335
341
  "additional_context" => additional_context
336
342
  .merge("external_classes" => options.external_classes),
@@ -346,7 +352,7 @@ module Metanorma
346
352
  result = {}
347
353
  packages = options.packages.reject { |p| p.send(key.to_sym).nil? }
348
354
  packages.each do |p|
349
- result[p.name] = p.send(key.to_sym).map { |n| [n, true] }.to_h
355
+ result[p.name] = p.send(key.to_sym).to_h { |n| [n, true] }
350
356
  end
351
357
  result
352
358
  end
@@ -370,7 +376,7 @@ module Metanorma
370
376
  options.skip.each do |skip_package|
371
377
  entity_regexp = config_entity_regexp(skip_package)
372
378
  all_packages.delete_if do |package|
373
- package["name"] =~ entity_regexp
379
+ package.name =~ entity_regexp
374
380
  end
375
381
  end
376
382
 
@@ -381,10 +387,10 @@ module Metanorma
381
387
  options.packages.each do |package|
382
388
  entity_regexp = config_entity_regexp(package.name)
383
389
  all_packages.each do |p|
384
- if p["name"]&.match?(entity_regexp)
390
+ if p.name&.match?(entity_regexp)
385
391
  result.push(p)
386
392
  all_packages.delete_if do |nest_package|
387
- nest_package["name"] == p["name"]
393
+ nest_package.name == p.name
388
394
  end
389
395
  end
390
396
  end
@@ -434,6 +440,149 @@ module Metanorma
434
440
  {% include "#{include_name}", depth: #{section_depth}, package_skip_sections: context.package_skip_sections, package_entities: context.package_entities, context: context, additional_context: context.additional_context, render_nested_packages: context.render_nested_packages %}
435
441
  LIQUID
436
442
  end
443
+
444
+ def render_table(context, context_name, parent, attrs) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
445
+ table_tmpl = get_template(parent.document, attrs)
446
+ table_tmpl.assigns[context_name] = context
447
+
448
+ if attrs["external_data"]
449
+ data_array = attrs["external_data"].split(";")
450
+ data_array.each do |data_item|
451
+ context_name, external_data_path = data_item.split(":")
452
+ external_data = content_from_file(
453
+ parent.document, external_data_path.strip
454
+ )
455
+ table_tmpl.assigns[context_name.strip] = external_data
456
+ end
457
+ end
458
+
459
+ rendered_table = table_tmpl.render
460
+ block = create_open_block(parent, "", attrs)
461
+ parse_content(block, rendered_table, attrs)
462
+ end
463
+
464
+ def get_template(document, attrs)
465
+ template = get_default_template
466
+ template = attrs["template"] if attrs["template"]
467
+
468
+ rel_tmpl_path = Utils.relative_file_path(
469
+ document, template
470
+ )
471
+
472
+ ::Liquid::Template.parse(File.read(rel_tmpl_path))
473
+ end
474
+
475
+ def get_name_path(attrs)
476
+ return attrs["path"] if attrs["path"]
477
+
478
+ return "#{attrs['package']}::#{attrs['name']}" if attrs["package"]
479
+
480
+ attrs["name"]
481
+ end
482
+
483
+ # The class methods `serialize_generalization_by_name` and
484
+ # `serialize_enumeration_by_name` were removed from
485
+ # `Lutaml::Xmi::Parsers::Xml` in lutaml 0.10. The replacements below
486
+ # rebuild the same shape of result by reusing the still-available
487
+ # path-aware finders on the parser instance and the new
488
+ # `XmiLookupService`, then bridging to the UML object that the
489
+ # current `KlassDrop` / `EnumDrop` constructors expect.
490
+ def serialize_klass_drop_by_name(xmi_path, name, document = nil,
491
+ guidance = nil)
492
+ parser, uml_doc = build_uml_document(xmi_path, document)
493
+ raw_klass = find_packaged_klass(parser.xmi_index, name)
494
+ warn "Class not found for name: #{name}" if raw_klass.nil?
495
+ klass = raw_klass && find_uml_node_by_xmi_id(
496
+ uml_doc, raw_klass.id, :classes
497
+ )
498
+ ::Lutaml::Xmi::LiquidDrops::KlassDrop.new(
499
+ klass, guidance, build_drop_options(parser)
500
+ )
501
+ end
502
+
503
+ def serialize_enum_drop_by_name(xmi_path, name, document = nil)
504
+ parser, uml_doc = build_uml_document(xmi_path, document)
505
+ raw_enum = find_packaged_enum(parser.xmi_index, name)
506
+ warn "Enumeration not found for name: #{name}" if raw_enum.nil?
507
+ enum = raw_enum && find_uml_node_by_xmi_id(
508
+ uml_doc, raw_enum.id, :enums
509
+ )
510
+ ::Lutaml::Xmi::LiquidDrops::EnumDrop.new(
511
+ enum, build_drop_options(parser)
512
+ )
513
+ end
514
+
515
+ def build_uml_document(xmi_path, _document = nil)
516
+ @@uml_doc_cache ||= {}
517
+ if @@uml_doc_cache[xmi_path]
518
+ return @@uml_doc_cache[xmi_path]
519
+ end
520
+
521
+ xmi_model = ::Xmi::Sparx::Root.parse_xml(File.read(xmi_path))
522
+ parser = ::Lutaml::Xmi::Parsers::Xml.new
523
+ result = [parser, parser.parse(xmi_model)]
524
+ @@uml_doc_cache[xmi_path] = result
525
+ result
526
+ end
527
+
528
+ def build_drop_options(parser)
529
+ lookup = ::Lutaml::Xmi::XmiLookupService.new(
530
+ parser.xmi_root_model, parser.id_name_mapping
531
+ )
532
+ {
533
+ xmi_root_model: parser.xmi_root_model,
534
+ id_name_mapping: parser.id_name_mapping,
535
+ lookup: lookup,
536
+ with_gen: true,
537
+ with_absolute_path: true,
538
+ }
539
+ end
540
+
541
+ def find_uml_node_by_xmi_id(container, xmi_id, collection)
542
+ found = container.public_send(collection)
543
+ .find { |node| node.xmi_id == xmi_id }
544
+ return found if found
545
+
546
+ container.packages.each do |pkg|
547
+ nested = find_uml_node_by_xmi_id(pkg, xmi_id, collection)
548
+ return nested if nested
549
+ end
550
+ nil
551
+ end
552
+
553
+ def find_packaged_klass(index, path)
554
+ segments = path.split("::")
555
+ if segments.one?
556
+ index.find_packaged_by_name_and_types(
557
+ path, ["uml:Class", "uml:AssociationClass"]
558
+ )
559
+ else
560
+ find_packaged_klass_by_path(index, segments)
561
+ end
562
+ end
563
+
564
+ def find_packaged_klass_by_path(index, segments)
565
+ klass_name = segments.pop
566
+ klass = index.find_packaged_by_name_and_types(
567
+ klass_name, ["uml:Class", "uml:AssociationClass"]
568
+ )
569
+ return unless klass
570
+
571
+ # Verify the path by walking up the parent chain
572
+ current = klass
573
+ segments.reverse_each do |pkg_name|
574
+ parent = index.find_parent(current.id)
575
+ return unless parent && parent.name == pkg_name
576
+
577
+ current = parent
578
+ end
579
+ klass
580
+ end
581
+
582
+ def find_packaged_enum(index, name)
583
+ index.packaged_elements_of_type("uml:Enumeration")
584
+ .find { |e| e.name == name }
585
+ end
437
586
  end
438
587
  end
439
588
  end
@@ -20,12 +20,12 @@ module Metanorma
20
20
  include LutamlEaXmiBase
21
21
 
22
22
  MACRO_REGEXP =
23
- /\[lutaml_ea_xmi,([^,]+),?(.+)?\]/.freeze
23
+ /\[lutaml_ea_xmi,([^,]+),?(.+)?\]/
24
24
 
25
25
  private
26
26
 
27
27
  def parse_result_document(full_path, guidance)
28
- ::Lutaml::XMI::Parsers::XML.serialize_xmi_to_liquid(
28
+ ::Lutaml::Xmi::Parsers::Xml.serialize_xmi_to_liquid(
29
29
  File.new(full_path, encoding: "UTF-8"),
30
30
  guidance,
31
31
  )
@@ -6,6 +6,7 @@ module Metanorma
6
6
  class LutamlEnumTableBlockMacro <
7
7
  ::Asciidoctor::Extensions::BlockMacroProcessor
8
8
  include LutamlEaXmiBase
9
+ include Content
9
10
 
10
11
  DEFAULT_TEMPLATE = File.join(
11
12
  Gem::Specification.find_by_name("metanorma-plugin-lutaml").gem_dir,
@@ -13,59 +14,24 @@ module Metanorma
13
14
  "_enum_table.liquid"
14
15
  )
15
16
 
17
+ CONTEXT_NAME = "enum"
18
+
16
19
  use_dsl
17
20
  named :lutaml_enum_table
18
21
 
19
22
  def process(parent, target, attrs) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
20
23
  xmi_path = get_xmi_path(parent, target, attrs)
24
+ path = get_name_path(attrs)
21
25
 
22
- if attrs["template"]
23
- attrs["template"] = Utils.relative_file_path(
24
- parent.document, attrs["template"]
25
- )
26
- end
27
-
28
- path = if !attrs["path"].nil?
29
- attrs["path"]
30
- elsif !attrs["package"].nil? && !attrs["name"].nil?
31
- "#{attrs['package']}::#{attrs['name']}"
32
- else
33
- attrs["name"]
34
- end
35
-
36
- enum = ::Lutaml::XMI::Parsers::XML.serialize_enumeration_by_name(
37
- xmi_path, path
38
- )
26
+ enum = serialize_enum_drop_by_name(xmi_path, path, parent.document)
39
27
 
40
- render(enum, parent, attrs)
28
+ render_table(enum, CONTEXT_NAME, parent, attrs)
41
29
  end
42
30
 
43
31
  private
44
32
 
45
- def render(enum, parent, attrs)
46
- rendered_table = render_table(enum, parent, attrs)
47
-
48
- block = create_open_block(parent, "", attrs)
49
- parse_content(block, rendered_table, attrs)
50
- end
51
-
52
- def render_table(enum, parent, attrs)
53
- table_tmpl = get_template(parent.document, attrs)
54
- table_tmpl.assigns["enum"] = enum
55
- table_tmpl.render
56
- end
57
-
58
- def get_template(document, attrs)
59
- template = DEFAULT_TEMPLATE
60
- if attrs["template"]
61
- template = attrs["template"]
62
- end
63
-
64
- rel_tmpl_path = Utils.relative_file_path(
65
- document, template
66
- )
67
-
68
- ::Liquid::Template.parse(File.read(rel_tmpl_path))
33
+ def get_default_template
34
+ DEFAULT_TEMPLATE
69
35
  end
70
36
  end
71
37
  end
@@ -5,6 +5,7 @@ module Metanorma
5
5
  module Lutaml
6
6
  class LutamlGmlDictionaryBlock < ::Asciidoctor::Extensions::BlockProcessor
7
7
  include LutamlGmlDictionaryBase
8
+
8
9
  use_dsl
9
10
  named :lutaml_gml_dictionary
10
11
  on_context :open
@@ -6,6 +6,7 @@ module Metanorma
6
6
  class LutamlKlassTableBlockMacro <
7
7
  ::Asciidoctor::Extensions::BlockMacroProcessor
8
8
  include LutamlEaXmiBase
9
+ include Content
9
10
 
10
11
  DEFAULT_TEMPLATE = File.join(
11
12
  Gem::Specification.find_by_name("metanorma-plugin-lutaml").gem_dir,
@@ -13,64 +14,30 @@ module Metanorma
13
14
  "_klass_table.liquid"
14
15
  )
15
16
 
17
+ CONTEXT_NAME = "klass"
18
+
16
19
  use_dsl
17
20
  named :lutaml_klass_table
18
21
 
19
22
  def process(parent, target, attrs) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
20
23
  xmi_path = get_xmi_path(parent, target, attrs)
21
-
22
- if attrs["template"]
23
- attrs["template"] = Utils.relative_file_path(
24
- parent.document, attrs["template"]
25
- )
26
- end
24
+ path = get_name_path(attrs)
27
25
 
28
26
  guidance = nil
29
27
  if attrs["guidance"]
30
28
  guidance = get_guidance(parent.document, attrs["guidance"])
31
29
  end
32
30
 
33
- path = if !attrs["path"].nil?
34
- attrs["path"]
35
- elsif !attrs["package"].nil? && !attrs["name"].nil?
36
- "#{attrs['package']}::#{attrs['name']}"
37
- else
38
- attrs["name"]
39
- end
31
+ klass = serialize_klass_drop_by_name(xmi_path, path, parent.document,
32
+ guidance)
40
33
 
41
- klass = ::Lutaml::XMI::Parsers::XML.serialize_generalization_by_name(
42
- xmi_path, path, guidance
43
- )
44
-
45
- render(klass, parent, attrs)
34
+ render_table(klass, CONTEXT_NAME, parent, attrs)
46
35
  end
47
36
 
48
37
  private
49
38
 
50
- def render(klass, parent, attrs)
51
- rendered_table = render_table(klass, parent, attrs)
52
-
53
- block = create_open_block(parent, "", attrs)
54
- parse_content(block, rendered_table, attrs)
55
- end
56
-
57
- def render_table(klass, parent, attrs)
58
- table_tmpl = get_template(parent.document, attrs)
59
- table_tmpl.assigns["klass"] = klass
60
- table_tmpl.render
61
- end
62
-
63
- def get_template(document, attrs)
64
- template = DEFAULT_TEMPLATE
65
- if attrs["template"]
66
- template = attrs["template"]
67
- end
68
-
69
- rel_tmpl_path = Utils.relative_file_path(
70
- document, template
71
- )
72
-
73
- ::Liquid::Template.parse(File.read(rel_tmpl_path))
39
+ def get_default_template
40
+ DEFAULT_TEMPLATE
74
41
  end
75
42
  end
76
43
  end
@@ -13,6 +13,8 @@ module Metanorma
13
13
  module Lutaml
14
14
  # Class for processing Lutaml files
15
15
  class LutamlPreprocessor < ::Asciidoctor::Extensions::Preprocessor
16
+ include Utils
17
+
16
18
  REMARKS_ATTRIBUTE = "remarks"
17
19
  EXPRESS_PREPROCESSOR_REGEX = %r{
18
20
  ^ # Start of line
@@ -26,7 +28,7 @@ module Metanorma
26
28
  (?<context_name>[^,]+)? # Optional context name
27
29
  (?<options>,.*)? # Optional options
28
30
  \] # Closing bracket
29
- }x.freeze
31
+ }x
30
32
 
31
33
  def process(document, reader) # rubocop:disable Metrics/MethodLength
32
34
  r = Asciidoctor::PreprocessorNoIfdefsReader.new(document,
@@ -83,8 +85,8 @@ module Metanorma
83
85
  index_names = block_header_match[:index_names].split(";").map(&:strip)
84
86
  context_name = block_header_match[:context_name].strip
85
87
 
86
- options = block_header_match[:options] &&
87
- parse_options(block_header_match[:options].to_s.strip) || {}
88
+ options = (block_header_match[:options] &&
89
+ parse_options(block_header_match[:options].to_s.strip)) || {}
88
90
 
89
91
  end_mark = input_lines.next
90
92
 
@@ -225,7 +227,8 @@ module Metanorma
225
227
  .new(include_paths, ["%s.liquid", "_%s.liquid", "_%s.adoc"])
226
228
 
227
229
  # Parse template once outside the loop
228
- template = ::Liquid::Template.parse(lines.join("\n"))
230
+ template = ::Liquid::Template
231
+ .parse(lines.join("\n"), environment: create_liquid_environment)
229
232
  template.registers[:file_system] = file_system
230
233
 
231
234
  # Render for each item
@@ -274,8 +277,7 @@ module Metanorma
274
277
  options_string
275
278
  .to_s
276
279
  .scan(/,\s*([^=]+?)=(\s*[^,]+)/)
277
- .map { |elem| elem.map(&:strip) }
278
- .to_h
280
+ .to_h { |elem| elem.map(&:strip) }
279
281
  end
280
282
  end
281
283
  end
@@ -6,6 +6,7 @@ module Metanorma
6
6
  class LutamlTableInlineMacro <
7
7
  ::Asciidoctor::Extensions::InlineMacroProcessor
8
8
  include LutamlDiagramBase
9
+
9
10
  SUPPORTED_OPTIONS = %w[class enum data_type].freeze
10
11
 
11
12
  use_dsl
@@ -20,12 +20,12 @@ module Metanorma
20
20
  include LutamlEaXmiBase
21
21
 
22
22
  MACRO_REGEXP =
23
- /\[lutaml_uml_datamodel_description,([^,]+),?(.+)?\]/.freeze
23
+ /\[lutaml_uml_datamodel_description,([^,]+),?(.+)?\]/
24
24
 
25
25
  private
26
26
 
27
27
  def parse_result_document(full_path, guidance)
28
- ::Lutaml::XMI::Parsers::XML.serialize_xmi_to_liquid(
28
+ ::Lutaml::Xmi::Parsers::Xml.serialize_xmi_to_liquid(
29
29
  File.new(full_path, encoding: "UTF-8"),
30
30
  guidance,
31
31
  )
@@ -15,12 +15,12 @@ module Metanorma
15
15
  class LutamlXmiUmlPreprocessor < ::Asciidoctor::Extensions::Preprocessor
16
16
  include LutamlEaXmiBase
17
17
 
18
- MACRO_REGEXP = /\[lutaml_xmi_uml,([^,]+),?(.+)?\]/.freeze
18
+ MACRO_REGEXP = /\[lutaml_xmi_uml,([^,]+),?(.+)?\]/
19
19
 
20
20
  private
21
21
 
22
22
  def parse_result_document(full_path, _guidance)
23
- ::Lutaml::XMI::Parsers::XML.parse(
23
+ ::Lutaml::Xmi::Parsers::Xml.parse(
24
24
  File.new(full_path, encoding: "UTF-8"),
25
25
  )
26
26
  end
@@ -10,17 +10,17 @@ module Metanorma
10
10
 
11
11
  # example:
12
12
  # - [[abc]]
13
- ANCHOR_REGEX_1 = /^\[\[(?<id>[^\]]*)\]\]\s*$/.freeze
13
+ ANCHOR_REGEX_1 = /^\[\[(?<id>[^\]]*)\]\]\s*$/
14
14
 
15
15
  # examples:
16
16
  # - [#abc]
17
17
  # - [source#abc,ruby]
18
- ANCHOR_REGEX_2 = /^\[[^#,]*#(?<id>[^,\]]*)[,\]]/.freeze
18
+ ANCHOR_REGEX_2 = /^\[[^#,]*#(?<id>[^,\]]*)[,\]]/
19
19
 
20
20
  # examples:
21
21
  # - [id=abc]
22
22
  # - [source,id="abc"]
23
- ANCHOR_REGEX_3 = /^\[(?:.+,)?id=['"]?(?<id>[^,\]'"]*)['"]?[,\]]/.freeze
23
+ ANCHOR_REGEX_3 = /^\[(?:.+,)?id=['"]?(?<id>[^,\]'"]*)['"]?[,\]]/
24
24
 
25
25
  def initialize(document, input_lines)
26
26
  @document = document
@@ -14,6 +14,24 @@ module Metanorma
14
14
  module Lutaml
15
15
  # Helpers for lutaml macros
16
16
  module Utils
17
+ # Prepended to Liquid::Context to preserve the original exception
18
+ # that Liquid 5.x wraps as InternalError (discarding the cause).
19
+ module LiquidErrorCapturer
20
+ def handle_error(e, line_number = nil)
21
+ if e.is_a?(::Liquid::Error)
22
+ super
23
+ else
24
+ ie = ::Liquid::InternalError.new("internal")
25
+ ie.set_backtrace(e.backtrace)
26
+ ie.define_singleton_method(:original_error) { e }
27
+ ie.template_name ||= template_name
28
+ ie.line_number ||= line_number
29
+ errors.push(ie)
30
+ exception_renderer.call(ie).to_s
31
+ end
32
+ end
33
+ end
34
+
17
35
  LUTAML_EXP_IDX_TAG = %r{
18
36
  ^:lutaml-express-index: # Start of the pattern
19
37
  (?<index_name>.+?) # Capture index name
@@ -25,7 +43,7 @@ module Metanorma
25
43
  (?<cache_path>.+) # Capture cache path
26
44
  )? # End of optional group
27
45
  $ # End of the pattern
28
- }x.freeze
46
+ }x
29
47
 
30
48
  module_function
31
49
 
@@ -42,6 +60,10 @@ module Metanorma
42
60
  contexts:, document:,
43
61
  template_string: nil, include_path: nil, template_path: nil
44
62
  )
63
+ unless ::Liquid::Context <= LiquidErrorCapturer
64
+ ::Liquid::Context.prepend(LiquidErrorCapturer)
65
+ end
66
+
45
67
  # Allow includes for the template
46
68
  include_paths = [
47
69
  Utils.relative_file_path(document, ""),
@@ -85,16 +107,38 @@ module Metanorma
85
107
  result
86
108
  end
87
109
 
110
+ @seen_liquid_errors = Set.new
111
+
88
112
  def notify_render_errors(_document, errors)
89
- errors.each do |error_obj|
90
- ::Metanorma::Util.log(
91
- "[metanorma-plugin-lutaml] Liquid render error: " \
92
- "#{error_obj.message}",
93
- :error,
94
- )
113
+ return if errors.empty?
114
+
115
+ grouped = errors.group_by { |e| error_signature(e) }
116
+ grouped.each do |sig, errs|
117
+ total = errs.size
118
+ already_seen = @seen_liquid_errors.include?(sig)
119
+ @seen_liquid_errors << sig
120
+ next if already_seen
121
+
122
+ err = errs.first
123
+ backtrace = err.backtrace&.first(3)&.join("\n").to_s
124
+ count_label = total > 1 ? " (#{total}x)" : ""
125
+ parts = ["[metanorma-plugin-lutaml] Liquid render error#{count_label}:"]
126
+ parts << " #{err.class}: #{err.message}"
127
+ if err.respond_to?(:original_error) && err.original_error
128
+ orig = err.original_error
129
+ parts << " Caused by: #{orig.class}: #{orig.message}"
130
+ end
131
+ parts << backtrace unless backtrace.empty?
132
+ ::Metanorma::Util.log(parts.join("\n"), :error)
95
133
  end
96
134
  end
97
135
 
136
+ def error_signature(err)
137
+ orig = err.respond_to?(:original_error) && err.original_error
138
+ base = "#{err.class}: #{err.message}"
139
+ orig ? "#{base} (#{orig.class}: #{orig.message})" : base
140
+ end
141
+
98
142
  def load_express_repositories( # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
99
143
  path:, cache_path:, document:, force_read: false
100
144
  )
@@ -166,7 +210,7 @@ module Metanorma
166
210
 
167
211
  # TODO: Refactor this using Suma::SchemaConfig
168
212
  def load_express_from_index(_document, path) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
169
- yaml_content = YAML.safe_load(File.read(path))
213
+ yaml_content = YAML.safe_load_file(path)
170
214
  schema_yaml_base_path = Pathname.new(File.dirname(path))
171
215
 
172
216
  # If there is a global root path set, all subsequent paths are
@@ -1,7 +1,7 @@
1
1
  module Metanorma
2
2
  module Plugin
3
3
  module Lutaml
4
- VERSION = "0.7.37".freeze
4
+ VERSION = "0.7.39".freeze
5
5
  end
6
6
  end
7
7
  end
@@ -8,6 +8,7 @@ module Metanorma
8
8
  module Lutaml
9
9
  class Yaml2TextPreprocessor < BaseStructuredTextPreprocessor
10
10
  include Content
11
+
11
12
  # search document for block `yaml2text`
12
13
  # after that take template from block and read file into this template
13
14
  # example: