phlex 2.0.0.beta2 → 2.0.0.rc2

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.
@@ -2,71 +2,98 @@
2
2
 
3
3
  # Void HTML elements don't accept content and never have a closing tag.
4
4
  module Phlex::HTML::VoidElements
5
- extend Phlex::Elements
5
+ extend Phlex::SGML::Elements
6
6
 
7
- # @!method area(**attributes, &content)
8
- # Outputs an `<area>` tag.
9
- # @return [nil]
10
- # @see https://developer.mozilla.org/docs/Web/HTML/Element/area
11
- register_void_element :area
12
-
13
- # @!method br(**attributes, &content)
14
- # Outputs a `<br>` tag.
15
- # @return [nil]
16
- # @see https://developer.mozilla.org/docs/Web/HTML/Element/br
17
- register_void_element :br
18
-
19
- # @!method col(**attributes, &content)
20
- # Outputs a `<col>` tag.
21
- # @return [nil]
22
- # @see https://developer.mozilla.org/docs/Web/HTML/Element/col
23
- register_void_element :col
24
-
25
- # @!method embed(**attributes, &content)
26
- # Outputs an `<embed>` tag.
27
- # @return [nil]
28
- # @see https://developer.mozilla.org/docs/Web/HTML/Element/embed
29
- register_void_element :embed
30
-
31
- # @!method hr(**attributes, &content)
32
- # Outputs an `<hr>` tag.
33
- # @return [nil]
34
- # @see https://developer.mozilla.org/docs/Web/HTML/Element/hr
35
- register_void_element :hr
36
-
37
- # @!method img(**attributes, &content)
38
- # Outputs an `<img>` tag.
39
- # @return [nil]
40
- # @see https://developer.mozilla.org/docs/Web/HTML/Element/img
41
- register_void_element :img
42
-
43
- # @!method input(**attributes, &content)
44
- # Outputs an `<input>` tag.
45
- # @return [nil]
46
- # @see https://developer.mozilla.org/docs/Web/HTML/Element/input
47
- register_void_element :input
48
-
49
- # @!method link(**attributes, &content)
50
- # Outputs a `<link>` tag.
51
- # @return [nil]
52
- # @see https://developer.mozilla.org/docs/Web/HTML/Element/link
53
- register_void_element :link
54
-
55
- # @!method meta(**attributes, &content)
56
- # Outputs a `<meta>` tag.
57
- # @return [nil]
58
- # @see https://developer.mozilla.org/docs/Web/HTML/Element/meta
59
- register_void_element :meta
60
-
61
- # @!method source(**attributes, &content)
62
- # Outputs a `<source>` tag.
63
- # @return [nil]
64
- # @see https://developer.mozilla.org/docs/Web/HTML/Element/source
65
- register_void_element :source
66
-
67
- # @!method track(**attributes, &content)
68
- # Outputs a `<track>` tag.
69
- # @return [nil]
70
- # @see https://developer.mozilla.org/docs/Web/HTML/Element/track
71
- register_void_element :track
7
+ # Outputs an `<area>` tag.
8
+ # See https://developer.mozilla.org/docs/Web/HTML/Element/area
9
+ __register_void_element__ def area(
10
+ class: nil,
11
+ id: nil,
12
+ **attributes
13
+ ) = nil
14
+ # Outputs a `<base>` tag.
15
+ # See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
16
+ __register_void_element__ def base(
17
+ class: nil,
18
+ href: nil,
19
+ id: nil,
20
+ target: nil,
21
+ **attributes
22
+ ) = nil
23
+ # Outputs a `<br>` tag.
24
+ # See https://developer.mozilla.org/docs/Web/HTML/Element/br
25
+ __register_void_element__ def br(
26
+ class: nil,
27
+ id: nil,
28
+ **attributes
29
+ ) = nil
30
+ # Outputs a `<col>` tag.
31
+ # See https://developer.mozilla.org/docs/Web/HTML/Element/col
32
+ __register_void_element__ def col(
33
+ class: nil,
34
+ id: nil,
35
+ **attributes
36
+ ) = nil
37
+ # Outputs an `<embed>` tag.
38
+ # See https://developer.mozilla.org/docs/Web/HTML/Element/embed
39
+ __register_void_element__ def embed(
40
+ class: nil,
41
+ id: nil,
42
+ **attributes
43
+ ) = nil
44
+ # Outputs an `<hr>` tag.
45
+ # See https://developer.mozilla.org/docs/Web/HTML/Element/hr
46
+ __register_void_element__ def hr(
47
+ class: nil,
48
+ id: nil,
49
+ **attributes
50
+ ) = nil
51
+ # Outputs an `<img>` tag.
52
+ # See https://developer.mozilla.org/docs/Web/HTML/Element/img
53
+ __register_void_element__ def img(
54
+ alt: nil,
55
+ class: nil,
56
+ id: nil,
57
+ src: nil,
58
+ **attributes
59
+ ) = nil
60
+ # Outputs an `<input>` tag.
61
+ # See https://developer.mozilla.org/docs/Web/HTML/Element/input
62
+ __register_void_element__ def input(
63
+ class: nil,
64
+ id: nil,
65
+ name: nil,
66
+ type: nil,
67
+ **attributes
68
+ ) = nil
69
+ # Outputs a `<link>` tag.
70
+ # See https://developer.mozilla.org/docs/Web/HTML/Element/link
71
+ __register_void_element__ def link(
72
+ class: nil,
73
+ id: nil,
74
+ **attributes
75
+ ) = nil
76
+ # Outputs a `<meta>` tag.
77
+ # See https://developer.mozilla.org/docs/Web/HTML/Element/meta
78
+ __register_void_element__ def meta(
79
+ charset: nil,
80
+ class: nil,
81
+ id: nil,
82
+ name: nil,
83
+ **attributes
84
+ ) = nil
85
+ # Outputs a `<source>` tag.
86
+ # See https://developer.mozilla.org/docs/Web/HTML/Element/source
87
+ __register_void_element__ def source(
88
+ class: nil,
89
+ id: nil,
90
+ **attributes
91
+ ) = nil
92
+ # Outputs a `<track>` tag.
93
+ # See https://developer.mozilla.org/docs/Web/HTML/Element/track
94
+ __register_void_element__ def track(
95
+ class: nil,
96
+ id: nil,
97
+ **attributes
98
+ ) = nil
72
99
  end
