utilities.rb 12.5 KB
Newer Older
Sylvester Keil's avatar
Sylvester Keil committed
1
module Jekyll
2
  class Scholar
3

Sylvester Keil's avatar
Sylvester Keil committed
4
5
6
7
    # Load styles into static memory.
    # They should be thread safe as long as they are
    # treated as being read-only.
    STYLES = Hash.new do |h, k|
Sylvester Keil's avatar
Sylvester Keil committed
8
9
10
      style = CSL::Style.load k
      style = style.independent_parent unless style.independent?
      h[k.to_s] = style
Sylvester Keil's avatar
Sylvester Keil committed
11
12
13
    end


14
    # Utility methods used by several Scholar plugins. The methods in this
15
    # module may depend on the presence of #config, #bibtex_files, and
16
17
    # #site readers
    module Utilities
18

19
      attr_reader :config, :site, :context, :prefix, :text, :offset, :max
20
21
22
23
24
25
26
27
28
29

      def split_arguments(arguments)

        tokens = arguments.strip.split(/\s+/)

        args = tokens.take_while { |a| !a.start_with?('-') }
        opts = (tokens - args).join(' ')

        [args, opts]
      end
30
31
32
33
34
35
36
37
38

      def optparse(arguments)
        return if arguments.nil? || arguments.empty?

        parser = OptionParser.new do |opts|
          opts.on('-c', '--cited') do |cited|
            @cited = true
          end

Sylvester Keil's avatar
Sylvester Keil committed
39
40
41
42
          opts.on('-C', '--cited_in_order') do |cited|
            @cited, @skip_sort = true, true
          end

Sylvester Keil's avatar
Sylvester Keil committed
43
44
45
46
          opts.on('-A', '--suppress_author') do |cited|
            @suppress_author = true
          end

47
          opts.on('-f', '--file FILE') do |file|
48
49
            @bibtex_files ||= []
            @bibtex_files << file
50
51
52
53
54
55
56
57
58
59
60
61
62
          end

          opts.on('-q', '--query QUERY') do |query|
            @query = query
          end

          opts.on('-p', '--prefix PREFIX') do |prefix|
            @prefix = prefix
          end

          opts.on('-t', '--text TEXT') do |text|
            @text = text
          end
63

64
65
66
67
          opts.on('-l', '--locator LOCATOR') do |locator|
            locators << locator
          end

68
69
70
71
          opts.on('-o', '--offset OFFSET') do |offset|
            @offset = offset.to_i
          end

Sylvester Keil's avatar
Sylvester Keil committed
72
          opts.on('-m', '--max MAX') do |max|
73
            @max = max.to_i
Sylvester Keil's avatar
Sylvester Keil committed
74
75
          end

76
77
          opts.on('-s', '--style STYLE') do |style|
            @style = style
78
          end
79
80
81
82

          opts.on('-T', '--template TEMPLATE') do |template|
            @bibliography_template = template
          end
83
84
        end

85
        argv = arguments.split(/(\B-[cCfqptTslomA]|\B--(?:cited(_in_order)?|file|query|prefix|text|style|template|locator|offset|max|suppress_author|))/)
86
87
88

        parser.parse argv.map(&:strip).reject(&:empty?)
      end
89

90
91
92
93
      def locators
        @locators ||= []
      end

94
95
96
97
98
99
100
101
102
      def bibtex_files
        @bibtex_files ||= [config['bibliography']]
      end

      # :nodoc: backwards compatibility
      def bibtex_file
        bibtex_files[0]
      end

103
      def bibtex_options
Sylvester Keil's avatar
Sylvester Keil committed
104
105
        @bibtex_options ||=
          (config['bibtex_options'] || {}).symbolize_keys
106
107
108
109
      end

      def bibtex_filters
        config['bibtex_filters'] ||= []
110
      end
111

112
113
114
115
116
117
118
      def bibtex_paths
        @bibtex_paths ||= bibtex_files.map { |file|
          extend_path file
        }
      end

      # :nodoc: backwards compatibility
119
      def bibtex_path
120
        bibtex_paths[0]
121
      end
122

123
      def bibliography
124
        unless @bibliography
125
126
127
128
          @bibliography = BibTeX.parse(
            bibtex_paths.reduce('') { |s, p| s << IO.read(p) },
            bibtex_options
          )
129
          @bibliography.replace_strings if replace_strings?
130
          @bibliography.join if join_strings? && replace_strings?
131
132
133
        end

        @bibliography
134
135
      end

136
137
138
139
      def query
        interpolate @query
      end

140
      def entries
141
142
        sort bibliography[query || config['query']]
      end
143

