spoonerize 1.0.0 → 2.1.0

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: 1b4c993b75b971449d8795c0666e4645bee48ff903afaea84463025da44ffdbf
4
- data.tar.gz: b57070d3e1dc670ef4cdf17f9657c8759828e86a039571d305a6d6716aaa5b65
3
+ metadata.gz: bf971b88e97e4e680e1aaa25fdd21238734738ff4b6e230580f93305f5c823a5
4
+ data.tar.gz: da9f97e86eeb122f7c1ecbe057a5f73bc0908a86ab5dc5acbb76898ff4e326cd
5
5
  SHA512:
6
- metadata.gz: ccf19838d1a10609dddbd2da4cfe8286c548b9bc91e2b957ee592120225305c93a166da7ae10c34ead80a402f8cb767fbb192162fade18afc046298ff3d5dd1c
7
- data.tar.gz: 3420c7dc81ea682e3388b9f9097a6c538199cbb38e2f5179e3a5f68f8200b278674da04397df05e14a259ad6ba5878daf3b63390f244a3988c3678f08e8b85a3
6
+ metadata.gz: 8a74bb8fa978b4a298ea9c3e5fbb3fb53be59875a9a41749570a1014462d5fe4a8aa13dd02683163ee1177b812cd3a69edff1a880975463b83a1fa143f5b2508
7
+ data.tar.gz: 263a67ba0a8d3bc0d3d89f46a4f2ef9a4f2cb9475684ca0a16309916dab7ffc370c5bd0796224e411f0b22a209aac1779aa21b340bdae96fd88a27836d50bc11
data/Gemfile.lock CHANGED
@@ -1,19 +1,26 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- spoonerize (1.0.0)
4
+ spoonerize (2.1.0)
5
5
  csv
6
+ puma (~> 8.0)
7
+ rackup (~> 2.3)
8
+ sinatra (~> 4.2)
6
9
 
7
10
  GEM
8
11
  remote: https://rubygems.org/
9
12
  specs:
10
13
  ast (2.4.3)
14
+ base64 (0.3.0)
11
15
  csv (3.3.5)
12
16
  date (3.5.1)
13
17
  erb (6.0.4)
14
18
  json (2.20.0)
15
19
  language_server-protocol (3.17.0.5)
16
20
  lint_roller (1.1.0)
21
+ logger (1.7.0)
22
+ mustermann (3.1.1)
23
+ nio4r (2.7.5)
17
24
  parallel (1.28.0)
18
25
  parser (3.3.11.1)
19
26
  ast (~> 2.4.1)
@@ -23,7 +30,19 @@ GEM
23
30
  psych (5.4.0)
24
31
  date
25
32
  stringio
33
+ puma (8.0.2)
34
+ nio4r (~> 2.0)
26
35
  racc (1.8.1)
36
+ rack (3.2.6)
37
+ rack-protection (4.2.1)
38
+ base64 (>= 0.1.0)
39
+ logger (>= 1.6.0)
40
+ rack (>= 3.0.0, < 4)
41
+ rack-session (2.1.2)
42
+ base64 (>= 0.1.0)
43
+ rack (>= 3.0.0)
44
+ rackup (2.3.1)
45
+ rack (>= 3)
27
46
  rainbow (3.1.1)
28
47
  rake (13.4.2)
29
48
  rdoc (7.2.0)
@@ -50,6 +69,13 @@ GEM
50
69
  rubocop (>= 1.75.0, < 2.0)
51
70
  rubocop-ast (>= 1.47.1, < 2.0)
52
71
  ruby-progressbar (1.13.0)
72
+ sinatra (4.2.1)
73
+ logger (>= 1.6.0)
74
+ mustermann (~> 3.0)
75
+ rack (>= 3.0.0, < 4)
76
+ rack-protection (= 4.2.1)
77
+ rack-session (>= 2.0.0, < 3)
78
+ tilt (~> 2.0)
53
79
  standard (1.54.0)
54
80
  language_server-protocol (~> 3.17.0.2)
55
81
  lint_roller (~> 1.0)
@@ -65,6 +91,7 @@ GEM
65
91
  stringio (3.2.0)
66
92
  test-unit (3.7.8)
67
93
  power_assert
94
+ tilt (2.7.0)
68
95
  tsort (0.2.0)
69
96
  unicode-display_width (3.2.0)
