metanorma-cli 1.12.4 → 1.12.5

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: 83127d9943ff11c57968eba5da57a1506bab8cdbe1e035e055c382b5a70ba8d9
4
- data.tar.gz: 76a08db2cd6f26d9f82a795fd2075eabe1f4ec7b1789bb56d6fccd49deaf188d
3
+ metadata.gz: 838952cee69c23736d7547e92a620e72a827197a29a30e1ea1836f234ed04d82
4
+ data.tar.gz: 74374105306969e0ad2ef4cb50463f56a10cafb783e7aeaa9e7c1c833ed593a2
5
5
  SHA512:
6
- metadata.gz: 3e43bd5b2d5a3cb5bea64d82109963de633ab0c3e139ae36283cf4cbc0756a54e7952157c37e20c2bf0ee1c6edb09245671b77a5b1054a5073d13794063557e0
7
- data.tar.gz: ac22976c0ee80b29e48ba6559230c742f97d86bbc2243cd51a4657833dc54ffb6b60b5a9919c8bbfc6be7404e9d6c4593fe8e5028ceb8cd45213d64b6b6f6ed9
6
+ metadata.gz: 4c0ab5db08eaf4d709d9f20c18e690b7eeab0997bd188122f514b022063a94d58454a6d5b06ae8018564724b35dc43a65862057c2fb83723588bd204b6eb5db4
7
+ data.tar.gz: 03a01f8b46739e421fb458a05ce48e11970535afc921990489edd3b7f6b4c5a1bcff61f4a3149229040e2718bba608576275d2974081e8036869707896c6dd54
@@ -3,6 +3,8 @@ require "yaml"
3
3
  module Metanorma
4
4
  module Cli
5
5
  class Collection
6
+ attr_reader :file, :options
7
+
6
8
  def initialize(file, options)
7
9
  @file = file
8
10
  @options = Cli.with_indifferent_access(options)
@@ -17,27 +19,26 @@ module Metanorma
17
19
  def render
18
20
  extract_options_from_file
19
21
  collection_file.render(collection_options.compact)
22
+ self
20
23
  end
21
24
 
22
- private
23
-
24
- attr_reader :file, :options
25
-
26
25
  def collection_file
27
26
  @collection_file ||= Metanorma::Collection.parse(file)
28
27
  end
29
28
 
29
+ private
30
+
30
31
  def source_folder
31
- @source_folder ||= File.dirname(File.expand_path(file))
32
+ @source_folder ||= Pathname.new(file).realpath.parent
32
33
  end
33
34
 
34
35
  def collection_options
35
36
  @collection_options ||= {
36
37
  compile: @compile_options,
37
38
  output_folder: build_output_folder,
38
- coverpage: @options.fetch(:coverpage, nil),
39
- format: collection_output_formats(@options.fetch(:format, "")),
40
- site_generate: @options["site_generate"],
39
+ coverpage: options.fetch(:coverpage, nil),
40
+ format: collection_output_formats(options.fetch(:format, "")),
41
+ site_generate: options["site_generate"],
41
42
  }
42
43
  end
43
44
 
@@ -45,9 +46,11 @@ module Metanorma
45
46
  output_folder = options.fetch(:output_folder, nil)
46
47
 
47
48
  if output_folder && @output_dir
48
- @output_dir.join(output_folder).to_s
49
+ @output_dir.join(output_folder)
50
+ elsif output_folder
51
+ Pathname.new(output_folder)
49
52
  else
50
- output_folder || source_folder
53
+ source_folder
51
54
  end
52
55
  end
53
56
 
@@ -60,13 +63,13 @@ module Metanorma
60
63
  end
61
64
 
62
65
  def extract_options_from_file
63
- yaml_file = if /\.ya?ml$/.match?(@file.to_s)
64
- YAML.safe_load(File.read(@file.to_s))
65
- elsif /\.xml$/.match?(@file.to_s)
66
+ yaml_file = if /\.ya?ml$/.match?(file.to_s)
67
+ YAML.safe_load(File.read(file.to_s))
68
+ elsif /\.xml$/.match?(file.to_s)
66
69
  xml_extract_options_from_file
