utilities.rb 15.9 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
          @bibliography = BibTeX::Bibliography.parse(
126 127 128
            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 163 164 165 166 167 168 169 170
        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
171
        end
172
 
173
        sorted
174
      end
175

176
      def sort_keys
177 178 179 180 181 182
        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 }
183 184
      end

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
      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

      def group?
        config['group_by'] != 'none'
      end
 
      def group(ungrouped)
        def grouper(items,keys,order)
          groups = items
            .group_by do |item|
              group_value(keys.first,item)
            end
            .sort do |e1, e2|
              v1 = group_sort_value(keys.first,e1[0])
              v2 = group_sort_value(keys.first,e2[0])
              if order.first =~ /^(desc|reverse)/i
                v2 <=> v1
              else
                v1 <=> v2
              end
            end
            .to_h
          if keys.count == 1
            groups
          else
            groups.merge(groups) do |key,items|
              grouper(items,keys.drop(1), order.length > 1 ? order.drop(1) : order)
            end
          end
        end
        grouper(ungrouped,group_keys,group_order)
      end
 
      def group_keys
        return @group_keys unless @group_keys.nil?

        @group_keys = Array(config['group_by'])
          .map { |key| key.to_s.split(/\s*,\s*/) }
          .flatten
          .map { |key| key == 'month' ? 'month_numeric' : key }
      end
 
      def group_order
        return @group_order unless @group_order.nil?

        @group_order = Array(config['group_order'])
          .map { |key| key.to_s.split(/\s*,\s*/) }
          .flatten
      end
 
      def group_sort_value(key,value)
        case key
        when 'type'
          config['type_order'].find_index(value) || 99
        else
          value
        end
      end
      
      def group_value(key,item)
        case key
        when 'type'
          config['type_aliases'][item.type.to_s] || item.type.to_s
        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
 
      def group_name(key,value)
        case key
        when 'type'
          config['type_names'][value] || value.to_s
        when 'month_numeric'
279
          month_name(value)
280 281 282 283 284
        else
          value.to_s
        end
      end
      
Hendrik van Antwerpen's avatar
Hendrik van Antwerpen committed
285
      def month_name(month)
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
        case month
        when 1
          'January'
        when 2
          'February'
        when 3
          'March'
        when 4
          'April'
        when 5
          'May'
        when 6
          'June'
        when 7
          'July'
        when 8
          'August'
        when 9
          'September'
        when 10
          'October'
        when 11
          'November'
        when 12
          'December'
        else
          'Unknown'
        end
Hendrik van Antwerpen's avatar
Hendrik van Antwerpen committed
314
      end
315

Sylvester Keil's avatar
Sylvester Keil committed
316 317 318 319
      def suppress_author?
        !!@suppress_author
      end

Sylvester Keil's avatar
Sylvester Keil committed
320
      def raw_bibtex?
321
        config['use_raw_bibtex_entry']
Sylvester Keil's avatar
Sylvester Keil committed
322 323
      end

324 325 326 327 328 329 330 331 332
      def repository?
        !config['repository'].nil? && !config['repository'].empty?
      end

      def repository
        @repository ||= load_repository
      end

      def load_repository
333
        repo = Hash.new { |h,k| h[k] = {} }
334

335 336 337 338 339 340 341 342
        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
343 344 345 346 347 348
      end

      def repository_path
        config['repository']
      end

349 350 351 352
      def replace_strings?
        config['replace_strings']
      end

353 354 355 356
      def join_strings?
        config['join_strings']
      end

357 358 359 360
      def cited_only?
        !!@cited
      end

Sylvester Keil's avatar
Sylvester Keil committed
361 362 363 364
      def skip_sort?
        @skip_sort || config['sort_by'] == 'none'
      end

365 366
      def extend_path(name)
        if name.nil? || name.empty?
367
          name = config['bibliography']
368
        end
369

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

Sylvester Keil's avatar
Sylvester Keil committed
374 375 376 377 378 379 380 381 382 383 384 385
        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
386
      end
387

Sylvester Keil's avatar
Sylvester Keil committed
388
      def reference_tag(entry, index = nil)
Sylvester Keil's avatar
Sylvester Keil committed
389
        return missing_reference unless entry
390

391
        entry = entry.convert(*bibtex_filters) unless bibtex_filters.empty?
