Skip to content

Commit 8b47b85

Browse files
committed
Refactor: autoload, OOP cleanup, and comprehensive spec improvements
1 parent 9deff46 commit 8b47b85

29 files changed

Lines changed: 525 additions & 1041 deletions

.github/workflows/rubocop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,4 @@ jobs:
4545
ruby-version: ${{ env.RUBY_VERSION }}
4646
- uses: reclaim-the-stack/rubocop-action@v1.1.0
4747
with:
48-
gem_versions: rubocop:1.72.2 rubocop-performance:1.24.0
48+
gem_versions: rubocop:1.86.0 rubocop-performance:1.26.1

.rspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
--color
2-
--profile
2+
--format progress
33
--require spec_helper

CLAUDE.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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-cli is the command-line interface for the Metanorma document processing toolchain. It provides the `metanorma` executable for compiling Metanorma documents across multiple standard flavors (ISO, IEC, IETF, BIPM, ITU, OGC, IEEE, JIS, etc.). It wraps the `metanorma` gem and adds CLI-level features: document compilation, collection rendering, site generation, template management, and STS XML conversion via `mnconvert`.
8+
9+
## Build & Development Commands
10+
11+
```bash
12+
# Install dependencies
13+
bundle install
14+
15+
# Run full test suite
16+
bundle exec rake
17+
18+
# Run a single spec file
19+
bundle exec rspec spec/metanorma/cli/compiler_spec.rb
20+
21+
# Run a single example by line number
22+
bundle exec rspec spec/metanorma/cli/compiler_spec.rb:42
23+
24+
# Lint
25+
bundle exec rubocop
26+
27+
# Run the CLI locally
28+
bundle exec exe/metanorma compile document.adoc -t iso
29+
30+
# IRB console with project loaded
31+
bin/console
32+
```
33+
34+
The project uses direnv + nix flake (see `flake.nix`) for environment setup. If using nix, run `direnv allow` to enter the dev shell.
35+
36+
Tests use RSpec with VCR cassettes (for HTTP stubbing), webmock, and rspec-command. SimpleCov is configured for coverage. Test state persistence is in `.rspec_status`.
37+
38+
## Architecture
39+
40+
### Entry Points
41+
42+
- **`exe/metanorma`** — Main CLI executable. Sets up encoding, handles SOCKS proxy support via `socksify`, then delegates to `Metanorma::Cli.start(ARGV)`.
43+
- **`exe/metanorma-manifest`** — Standalone tool that converts YAML manifests into Metanorma XML/HTML collection indexes.
44+
- **`lib/metanorma-cli.rb`** — Just requires `metanorma/cli`.
45+
- **`lib/metanorma/cli.rb`** — Core module. `Cli.start` intercepts arguments: if no known command is found, it prepends `"compile"` (so bare `metanorma foo.adoc` works). Defines config paths (`~/.metanorma/config.yml` global, `.metanorma/config.yml` local).
46+
47+
### Command System (Thor)
48+
49+
- **`Command`** (`lib/metanorma/cli/command.rb`) — Main Thor command class. Defines all top-level commands: `compile`, `collection`, `convert`, `version`, `new`, `log_messages`, `list-extensions`, `list-doctypes`, `export-config`. Also has three subcommands: `template-repo`, `site`, `config`.
50+
- **`ThorWithConfig`** (`lib/metanorma/cli/thor_with_config.rb`) — Base class for commands. Overrides `options` to merge CLI arguments with values from global and local config files (`~/.metanorma/config.yml` and `.metanorma/config.yml`). The merging is done by `Commands::Config.load_configs`.
51+
- **`flavor.rb`** (module, not the one in `lib/metanorma/`) — Mixed into `Command`. Provides flavor discovery, extension listing, doctype tables, backend version lookups, and the `export-config` command.
52+
53+
### Key Classes
54+
55+
- **`Compiler`** (`compiler.rb`) — Wraps `Metanorma::Compile`. Validates input file existence, normalizes options (splits extract/extensions on commas), and calls the metanorma core compile pipeline.
56+
- **`Collection`** (`collection.rb`) — Renders Metanorma collections from YAML or XML files. Delegates to `Metanorma::Collection.parse` for the actual processing.
57+
- **`SiteGenerator`** (`site_generator.rb`) — Generates an HTML site from a collection of Metanorma documents. Reads a `metanorma.yml` manifest (via `SiteManifest` models), compiles individual `.adoc` files and collection files, builds a Relaton collection index, then converts to HTML using Liquid templates.
58+
- **`Generator`** (`generator.rb`) — Creates new Metanorma documents from templates. Merges base templates (`templates/base/`) with type-specific templates downloaded from Git repos.
59+
- **`GitTemplate`** (`git_template.rb`) — Manages Git-hosted document templates. Clones template repos to `~/.metanorma/templates/git/`.
60+
- **`TemplateRepo`** (`template_repo.rb`) — Manages template repo registrations in the global config file.
61+
62+
### Subcommands
63+
64+
- **`Commands::Config`** (`commands/config.rb`) — `metanorma config get/set/unset`. Manages hierarchical YAML config (global + local). The `load_configs` class method merges global then local config into CLI options.
65+
- **`Commands::Site`** (`commands/site.rb`) — `metanorma site generate`. Resolves manifest paths, stylesheet/template paths, and delegates to `SiteGenerator`.
66+
- **`Commands::TemplateRepo`** (`commands/template_repo.rb`) — `metanorma template-repo add`. Registers template repos.
67+
68+
### Flavor System
69+
70+
- **`Metanorma::Flavor`** (`lib/metanorma/flavor.rb`) — Activates and loads all supported metanorma flavor gems (`metanorma-iso`, `metanorma-iec`, etc.). Activation happens at require time (line 77). Private gems (nist, ribose) fail silently.
71+
- **`Metanorma::SiteManifest`** (`lib/metanorma/site_manifest.rb`) — Lutaml::Model serializable classes for parsing `metanorma.yml` site manifest files.
72+
73+
### Supporting Code
74+
75+
- **`UI`** (`ui.rb`) — Thin Thor-based wrapper for console output (say, info, error, table).
76+
- **`Errors`** (`errors.rb`) — Custom error classes: `FileNotFoundError`, `FatalCompilationError`, `InvalidManifestFileError`, `DuplicateTemplateError`.
77+
- **`stringify_all_keys.rb`** — Core extensions for deep key stringification/symbolization on Hash and Array. Note: `metanorma-utils` provides similar functionality via `Metanorma::Utils::Hash`.
78+
79+
## Configuration
80+
81+
Config files are YAML with a `cli` key at the top level. Values from config are merged into Thor options (global first, then local). Example:
82+
83+
```yaml
84+
cli:
85+
agree_to_terms: true
86+
install_fonts: false
87+
```
88+
89+
- Global: `~/.metanorma/config.yml`
90+
- Local: `.metanorma/config.yml`
91+
92+
## Testing Notes
93+
94+
- Spec helper mocks `Mn2pdf.convert` and `MnConvert.convert` to copy files instead of running real conversions.
95+
- `strip_guid` helper normalizes UUIDs in XML output for deterministic comparisons.
96+
- Acceptance specs (`spec/acceptance/`) test the CLI end-to-end via `rspec-command`.
97+
- VCR cassettes are in `spec/vcr_cassettes/` for HTTP request stubs.

