rblade 2.0.2 → 3.0.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +24 -0
  3. data/CHANGELOG.md +11 -0
  4. data/README.md +83 -16
  5. data/REFERENCE.md +4 -2
  6. data/do +4 -4
  7. data/docker-compose.yml +4 -1
  8. data/lib/rblade/compiler/compiles_comments.rb +2 -2
  9. data/lib/rblade/compiler/compiles_components.rb +26 -5
  10. data/lib/rblade/compiler/compiles_injections.rb +81 -0
  11. data/lib/rblade/compiler/compiles_statements.rb +69 -64
  12. data/lib/rblade/compiler/compiles_verbatim.rb +1 -1
  13. data/lib/rblade/compiler/statements/compiles_component_helpers.rb +17 -13
  14. data/lib/rblade/compiler/statements/compiles_conditionals.rb +18 -18
  15. data/lib/rblade/compiler/statements/compiles_form.rb +8 -8
  16. data/lib/rblade/compiler/statements/compiles_html_attributes.rb +2 -2
  17. data/lib/rblade/compiler/statements/compiles_inline_ruby.rb +1 -1
  18. data/lib/rblade/compiler/statements/compiles_loops.rb +11 -11
  19. data/lib/rblade/compiler/statements/compiles_once.rb +3 -3
  20. data/lib/rblade/compiler/statements/compiles_stacks.rb +5 -5
  21. data/lib/rblade/compiler/tokenizes_components.rb +30 -31
  22. data/lib/rblade/compiler/tokenizes_statements.rb +29 -30
  23. data/lib/rblade/compiler.rb +20 -19
  24. data/lib/rblade/component_store.rb +20 -20
  25. data/lib/rblade/helpers/attributes_manager.rb +10 -9
  26. data/lib/rblade/helpers/class_manager.rb +1 -1
  27. data/lib/rblade/helpers/slot_manager.rb +2 -2
  28. data/lib/rblade/helpers/stack_manager.rb +3 -3
  29. data/lib/rblade/helpers/style_manager.rb +1 -1
  30. data/lib/rblade/helpers/tokenizer.rb +5 -5
  31. data/lib/rblade/rails_template.rb +9 -2
  32. data/lib/rblade/railtie.rb +34 -2
  33. data/rblade.gemspec +4 -1
  34. metadata +50 -8
  35. data/lib/rblade/compiler/compiles_prints.rb +0 -83
  36. data/lib/rblade/compiler/compiles_ruby.rb +0 -59
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb23406b9206cc5eb7d20f10d700375c1b49c588c2805ae77710aff217ab39f1
4
- data.tar.gz: '0330179c0e5113c74d0678c54fa08a489820f0d72ea21eb179caa372c7e54a42'
3
+ metadata.gz: 5760d0a2af4a86bf0ffcc9451abb3a46357029b9f5fea33afdf8305282c3d5e3
4
+ data.tar.gz: aaf764e044528fac1287219268ed117d332fd13204eb52971de9a88402665780
5
5
  SHA512:
6
- metadata.gz: c592ebfb01c89d56e996c1e0c7815e50ab2363a2d9badb41a8573067b230796807392789418b81593eecbacfa1160b34843ba6bce102f600acd5ea08ca9a2521
7
- data.tar.gz: dcb59ca067167dbbe63a4ea86b2fce0d04e25beb4fe728826abc1e19f33613a6d3a472a36896f55bbc0c97979d1c265fa4be8b20e8c0ec62569218099bb62d1a
6
+ metadata.gz: d38f0abf519963a665c5780a2e2328f4bc357455c2cbec99d49362eb76f4529ece7edd65b08ec4c9669700a2c0fca5feeade897fff938aa5c8ed64790f96c152
7
+ data.tar.gz: 4ef6e7d9bffb6b9a4360af6ffe7030d9cac15ce6b3c67da546af9739659cb68ea801f06e4ddeaa67f66ce48e4885d4b2931a090518d39b05b994592bd7db0447
data/.rubocop.yml ADDED
@@ -0,0 +1,24 @@
1
+ AllCops:
2
+ DisabledByDefault: true
3
+ SuggestExtensions: false
4
+ TargetRubyVersion: 3.4
5
+ UseCache: true
6
+
7
+ inherit_gem:
8
+ standard: config/base.yml
9
+
10
+ # Enforce commas at the end of multiline arguments, hashes and arrays, improving git diffs
11
+ Style/TrailingCommaInArrayLiteral:
12
+ EnforcedStyleForMultiline: diff_comma
13
+
14
+ Style/TrailingCommaInHashLiteral:
15
+ EnforcedStyleForMultiline: diff_comma
16
+
17
+ Style/TrailingCommaInArguments:
18
+ EnforcedStyleForMultiline: comma
19
+
20
+ # Enforce standardisation of method definitions
21
+ Style/MethodDefParentheses:
22
+ Enabled: true
23
+ Naming/MethodName:
24
+ Enabled: true
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## 3.0.0 [2025-03-18]
2
+ - Add ability to add raw directive handlers that add ruby code to the template
3
+ - Add `RBlade.direct_component_rendering` option to allow RBlade components to be rendered directly
4
+ - Add `component` view helper for rendering RBlade components in other templates
5
+ - Add support for dynamic components using `<x-dynamic component="...">`
6
+ - Improve performance of regular expressions
7
+ - Refactor public RBlade methods
8
+ - Remove deprecated "_required" option for props statement values (use "required" instead)
9
+ - Update README
10
+ - Fix issue with component tokenizer hanging
11
+
1
12
  ## 2.0.2 [2025-03-10]
2
13
  - Fix issue with empty component attributes
3
14
 
