cff 0.2.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile 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.
@@ -12,22 +14,25 @@
12
14
  # See the License for the specific language governing permissions and
13
15
  # limitations under the License.
14
16
 
15
- require "bundler/gem_tasks"
16
- require "rake/testtask"
17
- require "rdoc/task"
17
+ require 'bundler/gem_tasks'
18
+ require 'rake/testtask'
19
+ require 'rdoc/task'
20
+ require 'rubocop/rake_task'
18
21
 
19
- task :default => :test
22
+ task default: :test
20
23
 
21
24
  Rake::TestTask.new(:test) do |t|
22
- t.libs << "test"
23
- t.libs << "lib"
24
- t.test_files = FileList["test/**/*_test.rb"]
25
+ t.libs << 'test'
26
+ t.libs << 'lib'
27
+ t.test_files = FileList['test/**/*_test.rb']
25
28
  end
26
29
 
27
30
  RDoc::Task.new do |r|
28
- r.main = "README.md"
29
- r.rdoc_files.include("README.md", "LICENCE", "lib/**/*.rb")
30
- r.options << "--markup=markdown"
31
- r.options << "--tab-width=2"
31
+ r.main = 'README.md'
32
+ r.rdoc_files.include('README.md', 'LICENCE', 'CHANGES.md', 'lib/**/*.rb')
33
+ r.options << '--markup=markdown'
34
+ r.options << '--tab-width=2'
32
35
  r.options << "-t Ruby CFF Library version #{::CFF::VERSION}"
33
36
  end
37
+
38
+ RuboCop::RakeTask.new
data/bin/console CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require "bundler/setup"
4
- require "cff"
4
+ require 'bundler/setup'
5
+ require 'cff'
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +11,5 @@ require "cff"
10
11
  # require "pry"
11
12
  # Pry.start
12
13
 
13
- require "irb"
14
+ require 'irb'
14
15
  IRB.start(__FILE__)
data/cff.gemspec 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.
@@ -12,34 +14,51 @@
12
14
  # See the License for the specific language governing permissions and
13
15
  # limitations under the License.
14
16
 
15
- lib = File.expand_path("../lib", __FILE__)
16
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
17
- require "cff/version"
17
+ require_relative 'lib/cff/version'
18
18
 
19
19
  Gem::Specification.new do |spec|
20
- spec.name = "cff"
20
+ spec.name = 'cff'
21
21
  spec.version = CFF::VERSION
22
- spec.authors = ["Robert Haines"]
23
- spec.email = ["robert.haines@manchester.ac.uk"]
22
+ spec.authors = [
23
+ 'Robert Haines',
24
+ 'The Ruby Citation File Format Developers'
25
+ ]
26
+ spec.email = ['robert.haines@manchester.ac.uk']
27
+
28
+ spec.summary = 'A Ruby library for manipulating CITATION.cff files.'
29
+ spec.description = 'See https://citation-file-format.github.io/ ' \
30
+ 'for more info.'
31
+ spec.homepage = 'https://github.com/citation-file-format/ruby-cff'
32
+ spec.license = 'Apache-2.0'
24
33
 
25
- spec.summary = "A Ruby library for manipulating CITATION.cff files."
26
- spec.description = "See https://citation-file-format.github.io/ for more info."
27
- spec.homepage = "https://github.com/hainesr/ruby-cff"
28
- spec.license = "Apache-2.0"
34
+ spec.metadata = {
35
+ 'bug_tracker_uri' => 'https://github.com/citation-file-format/ruby-cff/issues',
36
+ 'changelog_uri' => 'https://github.com/citation-file-format/ruby-cff/blob/main/CHANGES.md',
37
+ 'documentation_uri' => 'https://citation-file-format.github.io/ruby-cff/',
38
+ 'source_code_uri' => 'https://github.com/citation-file-format/ruby-cff'
39
+ }
29
40
 
30
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
31
- f.match(%r{^(test|spec|features)/})
41
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
42
+ f.match(%r{^((test|spec|features)/|\.)})
32
43
  end
33
- spec.bindir = "exe"
44
+
45
+ spec.bindir = 'exe'
34
46
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
35
- spec.require_paths = ["lib"]
47
+ spec.require_paths = ['lib']
48
+
49
+ spec.required_ruby_version = '>= 2.6'
36
50
 
