howzit 2.1.21 → 2.1.23

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6cac91a8b188ff256fa863ad94e134e820ed2aa2eb766a10fea38ee745de750
4
- data.tar.gz: 90799282be7a348b595df19a5c09850db8ac4d653bd81063d49f1830e6cb0761
3
+ metadata.gz: 6fc52d90b2a71d349a14e77f6e313baed51003a43ce2ca615a31f0b070f0681e
4
+ data.tar.gz: fa0fa63742822ef396b45abc56b6ae7dfbc7acb72a4c896a9e6570cc9e13b1c3
5
5
  SHA512:
6
- metadata.gz: 6b98ee5eb923878803f0268080ad1d56f4f9aed7279e6dcb245f13659e3a947348212741b3409861aacff145bc5abb19af7dd9810a854626e6b5067fc33ad954
7
- data.tar.gz: 9be180170d36f0f415e3f0977b5b951368f7498fe630cd4f900626bf852c5678b860cea8ac63472dec0c03c41531cf70eb58a5002298a5ed9f5661204aa6d7cb
6
+ metadata.gz: d7e01306310519e4454960b5246740572a7731aa4946fa3d4d455ee3f9a3c30b1415eabdf91a58dca48d0123a0b78332754d7e4d86bd112692574b517a328072
7
+ data.tar.gz: 99ab41b0612da8c5a0e456c5a5006e9dc6cf5e679726b461cba170622d3aa615fe6a259c50dec67b2b4c7151afff200af8d8cf2c3dfe1d775b0dea83e3975fba
data/CHANGELOG.md CHANGED
@@ -1,3 +1,35 @@
1
+ ### 2.1.23
2
+
3
+ 2025-12-13 06:38
4
+
5
+ #### CHANGED
6
+
7
+ - Output format changed from bordered text to markdown table
8
+
9
+ #### NEW
10
+
11
+ - Added emoji header () to run report table
12
+
13
+ #### IMPROVED
14
+
15
+ - Status indicators now use emoji (/) instead of text symbols
16
+ - Failure messages now show "(exit code X)" instead of "Failed: exit code X"
17
+
18
+ ### 2.1.22
19
+
20
+ 2025-12-13 06:14
21
+
22
+ #### NEW
23
+
24
+ - Template selection menu when creating new build notes
25
+ - Prompt for required template variables during note creation
26
+ - Gum support as fallback for menus and text input
27
+
28
+ #### IMPROVED
29
+
30
+ - Fuzzy matching for template names when fzf unavailable
31
+ - Text input uses Readline for proper line editing (backspace, ctrl-a/e)
32
+
1
33
  ### 2.1.21
2
34
 
3
35
  2025-12-13 05:03
@@ -293,27 +293,37 @@ module Howzit
293
293
  if default
294
294
  input = title
295
295
  else
296
- # title = prompt.ask("{bw}Project name:{x}".c, default: title)
297
- printf "{bw}Project name {xg}[#{title}]{bw}: {x}".c
298
- input = $stdin.gets.chomp
299
- title = input unless input.empty?
296
+ title = Prompt.get_line('{bw}Project name{x}'.c, default: title)
300
297
  end
301
298
  summary = ''
302
299
  unless default
303
- printf '{bw}Project summary: {x}'.c
304
- input = $stdin.gets.chomp
305
- summary = input unless input.empty?
300
+ summary = Prompt.get_line('{bw}Project summary{x}'.c)
301
+ end
302
+
303
+ # Template selection
304
+ selected_templates = []
305
+ template_metadata = {}
306
+ unless default
307
+ selected_templates, template_metadata = select_templates_for_note(title)
306
308
  end
307
309
 
308
310
  fname = 'buildnotes.md'
309
311
  unless default
310
- printf "{bw}Build notes filename (must begin with 'howzit' or 'build')\n{xg}[#{fname}]{bw}: {x}".c
311
- input = $stdin.gets.chomp
312
- fname = input unless input.empty?
312
+ fname = Prompt.get_line("{bw}Build notes filename{x}\n(must begin with 'howzit' or 'build')".c, default: fname)
313
+ end
314
+
315
+ # Build metadata section
316
+ metadata_lines = []
317
+ unless selected_templates.empty?
318
+ metadata_lines << "template: #{selected_templates.join(',')}"
319
+ end
320
+ template_metadata.each do |key, value|
321
+ metadata_lines << "#{key}: #{value}"
313
322
  end
