codeless_code 0.1.5 → 0.1.6

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: 9db33a7d878ec62fe6764ac8c19e36c54e79b27bd6a5a27c74c2ddf3ac339105
4
- data.tar.gz: 02c2096353f43e96970ab65b9f38de50f0f27cb3c4e018cb4beae317241c29eb
3
+ metadata.gz: cd5da6b3ee3fd39beace9072dc41a2247da1ed39f27ff050a3cbedc89ff18754
4
+ data.tar.gz: cdc34c935081bbbcbab8be9165997483adfb0a4bdc8c7b1bc69b5050fb940669
5
5
  SHA512:
6
- metadata.gz: 11f14612021cd8c058a288094f8da15aebc7468406eae664c2f9a92a79f444b2fda2fd2b9e608b7ba57b38c29ef0a64a6c1c7e418369753874ceeb4489423b9e
7
- data.tar.gz: 6c1c83e013f53e93494a92b18a0c2348eeda737d095b4598dd5dde37ca32a2b44b4f04da2e7a8099726801c186f3fbe460564ce93778ed2873fecfac06098e55
6
+ metadata.gz: c1b00ec239abfdee2b0cc4fb81355e5336ddace39dd71bf924946e4f147c4ba33503c0ab9c603e393a54b2dda09e14e455ec7f39f3920db8365dee99ef469eea
7
+ data.tar.gz: 3d2e674fe69b5185acbc12e7de1549e56b9c364429751197445cc853744e7c4d3f386e283e0cda31fc31a5d19dd62c506a13357beed869d44dfd3d34c4308b8d
data/Gemfile CHANGED
@@ -31,6 +31,6 @@ group :development do
31
31
  gem 'rake', '~> 12.3'
32
32
  gem 'rdoc', '~> 6.0'
33
33
  gem 'reek', '~> 5.2'
34
- gem 'simplecov', '~> 0.16'
34
+ gem 'simplecov', '~> 0.16', require: false
35
35
  gem 'yard', '~> 0.9'
36
36
  end
data/README.md CHANGED
@@ -52,6 +52,18 @@ will be returned.
52
52
  codeless_code -Da 2014 -Gg 2 -nS
53
53
  ```
54
54
 
55
+ ### Listing
56
+
57
+ If the given filter returns more than one fable, they will be printed as a
58
+ list. By default, only the Number header and title of each fable will be
59
+ printed, one per line. You may use `-e` to specify additional headers to print.
60
+ Note that not every fable will have an entry for every header.
61
+
62
+ ```sh
63
+ codeless_code -e Names,Geekiness
64
+ codeless_code -D 2015-06 -e Date -e Topics
65
+ ```
66
+
55
67
  ### Current Options
56
68
 
57
69
  ```
@@ -67,76 +79,92 @@ Info
67
79
  --version
68
80
 
69
81
  Options
70
- -o, --output write to the given file. "-" for STDOUT
71
- -f, --format one of: raw, term (default)
72
- -p, --path path to directory of fables. see github.com/aldesantis/the-codeless-code
73
- --random select one fable, randomly, from the filtered list
74
- --random-set select n fables, randomly, from the filtered list
75
- --daily select one fable, randomly, from the filtered listbased on today's date
76
- --trace print full error message if a fable fails to parse
82
+ -c, --columns when listing fables, format the output into columns
83
+ -e, --headers headers to include in the list output. may be repeated
84
+ -f, --format one of: raw, plain, or term (default)
85
+ -o, --output write to the given file. "-" for STDOUT
86
+ -p, --path path to directory of fables. see github.com/aldesantis/the-codeless-code
87
+ --random select one fable, randomly, from the filtered list
88
+ --random-set select n fables, randomly, from the filtered list
89
+ --daily select one fable, randomly, from the filtered listbased on today's date
90
+ --trace print full error message if a fable fails to parse
91
+ -s, --sort when listing fables, sort by the given header
92
+ -r, --reverse when listing fables, reverse the order
77
93
 
78
94
  Series Filters
79
95
  -S, --series
80
- -Ss, --series-start series starts with
81
- -Se, --series-end series ends with
82
- -hS, --has-series has series listed
83
- -nS, --no-series no series listed
96
+ -Ss, --series-start series starts with
97
+ -Se, --series-end series ends with
98
+ -hS, --has-series has series listed
99
+ -nS, --no-series no series listed
84
100
 
85
101
  Title Filters
86
102
  -T, --title
87
- -Ts, --title-start title starts with
88
- -Te, --title-end title ends with
89
- -hT, --has-title has title listed
90
- -nT, --no-title no title listed
103
+ -Ts, --title-start title starts with
104
+ -Te, --title-end title ends with
105
+ -hT, --has-title has title listed
106
+ -nT, --no-title no title listed
91
107
 
92
108
  Number Filters
93
- -N, --number number (this is the default argument)
94
- -Ng, --number-gte number or greater
95
- -Nl, --number-lte number or lower
96
- -hN, --has-number has number listed
97
- -nN, --no-number no number listed
109
+ -N, --number number (this is the default argument)
110
+ -Ng, --number-gte number or greater
111
+ -Nl, --number-lte number or lower
112
+ -hN, --has-number has number listed
113
+ -nN, --no-number no number listed
98
114
 
99
115
  Language Filters
100
- -L, --lang language code (default: en)
116
+ -L, --lang language code (default: en)
101
117
 
102
118
  Translator Filters
103
- -r, --translator translator's name (default: first one, alphabetically)
104
- -R, --translator-exact translator's name, case-sensitive (default: first one, alphabetically)
119
+ -R, --translator translator's name (default: first one, alphabetically)
120
+ -Rx, --translator-exact translator's name, case-sensitive (default: first one, alphabetically)
105
121
 
106
122
  Date Filters
107
- -D, --date publish date
108
- -Da, --date-after publish date or after
109
- -Db, --date-before publish date or before
110
- -nD, --no-date no publish date listed
123
+ -D, --date publish date
124
+ -Da, --date-after publish date or after
125
+ -Db, --date-before publish date or before
126
+ -nD, --no-date no publish date listed
111
127
 
112
128
  Geekiness Filters
113
- -G, --geekiness geekiness rating
114
- -Gg, --geekiness-gte geekiness rating or greater
115
- -Gl, --geekiness-lte geekiness rating or lower
116
- -nG, --no-geekiness no geekiness rating
129
+ -G, --geekiness geekiness rating
130
+ -Gg, --geekiness-gte geekiness rating or greater
131
+ -Gl, --geekiness-lte geekiness rating or lower
132
+ -nG, --no-geekiness no geekiness rating
117
133
 
118
134
  Name Filters
119
135
  -A, --name
120
- -As, --name-start name starts with
121
- -Ae, --name-end name ends with
122
- -nA, --no-name no name listed
136
+ -As, --name-start name starts with
137
+ -Ae, --name-end name ends with
138
+ -nA, --no-name no name listed
123
139
 
124
140
  Credits Filters
125
141
  -C, --credits
126
- -Cs, --credits-start credits starts with
127
- -Ce, --credits-end credits ends with
128
- -hC, --has-credits has credits listed
129
- -nC, --no-credits no credits listed
142
+ -Cs, --credits-start credits starts with
143
+ -Ce, --credits-end credits ends with
144
+ -hC, --has-credits has credits listed
145
+ -nC, --no-credits no credits listed
130
146
 
131
147
  Tagline Filters
132
148
  -I, --tagline
