utilities.rb 15.9 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 8
    # 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
9 10 11
      style = CSL::Style.load k
      style = style.independent_parent unless style.independent?
      h[k.to_s] = style
Sylvester Keil's avatar
Sylvester Keil committed
12 13 14
    end


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

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

      def split_arguments(arguments)

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

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

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

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

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

48
          opts.on('-f', '--file FILE') do |file|
49 50
            @bibtex_files ||= []
            @bibtex_files << file
51 52 53 54 55 56 57 58 59 60 61 62 63
          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
64

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        @bibliography
135 136
      end

137 138 139 140
      def query
        interpolate @query
      end

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

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

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

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

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

160 161 162 163 164 165 166 167 168 169 170 171
        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
172
        end
173
 
174
        sorted
175
      end
176

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

186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
      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|
              if order.first =~ /^(desc|reverse)/i
206
                group_compare(keys.first,e2[0],e1[0])
207
              else
208
                group_compare(keys.first,e1[0],e2[0])
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
              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
 
240
      def group_compare(key,v1,v2)
241 242
        case key
        when 'type'
243 244 245 246 247 248 249 250 251 252 253
          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
254
        else
255
          v1 <=> v2
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
        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'
286
          type_names[value] || value.to_s
287
        when 'month_numeric'
288
          month_names[value] || "(unknown)"
289 290 291 292
        else
          value.to_s
        end
      end
293 294 295 296 297 298 299 300 301 302 303 304 305

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

      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
306
      end
307

Sylvester Keil's avatar
Sylvester Keil committed
308 309 310 311
      def suppress_author?
        !!@suppress_author
      end

Sylvester Keil's avatar
Sylvester Keil committed
312
      def raw_bibtex?
313
        config['use_raw_bibtex_entry']
Sylvester Keil's avatar
Sylvester Keil committed
314 315
      end

316 317 318 319 320 321 322 323 324
      def repository?
        !config['repository'].nil? && !config['repository'].empty?
      end

      def repository
        @repository ||= load_repository
      end

      def load_repository
325
        repo = Hash.new { |h,k| h[k] = {} }
326

327 328 329 330 331 332 333 334
        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
335 336 337 338 339 340
      end

      def repository_path
        config['repository']
      end

341 342 343 344
      def replace_strings?
        config['replace_strings']
      end

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

349 350 351 352
      def cited_only?
        !!@cited
      end

Sylvester Keil's avatar
Sylvester Keil committed
353 354 355 356
      def skip_sort?
        @skip_sort || config['sort_by'] == 'none'
      end

357 358
      def extend_path(name)
        if name.nil? || name.empty?
359
          name = config['bibliography']
360
        end
361

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

Sylvester Keil's avatar
Sylvester Keil committed
366 367 368 369 370 371 372 373 374 375 376 377
        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
378
      end
379

Sylvester Keil's avatar
Sylvester Keil committed
380
      def reference_tag(entry, index = nil)
Sylvester Keil's avatar
Sylvester Keil committed
381
        return missing_reference unless entry
382

383
        entry = entry.convert(*bibtex_filters) unless bibtex_filters.empty?
Sylvester Keil's avatar
Sylvester Keil committed
384
        reference = render_bibliography entry, index
385

Sylvester Keil's avatar
Sylvester Keil committed
386 387 388 389
        content_tag reference_tagname, reference,
          :id => [prefix, entry.key].compact.join('-')
      end

390 391 392 393
      def style
        @style || config['style']
      end

Sylvester Keil's avatar
Sylvester Keil committed
394 395 396 397 398 399 400 401 402
      def missing_reference
        config['missing_reference']
      end

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

      def bibliography_template
403 404 405 406 407
        @bibliography_template || config['bibliography_template']
      end

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

410
        tmp = bibliography_template
411 412

        case
413
        when tmp.nil?, tmp.empty?
414 415 416 417 418
          tmp = '{{reference}}'
        when site.layouts.key?(tmp)
          tmp = site.layouts[tmp].content
        end

419
        @liquid_template = Liquid::Template.parse(tmp)
Sylvester Keil's avatar
Sylvester Keil committed
420 421
      end

422
      def bibliography_tag(entry, index)
Sylvester Keil's avatar
Sylvester Keil committed
423 424
        return missing_reference unless entry

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

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

Sylvester Keil's avatar
Sylvester Keil committed
450 451 452 453
      def liquidify(entry)
        e = {}

        e['key'] = entry.key
454
        e['type'] = entry.type.to_s
Sylvester Keil's avatar
Sylvester Keil committed
455

456 457 458
        if entry.field_names(config['bibtex_skip_fields']).empty?
          e['bibtex'] = entry.to_s
        else
Sylvester Keil's avatar
Sylvester Keil committed
459
          tmp = entry.dup
460 461 462 463 464

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

Sylvester Keil's avatar
Sylvester Keil committed
465 466
          e['bibtex'] = tmp.to_s
        end
Sylvester Keil's avatar
Sylvester Keil committed
467

Sylvester Keil's avatar
Sylvester Keil committed
468 469 470 471
        if raw_bibtex?
          e['bibtex'] = "{%raw%}#{e['bibtex']}{%endraw%}"
        end

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

          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
483 484 485 486 487
        end

        e
      end

488 489 490
      def bibtex_skip_fields
      end

491 492 493
      def generate_details?
        site.layouts.key?(File.basename(config['details_layout'], '.html'))
      end
494

495 496
      def details_file_for(entry)
        name = entry.key.to_s.dup
497

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

500
        if site.config['permalink'] == 'pretty'
501
          name << '/'
502 503 504
        else
          name << '.html'
        end
505
      end
506

507
      def repository_link_for(entry, base = base_url)
508 509 510
        links = repository[entry.key]
        url   = links['pdf'] || links['ps']

511 512 513 514 515
        return unless url

        File.join(base, url)
      end

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

Sylvester Keil's avatar
Sylvester Keil committed
526
      def base_url
527
        @base_url ||= site.config['baseurl'] || site.config['base_url'] || ''
528
      end
529

530 531 532
      def details_path
        config['details_dir']
      end
533

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

541 542
      def render_citation(items)
        renderer.render items.zip(locators).map { |entry, locator|
Sylvester Keil's avatar
Sylvester Keil committed
543
          cited_keys << entry.key
Sylvester Keil's avatar
4.2.1  
Sylvester Keil committed
544
          cited_keys.uniq!
545

Sylvester Keil's avatar
Sylvester Keil committed
546
          item = citation_item_for entry, citation_number(entry.key)
547
          item.locator = locator
548

549 550
          item
        }, STYLES[style].citation
Sylvester Keil's avatar
Sylvester Keil committed
551 552
      end

Sylvester Keil's avatar
Sylvester Keil committed
553 554 555
      def render_bibliography(entry, index = nil)
        renderer.render citation_item_for(entry, index),
          STYLES[style].bibliography
Sylvester Keil's avatar
Sylvester Keil committed
556 557
      end

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

566
      def cited_keys
567
        context['cited'] ||= []
568
      end
569

Sylvester Keil's avatar
Sylvester Keil committed
570 571
      def citation_number(key)
        (context['citation_numbers'] ||= {})[key] ||= cited_keys.length
Sylvester Keil's avatar
Sylvester Keil committed
572 573
      end

574 575 576 577 578 579 580 581
      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
582
        end
583 584

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

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

Sylvester Keil's avatar
Sylvester Keil committed
595 596 597 598 599 600
      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
601

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

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

Sylvester Keil's avatar
Sylvester Keil committed
611 612 613
      def link_to(href, content, attributes = {})
        content_tag :a, content || href, attributes.merge(:href => href)
      end
614

615 616 617 618
      def cited_references
        context && context['cited'] || []
      end

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

628 629 630 631
      def interpolate(string)
        return unless string

        string.gsub(/{{\s*([\w\.]+)\s*}}/) do |match|
632
          context[$1] || match
633 634 635
        end
      end

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

643
  end
Hiren Patel's avatar
Hiren Patel committed
644
end