rblade 1.1.0 → 1.2.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +36 -9
  4. data/REFERENCE.md +2 -2
  5. data/lib/rblade/compiler/compiles_comments.rb +2 -0
  6. data/lib/rblade/compiler/compiles_components.rb +5 -3
  7. data/lib/rblade/compiler/compiles_prints.rb +29 -9
  8. data/lib/rblade/compiler/compiles_ruby.rb +14 -6
  9. data/lib/rblade/compiler/compiles_statements.rb +21 -1
  10. data/lib/rblade/compiler/compiles_verbatim.rb +2 -0
  11. data/lib/rblade/compiler/statements/compiles_component_helpers.rb +7 -7
  12. data/lib/rblade/compiler/statements/compiles_conditionals.rb +2 -0
  13. data/lib/rblade/compiler/statements/compiles_form.rb +2 -0
  14. data/lib/rblade/compiler/statements/compiles_html_attributes.rb +2 -0
  15. data/lib/rblade/compiler/statements/compiles_inline_ruby.rb +2 -0
  16. data/lib/rblade/compiler/statements/compiles_loops.rb +2 -0
  17. data/lib/rblade/compiler/statements/compiles_once.rb +4 -2
  18. data/lib/rblade/compiler/statements/compiles_stacks.rb +5 -3
  19. data/lib/rblade/compiler/tokenizes_components.rb +2 -0
  20. data/lib/rblade/compiler/tokenizes_statements.rb +25 -3
  21. data/lib/rblade/compiler.rb +17 -6
  22. data/lib/rblade/component_store.rb +5 -8
  23. data/lib/rblade/helpers/attributes_manager.rb +2 -0
  24. data/lib/rblade/helpers/class_manager.rb +3 -1
  25. data/lib/rblade/helpers/html_string.rb +2 -0
  26. data/lib/rblade/helpers/slot_manager.rb +2 -0
  27. data/lib/rblade/helpers/stack_manager.rb +4 -2
  28. data/lib/rblade/helpers/style_manager.rb +3 -1
  29. data/lib/rblade/helpers/tokenizer.rb +3 -1
  30. data/lib/rblade/rails_template.rb +3 -1
  31. data/lib/rblade/railtie.rb +2 -0
  32. data/lib/rblade.rb +2 -0
  33. data/rblade.gemspec +1 -1
  34. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: edaa1af6299a8281520a73611def0838ef1017f2ec7bb3fd8a125bcd02e5c7ce
4
- data.tar.gz: 6363e44a009a29e8771c50ecf4768b0d365b0d2147ba998db72805e9b05c0390
3
+ metadata.gz: d1a00aab0f9732f42b21921bda5f23b25ce2581f2b0f898c26c5665f0a51046d
4
+ data.tar.gz: 46db536cfc37cb9307c446d8e23ae7bde18ad7d9947d09f83d1db766d42a505d
5
5
  SHA512:
6
- metadata.gz: 996e77805b56530e9f136c087753938c10c8dc71d45b1fbaec10e81e890878b9a0da00a10fa024a650cb2e463e155fe8da723fb16a6eb690f5811915ce4fe879
7
- data.tar.gz: 5bbe562c01ec5fb12e77cb53aa2991cebfd83792dd831a41957497e3b04e29df442511c890d48fcb05bef78107cf5347019c49c7c8dc9e5d3f9ef0557a63f940
6
+ metadata.gz: 79920ad59e36a19996a5bceb55370560b6af5ba6075f4db9494c6844f7b729fc63339f7cb106c76a7fb2be4d410554ac5c69793b09e6a4729a1a30213b1c78cb
7
+ data.tar.gz: 0ba0f79cc1b8cae6997c02821ca724f3211f50c8978f4923bfe0692a626e63a5fc09fb4bcb3de3b206dbda47e6c825a95a6c2faa91784e83cd420e5d31ba0a35
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## 1.2.0 [2024-10-22]
2
+ - Add support for custom directives (#2)
3
+ - Add support for helper functions that use block content (#14)
4
+ - Change to use frozen string literals
5
+ - Fix issue with no space after starting ERB style tag
6
+ - Ignore any @ directives that don't have a handler
7
+ - Make @endruby directive case insensitive and allow underscore
8
+
9
+ ## 1.1.1 [2024-08-23]
10
+ - Remove requirement for including braces in `@props` directive
11
+
1
12
  ## 1.1.0 [2024-08-23]
2
13
  - Add `@eachWithIndex` directive
3
14
  - Fix attribute manager output of attributes with no value (#10)
data/README.md CHANGED
@@ -26,6 +26,7 @@ For a quick overview of RBlade's capabilities, refer to the [reference file](REF
26
26
  * [Table of Contents](#table-of-contents)
27
27
  * [Displaying Data](#displaying-data)
28
28
  + [HTML Entity Encoding](#html-entity-encoding)
29
+ + [ERB Compatbility and Rails Helper Methods](#erb-compatibility-and-rails-helper-methods)
29
30
  + [RBlade and JavaScript Frameworks](#rblade-and-javascript-frameworks)
30
31
  - [The `@verbatim` Directive](#the-at-verbatim-directive)
31
32
  * [RBlade Directives](#rblade-directives)
@@ -37,6 +38,7 @@ For a quick overview of RBlade's capabilities, refer to the [reference file](REF
37
38
  + [Additional Attributes](#additional-attributes)
38
39
  + [The `@once` Directive](#the-once-directive)
39
40
  + [Raw Ruby](#raw-ruby)
41
+ + [Custom Directives](#custom-directives)
40
42
  + [Comments](#comments)
41
43
  * [Components](#components)
42
44
  + [Rendering Components](#rendering-components)
@@ -66,14 +68,14 @@ You can display data that is passed to your RBlade views by wrapping the variabl
66
68
 
67
69
  ```ruby
68
70
  def index
69
- name = "Samantha"
71
+ @name = "Samantha"
70
72
  end
71
73
  ```
72
74
 
73
75
  You can display the contents of the `name` variable like so:
74
76
 
75
77
  ```rblade
76
- Hello, {{ name }}.
78
+ Hello, {{ @name }}.
77
79
  ```
78
80
 
79
81
  > [!NOTE]
@@ -91,12 +93,20 @@ The current UNIX timestamp is {{ Time.now.to_i }}.
91
93
  By default, RBlade `{{ }}` directives are automatically sent through Rails' `h` function to prevent XSS attacks. If you do not want your data to be escaped, you can use the following syntax:
92
94
 
93
95
  ```rblade
94
- Hello, {!! name !!}.
96
+ Hello, {!! @name !!}.
95
97
  ```
96
98
 
97
99
  > [!WARNING]
98
100
  > Be very careful when printing content that is supplied by users of your application. You should typically use the escaped, double curly brace syntax to prevent XSS attacks when displaying user supplied data.
99
101
 
102
+
103
+ <a name="erb-compatibility-and-rails-helper-methods"></a>
104
+ ### ERB Compatbility and Rails Helper Methods
105
+
106
+ For the most part, RBlade templates are backwards compatible with the built in ERB templates. Anything that works in an ERB template should also work in an RBlade template.
107
+
108
+ This includes helper methods, path helpers and methods from third party gems such as `simple_forms` or `vite-ruby`. It additionally includes the ERB syntax for outputting data, `<%= ... %>`, and running ruby code, `<%= ... %>`.
109
+
100
110
  <a name="rblade-and-javascript-frameworks"></a>
101
111
  ### RBlade and JavaScript Frameworks
102
112
 
@@ -419,6 +429,23 @@ In some situations, it's useful to embed Ruby code into your views. You can use
419
429
  @endRuby
420
430
  ```
421
431
 
432
+ <a name="custom-directives"></a>
433
+ ### Custom Directives
434
+
435
+ RBlade also allows you to define your own directives using the `RBlade.register_directive_handler`
436
+ method. When the compiler encounters the custom directive, it will call the provided block and
437
+ output the returned value.
438
+
439
+ ```rblade
440
+ RBlade::register_directive_handler('sum') do |args|
441
+ args.inject(0) { |sum, num| sum + num.to_i }
442
+ end
443
+
444
+ @sum(1) -> 1
445
+ @sum(1, 2) -> 3
446
+ @sum(1, 2, 3) -> 6
447
+ ```
448
+
422
449
  <a name="comments"></a>
423
450
  ### Comments
424
451
 
@@ -491,7 +518,7 @@ You can define a component's data properties using a `@props` directive at the t
491
518
 
492
519
  ```rblade
493
520
  {{-- alert.rblade --}}
494
- @props({type: "warning", message: required})
521
+ @props(type: "warning", message: required)
495
522
  <div class="{{ type }}">{{ message }}</div>
496
523
  ```
497
524
 
@@ -505,7 +532,7 @@ The `@props` directive accepts a Hash where the key is the name of the attribute
505
532
  All properties in the `@props` directive are automatically removed from `attributes`. Properties with names that aren't valid Ruby variable names or are Ruby reserved keywords are not created as local variables. However, you can reference them via the `attributes` local variable:
506
533
 
507
534
  ```rblade
508
- @props({"for": required, "data-value": nil})
535
+ @props("for": required, "data-value": nil)
509
536
  <div>{{ attributes[:for] }} {{ attributes[:'data-value'] }}</div>
510
537
  ```
511
538
 
@@ -697,7 +724,7 @@ Sometimes a component may need to render multiple different slots in different l
697
724
 
698
725
  ```rblade
699
726
  {{-- /app/views/components/alert.rblade --}}
700
- @props({title: required})
727
+ @props(title: required)
701
728
  <span class="alert-title">{{ title }}</span>
702
729
  <div class="alert alert-danger">
703
730
  {{ slot }}
@@ -752,10 +779,10 @@ Like RBlade components, you can assign additional [attributes](#component-attrib
752
779
  To interact with slot attributes, you can access the `attributes` property of the slot's variable. For more information on how to interact with attributes, please consult the documentation on [component attributes](#component-attributes):
753
780
 
754
781
  ```rblade
755
- @props({
782
+ @props(
756
783
  "heading": required,
757
784
  "footer": required,
758
- })
785
+ )
759
786
 
760
787
  <div {{ attributes.class('border') }}>
761
788
  <h1 {{ heading.attributes.class('text-lg') }}>
@@ -777,7 +804,7 @@ Sometimes, you may wish to return early from a component without printing anythi
777
804
 
778
805
  ```rblade
779
806
  {{-- components/error.rblade --}}
780
- @props({errors: []})
807
+ @props(errors: [])
781
808
  @shouldRender(errors.present?)
782
809
  ...
783
810
  ```
data/REFERENCE.md CHANGED
@@ -30,8 +30,8 @@ By default, RBlade will look for components in the `app/views/components` folder
30
30
  | `<x-name @style({'bg-red-600': is_error})/>` | Conditionally pass styles to a component |
31
31
  | `<x-name attribute/>` | Pass an attribute to a component with value `true` |
32
32
  | `<x-name {{ attributes }}/>` | Pass attributes to a child component |
33
- | `@props({header: "Header"})` | Remove `header` from the attributes Hash and introduce it as a local variable, using the specified value as a default |
34
- | `@props({header: required})` | Remove `header` from the attributes Hash and introduce it as a local variable, raising an error if it is not set |
33
+ | `@props(header: "Header")` | Remove `header` from the attributes Hash and introduce it as a local variable, using the specified value as a default |
34
+ | `@props(header: required)` | Remove `header` from the attributes Hash and introduce it as a local variable, raising an error if it is not set |
35
35
  | `{{ slot }}` | Output the block content passed into the current component |
36
36
  | `<x-name><x-slot::header><h1>Header</h1><//>Content<//>` | Pass a named block to a component |
37
37
  | `{{ header }}` | Output the contents of a named block |
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RBlade
2
4
  class CompilesComments
3
5
  def compile!(tokens)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rblade/component_store"
2
4
 
3
5
  module RBlade
@@ -34,7 +36,7 @@ module RBlade
34
36
 
35
37
  attributes = compile_attributes token.value[:attributes]
36
38
 
37
- code = "_c#{component[:index]}_swap=_out;_out='';"
39
+ code = +"_c#{component[:index]}_swap=_out;_out=+'';"
38
40
  code << "_c#{component[:index]}_attr={#{attributes.join(",")}};"
39
41
 
40
42
  code
@@ -65,14 +67,14 @@ module RBlade
65
67
  def compile_slot_end name, component
66
68
  parent = @component_stack.last
67
69
 
68
- code = "_c#{parent[:index]}_attr[:'#{RBlade.escape_quotes(name)}']=RBlade::SlotManager.new(_out,_c#{component[:index]}_attr);"
70
+ code = +"_c#{parent[:index]}_attr[:'#{RBlade.escape_quotes(name)}']=RBlade::SlotManager.new(_out,_c#{component[:index]}_attr);"
69
71
  code << "_out=_c#{component[:index]}_swap;_c#{component[:index]}_swap=nil;_c#{component[:index]}_attr=nil;"
70
72
 
71
73
  code
72
74
  end
73
75
 
74
76
  def compile_component_end component
75
- code = "_slot=RBlade::SlotManager.new(_out);_out=_c#{component[:index]}_swap;"
77
+ code = +"_slot=RBlade::SlotManager.new(_out);_out=_c#{component[:index]}_swap;"
76
78
  code << "_out<<#{ComponentStore.component(component[:name])}(_slot,_c#{component[:index]}_attr,params,session,flash,cookies);"
77
79
  code << "_slot=nil;_c#{component[:index]}_swap=nil;_c#{component[:index]}_attr=nil;"
78
80
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RBlade
2
4
  class CompilesPrints
3
5
  def compile!(tokens)
@@ -8,8 +10,8 @@ module RBlade
8
10
  private
9
11
 
10
12
  def compile_regular_prints!(tokens)
11
- compile_prints! tokens, "{{", "}}", "RBlade.e"
12
- compile_prints! tokens, "<%=", "%>", "RBlade.e"
13
+ compile_prints! tokens, "{{", "}}", +"RBlade.e"
14
+ compile_prints! tokens, "<%=", "%>", +"RBlade.e"
13
15
  end
14
16
 
15
17
  def compile_unsafe_prints!(tokens)
@@ -35,14 +37,8 @@ module RBlade
35
37
  elsif segments[i] == start_token
36
38
  segments.delete_at i
37
39
  segments.delete_at i + 1
38
- segment_value = "_out<<"
39
40
 
40
- segment_value <<= if !wrapper_function.nil?
41
- wrapper_function + "(" + segments[i] + ");"
42
- else
43
- "(" + segments[i] + ").to_s;"
44
- end
45
- segments[i] = Token.new(:print, segment_value)
41
+ segments[i] = create_token(segments[i], wrapper_function)
46
42
 
47
43
  i += 1
48
44
  elsif !segments[i].nil? && segments[i] != ""
@@ -57,5 +53,29 @@ module RBlade
57
53
  segments
58
54
  end.flatten!
59
55
  end
56
+
57
+ def create_token expression, wrapper_function
58
+ if expression.match?(/
59
+ do\s*
60
+ (
61
+ \|\s*
62
+ [a-zA-Z0-9_]+\s*
63
+ (,\s*[a-zA-Z0-9_]+)?\s*
64
+ \|\s*
65
+ )?
66
+ $/x)
67
+ return Token.new(:print, "_out+=#{expression};_out=+'';")
68
+ elsif expression.match?(/^\s*end(?![a-zA-Z0-9_])/i)
69
+ return Token.new(:print, "_out;#{expression};")
70
+ end
71
+
72
+ segment_value = if !wrapper_function.nil?
73
+ "_out<<#{wrapper_function}(#{expression});"
74
+ else
75
+ "_out<<(#{expression}).to_s;"
76
+ end
77
+
78
+ Token.new(:print, segment_value)
79
+ end
60
80
  end
61
81
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RBlade
2
4
  class CompilesRuby
3
5
  def compile!(tokens)
@@ -6,15 +8,15 @@ module RBlade
6
8
 
7
9
  segments = token.value.split(/
8
10
  # @ escapes blade style tags
9
- (@)(@ruby.+?@endruby)
11
+ (@)(@ruby.+?@end_?ruby)
10
12
  |
11
13
  # <%% and %%> are escape ERB style tags
12
14
  (<%%)(.+?)(%%>)
13
15
  |
14
- \s?(?<!\w)(@ruby)\s+(.+?)[\s;]*(@endruby)(?!\w)\s?
16
+ \s?(?<!\w)(@ruby)\s+(.+?)[\s;]*(@end_?ruby)(?!\w)\s?
15
17
  |
16
- (<%)\s+(.+?)[\s;]*(%>)
17
- /xm)
18
+ (<%)(?!=)\s*(.+?)[\s;]*(%>)
19
+ /xmi)
18
20
 
19
21
  i = 0
20
22
  while i < segments.count
@@ -29,7 +31,7 @@ module RBlade
29
31
  segments[i] = Token.new(type: :raw_text, value: "<%#{segments[i]}%>")
30
32
 
31
33
  i += 1
32
- elsif segments[i] == "@ruby" || segments[i] == "<%"
34
+ elsif segments[i].downcase == "@ruby" || segments[i] == "<%"
33
35
  segments.delete_at i
34
36
  segments.delete_at i + 1
35
37
 
@@ -38,7 +40,13 @@ module RBlade
38
40
  segments[i] << ";"
39
41
  end
40
42
 
41
- segments[i] = Token.new(type: :ruby, value: segments[i])
43
+ # Ensure _out is returned at the end of any blocks
44
+ # See also ./compiles_prints.rb
45
+ segments[i] = if segments[i].match?(/^end(?![a-zA-Z0-9_])/i)
46
+ Token.new(type: :ruby, value: "_out;#{segments[i]}")
47
+ else
48
+ Token.new(type: :ruby, value: segments[i])
49
+ end
42
50
 
43
51
  i += 1
44
52
  elsif !segments[i].nil? && segments[i] != ""
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rblade/compiler/statements/compiles_component_helpers"
2
4
  require "rblade/compiler/statements/compiles_conditionals"
3
5
  require "rblade/compiler/statements/compiles_form"
@@ -13,6 +15,7 @@ module RBlade
13
15
  token_index = 0
14
16
  while token_index < tokens.count
15
17
  token = tokens[token_index]
18
+
16
19
  if token.type != :statement
17
20
  token_index += 1
18
21
  next
@@ -34,7 +37,11 @@ module RBlade
34
37
  end
35
38
  end
36
39
 
37
- token.value = handler.call(*handler_arguments)
40
+ token.value = if handler.is_a? Proc
41
+ "_out<<'#{RBlade.escape_quotes(handler.call(*handler_arguments).to_s)}';"
42
+ else
43
+ handler.call(*handler_arguments)
44
+ end
38
45
  token_index += 1
39
46
  end
40
47
  end
@@ -43,11 +50,24 @@ module RBlade
43
50
  "end;"
44
51
  end
45
52
 
53
+ def self.has_handler(name)
54
+ name = name.downcase
55
+ @@statement_handlers[name.tr("_", "")].present? || name.start_with?("end")
56
+ end
57
+
58
+ def self.register_handler(name, &block)
59
+ @@statement_handlers[name.tr("_", "").downcase] = ["proc", block]
60
+ end
61
+
46
62
  private
47
63
 
48
64
  def getHandler(name)
49
65
  handler_class, handler_method = @@statement_handlers[name.tr("_", "").downcase]
50
66
 
67
+ if handler_class == "proc"
68
+ return handler_method
69
+ end
70
+
51
71
  if !handler_class&.method_defined?(handler_method)
52
72
  if name.start_with? "end"
53
73
  ## Fallback to the default end handler
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RBlade
2
4
  class CompilesVerbatim
3
5
  def compile!(tokens)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rblade/helpers/tokenizer"
2
4
 
3
5
  module RBlade
@@ -18,17 +20,15 @@ module RBlade
18
20
 
19
21
  props = extractProps args[0]
20
22
  props.map do |key, value|
21
- compiled_code = ""
22
-
23
23
  # `_required` is deprecated. Use `required`. To be removed in 2.0.0
24
- compiled_code << if value == "_required" || value == "required"
24
+ compiled_code = if value == "_required" || value == "required"
25
25
  "if !attributes.has?(:'#{RBlade.escape_quotes(key)}');raise \"Props statement: #{key} is not defined\";end;"
26
26
  else
27
27
  "attributes.default(:'#{RBlade.escape_quotes(key)}', #{value});"
28
28
  end
29
29
 
30
30
  if isValidVariableName key
31
- compiled_code << if variableIsSlot key, tokens
31
+ compiled_code += if variableIsSlot key, tokens
32
32
  "#{key}=RBlade::SlotManager.wrap(attributes.delete :'#{RBlade.escape_quotes(key)}');"
33
33
  else
34
34
  "#{key}=attributes.delete :'#{RBlade.escape_quotes(key)}';"
@@ -42,12 +42,12 @@ module RBlade
42
42
  private
43
43
 
44
44
  def extractProps prop_string
45
- if !prop_string.start_with?("{") || !prop_string.end_with?("}")
46
- raise StandardError.new "Props statement: expecting hash as parameter"
45
+ if prop_string.start_with?("{") && prop_string.end_with?("}")
46
+ prop_string = prop_string.delete_prefix("{").delete_suffix("}")
47
47
  end
48
48
 
49
49
  props = {}
50
- prop_strings = Tokenizer.extractCommaSeparatedValues prop_string[1..-2]
50
+ prop_strings = Tokenizer.extractCommaSeparatedValues prop_string
51
51
 
52
52
  prop_strings.each do |prop|
53
53
  prop.strip!
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RBlade
2
4
  class CompilesStatements
3
5
  class CompilesConditionals
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RBlade
2
4
  class CompilesStatements
3
5
  class CompilesForm
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RBlade
2
4
  class CompilesStatements
3
5
  class CompilesHtmlAttributes
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RBlade
2
4
  class CompilesStatements
3
5
  class CompilesInlineRuby
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RBlade
2
4
  class CompilesStatements
3
5
  class CompilesLoops
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RBlade
2
4
  class CompilesStatements
3
5
  class CompilesOnce
@@ -23,7 +25,7 @@ module RBlade
23
25
  once_id = args[1].nil? ? ":_#{@once_counter}" : args[1]
24
26
 
25
27
  "unless $_once_tokens.include? #{once_id};$_once_tokens<<#{once_id};" \
26
- << "_p1_#{@once_counter}=#{args[0]};_p1_#{@once_counter}_b=_out;_out='';"
28
+ << "_p1_#{@once_counter}=#{args[0]};_p1_#{@once_counter}_b=_out;_out=+'';"
27
29
  end
28
30
 
29
31
  def compileEndPushOnce args
@@ -42,7 +44,7 @@ module RBlade
42
44
  once_id = args[1].nil? ? ":_#{@once_counter}" : args[1]
43
45
 
44
46
  "unless $_once_tokens.include? #{once_id};$_once_tokens<<#{once_id};" \
45
- << "_p1_#{@once_counter}=#{args[0]};_p1_#{@once_counter}_b=_out;_out='';"
47
+ << "_p1_#{@once_counter}=#{args[0]};_p1_#{@once_counter}_b=_out;_out=+'';"
46
48
  end
47
49
 
48
50
  def compileEndPrependOnce args
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RBlade
2
4
  class CompilesStatements
3
5
  class CompilesStacks
@@ -10,7 +12,7 @@ module RBlade
10
12
  raise StandardError.new "Stack statement: wrong number of arguments (given #{args&.count || 0}, expecting 1)"
11
13
  end
12
14
 
13
- "RBlade::StackManager.initialize(#{args[0]}, _out);_stacks.push(#{args[0]});_out = '';"
15
+ "RBlade::StackManager.initialize(#{args[0]}, _out);_stacks.push(#{args[0]});_out=+'';"
14
16
  end
15
17
 
16
18
  def compilePrepend args
@@ -20,7 +22,7 @@ module RBlade
20
22
 
21
23
  @push_counter += 1
22
24
 
23
- "_p_#{@push_counter}=#{args[0]};_p_#{@push_counter}_b=_out;_out='';"
25
+ "_p_#{@push_counter}=#{args[0]};_p_#{@push_counter}_b=_out;_out=+'';"
24
26
  end
25
27
 
26
28
  def compileEndPrepend args
@@ -56,7 +58,7 @@ module RBlade
56
58
 
57
59
  @push_counter += 1
58
60
 
59
- "_p_#{@push_counter}=#{args[0]};_p_#{@push_counter}_b=_out;_out='';"
61
+ "_p_#{@push_counter}=#{args[0]};_p_#{@push_counter}_b=_out;_out=+'';"
60
62
  end
61
63
 
62
64
  def compilePushIf args
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "ripper"
2
4
 
3
5
  module RBlade
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rblade/helpers/tokenizer"
2
4
  require "ripper"
3
5
 
@@ -18,7 +20,7 @@ module RBlade
18
20
  (?:
19
21
  (@)
20
22
  (\w+(?!\w)[!\?]?)
21
- (?:[ \t]*
23
+ (?:([ \t]*)
22
24
  (\(.*?\))
23
25
  )?
24
26
  )
@@ -44,8 +46,23 @@ module RBlade
44
46
 
45
47
  i += 1
46
48
  elsif segment == "@"
47
- tokenizeStatement! segments, i
48
- handleSpecialCases! segments, i
49
+ if CompilesStatements.has_handler(segments[i + 1])
50
+ tokenizeStatement! segments, i
51
+ handleSpecialCases! segments, i
52
+ else
53
+ # For unhandled statements, restore the original string
54
+ segments[i] = Token.new(type: :unprocessed, value: segments[i] + segments[i + 1])
55
+ segments.delete_at i + 1
56
+
57
+ if segments.count > i + 2 && segments[i + 1].match(/^[ \t]*$/) && segments[i + 2][0] == "("
58
+ segments[i].value += segments[i + 1] + segments[i + 2]
59
+ segments.delete_at i + 1
60
+ segments.delete_at i + 1
61
+ elsif segments.count > i + 1 && segments[i + 1][0] == "("
62
+ segments[i].value += segments[i + 1]
63
+ segments.delete_at i + 1
64
+ end
65
+ end
49
66
 
50
67
  i += 1
51
68
  elsif !segments[i].nil? && segments[i] != ""
@@ -64,6 +81,11 @@ module RBlade
64
81
  statement_data = {name: segments[i + 1]}
65
82
  segments.delete_at i + 1
66
83
 
84
+ # Remove optional whitespace
85
+ if segments.count > i + 2 && segments[i + 1].match(/^[ \t]*$/) && segments[i + 2][0] == "("
86
+ segments.delete_at i + 1
87
+ end
88
+
67
89
  if segments.count > i + 1 && segments[i + 1][0] == "("
68
90
  arguments = tokenizeArguments! segments, i + 1
69
91
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rblade/compiler/compiles_comments"
2
4
  require "rblade/compiler/compiles_components"
3
5
  require "rblade/compiler/compiles_prints"
@@ -21,7 +23,7 @@ module RBlade
21
23
  string.gsub(/['\\\x0]/, '\\\\\0')
22
24
  end
23
25
 
24
- def self.e string
26
+ def self.e(string)
25
27
  if string.is_a?(HtmlString) || string.is_a?(ActiveSupport::SafeBuffer)
26
28
  string
27
29
  else
@@ -29,6 +31,15 @@ module RBlade
29
31
  end
30
32
  end
31
33
 
34
+ # Register a new custom directive by providing a class and method that will compile the directive into ruby code.
35
+ #
36
+ # @param [String] name The directive tag without the "@", e.g. "if" for the "@if" directive
37
+ # @param [Proc] block The block that will return the compiled ruby code for the directive. Any arguments will be passed to this Proc as an array.
38
+ # @return [void]
39
+ def self.register_directive_handler(name, &)
40
+ CompilesStatements.register_handler(name, &)
41
+ end
42
+
32
43
  class Compiler
33
44
  def self.compileString(string_template)
34
45
  tokens = [Token.new(:unprocessed, string_template)]
@@ -56,13 +67,13 @@ module RBlade
56
67
  end
57
68
 
58
69
  def self.compileTokens tokens
59
- output = ""
70
+ output = +""
60
71
 
61
- tokens.each do |token, cake|
62
- if token.type == :unprocessed || token.type == :raw_text
63
- output << "_out<<'" << RBlade.escape_quotes(token.value) << "';"
72
+ tokens.each do |token|
73
+ output << if token.type == :unprocessed || token.type == :raw_text
74
+ "_out<<'#{RBlade.escape_quotes(token.value)}';"
64
75
  else
65
- output << token.value
76
+ token.value
66
77
  end
67
78
  end
68
79
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RBlade
2
4
  FILE_EXTENSIONS = [".rblade", ".html.rblade"]
3
5
 
@@ -48,7 +50,7 @@ module RBlade
48
50
  end
49
51
 
50
52
  def self.clear
51
- @@component_definitions = ""
53
+ @@component_definitions = +""
52
54
  @@component_method_names = {}
53
55
  @@component_name_stack = []
54
56
  end
@@ -78,12 +80,7 @@ module RBlade
78
80
 
79
81
  compiled_component = RBlade::Compiler.compileString(code)
80
82
 
81
- @@component_definitions \
82
- << "def #{@@component_method_names[name]}(slot,attributes,params,session,flash,cookies);_out='';" \
83
- << "_stacks=[];" \
84
- << "attributes=RBlade::AttributesManager.new(attributes);" \
85
- << compiled_component \
86
- << "RBlade::StackManager.get(_stacks) + _out;end;"
83
+ @@component_definitions << "def #{@@component_method_names[name]}(slot,attributes,params,session,flash,cookies);_out=+'';_stacks=[];attributes=RBlade::AttributesManager.new(attributes);#{compiled_component}RBlade::StackManager.get(_stacks) + _out;end;"
87
84
 
88
85
  @@component_method_names[name]
89
86
  end
@@ -91,7 +88,7 @@ module RBlade
91
88
 
92
89
  private
93
90
 
94
- @@component_definitions = ""
91
+ @@component_definitions = +""
95
92
  @@component_name_stack = []
96
93
  @@component_method_names = {}
97
94
  @@template_paths = {}
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rblade/helpers/html_string"
2
4
 
3
5
  module RBlade
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RBlade
2
4
  class ClassManager
3
5
  def initialize classes
@@ -6,7 +8,7 @@ module RBlade
6
8
  elsif classes.is_a? Array
7
9
  @classes = classes.join " "
8
10
  elsif classes.is_a? Hash
9
- @classes = ""
11
+ @classes = +""
10
12
  classes.map do |value, predicate|
11
13
  if predicate
12
14
  @classes << "#{value} "
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RBlade
2
4
  class HtmlString
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rblade/helpers/attributes_manager"
2
4
  require "rblade/helpers/html_string"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RBlade
2
4
  class StackManager
3
5
  def self.initialize stack_name, before_stack
@@ -34,8 +36,8 @@ module RBlade
34
36
 
35
37
  class Stack
36
38
  def initialize
37
- @prepends = ""
38
- @stack = ""
39
+ @prepends = +""
40
+ @stack = +""
39
41
  end
40
42
 
41
43
  def set_before_stack before_stack
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RBlade
2
4
  class StyleManager
3
5
  def initialize styles
@@ -16,7 +18,7 @@ module RBlade
16
18
  style
17
19
  end.join
18
20
  elsif styles.is_a? Hash
19
- @styles = ""
21
+ @styles = +""
20
22
  styles.each do |value, predicate|
21
23
  if predicate
22
24
  value = value.to_s.strip
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RBlade
2
4
  class Tokenizer
3
5
  def self.extractCommaSeparatedValues segment
@@ -36,7 +38,7 @@ module RBlade
36
38
  next
37
39
  end
38
40
 
39
- argument = ""
41
+ argument = +""
40
42
 
41
43
  # Concatenate all lines up to this token's line, including the tail end of the current line
42
44
  if token[0][0] != current_line
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rails"
2
4
  require "rblade/compiler"
3
5
  require "rblade/component_store"
@@ -25,7 +27,7 @@ module RBlade
25
27
  "view::#{view_name}"
26
28
  )
27
29
  end
28
- setup = "_out='';_stacks=[];$_once_tokens=[];"
30
+ setup = "_out=+'';_stacks=[];$_once_tokens=[];"
29
31
  code = RBlade::Compiler.compileString(source || template.source)
30
32
  setdown = "RBlade::StackManager.get(_stacks) + _out"
31
33
  setup + ComponentStore.get + code + setdown
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rails"
2
4
  require "rblade/rails_template"
3
5
  require "rblade/component_store"
data/lib/rblade.rb CHANGED
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rails"
2
4
  require "rblade/railtie"
data/rblade.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "rblade"
3
- s.version = "1.1.0"
3
+ s.version = "1.2.0"
4
4
  s.summary = "A component-first templating engine for Rails"
5
5
  s.description = "RBlade is a simple, yet powerful templating engine for Ruby on Rails, inspired by Laravel Blade."
6
6
  s.authors = ["Simon J"]
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rblade
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon J
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-23 00:00:00.000000000 Z
11
+ date: 2024-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -140,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
140
140
  - !ruby/object:Gem::Version
141
141
  version: '0'
142
142
  requirements: []
143
- rubygems_version: 3.3.5
143
+ rubygems_version: 3.5.11
144
144
  signing_key:
145
145
  specification_version: 4
146
146
  summary: A component-first templating engine for Rails