323
+ metadata_section = metadata_lines.empty? ? '' : "#{metadata_lines.join("\n")}\n\n"
314
324
 
315
325
  note = <<~EOBUILDNOTES
316
- # #{title}
326
+ #{metadata_section}# #{title}
317
327
 
318
328
  #{summary}
319
329
 
@@ -369,6 +379,68 @@ module Howzit
369
379
 
370
380
  private
371
381
 
382
+ ##
383
+ ## Select templates for a new build note
384
+ ##
385
+ ## @param project_title [String] The project title for prompts
386
+ ##
387
+ ## @return [Array<Array, Hash>] Array of [selected_template_names, required_vars_hash]
388
+ ##
389
+ def select_templates_for_note(project_title)
390
+ template_dir = Howzit.config.template_folder
391
+ template_glob = File.join(template_dir, '*.md')
392
+ template_files = Dir.glob(template_glob)
393
+
394
+ return [[], {}] if template_files.empty?
395
+
396
+ # Get basenames without extension for menu
397
+ template_names = template_files.map { |f| File.basename(f, '.md') }.sort
398
+
399
+ # Show multi-select menu
400
+ selected = Prompt.choose_templates(template_names, prompt_text: 'Select templates to include')
401
+ return [[], {}] if selected.empty?
402
+
403
+ # Prompt for required variables from each template
404
+ required_vars = {}
405
+ selected.each do |template_name|
406
+ template_path = File.join(template_dir, "#{template_name}.md")
407
+ next unless File.exist?(template_path)
408
+
409
+ vars = parse_template_required_vars(template_path)
410
+ vars.each do |var|
411
+ next if required_vars.key?(var)
412
+
413
+ value = Prompt.get_line("{bw}[#{template_name}] requires {by}#{var}{x}".c)
414
+ required_vars[var] = value unless value.empty?
415
+ end
416
+ end
417
+
418
+ [selected, required_vars]
419
+ end
420
+
421
+ ##
422
+ ## Parse a template file for required variables
423
+ ##
424
+ ## @param template_path [String] Path to the template file
425
+ ##
426
+ ## @return [Array] Array of required variable names
427
+ ##
428
+ def parse_template_required_vars(template_path)
429
+ content = File.read(template_path)
430
+
431
+ # Look for required: in the metadata at the top of the file
432
+ # Metadata is before the first # heading
433
+ meta_section = content.split(/^#/)[0]
434
+ return [] if meta_section.nil? || meta_section.strip.empty?
435
+
436
+ # Find the required: line
437
+ match = meta_section.match(/^required:\s*(.+)$/i)
438
+ return [] unless match
439
+
440
+ # Split by comma and strip whitespace
441
+ match[1].split(',').map(&:strip).reject(&:empty?)
442
+ end
443
+
372
444
  def topic_search_terms_from_cli
373
445
  args = Howzit.cli_args || []
374
446
  raw = args.join(' ').strip
data/lib/howzit/prompt.rb CHANGED
@@ -100,6 +100,10 @@ module Howzit
100
100
  return fzf_result(res)
101
101
  end
102
102
 
103
+ if Util.command_exist?('gum')
104
+ return gum_choose(matches, query: query, multi: true)
105
+ end
106
+
103
107
  tty_menu(matches, query: query)
104
108
  end
105
109
 
@@ -199,6 +203,190 @@ module Howzit
199
203
  end
200
204
  line == '' ? 1 : line.to_i
201
205
  end
206
+
207
+ ##
208
+ ## Multi-select menu for templates
209
+ ##
210
+ ## @param matches [Array] The options list
211
+ ## @param prompt_text [String] The prompt to display
212
+ ##
213
+ ## @return [Array] the selected results (can be empty)
214
+ ##
215
+ def choose_templates(matches, prompt_text: 'Select templates')
216
+ return [] if matches.count.zero?
217
+ return [] unless $stdout.isatty
218
+
219
+ if Util.command_exist?('fzf')
220
+ height = matches.count + 3
221
+ settings = fzf_template_options(height, prompt_text: prompt_text)
222
+
223
+ # Save terminal state before fzf
224
+ tty_state = `stty -g`.chomp
225
+ res = `echo #{Shellwords.escape(matches.join("\n"))} | fzf #{settings.join(' ')}`.strip
226
+ # Restore terminal state after fzf
227
+ system("stty #{tty_state}")
228
+
229
+ return res.empty? ? [] : res.split(/\n/)
230
+ end
231
+
232
+ if Util.command_exist?('gum')
233
+ return gum_choose(matches, prompt: prompt_text, multi: true, required: false)
234
+ end
235
+
236
+ text_template_input(matches)
237
+ end
238
+
239
+ ##
240
+ ## FZF options for template selection
241
+ ##
242
+ def fzf_template_options(height, prompt_text: 'Select templates')
243
+ [
244
+ '-0',
245
+ '-m',
246
+ "--height=#{height}",
247
+ '--header="Tab: add selection, ctrl-a/d: (de)select all, esc: skip, return: confirm"',
248
+ '--bind ctrl-a:select-all,ctrl-d:deselect-all,ctrl-t:toggle-all',
249
+ "--prompt=\"#{prompt_text} > \""
250
+ ]
251
+ end
252
+
253
+ ##
254
+ ## Text-based template input with fuzzy matching
255
+ ##
256
+ ## @param available [Array] Available template names
257
+ ##
258
+ ## @return [Array] Matched template names
259
+ ##
260
+ def text_template_input(available)
261
+ @stty_save = `stty -g`.chomp
262
+
263
+ trap('INT') do
264
+ system('stty', @stty_save)
265
+ exit
266
+ end
267
+
268
+ puts "\n{bw}Available templates:{x} #{available.join(', ')}".c
269
+ printf '{bw}Enter templates to include, comma-separated (return to skip):{x} '.c
270
+ input = Readline.readline('', true).strip
271
+
272
+ return [] if input.empty?
273
+
274
+ fuzzy_match_templates(input, available)
275
+ ensure
276
+ system('stty', @stty_save) if @stty_save
277
+ end
278
+
279
+ ##
280
+ ## Fuzzy match user input against available templates
281
+ ##
282
+ ## @param input [String] Comma-separated user input
283
+ ## @param available [Array] Available template names
284
+ ##
285
+ ## @return [Array] Matched template names
286
+ ##
287
+ def fuzzy_match_templates(input, available)
288
+ terms = input.split(',').map(&:strip).reject(&:empty?)
289
+ matched = []
290
+
291
+ terms.each do |term|
292
+ # Try exact match first (case-insensitive)
293
+ exact = available.find { |t| t.downcase == term.downcase }
294
+ if exact
295
+ matched << exact unless matched.include?(exact)
296
+ next
297
+ end
298
+
299
+ # Try fuzzy match using the same regex approach as topic matching
300
+ rx = term.to_rx
301
+ fuzzy = available.select { |t| t =~ rx }
302
+
303
+ # Prefer matches that start with the term
304
+ if fuzzy.length > 1
305
+ starts_with = fuzzy.select { |t| t.downcase.start_with?(term.downcase) }
306
+ fuzzy = starts_with unless starts_with.empty?
307
+ end
308
+
309
+ fuzzy.each { |t| matched << t unless matched.include?(t) }
310
+ end
311
+
312
+ matched
313
+ end
314
+
315
+ ##
316
+ ## Prompt for a single line of input
317
+ ##
318
+ ## @param prompt_text [String] The prompt to display
319
+ ## @param default [String] Default value if empty
320
+ ##
321
+ ## @return [String] the entered value
322
+ ##
323
+ def get_line(prompt_text, default: nil)
324
+ return (default || '') unless $stdout.isatty
325
+
326
+ if Util.command_exist?('gum')
327
+ result = gum_input(prompt_text, placeholder: default || '')
328
+ return result.empty? && default ? default : result
329
+ end
330
+
331
+ prompt_with_default = default ? "#{prompt_text} [#{default}]: " : "#{prompt_text}: "
332
+ result = Readline.readline(prompt_with_default, true).to_s.strip
333
+ result.empty? && default ? default : result
334
+ end
335
+
336
+ ##
337
+ ## Use gum for single or multi-select menu
338
+ ##
339
+ ## @param matches [Array] The options list
340
+ ## @param prompt [String] The prompt text
341
+ ## @param multi [Boolean] Allow multiple selections
342
+ ## @param required [Boolean] Require at least one selection
343
+ ## @param query [String] The search term for display
344
+ ##
345
+ ## @return [Array] Selected items
346
+ ##
347
+ def gum_choose(matches, prompt: nil, multi: false, required: true, query: nil)
348
+ prompt_text = prompt || (query ? "Select for '#{query}'" : 'Select')
349
+ args = ['gum', 'choose']
350
+ args << '--no-limit' if multi
351
+ args << "--header=#{Shellwords.escape(prompt_text)}"
352
+ args << '--cursor.foreground=6'
353
+ args << '--selected.foreground=2'
354
+
355
+ tty_state = `stty -g`.chomp
356
+ res = `echo #{Shellwords.escape(matches.join("\n"))} | #{args.join(' ')}`.strip
357
+ system("stty #{tty_state}")
358
+
359
+ if res.empty?
360
+ if required
361
+ Howzit.console.info 'Cancelled'
362
+ Process.exit 0
363
+ end
364
+ return []
365
+ end
366
+
367
+ res.split(/\n/)
368
+ end
369
+
370
+ ##
371
+ ## Use gum for text input
372
+ ##
373
+ ## @param prompt_text [String] The prompt to display
374
+ ## @param placeholder [String] Placeholder text
375
+ ##
376
+ ## @return [String] The entered value
377
+ ##
378
+ def gum_input(prompt_text, placeholder: '')
379
+ args = ['gum', 'input']
380
+ args << "--header=#{Shellwords.escape(prompt_text)}"
381
+ args << "--placeholder=#{Shellwords.escape(placeholder)}" unless placeholder.empty?
382
+ args << '--cursor.foreground=6'
383
+
384
+ tty_state = `stty -g`.chomp
385
+ res = `#{args.join(' ')}`.strip
386
+ system("stty #{tty_state}")
387
+
388
+ res
389
+ end
202
390
  end
203
391
  end
204
392
  end
@@ -21,31 +21,58 @@ module Howzit
21
21
  def format
22
22
  return '' if entries.empty?
23
23
 
24
- lines = entries.map { |entry| format_line(entry, Howzit.multi_topic_run) }
25
- lines.map! { |line| line.rstrip }
26
- widths = lines.map { |line| line.uncolor.length }
27
- width = widths.max
28
- top = '=' * width
29
- bottom = '-' * width
30
- output_lines = [top] + lines + [bottom]
31
- result = output_lines.join("\n")
32
- result = result.gsub(/\n[ \t]+\n/, "\n")
33
- result.gsub(/\n{2,}/, "\n")
24
+ rows = entries.map { |entry| format_row(entry, Howzit.multi_topic_run) }
25
+
26
+ # Status column: emoji + 1 space on each side = 3 chars wide visually
27
+ # But emojis are 2-width in terminal, so we need width of 4 for " ✅ "
28
+ status_width = 4
29
+ task_width = [4, rows.map { |r| r[:task_plain].length }.max].max
30
+
31
+ # Build the table with emoji header
32
+ header = "| 🚥 | #{'Task'.ljust(task_width)} |"
33
+ separator = "| #{':' + '-' * 2 + ':'} | #{':' + '-' * (task_width - 2)} |"
34
+
35
+ table_lines = [header, separator]
36
+ rows.each do |row|
37
+ table_lines << table_row_colored(row[:status], row[:task], row[:task_plain], status_width, task_width)
38
+ end
39
+
40
+ table_lines.join("\n")
41
+ end
42
+
43
+ def table_row_colored(status, task, task_plain, status_width, task_width)
44
+ task_padding = task_width - task_plain.length
45
+
46
+ "| #{status} | #{task}#{' ' * task_padding} |"
34
47
  end
35
48
 
36
- def format_line(entry, prefix_topic)
37
- bullet_start = '{mb}- [{x}'
38
- bullet_end = '{mb}] {x}'
39
- symbol = entry[:success] ? '{bg}✓{x}' : '{br}X{x}'
40
- parts = []
41
- parts << "#{bullet_start}#{symbol}#{bullet_end}"
42
- parts << "{bl}#{entry[:topic]}{x}: " if prefix_topic && entry[:topic] && !entry[:topic].empty?
43
- parts << "{by}#{entry[:task]}{x}"
49
+ def format_row(entry, prefix_topic)
50
+ symbol = entry[:success] ? '✅' : '❌'
51
+ symbol_colored = entry[:success] ? '{bg}✅{x}'.c : '{br}❌{x}'.c
52
+
53
+ task_parts = []
54
+ task_parts_plain = []
55
+
56
+ if prefix_topic && entry[:topic] && !entry[:topic].empty?
57
+ task_parts << "{bw}#{entry[:topic]}{x}: "
58
+ task_parts_plain << "#{entry[:topic]}: "
59
+ end
60
+
61
+ task_parts << "{by}#{entry[:task]}{x}"
62
+ task_parts_plain << entry[:task]
63
+
44
64
  unless entry[:success]
45
65
  reason = entry[:exit_status] ? "exit code #{entry[:exit_status]}" : 'failed'
46
- parts << " {br}(Failed: #{reason}){x}"
66
+ task_parts << " {br}(#{reason}){x}"
67
+ task_parts_plain << " (#{reason})"
47
68
  end
48
- parts.join.c
69
+
70
+ {
71
+ status: symbol_colored,
72
+ status_plain: symbol,
73
+ task: task_parts.join.c,
74
+ task_plain: task_parts_plain.join
75
+ }
49
76
  end
50
77
  end
51
78
  end
@@ -3,5 +3,5 @@
3
3
  # Primary module for this gem.
4
4
  module Howzit
5
5
  # Current Howzit version.
6
- VERSION = '2.1.21'
6
+ VERSION = '2.1.23'
7
7
  end
@@ -134,4 +134,106 @@ describe Howzit::BuildNote do
134
134
  expect(how.send(:topic_search_terms_from_cli)).to eq(['release, deploy', 'topic balogna'])
135
135
  end
136
136
  end
137
+
138
+ describe "#collect_topic_matches" do
139
+ before do
140
+ Howzit.options[:multiple_matches] = :first
141
+ end
142
+
143
+ it "collects matches for multiple search terms" do
144
+ search_terms = ['topic tropic', 'topic banana']
145
+ output = []
146
+ matches = how.send(:collect_topic_matches, search_terms, output)
147
+ expect(matches.count).to eq 2
148
+ expect(matches.map(&:title)).to include('Topic Tropic', 'Topic Banana')
149
+ end
150
+
151
+ it "prefers exact matches over fuzzy matches" do
152
+ # 'Topic Banana' should exact-match, not fuzzy match to multiple
153
+ search_terms = ['topic banana']
154
+ output = []
155
+ matches = how.send(:collect_topic_matches, search_terms, output)
156
+ expect(matches.count).to eq 1
157
+ expect(matches[0].title).to eq 'Topic Banana'
158
+ end
159
+
160
+ it "falls back to fuzzy match when no exact match" do
161
+ Howzit.options[:matching] = 'fuzzy'
162
+ search_terms = ['trpc'] # fuzzy for 'tropic'
163
+ output = []
164
+ matches = how.send(:collect_topic_matches, search_terms, output)
165
+ expect(matches.count).to eq 1
166
+ expect(matches[0].title).to eq 'Topic Tropic'
167
+ end
168
+
169
+ it "adds error message for unmatched terms" do
170
+ search_terms = ['nonexistent topic xyz']
171
+ output = []
172
+ matches = how.send(:collect_topic_matches, search_terms, output)
173
+ expect(matches.count).to eq 0
174
+ expect(output.join).to match(/no topic match found/i)
175
+ end
176
+
177
+ it "collects multiple topics from comma-separated input" do
178
+ Howzit.cli_args = ['topic tropic,topic banana']
179
+ search_terms = how.send(:topic_search_terms_from_cli)
180
+ output = []
181
+ matches = how.send(:collect_topic_matches, search_terms, output)
182
+ expect(matches.count).to eq 2
183
+ Howzit.cli_args = []
184
+ end
185
+ end
186
+
187
+ describe "#smart_split_topics" do
188
+ it "splits on comma when not part of topic title" do
189
+ result = how.send(:smart_split_topics, 'topic tropic,topic banana')
190
+ expect(result).to eq(['topic tropic', 'topic banana'])
191
+ end
192
+
193
+ it "preserves comma when part of topic title" do
194
+ result = how.send(:smart_split_topics, 'release, deploy,topic banana')
195
+ expect(result).to eq(['release, deploy', 'topic banana'])
196
+ end
197
+
198
+ it "preserves colon when part of topic title" do
199
+ result = how.send(:smart_split_topics, 'git:clean,blog:update post')
200
+ expect(result).to eq(['git:clean', 'blog:update post'])
201
+ end
202
+
203
+ it "handles mixed separators correctly" do
204
+ result = how.send(:smart_split_topics, 'git:clean:topic tropic')
205
+ expect(result).to eq(['git:clean', 'topic tropic'])
206
+ end
207
+ end
208
+
209
+ describe "#parse_template_required_vars" do
210
+ let(:template_with_required) do
211
+ Tempfile.new(['template', '.md']).tap do |f|
212
+ f.write("required: repo_url, author\n\n# Template\n\n## Section")
213
+ f.close
214
+ end
215
+ end
216
+
217
+ let(:template_without_required) do
218
+ Tempfile.new(['template', '.md']).tap do |f|
219
+ f.write("# Template\n\n## Section")
220
+ f.close
221
+ end
222
+ end
223
+
224
+ after do
225
+ template_with_required.unlink
226
+ template_without_required.unlink
227
+ end
228
+
229
+ it "parses required variables from template metadata" do
230
+ vars = how.send(:parse_template_required_vars, template_with_required.path)
231
+ expect(vars).to eq(['repo_url', 'author'])
232
+ end
233
+
234
+ it "returns empty array when no required metadata" do
235
+ vars = how.send(:parse_template_required_vars, template_without_required.path)
236
+ expect(vars).to eq([])
237
+ end
238
+ end
137
239
  end
@@ -13,14 +13,14 @@ describe Howzit::RunReport do
13
13
  Howzit.multi_topic_run = false
14
14
  end
15
15
 
16
- it 'renders a bordered summary for single topic runs' do
16
+ it 'renders a markdown table for single topic runs' do
17
17
  Howzit::RunReport.log({ topic: 'Git: Config', task: 'Run Git Origin', success: true, exit_status: 0 })
18
18
  plain = Howzit::RunReport.format.uncolor
19
- expect(plain).to include('- [✓] Run Git Origin')
19
+ expect(plain).to include('| 🚥 |')
20
+ expect(plain).to include('| Task')
21
+ expect(plain).to include('✅')
22
+ expect(plain).to include('Run Git Origin')
20
23
  expect(plain).not_to include('Git: Config:')
21
- top, line, bottom = plain.split("\n")
22
- expect(top).to eq('=' * line.length)
23
- expect(bottom).to eq('-' * line.length)
24
24
  end
25
25
 
26
26
  it 'prefixes topic titles and shows failures when multiple topics run' do
@@ -28,8 +28,25 @@ describe Howzit::RunReport do
28
28
  Howzit::RunReport.log({ topic: 'Git: Config', task: 'Run Git Origin', success: true, exit_status: 0 })
29
29
  Howzit::RunReport.log({ topic: 'Git: Clean Repo', task: 'Clean Git Repo', success: false, exit_status: 12 })
30
30
  plain = Howzit::RunReport.format.uncolor
31
- expect(plain).to include('- [✓] Git: Config: Run Git Origin')
32
- expect(plain).to include('- [X] Git: Clean Repo: Clean Git Repo (Failed: exit code 12)')
31
+ expect(plain).to include('')
32
+ expect(plain).to include('Git: Config: Run Git Origin')
33
+ expect(plain).to include('❌')
34
+ expect(plain).to include('Git: Clean Repo: Clean Git Repo')
35
+ expect(plain).to include('exit code 12')
36
+ end
37
+
38
+ it 'formats as a proper markdown table with aligned columns' do
39
+ Howzit::RunReport.log({ topic: 'Test', task: 'Short', success: true, exit_status: 0 })
40
+ Howzit::RunReport.log({ topic: 'Test', task: 'A much longer task name', success: true, exit_status: 0 })
41
+ plain = Howzit::RunReport.format.uncolor
42
+ lines = plain.split("\n")
43
+ # All lines should start and end with pipe
44
+ lines.each do |line|
45
+ expect(line).to start_with('|')
46
+ expect(line).to end_with('|')
47
+ end
48
+ # Second line should be separator
49
+ expect(lines[1]).to match(/^\|[\s:-]+\|[\s:-]+\|$/)
33
50
  end
34
51
  end
35
52
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: howzit
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.21
4
+ version: 2.1.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra