cff 0.4.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/cff/entity.rb CHANGED
@@ -1,4 +1,6 @@
1
- # Copyright (c) 2018 Robert Haines.
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
- # Copyright (c) 2018 Robert Haines.
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
- YAML_HEADER = "---\n".freeze # :nodoc:
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
- new(YAML.load_file(file))
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
- ::File.write(file, cff[YAML_HEADER.length...-1])
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
- # write(file)
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
- # Write this CFF File to `file`.
62
- def write(file)
63
- File.write(file, @model)
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) ? @model.send(name, *args) : super
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