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 +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
|