fled 0.0.2 → 0.0.3

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,9 +1,13 @@
1
1
  module FlEd
2
+ # In memory tree structure for storing
3
+ # a file-system
2
4
  class FileListing
3
5
  def initialize
4
6
  @objects_by_id = {}
5
7
  @objects = []
8
+ @errors = []
6
9
  end
10
+ attr_accessor :errors
7
11
  RELATION_KEYS = [:parent]
8
12
  def dup
9
13
  result = self.class.new
@@ -54,7 +58,7 @@ module FlEd
54
58
  previous_indent = nil
55
59
  previous = nil
56
60
  stack = []
57
- listing.split("\n").each do |line|
61
+ listing.split("\n").each_with_index do |line, line_number|
58
62
  next if line.strip.empty?
59
63
  raise RuntimeError, "Unparsable line #{line.inspect}" unless line =~ /^((?: )*)(.*?)(?::(\d+))?\r?$/
60
64
  indent = $1
@@ -62,7 +66,15 @@ module FlEd
62
66
  if (dir = name[-1..-1] == "/")
63
67
  name = name[0..-2]
64
68
  end
65
- current = objects.add($3, :name => name, :line => "#{$1}#{$2}")
69
+ current = nil
70
+ begin
71
+ current = objects.add($3, :name => name, :line => "#{$1}#{$2}")
72
+ rescue Exception => e
73
+ (objects.errors ||= []) << [:fail,
74
+ :duplicate_uid, {:name => name, :line_number => line_number + 1}
75
+ ]
76
+ end
77
+ next unless current
66
78
  current[:dir] = true if dir
67
79
  next if name.strip == "" # Ignore indent when there is no name - element will be deleted, parent isnt used
68
80
  if previous_indent && previous_indent != indent
@@ -72,6 +84,7 @@ module FlEd
72
84
  stack.pop
73
85
  end
74
86
  end
87
+ current[:line_number] = line_number + 1
75
88
  current[:parent] = stack.last if stack.count > 0
76
89
  previous = current
77
90
  previous_indent = indent
@@ -80,8 +93,21 @@ module FlEd
80
93
  end
81
94
  end
82
95
  class FileListing # Shell operation list builder
96
+ # Generate a list of operations that
97
+ # would transform the `source_listing`
98
+ # into the receiver.
99
+ #
100
+ # Result is an array with each entry of
101
+ # the format:
102
+ #
103
+ # - `[:fail, reason_symbol, target]`
104
+ # - `[:warn, reason_symbol, target]`
105
+ # - `[:mk, [path components]]`
106
+ # - `[:moved, [source path components], [dest path components]]`
107
+ # - `[:renamed, [source path components], new name]`
108
+ # - `[:rm, [path components], source_object]`
83
109
  def operations_from! source_listing
84
- errors = []
110
+ op_errors = []
85
111
  operations = []
86
112
  pending_renames = []
87
113
  running_source = source_listing.dup
@@ -100,7 +126,7 @@ module FlEd
100
126
  source = running_source[target[:uid]]
101
127
  if !(target[:source] = source)
102
128
  target[:error] = true
103
- errors += [[:fail, :no_such_uid, target]]
129
+ op_errors += [[:fail, :no_such_uid, target]]
104
130
  next
105
131
  end
106
132
  next if target[:name] == ""
@@ -148,7 +174,8 @@ module FlEd
148
174
  new_name = op[2]
149
175
  existing_names = running_source.children_of((target[:parent] || {})[:uid]).map { |o| o[:name] }
150
176
  if existing_names.any? { |n| n.casecmp(new_name) == 0 }
151
- errors += [[:warn, :would_overwrite, target, new_name]]
177
+ op_errors += [[:warn, :would_overwrite, target,
178
+ running_source.path_of(target[:parent]).map { |o| o[:name] } + [new_name]]]
152
179
  else
