cff 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/cff/file.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2018-2021 The Ruby Citation File Format Developers.
3
+ # Copyright (c) 2018-2022 The Ruby Citation File Format Developers.
4
4
  #
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
6
6
  # you may not use this file except in compliance with the License.
@@ -14,10 +14,16 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require_relative 'errors'
18
+ require_relative 'index'
19
+ require_relative 'version'
20
+
21
+ require 'date'
22
+ require 'yaml'
23
+
17
24
  ##
18
25
  module CFF
19
-
20
- # File provides direct access to a CFF Model, with the addition of some
26
+ # File provides direct access to a CFF Index, with the addition of some
21
27
  # filesystem utilities.
22
28
  #
23
29
  # To be a fully compliant and valid CFF file its filename should be
@@ -25,7 +31,6 @@ module CFF
25
31
  # and to validate the contents of those files independently of the preferred
26
32
  # filename.
27
33
  class File
28
-
29
34
  # A comment to be inserted at the top of the resultant CFF file.
30
35
  attr_reader :comment
31
36
 
@@ -42,18 +47,18 @@ module CFF
42
47
 
43
48
  # :call-seq:
44
49
  # new(filename, title) -> File
45
- # new(filename, model) -> File
50
+ # new(filename, index) -> File
46
51
  #
47
- # Create a new File. Either a pre-existing Model can be passed in or, as
48
- # with Model itself, a title can be supplied to initalize a new File.
52
+ # Create a new File. Either a pre-existing Index can be passed in or, as
53
+ # with Index itself, a title can be supplied to initalize a new File.
49
54
  #
50
- # All methods provided by Model are also available directly on File
55
+ # All methods provided by Index are also available directly on File
51
56
  # objects.
52
57
  def initialize(filename, param, comment = CFF_COMMENT, create: false)
53
- param = Model.new(param) unless param.is_a?(Model)
58
+ param = Index.new(param) unless param.is_a?(Index)
54
59
 
55
60
  @filename = filename
56
- @model = param
61
+ @index = param
57
62
  @comment = comment
58
63
  @dirty = create
59
64
  end
@@ -132,10 +137,10 @@ module CFF
132
137
 
133
138
  # :call-seq:
134
139
  # write(filename, File)
135
- # write(filename, Model)
140
+ # write(filename, Index)
136
141
  # write(filename, yaml)
137
142
  #
138
- # Write the supplied File, Model or yaml string to `file`.
143
+ # Write the supplied File, Index or yaml string to `file`.
139
144
  def self.write(file, cff, comment = '')
140
145
  comment = cff.comment if cff.respond_to?(:comment)
141
146
  cff = cff.to_yaml unless cff.is_a?(String)
@@ -157,7 +162,7 @@ module CFF
157
162
  # validation failure with the `fail_on_filename` parameter (default: true).
158
163
  def validate(fail_fast: false, fail_on_filename: true)
159
164
  valid_filename = (::File.basename(@filename) == CFF_VALID_FILENAME)
160
- result = (@model.validate(fail_fast: fail_fast) << valid_filename)
165
+ result = (@index.validate(fail_fast: fail_fast) << valid_filename)
161
166
  result[0] &&= valid_filename if fail_on_filename
162
167
 
163
168
  result
@@ -194,7 +199,7 @@ module CFF
194
199
  @dirty = true
195
200
  end
196
201
 
197
- File.write(@filename, @model, @comment) if @dirty
202
+ File.write(@filename, @index, @comment) if @dirty
198
203
  @dirty = false
199
204
  end
200
205
 
@@ -218,20 +223,20 @@ module CFF
218
223
  end
219
224
 
220
225
  def to_yaml # :nodoc:
221
- @model.to_yaml
226
+ @index.to_yaml
222
227
  end
223
228
 
224
229
  def method_missing(name, *args) # :nodoc:
225
- if @model.respond_to?(name)
230
+ if @index.respond_to?(name)
226
231
  @dirty = true if name.to_s.end_with?('=') # Remove to_s when Ruby >2.6.
227
- @model.send(name, *args)
232
+ @index.send(name, *args)
228
233
  else
229
234
  super
230
235
  end
231
236
  end
232
237
 
233
238
  def respond_to_missing?(name, *all) # :nodoc:
234
- @model.respond_to?(name, *all)
239
+ @index.respond_to?(name, *all)
235
240
  end
236
241
 
237
242
  def self.format_comment(comment) # :nodoc:
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018-2022 The Ruby Citation File Format Developers.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require_relative 'apalike'
18
+ require_relative 'bibtex'
19
+
20
+ ##
21
+ module CFF
22
+ module Formatters # :nodoc:
23
+ register_formatter(APALike)
24
+ register_formatter(BibTeX)
25
+ end
26
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018-2022 The Ruby Citation File Format Developers.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require_relative 'formatter'
18
+
19
+ ##
20
+ module CFF
21
+ module Formatters # :nodoc:
22
+ # Generates an APALIKE citation string
23
+ class APALike < Formatter # :nodoc:
24
+ def self.format(model:, preferred_citation: true) # rubocop:disable Metrics/AbcSize
25
+ model = select_and_check_model(model, preferred_citation)
26
+ return if model.nil?
27
+
28
+ output = []
29
+ output << combine_authors(
30
+ model.authors.map { |author| format_author(author) }
31
+ )
32
+
33
+ date = month_and_year_from_model(model)
34
+ output << "(#{date})" unless date.empty?
35
+
36
+ version = " (Version #{model.version})" unless model.version.to_s.empty?
37
+ output << "#{model.title}#{version}#{type_label(model)}"
38
+ output << publication_data_from_model(model)
39
+ output << url(model)
40
+
41
+ output.reject(&:empty?).join('. ')
42
+ end
43
+
44
+ def self.publication_data_from_model(model) # rubocop:disable Metrics
45
+ case model.type
46
+ when 'article'
47
+ [
48
+ model.journal,
49
+ volume_from_model(model),
50
+ pages_from_model(model, dash: '–'),
51
+ note_from_model(model) || ''
52
+ ].reject(&:empty?).join(', ')
53
+ when 'book'
54
+ model.publisher.empty? ? '' : model.publisher.name
55
+ when 'conference-paper'
56
+ [
57
+ model.collection_title,
58
+ volume_from_model(model),
59
+ pages_from_model(model, dash: '–')
60
+ ].reject(&:empty?).join(', ')
61
+ when 'report'
62
+ if model.institution.empty?
63
+ model.authors.first.affiliation
64
+ else
65
+ model.institution.name
66
+ end
67
+ when 'phdthesis'
68
+ type_and_school_from_model(model, 'Doctoral dissertation')
69
+ when 'mastersthesis'
70
+ type_and_school_from_model(model, "Master's thesis")
71
+ when 'unpublished'
72
+ note_from_model(model) || ''
73
+ else
74
+ ''
75
+ end
76
+ end
77
+
78
+ def self.type_and_school_from_model(model, type)
79
+ type = model.thesis_type == '' ? type : model.thesis_type
80
+ school = model.institution.empty? ? model.authors.first.affiliation : model.institution.name
81
+ "[#{type}, #{school}]"
82
+ end
83
+
84
+ def self.volume_from_model(model)
85
+ issue = model.issue.to_s.empty? ? '' : "(#{model.issue})"
86
+ model.volume.to_s.empty? ? '' : "#{model.volume}#{issue}"
87
+ end
88
+
89
+ # If we're citing a conference paper, try and use the date of the
90
+ # conference. Otherwise use the specified month and year, or the date
91
+ # of release.
92
+ def self.month_and_year_from_model(model)
93
+ if model.type == 'conference-paper' && !model.conference.empty?
94
+ start = model.conference.date_start
95
+ unless start == ''
96
+ finish = model.conference.date_end
97
+ return month_and_year_from_date(start)[1] if finish == '' || start >= finish
98
+
99
+ return date_range(start, finish)
100
+ end
101
+ end
102
+
103
+ super[1]
104
+ end
105
+
106
+ def self.date_range(start, finish)
107
+ start_str = '%Y, %B %-d'
108
+ finish_str = '%-d'
109
+ finish_str = "%B #{finish_str}" unless start.month == finish.month
110
+ finish_str = "%Y, #{finish_str}" unless start.year == finish.year
111
+
112
+ "#{start.strftime(start_str)}–#{finish.strftime(finish_str)}"
113
+ end
114
+
115
+ # Prefer a DOI over the other URI options.
116
+ def self.url(model)
117
+ model.doi.empty? ? super : "https://doi.org/#{model.doi}"
118
+ end
119
+
120
+ def self.type_label(model)
121
+ return ' [Data set]' if model.type.include?('data')
122
+ return ' [Conference paper]' if model.type.include?('conference')
123
+ return '' if model.is_a?(Reference) && !model.type.include?('software')
124
+
125
+ ' [Computer software]'
126
+ end
127
+
128
+ def self.combine_authors(authors)
129
+ return authors[0].chomp('.') if authors.length == 1
130
+
131
+ "#{authors[0..-2].join(', ')}, & #{authors[-1]}".chomp('.')
132
+ end
133
+
134
+ def self.format_author(author)
135
+ return author.name if author.is_a?(Entity)
136
+
137
+ particle =
138
+ author.name_particle.empty? ? '' : "#{author.name_particle} "
139
+ suffix = author.name_suffix.empty? ? '.' : "., #{author.name_suffix}"
140
+
141
+ "#{particle}#{author.family_names}, #{initials(author.given_names)}#{suffix}"
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018-2022 The Ruby Citation File Format Developers.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require_relative 'formatter'
18
+
19
+ ##
20
+ module CFF
21
+ module Formatters # :nodoc:
22
+ # Generates an BibTeX citation string
23
+ class BibTeX < Formatter # :nodoc:
24
+ # Fields without `!` have a simple one-to-one mapping between CFF and
25
+ # BibTeX. Those with `!` call out to a more complex getter.
26
+ ENTRY_TYPE_MAP = {
27
+ 'article' => %w[doi journal note! number! pages! volume],
28
+ 'book' => %w[address! doi editor! isbn number! pages! publisher! volume],
29
+ 'booklet' => %w[address! doi],
30
+ 'inproceedings' => %w[address! booktitle! doi editor! pages! publisher! series!],
31
+ 'manual' => %w[address! doi],
32
+ 'mastersthesis' => %w[address! doi school! type!],
33
+ 'misc' => %w[doi pages!],
34
+ 'phdthesis' => %w[address! doi school! type!],
35
+ 'proceedings' => %w[address! booktitle! doi editor! pages! publisher! series!],
36
+ 'software' => %w[doi license version],
37
+ 'techreport' => %w[address! doi institution! number!],
38
+ 'unpublished' => %w[doi note!]
39
+ }.freeze
40
+
41
+ def self.format(model:, preferred_citation: true) # rubocop:disable Metrics/AbcSize
42
+ model = select_and_check_model(model, preferred_citation)
43
+ return if model.nil?
44
+
45
+ values = {}
46
+ values['author'] = actor_list(model.authors)
47
+ values['title'] = "{#{model.title}}"
48
+
49
+ publication_type = bibtex_type(model)
50
+ publication_data_from_model(model, publication_type, values)
51
+
52
+ month, year = month_and_year_from_model(model)
53
+ values['month'] = month
54
+ values['year'] = year
55
+
56
+ values['url'] = url(model)
57
+
58
+ values['note'] ||= model.notes unless model.is_a?(Index)
59
+
60
+ values.reject! { |_, v| v.empty? }
61
+ sorted_values = values.sort.map do |key, value|
62
+ "#{key} = {#{value}}"
63
+ end
64
+ sorted_values.insert(0, generate_citekey(values))
65
+
66
+ "@#{publication_type}{#{sorted_values.join(",\n")}\n}"
67
+ end
68
+
69
+ # Get various bits of information about the reference publication.
70
+ # Reference: https://www.bibtex.com/format/
71
+ def self.publication_data_from_model(model, type, fields)
72
+ ENTRY_TYPE_MAP[type].each do |field|
73
+ if model.respond_to?(field)
74
+ fields[field] = model.send(field).to_s
75
+ else
76
+ field = field.chomp('!')
77
+ fields[field] = send("#{field}_from_model", model)
78
+ end
79
+ end
80
+ end
81
+
82
+ # BibTeX 'number' is CFF 'issue'.
83
+ def self.number_from_model(model)
84
+ model.issue.to_s
85
+ end
86
+
87
+ # BibTeX 'address' is taken from the publisher (book, others) or the
88
+ # conference (inproceedings).
89
+ def self.address_from_model(model)
90
+ entity = if model.type == 'conference-paper'
91
+ model.conference
92
+ else
93
+ model.publisher
94
+ end
95
+ return '' if entity.empty?
96
+
97
+ [entity.city, entity.region, entity.country].reject(&:empty?).join(', ')
98
+ end
99
+
100
+ # BibTeX 'institution' could be grabbed from an author's affiliation, or
101
+ # provided explicitly.
102
+ def self.institution_from_model(model)
103
+ return model.institution.name unless model.institution.empty?
104
+
105
+ model.authors.first.affiliation
106
+ end
107
+
108
+ # BibTeX 'school' is CFF 'institution'.
109
+ def self.school_from_model(model)
110
+ institution_from_model(model)
111
+ end
112
+
113
+ # BibTeX 'type' for theses is CFF 'thesis-type'.
114
+ def self.type_from_model(model)
115
+ model.thesis_type
116
+ end
117
+
118
+ # BibTeX 'booktitle' is CFF 'collection-title'.
119
+ def self.booktitle_from_model(model)
120
+ model.collection_title
121
+ end
122
+
123
+ # BibTeX 'editor' is CFF 'editors' or 'editors-series'.
124
+ def self.editor_from_model(model)
125
+ if model.editors.empty?
126
+ model.editors_series.empty? ? '' : actor_list(model.editors_series)
127
+ else
128
+ actor_list(model.editors)
129
+ end
130
+ end
131
+
132
+ def self.publisher_from_model(model)
133
+ model.publisher.empty? ? '' : model.publisher.name
134
+ end
135
+
136
+ def self.series_from_model(model)
137
+ model.conference.empty? ? '' : model.conference.name
138
+ end
139
+
140
+ # If we're citing a conference paper, try and use the date of the
141
+ # conference. Otherwise use the specified month and year, or the date
142
+ # of release.
143
+ def self.month_and_year_from_model(model)
144
+ if model.type == 'conference-paper' && !model.conference.empty?
145
+ date = model.conference.date_start
146
+ return month_and_year_from_date(date) unless date == ''
147
+ end
148
+
149
+ super
150
+ end
151
+
152
+ # Do what we can to map between CFF reference types and bibtex types.
153
+ # References:
154
+ # * https://www.bibtex.com/e/entry-types/
155
+ # * https://ctan.gutenberg.eu.org/macros/latex/contrib/biblatex-contrib/biblatex-software/software-biblatex.pdf
156
+ def self.bibtex_type(model) # rubocop:disable Metrics/CyclomaticComplexity
157
+ return 'software' if model.type.empty? || model.type.include?('software')
158
+
159
+ case model.type
160
+ when 'article', 'book', 'manual', 'unpublished', 'phdthesis', 'mastersthesis'
161
+ model.type
162
+ when 'conference', 'proceedings'
163
+ 'proceedings'
164
+ when 'conference-paper'
165
+ 'inproceedings'
166
+ when 'magazine-article', 'newspaper-article'
167
+ 'article'
168
+ when 'pamphlet'
169
+ 'booklet'
170
+ when 'report'
171
+ 'techreport'
172
+ else
173
+ 'misc'
174
+ end
175
+ end
176
+
177
+ def self.format_actor(author)
178
+ return "{#{author.name}}" if author.is_a?(Entity)
179
+
180
+ particle =
181
+ author.name_particle.empty? ? '' : "#{author.name_particle} "
182
+
183
+ [
184
+ "#{particle}#{author.family_names}",
185
+ author.name_suffix,
186
+ author.given_names
187
+ ].reject(&:empty?).join(', ')
188
+ end
189
+
190
+ def self.actor_list(actors)
191
+ actors.map { |actor| format_actor(actor) }.join(' and ')
192
+ end
193
+
194
+ def self.generate_citekey(fields)
195
+ reference = [
196
+ fields['author'].split(',', 2)[0],
197
+ fields['title'].split[0..2],
198
+ fields['year']
199
+ ].compact.join('_')
200
+
201
+ Util.parameterize(reference)
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018-2022 The Ruby Citation File Format Developers.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'date'
18
+
19
+ ##
20
+ module CFF
21
+ module Formatters # :nodoc:
22
+ # Formatter base class
23
+ class Formatter # :nodoc:
24
+ STATUS_TEXT_MAP = {
25
+ 'advance-online' => 'Advance online publication',
26
+ 'in-preparation' => 'Manuscript in preparation.',
27
+ 'submitted' => 'Manuscript submitted for publication.'
28
+ }.freeze
29
+
30
+ def self.label
31
+ @label ||= name.split('::')[-1]
32
+ end
33
+
34
+ def self.select_and_check_model(model, preferred_citation)
35
+ if preferred_citation && model.preferred_citation.is_a?(Reference)
36
+ model = model.preferred_citation
37
+ end
38
+
39
+ # Safe to assume valid `Index`s and `Reference`s will have these fields.
40
+ model.authors.empty? || model.title.empty? ? nil : model
41
+ end
42
+
43
+ def self.initials(name)
44
+ name.split.map { |part| part[0].capitalize }.join('. ')
45
+ end
46
+
47
+ def self.note_from_model(model)
48
+ STATUS_TEXT_MAP[model.status]
49
+ end
50
+
51
+ # Prefer `repository_code` over `url`
52
+ def self.url(model)
53
+ model.repository_code.empty? ? model.url : model.repository_code
54
+ end
55
+
56
+ def self.month_and_year_from_model(model) # rubocop:disable Metrics
57
+ return ['', 'in press'] if model.respond_to?(:status) && model.status == 'in-press'
58
+ if model.respond_to?(:year) && !model.year.to_s.empty?
59
+ return [model.month, model.year].map(&:to_s)
60
+ end
61
+
62
+ date = month_and_year_from_date(model.date_released)
63
+ if date == ['', ''] && model.respond_to?(:date_published)
64
+ date = month_and_year_from_date(model.date_published)
65
+ end
66
+ date
67
+ end
68
+
69
+ def self.month_and_year_from_date(value)
70
+ if value.is_a?(Date)
71
+ [value.month, value.year].map(&:to_s)
72
+ else
73
+ begin
74
+ date = Date.parse(value.to_s)
75
+ [date.month, date.year].map(&:to_s)
76
+ rescue ArgumentError
77
+ ['', '']
78
+ end
79
+ end
80
+ end
81
+
82
+ # CFF 'pages' is the number of pages, which has no equivalent in BibTeX
83
+ # or APA. References: https://www.bibtex.com/f/pages-field/,
84
+ # https://apastyle.apa.org/style-grammar-guidelines/references/examples
85
+ def self.pages_from_model(model, dash: '--')
86
+ return '' if !model.respond_to?(:start) || model.start.to_s.empty?
87
+
88
+ start = model.start.to_s
89
+ finish = model.end.to_s
90
+ if finish.empty?
91
+ start
92
+ else
93
+ start == finish ? start : "#{start}#{dash}#{finish}"
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018-2022 The Ruby Citation File Format Developers.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ ##
18
+ module CFF
19
+ # A registry of output formatters for converting CFF files into citations.
20
+ module Formatters
21
+ @formatters = {}
22
+
23
+ # :call-seq:
24
+ # formatters -> Array
25
+ #
26
+ # Return the list of formatters that are available.
27
+ def self.formatters
28
+ @formatters.keys
29
+ end
30
+
31
+ # :call-seq:
32
+ # register_formatter(class)
33
+ #
34
+ # Register a citation formatter. To be registered as a formatter, a
35
+ # class should at least provide the following class methods:
36
+ #
37
+ # * `format`, which takes the model to be formatted
38
+ # as a named parameter, and the option to cite a CFF file's
39
+ # `preferred-citation`:
40
+ # ```ruby
41
+ # def self.format(model:, preferred_citation: true); end
42
+ # ```
43
+ # * `label`, which returns a short name for the formatter, e.g.
44
+ # `'BibTeX'`. If your formatter class subclasses `CFF::Formatter`,
45
+ # then `label` is provided for you.
46
+ def self.register_formatter(clazz)
47
+ return unless clazz.singleton_methods.include?(:format)
48
+ return if @formatters.has_value?(clazz)
49
+
50
+ format = clazz.label.downcase.to_sym
51
+ @formatters[format] = clazz
52
+ Citable.add_to_format_method(format) if defined?(Citable)
53
+ end
54
+
55
+ def self.formatter_for(format) # :nodoc:
56
+ @formatters[format.downcase.to_sym]
57
+ end
58
+ end
59
+ end
60
+
61
+ require_relative 'formatters/all'