utilities.rb 17.7 KB
Newer Older
Sylvester Keil's avatar
Sylvester Keil committed
1
module Jekyll
2
  class Scholar
3
    require 'date'
4

Sylvester Keil's avatar
Sylvester Keil committed
5 6 7
    # Load styles into static memory.
    # They should be thread safe as long as they are
    # treated as being read-only.
Sylvester Keil's avatar
Sylvester Keil committed
8
    STYLES = {}
Sylvester Keil's avatar
Sylvester Keil committed
9

10
    # Utility methods used by several Scholar plugins. The methods in this
11
    # module may depend on the presence of #config, #bibtex_files, and
12 13
    # #site readers
    module Utilities
14

15

16
      attr_reader :config, :site, :context, :prefix, :text, :offset, :max
17

18 19 20 21 22 23 24 25 26 27

      def split_arguments(arguments)

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

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

        [args, opts]
      end
28 29 30 31 32 33 34 35 36

      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
37 38 39 40
          opts.on('-C', '--cited_in_order') do |cited|
            @cited, @skip_sort = true, true
          end

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

45
          opts.on('-f', '--file FILE') do |file|
46 47
            @bibtex_files ||= []
            @bibtex_files << file
48 49 50 51
          end

          opts.on('-q', '--query QUERY') do |query|
            @query = query
52
          end
53 54 55 56 57 58 59 60

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

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

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

Sylvester Keil's avatar
Sylvester Keil committed
66
          opts.on('-L', '--label LABEL') do |label|
67
            labels << label
Sylvester Keil's avatar
Sylvester Keil committed
68 69
          end

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

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

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

82 83 84 85
          opts.on('-g', '--group_by GROUP') do |group_by|
            @group_by = group_by
          end

86 87 88 89
          opts.on('-G', '--group_order ORDER') do |group_order|
            self.group_order = group_order
          end

90 91 92 93
          opts.on('-O', '--type_order ORDER') do |type_order|
            @group_by = type_order
          end

94 95 96
          opts.on('-T', '--template TEMPLATE') do |template|
            @bibliography_template = template
          end
97 98
        end

Sylvester Keil's avatar
Sylvester Keil committed
99
        argv = arguments.split(/(\B-[cCfqptTsgGOlLomA]|\B--(?:cited(_in_order)?|file|query|prefix|text|style|group_(?:by|order)|type_order|template|locator|label|offset|max|suppress_author|))/)
100 101 102

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

104 105 106 107
      def locators
        @locators ||= []
      end

108 109 110 111
      def labels
        @labels ||= []
      end

112 113 114 115 116 117 118 119 120
      def bibtex_files
        @bibtex_files ||= [config['bibliography']]
      end

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

121
      def bibtex_options
Sylvester Keil's avatar
Sylvester Keil committed
122 123
        @bibtex_options ||=
          (config['bibtex_options'] || {}).symbolize_keys
124 125 126 127
      end

      def bibtex_filters
        config['bibtex_filters'] ||= []
128
      end
129

130 131 132 133 134 135 136
      def bibtex_paths
        @bibtex_paths ||= bibtex_files.map { |file|
          extend_path file
        }
      end

      # :nodoc: backwards compatibility
137
      def bibtex_path
138
        bibtex_paths[0]
139
      end
140

141
      def bibliography
142
        unless @bibliography
143
          @bibliography = BibTeX::Bibliography.parse(
144 145 146
            bibtex_paths.reduce('') { |s, p| s << IO.read(p) },
            bibtex_options
          )
147
          @bibliography.replace_strings if replace_strings?
148
          @bibliography.join if join_strings? && replace_strings?
149 150 151
        end

        @bibliography
152 153
      end

154 155 156 157
      def query
        interpolate @query
      end

158
      def entries
159
        sort bibliography[query || config['query']].select { |x| x.instance_of? BibTeX::Entry}
160
      end
161

egon w. stemle's avatar
egon w. stemle committed
162 163 164 165 166 167 168 169
      def offset
        @offset ||= 0
      end

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

Sylvester Keil's avatar
Sylvester Keil committed
170
      def limit_entries?
egon w. stemle's avatar
egon w. stemle committed
171
        !offset.nil? || !max.nil?
Sylvester Keil's avatar
Sylvester Keil committed
172 173
      end

174
      def sort(unsorted)
Sylvester Keil's avatar
Sylvester Keil committed
175
        return unsorted if skip_sort?
176

