ruby_json_parser 0.1.0 → 0.2.1

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: 642bf9c4ccd18cfadf48b5ebfa77838c2b27f85e22f3ad87b800709b94697344
4
- data.tar.gz: e56908db728c244bd31105f6feb8093d5f5f2b04f56d4d0d04d43c140fdc5077
3
+ metadata.gz: f3e3c4f63c1962f108039b41218e9c8896aca15da3ee7c5a81d3717a0d112edd
4
+ data.tar.gz: fe68aa5dff821a00239b25d13b63b89cf3d5611a587aff3b3541a837b1c87d54
5
5
  SHA512:
6
- metadata.gz: 3488592ef9a18ab9e2dbc155c7e9f7c00fab7e523b41de5b9a0624c9462285a0d1b5239f2fc38b7d1723e2945ec4735731ffc52dae554d5146dcc09f54b76e56
7
- data.tar.gz: 39da06576d98f82b3d4f023b10e815c4f2f79684b13264414ba62f762d184327dcc0eca7327e8523fa379a957dc275c22b1186ad87134955e4e3b9ce8a31e41c
6
+ metadata.gz: 2cccc1d42e1a77e60d7f1296c31e2b3caff252532ba20836cdcc44bc9a306630b9ba98876d2a63d351733429b5ed2e1abf8ea65757626c60c271488f8fe26af7
7
+ data.tar.gz: 151531a62f742970174e2a472379dd413c4165a29cbee311247e2ca65596180e062f0d3dd352dd6f736eb9acbe3d268bef5bb93011be7714344fb0009eb57601
data/.rubocop.yml CHANGED
@@ -33,3 +33,6 @@ Metrics/ClassLength:
33
33
 
34
34
  Style/RaiseArgs:
35
35
  Enabled: false
36
+
37
+ Style/ZeroLengthPredicate:
38
+ Enabled: false
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # RubyJsonParser
2
2
 
3
- This library implements a JSON lexer, parser and evaluator in pure Ruby 💎.
3
+ This library implements a JSON lexer, parser, evaluator and syntax highlighter in pure Ruby 💎.
4
4
 
5
5
  It has been built for educational purposes, to serve as a simple example of what makes parsers tick.
6
6
 
@@ -128,6 +128,19 @@ RubyJsonParser.eval('{ "some" }')
128
128
  #! RubyJsonParser::SyntaxError: missing key in object literal for value: `"some"`
