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
      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
      
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
      def month_name(month):
        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

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

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

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

      def repository
        @repository ||= load_repository
      end

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

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

      def repository_path
        config['repository']
      end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

417
        tmp = bibliography_template
418 419

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

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

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

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

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

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

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

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

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

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

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

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

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

        e
      end

495 496 497
      def bibtex_skip_fields
      end

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

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

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

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

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

518 519 520 521 522
        return unless url

        File.join(base, url)
      end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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