metanorma-plugin-lutaml 0.7.38 → 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 (33) 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/express.adoc +55 -0
  11. data/lib/metanorma/plugin/lutaml/base_structured_text_preprocessor.rb +5 -5
  12. data/lib/metanorma/plugin/lutaml/data2_text_preprocessor.rb +1 -0
  13. data/lib/metanorma/plugin/lutaml/express_remarks_decorator.rb +1 -1
  14. data/lib/metanorma/plugin/lutaml/json2_text_preprocessor.rb +1 -0
  15. data/lib/metanorma/plugin/lutaml/liquid/multiply_local_file_system.rb +2 -2
  16. data/lib/metanorma/plugin/lutaml/liquid_templates/_klass_table.liquid +6 -6
  17. data/lib/metanorma/plugin/lutaml/lutaml_ea_diagram_block_macro.rb +1 -1
  18. data/lib/metanorma/plugin/lutaml/lutaml_ea_xmi_base.rb +125 -15
  19. data/lib/metanorma/plugin/lutaml/lutaml_ea_xmi_preprocessor.rb +2 -2
  20. data/lib/metanorma/plugin/lutaml/lutaml_enum_table_block_macro.rb +1 -3
  21. data/lib/metanorma/plugin/lutaml/lutaml_gml_dictionary_block.rb +1 -0
  22. data/lib/metanorma/plugin/lutaml/lutaml_klass_table_block_macro.rb +2 -3
  23. data/lib/metanorma/plugin/lutaml/lutaml_preprocessor.rb +4 -5
  24. data/lib/metanorma/plugin/lutaml/lutaml_table_inline_macro.rb +1 -0
  25. data/lib/metanorma/plugin/lutaml/lutaml_uml_datamodel_description_preprocessor.rb +2 -2
  26. data/lib/metanorma/plugin/lutaml/lutaml_xmi_uml_preprocessor.rb +2 -2
  27. data/lib/metanorma/plugin/lutaml/source_extractor.rb +3 -3
  28. data/lib/metanorma/plugin/lutaml/utils.rb +52 -8
  29. data/lib/metanorma/plugin/lutaml/version.rb +1 -1
  30. data/lib/metanorma/plugin/lutaml/yaml2_text_preprocessor.rb +1 -0
  31. data/metanorma-plugin-lutaml.gemspec +4 -4
  32. metadata +25 -11
  33. data/.hound.yml +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb242e5ff7bfcfa7b4baec472a6fc0ecf0be94558913c8bb39bcf19655640e12
4
- data.tar.gz: 81455de9a154af7534d43830562613fbee00bd8ce2c252ce13be79fc790efaec
3
+ metadata.gz: ad20f1059d6fb886f3b8f8e209a5eb2053bd84cf197b4df0a6f28c32ceb05b76
4
+ data.tar.gz: ea0e8cdfdc56ada195921fbefff8256f67e702dba4685a001ba22051031bfc74
5
5
  SHA512:
6
- metadata.gz: c84b7143c72c6bbb6334bc9c27faea196a516edd34c67607579f61b0a371be648c9087e5b297159f98d60654f43ffc9a2c736a5e5c546196f7251917cc042d28
7
- data.tar.gz: b68b53795e9576abc2e1beb3746219ae2ca0ab8e888ab15f0c79430f94eedb870a568fe425e9a3ae503ba34e03d603436cc2dcb0cd64068500d30db421e079e5
6
+ metadata.gz: a72753313c88e6a7bc1388c764fe2f315faf4ff7447690dd5d6a8851f5fe15285d1b53629ff6c79db3ba53ec20e7cdb9d05242e1ef2b5e9fe7b6e69076cedad0
7
+ data.tar.gz: 98fbf0c36b867aae35acf710853ea74506bfc7835e8ed4368e9acedc84fe9f622f2cc44aae66fabc75bd49c9242487027d94239cec948972cf0e6117941e1ac2
@@ -10,6 +10,10 @@ on:
10
10
 
11
11
  jobs:
12
12
  rake:
13
- uses: metanorma/ci/.github/workflows/graphviz-rake.yml@main
13
+ uses: metanorma/ci/.github/workflows/generic-rake.yml@main
14
+ with:
15
+ setup-tools: graphviz
16
+ submodules: true
17
+ choco-cache: true
14
18
  secrets:
15
19
  pat_token: ${{ secrets.METANORMA_CI_PAT_TOKEN }}
data/.gitignore CHANGED
@@ -5,4 +5,5 @@ test.err
5
5
 
6
6
  .rubocop-https--*
7
7
  .DS_Store
8
- .vscode
8
+ .vscode
9
+ *.log.txt
data/.rubocop.yml CHANGED
@@ -1,10 +1,19 @@
1
1
  # Auto-generated by Cimas: Do not edit it manually!
2
2
  # See https://github.com/metanorma/cimas
3
3
  inherit_from:
4
- - https://raw.githubusercontent.com/riboseinc/oss-guides/master/ci/rubocop.yml
4
+ - https://raw.githubusercontent.com/riboseinc/oss-guides/main/ci/rubocop.yml
5
+ - .rubocop_todo.yml
6
+
7
+ inherit_mode:
8
+ merge:
9
+ - Exclude
5
10
 