67
70
  end
68
71
 
69
- old = options.dup
72
+ old = @options.dup
70
73
  @options = Cli.with_indifferent_access(
71
74
  yaml_file.slice("coverpage", "format", "output_folder"),
72
75
  )
@@ -74,7 +77,7 @@ module Metanorma
74
77
  end
75
78
 
76
79
  def xml_extract_options_from_file
77
- xml = Nokogiri::XML File.read(@file.to_s, encoding: "UTF-8", &:huge)
80
+ xml = Nokogiri::XML File.read(file.to_s, encoding: "UTF-8", &:huge)
78
81
  { "coverpage" => xml.at("//coverpage"),
79
82
  "format" => xml.at("//format"),
80
83
  "output_folder" => xml.at("//output_folder") }.compact
@@ -10,14 +10,16 @@ module Metanorma
10
10
  class Site < ThorWithConfig
11
11
  SITE_OUTPUT_DIRNAME = "_site"
12
12
 
13
- desc "generate [SOURCE_PATH]", "Generate site from collection"
13
+ desc "generate [SITE_MANIFEST_PATH]", "Generate site from collection"
14
14
  option :config,
15
15
  aliases: "-c",
16
- desc: "Metanorma configuration file"
16
+ desc: "Metanorma configuration file " \
17
+ "(deprecated: use the first argument of " \
18
+ "the command instead)"
17
19
 
18
20
  option :output_dir,
19
21
  aliases: "-o",
20
- default: Pathname.new(Dir.pwd).join(SITE_OUTPUT_DIRNAME).to_s,
22
+ default: Pathname.pwd.join(SITE_OUTPUT_DIRNAME).to_s,
21
23
  desc: "Output directory for generated site"
22
24
 
23
25
  option :output_filename_template,
@@ -41,27 +43,97 @@ module Metanorma
41
43
 
42
44
  option :stylesheet,
43
45
  aliases: "-s",
44
- desc: "Stylesheet file path for rendering HTML page"
46
+ desc: "Stylesheet file path " \
47
+ "(relative to the current working directory) " \
48
+ "for rendering HTML page"
45
49
 
46
50
  option :template_dir,
47
51
  aliases: "-t",
48
- desc: "Liquid template directory to render site design"
52
+ desc: "Liquid template directory " \
53
+ "(relative to the current working directory) " \
54
+ "to render site design"
49
55
 
50
56
  option :strict,
51
57
  aliases: "-S",
52
58
  type: :boolean,
53
59
  desc: "Strict compilation: abort if there are any errors"
54
60
 
55
- def generate(source_path = Dir.pwd)
61
+ # If no argument is provided, work out the base
62
+ # path to use for calculation of full paths for
63
+ # files referenced in the site manifest.
64
+ #
65
+ # Additionally, if the config file is not provided,
66
+ # use the current working directory as the base path.
67
+ # If the config file is provided, use the directory
68
+ # of the config file as the base path.
69
+ #
70
+ # If the source path is a file and no config file is provided,
71
+ # treat the source path as the config file.
72
+ # Similar to the above, use the directory of the config file
73
+ # as the base path.
74
+ #
75
+ # For stylesheet and template_dir options, and if they are
76
+ # relative paths, they will be resolved relative to whatever
77
+ # defined them.
78
+ #
79
+ # So, if they are provided via the command line,
80
+ # resolve them relative to the current working directory.
81
+ # If they are provided via the site manifest,
82
+ # resolve them relative to the site manifest's directory.
83
+ def generate(manifest_path = nil)
84
+ my_options = options.dup # because options is not modifiable
85
+
86
+ base_path = calculate_base_path!(my_options, manifest_path).realpath
87
+
88
+ calculate_full_paths!(my_options)
89
+
56
90
  Cli::SiteGenerator.generate!(
57
- source_path,
58
- options,
59
- filter_compile_options(options),
91
+ base_path,
92
+ my_options,
93
+ filter_compile_options(my_options),
60
94
  )
