xrb 0.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/bake/xrb/entities.rb +60 -0
  4. data/bake/xrb/parsers.rb +69 -0
  5. data/ext/extconf.rb +21 -0
  6. data/ext/xrb/escape.c +152 -0
  7. data/ext/xrb/escape.h +15 -0
  8. data/ext/xrb/markup.c +1949 -0
  9. data/ext/xrb/markup.h +6 -0
  10. data/ext/xrb/markup.rl +226 -0
  11. data/ext/xrb/query.c +619 -0
  12. data/ext/xrb/query.h +6 -0
  13. data/ext/xrb/query.rl +82 -0
  14. data/ext/xrb/tag.c +204 -0
  15. data/ext/xrb/tag.h +21 -0
  16. data/ext/xrb/template.c +1114 -0
  17. data/ext/xrb/template.h +6 -0
  18. data/ext/xrb/template.rl +77 -0
  19. data/ext/xrb/xrb.c +72 -0
  20. data/ext/xrb/xrb.h +132 -0
  21. data/lib/xrb/buffer.rb +103 -0
  22. data/lib/xrb/builder.rb +222 -0
  23. data/lib/xrb/entities.rb +2137 -0
  24. data/lib/xrb/entities.xrb +30 -0
  25. data/lib/xrb/error.rb +81 -0
  26. data/lib/xrb/fallback/markup.rb +1658 -0
  27. data/lib/xrb/fallback/markup.rl +228 -0
  28. data/lib/xrb/fallback/query.rb +548 -0
  29. data/lib/xrb/fallback/query.rl +88 -0
  30. data/lib/xrb/fallback/template.rb +829 -0
  31. data/lib/xrb/fallback/template.rl +80 -0
  32. data/lib/xrb/markup.rb +56 -0
  33. data/lib/xrb/native.rb +15 -0
  34. data/lib/xrb/parse_delegate.rb +19 -0
  35. data/lib/xrb/parsers.rb +17 -0
  36. data/lib/xrb/query.rb +80 -0
  37. data/lib/xrb/reference.rb +108 -0
  38. data/lib/xrb/strings.rb +47 -0
  39. data/lib/xrb/tag.rb +115 -0
  40. data/lib/xrb/template.rb +164 -0
  41. data/lib/xrb/uri.rb +100 -0
  42. data/lib/xrb/version.rb +8 -0
  43. data/lib/xrb.rb +11 -0
  44. data/license.md +23 -0
  45. data/readme.md +29 -0
  46. data.tar.gz.sig +0 -0
  47. metadata +109 -58
  48. metadata.gz.sig +0 -0
  49. data/README +0 -60
  50. data/app/helpers/ui_helper.rb +0 -80
  51. data/app/models/xrb/element.rb +0 -9
  52. data/lib/xrb/engine.rb +0 -4
  53. data/rails/init.rb +0 -1
  54. data/xrb.gemspec +0 -12