6
11
  # local repo-specific modifications
7
12
  # ...
13
+ plugins:
14
+ - rubocop-rspec
15
+ - rubocop-performance
16
+ - rubocop-rake
8
17
 
9
18
  AllCops:
10
- TargetRubyVersion: 2.5
19
+ TargetRubyVersion: 3.0
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,238 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2026-05-12 14:53:16 UTC using RuboCop version 1.86.1.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 1
10
+ # This cop supports safe autocorrection (--autocorrect).
11
+ Gemspec/RequireMFA:
12
+ Exclude:
13
+ - 'metanorma-plugin-lutaml.gemspec'
14
+
15
+ # Offense count: 1
16
+ # This cop supports safe autocorrection (--autocorrect).
17
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
18
+ # SupportedStyles: with_first_argument, with_fixed_indentation
19
+ Layout/ArgumentAlignment:
20
+ Exclude:
21
+ - 'lib/metanorma/plugin/lutaml/lutaml_klass_table_block_macro.rb'
22
+
23
+ # Offense count: 1
24
+ # This cop supports safe autocorrection (--autocorrect).
25
+ Layout/ClosingParenthesisIndentation:
26
+ Exclude:
27
+ - 'lib/metanorma/plugin/lutaml/lutaml_ea_xmi_base.rb'
28
+
29
+ # Offense count: 1
30
+ # This cop supports safe autocorrection (--autocorrect).
31
+ Layout/EmptyLines:
32
+ Exclude:
33
+ - 'lib/metanorma/plugin/lutaml/lutaml_ea_xmi_base.rb'
34
+
35
+ # Offense count: 1
36
+ # This cop supports safe autocorrection (--autocorrect).
37
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
38
+ # SupportedStyles: consistent, consistent_relative_to_receiver, special_for_inner_method_call, special_for_inner_method_call_in_parentheses
39
+ Layout/FirstArgumentIndentation:
40
+ Exclude:
41
+ - 'lib/metanorma/plugin/lutaml/lutaml_ea_xmi_base.rb'
42
+
43
+ # Offense count: 9
44
+ # This cop supports safe autocorrection (--autocorrect).
45
+ # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
46
+ # URISchemes: http, https
47
+ Layout/LineLength:
48
+ Exclude:
49
+ - 'Rakefile'
50
+ - 'lib/metanorma/plugin/lutaml/lutaml_ea_xmi_base.rb'
51
+ - 'lib/metanorma/plugin/lutaml/lutaml_klass_table_block_macro.rb'
52
+ - 'lib/metanorma/plugin/lutaml/utils.rb'
53
+ - 'spec/metanorma/plugin/lutaml/lutaml_uml_datamodel_description_preprocessor_spec.rb'
54
+ - 'spec/support/shared_examples/structured_data_2_text_preprocessor.rb'
55
+
56
+ # Offense count: 1
57
+ # This cop supports safe autocorrection (--autocorrect).
58
+ # Configuration parameters: EnforcedStyle.
59
+ # SupportedStyles: symmetrical, new_line, same_line
60
+ Layout/MultilineMethodCallBraceLayout:
61
+ Exclude:
62
+ - 'lib/metanorma/plugin/lutaml/lutaml_ea_xmi_base.rb'
63
+
64
+ # Offense count: 3
65
+ # This cop supports safe autocorrection (--autocorrect).
66
+ # Configuration parameters: AllowInHeredoc.
67
+ Layout/TrailingWhitespace:
68
+ Exclude:
69
+ - 'lib/metanorma/plugin/lutaml/lutaml_ea_xmi_base.rb'
70
+ - 'lib/metanorma/plugin/lutaml/lutaml_klass_table_block_macro.rb'
71
+
72
+ # Offense count: 1
73
+ # This cop supports safe autocorrection (--autocorrect).
74
+ # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions.
75
+ # NotImplementedExceptions: NotImplementedError
76
+ Lint/UnusedMethodArgument:
77
+ Exclude:
78
+ - 'lib/metanorma/plugin/lutaml/lutaml_ea_xmi_base.rb'
79
+
80
+ # Offense count: 1
81
+ # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
82
+ Metrics/AbcSize:
83
+ Exclude:
84
+ - 'lib/metanorma/plugin/lutaml/utils.rb'
85
+
86
+ # Offense count: 2
87
+ # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
88
+ Metrics/CyclomaticComplexity:
89
+ Exclude:
90
+ - 'lib/metanorma/plugin/lutaml/lutaml_ea_xmi_base.rb'
91
+ - 'lib/metanorma/plugin/lutaml/utils.rb'
92
+
93
+ # Offense count: 3
94
+ # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
95
+ Metrics/MethodLength:
96
+ Max: 19
97
+
98
+ # Offense count: 2
99
+ # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
100
+ Metrics/PerceivedComplexity:
101
+ Exclude:
102
+ - 'lib/metanorma/plugin/lutaml/lutaml_ea_xmi_base.rb'
103
+ - 'lib/metanorma/plugin/lutaml/utils.rb'
104
+
105
+ # Offense count: 1
106
+ # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
107
+ # AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to
108
+ Naming/MethodParameterName:
109
+ Exclude:
110
+ - 'lib/metanorma/plugin/lutaml/utils.rb'
111
+
112
+ # Offense count: 2
113
+ # Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates.
114
+ # AllowedMethods: call
115
+ # WaywardPredicates: infinite?, nonzero?
116
+ Naming/PredicateMethod:
117
+ Exclude:
118
+ - 'lib/metanorma/plugin/lutaml/asciidoctor/preprocessor.rb'
119
+ - 'lib/metanorma/plugin/lutaml/liquid/custom_filters/file_exist.rb'
120
+
121
+ # Offense count: 1
122
+ # This cop supports unsafe autocorrection (--autocorrect-all).
123
+ Performance/AncestorsInclude:
124
+ Exclude:
125
+ - 'lib/metanorma/plugin/lutaml/utils.rb'
126
+
127
+ # Offense count: 86
128
+ # Configuration parameters: Prefixes, AllowedPatterns.
129
+ # Prefixes: when, with, without
130
+ RSpec/ContextWording:
131
+ Exclude:
132
+ - 'spec/metanorma/plugin/lutaml/lutaml_ea_xmi_preprocessor_spec.rb'
133
+ - 'spec/metanorma/plugin/lutaml/lutaml_enum_table_block_macro_spec.rb'
134
+ - 'spec/metanorma/plugin/lutaml/lutaml_gml_dictionary_block_macro_spec.rb'
135
+ - 'spec/metanorma/plugin/lutaml/lutaml_gml_dictionary_block_spec.rb'
136
+ - 'spec/metanorma/plugin/lutaml/lutaml_klass_table_block_macro_spec.rb'
137
+ - 'spec/metanorma/plugin/lutaml/lutaml_text_preprocessor_spec.rb'
138
+ - 'spec/metanorma/plugin/lutaml/lutaml_uml_datamodel_description_preprocessor_spec.rb'
139
+ - 'spec/metanorma/plugin/lutaml/lutaml_xmi_index_spec.rb'
140
+ - 'spec/metanorma/plugin/lutaml/lutaml_xmi_uml_preprocessor_spec.rb'
141
+ - 'spec/metanorma/plugin/lutaml/macros_data2text_spec.rb'
142
+ - 'spec/support/shared_examples/structured_data_2_text_preprocessor.rb'
143
+
144
+ # Offense count: 37
145
+ # Configuration parameters: CountAsOne.
146
+ RSpec/ExampleLength:
147
+ Max: 26
148
+
149
+ # Offense count: 13
150
+ # Configuration parameters: Max, AllowedIdentifiers, AllowedPatterns.
151
+ RSpec/IndexedLet:
152
+ Exclude:
153
+ - 'spec/metanorma/plugin/lutaml/lutaml_ea_diagram_block_macro_spec.rb'
154
+ - 'spec/metanorma/plugin/lutaml/lutaml_xmi_index_spec.rb'
155
+ - 'spec/metanorma/plugin/lutaml/macros_data2text_spec.rb'
156
+ - 'spec/support/shared_examples/structured_data_2_text_preprocessor.rb'
157
+
158
+ # Offense count: 4
159
+ # Configuration parameters: AssignmentOnly.
160
+ RSpec/InstanceVariable:
161
+ Exclude:
162
+ - 'spec/metanorma/plugin/lutaml/lutaml_express_preprocessor_spec.rb'
163
+
164
+ # Offense count: 44
165
+ RSpec/LeakyLocalVariable:
166
+ Exclude:
167
+ - 'spec/metanorma/plugin/lutaml/lutaml_ea_xmi_preprocessor_spec.rb'
168
+ - 'spec/metanorma/plugin/lutaml/lutaml_klass_table_block_macro_spec.rb'
169
+ - 'spec/metanorma/plugin/lutaml/lutaml_uml_datamodel_description_preprocessor_spec.rb'
170
+ - 'spec/metanorma/plugin/lutaml/lutaml_xmi_index_spec.rb'
171
+
172
+ # Offense count: 14
173
+ RSpec/MultipleExpectations:
174
+ Max: 6
175
+
176
+ # Offense count: 9
177
+ # Configuration parameters: AllowSubject.
178
+ RSpec/MultipleMemoizedHelpers:
179
+ Max: 8
180
+
181
+ # Offense count: 41
182
+ # Configuration parameters: EnforcedStyle, IgnoreSharedExamples.
183
+ # SupportedStyles: always, named_only
184
+ RSpec/NamedSubject:
185
+ Exclude:
186
+ - 'spec/metanorma/plugin/lutaml/config/root_spec.rb'
187
+ - 'spec/metanorma/plugin/lutaml/lutaml_ea_xmi_preprocessor_spec.rb'
188
+ - 'spec/metanorma/plugin/lutaml/lutaml_gml_dictionary_block_macro_spec.rb'
189
+ - 'spec/metanorma/plugin/lutaml/lutaml_gml_dictionary_block_spec.rb'
190
+ - 'spec/metanorma/plugin/lutaml/lutaml_uml_datamodel_description_preprocessor_spec.rb'
191
+ - 'spec/metanorma/plugin/lutaml/lutaml_xmi_index_spec.rb'
192
+ - 'spec/metanorma/plugin/lutaml/source_extractor_spec.rb'
193
+
194
+ # Offense count: 117
195
+ # Configuration parameters: AllowedGroups.
196
+ RSpec/NestedGroups:
197
+ Max: 7
198
+
199
+ # Offense count: 1
200
+ RSpec/PendingWithoutReason:
201
+ Exclude:
202
+ - 'spec/metanorma/plugin/lutaml/lutaml_text_preprocessor_spec.rb'
203
+
204
+ # Offense count: 2
205
+ RSpec/RepeatedExampleGroupBody:
206
+ Exclude:
207
+ - 'spec/metanorma/plugin/lutaml/macros_yaml2text_spec.rb'
208
+
209
+ # Offense count: 6
210
+ # Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector.
211
+ # SupportedInflectors: default, active_support
212
+ RSpec/SpecFilePathFormat:
213
+ Exclude:
214
+ - 'spec/metanorma/plugin/lutaml/lutaml_express_preprocessor_spec.rb'
215
+ - 'spec/metanorma/plugin/lutaml/lutaml_text_preprocessor_spec.rb'
216
+ - 'spec/metanorma/plugin/lutaml/lutaml_xmi_index_spec.rb'
217
+ - 'spec/metanorma/plugin/lutaml/macros_data2text_spec.rb'
218
+ - 'spec/metanorma/plugin/lutaml/macros_json2text_spec.rb'
219
+ - 'spec/metanorma/plugin/lutaml/macros_yaml2text_spec.rb'
220
+
221
+ # Offense count: 1
222
+ RSpec/SubjectDeclaration:
223
+ Exclude:
224
+ - 'spec/metanorma/plugin/lutaml/config/root_spec.rb'
225
+
226
+ # Offense count: 1
227
+ # This cop supports safe autocorrection (--autocorrect).
228
+ Style/RedundantBegin:
229
+ Exclude:
230
+ - 'lib/metanorma/plugin/lutaml/lutaml_ea_xmi_base.rb'
231
+
232
+ # Offense count: 1
233
+ # This cop supports safe autocorrection (--autocorrect).
234
+ # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
235
+ # SupportedStyles: single_quotes, double_quotes
236
+ Style/StringLiterals:
237
+ Exclude:
238
+ - 'metanorma-plugin-lutaml.gemspec'
data/CLAUDE.md ADDED
@@ -0,0 +1,78 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ `metanorma-plugin-lutaml` is a Ruby gem that provides Asciidoctor extensions for the Metanorma document publishing system. It enables Metanorma documents to embed and render data models from multiple formats: EXPRESS (ISO 10303), LutaML DSL, Enterprise Architect XMI, GML Dictionaries, JSON, and YAML.
8
+
9
+ ## Build and Test Commands
10
+
11
+ ```bash
12
+ # Install dependencies
13
+ bundle install
14
+
15
+ # Run all tests
16
+ bundle exec rake
17
+
18
+ # Run a single test file
19
+ bundle exec rspec spec/metanorma/plugin/lutaml/lutaml_preprocessor_spec.rb
20
+
21
+ # Run a specific test by line number
22
+ bundle exec rspec spec/metanorma/plugin/lutaml/lutaml_preprocessor_spec.rb:42
23
+
24
+ # Lint
25
+ bundle exec rubocop
26
+
27
+ # Build the gem
28
+ bundle exec rake build
29
+ ```
30
+
31
+ ## Architecture
32
+
33
+ ### Asciidoctor Extension Pattern
34
+
35
+ All extensions follow the Asciidoctor extension API. The two main extension types are:
36
+
37
+ - **Preprocessors** (`::Asciidoctor::Extensions::Preprocessor`): Process document text before rendering. They read input lines, detect custom block syntax (e.g., `[lutaml_express, ...]`), and replace them with rendered content via Liquid templates.
38
+ - **Block/BlockMacro/InlineMacro extensions**: Handle `lutaml_diagram`, `lutaml_ea_diagram`, `lutaml_gml_dictionary`, `lutaml_klass_table`, `lutaml_enum_table`, `lutaml_table`, `lutaml_figure` — these are Asciidoctor block-level or inline macros.
39
+
40
+ ### Preprocessor Inheritance Hierarchy
41
+
42
+ - `LutamlPreprocessor` — handles `[lutaml]`, `[lutaml_express]`, `[lutaml_express_liquid]` blocks. Parses EXPRESS files via the `lutaml`/`expressir` gems, builds Liquid contexts, and renders templates.
43
+ - `LutamlUmlDatamodelDescriptionPreprocessor` and `LutamlEaXmiPreprocessor` — both include `LutamlEaXmiBase`, which handles XMI parsing via `lutaml` gem and renders using bundled Liquid templates.
44
+ - `LutamlXmiUmlPreprocessor` — another XMI-based preprocessor with its own macro regex.
45
+ - `BaseStructuredTextPreprocessor` — base for `[yaml2text]`, `[json2text]`, `[data2text]` blocks. Its subclasses (`Yaml2TextPreprocessor`, `Json2TextPreprocessor`, `Data2TextPreprocessor`) differ only in how they load content (YAML vs JSON vs auto-detect). The `Content` module provides the actual parsing logic.
46
+
47
+ ### Key Shared Modules
48
+
49
+ - `Utils` (`lib/metanorma/plugin/lutaml/utils.rb`) — shared helpers: file path resolution relative to document, Liquid template rendering with custom environment/filters, EXPRESS repository loading with caching, parsing document-level `:lutaml-express-index:` attributes.
50
+ - `LutamlEaXmiBase` — shared logic for XMI-based preprocessors: XMI document loading/caching, config YAML parsing, package filtering/sorting, Liquid context building, nested macro collection.
51
+ - `LutamlDiagramBase` — shared logic for diagram block extensions: parses LutaML DSL, renders via Graphviz to PNG.
52
+ - `LutamlGmlDictionaryBase` — parses GML XML via `ogc-gml` gem, renders via Liquid.
53
+ - `SourceExtractor` — scans document lines for anchors (`[[id]]`, `[#id]`, `[id=...]`) and `include::`/`embed::` directives, extracting source blocks into `document.attributes["source_blocks"]` for later use by preprocessors.
54
+
55
+ ### Liquid Template System
56
+
57
+ The plugin uses Liquid templates extensively. Key components:
58
+
59
+ - **Custom Liquid environment** with registered `keyiterator` tag and custom filters (`html2adoc`, `values`, `replace_regex`, `loadfile`, `file_exist`).
60
+ - **`Liquid::LocalFileSystem`** subclass (`multiply_local_file_system.rb`) resolves includes from multiple paths.
61
+ - **Liquid Drops** (`liquid_drops/`) wrap domain objects (e.g., `GmlDictionaryDrop`) for template rendering.
62
+ - **Bundled templates** live in `lib/metanorma/plugin/lutaml/liquid_templates/` for XMI/EA rendering.
63
+
64
+ ### Config System
65
+
66
+ The `Config` module (`lib/metanorma/plugin/lutaml/config.rb`) uses `lutaml/model` (LutaML::Model) to define config schemas: `Root`, `Package`, `Guidance`, `GuidanceKlass`, `GuidanceAttribute`. Config YAML files control which packages/entities to render from XMI documents.
67
+
68
+ ### Testing Pattern
69
+
70
+ Tests use `metanorma-standoc` as the backend. The spec helper registers all extensions with Asciidoctor, then tests call `metanorma_convert(input)` which converts AsciiDoc input through the pipeline and compares XML output. Test fixtures (`.exp`, `.lutaml`, `.xmi`, `.xml`, `.liquid` files) are in `spec/fixtures/lutaml/` and `spec/assets/lutaml/`.
71
+
72
+ ## Key Dependencies
73
+
74
+ - `lutaml` — core LutaML parser/model library (EXPRESS, UML, XMI formats)
75
+ - `expressir` — EXPRESS schema parser
76
+ - `ogc-gml` — OGC GML dictionary parser
77
+ - `liquid` — template rendering engine
78
+ - `asciidoctor` — document processing framework
data/Gemfile CHANGED
@@ -12,18 +12,14 @@ rescue StandardError
12
12
  nil
