From eca8de8e72f72483f41a1d16d3e85e3e72cf4251 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Wed, 13 May 2026 20:03:45 +0800 Subject: [PATCH 1/5] refactor: extract BasePreprocessor with Template Method pattern Move shared preprocessor logic (block parsing, Liquid rendering, index lookup) into BasePreprocessor. LutamlPreprocessor now only implements format-specific hooks: load_lutaml_file dispatches to Express::Parsers::Exp or Uml::Parsers::Dsl, update_repo handles ExpFile/Repository unwrapping and remark decoration, template uses the custom Liquid environment with keyiterator tag and filters. --- .../plugin/lutaml/base_preprocessor.rb | 221 +++++++++++++++ .../plugin/lutaml/lutaml_preprocessor.rb | 256 ++++-------------- 2 files changed, 267 insertions(+), 210 deletions(-) create mode 100644 lib/metanorma/plugin/lutaml/base_preprocessor.rb diff --git a/lib/metanorma/plugin/lutaml/base_preprocessor.rb b/lib/metanorma/plugin/lutaml/base_preprocessor.rb new file mode 100644 index 0000000..8ea9f05 --- /dev/null +++ b/lib/metanorma/plugin/lutaml/base_preprocessor.rb @@ -0,0 +1,221 @@ +# frozen_string_literal: true + +require "liquid" +require "asciidoctor" +require "asciidoctor/reader" +require "metanorma/plugin/lutaml/utils" +require "metanorma/plugin/lutaml/asciidoctor/preprocessor" + +module Metanorma + module Plugin + module Lutaml + # Base preprocessor for LutaML format-specific preprocessors. + # + # Subclasses must implement: + # - #lutaml_liquid?(line) — match the macro header line + # - #load_lutaml_file(document, file_path, options) + # parse format-specific input + # + # Subclasses may override: + # - #index_type_name — human-readable format name for error messages + # - #update_repo(options, repo) — transform parsed repo before rendering + # - #template(lines) — parse Liquid template lines + # - #reorder_schemas(repo_liquid, options) — reorder/filter schemas + class BasePreprocessor < ::Asciidoctor::Extensions::Preprocessor + include Utils + + def process(document, reader) + input_lines = Asciidoctor::PreprocessorNoIfdefsReader + .new(document, reader.lines).readlines.to_enum + + express_indexes = Utils.parse_document_express_indexes( + document, input_lines + ) + + result_content = process_input_lines( + document: document, + input_lines: input_lines, + express_indexes: express_indexes, + ) + + Asciidoctor::PreprocessorNoIfdefsReader.new(document, result_content) + end + + protected + + def load_lutaml_file(_document, _file_path, _options) + raise NotImplementedError, + "#{self.class}#load_lutaml_file must be implemented" + end + + def lutaml_liquid?(_line) + raise NotImplementedError, + "#{self.class}#lutaml_liquid? must be implemented" + end + + def index_type_name + raise NotImplementedError, + "#{self.class}#index_type_name must be implemented" + end + + def update_repo(_options, repo) + repo + end + + def template(lines) + ::Liquid::Template.parse(lines.join("\n")) + end + + def reorder_schemas(repo_liquid, _options) + repo_liquid + end + + def index_missing_message(path) + "Unable to load #{index_type_name} file for `#{path}`, " \ + "please specify the full path." + end + + private + + def process_input_lines(document:, input_lines:, express_indexes:) + result = [] + loop do + result.push( + *process_text_blocks(document, input_lines, express_indexes), + ) + end + result + end + + def process_text_blocks(document, input_lines, express_indexes) # rubocop:disable Metrics/AbcSize + line = input_lines.next + block_header_match = lutaml_liquid?(line) + + return [line] unless block_header_match + + index_names = block_header_match[:index_names].split(";").map(&:strip) + context_name = block_header_match[:context_name].strip + options = (block_header_match[:options] && + parse_options(block_header_match[:options].to_s.strip)) || {} + + end_mark = input_lines.next + + render_liquid_template( + document: document, + lines: extract_block_lines(input_lines, end_mark), + index_names: index_names, + context_name: context_name, + options: options, + indexes: express_indexes, + ) + end + + def extract_block_lines(input_lines, end_mark) + block = [] + while (block_line = input_lines.next) != end_mark + block.push(block_line) + end + block + end + + # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/ParameterLists + def gather_context_liquid_items(index_names:, document:, + indexes:, options: {}) + index_names.map do |path| + if indexes[path] && indexes[path][:model] + repo = indexes[path][:model] + repo = update_repo(options, repo) + indexes[path][:liquid_drop] ||= repo.to_liquid + else + full_path = Utils.relative_file_path(document, path) + unless File.file?(full_path) + raise StandardError, index_missing_message(path) + end + + repo = load_lutaml_file(document, path, options) + repo = update_repo(options, repo) + indexes[path] = { liquid_drop: repo.to_liquid } + end + + indexes[path] + end + end + # rubocop:enable Metrics/AbcSize,Metrics/MethodLength,Metrics/ParameterLists + + def render_liquid_template(document:, lines:, context_name:, # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/ParameterLists + index_names:, options:, indexes:) + options = process_options(document, options) + + all_items = gather_context_liquid_items( + index_names: index_names, document: document, indexes: indexes, + options: options.merge("document" => document) + ) + + include_paths = [Utils.relative_file_path(document, "")] + options["include_path"]&.split(",")&.each do |path| + include_paths.push(Utils.relative_file_path(document, path)) + end + + file_system = ::Metanorma::Plugin::Lutaml::Liquid::LocalFileSystem + .new(include_paths, ["%s.liquid", "_%s.liquid", "_%s.adoc"]) + + parsed_template = template(lines) + parsed_template.registers[:file_system] = file_system + + all_items.map do |item| + parsed_template.assigns[context_name] = item[:liquid_drop] + parsed_template.assigns["ordered_schemas"] = reorder_schemas( + item[:liquid_drop], options + ) + parsed_template.assigns["schemas_order"] = + options["selected_schemas"] + parsed_template.render + end.flatten + rescue StandardError => e + ::Metanorma::Util.log( + "[#{self.class.name}] Failed to parse LutaML block: #{e.message}", + :error, + ) + raise e + end + + def process_options(document, options) + if (config_yaml_path = options.delete("config_yaml")) + config = read_config_yaml_file(document, config_yaml_path) + if config["selected_schemas"] + options["selected_schemas"] = + config["selected_schemas"] + end + end + options + end + + def read_config_yaml_file(document, file_path) # rubocop:disable Metrics/MethodLength + return {} unless file_path + + relative_file_path = Utils.relative_file_path(document, file_path) + config_yaml = YAML.safe_load( + File.read(relative_file_path, encoding: "UTF-8"), + ) + + return {} unless config_yaml["schemas"] + + unless config_yaml["schemas"].is_a?(Hash) + raise StandardError, + "[lutaml_express_liquid] attribute `config_yaml` must " \ + "point to a YAML file with the `schemas` key as a hash." + end + + { "selected_schemas" => config_yaml["schemas"].keys } + end + + def parse_options(options_string) + options_string + .to_s + .scan(/,\s*([^=]+?)=(\s*[^,]+)/) + .to_h { |elem| elem.map(&:strip) } + end + end + end + end +end diff --git a/lib/metanorma/plugin/lutaml/lutaml_preprocessor.rb b/lib/metanorma/plugin/lutaml/lutaml_preprocessor.rb index 70ab6a1..b919912 100644 --- a/lib/metanorma/plugin/lutaml/lutaml_preprocessor.rb +++ b/lib/metanorma/plugin/lutaml/lutaml_preprocessor.rb @@ -1,21 +1,15 @@ # frozen_string_literal: true -require "liquid" -require "asciidoctor" -require "asciidoctor/reader" -require "lutaml" -require "metanorma/plugin/lutaml/utils" -require "metanorma/plugin/lutaml/asciidoctor/preprocessor" require "metanorma/plugin/lutaml/express_remarks_decorator" module Metanorma module Plugin module Lutaml - # Class for processing Lutaml files - class LutamlPreprocessor < ::Asciidoctor::Extensions::Preprocessor - include Utils - - REMARKS_ATTRIBUTE = "remarks" + # Preprocessor for EXPRESS schema formats (lutaml, lutaml_express, + # lutaml_express_liquid). Parses EXPRESS files via the lutaml/expressir + # gems, decorates remarks with relative path resolution, and renders + # Liquid templates with the EXPRESS-specific Liquid environment. + class LutamlPreprocessor < BasePreprocessor EXPRESS_PREPROCESSOR_REGEX = %r{ ^ # Start of line \[ # Opening bracket @@ -30,117 +24,38 @@ class LutamlPreprocessor < ::Asciidoctor::Extensions::Preprocessor \] # Closing bracket }x - def process(document, reader) # rubocop:disable Metrics/MethodLength - r = Asciidoctor::PreprocessorNoIfdefsReader.new(document, - reader.lines) - input_lines = r.readlines.to_enum - - express_indexes = Utils.parse_document_express_indexes( - document, - input_lines, - ) - - result_content = process_input_lines( - document: document, - input_lines: input_lines, - express_indexes: express_indexes, - ) - - Asciidoctor::PreprocessorNoIfdefsReader.new(document, result_content) - end - protected def lutaml_liquid?(line) line.match(EXPRESS_PREPROCESSOR_REGEX) end - def load_express_lutaml_file(document, file_path) - ::Lutaml::Parser.parse( - File.new( - Utils.relative_file_path(document, file_path), - encoding: "UTF-8", - ), - ) - end - - private + def load_lutaml_file(document, file_path, _options) + full_path = Utils.relative_file_path(document, file_path) - def process_input_lines(document:, input_lines:, express_indexes:) - result = [] - loop do - result.push( - *process_text_blocks(document, input_lines, express_indexes), - ) + file = File.new(full_path, encoding: "UTF-8") + if full_path.end_with?(".exp") + ::Lutaml::Express::Parsers::Exp.parse(file) + else + ::Lutaml::Uml::Parsers::Dsl.parse(file) end - result - end - - def process_text_blocks(document, input_lines, express_indexes) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength - line = input_lines.next - block_header_match = lutaml_liquid?(line) - - return [line] if block_header_match.nil? - - index_names = block_header_match[:index_names].split(";").map(&:strip) - context_name = block_header_match[:context_name].strip - - options = (block_header_match[:options] && - parse_options(block_header_match[:options].to_s.strip)) || {} - - end_mark = input_lines.next - - render_liquid_template( - document: document, - lines: extract_block_lines(input_lines, end_mark), - index_names: index_names, - context_name: context_name, - options: options, - indexes: express_indexes, - ) end - def extract_block_lines(input_lines, end_mark) - block = [] - while (block_line = input_lines.next) != end_mark - block.push(block_line) - end - block + def index_type_name + "EXPRESS" end - def gather_context_liquid_items( # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/ParameterLists - index_names:, document:, indexes:, options: {} - ) - index_names.map do |path| # rubocop:disable Metrics/BlockLength - if indexes[path] && indexes[path][:model] - repo = indexes[path][:model] - repo = update_repo(options, repo) - indexes[path][:liquid_drop] ||= repo.to_liquid - else - full_path = Utils.relative_file_path(document, path) - unless File.file?(full_path) - raise StandardError.new( - "Unable to load EXPRESS index for `#{path}`, " \ - "please define it at `:lutaml-express-index:` or specify " \ - "the full path.", - ) - end - repo = load_express_lutaml_file(document, path) - repo = update_repo(options, repo) - indexes[path] = { - liquid_drop: repo.to_liquid, - } - end - - indexes[path] - end + def index_missing_message(path) + "Unable to load EXPRESS index for `#{path}`, " \ + "please define it at `:lutaml-express-index:` or specify " \ + "the full path." end def update_repo(options, repo) - # Unwrap repo if it's a cache - repo = repo.content if repo.is_a? Expressir::Model::Cache + repo = repo.content if repo.is_a?(Expressir::Model::Cache) + return repo unless repo.is_a?(Expressir::Model::Repository) || + repo.is_a?(Expressir::Model::ExpFile) - # Process each schema repo.schemas.each do |schema| options["relative_path_prefix"] = relative_path_prefix(options, schema) @@ -150,6 +65,25 @@ def update_repo(options, repo) repo end + def template(lines) + ::Liquid::Template.parse( + lines.join("\n"), + environment: create_liquid_environment, + ) + end + + def reorder_schemas(repo_liquid, options) + return repo_liquid.schemas unless options["selected_schemas"] + + options["selected_schemas"].filter_map do |schema_name| + repo_liquid.schemas.find do |schema| + schema.id == schema_name || schema.file_basename == schema_name + end + end + end + + private + def update_remarks(model, options) model.remarks = decorate_remarks(options, model.remarks) model.remark_items&.each do |ri| @@ -157,127 +91,29 @@ def update_remarks(model, options) end model.children.each do |child| - if child.respond_to?(:remarks) && child.respond_to?(:remark_items) - update_remarks(child, options) - end + next unless child.is_a?(Expressir::Model::ModelElement) + + update_remarks(child, options) end end def relative_path_prefix(options, model) - return nil if options.nil? || options["document"].nil? + return if options.nil? || options["document"].nil? document = options["document"] file_path = File.dirname(model.file) docfile_directory = File.dirname( document.attributes["docfile"] || ".", ) - document - .path_resolver - .system_path(file_path, docfile_directory) + document.path_resolver.system_path(file_path, docfile_directory) end def decorate_remarks(options, remarks) return [] unless remarks remarks.map do |remark| - ::Metanorma::Plugin::Lutaml::ExpressRemarksDecorator - .call(remark, options) - end - end - - def read_config_yaml_file(document, file_path) # rubocop:disable Metrics/MethodLength - return {} if file_path.nil? - - relative_file_path = Utils.relative_file_path(document, file_path) - config_yaml = YAML.safe_load( - File.read(relative_file_path, encoding: "UTF-8"), - ) - - return {} unless config_yaml["schemas"] - - unless config_yaml["schemas"].is_a?(Hash) - raise StandardError.new( - "[lutaml_express_liquid] attribute `config_yaml` must point " \ - "to a YAML file that has the `schemas` key containing a hash.", - ) + ExpressRemarksDecorator.call(remark, options) end - - { "selected_schemas" => config_yaml["schemas"].keys } - end - - def render_liquid_template(document:, lines:, context_name:, # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/ParameterLists - index_names:, options:, indexes:) - # Process options and configuration - options = process_options(document, options) - - # Get all context items in one go - all_items = gather_context_liquid_items( - index_names: index_names, document: document, indexes: indexes, - options: options.merge("document" => document) - ) - - # Setup include paths for liquid templates - include_paths = [Utils.relative_file_path(document, "")] - options["include_path"]&.split(",")&.each do |path| - # resolve include_path relative to the document - include_paths.push(Utils.relative_file_path(document, path)) - end - - file_system = ::Metanorma::Plugin::Lutaml::Liquid::LocalFileSystem - .new(include_paths, ["%s.liquid", "_%s.liquid", "_%s.adoc"]) - - # Parse template once outside the loop - template = ::Liquid::Template - .parse(lines.join("\n"), environment: create_liquid_environment) - template.registers[:file_system] = file_system - - # Render for each item - all_items.map do |item| - template.assigns[context_name] = item[:liquid_drop] - template.assigns["ordered_schemas"] = reorder_schemas( - item[:liquid_drop], options - ) - template.assigns["schemas_order"] = options["selected_schemas"] - template.render - end.flatten - rescue StandardError => e - ::Metanorma::Util - .log("[LutamlPreprocessor] Failed to parse LutaML block: " \ - "#{e.message}", :error) - raise e - end - - def reorder_schemas(repo_liquid, options) - return repo_liquid.schemas unless options["selected_schemas"] - - ordered_schemas = [] - options["selected_schemas"].each do |schema_name| - ordered_schema = repo_liquid.schemas.find do |schema| - schema.id == schema_name || schema.file_basename == schema_name - end - ordered_schemas.push(ordered_schema) - end - - ordered_schemas - end - - def process_options(document, options) - # Process config file if specified - if (config_yaml_path = options.delete("config_yaml")) - config = read_config_yaml_file(document, config_yaml_path) - if config["selected_schemas"] - options["selected_schemas"] = - config["selected_schemas"] - end - end - options - end - - def parse_options(options_string) - options_string - .to_s - .scan(/,\s*([^=]+?)=(\s*[^,]+)/) - .to_h { |elem| elem.map(&:strip) } end end end From 8209e27903a629e54e057e4d0d3458129c46e47f Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Wed, 13 May 2026 20:04:17 +0800 Subject: [PATCH 2/5] fix: migrate from removed Lutaml::Parser to format-specific parsers Lutaml::Parser was removed in lutaml 0.10. Replace all call sites: - Lutaml::Express::Parsers::Exp.parse_cache for cache loading - Expressir::Express::Parser.from_files for multi-file EXPRESS loading --- lib/metanorma/plugin/lutaml/utils.rb | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/lib/metanorma/plugin/lutaml/utils.rb b/lib/metanorma/plugin/lutaml/utils.rb index af55266..eb7270e 100644 --- a/lib/metanorma/plugin/lutaml/utils.rb +++ b/lib/metanorma/plugin/lutaml/utils.rb @@ -183,8 +183,7 @@ def load_express_repositories( # rubocop:disable Metrics/AbcSize,Metrics/Cycloma end def load_express_repo_from_cache(path) - ::Lutaml::Parser - .parse(File.new(path), ::Lutaml::Parser::EXPRESS_CACHE_PARSE_TYPE) + ::Lutaml::Express::Parsers::Exp.parse_cache(path) end def save_express_repo_to_cache(path, repository, document) @@ -202,10 +201,8 @@ def load_express_repo_from_path(document, path) end def load_express_from_folder(folder) - files = Dir["#{folder}/*.exp"].map do |nested_path| - File.new(nested_path, encoding: "UTF-8") - end - ::Lutaml::Parser.parse(files) + file_paths = Dir["#{folder}/*.exp"] + ::Expressir::Express::Parser.from_files(file_paths) end # TODO: Refactor this using Suma::SchemaConfig @@ -220,16 +217,12 @@ def load_express_from_index(_document, path) # rubocop:disable Metrics/AbcSize,M schema_yaml_base_path = schema_yaml_base_path + root_schema_path end - files_to_load = yaml_content["schemas"].map do |key, value| - # If there is no path: set for a schema, we assume it uses the - # schema name as the #{filename}.exp. + file_paths = yaml_content["schemas"].map do |key, value| schema_path = Pathname.new(value["path"] || "#{key}.exp") - - real_schema_path = schema_yaml_base_path + schema_path - File.new(real_schema_path.cleanpath.to_s, encoding: "UTF-8") + (schema_yaml_base_path + schema_path).cleanpath.to_s end - ::Lutaml::Parser.parse(files_to_load) + ::Expressir::Express::Parser.from_files(file_paths) end def parse_document_express_indexes(document, input_lines) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity From 93f2b9721bae6a52e38233de873b2a6bd7f8963b Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Wed, 13 May 2026 20:04:54 +0800 Subject: [PATCH 3/5] feat: add lutaml_xsd preprocessor with two-tier caching New LutamlXsdPreprocessor < BasePreprocessor handles [lutaml_xsd] blocks. Parses XSD files via lutaml-model, renders with Liquid, and caches parse results in both a class variable and per-document attributes to avoid re-parsing the same file across blocks or documents. --- README.adoc | 3 + docs/usages/lutaml-xsd.adoc | 94 +++++++++ lib/metanorma-plugin-lutaml.rb | 2 + .../plugin/lutaml/lutaml_xsd_preprocessor.rb | 95 +++++++++ metanorma-plugin-lutaml.gemspec | 5 +- .../lutaml/expected/xsd_person_fragments.xml | 13 ++ spec/fixtures/lutaml/xsd_schemas/person.xsd | 27 +++ .../lutaml/lutaml_xsd_preprocessor_spec.rb | 180 ++++++++++++++++++ spec/spec_helper.rb | 35 ++-- 9 files changed, 440 insertions(+), 14 deletions(-) create mode 100644 docs/usages/lutaml-xsd.adoc create mode 100644 lib/metanorma/plugin/lutaml/lutaml_xsd_preprocessor.rb create mode 100644 spec/fixtures/lutaml/expected/xsd_person_fragments.xml create mode 100644 spec/fixtures/lutaml/xsd_schemas/person.xsd create mode 100644 spec/metanorma/plugin/lutaml/lutaml_xsd_preprocessor_spec.rb diff --git a/README.adoc b/README.adoc index dc2f205..64a81e5 100644 --- a/README.adoc +++ b/README.adoc @@ -14,6 +14,7 @@ within a Metanorma document: * Enterprise Architect exported UML files in XMI format (`*.xmi`) * LutaML GML Dictionary files (`*.xml`) * JSON or YAML files (`*.json|*.yml|*.yaml`) +* XML Schema files (`*.xsd`) == Installation @@ -34,6 +35,8 @@ link:docs/usages/lutaml-gml.adoc[Usage with LutaML GML Dictionary by lutaml_gml_ link:docs/usages/json_yaml.adoc[Usage with JSON or YAML files by data2text, yaml2text or json2text] +link:docs/usages/lutaml-xsd.adoc[Usage with XML Schema files by lutaml_xsd] + == Documentation Please refer to https://www.metanorma.org. diff --git a/docs/usages/lutaml-xsd.adoc b/docs/usages/lutaml-xsd.adoc new file mode 100644 index 0000000..9a33e35 --- /dev/null +++ b/docs/usages/lutaml-xsd.adoc @@ -0,0 +1,94 @@ +== Usage with LutaML XSD + +=== Overview + +The `lutaml_xsd` macro parses *XML Schema (XSD)* files through `lutaml-model` +and exposes the parsed schema object to *Liquid* templates. + +=== Syntax + +[source,adoc] +----- +[lutaml_xsd,,[, option1=value1, option2=value2, ...]] +---- + +---- +----- + +* ``: Path to the XSD file to be processed. +* ``: The name of the context variable to use in the template. +* `option1=value1, ...`: Optional parameters (<>). + +[[options]] +=== Options + +* `location`: Base URL or path for resolving `` and `` + statements in the XSD. When omitted, the directory of `` is used. + +=== Liquid Template Context + +The context variable (e.g., `unitsml`) exposes the parsed +`Lutaml::Xml::Schema::Xsd::Schema` object through its Liquid drop. + +Commonly used schema collections: + +* `element`: List of elements defined in the XSD. +* `complex_type`: List of complex types defined in the XSD. +* `simple_type`, `attribute`, `attribute_group`, `group`, `import`, and + `include`: Other schema components exposed by `lutaml-model`. + +Commonly used helpers: + +* `elements_sorted_by_name`, `complex_types_sorted_by_name`, + `attribute_groups_sorted_by_name`: Sorted schema collections. +* `used_by`, `child_elements`, `attribute_elements`, and `referenced_type`: + Component helpers exposed by parsed XSD objects. + +=== Example: Listing Elements and Complex Types + +[source,adoc] +----- += Elements +[lutaml_xsd,path/to/unitsml.xsd,unitsml] +---- +{% for element in unitsml.elements_sorted_by_name %} +Name: *{{ element.name }}* +Type: *{{ element.type }}* +Used by: {{ element.used_by | map: "name" | join: ", " }} +{% endfor %} +---- + += ComplexTypes +[lutaml_xsd,path/to/unitsml.xsd,unitsml] +---- +{% for complex_type in unitsml.complex_types_sorted_by_name %} +Name: *{{ complex_type.name }}* +Children: {{ complex_type.child_elements | map: "name" | join: ", " }} +Attributes: {{ complex_type.attribute_elements | map: "name" | join: ", " }} +{% endfor %} +---- +----- + +=== Example: Using with Remote XSD and Options + +[source,adoc] +----- +[lutaml_xsd,path/to/omml.xsd,omml, location=https://raw.githubusercontent.com/t-yuki/ooxml-xsd/refs/heads/master] +---- +{% for element in omml.element %} +Name: *{{ element.name }}* +Type: *{{ element.type }}* +{% endfor %} +---- +----- + +=== Use Cases + +* Generate documentation for XML schemas. +* Extract and list schema elements and types or other details. +* Customize output using Liquid templates. + +=== Notes + +* The macro supports local files at ``. +* You can use all standard *Liquid* template features for formatting and logic. diff --git a/lib/metanorma-plugin-lutaml.rb b/lib/metanorma-plugin-lutaml.rb index b1a3a26..8925065 100644 --- a/lib/metanorma-plugin-lutaml.rb +++ b/lib/metanorma-plugin-lutaml.rb @@ -3,7 +3,9 @@ require "metanorma/plugin/lutaml/json2_text_preprocessor" require "metanorma/plugin/lutaml/yaml2_text_preprocessor" require "metanorma/plugin/lutaml/data2_text_preprocessor" +require "metanorma/plugin/lutaml/base_preprocessor" require "metanorma/plugin/lutaml/lutaml_preprocessor" +require "metanorma/plugin/lutaml/lutaml_xsd_preprocessor" require "metanorma/plugin/lutaml/lutaml_uml_datamodel_description_preprocessor" require "metanorma/plugin/lutaml/lutaml_ea_xmi_preprocessor" require "metanorma/plugin/lutaml/lutaml_xmi_uml_preprocessor" diff --git a/lib/metanorma/plugin/lutaml/lutaml_xsd_preprocessor.rb b/lib/metanorma/plugin/lutaml/lutaml_xsd_preprocessor.rb new file mode 100644 index 0000000..eb8e97a --- /dev/null +++ b/lib/metanorma/plugin/lutaml/lutaml_xsd_preprocessor.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require "lutaml/xml/parsers/xsd" + +module Metanorma + module Plugin + module Lutaml + # Preprocessor for XSD (XML Schema Definition) files. Parses XSD via + # lutaml-model's XSD parser and exposes the schema object to Liquid + # templates. + # + # Caching: parsed XSD results are cached at two levels: + # - Class-level (@@xsd_cache) persists across document invocations + # - Document-level (document.attributes["lutaml_xsd_cache"]) within a + # single document's processing + class LutamlXsdPreprocessor < BasePreprocessor + XSD_PREPROCESSOR_REGEX = %r{ + ^ # Start of line + \[ # Opening bracket + (?:\blutaml_xsd\b) # lutaml_xsd + , # Comma separator + (?[^,]+)? # Optional index names + ,? # Optional comma + (?[^,]+)? # Optional context name + (?,.*)? # Optional options + \] # Closing bracket + }x + + def initialize(_config = {}) + super + @@xsd_cache ||= {} + end + + protected + + def lutaml_liquid?(line) + line.match(XSD_PREPROCESSOR_REGEX) + end + + def load_lutaml_file(document, file_path, options) + full_path = Utils.relative_file_path(document, file_path) + location = xsd_location(full_path, options) + cache_key = [full_path, location] + + cached = document_cache_entry(document, cache_key) + return cached if cached + + result = @@xsd_cache[cache_key] ||= + parse_xsd_file(full_path, location) + + set_document_cache_entry(document, cache_key, result) + result + end + + def index_type_name + "XSD" + end + + def index_missing_message(path) + "Unable to load XSD file for `#{path}`, please specify the full path." + end + + def template(lines) + # XSD templates use double-newline joins to produce Asciidoctor + # paragraph breaks (single newlines are treated as continuation). + ::Liquid::Template.parse( + lines.join("\n\n"), + environment: create_liquid_environment, + ) + end + + private + + def parse_xsd_file(full_path, location) + File.open(full_path, "r:UTF-8") do |file| + ::Lutaml::Xml::Parsers::Xsd.parse(file, location: location) + end + end + + def xsd_location(full_path, options) + options["location"] || File.dirname(full_path) + end + + def document_cache_entry(document, cache_key) + document.attributes["lutaml_xsd_cache"]&.[](cache_key) + end + + def set_document_cache_entry(document, cache_key, result) + document.attributes["lutaml_xsd_cache"] ||= {} + document.attributes["lutaml_xsd_cache"][cache_key] = result + end + end + end + end +end diff --git a/metanorma-plugin-lutaml.gemspec b/metanorma-plugin-lutaml.gemspec index 93f423e..c177527 100644 --- a/metanorma-plugin-lutaml.gemspec +++ b/metanorma-plugin-lutaml.gemspec @@ -26,14 +26,15 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.required_ruby_version = ">= 2.7.0" # rubocop:disable Gemspec/RequiredRubyVersion + spec.required_ruby_version = ">= 3.0.0" # rubocop:disable Gemspec/RequiredRubyVersion spec.add_dependency "asciidoctor" spec.add_dependency "coradoc", "~> 1.1.8" - spec.add_dependency "expressir", "~> 2.3", ">= 2.3.4" + spec.add_dependency "expressir", "~> 2.3", ">= 2.3.5" spec.add_dependency "isodoc" spec.add_dependency "liquid" spec.add_dependency "lutaml", "~> 0.10", ">= 0.10.12" + spec.add_dependency "lutaml-model", "~> 0.8.4" spec.add_dependency "ogc-gml", "~> 1.1" spec.add_dependency "relaton-cli" diff --git a/spec/fixtures/lutaml/expected/xsd_person_fragments.xml b/spec/fixtures/lutaml/expected/xsd_person_fragments.xml new file mode 100644 index 0000000..77cc78b --- /dev/null +++ b/spec/fixtures/lutaml/expected/xsd_person_fragments.xml @@ -0,0 +1,13 @@ +Elements +