153
180
  operations << [:renamed,
154
181
  running_source.path_of(target).map { |o| o[:name] },
@@ -159,10 +186,11 @@ module FlEd
159
186
  end
160
187
  self.depth_first do |target, path|
161
188
  if target[:name] == ""
162
- operations << [:rm, running_source.path_of(target[:source]).map { |o| o[:name] }, target[:source]]
189
+ operation = target[:source][:dir] ? :rmdir : :rm
190
+ operations << [operation, running_source.path_of(target[:source]).map { |o| o[:name] }, target[:source]]
163
191
  end
164
192
  end
165
- errors + operations
193
+ errors + op_errors + operations
166
194
  end
167
195
  def has_child? parent, child
168
196
  while child = child[:parent]
@@ -207,59 +235,4 @@ module FlEd
207
235
  to_browse.each { |child| breadth_first child, path + [child], &block }
208
236
  end
209
237
  end
210
- class ListingBuilder < DTC::Utils::FileVisitor
211
- attr_accessor :listing
212
- def initialize listing = FileListing.new
213
- @listing = listing
214
- end
215
- def add_object path, dir
216
- uid = next_uid
217
- name = File.basename(path)
218
- object = @listing.add(uid,
219
- :path => path,
220
- :name => File.basename(path),
221
- :parent => @object_stack.last,
222
- :line => "#{" " * self.depth}#{name}#{dir ? "/" : ""}"
223
- )
224
- object[:dir] = true if dir
225
- object
226
- end
227
- def enter_folder dir
228
- depth = self.depth
229
- if self.depth == 0
230
- @object_stack = []
231
- else
232
- @object_stack.push add_object(dir, true)
233
- end
234
- super
235
- end
236
- def visit_file name, full_path
237
- add_object full_path, false
238
- super
239
- end
240
- def leave_folder
241
- @object_stack.pop
242
- super
243
- end
244
- protected
245
- def next_uid
246
- @listing.count
247
- end
248
- def source_path source
249
- res = []
250
- while source
251
- res.unshift target[:name]
252
- target = target[:parent]
253
- end
254
- File.join(res)
255
- end
256
- def target_path target
257
- res = []
258
- while target
259
- res.unshift target[:name]
260
- target = target[:parent]
261
- end
262
- File.join(res)
263
- end
264
- end
265
238
  end
@@ -0,0 +1,43 @@
1
+ module FlEd
2
+ class FileListingBuilder
3
+ attr_accessor :listing
4
+ def initialize listing = FileListing.new
5
+ @listing = listing
6
+ @folders = []
7
+ end
8
+ def add_object path, dir
9
+ uid = next_uid
10
+ name = File.basename(path)
11
+ object = @listing.add(uid,
12
+ :path => path,
13
+ :name => File.basename(path),
14
+ :parent => @object_stack.last,
15
+ :line => "#{" " * depth}#{name}#{dir ? "/" : ""}"
16
+ )
17
+ object[:dir] = true if dir
18
+ object
19
+ end
20
+ def depth ; @folders.count ; end
21
+ def full_path *args ; File.join((@folders || []) + args) ; end
22
+ def enter dir
23
+ if depth.zero?
24
+ @object_stack = []
25
+ else
26
+ @object_stack.push add_object(dir, true)
27
+ end
28
+ @folders << dir
29
+ true
30
+ end
31
+ def add name, full_path
32
+ add_object full_path, false
33
+ end
34
+ def leave
35
+ @object_stack.pop
36
+ @folders.pop
37
+ end
38
+ protected
39
+ def next_uid
40
+ @listing.count
41
+ end
42
+ end
43
+ end
data/lib/fled.rb CHANGED
@@ -3,9 +3,12 @@ require 'shellwords'
3
3
 
4
4
  module FlEd
5
5
  require 'fled/file_listing'
6
+ require 'fled/file_listing_builder'
6
7
 
7
- VERSION = '0.0.2'
8
+ VERSION = '0.0.3'
8
9
 
10
+ # Convert file listing operation list
11
+ # to bash-script friendly script
9
12
  def self.operation_list_to_bash ops
10
13
  ops = ops.map do |op|
11
14
  case op.first
@@ -15,18 +18,28 @@ module FlEd
15
18
  [:mv, File.join(op[1]), File.join(op[2])]
16
19
  when :renamed
17
20
  [:mv, File.join(op[1]), File.join((op[1].empty? ? [] : op[1][0..-2]) + [op[2]])]
18
- when :rm
19
- [op[2][:dir] ? :rmdir : :rm , File.join(op[1])]
21
+ when :rm, :rmdir
22
+ [op.first , File.join(op[1])]
20
23
  else
21
24
  op
22
25
  end
23
26
  end
24
27
  warnings, operations = *ops.partition { |e| e.first == :warn }
28
+ errors, operations = *operations.partition { |e| e.first == :fail }
25
29
  result = []
30
+ unless errors.empty?
31
+ result += ["# Error:"]
32
+ errors.each do |error|
33
+ line = error[2][:line_number]
34
+ result += ["# - line #{line}: #{error[1]}"]
35
+ end
36
+ result += ['', 'exit 1 # There are errors to check first !', '']
37
+ end
26
38
  unless warnings.empty?
27
- result = ["# Warning:"]
39
+ result += ["# Warning:"]
28
40
  warnings.each do |warning|
29
- result += ["# - #{warning[1]}: #{File.join(warning[2][:source][:path], warning[3])}"]
41
+ line = warning[2][:line_number]
42
+ result += ["# - line #{line}: #{warning[1]}: #{File.join(warning[3])}"]
30
43
  end
31
44
  result += ['', 'exit 1 # There are warnings to check first !', '']
32
45
  end
data/tests/helper.rb CHANGED
@@ -2,45 +2,33 @@
2
2
  $:.unshift File.join(File.dirname(__FILE__),'../lib')
3
3
  require 'fled'
4
4
 
5
- class PrintingFileVisitor < DTC::Utils::FileVisitor
6
- def enter_folder dir
7
- depth = self.depth
8
- puts (" " * depth) + (depth > 0 ? File.basename(dir) : dir)
9
- super
10
- end
11
- def visit_file name, full_path
12
- puts (" " * depth) + name
13
- super
14
- end
15
- end
16
-
17
- class TestListingBuilder < FlEd::ListingBuilder
5
+ class TestListingBuilder < FlEd::FileListingBuilder
18
6
  def next_uid
19
7
  @uid
20
8
  end
21
- def enter_folder dir, uid = nil
9
+ def enter dir, uid = nil
22
10
  @uid = uid
23
- super dir
11
+ super dir.to_s
24
12
  end
25
- def visit_file name, uid
13
+ def add name, uid
26
14
  @uid = uid
27
- super name, current_path(name)
15
+ super name, full_path(name.to_s)
28
16
  end
29
17
  end
30
18
 
31
19
  class TestFS
32
20
  def initialize &block
33
- @root = DTC::Utils::DSLDSL::DSLHashWriter.write_static_tree_dsl(&block)
21
+ @root = DTC::Utils::Visitor::DSL::accept(DTC::Utils::Visitor::HashBuilder, &block).root
34
22
  end
35
23
  def receive visitor, root = @root
36
24
  root.each_pair do |name, val|
37
- next if name == :options
25
+ next if name.nil?
38
26
  if val.is_a?(Array)
39
- visitor.visit_file name, val[0]
27
+ visitor.add name, val[0]
40
28
  elsif val.is_a?(Hash)
41
- if visitor.enter_folder(name, val[:options][0])
29
+ if visitor.enter(name, val[nil][0])
42
30
  receive(visitor, val)
43
- visitor.leave_folder
31
+ visitor.leave
44
32
  end
45
33
  else
46
34
  raise RuntimeError, "Unknown value #{val.inspect}"
@@ -49,9 +37,9 @@ class TestFS
49
37
  end
50
38
  def new_builder
51
39
  builder = TestListingBuilder.new
52
- builder.enter_folder("$")
40
+ builder.enter("$")
53
41
  receive builder
54
- builder.leave_folder
42
+ builder.leave
55
43
  builder
56
44
  end
57
45
  def new_listing
@@ -71,8 +59,8 @@ class TestFS
71
59
  [:mv, File.join(op[1]), File.join(op[2])]
72
60
  when :renamed
73
61
  [:mv, File.join(op[1]), File.join((op[1].empty? ? [] : op[1][0..-2]) + [op[2]])]
74
- when :rm
75
- [op[2][:dir] ? :rmdir : :rm , File.join(op[1])]
62
+ when :rm, :rmdir
63
+ [op.first, File.join(op[1])]
76
64
  else
77
65
  op
78
66
  end
data/tests/readme.rb CHANGED
@@ -4,75 +4,124 @@ else
4
4
  require File.join(File.dirname(__FILE__), 'helper')
5
5
  end
6
6
 
7
- class MarkdownDumbWriter
8
- def initialize
9
- @result = []
7
+ class MarkdownDumbLineWriter < DTC::Utils::Text::LineWriter
8
+ def nl ; push_raw "" ; end
9
+ def text *str
10
+ push DTC::Utils::Text.lines_without_indent(split_lines(*str))
10
11
  end
11
- def ensure_clear exception = nil
12
- unless @result.last == ""
13
- @result << ""
14
- else
15
- if exception && @result.length > 1 &&
16
- (@result[-2] || "")[0..exception.length - 1] == exception
17
- @result.pop
18
- end
19
- end
12
+ def para *str
13
+ enter_mode nil
14
+ text *str
20
15
  end
21
- def << str ; @result += str.is_a?(Array) ? str : [str] ; end
22
- def nl ; @result += [""] ; end
23
16
  def title str, depth = 2, *args
24
- ensure_clear "#"
25
- self << "#{"#" * depth} #{str}"
26
- nl
27
- args.each { |a| text a }
17
+ enter_mode :title
18
+ push_raw "#{"#" * depth} #{str}"
19
+ para *args unless args.empty?
28
20
  end
29
- def h1 str, *a ; title str, 1, *a ; end
30
- def h2 str, *a ; title str, 2, *a ; end
31
- def h3 str, *a ; title str, 3, *a ; end
32
- def code str, indent = " "
33
- ensure_clear
34
- self << reindent(str, indent)
35
- nl
36
- end
37
- def reindent str, indent = ""
38
- lines = str.split(/\r?\n/).map
39
- lines.shift if lines.first.empty?
40
- min_spaces = lines.map { |l| l =~ /^( +)/ ? $1.length : nil }.select{ |e| e }.min || 0
41
- lines.map { |l| indent + ((min_spaces == 0 ? l : l[min_spaces..-1]) || "") }
21
+ # h1-5
22
+ 5.times do |i|
23
+ define_method "h#{i + 1}".to_sym do |str, *a|
24
+ title str, i + 1, *a
25
+ end
42
26
  end
43
- def text str ; self << reindent(str, "") ; end
44
- def em str ; text "*#{str}*" ; end
45
- def li str ;
46
- ensure_clear "-"
47
- self << reindent(str, "- ")
48
- nl
27
+ def html *str ;
28
+ enter_mode :html
29
+ push_raw *str
49
30
  end
50
- def show_example fs, example
51
- code example
52
- ops = fs.commands_if_edited_as(example)
53
- text "Generates:"
54
- if ops.empty?
55
- em "No operation"
56
- else
57
- code ops.join("\n")
31
+ def strong str ; para "**#{str}**" ; end
32
+ def em str ; para "*#{str}*" ; end
33
+ ({
34
+ :li => "- ",
35
+ :pre => " ",
36
+ :quote => "> ",
37
+ }).each_pair do |method, indentation|
38
+ define_method method do |*str, &blk|
39
+ enter_mode method
40
+ push_indent(indentation) {
41
+ push_unindented_and_yield str, &blk
42
+ }
58
43
  end
59
44
  end
60
- def result ; @result.join("\n") ; end
45
+ protected
46
+ def enter_mode mode
47
+ nl if lines.last != "" && @mode != mode
48
+ @mode = mode
49
+ end
50
+ def split_lines *lines
51
+ result = lines.flatten.map { |l| DTC::Utils::Text::lines(l) }.flatten
52
+ result.shift while result.first == ""
53
+ result.pop while (result.last || "").strip == ""
54
+ result
55
+ end
56
+ def push_unindented_and_yield lines
57
+ push(DTC::Utils::Text.lines_without_indent(split_lines(lines))) if lines
58
+ yield if block_given?
59
+ end
60
+ end
61
+
62
+ class MarkdownDumbLineWriter
63
+ include DTC::Utils::Visitor::AcceptAsFlatMethodCalls
61
64
  end
62
65
 
63
- class ExampleDSLWriter < DTC::Utils::DSLDSL::DSLArrayWriter
64
- def self.run &blk
65
- visitor = self.new()
66
- visitor.visit_dsl(&blk)
67
- writer = MarkdownDumbWriter.new
68
- visitor.each do |method, *args|
69
- writer.__send__(method, *args)
66
+ class MarkdownAndHTMLVisitor < DTC::Utils::Visitor::Switcher
67
+ def initialize
68
+ @writer = MarkdownDumbLineWriter.new
69
+ super @writer
70
+ end
71
+ def to_s
72
+ @writer.to_s
73
+ end
74
+ protected
75
+ def visitor_for_subtree sym, *args
76
+ if @visitor_stack.count == 1 && sym == :html
77
+ DTC::Utils::Text::HTML::Writer.new
78
+ else
79
+ nil
70
80
  end
71
- writer.result
81
+ end
82
+ def visitor_left_subtree visitor, *args
83
+ add :html, visitor.to_s
72
84
  end
73
85
  end
74
86
 
75
- readme = ExampleDSLWriter.run do
87
+ def readme &blk
88
+ puts DTC::Utils::Visitor::DSL::accept(MarkdownAndHTMLVisitor, &blk).to_s
89
+ end
90
+
91
+ readme do
92
+ def show_example listing, example, operations
93
+ html {
94
+ table {
95
+ tr {
96
+ th { em "Original listing" }
97
+ th { em "Edited listing" }
98
+ }
99
+ tr {
100
+ td { pre listing }
101
+ td { pre(DTC::Utils::Text.lines_without_indent example) }
102
+ }
103
+ tr {
104
+ th(:colspan => 2) { em "Generates the script:" }
105
+ }
106
+ tr {
107
+ td(:colspan => 2) {
108
+ if operations.empty?
109
+ em "No operation"
110
+ else
111
+ ul {
112
+ operations.each { |op| li { code op } }
113
+ }
114
+ end
115
+ }
116
+ }
117
+ }
118
+ }
119
+ end
120
+ def run_example fs, example
121
+ ops = fs.commands_if_edited_as(example)
122
+ show_example fs.new_listing.to_s, example, ops
123
+ end
124
+
76
125
  h1 "FlEd", '`fled` lets you organise your files and folders in your favourite editor'
77
126
 
78
127
  h2 "Introduction", <<-MD
@@ -118,8 +167,8 @@ readme = ExampleDSLWriter.run do
118
167
  ["Edit current folder using options", "fled --load fled.config.yaml"],
119
168
  ["Add options to a command (`mkdir`, `mv`, `rm` or `rmdir`)", "fled | sed 's/^mv/mv -i/'"],
120
169
  ].each do |t, c|
121
- text t
122
- code c
170
+ para t
171
+ pre c
123
172
  end
124
173
 
125
174
  h2 "Listing Format"
@@ -133,29 +182,30 @@ readme = ExampleDSLWriter.run do
133
182
  }
134
183
  end
135
184
 
136
- code fs.new_listing.to_s
185
+ pre fs.new_listing.to_s
137
186
 
138
- text <<-MD
139
- Each line of the listing is in the format *`[indentation]`* *`[name]`* `:`*`[uid]`*
187
+ para <<-MD
188
+ Each line of the listing is in the format `[indentation] name: uid`
140
189
 
141
190
  - The *indentation* must consist of only spaces, and is used to indicate the parent folder
142
191
  - The *name* must not use colons (`:`). If it is cleared, it is assumed the file/folder is to be deleted
192
+ The *name* has a `/` appended if it is a directory.
143
193
  - The *uid* is used by FlEd to recognise the original of the edited line. Do not assume a *uid* does not
144
- change between runs. It is valid only once.
194
+ change between runs. It is valid only for the current run. Spaces before the *uid* are only cosmetic.
145
195
  MD
146
196
 
147
197
  h2 "Operations"
148
198
 
149
199
  h3 "Creating a new folder", 'Add a new line (therefore with no uid):'