13
13
  end
14
14
 
15
- gem "byebug"
16
- gem "debug"
17
- gem "equivalent-xml"
18
- gem "metanorma"
19
- gem "metanorma-standoc", "~> 3.0.8"
20
15
  gem "rake", "~> 13"
21
- gem "rspec", "~> 3.6"
16
+ gem "rspec"
22
17
  gem "rspec-html-matchers"
23
- gem "rubocop", "~> 1.58"
24
- gem "rubocop-performance", "~> 1.19"
25
- gem "simplecov", "~> 0.15"
26
- gem "timecop", "~> 0.9"
27
- gem "vcr", "~> 6.1.0"
18
+ gem "rubocop"
19
+ gem "rubocop-performance"
20
+ gem "rubocop-rake"
21
+ gem "rubocop-rspec"
22
+ gem "simplecov"
23
+ gem "timecop"
24
+ gem "vcr"
28
25
  gem "webmock"
29
- gem "xml-c14n"
data/Gemfile.devel ADDED
@@ -0,0 +1,2 @@
1
+ gem "metanorma-standoc", github: "metanorma/metanorma-standoc", branch: "main"
2
+ gem "metanorma", github: "metanorma/metanorma", branch: "main"
data/Rakefile CHANGED
@@ -1,6 +1,23 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
3
 