egon w. stemle's avatar
egon w. stemle committed
144
145
146
147
148
149
150
151
      def offset
        @offset ||= 0
      end

      def max
        @max.nil? ? -1 : @max + offset - 1
      end

Sylvester Keil's avatar
Sylvester Keil committed
152
      def limit_entries?
egon w. stemle's avatar
egon w. stemle committed
153
        !offset.nil? || !max.nil?
Sylvester Keil's avatar
Sylvester Keil committed
154
155
      end

156
      def sort(unsorted)
Sylvester Keil's avatar
Sylvester Keil committed
157
        return unsorted if skip_sort?
158

159
160
161
162
        sorted = unsorted.sort_by do |e|
          e.values_at(*sort_keys).map { |v| v.nil? ? BibTeX::Value.new : v }
        end

163
164
        sorted.reverse! if config['order'] =~ /^(desc|reverse)/i
        sorted
165
      end
166

167
      def sort_keys
168
169
170
171
172
173
        return @sort_keys unless @sort_keys.nil?

        @sort_keys = Array(config['sort_by'])
          .map { |key| key.to_s.split(/\s*,\s*/) }
          .flatten
          .map { |key| key == 'month' ? 'month_numeric' : key }
174
175
      end

Sylvester Keil's avatar
Sylvester Keil committed
176
177
178
179
      def suppress_author?
        !!@suppress_author
      end

Sylvester Keil's avatar
Sylvester Keil committed
180
      def raw_bibtex?
181
        config['use_raw_bibtex_entry']
Sylvester Keil's avatar
Sylvester Keil committed
182
183
      end

184
185
186
187
188
189
190
191
192
      def repository?
        !config['repository'].nil? && !config['repository'].empty?
      end

      def repository
        @repository ||= load_repository
      end

      def load_repository
193
        repo = Hash.new { |h,k| h[k] = {} }
194

195
196
197
198
199
200
201
202
        return repo unless repository?

        Dir[File.join(repository_path, '**/*')].each do |path|
          extname = File.extname(path)
          repo[File.basename(path, extname)][extname[1..-1]] = path
        end

        repo
203
204
205
206
207
208
      end

      def repository_path
        config['repository']
      end

209
210
211
212
      def replace_strings?
        config['replace_strings']
      end

213
214
215
216
      def join_strings?
        config['join_strings']
      end

217
218
219
220
      def cited_only?
        !!@cited
      end

Sylvester Keil's avatar
Sylvester Keil committed
221
222
223
224
      def skip_sort?
        @skip_sort || config['sort_by'] == 'none'
      end

225
226
      def extend_path(name)
        if name.nil? || name.empty?
227
          name = config['bibliography']
228
        end
229

Sylvester Keil's avatar
Sylvester Keil committed
230
231
        # Return as is if it is an absolute path
        # Improve by using Pathname from stdlib?
232
        return name if name.start_with?('/') && File.exists?(name)
233

Sylvester Keil's avatar
Sylvester Keil committed
234
235
236
237
238
239
240
241
242
243
244
245
        name = File.join scholar_source, name
        name << '.bib' if File.extname(name).empty? && !File.exists?(name)
        name
      end

      def scholar_source
        source = config['source']

        # Improve by using Pathname from stdlib?
        return source if source.start_with?('/') && File.exists?(source)

        File.join site.source, source
246
      end
247

Sylvester Keil's avatar
Sylvester Keil committed
248
      def reference_tag(entry, index = nil)
Sylvester Keil's avatar
Sylvester Keil committed
249
        return missing_reference unless entry
250

251
        entry = entry.convert(*bibtex_filters) unless bibtex_filters.empty?
Sylvester Keil's avatar
Sylvester Keil committed
252
        reference = render_bibliography entry, index
253

Sylvester Keil's avatar
Sylvester Keil committed
254
255
256
257
        content_tag reference_tagname, reference,
          :id => [prefix, entry.key].compact.join('-')
      end

258
259
260
261
      def style
        @style || config['style']
      end

Sylvester Keil's avatar
Sylvester Keil committed
262
263
264
265
266
267
268
269
270
      def missing_reference
        config['missing_reference']
      end

      def reference_tagname
        config['reference_tagname'] || :span
      end

      def bibliography_template
271
272
273
274
275
        @bibliography_template || config['bibliography_template']
      end

      def liquid_template
        return @liquid_template if @liquid_template
egon w. stemle's avatar
egon w. stemle committed
276
        Liquid::Template.register_filter(Jekyll::Filters)
277

278
        tmp = bibliography_template
279
280

        case
281
        when tmp.nil?, tmp.empty?