150
- show_example fs, <<-EXAMPLE
200
+ run_example fs, <<-EXAMPLE
151
201
  folder/ :0
152
202
  new_folder
153
203
  folder_two/ :2
154
204
  EXAMPLE
155
205
 
156
206
  h3 "Moving"
157
- text 'Change the indentation and/or line order to change the parent of a file or folder:'
158
- show_example fs, <<-EXAMPLE
207
+ para 'Change the indentation and/or line order to change the parent of a file or folder:'
208
+ run_example fs, <<-EXAMPLE
159
209
  folder/ :0
160
210
  folder_two/ :2
161
211
  file_one :1
@@ -164,17 +214,16 @@ readme = ExampleDSLWriter.run do
164
214
  em 'Moving an item below itself or its children is not recommended, as the listing may not be exhaustive'
165
215
 
166
216
  h3 "Renaming"
167
- text 'Edit the name while preserving the uid to rename the item'
168
- show_example fs, <<-EXAMPLE
217
+ para 'Edit the name while preserving the uid to rename the item'
218
+ run_example fs, <<-EXAMPLE
169
219
  folder_renamed/ :0
170
220
  file_one :1
171
221
  folder_two/ :2
172
222
  file_changed :3
173
223
  EXAMPLE
174
- text '*Swapping file names may not work in cases where the generated intermediary file exists but was not included in the listing*'
175
224
 