4
+ XMI_SPEC_FILES = %w[
5
+ spec/metanorma/plugin/lutaml/lutaml_ea_xmi_preprocessor_spec.rb
6
+ spec/metanorma/plugin/lutaml/lutaml_uml_datamodel_description_preprocessor_spec.rb
7
+ spec/metanorma/plugin/lutaml/lutaml_klass_table_block_macro_spec.rb
8
+ spec/metanorma/plugin/lutaml/lutaml_enum_table_block_macro_spec.rb
9
+ spec/metanorma/plugin/lutaml/lutaml_xmi_index_spec.rb
10
+ spec/metanorma/plugin/lutaml/lutaml_xmi_uml_preprocessor_spec.rb
11
+ ].freeze
12
+
4
13
  RSpec::Core::RakeTask.new(:spec)
5
14
 
15
+ RSpec::Core::RakeTask.new(:fast) do |t|
16
+ t.pattern = FileList["spec/**/*_spec.rb"] - XMI_SPEC_FILES
17
+ end
18
+
19
+ RSpec::Core::RakeTask.new(:xmi) do |t|
20
+ t.pattern = FileList[XMI_SPEC_FILES]
21
+ end
22
+
6
23
  task default: :spec
@@ -297,3 +297,58 @@ from the paths other than the location of the document.
297
297
  The resulting block adds the `include_path` to the Liquid renderer. The path is
