cff 0.8.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.
@@ -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,14 +14,13 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require_relative 'schema'
18
+
17
19
  ##
18
20
  module CFF
19
-
20
21
  # Functionality to add licence(s) to parts of the CFF model.
21
22
  module Licensable
22
-
23
- # :nodoc:
24
- LICENSES = SCHEMA_FILE['definitions']['license-enum']['enum'].dup.freeze
23
+ LICENSES = SCHEMA_FILE['definitions']['license-enum']['enum'].dup.freeze # :nodoc:
25
24
 
26
25
  # :call-seq:
27
26
  # license = license
@@ -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,18 +14,16 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require 'date'
18
+
17
19
  ##
18
20
  module CFF
19
-
20
- # ModelPart is the superclass of anything that makes up part of the CFF Model.
21
- # This includes Model, Person, Entity and Reference.
21
+ # ModelPart is the superclass of anything that makes up part of the CFF Index.
22
+ # This includes Index, Person, Entity and Reference.
22
23
  #
23
- # ModelPart does not provide any methods or fields for the public API.
24
+ # ModelPart provides only one method for the public API: `empty?`.
24
25
  class ModelPart
25
-
26
26
  # :stopdoc:
27
- include Util
28
-
29
27
  attr_reader :fields
30
28
 
31
29
  def method_missing(name, *args)
@@ -35,7 +33,7 @@ module CFF
35
33
  if n.end_with?('=')
36
34
  @fields[n.chomp('=')] = args[0] || ''
37
35
  else
38
- @fields[n]
36
+ @fields[n].nil? ? '' : @fields[n]
39
37
  end
40
38
  end
41
39
 
@@ -43,7 +41,45 @@ module CFF
43
41
  n = method_to_field(name.id2name)
44
42
  self.class::ALLOWED_FIELDS.include?(n.chomp('=')) || super
45
43
  end
46
-
47
44
  # :startdoc:
45
+
46
+ # :call-seq:
47
+ # empty? -> false
48
+ #
49
+ # Define `empty?` for CFF classes so that they can be tested in the
50
+ # same way as strings and arrays.
51
+ #
52
+ # This always returns `false` because CFF classes always return something
53
+ # from all of their methods.
54
+ def empty?
55
+ false
56
+ end
57
+
58
+ def self.attr_date(*symbols) # :nodoc:
59
+ symbols.each do |symbol|
60
+ field = symbol.to_s.tr('_', '-')
61
+
62
+ class_eval(
63
+ # def date_end=(date)
64
+ # date = (date.is_a?(Date) ? date.dup : Date.parse(date))
65
+ #
66
+ # @fields['date-end'] = date
67
+ # end
68
+ <<-END_SETTER, __FILE__, __LINE__ + 1
69
+ def #{symbol}=(date)
70
+ date = (date.is_a?(Date) ? date.dup : Date.parse(date))
71
+
72
+ @fields['#{field}'] = date
73
+ end
74
+ END_SETTER
75
+ )
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def method_to_field(name)
82
+ name.tr('_', '-')
83
+ end
48
84
  end
49
85
  end
data/lib/cff/person.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,9 +14,11 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require_relative 'model_part'
18
+ require_relative 'schema'
19
+
17
20
  ##
18
21
  module CFF
19
-
20
22
  # A Person represents a person in a CITATION.cff file. A Person might have a
21
23
  # number of roles, such as author, contact, editor, etc.
22
24
  #
@@ -43,12 +45,7 @@ module CFF
43
45
  # * `tel`
44
46
  # * `website`
45
47
  class Person < ModelPart
46
-
47
- ALLOWED_FIELDS = [
48
- 'address', 'affiliation', 'alias', 'city', 'country', 'email',
49
- 'family-names', 'fax', 'given-names', 'name-particle', 'name-suffix',
50
- 'orcid', 'post-code', 'region', 'tel', 'website'
51
- ].freeze # :nodoc:
48
+ ALLOWED_FIELDS = SCHEMA_FILE['definitions']['person']['properties'].keys.freeze # :nodoc:
52
49
 
53
50
  # :call-seq:
54
51
  # new -> Person
@@ -58,11 +55,12 @@ module CFF
58
55
  #
59
56
  # Create a new Person with the optionally supplied given and family names.
60
57
  def initialize(param = nil, *more)
58
+ super()
59
+
61
60
  if param.is_a?(Hash)