70
97
  unicode-emoji (~> 4.1)
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # Welcome to Spoonerize -- a word game.
2
2
  [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Fevanthegrayt%2Fspoonerize%2Fbadge%3Fref%3Dmaster&style=flat)](https://actions-badge.atrox.dev/evanthegrayt/spoonerize/goto?ref=master)
3
- [![Gem Version](https://badge.fury.io/rb/spoonerize.svg)](https://badge.fury.io/rb/spoonerize)
3
+ [![Gem Version](https://img.shields.io/gem/v/spoonerize.svg)](https://rubygems.org/gems/spoonerize)
4
+ ![Language: Ruby](https://img.shields.io/static/v1?label=language&message=Ruby&color=CC342D&style=flat&logo=ruby&logoColor=CC342D)
4
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
6
 
6
7
  > Spoonerism *[noun]* a verbal error in which a speaker accidentally transposes
@@ -100,7 +101,7 @@ Here is a list of all available options:
100
101
 
101
102
  ```
102
103
  -r, --[no-]reverse Reverse flipping
103
- -l, --[no-]lazy Skip small words
104
+ -l, --[no-]lazy Skip common words
104
105
  -c, --[no-]consonants-only Only flip consonant-starting words
105
106
  -m, --[no-]map Print words mapping
106
107
  -p, --[no-]print-log Print all entries in the log
@@ -108,10 +109,34 @@ Here is a list of all available options:
108
109
  --exclude=WORD Words to skip
109
110
  ```
110
111
 
112
+ ## Web Usage
113
+ The gem also installs a small Sinatra app:
114
+
115
+ ```sh
116
+ spoonerize-web
117
+ ```
118
+
119
+ By default, Sinatra starts on its normal local development address. You can
120
+ choose a host or port when you need to:
121
+
122
+ ```sh
123
+ spoonerize-web --host 127.0.0.1 --port 9292
124
+ ```
125
+
126
+ Open the printed local URL in your browser, enter a phrase, choose any options,
127
+ and submit the form. The page reloads with the spoonerized result and keeps
128
+ your phrase and options in the form. Check "Save result" to write a successful
129
+ result to the configured log file.
130
+
131
+ The web app ships with the main gem for now, so `gem install spoonerize`
132
+ installs both `spoonerize` and `spoonerize-web`.
133
+
111
134
  ### Config File
112
135
  You can create a Ruby config file called `~/.spoonerizerc`. The CLI loads this
113
136
  file automatically before it parses command-line options, so options set in the
114
- file can still be overridden at runtime by executable flags.
137
+ file can still be overridden at runtime by executable flags. The web app loads
138
+ the same file when it starts, and uses those values for the initial form
139
+ defaults.
115
140
 
116
141
  ```ruby
117
142
  Spoonerize.configure do |config|
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/spoonerize/web/cli"
4
+
5
+ Spoonerize::Web::Cli.execute(ARGV)
@@ -7,10 +7,10 @@ module Spoonerize
7
7
  # The class for handling the command-line interface.
8
8
  class Cli
9
9
  ##
10
- # The config file the user can create to change default runtime options.
10
+ # The config file the CLI loads before parsing runtime options.
11
11
  #
12
12
  # @return [String]
13
- CONFIG_FILE = File.expand_path(File.join(ENV["HOME"], ".spoonerizerc"))
13
+ CONFIG_FILE = Spoonerize::CONFIG_FILE
14
14
 
15
15
  ##
16
16
  # Creates an instance of +Spoonerism+ and runs what the user requested.
@@ -131,7 +131,7 @@ module Spoonerize
131
131
  o.on("-r", "--[no-]reverse", "Reverse flipping") do |v|
132
132
  prefs[:reverse] = v
133
133
  end
134
- o.on("-l", "--[no-]lazy", "Skip small words") do |v|
134
+ o.on("-l", "--[no-]lazy", "Skip common words") do |v|
135
135
  prefs[:lazy] = v
136
136
  end
137
137
  o.on("-c", "--[no-]consonants-only", "Only flip consonant-starting words") do |v|
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Spoonerize
4
+ ##
5
+ # Runtime options used by the CLI and Spoonerism instances.
4
6
  class Config
5
7
  ##
6
8
  # Lazy mode. If true, words in +lazy_words+ will not be altered.
@@ -4,8 +4,22 @@ module Spoonerize
4
4
  ##
5
5
  # The main word-flipper.
6
6
  class Spoonerism
7
+ ##
8
+ # Letters that always represent vowel sounds.
9
+ #
10
+ # @return [String]
7
11
  VOWEL_LETTERS = "aeio"
12
+
13
+ ##
14
+ # Letters that always represent consonant sounds.
15
+ #
16
+ # @return [String]
8
17
  CONSONANT_LETTERS = "bcdfghjklmnprstvwxz"
18
+
19
+ ##
20
+ # Letters that make an initial "y" act like a vowel sound.
21
+ #
22
+ # @return [String]
9
23
  Y_FOLLOWING_CONSONANTS = "bcdfghjklmnpqrstvwxz"
10
24
 
11
25
  ##
@@ -9,13 +9,13 @@ module Spoonerize
9
9
  # Major version.
10
10
  #
11
11
  # @return [Integer]
12
- MAJOR = 1
12
+ MAJOR = 2
13
13
 
14
14
  ##
15
15
  # Minor version.
16
16
  #
17
17
  # @return [Integer]
18
- MINOR = 0
18
+ MINOR = 1
19
19
 
20
20
  ##
21
21
  # Patch version.
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ require_relative "../web"
6
+
7
+ module Spoonerize
8
+ class Web
9
+ ##
10
+ # Command-line launcher for the web app.
11
+ class Cli
12
+ ##
13
+ # Server options used when no command-line overrides are passed.
14
+ #
15
+ # @return [Hash]
16
+ DEFAULT_OPTIONS = {
17
+ host: Web.bind,
18
+ port: Web.port
19
+ }.freeze
20
+
21
+ ##
22
+ # Starts the web app from command-line arguments.
23
+ #
24
+ # @param [Array] options
25
+ #
26
+ # @return [nil]
27
+ def self.execute(options = [])
28
+ new(options).execute
29
+ end
30
+
31
+ ##
32
+ # Parsed server options.
33
+ #
34
+ # @return [Hash]
35
+ attr_reader :options
36
+
37
+ ##
38
+ # Create a web CLI launcher.
39
+ #
40
+ # @param [Array] options Command-line arguments.
41
+ #
42
+ # @return [self]
43
+ def initialize(options)
44
+ @options = DEFAULT_OPTIONS.merge(parse(options))
45
+ end
46
+
47
+ ##
48
+ # Starts the Sinatra web app.
49
+ #
50
+ # @return [nil]
51
+ def execute
52
+ Web.run!(bind: options[:host], port: options[:port])
53
+ end
54
+
55
+ private
56
+
57
+ def parse(options)
58
+ {}.tap do |prefs|
59
+ OptionParser.new do |o|
60
+ o.version = ::Spoonerize::Version.to_s
61
+ o.on("--host=HOST", "Host to bind") do |value|
62
+ prefs[:host] = value
63
+ end
64
+ o.on("--port=PORT", Integer, "Port to bind") do |value|
65
+ prefs[:port] = value
66
+ end
67
+ end.parse!(options)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,336 @@
1
+ :root {
2
+ color-scheme: light;
3
+ --bg: #f7f7f4;
4
+ --ink: #24211f;
5
+ --muted: #6c655f;
6
+ --line: #d7d2c9;
7
+ --panel: #fffdfa;
8
+ --accent: #1d6f78;
9
+ --accent-dark: #145059;
10
+ --notice-bg: #fff0dc;
11
+ --notice-line: #e1a853;
12
+ }
13
+
14
+ * {
15
+ box-sizing: border-box;
16
+ }
17
+
18
+ body {
19
+ margin: 0;
20
+ min-height: 100vh;
21
+ background: var(--bg);
22
+ color: var(--ink);
23
+ font-family: ui-serif, Georgia, "Times New Roman", serif;
24
+ line-height: 1.5;
25
+ }
26
+
27
+ .page {
28
+ min-height: 100vh;
29
+ display: grid;
30
+ place-items: start center;
31
+ padding: 4rem 1rem;
32
+ }
33
+
34
+ .shell {
35
+ width: min(100%, 46rem);
36
+ }
37
+
38
+ .masthead {
39
+ margin-bottom: 2rem;
40
+ border-bottom: 1px solid var(--line);
41
+ padding-bottom: 1.25rem;
42
+ }
43
+
44
+ .masthead-with-action {
45
+ display: flex;
46
+ align-items: flex-end;
47
+ justify-content: space-between;
48
+ gap: 1rem;
49
+ }
50
+
51
+ h1 {
52
+ margin: 0;
53
+ font-size: clamp(1.9rem, 4vw, 2.75rem);
54
+ line-height: 1.08;
55
+ }
56
+
57
+ .lede {
58
+ margin: 0.5rem 0 0;
59
+ color: var(--muted);
60
+ font-family: ui-sans-serif, system-ui, sans-serif;
61
+ font-size: 0.95rem;
62
+ font-weight: 700;
63
+ }
64
+
65
+ .action-link {
66
+ flex: 0 0 auto;
67
+ color: var(--accent-dark);
68
+ font-family: ui-sans-serif, system-ui, sans-serif;
69
+ font-size: 0.95rem;
70
+ font-weight: 750;
71
+ text-decoration-color: color-mix(in srgb, var(--accent-dark) 35%, transparent);
72
+ text-underline-offset: 0.18em;
73
+ }
74
+
75
+ .action-link:hover,
76
+ .action-link:focus {
77
+ color: var(--accent);
78
+ text-decoration-color: currentcolor;
79
+ }
80
+
81
+ .result {
82
+ margin: 0 0 1.75rem;
83
+ font-size: clamp(2rem, 6vw, 3.6rem);
84
+ font-weight: 800;
85
+ line-height: 1.05;
86
+ }
87
+
88
+ .notice {
89
+ margin: 0 0 1.5rem;
90
+ border: 1px solid var(--notice-line);
91
+ background: var(--notice-bg);
92
+ padding: 0.8rem 1rem;
93
+ font-family: ui-sans-serif, system-ui, sans-serif;
94
+ font-weight: 650;
95
+ }
96
+
97
+ .spoonerize-form {
98
+ display: grid;
99
+ gap: 1rem;
100
+ }
101
+
102
+ .field,
103
+ fieldset {
104
+ display: grid;
105
+ gap: 0.45rem;
106
+ }
107
+
108
+ .field span,
109
+ legend {
110
+ color: var(--muted);
111
+ font-family: ui-sans-serif, system-ui, sans-serif;
112
+ font-size: 0.85rem;
113
+ font-weight: 700;
114
+ }
115
+
116
+ input[type="text"] {
117
+ width: 100%;
118
+ border: 1px solid var(--line);
119
+ border-radius: 4px;
120
+ background: var(--panel);
121
+ color: var(--ink);
122
+ font: inherit;
123
+ font-size: 1.1rem;
124
+ padding: 0.75rem 0.85rem;
125
+ }
126
+
127
+ input[type="text"]:focus {
128
+ border-color: var(--accent);
129
+ outline: 3px solid color-mix(in srgb, var(--accent) 22%, transparent);
130
+ }
131
+
132
+ fieldset {
133
+ margin: 0;
134
+ border: 1px solid var(--line);
135
+ border-radius: 4px;
136
+ background: var(--panel);
137
+ padding: 0.9rem 1rem 1rem;
138
+ }
139
+
140
+ legend {
141
+ padding: 0 0.35rem;
142
+ }
143
+
144
+ .check {
145
+ display: flex;
146
+ align-items: center;
147
+ gap: 0.55rem;
148
+ font-family: ui-sans-serif, system-ui, sans-serif;
149
+ }
150
+
151
+ .check input {
152
+ width: 1rem;
153
+ height: 1rem;
154
+ accent-color: var(--accent);
155
+ }
156
+
157
+ button {
158
+ width: fit-content;
159
+ border: 0;
160
+ border-radius: 4px;
161
+ background: var(--accent);
162
+ color: #fff;
163
+ cursor: pointer;
164
+ font-family: ui-sans-serif, system-ui, sans-serif;
165
+ font-size: 1rem;
166
+ font-weight: 750;
167
+ padding: 0.72rem 1rem;
168
+ }
169
+
170
+ button:hover,
171
+ button:focus {
172
+ background: var(--accent-dark);
173
+ }
174
+
175
+ .table-wrap {
176
+ overflow-x: auto;
177
+ border: 1px solid var(--line);
178
+ border-radius: 4px;
179
+ background: var(--panel);
180
+ }
181
+
182
+ .saved-table {
183
+ width: 100%;
184
+ border-collapse: collapse;
185
+ font-family: ui-sans-serif, system-ui, sans-serif;
186
+ font-size: 0.95rem;
187
+ }
188
+
189
+ .saved-table th {
190
+ border-bottom: 1px solid var(--line);
191
+ padding: 0.8rem 0.9rem;
192
+ text-align: left;
193
+ vertical-align: top;
194
+ }
195
+
196
+ .saved-table td {
197
+ border-bottom: 1px solid var(--line);
198
+ padding: 0;
199
+ text-align: left;
200
+ vertical-align: top;
201
+ }
202
+
203
+ .saved-table th {
204
+ color: var(--muted);
205
+ font-size: 0.78rem;
206
+ letter-spacing: 0;
207
+ text-transform: uppercase;
208
+ }
209
+
210
+ .saved-table tbody tr:last-child td {
211
+ border-bottom: 0;
212
+ }
213
+
214
+ .saved-table td {
215
+ color: var(--ink);
216
+ }
217
+
218
+ .saved-table .row-link {
219
+ display: block;
220
+ min-height: 100%;
221
+ padding: 0.8rem 0.9rem;
222
+ color: inherit;
223
+ text-decoration: none;
224
+ }
225
+
226
+ .saved-table tbody tr:hover,
227
+ .saved-table tbody tr:focus-within {
228
+ background: color-mix(in srgb, var(--accent) 7%, var(--panel));
229
+ }
230
+
231
+ .saved-table .row-link:focus {
232
+ outline: 3px solid color-mix(in srgb, var(--accent) 22%, transparent);
233
+ outline-offset: 2px;
234
+ }
235
+
236
+ .saved-table td:last-child {
237
+ color: var(--muted);
238
+ white-space: nowrap;
239
+ }
240
+
241
+ .footer {
242
+ width: min(100%, 46rem);
243
+ margin-top: 2.5rem;
244
+ border-top: 1px solid var(--line);
245
+ padding-top: 1rem;
246
+ color: var(--muted);
247
+ font-family: ui-sans-serif, system-ui, sans-serif;
248
+ font-size: 0.82rem;
249
+ }
250
+
251
+ .footer a {
252
+ color: inherit;
253
+ text-decoration-color: color-mix(in srgb, var(--muted) 45%, transparent);
254
+ text-underline-offset: 0.16em;
255
+ }
256
+
257
+ .footer a:hover,
258
+ .footer a:focus {
259
+ color: var(--accent-dark);
260
+ text-decoration-color: currentcolor;
261
+ }
262
+
263
+ .footer span {
264
+ margin: 0 0.35rem;
265
+ }
266
+
267
+ @media (max-width: 40rem) {
268
+ .page {
269
+ padding-top: 2rem;
270
+ }
271
+
272
+ button {
273
+ width: 100%;
274
+ }
275
+
276
+ .masthead-with-action {
277
+ display: grid;
278
+ align-items: start;
279
+ }
280
+ }
281
+
282
+ @media (max-width: 34rem) {
283
+ .table-wrap {
284
+ overflow: visible;
285
+ border: 0;
286
+ background: transparent;
287
+ }
288
+
289
+ .saved-table,
290
+ .saved-table thead,
291
+ .saved-table tbody,
292
+ .saved-table th,
293
+ .saved-table td,
294
+ .saved-table tr {
295
+ display: block;
296
+ }
297
+
298
+ .saved-table thead {
299
+ position: absolute;
300
+ width: 1px;
301
+ height: 1px;
302
+ overflow: hidden;
303
+ clip: rect(0 0 0 0);
304
+ }
305
+
306
+ .saved-table tr {
307
+ margin-bottom: 0.8rem;
308
+ border: 1px solid var(--line);
309
+ border-radius: 4px;
310
+ background: var(--panel);
311
+ }
312
+
313
+ .saved-table td {
314
+ display: grid;
315
+ grid-template-columns: 7rem 1fr;
316
+ gap: 0.75rem;
317
+ border-bottom: 1px solid var(--line);
318
+ }
319
+
320
+ .saved-table td::before {
321
+ content: attr(data-label);
322
+ padding: 0.8rem 0 0.8rem 0.9rem;
323
+ color: var(--muted);
324
+ font-size: 0.78rem;
325
+ font-weight: 800;
326
+ text-transform: uppercase;
327
+ }
328
+
329
+ .saved-table .row-link {
330
+ padding: 0.8rem 0.9rem 0.8rem 0;
331
+ }
332
+
333
+ .saved-table td:last-child {
334
+ white-space: normal;
335
+ }
336
+ }
@@ -0,0 +1,63 @@
1
+ <section class="shell">
2
+ <header class="masthead masthead-with-action">
3
+ <h1>Spoonerize a phrase</h1>
4
+ <a class="action-link" href="/saved/">Saved spoonerisms</a>
5
+ </header>
6
+
7
+ <% if @result %>
8
+ <p class="result"><%= h @result %></p>
9
+ <% if @saved %>
10
+ <p class="notice">Saved.</p>
11
+ <% end %>
12
+ <% elsif @error %>
13
+ <% display_error = @error == "Not enough words to flip." ? "Not enough words to spoonerize." : @error %>
14
+ <p class="notice"><%= h display_error %></p>
15
+ <% end %>
16
+
17
+ <form action="/" method="post" class="spoonerize-form">
18
+ <label class="field">
19
+ <span>Phrase</span>
20
+ <input
21
+ type="text"
22
+ name="phrase"
23
+ value="<%= h @phrase %>"
24
+ placeholder="Enter phrase to spoonerize..."
25
+ autofocus>
26
+ </label>
27
+
28
+ <fieldset>
29
+ <legend>Options</legend>
30
+
31
+ <label class="check">
32
+ <input type="checkbox" name="reverse" value="1" <%= checked?("reverse") %>>
33
+ <span>Reverse spoonerizing</span>
34
+ </label>
35
+
36
+ <label class="check">
37
+ <input type="checkbox" name="lazy" value="1" <%= checked?("lazy") %>>
38
+ <span>Skip common words</span>
39
+ </label>
40
+
41
+ <label class="check">
42
+ <input type="checkbox" name="consonants_only" value="1" <%= checked?("consonants_only") %>>
43
+ <span>Only spoonerize consonant-starting words</span>
44
+ </label>
45
+
46
+ <label class="check">
47
+ <input type="checkbox" name="save" value="1" <%= @save ? "checked" : nil %>>
48
+ <span>Save result</span>
49
+ </label>
50
+ </fieldset>
51
+
52
+ <label class="field">
53
+ <span>Excluded words</span>
54
+ <input
55
+ type="text"
56
+ name="excluded_words"
57
+ value="<%= h @excluded_words_value %>"
58
+ placeholder="word another">
59
+ </label>
60
+
61
+ <button type="submit">Spoonerize</button>
62
+ </form>
63
+ </section>
@@ -0,0 +1,20 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Spoonerize</title>
7
+ <link rel="stylesheet" href="/styles.css">
8
+ </head>
9
+ <body>
10
+ <main class="page">
11
+ <%= yield %>
12
+
13
+ <footer class="footer">
14
+ <a href="https://github.com/evanthegrayt">evanthegrayt</a>
15
+ <span>/</span>
16
+ <a href="https://github.com/evanthegrayt/spoonerize">spoonerize</a>
17
+ </footer>
18
+ </main>
19
+ </body>
20
+ </html>
@@ -0,0 +1,44 @@
1
+ <section class="shell">
2
+ <header class="masthead masthead-with-action">
3
+ <div>
4
+ <h1>Saved spoonerisms</h1>
5
+ <p class="lede"><%= @entries.size %> saved <%= @entries.size == 1 ? "spoonerism" : "spoonerisms" %></p>
6
+ </div>
7
+ <a class="action-link" href="/">New spoonerism</a>
8
+ </header>
9
+
10
+ <% if @entries.empty? %>
11
+ <p class="notice">No saved spoonerisms yet.</p>
12
+ <% else %>
13
+ <div class="table-wrap">
14
+ <table class="saved-table">
15
+ <thead>
16
+ <tr>
17
+ <th scope="col">Phrase</th>
18
+ <th scope="col">Result</th>
19
+ <th scope="col">Options</th>
20
+ </tr>
21
+ </thead>
22
+ <tbody>
23
+ <% @entries.each do |entry| %>
24
+ <% phrase = entry[0].to_s %>
25
+ <% result = entry[1].to_s %>
26
+ <% options = entry[2].to_s %>
27
+ <% entry_path = saved_entry_path(phrase, result) %>
28
+ <tr>
29
+ <td data-label="Phrase">
30
+ <a class="row-link" href="<%= h entry_path %>"><%= h phrase %></a>
31
+ </td>
32
+ <td data-label="Result">
33
+ <a class="row-link" href="<%= h entry_path %>"><strong><%= h result %></strong></a>
34
+ </td>
35
+ <td data-label="Options">
36
+ <a class="row-link" href="<%= h entry_path %>"><%= h options %></a>
37
+ </td>
38
+ </tr>
39
+ <% end %>
40
+ </tbody>
41
+ </table>
42
+ </div>
43
+ <% end %>
44
+ </section>
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sinatra/base"
4
+
5
+ require_relative "../spoonerize"
6
+
7
+ module Spoonerize
8
+ ##
9
+ # Sinatra app for spoonerizing phrases in the browser.
10
+ class Web < Sinatra::Base
11
+ ##
12
+ # Boolean Spoonerism options exposed by the web form.
13
+ #
14
+ # @return [Array<String>]
15
+ OPTION_NAMES = %w[reverse lazy consonants_only].freeze
16
+
17
+ set :root, File.expand_path(File.join(__dir__, "..", ".."))
18
+ set :public_folder, File.expand_path(File.join(__dir__, "web", "public"))
19
+ set :views, File.expand_path(File.join(__dir__, "web", "views"))
20
+ set :static, true
21
+ set :show_exceptions, false
22
+
23
+ configure do
24
+ Spoonerize.load_config_file(Spoonerize::CONFIG_FILE) if File.file?(Spoonerize::CONFIG_FILE)
25
+ end
26
+
27
+ helpers do
28
+ ##
29
+ # HTML checkbox attribute for a truthy option value.
30
+ #
31
+ # @param [String] name The option name.
32
+ #
33
+ # @return [String, nil]
34
+ def checked?(name)
35
+ option_value(name) ? "checked" : nil
36
+ end
37
+
38
+ ##
39
+ # Current boolean value for a web form option.
40
+ #
41
+ # @param [String] name The option name.
42
+ #
43
+ # @return [Boolean]
44
+ def option_value(name)
45
+ @options.fetch(name.to_sym)
46
+ end
47
+
48
+ ##
49
+ # Escapes a value for safe HTML output.
50
+ #
51
+ # @param [Object] value The value to escape.
52
+ #
53
+ # @return [String]
54
+ def h(value)
55
+ Rack::Utils.escape_html(value)
56
+ end
57
+
58
+ ##
59
+ # Path for loading a saved spoonerism on the main web form.
60
+ #
61
+ # @param [String] phrase The original phrase.
62
+ # @param [String] result The saved spoonerized result.
63
+ #
64
+ # @return [String]
65
+ def saved_entry_path(phrase, result)
66
+ "/?#{Rack::Utils.build_query("phrase" => phrase, "result" => result)}"
67
+ end
68
+ end
69
+
70
+ get "/" do
71
+ prepare_request(false)
72
+ @result = params["result"].to_s unless params["result"].to_s.empty?
73
+ erb :index
74
+ end
75
+
76
+ get "/saved" do
77
+ redirect "/saved/"
78
+ end
79
+
80
+ get "/saved/" do
81
+ @entries = saved_entries
82
+ erb :saved
83
+ end
84
+
85
+ post "/" do
86
+ prepare_request(true)
87
+ @result = spoonerize_phrase if @submitted && !@phrase.strip.empty?
88
+
89
+ erb :index
90
+ end
91
+
92
+ private
93
+
94
+ def options_from_params
95
+ OPTION_NAMES.to_h do |name|
96
+ value = @submitted ? params.key?(name) : Spoonerize.config.public_send(name)
97
+ [name.to_sym, value]
98
+ end
99
+ end
100
+
101
+ def excluded_words_from(value)
102
+ value.split(/[,\s]+/).reject(&:empty?)
103
+ end
104
+
105
+ def excluded_words_from_params
106
+ return excluded_words_from(params["excluded_words"].to_s) if @submitted
107
+
108
+ Spoonerize.config.excluded_words
109
+ end
110
+
111
+ def prepare_request(submitted)
112
+ @submitted = submitted
113
+ @phrase = params["phrase"].to_s
114
+ @options = options_from_params
115
+ @excluded_words = excluded_words_from_params
116
+ @excluded_words_value = @excluded_words.join(" ")
117
+ @save = @submitted && params.key?("save")
118
+ end
119
+
120
+ def saved_entries
121
+ Spoonerize::Log.new(Spoonerize.config.logfile_name).contents.reverse
122
+ end
123
+
124
+ def spoonerize_phrase
125
+ spoonerism = Spoonerism.new(
126
+ *@phrase.split,
127
+ **@options,
128
+ excluded_words: @excluded_words
129
+ )
130
+ result = spoonerism.to_s
131
+ spoonerism.save if @save
132
+ @saved = @save
133
+
134
+ result
135
+ rescue => error
136
+ @error = friendly_error(error)
137
+ nil
138
+ end
139
+
140
+ def friendly_error(error)
141
+ return "Not enough words to flip." if error.message == "Not enough words to flip"
142
+
143
+ error.message
144
+ end
145
+ end
146
+ end
data/lib/spoonerize.rb CHANGED
@@ -1,5 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ##
4
+ # The main namespace for the gem.
5
+ module Spoonerize
6
+ ##
7
+ # The config file the user can create to change default runtime options.
8
+ #
9
+ # @return [String]
10
+ CONFIG_FILE = File.expand_path(File.join(ENV["HOME"], ".spoonerizerc"))
11
+ end
12
+
3
13
  require_relative "spoonerize/config"
4
14
  require_relative "spoonerize/spoonerism"
5
15
  require_relative "spoonerize/bumper"
data/spoonerize.gemspec CHANGED
@@ -10,6 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.summary = %(Spoonerize phrases from the command line.)
11
11
  spec.description = %(Spoonerize phrases from the command line. Comes with an API)
12
12
  spec.homepage = "https://evanthegrayt.github.io/spoonerize/"
13
+ spec.required_ruby_version = ">= 3.2"
13
14
 
14
15
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
15
16
  # to allow pushing to a single host or delete this section to allow pushing to any host.
@@ -29,6 +30,9 @@ Gem::Specification.new do |spec|
29
30
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
30
31
  spec.require_paths = ["lib"]
31
32
  spec.add_dependency "csv"
33
+ spec.add_dependency "puma", "~> 8.0"
34
+ spec.add_dependency "rackup", "~> 2.3"
35
+ spec.add_dependency "sinatra", "~> 4.2"
32
36
  spec.add_development_dependency "rdoc"
33
37
  spec.add_development_dependency "rake", "~> 13.0", ">= 13.0.1"
34
38
  spec.add_development_dependency "standard", "= 1.54.0"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spoonerize
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Gray
@@ -23,6 +23,48 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: puma
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '8.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '8.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rackup
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.3'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.3'
54
+ - !ruby/object:Gem::Dependency
55
+ name: sinatra
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '4.2'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '4.2'
26
68
  - !ruby/object:Gem::Dependency
27
69
  name: rdoc
28
70
  requirement: !ruby/object:Gem::Requirement
@@ -95,6 +137,7 @@ description: Spoonerize phrases from the command line. Comes with an API
95
137
  email: evanthegrayt@vivaldi.net
96
138
  executables:
97
139
  - spoonerize
140
+ - spoonerize-web
98
141
  extensions: []
99
142
  extra_rdoc_files: []
100
143
  files:
@@ -107,6 +150,7 @@ files:
107
150
  - Rakefile
108
151
  - _config.yml
109
152
  - bin/spoonerize
153
+ - bin/spoonerize-web
110
154
  - lib/spoonerize.rb
111
155
  - lib/spoonerize/bumper.rb
112
156
  - lib/spoonerize/cli.rb
@@ -114,6 +158,12 @@ files:
114
158
  - lib/spoonerize/log.rb
115
159
  - lib/spoonerize/spoonerism.rb
116
160
  - lib/spoonerize/version.rb
161
+ - lib/spoonerize/web.rb
162
+ - lib/spoonerize/web/cli.rb
163
+ - lib/spoonerize/web/public/styles.css
164
+ - lib/spoonerize/web/views/index.erb
165
+ - lib/spoonerize/web/views/layout.erb
166
+ - lib/spoonerize/web/views/saved.erb
117
167
  - spoonerize.gemspec
118
168
  homepage: https://evanthegrayt.github.io/spoonerize/
119
169
  licenses:
@@ -130,7 +180,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
130
180
  requirements:
131
181
  - - ">="
132
182
  - !ruby/object:Gem::Version
133
- version: '0'
183
+ version: '3.2'
134
184
  required_rubygems_version: !ruby/object:Gem::Requirement
135
185
  requirements:
136
186
  - - ">="