codeless_code 0.1.7 → 0.1.8

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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.gitmodules +1 -1
  3. data/.reek.yml +14 -0
  4. data/.rubocop.yml +18 -0
  5. data/.ruby-version +1 -0
  6. data/{test/test_codeless_code.rb → .travis.yml} +12 -10
  7. data/Gemfile +5 -0
  8. data/Gemfile.lock +21 -0
  9. data/Guardfile +13 -1
  10. data/README.md +6 -0
  11. data/Rakefile +26 -11
  12. data/VERSION +1 -1
  13. data/bin/codeless_code +3 -1
  14. data/codeless_code.gemspec +33 -12
  15. data/lib/codeless_code/catalog.rb +3 -1
  16. data/lib/codeless_code/cli.rb +56 -24
  17. data/lib/codeless_code/commands/filter_fables.rb +15 -10
  18. data/lib/codeless_code/commands/list_translations.rb +3 -0
  19. data/lib/codeless_code/fable.rb +42 -53
  20. data/lib/codeless_code/fable_set.rb +8 -6
  21. data/lib/codeless_code/filters/builders.rb +4 -2
  22. data/lib/codeless_code/filters/composite.rb +3 -1
  23. data/lib/codeless_code/filters/date.rb +40 -30
  24. data/lib/codeless_code/filters/from_options.rb +45 -28
  25. data/lib/codeless_code/filters/headers/base.rb +61 -0
  26. data/lib/codeless_code/filters/{header_integer.rb → headers/integer.rb} +16 -19
  27. data/lib/codeless_code/filters/{header_string.rb → headers/string.rb} +12 -22
  28. data/lib/codeless_code/filters/lang.rb +3 -0
  29. data/lib/codeless_code/filters/translator.rb +7 -2
  30. data/lib/codeless_code/filters.rb +16 -8
  31. data/lib/codeless_code/formats/base.rb +20 -7
  32. data/lib/codeless_code/formats/parsers/base.rb +106 -0
  33. data/lib/codeless_code/formats/parsers/plain.rb +49 -0
  34. data/lib/codeless_code/formats/parsers/term.rb +79 -0
  35. data/lib/codeless_code/formats/plain.rb +21 -12
  36. data/lib/codeless_code/formats/raw.rb +2 -0
  37. data/lib/codeless_code/formats/term.rb +21 -15
  38. data/lib/codeless_code/language_set.rb +7 -4
  39. data/lib/codeless_code/options.rb +5 -3
  40. data/lib/codeless_code/renderers/fable.rb +13 -8
  41. data/lib/codeless_code/renderers/term_page.rb +46 -31
  42. data/lib/codeless_code.rb +119 -106
  43. data/test/codeless_code/commands/test_filter_fables.rb +89 -0
  44. data/test/codeless_code/filters/headers/test_integer.rb +86 -0
  45. data/test/codeless_code/filters/headers/test_string.rb +86 -0
  46. data/test/codeless_code/filters/test_builders.rb +4 -2
  47. data/test/codeless_code/filters/test_composite.rb +70 -0
  48. data/test/codeless_code/filters/test_date.rb +99 -0
  49. data/test/codeless_code/filters/test_langs.rb +50 -0
  50. data/test/codeless_code/filters/test_translator.rb +51 -0
  51. data/test/codeless_code/renderers/test_fable.rb +98 -0
  52. data/test/codeless_code/renderers/test_term_page.rb +87 -0
  53. data/test/codeless_code/test_catalog.rb +12 -5
  54. data/test/codeless_code/test_cli.rb +85 -0
  55. data/test/codeless_code/test_fable.rb +19 -10
  56. data/test/codeless_code/test_fable_set.rb +17 -5
  57. data/test/codeless_code/test_language_set.rb +16 -3
  58. data/test/codeless_code/test_options.rb +3 -11
  59. data/test/helper.rb +36 -10
  60. data/test/support/fs.rb +103 -0
  61. metadata +65 -11
  62. data/.document +0 -5
  63. data/data/README.md +0 -34
  64. data/lib/codeless_code/formats/plain_generator.rb +0 -157
  65. data/lib/codeless_code/formats/term_generator.rb +0 -167
  66. data/test/codeless_code/filters/test_header_integer.rb +0 -82
  67. data/test/codeless_code/filters/test_header_string.rb +0 -82
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # codeless_code filters and prints fables from http://thecodelesscode.com
2
4
  # Copyright (C) 2018 Jon Sangster
