Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ source 'https://rubygems.org'
# Specify your gem's dependencies in js_regex.gemspec
gemspec

gem 'colorize'
gem 'debug'
gem 'gouteur', '~> 1.0'
gem 'mini_racer', '~> 0.16'
Expand Down
29 changes: 16 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ Add it to your gemfile or run
In Ruby:

```ruby
require 'js_regex'
require 'lang_regex'

ruby_hex_regex = /0x\h+/i

js_regex = JsRegex.new(ruby_hex_regex)
# To JS
js_regex = LangRegex::JsRegex.new(ruby_hex_regex)
# To PHP
php_regex = LangRegex::PhpRegex.new(ruby_hex_regex)

js_regex.warnings # => []
js_regex.source # => '0x[0-9A-F]+'
Expand Down Expand Up @@ -59,7 +62,7 @@ var regExp = new RegExp(jsonObj.source, jsonObj.options);
You might have noticed the empty `warnings` array in the example above:

```ruby
js_regex = JsRegex.new(ruby_hex_regex)
js_regex = LangRegex::JsRegex.new(ruby_hex_regex)
js_regex.warnings # => []
```

Expand All @@ -68,18 +71,18 @@ If this array isn't empty, that means that your Ruby regex contained some stuff
```ruby
advanced_ruby_regex = /(?<!fizz)buzz/

js_regex = JsRegex.new(advanced_ruby_regex)
js_regex = LangRegex::JsRegex.new(advanced_ruby_regex)
js_regex.warnings # => ["Dropped unsupported negative lookbehind '(?<!fizz)' at index 0 (requires at least `target: 'ES2018'`)"]
js_regex.source # => 'buzz'
```

There is also a strict initializer, `JsRegex::new!`, which raises a `JsRegex::Error` if there are incompatibilites. This is particularly useful if you use JsRegex to convert regex-like strings, e.g. strings entered by users, as a `JsRegex::Error` might also occur if the given regex is invalid:
There is also a strict initializer, `LangRegex::new!`, which raises a `LangRegex::Error` if there are incompatibilites. This is particularly useful if you use JsRegex to convert regex-like strings, e.g. strings entered by users, as a `LangRegex::Error` might also occur if the given regex is invalid:

```ruby
begin
user_input = '('
JsRegex.new(user_input)
rescue JsRegex::Error => e
LangRegex::JsRegex.new(user_input)
rescue LangRegex::Error => e
e.message # => "Premature end of pattern (missing group closing parenthesis)"
end
```
Expand All @@ -89,7 +92,7 @@ end
An `options:` argument lets you append options (a.k.a. "flags") to the output:

```ruby
JsRegex.new(/x/i, options: 'g').to_h
LangRegex::JsRegex.new(/x/i, options: 'g').to_h
# => { source: 'x', options: 'gi' }
```

Expand All @@ -101,13 +104,13 @@ A `target:` argument can be given to target more recent versions of JS and unloc

```ruby
# ES2015 and greater use the u-flag to avoid lengthy escape sequences
JsRegex.new(/😋/, target: 'ES2009').to_s # => "/(?:\\uD83D\\uDE0B)/"
JsRegex.new(/😋/, target: 'ES2015').to_s # => "/😋/u"
JsRegex.new(/😋/, target: 'ES2018').to_s # => "/😋/u"
LangRegex::JsRegex.new(/😋/, target: 'ES2009').to_s # => "/(?:\\uD83D\\uDE0B)/"
LangRegex::JsRegex.new(/😋/, target: 'ES2015').to_s # => "/😋/u"
LangRegex::JsRegex.new(/😋/, target: 'ES2018').to_s # => "/😋/u"