Name: Person

+

Type: person:Person

+Complex Types +

Name: Address

+

Children: City, PostalCode

+

Attributes: kind

+

Used by: Address

+

Name: Person

+

Children: FirstName, LastName, Address

+

Used by: Person

+Attribute Groups +

Attribute Group: CommonAttributes

diff --git a/spec/fixtures/lutaml/xsd_schemas/person.xsd b/spec/fixtures/lutaml/xsd_schemas/person.xsd new file mode 100644 index 0000000..33c69de --- /dev/null +++ b/spec/fixtures/lutaml/xsd_schemas/person.xsd @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/metanorma/plugin/lutaml/lutaml_xsd_preprocessor_spec.rb b/spec/metanorma/plugin/lutaml/lutaml_xsd_preprocessor_spec.rb new file mode 100644 index 0000000..16f9d47 --- /dev/null +++ b/spec/metanorma/plugin/lutaml/lutaml_xsd_preprocessor_spec.rb @@ -0,0 +1,180 @@ +require "spec_helper" + +RSpec.describe Metanorma::Plugin::Lutaml::LutamlXsdPreprocessor do + before do + described_class.class_variable_set(:@@xsd_cache, {}) + end + + describe "#process" do + let(:schema_path) { fixtures_path("xsd_schemas/person.xsd") } + let(:schema_dir) { File.dirname(schema_path) } + let(:rendered_xml) { xml_string_content(metanorma_convert(input)) } + + context "with macro: lutaml_xsd" do + let(:input) do + <<~TEXT + = Document title + + = Elements + [lutaml_xsd,#{schema_path},schema] + ---- + {% for element in schema.elements_sorted_by_name %} + Name: *{{ element.name }}* + Type: *{{ element.type }}* + Target prefix: {{ element.target_prefix }} + Used by: {{ element.used_by | map: "name" | join: ", " }} + {% endfor %} + + = Complex Types + {% for complex_type in schema.complex_types_sorted_by_name %} + Name: *{{ complex_type.name }}* + Children: {{ complex_type.child_elements | map: "name" | join: ", " }} + Attributes: {{ complex_type.attribute_elements | map: "name" | join: ", " }} + Used by: {{ complex_type.used_by | map: "name" | join: ", " }} + {% endfor %} + + = Attribute Groups + {% for attribute_group in schema.attribute_groups_sorted_by_name %} + Attribute Group: *{{ attribute_group.name }}* + Used by: {{ attribute_group.used_by | map: "name" | join: ", " }} + {% endfor %} + ---- + TEXT + end + + it "renders XSD schema components and lutaml-model helpers" do + expected_fragments.each do |fragment| + expect(rendered_xml).to(include(fragment)) + end + end + + it "uses the schema directory as the default include/import location" do + allow(Lutaml::Xml::Schema::Xsd).to receive(:parse) + .and_call_original + + rendered_xml + + expect(Lutaml::Xml::Schema::Xsd).to have_received(:parse) + .with(kind_of(String), location: schema_dir) + end + + context "with a location option" do + let(:input) do + <<~TEXT + = Document title + + [lutaml_xsd,#{schema_path},schema, location=#{schema_dir}] + ---- + {{ schema.element.size }} + ---- + TEXT + end + + it "passes the explicit location to lutaml-model" do + allow(Lutaml::Xml::Schema::Xsd).to receive(:parse) + .and_call_original + + rendered_xml + + expect(Lutaml::Xml::Schema::Xsd).to have_received(:parse) + .with(kind_of(String), location: schema_dir) + end + end + + context "with a missing XSD file" do + let(:input) do + <<~TEXT + = Document title + + [lutaml_xsd,missing.xsd,schema] + ---- + {{ schema.element.size }} + ---- + TEXT + end + + it "raises an XSD-specific loading error" do + expect { rendered_xml }.to raise_error( + StandardError, + /Unable to load XSD file for `missing.xsd`/, + ) + end + end + + context "with caching" do + let(:input) do + <<~TEXT + = Document title + + [lutaml_xsd,#{schema_path},schema] + ---- + Elements: {{ schema.element.size }} + ---- + + [lutaml_xsd,#{schema_path},schema] + ---- + Types: {{ schema.complex_type.size }} + ---- + TEXT + end + + it "parses the XSD once for duplicate file references" do + allow(Lutaml::Xml::Schema::Xsd).to receive(:parse) + .and_call_original + + rendered_xml + + expect(Lutaml::Xml::Schema::Xsd).to have_received(:parse).once + end + + it "renders both blocks correctly from the cached result" do + expect(rendered_xml).to include("Elements: 1") + expect(rendered_xml).to include("Types: 2") + end + end + + context "with a Liquid syntax error" do + let(:input) do + <<~TEXT + = Document title + + [lutaml_xsd,#{schema_path},schema] + ---- + {% for element in schema.elements_sorted_by_name % + {{ element.name }} + {% endfor %} + ---- + TEXT + end + + it "raises a parsing error" do + expect { rendered_xml }.to raise_error(StandardError) + end + end + + context "with an empty Liquid template" do + let(:input) do + <<~TEXT + = Document title + + [lutaml_xsd,#{schema_path},schema] + ---- + + ---- + TEXT + end + + it "renders without error" do + expect { rendered_xml }.not_to raise_error + end + end + + def expected_fragments + File.readlines( + fixtures_path("expected/xsd_person_fragments.xml"), + chomp: true, + ).reject(&:empty?) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d0f76cc..ccfa55e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -16,6 +16,7 @@ preprocessor Metanorma::Plugin::Lutaml::Data2TextPreprocessor preprocessor Metanorma::Plugin::Lutaml::LutamlPreprocessor preprocessor Metanorma::Plugin::Lutaml::LutamlXmiUmlPreprocessor + preprocessor Metanorma::Plugin::Lutaml::LutamlXsdPreprocessor block_macro Metanorma::Plugin::Lutaml::LutamlDiagramBlockMacro block Metanorma::Plugin::Lutaml::LutamlDiagramBlock @@ -73,17 +74,27 @@ standoc - - - true - - - 2 - 2 - 2 - 2 - - + + + true + + + TOC Heading Levels + 2 + + + HTML TOC Heading Levels + 2 + + + DOC TOC Heading Levels + 2 + + + PDF TOC Heading Levels + 2 + + HDR def strip_guid(xml) @@ -99,7 +110,7 @@ def remove_xml_whitespaces(xml) end def xml_string_content(xml) - strip_guid(Xml::C14n.format(Nokogiri::XML(xml).to_s)) + strip_guid(Nokogiri::XML(xml).to_s) end def metanorma_convert(input) From 8d679662b4400ac5032988d8c623364f20f0bc9f Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Wed, 13 May 2026 20:05:18 +0800 Subject: [PATCH 4/5] fix: resolve XMI klass_table lookup for duplicate names and absolute paths - find_packaged_klass_by_path now iterates all candidates instead of returning the first match by name, correctly disambiguating classes with the same name in different packages. - find_packaged_klass strips the root model name prefix from absolute paths (e.g. ::EA_Model::...) so match_parent_chain? walks the actual parent chain without hitting a missing root entry. - Extract match_parent_chain? helper for clarity. --- .../plugin/lutaml/lutaml_ea_xmi_base.rb | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/lib/metanorma/plugin/lutaml/lutaml_ea_xmi_base.rb b/lib/metanorma/plugin/lutaml/lutaml_ea_xmi_base.rb index 0f65062..1352f1e 100644 --- a/lib/metanorma/plugin/lutaml/lutaml_ea_xmi_base.rb +++ b/lib/metanorma/plugin/lutaml/lutaml_ea_xmi_base.rb @@ -490,7 +490,9 @@ def get_name_path(attrs) def serialize_klass_drop_by_name(xmi_path, name, document = nil, guidance = nil) parser, uml_doc = build_uml_document(xmi_path, document) - raw_klass = find_packaged_klass(parser.xmi_index, name) + root_model_name = parser.xmi_root_model.model.name + raw_klass = find_packaged_klass(parser.xmi_index, name, + root_model_name: root_model_name) warn "Class not found for name: #{name}" if raw_klass.nil? klass = raw_klass && find_uml_node_by_xmi_id( uml_doc, raw_klass.id, :classes @@ -550,11 +552,14 @@ def find_uml_node_by_xmi_id(container, xmi_id, collection) nil end - def find_packaged_klass(index, path) - segments = path.split("::") + def find_packaged_klass(index, path, root_model_name: nil) + segments = path.split("::").reject(&:empty?) + if root_model_name && segments.first == root_model_name + segments.shift + end if segments.one? index.find_packaged_by_name_and_types( - path, ["uml:Class", "uml:AssociationClass"] + segments.first, ["uml:Class", "uml:AssociationClass"] ) else find_packaged_klass_by_path(index, segments) @@ -563,20 +568,25 @@ def find_packaged_klass(index, path) def find_packaged_klass_by_path(index, segments) klass_name = segments.pop - klass = index.find_packaged_by_name_and_types( - klass_name, ["uml:Class", "uml:AssociationClass"] - ) - return unless klass - # Verify the path by walking up the parent chain - current = klass - segments.reverse_each do |pkg_name| + candidates = ["uml:Class", "uml:AssociationClass"] + .flat_map { |t| index.packaged_elements_of_type(t) } + .select { |e| e.name == klass_name } + + candidates.find do |klass| + match_parent_chain?(index, klass, segments) + end + end + + def match_parent_chain?(index, element, parent_segments) + current = element + parent_segments.reverse_each do |pkg_name| parent = index.find_parent(current.id) - return unless parent && parent.name == pkg_name + return false unless parent && parent.name == pkg_name current = parent end - klass + true end def find_packaged_enum(index, name) From bf5bc38c158cd7622b824637f61be29bf3dd3ac6 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Wed, 13 May 2026 20:05:51 +0800 Subject: [PATCH 5/5] fix: use dynamic directory name in source_extractor spec Test now uses File.basename(Dir.pwd) instead of hardcoded directory name, making it work in any checkout location. Update Gemfile and Gemfile.lock for lutaml-model 0.8.7. --- CLAUDE.md | 10 +++++++--- Gemfile | 8 +++++++- .../test_relative_includes_cache.yaml | Bin 1537 -> 1536 bytes .../plugin/lutaml/source_extractor_spec.rb | 2 +- spec/spec_helper.rb | 18 ++++-------------- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9440943..a271dca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,10 +39,12 @@ All extensions follow the Asciidoctor extension API. The two main extension type ### Preprocessor Inheritance Hierarchy -- `LutamlPreprocessor` — handles `[lutaml]`, `[lutaml_express]`, `[lutaml_express_liquid]` blocks. Parses EXPRESS files via the `lutaml`/`expressir` gems, builds Liquid contexts, and renders templates. +- `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`. +- `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. +- `LutamlXsdPreprocessor` < `BasePreprocessor` — handles `[lutaml_xsd]` blocks. Parses XSD files via `lutaml-model`, double-newline template joins for Asciidoctor paragraph breaks. - `LutamlUmlDatamodelDescriptionPreprocessor` and `LutamlEaXmiPreprocessor` — both include `LutamlEaXmiBase`, which handles XMI parsing via `lutaml` gem and renders using bundled Liquid templates. - `LutamlXmiUmlPreprocessor` — another XMI-based preprocessor with its own macro regex. -- `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. +- `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). ### Key Shared Modules @@ -71,8 +73,10 @@ Tests use `metanorma-standoc` as the backend. The spec helper registers all exte ## Key Dependencies -- `lutaml` — core LutaML parser/model library (EXPRESS, UML, XMI formats) +- `lutaml` — core LutaML parser/model library (EXPRESS, UML, XMI, XSD formats) +- `lutaml-model` — LutaML serialization framework (provides XSD parsing, Liquid drops) - `expressir` — EXPRESS schema parser - `ogc-gml` — OGC GML dictionary parser - `liquid` — template rendering engine - `asciidoctor` — document processing framework +- `canon` — semantic XML comparison for test assertions diff --git a/Gemfile b/Gemfile index 5119c17..202a0e1 100644 --- a/Gemfile +++ b/Gemfile @@ -12,7 +12,12 @@ rescue StandardError nil end -gem "rake", "~> 13" +gem "canon" +gem "html2doc", github: "metanorma/html2doc", branch: "main" +gem "lutaml" +gem "metanorma", github: "metanorma/metanorma", branch: "main" +gem "metanorma-standoc", github: "metanorma/metanorma-standoc", branch: "main" +gem "rake" gem "rspec" gem "rspec-html-matchers" gem "rubocop" @@ -23,3 +28,4 @@ gem "simplecov" gem "timecop" gem "vcr" gem "webmock" + diff --git a/spec/fixtures/lutaml/expressir_relative_paths/test_relative_includes_cache.yaml b/spec/fixtures/lutaml/expressir_relative_paths/test_relative_includes_cache.yaml index 67a1afc4580623d64b72fc5472a3da5015614a1e..f8768715690aaf5e2a01904f594bf126c441188c 100644 GIT binary patch literal 1536 zcmV+b2LJhZob6iObK*7-f9J38aaYtyc*PD(;*aFn}E0RpY zwEf?^@)tJPfWf}lH|HmirQP4Ie!E&pK0iNiA>G8BtBUsOGQv4!s(R-b#8lNj*5{

9f zVnG)K$=yFh4?Y(U4eDO8z8n&wHj@MGm zE)hF4%Dz*Wy;7%5Q6e6NXo^kU@4?`3>;jt}z&rVea!26;+XiNaa(U6c=mO!GIKBRG z7U?_fo+-}cZ=#FS%d?_0fBOFHzmv#)f>=C4UzBFB%dWAy$>K!$n0#2brNT}eE|-m zk)h}(YD1bGX*b$PyYFjZYoJ|?)#pnektt!smPph`YPlwfTjJ3&ky`|F$W3Xx4h+=T zhLnRDnXUjI3powi_kKeh?J~#kbg>Vu>hAt*6JVhRiBQ=1c$$U2F?d7fTi|AJ__IfY z-!(>01cpk;KtUbngO*j!3l(!@d)9<7=7CEvrMlwk0C`rFs|x#9Y;twY4Rg>#?qT4C~9Lu*!48OvxRO z=?yC{sR1qrwh&fdj3%BCv%c{B10DHCEZyRpp)m|F0S-BpeV~=WPc2zHfM-fdCERe17VvJCc6>>qE&N)ESZXK4%0*Jp;@Mr3MJ&He+N4 z^#BS=&9sGO*ce)=-QJ{{v!|)RRQ*hCoNK6}IIavthpEc&KI@fjT&^oFOLbCtZ!j2+ zhY#xzwt?6N<5mV1Hl@o2g9M#1I6PDD-QPcqdt+^Yx&w6gFwkz%pf~QJPrvnUhvP4( zKN^m;(XfXe`@d>;y>=eJtI7a&?=lf>_$nXBaX}qC*`d8l{{G~bj|_nEmyhW%51#B` z51wQ5avH#MRqIU5e>~v=3UZMsQJ_%(4CB-!_(v8iOAcPdk@cOY`5caWiwXwb9z6xI6j3r9YTgpY! z-Hx}h$n2B7%3X_NLi{5*pb(m&AY)Lor04`FOB8# z#rMzS+%vKo(>E9Mv8>HKw)sKhn$x)0@mr0#UBr+|O7*xVMc8#s3YH{2ms!DAb6}Kr z=Uc^&VP(B=WPMI&@N2$KnzyW**SAu&CGg>2{Nz*;87%y!Q*q44aJ%C!ykpzAeoJhc zOJ_SZ-olSZ-1=slkt$Zcp&^G|4QJZ`VFv-@?sGcf_`R^1O1;%s7~d|JxDlwyNlflX zAIF;-hH|ii1b66fwRG%zn=hZbtRnkiJ~sDee=b>RAyfPl&GJ+JY8Ml!CvUO{?6_F2 m3m><_Jd%RhAG-!@NY=cIbop5gH@AFw`-6t{rQ!e1!cC1OBLbcP literal 1537 zcmV+c2LAbYob6gmbK5o$zWY~je2IOqWLr&U!b?Y$=vFhboycm_G}FOABqX8c%K#`_ zllH%N!IwyhdQc=|G83L85ZL{8@$F&(@Z#d)1nDN`TvfCeml4h>Q`K9?Af~GJus$az z3&J>cY*o2#by`22=#DLjEz}c5**+3lZm3D1kBh}zRk%y^_LRN|4?4C@PhiVzlV})X z#KeLw2$H>dTM#Z*ZPeCH&mg=7{XrlHOcv%+|HTdjeFCo-NN{Yfst^7k7@{cDP!(+3 zj=+K#sAC{Y4nPJL7(IE|q+*G5Mg?IMBfc8P;0Js3^-9hEUid#0<#wqE@*+;czb3UG zRl|9)VfIR$Hbsee6r#yDb$$4CU&wbJ+pHF>&_x z{duJCtaGk7lYfXV&aTe$&ivWC^Z!mG_X%S02z_4ah4=>`Bb1;hHnzYBFrjprkxNPx z9J>6C@8mb7BbKwaq3iAY@)a#oL|W;pz@~t8n;tA*(=quh3F%z`i9%*)Q>>FX%N+`E z5RD8)KT;dg>`1%OM%rCZ3tI#2YOH>{^bwg7Mr?^heV~?WlDH)vEfcv#Fo)cfw(H11 zjcrIdn33rUa8bx<(7yK@;%JvShNp`|XjOOjXPW>EHAsZQ#>dkv^o_wAGT#C>gTtRZ z8vL#?dLl4XLIw)zI3KjEa-OM}Bipkkgdu0@M@4;&;GPAg>vGM8h9sVJTsAAyG$p63 z`ItuP>dy{7$1eFE7NrbBKqg4@bEVve(J9r#7$D}VuB@#s$y|?h-D6l^HicE58)i!G zcua3tc}Wd$Ik1JW`eHQkgqZb(=O5_EKVs?T-wcgm(8)>|?pSdE$e?y~>TlHqW-}tv z(d^jeMpK@6flC=oKECPj$paB8X0D!^HXG#M2h4_v=k#W zD60rdu9%fuLT#B~<(z;6{e;zclfvw41g(uXCpG|09fnLNTnVA{2E}rbNo=O;Z(dZ8 zb11lYfZPXK8T{0ewFh{nq*THU=V$@%W@*QlB-+9+m58NwLabaQ1$lP z6u39>+3ZXYsUZ$6S#kh)p2K*=Jd+$z#e9;3N1mD1{%LfT2$ev>>ZE@+b+x!Zh`;e{ zU25uK(@cz>hRsQd#aH?O`@`Tb?fxdw?1Hrx0SEG=x?L_xnefpgY`JRL(D#&T!Lu17 zGpGkpP->iRweeO%6~%F7AUaG{26tJnY~yNOaapR9%Desk zU_7{Ahp-F8E*L`@SlE;<7Yq_~#^CTwy?b|eKkkmTKI-(*?R{SxqJDSWMIV3f4hQ4U zs5csnwb7u99(uoNx82q@fY+4)?A&G|+VItOAjN_@c(Ox#pZvqgFCQ5I5Z^jZO=#X-e zbhqPeEHZ~=Z+X-)i%)QZXX64nR&ZTKVRZWQ={j%LJyMpcBX;C9X7hgVg;Q|y=%ukd zzWn}qy!DK%#`N8axhQM59^3q&am{I*@A$38+|FZ2C8c^?lOpW8CIw59p3AJ@t2r>* zcIR8gj$viJaAbW>XYgyjPMX)Oo7cBewI%TJU;J#TBr;g|O{b#B$8h`OF1%ygxPD7) znoDOpHD1Gy1#W$_%}5n1-_Vfbu7*qzuLt_>dC7t0(&l& n>%zyKFbh&J`(xLD4au5!kuE>0;pUDnZ-3CRzBK$FmEBE@5n}^s diff --git a/spec/metanorma/plugin/lutaml/source_extractor_spec.rb b/spec/metanorma/plugin/lutaml/source_extractor_spec.rb index 5b700a8..3dd5cbb 100644 --- a/spec/metanorma/plugin/lutaml/source_extractor_spec.rb +++ b/spec/metanorma/plugin/lutaml/source_extractor_spec.rb @@ -208,7 +208,7 @@ def anchor data subject.send(:relative_file_path, document, "file.adoc") end - let(:expected_output) { "/metanorma-plugin-lutaml/file.adoc" } + let(:expected_output) { "/#{File.basename(Dir.pwd)}/file.adoc" } it { expect(relative_file_path).to include(expected_output) } end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ccfa55e..771e8c3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -79,20 +79,10 @@ true - TOC Heading Levels - 2 - - - HTML TOC Heading Levels - 2 - - - DOC TOC Heading Levels - 2 - - - PDF TOC Heading Levels - 2 + 2 + 2 + 2 + 2 HDR