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.
- 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'
|