repper 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0889e10cd309dbdd42315e1049c2f7e10a9ab46b949b08c390614965aa2c1483'
4
- data.tar.gz: 8ef2f61c38c6e23abcd933c1e3e462602c19d153824c7632114673b57b291ad5
3
+ metadata.gz: 6493f978abbb5042a52adf386b6222ed571ba0de09b0926e0fb16104a3b60ad4
4
+ data.tar.gz: 52604ae6ebccc88a5608de1b57c7e8e0de85ba55aaf1704a2e066c6ae4fe93b5
5
5
  SHA512:
6
- metadata.gz: a78030f011bb7d74c0263b2e6353114bdd94c9bdd5ee05f36ec7712dd5ad719f23050cea5627fb51cfc8ebe8bdf8b7813ec2094ea163c0253c58996e1fa5997c
7
- data.tar.gz: 28b9f8a9243497e1be1ae2243c81817e2dc566532b314b03b9aa3410a947a9eb50f16b79b9b33a8e76e189c63efbc98b173acf160f814ca9bff2e91deb1d839f
6
+ metadata.gz: 0306346ad34c63bec6558c6ef9516e675e6a8c6b5caa59d92c3ded9fee74687c74cb69e293df474552e8c9204f0c6dfb78d9fd35d03a7b4c9aa8b86edfd2c28c
7
+ data.tar.gz: 2c3ebb13aa9795f4992057da6cb318a2bc16b571ad15c52a4117f27c88e4803526a15a06dd0ce0d62eb1a5cd4133dc6a7ebee6ac03e2cd0e4f611571681b8309
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [1.0.0] - 2022-04-30
3
+ ...
4
+
5
+ ## [1.1.0] - 2022-05-29
6
+
7
+ ### Added
8
+
9
+ - codemod capability and corresponding `exe/repper` executable
10
+ - new format `Repper::Format::Extended`
11
+ - `Regexp#original_inspect` when using `Regexp#inspect` override
12
+
13
+ ### Fixed
14
+
15
+ - use the more appropriate `:inline` format when passing `format: nil | false`
16
+
17
+ ## [1.0.0] - 2022-05-09
4
18
 
5
19
  - Initial release
data/README.md CHANGED
@@ -1,9 +1,8 @@
1
- <h1>
2
- Repper
3
- <img alt='Rapper necklace with dollar pendant' src='https://user-images.githubusercontent.com/10758879/167485870-5c49284d-a783-453e-8be0-a3597c2ef97c.png' height='46' align='right' />
4
- </h1>
1
+ <img alt='Rapper necklace with dollar pendant' src='https://user-images.githubusercontent.com/10758879/167485870-5c49284d-a783-453e-8be0-a3597c2ef97c.png' height='64' align='right' />
5
2
 
6
- Repper is a regular expression pretty printer for Ruby.
3
+ # Repper
4
+
5
+ Repper is a regular expression pretty printer and formatter for Ruby.
7
6
 
8
7
  ## Installation
9
8
 
@@ -11,20 +10,28 @@ Repper is a regular expression pretty printer for Ruby.
11
10
 
12
11
  ## Usage
13
12
 
14
- `repper` can be integrated into the REPL (e.g. IRB) through core extensions or used manually.
13
+ `repper` can be integrated into the REPL (e.g. IRB) through core extensions for Regexp pretty-printing, integrated into editors to format Regexps, or called manually.
15
14
 
16
15
  There are also a few customization options.
17
16
 
18
- ### Full REPL integration (recommended)
17
+ ### REPL integration
18
+
19
+ #### Via Regexp#inspect (recommended)
19
20
 
20
21
  `require 'repper/core_ext/regexp'` in your `~/.irbrc` or `~/.pryrc` to override `Regexp#inspect` and automatically use `repper` to display Regexps:
21
22
 
22
- <img width="313" alt="screenshot1" src="https://user-images.githubusercontent.com/10758879/167497359-e5bb94db-1382-465b-903a-3e114721b7a6.png">
23
+ <img width="475" alt="screenshot1" src="https://user-images.githubusercontent.com/10758879/167719748-60f4013a-c8d4-4a62-843a-d9f27057bcd3.png">
23
24
 