176
225
  h3 "Deleting", 'Clear a name but leave the uid to delete that item'
177
- show_example fs, <<-EXAMPLE
226
+ run_example fs, <<-EXAMPLE
178
227
  folder_renamed/ :0
179
228
  :1
180
229
  :2
@@ -182,31 +231,29 @@ readme = ExampleDSLWriter.run do
182
231
  EXAMPLE
183
232
 
184
233
  h3 "No-op"
185
- text 'If a line (and all child-lines) is removed from the listing, it will have no operation.'
186
- show_example fs, <<-EXAMPLE
234
+ para 'If a line (and all child-lines) is removed from the listing, it will have no operation.'
235
+ run_example fs, <<-EXAMPLE
187
236
  folder/ :0
188
237
  EXAMPLE
189
- nl
190
- nl
191
- text '*Note that removing a folder without removing its children will move its children:*'
238
+ para '*Note that removing a folder without removing its children will move its children:*'
192
239
 
193
- show_example fs, <<-EXAMPLE
240
+ run_example fs, <<-EXAMPLE
194
241
  folder/ :0
195
242
  file_one :1
196
243
  file_three :3
197
244
  EXAMPLE
198
245
 
199
246
  nl
200
- text "If an indent is forgotten:"
247
+ para "If an indent is forgotten:"
201
248
 