37
- spec.required_ruby_version = ">= 2.2.0"
51
+ spec.add_runtime_dependency 'json_schema', '~> 0.20.0'
52
+ spec.add_runtime_dependency 'language_list', '~> 1.2'
38
53
 
39
- spec.add_development_dependency "bundler", "~> 1.16"
40
- spec.add_development_dependency "rake", "~> 10.0"
41
- spec.add_development_dependency "minitest", "~> 5.0"
42
- spec.add_development_dependency "coveralls", "~> 0.8"
43
- spec.add_development_dependency "test_construct", "~> 2.0"
44
- spec.add_development_dependency "rdoc", "~> 6.0"
54
+ spec.add_development_dependency 'minitest', '~> 5.14'
55
+ spec.add_development_dependency 'rake', '~> 13.0'
56
+ spec.add_development_dependency 'rdoc', '~> 6.3'
57
+ spec.add_development_dependency 'rubocop', '~> 1.15'
58
+ spec.add_development_dependency 'rubocop-minitest', '~> 0.13'
59
+ spec.add_development_dependency 'rubocop-performance', '~> 1.11.0'
60
+ spec.add_development_dependency 'rubocop-rake', '~> 0.5.0'
61
+ spec.add_development_dependency 'simplecov', '~> 0.20.0'
62
+ spec.add_development_dependency 'simplecov-lcov', '~> 0.8.0'
63
+ spec.add_development_dependency 'test_construct', '~> 2.0'
45
64
  end
data/lib/cff.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.
@@ -12,16 +14,11 @@
12
14
  # See the License for the specific language governing permissions and
13
15
  # limitations under the License.
14
16
 
15
- require "date"
16
- require "yaml"
17
+ require 'date'
18
+ require 'json'
19
+ require 'yaml'
17
20
 
18
- require "cff/version"
19
- require "cff/util"
20
- require "cff/model-part"
21
- require "cff/person"
22
- require "cff/entity"
23
- require "cff/model"
24
- require "cff/file"
21
+ require 'language_list'
25
22
 
26
23
  # This library provides a Ruby interface to manipulate CITATION.cff files. The
27
24
  # primary entry points are Model and File.
@@ -29,5 +26,22 @@ require "cff/file"
29
26
  # See the [CITATION.cff documentation](https://citation-file-format.github.io/)
30
27
  # for more details.
31
28
  module CFF
32
-
29
+ SCHEMA_PATH = ::File.join(__dir__, 'schema', '1.2.0.json') # :nodoc:
30
+ SCHEMA_FILE = JSON.parse(::File.read(SCHEMA_PATH)) # :nodoc:
33
31
  end
32
+
33
+ require 'cff/version'
34
+ require 'cff/errors'
35
+ require 'cff/util'
36
+ require 'cff/licensable'
37
+ require 'cff/validatable'
38
+ require 'cff/model_part'
39
+ require 'cff/person'
40
+ require 'cff/entity'
41
+ require 'cff/identifier'
42
+ require 'cff/reference'
43
+ require 'cff/model'
44
+ require 'cff/file'
45
+ require 'cff/formatter/formatter'
46
+ require 'cff/formatter/apa_formatter'
47
+ require 'cff/formatter/bibtex_formatter'
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.
@@ -12,42 +14,55 @@
12
14
  # See the License for the specific language governing permissions and
13
15
  # limitations under the License.
14
16
 
15
- #
17
+ ##
16
18
  module CFF
17
19
 
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 = [
24
- 'address',
25
- 'city',
26
- 'country',
27
- 'email',
28
- 'date-end',
29
- 'date-start',
30
- 'fax',
31
- 'location',
32
- 'name',
33
- 'orcid',
34
- 'post-code',
35
- 'region',
36
- 'tel',
37
- 'website'
47
+ 'address', 'city', 'country', 'email', 'date-end', 'date-start', 'fax',
48
+ 'location', 'name', 'orcid', 'post-code', 'region', 'tel', 'website'
38
49
  ].freeze # :nodoc:
39
50
 
40
51
  # :call-seq:
41
52
  # new(name) -> Entity