282
283
284
285
286
          tmp = '{{reference}}'
        when site.layouts.key?(tmp)
          tmp = site.layouts[tmp].content
        end

287
        @liquid_template = Liquid::Template.parse(tmp)
Sylvester Keil's avatar
Sylvester Keil committed
288
289
      end

290
      def bibliography_tag(entry, index)
Sylvester Keil's avatar
Sylvester Keil committed
291
292
        return missing_reference unless entry

293
        liquid_template.render(
294
295
296
297
298
299
300
301
302
303
304
          reference_data(entry,index)
            .merge(site.site_payload)
            .merge({
              'index' => index,
              'details' => details_link_for(entry)
            }),
          {
            :registers => { :site => site },
            :filters => [Jekyll::Filters]
          }
        )
305
306
307
308
      end

      def reference_data(entry, index = nil)
        {
Sylvester Keil's avatar
Sylvester Keil committed
309
          'entry' => liquidify(entry),
Sylvester Keil's avatar
Sylvester Keil committed
310
          'reference' => reference_tag(entry, index),
311
          'key' => entry.key,
312
          'type' => entry.type.to_s,
313
          'link' => repository_link_for(entry),
314
315
          'links' => repository_links_for(entry)
        }
316
317
      end

Sylvester Keil's avatar
Sylvester Keil committed
318
319
320
321
      def liquidify(entry)
        e = {}

        e['key'] = entry.key
322
        e['type'] = entry.type.to_s
Sylvester Keil's avatar
Sylvester Keil committed
323

324
325
326
        if entry.field_names(config['bibtex_skip_fields']).empty?
          e['bibtex'] = entry.to_s
        else
Sylvester Keil's avatar
Sylvester Keil committed
327
          tmp = entry.dup
328
329
330
331
332

          config['bibtex_skip_fields'].each do |name|
            tmp.delete name if tmp.field?(name)
          end

Sylvester Keil's avatar
Sylvester Keil committed
333
334
          e['bibtex'] = tmp.to_s
        end
Sylvester Keil's avatar
Sylvester Keil committed
335

Sylvester Keil's avatar
Sylvester Keil committed
336
337
338
339
        if raw_bibtex?
          e['bibtex'] = "{%raw%}#{e['bibtex']}{%endraw%}"
        end

Sylvester Keil's avatar
Sylvester Keil committed
340
341
342
        entry.fields.each do |key, value|
          value = value.convert(*bibtex_filters) unless bibtex_filters.empty?
          e[key.to_s] = value.to_s
343
344
345
346
347
348
349
350

          if value.is_a?(BibTeX::Names)
            value.each.with_index do |name, idx|
              name.each_pair do |k, v|
                e["#{key}_#{idx}_#{k}"] = v.to_s
              end
            end
          end
Sylvester Keil's avatar
Sylvester Keil committed
351
352
353
354
355
        end

        e
      end

356
357
358
      def bibtex_skip_fields
      end

359
360
361
      def generate_details?
        site.layouts.key?(File.basename(config['details_layout'], '.html'))
      end
362

363
364
      def details_file_for(entry)
        name = entry.key.to_s.dup
365

366
        name.gsub!(/[:\s]+/, '_')
367

368
        if site.config['permalink'] == 'pretty'
369
          name << '/'
370
371
372
        else
          name << '.html'
        end
373
      end
374

375
      def repository_link_for(entry, base = base_url)
376
377
378
        links = repository[entry.key]
        url   = links['pdf'] || links['ps']

379
380
381
382
383
        return unless url

        File.join(base, url)
      end

384
385
386
387
388
389
      def repository_links_for(entry, base = base_url)
        Hash[repository[entry.key].map { |ext, url|
          [ext, File.join(base, url)]
        }]
      end

Sylvester Keil's avatar
Sylvester Keil committed
390
      def details_link_for(entry, base = base_url)
391
        File.join(base, details_path, details_file_for(entry))
Sylvester Keil's avatar
Sylvester Keil committed
392
      end
393

Sylvester Keil's avatar
Sylvester Keil committed
394
      def base_url
395
        @base_url ||= site.config['baseurl'] || site.config['base_url'] || ''
396
      end
397

398
399
400
      def details_path
        config['details_dir']
      end
401

Sylvester Keil's avatar
Sylvester Keil committed
402
403
404
405
      def renderer
        @renderer ||= CiteProc::Ruby::Renderer.new :format => 'html',
          :style => style, :locale => config['locale']
      end
406