24
- ### Extending Kernel#pp
25
+ #### Via Kernel#pp
25
26
 
26
27
  Alternatively, `require 'repper/core_ext/kernel'` to make the `pp` command give nicer output for Regexps (which will look like above by default).
27
28
 
29
+ ### Editor integration
30
+
31
+ Use [vscode-repper](https://github.com/jaynetics/vscode-repper) to format Regexps in VSCode.
32
+
33
+ ![vscode-repper](https://user-images.githubusercontent.com/10758879/170892739-e2f408f2-e239-4b13-8d28-c14fb7a9dbb9.gif)
34
+
28
35
  ### Using Repper manually
29
36
 
30
37
  ```ruby
@@ -36,11 +43,13 @@ Repper.render(/foo/) # returns the pretty print String
36
43
 
37
44
  #### Customizing the format
38
45
 
39
- The default format is the annotated, indented format shown above.
40
-
41
- If you want to see the indented structure without annotations, use the `:structured` format.
46
+ Multiple formats are available out of the box:
42
47
 
43
- If you only want colorization you can use the `:inline` format.
48
+ - `:annotated` is the default, verbose format, shown above
49
+ - `:inline` adds only colorization and does not restructure the Regexp
50
+ - `:structured` is like `:annotated`, just without annotations
51
+ - `:x` (or `:extended`) returns a lightly formatted but equivalent Regexp
52
+ - this format is used for the repper executable and [vscode-repper](https://github.com/jaynetics/vscode-repper)
44
53
 
45
54
  You can change the format globally:
46
55
 
@@ -50,14 +59,14 @@ Repper.format = :structured
50
59
 
51
60
  Or pick a format on a case-by-case basis:
52
61
 
53
- <img width="445" alt="screenshot2" src="https://user-images.githubusercontent.com/10758879/167497599-105f39c7-91e0-4954-bce3-d04ad7266695.png">
62
+ <img width="711" alt="screenshot2" src="https://user-images.githubusercontent.com/10758879/167719567-ae8ee42f-839e-4ce4-af56-a139044d3436.png">
54
63
 
55
64
  Or create your own format:
56
65
 
57
66
  ```ruby
58
67
  require 'csv'
59
68
 
60
- csv_format = ->(elements, _theme) { elements.map(&:text).to_csv }
69
+ csv_format = ->(tokens, _theme) { tokens.map(&:text).to_csv }
61
70
  Repper.render(/re[\p{pe}\r]$/, format: csv_format)
62
71
  => "/,re,[,\\p{pe},\\r,],$,/\n"
63
72
  ```
@@ -69,7 +78,8 @@ The color theme can also be set globally or passed on call:
69
78
  ```ruby
70
79
  Repper.theme = :monokai # a nicer theme, if the terminal supports it
71
80
  ```
72
- <img width="316" alt="screenshot3" src="https://user-images.githubusercontent.com/10758879/167497895-0cdc017f-5c77-4b15-afaa-207f7eb887cc.png">
81
+
82
+ <img width="478" alt="screenshot3" src="https://user-images.githubusercontent.com/10758879/167719807-9170ba92-48d1-4669-a05d-a72f962b961d.png">
73
83
 
74
84
  ```ruby
75
85
  Repper.call(/foo/, theme: nil) # render without colors
data/exe/repper ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/repper"
4
+
5
+ Repper::Command.call(ARGV)
@@ -0,0 +1,68 @@
1
+ module Repper
2
+ # Formatter for Ruby code containing Regexp literals
3
+ module Codemod
4
+ module_function
5
+
6
+ def call(code)
7
+ formatted_code = code.dup
8
+
9
+ regexp_locations(code).reverse.each do |loc|
10
+ beg_idx = code[/\A(.*\n){#{loc.beg_line}}/].size + loc.beg_char
11
+ end_idx = code[/\A(.*\n){#{loc.end_line}}/].size + loc.end_char
12
+ range = beg_idx..end_idx
13
+
14
+ /\A(?<start>\/|%r.)(?<source>.*)(?<stop>[^a-z])(?<flags>[a-z]*)\z/m =~
15
+ code[range]
16
+
17
+ tokens = Tokenizer.call(source, delimiters: [start, stop], flags: flags)
18
+ formatted_regexp = Format::Extended.call(tokens, Theme::Plain)
19
+
20
+ # indent consistently by applying leading line indentation to all lines
21
+ lead_indentation = code.lines[loc.beg_line][/^ */]
22
+ formatted_regexp.gsub!("\n", "\n#{lead_indentation}")
23
+
24
+ formatted_code[range] = formatted_regexp
25
+ end
26
+
27
+ formatted_code
28
+ end
29
+
30
+ require 'ripper'
31
+
32
+ def regexp_locations(code)
33
+ embed_level = 0
34
+ beg_tokens = {}
35
+
36
+ Ripper.lex(code).each.with_object([]) do |token, acc|
37
+ case token[1]
38
+ when :on_regexp_beg
39
+ beg_tokens[embed_level] = token
40
+ # nested regexp literals are not supported a.t.m., so if we're
41
+ # in an embed, discard the location of the surrounding regexp.
42
+ beg_tokens[embed_level - 1] = nil
43
+ when :on_regexp_end
44
+ next unless beg_token = beg_tokens[embed_level]
45
+
46
+ acc << Location.new(
47
+ beg_line: beg_token[0][0] - 1, # note: using 0-indexed line values
48
+ beg_char: beg_token[0][1],
49
+ end_line: token[0][0] - 1,
50
+ end_char: token[0][1] + (token[2].length - 1), # lex includes flags
51
+ )
52
+ when :on_embexpr_beg # embedded expression a.k.a. interpolation (#{...})
53
+ embed_level += 1
54
+ when :on_embexpr_end
55
+ embed_level -= 1
56
+ when :on_const, :on_ident
57
+ # OK when embedded - Regexp::Parser will treat them as literals
58
+ else
59
+ # other embedded expressions are not supported
60
+ beg_tokens[embed_level - 1] = nil
61
+ end
62
+ end
63
+ end
64
+
65
+ Location = Struct.new(:beg_line, :beg_char, :end_line, :end_char,
66
+ keyword_init: true)
67
+ end
68
+ end
@@ -0,0 +1,39 @@
1
+ module Repper
2
+ # Service object for exe/repper
3
+ module Command
4
+ module_function
5
+
6
+ PARSE_ERROR_EXIT_STATUS = 1
7
+
8
+ def call(argv)
9
+ if argv.count == 0
10
+ format_stdio
11
+ else
12
+ format_files(argv)
13
+ end
14
+ end
15
+
16
+ def format_stdio
17
+ input = STDIN.read
18
+ output = format(input)
19
+ print output
20
+ end
21
+
22
+ def format(string)
23
+ Codemod.call(string)
24
+ rescue Repper::Error => e
25
+ warn "Parsing failed: #{e.class} - #{e.message}"
26
+ exit PARSE_ERROR_EXIT_STATUS
27
+ end
28
+
29
+ def format_files(paths)
30
+ paths.grep(/\.rb\z/).each { |path| format_file(path) }
31
+ end
32
+
33
+ def format_file(path)
34
+ code = File.read(path)
35
+ formatted_code = format(code)
36
+ File.write(path, formatted_code)
37
+ end
38
+ end
39
+ end
@@ -1,4 +1,6 @@
1
1
  require 'repper'
2
2
  require_relative 'regexp_ext'
3
3
 
4
+ Regexp.alias_method :original_inspect, :inspect
5
+
4
6
  ::Regexp.prepend(Repper::RegexpExt)
@@ -1,7 +1,8 @@
1
1
  module Repper
2
2
  module Format
3
- Annotated = ->(elements, theme) do
4
- table = Tabulo::Table.new(elements.reject(&:whitespace?), **TABULO_STYLE)
3
+ # A structured and colorized format with annotations about token types.
4
+ Annotated = ->(tokens, theme) do
5
+ table = Tabulo::Table.new(tokens.reject(&:whitespace?), **TABULO_STYLE)
5
6
  table.add_column(
6
7
  :indented_text,
7
8
  styler: ->(_, string, cell) { theme.colorize(string, cell.source.type) }
@@ -0,0 +1,41 @@
1
+ module Repper
2
+ module Format
3
+ # A lightly structured format that retains parsability
4
+ # and functional equivalence, for use in code.
5
+ Extended = ->(tokens, theme) do
6
+ run_types = %i[escape literal nonposixclass nonproperty
7
+ posixclass property set type]
8
+ forms_run = ->(el){ run_types.include?(el.type) || el.subtype == :dot }
9
+ prev = nil
10
+
11
+ tokens.each.with_object(''.dup) do |tok, acc|
12
+ # drop existing x-mode whitespace, if any
13
+ if tok.whitespace?
14
+ next
15
+ # keep some tokens in line:
16
+ # - option switches and conditions for syntactic correctness
17
+ # - quantifiers and codepoint runs for conciseness
18
+ elsif tok.type == :quantifier ||
19
+ tok.subtype == :options_switch && tok.text == ')' ||
20
+ tok.subtype == :condition ||
21
+ prev && forms_run.call(prev) && forms_run.call(tok)
22
+ acc << theme.colorize(tok.inlined_text, tok.type)
23
+ # keep comments in line, too, but with padding
24
+ elsif tok.comment?
25
+ acc << " #{theme.colorize(tok.inlined_text, tok.type)}"
26
+ # render root start as wtokl as empty root end in same line
27
+ elsif tok.subtype == :root && (prev.nil? || prev.subtype == :root)
28
+ acc << theme.colorize(tok.text, tok.type)
29
+ # tokse, if root is not empty, ensure x-flag is present at end
30
+ elsif tok.subtype == :root
31
+ acc << "\n#{theme.colorize(tok.text.sub(/x?\z/, 'x'), tok.type)}"
32
+ # render other tokens on their own lines for an indented structure,
33
+ # e.g. groups, alternations, anchors, assertions, ...
34
+ else
35
+ acc << "\n#{theme.colorize(tok.indented_text, tok.type)}"
36
+ end
37
+ prev = tok
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,7 +1,8 @@
1
1
  module Repper
2
2
  module Format
3
- Inline = ->(elements, theme) do
4
- elements.map { |el| theme.colorize(el.text, el.type) }.join
3
+ # A format that only adds color but does not change structure.
4
+ Inline = ->(tokens, theme) do
5
+ tokens.map { |tok| theme.colorize(tok.text, tok.type) }.join
5
6
  end
6
7
  end
7
8
  end
@@ -1,7 +1,8 @@
1
1
  module Repper
2
2
  module Format
3
- Plain = ->(elements, *) do
4
- elements.map(&:text).join
3
+ # A no-op format, equivalent to the original Regexp#inspect.
4
+ Plain = ->(tokens, *) do
5
+ tokens.map(&:text).join
5
6
  end
6
7
  end
7
8
  end
@@ -1,7 +1,8 @@
1
1
  module Repper
2
2
  module Format
3
- Structured = ->(elements, theme) do
4
- table = Tabulo::Table.new(elements.reject(&:whitespace?), **TABULO_STYLE)
3
+ # A structured format with colorization.
4
+ Structured = ->(tokens, theme) do
5
+ table = Tabulo::Table.new(tokens.reject(&:whitespace?), **TABULO_STYLE)
5
6
  table.add_column(
6
7
  :indented_text,
7
8
  styler: ->(_, string, cell) { theme.colorize(string, cell.source.type) }
@@ -4,7 +4,7 @@ module Repper
4
4
  module Format
5
5
  TABULO_STYLE = {
6
6
  border: :blank,
7
- header_frequency: nil,
7
+ header_frequency: nil, # i.e. omit column headers
8
8
  truncation_indicator: '…',
9
9
  wrap_body_cells_to: 1,
10
10
  }
data/lib/repper/format.rb CHANGED
@@ -3,8 +3,9 @@ module Repper
3
3
  def self.cast(arg)
4
4
  case arg
5
5
  when ::Proc then arg
6
+ when :x then Format::Extended
6
7
  when ::Symbol, ::String then Format.const_get(arg.capitalize) rescue nil
7
- when false, nil then Format::Plain
8
+ when false, nil then Format::Inline
8
9
  end || raise(Repper::ArgumentError, "unknown format #{arg.inspect}")
9
10
  end
10
11
  end
@@ -1,14 +1,27 @@
1
1
  module Repper
2
- Element = Struct.new(:type, :subtype, :level, :text, :id, keyword_init: true) do
2
+ Token = Struct.new(:type, :subtype, :level, :text, :id, keyword_init: true) do
3
3
  def indented_text
4
- inlined_text = text.gsub(/[\n\r\t\v]/) { |ws| ws.inspect.delete(?") }
5
4
  "#{' ' * level}#{inlined_text}"
6
5
  end
7
6
 
7
+ def inlined_text
8
+ if comment?
9
+ text.strip
10
+ else
11
+ text
12
+ .gsub(/(?<!\\) /, '\\ ')
13
+ .gsub(/[\n\r\t\v]/) { |ws| ws.inspect.delete(?") }
14
+ end
15
+ end
16
+
8
17
  def whitespace?
9
18
  subtype == :whitespace
10
19
  end
11
20
 
21
+ def comment?
22
+ subtype == :comment
23
+ end
24
+
12
25
  def annotation
13
26
  case [type, subtype]
14
27
  in [_, :root]
@@ -1,28 +1,28 @@
1
1
  require 'regexp_parser'
2
2
 
3
3
  module Repper
4
- module Parser
4
+ module Tokenizer
5
5
  module_function
6
6
 
7
- def call(regexp)
8
- tree = ::Regexp::Parser.parse(regexp)
9
- flatten(tree)
7
+ def call(regexp, delimiters: ['/', '/'], flags: nil)
8
+ tree = Regexp::Parser.parse(regexp, options: flags =~ /x/ && Regexp::EXTENDED)
9
+ flatten(tree, delimiters: delimiters, flags: flags)
10
10
  rescue ::Regexp::Parser::Error => e
11
11
  raise e.extend(Repper::Error)
12
12
  end
13
13
 
14
14
  # Turn Regexp::Parser AST back into a flat Array of visual elements
15
15
  # that match the Regexp notation.
16
- def flatten(exp, acc = [])
16
+ def flatten(exp, acc = [], delimiters: nil, flags: nil)
17
17
  # Add opening entry.
18
- exp.is?(:root) && acc << make_element(exp, '/')
18
+ exp.is?(:root) && acc << make_token(exp, delimiters[0])
19
19
 
20
20
  # Ignore nesting of invisible intermediate branches for better visuals.
21
21
  exp.is?(:sequence) && exp.nesting_level -= 1
22
22
 
23
23
  exp.parts.each do |part|
24
24
  if part.instance_of?(::String)
25
- acc << make_element(exp, part)
25
+ acc << make_token(exp, part)
26
26
  else # part.is_a?(Regexp::Expression::Base)
27
27
  flatten(part, acc)
28
28
  end
@@ -31,13 +31,16 @@ module Repper
31
31
  exp.quantified? && flatten(exp.quantifier, acc)
32
32
 
33
33
  # Add closing entry.
34
- exp.is?(:root) && acc << make_element(exp, "/#{exp.options.keys.join}")
34
+ exp.is?(:root) && begin
35
+ flags ||= exp.options.keys.join
36
+ acc << make_token(exp, "#{delimiters[1]}#{flags.chars.uniq.sort.join}")
37
+ end
35
38
 
36
39
  acc
37
40
  end
38
41
 
39
- def make_element(exp, text)
40
- Element.new(
42
+ def make_token(exp, text)
43
+ Token.new(
41
44
  type: exp.type,
42
45
  subtype: exp.token,
43
46
  level: exp.nesting_level,
@@ -1,3 +1,3 @@
1
1
  module Repper
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
data/lib/repper.rb CHANGED
@@ -1,7 +1,9 @@
1
- require_relative "repper/element"
1
+ require_relative "repper/codemod"
2
+ require_relative "repper/command"
2
3
  require_relative "repper/errors"
3
4
  require_relative "repper/format"
4
- require_relative "repper/parser"
5
+ require_relative "repper/token"
6
+ require_relative "repper/tokenizer"
5
7
  require_relative "repper/theme"
6
8
  require_relative "repper/version"
7
9
 
@@ -14,18 +16,18 @@ module Repper
14
16
  end
15
17
 
16
18
  def render(regexp, format: self.format, theme: self.theme)
17
- elements = Parser.call(regexp)
18
- format = Format.cast(format)
19
- theme = Theme.cast(theme)
20
- format.call(elements, theme)
19
+ tokens = Tokenizer.call(regexp)
20
+ format = Format.cast(format)
21
+ theme = Theme.cast(theme)
22
+ format.call(tokens, theme)
21
23
  end
22
24
 
23
- def theme=(theme)
24
- @theme = Theme.cast(theme)
25
+ def format=(arg)
26
+ @format = Format.cast(arg)
25
27
  end
26
28
 
27
- def format=(format)
28
- @format = Format.cast(format)
29
+ def theme=(arg)
30
+ @theme = Theme.cast(arg)
29
31
  end
30
32
  end
31
33
 
data/repper.gemspec CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |spec|
6
6
  spec.authors = ["Janosch Müller"]
7
7
  spec.email = ["janosch84@gmail.com"]
8
8
 
9
- spec.summary = "Regexp pretty printer for Ruby"
9
+ spec.summary = "Regexp pretty printer and formatter for Ruby"
10
10
  spec.homepage = "https://github.com/jaynetics/repper"
11
11
  spec.license = "MIT"
12
12
  spec.required_ruby_version = ">= 2.7.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: repper
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janosch Müller
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-05-09 00:00:00.000000000 Z
11
+ date: 2022-05-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rainbow
@@ -55,7 +55,8 @@ dependencies:
55
55
  description:
56
56
  email:
57
57
  - janosch84@gmail.com
58
- executables: []
58
+ executables:
59
+ - repper
59
60
  extensions: []
60
61
  extra_rdoc_files: []
61
62
  files:
@@ -65,24 +66,28 @@ files:
65
66
  - LICENSE.txt
66
67
  - README.md
67
68
  - Rakefile
69
+ - exe/repper
68
70
  - lib/repper.rb
71
+ - lib/repper/codemod.rb
72
+ - lib/repper/command.rb
69
73
  - lib/repper/core_ext/kernel.rb
70
74
  - lib/repper/core_ext/kernel_ext.rb
71
75
  - lib/repper/core_ext/regexp.rb
72
76
  - lib/repper/core_ext/regexp_ext.rb
73
- - lib/repper/element.rb
74
77
  - lib/repper/errors.rb
75
78
  - lib/repper/format.rb
76
79
  - lib/repper/format/annotated.rb
80
+ - lib/repper/format/extended.rb
77
81
  - lib/repper/format/inline.rb
78
82
  - lib/repper/format/plain.rb
79
83
  - lib/repper/format/structured.rb
80
84
  - lib/repper/format/tabulo_style.rb
81
- - lib/repper/parser.rb
82
85
  - lib/repper/theme.rb
83
86
  - lib/repper/theme/default.rb
84
87
  - lib/repper/theme/monokai.rb
85
88
  - lib/repper/theme/plain.rb
89
+ - lib/repper/token.rb
90
+ - lib/repper/tokenizer.rb
86
91
  - lib/repper/version.rb
87
92
  - repper.gemspec
88
93
  - repper.svg
@@ -112,5 +117,5 @@ requirements: []
112
117
  rubygems_version: 3.4.0.dev
113
118
  signing_key:
114
119
  specification_version: 4
115
- summary: Regexp pretty printer for Ruby
120
+ summary: Regexp pretty printer and formatter for Ruby
116
121
  test_files: []