62
61
  @fields = param
63
- @fields.default = ''
64
62
  else
65
- @fields = Hash.new('')
63
+ @fields = {}
66
64
 
67
65
  unless param.nil?
68
66
  @fields['family-names'] = more[0]
data/lib/cff/reference.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,9 +14,13 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require_relative 'licensable'
18
+ require_relative 'model_part'
19
+ require_relative 'schema'
20
+ require_relative 'util'
21
+
17
22
  ##
18
23
  module CFF
19
-
20
24
  # Reference provides a reference pertaining to the software version or the
21
25
  # software itself, e.g., a software paper describing the abstract concepts of
22
26
  # the software, a paper describing an algorithm that has been implemented in
@@ -74,12 +78,13 @@ module CFF
74
78
  # * `pmcid`
75
79
  # * `publisher`
76
80
  # * `repository`
77
- # * `repository_code`
78
81
  # * `repository_artifact`
82
+ # * `repository_code`
79
83
  # * `scope`
80
84
  # * `section`
81
85
  # * `start`
82
86
  # * `status` - *Note:* see documentation for `status =` below
87
+ # * `term`
83
88
  # * `thesis_type`
84
89
  # * `title`
85
90
  # * `type` - *Note:* see documentation for `type =` below
@@ -90,44 +95,24 @@ module CFF
90
95
  # * `year`
91
96
  # * `year_original`
92
97
  class Reference < ModelPart
93
-
94
98
  include Licensable
95
99
 
96
- ALLOWED_FIELDS = [
97
- 'abbreviation', 'abstract', 'authors', 'collection-doi',
98
- 'collection-title', 'collection-type', 'commit', 'conference', 'contact',
99
- 'copyright', 'data-type', 'database', 'database-provider',
100
- 'date-accessed', 'date-downloaded', 'date-published', 'date-released',
101
- 'department', 'doi', 'edition', 'editors', 'editors-series', 'end',
102
- 'entry', 'filename', 'identifiers', 'institution', 'isbn', 'issn',
103
- 'issue', 'issue-date', 'issue-title', 'journal', 'keywords', 'license',
104
- 'license-url', 'loc-end', 'loc-start', 'location', 'medium', 'month',
105
- 'nihmsid', 'notes', 'number', 'number-volumes', 'pages', 'patent-states',
106
- 'pmcid', 'publisher', 'recipients', 'repository', 'repository-code',
107
- 'repository-artifact', 'scope', 'section', 'senders', 'start', 'status',
108
- 'thesis-type', 'title', 'translators', 'type', 'url', 'version',
109
- 'volume', 'volume-title', 'year', 'year-original'
110
- ].freeze # :nodoc:
111
-
112
- # The [defined set of reference types](https://github.com/citation-file-format/citation-file-format#reference-types).
113
- REFERENCE_TYPES = [
114
- 'art', 'article', 'audiovisual', 'bill', 'blog', 'book', 'catalogue',
115
- 'conference', 'conference-paper', 'data', 'database', 'dictionary',
116
- 'edited-work', 'encyclopedia', 'film-broadcast', 'generic',
117
- 'government-document', 'grant', 'hearing', 'historical-work',
118
- 'legal-case', 'legal-rule', 'magazine-article', 'manual', 'map',
119
- 'multimedia', 'music', 'newspaper-article', 'pamphlet', 'patent',
120
- 'personal-communication', 'proceedings', 'report', 'serial', 'slides',
121
- 'software', 'software-code', 'software-container', 'software-executable',
122
- 'software-virtual-machine', 'sound-recording', 'standard', 'statute',
123
- 'thesis', 'unpublished', 'video', 'website'
124
- ].freeze
125
-
126
- # The [defined set of reference status types](https://github.com/citation-file-format/citation-file-format#status-strings).
127
- REFERENCE_STATUS_TYPES = [
128
- 'abstract', 'advance-online', 'in-preparation', 'in-press',
129
- 'pre-print', 'submitted'
130
- ].freeze
100
+ # This list does not include `format` for reasons explained below, where
101
+ # the `format` method is defined!
102
+ ALLOWED_FIELDS = (
103
+ SCHEMA_FILE['definitions']['reference']['properties'].keys - %w[format languages]
104
+ ).freeze # :nodoc:
105
+
106
+ # The [defined set of reference types](https://github.com/citation-file-format/citation-file-format/blob/main/schema-guide.md#definitionsreferencetype).
107
+ REFERENCE_TYPES =
108
+ SCHEMA_FILE['definitions']['reference']['properties']['type']['enum'].dup.freeze
109
+
110
+ # The [defined set of reference status types](https://github.com/citation-file-format/citation-file-format/blob/main/schema-guide.md#definitionsreferencestatus).
111
+ REFERENCE_STATUS_TYPES =
112
+ SCHEMA_FILE['definitions']['reference']['properties']['status']['enum'].dup.freeze
113
+
114
+ attr_date :date_accessed, :date_downloaded, :date_published,
115
+ :date_released, :issue_date
131
116
 
132
117
  # :call-seq:
133
118
  # new(title) -> Reference
@@ -137,29 +122,53 @@ module CFF
137
122
  #
138
123
  # Create a new Reference with the supplied title and, optionally, type.
139
124
  # If type is not given, or is not one of the
140
- # [defined set of reference types](https://github.com/citation-file-format/citation-file-format#reference-types),
125
+ # [defined set of reference types](https://github.com/citation-file-format/citation-file-format/blob/main/schema-guide.md#definitionsreferencetype),
141
126
  # 'generic' will be used by default.
142
- def initialize(param, *more) # rubocop:disable Metrics/AbcSize
127
+ def initialize(param, *more) # rubocop:disable Metrics
128
+ super()
129
+
143
130
  if param.is_a?(Hash)
144
131
  @fields = build_model(param)
145
- @fields.default = ''
146
132
  else
147
- @fields = Hash.new('')
133
+ @fields = {}
148
134
  type = more[0] &&= more[0].downcase
149
135
  @fields['type'] = REFERENCE_TYPES.include?(type) ? type : 'generic'
150
136
  @fields['title'] = param
151
137
  end
152
138
 
153
- [
154
- 'authors', 'contact', 'editors', 'editors-series', 'identifiers',
155
- 'keywords', 'patent-states', 'recipients', 'senders', 'translators'
139
+ %w[
140
+ authors contact editors editors-series identifiers
141
+ keywords patent-states recipients senders translators
156
142
  ].each do |field|
157
- @fields[field] = [] if @fields[field].empty?
143
+ @fields[field] = [] if @fields[field].nil? || @fields[field].empty?
158
144
  end
159
145
 
160
146
  yield self if block_given?
161
147
  end
162
148
 
149
+ # :call-seq:
150
+ # from_cff(File, type: 'software') -> Reference
151
+ # from_cff(Index, type: 'software') -> Reference
152
+ #
153
+ # Create a Reference from another CFF File or Index. This is useful for
154
+ # easily adding a reference to something with its own CITATION.cff file
155
+ # already.
156
+ #
157
+ # This method assumes that the type of the Reference should be `software`,
158
+ # but this can be overridden with the `type` parameter.
159
+ def self.from_cff(model, type: 'software')
160
+ new(model.title, type) do |ref|
161
+ %w[
162
+ abstract authors contact commit date_released doi
163
+ identifiers keywords license license_url repository
164
+ repository_artifact repository_code url version
165
+ ].each do |field|
166
+ value = model.send(field)
167
+ ref.send("#{field}=", value.dup) unless value == ''
168
+ end
169
+ end
170
+ end
171
+
163
172
  # :call-seq:
164
173
  # add_language language
165
174
  #
@@ -167,12 +176,13 @@ module CFF
167
176
  # three letter language code, so `GER` becomes `deu`, `french` becomes
168
177
  # `fra` and `en` becomes `eng`.
169
178
  def add_language(lang)
170
- @fields['languages'] = [] if @fields['languages'].empty?
179
+ require 'language_list'
180
+ @fields['languages'] = [] if @fields['languages'].nil? || @fields['languages'].empty?
171
181
  lang = LanguageList::LanguageInfo.find(lang)
172
182
  return if lang.nil?
173
183
 
174
184
  lang = lang.iso_639_3
175
- @fields['languages'] << lang unless @fields['languages'].include? lang
185
+ @fields['languages'] << lang unless @fields['languages'].include?(lang)
176
186
  end
177
187
 
178
188
  # :call-seq:
@@ -188,51 +198,7 @@ module CFF
188
198
  #
189
199
  # Return the list of languages associated with this Reference.
190
200
  def languages
191
- @fields['languages'].empty? ? [] : @fields['languages'].dup
192
- end
193
-
194
- # :call-seq:
195
- # date_accessed = date
196
- #
197
- # Set the `date-accessed` field. If a non-Date object is passed in it will
198
- # be parsed into a Date.
199
- def date_accessed=(date)
200
- date = Date.parse(date) unless date.is_a?(Date)
201
-
202
- @fields['date-accessed'] = date
203
- end
204
-
205
- # :call-seq:
206
- # date_downloaded = date
207
- #
208
- # Set the `date-downloaded` field. If a non-Date object is passed in it will
209
- # be parsed into a Date.
210
- def date_downloaded=(date)
211
- date = Date.parse(date) unless date.is_a?(Date)
212
-
213
- @fields['date-downloaded'] = date
214
- end
215
-
216
- # :call-seq:
217
- # date_published = date
218
- #
219
- # Set the `date-published` field. If a non-Date object is passed in it will
220
- # be parsed into a Date.
221
- def date_published=(date)
222
- date = Date.parse(date) unless date.is_a?(Date)
223
-
224
- @fields['date-published'] = date
225
- end
226
-
227
- # :call-seq:
228
- # date_released = date
229
- #
230
- # Set the `date-released` field. If a non-Date object is passed in it will
231
- # be parsed into a Date.
232
- def date_released=(date)
233
- date = Date.parse(date) unless date.is_a?(Date)
234
-
235
- @fields['date-released'] = date
201
+ @fields['languages'].nil? || @fields['languages'].empty? ? [] : @fields['languages'].dup
236
202
  end
237
203
 
238
204
  # Returns the format of this Reference.
@@ -240,7 +206,7 @@ module CFF
240
206
  # This method is explicitly defined to override the private format method
241
207
  # that all objects seem to have.
242
208
  def format # :nodoc:
243
- @fields['format']
209
+ @fields['format'].nil? ? '' : @fields['format']
244
210
  end
245
211
 
246
212
  # Sets the format of this Reference.
@@ -255,7 +221,7 @@ module CFF
255
221
  # status = status
256
222
  #
257
223
  # Sets the status of this Reference. The status is restricted to a
258
- # [defined set of status types](https://github.com/citation-file-format/citation-file-format#status-strings).
224
+ # [defined set of status types](https://github.com/citation-file-format/citation-file-format/blob/main/schema-guide.md#definitionsreferencestatus).
259
225
  def status=(status)
260
226
  status = status.downcase
261
227
  @fields['status'] = status if REFERENCE_STATUS_TYPES.include?(status)
@@ -265,7 +231,7 @@ module CFF
265
231
  # type = type
266
232
  #
267
233
  # Sets the type of this Reference. The type is restricted to a
268
- # [defined set of reference types](https://github.com/citation-file-format/citation-file-format#reference-types).
234
+ # [defined set of reference types](https://github.com/citation-file-format/citation-file-format/blob/main/schema-guide.md#definitionsreferencetype).
269
235
  def type=(type)
270
236
  type = type.downcase
271
237
  @fields['type'] = type if REFERENCE_TYPES.include?(type)
@@ -273,29 +239,27 @@ module CFF
273
239
 
274
240
  # Override superclass #fields as References contain model parts too.
275
241
  def fields # :nodoc:
276
- [
277
- 'authors', 'contact', 'editors', 'editors-series', 'identifiers',
278
- 'recipients', 'senders', 'translators'
242
+ %w[
243
+ authors contact editors editors-series identifiers
244
+ recipients senders translators
279
245
  ].each do |field|
280
- normalize_modelpart_array!(@fields[field])
246
+ Util.normalize_modelpart_array!(@fields[field])
281
247
  end
282
248
 
283
- fields_to_hash(@fields)
249
+ Util.fields_to_hash(@fields)
284
250
  end
285
251
 
286
252
  private
287
253
 
288
254
  def build_model(fields) # :nodoc:
289
- [
290
- 'authors', 'contact', 'editors', 'editors-series', 'recipients',
291
- 'senders', 'translators'
255
+ %w[
256
+ authors contact editors editors-series recipients senders translators
292
257
  ].each do |field|
293
- build_actor_collection!(fields[field]) if fields.include?(field)
258
+ Util.build_actor_collection!(fields[field]) if fields.include?(field)
294
259
  end
295
260
 
296
- [
297
- 'conference', 'database-provider', 'institution', 'location',
298
- 'publisher'
261
+ %w[
262
+ conference database-provider institution location publisher
299
263
  ].each do |field|
300
264
  fields[field] &&= Entity.new(fields[field])
301
265
  end
@@ -413,7 +377,7 @@ module CFF
413
377
  # the list, use:
414
378
  #
415
379
  # ```
416
- # model.identifiers << identifier
380
+ # reference.identifiers << identifier
417
381
  # ```
418
382
 
419
383
  ##
@@ -432,7 +396,7 @@ module CFF
432
396
  # list, use:
433
397
  #
434
398
  # ```
435
- # model.keywords << keyword
399
+ # reference.keywords << keyword
436
400
  # ```
437
401
  #
438
402
  # Keywords will be converted to Strings on output.
@@ -455,7 +419,7 @@ module CFF
455
419
  # state to the list, use:
456
420
  #
457
421
  # ```
458
- # model.patent_states << patent_state
422
+ # reference.patent_states << patent_state
459
423
  # ```
460
424
  #
461
425
  # Patent states will be converted to Strings on output.
data/lib/cff/schema.rb ADDED
@@ -0,0 +1,23 @@
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 'json'
18
+
19
+ ##
20
+ module CFF
21
+ SCHEMA_PATH = ::File.join(__dir__, 'schemas', '1.2.0.json') # :nodoc:
22
+ SCHEMA_FILE = JSON.parse(::File.read(SCHEMA_PATH)) # :nodoc:
23
+ end
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://citation-file-format.github.io/1.2.0/schema",
2
+ "$id": "https://citation-file-format.github.io/1.2.0/schema.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema",
4
4
  "additionalProperties": false,
5
5
  "definitions": {
data/lib/cff/util.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,17 +14,22 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require_relative 'entity'
18
+ require_relative 'person'
19
+ require_relative 'version'
20
+
17
21
  require 'rubygems'
18
22
 
19
23
  ##
20
24
  module CFF
21
-
22
25
  # Util provides utility methods useful throughout the rest of the CFF library.
23
26
  #
24
27
  # Util does not provide any methods or fields for the public API.
25
28
  module Util
26
29
  # :stopdoc:
27
30
 
31
+ module_function
32
+
28
33
  def update_cff_version(version)
29
34
  return '' if version.nil? || version.empty?
30
35
 
@@ -35,10 +40,6 @@ module CFF
35
40
  end
36
41
  end
37
42
 
38
- def method_to_field(name)
39
- name.tr('_', '-')
40
- end
41
-
42
43
  def build_actor_collection!(source)
43
44
  source.map! do |s|
44
45
  s.has_key?('given-names') ? Person.new(s) : Entity.new(s)
@@ -67,6 +68,62 @@ module CFF
67
68
  hash
68
69
  end
69
70
 
71
+ DEFAULT_CHAR_APPROXIMATIONS = {
72
+ 'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A',
73
+ 'Æ' => 'AE', 'Ç' => 'C', 'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E',
74
+ 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I', 'Ð' => 'D', 'Ñ' => 'N',
75
+ 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', '×' => 'x',
76
+ 'Ø' => 'O', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'Ý' => 'Y',
77
+ 'Þ' => 'Th', 'ß' => 'ss', 'à' => 'a', 'á' => 'a', 'â' => 'a',
78
+ 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'æ' => 'ae', 'ç' => 'c', 'è' => 'e',
79
+ 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i',
80
+ 'ï' => 'i', 'ð' => 'd', 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o',
81
+ 'õ' => 'o', 'ö' => 'o', 'ø' => 'o', 'ù' => 'u', 'ú' => 'u', 'û' => 'u',
82
+ 'ü' => 'u', 'ý' => 'y', 'þ' => 'th', 'ÿ' => 'y', 'Ā' => 'A', 'ā' => 'a',
83
+ 'Ă' => 'A', 'ă' => 'a', 'Ą' => 'A', 'ą' => 'a', 'Ć' => 'C', 'ć' => 'c',
84
+ 'Ĉ' => 'C', 'ĉ' => 'c', 'Ċ' => 'C', 'ċ' => 'c', 'Č' => 'C', 'č' => 'c',
85
+ 'Ď' => 'D', 'ď' => 'd', 'Đ' => 'D', 'đ' => 'd', 'Ē' => 'E', 'ē' => 'e',
86
+ 'Ĕ' => 'E', 'ĕ' => 'e', 'Ė' => 'E', 'ė' => 'e', 'Ę' => 'E', 'ę' => 'e',
87
+ 'Ě' => 'E', 'ě' => 'e', 'ệ' => 'e', 'Ĝ' => 'G', 'ĝ' => 'g', 'Ğ' => 'G',
88
+ 'ğ' => 'g', 'Ġ' => 'G', 'ġ' => 'g', 'Ģ' => 'G', 'ģ' => 'g', 'Ĥ' => 'H',
89
+ 'ĥ' => 'h', 'Ħ' => 'H', 'ħ' => 'h', 'Ĩ' => 'I', 'ĩ' => 'i', 'Ī' => 'I',
90
+ 'ī' => 'i', 'Ĭ' => 'I', 'ĭ' => 'i', 'Į' => 'I', 'į' => 'i', 'İ' => 'I',
91
+ 'ı' => 'i', 'IJ' => 'IJ', 'ij' => 'ij', 'Ĵ' => 'J', 'ĵ' => 'j',
92
+ 'Ķ' => 'K', 'ķ' => 'k', 'ĸ' => 'k', 'Ĺ' => 'L', 'ĺ' => 'l', 'Ļ' => 'L',
93
+ 'ļ' => 'l', 'Ľ' => 'L', 'ľ' => 'l', 'Ŀ' => 'L', 'ŀ' => 'l', 'Ł' => 'L',
94
+ 'ł' => 'l', 'Ń' => 'N', 'ń' => 'n', 'Ņ' => 'N', 'ņ' => 'n', 'Ň' => 'N',
95
+ 'ň' => 'n', 'ʼn' => "'n", 'Ŋ' => 'NG', 'ŋ' => 'ng', 'Ō' => 'O',
96
+ 'ō' => 'o', 'Ŏ' => 'O', 'ŏ' => 'o', 'Ő' => 'O', 'ő' => 'o', 'Œ' => 'OE',
97
+ 'œ' => 'oe', 'Ŕ' => 'R', 'ŕ' => 'r', 'Ŗ' => 'R', 'ŗ' => 'r', 'Ř' => 'R',
98
+ 'ř' => 'r', 'Ś' => 'S', 'ś' => 's', 'Ŝ' => 'S', 'ŝ' => 's', 'Ş' => 'S',
99
+ 'ş' => 's', 'Š' => 'S', 'š' => 's', 'Ţ' => 'T', 'ţ' => 't', 'Ť' => 'T',
100
+ 'ť' => 't', 'Ŧ' => 'T', 'ŧ' => 't', 'Ũ' => 'U', 'ũ' => 'u', 'Ū' => 'U',
101
+ 'ū' => 'u', 'Ŭ' => 'U', 'ŭ' => 'u', 'Ů' => 'U', 'ů' => 'u', 'Ű' => 'U',
102
+ 'ű' => 'u', 'Ų' => 'U', 'ų' => 'u', 'Ŵ' => 'W', 'ŵ' => 'w', 'Ŷ' => 'Y',
103
+ 'ŷ' => 'y', 'Ÿ' => 'Y', 'Ź' => 'Z', 'ź' => 'z', 'Ż' => 'Z', 'ż' => 'z',
104
+ 'Ž' => 'Z', 'ž' => 'z'
105
+ }.freeze
106
+
107
+ def transliterate(string, fallback: '')
108
+ string.gsub(/[^\x00-\x7f]/u) do |char|
109
+ DEFAULT_CHAR_APPROXIMATIONS[char] || fallback
110
+ end
111
+ end
112
+
113
+ def parameterize(string, separator: '_')
114
+ # Normalize into ASCII.
115
+ param = transliterate(string)
116
+
117
+ # Remove unwanted chars by turning them into the separator.
118
+ param.gsub!(/[^a-z0-9\-_]+/i, separator)
119
+
120
+ # Only one separator at a time.
121
+ param.gsub!(/#{separator}{2,}/, separator)
122
+
123
+ # No leading/trailing separators.
124
+ param.gsub(/^#{separator}|#{separator}$/i, '')
125
+ end
126
+
70
127
  # :startdoc:
71
128
  end
72
129
  end