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 +4 -4
- data/.rubocop.yml +3 -0
- data/README.md +14 -1
- data/lib/ruby_json_parser/ansi_codes.rb +97 -0
- data/lib/ruby_json_parser/ast.rb +2 -2
- data/lib/ruby_json_parser/evaluator.rb +1 -1
- data/lib/ruby_json_parser/highlighter.rb +60 -0
- data/lib/ruby_json_parser/lexer.rb +6 -5
- data/lib/ruby_json_parser/parser.rb +2 -2
- data/lib/ruby_json_parser/position.rb +29 -0
- data/lib/ruby_json_parser/result.rb +1 -1
- data/lib/ruby_json_parser/span.rb +33 -0
- data/lib/ruby_json_parser/token.rb +10 -6
- data/lib/ruby_json_parser/version.rb +2 -2
- data/lib/ruby_json_parser.rb +16 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f3e3c4f63c1962f108039b41218e9c8896aca15da3ee7c5a81d3717a0d112edd
|
4
|
+
data.tar.gz: fe68aa5dff821a00239b25d13b63b89cf3d5611a587aff3b3541a837b1c87d54
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2cccc1d42e1a77e60d7f1296c31e2b3caff252532ba20836cdcc44bc9a306630b9ba98876d2a63d351733429b5ed2e1abf8ea65757626c60c271488f8fe26af7
|
7
|
+
data.tar.gz: 151531a62f742970174e2a472379dd413c4165a29cbee311247e2ca65596180e062f0d3dd352dd6f736eb9acbe3d268bef5bb93011be7714344fb0009eb57601
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# RubyJsonParser
|
2
2
|
|
3
|
-
This library implements a JSON lexer, parser and
|
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
|
data/lib/ruby_json_parser/ast.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# typed: strong
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
|
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
|
52
|
+
"#{INDENT_UNIT * indent}(invalid #{token.inspect})"
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
@@ -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
|
-
|
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
|
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
|
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
|
-
|
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
|
@@ -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
|
-
|
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 {
|
61
|
-
|
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.
|
data/lib/ruby_json_parser.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
+
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
|