298
298
  resolved based on the location of the document. You can add multiple paths by
299
299
  separating them with commas.
300
+
301
+ === Using `loadfile` filter in templates
302
+
303
+ This functionality allows `[lutaml_express_liquid]` blocks to load data from
304
+ a yaml or json file from the specified path.
305
+
306
+ For example, you have the Metanorma document file (.adoc) with the following
307
+ content:
308
+
309
+ [source,adoc]
310
+ -----
311
+ :lutaml-express-index: all_schemas; ../schemas_all.yaml;
312
+
313
+ [lutaml_express_liquid,all_schemas,context,config_yaml=schemas.yaml,include_path=../templates]
314
+ ---
315
+ {% assign all_schemas = repo.schemas %}
316
+ {% render "templates/resources/schema" for ordered_schemas as schema %}
317
+ ...
318
+ ----
319
+ -----
320
+
321
+ You have the liquid template file
322
+ `templates/resources/schema.liquid` with the following content:
323
+
324
+ [source,liquid]
325
+ -----
326
+ {% assign data = "my_file.yaml" | loadfile: "." %}
327
+ This is {{ data.shape }} with color {{ data.color }}.
328
+ -----
329
+
330
+ Where:
331
+
332
+ * `loadfile:` is a liquid filter that loads the file content based on the path
333
+ `my_file.yaml` with argument `.`.
334
+ The argument is the path of the parent folder, which is the
335
+ current directory of the Metanorma document.
336
+
337
+ And the content of the yaml file `my_file.yaml` looks like:
338
+
339
+ [source,yaml]
340
+ ----
341
+ ---
342
+ shape: square
343
+ color: blue
344
+ corners: 4
345
+ ----
346
+
347
+ Will render as:
348
+ [source,asciidoc]
349
+ ----
350
+ ...
351
+ This is square with color blue.
352
+ ...
353
+ ----
354
+
@@ -15,9 +15,9 @@ module Metanorma
15
15
  ::Asciidoctor::Extensions::Preprocessor
