Class: Rhales::EarliestInjectionDetector
- Inherits:
-
Object
- Object
- Rhales::EarliestInjectionDetector
- Defined in:
- lib/rhales/hydration/earliest_injection_detector.rb
Overview
Detects the earliest safe injection points in HTML head and body sections for optimal hydration script placement performance
Injection Priority Order
For <head></head> section:
1. After the last <link> tag
2. After the last <meta> tag
3. After the first <script> tag (assuming early scripts are intentional)
4. Before the </head> tag
If no <head> but there is <body>:
- Before the <body> tag
All injection points are validated for safety using SafeInjectionValidator to prevent injection inside unsafe contexts (scripts, styles, comments).
Instance Method Summary collapse
-
#build_byte_to_char_map(str) ⇒ Hash<Integer, Integer>
private
Builds a mapping from byte positions to character positions for efficient conversion when processing UTF-8 strings with StringScanner.
-
#detect(template_html) ⇒ Object
-
#detect_body_injection_point(scanner, validator, template_html) ⇒ Object
private
-
#detect_head_injection_point(scanner, validator, template_html) ⇒ Object
private
-
#find_after_first_script(template_html, head_start, head_end) ⇒ Object
private
-
#find_after_last_link(template_html, head_start, head_end) ⇒ Object
private
-
#find_after_last_meta(template_html, head_start, head_end) ⇒ Object
private
-
#find_head_section(template_html) ⇒ Object
private
-
#find_safe_injection_position(validator, preferred_position) ⇒ Object
private
Instance Method Details
#build_byte_to_char_map(str) ⇒ Hash<Integer, Integer> (private)
Builds a mapping from byte positions to character positions for efficient conversion when processing UTF-8 strings with StringScanner.
This method creates a hash where keys are byte positions and values are the corresponding character positions. For multibyte UTF-8 characters, only the starting byte position has an entry in the map.
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/rhales/hydration/earliest_injection_detector.rb', line 191 def build_byte_to_char_map(str) map = {} char_pos = 0 byte_pos = 0 # Iterate through each character (not byte) in the string str.each_char do |char| # Map the starting byte position of this character map[byte_pos] = char_pos # Advance byte position by the byte size of this character byte_pos += char.bytesize char_pos += 1 end # Add final mapping for the end of the string map[byte_pos] = char_pos map end |
#detect(template_html) ⇒ Object
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/rhales/hydration/earliest_injection_detector.rb', line 26 def detect(template_html) scanner = StringScanner.new(template_html) validator = SafeInjectionValidator.new(template_html) # Build byte-to-char map once for the entire template @byte_to_char_map = build_byte_to_char_map(template_html) # Try head section injection points first head_injection_point = detect_head_injection_point(scanner, validator, template_html) return head_injection_point if head_injection_point # Fallback to body tag injection body_injection_point = detect_body_injection_point(scanner, validator, template_html) return body_injection_point if body_injection_point # No suitable injection point found nil end |
#detect_body_injection_point(scanner, validator, template_html) ⇒ Object (private)
68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/rhales/hydration/earliest_injection_detector.rb', line 68 def detect_body_injection_point(scanner, validator, template_html) scanner.pos = 0 # Find opening <body> tag if scanner.scan_until(/<body\b[^>]*>/i) # Convert byte position to character position using pre-built map byte_body_start = scanner.pos - scanner.matched.length body_start = @byte_to_char_map[byte_body_start] safe_position = find_safe_injection_position(validator, body_start) return safe_position if safe_position end nil end |
#detect_head_injection_point(scanner, validator, template_html) ⇒ Object (private)
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/rhales/hydration/earliest_injection_detector.rb', line 46 def detect_head_injection_point(scanner, validator, template_html) # Find head section bounds head_start, head_end = find_head_section(template_html) return nil unless head_start && head_end # Try injection points in priority order within head section injection_candidates = [ find_after_last_link(template_html, head_start, head_end), (template_html, head_start, head_end), find_after_first_script(template_html, head_start, head_end), head_end # Before </head> ].compact # Return first safe injection point injection_candidates.each do |position| safe_position = find_safe_injection_position(validator, position) return safe_position if safe_position end nil end |
#find_after_first_script(template_html, head_start, head_end) ⇒ Object (private)
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/rhales/hydration/earliest_injection_detector.rb', line 133 def find_after_first_script(template_html, head_start, head_end) head_content = template_html[head_start...head_end] scanner = StringScanner.new(head_content) byte_to_char_map = build_byte_to_char_map(head_content) # Find first script opening tag if scanner.scan_until(/<script\b[^>]*>/i) # Only the script end position is needed for this method, not the start position # Find corresponding closing tag if scanner.scan_until(/<\/script>/i) # scanner.pos is byte position within head_content byte_script_end = scanner.pos # Convert to character position using pre-built map first_script_end = byte_to_char_map[byte_script_end] return head_start + first_script_end end end nil end |
#find_after_last_link(template_html, head_start, head_end) ⇒ Object (private)
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/rhales/hydration/earliest_injection_detector.rb', line 101 def find_after_last_link(template_html, head_start, head_end) head_content = template_html[head_start...head_end] scanner = StringScanner.new(head_content) last_link_end = nil byte_to_char_map = build_byte_to_char_map(head_content) while scanner.scan_until(/<link\b[^>]*\/?>/i) # scanner.pos is byte position within head_content byte_pos = scanner.pos # Convert to character position using pre-built map last_link_end = byte_to_char_map[byte_pos] end last_link_end ? head_start + last_link_end : nil end |
#find_after_last_meta(template_html, head_start, head_end) ⇒ Object (private)
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/rhales/hydration/earliest_injection_detector.rb', line 117 def (template_html, head_start, head_end) head_content = template_html[head_start...head_end] scanner = StringScanner.new(head_content) = nil byte_to_char_map = build_byte_to_char_map(head_content) while scanner.scan_until(/<meta\b[^>]*\/?>/i) # scanner.pos is byte position within head_content byte_pos = scanner.pos # Convert to character position using pre-built map = byte_to_char_map[byte_pos] end ? head_start + : nil end |
#find_head_section(template_html) ⇒ Object (private)
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/rhales/hydration/earliest_injection_detector.rb', line 83 def find_head_section(template_html) scanner = StringScanner.new(template_html) # Find opening <head> tag return nil unless scanner.scan_until(/<head\b[^>]*>/i) # Convert byte position to character position using pre-built map byte_head_start = scanner.pos head_start = @byte_to_char_map[byte_head_start] # Find closing </head> tag return nil unless scanner.scan_until(/<\/head>/i) # Convert byte position to character position using pre-built map byte_head_end = scanner.pos - scanner.matched.length head_end = @byte_to_char_map[byte_head_end] [head_start, head_end] end |
#find_safe_injection_position(validator, preferred_position) ⇒ Object (private)
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/rhales/hydration/earliest_injection_detector.rb', line 155 def find_safe_injection_position(validator, preferred_position) return nil unless preferred_position # First check if the preferred position is safe return preferred_position if validator.safe_injection_point?(preferred_position) # Try to find a safe position before the preferred position safe_before = validator.nearest_safe_point_before(preferred_position) return safe_before if safe_before # As a last resort, try after the preferred position safe_after = validator.nearest_safe_point_after(preferred_position) return safe_after if safe_after # No safe position found nil end |