data/README.md CHANGED
@@ -54,12 +54,14 @@ For a quick overview of RBlade's capabilities, refer to the [reference file](REF
54
54
  - [Retrieving and Filtering Attributes](#retrieving-and-filtering-attributes)
55
55
  + [Slots](#slots)
56
56
  - [Slot Attributes](#slot-attributes)
57
+ + [Dynamic Components](#dynamic-components)
57
58
  + [Registering Additional Component Directories](#registering-additional-component-directories)
58
59
  + [Index Components](#index-components)
59
60
  * [Forms](#forms)
60
61
  + [Old Input](#old-input)
61
62
  + [Method Field](#method-field)
62
63
  * [Stacks](#stacks)
64
+ * [Integrating RBlade With Other Templates](#rblade-integration)
63
65
 
64
66
  <a name="displaying-data"></a>
65
67
  ## Displaying Data
@@ -140,7 +142,7 @@ If you are displaying JavaScript variables in a large portion of your template,
140
142
  <div class="container">
141
143
  Hello, {{ name }}.
142
144
  </div>
143
- @endverbatim
145
+ @endVerbatim
144
146
  ```
145
147
 
146
148
  <a name="rblade-directives"></a>
@@ -175,7 +177,7 @@ In addition to the conditional directives above, the `@blank?`, `defined?`, `@em
175
177
  // records is defined and is not nil
176
178
  @else
177
179
  // Since these directives are compiled to if statements, you can also use the @else directive
178
- @endempty?
180
+ @endEmpty?
179
181
  ```
180
182
 
181
183
  <a name="environment-directives"></a>
@@ -304,12 +306,12 @@ The `@class` directive conditionally adds CSS classes. The directive accepts a H
304
306
  hasError = true;
305
307
  @endRuby
306
308
 
307
- <span @class({
309
+ <span @class(
308
310
  "p-4": true,
309
311
  "font-bold": isActive,
310
312
  "text-gray-500": !isActive,
311
313
  "bg-red": hasError,
312
- })></span>
314
+ )></span>
313
315
 
314
316
  <span class="p-4 text-gray-500 bg-red"></span>
315
317
  ```
@@ -321,10 +323,10 @@ Likewise, the `@style` directive can be used to conditionally add inline CSS sty
321
323
  isActive = true;
322
324
  @endRuby
323
325
 
324
- <span @style({
326
+ <span @style(
325
327
  "background-color: red": true,
326
328
  "font-weight: bold" => isActive,
327
- })></span>
329
+ )></span>
328
330
 
329
331
  <span style="background-color: red; font-weight: bold;"></span>
330
332
  ```
@@ -432,9 +434,7 @@ In some situations, it's useful to embed Ruby code into your views. You can use
432
434
  <a name="custom-directives"></a>
433
435
  ### Custom Directives
434
436
 
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.
437
+ RBlade also allows you to define your own output directives using the `RBlade.register_directive_handler` method. When the compiler encounters the custom directive, it will call the provided block and output the returned value.
438
438
 
439
439
  ```rblade
440
440
  RBlade::register_directive_handler('sum') do |args|
@@ -446,6 +446,17 @@ end
446
446
  @sum(1, 2, 3) -> 6
447
447
  ```
448
448
 
449
+ The `RBlade.register_raw_directive_handler` method allows you to register a directive that outputs raw Ruby code into the template. When the compiler encounters the custom directive, it will call the provided block insert the returned value into the template. The returned ruby code must end in a semicolon, `;`.
450
+
451
+ ```rblade
452
+ RBlade::register_raw_directive_handler('not') do |args|
453
+ "if !(#{args[0]});"
454
+ end
455
+
456
+ @not(true) 1 @endNot -> ""
457
+ @not(false) 1 @endNot -> "1"
458
+ ```
459
+
449
460
  <a name="comments"></a>
450
461
  ### Comments
451
462
 
@@ -506,12 +517,26 @@ Namespaced components can be rendered using the namespace as a prefix to the nam
506
517
  <a name="passing-data-to-components"></a>
507
518
  ### Passing Data to Components
508
519
 
509
- You can pass data to RBlade components using HTML attributes. Hard-coded strings can be passed to the component using simple HTML attribute strings. Ruby expressions and variables should be passed to the component via attributes that use the `:` character as a prefix:
520
+ You can pass data to RBlade components using HTML-style attributes. Within these attributes, you can use `{{ ... }}` to insert Ruby code.
521
+
522
+ If the attribute name begins with a colon, `:`, the contents are parsed as Ruby.
510
523
 
511
524
  ```rblade
512
- <x-alert type="error" :message="message"/>
525
+ @ruby(what = 'Something')
526
+ @ruby(message = "#{what} happened!")
527
+
528
+ {{-- The message attribute will be identical in all of the following components --}}
529
+ <x-alert message="Something happened!"/>
530
+ <x-alert message="{{ what }} happened!"/>
531
+ <x-alert :message="message"/>
532
+ <x-alert :message/>
533
+ <x-alert message={{ message }}/>
534
+ <x-alert message="{{ message }}"/>
513
535
  ```
514
536
 
537
+ > [!NOTE]
538
+ > When using print blocks within an attribute, e.g. `{{ {hash: true} }}`, the contents will be converted to a string. If the print block is the entirety of the attribute, or the attribute name starts with a colon, the contents are passed through as-is.
539
+
515
540
  #### Component Properties
516
541
 
517
542
  You can define a component's data properties using a `@props` directive at the top of the component. You can then reference these properties using local variables within the template:
@@ -552,9 +577,13 @@ When passing attributes to components, you can also use a "short attribute" synt
552
577
  <a name="escaping-attribute-rendering"></a>
553
578
  #### Escaping Attribute Rendering
554
579
 
555
- Since some JavaScript frameworks such as Alpine.js also use colon-prefixed attributes, you can use a double colon (`::`) prefix to inform RBlade that the attribute is not a Ruby expression. For example, given the following component:
580
+ Since some JavaScript frameworks such as Alpine.js also use colon-prefixed attributes, you can use a double colon (`::`) prefix to inform RBlade that the attribute is not a Ruby expression. For example, given the following template:
556
581
 
557
582
  ```rblade
583
+ # The button component:
584
+ <button {{ attributes }}>{{ slot }}</button>
585
+
586
+ # Our template
558
587
  <x-button ::class="{ danger: isDeleting }">
559
588
  Submit
560
589
  </x-button>
@@ -591,7 +620,7 @@ All of the attributes that are not part of the component's constructor will auto
591
620
  Sometimes you can need to specify default values for attributes or merge additional values into some of the component's attributes. To accomplish this, you can use the attribute manager's `merge` method. This method is particularly useful for defining a set of default CSS classes that should always be applied to a component:
592
621
 
593
622
  ```rblade
594
- <div {{ attributes.merge({"class": "alert alert-#{type}"}) }}>
623
+ <div {{ attributes.merge("class": "alert alert-#{type}") }}>
595
624
  {{ message }}
596
625
  </div>
597
626
  ```
@@ -618,7 +647,7 @@ Both the `class` and `style` attributes are combined this way when using the `at
618
647
  When merging attributes that are not `class` or `style`, the values provided to the `merge` method will be considered the "default" values of the attribute. However, unlike the `class` and `style` attributes, these defaults will be overwritten if the attribute is defined in the component tag. For example:
619
648
 
620
649
  ```rblade
621
- <button {{ attributes.merge({type: "button"}) }}>
650
+ <button {{ attributes.merge(type: "button") }}>
622
651
  {{ slot }}
623
652
  </button>
624
653
  ```
@@ -645,7 +674,7 @@ The rendered HTML of the `button` component in this example would be:
645
674
  Sometimes you may wish to merge classes if a given condition is `true`. You can accomplish this via the `class` method, which accepts a Hash of classes where the array key contains the class or classes you wish to add, while the value is a boolean expression:
646
675
 
647
676
  ```rblade
648
- <div {{ attributes.class({'p-4': true, 'bg-red': hasError}) }}>
677
+ <div {{ attributes.class('p-4': true, 'bg-red': hasError) }}>
649
678
  {{ message }}
650
679
  </div>
651
680
  ```
@@ -653,7 +682,7 @@ Sometimes you may wish to merge classes if a given condition is `true`. You can
653
682
  If you need to merge other attributes onto your component, you can chain the `merge` method onto the `class` method:
654
683
 
655
684
  ```rblade
656
- <button {{ attributes.class({'bg-red': hasError}).merge({type: 'button'}) }}>
685
+ <button {{ attributes.class('bg-red': hasError).merge(type: 'button') }}>
657
686
  {{ slot }}
658
687
  </button>
659
688
  ```
@@ -809,6 +838,20 @@ Sometimes, you may wish to return early from a component without printing anythi
809
838
  ...
810
839
  ```
811
840
 
841
+ <a name="dynamic-components"></a>
842
+ ### Dynamic Components
843
+
844
+ Sometimes you may need to render a component but not know which component should be rendered until runtime. In this situation, you may use RBlades's built-in dynamic component to render the component based on a runtime value or variable:
845
+
846
+ ```rblade
847
+ <% component_name = 'button' %>
848
+
849
+ <x-dynamic :component="component_name"/>
850
+ ```
851
+
852
+ > [!NOTE]
853
+ > Dynamic components use the `component` helper class, and thus don't take advantage of RBlade's performance improvements over using partials
854
+
812
855
  <a name="registering-additional-component-directories"></a>
813
856
  ### Registering Additional Component Directories
814
857
 
@@ -928,3 +971,27 @@ If you would like to prepend content onto the beginning of a stack, you should u
928
971
  This will be first...
929
972
  @endPrepend
930
973
  ```
974
+
975
+
976
+ <a name="rblade-integration"></a>
977
+ ## Integrating RBlade With Other Templates
978
+
979
+ You might want to use RBlade components within other templates, e.g. if you are using a component library that uses them. By default, RBlade components cannot be rendered directly, but this can be enabled using the `direct_component_rendering` option.
980
+
981
+ ```ruby
982
+ # config/initializers/rblade.rb
983
+
984
+ # Enable partial rendering of RBlade components
985
+ RBlade.direct_component_rendering = true
986
+ ```
987
+
988
+ Once enabled, RBlade components can be rendered using `render partial: ...`. Block contents are passed to the component in the `slot` variable, `attributes` is initialized using `local_assigns`, and the `@props` directive will look for content set using `content_for`.
989
+
990
+ ```erb
991
+ <%= render template: "components/button", locals: {class: "mt-4", slot: capture do %>
992
+
993
+ <% end } %>
994
+ ```
995
+
996
+ > [!NOTE]
997
+ > RBlade's `{{ }}` print directives are automatically sent through Rails' `h` function to prevent XSS attacks.
data/REFERENCE.md CHANGED
@@ -24,12 +24,14 @@ By default, RBlade will look for components in the `app/views/components` folder
24
24
  | `<x-component.name>...</x-component.name>` | Render the component with the given slot content |
25
25
  | `<x-component.name>...<//>` | Short closing tag syntax (note: this bypasses some sanity checking during compilation) |
26
26
  | `<x-name attribute="STRING"/>` | Pass a string value to a component |
27
+ | `<x-name attribute="STRING{{ RUBY_EXPRESSION }}"/>` | Pass a string value to a component with an interpolated ruby value |
27
28
  | `<x-name :attribute="RUBY_EXPRESSION"/>` | Pass a ruby expression, executed in the local scope, to a component |
28
29
  | `<x-name :attribute/>` | Pass the `attribute` local variable into a component |
29
30
  | `<x-name @class({'bg-red-600': is_error})/>` | Conditionally pass classes to a component |
30
31
  | `<x-name @style({'bg-red-600': is_error})/>` | Conditionally pass styles to a component |
31
32
  | `<x-name attribute/>` | Pass an attribute to a component with value `true` |
32
33
  | `<x-name {{ attributes }}/>` | Pass attributes to a child component |
34
+ | `<x-dynamic component="name"/>` | Dynamically render a component based on the runetime value of the `component` attribute |
33
35
  | `@props(header: "Header")` | Remove `header` from the attributes Hash and introduce it as a local variable, using the specified value as a default |
34
36
  | `@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
37
  | `{{ slot }}` | Output the block content passed into the current component |
@@ -130,11 +132,11 @@ Note that the stack is printed once the current view or component finishes.
130
132
 
131
133
  ## Tips
132
134
 
133
- * Except for `@push`, `@prepend` and their variants, all end directives can simply be replaced with `@end` if preferred:
135
+ * All end directives can simply be replaced with `@end` if preferred:
134
136
  - `@nil?(...) ... @endnil?`
135
137
  - `@nil?(...) ... @endnil`
136
138
  - `@nil?(...) ... @end`
137
- * Except for `@ruby` and `@verbatim`, directives are case insensitive and can contain underscores. The following are identical:
139
+ * Directives are case insensitive and can contain underscores. The following are identical:
138
140
  - `@pushonce`
139
141
  - `@pushOnce`
140
142
  - `@PushOnce`
data/do CHANGED
@@ -57,15 +57,15 @@ fi
57
57
 
58
58
  if [ "$1" == "cs" ] || [ "$1" == "c" ]
59
59
  then
60
- echo Running: ${DOCKER_COMPOSE_COMMAND} run blade rake standard "${@:2}"
61
- ${DOCKER_COMPOSE_COMMAND} run blade rake standard "${@:2}"
60
+ echo Running: ${DOCKER_COMPOSE_COMMAND} run blade rubocop "${@:2}"
61
+ ${DOCKER_COMPOSE_COMMAND} run blade rubocop "${@:2}"
62
62
  exit 0
63
63
  fi
64
64
 
65
65
  if [ "$1" == "cs:fix" ]
66
66
  then
67
- echo Running: ${DOCKER_COMPOSE_COMMAND} run blade rake standard:fix "${@:2}"
68
- ${DOCKER_COMPOSE_COMMAND} run blade rake standard:fix "${@:2}"
67
+ echo Running: ${DOCKER_COMPOSE_COMMAND} run blade rubocop --autocorrect "${@:2}"
68
+ ${DOCKER_COMPOSE_COMMAND} run blade rubocop --autocorrect "${@:2}"
69
69
  exit 0
70
70
  fi
71
71
 
data/docker-compose.yml CHANGED
@@ -4,4 +4,7 @@ services:
4
4
  working_dir: /var/source
5
5
  volumes:
6
6
  - .:/var/source
7
- - ./storage/bundle_cache:/usr/local/bundle
7
+ - ruby-gems:/usr/local/bundle
8
+
9
+ volumes:
10
+ ruby-gems:
@@ -6,8 +6,8 @@ module RBlade
6
6
  tokens.each do |token|
7
7
  next if token.type != :unprocessed
8
8
 
9
- token.value.gsub!(/\{\{--.*?--\}\}/m, "")
10
- token.value.gsub!(/<%#.*?%>/m, "")
9
+ token.value.gsub!(/\{\{--(?:[^-]++|-)*?--}}/, "")
10
+ token.value.gsub!(/<%#(?:[^%]++|-)*?%>/, "")
11
11
  end
12
12
  end
13
13
  end
@@ -34,12 +34,14 @@ module RBlade
34
34
 
35
35
  private
36
36
 
37
- def compile_token_start token
37
+ def compile_token_start(token)
38
38
  component = {
39
- name: token.value[:name]
39
+ name: token.value[:name],
40
40
  }
41
41
  @component_stack << component
42
42
 
43
+ return compile_dynamic_component(token) if component[:name] == "dynamic"
44
+
43
45
  attributes = compile_attributes token.value[:attributes]
44
46
 
45
47
  if component[:name].start_with? "slot::"
@@ -49,7 +51,26 @@ module RBlade
49
51
  end
50
52
  end
51
53
 
52
- def compile_token_end token
54
+ def compile_dynamic_component(token)
55
+ component_index = token.value[:attributes].index { |item| item[:name] == "component" }
56
+ component = token.value[:attributes].delete_at(component_index)
57
+ component_value = case component[:type]
58
+ when "string"
59
+ process_string_attribute(component[:value])
60
+ when "ruby"
61
+ component[:value]
62
+ when "pass_through"
63
+ component[:name]
64
+ else
65
+ raise RBladeTemplateError.new "Component compiler: unexpected attribute type for component attribute (#{attribute[:type]})"
66
+ end
67
+
68
+ attributes = compile_attributes token.value[:attributes]
69
+
70
+ "@output_buffer.raw_buffer<<component(#{component_value}, '#{RBlade.escape_quotes(@component_store.current_view_name)}', #{attributes.join ","}) do;"
71
+ end
72
+
73
+ def compile_token_end(token)
53
74
  component = @component_stack.pop
54
75
  if component.nil?
55
76
  raise RBladeTemplateError.new "Unexpected closing tag </x-#{token.value[:name]}>"
@@ -62,7 +83,7 @@ module RBlade
62
83
  "end;"
63
84
  end
64
85
 
65
- def compile_attributes attributes
86
+ def compile_attributes(attributes)
66
87
  attributes.map do |attribute|
67
88
  case attribute[:type]
68
89
  when "class"
@@ -86,7 +107,7 @@ module RBlade
86
107
  end
87
108
 
88
109
  def process_string_attribute(string)
89
- result = string.split(/((?<!@)\{\{.*?\}\})/).map do |substring|
110
+ result = string.split(/((?<!@)\{\{(?:[^}]++|\})*?\}\})/).map do |substring|
90
111
  if substring.start_with?("{{") && substring.end_with?("}}")
91
112
  "(#{substring[2..-3]}).to_s"
92
113
  elsif !substring.empty?
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBlade
4
+ class CompilesInjections
5
+ def compile!(tokens)
6
+ tokens.map! do |token|
7
+ next(token) if token.type != :unprocessed
8
+
9
+ segments = token.value.split(/
10
+ (@?\{!!)(\s*+(?:[^!}%@]++|[!}%@])+?\s*+)(!!})
11
+ |
12
+ (@?\{\{)(\s*+(?:[^!}%@]++|[!}%@])+?\s*+)(}})
13
+ |
14
+ (\s?(?<!\w)@?@ruby)(\s++(?:[^!}%@]++|[!}%@])+?\s*+)((?<!\w)@end_?ruby(?!\w)\s?)
15
+ |
16
+ (<%%?=?=?)(\s*+(?:[^!}%@]++|[!}%@])+?\s*+)(%%?>)
17
+ /xi)
18
+
19
+ i = 0
20
+ while i < segments.count
21
+ case segments[i]
22
+ when "{{", "<%="
23
+ segments[i] = create_token(segments[i + 1].strip, true)
24
+ when "<%", /\A\s?@ruby\z/i
25
+ segments[i + 1].strip!
26
+ segments[i + 1] << ";" unless segments[i + 1].end_with?(";")
27
+ segments[i] = Token.new(type: :ruby, value: segments[i + 1])
28
+ when "{!!", "<%=="
29
+ segments[i] = create_token(segments[i + 1].strip, false)
30
+ when "@{!!", "@{{", /\A\s?@@ruby\z/i
31
+ segments[i].sub!("@", "")
32
+ segments[i] = Token.new(type: :raw_text, value: "#{segments[i]}#{segments[i + 1]}#{segments[i + 2]}")
33
+ when "<%%", "<%%=", "<%%=="
34
+ segments[i] = Token.new(type: :raw_text, value: "<#{segments[i].delete_prefix!("<%")}#{segments[i + 1]}%>")
35
+ when "", nil
36
+ segments.delete_at i
37
+ next
38
+ else
39
+ segments[i] = Token.new(type: :unprocessed, value: segments[i])
40
+ i += 1
41
+ next
42
+ end
43
+
44
+ segments.delete_at i + 1
45
+ segments.delete_at i + 1
46
+ i += 1
47
+ end
48
+
49
+ segments
50
+ end.flatten!
51
+ end
52
+
53
+ private
54
+
55
+ def create_token(expression, escape_html)
56
+ # Don't try to print ends
57
+ if expression.match?(/\A(?:}|end(?![[:alnum:]_]|[^\0-\177]))/i)
58
+ return Token.new(:ruby, "#{expression};")
59
+ end
60
+
61
+ segment_value = if escape_html
62
+ "@output_buffer.append=#{expression};"
63
+ # If this is a block, don't wrap in parentheses
64
+ elsif expression.match?(/
65
+ (?:\{|do)\s*+
66
+ (
67
+ \|\s*+
68
+ [a-zA-Z0-9_]++\s*+
69
+ (,\s*+[a-zA-Z0-9_]++)?\s*+
70
+ \|
71
+ )?
72
+ \z/x)
73
+ "@output_buffer.safe_expr_append=#{expression};"
74
+ else
75
+ "@output_buffer.raw_buffer<<(#{expression}).to_s;"
76
+ end
77
+
78
+ Token.new(:print, segment_value)
79
+ end
80
+ end
81
+ end