61
95
  UI.say("Site has been generated at #{options[:output_dir]}")
62
96
  rescue Cli::Errors::InvalidManifestFileError
63
97
  UI.error("Invalid data in: #{options[:config]}")
64
98
  end
99
+
100
+ private
101
+
102
+ # Make relative paths absolute.
103
+ def calculate_full_paths!(my_options)
104
+ %i[stylesheet template_dir].each do |key|
105
+ if my_options[key]
106
+ path = Pathname.new(my_options[key])
107
+ if path.relative?
108
+ my_options[key] = Pathname.pwd.join(path)
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ # Calculate the base path for the site generation.
115
+ def calculate_base_path!(my_options, manifest_path = nil)
116
+ config_file = options[:config]
117
+ if manifest_path.nil?
118
+ if config_file.nil?
119
+ # If no config file is given, use the current working directory
120
+ # as the base path.
121
+ Pathname.pwd
122
+ else
123
+ # If a config file is given, use it as the config file
124
+ # and use its directory as the base path.
125
+ Pathname.new(config_file).dirname
126
+ end
127
+ elsif File.file?(manifest_path) && config_file.nil?
128
+ # If a file is given, use it as the config file
129
+ # and use its directory as the base path.
130
+ my_options["config"] = manifest_path
131
+ Pathname.new(manifest_path).dirname
132
+ else
133
+ # If directory is given, use it as the base path.
134
+ Pathname.new(manifest_path)
135
+ end
136
+ end
65
137
  end
66
138
  end
67
139
  end
@@ -13,12 +13,12 @@ module Metanorma
13
13
  DEFAULT_CONFIG_FILE = "metanorma.yml"
14
14
 
15
15
  # rubocop:disable Metrics/AbcSize
16
- def initialize(source, options = {}, compile_options = {})
16
+ def initialize(source_path, options = {}, compile_options = {})
17
17
  @collection_queue = []
18
- @source = find_realpath(source)
19
- @site_path = options.fetch(
20
- :output_dir, Commands::Site::SITE_OUTPUT_DIRNAME
21
- ).to_s
18
+ @source_path = find_realpath(source_path)
19
+ @site_path = Pathname.new(
20
+ options.fetch(:output_dir, Commands::Site::SITE_OUTPUT_DIRNAME),
21
+ )
22
22
 
23
23
  @asset_folder = options.fetch(:asset_folder, DEFAULT_ASSET_FOLDER).to_s
24
24
  @relaton_collection_index = options.fetch(
@@ -34,11 +34,11 @@ module Metanorma
34
34
  template_data("output_filename"),
35
35
  )
36
36
 
37
- # Determine base path for template files
38
- # If template_dir is not absolute, then it is relative to the manifest
39
- # file.
40
- # If manifest file is not provided, then it is relative to the current
41
- # directory.
37
+ # Determine base path for stylesheet & template files
38
+ # If the file path is relative, it is relative to the directory
39
+ # containing the site manifest file.
40
+ # If site manifest file is not provided, then it is relative to the
41
+ # current directory.
42
42
  @base_path = if manifest_file.nil?
43
43
  Pathname.pwd
44
44
  else
@@ -56,85 +56,149 @@ module Metanorma
56
56
  def generate!
57
57
  ensure_site_asset_directory!
58
58
 
59
+ # compile individual document files
59
60
  compile_files!(select_source_files)
60
61
 
61
62
  site_directory = asset_directory.parent
62
63
 
64
+ # actually compile collection file(s)
65
+ compile_collections!
66
+
63
67
  Dir.chdir(site_directory) do
64
68
  build_collection_file!(relaton_collection_index)
65
69
  convert_to_html_page!(relaton_collection_index, DEFAULT_SITE_INDEX)
66
70
  end
67
-
68
- dequeue_jobs!
69
71
  end
70
72
 