Sylvester Keil's avatar
Sylvester Keil committed
392
        reference = render_bibliography entry, index
393

Sylvester Keil's avatar
Sylvester Keil committed
394 395 396 397
        content_tag reference_tagname, reference,
          :id => [prefix, entry.key].compact.join('-')
      end

398 399 400 401
      def style
        @style || config['style']
      end

Sylvester Keil's avatar
Sylvester Keil committed
402 403 404 405 406 407 408 409 410
      def missing_reference
        config['missing_reference']
      end

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

      def bibliography_template
411 412 413 414 415
        @bibliography_template || config['bibliography_template']
      end

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

418
        tmp = bibliography_template
419 420

        case
421
        when tmp.nil?, tmp.empty?
422 423 424 425 426
          tmp = '{{reference}}'
        when site.layouts.key?(tmp)
          tmp = site.layouts[tmp].content
        end

427
        @liquid_template = Liquid::Template.parse(tmp)
Sylvester Keil's avatar
Sylvester Keil committed
428 429
      end

430
      def bibliography_tag(entry, index)
Sylvester Keil's avatar
Sylvester Keil committed
431 432
        return missing_reference unless entry

433
        liquid_template.render(
434 435 436 437 438 439 440 441 442 443 444
          reference_data(entry,index)
            .merge(site.site_payload)
            .merge({
              'index' => index,
              'details' => details_link_for(entry)
            }),
          {
            :registers => { :site => site },
            :filters => [Jekyll::Filters]
          }
        )
445 446 447 448
      end

      def reference_data(entry, index = nil)
        {
Sylvester Keil's avatar
Sylvester Keil committed
449
          'entry' => liquidify(entry),
Sylvester Keil's avatar
Sylvester Keil committed
450
          'reference' => reference_tag(entry, index),
451
          'key' => entry.key,
452
          'type' => entry.type.to_s,
453
          'link' => repository_link_for(entry),
454 455
          'links' => repository_links_for(entry)
        }
456 457
      end

Sylvester Keil's avatar
Sylvester Keil committed
458 459 460 461
      def liquidify(entry)
        e = {}

        e['key'] = entry.key
462
        e['type'] = entry.type.to_s
Sylvester Keil's avatar
Sylvester Keil committed
463

464 465 466
        if entry.field_names(config['bibtex_skip_fields']).empty?
          e['bibtex'] = entry.to_s
        else
Sylvester Keil's avatar
Sylvester Keil committed
467
          tmp = entry.dup
468 469 470 471 472

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

Sylvester Keil's avatar
Sylvester Keil committed
473 474
          e['bibtex'] = tmp.to_s
        end
Sylvester Keil's avatar
Sylvester Keil committed
475

Sylvester Keil's avatar
Sylvester Keil committed
476 477 478 479
        if raw_bibtex?
          e['bibtex'] = "{%raw%}#{e['bibtex']}{%endraw%}"
        end

Sylvester Keil's avatar
Sylvester Keil committed
480 481 482
        entry.fields.each do |key, value|
          value = value.convert(*bibtex_filters) unless bibtex_filters.empty?
          e[key.to_s] = value.to_s
483 484 485 486 487 488 489 490

          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
491 492 493 494 495
        end

        e
      end

496 497 498
      def bibtex_skip_fields
      end

499 500 501
      def generate_details?
        site.layouts.key?(File.basename(config['details_layout'], '.html'))
      end
502

503 504
      def details_file_for(entry)
        name = entry.key.to_s.dup
505

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

508
        if site.config['permalink'] == 'pretty'
509
          name << '/'
510 511 512
        else
          name << '.html'
        end
513
      end
514

515
      def repository_link_for(entry, base = base_url)
516 517 518
        links = repository[entry.key]
        url   = links['pdf'] || links['ps']

519 520 521 522 523
        return unless url

        File.join(base, url)
      end

524 525 526 527 528 529
      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
530
      def details_link_for(entry, base = base_url)
531
        File.join(base, details_path, details_file_for(entry))
Sylvester Keil's avatar
Sylvester Keil committed
532
      end
533

Sylvester Keil's avatar
Sylvester Keil committed
534
      def base_url
535
        @base_url ||= site.config['baseurl'] || site.config['base_url'] || ''
536
      end
537

538 539 540
      def details_path
        config['details_dir']
      end