407
408
      def render_citation(items)
        renderer.render items.zip(locators).map { |entry, locator|
Sylvester Keil's avatar
Sylvester Keil committed
409
          cited_keys << entry.key
Sylvester Keil's avatar
4.2.1    
Sylvester Keil committed
410
          cited_keys.uniq!
411

Sylvester Keil's avatar
Sylvester Keil committed
412
          item = citation_item_for entry, citation_number(entry.key)
413
          item.locator = locator
414

415
416
          item
        }, STYLES[style].citation
Sylvester Keil's avatar
Sylvester Keil committed
417
418
      end

Sylvester Keil's avatar
Sylvester Keil committed
419
420
421
      def render_bibliography(entry, index = nil)
        renderer.render citation_item_for(entry, index),
          STYLES[style].bibliography
Sylvester Keil's avatar
Sylvester Keil committed
422
423
      end

Sylvester Keil's avatar
Sylvester Keil committed
424
      def citation_item_for(entry, citation_number = nil)
Sylvester Keil's avatar
Sylvester Keil committed
425
426
        CiteProc::CitationItem.new id: entry.id do |c|
          c.data = CiteProc::Item.new entry.to_citeproc
Sylvester Keil's avatar
Sylvester Keil committed
427
          c.data[:'citation-number'] = citation_number
Sylvester Keil's avatar
Sylvester Keil committed
428
          c.data.suppress! 'author' if suppress_author?
Sylvester Keil's avatar
Sylvester Keil committed
429
430
431
        end
      end

432
      def cited_keys
433
        context['cited'] ||= []
434
      end
435

Sylvester Keil's avatar
Sylvester Keil committed
436
437
      def citation_number(key)
        (context['citation_numbers'] ||= {})[key] ||= cited_keys.length
Sylvester Keil's avatar
Sylvester Keil committed
438
439
      end

440
441
442
443
444
445
446
447
      def cite(keys)
        items = keys.map do |key|
          if bibliography.key?(key)
            entry = bibliography[key]
            entry = entry.convert(*bibtex_filters) unless bibtex_filters.empty?
          else
            return missing_reference
          end
Sylvester Keil's avatar
Sylvester Keil committed
448
        end
449
450

        link_to "##{[prefix, keys[0]].compact.join('-')}", render_citation(items)
Sylvester Keil's avatar
Sylvester Keil committed
451
      end
Sylvester Keil's avatar
Sylvester Keil committed
452

453
      def cite_details(key, text)
Hiren Patel's avatar
Hiren Patel committed
454
        if bibliography.key?(key)
Sylvester Keil's avatar
Sylvester Keil committed
455
          link_to details_link_for(bibliography[key]), text || config['details_link']
Hiren Patel's avatar
Hiren Patel committed
456
        else
457
          missing_reference
Hiren Patel's avatar
Hiren Patel committed
458
459
        end
      end
460

Sylvester Keil's avatar
Sylvester Keil committed
461
462
463
464
465
466
      def content_tag(name, content_or_attributes, attributes = {})
        if content_or_attributes.is_a?(Hash)
          content, attributes = nil, content_or_attributes
        else
          content = content_or_attributes
        end
467

Sylvester Keil's avatar
Sylvester Keil committed
468
        attributes = attributes.map { |k,v| %Q(#{k}="#{v}") }
469

Sylvester Keil's avatar
Sylvester Keil committed
470
471
472
473
474
475
        if content.nil?
          "<#{[name, attributes].flatten.compact.join(' ')}/>"
        else
          "<#{[name, attributes].flatten.compact.join(' ')}>#{content}</#{name}>"
        end
      end
476

Sylvester Keil's avatar
Sylvester Keil committed
477
478
479
      def link_to(href, content, attributes = {})
        content_tag :a, content || href, attributes.merge(:href => href)
      end
480

481
482
483
484
      def cited_references
        context && context['cited'] || []
      end

485
486
      def keys
        # De-reference keys (in case they are variables)
Sylvester Keil's avatar
Sylvester Keil committed
487
488
        # We need to do this every time, to support for loops,
        # where the context can change for each invocation.
489
        Array(@keys).map do |key|
490
          context[key] || key
491
492
493
        end
      end

494
495
496
497
      def interpolate(string)
        return unless string

        string.gsub(/{{\s*([\w\.]+)\s*}}/) do |match|
498
          context[$1] || match
499
500
501
        end
      end

Sylvester Keil's avatar
Sylvester Keil committed
502
      def set_context_to(context)
503
        @context, @site, = context, context.registers[:site]
Sylvester Keil's avatar
Sylvester Keil committed
504
        config.merge!(site.config['scholar'] || {})
505
        self
Sylvester Keil's avatar
Sylvester Keil committed
506
      end
507
    end
508

509
  end
Hiren Patel's avatar
Hiren Patel committed
510
end