caps 0.1.0

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.
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caps
4
+ class Parser
5
+ module Entrypoints
6
+ using Caps::Parser::Helpers
7
+
8
+ def parse_full_sheet
9
+ base = parse_stylesheet
10
+ base[:value].map! do |obj|
11
+ if obj[:type] == :qualified_rule
12
+ p = Parser.new(obj.dig(:block, :value))
13
+ obj[:block][:value] = p.parse_style_block_contents
14
+ elsif obj[:type] == :at_rule && obj.dig(:name, :value).downcase == "font-face"
15
+ p = Parser.new(obj.dig(:block, :value))
16
+ obj[:block][:value] = p.parse_declaration_list
17
+ end
18
+ obj
19
+ end
20
+
21
+ base
22
+ end
23
+
24
+ def parse_stylesheet(location: nil)
25
+ {
26
+ type: :stylesheet,
27
+ location:,
28
+ value: consume_list_of_rules(top_level: true)
29
+ }
30
+ end
31
+
32
+ def parse_rule_list
33
+ consume_list_of_rules(top_level: false)
34
+ end
35
+
36
+ def parse_rule
37
+ consume_whitespace
38
+ val = peek
39
+ if val.eof?
40
+ # EOF. Raise syntax error.
41
+ syntax_error! "Unexpected EOF"
42
+ end
43
+
44
+ ret_val = if val.at_rule?
45
+ consume_at_rule
46
+ else
47
+ consume_qualified_rule
48
+ end
49
+
50
+ consume_whitespace
51
+
52
+ syntax_error! "Expected EOF, found #{peek.type} instead" unless peek.eof?
53
+
54
+ ret_val
55
+ end
56
+
57
+ def parse_declaration
58
+ consume_whitespace
59
+ syntax_error! "Unexpected #{peek.type}, expected ident" unless peek.ident?
60
+
61
+ consume_declaration.tap do |decl|
62
+ syntax_error! "Expected declaration to be consumed" if decl.nil?
63
+ end
64
+ end
65
+
66
+ def parse_style_block_contents
67
+ consume_style_block_contents
68
+ end
69
+
70
+ def parse_declaration_list
71
+ consume_declaration_list
72
+ end
73
+
74
+ def parse_component_value
75
+ consume_whitespace
76
+ syntax_error! "Unexpected EOF" if peek.eof?
77
+
78
+ ret_val = consume_component_value
79
+ consume_whitespace
80
+ return ret_val if peek.eof?
81
+
82
+ syntax_error! "Expected EOF"
83
+ end
84
+
85
+ def parse_component_value_list
86
+ arr = []
87
+ loop do
88
+ obj = consume_component_value
89
+ break if obj.eof?
90
+
91
+ arr << obj
92
+ end
93
+ arr
94
+ end
95
+
96
+ def parse_comma_separated_component_values
97
+ cvls = []
98
+
99
+ loop do
100
+ cv = consume_component_value
101
+ if cv.comma?
102
+ next
103
+ elsif cv.eof?
104
+ break
105
+ end
106
+
107
+ cvls << cv
108
+ end
109
+
110
+ cvls
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caps
4
+ class Parser
5
+ module Helpers
6
+ refine Hash do
7
+ def self.helper(name, &)
8
+ define_method(name, &)
9
+ end
10
+
11
+ def self.tassert(tname)
12
+ helper("#{tname}?".to_sym) { type == tname }
13
+ end
14
+
15
+ def start_line
16
+ dig(:position, :start, :line)
17
+ end
18
+
19
+ def start_column
20
+ dig(:position, :start, :column)
21
+ end
22
+
23
+ def end_column
24
+ dig(:position, :end, :column)
25
+ end
26
+
27
+ def end_line
28
+ dig(:position, :end, :line)
29
+ end
30
+
31
+ def type
32
+ self[:type]
33
+ end
34
+
35
+ def value
36
+ self[:value]
37
+ end
38
+
39
+ tassert(:eof)
40
+ tassert(:ident)
41
+ tassert(:whitespace)
42
+ tassert(:at_keyword)
43
+ tassert(:comma)
44
+ tassert(:semicolon)
45
+ tassert(:left_curly)
46
+ tassert(:delim)
47
+ tassert(:colon)
48
+ tassert(:bang)
49
+ tassert(:left_square)
50
+ tassert(:right_square)
51
+ tassert(:left_parens)
52
+ tassert(:right_parens)
53
+ tassert(:function)
54
+ tassert(:at_rule)
55
+ tassert(:cdo)
56
+ tassert(:cdc)
57
+ end
58
+
59
+ refine NilClass do
60
+ def self.helper(name, &)
61
+ define_method(name, &)
62
+ end
63
+
64
+ def self.tassert(tname)
65
+ helper("#{tname}?") { false }
66
+ end
67
+
68
+ def type
69
+ :eof
70
+ end
71
+
72
+ def eof?
73
+ true
74
+ end
75
+
76
+ tassert(:ident)
77
+ tassert(:whitespace)
78
+ tassert(:at_keyword)
79
+ tassert(:comma)
80
+ tassert(:semicolon)
81
+ tassert(:left_curly)
82
+ tassert(:delim)
83
+ tassert(:colon)
84
+ tassert(:bang)
85
+ tassert(:left_square)
86
+ tassert(:right_square)
87
+ tassert(:left_parens)
88
+ tassert(:right_square)
89
+ tassert(:function)
90
+ tassert(:at_rule)
91
+ tassert(:cdo)
92
+ tassert(:cdc)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "helpers"
4
+
5
+ module Caps
6
+ class Parser
7
+ using Caps::Parser::Helpers
8
+
9
+ def initialize(tokens)
10
+ @tokens = tokens
11
+ @idx = 0
12
+ end
13
+
14
+ def peek
15
+ return nil if @idx >= @tokens.length
16
+
17
+ @tokens[@idx]
18
+ end
19
+
20
+ def prev
21
+ return @tokens.first if @idx.zero?
22
+
23
+ @tokens[@idx - 1]
24
+ end
25
+
26
+ def advance
27
+ return nil if peek.nil?
28
+
29
+ peek.tap { @idx += 1 }
30
+ end
31
+
32
+ def consume_whitespace
33
+ advance while peek.whitespace?
34
+ end
35
+
36
+ def tracking
37
+ start = peek.dig(:position, :start)
38
+ result = yield
39
+ finish = prev.dig(:position, :end)
40
+ result.merge({
41
+ position: {
42
+ start:,
43
+ end: finish
44
+ }
45
+ })
46
+ end
47
+
48
+ def syntax_error!(reason)
49
+ token = peek.nil? ? prev : peek
50
+ raise "Syntax Error: #{reason} at line #{token.end_line} " \
51
+ "column #{token.end_column}"
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "parser/infra"
4
+ require_relative "parser/helpers"
5
+ require_relative "parser/entrypoints"
6
+ require_relative "parser/consumers"
7
+
8
+ module Caps
9
+ class Parser
10
+ using Caps::Parser::Helpers
11
+ include Caps::Parser::Consumers
12
+ include Caps::Parser::Entrypoints
13
+ end
14
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caps
4
+ class Tokenizer
5
+ module Helpers
6
+ refine String do
7
+ def self.helper(name, &)
8
+ define_method(name, &)
9
+ end
10
+
11
+ helper(:uni) { unpack1("U") }
12
+ helper(:between?) { |a, b| uni >= a && uni <= b }
13
+ helper(:digit?) { between? 0x30, 0x39 }
14
+ helper(:hex?) { digit? || between?(0x41, 0x46) || between?(0x61, 0x66) }
15
+ helper(:uppercase?) { between? 0x41, 0x5a }
16
+ helper(:lowercase?) { between? 0x61, 0x7a }
17
+ helper(:letter?) { uppercase? || lowercase? }
18
+
19
+ def non_ascii?
20
+ uni == 0xb7 ||
21
+ between?(0xc0, 0xd6) ||
22
+ between?(0xd8, 0xf6) ||
23
+ between?(0xf8, 0x37d) ||
24
+ between?(0x37f, 0x1fff) ||
25
+ uni == 0x200c ||
26
+ uni == 0x200d ||
27
+ uni == 0x203f ||
28
+ uni == 0x2040 ||
29
+ between?(0x2070, 0x218f) ||
30
+ between?(0x2c00, 0x2fef) ||
31
+ between?(0x3001, 0xd7ff) ||
32
+ between?(0xf900, 0xfdcf) ||
33
+ between?(0xfdf0, 0xfffd) ||
34
+ uni >= 0x10000
35
+ end
36
+
37
+ helper(:ident_start?) { letter? || non_ascii? || uni == 0x5f }
38
+ helper(:ident_char?) { ident_start? || digit? || uni == 0x2d }
39
+ helper(:non_printable?) { between?(0x00, 0x08) || uni == 0xb || between?(0xe, 0x1f) || uni == 0x7f }
40
+ helper(:newline?) { uni == 0xa }
41
+ helper(:whitespace?) { newline? || uni == 0x9 || uni == 0x20 }
42
+ helper(:bad_escape?) { newline? }
43
+ helper(:surrogate?) { between?(0xd800, 0xdfff) }
44
+ end
45
+
46
+ refine NilClass do
47
+ def self.helper(name, &)
48
+ define_method(name, &)
49
+ end
50
+
51
+ helper(:uni) { 0x00 }
52
+ helper(:between?) { |_a, _b| false }
53
+ helper(:digit?) { false }
54
+ helper(:hex?) { false }
55
+ helper(:uppercase?) { false }
56
+ helper(:lowercase?) { false }
57
+ helper(:letter?) { false }
58
+ helper(:non_ascii?) { false }
59
+ helper(:ident_start?) { false }
60
+ helper(:ident_char?) { false }
61
+ helper(:non_printable?) { false }
62
+ helper(:newline?) { false }
63
+ helper(:whitespace?) { false }
64
+ helper(:bad_escape?) { false }
65
+ helper(:surrogate?) { false }
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caps
4
+ class Tokenizer
5
+ using Caps::Tokenizer::Helpers
6
+ attr_accessor :contents, :tokens
7
+
8
+ def self.parse(src)
9
+ new(src).parse!
10
+ end
11
+
12
+ def initialize(contents)
13
+ @contents = self.class.preprocess(contents)
14
+ setup
15
+ end
16
+
17
+ def parse!
18
+ consume_token until eof?
19
+ @tokens
20
+ end
21
+
22
+ def pos
23
+ { idx: @idx, line: @line, column: @column }
24
+ end
25
+
26
+ private
27
+
28
+ def self.preprocess(contents)
29
+ codepoints = []
30
+ clusters = contents.grapheme_clusters
31
+ i = 0
32
+ until i == clusters.length
33
+ code = clusters[i]
34
+
35
+ if code.uni == 0x0d && clusters[i + 1].uni == 0x0a
36
+ codepoints << LINE_FEED
37
+ i += 1
38
+ next
39
+ end
40
+
41
+ code = LINE_FEED if code.uni == 0x0d || code.uni == 0x0c
42
+
43
+ code = REPLACEMENT_CHARACTER if code.between?(0xd800, 0xdbff) || code.between?(0xdc00, 0xdfff) || code.uni.zero?
44
+
45
+ codepoints << code
46
+ i += 1
47
+ end
48
+
49
+ codepoints
50
+ end
51
+
52
+ def setup
53
+ @idx = 0
54
+ @len = @contents.length
55
+ @line = 1
56
+ @column = 1
57
+ @last_line_length = 0
58
+ @tokens = []
59
+ end
60
+
61
+ def eof?
62
+ @idx >= @contents.length
63
+ end
64
+
65
+ def peek(qty = 0)
66
+ return nil if @idx + qty >= @len
67
+
68
+ @contents[@idx + qty]
69
+ end
70
+
71
+ def peek1
72
+ peek(1)
73
+ end
74
+
75
+ def peek2
76
+ peek(2)
77
+ end
78
+
79
+ def advance
80
+ chr = @contents[@idx]
81
+ @idx += 1
82
+ if @idx > @len
83
+ raise "[BUG] Over-read! @idx=#{@idx} @len=#{@len} #advance without #peek or #eof?\n#{Thread.current.backtrace.reject do |v|
84
+ v.start_with?("/opt") || v.start_with?("/usr")
85
+ end.join("\n")}"
86
+ end
87
+
88
+ return chr if eof?
89
+
90
+ if @contents[@idx].newline?
91
+ @line += 1
92
+ @last_line_length = @column
93
+ @column = 1
94
+ else
95
+ @column += 1
96
+ end
97
+
98
+ chr
99
+ end
100
+
101
+ def scoped
102
+ start = @idx
103
+ yield
104
+ @contents[start...@idx]
105
+ end
106
+
107
+ def isolated
108
+ old = pos
109
+ begin
110
+ yield
111
+ ensure
112
+ @idx = old[:idx]
113
+ @line = old[:line]
114
+ @column = old[:column]
115
+ end
116
+ end
117
+
118
+ def mark_pos
119
+ Location.new(self)
120
+ end
121
+
122
+ def pack_while(type)
123
+ start = mark_pos
124
+ data = scoped do
125
+ loop do
126
+ break if !yield || eof?
127
+
128
+ advance
129
+ end
130
+ end
131
+
132
+ start.push_node(type, value: data.join)
133
+ end
134
+
135
+ def pack_one(type)
136
+ pack = true
137
+ pack_while(type) { pack.tap { pack = !pack } }
138
+ end
139
+
140
+ def valid_escape?(offset: 0)
141
+ check = -> { peek == REVERSE_SOLIDUS && peek1 != LINE_FEED }
142
+ if offset.positive?
143
+ isolated do
144
+ offset.times { advance }
145
+ check.call
146
+ end
147
+ else
148
+ check.call
149
+ end
150
+ end
151
+
152
+ def ident_sequence_start?
153
+ case peek
154
+ when HYPHEN_MINUS
155
+ (peek1.ident_start? || peek1 == HYPHEN_MINUS) || valid_escape?(offset: 1)
156
+ when REVERSE_SOLIDUS
157
+ valid_escape?
158
+ else
159
+ peek.ident_start?
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caps
4
+ class Tokenizer
5
+ class Location
6
+ attr_reader :start
7
+
8
+ def initialize(parser)
9
+ @parser = parser
10
+ @start = parser.pos
11
+ end
12
+
13
+ def push_node(type, **opts)
14
+ @parser.push_node(type, **opts.merge({ position: finish }))
15
+ end
16
+
17
+ def finish
18
+ {
19
+ start: @start,
20
+ end: @parser.pos
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end