177 178 179 180 181 182 183 184 185 186 187 188
        sorted = unsorted.sort do |e1, e2|
          sort_keys
            .map.with_index do |key, idx|
              v1 = e1[key].nil? ? BibTeX::Value.new : e1[key]
              v2 = e2[key].nil? ? BibTeX::Value.new : e2[key]
              if (sort_order[idx] || sort_order.last) =~ /^(desc|reverse)/i
                v2 <=> v1
              else
                v1 <=> v2
              end
            end
            .find { |c| c != 0 } || 0
189
        end
190

191
        sorted
192
      end
193

194
      def sort_keys
195 196 197 198 199 200
        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 }
201 202
      end

203 204 205 206 207 208 209 210
      def sort_order
        return @sort_order unless @sort_order.nil?

        @sort_order = Array(config['order'])
          .map { |key| key.to_s.split(/\s*,\s*/) }
          .flatten
      end

Patrick de Kok's avatar
Patrick de Kok committed
211
      def group_by
Sylvester Keil's avatar
Sylvester Keil committed
212
        @group_by ||= config['group_by']
213
      end
Sylvester Keil's avatar
Sylvester Keil committed
214

Patrick de Kok's avatar
Patrick de Kok committed
215 216 217 218
      def group?
        group_by != 'none'
      end

219
      def group(ungrouped)
220 221 222 223 224
        def grouper(items, keys, order)
          groups = items.group_by do |item|
            group_value(keys.first, item)
          end

225 226 227
          if keys.count == 1
            groups
          else
228 229
            groups.merge(groups) do |key, items|
              grouper(items, keys.drop(1), order.drop(1))
230 231 232
            end
          end
        end
233 234

        grouper(ungrouped, group_keys, group_order)
235
      end
Sylvester Keil's avatar
Sylvester Keil committed
236

237

238 239 240
      def group_keys
        return @group_keys unless @group_keys.nil?

Patrick de Kok's avatar
Patrick de Kok committed
241
        @group_keys = Array(group_by)
242 243 244 245
          .map { |key| key.to_s.split(/\s*,\s*/) }
          .flatten
          .map { |key| key == 'month' ? 'month_numeric' : key }
      end
Sylvester Keil's avatar
Sylvester Keil committed
246

247
      def group_order
248 249 250
        self.group_order = config['group_order'] if @group_order.nil?
        @group_order
      end
251

252 253
      def group_order=(value)
        @group_order = Array(value)
254 255 256
          .map { |key| key.to_s.split(/\s*,\s*/) }
          .flatten
      end
Sylvester Keil's avatar
Sylvester Keil committed
257

258
      def group_compare(key,v1,v2)
259 260
        case key
        when 'type'
261 262 263 264 265 266 267 268 269 270 271
          o1 = type_order.find_index(v1)
          o2 = type_order.find_index(v2)
          if o1.nil? && o2.nil?
            0
          elsif o1.nil?
            1
          elsif o2.nil?
            -1
          else
            o1 <=> o2
          end
272
        else
273
          v1 <=> v2
274 275
        end
      end
Sylvester Keil's avatar
Sylvester Keil committed
276

277 278 279
      def group_value(key,item)
        case key
        when 'type'
280
          type_aliases[item.type.to_s] || item.type.to_s
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
        else
          value = item[key]
          if value.numeric?
            value.to_i
          elsif value.date?
            value.to_date
          else
            value.to_s
          end
        end
      end

      def group_tags
        return @group_tags unless @group_tags.nil?

        @group_tags = Array(config['bibliography_group_tag'])
          .map { |key| key.to_s.split(/\s*,\s*/) }
          .flatten
      end
300

301 302 303
      def group_name(key,value)
        case key
        when 'type'
304
          type_names[value] || value.to_s
305
        when 'month_numeric'
306
          month_names[value] || "(unknown)"
307 308 309 310
        else
          value.to_s
        end
      end
311 312 313 314 315

      def type_order
        @type_order ||= config['type_order']
      end

316 317 318 319
      def type_aliases
        @type_aliases ||= Scholar.defaults['type_aliases'].merge(config['type_aliases'])
      end

320 321 322 323 324 325 326 327
      def type_names
        @type_names ||= Scholar.defaults['type_names'].merge(config['type_names'])
      end

      def month_names
        return @month_names unless @month_names.nil?

        @month_names = config['month_names'].nil? ? Date::MONTHNAMES : config['month_names'].unshift(nil)
