utilities.rb 15.8 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 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
      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'
          case value
          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
        else
          value.to_s
        end
      end
      
Sylvester Keil's avatar
Sylvester Keil committed
312 313 314 315
      def suppress_author?
        !!@suppress_author
      end

Sylvester Keil's avatar
Sylvester Keil committed
316
      def raw_bibtex?
317
        config['use_raw_bibtex_entry']
Sylvester Keil's avatar
Sylvester Keil committed
318 319
      end

320 321 322 323 324 325 326 327 328
      def repository?
        !config['repository'].nil? && !config['repository'].empty?
      end

      def repository
        @repository ||= load_repository
      end

      def load_repository
329
        repo = Hash.new { |h,k| h[k] = {} }
330

331 332 333 334 335 336 337 338
        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
339 340 341 342 343 344
      end

      def repository_path
        config['repository']
      end

345 346 347 348
      def replace_strings?
        config['replace_strings']
      end

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

353 354 355 356
      def cited_only?
        !!@cited
      end

Sylvester Keil's avatar
Sylvester Keil committed
357 358 359 360
      def skip_sort?
        @skip_sort || config['sort_by'] == 'none'
      end

361 362
      def extend_path(name)
        if name.nil? || name.empty?
363
          name = config['bibliography']
364
        end
365

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

Sylvester Keil's avatar
Sylvester Keil committed
370 371 372 373 374 375 376 377 378 379 380 381
        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
382
      end
383

Sylvester Keil's avatar
Sylvester Keil committed
384
      def reference_tag(entry, index = nil)
Sylvester Keil's avatar
Sylvester Keil committed
385
        return missing_reference unless entry
386

387
        entry = entry.convert(*bibtex_filters) unless bibtex_filters.empty?
Sylvester Keil's avatar
Sylvester Keil committed
388
        reference = render_bibliography entry, index
389

Sylvester Keil's avatar
Sylvester Keil committed
390 391 392 393
        content_tag reference_tagname, reference,
          :id => [prefix, entry.key].compact.join('-')
      end

394 395 396 397
      def style
        @style || config['style']
      end

Sylvester Keil's avatar
Sylvester Keil committed
398 399 400 401 402 403 404 405 406
      def missing_reference
        config['missing_reference']
      end

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

      def bibliography_template
407 408 409 410 411
        @bibliography_template || config['bibliography_template']
      end

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

414
        tmp = bibliography_template
415 416

        case
417
        when tmp.nil?, tmp.empty?
418 419 420 421 422
          tmp = '{{reference}}'
        when site.layouts.key?(tmp)
          tmp = site.layouts[tmp].content
        end

423
        @liquid_template = Liquid::Template.parse(tmp)
Sylvester Keil's avatar
Sylvester Keil committed
424 425
      end

426
      def bibliography_tag(entry, index)
Sylvester Keil's avatar
Sylvester Keil committed
427 428
        return missing_reference unless entry

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

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

Sylvester Keil's avatar
Sylvester Keil committed
454 455 456 457
      def liquidify(entry)
        e = {}

        e['key'] = entry.key
458
        e['type'] = entry.type.to_s
Sylvester Keil's avatar
Sylvester Keil committed
459

460 461 462
        if entry.field_names(config['bibtex_skip_fields']).empty?
          e['bibtex'] = entry.to_s
        else
Sylvester Keil's avatar
Sylvester Keil committed
463
          tmp = entry.dup
464 465 466 467 468

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

Sylvester Keil's avatar
Sylvester Keil committed
469 470
          e['bibtex'] = tmp.to_s
        end
Sylvester Keil's avatar
Sylvester Keil committed
471

Sylvester Keil's avatar
Sylvester Keil committed
472 473 474 475
        if raw_bibtex?
          e['bibtex'] = "{%raw%}#{e['bibtex']}{%endraw%}"
        end

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

          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
487 488 489 490 491
        end

        e
      end

492 493 494
      def bibtex_skip_fields
      end

495 496 497
      def generate_details?
        site.layouts.key?(File.basename(config['details_layout'], '.html'))
      end
498

499 500
      def details_file_for(entry)
        name = entry.key.to_s.dup
501

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

504
        if site.config['permalink'] == 'pretty'
505
          name << '/'
506 507 508
        else
          name << '.html'
        end
509
      end
510

511
      def repository_link_for(entry, base = base_url)
512 513 514
        links = repository[entry.key]
        url   = links['pdf'] || links['ps']

515 516 517 518 519
        return unless url

        File.join(base, url)
      end

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

Sylvester Keil's avatar
Sylvester Keil committed
530
      def base_url
531
        @base_url ||= site.config['baseurl'] || site.config['base_url'] || ''
532
      end
533

534 535 536
      def details_path
        config['details_dir']
      end
537

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

545 546
      def render_citation(items)
        renderer.render items.zip(locators).map { |entry, locator|
Sylvester Keil's avatar
Sylvester Keil committed
547
          cited_keys << entry.key
Sylvester Keil's avatar
4.2.1  
Sylvester Keil committed
548
          cited_keys.uniq!
549

Sylvester Keil's avatar
Sylvester Keil committed
550
          item = citation_item_for entry, citation_number(entry.key)
551
          item.locator = locator
552

553 554
          item
        }, STYLES[style].citation
Sylvester Keil's avatar
Sylvester Keil committed
555 556
      end

Sylvester Keil's avatar
Sylvester Keil committed
557 558 559
      def render_bibliography(entry, index = nil)
        renderer.render citation_item_for(entry, index),
          STYLES[style].bibliography
Sylvester Keil's avatar
Sylvester Keil committed
560 561
      end

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

570
      def cited_keys
571
        context['cited'] ||= []
572
      end
573

Sylvester Keil's avatar
Sylvester Keil committed
574 575
      def citation_number(key)
        (context['citation_numbers'] ||= {})[key] ||= cited_keys.length
Sylvester Keil's avatar
Sylvester Keil committed
576 577
      end

578 579 580 581 582 583 584 585
      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
586
        end
587 588

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

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

Sylvester Keil's avatar
Sylvester Keil committed
599 600 601 602 603 604
      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
605

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

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

Sylvester Keil's avatar
Sylvester Keil committed
615 616 617
      def link_to(href, content, attributes = {})
        content_tag :a, content || href, attributes.merge(:href => href)
      end
618

619 620 621 622
      def cited_references
        context && context['cited'] || []
      end

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

632 633 634 635
      def interpolate(string)
        return unless string

        string.gsub(/{{\s*([\w\.]+)\s*}}/) do |match|
636
          context[$1] || match
637 638 639
        end
      end

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

647
  end
Hiren Patel's avatar
Hiren Patel committed
648
end