71
73
  private
72
74
 
73
- attr_reader :source, :asset_folder, :asset_directory, :site_path,
75
+ attr_reader :source_path, :asset_folder, :asset_directory, :site_path,
74
76
  :manifest_file, :relaton_collection_index, :stylesheet,
75
77
  :template_dir,
76
78
  :output_filename_template,
77
79
  :base_path
78
80
 
79
- def find_realpath(source_path)
80
- Pathname.new(source_path.to_s).realpath if source_path
81
+ def find_realpath(path)
82
+ Pathname.new(path).realpath if path
81
83
  rescue Errno::ENOENT
82
- source_path
84
+ path
83
85
  end
84
86
 
85
87
  def default_config
86
- default_file = Pathname.new(Dir.pwd).join(DEFAULT_CONFIG_FILE)
88
+ default_file = Pathname.pwd.join(DEFAULT_CONFIG_FILE)
87
89
  default_file if File.exist?(default_file)
88
90
  end
89
91
 
92
+ # @return [Array<Pathname>] the list of ADOC source files
93
+ def select_source_adoc_files
94
+ select_source_files do |source_path|
95
+ source_path.glob("**/*.adoc")
96
+ end
97
+ end
98
+
99
+ # @return [Array<Pathname>] the list of YAML/XML source files
100
+ def select_source_collection_files
101
+ select_source_files do |source_path|
102
+ source_path.glob("**/*.{yaml,yml,xml}")
103
+ end.select do |f|
104
+ collection_file?(f)
105
+ end
106
+ end
107
+
108
+ # Select source files from the manifest if available, otherwise
109
+ # select all .adoc files in the source directory.
110
+ # If a block is given, yield the source directory to the block.
111
+ #
112
+ # @return [Array<Pathname>] the list of source files
113
+ # @yieldparam source [Pathname] the source directory
114
+ # @yieldreturn [Array<Pathname>] the list of source files
115
+ # @example
116
+ # select_source_files do |source|
117
+ # source.glob("**/*.adoc")
118
+ # end
119
+ # # => [#<Pathname:source/1.adoc>, #<Pathname:source/2.adoc>]
120
+ #
90
121
  def select_source_files
91
122
  files = source_from_manifest
92
123
 
93
124
  if files.empty?
94
- files = Dir[File.join(source, "**", "*.adoc")]
125
+ files = if block_given?
126
+ yield(source_path)
127
+ else
128
+ source_path.glob("**/*.adoc")
129
+ end
95
130
  end
96
131
 
97
132
  result = files.flatten
98
133
  result.uniq!
99
- result.reject! { |file| File.directory?(file) }
134
+ result.reject!(&:directory?)
100
135
  result
101
136
  end
102
137
 
138
+ # @dependency: files (YAML, XML, RXL) in asset_directory's parent, from
139
+ # #compile_files! and #compile_collections!
140
+ #
141
+ # This looks for collection artifacts from the `collections`
142
+ # sub-directory, and individual document artifacts from the `documents`
143
+ # sub-directory.
144
+ #
145
+ # @output: documents.xml in site_path
146
+ #
147
+ # @param relaton_collection_index_filename [String] the name of the
148
+ # collection index file (usually documents.xml), but can be changed
149
+ # through the :collection_name option
103
150
  def build_collection_file!(relaton_collection_index_filename)
104
- collection_path = [site_path,
105
- relaton_collection_index_filename].join("/")
151
+ collection_path = site_path.join(relaton_collection_index_filename)
106
152
  UI.info("Building collection file: #{collection_path} ...")
107
153
 