3
5
  #
@@ -14,15 +16,21 @@
14
16
  # You should have received a copy of the GNU General Public License along with
15
17
  # this program. If not, see <https://www.gnu.org/licenses/>.
16
18
  module CodelessCode
19
+ # Functors which test whether a given {Fable} matches some criteria.
17
20
  module Filters
18
- autoload :Builders, 'codeless_code/filters/builders'
19
- autoload :Composite, 'codeless_code/filters/composite'
20
- autoload :Date, 'codeless_code/filters/date'
21
- autoload :FromOptions, 'codeless_code/filters/from_options'
22
- autoload :HeaderInteger, 'codeless_code/filters/header_integer'
23
- autoload :Lang, 'codeless_code/filters/lang'
24
- autoload :HeaderString, 'codeless_code/filters/header_string'
25
- autoload :Translator, 'codeless_code/filters/translator'
21
+ autoload :Builders, 'codeless_code/filters/builders'
22
+ autoload :Composite, 'codeless_code/filters/composite'
23
+ autoload :Date, 'codeless_code/filters/date'
24
+ autoload :FromOptions, 'codeless_code/filters/from_options'
25
+ autoload :Lang, 'codeless_code/filters/lang'
26
+ autoload :Translator, 'codeless_code/filters/translator'
27
+
28
+ # Filters which inspect a {Fable fable's} headers
29
+ module Headers
30
+ autoload :Base, 'codeless_code/filters/headers/base'
31
+ autoload :Integer, 'codeless_code/filters/headers/integer'
32
+ autoload :String, 'codeless_code/filters/headers/string'
33
+ end
26
34
 
27
35
  extend Builders
28
36
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # codeless_code filters and prints fables from http://thecodelesscode.com
2
4
  # Copyright (C) 2018 Jon Sangster
3
5
  #
@@ -18,8 +20,9 @@ require 'nokogiri'
18
20
 
19
21
  module CodelessCode
20
22
  module Formats
23
+ # Abstract base class for all formats.
21
24
  class Base
22
- attr_accessor :raw
25
+ attr_reader :raw
23
26
 
24
27
  def initialize(raw)
25
28
  @raw = raw
@@ -27,14 +30,24 @@ module CodelessCode
27
30
 
28
31
  protected
29
32
 
30
- def to_xhtml(str)
31
- # MediaCloth expects XHTML-ish pages and chokes on ommited end tags
32
- Nokogiri::HTML(str).css('body > *').to_xhtml
33
+ def from_wiki(doc, parser_name)
34
+ parser = Parsers.const_get(parser_name).new(self)
35
+ MediaCloth.wiki_to_html(doc.to_s, generator: parser)
36
+ end
37
+ end
38
+
39
+ # MediaCloth expects XHTML-ish pages and chokes on ommited end tags
40
+ class XhtmlDoc
41
+ def self.parse(html)
42
+ new(Nokogiri::HTML(html))
43
+ end
44
+
45
+ def initialize(doc)
46
+ @doc = doc
33
47
  end
34
48
 
35
- def from_wiki(str, generator)
36
- return "" if str.length == 0
37
- MediaCloth::wiki_to_html(str, generator: generator)
49
+ def to_s
50
+ @doc.css('body > *').to_xhtml
38
51
  end
39
52
  end
40
53
  end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ # codeless_code filters and prints fables from http://thecodelesscode.com
4
+ # Copyright (C) 2018 Jon Sangster
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify it under
7
+ # the terms of the GNU General Public License as published by the Free Software
8
+ # Foundation, either version 3 of the License, or (at your option) any later
9
+ # version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful, but WITHOUT
12
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14
+ # details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License along with
17
+ # this program. If not, see <https://www.gnu.org/licenses/>.
18
+ require 'mediacloth'
19
+
20
+ module CodelessCode
21
+ module Formats
22
+ module Parsers
23
+ # An abstract base class for the custom parsers for the syntax tree
24
+ # generated by the +MediaCloth+ gem.
25
+ class Base < MediaWikiWalker
26
+ # Reimplement these
27
+ #
28
+ # @example
29
+ # def parse_paragraph(ast)
30
+ # parse_wiki_ast(ast)
31
+ # end
32
+ ABSTRACT_METHODS = %i[
33
+ parse_paragraph
34
+ parse_paste
35
+ parse_formatted
36
+ parse_list_item
37
+ parse_list_term
38
+ parse_list_definition
39
+ parse_preformatted
40
+ parse_section
41
+ parse_link
42
+ parse_internal_link
43
+ parse_internal_link_item
44
+ parse_table
45
+ parse_table_row
46
+ parse_table_cell
47
+ parse_element
48
+ parse_template
49
+ parse_category
50
+ parse_keyword
51
+ ].freeze
52
+
53
+ attr_reader :ctx
54
+
55
+ def initialize(ctx)
56
+ @ctx = ctx
57
+ end
58
+
59
+ protected
60
+
61
+ def parse_wiki_ast(ast)
62
+ super(ast).join
63
+ end
64
+
65
+ ABSTRACT_METHODS.each do |meth|
66
+ define_method(meth) { |ast| parse_wiki_ast(ast) }
67
+ end
68
+
69
+ # Reimplement this
70
+ # :reek:UtilityFunction
71
+ def parse_text(ast)
72
+ ast.contents
73
+ end
74
+
75
+ # Reimplement this
76
+ def parse_list(ast)
77
+ ast.children.map(&method(:parse_list_child))
78
+ end
79
+
80
+ # Reimplement this
81
+ def parse_resource_link(ast)
82
+ ast.children.map do |child|
83
+ parse_internal_link_item(child) if child.is_a?(InternalLinkItemAST)
84
+ end
85
+ end
86
+
87
+ # Reimplement this
88
+ def parse_category_link(ast)
89
+ ast.children.map do |child|
90
+ parse_internal_link_item(child) if child.is_a?(InternalLinkItemAST)
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def parse_list_child(child)
97
+ case child
98
+ when ListItemAST then parse_list_item(child)
99
+ when ListTermAST then parse_list_term(child)
100
+ when ListDefinitionAST then parse_list_definition(child)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # codeless_code filters and prints fables from http://thecodelesscode.com
4
+ # Copyright (C) 2018 Jon Sangster
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify it under
7
+ # the terms of the GNU General Public License as published by the Free Software
8
+ # Foundation, either version 3 of the License, or (at your option) any later
9
+ # version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful, but WITHOUT
12
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14
+ # details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License along with
17
+ # this program. If not, see <https://www.gnu.org/licenses/>.
18
+ require 'mediacloth'
19
+
20
+ module CodelessCode
21
+ module Formats
22
+ module Parsers
23
+ # A custom parser for the syntax tree generated by the +MediaCloth+ gem
24
+ # which removes all formatting such that the rendered document should
25
+ # appear as plain text.
26
+ class Plain < Base
27
+ protected
28
+
29
+ def parse_section(ast)
30
+ parse_wiki_ast(ast).strip
31
+ end
32
+
33
+ def parse_internal_link(ast)
34
+ text = parse_wiki_ast(ast)
35
+ !text.empty? ? text : ast.locator
36
+ end
37
+
38
+ def parse_element(ast)
39
+ str = parse_wiki_ast(ast)
40
+ if ast.name == 'pre'
41
+ ctx.generate(str.gsub(/\A\n*(.*?)\n*\z/m, '\1'))
42
+ else
43
+ str
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ # codeless_code filters and prints fables from http://thecodelesscode.com
4
+ # Copyright (C) 2018 Jon Sangster
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify it under
7
+ # the terms of the GNU General Public License as published by the Free Software
8
+ # Foundation, either version 3 of the License, or (at your option) any later
9
+ # version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful, but WITHOUT
12
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14
+ # details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License along with
17
+ # this program. If not, see <https://www.gnu.org/licenses/>.
18
+ require 'mediacloth'
19
+
20
+ module CodelessCode
21
+ module Formats
22
+ module Parsers
23
+ # A custom parser for the syntax tree generated by the +MediaCloth+ gem
24
+ # which formats a document using ANSI codes for color, bold, italics, and
25
+ # other styles.
26
+ class Term < Base
27
+ protected
28
+
29
+ def parse_formatted(ast)
30
+ if ast.formatting == :Bold
31
+ color(parse_wiki_ast(ast)).bold
32
+ else
33
+ color(parse_wiki_ast(ast)).italic
34
+ end
35
+ end
36
+
37
+ def parse_preformatted(ast)
38
+ color(parse_wiki_ast(ast)).green
39
+ end
40
+
41
+ def parse_section(ast)
42
+ color(parse_wiki_ast(ast).strip).blue
43
+ end
44
+
45
+ def parse_link(ast)
46
+ color(parse_wiki_ast(ast)).underline
47
+ end
48
+
49
+ def parse_internal_link(ast)
50
+ text = parse_wiki_ast(ast)
51
+ color(!text.empty? ? text : ast.locator).underline
52
+ end
53
+
54
+ def parse_element(ast)
55
+ str = parse_wiki_ast(ast)
56
+ if ast.name == 'pre'
57
+ color(ctx.generate(str.gsub(/\A\n*(.*?)\n*\z/m, '\1'))).red
58
+ else
59
+ str
60
+ end
61
+ end
62
+
63
+ def parse_template(ast)
64
+ color(ast.template_name).yellow
65
+ end
66
+
67
+ def parse_keyword(ast)
68
+ color(parse_wiki_ast(ast)).underline
69
+ end
70
+
71
+ private
72
+
73
+ def color(str)
74
+ ctx.color(str)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # codeless_code filters and prints fables from http://thecodelesscode.com
2
4
  # Copyright (C) 2018 Jon Sangster
