giblish 0.2.12 → 0.3.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,231 @@
1
+ require "pathname"
2
+ require "asciidoctor"
3
+ require "asciidoctor-pdf"
4
+
5
+ require_relative "utils"
6
+
7
+ module Giblish
8
+
9
+ # Base class for document converters. It contains a hash of
10
+ # conversion options used by derived classes
11
+ class DocConverter
12
+ # a common set of converter options used for all output formats
13
+ COMMON_CONVERTER_OPTS = {
14
+ safe: Asciidoctor::SafeMode::UNSAFE,
15
+ header_footer: true,
16
+ mkdirs: true
17
+ }.freeze
18
+
19
+ # the giblish attribute defaults used if nothing else
20
+ # is required by the user
21
+ DEFAULT_ATTRIBUTES = {
22
+ "source-highlighter" => "rouge",
23
+ "xrefstyle" => "short"
24
+ }.freeze
25
+
26
+ # setup common options that are used regardless of the
27
+ # specific output format used
28
+ attr_reader :converter_options
29
+
30
+ # the path manager used by this converter
31
+ attr_accessor :paths
32
+
33
+ def initialize(paths, options)
34
+ # access the source highlight module
35
+ require "asciidoctor-rouge"
36
+
37
+ @paths = paths
38
+ @user_style = options[:userStyle]
39
+ @converter_options = COMMON_CONVERTER_OPTS.dup
40
+ @converter_options[:attributes] = DEFAULT_ATTRIBUTES.dup
41
+ @converter_options[:backend] = options[:backend]
42
+ end
43
+
44
+ # Public: Convert one single adoc file using the specific conversion
45
+ # options.
46
+ #
47
+ # filepath - a pathname with the absolute path to the input file to convert
48
+ #
49
+ # Returns: The resulting Asciidoctor::Document object
50
+ def convert(filepath)
51
+ unless filepath.is_a?(Pathname)
52
+ raise ArgumentError, "Trying to invoke convert with non-pathname!"
53
+ end
54
+
55
+ Giblog.logger.info {"Processing: #{filepath}"}
56
+
57
+ # create an asciidoc doc object and convert to requested
58
+ # output using current conversion options
59
+ @converter_options[:to_dir] = @paths.adoc_output_dir(filepath).to_s
60
+ @converter_options[:base_dir] =
61
+ Giblish::PathManager.closest_dir(filepath).to_s
62
+ @converter_options[:to_file] =
63
+ Giblish::PathManager.get_new_basename(filepath,
64
+ @converter_options[:fileext])
65
+
66
+ Giblog.logger.debug {"converter_options: #{@converter_options}"}
67
+
68
+ # do the actual conversion
69
+ Asciidoctor.convert_file filepath, @converter_options
70
+ end
71
+
72
+ # converts the supplied string to the file
73
+ # dst_dir/basename.<backend-ext>
74
+ #
75
+ # the supplied string must pass asciidoctor without
76
+ # any error to stderr, otherwise, nothing will be written
77
+ # to disk.
78
+ # returns 'true' if a file was written, 'false' if not
79
+ def convert_str(src_str, dst_dir, basename)
80
+ index_opts = @converter_options.dup
81
+
82
+ # use the same options as when converting all docs
83
+ # in the tree but make sure we don't write to file
84
+ # by trial and error, the following dirs seem to be
85
+ # necessary to change
86
+ index_opts[:to_dir] = dst_dir.to_s
87
+ index_opts[:base_dir] = dst_dir.to_s
88
+ index_opts.delete_if {|k, _v| %i[to_file].include? k}
89
+
90
+ # load and convert the document using the converter options
91
+ doc = nil, output = nil
92
+ adoc_stderr = Giblish.with_captured_stderr do
93
+ doc = Asciidoctor.load src_str, index_opts
94
+ output = doc.convert index_opts
95
+ end
96
+
97
+ index_filepath = dst_dir + "#{basename}.#{index_opts[:fileext]}"
98
+
99
+ # if we get anything from asciidoctor to stderr,
100
+ # consider this a failure and do not emit a file.
101
+ if !adoc_stderr.length.zero?
102
+ Giblog.logger.error {"Errors when converting string to asciidoc!!"}
103
+ Giblog.logger.error {"Will _not_ generate file #{index_filepath.to_s}"}
104
+ Giblog.logger.error {"Got following warnings/errors from asciidoc conversion:"}
105
+ Giblog.logger.error {adoc_stderr}
106
+ return false
107
+ end
108
+
109
+ # write the converted document to an index file located at the
110
+ # destination root
111
+ doc.write output, index_filepath.to_s
112
+ true
113
+ end
114
+
115
+ protected
116
+
117
+ # Protected: Adds the supplied backend specific options and
118
+ # attributes to the base ones.
119
+ # The following options must be provided by the derived class:
120
+ # :fileext - a string with the filename extention to use for the
121
+ # generated file
122
+ #
123
+ # backend_opts - the options specific to the asciidoctor backend
124
+ # that the derived class supports
125
+ # backend_attribs - the attributes specific to the asciidoctor backend
126
+ # that the derived class supports
127
+ def add_backend_options(backend_opts, backend_attribs)
128
+ @converter_options = @converter_options.merge(backend_opts)
129
+ @converter_options[:attributes] =
130
+ @converter_options[:attributes].merge(backend_attribs)
131
+ end
132
+ end
133
+
134
+ # Converts asciidoc files to html5 output.
135
+ class HtmlConverter < DocConverter
136
+ def initialize(paths, options)
137
+ super paths, options
138
+
139
+ # handle needed assets for the styling (css et al)
140
+ html_attrib = setup_web_assets options[:webRoot]
141
+
142
+ # Setting 'data-uri' makes asciidoctor embed images in the resulting
143
+ # html file
144
+ html_attrib["data-uri"] = 1
145
+
146
+ # tell asciidoctor to use the html5 backend
147
+ backend_options = {backend: "html5", fileext: "html"}
148
+ add_backend_options backend_options, html_attrib
149
+ end
150
+
151
+ private
152
+
153
+ def setup_stylesheet_attributes(css_dir)
154
+ return {} if @paths.resource_dir_abs.nil?
155
+
156
+ # use the supplied stylesheet if there is one
157
+ attrib = {"linkcss" => 1,
158
+ "stylesdir" => css_dir,
159
+ "stylesheet" => "giblish.css",
160
+ "copycss!" => 1}
161
+
162
+ # Make sure that a user supplied stylesheet ends with .css or .CSS
163
+ @user_style &&
164
+ attrib["stylesheet"] =
165
+ /\.(css|CSS)$/ =~ @user_style ? @user_style : "#{@user_style}.css"
166
+ Giblog.logger.debug {"stylesheet attributes: #{attrib}"}
167
+ attrib
168
+ end
169
+
170
+ # make sure that linked assets are available at dst_root
171
+ def setup_web_assets(html_dir_root = nil)
172
+ # only set this up if user has specified a resource dir
173
+ return {} unless @paths.resource_dir_abs
174
+
175
+ # create dir for web assets directly under dst_root
176
+ assets_dir = "#{@paths.dst_root_abs}/web_assets"
177
+ Dir.exist?(assets_dir) || FileUtils.mkdir_p(assets_dir)
178
+
179
+ # copy needed assets
180
+ %i[css fonts images].each do |dir|
181
+ src = "#{@paths.resource_dir_abs}/#{dir}"
182
+ Dir.exist?(src) && FileUtils.copy_entry(src, "#{assets_dir}/#{dir}")
183
+ end
184
+
185
+ # find the path to the assets dir that is correct when called from a url,
186
+ # taking the DirectoryRoot for the web site into consideration.
187
+ if html_dir_root
188
+ wr = Pathname.new(
189
+ assets_dir
190
+ ).relative_path_from Pathname.new(html_dir_root)
191
+ Giblog.logger.info {"Relative web root: #{wr}"}
192
+ assets_dir = "/" << wr.to_s
193
+ end
194
+
195
+ Giblog.logger.info {"stylesheet dir: #{assets_dir}"}
196
+ setup_stylesheet_attributes "#{assets_dir}/css"
197
+ end
198
+ end
199
+
200
+ class PdfConverter < DocConverter
201
+ def initialize(paths, options)
202
+ super paths, options
203
+
204
+ pdf_attrib = setup_pdf_attribs
205
+
206
+ backend_options = {backend: "pdf", fileext: "pdf"}
207
+ add_backend_options backend_options, pdf_attrib
208
+ end
209
+
210
+ private
211
+
212
+ def setup_pdf_attribs()
213
+ # only set this up if user has specified a resource dir
214
+ return {} unless @paths.resource_dir_abs
215
+
216
+ pdf_attrib = {
217
+ "pdf-stylesdir" => "#{@paths.resource_dir_abs}/themes",
218
+ "pdf-style" => "giblish.yml",
219
+ "pdf-fontsdir" => "#{@paths.resource_dir_abs}/fonts",
220
+ "icons" => "font"
221
+ }
222
+
223
+ # Make sure that the stylesheet ends with .yml or YML
224
+ @user_style &&
225
+ pdf_attrib["pdf-style"] =
226
+ /\.(yml|YML)$/ =~ @user_style ? @user_style : "#{@user_style}.yml"
227
+
228
+ pdf_attrib
229
+ end
230
+ end
231
+ end
data/lib/giblish/docid.rb CHANGED
@@ -1,23 +1,35 @@
1
1
 