53
+ # new(name) { |entity| block } -> Entity
42
54
  #
43
55
  # Create a new Entity with the supplied name.
44
56
  def initialize(param)
45
- if Hash === param
46
- super(param)
57
+ if param.is_a?(Hash)
58
+ @fields = param
59
+ @fields.default = ''
47
60
  else
48
61
  @fields = Hash.new('')
49
62
  @fields['name'] = param
50
63
  end
64
+
65
+ yield self if block_given?
51
66
  end
52
67
 
53
68
  # :call-seq:
@@ -56,9 +71,7 @@ module CFF
56
71
  # Set the `date-end` field. If a non-Date object is passed in it will
57
72
  # be parsed into a Date.
58
73
  def date_end=(date)
59
- unless Date === date
60
- date = Date.parse(date)
61
- end
74
+ date = Date.parse(date) unless date.is_a?(Date)
62
75
 
63
76
  @fields['date-end'] = date
64
77
  end
@@ -69,12 +82,9 @@ module CFF
69
82
  # Set the `date-start` field. If a non-Date object is passed in it will
70
83
  # be parsed into a Date.
71
84
  def date_start=(date)
72
- unless Date === date
73
- date = Date.parse(date)
74
- end
85
+ date = Date.parse(date) unless date.is_a?(Date)
75
86
 
76
87
  @fields['date-start'] = date
77
88
  end
78
-
79
89
  end
80
90
  end
data/lib/cff/errors.rb ADDED
@@ -0,0 +1,53 @@
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 validation. It contains
29
+ # details of each failure that was detected by the underlying JsonSchema
30
+ # library, which is used to perform the validation.
31
+ #
32
+ # Additionally, the `invalid_filename` flag is used to indicate whether the
33
+ # CFF file is named correctly. This is only used when validating a File;
34
+ # validating a Model directly will not set this flag to `true`.
35
+ class ValidationError < Error
36
+
37
+ # The list of JsonSchema::ValidationErrors found by the validator.
38
+ attr_reader :errors
39
+
40
+ # If a File was validated, was its filename invalid?
41
+ attr_reader :invalid_filename
42
+
43
+ def initialize(errors, invalid_filename: false) # :nodoc:
44
+ super('Validation error')
45
+ @errors = errors
46
+ @invalid_filename = invalid_filename
47
+ end
48
+
49
+ def to_s # :nodoc:
50
+ "#{super}: (Invalid filename: #{@invalid_filename}) #{@errors.join(' ')}"
51
+ end
52
+ end
53
+ 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.
@@ -12,56 +14,245 @@
12
14
  # See the License for the specific language governing permissions and
13
15
  # limitations under the License.
14
16
 
15
- #
17
+ ##
16
18
  module CFF
17
19
 
18
20
  # File provides direct access to a CFF Model, with the addition of some
19
21
  # filesystem utilities.
22
+ #
23
+ # To be a fully compliant and valid CFF file its filename should be
24
+ # 'CITATION.cff'. This class allows you to create files with any filename,
25
+ # and to validate the contents of those files independently of the preferred
26
+ # filename.
20
27
  class File
21
28
 
29
+ # A comment to be inserted at the top of the resultant CFF file.
30
+ attr_reader :comment
31
+
32
+ # The filename of this CFF file.
33
+ attr_reader :filename
34
+
22
35
  YAML_HEADER = "---\n" # :nodoc:
36
+ CFF_COMMENT = [
37
+ "This CITATION.cff file was created by ruby-cff (v #{CFF::VERSION}).",
38
+ 'Gem: https://rubygems.org/gems/cff',
39
+ 'CFF: https://citation-file-format.github.io/'
40
+ ].freeze # :nodoc:
41
+ CFF_VALID_FILENAME = 'CITATION.cff' # :nodoc:
23
42
 
24
43
  # :call-seq:
25
- # new(title) -> File
26
- # new(model) -> File
44
+ # new(filename, title) -> File
45
+ # new(filename, model) -> File
27
46
  #
28
47
  # Create a new File. Either a pre-existing Model can be passed in or, as
29
48
  # with Model itself, a title can be supplied to initalize a new File.
30
49
  #
31
50
  # All methods provided by Model are also available directly on File
32
51
  # objects.
33
- def initialize(param)
34
- unless Model === param
35
- param = Model.new(param)
36
- end
52
+ def initialize(filename, param, comment = CFF_COMMENT, create: false)
53
+ param = Model.new(param) unless param.is_a?(Model)
37
54
 
55
+ @filename = filename
38
56
  @model = param
57
+ @comment = comment
58
+ @dirty = create
39
59
  end
40
60
 
41
61
  # :call-seq:
42
- # read(file) -> File
62
+ # read(filename) -> File
43
63
  #
44
64
  # Read a file and parse it for subsequent manipulation.
45
65
  def self.read(file)
46
- new(YAML::load_file(file))
66
+ content = ::File.read(file)
67
+ comment = File.parse_comment(content)
68
+
69
+ new(
70
+ file, YAML.safe_load(content, permitted_classes: [Date, Time]), comment
71
+ )
47
72
  end
48
73
 
49
74
  # :call-seq:
50
- # write(file, model)
51
- # write(file, yaml)
75
+ # open(filename) -> File
76
+ # open(filename) { |cff| block }
52
77
  #
53
- # Write the supplied model or yaml string to `file`.
54
- def self.write(file, cff)
55
- unless String === cff
56
- cff = cff.to_yaml
78
+ # With no associated block, File.open is a synonym for ::read. If the
79
+ # optional code block is given, it will be passed the opened file as an
80
+ # argument and the File object will automatically be written (if edited)
81
+ # and closed when the block terminates.
82
+ #
83
+ # File.open will create a new file if one does not already exist with the
84
+ # provided file name.
85
+ def self.open(file)
86
+ if ::File.exist?(file)
87
+ content = ::File.read(file)
88
+ comment = File.parse_comment(content)
89
+ yaml = YAML.safe_load(content, permitted_classes: [Date, Time])
90
+ else
91
+ comment = CFF_COMMENT
92
+ yaml = ''
93
+ end
94
+
95
+ cff = new(file, yaml, comment, create: (yaml == ''))
96
+ return cff unless block_given?
97
+
98
+ begin
99
+ yield cff
100
+ ensure
101
+ cff.write
57
102
  end
103
+ end
58
104
 
