codeless_code 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
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