lib/metanorma/cli.rb

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,32 @@
22

33
require "metanorma"
44
require "metanorma/flavor"
5-
65
require "metanorma/site_manifest"
76

8-
require "metanorma/cli/version"
9-
require "metanorma/cli/errors"
10-
require "metanorma/cli/command"
11-
127
module Metanorma
138
module Cli
9+
autoload :Collection, "metanorma/cli/collection"
10+
autoload :Command, "metanorma/cli/command"
11+
autoload :Commands, "metanorma/cli/commands"
12+
autoload :Compiler, "metanorma/cli/compiler"
13+
autoload :ConfigExporter, "metanorma/cli/config_exporter"
14+
autoload :Errors, "metanorma/cli/errors"
15+
autoload :FlavorMethods, "metanorma/cli/flavor"
16+
autoload :Generator, "metanorma/cli/generator"
17+
autoload :GitTemplate, "metanorma/cli/git_template"
18+
autoload :SiteGenerator, "metanorma/cli/site_generator"
19+
autoload :TemplateRepo, "metanorma/cli/template_repo"
20+
autoload :ThorWithConfig, "metanorma/cli/thor_with_config"
21+
autoload :UI, "metanorma/cli/ui"
22+
autoload :VERSION, "metanorma/cli/version"
23+
1424
CONFIG_DIRNAME = ".metanorma"
1525
CONFIG_FILENAME = "config.yml"
1626

