metanorma-plugin-lutaml 0.7.39 → 0.7.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad20f1059d6fb886f3b8f8e209a5eb2053bd84cf197b4df0a6f28c32ceb05b76
4
- data.tar.gz: ea0e8cdfdc56ada195921fbefff8256f67e702dba4685a001ba22051031bfc74
3
+ metadata.gz: ab934a775aa2b0aa6abeb544d5dcaf3c4c8fcf37daa75119d5ba9d57a09196e8
4
+ data.tar.gz: ca930ec2f902c975ffaecc0b5bdd2fb15ad8e68f4dfb72cce3626cd54d9cc560
5
5
  SHA512:
6
- metadata.gz: a72753313c88e6a7bc1388c764fe2f315faf4ff7447690dd5d6a8851f5fe15285d1b53629ff6c79db3ba53ec20e7cdb9d05242e1ef2b5e9fe7b6e69076cedad0
7
- data.tar.gz: 98fbf0c36b867aae35acf710853ea74506bfc7835e8ed4368e9acedc84fe9f622f2cc44aae66fabc75bd49c9242487027d94239cec948972cf0e6117941e1ac2
6
+ metadata.gz: a6b32f1e6a256f7c548578fc324d25bfdf0c0a389233c02836178b95af7dca009723f72407fd8dab9f18761535915da9b665ab819bd6fdd8ee67ceae290d1139
7
+ data.tar.gz: dece27d9189a6b36ec3a0e80aa6c376eabcd8af181f52409f1d5ae559a85a7611eee30c4146a598a751dc26bbb7cf85482f540ca38dc767465a99013e914dee0
data/CLAUDE.md CHANGED
@@ -39,10 +39,12 @@ All extensions follow the Asciidoctor extension API. The two main extension type
39
39
 
40
40
  ### Preprocessor Inheritance Hierarchy
41
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.
42
+ - `BasePreprocessor` — abstract base for EXPRESS and XSD preprocessors. Uses Template Method pattern: subclasses implement `lutaml_liquid?`, `load_lutaml_file`, `index_type_name` and may override `update_repo`, `template`, `reorder_schemas`.
43
+ - `LutamlPreprocessor` < `BasePreprocessor` — handles `[lutaml]`, `[lutaml_express]`, `[lutaml_express_liquid]` blocks. Adds EXPRESS-specific `update_repo` (cache unwrap, remark decoration), Liquid environment with custom tags/filters, schema reordering.
44
+ - `LutamlXsdPreprocessor` < `BasePreprocessor` — handles `[lutaml_xsd]` blocks. Parses XSD files via `lutaml-model`, double-newline template joins for Asciidoctor paragraph breaks.
43
45
  - `LutamlUmlDatamodelDescriptionPreprocessor` and `LutamlEaXmiPreprocessor` — both include `LutamlEaXmiBase`, which handles XMI parsing via `lutaml` gem and renders using bundled Liquid templates.
44
46
  - `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.
47
+ - `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).
46
48
 
47
49
  ### Key Shared Modules
48
50
 
@@ -71,8 +73,10 @@ Tests use `metanorma-standoc` as the backend. The spec helper registers all exte
71
73
 
72
74
  ## Key Dependencies
73
75
 
74
- - `lutaml` — core LutaML parser/model library (EXPRESS, UML, XMI formats)
76
+ - `lutaml` — core LutaML parser/model library (EXPRESS, UML, XMI, XSD formats)
77
+ - `lutaml-model` — LutaML serialization framework (provides XSD parsing, Liquid drops)
75
78
  - `expressir` — EXPRESS schema parser
76
79
  - `ogc-gml` — OGC GML dictionary parser
77
80
  - `liquid` — template rendering engine
78
81
  - `asciidoctor` — document processing framework
82
+ - `canon` — semantic XML comparison for test assertions
data/Gemfile CHANGED
@@ -12,7 +12,12 @@ rescue StandardError
12
12
  nil
13
13
  end
14
14
 
15
- gem "rake", "~> 13"
15
+ gem "canon"
16
+ gem "html2doc", github: "metanorma/html2doc", branch: "main"
17
+ gem "lutaml"
18
+ gem "metanorma", github: "metanorma/metanorma", branch: "main"
19
+ gem "metanorma-standoc", github: "metanorma/metanorma-standoc", branch: "main"
20
+ gem "rake"
16
21
  gem "rspec"
17
22
  gem "rspec-html-matchers"
18
23
  gem "rubocop"
@@ -23,3 +28,4 @@ gem "simplecov"
23
28
  gem "timecop"
24
29
  gem "vcr"
25
30
  gem "webmock"
31
+
data/README.adoc CHANGED
@@ -14,6 +14,7 @@ within a Metanorma document:
14
14
  * Enterprise Architect exported UML files in XMI format (`*.xmi`)
15
15
  * LutaML GML Dictionary files (`*.xml`)
16
16
  * JSON or YAML files (`*.json|*.yml|*.yaml`)
17
+ * XML Schema files (`*.xsd`)
17
18
 
18
19
  == Installation
19
20
 
