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.
- checksums.yaml +5 -5
- data/.ruby-version +1 -0
- data/Rakefile +2 -3
- data/giblish.gemspec +1 -0
- data/lib/giblish/application.rb +14 -5
- data/lib/giblish/buildgraph.rb +179 -0
- data/lib/giblish/buildindex.rb +361 -414
- data/lib/giblish/cmdline.rb +11 -7
- data/lib/giblish/core.rb +251 -400
- data/lib/giblish/docconverter.rb +231 -0
- data/lib/giblish/docid.rb +107 -58
- data/lib/giblish/docinfo.rb +85 -0
- data/lib/giblish/gititf.rb +1 -0
- data/lib/giblish/utils.rb +29 -5
- data/lib/giblish/version.rb +1 -1
- metadata +21 -3
@@ -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
|
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
|
-
|
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
|
-
|
91
|
-
substitute_ids(File.read(path), path)
|
92
|
-
end
|
112
|
+
private
|
93
113
|
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
#
|
123
|
-
|
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
|
-
|
133
|
-
|
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
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
docid_cache[id]
|
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
|