I have always liked Rubular. It is simple, focused, and does exactly what it says. You paste in a regex, you paste in a string, and you see what matches. I have used it for years.
But Rubular, as of this post, is still using Ruby 2.5.9. I thought it would be interesting to have a more modern version. Also, since this is a statically generated blog I wanted to see if I could build something similar that runs entirely in the browser.
ruby.wasm
ruby.wasm is a build of CRuby compiled to WebAssembly. You can load it with a single script tag:
1 2 | <script src="https://cdn.jsdelivr.net/npm/@ruby/4.0-wasm-wasi@2.8.1/dist/browser.script.iife.js"></script> |
Once loaded, you can write Ruby directly in the page:
1 2 3 4 5 | <script type="text/ruby"> require "js" JS.global[:document].querySelector("#output")[:textContent] = "Hello from Ruby" </script> |
The js gem bridges Ruby and JavaScript. You can read DOM elements, set properties, and attach event listeners. The Ruby code runs in the browser with no server involved.
The regex engine
The core of the editor is a Ruby function that takes a pattern, flags, and test string, then returns structured match data as JSON:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | def run_regex(pattern_str, flags_str, test_str, sub_str) opts = 0 opts |= Regexp::IGNORECASE if flags_str.include?("i") opts |= Regexp::MULTILINE if flags_str.include?("m") opts |= Regexp::EXTENDED if flags_str.include?("x") regex = Regexp.new(pattern_str, opts) matches = [] offset = 0 while (m = regex.match(test_str, offset)) match_data = { full: m[0], index: m.begin(0), length: m[0].length, groups: [] } (1...m.size).each do |i| group = {number: i, value: m[i]} if i - 1 < m.names.length && m.names[i - 1] group[:name] = m.names[i - 1] end match_data[:groups] << group end matches << match_data new_offset = m.end(0) new_offset += 1 if new_offset == offset break if new_offset > test_str.length offset = new_offset end {error: nil, matches: matches}.to_json end |
This is real Regexp. Not a JavaScript approximation. Ruby-specific features like \A, \z, \h, \K, \R, and \p{L} all work because it is the actual Ruby regex engine running in the browser.
Connecting Ruby to the DOM
The function is exposed to JavaScript through a Proc:
1 2 3 4 | JS.global[:rubyRegex] = Proc.new { |pattern, flags, text, sub| run_regex(pattern.to_s, flags.to_s, text.to_s, sub.to_s) } |
On the JavaScript side, a thin layer reads the inputs, calls window.rubyRegex(), and renders the results. Match positions come back as indices, so building highlighted HTML is straightforward:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | function buildHighlightedHTML(text, matches) { let result = ''; let lastIndex = 0; for (const match of matches) { const start = match.index; const end = start + match.length; result += escapeHTML(text.slice(lastIndex, start)); result += '<mark>' + escapeHTML(match.full) + '</mark>'; lastIndex = end; } result += escapeHTML(text.slice(lastIndex)); return result; } |
Input events fire with a 100ms debounce, which keeps things responsive without overwhelming the WASM runtime.
The tool
The editor has a few features worth mentioning:
- Flag toggles for
i(case insensitive),m(multiline), andx(extended mode) - A gsub mode that shows the substitution result when toggled on
- Generated Ruby code with syntax highlighting, so you can copy the exact
.scanor.gsubcall into your project - Permalinks that encode the regex, test string, and flags in the URL for sharing
The loading time for ruby.wasm is noticeable—a few seconds on first visit while the WASM binary downloads. After that, everything runs instantly.