16
16
  include Utils
17
17
 
18
- BLOCK_START_REGEXP = /\{(.+?)\.\*,(.+),(.+)\}/.freeze
19
- BLOCK_END_REGEXP = /\A\{[A-Z]+\}\z/.freeze
20
- LOAD_FILE_REGEXP = /{% assign (.*) = (.*) \| load_file %}/.freeze
18
+ BLOCK_START_REGEXP = /\{(.+?)\.\*,(.+),(.+)\}/
19
+ BLOCK_END_REGEXP = /\A\{[A-Z]+\}\z/
20
+ LOAD_FILE_REGEXP = /{% assign (.*) = (.*) \| load_file %}/
21
21
  INCLUDE_PATH_OPTION = "include_path"
22
22
  TEMPLATE_OPTION = "template"
23
23
 
@@ -63,7 +63,7 @@ module Metanorma
63
63
  load_file_match = block_line.match(LOAD_FILE_REGEXP)
64
64
 
65
65
  # Add parent folder as argument to loadfile filter
66
- block_line = "{% assign #{load_file_match[1]} = "\
66
+ block_line = "{% assign #{load_file_match[1]} = " \
67
67
  "#{load_file_match[2]} | loadfile: " \
68
68
  "\"#{document.attributes['docdir']}\" %}"
69
69
  end
@@ -106,7 +106,7 @@ module Metanorma
106
106
  )
107
107
  multiple_contexts
108
108
  elsif block_match[1].start_with?("#")
109
- anchor = block_match[1].split(",").first.strip[1..-1]
109
+ anchor = block_match[1].split(",").first.strip[1..]
110
110
  {
111
111
  block_match[1].split(",").last.strip =>
112
112
  content_from_anchor(document, anchor),
@@ -8,6 +8,7 @@ module Metanorma
8
8
  module Lutaml
9
9
  class Data2TextPreprocessor < BaseStructuredTextPreprocessor
10
10
  include Content
11
+
11
12
  # search document for block `data2text`
12
13
  # after that take template from block and read file into this template
13
14
  # example:
@@ -16,7 +16,7 @@ module Metanorma
16
16
  ([^:\[]+) # Capture group 3: the path/name
17
17
  (\[.*\])? # Capture group 4: optional attribute
18
18
  $ # End of line
19
- }x.freeze
19
+ }x
20
20
 
21
21
  attr_reader :remark, :options
22
22
 
@@ -8,6 +8,7 @@ module Metanorma
8
8
  module Lutaml
9
9
  class Json2TextPreprocessor < BaseStructuredTextPreprocessor
10
10
  include Content
11
+
11
12
  # search document for block `json2text`
12
13
  # after that take template from block and read file into this template
13
14
  # example:
@@ -52,8 +52,8 @@ module Metanorma
52
52
 
53
53
  if result_path.nil?
54
54
  raise ::Liquid::FileSystemError,
55
- "No documents in template path: " \
56
- " #{File.expand_path(template_path)}"
55
+ "No documents in template path: " \
56
+ "#{File.expand_path(template_path)}"
57
57
  end
58
58
 
59
59
  unless roots.any? do |root|
@@ -22,7 +22,7 @@ h| Stereotype 2+| {{ stereotype }}
22
22
  3+h| Inherited Properties
23
23
  h| Property Name h| Property Type and Multiplicity h| Definition
24
24
  {% for attr in root.inherited_props %}
25
- {%- if attr.has_association? == false -%}
25
+ {%- unless attr.has_association? -%}
26
26
  {%- capture name_col -%}
27
27
  {{ attr.name_ns }}:{{ attr.name }}
28
28
  ({{ attr.gen_name }})
@@ -30,13 +30,13 @@ h| Property Name h| Property Type and Multiplicity h| Definition
30
30
  | {{ name_col | newline_to_br }}
31
31
  | {{ attr.type }} [{{ attr.cardinality.min }}..{{ attr.cardinality.max }}]
32
32
  | {{ attr.definition }}
33
- {%- endif -%}
33
+ {%- endunless -%}
34
34
  {% endfor %}
35
35
 
36
36
  3+h| Self-defined Properties
37
37
  h| Property Name h| Property Type and Multiplicity h| Definition
38
38
  {% for attr in root.owned_props %}
39
- {%- if attr.has_association? == false -%}
39
+ {%- unless attr.has_association? -%}
40
40
  {%- capture name_col -%}
41
41
  {{ attr.name_ns }}:{{ attr.name }}
42
42
  ({{ attr.gen_name }})
@@ -44,13 +44,13 @@ h| Property Name h| Property Type and Multiplicity h| Definition
44
44
  | {{ name_col | newline_to_br }}
45
45
  | {{ attr.type }} [{{ attr.cardinality.min }}..{{ attr.cardinality.max }}]
46
46
  | {{ attr.definition }}
47
- {%- endif -%}
47
+ {%- endunless -%}
48
48
  {% endfor %}
49
49
 
50
50
  3+h| Properties Inherited from Association
51
51
  h| Property Name h| Property Type and Multiplicity h| Definition
52
52
  {% for attr in root.inherited_assoc_props %}
53
- {%- if attr.has_association? == true -%}
53
+ {%- if attr.has_association? -%}
54
54
  {%- capture name_col -%}
55
55
  {{ attr.name_ns }}:{{ attr.name }}
56
56
  ({{ attr.gen_name }})
@@ -64,7 +64,7 @@ h| Property Name h| Property Type and Multiplicity h| Definition
64
64
  3+h| Properties Defined in Association
65
65
  h| Property Name h| Property Type and Multiplicity h| Definition
66
66
  {% for attr in root.assoc_props %}
67
- {%- if attr.has_association? == true -%}
67
+ {%- if attr.has_association? -%}
68
68
  {%- capture name_col -%}
69
69
  {{ attr.name_ns }}:{{ attr.name }}
70
70
  ({{ attr.gen_name }})
@@ -29,7 +29,7 @@ module Metanorma
29
29
  private
30
30
 
31
31
  def parse_result_document(full_path, guidance = nil)
32
- ::Lutaml::XMI::Parsers::XML.serialize_xmi_to_liquid(
32
+ ::Lutaml::Xmi::Parsers::Xml.serialize_xmi_to_liquid(
33
33
  File.new(full_path, encoding: "UTF-8"),
34
34
  guidance,
35
35
  )
@@ -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
@@ -473,6 +479,110 @@ module Metanorma
473
479
 
474
480
  attrs["name"]
475
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
476
586
  end
477
587
  end
478
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
  )
@@ -23,9 +23,7 @@ module Metanorma
23
23
  xmi_path = get_xmi_path(parent, target, attrs)
24
24
  path = get_name_path(attrs)
25
25
 
26
- enum = ::Lutaml::XMI::Parsers::XML.serialize_enumeration_by_name(
27
- xmi_path, path
28
- )
26
+ enum = serialize_enum_drop_by_name(xmi_path, path, parent.document)
29
27
 
30
28
  render_table(enum, CONTEXT_NAME, parent, attrs)
31
29
  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
@@ -28,9 +28,8 @@ module Metanorma
28
28
  guidance = get_guidance(parent.document, attrs["guidance"])
29
29
  end
30
30
 
31
- klass = ::Lutaml::XMI::Parsers::XML.serialize_generalization_by_name(
32
- xmi_path, path, guidance
33
- )
31
+ klass = serialize_klass_drop_by_name(xmi_path, path, parent.document,
32
+ guidance)
34
33
 
35
34
  render_table(klass, CONTEXT_NAME, parent, attrs)
36
35
  end
@@ -28,7 +28,7 @@ module Metanorma
28
28
  (?<context_name>[^,]+)? # Optional context name