3
5
  #
@@ -21,24 +23,31 @@ module CodelessCode
21
23
  class Plain < Base
22
24
  def call
23
25
  raw.split("\n\n")
24
- .map { |str| from_wiki(to_xhtml(regex(str))) }
26
+ .map { |str| from_wiki(CleanupBody.new(str)) }
25
27
  .join("\n\n")
26
28
  end
27
29
 
28
- private
30
+ protected
29
31
 
30
- def regex(str)
31
- [
32
- [/\/\/\w*$/, ''],
33
- [/<i>([^<]+)<\/i>/mi, '\1'],
34
- [/<b>([^<]+)<\/b>/mi, '\1'],
35
- [/<a[^>]+>([^<]+)<\/a>/mi, '\1'],
36
- [/\/(\w+)\//, '\1'],
37
- ].inject(str) { |str, args| str = str.gsub(*args) }
32
+ def from_wiki(str)
33
+ super(XhtmlDoc.parse(str.to_s), :Plain)
34
+ end
35
+ end
36
+
37
+ # Tidies up the mixed syntax found in fables
38
+ class CleanupBody
39
+ def initialize(body)
40
+ @body = body
38
41
  end
39
42
 
40
- def from_wiki(str)
41
- super(str, PlainGenerator.new(self))
43
+ def to_s
44
+ [
45
+ [%r{//\w*$}, ''],
46
+ [%r{<i>([^<]+)</i>}mi, '\1'],
47
+ [%r{<b>([^<]+)</b>}mi, '\1'],
48
+ [%r{<a[^>]+>([^<]+)</a>}mi, '\1'],
49
+ [%r{/(\w+)/}, '\1']
50
+ ].inject(@body) { |str, args| str.gsub(*args) }
42
51
  end
43
52
  end
44
53
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # codeless_code filters and prints fables from http://thecodelesscode.com
2
4
  # Copyright (C) 2018 Jon Sangster
3
5
  #
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # codeless_code filters and prints fables from http://thecodelesscode.com
2
4
  # Copyright (C) 2018 Jon Sangster
3
5
  #
@@ -15,7 +17,6 @@
15
17
  # this program. If not, see <https://www.gnu.org/licenses/>.
16
18
  require 'colorized_string'
17
19
  require 'mediacloth'
18
- require 'nokogiri'
19
20
 
20
21
  module CodelessCode
21
22
  module Formats
@@ -23,26 +24,31 @@ module CodelessCode
23
24
  # colors, etc.
24
25
  class Term < Base
25
26
  def call
26
- from_wiki(to_xhtml(regex(raw)))
27
+ from_wiki
27
28
  end
28
29
 
29
- def regex(str)
30
- [
31
- [/\/\/\w*$/, ''],
32
- [/^\| .*/, c('\\0').green],
33
- [/<i>([^<]+)<\/i>/mi, "''\\1''"],
34
- [/<b>([^<]+)<\/b>/mi, "'''\\1'''"],
35
- [/<a[^>]+>([^<]+)<\/a>/mi, '[[\1]]'],
36
- [/\/(\w+)\//, "''\\1''"],
37
- ].inject(str) { |str, args| str = str.gsub(*args) }
30
+ # :reek:UtilityFunction
31
+ def color(str)
32
+ ColorizedString.new(str)
38
33
  end
39
34
 
40
- def from_wiki(str)
41
- super(str, TermGenerator.new(self))
35
+ protected
36
+
37
+ def from_wiki
38
+ super(XhtmlDoc.parse(regex_raw), :Term)
42
39
  end
43
40
 
44
- def c(str)
45
- ColorizedString.new(str)
41
+ private
42
+
43
+ def regex_raw
44
+ [
45
+ [%r{//\w*$}, ''],
46
+ [/^\| .*/, ColorizedString.new('\\0').green],
47
+ [%r{<i>([^<]+)</i>}mi, "''\\1''"],
48
+ [%r{<b>([^<]+)</b>}mi, "'''\\1'''"],
49
+ [%r{<a[^>]+>([^<]+)</a>}mi, '[[\1]]'],
50
+ [%r{/(\w+)/}, "''\\1''"]
51
+ ].inject(raw) { |str, args| str.gsub(*args) }
46
52
  end
47
53
  end
48
54
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # codeless_code filters and prints fables from http://thecodelesscode.com
2
4
  # Copyright (C) 2018 Jon Sangster
3
5
  #
@@ -22,16 +24,17 @@ module CodelessCode
22
24
  extend Forwardable
23
25
  include Enumerable
24
26
 
27
+ # If the requested language set doesn't exist
25
28
  NotFoundError = Class.new(StandardError)
26
29
 
27
- LANG_PATTERN = '%s-*'.freeze
30
+ LANG_PATTERN = '%s-*'
28
31
 
29
- attr_accessor :lang, :root_dir
32
+ attr_reader :lang, :root_dir
30
33
  def_delegator :fable_sets, :each
31
34
 
32
35
  def initialize(lang, root_dir:)
33
- self.lang = lang
34
- self.root_dir = root_dir
36
+ @lang = lang
37
+ @root_dir = root_dir
35
38
  end
36
39
 
37
40
  def fable_sets
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # codeless_code filters and prints fables from http://thecodelesscode.com
2
4
  # Copyright (C) 2018 Jon Sangster
3
5
  #
@@ -62,9 +64,9 @@ module CodelessCode
62
64
  args = [@command_name] + @argv
63
65
  opts = CodelessCode::OPTIONS.curry[@command_name]
64
66
 
65
- Slop.parse(args, suppress_errors: suppress_errors, &opts).tap do |o|
66
- if !suppress_errors && o.arguments.size > MAX_ARGS + 1
67
- raise format('too many arguments: %p', o.arguments[1..-1])
67
+ Slop.parse(args, suppress_errors: suppress_errors, &opts).tap do |opt|
68
+ if !suppress_errors && opt.arguments.size > MAX_ARGS + 1
69
+ raise format('too many arguments: %p', opt.arguments[1..-1])
68
70
  end
69
71
  end
70
72
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # codeless_code filters and prints fables from http://thecodelesscode.com
2
4
  # Copyright (C) 2018 Jon Sangster
3
5
  #
@@ -24,7 +26,7 @@ module CodelessCode
24
26
  page.title = best_title
25
27
  page.body = render_with(format, fallback: fallback)
26
28
 
27
- headers_no_best_title.map { |k, v| page.add_header(k, v) }
29
+ headers_no_best_title.map { |key, val| page.add_header(key, val) }
28
30
  end
29
31
  end
30
32
 
@@ -42,14 +44,17 @@ module CodelessCode
42
44
  private
43
45
 
44
46
  def render_slice(keys)
45
- headers.slice(*keys).map {|k, v| format('%s: %p', k, v) }.join(', ')
47
+ headers.slice(*keys)
48
+ .map { |key, val| format('%s: %p', key, val) }
49
+ .join(', ')
46
50
  end
47
51
 
48
52
  def render_with(format, fallback:)
49
53
  format.new(body).call
50
- rescue => e
51
- raise e unless fallback
52
- warn format('Error parsing %s: %s', file, e.message.strip)
54
+ rescue StandardError => err
55
+ raise err unless fallback
56
+
57
+ warn format('Error parsing %s: %s', file, err.message.strip)
53
58
  fallback.new(body).call
54
59
  end
55
60
 
@@ -58,13 +63,13 @@ module CodelessCode
58
63
  end
59
64
 
60
65
  def headers_no_best_title
61
- sorted_headers.dup.delete_if { |_, v| v&.strip == title&.strip }
66
+ sorted_headers.dup.delete_if { |_, val| val&.strip == title&.strip }
62
67
  end
63
68
 
64
69
  def sorted_headers
65
70
  Hash[
66
- headers.each_with_index.sort_by do |(k,_), i|
67
- HEADER_SORT.index(k) || HEADER_SORT.size + i
71
+ headers.each_with_index.sort_by do |(key, _), index|
72
+ HEADER_SORT.index(key) || HEADER_SORT.size + index
68
73
  end.map(&:first)
69
74
  ]
70
75
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # codeless_code filters and prints fables from http://thecodelesscode.com
2
4
  # Copyright (C) 2018 Jon Sangster
3
5
  #
@@ -24,16 +26,21 @@ module CodelessCode
24
26
  '%<title>s', '%<sep1>s', '%<headers>s', '%<sep2>s', '', '%<body>s'
25
27
  ].join("\n").freeze
26
28
 
29
+ # :reek:Attribute
27
30
  attr_accessor :title, :body
31
+ attr_reader :headers
32
+
33
+ # :reek:ControlParameter
34
+ def initialize(max_width: nil, width_func: nil)
35
+ @max_width = max_width
36
+ @max_width ||= (width_func || TermWidth.new).call
28
37
 
29
- def initialize(max_width: nil)
30
- @max_width = max_width || term_width
31
38
  @headers = {}
32
39
  @key_width = 0
33
40
  end
34
41
 
35
42
  def to_s
36
- format(PAGER_FORMAT, title: title.center(width),
43
+ format(PAGER_FORMAT, title: title.center(width).rstrip,
37
44
  sep1: seperator('='), sep2: seperator('-'),
38
45
  headers: header_section, body: body)
39
46
  end
@@ -45,38 +52,31 @@ module CodelessCode
45
52
 
46
53
  private
47
54
 
48
- def term_width
49
- if (w = %x[tput cols].strip&.to_i)&.positive?
50
- w
51
- end
52
- rescue Errno::ENOENT
53
- nil
54
- end
55
-
56
- def seperator(ch = '=')
57
- (@seperator ||= {})[ch] ||= ch * width
55
+ def seperator(char = '=')
56
+ (@seperator ||= {})[char] ||= char * width
58
57
  end
59
58
 
60
59
  def header_section
61
- lines = @headers.map { |k, v| format_header(k, v, wrap: true) }
62
- max_line_width = lines.join("\n").lines.map { |s| s.size }.max
60
+ lines = format_header_section
61
+ max_line_width = lines.join("\n").lines.map(&:size).max || 0
63
62
  padding = [0, (width - max_line_width)].max / 2
64
63
 
65
- lines.map { |s| [' ' * padding, s].join }.join("\n")
64
+ lines.map { |line| [' ' * padding, line].join }.join("\n")
66
65
  end
67
66
 
68
- def format_header(k, v, wrap: false)
69
- line =
70
- if wrap
71
- v.chars
72
- .each_slice(width - @key_width - 2)
73
- .map { |s| s.join.strip }
74
- .inject { |str, s| str << "\n" << ' ' * (@key_width + 2) << s }
75
- else
76
- v
77
- end
78
-
79
- format("% #{@key_width}s: %s", k, line)
67
+ def format_header_section
68
+ headers.map { |key, value| format_header(key, wrap_header(value)) }
69
+ end
70
+
71
+ def format_header(key, value)
72
+ format("% #{@key_width}s: %s", key, value)
73
+ end
74
+
75
+ def wrap_header(head)
76
+ head.chars
77
+ .each_slice(width - @key_width - 2)
78
+ .map { |str| str.join.strip }
79
+ .inject { |str, part| str + "\n" + ' ' * (@key_width + 2) + part }
80
80
  end
81
81
 
82
82
  def width
@@ -85,12 +85,27 @@ module CodelessCode
85
85
 
86
86
  def content_width
87
87
  [
88
- body.lines.map do |s|
89
- ColorizedString[s].uncolorize.strip.size
88
+ body.lines.map do |line|
89
+ ColorizedString[line].uncolorize.strip.size
90
90
  end.max || 0,
91
- @headers.map(&method(:format_header)).map(&:size).max || 0
91
+ headers.map(&method(:format_header)).map(&:size).max || 0
92
92
  ].max
93
93
  end
94
94
  end
95
+
96
+ # Ask an external application how wide our terminal is
97
+ class TermWidth
98
+ def initialize(cmd = 'tputs cols')
99
+ @cmd = cmd
100
+ end
101
+
102
+ def call
103
+ if (tput_width = `#{@cmd}`.strip.to_i).positive?
104
+ tput_width
105
+ end
106
+ rescue Errno::ENOENT
107
+ nil
108
+ end
109
+ end
95
110
  end
96
111
  end