154
+ # First concatenate individual document files
155
+ # But be sure to provide a *relative* path of _site,
156
+ # that is relative to the manifest file itself? or relative to PWD!
157
+ #
158
+ # It has to be relative to PWD, otherwise the resolved relative paths
159
+ # will simply not be valid.
160
+ #
161
+ # If paths are desired to be relative from the manifest file, then
162
+ # `RelatonFile.concatenate` needs to accept a base path option, so
163
+ # `concatenate` can calculate the correct full path to use.
164
+ #
165
+ target_path = asset_directory.parent.relative_path_from(Pathname.pwd)
166
+
108
167
  Relaton::Cli::RelatonFile.concatenate(
109
- asset_folder,
168
+ target_path.to_s,
110
169
  relaton_collection_index_filename,
111
170
  title: manifest[:collection_name],
112
171
  organization: manifest[:collection_organization],
113
172
  )
114
173
  end
115
174
 
116
- def compile_file!(source)
117
- if collection_file?(source)
175
+ # @dependency: file in file_path, from #select_source_files
176
+ # @output: file in asset_folder
177
+ def compile_file!(file_path)
178
+ if collection_file?(file_path)
179
+ @collection_queue << file_path
118
180
  return
119
181
  end
120
182
 
121
- UI.info("Compiling #{source} ...")
183
+ UI.info("Compiling #{file_path} ...")
122
184
 
123
185
  # Incorporate output_filename_template so the output file
124
186
  # can be named as desired, using liquid template and Relaton LiquidDrop
125
187
  options = @compile_options.merge(
126
188
  output_filename_template: output_filename_template,
127
189
  format: :asciidoc,
128
- output_dir: build_asset_output_directory!(source),
190
+ output_dir: ensure_site_asset_output_sub_directory!(file_path),
129
191
  site_generate: true,
130
192
  )
131
193
 
132
- options[:baseassetpath] = Pathname.new(source.to_s).dirname.to_s
133
- Metanorma::Cli::Compiler.compile(source.to_s, options)
194
+ options[:baseassetpath] = Pathname.new(file_path.to_s).dirname.to_s
195
+ Metanorma::Cli::Compiler.compile(file_path.to_s, options)
134
196
  end
135
197
 
198
+ # @dependency: files in source_path, from #select_source_files
199
+ # @output: files in asset_folder
136
200
  def compile_files!(files)
137
- fatals = files.map { |source| compile_file!(source) }
201
+ fatals = files.map { |file| compile_file!(file) }
138
202
  fatals.flatten!
139
203
  fatals.compact!
140
204
 
@@ -156,9 +220,9 @@ module Metanorma
156
220
  end
157
221
  end
158
222
 
159
- def convert_to_html_page!(
160
- relaton_index_filename, page_name
161
- )
223
+ # @dependency: documents.xml from #build_collection_file!
224
+ # @output: index.html in site_path
225
+ def convert_to_html_page!(relaton_index_filename, page_name)
162
226
  UI.info("Generating html site in #{site_path} ...")
163
227
 
164
228
  Relaton::Cli::XMLConvertor.to_html(
@@ -167,10 +231,7 @@ module Metanorma
167
231
  full_path_for(template_dir),
168
232
  )
169
233
 
170
- File.rename(
171
- Pathname.new(relaton_index_filename).sub_ext(".html").to_s,
172
- page_name,
173
- )
234
+ Pathname.new(relaton_index_filename).sub_ext(".html").rename(page_name)
174
235
  end
175
236
 
176
237
  def template_data(node)
@@ -215,52 +276,83 @@ module Metanorma
215
276
  def source_from_manifest
216
277
  @source_from_manifest ||= begin
217
278
  result = manifest[:files].map do |source_file|
218
- file_path = source.join(source_file).to_s
219
- file_path.include?("*") ? Dir.glob(file_path) : file_path
279
+ file_path = source_path.join(source_file)
280
+ if file_path.to_s.include?("*")
281
+ source_path.glob(source_file)
282
+ else
283
+ file_path
284
+ end
220
285
  end
221
286
  result.flatten!
222
287
  result
223
288
  end
224
289
  end
225
290
 
291
+ # Use 'realpath' throughout to ensure consistency with file paths,
292
+ # especially with temporary directories generated in RSpec.
226
293
  def ensure_site_asset_directory!