Hendrik van Antwerpen's avatar
Hendrik van Antwerpen committed
328
      end
329

Sylvester Keil's avatar
Sylvester Keil committed
330 331 332 333
      def suppress_author?
        !!@suppress_author
      end

Sylvester Keil's avatar
Sylvester Keil committed
334
      def raw_bibtex?
335
        config['use_raw_bibtex_entry']
Sylvester Keil's avatar
Sylvester Keil committed
336 337
      end

338 339 340 341 342 343 344 345 346
      def repository?
        !config['repository'].nil? && !config['repository'].empty?
      end

      def repository
        @repository ||= load_repository
      end

      def load_repository
347
        repo = Hash.new { |h,k| h[k] = {} }
348

349
        return repo unless repository?
350 351

        # ensure that the base directory format is literally
352
        # the same as the entries that are in the directory
353 354
        base = Dir[site.source][0]

355
        Dir[File.join(site.source, repository_path, '**/*')].each do |path|
356 357 358
          parts = path.split(repository_file_delimiter, 2)
          repo[File.basename(parts[0])][parts[1]] =
            Pathname(path).relative_path_from(Pathname(base))
359 360 361
        end

        repo
362 363 364 365 366 367
      end

      def repository_path
        config['repository']
      end

368 369 370 371
      def repository_file_delimiter
        config['repository_file_delimiter']
      end

372 373 374 375
      def replace_strings?
        config['replace_strings']
      end

376 377 378 379
      def join_strings?
        config['join_strings']
      end

380 381 382 383
      def cited_only?
        !!@cited
      end

Sylvester Keil's avatar
Sylvester Keil committed
384 385 386 387
      def skip_sort?
        @skip_sort || config['sort_by'] == 'none'
      end

388 389
      def extend_path(name)
        if name.nil? || name.empty?
390
          name = config['bibliography']
391
        end
392

393 394
        # Return as is if it is an absolute path
        # Improve by using Pathname from stdlib?
395
        return name if name.start_with?('/') && File.exists?(name)
396

397 398 399 400 401 402 403
        name = File.join scholar_source, name
        name << '.bib' if File.extname(name).empty? && !File.exists?(name)
        name
      end

      def scholar_source
        source = config['source']
404

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

        File.join site.source, source
409
      end
410

Alex Gil's avatar
Alex Gil committed
411
      def relative
412
        config['relative']
Alex Gil's avatar
Alex Gil committed
413 414
      end

Sylvester Keil's avatar
Sylvester Keil committed
415
      def reference_tag(entry, index = nil)
Sylvester Keil's avatar
Sylvester Keil committed
416
        return missing_reference unless entry
417

418
        entry = entry.convert(*bibtex_filters) unless bibtex_filters.empty?
Sylvester Keil's avatar
Sylvester Keil committed
419
        reference = render_bibliography entry, index
420

Sylvester Keil's avatar
Sylvester Keil committed
421 422 423 424
        content_tag reference_tagname, reference,
          :id => [prefix, entry.key].compact.join('-')
      end

425 426 427 428
      def style
        @style || config['style']
      end

Sylvester Keil's avatar
Sylvester Keil committed
429 430 431 432 433 434 435 436 437
      def missing_reference
        config['missing_reference']
      end

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

      def bibliography_template
438 439 440 441 442
        @bibliography_template || config['bibliography_template']
      end

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

445
        tmp = bibliography_template
446 447

        case
448
        when tmp.nil?, tmp.empty?
449 450 451 452 453
          tmp = '{{reference}}'
        when site.layouts.key?(tmp)
          tmp = site.layouts[tmp].content
        end

454
        @liquid_template = Liquid::Template.parse(tmp)
Sylvester Keil's avatar
Sylvester Keil committed
455 456
      end

457
      def bibliography_tag(entry, index)
Sylvester Keil's avatar
Sylvester Keil committed
458
        return missing_reference unless entry
459

460
        tmp = liquid_template.render(
461 462 463 464 465 466 467 468 469 470 471
          reference_data(entry,index)
            .merge(site.site_payload)
            .merge({
              'index' => index,
              'details' => details_link_for(entry)
            }),
          {
            :registers => { :site => site },
            :filters => [Jekyll::Filters]
          }
        )
472 473 474
        # process the generated reference with Liquid, to get the same behaviour as 
        # when it is used on a page
        Liquid::Template.parse(tmp).render(
475
          site.site_payload,
476 477 478 479 480
          {
            :registers => { :site => site },
            :filters => [Jekyll::Filters]
          }
        )