data/lib/phlex/html.rb CHANGED
@@ -4,18 +4,15 @@ class Phlex::HTML < Phlex::SGML
4
4
  autoload :StandardElements, "phlex/html/standard_elements"
5
5
  autoload :VoidElements, "phlex/html/void_elements"
6
6
 
7
- # A list of HTML attributes that have the potential to execute unsafe JavaScript.
8
- UNSAFE_ATTRIBUTES = Set.new(%w[onabort onafterprint onbeforeprint onbeforeunload onblur oncanplay oncanplaythrough onchange onclick oncontextmenu oncopy oncuechange oncut ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended onerror onfocus onhashchange oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart onmessage onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onoffline ononline onpagehide onpageshow onpaste onpause onplay onplaying onpopstate onprogress onratechange onreset onresize onscroll onsearch onseeked onseeking onselect onstalled onstorage onsubmit onsuspend ontimeupdate ontoggle onunload onvolumechange onwaiting onwheel srcdoc]).freeze
9
-
10
- extend Phlex::Elements
7
+ extend Phlex::SGML::Elements
11
8
  include VoidElements, StandardElements
12
9
 
13
10
  # Output an HTML doctype.
14
11
  def doctype
15
- context = @_context
16
- return if context.fragments && !context.in_target_fragment
12
+ state = @_state
13
+ return unless state.should_render?
17
14
 
