Ruby Regular Expression Editor

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:

loading ruby.wasm
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:

inline Ruby
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:

regex matching
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:

exposing to JavaScript
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:

match highlighting
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), and x (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 .scan or .gsub call 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.

Try it out.