xrb 0.1 → 0.3.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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +1 -0
  3. data/bake/xrb/entities.rb +60 -0
  4. data/bake/xrb/parsers.rb +66 -0
  5. data/ext/Makefile +270 -0
  6. data/ext/XRB_Extension.bundle +0 -0
  7. data/ext/escape.o +0 -0
  8. data/ext/extconf.h +5 -0
  9. data/ext/extconf.rb +21 -0
  10. data/ext/markup.o +0 -0
  11. data/ext/mkmf.log +122 -0
  12. data/ext/query.o +0 -0
  13. data/ext/tag.o +0 -0
  14. data/ext/template.o +0 -0
  15. data/ext/xrb/escape.c +152 -0
  16. data/ext/xrb/escape.h +15 -0
  17. data/ext/xrb/markup.c +1949 -0
  18. data/ext/xrb/markup.h +6 -0
  19. data/ext/xrb/markup.rl +226 -0
  20. data/ext/xrb/query.c +619 -0
  21. data/ext/xrb/query.h +6 -0
  22. data/ext/xrb/query.rl +82 -0
  23. data/ext/xrb/tag.c +204 -0
  24. data/ext/xrb/tag.h +21 -0
  25. data/ext/xrb/template.c +1114 -0
  26. data/ext/xrb/template.h +6 -0
  27. data/ext/xrb/template.rl +77 -0
  28. data/ext/xrb/xrb.c +72 -0
  29. data/ext/xrb/xrb.h +132 -0
  30. data/ext/xrb.o +0 -0
  31. data/lib/xrb/buffer.rb +103 -0
  32. data/lib/xrb/builder.rb +229 -0
  33. data/lib/xrb/entities.rb +2137 -0
  34. data/lib/xrb/entities.xrb +15 -0
  35. data/lib/xrb/error.rb +81 -0
  36. data/lib/xrb/fallback/markup.rb +1657 -0
  37. data/lib/xrb/fallback/markup.rl +227 -0
  38. data/lib/xrb/fallback/query.rb +548 -0
  39. data/lib/xrb/fallback/query.rl +88 -0
  40. data/lib/xrb/fallback/template.rb +829 -0
  41. data/lib/xrb/fallback/template.rl +80 -0
  42. data/lib/xrb/markup.rb +56 -0
  43. data/lib/xrb/native.rb +15 -0
  44. data/lib/xrb/parsers.rb +16 -0
  45. data/lib/xrb/query.rb +80 -0
  46. data/lib/xrb/reference.rb +108 -0
  47. data/lib/xrb/strings.rb +47 -0
  48. data/lib/xrb/tag.rb +115 -0
  49. data/lib/xrb/template.rb +128 -0
  50. data/lib/xrb/uri.rb +100 -0
  51. data/lib/xrb/version.rb +8 -0
  52. data/lib/xrb.rb +11 -0
  53. data/license.md +23 -0
  54. data/readme.md +34 -0
  55. data.tar.gz.sig +0 -0
  56. metadata +118 -58
  57. metadata.gz.sig +2 -0
  58. data/README +0 -60
  59. data/app/helpers/ui_helper.rb +0 -80
  60. data/app/models/xrb/element.rb +0 -9
  61. data/lib/xrb/engine.rb +0 -4
  62. data/rails/init.rb +0 -1
  63. 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,16 @@
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
+
8
+ if defined? XRB::Native
9
+ XRB::Parsers = XRB::Native
10
+ else
11
+ require_relative 'fallback/markup'
12
+ require_relative 'fallback/template'
13
+ require_relative 'fallback/query'
14
+
15
+ XRB::Parsers = XRB::Fallback
16
+ 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,128 @@
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}.raw('#{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(code)
68
+ @code << "#{OUT}<<(#{code});"
69
+ end
70
+ end
71
+
72
+ def self.load_file(path, **options)
73
+ self.new(FileBuffer.new(path), **options).freeze
74
+ end
75
+
76
+ def self.load(string, *arguments, **options)
77
+ self.new(Buffer.new(string), **options).freeze
78
+ end
79
+
80
+ # @param binding [Binding] The binding in which the template is compiled. e.g. `TOPLEVEL_BINDING`.
81
+ def initialize(buffer, binding: BINDING)
82
+ @buffer = buffer
83
+ @binding = binding
84
+ end
85
+
86
+ def freeze
87
+ return self if frozen?
88
+
89
+ to_proc
90
+
91
+ super
92
+ end
93
+
94
+ def to_string(scope = Object.new, output = nil)
95
+ builder = Builder.new(output, encoding: code.encoding)
96
+
97
+ scope.instance_exec(builder, &to_proc)
98
+
99
+ return builder.output
100
+ end
101
+
102
+ def to_buffer(scope)
103
+ Buffer.new(to_string(scope), path: @buffer.path)
104
+ end
105
+
106
+ def to_proc(scope = @binding.dup)
107
+ @compiled_proc ||= eval("\# frozen_string_literal: true\nproc{|#{OUT}|;#{code}}", scope, @buffer.path, 0).freeze
108
+ end
109
+
110
+ protected
111
+
112
+ def code
113
+ @code ||= compile!
114
+ end
115
+
116
+ def make_assembler
117
+ Assembler.new
118
+ end
119
+
120
+ def compile!
121
+ assembler = make_assembler
122
+
123
+ Parsers.parse_template(@buffer, assembler)
124
+
125
+ assembler.code
126
+ end
127
+ end
128
+ end