129
129
  ```
130
130
 
131
+ ### Syntax highlighter
132
+
133
+ This library implements a JSON syntax highlighter based on a lexer.
134
+ It tokenizes a JSON source string and returns a new string highlighted with [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code)
135
+
136
+ You can use it by calling `RubyJsonParser.highlight` passing in a string
137
+ with JSON source.
138
+
139
+ ```rb
140
+ RubyJsonParser.highlight('{ "foo": 3, "lol": [5, false, null, dupa] }')
141
+ #=> "\e[95m{\e[0m \e[93m\"foo\"\e[0m\e[35m:\e[0m \e[94m3\e[0m\e[35m,\e[0m \e[93m\"lol\"\e[0m\e[35m:\e[0m \e[95m[\e[0m\e[94m5\e[0m\e[35m,\e[0m \e[32m\e[3mfalse\e[0m\e[35m,\e[0m \e[32m\e[3mnull\e[0m\e[35m,\e[0m \e[48;2;153;51;255m\e[9m\e[30mdupa\e[0m\e[95m]\e[0m \e[95m}\e[0m"
142
+ ```
143
+
131
144
  ## Development
132
145
 
133
146
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -0,0 +1,97 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'token'
5
+
6
+ module RubyJsonParser
7
+ # Contains common ANSI escape codes
8
+ module ANSICodes
9
+ RESET = "\e[0m"
10
+ BOLD = "\e[1m"
11
+ FAINT = "\e[2m"
12
+ ITALIC = "\e[3m"
13
+ UNDERLINE = "\e[4m"
14
+ SLOW_BLINK = "\e[5m"
15
+ RAPID_BLINK = "\e[6m"
16
+ STRIKE = "\e[9m"
17
+ ENCIRCLED = "\e[51m"
18
+ FRAMED = "\e[52m"
19
+ OVERLINE = "\e[53m"
20
+
21
+ FOREGROUND_BLACK = "\e[30m"
22
+ FOREGROUND_RED = "\e[31m"
23
+ FOREGROUND_GREEN = "\e[32m"
24
+ FOREGROUND_YELLOW = "\e[33m"
25
+ FOREGROUND_BLUE = "\e[34m"
26
+ FOREGROUND_MAGENTA = "\e[35m"
27
+ FOREGROUND_CYAN = "\e[36m"
28
+ FOREGROUND_WHITE = "\e[37m"
29
+
30
+ FOREGROUND_BRIGHT_BLACK = "\e[90m"
31
+ FOREGROUND_BRIGHT_RED = "\e[91m"
32
+ FOREGROUND_BRIGHT_GREEN = "\e[92m"
33
+ FOREGROUND_BRIGHT_YELLOW = "\e[93m"
34
+ FOREGROUND_BRIGHT_BLUE = "\e[94m"
35
+ FOREGROUND_BRIGHT_MAGENTA = "\e[95m"
36
+ FOREGROUND_BRIGHT_CYAN = "\e[96m"
37
+ FOREGROUND_BRIGHT_WHITE = "\e[97m"
38
+
39
+ BACKGROUND_BLACK = "\e[40m"
40
+ BACKGROUND_RED = "\e[41m"
41
+ BACKGROUND_GREEN = "\e[42m"
42
+ BACKGROUND_YELLOW = "\e[43m"
43
+ BACKGROUND_BLUE = "\e[44m"
44
+ BACKGROUND_MAGENTA = "\e[45m"
45
+ BACKGROUND_CYAN = "\e[46m"
46
+ BACKGROUND_WHITE = "\e[47m"
47
+
48
+ BACKGROUND_BRIGHT_BLACK = "\e[100m"
49
+ BACKGROUND_BRIGHT_RED = "\e[101m"
50
+ BACKGROUND_BRIGHT_GREEN = "\e[102m"
51
+ BACKGROUND_BRIGHT_YELLOW = "\e[103m"
52
+ BACKGROUND_BRIGHT_BLUE = "\e[104m"
53
+ BACKGROUND_BRIGHT_MAGENTA = "\e[105m"
54
+ BACKGROUND_BRIGHT_CYAN = "\e[106m"
55
+ BACKGROUND_BRIGHT_WHITE = "\e[107m"
56
+
57
+ class << self
58
+ extend T::Sig
59
+
60
+ sig { params(r: Integer, g: Integer, b: Integer).returns(String) }
61
+ def rgb_foreground(r, g, b)
62
+ "\e[38;2;#{r};#{g};#{b}m"
63
+ end
64
+
65
+ sig { params(r: Integer, g: Integer, b: Integer).returns(String) }
66
+ def rgb_background(r, g, b)
67
+ "\e[48;2;#{r};#{g};#{b}m"
68
+ end
69
+
70
+ # Creates a new string prepended with the given ANSI escape codes.
71
+ # Styles are reset at the end of the string.
72
+ sig { params(str: String, codes: T::Array[String]).returns(String) }
73
+ def style(str, codes)
74
+ return str if codes.length == 0
75
+
76
+ buff = String.new
77
+ codes.each do |code|
78
+ buff << code
79
+ end
80
+ buff << str << RESET
81
+ end
82
+
83
+ # Creates a new string prepended with the given ANSI escape codes.
84
+ # Styles are reset at the end of the string.
85
+ sig { params(str: String, codes: String).returns(String) }
86
+ def style!(str, *codes)
87
+ return str if codes.length == 0
88
+
89
+ buff = String.new
90
+ codes.each do |code|
91
+ buff << code
92
+ end
93
+ buff << str << RESET
94
+ end
95
+ end
96
+ end
97
+ end
@@ -1,7 +1,7 @@
1
1
  # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
- class RubyJsonParser
4
+ module RubyJsonParser
5
5
  # Contains the definitions of all AST (Abstract Syntax Tree) nodes.
6
6
  # AST is the data structure that is returned by the parser.
7
7
  module AST
@@ -49,7 +49,7 @@ class RubyJsonParser
49
49
 
50
50
  sig { override.params(indent: Integer).returns(String) }
51
51
  def inspect(indent = 0)
52
- "#{INDENT_UNIT * indent}(invalid `#{token}`)"
52
+ "#{INDENT_UNIT * indent}(invalid #{token.inspect})"
53
53
  end