@@ -34,6 +35,8 @@ link:docs/usages/lutaml-gml.adoc[Usage with LutaML GML Dictionary by lutaml_gml_
34
35
 
35
36
  link:docs/usages/json_yaml.adoc[Usage with JSON or YAML files by data2text, yaml2text or json2text]
36
37
 
38
+ link:docs/usages/lutaml-xsd.adoc[Usage with XML Schema files by lutaml_xsd]
39
+
37
40
  == Documentation
38
41
 
39
42
  Please refer to https://www.metanorma.org.
@@ -0,0 +1,94 @@
1
+ == Usage with LutaML XSD
2
+
3
+ === Overview
4
+
5
+ The `lutaml_xsd` macro parses *XML Schema (XSD)* files through `lutaml-model`
6
+ and exposes the parsed schema object to *Liquid* templates.
7
+
8
+ === Syntax
9
+
10
+ [source,adoc]
11
+ -----
12
+ [lutaml_xsd,<path_to_xsd>,<context_name>[, option1=value1, option2=value2, ...]]
13
+ ----
14
+ <your Liquid template here>
15
+ ----
16
+ -----
17
+
18
+ * `<path_to_xsd>`: Path to the XSD file to be processed.
19
+ * `<context_name>`: The name of the context variable to use in the template.
20
+ * `option1=value1, ...`: Optional parameters (<<options,supported options>>).
21
+
22
+ [[options]]
23
+ === Options
24
+
25
+ * `location`: Base URL or path for resolving `<xs:import>` and `<xs:include>`
26
+ statements in the XSD. When omitted, the directory of `<path_to_xsd>` is used.
27
+
28
+ === Liquid Template Context
29
+
30
+ The context variable (e.g., `unitsml`) exposes the parsed
31
+ `Lutaml::Xml::Schema::Xsd::Schema` object through its Liquid drop.
32
+
33
+ Commonly used schema collections:
34
+
35
+ * `element`: List of elements defined in the XSD.
36
+ * `complex_type`: List of complex types defined in the XSD.
37
+ * `simple_type`, `attribute`, `attribute_group`, `group`, `import`, and
38
+ `include`: Other schema components exposed by `lutaml-model`.
39
+
40
+ Commonly used helpers:
41
+
42
+ * `elements_sorted_by_name`, `complex_types_sorted_by_name`,
43
+ `attribute_groups_sorted_by_name`: Sorted schema collections.
44
+ * `used_by`, `child_elements`, `attribute_elements`, and `referenced_type`:
45
+ Component helpers exposed by parsed XSD objects.
46
+
47
+ === Example: Listing Elements and Complex Types
48
+
49
+ [source,adoc]
50
+ -----
51
+ = Elements
52
+ [lutaml_xsd,path/to/unitsml.xsd,unitsml]
53
+ ----
54
+ {% for element in unitsml.elements_sorted_by_name %}
55
+ Name: *{{ element.name }}*
56
+ Type: *{{ element.type }}*
57
+ Used by: {{ element.used_by | map: "name" | join: ", " }}
58
+ {% endfor %}
59
+ ----
60
+
61
+ = ComplexTypes
62
+ [lutaml_xsd,path/to/unitsml.xsd,unitsml]
63
+ ----
64
+ {% for complex_type in unitsml.complex_types_sorted_by_name %}
65
+ Name: *{{ complex_type.name }}*
66
+ Children: {{ complex_type.child_elements | map: "name" | join: ", " }}
67
+ Attributes: {{ complex_type.attribute_elements | map: "name" | join: ", " }}
68
+ {% endfor %}
69
+ ----
70
+ -----
71
+
72
+ === Example: Using with Remote XSD and Options
73
+
74
+ [source,adoc]
75
+ -----
76
+ [lutaml_xsd,path/to/omml.xsd,omml, location=https://raw.githubusercontent.com/t-yuki/ooxml-xsd/refs/heads/master]
77
+ ----
78
+ {% for element in omml.element %}
79
+ Name: *{{ element.name }}*
80
+ Type: *{{ element.type }}*
81
+ {% endfor %}
82
+ ----
83
+ -----
84
+
85
+ === Use Cases
86
+
87
+ * Generate documentation for XML schemas.
88
+ * Extract and list schema elements and types or other details.
89
+ * Customize output using Liquid templates.
90
+
91
+ === Notes
92
+
93
+ * The macro supports local files at `<path_to_xsd>`.
94
+ * You can use all standard *Liquid* template features for formatting and logic.
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "liquid"
4
+ require "asciidoctor"
5
+ require "asciidoctor/reader"
6
+ require "metanorma/plugin/lutaml/utils"
7
+ require "metanorma/plugin/lutaml/asciidoctor/preprocessor"
8
+
9
+ module Metanorma
10
+ module Plugin
11
+ module Lutaml
12
+ # Base preprocessor for LutaML format-specific preprocessors.
13
+ #
14
+ # Subclasses must implement:
15
+ # - #lutaml_liquid?(line) — match the macro header line
16
+ # - #load_lutaml_file(document, file_path, options)
17
+ # parse format-specific input
18
+ #
19
+ # Subclasses may override:
20
+ # - #index_type_name — human-readable format name for error messages
21
+ # - #update_repo(options, repo) — transform parsed repo before rendering
22
+ # - #template(lines) — parse Liquid template lines
23
+ # - #reorder_schemas(repo_liquid, options) — reorder/filter schemas
24
+ class BasePreprocessor < ::Asciidoctor::Extensions::Preprocessor
25
+ include Utils
26
+
27
+ def process(document, reader)
28
+ input_lines = Asciidoctor::PreprocessorNoIfdefsReader
29
+ .new(document, reader.lines).readlines.to_enum
30
+
31
+ express_indexes = Utils.parse_document_express_indexes(
32
+ document, input_lines
33
+ )
34
+
35
+ result_content = process_input_lines(
36
+ document: document,
37
+ input_lines: input_lines,
38
+ express_indexes: express_indexes,
39
+ )
40
+
41
+ Asciidoctor::PreprocessorNoIfdefsReader.new(document, result_content)
42
+ end
43
+
44
+ protected
45
+
46
+ def load_lutaml_file(_document, _file_path, _options)
47
+ raise NotImplementedError,
48
+ "#{self.class}#load_lutaml_file must be implemented"
49
+ end
50
+
51
+ def lutaml_liquid?(_line)
52
+ raise NotImplementedError,
53
+ "#{self.class}#lutaml_liquid? must be implemented"
54
+ end
55
+
56
+ def index_type_name
57
+ raise NotImplementedError,
58
+ "#{self.class}#index_type_name must be implemented"
59
+ end
60
+
61
+ def update_repo(_options, repo)
62
+ repo
63
+ end
64
+
65
+ def template(lines)
66
+ ::Liquid::Template.parse(lines.join("\n"))
67
+ end
68
+
69
+ def reorder_schemas(repo_liquid, _options)
70
+ repo_liquid
71
+ end
72
+
73
+ def index_missing_message(path)
74
+ "Unable to load #{index_type_name} file for `#{path}`, " \
75
+ "please specify the full path."
76
+ end
77
+
78
+ private
79
+
80
+ def process_input_lines(document:, input_lines:, express_indexes:)
81
+ result = []
82
+ loop do
83
+ result.push(
84
+ *process_text_blocks(document, input_lines, express_indexes),
85
+ )
86
+ end
87
+ result
88
+ end
89
+
90
+ def process_text_blocks(document, input_lines, express_indexes) # rubocop:disable Metrics/AbcSize
91
+ line = input_lines.next
92
+ block_header_match = lutaml_liquid?(line)
93
+
94
+ return [line] unless block_header_match
95
+
96
+ index_names = block_header_match[:index_names].split(";").map(&:strip)
97
+ context_name = block_header_match[:context_name].strip
98
+ options = (block_header_match[:options] &&
99
+ parse_options(block_header_match[:options].to_s.strip)) || {}
100
+
101
+ end_mark = input_lines.next
102
+
103
+ render_liquid_template(
104
+ document: document,
105
+ lines: extract_block_lines(input_lines, end_mark),
106
+ index_names: index_names,
107
+ context_name: context_name,
108
+ options: options,
109
+ indexes: express_indexes,
110
+ )
111
+ end
112
+
113
+ def extract_block_lines(input_lines, end_mark)
114
+ block = []
115
+ while (block_line = input_lines.next) != end_mark
116
+ block.push(block_line)
117
+ end
118
+ block
119
+ end
120
+
121
+ # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/ParameterLists
122
+ def gather_context_liquid_items(index_names:, document:,
123
+ indexes:, options: {})
124
+ index_names.map do |path|
125
+ if indexes[path] && indexes[path][:model]
126
+ repo = indexes[path][:model]
127
+ repo = update_repo(options, repo)
128
+ indexes[path][:liquid_drop] ||= repo.to_liquid
129
+ else
130
+ full_path = Utils.relative_file_path(document, path)
131
+ unless File.file?(full_path)
132
+ raise StandardError, index_missing_message(path)
133
+ end
134
+
135
+ repo = load_lutaml_file(document, path, options)
136
+ repo = update_repo(options, repo)
137
+ indexes[path] = { liquid_drop: repo.to_liquid }
138
+ end
139
+
140
+ indexes[path]
141
+ end
142
+ end
143
+ # rubocop:enable Metrics/AbcSize,Metrics/MethodLength,Metrics/ParameterLists
144
+
145
+ def render_liquid_template(document:, lines:, context_name:, # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/ParameterLists
146
+ index_names:, options:, indexes:)
147
+ options = process_options(document, options)
148
+
149
+ all_items = gather_context_liquid_items(
150
+ index_names: index_names, document: document, indexes: indexes,
151
+ options: options.merge("document" => document)
152
+ )
153
+
154
+ include_paths = [Utils.relative_file_path(document, "")]
155
+ options["include_path"]&.split(",")&.each do |path|
156
+ include_paths.push(Utils.relative_file_path(document, path))
157
+ end
158
+
159
+ file_system = ::Metanorma::Plugin::Lutaml::Liquid::LocalFileSystem
160
+ .new(include_paths, ["%s.liquid", "_%s.liquid", "_%s.adoc"])
161
+
162
+ parsed_template = template(lines)
163
+ parsed_template.registers[:file_system] = file_system
164
+
165
+ all_items.map do |item|
166
+ parsed_template.assigns[context_name] = item[:liquid_drop]
167
+ parsed_template.assigns["ordered_schemas"] = reorder_schemas(
168
+ item[:liquid_drop], options
169
+ )
170
+ parsed_template.assigns["schemas_order"] =
171
+ options["selected_schemas"]
172
+ parsed_template.render
173
+ end.flatten
174
+ rescue StandardError => e
175
+ ::Metanorma::Util.log(
176
+ "[#{self.class.name}] Failed to parse LutaML block: #{e.message}",
177
+ :error,
178
+ )
179
+ raise e
180
+ end
181
+
182
+ def process_options(document, options)
183
+ if (config_yaml_path = options.delete("config_yaml"))
184
+ config = read_config_yaml_file(document, config_yaml_path)
185
+ if config["selected_schemas"]
186
+ options["selected_schemas"] =
187
+ config["selected_schemas"]
188
+ end
189
+ end
190
+ options
191
+ end
192
+
193
+ def read_config_yaml_file(document, file_path) # rubocop:disable Metrics/MethodLength
194
+ return {} unless file_path
195
+
196
+ relative_file_path = Utils.relative_file_path(document, file_path)
197
+ config_yaml = YAML.safe_load(
198
+ File.read(relative_file_path, encoding: "UTF-8"),
199
+ )
200
+
201
+ return {} unless config_yaml["schemas"]
202
+
203
+ unless config_yaml["schemas"].is_a?(Hash)
204
+ raise StandardError,
205
+ "[lutaml_express_liquid] attribute `config_yaml` must " \
206
+ "point to a YAML file with the `schemas` key as a hash."
207
+ end
208
+
209
+ { "selected_schemas" => config_yaml["schemas"].keys }
210
+ end
211
+
212
+ def parse_options(options_string)
213
+ options_string
214
+ .to_s
215
+ .scan(/,\s*([^=]+?)=(\s*[^,]+)/)
216
+ .to_h { |elem| elem.map(&:strip) }
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
@@ -490,7 +490,9 @@ module Metanorma
490
490
  def serialize_klass_drop_by_name(xmi_path, name, document = nil,
491
491
  guidance = nil)
492
492
  parser, uml_doc = build_uml_document(xmi_path, document)
493
- raw_klass = find_packaged_klass(parser.xmi_index, name)
493
+ root_model_name = parser.xmi_root_model.model.name
494
+ raw_klass = find_packaged_klass(parser.xmi_index, name,
495
+ root_model_name: root_model_name)
494
496
  warn "Class not found for name: #{name}" if raw_klass.nil?
495
497
  klass = raw_klass && find_uml_node_by_xmi_id(
496
498
  uml_doc, raw_klass.id, :classes
@@ -550,11 +552,14 @@ guidance = nil)
550
552
  nil
551
553
  end
552
554
 
553
- def find_packaged_klass(index, path)
554
- segments = path.split("::")
555
+ def find_packaged_klass(index, path, root_model_name: nil)
556
+ segments = path.split("::").reject(&:empty?)
557
+ if root_model_name && segments.first == root_model_name
558
+ segments.shift
559
+ end
555
560
  if segments.one?
556
561
  index.find_packaged_by_name_and_types(
557
- path, ["uml:Class", "uml:AssociationClass"]
562
+ segments.first, ["uml:Class", "uml:AssociationClass"]
558
563
  )
559
564
  else
560
565
  find_packaged_klass_by_path(index, segments)
@@ -563,20 +568,25 @@ guidance = nil)
563
568
 
564
569
  def find_packaged_klass_by_path(index, segments)
565
570
  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
 
571
- # Verify the path by walking up the parent chain
572
- current = klass
573
- segments.reverse_each do |pkg_name|
572
+ candidates = ["uml:Class", "uml:AssociationClass"]
573
+ .flat_map { |t| index.packaged_elements_of_type(t) }
574
+ .select { |e| e.name == klass_name }
575
+
576
+ candidates.find do |klass|
577
+ match_parent_chain?(index, klass, segments)
578
+ end
579
+ end
580
+
581
+ def match_parent_chain?(index, element, parent_segments)
582
+ current = element
583
+ parent_segments.reverse_each do |pkg_name|
574
584
  parent = index.find_parent(current.id)
575
- return unless parent && parent.name == pkg_name
585
+ return false unless parent && parent.name == pkg_name
576
586
 
577
587
  current = parent
578
588
  end
579
- klass
589
+ true
580
590
  end
581
591
 
582
592
  def find_packaged_enum(index, name)
@@ -1,21 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "liquid"
4
- require "asciidoctor"
5
- require "asciidoctor/reader"
6
- require "lutaml"
7
- require "metanorma/plugin/lutaml/utils"
8
- require "metanorma/plugin/lutaml/asciidoctor/preprocessor"
9
3
  require "metanorma/plugin/lutaml/express_remarks_decorator"
10
4
 
11
5
  module Metanorma
12
6
  module Plugin
13
7
  module Lutaml
14
- # Class for processing Lutaml files
15
- class LutamlPreprocessor < ::Asciidoctor::Extensions::Preprocessor
16
- include Utils
17
-
18
- REMARKS_ATTRIBUTE = "remarks"
8
+ # Preprocessor for EXPRESS schema formats (lutaml, lutaml_express,
9
+ # lutaml_express_liquid). Parses EXPRESS files via the lutaml/expressir
10
+ # gems, decorates remarks with relative path resolution, and renders
11
+ # Liquid templates with the EXPRESS-specific Liquid environment.
12
+ class LutamlPreprocessor < BasePreprocessor
19
13
  EXPRESS_PREPROCESSOR_REGEX = %r{
20
14
  ^ # Start of line
21
15
  \[ # Opening bracket
@@ -30,117 +24,38 @@ module Metanorma
30
24
  \] # Closing bracket
31
25
  }x
32
26
 
33
- def process(document, reader) # rubocop:disable Metrics/MethodLength
34
- r = Asciidoctor::PreprocessorNoIfdefsReader.new(document,
35
- reader.lines)
36
- input_lines = r.readlines.to_enum
37
-
38
- express_indexes = Utils.parse_document_express_indexes(
39
- document,
40
- input_lines,
41
- )
42
-
43
- result_content = process_input_lines(
44
- document: document,
45
- input_lines: input_lines,
46
- express_indexes: express_indexes,
47
- )
48
-
49
- Asciidoctor::PreprocessorNoIfdefsReader.new(document, result_content)
50
- end
51
-
52
27
  protected
53
28
 
54
29
  def lutaml_liquid?(line)
55
30
  line.match(EXPRESS_PREPROCESSOR_REGEX)
56
31
  end
57
32
 
58
- def load_express_lutaml_file(document, file_path)
59
- ::Lutaml::Parser.parse(
60
- File.new(
61
- Utils.relative_file_path(document, file_path),
62
- encoding: "UTF-8",
63
- ),
64
- )
65
- end
66
-
67
- private
33
+ def load_lutaml_file(document, file_path, _options)
34
+ full_path = Utils.relative_file_path(document, file_path)
68
35
 
69
- def process_input_lines(document:, input_lines:, express_indexes:)
70
- result = []
71
- loop do
72
- result.push(
73
- *process_text_blocks(document, input_lines, express_indexes),
74
- )
36
+ file = File.new(full_path, encoding: "UTF-8")
37
+ if full_path.end_with?(".exp")
38
+ ::Lutaml::Express::Parsers::Exp.parse(file)
39
+ else
40
+ ::Lutaml::Uml::Parsers::Dsl.parse(file)
75
41
  end
76
- result
77
- end
78
-
79
- def process_text_blocks(document, input_lines, express_indexes) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength
80
- line = input_lines.next
81
- block_header_match = lutaml_liquid?(line)
82
-
83
- return [line] if block_header_match.nil?
84
-
85
- index_names = block_header_match[:index_names].split(";").map(&:strip)
86
- context_name = block_header_match[:context_name].strip
87
-
88
- options = (block_header_match[:options] &&
89
- parse_options(block_header_match[:options].to_s.strip)) || {}
90
-
91
- end_mark = input_lines.next
92
-
93
- render_liquid_template(
94
- document: document,
95
- lines: extract_block_lines(input_lines, end_mark),
96
- index_names: index_names,
97
- context_name: context_name,
98
- options: options,
99
- indexes: express_indexes,
100
- )
101
42
  end
102
43
 
103
- def extract_block_lines(input_lines, end_mark)
104
- block = []
105
- while (block_line = input_lines.next) != end_mark
106
- block.push(block_line)
107
- end
108
- block
44
+ def index_type_name
45
+ "EXPRESS"
109
46
  end
110
47
 
111
- def gather_context_liquid_items( # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/ParameterLists
112
- index_names:, document:, indexes:, options: {}
113
- )
114
- index_names.map do |path| # rubocop:disable Metrics/BlockLength
115
- if indexes[path] && indexes[path][:model]
116
- repo = indexes[path][:model]
117
- repo = update_repo(options, repo)
118
- indexes[path][:liquid_drop] ||= repo.to_liquid
119
- else
120
- full_path = Utils.relative_file_path(document, path)
121
- unless File.file?(full_path)
122
- raise StandardError.new(
123
- "Unable to load EXPRESS index for `#{path}`, " \
124
- "please define it at `:lutaml-express-index:` or specify " \
125
- "the full path.",
126
- )
127
- end
128
- repo = load_express_lutaml_file(document, path)
129
- repo = update_repo(options, repo)
130
- indexes[path] = {
131
- liquid_drop: repo.to_liquid,
132
- }
133
- end
134
-
135
- indexes[path]
136
- end
48
+ def index_missing_message(path)
49
+ "Unable to load EXPRESS index for `#{path}`, " \
50
+ "please define it at `:lutaml-express-index:` or specify " \
51
+ "the full path."
137
52
  end
138
53
 
139
54
  def update_repo(options, repo)
140
- # Unwrap repo if it's a cache
141
- repo = repo.content if repo.is_a? Expressir::Model::Cache
55
+ repo = repo.content if repo.is_a?(Expressir::Model::Cache)
56
+ return repo unless repo.is_a?(Expressir::Model::Repository) ||
57
+ repo.is_a?(Expressir::Model::ExpFile)
142
58
 
143
- # Process each schema
144
59
  repo.schemas.each do |schema|
145
60
  options["relative_path_prefix"] =
146
61
  relative_path_prefix(options, schema)
@@ -150,6 +65,25 @@ module Metanorma
150
65
  repo
151
66
  end
152
67
 
68
+ def template(lines)
69
+ ::Liquid::Template.parse(
70
+ lines.join("\n"),
71
+ environment: create_liquid_environment,
72
+ )
73
+ end
74
+
75
+ def reorder_schemas(repo_liquid, options)
76
+ return repo_liquid.schemas unless options["selected_schemas"]
77
+
78
+ options["selected_schemas"].filter_map do |schema_name|
79
+ repo_liquid.schemas.find do |schema|
80
+ schema.id == schema_name || schema.file_basename == schema_name
81
+ end
82
+ end
83
+ end
84
+
85
+ private
86
+
153
87
  def update_remarks(model, options)
154
88
  model.remarks = decorate_remarks(options, model.remarks)
155
89
  model.remark_items&.each do |ri|
@@ -157,127 +91,29 @@ module Metanorma
157
91
  end
158
92
 
159
93
  model.children.each do |child|
160
- if child.respond_to?(:remarks) && child.respond_to?(:remark_items)
161
- update_remarks(child, options)
162
- end
94
+ next unless child.is_a?(Expressir::Model::ModelElement)
95
+
96
+ update_remarks(child, options)
163
97
  end
164
98
  end
165
99
 
166
100
  def relative_path_prefix(options, model)
167
- return nil if options.nil? || options["document"].nil?
101
+ return if options.nil? || options["document"].nil?
168
102
 
169
103
  document = options["document"]
170
104
  file_path = File.dirname(model.file)
171
105
  docfile_directory = File.dirname(
172
106
  document.attributes["docfile"] || ".",
173
107
  )
174
- document
175
- .path_resolver
176
- .system_path(file_path, docfile_directory)
108
+ document.path_resolver.system_path(file_path, docfile_directory)
177
109
  end
178
110
 
179
111
  def decorate_remarks(options, remarks)
180
112
  return [] unless remarks
181
113
 
182
114
  remarks.map do |remark|
183
- ::Metanorma::Plugin::Lutaml::ExpressRemarksDecorator
184
- .call(remark, options)
185
- end
186
- end
187
-
188
- def read_config_yaml_file(document, file_path) # rubocop:disable Metrics/MethodLength
189
- return {} if file_path.nil?
190
-
191
- relative_file_path = Utils.relative_file_path(document, file_path)
192
- config_yaml = YAML.safe_load(
193
- File.read(relative_file_path, encoding: "UTF-8"),
194
- )
195
-
196
- return {} unless config_yaml["schemas"]
197
-
198
- unless config_yaml["schemas"].is_a?(Hash)
199
- raise StandardError.new(
200
- "[lutaml_express_liquid] attribute `config_yaml` must point " \
201
- "to a YAML file that has the `schemas` key containing a hash.",
202
- )
115
+ ExpressRemarksDecorator.call(remark, options)
203
116
  end
204
-
205
- { "selected_schemas" => config_yaml["schemas"].keys }
206
- end
207
-
208
- def render_liquid_template(document:, lines:, context_name:, # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/ParameterLists
209
- index_names:, options:, indexes:)
210
- # Process options and configuration
211
- options = process_options(document, options)
212
-
213
- # Get all context items in one go
214
- all_items = gather_context_liquid_items(
215
- index_names: index_names, document: document, indexes: indexes,
216
- options: options.merge("document" => document)
217
- )
218
-
219
- # Setup include paths for liquid templates
220
- include_paths = [Utils.relative_file_path(document, "")]
221
- options["include_path"]&.split(",")&.each do |path|
222
- # resolve include_path relative to the document
223
- include_paths.push(Utils.relative_file_path(document, path))
224
- end
225
-
226
- file_system = ::Metanorma::Plugin::Lutaml::Liquid::LocalFileSystem
227
- .new(include_paths, ["%s.liquid", "_%s.liquid", "_%s.adoc"])
228
-
229
- # Parse template once outside the loop
230
- template = ::Liquid::Template
231
- .parse(lines.join("\n"), environment: create_liquid_environment)
232
- template.registers[:file_system] = file_system
233
-
234
- # Render for each item
235
- all_items.map do |item|
236
- template.assigns[context_name] = item[:liquid_drop]
237
- template.assigns["ordered_schemas"] = reorder_schemas(
238
- item[:liquid_drop], options
239
- )
240
- template.assigns["schemas_order"] = options["selected_schemas"]
241
- template.render
242
- end.flatten
243
- rescue StandardError => e
244
- ::Metanorma::Util
245
- .log("[LutamlPreprocessor] Failed to parse LutaML block: " \
246
- "#{e.message}", :error)
247
- raise e
248
- end
249
-
250
- def reorder_schemas(repo_liquid, options)
251
- return repo_liquid.schemas unless options["selected_schemas"]
252
-
253
- ordered_schemas = []
254
- options["selected_schemas"].each do |schema_name|
255
- ordered_schema = repo_liquid.schemas.find do |schema|
256
- schema.id == schema_name || schema.file_basename == schema_name
257
- end
258
- ordered_schemas.push(ordered_schema)
259
- end
260
-
261
- ordered_schemas
262
- end
263
-
264
- def process_options(document, options)
265
- # Process config file if specified
266
- if (config_yaml_path = options.delete("config_yaml"))
267
- config = read_config_yaml_file(document, config_yaml_path)
268
- if config["selected_schemas"]
269
- options["selected_schemas"] =
270
- config["selected_schemas"]
271
- end
272
- end
273
- options
274
- end
275
-
276
- def parse_options(options_string)
277
- options_string
278
- .to_s
279
- .scan(/,\s*([^=]+?)=(\s*[^,]+)/)
280
- .to_h { |elem| elem.map(&:strip) }
281
117
  end
282
118
  end
283
119
  end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/xml/parsers/xsd"
4
+
5
+ module Metanorma
6
+ module Plugin
7
+ module Lutaml
8
+ # Preprocessor for XSD (XML Schema Definition) files. Parses XSD via
9
+ # lutaml-model's XSD parser and exposes the schema object to Liquid
10
+ # templates.
11
+ #
12
+ # Caching: parsed XSD results are cached at two levels:
13
+ # - Class-level (@@xsd_cache) persists across document invocations
14
+ # - Document-level (document.attributes["lutaml_xsd_cache"]) within a
15
+ # single document's processing
16
+ class LutamlXsdPreprocessor < BasePreprocessor
17
+ XSD_PREPROCESSOR_REGEX = %r{
18
+ ^ # Start of line
19
+ \[ # Opening bracket
20
+ (?:\blutaml_xsd\b) # lutaml_xsd
21
+ , # Comma separator
22
+ (?<index_names>[^,]+)? # Optional index names
23
+ ,? # Optional comma
24
+ (?<context_name>[^,]+)? # Optional context name
25
+ (?<options>,.*)? # Optional options
26
+ \] # Closing bracket
27
+ }x
28
+
29
+ def initialize(_config = {})
30
+ super
31
+ @@xsd_cache ||= {}
32
+ end
33
+
34
+ protected
35
+
36
+ def lutaml_liquid?(line)
37
+ line.match(XSD_PREPROCESSOR_REGEX)
38
+ end
39
+
40
+ def load_lutaml_file(document, file_path, options)
41
+ full_path = Utils.relative_file_path(document, file_path)
42
+ location = xsd_location(full_path, options)
43
+ cache_key = [full_path, location]
44
+
45
+ cached = document_cache_entry(document, cache_key)
46
+ return cached if cached
47
+
48
+ result = @@xsd_cache[cache_key] ||=
49
+ parse_xsd_file(full_path, location)
50
+
51
+ set_document_cache_entry(document, cache_key, result)
52
+ result
53
+ end
54
+
55
+ def index_type_name
56
+ "XSD"
57
+ end
58
+
59
+ def index_missing_message(path)
60
+ "Unable to load XSD file for `#{path}`, please specify the full path."
61
+ end
62
+
63
+ def template(lines)
64
+ # XSD templates use double-newline joins to produce Asciidoctor
65
+ # paragraph breaks (single newlines are treated as continuation).
66
+ ::Liquid::Template.parse(
67
+ lines.join("\n\n"),
68
+ environment: create_liquid_environment,
69
+ )
70
+ end
71
+
72
+ private
73
+
74
+ def parse_xsd_file(full_path, location)
75
+ File.open(full_path, "r:UTF-8") do |file|
76
+ ::Lutaml::Xml::Parsers::Xsd.parse(file, location: location)
77
+ end
78
+ end
79
+
80
+ def xsd_location(full_path, options)
81
+ options["location"] || File.dirname(full_path)
82
+ end
83
+
84
+ def document_cache_entry(document, cache_key)
85
+ document.attributes["lutaml_xsd_cache"]&.[](cache_key)
86
+ end
87
+
88
+ def set_document_cache_entry(document, cache_key, result)
89
+ document.attributes["lutaml_xsd_cache"] ||= {}
90
+ document.attributes["lutaml_xsd_cache"][cache_key] = result
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -183,8 +183,7 @@ module Metanorma
183
183
  end
184
184
 
185
185
  def load_express_repo_from_cache(path)
186
- ::Lutaml::Parser
187
- .parse(File.new(path), ::Lutaml::Parser::EXPRESS_CACHE_PARSE_TYPE)
186
+ ::Lutaml::Express::Parsers::Exp.parse_cache(path)
188
187
  end
189
188
 
190
189
  def save_express_repo_to_cache(path, repository, document)
@@ -202,10 +201,8 @@ module Metanorma
202
201
  end
203
202
 
204
203
  def load_express_from_folder(folder)
205
- files = Dir["#{folder}/*.exp"].map do |nested_path|
206
- File.new(nested_path, encoding: "UTF-8")
207
- end
208
- ::Lutaml::Parser.parse(files)
204
+ file_paths = Dir["#{folder}/*.exp"]
205
+ ::Expressir::Express::Parser.from_files(file_paths)
209
206
  end
210
207
 
211
208
  # TODO: Refactor this using Suma::SchemaConfig
@@ -220,16 +217,12 @@ module Metanorma
220
217
  schema_yaml_base_path = schema_yaml_base_path + root_schema_path
221
218
  end
222
219
 
223
- files_to_load = yaml_content["schemas"].map do |key, value|
224
- # If there is no path: set for a schema, we assume it uses the
225
- # schema name as the #{filename}.exp.
220
+ file_paths = yaml_content["schemas"].map do |key, value|
226
221
  schema_path = Pathname.new(value["path"] || "#{key}.exp")
227
-
228
- real_schema_path = schema_yaml_base_path + schema_path
229
- File.new(real_schema_path.cleanpath.to_s, encoding: "UTF-8")
222
+ (schema_yaml_base_path + schema_path).cleanpath.to_s
230
223
  end
231
224
 
232
- ::Lutaml::Parser.parse(files_to_load)
225
+ ::Expressir::Express::Parser.from_files(file_paths)
233
226
  end
234
227
 
235
228
  def parse_document_express_indexes(document, input_lines) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
@@ -1,7 +1,7 @@
1
1
  module Metanorma
2
2
  module Plugin
3
3
  module Lutaml
4
- VERSION = "0.7.39".freeze
4
+ VERSION = "0.7.40".freeze
5
5
  end
6
6
  end
7
7
  end
@@ -3,7 +3,9 @@ require "metanorma/plugin/lutaml/config"
3
3
  require "metanorma/plugin/lutaml/json2_text_preprocessor"
4
4
  require "metanorma/plugin/lutaml/yaml2_text_preprocessor"
5
5
  require "metanorma/plugin/lutaml/data2_text_preprocessor"
6
+ require "metanorma/plugin/lutaml/base_preprocessor"
6
7
  require "metanorma/plugin/lutaml/lutaml_preprocessor"
8
+ require "metanorma/plugin/lutaml/lutaml_xsd_preprocessor"
7
9
  require "metanorma/plugin/lutaml/lutaml_uml_datamodel_description_preprocessor"
8
10
  require "metanorma/plugin/lutaml/lutaml_ea_xmi_preprocessor"
9
11
  require "metanorma/plugin/lutaml/lutaml_xmi_uml_preprocessor"
@@ -26,14 +26,15 @@ Gem::Specification.new do |spec|
26
26
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
27
  spec.require_paths = ["lib"]
28
28
 
29
- spec.required_ruby_version = ">= 2.7.0" # rubocop:disable Gemspec/RequiredRubyVersion
29
+ spec.required_ruby_version = ">= 3.0.0" # rubocop:disable Gemspec/RequiredRubyVersion
30
30
 
31
31
  spec.add_dependency "asciidoctor"
32
32
  spec.add_dependency "coradoc", "~> 1.1.8"
33
- spec.add_dependency "expressir", "~> 2.3", ">= 2.3.4"
33
+ spec.add_dependency "expressir", "~> 2.3", ">= 2.3.5"
34
34
  spec.add_dependency "isodoc"
35
35
  spec.add_dependency "liquid"
36
36
  spec.add_dependency "lutaml", "~> 0.10", ">= 0.10.12"
37
+ spec.add_dependency "lutaml-model", "~> 0.8.4"
37
38
  spec.add_dependency "ogc-gml", "~> 1.1"
38
39
  spec.add_dependency "relaton-cli"
39
40
 
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.39
4
+ version: 0.7.40
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-12 00:00:00.000000000 Z
11
+ date: 2026-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: asciidoctor
@@ -47,7 +47,7 @@ dependencies:
47
47
  version: '2.3'
48
48
  - - ">="
49
49
  - !ruby/object:Gem::Version
50
- version: 2.3.4
50
+ version: 2.3.5
51
51
  type: :runtime
52
52
  prerelease: false
53
53
  version_requirements: !ruby/object:Gem::Requirement
@@ -57,7 +57,7 @@ dependencies:
57
57
  version: '2.3'
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: 2.3.4
60
+ version: 2.3.5
61
61
  - !ruby/object:Gem::Dependency
62
62
  name: isodoc
63
63
  requirement: !ruby/object:Gem::Requirement
@@ -106,6 +106,20 @@ dependencies:
106
106
  - - ">="
107
107
  - !ruby/object:Gem::Version
108
108
  version: 0.10.12
109
+ - !ruby/object:Gem::Dependency
110
+ name: lutaml-model
111
+ requirement: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: 0.8.4
116
+ type: :runtime
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - "~>"
121
+ - !ruby/object:Gem::Version
122
+ version: 0.8.4
109
123
  - !ruby/object:Gem::Dependency
110
124
  name: ogc-gml
111
125
  requirement: !ruby/object:Gem::Requirement
@@ -160,9 +174,11 @@ files:
160
174
  - docs/usages/json_yaml.adoc
161
175
  - docs/usages/lutaml-gml.adoc
162
176
  - docs/usages/lutaml-uml.adoc
177
+ - docs/usages/lutaml-xsd.adoc
163
178
  - docs/usages/xmi_to_uml.adoc
164
179
  - lib/metanorma-plugin-lutaml.rb
165
180
  - lib/metanorma/plugin/lutaml/asciidoctor/preprocessor.rb
181
+ - lib/metanorma/plugin/lutaml/base_preprocessor.rb
166
182
  - lib/metanorma/plugin/lutaml/base_structured_text_preprocessor.rb
167
183
  - lib/metanorma/plugin/lutaml/config.rb
168
184
  - lib/metanorma/plugin/lutaml/config/guidance.rb
@@ -213,6 +229,7 @@ files:
213
229
  - lib/metanorma/plugin/lutaml/lutaml_table_inline_macro.rb
214
230
  - lib/metanorma/plugin/lutaml/lutaml_uml_datamodel_description_preprocessor.rb
215
231
  - lib/metanorma/plugin/lutaml/lutaml_xmi_uml_preprocessor.rb
232
+ - lib/metanorma/plugin/lutaml/lutaml_xsd_preprocessor.rb
216
233
  - lib/metanorma/plugin/lutaml/parse_error.rb
217
234
  - lib/metanorma/plugin/lutaml/source_extractor.rb
218
235
  - lib/metanorma/plugin/lutaml/utils.rb
@@ -232,7 +249,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
232
249
  requirements:
233
250
  - - ">="
234
251
  - !ruby/object:Gem::Version
235
- version: 2.7.0
252
+ version: 3.0.0
236
253
  required_rubygems_version: !ruby/object:Gem::Requirement
237
254
  requirements:
238
255
  - - ">="