1727
def self.load_flavors
1828
Metanorma::Flavor.load_flavors
1929
end
2030

21-
# Invoking commands
22-
#
23-
# In the Metanorma CLI, we've included some custom behavior,
24-
# like exposing the compiation directly from the root command.
25-
#
26-
# So, for this use case we first check if the user is actually
27-
# trying to compile a document or not, and based on that we'll
28-
# compile the document or show the help documentation.
29-
#
3031
def self.start(arguments)
3132
if find_command(arguments).empty?
3233
arguments.unshift("compile")

lib/metanorma/cli/collection.rb

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ def self.render(filename, options = {})
1616
new(filename, options).render
1717
end
1818

19+
def self.parse_formats(formats)
20+
formats = formats.split(",") if formats.is_a?(String)
21+
(formats || []).map { |extension| extension.strip.to_sym }
22+
end
23+
1924
def render
2025
extract_options_from_file
2126
collection_file.render(collection_options.compact)
@@ -37,7 +42,7 @@ def collection_options
3742
compile: @compile_options,
3843
output_folder: build_output_folder,
3944
coverpage: options.fetch(:coverpage, nil),
40-
format: collection_output_formats(options.fetch(:format, "")),
45+
format: self.class.parse_formats(options.fetch(:format, "")),
4146
site_generate: options["site_generate"],
4247
}
4348
end
@@ -54,17 +59,9 @@ def build_output_folder
5459
end
5560
end
5661

57-
def collection_output_formats(formats)
58-
if formats.is_a?(String)
59-
formats = formats.split(",")
60-
end
61-
62-
(formats || []).map { |extension| extension.strip.to_sym }
63-
end
64-
6562
def extract_options_from_file
6663
yaml_file = if /\.ya?ml$/.match?(file.to_s)
67-
YAML.safe_load(File.read(file.to_s))
64+
YAML.safe_load_file(file.to_s)
6865
elsif /\.xml$/.match?(file.to_s)
6966
xml_extract_options_from_file
7067
end

lib/metanorma/cli/command.rb

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,15 @@
11
# frozen_string_literal: true
22

3-
require "metanorma/cli/compiler"
4-
require "metanorma/cli/generator"
5-
require "metanorma/cli/collection"
6-
require "metanorma/cli/git_template"
7-
require "metanorma/cli/thor_with_config"
8-
require "metanorma/cli/commands/config"
9-
require "metanorma/cli/commands/template_repo"
10-
require "metanorma/cli/commands/site"
113
require "mnconvert"
12-
require_relative "flavor"
134

145
module Metanorma
156
module Cli
167
class Command < ThorWithConfig
8+
include FlavorMethods
9+
1710
class_option :progress, aliases: "-s", type: :boolean, default: false,
18-
desc: "Show progress for long running tasks" \
19-
" (like download)"
11+
desc: "Show progress for long running tasks " \
12+
"(like download)"
2013