18
- context.buffer << "<!doctype html>"
15
+ state.buffer << "<!doctype html>"
19
16
  nil
20
17
  end
21
18
 
@@ -41,7 +38,4 @@ class Phlex::HTML < Phlex::SGML
41
38
  def content_type
42
39
  "text/html"
43
40
  end
44
-
45
- # This should be extended after all method definitions
46
- extend Phlex::ElementClobberingGuard
47
41
  end
data/lib/phlex/kit.rb CHANGED
@@ -3,7 +3,9 @@
3
3
  module Phlex::Kit
4
4
  module LazyLoader
5
5
  def method_missing(name, ...)
6
- if name[0] == name[0].upcase && __phlex_kit_constants__.include?(name) && __get_phlex_kit_constant__(name) && methods.include?(name)
6
+ mod = self.class
7
+
8
+ if name[0] == name[0].upcase && mod.constants.include?(name) && mod.const_get(name) && methods.include?(name)
7
9
  public_send(name, ...)
8
10
  else
9
11
  super
@@ -11,7 +13,9 @@ module Phlex::Kit
11
13
  end
12
14
 
13
15
  def respond_to_missing?(name, include_private = false)
14
- if name[0] == name[0].upcase && __phlex_kit_constants__.include?(name) && __get_phlex_kit_constant__(name) && methods.include?(name)
16
+ mod = self.class
17
+
18
+ if name[0] == name[0].upcase && mod.constants.include?(name) && mod.const_get(name) && methods.include?(name)
15
19
  true
16
20
  else
17
21
  super
@@ -19,20 +23,24 @@ module Phlex::Kit
19
23
  end
20
24
  end
21
25
 
22
- include LazyLoader
23
-
24
26
  def self.extended(mod)
25
27
  mod.include(LazyLoader)
26
- mod.define_method(:__phlex_kit_constants__) { mod.__phlex_kit_constants__ }
27
- mod.define_method(:__get_phlex_kit_constant__) { |name| mod.__get_phlex_kit_constant__(name) }
28
28
  end
29
29
 
30
- def __phlex_kit_constants__
31
- constants
30
+ def method_missing(name, ...)
31
+ if name[0] == name[0].upcase && constants.include?(name) && const_get(name) && methods.include?(name)
32
+ public_send(name, ...)
33
+ else
34
+ super
35
+ end
32
36
  end
33
37
 
34
- def __get_phlex_kit_constant__(name)
35
- const_get(name)
38
+ def respond_to_missing?(name, include_private = false)
39
+ if name[0] == name[0].upcase && constants.include?(name) && const_get(name) && methods.include?(name)
40
+ true
41
+ else
42
+ super
43
+ end
36
44
  end
37
45
 
38
46
  def const_added(name)
@@ -41,23 +49,32 @@ module Phlex::Kit
41
49
  me = self
42
50
  constant = const_get(name)
43
51
 
44
- if Class === constant && constant < Phlex::SGML
45
- constant.include(self)
52
+ case constant
53
+ when Class
54
+ if constant < Phlex::SGML
55
+ constant.include(self)
46
56
 
47
- define_method(name) do |*args, **kwargs, &block|
48
- constant = me.const_get(name)
49
- render(constant.new(*args, **kwargs), &block)
50
- end
57
+ constant = nil
58
+
59
+ define_method(name) do |*args, **kwargs, &block|
60
+ constant = me.const_get(name)
61
+ render(constant.new(*args, **kwargs), &block)
62
+ end
51
63
 
52
- define_singleton_method(name) do |*args, **kwargs, &block|
53
- if (component = Fiber[:__phlex_component__])
54
- component.instance_exec do
55
- render(constant.new(*args, **kwargs), &block)
64
+ define_singleton_method(name) do |*args, **kwargs, &block|
65
+ component, fiber_id = Thread.current[:__phlex_component__]
66
+ if (component && fiber_id == Fiber.current.object_id)
67
+ component.instance_exec do
68
+ constant = me.const_get(name)
69
+ render(constant.new(*args, **kwargs), &block)
70
+ end
71
+ else
72
+ raise "You can't call `#{name}' outside of a Phlex rendering context."
56
73
  end