481 482 483 484
      end

      def reference_data(entry, index = nil)
        {
Sylvester Keil's avatar
Sylvester Keil committed
485
          'entry' => liquidify(entry),
Sylvester Keil's avatar
Sylvester Keil committed
486
          'reference' => reference_tag(entry, index),
487
          'key' => entry.key,
488
          'type' => entry.type.to_s,
489
          'link' => repository_link_for(entry),
490 491
          'links' => repository_links_for(entry)
        }
492 493
      end

Sylvester Keil's avatar
Sylvester Keil committed
494 495 496 497
      def liquidify(entry)
        e = {}

        e['key'] = entry.key
498
        e['type'] = entry.type.to_s
Sylvester Keil's avatar
Sylvester Keil committed
499

500 501 502
        if entry.field_names(config['bibtex_skip_fields']).empty?
          e['bibtex'] = entry.to_s
        else
503
          tmp = entry.dup
504 505 506 507 508

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

509 510
          e['bibtex'] = tmp.to_s
        end
Sylvester Keil's avatar
Sylvester Keil committed
511

Sylvester Keil's avatar
Sylvester Keil committed
512 513 514 515
        if raw_bibtex?
          e['bibtex'] = "{%raw%}#{e['bibtex']}{%endraw%}"
        end

Sylvester Keil's avatar
Sylvester Keil committed
516 517 518
        entry.fields.each do |key, value|
          value = value.convert(*bibtex_filters) unless bibtex_filters.empty?
          e[key.to_s] = value.to_s
519 520

          if value.is_a?(BibTeX::Names)
521
            e["#{key}_array"] = arr = []
522
            value.each.with_index do |name, idx|
523
              parts = {}
524 525
              name.each_pair do |k, v|
                e["#{key}_#{idx}_#{k}"] = v.to_s
526
                parts[k.to_s] = v.to_s
527
              end
528
              arr << parts
529 530
            end
          end
Sylvester Keil's avatar
Sylvester Keil committed
531 532 533 534 535
        end

        e
      end

536 537 538
      def generate_details?
        site.layouts.key?(File.basename(config['details_layout'], '.html'))
      end
539

540 541
      def details_file_for(entry)
        name = entry.key.to_s.dup
542

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

545
        if site.config['permalink'] == 'pretty'
546
          name << '/'
547 548 549
        else
          name << '.html'
        end
550
      end
551

552
      def repository_link_for(entry, base = base_url)
553 554 555
        links = repository[entry.key]
        url   = links['pdf'] || links['ps']

556 557 558 559 560
        return unless url

        File.join(base, url)
      end

561 562 563 564 565 566
      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
567
      def details_link_for(entry, base = base_url)
568
        File.join(base, details_path, details_file_for(entry))
Sylvester Keil's avatar
Sylvester Keil committed
569
      end
570

Sylvester Keil's avatar
Sylvester Keil committed
571
      def base_url
572
        @base_url ||= site.config['baseurl'] || site.config['base_url'] || ''
573
      end
574

575 576 577
      def details_path
        config['details_dir']
      end
578

579 580
      def renderer(force = false)
        return @renderer if @renderer && !force
581

582
        @renderer = CiteProc::Ruby::Renderer.new :format => 'html',
Sylvester Keil's avatar
Sylvester Keil committed
583 584
          :style => style, :locale => config['locale']
      end
585

586
      def render_citation(items)
587
        renderer.render items.zip(locators.zip(labels)).map { |entry, (locator, label)|
Sylvester Keil's avatar
Sylvester Keil committed
588
          cited_keys << entry.key
Sylvester Keil's avatar
4.2.1  
Sylvester Keil committed
589
          cited_keys.uniq!
590

Sylvester Keil's avatar
Sylvester Keil committed
591
          item = citation_item_for entry, citation_number(entry.key)
592
          item.locator = locator
Sylvester Keil's avatar
Sylvester Keil committed
593
          item.label = label unless label.nil?
594

595
          item
Sylvester Keil's avatar
Sylvester Keil committed
596
        }, styles(style).citation
Sylvester Keil's avatar
Sylvester Keil committed
597 598
      end

Sylvester Keil's avatar
Sylvester Keil committed
599 600
      def render_bibliography(entry, index = nil)
        renderer.render citation_item_for(entry, index),