@@ -0,0 +1,80 @@
1
+ # Released under the MIT License.
2
+ # Copyright, 2016-2024, by Samuel Williams.
3
+
4
+ %%{
5
+ machine template;
6
+
7
+ action instruction_begin {
8
+ instruction_begin = p
9
+ }
10
+
11
+ action instruction_end {
12
+ instruction_end = p
13
+ }
14
+
15
+ action emit_instruction {
16
+ delegate.instruction(data.byteslice(instruction_begin...instruction_end))
17
+ }
18
+
19
+ action emit_instruction_line {
20
+ delegate.instruction(data.byteslice(instruction_begin...instruction_end), "\n")
21
+ }
22
+
23
+ action instruction_error {
24
+ raise ParseError.new("failed to parse instruction", buffer, p)
25
+ }
26
+
27
+ action expression_begin {
28
+ expression_begin = p
29
+ }
30
+
31
+ action expression_end {
32
+ expression_end = p
33
+ }
34
+
35
+ action emit_expression {
36
+ delegate.expression(data.byteslice(expression_begin...expression_end))
37
+ }
38
+
39
+ action expression_error {
40
+ raise ParseError.new("failed to parse expression", buffer, p)
41
+ }
42
+
43
+ action emit_text {
44
+ delegate.text(data.byteslice(ts...te))
45
+ }
46
+
47
+ # This magic ensures that we process bytes.
48
+ getkey bytes[p];
49
+
50
+ include template "xrb/template.rl";
51
+ }%%
52
+
53
+ require_relative '../error'
54
+
55
+ module XRB
56
+ module Fallback
57
+ %% write data;
58
+
59
+ def self.parse_template(buffer, delegate)
60
+ data = buffer.read
61
+ bytes = data.bytes
62
+
63
+ p = 0
64
+ pe = eof = data.bytesize
65
+ stack = []
66
+
67
+ expression_begin = expression_end = nil
68
+ instruction_begin = instruction_end = nil
69
+
70
+ %% write init;
71
+ %% write exec;
72
+
73
+ if p != eof
74
+ raise ParseError.new("could not consume all input", buffer, p)
75
+ end
76
+
77
+ return nil
78
+ end
79
+ end
80
+ end
data/lib/xrb/markup.rb ADDED
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2016-2024, by Samuel Williams.
5
+
6
+ require 'cgi'
7
+
8
+ module XRB
9
+ # A wrapper which indicates that `value` can be appended to the output buffer without any changes.
10
+ module Markup
11
+ # Converts special characters `<`, `>`, `&`, and `"` into their equivalent entities.
12
+ # @return [String] May return the original string if no changes were made.
13
+ def self.escape_string(string)
14
+ CGI.escape_html(string)
15
+ end
16
+
17
+ # Appends a string to the output buffer, escaping if if necessary.
18
+ def self.append(buffer, value)
19
+ if value.is_a? Markup
20
+ buffer << value
21
+ elsif value
22
+ buffer << self.escape_string(value.to_s)
23
+ end
24
+ end
25
+ end
26
+
27
+ # Initialized from text which is escaped to use HTML entities.
28
+ class MarkupString < String
29
+ include Markup
30
+
31
+ # @param string [String] the string value itself.
32
+ # @param escape [Boolean] whether or not to escape the string.
33
+ def initialize(string = nil, escape = true)
34
+ if string
35
+ if escape
36
+ string = Markup.escape_string(string)
37
+ end
38
+
39
+ super(string)
40
+ else
41
+ super()
42
+ end
43
+ end
44
+
45
+ # Generate a valid MarkupString withot any escaping.
46
+ def self.raw(string)
47
+ self.new(string, false)
48
+ end
49
+ end
50
+
51
+ module Script
52
+ def self.json(value)
53
+ MarkupString.new(JSON.dump(value), false)
54
+ end
55
+ end
56
+ end
data/lib/xrb/native.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2016-2024, by Samuel Williams.
5
+
6
+ require_relative 'error'
7
+
8
+ # Methods on the following classes may be replaced by native implementations:
9
+ require_relative 'tag'
10
+
11
+ begin
12
+ require 'XRB_Extension'
13
+ rescue LoadError => error
14
+ warn "Could not load native parsers: #{error}"
15
+ end unless ENV['XRB_PREFER_FALLBACK']
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2016-2024, by Samuel Williams.
5
+
6
+ module XRB
7
+ # This is a sample delegate for capturing all events. It's only use is for testing.
8
+ class ParseDelegate
9
+ def initialize
10
+ @events = []
11
+ end
12
+
13
+ attr :events
14
+
15
+ def method_missing(*args)
16
+ @events << args
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2012-2024, by Samuel Williams.
5
+
6
+ require_relative 'native'
7
+ require_relative 'parse_delegate'
8
+
9
+ if defined? XRB::Native
10
+ XRB::Parsers = XRB::Native
11
+ else
12
+ require_relative 'fallback/markup'
13
+ require_relative 'fallback/template'
14
+ require_relative 'fallback/query'
15
+
16
+ XRB::Parsers = XRB::Fallback
17
+ end
data/lib/xrb/query.rb ADDED
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2020-2024, by Samuel Williams.
5
+
6
+ require_relative 'buffer'
7
+ require_relative 'parsers'
8
+
9
+ require 'uri'
10
+
11
+ module XRB
12
+ module Query
13
+ def self.parse(buffer)
14
+ Hash.new.tap do |query|
15
+ Parsers.parse_query(buffer, Delegate.new(query))
16
+ end
17
+ end
18
+
19
+ class Delegate
20
+ def initialize(top = {})
21
+ @top = top
22
+
23
+ @current = @top
24
+ @index = nil
25
+ end
26
+
27
+ def string(key, encoded)
28
+ if encoded
29
+ key = ::URI.decode_www_form_component(key)
30
+ end
31
+
32
+ index(key.to_sym)
33
+ end
34
+
35
+ def integer(key)
36
+ index(key.to_i)
37
+ end
38
+
39
+ def index(key)
40
+ if @index
41
+ @current = @current.fetch(@index) do
42
+ @current[@index] = {}
43
+ end
44
+ end
45
+
46
+ @index = key
47
+ end
48
+
49
+ def append
50
+ if @index
51
+ @current = @current.fetch(@index) do
52
+ @current[@index] = []
53
+ end
54
+ end
55
+
56
+ @index = @current.size
57
+ end
58
+
59
+ def assign(value, encoded)
60
+ if encoded
61
+ value = ::URI.decode_www_form_component(value)
62
+ end
63
+
64
+ @current[@index] = value
65
+
66
+ @current = @top
67
+ @index = nil
68
+ end
69
+
70
+ def pair
71
+ if @index
72
+ @current[@index] = true
73
+ end
74
+
75
+ @current = @top
76
+ @index = nil
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2020-2024, by Samuel Williams.
5
+
6
+ require_relative 'query'
7
+
8
+ module XRB
9
+ class Reference
10
+ def initialize(path, query = {}, fragment: nil)
11
+ @path = path.to_s
12
+ @query = query
13
+ @fragment = fragment
14
+ end
15
+
16
+ # The path component of the URI, e.g. /foo/bar/index.html
17
+ attr :path
18
+
19
+ # The query parameters.
20
+ attr :query
21
+
22
+ # A fragment identifier, the part after the '#'
23
+ attr :fragment
24
+
25
+ def append(buffer)
26
+ buffer << escape_path(@path)
27
+
28
+ unless @query.empty?
29
+ buffer << '?' << query_string
30
+ end
31
+
32
+ if @fragment
33
+ buffer << '#' << escape(@fragment)
34
+ end
35
+
36
+ return buffer
37
+ end
38
+
39
+ def to_str
40
+ append(String.new)
41
+ end
42
+
43
+ alias to_s to_str
44
+
45
+ private
46
+
47
+ # According to https://tools.ietf.org/html/rfc3986#section-3.3, we escape non-pchar.
48
+ NON_PCHAR = /([^
49
+ a-zA-Z0-9
50
+ \-\._~
51
+ !\$&'\(\)\*\+,;=
52
+ :@\/
53
+ ]+)/x.freeze
54
+
55
+ def escape_path(path)
56
+ encoding = path.encoding
57
+ path.b.gsub(NON_PCHAR) do |m|
58
+ '%' + m.unpack('H2' * m.bytesize).join('%').upcase
59
+ end.force_encoding(encoding)
60
+ end
61
+
62
+ # Escapes a generic string, using percent encoding.
63
+ def escape(string)
64
+ encoding = string.encoding
65
+ string.b.gsub(/([^a-zA-Z0-9_.\-]+)/) do |m|
66
+ '%' + m.unpack('H2' * m.bytesize).join('%').upcase
67
+ end.force_encoding(encoding)
68
+ end
69
+
70
+ def query_string
71
+ build_nested_query(@query)
72
+ end
73
+
74
+ def build_nested_query(value, prefix = nil)
75
+ case value
76
+ when Array
77
+ value.map { |v|
78
+ build_nested_query(v, "#{prefix}[]")
79
+ }.join("&")
80
+ when Hash
81
+ value.map { |k, v|
82
+ build_nested_query(v, prefix ? "#{prefix}[#{escape(k.to_s)}]" : escape(k.to_s))
83
+ }.reject(&:empty?).join('&')
84
+ when nil
85
+ prefix
86
+ else
87
+ raise ArgumentError, "value must be a Hash" if prefix.nil?
88
+ "#{prefix}=#{escape(value.to_s)}"
89
+ end
90
+ end
91
+ end
92
+
93
+ # Generate a URI from a path and user parameters. The path may contain a `#fragment` or `?query=parameters`.
94
+ def self.Reference(path = '', **parameters)
95
+ base, fragment = path.split('#', 2)
96
+ path, query_string = base.split('?', 2)
97
+
98
+ if query_string
99
+ query = Query.parse(Buffer.new(query_string))
100
+ else
101
+ query = {}
102
+ end
103
+
104
+ query.update(parameters)
105
+
106
+ Reference.new(path, query, fragment: fragment)
107
+ end
108
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2012-2024, by Samuel Williams.
5
+
6
+ module XRB
7
+ module Strings
8
+ HTML_ESCAPE = {"&" => "&amp;", "<" => "&lt;", ">" => "&gt;", "\"" => "&quot;"}
9
+ HTML_ESCAPE_PATTERN = Regexp.new("[" + Regexp.quote(HTML_ESCAPE.keys.join) + "]")
10
+
11
+ def self.to_html(string)
12
+ string.gsub(HTML_ESCAPE_PATTERN){|c| HTML_ESCAPE[c]}
13
+ end
14
+
15
+ def self.to_quoted_string(string)
16
+ string = string.gsub('"', '\\"')
17
+ string.gsub!(/\r/, "\\r")
18
+ string.gsub!(/\n/, "\\n")
19
+
20
+ return "\"#{string}\""
21
+ end
22
+
23
+ # `value` must already be escaped.
24
+ def self.to_attribute(key, value)
25
+ %Q{#{key}="#{value}"}
26
+ end
27
+
28
+ def self.to_simple_attribute(key, strict)
29
+ strict ? %Q{#{key}="#{key}"} : key.to_s
30
+ end
31
+
32
+ def self.to_title(string)
33
+ string = string.gsub(/(^|[ \-_])(.)/){" " + $2.upcase}
34
+ string.strip!
35
+
36
+ return string
37
+ end
38
+
39
+ def self.to_snake(string)
40
+ string = string.gsub("::", "")
41
+ string.gsub!(/([A-Z]+)/){"_" + $1.downcase}
42
+ string.sub!(/^_+/, "")
43
+
44
+ return string
45
+ end
46
+ end
47
+ end
data/lib/xrb/tag.rb ADDED
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2017-2024, by Samuel Williams.
5
+
6
+ require_relative 'markup'
7
+
8
+ module XRB
9
+ # This represents an individual SGML tag, e.g. <a>, </a> or <a />, with attributes. Attribute values must be escaped.
10
+ Tag = Struct.new(:name, :closed, :attributes) do
11
+ include XRB::Markup
12
+
13
+ def self.split(qualified_name)
14
+ if i = qualified_name.index(':')
15
+ return qualified_name.slice(0...i), qualified_name.slice(i+1..-1)
16
+ else
17
+ return nil, qualified_name
18
+ end
19
+ end
20
+
21
+ def self.closed(name, attributes = {})
22
+ self.new(name, true, attributes)
23
+ end
24
+
25
+ def self.opened(name, attributes = {})
26
+ self.new(name, false, attributes)
27
+ end
28
+
29
+ def [] key
30
+ attributes[key]
31
+ end
32
+
33
+ alias to_hash attributes
34
+
35
+ def to_s(content = nil)
36
+ self.class.format_tag(name, attributes, content || !closed)
37
+ end
38
+
39
+ alias to_str to_s
40
+
41
+ def self_closed?
42
+ closed
43
+ end
44
+
45
+ def write_opening_tag(buffer)
46
+ buffer << '<' << name
47
+
48
+ self.class.append_attributes(buffer, attributes, nil)
49
+
50
+ if self_closed?
51
+ buffer << '/>'
52
+ else
53
+ buffer << '>'
54
+ end
55
+ end
56
+
57
+ def write_closing_tag(buffer)
58
+ buffer << '</' << name << '>'
59
+ end
60
+
61
+ def write(buffer, content = nil)
62
+ self.class.append_tag(buffer, name, attributes, content || !closed)
63
+ end
64
+
65
+ def self.format_tag(name, attributes, content)
66
+ buffer = String.new.force_encoding(name.encoding)
67
+
68
+ self.append_tag(buffer, name, attributes, content)
69
+
70
+ return buffer
71
+ end
72
+
73
+ def self.append_tag(buffer, name, attributes, content)
74
+ buffer << '<' << name.to_s
75
+
76
+ self.append_attributes(buffer, attributes, nil)
77
+
78
+ if !content
79
+ buffer << '/>'
80
+ else
81
+ buffer << '>'
82
+ unless content == true
83
+ Markup.append(buffer, content)
84
+ end
85
+ buffer << '</' << name.to_s << '>'
86
+ end
87
+
88
+ return nil
89
+ end
90
+
91
+ # Convert a set of attributes into a string suitable for use within a <tag>.
92
+ def self.append_attributes(buffer, attributes, prefix)
93
+ attributes.each do |key, value|
94
+ next unless value
95
+
96
+ attribute_key = prefix ? "#{prefix}-#{key}" : key
97
+
98
+ case value
99
+ when Hash
100
+ self.append_attributes(buffer, value, attribute_key)
101
+ when Array
102
+ self.append_attributes(buffer, value, attribute_key)
103
+ when TrueClass
104
+ buffer << ' ' << attribute_key.to_s
105
+ else
106
+ buffer << ' ' << attribute_key.to_s << '="'
107
+ Markup.append(buffer, value)
108
+ buffer << '"'
109
+ end
110
+ end
111
+
112
+ return nil
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2012-2024, by Samuel Williams.
5
+
6
+ require_relative 'parsers'
7
+ require_relative 'markup'
8
+ require_relative 'buffer'
9
+ require_relative 'builder'
10
+
11
+ module XRB
12
+ # The output variable that will be used in templates:
13
+ OUT = :_out
14
+ BINDING = binding
15
+
16
+ class Builder
17
+ def capture(*arguments, &block)
18
+ Template.capture(*arguments, output: self, &block)
19
+ end
20
+ end
21
+
22
+ class Template
23
+ # Returns the output produced by calling the given block.
24
+ def self.capture(*arguments, output: nil, &block)
25
+ scope = block.binding
26
+ previous_output = scope.local_variable_get(OUT)
27
+
28
+ output ||= previous_output.class.new(encoding: previous_output.encoding)
29
+ scope.local_variable_set(OUT, output)
30
+
31
+ begin
32
+ block.call(*arguments)
33
+ ensure
34
+ scope.local_variable_set(OUT, previous_output)
35
+ end
36
+
37
+ return output
38
+ end
39
+
40
+ # Returns the buffer used for capturing output.
41
+ def self.buffer(binding)
42
+ binding.local_variable_get(OUT)
43
+ end
44
+
45
+ class Assembler
46
+ def initialize(encoding: Encoding::UTF_8)
47
+ @code = String.new.force_encoding(encoding)
48
+ end
49
+
50
+ attr :code
51
+
52
+ # Output raw text to the template.
53
+ def text(text)
54
+ text = text.gsub("'", "\\\\'")
55
+ @code << "#{OUT}<<'#{text}';"
56
+
57
+ # This is an interesting approach, but it doens't preserve newlines or tabs as raw characters, so template line numbers don't match up.
58
+ # @parts << "#{OUT}<<#{text.dump};"
59
+ end
60
+
61
+ # Output a ruby expression (or part of).
62
+ def instruction(text, postfix = nil)
63
+ @code << text << (postfix || ';')
64
+ end
65
+
66
+ # Output a string interpolation.
67
+ def expression(text)
68
+ # Double brackets are required here to handle expressions like #{foo rescue "bar"}.
69
+ @code << "#{OUT}<<String(#{text});"
70
+ end
71
+ end
72
+
73
+ def self.load_file(path, **options)
74
+ self.new(FileBuffer.new(path), **options).freeze
75
+ end
76
+
77
+ def self.load(string, *arguments, **options)
78
+ self.new(Buffer.new(string), **options).freeze
79
+ end
80
+
81
+ # @param binding [Binding] The binding in which the template is compiled. e.g. `TOPLEVEL_BINDING`.
82
+ def initialize(buffer, binding: BINDING)
83
+ @buffer = buffer
84
+ @binding = binding
85
+ end
86
+
87
+ def freeze
88
+ return self if frozen?
89
+
90
+ to_proc
91
+
92
+ super
93
+ end
94
+
95
+ def to_string(scope = Object.new, output = nil)
96
+ output ||= output_buffer
97
+
98
+ scope.instance_exec(output, &to_proc)
99
+
100
+ return output
101
+ end
102
+
103
+ def to_buffer(scope)
104
+ Buffer.new(to_string(scope), path: @buffer.path)
105
+ end
106
+
107
+ def to_proc(scope = @binding.dup)
108
+ @compiled_proc ||= eval("\# frozen_string_literal: true\nproc{|#{OUT}|;#{code}}", scope, @buffer.path, 0).freeze
109
+ end
110
+
111
+ protected
112
+
113
+ def output_buffer
114
+ String.new.force_encoding(code.encoding)
115
+ end
116
+
117
+ def code
118
+ @code ||= compile!
119
+ end
120
+
121
+ def make_assembler
122
+ Assembler.new
123
+ end
124
+
125
+ def compile!
126
+ assembler = make_assembler
127
+
128
+ Parsers.parse_template(@buffer, assembler)
129
+
130
+ assembler.code
131
+ end
132
+ end
133
+
134
+ class MarkupTemplate < Template
135
+ class Assembler < Template::Assembler
136
+ # Output a string interpolation.
137
+ def expression(code)
138
+ @code << "#{OUT}<<(#{code});"
139
+ end
140
+
141
+ # Output raw text to the template.
142
+ def text(text)
143
+ text = text.gsub("'", "\\\\'")
144
+ @code << "#{OUT}.raw('#{text}');"
145
+ end
146
+ end
147
+
148
+ def to_string(scope = Object.new, output = nil)
149
+ super.output
150
+ end
151
+
152
+ protected
153
+
154
+ # We need an assembler which builds specific `Markup.append` sequences.
155
+ def make_assembler
156
+ Assembler.new
157
+ end
158
+
159
+ # The output of the markup template is encoded markup (e.g. with entities, tags, etc).
160
+ def output_buffer
161
+ Builder.new(encoding: code.encoding)
162
+ end
163
+ end
164
+ end