@@ -72,6 +72,35 @@ def initialize(template, options = {})
7272 @partials = { } # Registered partials: name => method_name
7373 @partial_sources = { } # Partial sources: name => source code
7474 @partial_counter = 0
75+ @external_tags = { } # External tags: var_name => tag object
76+ @external_tag_counter = 0
77+ @has_external_filters = false # Whether we need the filter helper
78+ end
79+
80+ # Mark that we have external filters
81+ def register_external_filter
82+ @has_external_filters = true
83+ end
84+
85+ # Check if external filters are used
86+ def has_external_filters?
87+ @has_external_filters
88+ end
89+
90+ # Register an external tag that will be called at runtime
91+ # @param tag [Liquid::Tag] The tag to register
92+ # @return [String] The variable name for this tag
93+ def register_external_tag ( tag )
94+ @external_tag_counter += 1
95+ var_name = "__ext_tag_#{ @external_tag_counter } __"
96+ @external_tags [ var_name ] = tag
97+ var_name
98+ end
99+
100+ # Get all registered external tags
101+ # @return [Hash] Map of variable names to tag objects
102+ def external_tags
103+ @external_tags
75104 end
76105
77106 # Get the file system for loading partials
@@ -158,6 +187,7 @@ def extract_raw_markup(node)
158187
159188 # Compile the template to a Ruby code string
160189 # @return [String] Ruby code that can be eval'd to create a render proc
190+ # @return [Hash] If external tags are used, returns { code: String, external_tags: Hash }
161191 def compile
162192 code = CodeGenerator . new
163193
@@ -169,8 +199,17 @@ def compile
169199 code . blank_line
170200 end
171201
172- # Generate the lambda header
173- code . line "->(assigns = {}) do"
202+ # First pass: compile the document body to discover partials and external tags
203+ main_code = CodeGenerator . new
204+ compile_node ( @template . root , main_code )
205+
206+ # Determine lambda parameters based on external dependencies
207+ params = [ "assigns = {}" ]
208+ params << "__external_tags__ = {}" unless @external_tags . empty?
209+ params << "__filter_handler__ = nil" if @has_external_filters
210+
211+ code . line "->(#{ params . join ( ', ' ) } ) do"
212+
174213 code . indent do
175214 # Initialize the output buffer
176215 code . line '__output__ = +""'
@@ -182,9 +221,17 @@ def compile
182221 code . blank_line
183222 end
184223
185- # First pass: compile the document body to discover partials
186- main_code = CodeGenerator . new
187- compile_node ( @template . root , main_code )
224+ # Add external tag runtime helper if needed
225+ unless @external_tags . empty?
226+ compile_external_tag_helper ( code )
227+ code . blank_line
228+ end
229+
230+ # Add external filter helper if needed
231+ if @has_external_filters
232+ compile_filter_helper ( code )
233+ code . blank_line
234+ end
188235
189236 # Compile partial methods (before main body so they're available)
190237 compile_partials ( code )
@@ -200,6 +247,41 @@ def compile
200247 code . to_s
201248 end
202249
250+ # Compile helper for calling external tags at runtime
251+ def compile_external_tag_helper ( code )
252+ code . line "# Helper for calling external (unknown) tags at runtime"
253+ code . line "__call_external_tag__ = ->(tag_var, tag_assigns) {"
254+ code . indent do
255+ code . line "tag = __external_tags__[tag_var]"
256+ code . line "next '' unless tag"
257+ code . line "# Create a context using the default environment (which has filters registered)"
258+ code . line "ctx = Liquid::Context.new([tag_assigns], {}, {}, false, nil, {}, Liquid::Environment.default)"
259+ code . line "output = +''"
260+ code . line "# Use render_to_output_buffer to ensure block tags work correctly"
261+ code . line "tag.render_to_output_buffer(ctx, output)"
262+ code . line "output"
263+ end
264+ code . line "}"
265+ end
266+
267+ # Compile helper for calling external filters at runtime
268+ def compile_filter_helper ( code )
269+ code . line "# Helper for calling external (unknown) filters at runtime"
270+ code . line "__call_filter__ = ->(name, input, args) {"
271+ code . indent do
272+ code . line "if __filter_handler__&.respond_to?(name)"
273+ code . indent do
274+ code . line "__filter_handler__.send(name, input, *args)"
275+ end
276+ code . line "else"
277+ code . indent do
278+ code . line "input # Return input unchanged if filter not found"
279+ end
280+ code . line "end"
281+ end
282+ code . line "}"
283+ end
284+
203285 # Compile all registered partials as inner methods
204286 def compile_partials ( code )
205287 @partials . each do |name , method_name |
@@ -293,16 +375,27 @@ def compile_tag(tag, code)
293375 if compiler_class
294376 compiler_class . compile ( tag , self , code )
295377 else
296- raise CompileError , "No compiler for tag: #{ tag . class } "
378+ # Unknown tag - delegate to the original tag's render method at runtime
379+ compile_external_tag ( tag , code )
297380 end
298381 end
299382
383+ def compile_external_tag ( tag , code )
384+ tag_var = register_external_tag ( tag )
385+ tag_name = tag . class . name . split ( '::' ) . last
386+ if debug?
387+ code . line "# External tag: #{ tag_name } (delegated to runtime)"
388+ code . line "$stderr.puts '* WARN: Liquid external tag call - #{ tag_name } (not compiled, delegated to runtime)' if $VERBOSE"
389+ end
390+ code . line "__output__ << __call_external_tag__.call(#{ tag_var . inspect } , assigns)"
391+ end
392+
300393 def find_tag_compiler ( tag )
301394 case tag
395+ when Liquid ::Unless # Check Unless before If since Unless < If
396+ Tags ::UnlessCompiler
302397 when Liquid ::If
303398 Tags ::IfCompiler
304- when Liquid ::Unless
305- Tags ::UnlessCompiler
306399 when Liquid ::Case
307400 Tags ::CaseCompiler
308401 when Liquid ::For
0 commit comments