ruby_json_parser 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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