1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
|
module Jekyll
module AsciiDoc
class Converter < ::Jekyll::Converter
DefaultAttributes = {
'idprefix' => '',
'idseparator' => '-',
'linkattrs' => '@',
}
DefaultFileExtensions = %w(asciidoc adoc ad)
DefaultPageAttributePrefix = 'page'
ImplicitAttributes = {
'env' => 'site',
'env-site' => '',
'site-gen' => 'jekyll',
'site-gen-jekyll' => '',
'builder' => 'jekyll',
'builder-jekyll' => '',
'jekyll-version' => ::Jekyll::VERSION,
}
MessageTopic = Utils::MessageTopic
NewLine = Utils::NewLine
AttributeReferenceRx = /\\?\{(\p{Word}[-\p{Word}]*)\}/
HeaderBoundaryRx = /(?<=\p{Graph}#{NewLine * 2})/
HeaderLineRx = /^=[ \t]+.|^:!?\w[-\w]*!?:(?:[ \t]+.)?/
# Enable plugin when running in safe mode; jekyll-asciidoc gem must also be declared in whitelist
safe true
# highlighter prefix/suffix not used by this plugin; defined only to avoid warning
highlighter_prefix nil
highlighter_suffix nil
def initialize config
@config = config
@logger = ::Jekyll.logger
@page_context = {}
# NOTE jekyll-watch reinitializes plugins using a shallow clone of config, so no need to reconfigure
# NOTE check for Configured only works if value of key is defined in _config.yml as Hash
unless Configured === (asciidoc_config = (config['asciidoc'] ||= {}))
if ::String === asciidoc_config
@logger.warn MessageTopic,
'The AsciiDoc configuration should be defined using Hash on asciidoc key instead of discrete entries.'
asciidoc_config = config['asciidoc'] = { 'processor' => asciidoc_config }
else
asciidoc_config['processor'] ||= 'asciidoctor'
end
old_asciidoc_ext = config.delete 'asciidoc_ext'
asciidoc_ext = (asciidoc_config['ext'] ||= (old_asciidoc_ext || (DefaultFileExtensions * ',')))
asciidoc_ext_re = asciidoc_config['ext_re'] = /^\.(?:#{asciidoc_ext.tr ',', '|'})$/ix
old_page_attr_prefix_def = config.key? 'asciidoc_page_attribute_prefix'
old_page_attr_prefix_val = config.delete 'asciidoc_page_attribute_prefix'
unless (page_attr_prefix = asciidoc_config['page_attribute_prefix'])
page_attr_prefix = old_page_attr_prefix_def ? old_page_attr_prefix_val || '' :
(asciidoc_config.key? 'page_attribute_prefix') ? '' : DefaultPageAttributePrefix
end
asciidoc_config['page_attribute_prefix'] = (page_attr_prefix = page_attr_prefix.chomp '-').empty? ?
'' : %(#{page_attr_prefix}-)
asciidoc_config['require_front_matter_header'] = !!asciidoc_config['require_front_matter_header']
asciidoc_config.extend Configured
if asciidoc_config['require_front_matter_header']
unless (::Jekyll::Utils.method :has_yaml_header?).owner == ::Jekyll::Utils
# NOTE restore original method
::Jekyll::Utils.extend (::Module.new do
define_method :has_yaml_header?, &(Utils.method :has_yaml_header?)
end)
end
else
::Jekyll::Utils.extend (::Module.new do
define_method :has_yaml_header?,
(Utils.method :has_front_matter?).curry[Utils.method :has_yaml_header?][asciidoc_ext_re]
end)
end
end
if (@asciidoc_config = asciidoc_config)['processor'] == 'asciidoctor'
unless Configured === (@asciidoctor_config = (config['asciidoctor'] ||= {}))
asciidoctor_config = @asciidoctor_config
asciidoctor_config.replace symbolize_keys asciidoctor_config
source = ::File.expand_path config['source']
dest = ::File.expand_path config['destination']
case (base = asciidoctor_config[:base_dir])
when ':source'
asciidoctor_config[:base_dir] = source
when ':docdir'
asciidoctor_config[:base_dir] = :docdir
else
asciidoctor_config[:base_dir] = ::File.expand_path base if base
end
asciidoctor_config[:safe] ||= 'safe'
site_attributes = {
'site-root' => ::Dir.pwd,
'site-source' => source,
'site-destination' => dest,
'site-baseurl' => (baseurl = config['baseurl']),
'site-url' => config['url'],
}
attrs = asciidoctor_config[:attributes] = compile_attributes asciidoctor_config[:attributes],
(compile_attributes asciidoc_config['attributes'],
((site_attributes.merge ImplicitAttributes).merge DefaultAttributes))
if (imagesdir = attrs['imagesdir']) && (imagesdir.start_with? '/')
attrs['imagesoutdir'] = ::File.join dest, (imagesdir.chomp '@') unless attrs.key? 'imagesoutdir'
attrs['imagesdir'] = baseurl + imagesdir unless baseurl.to_s.empty?
end
asciidoctor_config.extend Configured
end
end
load_processor
end
def load_processor
case @asciidoc_config['processor']
when 'asciidoctor'
begin
require 'asciidoctor' unless defined? ::Asciidoctor::VERSION
rescue ::LoadError
@logger.error MessageTopic, 'You\'re missing a library required to convert AsciiDoc files. Install using:'
@logger.error '', '$ [sudo] gem install asciidoctor'
@logger.abort_with 'Bailing out; missing required dependency: asciidoctor'
end
else
@logger.error MessageTopic, %(Invalid AsciiDoc processor given: #{@asciidoc_config['processor']})
@logger.error '', 'Valid options are: asciidoctor'
@logger.abort_with 'Bailing out; invalid Asciidoctor processor.'
end
nil
end
def self.get_instance site
site.find_converter_instance self
end
def matches ext
@asciidoc_config['ext_re'].match? ext
end
def output_ext _ext
'.html'
end
def self.before_render document, payload
(get_instance document.site).before_render document, payload if Document === document || Excerpt === document
end
def self.after_render document
(get_instance document.site).after_render document if Document === document || Excerpt === document
end
def before_render document, payload
# NOTE Jekyll 3.1 incorrectly maps the page payload to document.data instead of payload['page']
@page_context[:data] = ::Jekyll::AsciiDoc::Jekyll3_1 ? document.data : payload['page']
record_paths document
end
def after_render _document
@page_context.clear
end
def record_paths document, opts = {}
@page_context[:paths] = paths = {
'docfile' => (docfile = ::File.join document.site.source, document.relative_path),
'docdir' => (::File.dirname docfile),
'docname' => (::File.basename docfile, (::File.extname docfile)),
}
paths.update(
'outfile' => (outfile = document.destination document.site.dest),
'outdir' => (::File.dirname outfile),
'outpath' => document.url
) unless opts[:source_only]
end
def clear_paths
@page_context.delete :paths
end
def load_header document
record_paths document, source_only: true
case @asciidoc_config['processor']
when 'asciidoctor'
opts = @asciidoctor_config.merge parse_header_only: true
header = extract_header document
if (paths = @page_context[:paths])
if opts[:base_dir] == :docdir
opts[:base_dir] = paths['docdir'] # NOTE this assignment happens inside the processor anyway
else
paths.delete 'docdir'
end
opts[:attributes] = opts[:attributes].merge paths
end
if (layout_attr = resolve_default_layout document, opts[:attributes])
opts[:attributes] = opts[:attributes].merge layout_attr
end
# NOTE return instance even if header is empty since attributes may be inherited from config
doc = ::Asciidoctor.load header, opts
else
@logger.warn MessageTopic,
%(Unknown AsciiDoc processor: #{@asciidoc_config['processor']}. Cannot load document header.)
doc = nil
end
clear_paths
doc
end
def convert content
# NOTE don't use nil_or_empty? since that's only provided only by Asciidoctor
return '' unless content && !content.empty?
case @asciidoc_config['processor']
when 'asciidoctor'
opts = @asciidoctor_config.merge header_footer: (data = @page_context[:data] || {})['standalone']
if (paths = @page_context[:paths])
if opts[:base_dir] == :docdir
opts[:base_dir] = paths['docdir'] # NOTE this assignment happens inside the processor anyway
else
paths.delete 'docdir'
end
opts[:attributes] = opts[:attributes].merge paths
elsif opts[:base_dir] == :docdir
opts.delete :base_dir
end
if (doctype = data['doctype'])
opts[:doctype] = doctype
end
(data['document'] = ::Asciidoctor.load content, opts).extend(Liquidable).convert
else
@logger.warn MessageTopic,
%(Unknown AsciiDoc processor: #{@asciidoc_config['processor']}. Passing through unparsed content.)
content
end
end
private
# Take up to the AsciiDoc document header (if present), then continue to the excerpt separator, if non-blank.
def extract_header document
if (content = document.content)
header = (header_boundary = HeaderBoundaryRx =~ content) ? $` : content
# NOTE at this point, excerpt is already set to an instance of Jekyll::Excerpt unless set in front matter
if ::Jekyll::Page === document || !(::Jekyll::Excerpt === document.data['excerpt'])
header = '' unless HeaderLineRx.match? header
else
document.data['excerpt'] = nil
if (excerpt_separator = document.data['excerpt_separator'] || @asciidoc_config['excerpt_separator'] ||
@config['excerpt_separator']).to_s.empty?
header = '' unless HeaderLineRx.match? header
else
header_boundary = 0 unless header_boundary && (HeaderLineRx.match? header)
if (excerpt_boundary = content.index excerpt_separator, header_boundary)
header = content.slice 0, excerpt_boundary
else
header = content
end
end
end
header
else
''
end
end
def symbolize_keys hash
hash.each_with_object({}) {|(key, val), accum| accum[key.to_sym] = val }
end
def compile_attributes attrs, initial = {}
if (is_array = ::Array === attrs) || ::Hash === attrs
attrs.each_with_object(initial) do |entry, new_attrs|
key, val = is_array ? (((entry.split '=', 2) + ['', '']).slice 0, 2) : entry
if key.start_with? '!'
new_attrs[key.slice 1, key.length] = nil
elsif key.end_with? '!'
new_attrs[key.chop] = nil
# we're reserving -name to mean "unset implicit value but allow doc to override"
elsif key.start_with? '-'
new_attrs.delete key.slice 1, key.length
else
case val
when ::String
new_attrs[key] = resolve_attribute_refs val, new_attrs
when ::Numeric
new_attrs[key] = val.to_s
when true
new_attrs[key] = ''
when nil, false
# we may preserve false in the future to mean "unset implicit value but allow doc to override"
# false already has special meaning for page-layout, so don't coerce it
new_attrs[key] = key == 'page-layout' ? val : nil
else
new_attrs[key] = val
end
end
end
else
initial
end
end
def resolve_attribute_refs text, attrs
if text.empty?
text
elsif text.include? '{'
text.gsub AttributeReferenceRx do
($&.start_with? '\\') ? ($&.slice 1, $&.length) : ((attrs.fetch $1, $&).to_s.chomp '@')
end
else
text
end
end
def resolve_default_layout document, attributes
layout_attr_name = %(#{@asciidoc_config['page_attribute_prefix']}layout)
if attributes.key? layout_attr_name
if ::String === (layout = attributes[layout_attr_name])
if layout == '~@'
layout = 'none@'
elsif (layout.end_with? '@') && ((document.data.key? 'layout') || document.data['layout'])
layout = %(#{(layout = document.data['layout']).nil? ? 'none' : layout}@)
else
layout = nil
end
elsif layout.nil?
layout = 'none'
else
layout = layout.to_s
end
elsif (document.data.key? 'layout') || document.data['layout']
layout = %(#{(layout = document.data['layout']).nil? ? 'none' : layout}@)
else
layout = '@'
end
layout ? { layout_attr_name => layout } : nil
end
# Register pre and post render callbacks for saving and clearing contextual AsciiDoc attributes, respectively.
::Jekyll::Hooks.tap do |hooks|
hooks.register [:pages, :documents], :pre_render, &(method :before_render)
hooks.register [:pages, :documents], :post_render, &(method :after_render)
end
end
end
end
|