2
2
  require_relative "./utils"
3
-
4
- require 'asciidoctor'
5
- require 'asciidoctor/extensions'
3
+ require "asciidoctor"
4
+ require "asciidoctor/extensions"
6
5
 
7
6
  module Giblish
8
7
  # Parse all adoc files for :docid: attributes
9
8
  class DocidCollector < Asciidoctor::Extensions::Preprocessor
10
9
  # Use a class-global docid_cache since asciidoctor creates a new instance
11
10
  # for each preprocessor hook
11
+ # a hash of {doc_id => Pathname(src_path)}
12
12
  @docid_cache = {}
13
+
14
+ # A class-global hash of {src_path => [target doc_ids] }
15
+ @docid_deps = {}
16
+
13
17
  class << self
14
18
  def docid_cache
15
19
  @docid_cache
16
20
  end
17
21
 
22
+ def docid_deps
23
+ @docid_deps
24
+ end
25
+
18
26
  def clear_cache
19
27
  @docid_cache = {}
20
28
  end
29
+
30
+ def clear_deps
31
+ @docid_deps = {}
32
+ end
21
33
  end
22
34
 
23
35
  # The minimum number of characters required for a valid doc id
@@ -32,30 +44,6 @@ module Giblish
32
44
  # super(everything)
33
45
  # end
34
46
 
35
- # Helper method that provides the user with a way of processing only the
36
- # lines within the asciidoc header block.
37
- # The user must return nil to get the next line.
38
- #
39
- # ex:
40
- # process_header_lines(file_path) do |line|
41
- # if line == "Quack!"
42
- # puts "Donald!"
43
- # 1
44
- # else
45
- # nil
46
- # end
47
- # end
48
- def process_header_lines(path)
49
- state = "before_header"
50
- File.foreach(path) do |line|
51
- case state
52
- when "before_header" then (state = "in_header" if line =~ /^=+.*$/)
53
- when "in_header" then (state = "done" if line =~ /^\s*$/ || yield(line))
54
- when "done" then break
55
- end
56
- end
57
- end
58
-
59
47
  # Check if a :docid: <id> entry exists in the header.
60
48
  # According to http://www.methods.co.nz/asciidoc/userguide.html#X95
61
49
  # the header is optional, but if it exists it:
@@ -73,40 +61,95 @@ module Giblish
73
61
  end
74
62
  end
75
63
 
64
+ # add a new source document to the docid_deps
65
+ def add_source_dep(src_path)
66
+ return if docid_deps.key? src_path
67
+ docid_deps[src_path] = []
68
+ end
69
+
76
70
  # This hook is called by Asciidoctor once for each document _before_
77
71
  # Asciidoctor processes the adoc content.
78
72
  #
79
73
  # It replaces references of the format <<:docid: ID-1234,Hello >> with
80
74
  # references to a resolved relative path.
81
75
  def process(document, reader)
76
+ # Add doc as a source dependency for doc ids
77
+ src_path = document.attributes["docfile"]
78
+
79
+ # Note: the nil check is there to prevent us adding generated
80
+ # asciidoc docs that does not exist in the file system (e.g. the
81
+ # generated index pages). This is a bit hackish and should maybe be
82
+ # done differently
83
+ return if src_path.nil?
84
+
85
+ add_source_dep src_path
86
+
87
+ # Convert all docid refs to valid relative refs
82
88
  reader.lines.each do |line|
83
89
  line.gsub!(/<<\s*:docid:\s*(.*)>>/) do |_m|
84
- replace_doc_id Regexp.last_match(1), document.attributes["docfile"]
90
+ # parse the ref
91
+ target_id, section, display_str =
92
+ parse_doc_id_ref Regexp.last_match(1)
93
+
94
+ # The result is a valid ref in the form
95
+ # <<target_doc.adoc#[section][,display_str]>>
96
+ Giblog.logger.debug { "Replace docid ref in doc #{src_path}..." }
97
+ if docid_cache.key? target_id
98
+ # add the referenced doc id as a target dependency of this document
99
+ docid_deps[src_path] << target_id
100
+ docid_deps[src_path] = docid_deps[src_path].uniq
101
+
102
+ # resolve the doc id ref to a valid relative path
103
+ "<<#{get_rel_path(src_path, target_id)}##{section}#{display_str}>>"
104
+ else
105
+ "<<UNKNOWN_DOC, Could not resolve doc id reference !!!>>"
106
+ end
85
107
  end
86
108
  end
87
109
  reader
88
110
  end
89
111
 
90
- def substitute_ids_file(path)
91
- substitute_ids(File.read(path), path)
92
- end
112
+ private
93
113
 