54
54
  end
55
55
 
@@ -3,7 +3,7 @@
3
3
 
4
4
  require_relative 'token'
5
5
 
6
- class RubyJsonParser
6
+ module RubyJsonParser
7
7
  # An evaluator for JSON.
8
8
  # Creates Ruby structures based on an JSON AST.
9
9
  module Evaluator
@@ -0,0 +1,60 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'token'
5
+
6
+ module RubyJsonParser
7
+ # Color highlighter for JSON. Uses ANSI escape codes.
8
+ module Highlighter
9
+ class << self
10
+ extend T::Sig
11
+
12
+ sig { params(source: String).returns(String) }
13
+ def highlight(source)
14
+ lexer = Lexer.new(source)
15
+ buff = String.new
16
+
17
+ previous_end = 0
18
+ lexer.each do |token|
19
+ span = token.span
20
+ between_lexemes = T.must source[previous_end...span.start.char_index]
21
+ buff << between_lexemes if between_lexemes.length > 0
22
+
23
+ lexeme_range = span.start.char_index..span.end.char_index
24
+ lexeme = T.must source[lexeme_range]
25
+ styles = token_styles(token)
26
+ buff << ANSICodes.style(lexeme, styles)
27
+ previous_end = span.end.char_index + 1
28
+ end
29
+
30
+ between_lexemes = T.must source[previous_end..]
31
+ buff << between_lexemes if between_lexemes.length > 0
32
+
33
+ buff
34
+ end
35
+
36
+ private
37
+
38
+ sig { params(token: Token).returns(T::Array[String]) }
39
+ def token_styles(token)
40
+ case token.type
41
+ when Token::NULL, Token::FALSE, Token::TRUE
42
+ [ANSICodes::FOREGROUND_GREEN, ANSICodes::ITALIC]
43
+ when Token::COLON, Token::COMMA
44
+ [ANSICodes::FOREGROUND_MAGENTA]
45
+ when Token::LBRACE, Token::RBRACE, Token::LBRACKET, Token::RBRACKET
46
+ [ANSICodes::FOREGROUND_BRIGHT_MAGENTA]
47
+ when Token::NUMBER
48
+ [ANSICodes::FOREGROUND_BRIGHT_BLUE]
49
+ when Token::STRING
50
+ [ANSICodes::FOREGROUND_BRIGHT_YELLOW]
51
+ when Token::ERROR
52
+ [ANSICodes::BACKGROUND_RED, ANSICodes::STRIKE, ANSICodes::FOREGROUND_BLACK]
53
+ else
54
+ []
55
+ end
56
+ end
57
+
58
+ end
59
+ end
60
+ end
@@ -3,7 +3,7 @@
3
3
 
4
4
  require_relative 'token'
5
5
 
6
- class RubyJsonParser
6
+ module RubyJsonParser
7
7
  # A lexical analyzer (tokenizer) for JSON
8
8
  class Lexer
9
9
  extend T::Sig
@@ -35,7 +35,7 @@ class RubyJsonParser
35
35
 
36
36
  sig { returns(Token) }
37
37
  def next
38
- return Token.new(Token::END_OF_FILE) unless more_tokens?
38
+ return Token.new(Token::END_OF_FILE, Span.new(Position.new(0), Position.new(0))) unless more_tokens?
39
39
 
40
40
  scan_token
41
41
  end
@@ -68,8 +68,9 @@ class RubyJsonParser
68
68
 
69
69
  sig { params(type: Symbol, value: T.nilable(String)).returns(Token) }
70
70
  def token(type, value = nil)