2114
desc "new NAME", "Create new Metanorma document"
2215
option :type, aliases: "-t", required: true, desc: "Document type"
@@ -40,28 +33,28 @@ def new(name)
4033
option :wrapper, aliases: "-w", type: :boolean,
4134
desc: "Create wrapper folder for HTML output"
4235
option :asciimath, aliases: "-a", type: :boolean,
43-
desc: "Keep Asciimath in XML output instead of" \
44-
" converting it to MathM"
36+
desc: "Keep Asciimath in XML output instead of " \
37+
"converting it to MathM"
4538
option :datauriimage, aliases: "-d", type: :boolean,
4639
desc: "Encode HTML output images as data URIs"
4740
option :relaton, aliases: "-R",
48-
desc: "Export Relaton XML for document to nominated" \
49-
" filename"
41+
desc: "Export Relaton XML for document to nominated " \
42+
"filename"
5043
option :extract, aliases: "-e",
51-
desc: "Export sourcecode fragments from this document" \
52-
" to nominated directory"
44+
desc: "Export sourcecode fragments from this document " \
45+
"to nominated directory"
5346
option :version, aliases: "-v",
5447
desc: "Print version of code (accompanied with -t)"
5548
option :log_messages, aliases: "-L",
5649
desc: "Display available log messages " \
57-
"(accompanied with -t)"
50+
"(accompanied with -t)"
5851
option :output_dir, aliases: "-o",
5952
desc: "Directory to save compiled files"
6053
option :strict, aliases: "-S", type: :boolean,
6154
desc: "Strict compilation: abort if there are any errors"
6255
option :agree_to_terms,
6356
type: :boolean,
64-
desc: "Agree / Disagree with all third-party licensing terms "\
57+
desc: "Agree / Disagree with all third-party licensing terms " \
6558
"presented (WARNING: do know what you are agreeing with!)"
6659
option :install_fonts, type: :boolean, default: true,
6760
desc: "Install required fonts"
@@ -99,14 +92,14 @@ def compile(file_name = nil)
9992
option :coverpage, aliases: "-c", desc: "Liquid template"
10093
option :agree_to_terms,
10194
type: :boolean,
102-
desc: "Agree / Disagree with all third-party licensing terms "\
95+
desc: "Agree / Disagree with all third-party licensing terms " \
10396
"presented (WARNING: do know what you are agreeing with!)"
10497
option :install_fonts, type: :boolean, default: true,
10598
desc: "Install required fonts"
10699
option :continue_without_fonts,
107100
type: :boolean,
108101
desc: "Continue processing even when fonts are missing"
109-
option :strict, aliases: "-S", type: :boolean, \
102+
option :strict, aliases: "-S", type: :boolean,
110103
desc: "Strict compilation: abort if there are any errors"
111104

112105
def collection(filename = nil)
@@ -193,7 +186,7 @@ def list_doctypes(type = nil)
193186

194187
desc "export-config", "Export flvor configuration"
195188
def export_config(type = nil)
196-
export_config_flavor(type)
189+
ConfigExporter.new(type).export
197190
end
198191

199192
desc "template-repo", "Manage metanorma templates repository"

lib/metanorma/cli/commands/config.rb

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
require "pathname"
22
require "metanorma-utils"
33

4-
# require "metanorma/cli/stringify_all_keys"
5-
64
module Metanorma
75
module Cli
86
module Commands
@@ -48,17 +46,6 @@ def unset(name)
4846

4947
def self.exit_on_failure?() true end
5048

51-
# priority:
52-
# IDEAL:
53-
# thor defaults -> global conf -> local conf
54-
# -> env vars -> passed arguments
55-
#
56-
# ACTUAL:
57-
# all arguments -> global conf -> local conf
58-
#
59-
# - thor doesn't provide to differentiate default values against passed
60-
# args
61-
# - thor doesn't allow to get all args available for current command
6249
def self.load_configs(options, configs = [
6350
Metanorma::Cli.global_config_path, Metanorma::Cli.local_config_path
6451
])
@@ -108,19 +95,15 @@ def dig_path(str)
10895
end
10996

11097
def deep_set(hash, value, *keys)
111-
keys[0...-1].reduce(hash) do |acc, h|
112-
tmp = acc.public_send(:[], h)
113-
if tmp.nil?
114-
acc[h] = tmp = Hash.new
115-
end
116-
tmp
117-
end.public_send(:[]=, keys.last, value)
98+
leaf = keys[0...-1].reduce(hash) do |acc, k|
99+
acc[k] ||= {}
100+
end
101+
leaf[keys.last] = value
118102
end
119103

120104
def deep_unset(hash, *keys)
121-
keys[0...-1].reduce(hash) do |acc, h|
122-
acc.public_send(:[], h)
123-
end.delete(keys.last)
105+
leaf = keys[0...-1].reduce(hash) { |acc, k| acc[k] }
106+
leaf.delete(keys.last)
124107
end
125108

126109
def try_convert_to_bool(value)

0 commit comments

Comments
 (0)