94
- def substitute_ids(src_str, src_path)
95
- src_str.gsub!(/<<\s*:docid:\s*(.*)>>/) do |_m|
96
- replace_doc_id Regexp.last_match(1), src_path
114
+ # Helper method that provides the user with a way of processing only the
115
+ # lines within the asciidoc header block.
116
+ # The user must return nil to get the next line.
117
+ #
118
+ # ex:
119
+ # process_header_lines(file_path) do |line|
120
+ # if line == "Quack!"
121
+ # puts "Donald!"
122
+ # 1
123
+ # else
124
+ # nil
125
+ # end
126
+ # end
127
+ def process_header_lines(path)
128
+ state = "before_header"
129
+ File.foreach(path) do |line|
130
+ case state
131
+ when "before_header" then (state = "in_header" if line =~ /^=+.*$/)
132
+ when "in_header" then (state = "done" if line =~ /^\s*$/ || yield(line))
133
+ when "done" then break
134
+ end
97
135
  end
98
- src_str
99
136
  end
100
137
 
101
- private
102
-
103
138
  # Helper method to shorten calls to docid_cache from instance methods
104
139
  def docid_cache
105
140
  self.class.docid_cache
106
141
  end
107
142
 
143
+ def docid_deps
144
+ self.class.docid_deps
145
+ end
146
+
147
+ # Get the relative path from the src doc to the
148
+ # doc with the given doc id
108
149
  def get_rel_path(src_path, doc_id)
109
- return "UNKNOWN_DOC" unless docid_cache.key? doc_id
150
+ unless docid_cache.key? doc_id
151
+ raise ArgumentError("unknown doc id: #{doc_id}")
152
+ end
110
153
 
111
154
  rel_path = docid_cache[doc_id]
112
155
  .dirname
@@ -115,43 +158,49 @@ module Giblish
115
158
  rel_path.to_s
116
159
  end
117
160
 
118
- # The input string shall contain the expression between
161
+ # input_str shall be the expression between
119
162
  # <<:docid:<input_str>>> where the <input_str> is in the form
120
163
  # <id>[#section][,display_str]
121
164
  #
122
- # The result shall be a valid ref in the form
123
- # <<target_doc.adoc#[section][,display_str]>>
124
- def replace_doc_id(input_str, src_path)
165
+ # returns an array with [id, section, display_str]
166
+ def parse_doc_id_ref(input_str)
125
167
  ref, display_str = input_str.split(",").each(&:strip)
126
- display_str = "" if display_str.nil?
127
- display_str.prepend "," if display_str.length.positive?
128
-
129
168
  id, section = ref.split "#"
169
+
170
+ display_str = id.dup if display_str.nil?
171
+ display_str.prepend ","
172
+
130
173
  section = "" if section.nil?
131
174
 
132
- Giblog.logger.debug { "Replace docid ref in doc #{src_path}..." }
133
- "<<#{get_rel_path(src_path, id)}##{section}#{display_str}>>"
175
+ [id, section, display_str]
176
+ end
177
+
178
+ # make sure the id is within the designated length and
179
+ # does not contain a '#' symbol
180
+ def doc_id_ok?(doc_id)
181
+ (doc_id.length.between?(ID_MIN_LENGTH, ID_MAX_LENGTH) &&
182
+ !doc_id.include?("#"))
134
183
  end
135
184
 
136
185
  def validate_and_add(doc_id, path)
137
186
  id = doc_id.strip
138
187
  Giblog.logger.debug { "found possible docid: #{id}" }
139
188
 
140
- # make sure the id is within the designated length and
141
- # does not contain a '#' symbol
142
- if id.length.between?(ID_MIN_LENGTH, ID_MAX_LENGTH) &&
143
- !id.include?("#")
144
- # the id is ok
145
- if docid_cache.key? id
146
- Giblog.logger.warn { "Found same doc id twice (#{id}). Using last found id."}
147
- end
148
- docid_cache[id] = Pathname(path)
149
- else
150
- Giblog.logger.error { "Invalid docid: #{id}, this will be ignored!" }
189
+ unless doc_id_ok? doc_id
190
+ Giblog.logger.error { "Invalid docid: #{id} in file #{path}, this will be ignored!" }
191
+ return
192
+ end
193
+
194
+ if docid_cache.key? id
195
+ Giblog.logger.warn { "Found same doc id twice (#{id})." }
196
+ Giblog.logger.warn { "Assigning this id to the file #{path}." }
197
+ Giblog.logger.warn { "Discarding this id from the file #{docid_cache[id]}." }
151
198
  end
199
+ docid_cache[id] = Pathname(path)
152
200
  end
153
201
  end
154
202
 
203
+
155
204
  # Helper method to register the docid preprocessor extension with
156
205
  # the asciidoctor engine.
157
206
  def register_extensions