202
- show_example fs, <<-EXAMPLE
249
+ run_example fs, <<-EXAMPLE
203
250
  folder/ :0
204
251
  file_one :1
205
252
  file_three :3
206
253
  EXAMPLE
207
254
 
208
255
  h3 "All together"
209
- show_example fs, <<-EXAMPLE
256
+ run_example fs, <<-EXAMPLE
210
257
  folder_new/ :0
211
258
  new_folder/
212
259
  first :1
@@ -224,13 +271,14 @@ readme = ExampleDSLWriter.run do
224
271
  file_two(2)
225
272
  }
226
273
  end
227
- code fs.new_listing.to_s
228
- text "When applying"
229
- show_example fs, <<-EXAMPLE
274
+ pre fs.new_listing.to_s
275
+ para "When applying"
276
+ run_example fs, <<-EXAMPLE
230
277
  folder/ :0
231
278
  file_two :1
232
279
  file_one :2
233
280
  EXAMPLE
281
+ para '*Swapping file names may not work in cases where the generated intermediary file exists but was not included in the listing*'
234
282
 
235
283
  h3 "Tree swapping"
236
284
  fs = TestFS.new do
@@ -242,9 +290,9 @@ readme = ExampleDSLWriter.run do
242
290
  }
243
291
  }
244
292
  end
245
- code fs.new_listing.to_s
246
- text "When applying"
247
- show_example fs, <<-EXAMPLE
293
+ pre fs.new_listing.to_s
294
+ para "When applying"
295
+ run_example fs, <<-EXAMPLE
248
296
  sub_sub_folder/ :2
249
297
  sub_folder/ :1
250
298
  folder/ :0
@@ -260,9 +308,16 @@ readme = ExampleDSLWriter.run do
260
308
  'Fix: Moving files under files now moves up to parent folder of destination',
261
309
  'Meta: Travis-CI integration',
262
310
  ],
311
+ 'v0.0.3' => [
312
+ 'New: Interactive mode with `-u`',
313
+ 'New: Error and warning reporting with line numbers',
314
+ 'New: Default configuration file at `~/.fled.yaml`',
315
+ 'New: Editor and diff tool are configurable from configuration files',
316
+ 'Meta: Refactoring of code',
317
+ ],
263
318
  }).sort { |a, b| b[0] <=> a[0] }.each do |version, changes|
264
319
  em "Version #{version}"
265
- changes.each { |change| li change }
320
+ li *changes
266
321
  end
267
322
 
268
323
  h2 "Disclaimer", <<-MD
@@ -276,4 +331,3 @@ readme = ExampleDSLWriter.run do
276
331
  h2 "Licence", "[GPLv3](http://www.gnu.org/licenses/gpl-3.0.html)"
277
332
 
278
333
  end
279
- puts readme