cff 0.4.0 → 0.8.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 +68 -0
- data/CITATION.cff +9 -4
- data/Gemfile +2 -0
- data/LICENCE +1 -1
- data/README.md +136 -20
- data/Rakefile +3 -1
- data/bin/console +1 -0
- data/cff.gemspec +29 -15
- data/lib/cff.rb +22 -10
- data/lib/cff/entity.rb +28 -1
- data/lib/cff/errors.rb +45 -0
- data/lib/cff/file.rb +132 -13
- data/lib/cff/formatter/apa_formatter.rb +77 -0
- data/lib/cff/formatter/bibtex_formatter.rb +122 -0
- data/lib/cff/formatter/formatter.rb +63 -0
- data/lib/cff/identifier.rb +71 -0
- data/lib/cff/licensable.rb +46 -0
- data/lib/cff/model.rb +108 -14
- data/lib/cff/model_part.rb +3 -1
- data/lib/cff/person.rb +42 -8
- data/lib/cff/reference.rb +141 -42
- data/lib/cff/util.rb +15 -1
- data/lib/cff/validatable.rb +55 -0
- data/lib/cff/version.rb +6 -3
- data/lib/schema/1.2.0.json +1882 -0
- metadata +82 -28
data/lib/cff/entity.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2018-2021 The Ruby Citation File Format Developers.
|
2
4
|
#
|
3
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
6
|
# you may not use this file except in compliance with the License.
|
@@ -18,6 +20,27 @@ module CFF
|
|
18
20
|
# An Entity can represent different types of entities, e.g., a publishing
|
19
21
|
# company, or conference. Like a Person, an Entity might have a number of
|
20
22
|
# roles, such as author, contact, editor, etc.
|
23
|
+
#
|
24
|
+
# Entity implements all of the fields listed in the
|
25
|
+
# [CFF standard](https://citation-file-format.github.io/). All fields
|
26
|
+
# are simple strings and can be set as such. A field which has not been set
|
27
|
+
# will return the empty string. The simple fields are (with defaults in
|
28
|
+
# parentheses):
|
29
|
+
#
|
30
|
+
# * `address`
|
31
|
+
# * `city`
|
32
|
+
# * `country`
|
33
|
+
# * `email`
|
34
|
+
# * `date_end` - *Note:* returns a `Date` object
|
35
|
+
# * `date_start` - *Note:* returns a `Date` object
|
36
|
+
# * `fax`
|
37
|
+
# * `location`
|
38
|
+
# * `name`
|
39
|
+
# * `orcid`
|
40
|
+
# * `post_code`
|
41
|
+
# * `region`
|
42
|
+
# * `tel`
|
43
|
+
# * `website`
|
21
44
|
class Entity < ModelPart
|
22
45
|
|
23
46
|
ALLOWED_FIELDS = [
|
@@ -27,15 +50,19 @@ module CFF
|
|
27
50
|
|
28
51
|
# :call-seq:
|
29
52
|
# new(name) -> Entity
|
53
|
+
# new(name) { |entity| block } -> Entity
|
30
54
|
#
|
31
55
|
# Create a new Entity with the supplied name.
|
32
56
|
def initialize(param)
|
33
57
|
if param.is_a?(Hash)
|
34
58
|
@fields = param
|
59
|
+
@fields.default = ''
|
35
60
|
else
|
36
61
|
@fields = Hash.new('')
|
37
62
|
@fields['name'] = param
|
38
63
|
end
|
64
|
+
|
65
|
+
yield self if block_given?
|
39
66
|
end
|
40
67
|
|
41
68
|
# :call-seq:
|
data/lib/cff/errors.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2018-2021 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
|
+
|
20
|
+
# Error is the base class for all errors raised by this library.
|
21
|
+
class Error < RuntimeError
|
22
|
+
|
23
|
+
def initialize(message = nil) # :nodoc:
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# ValidationError is raised when a CFF file fails validatedation. It
|
29
|
+
# contains details of each failure that was detected by the underlying
|
30
|
+
# JsonSchema library, which is used to perform the validation.
|
31
|
+
class ValidationError < Error
|
32
|
+
|
33
|
+
# The list of JsonSchema::ValidationErrors found by the validator.
|
34
|
+
attr_reader :errors
|
35
|
+
|
36
|
+
def initialize(errors) # :nodoc:
|
37
|
+
super('Validation error')
|
38
|
+
@errors = errors
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s # :nodoc:
|
42
|
+
"#{super}: #{@errors.join(' ')}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/cff/file.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2018-2021 The Ruby Citation File Format Developers.
|
2
4
|
#
|
3
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
6
|
# you may not use this file except in compliance with the License.
|
@@ -19,21 +21,35 @@ module CFF
|
|
19
21
|
# filesystem utilities.
|
20
22
|
class File
|
21
23
|
|
22
|
-
|
24
|
+
# A comment to be inserted at the top of the resultant CFF file.
|
25
|
+
attr_reader :comment
|
26
|
+
|
27
|
+
# The filename of this CFF file.
|
28
|
+
attr_reader :filename
|
29
|
+
|
30
|
+
YAML_HEADER = "---\n" # :nodoc:
|
31
|
+
CFF_COMMENT = [
|
32
|
+
"This CITATION.cff file was created by ruby-cff (v #{CFF::VERSION}).",
|
33
|
+
'Gem: https://rubygems.org/gems/cff',
|
34
|
+
'CFF: https://citation-file-format.github.io/'
|
35
|
+
].freeze # :nodoc:
|
23
36
|
|
24
37
|
# :call-seq:
|
25
|
-
# new(title) -> File
|
26
|
-
# new(model) -> File
|
38
|
+
# new(filename, title) -> File
|
39
|
+
# new(filename, model) -> File
|
27
40
|
#
|
28
41
|
# Create a new File. Either a pre-existing Model can be passed in or, as
|
29
42
|
# with Model itself, a title can be supplied to initalize a new File.
|
30
43
|
#
|
31
44
|
# All methods provided by Model are also available directly on File
|
32
45
|
# objects.
|
33
|
-
def initialize(param)
|
46
|
+
def initialize(filename, param, comment = CFF_COMMENT, create: false)
|
34
47
|
param = Model.new(param) unless param.is_a?(Model)
|
35
48
|
|
49
|
+
@filename = filename
|
36
50
|
@model = param
|
51
|
+
@comment = comment
|
52
|
+
@dirty = create
|
37
53
|
end
|
38
54
|
|
39
55
|
# :call-seq:
|
@@ -41,7 +57,63 @@ module CFF
|
|
41
57
|
#
|
42
58
|
# Read a file and parse it for subsequent manipulation.
|
43
59
|
def self.read(file)
|
44
|
-
|
60
|
+
content = ::File.read(file)
|
61
|
+
comment = File.parse_comment(content)
|
62
|
+
|
63
|
+
new(
|
64
|
+
file, YAML.safe_load(content, permitted_classes: [Date, Time]), comment
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
# :call-seq:
|
69
|
+
# open(file) -> File
|
70
|
+
# open(file) {|cff| block }
|
71
|
+
#
|
72
|
+
# With no associated block, File.open is a synonym for ::read. If the
|
73
|
+
# optional code block is given, it will be passed the opened file as an
|
74
|
+
# argument and the File object will automatically be written (if edited)
|
75
|
+
# and closed when the block terminates.
|
76
|
+
#
|
77
|
+
# File.open will create a new file if one does not already exist with the
|
78
|
+
# provided file name.
|
79
|
+
def self.open(file)
|
80
|
+
if ::File.exist?(file)
|
81
|
+
content = ::File.read(file)
|
82
|
+
comment = File.parse_comment(content)
|
83
|
+
yaml = YAML.safe_load(content, permitted_classes: [Date, Time])
|
84
|
+
else
|
85
|
+
comment = CFF_COMMENT
|
86
|
+
yaml = ''
|
87
|
+
end
|
88
|
+
|
89
|
+
cff = new(file, yaml, comment, create: (yaml == ''))
|
90
|
+
return cff unless block_given?
|
91
|
+
|
92
|
+
begin
|
93
|
+
yield cff
|
94
|
+
ensure
|
95
|
+
cff.write
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# :call-seq:
|
100
|
+
# validate(file) -> Array
|
101
|
+
#
|
102
|
+
# Read a file and return an array with the result. The result array is a
|
103
|
+
# two-element array, with `true`/`false` at index 0 to indicate pass/fail,
|
104
|
+
# and an array of errors at index 1 (if any).
|
105
|
+
def self.validate(file)
|
106
|
+
File.read(file).validate
|
107
|
+
end
|
108
|
+
|
109
|
+
# :call-seq:
|
110
|
+
# validate!(file)
|
111
|
+
#
|
112
|
+
# Read a file and raise a ValidationError upon failure. If an error is
|
113
|
+
# raised it will contain the detected validation failures for further
|
114
|
+
# inspection.
|
115
|
+
def self.validate!(file)
|
116
|
+
File.read(file).validate!
|
45
117
|
end
|
46
118
|
|
47
119
|
# :call-seq:
|
@@ -49,26 +121,73 @@ module CFF
|
|
49
121
|
# write(file, yaml)
|
50
122
|
#
|
51
123
|
# Write the supplied model or yaml string to `file`.
|
52
|
-
def self.write(file, cff)
|
124
|
+
def self.write(file, cff, comment = '')
|
53
125
|
cff = cff.to_yaml unless cff.is_a?(String)
|
126
|
+
content = File.format_comment(comment) + cff[YAML_HEADER.length...-1]
|
127
|
+
|
128
|
+
::File.write(file, content)
|
129
|
+
end
|
54
130
|
|
55
|
-
|
131
|
+
# :call-seq:
|
132
|
+
# write
|
133
|
+
#
|
134
|
+
# Write this CFF File.
|
135
|
+
def write
|
136
|
+
File.write(@filename, @model, @comment) if @dirty
|
137
|
+
@dirty = false
|
56
138
|
end
|
57
139
|
|
58
140
|
# :call-seq:
|
59
|
-
#
|
141
|
+
# comment = string or array
|
142
|
+
#
|
143
|
+
# A comment to be inserted at the top of the resultant CFF file. This can
|
144
|
+
# be supplied as a simple string or an array of strings. When the file is
|
145
|
+
# saved this comment is formatted as follows:
|
146
|
+
#
|
147
|
+
# * a simple string is split into 75 character lines and `'# '` is prepended
|
148
|
+
# to each line;
|
149
|
+
# * an array of strings is joined into a single string with `'\n'` and
|
150
|
+
# `'# '` is prepended to each line;
|
60
151
|
#
|
61
|
-
#
|
62
|
-
|
63
|
-
|
152
|
+
# If you care about formatting, use an array of strings for your comment,
|
153
|
+
# if not, use a single string.
|
154
|
+
def comment=(comment)
|
155
|
+
@dirty = true
|
156
|
+
@comment = comment
|
64
157
|
end
|
65
158
|
|
66
159
|
def method_missing(name, *args) # :nodoc:
|
67
|
-
@model.respond_to?(name)
|
160
|
+
if @model.respond_to?(name)
|
161
|
+
@dirty = true if name.to_s.end_with?('=') # Remove to_s when Ruby >2.6.
|
162
|
+
@model.send(name, *args)
|
163
|
+
else
|
164
|
+
super
|
165
|
+
end
|
68
166
|
end
|
69
167
|
|
70
168
|
def respond_to_missing?(name, *all) # :nodoc:
|
71
169
|
@model.respond_to?(name, *all)
|
72
170
|
end
|
171
|
+
|
172
|
+
def self.format_comment(comment) # :nodoc:
|
173
|
+
return '' if comment.empty?
|
174
|
+
|
175
|
+
comment = comment.scan(/.{1,75}/) if comment.is_a?(String)
|
176
|
+
c = comment.map do |l|
|
177
|
+
l.empty? ? '#' : "# #{l}"
|
178
|
+
end.join("\n")
|
179
|
+
|
180
|
+
"#{c}\n\n"
|
181
|
+
end
|
182
|
+
|
183
|
+
def self.parse_comment(content) # :nodoc:
|
184
|
+
content = content.split("\n")
|
185
|
+
|
186
|
+
content.reduce([]) do |acc, line|
|
187
|
+
break acc unless line.start_with?('#')
|
188
|
+
|
189
|
+
acc << line.sub(/^#+/, '').strip
|
190
|
+
end
|
191
|
+
end
|
73
192
|
end
|
74
193
|
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2018-2021 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
|
+
# Generates an APALIKE citation string
|
20
|
+
class ApaFormatter < Formatter # :nodoc:
|
21
|
+
|
22
|
+
def self.format(model:, preferred_citation: true) # rubocop:disable Metrics/AbcSize
|
23
|
+
model = select_and_check_model(model, preferred_citation)
|
24
|
+
return if model.nil?
|
25
|
+
|
26
|
+
output = []
|
27
|
+
output << combine_authors(
|
28
|
+
model.authors.map { |author| format_author(author) }
|
29
|
+
)
|
30
|
+
|
31
|
+
_, year = month_and_year_from_model(model)
|
32
|
+
output << "(#{year})" unless year.empty?
|
33
|
+
|
34
|
+
version = " (Version #{model.version})" unless model.version.to_s.empty?
|
35
|
+
output << "#{model.title}#{version}#{software_label(model)}"
|
36
|
+
output << publication_data_from_model(model)
|
37
|
+
output << url(model)
|
38
|
+
|
39
|
+
output.reject(&:empty?).join('. ')
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.publication_data_from_model(model)
|
43
|
+
return '' unless model.respond_to?(:journal)
|
44
|
+
|
45
|
+
vol = "#{model.volume}(#{model.issue})" unless model.volume.to_s.empty?
|
46
|
+
|
47
|
+
[model.journal, vol, model.start.to_s].reject(&:empty?).join(', ')
|
48
|
+
end
|
49
|
+
|
50
|
+
# Prefer a DOI over the other URI options.
|
51
|
+
def self.url(model)
|
52
|
+
model.doi.empty? ? super : "https://doi.org/#{model.doi}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.software_label(model)
|
56
|
+
return '' if model.is_a?(Reference) && !model.type.include?('software')
|
57
|
+
|
58
|
+
' [Computer software]'
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.combine_authors(authors)
|
62
|
+
return authors[0].chomp('.') if authors.length == 1
|
63
|
+
|
64
|
+
"#{authors[0..-2].join(', ')}, & #{authors[-1]}".chomp('.')
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.format_author(author)
|
68
|
+
return author.name if author.is_a?(Entity)
|
69
|
+
|
70
|
+
particle =
|
71
|
+
author.name_particle.empty? ? '' : "#{author.name_particle} "
|
72
|
+
suffix = author.name_suffix.empty? ? '.' : "., #{author.name_suffix}"
|
73
|
+
|
74
|
+
"#{particle}#{author.family_names}, #{initials(author.given_names)}#{suffix}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2018-2021 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
|
+
# Generates an BibTex citation string
|
20
|
+
class BibtexFormatter < Formatter # :nodoc:
|
21
|
+
|
22
|
+
def self.format(model:, preferred_citation: true) # rubocop:disable Metrics/AbcSize
|
23
|
+
model = select_and_check_model(model, preferred_citation)
|
24
|
+
return if model.nil?
|
25
|
+
|
26
|
+
values = {}
|
27
|
+
values['author'] = combine_authors(
|
28
|
+
model.authors.map { |author| format_author(author) }
|
29
|
+
)
|
30
|
+
values['title'] = "{#{model.title}}"
|
31
|
+
|
32
|
+
publication_data_from_model(model, values)
|
33
|
+
|
34
|
+
month, year = month_and_year_from_model(model)
|
35
|
+
values['month'] = month
|
36
|
+
values['year'] = year
|
37
|
+
|
38
|
+
values['url'] = url(model)
|
39
|
+
|
40
|
+
values.reject! { |_, v| v.empty? }
|
41
|
+
sorted_values = values.sort.map do |key, value|
|
42
|
+
"#{key} = {#{value}}"
|
43
|
+
end
|
44
|
+
sorted_values.insert(0, generate_reference(values))
|
45
|
+
|
46
|
+
"@#{bibtex_type(model)}{#{sorted_values.join(",\n")}\n}"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Get various bits of information about the reference publication.
|
50
|
+
# Reference: https://www.bibtex.com/format/
|
51
|
+
def self.publication_data_from_model(model, fields)
|
52
|
+
%w[doi journal volume].each do |field|
|
53
|
+
fields[field] = model.send(field).to_s if model.respond_to?(field)
|
54
|
+
end
|
55
|
+
|
56
|
+
# BibTeX 'number' is CFF 'issue'.
|
57
|
+
fields['number'] = model.issue.to_s if model.respond_to?(:issue)
|
58
|
+
|
59
|
+
fields['pages'] = pages_from_model(model)
|
60
|
+
end
|
61
|
+
|
62
|
+
# CFF 'pages' is the number of pages, which has no equivalent in BibTeX.
|
63
|
+
# Reference: https://www.bibtex.com/f/pages-field/
|
64
|
+
def self.pages_from_model(model)
|
65
|
+
return '' if !model.respond_to?(:start) || model.start.to_s.empty?
|
66
|
+
|
67
|
+
start = model.start.to_s
|
68
|
+
finish = model.end.to_s
|
69
|
+
if finish.empty?
|
70
|
+
start
|
71
|
+
else
|
72
|
+
start == finish ? start : "#{start}--#{finish}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Do what we can to map between CFF reference types and bibtex types.
|
77
|
+
# Reference: https://www.bibtex.com/e/entry-types/
|
78
|
+
def self.bibtex_type(model)
|
79
|
+
return 'misc' unless model.is_a?(Reference)
|
80
|
+
|
81
|
+
case model.type
|
82
|
+
when 'article', 'book', 'manual', 'unpublished'
|
83
|
+
model.type
|
84
|
+
when 'conference', 'proceedings'
|
85
|
+
'proceedings'
|
86
|
+
when 'conference-paper'
|
87
|
+
'inproceedings'
|
88
|
+
when 'magazine-article', 'newspaper-article'
|
89
|
+
'article'
|
90
|
+
when 'pamphlet'
|
91
|
+
'booklet'
|
92
|
+
else
|
93
|
+
'misc'
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.format_author(author)
|
98
|
+
return "{#{author.name}}" if author.is_a?(Entity)
|
99
|
+
|
100
|
+
particle =
|
101
|
+
author.name_particle.empty? ? '' : "#{author.name_particle} "
|
102
|
+
|
103
|
+
[
|
104
|
+
"#{particle}#{author.family_names}",
|
105
|
+
author.name_suffix,
|
106
|
+
author.given_names
|
107
|
+
].reject(&:empty?).join(', ')
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.combine_authors(authors)
|
111
|
+
authors.join(' and ')
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.generate_reference(fields)
|
115
|
+
[
|
116
|
+
fields['author'].split(',', 2)[0].tr(' -', '_'),
|
117
|
+
fields['title'].split[0..2],
|
118
|
+
fields['year']
|
119
|
+
].compact.join('_').tr('-$£%&(){}+!?/\\:;\'"~#', '')
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|