Sylvester Keil's avatar
Sylvester Keil committed
601
          styles(style).bibliography
Sylvester Keil's avatar
Sylvester Keil committed
602 603
      end

Sylvester Keil's avatar
Sylvester Keil committed
604
      def citation_item_for(entry, citation_number = nil)
Sylvester Keil's avatar
Sylvester Keil committed
605 606
        CiteProc::CitationItem.new id: entry.id do |c|
          c.data = CiteProc::Item.new entry.to_citeproc
Sylvester Keil's avatar
Sylvester Keil committed
607
          c.data[:'citation-number'] = citation_number
Sylvester Keil's avatar
Sylvester Keil committed
608
          c.data.suppress! 'author' if suppress_author?
Sylvester Keil's avatar
Sylvester Keil committed
609 610 611
        end
      end

612
      def cited_keys
613
        context['cited'] ||= []
614
      end
615

Sylvester Keil's avatar
Sylvester Keil committed
616 617
      def citation_number(key)
        (context['citation_numbers'] ||= {})[key] ||= cited_keys.length
Sylvester Keil's avatar
Sylvester Keil committed
618 619
      end

Alex Gil's avatar
Alex Gil committed
620 621
      def link_target_for(key)
        "#{relative}##{[prefix, key].compact.join('-')}"
622 623
      end

624 625 626 627 628 629 630 631
      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
632
        end
633

634
        link_to link_target_for(keys[0]), render_citation(items)
Sylvester Keil's avatar
Sylvester Keil committed
635
      end
Sylvester Keil's avatar
Sylvester Keil committed
636

637
      def cite_details(key, text)
Hiren Patel's avatar
Hiren Patel committed
638
        if bibliography.key?(key)
Sylvester Keil's avatar
Sylvester Keil committed
639
          link_to details_link_for(bibliography[key]), text || config['details_link']
Hiren Patel's avatar
Hiren Patel committed
640
        else
641
          missing_reference
Hiren Patel's avatar
Hiren Patel committed
642 643
        end
      end
644

Sylvester Keil's avatar
Sylvester Keil committed
645 646 647 648 649 650
      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
651

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

Sylvester Keil's avatar
Sylvester Keil committed
654 655 656 657 658 659
        if content.nil?
          "<#{[name, attributes].flatten.compact.join(' ')}/>"
        else
          "<#{[name, attributes].flatten.compact.join(' ')}>#{content}</#{name}>"
        end
      end
660

Sylvester Keil's avatar
Sylvester Keil committed
661 662 663
      def link_to(href, content, attributes = {})
        content_tag :a, content || href, attributes.merge(:href => href)
      end
664

665 666 667 668
      def cited_references
        context && context['cited'] || []
      end

669 670
      def keys
        # De-reference keys (in case they are variables)
Sylvester Keil's avatar
Sylvester Keil committed
671 672
        # We need to do this every time, to support for loops,
        # where the context can change for each invocation.
673
        Array(@keys).map do |key|
674
          context[key] || key
675 676 677
        end
      end

678 679 680 681
      def interpolate(string)
        return unless string

        string.gsub(/{{\s*([\w\.]+)\s*}}/) do |match|
682
          context[$1] || match
683 684 685
        end
      end

Sylvester Keil's avatar
Sylvester Keil committed
686
      def set_context_to(context)
687
        @context, @site, = context, context.registers[:site]
Sylvester Keil's avatar
Sylvester Keil committed
688
        config.merge!(site.config['scholar'] || {})
689
        self
Sylvester Keil's avatar
Sylvester Keil committed
690
      end
691

Sylvester Keil's avatar
Sylvester Keil committed
692 693 694 695 696 697 698
      def load_style(uri)
        begin
          style = CSL::Style.load uri
        rescue CSL::ParseError => error
          # Try to resolve local style paths
          # relative to Jekyll's source directory
          site_relative_style = File.join(site.source, uri)
699

Sylvester Keil's avatar
Sylvester Keil committed
700 701 702 703 704 705 706 707 708 709
          raise error unless File.exist?(site_relative_style)
          style = CSL::Style.load site_relative_style
        end

        if style.independent?
          style
        else
          style.independent_parent
        end
      end
710

Sylvester Keil's avatar
Sylvester Keil committed
711 712
      def styles(style)
        STYLES[style] ||= load_style(style)
713
      end
714
    end
715

716
  end
Hiren Patel's avatar
Hiren Patel committed
717
end