giblish 0.1.0

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.
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require_relative "utils"
4
+ require_relative "version"
5
+
6
+ # Parse the cmd line arguments
7
+ # This implementation is heavily inspired by the following
8
+ # stack overflow answer:
9
+ # http://stackoverflow.com/questions/26434923/parse-command-line-arguments-in-a-ruby-script
10
+ class CmdLineParser
11
+ attr_accessor :args
12
+
13
+ USAGE = <<~ENDUSAGE.freeze
14
+ Usage:
15
+ giblish [options] source_dir_top dest_dir_top
16
+ ENDUSAGE
17
+ HELP = <<ENDHELP.freeze
18
+ Options:
19
+ -h --help show this help text
20
+ -v --version show version nr and exit
21
+ -f --format <format> the output format, currently html5 or pdf are supported
22
+ *html5* is used if -f is not supplied
23
+ -n --no-build-ref suppress generation of a reference document at the destination
24
+ tree root.
25
+ -r --resource-dir <dir> specify a directory where fonts, themes, css and other
26
+ central stuff needed for document generation are located.
27
+ The resources are expected to be located in a subfolder
28
+ whose name matches the resource type (font, theme
29
+ or css). If no resource dir is specified, the asciidoctor
30
+ defaults are used.
31
+ -s --style <name> The style information used when converting the documents
32
+ using the -r option for specifying resource directories.
33
+ For html this is a name of a css file, for pdf, this is
34
+ the name of an yml file. If no style is given 'giblish'
35
+ is used as default.
36
+ -w --web-root <path> Specifies the top dir (DirectoryRoot) of a file system
37
+ tree published by a web server. This switch is only used
38
+ when generating html. The typical use case is that giblish
39
+ is used to generate html docs which are linked to a css.
40
+ The css link needs to be relative to the top of the web
41
+ tree (DirectoryRoot on Apache) and not the full absolute
42
+ path to the css directory.
43
+ -g --git-branches <regExp> if the source_dir_top is located within a git repo,
44
+ generate docs for all remote branches that matches
45
+ the given regular expression. Each git branch will
46
+ be generated to a separate subdir under the destination
47
+ root dir.
48
+ -t --git-tags <regExp> if the source_dir_top is located within a git repo,
49
+ generate docs for all tags that matches the given
50
+ regular expression. Each tag will be generated to
51
+ a separate subdir under the destination root dir.
52
+ -c --local-only do not try to fetch git info from any remotes of the
53
+ repo before generating documents.
54
+ --log-level set the log level explicitly. Must be one of
55
+ debug, info, warn (default), error or fatal.
56
+ ENDHELP
57
+
58
+ def initialize(cmdline_args)
59
+ parse_cmdline cmdline_args
60
+
61
+ # handle help and version requests
62
+ if @args[:help]
63
+ puts HELP
64
+ exit
65
+ end
66
+ if @args[:version]
67
+ puts "Giblish v#{Giblish::VERSION}"
68
+ exit
69
+ end
70
+
71
+ # set log level
72
+ set_log_level
73
+ sanity_check_input
74
+ set_gitrepo_root
75
+ # if @args[:logfile]
76
+ # $stdout.reopen( ARGS[:logfile], "w" )
77
+ # $stdout.sync = true
78
+ # $stderr.reopen( $stdout )
79
+ # end
80
+ end
81
+
82
+ def usage
83
+ USAGE
84
+ end
85
+
86
+ private
87
+
88
+ def set_log_level
89
+ log_level = @args[:logLevel] || "warn"
90
+ case log_level
91
+ when "debug" then Giblog.logger.sev_threshold = Logger::DEBUG
92
+ when "info" then Giblog.logger.sev_threshold = Logger::INFO
93
+ when "warn" then Giblog.logger.sev_threshold = Logger::WARN
94
+ when "error" then Giblog.logger.sev_threshold = Logger::ERROR
95
+ when "fatal" then Giblog.logger.sev_threshold = Logger::FATAL
96
+ else
97
+ puts "Invalid log level specified. Run with -h to see supported levels"
98
+ puts USAGE
99
+ exit
100
+ end
101
+ end
102
+
103
+ def sanity_check_input
104
+ ensure_required_args
105
+ prevent_invalid_combos
106
+ end
107
+
108
+ def parse_cmdline(cmdline_args)
109
+ # default values for cmd line switches
110
+ @args = {
111
+ help: false,
112
+ version: false,
113
+ force: true,
114
+ format: "html5",
115
+ flatten: false,
116
+ suppressBuildRef: false,
117
+ localRepoOnly: false,
118
+ webRoot: false
119
+ }
120
+
121
+ # set default log level
122
+ Giblog.logger.sev_threshold = Logger::WARN
123
+
124
+ # defines args without a corresponding flag, the order is important
125
+ unflagged_args = %i[srcDirRoot dstDirRoot]
126
+
127
+ # parse cmd line
128
+ next_arg = unflagged_args.first
129
+ cmdline_args.each do |arg|
130
+ case arg
131
+ when "-h", "--help" then @args[:help] = true
132
+ when "-v", "--version" then @args[:version] = true
133
+ when "-f", "--format " then next_arg = :format
134
+ when "-r", "--resource-dir" then next_arg = :resourceDir
135
+ when "-n", "--no-build-ref" then @args[:suppressBuildRef] = true
136
+ when "-g", "--git-branches" then next_arg = :gitBranchRegexp
137
+ when "-t", "--git-tags" then next_arg = :gitTagRegexp
138
+ when "-c", "--local-only" then @args[:localRepoOnly] = true
139
+ when "-s", "--style" then next_arg = :userStyle
140
+ when "-w", "--web-root" then next_arg = :webRoot
141
+ when "--log-level" then next_arg = :logLevel
142
+ else
143
+ if next_arg
144
+ @args[next_arg] = arg
145
+ unflagged_args.delete(next_arg)
146
+ end
147
+ next_arg = unflagged_args.first
148
+ end
149
+ end
150
+ end
151
+
152
+ def prevent_invalid_combos
153
+ # Prevent contradicting options
154
+ if !@args[:resourceDir] && @args[:userStyle]
155
+ puts "Error: The given style would not be used since no resource dir "\
156
+ "was specified (-s specified without -r)"
157
+ else
158
+ return
159
+ end
160
+
161
+ puts USAGE
162
+ exit
163
+ end
164
+
165
+ def ensure_required_args
166
+ # Exit if the user does not supply the required arguments
167
+ return unless !@args[:srcDirRoot] || !@args[:dstDirRoot]
168
+
169
+ puts "Error: Too few arguments."
170
+ puts USAGE
171
+ exit
172
+ end
173
+
174
+ def set_gitrepo_root
175
+ # if user don't want no git repo, we're done
176
+ return unless @args[:gitBranchRegexp] || @args[:gitTagRegexp]
177
+
178
+ # The user wants to parse a git repo, check that the srcDirRoot is within a
179
+ # git repo if the user wants to generate git-branch specific docs
180
+ @args[:gitRepoRoot] = Giblish::PathManager.find_gitrepo_root(
181
+ @args[:srcDirRoot]
182
+ )
183
+ return unless @args[:gitRepoRoot].nil?
184
+
185
+ # We should not get here if everything is koscher...
186
+ puts "Error: Source dir not in a git working dir despite -g or -t option given!"
187
+ puts USAGE
188
+ exit
189
+ end
190
+ end
@@ -0,0 +1,375 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Converts a tree of asciidoc docs to pdf/html
4
+ #
5
+ #
6
+
7
+ require "find"
8
+ require "fileutils"
9
+ require "logger"
10
+ require "pathname"
11
+ require "asciidoctor"
12
+ require "asciidoctor-pdf"
13
+
14
+ require_relative "cmdline"
15
+ require_relative "buildindex"
16
+
17
+ # Base class for document converters. It contains a hash of
18
+ # conversion options used by derived classes
19
+ class DocConverter
20
+ # a common set of converter options used for all output formats
21
+ COMMON_CONVERTER_OPTS = {
22
+ safe: Asciidoctor::SafeMode::UNSAFE,
23
+ header_footer: true,
24
+ mkdirs: true
25
+ }.freeze
26
+
27
+ # setup common options that are used regardless of the
28
+ # specific output format used
29
+ attr_reader :converter_options
30
+
31
+ # Public: Setup common converter options. Required options are:
32
+ # :srcDirRoot
33
+ # :dstDirRoot
34
+ # :resourceDir
35
+ def initialize(options)
36
+ @paths = Giblish::PathManager.new(
37
+ options[:srcDirRoot], options[:dstDirRoot], options[:resourceDir]
38
+ )
39
+
40
+ @user_style = options[:userStyle]
41
+ @converter_options = COMMON_CONVERTER_OPTS.dup
42
+ @converter_options[:backend] = options[:backend]
43
+ end
44
+
45
+ # Public: Convert one single adoc file using the specific conversion
46
+ # options.
47
+ #
48
+ # filepath - a string with the absolute path to the input file to convert
49
+ #
50
+ # Returns: The resulting Asciidoctor::Document object
51
+ def convert(filepath)
52
+ Giblog.logger.info { "Processing: #{filepath}" }
53
+
54
+ # create an asciidoc doc object and convert to requested
55
+ # output using current conversion options
56
+ @converter_options[:to_dir] = @paths.adoc_output_dir(filepath).to_s
57
+ @converter_options[:base_dir] = Giblish::PathManager.closest_dir(
58
+ filepath
59
+ ).to_s
60
+ @converter_options[:to_file] = Giblish::PathManager.get_new_basename(
61
+ filepath,
62
+ @converter_options[:fileext]
63
+ )
64
+
65
+
66
+ Giblog.logger.debug { "converter_options: #{@converter_options}" }
67
+ # do the actual conversion
68
+ Asciidoctor.convert_file filepath, @converter_options
69
+ end
70
+
71
+ protected
72
+
73
+ # Protected: Adds the supplied backend specific options and
74
+ # attributes to the base ones.
75
+ # The following options must be provided by the derived class:
76
+ # :fileext - a string with the filename extention to use for the
77
+ # generated file
78
+ #
79
+ # backend_opts - the options specific to the asciidoctor backend
80
+ # that the derived class supports
81
+ # backend_attribs - the attributes specific to the asciidoctor backend
82
+ # that the derived class supports
83
+ def add_backend_options(backend_opts, backend_attribs)
84
+ @converter_options = @converter_options.merge(backend_opts)
85
+ @converter_options[:attributes] = backend_attribs
86
+ end
87
+ end
88
+
89
+ # Converts asciidoc files to html5 output.
90
+ class HtmlConverter < DocConverter
91
+ # Public: Setup common converter options. Required options are:
92
+ # :srcDirRoot
93
+ # :dstDirRoot
94
+ # :resourceDir
95
+ def initialize(options)
96
+ super options
97
+
98
+ # handle needed assets for the styling (css et al)
99
+ html_attrib = setup_web_assets options[:webRoot]
100
+
101
+ # Setting 'data-uri' makes asciidoctor embed images in the resulting
102
+ # html file
103
+ html_attrib["data-uri"] = 1
104
+
105
+ # tell asciidoctor to use the html5 backend
106
+ backend_options = { backend: "html5", fileext: "html" }
107
+ add_backend_options backend_options, html_attrib
108
+ end
109
+
110
+ private
111
+
112
+ def setup_stylesheet_attributes(css_dir)
113
+ return {} if @paths.resource_dir_abs.nil?
114
+
115
+ # use the supplied stylesheet if there is one
116
+ attrib = { "linkcss" => 1,
117
+ "stylesdir" => css_dir,
118
+ "stylesheet" => "giblish.css",
119
+ "copycss!" => 1 }
120
+
121
+ # Make sure that a user supplied stylesheet ends with .css or .CSS
122
+ @user_style &&
123
+ attrib["stylesheet"] =
124
+ /\.(css|CSS)$/ =~ @user_style ? @user_style : "#{@user_style}.css"
125
+
126
+ attrib
127
+ end
128
+
129
+ # make sure that linked assets are available at dst_root
130
+ def setup_web_assets(html_dir_root = nil)
131
+ # only set this up if user has specified a resource dir
132
+ return {} unless @paths.resource_dir_abs
133
+
134
+ # create dir for web assets directly under dst_root
135
+ assets_dir = "#{@paths.dst_root_abs}/web_assets"
136
+ Dir.exist?(assets_dir) || FileUtils.mkdir_p(assets_dir)
137
+
138
+ # copy needed assets
139
+ %i[css fonts images].each do |dir|
140
+ src = "#{@paths.resource_dir_abs}/#{dir}"
141
+ Dir.exist?(src) && FileUtils.copy_entry(src, "#{assets_dir}/#{dir}")
142
+ end
143
+
144
+ # find the path to the assets dir that is correct when called from a url,
145
+ # taking the DirectoryRoot for the web site into consideration.
146
+ if html_dir_root
147
+ wr = Pathname.new(
148
+ assets_dir
149
+ ).relative_path_from Pathname.new(html_dir_root)
150
+ Giblog.logger.info { "Relative web root: #{wr}" }
151
+ assets_dir = "/" << wr.to_s
152
+ end
153
+
154
+ Giblog.logger.info { "stylesheet dir: #{assets_dir}" }
155
+ setup_stylesheet_attributes "#{assets_dir}/css"
156
+ end
157
+ end
158
+
159
+ class PdfConverter < DocConverter
160
+ def initialize(options)
161
+ super options
162
+
163
+ pdf_attrib = {
164
+ "pdf-stylesdir" => "#{@paths.resource_dir_abs}/themes",
165
+ "pdf-style" => "giblish.yml",
166
+ "pdf-fontsdir" => "#{@paths.resource_dir_abs}/fonts",
167
+ "icons" => "font"
168
+ }
169
+
170
+ # Make sure that the stylesheet ends with .yml or YML
171
+ @user_style &&
172
+ pdf_attrib["pdf-style"] =
173
+ /\.(yml|YML)$/ =~ @user_style ? @user_style : "#{@user_style}.yml"
174
+
175
+ backend_options = { backend: "pdf", fileext: "pdf" }
176
+ add_backend_options backend_options, pdf_attrib
177
+ end
178
+ end
179
+
180
+ class TreeConverter
181
+
182
+ # def init_dst_root
183
+ # # make sure destination dir exists
184
+ # Giblog.logger.info do
185
+ # "Will generate docs to destination root: #{@paths.dst_root_abs}"
186
+ # end
187
+ # Dir.exist?(@paths.dst_root_abs) || FileUtils.mkdir_p(@paths.dst_root_abs)
188
+ # end
189
+
190
+ # Required options:
191
+ # srcDirRoot
192
+ # dstDirRoot
193
+ # resourceDir
194
+ def initialize(options)
195
+ @options = options.dup
196
+
197
+ @paths = Giblish::PathManager.new(
198
+ @options[:srcDirRoot],
199
+ @options[:dstDirRoot],
200
+ @options[:resourceDir]
201
+ )
202
+
203
+ # init_dst_root
204
+
205
+ # prepare the index page if requested
206
+ unless @options[:suppressBuildRef]
207
+ @index_builder = if @options[:gitRepoRoot]
208
+ GitRepoIndexBuilder.new(@paths, options[:gitRepoRoot])
209
+ else
210
+ SimpleIndexBuilder.new(@paths)
211
+ end
212
+ end
213
+
214
+ @conversion =
215
+ case options[:format]
216
+ when "html" then HtmlConverter.new options
217
+ when "pdf" then PdfConverter.new options
218
+ else
219
+ raise ArgumentError, "Unknown conversion format: #{options[:format]}"
220
+ end
221
+ end
222
+
223
+ def generate_index(src_str, dst_dir)
224
+ # use the same options as when converting all docs
225
+ # in the tree but make sure we don't write to file
226
+ index_opts = @conversion.converter_options.dup
227
+ index_opts.delete(:to_file)
228
+ index_opts.delete(:to_dir)
229
+
230
+ # load and convert the document using the converter options
231
+ doc = Asciidoctor.load src_str, index_opts
232
+ output = doc.convert index_opts
233
+
234
+ # write the converted document to an index file located at the
235
+ # destination root
236
+ index_filepath = dst_dir + "index.#{index_opts[:fileext]}"
237
+ doc.write output, index_filepath.to_s
238
+ end
239
+
240
+ def to_asciidoc(filepath)
241
+ adoc = nil
242
+ begin
243
+ # do the conversion and capture eventual errors that
244
+ # the asciidoctor lib writes to stderr
245
+ adoc_stderr = Giblish.with_captured_stderr do
246
+ adoc = @conversion.convert(filepath)
247
+ end
248
+
249
+ # build the reference index if the user wants it
250
+ @options[:suppressBuildRef] || @index_builder.add_doc(adoc, adoc_stderr)
251
+ rescue Exception => e
252
+ str = "Error when converting doc: #{e.message}\n"
253
+ e.backtrace.each { |l| str << "#{l}\n" }
254
+ Giblog.logger.warn { str }
255
+ @options[:suppressBuildRef] || @index_builder.add_doc_fail(filepath, e)
256
+ end
257
+ end
258
+
259
+ def walk_dirs
260
+ # traverse the src file tree and convert all files that ends with
261
+ # .adoc or .ADOC
262
+ Find.find(@paths.src_root_abs) do |path|
263
+ ext = File.extname(path)
264
+ to_asciidoc(path) if !ext.empty? && ext.casecmp(".ADOC").zero?
265
+ end
266
+
267
+ # check if we shall build index or not
268
+ return if @options[:suppressBuildRef]
269
+
270
+ # build a reference index
271
+ generate_index @index_builder.index_source, @paths.dst_root_abs
272
+
273
+ # clean up adoc resources
274
+ @index_builder = nil
275
+ GC.start
276
+ end
277
+ end
278
+
279
+ class GitRepoParser
280
+ def initialize(options)
281
+ @options = options
282
+ @paths = Giblish::PathManager.new(
283
+ @options[:srcDirRoot],
284
+ @options[:dstDirRoot],
285
+ @options[:resourceDir]
286
+ )
287
+ @git_repo_root = options[:gitRepoRoot]
288
+
289
+ # Sanity check git repo root
290
+ @git_repo_root || raise(ArgumentError("No git repo root dir given"))
291
+
292
+ # Connect to the git repo
293
+ begin
294
+ @git_repo = Git.open(@git_repo_root)
295
+ rescue Exception => e
296
+ raise "Could not find a git repo at #{@git_repo_root} !"\
297
+ "\n\n(#{e.message})"
298
+ end
299
+
300
+ # fetch all remote refs if ok with user
301
+ begin
302
+ @git_repo.fetch unless options[:localRepoOnly]
303
+ rescue Exception => e
304
+ raise "Could not fetch from origin"\
305
+ "(do you need '--local-only'?)!\n\n(#{e.message})"
306
+ end
307
+
308
+ # initialize summary builder
309
+ @index_builder = GitSummaryIndexBuilder.new @git_repo
310
+
311
+ # Get the branches the user wants to parse
312
+ if options[:gitBranchRegexp]
313
+ regexp = Regexp.new options[:gitBranchRegexp]
314
+ @user_branches = @git_repo.branches.remote.select do |b|
315
+ regexp.match b.name
316
+ end
317
+
318
+ # Render the docs from each branch and add info to the
319
+ # summary page
320
+ @user_branches.each do |b|
321
+ render_one_branch b, @options
322
+ @index_builder.add_branch b
323
+ end
324
+ end
325
+
326
+ # Get the branches the user wants to parse
327
+ if options[:gitTagRegexp]
328
+ regexp = Regexp.new options[:gitTagRegexp]
329
+ @user_tags = @git_repo.tags.select do |t|
330
+ regexp.match t.name
331
+ end
332
+
333
+ # Render the docs from each branch and add info to the
334
+ # summary page
335
+ @user_tags.each do |t|
336
+ render_one_branch t, @options, true
337
+ @index_builder.add_tag t
338
+ end
339
+ end
340
+
341
+ # Render the summary page
342
+ tc = TreeConverter.new options
343
+ tc.generate_index @index_builder.index_source, @paths.dst_root_abs
344
+
345
+ # clean up
346
+ @index_builder = nil
347
+ GC.start
348
+ end
349
+
350
+ def render_one_branch(b, opts, is_tag = false)
351
+ # work with local options
352
+ options = opts.dup
353
+
354
+ # check out the branch in question and make sure it is
355
+ # up-to-date
356
+ Giblog.logger.info { "Checking out #{b.name}" }
357
+ @git_repo.checkout b.name
358
+
359
+ unless is_tag
360
+ Giblog.logger.info { "Merging with origin/#{b.name}" }
361
+ @git_repo.merge "origin/#{b.name}"
362
+ end
363
+
364
+ # assign a branch-unique dst-dir
365
+ dir_name = b.name.tr("/", "_") << "/"
366
+
367
+ # Assign the branch specific dir as new destination root
368
+ options[:dstDirRoot] = @paths.dst_root_abs.realpath.join(dir_name).to_s
369
+
370
+ # Parse and render docs using given args
371
+ Giblog.logger.info { "Render docs to dir #{options[:dstDirRoot]}" }
372
+ tc = TreeConverter.new options
373
+ tc.walk_dirs
374
+ end
375
+ end