71
+ span = Span.new(Position.new(@start_cursor), Position.new(@cursor - 1))
71
72
  @start_cursor = @cursor
72
- Token.new(type, value)
73
+ Token.new(type, span, value)
73
74
  end
74
75
 
75
76
  # Returns the current token value.
@@ -342,7 +343,7 @@ class RubyJsonParser
342
343
  when 'u'
343
344
  unless accept_chars(Token::HEX_DIGITS, 4)
344
345
  swallow_rest_of_the_string
345
- return Token.new(Token::ERROR, 'invalid unicode escape')
346
+ return token(Token::ERROR, 'invalid unicode escape')
346
347
  end
347
348
 
348
349
  advance_chars(4)
@@ -350,7 +351,7 @@ class RubyJsonParser
350
351
  value_buffer << [last4.hex].pack('U')
351
352
  else
352
353
  swallow_rest_of_the_string
353
- return Token.new(Token::ERROR, "invalid escape `\\#{char}`")
354
+ return token(Token::ERROR, "invalid escape `\\#{char}`")
354
355
  end
355
356
  end
356
357
  end
@@ -1,7 +1,7 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- class RubyJsonParser
4
+ module RubyJsonParser
5
5
  # JSON parser
6
6
  class Parser
7
7
  extend T::Sig
@@ -22,7 +22,7 @@ class RubyJsonParser
22
22
  # Lexer/Tokenizer that produces tokens
23
23
  @lexer = T.let(Lexer.new(source), Lexer)
24
24
  # Next token used for predicting productions
25
- @lookahead = T.let(Token.new(Token::NONE), Token)
25
+ @lookahead = T.let(Token.new(Token::NONE, Span.new(Position.new(0), Position.new(0))), Token)
26
26
  @errors = T.let([], T::Array[String])
27
27
  end
28
28
 
@@ -0,0 +1,29 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ module RubyJsonParser
5
+ # A position of a single character in a piece of text
6
+ class Position
7
+ extend T::Sig
8
+
9
+ sig { returns(Integer) }
10
+ attr_reader :char_index
11
+
12
+ sig { params(char_index: Integer).void }
13
+ def initialize(char_index)
14
+ @char_index = char_index
15
+ end
16
+
17
+ sig { params(other: Object).returns(T::Boolean) }
18
+ def ==(other)
19
+ return false unless other.is_a?(Position)
20
+
21
+ @char_index == other.char_index
22
+ end
23
+
24
+ sig { returns(String) }
25
+ def inspect
26
+ "P(#{char_index.inspect})"
27
+ end
28
+ end
29
+ end
@@ -1,7 +1,7 @@
1
1
  # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
- class RubyJsonParser
4
+ module RubyJsonParser
5
5
  # The result of parsing a JSON string/file.
6
6
  # Combines an AST (Abstract Syntax Tree) and a list of errors.
7
7
  class Result
@@ -0,0 +1,33 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ module RubyJsonParser
5
+ # A collection of two positions: start and end
6
+ class Span
7
+ extend T::Sig
8
+
9
+ sig { returns(Position) }
10
+ attr_reader :start
11
+
12
+ sig { returns(Position) }
13
+ attr_reader :end
14
+
15
+ sig { params(start: Position, end_pos: Position).void }
16
+ def initialize(start, end_pos)
17
+ @start = start
18
+ @end = end_pos
19
+ end
20
+
21
+ sig { params(other: Object).returns(T::Boolean) }
22
+ def ==(other)
23
+ return false unless other.is_a?(Span)
24
+
25
+ @start == other.start && @end == other.end
26
+ end
27
+
28
+ sig { returns(String) }
29
+ def inspect
30
+ "S(#{@start.inspect}, #{@end.inspect})"
31
+ end
32
+ end
33
+ end
@@ -3,7 +3,7 @@
3
3
 
4
4
  require 'set'
5
5
 
6
- class RubyJsonParser
6
+ module RubyJsonParser
7
7
  # Represents a single token (word) produced by the lexer.
8
8
  class Token
9
9
  extend T::Sig
@@ -57,9 +57,13 @@ class RubyJsonParser
57
57
  sig { returns(T.nilable(String)) }
