cff 0.9.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +73 -0
- data/CITATION.cff +3 -3
- data/CONTRIBUTING.md +71 -0
- data/LICENCE +1 -1
- data/README.md +27 -21
- data/Rakefile +9 -8
- data/cff.gemspec +12 -11
- data/lib/cff/citable.rb +72 -0
- data/lib/cff/entity.rb +11 -31
- data/lib/cff/errors.rb +2 -5
- data/lib/cff/file.rb +23 -18
- data/lib/cff/formatters/all.rb +26 -0
- data/lib/cff/formatters/apalike.rb +145 -0
- data/lib/cff/formatters/bibtex.rb +205 -0
- data/lib/cff/formatters/formatter.rb +98 -0
- data/lib/cff/formatters.rb +61 -0
- data/lib/cff/identifier.rb +13 -8
- data/lib/cff/{model.rb → index.rb} +65 -81
- data/lib/cff/licensable.rb +3 -3
- data/lib/cff/model_part.rb +46 -10
- data/lib/cff/person.rb +8 -10
- data/lib/cff/reference.rb +55 -114
- data/lib/cff/schema.rb +23 -0
- data/lib/{schema → cff/schemas}/1.2.0.json +0 -0
- data/lib/cff/util.rb +63 -6
- data/lib/cff/validatable.rb +12 -11
- data/lib/cff/version.rb +2 -2
- data/lib/cff.rb +4 -27
- metadata +37 -31
- data/lib/cff/formatter/apa_formatter.rb +0 -77
- data/lib/cff/formatter/bibtex_formatter.rb +0 -122
- data/lib/cff/formatter/formatter.rb +0 -63
data/lib/cff/file.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright (c) 2018-
|
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,
|
50
|
+
# new(filename, index) -> File
|
46
51
|
#
|
47
|
-
# Create a new File. Either a pre-existing
|
48
|
-
# with
|
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
|
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 =
|
58
|
+
param = Index.new(param) unless param.is_a?(Index)
|
54
59
|
|
55
60
|
@filename = filename
|
56
|
-
@
|
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,
|
140
|
+
# write(filename, Index)
|
136
141
|
# write(filename, yaml)
|
137
142
|
#
|
138
|
-
# Write the supplied 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 = (@
|
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, @
|
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
|
-
@
|
226
|
+
@index.to_yaml
|
222
227
|
end
|
223
228
|
|
224
229
|
def method_missing(name, *args) # :nodoc:
|
225
|
-
if @
|
230
|
+
if @index.respond_to?(name)
|
226
231
|
@dirty = true if name.to_s.end_with?('=') # Remove to_s when Ruby >2.6.
|
227
|
-
@
|
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
|
-
@
|
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'
|