227
- asset_path = [site_path, asset_folder].join("/")
228
- @asset_directory = Pathname.new(Dir.pwd).join(asset_path)
229
-
230
- create_directory_if_not_present!(@asset_directory)
231
- end
232
-
233
- def create_directory_if_not_present!(directory)
234
- FileUtils.mkdir_p(directory) unless directory.exist?
294
+ asset_path = site_path.join(asset_folder)
295
+ @asset_directory = Pathname.pwd.join(asset_path)
296
+ @asset_directory.mkpath
297
+ @asset_directory = @asset_directory.realpath
298
+ @asset_directory
235
299
  end
236
300
 
237
- def build_asset_output_directory!(source)
238
- sub_directory = Pathname.new(source.gsub(@source.to_s, "")).dirname.to_s
301
+ # TODO: spec
302
+ def ensure_site_asset_output_sub_directory!(source)
303
+ sub_directory = Pathname.new(
304
+ source.to_s.gsub(@source_path.to_s, ""),
305
+ ).dirname.to_s
239
306
  sub_directory.gsub!("/sources", "")
240
- sub_directory.slice!(0)
307
+ sub_directory.sub!(%r{^/}, "")
241
308
 
242
- output_directory = asset_directory.join(sub_directory)
243
- create_directory_if_not_present!(output_directory)
309
+ outdir = asset_directory.join(sub_directory)
310
+ outdir.mkpath
244
311
 
245
- output_directory
312
+ outdir
246
313
  end
247
314
 
315
+ # @param source [Pathname] the source file
248
316
  def collection_file?(source)
249
- ext = File.extname(source)&.downcase
250
-
251
- if [".yml", ".yaml"].include?(ext)
252
- @collection_queue << source
253
- end
317
+ [".yml", ".yaml", ".xml"].include?(source.extname&.downcase)
254
318
  end
255
319
 
256
- def dequeue_jobs!
257
- job = @collection_queue.pop
258
-
259
- if job
320
+ # Compile each collection file encountered in the site manifest file.
321
+ #
322
+ # The collection files are compiled into the `collections` sub-directory
323
+ # under the asset_directory. The output folder specified in each of the
324
+ # collections will be relative to this `collections` folder.
325
+ #
326
+ # Putting the files under the asset_directory is important because
327
+ # the collection files are used to generate the collection index file
328
+ # and the HTML page. It is what `Relaton::Cli::RelatonFile.concatenate`
329
+ # uses to find all artifacts and generate the correct links for them on
330
+ # the site index.
331
+ #
332
+ # Potential conflicts considered:
333
+ # On the one hand, each individual collection.yml specifies its own
334
+ # output folder. This has to be respected.
335
+ #
336
+ # On the other hand, the output folders specified in collection.yml files
337
+ # naturally cannot be expected to live within the `asset_directory`.
338
+ #
339
+ # So, for the build_collection_file! method to correctly consider all
340
+ # generated artifacts, we need to copy the collection files over to the
341
+ # asset_directory.
342
+ #
343
+ # A question you may have: How much does the specific output folder
344
+ # matter, when doing a site generate? Since the intent is to generate a
345
+ # site, the output folder is not really relevant. The collection files
346
+ # are copied over to the asset_directory anyway.
347
+ #
348
+ # TODO: parallelize the compilation of collection files?
349
+ #
350
+ def compile_collections!
351
+ @collection_queue.compact.each do |file|
260
352
  Cli::Collection.render(
261
- job,
353
+ file.to_s,
262
354
  compile: @compile_options,
263
- output_dir: @asset_directory.join(".."),
355
+ output_dir: asset_directory,
264
356
  site_generate: true,
265
357
  )
266
358
  end
@@ -1,5 +1,5 @@
1
1
  module Metanorma
2
2
  module Cli
3
- VERSION = "1.12.4".freeze
3
+ VERSION = "1.12.5".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: metanorma-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.12.4
4
+ version: 1.12.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-04-01 00:00:00.000000000 Z
11
+ date: 2025-04-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: metanorma-ietf