57
- else
58
- raise "You can't call `#{name}' outside of a Phlex rendering context."
59
74
  end
60
75
  end
76
+ when Module
77
+ constant.extend(Phlex::Kit)
61
78
  end
62
79
 
63
80
  super
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex::NullCacheStore
4
+ extend self
5
+
6
+ def fetch(key)
7
+ yield
8
+ end
9
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex::SGML::Elements
4
+ def __registered_elements__
5
+ @__registered_elements__ ||= {}
6
+ end
7
+
8
+ def register_element(method_name, tag: method_name.name.tr("_", "-"))
9
+ class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
10
+ # frozen_string_literal: true
11
+
12
+ def #{method_name}(**attributes)
13
+ state = @_state
14
+ buffer = state.buffer
15
+ block_given = block_given?
16
+
17
+ unless state.should_render?
18
+ yield(self) if block_given
19
+ return nil
20
+ end
21
+
22
+ if attributes.length > 0 # with attributes
23
+ if block_given # with content block
24
+ buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << ">"
25
+
26
+ original_length = buffer.bytesize
27
+ content = yield(self)
28
+ if original_length == buffer.bytesize
29
+ case content
30
+ when nil
31
+ nil
32
+ when String
33
+ buffer << ::Phlex::Escape.html_escape(content)
34
+ when Symbol
35
+ buffer << ::Phlex::Escape.html_escape(content.name)
36
+ when ::Phlex::SGML::SafeObject
37
+ buffer << content.to_s
38
+ else
39
+ if (formatted_object = format_object(content))
40
+ buffer << ::Phlex::Escape.html_escape(formatted_object)
41
+ end
42
+ end
43
+ end
44
+
45
+ buffer << "</#{tag}>"
46
+ else # without content
47
+ buffer << "<#{tag}" << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << "></#{tag}>"
48
+ end
49
+ else # without attributes
50
+ if block_given # with content block
51
+ buffer << "<#{tag}>"
52
+
53
+ original_length = buffer.bytesize
54
+ content = yield(self)
55
+ if original_length == buffer.bytesize
56
+ case content
57
+ when nil
58
+ nil
59
+ when String
60
+ buffer << ::Phlex::Escape.html_escape(content)
61
+ when Symbol
62
+ buffer << ::Phlex::Escape.html_escape(content.name)
63
+ when ::Phlex::SGML::SafeObject
64
+ buffer << content.to_s
65
+ else
66
+ if (formatted_object = format_object(content))
67
+ buffer << ::Phlex::Escape.html_escape(formatted_object)
68
+ end
69
+ end
70
+ end
71
+
72
+ buffer << "</#{tag}>"
73
+ else # without content
74
+ buffer << "<#{tag}></#{tag}>"
75
+ end
76
+ end
77
+
78
+ #{'flush' if tag == 'head'}
79
+
80
+ nil
81
+ end
82
+ RUBY
83
+
84
+ __registered_elements__[method_name] = tag
85
+
86
+ method_name
87
+ end
88
+
89
+ def __register_void_element__(method_name, tag: method_name.name.tr("_", "-"))
90
+ class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
91
+ # frozen_string_literal: true
92
+
93
+ def #{method_name}(**attributes)
94
+ state = @_state
95
+
96
+ return unless state.should_render?
97
+
98
+ if attributes.length > 0 # with attributes
99
+ state.buffer << "<#{tag}" << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << ">"
100
+ else # without attributes
101
+ state.buffer << "<#{tag}>"
102
+ end
103
+
104
+ nil
105
+ end
106
+ RUBY
107
+
108
+ __registered_elements__[method_name] = tag
109
+
110
+ method_name
111
+ end
112
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Phlex::SGML::State
4
+ def initialize(user_context: {}, view_context: nil, output_buffer:, fragments:)
5
+ @buffer = +""
6
+ @capturing = false
7
+ @user_context = user_context
8
+ @fragments = fragments
9
+ @fragment_depth = 0
10
+ @cache_stack = []
11
+ @halt_signal = nil
12
+ @view_context = view_context
13
+ @output_buffer = output_buffer
14
+ end
15
+
16
+ attr_accessor :buffer, :capturing, :user_context
17
+
18
+ attr_reader :fragments, :fragment_depth, :view_context, :output_buffer
19
+
20
+ def around_render(component)
21
+ stack = @stack
22
+
23
+ if !@fragments || @halt_signal
24
+ yield
25
+ else
26
+ catch do |signal|
27
+ @halt_signal = signal
28
+ yield
29
+ end
30
+ end
31
+ end
32
+
33
+ def should_render?
34
+ !@fragments || @fragment_depth > 0
35
+ end
36
+
37
+ def begin_fragment(id)
38
+ @fragment_depth += 1 if @fragments&.include?(id)
39
+
40
+ if caching?
41
+ current_byte_offset = 0 # Start tracking the byte offset of this fragment from the start of the cache buffer
42
+ @cache_stack.reverse_each do |(cache_buffer, fragment_map)| # We'll iterate deepest to shallowest
43
+ current_byte_offset += cache_buffer.bytesize # Add the length of the cache buffer to the current byte offset
44
+ fragment_map[id] = [current_byte_offset, nil, []] # Record the byte offset, length, and store a list of the nested fragments
45
+
46
+ fragment_map.each do |name, (_offset, length, nested_fragments)| # Iterate over the other fragments
47
+ next if name == id || length # Skip if it's the current fragment, or if the fragment has already ended
48
+ nested_fragments << id # Add the current fragment to the list of nested fragments
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ def end_fragment(id)
55
+ if caching?
56
+ byte_length = nil
57
+ @cache_stack.reverse_each do |(cache_buffer, fragment_map)| # We'll iterate deepest to shallowest
58
+ byte_length ||= cache_buffer.bytesize - fragment_map[id][0] # The byte length is the difference between the current byte offset and the byte offset of the fragment
59
+ fragment_map[id][1] = byte_length # All cache contexts will use the same by
60
+ end
61
+ end
62
+
63
+ return unless @fragments&.include?(id)
64
+
65
+ @fragments.delete(id)
66
+ @fragment_depth -= 1
67
+ throw @halt_signal if @fragments.length == 0
68
+ end
69
+
70
+ def record_fragment(id, offset, length, nested_fragments)
71
+ return unless caching?
72
+
73
+ @cache_stack.reverse_each do |(cache_buffer, fragment_map)|
74
+ offset += cache_buffer.bytesize
75
+ fragment_map[id] = [offset, length, nested_fragments]
76
+ end
77
+ end
78
+
79
+ def caching(&)
80
+ buffer = +""
81
+ @cache_stack.push([buffer, {}].freeze)
82
+ capturing_into(buffer, &)
83
+ @cache_stack.pop
84
+ end
85
+
86
+ def caching?
87
+ @cache_stack.length > 0
88
+ end
89
+
90
+ def capturing_into(new_buffer)
91
+ original_buffer = @buffer
92
+ original_capturing = @capturing
93
+ original_fragments = @fragments
94
+
95
+ begin
96
+ @buffer = new_buffer
97
+ @capturing = true
98
+ @fragments = nil
99
+ yield
100
+ ensure
101
+ @buffer = original_buffer
102
+ @capturing = original_capturing
103
+ @fragments = original_fragments
104
+ end
105
+
106
+ new_buffer
107
+ end
108
+
109
+ def flush
110
+ return if capturing
111
+
112
+ buffer = @buffer
113
+ @output_buffer << buffer.dup
114
+
115
+ buffer.clear
116
+ nil
117
+ end
118
+ end