29
29
  (?<options>,.*)? # Optional options
30
30
  \] # Closing bracket
31
- }x.freeze
31
+ }x
32
32
 
33
33
  def process(document, reader) # rubocop:disable Metrics/MethodLength
34
34
  r = Asciidoctor::PreprocessorNoIfdefsReader.new(document,
@@ -85,8 +85,8 @@ module Metanorma
85
85
  index_names = block_header_match[:index_names].split(";").map(&:strip)
86
86
  context_name = block_header_match[:context_name].strip
87
87
 
88
- options = block_header_match[:options] &&
89
- 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)) || {}
90
90
 
91
91
  end_mark = input_lines.next
92
92
 
@@ -277,8 +277,7 @@ module Metanorma
277
277
  options_string
278
278
  .to_s
279
279
  .scan(/,\s*([^=]+?)=(\s*[^,]+)/)
280
- .map { |elem| elem.map(&:strip) }
281
- .to_h
280
+ .to_h { |elem| elem.map(&:strip) }
282
281
  end
283
282
  end
284
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.38".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:
@@ -29,12 +29,12 @@ Gem::Specification.new do |spec|
29
29
  spec.required_ruby_version = ">= 2.7.0" # rubocop:disable Gemspec/RequiredRubyVersion
30
30
 
31
31
  spec.add_dependency "asciidoctor"
32
- spec.add_dependency "coradoc", "~> 1.1"
33
- spec.add_dependency "expressir", "~> 2.1"
32
+ spec.add_dependency "coradoc", "~> 1.1.8"
33
+ spec.add_dependency "expressir", "~> 2.3", ">= 2.3.4"
34
34
  spec.add_dependency "isodoc"
35
35
  spec.add_dependency "liquid"
36
- spec.add_dependency "lutaml", "~> 0.9"
37
- spec.add_dependency "ogc-gml", "~>1.0.0"
36
+ spec.add_dependency "lutaml", "~> 0.10", ">= 0.10.12"
37
+ spec.add_dependency "ogc-gml", "~> 1.1"
38
38
  spec.add_dependency "relaton-cli"
39
39
 
40
40
  spec.metadata["rubygems_mfa_required"] = "false"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: metanorma-plugin-lutaml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.38
4
+ version: 0.7.39
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-10-28 00:00:00.000000000 Z
11
+ date: 2026-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: asciidoctor
@@ -30,28 +30,34 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.1'
33
+ version: 1.1.8
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.1'
40
+ version: 1.1.8
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: expressir
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '2.1'
47
+ version: '2.3'
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 2.3.4
48
51
  type: :runtime
49
52
  prerelease: false
50
53
  version_requirements: !ruby/object:Gem::Requirement
51
54
  requirements:
52
55
  - - "~>"
53
56
  - !ruby/object:Gem::Version
54
- version: '2.1'
57
+ version: '2.3'
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 2.3.4
55
61
  - !ruby/object:Gem::Dependency
56
62
  name: isodoc
57
63
  requirement: !ruby/object:Gem::Requirement
@@ -86,28 +92,34 @@ dependencies:
86
92
  requirements:
87
93
  - - "~>"
88
94
  - !ruby/object:Gem::Version
89
- version: '0.9'
95
+ version: '0.10'
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: 0.10.12
90
99
  type: :runtime
91
100
  prerelease: false
92
101
  version_requirements: !ruby/object:Gem::Requirement
93
102
  requirements:
94
103
  - - "~>"
95
104
  - !ruby/object:Gem::Version
96
- version: '0.9'
105
+ version: '0.10'
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: 0.10.12
97
109
  - !ruby/object:Gem::Dependency
98
110
  name: ogc-gml
99
111
  requirement: !ruby/object:Gem::Requirement
100
112
  requirements:
101
113
  - - "~>"
102
114
  - !ruby/object:Gem::Version
103
- version: 1.0.0
115
+ version: '1.1'
104
116
  type: :runtime
105
117
  prerelease: false
106
118
  version_requirements: !ruby/object:Gem::Requirement
107
119
  requirements:
108
120
  - - "~>"
109
121
  - !ruby/object:Gem::Version
110
- version: 1.0.0
122
+ version: '1.1'
111
123
  - !ruby/object:Gem::Dependency
112
124
  name: relaton-cli
113
125
  requirement: !ruby/object:Gem::Requirement
@@ -132,10 +144,12 @@ files:
132
144
  - ".github/workflows/rake.yml"
133
145
  - ".github/workflows/release.yml"
134
146
  - ".gitignore"
135
- - ".hound.yml"
136
147
  - ".rubocop.yml"
148
+ - ".rubocop_todo.yml"
149
+ - CLAUDE.md
137
150
  - CODE_OF_CONDUCT.md
138
151
  - Gemfile
152
+ - Gemfile.devel
139
153
  - LICENSE
140
154
  - README.adoc
141
155
  - Rakefile
data/.hound.yml DELETED
@@ -1,5 +0,0 @@
1
- # Auto-generated by Cimas: Do not edit it manually!
2
- # See https://github.com/metanorma/cimas
3
- ruby:
4
- enabled: true
5
- config_file: .rubocop.yml