58
58
  attr_reader :value
59
59
 
60
- sig { params(type: Symbol, value: T.nilable(String)).void }
61
- def initialize(type, value = nil)
60
+ sig { returns(Span) }
61
+ attr_reader :span
62
+
63
+ sig { params(type: Symbol, span: Span, value: T.nilable(String)).void }
64
+ def initialize(type, span, value = nil)
62
65
  @type = type
66
+ @span = span
63
67
  @value = value
64
68
  end
65
69
 
@@ -67,14 +71,14 @@ class RubyJsonParser
67
71
  def ==(other)
68
72
  return false unless other.is_a?(Token)
69
73
 
70
- type == other.type && value == other.value
74
+ type == other.type && value == other.value && span == other.span
71
75
  end
72
76
 
73
77
  sig { returns(String) }
74
78
  def inspect
75
- return "Token(#{type.inspect})" if value.nil?
79
+ return "Token(#{type.inspect}, #{span.inspect})" if value.nil?
76
80
 
77
- "Token(#{type.inspect}, #{value.inspect})"
81
+ "Token(#{type.inspect}, #{span.inspect}, #{value.inspect})"
78
82
  end
79
83
 
80
84
  # Converts a token into a human-readable string.
@@ -1,6 +1,6 @@
1
1
  # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
- class RubyJsonParser # rubocop:disable Style/StaticClass
5
- VERSION = '0.1.0'
4
+ module RubyJsonParser
5
+ VERSION = '0.2.1'
6
6
  end
@@ -4,8 +4,12 @@
4
4
  require 'sorbet-runtime'
5
5
 
6
6
  require_relative 'ruby_json_parser/version'
7
+ require_relative 'ruby_json_parser/position'
8
+ require_relative 'ruby_json_parser/span'
7
9
  require_relative 'ruby_json_parser/token'
8
10
  require_relative 'ruby_json_parser/lexer'
11
+ require_relative 'ruby_json_parser/ansi_codes'
12
+ require_relative 'ruby_json_parser/highlighter'
9
13
  require_relative 'ruby_json_parser/ast'
10
14
  require_relative 'ruby_json_parser/result'
11
15
  require_relative 'ruby_json_parser/parser'
@@ -13,7 +17,7 @@ require_relative 'ruby_json_parser/evaluator'
13
17
 
14
18
  # Implements a JSON lexer, parser and evaluator in pure Ruby.
15
19
  # Built for educational purposes.
16
- class RubyJsonParser
20
+ module RubyJsonParser
17
21
  extend T::Sig
18
22
 
19
23
  # JSON syntax error
@@ -71,6 +75,17 @@ class RubyJsonParser
71
75
  def eval(source)
72
76
  Evaluator.eval(source)
73
77
  end
78
+
79
+ # Tokenizes the given source string and creates a new
80
+ # colorized string using ANSI escape codes.
81
+ sig do
82
+ params(
83
+ source: String,
84
+ ).returns(String)
85
+ end
86
+ def highlight(source)
87
+ Highlighter.highlight(source)
88
+ end
74
89
  end
75
90
 
76
91
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_json_parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mateusz Drewniak
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-11-29 00:00:00.000000000 Z
11
+ date: 2024-12-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sorbet-runtime
@@ -37,11 +37,15 @@ files:
37
37
  - README.md
38
38
  - Rakefile
39
39
  - lib/ruby_json_parser.rb
40
+ - lib/ruby_json_parser/ansi_codes.rb
40
41
  - lib/ruby_json_parser/ast.rb
41
42
  - lib/ruby_json_parser/evaluator.rb
43
+ - lib/ruby_json_parser/highlighter.rb
42
44
  - lib/ruby_json_parser/lexer.rb
43
45
  - lib/ruby_json_parser/parser.rb
46
+ - lib/ruby_json_parser/position.rb
44
47
  - lib/ruby_json_parser/result.rb
48
+ - lib/ruby_json_parser/span.rb
45
49
  - lib/ruby_json_parser/token.rb
46
50
  - lib/ruby_json_parser/version.rb
47
51
  - sorbet/config