chunky_bacon 0.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 +7 -0
- data/CHANGELOG.md +13 -0
- data/LICENSE +21 -0
- data/LICENSE-ASSETS +13 -0
- data/README.md +55 -0
- data/exe/chunkybacon +11 -0
- data/lib/chunky_bacon/cli.rb +64 -0
- data/lib/chunky_bacon/fox.rb +16 -0
- data/lib/chunky_bacon/fox.txt +10 -0
- data/lib/chunky_bacon/helpers.rb +208 -0
- data/lib/chunky_bacon/opener.rb +38 -0
- data/lib/chunky_bacon/rack_env.rb +50 -0
- data/lib/chunky_bacon/server.rb +92 -0
- data/lib/chunky_bacon/version.rb +5 -0
- data/lib/chunky_bacon.rb +40 -0
- metadata +66 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 79373a42eb29eb28f1fdf188d7837fd3c97e5cebc5a2419cf0edbfe93535acfc
|
|
4
|
+
data.tar.gz: c13f00849c79d15e17094dfdd316c8ca00aa50e8d9673c0c71b381ce59d56edc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 45360a618e747dbfb441d77fc29f5505354537947bfda4b3783d1dfcc27488e452fbfc102bf1a4874d9586f4a434b99ad70e73db3dc6ac1eb78137b27722e6b3
|
|
7
|
+
data.tar.gz: 873db5d5a4e07063229ff494b9d97f336c5fd4eeb3fe42d36104571037039c2edcf3f5e51a175b428fed11082a56abb33598409f01cb9b0798d7265406b79e2b
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- The course's helpers on a computer: `install_gem`, `show_image`,
|
|
6
|
+
`show_pdf`, `download_file`, `show_browser` (with a small built-in server),
|
|
7
|
+
`mock_get`, `show_irb`, `show_files`, `run_tests`; `show_three` and
|
|
8
|
+
`show_shoes` explain what to do instead.
|
|
9
|
+
- `chunkybacon` shows Chunky; `chunkybacon run [FILE]` runs a program with
|
|
10
|
+
the helpers loaded.
|
|
11
|
+
- In the course page (ruby.wasm) the page's own helpers stay.
|
|
12
|
+
- Code MIT; the fox drawing (`lib/chunky_bacon/fox.txt`) CC BY-SA 4.0.
|
|
13
|
+
- Also installable as `chunkybacon` and `chunky-bacon` (alias gems).
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andi Idogawa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/LICENSE-ASSETS
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
The assets of chunky_bacon - the drawing of Chunky, the fox, in
|
|
2
|
+
lib/chunky_bacon/fox.txt - are licensed under the Creative Commons
|
|
3
|
+
Attribution-ShareAlike 4.0 International License (CC BY-SA 4.0):
|
|
4
|
+
https://creativecommons.org/licenses/by-sa/4.0/
|
|
5
|
+
|
|
6
|
+
Copyright (c) 2026 Andi Idogawa. Chunky is the mascot of the Ruby course
|
|
7
|
+
"Ruby lernen mit Chunky Bacon" (https://github.com/Largo/chunkybacon).
|
|
8
|
+
|
|
9
|
+
You may share and adapt the drawing, also commercially, as long as you give
|
|
10
|
+
credit (name the course and Andi Idogawa, link the license) and share
|
|
11
|
+
adaptations under the same license.
|
|
12
|
+
|
|
13
|
+
The code of chunky_bacon is licensed under the MIT License (LICENSE).
|
data/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# chunky_bacon
|
|
2
|
+
|
|
3
|
+
The companion gem of **[Ruby lernen mit Chunky Bacon](https://github.com/Largo/chunkybacon)**,
|
|
4
|
+
an interactive Ruby course that runs in the browser.
|
|
5
|
+
|
|
6
|
+
In the course, notebook cells and the workshop have helpers like `show_image`,
|
|
7
|
+
`show_browser` and `download_file`. This gem gives your own computer the same
|
|
8
|
+
helpers, so a program you wrote in the course runs unchanged with plain Ruby.
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
gem install chunky_bacon # or: gem install chunkybacon - the same gem
|
|
12
|
+
chunkybacon # Chunky shouts
|
|
13
|
+
chunkybacon run # runs main.rb with the helpers loaded
|
|
14
|
+
chunkybacon run spiel.rb # ... or another file
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
In your own code:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require "chunky_bacon"
|
|
21
|
+
|
|
22
|
+
require "chunky_png"
|
|
23
|
+
image = ChunkyPNG::Image.new(64, 64, ChunkyPNG::Color.rgb(232, 114, 42))
|
|
24
|
+
show_image image # saves chunky-image-1.png and opens it
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## The helpers
|
|
28
|
+
|
|
29
|
+
What the course page showed below the cell becomes a file in the program's
|
|
30
|
+
folder, opened in your system's viewer. Status lines go to stderr, so they
|
|
31
|
+
never mix with your program's output.
|
|
32
|
+
|
|
33
|
+
| Helper | On your computer |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `install_gem "name"` | installs the gem unless it is there, and activates it |
|
|
36
|
+
| `show_image image` | saves `chunky-image-N.png` (ChunkyPNG image, PNG bytes or a path) and opens it |
|
|
37
|
+
| `show_pdf pdf` | saves `chunky-document-N.pdf` (Prawn/HexaPDF document, bytes or a path) and opens it |
|
|
38
|
+
| `download_file data, "name"` | saves the data as `name` in the program's folder |
|
|
39
|
+
| `show_browser App, "/path"` | starts the Rack app (Sinatra, Roda, ...) on 127.0.0.1 and opens it; after the program's last line the server runs on until Ctrl+C |
|
|
40
|
+
| `mock_get App, "/path"` | `[status, body]` without any server |
|
|
41
|
+
| `show_irb` | an IRB session; `exit` returns to the program |
|
|
42
|
+
| `show_files` | lists the files in the program's folder |
|
|
43
|
+
| `run_tests` | runs the Minitest tests defined so far |
|
|
44
|
+
| `show_three`, `show_shoes` | explain how to do it on a computer (they need the course page for now) |
|
|
45
|
+
|
|
46
|
+
`CHUNKYBACON_OPEN=0` stops the viewer and the browser from opening (CI, a
|
|
47
|
+
machine without a desktop); the files are saved either way. Inside the course
|
|
48
|
+
page the gem keeps the page's own helpers and only adds the fox.
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
The code is MIT ([LICENSE](LICENSE)). The drawing of Chunky, the fox
|
|
53
|
+
(`lib/chunky_bacon/fox.txt`), is CC BY-SA 4.0 like the course's other content
|
|
54
|
+
([LICENSE-ASSETS](LICENSE-ASSETS)). The course's lesson texts are not part of
|
|
55
|
+
this gem.
|
data/exe/chunkybacon
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
|
|
5
|
+
module ChunkyBacon
|
|
6
|
+
# The `chunkybacon` command.
|
|
7
|
+
class CLI
|
|
8
|
+
HELP = <<~TEXT
|
|
9
|
+
Usage: chunkybacon [command]
|
|
10
|
+
|
|
11
|
+
(no command) Chunky shouts
|
|
12
|
+
run [FILE] [ARGS] runs FILE (main.rb if not given) with the course's
|
|
13
|
+
helpers loaded - show_image, show_browser,
|
|
14
|
+
download_file, ... - like the workshop's Run button
|
|
15
|
+
version the version of chunky_bacon
|
|
16
|
+
help this text
|
|
17
|
+
|
|
18
|
+
The course: https://github.com/Largo/chunkybacon
|
|
19
|
+
TEXT
|
|
20
|
+
|
|
21
|
+
def initialize(out: $stdout, err: $stderr)
|
|
22
|
+
@out = out
|
|
23
|
+
@err = err
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# the exit status
|
|
27
|
+
def call(args)
|
|
28
|
+
command, *rest = args
|
|
29
|
+
case command
|
|
30
|
+
when nil
|
|
31
|
+
ChunkyBacon.shout(@out)
|
|
32
|
+
0
|
|
33
|
+
when "run"
|
|
34
|
+
run(rest)
|
|
35
|
+
when "version", "--version", "-v"
|
|
36
|
+
@out.puts VERSION
|
|
37
|
+
0
|
|
38
|
+
when "help", "--help", "-h"
|
|
39
|
+
@out.puts HELP
|
|
40
|
+
0
|
|
41
|
+
else
|
|
42
|
+
@err.puts "chunkybacon: unknown command #{command.inspect}\n\n#{HELP}"
|
|
43
|
+
1
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# In a child Ruby, so the program has the terminal - input for gets,
|
|
50
|
+
# Ctrl+C - and its exit status is the command's.
|
|
51
|
+
def run(args)
|
|
52
|
+
file = args.first || "main.rb"
|
|
53
|
+
unless File.file?(file)
|
|
54
|
+
others = Dir["*.rb"].sort
|
|
55
|
+
hint = others.empty? ? "" : " - there is #{others.join(", ")}: chunkybacon run #{others.first}"
|
|
56
|
+
@err.puts "chunkybacon: no #{file} here#{hint}"
|
|
57
|
+
return 1
|
|
58
|
+
end
|
|
59
|
+
lib = File.expand_path("..", __dir__)
|
|
60
|
+
system(RbConfig.ruby, "-I", lib, "-r", "chunky_bacon", file, *args.drop(1))
|
|
61
|
+
$?.exitstatus || 1
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChunkyBacon
|
|
4
|
+
# Chunky, the course's fox, holding up a strip of bacon and shouting - the
|
|
5
|
+
# same original character as the site's mascot (html/assets/chunky.svg),
|
|
6
|
+
# in plain ASCII so any terminal shows it. The drawing is fox.txt next to
|
|
7
|
+
# this file; like the course's other content it is licensed CC BY-SA 4.0
|
|
8
|
+
# (LICENSE-ASSETS), while this code is MIT.
|
|
9
|
+
FOX = File.read(File.expand_path("fox.txt", __dir__), encoding: "UTF-8").gsub("\r\n", "\n").freeze
|
|
10
|
+
|
|
11
|
+
# Puts the fox on +io+ (the terminal unless told otherwise).
|
|
12
|
+
def self.shout(io = $stdout)
|
|
13
|
+
io.puts FOX
|
|
14
|
+
nil
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChunkyBacon
|
|
4
|
+
class << self
|
|
5
|
+
# Servers started by show_browser keep the program alive after its last
|
|
6
|
+
# line, until Ctrl+C - as the mini browser stayed below the cell. Tests
|
|
7
|
+
# switch this off.
|
|
8
|
+
attr_writer :keep_serving
|
|
9
|
+
|
|
10
|
+
def keep_serving
|
|
11
|
+
@keep_serving.nil? ? true : @keep_serving
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def servers
|
|
15
|
+
@servers ||= []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Status lines (where a file went, which address a server has) go to
|
|
19
|
+
# stderr, so they never mix with what the program itself puts.
|
|
20
|
+
def say(message)
|
|
21
|
+
$stderr.puts "[chunky] #{message}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# "chunky-image-1.png", "chunky-image-2.png", ... per run of the program
|
|
25
|
+
def next_file(stem, extension)
|
|
26
|
+
@counters ||= Hash.new(0)
|
|
27
|
+
@counters[stem] += 1
|
|
28
|
+
File.join(output_dir, "#{stem}-#{@counters[stem]}.#{extension}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def save(path, bytes)
|
|
32
|
+
File.binwrite(path, bytes)
|
|
33
|
+
say "saved #{relative(path)} (#{bytes.bytesize} B)"
|
|
34
|
+
path
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def relative(path)
|
|
38
|
+
path = File.expand_path(path)
|
|
39
|
+
base = File.expand_path(output_dir) + File::SEPARATOR
|
|
40
|
+
path.start_with?(base) ? path.delete_prefix(base) : path
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def serve_until_interrupt
|
|
44
|
+
return if servers.empty? || !keep_serving
|
|
45
|
+
|
|
46
|
+
say "the server keeps running - Ctrl+C stops it"
|
|
47
|
+
servers.each(&:join)
|
|
48
|
+
rescue Interrupt
|
|
49
|
+
$stderr.puts
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The course's notebook helpers, for a program on a computer. Same names,
|
|
54
|
+
# arguments and defaults as on the course page (html/main.rb); what showed
|
|
55
|
+
# below the cell there is a file in the program's folder here, opened in
|
|
56
|
+
# the system's viewer.
|
|
57
|
+
module Helpers
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# Installs a gem now, unless it is installed already, and activates it -
|
|
61
|
+
# afterwards `require` finds it. true when it is there.
|
|
62
|
+
def install_gem(name)
|
|
63
|
+
name = name.to_s
|
|
64
|
+
begin
|
|
65
|
+
gem name
|
|
66
|
+
return true
|
|
67
|
+
rescue Gem::LoadError
|
|
68
|
+
nil # not installed yet
|
|
69
|
+
end
|
|
70
|
+
if defined?(Bundler) && ENV["BUNDLE_GEMFILE"]
|
|
71
|
+
ChunkyBacon.say "this program runs under Bundler: add gem \"#{name}\" to its Gemfile and run bundle install"
|
|
72
|
+
return false
|
|
73
|
+
end
|
|
74
|
+
ChunkyBacon.say "installing #{name} ..."
|
|
75
|
+
Gem.install(name)
|
|
76
|
+
Gem::Specification.reset
|
|
77
|
+
gem name
|
|
78
|
+
true
|
|
79
|
+
rescue Gem::Exception, SystemCallError => e
|
|
80
|
+
ChunkyBacon.say "#{name} could not be installed: #{e.message}"
|
|
81
|
+
false
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# A picture: a ChunkyPNG image (anything with to_blob or to_data_url),
|
|
85
|
+
# PNG bytes, or the path of an image file.
|
|
86
|
+
def show_image(image)
|
|
87
|
+
# a path: text without the PNG signature or NUL bytes, naming a file
|
|
88
|
+
if image.is_a?(String) && !image.b.start_with?("\x89PNG".b) && !image.include?("\0") && File.file?(image)
|
|
89
|
+
ChunkyBacon::Opener.open(File.expand_path(image))
|
|
90
|
+
return nil
|
|
91
|
+
end
|
|
92
|
+
bytes = if image.respond_to?(:to_blob)
|
|
93
|
+
image.to_blob
|
|
94
|
+
elsif image.respond_to?(:to_data_url)
|
|
95
|
+
image.to_data_url.split(",", 2).last.unpack1("m0")
|
|
96
|
+
else
|
|
97
|
+
image.to_s.b
|
|
98
|
+
end
|
|
99
|
+
path = ChunkyBacon.save(ChunkyBacon.next_file("chunky-image", "png"), bytes)
|
|
100
|
+
ChunkyBacon::Opener.open(path)
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# A PDF: a file's path, a Prawn::Document or HexaPDF::Document/Composer,
|
|
105
|
+
# or the PDF itself as a String.
|
|
106
|
+
def show_pdf(pdf)
|
|
107
|
+
pdf = pdf.document if defined?(HexaPDF::Composer) && pdf.is_a?(HexaPDF::Composer)
|
|
108
|
+
if pdf.is_a?(String) && !pdf.b.start_with?("%PDF")
|
|
109
|
+
ChunkyBacon::Opener.open(File.expand_path(pdf))
|
|
110
|
+
return nil
|
|
111
|
+
end
|
|
112
|
+
bytes = if pdf.is_a?(String)
|
|
113
|
+
pdf
|
|
114
|
+
elsif pdf.respond_to?(:render)
|
|
115
|
+
pdf.render
|
|
116
|
+
else
|
|
117
|
+
StringIO.new("".b).tap { |io| pdf.write(io) }.string
|
|
118
|
+
end
|
|
119
|
+
path = ChunkyBacon.save(ChunkyBacon.next_file("chunky-document", "pdf"), bytes)
|
|
120
|
+
ChunkyBacon::Opener.open(path)
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# What the page offered as a download: the data saved as +name+ in the
|
|
125
|
+
# program's folder. download_file "notizen.txt" names a file that is
|
|
126
|
+
# there already.
|
|
127
|
+
def download_file(data, name = nil)
|
|
128
|
+
if name.nil?
|
|
129
|
+
path = File.expand_path(data.to_s, ChunkyBacon.output_dir)
|
|
130
|
+
raise Errno::ENOENT, data.to_s unless File.file?(path)
|
|
131
|
+
|
|
132
|
+
ChunkyBacon.say "#{ChunkyBacon.relative(path)} is ready (#{File.size(path)} B)"
|
|
133
|
+
return nil
|
|
134
|
+
end
|
|
135
|
+
data = data.to_blob if data.respond_to?(:to_blob)
|
|
136
|
+
ChunkyBacon.save(File.expand_path(name.to_s, ChunkyBacon.output_dir), data.to_s.b)
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Starts the Rack app (a Sinatra/Roda class or anything with #call) on
|
|
141
|
+
# this computer and opens it in the browser. The program goes on; once
|
|
142
|
+
# it has finished, the server keeps running until Ctrl+C.
|
|
143
|
+
def show_browser(app, path = "/")
|
|
144
|
+
server = ChunkyBacon::Server.new(app).start
|
|
145
|
+
if ChunkyBacon.servers.empty? && ChunkyBacon.keep_serving
|
|
146
|
+
at_exit { ChunkyBacon.serve_until_interrupt }
|
|
147
|
+
end
|
|
148
|
+
ChunkyBacon.servers << server
|
|
149
|
+
ChunkyBacon.say "#{app.respond_to?(:name) && app.name ? app.name : "the app"} runs at #{server.url(path)}"
|
|
150
|
+
ChunkyBacon::Opener.open(server.url(path))
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# A GET against a Rack app without any server, for experiments and
|
|
155
|
+
# checks: status, body = mock_get(MyApp, "/hello")
|
|
156
|
+
def mock_get(app, path)
|
|
157
|
+
status, _headers, body = ChunkyBacon::RackEnv.call(app, ChunkyBacon::RackEnv.for(path))
|
|
158
|
+
[status, body]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# An IRB session, here and now (at the top level, like the page's fresh
|
|
162
|
+
# one). `exit` ends it, and the program carries on.
|
|
163
|
+
def show_irb
|
|
164
|
+
require "irb"
|
|
165
|
+
TOPLEVEL_BINDING.irb
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# The files in the program's folder, as the page's file explorer showed
|
|
170
|
+
# the simulated ones.
|
|
171
|
+
def show_files
|
|
172
|
+
base = ChunkyBacon.output_dir
|
|
173
|
+
puts "#{File.basename(File.expand_path(base))}/"
|
|
174
|
+
entries = Dir.glob("**/*", base: base).reject { |p| p.split("/").any? { |part| part.start_with?(".") } }.sort
|
|
175
|
+
entries.first(200).each do |entry|
|
|
176
|
+
depth = entry.count("/")
|
|
177
|
+
full = File.join(base, entry)
|
|
178
|
+
name = File.basename(entry)
|
|
179
|
+
puts File.directory?(full) ? "#{" " * (depth + 1)}#{name}/" : "#{" " * (depth + 1)}#{name} (#{File.size(full)} B)"
|
|
180
|
+
end
|
|
181
|
+
puts " ... #{entries.size - 200} more" if entries.size > 200
|
|
182
|
+
nil
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Runs the Minitest tests defined so far and clears them, like the page.
|
|
186
|
+
# (In a file of its own, `require "minitest/autorun"` does the same.)
|
|
187
|
+
def run_tests
|
|
188
|
+
require "minitest"
|
|
189
|
+
result = Minitest.run([])
|
|
190
|
+
Minitest::Runnable.runnables.clear
|
|
191
|
+
result
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def show_three(*)
|
|
195
|
+
raise ChunkyBacon::NotHere, "show_three needs the course page for now - three-rb draws with three.js " \
|
|
196
|
+
"in the browser. On your computer, three-rb's own examples show the way."
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def show_shoes(**, &)
|
|
200
|
+
raise ChunkyBacon::NotHere, "On your computer, Shoes apps run with Scarpe: gem install scarpe, put " \
|
|
201
|
+
"Shoes.app do ... end in a file and start it with: scarpe app.rb"
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
module Kernel
|
|
207
|
+
include ChunkyBacon::Helpers
|
|
208
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChunkyBacon
|
|
4
|
+
# Opens a file or a URL with whatever the system uses for it - the image
|
|
5
|
+
# viewer, the PDF reader, the browser. CHUNKYBACON_OPEN=0 turns it off
|
|
6
|
+
# (tests, CI, a machine without a desktop); the file is saved either way.
|
|
7
|
+
module Opener
|
|
8
|
+
def self.enabled?
|
|
9
|
+
ENV["CHUNKYBACON_OPEN"] != "0"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# true when a viewer was started
|
|
13
|
+
def self.open(target)
|
|
14
|
+
return false unless enabled?
|
|
15
|
+
|
|
16
|
+
pid = Process.spawn(*command(target.to_s), out: File::NULL, err: File::NULL)
|
|
17
|
+
Process.detach(pid)
|
|
18
|
+
true
|
|
19
|
+
rescue SystemCallError
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.command(target)
|
|
24
|
+
if Gem.win_platform?
|
|
25
|
+
# `start` is a cmd built-in; its first quoted argument is a window
|
|
26
|
+
# title. cmd reads & | < > ^ as operators unless escaped - Ruby only
|
|
27
|
+
# quotes an argument that has a space, and inside quotes the escape
|
|
28
|
+
# would be taken literally.
|
|
29
|
+
target = target.gsub(/[&|<>^]/) { |c| "^#{c}" } unless target.include?(" ")
|
|
30
|
+
["cmd", "/c", "start", "", target]
|
|
31
|
+
elsif RUBY_PLATFORM.include?("darwin")
|
|
32
|
+
["open", target]
|
|
33
|
+
else
|
|
34
|
+
["xdg-open", target]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module ChunkyBacon
|
|
6
|
+
# Rack environments without a Rack server - what mock_get hands an app,
|
|
7
|
+
# and what ChunkyBacon::Server builds from a real HTTP request. Mirrors the
|
|
8
|
+
# course page's RackPlayground (html/rack_playground.rb), so an app answers
|
|
9
|
+
# here as it does in the lesson.
|
|
10
|
+
module RackEnv
|
|
11
|
+
# "speisekarte", "/a?b=1", "http://localhost/x" -> "/speisekarte" etc.
|
|
12
|
+
def self.normalize(path)
|
|
13
|
+
"/" + path.to_s.sub(%r{\Ahttps?://[^/]*}, "").sub(%r{\A/+}, "")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.for(path, method: "GET", headers: {}, body: "", host: "localhost", port: 80)
|
|
17
|
+
path, query = normalize(path).split("?", 2)
|
|
18
|
+
env = {
|
|
19
|
+
"REQUEST_METHOD" => method.to_s.upcase,
|
|
20
|
+
"SCRIPT_NAME" => "",
|
|
21
|
+
"PATH_INFO" => path,
|
|
22
|
+
"QUERY_STRING" => query.to_s,
|
|
23
|
+
"SERVER_NAME" => host,
|
|
24
|
+
"SERVER_PORT" => port.to_s,
|
|
25
|
+
"HTTP_HOST" => port.to_i == 80 ? host : "#{host}:#{port}",
|
|
26
|
+
"SERVER_PROTOCOL" => "HTTP/1.1",
|
|
27
|
+
"rack.url_scheme" => "http",
|
|
28
|
+
"rack.input" => StringIO.new(body.to_s.b),
|
|
29
|
+
"rack.errors" => $stderr
|
|
30
|
+
}
|
|
31
|
+
headers.each do |name, value|
|
|
32
|
+
key = name.to_s.upcase.tr("-", "_")
|
|
33
|
+
case key
|
|
34
|
+
when "CONTENT_TYPE", "CONTENT_LENGTH" then env[key] = value.to_s
|
|
35
|
+
else env["HTTP_#{key}"] = value.to_s
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
env
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Calls the app; returns [status, headers, body as one String].
|
|
42
|
+
def self.call(app, env)
|
|
43
|
+
status, headers, body = app.call(env)
|
|
44
|
+
chunks = []
|
|
45
|
+
body.each { |chunk| chunks << chunk.to_s }
|
|
46
|
+
body.close if body.respond_to?(:close)
|
|
47
|
+
[status.to_i, headers.respond_to?(:to_h) ? headers.to_h : headers, chunks.join]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
|
|
5
|
+
module ChunkyBacon
|
|
6
|
+
# The smallest web server that lets a browser click through a Rack app -
|
|
7
|
+
# what show_browser starts on a computer, where the lesson page had its
|
|
8
|
+
# mini browser. One app, 127.0.0.1 only (nothing outside this computer
|
|
9
|
+
# reaches it), one request at a time, no keep-alive. For a real app, use a
|
|
10
|
+
# real server: rackup with puma, falcon, webrick.
|
|
11
|
+
class Server
|
|
12
|
+
REASONS = { 200 => "OK", 201 => "Created", 204 => "No Content", 301 => "Moved Permanently",
|
|
13
|
+
302 => "Found", 303 => "See Other", 304 => "Not Modified", 400 => "Bad Request",
|
|
14
|
+
401 => "Unauthorized", 403 => "Forbidden", 404 => "Not Found",
|
|
15
|
+
405 => "Method Not Allowed", 422 => "Unprocessable Content",
|
|
16
|
+
500 => "Internal Server Error" }.freeze
|
|
17
|
+
|
|
18
|
+
attr_reader :app, :port
|
|
19
|
+
|
|
20
|
+
# port 0: whichever port is free
|
|
21
|
+
def initialize(app, port: 0)
|
|
22
|
+
@app = app
|
|
23
|
+
@tcp = TCPServer.new("127.0.0.1", port)
|
|
24
|
+
@port = @tcp.addr[1]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def url(path = "/")
|
|
28
|
+
"http://127.0.0.1:#{port}#{RackEnv.normalize(path)}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def start
|
|
32
|
+
@thread = Thread.new do
|
|
33
|
+
loop { handle(@tcp.accept) }
|
|
34
|
+
rescue IOError, Errno::EBADF
|
|
35
|
+
nil # stopped
|
|
36
|
+
end
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def join
|
|
41
|
+
@thread&.join
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def stop
|
|
45
|
+
@tcp.close unless @tcp.closed?
|
|
46
|
+
@thread&.join(1)
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def handle(socket)
|
|
53
|
+
line = socket.gets
|
|
54
|
+
return if line.nil?
|
|
55
|
+
|
|
56
|
+
method, target = line.split(" ", 3)
|
|
57
|
+
headers = {}
|
|
58
|
+
while (header = socket.gets) && !header.strip.empty?
|
|
59
|
+
name, value = header.split(":", 2)
|
|
60
|
+
headers[name.strip.downcase] = value.to_s.strip
|
|
61
|
+
end
|
|
62
|
+
body = headers["content-length"] ? socket.read(headers["content-length"].to_i).to_s : ""
|
|
63
|
+
env = RackEnv.for(target, method: method, headers: headers, body: body, host: "127.0.0.1", port: port)
|
|
64
|
+
status, response_headers, response_body = respond(env)
|
|
65
|
+
write(socket, status, response_headers, response_body)
|
|
66
|
+
rescue IOError, SystemCallError
|
|
67
|
+
nil # the browser went away
|
|
68
|
+
ensure
|
|
69
|
+
socket.close unless socket.closed?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The app's answer - an error in the app becomes a 500 page that names
|
|
73
|
+
# it, like the error below a cell.
|
|
74
|
+
def respond(env)
|
|
75
|
+
RackEnv.call(app, env)
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
warn "#{e.class}: #{e.message}"
|
|
78
|
+
[500, { "content-type" => "text/plain; charset=utf-8" }, "#{e.class}: #{e.message}\n"]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def write(socket, status, headers, body)
|
|
82
|
+
socket.write("HTTP/1.1 #{status} #{REASONS.fetch(status, "Status")}\r\n")
|
|
83
|
+
headers.each do |name, value|
|
|
84
|
+
next if %w[connection content-length transfer-encoding].include?(name.to_s.downcase)
|
|
85
|
+
|
|
86
|
+
Array(value).each { |v| v.to_s.split("\n").each { |part| socket.write("#{name}: #{part}\r\n") } }
|
|
87
|
+
end
|
|
88
|
+
socket.write("content-length: #{body.bytesize}\r\nconnection: close\r\n\r\n")
|
|
89
|
+
socket.write(body)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/chunky_bacon.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# chunky_bacon: the companion gem of "Ruby lernen mit Chunky Bacon", the Ruby
|
|
4
|
+
# course that runs in the browser (https://github.com/Largo/chunkybacon).
|
|
5
|
+
#
|
|
6
|
+
# On your own computer, `require "chunky_bacon"` gives a program the helpers
|
|
7
|
+
# the course's notebook cells and workshop have - show_image, show_browser,
|
|
8
|
+
# download_file and friends - so a program written in the browser runs
|
|
9
|
+
# unchanged with plain Ruby (or with `chunkybacon run`, which loads them for
|
|
10
|
+
# you). In the browser the page brings its own helpers; there the gem only
|
|
11
|
+
# adds the fox.
|
|
12
|
+
require_relative "chunky_bacon/version"
|
|
13
|
+
require_relative "chunky_bacon/fox"
|
|
14
|
+
|
|
15
|
+
module ChunkyBacon
|
|
16
|
+
# A helper that needs the browser (for now), called on a computer.
|
|
17
|
+
class NotHere < StandardError; end
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# true inside the course's page (ruby.wasm), false on a computer
|
|
21
|
+
def browser?
|
|
22
|
+
RUBY_PLATFORM.start_with?("wasm")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# where show_image, show_pdf and download_file put their files: the
|
|
26
|
+
# folder the program runs in, like the downloads below a cell
|
|
27
|
+
attr_writer :output_dir
|
|
28
|
+
|
|
29
|
+
def output_dir
|
|
30
|
+
@output_dir || Dir.pwd
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
unless ChunkyBacon.browser?
|
|
36
|
+
require_relative "chunky_bacon/opener"
|
|
37
|
+
require_relative "chunky_bacon/rack_env"
|
|
38
|
+
require_relative "chunky_bacon/server"
|
|
39
|
+
require_relative "chunky_bacon/helpers"
|
|
40
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: chunky_bacon
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andi Idogawa
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: The companion gem of "Ruby lernen mit Chunky Bacon", an interactive Ruby
|
|
13
|
+
course that runs in the browser. require "chunky_bacon" gives a program the course's
|
|
14
|
+
helpers - show_image, show_pdf, show_browser, download_file, mock_get, show_irb,
|
|
15
|
+
show_files, run_tests, install_gem - so code written in the course runs unchanged
|
|
16
|
+
with plain Ruby; `chunkybacon run` starts a program with them loaded. Pure Ruby,
|
|
17
|
+
no dependencies.
|
|
18
|
+
email:
|
|
19
|
+
- web@idogawa.com
|
|
20
|
+
executables:
|
|
21
|
+
- chunkybacon
|
|
22
|
+
extensions: []
|
|
23
|
+
extra_rdoc_files: []
|
|
24
|
+
files:
|
|
25
|
+
- CHANGELOG.md
|
|
26
|
+
- LICENSE
|
|
27
|
+
- LICENSE-ASSETS
|
|
28
|
+
- README.md
|
|
29
|
+
- exe/chunkybacon
|
|
30
|
+
- lib/chunky_bacon.rb
|
|
31
|
+
- lib/chunky_bacon/cli.rb
|
|
32
|
+
- lib/chunky_bacon/fox.rb
|
|
33
|
+
- lib/chunky_bacon/fox.txt
|
|
34
|
+
- lib/chunky_bacon/helpers.rb
|
|
35
|
+
- lib/chunky_bacon/opener.rb
|
|
36
|
+
- lib/chunky_bacon/rack_env.rb
|
|
37
|
+
- lib/chunky_bacon/server.rb
|
|
38
|
+
- lib/chunky_bacon/version.rb
|
|
39
|
+
homepage: https://github.com/Largo/chunkybacon
|
|
40
|
+
licenses:
|
|
41
|
+
- MIT
|
|
42
|
+
- CC-BY-SA-4.0
|
|
43
|
+
metadata:
|
|
44
|
+
homepage_uri: https://github.com/Largo/chunkybacon
|
|
45
|
+
source_code_uri: https://github.com/Largo/chunkybacon/tree/main/gem/chunky_bacon
|
|
46
|
+
changelog_uri: https://github.com/Largo/chunkybacon/blob/main/gem/chunky_bacon/CHANGELOG.md
|
|
47
|
+
rubygems_mfa_required: 'true'
|
|
48
|
+
rdoc_options: []
|
|
49
|
+
require_paths:
|
|
50
|
+
- lib
|
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '3.1'
|
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
requirements: []
|
|
62
|
+
rubygems_version: 4.0.20
|
|
63
|
+
specification_version: 4
|
|
64
|
+
summary: The Chunky Bacon Ruby course's helpers on your own computer - and a fox that
|
|
65
|
+
shouts.
|
|
66
|
+
test_files: []
|