541

542 543 544 545
      def renderer(force = false)
        return @renderer if @renderer && !force
          
        @renderer = CiteProc::Ruby::Renderer.new :format => 'html',
Sylvester Keil's avatar
Sylvester Keil committed
546 547
          :style => style, :locale => config['locale']
      end
548

549 550
      def render_citation(items)
        renderer.render items.zip(locators).map { |entry, locator|
Sylvester Keil's avatar
Sylvester Keil committed
551
          cited_keys << entry.key
Sylvester Keil's avatar
4.2.1  
Sylvester Keil committed
552
          cited_keys.uniq!
553

Sylvester Keil's avatar
Sylvester Keil committed
554
          item = citation_item_for entry, citation_number(entry.key)
555
          item.locator = locator
556

557 558
          item
        }, STYLES[style].citation
Sylvester Keil's avatar
Sylvester Keil committed
559 560
      end

Sylvester Keil's avatar
Sylvester Keil committed
561 562 563
      def render_bibliography(entry, index = nil)
        renderer.render citation_item_for(entry, index),
          STYLES[style].bibliography
Sylvester Keil's avatar
Sylvester Keil committed
564 565
      end

Sylvester Keil's avatar
Sylvester Keil committed
566
      def citation_item_for(entry, citation_number = nil)
Sylvester Keil's avatar
Sylvester Keil committed
567 568
        CiteProc::CitationItem.new id: entry.id do |c|
          c.data = CiteProc::Item.new entry.to_citeproc
Sylvester Keil's avatar
Sylvester Keil committed
569
          c.data[:'citation-number'] = citation_number
Sylvester Keil's avatar
Sylvester Keil committed
570
          c.data.suppress! 'author' if suppress_author?
Sylvester Keil's avatar
Sylvester Keil committed
571 572 573
        end
      end

574
      def cited_keys
575
        context['cited'] ||= []
576
      end
577

Sylvester Keil's avatar
Sylvester Keil committed
578 579
      def citation_number(key)
        (context['citation_numbers'] ||= {})[key] ||= cited_keys.length
Sylvester Keil's avatar
Sylvester Keil committed
580 581
      end

582 583 584 585 586 587 588 589
      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
590
        end
591 592

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

595
      def cite_details(key, text)
Hiren Patel's avatar
Hiren Patel committed
596
        if bibliography.key?(key)
Sylvester Keil's avatar
Sylvester Keil committed
597
          link_to details_link_for(bibliography[key]), text || config['details_link']
Hiren Patel's avatar
Hiren Patel committed
598
        else
599
          missing_reference
Hiren Patel's avatar
Hiren Patel committed
600 601
        end
      end
602

Sylvester Keil's avatar
Sylvester Keil committed
603 604 605 606 607 608
      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
609

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

Sylvester Keil's avatar
Sylvester Keil committed
612 613 614 615 616 617
        if content.nil?
          "<#{[name, attributes].flatten.compact.join(' ')}/>"
        else
          "<#{[name, attributes].flatten.compact.join(' ')}>#{content}</#{name}>"
        end
      end
618

Sylvester Keil's avatar
Sylvester Keil committed
619 620 621
      def link_to(href, content, attributes = {})
        content_tag :a, content || href, attributes.merge(:href => href)
      end
622

623 624 625 626
      def cited_references
        context && context['cited'] || []
      end

627 628
      def keys
        # De-reference keys (in case they are variables)
Sylvester Keil's avatar
Sylvester Keil committed
629 630
        # We need to do this every time, to support for loops,
        # where the context can change for each invocation.
631
        Array(@keys).map do |key|
632
          context[key] || key
633 634 635
        end
      end

636 637 638 639
      def interpolate(string)
        return unless string

        string.gsub(/{{\s*([\w\.]+)\s*}}/) do |match|
640
          context[$1] || match
641 642 643
        end
      end

Sylvester Keil's avatar
Sylvester Keil committed
644
      def set_context_to(context)
645
        @context, @site, = context, context.registers[:site]
Sylvester Keil's avatar
Sylvester Keil committed
646
        config.merge!(site.config['scholar'] || {})
647
        self
Sylvester Keil's avatar
Sylvester Keil committed
648
      end
649
    end
650

651
  end
Hiren Patel's avatar
Hiren Patel committed
652
end