File: converter.rb

package info (click to toggle)
ruby-jekyll-asciidoc 3.0.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,536 kB
  • sloc: ruby: 1,802; sh: 36; makefile: 6
file content (344 lines) | stat: -rw-r--r-- 14,140 bytes parent folder | download
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