133
- -Is, --tagline-start tagline starts with
134
- -Ie, --tagline-end tagline ends with
135
- -hI, --has-tagline has tagline listed
136
- -nI, --no-tagline no tagline listed
149
+ -Is, --tagline-start tagline starts with
150
+ -Ie, --tagline-end tagline ends with
151
+ -hI, --has-tagline has tagline listed
152
+ -nI, --no-tagline no tagline listed
153
+ ```
154
+
155
+ ## Development
156
+
157
+ ### Testing
158
+
159
+ ```sh
160
+ bundle exec rake test
161
+ COVERAGE=surewhynot bundle exec rake test
162
+
163
+ bundle exec guard
164
+ COVERAGE=1 bundle exec guard
137
165
  ```
138
166
 
139
- ## Contributing to codeless_code
167
+ ### Contributing to codeless_code
140
168
 
141
169
  * Check out the latest master to make sure the feature hasn't been
142
170
  implemented or the bug hasn't been fixed yet.
data/Rakefile CHANGED
@@ -60,6 +60,7 @@ Rake::TestTask.new(:test) do |test|
60
60
  test.pattern = 'test/**/test_*.rb'
61
61
  test.verbose = true
62
62
  end
63
+ Rake::Task[:build].prerequisites << :test
63
64
 
64
65
  desc "Code coverage detail"
65
66
  task :simplecov do
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.5
1
+ 0.1.6
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: codeless_code 0.1.5 ruby lib
5
+ # stub: codeless_code 0.1.6 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "codeless_code".freeze
9
- s.version = "0.1.5"
9
+ s.version = "0.1.6"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Jon Sangster".freeze]
14
- s.date = "2018-11-04"
14
+ s.date = "2018-11-05"
15
15
  s.description = "http://thecodelesscode.com contains many humorous and interesting fables and koans. The authors have open-sourced these fables, and many tanslations, available at https://github.com/aldesantis/the-codeless-code. This tool provides a CLI to filter through these fables and view them.".freeze
16
16
  s.email = "jon@ertt.ca".freeze
17
17
  s.executables = ["codeless_code".freeze]
@@ -1500,10 +1500,12 @@ Gem::Specification.new do |s|
1500
1500
  "lib/codeless_code/language_set.rb",
1501
1501
  "lib/codeless_code/options.rb",
1502
1502
  "lib/codeless_code/renderers/fable.rb",
1503
- "lib/codeless_code/renderers/page.rb",
1503
+ "lib/codeless_code/renderers/term_page.rb",
1504
+ "test/codeless_code/test_catalog.rb",
1504
1505
  "test/codeless_code/test_fable.rb",
1505
1506
  "test/codeless_code/test_fable_set.rb",
1506
1507
  "test/codeless_code/test_language_set.rb",
1508
+ "test/codeless_code/test_options.rb",
1507
1509
  "test/helper.rb",
1508
1510
  "test/test_codeless_code.rb"
1509
1511
  ]
@@ -14,14 +14,17 @@
14
14
  # You should have received a copy of the GNU General Public License along with
15
15
  # this program. If not, see <https://www.gnu.org/licenses/>.
16
16
  module CodelessCode
17
+ # The entire collection of fables available to the application from a given
18
+ # data directory.
17
19
  class Catalog
18
- # extend Forwardable
19
20
  include Enumerable
20
21
 
22
+ # @param root_dir [Pathname] A directory that contains {Fable} files
21
23
  def initialize(root_dir)
22
24
  @root_dir = root_dir
23
25
  end
24
26
 
27
+ # @return [Enumerable<Symbol>] the codes of all available languages
25
28
  def languages
26
29
  @languages ||= @root_dir.glob('*-*')
27
30
  .select(&:directory?)
@@ -31,10 +34,11 @@ module CodelessCode
31
34
  .sort
32
35
  end
33
36
 
37
+ # @param lang [Symbol]
34
38
  # @return [LanguageSet]
35
39
  def fetch(lang)
36
40
  if languages.include?(lang)
37
- LanguageSet.new(lang)
41
+ LanguageSet.new(lang, root_dir: @root_dir)
38
42
  else
39
43
  raise LanguageSet::NotFoundError,
40
44
  format("No fables for language %p", lang)
@@ -24,36 +24,28 @@ module CodelessCode
24
24
  end
25
25
 
26
26
  def call
27
- io = io_open
27
+ user_io = io_open
28
28
 
29
29
  if options.key?(:help)
30
- (io || $stdout).puts options.help
30
+ (user_io || $stdout).puts options.help
31
31
  elsif options.key?(:version)
32
- (io || $stdout).puts version_str
32
+ (user_io || $stdout).puts version_str
33
33
  elsif options.key?(:list_translations)
34
- Commands::ListTranslations.new(catalog, io: io).call
34
+ Commands::ListTranslations.new(catalog, io: user_io).call
35
35
  else
36
- filter_fables(io)
36
+ Commands::FilterFables.new(catalog, options, io: user_io)
37
+ .call(&method(:select))
37
38
  end
38
39
 
39
40
  rescue Slop::Error => e
40
41
  warn format("%s\n\n%s", e, options.help)
41
42
  exit 1
42
43
  ensure
43
- io&.close
44
+ user_io&.close
44
45
  end
45
46
 
46
47
  private
47
48
 
48
- def filter_fables(io)
49
- filter = Filters::FromOptions.new(options)
50
- fallback = options[:trace] ? nil : Formats::Raw
51
-
52
- cmd = Commands::FilterFables.new(filter, output_format, io: io,
53
- fallback_filter: fallback)
54
- cmd.call(catalog, &method(:select))
55
- end
56
-
57
49
  def options
58
50
  @options ||= Options.new(@command_name, @args)
59
51
  end
@@ -62,24 +54,19 @@ module CodelessCode
62
54
  @catalog ||= Catalog.new(options.data_dir)
63
55
  end
64
56
 
57
+ # @return [IO,nil] the user's chosen output stream. +nil+ if unspecified
65
58
  def io_open
66
59
  if (path = options[:output])
67
60
  path == '-' ? $stdout.dup : File.open(path, 'w')
68
61
  end
69
62
  end
70
63
 
71
- def output_format
72
- case options[:format]
73
- when 'plain' then Formats::Plain
74
- when 'raw' then Formats::Raw
75
- else Formats::Term
76
- end
77
- end
78
-
79
64
  def select(fables)
80
65
  select_rand(fables)
81
66
  end
82
67
 
68
+ # @return [Enumerable<Fable>] a random subset of the given collection, as
69
+ # specified by {#options}
83
70
  def select_rand(fables)
84
71
  if options[:daily]
85
72
  fables.sample(1, random: Random.new(Date.today.strftime('%Y%m%d').to_i))
@@ -15,19 +15,25 @@
15
15
  # this program. If not, see <https://www.gnu.org/licenses/>.
16
16
  module CodelessCode
17
17
  module Commands
18
+ # Filters down a {Catalog} of {Fable Fables} with criteria specified via
19
+ # the CLI. The results will be listed line-by-line, unless only one fable
20
+ # is found. In that case, it will be printed out.
18
21
  class FilterFables
22
+ attr_reader :options
23
+
19
24
  # @param io [IO] if given, the output will be written to this stream,
20
25
  # otherwise we will attempt to invoke the user's PAGER app
21
- def initialize(filter, format, io: nil, fallback_filter: Formats::Raw)
22
- @filter = filter
23
- @format = format
26
+ def initialize(catalog, options, io: nil)
27
+ @catalog = catalog
28
+ @options = options
24
29
  @io = io
25
- @fallback_filter = fallback_filter
26
30
  end
27
31
 
28
- def call(catalog)
29
- fables = catalog.select(@filter)
32
+ def call
33
+ filter = Filters::FromOptions.new(options)
34
+ fables = @catalog.select(filter)
30
35
  fables = yield fables if block_given?
36
+ fables = sort(fables)
31
37
 
32
38
  case fables.size
33
39
  when 0
@@ -41,11 +47,19 @@ module CodelessCode
41
47
 
42
48
  private
43
49
 
50
+ def sort(fables)
51
+ if options.key?(:sort)
52
+ fables = fables.sort_by { |f| f[options[:sort]] || "\uffff" }
53
+ end
54
+ fables = fables.reverse if options[:reverse]
55
+ fables
56
+ end
57
+
44
58
  def show(fable)
45
59
  if @io.nil? && ENV.key?('PAGER')
46
60
  pager(ENV['PAGER'], fable)
47
61
  else
48
- puts render(fable).for_pager(@format, fallback: @fallback_filter)
62
+ puts for_pager(fable)
49
63
  end
50
64
  end
51
65
 
@@ -53,12 +67,41 @@ module CodelessCode
53
67
  io = open format('|%s', cmd), 'w'
54
68
  pid = io.pid
55
69
 
56
- io.write render(fable).for_pager(@format, fallback: @fallback_filter)
57
- io.close
70
+ io.puts for_pager(fable)
71
+ ensure
72
+ io&.close
73
+ end
74
+
75
+ def for_pager(fable)
76
+ render(fable).for_pager(output_format, fallback: fallback_filter)
77
+ end
78
+
79
+ def output_format
80
+ case options[:format]
81
+ when 'plain' then Formats::Plain
82
+ when 'raw' then Formats::Raw
83
+ else Formats::Term
84
+ end
85
+ end
86
+
87
+ def fallback_filter
88
+ options[:trace] ? nil : Formats::Raw
58
89
  end
59
90
 
60
91
  def list(fables)
61
- fables.each { |fable| puts render(fable).for_list }
92
+ width = title_width(fables)
93
+
94
+ fables.each do |fable|
95
+ puts render(fable).for_list(options[:headers], title_width: width)
96
+ end
97
+ end
98
+
99
+ def title_width(fables)
100
+ if options[:columns]
101
+ -fables.map { |f| render(f).best_title.size }.compact.max
102
+ else
103
+ ''
104
+ end
62
105
  end
63
106
 
64
107
  def render(fable)
@@ -17,28 +17,30 @@ require 'forwardable'
17
17
  require 'date'
18
18
 
19
19
  module CodelessCode
20
+ # Model/Adapter for a "Codeless Code" fable stored in a text file.
20
21
  class Fable
21
22
  extend Forwardable
22
23
 
23
24
  HEADER_PATTERN = /([^:\s]+)\s*:\s*(.+)\s*$/.freeze
24
25
 
25
- attr_accessor :file
26
- attr_reader :read_headers
26
+ attr_reader :file, :read_headers
27
27
 
28
28
  alias_method :read_headers?, :read_headers
29
29
  def_delegators :headers, :[], :fetch, :key?
30
30
 
31
31
  def initialize(file)
32
- self.file = file
32
+ @file = file
33
33
  @read_headers = false
34
34
  @body_pos = nil
35
35
  end
36
36
 
37
+ # @return [String] the actual story, including MediaWiki markup
37
38
  def body
38
39
  @body ||= read_body.freeze
39
40
  end
40
41
  alias_method :to_s, :body
41
42
 
43
+ # @return [Hash<String, String>] the story's metadata
42
44
  def headers
43
45
  @headers ||= begin
44
46
  @read_headers = true
@@ -50,10 +52,12 @@ module CodelessCode
50
52
  headers.key?(key)
51
53
  end
52
54
 
55
+ # @return [::Date, nil]
53
56
  def date
54
57
  ::Date.parse(self['Date']) if header?('Date')
55
58
  end
56
59
 
60
+ # @return [Symbol]
57
61
  def lang
58
62
  @lang ||= dir_parts.first.to_sym
59
63
  end
@@ -16,10 +16,15 @@
16
16
  require 'forwardable'
17
17
 
18
18
  module CodelessCode
19
+ # An {Enumerable collection} of {Fable fables} in a given language, all
20
+ # translated by the same person.
19
21
  class FableSet
20
22
  extend Forwardable
21
23
  include Enumerable
22
24
 
25
+ FILE_PATTERN = '*.txt'.freeze
26
+ LANG_SEP = '-'.freeze
27
+
23
28
  attr_accessor :dir
24
29
  def_delegator :fables, :each
25
30
 
@@ -46,11 +51,11 @@ module CodelessCode
46
51
  private
47
52
 
48
53
  def files
49
- dir.glob('*.txt')
54
+ dir.glob(FILE_PATTERN)
50
55
  end
51
56
 
52
57
  def name_parts
53
- dir.basename.to_s.split('-')
58
+ dir.basename.to_s.split(LANG_SEP)
54
59
  end
55
60
  end
56
61
  end
@@ -15,6 +15,7 @@
15
15
  # this program. If not, see <https://www.gnu.org/licenses/>.
16
16
  module CodelessCode
17
17
  module Filters
18
+ # Module functions to generate filter subclasses.
18
19
  module Builders
19
20
  module_function
20
21
 
@@ -17,6 +17,8 @@ require 'forwardable'
17
17
 
18
18
  module CodelessCode
19
19
  module Filters
20
+ # A filter that containers a collection of other filters. It will only
21
+ # match a {Fable} if it's matched by each contained filter.
20
22
  class Composite
21
23
  extend Forwardable
22
24
  include Enumerable
@@ -18,9 +18,11 @@ require 'slop'
18
18
 
19
19
  module CodelessCode
20
20
  module Filters
21
+ # A {Composite} filter build from the arguments supplied via the CLI.
21
22
  class FromOptions < Composite
22
23
  extend Forwardable
23
24
 
25
+ # @param opts [Options]
24
26
  def initialize(opts)
25
27
  @opts = opts
26
28
  @filters = nil
@@ -17,6 +17,7 @@ require 'mediacloth'
17
17
 
18
18
  module CodelessCode
19
19
  module Formats
20
+ # Prints the {Fable} without any formatting, but removes the markup.
20
21
  class Plain < Base
21
22
  def call
22
23
  raw.split("\n\n")
@@ -15,6 +15,7 @@
15
15
  # this program. If not, see <https://www.gnu.org/licenses/>.
16
16
  module CodelessCode
17
17
  module Formats
18
+ # Prints the body of the {Fable} verbatim
18
19
  class Raw < Base
19
20
  def call
20
21
  raw
@@ -19,11 +19,11 @@ require 'nokogiri'
19
19
 
20
20
  module CodelessCode
21
21
  module Formats
22
+ # Renders the {Fable} using ANSI control characters for bold, italics,
23
+ # colors, etc.
22
24
  class Term < Base
23
25
  def call
24
- raw.split("\n\n")
25
- .map { |str| from_wiki(to_xhtml(regex(str))) }
26
- .join("\n\n")
26
+ from_wiki(to_xhtml(regex(raw)))
27
27
  end
28
28
 
29
29
  def regex(str)
@@ -16,12 +16,16 @@
16
16
  require 'forwardable'
17
17
 
18
18
  module CodelessCode
19
+ # An {Enumerable collection} of {FableSet fable sets} in a given language, but
20
+ # possibly translated by the different people.
19
21
  class LanguageSet
20
22
  extend Forwardable
21
23
  include Enumerable
22
24
 
23
25
  NotFoundError = Class.new(StandardError)
24
26
 
27
+ LANG_PATTERN = '%s-*'.freeze
28
+
25
29
  attr_accessor :lang, :root_dir
26
30
  def_delegator :fable_sets, :each
27
31
 
@@ -34,8 +38,10 @@ module CodelessCode
34
38
  dirs.map { |dir| FableSet.new(dir) }
35
39
  end
36
40
 
41
+ private
42
+
37
43
  def dirs
38
- root_dir.glob(format('%s-*', lang)).select(&:directory?)
44
+ root_dir.glob(format(LANG_PATTERN, lang)).select(&:directory?)
39
45
  end
40
46
  end
41
47
  end
@@ -17,10 +17,14 @@ require 'forwardable'
17
17
  require 'slop'
18
18
 
19
19
  module CodelessCode
20
+ # Adapter class for ARGV which exposes the command-line arguments as data
21
+ # types.
20
22
  class Options
21
23
  extend Forwardable
22
24
  include Enumerable
23
25
 
26
+ MAX_ARGS = 1 # --number flag may be passed as argument +cmd 3+ vs +cmd -N 3+
27
+
24
28
  def_delegators :opts, :to_hash, :[]
25
29
  def_delegators :to_hash, :each, :slice
26
30
 
@@ -29,34 +33,40 @@ module CodelessCode
29
33
  @argv = argv
30
34
  end
31
35
 
32
- def opts(suppress: false)
33
- @opts ||=
34
- begin
35
- args = [@command_name] + @argv
36
- opts = CodelessCode::OPTIONS.curry[@command_name]
37
-
38
- Slop.parse(args, suppress_errors: suppress, &opts).tap do |opt|
39
- if !suppress && opt.arguments.size > 2
40
- raise format('too many arguments: %p', opt.arguments[1..-1])
41
- end
42
- end
43
- end
36
+ def opts
37
+ @opts ||= parse_opts(suppress_errors: false)
38
+ end
39
+
40
+ def args
41
+ opts.arguments[1..-1]
44
42
  end
45
43
 
46
44
  def key?(key)
47
45
  !!self[key]
48
46
  end
49
47
 
48
+ # @return [Pathname] Where on the file system to search for fables. Will
49
+ # default to {CodelessCode.DEFAULT_DATA} if unspecified
50
50
  def data_dir
51
51
  @data_dir ||= self[:path] ? Pathname.new(self[:path]) : DEFAULT_DATA
52
52
  end
53
53
 
54
+ # @return [String] a description of the supported command-line options
54
55
  def help
55
- opts(suppress: true).to_s
56
+ parse_opts(suppress_errors: true).to_s
56
57
  end
57
58
 
58
- def args
59
- opts.arguments[1..-1]
59
+ private
60
+
61
+ def parse_opts(suppress_errors:)
62
+ args = [@command_name] + @argv
63
+ opts = CodelessCode::OPTIONS.curry[@command_name]
64
+
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])
68
+ end
69
+ end
60
70
  end
61
71
  end
62
72
  end
@@ -15,11 +15,12 @@
15
15
  # this program. If not, see <https://www.gnu.org/licenses/>.
16
16
  module CodelessCode
17
17
  module Renderers
18
+ # Prints a {Fable} is various ways.
18
19
  class Fable < SimpleDelegator
19
20
  HEADER_SORT = %w[Tagline Number Date].freeze
20
21
 
21
22
  def for_pager(format, fallback: nil)
22
- Page.new.tap do |page|
23
+ TermPage.new.tap do |page|
23
24
  page.title = best_title
24
25
  page.body = render_with(format, fallback: fallback)
25
26
 
@@ -27,12 +28,23 @@ module CodelessCode
27
28
  end
28
29
  end
29
30
 
30
- def for_list
31
- format('%s %s', wide_number, best_title)
31
+ def for_list(head_keys = [], title_width: '')
32
+ format("%s %#{title_width}s %s",
33
+ wide_number, best_title, render_slice(head_keys)).strip
34
+ end
35
+
36
+ def best_title
37
+ return title_with_subtitle if title&.size&.positive?
38
+
39
+ self['Name'] || self['Tagline'] || inspect
32
40
  end
33
41
 
34
42
  private
35
43
 
44
+ def render_slice(keys)
45
+ headers.slice(*keys).map {|k, v| format('%s: %p', k, v) }.join(', ')
46
+ end
47
+
36
48
  def render_with(format, fallback:)
37
49
  format.new(body).call
38
50
  rescue => e
@@ -41,12 +53,6 @@ module CodelessCode
41
53
  fallback.new(body).call
42
54
  end
43
55
 
44
- def best_title
45
- return title_with_subtitle if title&.size&.positive?
46
-
47
- self['Name'] || self['Tagline'] || inspect
48
- end
49
-
50
56
  def title_with_subtitle
51
57
  [self['Series'], title, self['Subtitle']].compact.join(': ')
52
58
  end
@@ -17,7 +17,9 @@ require 'colorized_string'
17
17
 
18
18
  module CodelessCode
19
19
  module Renderers
20
- class Page
20
+ # Attempts to format the output for the +PAGER+ such that it fits within
21
+ # the bounds of the terminal window.
22
+ class TermPage
21
23
  PAGER_FORMAT = [
22
24
  '%<title>s', '%<sep1>s', '%<headers>s', '%<sep2>s', '', '%<body>s'
23
25
  ].join("\n").freeze
data/lib/codeless_code.rb CHANGED
@@ -39,8 +39,8 @@ module CodelessCode
39
39
  end
40
40
 
41
41
  module Renderers
42
- autoload :Fable, 'codeless_code/renderers/fable'
43
- autoload :Page, 'codeless_code/renderers/page'
42
+ autoload :Fable, 'codeless_code/renderers/fable'
43
+ autoload :TermPage, 'codeless_code/renderers/term_page'
44
44
  end
45
45
 
46
46
  VERSION = Pathname.new(__dir__).join('..', 'VERSION').read.strip.freeze
@@ -68,8 +68,12 @@ module CodelessCode
68
68
 
69
69
  o.separator ''
70
70
  o.separator 'Options'
71
- o.string '-o', '--output', 'write to the given file. "-" for STDOUT'
71
+ o.boolean '-c', '--columns', 'when listing fables, format the output ' \
72
+ 'into columns'
73
+ o.array '-e', '--headers', 'headers to include in the list output. ' \
74
+ 'may be repeated'
72
75
  o.string '-f', '--format', 'one of: raw, plain, or term (default)'
76
+ o.string '-o', '--output', 'write to the given file. "-" for STDOUT'
73
77
  o.string '-p', '--path', 'path to directory of fables. ' \
74
78
  'see github.com/aldesantis/the-codeless-code'
75
79
  o.boolean '--random', 'select one fable, randomly, from the filtered list'
@@ -78,6 +82,9 @@ module CodelessCode
78
82
  'based on today\'s date'
79
83
  o.boolean '--trace', 'print full error message if a fable fails to parse'
80
84
 
85
+ o.string '-s', '--sort', 'when listing fables, sort by the given header'
86
+ o.boolean '-r', '--reverse', 'when listing fables, reverse the order'
87
+
81
88
  o.separator ''
82
89
  o.separator 'Series Filters'
83
90
  o.string '-S', '--series'
@@ -108,9 +115,9 @@ module CodelessCode
108
115
 
109
116
  o.separator ''
110
117
  o.separator 'Translator Filters'
111
- o.string '-r', '--translator', 'translator\'s name (default: first ' \
118
+ o.string '-R', '--translator', 'translator\'s name (default: first ' \
112
119
  'one, alphabetically)'
113
- o.string '-R', '--translator-exact', 'translator\'s name, ' \
120
+ o.string '-Rx', '--translator-exact', 'translator\'s name, ' \
114
121
  'case-sensitive (default: first one, alphabetically)'
115
122
 
116
123
  o.separator ''
@@ -0,0 +1,68 @@
1
+ # codeless_code filters and prints fables from http://thecodelesscode.com
2
+ # Copyright (C) 2018 Jon Sangster
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify it under
5
+ # the terms of the GNU General Public License as published by the Free Software
6
+ # Foundation, either version 3 of the License, or (at your option) any later
7
+ # version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful, but WITHOUT
10
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
+ # details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License along with
15
+ # this program. If not, see <https://www.gnu.org/licenses/>.
16
+ require 'helper'
17
+
18
+ class TestCatalog < UnitTest
19
+ def test_languages
20
+ assert_kind_of Enumerable, catalog.languages
21
+
22
+ catalog.languages.each do |lang|
23
+ assert_kind_of Symbol, lang
24
+ end
25
+ end
26
+
27
+ def test_fetch
28
+ assert_kind_of LanguageSet, catalog.fetch(:en)
29
+ assert_equal :en, catalog.fetch(:en).lang
30
+ end
31
+
32
+ def test_fetch_not_found
33
+ assert_raises(LanguageSet::NotFoundError) { catalog.fetch(:unknown) }
34
+ end
35
+
36
+ def test_language_sets
37
+ assert_kind_of Enumerable, catalog.language_sets
38
+ catalog.language_sets.each { |set| assert_kind_of LanguageSet, set }
39
+ end
40
+
41
+ def test_fable_sets
42
+ assert_kind_of Enumerable, catalog.fable_sets
43
+ catalog.fable_sets.each { |set| assert_kind_of FableSet, set }
44
+ end
45
+
46
+ def test_fables
47
+ assert_kind_of Enumerable, catalog.fables
48
+ catalog.fables.each { |fable| assert_kind_of Fable, fable }
49
+ end
50
+
51
+ def test_select
52
+ all_filter = ->(fable) { true }
53
+ none_filter = ->(fable) { false }
54
+
55
+ assert_kind_of Enumerable, catalog.select(all_filter)
56
+ catalog.select(all_filter).each { |fable| assert_kind_of Fable, fable }
57
+
58
+ assert_equal catalog.fables.size, catalog.select(all_filter).size
59
+ assert_equal 0, catalog.select(none_filter).size
60
+ end
61
+
62
+ private
63
+
64
+ def catalog(dir = DEFAULT_DATA)
65
+ (@catalog ||= {})[dir] ||= Catalog.new(dir)
66
+ end
67
+ end
68
+
@@ -56,8 +56,8 @@ class TestFable < UnitTest
56
56
  assert_kind_of Enumerable, fable.names
57
57
  end
58
58
 
59
- def test_names
60
- assert_kind_of Enumerable, fable.names
59
+ def test_topics
60
+ assert_kind_of Enumerable, fable.topics
61
61
  end
62
62
 
63
63
  private
@@ -17,21 +17,34 @@ require 'helper'
17
17
 
18
18
  class TestFableSet < UnitTest
19
19
  def test_lang
20
+ assert_kind_of Symbol, set.lang
21
+
20
22
  assert_equal :en, set('en-*').lang
21
23
  assert_equal :zh, set('zh-*').lang
22
24
  end
23
25
 
24
26
  def test_translator
27
+ assert_kind_of String, set.translator
28
+
25
29
  assert_equal 'qi', set('*-qi').translator
26
30
  assert_equal 'hanzik', set('*-hanzik').translator
27
31
  end
28
32
 
29
- def test_files
30
- assert_kind_of Enumerable, set.files
33
+ def test_fables
34
+ assert_kind_of Enumerable, set.fables
35
+
36
+ set.fables.each { |fable| assert_kind_of Fable, fable }
37
+ end
38
+
39
+ def test_filter
40
+ all_filter = ->(fable) { true }
41
+ none_filter = ->(fable) { false }
42
+
43
+ assert_kind_of Enumerable, set.filter(all_filter)
44
+ set.filter(all_filter).each { |fable| assert_kind_of Fable, fable }
31
45
 
32
- set.files.each do |entry|
33
- flunk format('%p is not a file', entry) unless entry.file?
34
- end
46
+ assert_equal set.fables.size, set.filter(all_filter).size
47
+ assert_equal 0, set.filter(none_filter).size
35
48
  end
36
49
 
37
50
  private
@@ -16,14 +16,6 @@
16
16
  require 'helper'
17
17
 
18
18
  class TestLanguageSet < UnitTest
19
- def test_class_available_languages
20
- assert_kind_of Enumerable, LanguageSet.available_languages
21
-
22
- LanguageSet.available_languages.each do |lang|
23
- assert_kind_of Symbol, lang
24
- end
25
- end
26
-
27
19
  def test_lang
28
20
  assert_equal :en, set(:en).lang
29
21
  assert_equal :zh, set(:zh).lang
@@ -32,23 +24,14 @@ class TestLanguageSet < UnitTest
32
24
  def test_fable_sets
33
25
  assert_kind_of Enumerable, set.fable_sets
34
26
  refute_empty set.fable_sets
35
- assert_equal set.fable_sets.size, set.dirs.size
36
27
 
37
28
  set.fable_sets.each { |set| assert_kind_of FableSet, set }
38
29
  end
39
30
 
40
- def test_dirs
41
- assert_kind_of Enumerable, set.dirs
42
-
43
- set.dirs.each do |entry|
44
- flunk format('%p is not a directory', entry) unless entry.directory?
45
- end
46
- end
47
-
48
31
  private
49
32
 
50
33
  def set(lang = :en)
51
- (@set ||= {})[lang] ||= LanguageSet.new(lang)
34
+ (@set ||= {})[lang] ||= LanguageSet.new(lang, root_dir: DEFAULT_DATA)
52
35
  end
53
36
  end
54
37
 
@@ -0,0 +1,65 @@
1
+ # codeless_code filters and prints fables from http://thecodelesscode.com
2
+ # Copyright (C) 2018 Jon Sangster
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify it under
5
+ # the terms of the GNU General Public License as published by the Free Software
6
+ # Foundation, either version 3 of the License, or (at your option) any later
7
+ # version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful, but WITHOUT
10
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
+ # details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License along with
15
+ # this program. If not, see <https://www.gnu.org/licenses/>.
16
+ require 'helper'
17
+
18
+ class TestOptions < UnitTest
19
+ def test_opts
20
+ assert_kind_of Slop::Result, options.opts
21
+ end
22
+
23
+ def test_opts_does_not_accept_bad_options
24
+ assert_raises { options('--unknown-argument').opts }
25
+ end
26
+
27
+ def test_args
28
+ assert_empty options.args
29
+ assert_empty options('--daily').args
30
+ assert_equal ['123'], options('-D', '2015', '123').args
31
+ end
32
+
33
+ def test_args_too_many
34
+ assert_raises { options('first', 'second').opts }
35
+ end
36
+
37
+ def test_help
38
+ assert_kind_of String, options.help
39
+ end
40
+
41
+ def test_help_accepts_bad_options
42
+ options('--unknown-argument').help
43
+ pass 'did not raise'
44
+ end
45
+
46
+ def test_key?
47
+ assert options('--help').key?(:help), '--help should supply :help key'
48
+ assert options('-h').key?(:help), '-h should supply :help key'
49
+ end
50
+
51
+ def test_data
52
+ assert_equal Pathname.new('/foo'), options('-p', '/foo').data_dir
53
+ end
54
+
55
+ def test_data_dir_default
56
+ assert_equal DEFAULT_DATA, options.data_dir
57
+ end
58
+
59
+ private
60
+
61
+ def options(*args)
62
+ (@options ||= {})[args] ||= Options.new('codeless_code', args)
63
+ end
64
+ end
65
+
data/test/helper.rb CHANGED
@@ -23,12 +23,13 @@ end
23
23
 
24
24
  SimpleCov.configure do
25
25
  clean_filters
26
- load_adapter 'test_frameworks'
26
+ load_profile 'test_frameworks'
27
27
  end
28
28
 
29
29
  ENV['COVERAGE'] && SimpleCov.start do
30
- add_filter '/.rvm/'
30
+ add_filter %r{/\..*/}
31
31
  end
32
+
32
33
  require 'rubygems'
33
34
  require 'bundler'
34
35
  begin
@@ -45,7 +46,6 @@ require 'minitest/autorun'
45
46
 
46
47
  Minitest::Reporters.use!(Minitest::Reporters::SpecReporter.new)
47
48
 
48
- $LOAD_PATH.unshift(__dir__)
49
49
  $LOAD_PATH.unshift(File.join(__dir__, '..', 'lib'))
50
50
  require 'pry-byebug'
51
51
  require 'codeless_code'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: codeless_code
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Sangster
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-11-04 00:00:00.000000000 Z
11
+ date: 2018-11-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -1726,10 +1726,12 @@ files:
1726
1726
  - lib/codeless_code/language_set.rb
1727
1727
  - lib/codeless_code/options.rb
1728
1728
  - lib/codeless_code/renderers/fable.rb
1729
- - lib/codeless_code/renderers/page.rb
1729
+ - lib/codeless_code/renderers/term_page.rb
1730
+ - test/codeless_code/test_catalog.rb
1730
1731
  - test/codeless_code/test_fable.rb
1731
1732
  - test/codeless_code/test_fable_set.rb
1732
1733
  - test/codeless_code/test_language_set.rb
1734
+ - test/codeless_code/test_options.rb
1733
1735
  - test/helper.rb
1734
1736
  - test/test_codeless_code.rb
1735
1737
  homepage: https://github.com/sangster/codeless_code