59
- ::File.write(file, cff[YAML_HEADER.length...-1])
105
+ # :call-seq:
106
+ # validate(filename, fail_on_filename: true) -> Array
107
+ #
108
+ # Read a file and return an array with the result. The result array is a
109
+ # three-element array, with `true`/`false` at index 0 to indicate
110
+ # pass/fail, an array of schema validation errors at index 1 (if any), and
111
+ # `true`/`false` at index 2 to indicate whether the filename passed/failed
112
+ # validation.
113
+ #
114
+ # You can choose whether filename validation failure should cause overall
115
+ # validation failure with the `fail_on_filename` parameter (default: true).
116
+ def self.validate(file, fail_on_filename: true)
117
+ File.read(file).validate(fail_on_filename: fail_on_filename)
118
+ end
119
+
120
+ # :call-seq:
121
+ # validate!(filename, fail_on_filename: true)
122
+ #
123
+ # Read a file and raise a ValidationError upon failure. If an error is
124
+ # raised it will contain the detected validation failures for further
125
+ # inspection.
126
+ #
127
+ # You can choose whether filename validation failure should cause overall
128
+ # validation failure with the `fail_on_filename` parameter (default: true).
129
+ def self.validate!(file, fail_on_filename: true)
130
+ File.read(file).validate!(fail_on_filename: fail_on_filename)
131
+ end
132
+
133
+ # :call-seq:
134
+ # write(filename, File)
135
+ # write(filename, Model)
136
+ # write(filename, yaml)
137
+ #
138
+ # Write the supplied File, Model or yaml string to `file`.
139
+ def self.write(file, cff, comment = '')
140
+ comment = cff.comment if cff.respond_to?(:comment)
141
+ cff = cff.to_yaml unless cff.is_a?(String)
142
+ content = File.format_comment(comment) + cff[YAML_HEADER.length...-1]
143
+
144
+ ::File.write(file, content)
145
+ end
146
+
147
+ # :call-seq:
148
+ # validate(fail_fast: false, fail_on_filename: true) -> Array
149
+ #
150
+ # Validate this file and return an array with the result. The result array
151
+ # is a three-element array, with `true`/`false` at index 0 to indicate
152
+ # pass/fail, an array of schema validation errors at index 1 (if any), and
153
+ # `true`/`false` at index 2 to indicate whether the filename passed/failed
154
+ # validation.
155
+ #
156
+ # You can choose whether filename validation failure should cause overall
157
+ # validation failure with the `fail_on_filename` parameter (default: true).
158
+ def validate(fail_fast: false, fail_on_filename: true)
159
+ valid_filename = (::File.basename(@filename) == CFF_VALID_FILENAME)
160
+ result = (@model.validate(fail_fast: fail_fast) << valid_filename)
161
+ result[0] &&= valid_filename if fail_on_filename
162
+
163
+ result
164
+ end
165
+
166
+ # :call-seq:
167
+ # validate!(fail_fast: false, fail_on_filename: true)
168
+ #
169
+ # Validate this file and raise a ValidationError upon failure. If an error
170
+ # is raised it will contain the detected validation failures for further
171
+ # inspection.
172
+ #
173
+ # You can choose whether filename validation failure should cause overall
174
+ # validation failure with the `fail_on_filename` parameter (default: true).
175
+ def validate!(fail_fast: false, fail_on_filename: true)
176
+ result = validate(
177
+ fail_fast: fail_fast, fail_on_filename: fail_on_filename
178
+ )
179
+ return if result[0]
180
+
181
+ raise ValidationError.new(result[1], invalid_filename: !result[2])
182
+ end
183
+
184
+ # :call-seq:
185
+ # write(save_as: filename)
186
+ #
187
+ # Write this CFF File. The `save_as` parameter can be used to save a new
188
+ # copy of this CFF File under a different filename, leaving the original
189
+ # file untouched. If `save_as` is used then the internal filename of the
190
+ # File will be updated to the supplied filename.
191
+ def write(save_as: nil)
192
+ unless save_as.nil?
193
+ @filename = save_as
194
+ @dirty = true
195
+ end
196
+
197
+ File.write(@filename, @model, @comment) if @dirty
198
+ @dirty = false
199
+ end
200
+
201
+ # :call-seq:
202
+ # comment = string or array
203
+ #
204
+ # A comment to be inserted at the top of the resultant CFF file. This can
205
+ # be supplied as a simple string or an array of strings. When the file is
206
+ # saved this comment is formatted as follows:
207
+ #
208
+ # * a simple string is split into 75 character lines and `'# '` is prepended
209
+ # to each line;
210
+ # * an array of strings is joined into a single string with `'\n'` and
211
+ # `'# '` is prepended to each line;
212
+ #
213
+ # If you care about formatting, use an array of strings for your comment,
214
+ # if not, use a single string.
215
+ def comment=(comment)
216
+ @dirty = true
217
+ @comment = comment
218
+ end
219
+
220
+ def to_yaml # :nodoc:
221
+ @model.to_yaml
60
222
  end
61
223
 
62
224
  def method_missing(name, *args) # :nodoc:
63
- @model.send name, *args
225
+ if @model.respond_to?(name)
226
+ @dirty = true if name.to_s.end_with?('=') # Remove to_s when Ruby >2.6.
227
+ @model.send(name, *args)
228
+ else
229
+ super
230
+ end
231
+ end
232
+
233
+ def respond_to_missing?(name, *all) # :nodoc:
234
+ @model.respond_to?(name, *all)
64
235
  end
65
236
 
237
+ def self.format_comment(comment) # :nodoc:
238
+ return '' if comment.empty?
239
+
240
+ comment = comment.scan(/.{1,75}/) if comment.is_a?(String)
241
+ c = comment.map do |l|
242
+ l.empty? ? '#' : "# #{l}"
243
+ end.join("\n")
244
+
245
+ "#{c}\n\n"
246
+ end
247
+
248
+ def self.parse_comment(content) # :nodoc:
249
+ content = content.split("\n")
250
+
251
+ content.reduce([]) do |acc, line|
252
+ break acc unless line.start_with?('#')
253
+
254
+ acc << line.sub(/^#+/, '').strip
255
+ end
256
+ end
66
257
  end
67
258
  end