# ES2018 adds support for lookbehinds, properties etc.
JsRegex.new(/foo\K\p{ascii}/, target: 'ES2015').to_s # => "/foo[\x00-\x7f]/"
JsRegex.new(/foo\K\p{ascii}/, target: 'ES2018').to_s # => "/(?<=foo)\p{ASCII}/"
LangRegex::JsRegex.new(/foo\K\p{ascii}/, target: 'ES2015').to_s # => "/foo[\x00-\x7f]/"
LangRegex::JsRegex.new(/foo\K\p{ascii}/, target: 'ES2018').to_s # => "/(?<=foo)\p{ASCII}/"
```

<a name='SF'></a>
Expand Down
2 changes: 1 addition & 1 deletion bin/console
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby

require 'bundler/setup'
require 'js_regex'
require 'lang_regex'

RP = Regexp::Parser
RS = Regexp::Scanner
Expand Down
64 changes: 64 additions & 0 deletions bin/lang_regex
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env ruby

require 'bundler/setup'
require 'colorize'
require 'optparse'

require 'lang_regex'

def display_conversion(lang, regex)
puts "------------------- #{lang}".green
if LangRegex::Target::JS.include? lang.to_s
puts LangRegex::JsRegex.new(Regexp.new(regex), target: lang)
else
puts Object.const_get("LangRegex::#{lang}Regex").new(Regexp.new(regex))
end
end

def display_all_conversions(langs, regex)
langs.each do |lang|
display_conversion lang, regex
end
end

langs = []

option_parser = OptionParser.new do |opts|
opts.banner = 'Usage: lang_regex [options] [single regex]'

# Output languages.
opts.separator ''
opts.separator 'Output languages:'
opts.on('-j', '--java', 'convert to Java regex') do
langs << :Java
end
opts.separator ''
opts.on('-J', '--es09', 'convert to ES09 regex') do
langs << :ES2009
end
opts.on('--es15', 'convert to ES15 regex') do
langs << :ES2015
end
opts.on('--es18', 'convert to ES18 regex') do
langs << :ES2018
end
opts.separator ''
opts.on('-p', '--php', 'convert to Php regex') do
langs << :Php
end
opts.separator ''
opts.on('-y', '--python', 'convert to Python regex') do
langs << :Python
end
end

option_parser.parse!

if ARGV.empty?
require 'readline'
while (buf = Readline.readline('❯❯❯ '.red, true))
display_all_conversions langs, buf
end
else
display_all_conversions langs, ARGV.last
end
2 changes: 1 addition & 1 deletion js_regex.gemspec → lang_regex.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ require File.join(dir, 'lib', 'js_regex', 'version')
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.name = 'js_regex'
s.version = JsRegex::VERSION
s.version = LangRegex::VERSION
s.license = 'MIT'

s.summary = 'Converts Ruby regexes to JavaScript regexes.'
Expand Down
46 changes: 0 additions & 46 deletions lib/js_regex.rb

This file was deleted.

14 changes: 7 additions & 7 deletions lib/js_regex/conversion.rb
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
class JsRegex
module LangRegex
#
# This class acts as a facade, passing a Regexp to the Converters.
#
# ::of returns a source String, options String, warnings Array, target String.
#
class Conversion
require 'regexp_parser'
require_relative 'target'
require_relative 'converter'
require_relative 'error'
require_relative 'node'
require_relative 'second_pass'
require_relative 'target'

class << self
def of(input, options: nil, target: Target::ES2009, fail_fast: false)
def of(input, converter, options: nil, target: Target::ES2009, fail_fast: false)
target = Target.cast(target)
source, warnings, extra_opts = convert_source(input, target, fail_fast)
source, warnings, extra_opts = convert_source(input, converter, target, fail_fast)
options_string = convert_options(input, options, extra_opts)
[source, options_string, warnings, target]
end

private

def convert_source(input, target, fail_fast)
def convert_source(input, converter, target, fail_fast)
tree = Regexp::Parser.parse(input)
context = Converter::Context.new(
case_insensitive_root: tree.i?,
target: target,
fail_fast: fail_fast,
)
converted_tree = Converter.convert(tree, context)
converted_tree = converter.convert(tree, context)
final_tree = SecondPass.call(converted_tree)
[final_tree.to_s, context.warnings, context.required_options]
rescue Regexp::Parser::Error => e
raise e.extend(JsRegex::Error)
raise e.extend(Error)
end

def convert_options(input, custom_options, required_options)
Expand Down
29 changes: 9 additions & 20 deletions lib/js_regex/converter.rb
Original file line number Diff line number Diff line change
@@ -1,36 +1,25 @@
class JsRegex
module LangRegex
module Converter
Dir[File.join(__dir__, 'converter', '*.rb')].sort.each do |file|
require file
end

MAP = Hash.new(UnsupportedTokenConverter).merge(
anchor: AnchorConverter,
assertion: AssertionConverter,
backref: BackreferenceConverter,
conditional: ConditionalConverter,
escape: EscapeConverter,
expression: SubexpressionConverter,
free_space: FreespaceConverter,
group: GroupConverter,
keep: KeepConverter,
literal: LiteralConverter,
meta: MetaConverter,
nonproperty: PropertyConverter,
property: PropertyConverter,
set: SetConverter,
type: TypeConverter
).freeze
class Converter
def initialize(converters_map)
@converters_map = converters_map
@converters_map.default ||= UnsupportedTokenConverter
end

class << self
def convert(exp, context = nil)
self.for(exp).convert(exp, context || Context.new)
end

def for(expression)
MAP[expression.type].new
@converters_map[expression.type].new(self)
end
end

class << self
# Legacy method. Remove in v4.0.0.
def surrogate_pair_limit=(_arg)
warn '#surrogate_pair_limit= is deprecated and has no effect anymore.'
Expand Down
41 changes: 20 additions & 21 deletions lib/js_regex/converter/anchor_converter.rb
Original file line number Diff line number Diff line change
@@ -1,46 +1,45 @@
require_relative 'base'

class JsRegex
module LangRegex
module Converter
#
# Template class implementation.
#
class AnchorConverter < JsRegex::Converter::Base
class AnchorConverter < Base
private

def convert_data
case subtype
when :bol, :bos then '^'
when :eol, :eos then '$'
when :eos_ob_eol then '(?=\n?$)'
when :word_boundary then convert_boundary
when :bol, :eol then pass_through
when :bos then convert_beginning_of_string
when :eos then convert_end_of_string
when :eos_ob_eol then convert_end_of_string_with_new_line
when :word_boundary then convert_boundary
when :nonword_boundary then convert_nonboundary
else
warn_of_unsupported_feature
end
end

def convert_beginning_of_string
'(?<!.|\n)'
end

def convert_boundary
if context.es_2018_or_higher? && context.enable_u_option
BOUNDARY_EXPANSION
else
pass_boundary_with_warning
end
pass_boundary_with_warning
end

def convert_nonboundary
if context.es_2018_or_higher? && context.enable_u_option
NONBOUNDARY_EXPANSION
else
pass_boundary_with_warning
end
pass_boundary_with_warning
end

def convert_end_of_string
'(?!.|\n)'
end

# This is an approximation to the word boundary behavior in Ruby, c.f.
# https://github.com/ruby/ruby/blob/08476c45/tool/enc-unicode.rb#L130
W = '\d\p{L}\p{M}\p{Pc}'
BOUNDARY_EXPANSION = "(?:(?<=[#{W}])(?=[^#{W}]|$)|(?<=[^#{W}]|^)(?=[#{W}]))"
NONBOUNDARY_EXPANSION = "(?<=[#{W}])(?=[#{W}])"
def convert_end_of_string_with_new_line
'(?=\n?(?!.|\n))'
end

def pass_boundary_with_warning
warn_of("The anchor '#{data}' at index #{expression.ts} only works "\
Expand Down
4 changes: 2 additions & 2 deletions lib/js_regex/converter/assertion_converter.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
require_relative 'base'
require_relative 'group_converter'

class JsRegex
module LangRegex
module Converter
#
# Template class implementation.
#
# Note the inheritance from GroupConverter.
#
class AssertionConverter < JsRegex::Converter::GroupConverter
class AssertionConverter < GroupConverter
private

def convert_data
Expand Down
Loading