Skip to content

About

Learn Ruby in the browser with Chunky Bacon - interactive de/en notebook course on ruby.wasm

Resources

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

Ruby lernen mit Chunky Bacon 🦊🥓

Chunky Bacon: learn Ruby right in your browser

Learn Ruby in your browser — an interactive, notebook-style course in German, English, Japanese, French and Italian. No setup: Ruby itself runs client-side via ruby.wasm.

What's inside

  • Lessons from puts "Hallo, Welt!" through strings, map and select, nil, truthiness and symbols, patterns drawn with loops, sorting and binary search, time and dates, methods, secret codes (Caesar and ROT13), reading error messages, recursion (up to the Towers of Hanoi), classic puzzles (primes, anagrams, Collatz, Goldbach), Struct and Data, classes, inheritance, modules, IRB and reading the Ruby documentation, to gems, HTML parsing with Nokogiri, exact arithmetic with BigDecimal, web routing with Sinatra and Roda, Ruby on Rails itself booted in the page (routes, controllers and views, Active Record on SQLite, a page from the database with resources, forms, all seven REST actions, has_many and belongs_to), 3D graphics, tables, frames and colours for the terminal with the TTY toolkit, sketches that move and follow the mouse with Processing, test data that looks real with Faker, ERB templates checked by Herb (its C parser as WebAssembly), PowerPoint decks with ruby_pptx, PDFs with Prawn and HexaPDF, JPEG photos with pure_jpeg, pandas, SymPy, NumPy, charts with matplotlib and machine learning with scikit-learn from Ruby through PyCall, machine learning in Ruby itself with Rumale (a postcode you write on a letter, read by a nearest-neighbours classifier), tables, statistics and networks with SciRuby's gems (data frames with Daru, the bell curve with distribution, integration and minimization, charts drawn by Rubyvis, the fastest train across Switzerland with networkx.rb), a SQLite database with Sequel, shipping a program as an .exe with OCRAN, a look at the Ruby community (RubyKaigi, weird code, how IRB reads code), music computed in plain Ruby (samples as an Array, a WAV file built with pack, chords and a drum beat - played below the cell), Snake built in five steps and played with the arrow keys below the cell, games with ruby2d whose code opens a real window on your computer too, the other Rubies (JRuby, TruffleRuby, mruby, PicoRuby, IronRuby on .NET, RubyMotion, DragonRuby and more), a lesson whose cells and IRB run on PicoRuby.wasm instead of CRuby, Android apps in Ruby with Ruboto on a simulated phone (JRuby in the browser, Ruboto's own Ruby, a photo frame), Ruby on a simulated Raspberry Pi Pico 2 W (LEDs, buttons, sensors, a robot, an e-paper display, Wi-Fi and Bluetooth, then a real board), Ruby compiled ahead of time by Matz's Spinel
    • to C, then to WebAssembly, in the browser - Swiss QR-bills written with qr-bills into a PDF and read back from it with swiss_qr and qr_reader, and a project track that builds a small time tracker (with Time and Date, objects and copies, tests, pattern matching, a DSL and a web front end).
  • Notebook UI: lessons interleave text with runnable CodeMirror cells (Shift+Enter). All cells of a lesson share one binding, and every cell shows its last expression as => … — puts is never required. Run a cell first and the cells above it that have not run yet run before it, quietly ("Ran cells 1–3 above first").
  • Errors explained kindly: when a cell fails, a short explanation in the lesson's language (German, English, Japanese) says what went wrong and what to write instead, with the line and a caret under the culprit (6 x 7 → "Ruby multiplies with *"; a missing end names the def that lost it; fox["name"] on a hash with symbol keys suggests fox[:name]). Ruby's own message stays one click away. 44 rules, checked against 70 typical beginner mistakes drawn from the exercises. A misspelt name also gets Ruby's own suggestion ("💡 Did you mean name?"), in the learner's language.
  • Hints in steps, never stuck: every exercise has a 💡 button beside ▶. The first click gives a nudge (where to start, which method), the next the detailed hint, and then a finished solution to compare with - one click puts it into the cell. A failed check reveals the next hint too, Chunky says it; an error points at the explanation and at 💡. Every solution shown is one the test harness verifies.
  • Bonus exercises: most Basics lessons end with a second, harder task in a "Bonus" box - checked and cheered like the first, with its own hints, but the lesson's tick comes from the main task alone.
  • Ruby cheat sheet: one click in the header opens the course's Ruby on one page - 17 topics, each with the lessons that teach it and links into the official documentation, searchable. On a wide screen it stands beside the lesson, so you read it while typing; on a phone it slides over the page.
  • The documentation, everywhere: every lesson ends with a "📖 Look it up" box linking the official docs of what it used (docs.ruby-lang.org; Rurima, the Japanese reference, for Japanese), and lesson doku teaches how to read them.
  • Lesson sidebar: the lessons in four groups (basics, side trips, Ruby on a Pico, the timelog track) with done counts, ticks and a bacon progress strip, searchable and foldable; it can be put away, and on a phone it is a drawer.
  • Keyboard and screen readers: a skip link past the index, editors that Escape, then Tab leaves, Run buttons named with their cell that keep the focus, a status line that reads out what a run printed and what the fox says, the focus on the new lesson's heading after every lesson change, a modal drawer on a phone, named widgets (IRB, mini browser, 3D, a game you can leave with Tab, the step-by-step slider) and contrast at WCAG AA (audit: experiments/10-accessibility).
  • Live runs: a cell runs by itself a second after you stop typing, as long as the code parses - a rehearsal that keeps no file it writes, installs only gems already in the cache, fetches nothing from the web and is stopped after a second; ▶ runs the code for real. The foxes only speak up when an exercise passes. On in the lessons, off in the workshop (the ⚡ Live switch beside ▶); a lesson whose cells compute too much for it ("live": false, the music lesson) turns it off and says why.
  • In-browser gem installer: pure-Ruby gems install at runtime (install_gem "chunky_png"), fetched from a local cache or from rubygems.org through a same-origin nginx proxy, and unpacked onto the wasm filesystem (writable memory) with their declared load paths on $LOAD_PATH - so require, require_relative, autoload, __dir__ and data files next to lib/ behave as on disk.
  • Nokogiri in the browser: the cache holds nokogiri-pure - Nokogiri 1.19.4 with its C extension, libxml2, libxslt and gumbo ported to Ruby - built under the name nokogiri, so gems that depend on nokogiri (loofah, sanitize, rails-html-sanitizer, premailer, feedjira, rubyXL, roo, caxlsx, reverse_markdown …) install and run too. Other native gems fail with a friendly explanation of the wasm limitation that names the gem with the C code (often a dependency), unless it is built into the wasm image (json, date, openssl …), has a pure-Ruby stand-in (Numo, the arrays under Rumale: html/numo_narray.rb; sqlite3, on sql.js - SQLite in WebAssembly, loaded only when needed - so Sequel's own SQLite adapter runs, and Sequel.sqlite("x.db") is a real SQLite file you can download: html/sqlite3_sqljs.rb; Processing, which draws through OpenGL: html/processing.rb; ruby2d, whose own Ruby runs here with its C extension on SDL3 written in Ruby: html/ruby2d.rb; in the cache, SUBSTITUTES: a dependency on bigdecimal installs bigdecimal-pure, which unblocks activesupport, liquid, prawn, dry-types …) or the gem that wants it can do without it (OPTIONAL_NATIVE_DEPS: ruby_pptx falls back to REXML).
  • Downloads: any file a cell writes - deck.save("chunky.pptx"), File.write("notes.txt", …) - appears below the cell as a download link; download_file(data, "name") offers data that never went through a file, show_pdf puts a PDF in the browser's own viewer below the cell, and show_audio a sound (a WAV, a file or an Array of samples) in the browser's player, with a picture of its wave.
  • Workshop: beside the lessons, a small IDE for your own multi-file programs - require_relative between files, File.read/File.write on the project, input for gets, pictures, PDFs and sounds it writes previewed and kept, SQLite databases kept with the project, files renamable - with every widget below available.
  • Your progress stays yours: nothing is stored on a server. Progress, code and workshop files live in the browser and can be saved as a progress file (download, load again, merged key by key) or - in Chrome and Edge over https - into a connected folder, where workshop files are real files.
  • Offline, if you like: one click keeps the whole course on your device (about 104 MB, or 52 MB with Python left out - a checkbox), and it opens and runs without a connection - lessons, cells, the bundled gems, Python. Online it always loads the current version, and the copy updates itself.
  • Python next to Ruby: the PyCall lessons run real pandas, SymPy, NumPy, matplotlib (its charts drawn as SVG below the cell) and scikit-learn - Pyodide, CPython in WebAssembly, loaded only for those lessons, each with just the packages it imports - through a small bridge with the pycall gem's API (html/pycall.rb), so their code runs unchanged with the real gem.
  • Data frames as tables: a cell that ends with a Daru DataFrame or Vector shows it as a table, the way a pandas DataFrame shows (lesson 30) - the first 30 rows, numbers to six places.
  • Programs from a cell (optional, the Spinel lesson): a 📦 button builds the cell's Ruby as a standalone Windows .exe or Linux program with Spinel, compiled in the browser tab by clang and lld running as WebAssembly. The toolchain (45 MB) is built on a laptop and copied to the host: tools/compile/build.sh, publish.sh, see docs/HANDOVER.md §6d.
  • Interactive widgets: show_irb (a real IRB terminal with _, multi-line input, and authentic prompts), show_browser (a fake browser window that speaks Rack directly to your Sinatra, Roda or Rails app - links, forms, cookies and redirects as in a browser, with the app's log under the page), show_image (inline pictures: PNGs from chunky_png, JPEGs from pure_jpeg, SVG, charts from Rubyvis; alt: says what they show), show_three (a WebGL stage for scenes built with three-rb, optionally animated per frame and orbitable with the mouse), and show_letter (an envelope to write a postcode on with mouse or finger; a Ruby block reads it).
  • Objects as boxes and arrows: show_objects(a: a, b: b) or show_objects(binding) draws the names and the objects behind them, an arrow for every reference, like Python Tutor - so b = a vs a.dup, a shallow copy of a hash, or two cats with the same name are seen, not just told (lessons arrays, hashes, klassen). Pure Ruby to SVG; a screen reader hears the arrows in words.
  • Step by step through a cell: in the basics lessons with plain Ruby (3-12), ⏯ beside ▶ runs a cell and records it line by line - then a slider walks through the run: the line about to run marked in the editor, the variables of the cell, a method or a loop's pass (what just changed in yellow), the output so far, and Chunky saying what happens ("Line 2 is next - for the 3rd time"). The arrow keys walk it, a screen reader hears each step; an edit clears it.
  • Turtle graphics with Chunky as the turtle: turtle { 4.times { forward 100; right 90 } } draws below the cell, as an SVG that draws itself while Chunky runs along the path (lesson turtle: loops, methods with parameters, recursion up to a Koch snowflake). The exercise is graded on what was drawn - edges, corners, closed or not - not on pixels.
  • A terminal below each cell: colours from ANSI escape codes (pastel, test runners) show as colours, and box-drawing characters (┌─┐, from tty-table, tty-box) are as wide as the code font's letters, so tables line up.
  • Processing sketches: setup and draw from the processing gem run below the cell, about 60 frames a second, and hear the mouse and the keys - the gem's API in pure Ruby, painted on a canvas.
  • Games in a cell: show_game(width: 20, height: 15) { |g| ... } draws a grid of cells below the cell; g.every(0.15) { ... } and g.on_key(:left) { ... } make it move and steer (Chunky's Snake, lesson 41). The page runs the loop and calls Ruby only when a timer is due or a key came in; the game runs only while it has the focus (a click, or Tab and Space; Esc pauses, Tab leaves), a screen reader hears the score and the end of a round, and an endless loop in a round is stopped after a second. Checks play the game headless.
  • ruby2d games, unchanged: require "ruby2d", shapes, update do, on :key_held, show - lesson ruby2d runs real ruby2d 1.0 programs: the gem's own Ruby (vendored as it is, tools/vendor_ruby2d.rb) with the part it writes in C on SDL3 stood in for by html/ruby2d.rb, the window a canvas below the cell that the page drives like a game. The same file opens a real window with ruby game.rb on a computer. Shapes, colours, text, keys, mouse; not images, sprites, sound or gamepads.
  • A compiler in the browser: in lesson spinel a cell's plain Ruby is a program for Spinel, Matz's ahead-of-time compiler, which runs in the page as WebAssembly together with clang (YoWASP's, itself WebAssembly), driven by Ruby on PicoRuby.wasm in workers: Ruby becomes C, the C becomes a WebAssembly module, and the module runs below the cell - with the time of each step, the C to read, the module to download (wasmtime run -W exceptions=y main.wasm), and CRuby's output of the same code beside it, as the oracle spin test takes too. What an AOT compiler cannot do (eval of a String, method_missing) is refused with the line. show_spinel_irb is IRB on it: every line compiled with the ones before into a program of its own and run from the start, only the new line's output shown (lesson spinel, about 27 MB, loaded when it opens). Not in git: the deploy builds it (tools/build_spinel.rb, pinned in tools/spinel.json).
  • A Pico on the page: the hardware lessons run on PicoRuby.wasm with PicoRuby's own GPIO, PWM, ADC, I2C, SPI and Net::HTTP code over a simulated board, drawn below the cell as a Pico 2 W on a breadboard with the parts wired to it - press its button, move the light or the robot's box, read the e-paper, type to it over Bluetooth, or ■ reset it. Drivers sit in read-only tabs beside the code, as files in /lib do on a real Pico, and exercises are checked against what the board did.
  • _why's own cartoon foxes, from the poignant guide, cheer you on (CC BY-SA 2.5, traced to SVG and animated).

On your own computer: the chunky_bacon gem

gem/chunky_bacon gives a Ruby program on your own computer the course's helpers (show_image, show_objects, turtle, show_audio, show_browser, download_file, ...), so code from the lessons and the workshop runs unchanged. chunkybacon run starts a program with them loaded. gem/chunkybacon and gem/chunky-bacon are aliases: all three names install the same gem.

Embedding a cell

One runnable cell on another page - a blog post, a slide - with the course's Ruby, its output and its widgets:

<pre data-chunky data-gems="chunky_png" data-lang="en">
  words = %w[chunky bacon is chunky]
  words.tally
</pre>
<script src="https://letsruby.com/embed.js" defer></script>

embed.js turns every pre[data-chunky] into an iframe of the course's embed.html, as tall as the cell. Options on the <pre>: data-gems, data-lang (de, en, ja; else the page's language), data-load (visible, the default: Ruby loads when the cell scrolls into view; click: on the first ▶; eager), data-run="1" and aria-label (the iframe's title). The script's own data-base names the course's address; without it, it is where embed.js came from.

The code goes into the address's fragment (#code=…, compressed), so it never reaches the server. For an <iframe> by hand, or a "▶ Run" link in a README (GitHub drops iframes; the link opens the cell as a page of its own):

ruby tools/make_embed_url.rb example.rb --gems chunky_png --lang en   # --load click, --run, --base URL

Only pages on https://idogawa.com may put a cell in an iframe (frame-ancestors, nginx/default.conf); a plain link works from anywhere. The cell runs sandboxed, in an origin of its own: code from a link sees none of the course's progress, files or offline copy, although it comes from the course's host. Gems come from the course's cache only (the rubygems bridge serves the course's own pages). How it works: docs/HANDOVER.md §6k.

Architecture

For operations, internals and traps see docs/HANDOVER.md.

The page runs on two Rubies: PicoRuby.wasm (0.9 MB) draws everything you read within a fraction of a second, while CRuby's ruby.wasm (10 MB) loads behind it and runs the code - see docs/PICORUBY_SHELL.md. The PicoRuby lesson (41) runs its cells on a second PicoRuby.wasm, in a Web Worker - the same runtime, nothing more to download (docs/HANDOVER.md §6m).

The runtimes and libraries the page loads from its own server (ruby.wasm, PicoRuby.wasm, Pyodide, sql.js, Herb, the gem cache) are kept in the repository, because a push is the deploy; each is written by a tool under tools/ that downloads a pinned version, checks it against the registry's checksum and can say whether the files on disk are still those (--check) - §9a of the handover.

Built on the same foundation as BrowserRubyKoans (koans.idogawa.com): browser.script.iife.js + ruby-app.wasm with app logic written in Ruby (html/main.rb) via the JS bridge. Lesson content lives in html/lessons.js; the gem loader in html/browser_gems.rb unpacks .gem files in Ruby onto the wasm filesystem, and hooks require for shims of stdlib that cannot load in WASI (socket, net/http over the browser's fetch, resolv).

three.js is vendored under html/assets/three/ and imported lazily (window.ensureThree in index.html) only by lessons whose cells call show_three - three-rb builds the scene graph in Ruby and looks the library up as globalThis.THREE. All stages on the page share one Three::Backends::ThreeJS: that backend caches the three.js objects it built and afterwards only pushes what changed, so a second one would rebuild an already-clean scene as bare defaults.

Run locally

docker compose up -d      # serves on port 8011

Lesson spinel (Spinel) needs its build in html/assets/spinel/, which is not in git: ruby tools/build_spinel.rb (Ruby 3.3+, no C compiler and no Node: clang is WebAssembly, run through the wasmtime gem; a few minutes the first time, afterwards a no-op until tools/spinel.json changes).

Or any static file server over html/ (the rubygems proxy then needs nginx, see nginx/default.conf). Lessons then live at /index.html#methoden. nginx, the dev server and server/ serve the landing page (welcome.html) at / and the course at /learn; an old /#methoden link goes on to the course. Without Docker, ruby tools/dev_server.rb (port 8011) serves html/ with nginx's bridges, headers and rules.

Permalinks and a backend (optional)

server/ is a small Roda app that serves the same html/ and gives every lesson a permalink - /de/methoden, /en/werkstatt - with its own title and link preview, plus room for backend code under /api:

cd server && bundle install && bundle exec puma -b tcp://127.0.0.1:8012 config.ru

The page notices it is served with permalinks and uses them; without the server everything works as before. With Docker: docker compose --profile server up -d (see docs/HANDOVER.md §7a for nginx).

Tests

cd test
node make_lessons_json.js # test/lessons.json for the Ruby harnesses
ruby check_harness.rb     # every lesson: starter fails, solutions pass
ruby lint_lessons.rb      # lessons.js content: de/en/ja/fr/it parity, references, Japanese rules, Prism, gems, counts
ruby lint_lessons_test.rb # the linter's fault-injection tests
ruby gems_harness.rb      # gem installer, sinatra + roda offline
ruby shell/run.rb         # the page shell (PicoRuby code) under Minitest
ruby autorun_test.rb      # live runs: what may run, the time limit
ruby earlier_cells_test.rb # which cells above a cell run first
ruby ansi_test.rb         # terminal colours in a cell's output
ruby object_graph_test.rb # show_objects: the walk, the SVG, the alt text
ruby turtle_test.rb       # turtle graphics: the recorded path, the check helpers, the SVG
ruby game_test.rb         # show_game headless: the lesson's Snake by timer and keys, the copies checks play
ruby step_recorder_test.rb # ⏯'s recorder: steps, frames, hidden locals, caps, every cell of the stepper lessons
ruby friendly_errors_harness.rb --summary  # 70 beginner mistakes, each explained by the expected rule
ruby friendly_errors_robustness.rb         # the explanations never raise, never leave a %{...}
node browser_test.mjs     # Playwright end-to-end against port 8011
node progress_test.mjs    # progress file, workshop, connected folder
node boot_failure_test.mjs # what the page says when a runtime fails
node language_test.mjs    # ?lang=, last choice, browser languages, English
node live_test.mjs        # live runs in a lesson and in the workshop
node offline_test.mjs     # offline mode: the copy, offline, a deploy, turning it off
node spinel_test.mjs      # lesson `spinel` in a browser: Spinel compiling, IRB on it (after tools/build_spinel.rb)
ruby spinel_rb_test.rb    # spinel(code) without the page: CRuby's run, the shell's ui strings
ruby spinel_build_test.rb # the compiler worker's plain Ruby: --print-build, clang's command line, IRB's verdict
ruby server_test.rb       # the optional server (server/)
ruby dev_server_test.rb   # the bridges serve only the course's pages, the embed's headers; nginx says the same
node embed_test.mjs       # an embedded cell on another site (dev server with FRAME_ANCESTORS="http://blog.test:*")
node picoruby_test.mjs    # the PicoRuby lesson: cells and IRB on PicoRuby.wasm in a worker
node rails_test.mjs       # the Rails lessons: Rails boots from the cache, views, the log pane, Active Record on SQLite, a form sent (en, de, ja)
ruby ../tools/vendor_picoruby.rb --check # the PicoRuby runtime is the one PICORUBY_VERSION.txt records
ruby ../tools/build_embed_ui.rb --check   # html/embed-ui.js has lessons.js's ui strings
BASE=http://127.0.0.1:8012/ node permalink_test.mjs   # permalinks, against the server
cd ../gem/chunky_bacon && rake test   # the companion gem

Legal pages

The legal notice and privacy policy name whoever runs a deployment, so they are not in git: html/impressum.html and html/datenschutz.html are ignored and live only on the server. Copy the templates and fill in every yellow .todo:

cp html/impressum.example.html html/impressum.html
cp html/datenschutz.example.html html/datenschutz.html

The deploy hooks leave untracked files alone. Without the files the footer links to them disappear (html/legal_links.js); the privacy template lists what the course sends where, so check it again when a lesson starts talking to a new host.

License

Both licenses allow commercial use.

  • Code: MIT, © Andi Idogawa - the app, tools and tests.
  • Course content: CC BY-SA 4.0, © Andi Idogawa - the lesson and interface texts in html/lessons.js and the Chunky Bacon fox (html/assets/chunky.svg, and in ASCII in the gem, gem/chunky_bacon/lib/chunky_bacon/fox.txt). Credit the course and keep adapted versions under CC BY-SA. The code examples inside the lessons are also available under the MIT license, so you can use them in your own programs freely.
  • Third-party components (the ruby.wasm build, CodeMirror, three.js, the cached gems, the web fonts) keep their own licenses - see THIRD_PARTY_NOTICES.md.

The phrase "Chunky Bacon" is an homage to why's (poignant) guide to Ruby by why the lucky stiff — fondly remembered. The foxes on the page are his, from chapter 3 of the guide (CC BY-SA 2.5), traced to SVG in html/assets/why-foxes/, and on the social cards; the original Chunky drawing (html/assets/chunky.svg) stays with the companion gem. The look borrows the design language of Citroën's 2 CV Concept (redrawn, no Citroën marks): see docs/HANDOVER.md §10b.

About

Learn Ruby in the browser with Chunky Bacon - interactive de/en notebook course on ruby.wasm

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages