codeless_code 0.1.8 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,152 @@
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 'nokogiri'
19
+
20
+ module CodelessCode
21
+ module Markup
22
+ # Parses the body of a {Fable}, including HTML, MediaWiki syntax, and custom
23
+ # syntax, and returns it as an HTML DOM.
24
+ class Parser
25
+ # [[href|title]] or [[title]]
26
+ LINK_PATTERN = /\[\[(?:([^|]+)\|)?([^\]]+)\]\]/.freeze
27
+
28
+ ITALIC_PATTERN = %r{/([^/]+)/}.freeze # /some text/
29
+ SUP_PATTERN = /{{([^}]+)}}/.freeze # {{*}}
30
+ HR_PATTERN = /^- - -(?: -)*$/.freeze # - - -
31
+ BR_PATTERN = %r{\s*//\s*(?:\n|$)}m.freeze # end of line //
32
+
33
+ attr_reader :doc
34
+
35
+ def initialize(str)
36
+ @doc = Nokogiri::HTML(format('<main>%s</main>', str))
37
+ end
38
+
39
+ # @return [Nokogiri::XML::Element] The body of the fable, with non-HTML
40
+ # markup converted into HTML
41
+ def call
42
+ new_elem(:main).tap do |main|
43
+ paragraphs.each do |para|
44
+ main << parse_paragraph(para) unless para.inner_html.empty?
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def paragraphs
52
+ @paragraphs ||=
53
+ doc.css('main')
54
+ .flat_map(&method(:split_double_newline))
55
+ .reject { |node| node.inner_html.empty? }
56
+ end
57
+
58
+ def split_double_newline(para)
59
+ body = para.text? ? para.to_s : para.inner_html
60
+
61
+ case body.split(/\n\n+/).size
62
+ when 0 then new_elem(:span) << ''
63
+ when 1 then split_single_line(para)
64
+ else
65
+ str_node_set(format('<p>%s</p>', body.gsub(/\n\n+/, '</p><p>')))
66
+ end
67
+ end
68
+
69
+ def split_single_line(para)
70
+ case para.name
71
+ when 'p' then para
72
+ when 'main' then new_elem(:p) << para.children
73
+ else
74
+ new_elem(:p) << para
75
+ end
76
+ end
77
+
78
+ def new_elem(name)
79
+ Nokogiri::XML::Element.new(name.to_s, doc)
80
+ end
81
+
82
+ def str_node_set(str)
83
+ doc = Nokogiri::HTML(format('<main>%s</main>', str))
84
+ doc.css('body > main').tap { |ns| ns.document = doc }.children
85
+ end
86
+
87
+ def parse_paragraph(para)
88
+ html = para.inner_html
89
+
90
+ if HR_PATTERN.match?(html)
91
+ new_elem(:hr)
92
+ elsif blockquote?(html)
93
+ new_blockquote(html)
94
+ elsif html.lstrip.start_with?('== ')
95
+ new_elem(:h2) << html.lstrip[3..-1]
96
+ else
97
+ parse_node(para)
98
+ end
99
+ end
100
+
101
+ # Does every line start with the same number of spaces?
102
+ # :reek:UtilityFunction
103
+ def blockquote?(html)
104
+ match = /^\s+/.match(html)
105
+ return unless match
106
+
107
+ lines = html.lines
108
+ lines.size > 1 && lines.all? { |line| line.start_with?(match[0]) }
109
+ end
110
+
111
+ def new_blockquote(html)
112
+ match = /^\s+/.match(html)
113
+ len = match[0].length
114
+ span = new_elem(:span) << html.lines.map { |line| line[len..-1] }.join
115
+ new_elem(:blockquote) << parse_node(span.child)
116
+ end
117
+
118
+ # @return [NodeSet]
119
+ def parse_node(node)
120
+ return parse_text(node) if node.text?
121
+
122
+ node.children
123
+ .map(&method(:parse_node))
124
+ .inject(new_elem(node.name)) { |elem, child| elem << child }
125
+ end
126
+
127
+ def parse_text(text_node)
128
+ str_node_set(
129
+ gsub_links(
130
+ parse_slashes(text_node.content.dup)
131
+ .gsub(BR_PATTERN) { new_elem(:br) }
132
+ .gsub(SUP_PATTERN) { new_elem(:sup) << Regexp.last_match(1) }
133
+ ).tr("\n", ' ')
134
+ )
135
+ end
136
+
137
+ def parse_slashes(text)
138
+ text.split(BR_PATTERN).map do |str|
139
+ str.gsub(ITALIC_PATTERN) { new_elem(:em) << Regexp.last_match(1) }
140
+ end.join("//\n")
141
+ end
142
+
143
+ def gsub_links(text)
144
+ text.gsub(LINK_PATTERN) do
145
+ (new_elem(:a) << Regexp.last_match(2)).tap do |link|
146
+ link['href'] = Regexp.last_match(1) if Regexp.last_match(1)
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -95,7 +95,7 @@ module CodelessCode
95
95
 
96
96
  # Ask an external application how wide our terminal is
97
97
  class TermWidth
98
- def initialize(cmd = 'tputs cols')
98
+ def initialize(cmd = 'tput cols')
99
99
  @cmd = cmd
100
100
  end
101
101
 
data/lib/codeless_code.rb CHANGED
@@ -27,7 +27,7 @@ module CodelessCode
27
27
  autoload :LanguageSet, 'codeless_code/language_set'
28
28
  autoload :Options, 'codeless_code/options'
29
29
 
30
- # The "main" methods this applications supports
30
+ # The "main" methods this applications supports.
31
31
  module Commands
32
32
  autoload :FilterFables, 'codeless_code/commands/filter_fables'
33
33
  autoload :ListTranslations, 'codeless_code/commands/list_translations'
@@ -39,13 +39,13 @@ module CodelessCode
39
39
  autoload :Plain, 'codeless_code/formats/plain'
40
40
  autoload :Raw, 'codeless_code/formats/raw'
41
41
  autoload :Term, 'codeless_code/formats/term'
42
+ end
42
43
 
43
- # Custom parsers for the syntax tree generated by the +MediaCloth+ gem.
44
- module Parsers
45
- autoload :Base, 'codeless_code/formats/parsers/base'
46
- autoload :Plain, 'codeless_code/formats/parsers/plain'
47
- autoload :Term, 'codeless_code/formats/parsers/term'
48
- end
44
+ # Parses the markup in a {Fable}
45
+ module Markup
46
+ autoload :Converter, 'codeless_code/markup/converter'
47
+ autoload :Nodes, 'codeless_code/markup/nodes'
48
+ autoload :Parser, 'codeless_code/markup/parser'
49
49
  end
50
50
 
51
51
  # The methods in which a {Fable fables's} body may be rendered as text.
@@ -0,0 +1,74 @@
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 'helper'
19
+
20
+ module Formats
21
+ class TestPlain < UnitTest
22
+ def test_basic_html
23
+ assert_equal 'italic text', plain('<i>italic</i> text')
24
+ assert_equal 'bold text', plain('<b>bold</b> text')
25
+ assert_equal 'link text', plain('<a href="url">link</a> text')
26
+ end
27
+
28
+ def test_basic_html__not_across_paragraphs
29
+ assert_equal ['not italic', 'text across paragraphs'].join("\n\n"),
30
+ plain(['not <i>italic',
31
+ 'text</i> across paragraphs'].join("\n\n"))
32
+ end
33
+
34
+ def test_custom_syntax
35
+ assert_equal 'italic text', plain('/italic/ text')
36
+ end
37
+
38
+ def test_custom_syntax__not_across_paragraphs
39
+ input = ['not/italic', 'text/across paragraphs'].join("\n\n")
40
+ assert_equal input, plain(input)
41
+ end
42
+
43
+ def test_line_breaks
44
+ assert_equal "line\nbreaks", plain("line //\nbreaks")
45
+ end
46
+
47
+ def test_remove_bad_html
48
+ assert_equal 'bad html', plain('<a>bad html</b>')
49
+ assert_equal 'bad html', plain('<a>bad html')
50
+ end
51
+
52
+ def test_rule
53
+ assert_equal '- - - - - - - - -', plain('- - - - -')
54
+ end
55
+
56
+ def test_reference
57
+ assert_equal '[ref]', plain('{{ref}}')
58
+ end
59
+
60
+ def test_header
61
+ assert_equal "Some Header\n-----------", plain('== Some Header')
62
+ end
63
+
64
+ def test_quote
65
+ assert_equal "\tQuote Lines", plain(" Quote\n Lines")
66
+ end
67
+
68
+ private
69
+
70
+ def plain(body)
71
+ Formats::Plain.new(body).call
72
+ end
73
+ end
74
+ end
@@ -15,35 +15,13 @@
15
15
  #
16
16
  # You should have received a copy of the GNU General Public License along with
17
17
  # this program. If not, see <https://www.gnu.org/licenses/>.
18
- require 'mediacloth'
18
+ require 'helper'
19
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
20
+ module Formats
21
+ class TestRaw < UnitTest
22
+ def test_call
23
+ input = 'Some body content'
24
+ assert_same input, Formats::Raw.new(input).call
47
25
  end
48
26
  end
49
27
  end
@@ -0,0 +1,72 @@
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 'helper'
19
+ require 'colorized_string'
20
+
21
+ module Formats
22
+ class TestTerm < UnitTest
23
+ def test_basic_html
24
+ assert_equal color('italic').italic + ' text',
25
+ term('<i>italic</i> text')
26
+ assert_equal color('bold').bold + ' text',
27
+ term('<b>bold</b> text')
28
+ assert_equal color('link').underline + ' text',
29
+ term('<a href="url">link</a> text')
30
+ end
31
+
32
+ def test_custom_syntax
33
+ assert_equal color('italic').italic + ' text', term('/italic/ text')
34
+ end
35
+
36
+ def test_custom_syntax__not_across_paragraphs
37
+ input = ['not/italic', 'text/across paragraphs'].join("\n\n")
38
+ assert_equal input, term(input)
39
+ end
40
+
41
+ def test_line_breaks
42
+ assert_equal "line\nbreaks", term("line //\nbreaks")
43
+ end
44
+
45
+ def test_rule
46
+ assert_equal color('- - - - - - - - -').yellow, term('- - - - -')
47
+ end
48
+
49
+ def test_reference
50
+ assert_equal color('ref').yellow, term('{{ref}}')
51
+ end
52
+
53
+ def test_header
54
+ assert_equal color("Some Header\n-----------").blue,
55
+ term('== Some Header')
56
+ end
57
+
58
+ def test_quote
59
+ assert_equal color('Quote Lines').green, term(" Quote\n Lines")
60
+ end
61
+
62
+ private
63
+
64
+ def term(body)
65
+ Formats::Term.new(body).call.to_s
66
+ end
67
+
68
+ def color(str)
69
+ ColorizedString.new(str)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,177 @@
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 'helper'
19
+
20
+ module Markup
21
+ class TestParser < UnitTest # rubocop:disable Metrics/ClassLength
22
+ def test_name_is_main
23
+ assert_equal 'main', parse.name
24
+ end
25
+
26
+ def test_anchor
27
+ para = parse('[[label]]').child
28
+
29
+ assert_equal 'p', para.name
30
+ assert_equal 'a', para.child.name
31
+ assert_equal 'label', para.child.content
32
+ end
33
+
34
+ def test_link
35
+ link = parse('[[url|label]]').child.child
36
+
37
+ assert_equal 'a', link.name
38
+ assert_equal 'label', link.content
39
+ assert_equal 'url', link['href']
40
+ end
41
+
42
+ def test_italic
43
+ para = parse('/italic/').child
44
+
45
+ assert_equal 'p', para.name
46
+ assert_equal 'em', para.child.name
47
+ assert_equal 'italic', para.child.content
48
+ end
49
+
50
+ def test_sup
51
+ para = parse('{{ref}}').child
52
+
53
+ assert_equal 'p', para.name
54
+ assert_equal 'sup', para.child.name
55
+ assert_equal 'ref', para.child.content
56
+ end
57
+
58
+ def test_hr
59
+ rule = parse('- - - -').child
60
+
61
+ assert_equal 'hr', rule.name
62
+ end
63
+
64
+ def test_br
65
+ para = parse("first //\nsecond").child
66
+
67
+ assert_equal 'p', para.name
68
+ children = para.children
69
+ assert_equal 'first', children[0].content
70
+ assert_equal 'br', children[1].name
71
+ assert_equal 'second', children[2].content
72
+ end
73
+
74
+ def test_quote
75
+ quote = parse(" Quote\n Lines").child
76
+
77
+ assert_equal 'blockquote', quote.name
78
+ assert_equal 'Quote Lines', quote.content
79
+ end
80
+
81
+ private
82
+
83
+ def parse(body = nil)
84
+ Markup::Parser.new(body || fable.body).call
85
+ end
86
+
87
+ def fable(dir = 'en-test', fable = 'case-123.txt', root: fake_fs)
88
+ (@fable ||= {})["#{dir}/#{fable}"] ||=
89
+ Fable.new(root.glob(dir).first.glob(fable).first)
90
+ end
91
+
92
+ def fake_fs
93
+ FakeDir.new('/').tap do |fs|
94
+ fs.create_path('en-test/case-123.txt', fable_text)
95
+ end
96
+ end
97
+
98
+ def fable_text
99
+ <<-FABLE.gsub(/^ {8}/, '')
100
+ Date: 2013-12-28
101
+ Number: 125
102
+ Geekiness: 0
103
+ Title: Power
104
+ Names: Subashikoi, Shinpuru, Spider Clan
105
+ Topics: power, promotion, management, work-life balance
106
+ Illus.0.src: Vine.jpg
107
+ Illus.0.title: Even a garden needs a little debugging now and then.
108
+
109
+ It is the function of the Temple abbots to direct the
110
+ activities of their respective clans: choosing projects,
111
+ setting deadlines, apportioning tasks, and employing
112
+ whatever means are necessary to ensure that schedules are
113
+ met. It is for these powers that the abbots are both envied
114
+ and despised. Indeed, it is rare for abbot and monk to
115
+ cross paths without the latter finding himself more
116
+ miserable for the experience.
117
+
118
+ Thus it was with no great joy that the elder monk
119
+ [[Shinpuru]] found himself visited by the new head abbot of
120
+ the [[Spider Clan]].
121
+
122
+ - - -
123
+
124
+ Shinpuru was in the temple greenhouse, tending the plants of
125
+ a small winter garden that he kept as a hobby, when the
126
+ head abbot approached and bowed low, saying: "Have I the good
127
+ fortune of being in the presence of the monk Shinpuru, whose
128
+ code is admired throughout the Temple?"
129
+
130
+ "This miserable soul is he," said Shinpuru, returning the bow.
131
+
132
+ "I have come to ask if you have given any thought to the
133
+ future," said the abbot.
134
+
135
+ "Tomorrow I expect the sun shall rise," answered Shinpuru.
136
+ "Unless I am wrong, in which case it will not."
137
+
138
+ "I was thinking of your future, specifically," replied the
139
+ abbot.
140
+
141
+ The head abbot frowned. "What would Shinpuru think of a
142
+ seed that refused to sprout, or a tree that refused to yield
143
+ fruit? What else should I think of a monk who so quickly
144
+ declines an opportunity for growth, for command, for power?"
145
+
146
+ Shinpuru set aside his shears to tie up the vine. "Define
147
+ power," he said.
148
+
149
+ "The ability to do as one wishes," said the abbot.
150
+
151
+ "Well, then," said Shinpuru. "Tomorrow I wish to greet the
152
+ sunrise with my little bowl. Then I wish to take some hot
153
+ tea at my workstation as I read the technical sites I find
154
+ most illuminating, after which I look forward to a fruitful
155
+ day of coding interrupted only by some pleasant exchanges
156
+ with my fellows and a midday meal at this very spot, tending
157
+ my garden. When night falls I wish to find myself in my
158
+ cozy room with a belly full of rice, a cup full of hot sake,
159
+ a purse full of coins sufficient to buy more seeds, and a
160
+ mind empty of all other cares."
161
+
162
+ The abbot bowed. "I expect that Shinpuru has all the power
163
+ he could desire, then. Unless he is wrong."
164
+
165
+ "I am seldom wrong about such things," said Shinpuru,
166
+ picking up his shears again as another yellow leaf caught
167
+ his eye. "In a world where even the sunrise is uncertain, a
168
+ man may be excused for not knowing a great many things. But
169
+ to not know my own heart? I hope to never be so hopeless
170
+ a fool."
171
+
172
+
173
+ {{*}} As documented in cases [[#61|61]], [[#62|62]], [[#67|67]], [[#120|120]], and probably others besides. Abbots in the Spider Clan have the average life expectancy of a dolphin in the Gobi desert.
174
+ FABLE
175
+ end
176
+ end
177
+ end
@@ -18,7 +18,7 @@
18
18
  require 'helper'
19
19
  require 'minitest/mock'
20
20
 
21
- class TestCli < UnitTest
21
+ class TestCli < UnitTest # rubocop:disable Metrics/ClassLength
22
22
  def setup
23
23
  @pager = ENV.delete('PAGER')
24
24
  end
@@ -27,11 +27,29 @@ class TestCli < UnitTest
27
27
  ENV['PAGER'] = @pager
28
28
  end
29
29
 
30
+ def test_bad_arguments
31
+ assert_output(nil, /unknown option `--unknown-flag'/) do
32
+ cli('--unknown-flag').call
33
+ end
34
+ assert_output(nil, /Usage: test_app/) do
35
+ cli('--unknown-flag').call
36
+ end
37
+ end
38
+
39
+ def test_bad_number
40
+ assert_raises(ArgumentError) { cli('--random-set', 'string').call }
41
+ end
42
+
30
43
  def test_call_default
31
44
  expected = "00123 Test Case\n00234 Test Case 2\n"
32
45
  assert_output(expected) { cli.call }
33
46
  end
34
47
 
48
+ def test_force_stdout
49
+ expected = "00123 Test Case\n00234 Test Case 2\n"
50
+ assert_output(expected) { cli('-o', '-').call }
51
+ end
52
+
35
53
  def test_call_single_by_number
36
54
  [
37
55
  " Test Case\n" \
@@ -56,6 +74,42 @@ class TestCli < UnitTest
56
74
  assert_output("en test\n") { cli('--list-translations').call }
57
75
  end
58
76
 
77
+ def test_random_stability
78
+ create_cases(10)
79
+ cli = cli('--random')
80
+
81
+ first = capture_io { Random.srand(0) && cli.call }
82
+ second = capture_io { Random.srand(1) && cli.call }
83
+ third = capture_io { Random.srand(0) && cli.call }
84
+
85
+ assert_equal first, third
86
+ refute_equal first, second
87
+ end
88
+
89
+ def test_random_set_stability
90
+ create_cases(10)
91
+ cli = cli('--random-set', '10')
92
+
93
+ first = capture_io { Random.srand(0) && cli.call }
94
+ second = capture_io { Random.srand(1) && cli.call }
95
+ third = capture_io { Random.srand(0) && cli.call }
96
+
97
+ assert_equal first, third
98
+ refute_equal first, second
99
+ end
100
+
101
+ def test_daily_stability
102
+ create_cases(10)
103
+ cli = cli('--daily')
104
+
105
+ first = capture_io { stub_today('2018-12-23') { cli.call } }
106
+ second = capture_io { stub_today('2000-11-11') { cli.call } }
107
+ third = capture_io { stub_today('2018-12-23') { cli.call } }
108
+
109
+ assert_equal first, third
110
+ refute_equal first, second
111
+ end
112
+
59
113
  private
60
114
 
61
115
  def cli(*args)
@@ -64,7 +118,11 @@ class TestCli < UnitTest
64
118
  end
65
119
  end
66
120
 
67
- def fake_fs # rubocop:disable Metrics/MethodLength
121
+ def fake_fs
122
+ @fake_fs ||= create_fake_fs
123
+ end
124
+
125
+ def create_fake_fs # rubocop:disable Metrics/MethodLength
68
126
  FakeDir.new('/').tap do |fs|
69
127
  fs.create_path('en-test/case-123.txt', <<-FABLE)
70
128
  Title: Test Case
@@ -82,4 +140,19 @@ class TestCli < UnitTest
82
140
  FABLE
83
141
  end
84
142
  end
143
+
144
+ def create_cases(count)
145
+ (100..(100 + count)).each do |num|
146
+ fake_fs.create_path("en/test/case-#{num}.txt", <<-FABLE)
147
+ Title: Case #{num}
148
+ Number: #{num}
149
+
150
+ Case #{num}
151
+ FABLE
152
+ end
153
+ end
154
+
155
+ def stub_today(date, &blk)
156
+ Date.stub(:today, Date.parse(date), &blk)
157
+ end
85
158
  end
data/test/support/fs.rb CHANGED
@@ -1,5 +1,20 @@
1
1
  # frozen_string_literal: true
2
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/>.
3
18
  module Support
4
19
  class FakeFile
5
20
  attr_reader :name, :path, :body, :parent